mocode-ai 1.3.9 → 1.4.0
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 +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
package/dist/mcp/client.js
CHANGED
|
@@ -28,12 +28,23 @@ class StdioTransport extends McpTransport {
|
|
|
28
28
|
});
|
|
29
29
|
this.child = child;
|
|
30
30
|
child.stdout?.on('data', (chunk) => this.onData(chunk));
|
|
31
|
-
child.stderr?.on('data', () => {
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
child.stderr?.on('data', () => {
|
|
32
|
+
/* MCP stderr 是诊断输出,不混入协议流。 */
|
|
33
|
+
});
|
|
34
|
+
child.on('exit', (code, signal) => {
|
|
35
|
+
this.child = null;
|
|
36
|
+
if (code !== 0 && signal == null)
|
|
37
|
+
this.receive({ error: { message: `stdio server 已退出 (${code})` } });
|
|
38
|
+
});
|
|
34
39
|
await new Promise((resolve, reject) => {
|
|
35
|
-
const onSpawn = () => {
|
|
36
|
-
|
|
40
|
+
const onSpawn = () => {
|
|
41
|
+
child.off('error', onError);
|
|
42
|
+
resolve();
|
|
43
|
+
};
|
|
44
|
+
const onError = (error) => {
|
|
45
|
+
child.off('spawn', onSpawn);
|
|
46
|
+
reject(error);
|
|
47
|
+
};
|
|
37
48
|
child.once('spawn', onSpawn);
|
|
38
49
|
child.once('error', onError);
|
|
39
50
|
});
|
|
@@ -43,7 +54,7 @@ class StdioTransport extends McpTransport {
|
|
|
43
54
|
throw new Error('MCP stdio server 未连接');
|
|
44
55
|
const body = `${JSON.stringify(message)}\n`;
|
|
45
56
|
// MCP stdio 传输使用 NDJSON;接收端仍兼容 Content-Length,方便接入历史 LSP 风格服务。
|
|
46
|
-
await new Promise((resolve, reject) => this.child.stdin.write(body, (error) => error ? reject(error) : resolve()));
|
|
57
|
+
await new Promise((resolve, reject) => this.child.stdin.write(body, (error) => (error ? reject(error) : resolve())));
|
|
47
58
|
}
|
|
48
59
|
async close() {
|
|
49
60
|
const child = this.child;
|
|
@@ -83,7 +94,9 @@ class StdioTransport extends McpTransport {
|
|
|
83
94
|
try {
|
|
84
95
|
this.receive(JSON.parse(raw));
|
|
85
96
|
}
|
|
86
|
-
catch {
|
|
97
|
+
catch {
|
|
98
|
+
/* 忽略 server 的非协议 stdout,避免打断会话。 */
|
|
99
|
+
}
|
|
87
100
|
}
|
|
88
101
|
}
|
|
89
102
|
class StreamableHttpTransport extends McpTransport {
|
|
@@ -93,7 +106,9 @@ class StreamableHttpTransport extends McpTransport {
|
|
|
93
106
|
super(receive);
|
|
94
107
|
this.spec = spec;
|
|
95
108
|
}
|
|
96
|
-
async start() {
|
|
109
|
+
async start() {
|
|
110
|
+
/* 每个 JSON-RPC 请求自行 POST,无长连接需预热。 */
|
|
111
|
+
}
|
|
97
112
|
async send(message) {
|
|
98
113
|
const controller = new AbortController();
|
|
99
114
|
const timer = setTimeout(() => controller.abort(), timeoutFor(this.spec));
|
|
@@ -106,7 +121,11 @@ class StreamableHttpTransport extends McpTransport {
|
|
|
106
121
|
if (this.sessionId)
|
|
107
122
|
headers['mcp-session-id'] = this.sessionId;
|
|
108
123
|
const response = await fetch(this.spec.url, {
|
|
109
|
-
method: 'POST',
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers,
|
|
126
|
+
body: JSON.stringify(message),
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
redirect: 'error',
|
|
110
129
|
});
|
|
111
130
|
this.captureSession(response);
|
|
112
131
|
if (!response.ok)
|
|
@@ -125,12 +144,20 @@ class StreamableHttpTransport extends McpTransport {
|
|
|
125
144
|
clearTimeout(timer);
|
|
126
145
|
}
|
|
127
146
|
}
|
|
128
|
-
async close() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
async close() {
|
|
148
|
+
this.sessionId = null;
|
|
149
|
+
}
|
|
150
|
+
captureSession(response) {
|
|
151
|
+
this.sessionId = response.headers.get('mcp-session-id') ?? this.sessionId;
|
|
152
|
+
}
|
|
153
|
+
receiveEvent(event) {
|
|
154
|
+
try {
|
|
155
|
+
this.receive(JSON.parse(event.data));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* 非 JSON SSE event 无法作为 MCP 响应。 */
|
|
159
|
+
}
|
|
132
160
|
}
|
|
133
|
-
catch { /* 非 JSON SSE event 无法作为 MCP 响应。 */ } }
|
|
134
161
|
}
|
|
135
162
|
class SseTransport extends McpTransport {
|
|
136
163
|
spec;
|
|
@@ -147,7 +174,10 @@ class SseTransport extends McpTransport {
|
|
|
147
174
|
if (this.connecting)
|
|
148
175
|
return this.connecting;
|
|
149
176
|
this.controller = new AbortController();
|
|
150
|
-
this.connecting = new Promise((resolve, reject) => {
|
|
177
|
+
this.connecting = new Promise((resolve, reject) => {
|
|
178
|
+
this.resolveEndpoint = resolve;
|
|
179
|
+
this.rejectEndpoint = reject;
|
|
180
|
+
});
|
|
151
181
|
void this.open();
|
|
152
182
|
return this.connecting;
|
|
153
183
|
}
|
|
@@ -161,7 +191,9 @@ class SseTransport extends McpTransport {
|
|
|
161
191
|
const response = await fetch(this.endpoint, {
|
|
162
192
|
method: 'POST',
|
|
163
193
|
headers: { 'content-type': 'application/json', ...this.spec.headers },
|
|
164
|
-
body: JSON.stringify(message),
|
|
194
|
+
body: JSON.stringify(message),
|
|
195
|
+
signal: controller.signal,
|
|
196
|
+
redirect: 'error',
|
|
165
197
|
});
|
|
166
198
|
if (!response.ok)
|
|
167
199
|
throw new Error(`HTTP ${response.status}: ${(await response.text()).slice(0, 500)}`);
|
|
@@ -179,7 +211,9 @@ class SseTransport extends McpTransport {
|
|
|
179
211
|
async open() {
|
|
180
212
|
try {
|
|
181
213
|
const response = await fetch(this.spec.url, {
|
|
182
|
-
headers: { accept: 'text/event-stream', ...this.spec.headers },
|
|
214
|
+
headers: { accept: 'text/event-stream', ...this.spec.headers },
|
|
215
|
+
signal: this.controller?.signal,
|
|
216
|
+
redirect: 'error',
|
|
183
217
|
});
|
|
184
218
|
if (!response.ok)
|
|
185
219
|
throw new Error(`HTTP ${response.status}: ${(await response.text()).slice(0, 500)}`);
|
|
@@ -192,7 +226,9 @@ class SseTransport extends McpTransport {
|
|
|
192
226
|
try {
|
|
193
227
|
this.receive(JSON.parse(event.data));
|
|
194
228
|
}
|
|
195
|
-
catch {
|
|
229
|
+
catch {
|
|
230
|
+
/* 忽略非 MCP SSE event。 */
|
|
231
|
+
}
|
|
196
232
|
}, this.controller?.signal);
|
|
197
233
|
if (!this.endpoint)
|
|
198
234
|
throw new Error('SSE MCP server 在连接关闭前未提供 endpoint');
|
|
@@ -218,13 +254,16 @@ export class McpClient {
|
|
|
218
254
|
this.spec = spec;
|
|
219
255
|
const receive = (message) => this.onMessage(message);
|
|
220
256
|
// 向后兼容早期仅提供 command/args 的 stdio 配置;新配置显式写 transport 更清晰。
|
|
221
|
-
this.transport =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
257
|
+
this.transport =
|
|
258
|
+
spec.transport === 'stdio' || ('command' in spec && typeof spec.command === 'string')
|
|
259
|
+
? new StdioTransport(receive, spec)
|
|
260
|
+
: spec.transport === 'sse'
|
|
261
|
+
? new SseTransport(receive, spec)
|
|
262
|
+
: new StreamableHttpTransport(receive, spec);
|
|
263
|
+
}
|
|
264
|
+
isReady() {
|
|
265
|
+
return this.initialized && !this.closed;
|
|
226
266
|
}
|
|
227
|
-
isReady() { return this.initialized && !this.closed; }
|
|
228
267
|
async initialize() {
|
|
229
268
|
if (this.isReady())
|
|
230
269
|
return;
|
|
@@ -353,7 +392,13 @@ async function consumeSse(body, onEvent, signal) {
|
|
|
353
392
|
let boundary;
|
|
354
393
|
while ((boundary = buffered.search(/\r?\n\r?\n/)) !== -1) {
|
|
355
394
|
const block = buffered.slice(0, boundary);
|
|
356
|
-
const separatorLength = buffered[boundary] === '\r'
|
|
395
|
+
const separatorLength = buffered[boundary] === '\r'
|
|
396
|
+
? buffered[boundary + 2] === '\r'
|
|
397
|
+
? 4
|
|
398
|
+
: 3
|
|
399
|
+
: buffered[boundary + 1] === '\r'
|
|
400
|
+
? 3
|
|
401
|
+
: 2;
|
|
357
402
|
buffered = buffered.slice(boundary + separatorLength);
|
|
358
403
|
const event = parseSseBlock(block);
|
|
359
404
|
if (event)
|
package/dist/mcp/config.js
CHANGED
|
@@ -114,9 +114,7 @@ function positiveNumber(value) {
|
|
|
114
114
|
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
115
115
|
}
|
|
116
116
|
function stringArray(value) {
|
|
117
|
-
return Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
118
|
-
? value.map(expandEnv)
|
|
119
|
-
: undefined;
|
|
117
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value.map(expandEnv) : undefined;
|
|
120
118
|
}
|
|
121
119
|
function stringRecord(value) {
|
|
122
120
|
if (!isRecord(value))
|
package/dist/mcp/index.js
CHANGED
|
@@ -68,7 +68,9 @@ export async function closeAllMcp() {
|
|
|
68
68
|
clients.clear();
|
|
69
69
|
await Promise.allSettled(active.map((client) => client.close()));
|
|
70
70
|
}
|
|
71
|
-
export function getMcpWarnings() {
|
|
71
|
+
export function getMcpWarnings() {
|
|
72
|
+
return [...startupWarnings];
|
|
73
|
+
}
|
|
72
74
|
function shouldRegisterTool(spec, remoteName) {
|
|
73
75
|
if (spec.excludeTools?.includes(remoteName))
|
|
74
76
|
return false;
|
package/dist/memory/reflect.js
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { chat } from '../llm/index.js';
|
|
14
14
|
import { config, isMemoryEnabled } from '../config/index.js';
|
|
15
|
-
import { saveEntry, updateEntry, forgetEntry, loadAll, gcMemories
|
|
15
|
+
import { saveEntry, updateEntry, forgetEntry, loadAll, gcMemories } from './store.js';
|
|
16
16
|
import { addTriple } from './graph.js';
|
|
17
17
|
// ── 日志(静默容错,裁尾保最近)─────────────────────────────────────────────
|
|
18
18
|
function logPath() {
|
|
@@ -41,9 +41,7 @@ function textOf(c) {
|
|
|
41
41
|
if (c == null)
|
|
42
42
|
return '';
|
|
43
43
|
if (Array.isArray(c)) {
|
|
44
|
-
return c
|
|
45
|
-
.map((p) => (typeof p === 'string' ? p : p?.text ?? ''))
|
|
46
|
-
.join('');
|
|
44
|
+
return c.map((p) => (typeof p === 'string' ? p : (p?.text ?? ''))).join('');
|
|
47
45
|
}
|
|
48
46
|
return String(c);
|
|
49
47
|
}
|
|
@@ -56,8 +54,7 @@ export function snapshotTranscript(history, K) {
|
|
|
56
54
|
const lines = convo.map((m) => {
|
|
57
55
|
const role = m.role ?? '?';
|
|
58
56
|
let line = `${role}: ${textOf(m.content)}`;
|
|
59
|
-
const tcs = m
|
|
60
|
-
.tool_calls;
|
|
57
|
+
const tcs = m.tool_calls;
|
|
61
58
|
if (Array.isArray(tcs)) {
|
|
62
59
|
for (const tc of tcs) {
|
|
63
60
|
line += `\n [tool_call ${tc?.function?.name ?? ''}] ${tc?.function?.arguments ?? ''}`;
|
package/dist/memory/store.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import { MAX_ACTIVE, MAX_INDEX_ENTRIES, MAX_MEMORY_ENTRY, DECAY_DAYS, GC_DAYS
|
|
14
|
+
import { MAX_ACTIVE, MAX_INDEX_ENTRIES, MAX_MEMORY_ENTRY, DECAY_DAYS, GC_DAYS } from '../tools/constants.js';
|
|
15
15
|
// ── 路径 ──────────────────────────────────────────────────────────────────
|
|
16
16
|
function globalPath() {
|
|
17
17
|
return path.join(os.homedir(), '.mocode', 'memory.jsonl');
|
|
@@ -163,12 +163,11 @@ function scoreEntry(e, terms) {
|
|
|
163
163
|
export function searchEntries(query, opts = {}) {
|
|
164
164
|
const all = loadAll();
|
|
165
165
|
const status = opts.status ?? 'active';
|
|
166
|
-
const pool = all
|
|
166
|
+
const pool = all
|
|
167
|
+
.filter((e) => (status === 'any' ? true : e.status === status))
|
|
167
168
|
.filter((e) => (opts.type ? e.type === opts.type : true));
|
|
168
169
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
169
|
-
const scored = pool
|
|
170
|
-
.map((e) => ({ e, s: scoreEntry(e, terms) }))
|
|
171
|
-
.filter((x) => x.s > 0);
|
|
170
|
+
const scored = pool.map((e) => ({ e, s: scoreEntry(e, terms) })).filter((x) => x.s > 0);
|
|
172
171
|
scored.sort((a, b) => b.s - a.s);
|
|
173
172
|
const limit = Math.max(1, Math.min(opts.limit ?? 5, 20));
|
|
174
173
|
const top = scored.slice(0, limit);
|
|
@@ -324,10 +323,9 @@ export function gcMemories() {
|
|
|
324
323
|
* 封顶 MAX_INDEX_ENTRIES;尾部标 hidden 数量,引导用 memory_list/memory_search 兜底。
|
|
325
324
|
* 真正「陈旧」被滤掉时也明示(让 LLM 知道有内容存在但被策略隐藏,而不是误以为空)。
|
|
326
325
|
*
|
|
327
|
-
* memoryEnabled=false
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
* isMemoryEnabled() 注入(本文件是叶子,避免直接引 config 起环)。
|
|
326
|
+
* memoryEnabled=false 时直接返空串,使 Memory Index 零注入;memory 工具仍在 registry 中,
|
|
327
|
+
* 但官方主 Agent 是否看见它们由独立的 per-turn ToolPolicy route gate 决定。传参由 repl
|
|
328
|
+
* 在拼装 system message 前调用 isMemoryEnabled() 得到(本文件保持叶子依赖)。
|
|
331
329
|
*/
|
|
332
330
|
export function buildMemoryIndexSection(memoryEnabled = true) {
|
|
333
331
|
if (!memoryEnabled)
|
|
@@ -372,9 +370,9 @@ export function buildMemoryIndexSection(memoryEnabled = true) {
|
|
|
372
370
|
'',
|
|
373
371
|
'',
|
|
374
372
|
'## Memory Index (retrieve full body via memory_search)',
|
|
375
|
-
'The following are saved memory entries (title/summary only). Retrieve full body via memory_search (pass id or keyword); use memory_list to see all,'
|
|
376
|
-
|
|
377
|
-
|
|
373
|
+
'The following are saved memory entries (title/summary only). Retrieve full body via memory_search (pass id or keyword); use memory_list to see all,' +
|
|
374
|
+
' memory_update to modify, memory_forget to archive. This list is a startup snapshot; entries added during the session are not listed here — use memory_list/memory_search to find them.' +
|
|
375
|
+
' Index policy: pinned + recently-recalled always shown; long-untouched active entries are hidden to keep this section lean.',
|
|
378
376
|
...lines,
|
|
379
377
|
tail,
|
|
380
378
|
].join('\n');
|
|
@@ -27,12 +27,36 @@ function canonicalProjectRoot(root) {
|
|
|
27
27
|
const resolved = path.resolve(root);
|
|
28
28
|
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* computer 工具的 type/key 文本内容审查(docs/computer-use-design.md §1.3):
|
|
32
|
+
* 命中 URL / 密码形态 / 支付关键词时,无论是否已授权都强制 once 级确认——
|
|
33
|
+
* 这类文本是「把内容敲进任意应用」的载体,不能因 session/项目授权而放行后续所有输入。
|
|
34
|
+
* 纯函数,独立可单测。
|
|
35
|
+
*/
|
|
36
|
+
export function computerTextNeedsReview(text) {
|
|
37
|
+
const s = text.toLowerCase();
|
|
38
|
+
if (/https?:\/\/|www\.[a-z0-9-]+\.[a-z]{2,}/i.test(s))
|
|
39
|
+
return true;
|
|
40
|
+
if (/\b(pass(word)?|passwd|pwd|secret|token|api[_-]?key|credential)\b\s*[:=]/i.test(s))
|
|
41
|
+
return true;
|
|
42
|
+
if (/\b(card\s*number|cvv|cvc|expiry)\b/i.test(s))
|
|
43
|
+
return true;
|
|
44
|
+
// CJK 关键词没有 \b 词边界概念(\b 只在 \w 与非 \w 之间成立,中文不是 \w),用普通子串匹配。
|
|
45
|
+
if (/信用卡|卡号|密码|支付|付款|验证码/.test(s))
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
30
49
|
/** A grant is bound to the actual command or logical resources, never merely a tool name. */
|
|
31
50
|
export function permissionFingerprint(tool, args) {
|
|
32
51
|
let subject;
|
|
33
52
|
if (tool.name === 'run_command' && typeof args.command === 'string') {
|
|
34
53
|
subject = { command: args.command.trim() };
|
|
35
54
|
}
|
|
55
|
+
else if (tool.name === 'computer') {
|
|
56
|
+
// computer:坐标/文本每次不同,进指纹会让每次点击都弹窗(功能上等同不可用)。
|
|
57
|
+
// 按动作粒度授权:允许一次 left_click = 本会话允许任意坐标的左键(docs/computer-use-design.md §3.6)。
|
|
58
|
+
subject = { computer: typeof args.action === 'string' ? args.action : 'unknown' };
|
|
59
|
+
}
|
|
36
60
|
else {
|
|
37
61
|
let resources;
|
|
38
62
|
try {
|
|
@@ -43,9 +67,7 @@ export function permissionFingerprint(tool, args) {
|
|
|
43
67
|
}
|
|
44
68
|
// File mutations may be granted by their concrete resource. Coarse resources such as
|
|
45
69
|
// "workspace" must retain arguments so task/process-like calls cannot become tool-wide.
|
|
46
|
-
subject = resources?.length && typeof args.path === 'string'
|
|
47
|
-
? { resources }
|
|
48
|
-
: stable(args);
|
|
70
|
+
subject = resources?.length && typeof args.path === 'string' ? { resources } : stable(args);
|
|
49
71
|
}
|
|
50
72
|
return crypto.createHash('sha256').update(JSON.stringify(subject)).digest('hex');
|
|
51
73
|
}
|
|
@@ -53,10 +75,10 @@ function validGrant(value) {
|
|
|
53
75
|
if (!value || typeof value !== 'object')
|
|
54
76
|
return false;
|
|
55
77
|
const grant = value;
|
|
56
|
-
return typeof grant.tool === 'string'
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
return (typeof grant.tool === 'string' &&
|
|
79
|
+
typeof grant.fingerprint === 'string' &&
|
|
80
|
+
grant.fingerprint.length > 0 &&
|
|
81
|
+
(grant.scope === 'project' || grant.scope === 'session' || grant.scope === 'once'));
|
|
60
82
|
}
|
|
61
83
|
function loadPermanent() {
|
|
62
84
|
if (permanentLoaded)
|
|
@@ -69,9 +91,10 @@ function loadPermanent() {
|
|
|
69
91
|
: [];
|
|
70
92
|
// Only the explicit v3 field enables broad grants. The retired legacy allowForever
|
|
71
93
|
// field remains ignored so upgrades cannot silently restore old authorization.
|
|
72
|
-
permanentToolAllows =
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
permanentToolAllows =
|
|
95
|
+
parsed.version === PERMISSIONS_VERSION && Array.isArray(parsed.alwaysAllowTools)
|
|
96
|
+
? new Set(parsed.alwaysAllowTools.filter((tool) => typeof tool === 'string' && tool.length > 0))
|
|
97
|
+
: new Set();
|
|
75
98
|
}
|
|
76
99
|
catch {
|
|
77
100
|
permanentGrants = [];
|
|
@@ -102,6 +125,17 @@ function summarizeArgs(args) {
|
|
|
102
125
|
lines.push(t('permission.path', { value: args.path }));
|
|
103
126
|
if (typeof args.command === 'string')
|
|
104
127
|
lines.push(t('permission.command', { value: args.command }));
|
|
128
|
+
if (typeof args.action === 'string') {
|
|
129
|
+
// computer 工具:动作名 + 坐标/文本预览(文本截断防长串刷屏)。
|
|
130
|
+
const parts = [args.action];
|
|
131
|
+
if (Array.isArray(args.coordinate))
|
|
132
|
+
parts.push(`@(${args.coordinate.join(', ')})`);
|
|
133
|
+
if (typeof args.text === 'string' && args.text) {
|
|
134
|
+
const preview = args.text.slice(0, 80);
|
|
135
|
+
parts.push(`"${preview}${args.text.length > 80 ? '…' : ''}"`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(t('permission.computerAction', { value: parts.join(' ') }));
|
|
138
|
+
}
|
|
105
139
|
if (typeof args.prompt === 'string') {
|
|
106
140
|
const preview = args.prompt.slice(0, 100);
|
|
107
141
|
lines.push(t('permission.task', { value: `${preview}${args.prompt.length > 100 ? '…' : ''}` }));
|
|
@@ -109,9 +143,9 @@ function summarizeArgs(args) {
|
|
|
109
143
|
return lines.length ? lines.join('\n') : t('permission.noArgs');
|
|
110
144
|
}
|
|
111
145
|
function matches(grant, tool, fingerprint, projectRoot) {
|
|
112
|
-
return grant.tool === tool
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
return (grant.tool === tool &&
|
|
147
|
+
grant.fingerprint === fingerprint &&
|
|
148
|
+
(grant.scope !== 'project' || grant.projectRoot === projectRoot));
|
|
115
149
|
}
|
|
116
150
|
export async function checkPermission(tool, args, signal, options = {}) {
|
|
117
151
|
if (!config.permissionEnabled || getToolRisk(tool) === 'safe')
|
|
@@ -121,12 +155,21 @@ export async function checkPermission(tool, args, signal, options = {}) {
|
|
|
121
155
|
loadPermanent();
|
|
122
156
|
if (permanentToolAllows.has(tool.name))
|
|
123
157
|
return 'allow';
|
|
158
|
+
// computer 的 type/key 命中敏感内容(URL/密码/支付)时强制 once 级确认:
|
|
159
|
+
// 不看任何授权缓存,也不提供 session/项目/永久授权选项——这类文本是「敲进任意应用」的载体,
|
|
160
|
+
// 一旦放行不该沿用到下一次输入(docs/computer-use-design.md §1.3/§3.6)。
|
|
161
|
+
const forceOnce = tool.name === 'computer' &&
|
|
162
|
+
(args.action === 'type' || args.action === 'key') &&
|
|
163
|
+
typeof args.text === 'string' &&
|
|
164
|
+
computerTextNeedsReview(args.text);
|
|
124
165
|
const fingerprint = permissionFingerprint(tool, args);
|
|
125
166
|
const projectRoot = canonicalProjectRoot(options.projectRoot ?? getSandboxRoot() ?? process.cwd());
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
if (!forceOnce) {
|
|
168
|
+
if (sessionGrants.some((grant) => matches(grant, tool.name, fingerprint, projectRoot)))
|
|
169
|
+
return 'allow';
|
|
170
|
+
if (permanentGrants.some((grant) => matches(grant, tool.name, fingerprint, projectRoot)))
|
|
171
|
+
return 'allow';
|
|
172
|
+
}
|
|
130
173
|
// CI/pipes must fail closed. Operators can deliberately restore unattended behavior.
|
|
131
174
|
if (!process.stdin.isTTY && !config.permissionNonInteractiveAllow && !options.prompt)
|
|
132
175
|
return 'deny';
|
|
@@ -138,10 +181,14 @@ export async function checkPermission(tool, args, signal, options = {}) {
|
|
|
138
181
|
const alwaysOption = t('permission.allowForever');
|
|
139
182
|
const denyOption = t('permission.deny');
|
|
140
183
|
const dangerous = getToolRisk(tool) === 'dangerous';
|
|
141
|
-
const choices =
|
|
184
|
+
const choices = forceOnce
|
|
185
|
+
? [onceOption, denyOption]
|
|
186
|
+
: [onceOption, sessionOption, projectOption, alwaysOption, denyOption];
|
|
142
187
|
// 新手首次审批引导:第一次弹出确认面板时附一段说明(每个用户只出现一次,
|
|
143
188
|
// ~/.mocode/config 的 onboarding_approval_hint 标记;读写失败静默,不阻断审批)。
|
|
144
189
|
let detail = summarizeArgs(args) + (dangerous ? `\n\n${t('permission.dangerWarning')}` : '');
|
|
190
|
+
if (forceOnce)
|
|
191
|
+
detail += `\n\n${t('permission.computerSensitiveReview')}`;
|
|
145
192
|
const ONBOARDING_KEY = 'onboarding_approval_hint';
|
|
146
193
|
try {
|
|
147
194
|
if (!readConfigFile()[ONBOARDING_KEY]) {
|
|
@@ -205,10 +252,7 @@ export function listPermanentToolAllows() {
|
|
|
205
252
|
/** Compatibility API: returns tools having any persistent resource or tool-wide grant. */
|
|
206
253
|
export function listPermanentAllow() {
|
|
207
254
|
loadPermanent();
|
|
208
|
-
return [...new Set([
|
|
209
|
-
...permanentGrants.map((grant) => grant.tool),
|
|
210
|
-
...permanentToolAllows,
|
|
211
|
-
])].sort();
|
|
255
|
+
return [...new Set([...permanentGrants.map((grant) => grant.tool), ...permanentToolAllows])].sort();
|
|
212
256
|
}
|
|
213
257
|
export function clearSessionPermissionGrants() {
|
|
214
258
|
sessionGrants.length = 0;
|
package/dist/pet/bridge.js
CHANGED
|
@@ -27,7 +27,7 @@ let lastSentState = null;
|
|
|
27
27
|
let heartbeatTimer = null;
|
|
28
28
|
let heartbeatTimeoutTimer = null;
|
|
29
29
|
/** list_skins 请求的等待队列(先进先出;桌宠单连接场景下不会有并发歧义)。 */
|
|
30
|
-
|
|
30
|
+
const pendingSkinListResolvers = [];
|
|
31
31
|
function clearHeartbeat() {
|
|
32
32
|
if (heartbeatTimer)
|
|
33
33
|
clearInterval(heartbeatTimer);
|
|
@@ -117,7 +117,7 @@ export function spawnPetProcess() {
|
|
|
117
117
|
const require = createRequire(import.meta.url);
|
|
118
118
|
binPath = require.resolve('mocode-pet-app/bin/pet-app.js');
|
|
119
119
|
}
|
|
120
|
-
catch
|
|
120
|
+
catch {
|
|
121
121
|
reject(new Error('mocode-pet-app 未安装,请运行 npm install mocode-pet-app'));
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 外观命令组:/theme [name|list]
|
|
3
|
+
*
|
|
4
|
+
* 无参开菜单(↑↓ 选,Enter 切换,Esc 取消);/theme <name> 直切;/theme list 或未知名 → 列出。
|
|
5
|
+
*
|
|
6
|
+
* 切换路径:setTheme → 重算状态行(新色)→ 清内容重绘(历史 / 横幅,镜像启动 + /resume)→
|
|
7
|
+
* 确认 → 持久化。markdown MEMO 按 themeVersion 自动失效,故 renderHistory 取新色;
|
|
8
|
+
* 状态栏 / 输入框由 next 回 INPUT 态时读 getter 刷。
|
|
9
|
+
*/
|
|
10
|
+
import * as layout from '../../ui/layout.js';
|
|
11
|
+
import { ui, setTheme, getTheme, listThemes, themeExists } from '../../ui/theme.js';
|
|
12
|
+
import { bannerLines } from '../../ui/render.js';
|
|
13
|
+
import { t } from '../../i18n/index.js';
|
|
14
|
+
import { config } from '../../config/index.js';
|
|
15
|
+
import { updateConfigKey } from '../../config/file.js';
|
|
16
|
+
import { promptThemePicker } from '../../ui/prompt.js';
|
|
17
|
+
import { themeDescription } from '../commands.js';
|
|
18
|
+
import { renderHistory } from '../message-format.js';
|
|
19
|
+
import { unhandled, next } from './types.js';
|
|
20
|
+
export const appearanceCommands = [
|
|
21
|
+
async (ctx) => {
|
|
22
|
+
const { line } = ctx;
|
|
23
|
+
if (line !== '/theme' && !line.startsWith('/theme '))
|
|
24
|
+
return unhandled();
|
|
25
|
+
const arg = line.startsWith('/theme ') ? line.slice('/theme '.length).trim() : '';
|
|
26
|
+
let name;
|
|
27
|
+
if (arg === '') {
|
|
28
|
+
// 无参:菜单选(仿 /resume /rollback 的 picker)
|
|
29
|
+
const items = listThemes().map((th) => ({
|
|
30
|
+
id: th,
|
|
31
|
+
title: th,
|
|
32
|
+
subtitle: themeDescription(th),
|
|
33
|
+
}));
|
|
34
|
+
let pick;
|
|
35
|
+
try {
|
|
36
|
+
pick = await promptThemePicker(items);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return next(); // Ctrl+C(SIGINT)→ 取消
|
|
40
|
+
}
|
|
41
|
+
name = pick?.id ?? null;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === 'list' || !themeExists(arg)) {
|
|
44
|
+
layout.contentWrite(`${ui.dim}${t('repl.themeList')}${ui.reset}\n`);
|
|
45
|
+
for (const theme of listThemes()) {
|
|
46
|
+
layout.contentWrite(` ${ui.accent}${theme}${ui.reset} ${ui.dim}${themeDescription(theme)}${ui.reset}\n`);
|
|
47
|
+
}
|
|
48
|
+
layout.contentWrite(`${ui.dim}${t('repl.themeCurrent', { theme: getTheme() })}${ui.reset}\n`);
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
name = arg;
|
|
53
|
+
}
|
|
54
|
+
if (name === null)
|
|
55
|
+
return next(); // Esc / Ctrl+D 取消
|
|
56
|
+
setTheme(name);
|
|
57
|
+
ctx.refreshStatusBase(ctx.history);
|
|
58
|
+
layout.clearContent();
|
|
59
|
+
if (ctx.history.some((m) => m.role === 'user')) {
|
|
60
|
+
renderHistory(ctx.history);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
layout.writeBanner(bannerLines(ctx.banner()));
|
|
64
|
+
}
|
|
65
|
+
layout.contentWrite(`${ui.dim}${t('repl.themeChanged', { theme: name })}${ui.reset}\n`);
|
|
66
|
+
updateConfigKey('MOCODE_THEME', name);
|
|
67
|
+
if (config.themeFromShell) {
|
|
68
|
+
layout.contentWrite(`${ui.dim}(shell 环境变量 MOCODE_THEME 已设,文件写入下次启动被其覆盖;取消该 shell 设置后生效)${ui.reset}\n`);
|
|
69
|
+
}
|
|
70
|
+
return next();
|
|
71
|
+
},
|
|
72
|
+
];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 压缩命令组:/compact [focus] · /compact --no-force [focus]
|
|
3
|
+
*
|
|
4
|
+
* 摘要是几十秒的 LLM 调用:包一层 startRunningListener 让 Ctrl+C 能掐断
|
|
5
|
+
* (信号透传 manualCompact → maybeCompact → compactHistory → chat)。
|
|
6
|
+
* 中断时 history 未被改动(重建发生在摘要成功之后),直接提示回输入态。
|
|
7
|
+
*
|
|
8
|
+
* 会话状态(plan + 笔记段)不在此处回写 history[0]:agent/core 每步都在
|
|
9
|
+
* requestHistory 末尾注入最新副本(buildSessionStateReminder),压缩后下一步
|
|
10
|
+
* 自然恢复,且系统提示保持逐字节稳定以命中 prompt 缓存。
|
|
11
|
+
*/
|
|
12
|
+
import * as layout from '../../ui/layout.js';
|
|
13
|
+
import { ui } from '../../ui/theme.js';
|
|
14
|
+
import { t } from '../../i18n/index.js';
|
|
15
|
+
import { manualCompact, appendCurrentSessionRuntimeEvent, hashTraceValue } from '../../session/index.js';
|
|
16
|
+
import { startRunningListener, stopRunningListener } from '../running-input.js';
|
|
17
|
+
import { unhandled, next } from './types.js';
|
|
18
|
+
export const compactCommands = [
|
|
19
|
+
async (ctx) => {
|
|
20
|
+
const { line } = ctx;
|
|
21
|
+
if (line !== '/compact' && !line.startsWith('/compact '))
|
|
22
|
+
return unhandled();
|
|
23
|
+
// /compact 默认强制压缩(force=true),不受阈值/保护区限制
|
|
24
|
+
const rest = line.slice('/compact'.length).trim();
|
|
25
|
+
let force = true; // 默认强制
|
|
26
|
+
let focus;
|
|
27
|
+
if (rest === '--no-force')
|
|
28
|
+
force = false;
|
|
29
|
+
else if (rest.startsWith('--no-force ')) {
|
|
30
|
+
force = false;
|
|
31
|
+
focus = rest.slice('--no-force '.length).trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
else if (rest)
|
|
34
|
+
focus = rest;
|
|
35
|
+
// 走调度器路径:与自动每步压缩完全一致——五区按 ROI 压(cold tools 优先 → history 摘要最后)。
|
|
36
|
+
// focus 透传到 compact_history action 的 LLM 摘要 prompt。
|
|
37
|
+
// 返回 SchedulerRunLog 给 UI 显示决策;退化路径(开关关时)在 manualCompact 内部走 compactHistory。
|
|
38
|
+
const log = await (async () => {
|
|
39
|
+
const signal = startRunningListener(t('running.compacting'));
|
|
40
|
+
try {
|
|
41
|
+
return await manualCompact(ctx.history, focus, { force, signal });
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
stopRunningListener();
|
|
45
|
+
}
|
|
46
|
+
})().catch((e) => {
|
|
47
|
+
// 中断:history 未被改动(重建在摘要成功之后),直接提示并回到输入态。
|
|
48
|
+
if (e instanceof Error && (e.name === 'AbortError' || e.name === 'APIUserAbortError')) {
|
|
49
|
+
layout.contentWrite(`${ui.dim}(已取消压缩)${ui.reset}\n`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
throw e;
|
|
53
|
+
});
|
|
54
|
+
if (!log)
|
|
55
|
+
return next();
|
|
56
|
+
const d = log.compactDetail;
|
|
57
|
+
appendCurrentSessionRuntimeEvent('compact', {
|
|
58
|
+
source: 'manual',
|
|
59
|
+
force,
|
|
60
|
+
called: log.compactHistoryCalled,
|
|
61
|
+
reason: d?.reason ?? 'unknown',
|
|
62
|
+
estimateBefore: d?.estimateBefore,
|
|
63
|
+
estimateAfter: d?.estimateAfter,
|
|
64
|
+
focusHash: focus ? hashTraceValue(focus) : undefined,
|
|
65
|
+
});
|
|
66
|
+
if (!d) {
|
|
67
|
+
// 兜底(旧调用):只显示 old 文案
|
|
68
|
+
if (!log.compactHistoryCalled) {
|
|
69
|
+
layout.contentWrite(`${ui.dim}(无需压缩:没有可压缩的旧消息)${ui.reset}\n`);
|
|
70
|
+
}
|
|
71
|
+
else if (focus) {
|
|
72
|
+
layout.contentWrite(`${ui.dim}(带焦点压缩:${focus})${ui.reset}\n`);
|
|
73
|
+
}
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
// 详细文案:按 reason 分类
|
|
77
|
+
const reason = d.reason;
|
|
78
|
+
const before = d.estimateBefore;
|
|
79
|
+
const after = d.estimateAfter;
|
|
80
|
+
const proto = d.protectedRatio !== undefined ? `保护区占比 ${(d.protectedRatio * 100).toFixed(0)}%` : '';
|
|
81
|
+
const oldCt = d.oldGroupCount !== undefined ? `旧区组数 ${d.oldGroupCount}` : '';
|
|
82
|
+
const focusNote = focus ? `焦点:${focus}` : '';
|
|
83
|
+
const stats = [proto, oldCt].filter(Boolean).join(' · ');
|
|
84
|
+
if (reason === 'microcompact') {
|
|
85
|
+
layout.contentWrite(`${ui.cyan}✓ 微压缩:${ui.reset} ${before} → ${after} tokens${stats ? ` (${ui.dim}${stats}${ui.reset})` : ''}\n`);
|
|
86
|
+
}
|
|
87
|
+
else if (reason === 'summarize') {
|
|
88
|
+
layout.contentWrite(`${ui.cyan}✓ LLM 摘要:${ui.reset} ${before} → ${after} tokens${focusNote ? ` (${ui.dim}${focusNote}${ui.reset})` : ''}\n`);
|
|
89
|
+
}
|
|
90
|
+
else if (reason === 'noop-empty') {
|
|
91
|
+
layout.contentWrite(`${ui.dim}(history 太短,只有 system 提示,无可压旧区)${ui.reset}\n`);
|
|
92
|
+
}
|
|
93
|
+
else if (reason === 'noop-protected') {
|
|
94
|
+
layout.contentWrite(`${ui.dim}(无可压旧区:全部在保护区 system + 当前轮)${ui.reset}${stats ? ` ${ui.dim}(${stats})${ui.reset}` : ''}\n`);
|
|
95
|
+
layout.contentWrite(`${ui.dim}提示:/compact --force 强行把早期对话压成摘要${ui.reset}\n`);
|
|
96
|
+
}
|
|
97
|
+
else if (reason === 'noop-ml-only') {
|
|
98
|
+
layout.contentWrite(`${ui.dim}(LLM 摘要失败,且无超大单条可微压;可能是后端不可用)${ui.reset}\n`);
|
|
99
|
+
layout.contentWrite(`${ui.dim}回退:只跑了 keep-current 结构,history 未变${ui.reset}\n`);
|
|
100
|
+
}
|
|
101
|
+
else if (reason === 'noop-shrunk-too-large') {
|
|
102
|
+
layout.contentWrite(`${ui.yellow}● 上下文已超阈但无可压缩项(全在保护区),建议 /clear 或缩短输入。${ui.reset}\n`);
|
|
103
|
+
if (stats)
|
|
104
|
+
layout.contentWrite(`${ui.dim}(${stats})${ui.reset}\n`);
|
|
105
|
+
}
|
|
106
|
+
else if (reason === 'noop-noold-noop') {
|
|
107
|
+
layout.contentWrite(`${ui.dim}(无需压缩:没有可压缩的旧消息,且不在手动触发)${ui.reset}\n`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
layout.contentWrite(`${ui.dim}(reason=${reason},${before} → ${after} tokens)${ui.reset}\n`);
|
|
111
|
+
}
|
|
112
|
+
return next();
|
|
113
|
+
},
|
|
114
|
+
];
|