imtoagent 0.3.3 → 0.3.5

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 (39) hide show
  1. package/README.md +97 -97
  2. package/bin/imtoagent-real +96 -96
  3. package/bin/imtoagent.cjs +1 -1
  4. package/index.ts +106 -106
  5. package/modules/agent/claude-adapter.ts +6 -6
  6. package/modules/agent/claude.ts +6 -6
  7. package/modules/agent/codex-adapter.ts +13 -13
  8. package/modules/agent/codex-exec-server.ts +11 -11
  9. package/modules/agent/codex.ts +29 -29
  10. package/modules/agent/opencode-adapter.ts +17 -17
  11. package/modules/agent/opencode.ts +10 -10
  12. package/modules/capabilities.ts +33 -33
  13. package/modules/cli/setup.ts +171 -163
  14. package/modules/core/config.ts +5 -5
  15. package/modules/core/error.ts +8 -8
  16. package/modules/core/runtime.ts +10 -10
  17. package/modules/core/session.ts +4 -4
  18. package/modules/core/stats.ts +14 -14
  19. package/modules/core/types.ts +7 -7
  20. package/modules/im/feishu.ts +56 -56
  21. package/modules/im/telegram.ts +23 -23
  22. package/modules/im/wechat.ts +54 -54
  23. package/modules/im/wecom.ts +50 -50
  24. package/modules/media/feishu-inbound-adapter.ts +4 -4
  25. package/modules/media/resolver.ts +11 -11
  26. package/modules/media/telegram-inbound-adapter.ts +8 -8
  27. package/modules/prompt-builder.ts +12 -12
  28. package/modules/proxy/anthropic-proxy.ts +39 -28
  29. package/modules/proxy/codex-proxy.ts +18 -18
  30. package/modules/utils/backend-check.ts +12 -12
  31. package/modules/utils/paths.ts +8 -8
  32. package/package.json +1 -1
  33. package/scripts/postinstall.cjs +10 -10
  34. package/scripts/postinstall.ts +13 -13
  35. package/templates/soul.template/identity.md +5 -5
  36. package/templates/soul.template/profile.md +7 -7
  37. package/templates/soul.template/rules.md +5 -5
  38. package/templates/soul.template/skills.md +2 -2
  39. package/templates/soul.template/workspace.md +3 -3
@@ -1,12 +1,12 @@
1
1
  // ================================================================
2
- // setup.ts — 交互式配置向导(v2
2
+ // setup.ts — Interactive Setup Wizard (v2)
3
3
  // ================================================================
4
- // 交互方式:
5
- // ↑↓ 空格切换选项
6
- // 回车确认选择
7
- // ESC — 返回上一步
4
+ // Interaction:
5
+ // ↑↓ or Spacenavigate options
6
+ // Enterconfirm selection
7
+ // ESC — go back
8
8
  //
9
- // 支持的 IM 平台:飞书 / Telegram / 企业微信 / 个人微信
9
+ // Supported IM platforms: Feishu / Telegram / WeCom / WeChat
10
10
  // ================================================================
11
11
 
12
12
  import * as fs from 'fs';
@@ -17,7 +17,7 @@ import { randomUUID } from 'crypto';
17
17
  import { checkAllBackends, formatBackendStatus } from '../utils/backend-check';
18
18
 
19
19
  // ================================================================
20
- // 键盘输入(raw mode
20
+ // Keyboard input (raw mode)
21
21
  // ================================================================
22
22
 
