Files
eaiadminandClaude Code b4c8ea38d1 feat(asr): 降级兜底——分离路由全挂时退到无分离路由,只交付逐字稿
回退链从「一条主路由 + 一串替补」改成两级:先把同能力(有说话人分离)的路由
试完,全挂才降级到不分离的路由。降级是**本次事实**而不是配置事实,写进第 2 步
产物(capability_degraded / capability_zh),第 3 步与第 5/6 步据此判为不可用。

- transcribe.go:Result 加 CapabilityDegraded,判据 chain[0] 能分离而实际这条
  不能;与 HasSpeakers 分开记(后者可能是「配了却没输出」那种异常)
- audio_handlers.go:闸门从「读路由声明的能力」改成「读稿子里实际有没有标签」
  (audioTranscriptSpeakerKeys),与第 3 步共用同一句 SpeakerKeysOf;本次没分离
  → 哨兵错误 errAudioSpeakersUnavailableThisRun,不再放行去写一份看不出残缺的纪要
- 闸门**不看** capability_degraded:改动前落库的老产物没有这个字段,看它就 fail-open
- 第 1 步「转写要求」提前把降级的后果说清;第 2 步产物带完整措辞与「⚠」日志
- 前端两处(SpecialistPanel.vue / audioSkill.js)判断顺序改为先读实际结果
  has_speakers、再退回声明 speakers —— 顺序反了会在降级那一次照旧显示第 3 步
- ai_config.json:补 3 条云端无分离路由与各自的回退链

验证:三处变异(产物 key 拼错、标记写死 true、闸门 fail-closed)都验过会红;
新增两个用例文件走真实 gin 路由 + 真实鉴权中间件,断言拦下来的**理由**而不只是
「拦下来了」;go test ./... 全绿、gofmt 干净、前端构建通过。

同期把本地 ASR 装成 systemd 常驻服务(deploy/install_asr_local.sh 七步全过,
开机自启,实测 26.7 分钟录音 → 3.1 分钟)。装的过程挖出两个只在服务化时才暴露的坑:

- E12 转写堵住事件循环 → 探活超时 → 本地被判不健康 → auto 静默退云端、音频出网,
  全程没有任何报错。修法 run_in_threadpool(deploy/asr/serve.py)
- E13 服务账号的 ~ 不可写,pyannote 写不了 ~/.pyannote/database.yml,每次转写 500。
  修法 asr.env 加 HOME=<cache 目录>(该目录在 unit 的 ReadWritePaths 里)

顺带收口一处交付缺口:服务源码原先只有 ~/asr-poc 一份,而 DELIVERY.md 的清理计划
要 rm -rf 它 —— 那会让唯一副本变成 /opt 下 root 所有、不在任何版本库里的文件。
现在 deploy/asr/ 是唯一事实源,装机脚本与文档同步改。

已知偏离 / 未做(记在案):
- 界面那句「本次没有说话人分离,后续步骤不可用」只验到后端接口层,没有造出真实
  降级场景渲染出来看过
- deploy/asr/ 的引入改变了装机来源:原型目录 $SRC_DIR 从此只提供 venv 与模型,
  服务代码一律从仓库取

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 23:36:00 +08:00

13 KiB
Raw Permalink Blame History

交付手册(静态单二进制 + 本地语音转写 + Clonezilla 整盘克隆)

面向交付工程师。目标:把一台配置好的原型机整盘克隆到客户同型号机器, 客户拿到手即用,机器上无源代码、无开发痕迹、无测试数据、无默认口令。


0. 交付物清单

文件 说明
eai_agentplatform-server 单二进制(CGO 关闭,静态链接,约 38MB,无任何运行时依赖)
eai_agentplatform.service systemd 单元文件
eai_agentplatform.env 环境变量样例
eai_agentplatform-asr.service 本地语音转写服务的 systemd 单元(需要 Python,见下)
asr.env 本地语音转写服务的环境变量样例
clonezilla-cleanup.sh 克隆前清理脚本(DRY-RUN 默认)
data/eai_agentplatform.db SQLite 数据库(首启自动建表)
data/kb_data/ 已审批素材目录
assets/knowledge/source/ 知识源 Markdown(待入库,由管理员审批)
assets/training/materials/ 培训资料资产(课程种子、PDF、视频、脚本)

