@wu529778790/open-im 1.8.1-beta.2 → 1.8.1-beta.21

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 (55) hide show
  1. package/dist/access/access-control.js +1 -1
  2. package/dist/adapters/claude-sdk-adapter.js +94 -36
  3. package/dist/channels/capabilities.js +5 -0
  4. package/dist/cli.js +5 -2
  5. package/dist/commands/handler.d.ts +1 -2
  6. package/dist/commands/handler.js +6 -18
  7. package/dist/config-web-page-i18n.d.ts +12 -0
  8. package/dist/config-web-page-i18n.js +12 -0
  9. package/dist/config-web-page-script.js +1 -0
  10. package/dist/config-web-page-template.js +48 -1
  11. package/dist/config-web.js +110 -7
  12. package/dist/config.d.ts +25 -1
  13. package/dist/config.js +46 -0
  14. package/dist/constants.d.ts +2 -0
  15. package/dist/constants.js +2 -0
  16. package/dist/dingtalk/client.js +11 -3
  17. package/dist/dingtalk/event-handler.js +18 -3
  18. package/dist/dingtalk/message-sender.js +13 -0
  19. package/dist/feishu/event-handler.js +144 -10
  20. package/dist/index.js +26 -2
  21. package/dist/manager-control.js +7 -0
  22. package/dist/qq/client.js +111 -88
  23. package/dist/qq/event-handler.js +16 -2
  24. package/dist/qq/message-sender.js +11 -0
  25. package/dist/service-control.js +4 -0
  26. package/dist/session/session-manager.js +11 -1
  27. package/dist/setup.js +2 -1
  28. package/dist/shared/active-chats.d.ts +2 -2
  29. package/dist/shared/ai-task.js +13 -1
  30. package/dist/shared/chat-user-map.js +11 -0
  31. package/dist/shared/media-storage.js +27 -0
  32. package/dist/telegram/client.js +25 -3
  33. package/dist/telegram/event-handler.js +44 -8
  34. package/dist/telegram/message-sender.js +13 -0
  35. package/dist/wechat/auth/qclaw-api.js +1 -1
  36. package/dist/wechat/client.js +81 -4
  37. package/dist/wechat/event-handler.js +10 -3
  38. package/dist/wework/client.js +36 -14
  39. package/dist/wework/event-handler.js +39 -4
  40. package/dist/wework/message-sender.js +53 -21
  41. package/dist/workbuddy/centrifuge-client.d.ts +74 -0
  42. package/dist/workbuddy/centrifuge-client.js +272 -0
  43. package/dist/workbuddy/client.d.ts +27 -0
  44. package/dist/workbuddy/client.js +162 -0
  45. package/dist/workbuddy/event-handler.d.ts +11 -0
  46. package/dist/workbuddy/event-handler.js +118 -0
  47. package/dist/workbuddy/index.d.ts +8 -0
  48. package/dist/workbuddy/index.js +8 -0
  49. package/dist/workbuddy/message-sender.d.ts +16 -0
  50. package/dist/workbuddy/message-sender.js +51 -0
  51. package/dist/workbuddy/oauth.d.ts +114 -0
  52. package/dist/workbuddy/oauth.js +310 -0
  53. package/dist/workbuddy/types.d.ts +86 -0
  54. package/dist/workbuddy/types.js +4 -0
  55. package/package.json +4 -2
@@ -8,7 +8,7 @@ export class AccessControl {
8
8
  }
