mingdao-harness 0.2.4 → 0.2.6
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/docs/EVALUATION-ROUND2.md +58 -0
- package/docs/ROADMAP-NEXT.md +5 -5
- package/package.json +2 -2
- package/src/agent.js +74 -36
- package/src/atomic-write.js +8 -2
- package/src/audit.js +2 -2
- package/src/batch.js +7 -7
- package/src/cachestats.js +9 -9
- package/src/cli.js +42 -688
- package/src/commands/desktop.js +1 -1
- package/src/commands/key.js +1 -1
- package/src/commands/repl.js +711 -0
- package/src/commands/schedule.js +7 -7
- package/src/commands/skill.js +18 -16
- package/src/commands/sync.js +5 -5
- package/src/commands/update.js +7 -6
- package/src/commands/workspace.js +2 -2
- package/src/compact.js +6 -6
- package/src/config.js +7 -4
- package/src/context.js +37 -21
- package/src/cost-guard.js +14 -6
- package/src/credentials.js +6 -0
- package/src/hooks.js +11 -11
- package/src/mcp-presets.js +5 -5
- package/src/mcp.js +19 -19
- package/src/memory.js +16 -16
- package/src/model-discovery.js +16 -15
- package/src/models.js +4 -4
- package/src/notify.js +2 -2
- package/src/permissions.js +7 -3
- package/src/pricing.js +35 -1
- package/src/prompts.js +1 -1
- package/src/providers/index.js +7 -7
- package/src/providers/openai-compatible.js +13 -11
- package/src/routing.js +8 -8
- package/src/schedule.js +30 -30
- package/src/session-index.js +5 -1
- package/src/session.js +11 -2
- package/src/skill-lib.js +62 -3
- package/src/skill-registry.js +10 -9
- package/src/skills.js +14 -12
- package/src/sync-server.js +86 -3
- package/src/sync.js +53 -20
- package/src/tasks/worker.js +126 -0
- package/src/tasks.js +14 -14
- package/src/titles.js +5 -5
- package/src/tokenizer.js +23 -5
- package/src/tools/bash.js +12 -12
- package/src/tools/fs-tools.js +66 -9
- package/src/tools/index.js +7 -7
- package/src/ui.js +83 -6
- package/src/update.js +8 -8
- package/src/web/app.js +16 -5
- package/src/web/attachments.js +1 -1
- package/src/web/index.html +21 -1
- package/src/web/routes/api.js +38 -786
- package/src/web/routes/domains/config.js +280 -0
- package/src/web/routes/domains/misc.js +129 -0
- package/src/web/routes/domains/schedule.js +127 -0
- package/src/web/routes/domains/sessions.js +105 -0
- package/src/web/routes/domains/skills.js +79 -0
- package/src/web/routes/domains/sync.js +101 -0
- package/src/web/routes/domains/workspace.js +119 -0
- package/src/web/server.js +73 -31
- package/src/web/web-io.js +54 -12
- package/src/workspace.js +14 -14
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
// 交互式 TUI REPL(Phase C C2:自 cli.js 抽取)。
|
|
2
|
+
// 会话主循环:斜杠命令 + 多行输入 + 回合执行 + 自动压缩/标题/记忆/同步。
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import { loadConfig, saveConfig, runWizard, ensureHome, mingdaoHome } from '../config.js';
|
|
7
|
+
import { modelPreset, PROVIDERS } from '../models.js';
|
|
8
|
+
import { maskKey, getStoredKey } from '../credentials.js';
|
|
9
|
+
import { createProvider, resolveProviderConfig, helperProvider } from '../providers/index.js';
|
|
10
|
+
import { compactConversation } from '../compact.js';
|
|
11
|
+
import { makeTokenCounter } from '../tokenizer.js';
|
|
12
|
+
import { messageTokens } from '../context.js';
|
|
13
|
+
import { startMcpServers } from '../mcp.js';
|
|
14
|
+
import { startTask, listTasks, patchTask, killTask, formatTaskRow } from '../tasks.js';
|
|
15
|
+
import { enableAutostart, disableAutostart, autostartStatus, autostartPath } from '../autostart.js';
|
|
16
|
+
import { notifyTaskDone } from '../notify.js';
|
|
17
|
+
import { addWorkspace, removeWorkspace, workspacePath, touchWorkspace, listWorkspaces, currentWorkspace } from '../workspace.js';
|
|
18
|
+
import { finalizeSession, extractMemory, loadMemory, appendMemory, recentJournal, dedupeMemory, removeMemoryLines } from '../memory.js';
|
|
19
|
+
import { recordUsage, listCacheStats, summarizeCacheStats, formatCacheSummary, costBreakdown } from '../cachestats.js';
|
|
20
|
+
import { presetList, buildPreset } from '../mcp-presets.js';
|
|
21
|
+
import {
|
|
22
|
+
addSchedule,
|
|
23
|
+
listSchedules,
|
|
24
|
+
readSchedule,
|
|
25
|
+
removeSchedule,
|
|
26
|
+
pauseSchedule,
|
|
27
|
+
resumeSchedule,
|
|
28
|
+
chainSchedules,
|
|
29
|
+
reconcileSchedules,
|
|
30
|
+
runSleeper,
|
|
31
|
+
formatScheduleRow,
|
|
32
|
+
} from '../schedule.js';
|
|
33
|
+
import { createAgent } from '../agent.js';
|
|
34
|
+
import { createPermission } from '../permissions.js';
|
|
35
|
+
import { buildSystemPrompt } from '../prompts.js';
|
|
36
|
+
import { listSkills, tamperedSkillNames } from '../skills.js';
|
|
37
|
+
import { costGuardStatus } from '../cost-guard.js';
|
|
38
|
+
import { libraryList, searchLibrary, installSkill, uninstallSkill, reinstallSkill, trustSkill } from '../skill-lib.js';
|
|
39
|
+
import { syncLogin, syncLogout, syncPush, syncPull, syncStatus, syncRemoteList, maybeAutoSync, syncChangePassword, syncShareCreate, syncShareList, syncShareAccept, syncShareRevoke, listSyncConflicts, resolveSyncConflict } from '../sync.js';
|
|
40
|
+
import { runWebServer } from '../web/server.js';
|
|
41
|
+
import { routeTask, routingConfig } from '../routing.js';
|
|
42
|
+
import { detectSandbox } from '../tools/bash.js';
|
|
43
|
+
import { generateTitle, renameSessionFile, titleModel } from '../titles.js';
|
|
44
|
+
import { estimateCost, isPeakHour, deferToOffpeak } from '../pricing.js';
|
|
45
|
+
import { createIO, style, C } from '../ui.js';
|
|
46
|
+
import {
|
|
47
|
+
createSession,
|
|
48
|
+
latestSession,
|
|
49
|
+
appendMessages,
|
|
50
|
+
rewriteSession,
|
|
51
|
+
loadSession,
|
|
52
|
+
listSessions,
|
|
53
|
+
sessionPreview,
|
|
54
|
+
relativeTime,
|
|
55
|
+
searchSessions,
|
|
56
|
+
} from '../session.js';
|
|
57
|
+
|
|
58
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
59
|
+
|
|
60
|
+
const HELP_LINES = [
|
|
61
|
+
[`MingDao Harness · AI 智能体框架 v${pkg.version}(命令:mingdao,简写 mdh)`, C.bold + C.cyan],
|
|
62
|
+
['', null],
|
|
63
|
+
['用法', C.bold + C.yellow],
|
|
64
|
+
[' mingdao 交互式对话(TUI)', null],
|
|
65
|
+
[' mingdao "你的问题" 单次提问(适合脚本与管道)', null],
|
|
66
|
+
[' mingdao --format json "…" 单次提问,输出结构化 JSON', null],
|
|
67
|
+
[' mingdao web [端口] 启动 WebUI(默认 http://127.0.0.1:3820)', null],
|
|
68
|
+
[' mingdao sessions search <词> 全文检索历史会话', null],
|
|
69
|
+
[' mingdao run "<任务>" 后台启动任务(tasks 面板管理)', null],
|
|
70
|
+
[' mingdao tasks [watch|kill <id>] 查看/实时刷新/停止后台任务', null],
|
|
71
|
+
[' mingdao schedule add/list/remove/pause/resume/chain 定时任务与依赖编排', null],
|
|
72
|
+
[' mingdao autostart on|off 开机自启(登录后自动启动服务器)', null],
|
|
73
|
+
[' mingdao workspace add/list/use/path/remove 工作空间(项目目录登记与快速切换)', null],
|
|
74
|
+
[' mingdao --continue 继续最近一次会话', null],
|
|
75
|
+
[' mingdao --journal 新会话带上最近会话日志(默认不注入,新会话全新开始)', null],
|
|
76
|
+
[' mingdao --resume 从会话列表选择恢复', null],
|
|
77
|
+
[' mingdao --model <模型名> 指定模型,例如 deepseek-v4-pro', null],
|
|
78
|
+
[' mingdao init 初始化配置向导', null],
|
|
79
|
+
[' mingdao update [--check] 一键自更新(git 安装形态;--check 只对比版本)', null],
|
|
80
|
+
[' mingdao rollback 回滚到上次 update 之前的提交', null],
|
|
81
|
+
[' mingdao audit [数量] 查看工具调用审计日志(默认最近 20 条)', null],
|
|
82
|
+
[' mingdao desktop 启动桌面版(Electron,任意目录可用,托盘常驻)', null],
|
|
83
|
+
[' mingdao --help / --version 帮助 / 版本', null],
|
|
84
|
+
['', null],
|
|
85
|
+
['凭证管理(API Key 独立存储,绝不写入 config.json / 仓库)', C.bold + C.yellow],
|
|
86
|
+
[' mingdao key 查看凭证状态(脱敏显示)', null],
|
|
87
|
+
[' mingdao key set <服务商> 交互式保存 API Key(隐藏输入)', null],
|
|
88
|
+
[' mingdao key remove <服务商> 删除凭证', null],
|
|
89
|
+
[' mingdao key import 从环境变量导入所有可用 Key', null],
|
|
90
|
+
['', null],
|
|
91
|
+
['云同步与技能库(跨设备会话同步 / 技能安装)', C.bold + C.yellow],
|
|
92
|
+
[' mingdao sync login <用户名> [密码] <服务器地址> 登录云同步(自动注册)', null],
|
|
93
|
+
[' mingdao sync push|pull|status|logout 推送 / 拉取 / 状态 / 退出', null],
|
|
94
|
+
[' mingdao sync-server [端口] 自建云同步服务器(数据目录 /var/lib/mingdao-sync)', null],
|
|
95
|
+
[' mingdao skill search|install|uninstall|update <名称> 技能库(内置 + 线上 registry)', null],
|
|
96
|
+
['', null],
|
|
97
|
+
['会话内命令', C.bold + C.yellow],
|
|
98
|
+
[' /help 显示帮助 /clear 清空上下文', null],
|
|
99
|
+
[' /model <名> 切换模型 /mode pro/flash 快捷切换', null],
|
|
100
|
+
[' /compact 压缩上下文 /plan 计划模式(先计划后执行)', null],
|
|
101
|
+
[' /init 生成 AGENTS.md /memory add <内容> 追加用户记忆', null],
|
|
102
|
+
[' /skills 列出技能 /status 会话状态 · /cost 累计费用', null],
|
|
103
|
+
[' /sessions 历史会话/检索 /route on|off 自动路由开关', null],
|
|
104
|
+
[' /mcp MCP 服务器状态 /verbose 思考开关 · /title <别名> 会话命名', null],
|
|
105
|
+
[' /usage 上轮用量 /exit 退出 · /save 会话文件', null],
|
|
106
|
+
[' /exit 退出 Tab 补全命令 · Ctrl+C 中断生成', null],
|
|
107
|
+
['', null],
|
|
108
|
+
[`配置目录: ${mingdaoHome()}`, C.dim],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
/** @param {any} out */
|
|
112
|
+
function printHelpLines(out) {
|
|
113
|
+
for (const [text, code] of HELP_LINES) out(code ? style(text, code) : text);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @param {any} provider @param {any} modelName @param {any} task */
|
|
117
|
+
async function generatePlan(provider, modelName, task) {
|
|
118
|
+
const res = await provider.chat({
|
|
119
|
+
model: modelName,
|
|
120
|
+
messages: [
|
|
121
|
+
{
|
|
122
|
+
role: 'system',
|
|
123
|
+
content:
|
|
124
|
+
'你是规划专家。为任务输出可执行计划:步骤列表(注明每步将使用的工具)、涉及的文件、风险点。不要执行任何工具、不要写代码。≤400 字,中文。',
|
|
125
|
+
},
|
|
126
|
+
{ role: 'user', content: task },
|
|
127
|
+
],
|
|
128
|
+
tools: [],
|
|
129
|
+
temperature: 0.3,
|
|
130
|
+
maxTokens: 2048,
|
|
131
|
+
});
|
|
132
|
+
return res.text || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 交互式 TUI 主循环(cli.js 装配好 provider/permission/agent 后调用)。
|
|
137
|
+
* 可变状态(modelName/provider/agent)在 ctx 上传递:切换模型时重建。
|
|
138
|
+
* @param {any} ctx
|
|
139
|
+
*/
|
|
140
|
+
export async function runRepl(ctx) {
|
|
141
|
+
const { io, cfg, home, pc0, opts, permission, workingDir, sessionUndoStore, mcpFacade, sessionRef, preset, withJournal, tuiState } = ctx;
|
|
142
|
+
let mcpManager = ctx.mcpManager;
|
|
143
|
+
let modelName = ctx.modelName;
|
|
144
|
+
let provider = ctx.provider;
|
|
145
|
+
let agent = ctx.agent;
|
|
146
|
+
|
|
147
|
+
// —— 交互式 TUI ——
|
|
148
|
+
const storedKey = getStoredKey(pc0.name);
|
|
149
|
+
const envKeyDetected = (pc0.envHint && process.env[pc0.envHint]) || process.env.MINGDAO_API_KEY;
|
|
150
|
+
const keySource = envKeyDetected ? '环境变量' : storedKey ? `凭证库 ${maskKey(storedKey)}` : 'config 字段';
|
|
151
|
+
const sandboxMode = cfg.sandbox || 'off';
|
|
152
|
+
const sandboxLabel =
|
|
153
|
+
sandboxMode === 'off' ? 'off' : detectSandbox() === 'bwrap' ? sandboxMode : `${sandboxMode}(bwrap 缺失,已降级)`;
|
|
154
|
+
const routing = routingConfig(cfg);
|
|
155
|
+
const wsNow = currentWorkspace(workingDir);
|
|
156
|
+
io.print('');
|
|
157
|
+
io.box(`MingDao Harness v${pkg.version}`, [
|
|
158
|
+
`模型 ${modelName}${preset?.label ? '(' + preset.label + ')' : ''}`,
|
|
159
|
+
`权限 ${permission.mode} · 密钥 ${keySource}`,
|
|
160
|
+
`沙箱 ${sandboxLabel}${routing ? ` · 路由 ${routing.planner}⇄${routing.executor}` : ''}`,
|
|
161
|
+
wsNow ? `工作空间 ${wsNow.name}(${wsNow.dir})` : '',
|
|
162
|
+
].filter(Boolean));
|
|
163
|
+
io.print(style('输入问题开始对话 · /help 查看命令 · Tab 补全 · Ctrl+C 中断生成\n', C.dim));
|
|
164
|
+
|
|
165
|
+
// WebUI 自动启动(首次运行 mingdao web 时可选开启,存 config.web.autoStart):
|
|
166
|
+
// 独立后台进程拉起,退出 TUI 后 WebUI 继续可用;关闭:mingdao web --no-autostart
|
|
167
|
+
if (cfg.web?.autoStart && !process.env.MINGDAO_NO_WEB_AUTOSTART) {
|
|
168
|
+
try {
|
|
169
|
+
const { spawn } = await import('node:child_process');
|
|
170
|
+
const { fileURLToPath } = await import('node:url');
|
|
171
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'web'], {
|
|
172
|
+
detached: true,
|
|
173
|
+
stdio: 'ignore',
|
|
174
|
+
env: process.env,
|
|
175
|
+
});
|
|
176
|
+
child.on('error', (err) => io.print(style(`[WebUI 自启失败] ${err?.message || err}(可手动运行 mingdao web ${cfg.web?.port || 3820})`, C.red)));
|
|
177
|
+
child.unref();
|
|
178
|
+
io.print(style(`🌐 WebUI 后台启动中:http://127.0.0.1:${cfg.web?.port || 3820}(关闭自动启动:mingdao web --no-autostart)`, C.dim));
|
|
179
|
+
} catch (/** @type {any} */ err) {
|
|
180
|
+
io.print(style(`[WebUI 自启失败] ${err?.message || err}`, C.red)); // 质检 L6:不再静默吞掉
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @type {any} */
|
|
185
|
+
let session = null;
|
|
186
|
+
if (opts.resume) {
|
|
187
|
+
const list = listSessions(home).slice(0, 10);
|
|
188
|
+
if (!list.length) {
|
|
189
|
+
io.print(style('没有可恢复的历史会话,已新建。', C.dim));
|
|
190
|
+
} else {
|
|
191
|
+
const choice = await io.choose(
|
|
192
|
+
'选择要恢复的会话:',
|
|
193
|
+
list.map((s) => ({ value: s.file, label: `${relativeTime(s.mtime)} · ${sessionPreview(s.file)}` }))
|
|
194
|
+
);
|
|
195
|
+
const loaded = loadSession(choice);
|
|
196
|
+
if (loaded.messages.length) {
|
|
197
|
+
session = loaded;
|
|
198
|
+
io.print(style(`✓ 已载入会话 ${path.basename(choice)}(${loaded.messages.length} 条消息)`, C.green));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!session && opts.continueSession) {
|
|
203
|
+
const latest = latestSession(home);
|
|
204
|
+
if (latest) {
|
|
205
|
+
const loaded = loadSession(latest.file);
|
|
206
|
+
if (loaded.messages.length) {
|
|
207
|
+
session = loaded;
|
|
208
|
+
io.print(style(`已载入会话 ${latest.name}(${loaded.messages.length} 条消息)`, C.dim));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!session) io.print('没有可继续的历史会话,已新建。');
|
|
212
|
+
}
|
|
213
|
+
if (!session) session = createSession(home);
|
|
214
|
+
tuiState.session = session;
|
|
215
|
+
sessionRef.name = path.basename(session.file);
|
|
216
|
+
io.print(style(`会话 ${path.basename(session.file)}`, C.dim));
|
|
217
|
+
|
|
218
|
+
const systemPrompt = buildSystemPrompt({ modelName, workingDir, withJournal });
|
|
219
|
+
// 恢复会话时刷新 system prompt(用户记忆 / AGENTS.md / 技能清单 / 时间戳以当前为准),
|
|
220
|
+
// 旧 system 消息保留在会话文件中,不影响追加历史。
|
|
221
|
+
const loadedMsgs = session.messages || [];
|
|
222
|
+
const hasOldSystem = loadedMsgs[0]?.role === 'system';
|
|
223
|
+
let messages = hasOldSystem
|
|
224
|
+
? [{ role: 'system', content: systemPrompt }, ...loadedMsgs.slice(1)]
|
|
225
|
+
: [{ role: 'system', content: systemPrompt }, ...loadedMsgs];
|
|
226
|
+
tuiState.persisted = messages.length;
|
|
227
|
+
let lastUsage = null;
|
|
228
|
+
let lastText = '';
|
|
229
|
+
let planMode = false;
|
|
230
|
+
let routingEnabled = Boolean(routing);
|
|
231
|
+
let lastRouteModel = null; // 会话级路由粘滞(评估 P2-1:执行类会话不再逐轮分类)
|
|
232
|
+
let autoTitled = Boolean(session.messages?.length);
|
|
233
|
+
const stats = { turns: 0, promptTokens: 0, completionTokens: 0 };
|
|
234
|
+
io.setHistory(messages.filter((m) => m.role === 'user').map((m) => m.content));
|
|
235
|
+
|
|
236
|
+
/** @param {any} target */
|
|
237
|
+
async function switchToModel(target, { silent = false, persist = true } = {}) {
|
|
238
|
+
try {
|
|
239
|
+
const npc = resolveProviderConfig(cfg, target);
|
|
240
|
+
if (!npc.apiKey) {
|
|
241
|
+
io.print(style('该模型没有可用的 API Key,请先运行 mingdao init 配置。', C.red));
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const newProvider = await createProvider(cfg, target);
|
|
245
|
+
provider = newProvider;
|
|
246
|
+
modelName = target;
|
|
247
|
+
// 自动路由的切换只改会话内存态(评估 P3-6:不悄悄改写用户持久默认模型);/model 显式切换才落盘
|
|
248
|
+
if (persist) {
|
|
249
|
+
cfg.model = target;
|
|
250
|
+
saveConfig(cfg);
|
|
251
|
+
}
|
|
252
|
+
agent = createAgent({
|
|
253
|
+
provider,
|
|
254
|
+
permission,
|
|
255
|
+
io,
|
|
256
|
+
modelName,
|
|
257
|
+
workingDir,
|
|
258
|
+
cfg,
|
|
259
|
+
undoStore: sessionUndoStore,
|
|
260
|
+
mcp: mcpFacade,
|
|
261
|
+
sessionRef,
|
|
262
|
+
onCompact: (/** @type {any} */ msgs) => {
|
|
263
|
+
rewriteSession(session.file, msgs);
|
|
264
|
+
tuiState.persisted = msgs.length;
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
messages[0] = { role: 'system', content: buildSystemPrompt({ workingDir, withJournal }) };
|
|
268
|
+
if (!silent) {
|
|
269
|
+
const p2 = modelPreset(modelName);
|
|
270
|
+
io.print(style(`✓ 已切换到 ${C.bold}${modelName}${C.reset}${p2 ? `(${p2.label})` : ''}`, C.green));
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
} catch (/** @type {any} */ err) {
|
|
274
|
+
io.print(style('[错误] ' + (err?.message || err), C.red));
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (;;) {
|
|
280
|
+
let input;
|
|
281
|
+
try {
|
|
282
|
+
input = await io.askMultiline(style('你> ', C.green));
|
|
283
|
+
} catch {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (input === '') continue;
|
|
287
|
+
|
|
288
|
+
if (input.startsWith('/')) {
|
|
289
|
+
const [cmd, ...rest] = input.split(/\s+/);
|
|
290
|
+
const arg = rest.join(' ');
|
|
291
|
+
// 审计(第五轮 P1-1 教训):斜杠命令统一 try/catch——单条命令异常只提示不退出,
|
|
292
|
+
// 绝不再因一条命令的错误杀死整个 REPL 会话(历史 P1-1 曾导致会话上下文全丢)
|
|
293
|
+
try {
|
|
294
|
+
if (cmd === '/exit' || cmd === '/quit') break;
|
|
295
|
+
else if (cmd === '/help') printHelpLines(io.print);
|
|
296
|
+
else if (cmd === '/clear') {
|
|
297
|
+
messages = [{ role: 'system', content: systemPrompt }];
|
|
298
|
+
// 评估 P2-1:会话文件同步原子重写为仅新 system——否则旧上下文残留会被 --continue 读回,
|
|
299
|
+
// 且后续 appendMessages 会把 system+新消息重复追加到旧历史之后。
|
|
300
|
+
try {
|
|
301
|
+
rewriteSession(session.file, messages);
|
|
302
|
+
} catch {}
|
|
303
|
+
tuiState.persisted = messages.length;
|
|
304
|
+
io.print('已清空上下文(会话文件已同步重置)。');
|
|
305
|
+
} else if (cmd === '/model') {
|
|
306
|
+
if (!arg) {
|
|
307
|
+
io.print(`当前模型:${modelName}`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
await switchToModel(arg);
|
|
311
|
+
} else if (cmd === '/mode') {
|
|
312
|
+
const map = { pro: 'deepseek-v4-pro', flash: 'deepseek-v4-flash' };
|
|
313
|
+
const target = /** @type {Record<string, any>} */ (map)[arg] || arg;
|
|
314
|
+
if (!target) {
|
|
315
|
+
io.print('用法:/mode pro|flash|<模型名>(pro=deepseek-v4-pro,flash=deepseek-v4-flash)');
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
await switchToModel(target);
|
|
319
|
+
} else if (cmd === '/think') {
|
|
320
|
+
const vals = ['low', 'high', 'max'];
|
|
321
|
+
if (!arg) {
|
|
322
|
+
io.print(`用法:/think low|high|max|off(当前:${cfg.reasoningEffort || '跟随模型默认 high'})`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (arg === 'off') cfg.reasoningEffort = 'off'; // 显式禁用(provider 转 thinking:{type:'disabled'})
|
|
326
|
+
else if (vals.includes(arg)) cfg.reasoningEffort = arg;
|
|
327
|
+
else { io.print(style('无效取值:low|high|max|off', C.red)); continue; }
|
|
328
|
+
agent = createAgent({
|
|
329
|
+
provider, permission, io, modelName, workingDir, cfg,
|
|
330
|
+
undoStore: sessionUndoStore, mcp: mcpFacade, sessionRef,
|
|
331
|
+
onCompact: (/** @type {any} */ msgs) => {
|
|
332
|
+
rewriteSession(session.file, msgs);
|
|
333
|
+
tuiState.persisted = msgs.length;
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
io.print(style(`✓ 思考强度:${cfg.reasoningEffort || 'off(模型默认)'}`, C.green));
|
|
337
|
+
} else if (cmd === '/plan') {
|
|
338
|
+
planMode = !planMode;
|
|
339
|
+
io.print(style(`✓ 计划模式:${planMode ? '开' : '关'}${planMode ? '(先出计划,确认后执行)' : ''}`, C.green));
|
|
340
|
+
} else if (cmd === '/route') {
|
|
341
|
+
if (!routing) {
|
|
342
|
+
io.print(style('未配置自动路由(config.json 的 routing 字段,如 {"enabled":true,"planner":"deepseek-v4-pro","executor":"deepseek-v4-flash"})', C.dim));
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (arg === 'on' || arg === 'off') routingEnabled = arg === 'on';
|
|
346
|
+
else routingEnabled = !routingEnabled;
|
|
347
|
+
io.print(style(`✓ 自动路由:${routingEnabled ? '开' : '关'}(规划类→${routing.planner},执行类→${routing.executor})`, C.green));
|
|
348
|
+
} else if (cmd === '/verbose') {
|
|
349
|
+
io.setShowReasoning(!io.showReasoning);
|
|
350
|
+
io.print(style(`✓ 思考过程显示:${io.showReasoning ? '开' : '关'}`, C.green));
|
|
351
|
+
} else if (cmd === '/compact') {
|
|
352
|
+
if (messages.length <= 6) {
|
|
353
|
+
io.print(style('消息较少(≤6 条),无需压缩。', C.dim));
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
io.startSpinner('正在压缩上下文…');
|
|
357
|
+
let compacted = null;
|
|
358
|
+
try {
|
|
359
|
+
const count = makeTokenCounter(modelName);
|
|
360
|
+
let total = 0;
|
|
361
|
+
for (const m of messages) total += messageTokens(m, count);
|
|
362
|
+
const budget = Math.max(1000, Math.round(total * 0.5)); // 手动触发:保留约 30% 尾部
|
|
363
|
+
compacted = await compactConversation({
|
|
364
|
+
messages,
|
|
365
|
+
budget,
|
|
366
|
+
count,
|
|
367
|
+
provider,
|
|
368
|
+
executorModel: modelName,
|
|
369
|
+
triggerRatio: 0,
|
|
370
|
+
force: true, // 手动意图明确:跳过最小裁剪门槛
|
|
371
|
+
});
|
|
372
|
+
} catch (/** @type {any} */ err) {
|
|
373
|
+
io.print(style('[压缩失败] ' + (err?.message || err), C.red));
|
|
374
|
+
}
|
|
375
|
+
io.stopSpinner();
|
|
376
|
+
if (!compacted?.messages) {
|
|
377
|
+
io.print(style('未能压缩(被裁段落不足或摘要失败)。', C.dim));
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
messages = compacted.messages;
|
|
381
|
+
appendMessages(session.file, [{ role: 'system', content: '── /compact 压缩点 ──' }, ...messages.slice(1)]);
|
|
382
|
+
tuiState.persisted = messages.length;
|
|
383
|
+
io.print(style(`✓ 已压缩上下文:${compacted.droppedCount} 条早期消息 → 摘要(回收约 ${compacted.droppedTokens} tokens)`, C.green));
|
|
384
|
+
} else if (cmd === '/init') {
|
|
385
|
+
const target = path.join(workingDir, 'AGENTS.md');
|
|
386
|
+
if (fs.existsSync(target) && arg !== 'force') {
|
|
387
|
+
io.print(style(`已存在 ${target},如需覆盖:/init force`, C.yellow));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const entries = fs.readdirSync(workingDir).slice(0, 20).join('、') || '(空目录)';
|
|
391
|
+
const template =
|
|
392
|
+
`# ${path.basename(workingDir)} 项目约定\n\n` +
|
|
393
|
+
`## 项目概述\n\n(一句话说明项目用途)\n\n` +
|
|
394
|
+
`## 常用命令\n\n(构建、测试、运行命令)\n\n` +
|
|
395
|
+
`## 代码结构\n\n顶层内容:${entries}\n\n` +
|
|
396
|
+
`## 约定与规范\n\n(团队约定、注意事项;MingDao 每次会话会自动读取本文件)\n`;
|
|
397
|
+
fs.writeFileSync(target, template);
|
|
398
|
+
io.print(style(`✓ 已生成 ${target},将在后续会话自动注入。`, C.green));
|
|
399
|
+
} else if (cmd === '/memory') {
|
|
400
|
+
const memPath = path.join(mingdaoHome(), 'AGENTS.md');
|
|
401
|
+
if (arg.startsWith('add ')) {
|
|
402
|
+
const text = arg.slice(4).trim();
|
|
403
|
+
if (!text) {
|
|
404
|
+
io.print('用法:/memory add <内容>');
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
fs.appendFileSync(memPath, `- ${text}\n`);
|
|
408
|
+
io.print(style(`✓ 已追加到用户记忆 ${memPath}(后续会话自动生效)`, C.green));
|
|
409
|
+
} else if (arg === 'extract') {
|
|
410
|
+
io.startSpinner('正在从当前对话提炼记忆…');
|
|
411
|
+
try {
|
|
412
|
+
const existing = loadMemory();
|
|
413
|
+
const lines = await extractMemory(provider, titleModel(cfg, modelName), messages, existing);
|
|
414
|
+
io.stopSpinner();
|
|
415
|
+
if (lines.length) {
|
|
416
|
+
appendMemory(lines);
|
|
417
|
+
io.print(style(`✓ 新增 ${lines.length} 条记忆:`, C.green));
|
|
418
|
+
for (const l of lines) io.print(style(' ' + l, C.dim));
|
|
419
|
+
} else {
|
|
420
|
+
io.print(style('没有发现新的值得记住的内容。', C.dim));
|
|
421
|
+
}
|
|
422
|
+
} catch (/** @type {any} */ err) {
|
|
423
|
+
io.stopSpinner();
|
|
424
|
+
io.print(style('[错误] ' + (err?.message || err), C.red));
|
|
425
|
+
}
|
|
426
|
+
} else if (arg === 'show') {
|
|
427
|
+
const raw = loadMemory();
|
|
428
|
+
if (!raw.trim()) {
|
|
429
|
+
io.print('记忆库为空。');
|
|
430
|
+
} else {
|
|
431
|
+
io.print(style('记忆库(' + memPath + '):', C.bold));
|
|
432
|
+
raw.split('\n').forEach((l, i) => {
|
|
433
|
+
if (l.trim()) io.print(style(` ${String(i + 1).padStart(3)} ${l}`, C.dim));
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
} else if (arg.startsWith('remove ')) {
|
|
437
|
+
const kw = arg.slice(7).trim();
|
|
438
|
+
if (!kw) {
|
|
439
|
+
io.print('用法:/memory remove <关键词>(删除包含该词的条目)');
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const removed = removeMemoryLines(kw);
|
|
443
|
+
io.print(removed > 0 ? style(`✓ 已删除 ${removed} 条(原文件备份于 ${memPath}.bak)`, C.green) : style('没有匹配的条目。', C.dim));
|
|
444
|
+
} else if (arg === 'dedupe') {
|
|
445
|
+
const removed = dedupeMemory();
|
|
446
|
+
io.print(removed > 0 ? style(`✓ 去重完成:合并 ${removed} 条重复记忆`, C.green) : style('没有重复条目。', C.dim));
|
|
447
|
+
} else if (arg === 'edit') {
|
|
448
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
449
|
+
if (!editor) {
|
|
450
|
+
io.print(style('未设置 EDITOR 环境变量。可 export EDITOR=nano(或 vim/code)后用 /memory edit 打开记忆文件。', C.yellow));
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const { spawnSync } = await import('node:child_process');
|
|
454
|
+
spawnSync(editor, [memPath], { stdio: 'inherit' });
|
|
455
|
+
io.print(style('✓ 记忆文件已编辑(后续会话自动生效)。', C.green));
|
|
456
|
+
} else {
|
|
457
|
+
io.print(style(`用户记忆文件:${memPath}${fs.existsSync(memPath) ? '' : '(尚不存在)'}`, C.dim));
|
|
458
|
+
if (fs.existsSync(memPath)) io.print(style(fs.readFileSync(memPath, 'utf8').slice(0, 2000), C.dim));
|
|
459
|
+
const journal = recentJournal(home, 5);
|
|
460
|
+
if (journal.length) {
|
|
461
|
+
io.print(style('最近会话:', C.bold));
|
|
462
|
+
for (const e of journal.reverse()) io.print(style(` ${new Date(e.at).toISOString().slice(0, 10)} ${e.firstUser?.slice(0, 40)}`, C.dim));
|
|
463
|
+
}
|
|
464
|
+
io.print('用法:/memory add <内容> 追加 · extract 自动提炼 · show 查看 · remove <词> 删除 · dedupe 去重 · edit 编辑器修改');
|
|
465
|
+
}
|
|
466
|
+
} else if (cmd === '/skills') {
|
|
467
|
+
const skills = listSkills(workingDir);
|
|
468
|
+
if (!skills.length) {
|
|
469
|
+
io.print(
|
|
470
|
+
style(
|
|
471
|
+
'未安装技能。目录:~/.mingdao/skills/(用户级)、<项目>/.mingdao/skills/(项目级)与内置技能库',
|
|
472
|
+
C.dim
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const label = (/** @type {any} */ s) => `${s.name}${s.source === 'user' ? '(用户级)' : s.source === 'builtin' ? '(内置)' : ''}`;
|
|
478
|
+
io.box(
|
|
479
|
+
`已安装技能(${skills.length})`,
|
|
480
|
+
skills.map((s) => `${label(s)}:${s.description || '(无描述)'}`)
|
|
481
|
+
);
|
|
482
|
+
io.print(style(`技能库安装:退出会话后运行 mingdao skill search [关键词] → mingdao skill install <名称>`, C.dim));
|
|
483
|
+
} else if (cmd === '/title') {
|
|
484
|
+
if (!arg) {
|
|
485
|
+
io.print('用法:/title <别名>(给当前会话命名,便于 --resume 识别)');
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const safe = arg.replace(/[^\w\u4e00-\u9fa5.-]/g, '_').slice(0, 40);
|
|
489
|
+
let newFile = path.join(home, 'sessions', safe + '.jsonl');
|
|
490
|
+
try {
|
|
491
|
+
fs.appendFileSync(session.file, ''); // 确保文件已创建(尚未写消息时也可能重命名)
|
|
492
|
+
// 同名会话已存在时附加随机后缀,绝不静默覆盖
|
|
493
|
+
if (fs.existsSync(newFile)) {
|
|
494
|
+
newFile = path.join(home, 'sessions', safe + '-' + Math.random().toString(36).slice(2, 6) + '.jsonl');
|
|
495
|
+
}
|
|
496
|
+
fs.renameSync(session.file, newFile);
|
|
497
|
+
session.file = newFile;
|
|
498
|
+
io.print(style(`✓ 会话已命名为 ${path.basename(newFile)}`, C.green));
|
|
499
|
+
} catch (/** @type {any} */ err) {
|
|
500
|
+
io.print(style('[错误] ' + (err?.message || err), C.red));
|
|
501
|
+
}
|
|
502
|
+
} else if (cmd === '/audit') {
|
|
503
|
+
const n = Number(arg) || 10;
|
|
504
|
+
const { listAudit } = await import('../audit.js');
|
|
505
|
+
const rows = listAudit(n);
|
|
506
|
+
if (!rows.length) {
|
|
507
|
+
io.print(style('暂无审计记录(工具调用会自动记录)。', C.dim));
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
io.print(style(`最近 ${rows.length} 条工具调用审计:`, C.bold));
|
|
511
|
+
for (const r of rows) {
|
|
512
|
+
const when = new Date(r.at).toISOString().slice(0, 19).replace('T', ' ');
|
|
513
|
+
const status = r.denied ? `✖拒绝(${r.reason || ''})` : r.ok ? '✓' : '✖错误';
|
|
514
|
+
io.print(` ${when} ${status} ${r.tool} ${String(r.args || '').slice(0, 60)}`);
|
|
515
|
+
}
|
|
516
|
+
} else if (cmd === '/mcp') {
|
|
517
|
+
const status = mcpFacade.status();
|
|
518
|
+
if (status.length === 1 && status[0].name === '(连接中…)' && !status[0].ok && !mcpManager) {
|
|
519
|
+
if (!cfg.mcpServers || !Object.keys(cfg.mcpServers).length) {
|
|
520
|
+
io.print(style('未配置 MCP 服务器(config.json 的 mcpServers 字段)。', C.dim));
|
|
521
|
+
} else {
|
|
522
|
+
io.print(style('MCP 服务器连接中…(npx 首次下载依赖可能较慢)', C.dim));
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
io.box(
|
|
527
|
+
`MCP 服务器(${status.length})`,
|
|
528
|
+
status.map((/** @type {any} */ s) => `${s.ok ? '✓' : '✖'} ${s.name}${s.ok ? ` · ${s.tools} 个工具` : `:${s.error}`}`)
|
|
529
|
+
);
|
|
530
|
+
} else if (cmd === '/sessions') {
|
|
531
|
+
if (arg) {
|
|
532
|
+
// 关键词全文检索历史会话
|
|
533
|
+
const hits = /** @type {any[]} */ (searchSessions(home, arg));
|
|
534
|
+
if (!hits.length) {
|
|
535
|
+
io.print(style(`未找到包含「${arg}」的会话。`, C.dim));
|
|
536
|
+
} else {
|
|
537
|
+
io.box(`会话检索:${arg}(${hits.length} 个命中)`, hits.map((h) => `${h.name}(${relativeTime(h.mtime)})`));
|
|
538
|
+
for (const h of hits.slice(0, 5)) io.print(style(` ${h.snippet}`, C.dim));
|
|
539
|
+
io.print(style('恢复:mingdao --resume(或 mingdao sessions search 命令)', C.dim));
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
const list = listSessions(home).slice(0, 10);
|
|
543
|
+
if (!list.length) io.print('暂无历史会话。');
|
|
544
|
+
else {
|
|
545
|
+
io.box('历史会话(最近 10 个)', list.map((s) => `${relativeTime(s.mtime)} · ${sessionPreview(s.file)}`));
|
|
546
|
+
io.print(style('检索:/sessions <关键词> · 恢复:mingdao --resume(文件:' + path.join(home, 'sessions') + ')', C.dim));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} else if (cmd === '/save') {
|
|
550
|
+
io.print(`当前会话自动保存于:${session.file}`);
|
|
551
|
+
} else if (cmd === '/usage') {
|
|
552
|
+
if (lastUsage) {
|
|
553
|
+
io.print(
|
|
554
|
+
style(
|
|
555
|
+
`上轮用量:${lastUsage.prompt_tokens} prompt + ${lastUsage.completion_tokens} completion tokens`,
|
|
556
|
+
C.dim
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
} else io.print('尚无用量记录。');
|
|
560
|
+
} else if (cmd === '/status') {
|
|
561
|
+
io.box('会话状态', [
|
|
562
|
+
`模型 ${modelName} · 权限 ${permission.mode}`,
|
|
563
|
+
`沙箱 ${cfg.sandbox || 'off'}${routing ? ` · 自动路由 ${routingEnabled ? '开' : '关'}(${routing.planner}⇄${routing.executor})` : ''}`,
|
|
564
|
+
`会话 ${path.basename(session.file)}`,
|
|
565
|
+
`轮次 ${stats.turns} · 消息 ${messages.length} 条`,
|
|
566
|
+
`Tokens ↑${stats.promptTokens} ↓${stats.completionTokens}`,
|
|
567
|
+
`费用 ≈¥${estimateCost(modelName, stats.promptTokens, stats.completionTokens).toFixed(5)}(累计·按当前模型计价)`,
|
|
568
|
+
`计划模式 ${planMode ? '开' : '关'} · 思考显示 ${io.showReasoning ? '开' : '关'} · 任务 ${agent.getTodos().length} 项`,
|
|
569
|
+
]);
|
|
570
|
+
} else if (cmd === '/cost') {
|
|
571
|
+
const bd = costBreakdown();
|
|
572
|
+
io.box('费用分账(含缓存折扣与 Batch 半价的真实口径)', [
|
|
573
|
+
`累计 ≈¥${bd.totalCost.toFixed(5)} · 今日 ≈¥${bd.today.toFixed(5)}` +
|
|
574
|
+
`${bd.totalSaved > 0 ? ` · 相比全未命中已省 ≈¥${bd.totalSaved.toFixed(5)}` : ''}`,
|
|
575
|
+
`缓存命中率 ${bd.rate != null ? (bd.rate * 100).toFixed(0) + '%' : '暂无缓存数据'}${bd.batchCost > 0 ? ` · Batch 半价任务 ≈¥${bd.batchCost.toFixed(5)}` : ''}`,
|
|
576
|
+
...bd.byModel.slice(0, 8).map((m) => ` ${m.model}:${m.turns} 轮(${m.batchTurns ? m.batchTurns + ' 批' : ''})· ↑${m.prompt} ↓${m.completion} · ≈¥${m.cost.toFixed(5)}${m.saved > 0 ? ` · 省 ¥${m.saved.toFixed(5)}` : ''}`),
|
|
577
|
+
]);
|
|
578
|
+
const guard = costGuardStatus();
|
|
579
|
+
if (guard) {
|
|
580
|
+
io.print(
|
|
581
|
+
style(
|
|
582
|
+
`费用护栏:今日 ¥${(guard.cost ?? 0).toFixed(4)} / 上限 ¥${guard.limit.toFixed(2)}${guard.overLimit ? '(已达上限' + (guard.action === 'block' ? ',执行已暂停' : ',仅提醒') + ')' : ''}`,
|
|
583
|
+
guard.overLimit ? C.yellow : C.dim
|
|
584
|
+
)
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
io.print(style('会话内累计(本次)≈¥' + estimateCost(modelName, stats.promptTokens, stats.completionTokens).toFixed(5), C.dim));
|
|
588
|
+
} else if (cmd === '/cache') {
|
|
589
|
+
const entries = listCacheStats();
|
|
590
|
+
if (!entries.length) {
|
|
591
|
+
io.print(style('暂无缓存统计(对话若干轮后自动累积)。', C.dim));
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const sum = summarizeCacheStats(entries);
|
|
595
|
+
io.box('缓存命中率仪表盘', formatCacheSummary(sum));
|
|
596
|
+
io.print(style('近 10 次命中率趋势:', C.dim));
|
|
597
|
+
const recent = entries.slice(-10);
|
|
598
|
+
const maxBar = 24;
|
|
599
|
+
for (const e of recent) {
|
|
600
|
+
const rate = e.hit != null && e.hit + e.miss > 0 ? e.hit / (e.hit + e.miss) : null;
|
|
601
|
+
const bar = rate == null ? '—'.repeat(maxBar) : '█'.repeat(Math.round(rate * maxBar));
|
|
602
|
+
io.print(style(` ${bar.padEnd(maxBar)} ${rate == null ? 'n/a' : (rate * 100).toFixed(0) + '%'} ${e.model}`, C.dim));
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
io.print(style('未知命令,输入 /help 查看可用命令。', C.yellow));
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
} catch (/** @type {any} */ err) {
|
|
609
|
+
io.print(style('[错误] 命令执行失败:' + (err?.message || err), C.red));
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 自动路由:规划类任务切 planner,执行类走 executor(会话粘滞 + 分类缓存见 routing.js)
|
|
615
|
+
if (routingEnabled) {
|
|
616
|
+
const route = await routeTask({ cfg, provider, currentModel: modelName, text: input, sticky: lastRouteModel });
|
|
617
|
+
lastRouteModel = route.model;
|
|
618
|
+
if (route.model !== modelName) {
|
|
619
|
+
const okSwitch = await switchToModel(route.model, { silent: true, persist: false });
|
|
620
|
+
if (okSwitch) io.print(style(`⤷ 自动路由 → ${route.model}(${route.reason})`, C.dim));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 计划模式:先出计划,确认后执行
|
|
625
|
+
if (planMode) {
|
|
626
|
+
io.startSpinner('正在生成计划…');
|
|
627
|
+
let plan = null;
|
|
628
|
+
try {
|
|
629
|
+
plan = await generatePlan(provider, modelName, input);
|
|
630
|
+
} catch (/** @type {any} */ err) {
|
|
631
|
+
io.print(style('[计划生成失败] ' + (err?.message || err), C.red));
|
|
632
|
+
}
|
|
633
|
+
io.stopSpinner();
|
|
634
|
+
if (plan == null) continue;
|
|
635
|
+
io.print(style('── 执行计划 ──', C.bold + C.cyan));
|
|
636
|
+
io.print(plan);
|
|
637
|
+
const okGo = await io.confirm(style('是否按此计划执行?[y/N]', C.yellow));
|
|
638
|
+
if (!okGo) {
|
|
639
|
+
io.print(style('已取消执行,可修改要求后重试。', C.dim));
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const planMsg = { role: 'assistant', content: '[执行计划]\n' + plan };
|
|
643
|
+
messages.push(planMsg);
|
|
644
|
+
appendMessages(session.file, [planMsg]);
|
|
645
|
+
tuiState.persisted += 1;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const userMsg = { role: 'user', content: input };
|
|
649
|
+
messages.push(userMsg);
|
|
650
|
+
appendMessages(session.file, [userMsg]);
|
|
651
|
+
tuiState.persisted += 1;
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
const res = await agent.runTurn(messages);
|
|
655
|
+
lastUsage = res.usage;
|
|
656
|
+
lastText = res.text || lastText;
|
|
657
|
+
stats.turns += 1;
|
|
658
|
+
recordUsage(res.perf?.usedModel || modelName, res.usage, res.perf);
|
|
659
|
+
stats.promptTokens += res.usage.prompt_tokens || 0;
|
|
660
|
+
stats.completionTokens += res.usage.completion_tokens || 0;
|
|
661
|
+
const fresh = messages.slice(tuiState.persisted);
|
|
662
|
+
appendMessages(session.file, fresh);
|
|
663
|
+
tuiState.persisted = messages.length;
|
|
664
|
+
if (!autoTitled && cfg.autoTitle !== false && res.text) {
|
|
665
|
+
autoTitled = true;
|
|
666
|
+
const tModel = titleModel(cfg, modelName);
|
|
667
|
+
const title = await generateTitle(await helperProvider(cfg, tModel, provider), tModel, input);
|
|
668
|
+
if (title) {
|
|
669
|
+
const renamed = renameSessionFile(fs, path, home, session, title);
|
|
670
|
+
if (renamed) io.print(style(`✓ 会话标题:${path.basename(renamed)}`, C.dim));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (res.aborted) {
|
|
674
|
+
io.print(style('(已中断)', C.dim));
|
|
675
|
+
}
|
|
676
|
+
if (res.truncated) {
|
|
677
|
+
io.print(style('[警告] 达到最大工具调用步数,任务可能未完成。', C.yellow));
|
|
678
|
+
}
|
|
679
|
+
if (res.finish === 'length') {
|
|
680
|
+
io.print(style('[提示] 模型输出达到长度上限被截断。', C.yellow));
|
|
681
|
+
}
|
|
682
|
+
io.printUsageLine({ modelName, usage: res.usage, durationMs: res.durationMs });
|
|
683
|
+
} catch (/** @type {any} */ err) {
|
|
684
|
+
io.print(style('[错误] ' + (err?.message || err), C.red));
|
|
685
|
+
io.print(style('提示:可直接继续对话,或 /exit 退出。', C.dim));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
mcpFacade.stop();
|
|
690
|
+
if (stats.turns > 0) {
|
|
691
|
+
io.startSpinner('正在沉淀会话记忆…');
|
|
692
|
+
try {
|
|
693
|
+
await finalizeSession({
|
|
694
|
+
cfg,
|
|
695
|
+
provider,
|
|
696
|
+
model: titleModel(cfg, modelName),
|
|
697
|
+
home,
|
|
698
|
+
workingDir,
|
|
699
|
+
messages,
|
|
700
|
+
turns: stats.turns,
|
|
701
|
+
lastText,
|
|
702
|
+
});
|
|
703
|
+
} catch {}
|
|
704
|
+
io.stopSpinner();
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await maybeAutoSync();
|
|
708
|
+
} catch {}
|
|
709
|
+
io.print('再见,MingDao Harness 与你同行。');
|
|
710
|
+
io.close();
|
|
711
|
+
}
|