不需要:Go 运行时、Docker、MySQL、任何 npm 依赖。

平台本体确实不需要 Python:eai_agentplatform-server 是静态单二进制, ldd 输出 not a dynamic executable,这条一直成立。

但语音转写是另一个进程,它需要。 转写默认走本机(faster-whisper large-v3 + pyannote 3.1),由一个独立的 Python 服务(eai_agentplatform-asr.service, 监听 127.0.0.1:8090)提供。客户机器上因此会多出:

组件 体积 说明
/opt/eai_agentplatform-asr/venv 约 9.4 GB Python 环境(含 CUDA 版 PyTorch)
/opt/eai_agentplatform-asr/models 约 4.1 GB 模型权重,随整盘克隆带过去,不重新下载

合计约 13.5 GB,交付前请确认目标机磁盘够用。

这不是「为了本地化而放弃轻交付」——恰恰相反:把转写留在本机,是为了不让 客户会议的录音被送到公网 ASR。此处与「单二进制交付」的出入是明知并接受的取舍, 记在案。若某台客户机确实装不了 Python,可以把默认语音路由切到云端 (后台 → AI 路由 → 默认语音路由),代价是录音会离开本机, 界面在转写结果上会明确标注「音频已离开本机」。


1. 目录布局(客户机器最终形态)

/opt/eai_agentplatform/
├── eai_agentplatform-server     # 单二进制
├── .env                     # JWT 密钥等(交付前生成,勿提交源码库)
├── assets/
│   ├── knowledge/
│   │   └── source/          # 知识源 Markdown
│   └── training/
│       └── materials/       # 培训资料资产
├── data/
│   ├── eai_agentplatform.db     # SQLite(首启自动建)
│   ├── backups/             # 定期备份(服务自动维护,见第 7 节)
│   └── kb_data/             # 素材 + 提取缓存
└── (前端静态资源由 Nginx 托管,见 ../docs/02_Architecture/部署文档.md)

/opt/eai_agentplatform-asr/        # 本地语音转写(独立进程,独立单元)
├── serve.py                   # OpenAI 兼容 HTTP 端点(/v1/audio/transcriptions、/v1/models)
├── asr_core.py                # 转写 + 说话人分离
│                              # ↑ 这两个的**事实源在仓库** deploy/asr/,装机时由
│                              #   install_asr_local.sh 装过来;直接改 /opt 下那份
│                              #   会在下次装机时被覆盖,且没人 review 得到
├── asr.env                    # 环境变量(精度档位、离线开关)
├── venv/                      # Python 环境(约 9.4 GB)
├── models/                    # 模型权重(约 4.1 GB,实体目录)
│   ├── Systran/faster-whisper-large-v3/
│   └── pyannote/{segmentation-3.0,wespeaker-voxceleb-resnet34-LM,speaker-diarization-3.1}/
└── cache/                     # 运行时缓存(HF_HOME 指到这里)

serve.py 按 脚本所在目录/models 找模型,所以 models/ 必须是它的同级目录 (符号链接也行,但交付机上建议放实体目录,少一层依赖)。 原型机上 ~/asr-poc/models 是指向仓库 external_download/ 的软链 —— 那是开发期的 组织方式,克隆到客户机之前必须把权重拷成实体目录,客户机上没有仓库。


2. 交付前准备(原型机)

# 2.1 构建静态二进制(开发机执行,产物拷贝到原型机)
export PATH=$HOME/go-sdk/go/bin:$PATH CGO_ENABLED=0 GOPROXY=https://goproxy.cn,direct
go build -o bin/eai_agentplatform-server ./cmd/server
# 校验:file 输出应为 "statically linked",ldd 应为 "not a dynamic executable"

# 2.2 拷贝到原型机 + 建账号 + 装 systemd 单元
sudo install -m 0755 eai_agentplatform-server /opt/eai_agentplatform/eai_agentplatform-server
sudo useradd -r -s /usr/sbin/nologin eai_agentplatform
sudo mkdir -p /opt/eai_agentplatform/data/kb_data /opt/eai_agentplatform/assets/knowledge/source /opt/eai_agentplatform/assets/training/materials
sudo chown -R eai_agentplatform:eai_agentplatform /opt/eai_agentplatform
sudo install -m 0644 deploy/eai_agentplatform.service /etc/systemd/system/eai_agentplatform.service