23
23
  const KEY = {
@@ -29,7 +29,7 @@ const KEY = {
29
29
  BACKSPACE: '\x7f',
30
30
  };
31
31
 
32
- /** 读取单个按键 */
32
+ /** Read a single keypress */
33
33
  function readKey(): Promise<string> {
34
34
  return new Promise((resolve) => {
35
35
  const onData = (data: Buffer) => {
@@ -43,23 +43,22 @@ function readKey(): Promise<string> {
43
43
  }
44
44
 
45
45
  // ================================================================
46
- // 菜单选择(↑↓/空格 切换,回车确认)
46
+ // Menu selection (↑↓/Space navigate, Enter confirm)
47
47
  // ================================================================
48
48
 
49
49
  async function selectMenu(title: string, options: string[]): Promise<number> {
50
50
  let idx = 0;
51
- const linesAbove = options.length + 2;
52
51
 
53
52
  function render() {
54
- // 清除之前的输出
55
- process.stdout.write('\x1B[0G'); // 回到行首
53
+ // Clear previous output
54
+ process.stdout.write('\x1B[0G'); // Return to line start
56
55
  options.forEach((opt, i) => {
57
56
  const prefix = i === idx ? '▸ ' : ' ';
58
57
  process.stdout.write(`\x1B[0G${prefix}${opt}\x1B[0K\n`);
59
58
  });
60
59
  }
61
60
 
62
- // 显示标题
61
+ // Show title
63
62
  console.log(title);
64
63
  render();
65
64
 
@@ -71,15 +70,15 @@ async function selectMenu(title: string, options: string[]): Promise<number> {
71
70
  const key = await readKey();
72
71
 
73
72
  if (key === KEY.UP || key === KEY.DOWN || key === KEY.SPACE) {
74
- // 移动光标
73
+ // Move cursor
75
74
  idx = (idx + (key === KEY.UP ? -1 : 1) + options.length) % options.length;
76
- // 重绘所有选项
77
- process.stdout.write(`\x1B[${options.length}A`); // 上移 N
75
+ // Redraw all options
76
+ process.stdout.write(`\x1B[${options.length}A`); // Move up N lines
78
77
  render();
79
78
  } else if (key === KEY.ENTER) {
80
79
  break;
81
80
  } else if (key === KEY.ESC) {
82
- return -1; // ESC = 返回上一步
81
+ return -1; // ESC = go back
83
82
  }
84
83
  }
85
84
  } finally {
@@ -92,7 +91,7 @@ async function selectMenu(title: string, options: string[]): Promise<number> {
92
91
  }
93
92
 
94
93
  // ================================================================
95
- // 文本输入(回车确认,ESC 返回 -1
94
+ // Text input (Enter confirm, ESC returns -1)
96
95
  // ================================================================
97
96
 
98
97
  async function promptText(label: string, defaultValue = ''): Promise<string> {
@@ -111,19 +110,19 @@ async function promptText(label: string, defaultValue = ''): Promise<string> {
111
110
  break;
112
111
  } else if (key === KEY.ESC) {
113
112
  process.stdout.write('\x1B[0K\n');
114
- return -1 as unknown as string; // 特殊返回值表示 ESC
113
+ return -1 as unknown as string; // Special return value for ESC
115
114
  } else if (key === KEY.BACKSPACE) {
116
115
  if (buf.length > 0) {
117
116
  buf.pop();
118
- process.stdout.write('\x1B[1D \x1B[1D'); // 退格删除
117
+ process.stdout.write('\x1B[1D \x1B[1D'); // Backspace delete
119
118
  }
120
119
  } else if (key === KEY.UP || key === KEY.DOWN) {
121
- // 忽略方向键
120
+ // Ignore arrow keys
122
121
  } else if (key === KEY.SPACE) {
123
122
  buf.push(' ');
124
123
  process.stdout.write(' ');
125
124
  } else if (key.length >= 1 && !key.startsWith('\x1b')) {
126
- // 普通字符 / 粘贴的多字符块(不含转义序列的文本)
125
+ // Normal characters / pasted multi-char blocks (text without escape sequences)
127
126
  buf.push(key);
128
127
  process.stdout.write(key);
129
128
  }
@@ -139,7 +138,7 @@ async function promptText(label: string, defaultValue = ''): Promise<string> {
139
138
  }
140
139
 
141
140
  // ================================================================
142
- // 确认(Y/N,回车确认,ESC 返回 -1
141
+ // Confirmation (Y/N, Enter confirm, ESC returns -1)
143
142
  // ================================================================
144
143
 
145
144
  async function confirm(label: string, defaultYes = true): Promise<boolean | -1> {
@@ -172,7 +171,7 @@ async function confirm(label: string, defaultYes = true): Promise<boolean | -1>
172
171
  }
173
172
 
174
173
  // ================================================================
175
- // 供应商预设
174
+ // Provider presets
176
175
  // ================================================================
177
176
 
178
177
  interface ProviderPreset {
@@ -180,12 +179,12 @@ interface ProviderPreset {
180
179
  baseUrl: string;
181
180
  format: 'openai' | 'anthropic';
182
181
  models: string[];
183
- hint?: string; // 额外说明
182
+ hint?: string; // Additional note
184
183
  }
185
184
 
186
185
  const PROVIDER_PRESETS: ProviderPreset[] = [
187
186
  {
188
- name: 'DashScope(阿里百炼)',
187
+ name: 'DashScope (Alibaba Bailian)',
189
188
  baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
190
189
  format: 'openai',
191
190
  models: ['qwen-max', 'qwen-plus', 'qwen-turbo'],
@@ -197,7 +196,7 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
197
196
  models: ['deepseek-chat', 'deepseek-reasoner'],
198
197
  },
199
198
  {
200
- name: '智谱 AIZhipu',
199
+ name: 'Zhipu AI (Zhipu)',
201
200
  baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
202
201
  format: 'openai',
203
202
  models: ['glm-4-plus', 'glm-4-flash', 'glm-4'],
@@ -209,13 +208,13 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
209
208
  models: ['MiniMax-M2.5', 'MiniMax-M1'],
210
209
  },
211
210
  {
212
- name: '硅基流动(SiliconFlow',
211
+ name: 'SiliconFlow',
213
212
  baseUrl: 'https://api.siliconflow.cn/v1',
214
213
  format: 'openai',
215
214
  models: ['Qwen/Qwen2.5-72B-Instruct', 'deepseek-ai/DeepSeek-V3'],
216
215
  },
217
216
  {
218
- name: 'Moonshot(月之暗面)',
217
+ name: 'Moonshot (Moonshot AI)',
219
218
  baseUrl: 'https://api.moonshot.cn/v1',
220
219
  format: 'openai',
221
220
  models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'],
@@ -225,31 +224,31 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
225
224
  baseUrl: 'https://api.openai.com/v1',
226
225
  format: 'openai',
227
226
  models: ['gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'],
228
- hint: '需要代理才能访问',
227
+ hint: 'Proxy required to access',
229
228
  },
230
229
  {
231
230
  name: 'Anthropic',
232
231
  baseUrl: 'https://api.anthropic.com',
233
232
  format: 'anthropic',
234
233
  models: ['claude-sonnet-4-20250514', 'claude-haiku-4-20250514', 'claude-opus-4-20250514'],
235
- hint: '需要代理才能访问',
234
+ hint: 'Proxy required to access',
236
235
  },
237
236
  {
238
- name: 'GeminiGoogle',
237
+ name: 'Gemini (Google)',
239
238
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
240
239
  format: 'openai',
241
240
  models: ['gemini-2.5-pro', 'gemini-2.5-flash'],
242
- hint: '需要代理才能访问',
241
+ hint: 'Proxy required to access',
243
242
  },
244
243
  {
245
- name: 'xAIGrok',
244
+ name: 'xAI (Grok)',
246
245
  baseUrl: 'https://api.x.ai/v1',
247
246
  format: 'openai',
248
247
  models: ['grok-3', 'grok-3-mini'],
249
- hint: '需要代理才能访问',
248
+ hint: 'Requires proxy to access',
250
249
  },
251
250
  {
252
- name: 'Ollama(本地)',
251
+ name: 'Ollama (Local)',
253
252
  baseUrl: 'http://localhost:11434/v1',
254
253
  format: 'openai',
255
254
  models: ['qwen2.5', 'llama3.2', 'deepseek-r1'],
@@ -257,38 +256,38 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
257
256
  ];
258
257
 
259
258
  // ================================================================
260
- // IM 平台配置定义
259
+ // IM platform configuration
261
260
  // ================================================================
262
261
 
263
262
  const IM_PLATFORMS = [
264
- { value: 'feishu', label: '飞书', desc: 'WebSocket 长连接' },
265
- { value: 'telegram', label: 'Telegram', desc: '长轮询' },
266
- { value: 'wecom', label: '企业微信', desc: '扫码绑定 + WebSocket' },
267
- { value: 'wechat', label: '个人微信', desc: 'iLink + QR 扫码' },
263
+ { value: 'feishu', label: 'Feishu', desc: 'WebSocket long-lived connection' },
264
+ { value: 'telegram', label: 'Telegram', desc: 'Long polling' },
265
+ { value: 'wecom', label: 'WeCom', desc: 'QR scan binding + WebSocket' },
266
+ { value: 'wechat', label: 'WeChat', desc: 'iLink + QR scan' },
268
267
  ];
269
268
 
270
- /** 每种 IM 需要的凭证字段 */
269
+ /** Credential fields required by each IM type */
271
270
  const IM_FIELDS: Record<string, { key: string; label: string; required: boolean }[]> = {
272
271
  feishu: [
273
- { key: 'appId', label: '飞书 App ID (cli_...)', required: true },
274
- { key: 'appSecret', label: '飞书 App Secret', required: true },
272
+ { key: 'appId', label: 'Feishu App ID (cli_...)', required: true },
273
+ { key: 'appSecret', label: 'Feishu App Secret', required: true },
275
274
  ],
276
275
  telegram: [
277
276
  { key: 'appId', label: 'Bot Token', required: true },
278
- { key: 'proxy', label: '代理地址(留空不使用)', required: false },
277
+ { key: 'proxy', label: 'Proxy URL (leave blank to skip)', required: false },
279
278
  ],
280
279
  wecom: [
281
- // 企业微信使用扫码绑定,无需预填凭证,启动时自动触发 QR 扫码
280
+ // WeCom uses QR scan binding, no pre-filled credentials needed, QR scan triggered automatically on startup
282
281
  ],
283
282
  wechat: [
284
- // 微信通过 iLink + QR 扫码认证,无需预填凭证
285
- { key: 'botId', label: 'iLink Bot ID(留空使用 QR 扫码)', required: false },
286
- { key: 'botToken', label: 'iLink Bot Token(留空使用 QR 扫码)', required: false },
283
+ // WeChat authenticates via iLink + QR scan, no pre-filled credentials needed
284
+ { key: 'botId', label: 'iLink Bot ID (leave blank for QR scan)', required: false },
285
+ { key: 'botToken', label: 'iLink Bot Token (leave blank for QR scan)', required: false },
287
286
  ],
288
287
  };
289
288
 
290
289
  // ================================================================
291
- // 主流程
290
+ // Main flow
292
291
  // ================================================================
293
292
 
294
293
  export async function runSetupWizard(): Promise<void> {
@@ -296,123 +295,123 @@ export async function runSetupWizard(): Promise<void> {
296
295
  const configPath = path.join(dataDir, 'config.json');
297
296
 
298
297
  console.log('\n╔══════════════════════════════════════════════╗');
299
- console.log('║ 🚀 imtoagent 配置向导 ║');
298
+ console.log('║ 🚀 imtoagent Setup Wizard ║');
300
299
  console.log('╚══════════════════════════════════════════════╝');
301
- console.log(`\n数据目录: ${dataDir}`);
302
- console.log(`操作提示: ↑↓/空格 切换 | 回车确认 | ESC 返回\n`);
300
+ console.log(`\nData directory: ${dataDir}`);
301
+ console.log(`Controls: ↑↓/Space navigate | Enter confirm | ESC go back\n`);
303
302
 
304
- // ===== Step 1: 检测已有配置 =====
303
+ // ===== Step 1: Detect existing configuration =====
305
304
  let existingConfig: any = null;
306
305
  let mergeMode = false;
307
306
 
308
307
  if (fs.existsSync(configPath)) {
309
- console.log('📋 检测到已有配置\n');
310
- const idx = await selectMenu('选择操作', ['覆盖现有配置', '合并(保留现有 Bot)', '退出']);
308
+ console.log('📋 Existing configuration detected\n');
309
+ const idx = await selectMenu('Choose action', ['Overwrite existing config', 'Merge (keep existing Bots)', 'Exit']);
311
310
  if (idx === -1) return; // ESC
312
- if (idx === 2) { console.log('👋 已取消'); process.exit(0); }
311
+ if (idx === 2) { console.log('👋 Cancelled'); process.exit(0); }
313
312
  if (idx === 1) mergeMode = true;
314
313
 
315
314
  try {
316
315
  existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
317
316
  } catch {
318
- console.log('⚠️ 现有配置文件解析失败,将重新生成');
317
+ console.log('⚠️ Failed to parse existing config, will regenerate');
319
318
  existingConfig = null;
320
319
  mergeMode = false;
321
320
  }
322
321
  }
323
322
 
324
- // ===== Step 2: 检测后端 =====
325
- console.log('\n📌 检测后端 Agent...\n');
323
+ // ===== Step 2: Detect backends =====
324
+ console.log('\n📌 Detecting backend agents...\n');
326
325
  const backendStatus = checkAllBackends();
327
326
  console.log(formatBackendStatus(backendStatus));
328
327
 
329
328
  const installedBackends = backendStatus.filter(b => b.installed);
330
329
  if (installedBackends.length === 0) {
331
- console.log('\n⚠️ 未检测到任何后端 Agent。');
332
- console.log('推荐安装:');
330
+ console.log('\n⚠️ No backend agents detected.');
331
+ console.log('Recommended installs:');
333
332
  console.log(' npm install -g @anthropic-ai/claude-agent-sdk # Claude Code');
334
333
  console.log(' npm install -g @openai/codex # Codex');
335
334
  console.log(' npm install -g opencode # OpenCode');
336
- const r = await confirm('是否继续配置?(启动后发消息会报错,直到后端安装)', false);
337
- if (r === false || r === -1) { console.log('\n👋 安装后端后请重新运行 "imtoagent setup"'); process.exit(0); }
338
- console.log('\n⚠️ 已跳过,你可以稍后安装后端。\n');
335
+ const r = await confirm('Continue configuring? (Messaging will fail until backends are installed)', false);
336
+ if (r === false || r === -1) { console.log('\n👋 Run "imtoagent setup" again after installing backends'); process.exit(0); }
337
+ console.log('\n⚠️ Skipped, you can install backends later.\n');
339
338
  } else {
340
- console.log(`\n✅ 已安装: ${installedBackends.map(b => b.label).join(', ')}\n`);
339
+ console.log(`\n✅ Installed: ${installedBackends.map(b => b.label).join(', ')}\n`);
341
340
  }
342
341
 
343
- // ===== Step 3: 配置 Bot =====
344
- console.log('📌 Step 3: 配置 Bot\n');
342
+ // ===== Step 3: Configure Bots =====
343
+ console.log('📌 Step 3: Configure Bots\n');
345
344
 
346
345
  const bots: any[] = (mergeMode && existingConfig?.bots) ? [...existingConfig.bots] : [];
347
346
 
348
347
  let addingBots = true;
349
348
  while (addingBots) {
350
- console.log(`\n--- 添加新 Bot (已有 ${bots.length} ) ---\n`);
349
+ console.log(`\n--- Add New Bot (${bots.length} existing) ---\n`);
351
350
 
352
- // 3a: 选择 IM 平台
351
+ // 3a: Select IM platform
353
352
  const imLabels = IM_PLATFORMS.map(p => `${p.label} ${p.desc}`);
354
- const imIdx = await selectMenu('选择 IM 平台', imLabels);
353
+ const imIdx = await selectMenu('Select IM platform', imLabels);
355
354
  if (imIdx === -1) { if (bots.length === 0) return; break; } // ESC
356
355
  const imType = IM_PLATFORMS[imIdx].value;
357
356
 
358
- // 3b: 自动生成 Bot 名称,可自定义
357
+ // 3b: Auto-generate Bot name, customizable
359
358
  const defaultName = IM_PLATFORMS[imIdx].label + 'Bot';
360
- const nameInput = await promptText('Bot 名称', defaultName);
359
+ const nameInput = await promptText('Bot name', defaultName);
361
360
  if ((nameInput as any) === -1) { if (bots.length === 0) return; break; } // ESC
362
- const botName = nameInput || defaultName; // 留空用默认
361
+ const botName = nameInput || defaultName; // Use default if empty
363
362
 
364
- // 3c: 选择后端
363
+ // 3c: Select backend
365
364
  const backendLabels = backendStatus.map(b =>
366
- b.installed ? `${b.label} (v${b.version})` : `${b.label} ⚠️ 未安装`
365
+ b.installed ? `${b.label} (v${b.version})` : `${b.label} ⚠️ Not installed`
367
366
  );
368
- const backendIdx = await selectMenu('选择后端', backendLabels);
369
- if (backendIdx === -1) continue; // ESC 返回重新选 IM
367
+ const backendIdx = await selectMenu('Select backend', backendLabels);
368
+ if (backendIdx === -1) continue; // ESC go back to IM selection
370
369
  const backend = backendStatus[backendIdx].type;
371
370
  const isBackendInstalled = backendStatus[backendIdx].installed;
372
371
 
373
- // 后端未安装提示自动安装
372
+ // Backend not installed prompt for auto-install
374
373
  if (!isBackendInstalled) {
375
374
  const installCmd = backendStatus[backendIdx].installHint || '';
376
- console.log(`\n⚠️ ${backend} 未安装`);
375
+ console.log(`\n⚠️ ${backend} not installed`);
377
376
  console.log(` ${installCmd}\n`);
378
- const r = await confirm('是否现在自动安装?');
377
+ const r = await confirm('Auto-install now?');
379
378
  if (r === true) {
380
379
  const { installBackend } = await import('../utils/backend-check');
381
380
  const ok = await installBackend(backend as 'claude' | 'codex' | 'opencode');
382
381
  if (ok) {
383
- console.log(`✅ ${backend} 已安装\n`);
382
+ console.log(`✅ ${backend} installed\n`);
384
383
  } else {
385
- console.log(`⚠️ 安装失败,可稍后手动运行: ${installCmd}\n`);
386
- const r2 = await confirm('仍要继续配置此 Bot?');
384
+ console.log(`⚠️ Installation failed, run manually later: ${installCmd}\n`);
385
+ const r2 = await confirm('Continue configuring this Bot anyway?');
387
386
  if (r2 === false) continue;
388
387
  }
389
388
  } else if (r === -1) {
390
- continue; // ESC 返回
389
+ continue; // ESC go back
391
390
  } else {
392
- console.log('跳过安装\n');
391
+ console.log('Skipping installation\n');
393
392
  }
394
393
  }
395
394
 
396
- // 3d: 根据 IM 类型收集凭证
397
- console.log(`\n--- ${IM_PLATFORMS.find(p => p.value === imType)?.label} 凭证 ---`);
395
+ // 3d: Collect credentials based on IM type
396
+ console.log(`\n--- ${IM_PLATFORMS.find(p => p.value === imType)?.label} credentials ---`);
398
397
  const fields = IM_FIELDS[imType] || [];
399
398
  const credentials: Record<string, string> = {};
400
399
 
401
400
  for (const field of fields) {
402
- const val = await promptText(field.label + (field.required ? '' : '(可选)'));
401
+ const val = await promptText(field.label + (field.required ? '' : ' (optional)'));
403
402
  if ((val as any) === -1) { credentials._escaped = 'true'; break; } // ESC
404
403
  credentials[field.key] = val;
405
404
  }
406
- if (credentials._escaped) continue; // ESC 返回重新选后端
405
+ if (credentials._escaped) continue; // ESC go back and re-select backend
407
406
 
408
- // 3e: 工作目录
409
- const cwd = await promptText('工作目录', os.homedir());
407
+ // 3e: Working directory
408
+ const cwd = await promptText('Working directory', os.homedir());
410
409
  if ((cwd as any) === -1) continue;
411
410
 
412
- // 生成唯一 IDUUID,用于目录隔离,改名不影响)
411
+ // Generate unique ID (UUID, for directory isolation, renaming doesn't affect it)
413
412
  const botId = randomUUID();
414
413
 
415
- // 构建 Bot 配置(不同 IM 需要的字段不同)
414
+ // Build Bot configuration (different IM types need different fields)
416
415
  const bot: any = {
417
416
  id: botId,
418
417
  name: botName,
@@ -420,141 +419,147 @@ export async function runSetupWizard(): Promise<void> {
420
419
  cwd: cwd || os.homedir(),
421
420
  };
422
421
 
423
- // 飞书需要 appId + appSecret
422
+ // Feishu needs appId + appSecret
424
423
  if (imType === 'feishu') {
425
424
  bot.appId = credentials.appId || '';
426
425
  bot.appSecret = credentials.appSecret || '';
427
426
  }
428
- // Telegram 需要 appIdBot Token),可选 proxy
427
+ // Telegram needs appId (Bot Token), optional proxy
429
428
  else if (imType === 'telegram') {
430
429
  bot.appId = credentials.appId || '';
431
430
  if (credentials.proxy) bot.proxy = credentials.proxy;
432
431
  }
433
- // 企业微信:扫码绑定,无需预填凭证(可选 botId/secret
432
+ // WeCom: QR scan binding, no pre-filled credentials (optional botId/secret)
434
433
  else if (imType === 'wecom') {
435
434
  bot.im = 'wecom';
436
435
  if (credentials.botId) bot.botId = credentials.botId;
437
436
  if (credentials.secret) bot.secret = credentials.secret;
438
437
  }
439
- // 个人微信:可选 botId/botToken,留空则 QR 扫码
438
+ // WeChat: optional botId/botToken, QR scan if left blank
440
439
  else if (imType === 'wechat') {
441
440
  bot.im = 'wechat';
442
441
  if (credentials.botId) bot.botId = credentials.botId;
443
442
  if (credentials.botToken) bot.botToken = credentials.botToken;
444
443
  if (credentials.ilinkUserId) bot.ilinkUserId = credentials.ilinkUserId;
445
444
  }
446
- // 默认:非飞书平台加 im 字段
445
+ // Default: non-Feishu platforms add im field
447
446
  else {
448
447
  bot.im = imType;
449
448
  }
450
449
 
451
- // 检查重名
450
+ // Check for duplicate name
452
451
  const existingIdx = bots.findIndex(b => b.name === botName);
453
452
  if (existingIdx >= 0) {
454
453
  bots[existingIdx] = bot;
455
- console.log(`✅ 已替换: ${botName}`);
454
+ console.log(`✅ Replaced: ${botName}`);
456
455
  } else {
457
456
  bots.push(bot);
458
- console.log(`✅ 已添加: ${botName}`);
457
+ console.log(`✅ Added: ${botName}`);
459
458
  }
460
459
 
461
- // 是否继续添加
462
- const r = await confirm('继续添加其他 Bot?', true);
463
- if (r === -1) addingBots = false; // ESC = 不添加了,进入下一步
460
+ // Whether to continue adding
461
+ const r = await confirm('Add another Bot?', true);
462
+ if (r === -1) addingBots = false; // ESC = done adding, proceed to next step
464
463
  else addingBots = (r === true);
465
464
  }
466
465
 
467
466
  if (bots.length === 0) {
468
- console.log('\n⚠️ 未配置任何 Bot。');
469
- const r = await confirm('至少配置一个 Bot 吗?');
467
+ console.log('\n⚠️ No Bots configured.');
468
+ const r = await confirm('Configure at least one Bot?');
470
469
  if (r === true) return runSetupWizard();
471
- console.log('\n⚠️ 至少需要一个 Bot,配置已取消');
470
+ console.log('\n⚠️ At least one Bot required, configuration cancelled');
472
471
  return;
473
472
  }
474
473
 
475
- // ===== Step 4: 配置模型供应商 =====
476
- console.log('\n📌 Step 4: 配置模型供应商\n');
474
+ // ===== Step 4: Configure model providers =====
475
+ console.log('\n📌 Step 4: Configure model providers\n');
477
476
 
478
477
  const providers: Record<string, any> = {};
479
478
  if (mergeMode && existingConfig?.providers) {
480
479
  Object.assign(providers, existingConfig.providers);
481
- console.log(`✅ 已保留 ${Object.keys(providers).length} 个现有供应商\n`);
480
+ console.log(`✅ Kept ${Object.keys(providers).length} existing provider(s)\n`);
482
481
  }
483
482
 
484
- let addingProviders = true;
485
- while (addingProviders) {
486
- console.log('--- 添加新供应商 ---\n');
483
+ // Step 4 outer loop: ensure at least one provider (exit when user explicitly skips)
484
+ let step4Loop = true;
485
+ while (step4Loop) {
486
+ let addingProviders = true;
487
+ while (addingProviders) {
488
+ console.log('--- Add new provider ---\n');
487
489
 
488
- // 选择预设 or 自定义
490
+ // Choose preset or custom
489
491
  const presetOptions = PROVIDER_PRESETS.map(p => {
490
492
  const tag = p.hint ? ` ${p.hint}` : '';
491
493
  return `${p.name}${tag}`;
492
494
  });
493
- presetOptions.push('自定义...');
495
+ presetOptions.push('Custom...');
494
496
 
495
- const presetIdx = await selectMenu('选择供应商', presetOptions);
497
+ const presetIdx = await selectMenu('Select provider', presetOptions);
496
498
  if (presetIdx === -1) { addingProviders = false; continue; }
497
499
 
498
500
  let provName: string, baseUrl: string, format: 'openai' | 'anthropic', models: string[];
499
501
 
500
502
  if (presetIdx < PROVIDER_PRESETS.length) {
501
- // 使用预设
503
+ // Use preset
502
504
  const preset = PROVIDER_PRESETS[presetIdx];
503
- provName = preset.name.split('')[0].trim().toLowerCase(); // 取简短名称
505
+ provName = preset.name.split('(')[0].trim().toLowerCase(); // Take short name
504
506
  baseUrl = preset.baseUrl;
505
507
  format = preset.format;
506
508
  models = [...preset.models];
507
509
 
508
- console.log(`\n✅ 预设已加载:`);
509
- console.log(` 名称: ${provName}`);
510
+ console.log(`\n✅ Preset loaded:`);
511
+ console.log(` Name: ${provName}`);
510
512
  console.log(` URL: ${preset.baseUrl}`);
511
- console.log(` 格式: ${preset.format}`);
512
- console.log(` 模型: ${preset.models.join(', ')}\n`);
513
+ console.log(` Format: ${preset.format}`);
514
+ console.log(` Models: ${preset.models.join(', ')}\n`);
513
515
 
514
- // 确认/修改简短名称
515
- const nameEdit = await promptText('供应商名称(留空确认)', provName);
516
+ // Confirm/edit short name
517
+ const nameEdit = await promptText('Provider name (leave blank to confirm)', provName);
516
518
  if ((nameEdit as any) === -1) continue;
517
519
  provName = nameEdit || provName;
518
520
 
519
- // 确认/修改 Base URL
521
+ // Confirm/edit Base URL
520
522
  const urlEdit = await promptText('Base URL', baseUrl);
521
523
  if ((urlEdit as any) === -1) continue;
522
524
  baseUrl = urlEdit || baseUrl;
523
525
 
524
- // 确认/修改模型列表
525
- const modelsEdit = await promptText('模型列表(逗号分隔)', models.join(', '));
526
+ // Confirm/edit model list
527
+ const modelsEdit = await promptText('Model list (comma-separated)', models.join(', '));
526
528
  if ((modelsEdit as any) === -1) continue;
527
529
  if (modelsEdit) models = modelsEdit.split(',').map(s => s.trim()).filter(Boolean);
528
530
 
529
531
  if (providers[provName]) {
530
- console.log(`⚠️ 供应商 "${provName}" 已存在,将覆盖\n`);
532
+ console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
531
533
  }
532
534
  } else {
533
- // 自定义
534
- provName = await promptText('供应商名称 ( deepseek, dashscope)');
535
+ // Custom
536
+ provName = await promptText('Provider name (e.g. deepseek, dashscope)');
535
537
  if ((provName as any) === -1) { addingProviders = false; continue; }
536
538
  if (!provName) { addingProviders = false; continue; }
537
539
  if (providers[provName]) {
538
- console.log(`⚠️ 供应商 "${provName}" 已存在,将覆盖\n`);
540
+ console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
539
541
  }
540
542
 
541
- baseUrl = await promptText('Base URL ( https://api.deepseek.com/v1)');
543
+ baseUrl = await promptText('Base URL (e.g. https://api.deepseek.com/v1)');
542
544
  if ((baseUrl as any) === -1) continue;
543
- const modelsStr = await promptText('模型列表 (逗号分隔)');
545
+ const modelsStr = await promptText('Model list (comma-separated)');
544
546
  if ((modelsStr as any) === -1) continue;
545
547
  models = (modelsStr || '').split(',').map(s => s.trim()).filter(Boolean);
546
548
 
547
- const formatIdx = await selectMenu('API 格式', ['openai', 'anthropic']);
549
+ const formatIdx = await selectMenu('API format', ['openai', 'anthropic']);
548
550
  if (formatIdx === -1) continue;
549
551
  format = ['openai', 'anthropic'][formatIdx];
550
552
  }
551
553
 
552
- // API Key(所有路径都需要)
554
+ // API Key (required for all providers)
553
555
  const apiKey = await promptText('API Key');
554
556
  if ((apiKey as any) === -1) continue;
557
+ if (!apiKey) {
558
+ console.log('⚠️ API Key is empty, this provider will be temporarily unavailable\n');
559
+ }
555
560
 
556
- // 价格(可选)
557
- const priceInput = await promptText('价格 (入/出 每百万 Token,如 0.55,2.19,留空跳过)');
561
+ // Pricing (optional)
562
+ const priceInput = await promptText('Pricing (in/out per million tokens, e.g. 0.55,2.19, leave blank to skip)');
558
563
  if ((priceInput as any) === -1) continue;
559
564
 
560
565
  const pricing: any = {};
@@ -568,21 +573,24 @@ export async function runSetupWizard(): Promise<void> {
568
573
  }
569
574
 
570
575
  providers[provName] = { baseUrl, apiKey, models, format, ...(Object.keys(pricing).length ? { pricing } : {}) };
571
- console.log(`✅ 已添加: ${provName}\n`);
576
+ console.log(`✅ Added: ${provName}\n`);
572
577
 
573
- const r = await confirm('继续添加供应商?', false);
578
+ const r = await confirm('Continue adding providers?', false);
574
579
  if (r === -1) addingProviders = false;
575
580
  else addingProviders = (r === true);
576
581
  }
577
582
 
578
583
  if (Object.keys(providers).length === 0) {
579
- console.log('\n⚠️ 未配置任何供应商。');
580
- const r = await confirm('至少配置一个供应商吗?');
581
- if (r === true) { addingProviders = true; }
584
+ console.log('\n⚠️ No providers configured.');
585
+ const r = await confirm('Configure at least one provider?');
586
+ if (r === true) continue; // Re-enter step4Loop
587
+ if (r === -1) { console.log('\n⚠️ Skipped, you can configure this later.\n'); }
582
588
  }
589
+ step4Loop = false; // Has providers or user explicitly skipped
590
+ }
583
591
 
584
- // ===== Step 5: 选择默认模型 =====
585
- console.log('\n📌 Step 5: 选择默认模型\n');
592
+ // ===== Step 5: Select default model =====
593
+ console.log('\n📌 Step 5: Select default model\n');
586
594
 
587
595
  const allModels: string[] = [];
588
596
  for (const [provName, prov] of Object.entries(providers)) {
@@ -594,24 +602,24 @@ export async function runSetupWizard(): Promise<void> {
594
602
  let defaultModel = '';
595
603
  if (allModels.length > 0) {
596
604
  const existingDefault = existingConfig?.defaultModel || allModels[0];
597
- const val = await promptText('默认模型', existingDefault);
605
+ const val = await promptText('Default model', existingDefault);
598
606
  defaultModel = (val as any) === -1 ? existingDefault : (val || existingDefault);
599
607
  } else {
600
- defaultModel = await promptText('默认模型 (供应商/模型名)') || 'deepseek/deepseek-v4-pro';
608
+ defaultModel = await promptText('Default model (provider/model)') || 'deepseek/deepseek-v4-pro';
601
609
  if ((defaultModel as any) === -1) defaultModel = 'deepseek/deepseek-v4-pro';
602
610
  }
603
611
 
604
- // ===== Step 6: 生成灵魂文件 =====
605
- console.log('\n📌 Step 6: 生成灵魂文件\n');
612
+ // ===== Step 6: Generate soul files =====
613
+ console.log('\n📌 Step 6: Generate soul files\n');
606
614
 
607
615
  for (const bot of bots) {
608
616
  const botSoulDir = getSoulDir(getBotKey(bot));
609
617
  const templateSoulDir = path.join(getPkgDir(), 'templates', 'soul.template');
610
618
 
611
619
  if (fs.existsSync(botSoulDir)) {
612
- const r = await confirm(`已存在 ${bot.name} 的灵魂文件,重新生成?`, false);
620
+ const r = await confirm(`Soul files for ${bot.name} already exist, regenerate?`, false);
613
621
  if (r === -1 || r === false) {
614
- console.log(`⏭ 跳过: ${bot.name}`);
622
+ console.log(`⏭ Skipped: ${bot.name}`);
615
623
  continue;
616
624
  }
617
625
  }
@@ -633,11 +641,11 @@ export async function runSetupWizard(): Promise<void> {
633
641
  fs.writeFileSync(destPath, content);
634
642
  }
635
643
  }
636
- console.log(`✅ ${bot.name}: 灵魂文件 → ${botSoulDir}`);
644
+ console.log(`✅ ${bot.name}: soul files → ${botSoulDir}`);
637
645
  }
638
646
 
639
- // ===== Step 7: 写入配置文件 =====
640
- console.log('\n📌 Step 7: 写入配置文件\n');
647
+ // ===== Step 7: Write configuration files =====
648
+ console.log('\n📌 Step 7: Write configuration files\n');
641
649
 
642
650
  fs.mkdirSync(dataDir, { recursive: true });
643
651
 
@@ -692,22 +700,22 @@ export async function runSetupWizard(): Promise<void> {
692
700
 
693
701
  fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
694
702
  fs.mkdirSync(path.join(dataDir, 'logs'), { recursive: true });
695
- console.log('✅ 子目录已创建 (sessions/, logs/)');
703
+ console.log('✅ Sub-directories created (sessions/, logs/)');
696
704
 
697
- // ===== 完成 =====
705
+ // ===== Done =====
698
706
  console.log('\n╔══════════════════════════════════════════════╗');
699
- console.log('║ ✅ 配置完成! ║');
707
+ console.log('║ ✅ Configuration complete! ║');
700
708
  console.log('╚══════════════════════════════════════════════╝\n');
701
709
  console.log(`Bot: ${bots.map(b => b.name).join(', ')}`);
702
- console.log(`默认模型: ${defaultModel}`);
703
- console.log(`供应商: ${Object.keys(providers).join(', ') || ''}`);
704
- console.log(`\n下一步:`);
705
- console.log(` imtoagent start 启动网关`);
706
- console.log(` imtoagent status 查看状态\n`);
710
+ console.log(`Default model: ${defaultModel}`);
711
+ console.log(`Providers: ${Object.keys(providers).join(', ') || 'None'}`);
712
+ console.log(`\nNext steps:`);
713
+ console.log(` imtoagent start Start the gateway`);
714
+ console.log(` imtoagent status Check status\n`);
707
715
  }
708
716
 
709
717
  // ================================================================
710
- // 工具函数
718
+ // Utility functions
711
719
  // ================================================================
712
720
 
713
721
  function buildDefaultAliases(defaultModel: string): Record<string, string> {