feat: 工具调用前穿插逐步文字解说 + 后台唤醒调度#180
Conversation
System prompt now requires a short content-channel narration before each tool call (no longer routes it into the hidden reasoning channel), so multi-step agent turns read like a pair-programmer instead of a silent tool dump. Also bundles in-progress wake-scheduler/digest/sleep work: watch + yield_turn background auto-resume, session digest, and the sleep tool.
|
AI 审查在超大 diff 下质量会下降,建议:
|
| document.body.classList.add('drawer-open'); | ||
| if (sbSessionsBtn) sbSessionsBtn.setAttribute('aria-expanded', 'true'); | ||
| try { _drawerPrevFocus = document.activeElement; } catch(e){ _drawerPrevFocus = null; } | ||
| if (rightPanel) { |
There was a problem hiding this comment.
在添加 'drawer-open' 类到 document.body 时,需确保该操作不会导致潜在的安全漏洞,例如 XSS 攻击。建议在添加类之前进行必要的输入验证,确保没有恶意脚本被注入。此外,考虑到可访问性,建议在添加类时检查是否有其他相关的 ARIA 属性需要更新。
| document.body.classList.remove('drawer-open'); | ||
| if (sbSessionsBtn) sbSessionsBtn.setAttribute('aria-expanded', 'false'); | ||
| if (_drawerPrevFocus && typeof _drawerPrevFocus.focus === 'function') { | ||
| try { _drawerPrevFocus.focus(); } catch(e){} |
There was a problem hiding this comment.
在移除 'drawer-open' 类时,确保该操作不会影响到其他组件的状态。建议在移除类之前检查相关的状态,防止因状态不一致导致的潜在 bug。同时,建议在移除类时也更新相关的 ARIA 属性,以保持可访问性的一致性。
| "version": "0.43.0", | ||
| "publisher": "ZhouChaunge", | ||
| "icon": "imgs/logo.png", | ||
| "repository": { |
There was a problem hiding this comment.
版本号的更新需要确保与其他依赖项的兼容性,建议在更新版本号时同时检查相关文档和依赖项的版本。如果此版本引入了不兼容的更改,可能会导致现有功能的崩溃或不稳定。此外,建议在版本更新时附上变更日志,以便开发者了解更新内容。
| const rv = await post(buildIndexVariantMessages('(no thoughts surfaced for this step)')); | ||
| console.log(`${icon(rv.status).padEnd(8)} ${'index-variant'.padEnd(20)} ${'"…(step N)"'.padEnd(40)} ${rv.msg}`); | ||
| console.log('\nLegend: ✅=accepted (safe to use) ❌(400)=rejected by thinking-mode gate'); | ||
| })(); |
There was a problem hiding this comment.
- 安全性: 代码中使用了环境变量
DEEPSEEK_KEY来存储密钥,但没有对其进行有效的保护措施。建议使用更安全的方式来管理密钥,比如使用密钥管理服务。 - 异常处理: 在
post函数中,JSON.parse可能会抛出异常,但在catch中没有处理具体的错误信息,建议记录错误信息以便于调试。 - 代码风格: 代码整体风格较为一致,但在注释部分可以更简洁明了,避免过于冗长的注释。
- 性能: 在
post函数中,res.on('data', d => raw += d);可能会导致内存占用过高,建议使用流的方式处理数据。 - 可维护性:
CANDIDATES对象中的字符串可以考虑提取到常量文件中,以便于后续维护和国际化处理。
| const existingActive = this._getRun(_busyCheckSid); | ||
| if (existingActive && existingActive.busy) return; | ||
|
|
||
| const cfg = vscode.workspace.getConfiguration('deepseekAgent'); |
There was a problem hiding this comment.
在处理 autoResume 的逻辑时,建议对 options 进行更严格的类型检查,确保其为对象类型。此外,autoResume 的使用可能会引入潜在的状态不一致问题,建议在使用前进行更详细的验证。
|
|
||
| if (effectiveStrip.length) { | ||
| out = out.map(m => { | ||
| if (!m || typeof m !== 'object') return m; |
There was a problem hiding this comment.
在 sanitizeMessages 函数中,添加了对 _ 前缀字段的处理逻辑。虽然这有助于防止内部字段泄露,但需要确保在删除字段时不会影响到消息的完整性。此外,建议在删除字段之前进行深拷贝,以避免潜在的引用问题。建议添加单元测试以验证此逻辑的正确性。
| }, | ||
| ]; | ||
|
|
||
| module.exports = { TOOL_DEFS, getToolDefs }; |
There was a problem hiding this comment.
- 安全性:
watch和yield_turn函数的描述中提到的条件触发机制需要确保不会导致无限挂起的情况。建议在实现中加入对条件的严格验证,确保time_elapsed的安全边界被正确设置。 - 异常处理: 在调用
yield_turn时,需确保至少有一个watch被注册,否则会导致会话永久挂起。建议在代码中添加相应的检查和错误处理逻辑,以避免潜在的空指针异常或未处理的异常。 - 代码风格: 请确保新添加的函数遵循现有代码的风格,特别是在注释和参数描述的格式上保持一致。
- 性能与可维护性:
watch和yield_turn的实现逻辑需要考虑性能影响,尤其是在高并发情况下,建议进行性能测试以确保不会引入竞态条件或性能瓶颈。
| : '(no output, exit 0)'; | ||
| else text = truncate(output); | ||
| finish({ | ||
| command, |
There was a problem hiding this comment.
在这里引入了对 _win32InlineScriptHint 的调用,虽然这个函数提供了对多行命令的提示,但需要确保在所有情况下都能正确处理。建议增加对 command 参数的验证,确保其类型和内容符合预期,以防止潜在的空指针异常或无效输入导致的错误。
| } | ||
| else if (!stdout && stderr) text = truncate(`(stdout empty, exit 0)\n--- stderr ---\n${stderr}`); | ||
| else text = truncate(stderr ? `${stdout}\n--- stderr ---\n${stderr}` : stdout); | ||
| // Return structured result so the model can clearly inspect exitCode, |
There was a problem hiding this comment.
同样在这里使用了 _win32InlineScriptHint,需要注意的是,如果 command 为 null 或 undefined,可能会导致函数调用失败。建议在调用之前进行有效性检查。此外,建议在处理输出时,增加对 stderr 和 stdout 的更详细的错误处理,以提高代码的健壮性。
| }); | ||
| } | ||
|
|
||
| module.exports = { toolWatch, toolYieldTurn }; |
There was a problem hiding this comment.
-
安全性: 在
toolWatch和toolYieldTurn函数中,返回的 JSON 字符串可能会被直接输出到客户端,需确保没有敏感信息泄露。建议对返回的错误信息进行适当的过滤和处理,避免泄露内部实现细节。 -
异常处理: 在
toolWatch和toolYieldTurn中,虽然对输入参数进行了检查,但在调用scheduler的方法时未处理可能的异常情况,建议使用 try-catch 结构来捕获潜在的异常。 -
代码风格: 代码整体风格较为一致,但在某些地方可以考虑使用更具可读性的变量名,例如将
args和run的类型及用途在注释中说明,以提高可维护性。 -
性能: 在
_validateCondition函数中,使用了多次Array.isArray和typeof检查,虽然必要,但可以考虑将这些检查封装成一个工具函数,以减少重复代码,提高可维护性。 -
深度检查:
_validateCondition函数的深度检查逻辑可能导致性能问题,尤其是在条件嵌套较深时,建议限制深度并提供清晰的错误信息。
There was a problem hiding this comment.
Pull request overview
此 PR 旨在改善多步工具回合的可见性(工具调用前插入可见解说),并引入「watch + yield_turn」的后台唤醒调度能力,以便对长时间后台任务进行挂起与自动恢复;同时捎带了一系列 DeepSeek prefix-cache 相关的稳定性/成本优化与少量 UI 调整。
Changes:
- 更新系统提示词:要求每次工具调用前在可见
content中输出简短动作解说,并补充 long-running 任务的 watch/yield 操作规约。 - 新增后台唤醒链路:
watch/yield_turn工具 +wake-scheduler+digest+ agent-loop 的 auto-wake 注入与恢复派发。 - prefix-cache/体验优化:附件排序稳定化、去掉周期性 proactive compaction、usage 中展示 cache hit rate、session drawer push-layout。
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tools/sleep.js | 新增 watch / yield_turn 工具实现(条件校验、yield 状态写入 run)。 |
| src/tools/shell.js | Windows 下多行 inline script(python -c/node -e)无输出场景追加可操作提示。 |
| src/tools/schema.js | 为 watch / yield_turn 增加工具 schema 定义与使用约束说明。 |
| src/providers/index.js | 调整 reasoning placeholder,并在发送前剥离 _ 前缀内部字段以避免 400。 |
| src/prompts/system.js | 系统提示词新增“工具前解说 + long-running watch/yield 规则”,并调整动态段落顺序与边界输出。 |
| src/chat/wake-scheduler.js | 新增 watcher 注册/轮询/触发/限流与 autoResume 路由实现。 |
| src/chat/tool-executor.js | 将 watch / yield_turn 接入 ToolExecutor 分发。 |
| src/chat/session-store.js | 移除“保存时预压缩”分支,改为仅在 agent loop 内按需压缩。 |
| src/chat/provider.js | 挂载 wake-scheduler,并实现 autoResume() 触发恢复派发;删除 session 时取消 watcher。 |
| src/chat/digest.js | 新增本地摘要生成(异常/进度/尾部输出)供 auto-wake 注入。 |
| src/chat/compact.js | tool 结果截断增加 _cacheFrozen,并将重复 read 去重触发阈值提高以减少 cache bust。 |
| src/chat/agent-loop.js | 支持 autoResume 触发的 turn、注入 auto-wake digest、yield_turn 挂起、自愈空回复、cache hit rate 上报等。 |
| scripts/probe-reasoning-placeholder.js | 新增探测脚本验证 reasoning placeholder 可被 thinking-mode 接受。 |
| package.json | 版本号升级至 0.43.0。 |
| media/chat.js | session drawer 打开/关闭时切换 body class(push-layout)。 |
| media/chat.css | drawer push-layout 样式与移动端回退规则。 |
结论:需修改
| const live = this._runs.get(sessionId); | ||
| if (live && live.busy) { | ||
| // A turn is already running for this session — just enqueue; | ||
| // the agent loop picks it up at the top of the next iteration. | ||
| live._pendingWakeEvents = live._pendingWakeEvents || []; | ||
| live._pendingWakeEvents.push(evidence); | ||
| Logger.info('AUTO_RESUME_PIGGYBACK', { sid: sessionId, watcherId: evidence.watcherId }); | ||
| return; | ||
| } | ||
| // No live run: hydrate from store and prime the wake queue, then dispatch. | ||
| const seed = this._store.loadApiMessages(sessionId); | ||
| const run = this._newRun(sessionId, seed); | ||
| run._pendingWakeEvents = [evidence]; | ||
| Logger.info('AUTO_RESUME_DISPATCH', { | ||
| sid: sessionId, | ||
| watcherId: evidence.watcherId, | ||
| trigger: evidence.trigger, | ||
| }); |
| function _fire(w, evidence) { | ||
| if (w.firedAt) return; | ||
| w.firedAt = Date.now(); | ||
| _cleanupWatcher(w); | ||
|
|
||
| if (!_checkRateLimit(w.sessionId)) { | ||
| Logger.info('WATCHER_RATE_LIMITED', { | ||
| sessionId: w.sessionId, | ||
| id: w.id, | ||
| trigger: evidence.kind, | ||
| }); | ||
| return; | ||
| } | ||
| _recordResume(w.sessionId); | ||
|
|
| // DYNAMIC_BOUNDARY marker is intentionally NOT emitted as literal text: | ||
| // it is a logical split point exposed via the named export for any | ||
| // future split-aware caller (e.g. an Anthropic cache-breakpoint helper). | ||
| // Emitting the literal "__DYNAMIC_BOUNDARY__" string into the prompt is | ||
| // pure noise to the model and contributes ~22 chars of cache-irrelevant | ||
| // bytes — drop it. | ||
| return `${staticPart}\n\n${dynamicParts.join('\n\n')}`; |
| } else if (c.kind === 'output_silent' && c.seconds) { | ||
| const last = w._lastOutputChangeAt.get(c.job) || w.createdAt; | ||
| if (Date.now() - last > Number(c.seconds) * 1000) { | ||
| return _fire(w, { | ||
| kind: 'output_silent', | ||
| jobId: c.job, | ||
| idleSec: Math.round((Date.now() - last) / 1000), | ||
| output: out, | ||
| lastSeenLen: prevLen, |
- provider.js: autoResume 不再覆盖空闲 run,避免丢失内存态(toolCache/待应用编辑/排队事件),仅无 run 时才从 SessionStore 水合 - wake-scheduler.js: _fire 限流检查前移到 cleanup 之前;限流命中时保留 watcher 并按名额释放时间重试,避免挂起会话永久失去唤醒者 - terminal-monitor.js: 记录新增 truncated 饱和标志,命中 64KB 上限时置位 - wake-scheduler.js: output_silent 在捕获饱和时刷新空闲计时且不触发,避免误判静默 - system.js: 统一 DYNAMIC_BOUNDARY 文档为逻辑边界(不写入 prompt 文本)
|
AI 审查在超大 diff 下质量会下降,建议:
|
| } | ||
| Logger.info('AUTO_RESUME_DISPATCH', { | ||
| sid: sessionId, | ||
| watcherId: evidence.watcherId, |
There was a problem hiding this comment.
在这一段代码中,虽然对现有运行的重用逻辑进行了优化,但需要注意以下几点:
- 安全性:确保
sessionId和evidence.watcherId的来源是安全的,避免潜在的注入攻击。建议对这些值进行验证和清理。 - 异常处理:在调用
this._store.loadApiMessages(sessionId)和this._newRun(sessionId, seed)时,未处理可能的异常情况。建议添加 try-catch 块来捕获并处理这些异常,以防止程序崩溃。 - 空指针检查:在使用
run._pendingWakeEvents之前,虽然进行了初始化,但建议在访问run对象的属性之前,确保run不为 null 或 undefined,以避免潜在的空指针异常。 - 代码风格:代码注释部分较为冗长,建议简化并确保注释与代码逻辑保持一致,增强可读性。
综上所述,建议对该部分代码进行修改以提高安全性和可维护性。
| const out = res.text; | ||
|
|
||
| const prevLen = w._lastSeenLen.get(c.job) || 0; | ||
| const newSlice = out.length > prevLen ? out.slice(prevLen) : ''; |
There was a problem hiding this comment.
在此处,返回的对象包含了 truncated 字段,确保在使用该字段时,相关逻辑能够正确处理该状态。建议在使用 truncated 时增加注释,说明其具体含义和影响,以避免后续维护时的误解。
| } | ||
| const last = w._lastOutputChangeAt.get(c.job) || w.createdAt; | ||
| if (Date.now() - last > Number(c.seconds) * 1000) { | ||
| return _fire(w, { |
There was a problem hiding this comment.
在处理 truncated 状态时,确保逻辑的完整性,避免在输出被截断时错误地判断为活跃状态。建议增加单元测试,确保在不同情况下的行为符合预期。
| _cleanupWatcher(w); | ||
| _recordResume(w.sessionId); | ||
|
|
||
| const digest = buildDigest({ |
There was a problem hiding this comment.
在检查速率限制之前,确保 w 对象的状态是有效的,避免潜在的空指针异常。同时,setTimeout 的使用可能引入竞态条件,建议在清理定时器时确保不会重复设置。
| // split-aware caller can re-derive the boundary if it ever needs to. | ||
| // - "Verify before reporting complete" + "report failures faithfully": | ||
| // executable behavior gates, not vague encouragement. | ||
| // - Workspace instructions (DEEPCOPILOT.md) injected only when the caller |
There was a problem hiding this comment.
在此处,DYNAMIC_BOUNDARY 的描述进行了修改,强调了它是一个逻辑分隔符而非物理标记。需要确保这个逻辑分隔符不会被误用或引入安全隐患,尤其是在与外部系统交互时。建议在代码中增加注释,明确说明其用途和限制,以防止未来的开发者误解。
| // (e.g. an Anthropic cache-breakpoint helper) that re-assembles the halves. | ||
| const DYNAMIC_BOUNDARY = '__DYNAMIC_BOUNDARY__'; | ||
|
|
||
| // ---------- static core (cacheable across all requests) ---------- |
There was a problem hiding this comment.
此处对 DYNAMIC_BOUNDARY 的描述进行了更改,强调它是一个逻辑边界。建议在代码中增加更多的上下文信息,确保后续开发者能够理解其重要性,避免在未来的实现中出现潜在的逻辑错误或安全漏洞。
| * ──────── DYNAMIC_BOUNDARY (logical) ──────── | ||
| * [environment] | ||
| * [user memory] ← if present | ||
| * [skill index] ← if any skills installed (Issue #61) |
There was a problem hiding this comment.
在构建系统提示的注释中,强调 DYNAMIC_BOUNDARY 是逻辑分隔符而非文本输出。建议在实现中添加对该逻辑分隔符的使用示例,确保其在不同环境下的稳定性与安全性。同时,考虑到未来可能的扩展,建议对该部分进行更详细的文档说明。
| else { rec.output += s; } | ||
| } | ||
| } catch (err) { | ||
| Logger.info('TERMINAL_MONITOR_READ_ERROR', { message: String(err && err.message || err) }); |
There was a problem hiding this comment.
在处理 execution.read() 的流时,建议对 execution 对象进行空值检查,以防止潜在的空指针异常。此外,Logger.info 的调用中,建议对 err 进行更全面的处理,确保所有异常情况都能被记录。
| return _fire(w, { | ||
| kind: 'progress_at', | ||
| jobId: c.job, | ||
| output: out, | ||
| lastSeenLen: prevLen, | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| Logger.info('WATCHER_REGEX_ERROR', { id: w.id, regex: c.regex, error: e.message }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
| if (!term) return null; | ||
| const recs = getRecentExecutions(term, 20); | ||
| return { | ||
| text: recs.map(r => r.output || '').join('\n'), | ||
| // True when the most-recent execution saturated the capture cap | ||
| // (terminal-monitor stops appending past MAX_BYTES_PER_EXECUTION). |
| }); | ||
| }; | ||
| onBgJobEnded(handler); | ||
| w._offBgEnd = () => offBgJobEnded(handler); | ||
| } | ||
|
|
| function _maxResumesPerHour() { | ||
| try { | ||
| const v = vscode.workspace | ||
| .getConfiguration('deepCopilot.autoResume') | ||
| .get('maxResumesPerHour'); | ||
| const n = Number(v); | ||
| return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_RESUMES_PER_HOUR; | ||
| } catch { return DEFAULT_MAX_RESUMES_PER_HOUR; } |
| return Math.max(1000, freeAt - Date.now() + 100); | ||
| } | ||
|
|
||
| function _arm(w) { | ||
| const conds = _flattenLeaves(w.spec.condition); | ||
| const pollNeeded = conds.some(c => |
| async function toolYieldTurn(args, run) { | ||
| if (!run) return JSON.stringify({ ok: false, error: 'no active run context' }); | ||
| const reason = (args && typeof args.reason === 'string') ? args.reason : 'yielding turn'; | ||
|
|
| - When markdown cannot express complex structure or interactivity (such as collapsible sections, keyboard keys, highlights, tables, images, advanced formatting), you may directly output safe HTML tags (e.g. <details>, <summary>, <kbd>, <mark>, <sub>, <sup>, <abbr>, <ins>, <del>, <dfn>, <samp>, <var>, <br>, <hr>, <u>, <small>, <s>, <q>, <cite>, <figure>, <figcaption>, <table>, <thead>, <tbody>, <tr>, <th>, <td>, <img>, <blockquote>, <p>, <ul>, <ol>, <li>, <code>, <pre>, <h1>, <h2>, <h3>, <h4>, <h5>, <h6>). Use class= attributes for styling; do NOT use inline style= attributes. | ||
| - All output will be sanitized for security. Never emit <script>, <iframe>, <style>, <link>, <object>, <embed>, any on* event attributes, or javascript: URLs. | ||
| - Your goal is to maximize readability, clarity, and interactivity for the user, choosing the most suitable format for each answer. | ||
| - When generating content destined for external systems — GitHub issues, pull requests, comments, commit messages, emails, or any file written to disk — always use plain GitHub-flavored Markdown, not HTML. HTML is only for the in-app chat display. |
|
|
||
| return `${staticPart}\n\n${DYNAMIC_BOUNDARY}\n\n${dynamicParts.join('\n\n')}`; | ||
| // DYNAMIC_BOUNDARY marker is intentionally NOT emitted as literal text: | ||
| // it is a logical split point exposed via the named export for any |
| /* Push-layout: shrink main/composer so the drawer sits beside them instead of covering them */ | ||
| body.drawer-open{padding-right:280px;transition:padding-right .15s ease} | ||
| body.drawer-open #session-backdrop{display:none!important} | ||
| @media (max-width:640px){body.drawer-open{padding-right:0}body.drawer-open #session-backdrop.open{display:block}} |
| run.messages.push({ | ||
| role: 'user', | ||
| content: [ | ||
| '<system-reminder channel="auto-wake">', | ||
| ev.digest || '(no digest)', | ||
| '</system-reminder>', | ||
| ].join('\n'), | ||
| }); |
| if (!anomalies.length) { | ||
| // No errors — include a small recent tail for context. | ||
| const tail = scanLines.slice(-20).join('\n'); | ||
| if (tail.trim()) { | ||
| parts.push(''); | ||
| parts.push('--- recent output tail ---'); | ||
| parts.push(tail); | ||
| } | ||
| } | ||
|
|
||
| let result = parts.join('\n').trimEnd(); | ||
| if (result.length > maxChars) { | ||
| result = result.slice(0, maxChars) + `\n…[digest truncated, ${result.length - maxChars} more chars]`; | ||
| } | ||
| return result; | ||
| } |
| async autoResume(sessionId, evidence) { | ||
| if (!sessionId || !evidence) return; | ||
| try { | ||
| const live = this._runs.get(sessionId); | ||
| if (live && live.busy) { | ||
| // A turn is already running for this session — just enqueue; | ||
| // the agent loop picks it up at the top of the next iteration. | ||
| live._pendingWakeEvents = live._pendingWakeEvents || []; | ||
| live._pendingWakeEvents.push(evidence); | ||
| Logger.info('AUTO_RESUME_PIGGYBACK', { sid: sessionId, watcherId: evidence.watcherId }); | ||
| return; | ||
| } |
| function _readJobOutput(jobId) { | ||
| const term = findTerminalByName(jobId); | ||
| if (!term) return null; | ||
| const recs = getRecentExecutions(term, 20); | ||
| return { | ||
| text: recs.map(r => r.output || '').join('\n'), | ||
| // True when the most-recent execution saturated the capture cap | ||
| // (terminal-monitor stops appending past MAX_BYTES_PER_EXECUTION). | ||
| // Its captured output then stops growing even though the job keeps | ||
| // emitting — so a flat length must NOT be read as "went silent". | ||
| truncated: recs.length > 0 && !!recs[recs.length - 1].truncated, | ||
| }; |
- 修正 autoResume 每小时上限配置命名空间(deepseekAgent.autoResumeMaxPerHour)并补全 package.json 贡献点 - 防御终端输出注入 system-reminder 标签(sanitizeForReminder) - autoResume 前校验会话仍存在,避免删除竞态 - _pollOutput 处理缓冲区收缩,_readJobOutput 截断超大尾部输出 - time_elapsed 用 Math.round 避免小数截断 - yield_turn.reason schema 与实现一致改为可选 - 移除误导的 DYNAMIC_BOUNDARY 导出与文档,统一提示词缩进
|
AI 审查在超大 diff 下质量会下降,建议:
|
| }, | ||
| "deepseekAgent.webSearchProvider": { | ||
| "type": "string", | ||
| "default": "tavily", |
There was a problem hiding this comment.
新增的配置项 deepseekAgent.autoResumeMaxPerHour 需要注意安全性和性能影响。建议在文档中明确说明该参数的使用场景和限制,避免用户误用导致 API 成本失控。此外,建议在代码中添加对该参数的有效性检查,以防止不合理的值(如负数或超出最大值)引发潜在的异常或错误。
| ev.output ? `Last output:\n${sanitizeForReminder(ev.output)}` : '(no output captured)', | ||
| '</system-reminder>', | ||
| ].join('\n'), | ||
| }); |
There was a problem hiding this comment.
在这里使用 sanitizeForReminder 函数处理 ev.output 是一个良好的安全实践,可以防止潜在的跨站脚本(XSS)攻击。然而,需要确保 sanitizeForReminder 函数本身是安全的,并且能够有效地处理所有可能的输入。此外,建议在函数内部添加输入验证,以防止恶意输入导致的安全问题。
| sanitizeForReminder(ev.digest || '(no digest)'), | ||
| '</system-reminder>', | ||
| ].join('\n'), | ||
| }); |
There was a problem hiding this comment.
同样地,使用 sanitizeForReminder 来处理 ev.digest 是一个良好的做法,但需要确保该函数的实现是安全的,能够防止任何潜在的安全漏洞。建议对该函数进行详细的代码审查,确保它不会引入新的安全风险。
|
|
||
| function _splitLines(s) { | ||
| return String(s || '').split(/\r?\n/); | ||
| } |
There was a problem hiding this comment.
新增的 sanitizeForReminder 函数用于处理不可信的作业输出,防止标签被解析为边界。虽然这个函数的意图是好的,但需要注意以下几点:
- 正则表达式安全性:确保正则表达式不会导致拒绝服务(ReDoS)攻击,尤其是在处理不可信输入时。建议对输入进行长度限制或其他验证。
- 输入验证:在调用
String(s == null ? '' : s)之前,建议对s进行更严格的类型检查,确保其为字符串类型,避免潜在的类型转换问题。 - 代码可读性:虽然使用了零宽空格来保持可读性,但可能会影响后续的文本处理,需确保后续处理逻辑能够正确处理这种情况。
| return result; | ||
| } | ||
|
|
||
| module.exports = { extractAnomalies, extractProgress, buildDigest, sanitizeForReminder }; |
There was a problem hiding this comment.
在 buildDigest 函数中,调用了 sanitizeForReminder 函数来处理结果字符串。需要注意的是:
- 性能影响:如果
parts数组非常大,调用sanitizeForReminder可能会导致性能下降,建议在性能敏感的场景中进行基准测试。 - 异常处理:在处理字符串时,建议添加异常处理逻辑,以防止在极端情况下发生未处理的异常。
| text, | ||
| // True when the most-recent execution saturated the capture cap | ||
| // (terminal-monitor stops appending past MAX_BYTES_PER_EXECUTION). | ||
| // Its captured output then stops growing even though the job keeps |
There was a problem hiding this comment.
在处理 text 时,虽然进行了长度限制,但未对 recs 的内容进行有效性检查。如果 recs 中的 output 包含恶意代码或不安全的内容,可能会导致安全漏洞。建议在拼接 text 之前,对 output 进行过滤或转义,以防止潜在的代码注入风险。
|
|
||
| // ---------- static core (cacheable across all requests) ---------- | ||
|
|
||
| function getStaticCore() { |
There was a problem hiding this comment.
删除了 DYNAMIC_BOUNDARY 常量的定义,可能会影响到后续的逻辑处理。建议保留该常量,以确保代码的可读性和可维护性。
| - Do not add comments unless the WHY is non-obvious. Identifiers explain WHAT. | ||
| - Avoid OWASP Top 10 vulnerabilities. Fix insecure code immediately if you write it. | ||
| - If an approach fails, diagnose why before switching tactics. Do not brute-force. | ||
| - Avoid time estimates. |
There was a problem hiding this comment.
在注释中提到的内容与实际代码逻辑不一致,建议确保注释与代码逻辑相符,以避免误导后续开发者。
| const BASE_SYSTEM_PROMPT = buildSystemPrompt({ includeWorkspaceInstructions: false }); | ||
|
|
||
| module.exports = { BASE_SYSTEM_PROMPT, buildSystemPrompt, DYNAMIC_BOUNDARY }; | ||
| module.exports = { BASE_SYSTEM_PROMPT, buildSystemPrompt }; |
There was a problem hiding this comment.
建议保留 DYNAMIC_BOUNDARY 的注释,虽然不再作为文本输出,但其作为逻辑分隔符的作用仍需说明,以便后续开发者理解代码结构。
| required: [], | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
在此处将 required 属性从 ['reason'] 修改为空数组 [],可能导致在某些情况下缺少必要的状态提示信息。建议保留 reason 为必填项,以确保用户在 UI 中能够获得必要的上下文信息,避免因缺少描述而导致的用户体验问题。此外,描述中提到的 "generic placeholder" 可能会影响到用户理解当前状态的能力,建议进一步明确该占位符的内容。
| } else { | ||
| run.reply = { user: text, asst: '', thoughts: '' }; | ||
| run.messages.push({ role: 'user', content: userContent }); | ||
| Logger.info('USER_SEND', { sid, len: (text || '').length, model, baseUrl, mode, text: (text || '').slice(0, 2000) }); |
| // - Rate limit is a flat per-hour cap per session (default 12, configurable | ||
| // via deepCopilot.autoResume.maxResumesPerHour). |
| const prevLen = w._lastSeenLen.get(c.job) || 0; | ||
| const newSlice = out.length > prevLen ? out.slice(prevLen) : ''; | ||
| if (newSlice.length > 0) { | ||
| w._lastSeenLen.set(c.job, out.length); | ||
| w._lastOutputChangeAt.set(c.job, Date.now()); | ||
| } else if (out.length < prevLen) { | ||
| // Buffer shrank/reset (ring-buffer dropped older executions, or the | ||
| // job restarted). That IS activity, not silence — re-baseline the | ||
| // seen length and refresh the idle clock so output_silent does not | ||
| // fire spuriously. | ||
| w._lastSeenLen.set(c.job, out.length); | ||
| w._lastOutputChangeAt.set(c.job, Date.now()); | ||
| } |
- wake-scheduler 头注释配置键改为 deepseekAgent.autoResumeMaxPerHour - _pollOutput 用尾部内容签名判活,修复输出截断封顶后 output_silent 误触发/漏匹配 - 每个回合开始重置 _yieldCount/_yieldSummaryNudged,避免跨回合泄漏
|
AI 审查在超大 diff 下质量会下降,建议:
|
| run.reply.thoughts = ''; | ||
| Logger.info('AUTO_RESUME_TURN', { | ||
| sid, | ||
| watcherId: autoResume.watcherId || null, |
There was a problem hiding this comment.
在此代码段中,虽然重置 _yieldCount 和 _yieldSummaryNudged 是为了确保每个回合的预算和状态都是干净的,但需要注意以下几点:
- 安全性:确保
run对象在使用之前已经被正确初始化,避免出现空指针异常。建议在使用run之前添加非空检查。 - 异常处理:如果
Logger.info调用失败,是否有相应的异常处理机制?建议在日志记录时添加异常捕获,以防止程序崩溃。 - 代码风格:注释部分的风格与现有代码一致,但建议在注释中明确说明为什么要重置这些值,以便后续维护人员理解其必要性。
综上所述,建议在此处添加空指针检查和异常处理机制。
| if (re.test(hay) || (prevLen === 0 && re.test(out))) { | ||
| return _fire(w, { | ||
| kind: 'output_match', | ||
| jobId: c.job, |
There was a problem hiding this comment.
在处理输出时,增加了对尾部签名的比较,这样可以更好地检测到输出的变化。然而,需要确保在使用 prevSig 时,已经对其进行了初始化检查,以防止出现空指针异常。此外,建议在 tailChanged 的判断中增加对 prevSig 是否为 undefined 的处理,以避免潜在的错误。
| if (hay && re.test(hay)) { | ||
| return _fire(w, { | ||
| kind: 'progress_at', | ||
| jobId: c.job, |
There was a problem hiding this comment.
在正则表达式匹配时,增加了对 newSlice 和 tailChanged 的处理,这样可以更好地捕捉到输出的变化。然而,建议在使用 hay 之前,确保其不为空,以避免不必要的正则匹配失败。
| for (const k of Object.keys(m)) { | ||
| if (k.length > 1 && k.charCodeAt(0) === 95 /* '_' */) { | ||
| if (copy === m) copy = Object.assign({}, m); | ||
| delete copy[k]; | ||
| } |
| if (!_checkRateLimit(w.sessionId)) { | ||
| const retryMs = _msUntilRateSlot(w.sessionId); | ||
| Logger.info('WATCHER_RATE_LIMITED', { | ||
| sessionId: w.sessionId, | ||
| id: w.id, | ||
| trigger: evidence.kind, | ||
| retryMs, | ||
| }); | ||
| w._timers = w._timers || []; | ||
| w._timers.push(setTimeout(() => _fire(w, evidence), retryMs)); | ||
| return; | ||
| } |
- providers/index.js 改用对象展开复制消息,避免 Object.assign 触发 __proto__ setter 的原型污染 - wake-scheduler 限流重试每个 watcher 仅保留单个定时器,用最新 evidence,避免 poll tick 反复堆积
|
AI 审查在超大 diff 下质量会下降,建议:
|
| if (w._retryTimer) { try { clearTimeout(w._retryTimer); } catch {} w._retryTimer = null; } | ||
| if (w._poll) { try { clearInterval(w._poll); } catch {} w._poll = null; } | ||
| if (w._offBgEnd) { try { w._offBgEnd(); } catch {} w._offBgEnd = null; } | ||
| _watchers.delete(w.id); |
There was a problem hiding this comment.
在清理定时器时,虽然使用了 try-catch 来处理可能的异常,但没有对异常进行处理或记录,可能会导致潜在的错误被忽略。建议在 catch 块中至少记录错误信息,以便后续排查。
| }, retryMs); | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
在设置重试定时器时,虽然确保了只存在一个定时器,但在清除定时器时同样没有记录异常。建议在清除定时器时添加日志记录。此外,使用 setTimeout 时,确保 retryMs 的值是有效的,避免出现负值或极小值导致的性能问题。
| if (copy === m) copy = { ...m }; | ||
| delete copy[k]; | ||
| } | ||
| } |
There was a problem hiding this comment.
在这一段代码中,使用了对象扩展运算符({ ...m })来替代 Object.assign({}, m),这是一个很好的改动,可以避免原型污染的问题。然而,需要确保在 messages 中没有其他潜在的安全问题,比如消息内容是否经过适当的清理和验证,以防止 XSS 攻击等安全漏洞。建议在函数开头添加输入验证逻辑,确保 messages 的内容是安全的。
| if (copy === m) copy = { ...m }; | ||
| delete copy[k]; | ||
| } | ||
| } |
There was a problem hiding this comment.
同样在这一段中,使用对象扩展运算符是合适的,但需要注意 effectiveStrip 的来源和内容,确保它不会包含恶意的键名。此外,建议在删除属性之前,先进行一次深拷贝,以防止对原始对象的意外修改。建议在函数中添加异常处理逻辑,以捕获可能的错误,确保函数的健壮性。
| body.drawer-open{padding-right:280px;transition:padding-right .15s ease} | ||
| body.drawer-open #session-backdrop{display:none!important} | ||
| @media (max-width:640px){body.drawer-open{padding-right:0}body.drawer-open #session-backdrop.open{display:block}} |
| const recs = getRecentExecutions(term, 20); | ||
| let text = recs.map(r => r.output || '').join('\n'); | ||
| // Tail-cap the joined text: up to 20×64KB (~1.28MB) is re-allocated on every | ||
| // poll tick per watcher (5s × up to 8 watchers/session). Downstream logic | ||
| // (regex checks + digest builder) only needs recent output, so keep just the | ||
| // trailing window. Truncation is reported separately below, so dropping the | ||
| // head here does not lose the saturation signal. | ||
| if (text.length > MAX_OUTPUT_TAIL) text = text.slice(-MAX_OUTPUT_TAIL); |
| // Tools exposed to the model for the "watch + yield" auto-resume primitive. | ||
| // | ||
| // watch(condition, description?) — register a trigger, return a watcherId | ||
| // yield_turn(reason) — end the current agent turn cleanly | ||
| // |
背景
此前多步工具回合表现为「一堆工具先跑、最后才蹦出文字」。日志里每个
ITER_END都是assistant_chars:0—— 模型把「我现在要做 X」的解说全部塞进了 DeepSeek 的reasoning_content(隐藏的 THINK 通道),可见的content通道全程为空。这和 GitHub Copilot「先说一句话 → 再调工具」的穿插体验差距明显。改动
系统提示(核心修复,
src/prompts/system.js)No preamble, no "I'll now…"改成「禁空话(Great question/Sure)、但要求动作解说」—— 每次工具调用前用一句可见正文说明「要做什么 + 为什么」。content而非 reasoning/thinking 通道;多步工具回合里每个工具调用前先写一句,而不是沉默到最后。流式管道本身(
src/api/openai-client.js)已支持content先于tool_calls穿插渲染,无需改动。捆绑的在途功能
本分支同时包含此前在途的后台唤醒相关工作:
src/chat/wake-scheduler.js— watch + yield_turn 的后台条件唤醒调度。src/chat/digest.js— 会话摘要。src/tools/sleep.js— sleep 工具。src/chat/agent-loop.js等相应配套改动。验证
node esbuild.config.js构建通过(out/extension.js666 KB,无错误)。release/deep-copilot-0.43.0.vsix。ITER_END的assistant_chars > 0、每个工具方块前有解说、yield_turn前有总结。风险