# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
> 根 [CLAUDE.md](../../../../CLAUDE.md) 描述整体架构(lego 框架、反射注册路由、pb 生成);本文件只补充 `echomeet` 模块自身。
## 模块职责
`echomeet` 是「会议记录」模块:上传音频 → **转写(Transcribe)** → **翻译(Translate)** → **AI 总结(Summary)**,产出 `overview`(短总结)+ `summary`(按模板的完整纪要)。由 `home` 服务装载运行([services/home/main.go](../../services/home/main.go)),不是独立服务。HTTP 接口经反射注册为 `echomeet_<方法名小写>`,前缀路由示例 `/api/home/echomeet_addrecord`。
## 核心设计:识别/翻译/总结三类各自按优先级选服务(后台配置驱动)
> 旧设计是「一会一服务商,三段同源」(`ResolveServiceType` 定一个服务商三步同走)。现已改为**三类各自独立按优先级选择**,配置全部搬进管理后台。旧的 `ResolveServiceType` 仅作兜底保留。
四套服务商,每套三件套(见对应 `provider_*.go`):
| ServiceType | 转写 | 翻译 | 总结(Summarizer) |
|---|---|---|---|
| ByteDance | `sys/bytedance/audomodel` | 火山翻译 | 豆包 `sys/doubao` |
| Ali | DashScope `sys/aliyun/filetrans`(paraformer-v2,**不支持西语等**) | 阿里 MT | 通义千问(`sys/openai` 兼容端点)|
| Google | Cloud Speech | Cloud Translation | Gemini `sys/google/gemini` |
| Microsoft | Azure Speech | Azure Translator | Azure OpenAI(`sys/openai` 兼容端点)|
三类能力抽象为 `Transcriber` / `Translator` / `Summarizer` 接口(providers.go)。**新的选择/配置机制在 [svcconfig.go](svcconfig.go)**:
- 配置表 `echomeet_svc_config`(结构见 [comm/echomeetsvc.go](../../comm/echomeetsvc.go)):识别/翻译/总结三类,每类多条服务、按 `Priority` 升序;识别条目带 `SupportLangs`;总结条目=具体文本大模型。**存应用业务库、全局一份**(照 `app_module_config` 路子),密钥 AES 落库。
- 启动(`providersComp.Init` → `loadOrSeedSvcConfig`):建表;库空则把 yaml 四套服务商 seed 成行(**阿里识别的 SupportLangs seed 成 paraformer-v2 真实支持集,不含西语**);然后用库值(凭据来自库)重建各 provider,总结按条目 `NewSys` 建实例(一个服务商可多模型)。
- 选择:
- `selectASR(lang)`:按识别优先级取第一个支持该语言的服务(`SupportLangs` 非空以其为准,否则回退 `Transcriber.SupportsLanguage`)——**修复了西语被强路由到阿里的问题**。存 `record.ServiceType`。
- `selectMT(from,to)`:按翻译优先级独立选。
- 总结按 `record.SummaryModel`(客户端在 starttask/summary 传 `summary_model`,`echomeet_getsummarymodels` 拉清单)→ `GetSummarizerById`,空则默认(优先级最高)。
- 后台 CRUD 在 console([modules/console/api_echomeetsvc.go](../console/api_echomeetsvc.go),路由 `/console/api/echomeetsvc/*`,经 `getServiceDB` 写业务库),前端页 [admin/pages/meetingsvc.vue](../../../admin/app/pages/meetingsvc.vue)。**改配置需重启 home 生效**。
新增服务商 = 写 `provider_xxx.go` 实现三接口 + 在 `providersComp.Init` 注册进三张 map + 在 `EchoSvcCatalog` 补字段模板 + seed 映射。
## 组件与异步流水线
`module.go` 装配组件:`api`(HTTP) / `model`(gorm) / `cache`(模板缓存) / `providers` / `tasks`。
[tasks.go](tasks.go) 是异步引擎:**Redis 队列 + ants 协程池 + cron 每秒调度**。
- 两条流水线,各有「等待队列 / 在执行集合」两个 list:短音频转写(`keyShortAwait/Proc`)、AI 总结(`keyAIAwait/Proc`)。队列 key 用 `service.GetTag()` + `redissys.RKey`(应用分组前缀)命名空间隔离,多应用共用一个 Redis 不串扰。
- `TaskScheduling`(cron `*/1 * * * * ?`)每秒把等待队列搬进执行集合(受 `MaxShortAudioProcess`/`MaxAIAwaitProcess` 限流),再 `pool.Invoke` 真正处理。
- **转写完成两条路径**:异步回调(字节 [api_backcall.go](api_backcall.go) / 阿里 [api_alibackcall.go](api_alibackcall.go))+ 客户端拉取时主动轮询(`PollTranscribe`,由 [api_getrecord.go](api_getrecord.go)/[api_getrecords.go](api_getrecords.go) 在状态为 Transcribing 时驱动,`TranscribeQueryMinInterval` 限频)。**没有 cron 兜底轮询**。
- 短音频走字节 flash 同步通道(`ShortAudioProcess`);长音频/其他服务商走异步 submit+query。
状态机见 `DBEchoMeetRecordState`(proto):Unknow→AwaitTranscribing→Transcribing→AwaitSummarizing→Summarizing→Completed→Readed,失败 TranscribeFail/SummarizFail。转写失败会**退还** `Meetintegral` 时长并冲正统计埋点。
## 语言码映射(core.go,易错)
输入统一是客户端 BCP-47(`zh-CN`/`en-US`…)。每个平台格式不同,core.go 逐家维护 map。**铁律:未识别语言一律返回原值,绝不 fallback 成 `zh`**——否则「未知语言→中文」会藏在调用栈深处。翻译时在 `TranslateProcess` 直接传 BCP-47,由各 provider 内部转换,不要提前转(会被二次转换误判)。
## AI 总结输入约定(含图片)
`AIProcess`(tasks.go)拼 `originalText`:把 `translate`(已翻译的分句)按 `[说话人]:内容` 拼接;若 `record.Remark`(用户创建时填的备注)非空,用语言中立的结构化标签包裹:`…\n\n\n…\n`——**不要硬编码某语言的提示词**,避免污染多语言总结。模板 `template.Template`/`template.Outline` 作为 system prompt,分别产出 `summary`/`overview`(两路并发)。
**图片注释(支持多张)**:请求 `EchomeetAddRecordReq.imageurls` 是 `repeated string`,DB `DBEchoMeetRecord.imageurls` 是 **string 列存 JSON 数组**(编码方式同 `original`:`utils.ToString` 写、`json.Unmarshal` 读)。`AIProcess` 解析出 url 列表后,作为多模态内容随 user 消息一并发给总结模型(要求所配模型支持视觉)。链路:`ChatMessage.Images []string` → 各 `Summarizer.Chat` → sys 层(豆包/通义/Azure 用 OpenAI 风格 `image_url` 直传;Gemini 在 `sys/google/gemini` 内逐张下载转 base64 内联 `inlineData`,单张失败仅跳过)。列表为空时完全走原文本路径,不影响既有行为。
## 重新生成 pb
改了 `apps/proto/echomeet/*.proto` 后需重生成(protoc-gen-go v1.36.6,已安装),并复刻源项目 `pb.py` 的 `@go_tags` 注入(覆盖 json 去掉 omitempty、追加 gorm/自定义 tag):
```bash
cd apps/services
protoc --go_out=pb --go_opt=paths=import -I ../proto ../proto/echomeet/echomeet_db.proto
protoc --go_out=pb --go_opt=paths=import -I ../proto ../proto/echomeet/echomeet_msg.proto
# 再对生成的 pb/echomeet_*.pb.go 执行 @go_tags 注入(见根 CLAUDE.md / 源项目 pb.py)
```
注意:HTTP 层用 jsoniter 按 **json tag** 收发、DB 用 **gorm tag**,protobuf 二进制 wire 不参与这些 API,所以字段的 json/gorm tag 必须正确。