@xgjktech/xg_cwork_im 1.0.2 → 1.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgjktech/xg_cwork_im",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "XG CWork IM channel plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
@@ -24,6 +24,7 @@
24
24
  "install:prod": "npm install --omit=dev"
25
25
  },
26
26
  "dependencies": {
27
+ "@sinclair/typebox": "^0.32.0",
27
28
  "axios": "^1.6.0",
28
29
  "ws": "^8.17.0",
29
30
  "zod": "^3.22.0"
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: { isThinking: boolean; content: string }) => Promise<void>;
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 的事件(无论是 robotMention 还是普通 message
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: { isThinking: boolean; content: string }): Promise<void> {
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: { groupId, msgCount: limit },
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({