Browse Source

优化邮件登陆服务

main
liwei1dao 3 months ago
parent
commit
1390f50144
  1. 1
      apps/admin/.nuxt/manifest/meta/a1179f43-7381-4f05-9786-c76278046c3a.json
  2. 2
      apps/admin/.nuxt/nuxt.d.ts
  3. 2
      apps/admin/.nuxt/nuxt.node.d.ts
  4. 2
      apps/admin/.nuxt/tailwind/postcss.mjs
  5. 2
      apps/admin/.nuxt/tsconfig.app.json
  6. 2
      apps/admin/.nuxt/tsconfig.json
  7. 2
      apps/admin/.nuxt/tsconfig.shared.json
  8. 1
      apps/admin/app/layouts/default.vue
  9. 11
      apps/admin/app/pages/apps.vue
  10. 354
      apps/admin/app/pages/meetingsvc.vue
  11. 1
      apps/proto/echomeet/echomeet_db.proto
  12. 23
      apps/proto/echomeet/echomeet_msg.proto
  13. 10
      apps/services/Dockerfile.voitrans
  14. 7
      apps/services/comm/const.go
  15. 218
      apps/services/comm/echomeetsvc.go
  16. 10
      apps/services/comm/moduleconfig.go
  17. 1
      apps/services/comm/nats.go
  18. 5
      apps/services/comm/svcruntime.go
  19. 218
      apps/services/modules/console/api_echomeetsvc.go
  20. 4
      apps/services/modules/console/model_moduleconfig.go
  21. 6
      apps/services/modules/console/server.go
  22. 22
      apps/services/modules/echomeet/CLAUDE.md
  23. 30
      apps/services/modules/echomeet/api_getsummarymodels.go
  24. 15
      apps/services/modules/echomeet/api_starttask.go
  25. 8
      apps/services/modules/echomeet/api_summary.go
  26. 13
      apps/services/modules/echomeet/providers.go
  27. 466
      apps/services/modules/echomeet/svcconfig.go
  28. 24
      apps/services/modules/echomeet/tasks.go
  29. 13
      apps/services/pb/echomeet_db.pb.go
  30. 389
      apps/services/pb/echomeet_msg.pb.go
  31. 6
      deploy/voitrans/env/env.example

1
apps/admin/.nuxt/manifest/meta/a1179f43-7381-4f05-9786-c76278046c3a.json

@ -0,0 +1 @@
{}

2
apps/admin/.nuxt/nuxt.d.ts

@ -1,7 +1,7 @@
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@pinia/nuxt" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxt/devtools" />
/// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/builder-env.d.ts" />
/// <reference path="types/plugins.d.ts" />

2
apps/admin/.nuxt/nuxt.node.d.ts

@ -1,7 +1,7 @@
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="@pinia/nuxt" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxt/devtools" />
/// <reference path="types/nitro-layouts.d.ts" />
/// <reference path="types/modules.d.ts" />
/// <reference path="types/runtime-config.d.ts" />

2
apps/admin/.nuxt/tailwind/postcss.mjs

@ -1,4 +1,4 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 7/2/2026, 8:24:14 PM
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 7/7/2026, 7:51:39 PM
import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger";

2
apps/admin/.nuxt/tsconfig.app.json

