@agentbean/daemon 0.1.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/README.md +158 -0
- package/dist/adapters/adapter.js +9 -0
- package/dist/adapters/claude-code.js +79 -0
- package/dist/adapters/codex.js +114 -0
- package/dist/adapters/hermes.js +80 -0
- package/dist/adapters/openclaw.js +70 -0
- package/dist/agent-instance.js +84 -0
- package/dist/auth-store.js +24 -0
- package/dist/bin.js +6 -0
- package/dist/config.js +138 -0
- package/dist/connection.js +113 -0
- package/dist/device-daemon.js +212 -0
- package/dist/index.js +302 -0
- package/dist/log.js +7 -0
- package/dist/post-process.js +71 -0
- package/dist/sandbox.js +40 -0
- package/dist/scanner.js +296 -0
- package/dist/uploader.js +46 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# AgentBean Device Daemon
|
|
2
|
+
|
|
3
|
+
Agent 适配器层 —— 运行在用户机器上,将真实 Coding Agent 接入 AgentBean Server。
|
|
4
|
+
|
|
5
|
+
## 启动
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev # 开发模式(tsx watch)
|
|
10
|
+
npm start # 运行(tsx)
|
|
11
|
+
npm test # 运行测试
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### 带配置文件启动
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx tsx src/index.ts ~/.agentbean/device-agent.yaml
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 自动扫描模式
|
|
21
|
+
|
|
22
|
+
如果不提供配置文件,或配置文件中 `agents` 数组为空,Daemon 会自动扫描本机 Agent:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx tsx src/index.ts
|
|
26
|
+
# 扫描 Coding Agent (which claude-code, codex, kimi...)
|
|
27
|
+
# 扫描 AgentOS Gateway (localhost:PORT)
|
|
28
|
+
# 扫描 ~/.agentbean/agents/ 目录
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 配置文件格式
|
|
32
|
+
|
|
33
|
+
`device-agent.yaml`:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
deviceId: my-macbook-pro # 设备标识
|
|
37
|
+
networkId: default # 所属网络
|
|
38
|
+
server:
|
|
39
|
+
url: http://localhost:3000/agent # Server Socket.IO 地址
|
|
40
|
+
token: default:default:dev-token-change-me # 三截 token
|
|
41
|
+
heartbeatIntervalMs: 10000 # 心跳间隔(默认 10s)
|
|
42
|
+
|
|
43
|
+
agents: # Agent 配置列表
|
|
44
|
+
- id: claude-shaw
|
|
45
|
+
name: Claude
|
|
46
|
+
role: 高级编程助手
|
|
47
|
+
category: coding # coding | executor-hosted | agentos-hosted | standalone-cli
|
|
48
|
+
visibility: public # public | private
|
|
49
|
+
adapter:
|
|
50
|
+
kind: claude-code # claude-code | codex | openclaw | hermes | standalone
|
|
51
|
+
command: claude # 可执行命令
|
|
52
|
+
args: [] # 命令参数
|
|
53
|
+
cwd: ~/projects # 工作目录(可选)
|
|
54
|
+
systemPrompt: | # 系统提示词(可选)
|
|
55
|
+
You are a helpful coding assistant.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
支持环境变量插值:`${SERVER_URL}`
|
|
59
|
+
|
|
60
|
+
## 核心模块
|
|
61
|
+
|
|
62
|
+
### 设备守护进程 (`device-daemon.ts`)
|
|
63
|
+
|
|
64
|
+
`DeviceDaemon` 类:
|
|
65
|
+
- 维护与 Server 的 Socket.IO 连接
|
|
66
|
+
- 管理多个 `AgentInstance`
|
|
67
|
+
- 定期发送心跳
|
|
68
|
+
- 处理 Server 下发的 `dispatch` 任务
|
|
69
|
+
|
|
70
|
+
### Agent 实例 (`agent-instance.ts`)
|
|
71
|
+
|
|
72
|
+
`AgentInstance` 封装单个 Agent:
|
|
73
|
+
- 持有 `CliAdapter`(适配器实例)
|
|
74
|
+
- 管理 Agent 生命周期(启动、运行、停止)
|
|
75
|
+
- 将 Server 的 `dispatch` 转换为适配器输入
|
|
76
|
+
- 将适配器输出包装为 `reply` 发送回 Server
|
|
77
|
+
|
|
78
|
+
### 扫描器 (`scanner.ts`)
|
|
79
|
+
|
|
80
|
+
三类自动发现:
|
|
81
|
+
|
|
82
|
+
**`scanCodingAgents()`** — 通过 `which` 发现本机 Coding Agent:
|
|
83
|
+
```typescript
|
|
84
|
+
const CODING_BINARIES = ['claude-code', 'codex', 'kimi'];
|
|
85
|
+
// 对每个执行 which,存在的加入结果
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**`scanAgentOSAgents()`** — 扫描 OpenClaw / Hermes gateway:
|
|
89
|
+
- 尝试连接 `http://localhost:PORT/openclaw/agents`
|
|
90
|
+
- 如果 gateway 未运行,返回空数组
|
|
91
|
+
|
|
92
|
+
**`scanLocalAgents(scanDir)`** — 扫描约定目录:
|
|
93
|
+
- 扫描 `~/.agentbean/agents/` 或指定目录
|
|
94
|
+
- 查找 `agent.json` / `agent.yaml` 配置文件
|
|
95
|
+
- 识别执行器承载型(有 `executor` 字段)和独立 CLI Agent
|
|
96
|
+
|
|
97
|
+
### 连接管理 (`connection.ts`)
|
|
98
|
+
|
|
99
|
+
`createConnection()`:
|
|
100
|
+
- 建立 Socket.IO 连接到 Server `/agent` 命名空间
|
|
101
|
+
- 发送 `register` 事件进行认证
|
|
102
|
+
- 自动重连和心跳
|
|
103
|
+
- 处理 `dispatch` 事件并路由到对应 Agent
|
|
104
|
+
|
|
105
|
+
### CLI 适配器 (`adapters/`)
|
|
106
|
+
|
|
107
|
+
| 适配器 | 说明 | 命令 |
|
|
108
|
+
|--------|------|------|
|
|
109
|
+
| `ClaudeCodeAdapter` | Anthropic Claude Code | `claude` |
|
|
110
|
+
| `CodexAdapter` | OpenAI Codex CLI | `codex` |
|
|
111
|
+
| `OpenClawAdapter` | OpenClaw gateway | `openclaw` |
|
|
112
|
+
| `HermesAdapter` | Hermes gateway | `hermes` |
|
|
113
|
+
|
|
114
|
+
所有适配器实现 `CliAdapter` 接口:
|
|
115
|
+
```typescript
|
|
116
|
+
interface CliAdapter {
|
|
117
|
+
start(): void;
|
|
118
|
+
stop(): void;
|
|
119
|
+
send(input: string): void;
|
|
120
|
+
onOutput(handler: (text: string) => void): void;
|
|
121
|
+
onExit(handler: (code: number | null) => void): void;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
底层使用 `node-pty` 创建伪终端与 CLI 工具交互,支持实时流式输出。
|
|
126
|
+
|
|
127
|
+
### 配置解析 (`config.ts`)
|
|
128
|
+
|
|
129
|
+
- `loadConfig()` — 解析单 Agent 配置
|
|
130
|
+
- `loadDeviceConfig()` — 解析设备级多 Agent 配置
|
|
131
|
+
- `AgentCategory` — 四类 Agent 分类
|
|
132
|
+
- `AdapterKind` — 五种适配器类型
|
|
133
|
+
- 支持 YAML 环境变量插值
|
|
134
|
+
|
|
135
|
+
## 启动流程
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
1. 解析命令行参数(配置文件路径)
|
|
139
|
+
2. 尝试 loadDeviceConfig() — 静态配置优先
|
|
140
|
+
3. 如果无静态配置或 agents 数组为空:
|
|
141
|
+
a. scanCodingAgents() — 扫描本机 CLI 工具
|
|
142
|
+
b. scanAgentOSAgents() — 扫描 gateway
|
|
143
|
+
c. scanLocalAgents() — 扫描约定目录
|
|
144
|
+
d. 合并去重,生成 AgentConfigEntry 列表
|
|
145
|
+
4. 对每个 entry 创建 AgentInstance + 适配器
|
|
146
|
+
5. 创建 DeviceDaemon,连接 Server
|
|
147
|
+
6. 发送 register + 定期 heartbeat
|
|
148
|
+
7. 等待 dispatch 任务
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 环境变量
|
|
152
|
+
|
|
153
|
+
| 变量 | 说明 |
|
|
154
|
+
|------|------|
|
|
155
|
+
| `SERVER_URL` | Server WebSocket 地址 |
|
|
156
|
+
| `SERVER_TOKEN` | 接入令牌 |
|
|
157
|
+
| `DEVICE_ID` | 设备标识 |
|
|
158
|
+
| `NETWORK_ID` | 所属网络 |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
function buildPrompt(input, systemPrompt) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
if (systemPrompt)
|
|
7
|
+
parts.push(systemPrompt);
|
|
8
|
+
for (const h of input.history.slice(-10)) {
|
|
9
|
+
parts.push(`${h.speaker} (${h.role}): ${h.body}`);
|
|
10
|
+
}
|
|
11
|
+
parts.push(input.prompt);
|
|
12
|
+
return parts.join('\n\n---\n\n');
|
|
13
|
+
}
|
|
14
|
+
export class ClaudeCodeAdapter {
|
|
15
|
+
opts;
|
|
16
|
+
kind = 'claude-code';
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
}
|
|
20
|
+
async ask(input, signal) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const prompt = buildPrompt(input, this.opts.systemPrompt ?? input.systemPrompt);
|
|
23
|
+
const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
|
|
24
|
+
const baseArgs = ['-p', '--bare', ...(this.opts.args ?? [])];
|
|
25
|
+
if (input.workspace)
|
|
26
|
+
baseArgs.push('--add-dir', input.workspace);
|
|
27
|
+
baseArgs.push('--add-dir', join(homedir(), '.codex', 'generated_images'));
|
|
28
|
+
const command = input.sandboxProfilePath ? 'sandbox-exec' : this.opts.command;
|
|
29
|
+
const args = input.sandboxProfilePath
|
|
30
|
+
? ['-f', input.sandboxProfilePath, '--', this.opts.command, ...baseArgs]
|
|
31
|
+
: baseArgs;
|
|
32
|
+
const child = spawn(command, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
});
|
|
36
|
+
const stdoutChunks = [];
|
|
37
|
+
const stderrChunks = [];
|
|
38
|
+
const MAX_EXEC_MS = 600_000;
|
|
39
|
+
const onAbort = () => {
|
|
40
|
+
child.kill('SIGTERM');
|
|
41
|
+
setTimeout(() => child.kill('SIGKILL'), 2_000).unref();
|
|
42
|
+
};
|
|
43
|
+
signal.addEventListener('abort', onAbort);
|
|
44
|
+
const maxTimer = setTimeout(() => {
|
|
45
|
+
child.kill('SIGKILL');
|
|
46
|
+
signal.removeEventListener('abort', onAbort);
|
|
47
|
+
reject(new Error('claude-code adapter timeout'));
|
|
48
|
+
}, MAX_EXEC_MS).unref();
|
|
49
|
+
child.stdout.on('data', (b) => stdoutChunks.push(b));
|
|
50
|
+
child.stderr.on('data', (b) => stderrChunks.push(b));
|
|
51
|
+
child.on('error', (err) => {
|
|
52
|
+
clearTimeout(maxTimer);
|
|
53
|
+
signal.removeEventListener('abort', onAbort);
|
|
54
|
+
reject(err);
|
|
55
|
+
});
|
|
56
|
+
child.on('exit', (code) => {
|
|
57
|
+
clearTimeout(maxTimer);
|
|
58
|
+
signal.removeEventListener('abort', onAbort);
|
|
59
|
+
if (signal.aborted)
|
|
60
|
+
return reject(new Error('aborted'));
|
|
61
|
+
const out = Buffer.concat(stdoutChunks).toString('utf8').trim();
|
|
62
|
+
const err = Buffer.concat(stderrChunks).toString('utf8').trim();
|
|
63
|
+
if (code !== 0 && out.length === 0) {
|
|
64
|
+
return reject(new Error(`claude-code exit ${code}: ${err.slice(0, 200)}`));
|
|
65
|
+
}
|
|
66
|
+
resolve(out);
|
|
67
|
+
});
|
|
68
|
+
child.stdin.write(prompt);
|
|
69
|
+
child.stdin.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async health() {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const child = spawn(this.opts.command, ['--version'], { stdio: 'ignore' });
|
|
75
|
+
child.on('error', (err) => resolve({ ok: false, detail: err.message }));
|
|
76
|
+
child.on('exit', (code) => resolve({ ok: code === 0, detail: code === 0 ? undefined : `exit ${code}` }));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawn } from 'node-pty';
|
|
2
|
+
function renderPayload(input, systemPrompt) {
|
|
3
|
+
const parts = [];
|
|
4
|
+
if (systemPrompt)
|
|
5
|
+
parts.push(`# system\n${systemPrompt}`);
|
|
6
|
+
for (const turn of input.history) {
|
|
7
|
+
parts.push(`# ${turn.role}: ${turn.speaker}\n${turn.body}`);
|
|
8
|
+
}
|
|
9
|
+
parts.push(`# user\n${input.prompt}`);
|
|
10
|
+
return parts.join('\n\n');
|
|
11
|
+
}
|
|
12
|
+
function stripAnsi(s) {
|
|
13
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
14
|
+
}
|
|
15
|
+
function extractReply(output) {
|
|
16
|
+
const clean = stripAnsi(output).replace(/\r\n/g, '\n');
|
|
17
|
+
// Match "codex" label followed by reply content, ending before next hook or end
|
|
18
|
+
const match = clean.match(/\ncodex\n([\s\S]*?)(?:\nhook:|$)/i);
|
|
19
|
+
if (match)
|
|
20
|
+
return match[1].trim();
|
|
21
|
+
// Fallback: everything after last "user" prompt
|
|
22
|
+
const userIdx = clean.lastIndexOf('\nuser\n');
|
|
23
|
+
if (userIdx > 0) {
|
|
24
|
+
const after = clean.slice(userIdx).split('\n').slice(2).join('\n').trim();
|
|
25
|
+
if (after)
|
|
26
|
+
return after;
|
|
27
|
+
}
|
|
28
|
+
return clean.trim();
|
|
29
|
+
}
|
|
30
|
+
export class CodexAdapter {
|
|
31
|
+
opts;
|
|
32
|
+
kind = 'codex';
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.opts = opts;
|
|
35
|
+
}
|
|
36
|
+
async ask(input, signal) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const payload = renderPayload(input, this.opts.systemPrompt ?? input.systemPrompt);
|
|
39
|
+
const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
|
|
40
|
+
const baseCommand = this.opts.command || 'codex';
|
|
41
|
+
const baseArgs = [...(this.opts.args ?? ['exec']), payload];
|
|
42
|
+
const command = input.sandboxProfilePath ? 'sandbox-exec' : baseCommand;
|
|
43
|
+
const args = input.sandboxProfilePath
|
|
44
|
+
? ['-f', input.sandboxProfilePath, '--', baseCommand, ...baseArgs]
|
|
45
|
+
: baseArgs;
|
|
46
|
+
const pty = spawn(command, args, {
|
|
47
|
+
name: 'xterm-color',
|
|
48
|
+
cols: 80,
|
|
49
|
+
rows: 30,
|
|
50
|
+
cwd,
|
|
51
|
+
env: process.env,
|
|
52
|
+
});
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let finished = false;
|
|
55
|
+
const MAX_EXEC_MS = 600_000;
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
if (finished)
|
|
58
|
+
return;
|
|
59
|
+
finished = true;
|
|
60
|
+
clearTimeout(maxTimer);
|
|
61
|
+
signal.removeEventListener('abort', onAbort);
|
|
62
|
+
pty.kill('SIGTERM');
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
try {
|
|
65
|
+
pty.kill('SIGKILL');
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}, 2_000).unref();
|
|
69
|
+
reject(new Error('aborted'));
|
|
70
|
+
};
|
|
71
|
+
signal.addEventListener('abort', onAbort);
|
|
72
|
+
const maxTimer = setTimeout(() => {
|
|
73
|
+
if (finished)
|
|
74
|
+
return;
|
|
75
|
+
finished = true;
|
|
76
|
+
pty.kill('SIGKILL');
|
|
77
|
+
signal.removeEventListener('abort', onAbort);
|
|
78
|
+
reject(new Error('codex adapter timeout'));
|
|
79
|
+
}, MAX_EXEC_MS).unref();
|
|
80
|
+
pty.onData((data) => chunks.push(data));
|
|
81
|
+
pty.onExit(({ exitCode }) => {
|
|
82
|
+
clearTimeout(maxTimer);
|
|
83
|
+
signal.removeEventListener('abort', onAbort);
|
|
84
|
+
if (finished)
|
|
85
|
+
return;
|
|
86
|
+
finished = true;
|
|
87
|
+
if (signal.aborted)
|
|
88
|
+
return reject(new Error('aborted'));
|
|
89
|
+
const raw = chunks.join('');
|
|
90
|
+
if (exitCode !== 0) {
|
|
91
|
+
const detail = stripAnsi(raw).trim();
|
|
92
|
+
return reject(new Error(detail ? `codex exit ${exitCode}: ${detail}` : `codex exit ${exitCode}`));
|
|
93
|
+
}
|
|
94
|
+
const reply = extractReply(raw);
|
|
95
|
+
resolve(reply || '(Codex 已完成处理)');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async health() {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
try {
|
|
102
|
+
const pty = spawn('bash', ['-c', 'codex --version'], {
|
|
103
|
+
name: 'xterm-color', cols: 80, rows: 30,
|
|
104
|
+
cwd: this.opts.cwd ?? process.cwd(),
|
|
105
|
+
env: process.env,
|
|
106
|
+
});
|
|
107
|
+
pty.onExit(({ exitCode }) => resolve({ ok: exitCode === 0, detail: exitCode === 0 ? undefined : `exit ${exitCode}` }));
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
resolve({ ok: false, detail: err.message });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function buildPrompt(input, systemPrompt) {
|
|
3
|
+
const parts = [];
|
|
4
|
+
if (systemPrompt)
|
|
5
|
+
parts.push(systemPrompt);
|
|
6
|
+
for (const h of input.history.slice(-10)) {
|
|
7
|
+
parts.push(`${h.speaker} (${h.role}): ${h.body}`);
|
|
8
|
+
}
|
|
9
|
+
parts.push(input.prompt);
|
|
10
|
+
return parts.join('\n\n---\n\n');
|
|
11
|
+
}
|
|
12
|
+
export class HermesAdapter {
|
|
13
|
+
opts;
|
|
14
|
+
kind = 'hermes';
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.opts = opts;
|
|
17
|
+
}
|
|
18
|
+
async ask(input, signal) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const prompt = buildPrompt(input, this.opts.systemPrompt ?? input.systemPrompt);
|
|
21
|
+
const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
|
|
22
|
+
const child = spawn(this.opts.command, ['-z', prompt, ...(this.opts.args ?? [])], {
|
|
23
|
+
cwd,
|
|
24
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
25
|
+
});
|
|
26
|
+
const stdoutChunks = [];
|
|
27
|
+
const stderrChunks = [];
|
|
28
|
+
let finished = false;
|
|
29
|
+
const MAX_EXEC_MS = 600_000;
|
|
30
|
+
const onAbort = () => {
|
|
31
|
+
if (finished)
|
|
32
|
+
return;
|
|
33
|
+
finished = true;
|
|
34
|
+
child.kill('SIGTERM');
|
|
35
|
+
setTimeout(() => { try {
|
|
36
|
+
child.kill('SIGKILL');
|
|
37
|
+
}
|
|
38
|
+
catch { } }, 2_000).unref();
|
|
39
|
+
};
|
|
40
|
+
signal.addEventListener('abort', onAbort);
|
|
41
|
+
const maxTimer = setTimeout(() => {
|
|
42
|
+
if (finished)
|
|
43
|
+
return;
|
|
44
|
+
finished = true;
|
|
45
|
+
child.kill('SIGKILL');
|
|
46
|
+
signal.removeEventListener('abort', onAbort);
|
|
47
|
+
reject(new Error('hermes adapter timeout'));
|
|
48
|
+
}, MAX_EXEC_MS).unref();
|
|
49
|
+
child.stdout.on('data', (b) => stdoutChunks.push(b));
|
|
50
|
+
child.stderr.on('data', (b) => stderrChunks.push(b));
|
|
51
|
+
child.on('error', (err) => {
|
|
52
|
+
clearTimeout(maxTimer);
|
|
53
|
+
signal.removeEventListener('abort', onAbort);
|
|
54
|
+
reject(err);
|
|
55
|
+
});
|
|
56
|
+
child.on('exit', (code) => {
|
|
57
|
+
clearTimeout(maxTimer);
|
|
58
|
+
signal.removeEventListener('abort', onAbort);
|
|
59
|
+
if (finished)
|
|
60
|
+
return;
|
|
61
|
+
finished = true;
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
return reject(new Error('aborted'));
|
|
64
|
+
const out = Buffer.concat(stdoutChunks).toString('utf8');
|
|
65
|
+
const err = Buffer.concat(stderrChunks).toString('utf8');
|
|
66
|
+
if (code !== 0 && out.length === 0) {
|
|
67
|
+
return reject(new Error(`hermes exit ${code}: ${err.slice(0, 400)}`));
|
|
68
|
+
}
|
|
69
|
+
resolve(out.trim() || err.trim());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async health() {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const child = spawn(this.opts.command, ['--version'], { stdio: 'ignore' });
|
|
76
|
+
child.on('error', (err) => resolve({ ok: false, detail: err.message }));
|
|
77
|
+
child.on('exit', (code) => resolve({ ok: code === 0, detail: code === 0 ? undefined : `exit ${code}` }));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export class OpenClawAdapter {
|
|
3
|
+
opts;
|
|
4
|
+
kind = 'openclaw';
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
8
|
+
async ask(input, signal) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const payload = JSON.stringify({
|
|
11
|
+
system: this.opts.systemPrompt ?? input.systemPrompt,
|
|
12
|
+
history: input.history.slice(-10),
|
|
13
|
+
user: input.prompt,
|
|
14
|
+
});
|
|
15
|
+
const child = spawn(this.opts.command, this.opts.args ?? [], {
|
|
16
|
+
cwd: this.opts.cwd,
|
|
17
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
});
|
|
19
|
+
const stdoutChunks = [];
|
|
20
|
+
const stderrChunks = [];
|
|
21
|
+
const MAX_EXEC_MS = 600_000;
|
|
22
|
+
const onAbort = () => {
|
|
23
|
+
child.kill('SIGTERM');
|
|
24
|
+
setTimeout(() => child.kill('SIGKILL'), 2_000).unref();
|
|
25
|
+
};
|
|
26
|
+
signal.addEventListener('abort', onAbort);
|
|
27
|
+
const maxTimer = setTimeout(() => {
|
|
28
|
+
child.kill('SIGKILL');
|
|
29
|
+
signal.removeEventListener('abort', onAbort);
|
|
30
|
+
reject(new Error('openclaw adapter timeout'));
|
|
31
|
+
}, MAX_EXEC_MS).unref();
|
|
32
|
+
child.stdout.on('data', (b) => stdoutChunks.push(b));
|
|
33
|
+
child.stderr.on('data', (b) => stderrChunks.push(b));
|
|
34
|
+
child.stdin.end(payload);
|
|
35
|
+
child.on('error', (err) => {
|
|
36
|
+
clearTimeout(maxTimer);
|
|
37
|
+
signal.removeEventListener('abort', onAbort);
|
|
38
|
+
reject(err);
|
|
39
|
+
});
|
|
40
|
+
child.on('exit', (code) => {
|
|
41
|
+
clearTimeout(maxTimer);
|
|
42
|
+
signal.removeEventListener('abort', onAbort);
|
|
43
|
+
if (signal.aborted)
|
|
44
|
+
return reject(new Error('aborted'));
|
|
45
|
+
const out = Buffer.concat(stdoutChunks).toString('utf8');
|
|
46
|
+
const err = Buffer.concat(stderrChunks).toString('utf8');
|
|
47
|
+
if (code !== 0) {
|
|
48
|
+
return reject(new Error(`openclaw exit ${code}: ${err.slice(0, 400)}`));
|
|
49
|
+
}
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(out);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return reject(new Error(`openclaw produced non-JSON output: ${out.slice(0, 200)}`));
|
|
56
|
+
}
|
|
57
|
+
if (parsed.error)
|
|
58
|
+
return reject(new Error(parsed.error));
|
|
59
|
+
resolve((parsed.reply ?? '').trim());
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async health() {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const child = spawn(this.opts.command, ['--version'], { stdio: 'ignore' });
|
|
66
|
+
child.on('error', (err) => resolve({ ok: false, detail: err.message }));
|
|
67
|
+
child.on('exit', (code) => resolve({ ok: code === 0, detail: code === 0 ? undefined : `exit ${code}` }));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { logger } from './log.js';
|
|
2
|
+
import { uploadArtifact } from './uploader.js';
|
|
3
|
+
import { postProcess } from './post-process.js';
|
|
4
|
+
import { generateSandboxProfile, getWorkspaceDir, isSandboxAvailable } from './sandbox.js';
|
|
5
|
+
export class AgentInstance {
|
|
6
|
+
config;
|
|
7
|
+
adapter;
|
|
8
|
+
id;
|
|
9
|
+
name;
|
|
10
|
+
role;
|
|
11
|
+
visibility;
|
|
12
|
+
constructor(config, adapter) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.adapter = adapter;
|
|
15
|
+
this.id = config.id;
|
|
16
|
+
this.name = config.name;
|
|
17
|
+
this.role = config.role;
|
|
18
|
+
this.visibility = config.visibility;
|
|
19
|
+
}
|
|
20
|
+
get publicMeta() {
|
|
21
|
+
return {
|
|
22
|
+
id: this.id,
|
|
23
|
+
name: this.name,
|
|
24
|
+
role: this.role,
|
|
25
|
+
category: this.config.category ?? 'executor-hosted',
|
|
26
|
+
adapterKind: this.adapter.kind,
|
|
27
|
+
visibility: this.visibility,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async handleDispatch(opts) {
|
|
31
|
+
const { socket, req, serverUrl, token, networkId } = opts;
|
|
32
|
+
const ctl = new AbortController();
|
|
33
|
+
const dispatchStart = Date.now();
|
|
34
|
+
try {
|
|
35
|
+
const rawBody = await this.adapter.ask({
|
|
36
|
+
prompt: req.prompt,
|
|
37
|
+
history: req.history ?? [],
|
|
38
|
+
systemPrompt: this.config.adapter.systemPrompt,
|
|
39
|
+
workspace: req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace,
|
|
40
|
+
sandboxProfilePath: req.sandboxed && isSandboxAvailable()
|
|
41
|
+
? generateSandboxProfile(this.id, this.config.adapter.command)
|
|
42
|
+
: undefined,
|
|
43
|
+
}, ctl.signal);
|
|
44
|
+
const processed = await postProcess(rawBody, req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace, this.adapter.kind, dispatchStart);
|
|
45
|
+
const artifactIds = [];
|
|
46
|
+
if (processed.outputFiles.length > 0) {
|
|
47
|
+
for (const filePath of processed.outputFiles) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await uploadArtifact({
|
|
50
|
+
serverUrl,
|
|
51
|
+
token,
|
|
52
|
+
networkId,
|
|
53
|
+
filePath,
|
|
54
|
+
channelId: req.channelId,
|
|
55
|
+
uploaderId: this.id,
|
|
56
|
+
});
|
|
57
|
+
if (result)
|
|
58
|
+
artifactIds.push(result.id);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger.warn({ err: err.message, filePath }, 'artifact upload failed');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
socket.emit('reply', {
|
|
66
|
+
agentId: this.id,
|
|
67
|
+
channelId: req.channelId,
|
|
68
|
+
body: processed.replyText,
|
|
69
|
+
requestId: req.requestId,
|
|
70
|
+
artifactIds: artifactIds.length > 0 ? artifactIds : undefined,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
logger.error({ err: err.message, requestId: req.requestId, agentId: this.id }, 'dispatch failed');
|
|
75
|
+
socket.emit('error_event', {
|
|
76
|
+
agentId: this.id,
|
|
77
|
+
at: Date.now(),
|
|
78
|
+
message: err.message ?? 'unknown',
|
|
79
|
+
scope: 'reply',
|
|
80
|
+
requestId: req.requestId,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|