shennian 0.2.88 → 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 (143) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +22 -0
  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.d.ts +6 -0
  8. package/dist/src/agents/adapter.js +1 -19
  9. package/dist/src/agents/claude.js +8 -305
  10. package/dist/src/agents/codex-control.d.ts +35 -0
  11. package/dist/src/agents/codex-control.js +2 -0
  12. package/dist/src/agents/codex-utils.js +7 -200
  13. package/dist/src/agents/codex.d.ts +8 -0
  14. package/dist/src/agents/codex.js +15 -863
  15. package/dist/src/agents/command-spec.js +2 -413
  16. package/dist/src/agents/config-status.js +1 -226
  17. package/dist/src/agents/cursor.js +1 -249
  18. package/dist/src/agents/custom.js +4 -271
  19. package/dist/src/agents/detect.js +1 -56
  20. package/dist/src/agents/external-channel-instructions.js +10 -94
  21. package/dist/src/agents/gemini.js +1 -173
  22. package/dist/src/agents/manager.js +13 -157
  23. package/dist/src/agents/model-registry/cache.js +1 -37
  24. package/dist/src/agents/model-registry/discovery.js +2 -187
  25. package/dist/src/agents/model-registry/parsers.js +4 -447
  26. package/dist/src/agents/model-registry/runner.js +1 -30
  27. package/dist/src/agents/model-registry/service.js +1 -78
  28. package/dist/src/agents/model-registry/types.js +1 -8
  29. package/dist/src/agents/model-registry.js +1 -18
  30. package/dist/src/agents/openclaw.js +2 -275
  31. package/dist/src/agents/opencode.js +1 -231
  32. package/dist/src/agents/pi-context.js +12 -217
  33. package/dist/src/agents/pi.js +14 -723
  34. package/dist/src/agents/platform-instructions.js +9 -54
  35. package/dist/src/channels/base.d.ts +4 -1
  36. package/dist/src/channels/base.js +1 -3
  37. package/dist/src/channels/registry.js +1 -30
  38. package/dist/src/channels/reply-split.js +10 -89
  39. package/dist/src/channels/runtime.d.ts +1 -0
  40. package/dist/src/channels/runtime.js +5 -533
  41. package/dist/src/channels/secret-registry.d.ts +1 -0
  42. package/dist/src/channels/secret-registry.js +1 -46
  43. package/dist/src/channels/websocket.js +8 -378
  44. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  45. package/dist/src/channels/wechat-channel/anchor.js +1 -0
  46. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  47. package/dist/src/channels/wechat-channel/client.js +1 -0
  48. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  49. package/dist/src/channels/wechat-channel/cooldown.js +1 -0
  50. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  51. package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
  52. package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
  53. package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
  54. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  55. package/dist/src/channels/wechat-channel/helper-client.js +3 -0
  56. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  57. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
  58. package/dist/src/channels/wechat-channel/index.d.ts +17 -0
  59. package/dist/src/channels/wechat-channel/index.js +1 -0
  60. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  61. package/dist/src/channels/wechat-channel/ledger.js +1 -0
  62. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  63. package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
  64. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  65. package/dist/src/channels/wechat-channel/message-key.js +1 -0
  66. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  67. package/dist/src/channels/wechat-channel/observer.js +1 -0
  68. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
  69. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
  70. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  71. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  72. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  73. package/dist/src/channels/wechat-channel/preflight.js +1 -0
  74. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  75. package/dist/src/channels/wechat-channel/runner.js +1 -0
  76. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  77. package/dist/src/channels/wechat-channel/runtime.js +1 -0
  78. package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
  79. package/dist/src/channels/wechat-channel/scheduler.js +1 -0
  80. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  81. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  82. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  83. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  84. package/dist/src/channels/wechat-rpa.js +6 -1022
  85. package/dist/src/channels/wecom.js +4 -357
  86. package/dist/src/commands/agent.js +6 -131
  87. package/dist/src/commands/daemon-windows.js +8 -48
  88. package/dist/src/commands/daemon.js +19 -1013
  89. package/dist/src/commands/external-attachments.js +1 -51
  90. package/dist/src/commands/external.js +1 -137
  91. package/dist/src/commands/manager.js +2 -389
  92. package/dist/src/commands/pair-qr.js +1 -6
  93. package/dist/src/commands/pair.js +9 -287
  94. package/dist/src/commands/tools.js +1 -34
  95. package/dist/src/commands/upgrade.js +1 -198
  96. package/dist/src/config/index.js +1 -35
  97. package/dist/src/daemon-log.js +6 -58
  98. package/dist/src/env-path.js +1 -64
  99. package/dist/src/fs/boundary.js +1 -126
  100. package/dist/src/fs/handler.js +1 -130
  101. package/dist/src/fs/security.js +1 -32
  102. package/dist/src/fs/text-decoder.d.ts +10 -0
  103. package/dist/src/fs/text-decoder.js +1 -0
  104. package/dist/src/index.js +2 -404
  105. package/dist/src/log-reporter.js +1 -16
  106. package/dist/src/manager/prompt.js +29 -34
  107. package/dist/src/manager/registry.js +2 -269
  108. package/dist/src/manager/runtime.js +19 -1003
  109. package/dist/src/native-fusion/config.js +1 -5
  110. package/dist/src/native-fusion/opencode-parser.js +3 -123
  111. package/dist/src/native-fusion/parser-common.js +8 -264
  112. package/dist/src/native-fusion/parsers.js +8 -729
  113. package/dist/src/native-fusion/service.d.ts +10 -0
  114. package/dist/src/native-fusion/service.js +2 -198
  115. package/dist/src/native-fusion/state.js +1 -22
  116. package/dist/src/native-fusion/types.js +1 -1
  117. package/dist/src/region.js +1 -88
  118. package/dist/src/relay/client.js +1 -343
  119. package/dist/src/session/archive-zip.js +1 -220
  120. package/dist/src/session/handlers/agent-config.js +1 -150
  121. package/dist/src/session/handlers/agents.js +1 -55
  122. package/dist/src/session/handlers/chat.js +2 -733
  123. package/dist/src/session/handlers/control.js +1 -55
  124. package/dist/src/session/handlers/fs.js +1 -747
  125. package/dist/src/session/handlers/session-refresh.js +1 -35
  126. package/dist/src/session/handlers/skills.js +1 -121
  127. package/dist/src/session/handlers/title.js +1 -60
  128. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  129. package/dist/src/session/handlers/tool-detail.js +1 -0
  130. package/dist/src/session/manager.d.ts +3 -0
  131. package/dist/src/session/manager.js +1 -261
  132. package/dist/src/session/projection.js +1 -54
  133. package/dist/src/session/queue.js +4 -317
  134. package/dist/src/session/remote-attachments.js +1 -72
  135. package/dist/src/session/store.js +3 -109
  136. package/dist/src/session/types.d.ts +4 -0
  137. package/dist/src/session/types.js +1 -4
  138. package/dist/src/skills/registry.js +15 -148
  139. package/dist/src/skills/setup.js +1 -101
  140. package/dist/src/tools/markdown-to-pdf.js +10 -346
  141. package/dist/src/upgrade/engine.js +3 -347
  142. package/package.json +3 -2
  143. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
