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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自动工具路由的 capability gate 命令组:/subagent · /fe(/frontend) · /cu(/computer) ·
|
|
3
|
+
* /mcp · /memory_switch · /memory_status。
|
|
4
|
+
*
|
|
5
|
+
* `/subagent`、`/fe`、`/cu` 不再把工具常驻加入 schema:on/unset 仅允许下一真实用户轮的
|
|
6
|
+
* LLM router 按需选择对应簇,off 写入 env=false 并形成硬否决。refreshChatTools 只为没有
|
|
7
|
+
* ToolPolicy 的 legacy 嵌入路径保留。MCP 仍负责真实连接生命周期;memory 开关还控制索引。
|
|
8
|
+
*/
|
|
9
|
+
import * as layout from '../../ui/layout.js';
|
|
10
|
+
import { ui } from '../../ui/theme.js';
|
|
11
|
+
import { bannerLines } from '../../ui/render.js';
|
|
12
|
+
import { t } from '../../i18n/index.js';
|
|
13
|
+
import { isSubAgentRouteAllowed, updateSubAgentConfig, isFrontendRouteAllowed, updateFrontendToolsConfig, isComputerUseRouteAllowed, updateComputerUseConfig, isMcpEnabled, updateMcpConfig, isMemoryEnabled, isMemoryRouteAllowed, updateMemoryConfig, } from '../../config/index.js';
|
|
14
|
+
import { updateConfigKey, CONFIG_PATH } from '../../config/file.js';
|
|
15
|
+
import { getAgentMode } from '../../agent/mode.js';
|
|
16
|
+
import { refreshChatTools } from '../../llm/index.js';
|
|
17
|
+
import { registerToolsExtension } from '../../tools/registry.js';
|
|
18
|
+
import { initializeAllMcp, getMcpTools, closeAllMcp } from '../../mcp/index.js';
|
|
19
|
+
import { unhandled, next } from './types.js';
|
|
20
|
+
export const toolGroupCommands = [
|
|
21
|
+
// /subagent [status|on|off] — orchestration 簇的兼容 capability gate。
|
|
22
|
+
(ctx) => {
|
|
23
|
+
const { line } = ctx;
|
|
24
|
+
if (line !== '/subagent' && !line.startsWith('/subagent '))
|
|
25
|
+
return unhandled();
|
|
26
|
+
const arg = line.startsWith('/subagent ') ? line.slice('/subagent '.length).trim().toLowerCase() : 'status';
|
|
27
|
+
if (arg === '' || arg === 'status') {
|
|
28
|
+
const enabled = isSubAgentRouteAllowed();
|
|
29
|
+
const state = t(enabled ? 'subagent.stateOn' : 'subagent.stateOff');
|
|
30
|
+
layout.contentWrite(`${ui.accent}${t('subagent.status', { state })}${ui.reset}\n` +
|
|
31
|
+
`${ui.dim}MOCODE_SUBAGENT_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n`);
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
35
|
+
layout.contentWrite(`${ui.yellow}${t('subagent.usage')}${ui.reset}\n`);
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
const enabled = arg === 'on';
|
|
39
|
+
if (enabled !== isSubAgentRouteAllowed()) {
|
|
40
|
+
updateSubAgentConfig(enabled);
|
|
41
|
+
updateConfigKey('MOCODE_SUBAGENT_ENABLED', enabled ? 'true' : 'false');
|
|
42
|
+
refreshChatTools();
|
|
43
|
+
ctx.history[0] = { role: 'system', content: ctx.buildSystemMessage(getAgentMode() === 'plan') };
|
|
44
|
+
layout.rewriteBanner(bannerLines(ctx.banner()));
|
|
45
|
+
}
|
|
46
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'subagent.changedOn' : 'subagent.changedOff')}${ui.reset}\n`);
|
|
47
|
+
return next();
|
|
48
|
+
},
|
|
49
|
+
// /fe|/frontend [status|on|off] — browser-debug + desktop-observe 的兼容 gate。
|
|
50
|
+
// view_image 是 common 工具,不受此开关影响。
|
|
51
|
+
(ctx) => {
|
|
52
|
+
const { line } = ctx;
|
|
53
|
+
const isFe = line === '/fe' || line.startsWith('/fe ') || line === '/frontend' || line.startsWith('/frontend ');
|
|
54
|
+
if (!isFe)
|
|
55
|
+
return unhandled();
|
|
56
|
+
const raw = line.startsWith('/fe ')
|
|
57
|
+
? line.slice('/fe '.length)
|
|
58
|
+
: line.startsWith('/frontend ')
|
|
59
|
+
? line.slice('/frontend '.length)
|
|
60
|
+
: line === '/frontend'
|
|
61
|
+
? ''
|
|
62
|
+
: line.slice('/fe'.length);
|
|
63
|
+
const arg = raw.trim().toLowerCase();
|
|
64
|
+
if (arg === '' || arg === 'status') {
|
|
65
|
+
const enabled = isFrontendRouteAllowed();
|
|
66
|
+
const state = t(enabled ? 'fe.stateOn' : 'fe.stateOff');
|
|
67
|
+
layout.contentWrite(`${ui.accent}${t('fe.status', { state })}${ui.reset}\n` +
|
|
68
|
+
`${ui.dim}MOCODE_FRONTEND_TOOLS_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n`);
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
72
|
+
layout.contentWrite(`${ui.yellow}${t('fe.usage')}${ui.reset}\n`);
|
|
73
|
+
return next();
|
|
74
|
+
}
|
|
75
|
+
const enabled = arg === 'on';
|
|
76
|
+
if (enabled !== isFrontendRouteAllowed()) {
|
|
77
|
+
updateFrontendToolsConfig(enabled);
|
|
78
|
+
updateConfigKey('MOCODE_FRONTEND_TOOLS_ENABLED', enabled ? 'true' : 'false');
|
|
79
|
+
refreshChatTools();
|
|
80
|
+
ctx.history[0] = { role: 'system', content: ctx.buildSystemMessage(getAgentMode() === 'plan') };
|
|
81
|
+
layout.rewriteBanner(bannerLines(ctx.banner()));
|
|
82
|
+
}
|
|
83
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'fe.changedOn' : 'fe.changedOff')}${ui.reset}\n`);
|
|
84
|
+
return next();
|
|
85
|
+
},
|
|
86
|
+
// /cu|/computer [status|on|off] — computer-control 的高危 capability gate。
|
|
87
|
+
// on 仅允许 router 在用户明确要求 GUI 操作时选择;每个动作仍经过权限门,plan 永远过滤。
|
|
88
|
+
(ctx) => {
|
|
89
|
+
const { line } = ctx;
|
|
90
|
+
const isCu = line === '/cu' || line.startsWith('/cu ') || line === '/computer' || line.startsWith('/computer ');
|
|
91
|
+
if (!isCu)
|
|
92
|
+
return unhandled();
|
|
93
|
+
const raw = line.startsWith('/cu ')
|
|
94
|
+
? line.slice('/cu '.length)
|
|
95
|
+
: line.startsWith('/computer ')
|
|
96
|
+
? line.slice('/computer '.length)
|
|
97
|
+
: line === '/computer'
|
|
98
|
+
? ''
|
|
99
|
+
: line.slice('/cu'.length);
|
|
100
|
+
const arg = raw.trim().toLowerCase();
|
|
101
|
+
if (arg === '' || arg === 'status') {
|
|
102
|
+
const enabled = isComputerUseRouteAllowed();
|
|
103
|
+
const state = t(enabled ? 'cu.stateOn' : 'cu.stateOff');
|
|
104
|
+
layout.contentWrite(`${ui.accent}${t('cu.status', { state })}${ui.reset}\n` +
|
|
105
|
+
`${ui.dim}MOCODE_COMPUTER_USE_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n`);
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
108
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
109
|
+
layout.contentWrite(`${ui.yellow}${t('cu.usage')}${ui.reset}\n`);
|
|
110
|
+
return next();
|
|
111
|
+
}
|
|
112
|
+
const enabled = arg === 'on';
|
|
113
|
+
if (enabled !== isComputerUseRouteAllowed()) {
|
|
114
|
+
updateComputerUseConfig(enabled);
|
|
115
|
+
updateConfigKey('MOCODE_COMPUTER_USE_ENABLED', enabled ? 'true' : 'false');
|
|
116
|
+
refreshChatTools();
|
|
117
|
+
ctx.history[0] = { role: 'system', content: ctx.buildSystemMessage(getAgentMode() === 'plan') };
|
|
118
|
+
layout.rewriteBanner(bannerLines(ctx.banner()));
|
|
119
|
+
}
|
|
120
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'cu.changedOn' : 'cu.changedOff')}${ui.reset}\n`);
|
|
121
|
+
return next();
|
|
122
|
+
},
|
|
123
|
+
// /mcp [status|on|off] — 唯一带 async 副作用的开关(on 连 MCP,off 断)。
|
|
124
|
+
async (ctx) => {
|
|
125
|
+
const { line } = ctx;
|
|
126
|
+
if (line !== '/mcp' && !line.startsWith('/mcp '))
|
|
127
|
+
return unhandled();
|
|
128
|
+
const arg = line.startsWith('/mcp ') ? line.slice('/mcp '.length).trim().toLowerCase() : 'status';
|
|
129
|
+
if (arg === '' || arg === 'status') {
|
|
130
|
+
const enabled = isMcpEnabled();
|
|
131
|
+
const state = t(enabled ? 'mcp.stateOn' : 'mcp.stateOff');
|
|
132
|
+
layout.contentWrite(`${ui.accent}${t('mcp.status', { state })}${ui.reset}\n` +
|
|
133
|
+
`${ui.dim}MOCODE_MCP_ENABLED=${enabled ? 'true' : 'false'} · ${CONFIG_PATH}${ui.reset}\n` +
|
|
134
|
+
`${ui.dim}${t('mcp.restartHint')}${ui.reset}\n`);
|
|
135
|
+
return next();
|
|
136
|
+
}
|
|
137
|
+
if (arg !== 'on' && arg !== 'off') {
|
|
138
|
+
layout.contentWrite(`${ui.yellow}${t('mcp.usage')}${ui.reset}\n`);
|
|
139
|
+
return next();
|
|
140
|
+
}
|
|
141
|
+
const enabled = arg === 'on';
|
|
142
|
+
if (enabled === isMcpEnabled()) {
|
|
143
|
+
layout.contentWrite(`${ui.dim}${t('mcp.unchanged', { state: t(enabled ? 'mcp.stateOn' : 'mcp.stateOff') })}${ui.reset}\n`);
|
|
144
|
+
return next();
|
|
145
|
+
}
|
|
146
|
+
updateMcpConfig(enabled);
|
|
147
|
+
updateConfigKey('MOCODE_MCP_ENABLED', enabled ? 'true' : 'false');
|
|
148
|
+
if (enabled) {
|
|
149
|
+
await initializeAllMcp();
|
|
150
|
+
registerToolsExtension('mcp', getMcpTools());
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await closeAllMcp();
|
|
154
|
+
registerToolsExtension('mcp', []);
|
|
155
|
+
}
|
|
156
|
+
refreshChatTools();
|
|
157
|
+
ctx.history[0] = { role: 'system', content: ctx.buildSystemMessage(getAgentMode() === 'plan') };
|
|
158
|
+
layout.rewriteBanner(bannerLines(ctx.banner()));
|
|
159
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t(enabled ? 'mcp.changedOn' : 'mcp.changedOff')}${ui.reset}\n`);
|
|
160
|
+
return next();
|
|
161
|
+
},
|
|
162
|
+
// /memory_switch [on|off|status] · /memory_status — 同时管理 memory route gate 与 Memory Index。
|
|
163
|
+
// 无参切换;status 明确区分“可被路由”和“索引/反思已启用”,避免 unset 状态被误报为禁用。
|
|
164
|
+
(ctx) => {
|
|
165
|
+
const { line } = ctx;
|
|
166
|
+
const isMemSwitch = line === '/memory_switch' ||
|
|
167
|
+
line.startsWith('/memory_switch ') ||
|
|
168
|
+
line === '/memory_status' ||
|
|
169
|
+
line.startsWith('/memory_status ');
|
|
170
|
+
if (!isMemSwitch)
|
|
171
|
+
return unhandled();
|
|
172
|
+
try {
|
|
173
|
+
const switchArg = line.startsWith('/memory_switch ')
|
|
174
|
+
? line.slice('/memory_switch '.length).trim().toLowerCase()
|
|
175
|
+
: '';
|
|
176
|
+
const showStatus = line === '/memory_status' || line.startsWith('/memory_status ') || switchArg === 'status';
|
|
177
|
+
if (showStatus) {
|
|
178
|
+
const routeAllowed = isMemoryRouteAllowed();
|
|
179
|
+
const indexEnabled = isMemoryEnabled();
|
|
180
|
+
layout.contentWrite(`${ui.accent}${t('memory.routeStatus', {
|
|
181
|
+
state: t(routeAllowed ? 'memory.stateAllowed' : 'memory.stateBlocked'),
|
|
182
|
+
})}${ui.reset}\n` +
|
|
183
|
+
`${ui.accent}${t('memory.indexStatus', {
|
|
184
|
+
state: t(indexEnabled ? 'memory.stateEnabled' : 'memory.stateDisabled'),
|
|
185
|
+
})}${ui.reset}\n`);
|
|
186
|
+
layout.contentWrite(`${ui.dim}${t('memory.source', {
|
|
187
|
+
value: process.env.MEMORY_ENABLED ?? '<unset>',
|
|
188
|
+
path: CONFIG_PATH,
|
|
189
|
+
})}${ui.reset}\n` + `${ui.dim}${t('memory.statusHint')}${ui.reset}\n`);
|
|
190
|
+
return next();
|
|
191
|
+
}
|
|
192
|
+
let nextEnabled;
|
|
193
|
+
if (switchArg === '') {
|
|
194
|
+
nextEnabled = !isMemoryEnabled();
|
|
195
|
+
}
|
|
196
|
+
else if (['on', 'true', '1', 'yes', 'y', 'enable', 'enabled'].includes(switchArg)) {
|
|
197
|
+
nextEnabled = true;
|
|
198
|
+
}
|
|
199
|
+
else if (['off', 'false', '0', 'no', 'n', 'disable', 'disabled'].includes(switchArg)) {
|
|
200
|
+
nextEnabled = false;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
layout.contentWrite(`${ui.yellow}${t('memory.usage')}${ui.reset}\n`);
|
|
204
|
+
return next();
|
|
205
|
+
}
|
|
206
|
+
const indexEnabled = isMemoryEnabled();
|
|
207
|
+
const routeAllowed = isMemoryRouteAllowed();
|
|
208
|
+
const alreadyAligned = nextEnabled ? indexEnabled && routeAllowed : !indexEnabled && !routeAllowed;
|
|
209
|
+
if (alreadyAligned) {
|
|
210
|
+
layout.contentWrite(`${ui.dim}${t('memory.unchanged', {
|
|
211
|
+
state: t(nextEnabled ? 'memory.stateEnabled' : 'memory.stateDisabled'),
|
|
212
|
+
})}${ui.reset}\n`);
|
|
213
|
+
return next();
|
|
214
|
+
}
|
|
215
|
+
updateMemoryConfig(nextEnabled);
|
|
216
|
+
updateConfigKey('MEMORY_ENABLED', nextEnabled ? 'true' : 'false');
|
|
217
|
+
// 自动路由读取 env gate;legacy chatTools 同步刷新。
|
|
218
|
+
refreshChatTools();
|
|
219
|
+
ctx.history[0] = { role: 'system', content: ctx.buildSystemMessage(getAgentMode() === 'plan') };
|
|
220
|
+
layout.contentWrite(`${nextEnabled ? ui.green : ui.yellow}${t(nextEnabled ? 'memory.changedOn' : 'memory.changedOff')}${ui.reset}\n`);
|
|
221
|
+
layout.contentWrite(`${ui.dim}${t('memory.saved', {
|
|
222
|
+
path: CONFIG_PATH,
|
|
223
|
+
value: nextEnabled ? 'true' : 'false',
|
|
224
|
+
})}${ui.reset}\n`);
|
|
225
|
+
// 即时刷新 banner;当前已发出的 immutable policy snapshot 不受影响。
|
|
226
|
+
layout.rewriteBanner(bannerLines(ctx.banner()));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
layout.contentWrite(`${ui.red}${t('memory.failed', { error: error instanceof Error ? error.message : String(error) })}${ui.reset}\n`);
|
|
230
|
+
}
|
|
231
|
+
return next();
|
|
232
|
+
},
|
|
233
|
+
];
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { ui } from '../ui/theme.js';
|
|
2
|
+
import { t } from '../i18n/index.js';
|
|
3
|
+
import { DEFAULT_CONTEXT_WINDOW_TOKENS } from '../config/index.js';
|
|
4
|
+
export const PROMPT = '❯ ';
|
|
5
|
+
/**
|
|
6
|
+
* 斜杠命令树(仅用于输入菜单;分发仍走下方 if 链)。
|
|
7
|
+
* 分支节点只负责导航,叶子的 value 保持现有命令文本,因此不破坏命令兼容性。
|
|
8
|
+
*/
|
|
9
|
+
export function buildSlashCommands() {
|
|
10
|
+
const d = (key) => t(key);
|
|
11
|
+
return [
|
|
12
|
+
{ name: '/help', desc: d('commands.help') },
|
|
13
|
+
{ name: '/exit', desc: d('commands.exit') },
|
|
14
|
+
{ name: '/clear', desc: d('commands.clear') },
|
|
15
|
+
{ name: '/context', desc: d('commands.context') },
|
|
16
|
+
{ name: '/skills', desc: d('commands.skills') },
|
|
17
|
+
{ name: '/skill', desc: '执行某个 skill(/skill <name> [args-json])' },
|
|
18
|
+
{ name: '/compact', desc: d('commands.compact') },
|
|
19
|
+
{ name: '/resume', desc: d('commands.sessionResume') },
|
|
20
|
+
{ name: '/sessions', desc: d('commands.sessionBrowse') },
|
|
21
|
+
{ name: '/rollback', desc: d('commands.sessionRollback') },
|
|
22
|
+
{
|
|
23
|
+
name: '/memory',
|
|
24
|
+
desc: d('commands.memory'),
|
|
25
|
+
children: [
|
|
26
|
+
{ name: 'overview', value: '/memory', desc: d('commands.memoryOverview') },
|
|
27
|
+
{ name: 'toggle', value: '/memory_switch', desc: d('commands.memoryToggle') },
|
|
28
|
+
{ name: 'on', value: '/memory_switch on', desc: d('commands.memoryOn') },
|
|
29
|
+
{ name: 'off', value: '/memory_switch off', desc: d('commands.memoryOff') },
|
|
30
|
+
{ name: 'status', value: '/memory_status', desc: d('commands.memoryStatus') },
|
|
31
|
+
{ name: 'reflect', value: '/reflect', desc: d('commands.memoryReflect') },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{ name: '/init', desc: d('commands.memoryInit') },
|
|
35
|
+
{
|
|
36
|
+
name: '/subagent',
|
|
37
|
+
desc: d('commands.subagent'),
|
|
38
|
+
children: [
|
|
39
|
+
{ name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
|
|
40
|
+
{ name: 'off', value: '/subagent off', desc: d('commands.subagentOff') },
|
|
41
|
+
{ name: 'status', value: '/subagent status', desc: d('commands.subagentStatus') },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: '/fe',
|
|
46
|
+
desc: d('commands.fe'),
|
|
47
|
+
children: [
|
|
48
|
+
{ name: 'on', value: '/fe on', desc: d('commands.feOn') },
|
|
49
|
+
{ name: 'off', value: '/fe off', desc: d('commands.feOff') },
|
|
50
|
+
{ name: 'status', value: '/fe status', desc: d('commands.feStatus') },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: '/cu',
|
|
55
|
+
desc: d('commands.cu'),
|
|
56
|
+
children: [
|
|
57
|
+
{ name: 'on', value: '/cu on', desc: d('commands.cuOn') },
|
|
58
|
+
{ name: 'off', value: '/cu off', desc: d('commands.cuOff') },
|
|
59
|
+
{ name: 'status', value: '/cu status', desc: d('commands.cuStatus') },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: '/mcp',
|
|
64
|
+
desc: d('commands.mcp'),
|
|
65
|
+
children: [
|
|
66
|
+
{ name: 'on', value: '/mcp on', desc: d('commands.mcpOn') },
|
|
67
|
+
{ name: 'off', value: '/mcp off', desc: d('commands.mcpOff') },
|
|
68
|
+
{ name: 'status', value: '/mcp status', desc: d('commands.mcpStatus') },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{ name: '/theme', desc: d('commands.theme') },
|
|
72
|
+
{
|
|
73
|
+
name: '/model',
|
|
74
|
+
desc: d('commands.model'),
|
|
75
|
+
children: [
|
|
76
|
+
{ name: 'configure', value: '/model', desc: d('commands.modelConfigure') },
|
|
77
|
+
{ name: 'switch', value: '/model switch', desc: d('commands.modelSwitch') },
|
|
78
|
+
{ name: 'list', value: '/model list', desc: d('commands.modelList') },
|
|
79
|
+
{ name: 'show', value: '/model show', desc: d('commands.modelShow') },
|
|
80
|
+
{ name: 'use <name>', value: '/model use ', submit: false, desc: d('commands.modelUse') },
|
|
81
|
+
{ name: 'delete <name>', value: '/model delete ', submit: false, desc: d('commands.modelDelete') },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: '/mode',
|
|
86
|
+
desc: d('commands.mode'),
|
|
87
|
+
children: [
|
|
88
|
+
{ name: 'plan', value: '/plan', desc: d('commands.modePlan') },
|
|
89
|
+
{ name: 'auto', value: '/auto', desc: d('commands.modeAuto') },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: '/pet',
|
|
94
|
+
desc: d('commands.pet'),
|
|
95
|
+
children: [
|
|
96
|
+
{ name: 'toggle', value: '/pet', desc: d('commands.petToggle') },
|
|
97
|
+
{ name: 'skin', value: '/pet skin', desc: d('commands.petSkin') },
|
|
98
|
+
{ name: 'quit', value: '/pet quit', desc: d('commands.petQuit') },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: '/image',
|
|
103
|
+
desc: d('commands.image'),
|
|
104
|
+
children: [
|
|
105
|
+
{ name: 'attach <path>', value: '/image ', submit: false, desc: d('commands.imageAttach') },
|
|
106
|
+
{ name: 'list', value: '/image list', desc: d('commands.imageList') },
|
|
107
|
+
{ name: 'clear', value: '/image clear', desc: d('commands.imageClear') },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: '/language',
|
|
112
|
+
desc: d('commands.language'),
|
|
113
|
+
children: [
|
|
114
|
+
{ name: 'zh-CN', value: '/language zh-CN', desc: d('commands.languageZh') },
|
|
115
|
+
{ name: 'en', value: '/language en', desc: d('commands.languageEn') },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: '/upgrade',
|
|
120
|
+
desc: d('commands.upgrade'),
|
|
121
|
+
children: [
|
|
122
|
+
{ name: 'now', value: '/upgrade', desc: d('commands.upgradeNow') },
|
|
123
|
+
{ name: 'check', value: '/upgrade check', desc: d('commands.upgradeCheck') },
|
|
124
|
+
{ name: 'status', value: '/upgrade status', desc: d('commands.upgradeStatus') },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
/** 从菜单树递归生成 /help 内容;叶子 value 与菜单路径不同则同时展示真实命令。 */
|
|
130
|
+
export function slashHelpLines(nodes = buildSlashCommands(), parentPath = '', depth = 0) {
|
|
131
|
+
const lines = [];
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
const menuPath = parentPath ? `${parentPath} ${node.name}` : node.name;
|
|
134
|
+
const isBranch = Boolean(node.children?.length);
|
|
135
|
+
const actual = node.value?.trimEnd();
|
|
136
|
+
const mapping = actual && actual !== menuPath ? ` ${ui.dim}→ ${actual}${ui.reset}` : '';
|
|
137
|
+
const marker = isBranch ? ` ${ui.dim}›${ui.reset}` : '';
|
|
138
|
+
lines.push(`${' '.repeat(depth)}${ui.accent}${menuPath}${ui.reset}${marker}${mapping} ${ui.dim}${node.desc}${ui.reset}`);
|
|
139
|
+
if (node.children?.length)
|
|
140
|
+
lines.push(...slashHelpLines(node.children, menuPath, depth + 1));
|
|
141
|
+
}
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
/** chat 常见错误类别 → 中文引导文案的 i18n 键(识别不出的错误不走这张表,保留原始诊断)。 */
|
|
145
|
+
export const LLM_ERROR_HINT_KEYS = {
|
|
146
|
+
auth: 'repl.llmAuthError',
|
|
147
|
+
quota: 'repl.llmQuotaError',
|
|
148
|
+
timeout: 'repl.llmTimeoutError',
|
|
149
|
+
network: 'repl.llmNetworkError',
|
|
150
|
+
context: 'repl.llmContextError',
|
|
151
|
+
};
|
|
152
|
+
/** /help 的分组:按使用场景归组,组内保持菜单树顺序。未列入的顶层命令兜底进「其他」。 */
|
|
153
|
+
export const HELP_GROUPS = [
|
|
154
|
+
{ key: 'help.groupFrequent', names: ['/help', '/clear', '/context', '/compact', '/resume', '/model'] },
|
|
155
|
+
{ key: 'help.groupSession', names: ['/sessions', '/rollback', '/memory', '/skills', '/skill', '/init'] },
|
|
156
|
+
{
|
|
157
|
+
key: 'help.groupConfig',
|
|
158
|
+
names: ['/mode', '/subagent', '/fe', '/mcp', '/theme', '/pet', '/image', '/language', '/upgrade'],
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
/**
|
|
162
|
+
* 菜单树里全部可用命令名(含分支路径与叶子的真实命令),供未知命令纠错建议用。
|
|
163
|
+
* /quit、/frontend 确实可用但没登记进菜单树,单独补上——否则用户照提示打了却仍被判未知。
|
|
164
|
+
*/
|
|
165
|
+
export function knownCommandNames() {
|
|
166
|
+
const out = [];
|
|
167
|
+
const walk = (nodes, parent) => {
|
|
168
|
+
for (const node of nodes) {
|
|
169
|
+
const path = parent ? `${parent} ${node.name}` : node.name;
|
|
170
|
+
out.push(path);
|
|
171
|
+
const actual = node.value?.trim();
|
|
172
|
+
if (actual)
|
|
173
|
+
out.push(actual.split(/\s+/)[0]);
|
|
174
|
+
if (node.children?.length)
|
|
175
|
+
walk(node.children, path);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
walk(buildSlashCommands(), '');
|
|
179
|
+
out.push('/quit', '/frontend');
|
|
180
|
+
return [...new Set(out)];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 是否"长得像一条斜杠命令":单个前导 / + 词字符(字母开头,可含数字/下划线/连字符)。
|
|
184
|
+
* 用来把 /hepl 这类拼写错误与 /tmp/foo、/src/index.ts:12:5 这类路径区分开——
|
|
185
|
+
* 后者在编码 agent 里很常见,同样以 / 开头,绝不能被当成命令拦下。
|
|
186
|
+
*/
|
|
187
|
+
export function isCommandShape(cmd) {
|
|
188
|
+
return /^\/[A-Za-z][\w-]*$/.test(cmd);
|
|
189
|
+
}
|
|
190
|
+
/** Levenshtein 距离(标准 DP)。短命令字符串,直接两行数组滚动即可。 */
|
|
191
|
+
export function editDistance(a, b) {
|
|
192
|
+
if (a === b)
|
|
193
|
+
return 0;
|
|
194
|
+
if (!a.length)
|
|
195
|
+
return b.length;
|
|
196
|
+
if (!b.length)
|
|
197
|
+
return a.length;
|
|
198
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
199
|
+
for (let i = 1; i <= a.length; i++) {
|
|
200
|
+
const cur = [i];
|
|
201
|
+
for (let j = 1; j <= b.length; j++) {
|
|
202
|
+
cur[j] = Math.min(prev[j] + 1, // 删
|
|
203
|
+
cur[j - 1] + 1, // 插
|
|
204
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
205
|
+
}
|
|
206
|
+
prev = cur;
|
|
207
|
+
}
|
|
208
|
+
return prev[b.length];
|
|
209
|
+
}
|
|
210
|
+
/** 未知命令的纠错建议:返回编辑距离最近且在阈值内的命令名,没有则 null(不要瞎猜)。 */
|
|
211
|
+
export function suggestCommand(input) {
|
|
212
|
+
// 阈值随长度放宽,但对短命令保持严格:/hepl(5) vs /help 距离 2 → 命中;
|
|
213
|
+
// /x(2) 不该被建议成任何东西。
|
|
214
|
+
const threshold = Math.max(1, Math.ceil(input.length / 3));
|
|
215
|
+
let best = null;
|
|
216
|
+
let bestDist = Infinity;
|
|
217
|
+
for (const name of knownCommandNames()) {
|
|
218
|
+
if (name === input)
|
|
219
|
+
continue; // 别建议"你自己"(分支节点如 /mode 也在候选里)
|
|
220
|
+
const d = editDistance(input, name);
|
|
221
|
+
if (d < bestDist) {
|
|
222
|
+
bestDist = d;
|
|
223
|
+
best = name;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return bestDist <= threshold ? best : null;
|
|
227
|
+
}
|
|
228
|
+
/** 主题名 → 本地化描述。 */
|
|
229
|
+
export function themeDescription(name) {
|
|
230
|
+
const key = `theme.${name}`;
|
|
231
|
+
return name in
|
|
232
|
+
{
|
|
233
|
+
default: 1,
|
|
234
|
+
light: 1,
|
|
235
|
+
solarized: 1,
|
|
236
|
+
gruvbox: 1,
|
|
237
|
+
nord: 1,
|
|
238
|
+
orange: 1,
|
|
239
|
+
rose: 1,
|
|
240
|
+
emerald: 1,
|
|
241
|
+
amber: 1,
|
|
242
|
+
lavender: 1,
|
|
243
|
+
sunset: 1,
|
|
244
|
+
}
|
|
245
|
+
? t(key)
|
|
246
|
+
: '';
|
|
247
|
+
}
|
|
248
|
+
/** /model 预设后端:同时声明原生协议;旧服务继续走 OpenAI-compatible。 */
|
|
249
|
+
export const MODEL_PRESETS = [
|
|
250
|
+
{
|
|
251
|
+
label: 'Anthropic Claude',
|
|
252
|
+
provider: 'anthropic',
|
|
253
|
+
baseURL: 'https://api.anthropic.com',
|
|
254
|
+
model: 'claude-sonnet-4-5',
|
|
255
|
+
window: 200000,
|
|
256
|
+
anthropicPromptCache: true,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
label: 'GLM(智谱)',
|
|
260
|
+
provider: 'openai',
|
|
261
|
+
baseURL: 'https://open.bigmodel.cn/api/v3',
|
|
262
|
+
model: 'glm-4.6',
|
|
263
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
264
|
+
anthropicPromptCache: false,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
label: 'DeepSeek',
|
|
268
|
+
provider: 'openai',
|
|
269
|
+
baseURL: 'https://api.deepseek.com',
|
|
270
|
+
model: 'deepseek-chat',
|
|
271
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
272
|
+
anthropicPromptCache: false,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
label: 'Qwen(阿里)',
|
|
276
|
+
provider: 'openai',
|
|
277
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
278
|
+
model: 'qwen-plus',
|
|
279
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
280
|
+
anthropicPromptCache: false,
|
|
281
|
+
},
|
|
282
|
+
// MiniMax OpenAI 兼容端点(https://platform.minimax.io/docs/api-reference/text-openai-api)。
|
|
283
|
+
// MiniMax-M3 为唯一支持图片/视频输入的模型;M2 系列纯文本(见 llm/capabilities.ts KNOWN_TEXT_ONLY_PREFIXES)。
|
|
284
|
+
{
|
|
285
|
+
label: 'MiniMax',
|
|
286
|
+
provider: 'openai',
|
|
287
|
+
baseURL: 'https://api.minimax.io/v1',
|
|
288
|
+
model: 'MiniMax-M3',
|
|
289
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
290
|
+
anthropicPromptCache: false,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
label: '本地 Ollama',
|
|
294
|
+
provider: 'openai',
|
|
295
|
+
baseURL: 'http://localhost:11434/v1',
|
|
296
|
+
model: 'qwen2.5:7b',
|
|
297
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
298
|
+
anthropicPromptCache: false,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
label: '本地 vLLM',
|
|
302
|
+
provider: 'openai',
|
|
303
|
+
baseURL: 'http://localhost:8000/v1',
|
|
304
|
+
model: 'default',
|
|
305
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
306
|
+
anthropicPromptCache: false,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
label: '自定义 base_url',
|
|
310
|
+
provider: 'openai',
|
|
311
|
+
baseURL: '',
|
|
312
|
+
model: '',
|
|
313
|
+
window: DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
314
|
+
anthropicPromptCache: false,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
/** apiKey 脱敏:只露末 4 位,前面打星号(显示用,绝不把明文 key 写进内容区)。 */
|
|
318
|
+
export function maskKey(k) {
|
|
319
|
+
if (!k)
|
|
320
|
+
return '(未设置)';
|
|
321
|
+
if (k.length <= 8)
|
|
322
|
+
return '****';
|
|
323
|
+
return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
|
|
324
|
+
}
|