@rezti/dsh-rez-wechat 0.1.24 → 0.1.25

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/lib/web-shim.d.ts CHANGED
@@ -136,6 +136,16 @@ export declare function weixinBridgeEnv(opts: {
136
136
  shimScript: string;
137
137
  }): NodeJS.ProcessEnv;
138
138
  export declare function postRpc(baseUrl: string, method: string, params: Record<string, unknown>, timeoutMs?: number, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<unknown>;
139
+ /** Read later assistant output for channels with proactive delivery. */
140
+ export declare function sessionFollowupSnapshot(opts: {
141
+ room: string;
142
+ env?: NodeJS.ProcessEnv;
143
+ afterSeq?: number;
144
+ }): Promise<{
145
+ sessionId?: string;
146
+ cursor: number;
147
+ text: string;
148
+ }>;
139
149
  export declare function extractArchivedSessionIds(body: unknown): string[];
140
150
  /** Stock dsh archives one-way and hides blank sessions. Restore + attach + title. */
141
151
  export declare function ensureSessionVisible(rpc: RpcFn, sessionId: string, env?: NodeJS.ProcessEnv, workspaceId?: string, loopback?: boolean): Promise<void>;
package/lib/web-shim.js CHANGED
@@ -1065,6 +1065,21 @@ async function maybeSelectModel(rpc, sessionId, env) {
1065
1065
  async function fetchSessionHistory(rpc, sessionId, maxMessages = 40) {
1066
1066
  return historyEvents(await rpc('dsh-rez-suite/channel/history', { sessionId, maxMessages }));
1067
1067
  }
1068
+ /** Read later assistant output for channels with proactive delivery. */
1069
+ export async function sessionFollowupSnapshot(opts) {
1070
+ const env = opts.env ?? process.env;
1071
+ const store = loadSessionStore(sessionStorePath(env));
1072
+ const key = basename(opts.room) || 'default';
1073
+ const sessionId = store.sessions[key]?.sessionId;
1074
+ if (!sessionId)
1075
+ return { cursor: opts.afterSeq ?? -1, text: '' };
1076
+ const rpc = (method, params) => postRpc(webBaseUrl(env), method, params, RPC_TIMEOUT_MS, env);
1077
+ const events = await fetchSessionHistory(rpc, sessionId, 40);
1078
+ const cursor = historyCursor(events);
1079
+ const afterSeq = opts.afterSeq;
1080
+ const after = afterSeq === undefined ? events : events.filter(event => eventSeq(event) > afterSeq);
1081
+ return { sessionId, cursor, text: usableAssistantText(lastAssistantText(after)) };
1082
+ }
1068
1083
  async function sessionAlive(rpc, sessionId) {
1069
1084
  try {
1070
1085
  await fetchSessionHistory(rpc, sessionId, 1);
@@ -26,6 +26,12 @@ export interface WecomWsClient {
26
26
  disconnect: () => void;
27
27
  on: (event: string, handler: (...args: unknown[]) => void) => unknown;
28
28
  replyStream: (frame: unknown, streamId: string, content: string, finish?: boolean) => Promise<unknown>;
29
+ sendMessage?: (chatid: string, body: {
30
+ msgtype: 'markdown';
31
+ markdown: {
32
+ content: string;
33
+ };
34
+ }) => Promise<unknown>;
29
35
  replyWelcome: (frame: unknown, body: {
30
36
  msgtype: string;
31
37
  text: {
@@ -42,6 +48,7 @@ export type WecomRunTurn = (room: string, text: string, signal?: AbortSignal) =>
42
48
  export declare const WECOM_TURN_TIMEOUT_MS = 90000;
43
49
  export declare function extractWecomText(frame: unknown): string;
44
50
  export declare function wecomMsgid(frame: unknown): string;
51
+ export declare function wecomChatId(frame: unknown): string;
45
52
  export declare function createOfficialWecomClient(opts: {
46
53
  botId: string;
47
54
  secret: string;
@@ -76,5 +83,6 @@ export declare class WecomChannel {
76
83
  private enqueueText;
77
84
  private rememberMsgid;
78
85
  private handleText;
86
+ private armFollowup;
79
87
  private handleNonText;
80
88
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { join } from 'node:path';
6
6
  import { dshHome } from '@rezti/dsh-rez-sso';
7
- import { clearSessionStoreKey, isRestartCommand, runHeadlessViaWeb, } from './web-shim.js';
7
+ import { clearSessionStoreKey, isRestartCommand, runHeadlessViaWeb, sessionFollowupSnapshot, } from './web-shim.js';
8
8
  import { loadWecomChannels, publicWecomBots, saveWecomChannels, applyWecomBotPatch, WECOM_BINDABLE_ROOMS, } from './wecom-store.js';
9
9
  export const WECOM_NON_TEXT_HINT = '请发文字。图片和文件请走网页端或 Nextcloud。';
10
10
  /** Official replyStream content limit, measured in UTF-8 bytes. */
@@ -90,6 +90,21 @@ export function wecomMsgid(frame) {
90
90
  const id = body.msgid;
91
91
  return typeof id === 'string' ? id : '';
92
92
  }
93
+ export function wecomChatId(frame) {
94
+ if (typeof frame !== 'object' || frame === null)
95
+ return '';
96
+ const body = frame.body;
97
+ if (typeof body !== 'object' || body === null)
98
+ return '';
99
+ const value = body;
100
+ if (typeof value.chatid === 'string' && value.chatid.trim())
101
+ return value.chatid.trim();
102
+ const from = value.from;
103
+ if (typeof from === 'object' && from !== null && typeof from.userid === 'string') {
104
+ return from.userid.trim();
105
+ }
106
+ return '';
107
+ }
93
108
  function textContent(value) {
94
109
  if (typeof value === 'string')
95
110
  return value.trim();
@@ -200,6 +215,8 @@ export class WecomChannel {
200
215
  dropBindings() {
201
216
  for (const row of this.bindings.values()) {
202
217
  row.turnAbort?.abort();
218
+ if (row.followupTimer !== undefined)
219
+ clearInterval(row.followupTimer);
203
220
  try {
204
221
  row.client.disconnect();
205
222
  }
@@ -269,6 +286,9 @@ export class WecomChannel {
269
286
  if (this.rememberMsgid(row, frame))
270
287
  return;
271
288
  const text = extractWecomText(frame);
289
+ const chatId = wecomChatId(frame);
290
+ if (chatId.length > 0)
291
+ row.chatId = chatId;
272
292
  if (text.length > 0) {
273
293
  row.turnAbort?.abort();
274
294
  if (isRestartCommand(text)) {
@@ -316,6 +336,7 @@ export class WecomChannel {
316
336
  return;
317
337
  }
318
338
  await finish(reply.length > 0 ? reply : '(空回复)');
339
+ await this.armFollowup(row);
319
340
  }
320
341
  catch (error) {
321
342
  const why = error instanceof Error ? error.message : String(error);
@@ -352,6 +373,34 @@ export class WecomChannel {
352
373
  delete row.turnAbort;
353
374
  }
354
375
  }
376
+ async armFollowup(row) {
377
+ if (row.followupTimer !== undefined || row.client.sendMessage === undefined || row.chatId === undefined)
378
+ return;
379
+ const followupEnv = {
380
+ ...process.env,
381
+ REZ_WECHAT_SESSION_STORE: join(this.home, 'dsh-rez-wecom', row.bot.room, 'web-sessions.json'),
382
+ };
383
+ try {
384
+ const baseline = await sessionFollowupSnapshot({ room: row.bot.room, env: followupEnv });
385
+ row.followupCursor = baseline.cursor;
386
+ }
387
+ catch {
388
+ return;
389
+ }
390
+ row.followupTimer = setInterval(() => {
391
+ const options = row.followupCursor === undefined
392
+ ? { room: row.bot.room }
393
+ : { room: row.bot.room, afterSeq: row.followupCursor };
394
+ void sessionFollowupSnapshot({ ...options, env: followupEnv }).then(async (snapshot) => {
395
+ if (snapshot.sessionId === undefined)
396
+ return;
397
+ row.followupCursor = snapshot.cursor;
398
+ if (snapshot.text.length === 0 || row.chatId === undefined || row.client.sendMessage === undefined)
399
+ return;
400
+ await row.client.sendMessage(row.chatId, { msgtype: 'markdown', markdown: { content: clipWecomReply(snapshot.text) } });
401
+ }).catch(error => console.warn(`[wecom] ${row.bot.room} 定时提醒投递失败:${error instanceof Error ? error.message : String(error)}`));
402
+ }, 2000);
403
+ }
355
404
  async handleNonText(row, frame) {
356
405
  const text = extractWecomText(frame);
357
406
  if (text.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-wechat",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "ReZ-TI WeChat/WeCom bridges. Personal WeChat is QClaw/ClawBot scan-and-chat via dsh-wechat-bridge; WeCom AI bots use the official @wecom/aibot-node-sdk (BotID + Secret) bound per Harness room.",
5
5
  "type": "module",
6
6
  "engines": {