@wu529778790/open-im 1.11.7 → 1.11.8-beta.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/dist/setup.d.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  /**
2
- * 首次运行时的交互式配置引导(支持增量:保留已有平台配置)
3
- * 使用 prompts 库,兼容 tsx watch、IDE 终端等环境
4
- * Telegram Token 使用 readline 避免 Windows 终端 prompts 重绘问题
2
+ * 首次运行配置引导
3
+ *
4
+ * Web 向导已替代大部分 CLI 交互配置。
5
+ * 本文件仅保留:
6
+ * 1. runInteractiveSetup — 启动 Web 配置页面
7
+ * 2. runClaudeApiSetup — Claude API 凭证配置(写入 ~/.claude/settings.json)
5
8
  */
6
9
  /**
7
10
  * Claude API 专用配置向导,保存到 ~/.claude/settings.json(与 Claude Code 共用)
8
11
  */
9
12
  export declare function runClaudeApiSetup(): Promise<boolean>;
13
+ /**
14
+ * 交互式配置 — 现在直接引导到 Web 控制台
15
+ */
10
16
  export declare function runInteractiveSetup(): Promise<boolean>;
package/dist/setup.js CHANGED
@@ -1,228 +1,106 @@
1
1
  /**
2
- * 首次运行时的交互式配置引导(支持增量:保留已有平台配置)
3
- * 使用 prompts 库,兼容 tsx watch、IDE 终端等环境
4
- * Telegram Token 使用 readline 避免 Windows 终端 prompts 重绘问题
2
+ * 首次运行配置引导
3
+ *
4
+ * Web 向导已替代大部分 CLI 交互配置。
5
+ * 本文件仅保留:
6
+ * 1. runInteractiveSetup — 启动 Web 配置页面
7
+ * 2. runClaudeApiSetup — Claude API 凭证配置(写入 ~/.claude/settings.json)
5
8
  */