9
9
  isAllowed(userId) {
10
10
  if (this.allowedUserIds.size === 0) {
11
- log.debug(`Allowing user ${userId} (no whitelist configured)`);
11
+ log.warn(`Allowing user ${userId} no whitelist configured. Set allowedUserIds to restrict access.`);
12
12
  return true;
13
13
  }
14
14
  const allowed = this.allowedUserIds.has(userId);
@@ -16,6 +16,21 @@ const log = createLogger('ClaudeSDK');
16
16
  const activeSessions = new Map();
17
17
  // 存储正在进行的流式迭代器,用于中断
18
18
  const activeStreams = new Set();
19
+ // Mutex to serialize process.chdir() calls across concurrent users
20
+ let chdirMutex = Promise.resolve();
21
+ function withChdirMutex(fn) {
22
+ const previous = chdirMutex;
23
+ let resolve;
24
+ chdirMutex = new Promise((r) => { resolve = r; });
25
+ return previous.then(() => {
26
+ try {
27
+ return fn();
28
+ }
29
+ finally {
30
+ resolve();
31
+ }
32
+ });
33
+ }
19
34
  function isStreamEvent(msg) {
20
35
  return msg.type === 'stream_event';
21
36
  }
@@ -37,39 +52,51 @@ function isResult(msg) {
37
52
  * @param permissionMode 权限模式
38
53
  * @returns SDKSession 对象和实际的 sessionId
39
54
  */
40
- async function getOrCreateSession(sessionId, _workDir, // 保留参数以备将来使用
41
- model, permissionMode) {
55
+ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
42
56
  const resolvedModel = model?.trim() || 'claude-opus-4-5';
43
57
  const sessionOptions = {
44
58
  model: resolvedModel,
45
59
  permissionMode,
46
- // 可以添加其他选项,如 hooks, allowedTools 等
47
60
  };
48
61
  const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
49
- log.info(`[ClaudeSDK] model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl}`);
50
- let session;
51
- if (sessionId) {
52
- // 尝试恢复已有会话
62
+ 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
64
+ return withChdirMutex(() => {
65
+ let session;
66
+ const originalCwd = process.cwd();
53
67
  try {
54
- log.info(`Attempting to resume session: ${sessionId}`);
55
- session = unstable_v2_resumeSession(sessionId, sessionOptions);
56
- activeSessions.set(sessionId, session);
57
- log.info(`Successfully resumed session: ${sessionId}`);
58
- return { session, sessionId };
68
+ if (workDir && workDir !== originalCwd) {
69
+ process.chdir(workDir);
70
+ }
71
+ if (sessionId) {
72
+ // 尝试恢复已有会话
73
+ try {
74
+ log.info(`Attempting to resume session: ${sessionId}`);
75
+ session = unstable_v2_resumeSession(sessionId, sessionOptions);
76
+ activeSessions.set(sessionId, session);
77
+ log.info(`Successfully resumed session: ${sessionId}`);
78
+ return { session, sessionId };
79
+ }
80
+ catch (err) {
81
+ log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
82
+ // 恢复失败,创建新会话
83
+ }
84
+ }
85
+ // 创建新会话
86
+ session = unstable_v2_createSession(sessionOptions);
87
+ // 新会话的 sessionId 需要从第一个消息中获取
88
+ // 暂时返回 undefined,稍后在 init 消息中获取
89
+ const tempId = `pending-${Date.now()}`;
90
+ activeSessions.set(tempId, session);
91
+ log.info(`Created new session (tempId: ${tempId})`);
92
+ return { session, sessionId: tempId };
59
93
  }
60
- catch (err) {
61
- log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
62
- // 恢复失败,创建新会话
94
+ finally {
95
+ if (workDir && workDir !== originalCwd) {
96
+ process.chdir(originalCwd);
97
+ }
63
98
  }
64
- }
65
- // 创建新会话
66
- session = unstable_v2_createSession(sessionOptions);
67
- // 新会话的 sessionId 需要从第一个消息中获取
68
- // 暂时返回 undefined,稍后在 init 消息中获取
69
- const tempId = `pending-${Date.now()}`;
70
- activeSessions.set(tempId, session);
71
- log.info(`Created new session (tempId: ${tempId})`);
72
- return { session, sessionId: tempId };
99
+ });
73
100
  }
74
101
  export class ClaudeSDKAdapter {
75
102
  toolId = 'claude-sdk';
@@ -99,9 +126,12 @@ export class ClaudeSDKAdapter {
99
126
  activeSessions.clear();
100
127
  }
101
128
  run(prompt, sessionId, workDir, callbacks, options) {
129
+ log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
102
130
  const abortController = new AbortController();
103
131
  let streamClosed = false;
104
132
  let actualSessionId;
133
+ let pendingTempId; // 记录临时 ID,用于 abort 时清理
134
+ let runSettled = false;
105
135
  const permissionMode = options?.skipPermissions
106
136
  ? 'bypassPermissions'
107
137
  : options?.permissionMode === 'acceptEdits'
@@ -118,8 +148,12 @@ export class ClaudeSDKAdapter {
118
148
  log.warn('Claude SDK: No API credentials found in environment variables');
119
149
  }
120
150
  log.info(`[V2] Session: ${sessionId ?? 'new'}, prompt="${prompt.slice(0, 50)}..."`);
151
+ log.info(`[V2] model param=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
121
152
  // 获取或创建会话
122
- const { session } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
153
+ const { session, sessionId: returnedId } = await getOrCreateSession(sessionId, workDir, options?.model, permissionMode);
154
+ if (returnedId.startsWith('pending-')) {
155
+ pendingTempId = returnedId;
156
+ }
123
157
  // 发送用户消息
124
158
  await session.send(prompt);
125
159
  // 获取响应流
@@ -184,6 +218,7 @@ export class ClaudeSDKAdapter {
184
218
  log.info(`[V2] Result: subtype=${m.subtype}, num_turns=${m.num_turns}, sessionId=${actualSessionId ?? 'unknown'}`);
185
219
  // 检查会话错误
186
220
  if (!success) {
221
+ runSettled = true;
187
222
  const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
188
223
  if (noConvErr) {
189
224
  log.warn(`Session ${actualSessionId} not found, may need to create new one`);
@@ -210,22 +245,32 @@ export class ClaudeSDKAdapter {
210
245
  result.accumulated = accumulated;
211
246
  result.result = accumulated;
212
247
  }
248
+ runSettled = true;
213
249
  callbacks.onComplete(result);
214
250
  return;
215
251
  }
216
252
  }
217
253
  // 如果流正常结束但没有收到 result 消息
218
- if (!streamClosed && accumulated) {
219
- log.info('Stream ended without result message, using accumulated text');
220
- callbacks.onComplete({
221
- success: true,
222
- result: accumulated,
223
- accumulated,
224
- cost: 0,
225
- durationMs: 0,
226
- numTurns: 1,
227
- toolStats,
228
- });
254
+ if (!streamClosed) {
255
+ if (accumulated) {
256
+ log.info('Stream ended without result message, using accumulated text');
257
+ runSettled = true;
258
+ callbacks.onComplete({
259
+ success: true,
260
+ result: accumulated,
261
+ accumulated,
262
+ cost: 0,
263
+ durationMs: 0,
264
+ numTurns: 1,
265
+ toolStats,
266
+ });
267
+ }
268
+ else {
269
+ // 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
270
+ log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
271
+ runSettled = true;
272
+ callbacks.onError('AI 响应异常结束(无输出),请重试');
273
+ }
229
274
  }
230
275
  }
231
276
  finally {
@@ -236,14 +281,27 @@ export class ClaudeSDKAdapter {
236
281
  catch (err) {
237
282
  if (abortController.signal.aborted) {
238
283
  log.info('Session run aborted');
284
+ // 清理 pending tempId(abort 可能在 init 消息之前发生)
285
+ const idToClean = actualSessionId ?? pendingTempId;
286
+ if (idToClean?.startsWith('pending-')) {
287
+ activeSessions.delete(idToClean);
288
+ log.info(`Cleaned up pending session: ${idToClean}`);
289
+ }
239
290
  return;
240
291
  }
292
+ runSettled = true;
241
293
  const errorObj = err;
242
294
  const msg = errorObj.message || String(err);
243
295
  log.error(`Claude SDK V2 error: ${msg}`);
244
296
  if (errorObj.stack) {
245
297
  log.error(`Error stack: ${errorObj.stack}`);
246
298
  }
299
+ // 清理 pending tempId(session 在获取真实 ID 前就失败了)
300
+ const errIdToClean = actualSessionId ?? pendingTempId;
301
+ if (errIdToClean?.startsWith('pending-')) {
302
+ activeSessions.delete(errIdToClean);
303
+ log.info(`Cleaned up pending session after error: ${errIdToClean}`);
304
+ }
247
305
  callbacks.onError(msg);
248
306
  }
249
307
  };
@@ -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)
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ async function cmdStart() {
50
50
  console.log("\nopen-im is already running in the background.");
51
51
  console.log(` pid: ${status.pid}`);
52
52
  console.log(` config page: ${getWebConfigUrl()}`);
53
- return;
53
+ process.exit(0);
54
54
  }
55
55
  if (!(await ensureConfigured("start"))) {
56
56
  process.exit(1);
@@ -67,17 +67,19 @@ async function cmdStart() {
67
67
  console.log(" A one-time login URL (with login_token) has been printed by the config-web server logger.");
68
68
  console.log(" Please use that URL (replacing 127.0.0.1 with your server IP/hostname) for the first login.");
69
69
  }
70
+ process.exit(0);
70
71
  }
71
72
  async function cmdStop() {
72
73
  const status = getManagerStatus();
73
74
  if (!status.pid) {
74
75
  console.log("open-im is not running in the background.");
75
- return;
76
+ process.exit(0);
76
77
  }
77
78
  await stopBackgroundService();
78
79
  const result = await stopManagerProcess();
79
80
  console.log("\nopen-im stopped.");
80
81
  console.log(` pid: ${result.pid}`);
82
+ process.exit(0);
81
83
  }
82
84
  async function cmdRestart() {
83
85
  const status = getManagerStatus();
@@ -98,6 +100,7 @@ async function cmdRestart() {
98
100
  console.log("\nopen-im restarted in the background.");
99
101
  console.log(` pid: ${child.pid}`);
100
102
  console.log(` config page: ${getWebConfigUrl()}`);
103
+ process.exit(0);
101
104
  }
102
105
  async function cmdInit() {
103
106
  console.log("\nopen-im CLI setup\n");
@@ -18,8 +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>;
22
- private getClearHistoryHint;
21
+ dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
23
22
  private handleHelp;
24
23
  private handleNew;
25
24
  private handlePwd;
@@ -16,9 +16,9 @@ export class CommandHandler {
16
16
  return true;
17
17
  }
18
18
  if (t === '/help')
19
- return this.handleHelp(chatId, platform);
19
+ return this.handleHelp(chatId);
20
20
  if (t === '/new')
21
- return this.handleNew(chatId, userId, platform);
21
+ return this.handleNew(chatId, userId);
22
22
  if (t === '/pwd')
23
23
  return this.handlePwd(chatId, userId);
24
24
  if (t === '/status')
@@ -33,16 +33,7 @@ export class CommandHandler {
33
33
  }
34
34
  return false;
35
35
  }
36
- getClearHistoryHint(platform) {
37
- return platform === 'feishu'
38
- ? '💡 提示:如需清除本对话的历史消息,请点击飞书聊天右上角「...」→ 清除聊天记录'
39
- : platform === 'wechat'
40
- ? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
41
- : platform === 'dingtalk'
42
- ? '💡 提示:如需清除本对话的历史消息,请在钉钉中清空聊天记录'
43
- : '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
44
- }
45
- async handleHelp(chatId, platform) {
36
+ async handleHelp(chatId) {
46
37
  const help = [
47
38
  '📋 可用命令:',
48
39
  '',
@@ -51,16 +42,14 @@ export class CommandHandler {
51
42
  '/status - 显示状态',
52
43
  '/cd <路径> - 切换工作目录',
53
44
  '/pwd - 当前工作目录',
54
- '',
55
- this.getClearHistoryHint(platform),
56
45
  ].join('\n');
57
46
  await this.deps.sender.sendTextReply(chatId, help);
58
47
  return true;
59
48
  }
60
- async handleNew(chatId, userId, platform) {
49
+ async handleNew(chatId, userId) {
61
50
  const ok = this.deps.sessionManager.newSession(userId);
62
51
  await this.deps.sender.sendTextReply(chatId, ok
63
- ? `✅ AI 会话已重置,下一条消息将使用全新上下文。\n\n${this.getClearHistoryHint(platform)}`
52
+ ? '✅ AI 会话已重置,下一条消息将使用全新上下文。'
64
53
  : '当前没有活动会话。');
65
54
  return true;
66
55
  }
@@ -101,8 +90,7 @@ export class CommandHandler {
101
90
  try {
102
91
  const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
103
92
  await this.deps.sender.sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
104
- `🔄 AI 会话已重置,下一条消息将使用全新上下文。\n` +
105
- this.getClearHistoryHint(platform));
93
+ `🔄 AI 会话已重置,下一条消息将使用全新上下文。`);
106
94
  }
107
95
  catch (err) {
108
96
  await this.deps.sender.sendTextReply(chatId, err instanceof Error ? err.message : String(err));
@@ -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";
@@ -178,6 +184,7 @@ export declare const PAGE_TEXTS: {
178
184
  readonly qqSummary: "QQ 机器人 App ID 与 Secret。";
179
185
  readonly weworkSummary: "企业微信 Corp ID 与 Secret。";
180
186
  readonly dingtalkSummary: "钉钉 Client 凭证,可选配置卡片模板 ID。";
187
+ readonly workbuddySummary: "CodeBuddy OAuth 连接微信客服。";
181
188
  readonly platformCredentialsTitle: "凭证";
182
189
  readonly platformAccessTitle: "路由与访问";
183
190
  readonly platformTestNote: "只会检查该平台的必填凭证是否可用。";
@@ -198,7 +205,12 @@ export declare const PAGE_TEXTS: {
198
205
  readonly clientId: "Client ID / AppKey";
199
206
  readonly clientSecret: "Client Secret / AppSecret";
200
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 连接微信客服。";
201
209
  readonly cardTemplateId: "卡片模板 ID";
210
+ readonly workbuddyAccessToken: "Access Token";
211
+ readonly workbuddyRefreshToken: "Refresh Token";
212
+ readonly workbuddyUserId: "User ID";
213
+ readonly workbuddyBaseUrl: "基础 URL";
202
214
  readonly optional: "可选";
203
215
  readonly commaSeparatedIds: "多个 ID 用逗号分隔";
204
216
  readonly aiTitle: "AI 工具配置";
@@ -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",
@@ -178,6 +184,7 @@ export const PAGE_TEXTS = {
178
184
  qqSummary: "QQ \u673a\u5668\u4eba App ID \u4e0e Secret\u3002",
179
185
  weworkSummary: "\u4f01\u4e1a\u5fae\u4fe1 Corp ID \u4e0e Secret\u3002",
180
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",
181
188
  platformCredentialsTitle: "\u51ed\u8bc1",
182
189
  platformAccessTitle: "\u8def\u7531\u4e0e\u8bbf\u95ee",
183
190
  platformTestNote: "\u53ea\u4f1a\u68c0\u67e5\u8be5\u5e73\u53f0\u7684\u5fc5\u586b\u51ed\u8bc1\u662f\u5426\u53ef\u7528\u3002",
@@ -198,7 +205,12 @@ export const PAGE_TEXTS = {
198
205
  clientId: "Client ID / AppKey",
199
206
  clientSecret: "Client Secret / AppSecret",
200
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',
201
209
  cardTemplateId: "\u5361\u7247\u6a21\u677f ID",
210
+ workbuddyAccessToken: "Access Token",
211
+ workbuddyRefreshToken: "Refresh Token",
212
+ workbuddyUserId: "User ID",
213
+ workbuddyBaseUrl: "\u57fa\u7840 URL",
202
214
  optional: "\u53ef\u9009",
203
215
  commaSeparatedIds: "\u591a\u4e2a ID \u7528\u9017\u53f7\u5206\u9694",
204
216
  aiTitle: "AI \u5de5\u5177\u914d\u7f6e",
@@ -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"];
@@ -828,7 +828,7 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
828
828
  <div class="stats-grid">
829
829
  <div class="stat-card">
830
830
  <div class="stat-label" id="statConfiguredLabel">Configured</div>
831
- <div class="stat-value" id="statConfiguredValue">0/5</div>
831
+ <div class="stat-value" id="statConfiguredValue">0/6</div>
832
832
  </div>
833
833
  <div class="stat-card">
834
834
  <div class="stat-label" id="statEnabledLabel">Enabled</div>
@@ -1058,6 +1058,53 @@ export const PAGE_HTML_PREFIX = String.raw `<!doctype html>
1058
1058
  <div id="test-dingtalk-result" class="mt-4"></div>
1059
1059
  </div>
1060
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>
1061
1108
  </div>
1062
1109
  </section>
1063
1110