@ -107,7 +107,7 @@
"./imports"
],
"#app-manifest": [
"./manifest/meta/1a398374-98c8-4def-95d3-61ae1c7c5483.json"
"./manifest/meta/a1179f43-7381-4f05-9786-c76278046c3a.json"
],
"#components": [
"./components"

2
apps/admin/.nuxt/tsconfig.json

@ -107,7 +107,7 @@
"./imports"
],
"#app-manifest": [
"./manifest/meta/1a398374-98c8-4def-95d3-61ae1c7c5483.json"
"./manifest/meta/a1179f43-7381-4f05-9786-c76278046c3a.json"
],
"#components": [
"./components"

2
apps/admin/.nuxt/tsconfig.shared.json

@ -107,7 +107,7 @@
"./imports"
],
"#app-manifest": [
"./manifest/meta/1a398374-98c8-4def-95d3-61ae1c7c5483.json"
"./manifest/meta/a1179f43-7381-4f05-9786-c76278046c3a.json"
],
"#build": [
"."

1
apps/admin/app/layouts/default.vue

@ -171,6 +171,7 @@ const menuGroups = [
{ id: 'globalconfig', label: '全局环境配置', to: '/globalconfig', icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' },
{ id: 'mcpconfig', label: 'MCP配置', to: '/mcpconfig', icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-3-4h.01M16 16h.01' },
{ 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' },
{ id: 'meetingsvc', label: '会议记录服务配置', to: '/meetingsvc', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' },
{ 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: 'calltranslate', label: '通话翻译配置', to: '/calltranslate', icon: 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129' },
],

11
apps/admin/app/pages/apps.vue

@ -86,6 +86,8 @@
<div class="flex gap-1 justify-end">
<template v-if="a.status !== 'pending'">
<button class="btn btn-ghost btn-xs text-indigo-600" @click="openAppConfig(a)">服务配置</button>
<button v-if="a.base_url" class="btn btn-ghost btn-xs text-teal-600" @click="openSwagger(a)" title="打开该部署的 Swagger API 文档(可在线 Try it out 直接调用该应用接口)">API测试</button>
<button v-else class="btn btn-ghost btn-xs text-slate-300 cursor-not-allowed" disabled title="该部署未配置对外地址(base_url),无法打开 Swagger;到「编辑」里补上对外地址">API测试</button>
<button class="btn btn-ghost btn-xs text-slate-500" @click="showDeployForm(a.app_name, a)">编辑</button>
<button class="btn btn-ghost btn-xs text-amber-600" @click="openUpgrade(a)">升级</button>
<button class="btn btn-ghost btn-xs text-emerald-600" @click="openAnalyze(a.id, a.name || ('部署#' + a.id))">分析</button>
@ -867,6 +869,15 @@ async function runAnalyze() {
loadAll()
}
// 打开该部署的 Swagger API 文档:base_url + /swagger/index.html(新标签页)。
// swagger 已内嵌 gateway 二进制,SwaggerInfo.Host 留空 → UI 用当前页面 origin(应用域名) 作为调用地址,
// 故 Try it out 会直接打到该应用自己的真实网关接口(前提:该域名 /swagger、/api 均反代到 gateway 7100)。
function openSwagger(a: any) {
let base = (a?.base_url || '').trim().replace(/\/+$/, '')
if (!base) { error('该部署未配置对外地址(base_url)'); return }
if (!/^https?:\/\//i.test(base)) base = 'https://' + base
window.open(base + '/swagger/index.html', '_blank', 'noopener')
}
function notifyApp(id: number) {
if (!window.confirm(`向部署 #${id} 发送一条配置 reload 事件?`)) return
consoleApi<{ subject?: string }>('config/notify', { app_id: id, type: 'all', action: 'reload' })

354
apps/admin/app/pages/meetingsvc.vue

@ -0,0 +1,354 @@
<template>
<div class="p-4 space-y-4">
<!-- 顶部:标题 + 应用选择 -->
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 class="text-lg font-semibold">会议记录服务配置</h1>
<p class="text-xs text-base-content/60 mt-1">
识别 / 翻译 / 总结 三类服务统一在此管理:按优先级自动选择,识别叠加「支持语言」过滤,总结为客户端可选的具体模型条目。改配置后 home 需重启生效。
</p>
</div>
<label class="flex items-center gap-2 text-sm">
<span class="text-base-content/60">部署应用</span>
<select v-model.number="appId" class="select select-bordered select-sm" @change="loadList">
<option v-for="a in apps" :key="a.id" :value="a.id">{{ a.app_name || a.name }}({{ a.region }})</option>
</select>
</label>
</div>
<!-- 类别 tab -->
<div class="tabs tabs-boxed w-fit">
<a
v-for="c in categories"
:key="c.value"
class="tab"
:class="{ 'tab-active': curCat === c.value }"
@click="curCat = c.value"
>{{ c.label }}<span class="ml-1 opacity-60">{{ (grouped[c.value] || []).length }}</span></a>
</div>
<div v-if="dbError" class="alert alert-error text-sm">{{ dbError }}</div>
<!-- 列表 -->
<div class="bg-base-100 rounded-lg border border-base-200 overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th class="w-16">优先级</th>
<th>名称 / 条目id</th>
<th>服务商</th>
<th v-if="curCatHasLangs">支持语言</th>
<th class="w-20">启用</th>
<th class="w-28">凭据</th>
<th class="w-40 text-right">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(it, idx) in curList" :key="it.id">
<td>
<div class="flex items-center gap-1">
<button class="btn btn-ghost btn-xs" :disabled="idx === 0" @click="move(idx, -1)">↑</button>
<button class="btn btn-ghost btn-xs" :disabled="idx === curList.length - 1" @click="move(idx, 1)">↓</button>
</div>
</td>
<td>
<div class="font-medium">{{ it.name || it.id }}</div>
<div class="text-xs text-base-content/50 font-mono">{{ it.id }}</div>
</td>
<td><span class="badge badge-ghost badge-sm">{{ it.provider }}</span></td>
<td v-if="curCatHasLangs">
<div v-if="it.support_langs" class="flex flex-wrap gap-1">
<span v-for="l in it.support_langs.split(',').slice(0, 6)" :key="l" class="badge badge-outline badge-xs font-mono">{{ l }}</span>
<span v-if="it.support_langs.split(',').length > 6" class="text-xs opacity-50">…</span>
</div>
<span v-else class="text-xs opacity-40">全部(默认)</span>
</td>
<td>
<input type="checkbox" class="toggle toggle-success toggle-sm" :checked="it.enabled" @change="toggleEnabled(it)" />
</td>
<td>
<span v-if="secretCount(it) > 0" class="text-xs">🔒 {{ secretCount(it) }}</span>
<span v-else class="text-xs opacity-40">—</span>
</td>
<td class="text-right">
<button class="btn btn-ghost btn-xs" @click="openEdit(it)">编辑</button>
<button class="btn btn-ghost btn-xs text-error" @click="del(it)">删除</button>
</td>
</tr>
<tr v-if="!curList.length">
<td :colspan="curCatHasLangs ? 7 : 6" class="text-center text-base-content/40 py-6">暂无服务,点击下方「添加」</td>
</tr>
</tbody>
</table>
</div>
<button class="btn btn-primary btn-sm" @click="openAdd">+ 添加{{ curCatLabel }}服务</button>
<!-- 编辑/新增 弹窗 -->
<dialog ref="dlg" class="modal">
<div class="modal-box max-w-xl">
<h3 class="font-semibold mb-3">{{ editing.__isNew ? '添加' : '编辑' }}{{ curCatLabel }}服务</h3>
<div class="space-y-3">
<!-- 服务商(新增时可选,编辑时锁定) -->
<label class="form-control">
<span class="label-text text-xs">服务商</span>
<select
v-model="editing.provider"
class="select select-bordered select-sm"
:disabled="!editing.__isNew"
@change="onProviderChange"
>
<option v-for="p in providerOptions" :key="p.provider" :value="p.provider">{{ p.name }}({{ p.provider }})</option>
</select>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="form-control">
<span class="label-text text-xs">条目 id(唯一)</span>
<input v-model="editing.id" class="input input-bordered input-sm font-mono" :disabled="!editing.__isNew" />
</label>
<label class="form-control">
<span class="label-text text-xs">展示名{{ curCat === 3 ? '(客户端可见)' : '' }}</span>
<input v-model="editing.name" class="input input-bordered input-sm" />
</label>
</div>
<!-- 目录字段 -->
<div v-for="f in curFields" :key="f.key" class="form-control">
<span class="label-text text-xs">{{ f.label }}<span v-if="f.required" class="text-error">*</span></span>
<textarea
v-if="f.type === 'textarea'"
v-model="editing.values[f.key]"
class="textarea textarea-bordered textarea-sm font-mono"
rows="3"
:placeholder="f.placeholder"
></textarea>
<input
v-else
v-model="editing.values[f.key]"
:type="f.secret ? 'password' : 'text'"
class="input input-bordered input-sm"
:placeholder="f.placeholder"
autocomplete="off"
/>
<span v-if="f.desc" class="text-xs text-base-content/50 mt-0.5">{{ f.desc }}</span>
</div>
<!-- 识别:支持语言 -->
<div v-if="curCatHasLangs" class="form-control">
<span class="label-text text-xs">支持语言(留空=不限,按引擎默认)</span>
<LangSelect v-model="editing.support_langs" :multiple="true" />
</div>
<div class="grid grid-cols-2 gap-3 items-end">
<label class="form-control">
<span class="label-text text-xs">优先级(小=优先)</span>
<input v-model.number="editing.priority" type="number" class="input input-bordered input-sm" />
</label>
<label class="flex items-center gap-2 pb-2">
<input v-model="editing.enabled" type="checkbox" class="toggle toggle-success toggle-sm" />
<span class="text-sm">启用</span>
</label>
</div>
</div>
<div class="modal-action">
<button class="btn btn-ghost btn-sm" @click="closeEdit">取消</button>
<button class="btn btn-primary btn-sm" :disabled="saving" @click="save">{{ saving ? '保存中…' : '保存' }}</button>
</div>
</div>
<form method="dialog" class="modal-backdrop"><button>close</button></form>
</dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useApi } from '~/composables/useApi'
import { useToast } from '~/composables/useToast'
definePageMeta({ title: '会议记录服务配置' })
const { consoleApi } = useApi()
const { success, error } = useToast()
const EncMask = '[encrypted]'
interface Field { key: string; label: string; type: string; secret: boolean; required: boolean; placeholder?: string; desc?: string }
interface CatalogItem { category: number; provider: string; name: string; has_langs: boolean; multi: boolean; fields: Field[] }
interface Category { value: number; label: string }
interface SvcConfig {
id: string; category: number; provider: string; name: string
priority: number; enabled: boolean; support_langs: string
values: Record<string, string>
__isNew?: boolean
}
interface AppItem { id: number; app_name: string; name: string; region: string }
const apps = ref<AppItem[]>([])
const appId = ref(0)
const catalog = ref<CatalogItem[]>([])
const categories = ref<Category[]>([])
const curCat = ref(1)
const items = ref<SvcConfig[]>([])
const dbError = ref('')
const saving = ref(false)
const dlg = ref<HTMLDialogElement | null>(null)
const editing = reactive<SvcConfig>(blank(1, ''))
function blank(cat: number, provider: string): SvcConfig {
return { id: '', category: cat, provider, name: '', priority: 100, enabled: true, support_langs: '', values: {}, __isNew: true }
}
// 当前类别的目录(服务商列表)
const providerOptions = computed(() => catalog.value.filter((c) => c.category === curCat.value))
const curCatLabel = computed(() => categories.value.find((c) => c.value === curCat.value)?.label || '')
const curCatHasLangs = computed(() => providerOptions.value.some((c) => c.has_langs))
const curDef = computed(() => catalog.value.find((c) => c.category === editing.category && c.provider === editing.provider) || null)
const curFields = computed(() => curDef.value?.fields || [])
// 按类别归集 + 优先级排序
const grouped = computed<Record<number, SvcConfig[]>>(() => {
const m: Record<number, SvcConfig[]> = {}
for (const it of items.value) (m[it.category] ||= []).push(it)
for (const k in m) m[k].sort((a, b) => a.priority - b.priority)
return m
})
const curList = computed(() => grouped.value[curCat.value] || [])
function secretCount(it: SvcConfig): number {
const def = catalog.value.find((c) => c.category === it.category && c.provider === it.provider)
if (!def) return 0
return def.fields.filter((f) => f.secret && (it.values?.[f.key] ?? '') !== '').length
}
async function loadApps() {
try {
const list = await consoleApi<AppItem[]>('apps/list', {})
apps.value = list ?? []
if (apps.value.length && !appId.value) appId.value = apps.value[0].id
} catch (e: any) {
error(e?.message ?? '获取应用列表失败')
}
}
async function loadCatalog() {
try {
const d = await consoleApi<{ catalog: CatalogItem[]; categories: Category[] }>('echomeetsvc/catalog', {})
catalog.value = d?.catalog ?? []
categories.value = d?.categories ?? []
if (categories.value.length) curCat.value = categories.value[0].value
} catch (e: any) {
error(e?.message ?? '获取配置目录失败')
}
}
async function loadList() {
if (!appId.value) return
dbError.value = ''
try {
const d = await consoleApi<{ items: SvcConfig[] }>('echomeetsvc/list', { app_id: appId.value })
items.value = (d?.items ?? []).map((r) => ({ ...r, values: r.values || {} }))
} catch (e: any) {
dbError.value = e?.message ?? '获取配置失败'
}
}
function openAdd() {
const first = providerOptions.value[0]
Object.assign(editing, blank(curCat.value, first?.provider || ''))
onProviderChange()
dlg.value?.showModal()
}
function openEdit(it: SvcConfig) {
Object.assign(editing, JSON.parse(JSON.stringify(it)), { __isNew: false })
if (!editing.values) editing.values = {}
dlg.value?.showModal()
}
function closeEdit() { dlg.value?.close() }
// 新增时切换服务商:重置字段容器 + 生成默认 id/名称
function onProviderChange() {
const def = curDef.value
editing.values = {}
if (def) {
for (const f of def.fields) editing.values[f.key] = ''
const prefix = curCat.value === 1 ? 'asr' : curCat.value === 2 ? 'mt' : 'llm'
let base = `${prefix}_${editing.provider}`
// 总结允许一商多模型,冲突则追加序号
if (items.value.some((x) => x.id === base)) {
let n = 2
while (items.value.some((x) => x.id === `${base}_${n}`)) n++
base = `${base}_${n}`
}
editing.id = base
if (!editing.name) editing.name = def.name
if (curCat.value === 1) editing.support_langs = ''
}
}
async function save() {
if (!editing.id.trim()) { error('条目 id 必填'); return }
if (editing.__isNew && items.value.some((x) => x.id === editing.id)) { error('条目 id 已存在'); return }
saving.value = true
try {
const { __isNew, ...cfg } = editing
await consoleApi('echomeetsvc/save', { app_id: appId.value, config: cfg })
success('已保存')
closeEdit()
await loadList()
} catch (e: any) {
error(e?.message ?? '保存失败')
} finally {
saving.value = false
}
}
async function del(it: SvcConfig) {
if (!confirm(`确定删除「${it.name || it.id}」?`)) return
try {
await consoleApi('echomeetsvc/del', { app_id: appId.value, id: it.id })
success('已删除')
await loadList()
} catch (e: any) {
error(e?.message ?? '删除失败')
}
}
async function toggleEnabled(it: SvcConfig) {
try {
// 保存时密钥字段是脱敏值 [encrypted],后端会保留原密文,安全。
await consoleApi('echomeetsvc/save', { app_id: appId.value, config: { ...it, enabled: !it.enabled } })
await loadList()
} catch (e: any) {
error(e?.message ?? '操作失败')
await loadList()
}
}
// 同类内上下移:与相邻项交换优先级并提交
async function move(idx: number, dir: number) {
const list = curList.value
const a = list[idx]
const b = list[idx + dir]
if (!a || !b) return
const pa = a.priority, pb = b.priority
try {
await consoleApi('echomeetsvc/reorder', {
app_id: appId.value,
items: [{ id: a.id, priority: pb }, { id: b.id, priority: pa }],
})
await loadList()
} catch (e: any) {
error(e?.message ?? '排序失败')
}
}
onMounted(async () => {
await Promise.all([loadApps(), loadCatalog()])
await loadList()
})
</script>

1
apps/proto/echomeet/echomeet_db.proto

@ -72,6 +72,7 @@ message DBEchoMeetRecord {
int64 lastquerytime = 27; //@go_tags(`gorm:"lastquerytime" json:"lastquerytime"`) 上一次主动查询第三方服务的 unix 秒,用于限制查询频率
string remark = 28; //@go_tags(`gorm:"remark" json:"remark"`) 用户备注(创建记录时填写,参与AI总结)
string imageurls = 29; //@go_tags(`gorm:"imageurls" json:"imageurls"`) 注释图片url列表(JSON数组字符串,编码方式同original),作为多模态内容参与AI总结
string summary_model = 30; //@go_tags(`gorm:"summary_model" json:"summary_model"`) 总结所用文本大模型条目id(对应 echomeet_svc_config),空=按优先级默认
}
message ContextStruct{

23
apps/proto/echomeet/echomeet_msg.proto

@ -112,11 +112,12 @@ message EchomeetStartTaskReq{
bool isdistinguishspeaker = 4; //@go_tags(`json:"isdistinguishspeaker"`) //是否区分说话人
uint64 templateid = 5; //@go_tags(`json:"templateid"`) //会议记录模版索引id
string tid = 6; //@go_tags(`json:"tid"`) //会议记录模板tid
TranscribeService service_type = 7; //@go_tags(`json:"service_type"`) //转写服务类型,留空默认 Auto
TranscribeService service_type = 7; //@go_tags(`json:"service_type"`) //转写服务类型,留空默认 Auto(识别现按后台优先级+语言自动选择)
string summary_model = 8; //@go_tags(`json:"summary_model"`) //总结所用文本大模型条目id(echomeet_getsummarymodels 返回),留空=按优先级默认
}
message EchomeetStartTaskResp{
}
@ -141,10 +142,26 @@ message EchomeetSummaryReq {
string tid = 6; //@go_tags(`json:"tid"`) //会议记录模板tid
string tolanguage = 7; //@go_tags(`json:"tolanguage"`) 语言
TranscribeService service_type = 8; //@go_tags(`json:"service_type"`) //转写服务类型(用于翻译/总结路由),留空沿用记录已有
string summary_model = 9; //@go_tags(`json:"summary_model"`) //总结所用文本大模型条目id(echomeet_getsummarymodels 返回),留空沿用记录已有
}
message EchomeetSummaryResp{
}
//总结文本大模型条目(客户端选择用)
message SummaryModelInfo {
string id = 1; //@go_tags(`json:"id"`) //条目id,回传给 starttask/summary 的 summary_model
string name = 2; //@go_tags(`json:"name"`) //展示名
string provider = 3; //@go_tags(`json:"provider"`) //底层服务商 doubao/openai/gemini
bool is_default = 4; //@go_tags(`json:"is_default"`) //是否为默认(优先级最高)
}
//拉取可选的总结文本大模型清单
message EchomeetGetSummaryModelsReq{
}
message EchomeetGetSummaryModelsResp{
repeated SummaryModelInfo models = 1; //@go_tags(`json:"models"`) //可选总结模型清单(按优先级)
}
//已阅会议记录

10
apps/services/Dockerfile.voitrans

@ -20,6 +20,16 @@ RUN --mount=type=cache,target=/go/pkg/mod \
COPY . .
# 每次构建都重新生成「完整最新」的 swagger,内嵌进 gateway 二进制(gateway 里 _ "yunyan/swagger" 引用)。
# 这样「编译镜像 = 镜像内 API 文档与当前代码一致」永远成立,不再依赖谁本地跑没跑过 swag init。
# - 入口 services/home/main.go:承载 @title/@version 等通用注解
# - --parseDependency/--parseInternal:解析跨包(pb/modules/comm)引用的请求/响应类型
# - 版本 pin = go.mod 的 swaggo/swag v1.16.4,避免生成格式漂移
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go install github.com/swaggo/swag/cmd/swag@v1.16.4 && \
swag init -g services/home/main.go -o swagger --parseDependency --parseInternal
# 编译 5 个服务(统一 .a11 后缀,与镜像名 voitrans-a11 对齐)
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \

7
apps/services/comm/const.go

@ -57,9 +57,10 @@ const (
TableSvcConfig = "svc_config" //第三方服务配置(按应用;旧 third_svc_config 迁入其全局作用域)
TableSvcRegionOverride = "svc_region_override" //第三方服务区域字段覆盖(按应用;旧 third_svc_region_override 迁入)
TableAgentConfig = "agent_config" //yunyan 自管 Agent 配置(按应用;旧 console agent 表迁入)
TableAdmin = "adnim" //后台用户表
TableEchomeetTemplate = "echomeet_template" //模板表
TableEchomeetRecord = "echomeet_record" //会议记录表
TableAdmin = "adnim" //后台用户表
TableEchomeetTemplate = "echomeet_template" //模板表
TableEchomeetRecord = "echomeet_record" //会议记录表
TableEchomeetSvcConfig = "echomeet_svc_config" //会议记录服务配置(识别/翻译/总结三类,按优先级;存应用业务库,全局一份)
TableChannelApp = "channelapp" //渠道包管理
TableFactory = "factory" //厂商数据表

218
apps/services/comm/echomeetsvc.go

@ -0,0 +1,218 @@
package comm
import (
"yunyan/lego/sys/log"
"yunyan/lego/sys/mysql"
)
// 会议记录(echomeet)服务配置托管:把「识别 / 翻译 / 总结」三类服务配置全部搬进管理后台。
//
// 与旧的「一会一服务商,三段同源」不同:识别/翻译/总结各自维护一份**按优先级排序的服务清单**,
// - 识别:按优先级 + 各服务「支持语言」自动选择(跳过不支持源语言的服务);
// - 翻译:按优先级自动选择;
// - 总结:每条 = 一个具体文本大模型条目,客户端按 id 选择。
//
// 存储沿用 [[app_module_config]] 的路子:存**应用自己的业务库**(mysql/pg,随部署)、**全局一份**(无 app_name)。
// console 后台经 getServiceDB 写、home 的 echomeet 启动时 LoadOrSeed 读(改配置需重启生效)。
// 密钥字段以 AES-256-GCM 密文落库(comm.Encrypt,${FIELD_ENCRYPT_KEY},console 与业务侧同一把 key),
// 返回前端脱敏为 EncMask。字段清单由 EchoSvcCatalog 数据驱动(哪些字段、类型、是否密钥)。
// 服务类别。
const (
EchoSvcCatASR = 1 // 识别(转写)
EchoSvcCatMT = 2 // 翻译
EchoSvcCatLLM = 3 // 总结(文本大模型)
)
// EchoSvcConfig 一行 = 某类别下一条服务/模型条目。存应用业务库,全局一份。
type EchoSvcConfig struct {
Id string `gorm:"primaryKey;column:id;size:64" json:"id"` // 稳定键:asr_ali / mt_bytedance / llm_qwen_plus
Category int32 `gorm:"column:category;index:idx_echosvc_cat" json:"category"` // 1 识别 / 2 翻译 / 3 总结
Provider string `gorm:"column:provider;size:32" json:"provider"` // bytedance/ali/google/microsoft(识别/翻译);doubao/openai/gemini(总结)
Name string `gorm:"column:name;size:128" json:"name"` // 展示名(总结类即模型友好名,客户端可见)
Priority int32 `gorm:"column:priority" json:"priority"` // 同类内升序,小=优先
Enabled bool `gorm:"column:enabled" json:"enabled"` // 是否启用
SupportLangs string `gorm:"column:support_langs;type:text" json:"support_langs"` // 识别用:逗号分隔 BCP-47;空=按代码默认支持集
Values map[string]string `gorm:"column:values;type:text;serializer:json" json:"values"` // 参数/凭据值;密钥字段存密文
Createtime int64 `gorm:"column:createtime" json:"createtime"`
Updatetime int64 `gorm:"column:updatetime" json:"updatetime"`
}
func (EchoSvcConfig) TableName() string { return TableEchomeetSvcConfig }
// EchoSvcField 一个配置字段的元数据(驱动前端表单 + 后端密钥判定)。
type EchoSvcField struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"` // text|password|textarea|url
Secret bool `json:"secret"`
Required bool `json:"required"`
Placeholder string `json:"placeholder,omitempty"`
Desc string `json:"desc,omitempty"`
}
// EchoSvcProviderDef 某类别下某服务商/模型实现的字段模板。
type EchoSvcProviderDef struct {
Category int32 `json:"category"` // 所属类别
Provider string `json:"provider"` // 存库 Provider 值
Name string `json:"name"` // 展示名
HasLangs bool `json:"has_langs"` // 是否展示「支持语言」(识别类为 true)
Multi bool `json:"multi"` // 是否允许同 provider 多条(总结类为 true:一个服务商多个模型)
Fields []EchoSvcField `json:"fields"`
}
// 常用字段构造助手。
func esfText(key, label string, required bool) EchoSvcField {
return EchoSvcField{Key: key, Label: label, Type: "text", Required: required}
}
func esfSecret(key, label string, required bool) EchoSvcField {
return EchoSvcField{Key: key, Label: label, Type: "password", Secret: true, Required: required}
}
// EchoSvcCatalog 会议记录服务字段目录:识别/翻译/总结 × 各服务商,字段依据 home.yaml 的 echomeet 配置结构。
var EchoSvcCatalog = []EchoSvcProviderDef{
// ===== 识别(转写)=====
{Category: EchoSvcCatASR, Provider: "bytedance", Name: "字节跳动", HasLangs: true, Fields: []EchoSvcField{
esfText("AppID", "AppID", true),
esfSecret("Token", "Token", false),
esfText("Callback", "回调地址", false),
}},
{Category: EchoSvcCatASR, Provider: "ali", Name: "阿里云 DashScope", HasLangs: true, Fields: []EchoSvcField{
esfSecret("ApiKey", "ApiKey", true),
esfText("Callback", "回调地址", false),
}},
{Category: EchoSvcCatASR, Provider: "google", Name: "Google Cloud", HasLangs: true, Fields: []EchoSvcField{
{Key: "JsonPath", Label: "服务账号 JSON", Type: "textarea", Secret: true, Required: true,
Desc: "服务账号 JSON 内容或服务器上的文件路径"},
}},
{Category: EchoSvcCatASR, Provider: "microsoft", Name: "Azure 语音", HasLangs: true, Fields: []EchoSvcField{
esfSecret("Key", "Key", true),
esfText("Region", "Region", true),
}},
// ===== 翻译 =====
{Category: EchoSvcCatMT, Provider: "bytedance", Name: "火山翻译", Fields: []EchoSvcField{
esfText("AccessKey", "AccessKey", true),
esfSecret("SecretKey", "SecretKey", true),
esfText("Region", "Region", false),
}},
{Category: EchoSvcCatMT, Provider: "ali", Name: "阿里机器翻译", Fields: []EchoSvcField{
esfText("AccessKeyId", "AccessKeyId", true),
esfSecret("AccessKeySecret", "AccessKeySecret", true),
esfText("Region", "Region", false),
}},
{Category: EchoSvcCatMT, Provider: "google", Name: "Google 翻译", Fields: []EchoSvcField{
{Key: "JsonPath", Label: "服务账号 JSON", Type: "textarea", Secret: true, Required: true,
Desc: "服务账号 JSON 内容或服务器上的文件路径"},
}},
{Category: EchoSvcCatMT, Provider: "microsoft", Name: "Azure 翻译", Fields: []EchoSvcField{
esfSecret("Key", "Key", true),
esfText("Region", "Region", false),
esfText("Endpoint", "Endpoint", false),
}},
// ===== 总结(文本大模型;一个服务商可配多条不同模型)=====
{Category: EchoSvcCatLLM, Provider: "doubao", Name: "豆包(字节)", Multi: true, Fields: []EchoSvcField{
esfSecret("Apikey", "ApiKey", true),
esfText("Model", "模型 ID", true),
}},
{Category: EchoSvcCatLLM, Provider: "openai", Name: "OpenAI 兼容端点(通义/Azure OpenAI 等)", Multi: true, Fields: []EchoSvcField{
esfText("BaseURL", "BaseURL", true),
esfSecret("Token", "Token", true),
esfText("Model", "模型 ID", true),
}},
{Category: EchoSvcCatLLM, Provider: "gemini", Name: "Gemini(Google)", Multi: true, Fields: []EchoSvcField{
esfSecret("Apikey", "ApiKey", true),
esfText("Model", "模型 ID", true),
esfText("Endpoint", "自定义 Endpoint(可选)", false),
}},
}
// FindEchoSvcDef 按 (类别, provider) 取字段模板;找不到返回 nil。
func FindEchoSvcDef(category int32, provider string) *EchoSvcProviderDef {
for i := range EchoSvcCatalog {
if EchoSvcCatalog[i].Category == category && EchoSvcCatalog[i].Provider == provider {
return &EchoSvcCatalog[i]
}
}
return nil
}
// EchoSvcFieldIsSecret 判断某 (类别,provider,字段) 是否密钥字段。目录优先,回退通用命名规则。
func EchoSvcFieldIsSecret(category int32, provider, key string) bool {
if def := FindEchoSvcDef(category, provider); def != nil {
for _, f := range def.Fields {
if f.Key == key {
return f.Secret
}
}
}
return IsSecretField(key)
}
// EncryptEchoValues 返回 values 的副本,把密钥字段加密(空值/已是 EncMask 的不动,交由调用方决定)。
func EncryptEchoValues(encKey string, category int32, provider string, values map[string]string) map[string]string {
out := make(map[string]string, len(values))
for k, v := range values {
if EchoSvcFieldIsSecret(category, provider, k) && v != "" && v != EncMask {
if ct, err := Encrypt(encKey, v); err == nil {
out[k] = ct
continue
}
}
out[k] = v
}
return out
}
// DecryptEchoValues 返回 values 的副本,把密钥字段解密(供运行时消费)。
func DecryptEchoValues(encKey string, category int32, provider string, values map[string]string) map[string]string {
out := make(map[string]string, len(values))
for k, v := range values {
if EchoSvcFieldIsSecret(category, provider, k) && v != "" {
plain, err := Decrypt(encKey, v)
if err == nil {
out[k] = plain
continue
}
// 仍沿用密文(保持既有行为),但必须留痕:否则密文会被当明文用作第三方 API key,
// 表现为鉴权失败,而后台显示"已配置",无从排查。
log.Errorf("[EchoSvcConfig] 密钥字段解密失败 category=%d provider=%s key=%s err=%v —— 检查本服务 ${FIELD_ENCRYPT_KEY} 是否与 console 完全一致", category, provider, k, err)
}
out[k] = v
}
return out
}
// MaskEchoValues 返回 values 的副本,把密钥字段脱敏为 EncMask(返回前端用)。
func MaskEchoValues(category int32, provider string, values map[string]string) map[string]string {
out := make(map[string]string, len(values))
for k, v := range values {
if EchoSvcFieldIsSecret(category, provider, k) && v != "" {
out[k] = EncMask
continue
}
out[k] = v
}
return out
}
// EnsureEchoSvcTable 建表(幂等)。db 为应用业务库。
func EnsureEchoSvcTable(db mysql.ISys) error {
if db == nil {
return nil
}
return db.CreateTable(TableEchomeetSvcConfig, &EchoSvcConfig{})
}
// LoadEchoSvcConfigs 读全部会议记录服务配置行(未解密)。
func LoadEchoSvcConfigs(db mysql.ISys) ([]*EchoSvcConfig, error) {
if db == nil {
return nil, nil
}
rows := make([]*EchoSvcConfig, 0)
if err := db.Find(TableEchomeetSvcConfig, &rows, ""); err != nil {
return nil, err
}
return rows, nil
}

10
apps/services/comm/moduleconfig.go

@ -1,6 +1,9 @@
package comm
import "yunyan/lego/sys/mysql"
import (
"yunyan/lego/sys/log"
"yunyan/lego/sys/mysql"
)
// 应用「业务功能配置」托管(登录 / 短信 / 邮件 / 支付…)。
//
@ -213,6 +216,11 @@ func LoadOrSeedModuleConfig(db mysql.ISys, encKey string, sys map[string]map[str
if r.Encrypted && val != "" {
if plain, e := Decrypt(encKey, val); e == nil {
val = plain
} else {
// 仍沿用密文(保持既有行为),但必须留痕:否则密文会被当明文送去第三方
// (如把密文当 Resend API Key 发出 → 401 API key is invalid),
// 而 console 后台密钥字段脱敏显示"已配置",几乎无从排查。
log.Errorf("[ModuleConfig] 密钥字段解密失败 module=%s key=%s err=%v —— 检查本服务 ${FIELD_ENCRYPT_KEY} 是否与 console 完全一致", r.Module, r.Key, e)
}
}
sec[r.Key] = coerceVal(val, sec[r.Key])

1
apps/services/comm/nats.go

@ -37,6 +37,7 @@ const (
ConfigKindServiceConfig = "service_config" // 应用×服务基础设施/运行配置(后台改后广播;DSN 等多需重启才生效)
ConfigKindAgent = "agent" // yunyan 自管 Agent 配置(后台改后广播;下发客户端)
ConfigKindModuleConfig = "module_config" // 应用业务功能配置(登录/短信/邮件/支付;存业务库,后台改后广播、业务侧重载)
ConfigKindEchomeetSvc = "echomeet_svc" // 会议记录服务配置(识别/翻译/总结优先级+凭据;存业务库,后台改后广播;改配置重启生效)
)
// ConfigChangedEvent console→业务 的「配置已变更」广播事件。

5
apps/services/comm/svcruntime.go

@ -7,6 +7,7 @@ import (
"strconv"
"strings"
"yunyan/lego/sys/log"
"yunyan/lego/sys/postgres"
)
@ -140,6 +141,10 @@ func LoadOrSeedServiceConfig(encKey, app, region, service string, sys map[string
if r.Encrypted {
if plain, e := Decrypt(encKey, r.Value); e == nil {
val = plain
} else {
// 仍沿用密文(保持既有行为),但必须留痕:否则密文会被当明文用作 DSN/密码/token,
// 表现为连不上或鉴权失败,而后台显示"已配置",无从排查。
log.Errorf("[SvcRuntime] 密钥字段解密失败 section=%s key=%s err=%v —— 检查本服务 ${FIELD_ENCRYPT_KEY} 是否与 console 完全一致", r.Section, r.Key, e)
}
}
sec[r.Key] = coerceVal(val, sec[r.Key])

218
apps/services/modules/console/api_echomeetsvc.go

@ -0,0 +1,218 @@
package console
import (
"strings"
"time"
"yunyan/comm"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
// ============================ 会议记录服务配置 (echomeetsvc) ============================
//
// admin「会议记录服务配置」页的后端:读写**应用业务库**里的 echomeet_svc_config 表
// (识别/翻译/总结三类,各自按优先级;识别可配支持语言;总结为具体模型条目)。全局一份、无 app_name。
//
// 业务库连接按部署 appId 从 registry 动态取(与 modulecfg 同机制);
// 密钥字段(是否密钥以 comm.EchoSvcCatalog 为准)AES-GCM 加密落库、返回前端脱敏为 comm.EncMask。
// 改配置后广播 ConfigKindEchomeetSvc;home 侧 echomeet 改配置需重启生效。
// echoSvcCatalog 返回会议记录服务字段目录(前端据此渲染各服务商表单、识别语言选择器)。与选中应用无关。
func (this *serverComp) echoSvcCatalog(c *gin.Context) {
writeOK(c, gin.H{
"catalog": comm.EchoSvcCatalog,
"categories": []gin.H{
{"value": comm.EchoSvcCatASR, "label": "识别"},
{"value": comm.EchoSvcCatMT, "label": "翻译"},
{"value": comm.EchoSvcCatLLM, "label": "总结"},
},
})
}
// echoSvcList 列出某部署业务库里的会议记录服务配置;密钥脱敏。
func (this *serverComp) echoSvcList(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
}
_ = c.ShouldBindJSON(&req)
if req.AppId == 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 必填")
return
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
if err := comm.EnsureEchoSvcTable(db); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
rows, err := comm.LoadEchoSvcConfigs(db)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
for _, r := range rows {
r.Values = comm.MaskEchoValues(r.Category, r.Provider, r.Values)
}
writeOK(c, gin.H{"items": rows})
}
// echoSvcSave 保存单条服务配置:新增/更新(按 id)。
// 是否密钥以目录为准(不信前端);密钥字段回传占位/空则保留原密文,否则加密新明文;目录外的键忽略。
func (this *serverComp) echoSvcSave(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Config comm.EchoSvcConfig `json:"config"`
}
if err := c.ShouldBindJSON(&req); err != nil {
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
return
}
cfg := req.Config
cfg.Id = strings.TrimSpace(cfg.Id)
cfg.Provider = strings.TrimSpace(cfg.Provider)
if req.AppId == 0 || cfg.Id == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 与 id 必填")
return
}
def := comm.FindEchoSvcDef(cfg.Category, cfg.Provider)
if def == nil {
writeErr(c, pb.ErrorCode_ReqParameterError, "未知的服务类别/服务商组合")
return
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
if err := comm.EnsureEchoSvcTable(db); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
// 取旧行以保留密钥密文。
old := &comm.EchoSvcConfig{}
hasOld := db.FindOne(comm.TableEchomeetSvcConfig, old, "id=?", cfg.Id) == nil && old.Id != ""
// 合法字段过滤 + 密钥处理。
allowed := make(map[string]bool, len(def.Fields))
for _, f := range def.Fields {
allowed[f.Key] = true
}
encKey := this.options.EncryptKey
stored := make(map[string]string, len(cfg.Values))
for k, v := range cfg.Values {
if !allowed[k] {
continue
}
if comm.EchoSvcFieldIsSecret(cfg.Category, cfg.Provider, k) {
if v == "" || v == comm.EncMask {
if hasOld {
stored[k] = old.Values[k] // 保留原密文
}
continue
}
ct, ee := comm.Encrypt(encKey, v)
if ee != nil {
writeErr(c, pb.ErrorCode_SystemError, "加密失败: "+ee.Error())
return
}
stored[k] = ct
continue
}
stored[k] = v
}
cfg.Values = stored
now := time.Now().Unix()
cfg.Updatetime = now
if hasOld {
cfg.Createtime = old.Createtime
if e := db.Save(comm.TableEchomeetSvcConfig, &cfg); e != nil {
writeErr(c, pb.ErrorCode_DBError, e.Error())
return
}
} else {
cfg.Createtime = now
if e := db.Insert(comm.TableEchomeetSvcConfig, &cfg); e != nil {
writeErr(c, pb.ErrorCode_DBError, e.Error())
return
}
}
broadcastConfigChanged(comm.ConfigKindEchomeetSvc, "update", 0)
writeOK(c, gin.H{"id": cfg.Id})
}
// echoSvcDel 删除一条服务配置。
func (this *serverComp) echoSvcDel(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Id string `json:"id"`
}
_ = c.ShouldBindJSON(&req)
req.Id = strings.TrimSpace(req.Id)
if req.AppId == 0 || req.Id == "" {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 与 id 必填")
return
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
if err := db.Delete(comm.TableEchomeetSvcConfig, "id=?", req.Id); err != nil {
writeErr(c, pb.ErrorCode_DBError, err.Error())
return
}
broadcastConfigChanged(comm.ConfigKindEchomeetSvc, "delete", 0)
writeOK(c, gin.H{"deleted": req.Id})
}
// echoSvcReorder 批量调整同类内优先级(拖拽排序后提交)。
func (this *serverComp) echoSvcReorder(c *gin.Context) {
var req struct {
AppId uint32 `json:"app_id"`
Items []struct {
Id string `json:"id"`
Priority int32 `json:"priority"`
} `json:"items"`
}
if err := c.ShouldBindJSON(&req); err != nil {
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
return
}
if req.AppId == 0 {
writeErr(c, pb.ErrorCode_ReqParameterError, "app_id 必填")
return
}
db, err := this.module.registry.getServiceDB(req.AppId)
if err != nil {
writeErr(c, pb.ErrorCode_DBError, "连接应用业务库失败: "+err.Error())
return
}
now := time.Now().Unix()
updated := 0
for _, it := range req.Items {
id := strings.TrimSpace(it.Id)
if id == "" {
continue
}
row := &comm.EchoSvcConfig{}
if e := db.FindOne(comm.TableEchomeetSvcConfig, row, "id=?", id); e != nil || row.Id == "" {
continue
}
row.Priority = it.Priority
row.Updatetime = now
if e := db.Save(comm.TableEchomeetSvcConfig, row); e != nil {
writeErr(c, pb.ErrorCode_DBError, e.Error())
return
}
updated++
}
broadcastConfigChanged(comm.ConfigKindEchomeetSvc, "update", 0)
writeOK(c, gin.H{"updated": updated})
}

4
apps/services/modules/console/model_moduleconfig.go

@ -27,7 +27,9 @@ func listModuleConfig(db mysql.ISys, module string) (rows []*comm.AppModuleConfi
// getModuleConfigItem 按 (module,key) 取一项(更新时保留旧密文/建时间)。不存在返回 (nil,nil)。
func getModuleConfigItem(db mysql.ISys, module, key string) (*comm.AppModuleConfig, error) {
m := &comm.AppModuleConfig{}
err := db.FindOne(comm.TableAppModuleConfig, m, "module=? AND key=?", module, key)
// 用 map 条件而非原始字符串:"key" 是 MySQL 保留字,raw where 里 `key=?` 会触发 1064;
// map 条件让 gorm 按方言自动给列名加引号(MySQL 反引号 / PG 双引号),跨库安全。
err := db.FindOne(comm.TableAppModuleConfig, m, map[string]interface{}{"module": module, "key": key})
if err == mysql.ErrNoDocuments {
return nil, nil
}

6
apps/services/modules/console/server.go

@ -116,6 +116,12 @@ func (this *serverComp) routes(eng *gin.Engine) {
apps.POST("/modulecfg/list", this.moduleCfgList)
apps.POST("/modulecfg/save", this.moduleCfgSave)
apps.POST("/modulecfg/del", this.moduleCfgDel)
// 会议记录服务配置(识别/翻译/总结优先级 + 凭据 + 识别语言 + 总结模型清单;存应用业务库)。
apps.POST("/echomeetsvc/catalog", this.echoSvcCatalog)
apps.POST("/echomeetsvc/list", this.echoSvcList)
apps.POST("/echomeetsvc/save", this.echoSvcSave)
apps.POST("/echomeetsvc/del", this.echoSvcDel)
apps.POST("/echomeetsvc/reorder", this.echoSvcReorder)
// 部署环境(区域)管理:与应用注册同权限(超管 + 管理员)。
apps.POST("/envs/list", this.envList)
apps.POST("/envs/test", this.envTest)

22
apps/services/modules/echomeet/CLAUDE.md

@ -8,24 +8,30 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`echomeet` 是「会议记录」模块:上传音频 → **转写(Transcribe)** → **翻译(Translate)** → **AI 总结(Summary)**,产出 `overview`(短总结)+ `summary`(按模板的完整纪要)。由 `home` 服务装载运行([services/home/main.go](../../services/home/main.go)),不是独立服务。HTTP 接口经反射注册为 `echomeet_<方法名小写>`,前缀路由示例 `/api/home/echomeet_addrecord`。
## 核心设计:一会一服务商,三段同源
## 核心设计:识别/翻译/总结三类各自按优先级选服务(后台配置驱动)
每条会议记录在 [providers.go](providers.go) 的 `ResolveServiceType` 解析出**唯一服务商**,转写/翻译/总结三步都走它,结果记在 `record.ServiceType`。解析优先级:
1. 请求显式 `service_type` →
2. 模块配置 `DefaultService` →
3. 兜底按源语言:字节支持该语言走字节,否则阿里。
> 旧设计是「一会一服务商,三段同源」(`ResolveServiceType` 定一个服务商三步同走)。现已改为**三类各自独立按优先级选择**,配置全部搬进管理后台。旧的 `ResolveServiceType` 仅作兜底保留。
四套服务商,每套三件套(见对应 `provider_*.go`):
| ServiceType | 转写 | 翻译 | 总结(Summarizer) |
|---|---|---|---|
| ByteDance | `sys/bytedance/audomodel` | 火山翻译 | 豆包 `sys/doubao` |
| Ali | DashScope `sys/aliyun/filetrans` | 阿里 MT | 通义千问(`sys/openai` 兼容端点)|
| 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),运行期 `GetTranscriber/GetTranslator/GetSummarizer(svcType, language)` 按 ServiceType 路由。新增服务商 = 写一个 `provider_xxx.go` 实现三接口 + 在 `providersComp.Init` 注册进三张 map。
三类能力抽象为 `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 映射。
## 组件与异步流水线

30
apps/services/modules/echomeet/api_getsummarymodels.go

@ -0,0 +1,30 @@
package echomeet
import (
"yunyan/comm"
"yunyan/pb"
)
// @Summary 获取可选的总结文本大模型清单
// @Description 返回后台配置的、按优先级排序的启用总结模型条目,供客户端在发起转写/重新总结时选择(summary_model)。
// @Tags Echomeet
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param user body pb.EchomeetGetSummaryModelsReq true "请求数据"
// @Success 200 {object} comm.HttpResult{data=pb.EchomeetGetSummaryModelsResp} "响应数据"
// @Router /api/home/echomeet_getsummarymodels [post]
func (this *apiComp) GetSummaryModels(session comm.IUserSession, req *pb.EchomeetGetSummaryModelsReq) (resp *pb.EchomeetGetSummaryModelsResp, errdata *pb.ErrorData) {
models := this.module.providers.ListSummaryModels()
out := make([]*pb.SummaryModelInfo, 0, len(models))
for i, m := range models {
out = append(out, &pb.SummaryModelInfo{
Id: m.Id,
Name: m.Name,
Provider: m.Provider,
IsDefault: i == 0,
})
}
resp = &pb.EchomeetGetSummaryModelsResp{Models: out}
return
}

15
apps/services/modules/echomeet/api_starttask.go

@ -106,9 +106,18 @@ func (this *apiComp) StartTask(session comm.IUserSession, req *pb.EchomeetStartT
model.Isdistinguishspeaker = req.Isdistinguishspeaker
model.Formlanguage = req.Formlanguage
model.Tolanguage = req.Tolanguage
// 选定服务商:请求显式指定优先,其次配置默认,最后按语言自动路由;
// 一经确定,撰写/翻译/总结全部走同一服务商。
model.ServiceType = this.module.providers.ResolveServiceType(req.ServiceType, model.Formlanguage)
// 选定识别服务:请求显式指定优先;否则按「后台配置的识别优先级 + 支持语言」自动选择
// (跳过不支持源语言的服务,修复西语被强路由到不支持西语的阿里的问题)。翻译在 TranslateProcess 内按优先级独立选择。
if req.ServiceType != pb.TranscribeService_TranscribeService_Auto && req.ServiceType != pb.TranscribeService_TranscribeService_Unknown {
model.ServiceType = this.module.providers.ResolveServiceType(req.ServiceType, model.Formlanguage)
} else {
model.ServiceType = this.module.providers.selectASR(model.Formlanguage)
}
// 总结所用文本大模型:客户端选择优先,留空则用后台配置的默认(优先级最高的启用条目)。
model.SummaryModel = req.SummaryModel
if model.SummaryModel == "" {
model.SummaryModel = this.module.providers.DefaultSummaryId()
}
this.module.model.saverecord(model)
this.module.Infof("StartTask id:%d uid:%s formlang:%s tolang:%s seconds:%d size:%d distinguishSpeaker:%v service:%s",

8
apps/services/modules/echomeet/api_summary.go

@ -62,7 +62,13 @@ func (this *apiComp) Summary(session comm.IUserSession, req *pb.EchomeetSummaryR
svc = model.ServiceType
}
model.ServiceType = this.module.providers.ResolveServiceType(svc, model.Formlanguage)
this.module.Infof("Summary id:%d → service:%s", model.Id, ServiceTypeName(model.ServiceType))
// 总结所用文本大模型:客户端显式指定则更新,否则沿用记录已有(为空再回退默认)。
if req.SummaryModel != "" {
model.SummaryModel = req.SummaryModel
} else if model.SummaryModel == "" {
model.SummaryModel = this.module.providers.DefaultSummaryId()
}
this.module.Infof("Summary id:%d → service:%s summaryModel:%s", model.Id, ServiceTypeName(model.ServiceType), model.SummaryModel)
// 是否需要重新翻译:
// - 客户端显式传入 tolanguage(重新总结操作,强制重译以保证 Translate 与 Tolanguage 一致);或
// - 目标语言变化;或

13
apps/services/modules/echomeet/providers.go

@ -2,6 +2,7 @@ package echomeet
import (
"context"
"yunyan/comm"
"yunyan/lego/core"
"yunyan/lego/core/cbase"
"yunyan/pb"
@ -20,6 +21,14 @@ type providersComp struct {
transcribers map[pb.TranscribeService]Transcriber
translators map[pb.TranscribeService]Translator
summarizers map[pb.TranscribeService]Summarizer
// 会议记录服务配置(DB 驱动,启动时从 yaml seed 进应用业务库,改配置重启生效)。
// 识别/翻译各维护一份按优先级排序的启用清单,识别叠加「支持语言」过滤。见 svcconfig.go。
svcLoaded bool
asrOrder []*comm.EchoSvcConfig // 识别:enabled,priority 升序
mtOrder []*comm.EchoSvcConfig // 翻译:enabled,priority 升序
llmOrder []*comm.EchoSvcConfig // 总结:enabled,priority 升序(第一个为默认)
summarizersById map[string]Summarizer // 总结模型实例,key=条目 id(客户端按 id 选择)
}
// 任务状态常量,对齐底层 sys 包
@ -117,6 +126,10 @@ func (this *providersComp) Init(service core.IService, module core.IModule, comp
pb.TranscribeService_TranscribeService_Microsoft: msSummarizer,
}
this.module.Infof("echomeet providers 初始化完成 DefaultService=%s", opt.DefaultService)
// 会议记录服务配置:从 yaml seed 进应用业务库(幂等),并加载识别/翻译的优先级清单。
// best-effort:失败不阻断启动,选择逻辑会回退到旧的 ResolveServiceType。
this.loadOrSeedSvcConfig(opt)
return
}

466
apps/services/modules/echomeet/svcconfig.go

@ -0,0 +1,466 @@
package echomeet
import (
"yunyan/comm"
"yunyan/lego/sys/log"
"yunyan/lego/sys/mysql"
"yunyan/pb"
"yunyan/sys/doubao"
"yunyan/sys/google/gemini"
"yunyan/sys/openai"
"fmt"
"os"
"sort"
"strings"
"time"
)
// 会议记录服务配置的运行时消费(数据模型见 comm/echomeetsvc.go)。
//
// 启动流程(providersComp.Init 末尾调用 loadOrSeedSvcConfig):
// 1. 建表(应用业务库,全局一份);
// 2. 库空 → 把当前 yaml(Options) 的四套服务商 seed 成识别/翻译/总结行(密钥加密落库);
// 3. 加载识别/翻译两类的启用清单,按 priority 升序,供选择逻辑使用。
//
// 选择逻辑:
// - 识别 selectASR(lang):按优先级取第一个「支持该语言」的服务(支持语言以库里 SupportLangs 为准,
// 为空则回退各 Transcriber.SupportsLanguage)——这修复了「西语被强路由到不支持西语的阿里 paraformer-v2」。
// - 翻译 selectMT(from,to):按优先级取第一个启用的服务。
//
// 凭据托管:seed 会把 yaml 里的 AK/SK/Token 等一并写入库(密钥字段 AES 落库),
// 便于后台面板管理;SDK 侧改读库值将在后续阶段接入(当前仍由 yaml 初始化,行为不变)。
// paraformerV2Langs 阿里 DashScope paraformer-v2 录音文件识别真实支持的语种(BCP-47)。
// 依据阿里官方文档:中文(及方言)/英文/日语/韩语/德语/法语/俄语——**不含西班牙语等**。
// 作为阿里识别行 SupportLangs 的 seed 默认值;用户可在后台面板增删。
var paraformerV2Langs = []string{
"zh-CN", "zh-TW", "zh-HK", "yue-HK",
"en-US", "en-GB", "ja-JP", "ko-KR",
"de-DE", "fr-FR", "ru-RU", "ru-UA",
}
// providerToEnum 把库里的 provider 字符串(识别/翻译类)映射回 TranscribeService 枚举。
func providerToEnum(provider string) pb.TranscribeService {
switch provider {
case "bytedance":
return pb.TranscribeService_TranscribeService_ByteDance
case "ali":
return pb.TranscribeService_TranscribeService_Ali
case "google":
return pb.TranscribeService_TranscribeService_Google
case "microsoft":
return pb.TranscribeService_TranscribeService_Microsoft
default:
return pb.TranscribeService_TranscribeService_Unknown
}
}
// loadOrSeedSvcConfig 建表 + 库空则 seed + 加载识别/翻译优先级清单。best-effort,失败仅记录日志。
func (this *providersComp) loadOrSeedSvcConfig(opt *Options) {
db := mysql.GetSys()
if db == nil {
return
}
if err := comm.EnsureEchoSvcTable(db); err != nil {
this.module.Errorf("echomeet svcconfig 建表失败 err:%v", err)
return
}
rows, err := comm.LoadEchoSvcConfigs(db)
if err != nil {
this.module.Errorf("echomeet svcconfig 读取失败 err:%v", err)
return
}
if len(rows) == 0 {
if rows, err = this.seedSvcConfig(db, opt); err != nil {
this.module.Errorf("echomeet svcconfig seed 失败 err:%v", err)
return
}
this.module.Infof("echomeet svcconfig 首次 seed 完成 rows:%d", len(rows))
}
// 统一把 Values 解密为明文供运行时消费(seed 返回的本就是明文,Decrypt 对明文会失败并保留原值,安全)。
encKey := os.Getenv("FIELD_ENCRYPT_KEY")
for _, r := range rows {
r.Values = comm.DecryptEchoValues(encKey, r.Category, r.Provider, r.Values)
}
// 分类归集 + 优先级排序(仅启用项)。
asr := make([]*comm.EchoSvcConfig, 0)
mt := make([]*comm.EchoSvcConfig, 0)
llm := make([]*comm.EchoSvcConfig, 0)
for _, r := range rows {
if !r.Enabled {
continue
}
switch r.Category {
case comm.EchoSvcCatASR:
asr = append(asr, r)
case comm.EchoSvcCatMT:
mt = append(mt, r)
case comm.EchoSvcCatLLM:
llm = append(llm, r)
}
}
sort.SliceStable(asr, func(i, j int) bool { return asr[i].Priority < asr[j].Priority })
sort.SliceStable(mt, func(i, j int) bool { return mt[i].Priority < mt[j].Priority })
sort.SliceStable(llm, func(i, j int) bool { return llm[i].Priority < llm[j].Priority })
this.asrOrder = asr
this.mtOrder = mt
this.llmOrder = llm
// 用库值(凭据来自库)重建各服务商的转写/翻译/总结实例;总结按条目建实例支持一商多模型。
this.buildProvidersFromDB(rows, opt)
this.svcLoaded = true
this.module.Infof("echomeet svcconfig 加载完成 识别:%d 翻译:%d 总结:%d", len(asr), len(mt), len(llm))
}
// buildProvidersFromDB 用库里的(已解密)配置重建 provider:
// - 识别/翻译:逐服务商用库值重新初始化底层 sys(凭据托管到后台的落点),覆盖 enum→provider 映射;某项缺行则回退 yaml。
// - 总结:每条启用的 LLM 行建一个模型实例(key=条目 id),客户端按 id 选择;同时按源服务商回填 enum→summarizer 保持旧调用兼容。
func (this *providersComp) buildProvidersFromDB(rows []*comm.EchoSvcConfig, opt *Options) {
byId := make(map[string]*comm.EchoSvcConfig, len(rows))
for _, r := range rows {
byId[r.Id] = r
}
type provSeed struct {
name string
enum pb.TranscribeService
po ProviderOptions
}
provs := []provSeed{
{"bytedance", pb.TranscribeService_TranscribeService_ByteDance, opt.ByteDance},
{"ali", pb.TranscribeService_TranscribeService_Ali, opt.Ali},
{"google", pb.TranscribeService_TranscribeService_Google, opt.Google},
{"microsoft", pb.TranscribeService_TranscribeService_Microsoft, opt.Microsoft},
}
for _, p := range provs {
po := p.po // 缺行回退 yaml
if r := byId["asr_"+p.name]; r != nil {
po.Transcribe = toIfaceMap(r.Values)
if cb := r.Values["Callback"]; cb != "" {
po.Callback = cb
}
}
if r := byId["mt_"+p.name]; r != nil {
po.Translate = toIfaceMap(r.Values)
}
if r := byId["llm_"+p.name]; r != nil {
po.Summary = summaryFromValues(r.Provider, r.Values)
}
t, tr, sm := buildProviderSet(p.name, po)
if t != nil {
this.transcribers[p.enum] = t
}
if tr != nil {
this.translators[p.enum] = tr
}
if sm != nil {
this.summarizers[p.enum] = sm
}
}
// 总结模型实例(一个服务商可多条不同模型)。
this.summarizersById = make(map[string]Summarizer)
for _, r := range rows {
if r.Category != comm.EchoSvcCatLLM || !r.Enabled {
continue
}
if s := buildSummarizer(r.Provider, r.Values); s != nil {
this.summarizersById[r.Id] = s
}
}
}
// buildProviderSet 按服务商名调对应的成套初始化(从 DB 值构造的 ProviderOptions)。
func buildProviderSet(name string, po ProviderOptions) (Transcriber, Translator, Summarizer) {
switch name {
case "bytedance":
return newByteDanceProviders(po)
case "ali":
return newAliProviders(po)
case "google":
return newGoogleProviders(po)
case "microsoft":
return newMicrosoftProviders(po)
}
return nil, nil, nil
}
// buildSummarizer 按总结条目的 provider(doubao/openai/gemini) + 值构造一个总结模型实例。
func buildSummarizer(provider string, v map[string]string) Summarizer {
switch provider {
case "doubao":
sys, err := doubao.NewSys(doubao.SetApikey(v["Apikey"]), doubao.SetModel(v["Model"]))
if err != nil {
log.Errorf("echomeet buildSummarizer doubao err:%v", err)
return nil
}
return &doubaoSummarizer{sys: sys}
case "openai":
sys, err := openai.NewSys(openai.SetBaseURL(v["BaseURL"]), openai.SetToken(v["Token"]), openai.SetModel(v["Model"]), openai.SetMaxfunccall(1))
if err != nil {
log.Errorf("echomeet buildSummarizer openai err:%v", err)
return nil
}
return &aliSummarizer{sys: sys} // aliSummarizer 是通用 OpenAI 兼容端点包装
case "gemini":
sys, err := gemini.NewSys(gemini.SetApikey(v["Apikey"]), gemini.SetModel(v["Model"]), gemini.SetEndpoint(v["Endpoint"]))
if err != nil {
log.Errorf("echomeet buildSummarizer gemini err:%v", err)
return nil
}
return &geminiSummarizer{sys: sys}
}
return nil
}
// summaryFromValues 把库 Values 还原成 SummaryOptions(summaryToValues 的逆过程)。
func summaryFromValues(provider string, v map[string]string) SummaryOptions {
switch provider {
case "doubao":
return SummaryOptions{Apikey: v["Apikey"], Model: v["Model"]}
case "gemini":
return SummaryOptions{Apikey: v["Apikey"], Model: v["Model"], Endpoint: v["Endpoint"]}
case "openai":
return SummaryOptions{BaseURL: v["BaseURL"], Token: v["Token"], Model: v["Model"]}
}
return SummaryOptions{}
}
// toIfaceMap 把 map[string]string 转成底层 sys OnInit 需要的 map[string]interface{}。
func toIfaceMap(m map[string]string) map[string]interface{} {
out := make(map[string]interface{}, len(m))
for k, v := range m {
out[k] = v
}
return out
}
// GetSummarizerById 按条目 id 取总结模型实例;空或未命中回退优先级最高的启用条目。
func (this *providersComp) GetSummarizerById(id string) Summarizer {
if id != "" && this.summarizersById != nil {
if s, ok := this.summarizersById[id]; ok && s != nil {
return s
}
}
if len(this.llmOrder) > 0 && this.summarizersById != nil {
return this.summarizersById[this.llmOrder[0].Id]
}
return nil
}
// DefaultSummaryId 返回优先级最高的启用总结条目 id(无则空)。
func (this *providersComp) DefaultSummaryId() string {
if len(this.llmOrder) > 0 {
return this.llmOrder[0].Id
}
return ""
}
// ListSummaryModels 返回按优先级排序的启用总结条目(供客户端选择)。
func (this *providersComp) ListSummaryModels() []*comm.EchoSvcConfig {
return this.llmOrder
}
// seedSvcConfig 把 yaml(Options) 的四套服务商映射为识别/翻译/总结行并落库;返回落库后的行(Values 为明文,供内存直接用)。
func (this *providersComp) seedSvcConfig(db mysql.ISys, opt *Options) ([]*comm.EchoSvcConfig, error) {
encKey := os.Getenv("FIELD_ENCRYPT_KEY")
now := time.Now().Unix()
// provider 顺序即默认优先级(字节→阿里→Google→微软,留间隔便于后台调序)。
type provSeed struct {
provider string
prio int32
po ProviderOptions
}
provs := []provSeed{
{"bytedance", 10, opt.ByteDance},
{"ali", 20, opt.Ali},
{"google", 30, opt.Google},
{"microsoft", 40, opt.Microsoft},
}
rows := make([]*comm.EchoSvcConfig, 0, len(provs)*3)
add := func(r *comm.EchoSvcConfig) {
r.Createtime, r.Updatetime = now, now
rows = append(rows, r)
}
for _, p := range provs {
// —— 识别 ——
asrVals := mapToStrings(p.po.Transcribe)
if p.po.Callback != "" {
asrVals["Callback"] = p.po.Callback
}
add(&comm.EchoSvcConfig{
Id: "asr_" + p.provider, Category: comm.EchoSvcCatASR, Provider: p.provider,
Name: providerDisplayName(comm.EchoSvcCatASR, p.provider), Priority: p.prio,
Enabled: hasCreds(asrVals), SupportLangs: seedSupportLangs(p.provider), Values: asrVals,
})
// —— 翻译 ——
mtVals := mapToStrings(p.po.Translate)
add(&comm.EchoSvcConfig{
Id: "mt_" + p.provider, Category: comm.EchoSvcCatMT, Provider: p.provider,
Name: providerDisplayName(comm.EchoSvcCatMT, p.provider), Priority: p.prio,
Enabled: hasCreds(mtVals), Values: mtVals,
})
// —— 总结(文本大模型)——
if llmProvider, llmVals := summaryToValues(p.provider, p.po.Summary); llmVals != nil {
add(&comm.EchoSvcConfig{
Id: "llm_" + p.provider, Category: comm.EchoSvcCatLLM, Provider: llmProvider,
Name: summaryDisplayName(p.provider, p.po.Summary.Model), Priority: p.prio,
Enabled: p.po.Summary.Model != "", Values: llmVals,
})
}
}
// 落库:密钥字段加密。内存返回明文副本。
for _, r := range rows {
enc := *r
enc.Values = comm.EncryptEchoValues(encKey, r.Category, r.Provider, r.Values)
if err := db.Insert(comm.TableEchomeetSvcConfig, &enc); err != nil {
return nil, err
}
}
return rows, nil
}
// selectASR 按优先级 + 支持语言选识别服务;无命中回退旧的 ResolveServiceType。
func (this *providersComp) selectASR(lang string) pb.TranscribeService {
if this.svcLoaded {
for _, r := range this.asrOrder {
if this.asrSupportsLang(r, lang) {
if svc := providerToEnum(r.Provider); svc != pb.TranscribeService_TranscribeService_Unknown {
return svc
}
}
}
}
// 回退:沿用旧的按语言自动路由(字节支持则字节,否则阿里)。
return this.ResolveServiceType(pb.TranscribeService_TranscribeService_Auto, lang)
}
// selectMT 按优先级选翻译服务;无命中回退到给定识别服务(保持三段同源的旧行为)。
func (this *providersComp) selectMT(from, to string, fallback pb.TranscribeService) pb.TranscribeService {
if this.svcLoaded {
for _, r := range this.mtOrder {
if svc := providerToEnum(r.Provider); svc != pb.TranscribeService_TranscribeService_Unknown {
if _, ok := this.translators[svc]; ok {
return svc
}
}
}
}
return fallback
}
// asrSupportsLang 判断某识别行是否支持该语言:库里 SupportLangs 非空以其为准,否则回退 Transcriber.SupportsLanguage。
func (this *providersComp) asrSupportsLang(r *comm.EchoSvcConfig, lang string) bool {
if strings.TrimSpace(r.SupportLangs) != "" {
return langInCSV(r.SupportLangs, lang)
}
if t, ok := this.transcribers[providerToEnum(r.Provider)]; ok && t != nil {
return t.SupportsLanguage(lang)
}
return false
}
// ==================== seed 辅助 ====================
// mapToStrings 把 yaml 的 map[string]interface{} 值统一转成字符串。
func mapToStrings(m map[string]interface{}) map[string]string {
out := make(map[string]string, len(m))
for k, v := range m {
if v == nil {
continue
}
if s, ok := v.(string); ok {
out[k] = s
} else {
out[k] = fmt.Sprintf("%v", v)
}
}
return out
}
// summaryToValues 把结构化的 SummaryOptions 转成库 Values,并返回该总结条目的 provider(doubao/openai/gemini)。
func summaryToValues(sourceProvider string, s SummaryOptions) (provider string, values map[string]string) {
switch sourceProvider {
case "bytedance":
return "doubao", map[string]string{"Apikey": s.Apikey, "Model": s.Model}
case "google":
return "gemini", map[string]string{"Apikey": s.Apikey, "Model": s.Model, "Endpoint": s.Endpoint}
case "ali", "microsoft":
return "openai", map[string]string{"BaseURL": s.BaseURL, "Token": s.Token, "Model": s.Model}
}
return "", nil
}
// hasCreds 判断一组配置是否有实质凭据(任一非空即视为已配)。
func hasCreds(vals map[string]string) bool {
for k, v := range vals {
if k == "Region" || k == "Endpoint" || k == "Callback" {
continue // 这些是环境/地址项,不算凭据
}
if strings.TrimSpace(v) != "" {
return true
}
}
return false
}
// seedSupportLangs 识别行的支持语言默认 seed 值。
func seedSupportLangs(provider string) string {
switch provider {
case "ali":
// 阿里 paraformer-v2 真实支持集(不含西语等)——从根上避免西语被路由到阿里后转写空。
return strings.Join(paraformerV2Langs, ",")
case "bytedance":
return strings.Join(byteDanceSupportLangs(), ",")
default:
return "" // Google/微软支持较广,留空表示按代码默认(SupportsLanguage=true)
}
}
// byteDanceSupportLangs 从 bcp47ToByteDance 收集字节支持的 BCP-47 语种。
func byteDanceSupportLangs() []string {
out := make([]string, 0, len(bcp47ToByteDance))
for k := range bcp47ToByteDance {
if k != "" {
out = append(out, k)
}
}
sort.Strings(out)
return out
}
// langInCSV 判断 lang 是否在逗号分隔集合内(精确匹配,或按基础子标签匹配,如 es 命中 es-ES)。
func langInCSV(csv, lang string) bool {
if lang == "" {
return false
}
base := baseLang(lang)
for _, item := range strings.Split(csv, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if strings.EqualFold(item, lang) || strings.EqualFold(baseLang(item), base) {
return true
}
}
return false
}
func providerDisplayName(category int32, provider string) string {
if def := comm.FindEchoSvcDef(category, provider); def != nil {
return def.Name
}
return provider
}
func summaryDisplayName(sourceProvider, model string) string {
name := providerDisplayName(comm.EchoSvcCatASR, sourceProvider)
if model != "" {
return name + "(" + model + ")"
}
return name
}

24
apps/services/modules/echomeet/tasks.go

@ -350,7 +350,9 @@ func (this *tasksComp) TranslateProcess(rec *pb.DBEchoMeetRecord, contexts []*pb
for _, item := range contexts {
tests = append(tests, item.Content)
}
translator, err := this.module.providers.GetTranslator(rec.ServiceType, rec.Formlanguage)
// 翻译服务按「后台配置的翻译优先级」独立选择;无配置时回退到识别所用服务商(旧的三段同源行为)。
mtSvc := this.module.providers.selectMT(rec.Formlanguage, rec.Tolanguage, rec.ServiceType)
translator, err := this.module.providers.GetTranslator(mtSvc, rec.Formlanguage)
if err != nil {
this.module.Errorf("TranslateProcess id:%d 取 translator 失败 err:%v", rec.Id, err)
return err
@ -425,14 +427,18 @@ func (this *tasksComp) AIProcess(task interface{}) {
_ = redissys.Conn().LRem(context.Background(), procKey, 1, fmt.Sprintf("%d", rec.Id)).Err()
return
}
summarizer, err := this.module.providers.GetSummarizer(rec.ServiceType, rec.Formlanguage)
if err != nil {
this.module.Errorf("AIProcess id:%d 取 summarizer 失败 err:%v", rec.Id, err)
rec.State = pb.DBEchoMeetRecordState_SummarizFail
this.module.model.saverecord(rec)
procKey := this.keyAIProc()
_ = redissys.Conn().LRem(context.Background(), procKey, 1, fmt.Sprintf("%d", rec.Id)).Err()
return
// 总结按客户端所选文本大模型条目 id 取实例;空/未命中回退优先级最高的启用条目,再回退旧的按服务商路由。
summarizer := this.module.providers.GetSummarizerById(rec.SummaryModel)
if summarizer == nil {
var serr error
if summarizer, serr = this.module.providers.GetSummarizer(rec.ServiceType, rec.Formlanguage); serr != nil || summarizer == nil {
this.module.Errorf("AIProcess id:%d 取 summarizer 失败 model:%s err:%v", rec.Id, rec.SummaryModel, serr)
rec.State = pb.DBEchoMeetRecordState_SummarizFail
this.module.model.saverecord(rec)
procKey := this.keyAIProc()
_ = redissys.Conn().LRem(context.Background(), procKey, 1, fmt.Sprintf("%d", rec.Id)).Err()
return
}
}
this.module.Infof("AIProcess id:%d → service:%s", rec.Id, ServiceTypeName(summarizer.Name()))
// 注释图片:用户创建记录时填写的 imageurls(JSON 数组字符串),解析后作为多模态内容随

13
apps/services/pb/echomeet_db.pb.go

@ -312,6 +312,7 @@ type DBEchoMeetRecord struct {
Lastquerytime int64 `protobuf:"varint,27,opt,name=lastquerytime,proto3" json:"lastquerytime" gorm:"lastquerytime"` //@go_tags(`gorm:"lastquerytime" json:"lastquerytime"`) 上一次主动查询第三方服务的 unix 秒,用于限制查询频率
Remark string `protobuf:"bytes,28,opt,name=remark,proto3" json:"remark" gorm:"remark"` //@go_tags(`gorm:"remark" json:"remark"`) 用户备注(创建记录时填写,参与AI总结)
Imageurls string `protobuf:"bytes,29,opt,name=imageurls,proto3" json:"imageurls" gorm:"imageurls"` //@go_tags(`gorm:"imageurls" json:"imageurls"`) 注释图片url列表(JSON数组字符串,编码方式同original),作为多模态内容参与AI总结
SummaryModel string `protobuf:"bytes,30,opt,name=summary_model,json=summaryModel,proto3" json:"summary_model" gorm:"summary_model"` //@go_tags(`gorm:"summary_model" json:"summary_model"`) 总结所用文本大模型条目id(对应 echomeet_svc_config),空=按优先级默认
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -549,6 +550,13 @@ func (x *DBEchoMeetRecord) GetImageurls() string {
return ""
}
func (x *DBEchoMeetRecord) GetSummaryModel() string {
if x != nil {
return x.SummaryModel
}
return ""
}
type ContextStruct struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meetingid uint64 `protobuf:"varint,1,opt,name=Meetingid,proto3" json:"meetingid"` //@go_tags(`json:"meetingid"`) 会议id
@ -993,7 +1001,7 @@ const file_echomeet_echomeet_db_proto_rawDesc = "" +
"\aoutline\x18\n" +
" \x01(\tR\aoutline\x12\x1a\n" +
"\btemplate\x18\v \x01(\tR\btemplate\x12\x12\n" +
"\x04sort\x18\f \x01(\x05R\x04sort\"\xfd\x06\n" +
"\x04sort\x18\f \x01(\x05R\x04sort\"\xa2\a\n" +
"\x10DBEchoMeetRecord\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x10\n" +
"\x03uid\x18\x02 \x01(\tR\x03uid\x12,\n" +
@ -1028,7 +1036,8 @@ const file_echomeet_echomeet_db_proto_rawDesc = "" +
"\x0fprocessduration\x18\x1a \x01(\x03R\x0fprocessduration\x12$\n" +
"\rlastquerytime\x18\x1b \x01(\x03R\rlastquerytime\x12\x16\n" +
"\x06remark\x18\x1c \x01(\tR\x06remark\x12\x1c\n" +
"\timageurls\x18\x1d \x01(\tR\timageurls\"\x99\x01\n" +
"\timageurls\x18\x1d \x01(\tR\timageurls\x12#\n" +
"\rsummary_model\x18\x1e \x01(\tR\fsummaryModel\"\x99\x01\n" +
"\rContextStruct\x12\x1c\n" +
"\tMeetingid\x18\x01 \x01(\x04R\tMeetingid\x12\x18\n" +
"\aContent\x18\x02 \x01(\tR\aContent\x12\x1c\n" +

389
apps/services/pb/echomeet_msg.pb.go

@ -975,7 +975,8 @@ type EchomeetStartTaskReq struct {
Isdistinguishspeaker bool `protobuf:"varint,4,opt,name=isdistinguishspeaker,proto3" json:"isdistinguishspeaker"` //@go_tags(`json:"isdistinguishspeaker"`) //是否区分说话人
Templateid uint64 `protobuf:"varint,5,opt,name=templateid,proto3" json:"templateid"` //@go_tags(`json:"templateid"`) //会议记录模版索引id
Tid string `protobuf:"bytes,6,opt,name=tid,proto3" json:"tid"` //@go_tags(`json:"tid"`) //会议记录模板tid
ServiceType TranscribeService `protobuf:"varint,7,opt,name=service_type,json=serviceType,proto3,enum=TranscribeService" json:"service_type"` //@go_tags(`json:"service_type"`) //转写服务类型,留空默认 Auto
ServiceType TranscribeService `protobuf:"varint,7,opt,name=service_type,json=serviceType,proto3,enum=TranscribeService" json:"service_type"` //@go_tags(`json:"service_type"`) //转写服务类型,留空默认 Auto(识别现按后台优先级+语言自动选择)
SummaryModel string `protobuf:"bytes,8,opt,name=summary_model,json=summaryModel,proto3" json:"summary_model"` //@go_tags(`json:"summary_model"`) //总结所用文本大模型条目id(echomeet_getsummarymodels 返回),留空=按优先级默认
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1059,6 +1060,13 @@ func (x *EchomeetStartTaskReq) GetServiceType() TranscribeService {
return TranscribeService_TranscribeService_Unknown
}
func (x *EchomeetStartTaskReq) GetSummaryModel() string {
if x != nil {
return x.SummaryModel
}
return ""
}
type EchomeetStartTaskResp struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@ -1224,6 +1232,7 @@ type EchomeetSummaryReq struct {
Tid string `protobuf:"bytes,6,opt,name=tid,proto3" json:"tid"` //@go_tags(`json:"tid"`) //会议记录模板tid
Tolanguage string `protobuf:"bytes,7,opt,name=tolanguage,proto3" json:"tolanguage"` //@go_tags(`json:"tolanguage"`) 语言
ServiceType TranscribeService `protobuf:"varint,8,opt,name=service_type,json=serviceType,proto3,enum=TranscribeService" json:"service_type"` //@go_tags(`json:"service_type"`) //转写服务类型(用于翻译/总结路由),留空沿用记录已有
SummaryModel string `protobuf:"bytes,9,opt,name=summary_model,json=summaryModel,proto3" json:"summary_model"` //@go_tags(`json:"summary_model"`) //总结所用文本大模型条目id(echomeet_getsummarymodels 返回),留空沿用记录已有
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -1293,6 +1302,13 @@ func (x *EchomeetSummaryReq) GetServiceType() TranscribeService {
return TranscribeService_TranscribeService_Unknown
}
func (x *EchomeetSummaryReq) GetSummaryModel() string {
if x != nil {
return x.SummaryModel
}
return ""
}
type EchomeetSummaryResp struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@ -1329,6 +1345,156 @@ func (*EchomeetSummaryResp) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{25}
}
// 总结文本大模型条目(客户端选择用)
type SummaryModelInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id"` //@go_tags(`json:"id"`) //条目id,回传给 starttask/summary 的 summary_model
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name"` //@go_tags(`json:"name"`) //展示名
Provider string `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider"` //@go_tags(`json:"provider"`) //底层服务商 doubao/openai/gemini
IsDefault bool `protobuf:"varint,4,opt,name=is_default,json=isDefault,proto3" json:"is_default"` //@go_tags(`json:"is_default"`) //是否为默认(优先级最高)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SummaryModelInfo) Reset() {
*x = SummaryModelInfo{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SummaryModelInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SummaryModelInfo) ProtoMessage() {}
func (x *SummaryModelInfo) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[26]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SummaryModelInfo.ProtoReflect.Descriptor instead.
func (*SummaryModelInfo) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{26}
}
func (x *SummaryModelInfo) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *SummaryModelInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *SummaryModelInfo) GetProvider() string {
if x != nil {
return x.Provider
}
return ""
}
func (x *SummaryModelInfo) GetIsDefault() bool {
if x != nil {
return x.IsDefault
}
return false
}
// 拉取可选的总结文本大模型清单
type EchomeetGetSummaryModelsReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EchomeetGetSummaryModelsReq) Reset() {
*x = EchomeetGetSummaryModelsReq{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EchomeetGetSummaryModelsReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EchomeetGetSummaryModelsReq) ProtoMessage() {}
func (x *EchomeetGetSummaryModelsReq) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EchomeetGetSummaryModelsReq.ProtoReflect.Descriptor instead.
func (*EchomeetGetSummaryModelsReq) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{27}
}
type EchomeetGetSummaryModelsResp struct {
state protoimpl.MessageState `protogen:"open.v1"`
Models []*SummaryModelInfo `protobuf:"bytes,1,rep,name=models,proto3" json:"models"` //@go_tags(`json:"models"`) //可选总结模型清单(按优先级)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EchomeetGetSummaryModelsResp) Reset() {
*x = EchomeetGetSummaryModelsResp{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EchomeetGetSummaryModelsResp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EchomeetGetSummaryModelsResp) ProtoMessage() {}
func (x *EchomeetGetSummaryModelsResp) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EchomeetGetSummaryModelsResp.ProtoReflect.Descriptor instead.
func (*EchomeetGetSummaryModelsResp) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{28}
}
func (x *EchomeetGetSummaryModelsResp) GetModels() []*SummaryModelInfo {
if x != nil {
return x.Models
}
return nil
}
// 已阅会议记录
type EchomeetReadRecordReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -1339,7 +1505,7 @@ type EchomeetReadRecordReq struct {
func (x *EchomeetReadRecordReq) Reset() {
*x = EchomeetReadRecordReq{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[26]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1351,7 +1517,7 @@ func (x *EchomeetReadRecordReq) String() string {
func (*EchomeetReadRecordReq) ProtoMessage() {}
func (x *EchomeetReadRecordReq) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[26]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1364,7 +1530,7 @@ func (x *EchomeetReadRecordReq) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetReadRecordReq.ProtoReflect.Descriptor instead.
func (*EchomeetReadRecordReq) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{26}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{29}
}
func (x *EchomeetReadRecordReq) GetId() uint64 {
@ -1382,7 +1548,7 @@ type EchomeetReadRecordResp struct {
func (x *EchomeetReadRecordResp) Reset() {
*x = EchomeetReadRecordResp{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[27]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1394,7 +1560,7 @@ func (x *EchomeetReadRecordResp) String() string {
func (*EchomeetReadRecordResp) ProtoMessage() {}
func (x *EchomeetReadRecordResp) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[27]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1407,7 +1573,7 @@ func (x *EchomeetReadRecordResp) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetReadRecordResp.ProtoReflect.Descriptor instead.
func (*EchomeetReadRecordResp) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{27}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{30}
}
// 修改会议记录
@ -1425,7 +1591,7 @@ type EchomeetModifyRecordsReq struct {
func (x *EchomeetModifyRecordsReq) Reset() {
*x = EchomeetModifyRecordsReq{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[28]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1437,7 +1603,7 @@ func (x *EchomeetModifyRecordsReq) String() string {
func (*EchomeetModifyRecordsReq) ProtoMessage() {}
func (x *EchomeetModifyRecordsReq) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[28]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1450,7 +1616,7 @@ func (x *EchomeetModifyRecordsReq) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetModifyRecordsReq.ProtoReflect.Descriptor instead.
func (*EchomeetModifyRecordsReq) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{28}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{31}
}
func (x *EchomeetModifyRecordsReq) GetId() uint64 {
@ -1504,7 +1670,7 @@ type EchomeetModifyRecordsResp struct {
func (x *EchomeetModifyRecordsResp) Reset() {
*x = EchomeetModifyRecordsResp{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[29]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1516,7 +1682,7 @@ func (x *EchomeetModifyRecordsResp) String() string {
func (*EchomeetModifyRecordsResp) ProtoMessage() {}
func (x *EchomeetModifyRecordsResp) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[29]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1529,7 +1695,7 @@ func (x *EchomeetModifyRecordsResp) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetModifyRecordsResp.ProtoReflect.Descriptor instead.
func (*EchomeetModifyRecordsResp) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{29}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{32}
}
func (x *EchomeetModifyRecordsResp) GetRecord() *DBEchoMeetRecord {
@ -1549,7 +1715,7 @@ type EchomeetDelRecordsReq struct {
func (x *EchomeetDelRecordsReq) Reset() {
*x = EchomeetDelRecordsReq{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[30]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1561,7 +1727,7 @@ func (x *EchomeetDelRecordsReq) String() string {
func (*EchomeetDelRecordsReq) ProtoMessage() {}
func (x *EchomeetDelRecordsReq) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[30]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1574,7 +1740,7 @@ func (x *EchomeetDelRecordsReq) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetDelRecordsReq.ProtoReflect.Descriptor instead.
func (*EchomeetDelRecordsReq) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{30}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{33}
}
func (x *EchomeetDelRecordsReq) GetIds() []uint64 {
@ -1593,7 +1759,7 @@ type EchomeetDelRecordsResp struct {
func (x *EchomeetDelRecordsResp) Reset() {
*x = EchomeetDelRecordsResp{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[31]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1605,7 +1771,7 @@ func (x *EchomeetDelRecordsResp) String() string {
func (*EchomeetDelRecordsResp) ProtoMessage() {}
func (x *EchomeetDelRecordsResp) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[31]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1618,7 +1784,7 @@ func (x *EchomeetDelRecordsResp) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetDelRecordsResp.ProtoReflect.Descriptor instead.
func (*EchomeetDelRecordsResp) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{31}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{34}
}
func (x *EchomeetDelRecordsResp) GetIds() []uint64 {
@ -1642,7 +1808,7 @@ type EchomeetAliCallbackReq struct {
func (x *EchomeetAliCallbackReq) Reset() {
*x = EchomeetAliCallbackReq{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[32]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1654,7 +1820,7 @@ func (x *EchomeetAliCallbackReq) String() string {
func (*EchomeetAliCallbackReq) ProtoMessage() {}
func (x *EchomeetAliCallbackReq) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[32]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1667,7 +1833,7 @@ func (x *EchomeetAliCallbackReq) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetAliCallbackReq.ProtoReflect.Descriptor instead.
func (*EchomeetAliCallbackReq) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{32}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{35}
}
func (x *EchomeetAliCallbackReq) GetStatusCode() int32 {
@ -1714,7 +1880,7 @@ type EchomeetAliCallbackResult struct {
func (x *EchomeetAliCallbackResult) Reset() {
*x = EchomeetAliCallbackResult{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[33]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1726,7 +1892,7 @@ func (x *EchomeetAliCallbackResult) String() string {
func (*EchomeetAliCallbackResult) ProtoMessage() {}
func (x *EchomeetAliCallbackResult) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[33]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1739,7 +1905,7 @@ func (x *EchomeetAliCallbackResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetAliCallbackResult.ProtoReflect.Descriptor instead.
func (*EchomeetAliCallbackResult) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{33}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{36}
}
func (x *EchomeetAliCallbackResult) GetSentences() []*EchomeetAliSentence {
@ -1762,7 +1928,7 @@ type EchomeetAliSentence struct {
func (x *EchomeetAliSentence) Reset() {
*x = EchomeetAliSentence{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[34]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1774,7 +1940,7 @@ func (x *EchomeetAliSentence) String() string {
func (*EchomeetAliSentence) ProtoMessage() {}
func (x *EchomeetAliSentence) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[34]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1787,7 +1953,7 @@ func (x *EchomeetAliSentence) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetAliSentence.ProtoReflect.Descriptor instead.
func (*EchomeetAliSentence) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{34}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{37}
}
func (x *EchomeetAliSentence) GetText() string {
@ -1833,7 +1999,7 @@ type EchomeetAliCallbackResp struct {
func (x *EchomeetAliCallbackResp) Reset() {
*x = EchomeetAliCallbackResp{}
mi := &file_echomeet_echomeet_msg_proto_msgTypes[35]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1845,7 +2011,7 @@ func (x *EchomeetAliCallbackResp) String() string {
func (*EchomeetAliCallbackResp) ProtoMessage() {}
func (x *EchomeetAliCallbackResp) ProtoReflect() protoreflect.Message {
mi := &file_echomeet_echomeet_msg_proto_msgTypes[35]
mi := &file_echomeet_echomeet_msg_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1858,7 +2024,7 @@ func (x *EchomeetAliCallbackResp) ProtoReflect() protoreflect.Message {
// Deprecated: Use EchomeetAliCallbackResp.ProtoReflect.Descriptor instead.
func (*EchomeetAliCallbackResp) Descriptor() ([]byte, []int) {
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{35}
return file_echomeet_echomeet_msg_proto_rawDescGZIP(), []int{38}
}
var File_echomeet_echomeet_msg_proto protoreflect.FileDescriptor
@ -1912,7 +2078,7 @@ const file_echomeet_echomeet_msg_proto_rawDesc = "" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x1a\n" +
"\baudiourl\x18\x02 \x01(\tR\baudiourl\"A\n" +
"\x14EchomeetUpRecordResp\x12)\n" +
"\x06record\x18\x01 \x01(\v2\x11.DBEchoMeetRecordR\x06record\"\x87\x02\n" +
"\x06record\x18\x01 \x01(\v2\x11.DBEchoMeetRecordR\x06record\"\xac\x02\n" +
"\x14EchomeetStartTaskReq\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\"\n" +
"\fformlanguage\x18\x02 \x01(\tR\fformlanguage\x12\x1e\n" +
@ -1924,7 +2090,8 @@ const file_echomeet_echomeet_msg_proto_rawDesc = "" +
"templateid\x18\x05 \x01(\x04R\n" +
"templateid\x12\x10\n" +
"\x03tid\x18\x06 \x01(\tR\x03tid\x125\n" +
"\fservice_type\x18\a \x01(\x0e2\x12.TranscribeServiceR\vserviceType\"\x17\n" +
"\fservice_type\x18\a \x01(\x0e2\x12.TranscribeServiceR\vserviceType\x12#\n" +
"\rsummary_model\x18\b \x01(\tR\fsummaryModel\"\x17\n" +
"\x15EchomeetStartTaskResp\"\xcf\x01\n" +
"\x13EchomeetCallbackReq\x12\x12\n" +
"\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" +
@ -1935,7 +2102,7 @@ const file_echomeet_echomeet_msg_proto_rawDesc = "" +
"audio_info\x18\x05 \x01(\v2\n" +
".AudioInfoR\taudioInfo\x12#\n" +
"\rcallback_data\x18\x06 \x01(\tR\fcallbackData\"\x16\n" +
"\x14EchomeetCallbackResp\"\xad\x01\n" +
"\x14EchomeetCallbackResp\"\xd2\x01\n" +
"\x12EchomeetSummaryReq\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\x12\x1e\n" +
"\n" +
@ -1945,8 +2112,18 @@ const file_echomeet_echomeet_msg_proto_rawDesc = "" +
"\n" +
"tolanguage\x18\a \x01(\tR\n" +
"tolanguage\x125\n" +
"\fservice_type\x18\b \x01(\x0e2\x12.TranscribeServiceR\vserviceType\"\x15\n" +
"\x13EchomeetSummaryResp\"'\n" +
"\fservice_type\x18\b \x01(\x0e2\x12.TranscribeServiceR\vserviceType\x12#\n" +
"\rsummary_model\x18\t \x01(\tR\fsummaryModel\"\x15\n" +
"\x13EchomeetSummaryResp\"q\n" +
"\x10SummaryModelInfo\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" +
"\bprovider\x18\x03 \x01(\tR\bprovider\x12\x1d\n" +
"\n" +
"is_default\x18\x04 \x01(\bR\tisDefault\"\x1d\n" +
"\x1bEchomeetGetSummaryModelsReq\"I\n" +
"\x1cEchomeetGetSummaryModelsResp\x12)\n" +
"\x06models\x18\x01 \x03(\v2\x11.SummaryModelInfoR\x06models\"'\n" +
"\x15EchomeetReadRecordReq\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x04R\x02id\"\x18\n" +
"\x16EchomeetReadRecordResp\"\xb0\x01\n" +
@ -1995,76 +2172,80 @@ func file_echomeet_echomeet_msg_proto_rawDescGZIP() []byte {
return file_echomeet_echomeet_msg_proto_rawDescData
}
var file_echomeet_echomeet_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 36)
var file_echomeet_echomeet_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 39)
var file_echomeet_echomeet_msg_proto_goTypes = []any{
(*EchomeetGetTemplatesReq)(nil), // 0: EchomeetGetTemplatesReq
(*EchomeetGetTemplatesResp)(nil), // 1: EchomeetGetTemplatesResp
(*EchomeetAddTemplateReq)(nil), // 2: EchomeetAddTemplateReq
(*EchomeetAddTemplateResp)(nil), // 3: EchomeetAddTemplateResp
(*EchomeetUpdateTemplateReq)(nil), // 4: EchomeetUpdateTemplateReq
(*EchomeetUpdateTemplateResp)(nil), // 5: EchomeetUpdateTemplateResp
(*EchomeetDeleteTemplateReq)(nil), // 6: EchomeetDeleteTemplateReq
(*EchomeetDeleteTemplateResp)(nil), // 7: EchomeetDeleteTemplateResp
(*EchomeetGetAllRecordsReq)(nil), // 8: EchomeetGetAllRecordsReq
(*EchomeetGetAllRecordsResp)(nil), // 9: EchomeetGetAllRecordsResp
(*EchomeetGetRecordsReq)(nil), // 10: EchomeetGetRecordsReq
(*EchomeetGetRecordsResp)(nil), // 11: EchomeetGetRecordsResp
(*EchomeetGetRecordReq)(nil), // 12: EchomeetGetRecordReq
(*EchomeetGetRecordResp)(nil), // 13: EchomeetGetRecordResp
(*EchomeetPutRecordsReq)(nil), // 14: EchomeetPutRecordsReq
(*EchomeetPutRecordsResp)(nil), // 15: EchomeetPutRecordsResp
(*EchomeetAddRecordReq)(nil), // 16: EchomeetAddRecordReq
(*EchomeetAddRecordResp)(nil), // 17: EchomeetAddRecordResp
(*EchomeetUpRecordReq)(nil), // 18: EchomeetUpRecordReq
(*EchomeetUpRecordResp)(nil), // 19: EchomeetUpRecordResp
(*EchomeetStartTaskReq)(nil), // 20: EchomeetStartTaskReq
(*EchomeetStartTaskResp)(nil), // 21: EchomeetStartTaskResp
(*EchomeetCallbackReq)(nil), // 22: EchomeetCallbackReq
(*EchomeetCallbackResp)(nil), // 23: EchomeetCallbackResp
(*EchomeetSummaryReq)(nil), // 24: EchomeetSummaryReq
(*EchomeetSummaryResp)(nil), // 25: EchomeetSummaryResp
(*EchomeetReadRecordReq)(nil), // 26: EchomeetReadRecordReq
(*EchomeetReadRecordResp)(nil), // 27: EchomeetReadRecordResp
(*EchomeetModifyRecordsReq)(nil), // 28: EchomeetModifyRecordsReq
(*EchomeetModifyRecordsResp)(nil), // 29: EchomeetModifyRecordsResp
(*EchomeetDelRecordsReq)(nil), // 30: EchomeetDelRecordsReq
(*EchomeetDelRecordsResp)(nil), // 31: EchomeetDelRecordsResp
(*EchomeetAliCallbackReq)(nil), // 32: EchomeetAliCallbackReq
(*EchomeetAliCallbackResult)(nil), // 33: EchomeetAliCallbackResult
(*EchomeetAliSentence)(nil), // 34: EchomeetAliSentence
(*EchomeetAliCallbackResp)(nil), // 35: EchomeetAliCallbackResp
(*DBEchoMeetTemplate)(nil), // 36: DBEchoMeetTemplate
(*DBEchoMeetRecord)(nil), // 37: DBEchoMeetRecord
(*ContextStruct)(nil), // 38: ContextStruct
(TranscribeService)(0), // 39: TranscribeService
(*RecognitionResult)(nil), // 40: RecognitionResult
(*AudioInfo)(nil), // 41: AudioInfo
(*EchomeetGetTemplatesReq)(nil), // 0: EchomeetGetTemplatesReq
(*EchomeetGetTemplatesResp)(nil), // 1: EchomeetGetTemplatesResp
(*EchomeetAddTemplateReq)(nil), // 2: EchomeetAddTemplateReq
(*EchomeetAddTemplateResp)(nil), // 3: EchomeetAddTemplateResp
(*EchomeetUpdateTemplateReq)(nil), // 4: EchomeetUpdateTemplateReq
(*EchomeetUpdateTemplateResp)(nil), // 5: EchomeetUpdateTemplateResp
(*EchomeetDeleteTemplateReq)(nil), // 6: EchomeetDeleteTemplateReq
(*EchomeetDeleteTemplateResp)(nil), // 7: EchomeetDeleteTemplateResp
(*EchomeetGetAllRecordsReq)(nil), // 8: EchomeetGetAllRecordsReq
(*EchomeetGetAllRecordsResp)(nil), // 9: EchomeetGetAllRecordsResp
(*EchomeetGetRecordsReq)(nil), // 10: EchomeetGetRecordsReq
(*EchomeetGetRecordsResp)(nil), // 11: EchomeetGetRecordsResp
(*EchomeetGetRecordReq)(nil), // 12: EchomeetGetRecordReq
(*EchomeetGetRecordResp)(nil), // 13: EchomeetGetRecordResp
(*EchomeetPutRecordsReq)(nil), // 14: EchomeetPutRecordsReq
(*EchomeetPutRecordsResp)(nil), // 15: EchomeetPutRecordsResp
(*EchomeetAddRecordReq)(nil), // 16: EchomeetAddRecordReq
(*EchomeetAddRecordResp)(nil), // 17: EchomeetAddRecordResp
(*EchomeetUpRecordReq)(nil), // 18: EchomeetUpRecordReq
(*EchomeetUpRecordResp)(nil), // 19: EchomeetUpRecordResp
(*EchomeetStartTaskReq)(nil), // 20: EchomeetStartTaskReq
(*EchomeetStartTaskResp)(nil), // 21: EchomeetStartTaskResp
(*EchomeetCallbackReq)(nil), // 22: EchomeetCallbackReq
(*EchomeetCallbackResp)(nil), // 23: EchomeetCallbackResp
(*EchomeetSummaryReq)(nil), // 24: EchomeetSummaryReq
(*EchomeetSummaryResp)(nil), // 25: EchomeetSummaryResp
(*SummaryModelInfo)(nil), // 26: SummaryModelInfo
(*EchomeetGetSummaryModelsReq)(nil), // 27: EchomeetGetSummaryModelsReq
(*EchomeetGetSummaryModelsResp)(nil), // 28: EchomeetGetSummaryModelsResp
(*EchomeetReadRecordReq)(nil), // 29: EchomeetReadRecordReq
(*EchomeetReadRecordResp)(nil), // 30: EchomeetReadRecordResp
(*EchomeetModifyRecordsReq)(nil), // 31: EchomeetModifyRecordsReq
(*EchomeetModifyRecordsResp)(nil), // 32: EchomeetModifyRecordsResp
(*EchomeetDelRecordsReq)(nil), // 33: EchomeetDelRecordsReq
(*EchomeetDelRecordsResp)(nil), // 34: EchomeetDelRecordsResp
(*EchomeetAliCallbackReq)(nil), // 35: EchomeetAliCallbackReq
(*EchomeetAliCallbackResult)(nil), // 36: EchomeetAliCallbackResult
(*EchomeetAliSentence)(nil), // 37: EchomeetAliSentence
(*EchomeetAliCallbackResp)(nil), // 38: EchomeetAliCallbackResp
(*DBEchoMeetTemplate)(nil), // 39: DBEchoMeetTemplate
(*DBEchoMeetRecord)(nil), // 40: DBEchoMeetRecord
(*ContextStruct)(nil), // 41: ContextStruct
(TranscribeService)(0), // 42: TranscribeService
(*RecognitionResult)(nil), // 43: RecognitionResult
(*AudioInfo)(nil), // 44: AudioInfo
}
var file_echomeet_echomeet_msg_proto_depIdxs = []int32{
36, // 0: EchomeetGetTemplatesResp.templates:type_name -> DBEchoMeetTemplate
36, // 1: EchomeetAddTemplateReq.template:type_name -> DBEchoMeetTemplate
36, // 2: EchomeetAddTemplateResp.template:type_name -> DBEchoMeetTemplate
36, // 3: EchomeetUpdateTemplateReq.template:type_name -> DBEchoMeetTemplate
36, // 4: EchomeetUpdateTemplateResp.template:type_name -> DBEchoMeetTemplate
37, // 5: EchomeetGetAllRecordsResp.records:type_name -> DBEchoMeetRecord
37, // 6: EchomeetGetRecordsResp.records:type_name -> DBEchoMeetRecord
37, // 7: EchomeetGetRecordResp.record:type_name -> DBEchoMeetRecord
37, // 8: EchomeetPutRecordsReq.records:type_name -> DBEchoMeetRecord
38, // 9: EchomeetAddRecordReq.original:type_name -> ContextStruct
37, // 10: EchomeetAddRecordResp.record:type_name -> DBEchoMeetRecord
37, // 11: EchomeetUpRecordResp.record:type_name -> DBEchoMeetRecord
39, // 12: EchomeetStartTaskReq.service_type:type_name -> TranscribeService
40, // 13: EchomeetCallbackReq.result:type_name -> RecognitionResult
41, // 14: EchomeetCallbackReq.audio_info:type_name -> AudioInfo
39, // 15: EchomeetSummaryReq.service_type:type_name -> TranscribeService
37, // 16: EchomeetModifyRecordsResp.record:type_name -> DBEchoMeetRecord
33, // 17: EchomeetAliCallbackReq.Result:type_name -> EchomeetAliCallbackResult
34, // 18: EchomeetAliCallbackResult.Sentences:type_name -> EchomeetAliSentence
19, // [19:19] is the sub-list for method output_type
19, // [19:19] is the sub-list for method input_type
19, // [19:19] is the sub-list for extension type_name
19, // [19:19] is the sub-list for extension extendee
0, // [0:19] is the sub-list for field type_name
39, // 0: EchomeetGetTemplatesResp.templates:type_name -> DBEchoMeetTemplate
39, // 1: EchomeetAddTemplateReq.template:type_name -> DBEchoMeetTemplate
39, // 2: EchomeetAddTemplateResp.template:type_name -> DBEchoMeetTemplate
39, // 3: EchomeetUpdateTemplateReq.template:type_name -> DBEchoMeetTemplate
39, // 4: EchomeetUpdateTemplateResp.template:type_name -> DBEchoMeetTemplate
40, // 5: EchomeetGetAllRecordsResp.records:type_name -> DBEchoMeetRecord
40, // 6: EchomeetGetRecordsResp.records:type_name -> DBEchoMeetRecord
40, // 7: EchomeetGetRecordResp.record:type_name -> DBEchoMeetRecord
40, // 8: EchomeetPutRecordsReq.records:type_name -> DBEchoMeetRecord
41, // 9: EchomeetAddRecordReq.original:type_name -> ContextStruct
40, // 10: EchomeetAddRecordResp.record:type_name -> DBEchoMeetRecord
40, // 11: EchomeetUpRecordResp.record:type_name -> DBEchoMeetRecord
42, // 12: EchomeetStartTaskReq.service_type:type_name -> TranscribeService
43, // 13: EchomeetCallbackReq.result:type_name -> RecognitionResult
44, // 14: EchomeetCallbackReq.audio_info:type_name -> AudioInfo
42, // 15: EchomeetSummaryReq.service_type:type_name -> TranscribeService
26, // 16: EchomeetGetSummaryModelsResp.models:type_name -> SummaryModelInfo
40, // 17: EchomeetModifyRecordsResp.record:type_name -> DBEchoMeetRecord
36, // 18: EchomeetAliCallbackReq.Result:type_name -> EchomeetAliCallbackResult
37, // 19: EchomeetAliCallbackResult.Sentences:type_name -> EchomeetAliSentence
20, // [20:20] is the sub-list for method output_type
20, // [20:20] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
}
func init() { file_echomeet_echomeet_msg_proto_init() }
@ -2079,7 +2260,7 @@ func file_echomeet_echomeet_msg_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_echomeet_echomeet_msg_proto_rawDesc), len(file_echomeet_echomeet_msg_proto_rawDesc)),
NumEnums: 0,
NumMessages: 36,
NumMessages: 39,
NumExtensions: 0,
NumServices: 0,
},

6
deploy/voitrans/env/env.example

@ -25,6 +25,12 @@ REDIS_KEY_PREFIX=VoitransDev
# ── NATS(统计快照推送到 console,须与 console 端一致)──
NATS_URL=nats://nats:4222
# ── 业务配置字段解密(须与 console 的 FIELD_ENCRYPT_KEY 完全相同)──
# console 后台把邮件/短信/支付等密钥以 AES-256-GCM 密文写进「应用业务库」;本服务启动时用这把 key 解密。
# ⚠️ 留空或与 console 不一致时,解密会「静默失败并把密文当明文用」——例如把密文当 Resend API Key
# 发出去,表现为 401 "API key is invalid",而后台却显示配置已填好,极难排查。
FIELD_ENCRYPT_KEY=
# ── 统计上报(须与 console 注册表中的应用名/区域一致)──
ANALYZE_APP_NAME=voitrans-a11-dev
ANALYZE_REGION=1

Loading…
Cancel
Save