shennian 0.2.89 → 0.2.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +13 -4
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.js +1 -19
  8. package/dist/src/agents/claude.js +8 -305
  9. package/dist/src/agents/codex-control.js +2 -188
  10. package/dist/src/agents/codex-utils.js +7 -200
  11. package/dist/src/agents/codex.js +15 -916
  12. package/dist/src/agents/command-spec.js +2 -413
  13. package/dist/src/agents/config-status.js +1 -226
  14. package/dist/src/agents/cursor.js +1 -249
  15. package/dist/src/agents/custom.js +4 -271
  16. package/dist/src/agents/detect.js +1 -56
  17. package/dist/src/agents/external-channel-instructions.js +10 -94
  18. package/dist/src/agents/gemini.js +1 -173
  19. package/dist/src/agents/manager.js +13 -157
  20. package/dist/src/agents/model-registry/cache.js +1 -37
  21. package/dist/src/agents/model-registry/discovery.js +2 -187
  22. package/dist/src/agents/model-registry/parsers.js +4 -447
  23. package/dist/src/agents/model-registry/runner.js +1 -30
  24. package/dist/src/agents/model-registry/service.js +1 -78
  25. package/dist/src/agents/model-registry/types.js +1 -8
  26. package/dist/src/agents/model-registry.js +1 -18
  27. package/dist/src/agents/openclaw.js +2 -275
  28. package/dist/src/agents/opencode.js +1 -231
  29. package/dist/src/agents/pi-context.js +12 -217
  30. package/dist/src/agents/pi.js +14 -723
  31. package/dist/src/agents/platform-instructions.js +9 -54
  32. package/dist/src/channels/base.js +1 -3
  33. package/dist/src/channels/registry.js +1 -30
  34. package/dist/src/channels/reply-split.js +10 -89
  35. package/dist/src/channels/runtime.js +5 -564
  36. package/dist/src/channels/secret-registry.js +1 -46
  37. package/dist/src/channels/websocket.js +8 -378
  38. package/dist/src/channels/wechat-channel/anchor.js +1 -65
  39. package/dist/src/channels/wechat-channel/client.js +1 -96
  40. package/dist/src/channels/wechat-channel/cooldown.js +1 -38
  41. package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
  42. package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
  43. package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
  44. package/dist/src/channels/wechat-channel/helper-client.js +3 -149
  45. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
  46. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
  47. package/dist/src/channels/wechat-channel/index.d.ts +1 -0
  48. package/dist/src/channels/wechat-channel/index.js +1 -19
  49. package/dist/src/channels/wechat-channel/ledger.js +1 -54
  50. package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
  51. package/dist/src/channels/wechat-channel/message-key.js +1 -105
  52. package/dist/src/channels/wechat-channel/observer.js +1 -118
  53. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
  54. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
  55. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  56. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  57. package/dist/src/channels/wechat-channel/preflight.js +1 -48
  58. package/dist/src/channels/wechat-channel/runner.js +1 -84
  59. package/dist/src/channels/wechat-channel/runtime.js +1 -66
  60. package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
  61. package/dist/src/channels/wechat-channel/scheduler.js +1 -152
  62. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  63. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  64. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  65. package/dist/src/channels/wechat-rpa.js +6 -1028
  66. package/dist/src/channels/wecom.js +4 -357
  67. package/dist/src/commands/agent.js +6 -131
  68. package/dist/src/commands/daemon-windows.js +8 -48
  69. package/dist/src/commands/daemon.js +19 -1013
  70. package/dist/src/commands/external-attachments.js +1 -51
  71. package/dist/src/commands/external.js +1 -137
  72. package/dist/src/commands/manager.js +2 -391
  73. package/dist/src/commands/pair-qr.js +1 -6
  74. package/dist/src/commands/pair.js +9 -287
  75. package/dist/src/commands/tools.js +1 -34
  76. package/dist/src/commands/upgrade.js +1 -198
  77. package/dist/src/config/index.js +1 -35
  78. package/dist/src/daemon-log.js +6 -58
  79. package/dist/src/env-path.js +1 -64
  80. package/dist/src/fs/boundary.js +1 -126
  81. package/dist/src/fs/handler.js +1 -130
  82. package/dist/src/fs/security.js +1 -32
  83. package/dist/src/fs/text-decoder.js +1 -110
  84. package/dist/src/index.js +2 -404
  85. package/dist/src/log-reporter.js +1 -16
  86. package/dist/src/manager/prompt.js +29 -34
  87. package/dist/src/manager/registry.js +2 -269
  88. package/dist/src/manager/runtime.js +19 -1007
  89. package/dist/src/native-fusion/config.js +1 -5
  90. package/dist/src/native-fusion/opencode-parser.js +3 -123
  91. package/dist/src/native-fusion/parser-common.js +8 -264
  92. package/dist/src/native-fusion/parsers.js +8 -729
  93. package/dist/src/native-fusion/service.js +2 -225
  94. package/dist/src/native-fusion/state.js +1 -22
  95. package/dist/src/native-fusion/types.js +1 -1
  96. package/dist/src/region.js +1 -88
  97. package/dist/src/relay/client.js +1 -343
  98. package/dist/src/session/archive-zip.js +1 -220
  99. package/dist/src/session/handlers/agent-config.js +1 -150
  100. package/dist/src/session/handlers/agents.js +1 -55
  101. package/dist/src/session/handlers/chat.js +2 -751
  102. package/dist/src/session/handlers/control.js +1 -55
  103. package/dist/src/session/handlers/fs.js +1 -783
  104. package/dist/src/session/handlers/session-refresh.js +1 -47
  105. package/dist/src/session/handlers/skills.js +1 -121
  106. package/dist/src/session/handlers/title.js +1 -60
  107. package/dist/src/session/handlers/tool-detail.js +1 -218
  108. package/dist/src/session/manager.js +1 -319
  109. package/dist/src/session/projection.js +1 -54
  110. package/dist/src/session/queue.js +4 -317
  111. package/dist/src/session/remote-attachments.js +1 -72
  112. package/dist/src/session/store.js +3 -109
  113. package/dist/src/session/types.js +1 -4
  114. package/dist/src/skills/registry.js +15 -148
  115. package/dist/src/skills/setup.js +1 -101
  116. package/dist/src/tools/markdown-to-pdf.js +10 -346
  117. package/dist/src/upgrade/engine.js +3 -347
  118. package/package.json +3 -2
