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.
- package/dist/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,378 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};
|