@wu529778790/open-im 1.8.3-beta.7 → 1.8.3-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/setup.js CHANGED
@@ -460,6 +460,10 @@ export async function runInteractiveSetup() {
460
460
  // account info is optional
461
461
  }
462
462
  const userId = accountInfo?.uid?.toString() ?? "";
463
+ // Set oauth.userId so buildSessionId() produces the correct sessionId.
464
+ // Without this, the binding sessionId has an empty userId prefix, which
465
+ // won't match the sessionId the transport uses at runtime.
466
+ oauth.userId = userId;
463
467
  config.platforms.wechat = {
464
468
  enabled: true,
465
469
  workbuddyAccessToken: tokenResult.accessToken,
@@ -20,10 +20,16 @@ export declare class WorkBuddyTransport implements WeChatTransport {
20
20
  private state;
21
21
  private centrifugeClient;
22
22
  private oauth;
23
+ private stopped;
24
+ private reconnectAttempt;
25
+ private reconnectTimer;
26
+ private heartbeatTimer;
23
27
  private messageHandler;
24
28
  private stateChangeHandler;
25
29
  constructor(config: WorkBuddyTransportConfig);
26
30
  start(): Promise<void>;
31
+ private connect;
32
+ private scheduleReconnect;
27
33
  stop(): void;
28
34
  send(method: string, payload: unknown, replyTo?: string): void;
29
35
  onMessage(handler: MessageHandler): void;
@@ -12,28 +12,43 @@ import { createLogger } from '../logger.js';
12
12
  import { WorkBuddyCentrifugeClient } from '../workbuddy/centrifuge-client.js';
13
13
  import { WorkBuddyOAuth } from '../workbuddy/oauth.js';
14
14
  const log = createLogger('WeChat:WorkBuddy');
15
+ const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
16
+ const CHANNEL_HEARTBEAT_MS = 30_000;
15
17
  export class WorkBuddyTransport {
16
18
  config;
17
19
  state = 'disconnected';
18
20
  centrifugeClient = null;
19
21
  oauth = null;
22
+ stopped = false;
23
+ reconnectAttempt = 0;
24
+ reconnectTimer = null;
25
+ heartbeatTimer = null;
20
26
  messageHandler = null;
21
27
  stateChangeHandler = null;
22
28
  constructor(config) {
23
29
  this.config = config;
24
30
  }
25
31
  async start() {
32
+ this.stopped = false;
33
+ await this.connect();
34
+ }
35
+ async connect() {
36
+ if (this.stopped)
37
+ return;
26
38
  this.updateState('connecting');
27
39
  try {
28
- // Initialize OAuth
29
- this.oauth = new WorkBuddyOAuth(this.config.baseUrl ?? 'https://copilot.tencent.com');
30
- this.oauth.accessToken = this.config.accessToken;
31
- this.oauth.refreshToken = this.config.refreshToken;
32
- this.oauth.userId = this.config.userId;
33
- // Register workspace to get Centrifuge tokens
34
- log.info('Registering workspace for Centrifuge tokens...');
40
+ // Initialize (or reuse) OAuth
41
+ if (!this.oauth) {
42
+ this.oauth = new WorkBuddyOAuth(this.config.baseUrl ?? 'https://copilot.tencent.com');
43
+ this.oauth.accessToken = this.config.accessToken;
44
+ this.oauth.refreshToken = this.config.refreshToken;
45
+ this.oauth.userId = this.config.userId;
46
+ }
47
+ // Use a stable workspaceId so the server maintains a consistent channel
48
+ // across restarts — otherwise WeChat KF loses the routing association.
35
49
  const hostId = this.config.hostId ?? hostname();
36
- const workspaceId = randomUUID();
50
+ const workspaceId = `${hostId}-open-im-wechat`;
51
+ log.info('Registering workspace for Centrifuge tokens...');
37
52
  const tokens = await this.oauth.registerWorkspace({
38
53
  userId: this.config.userId,
39
54
  hostId,
@@ -41,15 +56,21 @@ export class WorkBuddyTransport {
41
56
  workspaceName: 'open-im-wechat',
42
57
  });
43
58
  log.info(`Workspace registered: channel=${tokens.channel}`);
44
- // Build workspaceSessionId for HTTP COPILOT_RESPONSE metadata
59
+ // workspaceSessionId must match the sessionId used when the WeChat KF was bound
45
60
  const workspacePath = this.config.workspacePath ?? join(homedir(), 'WorkBuddy', 'Claw');
46
61
  const workspaceSessionId = `${this.config.userId}_${hostId}_${workspacePath}`;
47
- // Create Centrifuge client
62
+ const channel = tokens.channel;
63
+ const oauth = this.oauth;
64
+ // Tear down previous client before creating a new one
65
+ if (this.centrifugeClient) {
66
+ this.centrifugeClient.stop();
67
+ this.centrifugeClient = null;
68
+ }
48
69
  const clientConfig = {
49
70
  url: tokens.url,
50
71
  connectionToken: tokens.connectionToken,
51
72
  subscriptionToken: tokens.subscriptionToken,
52
- channel: tokens.channel,
73
+ channel,
53
74
  guid: this.config.guid ?? randomUUID(),
54
75
  userId: this.config.userId,
55
76
  httpBaseUrl: this.config.baseUrl ?? 'https://copilot.tencent.com',
@@ -59,18 +80,43 @@ export class WorkBuddyTransport {
59
80
  const callbacks = {
60
81
  onConnected: () => {
61
82
  log.info('WorkBuddy Centrifuge connected');
83
+ log.info(`WorkBuddy sessionId (must match binding): ${workspaceSessionId}`);
84
+ this.reconnectAttempt = 0;
62
85
  this.updateState('connected');
86
+ const doRegister = () => {
87
+ if (this.stopped || this.state !== 'connected')
88
+ return;
89
+ oauth.registerChannel({
90
+ type: 'wechatkf',
91
+ sessionId: workspaceSessionId,
92
+ channelId: channel,
93
+ userId: this.config.userId,
94
+ }).then((res) => {
95
+ log.info(`Channel registered (WeChat KF online): ${JSON.stringify(res)}`);
96
+ }).catch((err) => {
97
+ log.warn(`registerChannel heartbeat failed: ${String(err)}`);
98
+ });
99
+ };
100
+ // Register immediately and then keep heartbeat alive.
101
+ doRegister();
102
+ if (this.heartbeatTimer)
103
+ clearInterval(this.heartbeatTimer);
104
+ this.heartbeatTimer = setInterval(doRegister, CHANNEL_HEARTBEAT_MS);
63
105
  },
64
106
  onDisconnected: (reason) => {
65
107
  log.info(`WorkBuddy Centrifuge disconnected: ${reason}`);
108
+ if (this.heartbeatTimer) {
109
+ clearInterval(this.heartbeatTimer);
110
+ this.heartbeatTimer = null;
111
+ }
66
112
  this.updateState('disconnected');
113
+ this.scheduleReconnect();
67
114
  },
68
115
  onError: (error) => {
69
116
  log.error('WorkBuddy Centrifuge error:', error);
70
117
  this.updateState('error');
71
118
  },
72
119
  onMessage: async (chatId, msgId, content) => {
73
- // Normalize WeChat KF or AGP message to AGPEnvelope
74
120
  const envelope = {
75
121
  msg_id: msgId,
76
122
  guid: this.config.guid,
@@ -90,14 +136,37 @@ export class WorkBuddyTransport {
90
136
  this.centrifugeClient = new WorkBuddyCentrifugeClient(clientConfig, callbacks);
91
137
  this.centrifugeClient.start();
92
138
  log.info('WorkBuddy transport initialized');
139
+ this.reconnectAttempt = 0;
93
140
  }
94
141
  catch (err) {
95
142
  log.error('Failed to start WorkBuddy transport:', err);
96
143
  this.updateState('error');
97
- throw err;
144
+ this.scheduleReconnect();
98
145
  }
99
146
  }
147
+ scheduleReconnect() {
148
+ if (this.stopped)
149
+ return;
150
+ if (this.reconnectTimer)
151
+ return;
152
+ const delayMs = RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
153
+ this.reconnectAttempt++;
154
+ log.info(`WorkBuddy transport reconnecting in ${delayMs}ms (attempt ${this.reconnectAttempt})...`);
155
+ this.reconnectTimer = setTimeout(() => {
156
+ this.reconnectTimer = null;
157
+ void this.connect();
158
+ }, delayMs);
159
+ }
100
160
  stop() {
161
+ this.stopped = true;
162
+ if (this.heartbeatTimer) {
163
+ clearInterval(this.heartbeatTimer);
164
+ this.heartbeatTimer = null;
165
+ }
166
+ if (this.reconnectTimer) {
167
+ clearTimeout(this.reconnectTimer);
168
+ this.reconnectTimer = null;
169
+ }
101
170
  if (this.centrifugeClient) {
102
171
  this.centrifugeClient.stop();
103
172
  this.centrifugeClient = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.8.3-beta.7",
3
+ "version": "1.8.3-beta.8",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",