@wu529778790/open-im 1.9.3-beta.1 → 1.9.3-beta.11

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.
@@ -15,5 +15,10 @@ export declare class ClaudeSDKAdapter implements ToolAdapter {
15
15
  * 清理所有活跃的 SDK 会话和流
16
16
  */
17
17
  static destroy(): void;
18
+ /**
19
+ * Remove a specific session from the in-memory cache and close it.
20
+ * Useful when the caller knows a session is corrupted.
21
+ */
22
+ static removeSession(sessionId: string): void;
18
23
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
19
24
  }
@@ -16,6 +16,52 @@ const log = createLogger('ClaudeSDK');
16
16
  const activeSessions = new Map();
17
17
  // 存储正在进行的流式迭代器,用于中断
18
18
  const activeStreams = new Set();
19
+ // 空闲会话清理:跟踪最后使用时间,定期清除超时会话
20
+ const sessionLastUsed = new Map();
21
+ const SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 分钟未使用则清理
22
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
23
+ const cleanupInterval = setInterval(() => {
24
+ const now = Date.now();
25
+ for (const [id, lastUsed] of sessionLastUsed) {
26
+ if (now - lastUsed > SESSION_IDLE_TTL_MS) {
27
+ const session = activeSessions.get(id);
28
+ if (session) {
29
+ try {
30
+ session.close();
31
+ }
32
+ catch { /* ignore */ }
33
+ activeSessions.delete(id);
34
+ }
35
+ sessionLastUsed.delete(id);
36
+ log.info(`Cleaned up idle session (unused ${Math.round((now - lastUsed) / 60000)}min): ${id}`);
37
+ }
38
+ }
39
+ }, CLEANUP_INTERVAL_MS);
40
+ cleanupInterval.unref(); // 不阻止进程退出
41
+ // Lazy cleanup: check idle sessions periodically during getOrCreateSession calls
42
+ let lazyCleanupCounter = 0;
43
+ const LAZY_CLEANUP_INTERVAL = 10;
44
+ let sessionSeq = 0;
45
+ function lazyCleanupIdleSessions() {
46
+ lazyCleanupCounter++;
47
+ if (lazyCleanupCounter % LAZY_CLEANUP_INTERVAL !== 0)
48
+ return;
49
+ const now = Date.now();
50
+ for (const [id, lastUsed] of sessionLastUsed) {
51
+ if (now - lastUsed > SESSION_IDLE_TTL_MS) {
52
+ const s = activeSessions.get(id);
53
+ if (s) {
54
+ try {
55
+ s.close();
56
+ }
57
+ catch { /* ignore */ }
58
+ activeSessions.delete(id);
59
+ }
60
+ sessionLastUsed.delete(id);
61
+ log.info(`Lazy cleanup: idle session ${id} (unused ${Math.round((now - lastUsed) / 60000)}min)`);
62
+ }
63
+ }
64
+ }
19
65
  // Mutex to serialize process.chdir() calls across concurrent users
20
66
  let chdirMutex = Promise.resolve();
