first commit

This commit is contained in:
魏天炜
2026-11-12 15:29:12 +08:00
commit 5dabea54ab
93 changed files with 12170 additions and 0 deletions

110
demo/laptop/daemon.js Normal file
View File

@@ -0,0 +1,110 @@
// 本地守护进程:跑在用户电脑上,持有仓库、.env、工具链。
// 它不调模型、不存会话,只做一件事:领取工具调用 → 在仓库里执行 → 回传结果。
// 为了演示,每次执行后顺手拍一下 git 快照(status / diff),写进 trace。
const fs = require('node:fs');
const path = require('node:path');
const { spawn, execFileSync } = require('node:child_process');
const { tracer } = require('../shared');
const CLOUD = process.env.CLOUD_URL;
const DEVICE = process.env.DEVICE_ID;
const REPO = process.env.REPO_DIR;
const trace = tracer('laptop-daemon');
let jobSeq = Number(process.env.JOB_SEQ_START || 1);
const done = new Map(); // call_id -> 已执行结果(同一个调用被重发时不重复执行)
const git = (...a) => { try { return execFileSync('git', a, { cwd: REPO, encoding: 'utf8' }); } catch (e) { return e.stdout || ''; } };
function repoSnapshot() {
return { status: git('status', '--short').trim(), diff: git('diff').trim(), head: git('rev-parse', '--short', 'HEAD').trim() };
}
function sh(command, timeoutMs) {
return new Promise((ok) => {
const child = spawn('/bin/sh', ['-c', command], { cwd: REPO, env: process.env });
let out = '';
child.stdout.on('data', (c) => (out += c));
child.stderr.on('data', (c) => (out += c));
const timer = timeoutMs ? setTimeout(() => child.kill('SIGKILL'), timeoutMs) : null;
child.on('exit', (code) => { if (timer) clearTimeout(timer); ok({ code, out, pid: child.pid }); });
});
}
async function runTool(cmd) {
const { tool, input } = cmd;
const file = input.path ? path.join(REPO, input.path) : null;
if (tool === 'read_file') {
const lines = fs.readFileSync(file, 'utf8').split('\n');
return { output: lines.map((l, i) => `${String(i + 1).padStart(3)}| ${l}`).join('\n') };
}
if (tool === 'edit_file') {
const src = fs.readFileSync(file, 'utf8');
const n = src.split(input.old_text).length - 1;
if (n !== 1) return { output: `错误:old_text 出现了 ${n} 次,需要恰好 1 次` };
fs.writeFileSync(file, src.replace(input.old_text, input.new_text));
return { output: `已修改 ${input.path}` };
}
if (tool === 'write_file') {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, input.content);
return { output: `已写入 ${input.path}(${input.content.length} 字节)` };
}
if (tool === 'run_command') {
const r = await sh(input.command, 15000);
trace('process_exit', { command: input.command, child_pid: r.pid, exit_code: r.code });
return { output: `退出码 ${r.code}\n${r.out.slice(-3000)}`, child_pid: r.pid };
}
if (tool === 'start_background') {
const jobId = `job-${jobSeq++}`;
const started = Date.now();
const child = spawn('/bin/sh', ['-c', input.command], { cwd: REPO, env: process.env });
let out = '';
child.stdout.on('data', (c) => (out += c));
child.stderr.on('data', (c) => (out += c));
trace('job_start', { job_id: jobId, command: input.command, child_pid: child.pid, session: cmd.session });
child.on('exit', async (code) => {
trace('job_exit', { job_id: jobId, child_pid: child.pid, exit_code: code, ms: Date.now() - started });
const tail = out.trim().split('\n').slice(-12).join('\n');
await postRetry(`/sessions/${cmd.session}/events`, { type: 'job_finished', job_id: jobId, command: input.command, exit_code: code, tail, ran_on: DEVICE, ran_pid: child.pid, by: `daemon:${process.pid}` });
});
return { output: `已在后台启动 ${jobId}(pid ${child.pid})`, child_pid: child.pid };
}
return { output: `未知工具 ${tool}` };
}
async function postRetry(p, b) {
for (let i = 0; i < 30; i++) {
try { return await (await fetch(CLOUD + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(b) })).json(); }
catch { await new Promise((r) => setTimeout(r, 1000)); }
}
}
async function handle(cmd) {
const started = Date.now();
let result = done.get(cmd.call_id);
const replay = !!result;
if (!result) {
try { result = await runTool(cmd); } catch (e) { result = { output: `错误:${e.message}` }; }
done.set(cmd.call_id, result);
}
const repo_after = repoSnapshot();
trace('tool_exec', { call_id: cmd.call_id, tool: cmd.tool, input: cmd.input, cwd: REPO, ms: Date.now() - started, replay, output: result.output, child_pid: result.child_pid, repo_after });
await postRetry(`/devices/${DEVICE}/result`, { req_id: cmd.req_id, output: result.output, host: DEVICE, pid: process.pid, repo_after: { status: repo_after.status } });
}
let stopping = false;
async function loop() {
trace('daemon_start', { device: DEVICE, repo: REPO, repo_state: repoSnapshot() });
while (!stopping) {
let cmd = null;
try { cmd = await (await fetch(`${CLOUD}/devices/${DEVICE}/poll`)).json(); }
catch { await new Promise((r) => setTimeout(r, 1000)); continue; }
if (cmd) handle(cmd);
}
}
process.on('SIGTERM', async () => {
stopping = true;
trace('daemon_stop', { device: DEVICE, reason: 'laptop lid closed (SIGTERM)' });
try { await fetch(`${CLOUD}/devices/${DEVICE}/bye`, { method: 'POST' }); } catch {}
process.exit(0);
});
loop();