@wu529778790/open-im 1.8.3-beta.9 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/README.zh-CN.md +22 -2
- package/dist/setup.js +69 -17
- package/dist/workbuddy/centrifuge-client.d.ts +8 -1
- package/dist/workbuddy/centrifuge-client.js +32 -23
- package/dist/workbuddy/client.js +73 -26
- package/dist/workbuddy/message-sender.js +2 -2
- package/dist/workbuddy/oauth.d.ts +1 -1
- package/dist/workbuddy/oauth.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Multi-platform IM bridge for AI CLI tools. Connect Telegram, Feishu, WeCom, Ding
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- Multi-platform support: Telegram, Feishu, WeCom, DingTalk, QQ,
|
|
9
|
+
- Multi-platform support: Telegram, Feishu, WeCom, DingTalk, QQ, WeChat (experimental), and WorkBuddy (WeChat KF via CodeBuddy), with multiple platforms enabled at the same time
|
|
10
10
|
- Multiple AI tools: Claude, Codex, and CodeBuddy
|
|
11
11
|
- Per-platform AI routing: each IM platform can use a different AI tool, with `aiCommand` as the global default and `platforms.<name>.aiCommand` as the override
|
|
12
12
|
- Streaming replies: relay AI output and tool execution progress in real time (DingTalk streaming is not fully supported yet)
|
|
@@ -56,7 +56,7 @@ Open the config page at [`http://127.0.0.1:39282`](http://127.0.0.1:39282) (or t
|
|
|
56
56
|
- **AI Tooling** – **General**: default AI tool (Claude / Codex / CodeBuddy), work directory, hook port, log level. **Per-tool tabs**: Claude (CLI path, timeout, proxy, config path, ANTHROPIC\_\* fields), Codex (CLI path, timeout, proxy), CodeBuddy (CLI path, timeout).
|
|
57
57
|
- **Service control** – Validate config, Save, Start bridge, Stop bridge.
|
|
58
58
|
|
|
59
|
-
WeChat
|
|
59
|
+
WeChat and WorkBuddy are not in the web UI; configure them in `~/.open-im/config.json` or via `open-im init`.
|
|
60
60
|
|
|
61
61
|
- `open-im start` serves both the config page and the bridge on your local machine.
|
|
62
62
|
- `open-im dev` opens the page automatically only when setup is incomplete.
|
|
@@ -240,6 +240,14 @@ The following is valid JSON and can be saved directly as `~/.open-im/config.json
|
|
|
240
240
|
"allowedUserIds": [],
|
|
241
241
|
"appId": "YOUR_WECHAT_APP_ID",
|
|
242
242
|
"appSecret": "YOUR_WECHAT_APP_SECRET"
|
|
243
|
+
},
|
|
244
|
+
"workbuddy": {
|
|
245
|
+
"enabled": false,
|
|
246
|
+
"aiCommand": "claude",
|
|
247
|
+
"allowedUserIds": [],
|
|
248
|
+
"accessToken": "",
|
|
249
|
+
"refreshToken": "",
|
|
250
|
+
"userId": ""
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
}
|
|
@@ -287,6 +295,13 @@ The following is valid JSON and can be saved directly as `~/.open-im/config.json
|
|
|
287
295
|
| `WECHAT_USER_ID` | WeChat AGP mode user ID |
|
|
288
296
|
| `WECHAT_WS_URL` | WeChat WebSocket URL |
|
|
289
297
|
| `WECHAT_ALLOWED_USER_IDS` | WeChat allowlist |
|
|
298
|
+
| `WORKBUDDY_ACCESS_TOKEN` | WorkBuddy OAuth access token (auto-generated by `open-im init`) |
|
|
299
|
+
| `WORKBUDDY_REFRESH_TOKEN` | WorkBuddy OAuth refresh token (auto-generated by `open-im init`) |
|
|
300
|
+
| `WORKBUDDY_USER_ID` | WorkBuddy user ID |
|
|
301
|
+
| `WORKBUDDY_BASE_URL` | WorkBuddy API base URL, defaults to `https://copilot.tencent.com` |
|
|
302
|
+
| `WORKBUDDY_GUID` | WorkBuddy connection GUID (optional) |
|
|
303
|
+
| `WORKBUDDY_WORKSPACE_PATH` | WorkBuddy workspace path (optional) |
|
|
304
|
+
| `WORKBUDDY_ALLOWED_USER_IDS` | WorkBuddy allowlist |
|
|
290
305
|
|
|
291
306
|
### Platform Setup Sources
|
|
292
307
|
|
|
@@ -296,6 +311,7 @@ The following is valid JSON and can be saved directly as `~/.open-im/config.json
|
|
|
296
311
|
- DingTalk: create an internal enterprise app in DingTalk Open Platform, enable bot Stream Mode, and get the `Client ID` and `Client Secret`
|
|
297
312
|
- WeCom: get the bot ID and secret from the [WeCom admin console](https://work.weixin.qq.com/)
|
|
298
313
|
- WeChat: experimental, supports both standard mode and AGP/Qclaw-related settings
|
|
314
|
+
- WorkBuddy: connects via CodeBuddy (copilot.tencent.com) Centrifuge WebSocket; run `open-im init` and select "WorkBuddy 微信客服 (WeChat KF)" to complete OAuth login and WeChat KF binding
|
|
299
315
|
|
|
300
316
|
Notes on DingTalk: the current implementation uses a hybrid model of "Stream Mode for receiving messages + OpenAPI for sending messages".
|
|
301
317
|
|
|
@@ -337,6 +353,10 @@ DingTalk AI card templates are already compatible with the official "Search Resu
|
|
|
337
353
|
|
|
338
354
|
**CodeBuddy prompts for login**: run `codebuddy login` first. `open-im` does not read CodeBuddy login state from `~/.open-im/config.json`.
|
|
339
355
|
|
|
356
|
+
**WorkBuddy cannot connect**: run `open-im init` to re-authenticate. Tokens may expire — the client will attempt auto-reconnect, but if the refresh token is invalid, a fresh login is required.
|
|
357
|
+
|
|
358
|
+
**WorkBuddy WeChat KF not receiving messages**: ensure the WeChat KF binding was completed during `open-im init`. You can re-run init to generate a new binding link.
|
|
359
|
+
|
|
340
360
|
## License
|
|
341
361
|
|
|
342
362
|
[MIT](LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
|
-
- 多平台:支持 Telegram、飞书、企业微信、钉钉、QQ
|
|
9
|
+
- 多平台:支持 Telegram、飞书、企业微信、钉钉、QQ、微信(测试中)、WorkBuddy(通过 CodeBuddy 接入微信客服),可同时启用
|
|
10
10
|
- 多 AI 工具:支持 Claude、Codex、CodeBuddy
|
|
11
11
|
- 按平台分配 AI:根级 `aiCommand` 作为默认值,`platforms.<name>.aiCommand` 可为不同 IM 单独指定 AI 工具
|
|
12
12
|
- 流式输出:实时回传 AI 回复与工具执行进度(目前钉钉暂未实现流式传输)
|
|
@@ -62,7 +62,7 @@ open-im start
|
|
|
62
62
|
- **AI 工具配置** – **公共**:默认 AI 工具(Claude / Codex / CodeBuddy)、工作目录、Hook 端口、日志级别。**分工具**:Claude(CLI 路径、超时、代理、配置路径、ANTHROPIC\_\* 等)、Codex(CLI 路径、超时、代理)、CodeBuddy(CLI 路径、超时)
|
|
63
63
|
- **服务控制** – 校验配置、保存、启动桥接、停止桥接
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
微信和 WorkBuddy 暂不在网页中配置,如需使用请在 `~/.open-im/config.json` 中手动配置或通过 `open-im init` 引导。
|
|
66
66
|
|
|
67
67
|
- `open-im start` 会同时启动桥接服务并提供该配置页(本机场景)。
|
|
68
68
|
- `open-im dev` 仅在未完成配置时自动打开页面。
|
|
@@ -246,6 +246,14 @@ codebuddy login
|
|
|
246
246
|
"allowedUserIds": [],
|
|
247
247
|
"appId": "YOUR_WECHAT_APP_ID",
|
|
248
248
|
"appSecret": "YOUR_WECHAT_APP_SECRET"
|
|
249
|
+
},
|
|
250
|
+
"workbuddy": {
|
|
251
|
+
"enabled": false,
|
|
252
|
+
"aiCommand": "claude",
|
|
253
|
+
"allowedUserIds": [],
|
|
254
|
+
"accessToken": "",
|
|
255
|
+
"refreshToken": "",
|
|
256
|
+
"userId": ""
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
259
|
}
|
|
@@ -293,6 +301,13 @@ codebuddy login
|
|
|
293
301
|
| `WECHAT_USER_ID` | 微信 AGP 模式 User ID |
|
|
294
302
|
| `WECHAT_WS_URL` | 微信 WebSocket 地址 |
|
|
295
303
|
| `WECHAT_ALLOWED_USER_IDS` | 微信白名单 |
|
|
304
|
+
| `WORKBUDDY_ACCESS_TOKEN` | WorkBuddy OAuth 访问令牌(由 `open-im init` 自动生成) |
|
|
305
|
+
| `WORKBUDDY_REFRESH_TOKEN` | WorkBuddy OAuth 刷新令牌(由 `open-im init` 自动生成) |
|
|
306
|
+
| `WORKBUDDY_USER_ID` | WorkBuddy 用户 ID |
|
|
307
|
+
| `WORKBUDDY_BASE_URL` | WorkBuddy API 地址,默认 `https://copilot.tencent.com` |
|
|
308
|
+
| `WORKBUDDY_GUID` | WorkBuddy 连接 GUID(可选) |
|
|
309
|
+
| `WORKBUDDY_WORKSPACE_PATH` | WorkBuddy 工作区路径(可选) |
|
|
310
|
+
| `WORKBUDDY_ALLOWED_USER_IDS` | WorkBuddy 白名单 |
|
|
296
311
|
|
|
297
312
|
### 平台配置来源
|
|
298
313
|
|
|
@@ -302,6 +317,7 @@ codebuddy login
|
|
|
302
317
|
- 钉钉:从钉钉开放平台创建企业内部应用,启用机器人 Stream Mode,获取 `Client ID` 和 `Client Secret`
|
|
303
318
|
- 企业微信:从 [企业微信管理后台](https://work.weixin.qq.com/) 获取 Bot ID 和 Secret
|
|
304
319
|
- 微信:测试中,支持标准模式和 AGP/Qclaw 相关配置
|
|
320
|
+
- WorkBuddy:通过 CodeBuddy(copilot.tencent.com)Centrifuge WebSocket 接入微信客服;运行 `open-im init` 并选择 "WorkBuddy 微信客服 (WeChat KF)" 完成 OAuth 登录和微信客服绑定
|
|
305
321
|
|
|
306
322
|
说明:钉钉当前采用“Stream Mode 收消息 + OpenAPI 发送消息”的混合模式。
|
|
307
323
|
|
|
@@ -343,6 +359,10 @@ codebuddy login
|
|
|
343
359
|
|
|
344
360
|
**CodeBuddy 提示需要登录**:先执行 `codebuddy login`。`open-im` 不会从 `~/.open-im/config.json` 读取 CodeBuddy 的登录态。
|
|
345
361
|
|
|
362
|
+
**WorkBuddy 无法连接**:重新运行 `open-im init` 登录。Token 可能过期——客户端会尝试自动重连,但如果 refresh token 失效,需要重新登录。
|
|
363
|
+
|
|
364
|
+
**WorkBuddy 微信客服收不到消息**:确认在 `open-im init` 中完成了微信客服绑定。可重新运行 init 生成新的绑定链接。
|
|
365
|
+
|
|
346
366
|
## License
|
|
347
367
|
|
|
348
368
|
[MIT](LICENSE)
|
package/dist/setup.js
CHANGED
|
@@ -442,6 +442,8 @@ export async function runInteractiveSetup() {
|
|
|
442
442
|
}, { onCancel });
|
|
443
443
|
if (wbModeResp.mode === "oauth") {
|
|
444
444
|
console.log("\n正在启动 WorkBuddy OAuth 登录...\n");
|
|
445
|
+
// Phase 1: OAuth token acquisition (fatal if fails)
|
|
446
|
+
let oauthOk = false;
|
|
445
447
|
try {
|
|
446
448
|
const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
|
|
447
449
|
const oauth = new WorkBuddyOAuth();
|
|
@@ -459,39 +461,58 @@ export async function runInteractiveSetup() {
|
|
|
459
461
|
catch {
|
|
460
462
|
// account info is optional
|
|
461
463
|
}
|
|
462
|
-
const
|
|
464
|
+
const ai = accountInfo;
|
|
465
|
+
// Try multiple field names; fall back to tokenResult.userId, then existing config
|
|
466
|
+
const existingUserId = wb?.userId;
|
|
467
|
+
const userId = String(ai?.uid ?? ai?.userId ?? ai?.user_id ??
|
|
468
|
+
tokenResult.userId ??
|
|
469
|
+
existingUserId ?? "");
|
|
470
|
+
if (!userId) {
|
|
471
|
+
console.warn("⚠️ 未能获取 WorkBuddy 用户 ID,sessionId 可能不正确,建议稍后重试");
|
|
472
|
+
}
|
|
463
473
|
// Set oauth.userId so buildSessionId() produces the correct sessionId.
|
|
464
474
|
oauth.userId = userId;
|
|
475
|
+
console.log(` userId: ${userId || "(空)"}, sessionId 将为: ${oauth.buildSessionId()}`);
|
|
476
|
+
// Save credentials immediately — even if binding fails below
|
|
465
477
|
config.platforms.workbuddy = {
|
|
466
478
|
enabled: true,
|
|
467
479
|
accessToken: tokenResult.accessToken,
|
|
468
480
|
refreshToken: tokenResult.refreshToken,
|
|
469
481
|
userId,
|
|
470
482
|
};
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
if (
|
|
481
|
-
console.log(
|
|
483
|
+
oauthOk = true;
|
|
484
|
+
console.log("\n✅ WorkBuddy 凭证已获取,正在生成微信客服绑定链接...");
|
|
485
|
+
// Phase 2: WeChat KF binding link (non-fatal, no polling needed)
|
|
486
|
+
try {
|
|
487
|
+
const { join: pathJoin } = await import("node:path");
|
|
488
|
+
const { homedir } = await import("node:os");
|
|
489
|
+
const clawPath = pathJoin(homedir(), "WorkBuddy", "Claw");
|
|
490
|
+
const sessionId = oauth.buildSessionId(clawPath);
|
|
491
|
+
const linkResult = await oauth.getWeChatKfLink(sessionId);
|
|
492
|
+
if (linkResult.success && linkResult.url) {
|
|
493
|
+
console.log("\n━━━ 微信客服绑定 ━━━");
|
|
494
|
+
console.log("请将以下链接发给微信「文件传输助手」并点击打开,即完成绑定:");
|
|
495
|
+
console.log(linkResult.url);
|
|
496
|
+
console.log("\n绑定完成后运行 open-im start 启动服务即可收发消息。");
|
|
482
497
|
}
|
|
483
498
|
else {
|
|
484
|
-
console.log(
|
|
499
|
+
console.log(`⚠️ 获取微信客服链接失败: ${linkResult.message ?? "未知错误"}`);
|
|
500
|
+
console.log(" 凭证已保存,稍后重新运行 open-im init 可重试绑定");
|
|
485
501
|
}
|
|
486
502
|
}
|
|
487
|
-
|
|
488
|
-
|
|
503
|
+
catch (bindErr) {
|
|
504
|
+
const cause = bindErr?.cause ?? bindErr?.code;
|
|
505
|
+
const detail = cause ? ` (${cause})` : "";
|
|
506
|
+
console.log(`⚠️ 获取微信客服绑定链接网络失败: ${bindErr instanceof Error ? bindErr.message : String(bindErr)}${detail}`);
|
|
507
|
+
console.log(" 凭证已保存,稍后重新运行 open-im init 可重试绑定");
|
|
489
508
|
}
|
|
490
509
|
console.log("\n✅ WorkBuddy 登录成功,配置已保存");
|
|
491
510
|
}
|
|
492
511
|
catch (err) {
|
|
493
|
-
|
|
494
|
-
|
|
512
|
+
const cause = err?.cause ?? err?.code;
|
|
513
|
+
const detail = cause ? ` (${cause})` : "";
|
|
514
|
+
console.error(`\n❌ WorkBuddy 登录失败: ${err instanceof Error ? err.message : String(err)}${detail}`);
|
|
515
|
+
if (!oauthOk && platform === "workbuddy")
|
|
495
516
|
return false;
|
|
496
517
|
}
|
|
497
518
|
}
|
|
@@ -500,6 +521,37 @@ export async function runInteractiveSetup() {
|
|
|
500
521
|
...wb,
|
|
501
522
|
enabled: true,
|
|
502
523
|
};
|
|
524
|
+
// Also (re-)generate the WeChat KF binding link with existing credentials
|
|
525
|
+
try {
|
|
526
|
+
const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
|
|
527
|
+
const oauthKeep = new WorkBuddyOAuth(wb?.baseUrl);
|
|
528
|
+
oauthKeep.loadCredentials({
|
|
529
|
+
accessToken: wb?.accessToken ?? '',
|
|
530
|
+
refreshToken: wb?.refreshToken ?? '',
|
|
531
|
+
userId: wb?.userId ?? '',
|
|
532
|
+
});
|
|
533
|
+
oauthKeep.userId = wb?.userId ?? '';
|
|
534
|
+
const { join: pathJoin2 } = await import("node:path");
|
|
535
|
+
const { homedir: homedir2 } = await import("node:os");
|
|
536
|
+
const clawPath2 = pathJoin2(homedir2(), "WorkBuddy", "Claw");
|
|
537
|
+
const sessionId = oauthKeep.buildSessionId(clawPath2);
|
|
538
|
+
console.log(`\n正在获取微信客服绑定链接... (sessionId: ${sessionId})`);
|
|
539
|
+
const linkResult = await oauthKeep.getWeChatKfLink(sessionId);
|
|
540
|
+
if (linkResult.success && linkResult.url) {
|
|
541
|
+
console.log("\n━━━ 微信客服绑定 ━━━");
|
|
542
|
+
console.log("请将以下链接发给微信「文件传输助手」并点击打开,即完成绑定:");
|
|
543
|
+
console.log(linkResult.url);
|
|
544
|
+
console.log("\n绑定完成后运行 open-im start 启动服务即可收发消息。");
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
console.log(`⚠️ 获取绑定链接失败: ${linkResult.message ?? "未知"}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (keepErr) {
|
|
551
|
+
const cause = keepErr?.cause ?? keepErr?.code;
|
|
552
|
+
console.log(`⚠️ 绑定链接请求失败: ${keepErr instanceof Error ? keepErr.message : String(keepErr)}${cause ? ` (${cause})` : ""}`);
|
|
553
|
+
console.log(" 稍后重新运行 open-im init 可重试");
|
|
554
|
+
}
|
|
503
555
|
}
|
|
504
556
|
else if (platform === "workbuddy") {
|
|
505
557
|
return false;
|
|
@@ -13,6 +13,13 @@ export interface CentrifugeClientConfig {
|
|
|
13
13
|
httpBaseUrl?: string;
|
|
14
14
|
httpAccessToken?: string;
|
|
15
15
|
workspaceSessionId?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Called before sending a WeChat KF reply to update the channel's channelId
|
|
18
|
+
* to the current WeChat user's externalUserId. The WorkBuddy server uses the
|
|
19
|
+
* registered channelId as the WeChat send_msg `touser`, so this must match the
|
|
20
|
+
* customer we are replying to.
|
|
21
|
+
*/
|
|
22
|
+
registerChannelFn?: (externalUserId: string) => Promise<void>;
|
|
16
23
|
}
|
|
17
24
|
/** Client callbacks */
|
|
18
25
|
export interface CentrifugeCallbacks {
|
|
@@ -58,7 +65,7 @@ export declare class WorkBuddyCentrifugeClient {
|
|
|
58
65
|
/**
|
|
59
66
|
* Send prompt response (for WeChat KF, use HTTP instead)
|
|
60
67
|
*/
|
|
61
|
-
sendPromptResponse(payload: PromptResponsePayload, _guid?: string, _userId?: string): void
|
|
68
|
+
sendPromptResponse(payload: PromptResponsePayload, _guid?: string, _userId?: string): Promise<void>;
|
|
62
69
|
/**
|
|
63
70
|
* Handle incoming publication from Centrifuge
|
|
64
71
|
*/
|
|
@@ -141,38 +141,47 @@ export class WorkBuddyCentrifugeClient {
|
|
|
141
141
|
/**
|
|
142
142
|
* Send prompt response (for WeChat KF, use HTTP instead)
|
|
143
143
|
*/
|
|
144
|
-
sendPromptResponse(payload, _guid, _userId) {
|
|
144
|
+
async sendPromptResponse(payload, _guid, _userId) {
|
|
145
145
|
// WeChat KF messages: send via HTTP COPILOT_RESPONSE
|
|
146
146
|
if (this.config.httpBaseUrl && this.config.httpAccessToken) {
|
|
147
147
|
const message = payload.content?.map((c) => c.text).join('') || payload.error || '';
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
const sessionId = payload.session_id; // e.g. "wmXXX::origin::wechatkfProxy"
|
|
149
|
+
// The WorkBuddy server uses the registered channelId as the WeChat KF send_msg
|
|
150
|
+
// `touser`. Re-register the channel with the current WeChat user's externalUserId
|
|
151
|
+
// so that the server sends the reply to the correct customer.
|
|
152
|
+
if (this.config.registerChannelFn && sessionId.includes('::')) {
|
|
153
|
+
const externalUserId = sessionId.split('::')[0];
|
|
154
|
+
try {
|
|
155
|
+
await this.config.registerChannelFn(externalUserId);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
log.warn(`${this.logPrefix} registerChannelFn failed (reply may go to wrong user):`, err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
152
161
|
const httpPayload = {
|
|
153
162
|
type: 'COPILOT_RESPONSE',
|
|
154
163
|
msgId: payload.prompt_id,
|
|
155
|
-
chatId:
|
|
156
|
-
toUser,
|
|
164
|
+
chatId: sessionId,
|
|
157
165
|
success: payload.stop_reason === 'end_turn',
|
|
158
166
|
message,
|
|
159
167
|
metadata: {
|
|
160
|
-
sessionId: this.config.workspaceSessionId ||
|
|
161
|
-
|
|
168
|
+
sessionId: this.config.workspaceSessionId || sessionId,
|
|
169
|
+
requestId: payload.prompt_id,
|
|
170
|
+
state: payload.stop_reason === 'end_turn' ? 'completed' : payload.stop_reason,
|
|
162
171
|
},
|
|
163
172
|
};
|
|
164
173
|
const url = `${this.config.httpBaseUrl}/v2/backgroundagent/wecom/local-proxy/receive`;
|
|
165
|
-
log.debug(`${this.logPrefix} HTTP COPILOT_RESPONSE → ${url} chatId=${
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
log.debug(`${this.logPrefix} HTTP COPILOT_RESPONSE → ${url} chatId=${sessionId} msgLen=${message.length}`);
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch(url, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
'Content-Type': 'application/json',
|
|
180
|
+
Authorization: `Bearer ${this.config.httpAccessToken}`,
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify(httpPayload),
|
|
183
|
+
signal: AbortSignal.timeout(30_000),
|
|
184
|
+
});
|
|
176
185
|
const body = await res.text().catch(() => '');
|
|
177
186
|
if (!res.ok) {
|
|
178
187
|
log.error(`${this.logPrefix} HTTP COPILOT_RESPONSE failed: ${res.status} ${body.substring(0, 300)}`);
|
|
@@ -180,10 +189,10 @@ export class WorkBuddyCentrifugeClient {
|
|
|
180
189
|
else {
|
|
181
190
|
log.info(`${this.logPrefix} HTTP COPILOT_RESPONSE ok: ${res.status} ${body.substring(0, 200)}`);
|
|
182
191
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
185
194
|
log.error(`${this.logPrefix} HTTP COPILOT_RESPONSE error:`, err);
|
|
186
|
-
}
|
|
195
|
+
}
|
|
187
196
|
return;
|
|
188
197
|
}
|
|
189
198
|
this.sendEnvelope('session.promptResponse', payload, _guid, _userId);
|
package/dist/workbuddy/client.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { hostname } from 'node:os';
|
|
10
|
+
import { hostname, homedir } from 'node:os';
|
|
11
11
|
import { createLogger } from '../logger.js';
|
|
12
12
|
import { WorkBuddyOAuth } from './oauth.js';
|
|
13
13
|
import { WorkBuddyCentrifugeClient } from './centrifuge-client.js';
|
|
@@ -62,31 +62,46 @@ async function connect() {
|
|
|
62
62
|
const pc = platformConfig;
|
|
63
63
|
const baseUrl = pc.baseUrl ?? 'https://copilot.tencent.com';
|
|
64
64
|
const hostId = hostname();
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Claw workspace path — matches the plugin's WorkBuddy Claw installation path.
|
|
66
|
+
// The directory does NOT need to exist; the server uses it as a string identifier.
|
|
67
|
+
const clawPath = join(homedir(), 'WorkBuddy', 'Claw');
|
|
68
|
+
log.info('Registering WorkBuddy host workspace...');
|
|
67
69
|
let tokens;
|
|
68
70
|
try {
|
|
71
|
+
// Step 1: Register host workspace (workspaceId="") — gets the Centrifuge connection
|
|
69
72
|
tokens = await oauth.registerWorkspace({
|
|
70
73
|
userId: pc.userId ?? '',
|
|
71
74
|
hostId,
|
|
72
|
-
workspaceId:
|
|
73
|
-
workspaceName: '
|
|
75
|
+
workspaceId: '',
|
|
76
|
+
workspaceName: 'Host Channel',
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
catch (err) {
|
|
77
|
-
log.error('
|
|
80
|
+
log.error('Host workspace registration failed:', err);
|
|
78
81
|
scheduleReconnect();
|
|
79
82
|
return;
|
|
80
83
|
}
|
|
81
|
-
// sessionId
|
|
82
|
-
const workspaceSessionId = oauth.buildSessionId();
|
|
84
|
+
// sessionId = userId_hostId_clawPath (matches plugin's buildSessionId format)
|
|
85
|
+
const workspaceSessionId = oauth.buildSessionId(clawPath);
|
|
83
86
|
const channel = tokens.channel;
|
|
84
87
|
const guid = pc.guid ?? randomUUID();
|
|
85
|
-
log.info(`
|
|
88
|
+
log.info(`Host workspace registered: channel=${channel}, clawSessionId=${workspaceSessionId}`);
|
|
86
89
|
if (centrifugeClient) {
|
|
87
90
|
centrifugeClient.stop();
|
|
88
91
|
centrifugeClient = null;
|
|
89
92
|
}
|
|
93
|
+
// Re-registers the WeChat KF channel with the given externalUserId as channelId.
|
|
94
|
+
// The WorkBuddy server uses channelId as the WeChat send_msg `touser`, so this
|
|
95
|
+
// must be called with the customer's external_userid before sending each reply.
|
|
96
|
+
const registerChannelFn = async (externalUserId) => {
|
|
97
|
+
const clawSessionId = oauth.buildSessionId(clawPath);
|
|
98
|
+
await oauth.registerChannel({
|
|
99
|
+
type: 'wechatkf',
|
|
100
|
+
sessionId: clawSessionId,
|
|
101
|
+
channelId: externalUserId,
|
|
102
|
+
userId: pc.userId ?? '',
|
|
103
|
+
});
|
|
104
|
+
};
|
|
90
105
|
centrifugeClient = new WorkBuddyCentrifugeClient({
|
|
91
106
|
url: tokens.url,
|
|
92
107
|
connectionToken: tokens.connectionToken,
|
|
@@ -97,28 +112,60 @@ async function connect() {
|
|
|
97
112
|
httpBaseUrl: baseUrl,
|
|
98
113
|
httpAccessToken: pc.accessToken ?? '',
|
|
99
114
|
workspaceSessionId,
|
|
115
|
+
registerChannelFn,
|
|
100
116
|
}, {
|
|
101
117
|
onConnected: () => {
|
|
102
118
|
log.info('WorkBuddy Centrifuge connected');
|
|
103
|
-
log.info(`
|
|
119
|
+
log.info(`WeChat KF sessionId: ${workspaceSessionId}`);
|
|
104
120
|
reconnectAttempt = 0;
|
|
105
121
|
updateState('connected');
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
// Step 2: Register Claw workspace to get WeChat KF routing channel + sessionId
|
|
123
|
+
oauth.registerWorkspace({
|
|
124
|
+
userId: pc.userId ?? '',
|
|
125
|
+
hostId,
|
|
126
|
+
workspaceId: clawPath,
|
|
127
|
+
workspaceName: 'Claw',
|
|
128
|
+
}).then((clawParams) => {
|
|
129
|
+
const clawSessionId = clawParams.sessionId ?? workspaceSessionId;
|
|
130
|
+
log.info(`Claw workspace registered: channel=${clawParams.channel}, sessionId=${clawSessionId}`);
|
|
131
|
+
// Subscribe to Claw channel — WeChat KF messages are published here
|
|
132
|
+
centrifugeClient?.subscribeChannel(clawParams.channel, clawParams.subscriptionToken);
|
|
133
|
+
const doRegister = () => {
|
|
134
|
+
if (stopped || channelState !== 'connected')
|
|
135
|
+
return;
|
|
136
|
+
oauth.registerChannel({
|
|
137
|
+
type: 'wechatkf',
|
|
138
|
+
sessionId: clawSessionId,
|
|
139
|
+
channelId: pc.userId ?? '', // plugin uses userId, not full channel name
|
|
140
|
+
userId: pc.userId ?? '',
|
|
141
|
+
})
|
|
142
|
+
.then((res) => log.info(`WeChat KF channel registered (online): ${JSON.stringify(res)}`))
|
|
143
|
+
.catch((err) => log.warn(`registerChannel failed: ${String(err)}`));
|
|
144
|
+
};
|
|
145
|
+
doRegister();
|
|
146
|
+
if (heartbeatTimer)
|
|
147
|
+
clearInterval(heartbeatTimer);
|
|
148
|
+
heartbeatTimer = setInterval(doRegister, CHANNEL_HEARTBEAT_MS);
|
|
149
|
+
}).catch((err) => {
|
|
150
|
+
log.error('Claw workspace registration failed:', err);
|
|
151
|
+
// Fallback: register with host sessionId
|
|
152
|
+
const doRegister = () => {
|
|
153
|
+
if (stopped || channelState !== 'connected')
|
|
154
|
+
return;
|
|
155
|
+
oauth.registerChannel({
|
|
156
|
+
type: 'wechatkf',
|
|
157
|
+
sessionId: workspaceSessionId,
|
|
158
|
+
channelId: pc.userId ?? '',
|
|
159
|
+
userId: pc.userId ?? '',
|
|
160
|
+
})
|
|
161
|
+
.then((res) => log.info(`WeChat KF channel registered (fallback): ${JSON.stringify(res)}`))
|
|
162
|
+
.catch((e) => log.warn(`registerChannel failed: ${String(e)}`));
|
|
163
|
+
};
|
|
164
|
+
doRegister();
|
|
165
|
+
if (heartbeatTimer)
|
|
166
|
+
clearInterval(heartbeatTimer);
|
|
167
|
+
heartbeatTimer = setInterval(doRegister, CHANNEL_HEARTBEAT_MS);
|
|
168
|
+
});
|
|
122
169
|
},
|
|
123
170
|
onDisconnected: (reason) => {
|
|
124
171
|
log.info(`WorkBuddy Centrifuge disconnected: ${reason}`);
|
|
@@ -14,7 +14,7 @@ export async function sendTextReply(_client, chatId, text, msgId) {
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
log.info(`Sending WorkBuddy reply to chatId=${chatId}, msgId=${msgId}`);
|
|
17
|
-
client.sendPromptResponse({
|
|
17
|
+
await client.sendPromptResponse({
|
|
18
18
|
session_id: chatId,
|
|
19
19
|
prompt_id: msgId,
|
|
20
20
|
content: [{ type: 'text', text }],
|
|
@@ -31,7 +31,7 @@ export async function sendErrorReply(_client, chatId, error, msgId) {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
log.info(`Sending WorkBuddy error to chatId=${chatId}, msgId=${msgId}`);
|
|
34
|
-
client.sendPromptResponse({
|
|
34
|
+
await client.sendPromptResponse({
|
|
35
35
|
session_id: chatId,
|
|
36
36
|
prompt_id: msgId,
|
|
37
37
|
error,
|
package/dist/workbuddy/oauth.js
CHANGED
|
@@ -174,10 +174,10 @@ export class WorkBuddyOAuth {
|
|
|
174
174
|
/**
|
|
175
175
|
* Build sessionId for WorkBuddy workspace
|
|
176
176
|
*/
|
|
177
|
-
buildSessionId(
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
return `${this.userId}_${this.hostId}`;
|
|
177
|
+
buildSessionId(workspacePath) {
|
|
178
|
+
// Must match the plugin's format: ${userId}_${hostId}_${workspacePath}
|
|
179
|
+
// The server uses this as the chatId/routing key for WeChat KF messages.
|
|
180
|
+
return `${this.userId}_${this.hostId}_${workspacePath ?? ''}`;
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
183
|
* Get WeChat KF binding link
|