psyclaw 0.24.0 → 0.25.2
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/README.md +46 -53
- package/{dist/apps → apps}/panel/index.html +28 -50
- package/package.json +10 -20
- package/scripts/__pycache__/build_dist.cpython-314.pyc +0 -0
- package/scripts/__pycache__/build_whitepaper_docx.cpython-314.pyc +0 -0
- package/scripts/build_preprint_docx.py +729 -0
- package/scripts/build_preprint_figures.py +238 -0
- package/scripts/build_three_line_table_fixture.py +56 -0
- package/scripts/build_whitepaper_docx.py +768 -0
- package/scripts/check-branding.mjs +159 -0
- package/scripts/copy-panel.mjs +11 -0
- package/scripts/rebrand-pi.mjs +292 -0
- package/scripts/verify_docx.py +41 -0
- package/skills/core/citation-audit/SKILL.md +9 -9
- package/skills/core/evidence-capture/SKILL.md +9 -9
- package/skills/core/manifest.json +15 -15
- package/skills/core/research-brief/SKILL.md +8 -8
- package/skills/core/research-intake/SKILL.md +9 -9
- package/dist/src/adapters/pi/extension.d.ts +0 -19
- package/dist/src/adapters/pi/extension.js +0 -735
- package/dist/src/adapters/pi/extension.js.map +0 -1
- package/dist/src/adapters/pi/model.d.ts +0 -72
- package/dist/src/adapters/pi/model.js +0 -152
- package/dist/src/adapters/pi/model.js.map +0 -1
- package/dist/src/adapters/pi/resources.d.ts +0 -23
- package/dist/src/adapters/pi/resources.js +0 -54
- package/dist/src/adapters/pi/resources.js.map +0 -1
- package/dist/src/adapters/pi/rpc.d.ts +0 -56
- package/dist/src/adapters/pi/rpc.js +0 -248
- package/dist/src/adapters/pi/rpc.js.map +0 -1
- package/dist/src/adapters/pi/session.d.ts +0 -30
- package/dist/src/adapters/pi/session.js +0 -64
- package/dist/src/adapters/pi/session.js.map +0 -1
- package/dist/src/agents/catalog.d.ts +0 -30
- package/dist/src/agents/catalog.js +0 -118
- package/dist/src/agents/catalog.js.map +0 -1
- package/dist/src/agents/discover.d.ts +0 -28
- package/dist/src/agents/discover.js +0 -127
- package/dist/src/agents/discover.js.map +0 -1
- package/dist/src/agents/import.d.ts +0 -67
- package/dist/src/agents/import.js +0 -801
- package/dist/src/agents/import.js.map +0 -1
- package/dist/src/analysis/hooks.d.ts +0 -81
- package/dist/src/analysis/hooks.js +0 -139
- package/dist/src/analysis/hooks.js.map +0 -1
- package/dist/src/analysis/pre-registration.d.ts +0 -18
- package/dist/src/analysis/pre-registration.js +0 -86
- package/dist/src/analysis/pre-registration.js.map +0 -1
- package/dist/src/branding.d.ts +0 -43
- package/dist/src/branding.js +0 -127
- package/dist/src/branding.js.map +0 -1
- package/dist/src/chat.d.ts +0 -16
- package/dist/src/chat.js +0 -67
- package/dist/src/chat.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.js +0 -316
- package/dist/src/cli.js.map +0 -1
- package/dist/src/core/citations.d.ts +0 -44
- package/dist/src/core/citations.js +0 -48
- package/dist/src/core/citations.js.map +0 -1
- package/dist/src/core/contracts.d.ts +0 -119
- package/dist/src/core/contracts.js +0 -2
- package/dist/src/core/contracts.js.map +0 -1
- package/dist/src/core/docx.d.ts +0 -11
- package/dist/src/core/docx.js +0 -46
- package/dist/src/core/docx.js.map +0 -1
- package/dist/src/core/doi.d.ts +0 -41
- package/dist/src/core/doi.js +0 -82
- package/dist/src/core/doi.js.map +0 -1
- package/dist/src/core/evidence-policy.d.ts +0 -24
- package/dist/src/core/evidence-policy.js +0 -308
- package/dist/src/core/evidence-policy.js.map +0 -1
- package/dist/src/core/filekind.d.ts +0 -15
- package/dist/src/core/filekind.js +0 -82
- package/dist/src/core/filekind.js.map +0 -1
- package/dist/src/core/hash.d.ts +0 -2
- package/dist/src/core/hash.js +0 -17
- package/dist/src/core/hash.js.map +0 -1
- package/dist/src/core/pricing.d.ts +0 -38
- package/dist/src/core/pricing.js +0 -110
- package/dist/src/core/pricing.js.map +0 -1
- package/dist/src/core/redact.d.ts +0 -12
- package/dist/src/core/redact.js +0 -48
- package/dist/src/core/redact.js.map +0 -1
- package/dist/src/core/references.d.ts +0 -55
- package/dist/src/core/references.js +0 -102
- package/dist/src/core/references.js.map +0 -1
- package/dist/src/core/schemas.d.ts +0 -162
- package/dist/src/core/schemas.js +0 -160
- package/dist/src/core/schemas.js.map +0 -1
- package/dist/src/extension.d.ts +0 -1
- package/dist/src/extension.js +0 -2
- package/dist/src/extension.js.map +0 -1
- package/dist/src/index.d.ts +0 -72
- package/dist/src/index.js +0 -73
- package/dist/src/index.js.map +0 -1
- package/dist/src/install/installer.d.ts +0 -147
- package/dist/src/install/installer.js +0 -743
- package/dist/src/install/installer.js.map +0 -1
- package/dist/src/integrations/contracts.d.ts +0 -35
- package/dist/src/integrations/contracts.js +0 -27
- package/dist/src/integrations/contracts.js.map +0 -1
- package/dist/src/integrations/mcp.d.ts +0 -155
- package/dist/src/integrations/mcp.js +0 -826
- package/dist/src/integrations/mcp.js.map +0 -1
- package/dist/src/knowledge-map/contracts.d.ts +0 -20
- package/dist/src/knowledge-map/contracts.js +0 -2
- package/dist/src/knowledge-map/contracts.js.map +0 -1
- package/dist/src/knowledge-map/index.d.ts +0 -4
- package/dist/src/knowledge-map/index.js +0 -39
- package/dist/src/knowledge-map/index.js.map +0 -1
- package/dist/src/literature/contracts.d.ts +0 -106
- package/dist/src/literature/contracts.js +0 -2
- package/dist/src/literature/contracts.js.map +0 -1
- package/dist/src/literature/fulltext.d.ts +0 -7
- package/dist/src/literature/fulltext.js +0 -24
- package/dist/src/literature/fulltext.js.map +0 -1
- package/dist/src/literature/knowledge-map.d.ts +0 -2
- package/dist/src/literature/knowledge-map.js +0 -20
- package/dist/src/literature/knowledge-map.js.map +0 -1
- package/dist/src/literature/ledger.d.ts +0 -3
- package/dist/src/literature/ledger.js +0 -17
- package/dist/src/literature/ledger.js.map +0 -1
- package/dist/src/literature/metadata.d.ts +0 -6
- package/dist/src/literature/metadata.js +0 -32
- package/dist/src/literature/metadata.js.map +0 -1
- package/dist/src/memory/store.d.ts +0 -22
- package/dist/src/memory/store.js +0 -73
- package/dist/src/memory/store.js.map +0 -1
- package/dist/src/orchestration/contracts.d.ts +0 -61
- package/dist/src/orchestration/contracts.js +0 -2
- package/dist/src/orchestration/contracts.js.map +0 -1
- package/dist/src/orchestration/pi-executor.d.ts +0 -28
- package/dist/src/orchestration/pi-executor.js +0 -169
- package/dist/src/orchestration/pi-executor.js.map +0 -1
- package/dist/src/orchestration/runner.d.ts +0 -196
- package/dist/src/orchestration/runner.js +0 -991
- package/dist/src/orchestration/runner.js.map +0 -1
- package/dist/src/orchestration/scheduler.d.ts +0 -8
- package/dist/src/orchestration/scheduler.js +0 -335
- package/dist/src/orchestration/scheduler.js.map +0 -1
- package/dist/src/panel/events.d.ts +0 -20
- package/dist/src/panel/events.js +0 -93
- package/dist/src/panel/events.js.map +0 -1
- package/dist/src/panel/extension.d.ts +0 -8
- package/dist/src/panel/extension.js +0 -114
- package/dist/src/panel/extension.js.map +0 -1
- package/dist/src/panel/literature-map.d.ts +0 -26
- package/dist/src/panel/literature-map.js +0 -34
- package/dist/src/panel/literature-map.js.map +0 -1
- package/dist/src/panel/projection.d.ts +0 -51
- package/dist/src/panel/projection.js +0 -288
- package/dist/src/panel/projection.js.map +0 -1
- package/dist/src/panel/server.d.ts +0 -46
- package/dist/src/panel/server.js +0 -1610
- package/dist/src/panel/server.js.map +0 -1
- package/dist/src/panel/usage.d.ts +0 -53
- package/dist/src/panel/usage.js +0 -150
- package/dist/src/panel/usage.js.map +0 -1
- package/dist/src/project/bootstrap.d.ts +0 -10
- package/dist/src/project/bootstrap.js +0 -83
- package/dist/src/project/bootstrap.js.map +0 -1
- package/dist/src/project/hitl.d.ts +0 -12
- package/dist/src/project/hitl.js +0 -43
- package/dist/src/project/hitl.js.map +0 -1
- package/dist/src/project/jsonl.d.ts +0 -16
- package/dist/src/project/jsonl.js +0 -93
- package/dist/src/project/jsonl.js.map +0 -1
- package/dist/src/project/manuscript.d.ts +0 -21
- package/dist/src/project/manuscript.js +0 -74
- package/dist/src/project/manuscript.js.map +0 -1
- package/dist/src/project/paths.d.ts +0 -38
- package/dist/src/project/paths.js +0 -166
- package/dist/src/project/paths.js.map +0 -1
- package/dist/src/project/versions.d.ts +0 -10
- package/dist/src/project/versions.js +0 -14
- package/dist/src/project/versions.js.map +0 -1
- package/dist/src/psyclaw-theme.d.ts +0 -88
- package/dist/src/psyclaw-theme.js +0 -89
- package/dist/src/psyclaw-theme.js.map +0 -1
- package/dist/src/research/brief.d.ts +0 -11
- package/dist/src/research/brief.js +0 -81
- package/dist/src/research/brief.js.map +0 -1
- package/dist/src/research/ledger.d.ts +0 -12
- package/dist/src/research/ledger.js +0 -43
- package/dist/src/research/ledger.js.map +0 -1
- package/dist/src/run.d.ts +0 -20
- package/dist/src/run.js +0 -34
- package/dist/src/run.js.map +0 -1
- package/dist/src/setup.d.ts +0 -54
- package/dist/src/setup.js +0 -221
- package/dist/src/setup.js.map +0 -1
- package/dist/src/skills/contracts.d.ts +0 -105
- package/dist/src/skills/contracts.js +0 -3
- package/dist/src/skills/contracts.js.map +0 -1
- package/dist/src/skills/preflight.d.ts +0 -10
- package/dist/src/skills/preflight.js +0 -39
- package/dist/src/skills/preflight.js.map +0 -1
- package/dist/src/skills/registry.d.ts +0 -28
- package/dist/src/skills/registry.js +0 -556
- package/dist/src/skills/registry.js.map +0 -1
- package/dist/src/style/artifact.d.ts +0 -6
- package/dist/src/style/artifact.js +0 -61
- package/dist/src/style/artifact.js.map +0 -1
- package/dist/src/style/cli-ui.d.ts +0 -41
- package/dist/src/style/cli-ui.js +0 -119
- package/dist/src/style/cli-ui.js.map +0 -1
- package/dist/src/style/contracts.d.ts +0 -34
- package/dist/src/style/contracts.js +0 -2
- package/dist/src/style/contracts.js.map +0 -1
- package/dist/src/style/registry.d.ts +0 -16
- package/dist/src/style/registry.js +0 -50
- package/dist/src/style/registry.js.map +0 -1
- package/dist/src/trust/manifest.d.ts +0 -42
- package/dist/src/trust/manifest.js +0 -53
- package/dist/src/trust/manifest.js.map +0 -1
- package/dist/src/trust/sbom.d.ts +0 -54
- package/dist/src/trust/sbom.js +0 -77
- package/dist/src/trust/sbom.js.map +0 -1
- package/dist/src/tui/app.d.ts +0 -3
- package/dist/src/tui/app.js +0 -103
- package/dist/src/tui/app.js.map +0 -1
- package/dist/src/tui/model.d.ts +0 -13
- package/dist/src/tui/model.js +0 -25
- package/dist/src/tui/model.js.map +0 -1
- package/dist/src/updates/check.d.ts +0 -43
- package/dist/src/updates/check.js +0 -68
- package/dist/src/updates/check.js.map +0 -1
- package/dist/src/updates/manifest.d.ts +0 -19
- package/dist/src/updates/manifest.js +0 -58
- package/dist/src/updates/manifest.js.map +0 -1
- package/dist/src/updates/registry.d.ts +0 -21
- package/dist/src/updates/registry.js +0 -82
- package/dist/src/updates/registry.js.map +0 -1
- package/dist/src/updates/skills.d.ts +0 -15
- package/dist/src/updates/skills.js +0 -39
- package/dist/src/updates/skills.js.map +0 -1
- package/dist/src/updates/status.d.ts +0 -7
- package/dist/src/updates/status.js +0 -37
- package/dist/src/updates/status.js.map +0 -1
- package/dist/src/updates/update.d.ts +0 -43
- package/dist/src/updates/update.js +0 -126
- package/dist/src/updates/update.js.map +0 -1
- package/dist/src/wizard.d.ts +0 -7
- package/dist/src/wizard.js +0 -90
- package/dist/src/wizard.js.map +0 -1
- package/dist/src/workflows/analysis-delegation.d.ts +0 -3
- package/dist/src/workflows/analysis-delegation.js +0 -96
- package/dist/src/workflows/analysis-delegation.js.map +0 -1
- package/dist/src/workflows/expert-review-runner.d.ts +0 -29
- package/dist/src/workflows/expert-review-runner.js +0 -35
- package/dist/src/workflows/expert-review-runner.js.map +0 -1
- package/dist/src/workflows/expert-review.d.ts +0 -140
- package/dist/src/workflows/expert-review.js +0 -111
- package/dist/src/workflows/expert-review.js.map +0 -1
- package/dist/src/workflows/institutional-fulltext.d.ts +0 -16
- package/dist/src/workflows/institutional-fulltext.js +0 -110
- package/dist/src/workflows/institutional-fulltext.js.map +0 -1
- package/dist/src/workflows/literature-review.d.ts +0 -3
- package/dist/src/workflows/literature-review.js +0 -76
- package/dist/src/workflows/literature-review.js.map +0 -1
- package/dist/src/workflows/meta-analysis.d.ts +0 -38
- package/dist/src/workflows/meta-analysis.js +0 -185
- package/dist/src/workflows/meta-analysis.js.map +0 -1
- package/dist/src/workflows/publish.d.ts +0 -51
- package/dist/src/workflows/publish.js +0 -153
- package/dist/src/workflows/publish.js.map +0 -1
- package/dist/src/workflows/spec.d.ts +0 -51
- package/dist/src/workflows/spec.js +0 -157
- package/dist/src/workflows/spec.js.map +0 -1
- package/dist/src/workflows/style-contract.d.ts +0 -24
- package/dist/src/workflows/style-contract.js +0 -23
- package/dist/src/workflows/style-contract.js.map +0 -1
- package/dist/src/workflows/writing-review.d.ts +0 -15
- package/dist/src/workflows/writing-review.js +0 -82
- package/dist/src/workflows/writing-review.js.map +0 -1
- package/package.json.psyclaw +0 -21
package/dist/src/panel/server.js
DELETED
|
@@ -1,1610 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { lstat, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { listRuns, projectRunSnapshot } from "./projection.js";
|
|
9
|
-
import { readSessionUsage } from "./usage.js";
|
|
10
|
-
import { buildClaimLiteratureMap } from "./literature-map.js";
|
|
11
|
-
import { discoverAgents } from "../agents/discover.js";
|
|
12
|
-
import { KNOWN_AGENTS } from "../agents/catalog.js";
|
|
13
|
-
import { planAgentInstall } from "../install/installer.js";
|
|
14
|
-
import { deepSeekProviderSpec, PiModelGateway } from "../adapters/pi/model.js";
|
|
15
|
-
import { PROVIDER_PRESETS, saveProviderConfig } from "../setup.js";
|
|
16
|
-
import { readHitlWorkspace } from "../project/hitl.js";
|
|
17
|
-
import { assertSafeProjectPath, projectPaths } from "../project/paths.js";
|
|
18
|
-
import { readManuscript } from "../project/manuscript.js";
|
|
19
|
-
import { appendJsonlIfMissing, atomicWriteFile, readJsonl } from "../project/jsonl.js";
|
|
20
|
-
import { appendClaim, appendClaimEvidenceLink, appendEvidence, loadLedger } from "../research/ledger.js";
|
|
21
|
-
import { publishManuscript } from "../workflows/publish.js";
|
|
22
|
-
import { sha256File, sha256Text } from "../core/hash.js";
|
|
23
|
-
import { docxToMarkdown } from "../core/docx.js";
|
|
24
|
-
import { verifyDoi, lookupOaPdfUrl } from "../core/doi.js";
|
|
25
|
-
import { checkCitations, listReferences, referenceFromVerification, upsertReference } from "../core/references.js";
|
|
26
|
-
import { listCitationUses, recordCitationUse } from "../core/citations.js";
|
|
27
|
-
import { readPublishedVersions } from "../workflows/publish.js";
|
|
28
|
-
// Backward-compatible re-export: existing tests import verifyDoi from the panel server.
|
|
29
|
-
export { verifyDoi } from "../core/doi.js";
|
|
30
|
-
import { resumePlanWithPi } from "../orchestration/pi-executor.js";
|
|
31
|
-
import { RunEventLog } from "./events.js";
|
|
32
|
-
import { PSYCLAW_IDENTITY_PROMPT } from "../branding.js";
|
|
33
|
-
const activePanelRuns = new Set();
|
|
34
|
-
async function readRecommendationState(root) {
|
|
35
|
-
try {
|
|
36
|
-
const value = JSON.parse(await readFile(join(root, ".psyclaw", "recommendations.json"), "utf8"));
|
|
37
|
-
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: Array.isArray(value.skills) ? value.skills.filter((id) => typeof id === "string") : [], mcp: Array.isArray(value.mcp) ? value.mcp.filter((id) => typeof id === "string") : [] };
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return { schemaVersion: "psyclaw/recommendation-state/v1", skills: [], mcp: [] };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function writeRecommendationState(root, state) {
|
|
44
|
-
await atomicWriteFile(await assertSafeProjectPath(root, ".psyclaw/recommendations.json"), `${JSON.stringify({ ...state, skills: [...new Set(state.skills)].sort(), mcp: [...new Set(state.mcp)].sort() }, null, 2)}\n`);
|
|
45
|
-
}
|
|
46
|
-
/** The bundled core skills, always listed so the user can disable (not uninstall) them. */
|
|
47
|
-
const CORE_SKILLS = [
|
|
48
|
-
{ id: "research-intake", name: "研究入口" },
|
|
49
|
-
{ id: "evidence-capture", name: "证据登记" },
|
|
50
|
-
{ id: "citation-audit", name: "引用审计" },
|
|
51
|
-
{ id: "research-brief", name: "研究简报" },
|
|
52
|
-
];
|
|
53
|
-
async function readDisabledCapabilities(root) {
|
|
54
|
-
try {
|
|
55
|
-
const value = JSON.parse(await readFile(join(root, ".psyclaw", "disabled-capabilities.json"), "utf8"));
|
|
56
|
-
return { coreSkills: Array.isArray(value.coreSkills) ? value.coreSkills.filter((id) => typeof id === "string") : [] };
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return { coreSkills: [] };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async function writeDisabledCapabilities(root, state) {
|
|
63
|
-
await atomicWriteFile(await assertSafeProjectPath(root, ".psyclaw/disabled-capabilities.json"), `${JSON.stringify({ ...state, coreSkills: [...new Set(state.coreSkills)].sort() }, null, 2)}\n`);
|
|
64
|
-
}
|
|
65
|
-
/** Persist a manuscript to a real project file under notes/ or outputs/ with a receipt. */
|
|
66
|
-
async function writeManuscript(root, relative, content) {
|
|
67
|
-
const rel = relative.replaceAll("\\", "/");
|
|
68
|
-
if (rel.split("/").some((part) => part === ".." || part === ""))
|
|
69
|
-
throw new Error("a relative manuscript path is required");
|
|
70
|
-
if (!/\.(md|markdown|doc|docx|txt)$/i.test(rel))
|
|
71
|
-
throw new Error("manuscript must be .md/.markdown/.doc/.docx/.txt");
|
|
72
|
-
if (!rel.startsWith("notes/") && !rel.startsWith("outputs/"))
|
|
73
|
-
throw new Error("manuscript must be under notes/ or outputs/");
|
|
74
|
-
const target = await assertSafeProjectPath(root, rel);
|
|
75
|
-
const stat = await lstat(target).catch(() => undefined);
|
|
76
|
-
if (stat !== undefined && (stat.isSymbolicLink() || !stat.isFile()))
|
|
77
|
-
throw new Error("manuscript must be a regular file");
|
|
78
|
-
await atomicWriteFile(target, content.endsWith("\n") ? content : `${content}\n`);
|
|
79
|
-
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
80
|
-
const receipt = {
|
|
81
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
82
|
-
runId,
|
|
83
|
-
taskId: "panel.manuscript.save",
|
|
84
|
-
tool: "panel.manuscript.save",
|
|
85
|
-
effect: "write",
|
|
86
|
-
approval: "approved",
|
|
87
|
-
idempotencyKey: `panel:manuscript:${sha256Text(`${rel}\u0000${content}`).slice(0, 24)}`,
|
|
88
|
-
ok: true,
|
|
89
|
-
startedAt: new Date().toISOString(),
|
|
90
|
-
finishedAt: new Date().toISOString(),
|
|
91
|
-
};
|
|
92
|
-
const receiptPath = await assertSafeProjectPath(root, `.psyclaw/manifests/${runId}.receipt.json`);
|
|
93
|
-
await atomicWriteFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
94
|
-
return { path: rel, bytes: Buffer.byteLength(content, "utf8"), receiptPath };
|
|
95
|
-
}
|
|
96
|
-
const CLAIM_KINDS = new Set(["existence", "definition", "method", "result", "interpretation"]);
|
|
97
|
-
const CLAIM_STATUSES = new Set(["supported", "uncertain", "unsupported"]);
|
|
98
|
-
/** Append a claim to the real `.psyclaw/claims.jsonl` ledger (append-only). */
|
|
99
|
-
async function appendLedgerClaim(root, body) {
|
|
100
|
-
const text = String(body.text ?? "").trim();
|
|
101
|
-
const kind = String(body.kind ?? "result");
|
|
102
|
-
const status = String(body.status ?? "uncertain");
|
|
103
|
-
const uncertainty = body.uncertainty === undefined ? undefined : String(body.uncertainty).trim();
|
|
104
|
-
if (!text)
|
|
105
|
-
throw new Error("claim text is required");
|
|
106
|
-
if (!CLAIM_KINDS.has(kind))
|
|
107
|
-
throw new Error(`unsupported claim kind: ${kind}`);
|
|
108
|
-
if (!CLAIM_STATUSES.has(status))
|
|
109
|
-
throw new Error(`unsupported claim status: ${status}`);
|
|
110
|
-
const claim = {
|
|
111
|
-
id: `claim_${sha256Text(text).slice(0, 16)}`,
|
|
112
|
-
text,
|
|
113
|
-
kind,
|
|
114
|
-
evidenceIds: [],
|
|
115
|
-
status,
|
|
116
|
-
...(uncertainty === undefined || uncertainty === "" ? {} : { uncertainty }),
|
|
117
|
-
};
|
|
118
|
-
await appendClaim(root, claim);
|
|
119
|
-
return claim;
|
|
120
|
-
}
|
|
121
|
-
/** Append evidence to the real `.psyclaw/evidence.jsonl` and link it to a claim. */
|
|
122
|
-
async function appendLedgerEvidence(root, body) {
|
|
123
|
-
const claimId = String(body.claimId ?? "").trim();
|
|
124
|
-
const title = String(body.title ?? "").trim();
|
|
125
|
-
const doi = String(body.doi ?? "").trim();
|
|
126
|
-
const quote = String(body.quote ?? "").trim();
|
|
127
|
-
if (!claimId)
|
|
128
|
-
throw new Error("claimId is required to attach evidence");
|
|
129
|
-
if (!title && !doi && !quote)
|
|
130
|
-
throw new Error("at least one of title / doi / quote is required");
|
|
131
|
-
const locator = doi || title || quote.slice(0, 80);
|
|
132
|
-
const id = `evidence_${sha256Text(`${locator}\u0000${quote}`).slice(0, 16)}`;
|
|
133
|
-
const evidence = {
|
|
134
|
-
id,
|
|
135
|
-
source: { kind: doi ? "doi" : "url", locator, ...(title === "" ? {} : { title }) },
|
|
136
|
-
level: "snippet",
|
|
137
|
-
...(quote === "" ? {} : { quote }),
|
|
138
|
-
retrievedAt: new Date().toISOString(),
|
|
139
|
-
// The panel records provenance, never independent verification: the
|
|
140
|
-
// accessStatus stays "partial" until a separate audit approves it.
|
|
141
|
-
accessStatus: "partial",
|
|
142
|
-
locators: doi ? [{ kind: "doi", value: doi }] : [],
|
|
143
|
-
};
|
|
144
|
-
await appendEvidence(root, evidence);
|
|
145
|
-
await appendClaimEvidenceLink(root, { claimId, evidenceId: evidence.id, relation: "supports", rationale: "panel evidence auditor binding" });
|
|
146
|
-
return { evidence, link: { claimId, evidenceId: evidence.id, relation: "supports" } };
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Standardized document locations (see docs/文档规范.md):
|
|
150
|
-
* paper/ → manuscripts (.md editable source, .docx/.pdf exports)
|
|
151
|
-
* docs/ → supplementary documentation
|
|
152
|
-
* data/ → raw + clean datasets
|
|
153
|
-
* outputs/→ workflow artifacts / reports
|
|
154
|
-
* Any root-level document (questionnaire exports etc.) is listed as well.
|
|
155
|
-
*/
|
|
156
|
-
const DOCUMENT_EXTENSIONS = new Set(["md", "markdown", "docx", "doc", "pdf", "xlsx", "xls", "csv", "txt"]);
|
|
157
|
-
const DOCUMENT_SKIP_DIRS = [".psyclaw", ".git", "node_modules", "analysis/scripts", "analysis/results", "analysis/configs", "artifacts", "logs", "paper/archive"];
|
|
158
|
-
/** Classify a document by its location and extension (shared by scan + import). */
|
|
159
|
-
function classifyDocumentKind(relative, extension) {
|
|
160
|
-
return relative.startsWith("paper/") || relative.startsWith("docs/")
|
|
161
|
-
? (extension === "md" || extension === "markdown" || extension === "docx" || extension === "doc" ? "manuscript" : "reference")
|
|
162
|
-
: relative.startsWith("data/")
|
|
163
|
-
? "data"
|
|
164
|
-
: relative.startsWith("outputs/")
|
|
165
|
-
? "report"
|
|
166
|
-
: "document";
|
|
167
|
-
}
|
|
168
|
-
async function scanDocuments(root) {
|
|
169
|
-
const found = [];
|
|
170
|
-
const walk = async (directory, prefix, depth) => {
|
|
171
|
-
if (depth > 4)
|
|
172
|
-
return;
|
|
173
|
-
let entries = [];
|
|
174
|
-
try {
|
|
175
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
for (const entry of entries) {
|
|
181
|
-
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
182
|
-
const normalized = relative.replaceAll("\\", "/");
|
|
183
|
-
if (DOCUMENT_SKIP_DIRS.some((blocked) => normalized === blocked || normalized.startsWith(`${blocked}/`)))
|
|
184
|
-
continue;
|
|
185
|
-
if (/credential|secret|\.env|session-[^/]*\.html/i.test(entry.name))
|
|
186
|
-
continue;
|
|
187
|
-
const absolute = join(directory, entry.name);
|
|
188
|
-
if (entry.isDirectory()) {
|
|
189
|
-
await walk(absolute, normalized, depth + 1);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
193
|
-
if (!DOCUMENT_EXTENSIONS.has(extension))
|
|
194
|
-
continue;
|
|
195
|
-
const stat = await lstat(absolute).catch(() => undefined);
|
|
196
|
-
if (!stat?.isFile())
|
|
197
|
-
continue;
|
|
198
|
-
found.push({ path: normalized, format: extension, kind: classifyDocumentKind(normalized, extension), bytes: stat.size });
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
await walk(root, "", 0);
|
|
202
|
-
return found.sort((left, right) => left.path.localeCompare(right.path));
|
|
203
|
-
}
|
|
204
|
-
async function importsPath(root) {
|
|
205
|
-
return assertSafeProjectPath(root, ".psyclaw/imports.jsonl");
|
|
206
|
-
}
|
|
207
|
-
/** Import a document: convert docx→md, register evidence, dedupe by sha256. */
|
|
208
|
-
async function importDocument(root, relative) {
|
|
209
|
-
const rel = relative.replaceAll("\\", "/");
|
|
210
|
-
if (rel.split("/").some((part) => part === ".." || part === ""))
|
|
211
|
-
throw new Error("a relative document path is required");
|
|
212
|
-
const target = await assertSafeProjectPath(root, rel);
|
|
213
|
-
const stat = await lstat(target);
|
|
214
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
215
|
-
throw new Error("document must be a regular file");
|
|
216
|
-
const extension = rel.split(".").pop()?.toLowerCase() ?? "";
|
|
217
|
-
if (!DOCUMENT_EXTENSIONS.has(extension))
|
|
218
|
-
throw new Error(`unsupported document format: ${extension}`);
|
|
219
|
-
const bytes = stat.size;
|
|
220
|
-
const sha256 = await sha256File(target);
|
|
221
|
-
const existing = (await readJsonl(await importsPath(root))).find((record) => record.sha256 === sha256);
|
|
222
|
-
if (existing)
|
|
223
|
-
return { record: existing, markdown: null };
|
|
224
|
-
let markdown = null;
|
|
225
|
-
if (extension === "md" || extension === "markdown") {
|
|
226
|
-
markdown = await readFile(target, "utf8");
|
|
227
|
-
}
|
|
228
|
-
else if (extension === "docx") {
|
|
229
|
-
markdown = await docxToMarkdown(target);
|
|
230
|
-
}
|
|
231
|
-
const kind = classifyDocumentKind(rel, extension);
|
|
232
|
-
const evidenceId = `evidence_${sha256.slice(0, 16)}`;
|
|
233
|
-
const evidence = {
|
|
234
|
-
id: evidenceId,
|
|
235
|
-
source: { kind: "file", locator: target, title: rel.split("/").pop() ?? rel },
|
|
236
|
-
level: kind === "data" ? "user" : markdown !== null ? "fulltext" : "metadata",
|
|
237
|
-
retrievedAt: new Date().toISOString(),
|
|
238
|
-
sha256,
|
|
239
|
-
accessStatus: "partial",
|
|
240
|
-
locators: [{ kind: "file", value: target }],
|
|
241
|
-
};
|
|
242
|
-
// Idempotent by evidence id so re-importing (or importing a file that was
|
|
243
|
-
// published through /api/publish) never duplicates ledger entries.
|
|
244
|
-
await appendJsonlIfMissing(projectPaths(root).evidence, evidence, (item) => item.id);
|
|
245
|
-
const record = {
|
|
246
|
-
schemaVersion: "psyclaw/import/v1",
|
|
247
|
-
path: rel,
|
|
248
|
-
format: extension,
|
|
249
|
-
kind,
|
|
250
|
-
bytes,
|
|
251
|
-
sha256,
|
|
252
|
-
converted: markdown !== null,
|
|
253
|
-
markdownLength: markdown?.length ?? 0,
|
|
254
|
-
evidenceId,
|
|
255
|
-
importedAt: new Date().toISOString(),
|
|
256
|
-
};
|
|
257
|
-
await appendJsonlIfMissing(await importsPath(root), record, (item) => item.sha256);
|
|
258
|
-
return { record, markdown };
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Download the open-access PDF for a DOI into literature/<slug>.pdf. Only
|
|
262
|
-
* https/http URLs returned by OpenAlex are accepted; size is capped at 40 MB.
|
|
263
|
-
*/
|
|
264
|
-
export async function downloadReferencePdf(root, doi, fetchFn = fetch) {
|
|
265
|
-
const lookup = await lookupOaPdfUrl(doi, fetchFn);
|
|
266
|
-
if (!lookup.isOa || !lookup.oaPdfUrl)
|
|
267
|
-
return { ok: false, reason: "not-open-access", doi };
|
|
268
|
-
let url;
|
|
269
|
-
try {
|
|
270
|
-
url = new URL(lookup.oaPdfUrl);
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
return { ok: false, reason: "invalid-oa-url", doi };
|
|
274
|
-
}
|
|
275
|
-
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
276
|
-
return { ok: false, reason: "unsafe-url", doi };
|
|
277
|
-
const response = await fetchFn(url.toString(), { signal: AbortSignal.timeout(30_000) });
|
|
278
|
-
if (!response.ok)
|
|
279
|
-
return { ok: false, reason: `download-failed-${response.status}`, doi };
|
|
280
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
281
|
-
if (buffer.length === 0 || buffer.length > 40 * 1024 * 1024)
|
|
282
|
-
return { ok: false, reason: "size-rejected", doi };
|
|
283
|
-
const slug = doi.replace(/[^A-Za-z0-9._-]+/g, "_").slice(0, 80);
|
|
284
|
-
const rel = `literature/${slug}.pdf`;
|
|
285
|
-
const target = await assertSafeProjectPath(root, rel);
|
|
286
|
-
await mkdir(dirname(target), { recursive: true });
|
|
287
|
-
await writeFile(target, buffer);
|
|
288
|
-
const sha256 = await sha256File(target);
|
|
289
|
-
const evidenceId = `evidence_${sha256.slice(0, 16)}`;
|
|
290
|
-
await appendJsonlIfMissing(projectPaths(root).evidence, {
|
|
291
|
-
id: evidenceId,
|
|
292
|
-
source: { kind: "url", locator: url.toString(), title: slug },
|
|
293
|
-
level: "fulltext",
|
|
294
|
-
retrievedAt: new Date().toISOString(),
|
|
295
|
-
sha256,
|
|
296
|
-
accessStatus: "partial",
|
|
297
|
-
locators: [{ kind: "url", value: url.toString() }, { kind: "file", value: target }],
|
|
298
|
-
}, (item) => item.id);
|
|
299
|
-
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
300
|
-
const receipt = {
|
|
301
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
302
|
-
runId,
|
|
303
|
-
taskId: "panel.reference.download",
|
|
304
|
-
tool: "panel.reference.download",
|
|
305
|
-
effect: "write",
|
|
306
|
-
approval: "approved",
|
|
307
|
-
idempotencyKey: `panel:download:${sha256.slice(0, 24)}`,
|
|
308
|
-
ok: true,
|
|
309
|
-
startedAt: new Date().toISOString(),
|
|
310
|
-
finishedAt: new Date().toISOString(),
|
|
311
|
-
};
|
|
312
|
-
await atomicWriteFile(await assertSafeProjectPath(root, `.psyclaw/manifests/${runId}.receipt.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
313
|
-
return { ok: true, path: rel, bytes: buffer.length, oaUrl: url.toString(), evidenceId };
|
|
314
|
-
}
|
|
315
|
-
async function listProjectFiles(root) {
|
|
316
|
-
const result = [];
|
|
317
|
-
const walk = async (directory, prefix) => {
|
|
318
|
-
if (result.length >= 500)
|
|
319
|
-
return;
|
|
320
|
-
let entries = [];
|
|
321
|
-
try {
|
|
322
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
for (const entry of entries) {
|
|
328
|
-
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
329
|
-
const normalized = relative.replaceAll("\\", "/");
|
|
330
|
-
if ([".git", "node_modules", "data/raw"].some((blocked) => normalized === blocked || normalized.startsWith(`${blocked}/`)))
|
|
331
|
-
continue;
|
|
332
|
-
if (/credential|secret|auth\.json|\.env/i.test(entry.name))
|
|
333
|
-
continue;
|
|
334
|
-
const absolute = join(directory, entry.name);
|
|
335
|
-
if (entry.isDirectory())
|
|
336
|
-
await walk(absolute, normalized);
|
|
337
|
-
else if (entry.isFile())
|
|
338
|
-
result.push(normalized);
|
|
339
|
-
if (result.length >= 500)
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
await walk(root, "");
|
|
344
|
-
return result.sort();
|
|
345
|
-
}
|
|
346
|
-
async function panelHistory(root) {
|
|
347
|
-
const runs = await listRuns(root);
|
|
348
|
-
const files = (await listProjectFiles(root)).filter((path) => path.startsWith("psyclaw-session-") || path.startsWith("notes/") || path.startsWith("outputs/") || path.startsWith(".psyclaw/runs/"));
|
|
349
|
-
return { schemaVersion: "psyclaw/panel-history/v1", runs, files };
|
|
350
|
-
}
|
|
351
|
-
/** Infer an artifact format from its file extension. */
|
|
352
|
-
function inferArtifactFormat(path) {
|
|
353
|
-
const lower = path.toLowerCase();
|
|
354
|
-
if (lower.endsWith(".svg"))
|
|
355
|
-
return "svg";
|
|
356
|
-
if (lower.endsWith(".png"))
|
|
357
|
-
return "png";
|
|
358
|
-
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))
|
|
359
|
-
return "jpg";
|
|
360
|
-
if (lower.endsWith(".gif"))
|
|
361
|
-
return "gif";
|
|
362
|
-
if (lower.endsWith(".pdf"))
|
|
363
|
-
return "pdf";
|
|
364
|
-
if (lower.endsWith(".csv"))
|
|
365
|
-
return "csv";
|
|
366
|
-
if (lower.endsWith(".json"))
|
|
367
|
-
return "json";
|
|
368
|
-
if (lower.endsWith(".md") || lower.endsWith(".markdown"))
|
|
369
|
-
return "markdown";
|
|
370
|
-
return "file";
|
|
371
|
-
}
|
|
372
|
-
/** Artifact-shaped entries for figure/table files dropped directly in outputs/. */
|
|
373
|
-
async function figureTableArtifacts(root) {
|
|
374
|
-
const files = await listProjectFiles(root);
|
|
375
|
-
return files
|
|
376
|
-
.filter((path) => /^(outputs|analysis\/outputs)\/(figures|tables)\//.test(path) && /\.(svg|png|jpe?g|gif|pdf|csv|json|md)$/i.test(path))
|
|
377
|
-
.map((path) => ({ path, format: inferArtifactFormat(path), status: "generated" }));
|
|
378
|
-
}
|
|
379
|
-
async function panelStats(root) {
|
|
380
|
-
const paths = projectPaths(root);
|
|
381
|
-
const countLines = async (path) => { try {
|
|
382
|
-
return (await readFile(path, "utf8")).split(/\r?\n/).filter(Boolean).length;
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
return 0;
|
|
386
|
-
} };
|
|
387
|
-
const files = await listProjectFiles(root);
|
|
388
|
-
return { schemaVersion: "psyclaw/panel-stats/v1", runs: (await listRuns(root)).length, evidence: await countLines(paths.evidence), claims: await countLines(paths.claims), auditEvents: await countLines(paths.audit), trackedFiles: files.length, outputs: files.filter((path) => path.startsWith("outputs/")).length, generatedAt: new Date().toISOString() };
|
|
389
|
-
}
|
|
390
|
-
function publicInstallPlan(plan) {
|
|
391
|
-
// `projectRoot` is an internal containment anchor. It is not needed by a
|
|
392
|
-
// read-only browser and would disclose a local filesystem path.
|
|
393
|
-
const { projectRoot: _projectRoot, ...publicFields } = plan;
|
|
394
|
-
return publicFields;
|
|
395
|
-
}
|
|
396
|
-
/** Metadata-only catalog for the optional panel. No install or credential read. */
|
|
397
|
-
async function panelCatalog(root) {
|
|
398
|
-
const scans = await discoverAgents();
|
|
399
|
-
const byId = new Map(scans.map((scan) => [scan.id, scan]));
|
|
400
|
-
const agents = KNOWN_AGENTS.map((agent) => {
|
|
401
|
-
const scan = byId.get(agent.id);
|
|
402
|
-
const install = agent.install;
|
|
403
|
-
const plan = planAgentInstall(agent, { projectRoot: root });
|
|
404
|
-
return {
|
|
405
|
-
id: agent.id,
|
|
406
|
-
kind: "agent",
|
|
407
|
-
name: agent.name,
|
|
408
|
-
source: install?.sourceRef ?? "builtin",
|
|
409
|
-
ref: install?.ref ?? "unknown",
|
|
410
|
-
version: install?.ref ?? "unknown",
|
|
411
|
-
license: "unknown",
|
|
412
|
-
sha256: null,
|
|
413
|
-
risk: install?.installCommand ? "high" : "unknown",
|
|
414
|
-
configured: scan?.found ?? false,
|
|
415
|
-
skills: (scan?.skills ?? []).map((skill) => ({
|
|
416
|
-
id: `${agent.id}:${skill.name}`,
|
|
417
|
-
kind: "skill",
|
|
418
|
-
name: skill.name,
|
|
419
|
-
source: agent.id,
|
|
420
|
-
ref: "local",
|
|
421
|
-
version: "unknown",
|
|
422
|
-
license: "unknown",
|
|
423
|
-
sha256: null,
|
|
424
|
-
risk: "unknown",
|
|
425
|
-
configured: true,
|
|
426
|
-
})),
|
|
427
|
-
installPlan: {
|
|
428
|
-
id: plan.id,
|
|
429
|
-
effect: plan.effect,
|
|
430
|
-
approval: "required",
|
|
431
|
-
status: "plan-only",
|
|
432
|
-
command: plan.command,
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
});
|
|
436
|
-
const spec = deepSeekProviderSpec();
|
|
437
|
-
const envForProvider = (provider) => ({
|
|
438
|
-
deepseek: "DEEPSEEK_API_KEY",
|
|
439
|
-
openai: "OPENAI_API_KEY",
|
|
440
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
441
|
-
google: "GEMINI_API_KEY",
|
|
442
|
-
}[provider] ?? PROVIDER_PRESETS.find((preset) => preset.id === provider)?.apiKeyEnv);
|
|
443
|
-
const configured = (provider) => {
|
|
444
|
-
const envName = envForProvider(provider);
|
|
445
|
-
return envName === undefined ? false : Boolean(process.env[envName]);
|
|
446
|
-
};
|
|
447
|
-
const helperModels = spec.models.map((model) => ({
|
|
448
|
-
id: `${spec.id}/${model.id}`,
|
|
449
|
-
kind: "model",
|
|
450
|
-
name: model.name ?? model.id,
|
|
451
|
-
provider: spec.id,
|
|
452
|
-
source: "builtin:psyclaw",
|
|
453
|
-
ref: "deepseek-provider-v1",
|
|
454
|
-
version: model.id,
|
|
455
|
-
license: "provider-terms",
|
|
456
|
-
sha256: null,
|
|
457
|
-
risk: "network",
|
|
458
|
-
configured: configured(spec.id),
|
|
459
|
-
endpoint: spec.baseUrl,
|
|
460
|
-
apiKeyEnv: spec.apiKeyEnv,
|
|
461
|
-
configSource: "environment-presence",
|
|
462
|
-
}));
|
|
463
|
-
const presetModels = PROVIDER_PRESETS.flatMap((preset) => preset.models.map((model) => ({
|
|
464
|
-
id: `${preset.id}/${model.id}`,
|
|
465
|
-
kind: "model",
|
|
466
|
-
name: model.name ?? model.id,
|
|
467
|
-
provider: preset.id,
|
|
468
|
-
source: "builtin:psyclaw-sync",
|
|
469
|
-
ref: "psyclaw/providers/__init__.py",
|
|
470
|
-
version: model.id,
|
|
471
|
-
license: "provider-terms",
|
|
472
|
-
sha256: null,
|
|
473
|
-
risk: preset.baseUrl.startsWith("http") ? "network" : "local",
|
|
474
|
-
configured: configured(preset.id) || Boolean(process.env[preset.apiKeyEnv]),
|
|
475
|
-
endpoint: preset.baseUrl,
|
|
476
|
-
apiKeyEnv: preset.apiKeyEnv,
|
|
477
|
-
configSource: "preset",
|
|
478
|
-
})));
|
|
479
|
-
let registered = [];
|
|
480
|
-
try {
|
|
481
|
-
// Pi's runtime is the source of truth for user/provider model catalogs. It
|
|
482
|
-
// is explicitly cache-only here so opening the panel never triggers a
|
|
483
|
-
// network refresh or an authentication flow.
|
|
484
|
-
const gateway = await PiModelGateway.create({ allowModelNetwork: false, refreshOnCreate: false });
|
|
485
|
-
registered = gateway.list();
|
|
486
|
-
}
|
|
487
|
-
catch {
|
|
488
|
-
registered = [];
|
|
489
|
-
}
|
|
490
|
-
const registeredModels = registered.map((model) => {
|
|
491
|
-
const apiKeyEnv = envForProvider(model.provider);
|
|
492
|
-
return {
|
|
493
|
-
id: `${model.provider}/${model.id}`,
|
|
494
|
-
kind: "model",
|
|
495
|
-
name: model.name,
|
|
496
|
-
provider: model.provider,
|
|
497
|
-
source: "pi:model-runtime",
|
|
498
|
-
ref: "models.json",
|
|
499
|
-
version: model.id,
|
|
500
|
-
license: "provider-terms",
|
|
501
|
-
sha256: null,
|
|
502
|
-
risk: "network",
|
|
503
|
-
configured: configured(model.provider),
|
|
504
|
-
endpoint: model.baseUrl,
|
|
505
|
-
apiKeyEnv: apiKeyEnv ?? "Pi auth/config",
|
|
506
|
-
configSource: "environment-presence",
|
|
507
|
-
};
|
|
508
|
-
});
|
|
509
|
-
// A real Pi registry entry wins over the helper template for the same
|
|
510
|
-
// provider/model id. Helpers fill only gaps, so a local custom endpoint is
|
|
511
|
-
// never silently replaced by psyclaw defaults.
|
|
512
|
-
// Curated presets are presented first so stale local catalog entries do not
|
|
513
|
-
// hide the synchronized provider list. Local-only models are retained below.
|
|
514
|
-
const models = [...presetModels, ...registeredModels];
|
|
515
|
-
const seenModelIds = new Set(models.map((model) => model.id));
|
|
516
|
-
for (const model of [...presetModels, ...helperModels]) {
|
|
517
|
-
if (seenModelIds.has(model.id))
|
|
518
|
-
continue;
|
|
519
|
-
models.push(model);
|
|
520
|
-
seenModelIds.add(model.id);
|
|
521
|
-
}
|
|
522
|
-
return { schemaVersion: "psyclaw/panel-catalog/v1", agents, models };
|
|
523
|
-
}
|
|
524
|
-
async function recommendedSkills() {
|
|
525
|
-
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
526
|
-
const candidates = [
|
|
527
|
-
join(moduleDir, "..", "..", "..", "skills", "recommended", "catalog.json"),
|
|
528
|
-
join(moduleDir, "..", "..", "..", "..", "skills", "recommended", "catalog.json"),
|
|
529
|
-
join(moduleDir, "..", "..", "skills", "recommended", "catalog.json"),
|
|
530
|
-
join(process.cwd(), "skills", "recommended", "catalog.json"),
|
|
531
|
-
];
|
|
532
|
-
for (const path of candidates) {
|
|
533
|
-
try {
|
|
534
|
-
const catalog = JSON.parse(await readFile(path, "utf8"));
|
|
535
|
-
return { ...catalog, items: (catalog.items ?? []).map((item) => ({ ...item, slashCommand: `/skills enable ${String(item.id ?? "")}`, installCommand: `/install skill ${String(item.id ?? "")}` })) };
|
|
536
|
-
}
|
|
537
|
-
catch { /* try package layout */ }
|
|
538
|
-
}
|
|
539
|
-
return { schemaVersion: "psyclaw/recommended-skills/v1", documentVersion: "0.1.0", items: [] };
|
|
540
|
-
}
|
|
541
|
-
async function recommendedMcps() {
|
|
542
|
-
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
543
|
-
const candidates = [
|
|
544
|
-
join(moduleDir, "..", "..", "..", "skills", "recommended", "mcp-catalog.json"),
|
|
545
|
-
join(moduleDir, "..", "..", "..", "..", "skills", "recommended", "mcp-catalog.json"),
|
|
546
|
-
join(moduleDir, "..", "..", "skills", "recommended", "mcp-catalog.json"),
|
|
547
|
-
join(process.cwd(), "skills", "recommended", "mcp-catalog.json"),
|
|
548
|
-
];
|
|
549
|
-
for (const path of candidates) {
|
|
550
|
-
try {
|
|
551
|
-
const catalog = JSON.parse(await readFile(path, "utf8"));
|
|
552
|
-
return { ...catalog, items: (catalog.items ?? []).map((item) => ({ ...item, slashCommand: `/mcp enable ${String(item.id ?? "")}`, installCommand: `/install mcp ${String(item.id ?? "")}` })) };
|
|
553
|
-
}
|
|
554
|
-
catch { /* try package layout */ }
|
|
555
|
-
}
|
|
556
|
-
return { schemaVersion: "psyclaw/recommended-mcp/v1", documentVersion: "0.1.0", items: [] };
|
|
557
|
-
}
|
|
558
|
-
async function recommendedInstallPrep() {
|
|
559
|
-
const [skills, mcps] = await Promise.all([recommendedSkills(), recommendedMcps()]);
|
|
560
|
-
const skillPrep = skills && typeof skills === "object" && Array.isArray(skills.installPrep)
|
|
561
|
-
? skills.installPrep.map((item) => ({ ...item, kind: "skill" })) : [];
|
|
562
|
-
const mcpPrep = mcps && typeof mcps === "object" && Array.isArray(mcps.installPrep)
|
|
563
|
-
? mcps.installPrep.map((item) => ({ ...item, kind: "mcp" })) : [];
|
|
564
|
-
return { schemaVersion: "psyclaw/recommended-install-prep/v1", documentVersion: "0.1.0", items: [...skillPrep, ...mcpPrep] };
|
|
565
|
-
}
|
|
566
|
-
async function readAgentSettings() {
|
|
567
|
-
try {
|
|
568
|
-
const parsed = JSON.parse(await readFile(join(getAgentDir(), "settings.json"), "utf8"));
|
|
569
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
570
|
-
return parsed;
|
|
571
|
-
}
|
|
572
|
-
catch { /* start empty */ }
|
|
573
|
-
return {};
|
|
574
|
-
}
|
|
575
|
-
async function writeAgentSettings(patch) {
|
|
576
|
-
const next = { ...(await readAgentSettings()), ...patch };
|
|
577
|
-
await atomicWriteFile(join(getAgentDir(), "settings.json"), `${JSON.stringify(next, null, 2)}\n`);
|
|
578
|
-
}
|
|
579
|
-
async function findRecommendedItem(kind, id) {
|
|
580
|
-
const catalog = await (kind === "skill" ? recommendedSkills() : recommendedMcps());
|
|
581
|
-
const item = (catalog.items ?? []).find((candidate) => candidate.id === id);
|
|
582
|
-
if (!item)
|
|
583
|
-
return undefined;
|
|
584
|
-
const prep = (catalog.installPrep ?? []).find((candidate) => candidate.id === id);
|
|
585
|
-
return { item, ...(prep === undefined ? {} : { prep }) };
|
|
586
|
-
}
|
|
587
|
-
function runShellCommand(command, cwd) {
|
|
588
|
-
return new Promise((resolve) => {
|
|
589
|
-
const child = spawn(command, { shell: true, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
590
|
-
let output = "";
|
|
591
|
-
const append = (chunk) => {
|
|
592
|
-
output += String(chunk);
|
|
593
|
-
if (output.length > 24_000)
|
|
594
|
-
output = output.slice(-24_000);
|
|
595
|
-
};
|
|
596
|
-
child.stdout?.on("data", append);
|
|
597
|
-
child.stderr?.on("data", append);
|
|
598
|
-
child.on("error", (error) => resolve({ exitCode: 1, output: `spawn error: ${error.message}` }));
|
|
599
|
-
child.on("close", (code) => resolve({ exitCode: code ?? 1, output }));
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
const USER_HOOK_ID = /^u-[a-z0-9][a-z0-9._-]{0,40}$/i;
|
|
603
|
-
const HOOK_EVENTS = new Set(["before-analysis", "before-write", "after-analysis"]);
|
|
604
|
-
function sanitizeUserHooks(body) {
|
|
605
|
-
const raw = Array.isArray(body.hooks) ? body.hooks : [];
|
|
606
|
-
const hooks = raw.filter((value) => Boolean(value) && typeof value === "object" && !Array.isArray(value));
|
|
607
|
-
const cleaned = [];
|
|
608
|
-
for (const hook of hooks) {
|
|
609
|
-
const id = String(hook.id ?? "");
|
|
610
|
-
if (!USER_HOOK_ID.test(id))
|
|
611
|
-
throw new Error(`hook id must be user-managed (u- prefix): ${id || "(empty)"}`);
|
|
612
|
-
if (!HOOK_EVENTS.has(String(hook.event ?? "")))
|
|
613
|
-
throw new Error("hook event must be before-analysis, before-write or after-analysis");
|
|
614
|
-
if (hook.severity !== "block" && hook.severity !== "warn")
|
|
615
|
-
throw new Error("hook severity must be block or warn");
|
|
616
|
-
if (typeof hook.message !== "string" || hook.message.trim().length < 1)
|
|
617
|
-
throw new Error("hook message is required");
|
|
618
|
-
if (hook.pattern !== undefined && (typeof hook.pattern !== "string" || hook.pattern.length > 500))
|
|
619
|
-
throw new Error("hook pattern must be a short string");
|
|
620
|
-
if (hook.pathPrefix !== undefined && (typeof hook.pathPrefix !== "string" || hook.pathPrefix.length > 200))
|
|
621
|
-
throw new Error("hook pathPrefix must be a short string");
|
|
622
|
-
const item = { id, event: hook.event, severity: hook.severity, message: hook.message.trim() };
|
|
623
|
-
if (typeof hook.pattern === "string" && hook.pattern.trim())
|
|
624
|
-
item.pattern = hook.pattern.trim();
|
|
625
|
-
if (typeof hook.pathPrefix === "string" && hook.pathPrefix.trim())
|
|
626
|
-
item.pathPrefix = hook.pathPrefix.trim();
|
|
627
|
-
if (hook.enabled === false)
|
|
628
|
-
item.enabled = false;
|
|
629
|
-
cleaned.push(item);
|
|
630
|
-
}
|
|
631
|
-
return { schemaVersion: "psyclaw/user-analysis-hooks/v1", hooks: cleaned };
|
|
632
|
-
}
|
|
633
|
-
async function readJsonBody(request) {
|
|
634
|
-
let body = "";
|
|
635
|
-
for await (const chunk of request) {
|
|
636
|
-
body += String(chunk);
|
|
637
|
-
if (body.length > 128_000)
|
|
638
|
-
throw new Error("request body too large");
|
|
639
|
-
}
|
|
640
|
-
const parsed = JSON.parse(body || "{}");
|
|
641
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
642
|
-
throw new Error("request body must be an object");
|
|
643
|
-
return parsed;
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Run a panel write handler and surface the real validation error back to the
|
|
647
|
-
* frontend (the outer handler intentionally hides internal failure details).
|
|
648
|
-
*/
|
|
649
|
-
async function runPanelWrite(response, fn) {
|
|
650
|
-
try {
|
|
651
|
-
await fn();
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
655
|
-
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* When the panel message references claims by id (e.g. the 追问此论断 flow),
|
|
660
|
-
* attach the real ledger context (.psyclaw) so the spawned read-only assistant
|
|
661
|
-
* answers grounded instead of fumbling for the claim.
|
|
662
|
-
*/
|
|
663
|
-
export async function enrichAssistantMessage(root, message) {
|
|
664
|
-
const ids = [...new Set([...message.matchAll(/claim_[a-f0-9]{16}/gi)].map((match) => match[0]))];
|
|
665
|
-
if (ids.length === 0)
|
|
666
|
-
return message;
|
|
667
|
-
let ledger;
|
|
668
|
-
try {
|
|
669
|
-
ledger = await loadLedger(root);
|
|
670
|
-
}
|
|
671
|
-
catch {
|
|
672
|
-
return message;
|
|
673
|
-
}
|
|
674
|
-
const lines = [];
|
|
675
|
-
for (const id of ids) {
|
|
676
|
-
const claim = ledger.claims.find((candidate) => candidate.id === id);
|
|
677
|
-
if (!claim) {
|
|
678
|
-
lines.push(`- ${id}:项目账本(.psyclaw/claims.jsonl)中不存在该论断。`);
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
const links = ledger.links.filter((link) => link.claimId === id);
|
|
682
|
-
const evidence = links.map((link) => ledger.evidence.find((item) => item.id === link.evidenceId)).filter((item) => Boolean(item));
|
|
683
|
-
lines.push(`- 论断 ${id}:文本="${claim.text}";类型=${claim.kind};状态=${claim.status}${claim.uncertainty === undefined ? "" : `;不确定说明="${claim.uncertainty}"`};关联证据 ${evidence.length} 条。`, ...(evidence.length === 0
|
|
684
|
-
? [" (该论断尚未登记任何证据/数据——无法核验样本量或效应量。诚实做法:说明需要先登记证据、核验 DOI 或下载全文,再要求方法学复核。)"]
|
|
685
|
-
: []), ...evidence.slice(0, 5).map((item) => ` - 证据 ${item.id}:来源=${item.source.kind}:${item.source.locator.slice(0, 80)};级别=${item.level};accessStatus=${item.accessStatus}${item.sha256 === undefined ? "" : `;sha256=${item.sha256.slice(0, 16)}…`}`));
|
|
686
|
-
}
|
|
687
|
-
return `${message}\n\n[项目证据上下文(panel 自动附加,来自 .psyclaw 账本,只读)]\n${lines.join("\n")}`;
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Narrow local panel server. Run facts and catalog endpoints are read-only;
|
|
691
|
-
* provider configuration is the one explicit POST action and delegates key
|
|
692
|
-
* persistence to Pi's auth storage.
|
|
693
|
-
*/
|
|
694
|
-
export function createPanelServer(root, options = {}) {
|
|
695
|
-
// Resolve against the package rather than the process cwd, so `the PsyClaw /panel extension`
|
|
696
|
-
// works from any project directory. The build step copies `apps/panel` into
|
|
697
|
-
// `dist/apps/panel`, keeping the same relative layout in src and dist.
|
|
698
|
-
const panelHtmlPath = options.panelHtmlPath ??
|
|
699
|
-
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "apps", "panel", "index.html");
|
|
700
|
-
return createServer(async (request, response) => {
|
|
701
|
-
const url = new URL(request.url ?? "/", "http://localhost");
|
|
702
|
-
// Read-only surface: any write method is rejected before routing.
|
|
703
|
-
const runAction = /^\/api\/runs\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\/(pause|resume)$/.test(url.pathname);
|
|
704
|
-
if (request.method !== "GET" && request.method !== "HEAD" && !(request.method === "POST" && (["/api/provider-config", "/api/hitl/decision", "/api/assistant", "/api/system-prompt", "/api/hooks", "/api/recommendation-state", "/api/install/execute", "/api/active-provider", "/api/artifact/save", "/api/manuscript", "/api/claim", "/api/evidence", "/api/doi/verify", "/api/documents/import", "/api/publish", "/api/references/verify", "/api/references/check", "/api/references/download", "/api/citations"].includes(url.pathname) || runAction))) {
|
|
705
|
-
response.writeHead(405, { "content-type": "application/json", allow: "GET, HEAD" });
|
|
706
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
try {
|
|
710
|
-
if (url.pathname === "/api/runs") {
|
|
711
|
-
const runs = await listRuns(root);
|
|
712
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
713
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/run-listing/v1", runs, projectRoot: root }));
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
if (url.pathname === "/api/snapshot") {
|
|
717
|
-
const runId = url.searchParams.get("runId");
|
|
718
|
-
if (!runId) {
|
|
719
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
720
|
-
response.end(JSON.stringify({ error: "runId query parameter is required" }));
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
const snapshot = await projectRunSnapshot(root, runId);
|
|
724
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
725
|
-
response.end(JSON.stringify(snapshot));
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
if (url.pathname === "/api/catalog") {
|
|
729
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
730
|
-
response.end(JSON.stringify(await panelCatalog(root)));
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (url.pathname === "/api/install-plan") {
|
|
734
|
-
const agentId = url.searchParams.get("agentId");
|
|
735
|
-
const agent = KNOWN_AGENTS.find((candidate) => candidate.id === agentId);
|
|
736
|
-
if (!agent) {
|
|
737
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
738
|
-
response.end(JSON.stringify({ error: "unknown agent" }));
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const plan = planAgentInstall(agent, { projectRoot: root });
|
|
742
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
743
|
-
response.end(JSON.stringify({
|
|
744
|
-
schemaVersion: "psyclaw/install-plan-view/v1",
|
|
745
|
-
plan: publicInstallPlan(plan),
|
|
746
|
-
approval: "required",
|
|
747
|
-
status: "plan-only",
|
|
748
|
-
}));
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
if (url.pathname === "/api/import-plan") {
|
|
752
|
-
const agentId = url.searchParams.get("agentId");
|
|
753
|
-
const scan = (await discoverAgents()).find((candidate) => candidate.id === agentId && candidate.found);
|
|
754
|
-
if (!scan) {
|
|
755
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
756
|
-
response.end(JSON.stringify({ error: "agent is not configured" }));
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
760
|
-
response.end(JSON.stringify({
|
|
761
|
-
schemaVersion: "psyclaw/import-plan-view/v1",
|
|
762
|
-
agentId: scan.id,
|
|
763
|
-
status: "plan-only",
|
|
764
|
-
approval: "required",
|
|
765
|
-
skills: scan.skills.map((skill) => ({ id: `${scan.id}:${skill.name}`, name: skill.name, kind: skill.kind, configured: true })),
|
|
766
|
-
}));
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (url.pathname === "/api/history") {
|
|
770
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
771
|
-
response.end(JSON.stringify(await panelHistory(root)));
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
if (url.pathname === "/api/stats") {
|
|
775
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
776
|
-
response.end(JSON.stringify(await panelStats(root)));
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
if (url.pathname === "/api/files") {
|
|
780
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
781
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/panel-files/v1", files: await listProjectFiles(root) }));
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
if (url.pathname === "/api/config") {
|
|
785
|
-
const promptPath = await assertSafeProjectPath(root, ".psyclaw/system-prompt.md");
|
|
786
|
-
let supplement = "";
|
|
787
|
-
try {
|
|
788
|
-
supplement = await readFile(promptPath, "utf8");
|
|
789
|
-
}
|
|
790
|
-
catch { /* no user supplement */ }
|
|
791
|
-
const hooksText = await readFile(projectPaths(root).analysisHooks, "utf8").catch(() => "{\n \"schemaVersion\": \"psyclaw/user-analysis-hooks/v1\",\n \"hooks\": []\n}\n");
|
|
792
|
-
let hooks;
|
|
793
|
-
try {
|
|
794
|
-
hooks = JSON.parse(hooksText);
|
|
795
|
-
}
|
|
796
|
-
catch {
|
|
797
|
-
hooks = { schemaVersion: "psyclaw/user-analysis-hooks/v1", hooks: [], invalid: true };
|
|
798
|
-
}
|
|
799
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
800
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/panel-config/v1", systemPromptBase: PSYCLAW_IDENTITY_PROMPT, systemPromptSupplement: supplement, hooks }));
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (url.pathname === "/api/token-usage") {
|
|
804
|
-
const report = await readSessionUsage(join(getAgentDir(), "sessions"));
|
|
805
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
806
|
-
response.end(JSON.stringify(report));
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
if (url.pathname === "/api/literature-map") {
|
|
810
|
-
const ledger = await loadLedger(root);
|
|
811
|
-
const map = buildClaimLiteratureMap(ledger.claims, ledger.evidence, ledger.links);
|
|
812
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
813
|
-
response.end(JSON.stringify(map));
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
if (url.pathname === "/api/enabled-capabilities") {
|
|
817
|
-
const state = await readRecommendationState(root);
|
|
818
|
-
const disabled = await readDisabledCapabilities(root);
|
|
819
|
-
const [skills, mcps] = await Promise.all([recommendedSkills(), recommendedMcps()]);
|
|
820
|
-
const nameOf = (kind, id) => {
|
|
821
|
-
const catalog = kind === "skill" ? skills : mcps;
|
|
822
|
-
const item = (catalog.items ?? []).find((candidate) => candidate.id === id);
|
|
823
|
-
return typeof item?.name === "string" ? item.name : id;
|
|
824
|
-
};
|
|
825
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
826
|
-
response.end(JSON.stringify({
|
|
827
|
-
schemaVersion: "psyclaw/enabled-capabilities/v1",
|
|
828
|
-
coreSkills: CORE_SKILLS.map((skill) => ({ id: skill.id, name: skill.name, enabled: !disabled.coreSkills.includes(skill.id) })),
|
|
829
|
-
skills: state.skills.map((id) => ({ id, name: nameOf("skill", id), enabled: true })),
|
|
830
|
-
mcp: state.mcp.map((id) => ({ id, name: nameOf("mcp", id), enabled: true })),
|
|
831
|
-
}));
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
if (url.pathname === "/api/active-provider") {
|
|
835
|
-
if (request.method === "GET") {
|
|
836
|
-
const settings = await readAgentSettings();
|
|
837
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
838
|
-
response.end(JSON.stringify({
|
|
839
|
-
schemaVersion: "psyclaw/active-provider/v1",
|
|
840
|
-
provider: typeof settings.defaultProvider === "string" ? settings.defaultProvider : undefined,
|
|
841
|
-
model: typeof settings.defaultModel === "string" ? settings.defaultModel : undefined,
|
|
842
|
-
}));
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
const body = await readJsonBody(request);
|
|
846
|
-
const provider = String(body.provider ?? "").trim();
|
|
847
|
-
const model = String(body.model ?? "").trim();
|
|
848
|
-
if (!/^[a-z0-9][a-z0-9._/-]{0,127}$/i.test(provider))
|
|
849
|
-
throw new Error("provider is required and may contain letters, digits, . _ / -");
|
|
850
|
-
if (!/^[a-z0-9][a-z0-9._:/()-]{0,127}$/i.test(model))
|
|
851
|
-
throw new Error("model is required and may contain letters, digits, . _ : / ( ) -");
|
|
852
|
-
await writeAgentSettings({ defaultProvider: provider, defaultModel: model });
|
|
853
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
854
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/active-provider-receipt/v1", ok: true, provider, model, note: "重启 Pi 会话后生效" }));
|
|
855
|
-
return;
|
|
856
|
-
}
|
|
857
|
-
if (url.pathname === "/api/install/execute") {
|
|
858
|
-
if (request.method !== "POST") {
|
|
859
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
860
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
const body = await readJsonBody(request);
|
|
864
|
-
const kind = body.kind === "skill" || body.kind === "mcp" ? body.kind : undefined;
|
|
865
|
-
const id = String(body.id ?? "").trim();
|
|
866
|
-
const approved = body.approved === true;
|
|
867
|
-
const actor = String(body.actor ?? "researcher").trim();
|
|
868
|
-
if (!kind || !id || !approved || actor.length < 1)
|
|
869
|
-
throw new Error("kind, id, approved and actor are required");
|
|
870
|
-
const found = await findRecommendedItem(kind, id);
|
|
871
|
-
if (!found) {
|
|
872
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
873
|
-
response.end(JSON.stringify({ error: "unknown recommended item" }));
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const command = typeof found.prep?.command === "string" && found.prep.command.trim() ? found.prep.command.trim() : undefined;
|
|
877
|
-
if (!command) {
|
|
878
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
879
|
-
response.end(JSON.stringify({ error: "no install command is available for this item", blockedReason: found.prep?.blockedReason ?? null }));
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
const startedAt = new Date().toISOString();
|
|
883
|
-
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
884
|
-
const idempotencyKey = `panel:install:${sha256Text(`${kind}\u0000${id}\u0000${actor}`).slice(0, 24)}`;
|
|
885
|
-
const receipt = {
|
|
886
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
887
|
-
runId,
|
|
888
|
-
taskId: `install:${kind}:${id}`,
|
|
889
|
-
tool: "panel.install.execute",
|
|
890
|
-
effect: "write",
|
|
891
|
-
approval: "approved",
|
|
892
|
-
idempotencyKey,
|
|
893
|
-
ok: false,
|
|
894
|
-
command,
|
|
895
|
-
startedAt,
|
|
896
|
-
};
|
|
897
|
-
const { exitCode, output } = await runShellCommand(command, root);
|
|
898
|
-
receipt.ok = exitCode === 0;
|
|
899
|
-
receipt.exitCode = exitCode;
|
|
900
|
-
receipt.finishedAt = new Date().toISOString();
|
|
901
|
-
const receiptPath = await assertSafeProjectPath(root, `.psyclaw/manifests/${runId}.receipt.json`);
|
|
902
|
-
await atomicWriteFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
903
|
-
if (receipt.ok) {
|
|
904
|
-
const state = await readRecommendationState(root);
|
|
905
|
-
const key = kind === "skill" ? "skills" : "mcp";
|
|
906
|
-
state[key] = [...new Set([...state[key], id])];
|
|
907
|
-
await writeRecommendationState(root, state);
|
|
908
|
-
}
|
|
909
|
-
await appendJsonlIfMissing(projectPaths(root).audit, {
|
|
910
|
-
schemaVersion: "psyclaw/audit-event/v1",
|
|
911
|
-
at: new Date().toISOString(),
|
|
912
|
-
actor,
|
|
913
|
-
action: `panel.install.${kind}`,
|
|
914
|
-
targetId: id,
|
|
915
|
-
ok: receipt.ok,
|
|
916
|
-
runId,
|
|
917
|
-
idempotencyKey,
|
|
918
|
-
}, (item) => item.runId);
|
|
919
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
920
|
-
response.end(JSON.stringify({
|
|
921
|
-
schemaVersion: "psyclaw/install-execution-receipt/v1",
|
|
922
|
-
ok: receipt.ok,
|
|
923
|
-
exitCode,
|
|
924
|
-
command,
|
|
925
|
-
output: output.slice(-2000),
|
|
926
|
-
reloadHint: "/reload",
|
|
927
|
-
...(typeof found.prep?.blockedReason === "string" ? { blockedReason: found.prep.blockedReason } : {}),
|
|
928
|
-
}));
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
if (url.pathname === "/api/recommendation-state") {
|
|
932
|
-
if (request.method === "GET") {
|
|
933
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
934
|
-
response.end(JSON.stringify(await readRecommendationState(root)));
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
const body = await readJsonBody(request);
|
|
938
|
-
const kind = body.kind === "skill" || body.kind === "skills" ? "skills" : body.kind === "mcp" ? "mcp" : body.kind === "core-skill" ? "core-skill" : undefined;
|
|
939
|
-
const id = String(body.id ?? "").trim();
|
|
940
|
-
if (!kind || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(id) || typeof body.enabled !== "boolean")
|
|
941
|
-
throw new Error("kind, id and enabled are required");
|
|
942
|
-
if (kind === "core-skill") {
|
|
943
|
-
if (!CORE_SKILLS.some((skill) => skill.id === id))
|
|
944
|
-
throw new Error(`unknown core skill: ${id}`);
|
|
945
|
-
const disabled = await readDisabledCapabilities(root);
|
|
946
|
-
const values = new Set(disabled.coreSkills);
|
|
947
|
-
if (body.enabled)
|
|
948
|
-
values.delete(id);
|
|
949
|
-
else
|
|
950
|
-
values.add(id);
|
|
951
|
-
disabled.coreSkills = [...values];
|
|
952
|
-
await writeDisabledCapabilities(root, disabled);
|
|
953
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
954
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/capability-state-receipt/v1", ok: true, coreSkills: disabled.coreSkills }));
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
const state = await readRecommendationState(root);
|
|
958
|
-
const values = new Set(state[kind]);
|
|
959
|
-
if (body.enabled)
|
|
960
|
-
values.add(id);
|
|
961
|
-
else
|
|
962
|
-
values.delete(id);
|
|
963
|
-
state[kind] = [...values];
|
|
964
|
-
await writeRecommendationState(root, state);
|
|
965
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
966
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/recommendation-state-receipt/v1", ok: true, state }));
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
if (url.pathname === "/api/system-prompt" || url.pathname === "/api/hooks") {
|
|
970
|
-
const body = await readJsonBody(request);
|
|
971
|
-
if (url.pathname === "/api/system-prompt") {
|
|
972
|
-
const prompt = String(body.systemPrompt ?? "").trim();
|
|
973
|
-
if (!prompt || prompt.length > 40_000)
|
|
974
|
-
throw new Error("systemPrompt is required and must be under 40000 characters");
|
|
975
|
-
await atomicWriteFile(await assertSafeProjectPath(root, ".psyclaw/system-prompt.md"), `${prompt}\n`);
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
const cleaned = sanitizeUserHooks(body);
|
|
979
|
-
await atomicWriteFile(await assertSafeProjectPath(root, ".psyclaw/analysis-hooks.json"), `${JSON.stringify(cleaned, null, 2)}\n`);
|
|
980
|
-
}
|
|
981
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
982
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/panel-config-receipt/v1", ok: true, updated: url.pathname.slice(5) }));
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
if (url.pathname === "/api/assistant") {
|
|
986
|
-
if (request.method !== "POST") {
|
|
987
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
988
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
if (options.assistant === undefined) {
|
|
992
|
-
response.writeHead(503, { "content-type": "application/json" });
|
|
993
|
-
response.end(JSON.stringify({
|
|
994
|
-
error: "面板聊天未连接智能体:请从 psyclaw 对话中执行 /panel 打开工作台(智能体通道随 /panel 建立);当前为独立 serve,只能查看项目、无法回复。也可直接到 psyclaw 对话中提问。",
|
|
995
|
-
reasonCode: "panel.assistant-unavailable",
|
|
996
|
-
}));
|
|
997
|
-
return;
|
|
998
|
-
}
|
|
999
|
-
const body = await readJsonBody(request);
|
|
1000
|
-
const message = String(body.message ?? "").trim();
|
|
1001
|
-
if (!message || message.length > 12_000) {
|
|
1002
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
1003
|
-
response.end(JSON.stringify({ error: "message is required and must be under 12000 characters" }));
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
try {
|
|
1007
|
-
const enriched = await enrichAssistantMessage(root, message);
|
|
1008
|
-
const answer = await options.assistant(enriched);
|
|
1009
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1010
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/workbench-message/v1", ...answer }));
|
|
1011
|
-
}
|
|
1012
|
-
catch (error) {
|
|
1013
|
-
response.writeHead(502, { "content-type": "application/json" });
|
|
1014
|
-
response.end(JSON.stringify({ error: error instanceof Error ? error.message : "assistant request failed", reasonCode: "panel.assistant-failed" }));
|
|
1015
|
-
}
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
if (runAction) {
|
|
1019
|
-
const match = url.pathname.match(/^\/api\/runs\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/(pause|resume)$/);
|
|
1020
|
-
const runId = match?.[1] ?? "";
|
|
1021
|
-
const action = match?.[2] ?? "";
|
|
1022
|
-
const body = await readJsonBody(request);
|
|
1023
|
-
const approved = body.approved === true;
|
|
1024
|
-
const actor = String(body.actor ?? "").trim();
|
|
1025
|
-
const reason = String(body.reason ?? "").trim();
|
|
1026
|
-
if (!approved || !actor || reason.length < 3) {
|
|
1027
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
1028
|
-
response.end(JSON.stringify({ error: "approved=true, actor, and reason (3+ chars) are required" }));
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const pausePath = await assertSafeProjectPath(root, `.psyclaw/runs/${runId}.pause`);
|
|
1032
|
-
const planPath = await assertSafeProjectPath(root, `.psyclaw/plans/${runId}.json`);
|
|
1033
|
-
if (action === "pause") {
|
|
1034
|
-
await atomicWriteFile(pausePath, `${JSON.stringify({ schemaVersion: "psyclaw/pause-request/v1", runId, actor, reason, requestedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
1035
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1036
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/run-action-receipt/v1", action, runId, status: "pause-requested", actor }));
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
if (activePanelRuns.has(runId)) {
|
|
1040
|
-
response.writeHead(409, { "content-type": "application/json" });
|
|
1041
|
-
response.end(JSON.stringify({ error: "run is already being resumed" }));
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
let plan;
|
|
1045
|
-
try {
|
|
1046
|
-
plan = JSON.parse(await readFile(planPath, "utf8"));
|
|
1047
|
-
}
|
|
1048
|
-
catch {
|
|
1049
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
1050
|
-
response.end(JSON.stringify({ error: "run plan not found" }));
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
await unlink(pausePath).catch(() => undefined);
|
|
1054
|
-
activePanelRuns.add(runId);
|
|
1055
|
-
try {
|
|
1056
|
-
const eventLog = new RunEventLog(root, runId);
|
|
1057
|
-
const result = await resumePlanWithPi(plan, {
|
|
1058
|
-
cwd: root,
|
|
1059
|
-
root,
|
|
1060
|
-
pauseRequested: async () => {
|
|
1061
|
-
try {
|
|
1062
|
-
await lstat(pausePath);
|
|
1063
|
-
return true;
|
|
1064
|
-
}
|
|
1065
|
-
catch {
|
|
1066
|
-
return false;
|
|
1067
|
-
}
|
|
1068
|
-
},
|
|
1069
|
-
onEvent: async (event) => { await eventLog.append(event); },
|
|
1070
|
-
});
|
|
1071
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1072
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/run-action-receipt/v1", action, runId, status: result.status, diagnostics: result.diagnostics, actor }));
|
|
1073
|
-
}
|
|
1074
|
-
finally {
|
|
1075
|
-
activePanelRuns.delete(runId);
|
|
1076
|
-
}
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
if (url.pathname === "/api/hitl") {
|
|
1080
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1081
|
-
response.end(JSON.stringify(await readHitlWorkspace(root, url.searchParams.get("contents") === "1")));
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
if (url.pathname === "/api/artifacts") {
|
|
1085
|
-
const indexPath = projectPaths(root).outputs + "/index.json";
|
|
1086
|
-
let indexedArtifacts = [];
|
|
1087
|
-
let rest = {};
|
|
1088
|
-
try {
|
|
1089
|
-
const index = JSON.parse(await readFile(indexPath, "utf8"));
|
|
1090
|
-
if (Array.isArray(index.artifacts)) {
|
|
1091
|
-
indexedArtifacts = index.artifacts.filter((item) => Boolean(item) && typeof item === "object");
|
|
1092
|
-
}
|
|
1093
|
-
const { artifacts: _artifacts, ...remaining } = index;
|
|
1094
|
-
rest = remaining;
|
|
1095
|
-
}
|
|
1096
|
-
catch { /* no index.json yet */ }
|
|
1097
|
-
const figureFiles = await figureTableArtifacts(root);
|
|
1098
|
-
const indexedPaths = new Set(indexedArtifacts.map((item) => String(item.path ?? "")).filter(Boolean));
|
|
1099
|
-
const merged = [...indexedArtifacts, ...figureFiles.filter((item) => !indexedPaths.has(String(item.path)))];
|
|
1100
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1101
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/artifact-listing/v1", ...rest, artifacts: merged }));
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
if (url.pathname === "/api/artifact") {
|
|
1105
|
-
const relative = url.searchParams.get("path")?.trim() ?? "";
|
|
1106
|
-
if (!relative || relative.replaceAll("\\", "/").split("/").some((part) => part === ".." || part === "")) {
|
|
1107
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
1108
|
-
response.end(JSON.stringify({ error: "a relative artifact path is required" }));
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
// Resolve the path as-is first (paper/archive/, literature/, ...),
|
|
1112
|
-
// then fall back to outputs/<path> for legacy relative artifact entries.
|
|
1113
|
-
let target = await assertSafeProjectPath(root, relative);
|
|
1114
|
-
let stat = await lstat(target).catch(() => undefined);
|
|
1115
|
-
if (!stat?.isFile()) {
|
|
1116
|
-
const fallback = await assertSafeProjectPath(root, `outputs/${relative}`);
|
|
1117
|
-
const fallbackStat = await lstat(fallback).catch(() => undefined);
|
|
1118
|
-
if (fallbackStat?.isFile()) {
|
|
1119
|
-
target = fallback;
|
|
1120
|
-
stat = fallbackStat;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
if (!stat?.isFile() || stat.isSymbolicLink())
|
|
1124
|
-
throw new Error("artifact must be a regular file");
|
|
1125
|
-
const body = await readFile(target);
|
|
1126
|
-
const lower = target.toLowerCase();
|
|
1127
|
-
const contentType = lower.endsWith(".svg") ? "image/svg+xml; charset=utf-8" :
|
|
1128
|
-
lower.endsWith(".png") ? "image/png" :
|
|
1129
|
-
lower.endsWith(".jpg") || lower.endsWith(".jpeg") ? "image/jpeg" :
|
|
1130
|
-
lower.endsWith(".gif") ? "image/gif" :
|
|
1131
|
-
lower.endsWith(".pdf") ? "application/pdf" :
|
|
1132
|
-
lower.endsWith(".json") ? "application/json" :
|
|
1133
|
-
lower.endsWith(".md") ? "text/markdown; charset=utf-8" :
|
|
1134
|
-
"text/plain; charset=utf-8";
|
|
1135
|
-
const fileName = relative.split(/[\\/]/).at(-1) ?? "file";
|
|
1136
|
-
// Node rejects non-ASCII header values; use RFC 5987 for CJK names.
|
|
1137
|
-
const contentDisposition = /[^\x20-\x7E]/.test(fileName)
|
|
1138
|
-
? `inline; filename="${fileName.replace(/[^\x20-\x7E]/g, "_")}"; filename*=UTF-8''${encodeURIComponent(fileName)}`
|
|
1139
|
-
: `inline; filename="${fileName}"`;
|
|
1140
|
-
response.writeHead(200, { "content-type": contentType, "content-disposition": contentDisposition });
|
|
1141
|
-
response.end(body);
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
if (url.pathname === "/api/artifact/save") {
|
|
1145
|
-
if (request.method !== "POST") {
|
|
1146
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1147
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
const body = await readJsonBody(request);
|
|
1151
|
-
const relative = String(body.path ?? "").trim();
|
|
1152
|
-
const content = typeof body.content === "string" ? body.content : "";
|
|
1153
|
-
if (!relative || !/\.svg$/i.test(relative))
|
|
1154
|
-
throw new Error("only SVG artifacts can be edited");
|
|
1155
|
-
const rel = relative.replaceAll("\\", "/");
|
|
1156
|
-
if (rel.split("/").some((part) => part === ".." || part === ""))
|
|
1157
|
-
throw new Error("a relative artifact path is required");
|
|
1158
|
-
if (!rel.startsWith("outputs/") && !rel.startsWith("analysis/outputs/"))
|
|
1159
|
-
throw new Error("artifact must be under outputs/ or analysis/outputs/");
|
|
1160
|
-
const target = await assertSafeProjectPath(root, rel);
|
|
1161
|
-
const stat = await lstat(target).catch(() => undefined);
|
|
1162
|
-
if (stat !== undefined && (stat.isSymbolicLink() || !stat.isFile()))
|
|
1163
|
-
throw new Error("artifact must be a regular file");
|
|
1164
|
-
await atomicWriteFile(target, content.endsWith("\n") ? content : `${content}\n`);
|
|
1165
|
-
const receipt = {
|
|
1166
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
1167
|
-
runId: `panel_${randomUUID().replaceAll("-", "")}`,
|
|
1168
|
-
taskId: "panel.artifact.save",
|
|
1169
|
-
tool: "panel.artifact.save",
|
|
1170
|
-
effect: "write",
|
|
1171
|
-
approval: "approved",
|
|
1172
|
-
idempotencyKey: `panel:artifact:${sha256Text(`${rel}\u0000${content}`).slice(0, 24)}`,
|
|
1173
|
-
ok: true,
|
|
1174
|
-
startedAt: new Date().toISOString(),
|
|
1175
|
-
finishedAt: new Date().toISOString(),
|
|
1176
|
-
};
|
|
1177
|
-
await atomicWriteFile(await assertSafeProjectPath(root, `.psyclaw/manifests/${receipt.runId}.receipt.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1178
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1179
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/artifact-save-receipt/v1", ok: true, path: rel, bytes: Buffer.byteLength(content, "utf8") }));
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
if (url.pathname === "/api/ledger") {
|
|
1183
|
-
const ledger = await loadLedger(root);
|
|
1184
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1185
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/ledger/v1", ...ledger, projectRoot: root }));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
if (url.pathname === "/api/manuscript") {
|
|
1189
|
-
if (request.method === "POST") {
|
|
1190
|
-
await runPanelWrite(response, async () => {
|
|
1191
|
-
const body = await readJsonBody(request);
|
|
1192
|
-
const relative = String(body.path ?? "notes/manuscript.md").trim();
|
|
1193
|
-
const content = typeof body.content === "string" ? body.content : "";
|
|
1194
|
-
if (!content.trim())
|
|
1195
|
-
throw new Error("manuscript content is required");
|
|
1196
|
-
const saved = await writeManuscript(root, relative, content);
|
|
1197
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1198
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/manuscript-save/v1", ok: true, ...saved }));
|
|
1199
|
-
});
|
|
1200
|
-
return;
|
|
1201
|
-
}
|
|
1202
|
-
const manuscript = await readManuscript(root);
|
|
1203
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1204
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/manuscript/v1", ...manuscript }));
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
if (url.pathname === "/api/claim") {
|
|
1208
|
-
if (request.method !== "POST") {
|
|
1209
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1210
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
await runPanelWrite(response, async () => {
|
|
1214
|
-
const body = await readJsonBody(request);
|
|
1215
|
-
const claim = await appendLedgerClaim(root, body);
|
|
1216
|
-
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
1217
|
-
const receipt = {
|
|
1218
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
1219
|
-
runId,
|
|
1220
|
-
taskId: "panel.claim.append",
|
|
1221
|
-
tool: "panel.claim.append",
|
|
1222
|
-
effect: "write",
|
|
1223
|
-
approval: "approved",
|
|
1224
|
-
idempotencyKey: `panel:claim:${claim.id}`,
|
|
1225
|
-
ok: true,
|
|
1226
|
-
startedAt: new Date().toISOString(),
|
|
1227
|
-
finishedAt: new Date().toISOString(),
|
|
1228
|
-
};
|
|
1229
|
-
await atomicWriteFile(await assertSafeProjectPath(root, `.psyclaw/manifests/${runId}.receipt.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1230
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1231
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/claim-append/v1", ok: true, claim }));
|
|
1232
|
-
});
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
if (url.pathname === "/api/evidence") {
|
|
1236
|
-
if (request.method !== "POST") {
|
|
1237
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1238
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
await runPanelWrite(response, async () => {
|
|
1242
|
-
const body = await readJsonBody(request);
|
|
1243
|
-
const { evidence, link } = await appendLedgerEvidence(root, body);
|
|
1244
|
-
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
1245
|
-
const receipt = {
|
|
1246
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
1247
|
-
runId,
|
|
1248
|
-
taskId: "panel.evidence.append",
|
|
1249
|
-
tool: "panel.evidence.append",
|
|
1250
|
-
effect: "write",
|
|
1251
|
-
approval: "approved",
|
|
1252
|
-
idempotencyKey: `panel:evidence:${evidence.id}`,
|
|
1253
|
-
ok: true,
|
|
1254
|
-
startedAt: new Date().toISOString(),
|
|
1255
|
-
finishedAt: new Date().toISOString(),
|
|
1256
|
-
};
|
|
1257
|
-
await atomicWriteFile(await assertSafeProjectPath(root, `.psyclaw/manifests/${runId}.receipt.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1258
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1259
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/evidence-append/v1", ok: true, evidence, link }));
|
|
1260
|
-
});
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
if (url.pathname === "/api/doi/verify") {
|
|
1264
|
-
if (request.method !== "POST") {
|
|
1265
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1266
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
await runPanelWrite(response, async () => {
|
|
1270
|
-
const body = await readJsonBody(request);
|
|
1271
|
-
const doi = typeof body.doi === "string" ? body.doi : "";
|
|
1272
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1273
|
-
response.end(JSON.stringify(await verifyDoi(doi)));
|
|
1274
|
-
});
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
if (url.pathname === "/api/documents") {
|
|
1278
|
-
const documents = await scanDocuments(root);
|
|
1279
|
-
const imported = await readJsonl(await importsPath(root));
|
|
1280
|
-
const importedBySha = new Set(imported.map((record) => record.sha256));
|
|
1281
|
-
const manuscript = await readManuscript(root);
|
|
1282
|
-
const enriched = await Promise.all(documents.map(async (doc) => {
|
|
1283
|
-
let sha256 = "";
|
|
1284
|
-
try {
|
|
1285
|
-
sha256 = await sha256File(await assertSafeProjectPath(root, doc.path));
|
|
1286
|
-
}
|
|
1287
|
-
catch { /* unreadable */ }
|
|
1288
|
-
return { ...doc, sha256, imported: importedBySha.has(sha256), isManuscript: manuscript.path === doc.path };
|
|
1289
|
-
}));
|
|
1290
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1291
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/documents/v1", projectRoot: root, documents: enriched }));
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
if (url.pathname === "/api/documents/import") {
|
|
1295
|
-
if (request.method !== "POST") {
|
|
1296
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1297
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
await runPanelWrite(response, async () => {
|
|
1301
|
-
const body = await readJsonBody(request);
|
|
1302
|
-
const relative = typeof body.path === "string" ? body.path : "";
|
|
1303
|
-
const { record, markdown } = await importDocument(root, relative);
|
|
1304
|
-
const runId = `panel_${randomUUID().replaceAll("-", "")}`;
|
|
1305
|
-
const receipt = {
|
|
1306
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
1307
|
-
runId,
|
|
1308
|
-
taskId: "panel.document.import",
|
|
1309
|
-
tool: "panel.document.import",
|
|
1310
|
-
effect: "write",
|
|
1311
|
-
approval: "approved",
|
|
1312
|
-
idempotencyKey: `panel:import:${record.sha256}`,
|
|
1313
|
-
ok: true,
|
|
1314
|
-
startedAt: new Date().toISOString(),
|
|
1315
|
-
finishedAt: new Date().toISOString(),
|
|
1316
|
-
};
|
|
1317
|
-
await atomicWriteFile(await assertSafeProjectPath(root, `.psyclaw/manifests/${runId}.receipt.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1318
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1319
|
-
response.end(JSON.stringify({
|
|
1320
|
-
schemaVersion: "psyclaw/import/v1",
|
|
1321
|
-
ok: true,
|
|
1322
|
-
imported: true,
|
|
1323
|
-
alreadyImported: markdown === null,
|
|
1324
|
-
path: record.path,
|
|
1325
|
-
format: record.format,
|
|
1326
|
-
kind: record.kind,
|
|
1327
|
-
bytes: record.bytes,
|
|
1328
|
-
sha256: record.sha256,
|
|
1329
|
-
evidenceId: record.evidenceId,
|
|
1330
|
-
markdown,
|
|
1331
|
-
}));
|
|
1332
|
-
});
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
if (url.pathname === "/api/publish") {
|
|
1336
|
-
if (request.method !== "POST") {
|
|
1337
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1338
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
await runPanelWrite(response, async () => {
|
|
1342
|
-
const body = await readJsonBody(request);
|
|
1343
|
-
const result = await publishManuscript(root, {
|
|
1344
|
-
...(typeof body.name === "string" && body.name.trim() ? { name: body.name } : {}),
|
|
1345
|
-
...(typeof body.content === "string" ? { markdown: body.content } : {}),
|
|
1346
|
-
...(body.exportDocx === undefined ? {} : { exportDocx: body.exportDocx !== false }),
|
|
1347
|
-
});
|
|
1348
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1349
|
-
response.end(JSON.stringify({ ok: true, ...result }));
|
|
1350
|
-
});
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
if (url.pathname === "/api/versions") {
|
|
1354
|
-
const versions = await readPublishedVersions(root);
|
|
1355
|
-
const current = versions.at(-1) ?? null;
|
|
1356
|
-
const withLoadPaths = versions.map((version) => {
|
|
1357
|
-
const isCurrent = current !== null && version.version === current.version;
|
|
1358
|
-
return {
|
|
1359
|
-
...version,
|
|
1360
|
-
markdownLoadPath: isCurrent ? version.markdownPath : `paper/archive/${version.name}_v${version.version}.md`,
|
|
1361
|
-
...(version.docxPath === null ? {} : { docxLoadPath: isCurrent ? version.docxPath : `paper/archive/${version.name}_v${version.version}_APA7.docx` }),
|
|
1362
|
-
};
|
|
1363
|
-
});
|
|
1364
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1365
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/versions/v1", versions: withLoadPaths, current }));
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
if (url.pathname === "/api/references") {
|
|
1369
|
-
const references = await listReferences(root);
|
|
1370
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1371
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/references/v1", references }));
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
if (url.pathname === "/api/references/verify") {
|
|
1375
|
-
if (request.method !== "POST") {
|
|
1376
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1377
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
await runPanelWrite(response, async () => {
|
|
1381
|
-
const body = await readJsonBody(request);
|
|
1382
|
-
const doi = typeof body.doi === "string" ? body.doi.trim() : "";
|
|
1383
|
-
if (!doi)
|
|
1384
|
-
throw new Error("doi is required");
|
|
1385
|
-
const [verification, oa] = await Promise.all([verifyDoi(doi), lookupOaPdfUrl(doi)]);
|
|
1386
|
-
const record = referenceFromVerification(verification);
|
|
1387
|
-
if (!record) {
|
|
1388
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1389
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/reference-verify/v1", ok: false, verification, reason: "no-metadata" }));
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
if (oa.isOa && oa.oaPdfUrl)
|
|
1393
|
-
record.oaPdfUrl = oa.oaPdfUrl;
|
|
1394
|
-
const { appended } = await upsertReference(root, record);
|
|
1395
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1396
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/reference-verify/v1", ok: true, appended, record }));
|
|
1397
|
-
});
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
if (url.pathname === "/api/references/check") {
|
|
1401
|
-
if (request.method !== "POST") {
|
|
1402
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1403
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
await runPanelWrite(response, async () => {
|
|
1407
|
-
const body = await readJsonBody(request);
|
|
1408
|
-
const text = typeof body.text === "string" ? body.text : "";
|
|
1409
|
-
if (!text.trim())
|
|
1410
|
-
throw new Error("text is required");
|
|
1411
|
-
const references = await listReferences(root);
|
|
1412
|
-
const result = checkCitations(text, references);
|
|
1413
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1414
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/reference-check/v1", ...result, archiveSize: references.length }));
|
|
1415
|
-
});
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
if (url.pathname === "/api/references/download") {
|
|
1419
|
-
if (request.method !== "POST") {
|
|
1420
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1421
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
await runPanelWrite(response, async () => {
|
|
1425
|
-
const body = await readJsonBody(request);
|
|
1426
|
-
const doi = typeof body.doi === "string" ? body.doi.trim() : "";
|
|
1427
|
-
if (!doi)
|
|
1428
|
-
throw new Error("doi is required");
|
|
1429
|
-
const result = await downloadReferencePdf(root, doi);
|
|
1430
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1431
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/reference-download/v1", ...result }));
|
|
1432
|
-
});
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
if (url.pathname === "/api/citations") {
|
|
1436
|
-
if (request.method === "GET") {
|
|
1437
|
-
const citations = await listCitationUses(root);
|
|
1438
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1439
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/citations/v1", citations }));
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
if (request.method !== "POST") {
|
|
1443
|
-
response.writeHead(405, { "content-type": "application/json", allow: "GET, POST" });
|
|
1444
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
await runPanelWrite(response, async () => {
|
|
1448
|
-
const body = await readJsonBody(request);
|
|
1449
|
-
const doi = typeof body.doi === "string" ? body.doi : "";
|
|
1450
|
-
const reason = typeof body.reason === "string" ? body.reason : "";
|
|
1451
|
-
const context = typeof body.context === "string" ? body.context : "";
|
|
1452
|
-
if (!doi || !reason || !context)
|
|
1453
|
-
throw new Error("doi, reason, and context are all required");
|
|
1454
|
-
const result = await recordCitationUse(root, {
|
|
1455
|
-
doi,
|
|
1456
|
-
reason,
|
|
1457
|
-
context,
|
|
1458
|
-
...(typeof body.section === "string" && body.section.trim() ? { section: body.section } : {}),
|
|
1459
|
-
...(typeof body.claimId === "string" && body.claimId.trim() ? { claimId: body.claimId } : {}),
|
|
1460
|
-
});
|
|
1461
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1462
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/citation-record/v1", ok: true, ...result }));
|
|
1463
|
-
});
|
|
1464
|
-
return;
|
|
1465
|
-
}
|
|
1466
|
-
if (url.pathname === "/api/recommended-skills") {
|
|
1467
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1468
|
-
response.end(JSON.stringify(await recommendedSkills()));
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
if (url.pathname === "/api/recommended-mcps") {
|
|
1472
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1473
|
-
response.end(JSON.stringify(await recommendedMcps()));
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
if (url.pathname === "/api/recommended-install-prep") {
|
|
1477
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1478
|
-
response.end(JSON.stringify(await recommendedInstallPrep()));
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
if (url.pathname === "/api/mcp-plan") {
|
|
1482
|
-
const id = url.searchParams.get("id");
|
|
1483
|
-
const catalog = await recommendedMcps();
|
|
1484
|
-
const item = catalog.items?.find((candidate) => candidate.id === id);
|
|
1485
|
-
if (!item) {
|
|
1486
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
1487
|
-
response.end(JSON.stringify({ error: "unknown MCP recommendation" }));
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1491
|
-
response.end(JSON.stringify({
|
|
1492
|
-
schemaVersion: "psyclaw/mcp-plan-view/v1",
|
|
1493
|
-
id: item.id,
|
|
1494
|
-
name: item.name,
|
|
1495
|
-
sourceRef: item.sourceRef,
|
|
1496
|
-
transport: item.transport,
|
|
1497
|
-
risk: item.risk,
|
|
1498
|
-
writeEffects: item.writeEffects,
|
|
1499
|
-
status: "plan-only",
|
|
1500
|
-
approval: "required",
|
|
1501
|
-
note: "Review source, version, license, dependencies, and tool allowlist before enabling.",
|
|
1502
|
-
}));
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
if (url.pathname === "/api/provider-config") {
|
|
1506
|
-
if (request.method !== "POST") {
|
|
1507
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1508
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
const body = await readJsonBody(request);
|
|
1512
|
-
const id = String(body.id ?? "").trim();
|
|
1513
|
-
const preset = PROVIDER_PRESETS.find((candidate) => candidate.id === id);
|
|
1514
|
-
const name = String(body.name ?? preset?.name ?? id).trim();
|
|
1515
|
-
const baseUrl = String(body.baseUrl ?? preset?.baseUrl ?? "").trim();
|
|
1516
|
-
const api = body.api === "anthropic-messages" ? "anthropic-messages" : "openai-completions";
|
|
1517
|
-
const apiKeyEnv = String(body.apiKeyEnv ?? preset?.apiKeyEnv ?? `PSYCLAW_${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`).trim();
|
|
1518
|
-
const modelId = String(body.modelId ?? "").trim();
|
|
1519
|
-
const models = Array.isArray(body.models)
|
|
1520
|
-
? body.models.filter((value) => typeof value === "string").map((value) => ({ id: value.trim(), name: value.trim() })).filter((value) => value.id)
|
|
1521
|
-
: (modelId ? [{ id: modelId, name: modelId }] : (preset?.models ?? []));
|
|
1522
|
-
const apiKey = typeof body.apiKey === "string" ? body.apiKey : undefined;
|
|
1523
|
-
if (!id || !name || !baseUrl || models.length === 0)
|
|
1524
|
-
throw new Error("id, name, baseUrl and at least one model are required");
|
|
1525
|
-
const result = await saveProviderConfig({
|
|
1526
|
-
id,
|
|
1527
|
-
name,
|
|
1528
|
-
baseUrl,
|
|
1529
|
-
api,
|
|
1530
|
-
apiKeyEnv,
|
|
1531
|
-
models,
|
|
1532
|
-
...(apiKey === undefined ? {} : { apiKey }),
|
|
1533
|
-
});
|
|
1534
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1535
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/provider-config-receipt/v1", ok: true, provider: id, modelCount: models.length, apiKeyStored: Boolean(apiKey?.trim()) }));
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
if (url.pathname === "/api/hitl/decision") {
|
|
1539
|
-
if (request.method !== "POST") {
|
|
1540
|
-
response.writeHead(405, { "content-type": "application/json", allow: "POST" });
|
|
1541
|
-
response.end(JSON.stringify({ error: "method not allowed" }));
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
const body = await readJsonBody(request);
|
|
1545
|
-
const decision = String(body.decision ?? "").trim();
|
|
1546
|
-
const rationale = String(body.rationale ?? "").trim();
|
|
1547
|
-
const actor = String(body.actor ?? "researcher").trim();
|
|
1548
|
-
if (!["approved", "denied", "needs-changes"].includes(decision) || rationale.length < 3 || actor.length < 1) {
|
|
1549
|
-
response.writeHead(400, { "content-type": "application/json" });
|
|
1550
|
-
response.end(JSON.stringify({ error: "decision, rationale (3+ chars), and actor are required" }));
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
const recordedAt = new Date().toISOString();
|
|
1554
|
-
const idempotencyKey = `panel:hitl:${sha256Text(`${decision}\u0000${rationale}\u0000${actor}`).slice(0, 24)}`;
|
|
1555
|
-
const decisionDocument = [
|
|
1556
|
-
"---",
|
|
1557
|
-
"schemaVersion: psyclaw/hitl-decision-request/v1",
|
|
1558
|
-
"documentVersion: 1.0.0",
|
|
1559
|
-
`recordedAt: ${recordedAt}`,
|
|
1560
|
-
`actor: ${actor.replaceAll("\n", " ")}`,
|
|
1561
|
-
`decision: ${decision}`,
|
|
1562
|
-
`idempotencyKey: ${idempotencyKey}`,
|
|
1563
|
-
"---",
|
|
1564
|
-
"",
|
|
1565
|
-
"# Decision Request",
|
|
1566
|
-
"",
|
|
1567
|
-
`Decision: ${decision}`,
|
|
1568
|
-
"",
|
|
1569
|
-
"## Rationale",
|
|
1570
|
-
"",
|
|
1571
|
-
rationale,
|
|
1572
|
-
"",
|
|
1573
|
-
].join("\n");
|
|
1574
|
-
const decisionPath = await assertSafeProjectPath(root, "notes/decision_request.md");
|
|
1575
|
-
await atomicWriteFile(decisionPath, decisionDocument);
|
|
1576
|
-
const receipt = {
|
|
1577
|
-
schemaVersion: "psyclaw/tool-receipt/v1",
|
|
1578
|
-
runId: `panel_${randomUUID().replaceAll("-", "")}`,
|
|
1579
|
-
taskId: "hitl-decision",
|
|
1580
|
-
tool: "panel.hitl.decision",
|
|
1581
|
-
effect: "write",
|
|
1582
|
-
approval: "approved",
|
|
1583
|
-
idempotencyKey,
|
|
1584
|
-
ok: true,
|
|
1585
|
-
resultHash: sha256Text(decisionDocument),
|
|
1586
|
-
startedAt: recordedAt,
|
|
1587
|
-
finishedAt: new Date().toISOString(),
|
|
1588
|
-
};
|
|
1589
|
-
const receiptPath = await assertSafeProjectPath(root, `.psyclaw/manifests/${receipt.runId}.receipt.json`);
|
|
1590
|
-
await atomicWriteFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
1591
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
1592
|
-
response.end(JSON.stringify({ schemaVersion: "psyclaw/hitl-decision-receipt/v1", ok: true, decision, decisionPath, receiptPath, idempotencyKey }));
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1596
|
-
const html = await readFile(panelHtmlPath, "utf8");
|
|
1597
|
-
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1598
|
-
response.end(html);
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
1602
|
-
response.end(JSON.stringify({ error: "not found" }));
|
|
1603
|
-
}
|
|
1604
|
-
catch {
|
|
1605
|
-
response.writeHead(500, { "content-type": "application/json" });
|
|
1606
|
-
response.end(JSON.stringify({ error: "panel request failed", reasonCode: "panel.request-failed" }));
|
|
1607
|
-
}
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
//# sourceMappingURL=server.js.map
|