@xgjktech/xg_cwork_im 1.0.2 → 1.0.3
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/package.json +1 -1
- package/src/channel.ts +22 -5
- package/src/connection.ts +22 -6
- package/src/group-history-tool.ts +18 -4
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -392,6 +392,17 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
392
392
|
XgImExt: msgExt,
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
+
// 若消息内容为 /reset,则在将其转交给 OpenClaw 之前完整打印一次入站上下文,便于排查 reset 行为
|
|
396
|
+
if (text.trim() === "/reset") {
|
|
397
|
+
try {
|
|
398
|
+
log.info?.(
|
|
399
|
+
`${logPrefix} [reset] inboundCtx payload before dispatch: ${JSON.stringify(inboundCtx)}`,
|
|
400
|
+
);
|
|
401
|
+
} catch {
|
|
402
|
+
// 忽略 JSON 序列化异常,避免影响正常流程
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
395
406
|
// 【所有消息都必须做】记录到数据库!让 AI 产生“记忆”
|
|
396
407
|
log.info(`${logPrefix} [session] Recording inbound session sessionKey=${inboundCtx.SessionKey || route.sessionKey}`);
|
|
397
408
|
await rt.channel.session.recordInboundSession({
|
|
@@ -423,17 +434,23 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
423
434
|
|
|
424
435
|
// 2. 分发消息给 AI,deliver 回调负责把增量文本通过 CHUNK 推给 IM
|
|
425
436
|
let fullText = "";
|
|
437
|
+
// think 与 answer 阶段各自维护 seq,便于集群/跨服务按序重排
|
|
438
|
+
let thinkSeq = 0;
|
|
439
|
+
let answerSeq = 0;
|
|
426
440
|
|
|
427
441
|
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
428
442
|
ctx: inboundCtx,
|
|
429
443
|
cfg: ctx.cfg,
|
|
430
444
|
dispatcherOptions: {
|
|
431
445
|
responsePrefix: "",
|
|
432
|
-
deliver: async (payload: { markdown?: string; text?: string }) => {
|
|
446
|
+
deliver: async (payload: { markdown?: string; text?: string; isThinking?: boolean }) => {
|
|
433
447
|
try {
|
|
434
448
|
const textToSend = payload.markdown || payload.text;
|
|
435
449
|
if (!textToSend) return;
|
|
436
450
|
|
|
451
|
+
const isThinking = payload.isThinking === true;
|
|
452
|
+
const seq = isThinking ? thinkSeq++ : answerSeq++;
|
|
453
|
+
|
|
437
454
|
if (isFirstReply) {
|
|
438
455
|
const ttfr = Date.now() - dispatchStart;
|
|
439
456
|
log.info(`${logPrefix} [deliver] First response block received from AI (TTFB: ${ttfr}ms)`);
|
|
@@ -442,14 +459,14 @@ export const xgCworkImChannelPlugin: XgImChannelPlugin = {
|
|
|
442
459
|
|
|
443
460
|
fullText += textToSend;
|
|
444
461
|
|
|
445
|
-
// 将增量 block 以 CHUNK 形式推给 IM
|
|
462
|
+
// 将增量 block 以 CHUNK 形式推给 IM,带 seq 便于服务端按序
|
|
446
463
|
await wsHandle.streamClient.chunk(msgId, {
|
|
447
|
-
|
|
448
|
-
isThinking: false,
|
|
464
|
+
isThinking,
|
|
449
465
|
content: textToSend,
|
|
466
|
+
seq,
|
|
450
467
|
});
|
|
451
468
|
log.info(
|
|
452
|
-
`${logPrefix} [stream] CHUNK sent: msgId=${msgId} length=${textToSend.length}`,
|
|
469
|
+
`${logPrefix} [stream] CHUNK sent: msgId=${msgId} isThinking=${isThinking} seq=${seq} length=${textToSend.length}`,
|
|
453
470
|
);
|
|
454
471
|
} catch (err: unknown) {
|
|
455
472
|
log.error(`${logPrefix} Reply deliver failed: ${String(err)}`);
|
package/src/connection.ts
CHANGED
|
@@ -16,16 +16,28 @@ import type { WsMessage, XgImConfig } from "./types.js";
|
|
|
16
16
|
|
|
17
17
|
export type OnMessageCallback = (msg: WsMessage) => void;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* CHUNK 载荷:发送端带 seq,便于集群/跨服务按序重排。
|
|
21
|
+
* - isThinking=true 为思考阶段,seq 从 0 递增(thinkSeq);
|
|
22
|
+
* - isThinking=false 为回答阶段,seq 从 0 递增(answerSeq)。
|
|
23
|
+
*/
|
|
24
|
+
export interface ImStreamChunkData {
|
|
25
|
+
isThinking: boolean;
|
|
26
|
+
content: string;
|
|
27
|
+
/** 当前阶段内的序号,think 与 answer 各自从 0 递增 */
|
|
28
|
+
seq: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* 基于 WebSocket 的流式消息客户端。
|
|
21
33
|
*
|
|
22
34
|
* - start(): 发送 START,等待 ack,返回 msgId。
|
|
23
|
-
* - chunk(): 基于 msgId
|
|
35
|
+
* - chunk(): 基于 msgId 发送增量文本,data 中带 seq(think/answer 阶段分开)。
|
|
24
36
|
* - end(): 发送 END,通知服务端结束本次流式消息。
|
|
25
37
|
*/
|
|
26
38
|
export interface ImStreamClient {
|
|
27
39
|
start: (opts: { groupId?: string; toUserId?: string }) => Promise<{ msgId: string }>;
|
|
28
|
-
chunk: (msgId: string, data:
|
|
40
|
+
chunk: (msgId: string, data: ImStreamChunkData) => Promise<void>;
|
|
29
41
|
end: (msgId: string, reason: string) => Promise<void>;
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -186,7 +198,7 @@ export function startWebSocket(
|
|
|
186
198
|
return;
|
|
187
199
|
}
|
|
188
200
|
|
|
189
|
-
// 处理包含 params
|
|
201
|
+
// 处理包含 params 的事件(robotMention / groupMessage)
|
|
190
202
|
if (parsed.params && (parsed.cmd === "robotMention" || parsed.cmd === "groupMessage")) {
|
|
191
203
|
if (parsed.cmd === "robotMention") {
|
|
192
204
|
log.info(`${logPrefix} 📨 robotMention received: groupId=${parsed.params.groupId} msgId=${parsed.params.msgId}`);
|
|
@@ -281,18 +293,22 @@ export function startWebSocket(
|
|
|
281
293
|
return { msgId };
|
|
282
294
|
},
|
|
283
295
|
|
|
284
|
-
async chunk(msgId: string, data:
|
|
296
|
+
async chunk(msgId: string, data: ImStreamChunkData): Promise<void> {
|
|
285
297
|
const socket = ensureSocketReady();
|
|
286
298
|
const payload = {
|
|
287
299
|
cmd: "im_stream_msg",
|
|
288
300
|
params: {
|
|
289
301
|
event: "CHUNK",
|
|
290
302
|
msgId,
|
|
291
|
-
data
|
|
303
|
+
data: {
|
|
304
|
+
isThinking: data.isThinking,
|
|
305
|
+
content: data.content,
|
|
306
|
+
seq: data.seq,
|
|
307
|
+
},
|
|
292
308
|
},
|
|
293
309
|
};
|
|
294
310
|
if (config.debug) {
|
|
295
|
-
log.debug?.(`${logPrefix} ⇒ WS CHUNK stream: msgId=${msgId} isThinking=${data.isThinking} length=${data.content.length}`);
|
|
311
|
+
log.debug?.(`${logPrefix} ⇒ WS CHUNK stream: msgId=${msgId} isThinking=${data.isThinking} seq=${data.seq} length=${data.content.length}`);
|
|
296
312
|
}
|
|
297
313
|
socket.send(JSON.stringify(payload));
|
|
298
314
|
},
|
|
@@ -18,6 +18,11 @@ const GroupHistoryParams = Type.Object({
|
|
|
18
18
|
groupId: Type.String({
|
|
19
19
|
description: "群聊 ID,即当前消息来自的群组唯一标识符",
|
|
20
20
|
}),
|
|
21
|
+
userId: Type.Optional(
|
|
22
|
+
Type.String({
|
|
23
|
+
description: "(可选)以哪个用户身份拉取历史消息的 userId,普通对话场景建议传当前发消息用户 ID;若为空则按后台默认身份查询",
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
21
26
|
accountId: Type.Optional(
|
|
22
27
|
Type.String({
|
|
23
28
|
description: "(可选)要使用的 xg_cwork_im 账户下标(数字字符串,默认 0)",
|
|
@@ -70,15 +75,24 @@ async function fetchGroupHistory(
|
|
|
70
75
|
token: string,
|
|
71
76
|
groupId: string,
|
|
72
77
|
limit: number,
|
|
78
|
+
userId?: string,
|
|
73
79
|
): Promise<WsMessageParams[]> {
|
|
74
80
|
const url = `${config.baseUrl}/im/message/getLatestMsgListForAI`;
|
|
75
81
|
|
|
82
|
+
const params: Record<string, unknown> = {
|
|
83
|
+
groupId,
|
|
84
|
+
msgCount: limit,
|
|
85
|
+
};
|
|
86
|
+
if (userId) {
|
|
87
|
+
params.userId = userId;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
const res = await axios.get<{
|
|
77
91
|
resultCode: number;
|
|
78
92
|
message?: string;
|
|
79
93
|
data?: WsMessageParams[];
|
|
80
94
|
}>(url, {
|
|
81
|
-
params
|
|
95
|
+
params,
|
|
82
96
|
headers: { "access-token": token },
|
|
83
97
|
timeout: 8_000,
|
|
84
98
|
});
|
|
@@ -116,7 +130,7 @@ export function buildGroupHistoryTool(config: XgImConfig): AnyAgentTool {
|
|
|
116
130
|
parameters: GroupHistoryParams,
|
|
117
131
|
async execute(_toolCallId: string, params: Static<typeof GroupHistoryParams>) {
|
|
118
132
|
const limit = params.limit ?? 10;
|
|
119
|
-
const { groupId, accountId } = params;
|
|
133
|
+
const { groupId, accountId, userId } = params;
|
|
120
134
|
|
|
121
135
|
console.log(`[cwork_im][tool] xg_cwork_im_get_group_chat_history called: groupId=${groupId} limit=${limit}`);
|
|
122
136
|
|
|
@@ -136,9 +150,9 @@ export function buildGroupHistoryTool(config: XgImConfig): AnyAgentTool {
|
|
|
136
150
|
let messages: WsMessageParams[];
|
|
137
151
|
try {
|
|
138
152
|
const accountConfig = resolveAccountConfig(config, accountId);
|
|
139
|
-
messages = await fetchGroupHistory(accountConfig, identity.token, groupId, limit);
|
|
153
|
+
messages = await fetchGroupHistory(accountConfig, identity.token, groupId, limit, userId);
|
|
140
154
|
console.log(
|
|
141
|
-
`[cwork_im][tool] fetched ${messages.length} messages from group ${groupId} using accountId=${accountId ?? "0"}`,
|
|
155
|
+
`[cwork_im][tool] fetched ${messages.length} messages from group ${groupId} using accountId=${accountId ?? "0"} userId=${userId ?? "N/A"}`,
|
|
142
156
|
);
|
|
143
157
|
} catch (err: unknown) {
|
|
144
158
|
return jsonResult({
|