From 5193b0e5e866e36845876f8c65ad9f71ad1b871d Mon Sep 17 00:00:00 2001 From: Rodger-Wang <1367893453@qq.com> Date: Fri, 18 Sep 2026 22:51:02 +0800 Subject: [PATCH] =?UTF-8?q?admin,console:=20=E3=80=8C=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E3=80=8D=E6=94=B9=E6=88=90=E3=80=8C=E4=BC=9A?= =?UTF-8?q?=E8=AE=AE=E9=85=8D=E7=BD=AE=E3=80=8D=EF=BC=8C=E4=B8=80=E9=A1=B5?= =?UTF-8?q?=E7=AE=A1=E8=BD=AC=E5=86=99/=E6=80=BB=E7=BB=93/=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后台恢复了会议转写段与总结段的配置入口——它们随「会议记录服务」页一起下线后, 只能直接改库(echomeet_orch),而那正是语音纪要整条流水线的服务选型来源。 - 路由 /meettemplates → /meetconfig,旧地址留重定向;**菜单 id 不变**: 存量后台账号的 access 清单里存的是这个串,改 id 等于让这些账号丢入口; - 一页全展开不分标签页,顶部一个应用选择器三块共用——各拉各的会出现 「转写配的是 A、模板改的是 B」这种对不上的状态; - 保存是声明式的,asr/llm 都是**指针**:缺哪个不动哪段。写成值类型的话 「保存会议总结」会带一个空的 asr 数组把转写编排整段清空; - 翻译段(kind=2) 本页不编辑但保存时必须原样保留,跟着 reconcile 会让纪要 能转写、译文整体消失; - 服务池查询是 app_name=? 不带 OR app_name='':作用域归一后没有全局默认层了, 照旧版写回退查询会让一个应用读到另一个应用的服务选型,且不报任何错。 段→服务类别的映射由 TestEchoKindSvcCats 钉住:类别编号是 comm / console / admin 三处同写的,漏改一处的表现是某一段的下拉空空如也且不报错。 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 41 +- .../app/components/MeetTemplatePanel.vue | 64 +-- apps/admin/app/pages/meetconfig.vue | 383 ++++++++++++++++++ apps/admin/app/pages/meettemplates.vue | 11 + apps/admin/app/utils/menus.ts | 9 +- .../modules/console/api_echomeet_orch.go | 274 +++++++++++++ .../modules/console/echomeet_orch_test.go | 33 ++ apps/services/modules/console/server.go | 9 + 8 files changed, 775 insertions(+), 49 deletions(-) create mode 100644 apps/admin/app/pages/meetconfig.vue create mode 100644 apps/admin/app/pages/meettemplates.vue create mode 100644 apps/services/modules/console/api_echomeet_orch.go create mode 100644 apps/services/modules/console/echomeet_orch_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 8a02b303..d3af5b4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1515,7 +1515,7 @@ apps/products/regions/channelid 四维,而它们这四项**本来就是空的* | 应用环境配置 | 先并入「服务与环境配置 → 应用参数」标签页;**2026-09-14 连标签页也下线**(见下文) | 凭据搬走后只剩应用自己的业务参数,而这些也搬进了 svc_config | | 第三方服务配置 | 改名「服务与环境配置」,成为凭据唯一出处;2026-09-14 菜单改回「第三方服务配置」 | — | | Agent 配置 / 通话翻译配置 | **删除**(页面 + console 接口) | `agent_config`、`call_translate_rule` 在测试机与正式机上**都是 0 行**;通话翻译选路全在端侧 `language_manager.findBestMatchingProvider` | -| 会议记录服务 | **删除页面**,⚠️ 数据与运行时保留 | 见下方警告 | +| 会议记录服务 | **删除页面**,⚠️ 数据与运行时保留;2026-09-17 以「会议配置」的形式回来(见下文) | 见下方警告 | ⚠️ **两张扁平 env 表的坑(这是本次改造的起因)**:同一个 `AZURE_SPEECH_REGION` / `OPENAI_API_KEY` / `VOLC_OPENSPEECH_APP_ID` / `XUNFEI_ASR_*` @@ -1656,12 +1656,49 @@ console 启动跑、幂等、排在 `migrateGlobalScopeToApps` 之后): ⚠️ **「会议记录服务」删的只是后台入口,`echomeet_orch` 的数据与运行时都还在** (两台机各 4 行:`asrfile_alibaba`/`asrfile_azure` 识别、`mt_alibaba` 翻译、`llmv_qwen` 总结), 它是语音纪要整条流水线的服务选型来源,见 -[orch_resolver.go](apps/services/modules/echomeet/orch_resolver.go)。**要换服务商现在只能直接改库。** +[orch_resolver.go](apps/services/modules/echomeet/orch_resolver.go)。 +**转写段与总结段的后台入口已于 2026-09-17 在「会议配置」页恢复(见下节);翻译段仍只能改库。** ⚠️ 存量后台账号 access CSV 里的 `globalconfig`/`appconfig`/`agents`/`meetingsvc`/`calltranslate` 不再对应任何菜单项,权限编辑器里也勾不到了。只勾过 `appconfig` 的账号由 [auth.ts](apps/admin/app/stores/auth.ts) 的 `hasAccess` 兼容到 `svcconfig`,不会因为一次合并丢入口。 +## 「会议模板」改成「会议配置」:一页管转写/总结/模板(2026-09-17) + +后台菜单项 `meettemplate` 的**标签与路由**变了(`/meettemplates` → `/meetconfig`,旧地址留重定向), +**菜单 id 没变**——存量后台账号的 access 清单里存的是这个串,改 id 等于让这些账号丢入口。 +**一页全展开,不分标签页**:上半区两栏(左「会议转写」、右「会议总结」,1100px 以下叠成一列), +下半区「会议模板」占满剩余空间、内容多了整页往下滚(**刻意不给模板区单独开滚动条**—— +模板卡片网格本来就高,嵌套滚动会出现两条滚动条、还会把筛选条挤在半空)。 +顶部**一个**应用选择器三块共用(都按应用隔离,各拉各的会出现「转写配的是 A、模板改的是 B」 +这种对不上的状态): + +| 区块 | 配什么 | 存哪 | +|---|---|---| +| 会议转写 | 有序选若干个「录音文件识别」(类别 8) 服务 | `echomeet_orch` kind=1,顺序 = `priority` 升序 | +| 会议总结 | 单选一个「文本/多媒体大模型」(类别 5/9) 服务 | `echomeet_orch` kind=3,选中的那条 `is_default=true` | +| 会议模板 | 原「会议模板」页原样搬来 | `echomeet_template`,`source='public'` | + +模板页本体已拆成组件 [MeetTemplatePanel.vue](apps/admin/app/components/MeetTemplatePanel.vue)(应用名由父页面以 prop 下发), +页面壳是 [meetconfig.vue](apps/admin/app/pages/meetconfig.vue),后端是重新写的 +[api_echomeet_orch.go](apps/services/modules/console/api_echomeet_orch.go)(`api_getechoorch` / `api_saveechoorch`)。 + +- **「选一个就用一个、选两个第二个兜底」是运行时本来就有的行为**,这一页只是把它的入口补回来: + `PickASR` 按 priority 升序取第一个「已启用 + 支持源语言」的,提交失败由 `NextASR` 顺延下一家 + (2026-09-15 线上 `asrfile_alibaba` 的 key 失效、排后面的 `asrfile_azure` 完好却一次没被碰, + 就是因为当时还没有 `NextASR`)。**总结段没有兜底**:`PickLLM` 只认 `is_default`,选中的不可用就直接失败。 +- ⚠️ **保存是声明式的,两段各自独立**:请求里 `asr` / `llm` 都是**指针**,缺哪个就不动哪段。 + 写成值类型的话「保存会议总结」会带一个空的 `asr` 数组过来,把转写编排整段清空。 +- ⚠️ **翻译段 (kind=2) 本页不编辑,保存时必须原样保留**:它同样是语音纪要流水线的一环, + 跟着一起 reconcile 会让纪要能转写、**译文整体消失**。 +- ⚠️ **服务池查询是 `app_name=?`,不带 `OR app_name=''`**:2026-09-14 作用域归一后没有全局默认层了, + 照旧版写回退查询会让一个应用读到另一个应用(或迁移前残留的全局行)的服务选型,且不报任何错。 +- 本页只编辑 `region=0`(全区域)那一层。区域分叉(`region>0`)优先级更高、仍由运行时消费, + 接口把它们的行数回报给前端并在页面上明说——后台显示的若与线上跑的不是一回事,排查会被带偏。 +- 段 → 服务类别的映射在 `echoKindSvcCats`(转写 8、翻译 3、总结 5+9), + `TestEchoKindSvcCats` 钉着它:类别编号是三处同写的(comm / console / admin 前端 `SCAT`), + 漏改一处的表现是**某一段的下拉空空如也且不报任何错**,很容易去怀疑数据。 + ## 第三方服务配置:服务端专用类别(2026-08-28 新增) `svc_config`(后台「服务与环境配置 → 第三方服务」)默认是**面向客户端**的:[svcresolve.go](apps/services/modules/user/svcresolve.go) diff --git a/apps/admin/app/components/MeetTemplatePanel.vue b/apps/admin/app/components/MeetTemplatePanel.vue index 5b7a9d1c..4125f39a 100644 --- a/apps/admin/app/components/MeetTemplatePanel.vue +++ b/apps/admin/app/components/MeetTemplatePanel.vue @@ -3,21 +3,11 @@
-
-

