shennian 0.2.89 → 0.2.90

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.
Files changed (118) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +13 -4
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.js +1 -19
  8. package/dist/src/agents/claude.js +8 -305
  9. package/dist/src/agents/codex-control.js +2 -188
  10. package/dist/src/agents/codex-utils.js +7 -200
  11. package/dist/src/agents/codex.js +15 -916
  12. package/dist/src/agents/command-spec.js +2 -413
  13. package/dist/src/agents/config-status.js +1 -226
  14. package/dist/src/agents/cursor.js +1 -249
  15. package/dist/src/agents/custom.js +4 -271
  16. package/dist/src/agents/detect.js +1 -56
  17. package/dist/src/agents/external-channel-instructions.js +10 -94
  18. package/dist/src/agents/gemini.js +1 -173
  19. package/dist/src/agents/manager.js +13 -157
  20. package/dist/src/agents/model-registry/cache.js +1 -37
  21. package/dist/src/agents/model-registry/discovery.js +2 -187
  22. package/dist/src/agents/model-registry/parsers.js +4 -447
  23. package/dist/src/agents/model-registry/runner.js +1 -30
  24. package/dist/src/agents/model-registry/service.js +1 -78
  25. package/dist/src/agents/model-registry/types.js +1 -8
  26. package/dist/src/agents/model-registry.js +1 -18
  27. package/dist/src/agents/openclaw.js +2 -275
  28. package/dist/src/agents/opencode.js +1 -231
  29. package/dist/src/agents/pi-context.js +12 -217
  30. package/dist/src/agents/pi.js +14 -723
  31. package/dist/src/agents/platform-instructions.js +9 -54
  32. package/dist/src/channels/base.js +1 -3
  33. package/dist/src/channels/registry.js +1 -30
  34. package/dist/src/channels/reply-split.js +10 -89
  35. package/dist/src/channels/runtime.js +5 -564
  36. package/dist/src/channels/secret-registry.js +1 -46
  37. package/dist/src/channels/websocket.js +8 -378
  38. package/dist/src/channels/wechat-channel/anchor.js +1 -65
  39. package/dist/src/channels/wechat-channel/client.js +1 -96
  40. package/dist/src/channels/wechat-channel/cooldown.js +1 -38
  41. package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
  42. package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
  43. package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
  44. package/dist/src/channels/wechat-channel/helper-client.js +3 -149
  45. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
  46. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
  47. package/dist/src/channels/wechat-channel/index.d.ts +1 -0
  48. package/dist/src/channels/wechat-channel/index.js +1 -19
  49. package/dist/src/channels/wechat-channel/ledger.js +1 -54
  50. package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
  51. package/dist/src/channels/wechat-channel/message-key.js +1 -105
  52. package/dist/src/channels/wechat-channel/observer.js +1 -118
  53. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
  54. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
  55. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  56. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  57. package/dist/src/channels/wechat-channel/preflight.js +1 -48
  58. package/dist/src/channels/wechat-channel/runner.js +1 -84
  59. package/dist/src/channels/wechat-channel/runtime.js +1 -66
  60. package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
  61. package/dist/src/channels/wechat-channel/scheduler.js +1 -152
  62. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  63. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  64. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  65. package/dist/src/channels/wechat-rpa.js +6 -1028
  66. package/dist/src/channels/wecom.js +4 -357
  67. package/dist/src/commands/agent.js +6 -131
  68. package/dist/src/commands/daemon-windows.js +8 -48
  69. package/dist/src/commands/daemon.js +19 -1013
  70. package/dist/src/commands/external-attachments.js +1 -51
  71. package/dist/src/commands/external.js +1 -137
  72. package/dist/src/commands/manager.js +2 -391
  73. package/dist/src/commands/pair-qr.js +1 -6
  74. package/dist/src/commands/pair.js +9 -287
  75. package/dist/src/commands/tools.js +1 -34
  76. package/dist/src/commands/upgrade.js +1 -198
  77. package/dist/src/config/index.js +1 -35
  78. package/dist/src/daemon-log.js +6 -58
  79. package/dist/src/env-path.js +1 -64
  80. package/dist/src/fs/boundary.js +1 -126
  81. package/dist/src/fs/handler.js +1 -130
  82. package/dist/src/fs/security.js +1 -32
  83. package/dist/src/fs/text-decoder.js +1 -110
  84. package/dist/src/index.js +2 -404
  85. package/dist/src/log-reporter.js +1 -16
  86. package/dist/src/manager/prompt.js +29 -34
  87. package/dist/src/manager/registry.js +2 -269
  88. package/dist/src/manager/runtime.js +19 -1007
  89. package/dist/src/native-fusion/config.js +1 -5
  90. package/dist/src/native-fusion/opencode-parser.js +3 -123
  91. package/dist/src/native-fusion/parser-common.js +8 -264
  92. package/dist/src/native-fusion/parsers.js +8 -729
  93. package/dist/src/native-fusion/service.js +2 -225
  94. package/dist/src/native-fusion/state.js +1 -22
  95. package/dist/src/native-fusion/types.js +1 -1
  96. package/dist/src/region.js +1 -88
  97. package/dist/src/relay/client.js +1 -343
  98. package/dist/src/session/archive-zip.js +1 -220
  99. package/dist/src/session/handlers/agent-config.js +1 -150
  100. package/dist/src/session/handlers/agents.js +1 -55
  101. package/dist/src/session/handlers/chat.js +2 -751
  102. package/dist/src/session/handlers/control.js +1 -55
  103. package/dist/src/session/handlers/fs.js +1 -783
  104. package/dist/src/session/handlers/session-refresh.js +1 -47
  105. package/dist/src/session/handlers/skills.js +1 -121
  106. package/dist/src/session/handlers/title.js +1 -60
  107. package/dist/src/session/handlers/tool-detail.js +1 -218
  108. package/dist/src/session/manager.js +1 -319
  109. package/dist/src/session/projection.js +1 -54
  110. package/dist/src/session/queue.js +4 -317
  111. package/dist/src/session/remote-attachments.js +1 -72
  112. package/dist/src/session/store.js +3 -109
  113. package/dist/src/session/types.js +1 -4
  114. package/dist/src/skills/registry.js +15 -148
  115. package/dist/src/skills/setup.js +1 -101
  116. package/dist/src/tools/markdown-to-pdf.js +10 -346
  117. package/dist/src/upgrade/engine.js +3 -347
  118. package/package.json +3 -2
