shennian 0.3.85 → 0.3.86
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/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +28 -28
- package/dist/src/agents/adapter.js +19 -1
- package/dist/src/agents/claude-managed-permissions.js +248 -1
- package/dist/src/agents/claude.js +393 -8
- package/dist/src/agents/codex-managed-permissions.js +294 -1
- package/dist/src/agents/codex-managed-room-tools.js +249 -1
- package/dist/src/agents/codex-utils.js +200 -7
- package/dist/src/agents/codex.js +1352 -17
- package/dist/src/agents/config-status.js +226 -1
- package/dist/src/agents/cursor-managed-permissions.js +299 -2
- package/dist/src/agents/cursor.js +314 -1
- package/dist/src/agents/custom.js +418 -4
- package/dist/src/agents/external-channel-instructions.js +94 -11
- package/dist/src/agents/gemini.js +180 -1
- package/dist/src/agents/manager.d.ts +2 -0
- package/dist/src/agents/manager.js +250 -18
- package/dist/src/agents/model-registry/cache.js +37 -1
- package/dist/src/agents/model-registry/discovery.js +188 -2
- package/dist/src/agents/model-registry/parsers.js +521 -4
- package/dist/src/agents/model-registry/runner.js +30 -1
- package/dist/src/agents/model-registry/service.js +115 -1
- package/dist/src/agents/model-registry/types.js +8 -1
- package/dist/src/agents/model-registry.js +19 -1
- package/dist/src/agents/native-capabilities.js +237 -1
- package/dist/src/agents/openclaw.js +293 -2
- package/dist/src/agents/opencode-managed-permissions.js +187 -2
- package/dist/src/agents/opencode.js +298 -1
- package/dist/src/agents/platform-instructions.js +46 -17
- package/dist/src/agents/runtime-compatibility.js +44 -1
- package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
- package/dist/src/agents/workbuddy.js +444 -3
- package/dist/src/commands/room.js +2 -2
- package/dist/src/fs/boundary.js +185 -1
- package/dist/src/fs/handler.js +130 -1
- package/dist/src/fs/security.js +32 -1
- package/dist/src/fs/text-decoder.js +110 -1
- package/dist/src/index.js +587 -3
- package/dist/src/manager/prompt.js +34 -29
- package/dist/src/manager/registry.js +317 -1
- package/dist/src/manager/retired-external-channel-compat.js +40 -1
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +1514 -23
- package/dist/src/native-fusion/codex-parser.js +660 -4
- package/dist/src/native-fusion/codex-user-input.js +102 -5
- package/dist/src/native-fusion/config.js +5 -1
- package/dist/src/native-fusion/file-watcher.js +191 -1
- package/dist/src/native-fusion/local-repository.js +569 -2
- package/dist/src/native-fusion/opencode-parser.js +130 -3
- package/dist/src/native-fusion/parsers.js +140 -3
- package/dist/src/native-fusion/service.js +739 -2
- package/dist/src/native-fusion/session-header.js +101 -2
- package/dist/src/native-fusion/state.js +104 -2
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/machine-authority.js +34 -1
- package/dist/src/room/activation-audit.js +45 -2
- package/dist/src/room/activation-policy.js +30 -1
- package/dist/src/room/activation-service.d.ts +1 -0
- package/dist/src/room/activation-service.js +408 -8
- package/dist/src/room/invite-bootstrap.js +156 -1
- package/dist/src/room/managed-context.d.ts +1 -1
- package/dist/src/room/managed-context.js +1 -1
- package/dist/src/room/managed-prompt-composer.d.ts +1 -1
- package/dist/src/room/managed-prompt-composer.js +6 -5
- package/dist/src/room/managed-room-binding.js +167 -1
- package/dist/src/room/managed-room-ipc.d.ts +2 -2
- package/dist/src/room/managed-room-ipc.js +2 -2
- package/dist/src/room/mirror-sync-service.js +183 -1
- package/dist/src/room/room-action-client.d.ts +11 -0
- package/dist/src/room/room-action-client.js +1 -1
- package/dist/src/room/room-binding-client.d.ts +1 -0
- package/dist/src/room/room-binding-client.js +151 -1
- package/dist/src/security/managed-agent-policy.d.ts +3 -2
- package/dist/src/security/managed-agent-policy.js +1 -1
- package/dist/src/session/archive-zip.js +220 -1
- package/dist/src/session/deletion-sync-state.js +136 -2
- package/dist/src/session/handlers/agent-capabilities.js +14 -1
- package/dist/src/session/handlers/agent-config.js +150 -1
- package/dist/src/session/handlers/agents.js +55 -1
- package/dist/src/session/handlers/chat.d.ts +1 -1
- package/dist/src/session/handlers/chat.js +1030 -2
- package/dist/src/session/handlers/control.js +61 -1
- package/dist/src/session/handlers/fs.js +594 -1
- package/dist/src/session/handlers/message-status.js +15 -1
- package/dist/src/session/handlers/remote-access.js +43 -1
- package/dist/src/session/handlers/room-managed.js +101 -1
- package/dist/src/session/handlers/session-refresh.js +35 -1
- package/dist/src/session/handlers/title.js +79 -1
- package/dist/src/session/handlers/tool-detail.js +149 -1
- package/dist/src/session/handlers/voice-window.js +62 -1
- package/dist/src/session/history-subscriptions.js +470 -1
- package/dist/src/session/manager.js +658 -1
- package/dist/src/session/native-cleanup-outbox.js +287 -2
- package/dist/src/session/native-session-lifecycle.js +327 -6
- package/dist/src/session/queue.d.ts +5 -0
- package/dist/src/session/queue.js +8 -8
- package/dist/src/upgrade/scheduler.js +126 -1
- package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
- package/package.json +1 -1
|
@@ -1,25 +1,1516 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// @arch docs/features/manager-agent.md
|
|
2
|
+
// @test src/__tests__/manager-inbox.test.ts
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { AVAILABLE_BUILTIN_AGENT_TYPES, extractPayloadText, formatExternalConversationText, formatExternalMessageLine, formatExternalAttachmentReference, isAgentHiddenPayload, isToolPayload, } from '@shennian/wire';
|
|
9
|
+
import { ManagerRegistry } from './registry.js';
|
|
10
|
+
import { readMessages } from '../session/store.js';
|
|
11
|
+
import { listProjectedSessions } from '../session/projection.js';
|
|
12
|
+
import { RetiredChannelRuntime as ChannelRuntime, RetiredWeChatRpaSessionBindingSync as WeChatRpaSessionBindingSync, createRetiredAutomationLane as createWeChatAutomationLane, createRetiredChannelApiClient as createWeChatChannelApiClient, retiredDirectWeChat as readDirectWeChatLatestOnce, retiredDirectWeChat as sendDirectWeChatMessageOnce, splitExternalReplyText, weChatChannelConversationId, } from './retired-external-channel-compat.js';
|
|
13
|
+
import { loadConfig, resolveShennianPath } from '../config/index.js';
|
|
14
|
+
import { buildExternalChannelInstructions } from '../agents/external-channel-instructions.js';
|
|
15
|
+
import { getPersonalSyncIdentity } from '../personal-sync/epoch.js';
|
|
16
|
+
const MAX_MANAGER_IPC_BODY_BYTES = Number(process.env.SHENNIAN_MANAGER_IPC_BODY_MAX_BYTES || 2 * 1024 * 1024);
|
|
17
|
+
const MANAGER_IPC_LONG_OPERATION_TIMEOUT_MS = 12 * 60_000;
|
|
18
|
+
let singleton = null;
|
|
19
|
+
export function setManagerRuntimeService(service) {
|
|
20
|
+
singleton = service;
|
|
21
|
+
}
|
|
22
|
+
export function getManagerRuntimeService() {
|
|
23
|
+
return singleton;
|
|
24
|
+
}
|
|
25
|
+
function normalizeWorkDir(workDir) {
|
|
26
|
+
return path.resolve(workDir || os.homedir());
|
|
27
|
+
}
|
|
28
|
+
function json(res, status, body) {
|
|
29
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
30
|
+
res.end(JSON.stringify(body));
|
|
31
|
+
}
|
|
32
|
+
function runIdFromMessageId(id) {
|
|
33
|
+
const match = /^agent-(.+)-\d+$/.exec(id);
|
|
34
|
+
return match?.[1] ?? null;
|
|
35
|
+
}
|
|
36
|
+
function isManagerWorkerAgentType(agentType) {
|
|
37
|
+
if (agentType === 'manager' || agentType === 'pi')
|
|
38
|
+
return false;
|
|
39
|
+
if (agentType.startsWith('custom:'))
|
|
40
|
+
return true;
|
|
41
|
+
return AVAILABLE_BUILTIN_AGENT_TYPES.includes(agentType);
|
|
42
|
+
}
|
|
43
|
+
function seqFromMessageId(id) {
|
|
44
|
+
const match = /^agent-.+-(\d+)$/.exec(id);
|
|
45
|
+
if (!match)
|
|
46
|
+
return null;
|
|
47
|
+
const seq = Number(match[1]);
|
|
48
|
+
return Number.isInteger(seq) && seq >= 0 ? seq : null;
|
|
49
|
+
}
|
|
50
|
+
function normalizeMarkdownForWorkerSummary(text) {
|
|
51
|
+
return text.replace(/\r\n/g, '\n').trim();
|
|
52
|
+
}
|
|
53
|
+
function formatManagerInboxPrompt(events) {
|
|
54
|
+
const lines = events.map((event, index) => [
|
|
55
|
+
`${index + 1}. 事件类型:${event.kind}`,
|
|
56
|
+
`Worker:${event.workerSessionId}`,
|
|
57
|
+
event.runId ? `Run:${event.runId}` : '',
|
|
58
|
+
`摘要:\n${event.summary}`,
|
|
59
|
+
].filter(Boolean).join('\n'));
|
|
60
|
+
return `Manager 事件收件箱中有 ${events.length} 条待处理事件:\n\n${lines.join('\n\n')}\n\n请结合用户目标决定下一步:继续等待、创建或指派 worker、停止 worker、询问用户,或向用户汇报。`;
|
|
61
|
+
}
|
|
62
|
+
function toolSummary(payload) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(payload);
|
|
65
|
+
const type = parsed.type === 'tool_result' || parsed.result ? 'tool_result' : 'tool_call';
|
|
66
|
+
const name = parsed.name || 'tool';
|
|
67
|
+
const result = typeof parsed.result === 'string' ? parsed.result.replace(/\s+/g, ' ').trim() : '';
|
|
68
|
+
const clipped = result.length > 220 ? `${result.slice(0, 220)}...` : result;
|
|
69
|
+
return clipped ? `[${type}] ${name}: ${clipped}` : `[${type}] ${name}`;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return '[tool]';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function parseExternalReplyAttachment(value) {
|
|
76
|
+
if (!value || typeof value !== 'object')
|
|
77
|
+
return undefined;
|
|
78
|
+
const record = value;
|
|
79
|
+
const kind = String(record.kind || '');
|
|
80
|
+
const name = String(record.name || '');
|
|
81
|
+
const mimeType = String(record.mimeType || '');
|
|
82
|
+
const dataBase64 = String(record.dataBase64 || '');
|
|
83
|
+
const localPath = String(record.localPath || '');
|
|
84
|
+
const url = String(record.url || '');
|
|
85
|
+
const size = Number(record.size || 0);
|
|
86
|
+
if (dataBase64)
|
|
87
|
+
throw new Error('Manager IPC external attachments must use localPath or url; dataBase64 is not accepted');
|
|
88
|
+
if (kind !== 'image' && kind !== 'video' && kind !== 'file')
|
|
89
|
+
return undefined;
|
|
90
|
+
if (!name || !mimeType || !Number.isFinite(size) || size < 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (!localPath && !url)
|
|
93
|
+
return undefined;
|
|
94
|
+
return {
|
|
95
|
+
kind,
|
|
96
|
+
name,
|
|
97
|
+
mimeType,
|
|
98
|
+
size,
|
|
99
|
+
...(localPath ? { localPath } : {}),
|
|
100
|
+
...(url ? { url } : {}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parseWeChatToolBindingFromIpc(body) {
|
|
104
|
+
const binding = body.binding && typeof body.binding === 'object' && !Array.isArray(body.binding)
|
|
105
|
+
? body.binding
|
|
106
|
+
: {};
|
|
107
|
+
const sessionId = String(binding.sessionId || body.managerSessionId || '').trim();
|
|
108
|
+
const channelId = String(binding.channelId || '').trim();
|
|
109
|
+
const conversationId = String(binding.conversationId || '').trim();
|
|
110
|
+
const conversationName = String(binding.conversationName || body.conversation || '').trim();
|
|
111
|
+
const workDir = String(binding.workDir || body.workDir || '').trim();
|
|
112
|
+
if (!sessionId)
|
|
113
|
+
throw new Error('WeChat tool sessionId is required');
|
|
114
|
+
if (!channelId)
|
|
115
|
+
throw new Error('WeChat tool channelId is required');
|
|
116
|
+
if (!conversationId)
|
|
117
|
+
throw new Error('WeChat tool conversationId is required');
|
|
118
|
+
if (!conversationName)
|
|
119
|
+
throw new Error('WeChat tool conversationName is required');
|
|
120
|
+
if (!workDir)
|
|
121
|
+
throw new Error('WeChat tool workDir is required');
|
|
122
|
+
return { sessionId, channelId, conversationId, conversationName, workDir };
|
|
123
|
+
}
|
|
124
|
+
function compactWorkerTranscript(rawMessages, limit) {
|
|
125
|
+
const chronological = [...rawMessages].sort((a, b) => a.ts - b.ts);
|
|
126
|
+
const compacted = [];
|
|
127
|
+
let buffer = null;
|
|
128
|
+
let bufferRunId = null;
|
|
129
|
+
let bufferSeq = null;
|
|
130
|
+
let bufferText = '';
|
|
131
|
+
const flush = () => {
|
|
132
|
+
if (!buffer)
|
|
133
|
+
return;
|
|
134
|
+
const text = bufferText.trim();
|
|
135
|
+
if (text) {
|
|
136
|
+
compacted.push({
|
|
137
|
+
...buffer,
|
|
138
|
+
id: `${buffer.id}-compact`,
|
|
139
|
+
payload: text,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
buffer = null;
|
|
143
|
+
bufferRunId = null;
|
|
144
|
+
bufferSeq = null;
|
|
145
|
+
bufferText = '';
|
|
146
|
+
};
|
|
147
|
+
for (const message of chronological) {
|
|
148
|
+
if (isAgentHiddenPayload(message.payload)) {
|
|
149
|
+
flush();
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (message.role === 'user') {
|
|
153
|
+
flush();
|
|
154
|
+
compacted.push(message);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (isToolPayload(message.payload)) {
|
|
158
|
+
flush();
|
|
159
|
+
compacted.push({
|
|
160
|
+
...message,
|
|
161
|
+
payload: toolSummary(message.payload),
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const text = extractPayloadText(message.payload);
|
|
166
|
+
if (!text.trim())
|
|
167
|
+
continue;
|
|
168
|
+
const runId = runIdFromMessageId(message.id);
|
|
169
|
+
const seq = seqFromMessageId(message.id);
|
|
170
|
+
if (buffer && buffer.role === message.role && bufferRunId === runId && runId && seq !== null && bufferSeq !== null && seq === bufferSeq + 1) {
|
|
171
|
+
bufferText += text;
|
|
172
|
+
buffer.ts = message.ts;
|
|
173
|
+
bufferSeq = seq;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
flush();
|
|
177
|
+
buffer = message;
|
|
178
|
+
bufferRunId = runId;
|
|
179
|
+
bufferSeq = seq;
|
|
180
|
+
bufferText = text;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
flush();
|
|
184
|
+
return compacted.slice(-limit).sort((a, b) => b.ts - a.ts);
|
|
185
|
+
}
|
|
186
|
+
async function readJson(req) {
|
|
187
|
+
const chunks = [];
|
|
188
|
+
let total = 0;
|
|
189
|
+
for await (const chunk of req) {
|
|
190
|
+
const buffer = Buffer.from(chunk);
|
|
191
|
+
total += buffer.byteLength;
|
|
192
|
+
if (Number.isFinite(MAX_MANAGER_IPC_BODY_BYTES) && MAX_MANAGER_IPC_BODY_BYTES > 0 && total > MAX_MANAGER_IPC_BODY_BYTES) {
|
|
193
|
+
throw new Error(`Manager IPC request body is too large. Max: ${MAX_MANAGER_IPC_BODY_BYTES} bytes.`);
|
|
194
|
+
}
|
|
195
|
+
chunks.push(buffer);
|
|
196
|
+
}
|
|
197
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
198
|
+
return raw ? JSON.parse(raw) : {};
|
|
199
|
+
}
|
|
200
|
+
function appJson(runtime, reqId, ok, payload) {
|
|
201
|
+
runtime.client.sendRes({
|
|
202
|
+
type: 'res',
|
|
203
|
+
id: reqId,
|
|
204
|
+
ok,
|
|
205
|
+
...(ok ? { payload } : { error: String(payload.error || 'unknown error') }),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function shouldFallbackToLocalChannel(error) {
|
|
209
|
+
return /binding not found|unknown method|not supported|relay is not connected|no external channel/i.test(error);
|
|
210
|
+
}
|
|
211
|
+
function managerIpcRuntimePath() {
|
|
212
|
+
if (isVitestRuntimeWithoutExplicitHome()) {
|
|
213
|
+
return path.join(os.tmpdir(), 'shennian-vitest-runtime', String(process.pid), 'manager-ipc.json');
|
|
214
|
+
}
|
|
215
|
+
return resolveShennianPath('runtime', 'manager-ipc.json');
|
|
216
|
+
}
|
|
217
|
+
function isVitestRuntimeWithoutExplicitHome() {
|
|
218
|
+
return (process.env.VITEST === 'true' || Boolean(process.env.VITEST_WORKER_ID)) && !process.env.SHENNIAN_HOME?.trim();
|
|
219
|
+
}
|
|
220
|
+
export class ManagerRuntimeService {
|
|
221
|
+
opts;
|
|
222
|
+
managedPolicies = new Map();
|
|
223
|
+
registry = new ManagerRegistry();
|
|
224
|
+
channelRuntime;
|
|
225
|
+
weChatRpaSessionSync;
|
|
226
|
+
server = null;
|
|
227
|
+
ipcUrl = null;
|
|
228
|
+
ipcToken = randomBytes(24).toString('hex');
|
|
229
|
+
healthTimer = null;
|
|
230
|
+
startPromise = null;
|
|
231
|
+
workerTextAcc = new Map();
|
|
232
|
+
weChatAutomationLane;
|
|
233
|
+
constructor(opts) {
|
|
234
|
+
this.opts = opts;
|
|
235
|
+
this.weChatAutomationLane = opts.weChatAutomationLane ?? createWeChatAutomationLane();
|
|
236
|
+
this.channelRuntime = opts.channelRuntime ?? new ChannelRuntime((managerSessionId, event) => {
|
|
237
|
+
this.handleExternalMessage(managerSessionId, event);
|
|
238
|
+
}, (input) => this.registry.createReplyTarget(input).replyTarget, {
|
|
239
|
+
createWeChatRpaProductRunner: opts.createWeChatRpaProductRunner,
|
|
240
|
+
weChatAutomationLane: this.weChatAutomationLane,
|
|
241
|
+
});
|
|
242
|
+
this.weChatRpaSessionSync = new WeChatRpaSessionBindingSync({
|
|
243
|
+
channelRuntime: this.channelRuntime,
|
|
244
|
+
getLocalMachineId: () => process.env.SHENNIAN_MACHINE_ID || loadConfig().machineId || '',
|
|
245
|
+
listSessions: () => this.listWeChatRpaSessionContexts(),
|
|
246
|
+
listAuthoritativeSessions: () => this.listAuthoritativeWeChatRpaSessionContexts(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
*listWeChatRpaSessionContexts() {
|
|
250
|
+
const sessions = this.opts.getRuntime().sessions;
|
|
251
|
+
for (const [sessionId, session] of sessions) {
|
|
252
|
+
yield {
|
|
253
|
+
sessionId,
|
|
254
|
+
workDir: session.workDir,
|
|
255
|
+
agentType: session.agentType,
|
|
256
|
+
agentSessionId: session.agentSessionId,
|
|
257
|
+
externalChannel: session.externalChannel ?? null,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async listAuthoritativeWeChatRpaSessionContexts() {
|
|
262
|
+
const config = loadConfig();
|
|
263
|
+
const localMachineId = process.env.SHENNIAN_MACHINE_ID || config.machineId || '';
|
|
264
|
+
if (!config.machineToken)
|
|
265
|
+
return null;
|
|
266
|
+
if (!localMachineId)
|
|
267
|
+
return null;
|
|
268
|
+
const localContexts = this.listLocalWeChatRpaSessionContexts(localMachineId);
|
|
269
|
+
const response = await createWeChatChannelApiClient({
|
|
270
|
+
serverUrl: config.serverUrl,
|
|
271
|
+
machineToken: config.machineToken,
|
|
272
|
+
}).listBindings({ machineId: localMachineId });
|
|
273
|
+
return response.bindings
|
|
274
|
+
.filter((binding) => binding.enabled !== false)
|
|
275
|
+
.map((binding) => {
|
|
276
|
+
const local = localContexts.get(binding.sessionId);
|
|
277
|
+
return {
|
|
278
|
+
sessionId: binding.sessionId,
|
|
279
|
+
workDir: binding.sessionWorkDir?.trim() || local?.workDir?.trim() || os.homedir(),
|
|
280
|
+
agentType: binding.sessionAgentType?.trim() || local?.agentType || undefined,
|
|
281
|
+
agentSessionId: binding.sessionAgentSessionId ?? local?.agentSessionId ?? null,
|
|
282
|
+
modelId: binding.sessionModelId ?? local?.modelId ?? null,
|
|
283
|
+
externalChannel: {
|
|
284
|
+
connected: true,
|
|
285
|
+
configured: true,
|
|
286
|
+
type: 'wechat-rpa',
|
|
287
|
+
channelId: binding.id,
|
|
288
|
+
machineId: binding.machineId,
|
|
289
|
+
name: binding.conversationName,
|
|
290
|
+
canReply: binding.allowReply !== false,
|
|
291
|
+
systemPrompt: null,
|
|
292
|
+
wechatRpaSource: 'wechat-channel',
|
|
293
|
+
wechatRpaGroups: [{ name: binding.conversationName }],
|
|
294
|
+
downloadAttachments: binding.downloadMedia !== false,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
listLocalWeChatRpaSessionContexts(localMachineId) {
|
|
300
|
+
const contexts = new Map();
|
|
301
|
+
for (const session of this.listWeChatRpaSessionContexts()) {
|
|
302
|
+
contexts.set(session.sessionId, session);
|
|
303
|
+
}
|
|
304
|
+
for (const session of listProjectedSessions()) {
|
|
305
|
+
if (!session?.id || session.deletedAt)
|
|
306
|
+
continue;
|
|
307
|
+
if (session.machineId && localMachineId && session.machineId !== localMachineId)
|
|
308
|
+
continue;
|
|
309
|
+
if (contexts.has(session.id))
|
|
310
|
+
continue;
|
|
311
|
+
contexts.set(session.id, {
|
|
312
|
+
sessionId: session.id,
|
|
313
|
+
workDir: session.workDir,
|
|
314
|
+
agentType: session.agentType,
|
|
315
|
+
agentSessionId: session.agentSessionId,
|
|
316
|
+
modelId: session.modelId,
|
|
317
|
+
externalChannel: session.externalChannel ?? null,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return contexts;
|
|
321
|
+
}
|
|
322
|
+
// chat.enqueue/send 带来最新 externalChannel 或 session dispose 时的增量对账入口。
|
|
323
|
+
// 让接收侧的本机通道跟随服务端绑定起停,与发送侧对称。
|
|
324
|
+
async notifyWeChatRpaSessionBinding(session) {
|
|
325
|
+
await this.weChatRpaSessionSync.reconcileSession(session).catch((error) => {
|
|
326
|
+
console.error(`[wechat-rpa-sync] reconcileSession failed sessionId=${session.sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async reconcileWeChatRpaSessionBindings() {
|
|
330
|
+
await this.weChatRpaSessionSync.reconcileAll().catch((error) => {
|
|
331
|
+
console.error(`[wechat-rpa-sync] reconcileAll failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
async start() {
|
|
335
|
+
if (this.startPromise)
|
|
336
|
+
return this.startPromise;
|
|
337
|
+
this.startPromise = this.doStart();
|
|
338
|
+
return this.startPromise;
|
|
339
|
+
}
|
|
340
|
+
async doStart() {
|
|
341
|
+
if (this.server)
|
|
342
|
+
return;
|
|
343
|
+
this.server = http.createServer((req, res) => {
|
|
344
|
+
void this.handleIpc(req, res);
|
|
345
|
+
});
|
|
346
|
+
this.server.requestTimeout = MANAGER_IPC_LONG_OPERATION_TIMEOUT_MS;
|
|
347
|
+
this.server.timeout = MANAGER_IPC_LONG_OPERATION_TIMEOUT_MS;
|
|
348
|
+
this.server.keepAliveTimeout = MANAGER_IPC_LONG_OPERATION_TIMEOUT_MS;
|
|
349
|
+
this.server.headersTimeout = MANAGER_IPC_LONG_OPERATION_TIMEOUT_MS + 5_000;
|
|
350
|
+
await new Promise((resolve, reject) => {
|
|
351
|
+
this.server.once('error', reject);
|
|
352
|
+
this.server.listen(0, '127.0.0.1', () => resolve());
|
|
353
|
+
});
|
|
354
|
+
const address = this.server.address();
|
|
355
|
+
if (typeof address === 'object' && address) {
|
|
356
|
+
this.ipcUrl = `http://127.0.0.1:${address.port}`;
|
|
357
|
+
this.writeIpcRuntimeFile();
|
|
358
|
+
}
|
|
359
|
+
this.server.unref();
|
|
360
|
+
await this.channelRuntime.start();
|
|
361
|
+
// 启动时按当前会话快照对账一次:老 daemon 升级 / 服务端已有绑定但本机 channels.json
|
|
362
|
+
// 尚无镜像时,补齐接收侧的轮询 timer。会话快照此刻可能为空,后续 chat 增量会再补。
|
|
363
|
+
void this.reconcileWeChatRpaSessionBindings();
|
|
364
|
+
this.broadcastConfiguredChannelStatuses();
|
|
365
|
+
this.healthTimer = setInterval(() => this.scanWorkerHealth(), 60_000);
|
|
366
|
+
this.healthTimer.unref();
|
|
367
|
+
}
|
|
368
|
+
async ready() {
|
|
369
|
+
await this.start();
|
|
370
|
+
}
|
|
371
|
+
async stop() {
|
|
372
|
+
if (this.healthTimer)
|
|
373
|
+
clearInterval(this.healthTimer);
|
|
374
|
+
this.healthTimer = null;
|
|
375
|
+
await this.channelRuntime.stop();
|
|
376
|
+
await new Promise((resolve) => {
|
|
377
|
+
if (!this.server)
|
|
378
|
+
return resolve();
|
|
379
|
+
this.server.close(() => resolve());
|
|
380
|
+
});
|
|
381
|
+
this.server = null;
|
|
382
|
+
this.ipcUrl = null;
|
|
383
|
+
this.removeIpcRuntimeFile();
|
|
384
|
+
}
|
|
385
|
+
writeIpcRuntimeFile() {
|
|
386
|
+
if (!this.ipcUrl)
|
|
387
|
+
return;
|
|
388
|
+
try {
|
|
389
|
+
const filePath = managerIpcRuntimePath();
|
|
390
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
391
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
392
|
+
url: this.ipcUrl,
|
|
393
|
+
token: this.ipcToken,
|
|
394
|
+
pid: process.pid,
|
|
395
|
+
updatedAt: new Date().toISOString(),
|
|
396
|
+
}, null, 2), { mode: 0o600 });
|
|
397
|
+
fs.chmodSync(filePath, 0o600);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Best effort. Injected env remains the primary path for managed agents.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
removeIpcRuntimeFile() {
|
|
404
|
+
try {
|
|
405
|
+
const filePath = managerIpcRuntimePath();
|
|
406
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
407
|
+
if (parsed.url === this.ipcUrl || parsed.token === this.ipcToken) {
|
|
408
|
+
fs.unlinkSync(filePath);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// The file may not exist or may already have been replaced by a newer daemon.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
getInjectedEnv(managerSessionId, agentSessionId, workDir, modelId) {
|
|
416
|
+
if (!this.ipcUrl)
|
|
417
|
+
return {};
|
|
418
|
+
return {
|
|
419
|
+
SHENNIAN_MANAGER_SESSION_ID: managerSessionId,
|
|
420
|
+
SHENNIAN_MANAGER_AGENT_SESSION_ID: agentSessionId ?? '',
|
|
421
|
+
SHENNIAN_MANAGER_WORKDIR: normalizeWorkDir(workDir),
|
|
422
|
+
SHENNIAN_MANAGER_MODEL: modelId,
|
|
423
|
+
SHENNIAN_MANAGER_IPC_URL: this.ipcUrl,
|
|
424
|
+
SHENNIAN_MANAGER_IPC_TOKEN: this.ipcToken,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
registerManager(input) {
|
|
428
|
+
this.registry.upsertManager({
|
|
429
|
+
...input,
|
|
430
|
+
workDir: normalizeWorkDir(input.workDir),
|
|
431
|
+
machineId: process.env.SHENNIAN_MACHINE_ID ?? null,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
setManagedAgentPolicy(sessionId, policy) {
|
|
435
|
+
if (policy)
|
|
436
|
+
this.managedPolicies.set(sessionId, policy);
|
|
437
|
+
else
|
|
438
|
+
this.managedPolicies.delete(sessionId);
|
|
439
|
+
}
|
|
440
|
+
getManagedAgentPolicyForWorker(managerSessionId, workDir) {
|
|
441
|
+
const manager = this.registry.getManager(managerSessionId);
|
|
442
|
+
const policy = this.managedPolicies.get(managerSessionId);
|
|
443
|
+
if (!manager || !policy || normalizeWorkDir(workDir) !== manager.workDir)
|
|
444
|
+
return null;
|
|
445
|
+
return policy;
|
|
446
|
+
}
|
|
447
|
+
setManagerWorkerDefaults(sessionId, agentType, modelId) {
|
|
448
|
+
const manager = this.registry.getManager(sessionId);
|
|
449
|
+
if (!manager)
|
|
450
|
+
return;
|
|
451
|
+
this.registry.upsertManager({
|
|
452
|
+
...manager,
|
|
453
|
+
defaultWorkerAgentType: agentType ?? null,
|
|
454
|
+
defaultWorkerModelId: modelId ?? null,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
getManagerWorkerDefaults(sessionId) {
|
|
458
|
+
const manager = this.registry.getManager(sessionId);
|
|
459
|
+
return {
|
|
460
|
+
agentType: manager?.defaultWorkerAgentType ?? null,
|
|
461
|
+
modelId: manager?.defaultWorkerModelId ?? null,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
noteManagerAgentSession(sessionId, agentSessionId, workDir, modelId) {
|
|
465
|
+
const manager = this.registry.getManager(sessionId);
|
|
466
|
+
this.registry.upsertManager({
|
|
467
|
+
sessionId,
|
|
468
|
+
agentSessionId,
|
|
469
|
+
workDir: normalizeWorkDir(workDir),
|
|
470
|
+
machineId: process.env.SHENNIAN_MACHINE_ID ?? null,
|
|
471
|
+
modelId,
|
|
472
|
+
agentType: manager?.agentType,
|
|
473
|
+
providerModelId: manager?.providerModelId,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
noteAgentEvent(sessionId, event) {
|
|
477
|
+
const worker = this.findWorker(sessionId);
|
|
478
|
+
if (!worker)
|
|
479
|
+
return;
|
|
480
|
+
const patch = {
|
|
481
|
+
lastActivityAt: new Date().toISOString(),
|
|
482
|
+
runId: event.runId,
|
|
483
|
+
};
|
|
484
|
+
if (event.agentSessionId)
|
|
485
|
+
patch.agentSessionId = event.agentSessionId;
|
|
486
|
+
const textKey = `${sessionId}:${event.runId}`;
|
|
487
|
+
if (event.state === 'delta' && event.text && !event.thinking) {
|
|
488
|
+
const nextText = (this.workerTextAcc.get(textKey) ?? '') + event.text;
|
|
489
|
+
this.workerTextAcc.set(textKey, nextText);
|
|
490
|
+
const normalized = normalizeMarkdownForWorkerSummary(nextText);
|
|
491
|
+
if (normalized) {
|
|
492
|
+
patch.summary = normalized.length > 160 ? `${normalized.slice(0, 160)}...` : normalized;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (event.state === 'final' || event.state === 'error' || event.state === 'aborted') {
|
|
496
|
+
patch.status = event.state;
|
|
497
|
+
const accumulated = normalizeMarkdownForWorkerSummary(this.workerTextAcc.get(textKey) ?? '');
|
|
498
|
+
if (accumulated) {
|
|
499
|
+
patch.summary = accumulated.length > 240 ? `${accumulated.slice(0, 240)}...` : accumulated;
|
|
500
|
+
}
|
|
501
|
+
this.workerTextAcc.delete(textKey);
|
|
502
|
+
}
|
|
503
|
+
else if (event.state === 'start') {
|
|
504
|
+
patch.status = 'running';
|
|
505
|
+
}
|
|
506
|
+
const updated = this.registry.updateWorker(sessionId, patch);
|
|
507
|
+
if (updated && (event.state === 'final' || event.state === 'error' || event.state === 'aborted')) {
|
|
508
|
+
this.enqueueWorkerEvent(updated.managedBy, updated, event.state, event.runId, event.message);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
findWorker(sessionId) {
|
|
512
|
+
const registry = this.registry.load();
|
|
513
|
+
return registry.workers[sessionId];
|
|
514
|
+
}
|
|
515
|
+
async handleAppReq(req) {
|
|
516
|
+
const runtime = this.opts.getRuntime();
|
|
517
|
+
if (req.method.includes('channel') || req.method.includes('wechat-rpa')) {
|
|
518
|
+
appJson(runtime, req.id, false, { error: 'feature_retired' });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const body = (req.params ?? {});
|
|
522
|
+
try {
|
|
523
|
+
const managerSessionId = String(body.managerSessionId || body.sessionId || '');
|
|
524
|
+
if (!managerSessionId)
|
|
525
|
+
throw new Error('sessionId is required');
|
|
526
|
+
const manager = this.registry.getManager(managerSessionId);
|
|
527
|
+
const isWeChatRpaGet = req.method === 'session.wechat-rpa.channel.get' || req.method === 'manager.wechat-rpa.channel.get';
|
|
528
|
+
const isWeChatRpaUpsert = req.method === 'session.wechat-rpa.channel.upsert' || req.method === 'manager.wechat-rpa.channel.upsert';
|
|
529
|
+
const isWeChatRpaSync = req.method === 'session.wechat-rpa.channel.sync' || req.method === 'manager.wechat-rpa.channel.sync';
|
|
530
|
+
const isWeChatRpaOutboundCancel = req.method === 'session.wechat-rpa.outbound.cancel' || req.method === 'manager.wechat-rpa.outbound.cancel';
|
|
531
|
+
if (req.method === 'manager.channel.get') {
|
|
532
|
+
appJson(runtime, req.id, true, {
|
|
533
|
+
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'websocket', { includeSecret: true }),
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (req.method === 'manager.channel.upsert') {
|
|
538
|
+
const channel = await this.channelRuntime.upsertManagerChannel({
|
|
539
|
+
id: String(body.id || `websocket:${managerSessionId}`),
|
|
540
|
+
managerSessionId,
|
|
541
|
+
sessionId: managerSessionId,
|
|
542
|
+
workDir: String(body.workDir || manager?.workDir || ''),
|
|
543
|
+
type: 'websocket',
|
|
544
|
+
name: typeof body.name === 'string' ? body.name : undefined,
|
|
545
|
+
agentType: typeof body.agentType === 'string' ? body.agentType : undefined,
|
|
546
|
+
agentSessionId: typeof body.agentSessionId === 'string' ? body.agentSessionId : null,
|
|
547
|
+
modelId: typeof body.modelId === 'string' ? body.modelId : null,
|
|
548
|
+
enabled: Boolean(body.enabled),
|
|
549
|
+
wsUrl: typeof body.wsUrl === 'string' ? body.wsUrl : undefined,
|
|
550
|
+
token: typeof body.token === 'string' ? body.token : undefined,
|
|
551
|
+
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
552
|
+
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
553
|
+
});
|
|
554
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
555
|
+
appJson(runtime, req.id, true, { channel });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (isWeChatRpaGet) {
|
|
559
|
+
appJson(runtime, req.id, true, {
|
|
560
|
+
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'wechat-rpa', { includeSecret: true }),
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (isWeChatRpaUpsert) {
|
|
565
|
+
const channel = await this.channelRuntime.upsertManagerWeChatRpaChannel({
|
|
566
|
+
id: String(body.id || `wechat-rpa:${managerSessionId}`),
|
|
567
|
+
managerSessionId,
|
|
568
|
+
sessionId: managerSessionId,
|
|
569
|
+
workDir: normalizeWorkDir(String(body.workDir || manager?.workDir || '')),
|
|
570
|
+
name: typeof body.name === 'string' ? body.name : undefined,
|
|
571
|
+
agentType: typeof body.agentType === 'string' ? body.agentType : undefined,
|
|
572
|
+
agentSessionId: typeof body.agentSessionId === 'string' ? body.agentSessionId : null,
|
|
573
|
+
modelId: typeof body.modelId === 'string' ? body.modelId : null,
|
|
574
|
+
enabled: Boolean(body.enabled),
|
|
575
|
+
groups: parseWeChatRpaGroups(body.groups),
|
|
576
|
+
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
577
|
+
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
578
|
+
source: parseWeChatRpaSource(body.source),
|
|
579
|
+
pollIntervalMs: optionalNumber(body.pollIntervalMs),
|
|
580
|
+
recentLimit: optionalNumber(body.recentLimit),
|
|
581
|
+
idleSeconds: optionalNumber(body.idleSeconds),
|
|
582
|
+
forceForeground: body.forceForeground === undefined ? undefined : Boolean(body.forceForeground),
|
|
583
|
+
noRestore: body.noRestore === undefined ? undefined : Boolean(body.noRestore),
|
|
584
|
+
downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
|
|
585
|
+
downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
|
|
586
|
+
selfNickname: typeof body.selfNickname === 'string' ? body.selfNickname : undefined,
|
|
587
|
+
selfTriggerMarker: typeof body.selfTriggerMarker === 'string' ? body.selfTriggerMarker : undefined,
|
|
588
|
+
deferInitialPoll: body.deferInitialPoll === undefined ? undefined : Boolean(body.deferInitialPoll),
|
|
589
|
+
privacyConsentAccepted: body.privacyConsentAccepted === undefined ? undefined : Boolean(body.privacyConsentAccepted),
|
|
590
|
+
flowScriptPath: typeof body.flowScriptPath === 'string' ? body.flowScriptPath : undefined,
|
|
591
|
+
});
|
|
592
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
593
|
+
appJson(runtime, req.id, true, { channel });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (isWeChatRpaSync) {
|
|
597
|
+
const result = await this.channelRuntime.syncManagerWeChatRpaChannel(managerSessionId);
|
|
598
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
599
|
+
appJson(runtime, req.id, true, result);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (isWeChatRpaOutboundCancel) {
|
|
603
|
+
const result = await this.channelRuntime.cancelManagerWeChatRpaOutbound({
|
|
604
|
+
managerSessionId,
|
|
605
|
+
idempotencyKey: typeof body.idempotencyKey === 'string' ? body.idempotencyKey : null,
|
|
606
|
+
replyId: typeof body.replyId === 'string' ? body.replyId : null,
|
|
607
|
+
reason: typeof body.reason === 'string' ? body.reason : 'user_cancelled',
|
|
608
|
+
});
|
|
609
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
610
|
+
appJson(runtime, req.id, true, result);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
throw new Error(`Unsupported manager app method: ${req.method}`);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
appJson(runtime, req.id, false, { error: error instanceof Error ? error.message : String(error) });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
broadcastConfiguredChannelStatuses() {
|
|
620
|
+
for (const entry of this.channelRuntime.listManagerChannelStatuses()) {
|
|
621
|
+
this.broadcastManagerChannelStatus(entry.managerSessionId);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
broadcastManagerChannelStatus(managerSessionId) {
|
|
625
|
+
const runtime = this.opts.getRuntime();
|
|
626
|
+
if (!runtime.client?.sendEvent)
|
|
627
|
+
return;
|
|
628
|
+
const manager = this.registry.getManager(managerSessionId);
|
|
629
|
+
const status = this.channelRuntime.getManagerChannelStatus(managerSessionId);
|
|
630
|
+
const channel = this.channelRuntime.getManagerChannel(managerSessionId, 'wechat-rpa')
|
|
631
|
+
?? this.channelRuntime.getManagerChannel(managerSessionId, 'websocket');
|
|
632
|
+
runtime.client.sendEvent({
|
|
633
|
+
type: 'event',
|
|
634
|
+
event: 'session.update',
|
|
635
|
+
payload: {
|
|
636
|
+
session: {
|
|
637
|
+
id: managerSessionId,
|
|
638
|
+
agentType: manager ? 'manager' : channel?.agentType,
|
|
639
|
+
agentSessionId: manager?.agentSessionId ?? channel?.agentSessionId ?? null,
|
|
640
|
+
modelId: manager?.modelId ?? channel?.modelId ?? null,
|
|
641
|
+
workDir: manager?.workDir ?? channel?.workDir,
|
|
642
|
+
externalChannel: status,
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async handleIpc(req, res) {
|
|
648
|
+
if (req.headers.authorization !== `Bearer ${this.ipcToken}`) {
|
|
649
|
+
json(res, 401, { ok: false, error: 'Unauthorized' });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
654
|
+
const body = await readJson(req);
|
|
655
|
+
if (url.pathname.startsWith('/channel/') || url.pathname.startsWith('/external/') || url.pathname.startsWith('/wechat-rpa/')) {
|
|
656
|
+
json(res, 410, { ok: false, error: 'feature_retired' });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const managerSessionId = String(body.managerSessionId || req.headers['x-shennian-manager-session-id'] || '');
|
|
660
|
+
if (!managerSessionId)
|
|
661
|
+
throw new Error('managerSessionId is required');
|
|
662
|
+
const manager = this.registry.getManager(managerSessionId);
|
|
663
|
+
if (url.pathname === '/sessions/list') {
|
|
664
|
+
if (!manager)
|
|
665
|
+
throw new Error('Manager runtime is not registered');
|
|
666
|
+
const runningSessionIds = new Set(this.opts.getRuntime().sessions.keys());
|
|
667
|
+
json(res, 200, {
|
|
668
|
+
ok: true,
|
|
669
|
+
sessions: this.registry.listWorkers(managerSessionId, { runningSessionIds }),
|
|
670
|
+
});
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (url.pathname === '/sessions/start') {
|
|
674
|
+
if (!manager)
|
|
675
|
+
throw new Error('Manager runtime is not registered');
|
|
676
|
+
const agentType = String(body.agentType || body.agent || manager.defaultWorkerAgentType || 'codex');
|
|
677
|
+
if (!isManagerWorkerAgentType(agentType)) {
|
|
678
|
+
throw new Error(`Unsupported manager worker agent: ${agentType}`);
|
|
679
|
+
}
|
|
680
|
+
const workDir = normalizeWorkDir(String(body.workDir || manager.workDir));
|
|
681
|
+
if (workDir !== manager.workDir)
|
|
682
|
+
throw new Error('Manager can only start workers in the same workDir');
|
|
683
|
+
const message = String(body.message || '');
|
|
684
|
+
if (!message)
|
|
685
|
+
throw new Error('message is required');
|
|
686
|
+
const worker = this.registry.addWorker({
|
|
687
|
+
managerSessionId,
|
|
688
|
+
agentType,
|
|
689
|
+
workDir,
|
|
690
|
+
summary: message.slice(0, 120),
|
|
691
|
+
});
|
|
692
|
+
const workerModelId = String(body.modelId || (agentType === manager.defaultWorkerAgentType ? manager.defaultWorkerModelId ?? '' : ''));
|
|
693
|
+
await this.dispatchChatSend(worker.sessionId, agentType, workDir, message, null, workerModelId, managerSessionId);
|
|
694
|
+
json(res, 200, { ok: true, session: worker });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (url.pathname === '/sessions/send') {
|
|
698
|
+
const sessionId = String(body.sessionId || '');
|
|
699
|
+
const message = String(body.message || '');
|
|
700
|
+
const enqueue = body.enqueue === undefined ? true : Boolean(body.enqueue);
|
|
701
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
702
|
+
if (!worker)
|
|
703
|
+
throw new Error('Worker not found in this manager scope');
|
|
704
|
+
const workerModelId = String(body.modelId || (worker.agentType === manager?.defaultWorkerAgentType ? manager.defaultWorkerModelId ?? '' : ''));
|
|
705
|
+
if (enqueue) {
|
|
706
|
+
await this.dispatchChatEnqueue(worker.sessionId, worker.agentType, worker.workDir, message, worker.agentSessionId ?? null, workerModelId, managerSessionId);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
await this.dispatchChatSend(worker.sessionId, worker.agentType, worker.workDir, message, worker.agentSessionId ?? null, workerModelId, managerSessionId);
|
|
710
|
+
}
|
|
711
|
+
json(res, 200, { ok: true });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (url.pathname === '/sessions/queue') {
|
|
715
|
+
const sessionId = String(body.sessionId || '');
|
|
716
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
717
|
+
if (!worker)
|
|
718
|
+
throw new Error('Worker not found in this manager scope');
|
|
719
|
+
const queue = this.opts.getRuntime().chatQueue?.getSnapshot(sessionId);
|
|
720
|
+
json(res, 200, { ok: true, queue });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (url.pathname === '/sessions/queue/edit') {
|
|
724
|
+
const sessionId = String(body.sessionId || '');
|
|
725
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
726
|
+
if (!worker)
|
|
727
|
+
throw new Error('Worker not found in this manager scope');
|
|
728
|
+
await this.opts.dispatchReq({
|
|
729
|
+
type: 'req',
|
|
730
|
+
id: `manager-queue-edit-${randomUUID()}`,
|
|
731
|
+
method: 'chat.queue.edit',
|
|
732
|
+
params: {
|
|
733
|
+
sessionId,
|
|
734
|
+
queueMessageId: String(body.queueMessageId || body.messageId || ''),
|
|
735
|
+
text: String(body.message || body.text || ''),
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
json(res, 200, { ok: true, queue: this.opts.getRuntime().chatQueue?.getSnapshot(sessionId) });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (url.pathname === '/sessions/queue/delete') {
|
|
742
|
+
const sessionId = String(body.sessionId || '');
|
|
743
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
744
|
+
if (!worker)
|
|
745
|
+
throw new Error('Worker not found in this manager scope');
|
|
746
|
+
await this.opts.dispatchReq({
|
|
747
|
+
type: 'req',
|
|
748
|
+
id: `manager-queue-delete-${randomUUID()}`,
|
|
749
|
+
method: 'chat.queue.delete',
|
|
750
|
+
params: {
|
|
751
|
+
sessionId,
|
|
752
|
+
queueMessageId: String(body.queueMessageId || body.messageId || ''),
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
json(res, 200, { ok: true, queue: this.opts.getRuntime().chatQueue?.getSnapshot(sessionId) });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (url.pathname === '/sessions/stop' || url.pathname === '/sessions/terminate') {
|
|
759
|
+
const sessionId = String(body.sessionId || '');
|
|
760
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
761
|
+
if (!worker)
|
|
762
|
+
throw new Error('Worker not found in this manager scope');
|
|
763
|
+
await this.opts.dispatchReq({ type: 'req', id: `manager-abort-${randomUUID()}`, method: 'chat.abort', params: { sessionId } });
|
|
764
|
+
this.registry.updateWorker(sessionId, { status: 'aborted' });
|
|
765
|
+
json(res, 200, { ok: true });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (url.pathname === '/sessions/read') {
|
|
769
|
+
const sessionId = String(body.sessionId || '');
|
|
770
|
+
const limit = Number(body.limit || 200);
|
|
771
|
+
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
772
|
+
if (!worker)
|
|
773
|
+
throw new Error('Worker not found in this manager scope');
|
|
774
|
+
const rawMessages = readMessages(sessionId, { limit: Math.max(limit * 20, limit) });
|
|
775
|
+
json(res, 200, {
|
|
776
|
+
ok: true,
|
|
777
|
+
messages: compactWorkerTranscript(rawMessages, limit),
|
|
778
|
+
rawMessageCount: rawMessages.length,
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (url.pathname === '/memory/path') {
|
|
783
|
+
if (!manager)
|
|
784
|
+
throw new Error('Manager runtime is not registered');
|
|
785
|
+
json(res, 200, { ok: true, path: path.join(manager.workDir, '.shennian') });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (url.pathname === '/external/reply') {
|
|
789
|
+
const replyTarget = typeof body.replyTarget === 'string'
|
|
790
|
+
? this.registry.getReplyTarget(body.replyTarget)
|
|
791
|
+
: this.registry.getLatestReplyTargetForManager(managerSessionId);
|
|
792
|
+
const text = String(body.text || '');
|
|
793
|
+
const attachment = parseExternalReplyAttachment(body.attachment);
|
|
794
|
+
const idempotencyKey = String(body.idempotencyKey || randomUUID());
|
|
795
|
+
const explicitChannelId = String(body.channelId || '');
|
|
796
|
+
const explicitConversationId = String(body.conversationId || '');
|
|
797
|
+
const defaultTarget = !replyTarget && (!explicitChannelId || !explicitConversationId)
|
|
798
|
+
? await this.channelRuntime.getDefaultReplyTarget(managerSessionId).catch(() => null)
|
|
799
|
+
: null;
|
|
800
|
+
const channelId = replyTarget?.channelId || explicitChannelId || defaultTarget?.channelId || '';
|
|
801
|
+
const conversationId = replyTarget?.conversationId || explicitConversationId || defaultTarget?.conversationId || '';
|
|
802
|
+
const hitChannel = channelId ? this.channelRuntime.getChannelById(channelId) : undefined;
|
|
803
|
+
// sync 镜像出来的 wechat-rpa 条目只用于起接收侧轮询 timer;发送仍然优先走 tryDirectWeChatRpaReply
|
|
804
|
+
// bypass,保持和老 daemon 一致的行为。只有调用方带了明确 replyTarget/channelId 时才走 channel.reply。
|
|
805
|
+
const hasExplicitTarget = Boolean(replyTarget || explicitChannelId);
|
|
806
|
+
const shouldUseChannel = hitChannel && (hasExplicitTarget || hitChannel.managedBy !== 'session-sync');
|
|
807
|
+
if (channelId && shouldUseChannel) {
|
|
808
|
+
if (!conversationId)
|
|
809
|
+
throw new Error('No external channel target is available for this Manager');
|
|
810
|
+
const result = await this.channelRuntime.reply({
|
|
811
|
+
managerSessionId,
|
|
812
|
+
channelId,
|
|
813
|
+
conversationId,
|
|
814
|
+
messageId: replyTarget?.messageId ?? undefined,
|
|
815
|
+
text,
|
|
816
|
+
attachment,
|
|
817
|
+
idempotencyKey,
|
|
818
|
+
});
|
|
819
|
+
json(res, result.ok ? 200 : 400, result);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const wechatRpaResult = await this.tryDirectWeChatRpaReply(managerSessionId, text, attachment);
|
|
823
|
+
if (wechatRpaResult) {
|
|
824
|
+
json(res, wechatRpaResult.ok ? 200 : 400, wechatRpaResult);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
let relayResult;
|
|
828
|
+
try {
|
|
829
|
+
relayResult = await this.sendManagedWeComReply({
|
|
830
|
+
managerSessionId,
|
|
831
|
+
text,
|
|
832
|
+
attachment,
|
|
833
|
+
idempotencyKey,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
838
|
+
if (!shouldFallbackToLocalChannel(message))
|
|
839
|
+
throw error;
|
|
840
|
+
relayResult = { ok: false, error: message };
|
|
841
|
+
}
|
|
842
|
+
if (relayResult.ok) {
|
|
843
|
+
json(res, 200, { ok: true, payload: relayResult.payload });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (!shouldFallbackToLocalChannel(relayResult.error || '') || !channelId || !conversationId) {
|
|
847
|
+
json(res, 400, { ok: false, error: relayResult.error || 'External send failed' });
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
json(res, 400, { ok: false, error: `No local external channel is configured for ${channelId}` });
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (url.pathname === '/channel/get') {
|
|
854
|
+
json(res, 200, {
|
|
855
|
+
ok: true,
|
|
856
|
+
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'websocket'),
|
|
857
|
+
});
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (url.pathname === '/channel/upsert') {
|
|
861
|
+
if (!manager)
|
|
862
|
+
throw new Error('Manager runtime is not registered');
|
|
863
|
+
const channel = await this.channelRuntime.upsertManagerChannel({
|
|
864
|
+
id: String(body.id || `websocket:${managerSessionId}`),
|
|
865
|
+
managerSessionId,
|
|
866
|
+
workDir: manager.workDir,
|
|
867
|
+
type: 'websocket',
|
|
868
|
+
name: typeof body.name === 'string' ? body.name : undefined,
|
|
869
|
+
enabled: Boolean(body.enabled),
|
|
870
|
+
wsUrl: typeof body.wsUrl === 'string' ? body.wsUrl : undefined,
|
|
871
|
+
token: typeof body.token === 'string' ? body.token : undefined,
|
|
872
|
+
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
873
|
+
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
874
|
+
});
|
|
875
|
+
this.registry.upsertManager({
|
|
876
|
+
...manager,
|
|
877
|
+
status: manager.status,
|
|
878
|
+
});
|
|
879
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
880
|
+
json(res, 200, { ok: true, channel });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (url.pathname === '/wechat-rpa/tool/read') {
|
|
884
|
+
const binding = parseWeChatToolBindingFromIpc(body);
|
|
885
|
+
const limit = optionalNumber(body.limit) ?? 10;
|
|
886
|
+
const traceId = typeof body.traceId === 'string' ? body.traceId : undefined;
|
|
887
|
+
let result;
|
|
888
|
+
try {
|
|
889
|
+
result = await this.weChatAutomationLane.run(`wechat-tool:read:${binding.channelId}`, () => readDirectWeChatLatestOnce(binding, {
|
|
890
|
+
conversation: binding.conversationName,
|
|
891
|
+
workDir: binding.workDir,
|
|
892
|
+
sessionId: binding.sessionId,
|
|
893
|
+
limit,
|
|
894
|
+
recentLimit: limit,
|
|
895
|
+
download: body.download === 'never' ? 'never' : 'auto',
|
|
896
|
+
traceId,
|
|
897
|
+
timeoutMs: optionalNumber(body.timeoutMs),
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
json(res, 400, { ok: false, ...wechatToolErrorPayload(error, traceId) });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
json(res, 200, {
|
|
905
|
+
ok: true,
|
|
906
|
+
messages: result.messages,
|
|
907
|
+
outDir: result.outDir,
|
|
908
|
+
helperTracePath: result.helperTracePath,
|
|
909
|
+
activityGuardPath: result.activityGuardPath,
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (url.pathname === '/wechat-rpa/tool/send') {
|
|
914
|
+
const binding = parseWeChatToolBindingFromIpc(body);
|
|
915
|
+
const text = String(body.text || '');
|
|
916
|
+
const attachment = parseExternalReplyAttachment(body.attachment);
|
|
917
|
+
const traceId = typeof body.traceId === 'string' ? body.traceId : undefined;
|
|
918
|
+
let result;
|
|
919
|
+
try {
|
|
920
|
+
result = await this.weChatAutomationLane.run(`wechat-tool:send:${binding.channelId}`, () => sendDirectWeChatMessageOnce(binding, {
|
|
921
|
+
conversation: binding.conversationName,
|
|
922
|
+
workDir: binding.workDir,
|
|
923
|
+
sessionId: binding.sessionId,
|
|
924
|
+
text,
|
|
925
|
+
attachment,
|
|
926
|
+
traceId,
|
|
927
|
+
timeoutMs: optionalNumber(body.timeoutMs),
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
json(res, 400, { ok: false, ...wechatToolErrorPayload(error, traceId) });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
json(res, 200, { ok: true, ...result });
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (url.pathname === '/wechat-rpa/channel/get') {
|
|
938
|
+
json(res, 200, {
|
|
939
|
+
ok: true,
|
|
940
|
+
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'wechat-rpa', { includeSecret: true }),
|
|
941
|
+
});
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (url.pathname === '/wechat-rpa/channel/upsert') {
|
|
945
|
+
const workDir = normalizeWorkDir(String(body.workDir || manager?.workDir || ''));
|
|
946
|
+
if (!workDir)
|
|
947
|
+
throw new Error('workDir is required');
|
|
948
|
+
const channel = await this.channelRuntime.upsertManagerWeChatRpaChannel({
|
|
949
|
+
id: String(body.id || `wechat-rpa:${managerSessionId}`),
|
|
950
|
+
managerSessionId,
|
|
951
|
+
sessionId: managerSessionId,
|
|
952
|
+
workDir,
|
|
953
|
+
name: typeof body.name === 'string' ? body.name : undefined,
|
|
954
|
+
agentType: typeof body.agentType === 'string' ? body.agentType : undefined,
|
|
955
|
+
agentSessionId: typeof body.agentSessionId === 'string' ? body.agentSessionId : null,
|
|
956
|
+
modelId: typeof body.modelId === 'string' ? body.modelId : null,
|
|
957
|
+
enabled: Boolean(body.enabled),
|
|
958
|
+
groups: parseWeChatRpaGroups(body.groups),
|
|
959
|
+
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
960
|
+
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
961
|
+
source: parseWeChatRpaSource(body.source),
|
|
962
|
+
pollIntervalMs: optionalNumber(body.pollIntervalMs),
|
|
963
|
+
recentLimit: optionalNumber(body.recentLimit),
|
|
964
|
+
idleSeconds: optionalNumber(body.idleSeconds),
|
|
965
|
+
forceForeground: body.forceForeground === undefined ? undefined : Boolean(body.forceForeground),
|
|
966
|
+
noRestore: body.noRestore === undefined ? undefined : Boolean(body.noRestore),
|
|
967
|
+
downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
|
|
968
|
+
downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
|
|
969
|
+
selfNickname: typeof body.selfNickname === 'string' ? body.selfNickname : undefined,
|
|
970
|
+
selfTriggerMarker: typeof body.selfTriggerMarker === 'string' ? body.selfTriggerMarker : undefined,
|
|
971
|
+
deferInitialPoll: body.deferInitialPoll === undefined ? undefined : Boolean(body.deferInitialPoll),
|
|
972
|
+
privacyConsentAccepted: body.privacyConsentAccepted === undefined ? undefined : Boolean(body.privacyConsentAccepted),
|
|
973
|
+
flowScriptPath: typeof body.flowScriptPath === 'string' ? body.flowScriptPath : undefined,
|
|
974
|
+
});
|
|
975
|
+
if (manager) {
|
|
976
|
+
this.registry.upsertManager({
|
|
977
|
+
...manager,
|
|
978
|
+
status: manager.status,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
982
|
+
json(res, 200, { ok: true, channel });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (url.pathname === '/wechat-rpa/channel/sync') {
|
|
986
|
+
const { channel, messages } = await this.channelRuntime.syncManagerWeChatRpaChannel(managerSessionId);
|
|
987
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
988
|
+
json(res, 200, { ok: true, channel, messages });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (url.pathname === '/wechat-rpa/outbound/cancel') {
|
|
992
|
+
const result = await this.channelRuntime.cancelManagerWeChatRpaOutbound({
|
|
993
|
+
managerSessionId,
|
|
994
|
+
idempotencyKey: typeof body.idempotencyKey === 'string' ? body.idempotencyKey : null,
|
|
995
|
+
replyId: typeof body.replyId === 'string' ? body.replyId : null,
|
|
996
|
+
reason: typeof body.reason === 'string' ? body.reason : 'user_cancelled',
|
|
997
|
+
});
|
|
998
|
+
this.broadcastManagerChannelStatus(managerSessionId);
|
|
999
|
+
json(res, 200, { ok: true, ...result });
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
json(res, 404, { ok: false, error: `Unknown manager IPC path: ${url.pathname}` });
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
json(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async dispatchChatSend(sessionId, agentType, workDir, text, agentSessionId, modelId, managedWorkerParentSessionId) {
|
|
1009
|
+
await this.opts.dispatchReq({
|
|
1010
|
+
type: 'req',
|
|
1011
|
+
id: `manager-send-${randomUUID()}`,
|
|
1012
|
+
method: 'chat.send',
|
|
1013
|
+
params: {
|
|
1014
|
+
...getPersonalSyncIdentity(),
|
|
1015
|
+
sessionId, text, agentType, workDir, agentSessionId, modelId, managedWorkerParentSessionId,
|
|
1016
|
+
},
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
async dispatchChatEnqueue(sessionId, agentType, workDir, text, agentSessionId, modelId, managedWorkerParentSessionId) {
|
|
1020
|
+
await this.opts.dispatchReq({
|
|
1021
|
+
type: 'req',
|
|
1022
|
+
id: `manager-enqueue-${randomUUID()}`,
|
|
1023
|
+
method: 'chat.enqueue',
|
|
1024
|
+
params: {
|
|
1025
|
+
...getPersonalSyncIdentity(),
|
|
1026
|
+
sessionId, text, agentType, workDir, agentSessionId, modelId, managedWorkerParentSessionId,
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
async tryDirectWeChatRpaReply(sessionId, text, attachment) {
|
|
1031
|
+
const active = this.opts.getRuntime().sessions.get(sessionId);
|
|
1032
|
+
const channel = active?.externalChannel;
|
|
1033
|
+
if (!channel || channel.type !== 'wechat-rpa' || !channel.channelId || !channel.name)
|
|
1034
|
+
return null;
|
|
1035
|
+
const localMachineId = process.env.SHENNIAN_MACHINE_ID || loadConfig().machineId || '';
|
|
1036
|
+
if (channel.machineId && localMachineId && channel.machineId !== localMachineId) {
|
|
1037
|
+
return { ok: false, error: `WeChat 绑定属于其他机器 (${channel.machineId}),请在绑定机器上发送。` };
|
|
1038
|
+
}
|
|
1039
|
+
if (!text.trim() && !attachment)
|
|
1040
|
+
return { ok: false, error: 'text or attachment is required' };
|
|
1041
|
+
const workDir = active?.workDir || process.cwd();
|
|
1042
|
+
const binding = {
|
|
1043
|
+
sessionId,
|
|
1044
|
+
channelId: channel.channelId,
|
|
1045
|
+
conversationId: weChatChannelConversationId(channel.name),
|
|
1046
|
+
conversationName: channel.name,
|
|
1047
|
+
workDir,
|
|
1048
|
+
};
|
|
1049
|
+
try {
|
|
1050
|
+
const result = await this.weChatAutomationLane.run(`wechat-tool:send:${binding.channelId}`, () => sendDirectWeChatMessageOnce(binding, {
|
|
1051
|
+
conversation: binding.conversationName,
|
|
1052
|
+
workDir: binding.workDir,
|
|
1053
|
+
sessionId: binding.sessionId,
|
|
1054
|
+
text,
|
|
1055
|
+
attachment,
|
|
1056
|
+
}));
|
|
1057
|
+
return { ok: true, payload: result };
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1061
|
+
return { ok: false, error: message };
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async sendManagedWeComReply(input) {
|
|
1065
|
+
const parts = splitExternalReplyText(input.text);
|
|
1066
|
+
if (!parts.length && !input.attachment)
|
|
1067
|
+
return { ok: false, error: 'text or attachment is required' };
|
|
1068
|
+
const client = this.opts.getRuntime().client;
|
|
1069
|
+
if (!client || typeof client.sendReq !== 'function') {
|
|
1070
|
+
return { ok: false, error: 'Relay is not connected' };
|
|
1071
|
+
}
|
|
1072
|
+
const payloads = [];
|
|
1073
|
+
for (const [index, text] of parts.entries()) {
|
|
1074
|
+
const frame = await client.sendReq({
|
|
1075
|
+
type: 'req',
|
|
1076
|
+
id: `external-send-${randomUUID()}`,
|
|
1077
|
+
method: 'external.send',
|
|
1078
|
+
params: {
|
|
1079
|
+
managerSessionId: input.managerSessionId,
|
|
1080
|
+
text,
|
|
1081
|
+
idempotencyKey: parts.length > 1 ? `${input.idempotencyKey}:${index + 1}` : input.idempotencyKey,
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
if (!frame.ok)
|
|
1085
|
+
return { ok: false, error: frame.error || 'External send failed' };
|
|
1086
|
+
payloads.push(frame.payload);
|
|
1087
|
+
}
|
|
1088
|
+
if (input.attachment) {
|
|
1089
|
+
const frame = await client.sendReq({
|
|
1090
|
+
type: 'req',
|
|
1091
|
+
id: `external-send-${randomUUID()}`,
|
|
1092
|
+
method: 'external.send',
|
|
1093
|
+
params: {
|
|
1094
|
+
managerSessionId: input.managerSessionId,
|
|
1095
|
+
attachment: input.attachment,
|
|
1096
|
+
idempotencyKey: parts.length ? `${input.idempotencyKey}:attachment` : input.idempotencyKey,
|
|
1097
|
+
},
|
|
1098
|
+
});
|
|
1099
|
+
if (!frame.ok)
|
|
1100
|
+
return { ok: false, error: frame.error || 'External send failed' };
|
|
1101
|
+
payloads.push(frame.payload);
|
|
1102
|
+
}
|
|
1103
|
+
return { ok: true, payload: payloads.length === 1 ? payloads[0] : payloads };
|
|
1104
|
+
}
|
|
1105
|
+
enqueueWorkerEvent(managerSessionId, worker, state, runId, message) {
|
|
1106
|
+
const event = this.registry.enqueueInboxEvent({
|
|
1107
|
+
managerSessionId,
|
|
1108
|
+
workerSessionId: worker.sessionId,
|
|
1109
|
+
kind: state === 'final' ? 'worker.final' : state === 'error' ? 'worker.error' : 'worker.aborted',
|
|
1110
|
+
priority: 'normal',
|
|
1111
|
+
runId: runId ?? null,
|
|
1112
|
+
summary: message || worker.summary || '(无可见摘要)',
|
|
1113
|
+
});
|
|
1114
|
+
if (event)
|
|
1115
|
+
void this.drainManagerInbox(managerSessionId);
|
|
1116
|
+
}
|
|
1117
|
+
handleExternalMessage(managerSessionId, event) {
|
|
1118
|
+
const config = this.channelRuntime.getChannelById(event.channelId)
|
|
1119
|
+
?? this.channelRuntime.getManagerChannel(managerSessionId, event.channelType);
|
|
1120
|
+
const externalChannel = this.channelRuntime.getChannelStatusById(event.channelId)
|
|
1121
|
+
?? this.channelRuntime.getManagerChannelStatus(managerSessionId);
|
|
1122
|
+
const manager = this.registry.getManager(managerSessionId);
|
|
1123
|
+
const agentType = (config?.agentType || (manager ? 'manager' : 'codex'));
|
|
1124
|
+
const workDir = config?.workDir || manager?.workDir || process.cwd();
|
|
1125
|
+
const agentSessionId = config?.agentSessionId ?? manager?.agentSessionId ?? null;
|
|
1126
|
+
const modelId = config?.modelId || manager?.modelId || '';
|
|
1127
|
+
const attachmentInputs = externalAttachmentsForAgent(event.attachments, event.channelType);
|
|
1128
|
+
const visibleMessage = formatExternalMessageForAgent(event);
|
|
1129
|
+
void this.dispatchExternalMessage({
|
|
1130
|
+
sessionId: managerSessionId,
|
|
1131
|
+
agentType,
|
|
1132
|
+
workDir,
|
|
1133
|
+
agentSessionId,
|
|
1134
|
+
modelId,
|
|
1135
|
+
text: visibleMessage,
|
|
1136
|
+
messageId: event.messageId,
|
|
1137
|
+
attachments: attachmentInputs,
|
|
1138
|
+
externalChannel: externalChannelForChatEnqueue(externalChannel),
|
|
1139
|
+
replyTarget: event.replyTarget,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
async dispatchExternalMessage(input) {
|
|
1143
|
+
const externalMessageId = typeof input.messageId === 'string' && input.messageId.trim()
|
|
1144
|
+
? input.messageId.trim()
|
|
1145
|
+
: undefined;
|
|
1146
|
+
await this.opts.dispatchReq({
|
|
1147
|
+
type: 'req',
|
|
1148
|
+
id: `external-enqueue-${randomUUID()}`,
|
|
1149
|
+
method: 'chat.enqueue',
|
|
1150
|
+
params: {
|
|
1151
|
+
...getPersonalSyncIdentity(),
|
|
1152
|
+
sessionId: input.sessionId,
|
|
1153
|
+
text: input.text,
|
|
1154
|
+
agentType: input.agentType,
|
|
1155
|
+
workDir: input.workDir,
|
|
1156
|
+
agentSessionId: input.agentSessionId,
|
|
1157
|
+
modelId: input.modelId,
|
|
1158
|
+
origin: 'external',
|
|
1159
|
+
queueMessageId: externalMessageId,
|
|
1160
|
+
clientMessageId: externalMessageId,
|
|
1161
|
+
attachments: input.attachments,
|
|
1162
|
+
externalChannel: input.externalChannel ?? null,
|
|
1163
|
+
replyTarget: input.replyTarget,
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
scanWorkerHealth() {
|
|
1168
|
+
const now = Date.now();
|
|
1169
|
+
const registry = this.registry.load();
|
|
1170
|
+
for (const worker of Object.values(registry.workers)) {
|
|
1171
|
+
if (worker.status !== 'running')
|
|
1172
|
+
continue;
|
|
1173
|
+
const ageMs = now - Date.parse(worker.createdAt);
|
|
1174
|
+
if (ageMs < 10 * 60_000)
|
|
1175
|
+
continue;
|
|
1176
|
+
const lastHealthMs = worker.healthNotifiedAt ? Date.parse(worker.healthNotifiedAt) : 0;
|
|
1177
|
+
if (lastHealthMs && now - lastHealthMs < 10 * 60_000)
|
|
1178
|
+
continue;
|
|
1179
|
+
const manager = registry.managers[worker.managedBy];
|
|
1180
|
+
if (!manager)
|
|
1181
|
+
continue;
|
|
1182
|
+
const inactiveMinutes = Math.max(0, Math.floor((now - Date.parse(worker.lastActivityAt)) / 60_000));
|
|
1183
|
+
const prompt = `已运行 ${Math.floor(ageMs / 60_000)} 分钟,尚未结束。
|
|
5
1184
|
|
|
6
|
-
|
|
1185
|
+
当前可见进展:
|
|
1186
|
+
- 最近文本摘要:${worker.summary || '(无可见摘要)'}
|
|
1187
|
+
- 最近活动时间:${worker.lastActivityAt}
|
|
1188
|
+
- 最近 ${inactiveMinutes} 分钟没有新活动。
|
|
7
1189
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
\u8BF7\u7ED3\u5408\u7528\u6237\u76EE\u6807\u51B3\u5B9A\u4E0B\u4E00\u6B65\uFF1A\u7EE7\u7EED\u7B49\u5F85\u3001\u521B\u5EFA\u6216\u6307\u6D3E worker\u3001\u505C\u6B62 worker\u3001\u8BE2\u95EE\u7528\u6237\uFF0C\u6216\u5411\u7528\u6237\u6C47\u62A5\u3002`}function ke(a){try{const e=JSON.parse(a),n=e.type==="tool_result"||e.result?"tool_result":"tool_call",t=e.name||"tool",r=typeof e.result=="string"?e.result.replace(/\s+/g," ").trim():"",s=r.length>220?`${r.slice(0,220)}...`:r;return s?`[${n}] ${t}: ${s}`:`[${n}] ${t}`}catch{return"[tool]"}}function q(a){if(!a||typeof a!="object")return;const e=a,n=String(e.kind||""),t=String(e.name||""),r=String(e.mimeType||""),s=String(e.dataBase64||""),o=String(e.localPath||""),i=String(e.url||""),c=Number(e.size||0);if(s)throw new Error("Manager IPC external attachments must use localPath or url; dataBase64 is not accepted");if(!(n!=="image"&&n!=="video"&&n!=="file")&&!(!t||!r||!Number.isFinite(c)||c<0)&&!(!o&&!i))return{kind:n,name:t,mimeType:r,size:c,...o?{localPath:o}:{},...i?{url:i}:{}}}function L(a){const e=a.binding&&typeof a.binding=="object"&&!Array.isArray(a.binding)?a.binding:{},n=String(e.sessionId||a.managerSessionId||"").trim(),t=String(e.channelId||"").trim(),r=String(e.conversationId||"").trim(),s=String(e.conversationName||a.conversation||"").trim(),o=String(e.workDir||a.workDir||"").trim();if(!n)throw new Error("WeChat tool sessionId is required");if(!t)throw new Error("WeChat tool channelId is required");if(!r)throw new Error("WeChat tool conversationId is required");if(!s)throw new Error("WeChat tool conversationName is required");if(!o)throw new Error("WeChat tool workDir is required");return{sessionId:n,channelId:t,conversationId:r,conversationName:s,workDir:o}}function Se(a,e){const n=[...a].sort((l,d)=>l.ts-d.ts),t=[];let r=null,s=null,o=null,i="";const c=()=>{if(!r)return;const l=i.trim();l&&t.push({...r,id:`${r.id}-compact`,payload:l}),r=null,s=null,o=null,i=""};for(const l of n){if(ne(l.payload)){c();continue}if(l.role==="user"){c(),t.push(l);continue}if(re(l.payload)){c(),t.push({...l,payload:ke(l.payload)});continue}const d=X(l.payload);if(!d.trim())continue;const h=fe(l.id),g=we(l.id);r&&r.role===l.role&&s===h&&h&&g!==null&&o!==null&&g===o+1?(i+=d,r.ts=l.ts,o=g):(c(),r=l,s=h,o=g,i=d)}return c(),t.slice(-e).sort((l,d)=>d.ts-l.ts)}async function Re(a){const e=[];let n=0;for await(const r of a){const s=Buffer.from(r);if(n+=s.byteLength,Number.isFinite(C)&&C>0&&n>C)throw new Error(`Manager IPC request body is too large. Max: ${C} bytes.`);e.push(s)}const t=Buffer.concat(e).toString("utf-8");return t?JSON.parse(t):{}}function f(a,e,n,t){a.client.sendRes({type:"res",id:e,ok:n,...n?{payload:t}:{error:String(t.error||"unknown error")}})}function F(a){return/binding not found|unknown method|not supported|relay is not connected|no external channel/i.test(a)}function O(){return Me()?w.join(b.tmpdir(),"shennian-vitest-runtime",String(process.pid),"manager-ipc.json"):pe("runtime","manager-ipc.json")}function Me(){return(process.env.VITEST==="true"||!!process.env.VITEST_WORKER_ID)&&!process.env.SHENNIAN_HOME?.trim()}class Ve{opts;registry=new ae;channelRuntime;weChatRpaSessionSync;server=null;ipcUrl=null;ipcToken=V(24).toString("hex");healthTimer=null;startPromise=null;workerTextAcc=new Map;weChatAutomationLane;constructor(e){this.opts=e,this.weChatAutomationLane=e.weChatAutomationLane??le(),this.channelRuntime=e.channelRuntime??new oe((n,t)=>{this.handleExternalMessage(n,t)},n=>this.registry.createReplyTarget(n).replyTarget,{createWeChatRpaProductRunner:e.createWeChatRpaProductRunner,weChatAutomationLane:this.weChatAutomationLane}),this.weChatRpaSessionSync=new ce({channelRuntime:this.channelRuntime,getLocalMachineId:()=>process.env.SHENNIAN_MACHINE_ID||N().machineId||"",listSessions:()=>this.listWeChatRpaSessionContexts(),listAuthoritativeSessions:()=>this.listAuthoritativeWeChatRpaSessionContexts()})}*listWeChatRpaSessionContexts(){const e=this.opts.getRuntime().sessions;for(const[n,t]of e)yield{sessionId:n,workDir:t.workDir,agentType:t.agentType,agentSessionId:t.agentSessionId,externalChannel:t.externalChannel??null}}async listAuthoritativeWeChatRpaSessionContexts(){const e=N(),n=process.env.SHENNIAN_MACHINE_ID||e.machineId||"";if(!e.machineToken||!n)return null;const t=this.listLocalWeChatRpaSessionContexts(n);return(await de({serverUrl:e.serverUrl,machineToken:e.machineToken}).listBindings({machineId:n})).bindings.filter(s=>s.enabled!==!1).map(s=>{const o=t.get(s.sessionId);return{sessionId:s.sessionId,workDir:s.sessionWorkDir?.trim()||o?.workDir?.trim()||b.homedir(),agentType:s.sessionAgentType?.trim()||o?.agentType||void 0,agentSessionId:s.sessionAgentSessionId??o?.agentSessionId??null,modelId:s.sessionModelId??o?.modelId??null,externalChannel:{connected:!0,configured:!0,type:"wechat-rpa",channelId:s.id,machineId:s.machineId,name:s.conversationName,canReply:s.allowReply!==!1,systemPrompt:null,wechatRpaSource:"wechat-channel",wechatRpaGroups:[{name:s.conversationName}],downloadAttachments:s.downloadMedia!==!1}}})}listLocalWeChatRpaSessionContexts(e){const n=new Map;for(const t of this.listWeChatRpaSessionContexts())n.set(t.sessionId,t);for(const t of ie())!t?.id||t.deletedAt||t.machineId&&e&&t.machineId!==e||n.has(t.id)||n.set(t.id,{sessionId:t.id,workDir:t.workDir,agentType:t.agentType,agentSessionId:t.agentSessionId,modelId:t.modelId,externalChannel:t.externalChannel??null});return n}async notifyWeChatRpaSessionBinding(e){await this.weChatRpaSessionSync.reconcileSession(e).catch(n=>{console.error(`[wechat-rpa-sync] reconcileSession failed sessionId=${e.sessionId}: ${n instanceof Error?n.message:String(n)}`)})}async reconcileWeChatRpaSessionBindings(){await this.weChatRpaSessionSync.reconcileAll().catch(e=>{console.error(`[wechat-rpa-sync] reconcileAll failed: ${e instanceof Error?e.message:String(e)}`)})}async start(){return this.startPromise?this.startPromise:(this.startPromise=this.doStart(),this.startPromise)}async doStart(){if(this.server)return;this.server=Y.createServer((n,t)=>{this.handleIpc(n,t)}),this.server.requestTimeout=A,this.server.timeout=A,this.server.keepAliveTimeout=A,this.server.headersTimeout=A+5e3,await new Promise((n,t)=>{this.server.once("error",t),this.server.listen(0,"127.0.0.1",()=>n())});const e=this.server.address();typeof e=="object"&&e&&(this.ipcUrl=`http://127.0.0.1:${e.port}`,this.writeIpcRuntimeFile()),this.server.unref(),await this.channelRuntime.start(),this.reconcileWeChatRpaSessionBindings(),this.broadcastConfiguredChannelStatuses(),this.healthTimer=setInterval(()=>this.scanWorkerHealth(),6e4),this.healthTimer.unref()}async ready(){await this.start()}async stop(){this.healthTimer&&clearInterval(this.healthTimer),this.healthTimer=null,await this.channelRuntime.stop(),await new Promise(e=>{if(!this.server)return e();this.server.close(()=>e())}),this.server=null,this.ipcUrl=null,this.removeIpcRuntimeFile()}writeIpcRuntimeFile(){if(this.ipcUrl)try{const e=O();S.mkdirSync(w.dirname(e),{recursive:!0}),S.writeFileSync(e,JSON.stringify({url:this.ipcUrl,token:this.ipcToken,pid:process.pid,updatedAt:new Date().toISOString()},null,2),{mode:384}),S.chmodSync(e,384)}catch{}}removeIpcRuntimeFile(){try{const e=O(),n=JSON.parse(S.readFileSync(e,"utf-8"));(n.url===this.ipcUrl||n.token===this.ipcToken)&&S.unlinkSync(e)}catch{}}getInjectedEnv(e,n,t,r){return this.ipcUrl?{SHENNIAN_MANAGER_SESSION_ID:e,SHENNIAN_MANAGER_AGENT_SESSION_ID:n??"",SHENNIAN_MANAGER_WORKDIR:R(t),SHENNIAN_MANAGER_MODEL:r,SHENNIAN_MANAGER_IPC_URL:this.ipcUrl,SHENNIAN_MANAGER_IPC_TOKEN:this.ipcToken}:{}}registerManager(e){this.registry.upsertManager({...e,workDir:R(e.workDir),machineId:process.env.SHENNIAN_MACHINE_ID??null})}setManagerWorkerDefaults(e,n,t){const r=this.registry.getManager(e);r&&this.registry.upsertManager({...r,defaultWorkerAgentType:n??null,defaultWorkerModelId:t??null})}getManagerWorkerDefaults(e){const n=this.registry.getManager(e);return{agentType:n?.defaultWorkerAgentType??null,modelId:n?.defaultWorkerModelId??null}}noteManagerAgentSession(e,n,t,r){const s=this.registry.getManager(e);this.registry.upsertManager({sessionId:e,agentSessionId:n,workDir:R(t),machineId:process.env.SHENNIAN_MACHINE_ID??null,modelId:r,agentType:s?.agentType,providerModelId:s?.providerModelId})}noteAgentEvent(e,n){if(!this.findWorker(e))return;const r={lastActivityAt:new Date().toISOString(),runId:n.runId};n.agentSessionId&&(r.agentSessionId=n.agentSessionId);const s=`${e}:${n.runId}`;if(n.state==="delta"&&n.text&&!n.thinking){const i=(this.workerTextAcc.get(s)??"")+n.text;this.workerTextAcc.set(s,i);const c=B(i);c&&(r.summary=c.length>160?`${c.slice(0,160)}...`:c)}if(n.state==="final"||n.state==="error"||n.state==="aborted"){r.status=n.state;const i=B(this.workerTextAcc.get(s)??"");i&&(r.summary=i.length>240?`${i.slice(0,240)}...`:i),this.workerTextAcc.delete(s)}else n.state==="start"&&(r.status="running");const o=this.registry.updateWorker(e,r);o&&(n.state==="final"||n.state==="error"||n.state==="aborted")&&this.enqueueWorkerEvent(o.managedBy,o,n.state,n.runId,n.message)}findWorker(e){return this.registry.load().workers[e]}async handleAppReq(e){const n=this.opts.getRuntime();if(e.method.includes("channel")||e.method.includes("wechat-rpa")){f(n,e.id,!1,{error:"feature_retired"});return}const t=e.params??{};try{const r=String(t.managerSessionId||t.sessionId||"");if(!r)throw new Error("sessionId is required");const s=this.registry.getManager(r),o=e.method==="session.wechat-rpa.channel.get"||e.method==="manager.wechat-rpa.channel.get",i=e.method==="session.wechat-rpa.channel.upsert"||e.method==="manager.wechat-rpa.channel.upsert",c=e.method==="session.wechat-rpa.channel.sync"||e.method==="manager.wechat-rpa.channel.sync",l=e.method==="session.wechat-rpa.outbound.cancel"||e.method==="manager.wechat-rpa.outbound.cancel";if(e.method==="manager.channel.get"){f(n,e.id,!0,{channel:this.channelRuntime.getManagerChannel(r,"websocket",{includeSecret:!0})});return}if(e.method==="manager.channel.upsert"){const d=await this.channelRuntime.upsertManagerChannel({id:String(t.id||`websocket:${r}`),managerSessionId:r,sessionId:r,workDir:String(t.workDir||s?.workDir||""),type:"websocket",name:typeof t.name=="string"?t.name:void 0,agentType:typeof t.agentType=="string"?t.agentType:void 0,agentSessionId:typeof t.agentSessionId=="string"?t.agentSessionId:null,modelId:typeof t.modelId=="string"?t.modelId:null,enabled:!!t.enabled,wsUrl:typeof t.wsUrl=="string"?t.wsUrl:void 0,token:typeof t.token=="string"?t.token:void 0,canReply:t.canReply===void 0?void 0:!!t.canReply,systemPrompt:typeof t.systemPrompt=="string"?t.systemPrompt:void 0});this.broadcastManagerChannelStatus(r),f(n,e.id,!0,{channel:d});return}if(o){f(n,e.id,!0,{channel:this.channelRuntime.getManagerChannel(r,"wechat-rpa",{includeSecret:!0})});return}if(i){const d=await this.channelRuntime.upsertManagerWeChatRpaChannel({id:String(t.id||`wechat-rpa:${r}`),managerSessionId:r,sessionId:r,workDir:R(String(t.workDir||s?.workDir||"")),name:typeof t.name=="string"?t.name:void 0,agentType:typeof t.agentType=="string"?t.agentType:void 0,agentSessionId:typeof t.agentSessionId=="string"?t.agentSessionId:null,modelId:typeof t.modelId=="string"?t.modelId:null,enabled:!!t.enabled,groups:U(t.groups),canReply:t.canReply===void 0?void 0:!!t.canReply,systemPrompt:typeof t.systemPrompt=="string"?t.systemPrompt:void 0,source:H(t.source),pollIntervalMs:m(t.pollIntervalMs),recentLimit:m(t.recentLimit),idleSeconds:m(t.idleSeconds),forceForeground:t.forceForeground===void 0?void 0:!!t.forceForeground,noRestore:t.noRestore===void 0?void 0:!!t.noRestore,downloadAttachments:t.downloadAttachments===void 0?void 0:!!t.downloadAttachments,downloadAttachmentsDir:typeof t.downloadAttachmentsDir=="string"?t.downloadAttachmentsDir:void 0,selfNickname:typeof t.selfNickname=="string"?t.selfNickname:void 0,selfTriggerMarker:typeof t.selfTriggerMarker=="string"?t.selfTriggerMarker:void 0,deferInitialPoll:t.deferInitialPoll===void 0?void 0:!!t.deferInitialPoll,privacyConsentAccepted:t.privacyConsentAccepted===void 0?void 0:!!t.privacyConsentAccepted,flowScriptPath:typeof t.flowScriptPath=="string"?t.flowScriptPath:void 0});this.broadcastManagerChannelStatus(r),f(n,e.id,!0,{channel:d});return}if(c){const d=await this.channelRuntime.syncManagerWeChatRpaChannel(r);this.broadcastManagerChannelStatus(r),f(n,e.id,!0,d);return}if(l){const d=await this.channelRuntime.cancelManagerWeChatRpaOutbound({managerSessionId:r,idempotencyKey:typeof t.idempotencyKey=="string"?t.idempotencyKey:null,replyId:typeof t.replyId=="string"?t.replyId:null,reason:typeof t.reason=="string"?t.reason:"user_cancelled"});this.broadcastManagerChannelStatus(r),f(n,e.id,!0,d);return}throw new Error(`Unsupported manager app method: ${e.method}`)}catch(r){f(n,e.id,!1,{error:r instanceof Error?r.message:String(r)})}}broadcastConfiguredChannelStatuses(){for(const e of this.channelRuntime.listManagerChannelStatuses())this.broadcastManagerChannelStatus(e.managerSessionId)}broadcastManagerChannelStatus(e){const n=this.opts.getRuntime();if(!n.client?.sendEvent)return;const t=this.registry.getManager(e),r=this.channelRuntime.getManagerChannelStatus(e),s=this.channelRuntime.getManagerChannel(e,"wechat-rpa")??this.channelRuntime.getManagerChannel(e,"websocket");n.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:e,agentType:t?"manager":s?.agentType,agentSessionId:t?.agentSessionId??s?.agentSessionId??null,modelId:t?.modelId??s?.modelId??null,workDir:t?.workDir??s?.workDir,externalChannel:r}}})}async handleIpc(e,n){if(e.headers.authorization!==`Bearer ${this.ipcToken}`){u(n,401,{ok:!1,error:"Unauthorized"});return}try{const t=new URL(e.url??"/","http://127.0.0.1"),r=await Re(e);if(t.pathname.startsWith("/channel/")||t.pathname.startsWith("/external/")||t.pathname.startsWith("/wechat-rpa/")){u(n,410,{ok:!1,error:"feature_retired"});return}const s=String(r.managerSessionId||e.headers["x-shennian-manager-session-id"]||"");if(!s)throw new Error("managerSessionId is required");const o=this.registry.getManager(s);if(t.pathname==="/sessions/list"){if(!o)throw new Error("Manager runtime is not registered");const i=new Set(this.opts.getRuntime().sessions.keys());u(n,200,{ok:!0,sessions:this.registry.listWorkers(s,{runningSessionIds:i})});return}if(t.pathname==="/sessions/start"){if(!o)throw new Error("Manager runtime is not registered");const i=String(r.agentType||r.agent||o.defaultWorkerAgentType||"codex");if(!ye(i))throw new Error(`Unsupported manager worker agent: ${i}`);const c=R(String(r.workDir||o.workDir));if(c!==o.workDir)throw new Error("Manager can only start workers in the same workDir");const l=String(r.message||"");if(!l)throw new Error("message is required");const d=this.registry.addWorker({managerSessionId:s,agentType:i,workDir:c,summary:l.slice(0,120)}),h=String(r.modelId||(i===o.defaultWorkerAgentType?o.defaultWorkerModelId??"":""));await this.dispatchChatSend(d.sessionId,i,c,l,null,h),u(n,200,{ok:!0,session:d});return}if(t.pathname==="/sessions/send"){const i=String(r.sessionId||""),c=String(r.message||""),l=r.enqueue===void 0?!0:!!r.enqueue,d=this.registry.getWorkerForManager(s,i);if(!d)throw new Error("Worker not found in this manager scope");const h=String(r.modelId||(d.agentType===o?.defaultWorkerAgentType?o.defaultWorkerModelId??"":""));l?await this.dispatchChatEnqueue(d.sessionId,d.agentType,d.workDir,c,d.agentSessionId??null,h):await this.dispatchChatSend(d.sessionId,d.agentType,d.workDir,c,d.agentSessionId??null,h),u(n,200,{ok:!0});return}if(t.pathname==="/sessions/queue"){const i=String(r.sessionId||"");if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");const l=this.opts.getRuntime().chatQueue?.getSnapshot(i);u(n,200,{ok:!0,queue:l});return}if(t.pathname==="/sessions/queue/edit"){const i=String(r.sessionId||"");if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");await this.opts.dispatchReq({type:"req",id:`manager-queue-edit-${p()}`,method:"chat.queue.edit",params:{sessionId:i,queueMessageId:String(r.queueMessageId||r.messageId||""),text:String(r.message||r.text||"")}}),u(n,200,{ok:!0,queue:this.opts.getRuntime().chatQueue?.getSnapshot(i)});return}if(t.pathname==="/sessions/queue/delete"){const i=String(r.sessionId||"");if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");await this.opts.dispatchReq({type:"req",id:`manager-queue-delete-${p()}`,method:"chat.queue.delete",params:{sessionId:i,queueMessageId:String(r.queueMessageId||r.messageId||"")}}),u(n,200,{ok:!0,queue:this.opts.getRuntime().chatQueue?.getSnapshot(i)});return}if(t.pathname==="/sessions/stop"||t.pathname==="/sessions/terminate"){const i=String(r.sessionId||"");if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");await this.opts.dispatchReq({type:"req",id:`manager-abort-${p()}`,method:"chat.abort",params:{sessionId:i}}),this.registry.updateWorker(i,{status:"aborted"}),u(n,200,{ok:!0});return}if(t.pathname==="/sessions/read"){const i=String(r.sessionId||""),c=Number(r.limit||200);if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");const d=se(i,{limit:Math.max(c*20,c)});u(n,200,{ok:!0,messages:Se(d,c),rawMessageCount:d.length});return}if(t.pathname==="/memory/path"){if(!o)throw new Error("Manager runtime is not registered");u(n,200,{ok:!0,path:w.join(o.workDir,".shennian")});return}if(t.pathname==="/external/reply"){const i=typeof r.replyTarget=="string"?this.registry.getReplyTarget(r.replyTarget):this.registry.getLatestReplyTargetForManager(s),c=String(r.text||""),l=q(r.attachment),d=String(r.idempotencyKey||p()),h=String(r.channelId||""),g=String(r.conversationId||""),M=!i&&(!h||!g)?await this.channelRuntime.getDefaultReplyTarget(s).catch(()=>null):null,I=i?.channelId||h||M?.channelId||"",E=i?.conversationId||g||M?.conversationId||"",W=I?this.channelRuntime.getChannelById(I):void 0,J=W&&(!!(i||h)||W.managedBy!=="session-sync");if(I&&J){if(!E)throw new Error("No external channel target is available for this Manager");const y=await this.channelRuntime.reply({managerSessionId:s,channelId:I,conversationId:E,messageId:i?.messageId??void 0,text:c,attachment:l,idempotencyKey:d});u(n,y.ok?200:400,y);return}const x=await this.tryDirectWeChatRpaReply(s,c,l);if(x){u(n,x.ok?200:400,x);return}let k;try{k=await this.sendManagedWeComReply({managerSessionId:s,text:c,attachment:l,idempotencyKey:d})}catch(y){const P=y instanceof Error?y.message:String(y);if(!F(P))throw y;k={ok:!1,error:P}}if(k.ok){u(n,200,{ok:!0,payload:k.payload});return}if(!F(k.error||"")||!I||!E){u(n,400,{ok:!1,error:k.error||"External send failed"});return}u(n,400,{ok:!1,error:`No local external channel is configured for ${I}`});return}if(t.pathname==="/channel/get"){u(n,200,{ok:!0,channel:this.channelRuntime.getManagerChannel(s,"websocket")});return}if(t.pathname==="/channel/upsert"){if(!o)throw new Error("Manager runtime is not registered");const i=await this.channelRuntime.upsertManagerChannel({id:String(r.id||`websocket:${s}`),managerSessionId:s,workDir:o.workDir,type:"websocket",name:typeof r.name=="string"?r.name:void 0,enabled:!!r.enabled,wsUrl:typeof r.wsUrl=="string"?r.wsUrl:void 0,token:typeof r.token=="string"?r.token:void 0,canReply:r.canReply===void 0?void 0:!!r.canReply,systemPrompt:typeof r.systemPrompt=="string"?r.systemPrompt:void 0});this.registry.upsertManager({...o,status:o.status}),this.broadcastManagerChannelStatus(s),u(n,200,{ok:!0,channel:i});return}if(t.pathname==="/wechat-rpa/tool/read"){const i=L(r),c=m(r.limit)??10,l=typeof r.traceId=="string"?r.traceId:void 0;let d;try{d=await this.weChatAutomationLane.run(`wechat-tool:read:${i.channelId}`,()=>ue(i,{conversation:i.conversationName,workDir:i.workDir,sessionId:i.sessionId,limit:c,recentLimit:c,download:r.download==="never"?"never":"auto",traceId:l,timeoutMs:m(r.timeoutMs)}))}catch(h){u(n,400,{ok:!1,...z(h,l)});return}u(n,200,{ok:!0,messages:d.messages,outDir:d.outDir,helperTracePath:d.helperTracePath,activityGuardPath:d.activityGuardPath});return}if(t.pathname==="/wechat-rpa/tool/send"){const i=L(r),c=String(r.text||""),l=q(r.attachment),d=typeof r.traceId=="string"?r.traceId:void 0;let h;try{h=await this.weChatAutomationLane.run(`wechat-tool:send:${i.channelId}`,()=>$(i,{conversation:i.conversationName,workDir:i.workDir,sessionId:i.sessionId,text:c,attachment:l,traceId:d,timeoutMs:m(r.timeoutMs)}))}catch(g){u(n,400,{ok:!1,...z(g,d)});return}u(n,200,{ok:!0,...h});return}if(t.pathname==="/wechat-rpa/channel/get"){u(n,200,{ok:!0,channel:this.channelRuntime.getManagerChannel(s,"wechat-rpa",{includeSecret:!0})});return}if(t.pathname==="/wechat-rpa/channel/upsert"){const i=R(String(r.workDir||o?.workDir||""));if(!i)throw new Error("workDir is required");const c=await this.channelRuntime.upsertManagerWeChatRpaChannel({id:String(r.id||`wechat-rpa:${s}`),managerSessionId:s,sessionId:s,workDir:i,name:typeof r.name=="string"?r.name:void 0,agentType:typeof r.agentType=="string"?r.agentType:void 0,agentSessionId:typeof r.agentSessionId=="string"?r.agentSessionId:null,modelId:typeof r.modelId=="string"?r.modelId:null,enabled:!!r.enabled,groups:U(r.groups),canReply:r.canReply===void 0?void 0:!!r.canReply,systemPrompt:typeof r.systemPrompt=="string"?r.systemPrompt:void 0,source:H(r.source),pollIntervalMs:m(r.pollIntervalMs),recentLimit:m(r.recentLimit),idleSeconds:m(r.idleSeconds),forceForeground:r.forceForeground===void 0?void 0:!!r.forceForeground,noRestore:r.noRestore===void 0?void 0:!!r.noRestore,downloadAttachments:r.downloadAttachments===void 0?void 0:!!r.downloadAttachments,downloadAttachmentsDir:typeof r.downloadAttachmentsDir=="string"?r.downloadAttachmentsDir:void 0,selfNickname:typeof r.selfNickname=="string"?r.selfNickname:void 0,selfTriggerMarker:typeof r.selfTriggerMarker=="string"?r.selfTriggerMarker:void 0,deferInitialPoll:r.deferInitialPoll===void 0?void 0:!!r.deferInitialPoll,privacyConsentAccepted:r.privacyConsentAccepted===void 0?void 0:!!r.privacyConsentAccepted,flowScriptPath:typeof r.flowScriptPath=="string"?r.flowScriptPath:void 0});o&&this.registry.upsertManager({...o,status:o.status}),this.broadcastManagerChannelStatus(s),u(n,200,{ok:!0,channel:c});return}if(t.pathname==="/wechat-rpa/channel/sync"){const{channel:i,messages:c}=await this.channelRuntime.syncManagerWeChatRpaChannel(s);this.broadcastManagerChannelStatus(s),u(n,200,{ok:!0,channel:i,messages:c});return}if(t.pathname==="/wechat-rpa/outbound/cancel"){const i=await this.channelRuntime.cancelManagerWeChatRpaOutbound({managerSessionId:s,idempotencyKey:typeof r.idempotencyKey=="string"?r.idempotencyKey:null,replyId:typeof r.replyId=="string"?r.replyId:null,reason:typeof r.reason=="string"?r.reason:"user_cancelled"});this.broadcastManagerChannelStatus(s),u(n,200,{ok:!0,...i});return}u(n,404,{ok:!1,error:`Unknown manager IPC path: ${t.pathname}`})}catch(t){u(n,400,{ok:!1,error:t instanceof Error?t.message:String(t)})}}async dispatchChatSend(e,n,t,r,s,o){await this.opts.dispatchReq({type:"req",id:`manager-send-${p()}`,method:"chat.send",params:{...v(),sessionId:e,text:r,agentType:n,workDir:t,agentSessionId:s,modelId:o}})}async dispatchChatEnqueue(e,n,t,r,s,o){await this.opts.dispatchReq({type:"req",id:`manager-enqueue-${p()}`,method:"chat.enqueue",params:{...v(),sessionId:e,text:r,agentType:n,workDir:t,agentSessionId:s,modelId:o}})}async tryDirectWeChatRpaReply(e,n,t){const r=this.opts.getRuntime().sessions.get(e),s=r?.externalChannel;if(!s||s.type!=="wechat-rpa"||!s.channelId||!s.name)return null;const o=process.env.SHENNIAN_MACHINE_ID||N().machineId||"";if(s.machineId&&o&&s.machineId!==o)return{ok:!1,error:`WeChat \u7ED1\u5B9A\u5C5E\u4E8E\u5176\u4ED6\u673A\u5668 (${s.machineId})\uFF0C\u8BF7\u5728\u7ED1\u5B9A\u673A\u5668\u4E0A\u53D1\u9001\u3002`};if(!n.trim()&&!t)return{ok:!1,error:"text or attachment is required"};const i=r?.workDir||process.cwd(),c={sessionId:e,channelId:s.channelId,conversationId:ge(s.name),conversationName:s.name,workDir:i};try{return{ok:!0,payload:await this.weChatAutomationLane.run(`wechat-tool:send:${c.channelId}`,()=>$(c,{conversation:c.conversationName,workDir:c.workDir,sessionId:c.sessionId,text:n,attachment:t}))}}catch(l){return{ok:!1,error:l instanceof Error?l.message:String(l)}}}async sendManagedWeComReply(e){const n=he(e.text);if(!n.length&&!e.attachment)return{ok:!1,error:"text or attachment is required"};const t=this.opts.getRuntime().client;if(!t||typeof t.sendReq!="function")return{ok:!1,error:"Relay is not connected"};const r=[];for(const[s,o]of n.entries()){const i=await t.sendReq({type:"req",id:`external-send-${p()}`,method:"external.send",params:{managerSessionId:e.managerSessionId,text:o,idempotencyKey:n.length>1?`${e.idempotencyKey}:${s+1}`:e.idempotencyKey}});if(!i.ok)return{ok:!1,error:i.error||"External send failed"};r.push(i.payload)}if(e.attachment){const s=await t.sendReq({type:"req",id:`external-send-${p()}`,method:"external.send",params:{managerSessionId:e.managerSessionId,attachment:e.attachment,idempotencyKey:n.length?`${e.idempotencyKey}:attachment`:e.idempotencyKey}});if(!s.ok)return{ok:!1,error:s.error||"External send failed"};r.push(s.payload)}return{ok:!0,payload:r.length===1?r[0]:r}}enqueueWorkerEvent(e,n,t,r,s){this.registry.enqueueInboxEvent({managerSessionId:e,workerSessionId:n.sessionId,kind:t==="final"?"worker.final":t==="error"?"worker.error":"worker.aborted",priority:"normal",runId:r??null,summary:s||n.summary||"(\u65E0\u53EF\u89C1\u6458\u8981)"})&&this.drainManagerInbox(e)}handleExternalMessage(e,n){const t=this.channelRuntime.getChannelById(n.channelId)??this.channelRuntime.getManagerChannel(e,n.channelType),r=this.channelRuntime.getChannelStatusById(n.channelId)??this.channelRuntime.getManagerChannelStatus(e),s=this.registry.getManager(e),o=t?.agentType||(s?"manager":"codex"),i=t?.workDir||s?.workDir||process.cwd(),c=t?.agentSessionId??s?.agentSessionId??null,l=t?.modelId||s?.modelId||"",d=Ce(n.attachments,n.channelType),h=Te(n);this.dispatchExternalMessage({sessionId:e,agentType:o,workDir:i,agentSessionId:c,modelId:l,text:h,messageId:n.messageId,attachments:d,externalChannel:be(r),replyTarget:n.replyTarget})}async dispatchExternalMessage(e){const n=typeof e.messageId=="string"&&e.messageId.trim()?e.messageId.trim():void 0;await this.opts.dispatchReq({type:"req",id:`external-enqueue-${p()}`,method:"chat.enqueue",params:{...v(),sessionId:e.sessionId,text:e.text,agentType:e.agentType,workDir:e.workDir,agentSessionId:e.agentSessionId,modelId:e.modelId,origin:"external",queueMessageId:n,clientMessageId:n,attachments:e.attachments,externalChannel:e.externalChannel??null,replyTarget:e.replyTarget}})}scanWorkerHealth(){const e=Date.now(),n=this.registry.load();for(const t of Object.values(n.workers)){if(t.status!=="running")continue;const r=e-Date.parse(t.createdAt);if(r<10*6e4)continue;const s=t.healthNotifiedAt?Date.parse(t.healthNotifiedAt):0;if(s&&e-s<10*6e4)continue;const o=n.managers[t.managedBy];if(!o)continue;const i=Math.max(0,Math.floor((e-Date.parse(t.lastActivityAt))/6e4)),c=`\u5DF2\u8FD0\u884C ${Math.floor(r/6e4)} \u5206\u949F\uFF0C\u5C1A\u672A\u7ED3\u675F\u3002
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1190
|
+
请决定是否继续等待、询问用户、停止 worker、或创建其他 worker 协助。`;
|
|
1191
|
+
this.registry.updateWorker(worker.sessionId, {
|
|
1192
|
+
healthNotifiedAt: new Date(now).toISOString(),
|
|
1193
|
+
lastHealthSummary: prompt,
|
|
1194
|
+
});
|
|
1195
|
+
const event = this.registry.enqueueInboxEvent({
|
|
1196
|
+
managerSessionId: manager.sessionId,
|
|
1197
|
+
workerSessionId: worker.sessionId,
|
|
1198
|
+
kind: 'worker.health',
|
|
1199
|
+
priority: 'normal',
|
|
1200
|
+
runId: worker.runId ?? null,
|
|
1201
|
+
summary: prompt,
|
|
1202
|
+
});
|
|
1203
|
+
if (event)
|
|
1204
|
+
void this.drainManagerInbox(manager.sessionId);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async interruptAndResumeManager(manager, prompt, reason) {
|
|
1208
|
+
const runtime = this.opts.getRuntime();
|
|
1209
|
+
const active = runtime.sessions.get(manager.sessionId);
|
|
1210
|
+
if (active?.currentRunId) {
|
|
1211
|
+
this.registry.upsertManager({ ...manager, status: 'interrupting' });
|
|
1212
|
+
await this.opts.dispatchReq({ type: 'req', id: `manager-interrupt-${randomUUID()}`, method: 'chat.abort', params: { sessionId: manager.sessionId } });
|
|
1213
|
+
}
|
|
1214
|
+
const continuation = reason ? `事件类型:${reason}\n\n${prompt}` : prompt;
|
|
1215
|
+
await this.dispatchManagerContinuation(manager, continuation);
|
|
1216
|
+
}
|
|
1217
|
+
bindManagerAdapterEvents(sessionId, adapter) {
|
|
1218
|
+
adapter.on('agentEvent', (event) => {
|
|
1219
|
+
if (event.state === 'start' && event.agentSessionId) {
|
|
1220
|
+
const manager = this.registry.getManager(sessionId);
|
|
1221
|
+
if (manager)
|
|
1222
|
+
this.noteManagerAgentSession(sessionId, event.agentSessionId, manager.workDir, manager.modelId);
|
|
1223
|
+
}
|
|
1224
|
+
if (event.state === 'start')
|
|
1225
|
+
this.updateManagerStatus(sessionId, 'running');
|
|
1226
|
+
if (event.state === 'final' || event.state === 'error' || event.state === 'aborted') {
|
|
1227
|
+
this.updateManagerStatus(sessionId, 'idle');
|
|
1228
|
+
queueMicrotask(() => void this.drainManagerInbox(sessionId));
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
updateManagerStatus(sessionId, status) {
|
|
1233
|
+
const manager = this.registry.getManager(sessionId);
|
|
1234
|
+
if (!manager)
|
|
1235
|
+
return;
|
|
1236
|
+
this.registry.upsertManager({ ...manager, status });
|
|
1237
|
+
}
|
|
1238
|
+
async drainManagerInbox(managerSessionId) {
|
|
1239
|
+
const manager = this.registry.getManager(managerSessionId);
|
|
1240
|
+
if (!manager)
|
|
1241
|
+
return;
|
|
1242
|
+
const events = this.registry.listInboxEvents(managerSessionId);
|
|
1243
|
+
if (!events.length)
|
|
1244
|
+
return;
|
|
1245
|
+
const highPriority = events.find((event) => event.priority === 'high');
|
|
1246
|
+
const active = this.opts.getRuntime().sessions.get(managerSessionId);
|
|
1247
|
+
if (manager.status !== 'idle' && !highPriority)
|
|
1248
|
+
return;
|
|
1249
|
+
if (active?.currentRunId && !highPriority)
|
|
1250
|
+
return;
|
|
1251
|
+
const selected = highPriority ? [highPriority] : events.slice(0, 20);
|
|
1252
|
+
const prompt = formatManagerInboxPrompt(selected);
|
|
1253
|
+
try {
|
|
1254
|
+
if (highPriority && active?.currentRunId) {
|
|
1255
|
+
await this.interruptAndResumeManager(manager, prompt, highPriority.kind);
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
this.registry.upsertManager({ ...manager, status: 'running' });
|
|
1259
|
+
await this.dispatchManagerContinuation(manager, prompt, true);
|
|
1260
|
+
}
|
|
1261
|
+
this.registry.removeInboxEvents(managerSessionId, selected.map((event) => event.id));
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
this.registry.upsertManager({ ...manager, status: 'idle' });
|
|
1265
|
+
console.error(`[manager-inbox] delivery failed sessionId=${managerSessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async dispatchManagerContinuation(manager, text, enqueue = false) {
|
|
1269
|
+
const agentType = manager.agentType ?? 'manager';
|
|
1270
|
+
const modelId = agentType === 'manager' ? manager.modelId : manager.providerModelId ?? undefined;
|
|
1271
|
+
const params = {
|
|
1272
|
+
sessionId: manager.sessionId,
|
|
1273
|
+
text,
|
|
1274
|
+
agentType,
|
|
1275
|
+
sessionMode: 'manager',
|
|
1276
|
+
workDir: manager.workDir,
|
|
1277
|
+
agentSessionId: manager.agentSessionId,
|
|
1278
|
+
modelId,
|
|
1279
|
+
managerDefaultWorkerAgentType: manager.defaultWorkerAgentType ?? null,
|
|
1280
|
+
managerDefaultWorkerModelId: manager.defaultWorkerModelId ?? null,
|
|
1281
|
+
};
|
|
1282
|
+
await this.opts.dispatchReq({
|
|
1283
|
+
type: 'req',
|
|
1284
|
+
id: `manager-${enqueue ? 'enqueue' : 'send'}-${randomUUID()}`,
|
|
1285
|
+
method: enqueue ? 'chat.enqueue' : 'chat.send',
|
|
1286
|
+
params,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
getExternalChannelStatus(managerSessionId) {
|
|
1290
|
+
return this.channelRuntime.getManagerChannelStatus(managerSessionId);
|
|
1291
|
+
}
|
|
1292
|
+
getManagerExternalChannelSystemPrompt(managerSessionId) {
|
|
1293
|
+
return this.channelRuntime
|
|
1294
|
+
.listManagerExternalChannels(managerSessionId)
|
|
1295
|
+
.map((channel) => buildExternalChannelInstructions(channel, undefined, managerSessionId, 'manager'))
|
|
1296
|
+
.join('\n\n')
|
|
1297
|
+
.trim();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function parseWeChatRpaGroups(value) {
|
|
1301
|
+
if (!Array.isArray(value))
|
|
1302
|
+
return [];
|
|
1303
|
+
return value
|
|
1304
|
+
.map((item) => ({ name: String(item?.name || '').trim() }))
|
|
1305
|
+
.filter((item) => item.name);
|
|
1306
|
+
}
|
|
1307
|
+
function parseWeChatRpaSource(value) {
|
|
1308
|
+
return value === 'wechat-channel' || value === 'macos-flow' || value === 'macos-probe' || value === 'windows-visual-flow' || value === 'wechat-rpa-lab' || value === 'fixture-jsonl' ? value : undefined;
|
|
1309
|
+
}
|
|
1310
|
+
function externalAttachmentsForAgent(attachments, channelType) {
|
|
1311
|
+
const result = attachments
|
|
1312
|
+
.map((attachment) => externalAttachmentForAgent(attachment, channelType))
|
|
1313
|
+
.filter((item) => item != null);
|
|
1314
|
+
return result.length ? result : undefined;
|
|
1315
|
+
}
|
|
1316
|
+
function externalAttachmentMarkdown(attachments, existingText, senderName, channelType) {
|
|
1317
|
+
const text = existingText || '';
|
|
1318
|
+
const seen = new Set();
|
|
1319
|
+
const lines = attachments
|
|
1320
|
+
.map((attachment, index) => {
|
|
1321
|
+
const readable = externalAttachmentForAgent(attachment, channelType);
|
|
1322
|
+
const reference = formatExternalAttachmentReference({
|
|
1323
|
+
type: attachment.type,
|
|
1324
|
+
name: readable?.name || attachment.name,
|
|
1325
|
+
path: readable?.path,
|
|
1326
|
+
mimeType: readable?.mimeType || attachment.mimeType,
|
|
1327
|
+
availability: attachment.availability,
|
|
1328
|
+
providerError: attachment.providerError || unreadableEdgeLocalStatus(attachment),
|
|
1329
|
+
}, index);
|
|
1330
|
+
const pathOrUrl = readable?.path || '';
|
|
1331
|
+
if (pathOrUrl) {
|
|
1332
|
+
const key = `${reference}\n${pathOrUrl}`;
|
|
1333
|
+
if (seen.has(key) || text.includes(pathOrUrl))
|
|
1334
|
+
return '';
|
|
1335
|
+
seen.add(key);
|
|
1336
|
+
return `${senderName}: ${reference}`;
|
|
1337
|
+
}
|
|
1338
|
+
const key = reference;
|
|
1339
|
+
if (seen.has(key))
|
|
1340
|
+
return '';
|
|
1341
|
+
seen.add(key);
|
|
1342
|
+
return `${senderName}: ${reference}`;
|
|
1343
|
+
})
|
|
1344
|
+
.filter((line) => !text.includes(line));
|
|
1345
|
+
return lines.join('\n');
|
|
1346
|
+
}
|
|
1347
|
+
function formatExternalMessageForAgent(event) {
|
|
1348
|
+
const visibleSender = event.channelType === 'wechat-rpa'
|
|
1349
|
+
? visibleWeChatSenderName(event.sender.name)
|
|
1350
|
+
: event.sender.name || event.sender.id;
|
|
1351
|
+
const senderName = visibleSender || (event.channelType === 'wechat-rpa' ? '对方' : event.sender.id) || '对方';
|
|
1352
|
+
const attachmentMarkdown = externalAttachmentMarkdown(event.attachments, event.text, senderName, event.channelType);
|
|
1353
|
+
const messageLine = event.text
|
|
1354
|
+
? formatExternalMessageLine({ senderName, senderId: event.sender.id, text: event.text })
|
|
1355
|
+
: attachmentMarkdown
|
|
1356
|
+
? ''
|
|
1357
|
+
: `${senderName}:`;
|
|
1358
|
+
const body = [messageLine, attachmentMarkdown].filter(Boolean).join('\n');
|
|
1359
|
+
return [
|
|
1360
|
+
formatExternalConversationText({
|
|
1361
|
+
conversationName: event.conversationName || event.conversationId,
|
|
1362
|
+
messages: [],
|
|
1363
|
+
}),
|
|
1364
|
+
body,
|
|
1365
|
+
].filter(Boolean).join('\n');
|
|
1366
|
+
}
|
|
1367
|
+
function visibleWeChatSenderName(value) {
|
|
1368
|
+
const trimmed = value?.trim() || '';
|
|
1369
|
+
if (!trimmed)
|
|
1370
|
+
return '';
|
|
1371
|
+
if (/^(contact|self|system|unknown)$/i.test(trimmed))
|
|
1372
|
+
return '';
|
|
1373
|
+
if (/^wechat-sender:/i.test(trimmed))
|
|
1374
|
+
return '';
|
|
1375
|
+
return trimmed;
|
|
1376
|
+
}
|
|
1377
|
+
function unreadableEdgeLocalStatus(attachment) {
|
|
1378
|
+
if (!attachment.localPath)
|
|
1379
|
+
return '';
|
|
1380
|
+
if (attachment.availability && attachment.availability !== 'edge-local')
|
|
1381
|
+
return '';
|
|
1382
|
+
if (isReadableLocalAttachment(attachment.localPath))
|
|
1383
|
+
return '';
|
|
1384
|
+
return 'edge-local-unavailable';
|
|
1385
|
+
}
|
|
1386
|
+
function externalAttachmentForAgent(attachment, channelType) {
|
|
1387
|
+
if (attachment.localPath && canUseOriginalLocalAttachment(attachment, channelType) && isReadableLocalAttachment(attachment.localPath)) {
|
|
1388
|
+
return {
|
|
1389
|
+
path: attachment.localPath,
|
|
1390
|
+
name: attachment.name || path.basename(attachment.localPath) || 'attachment',
|
|
1391
|
+
mimeType: attachment.mimeType || mimeTypeFromExternalAttachment(attachment),
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
if (attachment.url && canUseOriginalUrlAttachment(attachment)) {
|
|
1395
|
+
return {
|
|
1396
|
+
path: attachment.url,
|
|
1397
|
+
name: attachment.name || attachment.url.split('/').filter(Boolean).at(-1) || 'attachment',
|
|
1398
|
+
mimeType: attachment.mimeType || mimeTypeFromExternalAttachment(attachment),
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
if (attachment.thumbnailPath
|
|
1402
|
+
&& !isStrictWeChatOriginalKind(attachment, channelType)
|
|
1403
|
+
&& isReadableLocalAttachment(attachment.thumbnailPath)) {
|
|
1404
|
+
return {
|
|
1405
|
+
path: attachment.thumbnailPath,
|
|
1406
|
+
name: attachment.name ? `${attachment.name}-preview.png` : path.basename(attachment.thumbnailPath) || 'preview.png',
|
|
1407
|
+
mimeType: 'image/png',
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
function externalChannelForChatEnqueue(channel) {
|
|
1413
|
+
if (!channel)
|
|
1414
|
+
return null;
|
|
1415
|
+
if (channel.type !== 'wechat-rpa')
|
|
1416
|
+
return channel;
|
|
1417
|
+
return {
|
|
1418
|
+
configured: channel.configured,
|
|
1419
|
+
connected: channel.connected,
|
|
1420
|
+
type: channel.type,
|
|
1421
|
+
channelId: channel.channelId,
|
|
1422
|
+
name: channel.name,
|
|
1423
|
+
canReply: channel.canReply,
|
|
1424
|
+
systemPrompt: channel.systemPrompt,
|
|
1425
|
+
wechatRpaSource: channel.wechatRpaSource,
|
|
1426
|
+
wechatRpaGroups: channel.wechatRpaGroups,
|
|
1427
|
+
pollIntervalMs: channel.pollIntervalMs,
|
|
1428
|
+
recentLimit: channel.recentLimit,
|
|
1429
|
+
idleSeconds: channel.idleSeconds,
|
|
1430
|
+
forceForeground: channel.forceForeground,
|
|
1431
|
+
noRestore: channel.noRestore,
|
|
1432
|
+
downloadAttachments: channel.downloadAttachments,
|
|
1433
|
+
selfNickname: channel.selfNickname,
|
|
1434
|
+
wechatRpaPrivacyConsentAccepted: channel.wechatRpaPrivacyConsentAccepted,
|
|
1435
|
+
wechatRpaServerDecisionAvailable: channel.wechatRpaServerDecisionAvailable,
|
|
1436
|
+
wechatRpaPreflightChecks: channel.wechatRpaPreflightChecks,
|
|
1437
|
+
wechatRpaRuntimeState: channel.wechatRpaRuntimeState,
|
|
1438
|
+
wechatRpaLastMessageAt: channel.wechatRpaLastMessageAt,
|
|
1439
|
+
wechatRpaPendingReplyCount: channel.wechatRpaPendingReplyCount,
|
|
1440
|
+
wechatRpaLastError: channel.wechatRpaLastError,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function canUseOriginalLocalAttachment(attachment, channelType) {
|
|
1444
|
+
if (attachment.providerError)
|
|
1445
|
+
return false;
|
|
1446
|
+
if (attachment.availability && attachment.availability !== 'edge-local')
|
|
1447
|
+
return false;
|
|
1448
|
+
if (!isStrictWeChatOriginalKind(attachment, channelType))
|
|
1449
|
+
return true;
|
|
1450
|
+
return attachment.materializationKind === 'original-file'
|
|
1451
|
+
&& attachment.isOriginal === true
|
|
1452
|
+
&& attachment.mimeKindMatches === true
|
|
1453
|
+
&& !isDisallowedWeChatAttachmentPath(attachment.localPath);
|
|
1454
|
+
}
|
|
1455
|
+
function canUseOriginalUrlAttachment(attachment) {
|
|
1456
|
+
return !attachment.providerError
|
|
1457
|
+
&& (!attachment.availability || attachment.availability === 'server-url');
|
|
1458
|
+
}
|
|
1459
|
+
function isStrictWeChatOriginalKind(attachment, channelType) {
|
|
1460
|
+
if (channelType !== 'wechat-rpa')
|
|
1461
|
+
return false;
|
|
1462
|
+
const type = String(attachment.type || '').toLowerCase();
|
|
1463
|
+
return type === 'file' || type === 'video' || type === 'video-file';
|
|
1464
|
+
}
|
|
1465
|
+
function isDisallowedWeChatAttachmentPath(filePath) {
|
|
1466
|
+
const normalized = String(filePath || '').replace(/\\/g, '/');
|
|
1467
|
+
const basename = path.basename(normalized).toLowerCase();
|
|
1468
|
+
return /(^|\/)\.uploads(\/|$)/.test(normalized)
|
|
1469
|
+
|| /(^|-)preview([-.]|$)/i.test(basename);
|
|
1470
|
+
}
|
|
1471
|
+
function isReadableLocalAttachment(filePath) {
|
|
1472
|
+
try {
|
|
1473
|
+
return fs.statSync(filePath).isFile();
|
|
1474
|
+
}
|
|
1475
|
+
catch {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function mimeTypeFromExternalAttachment(attachment) {
|
|
1480
|
+
if (attachment.type === 'image')
|
|
1481
|
+
return 'image/*';
|
|
1482
|
+
if (attachment.type === 'video')
|
|
1483
|
+
return 'video/*';
|
|
1484
|
+
if (attachment.type === 'audio')
|
|
1485
|
+
return 'audio/*';
|
|
1486
|
+
return 'application/octet-stream';
|
|
1487
|
+
}
|
|
1488
|
+
function optionalNumber(value) {
|
|
1489
|
+
const number = Number(value);
|
|
1490
|
+
return Number.isFinite(number) ? number : undefined;
|
|
1491
|
+
}
|
|
1492
|
+
function wechatToolErrorPayload(error, traceId) {
|
|
1493
|
+
const reasonCode = stringProp(error, 'reasonCode') || reasonCodeFromMessage(error);
|
|
1494
|
+
const outDir = stringProp(error, 'outDir');
|
|
1495
|
+
const helperTracePath = stringProp(error, 'helperTracePath');
|
|
1496
|
+
const activityGuardPath = stringProp(error, 'activityGuardPath');
|
|
1497
|
+
return {
|
|
1498
|
+
error: error instanceof Error ? error.message : String(error || reasonCode),
|
|
1499
|
+
reasonCode,
|
|
1500
|
+
...(outDir ? { outDir } : {}),
|
|
1501
|
+
...(helperTracePath ? { helperTracePath } : {}),
|
|
1502
|
+
...(activityGuardPath ? { activityGuardPath } : {}),
|
|
1503
|
+
...(traceId ? { traceId } : {}),
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function stringProp(value, key) {
|
|
1507
|
+
if (!value || typeof value !== 'object')
|
|
1508
|
+
return '';
|
|
1509
|
+
const raw = value[key];
|
|
1510
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : '';
|
|
1511
|
+
}
|
|
1512
|
+
function reasonCodeFromMessage(error) {
|
|
1513
|
+
const message = error instanceof Error ? error.message : String(error || '');
|
|
1514
|
+
const match = /^([a-z][a-z0-9_]*)(?::|\b)/i.exec(message.trim());
|
|
1515
|
+
return match?.[1] || 'wechat_tool_failed';
|
|
1516
|
+
}
|