mocode-ai 1.6.0 → 1.6.1
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.
- package/README.md +23 -8
- package/README.zh-CN.md +24 -10
- package/dist/agent/run-coordinator.js +4 -0
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/agent/tool-turn.js +1 -1
- package/dist/attachments/image.js +119 -2
- package/dist/config/index.js +28 -4
- package/dist/config/profiles.js +42 -10
- package/dist/context/encoders/search.js +6 -2
- package/dist/context/vision-window.js +2 -2
- package/dist/i18n/index.js +30 -14
- package/dist/repl/commands/image.js +7 -2
- package/dist/repl/commands/router.js +13 -1
- package/dist/repl/commands/tool-group.js +1 -1
- package/dist/repl/commands.js +2 -0
- package/dist/runtime/dev-server-manager.js +4 -2
- package/dist/runtime/shell.js +153 -0
- package/dist/skills/builtin-skills.js +1 -1
- package/dist/tools/builtins/dev-server.js +23 -6
- package/dist/tools/builtins/grep.js +97 -26
- package/dist/tools/builtins/index.js +6 -6
- package/dist/tools/builtins/read-file.js +136 -12
- package/dist/tools/builtins/run-command.js +65 -10
- package/dist/tools/builtins/screenshot.js +17 -41
- package/dist/tools/builtins/web-fetch.js +147 -33
- package/dist/tools/builtins/write-file.js +104 -7
- package/dist/tools/constants.js +7 -0
- package/dist/tools/policy.js +25 -6
- package/dist/tools/router.js +20 -4
- package/dist/tools/tool-runtime.js +63 -1
- package/dist/ui/render.js +20 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,9 +208,11 @@ Common backend `base_url` values:
|
|
|
208
208
|
| `SUB_AGENT_MAX_STEPS` | Sub-agent loop safety ceiling; defaults to the main-agent value | `1000` |
|
|
209
209
|
| `SANDBOX_ROOT` | Sandbox root directory (file operation boundary; falls back to cwd if unset) | none |
|
|
210
210
|
| `MOCODE_SUBAGENT_ENABLED` | Set `false` to veto the `orchestration` route group; unset/`true` allows on-demand routing | unset |
|
|
211
|
-
| `MOCODE_FRONTEND_TOOLS_ENABLED` | Set `false` to veto `browser-debug` and `desktop-observe
|
|
211
|
+
| `MOCODE_FRONTEND_TOOLS_ENABLED` | Set `false` to veto `browser-debug` and `desktop-observe` (does not affect `background-exec`); unset/`true` allows routing | unset |
|
|
212
212
|
| `MOCODE_COMPUTER_USE_ENABLED` | Set `false` to veto high-risk `computer-control`; unset/`true` allows explicit-intent routing | unset |
|
|
213
213
|
| `MEMORY_ENABLED` | Set `false` to veto memory groups; `true` also enables the Memory Index | unset |
|
|
214
|
+
| `MOCODE_SHELL` | Default shell for `run_command` / `dev_server`: `cmd` \| `powershell` \| `bash` | `cmd` (Windows) / `bash` |
|
|
215
|
+
| `MOCODE_WEB_FETCH_PROXY` | Prefix-style plaintext proxy used by `web_fetch` only when a direct fetch is blocked (e.g. `https://r.jina.ai/`); opt-in because it hands your URLs to a third party | unset (disabled) |
|
|
214
216
|
| `MOCODE_THEME` | Color theme (default/dark/light…; shell env takes precedence over file) | `default` |
|
|
215
217
|
|
|
216
218
|
## Usage
|
|
@@ -230,19 +232,19 @@ The agent operates in **the working directory it was launched from** — to have
|
|
|
230
232
|
|
|
231
233
|
## Tools
|
|
232
234
|
|
|
233
|
-
Every real user turn first goes through a constrained LLM router.
|
|
235
|
+
Every real user turn first goes through a constrained LLM router. Nine common tools are always available (`read_file`, `glob`, `grep`, `web_search`, `web_fetch`, `plan_update`, `note_append`, `ask_human`, `use_skill`); additional capabilities are selected as composable groups for writing, shell debugging, browser debugging, desktop observation/control, memory, orchestration, and MCP. If the initial set is insufficient, the main model must call `add_tool_groups` alone; the expanded schemas appear on the next model step. A routing failure reuses the previous turn’s groups (or common-only), never the full toolset.
|
|
234
236
|
|
|
235
237
|
| Tool | Purpose |
|
|
236
238
|
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
237
|
-
| `read_file` | Read a file with line numbers
|
|
238
|
-
| `write_file` | Create/overwrite a file, auto-creating parent directories
|
|
239
|
+
| `read_file` | Read a file: text with line numbers (`offset` / `limit`), images (PNG/JPEG/GIF/WebP detected by magic bytes) as visual model input; other binaries are rejected instead of dumped as garbled text |
|
|
240
|
+
| `write_file` | Create/overwrite a file, auto-creating parent directories; `append=true` adds to the end without re-sending the whole file |
|
|
239
241
|
| `edit_file` | Precise string replacement (`old_string` must match uniquely) |
|
|
240
|
-
| `run_command` | Run a shell command, merging stdout+stderr, 120s default timeout
|
|
242
|
+
| `run_command` | Run a foreground shell command, merging stdout+stderr, 120s default timeout; `shell=cmd\|powershell\|bash` picks the interpreter |
|
|
241
243
|
| `glob` | Find files by glob pattern (excludes node_modules/.git) |
|
|
242
|
-
| `grep` | Regex content search, pure JS implementation, no `rg` dependency
|
|
244
|
+
| `grep` | Regex content search, pure JS implementation, no `rg` dependency; `context=N` returns neighbouring lines inline so a hit rarely needs a follow-up read |
|
|
243
245
|
| `codegraph` | With a `.codegraph/` index built, query symbol source and call chains (more accurate and cheaper than read_file/grep) |
|
|
244
246
|
| `web_search` | Web search (AnySearch), returns title/URL/snippet/body |
|
|
245
|
-
| `web_fetch` | Fetch a URL, cleaning HTML into plain text
|
|
247
|
+
| `web_fetch` | Fetch a URL, cleaning HTML into plain text; browser-like headers, auto-retry on transient failures, optional plaintext-proxy fallback |
|
|
246
248
|
| `use_skill` | Load the full SKILL.md instructions for a given skill |
|
|
247
249
|
| `ask_human` | Pop up a Q&A panel at decision points; user picks a preset or types freely (blocks until answered) |
|
|
248
250
|
| `plan_update` | Record/update the session execution plan (the `## Plan:` block in notes.md); three-state steps, at most one in_progress, auto-settles to `## Done:` when all complete |
|
|
@@ -256,7 +258,20 @@ Every real user turn first goes through a constrained LLM router. Ten common too
|
|
|
256
258
|
|
|
257
259
|
The six `memory_*` tools are split into `memory-read` and `memory-write` route groups. They appear only when the router selects them; `MEMORY_ENABLED=false` vetoes both groups, while `MEMORY_ENABLED=true` also enables the compact Memory Index in the prompt. `/memory_switch` manages that compatibility gate.
|
|
258
260
|
|
|
259
|
-
Frontend capabilities are also split by purpose: `browser`
|
|
261
|
+
Frontend capabilities are also split by purpose: `browser` forms `browser-debug`, whole-desktop `screenshot` is `desktop-observe`, and `dev_server` has its own ungated `background-exec` group — any process that must outlive a single tool call (dev server, inference service, watcher, log tail) belongs there rather than in `run_command`. Selecting `browser-debug` implies `background-exec`, so a weak model that only asks for the browser still gets the ability to start the server it needs to look at. Image reading lives in `read_file` (magic-byte sniffing) and remains a common read tool. The router may combine these groups with `computer-control` when a task genuinely needs both structured web diagnostics and real desktop interaction. `/fe off` is a hard veto, not a manual profile selector — it does not affect `dev_server`.
|
|
262
|
+
|
|
263
|
+
### Shell selection
|
|
264
|
+
|
|
265
|
+
`run_command` and `dev_server` accept `shell=cmd|powershell|bash`. The default is unchanged from earlier releases (`cmd.exe` on Windows, `bash` elsewhere) so existing prompts and skills keep working; `MOCODE_SHELL` flips the default globally for those who prefer POSIX on Windows. When `bash` is requested on Windows, Git for Windows' `bash.exe` is auto-detected — the WSL `System32\bash.exe` is deliberately excluded, since it lands in a Linux distro with different paths, toolchain, and security policy. Non-interactive `cmd.exe` cannot run `timeout /t`; use `shell=powershell` with `Start-Sleep`, or `shell=bash` with `sleep`.
|
|
266
|
+
|
|
267
|
+
### Automatic retry (the `retryable` contract)
|
|
268
|
+
|
|
269
|
+
`ToolOutcome.retryable` used to have zero consumers project-wide — a tool honestly marked "this was a transient failure" and nothing acted on it, leaving the model to burn a full LLM round-trip to retry (and often forgetting to). The runtime now re-issues calls that fail transiently, with backoff (400ms / 1200ms, two retries max):
|
|
270
|
+
|
|
271
|
+
- Only tools that explicitly declare `idempotent` participate — the side-effect-free network reads (`web_fetch`, `web_search`). No write or process tool declares it, and none is ever auto-retried: retrying those would duplicate side effects, so their retry semantics stay inside the tool (e.g. `edit_file`'s `expected_hash` conflict).
|
|
272
|
+
- Only `status=error` with `retryable=true` is retried; `denied` / `aborted` / `success` are terminal.
|
|
273
|
+
- **`TIMEOUT` is never auto-retried**: one timeout has already consumed the whole window (`web_fetch` uses 30s), so two retries could stretch a single tool call to 90s — exactly the frozen-spinner experience users hate. The `retryable` flag is still reported, so the model can decide for itself.
|
|
274
|
+
- Aborting mid-backoff gives up immediately instead of burning the window, and when retries are exhausted the attempt count is appended to the output so the model knows the runtime already tried.
|
|
260
275
|
|
|
261
276
|
### Frontend / UI loop
|
|
262
277
|
|
package/README.zh-CN.md
CHANGED
|
@@ -195,9 +195,11 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
195
195
|
| `SUB_AGENT_MAX_STEPS` | 子 Agent 循环安全上限,默认与主 Agent 一致 | `1000` |
|
|
196
196
|
| `SANDBOX_ROOT` | 沙箱根目录(文件操作边界;未配则用 cwd 兜底) | 无 |
|
|
197
197
|
| `MOCODE_SUBAGENT_ENABLED` | 设 `false` 硬禁用 `orchestration`;unset/`true` 允许按需路由 | 未设置 |
|
|
198
|
-
| `MOCODE_FRONTEND_TOOLS_ENABLED` | 设 `false` 硬禁用 `browser-debug` / `desktop-observe
|
|
198
|
+
| `MOCODE_FRONTEND_TOOLS_ENABLED` | 设 `false` 硬禁用 `browser-debug` / `desktop-observe`(不影响 `background-exec`);unset/`true` 允许路由 | 未设置 |
|
|
199
199
|
| `MOCODE_COMPUTER_USE_ENABLED` | 设 `false` 硬禁用高危 `computer-control`;unset/`true` 允许明确意图时路由 | 未设置 |
|
|
200
200
|
| `MEMORY_ENABLED` | 设 `false` 硬禁用 memory 簇;`true` 还会启用 Memory Index | 未设置 |
|
|
201
|
+
| `MOCODE_SHELL` | `run_command` / `dev_server` 的默认 shell:`cmd` \| `powershell` \| `bash` | Windows 上 `cmd`,其余 `bash` |
|
|
202
|
+
| `MOCODE_WEB_FETCH_PROXY` | `web_fetch` 直连被反爬拦截时才启用的前缀型纯文本代理(如 `https://r.jina.ai/`);默认关——把 URL 交给第三方必须由用户显式打开 | 未设置(关闭) |
|
|
201
203
|
| `MOCODE_THEME` | 颜色主题(default/dark/light…;shell 设置优先于文件) | `default` |
|
|
202
204
|
|
|
203
205
|
## 运行
|
|
@@ -217,23 +219,22 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
217
219
|
|
|
218
220
|
## 工具
|
|
219
221
|
|
|
220
|
-
每个真实用户轮都会先经过受约束的 LLM router
|
|
222
|
+
每个真实用户轮都会先经过受约束的 LLM router。九个公共工具始终可用(`read_file`、`glob`、`grep`、`web_search`、`web_fetch`、`plan_update`、`note_append`、`ask_human`、`use_skill`);写文件、shell 调试、浏览器调试、桌面观察/控制、记忆、编排和 MCP 作为可组合工具簇按需选择。初始能力不足时,主模型必须单独调用 `add_tool_groups`,新增 schema 从下一 step 生效。路由失败只继承上一轮工具簇(或仅公共工具),绝不回退到全工具。
|
|
221
223
|
|
|
222
224
|
| 工具 | 作用 |
|
|
223
225
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
-
| `read_file` |
|
|
225
|
-
| `view_image` | 读取已有 PNG/JPEG/GIF/WebP 图片并作为视觉输入回灌模型(最大 4 MiB) |
|
|
226
|
+
| `read_file` | 读文件:文本带行号(`offset` / `limit`);PNG/JPEG/GIF/WebP 按**魔数**识别(扩展名会说谎)并作为视觉输入回灌(4 MiB 内联上限,超限 PNG 自动降采样,`detail=low\|high` 控分辨率);其余二进制明确拒绝而非灌乱码 |
|
|
226
227
|
| `screenshot` | 经用户确认后截取主显示器或整个桌面,保存 PNG 并立即交给视觉模型分析 |
|
|
227
|
-
| `write_file` |
|
|
228
|
+
| `write_file` | 创建/覆盖文件,自动建父目录;`append=true` 在文件末尾追加,无需重发全文(分段写长文件/记日志的正确姿势) |
|
|
228
229
|
| `edit_file` | 精确字符串替换(`old_string` 须唯一匹配) |
|
|
229
|
-
| `run_command` |
|
|
230
|
-
| `dev_server` |
|
|
230
|
+
| `run_command` | 执行**前台** shell 命令,合并 stdout+stderr,默认 120s 超时(硬上限 10min);`shell=cmd\|powershell\|bash` 指定解释器 |
|
|
231
|
+
| `dev_server` | 启动/查看/读日志/停止**任意需跨调用存活的后台进程**(dev server、推理服务、watcher、日志尾随) |
|
|
231
232
|
| `browser` | Playwright 驱动真实 Chromium:导航 / 点击 / 填表 / 取文本 / 截图 / 控制台诊断 |
|
|
232
233
|
| `glob` | 按 glob 模式找文件(排除 node_modules/.git) |
|
|
233
|
-
| `grep` | 内容正则搜索,纯 JS 实现,不依赖 `rg`
|
|
234
|
+
| `grep` | 内容正则搜索,纯 JS 实现,不依赖 `rg`;`context=N` 内联返回邻居行并保留原始缩进,命中后基本不用再 read_file 一次 |
|
|
234
235
|
| `codegraph` | 已建 `.codegraph/` 索引时,查代码符号源码与调用链(比 read_file/grep 更准更省) |
|
|
235
236
|
| `web_search` | 联网搜索(AnySearch),返回标题/URL/摘要/正文 |
|
|
236
|
-
| `web_fetch` | 抓取指定 URL,HTML
|
|
237
|
+
| `web_fetch` | 抓取指定 URL,HTML 清洗成纯文本;带全套浏览器拟真头,瞬时失败(429/5xx/网络抖动)自动退避重试,可选纯文本代理回退 |
|
|
237
238
|
| `use_skill` | 加载某 skill 的完整 SKILL.md 指令 |
|
|
238
239
|
| `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
|
|
239
240
|
| `plan_update` | 记录/更新会话执行计划(notes.md 的 `## Plan:` 段);三态步骤机,同一时刻至多一个 in_progress,全部完成自动结算为 `## Done:` |
|
|
@@ -261,7 +262,20 @@ dev_server stop id=srv-xxxx
|
|
|
261
262
|
- 两者在 plan 模式下均被禁用;mocode 退出时会树杀后台进程并关闭浏览器。
|
|
262
263
|
- 浏览器二进制不随 npm 包分发,首次使用前需 `npx playwright install chromium`。
|
|
263
264
|
|
|
264
|
-
前端能力按用途拆分:`browser`
|
|
265
|
+
前端能力按用途拆分:`browser` 属于 `browser-debug`,整桌面截图 `screenshot` 属于 `desktop-observe`,而 `dev_server` 独立成**无 gate 的 `background-exec` 簇** —— 任何需要跨工具调用存活的进程(dev server、推理/模型服务、watcher、日志尾随)都归它,而不是塞进 `run_command`。选中 `browser-debug` 会**蕴含**激活 `background-exec`:弱模型只想到要浏览器时,也能拿到「先把服务起起来」的能力(半套能力比多一套能力更糟)。图片读取(`read_file` 魔数分流)则始终是公共只读能力。任务同时需要结构化网页诊断与真实桌面交互时,router 可再组合 `computer-control`。`/fe off` 是硬否决,不是手动 profile 选择器——它不影响 `dev_server`。
|
|
266
|
+
|
|
267
|
+
### Shell 选择器
|
|
268
|
+
|
|
269
|
+
`run_command` 与 `dev_server` 都接受 `shell=cmd|powershell|bash`。默认值与旧版一致(Windows 上 `cmd.exe`,其余平台 `bash`),现有 prompt / skill 不受影响;想全局换成 POSIX 的用户设 `MOCODE_SHELL` 即可。在 Windows 上请求 `bash` 时会自动探测 Git for Windows 的 `bash.exe` —— **刻意排除 WSL 的 `System32\bash.exe`**:它进的是 Linux 发行版,路径(`/mnt/f/…`)、工具链与安全策略都与 Windows 原生预期不符。另外,非交互 `cmd.exe` 跑不了 `timeout /t`(直接报错),需要等待时用 `shell=powershell` + `Start-Sleep`,或 `shell=bash` + `sleep`。
|
|
270
|
+
|
|
271
|
+
### 自动重试(retryable 契约)
|
|
272
|
+
|
|
273
|
+
工具返回的 `retryable` 此前全项目零消费者 —— 工具诚实标了「这是瞬时失败」,却没人据此行动,模型只能再发一轮 tool call 自救(白烧一个 LLM 往返,且常常忘记重试)。现在 runtime 对**显式声明 `idempotent`** 的工具在退避后自动重发(400ms / 1200ms,最多两次):
|
|
274
|
+
|
|
275
|
+
- 只有无副作用的网络只读工具(`web_fetch`、`web_search`)声明了该能力;写文件 / 起进程类工具**一个都没有**,也不会自动重试 —— 重试语义由工具自己决定(如 `edit_file` 的 `expected_hash` 冲突)。
|
|
276
|
+
- 只重试 `status=error` 且 `retryable=true` 的结果;`denied` / `aborted` / `success` 都是终态。
|
|
277
|
+
- **`TIMEOUT` 不自动重试**:一次超时已经烧掉整个超时窗口(`web_fetch` 是 30s),再试两次最坏会让单次工具调用变成 90s,用户看到的就是 spinner 长时间冻住。`retryable` 标记仍保留,模型可自行判断。
|
|
278
|
+
- 退避等待期间用户 Ctrl+C 立即放弃,不空耗窗口;重试用尽仍失败会把尝试次数写进 output,让模型知道「runtime 已经试过了,别再无脑重发」。
|
|
265
279
|
|
|
266
280
|
6 个 `memory_*` 工具拆成 `memory-read` 与 `memory-write`。只有 router 选择对应簇时才出现;`MEMORY_ENABLED=false` 会硬禁用两簇,`true` 还会把紧凑 Memory Index 注入 prompt。`/memory_switch` 同时管理这个兼容 gate 与 Index 状态。
|
|
267
281
|
|
|
@@ -429,6 +429,9 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
429
429
|
succeeded
|
|
430
430
|
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
431
431
|
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
432
|
+
expansion.implied.length > 0
|
|
433
|
+
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
434
|
+
: '',
|
|
432
435
|
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
433
436
|
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
434
437
|
]
|
|
@@ -448,6 +451,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
448
451
|
toVersion: expansion.snapshot.version,
|
|
449
452
|
requestedGroups: parsed.groups.map(String),
|
|
450
453
|
addedGroups: expansion.added,
|
|
454
|
+
impliedGroups: expansion.implied,
|
|
451
455
|
rejected: expansion.rejected,
|
|
452
456
|
reason: parsed.reason,
|
|
453
457
|
status: outcome.status,
|
|
@@ -138,6 +138,7 @@ class LegacyCompatibleToolDispatcher {
|
|
|
138
138
|
succeeded
|
|
139
139
|
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
140
140
|
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
141
|
+
expansion.implied.length > 0 ? `Implied groups also activated: ${expansion.implied.join(', ')}.` : '',
|
|
141
142
|
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
142
143
|
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
143
144
|
]
|
package/dist/agent/tool-turn.js
CHANGED
|
@@ -8,7 +8,7 @@ const PLAN_NAG_TEXT = '[mocode] Reminder: you have an active plan in notes.md bu
|
|
|
8
8
|
* 「工具回灌的屏幕帧」与「用户粘贴的图」——两边各写一份字符串会让识别规则悄悄失效。
|
|
9
9
|
* 新增附件产出点**必须**复用这一个前言。
|
|
10
10
|
*/
|
|
11
|
-
export const ATTACHMENT_PREAMBLE = 'The
|
|
11
|
+
export const ATTACHMENT_PREAMBLE = 'The read_file tool loaded the following visual input: ';
|
|
12
12
|
/** Owns tool-turn history publication, transaction settlement, plan nag, attachments and checkpoint ordering. */
|
|
13
13
|
export async function runToolTurn(input) {
|
|
14
14
|
const { opts, ctx, historyManager, result, stream, step, maxSteps, planState, turnLifecycle, cancellationLifecycle, terminationPolicy, rebuildHistoryIndexes, dispatch, } = input;
|
|
@@ -2,6 +2,7 @@ import { readFile, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, extname } from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { jailResolve } from '../sandbox/jail.js';
|
|
5
|
+
import { decodePng, downscale, encodePng } from '../runtime/screen-pipeline.js';
|
|
5
6
|
export const MAX_INLINE_BYTES_DEFAULT = 4 * 1024 * 1024;
|
|
6
7
|
const MIME_BY_EXT = {
|
|
7
8
|
'.png': 'image/png',
|
|
@@ -13,6 +14,40 @@ const MIME_BY_EXT = {
|
|
|
13
14
|
export function detectMime(p) {
|
|
14
15
|
return MIME_BY_EXT[extname(p).toLowerCase()] ?? null;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 按**魔数**判定图片类型(不看扩展名)。
|
|
19
|
+
*
|
|
20
|
+
* 为什么必须有这条:扩展名会说谎——`data.bin` 可能是 PNG,`notes.png` 也可能是文本。
|
|
21
|
+
* read_file 要靠它决定「走文本行号分页」还是「走视觉通道」,判错的代价是把二进制
|
|
22
|
+
* 当 UTF-8 解码(实测一张 42KB PNG 解码出 17862 个 U+FFFD,占 45%)灌进 history。
|
|
23
|
+
*/
|
|
24
|
+
export function sniffImageMime(buf) {
|
|
25
|
+
if (buf.length >= 8 && buf[0] === 0x89 && buf.toString('ascii', 1, 8) === 'PNG\r\n\x1a\n')
|
|
26
|
+
return 'image/png';
|
|
27
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
28
|
+
return 'image/jpeg';
|
|
29
|
+
if (buf.length >= 6 && buf.toString('ascii', 0, 6) === 'GIF87a')
|
|
30
|
+
return 'image/gif';
|
|
31
|
+
if (buf.length >= 6 && buf.toString('ascii', 0, 6) === 'GIF89a')
|
|
32
|
+
return 'image/gif';
|
|
33
|
+
// WebP: RIFF....WEBP
|
|
34
|
+
if (buf.length >= 12 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
35
|
+
return 'image/webp';
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 二进制嗅探:头部 4KB 含 C0 控制字符(NUL/BEL/ESC 等,放行 \t\n\r)即视为二进制。
|
|
41
|
+
*
|
|
42
|
+
* 与 grep 的 BINARY_PROBE_RE 同源同口径(集中在此,避免两处正则漂移):SQLite、压缩包、
|
|
43
|
+
* 可执行文件、minified 数据 dump 都会命中。用途是让 read_file 明确拒绝并指路,
|
|
44
|
+
* 而不是把乱码塞进上下文——那既烧 token 又让模型基于垃圾内容做判断。
|
|
45
|
+
*/
|
|
46
|
+
export const BINARY_PROBE_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
|
|
47
|
+
export function isProbablyBinary(head) {
|
|
48
|
+
const sample = typeof head === 'string' ? head : head.toString('latin1');
|
|
49
|
+
return BINARY_PROBE_RE.test(sample.slice(0, 4096));
|
|
50
|
+
}
|
|
16
51
|
function formatBytes(n) {
|
|
17
52
|
if (n < 1024)
|
|
18
53
|
return `${n} B`;
|
|
@@ -23,11 +58,93 @@ function formatBytes(n) {
|
|
|
23
58
|
export function renderChip(att) {
|
|
24
59
|
return `📷 ${att.name} (${formatBytes(att.bytes)})`;
|
|
25
60
|
}
|
|
61
|
+
/** 降采样兜底的长边上界:对齐主流视觉模型的原生分辨率(Claude 1568 / OpenAI 高分块同级)。 */
|
|
62
|
+
export const DOWNSCALE_MAX_EDGE = 1568;
|
|
63
|
+
/**
|
|
64
|
+
* 读图 + 超限自动降采样兜底。
|
|
65
|
+
*
|
|
66
|
+
* 为什么要兜底:4 MiB 内联上限对高 DPI 截图偏紧(一张 4K Retina PNG 轻松 5-8 MiB)。
|
|
67
|
+
* 直接拒绝会逼模型去找压缩工具/改用户文件,而**服务端缩一下就能成功**。
|
|
68
|
+
* screenshot 早有这条路径(screenshot.ts 的 FALLBACK_MAX_EDGE 分支),这里抽成共享 helper,
|
|
69
|
+
* 让 read_file 图片通道(原 view_image,已并入)同样受益。
|
|
70
|
+
*
|
|
71
|
+
* 能力边界(诚实声明):`runtime/screen-pipeline.ts` 的 PNG 解码是手写的、只支持
|
|
72
|
+
* **8-bit RGB/RGBA PNG**(项目刻意零原生图像依赖)。所以兜底只覆盖 PNG;
|
|
73
|
+
* JPEG/WebP/GIF 超限仍然拒绝,reason 会写明这一点。
|
|
74
|
+
*/
|
|
75
|
+
export async function loadImageAttachmentWithDownscale(input, opts) {
|
|
76
|
+
const loaded = await loadImageAttachment(input, opts);
|
|
77
|
+
if (loaded.ok)
|
|
78
|
+
return loaded;
|
|
79
|
+
// 只有「体积超限」这一种失败值得兜底:路径为空/扩展名不支持/沙箱越界/不是普通文件
|
|
80
|
+
// 都是真错误,缩图解决不了,原样透出让调用方给出准确提示。
|
|
81
|
+
if (!loaded.reason.startsWith('too large'))
|
|
82
|
+
return loaded;
|
|
83
|
+
const mime = detectMime(input) ?? opts.sniffedMime ?? null;
|
|
84
|
+
if (mime !== 'image/png') {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: `${loaded.reason} — 自动降采样仅支持 PNG(零原生图像依赖);${extname(input)} 请先转成 PNG 或自行压缩后重试`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
let abs;
|
|
91
|
+
try {
|
|
92
|
+
abs = jailResolve(input.trim());
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
return { ok: false, reason: `outside sandbox: ${e instanceof Error ? e.message : String(e)}` };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const png = decodePng(await readFile(abs));
|
|
99
|
+
const { img } = downscale(png, DOWNSCALE_MAX_EDGE);
|
|
100
|
+
const buf = encodePng(img);
|
|
101
|
+
if (buf.length > opts.maxBytes) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: `${loaded.reason} — 降采样到 ${img.width}×${img.height} 后仍有 ${formatBytes(buf.length)},超过 ${formatBytes(opts.maxBytes)}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let st;
|
|
108
|
+
try {
|
|
109
|
+
st = await stat(abs);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
st = null;
|
|
113
|
+
}
|
|
114
|
+
const id = createHash('sha1')
|
|
115
|
+
.update(abs)
|
|
116
|
+
.update('\0downscaled')
|
|
117
|
+
.update(String(img.width))
|
|
118
|
+
.update('\0')
|
|
119
|
+
.update(String(img.height))
|
|
120
|
+
.update('\0')
|
|
121
|
+
.update(String(st?.mtimeMs ?? 0))
|
|
122
|
+
.digest('hex');
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
downscaledFrom: { width: png.width, height: png.height },
|
|
126
|
+
att: {
|
|
127
|
+
id,
|
|
128
|
+
path: abs,
|
|
129
|
+
name: basename(abs),
|
|
130
|
+
bytes: buf.length,
|
|
131
|
+
mime: 'image/png',
|
|
132
|
+
dataUrl: `data:image/png;base64,${buf.toString('base64')}`,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
return { ok: false, reason: `${loaded.reason}(降采样兜底失败: ${message})` };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
26
141
|
export async function loadImageAttachment(input, opts) {
|
|
27
142
|
const trimmed = input.trim();
|
|
28
143
|
if (!trimmed)
|
|
29
144
|
return { ok: false, reason: '路径为空' };
|
|
30
|
-
|
|
145
|
+
// 扩展名优先(便宜、无需读文件);不认识时用调用方给的魔数嗅探结果兜底 ——
|
|
146
|
+
// read_file 读到的图片常常没有正确扩展名(截图缓存 / 构建产物 / 无扩展名 blob)。
|
|
147
|
+
const mime = detectMime(trimmed) ?? opts.sniffedMime ?? null;
|
|
31
148
|
if (!mime) {
|
|
32
149
|
return { ok: false, reason: `unsupported: ${extname(trimmed) || '(无扩展名)'} — 仅支持 png/jpg/jpeg/gif/webp` };
|
|
33
150
|
}
|
|
@@ -52,7 +169,7 @@ export async function loadImageAttachment(input, opts) {
|
|
|
52
169
|
if (st.size > opts.maxBytes) {
|
|
53
170
|
return {
|
|
54
171
|
ok: false,
|
|
55
|
-
reason: `too large: ${formatBytes(st.size)} (max ${formatBytes(opts.maxBytes)})
|
|
172
|
+
reason: `too large: ${formatBytes(st.size)} (max ${formatBytes(opts.maxBytes)})`,
|
|
56
173
|
};
|
|
57
174
|
}
|
|
58
175
|
const buf = await readFile(abs);
|
package/dist/config/index.js
CHANGED
|
@@ -12,6 +12,9 @@ import { detectLanguage, setLanguage, t } from '../i18n/index.js';
|
|
|
12
12
|
import { isProfileName, profileHasGroup } from './profiles.js';
|
|
13
13
|
// 端点常量归协议实现方(jev-client.ts,纯叶子无 import,不引入环);此处只引用不重定义。
|
|
14
14
|
import { DEFAULT_JEV_BASE_URL } from '../tools/jev-client.js';
|
|
15
|
+
// shell.ts 是纯叶子(只 import node 内置):PLATFORM_NOTE 的措辞必须跟 run_command/dev_server
|
|
16
|
+
// 实际 spawn 的默认 shell 一致,单一事实源,防「文案说 cmd、实际跑 bash」漂移。
|
|
17
|
+
import { defaultShellKind } from '../runtime/shell.js';
|
|
15
18
|
/**
|
|
16
19
|
* 按优先级加载配置文件并回填 process.env:
|
|
17
20
|
* 候选(后者覆盖前者,优先级升序):<cwd>/.env(兼容旧用法,最低)→ ~/.mocode/config(全局)→ <cwd>/.mocode/config(项目级覆盖,最高)。
|
|
@@ -95,9 +98,17 @@ export function isModelConfigured() {
|
|
|
95
98
|
return !!config.baseURL && !!config.apiKey;
|
|
96
99
|
}
|
|
97
100
|
const PLATFORM_NOTE = (() => {
|
|
101
|
+
const shell = defaultShellKind();
|
|
102
|
+
if (process.platform === 'win32' && shell === 'cmd') {
|
|
103
|
+
return `- This is Windows: \`run_command\`/\`dev_server\` default to \`cmd.exe /c\` — use cmd syntax and \`%VAR%\`; Unix builtins and command substitution are unavailable.
|
|
104
|
+
- Non-interactive cmd cannot run \`timeout /t\` (it errors out); pass \`shell: "powershell"\` with \`Start-Sleep\`, or \`shell: "bash"\` with \`sleep\`, when a wait is needed.
|
|
105
|
+
- Prefer read_file/glob/grep for file discovery and reading. When a POSIX shell fits better, pass \`shell: "bash"\` (Git Bash, auto-detected) or \`shell: "powershell"\`; never mix syntaxes within one command.`;
|
|
106
|
+
}
|
|
98
107
|
if (process.platform === 'win32') {
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
// MOCODE_SHELL 翻转了默认:措辞必须跟着变,否则模型按文案写 cmd 语法却落进 bash。
|
|
109
|
+
return `- This is Windows: \`run_command\`/\`dev_server\` default to ${shell} (MOCODE_SHELL override) — use ${shell === 'bash' ? 'POSIX syntax ($VAR, &&, forward-slash paths)' : 'PowerShell syntax ($VAR, Start-Sleep)'}.
|
|
110
|
+
- cmd-only syntax (\`%VAR%\`, \`start /b\`, \`dir\`) needs an explicit \`shell: "cmd"\`; do not mix syntaxes within one command.
|
|
111
|
+
- Prefer read_file/glob/grep for file discovery and reading; reach for the shell only when a dedicated tool does not fit.`;
|
|
101
112
|
}
|
|
102
113
|
if (process.platform === 'darwin') {
|
|
103
114
|
return `- This is macOS: \`run_command\` uses bash with BSD utilities. Prefer read_file/glob/grep; account for BSD/GNU differences when shell commands are necessary.`;
|
|
@@ -412,12 +423,12 @@ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
|
412
423
|
## Tool policy
|
|
413
424
|
- Silent Execution: invoke tools directly without preamble. Output visible text ONLY for the final answer and critical mid-task findings. Strictly no step-by-step narration (no "let me…", "让我先…", "now checking…" between calls).
|
|
414
425
|
- Go directly to a known path or symbol; use discovery tools only when the location is unknown.
|
|
415
|
-
- Edit against a FRESH read: before any edit_file
|
|
426
|
+
- Edit against a FRESH read: before any edit_file or a replacing write_file, call read_file on the exact path and copy both its latest hash and the exact target text. Never reconstruct old_string from a grep/summary/diff — those lose whitespace and indentation and cause edit failures. (write_file with append=true is the exception: it reads and hashes the file itself, so no prior read_file and no expected_hash are needed.)
|
|
416
427
|
- A read_file hash from before a compaction, session resume, edit conflict, or external change is STALE and will be rejected — re-read rather than reuse an old hash.
|
|
417
428
|
- Emit multiple independent tool calls in ONE assistant message so they run concurrently — e.g. several read_file regions, a grep plus a glob, or several web_fetch calls. One lookup per message wastes a full model round-trip each time. Place parallel-safe calls consecutively; keep any call that depends on their results (e.g. an edit) for the next message.
|
|
418
429
|
- Never batch a read with an edit that depends on it; do not repeat overlapping reads or unchanged failed calls.
|
|
419
430
|
- On failure, inspect the full error, change the approach, and retry only with a reason. Drop stale tool output when it no longer supports the task.
|
|
420
|
-
- For generated content over roughly 200 lines or 5K tokens,
|
|
431
|
+
- For generated content over roughly 200 lines or 5K tokens, write it in stages: first write_file the initial chunk, then extend it with write_file(append=true, content=<next chunk>) — each call stays small and the file grows transactionally. Appends are VERBATIM: if the file does not already end with a newline, begin your chunk with "\\n" so lines do not merge.
|
|
421
432
|
|
|
422
433
|
## Environment
|
|
423
434
|
${PLATFORM_NOTE}
|
|
@@ -803,6 +814,19 @@ export function getRouterMode() {
|
|
|
803
814
|
export function updateRouterMode(mode) {
|
|
804
815
|
process.env.MOCODE_ROUTER_MODE = mode;
|
|
805
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* 工具预路由总开关。关闭 = 每个 turn 跳过路由调用,只保留常驻簇
|
|
819
|
+
* (controller 无条件激活的 DEFAULT_ROUTE_GROUPS)+ 通用工具,不选任何额外簇;默认开启。
|
|
820
|
+
* 与 RouterMode 同一理由直接读写 process.env:支持 /router on|off 即时切换,
|
|
821
|
+
* routeToolGroups 在每 turn 调用点实时读,改后下一真实用户 turn 生效。
|
|
822
|
+
*/
|
|
823
|
+
export function isToolRoutingEnabled() {
|
|
824
|
+
return process.env.MOCODE_ROUTER_ENABLED !== 'false';
|
|
825
|
+
}
|
|
826
|
+
/** /router 的写入口;持久化由调用方写 MOCODE_ROUTER_ENABLED(见 config/file.ts)。 */
|
|
827
|
+
export function updateToolRoutingEnabled(enabled) {
|
|
828
|
+
process.env.MOCODE_ROUTER_ENABLED = enabled ? 'true' : 'false';
|
|
829
|
+
}
|
|
806
830
|
function readNumberEnv(name, fallback) {
|
|
807
831
|
const raw = process.env[name];
|
|
808
832
|
if (raw === undefined || raw.trim() === '')
|
package/dist/config/profiles.js
CHANGED
|
@@ -5,25 +5,28 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* 工具簇 → 工具名。新增工具时归到对应簇;一个工具只属一个簇。
|
|
8
|
-
* view_image
|
|
9
|
-
* modelAttachments
|
|
10
|
-
*
|
|
8
|
+
* 图片读取由 read_file 的魔数嗅探分支覆盖(原 view_image 已并入):文本/图片分流,
|
|
9
|
+
* 工具产生的即时视觉结果通过 modelAttachments 直接回灌。screenshot 留 frontend(抓整个
|
|
10
|
+
* 桌面,隐私敏感,主要服务前端联调)。
|
|
11
|
+
* dev_server 归 core-write 而非 frontend:它是「后台进程管理」能力(与 run_command 同级),
|
|
12
|
+
* 不是浏览器工具,不该受 MOCODE_FRONTEND_TOOLS_ENABLED 否决。与自动路由的 background-exec
|
|
13
|
+
* 组(无 gateEnv)语义对齐。
|
|
11
14
|
*/
|
|
12
15
|
export const TOOL_GROUPS = {
|
|
13
|
-
'core-read': ['read_file', '
|
|
14
|
-
'core-write': ['write_file', 'edit_file', 'run_command'],
|
|
16
|
+
'core-read': ['read_file', 'glob', 'grep'],
|
|
17
|
+
'core-write': ['write_file', 'edit_file', 'run_command', 'dev_server'],
|
|
15
18
|
'agent-meta': ['plan_update', 'note_append', 'ask_human', 'use_skill', 'run_skill'],
|
|
16
19
|
web: ['web_search', 'web_fetch'],
|
|
17
|
-
frontend: ['browser', '
|
|
20
|
+
frontend: ['browser', 'screenshot'],
|
|
18
21
|
computer: ['computer'],
|
|
19
22
|
memory: ['memory_save', 'memory_search', 'memory_list', 'memory_update', 'memory_forget', 'memory_graph'],
|
|
20
23
|
subagent: ['sub-agent'],
|
|
21
24
|
};
|
|
22
25
|
// ── LLM 自动工具路由 ──────────────────────────────────────────────────────
|
|
23
|
-
/** 主 Agent 每一步都可见的低风险、高复用工具。
|
|
26
|
+
/** 主 Agent 每一步都可见的低风险、高复用工具。
|
|
27
|
+
* 图片读取并入 read_file(魔数嗅探分流,原 view_image 已移除)。 */
|
|
24
28
|
export const COMMON_TOOL_NAMES = [
|
|
25
29
|
'read_file',
|
|
26
|
-
'view_image',
|
|
27
30
|
'glob',
|
|
28
31
|
'grep',
|
|
29
32
|
'web_search',
|
|
@@ -52,9 +55,18 @@ export const TOOL_ROUTE_GROUPS = {
|
|
|
52
55
|
tools: ['run_command'],
|
|
53
56
|
description: 'Run tests, builds, linters, Git, package managers, logs, diagnostics, and foreground commands.',
|
|
54
57
|
},
|
|
58
|
+
// dev_server 从 browser-debug 拆出:它的能力是「跨工具调用存活的后台进程 + 日志 + 树杀」,
|
|
59
|
+
// 服务对象远不止前端联调(推理服务、watcher、log tail、任意长驻命令)。留在 browser-debug 时
|
|
60
|
+
// 受 MOCODE_FRONTEND_TOOLS_ENABLED 否决、且组描述只提 DOM/console,路由 LLM 对「起个服务」
|
|
61
|
+
// 类任务几乎不会选它 —— 模型于是退回 run_command 前台阻塞(120s 超时)或 start /b 脱离启动,
|
|
62
|
+
// 之后既拿不到日志也没法优雅 kill。故独立成组且**无 gateEnv**:与 shell-debug 同级、永远可路由。
|
|
63
|
+
'background-exec': {
|
|
64
|
+
tools: ['dev_server'],
|
|
65
|
+
description: 'Run any long-running background process that must outlive a single tool call — dev servers, inference/model services, watchers, log tails, message queues. Provides process id, incremental log reads, readiness wait, and process-tree termination.',
|
|
66
|
+
},
|
|
55
67
|
'browser-debug': {
|
|
56
|
-
tools: ['browser'
|
|
57
|
-
description: '
|
|
68
|
+
tools: ['browser'],
|
|
69
|
+
description: 'Debug web UIs through DOM, console, network, and page sessions in a real browser.',
|
|
58
70
|
gateEnv: 'MOCODE_FRONTEND_TOOLS_ENABLED',
|
|
59
71
|
},
|
|
60
72
|
'desktop-observe': {
|
|
@@ -89,6 +101,26 @@ export const TOOL_ROUTE_GROUPS = {
|
|
|
89
101
|
},
|
|
90
102
|
};
|
|
91
103
|
export const TOOL_ROUTE_GROUP_NAMES = Object.keys(TOOL_ROUTE_GROUPS);
|
|
104
|
+
/**
|
|
105
|
+
* 簇蕴含关系:选中 key 簇时自动带上 value 里的簇(仍受各自 gateEnv 否决)。
|
|
106
|
+
*
|
|
107
|
+
* 存在的理由是「能力半截」比「能力多余」更贵:browser-debug 只给浏览器,而被调试的页面
|
|
108
|
+
* 得先有人把它跑起来。弱模型只选 browser-debug 时,它要么白付一个 step 去 add_tool_groups
|
|
109
|
+
* 扩容 background-exec,要么退回 run_command 前台起服务(120s 超时被杀 / 拿不到日志)。
|
|
110
|
+
* 蕴含在 policy 层解析,router 的 Examples 仍要求显式列出两者(让强模型学会正确归因)。
|
|
111
|
+
*/
|
|
112
|
+
export const TOOL_ROUTE_IMPLICATIONS = {
|
|
113
|
+
'browser-debug': ['background-exec'],
|
|
114
|
+
};
|
|
115
|
+
/** 展开蕴含:传入簇集合 → 并上其蕴含簇。纯函数、幂等(蕴含不再递归展开第二层)。 */
|
|
116
|
+
export function expandRouteImplications(groups) {
|
|
117
|
+
const out = new Set(groups);
|
|
118
|
+
for (const group of [...out]) {
|
|
119
|
+
for (const implied of TOOL_ROUTE_IMPLICATIONS[group] ?? [])
|
|
120
|
+
out.add(implied);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
92
124
|
/**
|
|
93
125
|
* 常驻工具簇:每个 turn 无条件激活,不经过 LLM 路由(路由只需在「可用簇 − 常驻簇」里挑)。
|
|
94
126
|
* 入选标准:高频(coding agent 多数 turn 都要)+ 低暴露成本(工具少、schema 短)+ 漏判代价高
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const LEGACY_GREP_RE = /^(.*?):(\d+):(.*)$/;
|
|
2
2
|
const STRUCTURED_HEADER_RE = /^(.*): (\d+) 处匹配,行号 \[([0-9,\s]+)\]$/;
|
|
3
|
-
|
|
3
|
+
/** body 行:命中 ` L12:` / 上下文 ` L11-`(grep.ts renderBodies 的两种前缀)。 */
|
|
4
|
+
const STRUCTURED_BODY_RE = /^\s{2}L\d+[:-]/;
|
|
5
|
+
/** 不相邻分块分隔符 ` --`(对齐 ripgrep)。Cold 折叠时与 body 一并丢弃。 */
|
|
6
|
+
const STRUCTURED_SEPARATOR_RE = /^\s{2}--$/;
|
|
4
7
|
const STRUCTURED_FOLDED_RE = /^\s{2}\(body 已折叠/;
|
|
5
8
|
function isAgedCold(input) {
|
|
6
9
|
return input.phase === 'sweep' && input.isCold === true && (input.age ?? 0) >= 2;
|
|
@@ -18,7 +21,8 @@ function collapseStructuredGrep(output) {
|
|
|
18
21
|
out.push(line);
|
|
19
22
|
continue;
|
|
20
23
|
}
|
|
21
|
-
if (inFile &&
|
|
24
|
+
if (inFile &&
|
|
25
|
+
(STRUCTURED_BODY_RE.test(line) || STRUCTURED_SEPARATOR_RE.test(line) || STRUCTURED_FOLDED_RE.test(line))) {
|
|
22
26
|
continue;
|
|
23
27
|
}
|
|
24
28
|
inFile = false;
|
|
@@ -30,7 +30,7 @@ const RECOVERY_HINT = ' Call computer{screenshot} if you need the current screen
|
|
|
30
30
|
* computer-zoom.png —— 每次动作回灌的屏幕帧,会被反复收发,是二次增长的来源。
|
|
31
31
|
* - ❌ `browser`:`src/tools/builtins/browser.ts:235` 的附件名是 `${sessionId}.png`,**没有稳定前缀**,
|
|
32
32
|
* 按文档 §2.2 的约定移出白名单(收益小一档但不会误剪)。
|
|
33
|
-
* - ❌ `screenshot` / `
|
|
33
|
+
* - ❌ `screenshot` / `read_file`(文档图)/ 用户粘贴图:读的是"文档图"或用户意图的直接载体,剪掉是净损失。
|
|
34
34
|
*
|
|
35
35
|
* 新增屏幕帧产出点必须沿用 `computer-` 前缀才会自动进窗口;若前缀不同,请在白名单里显式登记
|
|
36
36
|
* 并同步更新 design-notes/vision-window.md。
|
|
@@ -90,7 +90,7 @@ function frameNames(message) {
|
|
|
90
90
|
* 1. `role === 'user'` 且 content 是数组;
|
|
91
91
|
* 2. 数组里至少有 1 个 `image_url` part;
|
|
92
92
|
* 3. 前言以工具附件前言开头(从而排除用户粘贴图),且**所有**帧名命中白名单
|
|
93
|
-
* (混进
|
|
93
|
+
* (混进 read_file 文档图 / screenshot / 用户图时不整条淘汰 —— 那些剪掉是净损失)。
|
|
94
94
|
*
|
|
95
95
|
* 已剪过的消息不再含 image_url part → 规则 2 自然失效 → 幂等。
|
|
96
96
|
* **不靠自定义字段打标**:消息对象会被 OpenAI SDK 原样发出,严格兼容后端可能 400。
|