# 2.3 生成 .env(含随机 JWT 密钥)
sudo -u eai_agentplatform cp deploy/eai_agentplatform.env /opt/eai_agentplatform/.env
NEW_SECRET=$(python3 -c "import secrets; print(secrets.token_urlsafe(48))")
sudo sed -i "s|^JWT_SECRET=.*|JWT_SECRET=$NEW_SECRET|" /opt/eai_agentplatform/.env

# 2.4 启动验证
sudo systemctl daemon-reload
sudo systemctl enable --now eai_agentplatform
curl http://127.0.0.1:8080/api/health   # {"status":"ok",...}

# 2.5 本地语音转写服务(ASR)
# 整个 2.5 已经写成脚本,它会先打印计划再执行(--dry-run 只看计划):
sudo bash deploy/install_asr_local.sh
#
# 它做的事(等价的手工命令如下,仅供理解,**别照着敲**):
# 环境本身随原型机整盘克隆过来,这一步只是把它装到交付位置 + 起服务。
# venv 与模型都很大(9.4G + 4.1G),用 mv 而不是 cp —— 两台机器上各留一份没意义。
sudo mkdir -p /opt/eai_agentplatform-asr
sudo install -m 0644 deploy/asr/{serve.py,asr_core.py} /opt/eai_agentplatform-asr/
sudo mv ~/asr-poc/venv /opt/eai_agentplatform-asr/venv
sudo cp -aL ~/asr-poc/models /opt/eai_agentplatform-asr/models   # -L 解开软链,客户机上没有仓库可指
sudo install -m 0644 deploy/asr.env /opt/eai_agentplatform-asr/asr.env
sudo install -m 0644 deploy/eai_agentplatform-asr.service /etc/systemd/system/
sudo chown -R eai_agentplatform:eai_agentplatform /opt/eai_agentplatform-asr
sudo mkdir -p /opt/eai_agentplatform-asr/cache && sudo chown eai_agentplatform:eai_agentplatform /opt/eai_agentplatform-asr/cache

sudo systemctl daemon-reload
sudo systemctl enable --now eai_agentplatform-asr

# 2.6 验证:模型加载要几十秒,起来之前 /v1/models 是连不上的
systemctl is-active eai_agentplatform-asr        # active
curl -s http://127.0.0.1:8090/v1/models | head -c 200
# 再确认平台侧认它:后台 → AI 路由 → 语音转写,应显示「本地转写 · Whisper large-v3」

转写模型与平台是强耦合的吗? 不是。平台启动时读 config/ai_config.json, audio_route_local_whisper 指向 http://127.0.0.1:8090/v1。ASR 服务没起来 不影响平台启动:转写会自动回退到 fallback_routes 里的云端路由, 并在转写产物的日志与界面警示区写明「已回退到云端,音频已离开本机」。 这正是让 ASR 单独成一个 systemd 单元(而不是塞进主进程)的原因。


3. 克隆前清理(不可逆,务必先 DRY-RUN)

sudo bash clonezilla-cleanup.sh          # DRY-RUN,只列动作
sudo bash clonezilla-cleanup.sh --confirm # 确认后真实清理

脚本负责:

  • 删除所有 Go 源代码(cmd/ internal/ go.mod go.sum)
  • 清除 .git / 版本控制痕迹
  • 清空日志与 shell 历史
  • 删除本地 ASR 工作区里的原型机数据:~/asr-poc/audio(真实会议录音)、 ~/asr-poc/out(完整逐字稿与说话人分离结果,含一份云端转写对照稿)、安装日志

脚本不负责(需手工确认,见下):

手工清单(缺一不可)

# 动作 命令
A 重置管理员密码 sudo -u eai_agentplatform /opt/eai_agentplatform/eai_agentplatform-server -reset-admin '<强密码>'
B 确认 JWT 密钥随机 grep -c '__CHANGE_ME__' /opt/eai_agentplatform/.env(应为 0)
C 清空测试数据 Web 后台删除演示用户/素材/考试记录,或用 sqlite3(见第 4 节)
D 删掉 ASR 工作区,只留一份 venv 装好 /opt/eai_agentplatform-asr 并确认服务能起后,rm -rf ~/asr-poc(清理脚本会给提示,不代删)