21
67
  function withChdirMutex(fn) {
@@ -44,6 +90,9 @@ function isAssistant(msg) {
44
90
  function isResult(msg) {
45
91
  return msg.type === 'result';
46
92
  }
93
+ function isSessionCorruptionError(msg) {
94
+ return /session\s*(not found|expired|corrupt)|no\s*conversation\s*found/i.test(msg);
95
+ }
47
96
  /**
48
97
  * 获取或创建 SDKSession
49
98
  * @param sessionId 已有的 sessionId,如果为 undefined 则创建新会话
@@ -53,6 +102,7 @@ function isResult(msg) {
53
102
  * @returns SDKSession 对象和实际的 sessionId
54
103
  */
55
104
  async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
105
+ lazyCleanupIdleSessions();
56
106
  const resolvedModel = model?.trim() || 'claude-opus-4-5';
57
107
  const sessionOptions = {
58
108
  model: resolvedModel,
@@ -60,7 +110,9 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
60
110
  };
61
111
  const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
62
112
  log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl} workDir=${workDir}`);
63
- // Use mutex to serialize process.chdir() calls across concurrent users
113
+ // NOTE: process.chdir() 是进程级全局副作用,在并发服务器中不理想。
114
+ // 但 SDK 的 createSession/resumeSession 不接受 cwd 参数,且这些调用是同步的,
115
+ // 所以 mutex + try/finally 已是最优方案。如果 SDK 未来支持 cwd 选项,应移除 chdir。
64
116
  return withChdirMutex(() => {
65
117
  let session;
66
118
  const originalCwd = process.cwd();
@@ -69,11 +121,19 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
69
121
  process.chdir(workDir);
70
122
  }
71
123
  if (sessionId) {
72
- // 尝试恢复已有会话
124
+ // 优先复用内存中已有的 SDKSession,避免每次都启动新进程
125
+ const existing = activeSessions.get(sessionId);
126
+ if (existing) {
127
+ log.info(`Reusing existing in-memory session: ${sessionId}`);
128
+ sessionLastUsed.set(sessionId, Date.now());
129
+ return { session: existing, sessionId };
130
+ }
131
+ // 内存中没有,尝试通过 resume 恢复(会启动新 CLI 进程)
73
132
  try {
74
133
  log.info(`Attempting to resume session: ${sessionId}`);
75
134
  session = unstable_v2_resumeSession(sessionId, sessionOptions);
76
135
  activeSessions.set(sessionId, session);
136
+ sessionLastUsed.set(sessionId, Date.now());
77
137
  log.info(`Successfully resumed session: ${sessionId}`);
78
138
  return { session, sessionId };
79
139
  }
@@ -86,8 +146,9 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
86
146
  session = unstable_v2_createSession(sessionOptions);
87
147
  // 新会话的 sessionId 需要从第一个消息中获取
88
148
  // 暂时返回 undefined,稍后在 init 消息中获取
89
- const tempId = `pending-${Date.now()}`;
149
+ const tempId = `pending-${++sessionSeq}`;
90
150
  activeSessions.set(tempId, session);
151
+ sessionLastUsed.set(tempId, Date.now());
91
152
  log.info(`Created new session (tempId: ${tempId})`);
92
153
  return { session, sessionId: tempId };
93
154
  }
@@ -104,6 +165,7 @@ export class ClaudeSDKAdapter {
104
165
  * 清理所有活跃的 SDK 会话和流
105
166
  */
106
167
  static destroy() {
168
+ clearInterval(cleanupInterval);
107
169
  for (const stream of activeStreams) {
108
170
  try {
109
171
  if (stream && typeof stream.return === 'function') {
@@ -124,6 +186,23 @@ export class ClaudeSDKAdapter {
124
186
  }
125
187
  }
126
188
  activeSessions.clear();
189
+ sessionLastUsed.clear();
190
+ }
191
+ /**
192
+ * Remove a specific session from the in-memory cache and close it.
193
+ * Useful when the caller knows a session is corrupted.
194
+ */
195
+ static removeSession(sessionId) {
196
+ const session = activeSessions.get(sessionId);
197
+ if (session) {
198
+ try {
199
+ session.close();
200
+ }
201
+ catch { /* ignore */ }
202
+ activeSessions.delete(sessionId);
203
+ sessionLastUsed.delete(sessionId);
204
+ log.info(`Explicitly removed session: ${sessionId}`);
205
+ }
127
206
  }
128
207
  run(prompt, sessionId, workDir, callbacks, options) {
129
208
  log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
@@ -132,6 +211,8 @@ export class ClaudeSDKAdapter {
132
211
  let actualSessionId;
133
212
  let pendingTempId; // 记录临时 ID,用于 abort 时清理
134
213
  let runSettled = false;
214
+ let currentStream; // 用于 abort 时立即中断 stream
215
+ let timeoutHandle;
135
216
  const permissionMode = options?.skipPermissions
136
217
  ? 'bypassPermissions'
137
218
  : options?.permissionMode === 'acceptEdits'
@@ -158,6 +239,7 @@ export class ClaudeSDKAdapter {
158
239
  await session.send(prompt);
159
240
  // 获取响应流
160
241
  const stream = session.stream();
242
+ currentStream = stream;
161
243
  activeStreams.add(stream);
162
244
  let accumulated = '';
163
245
  let accumulatedThinking = '';
@@ -173,10 +255,15 @@ export class ClaudeSDKAdapter {
173
255
  const newSessionId = msg.session_id;
174
256
  if (newSessionId && newSessionId !== actualSessionId) {
175
257
  // 更新 sessionId 映射
176
- if (actualSessionId && actualSessionId.startsWith('pending-')) {
177
- activeSessions.delete(actualSessionId);
258
+ // 清理 pending 临时 ID(actualSessionId 尚未赋值时用 pendingTempId)
259
+ const idToClean = actualSessionId ?? pendingTempId;
260
+ if (idToClean?.startsWith('pending-')) {
261
+ activeSessions.delete(idToClean);
178
262
  }
179
263
  activeSessions.set(newSessionId, session);
264
+ sessionLastUsed.set(newSessionId, Date.now());
265
+ if (idToClean)
266
+ sessionLastUsed.delete(idToClean);
180
267
  actualSessionId = newSessionId;
181
268
  log.info(`[V2] Got actual sessionId: ${newSessionId}`);
182
269
  callbacks.onSessionId?.(newSessionId);
@@ -218,10 +305,20 @@ export class ClaudeSDKAdapter {
218
305
  log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
219
306
  // 检查会话错误
220
307
  if (!success) {
308
+ if (timeoutHandle)
309
+ clearTimeout(timeoutHandle);
221
310
  runSettled = true;
222
311
  const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
223
312
  if (noConvErr) {
224
- log.warn(`Session ${actualSessionId} not found, may need to create new one`);
313
+ log.warn(`Session ${actualSessionId} not found, removing from active sessions`);
314
+ if (actualSessionId) {
315
+ activeSessions.delete(actualSessionId);
316
+ sessionLastUsed.delete(actualSessionId);
317
+ try {
318
+ session.close();
319
+ }
320
+ catch { /* ignore */ }
321
+ }
225
322
  callbacks.onSessionInvalid?.();
226
323
  }
227
324
  const errMsg = errs[0] || '未知错误';
@@ -246,6 +343,8 @@ export class ClaudeSDKAdapter {
246
343
  result.result = accumulated;
247
344
  }
248
345
  runSettled = true;
346
+ if (timeoutHandle)
347
+ clearTimeout(timeoutHandle);
249
348
  callbacks.onComplete(result);
250
349
  return;
251
350
  }
@@ -254,6 +353,8 @@ export class ClaudeSDKAdapter {
254
353
  if (!streamClosed) {
255
354
  if (accumulated) {
256
355
  log.info('Stream ended without result message, using accumulated text');
356
+ if (timeoutHandle)
357
+ clearTimeout(timeoutHandle);
257
358
  runSettled = true;
258
359
  callbacks.onComplete({
259
360
  success: true,
@@ -268,6 +369,8 @@ export class ClaudeSDKAdapter {
268
369
  else {
269
370
  // 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
270
371
  log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
372
+ if (timeoutHandle)
373
+ clearTimeout(timeoutHandle);
271
374
  runSettled = true;
272
375
  callbacks.onError('AI 响应异常结束(无输出),请重试');
273
376
  }
@@ -290,6 +393,8 @@ export class ClaudeSDKAdapter {
290
393
  return;
291
394
  }
292
395
  runSettled = true;
396
+ if (timeoutHandle)
397
+ clearTimeout(timeoutHandle);
293
398
  const errorObj = err;
294
399
  const msg = errorObj.message || String(err);
295
400
  log.error(`Claude SDK V2 error: ${msg}`);
@@ -302,15 +407,66 @@ export class ClaudeSDKAdapter {
302
407
  activeSessions.delete(errIdToClean);
303
408
  log.info(`Cleaned up pending session after error: ${errIdToClean}`);
304
409
  }
410
+ // If error suggests a corrupted session, remove it from cache to prevent reuse
411
+ if (actualSessionId && isSessionCorruptionError(msg)) {
412
+ const corrupted = activeSessions.get(actualSessionId);
413
+ activeSessions.delete(actualSessionId);
414
+ sessionLastUsed.delete(actualSessionId);
415
+ if (corrupted) {
416
+ try {
417
+ corrupted.close();
418
+ }
419
+ catch { /* ignore */ }
420
+ }
421
+ log.warn(`Removed corrupted session ${actualSessionId} after error: ${msg}`);
422
+ callbacks.onSessionInvalid?.();
423
+ }
305
424
  callbacks.onError(msg);
306
425
  }
307
426
  };
308
- // 启动会话(不等待)
309
- runSession();
427
+ // 启动会话(不等待),catch 兜底防止 unhandledRejection 导致用户请求挂起
428
+ runSession().catch((err) => {
429
+ if (!runSettled) {
430
+ runSettled = true;
431
+ if (timeoutHandle)
432
+ clearTimeout(timeoutHandle);
433
+ const msg = err instanceof Error ? err.message : String(err);
434
+ log.error(`Unhandled runSession error: ${msg}`);
435
+ callbacks.onError(msg);
436
+ }
437
+ });
438
+ // 强制执行超时
439
+ if (options?.timeoutMs && options.timeoutMs > 0) {
440
+ timeoutHandle = setTimeout(() => {
441
+ if (!runSettled) {
442
+ log.warn(`Session timed out after ${options.timeoutMs}ms, aborting`);
443
+ abortController.abort();
444
+ // 立即中断 stream,不等下一条消息
445
+ if (currentStream) {
446
+ try {
447
+ currentStream.return?.();
448
+ }
449
+ catch { /* ignore */ }
450
+ }
451
+ runSettled = true;
452
+ callbacks.onError(`AI 响应超时(${Math.round(options.timeoutMs / 1000)}s),请重试`);
453
+ }
454
+ }, options.timeoutMs);
455
+ timeoutHandle.unref();
456
+ }
310
457
  return {
311
458
  abort: () => {
312
459
  log.info('Aborting session run');
313
460
  abortController.abort();
461
+ if (timeoutHandle)
462
+ clearTimeout(timeoutHandle);
463
+ // 立即中断 stream,不等下一条消息
464
+ if (currentStream) {
465
+ try {
466
+ currentStream.return?.();
467
+ }
468
+ catch { /* ignore */ }
469
+ }
314
470
  },
315
471
  };
316
472
  }
@@ -8,6 +8,7 @@ export declare const PAGE_TEXTS: {
8
8
  readonly heroKicker: "Local AI bridge";
9
9
  readonly langButton: "中文";
10
10
  readonly darkModeToggle: "Toggle dark mode";
11
+ readonly headerToolbarAria: "Bridge: validate, save, start, stop";
11
12
  readonly controlCenter: "Control center";
12
13
  readonly sidebarNoteTitle: "Local workflow";
13
14
  readonly sidebarNoteBody: "Configure at least one platform, save the config, then start the bridge from Service.";
@@ -40,7 +41,13 @@ export declare const PAGE_TEXTS: {
40
41
  readonly serviceIdleMeta: "Bridge has not been started yet";
41
42
  readonly listSeparator: ", ";
42
43
  readonly platformsTitle: "Platforms";
43
- readonly platformsHint: "Disabled platforms keep their saved values.";
44
+ readonly navConfigFiles: "Config files";
45
+ readonly configFilesTitle: "Config files (JSON)";
46
+ readonly configFilesHint: "Edit the files on disk directly. Each card has its own Save. Platform / AI forms and Service → Save config still apply.";
47
+ readonly openImConfigCardHint: "Full open-im configuration. Use Format, then Save. Invalid JSON is rejected.";
48
+ readonly claudeSettingsCardHint: "Claude SDK environment (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, etc.). Set API access here without shell exports.";
49
+ readonly claudeJsonShortcutHint: "Edit ~/.claude/settings.json in the sidebar «Config files» section below.";
50
+ readonly platformsHint: "Follow the setup checklist on Overview first. Disabled platforms still keep saved values.";
44
51
  readonly enabled: "Enabled";
45
52
  readonly readyState: "Ready";
46
53
  readonly setupRequired: "Setup required";
@@ -81,7 +88,7 @@ export declare const PAGE_TEXTS: {
81
88
  readonly optional: "Optional";
82
89
  readonly commaSeparatedIds: "Comma-separated IDs";
83
90
  readonly aiTitle: "AI Tooling";
84
- readonly aiHint: "";
91
+ readonly aiHint: "Pick the default tool first. Claude SDK keys: sidebar Config files → ~/.claude/settings.json. Codex / CodeBuddy need a CLI path here.";
85
92
  readonly claudeNote: "Claude credentials are still read from environment variables or ~/.claude/settings.json. This page manages local bridge config, not Claude account auth.";
86
93
  readonly aiCommonTitle: "Shared defaults";
87
94
  readonly aiCommonHint: "Choose the default AI tool first. Tool-specific fields are shown below.";
@@ -134,6 +141,36 @@ export declare const PAGE_TEXTS: {
134
141
  readonly saveBtn: "Save";
135
142
  readonly jsonValid: "Valid JSON";
136
143
  readonly jsonInvalid: "Invalid JSON: {error}";
144
+ readonly wizardTitle: "First-time setup";
145
+ readonly wizardStep1Title: "Connect an IM channel";
146
+ readonly wizardStep1Desc: "Enable at least one platform below and fill every required field. Use Check config when available.";
147
+ readonly wizardStep2Title: "Set the default AI tool";
148
+ readonly wizardStep2Desc: "Claude SDK: edit ~/.claude/settings.json under sidebar Config files. Codex / CodeBuddy: set the CLI path in the AI section.";
149
+ readonly wizardStep3Title: "Validate and save";
150
+ readonly wizardStep3Desc: "Click Validate, then Save config. Fix any errors shown in the banner above.";
151
+ readonly wizardStep4Title: "Start the bridge";
152
+ readonly wizardStep4Desc: "Open Service and click Start bridge. If it exits, read ~/.open-im/logs.";
153
+ readonly wizardStatusDone: "Done";
154
+ readonly wizardStatusTodo: "To do";
155
+ readonly wizardJumpPlatforms: "Open Platforms";
156
+ readonly wizardJumpAi: "Open AI";
157
+ readonly wizardJumpService: "Open Service";
158
+ readonly validationNoPlatformEnabled: "Enable at least one IM platform and fill its required credentials before saving.";
159
+ readonly validationPlatformIncomplete: "Platform \"{platform}\" is enabled but these required fields are empty: {fields}";
160
+ readonly validationAiCodexNoCli: "Default AI is Codex but Codex CLI path is empty. Set it under AI Tooling or change the default tool.";
161
+ readonly validationAiCodebuddyNoCli: "Default AI is CodeBuddy but CodeBuddy CLI path is empty. Set it under AI Tooling or change the default tool.";
162
+ readonly onboardingTitle: "Welcome to open-im";
163
+ readonly onboardingDismiss: "Got it";
164
+ readonly onboardingReadme: "README (setup)";
165
+ readonly onboardingBody: "<p><strong>open-im</strong> bridges Telegram, Feishu, QQ, WeWork, DingTalk, and WorkBuddy to Claude / Codex / CodeBuddy on your machine.</p><ol style=\"margin:12px 0 12px 18px;line-height:1.65\"><li>Pick <strong>one</strong> chat app in <strong>Platforms</strong> and paste credentials (see hints under each field).</li><li>Set the <strong>default AI</strong> under AI Tooling; for Claude SDK, put API keys in sidebar <strong>Config files</strong> → <strong>~/.claude/settings.json</strong>.</li><li>Edit <strong>~/.open-im/config.json</strong> in the same <strong>Config files</strong> section when you need raw JSON.</li><li>Use <strong>Validate</strong> then <strong>Save config</strong>, then <strong>Start bridge</strong> under Service.</li><li>CLI alternative: run <code style=\"background:var(--bg-tertiary);padding:2px 6px;border-radius:4px\">open-im init</code> in a terminal.</li></ol>";
166
+ readonly tipTelegramToken: "From Telegram, open <a href=\"https://t.me/BotFather\" target=\"_blank\" rel=\"noopener\">@BotFather</a> → /newbot → copy the token.";
167
+ readonly tipFeishuAppId: "Feishu Open Platform → your app → Credentials → App ID.";
168
+ readonly tipFeishuSecret: "Same page → App Secret (click show / reset if needed).";
169
+ readonly tipQqAppId: "<a href=\"https://bot.q.qq.com\" target=\"_blank\" rel=\"noopener\">QQ bot console</a> → your bot → App ID.";
170
+ readonly tipQqSecret: "Same console → App Secret.";
171
+ readonly tipWeworkCorp: "WeCom admin → app → view Corp ID (or smart-bot Bot ID) and Secret.";
172
+ readonly tipDingtalkClient: "<a href=\"https://open-dev.dingtalk.com\" target=\"_blank\" rel=\"noopener\">DingTalk Open Platform</a> → internal app → App Key & App Secret.";
173
+ readonly tipWorkbuddyToken: "Use terminal <code>open-im init</code> for WorkBuddy OAuth, or paste tokens from CodeBuddy login.";
137
174
  };
138
175
  readonly zh: {
139
176
  readonly pageTitle: "open-im 本地控制台";
@@ -144,6 +181,7 @@ export declare const PAGE_TEXTS: {
144
181
  readonly heroKicker: "本地 AI 桥接";
145
182
  readonly langButton: "EN";
146
183
  readonly darkModeToggle: "切换暗黑模式";
184
+ readonly headerToolbarAria: "桥接:校验、保存、启动、停止";
147
185
  readonly controlCenter: "控制中心";
148
186
  readonly sidebarNoteTitle: "本地工作流";
149
187
  readonly sidebarNoteBody: "至少配置一个平台,保存配置,然后在服务控制区启动桥接。";
@@ -174,7 +212,13 @@ export declare const PAGE_TEXTS: {
174
212
  readonly serviceIdleMeta: "桥接服务尚未启动";
175
213
  readonly listSeparator: "、";
176
214
  readonly platformsTitle: "平台配置";
177
- readonly platformsHint: "禁用的平台会保留已保存的值。";
215
+ readonly navConfigFiles: "配置文件";
216
+ readonly configFilesTitle: "配置文件(JSON)";
217
+ readonly configFilesHint: "直接编辑磁盘上的文件,每张卡片有独立保存按钮。平台 / AI 表单与服务区的「保存配置」仍然有效。";
218
+ readonly openImConfigCardHint: "open-im 完整配置。先格式化再保存;JSON 不合法时无法写入。";
219
+ readonly claudeSettingsCardHint: "Claude SDK 环境变量(ANTHROPIC_API_KEY、ANTHROPIC_BASE_URL、ANTHROPIC_MODEL 等)。在此配置 API,无需在终端 export。";
220
+ readonly claudeJsonShortcutHint: "提示:请在左侧栏「配置文件」分区编辑 ~/.claude/settings.json。";
221
+ readonly platformsHint: "建议先在概览页按引导步骤操作。禁用的平台仍保留已保存的值。";
178
222
  readonly enabled: "启用";
179
223
  readonly readyState: "已就绪";
180
224
  readonly setupRequired: "需要完成配置";
@@ -214,7 +258,7 @@ export declare const PAGE_TEXTS: {
214
258
  readonly optional: "可选";
215
259
  readonly commaSeparatedIds: "多个 ID 用逗号分隔";
216
260
  readonly aiTitle: "AI 工具配置";
217
- readonly aiHint: "";
261
+ readonly aiHint: "先选默认 AI。Claude SDK:左侧栏「配置文件」→ ~/.claude/settings.json。Codex / CodeBuddy 在此填 CLI 路径。";
218
262
  readonly claudeNote: "Claude 凭证仍然从环境变量或 ~/.claude/settings.json 读取。这个页面只管理本地桥接配置,不负责 Claude 账号登录。";
219
263
  readonly aiCommonTitle: "公共默认配置";
220
264
  readonly aiCommonHint: "先选择默认 AI 工具,下方再显示对应的工具专属配置。";
@@ -264,5 +308,35 @@ export declare const PAGE_TEXTS: {
264
308
  readonly saveBtn: "保存";
265
309
  readonly jsonValid: "JSON 有效";
266
310
  readonly jsonInvalid: "JSON 无效:{error}";
311
+ readonly wizardTitle: "首次使用引导";
312
+ readonly wizardStep1Title: "接入一个 IM 渠道";
313
+ readonly wizardStep1Desc: "在下方启用至少一个平台,并填写所有必填项。有条件时用「校验配置」测试凭证。";
314
+ readonly wizardStep2Title: "设置默认 AI 工具";
315
+ readonly wizardStep2Desc: "Claude SDK:在左侧栏「配置文件」编辑 ~/.claude/settings.json。Codex / CodeBuddy:在 AI 区填 CLI 路径。";
316
+ readonly wizardStep3Title: "校验并保存";
317
+ readonly wizardStep3Desc: "先点「校验」,再点「保存配置」。按顶部提示修复错误。";
318
+ readonly wizardStep4Title: "启动桥接";
319
+ readonly wizardStep4Desc: "到「服务」区点「启动桥接」。若马上退出,查 ~/.open-im/logs 日志。";
320
+ readonly wizardStatusDone: "已完成";
321
+ readonly wizardStatusTodo: "待完成";
322
+ readonly wizardJumpPlatforms: "打开平台配置";
323
+ readonly wizardJumpAi: "打开 AI 配置";
324
+ readonly wizardJumpService: "打开服务控制";
325
+ readonly validationNoPlatformEnabled: "保存前请至少启用一个 IM 平台,并填完必填凭证。";
326
+ readonly validationPlatformIncomplete: "平台「{platform}」已启用,但以下必填项为空:{fields}";
327
+ readonly validationAiCodexNoCli: "默认 AI 为 Codex,但 Codex CLI 路径为空。请在 AI 区填写,或改默认工具。";
328
+ readonly validationAiCodebuddyNoCli: "默认 AI 为 CodeBuddy,但 CodeBuddy CLI 路径为空。请在 AI 区填写,或改默认工具。";
329
+ readonly onboardingTitle: "欢迎使用 open-im";
330
+ readonly onboardingDismiss: "知道了";
331
+ readonly onboardingReadme: "README(部署说明)";
332
+ readonly onboardingBody: "<p><strong>open-im</strong> 在本机把 Telegram、飞书、QQ、企微、钉钉、WorkBuddy 等渠道连到 Claude / Codex / CodeBuddy。</p><ol style=\"margin:12px 0 12px 18px;line-height:1.65\"><li>在<strong>平台配置</strong>选一个聊天应用,按字段下方提示填凭证。</li><li>在<strong>AI 工具配置</strong>设默认 AI;用 Claude SDK 时在左侧<strong>配置文件</strong>分区编辑<strong>~/.claude/settings.json</strong>。</li><li>同一<strong>配置文件</strong>分区可编辑<strong>~/.open-im/config.json</strong>。</li><li>先<strong>校验</strong>再<strong>保存配置</strong>,然后到<strong>服务</strong>启动桥接。</li><li>也可在终端运行 <code style=\"background:var(--bg-tertiary);padding:2px 6px;border-radius:4px\">open-im init</code> 交互配置。</li></ol>";
333
+ readonly tipTelegramToken: "在 Telegram 搜 <a href=\"https://t.me/BotFather\" target=\"_blank\" rel=\"noopener\">@BotFather</a>,发 /newbot 创建机器人后复制 Token。";
334
+ readonly tipFeishuAppId: "飞书开放平台 → 应用 → 凭证与基础信息 → App ID。";
335
+ readonly tipFeishuSecret: "同一页 App Secret(可重置后查看)。";
336
+ readonly tipQqAppId: "<a href=\"https://bot.q.qq.com\" target=\"_blank\" rel=\"noopener\">QQ 开放平台</a> → 机器人 → App ID。";
337
+ readonly tipQqSecret: "同一处获取 App Secret。";
338
+ readonly tipWeworkCorp: "企业微信管理后台 → 应用 → 查省 Corp ID / 智能机器人 Bot ID 与 Secret。";
339
+ readonly tipDingtalkClient: "<a href=\"https://open-dev.dingtalk.com\" target=\"_blank\" rel=\"noopener\">钉钉开放平台</a> → 企业内部应用 → AppKey / AppSecret。";
340
+ readonly tipWorkbuddyToken: "建议在终端运行 <code>open-im init</code> 完成 WorkBuddy 授权;或粘贴 CodeBuddy 登录后的 Token。";
267
341
  };
268
342
  };
@@ -8,6 +8,7 @@ export const PAGE_TEXTS = {
8
8
  heroKicker: "Local AI bridge",
9
9
  langButton: "\u4e2d\u6587",
10
10
  darkModeToggle: "Toggle dark mode",
11
+ headerToolbarAria: "Bridge: validate, save, start, stop",
11
12
  controlCenter: "Control center",
12
13
  sidebarNoteTitle: "Local workflow",
13
14
  sidebarNoteBody: "Configure at least one platform, save the config, then start the bridge from Service.",
@@ -40,7 +41,13 @@ export const PAGE_TEXTS = {
40
41
  serviceIdleMeta: "Bridge has not been started yet",
41
42
  listSeparator: ", ",
42
43
  platformsTitle: "Platforms",
43
- platformsHint: "Disabled platforms keep their saved values.",
44
+ navConfigFiles: "Config files",
45
+ configFilesTitle: "Config files (JSON)",
46
+ configFilesHint: "Edit the files on disk directly. Each card has its own Save. Platform / AI forms and Service → Save config still apply.",
47
+ openImConfigCardHint: "Full open-im configuration. Use Format, then Save. Invalid JSON is rejected.",
48
+ claudeSettingsCardHint: "Claude SDK environment (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, etc.). Set API access here without shell exports.",
49
+ claudeJsonShortcutHint: "Edit ~/.claude/settings.json in the sidebar «Config files» section below.",
50
+ platformsHint: "Follow the setup checklist on Overview first. Disabled platforms still keep saved values.",
44
51
  enabled: "Enabled",
45
52
  readyState: "Ready",
46
53
  setupRequired: "Setup required",
@@ -81,7 +88,7 @@ export const PAGE_TEXTS = {
81
88
  optional: "Optional",
82
89
  commaSeparatedIds: "Comma-separated IDs",
83
90
  aiTitle: "AI Tooling",
84
- aiHint: "",
91
+ aiHint: "Pick the default tool first. Claude SDK keys: sidebar Config files → ~/.claude/settings.json. Codex / CodeBuddy need a CLI path here.",
85
92
  claudeNote: "Claude credentials are still read from environment variables or ~/.claude/settings.json. This page manages local bridge config, not Claude account auth.",
86
93
  aiCommonTitle: "Shared defaults",
87
94
  aiCommonHint: "Choose the default AI tool first. Tool-specific fields are shown below.",
@@ -134,6 +141,36 @@ export const PAGE_TEXTS = {
134
141
  saveBtn: "Save",
135
142
  jsonValid: "Valid JSON",
136
143
  jsonInvalid: "Invalid JSON: {error}",
144
+ wizardTitle: "First-time setup",
145
+ wizardStep1Title: "Connect an IM channel",
146
+ wizardStep1Desc: "Enable at least one platform below and fill every required field. Use Check config when available.",
147
+ wizardStep2Title: "Set the default AI tool",
148
+ wizardStep2Desc: "Claude SDK: edit ~/.claude/settings.json under sidebar Config files. Codex / CodeBuddy: set the CLI path in the AI section.",
149
+ wizardStep3Title: "Validate and save",
150
+ wizardStep3Desc: "Click Validate, then Save config. Fix any errors shown in the banner above.",
151
+ wizardStep4Title: "Start the bridge",
152
+ wizardStep4Desc: "Open Service and click Start bridge. If it exits, read ~/.open-im/logs.",
153
+ wizardStatusDone: "Done",
154
+ wizardStatusTodo: "To do",
155
+ wizardJumpPlatforms: "Open Platforms",
156
+ wizardJumpAi: "Open AI",
157
+ wizardJumpService: "Open Service",
158
+ validationNoPlatformEnabled: "Enable at least one IM platform and fill its required credentials before saving.",
159
+ validationPlatformIncomplete: "Platform \"{platform}\" is enabled but these required fields are empty: {fields}",
160
+ validationAiCodexNoCli: "Default AI is Codex but Codex CLI path is empty. Set it under AI Tooling or change the default tool.",
161
+ validationAiCodebuddyNoCli: "Default AI is CodeBuddy but CodeBuddy CLI path is empty. Set it under AI Tooling or change the default tool.",
162
+ onboardingTitle: "Welcome to open-im",
163
+ onboardingDismiss: "Got it",
164
+ onboardingReadme: "README (setup)",
165
+ onboardingBody: "<p><strong>open-im</strong> bridges Telegram, Feishu, QQ, WeWork, DingTalk, and WorkBuddy to Claude / Codex / CodeBuddy on your machine.</p><ol style=\"margin:12px 0 12px 18px;line-height:1.65\"><li>Pick <strong>one</strong> chat app in <strong>Platforms</strong> and paste credentials (see hints under each field).</li><li>Set the <strong>default AI</strong> under AI Tooling; for Claude SDK, put API keys in sidebar <strong>Config files</strong> → <strong>~/.claude/settings.json</strong>.</li><li>Edit <strong>~/.open-im/config.json</strong> in the same <strong>Config files</strong> section when you need raw JSON.</li><li>Use <strong>Validate</strong> then <strong>Save config</strong>, then <strong>Start bridge</strong> under Service.</li><li>CLI alternative: run <code style=\"background:var(--bg-tertiary);padding:2px 6px;border-radius:4px\">open-im init</code> in a terminal.</li></ol>",
166
+ tipTelegramToken: 'From Telegram, open <a href="https://t.me/BotFather" target="_blank" rel="noopener">@BotFather</a> → /newbot → copy the token.',
167
+ tipFeishuAppId: "Feishu Open Platform → your app → Credentials → App ID.",
168
+ tipFeishuSecret: "Same page → App Secret (click show / reset if needed).",
169
+ tipQqAppId: '<a href="https://bot.q.qq.com" target="_blank" rel="noopener">QQ bot console</a> → your bot → App ID.',
170
+ tipQqSecret: "Same console → App Secret.",
171
+ tipWeworkCorp: "WeCom admin → app → view Corp ID (or smart-bot Bot ID) and Secret.",
172
+ tipDingtalkClient: '<a href="https://open-dev.dingtalk.com" target="_blank" rel="noopener">DingTalk Open Platform</a> → internal app → App Key & App Secret.',
173
+ tipWorkbuddyToken: 'Use terminal <code>open-im init</code> for WorkBuddy OAuth, or paste tokens from CodeBuddy login.',
137
174
  },
138
175
  zh: {
139
176
  pageTitle: "open-im \u672c\u5730\u63a7\u5236\u53f0",
@@ -144,6 +181,7 @@ export const PAGE_TEXTS = {
144
181
  heroKicker: "\u672c\u5730 AI \u6865\u63a5",
145
182
  langButton: "EN",
146
183
  darkModeToggle: "\u5207\u6362\u6697\u9ed1\u6a21\u5f0f",
184
+ headerToolbarAria: "\u6865\u63a5\uff1a\u6821\u9a8c\u3001\u4fdd\u5b58\u3001\u542f\u52a8\u3001\u505c\u6b62",
147
185
  controlCenter: "\u63a7\u5236\u4e2d\u5fc3",
148
186
  sidebarNoteTitle: "\u672c\u5730\u5de5\u4f5c\u6d41",
149
187
  sidebarNoteBody: "\u81f3\u5c11\u914d\u7f6e\u4e00\u4e2a\u5e73\u53f0\uff0c\u4fdd\u5b58\u914d\u7f6e\uff0c\u7136\u540e\u5728\u670d\u52a1\u63a7\u5236\u533a\u542f\u52a8\u6865\u63a5\u3002",
@@ -174,7 +212,13 @@ export const PAGE_TEXTS = {
174
212
  serviceIdleMeta: "\u6865\u63a5\u670d\u52a1\u5c1a\u672a\u542f\u52a8",
175
213
  listSeparator: "\u3001",
176
214
  platformsTitle: "\u5e73\u53f0\u914d\u7f6e",
177
- platformsHint: "\u7981\u7528\u7684\u5e73\u53f0\u4f1a\u4fdd\u7559\u5df2\u4fdd\u5b58\u7684\u503c\u3002",
215
+ navConfigFiles: "\u914d\u7f6e\u6587\u4ef6",
216
+ configFilesTitle: "\u914d\u7f6e\u6587\u4ef6\uff08JSON\uff09",
217
+ configFilesHint: "\u76f4\u63a5\u7f16\u8f91\u78c1\u76d8\u4e0a\u7684\u6587\u4ef6\uff0c\u6bcf\u5f20\u5361\u7247\u6709\u72ec\u7acb\u4fdd\u5b58\u6309\u94ae\u3002\u5e73\u53f0 / AI \u8868\u5355\u4e0e\u670d\u52a1\u533a\u7684\u300c\u4fdd\u5b58\u914d\u7f6e\u300d\u4ecd\u7136\u6709\u6548\u3002",
218
+ openImConfigCardHint: "open-im \u5b8c\u6574\u914d\u7f6e\u3002\u5148\u683c\u5f0f\u5316\u518d\u4fdd\u5b58\uff1bJSON \u4e0d\u5408\u6cd5\u65f6\u65e0\u6cd5\u5199\u5165\u3002",
219
+ claudeSettingsCardHint: "Claude SDK \u73af\u5883\u53d8\u91cf\uff08ANTHROPIC_API_KEY\u3001ANTHROPIC_BASE_URL\u3001ANTHROPIC_MODEL \u7b49\uff09\u3002\u5728\u6b64\u914d\u7f6e API\uff0c\u65e0\u9700\u5728\u7ec8\u7aef export\u3002",
220
+ claudeJsonShortcutHint: "\u63d0\u793a\uff1a\u8bf7\u5728\u5de6\u4fa7\u680f\u300c\u914d\u7f6e\u6587\u4ef6\u300d\u5206\u533a\u7f16\u8f91 ~/.claude/settings.json\u3002",
221
+ platformsHint: "\u5efa\u8bae\u5148\u5728\u6982\u89c8\u9875\u6309\u5f15\u5bfc\u6b65\u9aa4\u64cd\u4f5c\u3002\u7981\u7528\u7684\u5e73\u53f0\u4ecd\u4fdd\u7559\u5df2\u4fdd\u5b58\u7684\u503c\u3002",
178
222
  enabled: "\u542f\u7528",
179
223
  readyState: "\u5df2\u5c31\u7eea",
180
224
  setupRequired: "\u9700\u8981\u5b8c\u6210\u914d\u7f6e",
@@ -214,7 +258,7 @@ export const PAGE_TEXTS = {
214
258
  optional: "\u53ef\u9009",
215
259
  commaSeparatedIds: "\u591a\u4e2a ID \u7528\u9017\u53f7\u5206\u9694",
216
260
  aiTitle: "AI \u5de5\u5177\u914d\u7f6e",
217
- aiHint: "",
261
+ aiHint: "\u5148\u9009\u9ed8\u8ba4 AI\u3002Claude SDK\uff1a\u5de6\u4fa7\u680f\u300c\u914d\u7f6e\u6587\u4ef6\u300d\u2192 ~/.claude/settings.json\u3002Codex / CodeBuddy \u5728\u6b64\u586b CLI \u8def\u5f84\u3002",
218
262
  claudeNote: "Claude \u51ed\u8bc1\u4ecd\u7136\u4ece\u73af\u5883\u53d8\u91cf\u6216 ~/.claude/settings.json \u8bfb\u53d6\u3002\u8fd9\u4e2a\u9875\u9762\u53ea\u7ba1\u7406\u672c\u5730\u6865\u63a5\u914d\u7f6e\uff0c\u4e0d\u8d1f\u8d23 Claude \u8d26\u53f7\u767b\u5f55\u3002",
219
263
  aiCommonTitle: "\u516c\u5171\u9ed8\u8ba4\u914d\u7f6e",
220
264
  aiCommonHint: "\u5148\u9009\u62e9\u9ed8\u8ba4 AI \u5de5\u5177\uff0c\u4e0b\u65b9\u518d\u663e\u793a\u5bf9\u5e94\u7684\u5de5\u5177\u4e13\u5c5e\u914d\u7f6e\u3002",
@@ -264,5 +308,35 @@ export const PAGE_TEXTS = {
264
308
  saveBtn: "\u4fdd\u5b58",
265
309
  jsonValid: "JSON \u6709\u6548",
266
310
  jsonInvalid: "JSON \u65e0\u6548\uff1a{error}",
311
+ wizardTitle: "\u9996\u6b21\u4f7f\u7528\u5f15\u5bfc",
312
+ wizardStep1Title: "\u63a5\u5165\u4e00\u4e2a IM \u6e20\u9053",
313
+ wizardStep1Desc: "\u5728\u4e0b\u65b9\u542f\u7528\u81f3\u5c11\u4e00\u4e2a\u5e73\u53f0\uff0c\u5e76\u586b\u5199\u6240\u6709\u5fc5\u586b\u9879\u3002\u6709\u6761\u4ef6\u65f6\u7528\u300c\u6821\u9a8c\u914d\u7f6e\u300d\u6d4b\u8bd5\u51ed\u8bc1\u3002",
314
+ wizardStep2Title: "\u8bbe\u7f6e\u9ed8\u8ba4 AI \u5de5\u5177",
315
+ wizardStep2Desc: "Claude SDK\uff1a\u5728\u5de6\u4fa7\u680f\u300c\u914d\u7f6e\u6587\u4ef6\u300d\u7f16\u8f91 ~/.claude/settings.json\u3002Codex / CodeBuddy\uff1a\u5728 AI \u533a\u586b CLI \u8def\u5f84\u3002",
316
+ wizardStep3Title: "\u6821\u9a8c\u5e76\u4fdd\u5b58",
317
+ wizardStep3Desc: "\u5148\u70b9\u300c\u6821\u9a8c\u300d\uff0c\u518d\u70b9\u300c\u4fdd\u5b58\u914d\u7f6e\u300d\u3002\u6309\u9876\u90e8\u63d0\u793a\u4fee\u590d\u9519\u8bef\u3002",
318
+ wizardStep4Title: "\u542f\u52a8\u6865\u63a5",
319
+ wizardStep4Desc: "\u5230\u300c\u670d\u52a1\u300d\u533a\u70b9\u300c\u542f\u52a8\u6865\u63a5\u300d\u3002\u82e5\u9a6c\u4e0a\u9000\u51fa\uff0c\u67e5 ~/.open-im/logs \u65e5\u5fd7\u3002",
320
+ wizardStatusDone: "\u5df2\u5b8c\u6210",
321
+ wizardStatusTodo: "\u5f85\u5b8c\u6210",
322
+ wizardJumpPlatforms: "\u6253\u5f00\u5e73\u53f0\u914d\u7f6e",
323
+ wizardJumpAi: "\u6253\u5f00 AI \u914d\u7f6e",
324
+ wizardJumpService: "\u6253\u5f00\u670d\u52a1\u63a7\u5236",
325
+ validationNoPlatformEnabled: "\u4fdd\u5b58\u524d\u8bf7\u81f3\u5c11\u542f\u7528\u4e00\u4e2a IM \u5e73\u53f0\uff0c\u5e76\u586b\u5b8c\u5fc5\u586b\u51ed\u8bc1\u3002",
326
+ validationPlatformIncomplete: "\u5e73\u53f0\u300c{platform}\u300d\u5df2\u542f\u7528\uff0c\u4f46\u4ee5\u4e0b\u5fc5\u586b\u9879\u4e3a\u7a7a\uff1a{fields}",
327
+ validationAiCodexNoCli: "\u9ed8\u8ba4 AI \u4e3a Codex\uff0c\u4f46 Codex CLI \u8def\u5f84\u4e3a\u7a7a\u3002\u8bf7\u5728 AI \u533a\u586b\u5199\uff0c\u6216\u6539\u9ed8\u8ba4\u5de5\u5177\u3002",
328
+ validationAiCodebuddyNoCli: "\u9ed8\u8ba4 AI \u4e3a CodeBuddy\uff0c\u4f46 CodeBuddy CLI \u8def\u5f84\u4e3a\u7a7a\u3002\u8bf7\u5728 AI \u533a\u586b\u5199\uff0c\u6216\u6539\u9ed8\u8ba4\u5de5\u5177\u3002",
329
+ onboardingTitle: "\u6b22\u8fce\u4f7f\u7528 open-im",
330
+ onboardingDismiss: "\u77e5\u9053\u4e86",
331
+ onboardingReadme: "README\uff08\u90e8\u7f72\u8bf4\u660e\uff09",
332
+ onboardingBody: "<p><strong>open-im</strong> \u5728\u672c\u673a\u628a Telegram\u3001\u98de\u4e66\u3001QQ\u3001\u4f01\u5fae\u3001\u9489\u9489\u3001WorkBuddy \u7b49\u6e20\u9053\u8fde\u5230 Claude / Codex / CodeBuddy\u3002</p><ol style=\"margin:12px 0 12px 18px;line-height:1.65\"><li>\u5728<strong>\u5e73\u53f0\u914d\u7f6e</strong>\u9009\u4e00\u4e2a\u804a\u5929\u5e94\u7528\uff0c\u6309\u5b57\u6bb5\u4e0b\u65b9\u63d0\u793a\u586b\u51ed\u8bc1\u3002</li><li>\u5728<strong>AI \u5de5\u5177\u914d\u7f6e</strong>\u8bbe\u9ed8\u8ba4 AI\uff1b\u7528 Claude SDK \u65f6\u5728\u5de6\u4fa7<strong>\u914d\u7f6e\u6587\u4ef6</strong>\u5206\u533a\u7f16\u8f91<strong>~/.claude/settings.json</strong>\u3002</li><li>\u540c\u4e00<strong>\u914d\u7f6e\u6587\u4ef6</strong>\u5206\u533a\u53ef\u7f16\u8f91<strong>~/.open-im/config.json</strong>\u3002</li><li>\u5148<strong>\u6821\u9a8c</strong>\u518d<strong>\u4fdd\u5b58\u914d\u7f6e</strong>\uff0c\u7136\u540e\u5230<strong>\u670d\u52a1</strong>\u542f\u52a8\u6865\u63a5\u3002</li><li>\u4e5f\u53ef\u5728\u7ec8\u7aef\u8fd0\u884c <code style=\"background:var(--bg-tertiary);padding:2px 6px;border-radius:4px\">open-im init</code> \u4ea4\u4e92\u914d\u7f6e\u3002</li></ol>",
333
+ tipTelegramToken: '\u5728 Telegram \u641c <a href="https://t.me/BotFather" target="_blank" rel="noopener">@BotFather</a>\uff0c\u53d1 /newbot \u521b\u5efa\u673a\u5668\u4eba\u540e\u590d\u5236 Token\u3002',
334
+ tipFeishuAppId: "\u98de\u4e66\u5f00\u653e\u5e73\u53f0 \u2192 \u5e94\u7528 \u2192 \u51ed\u8bc1\u4e0e\u57fa\u7840\u4fe1\u606f \u2192 App ID\u3002",
335
+ tipFeishuSecret: "\u540c\u4e00\u9875 App Secret\uff08\u53ef\u91cd\u7f6e\u540e\u67e5\u770b\uff09\u3002",
336
+ tipQqAppId: '<a href="https://bot.q.qq.com" target="_blank" rel="noopener">QQ \u5f00\u653e\u5e73\u53f0</a> \u2192 \u673a\u5668\u4eba \u2192 App ID\u3002',
337
+ tipQqSecret: "\u540c\u4e00\u5904\u83b7\u53d6 App Secret\u3002",
338
+ tipWeworkCorp: "\u4f01\u4e1a\u5fae\u4fe1\u7ba1\u7406\u540e\u53f0 \u2192 \u5e94\u7528 \u2192 \u67e5\u7701 Corp ID / \u667a\u80fd\u673a\u5668\u4eba Bot ID \u4e0e Secret\u3002",
339
+ tipDingtalkClient: '<a href="https://open-dev.dingtalk.com" target="_blank" rel="noopener">\u9489\u9489\u5f00\u653e\u5e73\u53f0</a> \u2192 \u4f01\u4e1a\u5185\u90e8\u5e94\u7528 \u2192 AppKey / AppSecret\u3002',
340
+ tipWorkbuddyToken: "\u5efa\u8bae\u5728\u7ec8\u7aef\u8fd0\u884c <code>open-im init</code> \u5b8c\u6210 WorkBuddy \u6388\u6743\uff1b\u6216\u7c98\u8d34 CodeBuddy \u767b\u5f55\u540e\u7684 Token\u3002",
267
341
  }
268
342
  };