* P5 HTTP 接口:本地服务,POST 端点供外部系统/Agent 调用
* - /convert 数据 → 笔记(run 等价)
* - /compile raw → wiki(LLM 编译)
* - /query 基于 wiki 问答(可选归档)
* - /session/* 会话记忆生命周期
* - /memory/* 记忆写入/检索/提炼/确认
* - /lint wiki 健康检查
* 仅监听 127.0.0.1(本地安全),可通过环境变量 WORK_VAULT_PORT 指定端口
*/
import * as http from "http";
import * as path from "path";
import type { Profile } from "./core/types";
import { renderSource } from "./core/engine";
import { FsWriter } from "./core/fsWriter";
import { applyNotes } from "./core/writer";
import { createProvider } from "./core/provider";
import type { LlmProvider } from "./core/provider";
import { compileRaw } from "./core/compiler";
import { queryWiki } from "./core/query";
import { writeClaims } from "./core/ledger";
import { lintWiki } from "./core/lint";
import {
writeMemory,
searchMemory,
listMemory,
captureMemory,
confirmPending,
listPendingMemory,
sessionStart,
sessionEnd,
} from "./core/memory";
export interface HttpServerOptions {
vault: string;
port?: number;
profileDir?: string;
}
export function startServer(opts: HttpServerOptions): http.Server {
const port = opts.port ?? Number(process.env.WORK_VAULT_PORT ?? 34567);
const server = http.createServer((req, res) => {
const send = (code: number, body: unknown) => {
const json = JSON.stringify(body);
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
res.end(json);
};
if (req.method !== "POST") {
send(405, { ok: false, error: "method not allowed" });
return;
}
const chunks: Buffer[] = [];
req.on("data", (c) => chunks.push(c as Buffer));
req.on("end", async () => {
try {
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
const pathname = (req.url ?? "/").split("?")[0];
if (pathname === "/compile") {
await handleCompile(body, send, opts.vault);
return;
}
if (pathname === "/query") {
await handleQuery(body, send, opts.vault);
return;
}
if (pathname === "/session/start") {
await handleSessionStart(body, send, opts.vault);
return;
}
if (pathname === "/session/end") {
await handleSessionEnd(body, send, opts.vault);
return;
}
if (pathname === "/memory/write") {
await handleMemoryWrite(body, send, opts.vault);
return;
}
if (pathname === "/memory/search") {
await handleMemorySearch(body, send, opts.vault);
return;
}
if (pathname === "/memory/list") {
await handleMemoryList(body, send, opts.vault);
return;
}
if (pathname === "/memory/capture") {
await handleMemoryCapture(body, send, opts.vault);
return;
}
if (pathname === "/memory/pending") {
await handleMemoryPending(body, send, opts.vault);
return;
}
if (pathname === "/lint") {
await handleLint(body, send, opts.vault);
return;
}
const data = body.data ?? "";
const profileRef = body.profile ?? "person";
const dryRun = body.dry_run === true;
let profile: Profile;
if (typeof profileRef === "object" && profileRef !== null) {
profile = profileRef as Profile;
} else {
const fs = await import("fs");
const dirs = opts.profileDir
? [opts.profileDir]
: [path.join(__dirname, "templates", "profiles"), path.join(__dirname, "examples")];
let found: string | undefined;
for (const d of dirs) {
const direct = `${d}/${profileRef}.json`;
if (fs.existsSync(direct)) { found = direct; break; }
if (fs.existsSync(d) && fs.statSync(d).isDirectory()) {
for (const pkg of fs.readdirSync(d)) {
const cand = path.join(d, pkg, "profiles", `${profileRef}.json`);
if (fs.existsSync(cand)) { found = cand; break; }
}
if (found) break;
}
}
if (!found) {
send(400, { ok: false, error: `profile 不存在: ${profileRef}` });
return;
}
profile = JSON.parse(fs.readFileSync(found, "utf-8")) as Profile;
}
const source = { kind: "json" as const, content: typeof data === "string" ? data : JSON.stringify(data) };
const { notes, issues, warnings } = renderSource(source, profile);
if (issues.length > 0) {
send(400, { ok: false, issues, warnings });
return;
}
const writer = new FsWriter(opts.vault);
const existing = await writer.listExistingPaths();
const result = await applyNotes(notes, writer, {
dryRun,
conflictStrategy: profile.conflictStrategy ?? "overwrite",
existingPaths: existing,
});
send(200, { ok: true, written: result.written, skipped: result.skipped, errors: result.errors, dryRun });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
});
});
server.listen(port, "127.0.0.1", () => {
console.log(`[workvault] HTTP 服务已启动: http://127.0.0.1:${port} (vault: ${opts.vault})`);
});
return server;
}
if (require.main === module) {
const args = process.argv.slice(2);
const vaultIdx = args.indexOf("--vault");
const portIdx = args.indexOf("--port");
const vault = vaultIdx >= 0 ? args[vaultIdx + 1] : undefined;
if (!vault) {
console.error("用法: node http.js --vault <dir> [--port <n>]");
process.exit(2);
}
startServer({
vault,
port: portIdx >= 0 ? Number(args[portIdx + 1]) : undefined,
});
}
async function handleQuery(
body: Record<string, unknown>,
send: (code: number, body: unknown) => void,
vault: string,
): Promise<void> {
try {
const question = String(body.question ?? "").trim();
if (!question) {
send(400, { ok: false, error: "需要 question" });
return;
}
let schema = body.schema ? String(body.schema) : "";
if (!schema) {
const fs = await import("fs");
const p = require("path").join(__dirname, "templates", "schemas", "query.AGENTS.md");
schema = fs.existsSync(p) ? fs.readFileSync(p, "utf-8") : "";
}
const provider = createProvider(String(body.provider ?? "echo"), {
apiBase: body.apiBase ? String(body.apiBase) : undefined,
apiKey: body.apiKey ? String(body.apiKey) : undefined,
model: body.model ? String(body.model) : undefined,
});
const result = await queryWiki({
provider,
vault,
question,
schema,
wikiDir: body.wikiDir ? String(body.wikiDir) : "wiki",
archive: body.archive === true,
dryRun: body.dry_run === true,
});
send(200, { ok: true, result });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleCompile(
body: Record<string, unknown>,
send: (code: number, body: unknown) => void,
vault: string,
): Promise<void> {
try {
const schema = String(body.schema ?? "");
const instruction = body.instruction ? String(body.instruction) : undefined;
const providerKind = String(body.provider ?? "echo");
const wikiDir = body.wikiDir ? String(body.wikiDir) : "wiki";
const dryRun = body.dry_run === true;
let inputs: Array<{ name: string; content: string }> = [];
if (Array.isArray(body.input)) {
inputs = (body.input as Array<Record<string, unknown>>).map((x) => ({
name: String(x.name ?? "input"),
content: String(x.content ?? ""),
}));
} else if (body.input && typeof body.input === "object" && "text" in (body.input as object)) {
inputs = [{ name: "input.txt", content: String((body.input as { text: unknown }).text) }];
}
if (!schema || inputs.length === 0) {
send(400, { ok: false, error: "需要 schema 与 input" });
return;
}
const provider = createProvider(providerKind, {
apiBase: body.apiBase ? String(body.apiBase) : undefined,
apiKey: body.apiKey ? String(body.apiKey) : undefined,
model: body.model ? String(body.model) : undefined,
});
const result = await compileRaw({
provider,
schema,
inputs,
instruction,
vault,
wikiDir,
dryRun,
});
if (!dryRun && result.claims.length > 0) {
writeClaims(vault, wikiDir, result.claims);
}
send(200, { ok: true, result });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
function httpProvider(body: Record<string, unknown>): LlmProvider {
return createProvider(String(body.provider ?? "echo"), {
apiBase: body.apiBase ? String(body.apiBase) : undefined,
apiKey: body.apiKey ? String(body.apiKey) : undefined,
model: body.model ? String(body.model) : undefined,
});
}
async function handleSessionStart(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const context = sessionStart(vault, {
project: body.project ? String(body.project) : undefined,
focus: body.focus ? String(body.focus) : undefined,
memoryDir: body.memory_dir ? String(body.memory_dir) : undefined,
});
send(200, { ok: true, context });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleSessionEnd(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const summary = String(body.summary ?? "").trim();
if (!summary) {
send(400, { ok: false, error: "需要 summary" });
return;
}
let provider: LlmProvider | undefined;
if (body.auto_capture === true) provider = httpProvider(body);
const result = await sessionEnd(vault, {
summary,
project: body.project ? String(body.project) : undefined,
agent: body.agent ? String(body.agent) : undefined,
memoryDir: body.memory_dir ? String(body.memory_dir) : undefined,
provider,
autoCapture: body.auto_capture === true,
autoCommit: body.auto_commit === true,
});
send(result.ok ? 200 : 500, result);
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleMemoryWrite(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const entry = (body.memory ?? body) as Record<string, unknown>;
const type = String(entry.type ?? "");
if (!type || !entry.title || !entry.description) {
send(400, { ok: false, error: "需要 type/title/description" });
return;
}
const r = writeMemory(
vault,
{
type: type as never,
title: String(entry.title),
description: String(entry.description),
content: String(entry.content ?? ""),
project: entry.project ? String(entry.project) : undefined,
agent: entry.agent ? String(entry.agent) : undefined,
source: entry.source ? String(entry.source) : undefined,
tags: Array.isArray(entry.tags) ? entry.tags.map(String) : undefined,
},
{ memoryDir: body.memory_dir ? String(body.memory_dir) : undefined, dryRun: body.dry_run === true },
);
send(r.ok ? 200 : 409, r);
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleMemorySearch(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const hits = searchMemory(
vault,
{
query: body.query ? String(body.query) : undefined,
type: body.type ? String(body.type) : undefined,
project: body.project ? String(body.project) : undefined,
limit: body.limit ? Number(body.limit) : undefined,
},
body.memory_dir ? String(body.memory_dir) : undefined,
);
send(200, { ok: true, hits });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleMemoryList(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const entries = listMemory(vault, { type: body.type ? String(body.type) : undefined, project: body.project ? String(body.project) : undefined }, body.memory_dir ? String(body.memory_dir) : undefined);
send(200, { ok: true, entries });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleMemoryCapture(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const text = String(body.text ?? "").trim();
if (!text) {
send(400, { ok: false, error: "需要 text" });
return;
}
const cap = await captureMemory(vault, {
provider: httpProvider(body),
text,
project: body.project ? String(body.project) : undefined,
agent: body.agent ? String(body.agent) : undefined,
memoryDir: body.memory_dir ? String(body.memory_dir) : undefined,
writePending: body.preview !== true,
});
send(200, { ok: !cap.error, ...cap });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleMemoryPending(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const memoryDir = body.memory_dir ? String(body.memory_dir) : undefined;
if (body.action === "confirm") {
const name = String(body.name ?? "");
if (!name) {
send(400, { ok: false, error: "confirm 需要 name" });
return;
}
const r = confirmPending(vault, name, { memoryDir });
send(r.ok ? 200 : 404, r);
return;
}
const pending = listPendingMemory(vault, memoryDir);
send(200, { ok: true, pending });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}
async function handleLint(body: Record<string, unknown>, send: (code: number, body: unknown) => void, vault: string): Promise<void> {
try {
const issues = lintWiki(vault, body.wiki_dir ? String(body.wiki_dir) : "wiki");
send(200, { ok: true, issues });
} catch (e) {
send(500, { ok: false, error: (e as Error).message });
}
}