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,533 +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
- const configs = allConfigs
354
- .filter((channel) => channel.id !== nextConfig.id)
355
- .map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === 'wechat-rpa'
356
- ? { ...channel, enabled: false }
357
- : channel);
358
- configs.push(nextConfig);
359
- this.configs.replaceAll(configs);
360
- this.secrets.upsert(nextConfig.secretRef, {
361
- type: 'wechat-rpa',
362
- source,
363
- groups,
364
- pollIntervalMs: clampOptionalNumber(input.pollIntervalMs, priorSecret?.pollIntervalMs),
365
- recentLimit: clampOptionalNumber(input.recentLimit, priorSecret?.recentLimit),
366
- idleSeconds: clampOptionalNumber(input.idleSeconds, priorSecret?.idleSeconds),
367
- forceForeground: input.forceForeground ?? Boolean(priorSecret?.forceForeground),
368
- noRestore: input.noRestore ?? (priorSecret?.noRestore === undefined ? true : Boolean(priorSecret.noRestore)),
369
- downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
370
- downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
371
- selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
372
- flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
373
- canReply: input.canReply ?? priorSecret?.canReply ?? false,
374
- systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
375
- });
376
- const adapter = this.adapters.get(nextConfig.type);
377
- for (const config of allConfigs) {
378
- if ((config.sessionId ?? config.managerSessionId) === boundSessionId && config.type === 'wechat-rpa' && config.enabled) {
379
- await this.adapters.get(config.type)?.disconnect(config).catch(() => { });
380
- }
381
- }
382
- if (nextConfig.enabled) {
383
- void adapter?.connect(nextConfig).catch(() => { });
384
- }
385
- return this.getManagerChannel(boundSessionId, 'wechat-rpa', { includeSecret: true });
386
- }
387
- }
388
- function isChannelSecretConfigured(config, secret) {
389
- if (!secret || secret.type !== config.type)
390
- return false;
391
- if (config.type === 'wechat-rpa')
392
- return true;
393
- if (config.type === 'websocket')
394
- return Boolean(secret.wsUrl && secret.token);
395
- if (config.type === 'wecom')
396
- return Boolean(secret.token || secret.botId || secret.secret);
397
- return Boolean(secret.token);
398
- }
399
- function wechatRpaViewFields(secret) {
400
- if (!secret || secret.type !== 'wechat-rpa')
401
- return {};
402
- return {
403
- wechatRpaSource: typeof secret.source === 'string' ? secret.source : '',
404
- wechatRpaGroups: normalizeWeChatRpaGroups(Array.isArray(secret.groups) ? secret.groups : []),
405
- pollIntervalMs: Number.isFinite(secret.pollIntervalMs) ? Number(secret.pollIntervalMs) : undefined,
406
- recentLimit: Number.isFinite(secret.recentLimit) ? Number(secret.recentLimit) : undefined,
407
- idleSeconds: Number.isFinite(secret.idleSeconds) ? Number(secret.idleSeconds) : undefined,
408
- forceForeground: Boolean(secret.forceForeground),
409
- noRestore: secret.noRestore === undefined ? undefined : Boolean(secret.noRestore),
410
- downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
411
- downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
412
- selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
413
- };
414
- }
415
- function wechatRpaStatusFields(secret) {
416
- if (!secret || secret.type !== 'wechat-rpa')
417
- return {};
418
- return {
419
- wechatRpaSource: typeof secret.source === 'string' ? secret.source : null,
420
- wechatRpaGroups: normalizeWeChatRpaGroups(Array.isArray(secret.groups) ? secret.groups : []),
421
- pollIntervalMs: Number.isFinite(secret.pollIntervalMs) ? Number(secret.pollIntervalMs) : null,
422
- recentLimit: Number.isFinite(secret.recentLimit) ? Number(secret.recentLimit) : null,
423
- idleSeconds: Number.isFinite(secret.idleSeconds) ? Number(secret.idleSeconds) : null,
424
- forceForeground: Boolean(secret.forceForeground),
425
- noRestore: secret.noRestore === undefined ? null : Boolean(secret.noRestore),
426
- downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
427
- downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
428
- selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
429
- };
430
- }
431
- function normalizeWeChatRpaGroups(groups) {
432
- const seen = new Set();
433
- const result = [];
434
- for (const group of groups) {
435
- const name = String(group?.name || '').replace(/\s+/g, ' ').trim();
436
- if (!name || seen.has(name))
437
- continue;
438
- seen.add(name);
439
- result.push({ name });
440
- }
441
- return result;
442
- }
443
- function defaultWeChatRpaSource() {
444
- return 'wechat-rpa-lab';
445
- }
446
- export function planExternalReplySends(channelType, input) {
447
- const parts = splitExternalReplyText(input.text);
448
- if (!parts.length && !input.attachment)
449
- return [];
450
- if (channelType === 'wechat-rpa' && input.attachment && parts.length <= 1) {
451
- return [{
452
- text: parts[0] ?? '',
453
- attachment: input.attachment,
454
- idempotencyKey: input.idempotencyKey,
455
- }];
456
- }
457
- const sends = parts.map((text, index) => ({
458
- text,
459
- attachment: undefined,
460
- idempotencyKey: parts.length > 1 && input.idempotencyKey
461
- ? `${input.idempotencyKey}:${index + 1}`
462
- : input.idempotencyKey,
463
- }));
464
- if (input.attachment) {
465
- sends.push({
466
- text: '',
467
- attachment: input.attachment,
468
- idempotencyKey: parts.length && input.idempotencyKey
469
- ? `${input.idempotencyKey}:attachment`
470
- : input.idempotencyKey,
471
- });
472
- }
473
- return sends;
474
- }
475
- function mergeExternalMessages(...groups) {
476
- const seen = new Set();
477
- const out = [];
478
- for (const event of groups.flat()) {
479
- const key = `${event.channelId}\n${event.conversationId}\n${event.messageId}`;
480
- if (seen.has(key))
481
- continue;
482
- seen.add(key);
483
- out.push(event);
484
- }
485
- return out;
486
- }
487
- function replyCompletionKey(channelId, conversationId, idempotencyKey) {
488
- return crypto.createHash('sha256')
489
- .update(`${channelId}\n${conversationId}\n${idempotencyKey}`)
490
- .digest('hex')
491
- .slice(0, 32);
492
- }
493
- function replyCompletionFile(workDir) {
494
- return path.join(workDir, '.shennian', 'external-reply-idempotency.json');
495
- }
496
- function readReplyCompletionSet(workDir) {
497
- try {
498
- const parsed = JSON.parse(fs.readFileSync(replyCompletionFile(workDir), 'utf8'));
499
- const rows = Array.isArray(parsed.completed) ? parsed.completed : [];
500
- return new Set(rows
501
- .map((row) => {
502
- if (typeof row === 'string')
503
- return row;
504
- if (row && typeof row === 'object' && typeof row.key === 'string') {
505
- return row.key;
506
- }
507
- return '';
508
- })
509
- .filter(Boolean));
510
- }
511
- catch {
512
- return new Set();
513
- }
514
- }
515
- function persistReplyCompletionSet(workDir, set) {
516
- const filePath = replyCompletionFile(workDir);
517
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
518
- const keys = Array.from(set).slice(-500);
519
- fs.writeFileSync(filePath, JSON.stringify({
520
- updatedAt: new Date().toISOString(),
521
- completed: keys.map((key) => ({ key })),
522
- }, null, 2));
523
- set.clear();
524
- for (const key of keys)
525
- set.add(key);
526
- }
527
- function clampOptionalNumber(value, fallback) {
528
- const number = Number(value ?? fallback);
529
- return Number.isFinite(number) && number >= 0 ? number : undefined;
530
- }
531
- function stringOrUndefined(value) {
532
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
533
- }
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};
@@ -17,6 +17,7 @@ type ChannelSecretRecord = {
17
17
  downloadAttachments?: boolean;
18
18
  downloadAttachmentsDir?: string;
19
19
  selfNickname?: string;
20
+ privacyConsentAccepted?: boolean;
20
21
  idleSeconds?: number;
21
22
  recentLimit?: number;
22
23
  flowScriptPath?: string;
@@ -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};