@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 +10 -0
- package/lib/web-shim.js +15 -0
- package/lib/wecom-channel.d.ts +8 -0
- package/lib/wecom-channel.js +50 -1
- package/package.json +1 -1
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);
|
package/lib/wecom-channel.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/wecom-channel.js
CHANGED
|
@@ -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.
|
|
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": {
|