@wu529778790/open-im 1.8.1-beta.0 → 1.8.1-beta.10

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 (40) hide show
  1. package/dist/adapters/claude-sdk-adapter.js +43 -1
  2. package/dist/channels/capabilities.js +5 -0
  3. package/dist/commands/handler.d.ts +1 -1
  4. package/dist/commands/handler.js +3 -1
  5. package/dist/config-web-page-i18n.d.ts +19 -3
  6. package/dist/config-web-page-i18n.js +19 -3
  7. package/dist/config-web-page-script.js +35 -7
  8. package/dist/config-web-page-template.js +72 -48
  9. package/dist/config-web.js +42 -0
  10. package/dist/config.d.ts +25 -1
  11. package/dist/config.js +46 -0
  12. package/dist/constants.d.ts +2 -0
  13. package/dist/constants.js +2 -0
  14. package/dist/dingtalk/event-handler.js +12 -1
  15. package/dist/feishu/event-handler.js +115 -1
  16. package/dist/index.js +17 -2
  17. package/dist/qq/event-handler.js +12 -1
  18. package/dist/setup.js +2 -1
  19. package/dist/shared/active-chats.d.ts +2 -2
  20. package/dist/shared/ai-task.js +13 -1
  21. package/dist/telegram/client.js +24 -4
  22. package/dist/telegram/event-handler.js +4 -0
  23. package/dist/wechat/event-handler.js +4 -0
  24. package/dist/wework/client.js +4 -8
  25. package/dist/wework/event-handler.js +12 -1
  26. package/dist/workbuddy/centrifuge-client.d.ts +74 -0
  27. package/dist/workbuddy/centrifuge-client.js +272 -0
  28. package/dist/workbuddy/client.d.ts +27 -0
  29. package/dist/workbuddy/client.js +162 -0
  30. package/dist/workbuddy/event-handler.d.ts +11 -0
  31. package/dist/workbuddy/event-handler.js +118 -0
  32. package/dist/workbuddy/index.d.ts +8 -0
  33. package/dist/workbuddy/index.js +8 -0
  34. package/dist/workbuddy/message-sender.d.ts +16 -0
  35. package/dist/workbuddy/message-sender.js +51 -0
  36. package/dist/workbuddy/oauth.d.ts +114 -0
  37. package/dist/workbuddy/oauth.js +310 -0
  38. package/dist/workbuddy/types.d.ts +86 -0
  39. package/dist/workbuddy/types.js +4 -0
  40. package/package.json +3 -1
