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,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上下文命令组:/context
|
|
3
|
+
*
|
|
4
|
+
* dialog-only:打印一条 renderContextBar 摘要行。
|
|
5
|
+
* 设计意图是"我说了多少"(见 status-bar.ts 的注释),不进状态栏 inline。
|
|
6
|
+
*/
|
|
7
|
+
import * as layout from '../../ui/layout.js';
|
|
8
|
+
import { renderContextBar } from '../status-bar.js';
|
|
9
|
+
import { unhandled, next } from './types.js';
|
|
10
|
+
export const contextCommands = [
|
|
11
|
+
(ctx) => {
|
|
12
|
+
if (ctx.line !== '/context')
|
|
13
|
+
return unhandled();
|
|
14
|
+
layout.contentWrite(` ${renderContextBar(ctx.history)}\n`);
|
|
15
|
+
return next();
|
|
16
|
+
},
|
|
17
|
+
];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图片附件命令组:/image · /image list · /image clear · /image <path>
|
|
3
|
+
*
|
|
4
|
+
* 单 handler 内部按完整 line 分流,顺序与原 if 链一致:
|
|
5
|
+
* 先 list/clear(完整匹配),再落到 <path> 分支。
|
|
6
|
+
*
|
|
7
|
+
* dispatch 阶段**不调 runTurn**:仅 mutate ctx.attachments 状态,
|
|
8
|
+
* 提交时(runTurn 入口)才 flush 进 history。
|
|
9
|
+
*/
|
|
10
|
+
import * as layout from '../../ui/layout.js';
|
|
11
|
+
import { ui } from '../../ui/theme.js';
|
|
12
|
+
import { config } from '../../config/index.js';
|
|
13
|
+
import { loadImageAttachment, renderChip, MAX_INLINE_BYTES_DEFAULT } from '../../attachments/image.js';
|
|
14
|
+
import { modelSupportsVision } from '../../llm/capabilities.js';
|
|
15
|
+
import { unhandled, next } from './types.js';
|
|
16
|
+
export const imageCommands = [
|
|
17
|
+
async (ctx) => {
|
|
18
|
+
const { line } = ctx;
|
|
19
|
+
const isImage = line === '/image' || line === '/image list' || line === '/image clear' || line.startsWith('/image ');
|
|
20
|
+
if (!isImage)
|
|
21
|
+
return unhandled();
|
|
22
|
+
const pending = ctx.attachments.list();
|
|
23
|
+
if (line === '/image list' || (line === '/image' && pending.length > 0)) {
|
|
24
|
+
// 空 /image 视为 list(无歧义;若用户想加图必须 /image <path>)
|
|
25
|
+
if (pending.length === 0) {
|
|
26
|
+
layout.contentWrite(`${ui.dim}(无待发送图片)${ui.reset}\n`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
for (const a of pending) {
|
|
30
|
+
layout.contentWrite(` ${ui.dim}${renderChip(a)}${ui.reset}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return next();
|
|
34
|
+
}
|
|
35
|
+
if (line === '/image clear' || (line === '/image' && pending.length === 0)) {
|
|
36
|
+
// /image 单独输入 + 无 pending:也走 list(空集)
|
|
37
|
+
if (line === '/image' && pending.length === 0) {
|
|
38
|
+
layout.contentWrite(`${ui.dim}(无待发送图片)${ui.reset}\n`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
ctx.attachments.clear();
|
|
42
|
+
layout.contentWrite(`${ui.dim}(已清空待发送图片)${ui.reset}\n`);
|
|
43
|
+
}
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
const arg = line
|
|
47
|
+
.slice('/image'.length)
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/^["']|["']$/g, '');
|
|
50
|
+
if (!arg) {
|
|
51
|
+
layout.contentWrite(`${ui.dim}用法: /image <path>${ui.reset}\n`);
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
54
|
+
const maxBytes = config.maxImageBytes ?? MAX_INLINE_BYTES_DEFAULT;
|
|
55
|
+
const r = await loadImageAttachment(arg, { maxBytes });
|
|
56
|
+
if (!r.ok) {
|
|
57
|
+
layout.contentWrite(`${ui.red}[image] ${r.reason}${ui.reset}\n`);
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
if (!ctx.attachments.list().find((a) => a.id === r.att.id)) {
|
|
61
|
+
ctx.attachments.push(r.att);
|
|
62
|
+
}
|
|
63
|
+
layout.contentWrite(` ${ui.dim}${renderChip(r.att)} — will attach to next message${ui.reset}\n`);
|
|
64
|
+
// 提前警告(不阻断附加):当前模型已知不支持视觉(如 MiniMax M2.x / gpt-3.5 等)时,
|
|
65
|
+
// 附加时就提示,而不是等发送后才在 catch 块里翻译 API 报错——减少一轮无意义请求。
|
|
66
|
+
if (!modelSupportsVision(config.model)) {
|
|
67
|
+
layout.contentWrite(` ${ui.yellow}⚠ 当前模型 ${config.model} 已知不支持视觉输入,发送图片可能会失败。可用 /model 切换。${ui.reset}\n`);
|
|
68
|
+
}
|
|
69
|
+
return next();
|
|
70
|
+
},
|
|
71
|
+
];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆命令组:/memory · /reflect
|
|
3
|
+
*
|
|
4
|
+
* 只读概览 + 后台反思触发。/memory_switch /memory_status 是**配置写入**命令,
|
|
5
|
+
* 与 /subagent /fe /mcp 同属 tool-group,放 tool-group.ts(见该文件),不在这里。
|
|
6
|
+
*/
|
|
7
|
+
import * as layout from '../../ui/layout.js';
|
|
8
|
+
import { ui } from '../../ui/theme.js';
|
|
9
|
+
import { isMemoryEnabled } from '../../config/index.js';
|
|
10
|
+
import { kickoffReflection, snapshotTranscript, loadAll } from '../../memory/index.js';
|
|
11
|
+
import { unhandled, next } from './types.js';
|
|
12
|
+
export const memoryCommands = [
|
|
13
|
+
(ctx) => {
|
|
14
|
+
if (ctx.line !== '/memory')
|
|
15
|
+
return unhandled();
|
|
16
|
+
// 记忆库概览:计数 + 近期 active 索引(详情用 memory_search)。
|
|
17
|
+
const all = loadAll();
|
|
18
|
+
const active = all.filter((e) => e.status === 'active');
|
|
19
|
+
const archived = all.filter((e) => e.status === 'archived').length;
|
|
20
|
+
const byType = {};
|
|
21
|
+
for (const e of active)
|
|
22
|
+
byType[e.type] = (byType[e.type] || 0) + 1;
|
|
23
|
+
layout.contentWrite(`${ui.dim}记忆库:active ${active.length}${archived ? ` · archived ${archived}` : ''}${ui.reset}\n`);
|
|
24
|
+
if (Object.keys(byType).length) {
|
|
25
|
+
layout.contentWrite(`${ui.dim}按类:${Object.entries(byType)
|
|
26
|
+
.map(([t, n]) => `${t} ${n}`)
|
|
27
|
+
.join(' ')}${ui.reset}\n`);
|
|
28
|
+
}
|
|
29
|
+
const recent = [...active].sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || '')).slice(0, 10);
|
|
30
|
+
for (const e of recent) {
|
|
31
|
+
layout.contentWrite(` ${ui.accent}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
|
|
32
|
+
}
|
|
33
|
+
if (active.length === 0)
|
|
34
|
+
layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 AGENTS.md)${ui.reset}\n`);
|
|
35
|
+
layout.contentWrite(`${ui.dim}(详情用 memory_search;启动索引已注入 systemPrompt)${ui.reset}\n`);
|
|
36
|
+
return next();
|
|
37
|
+
},
|
|
38
|
+
(ctx) => {
|
|
39
|
+
if (ctx.line !== '/reflect')
|
|
40
|
+
return unhandled();
|
|
41
|
+
// 记忆子系统总开关关闭时反思无意义(kickoffReflection 内部也会短路),直接提示,不误导用户"已触发"。
|
|
42
|
+
if (!isMemoryEnabled()) {
|
|
43
|
+
layout.contentWrite(`${ui.dim}(记忆子系统已关闭,/reflect 无效。用 /memory_switch 打开后再试)${ui.reset}\n`);
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
// 手动触发后台反思 pass(不等;完成后下次 INPUT 态显摘要)。
|
|
47
|
+
kickoffReflection(snapshotTranscript(ctx.history, 20));
|
|
48
|
+
layout.contentWrite(`${ui.dim}(反思已触发,后台进行;完成后下次输入态显示摘要。日志见 .mocode/memory.log)${ui.reset}\n`);
|
|
49
|
+
return next();
|
|
50
|
+
},
|
|
51
|
+
];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模式命令组:/plan · /auto · /mode
|
|
3
|
+
*
|
|
4
|
+
* 三者条件互斥(都是完整 line 精确匹配),组内顺序无关。
|
|
5
|
+
* setAgentMode 触发 runtime 注册的 onModeChange listener,由它统一重写 history[0] +
|
|
6
|
+
* 刷状态行 modeTag;这里只负责切状态和回显文案,不碰 history。
|
|
7
|
+
*/
|
|
8
|
+
import * as layout from '../../ui/layout.js';
|
|
9
|
+
import { ui } from '../../ui/theme.js';
|
|
10
|
+
import { t } from '../../i18n/index.js';
|
|
11
|
+
import { getAgentMode, setAgentMode } from '../../agent/mode.js';
|
|
12
|
+
import { unhandled, next } from './types.js';
|
|
13
|
+
export const modeCommands = [
|
|
14
|
+
// /plan:切到 plan 模式(只读探查 + 产出计划)。
|
|
15
|
+
(ctx) => {
|
|
16
|
+
if (ctx.line !== '/plan')
|
|
17
|
+
return unhandled();
|
|
18
|
+
if (getAgentMode() === 'plan') {
|
|
19
|
+
layout.contentWrite(`${ui.dim}${t('repl.planAlready')}${ui.reset}\n`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
setAgentMode('plan');
|
|
23
|
+
layout.contentWrite(`${ui.dim}${t('repl.planChanged')}${ui.reset}\n`);
|
|
24
|
+
}
|
|
25
|
+
return next();
|
|
26
|
+
},
|
|
27
|
+
(ctx) => {
|
|
28
|
+
if (ctx.line !== '/auto')
|
|
29
|
+
return unhandled();
|
|
30
|
+
if (getAgentMode() === 'auto') {
|
|
31
|
+
layout.contentWrite(`${ui.dim}${t('repl.autoAlready')}${ui.reset}\n`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
setAgentMode('auto');
|
|
35
|
+
layout.contentWrite(`${ui.dim}${t('repl.autoChanged')}${ui.reset}\n`);
|
|
36
|
+
}
|
|
37
|
+
return next();
|
|
38
|
+
},
|
|
39
|
+
// /mode:菜单里的分支节点(本身不切换,只挂 /plan 与 /auto)。/help 会把它列出来,
|
|
40
|
+
// 所以直接敲它必须给点有用的东西——打印当前模式与两个子命令,而不是报未知命令。
|
|
41
|
+
(ctx) => {
|
|
42
|
+
if (ctx.line !== '/mode')
|
|
43
|
+
return unhandled();
|
|
44
|
+
layout.contentWrite(`${ui.dim}${t('repl.modeCurrent', { mode: getAgentMode() })}${ui.reset}\n`);
|
|
45
|
+
return next();
|
|
46
|
+
},
|
|
47
|
+
];
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型命令组:/model(无参向导)· /model switch · /model list(presets) · /model show ·
|
|
3
|
+
* /model use <name> · /model delete <name>
|
|
4
|
+
*
|
|
5
|
+
* 运行时配置大模型(baseURL/apiKey/model/contextWindowTokens):
|
|
6
|
+
* 即时生效(updateModelConfig 改内存 + env,reconfigureClient 重建 OpenAI 实例)+
|
|
7
|
+
* 持久化(writeConfigKeys 写 ~/.mocode/config)。
|
|
8
|
+
* 仿 /theme:promptIntervention 弹菜单/输入 → 改 config → refreshStatusBase 刷底栏 →
|
|
9
|
+
* clearContent+banner 重显横幅 → dim 警告(shell env 覆盖)。
|
|
10
|
+
*
|
|
11
|
+
* 迁移说明:原分支里的全部 `continue`(取消/校验失败路径)改为 `return next()`,
|
|
12
|
+
* 语义一致(回 INPUT 态);两个内嵌闭包 applyPresetAndPersist / uniquePresetName
|
|
13
|
+
* 原样保留——前者依赖 ctx(history/banner/refreshStatusBase),后者是纯函数提到模块级。
|
|
14
|
+
*/
|
|
15
|
+
import * as layout from '../../ui/layout.js';
|
|
16
|
+
import { ui } from '../../ui/theme.js';
|
|
17
|
+
import { bannerLines } from '../../ui/render.js';
|
|
18
|
+
import { config, updateModelConfig } from '../../config/index.js';
|
|
19
|
+
import { writeConfigKeys, CONFIG_PATH } from '../../config/file.js';
|
|
20
|
+
import { deletePreset, getPreset, isValidPresetName, listPresets, savePreset, setActivePresetName, } from '../../config/presets.js';
|
|
21
|
+
import { reconfigureClient } from '../../llm/index.js';
|
|
22
|
+
import { promptIntervention } from '../../ui/intervention.js';
|
|
23
|
+
import { renderHistory } from '../message-format.js';
|
|
24
|
+
import { MODEL_PRESETS, maskKey } from '../commands.js';
|
|
25
|
+
import { unhandled, next } from './types.js';
|
|
26
|
+
/** 决定自动存的预设名:协议、缓存配置和连接四元组都一致时不重复存。 */
|
|
27
|
+
function uniquePresetName(desired, provider, baseURL, apiKey, model, contextWindow, anthropicPromptCache) {
|
|
28
|
+
const existing = listPresets();
|
|
29
|
+
const sameEntry = existing.find((p) => p.provider === provider &&
|
|
30
|
+
p.baseURL === baseURL &&
|
|
31
|
+
p.apiKey === apiKey &&
|
|
32
|
+
p.model === model &&
|
|
33
|
+
p.contextWindow === contextWindow &&
|
|
34
|
+
p.anthropicPromptCache === anthropicPromptCache);
|
|
35
|
+
if (sameEntry)
|
|
36
|
+
return null;
|
|
37
|
+
const sanitized = desired
|
|
38
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '')
|
|
40
|
+
.slice(0, 32) || 'preset';
|
|
41
|
+
const base = isValidPresetName(sanitized) ? sanitized : 'preset';
|
|
42
|
+
if (!existing.some((p) => p.name === base))
|
|
43
|
+
return base;
|
|
44
|
+
for (let i = 2; i < 1000; i++) {
|
|
45
|
+
const candidate = `${base}-${i}`;
|
|
46
|
+
if (candidate.length > 32)
|
|
47
|
+
return `${base.slice(0, 32 - String(i).length - 1)}-${i}`;
|
|
48
|
+
if (!existing.some((p) => p.name === candidate))
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
return `${base}-${Date.now()}`;
|
|
52
|
+
}
|
|
53
|
+
export const modelCommands = [
|
|
54
|
+
async (ctx) => {
|
|
55
|
+
const { line } = ctx;
|
|
56
|
+
if (line !== '/model' && !line.startsWith('/model '))
|
|
57
|
+
return unhandled();
|
|
58
|
+
const arg = line.startsWith('/model ') ? line.slice('/model '.length).trim() : '';
|
|
59
|
+
const { history } = ctx;
|
|
60
|
+
// 共用:apply 一个预设到 config + 持久化 + 重建 client + 重显横幅。无参 /model 选菜单和 /model use 都走这里。
|
|
61
|
+
const applyPresetAndPersist = (target) => {
|
|
62
|
+
updateModelConfig({
|
|
63
|
+
provider: target.provider,
|
|
64
|
+
model: target.model,
|
|
65
|
+
baseURL: target.baseURL,
|
|
66
|
+
apiKey: target.apiKey,
|
|
67
|
+
contextWindowTokens: target.contextWindow,
|
|
68
|
+
anthropicPromptCache: target.anthropicPromptCache,
|
|
69
|
+
});
|
|
70
|
+
writeConfigKeys({
|
|
71
|
+
LLM_PROVIDER: target.provider,
|
|
72
|
+
LLM_BASE_URL: target.baseURL,
|
|
73
|
+
LLM_API_KEY: target.apiKey,
|
|
74
|
+
LLM_MODEL: target.model,
|
|
75
|
+
CONTEXT_WINDOW_TOKENS: String(target.contextWindow),
|
|
76
|
+
ANTHROPIC_PROMPT_CACHE: target.anthropicPromptCache ? 'true' : 'false',
|
|
77
|
+
});
|
|
78
|
+
reconfigureClient();
|
|
79
|
+
// 记为激活预设:让上下文窗口等配置从此跟随该预设文件(下次启动也用它,不再回退 config 裸键)。
|
|
80
|
+
try {
|
|
81
|
+
setActivePresetName(target.name);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* 指针写失败不阻断切换 */
|
|
85
|
+
}
|
|
86
|
+
ctx.refreshStatusBase(history);
|
|
87
|
+
layout.clearContent();
|
|
88
|
+
if (history.some((m) => m.role === 'user')) {
|
|
89
|
+
renderHistory(history);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
layout.writeBanner(bannerLines(ctx.banner()));
|
|
93
|
+
}
|
|
94
|
+
const cacheLabel = target.provider === 'anthropic' ? ` · Prompt Cache ${target.anthropicPromptCache ? 'on' : 'off'}` : '';
|
|
95
|
+
layout.contentWrite(`${ui.dim}(已切换到预设 “${target.name}” → ${target.model} · ${target.provider}${cacheLabel} · 窗口 ${target.contextWindow} @ ${target.baseURL})${ui.reset}\n`);
|
|
96
|
+
if (config.llmKeysFromShell.length > 0) {
|
|
97
|
+
layout.contentWrite(`${ui.dim}(shell 环境变量已设 ${config.llmKeysFromShell.join(' / ')},优先级最高,下次启动会盖掉预设的对应字段;预设仍记为激活,取消 shell 设置后恢复跟随)${ui.reset}\n`);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
// /model switch:弹 ↑↓·Enter 菜单挑预设切换。无预设时给一行引导。
|
|
101
|
+
if (arg === 'switch') {
|
|
102
|
+
const presets = listPresets();
|
|
103
|
+
if (presets.length === 0) {
|
|
104
|
+
layout.contentWrite(`${ui.dim}(还没有预设;先跑 /model 添加一个)${ui.reset}\n`);
|
|
105
|
+
return next();
|
|
106
|
+
}
|
|
107
|
+
const isCurrent = (p) => p.provider === config.provider &&
|
|
108
|
+
p.baseURL === config.baseURL &&
|
|
109
|
+
p.apiKey === config.apiKey &&
|
|
110
|
+
p.model === config.model &&
|
|
111
|
+
p.contextWindow === config.contextWindowTokens &&
|
|
112
|
+
p.anthropicPromptCache === (config.provider === 'anthropic' && config.anthropicPromptCache);
|
|
113
|
+
const cols = layout.getGeo().cols;
|
|
114
|
+
const labelFor = (p) => {
|
|
115
|
+
const tag = isCurrent(p) ? ' ★current' : '';
|
|
116
|
+
const cache = p.provider === 'anthropic' ? ` · cache ${p.anthropicPromptCache ? 'on' : 'off'}` : '';
|
|
117
|
+
const right = `${p.provider}${cache} · ${p.model} @ ${p.baseURL}`;
|
|
118
|
+
const left = `${p.name}${tag}`;
|
|
119
|
+
const sep = left.length + 1 + right.length;
|
|
120
|
+
if (sep <= cols - 2)
|
|
121
|
+
return `${left} ${ui.dim}${right}${ui.reset}`;
|
|
122
|
+
return left;
|
|
123
|
+
};
|
|
124
|
+
const choice = await promptIntervention({
|
|
125
|
+
type: 'choice',
|
|
126
|
+
title: '切换模型预设',
|
|
127
|
+
detail: `当前: ${config.provider} · ${config.model} @ ${config.baseURL}(★ = 已匹配)`,
|
|
128
|
+
options: presets.map(labelFor),
|
|
129
|
+
allowCustom: false, // 纯切换,不需要「其他」干扰
|
|
130
|
+
});
|
|
131
|
+
if (choice.action === 'selected' && choice.value) {
|
|
132
|
+
// 精确匹配 labelFor 生成的完整选项串,避免 name 前缀误命中
|
|
133
|
+
// (如 'qwen3-8-27b' 是 'qwen3-8-27b-2' 的前缀,排序在前会抢中,应用错 contextWindow)。
|
|
134
|
+
const target = presets.find((p) => labelFor(p) === choice.value);
|
|
135
|
+
if (target)
|
|
136
|
+
applyPresetAndPersist(target);
|
|
137
|
+
}
|
|
138
|
+
return next();
|
|
139
|
+
}
|
|
140
|
+
// /model list:列已配置的预设(★ 标当前);无预设给一行引导。
|
|
141
|
+
// /model presets 是同义别名(老用户习惯)。
|
|
142
|
+
if (arg === 'list' || arg === 'presets') {
|
|
143
|
+
const ps = listPresets();
|
|
144
|
+
if (ps.length === 0) {
|
|
145
|
+
layout.contentWrite(`${ui.dim}(还没有预设;先跑 /model 添加一个)${ui.reset}\n`);
|
|
146
|
+
return next();
|
|
147
|
+
}
|
|
148
|
+
layout.contentWrite(`${ui.dim}已配置 ${ps.length} 个预设:${ui.reset}\n`);
|
|
149
|
+
for (const p of ps) {
|
|
150
|
+
const current = p.provider === config.provider &&
|
|
151
|
+
p.baseURL === config.baseURL &&
|
|
152
|
+
p.apiKey === config.apiKey &&
|
|
153
|
+
p.model === config.model &&
|
|
154
|
+
p.contextWindow === config.contextWindowTokens &&
|
|
155
|
+
p.anthropicPromptCache === (config.provider === 'anthropic' && config.anthropicPromptCache);
|
|
156
|
+
const star = current ? ' ★' : '';
|
|
157
|
+
const cache = p.provider === 'anthropic' ? ` · cache ${p.anthropicPromptCache ? 'on' : 'off'}` : '';
|
|
158
|
+
layout.contentWrite(` ${ui.accent}${p.name}${ui.reset}${star} ${ui.dim}${p.provider}${cache} · ${p.model} @ ${p.baseURL}${ui.reset}\n`);
|
|
159
|
+
}
|
|
160
|
+
layout.contentWrite(`${ui.dim}(★ = 与当前协议及缓存配置一致;切换用 /model switch)${ui.reset}\n`);
|
|
161
|
+
return next();
|
|
162
|
+
}
|
|
163
|
+
// /model show:显示当前协议、连接与缓存配置(apiKey 脱敏)。
|
|
164
|
+
if (arg === 'show') {
|
|
165
|
+
layout.contentWrite(`${ui.dim}当前模型配置:${ui.reset}\n`);
|
|
166
|
+
layout.contentWrite(` ${ui.accent}provider${ui.reset} ${config.provider}\n`);
|
|
167
|
+
layout.contentWrite(` ${ui.accent}baseURL ${ui.reset} ${config.baseURL}\n`);
|
|
168
|
+
layout.contentWrite(` ${ui.accent}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
|
|
169
|
+
layout.contentWrite(` ${ui.accent}model ${ui.reset} ${config.model}\n`);
|
|
170
|
+
layout.contentWrite(` ${ui.accent}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
|
|
171
|
+
if (config.provider === 'anthropic') {
|
|
172
|
+
layout.contentWrite(` ${ui.accent}缓存 ${ui.reset} Prompt Cache ${config.anthropicPromptCache ? 'on' : 'off'}\n`);
|
|
173
|
+
}
|
|
174
|
+
layout.contentWrite(`${ui.dim}(配置文件: ${CONFIG_PATH})${ui.reset}\n`);
|
|
175
|
+
return next();
|
|
176
|
+
}
|
|
177
|
+
// /model use <name>:一键把预设应用到 config + 持久化 + 重建 client。
|
|
178
|
+
if (arg.startsWith('use ')) {
|
|
179
|
+
const name = arg.slice(4).trim();
|
|
180
|
+
if (!name) {
|
|
181
|
+
layout.contentWrite(`${ui.yellow}用法: /model use <name>${ui.reset}\n`);
|
|
182
|
+
return next();
|
|
183
|
+
}
|
|
184
|
+
if (!isValidPresetName(name)) {
|
|
185
|
+
layout.contentWrite(`${ui.yellow}非法名字: ${name}(仅允许 [a-zA-Z0-9_-]{1,32})${ui.reset}\n`);
|
|
186
|
+
return next();
|
|
187
|
+
}
|
|
188
|
+
let preset;
|
|
189
|
+
try {
|
|
190
|
+
preset = getPreset(name);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
if (e.code === 'ENOENT') {
|
|
194
|
+
layout.contentWrite(`${ui.yellow}没有预设 “${name}”;先 /model save ${name}${ui.reset}\n`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
layout.contentWrite(`${ui.red}/model use 失败: ${e.message}${ui.reset}\n`);
|
|
198
|
+
}
|
|
199
|
+
return next();
|
|
200
|
+
}
|
|
201
|
+
applyPresetAndPersist(preset);
|
|
202
|
+
return next();
|
|
203
|
+
}
|
|
204
|
+
// /model delete <name>:删一个预设。无参 / 重名 / 非法名字分别给不同提示。
|
|
205
|
+
if (arg.startsWith('delete ')) {
|
|
206
|
+
const name = arg.slice(7).trim();
|
|
207
|
+
if (!name) {
|
|
208
|
+
layout.contentWrite(`${ui.yellow}用法: /model delete <name>${ui.reset}\n`);
|
|
209
|
+
return next();
|
|
210
|
+
}
|
|
211
|
+
if (!isValidPresetName(name)) {
|
|
212
|
+
layout.contentWrite(`${ui.yellow}非法名字: ${name}(仅允许 [a-zA-Z0-9_-]{1,32})${ui.reset}\n`);
|
|
213
|
+
return next();
|
|
214
|
+
}
|
|
215
|
+
if (deletePreset(name)) {
|
|
216
|
+
layout.contentWrite(`${ui.dim}已删除预设 “${name}”${ui.reset}\n`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
layout.contentWrite(`${ui.yellow}没有预设 “${name}”${ui.reset}\n`);
|
|
220
|
+
}
|
|
221
|
+
return next();
|
|
222
|
+
}
|
|
223
|
+
// /model 后跟了未知子命令 → 给一行简短用法提示,免得静默吞用户输入。
|
|
224
|
+
// arg === '' → 直接进向导(下方 4 步链);这里只兜底非法子命令。
|
|
225
|
+
if (arg !== '') {
|
|
226
|
+
layout.contentWrite(`${ui.yellow}未知子命令: ${arg}${ui.reset}\n` +
|
|
227
|
+
`${ui.dim}用法: /model(配置新模型向导) · /model switch · /model list · /model delete <name>${ui.reset}\n`);
|
|
228
|
+
return next();
|
|
229
|
+
}
|
|
230
|
+
// /model 无参 → 直接进入「配置新模型」4 步向导(不弹动作菜单)。
|
|
231
|
+
// 切换 / 查看 / 删除已配置预设改用显式子命令:/model switch · /model list · /model delete <name>。
|
|
232
|
+
// 1) 选 provider 预设(预填 baseURL,后续仍可逐项改)。
|
|
233
|
+
let preset;
|
|
234
|
+
try {
|
|
235
|
+
const res = await promptIntervention({
|
|
236
|
+
type: 'choice',
|
|
237
|
+
title: '选择后端预设(预填 baseURL,后续可改)',
|
|
238
|
+
detail: '选一个会预填 baseURL/model/窗口,之后逐项确认。选「自定义」全部手填。',
|
|
239
|
+
options: MODEL_PRESETS.map((p) => p.label),
|
|
240
|
+
});
|
|
241
|
+
if (res.action === 'cancelled') {
|
|
242
|
+
return next();
|
|
243
|
+
}
|
|
244
|
+
const idx = MODEL_PRESETS.findIndex((p) => p.label === res.value);
|
|
245
|
+
if (idx === -1) {
|
|
246
|
+
return next();
|
|
247
|
+
}
|
|
248
|
+
preset = MODEL_PRESETS[idx];
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return next(); // Ctrl+C
|
|
252
|
+
}
|
|
253
|
+
// 1.5) 一键应用确认:非「自定义」预设(带预填值)给直接应用入口,免连按 4 次回车。
|
|
254
|
+
// 直接应用 = 用预设 model/baseURL/window + 保留当前 apiKey(等价于下方逐项链连按回车)。
|
|
255
|
+
// 逐项修改 / 自定义输入(promptIntervention choice 自动追加的「其他」项 submitted)→ 回落 4 步链。
|
|
256
|
+
// 「自定义」预设字段空,跳过确认直接进链。
|
|
257
|
+
let quickApply = false;
|
|
258
|
+
if (preset.model || preset.baseURL) {
|
|
259
|
+
try {
|
|
260
|
+
const res = await promptIntervention({
|
|
261
|
+
type: 'choice',
|
|
262
|
+
title: `应用 ${preset.label}?`,
|
|
263
|
+
detail: `model ${preset.model}\nbaseURL ${preset.baseURL}\napiKey ${maskKey(config.apiKey)}(直接应用=保留当前)\n窗口 ${preset.window}`,
|
|
264
|
+
options: ['直接应用', '逐项修改'],
|
|
265
|
+
allowCustom: false,
|
|
266
|
+
});
|
|
267
|
+
if (res.action === 'cancelled') {
|
|
268
|
+
return next();
|
|
269
|
+
}
|
|
270
|
+
if (res.action === 'selected' && res.value === '直接应用') {
|
|
271
|
+
quickApply = true;
|
|
272
|
+
}
|
|
273
|
+
// 其余(逐项修改 / 自定义输入 submitted)→ quickApply 保持 false,走下方逐项链
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return next(); // Ctrl+C
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// 2) 收集 baseURL / apiKey / model / contextWindowTokens。
|
|
280
|
+
// quickApply:直接取预设值 + 当前 apiKey;否则逐项 input(预填 preset 值,回车=采纳;apiKey 不预填明文,回车=保留旧值)。
|
|
281
|
+
let baseURL;
|
|
282
|
+
let apiKey;
|
|
283
|
+
let model;
|
|
284
|
+
let window;
|
|
285
|
+
if (quickApply) {
|
|
286
|
+
baseURL = preset.baseURL;
|
|
287
|
+
apiKey = config.apiKey;
|
|
288
|
+
model = preset.model;
|
|
289
|
+
window = preset.window;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// baseURL
|
|
293
|
+
{
|
|
294
|
+
const res = await promptIntervention({
|
|
295
|
+
type: 'input',
|
|
296
|
+
title: 'LLM_BASE_URL',
|
|
297
|
+
detail: 'OpenAI 兼容 API 端点。回车采纳预填值。',
|
|
298
|
+
seed: preset.baseURL || config.baseURL,
|
|
299
|
+
});
|
|
300
|
+
if (res.action === 'cancelled') {
|
|
301
|
+
return next();
|
|
302
|
+
}
|
|
303
|
+
baseURL = (res.value ?? '').trim() || preset.baseURL || config.baseURL;
|
|
304
|
+
}
|
|
305
|
+
if (!baseURL) {
|
|
306
|
+
layout.contentWrite(`${ui.yellow}baseURL 不能为空,已取消。${ui.reset}\n`);
|
|
307
|
+
return next();
|
|
308
|
+
}
|
|
309
|
+
// apiKey(不预填明文:回车=保留旧值,输入新值=覆盖)
|
|
310
|
+
{
|
|
311
|
+
const res = await promptIntervention({
|
|
312
|
+
type: 'input',
|
|
313
|
+
title: 'LLM_API_KEY',
|
|
314
|
+
detail: `回车保留当前 ${maskKey(config.apiKey)};输入新值则覆盖。`,
|
|
315
|
+
seed: '',
|
|
316
|
+
});
|
|
317
|
+
if (res.action === 'cancelled') {
|
|
318
|
+
return next();
|
|
319
|
+
}
|
|
320
|
+
const v = (res.value ?? '').trim();
|
|
321
|
+
apiKey = v || config.apiKey;
|
|
322
|
+
}
|
|
323
|
+
if (!apiKey) {
|
|
324
|
+
layout.contentWrite(`${ui.yellow}apiKey 不能为空,已取消。${ui.reset}\n`);
|
|
325
|
+
return next();
|
|
326
|
+
}
|
|
327
|
+
// model
|
|
328
|
+
{
|
|
329
|
+
const res = await promptIntervention({
|
|
330
|
+
type: 'input',
|
|
331
|
+
title: 'LLM_MODEL',
|
|
332
|
+
detail: '模型名(须支持 function calling)。回车采纳预填值。',
|
|
333
|
+
seed: preset.model || config.model,
|
|
334
|
+
});
|
|
335
|
+
if (res.action === 'cancelled') {
|
|
336
|
+
return next();
|
|
337
|
+
}
|
|
338
|
+
model = (res.value ?? '').trim() || preset.model || config.model;
|
|
339
|
+
}
|
|
340
|
+
if (!model) {
|
|
341
|
+
layout.contentWrite(`${ui.yellow}model 不能为空,已取消。${ui.reset}\n`);
|
|
342
|
+
return next();
|
|
343
|
+
}
|
|
344
|
+
// contextWindowTokens
|
|
345
|
+
{
|
|
346
|
+
const res = await promptIntervention({
|
|
347
|
+
type: 'input',
|
|
348
|
+
title: 'CONTEXT_WINDOW_TOKENS',
|
|
349
|
+
detail: '模型上下文窗口,全局默认 256k;如需不同窗口可手动覆盖。回车采纳预填值。',
|
|
350
|
+
seed: String(preset.window || config.contextWindowTokens),
|
|
351
|
+
});
|
|
352
|
+
if (res.action === 'cancelled') {
|
|
353
|
+
return next();
|
|
354
|
+
}
|
|
355
|
+
const v = (res.value ?? '').trim();
|
|
356
|
+
const n = Number(v);
|
|
357
|
+
if (!v || !Number.isFinite(n) || n <= 0) {
|
|
358
|
+
// 非法输入:保留旧值,不阻断(用 preset.window 或当前值兜底)
|
|
359
|
+
window = preset.window || config.contextWindowTokens;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
window = Math.floor(n);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// 3) 应用协议、连接与缓存配置;Anthropic 原生协议无需重建 OpenAI client,但统一刷新无害。
|
|
367
|
+
const provider = preset.provider;
|
|
368
|
+
const anthropicPromptCache = provider === 'anthropic' && preset.anthropicPromptCache;
|
|
369
|
+
updateModelConfig({
|
|
370
|
+
provider,
|
|
371
|
+
model,
|
|
372
|
+
baseURL,
|
|
373
|
+
apiKey,
|
|
374
|
+
contextWindowTokens: window,
|
|
375
|
+
anthropicPromptCache,
|
|
376
|
+
});
|
|
377
|
+
writeConfigKeys({
|
|
378
|
+
LLM_PROVIDER: provider,
|
|
379
|
+
LLM_BASE_URL: baseURL,
|
|
380
|
+
LLM_API_KEY: apiKey,
|
|
381
|
+
LLM_MODEL: model,
|
|
382
|
+
CONTEXT_WINDOW_TOKENS: String(window),
|
|
383
|
+
ANTHROPIC_PROMPT_CACHE: anthropicPromptCache ? 'true' : 'false',
|
|
384
|
+
});
|
|
385
|
+
reconfigureClient();
|
|
386
|
+
// 3.5) 自动存为命名预设;协议与缓存策略也是去重键的一部分。
|
|
387
|
+
let savedName = null;
|
|
388
|
+
try {
|
|
389
|
+
const finalName = uniquePresetName(model, provider, baseURL, apiKey, model, window, anthropicPromptCache);
|
|
390
|
+
if (finalName) {
|
|
391
|
+
savePreset({
|
|
392
|
+
name: finalName,
|
|
393
|
+
provider,
|
|
394
|
+
baseURL,
|
|
395
|
+
apiKey,
|
|
396
|
+
model,
|
|
397
|
+
contextWindow: window,
|
|
398
|
+
anthropicPromptCache,
|
|
399
|
+
});
|
|
400
|
+
savedName = finalName;
|
|
401
|
+
}
|
|
402
|
+
// 记为激活预设(新存或复用同名都记),让窗口跟随该预设文件。
|
|
403
|
+
if (savedName) {
|
|
404
|
+
try {
|
|
405
|
+
setActivePresetName(savedName);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
/* 指针写失败不阻断 */
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch (e) {
|
|
413
|
+
layout.contentWrite(`${ui.red}保存预设失败: ${e.message}${ui.reset}\n`);
|
|
414
|
+
}
|
|
415
|
+
// 4) 刷新 UI:底栏模型名 + 重显横幅(banner() 闭包实时读 config,自动反映新值)。
|
|
416
|
+
ctx.refreshStatusBase(history);
|
|
417
|
+
layout.clearContent();
|
|
418
|
+
if (history.some((m) => m.role === 'user')) {
|
|
419
|
+
renderHistory(history);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
layout.writeBanner(bannerLines(ctx.banner()));
|
|
423
|
+
}
|
|
424
|
+
const cacheLabel = provider === 'anthropic' ? ` · Prompt Cache ${anthropicPromptCache ? 'on' : 'off'}` : '';
|
|
425
|
+
layout.contentWrite(`${ui.dim}(已切换模型 → ${model} · ${provider}${cacheLabel} @ ${baseURL})${ui.reset}\n`);
|
|
426
|
+
if (savedName) {
|
|
427
|
+
layout.contentWrite(`${ui.dim}(已保存为预设 “${savedName}”,下次 /model use ${savedName} 一键切回)${ui.reset}\n`);
|
|
428
|
+
}
|
|
429
|
+
// 5) dim 警告:shell export 的 LLM 键下次启动会覆盖文件值。
|
|
430
|
+
if (config.llmKeysFromShell.length > 0) {
|
|
431
|
+
layout.contentWrite(`${ui.dim}(shell 环境变量已设 ${config.llmKeysFromShell.join(' / ')},文件写入下次启动被其覆盖;取消该 shell 设置后生效)${ui.reset}\n`);
|
|
432
|
+
}
|
|
433
|
+
return next();
|
|
434
|
+
},
|
|
435
|
+
];
|