shennian 0.2.89 → 0.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,1011 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/manager-runtime.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, isToolPayload, } from '@shennian/wire';
|
|
9
|
-
import { ManagerRegistry } from './registry.js';
|
|
10
|
-
import { readMessages } from '../session/store.js';
|
|
11
|
-
import { ChannelRuntime } from '../channels/runtime.js';
|
|
12
|
-
import { splitExternalReplyText } from '../channels/reply-split.js';
|
|
13
|
-
import { resolveShennianPath } from '../config/index.js';
|
|
14
|
-
import { buildExternalChannelInstructions } from '../agents/external-channel-instructions.js';
|
|
15
|
-
const MAX_MANAGER_IPC_BODY_BYTES = Number(process.env.SHENNIAN_MANAGER_IPC_BODY_MAX_BYTES || 2 * 1024 * 1024);
|
|
16
|
-
let singleton = null;
|
|
17
|
-
export function setManagerRuntimeService(service) {
|
|
18
|
-
singleton = service;
|
|
19
|
-
}
|
|
20
|
-
export function getManagerRuntimeService() {
|
|
21
|
-
return singleton;
|
|
22
|
-
}
|
|
23
|
-
function normalizeWorkDir(workDir) {
|
|
24
|
-
return path.resolve(workDir || os.homedir());
|
|
25
|
-
}
|
|
26
|
-
function json(res, status, body) {
|
|
27
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
28
|
-
res.end(JSON.stringify(body));
|
|
29
|
-
}
|
|
30
|
-
function runIdFromMessageId(id) {
|
|
31
|
-
const match = /^agent-(.+)-\d+$/.exec(id);
|
|
32
|
-
return match?.[1] ?? null;
|
|
33
|
-
}
|
|
34
|
-
function isManagerWorkerAgentType(agentType) {
|
|
35
|
-
if (agentType === 'manager')
|
|
36
|
-
return false;
|
|
37
|
-
if (agentType.startsWith('custom:'))
|
|
38
|
-
return true;
|
|
39
|
-
return AVAILABLE_BUILTIN_AGENT_TYPES.includes(agentType);
|
|
40
|
-
}
|
|
41
|
-
function seqFromMessageId(id) {
|
|
42
|
-
const match = /^agent-.+-(\d+)$/.exec(id);
|
|
43
|
-
if (!match)
|
|
44
|
-
return null;
|
|
45
|
-
const seq = Number(match[1]);
|
|
46
|
-
return Number.isInteger(seq) && seq >= 0 ? seq : null;
|
|
47
|
-
}
|
|
48
|
-
function normalizeMarkdownForWorkerSummary(text) {
|
|
49
|
-
return text.replace(/\r\n/g, '\n').trim();
|
|
50
|
-
}
|
|
51
|
-
function toolSummary(payload) {
|
|
52
|
-
try {
|
|
53
|
-
const parsed = JSON.parse(payload);
|
|
54
|
-
const type = parsed.type === 'tool_result' || parsed.result ? 'tool_result' : 'tool_call';
|
|
55
|
-
const name = parsed.name || 'tool';
|
|
56
|
-
const result = typeof parsed.result === 'string' ? parsed.result.replace(/\s+/g, ' ').trim() : '';
|
|
57
|
-
const clipped = result.length > 220 ? `${result.slice(0, 220)}...` : result;
|
|
58
|
-
return clipped ? `[${type}] ${name}: ${clipped}` : `[${type}] ${name}`;
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return '[tool]';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function parseExternalReplyAttachment(value) {
|
|
65
|
-
if (!value || typeof value !== 'object')
|
|
66
|
-
return undefined;
|
|
67
|
-
const record = value;
|
|
68
|
-
const kind = String(record.kind || '');
|
|
69
|
-
const name = String(record.name || '');
|
|
70
|
-
const mimeType = String(record.mimeType || '');
|
|
71
|
-
const dataBase64 = String(record.dataBase64 || '');
|
|
72
|
-
const localPath = String(record.localPath || '');
|
|
73
|
-
const url = String(record.url || '');
|
|
74
|
-
const size = Number(record.size || 0);
|
|
75
|
-
if (dataBase64)
|
|
76
|
-
throw new Error('Manager IPC external attachments must use localPath or url; dataBase64 is not accepted');
|
|
77
|
-
if (kind !== 'image' && kind !== 'video' && kind !== 'file')
|
|
78
|
-
return undefined;
|
|
79
|
-
if (!name || !mimeType || !Number.isFinite(size) || size < 0)
|
|
80
|
-
return undefined;
|
|
81
|
-
if (!localPath && !url)
|
|
82
|
-
return undefined;
|
|
83
|
-
return {
|
|
84
|
-
kind,
|
|
85
|
-
name,
|
|
86
|
-
mimeType,
|
|
87
|
-
size,
|
|
88
|
-
...(localPath ? { localPath } : {}),
|
|
89
|
-
...(url ? { url } : {}),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function compactWorkerTranscript(rawMessages, limit) {
|
|
93
|
-
const chronological = [...rawMessages].sort((a, b) => a.ts - b.ts);
|
|
94
|
-
const compacted = [];
|
|
95
|
-
let buffer = null;
|
|
96
|
-
let bufferRunId = null;
|
|
97
|
-
let bufferSeq = null;
|
|
98
|
-
let bufferText = '';
|
|
99
|
-
const flush = () => {
|
|
100
|
-
if (!buffer)
|
|
101
|
-
return;
|
|
102
|
-
const text = bufferText.trim();
|
|
103
|
-
if (text) {
|
|
104
|
-
compacted.push({
|
|
105
|
-
...buffer,
|
|
106
|
-
id: `${buffer.id}-compact`,
|
|
107
|
-
payload: text,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
buffer = null;
|
|
111
|
-
bufferRunId = null;
|
|
112
|
-
bufferSeq = null;
|
|
113
|
-
bufferText = '';
|
|
114
|
-
};
|
|
115
|
-
for (const message of chronological) {
|
|
116
|
-
if (message.role === 'user') {
|
|
117
|
-
flush();
|
|
118
|
-
compacted.push(message);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (isToolPayload(message.payload)) {
|
|
122
|
-
flush();
|
|
123
|
-
compacted.push({
|
|
124
|
-
...message,
|
|
125
|
-
payload: toolSummary(message.payload),
|
|
126
|
-
});
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const text = extractPayloadText(message.payload);
|
|
130
|
-
if (!text.trim())
|
|
131
|
-
continue;
|
|
132
|
-
const runId = runIdFromMessageId(message.id);
|
|
133
|
-
const seq = seqFromMessageId(message.id);
|
|
134
|
-
if (buffer && buffer.role === message.role && bufferRunId === runId && runId && seq !== null && bufferSeq !== null && seq === bufferSeq + 1) {
|
|
135
|
-
bufferText += text;
|
|
136
|
-
buffer.ts = message.ts;
|
|
137
|
-
bufferSeq = seq;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
flush();
|
|
141
|
-
buffer = message;
|
|
142
|
-
bufferRunId = runId;
|
|
143
|
-
bufferSeq = seq;
|
|
144
|
-
bufferText = text;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
flush();
|
|
148
|
-
return compacted.slice(-limit).sort((a, b) => b.ts - a.ts);
|
|
149
|
-
}
|
|
150
|
-
async function readJson(req) {
|
|
151
|
-
const chunks = [];
|
|
152
|
-
let total = 0;
|
|
153
|
-
for await (const chunk of req) {
|
|
154
|
-
const buffer = Buffer.from(chunk);
|
|
155
|
-
total += buffer.byteLength;
|
|
156
|
-
if (Number.isFinite(MAX_MANAGER_IPC_BODY_BYTES) && MAX_MANAGER_IPC_BODY_BYTES > 0 && total > MAX_MANAGER_IPC_BODY_BYTES) {
|
|
157
|
-
throw new Error(`Manager IPC request body is too large. Max: ${MAX_MANAGER_IPC_BODY_BYTES} bytes.`);
|
|
158
|
-
}
|
|
159
|
-
chunks.push(buffer);
|
|
160
|
-
}
|
|
161
|
-
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
162
|
-
return raw ? JSON.parse(raw) : {};
|
|
163
|
-
}
|
|
164
|
-
function appJson(runtime, reqId, ok, payload) {
|
|
165
|
-
runtime.client.sendRes({
|
|
166
|
-
type: 'res',
|
|
167
|
-
id: reqId,
|
|
168
|
-
ok,
|
|
169
|
-
...(ok ? { payload } : { error: String(payload.error || 'unknown error') }),
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
function shouldFallbackToLocalChannel(error) {
|
|
173
|
-
return /binding not found|unknown method|not supported|relay is not connected|no external channel/i.test(error);
|
|
174
|
-
}
|
|
175
|
-
function managerIpcRuntimePath() {
|
|
176
|
-
return resolveShennianPath('runtime', 'manager-ipc.json');
|
|
177
|
-
}
|
|
178
|
-
export class ManagerRuntimeService {
|
|
179
|
-
opts;
|
|
180
|
-
registry = new ManagerRegistry();
|
|
181
|
-
channelRuntime = new ChannelRuntime((managerSessionId, event) => {
|
|
182
|
-
this.handleExternalMessage(managerSessionId, event);
|
|
183
|
-
}, (input) => this.registry.createReplyTarget(input).replyTarget);
|
|
184
|
-
server = null;
|
|
185
|
-
ipcUrl = null;
|
|
186
|
-
ipcToken = randomBytes(24).toString('hex');
|
|
187
|
-
healthTimer = null;
|
|
188
|
-
startPromise = null;
|
|
189
|
-
workerTextAcc = new Map();
|
|
190
|
-
constructor(opts) {
|
|
191
|
-
this.opts = opts;
|
|
192
|
-
}
|
|
193
|
-
async start() {
|
|
194
|
-
if (this.startPromise)
|
|
195
|
-
return this.startPromise;
|
|
196
|
-
this.startPromise = this.doStart();
|
|
197
|
-
return this.startPromise;
|
|
198
|
-
}
|
|
199
|
-
async doStart() {
|
|
200
|
-
if (this.server)
|
|
201
|
-
return;
|
|
202
|
-
this.server = http.createServer((req, res) => {
|
|
203
|
-
void this.handleIpc(req, res);
|
|
204
|
-
});
|
|
205
|
-
await new Promise((resolve, reject) => {
|
|
206
|
-
this.server.once('error', reject);
|
|
207
|
-
this.server.listen(0, '127.0.0.1', () => resolve());
|
|
208
|
-
});
|
|
209
|
-
const address = this.server.address();
|
|
210
|
-
if (typeof address === 'object' && address) {
|
|
211
|
-
this.ipcUrl = `http://127.0.0.1:${address.port}`;
|
|
212
|
-
this.writeIpcRuntimeFile();
|
|
213
|
-
}
|
|
214
|
-
this.server.unref();
|
|
215
|
-
await this.channelRuntime.start();
|
|
216
|
-
this.broadcastConfiguredChannelStatuses();
|
|
217
|
-
this.healthTimer = setInterval(() => this.scanWorkerHealth(), 60_000);
|
|
218
|
-
this.healthTimer.unref();
|
|
219
|
-
}
|
|
220
|
-
async ready() {
|
|
221
|
-
await this.start();
|
|
222
|
-
}
|
|
223
|
-
async stop() {
|
|
224
|
-
if (this.healthTimer)
|
|
225
|
-
clearInterval(this.healthTimer);
|
|
226
|
-
this.healthTimer = null;
|
|
227
|
-
await this.channelRuntime.stop();
|
|
228
|
-
await new Promise((resolve) => {
|
|
229
|
-
if (!this.server)
|
|
230
|
-
return resolve();
|
|
231
|
-
this.server.close(() => resolve());
|
|
232
|
-
});
|
|
233
|
-
this.server = null;
|
|
234
|
-
this.ipcUrl = null;
|
|
235
|
-
this.removeIpcRuntimeFile();
|
|
236
|
-
}
|
|
237
|
-
writeIpcRuntimeFile() {
|
|
238
|
-
if (!this.ipcUrl)
|
|
239
|
-
return;
|
|
240
|
-
try {
|
|
241
|
-
const filePath = managerIpcRuntimePath();
|
|
242
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
243
|
-
fs.writeFileSync(filePath, JSON.stringify({
|
|
244
|
-
url: this.ipcUrl,
|
|
245
|
-
token: this.ipcToken,
|
|
246
|
-
pid: process.pid,
|
|
247
|
-
updatedAt: new Date().toISOString(),
|
|
248
|
-
}, null, 2), { mode: 0o600 });
|
|
249
|
-
fs.chmodSync(filePath, 0o600);
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
// Best effort. Injected env remains the primary path for managed agents.
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
removeIpcRuntimeFile() {
|
|
256
|
-
try {
|
|
257
|
-
const filePath = managerIpcRuntimePath();
|
|
258
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
259
|
-
if (parsed.url === this.ipcUrl || parsed.token === this.ipcToken) {
|
|
260
|
-
fs.unlinkSync(filePath);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
// The file may not exist or may already have been replaced by a newer daemon.
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
getInjectedEnv(managerSessionId, agentSessionId, workDir, modelId) {
|
|
268
|
-
if (!this.ipcUrl)
|
|
269
|
-
return {};
|
|
270
|
-
return {
|
|
271
|
-
SHENNIAN_MANAGER_SESSION_ID: managerSessionId,
|
|
272
|
-
SHENNIAN_MANAGER_AGENT_SESSION_ID: agentSessionId ?? '',
|
|
273
|
-
SHENNIAN_MANAGER_WORKDIR: normalizeWorkDir(workDir),
|
|
274
|
-
SHENNIAN_MANAGER_MODEL: modelId,
|
|
275
|
-
SHENNIAN_MANAGER_IPC_URL: this.ipcUrl,
|
|
276
|
-
SHENNIAN_MANAGER_IPC_TOKEN: this.ipcToken,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
registerManager(input) {
|
|
280
|
-
this.registry.upsertManager({
|
|
281
|
-
...input,
|
|
282
|
-
workDir: normalizeWorkDir(input.workDir),
|
|
283
|
-
machineId: process.env.SHENNIAN_MACHINE_ID ?? null,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
setManagerWorkerDefaults(sessionId, agentType, modelId) {
|
|
287
|
-
const manager = this.registry.getManager(sessionId);
|
|
288
|
-
if (!manager)
|
|
289
|
-
return;
|
|
290
|
-
this.registry.upsertManager({
|
|
291
|
-
...manager,
|
|
292
|
-
defaultWorkerAgentType: agentType ?? null,
|
|
293
|
-
defaultWorkerModelId: modelId ?? null,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
getManagerWorkerDefaults(sessionId) {
|
|
297
|
-
const manager = this.registry.getManager(sessionId);
|
|
298
|
-
return {
|
|
299
|
-
agentType: manager?.defaultWorkerAgentType ?? null,
|
|
300
|
-
modelId: manager?.defaultWorkerModelId ?? null,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
noteManagerAgentSession(sessionId, agentSessionId, workDir, modelId) {
|
|
304
|
-
this.registry.upsertManager({
|
|
305
|
-
sessionId,
|
|
306
|
-
agentSessionId,
|
|
307
|
-
workDir: normalizeWorkDir(workDir),
|
|
308
|
-
machineId: process.env.SHENNIAN_MACHINE_ID ?? null,
|
|
309
|
-
modelId,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
noteAgentEvent(sessionId, event) {
|
|
313
|
-
const worker = this.findWorker(sessionId);
|
|
314
|
-
if (!worker)
|
|
315
|
-
return;
|
|
316
|
-
const patch = {
|
|
317
|
-
lastActivityAt: new Date().toISOString(),
|
|
318
|
-
runId: event.runId,
|
|
319
|
-
};
|
|
320
|
-
if (event.agentSessionId)
|
|
321
|
-
patch.agentSessionId = event.agentSessionId;
|
|
322
|
-
const textKey = `${sessionId}:${event.runId}`;
|
|
323
|
-
if (event.state === 'delta' && event.text && !event.thinking) {
|
|
324
|
-
const nextText = (this.workerTextAcc.get(textKey) ?? '') + event.text;
|
|
325
|
-
this.workerTextAcc.set(textKey, nextText);
|
|
326
|
-
const normalized = normalizeMarkdownForWorkerSummary(nextText);
|
|
327
|
-
if (normalized) {
|
|
328
|
-
patch.summary = normalized.length > 160 ? `${normalized.slice(0, 160)}...` : normalized;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (event.state === 'final' || event.state === 'error' || event.state === 'aborted') {
|
|
332
|
-
patch.status = event.state;
|
|
333
|
-
const accumulated = normalizeMarkdownForWorkerSummary(this.workerTextAcc.get(textKey) ?? '');
|
|
334
|
-
if (accumulated) {
|
|
335
|
-
patch.summary = accumulated.length > 240 ? `${accumulated.slice(0, 240)}...` : accumulated;
|
|
336
|
-
}
|
|
337
|
-
this.workerTextAcc.delete(textKey);
|
|
338
|
-
}
|
|
339
|
-
else if (event.state === 'start') {
|
|
340
|
-
patch.status = 'running';
|
|
341
|
-
}
|
|
342
|
-
const updated = this.registry.updateWorker(sessionId, patch);
|
|
343
|
-
if (updated && (event.state === 'final' || event.state === 'error' || event.state === 'aborted')) {
|
|
344
|
-
this.wakeManagerForWorker(updated.managedBy, updated, event.state, event.message);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
findWorker(sessionId) {
|
|
348
|
-
const registry = this.registry.load();
|
|
349
|
-
return registry.workers[sessionId];
|
|
350
|
-
}
|
|
351
|
-
async handleAppReq(req) {
|
|
352
|
-
const runtime = this.opts.getRuntime();
|
|
353
|
-
const body = (req.params ?? {});
|
|
354
|
-
try {
|
|
355
|
-
const managerSessionId = String(body.managerSessionId || body.sessionId || '');
|
|
356
|
-
if (!managerSessionId)
|
|
357
|
-
throw new Error('sessionId is required');
|
|
358
|
-
const manager = this.registry.getManager(managerSessionId);
|
|
359
|
-
if (req.method === 'manager.channel.get') {
|
|
360
|
-
appJson(runtime, req.id, true, {
|
|
361
|
-
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'websocket', { includeSecret: true }),
|
|
362
|
-
});
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (req.method === 'manager.channel.upsert') {
|
|
366
|
-
const channel = await this.channelRuntime.upsertManagerChannel({
|
|
367
|
-
id: String(body.id || `websocket:${managerSessionId}`),
|
|
368
|
-
managerSessionId,
|
|
369
|
-
sessionId: managerSessionId,
|
|
370
|
-
workDir: String(body.workDir || manager?.workDir || ''),
|
|
371
|
-
type: 'websocket',
|
|
372
|
-
name: typeof body.name === 'string' ? body.name : undefined,
|
|
373
|
-
agentType: typeof body.agentType === 'string' ? body.agentType : undefined,
|
|
374
|
-
agentSessionId: typeof body.agentSessionId === 'string' ? body.agentSessionId : null,
|
|
375
|
-
modelId: typeof body.modelId === 'string' ? body.modelId : null,
|
|
376
|
-
enabled: Boolean(body.enabled),
|
|
377
|
-
wsUrl: typeof body.wsUrl === 'string' ? body.wsUrl : undefined,
|
|
378
|
-
token: typeof body.token === 'string' ? body.token : undefined,
|
|
379
|
-
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
380
|
-
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
381
|
-
});
|
|
382
|
-
this.broadcastManagerChannelStatus(managerSessionId);
|
|
383
|
-
appJson(runtime, req.id, true, { channel });
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
throw new Error(`Unsupported manager app method: ${req.method}`);
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
appJson(runtime, req.id, false, { error: error instanceof Error ? error.message : String(error) });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
broadcastConfiguredChannelStatuses() {
|
|
393
|
-
for (const entry of this.channelRuntime.listManagerChannelStatuses()) {
|
|
394
|
-
this.broadcastManagerChannelStatus(entry.managerSessionId);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
broadcastManagerChannelStatus(managerSessionId) {
|
|
398
|
-
const runtime = this.opts.getRuntime();
|
|
399
|
-
if (!runtime.client?.sendEvent)
|
|
400
|
-
return;
|
|
401
|
-
const manager = this.registry.getManager(managerSessionId);
|
|
402
|
-
const status = this.channelRuntime.getManagerChannelStatus(managerSessionId);
|
|
403
|
-
runtime.client.sendEvent({
|
|
404
|
-
type: 'event',
|
|
405
|
-
event: 'session.update',
|
|
406
|
-
payload: {
|
|
407
|
-
session: {
|
|
408
|
-
id: managerSessionId,
|
|
409
|
-
agentType: manager ? 'manager' : undefined,
|
|
410
|
-
agentSessionId: manager?.agentSessionId ?? null,
|
|
411
|
-
modelId: manager?.modelId ?? null,
|
|
412
|
-
workDir: manager?.workDir,
|
|
413
|
-
externalChannel: status,
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
async handleIpc(req, res) {
|
|
419
|
-
if (req.headers.authorization !== `Bearer ${this.ipcToken}`) {
|
|
420
|
-
json(res, 401, { ok: false, error: 'Unauthorized' });
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
425
|
-
const body = await readJson(req);
|
|
426
|
-
const managerSessionId = String(body.managerSessionId || req.headers['x-shennian-manager-session-id'] || '');
|
|
427
|
-
if (!managerSessionId)
|
|
428
|
-
throw new Error('managerSessionId is required');
|
|
429
|
-
const manager = this.registry.getManager(managerSessionId);
|
|
430
|
-
if (url.pathname === '/sessions/list') {
|
|
431
|
-
if (!manager)
|
|
432
|
-
throw new Error('Manager runtime is not registered');
|
|
433
|
-
const runningSessionIds = new Set(this.opts.getRuntime().sessions.keys());
|
|
434
|
-
json(res, 200, {
|
|
435
|
-
ok: true,
|
|
436
|
-
sessions: this.registry.listWorkers(managerSessionId, { runningSessionIds }),
|
|
437
|
-
});
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
if (url.pathname === '/sessions/start') {
|
|
441
|
-
if (!manager)
|
|
442
|
-
throw new Error('Manager runtime is not registered');
|
|
443
|
-
const agentType = String(body.agentType || body.agent || manager.defaultWorkerAgentType || 'codex');
|
|
444
|
-
if (!isManagerWorkerAgentType(agentType)) {
|
|
445
|
-
throw new Error(`Unsupported manager worker agent: ${agentType}`);
|
|
446
|
-
}
|
|
447
|
-
const workDir = normalizeWorkDir(String(body.workDir || manager.workDir));
|
|
448
|
-
if (workDir !== manager.workDir)
|
|
449
|
-
throw new Error('Manager can only start workers in the same workDir');
|
|
450
|
-
const message = String(body.message || '');
|
|
451
|
-
if (!message)
|
|
452
|
-
throw new Error('message is required');
|
|
453
|
-
const worker = this.registry.addWorker({
|
|
454
|
-
managerSessionId,
|
|
455
|
-
agentType,
|
|
456
|
-
workDir,
|
|
457
|
-
summary: message.slice(0, 120),
|
|
458
|
-
});
|
|
459
|
-
const workerModelId = String(body.modelId || (agentType === manager.defaultWorkerAgentType ? manager.defaultWorkerModelId ?? '' : ''));
|
|
460
|
-
await this.dispatchChatSend(worker.sessionId, agentType, workDir, message, null, workerModelId);
|
|
461
|
-
json(res, 200, { ok: true, session: worker });
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (url.pathname === '/sessions/send') {
|
|
465
|
-
const sessionId = String(body.sessionId || '');
|
|
466
|
-
const message = String(body.message || '');
|
|
467
|
-
const enqueue = body.enqueue === undefined ? true : Boolean(body.enqueue);
|
|
468
|
-
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
469
|
-
if (!worker)
|
|
470
|
-
throw new Error('Worker not found in this manager scope');
|
|
471
|
-
const workerModelId = String(body.modelId || (worker.agentType === manager?.defaultWorkerAgentType ? manager.defaultWorkerModelId ?? '' : ''));
|
|
472
|
-
if (enqueue) {
|
|
473
|
-
await this.dispatchChatEnqueue(worker.sessionId, worker.agentType, worker.workDir, message, worker.agentSessionId ?? null, workerModelId);
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
await this.dispatchChatSend(worker.sessionId, worker.agentType, worker.workDir, message, worker.agentSessionId ?? null, workerModelId);
|
|
477
|
-
}
|
|
478
|
-
json(res, 200, { ok: true });
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (url.pathname === '/sessions/queue') {
|
|
482
|
-
const sessionId = String(body.sessionId || '');
|
|
483
|
-
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
484
|
-
if (!worker)
|
|
485
|
-
throw new Error('Worker not found in this manager scope');
|
|
486
|
-
const queue = this.opts.getRuntime().chatQueue?.getSnapshot(sessionId);
|
|
487
|
-
json(res, 200, { ok: true, queue });
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
if (url.pathname === '/sessions/queue/edit') {
|
|
491
|
-
const sessionId = String(body.sessionId || '');
|
|
492
|
-
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
493
|
-
if (!worker)
|
|
494
|
-
throw new Error('Worker not found in this manager scope');
|
|
495
|
-
await this.opts.dispatchReq({
|
|
496
|
-
type: 'req',
|
|
497
|
-
id: `manager-queue-edit-${randomUUID()}`,
|
|
498
|
-
method: 'chat.queue.edit',
|
|
499
|
-
params: {
|
|
500
|
-
sessionId,
|
|
501
|
-
queueMessageId: String(body.queueMessageId || body.messageId || ''),
|
|
502
|
-
text: String(body.message || body.text || ''),
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
json(res, 200, { ok: true, queue: this.opts.getRuntime().chatQueue?.getSnapshot(sessionId) });
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
if (url.pathname === '/sessions/queue/delete') {
|
|
509
|
-
const sessionId = String(body.sessionId || '');
|
|
510
|
-
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
511
|
-
if (!worker)
|
|
512
|
-
throw new Error('Worker not found in this manager scope');
|
|
513
|
-
await this.opts.dispatchReq({
|
|
514
|
-
type: 'req',
|
|
515
|
-
id: `manager-queue-delete-${randomUUID()}`,
|
|
516
|
-
method: 'chat.queue.delete',
|
|
517
|
-
params: {
|
|
518
|
-
sessionId,
|
|
519
|
-
queueMessageId: String(body.queueMessageId || body.messageId || ''),
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
json(res, 200, { ok: true, queue: this.opts.getRuntime().chatQueue?.getSnapshot(sessionId) });
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
if (url.pathname === '/sessions/stop' || url.pathname === '/sessions/terminate') {
|
|
526
|
-
const sessionId = String(body.sessionId || '');
|
|
527
|
-
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
528
|
-
if (!worker)
|
|
529
|
-
throw new Error('Worker not found in this manager scope');
|
|
530
|
-
await this.opts.dispatchReq({ type: 'req', id: `manager-abort-${randomUUID()}`, method: 'chat.abort', params: { sessionId } });
|
|
531
|
-
this.registry.updateWorker(sessionId, { status: 'aborted' });
|
|
532
|
-
json(res, 200, { ok: true });
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
if (url.pathname === '/sessions/read') {
|
|
536
|
-
const sessionId = String(body.sessionId || '');
|
|
537
|
-
const limit = Number(body.limit || 200);
|
|
538
|
-
const worker = this.registry.getWorkerForManager(managerSessionId, sessionId);
|
|
539
|
-
if (!worker)
|
|
540
|
-
throw new Error('Worker not found in this manager scope');
|
|
541
|
-
const rawMessages = readMessages(sessionId, { limit: Math.max(limit * 20, limit) });
|
|
542
|
-
json(res, 200, {
|
|
543
|
-
ok: true,
|
|
544
|
-
messages: compactWorkerTranscript(rawMessages, limit),
|
|
545
|
-
rawMessageCount: rawMessages.length,
|
|
546
|
-
});
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
if (url.pathname === '/memory/path') {
|
|
550
|
-
if (!manager)
|
|
551
|
-
throw new Error('Manager runtime is not registered');
|
|
552
|
-
json(res, 200, { ok: true, path: path.join(manager.workDir, '.shennian') });
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (url.pathname === '/external/reply') {
|
|
556
|
-
const replyTarget = typeof body.replyTarget === 'string'
|
|
557
|
-
? this.registry.getReplyTarget(body.replyTarget)
|
|
558
|
-
: this.registry.getLatestReplyTargetForManager(managerSessionId);
|
|
559
|
-
const text = String(body.text || '');
|
|
560
|
-
const attachment = parseExternalReplyAttachment(body.attachment);
|
|
561
|
-
const idempotencyKey = String(body.idempotencyKey || randomUUID());
|
|
562
|
-
const explicitChannelId = String(body.channelId || '');
|
|
563
|
-
const explicitConversationId = String(body.conversationId || '');
|
|
564
|
-
const defaultTarget = !replyTarget && (!explicitChannelId || !explicitConversationId)
|
|
565
|
-
? await this.channelRuntime.getDefaultReplyTarget(managerSessionId).catch(() => null)
|
|
566
|
-
: null;
|
|
567
|
-
const channelId = replyTarget?.channelId || explicitChannelId || defaultTarget?.channelId || '';
|
|
568
|
-
const conversationId = replyTarget?.conversationId || explicitConversationId || defaultTarget?.conversationId || '';
|
|
569
|
-
if (channelId && this.channelRuntime.getChannelById(channelId)) {
|
|
570
|
-
if (!conversationId)
|
|
571
|
-
throw new Error('No external channel target is available for this Manager');
|
|
572
|
-
const result = await this.channelRuntime.reply({
|
|
573
|
-
managerSessionId,
|
|
574
|
-
channelId,
|
|
575
|
-
conversationId,
|
|
576
|
-
messageId: replyTarget?.messageId ?? undefined,
|
|
577
|
-
text,
|
|
578
|
-
attachment,
|
|
579
|
-
idempotencyKey,
|
|
580
|
-
});
|
|
581
|
-
json(res, result.ok ? 200 : 400, result);
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
let relayResult;
|
|
585
|
-
try {
|
|
586
|
-
relayResult = await this.sendManagedWeComReply({
|
|
587
|
-
managerSessionId,
|
|
588
|
-
text,
|
|
589
|
-
attachment,
|
|
590
|
-
idempotencyKey,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
594
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
595
|
-
if (!shouldFallbackToLocalChannel(message))
|
|
596
|
-
throw error;
|
|
597
|
-
relayResult = { ok: false, error: message };
|
|
598
|
-
}
|
|
599
|
-
if (relayResult.ok) {
|
|
600
|
-
json(res, 200, { ok: true, payload: relayResult.payload });
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
if (!shouldFallbackToLocalChannel(relayResult.error || '') || !channelId || !conversationId) {
|
|
604
|
-
json(res, 400, { ok: false, error: relayResult.error || 'External send failed' });
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
json(res, 400, { ok: false, error: `No local external channel is configured for ${channelId}` });
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
if (url.pathname === '/channel/get') {
|
|
611
|
-
json(res, 200, {
|
|
612
|
-
ok: true,
|
|
613
|
-
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'websocket'),
|
|
614
|
-
});
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (url.pathname === '/channel/upsert') {
|
|
618
|
-
if (!manager)
|
|
619
|
-
throw new Error('Manager runtime is not registered');
|
|
620
|
-
const channel = await this.channelRuntime.upsertManagerChannel({
|
|
621
|
-
id: String(body.id || `websocket:${managerSessionId}`),
|
|
622
|
-
managerSessionId,
|
|
623
|
-
workDir: manager.workDir,
|
|
624
|
-
type: 'websocket',
|
|
625
|
-
name: typeof body.name === 'string' ? body.name : undefined,
|
|
626
|
-
enabled: Boolean(body.enabled),
|
|
627
|
-
wsUrl: typeof body.wsUrl === 'string' ? body.wsUrl : undefined,
|
|
628
|
-
token: typeof body.token === 'string' ? body.token : undefined,
|
|
629
|
-
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
630
|
-
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
631
|
-
});
|
|
632
|
-
this.registry.upsertManager({
|
|
633
|
-
...manager,
|
|
634
|
-
status: manager.status,
|
|
635
|
-
});
|
|
636
|
-
this.broadcastManagerChannelStatus(managerSessionId);
|
|
637
|
-
json(res, 200, { ok: true, channel });
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
if (url.pathname === '/wechat-rpa/channel/get') {
|
|
641
|
-
json(res, 200, {
|
|
642
|
-
ok: true,
|
|
643
|
-
channel: this.channelRuntime.getManagerChannel(managerSessionId, 'wechat-rpa', { includeSecret: true }),
|
|
644
|
-
});
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (url.pathname === '/wechat-rpa/channel/upsert') {
|
|
648
|
-
if (!manager)
|
|
649
|
-
throw new Error('Manager runtime is not registered');
|
|
650
|
-
const channel = await this.channelRuntime.upsertManagerWeChatRpaChannel({
|
|
651
|
-
id: String(body.id || `wechat-rpa:${managerSessionId}`),
|
|
652
|
-
managerSessionId,
|
|
653
|
-
workDir: manager.workDir,
|
|
654
|
-
name: typeof body.name === 'string' ? body.name : undefined,
|
|
655
|
-
enabled: Boolean(body.enabled),
|
|
656
|
-
groups: parseWeChatRpaGroups(body.groups),
|
|
657
|
-
canReply: body.canReply === undefined ? undefined : Boolean(body.canReply),
|
|
658
|
-
systemPrompt: typeof body.systemPrompt === 'string' ? body.systemPrompt : undefined,
|
|
659
|
-
source: parseWeChatRpaSource(body.source),
|
|
660
|
-
pollIntervalMs: optionalNumber(body.pollIntervalMs),
|
|
661
|
-
recentLimit: optionalNumber(body.recentLimit),
|
|
662
|
-
idleSeconds: optionalNumber(body.idleSeconds),
|
|
663
|
-
forceForeground: body.forceForeground === undefined ? undefined : Boolean(body.forceForeground),
|
|
664
|
-
noRestore: body.noRestore === undefined ? undefined : Boolean(body.noRestore),
|
|
665
|
-
downloadAttachments: body.downloadAttachments === undefined ? undefined : Boolean(body.downloadAttachments),
|
|
666
|
-
downloadAttachmentsDir: typeof body.downloadAttachmentsDir === 'string' ? body.downloadAttachmentsDir : undefined,
|
|
667
|
-
privacyConsentAccepted: body.privacyConsentAccepted === undefined ? undefined : Boolean(body.privacyConsentAccepted),
|
|
668
|
-
flowScriptPath: typeof body.flowScriptPath === 'string' ? body.flowScriptPath : undefined,
|
|
669
|
-
});
|
|
670
|
-
this.registry.upsertManager({
|
|
671
|
-
...manager,
|
|
672
|
-
status: manager.status,
|
|
673
|
-
});
|
|
674
|
-
this.broadcastManagerChannelStatus(managerSessionId);
|
|
675
|
-
json(res, 200, { ok: true, channel });
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (url.pathname === '/wechat-rpa/channel/sync') {
|
|
679
|
-
const { channel, messages } = await this.channelRuntime.syncManagerWeChatRpaChannel(managerSessionId);
|
|
680
|
-
this.broadcastManagerChannelStatus(managerSessionId);
|
|
681
|
-
json(res, 200, { ok: true, channel, messages });
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
json(res, 404, { ok: false, error: `Unknown manager IPC path: ${url.pathname}` });
|
|
685
|
-
}
|
|
686
|
-
catch (err) {
|
|
687
|
-
json(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
async dispatchChatSend(sessionId, agentType, workDir, text, agentSessionId, modelId) {
|
|
691
|
-
await this.opts.dispatchReq({
|
|
692
|
-
type: 'req',
|
|
693
|
-
id: `manager-send-${randomUUID()}`,
|
|
694
|
-
method: 'chat.send',
|
|
695
|
-
params: { sessionId, text, agentType, workDir, agentSessionId, modelId },
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
async dispatchChatEnqueue(sessionId, agentType, workDir, text, agentSessionId, modelId) {
|
|
699
|
-
await this.opts.dispatchReq({
|
|
700
|
-
type: 'req',
|
|
701
|
-
id: `manager-enqueue-${randomUUID()}`,
|
|
702
|
-
method: 'chat.enqueue',
|
|
703
|
-
params: { sessionId, text, agentType, workDir, agentSessionId, modelId },
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
async sendManagedWeComReply(input) {
|
|
707
|
-
const parts = splitExternalReplyText(input.text);
|
|
708
|
-
if (!parts.length && !input.attachment)
|
|
709
|
-
return { ok: false, error: 'text or attachment is required' };
|
|
710
|
-
const client = this.opts.getRuntime().client;
|
|
711
|
-
if (!client || typeof client.sendReq !== 'function') {
|
|
712
|
-
return { ok: false, error: 'Relay is not connected' };
|
|
713
|
-
}
|
|
714
|
-
const payloads = [];
|
|
715
|
-
for (const [index, text] of parts.entries()) {
|
|
716
|
-
const frame = await client.sendReq({
|
|
717
|
-
type: 'req',
|
|
718
|
-
id: `external-send-${randomUUID()}`,
|
|
719
|
-
method: 'external.send',
|
|
720
|
-
params: {
|
|
721
|
-
managerSessionId: input.managerSessionId,
|
|
722
|
-
text,
|
|
723
|
-
idempotencyKey: parts.length > 1 ? `${input.idempotencyKey}:${index + 1}` : input.idempotencyKey,
|
|
724
|
-
},
|
|
725
|
-
});
|
|
726
|
-
if (!frame.ok)
|
|
727
|
-
return { ok: false, error: frame.error || 'External send failed' };
|
|
728
|
-
payloads.push(frame.payload);
|
|
729
|
-
}
|
|
730
|
-
if (input.attachment) {
|
|
731
|
-
const frame = await client.sendReq({
|
|
732
|
-
type: 'req',
|
|
733
|
-
id: `external-send-${randomUUID()}`,
|
|
734
|
-
method: 'external.send',
|
|
735
|
-
params: {
|
|
736
|
-
managerSessionId: input.managerSessionId,
|
|
737
|
-
attachment: input.attachment,
|
|
738
|
-
idempotencyKey: parts.length ? `${input.idempotencyKey}:attachment` : input.idempotencyKey,
|
|
739
|
-
},
|
|
740
|
-
});
|
|
741
|
-
if (!frame.ok)
|
|
742
|
-
return { ok: false, error: frame.error || 'External send failed' };
|
|
743
|
-
payloads.push(frame.payload);
|
|
744
|
-
}
|
|
745
|
-
return { ok: true, payload: payloads.length === 1 ? payloads[0] : payloads };
|
|
746
|
-
}
|
|
747
|
-
wakeManagerForWorker(managerSessionId, worker, state, message) {
|
|
748
|
-
const manager = this.registry.getManager(managerSessionId);
|
|
749
|
-
if (!manager)
|
|
750
|
-
return;
|
|
751
|
-
const prompt = `你管理的 worker 已结束。
|
|
1
|
+
import W from"node:http";import{randomBytes as q,randomUUID as g}from"node:crypto";import k from"node:fs";import v from"node:os";import I from"node:path";import{AVAILABLE_BUILTIN_AGENT_TYPES as _,extractPayloadText as B,isToolPayload as F}from"@shennian/wire";import{ManagerRegistry as U}from"./registry.js";import{readMessages as L}from"../session/store.js";import{ChannelRuntime as O}from"../channels/runtime.js";import{splitExternalReplyText as H}from"../channels/reply-split.js";import{resolveShennianPath as G}from"../config/index.js";import{buildExternalChannelInstructions as j}from"../agents/external-channel-instructions.js";const M=Number(process.env.SHENNIAN_MANAGER_IPC_BODY_MAX_BYTES||2*1024*1024);let x=null;function ke(a){x=a}function Ie(){return x}function R(a){return I.resolve(a||v.homedir())}function u(a,e,r){a.writeHead(e,{"content-type":"application/json; charset=utf-8"}),a.end(JSON.stringify(r))}function K(a){return/^agent-(.+)-\d+$/.exec(a)?.[1]??null}function z(a){return a==="manager"?!1:a.startsWith("custom:")?!0:_.includes(a)}function J(a){const e=/^agent-.+-(\d+)$/.exec(a);if(!e)return null;const r=Number(e[1]);return Number.isInteger(r)&&r>=0?r:null}function T(a){return a.replace(/\r\n/g,`
|
|
2
|
+
`).trim()}function Y(a){try{const e=JSON.parse(a),r=e.type==="tool_result"||e.result?"tool_result":"tool_call",n=e.name||"tool",t=typeof e.result=="string"?e.result.replace(/\s+/g," ").trim():"",s=t.length>220?`${t.slice(0,220)}...`:t;return s?`[${r}] ${n}: ${s}`:`[${r}] ${n}`}catch{return"[tool]"}}function Q(a){if(!a||typeof a!="object")return;const e=a,r=String(e.kind||""),n=String(e.name||""),t=String(e.mimeType||""),s=String(e.dataBase64||""),o=String(e.localPath||""),i=String(e.url||""),l=Number(e.size||0);if(s)throw new Error("Manager IPC external attachments must use localPath or url; dataBase64 is not accepted");if(!(r!=="image"&&r!=="video"&&r!=="file")&&!(!n||!t||!Number.isFinite(l)||l<0)&&!(!o&&!i))return{kind:r,name:n,mimeType:t,size:l,...o?{localPath:o}:{},...i?{url:i}:{}}}function X(a,e){const r=[...a].sort((c,d)=>c.ts-d.ts),n=[];let t=null,s=null,o=null,i="";const l=()=>{if(!t)return;const c=i.trim();c&&n.push({...t,id:`${t.id}-compact`,payload:c}),t=null,s=null,o=null,i=""};for(const c of r){if(c.role==="user"){l(),n.push(c);continue}if(F(c.payload)){l(),n.push({...c,payload:Y(c.payload)});continue}const d=B(c.payload);if(!d.trim())continue;const h=K(c.id),p=J(c.id);t&&t.role===c.role&&s===h&&h&&p!==null&&o!==null&&p===o+1?(i+=d,t.ts=c.ts,o=p):(l(),t=c,s=h,o=p,i=d)}return l(),n.slice(-e).sort((c,d)=>d.ts-c.ts)}async function V(a){const e=[];let r=0;for await(const t of a){const s=Buffer.from(t);if(r+=s.byteLength,Number.isFinite(M)&&M>0&&r>M)throw new Error(`Manager IPC request body is too large. Max: ${M} bytes.`);e.push(s)}const n=Buffer.concat(e).toString("utf-8");return n?JSON.parse(n):{}}function A(a,e,r,n){a.client.sendRes({type:"res",id:e,ok:r,...r?{payload:n}:{error:String(n.error||"unknown error")}})}function C(a){return/binding not found|unknown method|not supported|relay is not connected|no external channel/i.test(a)}function N(){return G("runtime","manager-ipc.json")}class Se{opts;registry=new U;channelRuntime=new O((e,r)=>{this.handleExternalMessage(e,r)},e=>this.registry.createReplyTarget(e).replyTarget);server=null;ipcUrl=null;ipcToken=q(24).toString("hex");healthTimer=null;startPromise=null;workerTextAcc=new Map;constructor(e){this.opts=e}async start(){return this.startPromise?this.startPromise:(this.startPromise=this.doStart(),this.startPromise)}async doStart(){if(this.server)return;this.server=W.createServer((r,n)=>{this.handleIpc(r,n)}),await new Promise((r,n)=>{this.server.once("error",n),this.server.listen(0,"127.0.0.1",()=>r())});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.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=N();k.mkdirSync(I.dirname(e),{recursive:!0}),k.writeFileSync(e,JSON.stringify({url:this.ipcUrl,token:this.ipcToken,pid:process.pid,updatedAt:new Date().toISOString()},null,2),{mode:384}),k.chmodSync(e,384)}catch{}}removeIpcRuntimeFile(){try{const e=N(),r=JSON.parse(k.readFileSync(e,"utf-8"));(r.url===this.ipcUrl||r.token===this.ipcToken)&&k.unlinkSync(e)}catch{}}getInjectedEnv(e,r,n,t){return this.ipcUrl?{SHENNIAN_MANAGER_SESSION_ID:e,SHENNIAN_MANAGER_AGENT_SESSION_ID:r??"",SHENNIAN_MANAGER_WORKDIR:R(n),SHENNIAN_MANAGER_MODEL:t,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,r,n){const t=this.registry.getManager(e);t&&this.registry.upsertManager({...t,defaultWorkerAgentType:r??null,defaultWorkerModelId:n??null})}getManagerWorkerDefaults(e){const r=this.registry.getManager(e);return{agentType:r?.defaultWorkerAgentType??null,modelId:r?.defaultWorkerModelId??null}}noteManagerAgentSession(e,r,n,t){this.registry.upsertManager({sessionId:e,agentSessionId:r,workDir:R(n),machineId:process.env.SHENNIAN_MACHINE_ID??null,modelId:t})}noteAgentEvent(e,r){if(!this.findWorker(e))return;const t={lastActivityAt:new Date().toISOString(),runId:r.runId};r.agentSessionId&&(t.agentSessionId=r.agentSessionId);const s=`${e}:${r.runId}`;if(r.state==="delta"&&r.text&&!r.thinking){const i=(this.workerTextAcc.get(s)??"")+r.text;this.workerTextAcc.set(s,i);const l=T(i);l&&(t.summary=l.length>160?`${l.slice(0,160)}...`:l)}if(r.state==="final"||r.state==="error"||r.state==="aborted"){t.status=r.state;const i=T(this.workerTextAcc.get(s)??"");i&&(t.summary=i.length>240?`${i.slice(0,240)}...`:i),this.workerTextAcc.delete(s)}else r.state==="start"&&(t.status="running");const o=this.registry.updateWorker(e,t);o&&(r.state==="final"||r.state==="error"||r.state==="aborted")&&this.wakeManagerForWorker(o.managedBy,o,r.state,r.message)}findWorker(e){return this.registry.load().workers[e]}async handleAppReq(e){const r=this.opts.getRuntime(),n=e.params??{};try{const t=String(n.managerSessionId||n.sessionId||"");if(!t)throw new Error("sessionId is required");const s=this.registry.getManager(t);if(e.method==="manager.channel.get"){A(r,e.id,!0,{channel:this.channelRuntime.getManagerChannel(t,"websocket",{includeSecret:!0})});return}if(e.method==="manager.channel.upsert"){const o=await this.channelRuntime.upsertManagerChannel({id:String(n.id||`websocket:${t}`),managerSessionId:t,sessionId:t,workDir:String(n.workDir||s?.workDir||""),type:"websocket",name:typeof n.name=="string"?n.name:void 0,agentType:typeof n.agentType=="string"?n.agentType:void 0,agentSessionId:typeof n.agentSessionId=="string"?n.agentSessionId:null,modelId:typeof n.modelId=="string"?n.modelId:null,enabled:!!n.enabled,wsUrl:typeof n.wsUrl=="string"?n.wsUrl:void 0,token:typeof n.token=="string"?n.token:void 0,canReply:n.canReply===void 0?void 0:!!n.canReply,systemPrompt:typeof n.systemPrompt=="string"?n.systemPrompt:void 0});this.broadcastManagerChannelStatus(t),A(r,e.id,!0,{channel:o});return}throw new Error(`Unsupported manager app method: ${e.method}`)}catch(t){A(r,e.id,!1,{error:t instanceof Error?t.message:String(t)})}}broadcastConfiguredChannelStatuses(){for(const e of this.channelRuntime.listManagerChannelStatuses())this.broadcastManagerChannelStatus(e.managerSessionId)}broadcastManagerChannelStatus(e){const r=this.opts.getRuntime();if(!r.client?.sendEvent)return;const n=this.registry.getManager(e),t=this.channelRuntime.getManagerChannelStatus(e);r.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:e,agentType:n?"manager":void 0,agentSessionId:n?.agentSessionId??null,modelId:n?.modelId??null,workDir:n?.workDir,externalChannel:t}}})}async handleIpc(e,r){if(e.headers.authorization!==`Bearer ${this.ipcToken}`){u(r,401,{ok:!1,error:"Unauthorized"});return}try{const n=new URL(e.url??"/","http://127.0.0.1"),t=await V(e),s=String(t.managerSessionId||e.headers["x-shennian-manager-session-id"]||"");if(!s)throw new Error("managerSessionId is required");const o=this.registry.getManager(s);if(n.pathname==="/sessions/list"){if(!o)throw new Error("Manager runtime is not registered");const i=new Set(this.opts.getRuntime().sessions.keys());u(r,200,{ok:!0,sessions:this.registry.listWorkers(s,{runningSessionIds:i})});return}if(n.pathname==="/sessions/start"){if(!o)throw new Error("Manager runtime is not registered");const i=String(t.agentType||t.agent||o.defaultWorkerAgentType||"codex");if(!z(i))throw new Error(`Unsupported manager worker agent: ${i}`);const l=R(String(t.workDir||o.workDir));if(l!==o.workDir)throw new Error("Manager can only start workers in the same workDir");const c=String(t.message||"");if(!c)throw new Error("message is required");const d=this.registry.addWorker({managerSessionId:s,agentType:i,workDir:l,summary:c.slice(0,120)}),h=String(t.modelId||(i===o.defaultWorkerAgentType?o.defaultWorkerModelId??"":""));await this.dispatchChatSend(d.sessionId,i,l,c,null,h),u(r,200,{ok:!0,session:d});return}if(n.pathname==="/sessions/send"){const i=String(t.sessionId||""),l=String(t.message||""),c=t.enqueue===void 0?!0:!!t.enqueue,d=this.registry.getWorkerForManager(s,i);if(!d)throw new Error("Worker not found in this manager scope");const h=String(t.modelId||(d.agentType===o?.defaultWorkerAgentType?o.defaultWorkerModelId??"":""));c?await this.dispatchChatEnqueue(d.sessionId,d.agentType,d.workDir,l,d.agentSessionId??null,h):await this.dispatchChatSend(d.sessionId,d.agentType,d.workDir,l,d.agentSessionId??null,h),u(r,200,{ok:!0});return}if(n.pathname==="/sessions/queue"){const i=String(t.sessionId||"");if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");const c=this.opts.getRuntime().chatQueue?.getSnapshot(i);u(r,200,{ok:!0,queue:c});return}if(n.pathname==="/sessions/queue/edit"){const i=String(t.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-${g()}`,method:"chat.queue.edit",params:{sessionId:i,queueMessageId:String(t.queueMessageId||t.messageId||""),text:String(t.message||t.text||"")}}),u(r,200,{ok:!0,queue:this.opts.getRuntime().chatQueue?.getSnapshot(i)});return}if(n.pathname==="/sessions/queue/delete"){const i=String(t.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-${g()}`,method:"chat.queue.delete",params:{sessionId:i,queueMessageId:String(t.queueMessageId||t.messageId||"")}}),u(r,200,{ok:!0,queue:this.opts.getRuntime().chatQueue?.getSnapshot(i)});return}if(n.pathname==="/sessions/stop"||n.pathname==="/sessions/terminate"){const i=String(t.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-${g()}`,method:"chat.abort",params:{sessionId:i}}),this.registry.updateWorker(i,{status:"aborted"}),u(r,200,{ok:!0});return}if(n.pathname==="/sessions/read"){const i=String(t.sessionId||""),l=Number(t.limit||200);if(!this.registry.getWorkerForManager(s,i))throw new Error("Worker not found in this manager scope");const d=L(i,{limit:Math.max(l*20,l)});u(r,200,{ok:!0,messages:X(d,l),rawMessageCount:d.length});return}if(n.pathname==="/memory/path"){if(!o)throw new Error("Manager runtime is not registered");u(r,200,{ok:!0,path:I.join(o.workDir,".shennian")});return}if(n.pathname==="/external/reply"){const i=typeof t.replyTarget=="string"?this.registry.getReplyTarget(t.replyTarget):this.registry.getLatestReplyTargetForManager(s),l=String(t.text||""),c=Q(t.attachment),d=String(t.idempotencyKey||g()),h=String(t.channelId||""),p=String(t.conversationId||""),S=!i&&(!h||!p)?await this.channelRuntime.getDefaultReplyTarget(s).catch(()=>null):null,y=i?.channelId||h||S?.channelId||"",w=i?.conversationId||p||S?.conversationId||"";if(y&&this.channelRuntime.getChannelById(y)){if(!w)throw new Error("No external channel target is available for this Manager");const f=await this.channelRuntime.reply({managerSessionId:s,channelId:y,conversationId:w,messageId:i?.messageId??void 0,text:l,attachment:c,idempotencyKey:d});u(r,f.ok?200:400,f);return}let m;try{m=await this.sendManagedWeComReply({managerSessionId:s,text:l,attachment:c,idempotencyKey:d})}catch(f){const E=f instanceof Error?f.message:String(f);if(!C(E))throw f;m={ok:!1,error:E}}if(m.ok){u(r,200,{ok:!0,payload:m.payload});return}if(!C(m.error||"")||!y||!w){u(r,400,{ok:!1,error:m.error||"External send failed"});return}u(r,400,{ok:!1,error:`No local external channel is configured for ${y}`});return}if(n.pathname==="/channel/get"){u(r,200,{ok:!0,channel:this.channelRuntime.getManagerChannel(s,"websocket")});return}if(n.pathname==="/channel/upsert"){if(!o)throw new Error("Manager runtime is not registered");const i=await this.channelRuntime.upsertManagerChannel({id:String(t.id||`websocket:${s}`),managerSessionId:s,workDir:o.workDir,type:"websocket",name:typeof t.name=="string"?t.name:void 0,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.registry.upsertManager({...o,status:o.status}),this.broadcastManagerChannelStatus(s),u(r,200,{ok:!0,channel:i});return}if(n.pathname==="/wechat-rpa/channel/get"){u(r,200,{ok:!0,channel:this.channelRuntime.getManagerChannel(s,"wechat-rpa",{includeSecret:!0})});return}if(n.pathname==="/wechat-rpa/channel/upsert"){if(!o)throw new Error("Manager runtime is not registered");const i=await this.channelRuntime.upsertManagerWeChatRpaChannel({id:String(t.id||`wechat-rpa:${s}`),managerSessionId:s,workDir:o.workDir,name:typeof t.name=="string"?t.name:void 0,enabled:!!t.enabled,groups:Z(t.groups),canReply:t.canReply===void 0?void 0:!!t.canReply,systemPrompt:typeof t.systemPrompt=="string"?t.systemPrompt:void 0,source:ee(t.source),pollIntervalMs:b(t.pollIntervalMs),recentLimit:b(t.recentLimit),idleSeconds:b(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,privacyConsentAccepted:t.privacyConsentAccepted===void 0?void 0:!!t.privacyConsentAccepted,flowScriptPath:typeof t.flowScriptPath=="string"?t.flowScriptPath:void 0});this.registry.upsertManager({...o,status:o.status}),this.broadcastManagerChannelStatus(s),u(r,200,{ok:!0,channel:i});return}if(n.pathname==="/wechat-rpa/channel/sync"){const{channel:i,messages:l}=await this.channelRuntime.syncManagerWeChatRpaChannel(s);this.broadcastManagerChannelStatus(s),u(r,200,{ok:!0,channel:i,messages:l});return}u(r,404,{ok:!1,error:`Unknown manager IPC path: ${n.pathname}`})}catch(n){u(r,400,{ok:!1,error:n instanceof Error?n.message:String(n)})}}async dispatchChatSend(e,r,n,t,s,o){await this.opts.dispatchReq({type:"req",id:`manager-send-${g()}`,method:"chat.send",params:{sessionId:e,text:t,agentType:r,workDir:n,agentSessionId:s,modelId:o}})}async dispatchChatEnqueue(e,r,n,t,s,o){await this.opts.dispatchReq({type:"req",id:`manager-enqueue-${g()}`,method:"chat.enqueue",params:{sessionId:e,text:t,agentType:r,workDir:n,agentSessionId:s,modelId:o}})}async sendManagedWeComReply(e){const r=H(e.text);if(!r.length&&!e.attachment)return{ok:!1,error:"text or attachment is required"};const n=this.opts.getRuntime().client;if(!n||typeof n.sendReq!="function")return{ok:!1,error:"Relay is not connected"};const t=[];for(const[s,o]of r.entries()){const i=await n.sendReq({type:"req",id:`external-send-${g()}`,method:"external.send",params:{managerSessionId:e.managerSessionId,text:o,idempotencyKey:r.length>1?`${e.idempotencyKey}:${s+1}`:e.idempotencyKey}});if(!i.ok)return{ok:!1,error:i.error||"External send failed"};t.push(i.payload)}if(e.attachment){const s=await n.sendReq({type:"req",id:`external-send-${g()}`,method:"external.send",params:{managerSessionId:e.managerSessionId,attachment:e.attachment,idempotencyKey:r.length?`${e.idempotencyKey}:attachment`:e.idempotencyKey}});if(!s.ok)return{ok:!1,error:s.error||"External send failed"};t.push(s.payload)}return{ok:!0,payload:t.length===1?t[0]:t}}wakeManagerForWorker(e,r,n,t){const s=this.registry.getManager(e);if(!s)return;const o=`\u4F60\u7BA1\u7406\u7684 worker \u5DF2\u7ED3\u675F\u3002
|
|
752
3
|
|
|
753
|
-
Worker: ${
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
${
|
|
4
|
+
Worker: ${r.sessionId}
|
|
5
|
+
\u72B6\u6001\uFF1A${n}
|
|
6
|
+
\u6700\u7EC8\u7ED3\u679C\uFF1A
|
|
7
|
+
${t||r.summary||"(\u65E0\u53EF\u89C1\u6458\u8981)"}
|
|
757
8
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
handleExternalMessage(managerSessionId, event) {
|
|
762
|
-
const config = this.channelRuntime.getChannelById(event.channelId)
|
|
763
|
-
?? this.channelRuntime.getManagerChannel(managerSessionId, event.channelType);
|
|
764
|
-
const externalChannel = this.channelRuntime.getChannelStatusById(event.channelId)
|
|
765
|
-
?? this.channelRuntime.getManagerChannelStatus(managerSessionId);
|
|
766
|
-
const manager = this.registry.getManager(managerSessionId);
|
|
767
|
-
const agentType = (config?.agentType || (manager ? 'manager' : 'codex'));
|
|
768
|
-
const workDir = config?.workDir || manager?.workDir || process.cwd();
|
|
769
|
-
const agentSessionId = config?.agentSessionId ?? manager?.agentSessionId ?? null;
|
|
770
|
-
const modelId = config?.modelId || manager?.modelId || '';
|
|
771
|
-
const attachmentInputs = externalAttachmentsForAgent(event.attachments);
|
|
772
|
-
const attachmentSummary = externalAttachmentSummary(event.attachments, event.text);
|
|
773
|
-
const mentionSummary = event.isMentioned ? '提及:是' : '';
|
|
774
|
-
const visibleReplyTarget = event.replyTarget ? `回复目标:${event.replyTarget}` : '';
|
|
775
|
-
const visibleConversation = event.channelType === 'wechat-rpa' && event.conversationName
|
|
776
|
-
? `来源群:${event.conversationName}`
|
|
777
|
-
: '';
|
|
778
|
-
const visibleBody = [visibleConversation, event.text, attachmentSummary, mentionSummary, visibleReplyTarget].filter(Boolean).join('\n');
|
|
779
|
-
const visibleMessage = visibleBody
|
|
780
|
-
? `外部消息 / ${event.sender.name || event.sender.id}\n${visibleBody}`
|
|
781
|
-
: `外部消息 / ${event.sender.name || event.sender.id}`;
|
|
782
|
-
this.registry.createReplyTarget({
|
|
783
|
-
managerSessionId,
|
|
784
|
-
channelId: event.channelId,
|
|
785
|
-
conversationId: event.conversationId,
|
|
786
|
-
messageId: event.messageId,
|
|
787
|
-
});
|
|
788
|
-
void this.dispatchExternalMessage({
|
|
789
|
-
sessionId: managerSessionId,
|
|
790
|
-
agentType,
|
|
791
|
-
workDir,
|
|
792
|
-
agentSessionId,
|
|
793
|
-
modelId,
|
|
794
|
-
text: visibleMessage,
|
|
795
|
-
attachments: attachmentInputs,
|
|
796
|
-
externalChannel: externalChannelForChatEnqueue(externalChannel),
|
|
797
|
-
replyTarget: event.replyTarget,
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
async dispatchExternalMessage(input) {
|
|
801
|
-
await this.opts.dispatchReq({
|
|
802
|
-
type: 'req',
|
|
803
|
-
id: `external-enqueue-${randomUUID()}`,
|
|
804
|
-
method: 'chat.enqueue',
|
|
805
|
-
params: {
|
|
806
|
-
sessionId: input.sessionId,
|
|
807
|
-
text: input.text,
|
|
808
|
-
agentType: input.agentType,
|
|
809
|
-
workDir: input.workDir,
|
|
810
|
-
agentSessionId: input.agentSessionId,
|
|
811
|
-
modelId: input.modelId,
|
|
812
|
-
origin: 'external',
|
|
813
|
-
attachments: input.attachments,
|
|
814
|
-
externalChannel: input.externalChannel ?? null,
|
|
815
|
-
replyTarget: input.replyTarget,
|
|
816
|
-
},
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
scanWorkerHealth() {
|
|
820
|
-
const now = Date.now();
|
|
821
|
-
const registry = this.registry.load();
|
|
822
|
-
for (const worker of Object.values(registry.workers)) {
|
|
823
|
-
if (worker.status !== 'running')
|
|
824
|
-
continue;
|
|
825
|
-
const ageMs = now - Date.parse(worker.createdAt);
|
|
826
|
-
if (ageMs < 10 * 60_000)
|
|
827
|
-
continue;
|
|
828
|
-
const lastHealthMs = worker.healthNotifiedAt ? Date.parse(worker.healthNotifiedAt) : 0;
|
|
829
|
-
if (lastHealthMs && now - lastHealthMs < 10 * 60_000)
|
|
830
|
-
continue;
|
|
831
|
-
const manager = registry.managers[worker.managedBy];
|
|
832
|
-
if (!manager)
|
|
833
|
-
continue;
|
|
834
|
-
const inactiveMinutes = Math.max(0, Math.floor((now - Date.parse(worker.lastActivityAt)) / 60_000));
|
|
835
|
-
const prompt = `Worker ${worker.sessionId} 已运行 ${Math.floor(ageMs / 60_000)} 分钟,尚未结束。
|
|
9
|
+
\u8BF7\u51B3\u5B9A\u4E0B\u4E00\u6B65\uFF1A\u7EE7\u7EED\u7B49\u5F85\u3001\u521B\u5EFA/\u6307\u6D3E worker\u3001\u505C\u6B62 worker\u3001\u8BE2\u95EE\u7528\u6237\uFF0C\u6216\u5411\u7528\u6237\u6C47\u62A5\u3002`;this.interruptAndResumeManager(s,o,n==="final"?"worker.final":`worker.${n}`)}handleExternalMessage(e,r){const n=this.channelRuntime.getChannelById(r.channelId)??this.channelRuntime.getManagerChannel(e,r.channelType),t=this.channelRuntime.getChannelStatusById(r.channelId)??this.channelRuntime.getManagerChannelStatus(e),s=this.registry.getManager(e),o=n?.agentType||(s?"manager":"codex"),i=n?.workDir||s?.workDir||process.cwd(),l=n?.agentSessionId??s?.agentSessionId??null,c=n?.modelId||s?.modelId||"",d=te(r.attachments),h=re(r.attachments,r.text),p=r.isMentioned?"\u63D0\u53CA\uFF1A\u662F":"",S=r.replyTarget?`\u56DE\u590D\u76EE\u6807\uFF1A${r.replyTarget}`:"",w=[r.channelType==="wechat-rpa"&&r.conversationName?`\u6765\u6E90\u7FA4\uFF1A${r.conversationName}`:"",r.text,h,p,S].filter(Boolean).join(`
|
|
10
|
+
`),m=w?`\u5916\u90E8\u6D88\u606F / ${r.sender.name||r.sender.id}
|
|
11
|
+
${w}`:`\u5916\u90E8\u6D88\u606F / ${r.sender.name||r.sender.id}`;this.registry.createReplyTarget({managerSessionId:e,channelId:r.channelId,conversationId:r.conversationId,messageId:r.messageId}),this.dispatchExternalMessage({sessionId:e,agentType:o,workDir:i,agentSessionId:l,modelId:c,text:m,attachments:d,externalChannel:ae(t),replyTarget:r.replyTarget})}async dispatchExternalMessage(e){await this.opts.dispatchReq({type:"req",id:`external-enqueue-${g()}`,method:"chat.enqueue",params:{sessionId:e.sessionId,text:e.text,agentType:e.agentType,workDir:e.workDir,agentSessionId:e.agentSessionId,modelId:e.modelId,origin:"external",attachments:e.attachments,externalChannel:e.externalChannel??null,replyTarget:e.replyTarget}})}scanWorkerHealth(){const e=Date.now(),r=this.registry.load();for(const n of Object.values(r.workers)){if(n.status!=="running")continue;const t=e-Date.parse(n.createdAt);if(t<10*6e4)continue;const s=n.healthNotifiedAt?Date.parse(n.healthNotifiedAt):0;if(s&&e-s<10*6e4)continue;const o=r.managers[n.managedBy];if(!o)continue;const i=Math.max(0,Math.floor((e-Date.parse(n.lastActivityAt))/6e4)),l=`Worker ${n.sessionId} \u5DF2\u8FD0\u884C ${Math.floor(t/6e4)} \u5206\u949F\uFF0C\u5C1A\u672A\u7ED3\u675F\u3002
|
|
836
12
|
|
|
837
|
-
|
|
838
|
-
-
|
|
839
|
-
-
|
|
840
|
-
-
|
|
13
|
+
\u5F53\u524D\u53EF\u89C1\u8FDB\u5C55\uFF1A
|
|
14
|
+
- \u6700\u8FD1\u6587\u672C\u6458\u8981\uFF1A${n.summary||"(\u65E0\u53EF\u89C1\u6458\u8981)"}
|
|
15
|
+
- \u6700\u8FD1\u6D3B\u52A8\u65F6\u95F4\uFF1A${n.lastActivityAt}
|
|
16
|
+
- \u6700\u8FD1 ${i} \u5206\u949F\u6CA1\u6709\u65B0\u6D3B\u52A8\u3002
|
|
841
17
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
void this.dispatchChatSend(manager.sessionId, 'manager', manager.workDir, prompt, manager.agentSessionId, manager.modelId);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
async interruptAndResumeManager(manager, prompt, reason) {
|
|
853
|
-
const runtime = this.opts.getRuntime();
|
|
854
|
-
const active = runtime.sessions.get(manager.sessionId);
|
|
855
|
-
if (active?.currentRunId) {
|
|
856
|
-
this.registry.upsertManager({ ...manager, status: 'interrupting' });
|
|
857
|
-
await this.opts.dispatchReq({ type: 'req', id: `manager-interrupt-${randomUUID()}`, method: 'chat.abort', params: { sessionId: manager.sessionId } });
|
|
858
|
-
}
|
|
859
|
-
const continuation = reason ? `事件类型:${reason}\n\n${prompt}` : prompt;
|
|
860
|
-
await this.dispatchChatSend(manager.sessionId, 'manager', manager.workDir, continuation, manager.agentSessionId, manager.modelId);
|
|
861
|
-
}
|
|
862
|
-
bindManagerAdapterEvents(sessionId, adapter) {
|
|
863
|
-
adapter.on('agentEvent', (event) => {
|
|
864
|
-
if (event.state === 'start' && event.agentSessionId) {
|
|
865
|
-
const manager = this.registry.getManager(sessionId);
|
|
866
|
-
if (manager)
|
|
867
|
-
this.noteManagerAgentSession(sessionId, event.agentSessionId, manager.workDir, manager.modelId);
|
|
868
|
-
}
|
|
869
|
-
if (event.state === 'start')
|
|
870
|
-
this.updateManagerStatus(sessionId, 'running');
|
|
871
|
-
if (event.state === 'final' || event.state === 'error' || event.state === 'aborted') {
|
|
872
|
-
this.updateManagerStatus(sessionId, 'idle');
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
updateManagerStatus(sessionId, status) {
|
|
877
|
-
const manager = this.registry.getManager(sessionId);
|
|
878
|
-
if (!manager)
|
|
879
|
-
return;
|
|
880
|
-
this.registry.upsertManager({ ...manager, status });
|
|
881
|
-
}
|
|
882
|
-
getExternalChannelStatus(managerSessionId) {
|
|
883
|
-
return this.channelRuntime.getManagerChannelStatus(managerSessionId);
|
|
884
|
-
}
|
|
885
|
-
getManagerExternalChannelSystemPrompt(managerSessionId) {
|
|
886
|
-
return this.channelRuntime
|
|
887
|
-
.listManagerExternalChannels(managerSessionId)
|
|
888
|
-
.map((channel) => buildExternalChannelInstructions(channel, undefined, managerSessionId, 'manager'))
|
|
889
|
-
.join('\n\n')
|
|
890
|
-
.trim();
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
function parseWeChatRpaGroups(value) {
|
|
894
|
-
if (!Array.isArray(value))
|
|
895
|
-
return [];
|
|
896
|
-
return value
|
|
897
|
-
.map((item) => ({ name: String(item?.name || '').trim() }))
|
|
898
|
-
.filter((item) => item.name);
|
|
899
|
-
}
|
|
900
|
-
function parseWeChatRpaSource(value) {
|
|
901
|
-
return value === 'macos-flow' || value === 'macos-probe' || value === 'windows-visual-flow' || value === 'wechat-rpa-lab' || value === 'fixture-jsonl' ? value : undefined;
|
|
902
|
-
}
|
|
903
|
-
function externalAttachmentsForAgent(attachments) {
|
|
904
|
-
const result = attachments
|
|
905
|
-
.map((attachment) => externalAttachmentForAgent(attachment))
|
|
906
|
-
.filter((item) => item != null);
|
|
907
|
-
return result.length ? result : undefined;
|
|
908
|
-
}
|
|
909
|
-
function externalAttachmentSummary(attachments, existingText) {
|
|
910
|
-
const text = existingText || '';
|
|
911
|
-
const lines = attachments
|
|
912
|
-
.filter((attachment) => !externalAttachmentForAgent(attachment))
|
|
913
|
-
.map((attachment) => {
|
|
914
|
-
const label = attachment.name || attachment.type || 'attachment';
|
|
915
|
-
const status = externalAttachmentUnavailableStatus(attachment);
|
|
916
|
-
return `附件:${attachment.type || 'file'} ${label} (${status})`;
|
|
917
|
-
})
|
|
918
|
-
.filter((line) => !text.includes(line));
|
|
919
|
-
return lines.join('\n');
|
|
920
|
-
}
|
|
921
|
-
function externalAttachmentForAgent(attachment) {
|
|
922
|
-
if (attachment.localPath && canUseOriginalLocalAttachment(attachment) && isReadableLocalAttachment(attachment.localPath)) {
|
|
923
|
-
return {
|
|
924
|
-
path: attachment.localPath,
|
|
925
|
-
name: attachment.name || path.basename(attachment.localPath) || 'attachment',
|
|
926
|
-
mimeType: attachment.mimeType || mimeTypeFromExternalAttachment(attachment),
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
if (attachment.url && canUseOriginalUrlAttachment(attachment)) {
|
|
930
|
-
return {
|
|
931
|
-
path: attachment.url,
|
|
932
|
-
name: attachment.name || attachment.url.split('/').filter(Boolean).at(-1) || 'attachment',
|
|
933
|
-
mimeType: attachment.mimeType || mimeTypeFromExternalAttachment(attachment),
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
if (attachment.thumbnailPath && isReadableLocalAttachment(attachment.thumbnailPath)) {
|
|
937
|
-
return {
|
|
938
|
-
path: attachment.thumbnailPath,
|
|
939
|
-
name: attachment.name ? `${attachment.name}-preview.png` : path.basename(attachment.thumbnailPath) || 'preview.png',
|
|
940
|
-
mimeType: 'image/png',
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
-
function externalAttachmentUnavailableStatus(attachment) {
|
|
946
|
-
if (attachment.providerError)
|
|
947
|
-
return attachment.providerError;
|
|
948
|
-
if (attachment.localPath && (!attachment.availability || attachment.availability === 'edge-local'))
|
|
949
|
-
return 'edge-local-unavailable';
|
|
950
|
-
return attachment.availability || 'metadata-only';
|
|
951
|
-
}
|
|
952
|
-
function externalChannelForChatEnqueue(channel) {
|
|
953
|
-
if (!channel)
|
|
954
|
-
return null;
|
|
955
|
-
if (channel.type !== 'wechat-rpa')
|
|
956
|
-
return channel;
|
|
957
|
-
return {
|
|
958
|
-
configured: channel.configured,
|
|
959
|
-
connected: channel.connected,
|
|
960
|
-
type: channel.type,
|
|
961
|
-
channelId: channel.channelId,
|
|
962
|
-
name: channel.name,
|
|
963
|
-
canReply: channel.canReply,
|
|
964
|
-
systemPrompt: channel.systemPrompt,
|
|
965
|
-
wechatRpaSource: channel.wechatRpaSource,
|
|
966
|
-
wechatRpaGroups: channel.wechatRpaGroups,
|
|
967
|
-
pollIntervalMs: channel.pollIntervalMs,
|
|
968
|
-
recentLimit: channel.recentLimit,
|
|
969
|
-
idleSeconds: channel.idleSeconds,
|
|
970
|
-
forceForeground: channel.forceForeground,
|
|
971
|
-
noRestore: channel.noRestore,
|
|
972
|
-
downloadAttachments: channel.downloadAttachments,
|
|
973
|
-
selfNickname: channel.selfNickname,
|
|
974
|
-
wechatRpaPrivacyConsentAccepted: channel.wechatRpaPrivacyConsentAccepted,
|
|
975
|
-
wechatRpaServerDecisionAvailable: channel.wechatRpaServerDecisionAvailable,
|
|
976
|
-
wechatRpaPreflightChecks: channel.wechatRpaPreflightChecks,
|
|
977
|
-
wechatRpaRuntimeState: channel.wechatRpaRuntimeState,
|
|
978
|
-
wechatRpaLastMessageAt: channel.wechatRpaLastMessageAt,
|
|
979
|
-
wechatRpaPendingReplyCount: channel.wechatRpaPendingReplyCount,
|
|
980
|
-
wechatRpaLastError: channel.wechatRpaLastError,
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
function canUseOriginalLocalAttachment(attachment) {
|
|
984
|
-
return !attachment.providerError
|
|
985
|
-
&& (!attachment.availability || attachment.availability === 'edge-local');
|
|
986
|
-
}
|
|
987
|
-
function canUseOriginalUrlAttachment(attachment) {
|
|
988
|
-
return !attachment.providerError
|
|
989
|
-
&& (!attachment.availability || attachment.availability === 'server-url');
|
|
990
|
-
}
|
|
991
|
-
function isReadableLocalAttachment(filePath) {
|
|
992
|
-
try {
|
|
993
|
-
return fs.statSync(filePath).isFile();
|
|
994
|
-
}
|
|
995
|
-
catch {
|
|
996
|
-
return false;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
function mimeTypeFromExternalAttachment(attachment) {
|
|
1000
|
-
if (attachment.type === 'image')
|
|
1001
|
-
return 'image/*';
|
|
1002
|
-
if (attachment.type === 'video')
|
|
1003
|
-
return 'video/*';
|
|
1004
|
-
if (attachment.type === 'audio')
|
|
1005
|
-
return 'audio/*';
|
|
1006
|
-
return 'application/octet-stream';
|
|
1007
|
-
}
|
|
1008
|
-
function optionalNumber(value) {
|
|
1009
|
-
const number = Number(value);
|
|
1010
|
-
return Number.isFinite(number) ? number : undefined;
|
|
1011
|
-
}
|
|
18
|
+
\u8BF7\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u7B49\u5F85\u3001\u8BE2\u95EE\u7528\u6237\u3001\u505C\u6B62 worker\u3001\u6216\u521B\u5EFA\u5176\u4ED6 worker \u534F\u52A9\u3002`;this.registry.updateWorker(n.sessionId,{healthNotifiedAt:new Date(e).toISOString(),lastHealthSummary:l}),o.status==="idle"&&this.dispatchChatSend(o.sessionId,"manager",o.workDir,l,o.agentSessionId,o.modelId)}}async interruptAndResumeManager(e,r,n){this.opts.getRuntime().sessions.get(e.sessionId)?.currentRunId&&(this.registry.upsertManager({...e,status:"interrupting"}),await this.opts.dispatchReq({type:"req",id:`manager-interrupt-${g()}`,method:"chat.abort",params:{sessionId:e.sessionId}}));const o=n?`\u4E8B\u4EF6\u7C7B\u578B\uFF1A${n}
|
|
19
|
+
|
|
20
|
+
${r}`:r;await this.dispatchChatSend(e.sessionId,"manager",e.workDir,o,e.agentSessionId,e.modelId)}bindManagerAdapterEvents(e,r){r.on("agentEvent",n=>{if(n.state==="start"&&n.agentSessionId){const t=this.registry.getManager(e);t&&this.noteManagerAgentSession(e,n.agentSessionId,t.workDir,t.modelId)}n.state==="start"&&this.updateManagerStatus(e,"running"),(n.state==="final"||n.state==="error"||n.state==="aborted")&&this.updateManagerStatus(e,"idle")})}updateManagerStatus(e,r){const n=this.registry.getManager(e);n&&this.registry.upsertManager({...n,status:r})}getExternalChannelStatus(e){return this.channelRuntime.getManagerChannelStatus(e)}getManagerExternalChannelSystemPrompt(e){return this.channelRuntime.listManagerExternalChannels(e).map(r=>j(r,void 0,e,"manager")).join(`
|
|
21
|
+
|
|
22
|
+
`).trim()}}function Z(a){return Array.isArray(a)?a.map(e=>({name:String(e?.name||"").trim()})).filter(e=>e.name):[]}function ee(a){return a==="macos-flow"||a==="macos-probe"||a==="windows-visual-flow"||a==="wechat-rpa-lab"||a==="fixture-jsonl"?a:void 0}function te(a){const e=a.map(r=>$(r)).filter(r=>r!=null);return e.length?e:void 0}function re(a,e){const r=e||"";return a.filter(t=>!$(t)).map(t=>{const s=t.name||t.type||"attachment",o=ne(t);return`\u9644\u4EF6\uFF1A${t.type||"file"} ${s} (${o})`}).filter(t=>!r.includes(t)).join(`
|
|
23
|
+
`)}function $(a){return a.localPath&&se(a)&&P(a.localPath)?{path:a.localPath,name:a.name||I.basename(a.localPath)||"attachment",mimeType:a.mimeType||D(a)}:a.url&&ie(a)?{path:a.url,name:a.name||a.url.split("/").filter(Boolean).at(-1)||"attachment",mimeType:a.mimeType||D(a)}:a.thumbnailPath&&P(a.thumbnailPath)?{path:a.thumbnailPath,name:a.name?`${a.name}-preview.png`:I.basename(a.thumbnailPath)||"preview.png",mimeType:"image/png"}:null}function ne(a){return a.providerError?a.providerError:a.localPath&&(!a.availability||a.availability==="edge-local")?"edge-local-unavailable":a.availability||"metadata-only"}function ae(a){return a?a.type!=="wechat-rpa"?a:{configured:a.configured,connected:a.connected,type:a.type,channelId:a.channelId,name:a.name,canReply:a.canReply,systemPrompt:a.systemPrompt,wechatRpaSource:a.wechatRpaSource,wechatRpaGroups:a.wechatRpaGroups,pollIntervalMs:a.pollIntervalMs,recentLimit:a.recentLimit,idleSeconds:a.idleSeconds,forceForeground:a.forceForeground,noRestore:a.noRestore,downloadAttachments:a.downloadAttachments,selfNickname:a.selfNickname,wechatRpaPrivacyConsentAccepted:a.wechatRpaPrivacyConsentAccepted,wechatRpaServerDecisionAvailable:a.wechatRpaServerDecisionAvailable,wechatRpaPreflightChecks:a.wechatRpaPreflightChecks,wechatRpaRuntimeState:a.wechatRpaRuntimeState,wechatRpaLastMessageAt:a.wechatRpaLastMessageAt,wechatRpaPendingReplyCount:a.wechatRpaPendingReplyCount,wechatRpaLastError:a.wechatRpaLastError}:null}function se(a){return!a.providerError&&(!a.availability||a.availability==="edge-local")}function ie(a){return!a.providerError&&(!a.availability||a.availability==="server-url")}function P(a){try{return k.statSync(a).isFile()}catch{return!1}}function D(a){return a.type==="image"?"image/*":a.type==="video"?"video/*":a.type==="audio"?"audio/*":"application/octet-stream"}function b(a){const e=Number(a);return Number.isFinite(e)?e:void 0}export{Se as ManagerRuntimeService,Ie as getManagerRuntimeService,ke as setManagerRuntimeService};
|