默认种子账号为 admin / admin123,克隆前必须改密(A 项),否则客户拿到默认口令。


4. 清空测试数据的 SQL 参考(可选)

# 若需彻底清库只保留结构 + admin:
sqlite3 /opt/eai_agentplatform/data/eai_agentplatform.db <<'SQL'
-- 先停服务
DELETE FROM exam_record;
DELETE FROM knowledge_chunk;
DELETE FROM knowledge_source;
DELETE FROM media_file;
DELETE FROM course;
DELETE FROM product;
DELETE FROM question;
DELETE FROM exam_paper;
DELETE FROM "user" WHERE username != 'admin';
DELETE FROM system_config WHERE config_key NOT IN ('company_intro','llm_base_url','llm_model','embed_model','llm_api_key');
SQL

表名以实际 schema 为准;无 sqlite3 时,用管理员 Web 界面逐项删除亦可。


5. Clonezilla 整盘克隆

  1. 原型机清理完成、复核 A/B/C 三项后关机。
  2. U 盘启动 Clonezilla → device-device(整盘复制)。
  3. 目标机为同型号机器,逐台克隆。
  4. 客户机器首启:systemd 自动拉起服务;Nginx 反代 8080。
  5. 交付验收:登录(新密码)→ 上传素材 → 审批 → 考试 → AI 问答(若接内网 LLM)。

6. 交付红线(每次克隆前过一遍)

  • ldd eai_agentplatform-server 输出 not a dynamic executable
  • find /opt/eai_agentplatform -name '*.go' -o -name 'go.mod' 无结果
  • /opt/eai_agentplatform/.env 无 __CHANGE_ME__
  • 管理员密码非 admin123
  • .bash_history 已清空、无 .git
  • 测试账号(zhangsan 等)、演示素材、演示考试记录已删除
  • data/backups/ 里没有原型机自己的数据备份(见第 7 节末)
  • ~/asr-poc/audio、~/asr-poc/out 已删(真实录音与逐字稿)
  • 全盘只有一份 venv(在 /opt/eai_agentplatform-asr),~/asr-poc 已删
  • /opt/eai_agentplatform-asr/models 是实体目录,不是指向仓库的软链
  • systemctl is-active eai_agentplatform-asr 为 active,且转写可选到「本地转写」路由

7. 数据备份与恢复

SQLite 是单文件、没有主从副本,文件坏一份就是全丢,所以服务自带定期备份,默认开着。

怎么跑:进程启动时先检查一次(距上次备份够久就补一份),之后每小时醒一次看是否到期。 不需要外部 cron / systemd timer,备份逻辑在二进制里。间隔和保留份数由 .env 控制:

BACKUP_ENABLED=true          # 关掉就完全不备
BACKUP_DIR=data/backups      # 必须在 data/ 下(ProtectSystem=strict 只放开这里)
BACKUP_KEEP=7                # 保留最近几份,更旧的自动删
BACKUP_INTERVAL_HOURS=24     # 间隔

备份文件长这样:data/backups/eai_agentplatform-20260914-162401.db。 用 VACUUM INTO 产出,不是 cp 主库 —— 服务边跑边写时 cp 可能拷到写了一半的页; VACUUM INTO 出来的是已压实、内部一致的完整副本,且不用停服。产出的文件会校验 SQLite 文件头, 不是真库就删掉并报错,不会留下「看着像备份的废物」。

手工备一份(升级、迁移、动数据之前留个手边的副本):

sudo -u eai_agentplatform /opt/eai_agentplatform/eai_agentplatform-server -backup

恢复(会覆盖当前数据,先停服):

sudo systemctl stop eai_agentplatform
sudo -u eai_agentplatform cp data/backups/eai_agentplatform-20260914-162401.db data/eai_agentplatform.db
sudo systemctl start eai_agentplatform

备份只在本机 data/ 目录里,防的是误删/误改/写坏,防不了整盘损坏。 要防整盘,得把 data/backups/ 定期拷到机器之外(U 盘 / 内网文件服务器)。

克隆前注意:原型机的 data/backups/ 里会有原型机自己的历史数据(测试账号、演示素材), 整盘克隆会原样带到客户机器上。清理时一并删掉该目录内容,让客户机器从干净状态开始。

恢复之后想核对内容,直接看文件时间戳和大小即可 —— 文件名里的时间就是备份时刻。