修复: sendQueuedNow立即发送退化入队+裸diff围栏渲染+工具卡轮次自动收起

This commit is contained in:
2026-06-17 13:00:37 +08:00
parent 9d1e5c2737
commit ca9b3187b5
4 changed files with 727 additions and 7 deletions

View File

@@ -469,7 +469,13 @@ async function sendQueuedNow(index: number) {
if (index < 0 || index >= state.queue.length) return
const spliced = state.queue.splice(index, 1)[0]
await stopChat()
await sendMessage(spliced.text, spliced.skill, false, spliced.parts)
// B-260617-01: forceMode=true 跳过 L1 预检(ai_is_generating)直走 force_send。
// stopChat() 仅 await stop IPC 发出(置 stop_flag=true),不等后端 loop 跑到
// agentic.rs:444/:819 检测点+guard.reset()(generating 才复位)。紧接 sendMessage
// 命中 L1 的 backendGenerating(后端真值仍 true)→ spliced 被入队而非立即发,
// 与"立即发送"语义不符。force_send(commands.rs:742-748)原子复位 generating=false
// 再发,无竞态窗口;stopChat 的 stop_flag 让旧 loop 在下个检测点退出,不冲突。
await sendMessage(spliced.text, spliced.skill, true, spliced.parts)
}
/** 停止当前生成:本地先复位 streaming再发停止信号。

View File

@@ -183,10 +183,100 @@ function renderMd(text: string): string {
if (!mdReady.value || !_marked || !_purify) {
return escapeFallback(text)
}
const cached = _mdCache.get(text)
// B-260616-11(①子项): 裸 diff 文本预处理。渲染前将无围栏的连续 +/- 行包成 ```diff
// 围栏代码块,复用②子项已注册的 diff 语言 hljs 红绿样式,零额外 CSS。
// 防御性:即使现网 AI 多带围栏,裸行也正确处理(围栏内整段不动)。
const wrapped = wrapNakedDiff(text)
const cached = _mdCache.get(wrapped)
if (cached !== undefined) return cached
const html = _purify.sanitize(_marked.parse(text) as string)
const html = _purify.sanitize(_marked.parse(wrapped) as string)
if (_mdCache.size > 200) _mdCache.clear()
_mdCache.set(text, html)
_mdCache.set(wrapped, html)
return html
}
/**
* B-260616-11(①子项,方案A wrapNakedDiff): 裸 +/- diff 文本预处理。
*
* 现网 AI(尤其 deepseek/glm/qwen 等)在描述「改动/对比」时常直接吐裸 -/+ 行而忘记
* 围栏(或仅用普通文本),导致 marked 当成普通段落/list 渲染,丢失 diff 视觉语义。
* 本函数在 marked.parse 前扫描文本,把「连续 ≥2 行以 + 或 - 开头、且至少含 1 行 +」的
* 段落整段包成 ```diff 围栏代码块,复用②子项已就绪的 hljs diff 红绿样式,零额外 CSS。
*
* 守卫:
* 1. **纯 - 列表不触发**: 普通 markdown ul 每行 `- ` 是常见语法,纯 - 会误伤成代码块。
* 故要求段内至少含 1 行 `+ `(+ 必须含),这是裸 diff 区别于普通列表的强特征。
* 2. **连续 ≥2 行 ±开头**: 单行 +/- 不构成 diff 块(可能只是 emoji/符号起首),不处理。
* 3. **已在围栏内不动**: 段落在 ```...```/~~~...~~~ 内(行首可选 ≤3 空格 + 围栏)整段跳过,
* 避免双重包裹破坏已围栏的 diff 代码块。
* 4. **段落级处理**: 按 blank line 切段(\n\n),非 diff 段落(标题/普通段落/list)原样保留,
* 避免 +- 混排在正文里被整段吞掉。
*
* 注意:仅 marked.parse 前文本预处理,不碰 marked/hljs/DOMPurify 流程,行为可逆(去掉包裹即原文)。
* 缓存 key 用 wrapped(预处理后),同一原文命中同一缓存项。
*/
const _DIFF_LINE_RE = /^[ \t]*[+-][^\n]*$/ // 行首可选空格/tab + +/- + 任意内容(+ 不限空格)
const _PLUS_LINE_RE = /^[ \t]*\+[^\n]*$/ // 含 + 的行(+ 必须含的判定)
const _FENCE_OPEN_RE = /^[ \t]{0,3}(```|~~~)/ // marked 围栏开头(行首 ≤3 空格 + 3+ 反引号/波浪)
export function wrapNakedDiff(text: string): string {
if (!text) return text
// 快速短路:全文无 +/- 行首特征直接返回原文(避免大文本白跑切段)
if (!/[+-]/.test(text)) return text
// 按 blank line 切段(保留分隔符便于重组)。\n\n 为段落边界(marked 与 gfm 段落规则一致)。
// 用 lookahead split 保留分隔符,避免段落内容错位。
const paragraphs = text.split(/(\n{2,})/)
let touched = false
for (let i = 0; i < paragraphs.length; i++) {
const seg = paragraphs[i]
if (!seg || /^\n{2,}$/.test(seg)) continue // 分隔符或空串跳过
// 守卫3: 整段在围栏内不动。检测段内是否含围栏开头行,含则跳过该段。
if (_FENCE_OPEN_RE.test(seg)) continue
const lines = seg.split('\n')
// 守卫2 + 特征判定:连续 ≥2 行以 +/- 开头,且(守卫1)段内至少含 1 行 +
// 用最大连续 run 判定:找到连续 ≥2 行 ±开头的 run,且该 run 含 +
let runStart = -1
let runLen = 0
let hasPlus = false
let bestStart = -1
let bestLen = 0
for (let j = 0; j < lines.length; j++) {
if (_DIFF_LINE_RE.test(lines[j])) {
if (runStart < 0) {
runStart = j
runLen = 0
hasPlus = false
}
runLen++
if (_PLUS_LINE_RE.test(lines[j])) hasPlus = true
} else {
// 收尾当前 run:连续 ≥2 且含 + 才记为候选 diff 段
if (runStart >= 0 && runLen >= 2 && hasPlus) {
if (runLen > bestLen) {
bestLen = runLen
bestStart = runStart
}
}
runStart = -1
runLen = 0
hasPlus = false
}
}
// 段尾收尾
if (runStart >= 0 && runLen >= 2 && hasPlus) {
if (runLen > bestLen) {
bestLen = runLen
bestStart = runStart
}
}
// 找到满足条件的 diff 段(整段都是连续 ± run,即段首即 diff):整段包成围栏
// 注意:仅当 diff run 覆盖整段时才包(混合段不包,避免吞掉正常文字)。
// bestStart===0 且 bestLen===lines.length → 整段都是 ± 行
if (bestStart === 0 && bestLen === lines.length && bestLen >= 2) {
paragraphs[i] = '```diff\n' + seg + '\n```'
touched = true
}
}
return touched ? paragraphs.join('') : text
}