@@ -39,11 +39,14 @@ function isResult(msg) {
39
39
  */
40
40
  async function getOrCreateSession(sessionId, _workDir, // 保留参数以备将来使用
41
41
  model, permissionMode) {
42
+ const resolvedModel = model?.trim() || 'claude-opus-4-5';
42
43
  const sessionOptions = {
43
- model: model || 'claude-opus-4-5',
44
+ model: resolvedModel,
44
45
  permissionMode,
45
46
  // 可以添加其他选项,如 hooks, allowedTools 等
46
47
  };
48
+ const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
49
+ log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl}`);
47
50
  let session;
48
51
  if (sessionId) {
49
52
  // 尝试恢复已有会话
@@ -96,9 +99,19 @@ export class ClaudeSDKAdapter {
96
99
  activeSessions.clear();
97
100
  }
98
101
  run(prompt, sessionId, workDir, callbacks, options) {
102
+ log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
99
103
  const abortController = new AbortController();
100
104
  let streamClosed = false;
101
105
  let actualSessionId;
106
+ let runSettled = false;
107
+ let timeoutId = null;
108
+ const timeoutMs = options?.timeoutMs ?? 600_000;
109
+ const clearRunTimeout = () => {
110
+ if (timeoutId !== null) {
111
+ clearTimeout(timeoutId);
112
+ timeoutId = null;
113
+ }
114
+ };
102
115
  const permissionMode = options?.skipPermissions
103
116
  ? 'bypassPermissions'
104
117
  : options?.permissionMode === 'acceptEdits'
@@ -107,6 +120,15 @@ export class ClaudeSDKAdapter {
107
120
  ? 'plan'
108
121
  : 'default';
109
122
  const runSession = async () => {
123
+ timeoutId = setTimeout(() => {
124
+ if (runSettled)
125
+ return;
126
+ runSettled = true;
127
+ clearRunTimeout();
128
+ log.warn(`[ClaudeSDK] Request timeout after ${timeoutMs}ms`);
129
+ abortController.abort();
130
+ callbacks.onError(`请求超时(${Math.round(timeoutMs / 1000)}s),请重试或缩短问题。`);
131
+ }, timeoutMs);
110
132
  try {
111
133
  // 检查环境变量
112
134
  const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
@@ -115,6 +137,7 @@ export class ClaudeSDKAdapter {
115
137
  log.warn('Claude SDK: No API credentials found in environment variables');
116
138
  }
117
139
  log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
140
+ log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
118
141
  // 获取或创建会话
119
142
  const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
120
143
  // 发送用户消息
@@ -181,6 +204,8 @@ export class ClaudeSDKAdapter {
181
204
  log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
182
205
  // 检查会话错误
183
206
  if (!success) {
207
+ runSettled = true;
208
+ clearRunTimeout();
184
209
  const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
185
210
  if (noConvErr) {
186
211
  log.warn(`Session ${actualSessionId} not found, may need to create new one`);
@@ -207,6 +232,8 @@ export class ClaudeSDKAdapter {
207
232
  result.accumulated = accumulated;
208
233
  result.result = accumulated;
209
234
  }
235
+ runSettled = true;
236
+ clearRunTimeout();
210
237
  callbacks.onComplete(result);
211
238
  return;
212
239
  }
@@ -214,6 +241,8 @@ export class ClaudeSDKAdapter {
214
241
  // 如果流正常结束但没有收到 result 消息
215
242
  if (!streamClosed && accumulated) {
216
243
  log.info('Stream ended without result message, using accumulated text');
244
+ runSettled = true;
245
+ clearRunTimeout();
217
246
  callbacks.onComplete({
218
247
  success: true,
219
248
  result: accumulated,
@@ -233,14 +262,27 @@ export class ClaudeSDKAdapter {
233
262
  catch (err) {
234
263
  if (abortController.signal.aborted) {
235
264
  log.info('Session run aborted');
265
+ clearRunTimeout();
266
+ // 清理 pending tempId
267
+ if (actualSessionId?.startsWith('pending-')) {
268
+ activeSessions.delete(actualSessionId);
269
+ log.info(`Cleaned up pending session: ${actualSessionId}`);
270
+ }
236
271
  return;
237
272
  }
273
+ runSettled = true;
274
+ clearRunTimeout();
238
275
  const errorObj = err;
239
276
  const msg = errorObj.message || String(err);
240
277
  log.error(`Claude SDK V2 error: ${msg}`);
241
278
  if (errorObj.stack) {
242
279
  log.error(`Error stack: ${errorObj.stack}`);
243
280
  }
281
+ // 清理 pending tempId(session 在获取真实 ID 前就失败了)
282
+ if (actualSessionId?.startsWith('pending-')) {
283
+ activeSessions.delete(actualSessionId);
284
+ log.info(`Cleaned up pending session after error: ${actualSessionId}`);
285
+ }
244
286
  callbacks.onError(msg);
245
287
  }
246
288
  };
@@ -5,6 +5,7 @@ const PLATFORM_LABELS = {
5
5
  wechat: "微信",
6
6
  wework: "企业微信",
7
7
  dingtalk: "钉钉",
8
+ workbuddy: "WorkBuddy",
8
9
  };
9
10
  export const CHANNEL_CAPABILITIES = {
10
11
  telegram: {
@@ -31,6 +32,10 @@ export const CHANNEL_CAPABILITIES = {
31
32
  inbound: { text: "native", image: "fallback", file: "fallback", voice: "fallback", video: "fallback" },
32
33
  outbound: { streamEdit: "native", streamPush: "fallback", image: "fallback", card: "native", typing: "native" },
33
34
  },
35
+ workbuddy: {
36
+ inbound: { text: "native", image: "none", file: "none", voice: "none", video: "none" },
37
+ outbound: { streamEdit: "none", streamPush: "none", image: "none", card: "none", typing: "none" },
38
+ },
34
39
  };
35
40
  function listPreferredPlatforms(kind) {
36
41
  return Object.entries(CHANNEL_CAPABILITIES)
@@ -18,7 +18,7 @@ export type ClaudeRequestHandler = (userId: string, chatId: string, prompt: stri
18
18
  export declare class CommandHandler {
19
19
  private deps;
20
20
  constructor(deps: CommandHandlerDeps);
21
- dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
21
+ dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
22
22
  private getClearHistoryHint;
23
23
  private handleHelp;
24
24
  private handleNew;
@@ -40,7 +40,9 @@ export class CommandHandler {
40
40
  ? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
41
41
  : platform === 'dingtalk'
42
42
  ? '💡 提示:如需清除本对话的历史消息,请在钉钉中清空聊天记录'
43
- : '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
43
+ : platform === 'workbuddy'
44
+ ? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
45
+ : '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
44
46
  }
45
47
  async handleHelp(chatId, platform) {
46
48
  const help = [
@@ -50,6 +50,7 @@ export declare const PAGE_TEXTS: {
50
50
  readonly qqSummary: "App ID and secret for bot access.";
51
51
  readonly weworkSummary: "Corp ID and secret for enterprise delivery.";
52
52
  readonly dingtalkSummary: "Client credentials plus optional card template.";
53
+ readonly workbuddySummary: "CodeBuddy OAuth for WeChat customer service.";
53
54
  readonly platformCredentialsTitle: "Credentials";
54
55
  readonly platformAccessTitle: "Routing and access";
55
56
  readonly platformTestNote: "Checks required credentials against the platform.";
@@ -70,8 +71,13 @@ export declare const PAGE_TEXTS: {
70
71
  readonly clientId: "Client ID / AppKey";
71
72
  readonly clientSecret: "Client Secret / AppSecret";
72
73
  readonly dingtalkHelp: "Get credentials: Create an enterprise internal app on <a href=\"https://open-dev.dingtalk.com/\" target=\"_blank\">DingTalk Open Platform</a>, enable Stream Mode, and get Client ID / Client Secret";
74
+ readonly workbuddyHelp: "Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. WorkBuddy connects WeChat customer service through Centrifuge WebSocket.";
73
75
  readonly secret: "Secret";
74
76
  readonly cardTemplateId: "Card template ID";
77
+ readonly workbuddyAccessToken: "Access Token";
78
+ readonly workbuddyRefreshToken: "Refresh Token";
79
+ readonly workbuddyUserId: "User ID";
80
+ readonly workbuddyBaseUrl: "Base URL";
75
81
  readonly optional: "Optional";
76
82
  readonly commaSeparatedIds: "Comma-separated IDs";
77
83
  readonly aiTitle: "AI Tooling";
@@ -120,10 +126,12 @@ export declare const PAGE_TEXTS: {
120
126
  readonly stopOk: "Bridge stopped.";
121
127
  readonly configEditorTitle: "Config Editor";
122
128
  readonly configEditorHint: "Edit ~/.open-im/config.json directly";
123
- readonly configJson: "Configuration (JSON)";
129
+ readonly claudeSettingsLabel: "~/.claude/settings.json";
130
+ readonly configJson: "~/.open-im/config.json";
124
131
  readonly configJsonHint: "Edit the configuration JSON. Changes will be saved when you click \"Save Config\" in the Service section.";
125
132
  readonly formatJson: "Format";
126
133
  readonly resetJson: "Reset";
134
+ readonly saveBtn: "Save";
127
135
  readonly jsonValid: "Valid JSON";
128
136
  readonly jsonInvalid: "Invalid JSON: {error}";
129
137
  };
@@ -176,6 +184,7 @@ export declare const PAGE_TEXTS: {
176
184
  readonly qqSummary: "QQ 机器人 App ID 与 Secret。";
177
185
  readonly weworkSummary: "企业微信 Corp ID 与 Secret。";
178
186
  readonly dingtalkSummary: "钉钉 Client 凭证,可选配置卡片模板 ID。";
187
+ readonly workbuddySummary: "CodeBuddy OAuth 连接微信客服。";
179
188
  readonly platformCredentialsTitle: "凭证";
180
189
  readonly platformAccessTitle: "路由与访问";
181
190
  readonly platformTestNote: "只会检查该平台的必填凭证是否可用。";
@@ -196,7 +205,12 @@ export declare const PAGE_TEXTS: {
196
205
  readonly clientId: "Client ID / AppKey";
197
206
  readonly clientSecret: "Client Secret / AppSecret";
198
207
  readonly dingtalkHelp: "获取凭证:在 <a href=\"https://open-dev.dingtalk.com/\" target=\"_blank\">钉钉开放平台</a> 创建企业内部应用,启用 Stream Mode,并拿到 Client ID / Client Secret";
208
+ readonly workbuddyHelp: "获取凭证:通过 CodeBuddy OAuth 登录获取 access/refresh token。WorkBuddy 通过 Centrifuge WebSocket 连接微信客服。";
199
209
  readonly cardTemplateId: "卡片模板 ID";
210
+ readonly workbuddyAccessToken: "Access Token";
211
+ readonly workbuddyRefreshToken: "Refresh Token";
212
+ readonly workbuddyUserId: "User ID";
213
+ readonly workbuddyBaseUrl: "基础 URL";
200
214
  readonly optional: "可选";
201
215
  readonly commaSeparatedIds: "多个 ID 用逗号分隔";
202
216
  readonly aiTitle: "AI 工具配置";
@@ -242,10 +256,12 @@ export declare const PAGE_TEXTS: {
242
256
  readonly stopOk: "桥接已停止。";
243
257
  readonly configEditorTitle: "JSON 配置编辑器";
244
258
  readonly configEditorHint: "直接编辑 ~/.open-im/config.json";
245
- readonly configJson: "配置文件 (JSON)";
246
- readonly configJsonHint: "编辑配置 JSON。点击服务区的“保存配置”按钮后保存更改。";
259
+ readonly claudeSettingsLabel: "~/.claude/settings.json";
260
+ readonly configJson: "~/.open-im/config.json";
261
+ readonly configJsonHint: "编辑配置 JSON。点击服务区的“保存配置”按钮后侘存更改。";
247
262
  readonly formatJson: "格式化";
248
263
  readonly resetJson: "重置";
264
+ readonly saveBtn: "保存";
249
265
  readonly jsonValid: "JSON 有效";
250
266
  readonly jsonInvalid: "JSON 无效:{error}";
251
267
  };
@@ -50,6 +50,7 @@ export const PAGE_TEXTS = {
50
50
  qqSummary: "App ID and secret for bot access.",
51
51
  weworkSummary: "Corp ID and secret for enterprise delivery.",
52
52
  dingtalkSummary: "Client credentials plus optional card template.",
53
+ workbuddySummary: "CodeBuddy OAuth for WeChat customer service.",
53
54
  platformCredentialsTitle: "Credentials",
54
55
  platformAccessTitle: "Routing and access",
55
56
  platformTestNote: "Checks required credentials against the platform.",
@@ -70,8 +71,13 @@ export const PAGE_TEXTS = {
70
71
  clientId: "Client ID / AppKey",
71
72
  clientSecret: "Client Secret / AppSecret",
72
73
  dingtalkHelp: 'Get credentials: Create an enterprise internal app on <a href="https://open-dev.dingtalk.com/" target="_blank">DingTalk Open Platform</a>, enable Stream Mode, and get Client ID / Client Secret',
74
+ workbuddyHelp: 'Get credentials: Login via CodeBuddy OAuth to get access/refresh tokens. WorkBuddy connects WeChat customer service through Centrifuge WebSocket.',
73
75
  secret: "Secret",
74
76
  cardTemplateId: "Card template ID",
77
+ workbuddyAccessToken: "Access Token",
78
+ workbuddyRefreshToken: "Refresh Token",
79
+ workbuddyUserId: "User ID",
80
+ workbuddyBaseUrl: "Base URL",
75
81
  optional: "Optional",
76
82
  commaSeparatedIds: "Comma-separated IDs",
77
83
  aiTitle: "AI Tooling",
@@ -120,10 +126,12 @@ export const PAGE_TEXTS = {
120
126
  stopOk: "Bridge stopped.",
121
127
  configEditorTitle: "Config Editor",
122
128
  configEditorHint: "Edit ~/.open-im/config.json directly",
123
- configJson: "Configuration (JSON)",
129
+ claudeSettingsLabel: "~/.claude/settings.json",
130
+ configJson: "~/.open-im/config.json",
124
131
  configJsonHint: "Edit the configuration JSON. Changes will be saved when you click \"Save Config\" in the Service section.",
125
132
  formatJson: "Format",
126
133
  resetJson: "Reset",
134
+ saveBtn: "Save",
127
135
  jsonValid: "Valid JSON",
128
136
  jsonInvalid: "Invalid JSON: {error}",
129
137
  },
@@ -176,6 +184,7 @@ export const PAGE_TEXTS = {
176
184
  qqSummary: "QQ \u673a\u5668\u4eba App ID \u4e0e Secret\u3002",
177
185
  weworkSummary: "\u4f01\u4e1a\u5fae\u4fe1 Corp ID \u4e0e Secret\u3002",
178
186
  dingtalkSummary: "\u9489\u9489 Client \u51ed\u8bc1\uff0c\u53ef\u9009\u914d\u7f6e\u5361\u7247\u6a21\u677f ID\u3002",
187
+ workbuddySummary: "CodeBuddy OAuth \u8fde\u63a5\u5fae\u4fe1\u5ba2\u670d\u3002",
179
188
  platformCredentialsTitle: "\u51ed\u8bc1",
180
189
  platformAccessTitle: "\u8def\u7531\u4e0e\u8bbf\u95ee",
181
190
  platformTestNote: "\u53ea\u4f1a\u68c0\u67e5\u8be5\u5e73\u53f0\u7684\u5fc5\u586b\u51ed\u8bc1\u662f\u5426\u53ef\u7528\u3002",
@@ -196,7 +205,12 @@ export const PAGE_TEXTS = {
196
205
  clientId: "Client ID / AppKey",
197
206
  clientSecret: "Client Secret / AppSecret",
198
207
  dingtalkHelp: '\u83b7\u53d6\u51ed\u8bc1\uff1a\u5728 <a href="https://open-dev.dingtalk.com/" target="_blank">\u9489\u9489\u5f00\u653e\u5e73\u53f0</a> \u521b\u5efa\u4f01\u4e1a\u5185\u90e8\u5e94\u7528\uff0c\u542f\u7528 Stream Mode\uff0c\u5e76\u62ff\u5230 Client ID / Client Secret',
208
+ workbuddyHelp: '\u83b7\u53d6\u51ed\u8bc1\uff1a\u901a\u8fc7 CodeBuddy OAuth \u767b\u5f55\u83b7\u53d6 access/refresh token\u3002WorkBuddy \u901a\u8fc7 Centrifuge WebSocket \u8fde\u63a5\u5fae\u4fe1\u5ba2\u670d\u3002',
199
209
  cardTemplateId: "\u5361\u7247\u6a21\u677f ID",
210
+ workbuddyAccessToken: "Access Token",
211
+ workbuddyRefreshToken: "Refresh Token",
212
+ workbuddyUserId: "User ID",
213
+ workbuddyBaseUrl: "\u57fa\u7840 URL",
200
214
  optional: "\u53ef\u9009",
201
215
  commaSeparatedIds: "\u591a\u4e2a ID \u7528\u9017\u53f7\u5206\u9694",
202
216
  aiTitle: "AI \u5de5\u5177\u914d\u7f6e",
@@ -242,10 +256,12 @@ export const PAGE_TEXTS = {
242
256
  stopOk: "\u6865\u63a5\u5df2\u505c\u6b62\u3002",
243
257
  configEditorTitle: "JSON \u914d\u7f6e\u7f16\u8f91\u5668",
244
258
  configEditorHint: "\u76f4\u63a5\u7f16\u8f91 ~/.open-im/config.json",
245
- configJson: "\u914d\u7f6e\u6587\u4ef6 (JSON)",
246
- configJsonHint: "\u7f16\u8f91\u914d\u7f6e JSON\u3002\u70b9\u51fb\u670d\u52a1\u533a\u7684\u201c\u4fdd\u5b58\u914d\u7f6e\u201d\u6309\u94ae\u540e\u4fdd\u5b58\u66f4\u6539\u3002",
259
+ claudeSettingsLabel: "~/.claude/settings.json",
260
+ configJson: "~/.open-im/config.json",
261
+ configJsonHint: "\u7f16\u8f91\u914d\u7f6e JSON\u3002\u70b9\u51fb\u670d\u52a1\u533a\u7684\u201c\u4fdd\u5b58\u914d\u7f6e\u201d\u6309\u94ae\u540e\u4f98\u5b58\u66f4\u6539\u3002",
247
262
  formatJson: "\u683c\u5f0f\u5316",
248
263
  resetJson: "\u91cd\u7f6e",
264
+ saveBtn: "\u4fdd\u5b58",
249
265
  jsonValid: "JSON \u6709\u6548",
250
266
  jsonInvalid: "JSON \u65e0\u6548\uff1a{error}",
251
267
  }
@@ -4,6 +4,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
4
4
  { key: "qq", label: "QQ", fields: ["aiCommand", "appId", "secret", "allowedUserIds"], testFields: ["appId", "secret"], requiredFields: ["appId", "secret"] },
5
5
  { key: "wework", label: "WeWork", fields: ["aiCommand", "corpId", "secret", "allowedUserIds"], testFields: ["corpId", "secret"], requiredFields: ["corpId", "secret"] },
6
6
  { key: "dingtalk", label: "DingTalk", fields: ["aiCommand", "clientId", "clientSecret", "cardTemplateId", "allowedUserIds"], testFields: ["clientId", "clientSecret"], requiredFields: ["clientId", "clientSecret"] },
7
+ { key: "workbuddy", label: "WorkBuddy", fields: ["aiCommand", "accessToken", "refreshToken", "userId", "baseUrl", "allowedUserIds"], testFields: ["accessToken", "refreshToken", "userId"], requiredFields: ["accessToken", "refreshToken", "userId"] },
7
8
  ];
8
9
  const platformKeys = platformDefinitions.map((platform) => platform.key);
9
10
  const aiTools = ["claude", "codex", "codebuddy"];
@@ -159,6 +160,12 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
159
160
  { id: "statConfiguredLabel", key: "statConfiguredLabel" },
160
161
  { id: "statEnabledLabel", key: "statEnabledLabel" },
161
162
  { id: "statServiceLabel", key: "statServiceLabel" },
163
+ { id: "openImConfigSummary", key: "configJson" },
164
+ { id: "claudeSettingsSummary", key: "claudeSettingsLabel" },
165
+ { id: "formatJsonButtonText", key: "formatJson" },
166
+ { id: "resetJsonButtonText", key: "resetJson" },
167
+ { id: "saveClaudeSettingsBtnText", key: "saveBtn" },
168
+ { id: "saveOpenImConfigBtnText", key: "saveBtn" },
162
169
  ],
163
170
  platformLabels: {
164
171
  enabled: { suffix: "-label", key: "enabled" },
@@ -380,7 +387,7 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
380
387
 
381
388
  // Navigation
382
389
  function setActiveNav(targetId) {
383
- ["navOverviewBtn","navPlatformsBtn","navAiBtn","navServiceBtn","navConfigEditorBtn"].forEach((id) => {
390
+ ["navOverviewBtn","navPlatformsBtn","navAiBtn","navServiceBtn"].forEach((id) => {
384
391
  const btn = el(id);
385
392
  if (btn) btn.classList.toggle("active", id === targetId);
386
393
  });
@@ -604,16 +611,37 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
604
611
  }
605
612
  });
606
613
 
607
- // Claude settings.json editor (advanced, inline & collapsible)
614
+ // Open-im config.json: load when expanded
615
+ const openImConfigContainer = document.getElementById("openImConfigContainer");
616
+ if (openImConfigContainer && openImConfigContainer instanceof HTMLDetailsElement) {
617
+ openImConfigContainer.addEventListener("toggle", () => {
618
+ if (openImConfigContainer.open) void loadOpenImConfig();
619
+ });
620
+ }
621
+ // Claude settings.json: load when expanded
608
622
  const claudeSettingsContainer = document.getElementById("claudeSettingsContainer");
609
623
  if (claudeSettingsContainer && claudeSettingsContainer instanceof HTMLDetailsElement) {
610
624
  claudeSettingsContainer.addEventListener("toggle", () => {
611
- if (claudeSettingsContainer.open) {
612
- void loadClaudeSettings();
613
- }
625
+ if (claudeSettingsContainer.open) void loadClaudeSettings();
614
626
  });
615
627
  }
616
628
 
629
+ el("saveClaudeSettingsBtn").onclick = async () => {
630
+ try {
631
+ await saveClaudeSettings();
632
+ } catch (e) {
633
+ setMessage(e && e.message ? e.message : String(e), "error");
634
+ }
635
+ };
636
+ el("saveOpenImConfigBtn").onclick = async () => {
637
+ try {
638
+ await saveOpenImConfig();
639
+ setMessage(t("saveOk"), "success");
640
+ } catch (e) {
641
+ setMessage(e && e.message ? e.message : String(e), "error");
642
+ }
643
+ };
644
+
617
645
  // AI tool switcher
618
646
  document.querySelectorAll(".tab[data-tool]").forEach((tab) => {
619
647
  tab.addEventListener("click", () => {
@@ -648,7 +676,6 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
648
676
  el("navPlatformsBtn").onclick = () => scrollToSection("configSection", "navPlatformsBtn");
649
677
  el("navAiBtn").onclick = () => scrollToSection("aiSection", "navAiBtn");
650
678
  el("navServiceBtn").onclick = () => scrollToSection("serviceSection", "navServiceBtn");
651
- el("navConfigEditorBtn").onclick = () => scrollToSection("configEditorSection", "navConfigEditorBtn");
652
679
 
653
680
  // Language toggle
654
681
  el("langButton").onclick = () => {
@@ -718,7 +745,8 @@ export const PAGE_SCRIPT = String.raw ` const platformDefinitions = [
718
745
  async function saveOpenImConfig() {
719
746
  const textarea = document.getElementById("configJson");
720
747
  if (!(textarea instanceof HTMLTextAreaElement)) return;
721
- const json = textarea.value;
748
+ const json = textarea.value.trim();
749
+ if (!json) return;
722
750
 
723
751
  // Validate JSON
724
752
  try {
@@ -781,16 +781,6 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
781
781
  </svg>
782
782
  <span id="navServiceText">Service</span>
783
783
  </button>
784
- <button class="nav-item" id="navConfigEditorBtn">
785
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
786
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
787
- <polyline points="14 2 14 8 20 8"/>
788
- <line x1="16" y1="13" x2="8" y2="13"/>
789
- <line x1="16" y1="17" x2="8" y2="17"/>
790
- <polyline points="10 9 9 9 8 9"/>
791
- </svg>
792
- <span id="navConfigEditorText">Config Editor</span>
793
- </button>
794
784
  </nav>
795
785
  </aside>
796
786
 
@@ -838,7 +828,7 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
838
828
  <div class="stats-grid">
839
829
  <div class="stat-card">
840
830
  <div class="stat-label" id="statConfiguredLabel">Configured</div>
841
- <div class="stat-value" id="statConfiguredValue">0/5</div>
831
+ <div class="stat-value" id="statConfiguredValue">0/6</div>
842
832
  </div>
843
833
  <div class="stat-card">
844
834
  <div class="stat-label" id="statEnabledLabel">Enabled</div>
@@ -1068,6 +1058,53 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
1068
1058
  <div id="test-dingtalk-result" class="mt-4"></div>
1069
1059
  </div>
1070
1060
  </div>
1061
+
1062
+ <!-- WorkBuddy -->
1063
+ <div class="platform-card">
1064
+ <div class="platform-header">
1065
+ <h3 class="platform-title">WorkBuddy</h3>
1066
+ <label class="toggle">
1067
+ <input type="checkbox" id="workbuddy-enabled" class="toggle-input">
1068
+ <span class="toggle-switch"></span>
1069
+ <span class="toggle-label" id="workbuddy-label">Enabled</span>
1070
+ </label>
1071
+ </div>
1072
+ <div class="platform-body">
1073
+ <div class="form-group">
1074
+ <label class="form-label" id="workbuddy-aiCommand-label">AI Tool</label>
1075
+ <select id="workbuddy-aiCommand" class="form-select">
1076
+ <option value="claude">claude</option>
1077
+ <option value="codex">codex</option>
1078
+ <option value="codebuddy">codebuddy</option>
1079
+ </select>
1080
+ </div>
1081
+ <div class="form-group">
1082
+ <label class="form-label" id="workbuddy-accessToken-label">Access Token</label>
1083
+ <input id="workbuddy-accessToken" class="form-input mono" type="password" autocomplete="off" />
1084
+ </div>
1085
+ <div class="form-group">
1086
+ <label class="form-label" id="workbuddy-refreshToken-label">Refresh Token</label>
1087
+ <input id="workbuddy-refreshToken" class="form-input mono" type="password" autocomplete="off" />
1088
+ </div>
1089
+ <div class="form-group">
1090
+ <label class="form-label" id="workbuddy-userId-label">User ID</label>
1091
+ <input id="workbuddy-userId" class="form-input mono" type="text" />
1092
+ </div>
1093
+ <div class="form-group">
1094
+ <label class="form-label" id="workbuddy-baseUrl-label">Base URL (optional)</label>
1095
+ <input id="workbuddy-baseUrl" class="form-input mono" type="text" placeholder="https://copilot.tencent.com" />
1096
+ </div>
1097
+ <div class="form-group">
1098
+ <label class="form-label" id="workbuddy-allowedUserIds-label">Allowed User IDs</label>
1099
+ <textarea id="workbuddy-allowedUserIds" class="form-textarea mono"></textarea>
1100
+ </div>
1101
+ <div class="form-help" id="workbuddy-help">WorkBuddy uses CodeBuddy OAuth to connect WeChat customer service. Get credentials from CodeBuddy login.</div>
1102
+ <div style="display: flex; gap: 12px; flex-wrap: wrap;">
1103
+ <button id="test-workbuddy" class="btn btn-secondary btn-sm" type="button">Test Configuration</button>
1104
+ </div>
1105
+ <div id="test-workbuddy-result" class="mt-4"></div>
1106
+ </div>
1107
+ </div>
1071
1108
  </div>
1072
1109
  </section>
1073
1110
 
@@ -1138,18 +1175,31 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
1138
1175
  <div class="form-hint" id="ai-claudeConfigPath-hint">Environment variables are saved to ~/.claude/settings.json</div>
1139
1176
  </div>
1140
1177
  <div class="form-group">
1141
- <details id="claudeSettingsContainer">
1142
- <summary class="form-label" style="cursor: pointer;">Edit ~/.claude/settings.json (advanced)</summary>
1143
- <div class="form-hint" style="margin-top: 8px; margin-bottom: 8px;">
1144
- JSON will be auto-formatted; invalid JSON 会提示错误。
1178
+ <details id="openImConfigContainer">
1179
+ <summary class="form-label" style="cursor: pointer;" id="openImConfigSummary">~/.open-im/config.json</summary>
1180
+ <div style="margin-top: 12px;">
1181
+ <div style="display:flex; justify-content:flex-end; gap:8px; margin-bottom:8px;">
1182
+ <button type="button" id="formatJsonButton" class="btn btn-sm btn-ghost"><span id="formatJsonButtonText">Format</span></button>
1183
+ <button type="button" id="resetJsonButton" class="btn btn-sm btn-ghost"><span id="resetJsonButtonText">Reset</span></button>
1184
+ </div>
1185
+ <textarea id="configJson" class="form-input mono" rows="16" style="font-family:monospace; font-size:13px; line-height:1.5; min-height:320px; resize:vertical; white-space:pre;" spellcheck="false"></textarea>
1186
+ <div id="jsonValidationMessage" class="message hidden" style="margin-top:6px;" aria-live="polite"></div>
1187
+ <div style="margin-top: 8px;">
1188
+ <button type="button" id="saveOpenImConfigBtn" class="btn btn-secondary btn-sm"><span id="saveOpenImConfigBtnText">Save</span></button>
1189
+ </div>
1145
1190
  </div>
1146
- <textarea
1147
- id="claudeSettingsEditor"
1148
- class="form-input mono"
1149
- style="min-height: 200px; white-space: pre; font-family: var(--font-mono);"
1150
- ></textarea>
1151
- <div class="form-hint" style="margin-top: 4px;">
1152
- 折叠/展开以隐藏或查看完整配置。
1191
+ </details>
1192
+ <details id="claudeSettingsContainer" style="margin-top: 12px;">
1193
+ <summary class="form-label" style="cursor: pointer;" id="claudeSettingsSummary">~/.claude/settings.json</summary>
1194
+ <div style="margin-top: 12px;">
1195
+ <textarea
1196
+ id="claudeSettingsEditor"
1197
+ class="form-input mono"
1198
+ style="min-height: 180px; white-space: pre; font-family: var(--font-mono);"
1199
+ ></textarea>
1200
+ <div style="margin-top: 8px;">
1201
+ <button type="button" id="saveClaudeSettingsBtn" class="btn btn-secondary btn-sm"><span id="saveClaudeSettingsBtnText">Save</span></button>
1202
+ </div>
1153
1203
  </div>
1154
1204
  </details>
1155
1205
  </div>
@@ -1207,32 +1257,6 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
1207
1257
  </div>
1208
1258
  </section>
1209
1259
 
1210
- <!-- Config Editor Section -->
1211
- <section class="section" id="configEditorSection" style="display:none">
1212
- <div class="section-header">
1213
- <h2 class="section-title" id="configEditorTitle">Config Editor</h2>
1214
- <p class="section-description" id="configEditorHint">Edit ~/.open-im/config.json directly</p>
1215
- </div>
1216
-
1217
- <div class="card">
1218
- <div class="card-body">
1219
- <div class="form-group">
1220
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
1221
- <label class="form-label" id="configJson-label">Configuration (JSON)</label>
1222
- <div style="display:flex; gap:8px;">
1223
- <button id="formatJsonButton" class="btn btn-sm btn-ghost">Format</button>
1224
- <button id="resetJsonButton" class="btn btn-sm btn-ghost">Reset</button>
1225
- </div>
1226
- </div>
1227
- <textarea id="configJson" class="form-input mono" rows="20" style="font-family:monospace; font-size:13px; line-height:1.5; min-height:400px; resize:vertical;" spellcheck="false"></textarea>
1228
- <p class="form-hint" id="configJson-hint">Edit the configuration JSON. Changes will be saved when you click "Save Config" in the Service section.</p>
1229
- </div>
1230
- <div class="form-group">
1231
- <div id="jsonValidationMessage" class="message hidden" aria-live="polite"></div>
1232
- </div>
1233
- </div>
1234
- </div>
1235
- </section>
1236
1260
  </div>
1237
1261
  </main>
1238
1262
  </div>
@@ -359,6 +359,7 @@ function createProbeConfig(values) {
359
359
  wechatAllowedUserIds: [],
360
360
  weworkAllowedUserIds: [],
361
361
  dingtalkAllowedUserIds: [],
362
+ workbuddyAllowedUserIds: [],
362
363
  aiCommand: "claude",
363
364
  codexCliPath: "codex",
364
365
  claudeWorkDir: process.cwd(),
@@ -673,6 +674,47 @@ export async function startWebConfigServer(options) {
673
674
  });
674
675
  return;
675
676
  }
677
+ if (request.method === "GET" && requestUrl.pathname === "/api/config/file") {
678
+ try {
679
+ let contents = "{}";
680
+ if (existsSync(CONFIG_PATH)) {
681
+ contents = readFileSync(CONFIG_PATH, "utf-8");
682
+ }
683
+ json(response, 200, { path: CONFIG_PATH, contents });
684
+ }
685
+ catch (error) {
686
+ json(response, 500, { error: error instanceof Error ? error.message : String(error) });
687
+ }
688
+ return;
689
+ }
690
+ if (request.method === "POST" && requestUrl.pathname === "/api/config/file") {
691
+ try {
692
+ const body = await readJson(request);
693
+ const raw = body.contents ?? "";
694
+ if (!raw.trim()) {
695
+ json(response, 400, { error: "contents is required" });
696
+ return;
697
+ }
698
+ try {
699
+ JSON.parse(raw);
700
+ }
701
+ catch {
702
+ json(response, 400, { error: "Invalid JSON" });
703
+ return;
704
+ }
705
+ const dir = dirname(CONFIG_PATH);
706
+ if (!existsSync(dir)) {
707
+ mkdirSync(dir, { recursive: true });
708
+ }
709
+ writeFileSync(CONFIG_PATH, raw, "utf-8");
710
+ loadConfig();
711
+ json(response, 200, { message: "Config file saved.", path: CONFIG_PATH });
712
+ }
713
+ catch (error) {
714
+ json(response, 500, { error: error instanceof Error ? error.message : String(error) });
715
+ }
716
+ return;
717
+ }
676
718
  if (request.method === "POST" && requestUrl.pathname === "/api/config/validate") {
677
719
  try {
678
720
  const body = await readJson(request);