会议模板

-

- 会议纪要公共模板(source=public),按应用各一套。同一 tid 下每种语言一份,卡面展示 - {{ PRIMARY_LANG }} 版本。修改后各服务约 10 分钟内自动同步。 -

-
-
- 应用 - - -
+

+ 会议纪要公共模板(source=public),按应用各一套。同一 tid 下每种语言一份,卡面展示 + {{ PRIMARY_LANG }} 版本。修改后各服务约 10 分钟内自动同步。 +

+
@@ -274,13 +264,11 @@ diff --git a/apps/admin/app/pages/meettemplates.vue b/apps/admin/app/pages/meettemplates.vue new file mode 100644 index 00000000..76dd2b04 --- /dev/null +++ b/apps/admin/app/pages/meettemplates.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/admin/app/utils/menus.ts b/apps/admin/app/utils/menus.ts index ce7dee24..540515ac 100644 --- a/apps/admin/app/utils/menus.ts +++ b/apps/admin/app/utils/menus.ts @@ -70,13 +70,14 @@ export const MENU_GROUPS: MenuGroup[] = [ // · Agent 配置(agents) / 通话翻译配置(calltranslate) —— agent_config、call_translate_rule // 两张表在测试机与正式机上**都是 0 行**,客户端的通话翻译选路全在端侧 // (language_manager.findBestMatchingProvider),后台这两页从未产生过数据。 - // · 会议记录服务(meetingsvc) —— 按要求下架。⚠️ 它背后的 echomeet_orch 仍有 4 行在跑 - // (语音纪要的识别/翻译/总结选型),**数据与运行时保留**,只是后台不再提供编辑入口; - // 要换服务商得直接改库(见 services/modules/echomeet/orch_resolver.go)。 + // · 会议记录服务(meetingsvc) —— 2026-09-12 下架,2026-09-17 以「会议配置」的形式回来: + // 转写/总结两段的服务选型并进了会议模板那一页(翻译段仍只能改库)。 { id: 'svcconfig', label: '第三方服务配置', to: '/serviceconfig', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' }, { id: 'appgoods', label: '应用商品配置', to: '/appgoods', icon: 'M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z' }, { id: 'compute', label: '会员与算力', to: '/compute', icon: 'M13 10V3L4 14h7v7l9-11h-7z' }, - { id: 'meettemplate', label: '会议模板', to: '/meettemplates', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' }, + // 2026-09-17「会议模板」→「会议配置」:同一页里管会议转写(录音文件识别)、会议总结(大模型)、会议模板三块。 + // ⚠️ 菜单 id 仍是 meettemplate:存量后台账号的 access 清单里存的是这个串,改 id 等于让这些账号丢入口。 + { id: 'meettemplate', label: '会议配置', to: '/meetconfig', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' }, // 版本控制 / 游客显隐控制:按应用配、不分渠道(存 app_release 表)。 // 这两组开关原先埋在「SaaS 管理 → 渠道分发」里按渠道各存一份,同一个开关有 8 处能改, // 客户端只读一处,出过「后台开了游客登录但 App 里没有」的事故,故拆到这里单独管。 diff --git a/apps/services/modules/console/api_echomeet_orch.go b/apps/services/modules/console/api_echomeet_orch.go new file mode 100644 index 00000000..4d2e501e --- /dev/null +++ b/apps/services/modules/console/api_echomeet_orch.go @@ -0,0 +1,274 @@ +package console + +import ( + "sort" + "strconv" + "strings" + "time" + + "yunyan/comm" + "yunyan/lego/sys/postgres" + "yunyan/pb" + + "github.com/gin-gonic/gin" +) + +// ============================ 会议配置 · 服务编排 (echomeet orch) ============================ +// +// admin「会议配置」页的后端(2026-09-17 恢复;上一版「会议记录服务」页于 2026-09-12 随菜单收敛 +// 一起删掉了后台入口,但 echomeet_orch 的数据与运行时一直在跑,换服务商只能直接改库)。 +// +// 编排**不存凭据**,只引用「第三方服务配置」(svc_config) 里配好的服务 id: +// · 会议转写 kind=1 → 服务类别「录音文件识别」(svcCatASRFile) +// · 会议总结 kind=3 → 服务类别「文本大模型」(svcCatLLM) / 「多媒体大模型」(svcCatLLMVision) +// 运行时选路见 modules/echomeet/providers.go 的 PickASR/NextASR/PickLLM: +// 转写按 priority 升序取第一个「已启用 + 支持源语言」的,提交失败再顺延下一个,所以 +// **配两家 = 第二家兜底**;总结取 is_default,没有则按 priority 第一个。 +// +// ⚠️ 作用域是 app_name,**没有「全局默认」层了**(2026-09-14 作用域归一,见 migrate_scope.go): +// 每个应用一套、互不干涉,app_name 空一律拒绝。别再照旧版写 `OR app_name=''` 的回退查询—— +// 那会让一个应用读到另一个应用(或迁移前残留的全局行)的服务选型,且不报任何错。 +// +// 本页只编辑 region=0(全区域)那一层。区域分叉(region>0)仍由运行时的 scopeRank 消费, +// 页面不编辑但会把行数回报给前端,免得后台显示的与线上跑的不是一回事。 + +// 本页编辑的区域层:0 = 全区域。区域分叉留给运行时,后台不再提供按区域编排的入口。 +const echoOrchEditRegion int32 = 0 + +// echoOrchScopeOK app_name 必填 + 账号权限(同 svcScopeOK / meetTplScopeOK 的规矩)。 +func (this *serverComp) echoOrchScopeOK(c *gin.Context, appName string) bool { + if appName == "" { + writeErr(c, pb.ErrorCode_ReqParameterError, "app_name 必填:会议配置按应用隔离,请先选择应用") + return false + } + return this.requireAppNameScope(c, appName) +} + +// echoOrchSvcView 供前端选服务/显示名的精简服务视图(**不含凭据**)。 +type echoOrchSvcView struct { + Id string `json:"id"` + Name string `json:"name"` + Provider string `json:"provider"` + Categories string `json:"categories"` // 逗号分隔类别,前端按 kind_cats 筛 + Languages string `json:"languages"` // 支持语言(逗号分隔 BCP-47),空=不限;转写选路按它跳过不支持的 +} + +// echoOrchAvailableSvcs 该应用作用域下启用中的服务(凭据一律不出这个函数)。 +func (this *serverComp) echoOrchAvailableSvcs(appName string) []echoOrchSvcView { + rows := make([]*ThirdSvcConfig, 0) + if err := postgres.Find(comm.TableSvcConfig, &rows, "app_name=? AND enable=true", appName); err != nil { + return nil + } + out := make([]echoOrchSvcView, 0, len(rows)) + for _, r := range rows { + langs := "" + for _, f := range r.Fields { + if f.Key == "languages" { + langs = f.DefValue + break + } + } + out = append(out, echoOrchSvcView{ + Id: r.Id, Name: r.Name, Provider: r.Provider, Categories: r.Categories, Languages: langs, + }) + } + sort.SliceStable(out, func(i, j int) bool { return out[i].Id < out[j].Id }) + return out +} + +// echoOrchSvcAllowed 校验 svc_id 在该应用下可见、已启用,且类别与 kind 匹配。 +func (this *serverComp) echoOrchSvcAllowed(appName, svcId string, kind int32) (bool, string) { + wantCats := echoKindSvcCats[kind] + if len(wantCats) == 0 { + return false, "未知的编排类别" + } + rows := make([]*ThirdSvcConfig, 0) + if err := postgres.Find(comm.TableSvcConfig, &rows, "app_name=? AND id=?", appName, svcId); err != nil { + return false, err.Error() + } + for _, r := range rows { + if !r.Enable { + continue + } + for _, cat := range strings.Split(r.Categories, ",") { + for _, want := range wantCats { + if strings.TrimSpace(cat) == strconv.Itoa(int(want)) { + return true, "" + } + } + } + } + return false, "所选服务「" + svcId + "」不存在于该应用、已停用,或类别与该段不匹配" +} + +// getEchoOrch 列出某应用的会议服务编排 + 可选服务池。 +func (this *serverComp) getEchoOrch(c *gin.Context) { + var req struct { + AppName string `json:"app_name"` + } + _ = c.ShouldBindJSON(&req) + req.AppName = strings.TrimSpace(req.AppName) + if !this.echoOrchScopeOK(c, req.AppName) { + return + } + + all := make([]*EchoOrchestration, 0) + if err := postgres.Find(comm.TableEchomeetOrch, &all, "app_name=?", req.AppName); err != nil { + writeErr(c, pb.ErrorCode_DBError, err.Error()) + return + } + items := make([]*EchoOrchestration, 0, len(all)) + // 区域分叉本页不编辑,但要如实回报行数:后台显示的若与线上跑的不是一回事,排查会被带偏。 + otherRegions := map[string]int{} + for _, r := range all { + if r.Region == echoOrchEditRegion { + items = append(items, r) + continue + } + otherRegions[strconv.Itoa(int(r.Region))]++ + } + sort.SliceStable(items, func(i, j int) bool { + if items[i].Kind != items[j].Kind { + return items[i].Kind < items[j].Kind + } + return items[i].Priority < items[j].Priority + }) + + setting := &EchoOrchSetting{} + if err := postgres.FindOne(comm.TableEchomeetOrchSetting, setting, "app_name=? AND region=?", req.AppName, echoOrchEditRegion); err != nil { + setting = &EchoOrchSetting{AppName: req.AppName, Region: echoOrchEditRegion} // 未设置 → 零值 + } + + writeOK(c, gin.H{ + "items": items, + "setting": setting, + "available": this.echoOrchAvailableSvcs(req.AppName), + // 各段对应的服务池类别,前端据此从「可选服务」里筛出每段能选的服务。 + "kind_cats": gin.H{ + strconv.Itoa(int(echoKindASR)): echoKindSvcCats[echoKindASR], + strconv.Itoa(int(echoKindMT)): echoKindSvcCats[echoKindMT], + strconv.Itoa(int(echoKindLLM)): echoKindSvcCats[echoKindLLM], + }, + "other_regions": otherRegions, + }) +} + +// saveEchoOrch 声明式保存:请求给的就是该应用**最终**的转写顺序与总结模型,后端按差异增删改。 +// +// 为什么不是逐行 CRUD:这一页只有「选哪几家、谁在前」两个决定,声明式保存让页面所见即库内所存, +// 不会出现「界面上删掉了但库里还留着一行 enabled=true」这种后台与运行时不一致的中间态。 +// +// ⚠️ **翻译段(kind=2) 不在本页范围内,必须原样保留**:它同样是语音纪要流水线的一环 +// (见 orch_resolver.go),这里若跟着一起 reconcile 就会把翻译服务清空,表现为纪要能转写、 +// 译文整体消失。 +func (this *serverComp) saveEchoOrch(c *gin.Context) { + // ⚠️ 两段都是**指针**:nil = 本次不动这一段。页面上转写与总结是两个标签页各自保存, + // 用值类型的话「保存总结」会带着一个空的 asr 数组过来,把转写编排整段清空。 + var req struct { + AppName string `json:"app_name"` + ASR *[]string `json:"asr"` // 转写:按顺序,第 1 个主用,其余依次兜底 + LLM *string `json:"llm"` // 总结:单选,空串 = 不配总结 + } + if err := c.ShouldBindJSON(&req); err != nil { + writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) + return + } + req.AppName = strings.TrimSpace(req.AppName) + if !this.echoOrchScopeOK(c, req.AppName) { + return + } + + // 去重并保序;顺带校验每个 svc 的可用性与类别,有一个不合法就整体拒绝(别写一半)。 + want := map[int32][]string{} + kinds := make([]int32, 0, 2) + if req.ASR != nil { + asr := make([]string, 0, len(*req.ASR)) + seen := map[string]bool{} + for _, id := range *req.ASR { + id = strings.TrimSpace(id) + if id == "" || seen[id] { + continue + } + if ok, msg := this.echoOrchSvcAllowed(req.AppName, id, echoKindASR); !ok { + writeErr(c, pb.ErrorCode_ReqParameterError, msg) + return + } + seen[id] = true + asr = append(asr, id) + } + want[echoKindASR] = asr + kinds = append(kinds, echoKindASR) + } + if req.LLM != nil { + llm := strings.TrimSpace(*req.LLM) + if llm != "" { + if ok, msg := this.echoOrchSvcAllowed(req.AppName, llm, echoKindLLM); !ok { + writeErr(c, pb.ErrorCode_ReqParameterError, msg) + return + } + want[echoKindLLM] = []string{llm} + } + kinds = append(kinds, echoKindLLM) + } + if len(kinds) == 0 { + writeErr(c, pb.ErrorCode_ReqParameterError, "asr / llm 至少给一段") + return + } + + old := make([]*EchoOrchestration, 0) + if err := postgres.Find(comm.TableEchomeetOrch, &old, "app_name=? AND region=?", req.AppName, echoOrchEditRegion); err != nil { + writeErr(c, pb.ErrorCode_DBError, err.Error()) + return + } + oldBy := map[int32]map[string]*EchoOrchestration{} + for _, r := range old { + if oldBy[r.Kind] == nil { + oldBy[r.Kind] = map[string]*EchoOrchestration{} + } + oldBy[r.Kind][r.SvcId] = r + } + + now := time.Now().Unix() + for _, kind := range kinds { + keep := map[string]bool{} + for i, id := range want[kind] { + keep[id] = true + row := oldBy[kind][id] + priority := int32((i + 1) * 10) + isDefault := kind == echoKindLLM && i == 0 // 总结取 is_default;转写不用这个标记 + if row != nil { + // 已有行:只动顺序/启用/默认,cost_hint、client_visible 等运营填过的字段原样保留。 + row.Priority = priority + row.Enabled = true + row.IsDefault = isDefault + row.Updatetime = now + if err := postgres.Save(comm.TableEchomeetOrch, row); err != nil { + writeErr(c, pb.ErrorCode_DBError, err.Error()) + return + } + continue + } + add := &EchoOrchestration{ + AppName: req.AppName, Region: echoOrchEditRegion, Kind: kind, SvcId: id, + Priority: priority, IsDefault: isDefault, Enabled: true, + Createtime: now, Updatetime: now, + } + if err := postgres.Insert(comm.TableEchomeetOrch, add); err != nil { + writeErr(c, pb.ErrorCode_DBError, err.Error()) + return + } + } + for id, row := range oldBy[kind] { + if keep[id] { + continue + } + if err := postgres.Delete(comm.TableEchomeetOrch, "row_id=?", row.RowId); err != nil { + writeErr(c, pb.ErrorCode_DBError, err.Error()) + return + } + } + } + + broadcastConfigChanged(comm.ConfigKindEchomeetOrch, "update", echoOrchEditRegion) // 通知 echomeet 运行时重载编排 + writeOK(c, gin.H{"saved": kinds}) +} diff --git a/apps/services/modules/console/echomeet_orch_test.go b/apps/services/modules/console/echomeet_orch_test.go new file mode 100644 index 00000000..6d03ed78 --- /dev/null +++ b/apps/services/modules/console/echomeet_orch_test.go @@ -0,0 +1,33 @@ +package console + +import "testing" + +// 会议配置页的两段各自对应哪些服务类别,是后台下拉能不能列出服务的唯一依据, +// 而类别 id 同时写在三处(comm.SvcCat* / console svcCat* / admin 前端 SCAT)。 +// 这里把编排段 → 类别的映射钉死:改类别编号而漏改这张表,表现是后台某一段的下拉 +// **空空如也且不报任何错**(服务都在、只是筛不出来),排查时很容易去怀疑数据。 +func TestEchoKindSvcCats(t *testing.T) { + want := map[int32][]int32{ + echoKindASR: {8}, // 录音文件识别 → 会议转写 + echoKindMT: {3}, // 机器翻译(本页不编辑,运行时仍在用) + echoKindLLM: {5, 9}, // 文本大模型 + 多媒体大模型 → 会议总结 + } + if len(echoKindSvcCats) != len(want) { + t.Fatalf("编排段数量变了: got %d want %d", len(echoKindSvcCats), len(want)) + } + for kind, cats := range want { + got := echoKindSvcCats[kind] + if len(got) != len(cats) { + t.Fatalf("kind=%d 类别数不符: got %v want %v", kind, got, cats) + } + for i := range cats { + if got[i] != cats[i] { + t.Fatalf("kind=%d 类别不符: got %v want %v", kind, got, cats) + } + } + } + // 前端 meetconfig.vue 的兜底常量与这里同值,接口拿不到 kind_cats 时按它筛。 + if echoKindASR != 1 || echoKindLLM != 3 { + t.Fatalf("编排段编号变了: asr=%d llm=%d", echoKindASR, echoKindLLM) + } +} diff --git a/apps/services/modules/console/server.go b/apps/services/modules/console/server.go index a364fb99..e688c882 100644 --- a/apps/services/modules/console/server.go +++ b/apps/services/modules/console/server.go @@ -318,6 +318,15 @@ func (this *serverComp) handleWeb(c *gin.Context) { case "api_syncmeettemplate": this.syncMeetTemplate(c) return + // 会议配置 · 服务编排:会议转写(kind=1)/会议总结(kind=3) 各选哪些第三方服务。 + // 只引用 svc_config 里的服务 id,不存凭据;按 app_name 隔离(没有全局默认层)。 + // 2026-09-12 随「会议记录服务」页一起删过,2026-09-17 并进「会议配置」页恢复。 + case "api_getechoorch": + this.getEchoOrch(c) + return + case "api_saveechoorch": + this.saveEchoOrch(c) + return // 第三方服务配置(svcconfig):服务字段定义/默认值 + 区域字段分叉(值覆盖/专属字段/停用字段) + 多区域同步,存 console 主库,与选中应用无关。 case "api_getsvcconfigs": this.getSvcConfigs(c)