@@ -1,564 +1,5 @@
1
- // @arch docs/features/manager-agent.md
2
- // @test src/__tests__/manager-runtime.test.ts
3
- import crypto from 'node:crypto';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { ChannelConfigRegistry } from './registry.js';
7
- import { ChannelSecretRegistry } from './secret-registry.js';
8
- import { WeComChannelAdapter } from './wecom.js';
9
- import { WeChatRpaChannelAdapter } from './wechat-rpa.js';
10
- import { ExternalWebSocketChannelAdapter } from './websocket.js';
11
- import { splitExternalReplyText } from './reply-split.js';
12
- export class ChannelRuntime {
13
- onExternalMessage;
14
- createReplyTarget;
15
- configs = new ChannelConfigRegistry();
16
- secrets = new ChannelSecretRegistry();
17
- adapters = new Map();
18
- completedReplyKeys = new Map();
19
- recentMessages = new Map();
20
- constructor(onExternalMessage, createReplyTarget) {
21
- this.onExternalMessage = onExternalMessage;
22
- this.createReplyTarget = createReplyTarget;
23
- const wecom = new WeComChannelAdapter((event) => this.ingest({ ...event, type: 'external.message' }));
24
- this.adapters.set(wecom.type, wecom);
25
- const websocket = new ExternalWebSocketChannelAdapter((event) => this.ingest({ ...event, type: 'external.message' }));
26
- this.adapters.set(websocket.type, websocket);
27
- const wechatRpa = new WeChatRpaChannelAdapter((event) => this.ingest(event));
28
- this.adapters.set(wechatRpa.type, wechatRpa);
29
- }
30
- async start() {
31
- for (const config of this.configs.list().filter((channel) => channel.enabled)) {
32
- await this.adapters.get(config.type)?.connect(config).catch(() => { });
33
- }
34
- }
35
- async stop() {
36
- for (const config of this.configs.list()) {
37
- await this.adapters.get(config.type)?.disconnect(config).catch(() => { });
38
- }
39
- }
40
- ingest(event) {
41
- const config = this.configs.get(event.channelId);
42
- const sessionId = event.managerSessionId ?? config?.sessionId ?? config?.managerSessionId;
43
- if (!sessionId)
44
- throw new Error(`No session bound for channel ${event.channelId}`);
45
- const replyTarget = event.replyTarget || this.createReplyTarget({
46
- managerSessionId: sessionId,
47
- channelId: event.channelId,
48
- conversationId: event.conversationId,
49
- messageId: event.messageId,
50
- });
51
- const normalized = { ...event, replyTarget };
52
- this.onExternalMessage(sessionId, normalized);
53
- this.recordRecentMessage(normalized);
54
- return normalized;
55
- }
56
- async reply(input) {
57
- const config = this.configs.get(input.channelId);
58
- if (!config)
59
- return { ok: false, error: `Unknown channel: ${input.channelId}` };
60
- if ((config.sessionId ?? config.managerSessionId) !== input.managerSessionId) {
61
- return { ok: false, error: 'Channel is not bound to this session' };
62
- }
63
- const adapter = this.adapters.get(config.type);
64
- if (!adapter)
65
- return { ok: false, error: `Unsupported channel type: ${config.type}` };
66
- try {
67
- const sends = planExternalReplySends(config.type, input);
68
- if (!sends.length)
69
- return { ok: false, error: 'Reply text or attachment is required' };
70
- let pending = false;
71
- for (const send of sends) {
72
- const idempotencyKey = send.idempotencyKey;
73
- if (idempotencyKey && this.isReplyCompleted(config, input.conversationId, idempotencyKey))
74
- continue;
75
- const result = await adapter.send(config, {
76
- ...input,
77
- ...send,
78
- });
79
- if (result?.status === 'queued') {
80
- pending = true;
81
- continue;
82
- }
83
- if (result?.status === 'manual-review') {
84
- return { ok: false, error: result.reason || 'Reply requires manual review before retry' };
85
- }
86
- if (idempotencyKey)
87
- this.markReplyCompleted(config, input.conversationId, idempotencyKey);
88
- }
89
- return pending ? { ok: true, pending: true } : { ok: true };
90
- }
91
- catch (err) {
92
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
93
- }
94
- }
95
- isReplyCompleted(config, conversationId, idempotencyKey) {
96
- const set = this.loadReplyCompletionSet(config);
97
- return set.has(replyCompletionKey(config.id, conversationId, idempotencyKey));
98
- }
99
- markReplyCompleted(config, conversationId, idempotencyKey) {
100
- const set = this.loadReplyCompletionSet(config);
101
- set.add(replyCompletionKey(config.id, conversationId, idempotencyKey));
102
- try {
103
- persistReplyCompletionSet(config.workDir, set);
104
- }
105
- catch {
106
- // Some tests and diagnostic channels use virtual workDirs. In-memory idempotency
107
- // still protects the current daemon; persistence resumes when workDir is writable.
108
- }
109
- }
110
- loadReplyCompletionSet(config) {
111
- const cacheKey = path.resolve(config.workDir);
112
- const cached = this.completedReplyKeys.get(cacheKey);
113
- if (cached)
114
- return cached;
115
- const set = readReplyCompletionSet(config.workDir);
116
- this.completedReplyKeys.set(cacheKey, set);
117
- return set;
118
- }
119
- async getDefaultReplyTarget(sessionId) {
120
- const config = this.configs.list().find((channel) => (channel.sessionId ?? channel.managerSessionId) === sessionId && channel.enabled);
121
- if (!config)
122
- throw new Error('No enabled external channel is bound to this session');
123
- const adapter = this.adapters.get(config.type);
124
- if (!adapter?.defaultConversation)
125
- throw new Error(`External channel ${config.type} has no default conversation`);
126
- const conversation = await adapter.defaultConversation(config);
127
- return {
128
- channelId: config.id,
129
- conversationId: conversation.conversationId,
130
- };
131
- }
132
- getManagerChannel(managerSessionId, type, opts = {}) {
133
- const configs = this.configs.list()
134
- .filter((channel) => (channel.sessionId ?? channel.managerSessionId) === managerSessionId && channel.type === type);
135
- const config = configs.find((channel) => channel.enabled) ?? configs.at(-1);
136
- if (!config)
137
- return null;
138
- const secret = this.secrets.get(config.secretRef);
139
- const adapterStatus = this.adapters.get(config.type)?.runtimeStatus?.(config) ?? {};
140
- return {
141
- id: config.id,
142
- type: config.type,
143
- name: config.name,
144
- sessionId: config.sessionId ?? config.managerSessionId,
145
- managerSessionId: config.managerSessionId,
146
- workDir: config.workDir,
147
- agentType: config.agentType,
148
- agentSessionId: config.agentSessionId,
149
- modelId: config.modelId,
150
- enabled: config.enabled,
151
- wsUrl: secret?.wsUrl ?? '',
152
- token: opts.includeSecret ? secret?.token ?? '' : '',
153
- tokenConfigured: Boolean(secret?.token),
154
- canReply: Boolean(secret?.canReply),
155
- systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
156
- ...wechatRpaViewFields(secret),
157
- ...adapterStatus,
158
- };
159
- }
160
- getChannelById(channelId, opts = {}) {
161
- const config = this.configs.get(channelId);
162
- if (!config)
163
- return null;
164
- const secret = this.secrets.get(config.secretRef);
165
- const adapterStatus = this.adapters.get(config.type)?.runtimeStatus?.(config) ?? {};
166
- return {
167
- id: config.id,
168
- type: config.type,
169
- name: config.name,
170
- sessionId: config.sessionId ?? config.managerSessionId,
171
- managerSessionId: config.managerSessionId,
172
- workDir: config.workDir,
173
- agentType: config.agentType,
174
- agentSessionId: config.agentSessionId,
175
- modelId: config.modelId,
176
- enabled: config.enabled,
177
- wsUrl: secret?.wsUrl ?? '',
178
- token: opts.includeSecret ? secret?.token ?? '' : '',
179
- tokenConfigured: Boolean(secret?.token),
180
- canReply: Boolean(secret?.canReply),
181
- systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
182
- ...wechatRpaViewFields(secret),
183
- ...adapterStatus,
184
- };
185
- }
186
- getChannelStatusById(channelId) {
187
- const config = this.configs.get(channelId);
188
- if (!config)
189
- return null;
190
- const secret = this.secrets.get(config.secretRef);
191
- const adapterStatus = this.adapters.get(config.type)?.runtimeStatus?.(config) ?? {};
192
- return {
193
- configured: true,
194
- connected: isChannelSecretConfigured(config, secret),
195
- type: config.type,
196
- channelId: config.id,
197
- name: config.name,
198
- canReply: Boolean(secret?.canReply),
199
- systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
200
- ...wechatRpaStatusFields(secret),
201
- ...adapterStatus,
202
- };
203
- }
204
- getManagerChannelStatus(managerSessionId) {
205
- const config = this.configs.list().find((channel) => (channel.sessionId ?? channel.managerSessionId) === managerSessionId && channel.enabled);
206
- if (!config)
207
- return null;
208
- const secret = this.secrets.get(config.secretRef);
209
- const adapterStatus = this.adapters.get(config.type)?.runtimeStatus?.(config) ?? {};
210
- return {
211
- configured: true,
212
- connected: isChannelSecretConfigured(config, secret),
213
- type: config.type,
214
- channelId: config.id,
215
- name: config.name,
216
- canReply: Boolean(secret?.canReply),
217
- systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
218
- ...wechatRpaStatusFields(secret),
219
- ...adapterStatus,
220
- };
221
- }
222
- listManagerChannelStatuses() {
223
- return this.configs.list()
224
- .filter((channel) => channel.enabled)
225
- .map((channel) => ({
226
- managerSessionId: channel.sessionId ?? channel.managerSessionId,
227
- status: this.getManagerChannelStatus(channel.sessionId ?? channel.managerSessionId),
228
- }))
229
- .filter((entry) => Boolean(entry.status));
230
- }
231
- listManagerExternalChannels(managerSessionId) {
232
- return this.configs.list()
233
- .filter((channel) => channel.enabled && (channel.sessionId ?? channel.managerSessionId) === managerSessionId)
234
- .map((channel) => {
235
- const secret = this.secrets.get(channel.secretRef);
236
- const adapterStatus = this.adapters.get(channel.type)?.runtimeStatus?.(channel) ?? {};
237
- return {
238
- configured: true,
239
- connected: isChannelSecretConfigured(channel, secret),
240
- type: channel.type,
241
- channelId: channel.id,
242
- name: channel.name,
243
- canReply: Boolean(secret?.canReply),
244
- systemPrompt: typeof secret?.systemPrompt === 'string' ? secret.systemPrompt : '',
245
- ...wechatRpaStatusFields(secret),
246
- ...adapterStatus,
247
- };
248
- });
249
- }
250
- async syncManagerWeChatRpaChannel(managerSessionId) {
251
- const config = this.configs.list()
252
- .find((channel) => channel.enabled && channel.type === 'wechat-rpa' && (channel.sessionId ?? channel.managerSessionId) === managerSessionId);
253
- if (!config)
254
- throw new Error('No enabled WeChat RPA channel is bound to this session');
255
- const adapter = this.adapters.get(config.type);
256
- if (!adapter?.syncNow)
257
- throw new Error('WeChat RPA channel does not support manual sync');
258
- const recentSince = Date.now() - 2 * 60 * 1000;
259
- const messages = mergeExternalMessages(await adapter.syncNow(config) ?? [], this.getRecentMessages(config.id, recentSince));
260
- return {
261
- channel: this.getManagerChannel(managerSessionId, 'wechat-rpa', { includeSecret: true }),
262
- messages,
263
- };
264
- }
265
- recordRecentMessage(event) {
266
- const current = this.recentMessages.get(event.channelId) ?? [];
267
- current.push(event);
268
- this.recentMessages.set(event.channelId, current.slice(-50));
269
- }
270
- getRecentMessages(channelId, sinceMs) {
271
- const messages = this.recentMessages.get(channelId) ?? [];
272
- return messages.filter((event) => {
273
- const receivedAt = Date.parse(event.receivedAt);
274
- return !Number.isFinite(receivedAt) || receivedAt >= sinceMs;
275
- });
276
- }
277
- async upsertManagerChannel(input) {
278
- const previous = this.configs.get(input.id);
279
- const allConfigs = this.configs.list();
280
- const boundSessionId = input.sessionId || input.managerSessionId;
281
- const nextConfig = {
282
- id: input.id,
283
- type: input.type,
284
- name: input.name?.trim() || previous?.name || '外部消息通道',
285
- sessionId: boundSessionId,
286
- managerSessionId: boundSessionId,
287
- workDir: input.workDir,
288
- agentType: input.agentType || previous?.agentType,
289
- agentSessionId: input.agentSessionId ?? previous?.agentSessionId ?? null,
290
- modelId: input.modelId ?? previous?.modelId ?? null,
291
- enabled: input.enabled,
292
- secretRef: previous?.secretRef || `channel:${input.id}`,
293
- };
294
- const priorSecret = this.secrets.get(nextConfig.secretRef);
295
- const wsUrl = input.wsUrl?.trim() || priorSecret?.wsUrl || '';
296
- const token = input.token?.trim() || priorSecret?.token || '';
297
- const canReply = input.canReply ?? priorSecret?.canReply ?? false;
298
- const systemPrompt = input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : '');
299
- if (nextConfig.enabled && (!wsUrl || !token)) {
300
- throw new Error('WebSocket 地址和 Token 必填');
301
- }
302
- const configs = allConfigs
303
- .filter((channel) => channel.id !== nextConfig.id)
304
- .map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === input.type
305
- ? { ...channel, enabled: false }
306
- : channel);
307
- configs.push(nextConfig);
308
- this.configs.replaceAll(configs);
309
- if (wsUrl || token) {
310
- this.secrets.upsert(nextConfig.secretRef, {
311
- type: 'websocket',
312
- wsUrl,
313
- token,
314
- canReply,
315
- systemPrompt,
316
- });
317
- }
318
- const adapter = this.adapters.get(nextConfig.type);
319
- for (const config of allConfigs) {
320
- if ((config.sessionId ?? config.managerSessionId) === boundSessionId && config.type === input.type && config.enabled) {
321
- await this.adapters.get(config.type)?.disconnect(config).catch(() => { });
322
- }
323
- }
324
- if (nextConfig.enabled) {
325
- void adapter?.connect(nextConfig).catch(() => { });
326
- }
327
- return this.getManagerChannel(boundSessionId, input.type, { includeSecret: true });
328
- }
329
- async upsertManagerWeChatRpaChannel(input) {
330
- const previous = this.configs.get(input.id);
331
- const allConfigs = this.configs.list();
332
- const boundSessionId = input.sessionId || input.managerSessionId;
333
- const groups = normalizeWeChatRpaGroups(input.groups);
334
- if (input.enabled && !groups.length)
335
- throw new Error('WeChat RPA 至少需要配置一个群');
336
- if (input.enabled && groups.length > 1)
337
- throw new Error('WeChat RPA 每个对话只能绑定一个群');
338
- const nextConfig = {
339
- id: input.id,
340
- type: 'wechat-rpa',
341
- name: input.name?.trim() || previous?.name || '本机微信 RPA',
342
- sessionId: boundSessionId,
343
- managerSessionId: boundSessionId,
344
- workDir: input.workDir,
345
- agentType: input.agentType || previous?.agentType,
346
- agentSessionId: input.agentSessionId ?? previous?.agentSessionId ?? null,
347
- modelId: input.modelId ?? previous?.modelId ?? null,
348
- enabled: input.enabled,
349
- secretRef: previous?.secretRef || `channel:${input.id}`,
350
- };
351
- const priorSecret = this.secrets.get(nextConfig.secretRef);
352
- const source = input.source || (priorSecret?.source === 'macos-probe' || priorSecret?.source === 'fixture-jsonl' || priorSecret?.source === 'macos-flow' || priorSecret?.source === 'windows-visual-flow' || priorSecret?.source === 'wechat-rpa-lab' ? priorSecret.source : defaultWeChatRpaSource());
353
- if (input.enabled && source === 'windows-visual-flow')
354
- throw new Error('个人微信通道当前仅支持 macOS');
355
- const configs = allConfigs
356
- .filter((channel) => channel.id !== nextConfig.id)
357
- .map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === 'wechat-rpa'
358
- ? { ...channel, enabled: false }
359
- : channel);
360
- configs.push(nextConfig);
361
- this.configs.replaceAll(configs);
362
- this.secrets.upsert(nextConfig.secretRef, {
363
- type: 'wechat-rpa',
364
- source,
365
- groups,
366
- pollIntervalMs: clampOptionalNumber(input.pollIntervalMs, priorSecret?.pollIntervalMs),
367
- recentLimit: clampOptionalNumber(input.recentLimit, priorSecret?.recentLimit),
368
- idleSeconds: clampOptionalNumber(input.idleSeconds, priorSecret?.idleSeconds),
369
- forceForeground: input.forceForeground ?? Boolean(priorSecret?.forceForeground),
370
- noRestore: input.noRestore ?? (priorSecret?.noRestore === undefined ? true : Boolean(priorSecret.noRestore)),
371
- downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
372
- downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
373
- selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
374
- privacyConsentAccepted: input.privacyConsentAccepted ?? Boolean(priorSecret?.privacyConsentAccepted),
375
- flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
376
- canReply: input.canReply ?? priorSecret?.canReply ?? false,
377
- systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
378
- });
379
- const adapter = this.adapters.get(nextConfig.type);
380
- for (const config of allConfigs) {
381
- if ((config.sessionId ?? config.managerSessionId) === boundSessionId && config.type === 'wechat-rpa' && config.enabled) {
382
- await this.adapters.get(config.type)?.disconnect(config).catch(() => { });
383
- }
384
- }
385
- if (nextConfig.enabled) {
386
- void adapter?.connect(nextConfig).catch(() => { });
387
- }
388
- return this.getManagerChannel(boundSessionId, 'wechat-rpa', { includeSecret: true });
389
- }
390
- }
391
- function isChannelSecretConfigured(config, secret) {
392
- if (!secret || secret.type !== config.type)
393
- return false;
394
- if (config.type === 'wechat-rpa')
395
- return true;
396
- if (config.type === 'websocket')
397
- return Boolean(secret.wsUrl && secret.token);
398
- if (config.type === 'wecom')
399
- return Boolean(secret.token || secret.botId || secret.secret);
400
- return Boolean(secret.token);
401
- }
402
- function wechatRpaViewFields(secret) {
403
- if (!secret || secret.type !== 'wechat-rpa')
404
- return {};
405
- return {
406
- wechatRpaSource: typeof secret.source === 'string' ? secret.source : '',
407
- wechatRpaGroups: normalizeWeChatRpaGroups(Array.isArray(secret.groups) ? secret.groups : []),
408
- pollIntervalMs: Number.isFinite(secret.pollIntervalMs) ? Number(secret.pollIntervalMs) : undefined,
409
- recentLimit: Number.isFinite(secret.recentLimit) ? Number(secret.recentLimit) : undefined,
410
- idleSeconds: Number.isFinite(secret.idleSeconds) ? Number(secret.idleSeconds) : undefined,
411
- forceForeground: Boolean(secret.forceForeground),
412
- noRestore: secret.noRestore === undefined ? undefined : Boolean(secret.noRestore),
413
- downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
414
- downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
415
- selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
416
- wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
417
- wechatRpaServerDecisionAvailable: true,
418
- wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
419
- };
420
- }
421
- function wechatRpaStatusFields(secret) {
422
- if (!secret || secret.type !== 'wechat-rpa')
423
- return {};
424
- return {
425
- wechatRpaSource: typeof secret.source === 'string' ? secret.source : null,
426
- wechatRpaGroups: normalizeWeChatRpaGroups(Array.isArray(secret.groups) ? secret.groups : []),
427
- pollIntervalMs: Number.isFinite(secret.pollIntervalMs) ? Number(secret.pollIntervalMs) : null,
428
- recentLimit: Number.isFinite(secret.recentLimit) ? Number(secret.recentLimit) : null,
429
- idleSeconds: Number.isFinite(secret.idleSeconds) ? Number(secret.idleSeconds) : null,
430
- forceForeground: Boolean(secret.forceForeground),
431
- noRestore: secret.noRestore === undefined ? null : Boolean(secret.noRestore),
432
- downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
433
- downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
434
- selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
435
- wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
436
- wechatRpaServerDecisionAvailable: true,
437
- wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
438
- };
439
- }
440
- function normalizeWeChatRpaGroups(groups) {
441
- const seen = new Set();
442
- const result = [];
443
- for (const group of groups) {
444
- const name = String(group?.name || '').replace(/\s+/g, ' ').trim();
445
- if (!name || seen.has(name))
446
- continue;
447
- seen.add(name);
448
- result.push({ name });
449
- }
450
- return result;
451
- }
452
- function defaultWeChatRpaSource() {
453
- return 'macos-flow';
454
- }
455
- function buildWeChatRpaPreflightChecks(secret) {
456
- const checks = [];
457
- checks.push({
458
- code: 'mac_only',
459
- ok: secret.source !== 'windows-visual-flow',
460
- severity: 'blocking',
461
- message: secret.source === 'windows-visual-flow' ? '个人微信通道当前仅支持 macOS。' : '当前配置使用 macOS 微信通道。',
462
- });
463
- checks.push({
464
- code: 'privacy_consent_required',
465
- ok: Boolean(secret.privacyConsentAccepted),
466
- severity: 'blocking',
467
- message: Boolean(secret.privacyConsentAccepted) ? '已确认微信通道数据与隐私授权。' : '启用前需要确认微信通道数据与隐私授权。',
468
- });
469
- checks.push({
470
- code: 'server_decision_unavailable',
471
- ok: true,
472
- severity: 'blocking',
473
- message: '服务端判断能力可用。',
474
- });
475
- return checks;
476
- }
477
- export function planExternalReplySends(channelType, input) {
478
- const parts = splitExternalReplyText(input.text);
479
- if (!parts.length && !input.attachment)
480
- return [];
481
- if (channelType === 'wechat-rpa' && input.attachment && parts.length <= 1) {
482
- return [{
483
- text: parts[0] ?? '',
484
- attachment: input.attachment,
485
- idempotencyKey: input.idempotencyKey,
486
- }];
487
- }
488
- const sends = parts.map((text, index) => ({
489
- text,
490
- attachment: undefined,
491
- idempotencyKey: parts.length > 1 && input.idempotencyKey
492
- ? `${input.idempotencyKey}:${index + 1}`
493
- : input.idempotencyKey,
494
- }));
495
- if (input.attachment) {
496
- sends.push({
497
- text: '',
498
- attachment: input.attachment,
499
- idempotencyKey: parts.length && input.idempotencyKey
500
- ? `${input.idempotencyKey}:attachment`
501
- : input.idempotencyKey,
502
- });
503
- }
504
- return sends;
505
- }
506
- function mergeExternalMessages(...groups) {
507
- const seen = new Set();
508
- const out = [];
509
- for (const event of groups.flat()) {
510
- const key = `${event.channelId}\n${event.conversationId}\n${event.messageId}`;
511
- if (seen.has(key))
512
- continue;
513
- seen.add(key);
514
- out.push(event);
515
- }
516
- return out;
517
- }
518
- function replyCompletionKey(channelId, conversationId, idempotencyKey) {
519
- return crypto.createHash('sha256')
520
- .update(`${channelId}\n${conversationId}\n${idempotencyKey}`)
521
- .digest('hex')
522
- .slice(0, 32);
523
- }
524
- function replyCompletionFile(workDir) {
525
- return path.join(workDir, '.shennian', 'external-reply-idempotency.json');
526
- }
527
- function readReplyCompletionSet(workDir) {
528
- try {
529
- const parsed = JSON.parse(fs.readFileSync(replyCompletionFile(workDir), 'utf8'));
530
- const rows = Array.isArray(parsed.completed) ? parsed.completed : [];
531
- return new Set(rows
532
- .map((row) => {
533
- if (typeof row === 'string')
534
- return row;
535
- if (row && typeof row === 'object' && typeof row.key === 'string') {
536
- return row.key;
537
- }
538
- return '';
539
- })
540
- .filter(Boolean));
541
- }
542
- catch {
543
- return new Set();
544
- }
545
- }
546
- function persistReplyCompletionSet(workDir, set) {
547
- const filePath = replyCompletionFile(workDir);
548
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
549
- const keys = Array.from(set).slice(-500);
550
- fs.writeFileSync(filePath, JSON.stringify({
551
- updatedAt: new Date().toISOString(),
552
- completed: keys.map((key) => ({ key })),
553
- }, null, 2));
554
- set.clear();
555
- for (const key of keys)
556
- set.add(key);
557
- }
558
- function clampOptionalNumber(value, fallback) {
559
- const number = Number(value ?? fallback);
560
- return Number.isFinite(number) && number >= 0 ? number : undefined;
561
- }
562
- function stringOrUndefined(value) {
563
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
564
- }
1
+ import A from"node:crypto";import y from"node:fs";import g from"node:path";import{ChannelConfigRegistry as v}from"./registry.js";import{ChannelSecretRegistry as P}from"./secret-registry.js";import{WeComChannelAdapter as M}from"./wecom.js";import{WeChatRpaChannelAdapter as N}from"./wechat-rpa.js";import{ExternalWebSocketChannelAdapter as B}from"./websocket.js";import{splitExternalReplyText as D}from"./reply-split.js";class z{onExternalMessage;createReplyTarget;configs=new v;secrets=new P;adapters=new Map;completedReplyKeys=new Map;recentMessages=new Map;constructor(e,t){this.onExternalMessage=e,this.createReplyTarget=t;const n=new M(a=>this.ingest({...a,type:"external.message"}));this.adapters.set(n.type,n);const o=new B(a=>this.ingest({...a,type:"external.message"}));this.adapters.set(o.type,o);const r=new N(a=>this.ingest(a));this.adapters.set(r.type,r)}async start(){for(const e of this.configs.list().filter(t=>t.enabled))await this.adapters.get(e.type)?.connect(e).catch(()=>{})}async stop(){for(const e of this.configs.list())await this.adapters.get(e.type)?.disconnect(e).catch(()=>{})}ingest(e){const t=this.configs.get(e.channelId),n=e.managerSessionId??t?.sessionId??t?.managerSessionId;if(!n)throw new Error(`No session bound for channel ${e.channelId}`);const o=e.replyTarget||this.createReplyTarget({managerSessionId:n,channelId:e.channelId,conversationId:e.conversationId,messageId:e.messageId}),r={...e,replyTarget:o};return this.onExternalMessage(n,r),this.recordRecentMessage(r),r}async reply(e){const t=this.configs.get(e.channelId);if(!t)return{ok:!1,error:`Unknown channel: ${e.channelId}`};if((t.sessionId??t.managerSessionId)!==e.managerSessionId)return{ok:!1,error:"Channel is not bound to this session"};const n=this.adapters.get(t.type);if(!n)return{ok:!1,error:`Unsupported channel type: ${t.type}`};try{const o=E(t.type,e);if(!o.length)return{ok:!1,error:"Reply text or attachment is required"};let r=!1;for(const a of o){const i=a.idempotencyKey;if(i&&this.isReplyCompleted(t,e.conversationId,i))continue;const c=await n.send(t,{...e,...a});if(c?.status==="queued"){r=!0;continue}if(c?.status==="manual-review")return{ok:!1,error:c.reason||"Reply requires manual review before retry"};i&&this.markReplyCompleted(t,e.conversationId,i)}return r?{ok:!0,pending:!0}:{ok:!0}}catch(o){return{ok:!1,error:o instanceof Error?o.message:String(o)}}}isReplyCompleted(e,t,n){return this.loadReplyCompletionSet(e).has(k(e.id,t,n))}markReplyCompleted(e,t,n){const o=this.loadReplyCompletionSet(e);o.add(k(e.id,t,n));try{K(e.workDir,o)}catch{}}loadReplyCompletionSet(e){const t=g.resolve(e.workDir),n=this.completedReplyKeys.get(t);if(n)return n;const o=T(e.workDir);return this.completedReplyKeys.set(t,o),o}async getDefaultReplyTarget(e){const t=this.configs.list().find(r=>(r.sessionId??r.managerSessionId)===e&&r.enabled);if(!t)throw new Error("No enabled external channel is bound to this session");const n=this.adapters.get(t.type);if(!n?.defaultConversation)throw new Error(`External channel ${t.type} has no default conversation`);const o=await n.defaultConversation(t);return{channelId:t.id,conversationId:o.conversationId}}getManagerChannel(e,t,n={}){const o=this.configs.list().filter(c=>(c.sessionId??c.managerSessionId)===e&&c.type===t),r=o.find(c=>c.enabled)??o.at(-1);if(!r)return null;const a=this.secrets.get(r.secretRef),i=this.adapters.get(r.type)?.runtimeStatus?.(r)??{};return{id:r.id,type:r.type,name:r.name,sessionId:r.sessionId??r.managerSessionId,managerSessionId:r.managerSessionId,workDir:r.workDir,agentType:r.agentType,agentSessionId:r.agentSessionId,modelId:r.modelId,enabled:r.enabled,wsUrl:a?.wsUrl??"",token:n.includeSecret?a?.token??"":"",tokenConfigured:!!a?.token,canReply:!!a?.canReply,systemPrompt:typeof a?.systemPrompt=="string"?a.systemPrompt:"",...R(a),...i}}getChannelById(e,t={}){const n=this.configs.get(e);if(!n)return null;const o=this.secrets.get(n.secretRef),r=this.adapters.get(n.type)?.runtimeStatus?.(n)??{};return{id:n.id,type:n.type,name:n.name,sessionId:n.sessionId??n.managerSessionId,managerSessionId:n.managerSessionId,workDir:n.workDir,agentType:n.agentType,agentSessionId:n.agentSessionId,modelId:n.modelId,enabled:n.enabled,wsUrl:o?.wsUrl??"",token:t.includeSecret?o?.token??"":"",tokenConfigured:!!o?.token,canReply:!!o?.canReply,systemPrompt:typeof o?.systemPrompt=="string"?o.systemPrompt:"",...R(o),...r}}getChannelStatusById(e){const t=this.configs.get(e);if(!t)return null;const n=this.secrets.get(t.secretRef),o=this.adapters.get(t.type)?.runtimeStatus?.(t)??{};return{configured:!0,connected:h(t,n),type:t.type,channelId:t.id,name:t.name,canReply:!!n?.canReply,systemPrompt:typeof n?.systemPrompt=="string"?n.systemPrompt:"",...u(n),...o}}getManagerChannelStatus(e){const t=this.configs.list().find(r=>(r.sessionId??r.managerSessionId)===e&&r.enabled);if(!t)return null;const n=this.secrets.get(t.secretRef),o=this.adapters.get(t.type)?.runtimeStatus?.(t)??{};return{configured:!0,connected:h(t,n),type:t.type,channelId:t.id,name:t.name,canReply:!!n?.canReply,systemPrompt:typeof n?.systemPrompt=="string"?n.systemPrompt:"",...u(n),...o}}listManagerChannelStatuses(){return this.configs.list().filter(e=>e.enabled).map(e=>({managerSessionId:e.sessionId??e.managerSessionId,status:this.getManagerChannelStatus(e.sessionId??e.managerSessionId)})).filter(e=>!!e.status)}listManagerExternalChannels(e){return this.configs.list().filter(t=>t.enabled&&(t.sessionId??t.managerSessionId)===e).map(t=>{const n=this.secrets.get(t.secretRef),o=this.adapters.get(t.type)?.runtimeStatus?.(t)??{};return{configured:!0,connected:h(t,n),type:t.type,channelId:t.id,name:t.name,canReply:!!n?.canReply,systemPrompt:typeof n?.systemPrompt=="string"?n.systemPrompt:"",...u(n),...o}})}async syncManagerWeChatRpaChannel(e){const t=this.configs.list().find(a=>a.enabled&&a.type==="wechat-rpa"&&(a.sessionId??a.managerSessionId)===e);if(!t)throw new Error("No enabled WeChat RPA channel is bound to this session");const n=this.adapters.get(t.type);if(!n?.syncNow)throw new Error("WeChat RPA channel does not support manual sync");const o=Date.now()-120*1e3,r=F(await n.syncNow(t)??[],this.getRecentMessages(t.id,o));return{channel:this.getManagerChannel(e,"wechat-rpa",{includeSecret:!0}),messages:r}}recordRecentMessage(e){const t=this.recentMessages.get(e.channelId)??[];t.push(e),this.recentMessages.set(e.channelId,t.slice(-50))}getRecentMessages(e,t){return(this.recentMessages.get(e)??[]).filter(o=>{const r=Date.parse(o.receivedAt);return!Number.isFinite(r)||r>=t})}async upsertManagerChannel(e){const t=this.configs.get(e.id),n=this.configs.list(),o=e.sessionId||e.managerSessionId,r={id:e.id,type:e.type,name:e.name?.trim()||t?.name||"\u5916\u90E8\u6D88\u606F\u901A\u9053",sessionId:o,managerSessionId:o,workDir:e.workDir,agentType:e.agentType||t?.agentType,agentSessionId:e.agentSessionId??t?.agentSessionId??null,modelId:e.modelId??t?.modelId??null,enabled:e.enabled,secretRef:t?.secretRef||`channel:${e.id}`},a=this.secrets.get(r.secretRef),i=e.wsUrl?.trim()||a?.wsUrl||"",c=e.token?.trim()||a?.token||"",p=e.canReply??a?.canReply??!1,m=e.systemPrompt??(typeof a?.systemPrompt=="string"?a.systemPrompt:"");if(r.enabled&&(!i||!c))throw new Error("WebSocket \u5730\u5740\u548C Token \u5FC5\u586B");const f=n.filter(l=>l.id!==r.id).map(l=>(l.sessionId??l.managerSessionId)===o&&l.type===e.type?{...l,enabled:!1}:l);f.push(r),this.configs.replaceAll(f),(i||c)&&this.secrets.upsert(r.secretRef,{type:"websocket",wsUrl:i,token:c,canReply:p,systemPrompt:m});const d=this.adapters.get(r.type);for(const l of n)(l.sessionId??l.managerSessionId)===o&&l.type===e.type&&l.enabled&&await this.adapters.get(l.type)?.disconnect(l).catch(()=>{});return r.enabled&&d?.connect(r).catch(()=>{}),this.getManagerChannel(o,e.type,{includeSecret:!0})}async upsertManagerWeChatRpaChannel(e){const t=this.configs.get(e.id),n=this.configs.list(),o=e.sessionId||e.managerSessionId,r=w(e.groups);if(e.enabled&&!r.length)throw new Error("WeChat RPA \u81F3\u5C11\u9700\u8981\u914D\u7F6E\u4E00\u4E2A\u7FA4");if(e.enabled&&r.length>1)throw new Error("WeChat RPA \u6BCF\u4E2A\u5BF9\u8BDD\u53EA\u80FD\u7ED1\u5B9A\u4E00\u4E2A\u7FA4");const a={id:e.id,type:"wechat-rpa",name:e.name?.trim()||t?.name||"\u672C\u673A\u5FAE\u4FE1 RPA",sessionId:o,managerSessionId:o,workDir:e.workDir,agentType:e.agentType||t?.agentType,agentSessionId:e.agentSessionId??t?.agentSessionId??null,modelId:e.modelId??t?.modelId??null,enabled:e.enabled,secretRef:t?.secretRef||`channel:${e.id}`},i=this.secrets.get(a.secretRef),c=e.source||(i?.source==="macos-probe"||i?.source==="fixture-jsonl"||i?.source==="macos-flow"||i?.source==="windows-visual-flow"||i?.source==="wechat-rpa-lab"?i.source:x());if(e.enabled&&c==="windows-visual-flow")throw new Error("\u4E2A\u4EBA\u5FAE\u4FE1\u901A\u9053\u5F53\u524D\u4EC5\u652F\u6301 macOS");const p=e.privacyConsentAccepted??!!i?.privacyConsentAccepted;if(e.enabled&&!p)throw new Error("\u542F\u7528\u524D\u9700\u8981\u786E\u8BA4\u5FAE\u4FE1\u901A\u9053\u6570\u636E\u4E0E\u9690\u79C1\u6388\u6743");const m=n.filter(d=>d.id!==a.id).map(d=>(d.sessionId??d.managerSessionId)===o&&d.type==="wechat-rpa"?{...d,enabled:!1}:d);m.push(a),this.configs.replaceAll(m),this.secrets.upsert(a.secretRef,{type:"wechat-rpa",source:c,groups:r,pollIntervalMs:I(e.pollIntervalMs,i?.pollIntervalMs),recentLimit:I(e.recentLimit,i?.recentLimit),idleSeconds:I(e.idleSeconds,i?.idleSeconds),forceForeground:e.forceForeground??!!i?.forceForeground,noRestore:e.noRestore??(i?.noRestore===void 0?!0:!!i.noRestore),downloadAttachments:e.downloadAttachments??(i?.downloadAttachments===void 0?!0:!!i.downloadAttachments),downloadAttachmentsDir:e.downloadAttachmentsDir?.trim()||S(i?.downloadAttachmentsDir),selfNickname:e.selfNickname?.trim()||S(i?.selfNickname),privacyConsentAccepted:p,flowScriptPath:e.flowScriptPath?.trim()||S(i?.flowScriptPath),canReply:e.canReply??i?.canReply??!1,systemPrompt:e.systemPrompt??(typeof i?.systemPrompt=="string"?i.systemPrompt:"")});const f=this.adapters.get(a.type);for(const d of n)(d.sessionId??d.managerSessionId)===o&&d.type==="wechat-rpa"&&d.enabled&&await this.adapters.get(d.type)?.disconnect(d).catch(()=>{});return a.enabled&&f?.connect(a).catch(()=>{}),this.getManagerChannel(o,"wechat-rpa",{includeSecret:!0})}}function h(s,e){return!e||e.type!==s.type?!1:s.type==="wechat-rpa"?!0:s.type==="websocket"?!!(e.wsUrl&&e.token):s.type==="wecom"?!!(e.token||e.botId||e.secret):!!e.token}function R(s){return!s||s.type!=="wechat-rpa"?{}:{wechatRpaSource:typeof s.source=="string"?s.source:"",wechatRpaGroups:w(Array.isArray(s.groups)?s.groups:[]),pollIntervalMs:Number.isFinite(s.pollIntervalMs)?Number(s.pollIntervalMs):void 0,recentLimit:Number.isFinite(s.recentLimit)?Number(s.recentLimit):void 0,idleSeconds:Number.isFinite(s.idleSeconds)?Number(s.idleSeconds):void 0,forceForeground:!!s.forceForeground,noRestore:s.noRestore===void 0?void 0:!!s.noRestore,downloadAttachments:s.downloadAttachments===void 0?!0:!!s.downloadAttachments,downloadAttachmentsDir:typeof s.downloadAttachmentsDir=="string"?s.downloadAttachmentsDir:"",selfNickname:typeof s.selfNickname=="string"?s.selfNickname:"",wechatRpaPrivacyConsentAccepted:!!s.privacyConsentAccepted,wechatRpaServerDecisionAvailable:!0,wechatRpaPreflightChecks:b(s)}}function u(s){return!s||s.type!=="wechat-rpa"?{}:{wechatRpaSource:typeof s.source=="string"?s.source:null,wechatRpaGroups:w(Array.isArray(s.groups)?s.groups:[]),pollIntervalMs:Number.isFinite(s.pollIntervalMs)?Number(s.pollIntervalMs):null,recentLimit:Number.isFinite(s.recentLimit)?Number(s.recentLimit):null,idleSeconds:Number.isFinite(s.idleSeconds)?Number(s.idleSeconds):null,forceForeground:!!s.forceForeground,noRestore:s.noRestore===void 0?null:!!s.noRestore,downloadAttachments:s.downloadAttachments===void 0?!0:!!s.downloadAttachments,downloadAttachmentsDir:typeof s.downloadAttachmentsDir=="string"?s.downloadAttachmentsDir:null,selfNickname:typeof s.selfNickname=="string"?s.selfNickname:null,wechatRpaPrivacyConsentAccepted:!!s.privacyConsentAccepted,wechatRpaServerDecisionAvailable:!0,wechatRpaPreflightChecks:b(s)}}function w(s){const e=new Set,t=[];for(const n of s){const o=String(n?.name||"").replace(/\s+/g," ").trim();!o||e.has(o)||(e.add(o),t.push({name:o}))}return t}function x(){return"macos-flow"}function b(s){const e=[];return e.push({code:"mac_only",ok:s.source!=="windows-visual-flow",severity:"blocking",message:s.source==="windows-visual-flow"?"\u4E2A\u4EBA\u5FAE\u4FE1\u901A\u9053\u5F53\u524D\u4EC5\u652F\u6301 macOS\u3002":"\u5F53\u524D\u914D\u7F6E\u4F7F\u7528 macOS \u5FAE\u4FE1\u901A\u9053\u3002"}),e.push({code:"privacy_consent_required",ok:!!s.privacyConsentAccepted,severity:"blocking",message:s.privacyConsentAccepted?"\u5DF2\u786E\u8BA4\u5FAE\u4FE1\u901A\u9053\u6570\u636E\u4E0E\u9690\u79C1\u6388\u6743\u3002":"\u542F\u7528\u524D\u9700\u8981\u786E\u8BA4\u5FAE\u4FE1\u901A\u9053\u6570\u636E\u4E0E\u9690\u79C1\u6388\u6743\u3002"}),e.push({code:"server_decision_unavailable",ok:!0,severity:"blocking",message:"\u670D\u52A1\u7AEF\u5224\u65AD\u80FD\u529B\u53EF\u7528\u3002"}),e}function E(s,e){const t=D(e.text);if(!t.length&&!e.attachment)return[];if(s==="wechat-rpa"&&e.attachment&&t.length<=1)return[{text:t[0]??"",attachment:e.attachment,idempotencyKey:e.idempotencyKey}];const n=t.map((o,r)=>({text:o,attachment:void 0,idempotencyKey:t.length>1&&e.idempotencyKey?`${e.idempotencyKey}:${r+1}`:e.idempotencyKey}));return e.attachment&&n.push({text:"",attachment:e.attachment,idempotencyKey:t.length&&e.idempotencyKey?`${e.idempotencyKey}:attachment`:e.idempotencyKey}),n}function F(...s){const e=new Set,t=[];for(const n of s.flat()){const o=`${n.channelId}
2
+ ${n.conversationId}
3
+ ${n.messageId}`;e.has(o)||(e.add(o),t.push(n))}return t}function k(s,e,t){return A.createHash("sha256").update(`${s}
4
+ ${e}
5
+ ${t}`).digest("hex").slice(0,32)}function C(s){return g.join(s,".shennian","external-reply-idempotency.json")}function T(s){try{const e=JSON.parse(y.readFileSync(C(s),"utf8")),t=Array.isArray(e.completed)?e.completed:[];return new Set(t.map(n=>typeof n=="string"?n:n&&typeof n=="object"&&typeof n.key=="string"?n.key:"").filter(Boolean))}catch{return new Set}}function K(s,e){const t=C(s);y.mkdirSync(g.dirname(t),{recursive:!0});const n=Array.from(e).slice(-500);y.writeFileSync(t,JSON.stringify({updatedAt:new Date().toISOString(),completed:n.map(o=>({key:o}))},null,2)),e.clear();for(const o of n)e.add(o)}function I(s,e){const t=Number(s??e);return Number.isFinite(t)&&t>=0?t:void 0}function S(s){return typeof s=="string"&&s.trim()?s.trim():void 0}export{z as ChannelRuntime,E as planExternalReplySends};
@@ -1,46 +1 @@
1
- // @arch docs/features/manager-agent.md
2
- // @test src/__tests__/manager-runtime.test.ts
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { resolveShennianPath } from '../config/index.js';
6
- const SECRETS_PATH = resolveShennianPath('channels.secrets.json');
7
- function nowIso() {
8
- return new Date().toISOString();
9
- }
10
- function emptySecrets() {
11
- return { secrets: {} };
12
- }
13
- export class ChannelSecretRegistry {
14
- load() {
15
- try {
16
- const parsed = JSON.parse(fs.readFileSync(SECRETS_PATH, 'utf-8'));
17
- return { secrets: parsed.secrets ?? {} };
18
- }
19
- catch {
20
- return emptySecrets();
21
- }
22
- }
23
- save(file) {
24
- fs.mkdirSync(path.dirname(SECRETS_PATH), { recursive: true });
25
- fs.writeFileSync(SECRETS_PATH, JSON.stringify(file, null, 2), { mode: 0o600 });
26
- try {
27
- fs.chmodSync(SECRETS_PATH, 0o600);
28
- }
29
- catch {
30
- // Best effort on filesystems that do not support POSIX modes.
31
- }
32
- }
33
- get(secretRef) {
34
- return this.load().secrets[secretRef];
35
- }
36
- upsert(secretRef, value) {
37
- const file = this.load();
38
- const record = {
39
- ...value,
40
- updatedAt: nowIso(),
41
- };
42
- file.secrets[secretRef] = record;
43
- this.save(file);
44
- return record;
45
- }
46
- }
1
+ import r from"node:fs";import o from"node:path";import{resolveShennianPath as a}from"../config/index.js";const t=a("channels.secrets.json");function i(){return new Date().toISOString()}function u(){return{secrets:{}}}class m{load(){try{return{secrets:JSON.parse(r.readFileSync(t,"utf-8")).secrets??{}}}catch{return u()}}save(e){r.mkdirSync(o.dirname(t),{recursive:!0}),r.writeFileSync(t,JSON.stringify(e,null,2),{mode:384});try{r.chmodSync(t,384)}catch{}}get(e){return this.load().secrets[e]}upsert(e,c){const s=this.load(),n={...c,updatedAt:i()};return s.secrets[e]=n,this.save(s),n}}export{m as ChannelSecretRegistry};