|
|
@ -1,15 +1,15 @@ |
|
|
package echomeet |
|
|
package echomeet |
|
|
|
|
|
|
|
|
import ( |
|
|
import ( |
|
|
|
|
|
"fmt" |
|
|
|
|
|
"path/filepath" |
|
|
|
|
|
"runtime" |
|
|
"yunyan/comm" |
|
|
"yunyan/comm" |
|
|
"yunyan/lego/core" |
|
|
"yunyan/lego/core" |
|
|
"yunyan/lego/core/cbase" |
|
|
"yunyan/lego/core/cbase" |
|
|
"yunyan/lego/sys/mysql" |
|
|
"yunyan/lego/sys/mysql" |
|
|
"yunyan/lego/sys/postgres" |
|
|
"yunyan/lego/sys/postgres" |
|
|
"yunyan/pb" |
|
|
"yunyan/pb" |
|
|
"fmt" |
|
|
|
|
|
"path/filepath" |
|
|
|
|
|
"runtime" |
|
|
|
|
|
|
|
|
|
|
|
"gorm.io/gorm/clause" |
|
|
"gorm.io/gorm/clause" |
|
|
) |
|
|
) |
|
|
@ -17,6 +17,7 @@ import ( |
|
|
// templatePrivateIdStart 模板 id 分段阈值(唯一的归属判定依据):
|
|
|
// templatePrivateIdStart 模板 id 分段阈值(唯一的归属判定依据):
|
|
|
// - id < 此值 → 公共模板,存公共数据库(Postgres),可有多语言版本;
|
|
|
// - id < 此值 → 公共模板,存公共数据库(Postgres),可有多语言版本;
|
|
|
// - id >= 此值 → 用户私有模板,存业务服务数据库(MySQL),单条、无多语言版本。
|
|
|
// - id >= 此值 → 用户私有模板,存业务服务数据库(MySQL),单条、无多语言版本。
|
|
|
|
|
|
//
|
|
|
// 由建表时对私有模板表设置自增起点保证两段 id 不重叠(见 modelComp.Init)。
|
|
|
// 由建表时对私有模板表设置自增起点保证两段 id 不重叠(见 modelComp.Init)。
|
|
|
const templatePrivateIdStart = 10000 |
|
|
const templatePrivateIdStart = 10000 |
|
|
|
|
|
|
|
|
@ -35,10 +36,56 @@ func (this *modelComp) Init(service core.IService, module core.IModule, comp cor |
|
|
// 从而 gettemplateforid 能仅凭 id 把请求路由到正确的库(仅空表生效)。
|
|
|
// 从而 gettemplateforid 能仅凭 id 把请求路由到正确的库(仅空表生效)。
|
|
|
mysql.AutoIncrementStart(comm.TableEchomeetTemplate, "id", templatePrivateIdStart) |
|
|
mysql.AutoIncrementStart(comm.TableEchomeetTemplate, "id", templatePrivateIdStart) |
|
|
} |
|
|
} |
|
|
err = mysql.CreateTable(comm.TableEchomeetRecord, &pb.DBEchoMeetRecord{}) |
|
|
if err = mysql.CreateTable(comm.TableEchomeetRecord, &pb.DBEchoMeetRecord{}); err != nil { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
this.ensureRecordFulltextIndex() |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 会议纪要全文检索索引的名字与列。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 列的顺序和个数**必须与查询里的 MATCH(...) 完全一致**,MySQL 才会用上这个索引;
|
|
|
|
|
|
// 少一列多一列都会报 "Can't find FULLTEXT index matching the column list"。
|
|
|
|
|
|
// 改这里就要同步改 modules/mcp/tool_meeting.go 的 fulltextColumns。
|
|
|
|
|
|
const ( |
|
|
|
|
|
recordFulltextIndex = "ft_echomeet_record_content" |
|
|
|
|
|
recordFulltextColumns = "title,summary,overview,remark" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// ensureRecordFulltextIndex 给会议记录建 ngram 全文索引(幂等,失败不阻断启动)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 为什么要它:MCP 的 search_meeting_notes 原来用 `LIKE %kw%` 逐个关键词 AND,
|
|
|
|
|
|
// 中文场景下召回极差——用户问「关于降本增效的会议」,纪要里写的是「成本优化」就一条都匹配不到,
|
|
|
|
|
|
// 而且每个关键词都是全表扫描。换成全文索引后按相关度排序,多关键词是 OR + 打分,
|
|
|
|
|
|
// 召回和排序都合理得多。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 用 ngram 解析器,因为默认解析器按空格切词,中文整段会被当成一个巨型 token,等于没索引。
|
|
|
|
|
|
// 服务器上 `ngram_token_size=2`,所以**单字关键词检索不到**(工具那边对单字回退用 LIKE)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 失败只记日志:索引建不出来(权限不足、旧版 MySQL 不支持 ngram)时,
|
|
|
|
|
|
// 检索侧会自动退回 LIKE,功能降级但不能因此让 home 服务起不来。
|
|
|
|
|
|
func (this *modelComp) ensureRecordFulltextIndex() { |
|
|
|
|
|
var n int64 |
|
|
|
|
|
if err := mysql.Table("information_schema.STATISTICS"). |
|
|
|
|
|
Where("TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?", |
|
|
|
|
|
comm.TableEchomeetRecord, recordFulltextIndex). |
|
|
|
|
|
Count(&n).Error; err != nil { |
|
|
|
|
|
this.module.Warnf("检查会议全文索引失败,跳过建索引: %v", err) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if n > 0 { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
sql := fmt.Sprintf("CREATE FULLTEXT INDEX %s ON `%s` (%s) WITH PARSER ngram", |
|
|
|
|
|
recordFulltextIndex, comm.TableEchomeetRecord, recordFulltextColumns) |
|
|
|
|
|
if err := mysql.Exec(sql).Error; err != nil { |
|
|
|
|
|
this.module.Warnf("创建会议全文索引失败(检索将退回 LIKE): %v", err) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
this.module.Infof("会议全文索引已创建: %s(%s)", recordFulltextIndex, recordFulltextColumns) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func (this *modelComp) getuser(uid string) (user *pb.DBUser, err error) { |
|
|
func (this *modelComp) getuser(uid string) (user *pb.DBUser, err error) { |
|
|
user = &pb.DBUser{} |
|
|
user = &pb.DBUser{} |
|
|
err = mysql.FindOne(comm.TableUser, user, "uid=?", uid) |
|
|
err = mysql.FindOne(comm.TableUser, user, "uid=?", uid) |
|
|
@ -109,6 +156,7 @@ func (this *modelComp) gettemplates(uid string, language string) (models []*pb.D |
|
|
gettemplateforid 按主键读取模板,仅凭 id 段判定归属并路由到对应库: |
|
|
gettemplateforid 按主键读取模板,仅凭 id 段判定归属并路由到对应库: |
|
|
- id < templatePrivateIdStart:公共模板 → 公共数据库(Postgres) |
|
|
- id < templatePrivateIdStart:公共模板 → 公共数据库(Postgres) |
|
|
- id >= templatePrivateIdStart:用户私有模板 → 业务服务数据库(MySQL) |
|
|
- id >= templatePrivateIdStart:用户私有模板 → 业务服务数据库(MySQL) |
|
|
|
|
|
|
|
|
私有模板单条、无多语言版本,按主键直接取即可。 |
|
|
私有模板单条、无多语言版本,按主键直接取即可。 |
|
|
*/ |
|
|
*/ |
|
|
func (this *modelComp) gettemplateforid(id uint64) (template *pb.DBEchoMeetTemplate, err error) { |
|
|
func (this *modelComp) gettemplateforid(id uint64) (template *pb.DBEchoMeetTemplate, err error) { |
|
|
@ -194,6 +242,39 @@ func (this *modelComp) getrecordByTaskId(taskId string) (model *pb.DBEchoMeetRec |
|
|
err = mysql.FindOne(comm.TableEchomeetRecord, model, "taskid=?", taskId) |
|
|
err = mysql.FindOne(comm.TableEchomeetRecord, model, "taskid=?", taskId) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// getstucktranscribing 捞一批「卡在转写中」的记录:已经拿到第三方 taskid,
|
|
|
|
|
|
// 但距上次主动查询超过阈值。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ lastquerytime=0 也要算进来——那表示**从来没有人查过**(客户端一次都没拉过,
|
|
|
|
|
|
// 比如它的轮询循环崩了)。只按 `lastquerytime<?` 过滤会把这批最需要兜底的漏掉。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ NULL 也要单独判。SQL 里 `NULL=0` 和 `NULL<x` 都不成立(结果是 NULL 不是
|
|
|
|
|
|
// true),漏掉 `IS NULL` 的话这类行对兜底**永远不可见**,卡死了也捞不出来。
|
|
|
|
|
|
// gorm 写入的新记录是 0 不是 NULL,但历史行/手工插入的不保证。
|
|
|
|
|
|
func (this *modelComp) getstucktranscribing(before int64, limit int) (models []*pb.DBEchoMeetRecord, err error) { |
|
|
|
|
|
models = make([]*pb.DBEchoMeetRecord, 0) |
|
|
|
|
|
err = mysql.Table(comm.TableEchomeetRecord). |
|
|
|
|
|
Where("state=? AND taskid<>'' AND (lastquerytime IS NULL OR lastquerytime=0 OR lastquerytime<?)", |
|
|
|
|
|
int32(pb.DBEchoMeetRecordState_Transcribing), before). |
|
|
|
|
|
Order("id").Limit(limit).Find(&models).Error |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// getstucksummarizing 捞一批停在「待总结/总结中」超过阈值的记录。
|
|
|
|
|
|
// 锚点用 starttime(StartTask 时写入),转写完成后它不会再变,正好当「这单开始多久了」。
|
|
|
|
|
|
func (this *modelComp) getstucksummarizing(before int64, limit int) (models []*pb.DBEchoMeetRecord, err error) { |
|
|
|
|
|
models = make([]*pb.DBEchoMeetRecord, 0) |
|
|
|
|
|
err = mysql.Table(comm.TableEchomeetRecord). |
|
|
|
|
|
Where("state IN ? AND starttime>0 AND starttime<?", |
|
|
|
|
|
[]int32{ |
|
|
|
|
|
int32(pb.DBEchoMeetRecordState_AwaitSummarizing), |
|
|
|
|
|
int32(pb.DBEchoMeetRecordState_Summarizing), |
|
|
|
|
|
}, before). |
|
|
|
|
|
Order("id").Limit(limit).Find(&models).Error |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func (this *modelComp) getrecords(ids []uint64) (models []*pb.DBEchoMeetRecord, err error) { |
|
|
func (this *modelComp) getrecords(ids []uint64) (models []*pb.DBEchoMeetRecord, err error) { |
|
|
models = make([]*pb.DBEchoMeetRecord, 0) |
|
|
models = make([]*pb.DBEchoMeetRecord, 0) |
|
|
err = mysql.Find(comm.TableEchomeetRecord, &models, "id IN ?", ids) |
|
|
err = mysql.Find(comm.TableEchomeetRecord, &models, "id IN ?", ids) |
|
|
|