From fe490e8711ca327bff37dd5762d2aaa0fc49a0ca Mon Sep 17 00:00:00 2001 From: chorewer <1175794600@qq.com> Date: Thu, 30 Apr 2026 18:01:14 +0800 Subject: [PATCH 1/3] feat: cross-host agent log ingest (issue #18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add POST /api/ingest endpoint and run.sh upload hook so agents running on any machine can stream their JSONL events to the deployed dashboard in real time. - ui/server/src/routes/ingest.ts: new endpoint. Validates Bearer token (KIP_INGEST_TOKEN env var, optional), checks agent/runId are safe path segments, writes events to agents//logs//absorber.jsonl and maintains meta.json (creates on first event, updates completedAt and status on session_end). No extra WebSocket wiring needed — the existing fs.watch in logs.ts detects the appended bytes and pushes them to connected clients automatically. - ui/server/src/index.ts: register ingest route. - agents/blueprint-absorber/run.sh: add ingest() inside the inline Python log-parser. Reads KIP_INGEST_URL / KIP_INGEST_TOKEN from env; if KIP_INGEST_URL is unset the function is a no-op so local-only mode is unchanged. Fire-and-forget with 3s timeout — failures are silently ignored and never block the agent. Co-Authored-By: Claude Opus 4.7 --- agents/blueprint-absorber/run.sh | 21 +++++++++- ui/server/src/index.ts | 2 + ui/server/src/routes/ingest.ts | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 ui/server/src/routes/ingest.ts diff --git a/agents/blueprint-absorber/run.sh b/agents/blueprint-absorber/run.sh index 710e1f8..833ad54 100755 --- a/agents/blueprint-absorber/run.sh +++ b/agents/blueprint-absorber/run.sh @@ -124,16 +124,35 @@ Instructions: - If the file was listed under '## Pending', remove it from there. 8. Report what changed and whether validation passed." \ 2>>"$stderr_dest" | python3 -u -c " -import sys, json, datetime +import sys, json, datetime, urllib.request, os VERBOSE = '$VERBOSE_LOGS' == 'true' RAW = open('$RAW_FILE', 'a') if VERBOSE else None JSONL = open('$JSONL_FILE', 'a') +_INGEST_URL = os.getenv('KIP_INGEST_URL', '') +_INGEST_TOKEN = os.getenv('KIP_INGEST_TOKEN', '') +_AGENT = 'blueprint-absorber' +_RUN_ID = 'run-$RUN_TS' +_HINT_FILE = '$HINT_FILE' + +def ingest(row): + if not _INGEST_URL: + return + payload = json.dumps({'agent': _AGENT, 'runId': _RUN_ID, 'hintFile': _HINT_FILE, 'row': row}).encode() + req = urllib.request.Request(_INGEST_URL, data=payload, + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _INGEST_TOKEN}, + method='POST') + try: + urllib.request.urlopen(req, timeout=3) + except Exception: + pass + def emit(event_type, **fields): row = {'ts': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z'), 'event': event_type, **fields} JSONL.write(json.dumps(row) + '\n') JSONL.flush() + ingest(row) last_result = '' diff --git a/ui/server/src/index.ts b/ui/server/src/index.ts index 47e248e..767d3b3 100644 --- a/ui/server/src/index.ts +++ b/ui/server/src/index.ts @@ -11,6 +11,7 @@ import { register as registerAgents } from './routes/agents.js'; import { register as registerLogs } from './routes/logs.js'; import { register as registerSummary } from './routes/summary.js'; import { register as registerNodes } from './routes/nodes.js'; +import { register as registerIngest } from './routes/ingest.js'; export interface ProjectPaths { projectPath: string; @@ -64,6 +65,7 @@ export async function createServer(options: { projectPath: string; port: number registerLogs(fastify, paths); registerSummary(fastify, paths); registerNodes(fastify, paths); + registerIngest(fastify, paths); await fastify.listen({ port, host: '0.0.0.0' }); return fastify; diff --git a/ui/server/src/routes/ingest.ts b/ui/server/src/routes/ingest.ts new file mode 100644 index 0000000..bb85906 --- /dev/null +++ b/ui/server/src/routes/ingest.ts @@ -0,0 +1,72 @@ +import fs from 'fs'; +import path from 'path'; +import type { FastifyInstance } from 'fastify'; +import type { ProjectPaths } from '../index.js'; + +const TOKEN = process.env.KIP_INGEST_TOKEN ?? ''; + +// agent name and runId must be safe path segments +function safeSeg(s: unknown): s is string { + return typeof s === 'string' && /^[\w.\-:]+$/.test(s) && !s.includes('..'); +} + +export function register(fastify: FastifyInstance, paths: ProjectPaths) { + fastify.post('/api/ingest', async (req, reply) => { + if (TOKEN) { + const auth = (req.headers.authorization ?? '') as string; + if (auth !== `Bearer ${TOKEN}`) { + return reply.code(401).send({ error: 'unauthorized' }); + } + } + + const { agent, runId, hintFile, row } = req.body as Record; + + if (!safeSeg(agent) || !safeSeg(runId)) { + return reply.code(400).send({ error: 'invalid agent or runId' }); + } + + const runDir = path.resolve(paths.agentsPath, agent, 'logs', runId); + if (!runDir.startsWith(paths.agentsPath + path.sep)) { + return reply.code(400).send({ error: 'path outside agents directory' }); + } + + fs.mkdirSync(runDir, { recursive: true }); + + const metaFile = path.join(runDir, 'meta.json'); + const jsonlFile = path.join(runDir, 'absorber.jsonl'); + + // Write meta.json on the first event for this run + if (!fs.existsSync(metaFile)) { + const meta = { + agent, + hint_file: typeof hintFile === 'string' ? hintFile : null, + startedAt: (row as Record)?.ts ?? new Date().toISOString(), + completedAt: null, + status: 'running', + model: null, + }; + fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2)); + } + + // Update meta on session_end + if ((row as Record)?.event === 'session_end') { + try { + const meta = JSON.parse(fs.readFileSync(metaFile, 'utf-8')); + meta.completedAt = (row as Record).ts ?? new Date().toISOString(); + meta.status = 'done'; + const usage = (row as Record).model_usage as Record | undefined; + if (usage) { + const first = Object.keys(usage)[0]; + if (first) meta.model = first; + } + fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2)); + } catch { /* ignore */ } + } + + // Append event row to JSONL — fs.watch in logs.ts picks this up automatically + // and broadcasts to any connected WebSocket clients watching this file + fs.appendFileSync(jsonlFile, JSON.stringify(row) + '\n'); + + return reply.code(204).send(); + }); +} From 2453f54efaf169dea73c2579e83459528f230bab Mon Sep 17 00:00:00 2001 From: chorewer <1175794600@qq.com> Date: Thu, 30 Apr 2026 18:19:58 +0800 Subject: [PATCH 2/3] fix(ingest): return 400 when request body is missing or unparseable Co-Authored-By: Claude Opus 4.7 --- ui/server/src/routes/ingest.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/server/src/routes/ingest.ts b/ui/server/src/routes/ingest.ts index bb85906..3188bdb 100644 --- a/ui/server/src/routes/ingest.ts +++ b/ui/server/src/routes/ingest.ts @@ -19,7 +19,10 @@ export function register(fastify: FastifyInstance, paths: ProjectPaths) { } } - const { agent, runId, hintFile, row } = req.body as Record; + const body = req.body as Record | undefined; + if (!body) return reply.code(400).send({ error: 'missing JSON body' }); + + const { agent, runId, hintFile, row } = body; if (!safeSeg(agent) || !safeSeg(runId)) { return reply.code(400).send({ error: 'invalid agent or runId' }); From 3aaa04a35438961e91cd5fa55cbd2f46834c7ea8 Mon Sep 17 00:00:00 2001 From: chorewer <1175794600@qq.com> Date: Thu, 30 Apr 2026 18:28:45 +0800 Subject: [PATCH 3/3] docs: add blueprint-absorber README with local and remote ingest usage Co-Authored-By: Claude Opus 4.7 --- agents/blueprint-absorber/README.md | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 agents/blueprint-absorber/README.md diff --git a/agents/blueprint-absorber/README.md b/agents/blueprint-absorber/README.md new file mode 100644 index 0000000..5803cac --- /dev/null +++ b/agents/blueprint-absorber/README.md @@ -0,0 +1,54 @@ +# Blueprint Absorber Agent + +将 `draft-hint/` 里的数学家提示文件吸收进 `blueprint/src/chapters/*.tex`。 + +## 前置条件 + +```bash +pip install -e tools/plastexdepgraph +pip install -e tools/leanblueprint +``` + +## 本地运行 + +```bash +# 处理单个文件 +./agents/blueprint-absorber/run.sh draft-hint/foo.md + +# 处理所有待吸收文件 +./agents/blueprint-absorber/run.sh + +# 指定模型 +./agents/blueprint-absorber/run.sh --model opus draft-hint/foo.md + +# 试运行(只显示会处理哪些文件,不实际调用 claude) +./agents/blueprint-absorber/run.sh --dry-run +``` + +运行日志保存在 `agents/blueprint-absorber/logs/run-/absorber.jsonl`。 + +## 远程监控(多机协作) + +如果需要在本机跑 agent、同时让部署在服务器上的 dashboard 实时看到运行进度, +设置以下两个环境变量即可: + +```bash +export KIP_INGEST_URL=https://kip.opensii.ai/dashboard/api/ingest +export KIP_INGEST_TOKEN= # 与服务器 KIP_INGEST_TOKEN 一致,未配置 token 时可不设 +``` + +设置后正常跑 agent,每条日志 event 会自动上报到服务器,在 dashboard 的 +**Logs** tab 实时可见。未设置 `KIP_INGEST_URL` 时行为与纯本地模式完全一致。 + +### 验证网络是否通 + +```bash +curl -s -w "\nHTTP %{http_code}" -X POST "$KIP_INGEST_URL" -H "Content-Type: application/json" -d '{"agent":"blueprint-absorber","runId":"run-test","hintFile":"test","row":{"ts":"2026-01-01T00:00:00Z","event":"text","content":"ping"}}' +``` + +返回 `HTTP 204` 即表示连接正常。 + +## 状态追踪 + +`agents/blueprint-absorber/STATUS.md` 记录哪些 hint 文件已吸收(Absorbed)、哪些待处理(Pending)。 +成功吸收后 agent 自动更新此文件。