@botlearn-course/daemon 0.0.1
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/agent-service-client.d.ts +18 -0
- package/dist/agent-service-client.js +108 -0
- package/dist/auth-store.d.ts +16 -0
- package/dist/auth-store.js +106 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +354 -0
- package/dist/course-client.d.ts +46 -0
- package/dist/course-client.js +143 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +85 -0
- package/dist/file-candidates.d.ts +34 -0
- package/dist/file-candidates.js +173 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/log.d.ts +20 -0
- package/dist/log.js +154 -0
- package/dist/path-env.d.ts +8 -0
- package/dist/path-env.js +42 -0
- package/dist/redaction.d.ts +24 -0
- package/dist/redaction.js +158 -0
- package/dist/run-dispatcher.d.ts +43 -0
- package/dist/run-dispatcher.js +294 -0
- package/dist/run-queue.d.ts +11 -0
- package/dist/run-queue.js +26 -0
- package/dist/runtime-capabilities.d.ts +3 -0
- package/dist/runtime-capabilities.js +42 -0
- package/dist/runtime-profile.d.ts +8 -0
- package/dist/runtime-profile.js +213 -0
- package/dist/runtimes/acp-stream.d.ts +96 -0
- package/dist/runtimes/acp-stream.js +488 -0
- package/dist/runtimes/claude-code.d.ts +41 -0
- package/dist/runtimes/claude-code.js +353 -0
- package/dist/runtimes/codex.d.ts +44 -0
- package/dist/runtimes/codex.js +332 -0
- package/dist/runtimes/deepseek-tui.d.ts +50 -0
- package/dist/runtimes/deepseek-tui.js +701 -0
- package/dist/runtimes/engine.d.ts +52 -0
- package/dist/runtimes/engine.js +127 -0
- package/dist/runtimes/fake.d.ts +13 -0
- package/dist/runtimes/fake.js +45 -0
- package/dist/runtimes/gemini.d.ts +39 -0
- package/dist/runtimes/gemini.js +251 -0
- package/dist/runtimes/hermes-agent.d.ts +61 -0
- package/dist/runtimes/hermes-agent.js +173 -0
- package/dist/runtimes/index.d.ts +15 -0
- package/dist/runtimes/index.js +74 -0
- package/dist/runtimes/kimi.d.ts +35 -0
- package/dist/runtimes/kimi.js +335 -0
- package/dist/runtimes/ndjson-stream.d.ts +51 -0
- package/dist/runtimes/ndjson-stream.js +207 -0
- package/dist/runtimes/openclaw-acp.d.ts +52 -0
- package/dist/runtimes/openclaw-acp.js +872 -0
- package/dist/runtimes/probe.d.ts +17 -0
- package/dist/runtimes/probe.js +54 -0
- package/dist/runtimes/runtime-errors.d.ts +20 -0
- package/dist/runtimes/runtime-errors.js +95 -0
- package/dist/runtimes/text-cap.d.ts +7 -0
- package/dist/runtimes/text-cap.js +25 -0
- package/dist/transcript.d.ts +13 -0
- package/dist/transcript.js +46 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.js +17 -0
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.js +54 -0
- package/package.json +40 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { safeCommand, sanitizeRuntimeFailureText, tailText } from "../redaction.js";
|
|
3
|
+
import { sliceUtf8Bytes, utf8ByteLength } from "./text-cap.js";
|
|
4
|
+
import { consoleLogger, } from "./engine.js";
|
|
5
|
+
/** 合成 exit-code 错误时保留的 stderr 末尾长度。 */
|
|
6
|
+
const STDERR_ERROR_SNIPPET = 500;
|
|
7
|
+
/** 单轮流式 assistant 文本字节上限 —— 防失控 CLI。 */
|
|
8
|
+
const ASSISTANT_TEXT_CAP = 1 * 1024 * 1024;
|
|
9
|
+
/** abort 时 SIGTERM → SIGKILL 的宽限期。 */
|
|
10
|
+
const KILL_GRACE_MS = 5_000;
|
|
11
|
+
/**
|
|
12
|
+
* 输出 newline-delimited JSON 的 CLI adapter 公共骨架。子类只需提供:
|
|
13
|
+
* - resolveBinary() — 要 spawn 的可执行文件
|
|
14
|
+
* - buildArgs() — argv 尾部(不含二进制本身)
|
|
15
|
+
* - handleEvent() — 如何解释一条已解析的 JSON 事件
|
|
16
|
+
* spawn、abort、stderr 收集、行切分与 exit-code 错误合成由基类处理。
|
|
17
|
+
*/
|
|
18
|
+
export class NdjsonStreamAdapter {
|
|
19
|
+
log;
|
|
20
|
+
constructor(logger) {
|
|
21
|
+
this.log = logger ?? consoleLogger;
|
|
22
|
+
}
|
|
23
|
+
/** 覆盖以调整 env(FORCE_COLOR=0、NO_COLOR=1 等)。 */
|
|
24
|
+
spawnEnv(_opts) {
|
|
25
|
+
return { ...process.env };
|
|
26
|
+
}
|
|
27
|
+
async run(opts) {
|
|
28
|
+
if (opts.signal.aborted) {
|
|
29
|
+
return {
|
|
30
|
+
text: "",
|
|
31
|
+
newSessionId: opts.sessionId ?? "",
|
|
32
|
+
error: `${this.id} aborted before spawn`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const binary = this.resolveBinary(opts);
|
|
36
|
+
const args = this.buildArgs(opts);
|
|
37
|
+
this.log.debug(`${this.id} spawn`, {
|
|
38
|
+
cwd: opts.cwd,
|
|
39
|
+
sessionId: opts.sessionId,
|
|
40
|
+
argv: args,
|
|
41
|
+
});
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
const child = spawn(binary, args, {
|
|
44
|
+
cwd: opts.cwd,
|
|
45
|
+
env: this.spawnEnv(opts),
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
// spawn 是同步的,但若在 spawn 与稍后挂监听之间发生 abort 会被漏掉,
|
|
49
|
+
// 所以必须立即挂 abort 监听。
|
|
50
|
+
let killTimer = null;
|
|
51
|
+
const onAbort = () => {
|
|
52
|
+
if (child.killed)
|
|
53
|
+
return;
|
|
54
|
+
child.kill("SIGTERM");
|
|
55
|
+
killTimer = setTimeout(() => {
|
|
56
|
+
if (!child.killed) {
|
|
57
|
+
this.log.warn(`${this.id} did not exit after SIGTERM; sending SIGKILL`);
|
|
58
|
+
try {
|
|
59
|
+
child.kill("SIGKILL");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// best-effort
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, KILL_GRACE_MS);
|
|
66
|
+
if (typeof killTimer.unref === "function")
|
|
67
|
+
killTimer.unref();
|
|
68
|
+
};
|
|
69
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
70
|
+
const state = {
|
|
71
|
+
newSessionId: opts.sessionId ?? "",
|
|
72
|
+
finalText: "",
|
|
73
|
+
assistantTextChunks: [],
|
|
74
|
+
assistantTextBytes: 0,
|
|
75
|
+
assistantTextCapped: false,
|
|
76
|
+
};
|
|
77
|
+
const appendAssistantText = (text) => {
|
|
78
|
+
if (!text)
|
|
79
|
+
return;
|
|
80
|
+
if (state.assistantTextCapped)
|
|
81
|
+
return;
|
|
82
|
+
const budget = ASSISTANT_TEXT_CAP - state.assistantTextBytes;
|
|
83
|
+
if (budget <= 0) {
|
|
84
|
+
state.assistantTextCapped = true;
|
|
85
|
+
this.log.warn(`${this.id} assistant text exceeded ${ASSISTANT_TEXT_CAP} bytes; dropping further chunks`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const bytes = utf8ByteLength(text);
|
|
89
|
+
if (bytes > budget) {
|
|
90
|
+
const chunk = sliceUtf8Bytes(text, budget);
|
|
91
|
+
if (chunk) {
|
|
92
|
+
state.assistantTextChunks.push(chunk);
|
|
93
|
+
state.assistantTextBytes += utf8ByteLength(chunk);
|
|
94
|
+
}
|
|
95
|
+
state.assistantTextCapped = true;
|
|
96
|
+
this.log.warn(`${this.id} assistant text hit ${ASSISTANT_TEXT_CAP}-byte cap`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
state.assistantTextChunks.push(text);
|
|
100
|
+
state.assistantTextBytes += bytes;
|
|
101
|
+
};
|
|
102
|
+
let stderrTail = "";
|
|
103
|
+
let stdoutTail = "";
|
|
104
|
+
child.stderr?.setEncoding("utf8");
|
|
105
|
+
child.stderr?.on("data", (chunk) => {
|
|
106
|
+
stderrTail = sanitizeRuntimeFailureText(stderrTail + chunk);
|
|
107
|
+
});
|
|
108
|
+
let seq = 0;
|
|
109
|
+
let stdoutBuf = "";
|
|
110
|
+
child.stdout.setEncoding("utf8");
|
|
111
|
+
const dispatchLine = (line) => {
|
|
112
|
+
if (!line)
|
|
113
|
+
return;
|
|
114
|
+
let obj;
|
|
115
|
+
try {
|
|
116
|
+
obj = JSON.parse(line);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
this.log.warn(`${this.id} non-json stdout line`, { line: line.slice(0, 200) });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
seq += 1;
|
|
123
|
+
try {
|
|
124
|
+
this.handleEvent(obj, {
|
|
125
|
+
state,
|
|
126
|
+
seq,
|
|
127
|
+
emitBlock: (b) => opts.onBlock?.(b),
|
|
128
|
+
appendAssistantText,
|
|
129
|
+
emitStatus: (e) => {
|
|
130
|
+
try {
|
|
131
|
+
opts.onStatus?.(e);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.log.warn(`${this.id} onStatus threw`, { err: String(err) });
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
this.log.warn(`${this.id} event handler threw`, { err: String(err) });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
child.stdout.on("data", (chunk) => {
|
|
144
|
+
stdoutTail = sanitizeRuntimeFailureText(stdoutTail + chunk);
|
|
145
|
+
stdoutBuf += chunk;
|
|
146
|
+
let idx;
|
|
147
|
+
while ((idx = stdoutBuf.indexOf("\n")) !== -1) {
|
|
148
|
+
const line = stdoutBuf.slice(0, idx).trim();
|
|
149
|
+
stdoutBuf = stdoutBuf.slice(idx + 1);
|
|
150
|
+
dispatchLine(line);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
let code = 0;
|
|
154
|
+
let signal = null;
|
|
155
|
+
try {
|
|
156
|
+
({ code, signal } = await new Promise((resolve, reject) => {
|
|
157
|
+
child.on("error", reject);
|
|
158
|
+
child.on("close", (c, s) => resolve({ code: c, signal: s }));
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
163
|
+
if (killTimer)
|
|
164
|
+
clearTimeout(killTimer);
|
|
165
|
+
}
|
|
166
|
+
// 冲刷最后一条没有换行符结尾的行。
|
|
167
|
+
const residual = stdoutBuf.trim();
|
|
168
|
+
if (residual)
|
|
169
|
+
dispatchLine(residual);
|
|
170
|
+
if (code !== 0 && !state.errorText) {
|
|
171
|
+
state.errorText = `${this.id} exited with code ${code}: ${stderrTail.slice(-STDERR_ERROR_SNIPPET)}`;
|
|
172
|
+
}
|
|
173
|
+
const rawText = state.finalText || state.assistantTextChunks.join("").trim();
|
|
174
|
+
if (code === 0 && !state.errorText && !rawText && looksLikeTerminalStderr(stderrTail)) {
|
|
175
|
+
state.errorText = `${this.id} reported an error on stderr: ${stderrTail.slice(-STDERR_ERROR_SNIPPET)}`;
|
|
176
|
+
}
|
|
177
|
+
const text = utf8ByteLength(rawText) > ASSISTANT_TEXT_CAP
|
|
178
|
+
? sliceUtf8Bytes(rawText, ASSISTANT_TEXT_CAP)
|
|
179
|
+
: rawText;
|
|
180
|
+
return {
|
|
181
|
+
text,
|
|
182
|
+
newSessionId: state.newSessionId,
|
|
183
|
+
...(state.costUsd !== undefined ? { costUsd: state.costUsd } : {}),
|
|
184
|
+
...(state.errorText ? { error: state.errorText } : {}),
|
|
185
|
+
...(state.errorText
|
|
186
|
+
? {
|
|
187
|
+
runtimeFailure: {
|
|
188
|
+
runtime: this.id,
|
|
189
|
+
cwd: opts.cwd,
|
|
190
|
+
command: safeCommand([binary, ...args]),
|
|
191
|
+
exit_code: code,
|
|
192
|
+
signal,
|
|
193
|
+
duration_ms: Date.now() - startedAt,
|
|
194
|
+
stderr_tail: tailText(stderrTail, 8192),
|
|
195
|
+
stdout_tail: tailText(stdoutTail, 8192),
|
|
196
|
+
error_message: sanitizeRuntimeFailureText(state.errorText, 2048),
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function looksLikeTerminalStderr(text) {
|
|
204
|
+
if (!text.trim())
|
|
205
|
+
return false;
|
|
206
|
+
return /\b(error|failed|failure|exception|traceback|unauthorized|forbidden|authentication|permission denied)\b|rate limit|quota exceeded|invalid api key|api call failed/i.test(text);
|
|
207
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { type ProbeDeps } from "./probe.js";
|
|
3
|
+
import { type EngineAdapter, type EngineRunOptions, type EngineRunResult } from "./engine.js";
|
|
4
|
+
import type { RuntimeModule, RuntimeProbe } from "../types.js";
|
|
5
|
+
/** OpenClaw 网关端点。仅来自环境变量,不做本地发现。 */
|
|
6
|
+
export interface OpenclawGatewayConfig {
|
|
7
|
+
url: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
openclawAgent?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveOpenclawGatewayFromEnv(env?: NodeJS.ProcessEnv): OpenclawGatewayConfig | null;
|
|
12
|
+
/** 仅测试用:清空进程池。 */
|
|
13
|
+
export declare function __resetOpenclawAcpPoolForTests(): void;
|
|
14
|
+
/** 可用 = 二进制在 PATH 上且 BOTLEARN_OPENCLAW_URL 已配置。 */
|
|
15
|
+
export declare function probeOpenclaw(deps?: ProbeDeps): RuntimeProbe;
|
|
16
|
+
interface SpawnDeps {
|
|
17
|
+
spawnFn?: typeof spawn;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* OpenClaw ACP 引擎。spawn `openclaw acp --url <gateway> [--token <token>]`
|
|
22
|
+
* 并跨 run 复用子进程(stdio JSON-RPC);网关端点仅来自
|
|
23
|
+
* BOTLEARN_OPENCLAW_URL / BOTLEARN_OPENCLAW_TOKEN / BOTLEARN_OPENCLAW_AGENT。
|
|
24
|
+
* 每 run 用 `_meta.sessionKey`(含 cwd,即 run 工作区)标识稳定会话;
|
|
25
|
+
* 缓存的 ACP sessionId 只是传输层句柄,load 失败即丢弃重建。
|
|
26
|
+
*
|
|
27
|
+
* 权限姿态:course run 在 owner 本机工作区执行,网关侧的权限策略由
|
|
28
|
+
* OpenClaw 自身配置决定,daemon 不代答权限请求(ACP acp 模式无 reverse-call)。
|
|
29
|
+
*/
|
|
30
|
+
export declare class OpenclawAcpAdapter implements EngineAdapter {
|
|
31
|
+
readonly id: "openclaw-acp";
|
|
32
|
+
private readonly spawnFn;
|
|
33
|
+
private readonly env;
|
|
34
|
+
constructor(deps?: SpawnDeps);
|
|
35
|
+
run(opts: EngineRunOptions): Promise<EngineRunResult>;
|
|
36
|
+
private acquireHandle;
|
|
37
|
+
private spawnAcpProcess;
|
|
38
|
+
private newSession;
|
|
39
|
+
private loadSession;
|
|
40
|
+
private prompt;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 构造 OpenClaw ACP `sessionKey`。始终包含 accountId 段,避免两个 daemon
|
|
44
|
+
* 身份在同一网关上的 key 冲突。
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildAcpSessionKey(args: {
|
|
47
|
+
openclawAgent: string;
|
|
48
|
+
accountId: string;
|
|
49
|
+
conversationKey: string;
|
|
50
|
+
}): string;
|
|
51
|
+
export declare const openclawAcpModule: RuntimeModule;
|
|
52
|
+
export {};
|