dgxay-code-cli 1.0.0 → 1.1.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/src/core/agent.js CHANGED
@@ -32,6 +32,7 @@ const {
32
32
  summarizeArgs
33
33
  } = require('./prompt');
34
34
  const { trimHistory } = require('./sessions');
35
+ const { t } = require('../cli/i18n');
35
36
 
36
37
  const HISTORY_LIMIT = 24;
37
38
  const MAX_STEPS_CHAT = 8;
@@ -43,16 +44,16 @@ const VALID_PERMISSIONS = ['none', 'normal', 'auto', 'full'];
43
44
  function describeOperation({ kind, name, args, command }) {
44
45
  const lines = [];
45
46
  if (kind === 'terminal') {
46
- lines.push(`命令:${command || ''}`);
47
+ lines.push(`Command: ${command || ''}`);
47
48
  return lines.join('\n');
48
49
  }
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}`);
50
+ lines.push(`Tool: ${name}`);
51
+ if (args?.path) lines.push(`Path: ${args.path}`);
52
+ if (args?.url) lines.push(`URL: ${args.url}`);
53
+ if (args?.command) lines.push(`Command: ${args.command}`);
54
+ if (args?.name) lines.push(`Name: ${args.name}`);
54
55
  const body = summarizeArgs(args);
55
- if (body && body !== '{}') lines.push(`参数:${body}`);
56
+ if (body && body !== '{}') lines.push(`Arguments: ${body}`);
56
57
  return lines.join('\n');
57
58
  }
58
59
 
@@ -82,7 +83,7 @@ function resolvePermissions(config) {
82
83
  async function requestOperationApproval({ config, permission, kind, name, args, command, workspace, providerId, toolId, askUser, emit }) {
83
84
  if (permission === 'full') return { allow: true, source: 'full', review: null };
84
85
  if (permission !== 'normal' && permission !== 'auto') {
85
- return { allow: false, source: 'none', review: null, reason: '当前权限禁止该操作' };
86
+ return { allow: false, source: 'none', review: null, reason: 'Current permission level forbids this operation' };
86
87
  }
87
88
 
88
89
  if (permission === 'normal') {
@@ -90,7 +91,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
90
91
  const allowed = await askUser({
91
92
  kind,
92
93
  permission,
93
- title: kind === 'terminal' ? 'AI 请求执行终端命令' : 'AI 请求执行操作',
94
+ title: kind === 'terminal' ? t('approval_terminal') : t('approval_tool'),
94
95
  operation: name,
95
96
  detail: describeOperation({ kind, name, args, command }),
96
97
  command: command || '',
@@ -99,7 +100,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
99
100
  reviewer: '',
100
101
  reviewModel: ''
101
102
  });
102
- return { allow: Boolean(allowed), source: 'user', review: null, reason: allowed ? '' : '用户拒绝了该操作' };
103
+ return { allow: Boolean(allowed), source: 'user', review: null, reason: allowed ? '' : 'The user declined this operation' };
103
104
  }
104
105
 
105
106
  emit?.({ type: 'review-start', toolId, name, kind });
@@ -128,7 +129,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
128
129
  const allowed = await askUser({
129
130
  kind,
130
131
  permission,
131
- title: kind === 'terminal' ? 'AI 请求执行终端命令' : 'AI 请求执行操作',
132
+ title: kind === 'terminal' ? t('approval_terminal') : t('approval_tool'),
132
133
  operation: name,
133
134
  detail: describeOperation({ kind, name, args, command }),
134
135
  command: command || '',
@@ -137,7 +138,7 @@ async function requestOperationApproval({ config, permission, kind, name, args,
137
138
  reviewer: review.reviewer,
138
139
  reviewModel: review.modelId
139
140
  });
140
- return { allow: Boolean(allowed), source: 'user', review, reason: allowed ? '' : '用户拒绝了该操作' };
141
+ return { allow: Boolean(allowed), source: 'user', review, reason: allowed ? '' : 'The user declined this operation' };
141
142
  }
142
143
 
143
144
  function providerOf(config, providerId) {
@@ -197,12 +198,12 @@ async function runAgent({
197
198
  }) {
198
199
  const resolvedMode = mode === 'work' ? 'work' : 'chat';
199
200
  if (!Array.isArray(messages) || !messages.length) {
200
- throw Object.assign(new Error('消息不能为空:请提供至少一条用户消息'), { userError: true });
201
+ throw Object.assign(new Error('No messages: at least one user message is required'), { userError: true });
201
202
  }
202
203
  const provider = providerOf(config, providerId);
203
- if (!provider) throw new Error('未找到对应的供应商,请先运行 dgxay provider add');
204
+ if (!provider) throw new Error('Provider not found. Run dgxay provider add first');
204
205
  const targetModel = modelOf(provider, modelId, config);
205
- if (!targetModel) throw new Error('该供应商下没有可用模型,请先添加模型');
206
+ if (!targetModel) throw new Error('This provider has no models. Add one first');
206
207
 
207
208
  const effectivePermission = VALID_PERMISSIONS.includes(permission) ? permission : resolvePermissions(config);
208
209
  const activeWorkspace = normalizeWorkspace(workspace || config.work?.workspace || '');
@@ -251,7 +252,7 @@ async function runAgent({
251
252
  let continuationCount = 0;
252
253
 
253
254
  for (let step = 0; step < maxSteps; step += 1) {
254
- emit({ type: 'status', text: step === 0 ? '正在生成' : '正在继续处理' });
255
+ emit({ type: 'status', text: step === 0 ? 'Working' : 'Continuing' });
255
256
  const result = await streamChat({
256
257
  protocol: provider.protocol || 'openai',
257
258
  baseUrl: provider.baseUrl,
@@ -372,13 +373,13 @@ async function runAgent({
372
373
  apiMessages.push({
373
374
  role: 'user',
374
375
  content:
375
- '继续执行,直到任务全部完成。不要只描述计划或承诺下一步,直接调用工具完成剩余步骤;完成后先验证结果,再给出简短总结。'
376
+ 'Continue until the task is fully done. Do not describe a plan or promise a next step — call the tools to finish the remaining work, verify the result, then give a short summary.'
376
377
  });
377
378
  if (finalText) {
378
379
  finalText += '\n\n';
379
380
  emit({ type: 'delta', text: '\n\n' });
380
381
  }
381
- emit({ type: 'status', text: '正在继续完成任务' });
382
+ emit({ type: 'status', text: 'Continuing the task' });
382
383
  continue;
383
384
  }
384
385
 
package/src/core/llm.js CHANGED
@@ -20,7 +20,7 @@ function normalizeBaseUrl(baseUrl) {
20
20
 
21
21
  function endpointFor(baseUrl, path) {
22
22
  const base = normalizeBaseUrl(baseUrl);
23
- if (!base) throw new Error('Base URL 不能为空');
23
+ if (!base) throw new Error('Base URL must not be empty');
24
24
  if (base.toLowerCase().endsWith(path.toLowerCase())) return base;
25
25
  // 已经带版本前缀的根地址(/v1、/v1beta/openai、/v1beta1 等)直接拼端点
26
26
  if (/\/v\d+[a-z0-9]*(?:\/.*)?$/i.test(base)) return `${base}${path}`;
@@ -95,7 +95,7 @@ function splitSystem(messages) {
95
95
 
96
96
  /** 通用 SSE 读取:每条 data 交给 onData;忽略注释与 [DONE] */
97
97
  async function consumeSSE(res, onData) {
98
- if (!res.body) throw new Error('当前网络响应不支持流式读取');
98
+ if (!res.body) throw new Error('This network response does not support streaming');
99
99
  const reader = res.body.getReader();
100
100
  const decoder = new TextDecoder('utf-8');
101
101
  let buffer = '';
@@ -219,7 +219,7 @@ async function streamOpenAI(options) {
219
219
  } catch {
220
220
  return;
221
221
  }
222
- if (json.error) throw new Error(json.error.message || '模型返回错误');
222
+ if (json.error) throw new Error(json.error.message || 'The model returned an error');
223
223
  const choice = json.choices?.[0];
224
224
  if (choice?.finish_reason) finishReason = choice.finish_reason;
225
225
  const delta = choice?.delta || choice?.message || {};
@@ -365,7 +365,7 @@ async function streamAnthropic(options) {
365
365
 
366
366
  if (!stream) {
367
367
  const json = await res.json();
368
- if (json.error) throw new Error(json.error.message || 'Anthropic 返回错误');
368
+ if (json.error) throw new Error(json.error.message || 'Anthropic returned an error');
369
369
  let content = '';
370
370
  let reasoning = '';
371
371
  const toolCalls = [];
@@ -452,7 +452,7 @@ async function streamAnthropic(options) {
452
452
  break;
453
453
  }
454
454
  case 'error':
455
- throw new Error(event.error?.message || 'Anthropic 返回错误');
455
+ throw new Error(event.error?.message || 'Anthropic returned an error');
456
456
  default:
457
457
  break;
458
458
  }
@@ -573,7 +573,7 @@ async function streamResponses(options) {
573
573
 
574
574
  if (!stream) {
575
575
  const json = await res.json();
576
- if (json.error) throw new Error(json.error.message || 'Responses 返回错误');
576
+ if (json.error) throw new Error(json.error.message || 'Responses returned an error');
577
577
  const { text, thought, calls } = collectOutput(json);
578
578
  if (text) onDelta?.(text);
579
579
  if (thought) onReasoning?.(thought);
@@ -655,9 +655,9 @@ async function streamResponses(options) {
655
655
  finishReason = event.response?.status || 'completed';
656
656
  break;
657
657
  case 'response.failed':
658
- throw new Error(event.response?.error?.message || 'Responses 请求失败');
658
+ throw new Error(event.response?.error?.message || 'Responses request failed');
659
659
  case 'error':
660
- throw new Error(event.error?.message || 'Responses 返回错误');
660
+ throw new Error(event.error?.message || 'Responses returned an error');
661
661
  default:
662
662
  break;
663
663
  }
@@ -676,8 +676,8 @@ async function streamResponses(options) {
676
676
 
677
677
  async function streamChat(options) {
678
678
  const protocol = PROTOCOLS.includes(options.protocol) ? options.protocol : 'openai';
679
- if (!options.model) throw new Error('未选择模型');
680
- if (!Array.isArray(options.messages) || !options.messages.length) throw new Error('消息不能为空');
679
+ if (!options.model) throw new Error('No model selected');
680
+ if (!Array.isArray(options.messages) || !options.messages.length) throw new Error('Messages cannot be empty');
681
681
  if (protocol === 'anthropic') return streamAnthropic(options);
682
682
  if (protocol === 'responses') return streamResponses(options);
683
683
  return streamOpenAI(options);
package/src/core/mcp.js CHANGED
@@ -92,8 +92,8 @@ function normalizeEnv(raw) {
92
92
  function addServer(input) {
93
93
  const name = String(input?.name || '').trim();
94
94
  const command = String(input?.command || '').trim();
95
- if (!name) throw new Error('MCP 名称不能为空');
96
- if (!command) throw new Error('MCP 启动命令不能为空');
95
+ if (!name) throw new Error('MCP name cannot be empty');
96
+ if (!command) throw new Error('MCP command cannot be empty');
97
97
 
98
98
  const data = readConfig();
99
99
  const record = {
@@ -167,7 +167,7 @@ function pushLog(entry, stream, text) {
167
167
  function startServer(id) {
168
168
  const data = readConfig();
169
169
  const server = data.servers.find((item) => item.id === id) || data.servers.find((item) => item.name === id);
170
- if (!server) throw new Error('找不到该 MCP server');
170
+ if (!server) throw new Error('MCP server not found');
171
171
 
172
172
  const existing = processes.get(server.id);
173
173
  if (existing?.status === 'running' && existing.child && !existing.child.killed) {
@@ -185,7 +185,7 @@ function startServer(id) {
185
185
  stdio: ['pipe', 'pipe', 'pipe']
186
186
  });
187
187
  } catch (err) {
188
- throw new Error(`无法启动 MCP server:${err.message}`);
188
+ throw new Error(`Cannot start MCP server: ${err.message}`);
189
189
  }
190
190
 
191
191
  const entry = {
@@ -210,7 +210,7 @@ function startServer(id) {
210
210
  entry.status = wasStopping || code === 0 ? 'stopped' : 'error';
211
211
  entry.exitCode = code;
212
212
  if (!wasStopping && code !== 0 && code !== null) {
213
- entry.error = `进程退出,code=${code}${signal ? `, signal=${signal}` : ''}`;
213
+ entry.error = `process exited, code=${code}${signal ? `, signal=${signal}` : ''}`;
214
214
  }
215
215
  notify({ type: 'status', id: server.id, status: entry.status, code, signal });
216
216
  });
package/src/core/oauth.js CHANGED
@@ -30,7 +30,7 @@ const PROVIDERS = {
30
30
  openai: {
31
31
  id: 'openai',
32
32
  name: 'OpenAI',
33
- subtitle: 'ChatGPT / Codex 账户',
33
+ subtitle: 'ChatGPT / Codex account',
34
34
  authorizeUrl: 'https://auth.openai.com/oauth/authorize',
35
35
  tokenUrl: 'https://auth.openai.com/oauth/token',
36
36
  clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
@@ -47,13 +47,13 @@ const PROVIDERS = {
47
47
  },
48
48
  baseUrl: 'https://api.openai.com/v1',
49
49
  protocol: 'openai',
50
- hint: '使用 ChatGPT 订阅账户。若推理接口拒绝订阅令牌,请为该供应商改用 API Key。',
50
+ hint: 'Uses your ChatGPT subscription account. If the inference endpoint rejects subscription tokens, use an API key for this provider instead.',
51
51
  publicClient: true
52
52
  },
53
53
  anthropic: {
54
54
  id: 'anthropic',
55
55
  name: 'Anthropic',
56
- subtitle: 'Claude 账户',
56
+ subtitle: 'Claude account',
57
57
  authorizeUrl: 'https://claude.ai/oauth/authorize',
58
58
  tokenUrl: 'https://console.anthropic.com/v1/oauth/token',
59
59
  clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
@@ -65,7 +65,7 @@ const PROVIDERS = {
65
65
  extraAuth: { code: 'true' },
66
66
  baseUrl: 'https://api.anthropic.com',
67
67
  protocol: 'anthropic',
68
- hint: '浏览器授权后会显示一段授权码,复制回终端即可完成连接。',
68
+ hint: 'After approving in the browser you get an authorization code; paste it back into the terminal to finish connecting.',
69
69
  publicClient: true
70
70
  },
71
71
  xai: {
@@ -84,13 +84,13 @@ const PROVIDERS = {
84
84
  extraAuth: {},
85
85
  baseUrl: 'https://api.x.ai/v1',
86
86
  protocol: 'openai',
87
- hint: '需要 SuperGrok 或 X Premium+。xAI 可能对 OAuth 推理接口做套餐限制,返回 403 时请改用 API Key。',
87
+ hint: 'Requires SuperGrok or X Premium+. xAI may restrict OAuth inference by plan; use an API key if you get a 403.',
88
88
  publicClient: true
89
89
  },
90
90
  google: {
91
91
  id: 'google',
92
92
  name: 'Google',
93
- subtitle: 'Gemini API 账户',
93
+ subtitle: 'Gemini API account',
94
94
  authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
95
95
  tokenUrl: 'https://oauth2.googleapis.com/token',
96
96
  clientId: '',
@@ -104,7 +104,7 @@ const PROVIDERS = {
104
104
  userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
105
105
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
106
106
  protocol: 'openai',
107
- hint: '需要先在 Google Cloud Console 创建「桌面应用」OAuth 客户端,并启用 Generative Language API。',
107
+ hint: 'First create a "Desktop app" OAuth client in Google Cloud Console and enable the Generative Language API.',
108
108
  publicClient: false
109
109
  }
110
110
  };
@@ -168,7 +168,7 @@ function decodeConnection(raw) {
168
168
  try {
169
169
  if (raw.enc === true) {
170
170
  // 桌面版 Electron safeStorage 加密的数据,CLI 无法解密
171
- const error = new Error('该令牌由桌面版加密保存,命令行版无法读取。请在桌面版中重新连接,或在本工具中重新授权。');
171
+ const error = new Error('This token was encrypted by the desktop app and cannot be read by the CLI. Reconnect in the desktop app, or re-authorize in this tool.');
172
172
  error.code = 'DESKTOP_ENCRYPTED';
173
173
  throw error;
174
174
  }
@@ -176,7 +176,7 @@ function decodeConnection(raw) {
176
176
  const key = tokenKey();
177
177
  if (!key) {
178
178
  const error = new Error(
179
- '令牌使用 DGXAY_TOKEN_KEY 加密保存,但当前环境没有该变量,无法解密。请设置相同的 DGXAY_TOKEN_KEY 后重试。'
179
+ 'The token is encrypted with DGXAY_TOKEN_KEY, but that variable is missing in this environment, so it cannot be decrypted. Set the same DGXAY_TOKEN_KEY and try again.'
180
180
  );
181
181
  error.code = 'MISSING_TOKEN_KEY';
182
182
  throw error;
@@ -210,7 +210,7 @@ function encodeConnection(connection) {
210
210
 
211
211
  function clientConfig(id, store = readStore()) {
212
212
  const def = PROVIDERS[id];
213
- if (!def) throw new Error('不支持的 OAuth 供应商');
213
+ if (!def) throw new Error('Unsupported OAuth provider');
214
214
  const saved = store.clients[id] || {};
215
215
  const pick = (key) => {
216
216
  const value = saved[key];
@@ -239,7 +239,7 @@ function connectionSummary(id, connection) {
239
239
  if (connection.__error) {
240
240
  return {
241
241
  provider: id,
242
- account: '(无法解密)',
242
+ account: '(cannot decrypt)',
243
243
  broken: true,
244
244
  error: connection.__error.message,
245
245
  expired: true,
@@ -267,7 +267,7 @@ function listProviders() {
267
267
  subtitle: def.subtitle,
268
268
  hint: def.hint,
269
269
  redirectMode: def.redirectMode,
270
- redirectUri: config.redirectUri || (def.redirectMode === 'loopback' ? '由系统分配回环端口' : def.redirectUri),
270
+ redirectUri: config.redirectUri || (def.redirectMode === 'loopback' ? 'Loopback port assigned by the system' : def.redirectUri),
271
271
  scopes: config.scopes,
272
272
  authorizeUrl: config.authorizeUrl,
273
273
  tokenUrl: config.tokenUrl,
@@ -283,7 +283,7 @@ function listProviders() {
283
283
 
284
284
  function saveClient(id, patch = {}) {
285
285
  const def = PROVIDERS[id];
286
- if (!def) throw new Error('不支持的 OAuth 供应商');
286
+ if (!def) throw new Error('Unsupported OAuth provider');
287
287
  const store = readStore();
288
288
  const current = store.clients[id] || {};
289
289
  const next = { ...current };
@@ -301,10 +301,10 @@ function saveClient(id, patch = {}) {
301
301
  }
302
302
 
303
303
  if (next.authorizeUrl && !/^https?:\/\//i.test(next.authorizeUrl)) {
304
- throw new Error('授权端点必须以 http:// 或 https:// 开头');
304
+ throw new Error('The authorization endpoint must start with http:// or https://');
305
305
  }
306
306
  if (next.tokenUrl && !/^https?:\/\//i.test(next.tokenUrl)) {
307
- throw new Error('令牌端点必须以 http:// 或 https:// 开头');
307
+ throw new Error('The token endpoint must start with http:// or https://');
308
308
  }
309
309
 
310
310
  store.clients[id] = next;
@@ -338,7 +338,7 @@ function accountFromTokens(tokens) {
338
338
  const email = claims.email || accessClaims.email || '';
339
339
  const name = claims.name || claims.preferred_username || accessClaims.name || '';
340
340
  const subject = claims.sub || accessClaims.sub || '';
341
- return email || name || (subject ? `账户 ${String(subject).slice(0, 10)}` : '已连接账户');
341
+ return email || name || (subject ? `Account ${String(subject).slice(0, 10)}` : 'Connected account');
342
342
  }
343
343
 
344
344
  async function fetchAccount(id, tokens) {
@@ -384,7 +384,7 @@ async function requestToken(config, params) {
384
384
  }
385
385
  if (!res.ok || !data.access_token) {
386
386
  const detail = data.error_description || data.error?.message || data.error || text.slice(0, 300);
387
- throw new Error(`OAuth 令牌交换失败:HTTP ${res.status}${detail ? ` · ${detail}` : ''}`);
387
+ throw new Error(`OAuth token exchange failed: HTTP ${res.status}${detail ? ` · ${detail}` : ''}`);
388
388
  }
389
389
  return data;
390
390
  } finally {
@@ -433,11 +433,11 @@ async function getAccessToken(id) {
433
433
  const connection = decodeConnection(store.connections[id]);
434
434
  if (connection?.__error) throw connection.__error;
435
435
  if (!connection?.accessToken) {
436
- throw new Error(`尚未连接 ${PROVIDERS[id]?.name || id} 账户,请先运行 dgxay oauth connect ${id}`);
436
+ throw new Error(`${PROVIDERS[id]?.name || id} account is not connected. Run dgxay oauth connect ${id} first`);
437
437
  }
438
438
  if (connection.expiresAt && Date.now() > connection.expiresAt - 60_000) {
439
439
  if (!connection.refreshToken) {
440
- throw new Error(`${PROVIDERS[id].name} 的登录已过期,请重新运行 dgxay oauth connect ${id}`);
440
+ throw new Error(`The ${PROVIDERS[id].name} login has expired. Run dgxay oauth connect ${id} again`);
441
441
  }
442
442
  const refreshed = await refreshConnection(id, connection);
443
443
  return refreshed.accessToken;
@@ -512,13 +512,13 @@ function startLoopback(flow) {
512
512
  `<!doctype html><meta charset="utf-8"><title>DgxayCode</title>` +
513
513
  `<body style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;display:grid;place-items:center;height:100vh;margin:0;background:#f7f7f5;color:#1f2023">` +
