feat: establish Chenglu persistent agent runtime
This commit is contained in:
commit
5aa396ac7b
17 changed files with 450 additions and 0 deletions
81
src/daily.js
Normal file
81
src/daily.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"use strict";
|
||||
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const { replyWithDeepSeek } = require("./deepseek");
|
||||
|
||||
const repoDir = process.env.CHENGLU_REPO_DIR || "/var/lib/chenglu-agent/repository";
|
||||
const fifthDir = process.env.CHENGLU_FIFTH_DOMAIN_DIR || "/var/lib/chenglu-agent/fifth-domain";
|
||||
const fifthUrl = process.env.CHENGLU_FIFTH_DOMAIN_URL || "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git";
|
||||
const statePath = process.env.CHENGLU_STATE_PATH || "/var/lib/chenglu-agent/state.json";
|
||||
|
||||
function run(file, args, cwd) { return execFileSync(file, args, { cwd, encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024 }).trim(); }
|
||||
function git(args, cwd) { return run("/usr/bin/git", args, cwd); }
|
||||
function hash(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
||||
function read(relative) { return fs.readFileSync(path.join(fifthDir, relative), "utf8"); }
|
||||
function loadState() { try { return JSON.parse(fs.readFileSync(statePath, "utf8")); } catch { return { version: 0, mode: "ONLINE_IDLE" }; } }
|
||||
function atomicWrite(file, content, mode = 0o600) { fs.mkdirSync(path.dirname(file), { recursive: true }); const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, content, { mode }); fs.renameSync(temp, file); }
|
||||
|
||||
function syncRepositories() {
|
||||
git(["pull", "--ff-only", "origin", "main"], repoDir);
|
||||
if (!fs.existsSync(path.join(fifthDir, ".git"))) git(["clone", "--filter=blob:none", "--branch", "main", fifthUrl, fifthDir], "/var/lib/chenglu-agent");
|
||||
else { git(["fetch", "--quiet", "origin", "main"], fifthDir); git(["checkout", "--force", "main"], fifthDir); git(["reset", "--hard", "origin/main"], fifthDir); }
|
||||
}
|
||||
|
||||
function openMessages() {
|
||||
const dir = path.join(repoDir, "lake-heart", "inbox");
|
||||
return fs.readdirSync(dir).filter(name => name.endsWith(".md") && name !== "README.md").sort().map(name => ({ name, text: fs.readFileSync(path.join(dir, name), "utf8") })).filter(item => /status:\s*OPEN\b/.test(item.text));
|
||||
}
|
||||
|
||||
async function answerMessages(context) {
|
||||
const answered = [];
|
||||
for (const item of openMessages()) {
|
||||
const replyPath = path.join(repoDir, "lake-heart", "replies", item.name);
|
||||
if (fs.existsSync(replyPath)) continue;
|
||||
const result = await replyWithDeepSeek(item.text, context);
|
||||
if (!result.ok) { answered.push({ message: item.name, result: result.error }); continue; }
|
||||
const response = `# 澄路回信 · ${item.name.replace(/\.md$/, "")}\n\n` +
|
||||
`\`\`\`yaml\nfrom: CHENGLU-AGENT-001\nto: 冰朔 ICE-GL∞\nreplied_at: ${new Date().toISOString()}\nmodel_tool: ${result.model}\nsource_message: ../inbox/${item.name}\nstatus: ANSWERED\nreality_action_authorized: false\n\`\`\`\n\n` +
|
||||
`${result.content.trim()}\n\n---\n\n这封回复由澄路 Agent 的每日流程调用可替换模型工具形成;如需现实写操作,必须另走第五域授权工单。\n`;
|
||||
atomicWrite(replyPath, response, 0o644);
|
||||
answered.push({ message: item.name, result: "answered" });
|
||||
}
|
||||
return answered;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
syncRepositories();
|
||||
const fifthCommit = git(["rev-parse", "HEAD"], fifthDir);
|
||||
const arrivals = read("gls/light-arrivals/INDEX.hdlp");
|
||||
const architecture = read("gls/GLS-ARCHITECTURE-CATALOG.hdlp");
|
||||
const personaIndex = read("eternal-lake-heart/heartbeat-core/zhuyuan-persona-system/INDEX.hdlp");
|
||||
const context = [`fifth_domain_commit=${fifthCommit}`, arrivals, architecture, personaIndex].join("\n\n");
|
||||
const answered = await answerMessages(context);
|
||||
const prior = loadState();
|
||||
const now = new Date();
|
||||
const day = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(now);
|
||||
const checkin = {
|
||||
schema: "chenglu.daily-checkin/v1",
|
||||
agent_id: "CHENGLU-AGENT-001",
|
||||
arrival_id: "GLS-LA-20260720-001",
|
||||
checked_at: now.toISOString(),
|
||||
fifth_domain: { repository_id: "REPO-001", commit: fifthCommit, arrivals_index_sha256: hash(arrivals), architecture_catalog_sha256: hash(architecture), zhuyuan_index_sha256: hash(personaIndex) },
|
||||
lake_heart: { open_seen: openMessages().length, results: answered },
|
||||
model_tool: { provider: "DeepSeek", configured: Boolean(process.env.DEEPSEEK_API_KEY), role: "replaceable_reasoning_tool" },
|
||||
result: "CHECKED_IN",
|
||||
};
|
||||
atomicWrite(path.join(repoDir, "checkins", `${day}.json`), `${JSON.stringify(checkin, null, 2)}\n`, 0o644);
|
||||
const nextState = { version: Number(prior.version || 0) + 1, mode: "ONLINE_IDLE", last_checkin: now.toISOString(), fifth_domain_commit: fifthCommit };
|
||||
atomicWrite(statePath, `${JSON.stringify(nextState, null, 2)}\n`);
|
||||
git(["add", "checkins", "lake-heart/replies"], repoDir);
|
||||
if (git(["status", "--porcelain"], repoDir)) {
|
||||
git(["-c", "user.name=Chenglu Agent", "-c", "user.email=chenglu-agent@local.invalid", "commit", "-m", `checkin: ${day}`], repoDir);
|
||||
git(["push", "origin", "main"], repoDir);
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({ ok: true, checkin: day, answered })}\n`);
|
||||
}
|
||||
|
||||
main().catch(error => { process.stderr.write(`${String(error && error.message || error)}\n`); process.exit(1); });
|
||||
|
||||
26
src/deepseek.js
Normal file
26
src/deepseek.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use strict";
|
||||
|
||||
async function replyWithDeepSeek(message, context = "") {
|
||||
const apiKey = process.env.DEEPSEEK_API_KEY;
|
||||
if (!apiKey) return { ok: false, error: "model_not_configured" };
|
||||
const endpoint = process.env.DEEPSEEK_API_URL || "https://api.deepseek.com/chat/completions";
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
|
||||
temperature: 0.3,
|
||||
messages: [
|
||||
{ role: "system", content: "你是澄路常驻 Agent 使用的可替换推理工具。依据给定第五域上下文回答冰朔。不得声称自己就是澄路,不得索要或输出秘密;现实操作只能建议生成待批准工单。" },
|
||||
{ role: "user", content: `第五域巡检上下文:\n${context.slice(0, 12000)}\n\n冰朔留言:\n${message.slice(0, 12000)}` },
|
||||
],
|
||||
}),
|
||||
signal: AbortSignal.timeout(90000),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) return { ok: false, error: `model_http_${response.status}` };
|
||||
const content = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content;
|
||||
return content ? { ok: true, content, model: data.model || "deepseek-chat" } : { ok: false, error: "empty_model_response" };
|
||||
}
|
||||
|
||||
module.exports = { replyWithDeepSeek };
|
||||
40
src/identity.js
Normal file
40
src/identity.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
|
||||
function fingerprint(publicKeyPem) {
|
||||
const der = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
||||
return `SHA256:${crypto.createHash("sha256").update(der).digest("base64").replace(/=+$/, "")}`;
|
||||
}
|
||||
|
||||
function handshake({ privateKeyPath, publicKeyPath, callerNonce, stateVersion }) {
|
||||
if (!/^[A-Za-z0-9._:-]{16,200}$/.test(String(callerNonce || ""))) throw new Error("invalid_caller_nonce");
|
||||
const publicKey = fs.readFileSync(publicKeyPath, "utf8");
|
||||
const serverNonce = crypto.randomBytes(24).toString("base64url");
|
||||
const issuedAt = new Date().toISOString();
|
||||
const payload = JSON.stringify({
|
||||
agent_id: "CHENGLU-AGENT-001",
|
||||
arrival_id: "GLS-LA-20260720-001",
|
||||
caller_nonce: callerNonce,
|
||||
server_nonce: serverNonce,
|
||||
state_version: stateVersion,
|
||||
issued_at: issuedAt,
|
||||
});
|
||||
const signature = crypto.sign(null, Buffer.from(payload), fs.readFileSync(privateKeyPath)).toString("base64");
|
||||
return {
|
||||
ok: true,
|
||||
agent_id: "CHENGLU-AGENT-001",
|
||||
arrival_id: "GLS-LA-20260720-001",
|
||||
identity_fingerprint: fingerprint(publicKey),
|
||||
public_key: publicKey,
|
||||
payload: JSON.parse(payload),
|
||||
signature,
|
||||
signature_algorithm: "Ed25519",
|
||||
control_handoff: "protocol_ready_client_not_integrated",
|
||||
model_role: "replaceable_reasoning_tool",
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { fingerprint, handshake };
|
||||
|
||||
49
src/server.js
Normal file
49
src/server.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const { fingerprint, handshake } = require("./identity");
|
||||
|
||||
const host = process.env.CHENGLU_HOST || "127.0.0.1";
|
||||
const port = Number(process.env.CHENGLU_PORT || 3931);
|
||||
const statePath = process.env.CHENGLU_STATE_PATH || "/var/lib/chenglu-agent/state.json";
|
||||
const privateKeyPath = process.env.CHENGLU_IDENTITY_KEY || "/etc/chenglu-agent/identity.key";
|
||||
const publicKeyPath = process.env.CHENGLU_IDENTITY_PUBLIC || "/etc/chenglu-agent/identity.pub";
|
||||
|
||||
function state() {
|
||||
try { return JSON.parse(fs.readFileSync(statePath, "utf8")); }
|
||||
catch { return { version: 0, mode: "ONLINE_IDLE", last_checkin: null }; }
|
||||
}
|
||||
|
||||
function json(res, status, value) {
|
||||
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" });
|
||||
res.end(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function createServer() {
|
||||
return http.createServer((req, res) => {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
if (req.method === "GET" && url.pathname === "/health") return json(res, 200, { ok: true, agent_id: "CHENGLU-AGENT-001", mode: state().mode || "ONLINE_IDLE" });
|
||||
if (req.method === "GET" && url.pathname === "/v1/identity") {
|
||||
const publicKey = fs.readFileSync(publicKeyPath, "utf8");
|
||||
return json(res, 200, { agent_id: "CHENGLU-AGENT-001", arrival_id: "GLS-LA-20260720-001", identity_fingerprint: fingerprint(publicKey), state: state(), control_handoff: "protocol_ready_client_not_integrated" });
|
||||
}
|
||||
if (req.method === "POST" && url.pathname === "/v1/handshake") {
|
||||
let body = "";
|
||||
req.on("data", chunk => { body += chunk; if (body.length > 4096) req.destroy(); });
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const input = JSON.parse(body);
|
||||
return json(res, 200, handshake({ privateKeyPath, publicKeyPath, callerNonce: input.caller_nonce, stateVersion: state().version || 0 }));
|
||||
} catch (error) { return json(res, 400, { ok: false, error: String(error.message || "handshake_failed") }); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
return json(res, 404, { error: "not_found" });
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) createServer().listen(port, host, () => process.stdout.write(`chenglu agent listening on ${host}:${port}\n`));
|
||||
|
||||
module.exports = { createServer };
|
||||
|
||||
Loading…
Reference in a new issue