mocode-ai 0.1.7 → 0.1.9
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 +22 -28
- package/dist/agent/core.js +16 -7
- package/dist/commands/config.js +1 -1
- package/dist/config/file.js +1 -1
- package/dist/config/index.js +48 -7
- package/dist/context/classifier.js +2 -1
- package/dist/context/encoders/_util.js +40 -0
- package/dist/context/encoders/code.js +77 -0
- package/dist/context/encoders/doc.js +28 -0
- package/dist/context/encoders/graph.js +31 -0
- package/dist/context/encoders/index.js +24 -2
- package/dist/context/encoders/log.js +52 -0
- package/dist/context/encoders/memory.js +63 -0
- package/dist/context/encoders/search.js +69 -0
- package/dist/context/encoders/summary.js +26 -0
- package/dist/context/encoders/table.js +64 -0
- package/dist/context/encoders/tree.js +86 -0
- package/dist/index.js +2 -1
- package/dist/llm/index.js +13 -1
- package/dist/repl/index.js +211 -14
- package/dist/session/compact.js +49 -18
- package/dist/session/drop.js +135 -0
- package/dist/session/index.js +1 -0
- package/dist/tools/builtins/ask-human.js +4 -4
- package/dist/tools/builtins/codegraph.js +3 -3
- package/dist/tools/builtins/drop-context.js +68 -0
- package/dist/tools/builtins/edit-file.js +1 -1
- package/dist/tools/builtins/glob.js +2 -2
- package/dist/tools/builtins/grep.js +2 -2
- package/dist/tools/builtins/index.js +2 -0
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-list.js +1 -1
- package/dist/tools/builtins/memory-save.js +1 -1
- package/dist/tools/builtins/memory-search.js +1 -1
- package/dist/tools/builtins/memory-update.js +1 -1
- package/dist/tools/builtins/read-file.js +2 -2
- package/dist/tools/builtins/run-command.js +1 -1
- package/dist/tools/builtins/switch-mode.js +3 -5
- package/dist/tools/builtins/task.js +4 -5
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/web-fetch.js +1 -1
- package/dist/tools/builtins/web-search.js +1 -1
- package/dist/tools/builtins/write-file.js +1 -1
- package/dist/tools/registry.js +6 -1
- package/dist/ui/layout.js +148 -50
- package/dist/ui/prompt.js +4 -4
- package/dist/ui/render.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,21 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
# mocode
|
|
4
4
|
|
|
5
|
-
一个终端编码 agent
|
|
5
|
+
一个终端编码 agent:你给一个目标,它**自主完成**——不需要你逐步指挥。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
mocode 自己探索代码、读写改文件、执行命令、联网查资料,以「思考 → 调用工具 → 观察结果 → 再思考」的循环一步步把任务推进到完成。接任意 OpenAI 兼容接口(GLM、DeepSeek、Qwen、本地 Ollama / vLLM 等),全屏 TUI 交互,流式输出、思考过程可见。
|
|
8
|
+
|
|
9
|
+
## 为什么用 mocode
|
|
10
|
+
|
|
11
|
+
mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
12
|
+
|
|
13
|
+
- **自主多步推进** — 一次对话里连续多步:读代码、改代码、跑测试、根据报错再改……agent 自己决定下一步,中途不用你反复催。遇到卡点会调 `ask_human` 弹面板问你(阻塞到回应)。
|
|
14
|
+
- **只读工具并行执行** — 一轮里连续的只读操作(读文件、grep、glob、codegraph、联网搜索/抓取)自动并发跑,总耗时 ≈ 最慢一个,而不是逐个排队。写文件 / 改文件这类有副作用的操作仍串行,保快照顺序与数据安全。
|
|
15
|
+
- **子 agent 分而治之** — 复杂任务可派生独立子 agent:各自有自己的对话历史(不污染主线),可限定只读工具集和步数上限,并行探查多片代码 / 多个方向,最后只把摘要回灌主线。主线据此决定下一步。
|
|
16
|
+
- **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式全量工具放开。agent 还能在两者间自切换——先把陌生代码库摸清,再动手改。
|
|
17
|
+
- **上下文自动压缩** — 接近窗口上限时三层压缩(单条结果裁剪 → 旧工具结果原地微压缩 → 旧对话摘要),长会话也不爆窗口;`/context` 实时显示 token 用量,`/compact` 可手动压缩(能带焦点指令聚焦保留)。
|
|
18
|
+
- **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
|
|
19
|
+
- **可中断、可回滚** — Ctrl+C 随时打断当前轮次(树杀子进程,历史还原到本轮开始前,不留残半的工具调用);`/rollback` 按轮次快照恢复文件改动,逐个文件「保留/撤销」,不依赖 git。
|
|
20
|
+
- **沙箱防护** — 文件读写经沙箱拦截,挡掉越界路径(`../../`、绝对外圈、软链出圈等),不碰工作目录之外的文件。
|
|
8
21
|
|
|
9
22
|
## 特性
|
|
10
23
|
|
|
11
|
-
-
|
|
24
|
+
- **流式输出 + 思考可见** — 回复边生成边显示;模型支持 reasoning 时思考过程实时可见,思考段自动折叠(不占屏),`/think N` 按需展开
|
|
12
25
|
- **全屏 TUI** — 备用屏(alt screen)+ 固定底栏状态行 + 滚动回看(PgUp/PgDn),运行中可打字(typeahead),下一轮自动预填
|
|
13
|
-
- **16 个内置工具** — 读 / 写 / 改文件、执行命令、glob 找路径、grep 搜内容、codegraph 代码索引、联网搜索、抓取网页、加载 skill、询问用户、5 个跨会话记忆工具(存/搜/列/改/遗忘)
|
|
14
26
|
- **会话持久化** — 每轮自动落盘,`--resume` / `/resume` 续接历史会话
|
|
15
|
-
-
|
|
16
|
-
- **上下文工程** — 接近窗口上限时自动三层压缩,`/context` 实时显示 token 用量条,`/compact` 手动压缩(可带焦点)
|
|
17
|
-
- **Skills 系统** — 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令
|
|
27
|
+
- **Skills 系统** — 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令(渐进式披露:先看简介,任务相关才加载正文)
|
|
18
28
|
- **斜杠命令** — `/exit` `/clear` `/context` `/skills` `/compact` `/resume` `/think` `/rollback`,输入时下拉过滤
|
|
19
|
-
- **Ctrl+C 中断** — 运行中随时中断当前 agent 轮次(不退进程)
|
|
20
29
|
|
|
21
30
|
## 安装
|
|
22
31
|
|
|
@@ -49,6 +58,8 @@ npm start
|
|
|
49
58
|
mocode config
|
|
50
59
|
```
|
|
51
60
|
|
|
61
|
+
也可直接 `mocode` 进入 REPL 后用 `/model` 命令配置(交互选后端预设 + 逐项填写,即时生效 + 持久化)。未配置时 REPL 仍能打开,会提示你跑 `/model`。
|
|
62
|
+
|
|
52
63
|
也可手写配置文件。mocode 按以下优先级加载(后者覆盖前者,仅回填未设置的环境变量;shell 里 `export` 的永远最优先):
|
|
53
64
|
|
|
54
65
|
1. `~/.mocode/config` — 全局(`mocode config` 写此文件)
|
|
@@ -120,6 +131,8 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
120
131
|
| `web_fetch` | 抓取指定 URL,HTML 清洗成纯文本 |
|
|
121
132
|
| `use_skill` | 加载某 skill 的完整 SKILL.md 指令 |
|
|
122
133
|
| `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
|
|
134
|
+
| `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
|
|
135
|
+
| `task` | 派生子 agent 执行独立子任务(独立历史、可受限工具集、可设步数上限);连续多个自动并行,只回摘要 |
|
|
123
136
|
| `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
|
|
124
137
|
| `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
|
|
125
138
|
| `memory_list` | 列记忆索引(id/标题/摘要,无正文) |
|
|
@@ -138,6 +151,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
138
151
|
| `/resume` | 续接已保存的会话 |
|
|
139
152
|
| `/think` | 展开折叠思考段(`/think N`) |
|
|
140
153
|
| `/rollback` | 菜单选轮次回滚(↑↓ · Enter) |
|
|
154
|
+
| `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
|
|
141
155
|
|
|
142
156
|
输入 `/` 触发下拉菜单,继续打字过滤;Esc 取消。
|
|
143
157
|
|
|
@@ -165,26 +179,6 @@ mocode 自动扫描以下目录的 skill(每个 skill 是 `<name>/SKILL.md`,带
|
|
|
165
179
|
|
|
166
180
|
skill 的 `description` 注入系统提示(渐进式披露第①层),模型只在任务相关时调 `use_skill` 加载完整正文(第②层)。用 `/skills` 查看已发现的 skill。
|
|
167
181
|
|
|
168
|
-
## 结构
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
src/
|
|
172
|
-
├── index.ts # 入口:装配 + 启动 REPL + --resume 续接
|
|
173
|
-
├── repl/index.ts # 全屏 TUI、斜杠命令、运行态交互、Ctrl+C 中断
|
|
174
|
-
├── agent/index.ts # tool-call 循环(调 LLM→执行工具→回灌→再调,≤200 步)
|
|
175
|
-
├── llm/index.ts # OpenAI 兼容客户端 + 工具格式转换 + chat() 流式
|
|
176
|
-
├── tools/ # types / constants / registry + builtins/(一工具一文件)
|
|
177
|
-
│ └── builtins/ # read-file write-file edit-file run-command glob grep
|
|
178
|
-
│ # codegraph web-search web-fetch use-skill ask-human memory-*
|
|
179
|
-
├── config/index.ts # 读配置、校验必填项、系统提示词
|
|
180
|
-
├── skills/ # discover(扫描)+ index(缓存/拼系统提示)
|
|
181
|
-
├── session/ # 落盘/续接 + compact(三层压缩)+ token 估算
|
|
182
|
-
├── memory/ # MOCODE.md 加载 + 跨会话记忆存储 + 后台反思
|
|
183
|
-
├── rollback/ # 轮次快照 + /rollback 菜单 + 文件撤销
|
|
184
|
-
├── ui/ # theme(颜色)render(横幅/摘要)layout(全屏布局)spinner prompt
|
|
185
|
-
└── agents/ commands/ mcp/ permissions/ # 未来子系统(agents/mcp/permissions 为空骨架)
|
|
186
|
-
```
|
|
187
|
-
|
|
188
182
|
## 类型检查
|
|
189
183
|
|
|
190
184
|
```bash
|
package/dist/agent/core.js
CHANGED
|
@@ -9,7 +9,8 @@ import { chat, planChatTools, } from '../llm/index.js';
|
|
|
9
9
|
import { executeTool } from '../tools/registry.js';
|
|
10
10
|
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
11
11
|
import { getAgentMode, setAgentMode } from './mode.js';
|
|
12
|
-
import { maybeCompact,
|
|
12
|
+
import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
|
|
13
|
+
import { optimizeToolResult } from '../context/index.js';
|
|
13
14
|
import { config } from '../config/index.js';
|
|
14
15
|
import { jailResolve } from '../sandbox/index.js';
|
|
15
16
|
/** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
|
|
@@ -67,12 +68,16 @@ function readDiffContext(tc, parsed) {
|
|
|
67
68
|
}
|
|
68
69
|
return { preWriteOld: null, editStartLine: 1 };
|
|
69
70
|
}
|
|
70
|
-
/** 回灌 tool 结果到 history
|
|
71
|
+
/** 回灌 tool 结果到 history:经 Context Optimization Pipeline 编码(tree/search/log/...)后裁到单条上限。
|
|
72
|
+
* tool_call_id 与 assistant.tool_calls 按序配对。未注册 encoder 时回落 capToolResultForHistory(零行为变化)。
|
|
73
|
+
* TUI 渲染(hooks.onToolResult)用原始 output,与此解耦——屏上看全量,LLM 看编码后紧凑版。 */
|
|
71
74
|
function pushToolResult(history, tc, output) {
|
|
72
75
|
history.push({
|
|
73
76
|
role: 'tool',
|
|
74
77
|
tool_call_id: tc.id,
|
|
75
|
-
|
|
78
|
+
// optimizeToolResult:classifier 选 encoder → encode(保不变量压缩)→ capToolResultForHistory 兜底。
|
|
79
|
+
// tc.arguments 透传给 encoder(上下文感知编码,如 read_file 的 offset/limit)。永不抛错。
|
|
80
|
+
content: optimizeToolResult(tc.name, output, tc.arguments),
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
/**
|
|
@@ -80,7 +85,7 @@ function pushToolResult(history, tc, output) {
|
|
|
80
85
|
* 流式调 LLM(经 hooks.onText 实时渲染)→ 有 tool_calls 就分组执行并回灌
|
|
81
86
|
* → 否则流式正文即最终回复。history 在调用间持久,由调用方持有。
|
|
82
87
|
* 步前经 session/maybeCompact 自动压缩(接近窗口上限时三层压缩);
|
|
83
|
-
* 工具结果进 history 前经
|
|
88
|
+
* 工具结果进 history 前经 Context Optimization Pipeline(optimizeToolResult:类型化编码 + 长度裁剪)。
|
|
84
89
|
*
|
|
85
90
|
* 中断语义:signal 经 executeTool(name, args, signal) 串进工具;run_command/web_fetch 等 abort 即时杀
|
|
86
91
|
* (树杀子进程 / 取消 fetch),循环顶 if(signal.aborted) 兜底还原。不会留下未配对的 tool_call_id。
|
|
@@ -102,6 +107,10 @@ export async function runAgentCore(opts) {
|
|
|
102
107
|
const t0 = Date.now();
|
|
103
108
|
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
104
109
|
history.push({ role: 'user', content: userInput });
|
|
110
|
+
// drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
|
|
111
|
+
// 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
|
|
112
|
+
// 子 agent 也在自己的 history 上操作(子 agent 独立 history);skipRollback 不影响此行为。
|
|
113
|
+
const dropContext = (filter) => dropContextFromHistory(history, filter);
|
|
105
114
|
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
106
115
|
let mode = 'idle';
|
|
107
116
|
let gotText = false;
|
|
@@ -197,7 +206,7 @@ export async function runAgentCore(opts) {
|
|
|
197
206
|
while (j < calls.length && READ_TOOL_NAMES.has(calls[j].name))
|
|
198
207
|
j++;
|
|
199
208
|
const batch = calls.slice(i, j);
|
|
200
|
-
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
209
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
|
|
201
210
|
for (let k = 0; k < batch.length; k++) {
|
|
202
211
|
const tc = batch[k];
|
|
203
212
|
hooks.onToolHeader?.(tc);
|
|
@@ -233,7 +242,7 @@ export async function runAgentCore(opts) {
|
|
|
233
242
|
while (j < calls.length && calls[j].name === 'task')
|
|
234
243
|
j++;
|
|
235
244
|
const batch = calls.slice(i, j);
|
|
236
|
-
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
245
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
|
|
237
246
|
// 先批量打印所有头 + 启 spinner(多 task 并发,spinner 只显一个,但 ● 头都打出来)
|
|
238
247
|
for (const tc of batch) {
|
|
239
248
|
hooks.onToolHeader?.(tc);
|
|
@@ -268,7 +277,7 @@ export async function runAgentCore(opts) {
|
|
|
268
277
|
: null;
|
|
269
278
|
const { preWriteOld, editStartLine } = readDiffContext(tc, parsed);
|
|
270
279
|
hooks.onToolStart?.(tc.name);
|
|
271
|
-
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback });
|
|
280
|
+
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext });
|
|
272
281
|
hooks.onToolDone?.();
|
|
273
282
|
hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
|
|
274
283
|
pushToolResult(history, tc, output);
|
package/dist/commands/config.js
CHANGED
|
@@ -9,7 +9,7 @@ function ask(rl, q) {
|
|
|
9
9
|
* 首跑配置向导:交互填 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL,写 ~/.mocode/config。
|
|
10
10
|
* 只交互三键、保留文件里其它键(MAX_TOKENS / CONTEXT_WINDOW_TOKENS / MOCODE_THEME 等)。
|
|
11
11
|
* prompt 全纯文本(readline 光标按字符算,不能含 ANSI)。
|
|
12
|
-
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(
|
|
12
|
+
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(避免触发 config 单例初始化 / loadEnvFiles,
|
|
13
13
|
* 直接退出);只 import config/file.ts(纯 I/O 叶子,无 env 校验 / process.exit)。
|
|
14
14
|
*/
|
|
15
15
|
export async function runConfigWizard() {
|
package/dist/config/file.js
CHANGED
|
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
/**
|
|
6
6
|
* ~/.mocode/config 全局 dotenv 配置的读写叶子(纯 node:fs / node:os / node:path + dotenv)。
|
|
7
7
|
*
|
|
8
|
-
* 独立于 `config/index.ts`——**不**触发
|
|
8
|
+
* 独立于 `config/index.ts`——**不**触发 config 单例初始化 / loadEnvFiles,故 `commands/config.ts`
|
|
9
9
|
* (首跑向导,刻意不 import config/index.ts)与 `repl/index.ts`(`/theme` 持久化)都能安全共享。
|
|
10
10
|
* 主题等 UI 偏好走 `updateConfigKey` 单键写;向导多键一次性写走 `writeConfigKeys`。
|
|
11
11
|
*/
|
package/dist/config/index.js
CHANGED
|
@@ -30,14 +30,26 @@ function loadEnvFiles() {
|
|
|
30
30
|
}
|
|
31
31
|
// 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
|
|
32
32
|
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
33
|
+
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
34
|
+
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
35
|
+
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
36
|
+
const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
|
|
37
|
+
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
33
38
|
loadEnvFiles();
|
|
39
|
+
/**
|
|
40
|
+
* 取环境变量;缺则返回空字符串(不退出)。
|
|
41
|
+
* 历史上缺 LLM_BASE_URL/LLM_API_KEY 会 process.exit(1),但 /model 命令已能在 REPL 内配置模型,
|
|
42
|
+
* 故首次未配置也应让 REPL 起来,由开场提示引导用户跑 /model。发消息时 chat() 会抛错被 runTurn catch,不崩。
|
|
43
|
+
*/
|
|
34
44
|
function requireEnv(key) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
return process.env[key] || '';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 模型是否已配置(baseURL + apiKey 非空)。REPL 开场据此决定是否提示 /model。
|
|
49
|
+
* 未配置时 config.model 仍回退 'gpt-4o-mini',但发消息会因 baseURL/apiKey 空而失败——由 runTurn catch 友好提示。
|
|
50
|
+
*/
|
|
51
|
+
export function isModelConfigured() {
|
|
52
|
+
return !!config.baseURL && !!config.apiKey;
|
|
41
53
|
}
|
|
42
54
|
const PLATFORM_NOTE = (() => {
|
|
43
55
|
if (process.platform === 'win32') {
|
|
@@ -76,6 +88,7 @@ ${PLATFORM_NOTE}
|
|
|
76
88
|
- Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
|
|
77
89
|
- Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
|
|
78
90
|
- Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) — list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
|
|
91
|
+
- **Drop irrelevant context** (use sparingly): call drop_context to stub-replace tool results in history that are BOTH (a) irrelevant to the current task AND (b) large (the freed tokens must clearly exceed the ~300 tokens the call itself costs — roughly only worth it when targeting ≥2 bulky results, e.g. wide grep/read sweeps that returned mostly-irrelevant hits). The call itself adds a tool-call round-trip, so don't call it for one small result or when you're near done. It preserves tool_call_id pairing (only content changes); the system prompt and current turn are never dropped. Use filters (toolNames / contains) to target precisely.
|
|
79
92
|
|
|
80
93
|
## Failure Handling
|
|
81
94
|
- Tools return errors as strings (edit_file no match or non-unique, run_command non-zero exit, etc.). Analyze the root cause, adjust, then retry — don't resend the same call verbatim.
|
|
@@ -111,7 +124,7 @@ You are in PLAN mode: investigate and design only — do NOT execute or change a
|
|
|
111
124
|
- Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Prefer codegraph when a .codegraph/ index exists.
|
|
112
125
|
- Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
|
|
113
126
|
- Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
|
|
114
|
-
- If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP
|
|
127
|
+
- If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
|
|
115
128
|
export const config = {
|
|
116
129
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
117
130
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
@@ -122,6 +135,7 @@ export const config = {
|
|
|
122
135
|
compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
|
|
123
136
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
124
137
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
138
|
+
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
|
|
125
139
|
autoReflect: process.env.AUTO_REFLECT !== 'false',
|
|
126
140
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
127
141
|
maxSteps: Number(process.env.MAX_STEPS) || 200,
|
|
@@ -132,4 +146,31 @@ export const config = {
|
|
|
132
146
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
133
147
|
theme: process.env.MOCODE_THEME || 'default',
|
|
134
148
|
themeFromShell,
|
|
149
|
+
llmKeysFromShell,
|
|
135
150
|
};
|
|
151
|
+
/**
|
|
152
|
+
* 运行时更新模型相关配置(/model 命令调)。
|
|
153
|
+
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
154
|
+
* - 同步 process.env(保持内存一致:其他读 process.env 的路径也拿到新值;且使新值在下次启动的
|
|
155
|
+
* loadEnvFiles 中被视为"已设",不被文件回填覆盖——即"优先拿这里的")。
|
|
156
|
+
* 持久化(写 ~/.mocode/config)由调用方走 writeConfigKeys,此处只管内存 + env。
|
|
157
|
+
* 重建 OpenAI 客户端(baseURL/apiKey 是构造时固化的实例字段)由调用方走 reconfigureClient。
|
|
158
|
+
*/
|
|
159
|
+
export function updateModelConfig(opts) {
|
|
160
|
+
if (opts.model !== undefined) {
|
|
161
|
+
config.model = opts.model;
|
|
162
|
+
process.env.LLM_MODEL = opts.model;
|
|
163
|
+
}
|
|
164
|
+
if (opts.baseURL !== undefined) {
|
|
165
|
+
config.baseURL = opts.baseURL;
|
|
166
|
+
process.env.LLM_BASE_URL = opts.baseURL;
|
|
167
|
+
}
|
|
168
|
+
if (opts.apiKey !== undefined) {
|
|
169
|
+
config.apiKey = opts.apiKey;
|
|
170
|
+
process.env.LLM_API_KEY = opts.apiKey;
|
|
171
|
+
}
|
|
172
|
+
if (opts.contextWindowTokens !== undefined) {
|
|
173
|
+
config.contextWindowTokens = opts.contextWindowTokens;
|
|
174
|
+
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
|
|
9
9
|
// 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
|
|
10
|
-
/** 工具名 → ContextKind 的强先验表(覆盖全部
|
|
10
|
+
/** 工具名 → ContextKind 的强先验表(覆盖全部 18 内置工具)。 */
|
|
11
11
|
const BY_NAME = {
|
|
12
12
|
// tree:路径列表 → 缩进树
|
|
13
13
|
glob: 'tree',
|
|
@@ -32,6 +32,7 @@ const BY_NAME = {
|
|
|
32
32
|
write_file: 'status',
|
|
33
33
|
ask_human: 'status',
|
|
34
34
|
switch_mode: 'status',
|
|
35
|
+
drop_context: 'status',
|
|
35
36
|
memory_save: 'status',
|
|
36
37
|
memory_update: 'status',
|
|
37
38
|
memory_forget: 'status',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// encoder 共享小工具(非 encoder,纯函数)。供 graph/doc/summary 等复用。
|
|
2
|
+
// log/code 各有专属逻辑(log 用重复行折叠;code 用前缀感知空行检测),不在此。
|
|
3
|
+
/** 去 ANSI CSI 序列(颜色 / 光标 / 清屏等)。语义无损:颜色码不含信息。 */
|
|
4
|
+
export function stripAnsi(s) {
|
|
5
|
+
return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 折叠连续空行(只含空白字符的行)≥ threshold → 单个空行。
|
|
9
|
+
* 用于 prose / source dump(web_fetch / use_skill / task / codegraph):HTML→文本与 markdown 常留多余空行,
|
|
10
|
+
* 多空行与单空行语义等价,折叠无损。默认 threshold=3(只动真正过量的空行,常见 ≤2 空行不动)。
|
|
11
|
+
*
|
|
12
|
+
* 注意:用 `.trim() === ''` 判空——故 read_file 的 ` 2\t`(行号前缀 + tab,trim 后剩 `2`)不会被
|
|
13
|
+
* 视作空行。read_file 的空行折叠见 code encoder(前缀感知)。
|
|
14
|
+
*/
|
|
15
|
+
export function collapseBlankRuns(text, threshold = 3) {
|
|
16
|
+
const lines = text.split('\n');
|
|
17
|
+
const out = [];
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < lines.length) {
|
|
20
|
+
if (lines[i].trim() === '') {
|
|
21
|
+
let j = i;
|
|
22
|
+
while (j < lines.length && lines[j].trim() === '')
|
|
23
|
+
j++;
|
|
24
|
+
const run = j - i;
|
|
25
|
+
if (run >= threshold) {
|
|
26
|
+
out.push(''); // 单个空行
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
for (let k = 0; k < run; k++)
|
|
30
|
+
out.push(lines[i]);
|
|
31
|
+
}
|
|
32
|
+
i = j;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
out.push(lines[i]);
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out.join('\n');
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Encoder(read_file):折叠连续空行(≥3 → 1,保留首空行行号前缀)。
|
|
3
|
+
*
|
|
4
|
+
* 输入:read_file 返回的 ` N\t<content>` 行(6 宽右对齐行号 + tab + 内容),可能带尾部
|
|
5
|
+
* `... (N 行未显示,共 M 行)`。
|
|
6
|
+
* 输出:连续 ≥3 个空行(行号前缀 + 空 content)折叠为 1 个(保留首行前缀,丢后续空行);其余逐字保留。
|
|
7
|
+
*
|
|
8
|
+
* 不变量(离线脚本断言):
|
|
9
|
+
* - 所有非空 content 行逐字保留(前缀 + 内容不变)→ edit_file 的 old_string 按内容匹配仍可用。
|
|
10
|
+
* - content 行的行号前缀不变 → LLM 看到的行号与文件一致。
|
|
11
|
+
* - 仅空行(前缀 + tab + 空 content)被折叠,且仅 ≥3 连续时;≤2 空行原样(常见,不动)。
|
|
12
|
+
* - 尾部标记 `... (...)` 不匹配行号前缀,原样保留。
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ 残余风险(可接受、可恢复):若 LLM 编辑一个含 ≥3 连续空行的区域,它看到的空行数比文件实际少,
|
|
15
|
+
* old_string 的空行数可能不匹配 → edit_file 返"未找到" → LLM 重读重试(系统提示已要求编辑前 read_file)。
|
|
16
|
+
* 真实代码极少 ≥3 连续空行(linter 通常强制 ≤2),故实际几乎不触发。出问题可设
|
|
17
|
+
* MOCODE_CONTEXT_OPTIMIZE=false 全局回退,或调高阈值。
|
|
18
|
+
*
|
|
19
|
+
* 不做长度裁剪(cap 兜底);不动行号前缀;不删 content 行;不改内容(含尾随空白)。
|
|
20
|
+
* 空行判定用前缀感知(前缀 + tab + 空 content),不能用 trim——read_file 空行 ` 2\t` trim 后剩 `2`。
|
|
21
|
+
*/
|
|
22
|
+
const LINE_RE = /^(\s*\d+)\t(.*)$/;
|
|
23
|
+
function isBlankCodeLine(l) {
|
|
24
|
+
const m = LINE_RE.exec(l);
|
|
25
|
+
return m ? m[2] === '' : false;
|
|
26
|
+
}
|
|
27
|
+
export const codeEncoder = {
|
|
28
|
+
kind: 'code',
|
|
29
|
+
encode({ output }) {
|
|
30
|
+
const lines = output.split('\n');
|
|
31
|
+
const out = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
let collapsedRuns = 0;
|
|
34
|
+
while (i < lines.length) {
|
|
35
|
+
if (isBlankCodeLine(lines[i])) {
|
|
36
|
+
let j = i;
|
|
37
|
+
while (j < lines.length && isBlankCodeLine(lines[j]))
|
|
38
|
+
j++;
|
|
39
|
+
const run = j - i;
|
|
40
|
+
if (run >= 3) {
|
|
41
|
+
out.push(lines[i]); // 保留首空行(含其行号前缀)
|
|
42
|
+
collapsedRuns++;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
for (let k = 0; k < run; k++)
|
|
46
|
+
out.push(lines[i]);
|
|
47
|
+
}
|
|
48
|
+
i = j;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
out.push(lines[i]);
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (collapsedRuns === 0) {
|
|
56
|
+
return {
|
|
57
|
+
text: output,
|
|
58
|
+
meta: {
|
|
59
|
+
kind: 'code',
|
|
60
|
+
originalLen: output.length,
|
|
61
|
+
encodedLen: output.length,
|
|
62
|
+
note: 'no ≥3 blank runs → passthrough',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const text = out.join('\n');
|
|
67
|
+
return {
|
|
68
|
+
text,
|
|
69
|
+
meta: {
|
|
70
|
+
kind: 'code',
|
|
71
|
+
originalLen: output.length,
|
|
72
|
+
encodedLen: text.length,
|
|
73
|
+
note: `collapsed ${collapsedRuns} blank runs (≥3 → 1)`,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { collapseBlankRuns } from './_util.js';
|
|
2
|
+
/**
|
|
3
|
+
* Doc Encoder(web_fetch / use_skill):折叠连续空行(≥3 → 1)。
|
|
4
|
+
*
|
|
5
|
+
* 输入:
|
|
6
|
+
* - web_fetch 的 `URL (HTTP ...)\n\n<body>`(HTML→文本,常留多余空行)。
|
|
7
|
+
* - use_skill 的 `# Skill: name\n\n<body>`(SKILL.md 正文,frontmatter 已剥离)。
|
|
8
|
+
* 输出:空行折叠;正文 / 指令逐字保留。
|
|
9
|
+
*
|
|
10
|
+
* 不变量:URL 前缀行 / Skill 标题行保留;正文文本逐字保留(仅折叠空行——markdown / HTML 多空行与
|
|
11
|
+
* 单空行语义等价,无损)。use_skill 走放宽 cap(MAX_SKILL_RESULT)保指令完整——本 encoder 在 cap 前先
|
|
12
|
+
* 折叠空行,减少被 cap 截断的风险。
|
|
13
|
+
*/
|
|
14
|
+
export const docEncoder = {
|
|
15
|
+
kind: 'doc',
|
|
16
|
+
encode({ output }) {
|
|
17
|
+
const text = collapseBlankRuns(output, 3);
|
|
18
|
+
return {
|
|
19
|
+
text,
|
|
20
|
+
meta: {
|
|
21
|
+
kind: 'doc',
|
|
22
|
+
originalLen: output.length,
|
|
23
|
+
encodedLen: text.length,
|
|
24
|
+
note: text !== output ? 'blank runs collapsed' : 'no change',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { stripAnsi, collapseBlankRuns } from './_util.js';
|
|
2
|
+
/**
|
|
3
|
+
* Graph Encoder(codegraph):去 ANSI + 折叠连续空行(≥3 → 1)。
|
|
4
|
+
*
|
|
5
|
+
* 输入:codegraph CLI 返回的 `[退出码 N]\n<源码 + 调用路径>`(可能含 ANSI 颜色、多余空行)。
|
|
6
|
+
* 输出:去 ANSI + 空行折叠;结构(调用路径块、源码段)不动。
|
|
7
|
+
*
|
|
8
|
+
* 不变量:退出码行保留;源码与调用路径文本逐字保留(仅去颜色码 + 折叠空行);不删内容行。
|
|
9
|
+
* 保守:不重构调用路径 / 不去重源码(codegraph CLI 输出格式未稳定,需先采样真实输出定不变量,留后续)。
|
|
10
|
+
*/
|
|
11
|
+
export const graphEncoder = {
|
|
12
|
+
kind: 'graph',
|
|
13
|
+
encode({ output }) {
|
|
14
|
+
const stripped = stripAnsi(output);
|
|
15
|
+
const text = collapseBlankRuns(stripped, 3);
|
|
16
|
+
const hadAnsi = /\x1b/.test(output);
|
|
17
|
+
const hadBlanks = text !== stripped;
|
|
18
|
+
const note = [hadAnsi ? 'ANSI stripped' : '', hadBlanks ? 'blank runs collapsed' : '']
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(', ') || 'no change';
|
|
21
|
+
return {
|
|
22
|
+
text,
|
|
23
|
+
meta: {
|
|
24
|
+
kind: 'graph',
|
|
25
|
+
originalLen: output.length,
|
|
26
|
+
encodedLen: text.length,
|
|
27
|
+
note,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
// 内置 encoder 清单。启动期 pipeline 首次调用时经 registerAll 注册到 registry。
|
|
2
2
|
//
|
|
3
|
-
// Phase 1
|
|
4
|
-
// Phase 2
|
|
3
|
+
// Phase 1:passthrough(identity 兜底)→ 全链路零行为变化。
|
|
4
|
+
// Phase 2:tree / search / log / table / memory(高价值低风险)。
|
|
5
|
+
// Phase 2.5:code / graph / doc / summary(覆盖剩余有 encoder 的 kind)。
|
|
6
|
+
// - code(read_file):仅折叠 ≥3 连续空行,行号保真(edit_file 依赖)。
|
|
7
|
+
// - graph(codegraph)/ doc(web_fetch, use_skill)/ summary(task):去 ANSI + 折叠空行,保守不重构结构。
|
|
8
|
+
// - status(edit/write/ask_human/switch_mode/mem 增删改)无 encoder:本就是一行,无需编码。
|
|
5
9
|
//
|
|
6
10
|
// 加 encoder:新建 encoders/xxx.ts 导出 ContextEncoder,在此数组加一行。无需动 agent / llm / core。
|
|
7
11
|
import { passthroughEncoder } from './passthrough.js';
|
|
12
|
+
import { treeEncoder } from './tree.js';
|
|
13
|
+
import { searchEncoder } from './search.js';
|
|
14
|
+
import { logEncoder } from './log.js';
|
|
15
|
+
import { tableEncoder } from './table.js';
|
|
16
|
+
import { memoryEncoder } from './memory.js';
|
|
17
|
+
import { codeEncoder } from './code.js';
|
|
18
|
+
import { graphEncoder } from './graph.js';
|
|
19
|
+
import { docEncoder } from './doc.js';
|
|
20
|
+
import { summaryEncoder } from './summary.js';
|
|
8
21
|
export const builtinEncoders = [
|
|
9
22
|
passthroughEncoder,
|
|
23
|
+
treeEncoder,
|
|
24
|
+
searchEncoder,
|
|
25
|
+
logEncoder,
|
|
26
|
+
tableEncoder,
|
|
27
|
+
memoryEncoder,
|
|
28
|
+
codeEncoder,
|
|
29
|
+
graphEncoder,
|
|
30
|
+
docEncoder,
|
|
31
|
+
summaryEncoder,
|
|
10
32
|
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Encoder(run_command):去 ANSI 颜色码 + 折叠连续重复行(≥3 → 单行 + [×N])。
|
|
3
|
+
*
|
|
4
|
+
* 输入:run_command 返回的 `[退出码 N]\n<合并 stdout+stderr>`,可能含 ANSI(tsc --pretty / 测试框架)、
|
|
5
|
+
* 重复行(构建 / 编译日志)、尾部 `...(输出已截断)`、`[已中断]` / `[超时,已终止]` 前缀。
|
|
6
|
+
* 输出:去 ANSI CSI 序列 + 连续重复行折叠;行顺序不变(退出码头恒在首、错误行与尾部原位保留)。
|
|
7
|
+
*
|
|
8
|
+
* 不变量(离线脚本断言):退出码行 `[退出码 N]` / `[已中断]` / `[超时,已终止]` 保留;
|
|
9
|
+
* 重复行以 `[×N]` 标注计数(语义不丢——LLM 仍知该行重复 N 次);ANSI 去除语义无损(颜色码不含信息)。
|
|
10
|
+
* 不做长度裁剪(由 pipeline 末尾 capToolResultForHistory 兜底 head+标记+tail,与改造前一致)。
|
|
11
|
+
* run ≤2 原样保留(常见输出不必标注,避免噪音);run ≥3 才折叠(真正的大规模重复才省)。
|
|
12
|
+
*/
|
|
13
|
+
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
14
|
+
export const logEncoder = {
|
|
15
|
+
kind: 'log',
|
|
16
|
+
encode({ output }) {
|
|
17
|
+
// 去 ANSI CSI 序列(颜色 / 光标 / 清屏等),保留所有可见文本。
|
|
18
|
+
const stripped = output.replace(ANSI_RE, '');
|
|
19
|
+
const lines = stripped.split('\n');
|
|
20
|
+
// 折叠连续重复行:run ≥3 → 单行 + [×N];run ≤2 原样。
|
|
21
|
+
const out = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
let collapsedRuns = 0;
|
|
24
|
+
while (i < lines.length) {
|
|
25
|
+
let j = i;
|
|
26
|
+
while (j < lines.length && lines[j] === lines[i])
|
|
27
|
+
j++;
|
|
28
|
+
const run = j - i;
|
|
29
|
+
if (run >= 3) {
|
|
30
|
+
out.push(`${lines[i]} [×${run}]`);
|
|
31
|
+
collapsedRuns++;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (let k = 0; k < run; k++)
|
|
35
|
+
out.push(lines[i]);
|
|
36
|
+
}
|
|
37
|
+
i = j;
|
|
38
|
+
}
|
|
39
|
+
const text = out.join('\n');
|
|
40
|
+
return {
|
|
41
|
+
text,
|
|
42
|
+
meta: {
|
|
43
|
+
kind: 'log',
|
|
44
|
+
originalLen: output.length,
|
|
45
|
+
encodedLen: text.length,
|
|
46
|
+
note: collapsedRuns > 0
|
|
47
|
+
? `ANSI stripped, ${collapsedRuns} dup runs collapsed`
|
|
48
|
+
: 'ANSI stripped',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Encoder(memory_search):去掉 `recalled N` 召回计数噪音(遗忘系统内部用,非 LLM 推理所需)。
|
|
3
|
+
*
|
|
4
|
+
* 输入:memory_search 返回的条目卡,每张:`# [id] name (type, recalled N)\nsummary: ...\n\nbody`,
|
|
5
|
+
* 多张以 `\n\n---\n\n` 分隔。
|
|
6
|
+
* 输出:头部 `# [id] name (type)`(丢 `, recalled N`);summary/body 原样。
|
|
7
|
+
*
|
|
8
|
+
* 不变量(离线脚本断言):id/name/type/summary/body 全保留;仅 `recalled N` 被省略。
|
|
9
|
+
* 正则锚定整行 `# [id] ... (type, recalled N)`,group1 贪婪捕获到 `(\w+` 为止,name 含 ` (` 也不影响。
|
|
10
|
+
* 不匹配的行原样返回(防误伤 body 正文里的 `#` 标题等)。
|
|
11
|
+
*/
|
|
12
|
+
const HEADER_RE = /^(# \[[^\]]+\] .+ \(\w+), recalled \d+\)$/;
|
|
13
|
+
export const memoryEncoder = {
|
|
14
|
+
kind: 'memory',
|
|
15
|
+
encode({ output }) {
|
|
16
|
+
if (output.startsWith('(无匹配记忆')) {
|
|
17
|
+
return {
|
|
18
|
+
text: output,
|
|
19
|
+
meta: {
|
|
20
|
+
kind: 'memory',
|
|
21
|
+
originalLen: output.length,
|
|
22
|
+
encodedLen: output.length,
|
|
23
|
+
note: 'no matches → passthrough',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const lines = output.split('\n');
|
|
28
|
+
const out = [];
|
|
29
|
+
let changed = 0;
|
|
30
|
+
for (const l of lines) {
|
|
31
|
+
const m = HEADER_RE.exec(l);
|
|
32
|
+
if (m) {
|
|
33
|
+
// m[1] = "# [id] name (type";补 ")" 收尾,丢 ", recalled N"
|
|
34
|
+
out.push(`${m[1]})`);
|
|
35
|
+
changed++;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
out.push(l);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (changed === 0) {
|
|
42
|
+
return {
|
|
43
|
+
text: output,
|
|
44
|
+
meta: {
|
|
45
|
+
kind: 'memory',
|
|
46
|
+
originalLen: output.length,
|
|
47
|
+
encodedLen: output.length,
|
|
48
|
+
note: 'no recalled headers → passthrough',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const text = out.join('\n');
|
|
53
|
+
return {
|
|
54
|
+
text,
|
|
55
|
+
meta: {
|
|
56
|
+
kind: 'memory',
|
|
57
|
+
originalLen: output.length,
|
|
58
|
+
encodedLen: text.length,
|
|
59
|
+
note: `dropped recalled count from ${changed} entries`,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|