6
- import prompts from "prompts";
7
- import { createInterface } from "node:readline";
8
- import { mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from "node:fs";
9
- import { join, dirname } from "node:path";
10
- import { homedir } from "node:os";
11
- import { APP_HOME } from "./constants.js";
12
- import { loadConfig, getPlatformsWithCredentials } from "./config.js";
13
- import { normalizeAiCommand } from "./adapters/tool-registry.js";
14
- function loadExistingConfig() {
15
- const configPath = join(APP_HOME, "config.json");
16
- if (!existsSync(configPath))
17
- return null;
18
- try {
19
- return JSON.parse(readFileSync(configPath, "utf-8"));
20
- }
21
- catch {
22
- return null;
23
- }
24
- }
25
- function getConfiguredPlatforms(existing) {
26
- if (!existing?.platforms)
27
- return [];
28
- const names = [
29
- { k: "telegram", label: "Telegram" },
30
- { k: "qq", label: "QQ" },
31
- { k: "feishu", label: "飞书" },
32
- { k: "wework", label: "企业微信" },
33
- { k: "dingtalk", label: "钉钉" },
34
- { k: "workbuddy", label: "WorkBuddy (微信)" },
35
- { k: "clawbot", label: "ClawBot (微信 iLink)" },
36
- ];
37
- return names
38
- .filter(({ k }) => {
39
- const p = existing.platforms?.[k];
40
- if (!p)
41
- return false;
42
- if (k === "telegram")
43
- return !!p.botToken;
44
- if (k === "feishu")
45
- return !!(p.appId && p.appSecret);
46
- if (k === "qq")
47
- return !!(p.appId && p.secret);
48
- if (k === "workbuddy")
49
- return !!(p.accessToken && p.refreshToken);
50
- if (k === "wework")
51
- return !!(p.corpId && p.secret);
52
- if (k === "dingtalk")
53
- return !!(p.clientId && p.clientSecret);
54
- if (k === "clawbot")
55
- return !!p.apiToken;
56
- return false;
57
- })
58
- .map(({ label }) => label);
59
- }
60
- function defaultPlatformAi(v) {
61
- return normalizeAiCommand(v, "claude");
62
- }
63
- function question(prompt) {
64
- const rl = createInterface({ input: process.stdin, output: process.stdout });
65
- return new Promise((resolve) => {
66
- rl.question(prompt, (answer) => {
67
- rl.close();
68
- resolve(answer.trim());
69
- });
70
- });
71
- }
72
- function printManualInstructions(configPath) {
73
- console.log("\n━━━ open-im 首次配置 ━━━\n");
74
- console.log("当前环境不支持交互输入,请手动创建配置文件:");
75
- console.log("");
76
- console.log(" 1. 创建目录:", dirname(configPath));
77
- console.log(" 2. 创建文件:", configPath);
78
- console.log(" 3. 填入以下内容(替换为你的 Token/App ID 和用户 ID):");
79
- console.log("");
80
- console.log(`{
81
- "tools": {
82
- "claude": {
83
- "cliPath": "claude",
84
- "workDir": "${process.cwd().replace(/\\/g, "/")}"
85
- },
86
- "codex": { "cliPath": "codex", "workDir": "${process.cwd().replace(/\\/g, "/")}", "proxy": "http://127.0.0.1:7890" },
87
- "codebuddy": { "cliPath": "codebuddy" }
88
- },
89
- "platforms": {
90
- "telegram": {
91
- "enabled": true,
92
- "aiCommand": "claude",
93
- "botToken": "你的 Telegram Bot Token(可选)",
94
- "allowedUserIds": ["允许访问的 Telegram 用户 ID(可选)"]
95
- },
96
- "feishu": {
97
- "enabled": false,
98
- "aiCommand": "claude",
99
- "appId": "你的飞书 App ID(可选)",
100
- "appSecret": "你的飞书 App Secret(可选)",
101
- "allowedUserIds": ["允许访问的飞书用户 ID(可选)"]
102
- },
103
- "qq": {
104
- "enabled": false,
105
- "aiCommand": "claude",
106
- "appId": "你的 QQ App ID(可选)",
107
- "secret": "你的 QQ App Secret(可选)",
108
- "allowedUserIds": ["允许访问的 QQ 用户 ID(可选)"]
109
- },
110
- "wework": {
111
- "enabled": false,
112
- "aiCommand": "claude",
113
- "corpId": "你的企业微信 Corp ID(可选)",
114
- "secret": "你的企业微信 Secret(可选)",
115
- "allowedUserIds": ["允许访问的企业微信用户 ID(可选)"]
116
- },
117
- "dingtalk": {
118
- "enabled": false,
119
- "aiCommand": "claude",
120
- "clientId": "你的钉钉 Client ID(可选)",
121
- "clientSecret": "你的钉钉 Client Secret(可选)",
122
- "cardTemplateId": "你的钉钉 AI 卡片模板 ID(可选,配置后启用单条流式)",
123
- "allowedUserIds": ["允许访问的钉钉用户 ID(可选)"]
124
- },
125
- "clawbot": {
126
- "enabled": false,
127
- "aiCommand": "claude",
128
- "apiUrl": "https://ilinkai.weixin.qq.com",
129
- "apiToken": "你的 ClawBot Bearer Token(可选)",
130
- "allowedUserIds": ["允许访问的微信用户 ID(可选)"]
131
- }
132
- }
133
- }`);
134
- console.log("");
135
- console.log("提示:至少需要配置 Telegram、Feishu、QQ、WeWork、DingTalk、WorkBuddy 或 ClawBot 其中一个平台");
136
- console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、QQ_BOT_APPID=xxx、WECHAT_WORKBUDDY_ACCESS_TOKEN=xxx、WEWORK_CORP_ID=xxx、DINGTALK_CLIENT_ID=xxx 或 CLAWBOT_API_TOKEN=xxx 后再运行");
137
- console.log("");
138
- }
139
- const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
9
+ import prompts from 'prompts';
10
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { createLogger } from './logger.js';
14
+ const log = createLogger('Setup');
15
+ const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
140
16
  function loadClaudeSettings() {
141
17
  if (!existsSync(CLAUDE_SETTINGS_PATH))
142
18
  return {};
143
19
  try {
144
- return JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
20
+ return JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8'));
145
21
  }
146
22
  catch {
147
23
  return {};
148
24
  }
149
25
  }
150
- /** 检查 ~/.claude/settings.json 中是否已有 API Key 或 Auth Token(env 内或顶层) */
26
+ /** 检查 ~/.claude/settings.json 中是否已有 API Key 或 Auth Token */
151
27
  function hasClaudeCredsInSettings() {
152
28
  const s = loadClaudeSettings();
153
29
  const env = s?.env;
154
- const fromEnv = !!(env?.ANTHROPIC_API_KEY || env?.ANTHROPIC_AUTH_TOKEN);
155
- const fromTop = !!(s?.ANTHROPIC_API_KEY || s?.ANTHROPIC_AUTH_TOKEN);
156
- return fromEnv || fromTop;
157
- }
158
- function printManualClaudeInstructions() {
159
- console.log("\n━━━ Claude API 配置 ━━━\n");
160
- console.log("当前环境不支持交互输入,请手动配置:");
161
- console.log("");
162
- console.log(" 1. 编辑 ~/.claude/settings.json(与 Claude Code 共用)");
163
- console.log(" 2. 添加 env 字段,例如:");
164
- console.log("");
165
- console.log(' { "env": { "ANTHROPIC_API_KEY": "sk-ant-..." } }');
166
- console.log("");
167
- console.log(" 或使用第三方模型:");
168
- console.log(' { "env": { "ANTHROPIC_AUTH_TOKEN": "xxx", "ANTHROPIC_BASE_URL": "https://...", "ANTHROPIC_MODEL": "glm-4.7" } }');
169
- console.log("");
30
+ return !!(env?.ANTHROPIC_API_KEY || env?.ANTHROPIC_AUTH_TOKEN);
170
31
  }
171
32
  /**
172
33
  * Claude API 专用配置向导,保存到 ~/.claude/settings.json(与 Claude Code 共用)
173
34
  */
174
35
  export async function runClaudeApiSetup() {
175
36
  if (!process.stdin.isTTY) {
176
- printManualClaudeInstructions();
37
+ console.log('\n━━━ Claude API 配置 ━━━\n');
38
+ console.log('当前环境不支持交互输入。请通过 Web 控制台配置:');
39
+ console.log(' 1. 运行 open-im start');
40
+ console.log(' 2. 打开 http://127.0.0.1:39282');
41
+ console.log(' 3. 在设置向导中配置 Claude API\n');
177
42
  return false;
178
43
  }
179
44
  const existing = loadClaudeSettings();
180
45
  const existingEnv = existing.env || {};
181
- console.log("\n━━━ Claude API 配置向导 ━━━\n");
182
- console.log("配置将保存到 ~/.claude/settings.json(Claude Code 配置,与 Claude Code 共用)\n");
46
+ console.log('\n━━━ Claude API 配置向导 ━━━\n');
47
+ console.log('配置将保存到 ~/.claude/settings.json\n');
183
48
  const onCancel = () => {
184
- console.log("\n已取消配置。");
49
+ console.log('\n已取消配置。');
185
50
  process.exit(0);
186
51
  };
187
52
  const apiTypeResp = await prompts({
188
- type: "select",
189
- name: "apiType",
190
- message: "选择 API 类型",
53
+ type: 'select',
54
+ name: 'apiType',
55
+ message: '选择 API 类型',
191
56
  choices: [
192
- { title: "官方 API(Anthropic)", value: "official" },
193
- { title: "第三方模型 / 自定义 API", value: "thirdparty" },
57
+ { title: '官方 API(Anthropic)', value: 'official' },
58
+ { title: '第三方模型 / 自定义 API', value: 'thirdparty' },
59
+ { title: '跳过(已在 Web 控制台配置)', value: 'skip' },
194
60
  ],
195
61
  initial: 0,
196
62
  }, { onCancel });
197
- if (!apiTypeResp.apiType)
63
+ if (!apiTypeResp.apiType || apiTypeResp.apiType === 'skip')
198
64
  return false;
199
65
  const env = { ...existingEnv };
200
- if (apiTypeResp.apiType === "official") {
66
+ if (apiTypeResp.apiType === 'official') {
201
67
  const keyTypeResp = await prompts({
202
- type: "select",
203
- name: "keyType",
204
- message: "选择认证方式",
68
+ type: 'select',
69
+ name: 'keyType',
70
+ message: '选择认证方式',
205
71
  choices: [
206
- { title: "API Key(sk-ant-...)", value: "apikey" },
207
- { title: "Auth Token(claude setup-token 生成)", value: "token" },
72
+ { title: 'API Key(sk-ant-...)', value: 'apikey' },
73
+ { title: 'Auth Token(claude setup-token 生成)', value: 'token' },
208
74
  ],
209
75
  initial: 0,
210
76
  }, { onCancel });
211
- if (!keyTypeResp.keyType)
212
- return false;
213
- if (keyTypeResp.keyType === "apikey") {
214
- const key = await question("ANTHROPIC_API_KEY: ");
77
+ if (keyTypeResp.keyType === 'apikey') {
78
+ const { default: readline } = await import('node:readline');
79
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
80
+ const key = await new Promise((resolve) => {
81
+ rl.question('ANTHROPIC_API_KEY: ', (answer) => {
82
+ rl.close();
83
+ resolve(answer.trim());
84
+ });
85
+ });
215
86
  if (!key.trim()) {
216
- console.log("API Key 不能为空");
87
+ console.log('API Key 不能为空');
217
88
  return false;
218
89
  }
219
90
  env.ANTHROPIC_API_KEY = key.trim();
220
91
  delete env.ANTHROPIC_AUTH_TOKEN;
221
92
  }
222
93
  else {
223
- const token = await question("ANTHROPIC_AUTH_TOKEN: ");
94
+ const { default: readline } = await import('node:readline');
95
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
96
+ const token = await new Promise((resolve) => {
97
+ rl.question('ANTHROPIC_AUTH_TOKEN: ', (answer) => {
98
+ rl.close();
99
+ resolve(answer.trim());
100
+ });
101
+ });
224
102
  if (!token.trim()) {
225
- console.log("Auth Token 不能为空");
103
+ console.log('Auth Token 不能为空');
226
104
  return false;
227
105
  }
228
106
  env.ANTHROPIC_AUTH_TOKEN = token.trim();
@@ -230,991 +108,50 @@ export async function runClaudeApiSetup() {
230
108
  }
231
109
  }
232
110
  else {
233
- const token = await question("ANTHROPIC_AUTH_TOKEN(第三方模型 Token): ");
234
- if (!token.trim()) {
235
- console.log("Token 不能为空");
111
+ const { default: readline } = await import('node:readline');
112
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
113
+ const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
114
+ const token = await ask('ANTHROPIC_AUTH_TOKEN(第三方模型 Token): ');
115
+ if (!token) {
116
+ console.log('Token 不能为空');
117
+ rl.close();
236
118
  return false;
237
119
  }
238
- const baseUrl = await question("ANTHROPIC_BASE_URL(API 地址): ");
239
- if (!baseUrl.trim()) {
240
- console.log("Base URL 不能为空");
120
+ const baseUrl = await ask('ANTHROPIC_BASE_URL(API 地址): ');
121
+ if (!baseUrl) {
122
+ console.log('Base URL 不能为空');
123
+ rl.close();
241
124
  return false;
242
125
  }
243
- const model = await question("ANTHROPIC_MODEL(模型名称,如 glm-4.7): ");
244
- if (!model.trim()) {
245
- console.log("模型名称不能为空");
126
+ const model = await ask('ANTHROPIC_MODEL(模型名称,如 glm-4.7): ');
127
+ if (!model) {
128
+ console.log('模型名称不能为空');
129
+ rl.close();
246
130
  return false;
247
131
  }
248
- env.ANTHROPIC_AUTH_TOKEN = token.trim();
249
- env.ANTHROPIC_BASE_URL = baseUrl.trim();
250
- env.ANTHROPIC_MODEL = model.trim();
132
+ rl.close();
133
+ env.ANTHROPIC_AUTH_TOKEN = token;
134
+ env.ANTHROPIC_BASE_URL = baseUrl;
135
+ env.ANTHROPIC_MODEL = model;
251
136
  delete env.ANTHROPIC_API_KEY;
252
137
  }
253
138
  const dir = dirname(CLAUDE_SETTINGS_PATH);
254
139
  if (!existsSync(dir))
255
140
  mkdirSync(dir, { recursive: true });
256
141
  const merged = { ...existing, env };
257
- writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(merged, null, 2), "utf-8");
258
- console.log("\n✓ Claude API 配置已保存到", CLAUDE_SETTINGS_PATH);
259
- return true;
260
- }
261
- export async function runInteractiveSetup() {
262
- const configPath = join(APP_HOME, "config.json");
263
- const forceManual = process.argv.includes("--manual") || process.argv.includes("-m");
264
- if (forceManual || !process.stdin.isTTY) {
265
- printManualInstructions(configPath);
266
- return false;
267
- }
268
- const existing = loadExistingConfig();
269
- const configured = getConfiguredPlatforms(existing);
270
- console.log("\n━━━ open-im 配置向导 ━━━\n");
271
- console.log("配置将保存到:", configPath);
272
- if (configured.length > 0) {
273
- console.log("当前已配置:", configured.join("、"));
274
- console.log("(本次可追加或修改,未选中的平台配置将保留)");
275
- }
276
- console.log("");
277
- const onCancel = () => {
278
- console.log("\n已取消配置。");
279
- process.exit(0);
280
- };
281
- const hasTg = !!existing?.platforms?.telegram?.botToken;
282
- const hasFs = !!(existing?.platforms?.feishu?.appId && existing?.platforms?.feishu?.appSecret);
283
- const hasQq = !!(existing?.platforms?.qq?.appId && existing?.platforms?.qq?.secret);
284
- const wc = existing?.platforms?.workbuddy;
285
- const hasWc = !!(wc?.accessToken && wc?.refreshToken);
286
- const hasWw = !!(existing?.platforms?.wework?.corpId && existing?.platforms?.wework?.secret);
287
- const hasDt = !!(existing?.platforms?.dingtalk?.clientId && existing?.platforms?.dingtalk?.clientSecret);
288
- // 第一步:选择平台(在选项和提示中显示已配置项)
289
- const configuredHint = configured.length > 0 ? `(当前已配置: ${configured.join("、")})` : "";
290
- const platformResp = await prompts({
291
- type: "select",
292
- name: "platform",
293
- message: `选择要配置的平台 ${configuredHint}(↑↓ 选择)`,
294
- choices: [
295
- {
296
- title: "Telegram - 需要 Bot Token" + (hasTg ? " ✓已配置" : ""),
297
- value: "telegram",
298
- },
299
- {
300
- title: "飞书 (Feishu/Lark) - 需要 App ID 和 App Secret" +
301
- (hasFs ? " ✓已配置" : ""),
302
- value: "feishu",
303
- },
304
- {
305
- title: "QQ Bot - App ID + Secret" + (hasQq ? " configured" : ""),
306
- value: "qq",
307
- },
308
- {
309
- title: "企业微信 (WeCom/WeWork) - 需要 Bot ID 和 Secret" +
310
- (hasWw ? " ✓已配置" : ""),
311
- value: "wework",
312
- },
313
- {
314
- title: "钉钉 (DingTalk) - 需要 Client ID 和 Client Secret" +
315
- (hasDt ? " ✓已配置" : ""),
316
- value: "dingtalk",
317
- },
318
- {
319
- title: "WorkBuddy 微信客服 (WeChat KF)" +
320
- (hasWc ? " ✓已配置" : ""),
321
- value: "workbuddy",
322
- },
323
- {
324
- title: "ClawBot 微信 iLink (需要 API Token)" +
325
- (existing?.platforms?.clawbot?.apiToken ? " ✓已配置" : ""),
326
- value: "clawbot",
327
- },
328
- { title: "配置多个平台", value: "multi" },
329
- ],
330
- initial: 0,
331
- }, { onCancel });
332
- if (!platformResp.platform) {
333
- return false;
334
- }
335
- const platform = platformResp.platform;
336
- const config = { platforms: {} };
337
- // 第二步:选择要配置的多个平台(如果选择了 multi)
338
- let selectedPlatforms = [];
339
- if (platform === "multi") {
340
- const multiResp = await prompts({
341
- type: "multiselect",
342
- name: "platforms",
343
- message: "选择要配置的平台(空格选择,回车确认)",
344
- choices: [
345
- { title: "Telegram" + (hasTg ? " ✓已配置" : ""), value: "telegram", selected: hasTg },
346
- { title: "飞书 (Feishu)" + (hasFs ? " ✓已配置" : ""), value: "feishu", selected: hasFs },
347
- { title: "企业微信 (WeWork)" + (hasWw ? " ✓已配置" : ""), value: "wework", selected: hasWw },
348
- { title: "钉钉 (DingTalk)" + (hasDt ? " ✓已配置" : ""), value: "dingtalk", selected: hasDt },
349
- { title: "WorkBuddy 微信客服 (WeChat KF)" + (hasWc ? " ✓已配置" : ""), value: "workbuddy", selected: hasWc },
350
- { title: "ClawBot 微信 iLink" + (existing?.platforms?.clawbot?.apiToken ? " ✓已配置" : ""), value: "clawbot" },
351
- ],
352
- }, { onCancel });
353
- if (!multiResp.platforms || multiResp.platforms.length === 0) {
354
- console.log("至少需要选择一个平台");
355
- return false;
356
- }
357
- selectedPlatforms = multiResp.platforms;
358
- }
359
- else {
360
- selectedPlatforms = [platform];
361
- }
362
- // 收集平台配置(Telegram 用 readline 避免 Windows 下 prompts 重绘/重复行问题)
363
- if (selectedPlatforms.includes("telegram")) {
364
- const hint = hasTg ? "(留空保留现有)" : "";
365
- const token = await question(`Telegram Bot Token(从 @BotFather 获取)${hint}: `);
366
- if (token) {
367
- config.telegramBotToken = token;
368
- }
369
- else if (hasTg) {
370
- config.telegramBotToken = existing.platforms.telegram.botToken;
371
- }
372
- else if (platform === "telegram") {
373
- console.log("Token 不能为空");
374
- return false;
375
- }
376
- }
377
- if (selectedPlatforms.includes("feishu")) {
378
- const feishuResp = await prompts([
379
- {
380
- type: "text",
381
- name: "appId",
382
- message: "飞书 App ID(从飞书开放平台获取)",
383
- initial: existing?.platforms?.feishu?.appId ?? "",
384
- validate: (v) => (v.trim() ? true : "App ID 不能为空"),
385
- },
386
- {
387
- type: "text",
388
- name: "appSecret",
389
- message: "飞书 App Secret(从飞书开放平台获取)",
390
- initial: existing?.platforms?.feishu?.appSecret ?? "",
391
- validate: (v) => (v.trim() ? true : "App Secret 不能为空"),
392
- },
393
- ], { onCancel });
394
- const fsAppId = feishuResp.appId?.trim() || existing?.platforms?.feishu?.appId;
395
- const fsAppSecret = feishuResp.appSecret?.trim() || existing?.platforms?.feishu?.appSecret;
396
- if (fsAppId && fsAppSecret) {
397
- config.platforms.feishu = {
398
- ...config.platforms?.feishu,
399
- enabled: true,
400
- appId: fsAppId,
401
- appSecret: fsAppSecret,
402
- };
403
- }
404
- else if (platform === "feishu") {
405
- return false;
406
- }
407
- }
408
- if (selectedPlatforms.includes("qq")) {
409
- const qqResp = await prompts([
410
- {
411
- type: "text",
412
- name: "appId",
413
- message: "QQ Bot App ID",
414
- initial: existing?.platforms?.qq?.appId ?? "",
415
- validate: (v) => (v.trim() ? true : "App ID 不能为空"),
416
- },
417
- {
418
- type: "text",
419
- name: "secret",
420
- message: "QQ Bot Secret",
421
- initial: existing?.platforms?.qq?.secret ?? "",
422
- validate: (v) => (v.trim() ? true : "Secret 不能为空"),
423
- },
424
- ], { onCancel });
425
- const qqAppId = qqResp.appId?.trim() || existing?.platforms?.qq?.appId;
426
- const qqSecret = qqResp.secret?.trim() || existing?.platforms?.qq?.secret;
427
- if (qqAppId && qqSecret) {
428
- config.platforms.qq = {
429
- enabled: true,
430
- appId: qqAppId,
431
- secret: qqSecret,
432
- };
433
- }
434
- else if (platform === "qq") {
435
- return false;
436
- }
437
- }
438
- if (selectedPlatforms.includes("workbuddy")) {
439
- const wb = existing?.platforms?.workbuddy;
440
- const hasWbCreds = !!(wb?.accessToken && wb?.refreshToken);
441
- const wbModeResp = await prompts({
442
- type: "select",
443
- name: "mode",
444
- message: "WorkBuddy 微信客服(CodeBuddy OAuth)",
445
- choices: [
446
- {
447
- title: "在浏览器中完成 CodeBuddy 登录并绑定微信客服(推荐)",
448
- value: "oauth",
449
- },
450
- {
451
- title: "使用已有 WorkBuddy 凭证" + (hasWbCreds ? " ✓" : ""),
452
- value: "keep",
453
- disabled: !hasWbCreds,
454
- },
455
- ],
456
- initial: hasWbCreds ? 1 : 0,
457
- }, { onCancel });
458
- if (wbModeResp.mode === "oauth") {
459
- console.log("\n正在启动 WorkBuddy OAuth 登录...\n");
460
- // Phase 1: OAuth token acquisition (fatal if fails)
461
- let oauthOk = false;
462
- try {
463
- const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
464
- const oauth = new WorkBuddyOAuth();
465
- console.log("步骤 1/3: 获取登录链接...");
466
- const { authUrl, state } = await oauth.fetchAuthState();
467
- console.log("\n请在浏览器中打开以下链接完成登录:");
468
- console.log(authUrl);
469
- console.log("\n等待登录完成(最长 5 分钟)...\n");
470
- const tokenResult = await oauth.pollToken(state);
471
- console.log("✅ 登录成功! 正在获取账号信息...");
472
- let accountInfo = {};
473
- try {
474
- accountInfo = await oauth.getAccount(state);
475
- }
476
- catch {
477
- // account info is optional
478
- }
479
- const ai = accountInfo;
480
- // Try multiple field names; fall back to tokenResult.userId, then existing config
481
- const existingUserId = wb?.userId;
482
- const userId = String(ai?.uid ?? ai?.userId ?? ai?.user_id ??
483
- tokenResult.userId ??
484
- existingUserId ?? "");
485
- if (!userId) {
486
- console.warn("⚠️ 未能获取 WorkBuddy 用户 ID,sessionId 可能不正确,建议稍后重试");
487
- }
488
- // Set oauth.userId so buildSessionId() produces the correct sessionId.
489
- oauth.userId = userId;
490
- console.log(` userId: ${userId || "(空)"}, sessionId 将为: ${oauth.buildSessionId()}`);
491
- // Save credentials immediately — even if binding fails below
492
- config.platforms.workbuddy = {
493
- enabled: true,
494
- accessToken: tokenResult.accessToken,
495
- refreshToken: tokenResult.refreshToken,
496
- userId,
497
- };
498
- oauthOk = true;
499
- console.log("\n✅ WorkBuddy 凭证已获取,正在生成微信客服绑定链接...");
500
- // Phase 2: WeChat KF binding link (non-fatal, no polling needed)
501
- try {
502
- const { join: pathJoin } = await import("node:path");
503
- const { homedir } = await import("node:os");
504
- const clawPath = pathJoin(homedir(), "WorkBuddy", "Claw");
505
- const sessionId = oauth.buildSessionId(clawPath);
506
- const linkResult = await oauth.getWeChatKfLink(sessionId);
507
- if (linkResult.success && linkResult.url) {
508
- console.log("\n━━━ 微信客服绑定 ━━━");
509
- console.log("请将以下链接发给微信「文件传输助手」并点击打开,即完成绑定:");
510
- console.log(linkResult.url);
511
- console.log("\n绑定完成后运行 open-im start 启动服务即可收发消息。");
512
- }
513
- else {
514
- console.log(`⚠️ 获取微信客服链接失败: ${linkResult.message ?? "未知错误"}`);
515
- console.log(" 凭证已保存,稍后重新运行 open-im init 可重试绑定");
516
- }
517
- }
518
- catch (bindErr) {
519
- const cause = bindErr?.cause ?? bindErr?.code;
520
- const detail = cause ? ` (${cause})` : "";
521
- console.log(`⚠️ 获取微信客服绑定链接网络失败: ${bindErr instanceof Error ? bindErr.message : String(bindErr)}${detail}`);
522
- console.log(" 凭证已保存,稍后重新运行 open-im init 可重试绑定");
523
- }
524
- console.log("\n✅ WorkBuddy 登录成功,配置已保存");
525
- }
526
- catch (err) {
527
- const cause = err?.cause ?? err?.code;
528
- const detail = cause ? ` (${cause})` : "";
529
- console.error(`\n❌ WorkBuddy 登录失败: ${err instanceof Error ? err.message : String(err)}${detail}`);
530
- if (!oauthOk && platform === "workbuddy")
531
- return false;
532
- }
533
- }
534
- else if (hasWbCreds) {
535
- config.platforms.workbuddy = {
536
- ...wb,
537
- enabled: true,
538
- };
539
- // Also (re-)generate the WeChat KF binding link with existing credentials
540
- try {
541
- const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
542
- const oauthKeep = new WorkBuddyOAuth(wb?.baseUrl);
543
- oauthKeep.loadCredentials({
544
- accessToken: wb?.accessToken ?? '',
545
- refreshToken: wb?.refreshToken ?? '',
546
- userId: wb?.userId ?? '',
547
- });
548
- oauthKeep.userId = wb?.userId ?? '';
549
- const { join: pathJoin2 } = await import("node:path");
550
- const { homedir: homedir2 } = await import("node:os");
551
- const clawPath2 = pathJoin2(homedir2(), "WorkBuddy", "Claw");
552
- const sessionId = oauthKeep.buildSessionId(clawPath2);
553
- console.log(`\n正在获取微信客服绑定链接... (sessionId: ${sessionId})`);
554
- const linkResult = await oauthKeep.getWeChatKfLink(sessionId);
555
- if (linkResult.success && linkResult.url) {
556
- console.log("\n━━━ 微信客服绑定 ━━━");
557
- console.log("请将以下链接发给微信「文件传输助手」并点击打开,即完成绑定:");
558
- console.log(linkResult.url);
559
- console.log("\n绑定完成后运行 open-im start 启动服务即可收发消息。");
560
- }
561
- else {
562
- console.log(`⚠️ 获取绑定链接失败: ${linkResult.message ?? "未知"}`);
563
- }
564
- }
565
- catch (keepErr) {
566
- const cause = keepErr?.cause ?? keepErr?.code;
567
- console.log(`⚠️ 绑定链接请求失败: ${keepErr instanceof Error ? keepErr.message : String(keepErr)}${cause ? ` (${cause})` : ""}`);
568
- console.log(" 稍后重新运行 open-im init 可重试");
569
- }
570
- }
571
- else if (platform === "workbuddy") {
572
- return false;
573
- }
574
- }
575
- if (selectedPlatforms.includes("clawbot")) {
576
- const hasCbToken = !!existing?.platforms?.clawbot?.apiToken;
577
- const cbModeResp = await prompts({
578
- type: "select",
579
- name: "mode",
580
- message: "ClawBot 微信 iLink",
581
- choices: [
582
- { title: "扫码登录(自动获取 Token)", value: "qr" },
583
- { title: "使用已有 Token" + (hasCbToken ? " ✓" : ""), value: "token", disabled: !hasCbToken },
584
- ],
585
- initial: 0,
586
- }, { onCancel });
587
- let cbApiToken = "";
588
- let cbApiUrl = existing?.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322";
589
- if (cbModeResp.mode === "qr") {
590
- console.log("\n正在获取二维码...\n");
591
- try {
592
- const { startQRLogin, waitForQRLogin } = await import("./clawbot/qr-login.js");
593
- const session = await startQRLogin();
594
- console.log("请用微信扫描以下链接中的二维码:");
595
- console.log(session.qrcodeUrl);
596
- console.log("\n等待扫码...(最长 5 分钟)\n");
597
- const result = await waitForQRLogin(session, (status) => {
598
- if (status === "scaned")
599
- process.stdout.write("已扫码,验证中...\n");
600
- });
601
- if (result.connected && result.botToken) {
602
- cbApiToken = result.botToken;
603
- if (result.baseUrl)
604
- cbApiUrl = result.baseUrl;
605
- console.log("\n✅ 登录成功!");
606
- if (result.userId) {
607
- console.log(` 用户 ID: ${result.userId}`);
608
- console.log(" 提示:可将此 ID 添加到白名单");
609
- }
610
- }
611
- else {
612
- console.log(`\n❌ 登录失败: ${result.message}`);
613
- if (platform === "clawbot")
614
- return false;
615
- }
616
- }
617
- catch (err) {
618
- console.error(`\n❌ 二维码获取失败: ${err instanceof Error ? err.message : String(err)}`);
619
- if (platform === "clawbot")
620
- return false;
621
- }
622
- }
623
- else {
624
- const cbResp = await prompts([
625
- {
626
- type: "text",
627
- name: "apiUrl",
628
- message: "ClawBot iLink API 地址(默认 http://127.0.0.1:26322)",
629
- initial: cbApiUrl,
630
- },
631
- {
632
- type: "text",
633
- name: "apiToken",
634
- message: "ClawBot Bearer Token",
635
- initial: existing?.platforms?.clawbot?.apiToken ?? "",
636
- validate: (v) => (v.trim() ? true : "Token 不能为空"),
637
- },
638
- ], { onCancel });
639
- cbApiUrl = cbResp.apiUrl?.trim() || cbApiUrl;
640
- cbApiToken = cbResp.apiToken?.trim() || "";
641
- }
642
- if (cbApiToken) {
643
- config.platforms.clawbot = {
644
- enabled: true,
645
- apiUrl: cbApiUrl,
646
- apiToken: cbApiToken,
647
- };
648
- }
649
- else if (platform === "clawbot") {
650
- return false;
651
- }
652
- }
653
- if (selectedPlatforms.includes("wework")) {
654
- const weworkResp = await prompts([
655
- {
656
- type: "text",
657
- name: "corpId",
658
- message: "企业微信 Bot ID(从企业微信管理后台获取)",
659
- initial: existing?.platforms?.wework?.corpId ?? "",
660
- validate: (v) => (v.trim() ? true : "Bot ID 不能为空"),
661
- },
662
- {
663
- type: "text",
664
- name: "secret",
665
- message: "企业微信 Secret(从企业微信管理后台获取)",
666
- initial: existing?.platforms?.wework?.secret ?? "",
667
- validate: (v) => (v.trim() ? true : "Secret 不能为空"),
668
- },
669
- ], { onCancel });
670
- const wwCorpId = weworkResp.corpId?.trim() || existing?.platforms?.wework?.corpId;
671
- const wwSecret = weworkResp.secret?.trim() || existing?.platforms?.wework?.secret;
672
- if (wwCorpId && wwSecret) {
673
- config.platforms.wework = {
674
- enabled: true,
675
- corpId: wwCorpId,
676
- secret: wwSecret,
677
- };
678
- }
679
- else if (platform === "wework") {
680
- return false;
681
- }
682
- }
683
- if (selectedPlatforms.includes("dingtalk")) {
684
- const dingtalkResp = await prompts([
685
- {
686
- type: "text",
687
- name: "clientId",
688
- message: "钉钉 Client ID / AppKey(从钉钉开放平台获取)",
689
- initial: existing?.platforms?.dingtalk?.clientId ?? "",
690
- validate: (v) => (v.trim() ? true : "Client ID 不能为空"),
691
- },
692
- {
693
- type: "text",
694
- name: "clientSecret",
695
- message: "钉钉 Client Secret / AppSecret(从钉钉开放平台获取)",
696
- initial: existing?.platforms?.dingtalk?.clientSecret ?? "",
697
- validate: (v) => (v.trim() ? true : "Client Secret 不能为空"),
698
- },
699
- {
700
- type: "text",
701
- name: "cardTemplateId",
702
- message: "钉钉 AI 卡片模板 ID(可选,配置后启用单条流式)",
703
- initial: existing?.platforms?.dingtalk?.cardTemplateId ?? "",
704
- },
705
- ], { onCancel });
706
- const dtClientId = dingtalkResp.clientId?.trim() || existing?.platforms?.dingtalk?.clientId;
707
- const dtClientSecret = dingtalkResp.clientSecret?.trim() || existing?.platforms?.dingtalk?.clientSecret;
708
- const dtCardTemplateId = dingtalkResp.cardTemplateId?.trim() || existing?.platforms?.dingtalk?.cardTemplateId;
709
- if (dtClientId && dtClientSecret) {
710
- config.platforms.dingtalk = {
711
- enabled: true,
712
- clientId: dtClientId,
713
- clientSecret: dtClientSecret,
714
- cardTemplateId: dtCardTemplateId,
715
- };
716
- }
717
- else if (platform === "dingtalk") {
718
- return false;
719
- }
720
- }
721
- // 通用配置:只询问所选平台的白名单,未选平台沿用已有配置
722
- const tgIds = existing?.platforms?.telegram?.allowedUserIds?.join(", ") ?? "";
723
- const fsIds = existing?.platforms?.feishu?.allowedUserIds?.join(", ") ?? "";
724
- const qqIds = existing?.platforms?.qq?.allowedUserIds?.join(", ") ?? "";
725
- const wcIds = existing?.platforms?.workbuddy?.allowedUserIds?.join(", ") ?? "";
726
- const wwIds = existing?.platforms?.wework?.allowedUserIds?.join(", ") ?? "";
727
- const dtIds = existing?.platforms?.dingtalk?.allowedUserIds?.join(", ") ?? "";
728
- const commonPrompts = [];
729
- if (selectedPlatforms.includes("qq")) {
730
- commonPrompts.push({
731
- type: "text",
732
- name: "qqAllowedUserIds",
733
- message: "QQ allowed user IDs (optional, comma-separated, empty means allow all)",
734
- initial: qqIds,
735
- });
736
- }
737
- if (selectedPlatforms.includes("telegram")) {
738
- commonPrompts.push({
739
- type: "text",
740
- name: "telegramAllowedUserIds",
741
- message: "Telegram 白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
742
- initial: tgIds,
743
- });
744
- }
745
- if (selectedPlatforms.includes("feishu")) {
746
- commonPrompts.push({
747
- type: "text",
748
- name: "feishuAllowedUserIds",
749
- message: "飞书白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
750
- initial: fsIds,
751
- });
752
- }
753
- if (selectedPlatforms.includes("workbuddy")) {
754
- commonPrompts.push({
755
- type: "text",
756
- name: "workbuddyAllowedUserIds",
757
- message: "WorkBuddy 白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
758
- initial: wcIds,
759
- });
760
- }
761
- if (selectedPlatforms.includes("wework")) {
762
- commonPrompts.push({
763
- type: "text",
764
- name: "weworkAllowedUserIds",
765
- message: "企业微信白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
766
- initial: wwIds,
767
- });
768
- }
769
- if (selectedPlatforms.includes("dingtalk")) {
770
- commonPrompts.push({
771
- type: "text",
772
- name: "dingtalkAllowedUserIds",
773
- message: "钉钉白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
774
- initial: dtIds,
775
- });
776
- }
777
- if (selectedPlatforms.includes("clawbot")) {
778
- const cbIds = existing?.platforms?.clawbot?.allowedUserIds?.join(", ") ?? "";
779
- commonPrompts.push({
780
- type: "text",
781
- name: "clawbotAllowedUserIds",
782
- message: "ClawBot 白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
783
- initial: cbIds,
784
- });
785
- }
786
- commonPrompts.push({
787
- type: "text",
788
- name: "workDir",
789
- message: "工作目录",
790
- initial: existing?.tools?.claude?.workDir ?? process.cwd(),
791
- }, {
792
- type: "text",
793
- name: "codexProxy",
794
- message: "Codex 代理(可选,如 http://127.0.0.1:7890)",
795
- initial: existing?.tools?.codex?.proxy ?? "",
796
- });
797
- const commonResp = await prompts(commonPrompts, { onCancel });
798
- // 若需配置 Claude API,询问 API 配置
799
- let claudeApiConfig = {};
800
- {
801
- const legacyRootEnv = existing?.env;
802
- const claudeFileEnv = existing?.tools?.claude?.env ?? legacyRootEnv;
803
- // 检查是否已配置 API 密钥(环境变量、tools.claude.env、旧版根 env、或 ~/.claude/settings.json)
804
- const hasExistingApiKey = !!(process.env.ANTHROPIC_API_KEY ||
805
- process.env.ANTHROPIC_AUTH_TOKEN ||
806
- process.env.CLAUDE_CODE_OAUTH_TOKEN ||
807
- claudeFileEnv?.ANTHROPIC_API_KEY ||
808
- claudeFileEnv?.ANTHROPIC_AUTH_TOKEN ||
809
- hasClaudeCredsInSettings());
810
- if (hasExistingApiKey) {
811
- // 已经配置过,直接保留原有配置,跳过询问
812
- if (claudeFileEnv && Object.keys(claudeFileEnv).length > 0) {
813
- claudeApiConfig = {
814
- apiKey: claudeFileEnv.ANTHROPIC_API_KEY || claudeFileEnv.ANTHROPIC_AUTH_TOKEN,
815
- baseUrl: claudeFileEnv.ANTHROPIC_BASE_URL,
816
- model: claudeFileEnv.ANTHROPIC_MODEL,
817
- haikuModel: claudeFileEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL,
818
- sonnetModel: claudeFileEnv.ANTHROPIC_DEFAULT_SONNET_MODEL,
819
- opusModel: claudeFileEnv.ANTHROPIC_DEFAULT_OPUS_MODEL,
820
- };
821
- }
822
- }
823
- else {
824
- // 没有配置过,引导用户配置
825
- console.log('');
826
- console.log('━━━ Claude API 配置 ━━━');
827
- console.log('提示:以下配置均为可选,留空将使用默认值');
828
- console.log('');
829
- const apiResp = await prompts([
830
- {
831
- type: "text",
832
- name: "apiKey",
833
- message: "API Key / Auth Token(回车跳过,稍后手动配置)",
834
- initial: "",
835
- },
836
- {
837
- type: "text",
838
- name: "baseUrl",
839
- message: "Base URL(回车跳过,使用官方 API)",
840
- initial: "",
841
- },
842
- {
843
- type: "text",
844
- name: "model",
845
- message: "默认模型(回车跳过)",
846
- initial: "",
847
- },
848
- {
849
- type: "text",
850
- name: "haikuModel",
851
- message: "Haiku 模型(回车跳过)",
852
- initial: "",
853
- },
854
- {
855
- type: "text",
856
- name: "sonnetModel",
857
- message: "Sonnet 模型(回车跳过)",
858
- initial: "",
859
- },
860
- {
861
- type: "text",
862
- name: "opusModel",
863
- message: "Opus 模型(回车跳过)",
864
- initial: "",
865
- },
866
- ], { onCancel });
867
- claudeApiConfig = apiResp;
868
- }
869
- }
870
- const parseIds = (value) => value
871
- ? value
872
- .split(",")
873
- .map((s) => s.trim())
874
- .filter(Boolean)
875
- : [];
876
- // 分平台白名单:已询问的用输入值,未询问的用已有配置
877
- const telegramIds = selectedPlatforms.includes("telegram")
878
- ? parseIds(commonResp.telegramAllowedUserIds)
879
- : parseIds(existing?.platforms?.telegram?.allowedUserIds?.join(", "));
880
- const feishuIds = selectedPlatforms.includes("feishu")
881
- ? parseIds(commonResp.feishuAllowedUserIds)
882
- : parseIds(existing?.platforms?.feishu?.allowedUserIds?.join(", "));
883
- const qqIdsFinal = selectedPlatforms.includes("qq")
884
- ? parseIds(commonResp.qqAllowedUserIds)
885
- : parseIds(existing?.platforms?.qq?.allowedUserIds?.join(", "));
886
- const workbuddyIds = selectedPlatforms.includes("workbuddy")
887
- ? parseIds(commonResp.workbuddyAllowedUserIds)
888
- : parseIds(existing?.platforms?.workbuddy?.allowedUserIds?.join(", "));
889
- const weworkIds = selectedPlatforms.includes("wework")
890
- ? parseIds(commonResp.weworkAllowedUserIds)
891
- : parseIds(existing?.platforms?.wework?.allowedUserIds?.join(", "));
892
- const dingtalkIds = selectedPlatforms.includes("dingtalk")
893
- ? parseIds(commonResp.dingtalkAllowedUserIds)
894
- : parseIds(existing?.platforms?.dingtalk?.allowedUserIds?.join(", "));
895
- const clawbotIds = selectedPlatforms.includes("clawbot")
896
- ? parseIds(commonResp.clawbotAllowedUserIds)
897
- : parseIds(existing?.platforms?.clawbot?.allowedUserIds?.join(", "));
898
- // 增量合并:以已有配置为底,只覆盖本次选中的平台(不写入根级旧字段 telegramBotToken 等)
899
- const base = existing
900
- ? JSON.parse(JSON.stringify(existing))
901
- : null;
902
- const { telegramBotToken: _, feishuAppId: __, feishuAppSecret: ___, claudeWorkDir: _cwd, claudeTimeoutMs: _ctm, claudeModel: _cm, env: _stripLegacyRootEnv, aiCommand: _stripLegacyGlobalAi, ...baseRest } = (base ?? {});
903
- // 若用户在向导中输入了 Claude 配置,写入 ~/.claude/settings.json(与 Claude Code 共用)
904
- if (claudeApiConfig.apiKey || claudeApiConfig.baseUrl || claudeApiConfig.model) {
905
- const claudeExisting = loadClaudeSettings();
906
- const claudeEnv = { ...(claudeExisting.env ?? {}) };
907
- if (claudeApiConfig.apiKey?.trim()) {
908
- const key = claudeApiConfig.apiKey.trim();
909
- if (key.startsWith("sk-")) {
910
- claudeEnv.ANTHROPIC_API_KEY = key;
911
- delete claudeEnv.ANTHROPIC_AUTH_TOKEN;
912
- }
913
- else {
914
- claudeEnv.ANTHROPIC_AUTH_TOKEN = key;
915
- delete claudeEnv.ANTHROPIC_API_KEY;
916
- }
917
- }
918
- if (claudeApiConfig.baseUrl?.trim())
919
- claudeEnv.ANTHROPIC_BASE_URL = claudeApiConfig.baseUrl.trim();
920
- if (claudeApiConfig.model?.trim())
921
- claudeEnv.ANTHROPIC_MODEL = claudeApiConfig.model.trim();
922
- if (claudeApiConfig.haikuModel?.trim())
923
- claudeEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = claudeApiConfig.haikuModel.trim();
924
- if (claudeApiConfig.sonnetModel?.trim())
925
- claudeEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = claudeApiConfig.sonnetModel.trim();
926
- if (claudeApiConfig.opusModel?.trim())
927
- claudeEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = claudeApiConfig.opusModel.trim();
928
- const claudeDir = dirname(CLAUDE_SETTINGS_PATH);
929
- if (!existsSync(claudeDir))
930
- mkdirSync(claudeDir, { recursive: true });
931
- writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify({ ...claudeExisting, env: claudeEnv }, null, 2), "utf-8");
932
- console.log("\n✓ Claude API 配置已保存到", CLAUDE_SETTINGS_PATH);
933
- }
934
- const workDir = (commonResp.workDir || process.cwd()).trim();
935
- const baseTools = base?.tools ?? {};
936
- const codexProxyVal = commonResp.codexProxy?.trim();
937
- const out = {
938
- ...baseRest,
939
- platforms: { ...(base?.platforms ?? {}) },
940
- tools: {
941
- claude: {
942
- ...baseTools.claude,
943
- cliPath: baseTools.claude?.cliPath ?? "claude",
944
- workDir,
945
- },
946
- codex: {
947
- ...baseTools.codex,
948
- cliPath: baseTools.codex?.cliPath ?? "codex",
949
- workDir: workDir,
950
- proxy: codexProxyVal || baseTools.codex?.proxy,
951
- },
952
- codebuddy: {
953
- ...baseTools.codebuddy,
954
- cliPath: baseTools.codebuddy?.cliPath ?? "codebuddy",
955
- },
956
- opencode: {
957
- ...baseTools.opencode,
958
- cliPath: baseTools.opencode?.cliPath ?? "opencode",
959
- },
960
- },
961
- };
962
- const outPlatforms = out.platforms;
963
- const configPlatforms = config.platforms;
964
- const basePlatforms = base?.platforms;
965
- if (selectedPlatforms.includes("telegram")) {
966
- outPlatforms.telegram = {
967
- ...basePlatforms?.telegram,
968
- enabled: true,
969
- aiCommand: defaultPlatformAi(basePlatforms?.telegram?.aiCommand),
970
- botToken: config.telegramBotToken ?? basePlatforms?.telegram?.botToken,
971
- allowedUserIds: telegramIds,
972
- };
973
- }
974
- else if (basePlatforms?.telegram) {
975
- outPlatforms.telegram = {
976
- ...basePlatforms.telegram,
977
- aiCommand: defaultPlatformAi(basePlatforms.telegram.aiCommand),
978
- allowedUserIds: telegramIds.length > 0 ? telegramIds : basePlatforms.telegram.allowedUserIds ?? [],
979
- };
980
- }
981
- else {
982
- outPlatforms.telegram = { enabled: false, aiCommand: "claude", allowedUserIds: telegramIds };
983
- }
984
- if (selectedPlatforms.includes("feishu")) {
985
- outPlatforms.feishu = {
986
- ...basePlatforms?.feishu,
987
- enabled: true,
988
- aiCommand: defaultPlatformAi(basePlatforms?.feishu?.aiCommand),
989
- appId: configPlatforms?.feishu?.appId,
990
- appSecret: configPlatforms?.feishu?.appSecret,
991
- allowedUserIds: feishuIds,
992
- };
993
- }
994
- else if (basePlatforms?.feishu) {
995
- outPlatforms.feishu = {
996
- ...basePlatforms.feishu,
997
- aiCommand: defaultPlatformAi(basePlatforms.feishu.aiCommand),
998
- allowedUserIds: feishuIds.length > 0 ? feishuIds : basePlatforms.feishu.allowedUserIds ?? [],
999
- };
1000
- }
1001
- else {
1002
- outPlatforms.feishu = { enabled: false, aiCommand: "claude", allowedUserIds: feishuIds };
1003
- }
1004
- if (selectedPlatforms.includes("qq")) {
1005
- outPlatforms.qq = {
1006
- ...basePlatforms?.qq,
1007
- enabled: true,
1008
- aiCommand: defaultPlatformAi(basePlatforms?.qq?.aiCommand),
1009
- appId: configPlatforms?.qq?.appId,
1010
- secret: configPlatforms?.qq?.secret,
1011
- allowedUserIds: qqIdsFinal,
1012
- };
1013
- }
1014
- else if (basePlatforms?.qq) {
1015
- outPlatforms.qq = {
1016
- ...basePlatforms.qq,
1017
- aiCommand: defaultPlatformAi(basePlatforms.qq.aiCommand),
1018
- allowedUserIds: qqIdsFinal.length > 0 ? qqIdsFinal : basePlatforms.qq.allowedUserIds ?? [],
1019
- };
1020
- }
1021
- else {
1022
- outPlatforms.qq = { enabled: false, aiCommand: "claude", allowedUserIds: qqIdsFinal };
1023
- }
1024
- if (selectedPlatforms.includes("workbuddy")) {
1025
- const wbConfig = config.platforms?.workbuddy;
1026
- const baseWb = base?.platforms?.workbuddy;
1027
- const wbOut = {
1028
- enabled: true,
1029
- aiCommand: defaultPlatformAi(baseWb?.aiCommand),
1030
- accessToken: wbConfig?.accessToken ?? baseWb?.accessToken,
1031
- refreshToken: wbConfig?.refreshToken ?? baseWb?.refreshToken,
1032
- userId: wbConfig?.userId ?? baseWb?.userId ?? "",
1033
- allowedUserIds: workbuddyIds,
1034
- };
1035
- const wbBaseUrl = wbConfig?.baseUrl ?? baseWb?.baseUrl;
1036
- if (wbBaseUrl)
1037
- wbOut.baseUrl = wbBaseUrl;
1038
- out.platforms.workbuddy = wbOut;
1039
- }
1040
- else if (basePlatforms?.workbuddy) {
1041
- outPlatforms.workbuddy = {
1042
- ...basePlatforms.workbuddy,
1043
- aiCommand: defaultPlatformAi(basePlatforms.workbuddy.aiCommand),
1044
- allowedUserIds: workbuddyIds.length > 0 ? workbuddyIds : basePlatforms.workbuddy.allowedUserIds ?? [],
1045
- };
1046
- }
1047
- else {
1048
- outPlatforms.workbuddy = { enabled: false, aiCommand: "claude", allowedUserIds: workbuddyIds };
1049
- }
1050
- if (selectedPlatforms.includes("wework")) {
1051
- outPlatforms.wework = {
1052
- ...basePlatforms?.wework,
1053
- enabled: true,
1054
- aiCommand: defaultPlatformAi(basePlatforms?.wework?.aiCommand),
1055
- corpId: configPlatforms?.wework?.corpId,
1056
- secret: configPlatforms?.wework?.secret,
1057
- allowedUserIds: weworkIds,
1058
- };
1059
- }
1060
- else if (basePlatforms?.wework) {
1061
- outPlatforms.wework = {
1062
- ...basePlatforms.wework,
1063
- aiCommand: defaultPlatformAi(basePlatforms.wework.aiCommand),
1064
- allowedUserIds: weworkIds.length > 0 ? weworkIds : basePlatforms.wework.allowedUserIds ?? [],
1065
- };
1066
- }
1067
- else {
1068
- outPlatforms.wework = { enabled: false, aiCommand: "claude", allowedUserIds: weworkIds };
1069
- }
1070
- if (selectedPlatforms.includes("dingtalk")) {
1071
- outPlatforms.dingtalk = {
1072
- ...basePlatforms?.dingtalk,
1073
- enabled: true,
1074
- aiCommand: defaultPlatformAi(basePlatforms?.dingtalk?.aiCommand),
1075
- clientId: configPlatforms?.dingtalk?.clientId,
1076
- clientSecret: configPlatforms?.dingtalk?.clientSecret,
1077
- cardTemplateId: configPlatforms?.dingtalk?.cardTemplateId,
1078
- allowedUserIds: dingtalkIds,
1079
- };
1080
- }
1081
- else if (basePlatforms?.dingtalk) {
1082
- outPlatforms.dingtalk = {
1083
- ...basePlatforms.dingtalk,
1084
- aiCommand: defaultPlatformAi(basePlatforms.dingtalk.aiCommand),
1085
- allowedUserIds: dingtalkIds.length > 0 ? dingtalkIds : basePlatforms.dingtalk.allowedUserIds ?? [],
1086
- };
1087
- }
1088
- else {
1089
- outPlatforms.dingtalk = { enabled: false, aiCommand: "claude", allowedUserIds: dingtalkIds };
1090
- }
1091
- if (selectedPlatforms.includes("clawbot")) {
1092
- const cbConfig = config.platforms?.clawbot;
1093
- const baseCb = base?.platforms?.clawbot;
1094
- outPlatforms.clawbot = {
1095
- enabled: true,
1096
- aiCommand: defaultPlatformAi(baseCb?.aiCommand),
1097
- apiUrl: String(cbConfig?.apiUrl ?? baseCb?.apiUrl ?? "http://127.0.0.1:26322"),
1098
- apiToken: String(cbConfig?.apiToken ?? baseCb?.apiToken ?? ""),
1099
- allowedUserIds: clawbotIds,
1100
- };
1101
- }
1102
- else if (basePlatforms?.clawbot) {
1103
- outPlatforms.clawbot = {
1104
- ...basePlatforms.clawbot,
1105
- aiCommand: defaultPlatformAi(basePlatforms.clawbot.aiCommand),
1106
- allowedUserIds: clawbotIds.length > 0 ? clawbotIds : basePlatforms.clawbot.allowedUserIds ?? [],
1107
- };
1108
- }
1109
- else {
1110
- outPlatforms.clawbot = { enabled: false, aiCommand: "claude", allowedUserIds: clawbotIds };
1111
- }
1112
- const dir = dirname(configPath);
1113
- if (!existsSync(dir)) {
1114
- mkdirSync(dir, { recursive: true });
1115
- }
1116
- writeFileSync(configPath, JSON.stringify(out, null, 2), "utf-8");
1117
- try {
1118
- chmodSync(configPath, 0o600);
1119
- }
1120
- catch { /* ignore on unsupported platforms */ }
1121
- console.log("\n✅ 配置已保存到", configPath);
1122
- console.log("");
142
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(merged, null, 2), 'utf-8');
143
+ console.log('\n✓ Claude API 配置已保存到', CLAUDE_SETTINGS_PATH);
1123
144
  return true;
1124
145
  }
1125
- const PLATFORM_LABELS = {
1126
- telegram: "Telegram",
1127
- qq: "QQ",
1128
- feishu: "飞书",
1129
- wework: "企业微信",
1130
- dingtalk: "钉钉",
1131
- workbuddy: "WorkBuddy 微信客服",
1132
- clawbot: "ClawBot 微信 iLink",
1133
- };
1134
- const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "workbuddy", "clawbot"];
1135
146
  /**
1136
- * 启动时让用户选择要启用的平台(无论单通道还是多通道)
1137
- * 显示全部 4 个平台,已配置的预选;若用户选择未配置的,引导运行 init
1138
- * @returns 更新后的 config,或 null 表示取消
147
+ * 交互式配置 — 现在直接引导到 Web 控制台
1139
148
  */
1140
- async function runPlatformSelectionPrompt(config) {
1141
- const withCreds = new Set(getPlatformsWithCredentials(config));
1142
- const configPath = join(APP_HOME, "config.json");
1143
- const existing = loadExistingConfig();
1144
- const choices = ALL_PLATFORMS.map((p) => {
1145
- const hasCreds = withCreds.has(p);
1146
- const isEnabled = config.enabledPlatforms.includes(p);
1147
- const suffix = hasCreds
1148
- ? (isEnabled ? " ✓已配置且启用" : " ✓已配置")
1149
- : "(未配置)";
1150
- return {
1151
- title: `${PLATFORM_LABELS[p]}${suffix}`,
1152
- value: p,
1153
- selected: isEnabled && hasCreds,
1154
- };
1155
- });
1156
- console.log("\n━━━ 选择要启用的平台 ━━━\n");
1157
- const resp = await prompts({
1158
- type: "multiselect",
1159
- name: "platforms",
1160
- message: "选择要启用的平台(空格切换,回车确认)",
1161
- choices,
1162
- hint: "未配置的平台需先运行 open-im init",
1163
- }, {
1164
- onCancel: () => {
1165
- console.log("\n已取消启动。");
1166
- process.exit(0);
1167
- },
1168
- });
1169
- if (!resp.platforms || !Array.isArray(resp.platforms))
1170
- return null;
1171
- const selected = new Set(resp.platforms);
1172
- const selectedUnconfigured = ALL_PLATFORMS.filter((p) => selected.has(p) && !withCreds.has(p));
1173
- if (selectedUnconfigured.length > 0) {
1174
- console.log("");
1175
- console.log("您选择了 " +
1176
- selectedUnconfigured.map((p) => PLATFORM_LABELS[p]).join("、") +
1177
- ",但这些平台尚未配置。");
1178
- console.log("请运行 open-im init 进行配置,配置完成后再次启动。");
1179
- console.log("");
1180
- const runNow = await prompts({
1181
- type: "confirm",
1182
- name: "run",
1183
- message: "是否现在运行配置向导?",
1184
- initial: true,
1185
- }, { onCancel: () => process.exit(0) });
1186
- if (runNow.run) {
1187
- const saved = await runInteractiveSetup();
1188
- if (saved) {
1189
- console.log("\n配置完成,请再次运行 open-im start 或 open-im dev 启动。");
1190
- }
1191
- }
1192
- process.exit(0);
1193
- }
1194
- // 至少需要 1 个已配置的平台被选中
1195
- const selectedWithCreds = ALL_PLATFORMS.filter((p) => selected.has(p) && withCreds.has(p));
1196
- if (selectedWithCreds.length === 0) {
1197
- console.log("\n请至少选择 1 个已配置的平台,或运行 open-im init 添加配置。");
1198
- return null;
1199
- }
1200
- // 更新 config.json 中的 platforms.xxx.enabled
1201
- const updated = { ...existing };
1202
- if (!updated.platforms)
1203
- updated.platforms = {};
1204
- for (const p of ALL_PLATFORMS) {
1205
- const plat = updated.platforms[p] ?? {};
1206
- updated.platforms[p] = {
1207
- ...plat,
1208
- enabled: selected.has(p) && withCreds.has(p),
1209
- };
1210
- }
1211
- const dir = dirname(configPath);
1212
- if (!existsSync(dir))
1213
- mkdirSync(dir, { recursive: true });
1214
- writeFileSync(configPath, JSON.stringify(updated, null, 2), "utf-8");
1215
- try {
1216
- chmodSync(configPath, 0o600);
1217
- }
1218
- catch { /* ignore on unsupported platforms */ }
1219
- return loadConfig();
149
+ export async function runInteractiveSetup() {
150
+ console.log('\n━━━ open-im 配置 ━━━\n');
151
+ console.log('请通过 Web 控制台完成配置:');
152
+ console.log(' 1. 运行 open-im start');
153
+ console.log(' 2. 打开 http://127.0.0.1:39282');
154
+ console.log(' 3. 按照设置向导完成配置\n');
155
+ console.log('或运行 open-im dashboard 仅启动 Web 配置服务。\n');
156
+ return true;
1220
157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.7",
3
+ "version": "1.11.8-beta.0",
4
4
  "description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",