@@ -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};
@@ -0,0 +1,10 @@
1
+ import type { WeChatChannelObservedMessage } from './client.js';
2
+ export declare function normalizeWeChatAnchorText(value: unknown): string;
3
+ export declare function weChatAnchorText(message: WeChatChannelObservedMessage): string;
4
+ export declare function weChatTextSimilarity(left: unknown, right: unknown): number;
5
+ export declare function isLikelySameWeChatMessage(previous: WeChatChannelObservedMessage, current: WeChatChannelObservedMessage, threshold?: number): boolean;
6
+ export declare function filterNewWeChatMessagesByAnchor(input: {
7
+ previous: WeChatChannelObservedMessage[];
8
+ current: WeChatChannelObservedMessage[];
9
+ threshold?: number;
10
+ }): WeChatChannelObservedMessage[];
@@ -0,0 +1 @@
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};
@@ -0,0 +1,74 @@
1
+ import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
2
+ export type WeChatChannelApiClientOptions = {
3
+ serverUrl?: string;
4
+ machineToken?: string;
5
+ fetchImpl?: typeof fetch;
6
+ };
7
+ export type WeChatChannelObservedMessage = {
8
+ stableMessageKey: string;
9
+ senderRole: 'self' | 'contact' | 'system' | 'unknown';
10
+ senderName?: string | null;
11
+ kind: string;
12
+ normalizedText?: string | null;
13
+ anchorText?: string | null;
14
+ anchorMetadata?: unknown;
15
+ neighborContext?: unknown;
16
+ textExcerpt?: string | null;
17
+ bbox?: unknown;
18
+ mediaMetadata?: unknown;
19
+ isBaseline?: boolean;
20
+ deliveryStatus?: string;
21
+ observedAt?: string;
22
+ visualBlocks?: Array<{
23
+ blockId: string;
24
+ blockKind: string;
25
+ bbox?: unknown;
26
+ model: string;
27
+ dims: number;
28
+ vectorBase64: string;
29
+ }>;
30
+ };
31
+ export type WeChatChannelApiClient = ReturnType<typeof createWeChatChannelApiClient>;
32
+ export type WeChatChannelObserveInput = {
33
+ screenshots: Array<{
34
+ captureIndex?: number;
35
+ mimeType: string;
36
+ dataBase64: string;
37
+ width: number;
38
+ height: number;
39
+ }>;
40
+ edgeOcrBlocks?: unknown[];
41
+ visibleConversationFingerprints?: unknown[];
42
+ localLedgerTailAnchors?: unknown[];
43
+ };
44
+ export type WeChatChannelObserveResponse = {
45
+ ok: true;
46
+ observedMessages?: WeChatChannelObservedMessage[];
47
+ diff?: unknown;
48
+ reasonCode?: string;
49
+ };
50
+ export type WeChatChannelOutboundStatusInput = {
51
+ replyId: string;
52
+ idempotencyKey: string;
53
+ status: string;
54
+ replyBaseRevision?: number;
55
+ sentAt?: string;
56
+ confirmedAt?: string;
57
+ failureCode?: string;
58
+ lastErrorSummary?: string;
59
+ };
60
+ export declare function createWeChatChannelApiClient(options?: WeChatChannelApiClientOptions): {
61
+ getRuntimePolicy: () => Promise<unknown>;
62
+ upsertRuntime: (runtime: WeChatChannelRuntime, binding?: WeChatChannelBindingConfig) => Promise<unknown>;
63
+ observe: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: WeChatChannelObserveInput) => Promise<WeChatChannelObserveResponse>;
64
+ ingest: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: {
65
+ idempotencyKey: string;
66
+ messages: WeChatChannelObservedMessage[];
67
+ }) => Promise<unknown>;
68
+ reportOutboundStatus: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: WeChatChannelOutboundStatusInput) => Promise<unknown>;
69
+ reportRunStatus: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: {
70
+ status: string;
71
+ reasonCode?: string;
72
+ traceId?: string;
73
+ }) => Promise<unknown>;
74
+ };
@@ -0,0 +1 @@
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};
@@ -0,0 +1,15 @@
1
+ export type WeChatChannelCooldownState = {
2
+ consecutiveInterruptions: number;
3
+ cooldownUntil?: string | null;
4
+ manualReviewReason?: string | null;
5
+ };
6
+ export declare const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_THRESHOLD = 3;
7
+ export declare const WECHAT_CHANNEL_INTERRUPTION_COOLDOWN_MS: number;
8
+ export declare function isWeChatChannelCooldownActive(state: WeChatChannelCooldownState | undefined, now?: Date): boolean;
9
+ export declare function noteWeChatChannelInterruption(input: {
10
+ state?: WeChatChannelCooldownState;
11
+ reason?: string;
12
+ now?: Date;
13
+ }): WeChatChannelCooldownState;
14
+ export declare function noteWeChatChannelStableRun(state?: WeChatChannelCooldownState): WeChatChannelCooldownState;
15
+ export declare function clearExpiredWeChatChannelCooldown(state: WeChatChannelCooldownState, now?: Date): WeChatChannelCooldownState;
@@ -0,0 +1 @@
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};
@@ -0,0 +1,28 @@
1
+ export type WeChatConversationListFingerprint = {
2
+ conversationId?: string | null;
3
+ title?: string | null;
4
+ preview?: string | null;
5
+ timeText?: string | null;
6
+ unreadText?: string | null;
7
+ fingerprint?: string | null;
8
+ bbox?: unknown;
9
+ };
10
+ export type WeChatConversationFingerprintTarget = {
11
+ bindingId: string;
12
+ conversationDisplayName: string;
13
+ lastConversationFingerprint?: string | null;
14
+ lastPreview?: string | null;
15
+ };
16
+ export type WeChatConversationFingerprintMatch = {
17
+ bindingId: string;
18
+ item: WeChatConversationListFingerprint;
19
+ score: number;
20
+ reason: 'fingerprint' | 'title-preview' | 'title' | 'none';
21
+ };
22
+ export declare function buildWeChatConversationFingerprint(input: WeChatConversationListFingerprint): string;
23
+ export declare function matchWeChatConversationFingerprints(input: {
24
+ visibleItems: WeChatConversationListFingerprint[];
25
+ targets: WeChatConversationFingerprintTarget[];
26
+ minScore?: number;
27
+ }): WeChatConversationFingerprintMatch[];
28
+ export declare function scoreConversationFingerprint(target: WeChatConversationFingerprintTarget, item: WeChatConversationListFingerprint): WeChatConversationFingerprintMatch;
@@ -0,0 +1 @@
1
+ function a(n){return[n.title,n.preview,n.timeText,n.unreadText].map(r=>l(r)).filter(Boolean).join("|")}function p(n){const r=n.minScore??.72,t=n.visibleItems.map(o=>({...o,fingerprint:o.fingerprint||a(o)})),i=[];for(const o of n.targets){let e=null;for(const s of t){const c=d(o,s);(!e||c.score>e.score)&&(e=c)}if(e&&e.score>=r&&e.reason!=="none"){i.push(e);const s=t.findIndex(c=>c===e.item);s>=0&&t.splice(s,1)}}return i}function d(n,r){const t=r.fingerprint||a(r);if(n.lastConversationFingerprint&&t&&n.lastConversationFingerprint===t)return{bindingId:n.bindingId,item:r,score:1,reason:"fingerprint"};const i=f(n.conversationDisplayName,r.title),o=n.lastPreview?f(n.lastPreview,r.preview):0;return i>=.8&&o>=.55?{bindingId:n.bindingId,item:r,score:i*.7+o*.3,reason:"title-preview"}:i>=.88?{bindingId:n.bindingId,item:r,score:i*.86,reason:"title"}:{bindingId:n.bindingId,item:r,score:i*.5+o*.2,reason:"none"}}function l(n){return typeof n=="string"?n.trim().replace(/\s+/g," ").toLowerCase():""}function f(n,r){const t=l(n),i=l(r);return!t||!i?0:t===i?1:1-g(t,i)/Math.max(t.length,i.length)}function g(n,r){const t=Array.from({length:r.length+1},(o,e)=>e),i=Array.from({length:r.length+1},()=>0);for(let o=1;o<=n.length;o+=1){i[0]=o;for(let e=1;e<=r.length;e+=1)i[e]=Math.min(t[e]+1,i[e-1]+1,t[e-1]+(n[o-1]===r[e-1]?0:1));for(let e=0;e<=r.length;e+=1)t[e]=i[e]}return t[r.length]}export{a as buildWeChatConversationFingerprint,p as matchWeChatConversationFingerprints,d as scoreConversationFingerprint};
@@ -0,0 +1,37 @@
1
+ export declare const WECHAT_CHANNEL_HELPER_VERSION = "0.1.1";
2
+ export type WeChatChannelHelperAssetManifest = {
3
+ schemaVersion: 1;
4
+ helperVersion: string;
5
+ protocolVersion: number;
6
+ platforms: Record<string, {
7
+ executable: string;
8
+ sha256: string | null;
9
+ signed: boolean;
10
+ notarized: boolean;
11
+ signing?: {
12
+ authority?: string | null;
13
+ teamIdentifier?: string | null;
14
+ hardenedRuntime?: boolean;
15
+ };
16
+ notarization?: {
17
+ status?: string;
18
+ id?: string;
19
+ };
20
+ }>;
21
+ };
22
+ export type WeChatChannelHelperAssetResolution = {
23
+ ok: true;
24
+ helperPath: string;
25
+ version: string;
26
+ manifest: WeChatChannelHelperAssetManifest;
27
+ warning?: string;
28
+ } | {
29
+ ok: false;
30
+ reasonCode: 'unsupported_platform' | 'manifest_missing' | 'helper_missing' | 'integrity_mismatch';
31
+ message: string;
32
+ };
33
+ export declare function resolveWeChatChannelHelperAsset(input?: {
34
+ platform?: NodeJS.Platform | string;
35
+ baseDir?: string;
36
+ verifyIntegrity?: boolean;
37
+ }): WeChatChannelHelperAssetResolution;
@@ -0,0 +1 @@
1
+ import o from"node:fs";import a from"node:path";import f from"node:crypto";import{fileURLToPath as m}from"node:url";const _="0.1.1",c=a.dirname(m(import.meta.url));function v(s={}){if((s.platform??process.platform)!=="darwin")return{ok:!1,reasonCode:"unsupported_platform",message:"WeChat channel helper is only available on macOS in the first version"};const i=s.baseDir??h(),l=a.join(i,"manifest.json"),n=p(l);if(!n)return{ok:!1,reasonCode:"manifest_missing",message:`WeChat channel helper manifest is missing: ${l}`};const e=n.platforms.darwin,r=e?a.join(i,e.executable):"";return!e||!r||!o.existsSync(r)?{ok:!1,reasonCode:"helper_missing",message:`WeChat channel helper executable is missing: ${r||i}`}:s.verifyIntegrity!==!1&&e.sha256&&f.createHash("sha256").update(o.readFileSync(r)).digest("hex")!==e.sha256?{ok:!1,reasonCode:"integrity_mismatch",message:`WeChat channel helper integrity mismatch: ${r}`}:{ok:!0,helperPath:r,version:n.helperVersion,manifest:n,warning:e.signed&&e.notarized?void 0:"helper_not_signed_or_notarized"}}function h(){return a.resolve(c,"../../../assets/wechat-channel/macos")}function p(s){try{const t=JSON.parse(o.readFileSync(s,"utf8"));return t.schemaVersion!==1||typeof t.helperVersion!="string"||!t.platforms?.darwin?null:t}catch{return null}}export{_ as WECHAT_CHANNEL_HELPER_VERSION,v as resolveWeChatChannelHelperAsset};
@@ -0,0 +1,25 @@
1
+ import { type WeChatChannelHelperCommandName, type WeChatChannelHelperHealthResult, type WeChatChannelHelperReady, type WeChatChannelHelperResponse, type WeChatChannelHelperWarmupSnapshot } from './helper-protocol.js';
2
+ export type WeChatChannelHelperClientOptions = {
3
+ helperPath: string;
4
+ expectedHelperVersion: string;
5
+ args?: string[];
6
+ cwd?: string;
7
+ };
8
+ export declare class WeChatChannelHelperClient {
9
+ private options;
10
+ private child;
11
+ private lines;
12
+ private readyState;
13
+ private warmupState;
14
+ private pending;
15
+ constructor(options: WeChatChannelHelperClientOptions);
16
+ start(): Promise<WeChatChannelHelperReady>;
17
+ request<T = unknown>(command: WeChatChannelHelperCommandName, params?: Record<string, unknown>, traceId?: string): Promise<WeChatChannelHelperResponse<T>>;
18
+ healthCheck(traceId?: string): Promise<WeChatChannelHelperResponse<WeChatChannelHelperHealthResult>>;
19
+ getReadyState(): WeChatChannelHelperReady | null;
20
+ getWarmupState(): WeChatChannelHelperWarmupSnapshot | null;
21
+ stop(): Promise<void>;
22
+ private handleLine;
23
+ private captureWarmupSnapshot;
24
+ private rejectAllPending;
25
+ }
@@ -0,0 +1,3 @@
1
+ import{spawn as c}from"node:child_process";import{randomUUID as d}from"node:crypto";import{createInterface as u}from"node:readline";import{createWeChatChannelHelperHello as m,extractWeChatChannelHelperWarmupSnapshot as f,timeoutForWeChatChannelHelperCommand as y,validateWeChatChannelHelperReady as S}from"./helper-protocol.js";class k{options;child=null;lines=null;readyState=null;warmupState=null;pending=new Map;constructor(e){this.options=e}async start(){if(this.child)throw new Error("WeChat channel helper is already started");const e=c(this.options.helperPath,this.options.args??[],{cwd:this.options.cwd,stdio:["pipe","pipe","pipe"]});this.child=e,this.lines=u({input:e.stdout}),this.lines.on("line",r=>this.handleLine(r)),e.once("exit",()=>this.rejectAllPending(new Error("WeChat channel helper process exited")));const t=new Promise((r,s)=>{const l=setTimeout(()=>s(new Error("WeChat channel helper handshake timed out")),2e3),o=h=>{let n;try{n=JSON.parse(h)}catch{clearTimeout(l),this.lines?.off("line",o),s(new Error("WeChat channel helper sent invalid handshake JSON"));return}if(!w(n))return;clearTimeout(l),this.lines?.off("line",o);const a=S(n,this.options.expectedHelperVersion);a.ok?(this.readyState=n,this.captureWarmupSnapshot(n),r(n)):s(new Error(`${a.errorCode}: ${a.errorSummary}`))};this.lines?.on("line",o)});return e.stdin.write(`${JSON.stringify(m(this.options.expectedHelperVersion))}
2
+ `),t}async request(e,t,r){if(!this.child)throw new Error("WeChat channel helper is not started");const s=d(),l=y(e),o=new Promise((n,a)=>{const p=setTimeout(()=>{this.pending.delete(s),a(new Error(`helper_command_timeout: ${e}`))},l);this.pending.set(s,{resolve:n,reject:a,timer:p})});this.child.stdin.write(`${JSON.stringify({id:s,command:e,params:t,traceId:r})}
3
+ `);const h=await o;return this.captureWarmupSnapshot(h),this.captureWarmupSnapshot(h.result),h}async healthCheck(e){return this.request("health.check",{},e)}getReadyState(){return this.readyState?{...this.readyState,capabilities:[...this.readyState.capabilities]}:null}getWarmupState(){return this.warmupState?{warmState:this.warmupState.warmState,metrics:this.warmupState.metrics?{...this.warmupState.metrics}:void 0}:null}async stop(){const e=this.child;this.child=null,this.readyState=null,this.lines?.close(),this.lines=null,this.rejectAllPending(new Error("WeChat channel helper stopped")),!(!e||e.killed)&&e.kill("SIGTERM")}handleLine(e){let t;try{t=JSON.parse(e)}catch{return}if(!g(t))return;const r=this.pending.get(t.id);r&&(clearTimeout(r.timer),this.pending.delete(t.id),r.resolve(t))}captureWarmupSnapshot(e){const t=f(e);t&&(this.warmupState=t)}rejectAllPending(e){for(const[t,r]of this.pending)clearTimeout(r.timer),r.reject(e),this.pending.delete(t)}}function w(i){if(!i||typeof i!="object")return!1;const e=i;return e.type==="ready"&&typeof e.helperVersion=="string"&&typeof e.protocolVersion=="number"&&Array.isArray(e.capabilities)&&typeof e.pid=="number"}function g(i){if(!i||typeof i!="object")return!1;const e=i;return typeof e.id=="string"&&typeof e.ok=="boolean"&&typeof e.latencyMs=="number"}export{k as WeChatChannelHelperClient};