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,357 +1,4 @@
1
- // @arch docs/features/manager-agent.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 DEFAULT_WS_URL = 'wss://openws.work.weixin.qq.com';
7
- const APP_CMD_SUBSCRIBE = 'aibot_subscribe';
8
- const APP_CMD_CALLBACK = 'aibot_msg_callback';
9
- const APP_CMD_LEGACY_CALLBACK = 'aibot_callback';
10
- const APP_CMD_SEND = 'aibot_send_msg';
11
- const APP_CMD_RESPONSE = 'aibot_respond_msg';
12
- const APP_CMD_PING = 'ping';
13
- const RECONNECT_BACKOFF_MS = [2_000, 5_000, 10_000, 30_000];
14
- const MAX_MESSAGE_LENGTH = 4_000;
15
- export class WeComChannelAdapter {
16
- onMessage;
17
- type = 'wecom';
18
- secrets = new ChannelSecretRegistry();
19
- connections = new Map();
20
- constructor(onMessage) {
21
- this.onMessage = onMessage;
22
- }
23
- async connect(config) {
24
- if (!config.enabled)
25
- return;
26
- const conn = this.ensureConnection(config);
27
- if (conn.connecting)
28
- return conn.connecting;
29
- conn.stopped = false;
30
- conn.config = config;
31
- conn.connecting = this.openConnection(conn).finally(() => {
32
- conn.connecting = null;
33
- });
34
- return conn.connecting;
35
- }
36
- async disconnect(config) {
37
- const conn = this.connections.get(config.id);
38
- if (!conn)
39
- return;
40
- conn.stopped = true;
41
- if (conn.reconnectTimer)
42
- clearTimeout(conn.reconnectTimer);
43
- if (conn.pingTimer)
44
- clearInterval(conn.pingTimer);
45
- conn.reconnectTimer = null;
46
- conn.pingTimer = null;
47
- for (const pending of conn.pending.values()) {
48
- clearTimeout(pending.timer);
49
- pending.reject(new Error('WeCom adapter disconnected'));
50
- }
51
- conn.pending.clear();
52
- const socket = conn.socket;
53
- conn.socket = null;
54
- if (socket)
55
- await new Promise((resolve) => {
56
- socket.once('close', () => resolve());
57
- socket.close();
58
- setTimeout(resolve, 500);
59
- });
60
- this.connections.delete(config.id);
61
- }
62
- async send(config, reply) {
63
- const conn = this.ensureConnection(config);
64
- await this.connect(config);
65
- const text = reply.text.trim();
66
- if (!text)
67
- throw new Error('Reply text is required');
68
- const replyReqId = reply.messageId ? conn.replyReqIds.get(reply.messageId) : undefined;
69
- const fallbackReplyReqId = conn.lastChatReqIds.get(reply.conversationId);
70
- const requestBody = replyReqId || fallbackReplyReqId
71
- ? {
72
- msg: {
73
- msgtype: 'markdown',
74
- markdown: { content: text.slice(0, MAX_MESSAGE_LENGTH) },
75
- },
76
- req_id: (replyReqId || fallbackReplyReqId),
77
- }
78
- : {
79
- chatid: reply.conversationId,
80
- msgtype: 'markdown',
81
- markdown: { content: text.slice(0, MAX_MESSAGE_LENGTH) },
82
- };
83
- const cmd = replyReqId || fallbackReplyReqId ? APP_CMD_RESPONSE : APP_CMD_SEND;
84
- const response = await this.sendRequest(conn, cmd, requestBody);
85
- this.raiseForWeComError(response, 'send message');
86
- }
87
- async health(config) {
88
- const secret = this.readSecret(config);
89
- if (!secret?.botId || !secret.secret) {
90
- return { ok: false, message: 'WeCom adapter requires local credentials' };
91
- }
92
- const conn = this.connections.get(config.id);
93
- return conn?.socket?.readyState === WebSocket.OPEN
94
- ? { ok: true }
95
- : { ok: false, message: 'WeCom websocket disconnected' };
96
- }
97
- ensureConnection(config) {
98
- let conn = this.connections.get(config.id);
99
- if (!conn) {
100
- conn = {
101
- config,
102
- socket: null,
103
- connecting: null,
104
- deviceId: randomUUID().replace(/-/g, ''),
105
- stopped: false,
106
- reconnectAttempt: 0,
107
- reconnectTimer: null,
108
- pingTimer: null,
109
- pending: new Map(),
110
- replyReqIds: new Map(),
111
- lastChatReqIds: new Map(),
112
- dedup: new Set(),
113
- dedupQueue: [],
114
- seenContextLineKeys: new Set(),
115
- seenContextLineQueue: [],
116
- };
117
- this.connections.set(config.id, conn);
118
- }
119
- conn.config = config;
120
- return conn;
121
- }
122
- readSecret(config) {
123
- const secret = this.secrets.get(config.secretRef);
124
- if (!secret || secret.type !== 'wecom' || !secret.botId || !secret.secret) {
125
- throw new Error('WeCom outbound adapter is not configured on this daemon');
126
- }
127
- return secret;
128
- }
129
- async openConnection(conn) {
130
- const secret = this.readSecret(conn.config);
131
- const socket = new WebSocket(DEFAULT_WS_URL);
132
- conn.socket = socket;
133
- await new Promise((resolve, reject) => {
134
- const cleanup = () => {
135
- socket.off('open', handleOpen);
136
- socket.off('error', handleError);
137
- };
138
- const handleOpen = () => {
139
- cleanup();
140
- resolve();
141
- };
142
- const handleError = (error) => {
143
- cleanup();
144
- reject(error);
145
- };
146
- socket.once('open', handleOpen);
147
- socket.once('error', handleError);
148
- });
149
- socket.on('message', (data) => {
150
- const payload = this.parsePayload(data.toString());
151
- if (!payload)
152
- return;
153
- this.handlePayload(conn, payload);
154
- });
155
- socket.on('close', () => this.scheduleReconnect(conn));
156
- socket.on('error', () => this.scheduleReconnect(conn));
157
- const response = await this.sendRequest(conn, APP_CMD_SUBSCRIBE, {
158
- bot_id: secret.botId,
159
- secret: secret.secret,
160
- device_id: conn.deviceId,
161
- }, 20_000);
162
- this.raiseForWeComError(response, 'subscribe');
163
- conn.reconnectAttempt = 0;
164
- if (conn.pingTimer)
165
- clearInterval(conn.pingTimer);
166
- conn.pingTimer = setInterval(() => {
167
- void this.sendJson(conn, { cmd: APP_CMD_PING, headers: { req_id: this.newReqId('ping') }, body: {} }).catch(() => { });
168
- }, 30_000);
169
- conn.pingTimer.unref();
170
- }
171
- scheduleReconnect(conn) {
172
- if (conn.stopped)
173
- return;
174
- if (conn.reconnectTimer || conn.connecting)
175
- return;
176
- const delay = RECONNECT_BACKOFF_MS[Math.min(conn.reconnectAttempt, RECONNECT_BACKOFF_MS.length - 1)];
177
- conn.reconnectAttempt += 1;
178
- conn.reconnectTimer = setTimeout(() => {
179
- conn.reconnectTimer = null;
180
- void this.connect(conn.config).catch(() => { });
181
- }, delay);
182
- conn.reconnectTimer.unref();
183
- }
184
- async sendJson(conn, payload) {
185
- if (!conn.socket || conn.socket.readyState !== WebSocket.OPEN) {
186
- throw new Error('WeCom websocket is not connected');
187
- }
188
- conn.socket.send(JSON.stringify(payload));
189
- }
190
- async sendRequest(conn, cmd, body, timeoutMs = 15_000) {
191
- const reqId = this.newReqId(cmd);
192
- return new Promise((resolve, reject) => {
193
- const timer = setTimeout(() => {
194
- conn.pending.delete(reqId);
195
- reject(new Error(`WeCom request timed out: ${cmd}`));
196
- }, timeoutMs);
197
- timer.unref();
198
- conn.pending.set(reqId, { resolve, reject, timer });
199
- void this.sendJson(conn, { cmd, headers: { req_id: reqId }, body }).catch((error) => {
200
- clearTimeout(timer);
201
- conn.pending.delete(reqId);
202
- reject(error instanceof Error ? error : new Error(String(error)));
203
- });
204
- });
205
- }
206
- handlePayload(conn, payload) {
207
- const reqId = this.payloadReqId(payload);
208
- const cmd = String(payload.cmd || '');
209
- if (reqId && conn.pending.has(reqId) && !this.isCallbackCommand(cmd)) {
210
- const pending = conn.pending.get(reqId);
211
- clearTimeout(pending.timer);
212
- conn.pending.delete(reqId);
213
- pending.resolve(payload);
214
- return;
215
- }
216
- if (!this.isCallbackCommand(cmd))
217
- return;
218
- const body = this.asRecord(payload.body);
219
- if (!body)
220
- return;
221
- const messageId = String(body.msgid || reqId || randomUUID());
222
- if (this.isDuplicate(conn, messageId))
223
- return;
224
- if (reqId)
225
- conn.replyReqIds.set(messageId, reqId);
226
- const sender = this.asRecord(body.from) ?? {};
227
- const senderId = String(sender.userid || '');
228
- const senderName = String(sender.name || sender.userid || '');
229
- const conversationId = String(body.chatid || senderId).trim();
230
- if (!conversationId)
231
- return;
232
- if (reqId)
233
- conn.lastChatReqIds.set(conversationId, reqId);
234
- const text = this.filterContextText(conn, conversationId, this.extractText(body).trim());
235
- if (!text)
236
- return;
237
- this.onMessage?.({
238
- managerSessionId: conn.config.managerSessionId,
239
- channelId: conn.config.id,
240
- channelType: 'wecom',
241
- conversationId,
242
- messageId,
243
- sender: { id: senderId || conversationId, name: senderName || senderId || conversationId },
244
- text,
245
- attachments: [],
246
- receivedAt: new Date().toISOString(),
247
- replyTarget: '',
248
- });
249
- }
250
- isDuplicate(conn, messageId) {
251
- if (conn.dedup.has(messageId))
252
- return true;
253
- conn.dedup.add(messageId);
254
- conn.dedupQueue.push(messageId);
255
- if (conn.dedupQueue.length > 1_000) {
256
- const removed = conn.dedupQueue.shift();
257
- if (removed)
258
- conn.dedup.delete(removed);
259
- }
260
- return false;
261
- }
262
- extractText(body) {
263
- const msgtype = String(body.msgtype || '').toLowerCase();
264
- if (msgtype === 'mixed') {
265
- const mixed = this.asRecord(body.mixed);
266
- const items = Array.isArray(mixed?.msg_item) ? mixed?.msg_item : [];
267
- return items
268
- .map((item) => {
269
- const block = this.asRecord(item);
270
- if (String(block?.msgtype || '').toLowerCase() !== 'text')
271
- return '';
272
- return String(this.asRecord(block?.text)?.content || '').trim();
273
- })
274
- .filter(Boolean)
275
- .join('\n')
276
- .trim();
277
- }
278
- const text = String(this.asRecord(body.text)?.content || '').trim();
279
- if (text)
280
- return text;
281
- if (msgtype === 'voice')
282
- return String(this.asRecord(body.voice)?.content || '').trim();
283
- if (msgtype === 'appmsg')
284
- return String(this.asRecord(body.appmsg)?.title || '').trim();
285
- return '';
286
- }
287
- filterContextText(conn, conversationId, text) {
288
- if (!text.includes('外部企业微信群消息上下文'))
289
- return text;
290
- const lines = text.split(/\r?\n/);
291
- const headerLines = [];
292
- const newMessageLines = [];
293
- let sawContextLine = false;
294
- for (const line of lines) {
295
- const normalized = this.normalizeContextLine(line);
296
- if (!normalized) {
297
- if (!sawContextLine)
298
- headerLines.push(line);
299
- continue;
300
- }
301
- sawContextLine = true;
302
- const key = `${conversationId}\n${normalized}`;
303
- if (conn.seenContextLineKeys.has(key))
304
- continue;
305
- this.rememberContextLine(conn, key);
306
- newMessageLines.push(line);
307
- }
308
- if (!sawContextLine)
309
- return text;
310
- if (!newMessageLines.length)
311
- return '';
312
- return [...headerLines, ...newMessageLines].filter((line) => line.trim()).join('\n').trim();
313
- }
314
- normalizeContextLine(line) {
315
- const trimmed = line.trim().replace(/^=>\s*/, '');
316
- return /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+.+/.test(trimmed) ? trimmed : null;
317
- }
318
- rememberContextLine(conn, key) {
319
- conn.seenContextLineKeys.add(key);
320
- conn.seenContextLineQueue.push(key);
321
- while (conn.seenContextLineQueue.length > 2_000) {
322
- const removed = conn.seenContextLineQueue.shift();
323
- if (removed)
324
- conn.seenContextLineKeys.delete(removed);
325
- }
326
- }
327
- raiseForWeComError(payload, operation) {
328
- const errcode = typeof payload.errcode === 'number' ? payload.errcode : Number(payload.errcode || 0);
329
- if (!Number.isFinite(errcode) || errcode === 0)
330
- return;
331
- const errmsg = String(payload.errmsg || 'unknown error');
332
- throw new Error(`WeCom ${operation} failed: ${errmsg} (${errcode})`);
333
- }
334
- parsePayload(raw) {
335
- try {
336
- const parsed = JSON.parse(raw);
337
- return parsed && typeof parsed === 'object' ? parsed : null;
338
- }
339
- catch {
340
- return null;
341
- }
342
- }
343
- payloadReqId(payload) {
344
- const headers = this.asRecord(payload.headers);
345
- const reqId = headers?.req_id;
346
- return typeof reqId === 'string' && reqId ? reqId : null;
347
- }
348
- asRecord(value) {
349
- return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
350
- }
351
- isCallbackCommand(cmd) {
352
- return cmd === APP_CMD_CALLBACK || cmd === APP_CMD_LEGACY_CALLBACK;
353
- }
354
- newReqId(prefix) {
355
- return `${prefix}_${randomUUID().replace(/-/g, '')}`;
356
- }
357
- }
1
+ import{randomUUID as m}from"node:crypto";import u from"ws";import{ChannelSecretRegistry as f}from"./secret-registry.js";const C="wss://openws.work.weixin.qq.com",w="aibot_subscribe",_="aibot_msg_callback",y="aibot_callback",S="aibot_send_msg",R="aibot_respond_msg",I="ping",h=[2e3,5e3,1e4,3e4],g=4e3;class q{onMessage;type="wecom";secrets=new f;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 r of t.pending.values())clearTimeout(r.timer),r.reject(new Error("WeCom adapter disconnected"));t.pending.clear();const n=t.socket;t.socket=null,n&&await new Promise(r=>{n.once("close",()=>r()),n.close(),setTimeout(r,500)}),this.connections.delete(e.id)}async send(e,t){const n=this.ensureConnection(e);await this.connect(e);const r=t.text.trim();if(!r)throw new Error("Reply text is required");const s=t.messageId?n.replyReqIds.get(t.messageId):void 0,i=n.lastChatReqIds.get(t.conversationId),o=s||i?{msg:{msgtype:"markdown",markdown:{content:r.slice(0,g)}},req_id:s||i}:{chatid:t.conversationId,msgtype:"markdown",markdown:{content:r.slice(0,g)}},c=s||i?R:S,d=await this.sendRequest(n,c,o);this.raiseForWeComError(d,"send message")}async health(e){const t=this.readSecret(e);return!t?.botId||!t.secret?{ok:!1,message:"WeCom adapter requires local credentials"}:this.connections.get(e.id)?.socket?.readyState===u.OPEN?{ok:!0}:{ok:!1,message:"WeCom websocket disconnected"}}ensureConnection(e){let t=this.connections.get(e.id);return t||(t={config:e,socket:null,connecting:null,deviceId:m().replace(/-/g,""),stopped:!1,reconnectAttempt:0,reconnectTimer:null,pingTimer:null,pending:new Map,replyReqIds:new Map,lastChatReqIds:new Map,dedup:new Set,dedupQueue:[],seenContextLineKeys:new Set,seenContextLineQueue:[]},this.connections.set(e.id,t)),t.config=e,t}readSecret(e){const t=this.secrets.get(e.secretRef);if(!t||t.type!=="wecom"||!t.botId||!t.secret)throw new Error("WeCom outbound adapter is not configured on this daemon");return t}async openConnection(e){const t=this.readSecret(e.config),n=new u(C);e.socket=n,await new Promise((s,i)=>{const o=()=>{n.off("open",c),n.off("error",d)},c=()=>{o(),s()},d=a=>{o(),i(a)};n.once("open",c),n.once("error",d)}),n.on("message",s=>{const i=this.parsePayload(s.toString());i&&this.handlePayload(e,i)}),n.on("close",()=>this.scheduleReconnect(e)),n.on("error",()=>this.scheduleReconnect(e));const r=await this.sendRequest(e,w,{bot_id:t.botId,secret:t.secret,device_id:e.deviceId},2e4);this.raiseForWeComError(r,"subscribe"),e.reconnectAttempt=0,e.pingTimer&&clearInterval(e.pingTimer),e.pingTimer=setInterval(()=>{this.sendJson(e,{cmd:I,headers:{req_id:this.newReqId("ping")},body:{}}).catch(()=>{})},3e4),e.pingTimer.unref()}scheduleReconnect(e){if(e.stopped||e.reconnectTimer||e.connecting)return;const t=h[Math.min(e.reconnectAttempt,h.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("WeCom websocket is not connected");e.socket.send(JSON.stringify(t))}async sendRequest(e,t,n,r=15e3){const s=this.newReqId(t);return new Promise((i,o)=>{const c=setTimeout(()=>{e.pending.delete(s),o(new Error(`WeCom request timed out: ${t}`))},r);c.unref(),e.pending.set(s,{resolve:i,reject:o,timer:c}),this.sendJson(e,{cmd:t,headers:{req_id:s},body:n}).catch(d=>{clearTimeout(c),e.pending.delete(s),o(d instanceof Error?d:new Error(String(d)))})})}handlePayload(e,t){const n=this.payloadReqId(t),r=String(t.cmd||"");if(n&&e.pending.has(n)&&!this.isCallbackCommand(r)){const p=e.pending.get(n);clearTimeout(p.timer),e.pending.delete(n),p.resolve(t);return}if(!this.isCallbackCommand(r))return;const s=this.asRecord(t.body);if(!s)return;const i=String(s.msgid||n||m());if(this.isDuplicate(e,i))return;n&&e.replyReqIds.set(i,n);const o=this.asRecord(s.from)??{},c=String(o.userid||""),d=String(o.name||o.userid||""),a=String(s.chatid||c).trim();if(!a)return;n&&e.lastChatReqIds.set(a,n);const l=this.filterContextText(e,a,this.extractText(s).trim());l&&this.onMessage?.({managerSessionId:e.config.managerSessionId,channelId:e.config.id,channelType:"wecom",conversationId:a,messageId:i,sender:{id:c||a,name:d||c||a},text:l,attachments:[],receivedAt:new Date().toISOString(),replyTarget:""})}isDuplicate(e,t){if(e.dedup.has(t))return!0;if(e.dedup.add(t),e.dedupQueue.push(t),e.dedupQueue.length>1e3){const n=e.dedupQueue.shift();n&&e.dedup.delete(n)}return!1}extractText(e){const t=String(e.msgtype||"").toLowerCase();if(t==="mixed"){const r=this.asRecord(e.mixed);return(Array.isArray(r?.msg_item)?r?.msg_item:[]).map(i=>{const o=this.asRecord(i);return String(o?.msgtype||"").toLowerCase()!=="text"?"":String(this.asRecord(o?.text)?.content||"").trim()}).filter(Boolean).join(`
2
+ `).trim()}const n=String(this.asRecord(e.text)?.content||"").trim();return n||(t==="voice"?String(this.asRecord(e.voice)?.content||"").trim():t==="appmsg"?String(this.asRecord(e.appmsg)?.title||"").trim():"")}filterContextText(e,t,n){if(!n.includes("\u5916\u90E8\u4F01\u4E1A\u5FAE\u4FE1\u7FA4\u6D88\u606F\u4E0A\u4E0B\u6587"))return n;const r=n.split(/\r?\n/),s=[],i=[];let o=!1;for(const c of r){const d=this.normalizeContextLine(c);if(!d){o||s.push(c);continue}o=!0;const a=`${t}
3
+ ${d}`;e.seenContextLineKeys.has(a)||(this.rememberContextLine(e,a),i.push(c))}return o?i.length?[...s,...i].filter(c=>c.trim()).join(`
4
+ `).trim():"":n}normalizeContextLine(e){const t=e.trim().replace(/^=>\s*/,"");return/^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s+.+/.test(t)?t:null}rememberContextLine(e,t){for(e.seenContextLineKeys.add(t),e.seenContextLineQueue.push(t);e.seenContextLineQueue.length>2e3;){const n=e.seenContextLineQueue.shift();n&&e.seenContextLineKeys.delete(n)}}raiseForWeComError(e,t){const n=typeof e.errcode=="number"?e.errcode:Number(e.errcode||0);if(!Number.isFinite(n)||n===0)return;const r=String(e.errmsg||"unknown error");throw new Error(`WeCom ${t} failed: ${r} (${n})`)}parsePayload(e){try{const t=JSON.parse(e);return t&&typeof t=="object"?t:null}catch{return null}}payloadReqId(e){const n=this.asRecord(e.headers)?.req_id;return typeof n=="string"&&n?n:null}asRecord(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:null}isCallbackCommand(e){return e===_||e===y}newReqId(e){return`${e}_${m().replace(/-/g,"")}`}}export{q as WeComChannelAdapter};
@@ -1,131 +1,6 @@
1
- import chalk from 'chalk';
2
- import { loadConfig, saveConfig } from '../config/index.js';
3
- import { spawnCommandString } from '../agents/command-spec.js';
4
- function getCapsModels(caps) {
5
- if (caps.models?.length)
6
- return caps.models;
7
- if (caps.model)
8
- return [caps.model];
9
- return [];
10
- }
11
- /**
12
- * Runs `<command> /caps` and returns the parsed caps JSON.
13
- */
14
- async function probeCaps(command) {
15
- return new Promise((resolve, reject) => {
16
- const proc = spawnCommandString(command, ['/caps'], { stdio: ['ignore', 'pipe', 'pipe'] });
17
- let stdout = '';
18
- let stderr = '';
19
- proc.stdout?.on('data', (d) => { stdout += d.toString(); });
20
- proc.stderr?.on('data', (d) => { stderr += d.toString(); });
21
- proc.on('error', (err) => reject(new Error(`Failed to run command: ${err.message}`)));
22
- proc.on('close', (_code) => {
23
- const line = stdout.trim().split('\n').find((l) => l.trim().startsWith('{'));
24
- if (!line) {
25
- reject(new Error(`Command did not output valid caps JSON.\nstdout: ${stdout.trim()}\nstderr: ${stderr.trim()}`));
26
- return;
27
- }
28
- try {
29
- const caps = JSON.parse(line);
30
- if (!caps.name || !caps.mode) {
31
- reject(new Error('Caps JSON must contain "name" and "mode" fields'));
32
- return;
33
- }
34
- if (getCapsModels(caps).length === 0) {
35
- reject(new Error('Caps JSON must contain either "model" or a non-empty "models" array'));
36
- return;
37
- }
38
- if (caps.mode !== 'spawn' && caps.mode !== 'stdio') {
39
- reject(new Error(`Caps "mode" must be "spawn" or "stdio", got: ${caps.mode}`));
40
- return;
41
- }
42
- resolve(caps);
43
- }
44
- catch {
45
- reject(new Error(`Failed to parse caps JSON: ${line}`));
46
- }
47
- });
48
- setTimeout(() => {
49
- proc.kill('SIGTERM');
50
- reject(new Error('Timed out waiting for caps response (5s)'));
51
- }, 5000);
52
- });
53
- }
54
- export function registerAgentCommand(program) {
55
- const agent = program
56
- .command('agent')
57
- .description('Manage custom agents');
58
- // ── add ──────────────────────────────────────────────────────────────────
59
- agent
60
- .command('add <name>')
61
- .description('Register a custom agent')
62
- .requiredOption('--command <cmd>', 'Command to run the agent (e.g. "python /path/agent.py")')
63
- .action(async (name, opts) => {
64
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
65
- console.error(chalk.red('✗ Agent name must contain only letters, numbers, hyphens, and underscores'));
66
- process.exit(1);
67
- }
68
- console.log(chalk.gray(`Probing caps: ${opts.command} /caps ...`));
69
- let caps;
70
- try {
71
- caps = await probeCaps(opts.command);
72
- }
73
- catch (err) {
74
- console.error(chalk.red(`✗ ${err instanceof Error ? err.message : String(err)}`));
75
- process.exit(1);
76
- }
77
- const config = loadConfig();
78
- config.customAgents = config.customAgents ?? {};
79
- config.customAgents[name] = { command: opts.command, caps };
80
- saveConfig(config);
81
- console.log(chalk.green(`✓ Agent "${name}" registered`));
82
- console.log(chalk.gray(` Name: ${caps.name}`));
83
- console.log(chalk.gray(` Models: ${getCapsModels(caps).join(', ')}`));
84
- console.log(chalk.gray(` Mode: ${caps.mode}`));
85
- if (caps.version)
86
- console.log(chalk.gray(` Version: ${caps.version}`));
87
- console.log();
88
- console.log(chalk.yellow(` The agent will appear as "custom:${name}" in the Shennian app.`));
89
- console.log(chalk.yellow(' Refresh Agents in the app to hot-reload this daemon, or restart the daemon as a fallback.'));
90
- });
91
- // ── list ─────────────────────────────────────────────────────────────────
92
- agent
93
- .command('list')
94
- .description('List registered custom agents')
95
- .action(() => {
96
- const config = loadConfig();
97
- const entries = Object.entries(config.customAgents ?? {});
98
- if (entries.length === 0) {
99
- console.log(chalk.gray('No custom agents registered.'));
100
- console.log(chalk.gray('Add one with: shennian agent add <name> --command "<cmd>"'));
101
- return;
102
- }
103
- console.log(chalk.bold('\nCustom agents:\n'));
104
- for (const [name, entry] of entries) {
105
- const models = entry.caps.models?.length
106
- ? entry.caps.models.join(', ')
107
- : entry.caps.model ?? '(none)';
108
- console.log(` ${chalk.cyan(name)}`);
109
- console.log(` ${chalk.gray('display:')} ${entry.caps.name}`);
110
- console.log(` ${chalk.gray('models:')} ${models}`);
111
- console.log(` ${chalk.gray('mode:')} ${entry.caps.mode}`);
112
- console.log(` ${chalk.gray('command:')} ${entry.command}`);
113
- console.log();
114
- }
115
- });
116
- // ── remove ────────────────────────────────────────────────────────────────
117
- agent
118
- .command('remove <name>')
119
- .description('Unregister a custom agent')
120
- .action((name) => {
121
- const config = loadConfig();
122
- if (!config.customAgents?.[name]) {
123
- console.error(chalk.red(`✗ No agent named "${name}"`));
124
- process.exit(1);
125
- }
126
- delete config.customAgents[name];
127
- saveConfig(config);
128
- console.log(chalk.green(`✓ Agent "${name}" removed`));
129
- console.log(chalk.yellow(' Refresh Agents in the app to hot-reload this daemon, or restart the daemon as a fallback.'));
130
- });
131
- }
1
+ import o from"chalk";import{loadConfig as d,saveConfig as l}from"../config/index.js";import{spawnCommandString as p}from"../agents/command-spec.js";function g(m){return m.models?.length?m.models:m.model?[m.model]:[]}async function u(m){return new Promise((c,e)=>{const n=p(m,["/caps"],{stdio:["ignore","pipe","pipe"]});let s="",t="";n.stdout?.on("data",r=>{s+=r.toString()}),n.stderr?.on("data",r=>{t+=r.toString()}),n.on("error",r=>e(new Error(`Failed to run command: ${r.message}`))),n.on("close",r=>{const i=s.trim().split(`
2
+ `).find(a=>a.trim().startsWith("{"));if(!i){e(new Error(`Command did not output valid caps JSON.
3
+ stdout: ${s.trim()}
4
+ stderr: ${t.trim()}`));return}try{const a=JSON.parse(i);if(!a.name||!a.mode){e(new Error('Caps JSON must contain "name" and "mode" fields'));return}if(g(a).length===0){e(new Error('Caps JSON must contain either "model" or a non-empty "models" array'));return}if(a.mode!=="spawn"&&a.mode!=="stdio"){e(new Error(`Caps "mode" must be "spawn" or "stdio", got: ${a.mode}`));return}c(a)}catch{e(new Error(`Failed to parse caps JSON: ${i}`))}}),setTimeout(()=>{n.kill("SIGTERM"),e(new Error("Timed out waiting for caps response (5s)"))},5e3)})}function $(m){const c=m.command("agent").description("Manage custom agents");c.command("add <name>").description("Register a custom agent").requiredOption("--command <cmd>",'Command to run the agent (e.g. "python /path/agent.py")').action(async(e,n)=>{/^[a-zA-Z0-9_-]+$/.test(e)||(console.error(o.red("\u2717 Agent name must contain only letters, numbers, hyphens, and underscores")),process.exit(1)),console.log(o.gray(`Probing caps: ${n.command} /caps ...`));let s;try{s=await u(n.command)}catch(r){console.error(o.red(`\u2717 ${r instanceof Error?r.message:String(r)}`)),process.exit(1)}const t=d();t.customAgents=t.customAgents??{},t.customAgents[e]={command:n.command,caps:s},l(t),console.log(o.green(`\u2713 Agent "${e}" registered`)),console.log(o.gray(` Name: ${s.name}`)),console.log(o.gray(` Models: ${g(s).join(", ")}`)),console.log(o.gray(` Mode: ${s.mode}`)),s.version&&console.log(o.gray(` Version: ${s.version}`)),console.log(),console.log(o.yellow(` The agent will appear as "custom:${e}" in the Shennian app.`)),console.log(o.yellow(" Refresh Agents in the app to hot-reload this daemon, or restart the daemon as a fallback."))}),c.command("list").description("List registered custom agents").action(()=>{const e=d(),n=Object.entries(e.customAgents??{});if(n.length===0){console.log(o.gray("No custom agents registered.")),console.log(o.gray('Add one with: shennian agent add <name> --command "<cmd>"'));return}console.log(o.bold(`
5
+ Custom agents:
6
+ `));for(const[s,t]of n){const r=t.caps.models?.length?t.caps.models.join(", "):t.caps.model??"(none)";console.log(` ${o.cyan(s)}`),console.log(` ${o.gray("display:")} ${t.caps.name}`),console.log(` ${o.gray("models:")} ${r}`),console.log(` ${o.gray("mode:")} ${t.caps.mode}`),console.log(` ${o.gray("command:")} ${t.command}`),console.log()}}),c.command("remove <name>").description("Unregister a custom agent").action(e=>{const n=d();n.customAgents?.[e]||(console.error(o.red(`\u2717 No agent named "${e}"`)),process.exit(1)),delete n.customAgents[e],l(n),console.log(o.green(`\u2713 Agent "${e}" removed`)),console.log(o.yellow(" Refresh Agents in the app to hot-reload this daemon, or restart the daemon as a fallback."))})}export{$ as registerAgentCommand};
@@ -1,45 +1,6 @@
1
- // @arch docs/architecture/cli/daemon.md#守护进程管理
2
- // @test src/__tests__/daemon-autostart.test.ts
3
- import path from 'node:path';
4
- export function escapeXml(text) {
5
- return text
6
- .replace(/&/g, '&amp;')
7
- .replace(/</g, '&lt;')
8
- .replace(/>/g, '&gt;')
9
- .replace(/"/g, '&quot;')
10
- .replace(/'/g, '&apos;');
11
- }
12
- export function quoteSystemdArg(text) {
13
- return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
14
- }
15
- export function quoteCmdArg(text) {
16
- return `"${text.replace(/"/g, '""')}"`;
17
- }
18
- function escapeVbsString(text) {
19
- return text.replace(/"/g, '""');
20
- }
21
- function escapeXmlValue(text) {
22
- return escapeXml(text);
23
- }
24
- export function isWindowsCmdScript(filePath) {
25
- const ext = path.extname(filePath).toLowerCase();
26
- return ext === '.cmd' || ext === '.bat';
27
- }
28
- export function buildWindowsLauncherCommand(spec, logFile) {
29
- const commandLine = [spec.command, ...spec.args].map(quoteCmdArg).join(' ');
30
- const invocation = isWindowsCmdScript(spec.command) ? `call ${commandLine}` : commandLine;
31
- return ['@echo off', 'setlocal', `${invocation} >> ${quoteCmdArg(logFile)} 2>&1`, ''].join('\r\n');
32
- }
33
- export function buildWindowsStartupVbs(cmdPath) {
34
- return [
35
- 'Set shell = CreateObject("WScript.Shell")',
36
- `shell.Run Chr(34) & "${escapeVbsString(cmdPath)}" & Chr(34), 0, False`,
37
- '',
38
- ].join('\r\n');
39
- }
40
- export function buildWindowsScheduledTaskXml(input) {
41
- const joinedArgs = input.arguments.join(' ');
42
- return `<?xml version="1.0" encoding="UTF-16"?>
1
+ import i from"node:path";function a(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")}function d(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function o(e){return`"${e.replace(/"/g,'""')}"`}function l(e){return e.replace(/"/g,'""')}function n(e){return a(e)}function s(e){const t=i.extname(e).toLowerCase();return t===".cmd"||t===".bat"}function m(e,t){const r=[e.command,...e.args].map(o).join(" ");return["@echo off","setlocal",`${s(e.command)?`call ${r}`:r} >> ${o(t)} 2>&1`,""].join(`\r
2
+ `)}function g(e){return['Set shell = CreateObject("WScript.Shell")',`shell.Run Chr(34) & "${l(e)}" & Chr(34), 0, False`,""].join(`\r
3
+ `)}function p(e){const t=e.arguments.join(" ");return`<?xml version="1.0" encoding="UTF-16"?>
43
4
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
44
5
  <RegistrationInfo>
45
6
  <Description>Shennian Agent Daemon</Description>
@@ -47,7 +8,7 @@ export function buildWindowsScheduledTaskXml(input) {
47
8
  <Triggers>
48
9
  <LogonTrigger>
49
10
  <Enabled>true</Enabled>
50
- <UserId>${escapeXmlValue(input.userId)}</UserId>
11
+ <UserId>${n(e.userId)}</UserId>
51
12
  </LogonTrigger>
52
13
  <EventTrigger>
53
14
  <Enabled>true</Enabled>
@@ -61,7 +22,7 @@ export function buildWindowsScheduledTaskXml(input) {
61
22
  </Triggers>
62
23
  <Principals>
63
24
  <Principal id="Author">
64
- <UserId>${escapeXmlValue(input.userId)}</UserId>
25
+ <UserId>${n(e.userId)}</UserId>
65
26
  <LogonType>InteractiveToken</LogonType>
66
27
  <RunLevel>LeastPrivilege</RunLevel>
67
28
  </Principal>
@@ -91,9 +52,8 @@ export function buildWindowsScheduledTaskXml(input) {
91
52
  </Settings>
92
53
  <Actions Context="Author">
93
54
  <Exec>
94
- <Command>${escapeXmlValue(input.command)}</Command>
95
- <Arguments>${escapeXmlValue(joinedArgs)}</Arguments>
55
+ <Command>${n(e.command)}</Command>
56
+ <Arguments>${n(t)}</Arguments>
96
57
  </Exec>
97
58
  </Actions>
98
- </Task>`;
99
- }
59
+ </Task>`}export{m as buildWindowsLauncherCommand,p as buildWindowsScheduledTaskXml,g as buildWindowsStartupVbs,a as escapeXml,s as isWindowsCmdScript,o as quoteCmdArg,d as quoteSystemdArg};