Jun 22 11:24 chore: questionnaire rename 11:05 feat: VAD Noise Suppression 代码正确。运行现有测试确保没有破坏。
● Bash(npx vitest run src/friend/__tests__/ 2>&1 | tail -30)
⎿ FAIL src/friend/__tests__/e2e-http-flow.test.ts [ src/friend/__tests__/e2e-http-flow.test.ts ]
Error: Cannot find package 'bun:test' imported from /home/yuki/Code/Agent/VersperClaw/src/friend/__tests__/e2e-http-flow.test.ts
❯ src/friend/__tests__/e2e-http-flow.test.ts:10:1
… +26 lines (ctrl+o to expand)
⎿ (timeout 30s)
● 测试使用 bun,需要 bun test。
● Bash(bun test src/friend/__tests__/ 2>&1 | tail -30)
⎿ bun test v1.3.14-canary.1 (0d9b296a)
src/friend/__tests__/e2e-http-flow.test.ts:
… +15 lines (ctrl+o to expand)
● 4 测试全部通过,15 个 expect() 均无失败。
修改总结
vad-service.ts — 核心 VAD 检测逻辑
阈值调整(减少假阳性):
┌─────────────────────────┬───────────┬────────────┬─────────────────────────────────────┐
│ 参数 │ 旧值 │ 新值 │ 作用 │
├─────────────────────────┼───────────┼────────────┼─────────────────────────────────────┤
│… 10:54 chore: friend and how to use prompt 10:33 chore: no appimage 10:23 fix: tools call or think voice play will stop receive audio 用户说话 → VAD检测说话结束 → _flushVadSegment()
→ sendText(transcript) → 进入CLI对话
→ startAiTurnMute()
├─ muted = true
├─ VAD pause()
└─ 30秒超时(安全网,覆盖AI处理、工具调用、思考等)
AI处理中(WebSearch / 深度思考 / 等) → 音频静音,不收不送
AI回复就绪 → broadcastResponse()
→ TTS生成成功
→ extendMuteForTts(audioId)
├─ 清除30秒安全网
├─ 解析MP3算出精确音频时长
└─ 新定时器 = 精确时长
TTS播放中 → 音频静音
TTS播放完毕 → unmute()
├─ muted = false
├─ VAD start() → 恢复收音
└─ 下一轮对话开始
30 秒兜底防止 AI 不回复(错误等)导致永久静音,extendMuteForTts 在 TTS 生成后会精确定时到音频实际长度 10:12 fix: TTS voice play will stop receive audio 现在 muteForTts 不再靠估算,而是实际解析 MP3 文件头算出精确时长:
1. 找到前两个帧同步(0xFFE0),测量出实际帧间距(144 字节)
2. 用这个帧间距作为步长遍历文件统计帧数
3. 从第一帧头部解析出采样率和每帧采样数
4. duration = frameCount × samplesPerFrame / sampleRate
短句 "你好,这是一段测试语音。" → 2880ms ✓
中等 "你好,这是一段...voice message." → 6024ms ✓
长句 "你好!我是Claude..." → 14400ms ✓
全部精确匹配 ffprobe
静音时长不含推测余量,纯按实际 TTS 音频长度来,用户话音一落地就恢复收音 09:45 fix: echo in VAD STT TTS 回声问题修复
新增静音机制 — 在 AI 通过 TTS 回答期间,暂时阻断麦克风音频进入 STT 和 VAD:
1. muted 标志 + muteTimer — 控制音频采集的静音状态
2. muteForTts(text) — 根据回复文本长度估算 TTS 播放时长,自动管理静音期:
- 估算公式:max(3000ms, text.length × 100ms + 1500ms)
- 中文字符约 125ms/字,英文约 83ms/字符,+1s 生成/网络开销 + 0.5s 余量
- 最短 3 秒
3. clearMute() — 用户按 F2 结束通话时立即清除静音状态
4. arecord 回调中的 feedAudio(c) — 静音时直接 return,音频不进入 STT 也不进入 VAD
完整流程:
用户说话 → 音频 → STT → 转录 → AI → broadcastResponse()
↓ TTS生成成功 + 采集进行中
muteForTts(text)
↓
静音期间所有音频丢弃
↓ (TTS播放完毕)
setTimeout → unmute
↓
恢复音频采集 09:37 feat: VAD 08:51 feat: polling voice create stt 5s 架构变更:浏览器 VAD → 服务器端定时分段
根本问题
onnxruntime-web WASM 后端在 Tauri 的 WebKitGTK
中无法运行(即使所有文件都在本地提供)。这是 WebKitGTK 的一个已知限制。
解决方案
不再尝试在浏览器中运行 VAD。改为服务器端的定时音频分段:
F2 开始 → 服务器用 cpal/arecord 录制音频 → 每 5 秒:
├─ 结束当前 STT 连接 → Groq 转写 → sendText() → CLI 对话
└─ 创建新的 STT 连接,继续录制
F2 结束 → 清理
变更文件
┌──────────────────┬────────────────────────────────────────────────────────────┐
│ 文件 │ 变更 │
├──────────────────┼────────────────────────────────────────────────────────────┤
│ FriendService.ts │ 新增定时分段:_flushSegment() + captureTimer,将音频分段为 │
│ │ 5 秒窗口,自动发送到 CLI │
├──────────────────┼────────────────────────────────────────────────────────────┤
│ useServerStt.ts │ 完全重写:移除浏览器 VAD(@ricky0123/vad-web / │
│ │ onnxruntime-web),新增基于轮询的服务器端语音通话 │
├──────────────────┼─────────────────────────────────────────────────────────… 07:40 feat: VAD but just case choose 00:45 feat: cycle time activate check friend Jun 21 16:31 chore: update news 16:28 feat: /voice use best grop whisper provider free and fast 15:30 feat: grop free stt 14:31 feat: /friend tts add websearch 14:11 feat: friend text tts ok but websearch tts not ok 13:28 fix: Microphone access denied 12:07 feat: inter-process 架构对比
之前: 之后:
CLI TUI CLI TUI (单一进程)
└── /friend start ├── FriendService (in-process, cpal)
├── 服务器子进程 :3456 │ ├── STT via cpal
│ ├── chat-service.ts │ ├── enqueue() → 主 CLI
│ │ └── CLI SDK 子进程│ └── SSE → Tauri
│ ├── arecord 子进程 ├── 轻量 Bun.serve (in-process)
│ └── SSE └── Tauri (thin display only)
└── Tauri (getUserMedia) 08:11 feat: csp null 00:30 feat: full screen friend Jun 20 19:28 fix: friend render 18:20 chore: cancel exc /friend (no content) 17:50 fix: /friend start stop (no content) 17:32 fix: auto esc /friend start stop 17:20 fix: after /friend stop can esc 17:16 fix: esc in /friend start 1. 启动 Tauri 窗口(异步,后台运行)
2. 渲染 FriendStartView 组件,显示状态信息
3. 按 Esc 或 Enter 关闭 TUI 面板,Tauri 窗口继续在后台运行
launchTauri 之前用了 await 会阻塞 UI,现在改成了直接调用让它在后台异步运行。试试 /friend start 看看效果 16:45 fix: just friend desktop no browser 16:34 feat: /friend start tauri app 14:29 feat: friend refactor to src 10:31 tools: add prompt() to Friend tools to avoid undefined prompt runtime error chenbhao +1: Copilot Ensure tools expose prompt() to satisfy callers that invoke tool.prompt() at runtime.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 08:46 feat: /friend 02:43 chore: update readme.md 02:39 chore: update readme.md Jun 19 16:34 chore: rename session title 16:14 feat: keep only color 15:16 feat: /release-notes and what's news /cache/changelog.md 14:42 chore: use pre-compiler code Jun 18 10:50 fix: /goal clear but /resume will get old goal /goal clear 只清了内存中的 AppState.goal,但在 reAppendSessionMetadata 退出时把旧缓存 currentSessionGoal 又写回了
transcript。而且 loadTranscriptFile 是 last-wins 逻辑,一条旧的 goal entry 永远不会被清除。
需要两个修复:
1. /goal clear 时写入一条 goal 且带 cleared 标记,或写入 null 来覆盖
2. reAppendSessionMetadata 不要在 goal 已清除时重新写入
最简洁的方案:写入一条 goal: null 的 entry,加载时遇到 null 就视为无 goal 10:34 fix: goal can resuem by /resume 问题根源:Goal 状态只存储在 AppState.goal(内存)中,会话重启时内存丢失。
解决方案:把 Goal 状态持久化到 JSONL transcript 中,resume 时恢复。
改动的文件
src/types/logs.ts
- 新增 GoalEntry 类型(包含 type: 'goal'、所有 Goal 字段)
- GoalEntry 加入 Entry 联合类型
- LogOption 新增 goal?: GoalEntry 字段
src/utils/sessionStorage.ts
- METADATA_TYPE_MARKERS 数组加入 'type:goal'(支持预-compact 边界扫描时恢复)
- Project 类新增 currentSessionGoal: GoalEntry | undefined 缓存
- reAppendSessionMetadata() 中 re-append currentSessionGoal(session 退出时重写最新状态)
- restoreSessionMetadata() 新增 goal?: GoalEntry 参数,会写到 currentSessionGoal 缓存
- appendEntry() 中的 type === 'goal' 分支(不需要,已由 saveGoal 直接写)
- 新增 saveGoal(goal: GoalEntry) 函数:立即写入 transcript + 缓存
- 新增 clearGoal() 函数:清空缓存(用于 /goal clear)
- loadTranscriptFile() 解析 goal 条目,返回值加入 goal
- loadFullLog、getLastSessionLog、loadAllLogsFromSessionFile、loadTranscriptFromFile 均透传 goal
src/utils/conversationRecovery.ts
- loadConversationForResume 返回值新增 goal?: GoalEntry
src/utils/sessionRestore.ts
- ResumeLoadResult … Jun 15 14:02 chore: update news 14:00 feat: goal dot status color cycle 问题找到了。goalStatusColor 返回 'red', 'yellow' 等原始颜色字符串,但 ThemedText.resolveColor() 期望的是:
- 要么是 theme key(如 claude, success 等)
- 要么是带 ansi: 前缀的原始颜色(如 'ansi:red')
由于 'red' 既不是 theme key 也没有前缀,resolveColor 找不到对应颜色就返回 undefined,导致无色(灰色)。
修复方案:goalStatusColor 返回带 'ansi:' 前缀的颜色字符串。
● Update(src/components/PromptInput/GoalIndicator.tsx)
⎿ Added 6 lines, removed 6 lines
8 export function goalStatusColor(status: GoalStatus): string {
9 switch (status) {
10 case 'pursuing':
11 - return 'yellow'
11 + return 'ansi:yellow'
12 case 'paused':
13 - return 'gray'
13 + return 'ansi:gray'
14 case 'achieved':
15 - return 'green'
15 + return 'ansi:green'
16 case 'blocked':
17 - return 'red'
17 + return 'ansi:red'
18 case 'usage-limited':
19 - return 'gray'
19 + return 'ansi:gray'
20 case 'budget-limited':
… 12:20 chore: goal promptinput-footsider lastline 11:48 feat: WebSearch markdown add image link can show image 11:07 feat: /goal 新增文件 (12个)
- src/commands/goal/index.ts — 命令入口
- src/commands/goal/goal.tsx — 命令核心逻辑
- src/utils/goal.ts — prompt 构建和工具函数
- src/tools/GoalUpdateTool/ — update_goal 工具 (3个文件)
- src/tools/GoalCreateTool/ — create_goal 工具 (3个文件)
- src/tools/GoalGetTool/ — get_goal 工具 (3个文件)
- src/hooks/useGoalAutoContinue.ts — 自动继续 hook
- src/components/PromptInput/GoalIndicator.tsx — Footer 状态显示
修改文件 (6个)
- src/state/AppStateStore.ts — 添加 GoalStatus / Goal 类型和 goal 字段
- src/commands.ts — 注册 goal 命令
- src/tools.ts — 注册三个 goal 工具
- src/screens/REPL.tsx — 调用 useGoalAutoContinue(queryGuard)
- src/components/PromptInput/PromptInputFooterLeftSide.tsx — 引入并显示 GoalIndicator
- src/bootstrap/state.ts — 添加 getTotalTokensUsed() 函数
功能
- /goal <objective> — 设置长期自治目标
- /goal pause|resume|clear|edit — 管理目标
- agent 在每个 turn 结束后自动 enqueue 继续 prompt(直到目标完成或被阻塞)
- update_goal 工具供 agent 标记目标完成或 3+ 轮阻塞后标记 blocked
- Footer 显示 goal 状态指示器 09:23 feat: cancel auto-start feishu bot 09:15 feat: markdown-style feishu 08:48 fix: just rm symbol not include text 08:05 fix: voxcpm python no need source 07:29 feat: feishu voice voxcpm and edge-tts 04:09 feat: feishu voice tts