@xgjktech/xg_cwork_im 1.0.4 → 1.0.5
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 +11 -1
- package/package.json +1 -1
- package/src/channel.ts +88 -40
- package/src/send-service.ts +1 -1
- package/src/types.ts +7 -0
package/README.md
CHANGED
|
@@ -11,7 +11,11 @@ IM 系统 → WebSocket → 插件(本项目) → OpenClaw AI → IM
|
|
|
11
11
|
1. 插件启动时用 `appKey` 换取 `access_token` 和 `userId`
|
|
12
12
|
2. 建立 WebSocket 长连接,接收 `robotMention` 事件
|
|
13
13
|
3. 将收到的消息转发给 OpenClaw 处理
|
|
14
|
-
4. OpenClaw AI
|
|
14
|
+
4. OpenClaw AI 回复后,插件:
|
|
15
|
+
- 先通过 WebSocket `START` 让 IM 端显示一条「思考中」占位消息;
|
|
16
|
+
- 当 AI 产出第一条完整回复时,用 HTTP `/im/message/send` 覆盖这条占位消息;
|
|
17
|
+
- 若有多条回复,则后续每条都通过 HTTP 作为**独立消息**发送;
|
|
18
|
+
- 首条回复若在配置的超时时间内一直未产生,会自动将占位消息更新为「当前请求处理超时,请稍后重试」。
|
|
15
19
|
|
|
16
20
|
---
|
|
17
21
|
|
|
@@ -166,6 +170,9 @@ openclaw gateway restart
|
|
|
166
170
|
"wsBaseUrl": "wss://cwork-web-test.xgjktech.com.cn",
|
|
167
171
|
"groupPolicy": "mention",
|
|
168
172
|
"debug": false,
|
|
173
|
+
// 可选:首条 AI 回复超时时间(毫秒),默认 5 分钟
|
|
174
|
+
// 未配置时:5 * 60_000 = 300000
|
|
175
|
+
"firstReplyTimeoutMs": 300000,
|
|
169
176
|
"accounts": [
|
|
170
177
|
{ "appKey": "appKey_机器人A", "agentId": "main", "name": "个人助手" },
|
|
171
178
|
{ "appKey": "appKey_机器人B", "agentId": "sales", "name": "销售助手" }
|
|
@@ -187,6 +194,8 @@ openclaw gateway restart
|
|
|
187
194
|
"debug": false,
|
|
188
195
|
"maxConnectionAttempts": 20,
|
|
189
196
|
"maxReconnectDelay": 120000,
|
|
197
|
+
// 生产环境可根据需要调整首回复超时时间(毫秒)
|
|
198
|
+
"firstReplyTimeoutMs": 300000,
|
|
190
199
|
"accounts": [
|
|
191
200
|
{ "appKey": "你的生产 appKey", "agentId": "main", "name": "个人助手" }
|
|
192
201
|
]
|
|
@@ -212,6 +221,7 @@ openclaw gateway restart
|
|
|
212
221
|
| `initialReconnectDelay` | number | `1000` | 初始重连延迟(ms) |
|
|
213
222
|
| `maxReconnectDelay` | number | `60000` | 最大重连延迟(ms) |
|
|
214
223
|
| `reconnectJitter` | number | `0.3` | 重连抖动因子(0-1) |
|
|
224
|
+
| `firstReplyTimeoutMs` | number | `300000` | **首条 AI 回复超时时间(毫秒)**。用于保护「思考中」占位消息:若在该时间窗口内 AI 没有任何回复,则自动将占位消息更新为「当前请求处理超时,请稍后重试」。|
|
|
215
225
|
|
|
216
226
|
### 账户配置(`accounts[n]`)
|
|
217
227
|
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -432,11 +432,48 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
432
432
|
});
|
|
433
433
|
log.info(`${logPrefix} [stream] START acknowledged: msgId=${msgId}`);
|
|
434
434
|
|
|
435
|
-
//
|
|
435
|
+
// 1.1 为“思考中”占位增加首回复超时保护(默认 5 分钟,可通过 firstReplyTimeoutMs 配置)
|
|
436
|
+
let hasFirstReply = false;
|
|
437
|
+
let firstReplyTimedOut = false;
|
|
438
|
+
const firstReplyTimeoutMs = config.firstReplyTimeoutMs ?? 5 * 60_000;
|
|
439
|
+
const timeoutLabel = `${logPrefix} [stream] First reply timeout after ${firstReplyTimeoutMs}ms, updating thinking message as error`;
|
|
440
|
+
const firstReplyTimeout = setTimeout(async () => {
|
|
441
|
+
if (hasFirstReply) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
firstReplyTimedOut = true;
|
|
445
|
+
log.warn(timeoutLabel);
|
|
446
|
+
try {
|
|
447
|
+
const senderId = params.userInfo?.id;
|
|
448
|
+
const senderName = params.userInfo?.name ?? "未知用户";
|
|
449
|
+
const text = params.msgContent?.text ?? "";
|
|
450
|
+
const reply = {
|
|
451
|
+
targetMsgId: params.msgId,
|
|
452
|
+
targetUserId: senderId ?? "",
|
|
453
|
+
targetUserName: senderName,
|
|
454
|
+
previewText: text,
|
|
455
|
+
};
|
|
456
|
+
const timeoutText = "当前请求处理超时,请稍后重试。";
|
|
457
|
+
await sendTextMessage(
|
|
458
|
+
config,
|
|
459
|
+
currentIdentity.token,
|
|
460
|
+
params.groupId,
|
|
461
|
+
timeoutText,
|
|
462
|
+
[senderId ?? ""] as string[],
|
|
463
|
+
log,
|
|
464
|
+
msgId,
|
|
465
|
+
reply,
|
|
466
|
+
);
|
|
467
|
+
log.info(
|
|
468
|
+
`${logPrefix} [send] Timeout reply sent via HTTP: groupId=${params.groupId} msgId=${msgId} text="${timeoutText}"`,
|
|
469
|
+
);
|
|
470
|
+
} catch (err: unknown) {
|
|
471
|
+
log.error(`${logPrefix} [timeout] Failed to send timeout reply: ${String(err)}`);
|
|
472
|
+
}
|
|
473
|
+
}, firstReplyTimeoutMs);
|
|
474
|
+
|
|
475
|
+
// 2. 分发消息给 AI,deliver 回调负责发送首条和后续回复
|
|
436
476
|
let fullText = "";
|
|
437
|
-
// think 与 answer 阶段各自维护 seq,便于集群/跨服务按序重排
|
|
438
|
-
let thinkSeq = 0;
|
|
439
|
-
let answerSeq = 0;
|
|
440
477
|
|
|
441
478
|
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
442
479
|
ctx: inboundCtx,
|
|
@@ -448,9 +485,6 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
448
485
|
const textToSend = payload.markdown || payload.text;
|
|
449
486
|
if (!textToSend) return;
|
|
450
487
|
|
|
451
|
-
const isThinking = payload.isThinking === true;
|
|
452
|
-
const seq = isThinking ? thinkSeq++ : answerSeq++;
|
|
453
|
-
|
|
454
488
|
if (isFirstReply) {
|
|
455
489
|
const ttfr = Date.now() - dispatchStart;
|
|
456
490
|
log.info(`${logPrefix} [deliver] First response block received from AI (TTFB: ${ttfr}ms)`);
|
|
@@ -459,14 +493,51 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
459
493
|
|
|
460
494
|
fullText += textToSend;
|
|
461
495
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
496
|
+
const senderId = params.userInfo?.id;
|
|
497
|
+
const senderName = params.userInfo?.name ?? "未知用户";
|
|
498
|
+
const text = params.msgContent?.text ?? "";
|
|
499
|
+
const reply = {
|
|
500
|
+
targetMsgId: params.msgId,
|
|
501
|
+
targetUserId: senderId ?? "",
|
|
502
|
+
targetUserName: senderName,
|
|
503
|
+
previewText: text,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// 第一次有效回复:覆盖“思考中”占位消息(带 msgId)
|
|
507
|
+
if (!hasFirstReply) {
|
|
508
|
+
hasFirstReply = true;
|
|
509
|
+
clearTimeout(firstReplyTimeout);
|
|
510
|
+
await sendTextMessage(
|
|
511
|
+
config,
|
|
512
|
+
currentIdentity.token,
|
|
513
|
+
params.groupId,
|
|
514
|
+
textToSend,
|
|
515
|
+
[senderId ?? ""] as string[],
|
|
516
|
+
log,
|
|
517
|
+
msgId,
|
|
518
|
+
reply,
|
|
519
|
+
);
|
|
520
|
+
const preview = textToSend.length > 80 ? `${textToSend.slice(0, 80)}...` : textToSend;
|
|
521
|
+
log.info(
|
|
522
|
+
`${logPrefix} [send] First reply sent via HTTP: groupId=${params.groupId} msgId=${msgId} text="${preview}"`,
|
|
523
|
+
);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 后续回复:作为独立消息发送(不再复用 msgId)
|
|
528
|
+
await sendTextMessage(
|
|
529
|
+
config,
|
|
530
|
+
currentIdentity.token,
|
|
531
|
+
params.groupId,
|
|
532
|
+
textToSend,
|
|
533
|
+
[senderId ?? ""] as string[],
|
|
534
|
+
log,
|
|
535
|
+
undefined,
|
|
536
|
+
reply,
|
|
537
|
+
);
|
|
538
|
+
const preview = textToSend.length > 80 ? `${textToSend.slice(0, 80)}...` : textToSend;
|
|
468
539
|
log.info(
|
|
469
|
-
`${logPrefix} [
|
|
540
|
+
`${logPrefix} [send] Additional reply sent via HTTP: groupId=${params.groupId} text="${preview}"`,
|
|
470
541
|
);
|
|
471
542
|
} catch (err: unknown) {
|
|
472
543
|
log.error(`${logPrefix} Reply deliver failed: ${String(err)}`);
|
|
@@ -476,35 +547,12 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
476
547
|
},
|
|
477
548
|
});
|
|
478
549
|
|
|
479
|
-
// 3. AI 推理完成,发送 END
|
|
550
|
+
// 3. AI 推理完成,发送 END,结束本次流式占位语义
|
|
551
|
+
// 一旦发送 END,就不再补发「请求超时」消息,因此这里无条件清理定时器。
|
|
552
|
+
clearTimeout(firstReplyTimeout);
|
|
480
553
|
await wsHandle.streamClient.end(msgId, "stop");
|
|
481
554
|
log.info(`${logPrefix} [stream] END sent: msgId=${msgId}`);
|
|
482
555
|
|
|
483
|
-
if (fullText) {
|
|
484
|
-
log.info(
|
|
485
|
-
`${logPrefix} [send] Updating final message via HTTP: groupId=${params.groupId} msgId=${msgId}`,
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
// 构造 reply 信息:回复用户刚发的这条消息
|
|
489
|
-
const reply = {
|
|
490
|
-
targetMsgId: params.msgId,
|
|
491
|
-
targetUserId: senderId ?? "",
|
|
492
|
-
targetUserName: senderName,
|
|
493
|
-
previewText: text,
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
await sendTextMessage(
|
|
497
|
-
config,
|
|
498
|
-
currentIdentity.token,
|
|
499
|
-
params.groupId,
|
|
500
|
-
fullText,
|
|
501
|
-
[senderId ?? ""] as string[],
|
|
502
|
-
log,
|
|
503
|
-
msgId,
|
|
504
|
-
reply,
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
556
|
log.info(`${logPrefix} [dispatch] Dispatch completed for sessionKey=${route.sessionKey}`);
|
|
509
557
|
} else {
|
|
510
558
|
// 没 @ 我,仅作为旁观者缓存记忆,不打扰群里聊天
|
package/src/send-service.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -69,6 +69,13 @@ export interface XgImConfig extends OpenClawConfig {
|
|
|
69
69
|
maxReconnectDelay?: number;
|
|
70
70
|
/** 重连延迟抖动因子 0-1(默认 0.3) */
|
|
71
71
|
reconnectJitter?: number;
|
|
72
|
+
/**
|
|
73
|
+
* 首次 AI 回复超时时间(毫秒)。
|
|
74
|
+
*
|
|
75
|
+
* - 未配置时默认 5 分钟(300_000ms)。
|
|
76
|
+
* - 仅用于保护「思考中」占位消息,避免长时间不被更新。
|
|
77
|
+
*/
|
|
78
|
+
firstReplyTimeoutMs?: number;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
// ─── IM 接口 Request / Response ─────────────────────────────────────────────
|