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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent 循环:与桌面版 chat:send 的流程一致,把事件流交给上层(REPL / 单次运行)。
|
|
5
|
+
*
|
|
6
|
+
* 事件:
|
|
7
|
+
* status { text }
|
|
8
|
+
* delta { text } 正文增量
|
|
9
|
+
* reasoning { text } 思考增量
|
|
10
|
+
* tool { id, name, status: 'running'|'ok'|'error', args, output, review }
|
|
11
|
+
* review { toolId, name, kind, decision, reason, reviewer, modelId }
|
|
12
|
+
* review-start { toolId, name, kind }
|
|
13
|
+
* waiting { toolId, name, kind, permission }
|
|
14
|
+
* file { action, path, display, bytes }
|
|
15
|
+
* done { text, usage }
|
|
16
|
+
* error { message }
|
|
17
|
+
* aborted {}
|
|
18
|
+
*
|
|
19
|
+
* 返回 { ok, aborted, text, reasoning, usage, toolLog, files, error }。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const { streamChat } = require('./llm');
|
|
24
|
+
const tools = require('./tools');
|
|
25
|
+
const skills = require('../services/skills');
|
|
26
|
+
const {
|
|
27
|
+
systemPromptFor,
|
|
28
|
+
resolveProviderAuth,
|
|
29
|
+
resolveReviewer,
|
|
30
|
+
reviewOperation,
|
|
31
|
+
looksUnfinishedWork,
|
|
32
|
+
summarizeArgs
|
|
33
|
+
} = require('./prompt');
|
|
34
|
+
const { trimHistory } = require('./sessions');
|
|
35
|
+
|
|
36
|
+
const HISTORY_LIMIT = 24;
|
|
37
|
+
const MAX_STEPS_CHAT = 8;
|
|
38
|
+
const MAX_STEPS_WORK = 24;
|
|
39
|
+
const MAX_CONTINUATIONS = 2;
|
|
40
|
+
|
|
41
|
+
const VALID_PERMISSIONS = ['none', 'normal', 'auto', 'full'];
|
|
42
|
+
|
|
43
|
+
function describeOperation({ kind, name, args, command }) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
if (kind === 'terminal') {
|
|
46
|
+
lines.push(`命令:${command || ''}`);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
lines.push(`工具:${name}`);
|
|
50
|
+
if (args?.path) lines.push(`路径:${args.path}`);
|
|
51
|
+
if (args?.url) lines.push(`地址:${args.url}`);
|
|
52
|
+
if (args?.command) lines.push(`命令:${args.command}`);
|
|
53
|
+
if (args?.name) lines.push(`名称:${args.name}`);
|
|
54
|
+
const body = summarizeArgs(args);
|
|
55
|
+
if (body && body !== '{}') lines.push(`参数:${body}`);
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeHistory(messages) {
|
|
60
|
+
return (Array.isArray(messages) ? messages : [])
|
|
61
|
+
.filter((m) => m && ['user', 'assistant'].includes(m.role) && typeof m.content === 'string' && m.content.trim())
|
|
62
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolvePermissions(config) {
|
|
66
|
+
const permission = VALID_PERMISSIONS.includes(config.ui?.permission) ? config.ui.permission : 'normal';
|
|
67
|
+
if (permission === 'auto' && !resolveReviewer(config, config.activeProviderId)) {
|
|
68
|
+
// 自动审查在未配置审查模型时不可用,与界面一致自动回落到普通权限
|
|
69
|
+
return 'normal';
|
|
70
|
+
}
|
|
71
|
+
return permission;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 请求一次操作的授权。
|
|
76
|
+
* @param {object} options
|
|
77
|
+
* @param {object} options.config
|
|
78
|
+
* @param {string} options.permission
|
|
79
|
+
* @param {(request)=>Promise<boolean>} options.askUser 用户确认回调
|
|
80
|
+
* @param {(event)=>void} [options.emit]
|
|
81
|
+
*/
|
|
82
|
+
async function requestOperationApproval({ config, permission, kind, name, args, command, workspace, providerId, toolId, askUser, emit }) {
|
|
83
|
+
if (permission === 'full') return { allow: true, source: 'full', review: null };
|
|
84
|
+
if (permission !== 'normal' && permission !== 'auto') {
|
|
85
|
+
return { allow: false, source: 'none', review: null, reason: '当前权限禁止该操作' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (permission === 'normal') {
|
|
89
|
+
emit?.({ type: 'waiting', toolId, name, kind, permission });
|
|
90
|
+
const allowed = await askUser({
|
|
91
|
+
kind,
|
|
92
|
+
permission,
|
|
93
|
+
title: kind === 'terminal' ? 'AI 请求执行终端命令' : 'AI 请求执行操作',
|
|
94
|
+
operation: name,
|
|
95
|
+
detail: describeOperation({ kind, name, args, command }),
|
|
96
|
+
command: command || '',
|
|
97
|
+
workspace: workspace || '',
|
|
98
|
+
review: '',
|
|
99
|
+
reviewer: '',
|
|
100
|
+
reviewModel: ''
|
|
101
|
+
});
|
|
102
|
+
return { allow: Boolean(allowed), source: 'user', review: null, reason: allowed ? '' : '用户拒绝了该操作' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emit?.({ type: 'review-start', toolId, name, kind });
|
|
106
|
+
const review = await reviewOperation({
|
|
107
|
+
config,
|
|
108
|
+
providerId: providerId || config.activeProviderId,
|
|
109
|
+
kind,
|
|
110
|
+
name,
|
|
111
|
+
args,
|
|
112
|
+
command,
|
|
113
|
+
workspace
|
|
114
|
+
});
|
|
115
|
+
emit?.({
|
|
116
|
+
type: 'review',
|
|
117
|
+
toolId,
|
|
118
|
+
name,
|
|
119
|
+
kind,
|
|
120
|
+
decision: review.decision,
|
|
121
|
+
reason: review.reason,
|
|
122
|
+
reviewer: review.reviewer,
|
|
123
|
+
modelId: review.modelId
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (review.decision === 'allow') return { allow: true, source: 'auto', review };
|
|
127
|
+
|
|
128
|
+
const allowed = await askUser({
|
|
129
|
+
kind,
|
|
130
|
+
permission,
|
|
131
|
+
title: kind === 'terminal' ? 'AI 请求执行终端命令' : 'AI 请求执行操作',
|
|
132
|
+
operation: name,
|
|
133
|
+
detail: describeOperation({ kind, name, args, command }),
|
|
134
|
+
command: command || '',
|
|
135
|
+
workspace: workspace || '',
|
|
136
|
+
review: review.reason,
|
|
137
|
+
reviewer: review.reviewer,
|
|
138
|
+
reviewModel: review.modelId
|
|
139
|
+
});
|
|
140
|
+
return { allow: Boolean(allowed), source: 'user', review, reason: allowed ? '' : '用户拒绝了该操作' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function providerOf(config, providerId) {
|
|
144
|
+
const providers = config.providers || [];
|
|
145
|
+
return providers.find((p) => p.id === providerId) || providers.find((p) => p.id === config.activeProviderId) || providers[0] || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function modelOf(provider, modelId, config) {
|
|
149
|
+
const models = provider?.models || [];
|
|
150
|
+
return (
|
|
151
|
+
models.find((m) => m.id === modelId)?.id ||
|
|
152
|
+
models.find((m) => m.id === config.activeModelId)?.id ||
|
|
153
|
+
models[0]?.id ||
|
|
154
|
+
''
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeWorkspace(value) {
|
|
159
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
160
|
+
if (!raw) return '';
|
|
161
|
+
try {
|
|
162
|
+
const stat = fs.statSync(raw);
|
|
163
|
+
return stat.isDirectory() ? raw : '';
|
|
164
|
+
} catch {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 跑一轮完整对话(可能包含多次工具循环与自动续跑)。
|
|
171
|
+
*
|
|
172
|
+
* @param {object} options
|
|
173
|
+
* @param {object} options.config 配置快照
|
|
174
|
+
* @param {object[]} options.messages 历史消息 [{ role, content }]
|
|
175
|
+
* @param {string} [options.providerId]
|
|
176
|
+
* @param {string} [options.modelId]
|
|
177
|
+
* @param {string} [options.mode] 'chat' | 'work'
|
|
178
|
+
* @param {string} [options.permission] none|normal|auto|full
|
|
179
|
+
* @param {string} [options.workspace]
|
|
180
|
+
* @param {string} [options.skill] 本次对话启用的技能名
|
|
181
|
+
* @param {(request)=>Promise<boolean>} options.askUser
|
|
182
|
+
* @param {(event)=>void} [options.onEvent]
|
|
183
|
+
* @param {AbortSignal} [options.signal]
|
|
184
|
+
*/
|
|
185
|
+
async function runAgent({
|
|
186
|
+
config,
|
|
187
|
+
messages,
|
|
188
|
+
providerId,
|
|
189
|
+
modelId,
|
|
190
|
+
mode = 'chat',
|
|
191
|
+
permission,
|
|
192
|
+
workspace,
|
|
193
|
+
skill,
|
|
194
|
+
askUser,
|
|
195
|
+
onEvent,
|
|
196
|
+
signal
|
|
197
|
+
}) {
|
|
198
|
+
const resolvedMode = mode === 'work' ? 'work' : 'chat';
|
|
199
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
200
|
+
throw Object.assign(new Error('消息不能为空:请提供至少一条用户消息'), { userError: true });
|
|
201
|
+
}
|
|
202
|
+
const provider = providerOf(config, providerId);
|
|
203
|
+
if (!provider) throw new Error('未找到对应的供应商,请先运行 dgxay provider add');
|
|
204
|
+
const targetModel = modelOf(provider, modelId, config);
|
|
205
|
+
if (!targetModel) throw new Error('该供应商下没有可用模型,请先添加模型');
|
|
206
|
+
|
|
207
|
+
const effectivePermission = VALID_PERMISSIONS.includes(permission) ? permission : resolvePermissions(config);
|
|
208
|
+
const activeWorkspace = normalizeWorkspace(workspace || config.work?.workspace || '');
|
|
209
|
+
const useTools = resolvedMode === 'work' && Boolean(activeWorkspace);
|
|
210
|
+
|
|
211
|
+
let toolDefs = [];
|
|
212
|
+
if (useTools) toolDefs = tools.toolsFor(effectivePermission);
|
|
213
|
+
else if (effectivePermission !== 'none') toolDefs = [tools.DEFINITIONS.install_mcp_server];
|
|
214
|
+
|
|
215
|
+
const activeSkill = skill ? skills.readSkill(skill) : null;
|
|
216
|
+
|
|
217
|
+
const apiMessages = [
|
|
218
|
+
{
|
|
219
|
+
role: 'system',
|
|
220
|
+
content: systemPromptFor({
|
|
221
|
+
mode: resolvedMode,
|
|
222
|
+
permission: effectivePermission,
|
|
223
|
+
workspace: activeWorkspace,
|
|
224
|
+
customPrompt: config.chat?.systemPrompt,
|
|
225
|
+
skill: activeSkill
|
|
226
|
+
})
|
|
227
|
+
},
|
|
228
|
+
...trimHistory(normalizeHistory(messages), HISTORY_LIMIT)
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const settings = config.chat || {};
|
|
232
|
+
const providerAuth = await resolveProviderAuth(provider);
|
|
233
|
+
|
|
234
|
+
let finalText = '';
|
|
235
|
+
let reasoningText = '';
|
|
236
|
+
let totalUsage = null;
|
|
237
|
+
const toolLog = [];
|
|
238
|
+
const files = [];
|
|
239
|
+
let aborted = false;
|
|
240
|
+
|
|
241
|
+
const emit = (event) => {
|
|
242
|
+
try {
|
|
243
|
+
onEvent?.(event);
|
|
244
|
+
} catch {
|
|
245
|
+
/* 渲染层异常不应打断对话 */
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const maxSteps = useTools ? MAX_STEPS_WORK : MAX_STEPS_CHAT;
|
|
251
|
+
let continuationCount = 0;
|
|
252
|
+
|
|
253
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
254
|
+
emit({ type: 'status', text: step === 0 ? '正在生成' : '正在继续处理' });
|
|
255
|
+
const result = await streamChat({
|
|
256
|
+
protocol: provider.protocol || 'openai',
|
|
257
|
+
baseUrl: provider.baseUrl,
|
|
258
|
+
apiKey: providerAuth.apiKey,
|
|
259
|
+
authType: providerAuth.authType,
|
|
260
|
+
model: targetModel,
|
|
261
|
+
messages: apiMessages,
|
|
262
|
+
tools: toolDefs,
|
|
263
|
+
temperature: settings.temperature,
|
|
264
|
+
maxTokens: settings.maxTokens,
|
|
265
|
+
stream: settings.stream !== false,
|
|
266
|
+
signal,
|
|
267
|
+
onDelta: (text) => {
|
|
268
|
+
finalText += text;
|
|
269
|
+
emit({ type: 'delta', text });
|
|
270
|
+
},
|
|
271
|
+
onReasoning: (text) => {
|
|
272
|
+
reasoningText += text;
|
|
273
|
+
emit({ type: 'reasoning', text });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (result.usage) totalUsage = result.usage;
|
|
278
|
+
|
|
279
|
+
if (Array.isArray(result.toolCalls) && result.toolCalls.length) {
|
|
280
|
+
const calls = result.toolCalls
|
|
281
|
+
.filter((call) => call?.function?.name)
|
|
282
|
+
.map((call, index) => ({
|
|
283
|
+
id: call.id || `call_${step}_${index}`,
|
|
284
|
+
type: 'function',
|
|
285
|
+
function: { name: call.function.name, arguments: call.function.arguments || '{}' }
|
|
286
|
+
}));
|
|
287
|
+
if (!calls.length) break;
|
|
288
|
+
|
|
289
|
+
apiMessages.push({ role: 'assistant', content: result.content || null, tool_calls: calls });
|
|
290
|
+
|
|
291
|
+
for (const call of calls) {
|
|
292
|
+
const toolId = `t_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
|
|
293
|
+
const name = call.function.name;
|
|
294
|
+
emit({ type: 'tool', id: toolId, name, status: 'running', args: call.function.arguments });
|
|
295
|
+
let output = '';
|
|
296
|
+
let ok = true;
|
|
297
|
+
let lastReview = null;
|
|
298
|
+
try {
|
|
299
|
+
const args = call.function.arguments?.trim() ? JSON.parse(call.function.arguments) : {};
|
|
300
|
+
const requestApproval = async (operation) => {
|
|
301
|
+
const decision = await requestOperationApproval({
|
|
302
|
+
config,
|
|
303
|
+
permission: effectivePermission,
|
|
304
|
+
kind: 'tool',
|
|
305
|
+
name: operation.name,
|
|
306
|
+
args: operation.args,
|
|
307
|
+
workspace: activeWorkspace,
|
|
308
|
+
providerId: provider.id,
|
|
309
|
+
toolId,
|
|
310
|
+
askUser,
|
|
311
|
+
emit
|
|
312
|
+
});
|
|
313
|
+
lastReview = decision;
|
|
314
|
+
return decision.allow;
|
|
315
|
+
};
|
|
316
|
+
output = await tools.execute(name, args, {
|
|
317
|
+
permission: effectivePermission,
|
|
318
|
+
workspace: activeWorkspace,
|
|
319
|
+
requestApproval,
|
|
320
|
+
onFile: (file) => {
|
|
321
|
+
files.push(file);
|
|
322
|
+
emit({ type: 'file', ...file });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
} catch (err) {
|
|
326
|
+
ok = false;
|
|
327
|
+
output = `Error: ${err.message}`;
|
|
328
|
+
}
|
|
329
|
+
const logItem = {
|
|
330
|
+
id: toolId,
|
|
331
|
+
name,
|
|
332
|
+
args: call.function.arguments,
|
|
333
|
+
ok,
|
|
334
|
+
output,
|
|
335
|
+
review: lastReview
|
|
336
|
+
? {
|
|
337
|
+
source: lastReview.allow ? lastReview.source : 'denied',
|
|
338
|
+
decision: lastReview.review?.decision || (lastReview.allow ? 'allow' : 'review'),
|
|
339
|
+
reason: lastReview.review?.reason || lastReview.reason || ''
|
|
340
|
+
}
|
|
341
|
+
: undefined,
|
|
342
|
+
at: Date.now()
|
|
343
|
+
};
|
|
344
|
+
toolLog.push(logItem);
|
|
345
|
+
emit({
|
|
346
|
+
type: 'tool',
|
|
347
|
+
id: toolId,
|
|
348
|
+
name,
|
|
349
|
+
status: ok ? 'ok' : 'error',
|
|
350
|
+
output: output.slice(0, 4000),
|
|
351
|
+
review: logItem.review
|
|
352
|
+
});
|
|
353
|
+
apiMessages.push({ role: 'tool', tool_call_id: call.id, content: output });
|
|
354
|
+
}
|
|
355
|
+
if (finalText) {
|
|
356
|
+
finalText += '\n\n';
|
|
357
|
+
emit({ type: 'delta', text: '\n\n' });
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 工作模式:模型提前收尾时自动推进一步,避免任务只做了一半
|
|
363
|
+
if (
|
|
364
|
+
resolvedMode === 'work' &&
|
|
365
|
+
useTools &&
|
|
366
|
+
continuationCount < MAX_CONTINUATIONS &&
|
|
367
|
+
step < maxSteps - 1 &&
|
|
368
|
+
looksUnfinishedWork(result)
|
|
369
|
+
) {
|
|
370
|
+
continuationCount += 1;
|
|
371
|
+
if (result.content) apiMessages.push({ role: 'assistant', content: result.content });
|
|
372
|
+
apiMessages.push({
|
|
373
|
+
role: 'user',
|
|
374
|
+
content:
|
|
375
|
+
'继续执行,直到任务全部完成。不要只描述计划或承诺下一步,直接调用工具完成剩余步骤;完成后先验证结果,再给出简短总结。'
|
|
376
|
+
});
|
|
377
|
+
if (finalText) {
|
|
378
|
+
finalText += '\n\n';
|
|
379
|
+
emit({ type: 'delta', text: '\n\n' });
|
|
380
|
+
}
|
|
381
|
+
emit({ type: 'status', text: '正在继续完成任务' });
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
emit({ type: 'done', text: finalText, usage: totalUsage });
|
|
389
|
+
return { ok: true, text: finalText, reasoning: reasoningText, usage: totalUsage, toolLog, files };
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (err?.name === 'AbortError' || signal?.aborted) {
|
|
392
|
+
aborted = true;
|
|
393
|
+
emit({ type: 'aborted' });
|
|
394
|
+
return { ok: false, aborted: true, text: finalText, reasoning: reasoningText, usage: totalUsage, toolLog, files };
|
|
395
|
+
}
|
|
396
|
+
emit({ type: 'error', message: err.message });
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
error: err.message,
|
|
400
|
+
text: finalText,
|
|
401
|
+
reasoning: reasoningText,
|
|
402
|
+
usage: totalUsage,
|
|
403
|
+
toolLog,
|
|
404
|
+
files
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
HISTORY_LIMIT,
|
|
411
|
+
MAX_STEPS_CHAT,
|
|
412
|
+
MAX_STEPS_WORK,
|
|
413
|
+
requestOperationApproval,
|
|
414
|
+
describeOperation,
|
|
415
|
+
providerOf,
|
|
416
|
+
modelOf,
|
|
417
|
+
resolvePermissions,
|
|
418
|
+
runAgent
|
|
419
|
+
};
|