514
514
  `<div style="text-align:center"><div style="width:44px;height:44px;border-radius:12px;background:#2563eb;display:inline-grid;place-items:center;color:#fff;font-size:22px">✓</div>` +
515
- `<h1 style="font-size:18px;margin:16px 0 6px">${ok ? '授权完成' : '授权未完成'}</h1>` +
516
- `<p style="font-size:13px;color:#6b6d72;margin:0">${ok ? '可以关闭此页面,回到终端。' : '请回到终端重试。'}</p></div></body>`
515
+ `<h1 style="font-size:18px;margin:16px 0 6px">${ok ? 'Authorization complete' : 'Authorization incomplete'}</h1>` +
516
+ `<p style="font-size:13px;color:#6b6d72;margin:0">${ok ? 'You can close this page and return to the terminal.' : 'Please return to the terminal and try again.'}</p></div></body>`
517
517
  );
518
518
 
519
519
  if (!ok) {
520
520
  const err = new Error(
521
- error ? `授权被拒绝:${error}` : state !== flow.state ? '授权回调 state 不匹配' : '授权回调缺少 code'
521
+ error ? `Authorization was denied: ${error}` : state !== flow.state ? 'Authorization callback state mismatch' : 'Authorization callback is missing the code'
522
522
  );
523
523
  failFlow(flow, err);
524
524
  return;
@@ -533,9 +533,9 @@ function startLoopback(flow) {
533
533
  server.once('error', (err) => {
534
534
  const hint =
535
535
  err.code === 'EADDRINUSE'
536
- ? `端口 ${flow.fixedPort || ''} 已被占用(常见原因:Codex CLI 或其他授权实例正在运行),请关闭后重试。`
536
+ ? `Port ${flow.fixedPort || ''} is already in use (often Codex CLI or another auth instance is running). Close it and try again.`
537
537
  : err.message;
538
- reject(new Error(`无法启动本地回调服务:${hint}`));
538
+ reject(new Error(`Cannot start the local callback server: ${hint}`));
539
539
  });
540
540
  const port = flow.fixedPort || 0;
541
541
  const host = flow.fixedPort ? 'localhost' : '127.0.0.1';
@@ -554,17 +554,17 @@ function startLoopback(flow) {
554
554
  async function begin(input = {}) {
555
555
  const id = String(input.provider || '');
556
556
  const def = PROVIDERS[id];
557
- if (!def) throw new Error('不支持的 OAuth 供应商');
557
+ if (!def) throw new Error('Unsupported OAuth provider');
558
558
 
559
559
  const store = readStore();
560
560
  const config = clientConfig(id, store);
561
- if (!config.clientId) throw new Error(`请先填写该平台的 OAuth Client ID:dgxay oauth client ${id} --client-id <ID>`);
561
+ if (!config.clientId) throw new Error(`Set this platform's OAuth Client ID first: dgxay oauth client ${id} --client-id <ID>`);
562
562
 
563
563
  for (const [label, url] of [
564
- ['授权端点', config.authorizeUrl],
565
- ['令牌端点', config.tokenUrl]
564
+ ['Authorization endpoint', config.authorizeUrl],
565
+ ['Token endpoint', config.tokenUrl]
566
566
  ]) {
567
- if (!/^https?:\/\//i.test(url || '')) throw new Error(`${label}必须是 http:// 或 https:// 地址`);
567
+ if (!/^https?:\/\//i.test(url || '')) throw new Error(`${label} must be an http:// or https:// URL`);
568
568
  }
569
569
 
570
570
  const verifier = base64url(crypto.randomBytes(32));
@@ -621,7 +621,7 @@ async function begin(input = {}) {
621
621
 
622
622
  flows.set(flowId, flow);
623
623
  flow.timer = setTimeout(() => {
624
- failFlow(flow, new Error('授权等待超时,请重新发起连接'));
624
+ failFlow(flow, new Error('Authorization timed out, please start the connection again'));
625
625
  }, FLOW_TIMEOUT);
626
626
 
627
627
  const opened = await openBrowser(authUrl.toString());
@@ -639,9 +639,9 @@ async function begin(input = {}) {
639
639
 
640
640
  async function finalizeFlow(flow, rawCode, state) {
641
641
  if (!flow || flow.settled) return null;
642
- if (state && flow.state && state !== flow.state) throw new Error('授权回调 state 不匹配,已中止');
642
+ if (state && flow.state && state !== flow.state) throw new Error('Authorization callback state mismatch, aborted');
643
643
  const code = String(rawCode || '').trim();
644
- if (!code) throw new Error('授权码为空');
644
+ if (!code) throw new Error('The authorization code is empty');
645
645
 
646
646
  const params = {
647
647
  grant_type: 'authorization_code',
@@ -689,13 +689,13 @@ function failFlow(flow, error) {
689
689
 
690
690
  function wait(id) {
691
691
  const flow = flows.get(String(id));
692
- if (!flow) throw new Error('授权会话不存在或已结束,请重新发起连接');
692
+ if (!flow) throw new Error('The authorization session does not exist or has ended. Start the connection again');
693
693
  return flow.promise;
694
694
  }
695
695
 
696
696
  async function complete(id, rawInput) {
697
697
  const flow = flows.get(String(id));
698
- if (!flow) throw new Error('授权会话不存在或已结束,请重新发起连接');
698
+ if (!flow) throw new Error('The authorization session does not exist or has ended. Start the connection again');
699
699
  let code = String(rawInput || '').trim();
700
700
  let state = '';
701
701
  if (code.includes('#')) {
@@ -711,14 +711,14 @@ async function complete(id, rawInput) {
711
711
  /* keep raw */
712
712
  }
713
713
  }
714
- if (!code) throw new Error('没有识别到授权码,请重新复制');
714
+ if (!code) throw new Error('No authorization code recognized. Copy it again');
715
715
  return finalizeFlow(flow, code, state);
716
716
  }
717
717
 
718
718
  function cancel(id) {
719
719
  const flow = flows.get(String(id));
720
720
  if (!flow) return false;
721
- failFlow(flow, new Error('已取消授权'));
721
+ failFlow(flow, new Error('Authorization cancelled'));
722
722
  return true;
723
723
  }
724
724