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,363 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 首次配置向导 / 配置查看。
|
|
5
|
+
*
|
|
6
|
+
* dgxay config init 交互式填写供应商、Base URL、API Key、模型
|
|
7
|
+
* dgxay config init --protocol openai --base-url ... --api-key ... --model gpt-4o
|
|
8
|
+
* dgxay config show 查看当前配置(密钥打码)
|
|
9
|
+
* dgxay config get <a.b.c> 读取单项配置
|
|
10
|
+
* dgxay config set <a.b.c> <value> 写入单项配置
|
|
11
|
+
* dgxay config path 显示数据目录与各文件的路径
|
|
12
|
+
* dgxay config reset 重置配置(不删除 oauth.json / mcp.json / skills)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const print = require('../cli/print');
|
|
18
|
+
const { parseArgs, actionFirst } = require('../cli/args');
|
|
19
|
+
const { COLORS, termWidth } = require('../cli/ansi');
|
|
20
|
+
const settings = require('../services/settings');
|
|
21
|
+
const cfg = require('../services/config');
|
|
22
|
+
const oauth = require('../core/oauth');
|
|
23
|
+
const { ask, askSecret, confirm } = require('../services/input');
|
|
24
|
+
const { VERSION } = require('../services/version');
|
|
25
|
+
|
|
26
|
+
const PRESETS = [
|
|
27
|
+
{ name: 'OpenAI', protocol: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o' },
|
|
28
|
+
{ name: 'DeepSeek', protocol: 'openai', baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat' },
|
|
29
|
+
{ name: 'Claude', protocol: 'anthropic', baseUrl: 'https://api.anthropic.com', model: 'claude-sonnet-4-5' },
|
|
30
|
+
{ name: 'OpenAI Responses', protocol: 'responses', baseUrl: 'https://api.openai.com/v1', model: 'gpt-5' },
|
|
31
|
+
{ name: '通义千问', protocol: 'openai', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' },
|
|
32
|
+
{ name: 'Moonshot', protocol: 'openai', baseUrl: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k' },
|
|
33
|
+
{ name: 'Ollama 本地', protocol: 'openai', baseUrl: 'http://localhost:11434/v1', model: 'llama3.1' }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const options = [
|
|
37
|
+
{ name: 'protocol', type: 'string', valueName: 'p', desc: 'openai / anthropic / responses' },
|
|
38
|
+
{ name: 'name', type: 'string', valueName: 'text', desc: '供应商名称' },
|
|
39
|
+
{ name: 'base-url', type: 'string', valueName: 'url', desc: '接口地址' },
|
|
40
|
+
{ name: 'api-key', type: 'string', valueName: 'key', desc: 'API Key(也可用 DGXAY_API_KEY 环境变量)' },
|
|
41
|
+
{ name: 'model', type: 'list', valueName: 'id', desc: '模型 ID,可重复指定或逗号分隔' },
|
|
42
|
+
{ name: 'oauth-provider', type: 'string', valueName: 'id', desc: '改用已连接的 OAuth 账户:openai/anthropic/xai/google' },
|
|
43
|
+
{ name: 'force', alias: 'f', type: 'boolean', desc: '已有配置时仍然覆盖' }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/* ------------------------------ init ------------------------------ */
|
|
47
|
+
|
|
48
|
+
async function promptProtocol(config) {
|
|
49
|
+
const interactive = process.stdin.isTTY;
|
|
50
|
+
if (!interactive) return 'openai';
|
|
51
|
+
print.blank();
|
|
52
|
+
print.heading('接口协议');
|
|
53
|
+
const list = ['openai', 'anthropic', 'responses'];
|
|
54
|
+
list.forEach((key, index) => {
|
|
55
|
+
const meta = settings.PROTOCOL_META[key];
|
|
56
|
+
print.write(` ${COLORS.accent(String(index + 1))}. ${meta.full} ${COLORS.faint(meta.path)}`);
|
|
57
|
+
});
|
|
58
|
+
const answer = await ask(`选择 [1-3,默认 1]: `);
|
|
59
|
+
const index = Number(String(answer || '').trim());
|
|
60
|
+
if (Number.isInteger(index) && index >= 1 && index <= 3) return list[index - 1];
|
|
61
|
+
return 'openai';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function promptPreset(protocol) {
|
|
65
|
+
const interactive = process.stdin.isTTY;
|
|
66
|
+
if (!interactive) return null;
|
|
67
|
+
print.blank();
|
|
68
|
+
print.heading('快速预设');
|
|
69
|
+
const matches = PRESETS.filter((preset) => preset.protocol === protocol);
|
|
70
|
+
matches.forEach((preset, index) => {
|
|
71
|
+
print.write(` ${COLORS.accent(String(index + 1))}. ${preset.name} ${COLORS.faint(preset.baseUrl)}`);
|
|
72
|
+
});
|
|
73
|
+
print.write(` ${COLORS.faint('直接回车跳过,手动填写')}`);
|
|
74
|
+
const answer = await ask('选择预设: ');
|
|
75
|
+
const index = Number(String(answer || '').trim());
|
|
76
|
+
if (Number.isInteger(index) && index >= 1 && index <= matches.length) return matches[index - 1];
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runInit(opts, runtime) {
|
|
81
|
+
const config = settings.readConfig();
|
|
82
|
+
if (config.providers?.length && !opts.force) {
|
|
83
|
+
print.warn(`已经配置了 ${config.providers.length} 个供应商。要追加新的供应商请使用 dgxay provider add`);
|
|
84
|
+
print.hint('如需重新初始化首个供应商,加 --force');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const interactive = Boolean(process.stdin.isTTY) && !opts.protocol && !opts.baseUrl;
|
|
89
|
+
let protocol = String(opts.protocol || '').trim();
|
|
90
|
+
let name = String(opts.name || '').trim();
|
|
91
|
+
let baseUrl = String(opts.baseUrl || '').trim();
|
|
92
|
+
let apiKey = opts.apiKey !== undefined ? String(opts.apiKey) : process.env.DGXAY_API_KEY || '';
|
|
93
|
+
let models = (opts.model || []).flatMap((item) => String(item).split(',')).map((item) => item.trim()).filter(Boolean);
|
|
94
|
+
let oauthProvider = String(opts.oauthProvider || '').trim();
|
|
95
|
+
|
|
96
|
+
if (interactive) {
|
|
97
|
+
print.write('');
|
|
98
|
+
print.write(`${COLORS.bold('DgxayCode 首次配置')} ${COLORS.faint(`v${VERSION}`)}`);
|
|
99
|
+
print.write(COLORS.faint('配置会写入本机数据目录,密钥仅保存在本地,不会上传。'));
|
|
100
|
+
print.write(COLORS.faint(`数据目录:${cfg.DCB_DIR}`));
|
|
101
|
+
|
|
102
|
+
protocol = protocol || (await promptProtocol(config));
|
|
103
|
+
const preset = await promptPreset(protocol);
|
|
104
|
+
if (preset) {
|
|
105
|
+
name = name || preset.name;
|
|
106
|
+
baseUrl = baseUrl || preset.baseUrl;
|
|
107
|
+
models = models.length ? models : [preset.model];
|
|
108
|
+
print.write(COLORS.faint(`已选择 ${preset.name},Base URL 预填 ${preset.baseUrl}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!name) {
|
|
112
|
+
const answer = await ask(`供应商名称 [${preset?.name || '我的供应商'}]: `);
|
|
113
|
+
name = String(answer || '').trim() || preset?.name || '我的供应商';
|
|
114
|
+
}
|
|
115
|
+
if (!baseUrl) {
|
|
116
|
+
const answer = await ask(`Base URL [${settings.PROTOCOL_META[protocol].basePlaceholder}]: `);
|
|
117
|
+
baseUrl = String(answer || '').trim() || settings.PROTOCOL_META[protocol].basePlaceholder;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const useOauth = await confirm('使用已连接的 OAuth 账户(而不是 API Key)?', false);
|
|
121
|
+
if (useOauth) {
|
|
122
|
+
const connected = oauth.listProviders().filter((item) => item.connection && !item.connection.expired);
|
|
123
|
+
if (!connected.length) {
|
|
124
|
+
print.warn('还没有可用账户,先继续填写 API Key。可稍后运行 dgxay oauth connect <平台>');
|
|
125
|
+
} else {
|
|
126
|
+
connected.forEach((item, index) =>
|
|
127
|
+
print.write(` ${COLORS.accent(String(index + 1))}. ${item.name} ${COLORS.faint(item.connection.account)}`)
|
|
128
|
+
);
|
|
129
|
+
const answer = await ask('选择账户 [1]: ');
|
|
130
|
+
const index = Number(String(answer || '').trim()) || 1;
|
|
131
|
+
oauthProvider = connected[index - 1]?.id || '';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!oauthProvider && !apiKey) {
|
|
136
|
+
const secret = await askSecret('API Key(本地模型可留空): ');
|
|
137
|
+
apiKey = secret !== null ? secret : '';
|
|
138
|
+
}
|
|
139
|
+
if (!models.length) {
|
|
140
|
+
const answer = await ask(`模型 ID [${settings.PROTOCOL_META[protocol].modelPlaceholder}]: `);
|
|
141
|
+
models = String(answer || '')
|
|
142
|
+
.split(/[,,\s]+/)
|
|
143
|
+
.map((item) => item.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
if (!models.length) {
|
|
147
|
+
print.fail('模型 ID 不能为空');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!protocol) protocol = 'openai';
|
|
153
|
+
if (!baseUrl) baseUrl = settings.PROTOCOL_META[protocol].basePlaceholder;
|
|
154
|
+
if (!models.length) {
|
|
155
|
+
throw Object.assign(new Error('缺少 --model,例如 --model deepseek-chat'), { userError: true });
|
|
156
|
+
}
|
|
157
|
+
if (!apiKey && !oauthProvider) {
|
|
158
|
+
print.warn('没有填写 API Key:本地模型(Ollama 等)可以忽略,云端接口会返回 401');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = settings.upsertProvider({
|
|
162
|
+
protocol,
|
|
163
|
+
name,
|
|
164
|
+
baseUrl,
|
|
165
|
+
apiKey,
|
|
166
|
+
models,
|
|
167
|
+
authMode: oauthProvider ? 'oauth' : 'apiKey',
|
|
168
|
+
oauthProvider
|
|
169
|
+
});
|
|
170
|
+
settings.saveConfig({ onboarded: true });
|
|
171
|
+
|
|
172
|
+
print.blank();
|
|
173
|
+
print.ok(`已保存供应商「${result.provider.name}」(${settings.PROTOCOL_META[result.provider.protocol].full})`);
|
|
174
|
+
print.keyValue([
|
|
175
|
+
{ label: 'Base URL', value: result.provider.baseUrl },
|
|
176
|
+
{ label: '模型', value: result.provider.models.map((m) => m.id).join(', ') },
|
|
177
|
+
{ label: '自动审查模型', value: result.provider.reviewModelId || '未配置' },
|
|
178
|
+
{ label: '认证方式', value: oauthProvider ? `OAuth · ${oauthProvider}` : apiKey ? 'API Key' : '无(本地模型)' },
|
|
179
|
+
{ label: '配置文件', value: settings.homeRelative(cfg.CONFIG_PATH) }
|
|
180
|
+
]);
|
|
181
|
+
print.blank();
|
|
182
|
+
print.hint('下一步:dgxay doctor 检查连通性,然后直接运行 dgxay 开始对话');
|
|
183
|
+
void interactive;
|
|
184
|
+
void runtime;
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ------------------------------ show / get / set / path / reset ------------------------------ */
|
|
189
|
+
|
|
190
|
+
function runShow() {
|
|
191
|
+
const config = settings.readConfig();
|
|
192
|
+
const provider = settings.activeProvider(config);
|
|
193
|
+
const model = settings.activeModel(config);
|
|
194
|
+
print.heading('当前配置');
|
|
195
|
+
print.keyValue(
|
|
196
|
+
[
|
|
197
|
+
{ label: '数据目录', value: settings.homeRelative(cfg.DCB_DIR) },
|
|
198
|
+
{ label: '配置文件', value: settings.homeRelative(cfg.CONFIG_PATH) },
|
|
199
|
+
{ label: '模式', value: config.ui?.mode === 'work' ? '工作模式' : '对话模式' },
|
|
200
|
+
{
|
|
201
|
+
label: '权限',
|
|
202
|
+
value: `${settings.PERMISSIONS[settings.currentPermission(config)].label}${
|
|
203
|
+
config.ui?.permission === 'auto' && !settings.hasReviewModel(config) ? '(审查模型缺失,已回退普通权限)' : ''
|
|
204
|
+
}`
|
|
205
|
+
},
|
|
206
|
+
{ label: '工作空间', value: config.work?.workspace || '(未选择)' },
|
|
207
|
+
{ label: '当前供应商', value: provider ? `${provider.name}(${provider.id})` : '未配置' },
|
|
208
|
+
{ label: '当前模型', value: model ? `${model.name}${model.id !== model.name ? `(${model.id})` : ''}` : '未配置' },
|
|
209
|
+
{ label: '发送方式', value: config.chat?.stream === false ? '非流式' : '流式' },
|
|
210
|
+
{ label: '温度', value: String(config.chat?.temperature ?? 0.7) },
|
|
211
|
+
{ label: '最大输出', value: String(config.chat?.maxTokens ?? 4096) },
|
|
212
|
+
{ label: '自定义指令', value: config.chat?.systemPrompt ? '已设置' : '未设置' }
|
|
213
|
+
],
|
|
214
|
+
{}
|
|
215
|
+
);
|
|
216
|
+
print.blank();
|
|
217
|
+
print.heading('供应商');
|
|
218
|
+
if (!config.providers?.length) {
|
|
219
|
+
print.hint(' 还没有供应商,运行 dgxay config init');
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
const rows = config.providers.map((item, index) => [
|
|
223
|
+
String(index + 1),
|
|
224
|
+
item.name,
|
|
225
|
+
settings.PROTOCOL_META[item.protocol || 'openai'].short,
|
|
226
|
+
item.baseUrl,
|
|
227
|
+
item.authMode === 'oauth' ? `OAuth(${item.oauthProvider || '?'})` : item.apiKey ? cfg.maskKey(item.apiKey) : '无',
|
|
228
|
+
(item.models || []).length,
|
|
229
|
+
item.reviewModelId || '-'
|
|
230
|
+
]);
|
|
231
|
+
print.table(
|
|
232
|
+
[
|
|
233
|
+
{ title: '#', max: 3 },
|
|
234
|
+
{ title: '名称', max: 18 },
|
|
235
|
+
{ title: '协议', max: 16 },
|
|
236
|
+
{ title: 'Base URL', max: 40 },
|
|
237
|
+
{ title: '认证', max: 16 },
|
|
238
|
+
{ title: '模型数', max: 5 },
|
|
239
|
+
{ title: '审查模型', max: 20 }
|
|
240
|
+
],
|
|
241
|
+
rows
|
|
242
|
+
);
|
|
243
|
+
print.blank();
|
|
244
|
+
print.hint('添加/修改供应商:dgxay provider --help');
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getPath(object, keyPath) {
|
|
249
|
+
return String(keyPath)
|
|
250
|
+
.split('.')
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.reduce((acc, key) => (acc == null ? undefined : acc[key]), object);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function runCmd(argv) {
|
|
256
|
+
const { positionals } = parseArgs(argv, []);
|
|
257
|
+
const action = positionals[0] || 'show';
|
|
258
|
+
return { action, rest: positionals.slice(1) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function run({ argv, runtime }) {
|
|
262
|
+
const { action, rest, options: opts } = actionFirst(argv, options);
|
|
263
|
+
|
|
264
|
+
if (action === 'init') return runInit(opts, runtime);
|
|
265
|
+
if (action === 'show' || action === 'list' || !action) return runShow();
|
|
266
|
+
|
|
267
|
+
if (action === 'get') {
|
|
268
|
+
if (!rest[0]) throw Object.assign(new Error('请指定配置键,例如 dgxay config get ui.permission'), { userError: true });
|
|
269
|
+
const value = getPath(settings.readConfig(), rest[0]);
|
|
270
|
+
if (value === undefined) {
|
|
271
|
+
print.fail(`没有这个配置项:${rest[0]}`);
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
process.stdout.write(`${typeof value === 'string' ? value : JSON.stringify(value)}\n`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (action === 'set') {
|
|
279
|
+
const key = rest[0];
|
|
280
|
+
const raw = rest.slice(1).join(' ');
|
|
281
|
+
if (!key || !raw) {
|
|
282
|
+
throw Object.assign(new Error('用法:dgxay config set <键> <值>,例如 dgxay config set chat.temperature 0.3'), {
|
|
283
|
+
userError: true
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const allowed = ['ui.permission', 'ui.mode', 'chat.temperature', 'chat.maxTokens', 'chat.stream', 'chat.systemPrompt'];
|
|
287
|
+
if (!allowed.includes(key)) {
|
|
288
|
+
throw Object.assign(new Error(`不支持修改 ${key}。可修改:${allowed.join(', ')}`), { userError: true });
|
|
289
|
+
}
|
|
290
|
+
let value = raw;
|
|
291
|
+
if (key === 'chat.temperature' || key === 'chat.maxTokens') value = Number(raw);
|
|
292
|
+
if (!Number.isFinite(value) && typeof value === 'number') {
|
|
293
|
+
throw Object.assign(new Error('需要数字'), { userError: true });
|
|
294
|
+
}
|
|
295
|
+
if (key === 'chat.stream') value = !/^(false|0|no|off)$/i.test(raw);
|
|
296
|
+
if (key === 'ui.permission' && !settings.PERMISSIONS[raw]) {
|
|
297
|
+
throw Object.assign(new Error('权限只能是 none / normal / auto / full'), { userError: true });
|
|
298
|
+
}
|
|
299
|
+
const [section, name] = key.split('.');
|
|
300
|
+
settings.saveConfig({ [section]: { [name]: value } });
|
|
301
|
+
print.ok(`${key} = ${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (action === 'path') {
|
|
306
|
+
const paths = settings.paths();
|
|
307
|
+
print.keyValue([
|
|
308
|
+
{ label: '数据目录', value: paths.dataDir },
|
|
309
|
+
{ label: '配置文件', value: paths.config },
|
|
310
|
+
{ label: '对话记录', value: paths.chats },
|
|
311
|
+
{ label: 'OAuth', value: paths.oauth },
|
|
312
|
+
{ label: 'MCP', value: paths.mcp },
|
|
313
|
+
{ label: '技能目录', value: paths.skills }
|
|
314
|
+
]);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (action === 'reset') {
|
|
319
|
+
const yes =
|
|
320
|
+
argv.includes('--yes') ||
|
|
321
|
+
(await confirm('将重置全部配置(供应商、密钥、偏好),对话记录也会清空。继续?', false));
|
|
322
|
+
if (!yes) {
|
|
323
|
+
print.hint('已取消');
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
const backup = `${cfg.CONFIG_PATH}.bak-${Date.now()}`;
|
|
327
|
+
try {
|
|
328
|
+
if (fs.existsSync(cfg.CONFIG_PATH)) fs.copyFileSync(cfg.CONFIG_PATH, backup);
|
|
329
|
+
} catch {
|
|
330
|
+
/* ignore */
|
|
331
|
+
}
|
|
332
|
+
cfg.resetConfig();
|
|
333
|
+
cfg.saveChats([]);
|
|
334
|
+
print.ok('配置已重置');
|
|
335
|
+
print.hint('oauth.json、mcp.json 与 skills/ 不会被删除;如需彻底清理请手动删除数据目录');
|
|
336
|
+
if (fs.existsSync(backup)) print.hint(`原配置已备份到 ${path.basename(backup)}`);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
print.fail(`未知的 config 子命令:${action}`);
|
|
341
|
+
print.hint('可用:init / show / get / set / path / reset');
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
name: 'config',
|
|
347
|
+
aliases: ['cfg'],
|
|
348
|
+
summary: '配置:初始化、查看、修改数据目录',
|
|
349
|
+
usage: 'dgxay config <init|show|get|set|path|reset>',
|
|
350
|
+
options,
|
|
351
|
+
subcommands: [
|
|
352
|
+
['init', '首次配置供应商与模型'],
|
|
353
|
+
['show', '查看当前配置(密钥打码)'],
|
|
354
|
+
['get <键>', '读取单项配置'],
|
|
355
|
+
['set <键> <值>', '修改单项配置'],
|
|
356
|
+
['path', '显示数据目录与文件路径'],
|
|
357
|
+
['reset', '重置配置(保留 OAuth / MCP / 技能)']
|
|
358
|
+
],
|
|
359
|
+
examples: ['dgxay config init', 'dgxay config show', 'dgxay config set chat.temperature 0.3', 'dgxay config path'],
|
|
360
|
+
run,
|
|
361
|
+
runCmd,
|
|
362
|
+
getPath
|
|
363
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* context:查看某段会话的上下文估算用量(对应桌面版的上下文圆环)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const print = require('../cli/print');
|
|
8
|
+
const { actionFirst } = require('../cli/args');
|
|
9
|
+
const sessions = require('../core/sessions');
|
|
10
|
+
const settings = require('../services/settings');
|
|
11
|
+
const { contextInfo, contextReport } = require('../cli/ui');
|
|
12
|
+
|
|
13
|
+
const options = [];
|
|
14
|
+
|
|
15
|
+
async function run({ argv }) {
|
|
16
|
+
const { action, rest } = actionFirst(argv, options);
|
|
17
|
+
const ref = action || rest[0] || '';
|
|
18
|
+
const config = settings.readConfig();
|
|
19
|
+
const model = settings.activeModel(config);
|
|
20
|
+
const chat = ref ? sessions.resolveChat(ref) : sessions.listChats()[0] || null;
|
|
21
|
+
if (ref && !chat) {
|
|
22
|
+
print.fail(`找不到会话:${ref}`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
if (!chat) {
|
|
26
|
+
print.hint('还没有会话记录');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
process.stdout.write(`\n${contextReport(contextInfo(chat, model))}\n`);
|
|
30
|
+
print.hint(`会话:${chat.title}(${chat.id})`);
|
|
31
|
+
print.blank();
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
name: 'context',
|
|
37
|
+
aliases: ['ctx'],
|
|
38
|
+
summary: '查看上下文用量估算',
|
|
39
|
+
usage: 'dgxay context [会话序号|ID]',
|
|
40
|
+
options,
|
|
41
|
+
examples: ['dgxay context', 'dgxay context 1'],
|
|
42
|
+
run
|
|
43
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doctor:环境自检。检查 Node 版本、数据目录、权限、供应商、模型连通性、OAuth、MCP、技能。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const print = require('../cli/print');
|
|
11
|
+
const { COLORS, SIGN } = require('../cli/ansi');
|
|
12
|
+
const settings = require('../services/settings');
|
|
13
|
+
const cfg = require('../services/config');
|
|
14
|
+
const mcp = require('../core/mcp');
|
|
15
|
+
const skills = require('../services/skills');
|
|
16
|
+
const oauth = require('../core/oauth');
|
|
17
|
+
const { streamChat } = require('../core/llm');
|
|
18
|
+
const { resolveProviderAuth } = require('../core/prompt');
|
|
19
|
+
const { VERSION } = require('../services/version');
|
|
20
|
+
|
|
21
|
+
const options = [
|
|
22
|
+
{ name: 'offline', type: 'boolean', desc: '跳过网络连通性测试' },
|
|
23
|
+
{ name: 'timeout', type: 'number', valueName: 'ms', desc: '连通性测试超时,默认 12000 毫秒' }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function line(status, label, detail = '') {
|
|
27
|
+
const symbol =
|
|
28
|
+
status === 'ok'
|
|
29
|
+
? COLORS.green(SIGN.ok)
|
|
30
|
+
: status === 'warn'
|
|
31
|
+
? COLORS.yellow('!')
|
|
32
|
+
: status === 'fail'
|
|
33
|
+
? COLORS.red(SIGN.fail)
|
|
34
|
+
: COLORS.faint('-');
|
|
35
|
+
process.stdout.write(` ${symbol} ${label}${detail ? COLORS.faint(` ${detail}`) : ''}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function checkConnectivity(provider, modelId, timeout) {
|
|
39
|
+
const auth = await resolveProviderAuth(provider);
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
try {
|
|
44
|
+
const result = await streamChat({
|
|
45
|
+
protocol: provider.protocol || 'openai',
|
|
46
|
+
baseUrl: provider.baseUrl,
|
|
47
|
+
apiKey: auth.apiKey,
|
|
48
|
+
authType: auth.authType,
|
|
49
|
+
model: modelId,
|
|
50
|
+
messages: [{ role: 'user', content: '回复 OK 两个字母即可。' }],
|
|
51
|
+
stream: false,
|
|
52
|
+
temperature: 0,
|
|
53
|
+
maxTokens: 16,
|
|
54
|
+
signal: controller.signal
|
|
55
|
+
});
|
|
56
|
+
return { ok: true, ms: Date.now() - started, text: String(result.content || '').trim().slice(0, 60) };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { ok: false, ms: Date.now() - started, error: err?.message || String(err) };
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function run({ argv }) {
|
|
65
|
+
const { parseArgs } = require('../cli/args');
|
|
66
|
+
const { options: opts } = parseArgs(argv, options);
|
|
67
|
+
const timeout = Number(opts.timeout) > 0 ? Number(opts.timeout) : 12000;
|
|
68
|
+
|
|
69
|
+
print.write('');
|
|
70
|
+
print.heading(`DgxayCode CLI 自检 v${VERSION}`);
|
|
71
|
+
print.write('');
|
|
72
|
+
|
|
73
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
74
|
+
line(major >= 20 ? 'ok' : major >= 18 ? 'warn' : 'fail', `Node.js ${process.versions.node}`, major >= 18 ? '' : '需要 18+');
|
|
75
|
+
line('ok', `平台 ${process.platform} ${process.arch}`, `主目录 ${os.homedir()}`);
|
|
76
|
+
if (typeof fetch !== 'function') line('fail', '全局 fetch 不可用', '需要 Node.js 18+');
|
|
77
|
+
|
|
78
|
+
// 数据目录
|
|
79
|
+
const dir = cfg.DCB_DIR;
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
82
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
83
|
+
line('ok', '数据目录可写', settings.homeRelative(dir));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
line('fail', '数据目录不可写', `${dir} · ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (fs.existsSync(cfg.CONFIG_PATH)) line('ok', '存在 config.json', settings.homeRelative(cfg.CONFIG_PATH));
|
|
88
|
+
else line('warn', '尚无 config.json', '运行 dgxay config init');
|
|
89
|
+
|
|
90
|
+
const config = settings.readConfig();
|
|
91
|
+
const mode = config.ui?.mode === 'work' ? 'work' : 'chat';
|
|
92
|
+
const permission = settings.currentPermission(config);
|
|
93
|
+
line('ok', `模式 / 权限`, `${mode === 'work' ? '工作模式' : '对话模式'} · ${settings.PERMISSIONS[permission].label}`);
|
|
94
|
+
if (config.ui?.permission === 'auto' && !settings.hasReviewModel(config)) {
|
|
95
|
+
line('warn', '自动审查已回退', '当前/默认供应商没有有效的审查模型');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const workspace = config.work?.workspace || '';
|
|
99
|
+
if (workspace) {
|
|
100
|
+
try {
|
|
101
|
+
const isDir = fs.statSync(workspace).isDirectory();
|
|
102
|
+
line(isDir ? 'ok' : 'fail', '工作空间', workspace);
|
|
103
|
+
} catch {
|
|
104
|
+
line('warn', '工作空间已不存在', workspace);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
line('warn', '未选择工作空间', '工作模式下无法读写文件,用 /cd 或 dgxay workspace set 设置');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 供应商
|
|
111
|
+
print.write('');
|
|
112
|
+
print.heading('供应商');
|
|
113
|
+
const providers = config.providers || [];
|
|
114
|
+
if (!providers.length) {
|
|
115
|
+
line('fail', '没有供应商', '运行 dgxay config init');
|
|
116
|
+
}
|
|
117
|
+
for (const provider of providers) {
|
|
118
|
+
const models = provider.models || [];
|
|
119
|
+
const reviewOk = models.some((m) => m.id === provider.reviewModelId);
|
|
120
|
+
line('ok', provider.name, `${provider.protocol || 'openai'} · ${provider.baseUrl} · ${models.length} 个模型`);
|
|
121
|
+
if (!reviewOk) line('warn', ` ${provider.name} 审查模型无效`, provider.reviewModelId || '未配置');
|
|
122
|
+
if (provider.authMode === 'oauth') {
|
|
123
|
+
const account = oauth.listProviders().find((item) => item.id === provider.oauthProvider);
|
|
124
|
+
if (account?.connection && !account.connection.expired) {
|
|
125
|
+
line('ok', ` OAuth 账户`, `${provider.oauthProvider} · ${account.connection.account}`);
|
|
126
|
+
} else {
|
|
127
|
+
line('fail', ` OAuth 账户不可用`, `dgxay oauth connect ${provider.oauthProvider || '<平台>'}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!opts.offline && providers.length) {
|
|
133
|
+
print.write('');
|
|
134
|
+
print.heading('连通性测试');
|
|
135
|
+
const active = settings.activeProvider(config);
|
|
136
|
+
const model = settings.activeModel(config);
|
|
137
|
+
if (!active || !model) {
|
|
138
|
+
line('warn', '跳过:没有当前模型');
|
|
139
|
+
} else {
|
|
140
|
+
process.stdout.write(` ${COLORS.faint(`${SIGN.run} 正在请求 ${active.name} / ${model.id}…`)}`);
|
|
141
|
+
const result = await checkConnectivity(active, model.id, timeout);
|
|
142
|
+
process.stdout.write('\r\x1b[2K');
|
|
143
|
+
if (result.ok) line('ok', `${active.name} / ${model.id}`, `${result.ms}ms${result.text ? ` · ${result.text}` : ''}`);
|
|
144
|
+
else line('fail', `${active.name} / ${model.id}`, `${result.ms}ms · ${result.error}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// OAuth / MCP / 技能
|
|
149
|
+
print.write('');
|
|
150
|
+
print.heading('账户与扩展');
|
|
151
|
+
const accounts = oauth.listProviders();
|
|
152
|
+
const connected = accounts.filter((item) => item.connection && !item.connection.expired);
|
|
153
|
+
if (connected.length) {
|
|
154
|
+
for (const account of connected) line('ok', `${account.name} 账户`, account.connection.account);
|
|
155
|
+
} else {
|
|
156
|
+
line('-', '没有连接的 OAuth 账户', '可选:dgxay oauth connect openai');
|
|
157
|
+
}
|
|
158
|
+
const servers = mcp.listServers();
|
|
159
|
+
if (servers.length) {
|
|
160
|
+
for (const server of servers) {
|
|
161
|
+
line('ok', `MCP ${server.name}`, `${server.command} ${(server.args || []).join(' ')}`.trim());
|
|
162
|
+
}
|
|
163
|
+
print.hint(' 提示:当前版本只管理 MCP 进程,尚未把 MCP 工具暴露给模型');
|
|
164
|
+
} else {
|
|
165
|
+
line('-', '没有 MCP server');
|
|
166
|
+
}
|
|
167
|
+
const skillList = skills.listSkills();
|
|
168
|
+
line(skillList.length ? 'ok' : '-', `技能 ${skillList.length} 个`, settings.homeRelative(cfg.SKILLS_DIR));
|
|
169
|
+
|
|
170
|
+
const chatsPath = path.join(dir, 'chats.json');
|
|
171
|
+
if (fs.existsSync(chatsPath)) {
|
|
172
|
+
try {
|
|
173
|
+
const chats = JSON.parse(fs.readFileSync(chatsPath, 'utf8'));
|
|
174
|
+
line('ok', '对话记录', `${Array.isArray(chats) ? chats.length : 0} 段`);
|
|
175
|
+
} catch {
|
|
176
|
+
line('warn', '对话记录无法解析', chatsPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
print.write('');
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
name: 'doctor',
|
|
186
|
+
aliases: ['check'],
|
|
187
|
+
summary: '自检:环境、配置、模型连通性',
|
|
188
|
+
usage: 'dgxay doctor [--offline]',
|
|
189
|
+
options,
|
|
190
|
+
examples: ['dgxay doctor', 'dgxay doctor --offline'],
|
|
191
|
+
run
|
|
192
|
+
};
|