Commit Graph

388 Commits

Author SHA1 Message Date
Blizzard 47589fae3f Merge pull request 'feat(voice): 按住空格键语音输入(PTT),松开发送' (#14) from feat/site into main
deploy-132 / deploy (push) Successful in 3m1s
Reviewed-on: #14
2026-07-24 13:05:06 +00:00
Blizzard 52988af196 feat(voice): 按住空格键语音输入(PTT),松开发送
按住空格键开始聆听,松开发送 end 指令提交任务。不销毁麦克风上下文,
下次重按直接复用,避免 getUserMedia/AudioContext 重建延迟。
支持输入框/文本区内不抢空格、窗口失焦自动取消。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-24 21:03:27 +08:00
Blizzard a9de0ebf7e Merge pull request 'fix(voice): TTS 只播前几字就断——tts_end 别打断,等排队音频放完' (#13) from feat/site into main
deploy-132 / deploy (push) Successful in 4m40s
Reviewed-on: #13
2026-07-24 08:55:30 +00:00
Blizzard aba88193d8 feat(voice): 提升桌面端语音交互与本地任务执行支持 2026-07-24 16:40:25 +08:00
Blizzard e9a8b9b4c6 docs: 本地 Agent 执行设计——双平面(执行下沉/控制中心) + 与 Claude Code 对比
在保留中心化治理前提下让 Agent 操作本机文件/执行命令的设计。
含:双平面架构、与 Claude Code 逐项对比、现有架构支持度分析、
Go+Wails 客户端可行性、档 A/档 B 演进、安全门禁、P0-P4 落地阶段。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:13:03 +08:00
Blizzard b8095688ba fix(voice): TTS 只播前几字就断——tts_end 别打断,等排队音频放完
火山合成远快于真实语速:一句话的 PCM 秒级全推到客户端,被 nextStart
预约到未来时刻顺序播。此前 tts_end 直接 resetPlayback() stop 掉所有还
没播的 source,于是只听到前三四个字就断。

改:tts_end 走 drainThenReady()——不打断,按 nextStart 算剩余时长等排队
音频自然放完,末段结束再回 ready;speaking 态维持到真播完(HUD 频谱靠它)。
resetPlayback 只留给 barge-in/close,并清 drainTimer 防迟到定时器改状态。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:01:02 +08:00
Blizzard b527a7241e Merge pull request 'feat(wechat): 关注公众号后自动回复欢迎语(被动回复,可后台配置)' (#12) from feat/site into main
deploy-132 / deploy (push) Successful in 4m18s
Reviewed-on: #12
2026-07-22 07:24:17 +00:00
Blizzard 4153046e0e feat(desktop): 全屏 JARVIS HUD 补齐概念版完整度——开机自检 + 遥测 + 解码文字
对齐概念版 artifact 的完整效果:
- 开机自检序列(INITIALIZING→AUDIO/UPLINK/VOICE MODEL OK→ONLINE),进场淡出,reduced-motion 跳过
- 右上遥测读数(SIGNAL/LATENCY/GAIN/SESSION),SIGNAL/LATENCY/GAIN 由真实电平实时驱动+抖动
- 回答走解码式打字机(逐字定稿、前沿字符跳乱码 glyph scramble),接真实 reply 流
- 修 ESC 按钮与遥测重叠:ESC 常显于右上角,遥测下移到其下,不再压住 SIGNAL

真机(浏览器预览)验证:遥测四行齐全无遮挡,STANDBY 呼吸,品牌位星期五。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 15:03:42 +08:00
Blizzard a4b9897eaf fix(voice): 麦克风打不开时给准确提示——多约束尝试 + 无设备预检
- 无麦克风设备(如 Mac Studio/Mini 无内置麦)时,getUserMedia 会以 OverconstrainedError
  "Invalid constraint" 误导报错;先 enumerateDevices 预检 audioinput,没有就直说"没检测到麦克风"
- getUserMedia 依次试 {audio:true}/{audio:{}}/带回声消除,全败则暴露真实错误名;
  NotAllowedError 单独提示去系统设置授权

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:56:08 +08:00
Blizzard fe5c4215f7 fix(voice): 麦克风约束兜底——WKWebView 不认高级约束("Invalid constraint")退 {audio:true}
原生壳(Wails WKWebView)的 getUserMedia 不认 channelCount/echoCancellation 等高级约束,
抛 "Invalid constraint"。先试高级约束(浏览器更好),失败退回最简 {audio:true}(最兼容)。
配合上一提交的 .app 打包+NSMicrophoneUsageDescription,原生壳麦克风打通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:47:19 +08:00
Blizzard 92341be32c fix(desktop): 麦克风优雅降级 + Info.plist 加 NSMicrophoneUsageDescription
- voice.ts: startMic 前探测 navigator.mediaDevices?.getUserMedia——WKWebView(原生壳)非安全
  上下文里它可能未暴露,直接调会抛 "undefined is not an object" 崩掉;改为给可读提示
- build/darwin/Info.plist + Info.dev.plist: 加 NSMicrophoneUsageDescription,否则 macOS
  不会授权麦克风(package 后的 .app 才能弹权限)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:44:27 +08:00
Blizzard ef2b815bd7 fix(desktop): 补 build:dev 脚本,修复 wails3 dev 启动
wails3 dev 的 DEV 构建走 `npm run build:dev`,而 package.json 只有 build(此前只用过
task build/package,dev 从没跑通)。补 build:dev=vite build --mode development。

注:原生 dev/build 还有个 go.work 坑——desktop 不在工作区,直接 go build 需 GOWORK=off;
且 wails3 generate bindings -clean 会误删已提交 bindings(因模块不在 go.work 解析不到 services)。
可靠的本地跑原生窗口:cd sundynix-desktop && (cd frontend && npm run build) &&
  GOWORK=off CGO_ENABLED=1 go build -tags production -buildvcs=false -o /tmp/jarvis_desktop . && /tmp/jarvis_desktop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:39:44 +08:00
Blizzard 2a743a33f6 feat(desktop): 全屏「JARVIS 模式」HUD——钢铁侠风,接真实语音音频
弧反应堆核心 + 同心旋转环 + 随真实声音反应的环形频谱 + 状态编排 + HUD 边框/扫描线/网格。
可视化数据全来自真实语音会话(不是模拟)。

- voice.ts: micCtx/playCtx 各挂 AnalyserNode + level()——说话读下行TTS、其余读麦克风,
  实时电平 0..1(HUD 每帧读,复用缓冲)
- JarvisHud.tsx: Canvas 2D HUD,按 VoiceState 编排(待命呼吸/聆听炸开/思考散粒子+雷达/说话频谱),
  单一青色强调+思考态琥珀;显示助手名(星期五)+ 我说的 + 打字机回答;尊重 prefers-reduced-motion
- VoiceDock: 加"全屏 JARVIS 模式"按钮(建客户端+拉名字);Esc 退出

真机验证:全屏 HUD 正常渲染,STANDBY 呼吸,品牌位显示自定义名"星期五"。晚上点中心说话即随真声反应。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:28:39 +08:00
Blizzard 24351014e0 feat(desktop): JARVIS 设置面板——名字/人设/自带豆包配置(用户级)
- lib/api.ts: getMyJarvis/saveMyJarvis + JarvisConfig 类型
- shell/JarvisSettings.tsx: 弹窗——名字 + 语气人设 + 折叠「高级:我的豆包配置」
  (api_key/asr/tts resource-id/音色;掩码不回传=沿用已存;填齐才覆盖系统)
- VoiceDock: 麦克风上方加设置齿轮,打开面板

真机验证:面板正确加载后端配置(名字=星期五、人设),高级区四项占位清晰,存档下次说话生效。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:12:03 +08:00
Blizzard 99ce07c1b0 feat(voice): 每用户 JARVIS——自定义名字/人设 + 自带豆包配置回落 + 语音不串主记忆
把 JARVIS 做成每用户独立:名字(你叫JARVIS别人叫星期五)、人设(与主偏好记忆分开)、
可自带豆包配置(齐全则用用户的,否则回落系统)。

- store/user_jarvis.go: sundynix_user_jarvis(user_id唯一;name/persona/火山creds,APIKey密文)
  + Get/Save(upsert) + AutoMigrate 注册
- voice_config.go resolveJarvis(uid): 火山配置用户优先系统兜底 + 取名字/人设
- voice_task.go: voiceSystemPrompt(name,persona)——简短是硬基线,名字/语气由用户定;
  buildVoiceGraph 带 name+persona;voice.go 会话升级时按用户解析
- dispatcher compose_compiler: 语音任务(useVoice)不拉主偏好记忆,改用用户 persona(保留短期历史)
- jarvis.go + 路由: GET/PUT /api/v1/me/jarvis(用户级,api_key 脱敏/留空沿用)

真机验证:设 name=星期五+干净人设→语音答"我是星期五…"(自称新名、无糙话、4.4s),
has_own_voice=false 用系统豆包。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:06:32 +08:00
Blizzard fb3685532c polish(admin): 补齐侧栏菜单缺失图标,全项统一对齐
之前 登录设置/语音设置/微信用户/订阅 四项无 ICON_MAP 条目→图标位空、与兄弟项不齐。
补齐 heroicons 描边风一致图标:登录设置=钥匙、语音设置=麦克风、微信用户=聊天气泡、
订阅=循环;删掉无对应路由的死条目 usage。现 17 条路由全有图标。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 13:34:28 +08:00
Blizzard 95f3781076 feat(admin): 模型配置加「编辑」按钮 + 语音默认提示改 deepseek-v4-flash
- ModelManager: 每行加「编辑」→ 载入表单就地改(api_key 留空=沿用现有密钥),
  后端按 id 走更新而非新增;表单头/按钮随编辑态切换,带「取消」;省去删了重登记
- ModelsPage: 语音默认模型提示 deepseek-chat→deepseek-v4-flash(chat 2026/07/24 弃用)

注:语音模型实例已通过 admin API 从 deepseek-chat 热切到 deepseek-v4-flash(dispatcher
现读 v4-flash,无需重启)——验证了编辑/激活的 NATS 热更新链路。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 13:07:13 +08:00
Blizzard a4669a0d82 fix(usage): 用量按实际所用模型计量(语音任务归语音模型,别错记工作模型)
分开工作/语音模型后,语音任务实际跑语音模型(deepseek-chat),但 emitUsage 没带 Model,
网关 SaveUsageEvent 拿激活 chat 模型(deepseek-v4-pro)兜底 → 语音用量被错价成工作模型。
修:emitUsage 按任务 model_profile 选池、type-assert ModelName() 填真实模型名。

真机验证:语音任务首字出声 ~2s(deepseek-chat,较 v4-pro 的 5-7s 骤降),usage_event.model
正确记为 deepseek-chat。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:43:30 +08:00
Blizzard aec7ad949c feat(model): 工作模型与 JARVIS 语音模型分开配置 + 语音任务路由到快模型
模型配置加"用途"维度:工作主力(chat,要强)与 JARVIS 语音(voice,要快/低时延)各配各激活,
语音任务走语音模型池,未配置则透明回落工作模型——不影响现有功能。

- contract: ConfigKindVoice="voice" + Meta[model_profile]=voice(与 intent==report 同类路由)
- gateway: ServeConfig/broadcastActive 循环纳入 voice;submitVoiceTask 打 model_profile=voice 标记
- dispatcher: 第二个 llm.Pool(voicePool)吃 voice 配置热更新;board.useVoice 从 Meta 派生(含快照);
  Orchestrator.agentPool(b) 按黑板选池——语音且语音池就绪→语音池,否则工作池;
  agent 生成路径(graph/react/coordinator/compose)全改走 agentPool(b),报告/护栏/记忆固定工作池
- admin: 模型页三 Tab(工作主力/JARVIS语音/向量化),复用 ModelManager;api Kind 加 voice

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:39:17 +08:00
Blizzard b6927247da feat(voice): 下行打字机文本流 + 首块快出,即时反馈
- protocol.go: 新增 ServerReply("reply") 消息——Agent 回答增量文本
- voice_tts.go: token 一到即转发客户端(打字机),同时攒句喂 TTS(音频随后)
- sentence_buffer.go: 本轮首句用低阈值(5 rune)抢首字延迟,之后回常规 12
- 桌面端 voice.ts onReply + VoiceDock 对话气泡(我说的 + JARVIS 打字机回答,思考态光标)

管线已最优:文字在 LLM 首 token 即刻上屏、音频紧随。剩余时延=大模型 TTFT(deepseek-v4-pro
4-7s 且波动大,疑似推理模型),这是模型的账、非管线——真要"马上响应"需换快模型。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:25:20 +08:00
Blizzard 6b31856551 perf(voice): 砍兜底等待 1.2s→0.5s + 语音提示词强制简短 + 时延测量
- voice.go: ClientEnd 兜底等待 1200ms→500ms(白吃的时延),首字出声 6.6s→5.3s
- voice_task.go: JARVIS 系统提示词改"必须简短/最多三句/先给结论"(语音场景长答案既拖慢
  首字又难听;注:deepseek-v4-pro 仍可能不理会长度指令,可靠收短需 max_tokens 硬顶)
- voicesim: 时延拆解(提交/首字出声/朗读完毕,以"说完"为0点)

现状:首字出声 ~5.3s,大头是 deepseek 生成第一句(大模型 TTFT);管线开销(兜底+TTS握手)已压到~1s。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:17:28 +08:00
Blizzard 9ae06229b8 feat(voice): 显式 end 兜底提交 + 端到端模拟工具,全链路真机跑通
火山流式 ASR 只在 VAD 静音时发 Final;客户端点"停"(ClientEnd)不能干等——否则整段说完
却因没触发 VAD Final 而永不提交任务(实测复现)。改为:
- voice.go 累计 latestText(每条转写更新);ClientEnd 后 1.2s 用最新转写兜底提交;
  turnMu+submitted 保证 Final 与 ClientEnd 两路只提交一次(替换旧 lastFinal 去重)
- cmd/voicesim: 端到端模拟(免麦)——TTS 合成问话→灌网关语音WS→ASR转写→提交任务→
  大模型回答→TTS朗读回推,问答音频各存 wav,签发测试用户 JWT(auth.Issue)

真机验证:问"你是谁?你能做什么?"→ task_bab1c6ee → JARVIS 语音回答 18.6s(sim_answer.wav)。
麦克风音频→ASR→任务→大模型→TTS 全程走网关跑通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:08:17 +08:00
Blizzard 08e6425fec feat(voice): 双向流式 TTS 真机跑通——TTS合成→ASR往返全绿
对齐官方 python demo 修 TTS payload:
- TaskRequest 必须带完整 req_params(speaker+audio_params)再加 text,只发 {text} 火山收不到
  (会回空 text 的 TTSSentenceStart 后直接结束、无音频)
- StartSession 去掉自造的 namespace/user,就是 {req_params:{speaker,audio_params}}
- ttsReqBase 基底 StartSession/TaskRequest 复用;TTSSession 存 reqBase
- 加 VOICE_DEBUG 帧级调试日志(联调用,默认关无开销)

真机验证(voicecheck):TTS 合成 143KB PCM24k → 降采样喂 ASR → 转写"北京今天的天气怎么样?
需要带伞吗?"≈原句。ASR(volc.bigasr.sauc.duration)+TTS(seed-tts-2.0)两协议全通。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:49:43 +08:00
Blizzard d6c1a787f4 fix(voice): 改用火山「新版控制台」API Key 鉴权(ASR 握手真机验通)
一直误用旧版 5 头方案(X-Api-App-Key+Access-Key+App-ID),火山始终 401 grant not found。
用户提供官方文档「新版本控制台」证实:新版只需**单个 X-Api-Key** 头。改对后真机联调:

- frame.go setVolcAuthHeaders: X-Api-Key + X-Api-Resource-Id + X-Api-Request-Id + X-Api-Sequence:-1
  (删旧 App-Key/Access-Key/App-ID/Connect-Id;去 appID 参数)
- asr/tts 调用点同步;config.go 删 Config.AppID;voicecheck/voiceconfig 删 VOLC_APP_ID
- 删临时诊断工具 voiceprobe

真机结果:ASR 握手(volc.bigasr.sauc.duration 有效);TTS 用 seed-tts-2.0 会话正常建立
(不再 resource mismatch),但暂无音频返回→双向 TTS 事件/payload 待调(下一步)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:42:36 +08:00
Blizzard c5397438d4 docs(voice): 记录联调结论——协议验通,剩服务开通(账号侧)
联调确认:鉴权/帧协议已被真火山完全接受。剩 grant not found=账号未开通对应服务。
- TTS 正确 Resource-Id 是 volc.service_type.10029(语音合成大模型),非 seed-tts-2.0(那是模型名)
- ASR 端点 sauc/bigmodel 认 volc.bigasr.sauc.duration(格式被识别)
- 两者均 grant not found → App 9604175735 需在语音控制台开通这两个大模型服务

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:07:48 +08:00
Blizzard 8283bc542f fix(voice): 火山鉴权头精确大小写(X-Api-App-ID 免被 Go 规范化)
Go 的 Header.Set 会把 X-Api-App-ID 规范化成 X-Api-App-Id;改直接赋 map 保留精确
大小写,对齐官方 demo。鉴权方案已与参考实现(realtime_dialog demo)完全一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:59:58 +08:00
Blizzard bb9c73a9b7 fix(voice): 修正火山 V3 流式端点鉴权头(真火山联调验通协议格式)
联调发现 Authorization: Bearer 不被 openspeech V3 二进制流端点接受。改用官方规范头:
- X-Api-App-Key: 固定常量 PlgvMymc7f3tQnJ6(所有客户端同值,缺它 400 "app key not found")
- X-Api-Access-Key: 新版 API Key(账号鉴权)
- X-Api-App-ID: 账号 App ID(解析资源授权,缺它 401 "grant not found")
- X-Api-Resource-Id / X-Api-Connect-Id

改动:Config 加 AppID 字段;frame.go setVolcAuthHeaders 统一挂头 + handshakeDetail
榨取握手失败的 HTTP 状态/logid/body(联调可诊断);asr/tts 共用;voicecheck 加握手探针
分别验 ASR/TTS 端点;voiceconfig/voicecheck 读 VOLC_APP_ID。

现状:协议格式已被真火山接受(过了 400 格式错,进到账号/资源授权查询)。剩 App ID +
资源开通确认(账号侧,联调补)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:53:26 +08:00
Blizzard b718bdbb21 feat(voice): voiceconfig 工具——语音配置一键入库(联调/无头环境免开 admin)
等价于 admin「语音设置」保存一次:读 VOLC_* 环境变量→AES 加密(复用 gateway
EncryptedForStore)→写 sundynix_setting。网关每请求现读,无需重启。凭证只走 env 不进 git。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:42:47 +08:00
Blizzard 5b871bc363 feat(voice): 火山协议自检工具 voicecheck(本地端到端验协议,免麦克风免全栈)
不用部署、不用麦克风:本机直连火山公网端点,端到端验证手搓的 ASR/TTS 帧协议。
TTS 合成一句→收音频存 out_tts.wav→降采样 24k→16k 喂 ASR→打印转写。
转写≈原句即证明两套协议都被真火山接受。凭证只走环境变量(VOLC_API_KEY 等),不进 git。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:33:55 +08:00
Blizzard 9458bf21f3 feat(voice): 桌面端 JARVIS 语音坞——麦克风上行 + TTS 播放队列(Phase 1 收口)
右下角悬浮麦克风:点按说话→转写→提交任务→跳运行页→朗读回答,接入既有运行·观测流。

- lib/voice.ts: VoiceClient——一条 WS 承载上行 PCM16k/下行转写/下行 TTS PCM24k;
  ScriptProcessorNode 采麦(48k→16k 降采样+Float32→Int16);AudioBufferSourceNode
  排队调度做无缝连续朗读;start/end/barge_in/bye 控制;ready/transcript/task/speaking/tts_end
- shell/VoiceDock.tsx: 悬浮麦克风 UI(聆听/思考/朗读态 + 转写气泡 + 打断),懒建客户端(用户手势启 AudioContext)
- App.tsx: onVoiceTask 把语音 task 挂回 attachRun(后端已提交,前端只 attach);挂载 VoiceDock

真识别/合成需部署联调(真连火山+admin录入语音配置);本地过 tsc/build/68 测试。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:29:47 +08:00
Blizzard 6fe0a58f1b feat(voice): 火山双向流 TTS 客户端 + 下行接线(Phase 1 嘴巴)
回答 token 流 → 攒句器 → 火山双向 TTS → 音频帧回推客户端,端到端连续朗读打通。

- tts_frame.go: V3 事件族帧编解码(事件号+会话ID+gzip),与 ASR 简帧不同族;
  从官方参考实现核实 generate_header/parse_response 字节布局;3 解析单测
- tts.go: seed-tts-2.0 双向流客户端 StartTTS(握手ConnectionStarted/SessionStarted)
  /Speak(逐句TaskRequest)/Finish/Audio()/Close,PCM 24k;新版 API Key 鉴权
- voice_tts.go: speak() 先订阅token流再建TTS(core NATS无持久,握手期攒句入pending
  就绪补吐,不丢开头);音频泵首帧ServerSpeaking、收尾ServerTTSEnd;打断stopTTS
- voice.go: barge_in→stopTTS;会话结束连带停TTS;final转写→go speak(taskID)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:25:49 +08:00
Blizzard 302e1ebaff feat(voice): 上行接线——最终转写→组DSL→提交任务(Phase 1 打通)
ASR 最终转写触发一次任务提交,复用 HTTP SubmitTask 那条共用关卡
(preflightCore/launchCore),语音只是"嘴替键盘",编排/工具/计费一行不新造。

- task_handler.go: preflight/launch 抽出无 gin 内核 preflightCore/launchCore
  (preflightBlock 承载拦截态),gin 版做薄封装;语音会话无 gin.Context 也走同一关卡
- voice_task.go: buildVoiceGraph(转写→input→agent 单图) + submitVoiceTask(校验/关卡/落库发射)
- voice.go: 会话升级时抓租户/会话;结果 goroutine 见 Final→onFinalTranscript 提交、
  回 ServerTask{task_id};去重连发的重复 final;画布图一次性消费
- voice_task_test.go: 组图合法性 + 带转写 + input→agent 连边

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:16:29 +08:00
Blizzard 4ee4a91a51 feat(voice): 火山 ASR 流式识别客户端 + 会话接线(Phase 1 耳朵)
voice/asr.go: 连 wss://openspeech.bytedance.com/api/v3/sauc/bigmodel,新版 API Key 鉴权
(Authorization: Bearer + X-Api-Resource-Id + Connect-Id),发初始配置帧(bigmodel/zh/ITN/
标点/VAD),PushAudio 流式喂 PCM、Finish 收尾;读 goroutine 解析响应(result 支持数组/对象/
字符串,type=final 为最终)推入 Results 通道。

handler/voice.go 接线:onAudio→PushAudio、start→重开识别、end→Finish;起 goroutine 把
转写 send(transcript) 实时回推客户端。加 writeMu 串行化写(读循环与 ASR 结果 goroutine
都写同一 WS,gorilla 禁并发写)。连接结束 stopASR 收尾。

带单测(配置JSON字段/result三形态解析)。真识别需部署联调(要真连火山);API Key 是否
还需 X-Api-App-Key 联调若 401 再补。上行接线(final→SubmitTask)与 TTS 是下一步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:27:32 +08:00
Blizzard b526b21dee feat(voice): 火山语音 WS 二进制帧编解码(ASR/TTS 共用核心)
从官方参考实现核实的 V3 协议帧格式,港到 Go:byte0=0x11、byte1=(msgType<<4)|flags、
byte2=序列化<<4|压缩、byte3=0x00、大端 uint32 payload 长度、payload;服务端响应含 4B
序列号故 payload 从第 12 字节起。encodeFrame/jsonFrame/audioFrame/parseServerFrame +
msgType(0x01 config/0x02 音频/0x09 结果/0x0F 错误)与 flags(0x02 最终)常量。
3 个单测钉死头布局/音频最终帧/响应解析(确定性,不依赖网络)。

ASR 初始配置 JSON、WS 连接与流式识别(接 onAudio)是下一步网络层实现。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:22:09 +08:00
Blizzard 15f5a85612 feat(voice): WebSocket 端点 + 客户端↔网关协议(Phase 1 地基)
加 gorilla/websocket(无 genproto 冲突)。voice/protocol.go 定死单条 WS 的消息协议:
二进制帧=音频(上行麦克风/下行TTS),文本帧=JSON 控制/事件(ClientMsg:start/end/barge_in/
bye;ServerMsg:ready/transcript/task/speaking/tts_end/error);音频 PCM 16k 单声道。
handler/voice.go: GET /api/v1/voice/stream 升级 WS,鉴权走 AuthFromHeaderOrQuery(?token=,
WS 带不了 Bearer),会话外壳 + 协议读循环(音频帧/控制消息分派)已通,火山 ASR/TTS 客户端
在下一步挂 onAudio/onControl 的 TODO 点接入。build+vet+test 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:17:58 +08:00
Blizzard a28ae49b6a refactor(voice): 语音配置改用新版 API Key 鉴权(弃旧版 appid+token)
用户提醒:火山新版走 API Key 鉴权,不用旧版 appid+access_token。新版 WS 握手只带两个
header——Authorization(Bearer <APIKey>) + X-Api-Resource-Id。

配置从 {appid, access_token, 2×resource-id, voice} 收敛为 {api_key, 2×resource-id,
voice}:APIKey 走 secrets AES 加密入库;ASREnabled/TTSEnabled 改为只看 api_key+对应
resource-id。admin 配置页两个字段并一个 API Key 字段,清单同步改为新版口径。build+tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:06:22 +08:00
Blizzard 618184689e feat(voice): 语音配置控制面——火山豆包 appid/token/resource-id/音色(参数落地处)
JARVIS 不依赖火山账号能先做的第二块:配置层,做成'你参数一填就落库'的形态。
- voice.Config: AppID/AccessToken/ASRResourceID/TTSResourceID/TTSVoiceType,AccessToken
  走 secrets AES 加密入库(镜像微信配置);ASREnabled/TTSEnabled 可各自独立判断。
- admin GET/PUT /admin/voice(RequireAdmin),token 空串=沿用已存、明文回显。
- admin「语音设置」页(运维组):五个字段 + ASR/TTS 就绪徽标 + '去哪拿参数'清单。

端点/音频格式(PCM 16k 单声道)由代码固定不入配置。WS 会话 + 火山 ASR/TTS 客户端等
用户回传 resource-id 后按官方 demo 协议做。build+tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:00:35 +08:00
Blizzard 644b635d73 feat(voice): 攒句器——语音下行 TTS 的 token 攒句(JARVIS 地基第一块)
语音交互(VOICE_DESIGN.md)不依赖火山账号的第一块地基:SentenceBuffer 把 LLM 逐 token
输出攒成'适合喂 TTS 的句子片段'。逐字喂 TTS 太碎(单字合成不自然、首包延迟高);句末标点
(。!?.!?;;换行)即成一句,从句标点(,,::)且攒够 12 rune 也吐(让长回答尽早出声)。
跨 Push 续半句,Flush 收尾吐无标点结尾。纯逻辑无外部依赖、4 个单测(跨 Push/短从句不断/
长从句先吐/Flush 收尾)。

WS 会话 + 火山 ASR/TTS 客户端 + 控制面配置等到真 API 参数到手再按真形状做,不写猜测桩。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:30:50 +08:00
Blizzard f16f63284f feat(cluster): gateway 后台定时器加 leader 锁——多副本安全(B5)
此前两个 gateway 定时器(订阅推进/掉单补偿)每实例各扫一遍:多副本下重复查库,
且对微信查单调用量随副本线性放大(微信有频控,先被限流的是自己)。

store.TryRunExclusive:用 PG advisory try-lock 做集群级单实例执行(leader 选举)——每轮
tick 非阻塞抢锁,抢到才跑、跑完释放;抢不到说明别的实例是 leader、本轮跳过。自愈:锁随
持有连接释放,leader 挂了下一轮别的实例自然抢到接管,无需显式故障转移。订阅/补偿用不同
锁键(可由不同实例分别 lead)。非 PG(sqlite 测试)/无 DB → 退回本地直接跑(单实例安全)。

两个定时器的 tick 各包一层 TryRunExclusive。至此 gateway 可安全多副本(SSE 走共享 Redis
流无需粘性、状态全外置、JWT 无状态,剩数据层 HA 属 C 层 ops)。带 fallback 单测。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:21:31 +08:00
Blizzard 9b153871eb feat(monitor): NATS 集群 Raft 副本健康 + 基建 ping 延迟(监测组完善)
此前 /status 把 NATS 当一盏二元灯(连不上就 fatal 故恒真),看不出 3 节点集群里
哪个节点掉了、JetStream 持久流的 Raft 副本是否还齐(计费/状态/评测流不丢的关键)。
DB/Redis/MinIO 也只二元 ping、无延迟。

- bus.ClusterStatus:连的节点名 + 集群发现节点数(nc.Servers) + RTT(nc.RTT) + 6 条关键
  持久流(tasks/status/usage/eval/ingest/approvals)的 Raft 副本健康(leader + healthy/total,
  单节点部署记 1/1;某节点掉队 → healthy<total 记降级)。
- /status 新增 nats 集群对象 + NATS 灯改为'连接且无副本降级才绿'、detail 显示'N 节点·连 X·
  流副本齐全/降级'、latency=RTT;PG/Redis/MinIO 加 ping 往返耗时。
- admin StatusPage:新增 NATS 集群面板(节点/RTT/连接 + 各流 leader/副本健康/消息数表),
  基建行显示延迟。

sundynix-shared/gateway build+vet+test 绿;admin tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:15:22 +08:00
Blizzard a42f30239f fix(payment): 微信 API 调用加超时兜底(B3 出网韧性)
微信是外部第三方、最可能慢/挂,而 wechatpay-go SDK 默认 http.Client 无 Timeout:
一次卡住的 Prepay/QueryOrder 会拖住请求 goroutine;尤其掉单补偿定时器用的是
context.Background()(无超时)→ 微信一挂那轮 tick 无限期卡死。

双保险:客户端级 HTTP 超时(WithHTTPClient Timeout=15s,belt) + CreatePay/QueryOrder
每次调用 ctx 超时(suspenders,兜住 SDK 忽略或背景 ctx 的情况)。

范围克制:只硬化微信这个真外部依赖。PG/Redis(连接池+降级)、NATS(无限重连)、mcp-go RAG
客户端(embed 30s/chat 60s/rerank 20s 已有超时)本就有韧性,不额外套熔断(那对本规模是过度设计)。

build+vet+test 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:01:09 +08:00
Blizzard ebbeed90e9 feat(auth): 登录账户级失败锁定 + 密码强度 8-72 位(B4)
补上 A5 只做了 IP 限流、没做账户锁定的缺口(审计 B4)。

- 账户级失败锁定:连续登录失败达 5 次即临时锁定 15min(Redis 计数+TTL),锁定期直接拒绝
  不再校验密码,成功登录即清零。与 A5 的 IP 限流互补:IP 限流挡'一个 IP 猛打',账户锁定
  挡'分布式慢速猜一个号'。权衡:per-account 可被人为锁死受害者(lockout DoS),故窗口设短
  自动解锁+叠加 IP 限流;Redis 降级则不锁定(尽力而为,IP 限流仍在)。
- 密码强度:注册从'至少 6 位'提到 8-72 位。上限 72 字节——bcrypt 超 72 字节静默截断,
  不拦会让超长密码实际只用前 72 字节。桌面端注册提示同步改 8-72 位。

存量用户不受影响(只校验注册/改密时的新密码)。build+vet+test 绿;desktop tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:56:29 +08:00
Blizzard da9c76f073 feat(prod): 实时就绪探针——gateway /readyz 真 ping + dispatcher/mcp-go 加 HTTP 探针(B2)
此前 gateway /readyz 用的是 Enabled() 启动期降级标志(反映不了运行中 PG 掉线,LB 会继续
往已不可用实例导流);dispatcher/mcp-go 干脆没有 HTTP 探针(只有 NATS ServeHealth,k8s/LB
够不着、只能靠 gateway 经 NATS 代探)。

- gateway /readyz 改用 db.Ping 实时探活。只把 **DB 当硬依赖门**:Redis 掉线仍可服务(限流有
  A5 进程内 fail-safe 兜底、SSE 回落 live NATS),一 blip 就把全部实例踢出轮转反而制造整站
  故障,故 Redis 只上报不 gate。NATS 启动即连(fatal)不单列。
- 新 sundynix-shared/health 包:Serve/Handler 提供 /healthz(恒 200 liveness) + /readyz(由
  ready() 决定 readiness)。dispatcher(:8091, DISPATCHER_HEALTH_ADDR)与 mcp-go(:8092,
  MCP_GO_HEALTH_ADDR)各起一个,readiness = NATS 连接可用(bus.IsConnected)。接入各自优雅停机。
- bus 加 IsConnected()(nc.IsConnected 实时);dispatcher Subscriber 透传。

四模块 build+vet+test 绿;health 带 Handler 单测。探针端口仅内部用(对外仍只暴露 gateway)。
compose/k8s 的 healthcheck 编排属 C 层 ops,另做。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:50:31 +08:00
Blizzard 4500335da7 refactor(store): 迁移路径务实硬化——advisory lock + 版本表 + 破坏性迁移移出启动路径(B1)
此前每次启动裸跑 AutoMigrate + 一串手写索引/回填,且启动路径上有两处 DROP TABLE
CASCADE 的 legacy 迁移;多实例并发启动无锁 → 并发 ALTER/建索引竞争,一方报错即掉降级。

务实硬化(不引外部工具,保留 gorm 结构体为源):
- PG advisory lock:整段迁移在 pg_advisory_lock 内串行,多实例同时启动只有一个进锁跑,
  其余阻塞等待。取锁 60s 超时兜底(取不到带告警继续,AutoMigrate/索引多幂等)。
- 破坏性 legacy 迁移移出默认路径:migrateLegacyIntIDs/migrateDocLinkToID(DROP TABLE
  CASCADE)默认不跑,仅 ALLOW_LEGACY_SCHEMA_MIGRATION=1 时执行;检测到旧 schema 但未开
  只告警不动手。现网早已是雪花 id 本就不触发,但从此不再是启动就可能 DROP。
- 版本化 runner:AutoMigrate 之外的步骤(3 个部分唯一索引 + NULL 余额回填)登记为
  schemaSteps,各跑一次并记入 sundynix_schema_migration 表,下次跳过;某步失败即停、
  不记录、下次重试。将来 AutoMigrate 做不了的破坏性/数据迁移在末尾追加新 id 即可。

pgsql.go 的迁移块收敛为一句 runMigrations(db)。带 runner 单测(跑一次/跳过/追加/失败即停)。
现网 DB 已有的索引/回填重跑无害(IF NOT EXISTS / WHERE IS NULL),跑后记录。build+vet+test 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:26:58 +08:00
Blizzard ac38d5e663 fix(prod): 后端生产级 A 类硬伤全清(7 项:授权/崩溃点/全表扫/限流/鉴权/限额)
部署前生产级审计(可靠性/数据层/安全三路)后,清掉 7 处代码级硬伤:

A1 后台定时器 goroutine 无 panic recover → 单个 DB panic 崩整个 gateway。加 safeGo/
   safeCall,包住订阅/掉单补偿/微信推送/探针 goroutine,单轮 tick 再兜一层。
A2 提示词控制面(建/激活/停用,热广播全服务)只 RequireAuth → 任意登录用户改全局提示词。
   三写端点+列表挂 RequireAdmin。
A3 HITL 审批端点无角色门 → viewer 可放行烧钱执行。加 RequireTenantRole(member)。
A4 审计/护栏列表 limit 无校验,limit=-1 让 gorm 取消 LIMIT 全表扫。加 clampLimit/
   clampOffset,AdminTasks/AdminSpaces 补上界。
A5 限流 Redis 一挂就完全放行(fail-open)。加进程内固定窗口兜底(fail-safe) + 登录/注册
   按 IP 专用严限流(10/min)。
A6 公开 by-id 端点(stream/exec/report导出/kb导入流)无鉴权无租户过滤。加
   AuthFromHeaderOrQuery(从 ?token= 取 JWT) + task/report 按 owner 归属校验;桌面端
   5 处 EventSource/下载 URL 经 tokenQuery 附 JWT。
A7 文件上传无大小上限(整文件进内存 OOM 面) → 50MB 闸(KB_MAX_UPLOAD_BYTES)+ LimitReader;
   http.Server 加 ReadHeaderTimeout/ReadTimeout/MaxHeaderBytes(不设 WriteTimeout 保 SSE)。

带单测:clampLimit/safeCall/procLimiter/AuthFromHeaderOrQuery/TaskOwner。
build+vet+全量 test 绿;desktop tsc 绿。B(迁移工具/实时探针/出网韧性/登录锁定/leader选举)
与 C(TLS/PG HA/K8s/备份自动化/可观测)分期后做,参照 production_readiness.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:04:47 +08:00
Blizzard ce7cca657e fix(backend): 补齐三处漏洞——暂停租户拦截/金额不符落审计终态/邀请码列表滤失效
P0 暂停租户是空开关:admin 能设 suspended,但 preflight 只查预算+余额、不看
租户 status → 暂停后照样能提交烧积分。加 TenantSuspended 校验(活跃租户 + 分叉时
的计费租户都拦),403 拒绝。

P1 金额不符只刷日志:回调/查单判了不符却没落审计、订单永远卡 pending 被补偿定时器
每轮重扫刷屏。加 disputed 终态 + MarkOrderDisputed(CAS 只挂一次) + 审计(首次写一次);
disputed 不在 pending 扫描内,停止无限重扫。admin /orders?status=disputed 可查。

P1 邀请码列表混入失效码:ListInvites 只按 status=active 过滤,过期/满员的码仍显示为
有效、误导邀请人。有效列表加 expires_at>now 且 used<max 过滤(RedeemInvite 本就会拒,
这里修的是展示一致性)。

三处均带 store 单测(TenantSuspended/MarkOrderDisputed CAS/ListInvites 过滤)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:20:34 +08:00
Blizzard bd829cfecb feat(invite): 租户成员二维码邀请(可复用团队码,扫码关注即入组)
owner/admin 在桌面端生成一张微信带参二维码发给团队,成员用微信扫码关注
即自动加入租户,并收到「 已加入团队【X】」被动回复。与登录二维码同一微信机制,
scene 加 inv_ 前缀分流;扫码入组走被动回复,不需要 access_token、不碰 IP 白名单。

- store: TenantInvite(可复用码=有效期+人数上限+可撤销三道闸);RedeemInvite 幂等入组、
  同一人重复扫不重复消耗名额、复活已移除者;带单测钉死过期/撤销/满员/幂等。
- handler: 建码/列表/撤销端点(RequireTenantRole admin);WxMPEvent inv_ 分支。
- 顺手修潜在生产 bug:微信用户此前都建成空邮箱,User.Email 整列唯一索引下第二个微信
  用户就撞唯一约束建号失败(登录"全通"只因当前仅一个微信用户)。改为按 openid 合成占位
  邮箱 wx-<openid>@wx.local,绕开冲突且不动索引。邀请功能会批量建微信用户,非修不可。
- desktop: 顶栏「邀请成员」入口(租户 admin/owner 可见) + 二维码弹窗(生成/大图展示/撤销)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:47:21 +08:00
Blizzard 68608c1592 feat(wechat): 支付成功后微信客服消息推回执(积分包/订阅购买)
用户扫码付款后(在 48h 互动窗口内),主动推一条客服消息回执:
到账积分/开通套餐 + 当前余额。只在 MarkOrderPaid changed=true 首次到账时推,
异步+超时隔离,失败只记日志、绝不影响入账。非微信用户自动跳过。

不做「周期刷新提醒」:客服消息受 48h 窗口限制,定时刷新那刻用户多半已超窗、
必然失败,那类隔天提醒须用模板消息(暂缓)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:23:41 +08:00
Blizzard 83269e067a feat(wechat): 关注公众号后自动回复欢迎语(被动回复,可后台配置)
用户关注服务号(扫登录码后关注 或 直接搜索关注)时,在回调 HTTP 响应里
回一条文本消息(微信「被动回复」)。被动回复不需要 access_token、不受 IP 白名单
限制,永远能发。欢迎语在管理端「登录设置」可配,留空用默认。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 11:17:41 +08:00
Blizzard 5d67821697 Merge pull request 'feat(auth): 微信 access_token 走中控服务器(腾讯云静态 IP 换 token)' (#11) from feat/site into main
deploy-132 / deploy (push) Successful in 2m35s
Reviewed-on: #11
2026-07-21 02:42:16 +00:00