jishushell 0.4.24 → 0.4.30

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 (167) hide show
  1. package/INSTALL-NOTICE +11 -0
  2. package/apps/browserless-chromium-container.yaml +78 -0
  3. package/apps/hermes-container.yaml +36 -2
  4. package/apps/ollama-binary.yaml +91 -90
  5. package/apps/ollama-cpu-container.yaml +8 -1
  6. package/apps/ollama-with-hollama-binary.yaml +91 -90
  7. package/apps/openclaw-binary.yaml +30 -1
  8. package/apps/openclaw-container.yaml +37 -2
  9. package/apps/openclaw-with-ollama-container.yaml +11 -2
  10. package/apps/openclaw-with-searxng-container.yaml +22 -2
  11. package/apps/openwebui-container.yaml +45 -1
  12. package/apps/playwright-container.yaml +7 -1
  13. package/apps/searxng-container.yaml +54 -4
  14. package/dist/cli/app.js +79 -9
  15. package/dist/cli/app.js.map +1 -1
  16. package/dist/cli/doctor.d.ts +12 -12
  17. package/dist/cli/doctor.js +242 -55
  18. package/dist/cli/doctor.js.map +1 -1
  19. package/dist/cli/llm.d.ts +4 -3
  20. package/dist/cli/llm.js +4 -3
  21. package/dist/cli/llm.js.map +1 -1
  22. package/dist/cli/panel.d.ts +6 -5
  23. package/dist/cli/panel.js +10 -9
  24. package/dist/cli/panel.js.map +1 -1
  25. package/dist/control.d.ts +7 -6
  26. package/dist/control.js +7 -6
  27. package/dist/control.js.map +1 -1
  28. package/dist/routes/agent-apps.d.ts +1 -1
  29. package/dist/routes/agent-apps.js +1 -1
  30. package/dist/routes/apps.js +44 -11
  31. package/dist/routes/apps.js.map +1 -1
  32. package/dist/routes/auth.js +3 -0
  33. package/dist/routes/auth.js.map +1 -1
  34. package/dist/routes/instances.js +787 -16
  35. package/dist/routes/instances.js.map +1 -1
  36. package/dist/routes/llm.js +24 -35
  37. package/dist/routes/llm.js.map +1 -1
  38. package/dist/routes/setup.js +1 -1
  39. package/dist/routes/setup.js.map +1 -1
  40. package/dist/server.d.ts +9 -0
  41. package/dist/server.js +410 -17
  42. package/dist/server.js.map +1 -1
  43. package/dist/services/agent-apps/catalog.js +4 -3
  44. package/dist/services/agent-apps/catalog.js.map +1 -1
  45. package/dist/services/agent-apps/index.d.ts +1 -1
  46. package/dist/services/agent-apps/index.js +1 -1
  47. package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
  48. package/dist/services/agent-apps/installers/adapter.js +1 -1
  49. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  50. package/dist/services/agent-apps/installers/shell-script.js +3 -3
  51. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  52. package/dist/services/agent-apps/types.d.ts +2 -2
  53. package/dist/services/agent-apps/types.js +1 -1
  54. package/dist/services/app/app-manager.d.ts +24 -1
  55. package/dist/services/app/app-manager.js +664 -116
  56. package/dist/services/app/app-manager.js.map +1 -1
  57. package/dist/services/app/hermes-agent-manager.js +6 -4
  58. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  59. package/dist/services/app/provide-resolver.d.ts +29 -0
  60. package/dist/services/app/provide-resolver.js +112 -0
  61. package/dist/services/app/provide-resolver.js.map +1 -0
  62. package/dist/services/capability-endpoint-validator.d.ts +41 -0
  63. package/dist/services/capability-endpoint-validator.js +104 -0
  64. package/dist/services/capability-endpoint-validator.js.map +1 -0
  65. package/dist/services/capability-health.d.ts +16 -0
  66. package/dist/services/capability-health.js +121 -0
  67. package/dist/services/capability-health.js.map +1 -0
  68. package/dist/services/capability-registry.d.ts +106 -0
  69. package/dist/services/capability-registry.js +313 -0
  70. package/dist/services/capability-registry.js.map +1 -0
  71. package/dist/services/connection-apply.d.ts +89 -0
  72. package/dist/services/connection-apply.js +421 -0
  73. package/dist/services/connection-apply.js.map +1 -0
  74. package/dist/services/connection-resolver.d.ts +65 -0
  75. package/dist/services/connection-resolver.js +281 -0
  76. package/dist/services/connection-resolver.js.map +1 -0
  77. package/dist/services/connection-transactor.d.ts +37 -0
  78. package/dist/services/connection-transactor.js +341 -0
  79. package/dist/services/connection-transactor.js.map +1 -0
  80. package/dist/services/instance-manager.d.ts +13 -0
  81. package/dist/services/instance-manager.js +137 -23
  82. package/dist/services/instance-manager.js.map +1 -1
  83. package/dist/services/llm-proxy/index.d.ts +16 -2
  84. package/dist/services/llm-proxy/index.js +48 -44
  85. package/dist/services/llm-proxy/index.js.map +1 -1
  86. package/dist/services/llm-proxy/probe.d.ts +6 -0
  87. package/dist/services/llm-proxy/probe.js +85 -0
  88. package/dist/services/llm-proxy/probe.js.map +1 -0
  89. package/dist/services/llm-proxy/ssrf.d.ts +1 -0
  90. package/dist/services/llm-proxy/ssrf.js +18 -7
  91. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  92. package/dist/services/nomad-manager.js +375 -16
  93. package/dist/services/nomad-manager.js.map +1 -1
  94. package/dist/services/process-manager.js +1 -1
  95. package/dist/services/process-manager.js.map +1 -1
  96. package/dist/services/runtime/adapters/hermes.d.ts +30 -1
  97. package/dist/services/runtime/adapters/hermes.js +218 -5
  98. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  99. package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
  100. package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
  101. package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
  102. package/dist/services/runtime/adapters/openclaw.d.ts +87 -0
  103. package/dist/services/runtime/adapters/openclaw.js +250 -2
  104. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  105. package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
  106. package/dist/services/runtime/mcp-shims/firewall.js +129 -0
  107. package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
  108. package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
  109. package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
  110. package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
  111. package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
  112. package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
  113. package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
  114. package/dist/services/runtime/migrations.d.ts +8 -0
  115. package/dist/services/runtime/migrations.js +100 -0
  116. package/dist/services/runtime/migrations.js.map +1 -1
  117. package/dist/services/runtime/types.d.ts +15 -0
  118. package/dist/services/setup-manager.js +6 -6
  119. package/dist/services/setup-manager.js.map +1 -1
  120. package/dist/services/suggestions.d.ts +27 -0
  121. package/dist/services/suggestions.js +133 -0
  122. package/dist/services/suggestions.js.map +1 -0
  123. package/dist/services/task-registry.js +4 -2
  124. package/dist/services/task-registry.js.map +1 -1
  125. package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
  126. package/dist/services/telemetry/device-fingerprint.js +1 -1
  127. package/dist/services/types-shim.d.ts +16 -0
  128. package/dist/services/types-shim.js +2 -0
  129. package/dist/services/types-shim.js.map +1 -0
  130. package/dist/types.d.ts +171 -1
  131. package/dist/utils/instance-lock.d.ts +22 -0
  132. package/dist/utils/instance-lock.js +48 -0
  133. package/dist/utils/instance-lock.js.map +1 -0
  134. package/dist/utils/safe-json.js +55 -22
  135. package/dist/utils/safe-json.js.map +1 -1
  136. package/install/jishu-install.sh +323 -27
  137. package/install/jishu-uninstall.sh +353 -20
  138. package/package.json +3 -1
  139. package/public/assets/Dashboard-rkWp-CXd.js +1 -0
  140. package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-_GHoklgo.js} +1 -1
  141. package/public/assets/HermesConfigForm-anDnwUp_.js +4 -0
  142. package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-ZU9_-hDr.js} +1 -1
  143. package/public/assets/InstanceDetail-CN0FH1aw.js +92 -0
  144. package/public/assets/{Login-BWsZH2mu.js → Login-BItXqYAJ.js} +1 -1
  145. package/public/assets/NewInstance-BousE6kY.js +1 -0
  146. package/public/assets/ProviderRecommendations-DFYj7Fb6.js +1 -0
  147. package/public/assets/Settings-Bttc6QmM.js +1 -0
  148. package/public/assets/Setup-Bsxx1zgj.js +1 -0
  149. package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-DPZpAKgO.js} +2 -2
  150. package/public/assets/index-8xZy1z5k.css +1 -0
  151. package/public/assets/index-Dw3HhUYE.js +19 -0
  152. package/public/assets/providers-DtNXh9JD.js +1 -0
  153. package/public/assets/registry-5s2UB6is.js +2 -0
  154. package/public/index.html +2 -2
  155. package/scripts/check-app-spec.mjs +443 -0
  156. package/scripts/check-i18n.mjs +154 -0
  157. package/scripts/run.sh +4 -4
  158. package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
  159. package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
  160. package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
  161. package/public/assets/NewInstance-BCIrAd86.js +0 -1
  162. package/public/assets/Settings-xkDcduFz.js +0 -1
  163. package/public/assets/Setup-Cfuwj4gV.js +0 -1
  164. package/public/assets/index-CPhVFEsx.css +0 -1
  165. package/public/assets/index-DQsM6Joa.js +0 -19
  166. package/public/assets/providers-V-vwrExZ.js +0 -1
  167. package/public/assets/registry-B4UFJdpA.js +0 -2
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validates `apps/*.yaml` against the Provider Authoring Contract
4
+ * (docs/app-interconnect-design.md §13.2.3).
5
+ *
6
+ * Three provide classes:
7
+ * A. Bindable category — capability has CAPABILITY_CATEGORIES prefix
8
+ * (`llm-` / `search-` / `browser-` / `mcp-`). Must declare task,
9
+ * port (resolvable to a task port via host_port ?? port), protocol
10
+ * from category whitelist, optional health, description.
11
+ * B. Legacy bindable exact — capability is in LEGACY_BINDABLE_EXACT_ALLOWLIST
12
+ * OR appears in some yaml's `requires.capability`. Same field
13
+ * requirements as A but protocol whitelist is `default` (broader).
14
+ * C. UI-only — capability is in UI_LEGACY_ALLOWLIST or matches a
15
+ * `web-*` / `*-terminal` / `*-dashboard` / `*-ui` pattern. Looser
16
+ * checks: only naming + (port resolves OR no port for terminal-only).
17
+ *
18
+ * `requires[*]` checks: capability is a category token OR is in
19
+ * `allowedRequiresExact = LEGACY_BINDABLE_EXACT_ALLOWLIST ∪ exactRequiresInRepo`.
20
+ * Plus an "orphan require" check that matches each require to at least one
21
+ * provide (severity gated by `required` — error for required:true, warning
22
+ * otherwise).
23
+ */
24
+ import { readFileSync, readdirSync, statSync } from "fs";
25
+ import { join, dirname } from "path";
26
+ import { fileURLToPath } from "url";
27
+ import { parse as parseYaml } from "yaml";
28
+
29
+ const __dirname = dirname(fileURLToPath(import.meta.url));
30
+ const APPS_DIR = process.env.APPS_DIR_OVERRIDE ?? join(__dirname, "..", "apps");
31
+
32
+ // ── Allowlists (kept here so check-app-spec stays mechanically driven) ──
33
+
34
+ const CAPABILITY_CATEGORIES = new Set(["llm", "search", "browser", "mcp"]);
35
+
36
+ const LEGACY_BINDABLE_EXACT_ALLOWLIST = new Set([
37
+ "ollama-api",
38
+ ]);
39
+
40
+ const UI_LEGACY_ALLOWLIST = new Set([
41
+ "openwebui-web",
42
+ "hollama-web",
43
+ "playwright-ui",
44
+ "openclaw-dashboard",
45
+ "ollama-terminal",
46
+ "web-ui",
47
+ "web-searxng",
48
+ "web-openwebui",
49
+ "web-hermes",
50
+ ]);
51
+
52
+ const PROTOCOL_WHITELIST = {
53
+ llm: new Set(["http", "https"]),
54
+ search: new Set(["http", "https"]),
55
+ browser: new Set(["ws", "wss", "http", "https"]),
56
+ mcp: new Set(["http", "https", "ws", "sse"]),
57
+ default: new Set(["http", "https", "ws", "wss", "tcp"]),
58
+ };
59
+
60
+ // ── Helpers ──
61
+
62
+ function categoryOf(capability) {
63
+ const dashIdx = capability.indexOf("-");
64
+ if (dashIdx <= 0) return null;
65
+ const head = capability.slice(0, dashIdx);
66
+ return CAPABILITY_CATEGORIES.has(head) ? head : null;
67
+ }
68
+
69
+ function isUILegacy(capability) {
70
+ if (UI_LEGACY_ALLOWLIST.has(capability)) return true;
71
+ if (capability.startsWith("web-")) return true;
72
+ return /-(terminal|dashboard|ui)$/.test(capability);
73
+ }
74
+
75
+ // ── Auth block validation (§6 of agents-as-llm-provider spec) ──
76
+
77
+ const AUTH_KINDS = new Set(["none", "bearer", "header", "query"]);
78
+ const AUTH_OPTIONAL_FIELDS_BY_KIND = {
79
+ none: new Set(),
80
+ bearer: new Set(["headerName", "tokenPrefix"]),
81
+ header: new Set(["headerName", "tokenPrefix"]),
82
+ query: new Set(["headerName", "tokenPrefix"]),
83
+ };
84
+ const TOKEN_SOURCE_PATTERNS = [
85
+ /^instance\.env\.[A-Z][A-Z0-9_]*$/,
86
+ /^instance\.config\..+$/,
87
+ /^proxy\.token$/,
88
+ ];
89
+
90
+ /**
91
+ * Validate the optional `auth` block on a provides[] entry.
92
+ * Reports errors via the `err(file, msg)` callback; returns nothing.
93
+ */
94
+ function validateAuth(file, capLabel, auth) {
95
+ if (auth === undefined || auth === null) return; // optional
96
+
97
+ if (typeof auth !== "object" || Array.isArray(auth)) {
98
+ err(file, `provide '${capLabel}' auth must be an object`);
99
+ return;
100
+ }
101
+
102
+ const kind = auth.kind;
103
+ if (typeof kind !== "string" || !AUTH_KINDS.has(kind)) {
104
+ err(file, `provide '${capLabel}' auth.kind must be one of ${[...AUTH_KINDS].join("|")}`);
105
+ return;
106
+ }
107
+
108
+ // Reject unknown keys
109
+ const allowed = new Set(["kind"]);
110
+ if (kind !== "none") allowed.add("tokenSource");
111
+ for (const opt of AUTH_OPTIONAL_FIELDS_BY_KIND[kind]) allowed.add(opt);
112
+ for (const key of Object.keys(auth)) {
113
+ if (!allowed.has(key)) {
114
+ err(file, `provide '${capLabel}' auth has unknown key '${key}' for kind '${kind}'`);
115
+ }
116
+ }
117
+
118
+ if (kind === "none") {
119
+ return; // nothing more to check
120
+ }
121
+
122
+ // tokenSource required for non-none kinds
123
+ if (typeof auth.tokenSource !== "string" || !auth.tokenSource) {
124
+ err(file, `provide '${capLabel}' auth.tokenSource required for kind '${kind}'`);
125
+ return;
126
+ }
127
+ const matchesPattern = TOKEN_SOURCE_PATTERNS.some((re) => re.test(auth.tokenSource));
128
+ if (!matchesPattern) {
129
+ err(
130
+ file,
131
+ `provide '${capLabel}' auth.tokenSource '${auth.tokenSource}' does not match any of: instance.env.<NAME>, instance.config.<path>, proxy.token`,
132
+ );
133
+ }
134
+
135
+ if (auth.headerName !== undefined) {
136
+ if (typeof auth.headerName !== "string" || !auth.headerName) {
137
+ err(file, `provide '${capLabel}' auth.headerName must be a non-empty string when present`);
138
+ }
139
+ }
140
+ if (auth.tokenPrefix !== undefined) {
141
+ if (typeof auth.tokenPrefix !== "string") {
142
+ err(file, `provide '${capLabel}' auth.tokenPrefix must be a string when present`);
143
+ }
144
+ }
145
+ }
146
+
147
+ function findTask(spec, taskName) {
148
+ return (spec.tasks ?? []).find((t) => t.name === taskName);
149
+ }
150
+
151
+ function hostPortOf(p) {
152
+ if (typeof p?.host_port === "number" && p.host_port > 0) return p.host_port;
153
+ if (typeof p?.port === "number" && p.port > 0) return p.port;
154
+ return undefined;
155
+ }
156
+
157
+ function findPortInTask(task, providePort) {
158
+ return (task?.ports ?? []).find((p) => hostPortOf(p) === providePort);
159
+ }
160
+
161
+ // ── Main ──
162
+
163
+ const errors = [];
164
+ const warnings = [];
165
+
166
+ function err(file, msg) {
167
+ errors.push(`${file}: ${msg}`);
168
+ }
169
+ function warn(file, msg) {
170
+ warnings.push(`${file}: ${msg}`);
171
+ }
172
+
173
+ const yamlFiles = readdirSync(APPS_DIR)
174
+ .filter((f) => f.endsWith(".yaml"))
175
+ .map((f) => join(APPS_DIR, f));
176
+
177
+ const specs = [];
178
+ for (const file of yamlFiles) {
179
+ try {
180
+ const text = readFileSync(file, "utf-8");
181
+ const doc = parseYaml(text);
182
+ if (!doc || typeof doc !== "object") {
183
+ err(file, "yaml does not parse to an object");
184
+ continue;
185
+ }
186
+ specs.push({ file: file.replace(APPS_DIR + "/", "apps/"), spec: doc });
187
+ } catch (e) {
188
+ err(file, `yaml parse failed: ${e.message}`);
189
+ }
190
+ }
191
+
192
+ // First pass: collect exact-name requires across the repo (B class membership).
193
+ const exactRequiresInRepo = new Set();
194
+ for (const { spec } of specs) {
195
+ for (const req of spec.requires ?? []) {
196
+ if (typeof req?.capability !== "string") continue;
197
+ if (CAPABILITY_CATEGORIES.has(req.capability)) continue;
198
+ exactRequiresInRepo.add(req.capability);
199
+ }
200
+ }
201
+
202
+ const allowedRequiresExact = new Set([
203
+ ...LEGACY_BINDABLE_EXACT_ALLOWLIST,
204
+ ...exactRequiresInRepo,
205
+ ]);
206
+
207
+ // Index every provide capability across the repo for orphan-require check.
208
+ const providersByCapability = new Map();
209
+ for (const { spec } of specs) {
210
+ for (const provide of spec.provides ?? []) {
211
+ if (typeof provide?.capability !== "string") continue;
212
+ if (!providersByCapability.has(provide.capability)) {
213
+ providersByCapability.set(provide.capability, []);
214
+ }
215
+ providersByCapability.get(provide.capability).push({ spec });
216
+ }
217
+ }
218
+
219
+ function hasProviderForRequire(req) {
220
+ // A path: prefix match against any provide name starting with `${req}-`.
221
+ if (CAPABILITY_CATEGORIES.has(req)) {
222
+ for (const cap of providersByCapability.keys()) {
223
+ if (cap.startsWith(req + "-")) return true;
224
+ }
225
+ return false;
226
+ }
227
+ // B path: exact match.
228
+ return providersByCapability.has(req);
229
+ }
230
+
231
+ // Second pass: validate each spec.
232
+ for (const { file, spec } of specs) {
233
+ const id = typeof spec.id === "string" ? spec.id : "<no id>";
234
+
235
+ // ── provides[*] ──────────────────────────────────────────────────────
236
+ for (const provide of spec.provides ?? []) {
237
+ if (typeof provide?.capability !== "string" || !provide.capability) {
238
+ err(file, `provide missing capability`);
239
+ continue;
240
+ }
241
+ const cap = provide.capability;
242
+ const cat = categoryOf(cap);
243
+ const isB =
244
+ !cat &&
245
+ (LEGACY_BINDABLE_EXACT_ALLOWLIST.has(cap) || exactRequiresInRepo.has(cap)) &&
246
+ !UI_LEGACY_ALLOWLIST.has(cap);
247
+ const isUI = !cat && !isB && isUILegacy(cap);
248
+
249
+ // url-only provide: skip endpoint checks (not in registry per §5.1).
250
+ if (typeof provide.url === "string" && provide.url.trim()) {
251
+ if (provide.auth !== undefined) {
252
+ err(file, `provide '${cap}' auth is not allowed on UI-only or url-only provides`);
253
+ }
254
+ continue;
255
+ }
256
+
257
+ if (cat) {
258
+ // Class A
259
+ if (typeof provide.task !== "string" || !provide.task) {
260
+ err(file, `provide '${cap}' (A) missing task`);
261
+ } else if (!findTask(spec, provide.task)) {
262
+ err(file, `provide '${cap}' (A) task '${provide.task}' not declared in tasks[]`);
263
+ }
264
+ if (typeof provide.port !== "number") {
265
+ err(file, `provide '${cap}' (A) missing port`);
266
+ } else {
267
+ const task = findTask(spec, provide.task);
268
+ if (task && !findPortInTask(task, provide.port)) {
269
+ err(file, `provide '${cap}' (A) port ${provide.port} not in task '${provide.task}' ports`);
270
+ }
271
+ }
272
+ const proto = (provide.protocol ?? "http").toLowerCase();
273
+ if (!PROTOCOL_WHITELIST[cat].has(proto)) {
274
+ err(file, `provide '${cap}' (A) protocol '${proto}' not in ${cat} whitelist`);
275
+ }
276
+ if (typeof provide.description !== "string" || !provide.description.trim()) {
277
+ warn(file, `provide '${cap}' (A) missing description`);
278
+ }
279
+ validateAuth(file, cap, provide.auth);
280
+ } else if (isB) {
281
+ // Class B
282
+ if (typeof provide.task !== "string" || !provide.task) {
283
+ err(file, `provide '${cap}' (B legacy bindable exact) missing task`);
284
+ } else if (!findTask(spec, provide.task)) {
285
+ err(file, `provide '${cap}' (B) task '${provide.task}' not declared`);
286
+ }
287
+ if (typeof provide.port !== "number") {
288
+ err(file, `provide '${cap}' (B) missing port`);
289
+ } else {
290
+ const task = findTask(spec, provide.task);
291
+ if (task && !findPortInTask(task, provide.port)) {
292
+ err(file, `provide '${cap}' (B) port ${provide.port} not in task '${provide.task}' ports`);
293
+ }
294
+ }
295
+ const proto = (provide.protocol ?? "").toLowerCase();
296
+ if (!proto) {
297
+ err(file, `provide '${cap}' (B) protocol must be declared (one of ${[...PROTOCOL_WHITELIST.default].join("|")})`);
298
+ } else if (!PROTOCOL_WHITELIST.default.has(proto)) {
299
+ err(file, `provide '${cap}' (B) protocol '${proto}' not in default whitelist`);
300
+ }
301
+ if (typeof provide.description !== "string" || !provide.description.trim()) {
302
+ warn(file, `provide '${cap}' (B) missing description`);
303
+ }
304
+ validateAuth(file, cap, provide.auth);
305
+ } else if (isUI) {
306
+ // Class C — looser. Terminal capabilities legitimately have no port.
307
+ if (provide.auth !== undefined) {
308
+ err(file, `provide '${cap}' auth is not allowed on UI-only or url-only provides`);
309
+ }
310
+ if (typeof provide.port === "number") {
311
+ if (typeof provide.task === "string" && provide.task) {
312
+ const task = findTask(spec, provide.task);
313
+ if (task && !findPortInTask(task, provide.port)) {
314
+ err(file, `provide '${cap}' (C) port ${provide.port} not in task '${provide.task}' ports`);
315
+ }
316
+ }
317
+ }
318
+ } else {
319
+ // Unrecognized — author must explicitly opt into one of A/B/C.
320
+ err(
321
+ file,
322
+ `provide '${cap}' is not recognized as A/B/C; rename with category prefix (llm-/search-/browser-/mcp-), add to LEGACY_BINDABLE_EXACT_ALLOWLIST, or add to UI_LEGACY_ALLOWLIST`,
323
+ );
324
+ }
325
+
326
+ // ── §17 (PR 8): tool_schema validation — applies to any bindable
327
+ // capability that an LLM-binding adapter may turn into an MCP
328
+ // server entry (i.e. category A search-/mcp-, plus any future
329
+ // bindable cap). Warn-only this release; promoted to err in v0.5.
330
+ if (provide.tool_schema !== undefined) {
331
+ const ts = provide.tool_schema;
332
+ if (typeof ts !== "object" || ts === null) {
333
+ err(file, `provide '${cap}' tool_schema must be an object`);
334
+ } else {
335
+ if (typeof ts.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(ts.name)) {
336
+ err(file, `provide '${cap}' tool_schema.name must match /^[a-z][a-z0-9_]*$/ (got ${JSON.stringify(ts.name)})`);
337
+ }
338
+ if (typeof ts.description !== "string" || !ts.description.trim()) {
339
+ err(file, `provide '${cap}' tool_schema.description is required`);
340
+ }
341
+ if (typeof ts.parameters !== "object" || ts.parameters === null) {
342
+ err(file, `provide '${cap}' tool_schema.parameters must be an object`);
343
+ } else {
344
+ if (ts.parameters.type !== "object") {
345
+ err(file, `provide '${cap}' tool_schema.parameters.type must be "object"`);
346
+ }
347
+ const props = ts.parameters.properties ?? {};
348
+ if (typeof props !== "object" || props === null) {
349
+ err(file, `provide '${cap}' tool_schema.parameters.properties must be an object`);
350
+ } else {
351
+ const required = Array.isArray(ts.parameters.required) ? ts.parameters.required : [];
352
+ for (const r of required) {
353
+ if (!(r in props)) {
354
+ err(file, `provide '${cap}' tool_schema.parameters.required has '${r}' not in properties`);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ if (typeof ts.upstream !== "object" || ts.upstream === null) {
360
+ err(file, `provide '${cap}' tool_schema.upstream is required`);
361
+ } else if (typeof ts.upstream.command !== "string" || !ts.upstream.command) {
362
+ err(file, `provide '${cap}' tool_schema.upstream.command is required`);
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ // ── requires[*] ──────────────────────────────────────────────────────
369
+ const seenSlots = new Set();
370
+ for (const req of spec.requires ?? []) {
371
+ if (typeof req?.capability !== "string" || !req.capability) {
372
+ err(file, `require missing capability`);
373
+ continue;
374
+ }
375
+ const isCategory = CAPABILITY_CATEGORIES.has(req.capability);
376
+ const isExact = allowedRequiresExact.has(req.capability);
377
+ if (!isCategory && !isExact) {
378
+ err(
379
+ file,
380
+ `require '${req.capability}' is neither a category token nor in LEGACY_BINDABLE_EXACT_ALLOWLIST / repo exact set`,
381
+ );
382
+ continue;
383
+ }
384
+
385
+ if (typeof req.inject_as !== "string" || !req.inject_as) {
386
+ err(file, `require '${req.capability}' missing inject_as`);
387
+ } else {
388
+ if (!/^[A-Z][A-Z0-9_]*$/.test(req.inject_as)) {
389
+ warn(file, `require '${req.capability}' inject_as '${req.inject_as}' should be UPPER_SNAKE_CASE`);
390
+ }
391
+ if (seenSlots.has(req.inject_as)) {
392
+ err(file, `require slot '${req.inject_as}' duplicated`);
393
+ }
394
+ seenSlots.add(req.inject_as);
395
+ }
396
+
397
+ if (req.cardinality && !["one", "many"].includes(req.cardinality)) {
398
+ err(file, `require '${req.capability}' cardinality must be 'one' or 'many'`);
399
+ }
400
+
401
+ if (req.apply !== undefined) {
402
+ if (!["proxy-upstream", "openai-env"].includes(req.apply)) {
403
+ err(file, `require '${req.capability}' apply must be 'proxy-upstream' or 'openai-env'`);
404
+ }
405
+ if (req.capability !== "llm" && categoryOf(req.capability) !== "llm") {
406
+ err(file, `require '${req.capability}' apply field is only valid for llm category`);
407
+ }
408
+ }
409
+
410
+ // Orphan-require check: at least one provider in the repo must satisfy.
411
+ if (!hasProviderForRequire(req.capability)) {
412
+ const isRequired = req.required !== false; // default true
413
+ if (isRequired) {
414
+ err(
415
+ file,
416
+ `orphan required require '${req.capability}' — no provider matches; add provider, fix typo, or set required: false`,
417
+ );
418
+ } else {
419
+ warn(
420
+ file,
421
+ `orphan optional require '${req.capability}' — no provider in repo (will resolve at runtime if user installs one)`,
422
+ );
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ // ── Report ──
429
+
430
+ if (warnings.length) {
431
+ console.warn("⚠ Warnings:");
432
+ for (const w of warnings) console.warn(" " + w);
433
+ }
434
+
435
+ if (errors.length) {
436
+ console.error("✗ check-app-spec failed:");
437
+ for (const e of errors) console.error(" " + e);
438
+ process.exit(1);
439
+ }
440
+
441
+ console.log(
442
+ `✓ check-app-spec passed: ${specs.length} yamls, ${warnings.length} warnings`,
443
+ );
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // Verify i18n integrity:
3
+ // 1. en and zh namespace files are paired (same set of .json files)
4
+ // 2. every flat dotted key in one language exists in the other
5
+ // 3. every t('...') call in frontend/src (excluding tests & dynamic template
6
+ // keys that use ${}) resolves to a key present in some locale file
7
+ // 4. Chinese-defaultValue regression guard: each source file's count of
8
+ // `defaultValue: '中文…'` literals must not exceed its baseline below.
9
+ // Locale files are the source of truth — the Chinese fallback is dead
10
+ // code (the existing key check above guarantees both en and zh have an
11
+ // entry), but it misleads readers and rots over time. New files must
12
+ // not introduce any; existing files may drop the count to clean up.
13
+ // Exits non-zero on any mismatch so pre-commit / CI can block the commit.
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+
18
+ const REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
19
+ const SRC = path.join(REPO, "frontend/src");
20
+ const LOC = path.join(SRC, "i18n/locales");
21
+
22
+ // Baseline of files with Chinese-defaultValue literals at the time the rule
23
+ // was introduced. Keep counts in sync as you clean up — when a file's count
24
+ // drops, lower the baseline (or delete the entry) so regressions are caught
25
+ // at the new floor.
26
+ const CHINESE_DEFAULT_VALUE_BASELINE = Object.freeze({
27
+ "frontend/src/components/instance/BackupActions.tsx": 73,
28
+ "frontend/src/pages/NewInstance.tsx": 46,
29
+ "frontend/src/components/instance/BackupStatus.tsx": 17,
30
+ "frontend/src/components/instance/ImportFlow.tsx": 15,
31
+ "frontend/src/pages/InstanceDetail.tsx": 12,
32
+ "frontend/src/components/instance/HermesConfigForm.tsx": 5,
33
+ "frontend/src/components/instance/FeishuLoginPanel.tsx": 1,
34
+ "frontend/src/components/instance/WeixinLoginPanel.tsx": 1,
35
+ "frontend/src/pages/Dashboard.tsx": 1,
36
+ });
37
+
38
+ function walkSrc(dir, out = []) {
39
+ for (const name of fs.readdirSync(dir)) {
40
+ const p = path.join(dir, name);
41
+ const st = fs.statSync(p);
42
+ if (st.isDirectory()) {
43
+ if (!/node_modules|dist|build|locales$|i18n$/.test(p)) walkSrc(p, out);
44
+ } else if (/\.(ts|tsx)$/.test(p)
45
+ && !/\.test\.(ts|tsx)$/.test(p)
46
+ && !/providers\.ts$/.test(p)) {
47
+ out.push(p);
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function flatten(obj, prefix = "", out = {}) {
54
+ for (const [k, v] of Object.entries(obj)) {
55
+ const key = prefix ? `${prefix}.${k}` : k;
56
+ if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, out);
57
+ else out[key] = v;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ const errors = [];
63
+
64
+ // ── 1/2: compare locale files ────────────────────────────────────────────────
65
+ const langs = fs.readdirSync(LOC).filter(f =>
66
+ fs.statSync(path.join(LOC, f)).isDirectory());
67
+ if (langs.length < 2) {
68
+ errors.push(`Expected at least two language directories under ${LOC}, got ${langs.join(",")}`);
69
+ }
70
+ const fileSets = Object.fromEntries(langs.map(l =>
71
+ [l, new Set(fs.readdirSync(path.join(LOC, l)).filter(f => f.endsWith(".json")))]));
72
+ const allFiles = new Set(langs.flatMap(l => [...fileSets[l]]));
73
+ for (const f of allFiles) {
74
+ for (const l of langs) {
75
+ if (!fileSets[l].has(f)) errors.push(`Missing locale file: ${l}/${f}`);
76
+ }
77
+ }
78
+
79
+ const allKeys = new Set();
80
+ const perLangKeys = Object.fromEntries(langs.map(l => [l, {}]));
81
+ for (const l of langs) {
82
+ for (const f of fileSets[l]) {
83
+ const ns = f.replace(/\.json$/, "");
84
+ let data;
85
+ try { data = JSON.parse(fs.readFileSync(path.join(LOC, l, f), "utf8")); }
86
+ catch (e) { errors.push(`${l}/${f}: invalid JSON — ${e.message}`); continue; }
87
+ const flat = flatten(data);
88
+ // Also accept flat-dotted keys stored as top-level string values
89
+ for (const k of Object.keys(data)) {
90
+ if (typeof data[k] === "string") flat[k] = data[k];
91
+ }
92
+ perLangKeys[l][ns] = flat;
93
+ for (const k of Object.keys(flat)) allKeys.add(`${ns}:${k}`);
94
+ }
95
+ }
96
+
97
+ for (const tag of allKeys) {
98
+ const [ns, ...rest] = tag.split(":");
99
+ const k = rest.join(":");
100
+ for (const l of langs) {
101
+ const bag = perLangKeys[l][ns];
102
+ if (!bag) { errors.push(`${l}/${ns}.json missing entirely`); break; }
103
+ if (bag[k] === undefined) errors.push(`Missing key in ${l}: ${ns}:${k}`);
104
+ }
105
+ }
106
+
107
+ // ── 3: source code t('...') coverage ─────────────────────────────────────────
108
+ const tCallRe = /\bt\(\s*['"`]([^'"`]+)['"`]/g;
109
+ const files = walkSrc(SRC);
110
+ for (const file of files) {
111
+ const src = fs.readFileSync(file, "utf8");
112
+ let m;
113
+ while ((m = tCallRe.exec(src)) !== null) {
114
+ const key = m[1];
115
+ if (key.includes("${") || key.includes("`")) continue; // dynamic, skip
116
+ const line = src.slice(0, m.index).split("\n").length;
117
+ let hit = false;
118
+ if (key.includes(":")) {
119
+ hit = allKeys.has(key);
120
+ } else {
121
+ for (const full of allKeys) if (full.endsWith(`:${key}`)) { hit = true; break; }
122
+ }
123
+ if (!hit) errors.push(`${path.relative(REPO, file)}:${line} — t('${key}') has no matching locale key`);
124
+ }
125
+ }
126
+
127
+ // ── 4: Chinese-defaultValue regression guard ─────────────────────────────────
128
+ const cnDefaultValueRe = /defaultValue:\s*(['"`])([^'"`]*[一-鿿][^'"`]*)\1/g;
129
+ let cnDefaultValueTotal = 0;
130
+ for (const file of files) {
131
+ const rel = path.relative(REPO, file).split(path.sep).join("/");
132
+ const src = fs.readFileSync(file, "utf8");
133
+ let count = 0;
134
+ while (cnDefaultValueRe.exec(src) !== null) count += 1;
135
+ cnDefaultValueRe.lastIndex = 0;
136
+ cnDefaultValueTotal += count;
137
+ const allowed = CHINESE_DEFAULT_VALUE_BASELINE[rel] ?? 0;
138
+ if (count > allowed) {
139
+ errors.push(
140
+ `${rel} — Chinese defaultValue count ${count} exceeds baseline ${allowed}; ` +
141
+ "add the locale entry to en/zh JSON instead of writing a Chinese fallback in source.",
142
+ );
143
+ }
144
+ }
145
+
146
+ if (errors.length) {
147
+ console.error(`i18n check failed (${errors.length} issues):`);
148
+ for (const e of errors) console.error(" " + e);
149
+ process.exit(1);
150
+ }
151
+ const cnNote = cnDefaultValueTotal
152
+ ? `, ${cnDefaultValueTotal} legacy Chinese-defaultValue literals (under baseline)`
153
+ : "";
154
+ console.log(`i18n check passed: ${allKeys.size} keys across ${langs.join("/")} × ${allFiles.size} namespaces, ${files.length} source files scanned${cnNote}`);
package/scripts/run.sh CHANGED
@@ -81,7 +81,7 @@ kill_port() {
81
81
 
82
82
  # ─── Nomad ────────────────────────────────────────────────────────────────────
83
83
  ensure_nomad() {
84
- # 如果 Nomad 相关端口有任何进程,全部 kill(确保干净重启)
84
+ # If anything is holding Nomad-related ports, kill it all to guarantee a clean restart.
85
85
  local nomad_ports_pids
86
86
  nomad_ports_pids="$(sudo lsof -ti tcp:4646,4647,4648 2>/dev/null || lsof -ti tcp:4646,4647,4648 2>/dev/null || true)"
87
87
  if [[ -n "$nomad_ports_pids" ]]; then
@@ -105,7 +105,7 @@ ensure_nomad() {
105
105
  success "Nomad ports 4646/4647/4648 freed"
106
106
  fi
107
107
 
108
- # 查找 nomad 二进制:优先用 jishushell 自带的
108
+ # Locate the nomad binary; prefer the one bundled with jishushell.
109
109
  local nomad_bin="${HOME}/.jishushell/bin/nomad"
110
110
  if [[ ! -x "$nomad_bin" ]]; then
111
111
  nomad_bin="$(command -v nomad 2>/dev/null || true)"
@@ -133,7 +133,7 @@ ensure_nomad() {
133
133
  nohup "$nomad_bin" agent -dev -bind=127.0.0.1 > "$log_path" 2>&1 &
134
134
  fi
135
135
 
136
- # 等待 Nomad 就绪(最多 15 秒)
136
+ # Wait for Nomad to become ready (up to 15 seconds).
137
137
  local tries=0
138
138
  while ! lsof -ti tcp:4646 &>/dev/null && [[ $tries -lt 15 ]]; do
139
139
  sleep 1
@@ -294,7 +294,7 @@ mode_server() {
294
294
  # ─── Entry point ──────────────────────────────────────────────────────────────
295
295
  check_node
296
296
 
297
- # Nomad 最先启动(build/frontend/help 模式不需要)
297
+ # Start Nomad first (not required in build/frontend/help modes).
298
298
  case "$MODE" in
299
299
  ""|dev|server) ensure_nomad ;;
300
300
  esac
@@ -1 +0,0 @@
1
- import{k as B,j as e,L as E,l as k,m as M,n as R,o as K,q as L,t as A,v as I,w as T,x as W}from"./index-DQsM6Joa.js";import{r as m,u as D}from"./vendor-react-Bk1hRGiY.js";import{u as P}from"./usePolling-Do5Erqm_.js";import{u as C}from"./vendor-i18n-ucpM0OR0.js";function q(t){if(!t)return"-";const l=Math.floor(t/86400),d=Math.floor(t%86400/3600),r=Math.floor(t%3600/60);return l>0?`${l}d ${d}h`:d>0?`${d}h ${r}m`:`${r}m`}function G({status:t}){const{t:l}=C(),r={running:{cls:"bg-emerald-500/10 text-emerald-400 border border-emerald-500/20",labelKey:"status.running"},pending:{cls:"bg-amber-500/10 text-amber-400 border border-amber-500/20",labelKey:"status.starting"},failed:{cls:"bg-red-500/10 text-red-400 border border-red-500/20",labelKey:"status.failed"},dead:{cls:"bg-red-500/10 text-red-400 border border-red-500/20",labelKey:"status.crashed"}}[t]||{cls:"bg-[var(--card)] text-muted border border-[var(--border)]",labelKey:"status.stopped"};return e.jsx("span",{className:`inline-flex items-center text-xs px-2 py-0.5 rounded-full font-medium ${r.cls}`,children:l(r.labelKey)})}function z(){const{t}=C(["dashboard","common"]),[l,d]=m.useState([]),[r,$]=m.useState(null),[h,v]=m.useState(""),[y,j]=m.useState(""),[p,N]=m.useState(!1),g=D(),{showToast:c}=B(),u=()=>{K().then(s=>{d(s),j("")}).catch(s=>j(s.message||t("common:error.loadFailed"))),L().then($).catch(()=>{})};P(u,1e4);const _=async()=>{if(window.confirm(t("engine.restartConfirm"))){N(!0);try{await A(),c(t("engine.restarted"),"success"),setTimeout(u,2e3)}catch(s){c(s.message||t("engine.restartFailed"),"error")}finally{N(!1)}}},f=async(s,a,i)=>{s.stopPropagation(),v(`${i}-${a}`);try{let n=null;a==="start"&&(n=await I(i)),a==="stop"&&(n=await T(i)),a==="restart"&&(n=await W(i)),c(t(`common:action.${a}Done`),"success");const o=n==null?void 0:n.port_allocation;o&&typeof o.from=="number"&&typeof o.to=="number"&&o.from!==o.to&&c(t("common:toast.portReallocated",{defaultValue:"端口 {{from}} 被占用,已自动切换到 {{to}}",from:o.from,to:o.to}),"info"),setTimeout(u,1e3)}catch(n){c(n.message||t("common:error.operationFailed"),"error")}finally{v("")}},F=l.filter(s=>{var a;return((a=s.service)==null?void 0:a.status)==="running"}).length,b=!!r&&r.disk.percent>90,S=r?[{label:t("stats.runningInstances"),value:`${F} / ${l.length}`,sub:r.nomad_running?t("stats.engineRunning"):t("stats.engineStopped"),subColor:r.nomad_running?"text-emerald-400":"text-red-400",icon:e.jsxs("svg",{className:"w-4 h-4",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,children:[e.jsx("rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}),e.jsx("line",{x1:"8",y1:"21",x2:"16",y2:"21"}),e.jsx("line",{x1:"12",y1:"17",x2:"12",y2:"21"})]}),iconColor:"text-[#0066FF]",glowColor:"rgba(0,102,255,0.12)",accent:"border-l-2 border-l-[#0066FF]"},{label:t("stats.cpu"),value:`${r.cpu_percent}%`,sub:r.temperature?`${r.temperature}°C`:null,icon:e.jsxs("svg",{className:"w-4 h-4",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,children:[e.jsx("rect",{x:"4",y:"4",width:"16",height:"16",rx:"2"}),e.jsx("rect",{x:"9",y:"9",width:"6",height:"6"}),e.jsx("line",{x1:"9",y1:"2",x2:"9",y2:"4"}),e.jsx("line",{x1:"15",y1:"2",x2:"15",y2:"4"}),e.jsx("line",{x1:"9",y1:"20",x2:"9",y2:"22"}),e.jsx("line",{x1:"15",y1:"20",x2:"15",y2:"22"}),e.jsx("line",{x1:"20",y1:"9",x2:"22",y2:"9"}),e.jsx("line",{x1:"20",y1:"15",x2:"22",y2:"15"}),e.jsx("line",{x1:"2",y1:"9",x2:"4",y2:"9"}),e.jsx("line",{x1:"2",y1:"15",x2:"4",y2:"15"})]}),iconColor:"text-[#0066FF]",glowColor:"rgba(0,102,255,0.12)",accent:"border-l-2 border-l-[#0066FF]",warn:r.cpu_percent>90},{label:t("stats.memory"),value:`${r.memory.percent}%`,sub:`${r.memory.used_mb}MB / ${r.memory.total_mb}MB`,icon:e.jsx("svg",{className:"w-4 h-4",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,children:e.jsx("path",{d:"M6 19v-8m4 8v-4m4 4v-6m4 6v-2"})}),iconColor:"text-[#00D4AA]",glowColor:"rgba(0,212,170,0.12)",accent:"border-l-2 border-l-[#00D4AA]"},{label:t("stats.disk"),value:`${r.disk.percent}%`,sub:`${r.disk.used_gb}GB / ${r.disk.total_gb}GB`,icon:e.jsxs("svg",{className:"w-4 h-4",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,children:[e.jsx("ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}),e.jsx("path",{d:"M3 5v14a9 3 0 0018 0V5"}),e.jsx("line",{x1:"12",y1:"8",x2:"12",y2:"22"})]}),iconColor:b?"text-red-400":"text-[#0066FF]",glowColor:b?"rgba(239,68,68,0.12)":"rgba(0,102,255,0.12)",accent:b?"border-l-2 border-l-red-400":"border-l-2 border-l-[#0066FF]",warn:b}]:[];return e.jsxs("div",{className:"p-4 max-w-5xl mx-auto",children:[e.jsxs("div",{className:"mb-4 flex items-center justify-between",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"text-base font-semibold text-foreground",children:t("title")}),e.jsx("p",{className:"text-xs text-muted mt-0.5",children:t("subtitle")})]}),e.jsx(E,{})]}),r&&e.jsx("div",{className:"grid grid-cols-2 lg:grid-cols-4 gap-3 mb-4",children:S.map(s=>e.jsxs("div",{className:`bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 relative overflow-hidden hover:border-[var(--border-hover)] hover:bg-[var(--card-hover)] transition-all duration-200 ${s.accent}`,children:[e.jsx("div",{className:"absolute top-0 right-0 w-16 h-16 rounded-full opacity-60 pointer-events-none",style:{background:`radial-gradient(circle, ${s.glowColor} 0%, transparent 70%)`}}),e.jsxs("div",{className:"flex items-center justify-between mb-1.5",children:[e.jsx("span",{className:"text-[11px] text-muted",children:s.label}),e.jsx("span",{className:`${s.iconColor} opacity-80`,children:s.icon})]}),e.jsx("div",{className:`text-lg font-semibold ${s.warn?"text-red-400":"text-foreground"}`,children:s.value}),s.sub&&e.jsx("div",{className:`text-[11px] mt-0.5 truncate ${s.subColor??"text-muted"}`,children:s.sub})]},s.label))}),e.jsxs("div",{className:"bg-[var(--card)] border border-[var(--border)] rounded-xl overflow-hidden",children:[e.jsxs("div",{className:"px-4 py-2.5 border-b border-[var(--border)] flex items-center justify-between",children:[e.jsx("h2",{className:"text-sm font-medium text-foreground",children:t("instances.title")}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{onClick:()=>g("/instances/new"),className:"bg-[#0066FF] text-white px-3 py-1.5 rounded-md text-xs font-medium hover:bg-[#0066FF]/90 transition-all duration-200 shadow-[0_0_12px_rgba(0,102,255,0.3)]",children:t("instances.new")}),e.jsx("button",{onClick:()=>g("/instances/new?import=true"),className:"px-3 py-1.5 rounded-md text-xs font-medium text-muted border border-[var(--border)] bg-[var(--card)] hover:bg-[var(--card-hover)] hover:text-foreground transition-all duration-200",children:t("instances.import")}),e.jsxs("button",{onClick:_,disabled:p,title:t("instances.restartEngineTitle"),className:"flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium text-red-400 border border-red-500/20 bg-red-500/5 hover:bg-red-500/15 disabled:opacity-40 disabled:cursor-not-allowed transition-all duration-200",children:[e.jsx(k,{className:`w-3 h-3 ${p?"animate-spin":""}`}),t(p?"instances.restarting":"instances.restartEngine")]})]})]}),y?e.jsxs("div",{className:"text-center py-12 px-4",children:[e.jsx("p",{className:"text-sm text-red-400 mb-2",children:t("instances.loadError",{error:y})}),e.jsx("button",{onClick:u,className:"text-xs text-muted hover:text-foreground underline",children:t("common:action.retry")})]}):l.length===0?e.jsxs("div",{className:"text-center py-12 px-4",children:[e.jsx("div",{className:"text-muted opacity-40 mb-3",children:e.jsxs("svg",{className:"w-8 h-8 mx-auto",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1,strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}),e.jsx("line",{x1:"8",y1:"21",x2:"16",y2:"21"}),e.jsx("line",{x1:"12",y1:"17",x2:"12",y2:"21"})]})}),e.jsx("p",{className:"text-sm text-muted mb-1",children:t("instances.empty")}),e.jsx("p",{className:"text-xs text-muted opacity-60",children:t("instances.emptyHint")})]}):e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"text-xs text-muted border-b border-[var(--border)]",children:[e.jsx("th",{className:"text-left font-medium px-4 py-2",children:t("table.name")}),e.jsx("th",{className:"text-left font-medium px-4 py-2",children:t("table.status")}),e.jsx("th",{className:"text-left font-medium px-4 py-2 hidden sm:table-cell",children:t("table.uptime")}),e.jsx("th",{className:"text-left font-medium px-4 py-2 hidden md:table-cell",children:t("table.memory")}),e.jsx("th",{className:"text-right font-medium px-4 py-2",children:t("table.actions")})]})}),e.jsx("tbody",{className:"divide-y divide-[var(--border)]",children:l.map(s=>{var n,o,w;const a=((n=s.service)==null?void 0:n.status)||"stopped",i=a==="running";return e.jsxs("tr",{className:"hover:bg-[var(--card-hover)] cursor-pointer transition-colors duration-150",onClick:()=>g(`/instances/${s.id}`),children:[e.jsxs("td",{className:"px-4 py-2.5",children:[e.jsx("div",{className:"font-medium text-foreground",children:s.name}),e.jsx("div",{className:"text-xs text-muted font-mono",children:s.id})]}),e.jsx("td",{className:"px-4 py-2.5",children:e.jsxs("div",{className:"flex items-center gap-1.5",children:[e.jsx(G,{status:a}),((o=s.auto_backup)==null?void 0:o.enabled)&&e.jsx("span",{className:`text-xs leading-none ${s.auto_backup.last_backup_ok===!1?"text-red-400":"text-green-400"}`,title:s.auto_backup.last_backup_ok===!1?"Backup failed":"Backup OK",children:s.auto_backup.last_backup_ok===!1?"⚠":"●"})]})}),e.jsx("td",{className:"px-4 py-2.5 text-muted hidden sm:table-cell font-mono text-xs",children:i?q(s.service.uptime):"-"}),e.jsx("td",{className:"px-4 py-2.5 text-muted hidden md:table-cell font-mono text-xs",children:(w=s.service)!=null&&w.memory_mb?`${s.service.memory_mb} MB`:"-"}),e.jsx("td",{className:"px-4 py-2.5 text-right",children:e.jsx("div",{className:"inline-flex items-center gap-1",children:i||a==="pending"?e.jsxs(e.Fragment,{children:[e.jsx("button",{title:t("common:action.restart"),onClick:x=>f(x,"restart",s.id),disabled:!!h,className:"p-1.5 rounded-md text-muted hover:text-foreground hover:bg-[var(--card-hover)] disabled:opacity-30 transition-colors duration-150",children:e.jsx(k,{className:"w-3.5 h-3.5"})}),e.jsx("button",{title:t("common:action.stop"),onClick:x=>f(x,"stop",s.id),disabled:!!h,className:"p-1.5 rounded-md text-muted hover:text-red-400 hover:bg-red-500/10 disabled:opacity-30 transition-colors duration-150",children:e.jsx(M,{className:"w-3.5 h-3.5"})})]}):e.jsx("button",{title:t("common:action.start"),onClick:x=>f(x,"start",s.id),disabled:!!h,className:"p-1.5 rounded-md text-muted hover:text-emerald-400 hover:bg-emerald-500/10 disabled:opacity-30 transition-colors duration-150",children:e.jsx(R,{className:"w-3.5 h-3.5"})})})})]},s.id)})})]})]})]})}export{z as default};