@@ -1,378 +1,8 @@
1
- // @arch docs/features/external-websocket-channel.md
2
- // @test src/__tests__/manager-runtime.test.ts
3
- import { randomUUID } from 'node:crypto';
4
- import WebSocket from 'ws';
5
- import { ChannelSecretRegistry } from './secret-registry.js';
6
- const RECONNECT_BACKOFF_MS = [2_000, 5_000, 10_000, 30_000];
7
- const HEARTBEAT_MS = 30_000;
8
- const SEND_TIMEOUT_MS = 15_000;
9
- export class ExternalWebSocketChannelAdapter {
10
- onMessage;
11
- type = 'websocket';
12
- secrets = new ChannelSecretRegistry();
13
- connections = new Map();
14
- constructor(onMessage) {
15
- this.onMessage = onMessage;
16
- }
17
- async connect(config) {
18
- if (!config.enabled)
19
- return;
20
- const conn = this.ensureConnection(config);
21
- if (conn.connecting)
22
- return conn.connecting;
23
- conn.stopped = false;
24
- conn.config = config;
25
- conn.connecting = this.openConnection(conn).finally(() => {
26
- conn.connecting = null;
27
- });
28
- return conn.connecting;
29
- }
30
- async disconnect(config) {
31
- const conn = this.connections.get(config.id);
32
- if (!conn)
33
- return;
34
- conn.stopped = true;
35
- if (conn.reconnectTimer)
36
- clearTimeout(conn.reconnectTimer);
37
- if (conn.pingTimer)
38
- clearInterval(conn.pingTimer);
39
- conn.reconnectTimer = null;
40
- conn.pingTimer = null;
41
- for (const pending of conn.pendingAcks.values()) {
42
- clearTimeout(pending.timer);
43
- pending.reject(new Error('External websocket adapter disconnected'));
44
- }
45
- conn.pendingAcks.clear();
46
- const socket = conn.socket;
47
- conn.socket = null;
48
- if (socket)
49
- await new Promise((resolve) => {
50
- socket.once('close', () => resolve());
51
- socket.close();
52
- setTimeout(resolve, 500);
53
- });
54
- this.connections.delete(config.id);
55
- }
56
- async send(config, reply) {
57
- const conn = this.ensureConnection(config);
58
- const secret = this.readSecret(config);
59
- if (secret.canReply === false) {
60
- throw new Error('External websocket channel does not allow replies');
61
- }
62
- await this.connect(config);
63
- const requestId = reply.idempotencyKey || `send_${randomUUID()}`;
64
- await this.sendAwaitAck(conn, requestId, {
65
- type: 'message.send',
66
- requestId,
67
- conversationId: reply.conversationId,
68
- contentType: 'text',
69
- text: reply.text,
70
- replyToMessageId: reply.messageId ?? undefined,
71
- });
72
- }
73
- async health(config) {
74
- const secret = this.readSecret(config);
75
- if (!secret.wsUrl || !secret.token) {
76
- return { ok: false, message: 'External websocket requires local wsUrl and token' };
77
- }
78
- const conn = this.connections.get(config.id);
79
- return conn?.socket?.readyState === WebSocket.OPEN
80
- ? { ok: true }
81
- : { ok: false, message: 'External websocket disconnected' };
82
- }
83
- async defaultConversation(config) {
84
- const secret = this.readSecret(config);
85
- const url = new URL(secret.wsUrl);
86
- url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
87
- url.pathname = url.pathname.replace(/\/ws\/?$/, '/subscription/self');
88
- url.search = '';
89
- const response = await fetch(url, {
90
- headers: {
91
- Authorization: `Bearer ${secret.token}`,
92
- },
93
- });
94
- const data = await response.json().catch(() => null);
95
- if (!response.ok || !data?.ok) {
96
- throw new Error(data?.error || `External websocket metadata failed: ${response.status}`);
97
- }
98
- if (data.subscription?.allowSend === false) {
99
- throw new Error('External websocket subscription does not allow send');
100
- }
101
- const conversationId = String(data.subscription?.defaultConversationId || '').trim();
102
- if (!conversationId) {
103
- throw new Error('External websocket subscription has no single default conversation');
104
- }
105
- return {
106
- conversationId,
107
- conversationName: String(data.subscription?.defaultConversationName || '').trim() || undefined,
108
- };
109
- }
110
- ensureConnection(config) {
111
- let conn = this.connections.get(config.id);
112
- if (!conn) {
113
- conn = {
114
- config,
115
- socket: null,
116
- connecting: null,
117
- stopped: false,
118
- reconnectAttempt: 0,
119
- reconnectTimer: null,
120
- pingTimer: null,
121
- dedup: new Set(),
122
- dedupQueue: [],
123
- seenBatchMessageKeys: new Set(),
124
- seenBatchMessageQueue: [],
125
- pendingAcks: new Map(),
126
- };
127
- this.connections.set(config.id, conn);
128
- }
129
- conn.config = config;
130
- return conn;
131
- }
132
- readSecret(config) {
133
- const secret = this.secrets.get(config.secretRef);
134
- if (!secret || secret.type !== 'websocket' || !secret.wsUrl || !secret.token) {
135
- throw new Error('External websocket channel is not configured on this daemon');
136
- }
137
- return secret;
138
- }
139
- async openConnection(conn) {
140
- const secret = this.readSecret(conn.config);
141
- const wsUrl = secret.wsUrl;
142
- const socket = new WebSocket(wsUrl, {
143
- headers: {
144
- Authorization: `Bearer ${secret.token}`,
145
- },
146
- });
147
- conn.socket = socket;
148
- await new Promise((resolve, reject) => {
149
- const cleanup = () => {
150
- socket.off('open', handleOpen);
151
- socket.off('error', handleError);
152
- };
153
- const handleOpen = () => {
154
- cleanup();
155
- resolve();
156
- };
157
- const handleError = (error) => {
158
- cleanup();
159
- reject(error);
160
- };
161
- socket.once('open', handleOpen);
162
- socket.once('error', handleError);
163
- });
164
- socket.on('message', (data) => {
165
- const payload = this.parsePayload(data.toString());
166
- if (!payload)
167
- return;
168
- this.handlePayload(conn, payload);
169
- });
170
- socket.on('close', () => this.scheduleReconnect(conn));
171
- socket.on('error', () => this.scheduleReconnect(conn));
172
- conn.reconnectAttempt = 0;
173
- if (conn.pingTimer)
174
- clearInterval(conn.pingTimer);
175
- conn.pingTimer = setInterval(() => {
176
- void this.sendJson(conn, { type: 'ping', timestamp: new Date().toISOString() }).catch(() => { });
177
- }, HEARTBEAT_MS);
178
- conn.pingTimer.unref();
179
- }
180
- scheduleReconnect(conn) {
181
- if (conn.stopped)
182
- return;
183
- if (conn.reconnectTimer || conn.connecting)
184
- return;
185
- const delay = RECONNECT_BACKOFF_MS[Math.min(conn.reconnectAttempt, RECONNECT_BACKOFF_MS.length - 1)];
186
- conn.reconnectAttempt += 1;
187
- conn.reconnectTimer = setTimeout(() => {
188
- conn.reconnectTimer = null;
189
- void this.connect(conn.config).catch(() => { });
190
- }, delay);
191
- conn.reconnectTimer.unref();
192
- }
193
- async sendJson(conn, payload) {
194
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) {
195
- throw new Error('External websocket is not connected');
196
- }
197
- conn.socket.send(JSON.stringify(payload));
198
- }
199
- async sendAwaitAck(conn, requestId, payload) {
200
- await new Promise((resolve, reject) => {
201
- const timer = setTimeout(() => {
202
- conn.pendingAcks.delete(requestId);
203
- reject(new Error('External websocket send timed out'));
204
- }, SEND_TIMEOUT_MS);
205
- timer.unref();
206
- conn.pendingAcks.set(requestId, { resolve, reject, timer });
207
- void this.sendJson(conn, payload).catch((error) => {
208
- clearTimeout(timer);
209
- conn.pendingAcks.delete(requestId);
210
- reject(error instanceof Error ? error : new Error(String(error)));
211
- });
212
- });
213
- }
214
- handlePayload(conn, payload) {
215
- const type = String(payload.type || '');
216
- if (type === 'ack' || type === 'error') {
217
- const requestId = String(payload.requestId || '');
218
- const pending = requestId ? conn.pendingAcks.get(requestId) : undefined;
219
- if (!pending)
220
- return;
221
- clearTimeout(pending.timer);
222
- conn.pendingAcks.delete(requestId);
223
- if (type === 'ack' && payload.ok !== false)
224
- pending.resolve();
225
- else
226
- pending.reject(new Error(String(payload.error || 'External websocket send failed')));
227
- return;
228
- }
229
- if (type === 'hello' || type === 'pong')
230
- return;
231
- if (type !== 'message.inbound' && type !== 'message.inbound.batch')
232
- return;
233
- const messageId = String(payload.messageId || payload.batchId || payload.id || '');
234
- if (!messageId || this.isDuplicate(conn, messageId))
235
- return;
236
- const attachments = this.normalizeAttachments(payload);
237
- const text = this.buildVisibleText(conn, payload, attachments);
238
- const conversationId = String(payload.conversationId || '').trim();
239
- if (Array.isArray(payload.messages) && !text)
240
- return;
241
- if ((!text && attachments.length === 0) || !conversationId)
242
- return;
243
- this.onMessage?.({
244
- managerSessionId: conn.config.managerSessionId,
245
- channelId: conn.config.id,
246
- channelType: 'websocket',
247
- conversationId,
248
- messageId,
249
- sender: {
250
- id: String(payload.senderId || 'unknown'),
251
- name: typeof payload.senderName === 'string' ? payload.senderName : null,
252
- },
253
- text,
254
- attachments,
255
- receivedAt: typeof payload.endedAt === 'string' ? payload.endedAt : new Date().toISOString(),
256
- replyTarget: '',
257
- });
258
- }
259
- buildVisibleText(conn, payload, attachments) {
260
- const text = String(payload.text || '').trim();
261
- if (text)
262
- return text;
263
- if (Array.isArray(payload.messages)) {
264
- return payload.messages
265
- .map((item, index) => {
266
- if (!item || typeof item !== 'object')
267
- return '';
268
- const message = item;
269
- const messageKey = this.batchMessageKey(message, payload);
270
- if (messageKey && this.hasSeenBatchMessage(conn, messageKey))
271
- return '';
272
- const isGroupMessage = String(message.conversationType || payload.conversationType || '') === 'group';
273
- const sender = String(message.senderName || (isGroupMessage ? '群友' : message.senderExternalId || message.senderId || 'unknown'));
274
- const time = this.formatMessageTime(message.timestampIso || message.timestamp);
275
- const messageText = String(message.text || '').trim();
276
- const messageAttachments = this.normalizeAttachments(message);
277
- const attachmentText = messageAttachments.map((attachment) => this.formatAttachment(attachment)).join(' ');
278
- const content = [messageText, attachmentText].filter(Boolean).join(' ').trim() || `[${String(message.contentType || 'message')}]`;
279
- if (messageKey)
280
- this.rememberBatchMessage(conn, messageKey);
281
- return `${index + 1}. ${time} ${sender}: ${content}`;
282
- })
283
- .filter(Boolean)
284
- .join('\n')
285
- .trim();
286
- }
287
- return attachments.map((attachment) => this.formatAttachment(attachment)).join('\n');
288
- }
289
- batchMessageKey(message, payload) {
290
- const explicitId = String(message.messageId || message.msgid || message.id || message.rawId || '').trim();
291
- if (explicitId)
292
- return `id:${explicitId}`;
293
- const sender = String(message.senderExternalId || message.senderId || message.senderName || '').trim();
294
- const timestamp = String(message.timestampIso || message.timestamp || message.receivedAt || '').trim();
295
- const text = String(message.text || '').trim();
296
- const contentType = String(message.contentType || '').trim();
297
- const conversationId = String(message.conversationId || payload.conversationId || '').trim();
298
- const attachmentKey = this.normalizeAttachments(message)
299
- .map((attachment) => [attachment.type, attachment.name || '', attachment.url || '', attachment.size ?? ''].join(':'))
300
- .join('|');
301
- return `content:${conversationId}\n${sender}\n${timestamp}\n${contentType}\n${text}\n${attachmentKey}`;
302
- }
303
- hasSeenBatchMessage(conn, key) {
304
- return conn.seenBatchMessageKeys.has(key);
305
- }
306
- rememberBatchMessage(conn, key) {
307
- if (conn.seenBatchMessageKeys.has(key))
308
- return;
309
- conn.seenBatchMessageKeys.add(key);
310
- conn.seenBatchMessageQueue.push(key);
311
- while (conn.seenBatchMessageQueue.length > 2_000) {
312
- const removed = conn.seenBatchMessageQueue.shift();
313
- if (removed)
314
- conn.seenBatchMessageKeys.delete(removed);
315
- }
316
- }
317
- normalizeAttachments(payload) {
318
- const direct = Array.isArray(payload.attachments) ? payload.attachments : [];
319
- const nested = Array.isArray(payload.messages)
320
- ? payload.messages.flatMap((item) => {
321
- if (!item || typeof item !== 'object')
322
- return [];
323
- const message = item;
324
- return Array.isArray(message.attachments) ? message.attachments : [];
325
- })
326
- : [];
327
- return [...direct, ...nested]
328
- .filter((item) => Boolean(item) && typeof item === 'object')
329
- .map((item) => ({
330
- type: String(item.type || 'file'),
331
- name: typeof item.name === 'string' ? item.name : undefined,
332
- url: typeof item.url === 'string' ? item.url : undefined,
333
- mimeType: typeof item.mimeType === 'string' ? item.mimeType : undefined,
334
- size: typeof item.size === 'number' ? item.size : undefined,
335
- }));
336
- }
337
- formatAttachment(attachment) {
338
- const label = attachment.name ? `${attachment.type}: ${attachment.name}` : attachment.type;
339
- return attachment.url ? `[${label} ${attachment.url}]` : `[${label}]`;
340
- }
341
- formatMessageTime(value) {
342
- const date = typeof value === 'number'
343
- ? new Date(value < 10_000_000_000 ? value * 1000 : value)
344
- : new Date(String(value || ''));
345
- if (Number.isNaN(date.getTime()))
346
- return String(value || '');
347
- return new Intl.DateTimeFormat('zh-CN', {
348
- timeZone: 'Asia/Shanghai',
349
- month: '2-digit',
350
- day: '2-digit',
351
- hour: '2-digit',
352
- minute: '2-digit',
353
- second: '2-digit',
354
- hour12: false,
355
- }).format(date).replace(/\//g, '-');
356
- }
357
- isDuplicate(conn, messageId) {
358
- if (conn.dedup.has(messageId))
359
- return true;
360
- conn.dedup.add(messageId);
361
- conn.dedupQueue.push(messageId);
362
- if (conn.dedupQueue.length > 1_000) {
363
- const removed = conn.dedupQueue.shift();
364
- if (removed)
365
- conn.dedup.delete(removed);
366
- }
367
- return false;
368
- }
369
- parsePayload(raw) {
370
- try {
371
- const parsed = JSON.parse(raw);
372
- return parsed && typeof parsed === 'object' ? parsed : null;
373
- }
374
- catch {
375
- return null;
376
- }
377
- }
378
- }
1
+ import{randomUUID as w}from"node:crypto";import u from"ws";import{ChannelSecretRegistry as S}from"./secret-registry.js";const l=[2e3,5e3,1e4,3e4],y=3e4,k=15e3;class M{onMessage;type="websocket";secrets=new S;connections=new Map;constructor(e){this.onMessage=e}async connect(e){if(!e.enabled)return;const t=this.ensureConnection(e);return t.connecting||(t.stopped=!1,t.config=e,t.connecting=this.openConnection(t).finally(()=>{t.connecting=null})),t.connecting}async disconnect(e){const t=this.connections.get(e.id);if(!t)return;t.stopped=!0,t.reconnectTimer&&clearTimeout(t.reconnectTimer),t.pingTimer&&clearInterval(t.pingTimer),t.reconnectTimer=null,t.pingTimer=null;for(const n of t.pendingAcks.values())clearTimeout(n.timer),n.reject(new Error("External websocket adapter disconnected"));t.pendingAcks.clear();const s=t.socket;t.socket=null,s&&await new Promise(n=>{s.once("close",()=>n()),s.close(),setTimeout(n,500)}),this.connections.delete(e.id)}async send(e,t){const s=this.ensureConnection(e);if(this.readSecret(e).canReply===!1)throw new Error("External websocket channel does not allow replies");await this.connect(e);const r=t.idempotencyKey||`send_${w()}`;await this.sendAwaitAck(s,r,{type:"message.send",requestId:r,conversationId:t.conversationId,contentType:"text",text:t.text,replyToMessageId:t.messageId??void 0})}async health(e){const t=this.readSecret(e);return!t.wsUrl||!t.token?{ok:!1,message:"External websocket requires local wsUrl and token"}:this.connections.get(e.id)?.socket?.readyState===u.OPEN?{ok:!0}:{ok:!1,message:"External websocket disconnected"}}async defaultConversation(e){const t=this.readSecret(e),s=new URL(t.wsUrl);s.protocol=s.protocol==="wss:"?"https:":"http:",s.pathname=s.pathname.replace(/\/ws\/?$/,"/subscription/self"),s.search="";const n=await fetch(s,{headers:{Authorization:`Bearer ${t.token}`}}),r=await n.json().catch(()=>null);if(!n.ok||!r?.ok)throw new Error(r?.error||`External websocket metadata failed: ${n.status}`);if(r.subscription?.allowSend===!1)throw new Error("External websocket subscription does not allow send");const o=String(r.subscription?.defaultConversationId||"").trim();if(!o)throw new Error("External websocket subscription has no single default conversation");return{conversationId:o,conversationName:String(r.subscription?.defaultConversationName||"").trim()||void 0}}ensureConnection(e){let t=this.connections.get(e.id);return t||(t={config:e,socket:null,connecting:null,stopped:!1,reconnectAttempt:0,reconnectTimer:null,pingTimer:null,dedup:new Set,dedupQueue:[],seenBatchMessageKeys:new Set,seenBatchMessageQueue:[],pendingAcks:new Map},this.connections.set(e.id,t)),t.config=e,t}readSecret(e){const t=this.secrets.get(e.secretRef);if(!t||t.type!=="websocket"||!t.wsUrl||!t.token)throw new Error("External websocket channel is not configured on this daemon");return t}async openConnection(e){const t=this.readSecret(e.config),s=t.wsUrl,n=new u(s,{headers:{Authorization:`Bearer ${t.token}`}});e.socket=n,await new Promise((r,o)=>{const i=()=>{n.off("open",c),n.off("error",a)},c=()=>{i(),r()},a=d=>{i(),o(d)};n.once("open",c),n.once("error",a)}),n.on("message",r=>{const o=this.parsePayload(r.toString());o&&this.handlePayload(e,o)}),n.on("close",()=>this.scheduleReconnect(e)),n.on("error",()=>this.scheduleReconnect(e)),e.reconnectAttempt=0,e.pingTimer&&clearInterval(e.pingTimer),e.pingTimer=setInterval(()=>{this.sendJson(e,{type:"ping",timestamp:new Date().toISOString()}).catch(()=>{})},y),e.pingTimer.unref()}scheduleReconnect(e){if(e.stopped||e.reconnectTimer||e.connecting)return;const t=l[Math.min(e.reconnectAttempt,l.length-1)];e.reconnectAttempt+=1,e.reconnectTimer=setTimeout(()=>{e.reconnectTimer=null,this.connect(e.config).catch(()=>{})},t),e.reconnectTimer.unref()}async sendJson(e,t){if(!e.socket||e.socket.readyState!==u.OPEN)throw new Error("External websocket is not connected");e.socket.send(JSON.stringify(t))}async sendAwaitAck(e,t,s){await new Promise((n,r)=>{const o=setTimeout(()=>{e.pendingAcks.delete(t),r(new Error("External websocket send timed out"))},k);o.unref(),e.pendingAcks.set(t,{resolve:n,reject:r,timer:o}),this.sendJson(e,s).catch(i=>{clearTimeout(o),e.pendingAcks.delete(t),r(i instanceof Error?i:new Error(String(i)))})})}handlePayload(e,t){const s=String(t.type||"");if(s==="ack"||s==="error"){const c=String(t.requestId||""),a=c?e.pendingAcks.get(c):void 0;if(!a)return;clearTimeout(a.timer),e.pendingAcks.delete(c),s==="ack"&&t.ok!==!1?a.resolve():a.reject(new Error(String(t.error||"External websocket send failed")));return}if(s==="hello"||s==="pong"||s!=="message.inbound"&&s!=="message.inbound.batch")return;const n=String(t.messageId||t.batchId||t.id||"");if(!n||this.isDuplicate(e,n))return;const r=this.normalizeAttachments(t),o=this.buildVisibleText(e,t,r),i=String(t.conversationId||"").trim();Array.isArray(t.messages)&&!o||!o&&r.length===0||!i||this.onMessage?.({managerSessionId:e.config.managerSessionId,channelId:e.config.id,channelType:"websocket",conversationId:i,messageId:n,sender:{id:String(t.senderId||"unknown"),name:typeof t.senderName=="string"?t.senderName:null},text:o,attachments:r,receivedAt:typeof t.endedAt=="string"?t.endedAt:new Date().toISOString(),replyTarget:""})}buildVisibleText(e,t,s){const n=String(t.text||"").trim();return n||(Array.isArray(t.messages)?t.messages.map((r,o)=>{if(!r||typeof r!="object")return"";const i=r,c=this.batchMessageKey(i,t);if(c&&this.hasSeenBatchMessage(e,c))return"";const a=String(i.conversationType||t.conversationType||"")==="group",d=String(i.senderName||(a?"\u7FA4\u53CB":i.senderExternalId||i.senderId||"unknown")),m=this.formatMessageTime(i.timestampIso||i.timestamp),h=String(i.text||"").trim(),g=this.normalizeAttachments(i).map(f=>this.formatAttachment(f)).join(" "),p=[h,g].filter(Boolean).join(" ").trim()||`[${String(i.contentType||"message")}]`;return c&&this.rememberBatchMessage(e,c),`${o+1}. ${m} ${d}: ${p}`}).filter(Boolean).join(`
2
+ `).trim():s.map(r=>this.formatAttachment(r)).join(`
3
+ `))}batchMessageKey(e,t){const s=String(e.messageId||e.msgid||e.id||e.rawId||"").trim();if(s)return`id:${s}`;const n=String(e.senderExternalId||e.senderId||e.senderName||"").trim(),r=String(e.timestampIso||e.timestamp||e.receivedAt||"").trim(),o=String(e.text||"").trim(),i=String(e.contentType||"").trim(),c=String(e.conversationId||t.conversationId||"").trim(),a=this.normalizeAttachments(e).map(d=>[d.type,d.name||"",d.url||"",d.size??""].join(":")).join("|");return`content:${c}
4
+ ${n}
5
+ ${r}
6
+ ${i}
7
+ ${o}
8
+ ${a}`}hasSeenBatchMessage(e,t){return e.seenBatchMessageKeys.has(t)}rememberBatchMessage(e,t){if(!e.seenBatchMessageKeys.has(t))for(e.seenBatchMessageKeys.add(t),e.seenBatchMessageQueue.push(t);e.seenBatchMessageQueue.length>2e3;){const s=e.seenBatchMessageQueue.shift();s&&e.seenBatchMessageKeys.delete(s)}}normalizeAttachments(e){const t=Array.isArray(e.attachments)?e.attachments:[],s=Array.isArray(e.messages)?e.messages.flatMap(n=>{if(!n||typeof n!="object")return[];const r=n;return Array.isArray(r.attachments)?r.attachments:[]}):[];return[...t,...s].filter(n=>!!n&&typeof n=="object").map(n=>({type:String(n.type||"file"),name:typeof n.name=="string"?n.name:void 0,url:typeof n.url=="string"?n.url:void 0,mimeType:typeof n.mimeType=="string"?n.mimeType:void 0,size:typeof n.size=="number"?n.size:void 0}))}formatAttachment(e){const t=e.name?`${e.type}: ${e.name}`:e.type;return e.url?`[${t} ${e.url}]`:`[${t}]`}formatMessageTime(e){const t=typeof e=="number"?new Date(e<1e10?e*1e3:e):new Date(String(e||""));return Number.isNaN(t.getTime())?String(e||""):new Intl.DateTimeFormat("zh-CN",{timeZone:"Asia/Shanghai",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}).format(t).replace(/\//g,"-")}isDuplicate(e,t){if(e.dedup.has(t))return!0;if(e.dedup.add(t),e.dedupQueue.push(t),e.dedupQueue.length>1e3){const s=e.dedupQueue.shift();s&&e.dedup.delete(s)}return!1}parsePayload(e){try{const t=JSON.parse(e);return t&&typeof t=="object"?t:null}catch{return null}}}export{M as ExternalWebSocketChannelAdapter};
@@ -1,65 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @test src/__tests__/wechat-channel-anchor.test.ts
3
- export function normalizeWeChatAnchorText(value) {
4
- if (typeof value !== 'string')
5
- return '';
6
- return value
7
- .normalize('NFKC')
8
- .replace(/\s+/g, ' ')
9
- .trim()
10
- .toLowerCase();
11
- }
12
- export function weChatAnchorText(message) {
13
- return normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '');
14
- }
15
- export function weChatTextSimilarity(left, right) {
16
- const a = normalizeWeChatAnchorText(left);
17
- const b = normalizeWeChatAnchorText(right);
18
- if (!a || !b)
19
- return 0;
20
- if (a === b)
21
- return 1;
22
- const distance = levenshtein(a, b);
23
- return 1 - distance / Math.max(a.length, b.length);
24
- }
25
- export function isLikelySameWeChatMessage(previous, current, threshold = 0.86) {
26
- if (previous.stableMessageKey && previous.stableMessageKey === current.stableMessageKey)
27
- return true;
28
- if (previous.senderRole !== current.senderRole)
29
- return false;
30
- if (previous.kind !== current.kind)
31
- return false;
32
- const previousAnchor = weChatAnchorText(previous);
33
- const currentAnchor = weChatAnchorText(current);
34
- if (!previousAnchor || !currentAnchor)
35
- return false;
36
- return weChatTextSimilarity(previousAnchor, currentAnchor) >= threshold;
37
- }
38
- export function filterNewWeChatMessagesByAnchor(input) {
39
- const consumed = new Set();
40
- const result = [];
41
- for (const current of input.current) {
42
- const index = input.previous.findIndex((previous, previousIndex) => {
43
- return !consumed.has(previousIndex) && isLikelySameWeChatMessage(previous, current, input.threshold);
44
- });
45
- if (index >= 0) {
46
- consumed.add(index);
47
- continue;
48
- }
49
- result.push(current);
50
- }
51
- return result;
52
- }
53
- function levenshtein(a, b) {
54
- const prev = Array.from({ length: b.length + 1 }, (_, index) => index);
55
- const curr = Array.from({ length: b.length + 1 }, () => 0);
56
- for (let i = 1; i <= a.length; i += 1) {
57
- curr[0] = i;
58
- for (let j = 1; j <= b.length; j += 1) {
59
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
60
- }
61
- for (let j = 0; j <= b.length; j += 1)
62
- prev[j] = curr[j];
63
- }
64
- return prev[b.length];
65
- }
1
+ function i(e){return typeof e!="string"?"":e.normalize("NFKC").replace(/\s+/g," ").trim().toLowerCase()}function a(e){return i(e.anchorText||e.normalizedText||e.textExcerpt||"")}function l(e,t){const o=i(e),r=i(t);return!o||!r?0:o===r?1:1-f(o,r)/Math.max(o.length,r.length)}function h(e,t,o=.86){if(e.stableMessageKey&&e.stableMessageKey===t.stableMessageKey)return!0;if(e.senderRole!==t.senderRole||e.kind!==t.kind)return!1;const r=a(e),s=a(t);return!r||!s?!1:l(r,s)>=o}function u(e){const t=new Set,o=[];for(const r of e.current){const s=e.previous.findIndex((n,c)=>!t.has(c)&&h(n,r,e.threshold));if(s>=0){t.add(s);continue}o.push(r)}return o}function f(e,t){const o=Array.from({length:t.length+1},(s,n)=>n),r=Array.from({length:t.length+1},()=>0);for(let s=1;s<=e.length;s+=1){r[0]=s;for(let n=1;n<=t.length;n+=1)r[n]=Math.min(o[n]+1,r[n-1]+1,o[n-1]+(e[s-1]===t[n-1]?0:1));for(let n=0;n<=t.length;n+=1)o[n]=r[n]}return o[t.length]}export{u as filterNewWeChatMessagesByAnchor,h as isLikelySameWeChatMessage,i as normalizeWeChatAnchorText,a as weChatAnchorText,l as weChatTextSimilarity};
@@ -1,96 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @test src/__tests__/wechat-channel-client.test.ts
3
- import { SERVERS } from '../../region.js';
4
- import { loadConfig } from '../../config/index.js';
5
- export function createWeChatChannelApiClient(options = {}) {
6
- const config = loadConfig();
7
- const serverUrl = normalizeServerUrl(options.serverUrl || config.serverUrl || SERVERS.cn.url);
8
- const machineToken = options.machineToken || config.machineToken;
9
- const fetchImpl = options.fetchImpl || fetch;
10
- if (!machineToken)
11
- throw new Error('WeChat channel requires a paired machine token');
12
- async function request(path, body) {
13
- const response = await fetchImpl(`${serverUrl}/api/channels/wechat${path}`, {
14
- method: body === undefined ? 'GET' : 'POST',
15
- headers: {
16
- authorization: `Bearer ${machineToken}`,
17
- ...(body === undefined ? {} : { 'content-type': 'application/json' }),
18
- },
19
- body: body === undefined ? undefined : JSON.stringify(body),
20
- });
21
- const text = await response.text();
22
- const payload = text ? JSON.parse(text) : null;
23
- if (!response.ok || payload?.ok === false) {
24
- const reason = payload?.reasonCode || response.statusText || 'request_failed';
25
- throw new Error(`WeChat channel API ${path} failed: ${reason}`);
26
- }
27
- return payload;
28
- }
29
- return {
30
- getRuntimePolicy: () => request('/runtime-policy'),
31
- upsertRuntime: (runtime, binding) => request('/runtime', {
32
- runtimeId: runtime.runtimeId,
33
- machineId: runtime.machineId,
34
- pollIntervalSeconds: Math.round(runtime.policy.pollIntervalMs / 1000),
35
- foregroundPolicy: runtime.foregroundPolicy,
36
- clientRuntimeVersion: String(runtime.policy.runtimeVersion),
37
- ...(binding ? bindingPayload(binding) : {}),
38
- }),
39
- observe: (runtime, binding, input) => request('/observe', {
40
- runtimeId: runtime.runtimeId,
41
- bindingId: binding.bindingId,
42
- sessionId: binding.sessionId,
43
- machineId: runtime.machineId,
44
- conversationName: binding.conversationDisplayName,
45
- schemaVersion: runtime.policy.runtimeVersion,
46
- screenshots: input.screenshots,
47
- edgeOcrBlocks: input.edgeOcrBlocks ?? [],
48
- visibleConversationFingerprints: input.visibleConversationFingerprints ?? [],
49
- localLedgerTailAnchors: input.localLedgerTailAnchors ?? [],
50
- }),
51
- ingest: (runtime, binding, input) => request('/ingest', {
52
- runtimeId: runtime.runtimeId,
53
- idempotencyKey: input.idempotencyKey,
54
- bindingId: binding.bindingId,
55
- sessionId: binding.sessionId,
56
- machineId: runtime.machineId,
57
- messages: input.messages,
58
- }),
59
- reportOutboundStatus: (runtime, binding, input) => request('/outbound-status', {
60
- runtimeId: runtime.runtimeId,
61
- replyId: input.replyId,
62
- idempotencyKey: input.idempotencyKey,
63
- bindingId: binding.bindingId,
64
- sessionId: binding.sessionId,
65
- machineId: runtime.machineId,
66
- status: input.status,
67
- replyBaseRevision: input.replyBaseRevision,
68
- sentAt: input.sentAt,
69
- confirmedAt: input.confirmedAt,
70
- failureCode: input.failureCode,
71
- lastErrorSummary: input.lastErrorSummary,
72
- }),
73
- reportRunStatus: (runtime, binding, input) => request('/run-status', {
74
- runtimeId: runtime.runtimeId,
75
- bindingId: binding.bindingId,
76
- sessionId: binding.sessionId,
77
- machineId: runtime.machineId,
78
- status: input.status,
79
- reasonCode: input.reasonCode,
80
- traceId: input.traceId,
81
- }),
82
- };
83
- }
84
- function bindingPayload(binding) {
85
- return {
86
- bindingId: binding.bindingId,
87
- sessionId: binding.sessionId,
88
- conversationName: binding.conversationDisplayName,
89
- enabled: binding.enabled,
90
- allowReply: binding.allowReply,
91
- downloadMedia: binding.downloadMedia,
92
- };
93
- }
94
- function normalizeServerUrl(url) {
95
- return url.replace(/\/+$/, '');
96
- }
1
+ import{SERVERS as m}from"../../region.js";import{loadConfig as u}from"../../config/index.js";function v(o={}){const t=u(),c=y(o.serverUrl||t.serverUrl||m.cn.url),i=o.machineToken||t.machineToken,l=o.fetchImpl||fetch;if(!i)throw new Error("WeChat channel requires a paired machine token");async function r(n,s){const e=await l(`${c}/api/channels/wechat${n}`,{method:s===void 0?"GET":"POST",headers:{authorization:`Bearer ${i}`,...s===void 0?{}:{"content-type":"application/json"}},body:s===void 0?void 0:JSON.stringify(s)}),d=await e.text(),a=d?JSON.parse(d):null;if(!e.ok||a?.ok===!1){const I=a?.reasonCode||e.statusText||"request_failed";throw new Error(`WeChat channel API ${n} failed: ${I}`)}return a}return{getRuntimePolicy:()=>r("/runtime-policy"),upsertRuntime:(n,s)=>r("/runtime",{runtimeId:n.runtimeId,machineId:n.machineId,pollIntervalSeconds:Math.round(n.policy.pollIntervalMs/1e3),foregroundPolicy:n.foregroundPolicy,clientRuntimeVersion:String(n.policy.runtimeVersion),...s?h(s):{}}),observe:(n,s,e)=>r("/observe",{runtimeId:n.runtimeId,bindingId:s.bindingId,sessionId:s.sessionId,machineId:n.machineId,conversationName:s.conversationDisplayName,schemaVersion:n.policy.runtimeVersion,screenshots:e.screenshots,edgeOcrBlocks:e.edgeOcrBlocks??[],visibleConversationFingerprints:e.visibleConversationFingerprints??[],localLedgerTailAnchors:e.localLedgerTailAnchors??[]}),ingest:(n,s,e)=>r("/ingest",{runtimeId:n.runtimeId,idempotencyKey:e.idempotencyKey,bindingId:s.bindingId,sessionId:s.sessionId,machineId:n.machineId,messages:e.messages}),reportOutboundStatus:(n,s,e)=>r("/outbound-status",{runtimeId:n.runtimeId,replyId:e.replyId,idempotencyKey:e.idempotencyKey,bindingId:s.bindingId,sessionId:s.sessionId,machineId:n.machineId,status:e.status,replyBaseRevision:e.replyBaseRevision,sentAt:e.sentAt,confirmedAt:e.confirmedAt,failureCode:e.failureCode,lastErrorSummary:e.lastErrorSummary}),reportRunStatus:(n,s,e)=>r("/run-status",{runtimeId:n.runtimeId,bindingId:s.bindingId,sessionId:s.sessionId,machineId:n.machineId,status:e.status,reasonCode:e.reasonCode,traceId:e.traceId})}}function h(o){return{bindingId:o.bindingId,sessionId:o.sessionId,conversationName:o.conversationDisplayName,enabled:o.enabled,allowReply:o.allowReply,downloadMedia:o.downloadMedia}}function y(o){return o.replace(/\/+$/,"")}export{v as createWeChatChannelApiClient};
@@ -1,38 +1 @@
1
- // @arch docs/features/wechat-rpa-productization-plan.md
2
- // @arch docs/features/wechat-rpa-outbound-ledger.md
3
- // @test src/__tests__/wechat-channel-cooldown.test.ts
4
- export const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD = 3;
5
- export const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS = 5 * 60 * 1000;
6
- export function isWeChatChannelCooldownActive(state, now = new Date()) {
7
- if (!state?.cooldownUntil)
8
- return false;
9
- const until = new Date(state.cooldownUntil).getTime();
10
- return Number.isFinite(until) && until > now.getTime();
11
- }
12
- export function noteWeChatChannelInterruption(input) {
13
- const now = input.now ?? new Date();
14
- const previous = input.state ?? { consecutiveInterruptions: 0 };
15
- const consecutiveInterruptions = previous.consecutiveInterruptions + 1;
16
- const next = {
17
- consecutiveInterruptions,
18
- cooldownUntil: previous.cooldownUntil ?? null,
19
- manualReviewReason: previous.manualReviewReason ?? null,
20
- };
21
- if (consecutiveInterruptions >= WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD) {
22
- next.cooldownUntil = new Date(now.getTime() + WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS).toISOString();
23
- next.manualReviewReason = input.reason || 'user_interruption_cooldown';
24
- }
25
- return next;
26
- }
27
- export function noteWeChatChannelStableRun(state) {
28
- return {
29
- consecutiveInterruptions: 0,
30
- cooldownUntil: state?.cooldownUntil ?? null,
31
- manualReviewReason: state?.manualReviewReason ?? null,
32
- };
33
- }
34
- export function clearExpiredWeChatChannelCooldown(state, now = new Date()) {
35
- if (isWeChatChannelCooldownActive(state, now))
36
- return state;
37
- return { ...state, cooldownUntil: null };
38
- }
1
+ const r=3,u=3e5;function i(n,o=new Date){if(!n?.cooldownUntil)return!1;const e=new Date(n.cooldownUntil).getTime();return Number.isFinite(e)&&e>o.getTime()}function c(n){const o=n.now??new Date,e=n.state??{consecutiveInterruptions:0},l=e.consecutiveInterruptions+1,t={consecutiveInterruptions:l,cooldownUntil:e.cooldownUntil??null,manualReviewReason:e.manualReviewReason??null};return l>=3&&(t.cooldownUntil=new Date(o.getTime()+3e5).toISOString(),t.manualReviewReason=n.reason||"user_interruption_cooldown"),t}function a(n){return{consecutiveInterruptions:0,cooldownUntil:n?.cooldownUntil??null,manualReviewReason:n?.manualReviewReason??null}}function s(n,o=new Date){return i(n,o)?n:{...n,cooldownUntil:null}}export{u as WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS,r as WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD,s as clearExpiredWeChatChannelCooldown,i as isWeChatChannelCooldownActive,c as noteWeChatChannelInterruption,a as noteWeChatChannelStableRun};