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.
Files changed (45) hide show
  1. package/LICENSE +674 -0
  2. package/README-zh.md +200 -0
  3. package/README.md +182 -2
  4. package/bin/dgxay.js +42 -0
  5. package/package.json +47 -4
  6. package/src/cli/ansi.js +509 -0
  7. package/src/cli/approval.js +186 -0
  8. package/src/cli/args.js +162 -0
  9. package/src/cli/command-table.js +52 -0
  10. package/src/cli/editor.js +574 -0
  11. package/src/cli/index.js +228 -0
  12. package/src/cli/print.js +148 -0
  13. package/src/cli/render.js +324 -0
  14. package/src/cli/repl-state.js +85 -0
  15. package/src/cli/repl.js +1167 -0
  16. package/src/cli/slash.js +74 -0
  17. package/src/cli/ui.js +52 -0
  18. package/src/commands/_shared.js +20 -0
  19. package/src/commands/chat.js +212 -0
  20. package/src/commands/config.js +363 -0
  21. package/src/commands/context.js +43 -0
  22. package/src/commands/doctor.js +192 -0
  23. package/src/commands/export.js +102 -0
  24. package/src/commands/mcp.js +257 -0
  25. package/src/commands/model.js +223 -0
  26. package/src/commands/oauth.js +246 -0
  27. package/src/commands/permission.js +108 -0
  28. package/src/commands/provider.js +332 -0
  29. package/src/commands/run.js +172 -0
  30. package/src/commands/skill.js +94 -0
  31. package/src/commands/workspace.js +100 -0
  32. package/src/core/agent.js +419 -0
  33. package/src/core/llm.js +709 -0
  34. package/src/core/mcp.js +271 -0
  35. package/src/core/oauth.js +737 -0
  36. package/src/core/prompt.js +269 -0
  37. package/src/core/sessions.js +208 -0
  38. package/src/core/tools.js +391 -0
  39. package/src/services/config.js +182 -0
  40. package/src/services/input.js +91 -0
  41. package/src/services/session-bus.js +104 -0
  42. package/src/services/settings.js +340 -0
  43. package/src/services/skills.js +125 -0
  44. package/src/services/terminal.js +124 -0
  45. package/src/services/version.js +25 -0
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * oauth:账户连接(对应桌面版「设置 → 账户连接」)。
5
+ *
6
+ * dgxay oauth list
7
+ * dgxay oauth connect <openai|anthropic|xai|google>
8
+ * dgxay oauth disconnect <平台>
9
+ * dgxay oauth client <平台> [--client-id X] [--client-secret Y] [--authorize-url U] [--token-url U] [--scopes S]
10
+ *
11
+ * 说明:CLI 用系统浏览器发起授权;Anthropic 需要把浏览器显示的 code#state 粘贴回来。
12
+ */
13
+
14
+ const print = require('../cli/print');
15
+ const { parseArgs, actionFirst } = require('../cli/args');
16
+ const { COLORS } = require('../cli/ansi');
17
+ const oauth = require('../core/oauth');
18
+ const { ask } = require('../services/input');
19
+
20
+ const options = [
21
+ { name: 'client-id', type: 'string', valueName: 'id', desc: 'OAuth Client ID' },
22
+ { name: 'client-secret', type: 'string', valueName: 'secret', desc: 'OAuth Client Secret(可选)' },
23
+ { name: 'authorize-url', type: 'string', valueName: 'url', desc: '授权端点' },
24
+ { name: 'token-url', type: 'string', valueName: 'url', desc: '令牌端点' },
25
+ { name: 'scopes', type: 'string', valueName: 'text', desc: '权限范围' },
26
+ { name: 'code', type: 'string', valueName: 'code', desc: '手动流程:直接提交授权码(code#state)' },
27
+ { name: 'no-browser', type: 'boolean', desc: '不自动打开浏览器,只打印授权链接' },
28
+ { name: 'yes', alias: 'y', type: 'boolean', desc: '跳过确认' }
29
+ ];
30
+
31
+ function formatExpiry(ts) {
32
+ if (!ts) return '长期有效';
33
+ const date = new Date(ts);
34
+ const pad = (value) => String(value).padStart(2, '0');
35
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(
36
+ date.getMinutes()
37
+ )}`;
38
+ }
39
+
40
+ function list() {
41
+ const providers = oauth.listProviders();
42
+ print.blank();
43
+ print.heading('账户连接');
44
+ const rows = providers.map((item) => {
45
+ const connection = item.connection;
46
+ const status = !connection ? '未连接' : connection.expired ? '已过期' : '已连接';
47
+ const tone = !connection ? (value) => value : connection.expired ? COLORS.yellow : COLORS.green;
48
+ return [
49
+ item.id,
50
+ item.name,
51
+ item.subtitle,
52
+ tone(status),
53
+ connection?.account || '',
54
+ connection ? formatExpiry(connection.expiresAt) : '',
55
+ item.redirectMode === 'manual' ? '手动授权码' : '系统浏览器'
56
+ ];
57
+ });
58
+ print.table(
59
+ [
60
+ { title: 'ID', max: 10 },
61
+ { title: '平台', max: 10 },
62
+ { title: '账户类型', max: 24 },
63
+ { title: '状态', max: 8 },
64
+ { title: '账号', max: 28 },
65
+ { title: '有效期', max: 18 },
66
+ { title: '授权方式', max: 12 }
67
+ ],
68
+ rows
69
+ );
70
+ print.blank();
71
+ print.hint('dgxay oauth connect <平台> 连接 · dgxay oauth disconnect <平台> 断开');
72
+ print.hint('连接后可在供应商中使用 OAuth:dgxay provider edit <名称> --oauth-provider <平台>');
73
+ print.blank();
74
+ return 0;
75
+ }
76
+
77
+ async function connect(id, opts) {
78
+ const def = oauth.PROVIDERS[id];
79
+ if (!def) {
80
+ print.fail(`不支持的平台:${id}(可选 openai / anthropic / xai / google)`);
81
+ return 1;
82
+ }
83
+ const current = oauth.listProviders().find((item) => item.id === id);
84
+ if (current.connection && !current.connection.expired && !opts.yes && process.stdin.isTTY) {
85
+ const answer = await ask(`${def.name} 已连接(${current.connection.account})。重新授权会覆盖现有令牌,继续?[y/N] `);
86
+ if (!/^y/i.test(String(answer || '').trim())) {
87
+ print.hint('已取消');
88
+ return 1;
89
+ }
90
+ }
91
+
92
+ if (opts.noBrowser) process.env.DGXAY_OAUTH_NO_BROWSER = '1';
93
+
94
+ let flow;
95
+ try {
96
+ flow = await oauth.begin({ provider: id });
97
+ } catch (err) {
98
+ print.fail(err.message);
99
+ if (/Client ID/i.test(err.message)) {
100
+ print.hint(`Google 需要先创建桌面应用 OAuth 客户端:dgxay oauth client google --client-id <ID>`);
101
+ }
102
+ return 1;
103
+ }
104
+
105
+ print.blank();
106
+ print.heading(`连接 ${def.name}`);
107
+ print.hint(def.hint);
108
+ print.blank();
109
+ if (flow.browserOpened) print.ok('已打开系统浏览器,请在浏览器中完成授权');
110
+ else {
111
+ print.warn('无法自动打开浏览器,请手动访问下面的链接:');
112
+ }
113
+ print.blank();
114
+ process.stdout.write(`${COLORS.cyan(flow.authUrl)}\n`);
115
+ print.blank();
116
+
117
+ if (flow.mode === 'manual') {
118
+ print.write('浏览器授权后会显示一段授权码(形如 code#state),把它粘贴到这里:');
119
+ let code = opts.code;
120
+ if (!code) {
121
+ if (!process.stdin.isTTY) {
122
+ print.fail('非交互环境请使用 --code <授权码>');
123
+ return 1;
124
+ }
125
+ code = await ask('授权码: ');
126
+ }
127
+ if (!code) {
128
+ print.fail('授权码为空');
129
+ return 1;
130
+ }
131
+ try {
132
+ const connection = await oauth.complete(flow.id, code);
133
+ print.ok(`${def.name} 账户已连接:${connection.account}`);
134
+ return 0;
135
+ } catch (err) {
136
+ print.fail(err.message || '授权失败');
137
+ return 1;
138
+ }
139
+ }
140
+
141
+ process.stdout.write(`${COLORS.faint('等待浏览器回调完成授权…(Ctrl+C 取消)')}\n`);
142
+ try {
143
+ const connection = await oauth.wait(flow.id);
144
+ print.blank();
145
+ print.ok(`${def.name} 账户已连接:${connection.account}`);
146
+ print.hint('在供应商里启用:dgxay provider edit <名称> --oauth-provider ' + id);
147
+ return 0;
148
+ } catch (err) {
149
+ print.fail(err.message || '授权失败');
150
+ return 1;
151
+ }
152
+ }
153
+
154
+ function disconnect(id, opts) {
155
+ const def = oauth.PROVIDERS[id];
156
+ if (!def) {
157
+ print.fail(`不支持的平台:${id}`);
158
+ return 1;
159
+ }
160
+ const current = oauth.listProviders().find((item) => item.id === id);
161
+ if (!current?.connection) {
162
+ print.warn(`${def.name} 尚未连接`);
163
+ return 1;
164
+ }
165
+ oauth.disconnect(id);
166
+ print.ok(`已断开 ${def.name} 账户(${current.connection.account})`);
167
+ print.hint('使用该账户的供应商将无法请求,直到重新连接或改用 API Key');
168
+ return 0;
169
+ }
170
+
171
+ function saveClient(id, opts) {
172
+ const def = oauth.PROVIDERS[id];
173
+ if (!def) {
174
+ print.fail(`不支持的平台:${id}`);
175
+ return 1;
176
+ }
177
+ const patch = {};
178
+ for (const key of ['clientId', 'clientSecret', 'authorizeUrl', 'tokenUrl', 'scopes']) {
179
+ if (opts[key] !== undefined) patch[key] = opts[key];
180
+ }
181
+ if (!Object.keys(patch).length) {
182
+ const current = oauth.listProviders().find((item) => item.id === id);
183
+ print.blank();
184
+ print.heading(`${def.name} OAuth 客户端`);
185
+ print.keyValue([
186
+ { label: 'Client ID', value: current.clientId ? `${current.clientId.slice(0, 8)}…` : '(空)' },
187
+ { label: 'Client Secret', value: current.hasSecret ? '已设置' : '未设置' },
188
+ { label: '授权端点', value: current.authorizeUrl },
189
+ { label: '令牌端点', value: current.tokenUrl },
190
+ { label: '权限范围', value: current.scopes },
191
+ { label: '回调地址', value: current.redirectUri }
192
+ ]);
193
+ print.blank();
194
+ print.hint(`修改:dgxay oauth client ${id} --client-id <ID> [--client-secret <Secret>]`);
195
+ print.blank();
196
+ return 0;
197
+ }
198
+ try {
199
+ const saved = oauth.saveClient(id, patch);
200
+ print.ok(`已保存 ${def.name} 的 OAuth 客户端配置`);
201
+ print.hint(`现在可以连接:dgxay oauth connect ${saved.id}`);
202
+ return 0;
203
+ } catch (err) {
204
+ print.fail(err.message);
205
+ return 1;
206
+ }
207
+ }
208
+
209
+ async function run({ argv }) {
210
+ const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
211
+ const action = rawAction || 'list';
212
+ const target = rest[0];
213
+
214
+ if (action === 'list' || action === 'ls') return list();
215
+ if (action === 'connect' || action === 'login') {
216
+ if (!target) throw Object.assign(new Error('用法:dgxay oauth connect <openai|anthropic|xai|google>'), { userError: true });
217
+ return connect(target, opts);
218
+ }
219
+ if (action === 'disconnect' || action === 'logout') {
220
+ if (!target) throw Object.assign(new Error('用法:dgxay oauth disconnect <平台>'), { userError: true });
221
+ return disconnect(target, opts);
222
+ }
223
+ if (action === 'client') {
224
+ if (!target) throw Object.assign(new Error('用法:dgxay oauth client <平台> [--client-id ...]'), { userError: true });
225
+ return saveClient(target, opts);
226
+ }
227
+ print.fail(`未知子命令:${action}`);
228
+ print.hint('可用:list / connect / disconnect / client');
229
+ return 1;
230
+ }
231
+
232
+ module.exports = {
233
+ name: 'oauth',
234
+ aliases: ['account'],
235
+ summary: 'OAuth 账户连接',
236
+ usage: 'dgxay oauth <list|connect|disconnect|client>',
237
+ options,
238
+ subcommands: [
239
+ ['list', '查看各平台连接状态'],
240
+ ['connect <平台>', '连接(系统浏览器 + PKCE)'],
241
+ ['disconnect <平台>', '断开并删除本地令牌'],
242
+ ['client <平台>', '查看 / 修改 OAuth 客户端配置']
243
+ ],
244
+ examples: ['dgxay oauth list', 'dgxay oauth connect anthropic', 'dgxay oauth client google --client-id xxx.apps.googleusercontent.com'],
245
+ run
246
+ };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * permission:四档权限(对应桌面版权限列表)。
5
+ *
6
+ * dgxay permission 查看当前权限与可用性
7
+ * dgxay permission list 同上(带说明)
8
+ * dgxay permission set auto 切换权限并写入配置
9
+ *
10
+ * 自动审查只有在「当前供应商或默认供应商」配置了有效审查模型时才可用;
11
+ * 移除审查模型后自动回落到普通权限。
12
+ */
13
+
14
+ const print = require('../cli/print');
15
+ const { actionFirst } = require('../cli/args');
16
+ const { COLORS } = require('../cli/ansi');
17
+ const settings = require('../services/settings');
18
+ const { resolveReviewer } = require('../core/prompt');
19
+
20
+ const options = [];
21
+
22
+ function overview() {
23
+ const config = settings.readConfig();
24
+ const current = settings.currentPermission(config);
25
+ const available = settings.availablePermissions(config);
26
+ const reviewer = resolveReviewer(config, config.activeProviderId);
27
+ print.blank();
28
+ print.heading('权限');
29
+ for (const [key, meta] of Object.entries(settings.PERMISSIONS)) {
30
+ const usable = available.includes(key);
31
+ const active = current === key;
32
+ print.write(
33
+ ` ${active ? COLORS.accent('❯') : ' '} ${COLORS.accent(key.padEnd(8))} ${usable ? meta.label : COLORS.faint(`${meta.label}(不可用)`)}`
34
+ );
35
+ print.write(` ${COLORS.faint(meta.desc)}`);
36
+ }
37
+ print.blank();
38
+ if (reviewer) {
39
+ print.hint(`自动审查模型:${reviewer.provider.name} / ${reviewer.modelId}(来源:${reviewer.source === 'own' ? '当前供应商' : '默认供应商'})`);
40
+ } else {
41
+ print.hint('尚未配置审查模型,自动审查不可用。用 dgxay provider review <供应商> --model <模型ID> 设置');
42
+ }
43
+ if (config.ui?.permission === 'auto' && !available.includes('auto')) {
44
+ print.warn('配置里的自动审查已失效,当前按普通权限运行');
45
+ }
46
+ print.blank();
47
+ print.hint('dgxay permission set <none|normal|auto|full>');
48
+ print.blank();
49
+ return 0;
50
+ }
51
+
52
+ async function run({ argv, runtime }) {
53
+ const { action: rawAction, rest } = actionFirst(argv, options);
54
+ const action = rawAction || 'list';
55
+ const config = settings.readConfig();
56
+
57
+ if (action === 'list' || action === 'show') return overview();
58
+
59
+ if (action === 'set' || action === 'use') {
60
+ const level = rest[0];
61
+ if (!level) throw Object.assign(new Error('用法:dgxay permission set <none|normal|auto|full>'), { userError: true });
62
+ if (!settings.PERMISSIONS[level]) {
63
+ print.fail('权限只能是 none / normal / auto / full');
64
+ return 1;
65
+ }
66
+ if (level === 'auto' && !settings.hasReviewModel(config)) {
67
+ print.fail('自动审查不可用:当前或默认供应商没有配置有效的审查模型');
68
+ print.hint('先运行:dgxay provider review <供应商> --model <模型ID>');
69
+ return 1;
70
+ }
71
+ settings.setPermission(level);
72
+ print.ok(`权限已设置为:${settings.PERMISSIONS[level].label}`);
73
+ if (level === 'full') {
74
+ print.warn('完全权限下 AI 可以执行任意命令、读写任意文件,且不会询问');
75
+ }
76
+ void runtime;
77
+ return 0;
78
+ }
79
+
80
+ if (settings.PERMISSIONS[action]) {
81
+ const level = action;
82
+ if (level === 'auto' && !settings.hasReviewModel(config)) {
83
+ print.fail('自动审查不可用:请先配置审查模型');
84
+ return 1;
85
+ }
86
+ settings.setPermission(level);
87
+ print.ok(`权限已设置为:${settings.PERMISSIONS[level].label}`);
88
+ return 0;
89
+ }
90
+
91
+ print.fail(`未知用法:${action}`);
92
+ print.hint('可用:list / set <档位>');
93
+ return 1;
94
+ }
95
+
96
+ module.exports = {
97
+ name: 'permission',
98
+ aliases: ['perm'],
99
+ summary: '四档权限:查看与切换',
100
+ usage: 'dgxay permission [list|show|set <档位>]',
101
+ options,
102
+ subcommands: [
103
+ ['list', '查看权限与可用性'],
104
+ ['set <档位>', '切换权限(写入配置)']
105
+ ],
106
+ examples: ['dgxay permission', 'dgxay permission set normal', 'dgxay permission set auto'],
107
+ run
108
+ };
@@ -0,0 +1,332 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * provider:供应商管理(对应桌面版「设置 → 供应商与模型」)。
5
+ *
6
+ * dgxay provider list
7
+ * dgxay provider add --name DeepSeek --base-url https://api.deepseek.com/v1 --model deepseek-chat [--api-key sk-...]
8
+ * dgxay provider edit <名称> [--base-url ...] [--api-key ...] [--model ...]
9
+ * dgxay provider rm <名称>
10
+ * dgxay provider use <名称> 设为当前供应商
11
+ * dgxay provider mv <名称> up|down 调整顺序(第一个是默认供应商,也是审查模型回退来源)
12
+ * dgxay provider review <名称> [--model <id>] 设置自动审查模型;不带 --model 显示当前值
13
+ */
14
+
15
+ const print = require('../cli/print');
16
+ const { parseArgs, actionFirst } = require('../cli/args');
17
+ const { COLORS } = require('../cli/ansi');
18
+ const settings = require('../services/settings');
19
+ const cfg = require('../services/config');
20
+ const oauth = require('../core/oauth');
21
+ const { ask, askSecret, confirm } = require('../services/input');
22
+
23
+ const options = [
24
+ { name: 'name', type: 'string', valueName: 'text', desc: '供应商名称' },
25
+ { name: 'protocol', type: 'string', valueName: 'p', desc: 'openai / anthropic / responses' },
26
+ { name: 'base-url', type: 'string', valueName: 'url', desc: '接口地址' },
27
+ { name: 'api-key', type: 'string', valueName: 'key', desc: 'API Key' },
28
+ { name: 'model', type: 'list', valueName: 'id', desc: '模型 ID(可重复或逗号分隔)' },
29
+ { name: 'oauth-provider', type: 'string', valueName: 'id', desc: '使用已连接的 OAuth 账户' },
30
+ { name: 'review-model', type: 'string', valueName: 'id', desc: '自动审查模型 ID' },
31
+ { name: 'context-window', type: 'number', valueName: 'n', desc: '上下文窗口(可选)' },
32
+ { name: 'yes', alias: 'y', type: 'boolean', desc: '跳过确认' }
33
+ ];
34
+
35
+ function list() {
36
+ const config = settings.readConfig();
37
+ const providers = config.providers || [];
38
+ print.blank();
39
+ if (!providers.length) {
40
+ print.hint('还没有供应商。运行 dgxay provider add 或 dgxay config init');
41
+ print.blank();
42
+ return 0;
43
+ }
44
+ print.heading(`供应商(${providers.length})`);
45
+ const rows = providers.map((provider, index) => [
46
+ String(index + 1),
47
+ index === 0 ? COLORS.accent('默认') : '',
48
+ provider.id === config.activeProviderId ? COLORS.green('当前') : '',
49
+ provider.name,
50
+ settings.PROTOCOL_META[provider.protocol || 'openai'].short,
51
+ provider.baseUrl,
52
+ (provider.models || []).map((m) => m.id || m.name).join(', '),
53
+ provider.reviewModelId || '-',
54
+ provider.authMode === 'oauth' ? `OAuth(${provider.oauthProvider || '?'})` : provider.apiKey ? cfg.maskKey(provider.apiKey) : '-'
55
+ ]);
56
+ print.table(
57
+ [
58
+ { title: '#', max: 3 },
59
+ { title: '', max: 4 },
60
+ { title: '', max: 4 },
61
+ { title: '名称', max: 16 },
62
+ { title: '协议', max: 16 },
63
+ { title: 'Base URL', max: 40 },
64
+ { title: '模型', max: 30 },
65
+ { title: '审查模型', max: 18 },
66
+ { title: '认证', max: 16 }
67
+ ],
68
+ rows
69
+ );
70
+ print.blank();
71
+ print.hint('供应商顺序有意义:第一个是默认供应商,也是未配置审查模型时的回退来源');
72
+ print.blank();
73
+ return 0;
74
+ }
75
+
76
+ async function add(opts) {
77
+ const interactive = process.stdin.isTTY && !opts.baseUrl;
78
+ let protocol = opts.protocol || 'openai';
79
+ let baseUrl = opts.baseUrl || '';
80
+ let name = opts.name || '';
81
+ let apiKey = opts.apiKey;
82
+ let models = opts.model || [];
83
+ let oauthProvider = opts.oauthProvider || '';
84
+
85
+ if (interactive) {
86
+ if (!opts.protocol) {
87
+ const answer = await ask('协议 [1] OpenAI ChatCompletions [2] Anthropic Messages [3] OpenAI Responses : ');
88
+ const index = Number(String(answer || '').trim()) || 1;
89
+ protocol = ['openai', 'anthropic', 'responses'][index - 1] || 'openai';
90
+ }
91
+ if (!baseUrl) {
92
+ const answer = await ask(`Base URL [${settings.PROTOCOL_META[protocol].basePlaceholder}]: `);
93
+ baseUrl = String(answer || '').trim() || settings.PROTOCOL_META[protocol].basePlaceholder;
94
+ }
95
+ if (!name) {
96
+ const answer = await ask('供应商名称: ');
97
+ name = String(answer || '').trim();
98
+ }
99
+ if (!oauthProvider && apiKey === undefined) {
100
+ const secret = await askSecret('API Key(本地模型可留空): ');
101
+ apiKey = secret === null ? undefined : secret;
102
+ }
103
+ if (!models.length) {
104
+ const answer = await ask('模型 ID(多个用逗号分隔): ');
105
+ models = String(answer || '')
106
+ .split(/[,,\s]+/)
107
+ .filter(Boolean);
108
+ }
109
+ }
110
+
111
+ const result = settings.upsertProvider({
112
+ name: name || undefined,
113
+ protocol,
114
+ baseUrl,
115
+ apiKey,
116
+ models,
117
+ authMode: oauthProvider ? 'oauth' : 'apiKey',
118
+ oauthProvider,
119
+ reviewModelId: opts.reviewModelId
120
+ });
121
+ print.ok(`已添加供应商「${result.provider.name}」(${result.provider.id})`);
122
+ return 0;
123
+ }
124
+
125
+ function edit(ref, opts) {
126
+ const provider = settings.findProvider(ref);
127
+ if (!provider) {
128
+ print.fail(`找不到供应商:${ref}`);
129
+ return 1;
130
+ }
131
+ const result = settings.upsertProvider({
132
+ ref: provider.id,
133
+ name: opts.name,
134
+ protocol: opts.protocol,
135
+ baseUrl: opts.baseUrl,
136
+ apiKey: opts.apiKey,
137
+ models: opts.model?.length ? opts.model : undefined,
138
+ authMode: opts.oauthProvider ? 'oauth' : provider.authMode,
139
+ oauthProvider: opts.oauthProvider,
140
+ reviewModelId: opts.reviewModelId
141
+ });
142
+ print.ok(`已更新供应商「${result.provider.name}」`);
143
+ return 0;
144
+ }
145
+
146
+ async function remove(ref, opts) {
147
+ const provider = settings.findProvider(ref);
148
+ if (!provider) {
149
+ print.fail(`找不到供应商:${ref}`);
150
+ return 1;
151
+ }
152
+ if (!opts.yes && process.stdin.isTTY) {
153
+ const ok = await confirm(`删除供应商「${provider.name}」及其密钥?`, false);
154
+ if (!ok) {
155
+ print.hint('已取消');
156
+ return 1;
157
+ }
158
+ }
159
+ const config = settings.removeProvider(provider.id);
160
+ print.ok(`已删除「${provider.name}」`);
161
+ if (!config.providers?.length) print.hint('已经没有供应商了,运行 dgxay config init 重新配置');
162
+ else if (config.activeProviderId !== settings.readConfig().activeProviderId) {
163
+ print.hint(`当前供应商已切换为「${(config.providers || []).find((p) => p.id === config.activeProviderId)?.name || '未知'}」`);
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ function use(ref) {
169
+ const provider = settings.findProvider(ref);
170
+ if (!provider) {
171
+ print.fail(`找不到供应商:${ref}`);
172
+ return 1;
173
+ }
174
+ const model = provider.models?.[0];
175
+ if (!model) {
176
+ print.fail(`供应商 ${provider.name} 没有模型`);
177
+ return 1;
178
+ }
179
+ settings.saveConfig({ activeProviderId: provider.id, activeModelId: model.id });
180
+ print.ok(`当前供应商:${provider.name} / ${model.name || model.id}`);
181
+ return 0;
182
+ }
183
+
184
+ function review(ref, opts) {
185
+ const provider = settings.findProvider(ref);
186
+ if (!provider) {
187
+ print.fail(`找不到供应商:${ref}`);
188
+ return 1;
189
+ }
190
+ if (opts.reviewModel === undefined) {
191
+ const model = (provider.models || []).find((m) => m.id === provider.reviewModelId);
192
+ if (model) print.write(`${provider.name} 的审查模型:${model.name || model.id}(${model.id})`);
193
+ else print.write(`${provider.name} 未配置审查模型,自动审查时会回退到默认供应商`);
194
+ return 0;
195
+ }
196
+ const models = provider.models || [];
197
+ const target = models.find((m) => m.id === opts.reviewModel);
198
+ if (opts.reviewModel && !target) {
199
+ print.fail(`供应商 ${provider.name} 下没有模型 ${opts.reviewModel}`);
200
+ print.hint(`可用:${models.map((m) => m.id).join(', ')}`);
201
+ return 1;
202
+ }
203
+ const next = (settings.readConfig().providers || []).map((p) =>
204
+ p.id === provider.id ? { ...p, reviewModelId: target ? target.id : '' } : p
205
+ );
206
+ settings.saveConfig({ providers: next });
207
+ if (target) {
208
+ print.ok(`${provider.name} 的自动审查模型:${target.name || target.id}`);
209
+ print.hint('现在可以在 /permission 中选择「自动审查」');
210
+ } else {
211
+ print.ok(`已清除 ${provider.name} 的审查模型`);
212
+ }
213
+ return 0;
214
+ }
215
+
216
+ function move(ref, direction) {
217
+ const dir = direction === 'up' || direction === 'u' || direction === '-1' ? -1 : 1;
218
+ settings.moveProvider(ref, dir);
219
+ print.ok(`已${dir < 0 ? '上移' : '下移'}「${settings.findProvider(ref)?.name || ref}」`);
220
+ return list();
221
+ }
222
+
223
+ function oauthAccounts() {
224
+ const list = oauth.listProviders();
225
+ print.blank();
226
+ print.heading('OAuth 账户');
227
+ const rows = list.map((item) => [
228
+ item.name,
229
+ item.subtitle,
230
+ item.connection && !item.connection.expired
231
+ ? COLORS.green('已连接')
232
+ : item.connection?.expired
233
+ ? COLORS.yellow('已过期')
234
+ : '未连接',
235
+ item.connection?.account || '',
236
+ item.redirectMode === 'manual' ? '手动授权码' : '系统浏览器'
237
+ ]);
238
+ print.table(
239
+ [
240
+ { title: '平台', max: 12 },
241
+ { title: '账户类型', max: 24 },
242
+ { title: '状态', max: 8 },
243
+ { title: '账号', max: 30 },
244
+ { title: '授权方式', max: 12 }
245
+ ],
246
+ rows
247
+ );
248
+ print.blank();
249
+ print.hint('dgxay oauth connect <平台> 连接账户;连接后可用 dgxay provider add --oauth-provider <平台>');
250
+ print.blank();
251
+ return 0;
252
+ }
253
+
254
+ async function run({ argv }) {
255
+ const { action: rawAction, rest, options: opts } = actionFirst(argv, options);
256
+ const action = rawAction || 'list';
257
+
258
+ switch (action) {
259
+ case 'list':
260
+ case 'ls':
261
+ return list();
262
+ case 'add':
263
+ return add(opts);
264
+ case 'edit':
265
+ case 'set': {
266
+ if (!rest[0]) throw Object.assign(new Error('用法:dgxay provider edit <名称> [选项]'), { userError: true });
267
+ return edit(rest[0], opts);
268
+ }
269
+ case 'rm':
270
+ case 'remove':
271
+ case 'delete': {
272
+ if (!rest[0]) throw Object.assign(new Error('用法:dgxay provider rm <名称>'), { userError: true });
273
+ return remove(rest[0], opts);
274
+ }
275
+ case 'use': {
276
+ if (!rest[0]) throw Object.assign(new Error('用法:dgxay provider use <名称>'), { userError: true });
277
+ return use(rest[0]);
278
+ }
279
+ case 'mv':
280
+ case 'move': {
281
+ if (!rest[0]) throw Object.assign(new Error('用法:dgxay provider mv <名称> up|down'), { userError: true });
282
+ return move(rest[0], rest[1] || 'down');
283
+ }
284
+ case 'review': {
285
+ if (!rest[0]) {
286
+ const config = settings.readConfig();
287
+ const rows = (config.providers || []).map((p) => [p.name, p.reviewModelId || '(未配置)']);
288
+ print.blank();
289
+ print.table(
290
+ [
291
+ { title: '供应商', max: 20 },
292
+ { title: '审查模型', max: 30 }
293
+ ],
294
+ rows
295
+ );
296
+ print.blank();
297
+ return 0;
298
+ }
299
+ return review(rest[0], opts);
300
+ }
301
+ case 'oauth':
302
+ return oauthAccounts();
303
+ default:
304
+ print.fail(`未知子命令:${action}`);
305
+ print.hint('可用:list / add / edit / rm / use / mv / review / oauth');
306
+ return 1;
307
+ }
308
+ }
309
+
310
+ module.exports = {
311
+ name: 'provider',
312
+ aliases: ['prov'],
313
+ summary: '供应商与审查模型管理',
314
+ usage: 'dgxay provider <list|add|edit|rm|use|mv|review>',
315
+ options,
316
+ subcommands: [
317
+ ['list', '列出所有供应商'],
318
+ ['add', '新增供应商'],
319
+ ['edit <名称>', '修改供应商'],
320
+ ['rm <名称>', '删除供应商'],
321
+ ['use <名称>', '设为当前供应商'],
322
+ ['mv <名称> up|down', '调整顺序'],
323
+ ['review [名称] [--model id]', '查看 / 设置自动审查模型']
324
+ ],
325
+ examples: [
326
+ 'dgxay provider list',
327
+ 'dgxay provider add --name DeepSeek --base-url https://api.deepseek.com/v1 --model deepseek-chat',
328
+ 'dgxay provider add --oauth-provider openai --base-url https://api.openai.com/v1 --model gpt-5',
329
+ 'dgxay provider review DeepSeek --model deepseek-chat'
330
+ ],
331
+ run
332
+ };