jishushell 0.4.24 → 0.5.15
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/INSTALL-NOTICE +11 -0
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +90 -0
- package/apps/filebrowser-container.yaml +163 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +91 -90
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +91 -90
- package/apps/openclaw-binary.yaml +38 -1
- package/apps/openclaw-container.yaml +45 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +26 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +58 -7
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/app.js +79 -9
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/doctor.d.ts +12 -12
- package/dist/cli/doctor.js +242 -55
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/llm.d.ts +4 -3
- package/dist/cli/llm.js +4 -3
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/panel.d.ts +6 -5
- package/dist/cli/panel.js +10 -9
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.js.map +1 -1
- package/dist/install.js +3 -3
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +1 -1
- package/dist/routes/agent-apps.js +1 -1
- package/dist/routes/apps.js +44 -11
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +5 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +826 -17
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +10 -10
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +751 -20
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.js +4 -3
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/index.d.ts +1 -1
- package/dist/services/agent-apps/index.js +1 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
- package/dist/services/agent-apps/installers/adapter.js +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +3 -3
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +2 -2
- package/dist/services/agent-apps/types.js +1 -1
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +25 -1
- package/dist/services/app/app-manager.js +829 -150
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +7 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +20 -3
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app/provide-resolver.d.ts +29 -0
- package/dist/services/app/provide-resolver.js +112 -0
- package/dist/services/app/provide-resolver.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +177 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.d.ts +41 -0
- package/dist/services/capability-endpoint-validator.js +104 -0
- package/dist/services/capability-endpoint-validator.js.map +1 -0
- package/dist/services/capability-health.d.ts +16 -0
- package/dist/services/capability-health.js +121 -0
- package/dist/services/capability-health.js.map +1 -0
- package/dist/services/capability-registry.d.ts +106 -0
- package/dist/services/capability-registry.js +313 -0
- package/dist/services/capability-registry.js.map +1 -0
- package/dist/services/connection-apply.d.ts +91 -0
- package/dist/services/connection-apply.js +475 -0
- package/dist/services/connection-apply.js.map +1 -0
- package/dist/services/connection-resolver.d.ts +65 -0
- package/dist/services/connection-resolver.js +281 -0
- package/dist/services/connection-resolver.js.map +1 -0
- package/dist/services/connection-transactor.d.ts +39 -0
- package/dist/services/connection-transactor.js +351 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +138 -46
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +16 -2
- package/dist/services/llm-proxy/index.js +48 -44
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/probe.d.ts +6 -0
- package/dist/services/llm-proxy/probe.js +85 -0
- package/dist/services/llm-proxy/probe.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +1 -0
- package/dist/services/llm-proxy/ssrf.js +24 -9
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +428 -35
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +20 -1
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +30 -1
- package/dist/services/runtime/adapters/hermes.js +219 -6
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
- package/dist/services/runtime/adapters/openclaw.js +1171 -11
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
- package/dist/services/runtime/mcp-shims/firewall.js +129 -0
- package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +8 -0
- package/dist/services/runtime/migrations.js +100 -0
- package/dist/services/runtime/migrations.js.map +1 -1
- package/dist/services/runtime/types.d.ts +46 -0
- package/dist/services/setup-manager.js +99 -24
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.d.ts +27 -0
- package/dist/services/suggestions.js +133 -0
- package/dist/services/suggestions.js.map +1 -0
- package/dist/services/task-registry.js +4 -2
- package/dist/services/task-registry.js.map +1 -1
- package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
- package/dist/services/telemetry/device-fingerprint.js +1 -1
- package/dist/services/types-shim.d.ts +16 -0
- package/dist/services/types-shim.js +2 -0
- package/dist/services/types-shim.js.map +1 -0
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +231 -1
- package/dist/utils/instance-lock.d.ts +22 -0
- package/dist/utils/instance-lock.js +48 -0
- package/dist/utils/instance-lock.js.map +1 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +18 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
- package/public/assets/Settings-Cc-tYBil.js +1 -0
- package/public/assets/Setup-lGZEk5jq.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-BWnkJgZ1.js +2 -0
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +457 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/run.sh +4 -4
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
- package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
- package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
- package/public/assets/NewInstance-BCIrAd86.js +0 -1
- package/public/assets/Settings-xkDcduFz.js +0 -1
- package/public/assets/Setup-Cfuwj4gV.js +0 -1
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/index-DQsM6Joa.js +0 -19
- package/public/assets/providers-V-vwrExZ.js +0 -1
- package/public/assets/registry-B4UFJdpA.js +0 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getInstance, getInstanceRuntime, getOpenclawHome, } from "../services/instance-manager.js";
|
|
2
|
+
import { validateMounts, ensureMountTargets, } from "../services/files-mounts.js";
|
|
3
|
+
import { rebuildWorkspace } from "../services/workspace-builder.js";
|
|
4
|
+
import { FILES_ROOT, INSTANCES_DIR } from "../config.js";
|
|
5
|
+
import { safeWriteJson } from "../utils/safe-json.js";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
export async function fileMountsRoutes(app) {
|
|
8
|
+
// ── GET /api/instances/:id/file-mounts ──────────
|
|
9
|
+
app.get("/api/instances/:id/file-mounts", async (req, reply) => {
|
|
10
|
+
const meta = getInstance(req.params.id);
|
|
11
|
+
if (!meta) {
|
|
12
|
+
return reply.code(404).send({ error: "instance not found", code: "not-found" });
|
|
13
|
+
}
|
|
14
|
+
const runtime = getInstanceRuntime(req.params.id);
|
|
15
|
+
const mounts = Array.isArray(runtime.fileMounts)
|
|
16
|
+
? runtime.fileMounts
|
|
17
|
+
: Array.isArray(runtime.file_mounts)
|
|
18
|
+
? runtime.file_mounts
|
|
19
|
+
: [];
|
|
20
|
+
reply.send({ mounts });
|
|
21
|
+
});
|
|
22
|
+
// ── PUT /api/instances/:id/file-mounts ──────────
|
|
23
|
+
app.put("/api/instances/:id/file-mounts", async (req, reply) => {
|
|
24
|
+
const id = req.params.id;
|
|
25
|
+
const meta = getInstance(id);
|
|
26
|
+
if (!meta) {
|
|
27
|
+
return reply.code(404).send({ error: "instance not found", code: "not-found" });
|
|
28
|
+
}
|
|
29
|
+
const candidate = Array.isArray(req.body?.mounts) ? req.body.mounts : null;
|
|
30
|
+
if (!candidate) {
|
|
31
|
+
return reply.code(400).send({
|
|
32
|
+
error: "body.mounts must be an array",
|
|
33
|
+
code: "invalid",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const v = validateMounts(candidate, id);
|
|
37
|
+
if (!v.ok) {
|
|
38
|
+
return reply.code(400).send({
|
|
39
|
+
error: "invalid mount set",
|
|
40
|
+
code: "invalid",
|
|
41
|
+
errors: v.errors.map((e) => ({
|
|
42
|
+
mount_index: e.mountIndex,
|
|
43
|
+
message: e.message,
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Persist into instance.json (full runtime block; safeWriteJson does
|
|
48
|
+
// an atomic write).
|
|
49
|
+
const newRuntime = { ...meta.runtime, fileMounts: candidate };
|
|
50
|
+
// Drop any stale snake_case from earlier writes
|
|
51
|
+
delete newRuntime.file_mounts;
|
|
52
|
+
const newMeta = { ...meta, runtime: newRuntime };
|
|
53
|
+
const metaPath = join(INSTANCES_DIR, id, "instance.json");
|
|
54
|
+
safeWriteJson(metaPath, newMeta);
|
|
55
|
+
// Mounts targets must exist on host before docker tries to bind
|
|
56
|
+
ensureMountTargets(FILES_ROOT, candidate);
|
|
57
|
+
// Lay down workspace symlinks immediately so raw_exec / process
|
|
58
|
+
// mode picks up changes without a restart. Docker still needs a
|
|
59
|
+
// restart for the volume binds, signalled via requires_restart.
|
|
60
|
+
try {
|
|
61
|
+
const home = getOpenclawHome(id);
|
|
62
|
+
if (home) {
|
|
63
|
+
rebuildWorkspace({
|
|
64
|
+
openclawHome: home,
|
|
65
|
+
filesRoot: FILES_ROOT,
|
|
66
|
+
mounts: candidate,
|
|
67
|
+
instanceId: id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e?.reason === "needs-migration") {
|
|
73
|
+
return reply.code(409).send({
|
|
74
|
+
error: "instance workspace contains pre-W2 user data; run legacy migration first",
|
|
75
|
+
code: "needs-migration",
|
|
76
|
+
detail: e.message,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return reply.code(500).send({
|
|
80
|
+
error: e?.message ?? "workspace rebuild failed",
|
|
81
|
+
code: "rebuild-failed",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
reply.send({
|
|
85
|
+
mounts: candidate,
|
|
86
|
+
requires_restart: true,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=file-mounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-mounts.js","sourceRoot":"","sources":["../../src/routes/file-mounts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/files/organize/* routes (W1.5 PR-4).
|
|
3
|
+
*
|
|
4
|
+
* Mounted alongside /api/files via the same Fastify scope so they
|
|
5
|
+
* share path-safety, octet-stream parser etc. Auth piggybacks on the
|
|
6
|
+
* global onRequest hook in server.ts.
|
|
7
|
+
*
|
|
8
|
+
* Endpoints:
|
|
9
|
+
* POST /api/files/organize/scan run rules and return batch
|
|
10
|
+
* GET /api/files/organize/batches list recent batches
|
|
11
|
+
* GET /api/files/organize/batches/:id fetch one
|
|
12
|
+
* PUT /api/files/organize/batches/:id/apply apply (selectedIds in body)
|
|
13
|
+
* POST /api/files/organize/batches/:id/revert revert within undo window
|
|
14
|
+
* DELETE /api/files/organize/batches/:id mark expired (cancel)
|
|
15
|
+
* GET /api/files/organize/rules current rule file content
|
|
16
|
+
* PUT /api/files/organize/rules replace rule file
|
|
17
|
+
*/
|
|
18
|
+
import type { FastifyInstance } from "fastify";
|
|
19
|
+
import { FilesManager } from "../services/files-manager.js";
|
|
20
|
+
import { BatchStore } from "../services/organize/store.js";
|
|
21
|
+
export interface OrganizeRoutesDeps {
|
|
22
|
+
filesManager: FilesManager;
|
|
23
|
+
/** Defaults to a store rooted at JISHUSHELL_HOME. */
|
|
24
|
+
store?: BatchStore;
|
|
25
|
+
/** Override files root in tests. Defaults to FILES_ROOT. */
|
|
26
|
+
filesRoot?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function organizeRoutes(app: FastifyInstance, deps: OrganizeRoutesDeps): Promise<void>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { FilesError } from "../services/files-manager.js";
|
|
2
|
+
import { readRules, writeRules, validateRules, } from "../services/organize/rules.js";
|
|
3
|
+
import { scan } from "../services/organize/scanner.js";
|
|
4
|
+
import { applyBatch, revertBatch } from "../services/organize/applier.js";
|
|
5
|
+
import { BatchStore } from "../services/organize/store.js";
|
|
6
|
+
import { FILES_ROOT, JISHUSHELL_HOME } from "../config.js";
|
|
7
|
+
export async function organizeRoutes(app, deps) {
|
|
8
|
+
const filesRoot = deps.filesRoot ?? FILES_ROOT;
|
|
9
|
+
const store = deps.store ?? new BatchStore(JISHUSHELL_HOME);
|
|
10
|
+
const audit = (op, p, meta) => {
|
|
11
|
+
deps.filesManager.audit(op, p, meta);
|
|
12
|
+
};
|
|
13
|
+
// W2 PR-3: /api/files/organize/* organize batches are also panel-internal
|
|
14
|
+
// substrate for the drive_* MCP shim only. Browser users now manage files
|
|
15
|
+
// via the Filebrowser app mounted at /apps/filebrowser/*. Reject any
|
|
16
|
+
// request that didn't pass the X-Jishushell-Internal-Token check in
|
|
17
|
+
// server.ts auth middleware. Return 404 rather than 401 so unauthenticated
|
|
18
|
+
// probes can't fingerprint the surface.
|
|
19
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
20
|
+
if (!request.internalCallerInstance) {
|
|
21
|
+
return reply.status(404).send();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
// ── POST /api/files/organize/scan ───────────────
|
|
25
|
+
app.post("/api/files/organize/scan", async (req, reply) => {
|
|
26
|
+
const path = typeof req.body?.path === "string" ? req.body.path : "";
|
|
27
|
+
try {
|
|
28
|
+
const { rules } = await readRules(filesRoot);
|
|
29
|
+
const batch = await scan(filesRoot, rules, path);
|
|
30
|
+
await store.put(batch);
|
|
31
|
+
reply.send(batch);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
sendOrganizeError(reply, e);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// ── GET /api/files/organize/batches ─────────────
|
|
38
|
+
app.get("/api/files/organize/batches", async (_req, reply) => {
|
|
39
|
+
reply.send({ batches: store.list() });
|
|
40
|
+
});
|
|
41
|
+
// ── GET /api/files/organize/batches/:id ─────────
|
|
42
|
+
app.get("/api/files/organize/batches/:id", async (req, reply) => {
|
|
43
|
+
const b = store.get(req.params.id);
|
|
44
|
+
if (!b)
|
|
45
|
+
return reply.code(404).send({ error: "batch not found", code: "not-found" });
|
|
46
|
+
reply.send(b);
|
|
47
|
+
});
|
|
48
|
+
// ── PUT /api/files/organize/batches/:id/apply ───
|
|
49
|
+
app.put("/api/files/organize/batches/:id/apply", async (req, reply) => {
|
|
50
|
+
const batch = store.get(req.params.id);
|
|
51
|
+
if (!batch) {
|
|
52
|
+
return reply.code(404).send({ error: "batch not found", code: "not-found" });
|
|
53
|
+
}
|
|
54
|
+
if (batch.status !== "pending") {
|
|
55
|
+
return reply.code(409).send({
|
|
56
|
+
error: `batch is ${batch.status}, not pending`,
|
|
57
|
+
code: "bad-state",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const selectedIds = new Set(req.body?.selected_ids ?? []);
|
|
61
|
+
// Re-read rules to get on_collision settings — and detect if rules
|
|
62
|
+
// changed since scan (rules_hash mismatch).
|
|
63
|
+
const { rules, hash } = await readRules(filesRoot);
|
|
64
|
+
if (hash !== batch.rules_hash) {
|
|
65
|
+
return reply.code(409).send({
|
|
66
|
+
error: "rules changed since scan, please re-scan",
|
|
67
|
+
code: "rules-changed",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const rulesById = new Map();
|
|
71
|
+
for (const r of rules)
|
|
72
|
+
rulesById.set(r.id, r);
|
|
73
|
+
try {
|
|
74
|
+
const { report } = await applyBatch(batch, {
|
|
75
|
+
filesRoot,
|
|
76
|
+
rulesById,
|
|
77
|
+
selectedIds,
|
|
78
|
+
audit,
|
|
79
|
+
});
|
|
80
|
+
await store.put(batch);
|
|
81
|
+
reply.send({ batch, report });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
sendOrganizeError(reply, e);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// ── POST /api/files/organize/batches/:id/revert ─
|
|
88
|
+
app.post("/api/files/organize/batches/:id/revert", async (req, reply) => {
|
|
89
|
+
const batch = store.get(req.params.id);
|
|
90
|
+
if (!batch) {
|
|
91
|
+
return reply.code(404).send({ error: "batch not found", code: "not-found" });
|
|
92
|
+
}
|
|
93
|
+
if (batch.status !== "applied") {
|
|
94
|
+
return reply.code(409).send({
|
|
95
|
+
error: `batch is ${batch.status}, not applied`,
|
|
96
|
+
code: "bad-state",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const { report } = await revertBatch(batch, { filesRoot, audit });
|
|
101
|
+
await store.put(batch);
|
|
102
|
+
reply.send({ batch, report });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (e?.code === "expired") {
|
|
106
|
+
await store.put(batch);
|
|
107
|
+
return reply.code(410).send({
|
|
108
|
+
error: "undo window has expired",
|
|
109
|
+
code: "expired",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
sendOrganizeError(reply, e);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// ── DELETE /api/files/organize/batches/:id ──────
|
|
116
|
+
app.delete("/api/files/organize/batches/:id", async (req, reply) => {
|
|
117
|
+
const batch = store.get(req.params.id);
|
|
118
|
+
if (!batch)
|
|
119
|
+
return reply.code(204).send();
|
|
120
|
+
await store.update(req.params.id, (b) => {
|
|
121
|
+
if (b.status === "pending")
|
|
122
|
+
b.status = "expired";
|
|
123
|
+
});
|
|
124
|
+
reply.code(204).send();
|
|
125
|
+
});
|
|
126
|
+
// ── GET /api/files/organize/rules ───────────────
|
|
127
|
+
app.get("/api/files/organize/rules", async (_req, reply) => {
|
|
128
|
+
try {
|
|
129
|
+
const { rules, source, hash } = await readRules(filesRoot);
|
|
130
|
+
reply.send({ rules, source, hash });
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
sendOrganizeError(reply, e);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// ── PUT /api/files/organize/rules ───────────────
|
|
137
|
+
app.put("/api/files/organize/rules", async (req, reply) => {
|
|
138
|
+
const result = validateRules({
|
|
139
|
+
version: 1,
|
|
140
|
+
rules: req.body?.rules ?? [],
|
|
141
|
+
});
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
return reply.code(400).send({
|
|
144
|
+
error: "invalid rules",
|
|
145
|
+
code: "invalid",
|
|
146
|
+
errors: result.errors,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await writeRules(filesRoot, result.valid_rules);
|
|
151
|
+
reply.send({ rules: result.valid_rules });
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
sendOrganizeError(reply, e);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function sendOrganizeError(reply, e) {
|
|
159
|
+
if (e instanceof FilesError) {
|
|
160
|
+
reply.code(e.httpStatus).send({ error: e.message, code: e.code });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const err = e;
|
|
164
|
+
console.error("[organize] unexpected:", err.message);
|
|
165
|
+
reply.code(500).send({ error: err.message ?? "internal error", code: err.code ?? "internal" });
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=files-organize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files-organize.js","sourceRoot":"","sources":["../../src/routes/files-organize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/files routes (M1 W1 PR-4).
|
|
3
|
+
*
|
|
4
|
+
* Mounted as a Fastify plugin so that the route-scoped octet-stream parser
|
|
5
|
+
* does not leak globally. The parser transparently passes the raw payload
|
|
6
|
+
* stream to handlers (no buffering), letting `FilesManager.writeStream`
|
|
7
|
+
* pipe straight to disk.
|
|
8
|
+
*
|
|
9
|
+
* Endpoints:
|
|
10
|
+
* GET /api/files list directory
|
|
11
|
+
* GET /api/files/raw stream file content
|
|
12
|
+
* GET /api/files/preview small text/image read (truncated)
|
|
13
|
+
* GET /api/files/quota quota info
|
|
14
|
+
* PUT /api/files upload (octet-stream only)
|
|
15
|
+
* POST /api/files/mkdir create directory
|
|
16
|
+
* POST /api/files/move rename / move
|
|
17
|
+
* DELETE /api/files soft-delete to .trash/
|
|
18
|
+
* GET /api/files/search full-text search index
|
|
19
|
+
* GET /api/files/meta read per-file sidecar metadata
|
|
20
|
+
* PUT /api/files/meta write/merge per-file sidecar metadata
|
|
21
|
+
* POST /api/files/reindex rebuild search index
|
|
22
|
+
* GET /api/files/resolve relative path → absolute host path
|
|
23
|
+
*
|
|
24
|
+
* Auth: covered by the global onRequest hook in server.ts.
|
|
25
|
+
*/
|
|
26
|
+
import type { FastifyInstance } from "fastify";
|
|
27
|
+
import { FilesManager } from "../services/files-manager.js";
|
|
28
|
+
export interface FilesRoutesDeps {
|
|
29
|
+
filesManager: FilesManager;
|
|
30
|
+
}
|
|
31
|
+
export declare function filesRoutes(app: FastifyInstance, deps: FilesRoutesDeps): Promise<void>;
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { FilesError, } from "../services/files-manager.js";
|
|
2
|
+
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // matches FilesManager default
|
|
3
|
+
export async function filesRoutes(app, deps) {
|
|
4
|
+
const { filesManager } = deps;
|
|
5
|
+
// ── Route-scoped parser ───────────────────────────
|
|
6
|
+
// Pass-through: hand the raw payload stream straight to the handler.
|
|
7
|
+
// Do NOT use parseAs:'buffer' — that would read the whole body into memory.
|
|
8
|
+
app.addContentTypeParser("application/octet-stream", (_req, payload, done) => done(null, payload));
|
|
9
|
+
// W2 PR-3: /api/files/* is the panel-internal substrate for the drive_*
|
|
10
|
+
// MCP shim only. Browser users now manage files via the Filebrowser app
|
|
11
|
+
// mounted at /apps/filebrowser/*. Reject any request that didn't pass
|
|
12
|
+
// the X-Jishushell-Internal-Token check in server.ts auth middleware.
|
|
13
|
+
// Return 404 rather than 401 so unauthenticated probes can't fingerprint
|
|
14
|
+
// the surface.
|
|
15
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
16
|
+
if (!request.internalCallerInstance) {
|
|
17
|
+
return reply.status(404).send();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
// ── GET /api/files (list) ─────────────────────────
|
|
21
|
+
app.get("/api/files", async (req, reply) => {
|
|
22
|
+
const path = decodeOrEmpty(req.query.path);
|
|
23
|
+
const showHidden = req.query.show_hidden === "true";
|
|
24
|
+
try {
|
|
25
|
+
const result = await filesManager.list(path, { showHidden });
|
|
26
|
+
reply.send(result);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
sendFilesError(reply, e);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// ── GET /api/files/raw ────────────────────────────
|
|
33
|
+
app.get("/api/files/raw", async (req, reply) => {
|
|
34
|
+
const path = decodeOrEmpty(req.query.path);
|
|
35
|
+
try {
|
|
36
|
+
// readStream returns a lazy stream factory — no fd opened yet
|
|
37
|
+
const r = await filesManager.readStream(path);
|
|
38
|
+
// Conditional GET (weak ETag) — answer without opening the file
|
|
39
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
40
|
+
if (typeof ifNoneMatch === "string" && weakETagMatch(ifNoneMatch, r.etag)) {
|
|
41
|
+
reply.code(304);
|
|
42
|
+
reply.header("ETag", r.etag);
|
|
43
|
+
return reply.send();
|
|
44
|
+
}
|
|
45
|
+
reply.header("Content-Type", r.mime);
|
|
46
|
+
reply.header("Content-Length", String(r.size));
|
|
47
|
+
reply.header("ETag", r.etag);
|
|
48
|
+
reply.header("Last-Modified", new Date(r.mtime * 1000).toUTCString());
|
|
49
|
+
reply.header("Cache-Control", "private, max-age=0");
|
|
50
|
+
reply.header("Content-Disposition", `inline; filename*=UTF-8''${encodeRfc5987(basename(path))}`);
|
|
51
|
+
// Open the fd only now, just before piping
|
|
52
|
+
return reply.send(r.openStream());
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
sendFilesError(reply, e);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// ── GET /api/files/preview ────────────────────────
|
|
59
|
+
app.get("/api/files/preview", async (req, reply) => {
|
|
60
|
+
const path = decodeOrEmpty(req.query.path);
|
|
61
|
+
const maxKb = parsePositiveInt(req.query.max_kb, 256) ?? 256;
|
|
62
|
+
const maxBytes = maxKb * 1024;
|
|
63
|
+
try {
|
|
64
|
+
const r = await filesManager.readSmall(path, maxBytes);
|
|
65
|
+
// Response is a JSON envelope; the underlying file's mime is in the body.
|
|
66
|
+
reply.header("ETag", r.etag);
|
|
67
|
+
reply.send({
|
|
68
|
+
path,
|
|
69
|
+
mime: r.mime,
|
|
70
|
+
size: r.size,
|
|
71
|
+
truncated: r.truncated,
|
|
72
|
+
content: r.buf.toString("utf8"),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
sendFilesError(reply, e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// ── GET /api/files/quota ──────────────────────────
|
|
80
|
+
app.get("/api/files/quota", async (_req, reply) => {
|
|
81
|
+
try {
|
|
82
|
+
const q = await filesManager.quota();
|
|
83
|
+
reply.send(q);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
sendFilesError(reply, e);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// ── PUT /api/files (upload) ───────────────────────
|
|
90
|
+
app.put("/api/files", { bodyLimit: MAX_UPLOAD_BYTES }, async (req, reply) => {
|
|
91
|
+
// Reject non-octet-stream up front for a clean 415
|
|
92
|
+
const ct = (req.headers["content-type"] || "")
|
|
93
|
+
.split(";")[0]
|
|
94
|
+
.trim()
|
|
95
|
+
.toLowerCase();
|
|
96
|
+
if (ct !== "application/octet-stream") {
|
|
97
|
+
return reply
|
|
98
|
+
.code(415)
|
|
99
|
+
.send({
|
|
100
|
+
error: "Content-Type must be application/octet-stream",
|
|
101
|
+
code: "unsupported-media-type",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const path = decodeOrEmpty(req.query.path);
|
|
105
|
+
const overwrite = req.query.overwrite === "true";
|
|
106
|
+
const ifMatch = req.headers["if-match"];
|
|
107
|
+
// Content-Length is mandatory: chunked uploads (no length) would let
|
|
108
|
+
// a client stream gigabytes before we hit the in-flight size guard,
|
|
109
|
+
// and the resulting "write past limit then soft-delete" path is
|
|
110
|
+
// dangerous when disk is near-full. Reject up front per spec.
|
|
111
|
+
const cl = req.headers["content-length"];
|
|
112
|
+
const clNum = typeof cl === "string" ? Number.parseInt(cl, 10) : Number.NaN;
|
|
113
|
+
if (!Number.isFinite(clNum) || clNum < 0) {
|
|
114
|
+
return reply.code(411).send({
|
|
115
|
+
error: "Content-Length header is required",
|
|
116
|
+
code: "length-required",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (clNum > MAX_UPLOAD_BYTES) {
|
|
120
|
+
return reply.code(413).send({
|
|
121
|
+
error: `Content-Length ${clNum} exceeds max ${MAX_UPLOAD_BYTES}`,
|
|
122
|
+
code: "too-large",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// The route-scoped parser passes payload as the request body (Readable)
|
|
126
|
+
const body = req.body;
|
|
127
|
+
if (!body) {
|
|
128
|
+
return reply
|
|
129
|
+
.code(400)
|
|
130
|
+
.send({ error: "missing request body", code: "no-body" });
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const result = await filesManager.writeStream(path, body, {
|
|
134
|
+
overwrite,
|
|
135
|
+
ifMatch: typeof ifMatch === "string" ? ifMatch : undefined,
|
|
136
|
+
expectedSize: clNum,
|
|
137
|
+
});
|
|
138
|
+
reply.code(overwrite ? 200 : 201).send({
|
|
139
|
+
path,
|
|
140
|
+
etag: result.etag,
|
|
141
|
+
size: result.size,
|
|
142
|
+
mime: result.mime,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
sendFilesError(reply, e);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// ── POST /api/files/mkdir ─────────────────────────
|
|
150
|
+
app.post("/api/files/mkdir", async (req, reply) => {
|
|
151
|
+
const p = req.body?.path ?? "";
|
|
152
|
+
try {
|
|
153
|
+
await filesManager.mkdir(p);
|
|
154
|
+
reply.code(201).send({ path: p });
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
sendFilesError(reply, e);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// ── POST /api/files/move ──────────────────────────
|
|
161
|
+
app.post("/api/files/move", async (req, reply) => {
|
|
162
|
+
const from = req.body?.from ?? "";
|
|
163
|
+
const to = req.body?.to ?? "";
|
|
164
|
+
const overwrite = req.body?.overwrite === true;
|
|
165
|
+
try {
|
|
166
|
+
await filesManager.move(from, to, overwrite);
|
|
167
|
+
reply.send({ from, to });
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
sendFilesError(reply, e);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// ── DELETE /api/files ─────────────────────────────
|
|
174
|
+
app.delete("/api/files", async (req, reply) => {
|
|
175
|
+
const path = decodeOrEmpty(req.query.path);
|
|
176
|
+
try {
|
|
177
|
+
await filesManager.remove(path);
|
|
178
|
+
reply.code(204).send();
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
sendFilesError(reply, e);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// ── GET /api/files/search ─────────────────────────
|
|
185
|
+
app.get("/api/files/search", async (req, reply) => {
|
|
186
|
+
const q = (req.query.q ?? "").trim();
|
|
187
|
+
if (!q) {
|
|
188
|
+
return reply.code(400).send({ detail: "q required" });
|
|
189
|
+
}
|
|
190
|
+
const rawLimit = parsePositiveInt(req.query.limit, 20) ?? 20;
|
|
191
|
+
const limit = Math.min(Math.max(rawLimit, 1), 100);
|
|
192
|
+
const pathPrefix = decodeOrEmpty(req.query.path) || undefined;
|
|
193
|
+
try {
|
|
194
|
+
const hits = await filesManager.searchIndex(q, { limit, pathPrefix });
|
|
195
|
+
reply.send({ hits });
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
sendFilesError(reply, e);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
// ── GET /api/files/meta ───────────────────────────
|
|
202
|
+
app.get("/api/files/meta", async (req, reply) => {
|
|
203
|
+
const { sha, path } = req.query;
|
|
204
|
+
if (sha && path) {
|
|
205
|
+
return reply.code(400).send({ detail: "provide sha or path, not both" });
|
|
206
|
+
}
|
|
207
|
+
if (!sha && !path) {
|
|
208
|
+
return reply.code(400).send({ detail: "sha or path required" });
|
|
209
|
+
}
|
|
210
|
+
if (sha && !/^[a-f0-9]{64}$/i.test(sha)) {
|
|
211
|
+
return reply.code(400).send({ detail: "sha must be a 64-char hex string" });
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const meta = await filesManager.getMeta({
|
|
215
|
+
sha256: sha,
|
|
216
|
+
path: path ? decodeOrEmpty(path) : undefined,
|
|
217
|
+
});
|
|
218
|
+
reply.send(meta);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
sendFilesError(reply, e);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// ── PUT /api/files/meta ───────────────────────────
|
|
225
|
+
// 96KB body cap: Agent A enforces 64KB on the serialized result;
|
|
226
|
+
// the slack covers merged result growth.
|
|
227
|
+
app.put("/api/files/meta", { bodyLimit: 96 * 1024 }, async (req, reply) => {
|
|
228
|
+
const { sha } = req.query;
|
|
229
|
+
if (!sha) {
|
|
230
|
+
return reply.code(400).send({ detail: "sha required" });
|
|
231
|
+
}
|
|
232
|
+
if (!/^[a-f0-9]{64}$/i.test(sha)) {
|
|
233
|
+
return reply.code(400).send({ detail: "sha must be a 64-char hex string" });
|
|
234
|
+
}
|
|
235
|
+
const merge = (req.query.merge ?? "").toLowerCase() !== "false";
|
|
236
|
+
const body = req.body;
|
|
237
|
+
if (body === null ||
|
|
238
|
+
body === undefined ||
|
|
239
|
+
typeof body !== "object" ||
|
|
240
|
+
Array.isArray(body)) {
|
|
241
|
+
return reply.code(400).send({ detail: "body must be a JSON object" });
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const result = await filesManager.setMeta(sha, body, { merge });
|
|
245
|
+
reply.send(result);
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
if (e instanceof FilesError && e.httpStatus === 413) {
|
|
249
|
+
return reply.code(413).send({ error: e.message, code: e.code });
|
|
250
|
+
}
|
|
251
|
+
sendFilesError(reply, e);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
// ── POST /api/files/reindex ───────────────────────
|
|
255
|
+
// Potentially long-running — no timeout; let it finish.
|
|
256
|
+
app.post("/api/files/reindex", async (req, reply) => {
|
|
257
|
+
const pathPrefix = decodeOrEmpty(req.query.path) || undefined;
|
|
258
|
+
try {
|
|
259
|
+
const result = await filesManager.reindex({ pathPrefix });
|
|
260
|
+
reply.send(result);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
sendFilesError(reply, e);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// ── GET /api/files/resolve ────────────────────────
|
|
267
|
+
// Returns the absolute host filesystem path for a files/-relative path,
|
|
268
|
+
// plus existence + stat. Used by the drive MCP shim so agents can hand
|
|
269
|
+
// a real filesystem path to IM channel plugins (Feishu / WeChat / ...)
|
|
270
|
+
// that send files. Host==container by buildVolumes(), so the same
|
|
271
|
+
// string is valid inside the OpenClaw container.
|
|
272
|
+
app.get("/api/files/resolve", async (req, reply) => {
|
|
273
|
+
const rel = decodeOrEmpty(req.query.path);
|
|
274
|
+
try {
|
|
275
|
+
const info = await filesManager.resolveLocalPath(rel);
|
|
276
|
+
reply.send(info);
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
sendFilesError(reply, e);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// ── Helpers ──────────────────────────────────────────
|
|
284
|
+
function decodeOrEmpty(input) {
|
|
285
|
+
if (!input)
|
|
286
|
+
return "";
|
|
287
|
+
// Fastify already URL-decodes query strings; this is just a defensive type guard.
|
|
288
|
+
return input;
|
|
289
|
+
}
|
|
290
|
+
function parsePositiveInt(s, def) {
|
|
291
|
+
if (s === undefined || s === null)
|
|
292
|
+
return def;
|
|
293
|
+
const n = Number.parseInt(s, 10);
|
|
294
|
+
if (Number.isFinite(n) && n >= 0)
|
|
295
|
+
return n;
|
|
296
|
+
return def;
|
|
297
|
+
}
|
|
298
|
+
function basename(p) {
|
|
299
|
+
if (!p)
|
|
300
|
+
return "";
|
|
301
|
+
const idx = p.lastIndexOf("/");
|
|
302
|
+
return idx === -1 ? p : p.slice(idx + 1);
|
|
303
|
+
}
|
|
304
|
+
function encodeRfc5987(s) {
|
|
305
|
+
// RFC 5987 encoding for filename* parameter
|
|
306
|
+
return encodeURIComponent(s).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
307
|
+
}
|
|
308
|
+
function weakETagMatch(provided, current) {
|
|
309
|
+
const strip = (x) => x.replace(/^W\//, "");
|
|
310
|
+
return strip(provided) === strip(current);
|
|
311
|
+
}
|
|
312
|
+
function sendFilesError(reply, e) {
|
|
313
|
+
if (e instanceof FilesError) {
|
|
314
|
+
reply.code(e.httpStatus).send({ error: e.message, code: e.code });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const err = e;
|
|
318
|
+
console.error("[files-routes] unexpected:", err.message);
|
|
319
|
+
reply.code(500).send({ error: "internal error", code: err.code ?? "internal" });
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=files.js.map
|