wezard 1.2.26 → 1.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1455,13 +1455,27 @@ export const startMirror = (deps) => {
|
|
|
1455
1455
|
const EARLY_LINK_MS = 3_000;
|
|
1456
1456
|
/** 软收口静默期。后端只能说"这条消息写完了"(codebuddy) 时, 等这么久没有新 item
|
|
1457
1457
|
* 才认定一轮结束。取值只需盖住"叙述消息落盘 → 紧随其后的 function_call 落盘"
|
|
1458
|
-
* 这一段, 与模型思考/工具执行时长无关。
|
|
1459
|
-
|
|
1458
|
+
* 这一段, 与模型思考/工具执行时长无关。
|
|
1459
|
+
* codebuddy 默认 10s (实测 <40ms, 但留余量防 fs.watch 抖动 / 落盘延迟);
|
|
1460
|
+
* claude 默认 4s (硬信号兜底, 软收口极少触发)。可由 config 覆盖。 */
|
|
1461
|
+
const SOFT_TURN_END_DEFAULT_CLAUDE = 4_000;
|
|
1462
|
+
const SOFT_TURN_END_DEFAULT_CB = 10_000;
|
|
1463
|
+
const softTurnEndMsFor = (a) => cfg.wrc.mirror.softTurnEndMs ?? (backendForPath(a.jsonlPath).name === "codebuddy" ? SOFT_TURN_END_DEFAULT_CB : SOFT_TURN_END_DEFAULT_CLAUDE);
|
|
1460
1464
|
const STREAM_SOFT_CAP = 18_000;
|
|
1461
1465
|
const TOOL_DETAIL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
1462
|
-
/**
|
|
1463
|
-
*
|
|
1464
|
-
const
|
|
1466
|
+
/** Standalone 发送前正文检查: 内容无正文 (纯 think 标记 / 工具行) 时,
|
|
1467
|
+
* 回队列等这么久。期间有新消息入队则重置; 到期且无新消息才发出。 */
|
|
1468
|
+
const STANDALONE_BODY_WAIT_MS = 8_000;
|
|
1469
|
+
// 判断 standalone 内容是否含有正文 (非纯 <think>…</think> / 工具行)。
|
|
1470
|
+
const standaloneHasBody = (content) => {
|
|
1471
|
+
// 剥掉 <think>…</think> 包裹 (含 tag header)
|
|
1472
|
+
const stripped = content.replace(/<think>[\s\S]*?<\/think>/g, "")
|
|
1473
|
+
// 工具行
|
|
1474
|
+
.replace(/^🔧 .*/gm, "")
|
|
1475
|
+
.replace(/^↳ .*/gm, "")
|
|
1476
|
+
.trim();
|
|
1477
|
+
return stripped.length > 0;
|
|
1478
|
+
};
|
|
1465
1479
|
const turnRegistry = new Map();
|
|
1466
1480
|
const evictTurns = () => {
|
|
1467
1481
|
const now = Date.now();
|
|
@@ -1641,12 +1655,22 @@ export const startMirror = (deps) => {
|
|
|
1641
1655
|
};
|
|
1642
1656
|
// Debounce 聚合: 仅 standalone 路径用。窗口内 onItem 多次落入 → 合并成单条 markdown。
|
|
1643
1657
|
// 0 关闭时退化为透传。flushStandalone 也用于 detach 时的 drain。
|
|
1644
|
-
|
|
1658
|
+
// 正文检查: 合并内容无正文 (纯 think/工具) 时, 回队列等 STANDALONE_BODY_WAIT_MS;
|
|
1659
|
+
// 期间有新消息入队 (parts 增长) 则重置; 到期且无新消息才强制发出。
|
|
1660
|
+
// force=true 跳过正文检查, 用于 teardown / detach / pre-card 等强制 drain 场景。
|
|
1661
|
+
const flushStandalone = (a, force = false) => {
|
|
1645
1662
|
const buf = a.standaloneBuf;
|
|
1646
1663
|
if (!buf)
|
|
1647
1664
|
return;
|
|
1665
|
+
const merged = buf.parts.join("\n\n");
|
|
1666
|
+
// 无正文 & 队列有新增 → 再等一轮 (force 跳过)
|
|
1667
|
+
if (!force && !standaloneHasBody(merged) && buf.bodyWaitLen !== buf.parts.length) {
|
|
1668
|
+
buf.bodyWaitLen = buf.parts.length;
|
|
1669
|
+
buf.timer = setTimeout(() => flushStandalone(a), STANDALONE_BODY_WAIT_MS);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1648
1672
|
a.standaloneBuf = undefined;
|
|
1649
|
-
sendStandalone(a,
|
|
1673
|
+
sendStandalone(a, merged);
|
|
1650
1674
|
};
|
|
1651
1675
|
const enqueueStandalone = (a, content) => {
|
|
1652
1676
|
const ms = cfg.wrc.mirror.standaloneDebounceMs;
|
|
@@ -1697,7 +1721,7 @@ export const startMirror = (deps) => {
|
|
|
1697
1721
|
if (!a.standaloneBuf)
|
|
1698
1722
|
return;
|
|
1699
1723
|
clearTimeout(a.standaloneBuf.timer);
|
|
1700
|
-
flushStandalone(a);
|
|
1724
|
+
flushStandalone(a, true);
|
|
1701
1725
|
};
|
|
1702
1726
|
// Path A: a needs-approval tool_use just landed. Aggregate the buffer as one
|
|
1703
1727
|
// standalone, send it BEFORE the approval card race, transition to AWAITING_APPR.
|
|
@@ -1856,6 +1880,11 @@ export const startMirror = (deps) => {
|
|
|
1856
1880
|
// 收口一条 loading 气泡: finish=true 写入最终内容, 只生效一次。发送失败退回 standalone。
|
|
1857
1881
|
// 排队中的 turn 也持有气泡, 所以气泡是显式入参而不是从 a 上取。
|
|
1858
1882
|
// raw=true skips withSessionTag (used when content already contains the linked tag header).
|
|
1883
|
+
// WeCom 客户端收到 finish=true 后仍有打字机动画要播放, 如果紧接着就下发
|
|
1884
|
+
// standalone (sendMessage), 用户会看到 standalone 抢在气泡动画结束之前出现。
|
|
1885
|
+
// 把 finishBubble 的 replyStream promise 链入 standalonePending, 让后续
|
|
1886
|
+
// standalone 自然排在 finish 之后; 额外加一小段延迟留给客户端渲染。
|
|
1887
|
+
const BUBBLE_FINISH_SETTLE_MS = 600;
|
|
1859
1888
|
const finishBubble = async (a, b, content, raw = false) => {
|
|
1860
1889
|
if (!b || b.done)
|
|
1861
1890
|
return;
|
|
@@ -1865,16 +1894,28 @@ export const startMirror = (deps) => {
|
|
|
1865
1894
|
clearTimeout(b.earlyTimer);
|
|
1866
1895
|
b.earlyTimer = undefined;
|
|
1867
1896
|
}
|
|
1897
|
+
// think-style: 清除待发的 flush 定时器, 避免 doStreamThink 在 done 后空跑。
|
|
1898
|
+
if (a.briefBubble === b && a.thinkFlushTimer) {
|
|
1899
|
+
clearTimeout(a.thinkFlushTimer);
|
|
1900
|
+
a.thinkFlushTimer = undefined;
|
|
1901
|
+
}
|
|
1868
1902
|
if (a.briefBubble === b)
|
|
1869
1903
|
a.briefBubble = undefined;
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1904
|
+
const p = (async () => {
|
|
1905
|
+
try {
|
|
1906
|
+
await client.replyStream(b.frame, b.streamId, raw ? (content || " ") : withLinkedTag(a, content || " "), true);
|
|
1907
|
+
// 给 WeCom 客户端留一点时间完成打字机动画, 再放行 standalone 队列。
|
|
1908
|
+
await new Promise((r) => setTimeout(r, BUBBLE_FINISH_SETTLE_MS));
|
|
1909
|
+
}
|
|
1910
|
+
catch (e) {
|
|
1911
|
+
log.warn({ sessionId: a.sessionId, err: e.message }, "brief: bubble finish failed; standalone fallback");
|
|
1912
|
+
if (content.trim())
|
|
1913
|
+
raw ? sendRaw(a, content) : sendStandalone(a, content);
|
|
1914
|
+
}
|
|
1915
|
+
})();
|
|
1916
|
+
// 链入 standalonePending, 后续 sendStandalone/sendRaw 自然等 finish 完成。
|
|
1917
|
+
a.standalonePending = a.standalonePending.then(() => p).catch(() => undefined);
|
|
1918
|
+
return p;
|
|
1878
1919
|
};
|
|
1879
1920
|
const finishBriefBubble = (a, content, raw = false) => finishBubble(a, a.briefBubble, content, raw);
|
|
1880
1921
|
// think-style tag: 紧跟在 `<think>` 之后, 不在整条消息最前面 —— `<think>🧙 …`。
|
|
@@ -1886,17 +1927,36 @@ export const startMirror = (deps) => {
|
|
|
1886
1927
|
// think-style: 把当前累积的 thinking 作为 `<think>🧙 …` 流进活着的 loading 气泡
|
|
1887
1928
|
// (finish=false, 气泡不关)。收口时再补 `</think>\n\n<final>` 并 finish。
|
|
1888
1929
|
// raw 发送: tag 已在 <think> 内, 不再经 withSessionTag 外包。
|
|
1889
|
-
|
|
1930
|
+
// streamPending → 首次调用时才真正开流 (fix: off-by-one race)。
|
|
1931
|
+
// fix: 加去抖 (FLUSH_MS) + 背压 (await 上一次完成), 消除 SDK 串行队列积压。
|
|
1932
|
+
const doStreamThink = async (a) => {
|
|
1890
1933
|
const b = a.briefBubble;
|
|
1891
1934
|
const think = a.briefThinking?.trim();
|
|
1892
1935
|
if (!b || b.done || !think)
|
|
1893
1936
|
return;
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1937
|
+
// 首条 replyStream 延迟到这里发出 (streamPending), 携带真实内容, 避免空开流竞态。
|
|
1938
|
+
b.streamPending = false;
|
|
1939
|
+
try {
|
|
1940
|
+
await client.replyStream(b.frame, b.streamId, openThink(a.target, think), false);
|
|
1941
|
+
log.debug({ sessionId: a.sessionId, streamId: b.streamId, thinkLen: think.length }, "brief: think stream ok");
|
|
1942
|
+
}
|
|
1943
|
+
catch (e) {
|
|
1944
|
+
log.warn({ sessionId: a.sessionId, err: e.message }, "brief: think stream failed");
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1947
|
+
const scheduleThinkFlush = (a) => {
|
|
1948
|
+
if (!a.briefBubble || a.briefBubble.done || a.thinkFlushTimer || a.thinkFlushing)
|
|
1949
|
+
return;
|
|
1950
|
+
a.thinkFlushTimer = setTimeout(() => {
|
|
1951
|
+
a.thinkFlushTimer = undefined;
|
|
1952
|
+
if (a.thinkFlushing)
|
|
1953
|
+
return; // 上一次还在飞 → 下次 pushThink 再排
|
|
1954
|
+
a.thinkFlushing = true;
|
|
1955
|
+
void doStreamThink(a).finally(() => { a.thinkFlushing = false; });
|
|
1956
|
+
}, FLUSH_MS);
|
|
1897
1957
|
};
|
|
1898
1958
|
// think-style 单一入口: 一段中间过程 (reasoning / 中途文本 / 工具调用) 进 think。
|
|
1899
|
-
// 气泡还活 → 累积进 briefThinking
|
|
1959
|
+
// 气泡还活 → 累积进 briefThinking 并排一次去抖 flush; 否则 (无气泡 turn / 气泡已收口)
|
|
1900
1960
|
// 只累积, 绝不单独 standalone —— 中间纯思考过程不下发消息, 等正式文本回复出现时
|
|
1901
1961
|
// 与正文拼成一条 `<think>…</think>\n\n正文` 整段下发 (见 concludeBriefTurn)。
|
|
1902
1962
|
const pushThink = (a, chunk) => {
|
|
@@ -1905,7 +1965,7 @@ export const startMirror = (deps) => {
|
|
|
1905
1965
|
return;
|
|
1906
1966
|
a.briefThinking = a.briefThinking ? `${a.briefThinking}\n\n${c}` : c;
|
|
1907
1967
|
if (a.briefBubble && !a.briefBubble.done)
|
|
1908
|
-
|
|
1968
|
+
scheduleThinkFlush(a);
|
|
1909
1969
|
};
|
|
1910
1970
|
// 让一个已建好记录的 turn 成为活跃 turn。turn 级状态在这里统一归零 —— 唯一入口。
|
|
1911
1971
|
const openBriefTurn = (a, q) => {
|
|
@@ -1916,18 +1976,11 @@ export const startMirror = (deps) => {
|
|
|
1916
1976
|
a.briefConcluded = false;
|
|
1917
1977
|
a.briefLastText = undefined;
|
|
1918
1978
|
a.briefThinking = undefined;
|
|
1919
|
-
if (a.
|
|
1920
|
-
clearTimeout(a.
|
|
1921
|
-
a.
|
|
1922
|
-
}
|
|
1923
|
-
if (thinkStyle) {
|
|
1924
|
-
a.thinkRescueTimer = setTimeout(() => {
|
|
1925
|
-
if (a.briefTurnId === q.turnId && !a.briefConcluded) {
|
|
1926
|
-
log.warn({ sessionId: a.sessionId, turnId: q.turnId }, "brief: thinkStyle rescue timeout — forcing close");
|
|
1927
|
-
closeBriefTurn(a, true);
|
|
1928
|
-
}
|
|
1929
|
-
}, THINK_RESCUE_TIMEOUT_MS);
|
|
1979
|
+
if (a.thinkFlushTimer) {
|
|
1980
|
+
clearTimeout(a.thinkFlushTimer);
|
|
1981
|
+
a.thinkFlushTimer = undefined;
|
|
1930
1982
|
}
|
|
1983
|
+
a.thinkFlushing = false;
|
|
1931
1984
|
log.info({ sessionId: a.sessionId, turnId: q.turnId, isSlash: q.isSlash }, "brief: turn started");
|
|
1932
1985
|
};
|
|
1933
1986
|
// WeCom 侧发起一个 turn: 立刻挂 loading 气泡当 ack, 再决定是马上激活还是排队。
|
|
@@ -1970,6 +2023,9 @@ export const startMirror = (deps) => {
|
|
|
1970
2023
|
// 活跃 turn 的 thinking 才是本气泡的内容; 排队 turn 没有 thinking, 空收即可。
|
|
1971
2024
|
const think = (a.briefBubble === bubble) ? a.briefThinking?.trim() : undefined;
|
|
1972
2025
|
const content = think ? openThink(a.target, think) : withSessionTag(a.target, " ");
|
|
2026
|
+
// 标记: thinking 快照已写进气泡, 后续 concludeBriefTurn 走 else 分支时不再重复发 thinking。
|
|
2027
|
+
if (think)
|
|
2028
|
+
bubble.thinkSnapshotFlushed = true;
|
|
1973
2029
|
void finishBubble(a, bubble, content, true);
|
|
1974
2030
|
}
|
|
1975
2031
|
else {
|
|
@@ -1980,7 +2036,11 @@ export const startMirror = (deps) => {
|
|
|
1980
2036
|
// 首条 item 到达时清除 (handleBriefItem 路径)。用显式 bubble ref 而非 a.briefBubble,
|
|
1981
2037
|
// 因为排队中的 turn 还没成为活跃 turn, a.briefBubble 指向的是前一个。
|
|
1982
2038
|
// think-style: 不设 earlyTimer —— 不发链接, 气泡留着等 thinking / 答复流进来。
|
|
1983
|
-
|
|
2039
|
+
// codebuddy: jsonl 按完整消息落盘 (status:"completed"), 不像 Claude Code 增量写,
|
|
2040
|
+
// 首条 item 常在 3s 之后才到达 → earlyTimer 提前收口气泡 → 结论被迫走 standalone。
|
|
2041
|
+
// 跳过 earlyTimer, 让气泡等到真正的内容写入 (hardTimer 兜底 6min 窗口)。
|
|
2042
|
+
const skipEarly = thinkStyle || backendForPath(a.jsonlPath).name === "codebuddy";
|
|
2043
|
+
if (!skipEarly)
|
|
1984
2044
|
bubble.earlyTimer = setTimeout(() => {
|
|
1985
2045
|
if (bubble.done)
|
|
1986
2046
|
return;
|
|
@@ -1994,10 +2054,12 @@ export const startMirror = (deps) => {
|
|
|
1994
2054
|
openBriefTurn(a, q);
|
|
1995
2055
|
}
|
|
1996
2056
|
try {
|
|
1997
|
-
// think-style:
|
|
1998
|
-
//
|
|
2057
|
+
// think-style: 延迟首条 replyStream 到首个内容到达 (streamPending=true),
|
|
2058
|
+
// 避免空 `<think>` 与紧随的 pushThink 产生两次快速 replyStream 触发 WeCom
|
|
2059
|
+
// 服务端合并/丢弃 (off-by-one)。flushStreamPending 在 pushThink/doStreamThink 首次
|
|
2060
|
+
// 调用时开流, 保证只有一次 replyStream 携带真实内容。
|
|
1999
2061
|
if (thinkStyle) {
|
|
2000
|
-
|
|
2062
|
+
bubble.streamPending = true;
|
|
2001
2063
|
}
|
|
2002
2064
|
else {
|
|
2003
2065
|
await client.replyStream(frame, streamId, withSessionTag(a.target, "…"), false); // 挂住 loading 气泡, 不关闭
|
|
@@ -2069,7 +2131,10 @@ export const startMirror = (deps) => {
|
|
|
2069
2131
|
else {
|
|
2070
2132
|
// 无气泡 turn / 气泡已收口: 中间 thinking 不单独 standalone, 累积的整段思考与
|
|
2071
2133
|
// 正式正文拼成一条下发 (think 取自 stripTrailingBody, 已摘掉尾部等于正文的重复段)。
|
|
2072
|
-
|
|
2134
|
+
// fix #1: 若 hardTimer 已把 thinking 快照写进气泡 (thinkSnapshotFlushed), 只发 body,
|
|
2135
|
+
// 不再重复发 thinking —— 用户已经在气泡里看到了完整 thinking。
|
|
2136
|
+
const snapshotFlushed = a.briefBubble?.thinkSnapshotFlushed;
|
|
2137
|
+
const t = snapshotFlushed ? undefined : think;
|
|
2073
2138
|
if (t)
|
|
2074
2139
|
sendRaw(a, `${openThink(a.target, t)}</think>\n\n${body}`);
|
|
2075
2140
|
else
|
|
@@ -2110,13 +2175,18 @@ export const startMirror = (deps) => {
|
|
|
2110
2175
|
if (thinkStyle) {
|
|
2111
2176
|
// 已结论 → 气泡已在 concludeBriefTurn 的 else 分支通过 sendRaw 发出
|
|
2112
2177
|
// think+body 整条消息,这里只需收掉气泡(避免重复下发)。
|
|
2113
|
-
// 未结论 →
|
|
2114
|
-
//
|
|
2178
|
+
// 未结论 → doStreamThink 可能已把半成品 thinking 流进气泡 (无 </think> 闭合),
|
|
2179
|
+
// 用闭合的 thinking 快照收口, 避免 WeCom 渲染标签未闭合的乱排版。
|
|
2180
|
+
// 如果连 thinking 都没有 (streamPending 还在), 空收。
|
|
2115
2181
|
if (a.briefConcluded) {
|
|
2116
2182
|
void finishBriefBubble(a, withSessionTag(a.target, " "), true);
|
|
2117
2183
|
}
|
|
2118
2184
|
else {
|
|
2119
|
-
|
|
2185
|
+
const think = a.briefThinking?.trim();
|
|
2186
|
+
const content = think
|
|
2187
|
+
? `${openThink(a.target, think)}</think>`
|
|
2188
|
+
: withSessionTag(a.target, " ");
|
|
2189
|
+
void finishBriefBubble(a, content, true);
|
|
2120
2190
|
}
|
|
2121
2191
|
}
|
|
2122
2192
|
else {
|
|
@@ -2132,10 +2202,11 @@ export const startMirror = (deps) => {
|
|
|
2132
2202
|
a.briefConcluded = false;
|
|
2133
2203
|
a.briefLastText = undefined;
|
|
2134
2204
|
a.briefThinking = undefined;
|
|
2135
|
-
if (a.
|
|
2136
|
-
clearTimeout(a.
|
|
2137
|
-
a.
|
|
2205
|
+
if (a.thinkFlushTimer) {
|
|
2206
|
+
clearTimeout(a.thinkFlushTimer);
|
|
2207
|
+
a.thinkFlushTimer = undefined;
|
|
2138
2208
|
}
|
|
2209
|
+
a.thinkFlushing = false;
|
|
2139
2210
|
// 放行下一个排队的 turn —— 它的气泡早在 ack 时就挂出去了, 这里只是激活。
|
|
2140
2211
|
const next = a.briefQueue?.shift();
|
|
2141
2212
|
if (next)
|
|
@@ -2154,8 +2225,8 @@ export const startMirror = (deps) => {
|
|
|
2154
2225
|
// 一个僵尸 turn 上来当活跃 turn, 白收一遍。
|
|
2155
2226
|
for (const q of a.briefQueue ?? []) {
|
|
2156
2227
|
closed++;
|
|
2157
|
-
// think-style: 不发链接,
|
|
2158
|
-
void finishBubble(a, q.bubble, thinkStyle ? " " : briefDetailLink(q.turnId, a.target), thinkStyle);
|
|
2228
|
+
// think-style: 不发链接, 排队气泡收为"已取消"提示 (避免空白气泡闪过)。
|
|
2229
|
+
void finishBubble(a, q.bubble, thinkStyle ? withSessionTag(a.target, "⏳ (queued, cancelled)") : briefDetailLink(q.turnId, a.target), thinkStyle);
|
|
2159
2230
|
recordTurnClose(q.turnId);
|
|
2160
2231
|
}
|
|
2161
2232
|
a.briefQueue = [];
|
|
@@ -2174,7 +2245,7 @@ export const startMirror = (deps) => {
|
|
|
2174
2245
|
}
|
|
2175
2246
|
if (a.standaloneBuf) {
|
|
2176
2247
|
clearTimeout(a.standaloneBuf.timer);
|
|
2177
|
-
flushStandalone(a);
|
|
2248
|
+
flushStandalone(a, true);
|
|
2178
2249
|
}
|
|
2179
2250
|
if (a.liveStream && !a.liveStream.closed) {
|
|
2180
2251
|
closed++;
|
|
@@ -2316,10 +2387,15 @@ export const startMirror = (deps) => {
|
|
|
2316
2387
|
if (item.quiet)
|
|
2317
2388
|
return;
|
|
2318
2389
|
// slash 命令 (/context…) 的 skill_output 即本轮答案 (无 final text 收口) —— 写进
|
|
2319
|
-
// loading 气泡。非 slash 场景的 skill_output
|
|
2390
|
+
// loading 气泡。非 slash 场景的 skill_output 是中间反馈:
|
|
2391
|
+
// think-style → 进 think 流 (与工具调用等中间过程一致);
|
|
2392
|
+
// 非 think-style → standalone。
|
|
2320
2393
|
if (a.briefIsSlash && !a.briefHadTool && a.briefBubble && !a.briefBubble.done) {
|
|
2321
2394
|
void finishBriefBubble(a, item.body);
|
|
2322
2395
|
}
|
|
2396
|
+
else if (thinkStyle) {
|
|
2397
|
+
pushThink(a, item.body);
|
|
2398
|
+
}
|
|
2323
2399
|
else {
|
|
2324
2400
|
sendStandalone(a, item.body);
|
|
2325
2401
|
}
|
|
@@ -2431,7 +2507,7 @@ export const startMirror = (deps) => {
|
|
|
2431
2507
|
a.softEnd = undefined;
|
|
2432
2508
|
}
|
|
2433
2509
|
if (item.kind === "turn_end" && item.soft === true) {
|
|
2434
|
-
a.softEnd = setTimeout(() => fireSoftTurnEnd(a),
|
|
2510
|
+
a.softEnd = setTimeout(() => fireSoftTurnEnd(a), softTurnEndMsFor(a));
|
|
2435
2511
|
return;
|
|
2436
2512
|
}
|
|
2437
2513
|
// codebuddy: ExitPlanMode 若被本地先答, result 落盘即作废挂着的计划审批卡
|
|
@@ -2682,7 +2758,7 @@ export const startMirror = (deps) => {
|
|
|
2682
2758
|
void finalizeStream(a, a.liveStream);
|
|
2683
2759
|
if (a.standaloneBuf) {
|
|
2684
2760
|
clearTimeout(a.standaloneBuf.timer);
|
|
2685
|
-
flushStandalone(a);
|
|
2761
|
+
flushStandalone(a, true);
|
|
2686
2762
|
}
|
|
2687
2763
|
a.tail.stop();
|
|
2688
2764
|
bySessionId.delete(a.sessionId);
|
|
@@ -4169,7 +4245,7 @@ export const startMirror = (deps) => {
|
|
|
4169
4245
|
// 3) standalone 防抖里 (3s 默认) 还压着的合并文案 — 强制立刻发。
|
|
4170
4246
|
if (a.standaloneBuf) {
|
|
4171
4247
|
clearTimeout(a.standaloneBuf.timer);
|
|
4172
|
-
flushStandalone(a);
|
|
4248
|
+
flushStandalone(a, true);
|
|
4173
4249
|
}
|
|
4174
4250
|
// 4) liveStream 半成品: 当前 acc 里有内容但还没 flush 出去 — 同步刷一刀,
|
|
4175
4251
|
// 保留 stream 不 finalize (后续 tool_result 还要继续 append 同一气泡)。
|
|
@@ -4520,7 +4596,7 @@ export const startMirror = (deps) => {
|
|
|
4520
4596
|
// race against the new stream's first content (Path B).
|
|
4521
4597
|
if (a.standaloneBuf) {
|
|
4522
4598
|
clearTimeout(a.standaloneBuf.timer);
|
|
4523
|
-
flushStandalone(a);
|
|
4599
|
+
flushStandalone(a, true);
|
|
4524
4600
|
}
|
|
4525
4601
|
// /clear produces no assistant output; opening a stream would leave a
|
|
4526
4602
|
// stale "…" + quoted-user bubble in WeCom. Skip the stream entirely on
|