@agentbean/daemon 0.1.34 → 0.2.0
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/apps/daemon-next/src/bin.d.ts +2 -0
- package/dist/apps/daemon-next/src/bin.js +6 -0
- package/dist/apps/daemon-next/src/cli.d.ts +26 -0
- package/dist/apps/daemon-next/src/cli.js +124 -0
- package/dist/apps/daemon-next/src/executor.d.ts +6 -0
- package/dist/apps/daemon-next/src/executor.js +51 -0
- package/dist/apps/daemon-next/src/index.d.ts +60 -0
- package/dist/apps/daemon-next/src/index.js +87 -0
- package/dist/apps/daemon-next/src/scanner.d.ts +15 -0
- package/dist/apps/daemon-next/src/scanner.js +94 -0
- package/dist/packages/contracts/src/agent.d.ts +69 -0
- package/dist/packages/contracts/src/agent.js +4 -0
- package/dist/packages/contracts/src/auth.d.ts +20 -0
- package/dist/packages/contracts/src/auth.js +1 -0
- package/dist/packages/contracts/src/channel.d.ts +58 -0
- package/dist/packages/contracts/src/channel.js +1 -0
- package/dist/packages/contracts/src/common.d.ts +17 -0
- package/dist/packages/contracts/src/common.js +27 -0
- package/dist/packages/contracts/src/device.d.ts +27 -0
- package/dist/packages/contracts/src/device.js +1 -0
- package/dist/packages/contracts/src/dispatch.d.ts +46 -0
- package/dist/packages/contracts/src/dispatch.js +1 -0
- package/dist/packages/contracts/src/index.d.ts +9 -0
- package/dist/packages/contracts/src/index.js +9 -0
- package/dist/packages/contracts/src/message.d.ts +20 -0
- package/dist/packages/contracts/src/message.js +1 -0
- package/dist/packages/contracts/src/socket.d.ts +74 -0
- package/dist/packages/contracts/src/socket.js +74 -0
- package/dist/packages/contracts/src/team.d.ts +13 -0
- package/dist/packages/contracts/src/team.js +1 -0
- package/package.json +14 -25
- package/README.md +0 -158
- package/dist/adapters/adapter.js +0 -9
- package/dist/adapters/claude-code.js +0 -83
- package/dist/adapters/codex.js +0 -280
- package/dist/adapters/factory.js +0 -38
- package/dist/adapters/hermes.js +0 -178
- package/dist/adapters/openclaw.js +0 -129
- package/dist/agent-instance.js +0 -181
- package/dist/auth-store.js +0 -24
- package/dist/bin.js +0 -6
- package/dist/config.js +0 -148
- package/dist/connection.js +0 -211
- package/dist/device-daemon.js +0 -529
- package/dist/index.js +0 -329
- package/dist/log.js +0 -7
- package/dist/post-process.js +0 -177
- package/dist/sandbox.js +0 -53
- package/dist/scanner.js +0 -421
- package/dist/uploader.js +0 -46
- package/dist/workspace-manager.js +0 -196
- package/dist/workspace-sync.js +0 -69
package/dist/agent-instance.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
import { logger } from './log.js';
|
|
4
|
-
import { uploadArtifact } from './uploader.js';
|
|
5
|
-
import { postProcess } from './post-process.js';
|
|
6
|
-
import { generateSandboxProfile, getWorkspaceDir, isSandboxAvailable } from './sandbox.js';
|
|
7
|
-
import { archiveOutputFiles, beginAgentWorkspaceRun, finishAgentWorkspaceRun, formatWorkspaceReply, workspaceEnv, } from './workspace-manager.js';
|
|
8
|
-
function errorMessage(err) {
|
|
9
|
-
if (err instanceof Error && err.message)
|
|
10
|
-
return err.message;
|
|
11
|
-
if (typeof err === 'string' && err.trim())
|
|
12
|
-
return err;
|
|
13
|
-
try {
|
|
14
|
-
const serialized = JSON.stringify(err);
|
|
15
|
-
if (serialized && serialized !== '{}')
|
|
16
|
-
return serialized;
|
|
17
|
-
}
|
|
18
|
-
catch { }
|
|
19
|
-
return 'unknown error';
|
|
20
|
-
}
|
|
21
|
-
function safeFilename(value) {
|
|
22
|
-
return basename(value).replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^-+|-+$/g, '') || 'attachment';
|
|
23
|
-
}
|
|
24
|
-
async function downloadAttachments(input) {
|
|
25
|
-
const downloaded = [];
|
|
26
|
-
for (const attachment of input.attachments ?? []) {
|
|
27
|
-
const sep = attachment.downloadUrl.includes('?') ? '&' : '?';
|
|
28
|
-
const url = `${input.serverUrl}${attachment.downloadUrl}${sep}token=${encodeURIComponent(input.token)}`;
|
|
29
|
-
try {
|
|
30
|
-
const resp = await fetch(url);
|
|
31
|
-
if (!resp.ok) {
|
|
32
|
-
logger.warn({ id: attachment.id, status: resp.status }, 'attachment download rejected');
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
36
|
-
const localPath = join(input.run.inputDir, `${attachment.id}-${safeFilename(attachment.filename)}`);
|
|
37
|
-
writeFileSync(localPath, bytes);
|
|
38
|
-
downloaded.push({ ...attachment, localPath });
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
logger.warn({ id: attachment.id, err: err?.message }, 'attachment download failed');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return downloaded;
|
|
45
|
-
}
|
|
46
|
-
function promptWithAttachments(prompt, attachments) {
|
|
47
|
-
if (attachments.length === 0)
|
|
48
|
-
return prompt;
|
|
49
|
-
const list = attachments
|
|
50
|
-
.map((file) => `- ${file.filename} (${file.mimeType}, ${file.sizeBytes} bytes): ${file.localPath}`)
|
|
51
|
-
.join('\n');
|
|
52
|
-
return `${prompt}\n\n用户随消息附加了以下本地文件,请在需要时读取并使用:\n${list}`;
|
|
53
|
-
}
|
|
54
|
-
function promptWithWorkspaceOutput(prompt, outputDir) {
|
|
55
|
-
return `${prompt}\n\n如果本次任务会生成图片、文档、数据或其他文件,请把最终产物保存到这个 AgentBean 输出目录:\n${outputDir}\n保存后在回复中说明文件名即可,系统会自动同步并在聊天中展示预览。`;
|
|
56
|
-
}
|
|
57
|
-
export class AgentInstance {
|
|
58
|
-
config;
|
|
59
|
-
adapter;
|
|
60
|
-
id;
|
|
61
|
-
name;
|
|
62
|
-
role;
|
|
63
|
-
visibility;
|
|
64
|
-
activeControllers = new Map();
|
|
65
|
-
constructor(config, adapter) {
|
|
66
|
-
this.config = config;
|
|
67
|
-
this.adapter = adapter;
|
|
68
|
-
this.id = config.id;
|
|
69
|
-
this.name = config.name;
|
|
70
|
-
this.role = config.role;
|
|
71
|
-
this.visibility = config.visibility;
|
|
72
|
-
}
|
|
73
|
-
get publicMeta() {
|
|
74
|
-
return {
|
|
75
|
-
id: this.id,
|
|
76
|
-
name: this.name,
|
|
77
|
-
role: this.role,
|
|
78
|
-
category: this.config.category ?? 'executor-hosted',
|
|
79
|
-
adapterKind: this.adapter.kind,
|
|
80
|
-
visibility: this.visibility,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
async handleDispatch(opts) {
|
|
84
|
-
const { socket, req, serverUrl, token, networkId, deviceId } = opts;
|
|
85
|
-
const ctl = new AbortController();
|
|
86
|
-
this.activeControllers.set(req.requestId, ctl);
|
|
87
|
-
const dispatchStart = Date.now();
|
|
88
|
-
const teamId = req.teamId ?? req.networkId ?? networkId;
|
|
89
|
-
const projectWorkspace = req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace;
|
|
90
|
-
const run = beginAgentWorkspaceRun({
|
|
91
|
-
teamId,
|
|
92
|
-
teamName: req.teamName,
|
|
93
|
-
agentId: this.id,
|
|
94
|
-
agentName: this.name,
|
|
95
|
-
runId: req.requestId,
|
|
96
|
-
prompt: req.prompt,
|
|
97
|
-
projectDir: projectWorkspace,
|
|
98
|
-
});
|
|
99
|
-
let archivedFiles = [];
|
|
100
|
-
try {
|
|
101
|
-
const downloadedAttachments = await downloadAttachments({ serverUrl, token, run, attachments: req.attachments });
|
|
102
|
-
const prompt = promptWithWorkspaceOutput(promptWithAttachments(req.prompt, downloadedAttachments), run.outputDir);
|
|
103
|
-
const rawBody = await this.adapter.ask({
|
|
104
|
-
prompt,
|
|
105
|
-
history: req.history ?? [],
|
|
106
|
-
systemPrompt: this.config.adapter.systemPrompt,
|
|
107
|
-
workspace: projectWorkspace,
|
|
108
|
-
sandboxProfilePath: req.sandboxed && isSandboxAvailable()
|
|
109
|
-
? generateSandboxProfile(this.id, this.config.adapter.command, [run.runDir])
|
|
110
|
-
: undefined,
|
|
111
|
-
env: { ...(this.config.adapter.env ?? {}), ...workspaceEnv(run) },
|
|
112
|
-
}, ctl.signal);
|
|
113
|
-
const processed = await postProcess(rawBody, projectWorkspace, this.adapter.kind, dispatchStart, {
|
|
114
|
-
outputDirs: [run.outputDir, run.intermediateDir],
|
|
115
|
-
});
|
|
116
|
-
archivedFiles = archiveOutputFiles(run, processed.outputFiles);
|
|
117
|
-
const replyText = formatWorkspaceReply(rawBody, archivedFiles, { exposeLocalPaths: false });
|
|
118
|
-
finishAgentWorkspaceRun(run, { replyText, files: archivedFiles, status: 'completed' });
|
|
119
|
-
const artifactIds = [];
|
|
120
|
-
if (archivedFiles.length > 0) {
|
|
121
|
-
for (const file of archivedFiles) {
|
|
122
|
-
try {
|
|
123
|
-
const result = await uploadArtifact({
|
|
124
|
-
serverUrl,
|
|
125
|
-
token,
|
|
126
|
-
networkId: teamId,
|
|
127
|
-
filePath: file.archivedPath,
|
|
128
|
-
channelId: req.channelId,
|
|
129
|
-
uploaderId: this.id,
|
|
130
|
-
metaJson: JSON.stringify({
|
|
131
|
-
kind: 'agent-workspace-file',
|
|
132
|
-
teamId,
|
|
133
|
-
agentId: this.id,
|
|
134
|
-
runId: req.requestId,
|
|
135
|
-
deviceId: deviceId ?? null,
|
|
136
|
-
pathKind: file.pathKind,
|
|
137
|
-
relativePath: file.relativePath,
|
|
138
|
-
sha256: file.sha256,
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
if (result)
|
|
142
|
-
artifactIds.push(result.id);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
logger.warn({ err: err.message, filePath: file.archivedPath }, 'artifact upload failed');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
socket.emit('reply', {
|
|
150
|
-
agentId: this.id,
|
|
151
|
-
channelId: req.channelId,
|
|
152
|
-
body: replyText,
|
|
153
|
-
requestId: req.requestId,
|
|
154
|
-
artifactIds: artifactIds.length > 0 ? artifactIds : undefined,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
const message = errorMessage(err);
|
|
159
|
-
finishAgentWorkspaceRun(run, { files: archivedFiles, status: 'failed', error: message });
|
|
160
|
-
logger.error({ err: message, requestId: req.requestId, agentId: this.id }, 'dispatch failed');
|
|
161
|
-
socket.emit('error_event', {
|
|
162
|
-
agentId: this.id,
|
|
163
|
-
at: Date.now(),
|
|
164
|
-
message,
|
|
165
|
-
scope: 'reply',
|
|
166
|
-
requestId: req.requestId,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
this.activeControllers.delete(req.requestId);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
cancelDispatch(requestId) {
|
|
174
|
-
const entries = requestId
|
|
175
|
-
? [...this.activeControllers.entries()].filter(([id]) => id === requestId)
|
|
176
|
-
: [...this.activeControllers.entries()];
|
|
177
|
-
for (const [, ctl] of entries)
|
|
178
|
-
ctl.abort();
|
|
179
|
-
return entries.length;
|
|
180
|
-
}
|
|
181
|
-
}
|
package/dist/auth-store.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
const AUTH_DIR = join(homedir(), '.agentbean');
|
|
5
|
-
const AUTH_FILE = join(AUTH_DIR, 'auth.json');
|
|
6
|
-
export function loadAuth() {
|
|
7
|
-
if (!existsSync(AUTH_FILE))
|
|
8
|
-
return null;
|
|
9
|
-
try {
|
|
10
|
-
return JSON.parse(readFileSync(AUTH_FILE, 'utf8'));
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function saveAuth(data) {
|
|
17
|
-
if (!existsSync(AUTH_DIR))
|
|
18
|
-
mkdirSync(AUTH_DIR, { recursive: true });
|
|
19
|
-
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
|
|
20
|
-
}
|
|
21
|
-
export function clearAuth() {
|
|
22
|
-
if (existsSync(AUTH_FILE))
|
|
23
|
-
unlinkSync(AUTH_FILE);
|
|
24
|
-
}
|
package/dist/bin.js
DELETED
package/dist/config.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { load as parseYaml } from 'js-yaml';
|
|
3
|
-
const KINDS = ['codex', 'claude-code', 'openclaw', 'hermes', 'standalone'];
|
|
4
|
-
const ENV_PATTERN = /\$\{([A-Z0-9_]+)\}/g;
|
|
5
|
-
function interpolate(value) {
|
|
6
|
-
return value.replace(ENV_PATTERN, (_match, name) => {
|
|
7
|
-
const v = process.env[name];
|
|
8
|
-
if (v === undefined)
|
|
9
|
-
throw new Error(`config references missing env var: ${name}`);
|
|
10
|
-
return v;
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
function deepInterpolate(node) {
|
|
14
|
-
if (typeof node === 'string')
|
|
15
|
-
return interpolate(node);
|
|
16
|
-
if (Array.isArray(node))
|
|
17
|
-
return node.map(deepInterpolate);
|
|
18
|
-
if (node && typeof node === 'object') {
|
|
19
|
-
const out = {};
|
|
20
|
-
for (const [k, v] of Object.entries(node))
|
|
21
|
-
out[k] = deepInterpolate(v);
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
24
|
-
return node;
|
|
25
|
-
}
|
|
26
|
-
function parseEnvMap(value) {
|
|
27
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
28
|
-
return undefined;
|
|
29
|
-
const out = {};
|
|
30
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
31
|
-
if (key.trim())
|
|
32
|
-
out[key.trim()] = String(raw ?? '');
|
|
33
|
-
}
|
|
34
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
35
|
-
}
|
|
36
|
-
export function loadConfig(path) {
|
|
37
|
-
const raw = parseYaml(readFileSync(path, 'utf8'));
|
|
38
|
-
if (!raw || typeof raw !== 'object')
|
|
39
|
-
throw new Error('config: top-level must be a mapping');
|
|
40
|
-
const interp = deepInterpolate(raw);
|
|
41
|
-
const need = ['id', 'name', 'role'];
|
|
42
|
-
for (const k of need) {
|
|
43
|
-
if (typeof interp[k] !== 'string' || interp[k].length === 0) {
|
|
44
|
-
throw new Error(`config: ${k} is required (non-empty string)`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
const a = interp.adapter ?? {};
|
|
48
|
-
if (!KINDS.includes(a.kind)) {
|
|
49
|
-
throw new Error(`config: adapter.kind must be one of ${KINDS.join(', ')}`);
|
|
50
|
-
}
|
|
51
|
-
if (typeof a.command !== 'string') {
|
|
52
|
-
throw new Error('config: adapter.command is required');
|
|
53
|
-
}
|
|
54
|
-
const s = interp.server ?? {};
|
|
55
|
-
if (typeof s.url !== 'string' || typeof s.token !== 'string') {
|
|
56
|
-
throw new Error('config: server.url and server.token are required');
|
|
57
|
-
}
|
|
58
|
-
const inferredCategory = a.kind === 'codex' || a.kind === 'claude-code' ? 'executor-hosted' :
|
|
59
|
-
'agentos-hosted';
|
|
60
|
-
const category = typeof interp.category === 'string' && ['executor-hosted', 'agentos-hosted'].includes(interp.category)
|
|
61
|
-
? interp.category
|
|
62
|
-
: inferredCategory;
|
|
63
|
-
return {
|
|
64
|
-
id: interp.id,
|
|
65
|
-
name: interp.name,
|
|
66
|
-
role: interp.role,
|
|
67
|
-
category,
|
|
68
|
-
adapter: {
|
|
69
|
-
kind: a.kind,
|
|
70
|
-
command: a.command,
|
|
71
|
-
args: Array.isArray(a.args) ? a.args.map(String) : [],
|
|
72
|
-
cwd: typeof a.cwd === 'string' ? a.cwd : undefined,
|
|
73
|
-
workspace: typeof a.workspace === 'string' ? a.workspace : undefined,
|
|
74
|
-
systemPrompt: typeof a.systemPrompt === 'string' ? a.systemPrompt : undefined,
|
|
75
|
-
env: parseEnvMap(a.env),
|
|
76
|
-
},
|
|
77
|
-
server: { url: s.url, token: s.token },
|
|
78
|
-
heartbeatIntervalMs: isValidHeartbeat(interp.heartbeatIntervalMs) ? interp.heartbeatIntervalMs : 10_000,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function isValidHeartbeat(v) {
|
|
82
|
-
return typeof v === 'number' && v > 0 && Number.isFinite(v);
|
|
83
|
-
}
|
|
84
|
-
export function loadDeviceConfig(path) {
|
|
85
|
-
const raw = parseYaml(readFileSync(path, 'utf8'));
|
|
86
|
-
if (!raw || typeof raw !== 'object')
|
|
87
|
-
throw new Error('config: top-level must be a mapping');
|
|
88
|
-
const interp = deepInterpolate(raw);
|
|
89
|
-
if (typeof interp.deviceId !== 'string' || interp.deviceId.length === 0) {
|
|
90
|
-
throw new Error('config: deviceId is required (non-empty string)');
|
|
91
|
-
}
|
|
92
|
-
if (typeof interp.networkId !== 'string' || interp.networkId.length === 0) {
|
|
93
|
-
throw new Error('config: networkId is required (non-empty string)');
|
|
94
|
-
}
|
|
95
|
-
const s = interp.server ?? {};
|
|
96
|
-
if (typeof s.url !== 'string' || typeof s.token !== 'string') {
|
|
97
|
-
throw new Error('config: server.url and server.token are required');
|
|
98
|
-
}
|
|
99
|
-
const agents = interp.agents ?? [];
|
|
100
|
-
if (!Array.isArray(agents) || agents.length === 0) {
|
|
101
|
-
throw new Error('config: agents array is required (at least one agent)');
|
|
102
|
-
}
|
|
103
|
-
const parsedAgents = [];
|
|
104
|
-
for (const a of agents) {
|
|
105
|
-
if (typeof a.id !== 'string' || a.id.length === 0) {
|
|
106
|
-
throw new Error('config: each agent must have an id (non-empty string)');
|
|
107
|
-
}
|
|
108
|
-
if (typeof a.name !== 'string' || a.name.length === 0) {
|
|
109
|
-
throw new Error('config: each agent must have a name (non-empty string)');
|
|
110
|
-
}
|
|
111
|
-
const ad = a.adapter ?? {};
|
|
112
|
-
if (!KINDS.includes(ad.kind)) {
|
|
113
|
-
throw new Error(`config: adapter.kind must be one of ${KINDS.join(', ')}`);
|
|
114
|
-
}
|
|
115
|
-
if (typeof ad.command !== 'string') {
|
|
116
|
-
throw new Error('config: adapter.command is required');
|
|
117
|
-
}
|
|
118
|
-
const inferredCategory = ad.kind === 'codex' || ad.kind === 'claude-code' ? 'executor-hosted' :
|
|
119
|
-
'agentos-hosted';
|
|
120
|
-
const category = typeof a.category === 'string' && ['executor-hosted', 'agentos-hosted'].includes(a.category)
|
|
121
|
-
? a.category
|
|
122
|
-
: inferredCategory;
|
|
123
|
-
parsedAgents.push({
|
|
124
|
-
id: a.id,
|
|
125
|
-
name: a.name,
|
|
126
|
-
role: typeof a.role === 'string' ? a.role : '',
|
|
127
|
-
category,
|
|
128
|
-
adapter: {
|
|
129
|
-
kind: ad.kind,
|
|
130
|
-
command: ad.command,
|
|
131
|
-
args: Array.isArray(ad.args) ? ad.args.map(String) : [],
|
|
132
|
-
cwd: typeof ad.cwd === 'string' ? ad.cwd : undefined,
|
|
133
|
-
workspace: typeof ad.workspace === 'string' ? ad.workspace : undefined,
|
|
134
|
-
systemPrompt: typeof ad.systemPrompt === 'string' ? ad.systemPrompt : undefined,
|
|
135
|
-
env: parseEnvMap(ad.env),
|
|
136
|
-
},
|
|
137
|
-
visibility: a.visibility === 'public' ? 'public' : 'private',
|
|
138
|
-
sandboxed: a.sandboxed === true,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
deviceId: interp.deviceId,
|
|
143
|
-
networkId: interp.networkId,
|
|
144
|
-
server: { url: s.url, token: s.token },
|
|
145
|
-
heartbeatIntervalMs: isValidHeartbeat(interp.heartbeatIntervalMs) ? interp.heartbeatIntervalMs : 10_000,
|
|
146
|
-
agents: parsedAgents,
|
|
147
|
-
};
|
|
148
|
-
}
|
package/dist/connection.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { io } from 'socket.io-client';
|
|
2
|
-
import { writeFileSync } from 'node:fs';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
4
|
-
import { logger } from './log.js';
|
|
5
|
-
import { uploadArtifact } from './uploader.js';
|
|
6
|
-
import { postProcess } from './post-process.js';
|
|
7
|
-
import { archiveOutputFiles, beginAgentWorkspaceRun, finishAgentWorkspaceRun, formatWorkspaceReply, workspaceEnv, } from './workspace-manager.js';
|
|
8
|
-
function errorMessage(err) {
|
|
9
|
-
if (err instanceof Error && err.message)
|
|
10
|
-
return err.message;
|
|
11
|
-
if (typeof err === 'string' && err.trim())
|
|
12
|
-
return err;
|
|
13
|
-
try {
|
|
14
|
-
const serialized = JSON.stringify(err);
|
|
15
|
-
if (serialized && serialized !== '{}')
|
|
16
|
-
return serialized;
|
|
17
|
-
}
|
|
18
|
-
catch { }
|
|
19
|
-
return 'unknown error';
|
|
20
|
-
}
|
|
21
|
-
function safeFilename(value) {
|
|
22
|
-
return basename(value).replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^-+|-+$/g, '') || 'attachment';
|
|
23
|
-
}
|
|
24
|
-
async function downloadAttachments(input) {
|
|
25
|
-
const downloaded = [];
|
|
26
|
-
for (const attachment of input.attachments ?? []) {
|
|
27
|
-
const sep = attachment.downloadUrl.includes('?') ? '&' : '?';
|
|
28
|
-
const url = `${input.serverUrl}${attachment.downloadUrl}${sep}token=${encodeURIComponent(input.token)}`;
|
|
29
|
-
try {
|
|
30
|
-
const resp = await fetch(url);
|
|
31
|
-
if (!resp.ok) {
|
|
32
|
-
logger.warn({ id: attachment.id, status: resp.status }, 'attachment download rejected');
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const localPath = join(input.run.inputDir, `${attachment.id}-${safeFilename(attachment.filename)}`);
|
|
36
|
-
writeFileSync(localPath, Buffer.from(await resp.arrayBuffer()));
|
|
37
|
-
downloaded.push({ ...attachment, localPath });
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
logger.warn({ id: attachment.id, err: err?.message }, 'attachment download failed');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return downloaded;
|
|
44
|
-
}
|
|
45
|
-
function promptWithAttachments(prompt, attachments) {
|
|
46
|
-
if (attachments.length === 0)
|
|
47
|
-
return prompt;
|
|
48
|
-
const list = attachments
|
|
49
|
-
.map((file) => `- ${file.filename} (${file.mimeType}, ${file.sizeBytes} bytes): ${file.localPath}`)
|
|
50
|
-
.join('\n');
|
|
51
|
-
return `${prompt}\n\n用户随消息附加了以下本地文件,请在需要时读取并使用:\n${list}`;
|
|
52
|
-
}
|
|
53
|
-
function promptWithWorkspaceOutput(prompt, outputDir) {
|
|
54
|
-
return `${prompt}\n\n如果本次任务会生成图片、文档、数据或其他文件,请把最终产物保存到这个 AgentBean 输出目录:\n${outputDir}\n保存后在回复中说明文件名即可,系统会自动同步并在聊天中展示预览。`;
|
|
55
|
-
}
|
|
56
|
-
export function createConnection(cfg, adapter) {
|
|
57
|
-
let socket = null;
|
|
58
|
-
let heartbeatTimer = null;
|
|
59
|
-
let queue = Promise.resolve();
|
|
60
|
-
const activeControllers = new Map();
|
|
61
|
-
return {
|
|
62
|
-
async start() {
|
|
63
|
-
const agentUrl = cfg.server.url.endsWith('/agent') ? cfg.server.url : cfg.server.url + '/agent';
|
|
64
|
-
socket = io(agentUrl, {
|
|
65
|
-
auth: {
|
|
66
|
-
token: cfg.server.token,
|
|
67
|
-
agentId: cfg.id,
|
|
68
|
-
name: cfg.name,
|
|
69
|
-
role: cfg.role,
|
|
70
|
-
adapterKind: cfg.adapter.kind,
|
|
71
|
-
},
|
|
72
|
-
reconnection: true,
|
|
73
|
-
reconnectionDelay: 1_000,
|
|
74
|
-
});
|
|
75
|
-
socket.on('connect', () => {
|
|
76
|
-
logger.info({ id: cfg.id }, 'connected to server');
|
|
77
|
-
socket.emit('register', {
|
|
78
|
-
id: cfg.id, name: cfg.name, role: cfg.role,
|
|
79
|
-
adapterKind: cfg.adapter.kind,
|
|
80
|
-
});
|
|
81
|
-
if (heartbeatTimer)
|
|
82
|
-
clearInterval(heartbeatTimer);
|
|
83
|
-
heartbeatTimer = setInterval(() => {
|
|
84
|
-
socket?.emit('heartbeat', { at: Date.now() });
|
|
85
|
-
}, cfg.heartbeatIntervalMs);
|
|
86
|
-
});
|
|
87
|
-
socket.on('connect_error', (err) => {
|
|
88
|
-
logger.error({ err: err.message }, 'connect_error');
|
|
89
|
-
});
|
|
90
|
-
socket.on('dispatch', (req) => {
|
|
91
|
-
const currentSocket = socket;
|
|
92
|
-
if (!currentSocket) {
|
|
93
|
-
logger.warn({ requestId: req.requestId }, 'dispatch received but socket is null');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
queue = queue.then(async () => {
|
|
97
|
-
const ctl = new AbortController();
|
|
98
|
-
activeControllers.set(req.requestId, ctl);
|
|
99
|
-
const dispatchStart = Date.now();
|
|
100
|
-
const teamId = 'default';
|
|
101
|
-
const run = beginAgentWorkspaceRun({
|
|
102
|
-
teamId,
|
|
103
|
-
agentId: cfg.id,
|
|
104
|
-
agentName: cfg.name,
|
|
105
|
-
runId: req.requestId,
|
|
106
|
-
prompt: req.prompt,
|
|
107
|
-
projectDir: cfg.adapter.workspace,
|
|
108
|
-
});
|
|
109
|
-
let archivedFiles = [];
|
|
110
|
-
try {
|
|
111
|
-
const httpBase = cfg.server.url.replace(/\/agent$/, '');
|
|
112
|
-
const downloadedAttachments = await downloadAttachments({
|
|
113
|
-
serverUrl: httpBase,
|
|
114
|
-
token: cfg.server.token,
|
|
115
|
-
run,
|
|
116
|
-
attachments: req.attachments,
|
|
117
|
-
});
|
|
118
|
-
const prompt = promptWithWorkspaceOutput(promptWithAttachments(req.prompt, downloadedAttachments), run.outputDir);
|
|
119
|
-
const rawBody = await adapter.ask({
|
|
120
|
-
prompt,
|
|
121
|
-
history: req.history ?? [],
|
|
122
|
-
systemPrompt: cfg.adapter.systemPrompt,
|
|
123
|
-
workspace: cfg.adapter.workspace,
|
|
124
|
-
env: workspaceEnv(run),
|
|
125
|
-
}, ctl.signal);
|
|
126
|
-
const processed = await postProcess(rawBody, cfg.adapter.workspace, cfg.adapter.kind, dispatchStart, {
|
|
127
|
-
outputDirs: [run.outputDir, run.intermediateDir],
|
|
128
|
-
});
|
|
129
|
-
archivedFiles = archiveOutputFiles(run, processed.outputFiles);
|
|
130
|
-
const replyText = formatWorkspaceReply(rawBody, archivedFiles, { exposeLocalPaths: false });
|
|
131
|
-
finishAgentWorkspaceRun(run, { replyText, files: archivedFiles, status: 'completed' });
|
|
132
|
-
const artifactIds = [];
|
|
133
|
-
if (archivedFiles.length > 0) {
|
|
134
|
-
for (const file of archivedFiles) {
|
|
135
|
-
try {
|
|
136
|
-
const result = await uploadArtifact({
|
|
137
|
-
serverUrl: httpBase,
|
|
138
|
-
token: cfg.server.token,
|
|
139
|
-
networkId: teamId,
|
|
140
|
-
filePath: file.archivedPath,
|
|
141
|
-
channelId: req.channelId,
|
|
142
|
-
uploaderId: cfg.id,
|
|
143
|
-
metaJson: JSON.stringify({
|
|
144
|
-
kind: 'agent-workspace-file',
|
|
145
|
-
teamId,
|
|
146
|
-
agentId: cfg.id,
|
|
147
|
-
runId: req.requestId,
|
|
148
|
-
pathKind: file.pathKind,
|
|
149
|
-
relativePath: file.relativePath,
|
|
150
|
-
sha256: file.sha256,
|
|
151
|
-
}),
|
|
152
|
-
});
|
|
153
|
-
if (result)
|
|
154
|
-
artifactIds.push(result.id);
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
logger.warn({ err: err.message, filePath: file.archivedPath }, 'artifact upload failed');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
currentSocket.emit('reply', {
|
|
162
|
-
channelId: req.channelId,
|
|
163
|
-
body: replyText,
|
|
164
|
-
requestId: req.requestId,
|
|
165
|
-
artifactIds: artifactIds.length > 0 ? artifactIds : undefined,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
catch (err) {
|
|
169
|
-
const message = errorMessage(err);
|
|
170
|
-
finishAgentWorkspaceRun(run, { files: archivedFiles, status: 'failed', error: message });
|
|
171
|
-
logger.error({ err: message, requestId: req.requestId }, 'dispatch failed');
|
|
172
|
-
currentSocket.emit('error_event', {
|
|
173
|
-
at: Date.now(),
|
|
174
|
-
message,
|
|
175
|
-
scope: 'reply',
|
|
176
|
-
requestId: req.requestId,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
activeControllers.delete(req.requestId);
|
|
181
|
-
}
|
|
182
|
-
}).catch((err) => {
|
|
183
|
-
logger.error({ err: err?.message, requestId: req.requestId }, 'dispatch queue error');
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
socket.on('dispatch:cancel', (payload) => {
|
|
187
|
-
const entries = payload.requestId
|
|
188
|
-
? [...activeControllers.entries()].filter(([id]) => id === payload.requestId)
|
|
189
|
-
: [...activeControllers.entries()];
|
|
190
|
-
for (const [, ctl] of entries)
|
|
191
|
-
ctl.abort();
|
|
192
|
-
logger.info({ requestId: payload.requestId, cancelled: entries.length, reason: payload.reason }, 'dispatch cancel requested');
|
|
193
|
-
});
|
|
194
|
-
socket.on('disconnect', (reason) => {
|
|
195
|
-
logger.warn({ reason }, 'disconnected');
|
|
196
|
-
if (heartbeatTimer) {
|
|
197
|
-
clearInterval(heartbeatTimer);
|
|
198
|
-
heartbeatTimer = null;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
},
|
|
202
|
-
async stop() {
|
|
203
|
-
if (heartbeatTimer) {
|
|
204
|
-
clearInterval(heartbeatTimer);
|
|
205
|
-
heartbeatTimer = null;
|
|
206
|
-
}
|
|
207
|
-
socket?.close();
|
|
208
|
-
socket = null;
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|