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