dgxay-code-cli 0.0.0-stage → 1.0.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/LICENSE +674 -0
- package/README-zh.md +200 -0
- package/README.md +182 -2
- package/bin/dgxay.js +42 -0
- package/package.json +47 -4
- package/src/cli/ansi.js +509 -0
- package/src/cli/approval.js +186 -0
- package/src/cli/args.js +162 -0
- package/src/cli/command-table.js +52 -0
- package/src/cli/editor.js +574 -0
- package/src/cli/index.js +228 -0
- package/src/cli/print.js +148 -0
- package/src/cli/render.js +324 -0
- package/src/cli/repl-state.js +85 -0
- package/src/cli/repl.js +1167 -0
- package/src/cli/slash.js +74 -0
- package/src/cli/ui.js +52 -0
- package/src/commands/_shared.js +20 -0
- package/src/commands/chat.js +212 -0
- package/src/commands/config.js +363 -0
- package/src/commands/context.js +43 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/export.js +102 -0
- package/src/commands/mcp.js +257 -0
- package/src/commands/model.js +223 -0
- package/src/commands/oauth.js +246 -0
- package/src/commands/permission.js +108 -0
- package/src/commands/provider.js +332 -0
- package/src/commands/run.js +172 -0
- package/src/commands/skill.js +94 -0
- package/src/commands/workspace.js +100 -0
- package/src/core/agent.js +419 -0
- package/src/core/llm.js +709 -0
- package/src/core/mcp.js +271 -0
- package/src/core/oauth.js +737 -0
- package/src/core/prompt.js +269 -0
- package/src/core/sessions.js +208 -0
- package/src/core/tools.js +391 -0
- package/src/services/config.js +182 -0
- package/src/services/input.js +91 -0
- package/src/services/session-bus.js +104 -0
- package/src/services/settings.js +340 -0
- package/src/services/skills.js +125 -0
- package/src/services/terminal.js +124 -0
- package/src/services/version.js +25 -0
package/src/cli/repl.js
ADDED
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 交互模式(REPL):把桌面版的对话界面完整搬到终端。
|
|
5
|
+
*
|
|
6
|
+
* dgxay 进入交互模式(自动恢复最近会话)
|
|
7
|
+
* dgxay "问题" 单次对话
|
|
8
|
+
* dgxay -c / -r <id> 继续最近会话 / 指定会话
|
|
9
|
+
*
|
|
10
|
+
* 保留的桌面能力:会话列表与恢复、斜杠命令、技能、四档权限与自动审查、
|
|
11
|
+
* 上下文压缩、`!` 终端、模型/供应商切换、MCP 管理、OAuth、工作空间选择。
|
|
12
|
+
* 去掉的只有 UI:上下文圆环改为状态行 + /context,弹窗改为终端里的确认框。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const { COLORS, SIGN, termWidth, setColorMode, colorEnabled, displayWidth } = require('./ansi');
|
|
20
|
+
const { LineEditor, createLineReader } = require('./editor');
|
|
21
|
+
const { parseArgs } = require('./args');
|
|
22
|
+
const print = require('./print');
|
|
23
|
+
const { askApproval } = require('./approval');
|
|
24
|
+
const { SLASH_COMMANDS, findCommand, completer } = require('./slash');
|
|
25
|
+
const { StreamRenderer } = require('./render');
|
|
26
|
+
const { contextInfo, ringLine, contextReport } = require('./ui');
|
|
27
|
+
const state = require('./repl-state');
|
|
28
|
+
|
|
29
|
+
const sessions = require('../core/sessions');
|
|
30
|
+
const settings = require('../services/settings');
|
|
31
|
+
const cfg = require('../services/config');
|
|
32
|
+
const skills = require('../services/skills');
|
|
33
|
+
const mcp = require('../core/mcp');
|
|
34
|
+
const oauth = require('../core/oauth');
|
|
35
|
+
const terminal = require('../services/terminal');
|
|
36
|
+
const sessionBus = require('../services/session-bus');
|
|
37
|
+
const { runAgent, HISTORY_LIMIT } = require('../core/agent');
|
|
38
|
+
const { compressMessages } = require('../core/prompt');
|
|
39
|
+
const { VERSION, BIN } = require('../services/version');
|
|
40
|
+
|
|
41
|
+
const GLOBAL_REPL_OPTIONS = [
|
|
42
|
+
{ name: 'print', alias: 'p', type: 'boolean' },
|
|
43
|
+
{ name: 'prompt', type: 'string' },
|
|
44
|
+
{ name: 'mode', alias: 'm', type: 'string' },
|
|
45
|
+
{ name: 'permission', type: 'string' },
|
|
46
|
+
{ name: 'workspace', alias: 'C', type: 'string' },
|
|
47
|
+
{ name: 'model', type: 'string' },
|
|
48
|
+
{ name: 'provider', type: 'string' },
|
|
49
|
+
{ name: 'skill', type: 'string' },
|
|
50
|
+
{ name: 'resume', alias: 'r', type: 'string' },
|
|
51
|
+
{ name: 'continue', alias: 'c', type: 'boolean' },
|
|
52
|
+
{ name: 'output', alias: 'o', type: 'string' },
|
|
53
|
+
{ name: 'yes', alias: 'y', type: 'boolean' }
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function createRuntime(options = {}) {
|
|
57
|
+
const st = state.createState();
|
|
58
|
+
const config = settings.readConfig();
|
|
59
|
+
const model = settings.activeModel(config);
|
|
60
|
+
const provider = settings.activeProvider(config);
|
|
61
|
+
|
|
62
|
+
st.runtime.mode = ['chat', 'work'].includes(options.mode) ? options.mode : settings.currentMode(config);
|
|
63
|
+
st.runtime.permission = settings.PERMISSIONS[options.permission]
|
|
64
|
+
? options.permission
|
|
65
|
+
: settings.currentPermission(config);
|
|
66
|
+
if (st.runtime.permission === 'auto' && !settings.hasReviewModel(config)) st.runtime.permission = 'normal';
|
|
67
|
+
st.runtime.modelId = options.model || model?.id || '';
|
|
68
|
+
st.runtime.providerId = options.provider
|
|
69
|
+
? settings.findProvider(options.provider)?.id || provider?.id || ''
|
|
70
|
+
: provider?.id || '';
|
|
71
|
+
st.runtime.skill = String(options.skill || '');
|
|
72
|
+
|
|
73
|
+
if (options.workspace) {
|
|
74
|
+
const resolved = path.resolve(options.workspace);
|
|
75
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) st.cwd = resolved;
|
|
76
|
+
else throw Object.assign(new Error(`工作空间不存在:${options.workspace}`), { userError: true });
|
|
77
|
+
} else if (config.work?.workspace && fs.existsSync(config.work.workspace)) {
|
|
78
|
+
st.cwd = config.work.workspace;
|
|
79
|
+
} else {
|
|
80
|
+
st.cwd = process.cwd();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.resume) {
|
|
84
|
+
st.chat = sessions.getChat(String(options.resume));
|
|
85
|
+
if (!st.chat) throw Object.assign(new Error(`找不到会话:${options.resume}`), { userError: true });
|
|
86
|
+
} else if (options.continueLast) {
|
|
87
|
+
st.chat = sessions.listChats()[0] || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
st.yes = Boolean(options.yes);
|
|
91
|
+
return st;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ------------------------------ 展示 ------------------------------ */
|
|
95
|
+
|
|
96
|
+
function permissionBadge(st) {
|
|
97
|
+
const meta = settings.PERMISSIONS[st.runtime.permission] || settings.PERMISSIONS.normal;
|
|
98
|
+
const tone = st.runtime.permission === 'full' ? COLORS.red : st.runtime.permission === 'auto' ? COLORS.green : COLORS.faint;
|
|
99
|
+
return tone(meta.label);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function modeBadge(st) {
|
|
103
|
+
return st.runtime.mode === 'work' ? COLORS.accent('工作') : COLORS.faint('对话');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function promptFor(st) {
|
|
107
|
+
const model = settings.activeModel(settings.readConfig());
|
|
108
|
+
const name = model?.name || '未选择模型';
|
|
109
|
+
const marker = st.runtime.mode === 'work' ? COLORS.accent('❯') : COLORS.faint('❯');
|
|
110
|
+
const skillChip = st.runtime.skill ? ` ${COLORS.accent(`⛭ ${st.runtime.skill}`)}` : '';
|
|
111
|
+
return `${marker} ${COLORS.faint(name)}${skillChip} `
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function showStatus(st, { withRing = true } = {}) {
|
|
115
|
+
const chat = st.chat;
|
|
116
|
+
const model = settings.activeModel(settings.readConfig());
|
|
117
|
+
const info = contextInfo(chat, model);
|
|
118
|
+
const parts = [
|
|
119
|
+
modeBadge(st),
|
|
120
|
+
permissionBadge(st),
|
|
121
|
+
state.workspaceLabel(st.cwd),
|
|
122
|
+
`${chat ? chat.messages.filter((m) => m.role !== 'terminal').length : 0} 条消息`
|
|
123
|
+
];
|
|
124
|
+
const left = parts.filter(Boolean).join(COLORS.faint(' · '));
|
|
125
|
+
const right = withRing ? ringLine(info) : '';
|
|
126
|
+
const width = termWidth();
|
|
127
|
+
const gap = Math.max(2, width - COLORS.dim('').length - stripLen(left) - stripLen(right) - 2);
|
|
128
|
+
process.stdout.write(`\n${left}${' '.repeat(gap)}${right}\n\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function stripLen(text) {
|
|
132
|
+
return text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '').length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function showWelcome(st) {
|
|
136
|
+
require('./index').showBanner();
|
|
137
|
+
const config = settings.readConfig();
|
|
138
|
+
const model = settings.activeModel(config);
|
|
139
|
+
const chat = st.chat;
|
|
140
|
+
if (chat && chat.messages.length) {
|
|
141
|
+
print.hint(`已恢复会话「${chat.title}」(${chat.messages.filter((m) => m.role !== 'terminal').length} 条消息)`);
|
|
142
|
+
}
|
|
143
|
+
print.keyValue([
|
|
144
|
+
{ label: '模型', value: model ? `${model.providerName} / ${model.name}` : COLORS.yellow('未配置,运行 dgxay config init') },
|
|
145
|
+
{ label: '工作空间', value: st.cwd },
|
|
146
|
+
{ label: '权限', value: `${settings.PERMISSIONS[st.runtime.permission].label} · ${settings.PERMISSIONS[st.runtime.permission].desc}` },
|
|
147
|
+
{ label: '模式', value: st.runtime.mode === 'work' ? '工作模式' : '对话模式' }
|
|
148
|
+
]);
|
|
149
|
+
if (!model) {
|
|
150
|
+
print.blank();
|
|
151
|
+
print.warn('还没有可用模型,请输入 /config 或退出后运行 dgxay config init');
|
|
152
|
+
}
|
|
153
|
+
print.blank();
|
|
154
|
+
print.hint('提示:/ 查看命令 · ! 执行终端命令 · \\ 结尾换行 · Ctrl+C 中断 · Ctrl+D 退出');
|
|
155
|
+
print.blank();
|
|
156
|
+
void chat;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ------------------------------ 消息与历史 ------------------------------ */
|
|
160
|
+
|
|
161
|
+
function ensureChat(st) {
|
|
162
|
+
if (st.chat) return st.chat;
|
|
163
|
+
const chat = sessions.createChat({ mode: st.runtime.mode, skill: st.runtime.skill });
|
|
164
|
+
st.chat = chat;
|
|
165
|
+
return chat;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pushMessage(chat, message) {
|
|
169
|
+
chat.messages.push({ ts: Date.now(), ...message });
|
|
170
|
+
chat.updatedAt = Date.now();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function persist(st) {
|
|
174
|
+
if (!st.chat) return;
|
|
175
|
+
const chat = st.chat;
|
|
176
|
+
if (!chat.messages.length) return;
|
|
177
|
+
if (!chat.title || chat.title === '新对话') {
|
|
178
|
+
const firstUser = chat.messages.find((m) => m.role === 'user' && m.content);
|
|
179
|
+
if (firstUser) chat.title = sessions.titleFrom(firstUser.content);
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
sessions.saveChat(chat);
|
|
183
|
+
} catch {
|
|
184
|
+
/* ignore */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 每轮请求都会重建 apiMessages,这里与桌面版一致:只发送最近 24 条 */
|
|
189
|
+
function historyForAgent(st) {
|
|
190
|
+
return sessions.trimHistory(sessions.buildHistory(st.chat), HISTORY_LIMIT);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ------------------------------ 对话 ------------------------------ */
|
|
194
|
+
|
|
195
|
+
async function sendMessage(st, editor, text, options = {}) {
|
|
196
|
+
const promptText = String(text || '').trim();
|
|
197
|
+
if (!promptText) return 0;
|
|
198
|
+
const config = settings.readConfig();
|
|
199
|
+
const model = settings.activeModel({
|
|
200
|
+
...config,
|
|
201
|
+
activeProviderId: st.runtime.providerId || config.activeProviderId,
|
|
202
|
+
activeModelId: st.runtime.modelId || config.activeModelId
|
|
203
|
+
});
|
|
204
|
+
if (!model) {
|
|
205
|
+
print.warn('还没有可用模型:先运行 dgxay config init 配置供应商');
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const chat = ensureChat(st);
|
|
210
|
+
chat.mode = st.runtime.mode;
|
|
211
|
+
chat.skill = st.runtime.skill;
|
|
212
|
+
pushMessage(chat, { role: 'user', content: promptText, mode: st.runtime.mode, permission: st.runtime.permission });
|
|
213
|
+
persist(st);
|
|
214
|
+
|
|
215
|
+
const jsonMode = String(options.output || 'text') === 'json';
|
|
216
|
+
const renderer = new StreamRenderer({
|
|
217
|
+
output: process.stdout,
|
|
218
|
+
editor,
|
|
219
|
+
redraw: Boolean(process.stdout.isTTY) && !jsonMode
|
|
220
|
+
});
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
st.requestController = controller;
|
|
223
|
+
st.streaming = true;
|
|
224
|
+
st.abortRequested = false;
|
|
225
|
+
editor.setAbortHandler(() => {
|
|
226
|
+
if (!st.streaming || st.abortRequested) return false;
|
|
227
|
+
st.abortRequested = true;
|
|
228
|
+
controller.abort();
|
|
229
|
+
process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} 正在中断…`)}\n`);
|
|
230
|
+
return true;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const startedAt = Date.now();
|
|
234
|
+
let result;
|
|
235
|
+
try {
|
|
236
|
+
result = await runAgent({
|
|
237
|
+
config,
|
|
238
|
+
messages: historyForAgent(st),
|
|
239
|
+
providerId: st.runtime.providerId,
|
|
240
|
+
modelId: st.runtime.modelId,
|
|
241
|
+
mode: st.runtime.mode,
|
|
242
|
+
permission: st.runtime.permission,
|
|
243
|
+
workspace: st.cwd,
|
|
244
|
+
skill: st.runtime.skill,
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
askUser: async (request) => {
|
|
247
|
+
if (st.yes) return true;
|
|
248
|
+
// 先让渲染器把已收到的正文落地并收起预览行,再暂停编辑器后弹出确认框
|
|
249
|
+
renderer.endInline();
|
|
250
|
+
editor.suspend();
|
|
251
|
+
const allowed = await askApproval(request);
|
|
252
|
+
editor.resume();
|
|
253
|
+
return allowed;
|
|
254
|
+
},
|
|
255
|
+
onEvent: (event) => {
|
|
256
|
+
if (jsonMode && !['review', 'review-start', 'waiting', 'file'].includes(event.type)) return;
|
|
257
|
+
renderer.handle(event);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
} finally {
|
|
261
|
+
st.streaming = false;
|
|
262
|
+
st.abortRequested = false;
|
|
263
|
+
st.requestController = null;
|
|
264
|
+
editor.setAbortHandler(null);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const assistantMessage = {
|
|
268
|
+
role: 'assistant',
|
|
269
|
+
content: result.text || '',
|
|
270
|
+
error: result.error || '',
|
|
271
|
+
reasoningChars: result.reasoning?.length || 0,
|
|
272
|
+
usage: result.usage || null,
|
|
273
|
+
toolLog: result.toolLog,
|
|
274
|
+
files: result.files,
|
|
275
|
+
elapsedMs: Date.now() - startedAt
|
|
276
|
+
};
|
|
277
|
+
pushMessage(chat, assistantMessage);
|
|
278
|
+
if (chat.title === '新对话') chat.title = sessions.titleFrom(promptText);
|
|
279
|
+
persist(st);
|
|
280
|
+
st.lastResponse = result;
|
|
281
|
+
updateBusState(st);
|
|
282
|
+
|
|
283
|
+
if (result.error) return 1;
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ------------------------------ 终端命令 ------------------------------ */
|
|
288
|
+
|
|
289
|
+
async function runTerminal(st, editor, rawCommand) {
|
|
290
|
+
const command = String(rawCommand || '').trim();
|
|
291
|
+
if (!command) {
|
|
292
|
+
print.warn('请输入要执行的终端命令');
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
if (st.terminalBusy) {
|
|
296
|
+
print.warn('已有命令正在执行');
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const config = settings.readConfig();
|
|
300
|
+
const permission = st.runtime.permission;
|
|
301
|
+
if (permission === 'none') {
|
|
302
|
+
print.warn('无权限模式禁止执行终端命令');
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (permission === 'normal' || permission === 'auto') {
|
|
307
|
+
editor.suspend();
|
|
308
|
+
const allowed = await askApproval(
|
|
309
|
+
{
|
|
310
|
+
title: 'AI 请求执行终端命令',
|
|
311
|
+
operation: 'run_terminal',
|
|
312
|
+
detail: `命令:${command}`,
|
|
313
|
+
command,
|
|
314
|
+
workspace: st.cwd,
|
|
315
|
+
review: '',
|
|
316
|
+
reviewer: ''
|
|
317
|
+
},
|
|
318
|
+
{}
|
|
319
|
+
);
|
|
320
|
+
editor.resume();
|
|
321
|
+
if (!allowed) return 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const chat = ensureChat(st);
|
|
325
|
+
const message = {
|
|
326
|
+
role: 'terminal',
|
|
327
|
+
command,
|
|
328
|
+
cwd: st.cwd,
|
|
329
|
+
output: '',
|
|
330
|
+
stderr: '',
|
|
331
|
+
status: 'running',
|
|
332
|
+
ts: Date.now()
|
|
333
|
+
};
|
|
334
|
+
chat.messages.push(message);
|
|
335
|
+
st.terminalBusy = true;
|
|
336
|
+
|
|
337
|
+
editor.suspend();
|
|
338
|
+
process.stdout.write(`\n${COLORS.faint(` ${SIGN.arrow} ${command}`)}\n`);
|
|
339
|
+
const result = await terminal.runShellCommand({
|
|
340
|
+
command,
|
|
341
|
+
workspace: st.cwd,
|
|
342
|
+
onOutput: (chunk) => {
|
|
343
|
+
process.stdout.write(chunk);
|
|
344
|
+
if (!message._streamed) message._streamed = '';
|
|
345
|
+
message._streamed += chunk;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
st.terminalBusy = false;
|
|
349
|
+
|
|
350
|
+
message.output = result.output;
|
|
351
|
+
message.stderr = result.stderr;
|
|
352
|
+
message.exitCode = Number.isInteger(result.code) ? result.code : null;
|
|
353
|
+
message.status = result.aborted ? 'done' : result.code === 0 ? 'done' : 'error';
|
|
354
|
+
if (result.error) message.error = result.error;
|
|
355
|
+
if (chat.title === '新对话') chat.title = sessions.titleFrom(`终端 · ${command}`);
|
|
356
|
+
chat.updatedAt = Date.now();
|
|
357
|
+
persist(st);
|
|
358
|
+
|
|
359
|
+
const code = message.exitCode;
|
|
360
|
+
if (result.aborted) print.hint('已停止');
|
|
361
|
+
else if (code === 0) process.stdout.write(COLORS.green(` ${SIGN.ok} 完成\n`));
|
|
362
|
+
else process.stdout.write(COLORS.red(` ${SIGN.fail} 退出码 ${code ?? '未知'}\n`));
|
|
363
|
+
editor.resume();
|
|
364
|
+
return code === 0 ? 0 : 1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ------------------------------ 状态栏刷新 ------------------------------ */
|
|
368
|
+
|
|
369
|
+
function updateBusState(st) {
|
|
370
|
+
try {
|
|
371
|
+
sessionBus.write({
|
|
372
|
+
pid: process.pid,
|
|
373
|
+
cwd: st.cwd,
|
|
374
|
+
chatId: st.chat?.id || '',
|
|
375
|
+
title: st.chat?.title || '',
|
|
376
|
+
mode: st.runtime.mode,
|
|
377
|
+
permission: st.runtime.permission,
|
|
378
|
+
providerId: st.runtime.providerId,
|
|
379
|
+
modelId: st.runtime.modelId,
|
|
380
|
+
skill: st.runtime.skill,
|
|
381
|
+
streaming: st.streaming
|
|
382
|
+
});
|
|
383
|
+
} catch {
|
|
384
|
+
/* ignore */
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* ------------------------------ 斜杠命令 ------------------------------ */
|
|
389
|
+
|
|
390
|
+
async function handleSlash(st, editor, input) {
|
|
391
|
+
const text = String(input || '').trim();
|
|
392
|
+
const [head, ...restParts] = text.split(/\s+/);
|
|
393
|
+
const rest = restParts.join(' ').trim();
|
|
394
|
+
const command = findCommand(head);
|
|
395
|
+
|
|
396
|
+
if (!command) {
|
|
397
|
+
print.warn(`未知命令:${head},输入 /help 查看全部命令`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
switch (command.name) {
|
|
402
|
+
case '/help':
|
|
403
|
+
showHelp();
|
|
404
|
+
return;
|
|
405
|
+
case '/new': {
|
|
406
|
+
persist(st);
|
|
407
|
+
st.chat = sessions.createChat({ mode: st.runtime.mode, skill: st.runtime.skill });
|
|
408
|
+
updateBusState(st);
|
|
409
|
+
print.ok('已开始新对话');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
case '/list':
|
|
413
|
+
listChats(st);
|
|
414
|
+
return;
|
|
415
|
+
case '/resume': {
|
|
416
|
+
const chats = sessions.listChats();
|
|
417
|
+
if (!chats.length) {
|
|
418
|
+
print.warn('还没有历史对话');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
let target;
|
|
422
|
+
try {
|
|
423
|
+
target = sessions.resolveChat(rest || '1', chats);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
print.fail(err.message);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!target) {
|
|
429
|
+
print.fail(`找不到会话:${rest}`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
persist(st);
|
|
433
|
+
st.chat = target;
|
|
434
|
+
updateBusState(st);
|
|
435
|
+
print.ok(`已恢复「${target.title}」(${target.messages.filter((m) => m.role !== 'terminal').length} 条消息)`);
|
|
436
|
+
printThread(st);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
case '/delete': {
|
|
440
|
+
const chats = sessions.listChats();
|
|
441
|
+
const target = rest ? sessions.resolveChat(rest, chats) : st.chat;
|
|
442
|
+
if (!target) {
|
|
443
|
+
print.fail(`找不到会话:${rest}`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
sessions.deleteChat(target.id);
|
|
447
|
+
if (st.chat?.id === target.id) st.chat = sessions.listChats()[0] || null;
|
|
448
|
+
updateBusState(st);
|
|
449
|
+
print.ok(`已删除「${target.title}」`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
case '/rename': {
|
|
453
|
+
if (!st.chat) {
|
|
454
|
+
print.warn('当前没有会话');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (!rest) {
|
|
458
|
+
print.fail('用法:/rename <新标题>');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
st.chat.title = rest;
|
|
462
|
+
persist(st);
|
|
463
|
+
print.ok(`已重命名为「${rest}」`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
case '/compress':
|
|
467
|
+
await compress(st, editor);
|
|
468
|
+
return;
|
|
469
|
+
case '/skill':
|
|
470
|
+
await handleSkillCommand(st, rest);
|
|
471
|
+
return;
|
|
472
|
+
case '/model':
|
|
473
|
+
handleModelCommand(st, rest);
|
|
474
|
+
return;
|
|
475
|
+
case '/provider':
|
|
476
|
+
handleProviderCommand(st, rest);
|
|
477
|
+
return;
|
|
478
|
+
case '/mode':
|
|
479
|
+
handleModeCommand(st, rest);
|
|
480
|
+
return;
|
|
481
|
+
case '/permission':
|
|
482
|
+
handlePermissionCommand(st, rest);
|
|
483
|
+
return;
|
|
484
|
+
case '/cd':
|
|
485
|
+
handleCd(st, rest);
|
|
486
|
+
return;
|
|
487
|
+
case '/ls':
|
|
488
|
+
handleLs(st, rest);
|
|
489
|
+
return;
|
|
490
|
+
case '/pwd':
|
|
491
|
+
print.write(st.cwd);
|
|
492
|
+
return;
|
|
493
|
+
case '/term':
|
|
494
|
+
await runTerminal(st, editor, rest);
|
|
495
|
+
return;
|
|
496
|
+
case '/context': {
|
|
497
|
+
const config = settings.readConfig();
|
|
498
|
+
const model = settings.activeModel(config);
|
|
499
|
+
process.stdout.write(`\n${contextReport(contextInfo(st.chat, model))}\n\n`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
case '/mcp':
|
|
503
|
+
await handleMcpCommand(st, rest);
|
|
504
|
+
return;
|
|
505
|
+
case '/theme':
|
|
506
|
+
handleTheme(rest);
|
|
507
|
+
return;
|
|
508
|
+
case '/clear':
|
|
509
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
510
|
+
return;
|
|
511
|
+
case '/exit':
|
|
512
|
+
st.exitRequested = true;
|
|
513
|
+
return;
|
|
514
|
+
default:
|
|
515
|
+
print.warn(`命令尚未实现:${command.name}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function showHelp() {
|
|
520
|
+
print.blank();
|
|
521
|
+
print.heading('斜杠命令');
|
|
522
|
+
// 命令名列按显示宽度对齐(中文别名占两格,不能用 padEnd)
|
|
523
|
+
const nameWidth = Math.max(...SLASH_COMMANDS.map((cmd) => displayWidth(cmd.name))) + 2;
|
|
524
|
+
for (const cmd of SLASH_COMMANDS) {
|
|
525
|
+
const name = `${cmd.name}${' '.repeat(Math.max(0, nameWidth - displayWidth(cmd.name)))}`;
|
|
526
|
+
print.write(` ${COLORS.accent(name)}${cmd.desc}`);
|
|
527
|
+
if (cmd.usage !== cmd.name) print.write(` ${' '.repeat(nameWidth)}${COLORS.faint(cmd.usage)}`);
|
|
528
|
+
}
|
|
529
|
+
print.blank();
|
|
530
|
+
print.heading('输入约定');
|
|
531
|
+
const tips = [
|
|
532
|
+
['! 命令', '执行终端命令,输出进入对话上下文(如 !git status)'],
|
|
533
|
+
['\\ 结尾', '续行输入多行消息'],
|
|
534
|
+
['Tab', '在 / 命令后循环选择补全项'],
|
|
535
|
+
['Ctrl+C', '生成中中断;空闲时退出;输入框有内容时清空'],
|
|
536
|
+
['Ctrl+D', '退出'],
|
|
537
|
+
['↑ / ↓', '浏览历史输入']
|
|
538
|
+
];
|
|
539
|
+
for (const [key, desc] of tips) {
|
|
540
|
+
print.write(` ${COLORS.cyan(key)} ${desc}`);
|
|
541
|
+
}
|
|
542
|
+
print.blank();
|
|
543
|
+
print.hint(`其他操作也可以直接用子命令:${BIN} provider list / ${BIN} mcp start ...`);
|
|
544
|
+
print.blank();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function listChats(st) {
|
|
548
|
+
const chats = sessions.listChats();
|
|
549
|
+
print.blank();
|
|
550
|
+
if (!chats.length) {
|
|
551
|
+
print.hint('还没有历史对话');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
print.heading(`历史对话(${chats.length})`);
|
|
555
|
+
const rows = chats.map((chat, index) => {
|
|
556
|
+
const messages = chat.messages.filter((m) => m.role !== 'terminal');
|
|
557
|
+
const last = messages[messages.length - 1];
|
|
558
|
+
const date = new Date(chat.updatedAt || chat.createdAt);
|
|
559
|
+
const stamp = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(
|
|
560
|
+
date.getHours()
|
|
561
|
+
).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
562
|
+
return [
|
|
563
|
+
chat.id === st.chat?.id ? COLORS.accent('*') : ' ',
|
|
564
|
+
String(index + 1),
|
|
565
|
+
chat.title || '新对话',
|
|
566
|
+
String(messages.length),
|
|
567
|
+
stamp,
|
|
568
|
+
chat.mode === 'work' ? '工作' : '对话',
|
|
569
|
+
last?.content ? last.content.slice(0, 40) : ''
|
|
570
|
+
];
|
|
571
|
+
});
|
|
572
|
+
print.table(
|
|
573
|
+
[
|
|
574
|
+
{ title: '', max: 1 },
|
|
575
|
+
{ title: '#', max: 3 },
|
|
576
|
+
{ title: '标题', max: 30 },
|
|
577
|
+
{ title: '消息', max: 4 },
|
|
578
|
+
{ title: '更新时间', max: 12 },
|
|
579
|
+
{ title: '模式', max: 4 },
|
|
580
|
+
{ title: '最后内容', max: 40 }
|
|
581
|
+
],
|
|
582
|
+
rows
|
|
583
|
+
);
|
|
584
|
+
print.blank();
|
|
585
|
+
print.hint('/resume <序号> 恢复 · /delete <序号> 删除');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function printThread(st) {
|
|
589
|
+
const chat = st.chat;
|
|
590
|
+
if (!chat?.messages?.length) return;
|
|
591
|
+
for (const message of chat.messages) {
|
|
592
|
+
if (message.role === 'user') {
|
|
593
|
+
process.stdout.write(`\n${COLORS.faint('› 你')}\n${message.content}\n`);
|
|
594
|
+
} else if (message.role === 'assistant') {
|
|
595
|
+
process.stdout.write(`\n${COLORS.accent('•')} ${COLORS.bold('DgxayCode')}\n`);
|
|
596
|
+
if (message.content) process.stdout.write(require('./ansi').renderMarkdown(message.content, { width: termWidth() }));
|
|
597
|
+
if (message.error) process.stdout.write(COLORS.red(message.error) + '\n');
|
|
598
|
+
} else if (message.role === 'terminal') {
|
|
599
|
+
process.stdout.write(`\n${COLORS.faint(`! ${message.command}`)}\n`);
|
|
600
|
+
process.stdout.write(`${message.output || ''}${message.stderr || ''}`.slice(-4000));
|
|
601
|
+
if (!String(message.output || '').endsWith('\n')) process.stdout.write('\n');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
process.stdout.write('\n');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function compress(st, editor) {
|
|
608
|
+
if (st.streaming) {
|
|
609
|
+
print.warn('请等待当前回复结束');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const chat = st.chat;
|
|
613
|
+
if (!chat?.messages?.length) {
|
|
614
|
+
print.warn('当前对话还没有可压缩的内容');
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const config = settings.readConfig();
|
|
618
|
+
const model = settings.activeModel(config);
|
|
619
|
+
if (!model) {
|
|
620
|
+
print.warn('请先配置模型');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const before = sessions.estimateChatTokens(chat);
|
|
624
|
+
editor.suspend();
|
|
625
|
+
process.stdout.write(`${COLORS.faint(`${SIGN.run} 正在压缩上下文…`)}`);
|
|
626
|
+
try {
|
|
627
|
+
const result = await compressMessages({
|
|
628
|
+
config,
|
|
629
|
+
providerId: st.runtime.providerId || config.activeProviderId,
|
|
630
|
+
modelId: st.runtime.modelId || config.activeModelId,
|
|
631
|
+
messages: sessions.buildHistory(chat)
|
|
632
|
+
});
|
|
633
|
+
if (!result.summary) throw new Error('模型没有返回摘要');
|
|
634
|
+
chat.messages = [
|
|
635
|
+
{ role: 'assistant', content: `> 上下文已压缩,仅保留重要信息。\n\n${result.summary}`, ts: Date.now(), compressed: true }
|
|
636
|
+
];
|
|
637
|
+
chat.updatedAt = Date.now();
|
|
638
|
+
persist(st);
|
|
639
|
+
const after = sessions.estimateChatTokens(chat);
|
|
640
|
+
process.stdout.write('\r\x1b[2K');
|
|
641
|
+
print.ok(`上下文已压缩:${sessions.formatTokens(before)} → ${sessions.formatTokens(after)} tokens(估算)`);
|
|
642
|
+
process.stdout.write(`\n${require('./ansi').renderMarkdown(result.summary, { width: termWidth() })}\n`);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
process.stdout.write('\r\x1b[2K');
|
|
645
|
+
print.fail(err.message || '压缩失败');
|
|
646
|
+
} finally {
|
|
647
|
+
editor.resume();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function handleSkillCommand(st, rest) {
|
|
652
|
+
if (!rest) {
|
|
653
|
+
print.blank();
|
|
654
|
+
print.heading(`技能(${settings.homeRelative(cfg.SKILLS_DIR)})`);
|
|
655
|
+
const list = skills.listSkills();
|
|
656
|
+
if (!list.length) {
|
|
657
|
+
print.hint(' 还没有技能。在 ~/.dcb/skills 下放一个 name.md 或 <目录>/SKILL.md');
|
|
658
|
+
print.blank();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const rows = list.map((skill, index) => [
|
|
662
|
+
String(index + 1),
|
|
663
|
+
skill.name,
|
|
664
|
+
st.runtime.skill === skill.name ? COLORS.green('已启用') : '',
|
|
665
|
+
skill.description
|
|
666
|
+
]);
|
|
667
|
+
print.table(
|
|
668
|
+
[
|
|
669
|
+
{ title: '#', max: 3 },
|
|
670
|
+
{ title: '名称', max: 22 },
|
|
671
|
+
{ title: '状态', max: 8 },
|
|
672
|
+
{ title: '描述', max: 60 }
|
|
673
|
+
],
|
|
674
|
+
rows
|
|
675
|
+
);
|
|
676
|
+
print.blank();
|
|
677
|
+
print.hint('/skill off 关闭 · /skill <名称> [提示词] 启用(本次对话)');
|
|
678
|
+
print.blank();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (rest === 'off' || rest === 'clear') {
|
|
682
|
+
st.runtime.skill = '';
|
|
683
|
+
if (st.chat) st.chat.skill = '';
|
|
684
|
+
updateBusState(st);
|
|
685
|
+
print.ok('已关闭技能');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const list = skills.listSkills();
|
|
689
|
+
const picked = list
|
|
690
|
+
.filter((item) => rest === item.name || rest.startsWith(`${item.name} `))
|
|
691
|
+
.sort((a, b) => b.name.length - a.name.length)[0];
|
|
692
|
+
if (!picked) {
|
|
693
|
+
print.warn(`未找到技能:${rest}。输入 /skill 查看列表`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
st.runtime.skill = picked.name;
|
|
697
|
+
if (st.chat) st.chat.skill = picked.name;
|
|
698
|
+
updateBusState(st);
|
|
699
|
+
print.ok(`已在本次对话启用技能:${picked.name}`);
|
|
700
|
+
const prompt = rest.slice(picked.name.length).trim();
|
|
701
|
+
if (prompt) return prompt;
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function handleModelCommand(st, rest) {
|
|
706
|
+
const config = settings.readConfig();
|
|
707
|
+
const provider = settings.findProvider(st.runtime.providerId) || settings.activeProvider(config);
|
|
708
|
+
const models = provider?.models || [];
|
|
709
|
+
if (!models.length) {
|
|
710
|
+
print.warn('当前供应商没有模型,先运行 dgxay provider add');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const current = models.find((m) => m.id === st.runtime.modelId) || models[0];
|
|
714
|
+
if (!rest) {
|
|
715
|
+
print.blank();
|
|
716
|
+
print.heading(`模型(${provider.name})`);
|
|
717
|
+
models.forEach((model, index) => {
|
|
718
|
+
const active = model.id === current?.id;
|
|
719
|
+
print.write(
|
|
720
|
+
` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(String(index + 1).padStart(2))}. ${
|
|
721
|
+
active ? COLORS.accent(model.name || model.id) : model.name || model.id
|
|
722
|
+
} ${COLORS.faint(model.id !== (model.name || model.id) ? `(${model.id})` : '')}${
|
|
723
|
+
model.contextWindow ? COLORS.faint(` · ${sessions.formatTokens(model.contextWindow)}`) : ''
|
|
724
|
+
}`
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
print.blank();
|
|
728
|
+
print.hint('/model <序号|ID> 切换 · 该选择会写入配置');
|
|
729
|
+
print.blank();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const index = Number(rest);
|
|
733
|
+
const target =
|
|
734
|
+
(Number.isInteger(index) && index >= 1 && index <= models.length ? models[index - 1] : null) ||
|
|
735
|
+
models.find((m) => m.id === rest) ||
|
|
736
|
+
models.find((m) => m.id.includes(rest));
|
|
737
|
+
if (!target) {
|
|
738
|
+
print.fail(`找不到模型:${rest}`);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
settings.setActiveModel(provider.id, target.id);
|
|
742
|
+
st.runtime.modelId = target.id;
|
|
743
|
+
st.runtime.providerId = provider.id;
|
|
744
|
+
updateBusState(st);
|
|
745
|
+
print.ok(`已切换到 ${target.name || target.id}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function handleProviderCommand(st, rest) {
|
|
749
|
+
const config = settings.readConfig();
|
|
750
|
+
const providers = config.providers || [];
|
|
751
|
+
if (!providers.length) {
|
|
752
|
+
print.warn('还没有供应商,运行 dgxay config init');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const current = settings.findProvider(st.runtime.providerId) || settings.activeProvider(config);
|
|
756
|
+
if (!rest) {
|
|
757
|
+
print.blank();
|
|
758
|
+
print.heading('供应商');
|
|
759
|
+
providers.forEach((provider, index) => {
|
|
760
|
+
const active = provider.id === current?.id;
|
|
761
|
+
print.write(
|
|
762
|
+
` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(String(index + 1).padStart(2))}. ${
|
|
763
|
+
active ? COLORS.accent(provider.name) : provider.name
|
|
764
|
+
} ${COLORS.faint(settings.PROTOCOL_META[provider.protocol || 'openai'].short)} ${COLORS.faint(
|
|
765
|
+
`${(provider.models || []).length} 个模型`
|
|
766
|
+
)}`
|
|
767
|
+
);
|
|
768
|
+
});
|
|
769
|
+
print.blank();
|
|
770
|
+
print.hint('/provider <序号|名称> 切换');
|
|
771
|
+
print.blank();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const index = Number(rest);
|
|
775
|
+
const target =
|
|
776
|
+
(Number.isInteger(index) && index >= 1 && index <= providers.length ? providers[index - 1] : null) ||
|
|
777
|
+
settings.findProvider(rest);
|
|
778
|
+
if (!target) {
|
|
779
|
+
print.fail(`找不到供应商:${rest}`);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const model = target.models?.[0];
|
|
783
|
+
if (!model) {
|
|
784
|
+
print.fail(`供应商 ${target.name} 没有模型`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
settings.saveConfig({ activeProviderId: target.id, activeModelId: model.id });
|
|
788
|
+
st.runtime.providerId = target.id;
|
|
789
|
+
st.runtime.modelId = model.id;
|
|
790
|
+
updateBusState(st);
|
|
791
|
+
print.ok(`已切换到 ${target.name} / ${model.name || model.id}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function handleModeCommand(st, rest) {
|
|
795
|
+
const next = rest ? (rest === 'work' || rest === '工作' ? 'work' : 'chat') : st.runtime.mode === 'work' ? 'chat' : 'work';
|
|
796
|
+
st.runtime.mode = next;
|
|
797
|
+
if (st.chat) st.chat.mode = next;
|
|
798
|
+
updateBusState(st);
|
|
799
|
+
print.ok(next === 'work' ? '已切换到工作模式' : '已切换到对话模式');
|
|
800
|
+
if (next === 'work' && !st.cwd) print.hint('当前没有工作空间,请用 /cd <目录> 选择');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function handlePermissionCommand(st, rest) {
|
|
804
|
+
const config = settings.readConfig();
|
|
805
|
+
if (!rest) {
|
|
806
|
+
print.blank();
|
|
807
|
+
print.heading('权限');
|
|
808
|
+
const available = settings.availablePermissions(config);
|
|
809
|
+
Object.entries(settings.PERMISSIONS).forEach(([key, meta]) => {
|
|
810
|
+
const usable = available.includes(key);
|
|
811
|
+
const active = st.runtime.permission === key;
|
|
812
|
+
print.write(
|
|
813
|
+
` ${active ? COLORS.accent('❯') : ' '} ${COLORS.faint(key.padEnd(8))} ${
|
|
814
|
+
usable ? meta.label : COLORS.faint(`${meta.label}(不可用)`)
|
|
815
|
+
} ${COLORS.faint(meta.desc)}`
|
|
816
|
+
);
|
|
817
|
+
});
|
|
818
|
+
if (!available.includes('auto')) print.hint('自动审查需要供应商配置有效的审查模型');
|
|
819
|
+
print.blank();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!settings.PERMISSIONS[rest]) {
|
|
823
|
+
print.fail('权限只能是 none / normal / auto / full');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (rest === 'auto' && !settings.hasReviewModel(config)) {
|
|
827
|
+
print.fail('自动审查不可用:当前或默认供应商没有配置有效的审查模型');
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
st.runtime.permission = rest;
|
|
831
|
+
settings.setPermission(rest);
|
|
832
|
+
updateBusState(st);
|
|
833
|
+
print.ok(`权限已切换为:${settings.PERMISSIONS[rest].label}`);
|
|
834
|
+
if (rest === 'full') print.warn('完全权限下 AI 可以直接执行任意命令、读写任意文件,请确认这是你想要的');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function handleCd(st, rest) {
|
|
838
|
+
if (!rest) {
|
|
839
|
+
print.write(st.cwd);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const target = path.resolve(st.cwd, rest);
|
|
843
|
+
let stat;
|
|
844
|
+
try {
|
|
845
|
+
stat = fs.statSync(target);
|
|
846
|
+
} catch {
|
|
847
|
+
print.fail(`目录不存在:${target}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (!stat.isDirectory()) {
|
|
851
|
+
print.fail(`不是文件夹:${target}`);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
st.cwd = target;
|
|
855
|
+
settings.setWorkspace(target);
|
|
856
|
+
updateBusState(st);
|
|
857
|
+
print.ok(`工作空间:${target}`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function handleLs(st, rest) {
|
|
861
|
+
const target = rest ? path.resolve(st.cwd, rest) : st.cwd;
|
|
862
|
+
let entries = [];
|
|
863
|
+
try {
|
|
864
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
865
|
+
} catch (err) {
|
|
866
|
+
print.fail(`无法读取 ${target}:${err.code || err.message}`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
entries.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
870
|
+
print.blank();
|
|
871
|
+
print.write(COLORS.faint(`${target}`));
|
|
872
|
+
for (const entry of entries.slice(0, 200)) {
|
|
873
|
+
if (entry.name.startsWith('.') && entry.name !== '.env.example') continue;
|
|
874
|
+
let size = '';
|
|
875
|
+
if (!entry.isDirectory()) {
|
|
876
|
+
try {
|
|
877
|
+
size = `${fs.statSync(path.join(target, entry.name)).size} B`;
|
|
878
|
+
} catch {
|
|
879
|
+
size = '';
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
print.write(` ${entry.isDirectory() ? COLORS.accent(`${entry.name}/`) : entry.name}${COLORS.faint(size ? ` ${size}` : '')}`);
|
|
883
|
+
}
|
|
884
|
+
print.blank();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function handleMcpCommand(st, rest) {
|
|
888
|
+
const [action, ...others] = rest.split(/\s+/).filter(Boolean);
|
|
889
|
+
const arg = others.join(' ');
|
|
890
|
+
if (!action || action === 'list' || action === 'ls') {
|
|
891
|
+
const list = mcp.listServers();
|
|
892
|
+
print.blank();
|
|
893
|
+
if (!list.length) {
|
|
894
|
+
print.hint(`还没有 MCP server,用 /mcp add 或 ${BIN} mcp add 添加`);
|
|
895
|
+
print.blank();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
print.heading(`MCP Server(${list.length})`);
|
|
899
|
+
const rows = list.map((server, index) => [
|
|
900
|
+
String(index + 1),
|
|
901
|
+
server.name,
|
|
902
|
+
server.status === 'running' ? COLORS.green('运行中') : server.status === 'error' ? COLORS.red('异常') : '已停止',
|
|
903
|
+
server.pid ? String(server.pid) : '',
|
|
904
|
+
`${server.command} ${(server.args || []).join(' ')}`
|
|
905
|
+
]);
|
|
906
|
+
print.table(
|
|
907
|
+
[
|
|
908
|
+
{ title: '#', max: 3 },
|
|
909
|
+
{ title: '名称', max: 18 },
|
|
910
|
+
{ title: '状态', max: 8 },
|
|
911
|
+
{ title: 'PID', max: 8 },
|
|
912
|
+
{ title: '命令', max: 50 }
|
|
913
|
+
],
|
|
914
|
+
rows
|
|
915
|
+
);
|
|
916
|
+
print.blank();
|
|
917
|
+
print.hint('/mcp start <名称> · /mcp stop <名称> · /mcp logs <名称> · /mcp add name command args');
|
|
918
|
+
print.blank();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const server = mcp.findServer(arg);
|
|
922
|
+
if (action === 'start') {
|
|
923
|
+
if (!server) return print.fail(`找不到 MCP:${arg}`);
|
|
924
|
+
try {
|
|
925
|
+
const result = mcp.startServer(server.id);
|
|
926
|
+
print.ok(`已启动 ${server.name}(pid ${result.pid})`);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
print.fail(err.message);
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (action === 'stop') {
|
|
933
|
+
if (!server) return print.fail(`找不到 MCP:${arg}`);
|
|
934
|
+
mcp.stopServer(server.id);
|
|
935
|
+
print.ok(`已停止 ${server.name}`);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (action === 'logs') {
|
|
939
|
+
if (!server) return print.fail(`找不到 MCP:${arg}`);
|
|
940
|
+
const logs = mcp.statusOf(server.id).logs || [];
|
|
941
|
+
if (!logs.length) return print.hint('暂无日志');
|
|
942
|
+
for (const entry of logs) {
|
|
943
|
+
print.write(`${entry.stream === 'stderr' ? COLORS.red(entry.text) : entry.text}`);
|
|
944
|
+
}
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (action === 'add') {
|
|
948
|
+
const [name, command, ...args] = others;
|
|
949
|
+
if (!name || !command) return print.fail('用法:/mcp add <名称> <命令> [参数…]');
|
|
950
|
+
const created = mcp.addServer({ name, command, args });
|
|
951
|
+
print.ok(`已添加 MCP「${created.name}」,用 /mcp start ${created.name} 启动`);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (action === 'rm' || action === 'remove') {
|
|
955
|
+
if (!server) return print.fail(`找不到 MCP:${arg}`);
|
|
956
|
+
mcp.removeServer(server.id);
|
|
957
|
+
print.ok(`已删除 ${server.name}`);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
print.fail(`未知的 /mcp 子命令:${action}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function handleTheme(rest) {
|
|
964
|
+
const mode = String(rest || '').toLowerCase();
|
|
965
|
+
if (mode === 'on' || mode === 'color') {
|
|
966
|
+
setColorMode('ansi256');
|
|
967
|
+
print.ok('已开启彩色输出');
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (mode === 'off' || mode === 'plain') {
|
|
971
|
+
setColorMode('none');
|
|
972
|
+
print.ok('已关闭彩色输出');
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
print.write(`彩色输出:${colorEnabled() ? '开启' : '关闭'}(/theme on | off)`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/* ------------------------------ 输入分发 ------------------------------ */
|
|
979
|
+
|
|
980
|
+
async function handleInput(st, editor, raw) {
|
|
981
|
+
const line = String(raw || '');
|
|
982
|
+
const text = line.trim();
|
|
983
|
+
if (!text) return;
|
|
984
|
+
|
|
985
|
+
if (text.startsWith('/')) {
|
|
986
|
+
const command = findCommand(text);
|
|
987
|
+
if (command?.name === '/skill') {
|
|
988
|
+
const result = await handleSkillCommand(st, text.split(/\s+/).slice(1).join(' ').trim());
|
|
989
|
+
if (typeof result === 'string') return sendMessage(st, editor, result);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
await handleSlash(st, editor, text);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (text.startsWith('!') && text.length > 1) {
|
|
997
|
+
editor.pushHistory(line);
|
|
998
|
+
await runTerminal(st, editor, text.slice(1).trim());
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// 技能命令后面可以附带提示词:/skill name 提示词
|
|
1003
|
+
editor.pushHistory(line);
|
|
1004
|
+
await sendMessage(st, editor, text);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/* ------------------------------ 主循环 ------------------------------ */
|
|
1008
|
+
|
|
1009
|
+
/** 单次运行(dgxay "问题" / -p):与 run 子命令共用同一套逻辑 */
|
|
1010
|
+
async function singleShot(options) {
|
|
1011
|
+
const argv = [];
|
|
1012
|
+
const prompt = String(options.initialPrompt || '').trim();
|
|
1013
|
+
if (prompt) argv.push('-p', prompt);
|
|
1014
|
+
if (options.mode) argv.push('--mode', options.mode);
|
|
1015
|
+
if (options.permission) argv.push('--permission', options.permission);
|
|
1016
|
+
if (options.workspace) argv.push('--workspace', options.workspace);
|
|
1017
|
+
if (options.model) argv.push('--model', options.model);
|
|
1018
|
+
if (options.provider) argv.push('--provider', options.provider);
|
|
1019
|
+
if (options.skill) argv.push('--skill', options.skill);
|
|
1020
|
+
if (options.resume) argv.push('--resume', options.resume);
|
|
1021
|
+
if (options.continueLast) argv.push('--continue');
|
|
1022
|
+
if (options.output) argv.push('--output', options.output);
|
|
1023
|
+
if (options.yes) argv.push('--yes');
|
|
1024
|
+
if (!prompt) {
|
|
1025
|
+
print.fail('单次运行需要提示词');
|
|
1026
|
+
return 2;
|
|
1027
|
+
}
|
|
1028
|
+
return require('../commands/run').run({ argv, runtime: options });
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function start(options = {}) {
|
|
1032
|
+
if (options.noColor) setColorMode('none');
|
|
1033
|
+
if (options.single || options.print) return singleShot(options);
|
|
1034
|
+
return startInteractive(options);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** 交互模式主体 */
|
|
1038
|
+
async function startInteractive(options) {
|
|
1039
|
+
const config = settings.readConfig();
|
|
1040
|
+
if (!config.providers?.length) {
|
|
1041
|
+
// 还没有任何供应商:交互终端里直接进入配置引导
|
|
1042
|
+
print.warn('还没有配置任何供应商');
|
|
1043
|
+
if (!process.stdin.isTTY) {
|
|
1044
|
+
print.hint('运行 dgxay config init 完成首次配置');
|
|
1045
|
+
return 1;
|
|
1046
|
+
}
|
|
1047
|
+
const result = await require('../commands/config').run({
|
|
1048
|
+
argv: ['init'],
|
|
1049
|
+
runtime: options
|
|
1050
|
+
});
|
|
1051
|
+
if (result !== 0) return result;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const st = createRuntime(options);
|
|
1055
|
+
const editor = process.stdin.isTTY && process.stdout.isTTY
|
|
1056
|
+
? new LineEditor({
|
|
1057
|
+
prompt: promptFor(st),
|
|
1058
|
+
continuationPrompt: COLORS.faint(' … '),
|
|
1059
|
+
completer,
|
|
1060
|
+
multiline: true
|
|
1061
|
+
})
|
|
1062
|
+
: createLineReader(process.stdin);
|
|
1063
|
+
|
|
1064
|
+
showWelcome(st);
|
|
1065
|
+
updateBusState(st);
|
|
1066
|
+
const heartbeat = new sessionBus.Heartbeat(() => ({
|
|
1067
|
+
pid: process.pid,
|
|
1068
|
+
cwd: st.cwd,
|
|
1069
|
+
chatId: st.chat?.id || '',
|
|
1070
|
+
title: st.chat?.title || '',
|
|
1071
|
+
mode: st.runtime.mode,
|
|
1072
|
+
permission: st.runtime.permission,
|
|
1073
|
+
providerId: st.runtime.providerId,
|
|
1074
|
+
modelId: st.runtime.modelId,
|
|
1075
|
+
skill: st.runtime.skill
|
|
1076
|
+
}));
|
|
1077
|
+
heartbeat.start();
|
|
1078
|
+
|
|
1079
|
+
const cleanup = () => {
|
|
1080
|
+
heartbeat.stop();
|
|
1081
|
+
sessionBus.clear();
|
|
1082
|
+
editor.finish({ type: 'eof', value: '' });
|
|
1083
|
+
persist(st);
|
|
1084
|
+
mcp.killAll();
|
|
1085
|
+
try {
|
|
1086
|
+
editor.close?.();
|
|
1087
|
+
} catch {
|
|
1088
|
+
/* ignore */
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
// 只在交互终端里接管信号;管道 / 脚本模式下不能注册 SIGINT 监听,
|
|
1092
|
+
// 否则 Node 会认为“有人在处理信号”,进程永远无法自然退出。
|
|
1093
|
+
const signalHandlers = {};
|
|
1094
|
+
if (process.stdin.isTTY) {
|
|
1095
|
+
signalHandlers.SIGINT = () => {
|
|
1096
|
+
if (st.streaming) {
|
|
1097
|
+
st.abortRequested = true;
|
|
1098
|
+
st.requestController?.abort();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
cleanup();
|
|
1102
|
+
process.exit(0);
|
|
1103
|
+
};
|
|
1104
|
+
signalHandlers.SIGTERM = () => {
|
|
1105
|
+
cleanup();
|
|
1106
|
+
process.exit(0);
|
|
1107
|
+
};
|
|
1108
|
+
process.on('SIGINT', signalHandlers.SIGINT);
|
|
1109
|
+
process.on('SIGTERM', signalHandlers.SIGTERM);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
editor.setAbortHandler(() => {
|
|
1113
|
+
if (st.streaming && !st.abortRequested) {
|
|
1114
|
+
st.abortRequested = true;
|
|
1115
|
+
st.requestController?.abort();
|
|
1116
|
+
process.stdout.write(`${COLORS.yellow(`${SIGN.bullet} 正在中断…`)}\n`);
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
if (st.terminalBusy) return true;
|
|
1120
|
+
return false;
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
while (!st.exitRequested) {
|
|
1125
|
+
editor.prompt = promptFor(st);
|
|
1126
|
+
const answer = await editor.question();
|
|
1127
|
+
if (answer.type === 'eof' || answer.type === 'cancel') break;
|
|
1128
|
+
try {
|
|
1129
|
+
await handleInput(st, editor, answer.value);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
if (err?.userError) print.fail(err.message);
|
|
1132
|
+
else {
|
|
1133
|
+
print.fail(err?.message || String(err));
|
|
1134
|
+
if (process.env.DGXAY_DEBUG === '1' && err?.stack) process.stderr.write(`${err.stack}\n`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
} finally {
|
|
1139
|
+
if (signalHandlers.SIGINT) process.removeListener('SIGINT', signalHandlers.SIGINT);
|
|
1140
|
+
if (signalHandlers.SIGTERM) process.removeListener('SIGTERM', signalHandlers.SIGTERM);
|
|
1141
|
+
heartbeat.stop();
|
|
1142
|
+
sessionBus.clear();
|
|
1143
|
+
persist(st);
|
|
1144
|
+
mcp.killAll();
|
|
1145
|
+
try {
|
|
1146
|
+
editor.close?.();
|
|
1147
|
+
} catch {
|
|
1148
|
+
/* ignore */
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof process.stdin.pause === 'function') process.stdin.pause();
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
process.stdout.write(`${COLORS.faint('再见。用 dgxay -c 继续上一次对话。')}\n`);
|
|
1154
|
+
return 0;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
module.exports = {
|
|
1158
|
+
start,
|
|
1159
|
+
startInteractive,
|
|
1160
|
+
createRuntime,
|
|
1161
|
+
handleInput,
|
|
1162
|
+
sendMessage,
|
|
1163
|
+
runTerminal,
|
|
1164
|
+
showHelp,
|
|
1165
|
+
permissionBadge,
|
|
1166
|
+
promptFor
|
|
1167
|
+
};
|