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.
Files changed (281) hide show
  1. package/INSTALL-NOTICE +11 -0
  2. package/apps/anythingllm-container.yaml +287 -0
  3. package/apps/browserless-chromium-container.yaml +90 -0
  4. package/apps/filebrowser-container.yaml +163 -0
  5. package/apps/hermes-container.yaml +36 -2
  6. package/apps/ollama-binary.yaml +91 -90
  7. package/apps/ollama-cpu-container.yaml +8 -1
  8. package/apps/ollama-with-hollama-binary.yaml +91 -90
  9. package/apps/openclaw-binary.yaml +38 -1
  10. package/apps/openclaw-container.yaml +45 -2
  11. package/apps/openclaw-with-ollama-container.yaml +11 -2
  12. package/apps/openclaw-with-searxng-container.yaml +26 -2
  13. package/apps/openwebui-container.yaml +45 -1
  14. package/apps/playwright-container.yaml +7 -1
  15. package/apps/searxng-container.yaml +58 -7
  16. package/apps/weknora-container.yaml +471 -0
  17. package/dist/cli/app.js +79 -9
  18. package/dist/cli/app.js.map +1 -1
  19. package/dist/cli/doctor.d.ts +12 -12
  20. package/dist/cli/doctor.js +242 -55
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/llm.d.ts +4 -3
  23. package/dist/cli/llm.js +4 -3
  24. package/dist/cli/llm.js.map +1 -1
  25. package/dist/cli/panel.d.ts +6 -5
  26. package/dist/cli/panel.js +10 -9
  27. package/dist/cli/panel.js.map +1 -1
  28. package/dist/config.d.ts +19 -0
  29. package/dist/config.js +99 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/control.d.ts +7 -6
  32. package/dist/control.js +7 -6
  33. package/dist/control.js.map +1 -1
  34. package/dist/install.js +3 -3
  35. package/dist/install.js.map +1 -1
  36. package/dist/routes/agent-apps.d.ts +1 -1
  37. package/dist/routes/agent-apps.js +1 -1
  38. package/dist/routes/apps.js +44 -11
  39. package/dist/routes/apps.js.map +1 -1
  40. package/dist/routes/auth.js +5 -2
  41. package/dist/routes/auth.js.map +1 -1
  42. package/dist/routes/backup.js +64 -11
  43. package/dist/routes/backup.js.map +1 -1
  44. package/dist/routes/external-mounts.d.ts +17 -0
  45. package/dist/routes/external-mounts.js +73 -0
  46. package/dist/routes/external-mounts.js.map +1 -0
  47. package/dist/routes/file-mounts.d.ts +13 -0
  48. package/dist/routes/file-mounts.js +90 -0
  49. package/dist/routes/file-mounts.js.map +1 -0
  50. package/dist/routes/files-organize.d.ts +28 -0
  51. package/dist/routes/files-organize.js +167 -0
  52. package/dist/routes/files-organize.js.map +1 -0
  53. package/dist/routes/files.d.ts +31 -0
  54. package/dist/routes/files.js +321 -0
  55. package/dist/routes/files.js.map +1 -0
  56. package/dist/routes/instances.js +826 -17
  57. package/dist/routes/instances.js.map +1 -1
  58. package/dist/routes/internal.d.ts +2 -0
  59. package/dist/routes/internal.js +59 -0
  60. package/dist/routes/internal.js.map +1 -0
  61. package/dist/routes/llm.js +24 -35
  62. package/dist/routes/llm.js.map +1 -1
  63. package/dist/routes/setup.js +10 -10
  64. package/dist/routes/setup.js.map +1 -1
  65. package/dist/routes/system.js +1 -1
  66. package/dist/routes/system.js.map +1 -1
  67. package/dist/routes/webdav.d.ts +17 -0
  68. package/dist/routes/webdav.js +114 -0
  69. package/dist/routes/webdav.js.map +1 -0
  70. package/dist/server.d.ts +9 -0
  71. package/dist/server.js +751 -20
  72. package/dist/server.js.map +1 -1
  73. package/dist/services/agent-apps/catalog.js +4 -3
  74. package/dist/services/agent-apps/catalog.js.map +1 -1
  75. package/dist/services/agent-apps/index.d.ts +1 -1
  76. package/dist/services/agent-apps/index.js +1 -1
  77. package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
  78. package/dist/services/agent-apps/installers/adapter.js +1 -1
  79. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  80. package/dist/services/agent-apps/installers/shell-script.js +3 -3
  81. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  82. package/dist/services/agent-apps/types.d.ts +2 -2
  83. package/dist/services/agent-apps/types.js +1 -1
  84. package/dist/services/app/app-compiler.d.ts +1 -1
  85. package/dist/services/app/app-compiler.js +5 -5
  86. package/dist/services/app/app-compiler.js.map +1 -1
  87. package/dist/services/app/app-manager.d.ts +25 -1
  88. package/dist/services/app/app-manager.js +829 -150
  89. package/dist/services/app/app-manager.js.map +1 -1
  90. package/dist/services/app/custom-manager.js.map +1 -1
  91. package/dist/services/app/hermes-agent-manager.js +7 -4
  92. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  93. package/dist/services/app/ollama-manager.js +1 -1
  94. package/dist/services/app/ollama-manager.js.map +1 -1
  95. package/dist/services/app/openclaw-manager.js +20 -3
  96. package/dist/services/app/openclaw-manager.js.map +1 -1
  97. package/dist/services/app/platform-transform.d.ts +32 -0
  98. package/dist/services/app/platform-transform.js +65 -0
  99. package/dist/services/app/platform-transform.js.map +1 -0
  100. package/dist/services/app/provide-resolver.d.ts +29 -0
  101. package/dist/services/app/provide-resolver.js +112 -0
  102. package/dist/services/app/provide-resolver.js.map +1 -0
  103. package/dist/services/app-passwords.d.ts +61 -0
  104. package/dist/services/app-passwords.js +173 -0
  105. package/dist/services/app-passwords.js.map +1 -0
  106. package/dist/services/backup-manager.d.ts +11 -0
  107. package/dist/services/backup-manager.js +177 -4
  108. package/dist/services/backup-manager.js.map +1 -1
  109. package/dist/services/capability-endpoint-validator.d.ts +41 -0
  110. package/dist/services/capability-endpoint-validator.js +104 -0
  111. package/dist/services/capability-endpoint-validator.js.map +1 -0
  112. package/dist/services/capability-health.d.ts +16 -0
  113. package/dist/services/capability-health.js +121 -0
  114. package/dist/services/capability-health.js.map +1 -0
  115. package/dist/services/capability-registry.d.ts +106 -0
  116. package/dist/services/capability-registry.js +313 -0
  117. package/dist/services/capability-registry.js.map +1 -0
  118. package/dist/services/connection-apply.d.ts +91 -0
  119. package/dist/services/connection-apply.js +475 -0
  120. package/dist/services/connection-apply.js.map +1 -0
  121. package/dist/services/connection-resolver.d.ts +65 -0
  122. package/dist/services/connection-resolver.js +281 -0
  123. package/dist/services/connection-resolver.js.map +1 -0
  124. package/dist/services/connection-transactor.d.ts +39 -0
  125. package/dist/services/connection-transactor.js +351 -0
  126. package/dist/services/connection-transactor.js.map +1 -0
  127. package/dist/services/external-mounts.d.ts +40 -0
  128. package/dist/services/external-mounts.js +187 -0
  129. package/dist/services/external-mounts.js.map +1 -0
  130. package/dist/services/files-manager.d.ts +252 -0
  131. package/dist/services/files-manager.js +1075 -0
  132. package/dist/services/files-manager.js.map +1 -0
  133. package/dist/services/files-mounts.d.ts +42 -0
  134. package/dist/services/files-mounts.js +207 -0
  135. package/dist/services/files-mounts.js.map +1 -0
  136. package/dist/services/instance-manager.d.ts +13 -0
  137. package/dist/services/instance-manager.js +138 -46
  138. package/dist/services/instance-manager.js.map +1 -1
  139. package/dist/services/llm-proxy/index.d.ts +16 -2
  140. package/dist/services/llm-proxy/index.js +48 -44
  141. package/dist/services/llm-proxy/index.js.map +1 -1
  142. package/dist/services/llm-proxy/probe.d.ts +6 -0
  143. package/dist/services/llm-proxy/probe.js +85 -0
  144. package/dist/services/llm-proxy/probe.js.map +1 -0
  145. package/dist/services/llm-proxy/ssrf.d.ts +1 -0
  146. package/dist/services/llm-proxy/ssrf.js +24 -9
  147. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  148. package/dist/services/nomad-manager.d.ts +4 -0
  149. package/dist/services/nomad-manager.js +428 -35
  150. package/dist/services/nomad-manager.js.map +1 -1
  151. package/dist/services/organize/applier.d.ts +46 -0
  152. package/dist/services/organize/applier.js +218 -0
  153. package/dist/services/organize/applier.js.map +1 -0
  154. package/dist/services/organize/rules.d.ts +57 -0
  155. package/dist/services/organize/rules.js +286 -0
  156. package/dist/services/organize/rules.js.map +1 -0
  157. package/dist/services/organize/scanner.d.ts +50 -0
  158. package/dist/services/organize/scanner.js +366 -0
  159. package/dist/services/organize/scanner.js.map +1 -0
  160. package/dist/services/organize/store.d.ts +14 -0
  161. package/dist/services/organize/store.js +82 -0
  162. package/dist/services/organize/store.js.map +1 -0
  163. package/dist/services/panel-manager.js +20 -1
  164. package/dist/services/panel-manager.js.map +1 -1
  165. package/dist/services/process-manager.js +4 -3
  166. package/dist/services/process-manager.js.map +1 -1
  167. package/dist/services/runtime/adapters/hermes.d.ts +30 -1
  168. package/dist/services/runtime/adapters/hermes.js +219 -6
  169. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  170. package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
  171. package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
  172. package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
  173. package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
  174. package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
  175. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
  176. package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
  177. package/dist/services/runtime/adapters/openclaw.js +1171 -11
  178. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  179. package/dist/services/runtime/instance.d.ts +1 -1
  180. package/dist/services/runtime/instance.js +1 -1
  181. package/dist/services/runtime/instance.js.map +1 -1
  182. package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
  183. package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
  184. package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
  185. package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
  186. package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
  187. package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
  188. package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
  189. package/dist/services/runtime/mcp-shims/firewall.js +129 -0
  190. package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
  191. package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
  192. package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
  193. package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
  194. package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
  195. package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
  196. package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
  197. package/dist/services/runtime/migrations.d.ts +8 -0
  198. package/dist/services/runtime/migrations.js +100 -0
  199. package/dist/services/runtime/migrations.js.map +1 -1
  200. package/dist/services/runtime/types.d.ts +46 -0
  201. package/dist/services/setup-manager.js +99 -24
  202. package/dist/services/setup-manager.js.map +1 -1
  203. package/dist/services/suggestions.d.ts +27 -0
  204. package/dist/services/suggestions.js +133 -0
  205. package/dist/services/suggestions.js.map +1 -0
  206. package/dist/services/task-registry.js +4 -2
  207. package/dist/services/task-registry.js.map +1 -1
  208. package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
  209. package/dist/services/telemetry/device-fingerprint.js +1 -1
  210. package/dist/services/types-shim.d.ts +16 -0
  211. package/dist/services/types-shim.js +2 -0
  212. package/dist/services/types-shim.js.map +1 -0
  213. package/dist/services/webdav/server.d.ts +24 -0
  214. package/dist/services/webdav/server.js +420 -0
  215. package/dist/services/webdav/server.js.map +1 -0
  216. package/dist/services/webdav/xml-builder.d.ts +73 -0
  217. package/dist/services/webdav/xml-builder.js +156 -0
  218. package/dist/services/webdav/xml-builder.js.map +1 -0
  219. package/dist/services/workspace-builder.d.ts +29 -0
  220. package/dist/services/workspace-builder.js +188 -0
  221. package/dist/services/workspace-builder.js.map +1 -0
  222. package/dist/types.d.ts +231 -1
  223. package/dist/utils/instance-lock.d.ts +22 -0
  224. package/dist/utils/instance-lock.js +48 -0
  225. package/dist/utils/instance-lock.js.map +1 -0
  226. package/dist/utils/path-locks.d.ts +30 -0
  227. package/dist/utils/path-locks.js +63 -0
  228. package/dist/utils/path-locks.js.map +1 -0
  229. package/dist/utils/path-safety.d.ts +41 -0
  230. package/dist/utils/path-safety.js +119 -0
  231. package/dist/utils/path-safety.js.map +1 -0
  232. package/dist/utils/safe-json.js +55 -22
  233. package/dist/utils/safe-json.js.map +1 -1
  234. package/dist/utils/safe-write.d.ts +24 -0
  235. package/dist/utils/safe-write.js +82 -0
  236. package/dist/utils/safe-write.js.map +1 -0
  237. package/install/jishu-install.sh +323 -27
  238. package/install/jishu-uninstall.sh +353 -20
  239. package/package.json +18 -1
  240. package/public/assets/Dashboard-BdWPtroF.js +1 -0
  241. package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
  242. package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
  243. package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
  244. package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
  245. package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
  246. package/public/assets/NewInstance-BIYDmJis.js +1 -0
  247. package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
  248. package/public/assets/Settings-Cc-tYBil.js +1 -0
  249. package/public/assets/Setup-lGZEk5jq.js +1 -0
  250. package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
  251. package/public/assets/index-87IJXG-w.css +1 -0
  252. package/public/assets/index-BZc5zH7u.js +19 -0
  253. package/public/assets/providers-DtNXh9JD.js +1 -0
  254. package/public/assets/registry-BWnkJgZ1.js +2 -0
  255. package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
  256. package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
  257. package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
  258. package/public/index.html +4 -4
  259. package/scripts/check-app-spec.mjs +457 -0
  260. package/scripts/check-i18n.mjs +154 -0
  261. package/scripts/check-new-file-tests.mjs +230 -0
  262. package/scripts/check-quarantine-expiry.mjs +105 -0
  263. package/scripts/perf/README.md +49 -0
  264. package/scripts/perf/auth.js +99 -0
  265. package/scripts/perf/config.js +63 -0
  266. package/scripts/perf/instances.js +143 -0
  267. package/scripts/perf/proxy.js +96 -0
  268. package/scripts/run.sh +4 -4
  269. package/scripts/smoke/files-w1.sh +142 -0
  270. package/scripts/smoke-backend.mjs +122 -0
  271. package/scripts/smoke-post-publish.mjs +346 -0
  272. package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
  273. package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
  274. package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
  275. package/public/assets/NewInstance-BCIrAd86.js +0 -1
  276. package/public/assets/Settings-xkDcduFz.js +0 -1
  277. package/public/assets/Setup-Cfuwj4gV.js +0 -1
  278. package/public/assets/index-CPhVFEsx.css +0 -1
  279. package/public/assets/index-DQsM6Joa.js +0 -19
  280. package/public/assets/providers-V-vwrExZ.js +0 -1
  281. 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