工作中氣泡顯示工具動作與助理說明
摘要: running 狀態的氣泡改成有東西就播報:優先顯示助理剛說那句話,否則顯示正在用的工具與對象。 根本原因: 使用者希望氣泡能像 telegram bot 那樣顯示 AI 的思考過程。 實際查證後思考文字拿不到——Claude Code 從 2.1.238 起不再把 thinking 寫進 transcript, 只留加密簽章(本 session 169 個 thinking 區塊文字全為空字串); 比對同樣用 claude-opus-5 但版本為 2.1.237 的舊 session 則存得到, 確認是 Claude Code 版本差異而非模型差異。Claude Code 也沒有對外事件串流, 所以 hook 拿不到思考內容,這點與 Codex CLI 的 --json reasoning 事件不同。 而參考的 cluemarket-tg-bot 其實也沒顯示思考:它的變數雖名為 lastThinkingText, 抓的卻是 text 區塊,另外兩個是寫死的「思考中」字串與工具標籤。 原本 running 狀態除了關閉舊氣泡外沒有任何資訊。 影響: 工作期間看不出 Claude Code 正在做什麼,只知道她在忙。 修法: - hook 補送 toolTarget(依 command / file_path / pattern / url 等順序取一個欄位並截斷, 避免 Write 這類工具把整份檔案內容塞進 payload)、agentType 與 transcriptPath。 - main.js 新增 latestNarration:由 transcript 尾端 192 KB 反向找最後一個非空的 assistant text 區塊,依檔案大小快取,transcript 長到數 MB 也不會變慢; 收到事件時把 narration 與其 uuid 併進 payload 再轉給 renderer。 - renderer 新增 sayActivity:有新的助理說明就顯示第一句,否則顯示工具標籤加對象。 同一段說明只播報一次;waiting 時不播報以免蓋掉固定氣泡。 另加 stripMarkdown,否則助理文字的 **粗體**、反引號、標題與連結語法會直接出現在氣泡裡。 - README 新增「能顯示什麼、不能顯示什麼」一節說明思考文字為何拿不到。 驗證: - 單元測試以 DOM stub 載入完整 renderer.js 並抽出 main.js 的 latestNarration,15 項全過: 對真實 transcript 取得助理說明與 uuid、快取一致、路徑不存在或為 null 時回 null 不拋錯、 firstSentence 取第一句與忽略空行、Bash 取指令前段、檔案路徑只取檔名、過長截斷、 新說明更新 id、同段不重複、waiting 時不播報、空 payload 不拋錯。 - markdown 清理另 7 項全過:粗體、反引號、標題、清單、連結、程式碼區塊、無標記純文字。 - 實機啟動並以真實事件驗證:events.log 顯示 hook 確實帶出 toolTarget 與 transcriptPath; 截圖確認氣泡顯示「⚙️ 執行 npm run dist」,以及傳入真實 transcript 後顯示「💬 助理說明」。 markdown 殘留問題就是在這一步的截圖中發現並修掉的。 - 以 ELECTRON_RUN_AS_NODE 讀打包後 app.asar 與 app.asar.unpacked,確認版本 2.0.4 與九項改動都在出貨檔案裡。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,52 @@ function appendLog(line) {
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ---------- transcript:取最新的助理說明文字 ----------
|
||||
|
||||
// Claude Code 從 2.1.238 起不再把 thinking 的文字寫進 transcript(只剩加密簽章),
|
||||
// 但助理的可見說明(text 區塊)還在。讀檔尾就夠了,transcript 會長到好幾 MB。
|
||||
const TRANSCRIPT_TAIL = 192 * 1024;
|
||||
const narrationCache = new Map(); // path → { size, id, text }
|
||||
|
||||
function latestNarration(file) {
|
||||
if (typeof file !== "string" || !file) return null;
|
||||
let stat;
|
||||
try { stat = fs.statSync(file); } catch { return null; }
|
||||
const cached = narrationCache.get(file);
|
||||
if (cached && cached.size === stat.size) return cached.value;
|
||||
|
||||
let value = null;
|
||||
try {
|
||||
const start = Math.max(0, stat.size - TRANSCRIPT_TAIL);
|
||||
const fd = fs.openSync(file, "r");
|
||||
const buf = Buffer.alloc(stat.size - start);
|
||||
fs.readSync(fd, buf, 0, buf.length, start);
|
||||
fs.closeSync(fd);
|
||||
const lines = buf.toString("utf8").split("\n");
|
||||
if (start > 0) lines.shift(); // 第一行多半被切一半
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
let entry;
|
||||
try { entry = JSON.parse(line); } catch { continue; }
|
||||
if (entry.type !== "assistant") continue;
|
||||
const content = entry.message?.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
for (const block of content) {
|
||||
if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
|
||||
value = { id: entry.uuid || entry.timestamp || String(i), text: block.text.trim().slice(0, 400) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (value) break;
|
||||
}
|
||||
} catch { /* 讀不到就算了,寵物不該因此出錯 */ }
|
||||
|
||||
narrationCache.set(file, { size: stat.size, value });
|
||||
if (narrationCache.size > 32) narrationCache.delete(narrationCache.keys().next().value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// ---------- pets ----------
|
||||
|
||||
function dirKey(p) {
|
||||
@@ -300,6 +346,11 @@ function startServer() {
|
||||
try { payload = JSON.parse(body || "{}"); } catch { /* bad json */ }
|
||||
if (payload && typeof payload === "object") {
|
||||
appendLog(JSON.stringify(payload));
|
||||
const narration = latestNarration(payload.transcriptPath);
|
||||
if (narration) {
|
||||
payload.narration = narration.text;
|
||||
payload.narrationId = narration.id;
|
||||
}
|
||||
send("pet:event", payload);
|
||||
res.writeHead(204);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user