@wingsky-1/dsh-decision-gateway 0.2.6

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +56 -0
  3. package/README.md +56 -0
  4. package/cordis.patch.yml +7 -0
  5. package/lib/THIRD-PARTY-LICENSES +35 -0
  6. package/lib/client/api/contract.d.ts +37 -0
  7. package/lib/client/api/interface.d.ts +9 -0
  8. package/lib/client/api/routes.d.ts +11 -0
  9. package/lib/client/index.d.ts +8 -0
  10. package/lib/client/locale.d.ts +31 -0
  11. package/lib/client/locales.d.ts +122 -0
  12. package/lib/client/settings/card.d.ts +7 -0
  13. package/lib/client/settings/connection.d.ts +10 -0
  14. package/lib/client/settings/format.d.ts +4 -0
  15. package/lib/client/settings/history.d.ts +9 -0
  16. package/lib/client/settings/presets.d.ts +9 -0
  17. package/lib/client/settings/prob.d.ts +10 -0
  18. package/lib/client.js +1518 -0
  19. package/lib/index.d.ts +32 -0
  20. package/lib/index.js +3142 -0
  21. package/lib/server/api/deps.d.ts +53 -0
  22. package/lib/server/api/impl/handlers.d.ts +11 -0
  23. package/lib/server/api/impl/route.d.ts +28 -0
  24. package/lib/server/api/impl/service.d.ts +4 -0
  25. package/lib/server/api/interface.d.ts +3 -0
  26. package/lib/server/config/deps.d.ts +32 -0
  27. package/lib/server/config/impl/envelope.d.ts +9 -0
  28. package/lib/server/config/impl/model.d.ts +54 -0
  29. package/lib/server/config/impl/paths.d.ts +12 -0
  30. package/lib/server/config/impl/service.d.ts +37 -0
  31. package/lib/server/config/interface.d.ts +12 -0
  32. package/lib/server/history/deps.d.ts +25 -0
  33. package/lib/server/history/impl/entry.d.ts +29 -0
  34. package/lib/server/history/impl/hash.d.ts +8 -0
  35. package/lib/server/history/impl/paths.d.ts +2 -0
  36. package/lib/server/history/impl/redact.d.ts +2 -0
  37. package/lib/server/history/impl/service.d.ts +30 -0
  38. package/lib/server/history/interface.d.ts +4 -0
  39. package/lib/server/store/deps.d.ts +6 -0
  40. package/lib/server/store/impl/io.d.ts +24 -0
  41. package/lib/server/store/interface.d.ts +4 -0
  42. package/lib/server/tools/deps.d.ts +83 -0
  43. package/lib/server/tools/impl/client.d.ts +62 -0
  44. package/lib/server/tools/impl/define.d.ts +30 -0
  45. package/lib/server/tools/impl/precheck.d.ts +8 -0
  46. package/lib/server/tools/impl/probe.d.ts +24 -0
  47. package/lib/server/tools/impl/semaphore.d.ts +4 -0
  48. package/lib/server/tools/impl/service.d.ts +18 -0
  49. package/lib/server/tools/impl/validate.d.ts +17 -0
  50. package/lib/server/tools/interface.d.ts +7 -0
  51. package/lib/server/upgrade/deps.d.ts +37 -0
  52. package/lib/server/upgrade/impl/service.d.ts +13 -0
  53. package/lib/server/upgrade/impl/steps.d.ts +3 -0
  54. package/lib/server/upgrade/interface.d.ts +6 -0
  55. package/lib/shared/contract.d.ts +184 -0
  56. package/lib/shared/interface.d.ts +7 -0
  57. package/package.json +99 -0
  58. package/shared/client/ensure-style.d.ts +21 -0
  59. package/shared/client/i18n.d.ts +15 -0
  60. package/shared/dsh-home.d.ts +15 -0
  61. package/shared/host-utils.d.ts +65 -0
  62. package/shared/loopback.d.ts +21 -0
  63. package/shared/paths.d.ts +12 -0
  64. package/shared/settings-namespace.d.ts +82 -0
  65. package/shared/sse-hub.d.ts +76 -0
  66. package/shared/upgrade-tick.d.ts +11 -0
package/lib/index.js ADDED
@@ -0,0 +1,3142 @@
1
+ import { createRequire as __decisionGatewayRequire } from "node:module";
2
+ const require = __decisionGatewayRequire(import.meta.url);
3
+
4
+ // lib/shared/contract.js
5
+ var PACKAGE_DIR = "@wingsky-1/dsh-decision-gateway";
6
+ var CONFIG_VERSION = 1;
7
+ var TEMPLATE_VERSION = 1;
8
+ var JEV_BASE_URL = "https://api.typesafe.ai/v1/systemone";
9
+ if (JEV_BASE_URL !== "https://api.typesafe.ai/v1/systemone") {
10
+ throw new Error("dsh-decision-gateway: JEV_BASE_URL 被篡改,拒绝加载");
11
+ }
12
+ var JEV_MODEL = "jev-latest";
13
+ if (JEV_MODEL !== "jev-latest") {
14
+ throw new Error("dsh-decision-gateway: JEV_MODEL 被篡改,拒绝加载");
15
+ }
16
+ var CONFIG_FILE_NAME = "config.json";
17
+ var PRESETS_FILE_NAME = "presets.json";
18
+ var SECRETS_FILE_NAME = "secrets.json";
19
+ var CUSTOM_PRESETS_FILE_NAME = "custom-presets.json";
20
+ var VERSION_FILE_NAME = "VERSION";
21
+ var ROUTES = {
22
+ health: "/api/dsh-decision-gateway/health",
23
+ config: "/api/dsh-decision-gateway/config",
24
+ presets: "/api/dsh-decision-gateway/presets",
25
+ history: "/api/dsh-decision-gateway/history",
26
+ testConnection: "/api/dsh-decision-gateway/test-connection"
27
+ };
28
+ var API_KEY_REF_RE = /^[A-Z][A-Z0-9_]{1,63}$/;
29
+ var PRESET_ID_RE = /^[a-z0-9-]{1,64}$/;
30
+ var QUESTION_ID_RE = /^[a-z0-9-]{1,64}$/;
31
+ var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
32
+ var CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
33
+ var RETIRED_KEYS = ["baseUrl", "apiBaseUrl", "endpoint", "apiEndpoint", "url"];
34
+ var MAX_QUESTION_TEXT = 255;
35
+ var MAX_QUESTIONS = 20;
36
+ var SNIPPET_MAX = 200;
37
+ var FROZEN_PRESETS = [
38
+ {
39
+ id: "general",
40
+ label: "general",
41
+ description: "Goal: teach callers how to ask a 2-10-candidate choice question over comparable options. Dimensions: option completeness, comparability, single conclusion. Mutual exclusion: the verdict must be one of the caller-supplied candidates, never an outside candidate and never an abstention. Good asking: background in state.text plus reachable, comparable candidates via questions_override (unique ids, 2-10 options per choice question). Bad asking: listing phenomena with no candidates, which forces a restating non-answer. Prohibitions: no preset questions; no outside-candidate verdicts; no restating without deciding. How to call: state.text carries background, questions_override carries the candidates.",
42
+ defaultEnabled: true,
43
+ automationCap: 2
44
+ },
45
+ {
46
+ id: "secret-leak",
47
+ label: "secret-leak",
48
+ description: "Goal: teach callers how to ask whether a text contains a real secret that must not leave the device. Dimensions: secret-shape match, surrounding semantics, certainty grading. Mutual exclusion: a local shape hit goes straight to human review with no automated verdict. Good asking: put the text under review in state.text with leak/clean candidates; genuine key shapes yield leak and human handoff. Bad asking: asking about key-management concepts in general yields clean. Prohibitions: never store real secret examples in templates; never judge concept discussion as a leak; never let secret text leave the device. How to call: state.text carries the text under review (any language), candidates are leak/clean. Disabled by default.",
49
+ defaultEnabled: false,
50
+ automationCap: 0
51
+ },
52
+ {
53
+ id: "plan-review",
54
+ label: "plan-review",
55
+ description: "Goal: teach callers how to ask a 1-5 plan rating question. Dimensions: completeness, feasibility, risk exposure. Mutual exclusion: score only, no go/no-go decision; the score never substitutes for a proceed decision. Good asking: per-dimension sub-scores rolled up into a total, e.g. landing at 4/5. Bad asking: demanding a total with no dimensions. Prohibitions: no preset plans in templates. How to call: state.text carries the plan, questions_override carries one score question (no options allowed).",
56
+ defaultEnabled: true,
57
+ automationCap: 1
58
+ },
59
+ {
60
+ id: "risk-check",
61
+ label: "risk-check",
62
+ description: "Goal: teach callers how to ask a binary proceed-or-not question. Dimensions: safety, compliance, reversibility. Mutual exclusion: safe/risky only, no middle state. Good asking: all three dimensions pass, verdict safe. Bad asking: judging safe while one dimension is doubtful. Prohibitions: no preset checklists in templates; never substitute hand-waving for a dimension verdict. How to call: state.text carries the change, candidates safe/risky come from the caller.",
63
+ defaultEnabled: true,
64
+ automationCap: 1
65
+ },
66
+ {
67
+ id: "custom",
68
+ label: "custom",
69
+ description: "Goal: the caller brings the full question set (1-20 questions); the template presets nothing. Mutual exclusion: questions_override is required and must be non-empty. Good calling: the first question drives the verdict; extra questions travel as context. Bad calling: empty questions are rejected outright. Prohibitions: no execution without questions; never rewrite caller questions. How to call: preset_id custom plus a full questions_override (unique ids, choice carries options, score carries none).",
70
+ defaultEnabled: true,
71
+ automationCap: 2
72
+ }
73
+ ];
74
+ var CUSTOM_PRESETS_VERSION = 1;
75
+ var MAX_CUSTOM_PRESETS = 50;
76
+ var MAX_CUSTOM_LABEL = 64;
77
+ var MAX_CUSTOM_DESCRIPTION = 2e3;
78
+ function frozenPresetOf(id) {
79
+ for (const preset of FROZEN_PRESETS) {
80
+ if (preset.id === id)
81
+ return preset;
82
+ }
83
+ return void 0;
84
+ }
85
+ var ALL_CAPS_REJECT_MIN_LEN = 20;
86
+ var ALL_CAPS_RUN_RE = /^[A-Z0-9_]+$/;
87
+ function hasExposedPrefix(key) {
88
+ return key.startsWith("AKIA") || key.startsWith("ASIA");
89
+ }
90
+ function rejectAllCapsLongRun(key) {
91
+ if (key.length < ALL_CAPS_REJECT_MIN_LEN)
92
+ return false;
93
+ if (!ALL_CAPS_RUN_RE.test(key))
94
+ return false;
95
+ if (hasExposedPrefix(key))
96
+ return true;
97
+ return /[A-Z]/.test(key) && /[0-9]/.test(key);
98
+ }
99
+ function keyShapeCategory(key) {
100
+ if (key.length === 0)
101
+ return "empty";
102
+ if (key.length < 16)
103
+ return "too-short";
104
+ if (key.length > 256 || !/^[A-Za-z0-9._~-]+$/.test(key))
105
+ return "charset";
106
+ if (!/[A-Za-z]/.test(key))
107
+ return "charset";
108
+ if (rejectAllCapsLongRun(key))
109
+ return "charset";
110
+ return null;
111
+ }
112
+ function truncateCodePoints(text, budget) {
113
+ const points = Array.from(text);
114
+ if (points.length <= budget)
115
+ return { text, truncated: false, originalLength: points.length };
116
+ return { text: points.slice(0, budget).join(""), truncated: true, originalLength: points.length };
117
+ }
118
+ function containsCjk(text) {
119
+ return CJK_RE.test(text);
120
+ }
121
+
122
+ // ../../shared/loopback.js
123
+ function isLoopbackRequest(request, options = {}) {
124
+ const address = request.socket?.remoteAddress;
125
+ if (address !== "127.0.0.1" && address !== "::1" && address !== "::ffff:127.0.0.1") return false;
126
+ const host = request.headers.host;
127
+ if (typeof host !== "string") return false;
128
+ let hostUrl;
129
+ try {
130
+ hostUrl = new URL(`http://${host}`);
131
+ } catch {
132
+ return false;
133
+ }
134
+ if (hostUrl.hostname !== "127.0.0.1" && hostUrl.hostname !== "localhost" && hostUrl.hostname !== "[::1]")
135
+ return false;
136
+ if (request.headers["sec-fetch-site"] === "cross-site") {
137
+ if (!(options.allowCrossSiteNoCors === true && request.headers["sec-fetch-mode"] === "no-cors")) {
138
+ return false;
139
+ }
140
+ }
141
+ const origin = request.headers.origin;
142
+ if (origin === void 0) return true;
143
+ try {
144
+ return new URL(origin).host === hostUrl.host;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ // lib/server/api/impl/route.js
151
+ function sendJson(res, status, body) {
152
+ res.writeHead(status, {
153
+ "content-type": "application/json; charset=utf-8",
154
+ "cache-control": "no-store"
155
+ });
156
+ res.end(JSON.stringify(body));
157
+ }
158
+ function sendFailure(res, status, errorCode, category, message) {
159
+ sendJson(res, status, { ok: false, error: { errorCode, category, message } });
160
+ }
161
+ function registerEndpoints(register, endpoints, logger) {
162
+ const disposers = [];
163
+ for (const endpoint of endpoints) {
164
+ disposers.push(register({
165
+ kind: "exact",
166
+ path: endpoint.path,
167
+ handler: (req, res) => {
168
+ if (!isLoopbackRequest(req)) {
169
+ sendFailure(res, 403, "FORBIDDEN_LOOPBACK", "loopback-only", "forbidden: loopback-only");
170
+ return;
171
+ }
172
+ const handle = endpoint.methods[req.method ?? ""];
173
+ if (handle === void 0) {
174
+ res.writeHead(405, {
175
+ "content-type": "application/json; charset=utf-8",
176
+ allow: Object.keys(endpoint.methods).join(", ")
177
+ });
178
+ res.end(JSON.stringify({
179
+ ok: false,
180
+ error: {
181
+ errorCode: "METHOD_NOT_ALLOWED",
182
+ category: "method-not-allowed",
183
+ message: "method not allowed"
184
+ }
185
+ }));
186
+ return;
187
+ }
188
+ try {
189
+ const done = handle(req, res);
190
+ if (done instanceof Promise) {
191
+ done.catch((cause) => reportFailure(res, logger, cause));
192
+ }
193
+ } catch (cause) {
194
+ reportFailure(res, logger, cause);
195
+ }
196
+ }
197
+ }));
198
+ }
199
+ return disposers;
200
+ }
201
+ function reportFailure(res, logger, cause) {
202
+ const reason = cause instanceof Error ? cause.message : String(cause);
203
+ logger.warn("dsh-decision-gateway: 端点处理失败 —— " + reason);
204
+ if (!res.headersSent)
205
+ sendFailure(res, 500, "INTERNAL", "internal", reason);
206
+ }
207
+
208
+ // lib/server/api/impl/handlers.js
209
+ var MAX_BODY_BYTES = 64 * 1024;
210
+ function readJsonBody(req) {
211
+ return new Promise((resolve, reject) => {
212
+ const chunks = [];
213
+ let size = 0;
214
+ req.on("data", (chunk) => {
215
+ size += chunk.length;
216
+ if (size > MAX_BODY_BYTES) {
217
+ reject(new Error("body too large"));
218
+ return;
219
+ }
220
+ chunks.push(chunk);
221
+ });
222
+ req.on("end", () => {
223
+ const text = Buffer.concat(chunks).toString("utf8").trim();
224
+ if (text.length === 0) {
225
+ resolve({});
226
+ return;
227
+ }
228
+ try {
229
+ resolve(JSON.parse(text));
230
+ } catch {
231
+ reject(new Error("body not JSON"));
232
+ }
233
+ });
234
+ req.on("error", (cause) => reject(cause instanceof Error ? cause : new Error(String(cause))));
235
+ });
236
+ }
237
+ function queryOf(req) {
238
+ const raw = req.url ?? "/";
239
+ return new URL(raw, "http://127.0.0.1").searchParams;
240
+ }
241
+ function numParam(params, key) {
242
+ const raw = params.get(key);
243
+ if (raw === null || raw === "")
244
+ return void 0;
245
+ const num = Number(raw);
246
+ return Number.isFinite(num) ? num : void 0;
247
+ }
248
+ function buildEndpoints(ports) {
249
+ return [
250
+ {
251
+ path: ROUTES.health,
252
+ methods: {
253
+ GET: (_req, res) => {
254
+ sendJson(res, 200, {
255
+ ok: true,
256
+ version: ports.version(),
257
+ templateVersion: TEMPLATE_VERSION
258
+ });
259
+ }
260
+ }
261
+ },
262
+ {
263
+ path: ROUTES.config,
264
+ methods: {
265
+ GET: (_req, res) => {
266
+ sendJson(res, 200, ports.readConfig());
267
+ },
268
+ PUT: (req, res) => {
269
+ readJsonBody(req).then((body) => {
270
+ try {
271
+ sendJson(res, 200, ports.writeConfig(body));
272
+ } catch (cause) {
273
+ const failure = asFailure(cause);
274
+ sendFailure(res, failure.status, failure.errorCode, failure.category, failure.message);
275
+ }
276
+ }, () => sendFailure(res, 400, "INVALID_BODY", "bad-request", "body must be JSON <=64KB")).catch(() => sendFailure(res, 500, "INTERNAL", "internal", "unreachable"));
277
+ }
278
+ }
279
+ },
280
+ {
281
+ path: ROUTES.presets,
282
+ methods: {
283
+ GET: (_req, res) => {
284
+ sendJson(res, 200, { ok: true, presets: ports.readPresets() });
285
+ }
286
+ }
287
+ },
288
+ {
289
+ path: ROUTES.history,
290
+ methods: {
291
+ GET: (req, res) => {
292
+ try {
293
+ const params = queryOf(req);
294
+ const entries = ports.readHistory({
295
+ root: params.get("root") ?? void 0,
296
+ sessionId: params.get("sessionId") ?? void 0,
297
+ limit: numParam(params, "limit")
298
+ });
299
+ sendJson(res, 200, { ok: true, entries });
300
+ } catch (cause) {
301
+ const failure = asFailure(cause);
302
+ sendFailure(res, failure.status, failure.errorCode, failure.category, failure.message);
303
+ }
304
+ },
305
+ DELETE: (req, res) => {
306
+ try {
307
+ const params = queryOf(req);
308
+ const removed = ports.removeHistory({
309
+ root: params.get("root") ?? void 0,
310
+ sessionId: params.get("sessionId") ?? void 0
311
+ });
312
+ sendJson(res, 200, { ok: true, deleted: removed.deleted });
313
+ } catch (cause) {
314
+ const failure = asFailure(cause);
315
+ sendFailure(res, failure.status, failure.errorCode, failure.category, failure.message);
316
+ }
317
+ }
318
+ }
319
+ },
320
+ {
321
+ path: ROUTES.testConnection,
322
+ methods: {
323
+ POST: (req, res) => {
324
+ readJsonBody(req).then((body) => {
325
+ ports.probeConnection(body).then((probe) => {
326
+ if (probe.ok)
327
+ sendJson(res, 200, { ok: true, latencyMs: probe.latencyMs });
328
+ else
329
+ sendFailure(res, probeStatus(probe.errorCode), probe.errorCode, probe.category, probe.message);
330
+ }, (cause) => {
331
+ const failure = asFailure(cause);
332
+ sendFailure(res, failure.status, failure.errorCode, failure.category, failure.message);
333
+ }).catch(() => sendFailure(res, 500, "INTERNAL", "internal", "unreachable"));
334
+ }, () => sendFailure(res, 400, "INVALID_BODY", "bad-request", "body must be JSON <=64KB")).catch(() => sendFailure(res, 500, "INTERNAL", "internal", "unreachable"));
335
+ }
336
+ }
337
+ }
338
+ ];
339
+ }
340
+ function asFailure(cause) {
341
+ const message = cause instanceof Error ? cause.message : String(cause);
342
+ try {
343
+ const parsed = JSON.parse(message);
344
+ if (typeof parsed.status === "number" && typeof parsed.errorCode === "string" && typeof parsed.category === "string" && typeof parsed.message === "string") {
345
+ return {
346
+ status: parsed.status,
347
+ errorCode: parsed.errorCode,
348
+ category: parsed.category,
349
+ message: parsed.message
350
+ };
351
+ }
352
+ } catch {
353
+ }
354
+ const match = /\[(\d+)\]/.exec(message);
355
+ if (match !== null) {
356
+ const status = Number(match[1]);
357
+ return {
358
+ status,
359
+ errorCode: "BAD_REQUEST",
360
+ category: "bad-request",
361
+ message: message.replace(match[0], "").trim()
362
+ };
363
+ }
364
+ return { status: 500, errorCode: "INTERNAL", category: "internal", message };
365
+ }
366
+ function probeStatus(errorCode) {
367
+ if (errorCode === "NO_KEY" || errorCode === "UNAUTHORIZED")
368
+ return 401;
369
+ if (errorCode === "TIMEOUT")
370
+ return 504;
371
+ return 502;
372
+ }
373
+
374
+ // lib/server/api/impl/service.js
375
+ function installApi(deps) {
376
+ return registerEndpoints(deps.register, buildEndpoints(deps), deps.logger);
377
+ }
378
+
379
+ // lib/server/config/impl/model.js
380
+ var ALLOWED_PUT_KEYS = [
381
+ "apiKeyRef",
382
+ "apiKeyPlaintext",
383
+ "confirm",
384
+ "timeoutMs",
385
+ "maxConcurrency",
386
+ "truncBudget",
387
+ "presets",
388
+ "customPresets",
389
+ "history"
390
+ ];
391
+ function buildDefaultConfig() {
392
+ return {
393
+ version: CONFIG_VERSION,
394
+ connection: { hasPlaintextKey: false, timeoutMs: 8e3, maxConcurrency: 4, truncBudget: 32e3 },
395
+ presets: FROZEN_PRESETS.map((preset) => ({
396
+ id: preset.id,
397
+ enabled: preset.defaultEnabled,
398
+ automationCap: preset.automationCap
399
+ })),
400
+ history: { perSession: 200, totalSessions: 50 }
401
+ };
402
+ }
403
+ function isRecord(value) {
404
+ return value !== null && typeof value === "object" && !Array.isArray(value);
405
+ }
406
+ function asFinite(value) {
407
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
408
+ }
409
+ function checkInt(value, min, max) {
410
+ const num = asFinite(value);
411
+ if (num === void 0 || !Number.isInteger(num) || num < min || num > max)
412
+ return void 0;
413
+ return num;
414
+ }
415
+ function normalizeLoadedConfig(raw) {
416
+ const fallback = buildDefaultConfig();
417
+ if (!isRecord(raw))
418
+ return { config: fallback, retired: [] };
419
+ const retired = [];
420
+ const scanScopes = [raw];
421
+ for (const scope of [raw["connection"], raw["history"]]) {
422
+ if (isRecord(scope))
423
+ scanScopes.push(scope);
424
+ }
425
+ for (const key of RETIRED_KEYS) {
426
+ if (scanScopes.some((scope) => key in scope) && !retired.includes(key))
427
+ retired.push(key);
428
+ }
429
+ const connRaw = isRecord(raw["connection"]) ? raw["connection"] : {};
430
+ const histRaw = isRecord(raw["history"]) ? raw["history"] : {};
431
+ const presetsRaw = Array.isArray(raw["presets"]) ? raw["presets"] : [];
432
+ const byId = new Map(fallback.presets.map((entry) => [entry.id, entry]));
433
+ for (const item of presetsRaw) {
434
+ if (!isRecord(item) || typeof item["id"] !== "string")
435
+ continue;
436
+ const prev = byId.get(item["id"]);
437
+ if (prev === void 0)
438
+ continue;
439
+ const enabled = typeof item["enabled"] === "boolean" ? item["enabled"] : prev.enabled;
440
+ const cap = item["automationCap"];
441
+ const automationCap = cap === 0 || cap === 1 || cap === 2 ? cap : prev.automationCap;
442
+ byId.set(item["id"], { id: prev.id, enabled, automationCap });
443
+ }
444
+ const refRaw = connRaw["apiKeyRef"];
445
+ const apiKeyRef = typeof refRaw === "string" && API_KEY_REF_RE.test(refRaw) ? refRaw : void 0;
446
+ const timeout = checkInt(connRaw["timeoutMs"], 1e3, 12e4) ?? fallback.connection.timeoutMs;
447
+ const concurrency = checkInt(connRaw["maxConcurrency"], 1, 16) ?? fallback.connection.maxConcurrency;
448
+ const budget = checkInt(connRaw["truncBudget"], 1e3, 2e5) ?? fallback.connection.truncBudget;
449
+ const perSession = checkInt(histRaw["perSession"], 10, 1e3) ?? fallback.history.perSession;
450
+ const totalSessions = checkInt(histRaw["totalSessions"], 1, 200) ?? fallback.history.totalSessions;
451
+ return {
452
+ config: {
453
+ version: CONFIG_VERSION,
454
+ connection: {
455
+ ...apiKeyRef !== void 0 ? { apiKeyRef } : {},
456
+ hasPlaintextKey: connRaw["hasPlaintextKey"] === true,
457
+ timeoutMs: timeout,
458
+ maxConcurrency: concurrency,
459
+ truncBudget: budget
460
+ },
461
+ presets: FROZEN_PRESETS.map((preset) => byId.get(preset.id) ?? {
462
+ id: preset.id,
463
+ enabled: preset.defaultEnabled,
464
+ automationCap: preset.automationCap
465
+ }),
466
+ history: { perSession, totalSessions }
467
+ },
468
+ retired
469
+ };
470
+ }
471
+ function validateCustomPresets(raw) {
472
+ const bad = (errorCode, message) => ({
473
+ ok: false,
474
+ failure: { errorCode, category: "bad-request", message }
475
+ });
476
+ if (!Array.isArray(raw))
477
+ return bad("INVALID_CUSTOM", "customPresets must be an array");
478
+ if (raw.length > MAX_CUSTOM_PRESETS) {
479
+ return bad("INVALID_CUSTOM", "customPresets holds <=" + MAX_CUSTOM_PRESETS + " entries");
480
+ }
481
+ const frozenIds = new Set(FROZEN_PRESETS.map((preset) => preset.id));
482
+ const seen = /* @__PURE__ */ new Set();
483
+ const list = [];
484
+ const now = Date.now();
485
+ for (const item of raw) {
486
+ if (!isRecord(item) || typeof item["id"] !== "string") {
487
+ return bad("INVALID_CUSTOM", "custom preset needs id");
488
+ }
489
+ const id = item["id"];
490
+ if (!PRESET_ID_RE.test(id) || containsCjk(id)) {
491
+ return bad("BAD_CUSTOM_ID", "custom id must be ASCII a-z0-9- 1..64");
492
+ }
493
+ if (frozenIds.has(id)) {
494
+ return bad("RESERVED_PRESET", "custom id collides with a frozen preset: " + id);
495
+ }
496
+ if (seen.has(id))
497
+ return bad("DUPLICATE_CUSTOM_ID", "custom ids must be unique");
498
+ seen.add(id);
499
+ const label = item["label"];
500
+ if (typeof label !== "string" || label.trim().length === 0 || Array.from(label).length > MAX_CUSTOM_LABEL) {
501
+ return bad("INVALID_CUSTOM", "custom label must be 1.." + MAX_CUSTOM_LABEL + " chars");
502
+ }
503
+ const description = item["description"];
504
+ if (typeof description !== "string" || description.trim().length === 0 || Array.from(description).length > MAX_CUSTOM_DESCRIPTION) {
505
+ return bad("INVALID_CUSTOM", "custom description must be 1.." + MAX_CUSTOM_DESCRIPTION + " chars");
506
+ }
507
+ if (typeof item["enabled"] !== "boolean") {
508
+ return bad("INVALID_CUSTOM", "custom entry needs boolean enabled");
509
+ }
510
+ const cap = item["automationCap"];
511
+ if (cap !== 0 && cap !== 1 && cap !== 2) {
512
+ return bad("INVALID_CUSTOM", "automationCap must be 0|1|2");
513
+ }
514
+ const stamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : now;
515
+ list.push({
516
+ id,
517
+ label,
518
+ description,
519
+ enabled: item["enabled"],
520
+ automationCap: cap,
521
+ createdAt: stamp(item["createdAt"]),
522
+ updatedAt: now
523
+ });
524
+ }
525
+ return { ok: true, list };
526
+ }
527
+ var PUT_INT_FIELDS = [
528
+ { key: "timeoutMs", min: 1e3, max: 12e4, message: "timeoutMs must be int 1000..120000" },
529
+ { key: "maxConcurrency", min: 1, max: 16, message: "maxConcurrency must be int 1..16" },
530
+ { key: "truncBudget", min: 1e3, max: 2e5, message: "truncBudget must be int 1000..200000" }
531
+ ];
532
+ var HISTORY_INT_FIELDS = [
533
+ { key: "perSession", min: 10, max: 1e3, message: "perSession must be int 10..1000" },
534
+ { key: "totalSessions", min: 1, max: 200, message: "totalSessions must be int 1..200" }
535
+ ];
536
+ function putRangedInt(source, field) {
537
+ const raw = source[field.key];
538
+ if (raw === void 0)
539
+ return { ok: true, value: void 0 };
540
+ const value = checkInt(raw, field.min, field.max);
541
+ if (value === void 0)
542
+ return {
543
+ ok: false,
544
+ failure: { errorCode: "INVALID_RANGE", category: "bad-request", message: field.message }
545
+ };
546
+ return { ok: true, value };
547
+ }
548
+ function checkApiKeyRef(ref) {
549
+ if (ref === void 0)
550
+ return { ok: true, value: void 0 };
551
+ if (ref === null)
552
+ return { ok: true, value: null };
553
+ if (typeof ref !== "string" || !API_KEY_REF_RE.test(ref)) {
554
+ return {
555
+ ok: false,
556
+ failure: {
557
+ errorCode: "INVALID_REF",
558
+ category: "shape",
559
+ message: "apiKeyRef must match ^[A-Z][A-Z0-9_]{1,63}$"
560
+ }
561
+ };
562
+ }
563
+ return { ok: true, value: ref };
564
+ }
565
+ function checkApiKeyPlaintext(plain) {
566
+ if (plain === void 0)
567
+ return { ok: true, value: void 0 };
568
+ if (typeof plain !== "string") {
569
+ return {
570
+ ok: false,
571
+ failure: {
572
+ errorCode: "INVALID_KEY_SHAPE",
573
+ category: "charset",
574
+ message: "apiKeyPlaintext must be a string"
575
+ }
576
+ };
577
+ }
578
+ const shape = keyShapeCategory(plain);
579
+ if (shape !== null) {
580
+ return {
581
+ ok: false,
582
+ failure: { errorCode: "INVALID_KEY_SHAPE", category: shape, message: "key shape rejected" }
583
+ };
584
+ }
585
+ return { ok: true, value: plain };
586
+ }
587
+ function isValidCap(cap) {
588
+ return cap === 0 || cap === 1 || cap === 2;
589
+ }
590
+ function checkPresetsField(value) {
591
+ if (value === void 0) {
592
+ return { ok: true, list: void 0 };
593
+ }
594
+ if (!Array.isArray(value)) {
595
+ return {
596
+ ok: false,
597
+ failure: {
598
+ errorCode: "INVALID_PRESETS",
599
+ category: "bad-request",
600
+ message: "presets must be an array"
601
+ }
602
+ };
603
+ }
604
+ const list = [];
605
+ for (const item of value) {
606
+ if (!isRecord(item) || typeof item["id"] !== "string") {
607
+ return {
608
+ ok: false,
609
+ failure: {
610
+ errorCode: "INVALID_PRESETS",
611
+ category: "bad-request",
612
+ message: "preset entry needs id"
613
+ }
614
+ };
615
+ }
616
+ const frozen = FROZEN_PRESETS.find((preset) => preset.id === item["id"]);
617
+ if (frozen === void 0) {
618
+ return {
619
+ ok: false,
620
+ failure: {
621
+ errorCode: "UNKNOWN_PRESET",
622
+ category: "bad-request",
623
+ message: "unknown preset"
624
+ }
625
+ };
626
+ }
627
+ if (typeof item["enabled"] !== "boolean") {
628
+ return {
629
+ ok: false,
630
+ failure: {
631
+ errorCode: "INVALID_PRESETS",
632
+ category: "bad-request",
633
+ message: "preset entry needs boolean enabled"
634
+ }
635
+ };
636
+ }
637
+ const cap = item["automationCap"];
638
+ if (!isValidCap(cap)) {
639
+ return {
640
+ ok: false,
641
+ failure: {
642
+ errorCode: "INVALID_PRESETS",
643
+ category: "bad-request",
644
+ message: "automationCap must be 0|1|2"
645
+ }
646
+ };
647
+ }
648
+ list.push({
649
+ id: item["id"],
650
+ enabled: item["enabled"],
651
+ automationCap: cap
652
+ });
653
+ }
654
+ return { ok: true, list };
655
+ }
656
+ function checkHistoryField(value) {
657
+ if (value === void 0) {
658
+ return { ok: true, history: void 0 };
659
+ }
660
+ if (!isRecord(value)) {
661
+ return {
662
+ ok: false,
663
+ failure: {
664
+ errorCode: "INVALID_HISTORY",
665
+ category: "bad-request",
666
+ message: "history must be an object"
667
+ }
668
+ };
669
+ }
670
+ const hist = value;
671
+ const histVals = {};
672
+ for (const field of HISTORY_INT_FIELDS) {
673
+ const checked = putRangedInt(hist, field);
674
+ if (!checked.ok)
675
+ return checked;
676
+ if (checked.value !== void 0)
677
+ histVals[field.key] = checked.value;
678
+ }
679
+ const perSession = histVals["perSession"];
680
+ const totalSessions = histVals["totalSessions"];
681
+ return {
682
+ ok: true,
683
+ history: {
684
+ ...perSession !== void 0 ? { perSession } : {},
685
+ ...totalSessions !== void 0 ? { totalSessions } : {}
686
+ }
687
+ };
688
+ }
689
+ function checkPutKeys(body) {
690
+ if (!isRecord(body)) {
691
+ return {
692
+ ok: false,
693
+ failure: {
694
+ errorCode: "INVALID_BODY",
695
+ category: "bad-request",
696
+ message: "body must be an object"
697
+ }
698
+ };
699
+ }
700
+ for (const key of Object.keys(body)) {
701
+ if (RETIRED_KEYS.includes(key)) {
702
+ return {
703
+ ok: false,
704
+ failure: {
705
+ errorCode: "RETIRED_KEY",
706
+ category: "retired-key",
707
+ message: "retired key: " + key
708
+ }
709
+ };
710
+ }
711
+ if (!ALLOWED_PUT_KEYS.includes(key)) {
712
+ return {
713
+ ok: false,
714
+ failure: {
715
+ errorCode: "UNKNOWN_KEY",
716
+ category: "unknown-key",
717
+ message: "unknown key: " + key
718
+ }
719
+ };
720
+ }
721
+ }
722
+ return null;
723
+ }
724
+ function checkPutApiKey(body) {
725
+ if (!isRecord(body))
726
+ return {
727
+ ok: false,
728
+ failure: {
729
+ errorCode: "INVALID_BODY",
730
+ category: "bad-request",
731
+ message: "body must be an object"
732
+ }
733
+ };
734
+ const ref = body["apiKeyRef"];
735
+ const plain = body["apiKeyPlaintext"];
736
+ const refChecked = checkApiKeyRef(ref);
737
+ if (!refChecked.ok)
738
+ return refChecked;
739
+ const apiKeyRef = refChecked.value;
740
+ const plainChecked = checkApiKeyPlaintext(plain);
741
+ if (!plainChecked.ok)
742
+ return plainChecked;
743
+ const apiKeyPlaintext = plainChecked.value;
744
+ if (apiKeyRef !== void 0 && apiKeyRef !== null && apiKeyPlaintext !== void 0) {
745
+ return {
746
+ ok: false,
747
+ failure: {
748
+ errorCode: "MUTUALLY_EXCLUSIVE",
749
+ category: "mutually-exclusive",
750
+ message: "apiKeyRef and apiKeyPlaintext are mutually exclusive"
751
+ }
752
+ };
753
+ }
754
+ return { ok: true, apiKeyRef, apiKeyPlaintext };
755
+ }
756
+ function checkPutInts(body) {
757
+ if (!isRecord(body))
758
+ return {
759
+ ok: false,
760
+ failure: {
761
+ errorCode: "INVALID_BODY",
762
+ category: "bad-request",
763
+ message: "body must be an object"
764
+ }
765
+ };
766
+ const intVals = {};
767
+ for (const field of PUT_INT_FIELDS) {
768
+ const checked = putRangedInt(body, field);
769
+ if (!checked.ok)
770
+ return checked;
771
+ if (checked.value !== void 0)
772
+ intVals[field.key] = checked.value;
773
+ }
774
+ return { ok: true, intVals };
775
+ }
776
+ function checkPutCustom(body) {
777
+ if (!isRecord(body))
778
+ return {
779
+ ok: false,
780
+ failure: {
781
+ errorCode: "INVALID_BODY",
782
+ category: "bad-request",
783
+ message: "body must be an object"
784
+ }
785
+ };
786
+ let customPresets;
787
+ if (body["customPresets"] !== void 0) {
788
+ const checked = validateCustomPresets(body["customPresets"]);
789
+ if (!checked.ok)
790
+ return checked;
791
+ customPresets = checked.list;
792
+ }
793
+ return { ok: true, customPresets };
794
+ }
795
+ function buildPutPatch(apiKeyRef, apiKeyPlaintext, timeoutMs, maxConcurrency, truncBudget, presets, history, customPresets) {
796
+ return {
797
+ ...apiKeyRef !== void 0 ? { apiKeyRef } : {},
798
+ ...apiKeyPlaintext !== void 0 ? { apiKeyPlaintext } : {},
799
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
800
+ ...maxConcurrency !== void 0 ? { maxConcurrency } : {},
801
+ ...truncBudget !== void 0 ? { truncBudget } : {},
802
+ ...presets !== void 0 ? { presets } : {},
803
+ ...history !== void 0 ? { history } : {},
804
+ ...customPresets !== void 0 ? { customPresets } : {}
805
+ };
806
+ }
807
+ function validatePutBody(body) {
808
+ const keysFailed = checkPutKeys(body);
809
+ if (keysFailed !== null)
810
+ return keysFailed;
811
+ const apiR = checkPutApiKey(body);
812
+ if (!apiR.ok)
813
+ return apiR;
814
+ const apiKeyRef = apiR.apiKeyRef;
815
+ const apiKeyPlaintext = apiR.apiKeyPlaintext;
816
+ const intsR = checkPutInts(body);
817
+ if (!intsR.ok)
818
+ return intsR;
819
+ const intVals = intsR.intVals;
820
+ const timeoutMs = intVals["timeoutMs"];
821
+ const maxConcurrency = intVals["maxConcurrency"];
822
+ const truncBudget = intVals["truncBudget"];
823
+ const checkedPresets = checkPresetsField(body["presets"]);
824
+ if (!checkedPresets.ok) {
825
+ return checkedPresets;
826
+ }
827
+ const presets = checkedPresets.list;
828
+ const checkedHistory = checkHistoryField(body["history"]);
829
+ if (!checkedHistory.ok) {
830
+ return checkedHistory;
831
+ }
832
+ const history = checkedHistory.history;
833
+ const customR = checkPutCustom(body);
834
+ if (!customR.ok)
835
+ return customR;
836
+ const customPresets = customR.customPresets;
837
+ return {
838
+ ok: true,
839
+ patch: buildPutPatch(apiKeyRef, apiKeyPlaintext, timeoutMs, maxConcurrency, truncBudget, presets, history, customPresets)
840
+ };
841
+ }
842
+
843
+ // lib/server/config/impl/envelope.js
844
+ var ENVELOPE_TOP_KEYS = [
845
+ "version",
846
+ "connection",
847
+ "presets",
848
+ "history",
849
+ "apiKeyPlaintext",
850
+ "confirm"
851
+ ];
852
+ var CONNECTION_INNER_KEYS = [
853
+ "apiKeyRef",
854
+ "timeoutMs",
855
+ "maxConcurrency",
856
+ "truncBudget",
857
+ "hasPlaintextKey"
858
+ ];
859
+ function normalizePutEnvelope(body) {
860
+ if (!isRecord(body)) {
861
+ return {
862
+ ok: false,
863
+ failure: {
864
+ errorCode: "INVALID_BODY",
865
+ category: "bad-request",
866
+ message: "body must be an object"
867
+ }
868
+ };
869
+ }
870
+ const conn = body["connection"];
871
+ if (conn === void 0 && body["version"] === void 0)
872
+ return { ok: true, flat: body };
873
+ if (conn !== void 0 && !isRecord(conn)) {
874
+ return {
875
+ ok: false,
876
+ failure: {
877
+ errorCode: "INVALID_BODY",
878
+ category: "bad-request",
879
+ message: "connection must be an object"
880
+ }
881
+ };
882
+ }
883
+ if (body["version"] !== void 0 && body["version"] !== 1) {
884
+ return {
885
+ ok: false,
886
+ failure: {
887
+ errorCode: "INVALID_VERSION",
888
+ category: "bad-request",
889
+ message: "version must be 1"
890
+ }
891
+ };
892
+ }
893
+ for (const key of Object.keys(body)) {
894
+ if (RETIRED_KEYS.includes(key)) {
895
+ return {
896
+ ok: false,
897
+ failure: {
898
+ errorCode: "RETIRED_KEY",
899
+ category: "retired-key",
900
+ message: "retired key: " + key
901
+ }
902
+ };
903
+ }
904
+ if (!ENVELOPE_TOP_KEYS.includes(key) && !ALLOWED_PUT_KEYS.includes(key)) {
905
+ return {
906
+ ok: false,
907
+ failure: {
908
+ errorCode: "UNKNOWN_KEY",
909
+ category: "unknown-key",
910
+ message: "unknown key: " + key
911
+ }
912
+ };
913
+ }
914
+ }
915
+ const flat = {};
916
+ if (isRecord(conn)) {
917
+ for (const key of Object.keys(conn)) {
918
+ if (!CONNECTION_INNER_KEYS.includes(key)) {
919
+ return {
920
+ ok: false,
921
+ failure: {
922
+ errorCode: "UNKNOWN_KEY",
923
+ category: "unknown-key",
924
+ message: "unknown connection key: " + key
925
+ }
926
+ };
927
+ }
928
+ if (key === "hasPlaintextKey")
929
+ continue;
930
+ flat[key] = conn[key];
931
+ }
932
+ }
933
+ for (const key of ALLOWED_PUT_KEYS) {
934
+ if (body[key] !== void 0)
935
+ flat[key] = body[key];
936
+ }
937
+ return { ok: true, flat };
938
+ }
939
+
940
+ // lib/server/config/impl/service.js
941
+ import { readFileSync } from "node:fs";
942
+ import { dirname, join as join4 } from "node:path";
943
+ import { fileURLToPath } from "node:url";
944
+
945
+ // lib/server/config/impl/paths.js
946
+ import { join as join3 } from "node:path";
947
+
948
+ // ../../shared/dsh-home.js
949
+ import { homedir } from "node:os";
950
+ import { join } from "node:path";
951
+ function userHome() {
952
+ const env = process.platform === "win32" ? process.env.USERPROFILE ?? process.env.HOME : process.env.HOME;
953
+ return env !== void 0 && env.trim().length > 0 ? env : homedir();
954
+ }
955
+ function dshHome() {
956
+ const env = process.env.DSH_HOME;
957
+ return env !== void 0 && env.trim().length > 0 ? env : join(userHome(), ".dsh");
958
+ }
959
+
960
+ // ../../shared/paths.js
961
+ import { join as join2 } from "node:path";
962
+ function pluginHome(base, ...segments) {
963
+ return join2(base, ...segments);
964
+ }
965
+
966
+ // lib/server/config/impl/paths.js
967
+ function decisionGatewayHome(home) {
968
+ return pluginHome(home ?? dshHome(), PACKAGE_DIR);
969
+ }
970
+ function configFile(home) {
971
+ return join3(decisionGatewayHome(home), CONFIG_FILE_NAME);
972
+ }
973
+ function presetsFile(home) {
974
+ return join3(decisionGatewayHome(home), PRESETS_FILE_NAME);
975
+ }
976
+ function customPresetsFile(home) {
977
+ return join3(decisionGatewayHome(home), CUSTOM_PRESETS_FILE_NAME);
978
+ }
979
+ function secretsFile(home) {
980
+ return join3(decisionGatewayHome(home), SECRETS_FILE_NAME);
981
+ }
982
+ function versionFile(home) {
983
+ return join3(decisionGatewayHome(home), VERSION_FILE_NAME);
984
+ }
985
+
986
+ // lib/server/config/impl/service.js
987
+ var BASELINE_VERSION = "0.0.0";
988
+ var UNKNOWN_VERSION = "0.0.0";
989
+ function readStoredVersion(home, deps) {
990
+ const read = deps.io.readTextSync(versionFile(home));
991
+ if (!read.ok)
992
+ return BASELINE_VERSION;
993
+ const text = read.text.trim();
994
+ return text === "" ? BASELINE_VERSION : text;
995
+ }
996
+ function writeStoredVersion(home, version, deps) {
997
+ deps.io.atomicWrite0600Sync(versionFile(home), version + "\n");
998
+ }
999
+ function compareVersions(left, right) {
1000
+ const parse = (text) => text.split(".").map((part) => Number.parseInt(part, 10) || 0);
1001
+ const a = parse(left);
1002
+ const b = parse(right);
1003
+ const length = Math.max(a.length, b.length);
1004
+ for (let index = 0; index < length; index += 1) {
1005
+ const difference = (a[index] || 0) - (b[index] || 0);
1006
+ if (difference !== 0)
1007
+ return difference > 0 ? 1 : -1;
1008
+ }
1009
+ return 0;
1010
+ }
1011
+ function pluginVersion() {
1012
+ try {
1013
+ const here = dirname(fileURLToPath(import.meta.url));
1014
+ const manifest = join4(here, "..", "..", "..", "..", "package.json");
1015
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
1016
+ return typeof parsed.version === "string" ? parsed.version : UNKNOWN_VERSION;
1017
+ } catch {
1018
+ return UNKNOWN_VERSION;
1019
+ }
1020
+ }
1021
+ function loadCustomPresets(home, deps) {
1022
+ const raw = deps.io.readJsonSync(customPresetsFile(home));
1023
+ if (!raw.ok)
1024
+ return [];
1025
+ const value = raw.value;
1026
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1027
+ deps.logger.warn("dsh-decision-gateway: custom-presets.json 形状非法,已按空列表读");
1028
+ return [];
1029
+ }
1030
+ const list = value["customPresets"];
1031
+ if (!Array.isArray(list)) {
1032
+ deps.logger.warn("dsh-decision-gateway: custom-presets.json 缺 customPresets 数组,已按空列表读");
1033
+ return [];
1034
+ }
1035
+ const checked = validateCustomPresets(list);
1036
+ if (!checked.ok) {
1037
+ deps.logger.warn("dsh-decision-gateway: custom-presets.json 条目非法,已按空列表读(" + checked.failure.errorCode + ")");
1038
+ return [];
1039
+ }
1040
+ return checked.list;
1041
+ }
1042
+ function loadState(home, deps) {
1043
+ const storedVersion = readStoredVersion(home, deps);
1044
+ const raw = deps.io.readJsonSync(configFile(home));
1045
+ const normalized = normalizeLoadedConfig(raw.ok ? raw.value : void 0);
1046
+ const overlay = deps.io.readJsonSync(presetsFile(home));
1047
+ let config = normalized.config;
1048
+ if (overlay.ok && overlay.value !== null && typeof overlay.value === "object" && !Array.isArray(overlay.value)) {
1049
+ const rec = overlay.value;
1050
+ if (Array.isArray(rec["presets"])) {
1051
+ const again = normalizeLoadedConfig({ connection: {}, presets: rec["presets"], history: {} });
1052
+ const switches = new Map(again.config.presets.map((entry) => [entry.id, entry]));
1053
+ config = {
1054
+ ...config,
1055
+ presets: config.presets.map((entry) => switches.get(entry.id) ?? entry)
1056
+ };
1057
+ }
1058
+ }
1059
+ const secrets = deps.io.readJsonSync(secretsFile(home));
1060
+ let plaintext;
1061
+ if (secrets.ok && secrets.value !== null && typeof secrets.value === "object" && !Array.isArray(secrets.value)) {
1062
+ const rec = secrets.value;
1063
+ if (typeof rec["apiKeyPlaintext"] === "string")
1064
+ plaintext = rec["apiKeyPlaintext"];
1065
+ }
1066
+ const retired = [...normalized.retired];
1067
+ if (retired.length > 0)
1068
+ deps.logger.warn("dsh-decision-gateway: 退役键已剥离 —— " + retired.join(", "));
1069
+ const synced = {
1070
+ ...config,
1071
+ connection: {
1072
+ ...config.connection,
1073
+ hasPlaintextKey: plaintext !== void 0 && plaintext.length > 0
1074
+ }
1075
+ };
1076
+ const customPresets = loadCustomPresets(home, deps);
1077
+ return { config: synced, plaintext, retired, storedVersion, customPresets };
1078
+ }
1079
+ function mergePresetSwitches(current, patch) {
1080
+ const switches = new Map(current.map((entry) => [entry.id, entry]));
1081
+ for (const entry of patch)
1082
+ switches.set(entry.id, { ...entry });
1083
+ return current.map((entry) => switches.get(entry.id) ?? entry);
1084
+ }
1085
+ function savePatch(home, patch, deps) {
1086
+ const current = loadState(home, deps);
1087
+ const next = {
1088
+ ...current.config,
1089
+ connection: {
1090
+ ...current.config.connection,
1091
+ ...patch.apiKeyRef !== void 0 ? patch.apiKeyRef === null ? { apiKeyRef: void 0 } : { apiKeyRef: patch.apiKeyRef } : {},
1092
+ ...patch.timeoutMs !== void 0 ? { timeoutMs: patch.timeoutMs } : {},
1093
+ ...patch.maxConcurrency !== void 0 ? { maxConcurrency: patch.maxConcurrency } : {},
1094
+ ...patch.truncBudget !== void 0 ? { truncBudget: patch.truncBudget } : {}
1095
+ },
1096
+ ...patch.presets !== void 0 ? { presets: mergePresetSwitches(current.config.presets, patch.presets) } : {},
1097
+ ...patch.history !== void 0 ? { history: { ...current.config.history, ...patch.history } } : {}
1098
+ };
1099
+ let plaintext = current.plaintext;
1100
+ if (patch.apiKeyPlaintext !== void 0) {
1101
+ plaintext = patch.apiKeyPlaintext;
1102
+ deps.io.atomicWrite0600Sync(secretsFile(home), JSON.stringify({ apiKeyPlaintext: plaintext }, null, 2) + "\n");
1103
+ } else if (patch.apiKeyRef !== void 0 && patch.apiKeyRef !== null) {
1104
+ plaintext = void 0;
1105
+ deps.io.atomicWrite0600Sync(secretsFile(home), JSON.stringify({}, null, 2) + "\n");
1106
+ }
1107
+ const synced = {
1108
+ ...next,
1109
+ connection: {
1110
+ ...next.connection,
1111
+ hasPlaintextKey: plaintext !== void 0 && plaintext.length > 0
1112
+ }
1113
+ };
1114
+ deps.io.atomicWrite0600Sync(configFile(home), JSON.stringify(synced, null, 2) + "\n");
1115
+ if (patch.presets !== void 0) {
1116
+ deps.io.atomicWrite0600Sync(presetsFile(home), JSON.stringify({ presets: synced.presets }, null, 2) + "\n");
1117
+ }
1118
+ let customPresets = current.customPresets;
1119
+ if (patch.customPresets !== void 0) {
1120
+ const born = new Map(current.customPresets.map((entry) => [entry.id, entry.createdAt]));
1121
+ customPresets = patch.customPresets.map((entry) => ({
1122
+ ...entry,
1123
+ createdAt: born.get(entry.id) ?? entry.createdAt
1124
+ }));
1125
+ deps.io.atomicWrite0600Sync(customPresetsFile(home), JSON.stringify({ version: CUSTOM_PRESETS_VERSION, customPresets }, null, 2) + "\n");
1126
+ }
1127
+ return {
1128
+ config: synced,
1129
+ plaintext,
1130
+ retired: [],
1131
+ storedVersion: current.storedVersion,
1132
+ customPresets
1133
+ };
1134
+ }
1135
+ function toMaskedConfig(state) {
1136
+ return {
1137
+ version: state.config.version,
1138
+ connection: {
1139
+ ...state.config.connection.apiKeyRef !== void 0 ? { apiKeyRef: state.config.connection.apiKeyRef } : {},
1140
+ hasPlaintextKey: state.config.connection.hasPlaintextKey,
1141
+ timeoutMs: state.config.connection.timeoutMs,
1142
+ maxConcurrency: state.config.connection.maxConcurrency,
1143
+ truncBudget: state.config.connection.truncBudget
1144
+ },
1145
+ presets: state.config.presets.map((entry) => ({ ...entry })),
1146
+ history: { ...state.config.history }
1147
+ };
1148
+ }
1149
+ function resolveApiKey(state, env, deps) {
1150
+ const ref = state.config.connection.apiKeyRef;
1151
+ if (ref !== void 0) {
1152
+ const value = env[ref];
1153
+ if (value !== void 0 && value.length > 0)
1154
+ return { key: value, source: "env" };
1155
+ }
1156
+ const plain = state.plaintext;
1157
+ if (plain !== void 0 && plain.length > 0) {
1158
+ if (keyShapeCategory(plain) === null)
1159
+ return { key: plain, source: "plaintext" };
1160
+ deps.logger.warn("dsh-decision-gateway: 明文密钥形状非法,已视同无 key");
1161
+ }
1162
+ return { key: void 0, source: "none" };
1163
+ }
1164
+
1165
+ // lib/server/history/impl/service.js
1166
+ import { basename as basename2, dirname as dirname2, join as join5 } from "node:path";
1167
+
1168
+ // lib/server/history/impl/paths.js
1169
+ function historyRoot(home) {
1170
+ return pluginHome(home ?? dshHome(), PACKAGE_DIR, "history");
1171
+ }
1172
+
1173
+ // lib/server/history/impl/hash.js
1174
+ import { createHash } from "node:crypto";
1175
+ import { basename } from "node:path";
1176
+ function rootHashOf(root) {
1177
+ return createHash("sha256").update(root, "utf8").digest("hex").slice(0, 32);
1178
+ }
1179
+ function stateHashOf(text) {
1180
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 32);
1181
+ }
1182
+ function rootDisplayOf(root) {
1183
+ const base = basename(root);
1184
+ return base === "" ? root.slice(-32) : base;
1185
+ }
1186
+ function resolveRootHash(root) {
1187
+ if (/^[0-9a-f]{16,64}$/.test(root))
1188
+ return root;
1189
+ return rootHashOf(root);
1190
+ }
1191
+
1192
+ // lib/server/history/impl/service.js
1193
+ function historyFile(home, rootHash, sessionId) {
1194
+ assertSessionId(sessionId);
1195
+ if (!/^[0-9a-f]{16,64}$/.test(rootHash))
1196
+ throw new Error("history[400]: rootHash 非法");
1197
+ return join5(historyRoot(home), rootHash, sessionId + ".jsonl");
1198
+ }
1199
+ function assertSessionId(sessionId) {
1200
+ if (typeof sessionId !== "string" || !SESSION_ID_RE.test(sessionId)) {
1201
+ throw new Error("history[400]: sessionId 非法");
1202
+ }
1203
+ }
1204
+ function listSessionFiles(home, deps) {
1205
+ const out = [];
1206
+ const dir = historyRoot(home);
1207
+ for (const root of deps.io.listFilesSync(dir)) {
1208
+ if (root === "" || root.startsWith("."))
1209
+ continue;
1210
+ const sub = join5(dir, root);
1211
+ for (const name2 of deps.io.listFilesSync(sub)) {
1212
+ if (!name2.endsWith(".jsonl"))
1213
+ continue;
1214
+ out.push(join5(sub, name2));
1215
+ }
1216
+ }
1217
+ return out;
1218
+ }
1219
+ function appendEntry(home, entry, limits, deps) {
1220
+ const file = historyFile(home, entry.rootHash, entry.sessionId);
1221
+ deps.io.ensureDir0700(join5(historyRoot(home), entry.rootHash));
1222
+ const prev = deps.io.readTextSync(file);
1223
+ const line = JSON.stringify(entry) + "\n";
1224
+ const merged = (prev.ok ? prev.text : "") + line;
1225
+ const lines = merged.split("\n").filter((item) => item.length > 0);
1226
+ const kept = lines.slice(Math.max(0, lines.length - limits.perSession));
1227
+ deps.io.atomicWrite0600Sync(file, kept.join("\n") + "\n");
1228
+ enforceTotalSessions(home, limits.totalSessions, deps);
1229
+ return file;
1230
+ }
1231
+ function enforceTotalSessions(home, totalSessions, deps) {
1232
+ const files = listSessionFiles(home, deps);
1233
+ if (files.length <= totalSessions)
1234
+ return;
1235
+ const ranked = files.map((file) => ({ file, mtime: deps.io.mtimeMs(file) }));
1236
+ ranked.sort((a, b) => a.mtime - b.mtime);
1237
+ for (const victim of ranked.slice(0, ranked.length - totalSessions)) {
1238
+ deps.io.removeFileSync(victim.file);
1239
+ }
1240
+ }
1241
+ function queryEntries(home, query, deps) {
1242
+ const limit = query.limit === void 0 ? 100 : Math.min(Math.max(1, Math.floor(query.limit)), 500);
1243
+ const rootParam = query.root === void 0 || query.root === "" ? void 0 : query.root;
1244
+ const wantRoot = rootParam === void 0 ? void 0 : /^[0-9a-f]{16,64}$/.test(rootParam) || rootParam.includes("/") || rootParam.includes("\\") ? resolveRootHash(rootParam) : void 0;
1245
+ const wantDisplay = rootParam !== void 0 && wantRoot === void 0 ? rootParam : void 0;
1246
+ if (query.sessionId !== void 0 && query.sessionId !== "")
1247
+ assertSessionId(query.sessionId);
1248
+ const files = listSessionFiles(home, deps);
1249
+ const out = [];
1250
+ for (const file of files) {
1251
+ const name2 = basename2(file);
1252
+ const dirName = basename2(dirname2(file));
1253
+ const sid = name2.endsWith(".jsonl") ? name2.slice(0, -6) : name2;
1254
+ if (wantRoot !== void 0 && dirName !== wantRoot)
1255
+ continue;
1256
+ if (query.sessionId !== void 0 && query.sessionId !== "" && sid !== query.sessionId)
1257
+ continue;
1258
+ const raw = deps.io.readTextSync(file);
1259
+ if (!raw.ok)
1260
+ continue;
1261
+ for (const line of raw.text.split("\n")) {
1262
+ if (line.trim() === "")
1263
+ continue;
1264
+ try {
1265
+ const parsed = JSON.parse(line);
1266
+ if (wantDisplay !== void 0 && parsed.rootDisplay !== wantDisplay)
1267
+ continue;
1268
+ out.push(parsed);
1269
+ } catch {
1270
+ }
1271
+ }
1272
+ }
1273
+ out.sort((a, b) => b.ts - a.ts);
1274
+ return out.slice(0, limit);
1275
+ }
1276
+ function deleteSession(home, query, deps) {
1277
+ if (query.root === void 0 || query.root === "" || query.sessionId === void 0 || query.sessionId === "") {
1278
+ throw new Error("history[400]: 仅支持单会话删除(root 与 sessionId 双必填)");
1279
+ }
1280
+ const rootParam = query.root;
1281
+ const direct = /^[0-9a-f]{16,64}$/.test(rootParam) || rootParam.includes("/") || rootParam.includes("\\");
1282
+ if (direct) {
1283
+ const file = historyFile(home, resolveRootHash(rootParam), query.sessionId);
1284
+ const prev = deps.io.readTextSync(file);
1285
+ if (!prev.ok)
1286
+ return { deleted: false };
1287
+ deps.io.removeFileSync(file);
1288
+ return { deleted: true };
1289
+ }
1290
+ const hits = [];
1291
+ for (const file of listSessionFiles(home, deps)) {
1292
+ const raw = deps.io.readTextSync(file);
1293
+ if (!raw.ok)
1294
+ continue;
1295
+ for (const line of raw.text.split("\n")) {
1296
+ if (line.trim() === "")
1297
+ continue;
1298
+ try {
1299
+ const parsed = JSON.parse(line);
1300
+ if (parsed.rootDisplay === rootParam && parsed.sessionId === query.sessionId) {
1301
+ hits.push(file);
1302
+ break;
1303
+ }
1304
+ } catch {
1305
+ }
1306
+ }
1307
+ }
1308
+ if (hits.length === 0)
1309
+ return { deleted: false };
1310
+ if (hits.length > 1) {
1311
+ throw new Error("history[400]: basename 命中多个会话目录,请传完整路径或 rootHash");
1312
+ }
1313
+ deps.io.removeFileSync(hits[0]);
1314
+ return { deleted: true };
1315
+ }
1316
+
1317
+ // lib/server/history/impl/redact.js
1318
+ var REDACT_PATTERNS = [
1319
+ /sk-[A-Za-z0-9]{8,}/g,
1320
+ /AKIA[0-9A-Z]{16}/g,
1321
+ /ghp_[A-Za-z0-9]{8,}/g,
1322
+ /xox[bpas]-[A-Za-z0-9-]+/g,
1323
+ // S2:头到尾非贪婪不限长——200 字窗口会把长 PEM 切成“掩码半截+密钥体残留”,截断后再存约 170 字原文。
1324
+ /-----BEGIN (?:RSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA )?PRIVATE KEY-----/g,
1325
+ /api[_-]?key\s*[:=]\s*[^\s,}]+/gi,
1326
+ /password\s*[:=]\s*[^\s,}]+/gi,
1327
+ /secret\s*[:=]\s*[^\s,}]+/gi
1328
+ ];
1329
+ function redactSnippet(text) {
1330
+ let out = text;
1331
+ for (const pattern of REDACT_PATTERNS) {
1332
+ pattern.lastIndex = 0;
1333
+ out = out.replace(pattern, "***");
1334
+ }
1335
+ const points = Array.from(out);
1336
+ if (points.length <= SNIPPET_MAX)
1337
+ return out;
1338
+ return points.slice(0, SNIPPET_MAX).join("");
1339
+ }
1340
+
1341
+ // lib/server/history/impl/entry.js
1342
+ function assembleEntry(root, sessionId, event, now) {
1343
+ return {
1344
+ ts: now,
1345
+ rootHash: rootHashOf(root),
1346
+ rootDisplay: rootDisplayOf(root),
1347
+ sessionId,
1348
+ presetId: event.presetId,
1349
+ templateVersion: TEMPLATE_VERSION,
1350
+ stateHash: stateHashOf(event.text),
1351
+ snippetRedacted: event.precheckHit ? "***" : redactSnippet(event.text),
1352
+ lang: event.lang,
1353
+ truncated: event.truncated,
1354
+ originalLength: event.originalLength,
1355
+ resultKind: event.resultKind,
1356
+ ...event.choice !== void 0 ? { choice: event.choice } : {},
1357
+ ...event.score !== void 0 ? { score: event.score } : {},
1358
+ confidence: event.confidence,
1359
+ tier: event.tier,
1360
+ automation: event.automation,
1361
+ provider: "official",
1362
+ latencyMs: event.latencyMs,
1363
+ ...event.errorCode !== void 0 ? { errorCode: event.errorCode } : {},
1364
+ questions: snapshotQuestions(event)
1365
+ };
1366
+ }
1367
+ function snapshotQuestions(event) {
1368
+ return event.questions.map((q) => ({
1369
+ id: q.id,
1370
+ text: redactSnippet(q.text),
1371
+ kind: q.kind,
1372
+ ...q.options !== void 0 ? { options: q.options.map((opt) => redactSnippet(opt)) } : {},
1373
+ ...q.levels !== void 0 ? { levels: q.levels.map((lv) => redactSnippet(lv)) } : {}
1374
+ }));
1375
+ }
1376
+
1377
+ // lib/server/store/impl/io.js
1378
+ import { chmodSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1379
+ import { dirname as dirname3 } from "node:path";
1380
+ function ensureDir0700(dir) {
1381
+ mkdirSync(dir, { recursive: true, mode: 448 });
1382
+ try {
1383
+ chmodSync(dir, 448);
1384
+ } catch {
1385
+ }
1386
+ }
1387
+ function atomicWrite0600Sync(file, text) {
1388
+ ensureDir0700(dirname3(file));
1389
+ const nonce = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
1390
+ const tmp = file + ".tmp-" + String(process.pid) + "-" + nonce;
1391
+ writeFileSync(tmp, text, { mode: 384 });
1392
+ try {
1393
+ chmodSync(tmp, 384);
1394
+ } catch {
1395
+ }
1396
+ renameSync(tmp, file);
1397
+ try {
1398
+ chmodSync(file, 384);
1399
+ } catch {
1400
+ }
1401
+ }
1402
+ function readTextSync(file) {
1403
+ try {
1404
+ return { ok: true, text: readFileSync2(file, "utf8") };
1405
+ } catch {
1406
+ return { ok: false };
1407
+ }
1408
+ }
1409
+ function readJsonSync(file) {
1410
+ const raw = readTextSync(file);
1411
+ if (!raw.ok)
1412
+ return { ok: false };
1413
+ try {
1414
+ return { ok: true, value: JSON.parse(raw.text) };
1415
+ } catch {
1416
+ return { ok: false };
1417
+ }
1418
+ }
1419
+ function mtimeMs(file) {
1420
+ try {
1421
+ return statSync(file).mtimeMs;
1422
+ } catch {
1423
+ return -1;
1424
+ }
1425
+ }
1426
+ function listFilesSync(dir) {
1427
+ try {
1428
+ return readdirSync(dir);
1429
+ } catch {
1430
+ return [];
1431
+ }
1432
+ }
1433
+ function removeFileSync(file) {
1434
+ try {
1435
+ unlinkSync(file);
1436
+ } catch {
1437
+ }
1438
+ }
1439
+
1440
+ // ../../node_modules/.pnpm/@typesafe-ai+sdk@0.6.0/node_modules/@typesafe-ai/sdk/dist/index.mjs
1441
+ var requestIdFrom = (headers) => headers.get("x-typesafe-request-id") ?? void 0;
1442
+ var APIPromise = class APIPromise2 extends Promise {
1443
+ #responsePromise;
1444
+ #parseResponse;
1445
+ #parsed;
1446
+ constructor(responsePromise, parseResponse) {
1447
+ super((resolve) => resolve(void 0));
1448
+ this.#responsePromise = responsePromise;
1449
+ this.#parseResponse = parseResponse;
1450
+ }
1451
+ /**
1452
+ * Resolves to the raw `Response` without parsing the body. SDK requests buffer the full
1453
+ * body under the request timeout before handoff; reading it afterwards is caller-owned.
1454
+ * The caller owns the body; don't also `await` the parsed result on the same promise.
1455
+ */
1456
+ asResponse() {
1457
+ return this.#responsePromise;
1458
+ }
1459
+ /** Return the parsed result, HTTP response, and request ID. */
1460
+ async withResponse() {
1461
+ const [data, response] = await Promise.all([this.#parse(), this.#responsePromise]);
1462
+ return {
1463
+ data,
1464
+ response,
1465
+ requestId: requestIdFrom(response.headers)
1466
+ };
1467
+ }
1468
+ /** Transform the parsed result, sharing the HTTP response and a single body parse. */
1469
+ map(fn) {
1470
+ return new APIPromise2(this.#responsePromise, () => this.#parse().then(fn));
1471
+ }
1472
+ #parse() {
1473
+ this.#parsed ??= this.#responsePromise.then(this.#parseResponse);
1474
+ return this.#parsed;
1475
+ }
1476
+ then(onfulfilled, onrejected) {
1477
+ return this.#parse().then(onfulfilled, onrejected);
1478
+ }
1479
+ catch(onrejected) {
1480
+ return this.#parse().catch(onrejected);
1481
+ }
1482
+ finally(onfinally) {
1483
+ return this.#parse().finally(onfinally);
1484
+ }
1485
+ };
1486
+ var ENV = {
1487
+ /** Required API key; used when `apiKey` is omitted. */
1488
+ apiKey: "TYPESAFE_API_KEY",
1489
+ /** API root; defaults to `https://api.typesafe.ai`. */
1490
+ baseURL: "TYPESAFE_BASE_URL",
1491
+ /** Default model name; defaults to `jev-latest`. */
1492
+ defaultModel: "TYPESAFE_DEFAULT_MODEL",
1493
+ /** Log level; defaults to `warn`. */
1494
+ logLevel: "TYPESAFE_LOG_LEVEL"
1495
+ };
1496
+ var readEnv = (name2) => {
1497
+ if (typeof process === "undefined" || !process.env) return void 0;
1498
+ return process.env[name2]?.trim() || void 0;
1499
+ };
1500
+ var fromCodeOrEnv = (fromCode, envVar) => fromCode ?? readEnv(envVar);
1501
+ var range = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
1502
+ var DEFAULT_RETRY_POLICY = {
1503
+ maxRetries: 2,
1504
+ backoffInitialMs: 500,
1505
+ backoffMaxMs: 5e3,
1506
+ backoffJitter: 0.25,
1507
+ /** HTTP 408, 429, and 5xx responses. */
1508
+ httpStatuses: /* @__PURE__ */ new Set([
1509
+ 408,
1510
+ 429,
1511
+ ...range(500, 600)
1512
+ ]),
1513
+ respectRetryAfter: true,
1514
+ /** Maximum server retry delay before falling back to backoff. */
1515
+ maxRetryAfterMs: 6e4,
1516
+ apiConnectionError: true,
1517
+ apiTimeoutError: true
1518
+ };
1519
+ DEFAULT_RETRY_POLICY.maxRetries;
1520
+ var isRetryableStatus = (status, policy = DEFAULT_RETRY_POLICY) => policy.httpStatuses.has(status);
1521
+ var parseRetryAfter = (headers, now = Date.now()) => {
1522
+ const ms = Number(headers.get("retry-after-ms"));
1523
+ if (headers.has("retry-after-ms") && Number.isFinite(ms) && ms >= 0) return ms;
1524
+ const raw = headers.get("retry-after");
1525
+ if (raw === null) return void 0;
1526
+ const seconds = Number(raw);
1527
+ if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : void 0;
1528
+ const date = Date.parse(raw);
1529
+ if (!Number.isNaN(date)) return Math.max(0, date - now);
1530
+ };
1531
+ var retryDelayMs = (attempt, headers, policy = DEFAULT_RETRY_POLICY, random = Math.random) => {
1532
+ if (policy.respectRetryAfter && headers !== void 0) {
1533
+ const retryAfter = parseRetryAfter(headers);
1534
+ if (retryAfter !== void 0 && retryAfter <= policy.maxRetryAfterMs) return retryAfter;
1535
+ }
1536
+ const exponential = Math.min(policy.backoffInitialMs * 2 ** attempt, policy.backoffMaxMs);
1537
+ return Math.round(exponential * (1 - random() * policy.backoffJitter));
1538
+ };
1539
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
1540
+ if (signal?.aborted) return reject(signal.reason);
1541
+ const onAbort = () => {
1542
+ clearTimeout(timer);
1543
+ reject(signal?.reason);
1544
+ };
1545
+ const timer = setTimeout(() => {
1546
+ signal?.removeEventListener("abort", onAbort);
1547
+ resolve();
1548
+ }, ms);
1549
+ signal?.addEventListener("abort", onAbort, { once: true });
1550
+ });
1551
+ var TypeSafeError = class extends Error {
1552
+ constructor(message, options) {
1553
+ super(message, options);
1554
+ this.name = new.target.name;
1555
+ }
1556
+ };
1557
+ var isRecord2 = (value) => typeof value === "object" && value !== null;
1558
+ var extractMessage = (body) => {
1559
+ if (typeof body === "string") return body || void 0;
1560
+ if (!isRecord2(body)) return void 0;
1561
+ const { error, message, detail } = body;
1562
+ if (typeof error === "string") return error;
1563
+ if (isRecord2(error) && typeof error.message === "string") return error.message;
1564
+ if (typeof message === "string") return message;
1565
+ if (typeof detail === "string") return detail;
1566
+ if (isRecord2(detail) && typeof detail.message === "string") return detail.message;
1567
+ if (Array.isArray(detail)) return describeValidationErrors(detail);
1568
+ };
1569
+ var describeValidationErrors = (errors) => {
1570
+ const parts = errors.flatMap((e) => {
1571
+ if (!isRecord2(e) || typeof e.msg !== "string") return [];
1572
+ const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x !== "body").join(".") : "";
1573
+ return [loc ? `${loc}: ${e.msg}` : e.msg];
1574
+ });
1575
+ return parts.length > 0 ? parts.join("; ") : void 0;
1576
+ };
1577
+ var MAX_RAW_BODY_IN_MESSAGE = 200;
1578
+ var APIError = class APIError2 extends TypeSafeError {
1579
+ /** HTTP response status code. */
1580
+ status;
1581
+ /** HTTP response headers. */
1582
+ headers;
1583
+ /** Parsed JSON, response text, or `undefined` for an empty body. */
1584
+ body;
1585
+ /** Request ID from `x-typesafe-request-id`, or `undefined` when absent. */
1586
+ requestId;
1587
+ constructor(status, body, headers, message) {
1588
+ super(message ?? APIError2.describe(status, body));
1589
+ this.status = status;
1590
+ this.body = body;
1591
+ this.headers = headers;
1592
+ this.requestId = requestIdFrom(headers);
1593
+ }
1594
+ static describe(status, body) {
1595
+ const detail = extractMessage(body);
1596
+ if (detail) return `${status} ${detail}`;
1597
+ if (body === void 0) return `${status} status code (no body)`;
1598
+ const raw = typeof body === "string" ? body : JSON.stringify(body);
1599
+ return `${status} ${raw.length > MAX_RAW_BODY_IN_MESSAGE ? `${raw.slice(0, MAX_RAW_BODY_IN_MESSAGE)}…` : raw}`;
1600
+ }
1601
+ /** Create the error subclass for an HTTP status code. */
1602
+ static fromResponse(status, body, headers) {
1603
+ if (status === 400) return new BadRequestError(status, body, headers);
1604
+ if (status === 401) return new AuthenticationError(status, body, headers);
1605
+ if (status === 403) return new PermissionDeniedError(status, body, headers);
1606
+ if (status === 404) return new NotFoundError(status, body, headers);
1607
+ if (status === 422) return new UnprocessableEntityError(status, body, headers);
1608
+ if (status === 429) return new RateLimitError(status, body, headers);
1609
+ if (status >= 500) return new InternalServerError(status, body, headers);
1610
+ return new APIError2(status, body, headers);
1611
+ }
1612
+ };
1613
+ var BadRequestError = class extends APIError {
1614
+ };
1615
+ var AuthenticationError = class extends APIError {
1616
+ };
1617
+ var PermissionDeniedError = class extends APIError {
1618
+ };
1619
+ var NotFoundError = class extends APIError {
1620
+ };
1621
+ var UnprocessableEntityError = class extends APIError {
1622
+ };
1623
+ var RateLimitError = class extends APIError {
1624
+ /** Server retry delay in milliseconds, or `undefined` when absent or invalid. */
1625
+ retryAfterMs = parseRetryAfter(this.headers);
1626
+ };
1627
+ var InternalServerError = class extends APIError {
1628
+ };
1629
+ var APIConnectionError = class extends TypeSafeError {
1630
+ constructor(message = "Connection error.", options) {
1631
+ super(message, options);
1632
+ }
1633
+ };
1634
+ var APITimeoutError = class extends APIConnectionError {
1635
+ /** Configured timeout in milliseconds. */
1636
+ timeoutMs;
1637
+ constructor(timeoutMs, options) {
1638
+ super(`Request timed out after ${timeoutMs}ms.`, options);
1639
+ this.timeoutMs = timeoutMs;
1640
+ }
1641
+ };
1642
+ var APIUserAbortError = class extends TypeSafeError {
1643
+ constructor(message = "Request was aborted.", options) {
1644
+ super(message, options);
1645
+ }
1646
+ };
1647
+ var LOG_LEVELS = [
1648
+ "debug",
1649
+ "info",
1650
+ "warn",
1651
+ "error",
1652
+ "off"
1653
+ ];
1654
+ var DEFAULT_LOG_LEVEL = "warn";
1655
+ var isLogLevel = (value) => LOG_LEVELS.includes(value);
1656
+ var parseLogLevel = (value, source) => {
1657
+ if (isLogLevel(value)) return value;
1658
+ throw new TypeSafeError(`Invalid log level "${value}" from ${source}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
1659
+ };
1660
+ var PREFIX = "[typesafe-sdk]";
1661
+ var consoleLogger = {
1662
+ debug: (message, ...args) => console.debug(`${PREFIX} ${message}`, ...args),
1663
+ info: (message, ...args) => console.info(`${PREFIX} ${message}`, ...args),
1664
+ warn: (message, ...args) => console.warn(`${PREFIX} ${message}`, ...args),
1665
+ error: (message, ...args) => console.error(`${PREFIX} ${message}`, ...args)
1666
+ };
1667
+ var RANK = {
1668
+ debug: 0,
1669
+ info: 1,
1670
+ warn: 2,
1671
+ error: 3,
1672
+ off: 4
1673
+ };
1674
+ var drop = () => {
1675
+ };
1676
+ var withLevel = (sink, level) => {
1677
+ const enabled = (at) => RANK[at] >= RANK[level];
1678
+ return {
1679
+ debug: enabled("debug") ? (message, ...args) => sink.debug(message, ...args) : drop,
1680
+ info: enabled("info") ? (message, ...args) => sink.info(message, ...args) : drop,
1681
+ warn: enabled("warn") ? (message, ...args) => sink.warn(message, ...args) : drop,
1682
+ error: enabled("error") ? (message, ...args) => sink.error(message, ...args) : drop
1683
+ };
1684
+ };
1685
+ var KEY_HEADERS = /* @__PURE__ */ new Set([
1686
+ "authorization",
1687
+ "proxy-authorization",
1688
+ "x-api-key"
1689
+ ]);
1690
+ var OPAQUE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
1691
+ var redactKey = (value) => {
1692
+ const [scheme, secret] = value.includes(" ") ? value.split(/\s+/, 2) : [void 0, value];
1693
+ const tail = secret && secret.length > 8 ? secret.slice(-4) : "";
1694
+ return `${scheme ? `${scheme} ` : ""}***${tail}`;
1695
+ };
1696
+ var redact = (name2, value) => {
1697
+ const lower = name2.toLowerCase();
1698
+ if (KEY_HEADERS.has(lower)) return redactKey(value);
1699
+ if (OPAQUE_HEADERS.has(lower)) return "***";
1700
+ return value;
1701
+ };
1702
+ var redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([name2, value]) => [name2, redact(name2, value)]));
1703
+ var validateQuestions = (questions) => {
1704
+ if (Object.keys(questions).length === 0) throw new TypeSafeError("At least one question is required.");
1705
+ for (const [name2, question] of Object.entries(questions)) {
1706
+ if (question.type !== "score") continue;
1707
+ if (!Array.isArray(question.criteria)) throw new TypeSafeError(`Score question "${name2}" has criteria that are not a list; score criteria must be a list of descriptions indexed by score from zero.`);
1708
+ if (question.criteria.length < 2) throw new TypeSafeError(`Score question "${name2}" has ${question.criteria.length} criteria; at least two scores are required.`);
1709
+ }
1710
+ };
1711
+ var Models = class {
1712
+ #transport;
1713
+ constructor(transport) {
1714
+ this.#transport = transport;
1715
+ }
1716
+ /** List the models available to the account. */
1717
+ list(options = {}) {
1718
+ return this.#transport.request("GET", "/v1/models", options).map(unwrapModels);
1719
+ }
1720
+ };
1721
+ var unwrapModels = (wire) => {
1722
+ if (Array.isArray(wire?.models)) return wire.models;
1723
+ throw new TypeSafeError("Unexpected response shape from GET /v1/models; expected { models: [...] }.");
1724
+ };
1725
+ var g = globalThis;
1726
+ var isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
1727
+ var describeRuntime = () => {
1728
+ const platform = g.process?.platform && g.process?.arch ? ` (${g.process.platform}; ${g.process.arch})` : "";
1729
+ if (g.Bun?.version) return `bun/${g.Bun.version}${platform}`;
1730
+ if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}${platform}`;
1731
+ if (g.EdgeRuntime !== void 0) return "vercel-edge";
1732
+ if (g.navigator?.userAgent === "Cloudflare-Workers") return "cloudflare-workers";
1733
+ if (g.process?.versions?.node) return `node/${g.process.versions.node}${platform}`;
1734
+ if (isBrowser()) return "browser";
1735
+ return "unknown";
1736
+ };
1737
+ var VERSION = "0.6.0";
1738
+ var missingApiKey = () => {
1739
+ throw new TypeSafeError(`No API key was provided. Pass \`apiKey\` to the TypeSafeClient constructor or set the ${ENV.apiKey} environment variable.`);
1740
+ };
1741
+ var missingFetch = () => {
1742
+ throw new TypeSafeError("No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.");
1743
+ };
1744
+ var refuseBrowser = () => {
1745
+ throw new TypeSafeError("TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
1746
+ };
1747
+ var defaultFetch = (input, init) => globalThis.fetch(input, init);
1748
+ var assertNonNegativeInteger = (name2, value) => {
1749
+ if (!Number.isInteger(value) || value < 0) throw new TypeSafeError(`\`${name2}\` must be a non-negative integer, got ${String(value)}.`);
1750
+ return value;
1751
+ };
1752
+ var assertPositiveMs = (name2, value) => {
1753
+ if (!Number.isFinite(value) || value <= 0) throw new TypeSafeError(`\`${name2}\` must be a positive number of milliseconds, got ${String(value)}.`);
1754
+ return value;
1755
+ };
1756
+ var assertNonNegativeMs = (name2, value) => {
1757
+ if (!Number.isFinite(value) || value < 0) throw new TypeSafeError(`\`${name2}\` must be a non-negative number of milliseconds, got ${String(value)}.`);
1758
+ return value;
1759
+ };
1760
+ var assertFraction = (name2, value) => {
1761
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new TypeSafeError(`\`${name2}\` must be between 0 and 1, got ${String(value)}.`);
1762
+ return value;
1763
+ };
1764
+ var assertStatusSet = (name2, statuses) => {
1765
+ for (const status of statuses) if (!Number.isInteger(status) || status < 100 || status > 999) throw new TypeSafeError(`\`${name2}\` must contain HTTP status codes, got ${String(status)}.`);
1766
+ return statuses;
1767
+ };
1768
+ var resolveRetryPolicy = (base, overrides) => {
1769
+ const o = overrides ?? {};
1770
+ return {
1771
+ maxRetries: o.maxRetries === void 0 ? base.maxRetries : assertNonNegativeInteger("retry.maxRetries", o.maxRetries),
1772
+ backoffInitialMs: o.backoffInitialMs === void 0 ? base.backoffInitialMs : assertNonNegativeMs("retry.backoffInitialMs", o.backoffInitialMs),
1773
+ backoffMaxMs: o.backoffMaxMs === void 0 ? base.backoffMaxMs : assertNonNegativeMs("retry.backoffMaxMs", o.backoffMaxMs),
1774
+ backoffJitter: o.backoffJitter === void 0 ? base.backoffJitter : assertFraction("retry.backoffJitter", o.backoffJitter),
1775
+ httpStatuses: new Set(o.httpStatuses === void 0 ? base.httpStatuses : assertStatusSet("retry.httpStatuses", o.httpStatuses)),
1776
+ respectRetryAfter: o.respectRetryAfter ?? base.respectRetryAfter,
1777
+ maxRetryAfterMs: o.maxRetryAfterMs === void 0 ? base.maxRetryAfterMs : assertNonNegativeMs("retry.maxRetryAfterMs", o.maxRetryAfterMs),
1778
+ apiConnectionError: o.apiConnectionError ?? base.apiConnectionError,
1779
+ apiTimeoutError: o.apiTimeoutError ?? base.apiTimeoutError
1780
+ };
1781
+ };
1782
+ var isRetryableError = (err, policy) => {
1783
+ if (err instanceof APITimeoutError) return policy.apiTimeoutError;
1784
+ if (err instanceof APIConnectionError) return policy.apiConnectionError;
1785
+ return false;
1786
+ };
1787
+ var resolveLogLevel = (fromCode) => {
1788
+ if (fromCode !== void 0) return parseLogLevel(fromCode, "the `logLevel` option");
1789
+ const fromEnv = readEnv(ENV.logLevel);
1790
+ if (fromEnv !== void 0) return parseLogLevel(fromEnv, ENV.logLevel);
1791
+ return DEFAULT_LOG_LEVEL;
1792
+ };
1793
+ var stripTrailingSlashes = (url) => url.replace(/\/+$/, "");
1794
+ var mergeHeaders = (...sources) => {
1795
+ const entries = /* @__PURE__ */ new Map();
1796
+ for (const source of sources) for (const [name2, value] of Object.entries(source)) if (value === void 0) entries.delete(name2.toLowerCase());
1797
+ else entries.set(name2.toLowerCase(), [name2, value]);
1798
+ return Object.fromEntries(entries.values());
1799
+ };
1800
+ var bufferResponse = async (response, signal) => {
1801
+ const reader = response.clone().body?.getReader();
1802
+ if (!reader) return;
1803
+ const cancel = () => {
1804
+ reader.cancel(signal.reason).catch(() => {
1805
+ });
1806
+ response.body?.cancel(signal.reason).catch(() => {
1807
+ });
1808
+ };
1809
+ signal.addEventListener("abort", cancel, { once: true });
1810
+ try {
1811
+ if (signal.aborted) cancel();
1812
+ signal.throwIfAborted();
1813
+ while (!(await reader.read()).done) signal.throwIfAborted();
1814
+ signal.throwIfAborted();
1815
+ } finally {
1816
+ signal.removeEventListener("abort", cancel);
1817
+ reader.releaseLock();
1818
+ }
1819
+ };
1820
+ var RUNTIME = describeRuntime();
1821
+ var TypeSafeClient = class {
1822
+ /** API key excluded from serialization and public properties. */
1823
+ #apiKey;
1824
+ /** API root with trailing slashes removed. */
1825
+ baseURL;
1826
+ /** Model used when a request omits `model`. */
1827
+ defaultModel;
1828
+ /** Configured log verbosity. */
1829
+ logLevel;
1830
+ /** The configured logger, filtered to `logLevel`. */
1831
+ logger;
1832
+ /** Retry settings with constructor overrides applied. */
1833
+ retry;
1834
+ /** Timeout per attempt in milliseconds. */
1835
+ timeout;
1836
+ /** Additional headers sent with each request. */
1837
+ defaultHeaders;
1838
+ /** HTTP fetch implementation. */
1839
+ fetch;
1840
+ /** The models available to the account. */
1841
+ models;
1842
+ #requestCount = 0;
1843
+ /**
1844
+ * Create a client for the TypeSafe AI API.
1845
+ *
1846
+ * Explicit options take precedence over environment variables, then SDK defaults.
1847
+ * Empty or whitespace-only environment values are ignored.
1848
+ *
1849
+ * @throws {TypeSafeError} The API key is missing, configuration is invalid, or the runtime is unsupported.
1850
+ */
1851
+ constructor(config = {}) {
1852
+ if (isBrowser() && !config.dangerouslyAllowBrowser) refuseBrowser();
1853
+ this.#apiKey = fromCodeOrEnv(config.apiKey, ENV.apiKey) ?? missingApiKey();
1854
+ this.baseURL = stripTrailingSlashes(fromCodeOrEnv(config.baseURL, ENV.baseURL) ?? "https://api.typesafe.ai");
1855
+ this.defaultModel = fromCodeOrEnv(config.defaultModel, ENV.defaultModel) ?? "jev-latest";
1856
+ this.logLevel = resolveLogLevel(config.logLevel);
1857
+ this.logger = withLevel(config.logger ?? consoleLogger, this.logLevel);
1858
+ this.retry = resolveRetryPolicy(DEFAULT_RETRY_POLICY, config.retry);
1859
+ this.timeout = assertPositiveMs("timeout", config.timeout ?? 1e4);
1860
+ this.defaultHeaders = { ...config.defaultHeaders };
1861
+ if (config.fetch === void 0 && typeof globalThis.fetch !== "function") missingFetch();
1862
+ this.fetch = config.fetch ?? defaultFetch;
1863
+ const transport = {
1864
+ request: (method, path, options) => this.#request(method, path, options),
1865
+ defaultModel: this.defaultModel
1866
+ };
1867
+ this.models = new Models(transport);
1868
+ }
1869
+ /**
1870
+ * Answer named questions about text or structured state.
1871
+ *
1872
+ * @param request - State, questions, and an optional model override.
1873
+ * @param options - Per-call timeout, retry, headers, and cancellation settings.
1874
+ * @returns Answers typed by question name and criteria, with model and token usage.
1875
+ * @throws {TypeSafeError} Questions are empty, or score criteria are not a list of at least two entries.
1876
+ * @throws {APIError} The server returns a non-2xx response after retries.
1877
+ * @throws {APIConnectionError} The request cannot connect or times out after retries.
1878
+ * @throws {APIUserAbortError} The caller aborts the request.
1879
+ *
1880
+ * @example
1881
+ * ```ts
1882
+ * const { answers } = await client.systemOne({
1883
+ * state: "I was charged twice. Please help.",
1884
+ * questions: { billing: noul("Is this about billing?") },
1885
+ * });
1886
+ * console.log(answers.billing.noul);
1887
+ * ```
1888
+ */
1889
+ systemOne(request, options = {}) {
1890
+ validateQuestions(request.questions);
1891
+ const body = {
1892
+ ...request,
1893
+ model: request.model ?? this.defaultModel
1894
+ };
1895
+ return this.#request("POST", "/v1/systemone", {
1896
+ ...options,
1897
+ body
1898
+ });
1899
+ }
1900
+ /** Send a request and parse its response body. */
1901
+ #request(method, path, options = {}) {
1902
+ const resolved = {
1903
+ method,
1904
+ path,
1905
+ body: options.body,
1906
+ headers: mergeHeaders(this.defaultHeaders, options.headers ?? {}),
1907
+ signal: options.signal,
1908
+ timeout: options.timeout === void 0 ? this.timeout : assertPositiveMs("timeout", options.timeout),
1909
+ retry: resolveRetryPolicy(this.retry, options.retry)
1910
+ };
1911
+ const tag = `#${++this.#requestCount} ${method} ${path}`;
1912
+ return new APIPromise(this.fetchWithRetries(tag, resolved), async (res) => {
1913
+ const parsed = await parseBody(res);
1914
+ this.logger.debug(`${tag} <- body`, parsed);
1915
+ return parsed;
1916
+ });
1917
+ }
1918
+ /** Retry eligible failures, logging attempt summaries at `info` and headers and bodies at `debug`. */
1919
+ async fetchWithRetries(tag, req) {
1920
+ const url = `${this.baseURL}${req.path}`;
1921
+ const headers = mergeHeaders(req.headers, {
1922
+ Authorization: `Bearer ${this.#apiKey}`,
1923
+ Accept: "application/json",
1924
+ "User-Agent": `typesafe-sdk/${VERSION}`,
1925
+ "X-TypeSafe-SDK": `typesafe-sdk/${VERSION}`,
1926
+ "X-TypeSafe-Runtime": RUNTIME,
1927
+ "Content-Type": req.body === void 0 ? void 0 : "application/json",
1928
+ "X-TypeSafe-Retry-Count": void 0
1929
+ });
1930
+ const body = req.body === void 0 ? void 0 : JSON.stringify(req.body);
1931
+ for (let attempt = 0; ; attempt++) {
1932
+ const retriesLeft = req.retry.maxRetries - attempt;
1933
+ const attemptHeaders = attempt === 0 ? headers : {
1934
+ ...headers,
1935
+ "X-TypeSafe-Retry-Count": String(attempt)
1936
+ };
1937
+ this.logger.debug(`${tag} -> ${url}`, {
1938
+ headers: redactHeaders(attemptHeaders),
1939
+ body: req.body
1940
+ });
1941
+ const started = Date.now();
1942
+ let res;
1943
+ try {
1944
+ res = await this.attempt(tag, url, {
1945
+ method: req.method,
1946
+ headers: attemptHeaders,
1947
+ body
1948
+ }, req);
1949
+ } catch (err) {
1950
+ if (err instanceof APIUserAbortError || retriesLeft <= 0) throw err;
1951
+ if (!isRetryableError(err, req.retry)) throw err;
1952
+ await this.backOff(tag, attempt, retriesLeft, err.message, void 0, req);
1953
+ continue;
1954
+ }
1955
+ const requestId = requestIdFrom(res.headers);
1956
+ this.logger.info(`${tag} <- ${res.status} in ${Date.now() - started}ms${requestId ? ` (request ${requestId})` : ""}`);
1957
+ if (res.ok) return res;
1958
+ const errorBody = await parseBody(res);
1959
+ this.logger.debug(`${tag} <- error body`, errorBody);
1960
+ const error = APIError.fromResponse(res.status, errorBody, res.headers);
1961
+ if (retriesLeft <= 0 || !isRetryableStatus(res.status, req.retry)) throw error;
1962
+ await this.backOff(tag, attempt, retriesLeft, `${res.status}`, res.headers, req);
1963
+ }
1964
+ }
1965
+ /**
1966
+ * One HTTP round trip, including body delivery, with a timeout. The caller's signal and our
1967
+ * timer both abort the same controller; we check which fired to choose the error class.
1968
+ */
1969
+ async attempt(tag, url, init, { signal, timeout }) {
1970
+ const controller = new AbortController();
1971
+ const abortFromCaller = () => controller.abort(signal?.reason);
1972
+ if (signal?.aborted) abortFromCaller();
1973
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
1974
+ let timedOut = false;
1975
+ const timer = setTimeout(() => {
1976
+ timedOut = true;
1977
+ controller.abort();
1978
+ }, timeout);
1979
+ const started = Date.now();
1980
+ const elapsed = () => `${Date.now() - started}ms`;
1981
+ try {
1982
+ const response = await this.fetch(url, {
1983
+ ...init,
1984
+ signal: controller.signal
1985
+ });
1986
+ await bufferResponse(response, controller.signal);
1987
+ return response;
1988
+ } catch (err) {
1989
+ if (signal?.aborted) {
1990
+ this.logger.info(`${tag} aborted by caller after ${elapsed()}`);
1991
+ throw new APIUserAbortError(void 0, { cause: err });
1992
+ }
1993
+ if (timedOut) {
1994
+ this.logger.info(`${tag} timed out after ${elapsed()}`);
1995
+ throw new APITimeoutError(timeout, { cause: err });
1996
+ }
1997
+ this.logger.info(`${tag} connection error after ${elapsed()}`, err);
1998
+ throw new APIConnectionError(err instanceof Error ? `Connection error: ${err.message}` : void 0, { cause: err });
1999
+ } finally {
2000
+ clearTimeout(timer);
2001
+ signal?.removeEventListener("abort", abortFromCaller);
2002
+ }
2003
+ }
2004
+ /** Wait before retrying; caller cancellation throws `APIUserAbortError`. */
2005
+ async backOff(tag, attempt, retriesLeft, reason, headers, { retry, signal }) {
2006
+ const delay = retryDelayMs(attempt, headers, retry);
2007
+ const nth = attempt + 1;
2008
+ const total = attempt + retriesLeft;
2009
+ this.logger.info(`${tag} retrying in ${delay}ms (retry ${nth}/${total}) after ${reason}`);
2010
+ try {
2011
+ await sleep(delay, signal);
2012
+ } catch (err) {
2013
+ this.logger.info(`${tag} aborted by caller while waiting to retry`);
2014
+ throw new APIUserAbortError(void 0, { cause: err });
2015
+ }
2016
+ }
2017
+ };
2018
+ var parseBody = async (res) => {
2019
+ const text = await res.text();
2020
+ if (text.length === 0) return void 0;
2021
+ if ((res.headers.get("content-type") ?? "").includes("application/json")) try {
2022
+ return JSON.parse(text);
2023
+ } catch {
2024
+ return text;
2025
+ }
2026
+ try {
2027
+ return JSON.parse(text);
2028
+ } catch {
2029
+ return text;
2030
+ }
2031
+ };
2032
+
2033
+ // lib/server/tools/impl/client.js
2034
+ function defaultFetchImpl() {
2035
+ return async (url, init) => {
2036
+ const impl = globalThis.fetch;
2037
+ if (typeof impl !== "function")
2038
+ throw new Error("decision-gateway[500]: global fetch unavailable");
2039
+ const res = await impl(url, {
2040
+ method: init.method,
2041
+ headers: init.headers,
2042
+ body: init.body,
2043
+ signal: init.signal
2044
+ });
2045
+ return { status: res.status, text: await res.text() };
2046
+ };
2047
+ }
2048
+ var SDK_PATH = "/v1/systemone";
2049
+ if (!JEV_BASE_URL.endsWith(SDK_PATH)) {
2050
+ throw new Error("dsh-decision-gateway: JEV_BASE_URL 须以 /v1/systemone 结尾");
2051
+ }
2052
+ var JEV_API_ROOT = JEV_BASE_URL.slice(0, -SDK_PATH.length);
2053
+ var SCORE_LEVELS = ["1", "2", "3", "4", "5"];
2054
+ function confidenceTier(confidence) {
2055
+ if (confidence >= 0.8)
2056
+ return 2;
2057
+ if (confidence >= 0.5)
2058
+ return 1;
2059
+ return 0;
2060
+ }
2061
+ function clamp01(value) {
2062
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
2063
+ }
2064
+ function toWireQuestions(questions) {
2065
+ const out = {};
2066
+ for (const q of questions) {
2067
+ out[q.id] = q.kind === "choice" ? {
2068
+ type: "choice",
2069
+ instructions: q.text,
2070
+ criteria: Object.fromEntries((q.options ?? []).map((o) => [o, null]))
2071
+ } : {
2072
+ type: "score",
2073
+ instructions: q.text,
2074
+ // 校验已保证 2-10 档;tuple 形状经 unknown 中转(string[] 无法直接断言为定长元组)。
2075
+ criteria: [...q.levels ?? SCORE_LEVELS]
2076
+ };
2077
+ }
2078
+ return out;
2079
+ }
2080
+ function callerAbortedFailure() {
2081
+ return {
2082
+ code: "ABORTED",
2083
+ category: "aborted",
2084
+ message: "caller aborted",
2085
+ retryable: false
2086
+ };
2087
+ }
2088
+ function isCallerAborted(callerSignal) {
2089
+ return callerSignal?.aborted === true;
2090
+ }
2091
+ function combineSignals(callerSignal, innerSignal) {
2092
+ if (callerSignal === void 0)
2093
+ return innerSignal;
2094
+ if (callerSignal.aborted)
2095
+ return callerSignal;
2096
+ if (innerSignal.aborted)
2097
+ return innerSignal;
2098
+ const anyFn = AbortSignal.any;
2099
+ if (typeof anyFn === "function")
2100
+ return anyFn([callerSignal, innerSignal]);
2101
+ const relay = new AbortController();
2102
+ const onAbort = () => {
2103
+ callerSignal.removeEventListener("abort", onAbort);
2104
+ innerSignal.removeEventListener("abort", onAbort);
2105
+ relay.abort();
2106
+ };
2107
+ callerSignal.addEventListener("abort", onAbort, { once: true });
2108
+ innerSignal.addEventListener("abort", onAbort, { once: true });
2109
+ return relay.signal;
2110
+ }
2111
+ function toSdkFetch(fetchImpl, callerSignal) {
2112
+ return async (input, init) => {
2113
+ const headers = Object.fromEntries(new Headers(init?.headers ?? {}).entries());
2114
+ const inner = init?.signal ?? new AbortController().signal;
2115
+ const res = await fetchImpl(input, {
2116
+ method: init?.method ?? "POST",
2117
+ headers,
2118
+ body: typeof init?.body === "string" ? init.body : "",
2119
+ signal: combineSignals(callerSignal, inner)
2120
+ });
2121
+ return new Response(res.text, {
2122
+ status: res.status,
2123
+ headers: { "content-type": "application/json" }
2124
+ });
2125
+ };
2126
+ }
2127
+ function mapFirstAnswer(body, result) {
2128
+ const bad = (message) => ({
2129
+ ok: false,
2130
+ failure: { code: "UPSTREAM", category: "bad-payload", message, retryable: false }
2131
+ });
2132
+ const ids = Object.keys(body.questions);
2133
+ const first = ids[0];
2134
+ const answers = result.answers;
2135
+ if (answers === null || typeof answers !== "object" || Array.isArray(answers)) {
2136
+ return bad("answers shape");
2137
+ }
2138
+ const ans = first === void 0 ? void 0 : answers[first];
2139
+ if (ans === null || typeof ans !== "object" || Array.isArray(ans)) {
2140
+ return bad("answer shape");
2141
+ }
2142
+ const rec = ans;
2143
+ if (rec["type"] === "choice") {
2144
+ const choice = rec["choice"];
2145
+ if (typeof choice !== "string" || choice.length === 0) {
2146
+ return bad("choice must be non-empty");
2147
+ }
2148
+ const confidence = clamp01(rec["confidence"]);
2149
+ const level = confidenceTier(confidence);
2150
+ return {
2151
+ ok: true,
2152
+ verdict: {
2153
+ resultKind: "choice",
2154
+ choice,
2155
+ confidence,
2156
+ tier: level,
2157
+ automation: level,
2158
+ codepoints: 0
2159
+ }
2160
+ };
2161
+ }
2162
+ if (rec["type"] === "score") {
2163
+ const raw = rec["score"];
2164
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
2165
+ return bad("score must be a number");
2166
+ }
2167
+ const sent = body.questions[first];
2168
+ const levels = Array.isArray(sent?.criteria) && sent.criteria.length >= 2 ? sent.criteria.length : SCORE_LEVELS.length;
2169
+ const score = Math.min(5, Math.max(1, Math.round(raw / (levels - 1) * 4) + 1));
2170
+ const confidence = clamp01(rec["confidence"]);
2171
+ const level = confidenceTier(confidence);
2172
+ return {
2173
+ ok: true,
2174
+ verdict: {
2175
+ resultKind: "score",
2176
+ score,
2177
+ confidence,
2178
+ tier: level,
2179
+ automation: level,
2180
+ codepoints: 0
2181
+ }
2182
+ };
2183
+ }
2184
+ return bad("answer type must be choice|score");
2185
+ }
2186
+ function sdkErrorToFailure(cause) {
2187
+ if (cause instanceof APIUserAbortError) {
2188
+ return callerAbortedFailure();
2189
+ }
2190
+ if (cause instanceof APITimeoutError) {
2191
+ return { code: "TIMEOUT", category: "timeout", message: "request timed out", retryable: true };
2192
+ }
2193
+ if (cause instanceof APIConnectionError) {
2194
+ return {
2195
+ code: "NETWORK",
2196
+ category: "network",
2197
+ message: "fetch failed: " + (cause instanceof Error ? cause.message : String(cause)),
2198
+ retryable: true
2199
+ };
2200
+ }
2201
+ if (cause instanceof APIError) {
2202
+ const status = cause.status;
2203
+ if (status === 401 || status === 403) {
2204
+ return {
2205
+ code: "UNAUTHORIZED",
2206
+ category: "unauthorized",
2207
+ message: "upstream rejected credentials",
2208
+ retryable: false,
2209
+ status
2210
+ };
2211
+ }
2212
+ if (status === 429) {
2213
+ return {
2214
+ code: "RATE_LIMITED",
2215
+ category: "rate-limited",
2216
+ message: "upstream rate limited",
2217
+ retryable: true,
2218
+ status
2219
+ };
2220
+ }
2221
+ if (status >= 500 && status <= 599) {
2222
+ return {
2223
+ code: "UPSTREAM",
2224
+ category: "upstream",
2225
+ message: "upstream status " + String(status),
2226
+ retryable: true,
2227
+ status
2228
+ };
2229
+ }
2230
+ return {
2231
+ code: "UPSTREAM",
2232
+ category: "upstream",
2233
+ message: "upstream status " + String(status),
2234
+ retryable: false,
2235
+ status
2236
+ };
2237
+ }
2238
+ if (cause instanceof Error && cause.name === "AbortError") {
2239
+ return { code: "TIMEOUT", category: "timeout", message: "request timed out", retryable: true };
2240
+ }
2241
+ if (cause instanceof TypeSafeError) {
2242
+ return {
2243
+ code: "UPSTREAM",
2244
+ category: "bad-payload",
2245
+ message: "client-side SDK error",
2246
+ retryable: false
2247
+ };
2248
+ }
2249
+ if (cause instanceof Error) {
2250
+ return {
2251
+ code: "NETWORK",
2252
+ category: "network",
2253
+ message: "fetch failed: " + cause.message,
2254
+ retryable: true
2255
+ };
2256
+ }
2257
+ return { code: "UPSTREAM", category: "upstream", message: "unknown", retryable: false };
2258
+ }
2259
+ async function attemptOnce(body, key, timeoutMs, fetchImpl, callerSignal) {
2260
+ if (isCallerAborted(callerSignal)) {
2261
+ return { ok: false, failure: callerAbortedFailure() };
2262
+ }
2263
+ if (!(timeoutMs > 0)) {
2264
+ return {
2265
+ ok: false,
2266
+ failure: {
2267
+ code: "TIMEOUT",
2268
+ category: "timeout",
2269
+ message: "request timed out",
2270
+ retryable: true
2271
+ }
2272
+ };
2273
+ }
2274
+ const client = new TypeSafeClient({
2275
+ apiKey: key,
2276
+ baseURL: JEV_API_ROOT,
2277
+ fetch: toSdkFetch(fetchImpl, callerSignal),
2278
+ logLevel: "warn",
2279
+ retry: { maxRetries: 0 }
2280
+ });
2281
+ try {
2282
+ const result = await client.systemOne({ model: body.model, state: body.state, questions: body.questions }, callerSignal === void 0 ? { timeout: timeoutMs } : { timeout: timeoutMs, signal: callerSignal });
2283
+ return mapFirstAnswer(body, result);
2284
+ } catch (cause) {
2285
+ if (isCallerAborted(callerSignal)) {
2286
+ return { ok: false, failure: callerAbortedFailure() };
2287
+ }
2288
+ return { ok: false, failure: sdkErrorToFailure(cause) };
2289
+ }
2290
+ }
2291
+ async function callWithRetry(body, key, timeoutMs, fetchImpl, callerSignal) {
2292
+ const deadline = Date.now() + Math.max(0, timeoutMs);
2293
+ const budgetLeft = () => Math.max(0, deadline - Date.now());
2294
+ const first = await attemptOnce(body, key, budgetLeft(), fetchImpl, callerSignal);
2295
+ if (first.ok)
2296
+ return { verdict: first.verdict, retries: 0 };
2297
+ if (!first.failure.retryable)
2298
+ return { failure: first.failure, retries: 0 };
2299
+ if (first.failure.code === "TIMEOUT")
2300
+ return { failure: first.failure, retries: 0 };
2301
+ if (isCallerAborted(callerSignal))
2302
+ return { failure: callerAbortedFailure(), retries: 0 };
2303
+ if (budgetLeft() <= 0)
2304
+ return { failure: first.failure, retries: 0 };
2305
+ const second = await attemptOnce(body, key, budgetLeft(), fetchImpl, callerSignal);
2306
+ if (second.ok)
2307
+ return { verdict: second.verdict, retries: 1 };
2308
+ if (!second.failure.retryable)
2309
+ return { failure: second.failure, retries: 1 };
2310
+ if (isCallerAborted(callerSignal))
2311
+ return { failure: callerAbortedFailure(), retries: 1 };
2312
+ if (budgetLeft() <= 0)
2313
+ return { failure: second.failure, retries: 1 };
2314
+ const third = await attemptOnce(body, key, budgetLeft(), fetchImpl, callerSignal);
2315
+ if (third.ok)
2316
+ return { verdict: third.verdict, retries: 2 };
2317
+ return { failure: third.failure, retries: 2 };
2318
+ }
2319
+
2320
+ // lib/server/tools/impl/semaphore.js
2321
+ function createSemaphore(max) {
2322
+ const limit = Number.isInteger(max) && max > 0 ? max : 1;
2323
+ let active = 0;
2324
+ const queue = [];
2325
+ const pump = () => {
2326
+ while (active < limit && queue.length > 0) {
2327
+ const next = queue.shift();
2328
+ if (next === void 0)
2329
+ return;
2330
+ next.start();
2331
+ }
2332
+ };
2333
+ const abortError = () => {
2334
+ const error = new Error("operation aborted");
2335
+ error.name = "AbortError";
2336
+ return error;
2337
+ };
2338
+ const run = (task, signal) => {
2339
+ if (signal?.aborted === true)
2340
+ return Promise.reject(abortError());
2341
+ return new Promise((resolve, reject) => {
2342
+ let settled = false;
2343
+ const waiter = {
2344
+ start: () => {
2345
+ if (settled)
2346
+ return;
2347
+ settled = true;
2348
+ signal?.removeEventListener("abort", onAbort);
2349
+ active += 1;
2350
+ try {
2351
+ void task().then(resolve, reject).finally(() => {
2352
+ active -= 1;
2353
+ pump();
2354
+ });
2355
+ } catch (cause) {
2356
+ active -= 1;
2357
+ pump();
2358
+ reject(cause);
2359
+ }
2360
+ }
2361
+ };
2362
+ const onAbort = () => {
2363
+ if (settled)
2364
+ return;
2365
+ const index = queue.indexOf(waiter);
2366
+ if (index >= 0)
2367
+ queue.splice(index, 1);
2368
+ settled = true;
2369
+ signal?.removeEventListener("abort", onAbort);
2370
+ reject(abortError());
2371
+ };
2372
+ signal?.addEventListener("abort", onAbort, { once: true });
2373
+ queue.push(waiter);
2374
+ pump();
2375
+ });
2376
+ };
2377
+ return { run };
2378
+ }
2379
+
2380
+ // lib/server/tools/impl/precheck.js
2381
+ var SECRET_HIT_PATTERNS = [
2382
+ /sk-[A-Za-z0-9]{8,}/,
2383
+ /AKIA[0-9A-Z]{16}/,
2384
+ /ghp_[A-Za-z0-9]{8,}/,
2385
+ /xox[bpas]-[A-Za-z0-9-]+/,
2386
+ /-----BEGIN (?:RSA )?PRIVATE KEY-----/,
2387
+ /api[_-]?key\s*[:=]\s*\S+/i,
2388
+ /password\s*[:=]\s*\S+/i,
2389
+ /secret\s*[:=]\s*\S+/i
2390
+ ];
2391
+ function localPrecheckHit(text) {
2392
+ for (const pattern of SECRET_HIT_PATTERNS) {
2393
+ pattern.lastIndex = 0;
2394
+ if (pattern.test(text))
2395
+ return true;
2396
+ }
2397
+ return false;
2398
+ }
2399
+
2400
+ // lib/server/tools/impl/validate.js
2401
+ function fail(errorCode, message) {
2402
+ return { ok: false, failure: { errorCode, message } };
2403
+ }
2404
+ function isRecord3(value) {
2405
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2406
+ }
2407
+ function checkQuestion(item) {
2408
+ if (!isRecord3(item))
2409
+ return fail("BAD_QUESTION", "question must be an object");
2410
+ const id = item["id"];
2411
+ const text = item["text"];
2412
+ const kind = item["kind"];
2413
+ if (typeof id !== "string" || id.length === 0 || !QUESTION_ID_RE.test(id) || containsCjk(id)) {
2414
+ return fail("BAD_QUESTION_ID", "question id must be ASCII a-z0-9- 1..64");
2415
+ }
2416
+ if (typeof text !== "string" || Array.from(text).length === 0 || Array.from(text).length > MAX_QUESTION_TEXT || text.trim().length === 0) {
2417
+ return fail("BAD_QUESTION_TEXT", "question text must be 1..255 codepoints and non-blank");
2418
+ }
2419
+ if (kind !== "choice" && kind !== "score") {
2420
+ return fail("BAD_QUESTION_KIND", "question kind must be choice|score");
2421
+ }
2422
+ const options = item["options"];
2423
+ if (kind === "choice") {
2424
+ if (!Array.isArray(options) || options.length < 2 || options.length > 10) {
2425
+ return fail("BAD_OPTIONS", "choice question needs 2..10 options");
2426
+ }
2427
+ for (const opt of options) {
2428
+ if (typeof opt !== "string" || opt.length === 0 || Array.from(opt).length > 64 || opt.trim().length === 0) {
2429
+ return fail("BAD_OPTIONS", "option must be non-empty string <=64 codepoints");
2430
+ }
2431
+ }
2432
+ if (item["levels"] !== void 0) {
2433
+ return fail("BAD_LEVELS", "choice question must not carry levels");
2434
+ }
2435
+ const opts = options.map((opt) => opt);
2436
+ return { ok: true, question: { id, text, kind, options: opts } };
2437
+ }
2438
+ if (options !== void 0) {
2439
+ return fail("BAD_OPTIONS", "score question must not carry options");
2440
+ }
2441
+ const levels = item["levels"];
2442
+ if (levels === void 0)
2443
+ return { ok: true, question: { id, text, kind } };
2444
+ if (!Array.isArray(levels) || levels.length < 2 || levels.length > 10) {
2445
+ return fail("BAD_LEVELS", "score levels must be 2..10 rubric strings (or omit for default 1-5)");
2446
+ }
2447
+ for (const lv of levels) {
2448
+ if (typeof lv !== "string" || lv.length === 0 || Array.from(lv).length > 64 || lv.trim().length === 0) {
2449
+ return fail("BAD_LEVELS", "level must be non-empty string <=64 codepoints");
2450
+ }
2451
+ }
2452
+ return {
2453
+ ok: true,
2454
+ question: { id, text, kind, levels: levels.map((lv) => lv) }
2455
+ };
2456
+ }
2457
+ function checkOverrideList(raw) {
2458
+ if (!Array.isArray(raw) || raw.length === 0) {
2459
+ return fail("EMPTY_OVERRIDE", "questions_override must be a non-empty array");
2460
+ }
2461
+ if (raw.length > MAX_QUESTIONS) {
2462
+ return fail("TOO_MANY_QUESTIONS", "questions_override must hold <=20 questions");
2463
+ }
2464
+ const questions = [];
2465
+ const seen = /* @__PURE__ */ new Set();
2466
+ for (const item of raw) {
2467
+ const checked = checkQuestion(item);
2468
+ if (!checked.ok)
2469
+ return checked;
2470
+ if (seen.has(checked.question.id)) {
2471
+ return fail("DUPLICATE_QUESTION_ID", "question ids must be unique");
2472
+ }
2473
+ seen.add(checked.question.id);
2474
+ questions.push(checked.question);
2475
+ }
2476
+ return { ok: true, questions };
2477
+ }
2478
+ function validateDecideArgs(args, custom) {
2479
+ if (!isRecord3(args))
2480
+ return fail("BAD_ARGS", "args must be an object");
2481
+ const presetRaw = args["preset_id"];
2482
+ if (typeof presetRaw !== "string" || presetRaw.length === 0) {
2483
+ return fail("MISSING_PRESET", "preset_id is required");
2484
+ }
2485
+ if (!PRESET_ID_RE.test(presetRaw) || containsCjk(presetRaw)) {
2486
+ return fail("BAD_PRESET_ID", "preset_id must be ASCII a-z0-9- 1..64");
2487
+ }
2488
+ const template = frozenPresetOf(presetRaw);
2489
+ const customPreset = template === void 0 ? custom?.get(presetRaw) : void 0;
2490
+ if (template === void 0 && customPreset === void 0) {
2491
+ return fail("UNKNOWN_PRESET", "unknown preset");
2492
+ }
2493
+ const state = args["state"];
2494
+ if (!isRecord3(state))
2495
+ return fail("MISSING_STATE", "state is required");
2496
+ const text = state["text"];
2497
+ if (typeof text !== "string" || text.length === 0 || text.trim().length === 0) {
2498
+ return fail("EMPTY_TEXT", "state.text must be a non-blank string");
2499
+ }
2500
+ const langRaw = state["lang"];
2501
+ const lang = langRaw === void 0 ? "unknown" : langRaw;
2502
+ if (lang !== "en" && lang !== "zh" && lang !== "unknown") {
2503
+ return fail("BAD_LANG", "state.lang must be en|zh|unknown");
2504
+ }
2505
+ const override = args["questions_override"];
2506
+ if (override === void 0) {
2507
+ return fail("MISSING_OVERRIDE", "questions_override is required (templates hold no questions)");
2508
+ }
2509
+ const checked = checkOverrideList(override);
2510
+ if (!checked.ok)
2511
+ return checked;
2512
+ return {
2513
+ ok: true,
2514
+ valid: {
2515
+ presetId: presetRaw,
2516
+ text,
2517
+ lang,
2518
+ questions: checked.questions,
2519
+ appliedSource: presetRaw === "custom" || customPreset !== void 0 ? "custom" : "override"
2520
+ }
2521
+ };
2522
+ }
2523
+
2524
+ // lib/server/tools/impl/service.js
2525
+ function envelope(errorCode, category, message) {
2526
+ return { ok: false, error: { errorCode, category, message } };
2527
+ }
2528
+ function tierOf(index) {
2529
+ return index === 2 ? "high" : index === 1 ? "low" : "none";
2530
+ }
2531
+ function automationOf(tier, index) {
2532
+ if (tier === "none")
2533
+ return "manual";
2534
+ return index >= 2 ? "auto" : index === 1 ? "assisted" : "manual";
2535
+ }
2536
+ function isAbortError(cause) {
2537
+ return typeof cause === "object" && cause !== null && "name" in cause && cause.name === "AbortError";
2538
+ }
2539
+ function safeSessionId(sessionId) {
2540
+ return SESSION_ID_RE.test(sessionId) ? sessionId : "unknown";
2541
+ }
2542
+ function safeRecord(deps, event) {
2543
+ try {
2544
+ deps.recordEvent?.({ ...event, sessionId: safeSessionId(event.sessionId) });
2545
+ } catch (cause) {
2546
+ deps.logger.warn("dsh-decision-gateway: 历史记录失败 —— " + (cause instanceof Error ? cause.message : String(cause)));
2547
+ }
2548
+ }
2549
+ async function decide(args, deps) {
2550
+ const customMap = deps.customPresets;
2551
+ const checked = validateDecideArgs(args, customMap);
2552
+ if (!checked.ok)
2553
+ return envelope(checked.failure.errorCode, "bad-request", checked.failure.message);
2554
+ const valid = checked.valid;
2555
+ if (!deps.isEnabled(valid.presetId)) {
2556
+ return envelope("PRESET_DISABLED", "preset-disabled", "preset disabled");
2557
+ }
2558
+ if (deps.signal.aborted) {
2559
+ return envelope("ABORTED", "aborted", "caller aborted");
2560
+ }
2561
+ const direct = (task) => task();
2562
+ const limit = deps.limit ?? direct;
2563
+ const execute = async (runRemote) => {
2564
+ const trunc = truncateCodePoints(valid.text, deps.connection.truncBudget);
2565
+ const now = deps.now ?? Date.now;
2566
+ const cap = deps.capOf(valid.presetId);
2567
+ const started = now();
2568
+ const hit = localPrecheckHit(valid.text);
2569
+ if (hit) {
2570
+ safeRecord(deps, {
2571
+ sessionId: deps.sessionId,
2572
+ precheckHit: true,
2573
+ presetId: valid.presetId,
2574
+ text: valid.text,
2575
+ lang: valid.lang,
2576
+ truncated: trunc.truncated,
2577
+ originalLength: trunc.originalLength,
2578
+ resultKind: "local-precheck",
2579
+ questions: valid.questions,
2580
+ choice: "human",
2581
+ confidence: 1,
2582
+ tier: "none",
2583
+ automation: "manual",
2584
+ latencyMs: 0
2585
+ });
2586
+ return {
2587
+ ok: true,
2588
+ provider: "official",
2589
+ appliedSource: "local-precheck",
2590
+ truncated: trunc.truncated,
2591
+ originalLength: trunc.originalLength,
2592
+ tier: "none",
2593
+ automation: "manual",
2594
+ codepoints: 0,
2595
+ retries: 0,
2596
+ latencyMs: 0,
2597
+ resultKind: "local-precheck",
2598
+ choice: "human",
2599
+ confidence: 1
2600
+ };
2601
+ }
2602
+ const resolved = deps.resolveKey();
2603
+ if (resolved.key === void 0) {
2604
+ safeRecord(deps, {
2605
+ sessionId: deps.sessionId,
2606
+ presetId: valid.presetId,
2607
+ text: valid.text,
2608
+ lang: valid.lang,
2609
+ truncated: trunc.truncated,
2610
+ originalLength: trunc.originalLength,
2611
+ resultKind: "not-executed",
2612
+ questions: valid.questions,
2613
+ precheckHit: hit,
2614
+ confidence: 0,
2615
+ tier: "none",
2616
+ automation: "manual",
2617
+ latencyMs: 0,
2618
+ errorCode: "NO_KEY"
2619
+ });
2620
+ return envelope("NO_KEY", "no-key", "no api key (set apiKeyRef or plaintext)");
2621
+ }
2622
+ const fetchImpl = deps.fetchImpl ?? defaultFetchImpl();
2623
+ const body = {
2624
+ model: JEV_MODEL,
2625
+ state: trunc.text,
2626
+ questions: toWireQuestions(valid.questions)
2627
+ };
2628
+ const outcome = await runRemote(() => callWithRetry(body, resolved.key, deps.connection.timeoutMs, fetchImpl, deps.signal));
2629
+ const latencyMs = now() - started;
2630
+ if (outcome.failure !== void 0 || outcome.verdict === void 0) {
2631
+ const failure = outcome.failure ?? {
2632
+ code: "UPSTREAM",
2633
+ category: "upstream",
2634
+ message: "unknown"
2635
+ };
2636
+ safeRecord(deps, {
2637
+ sessionId: deps.sessionId,
2638
+ presetId: valid.presetId,
2639
+ text: valid.text,
2640
+ lang: valid.lang,
2641
+ truncated: trunc.truncated,
2642
+ originalLength: trunc.originalLength,
2643
+ resultKind: "upstream-error",
2644
+ questions: valid.questions,
2645
+ precheckHit: hit,
2646
+ confidence: 0,
2647
+ tier: "none",
2648
+ automation: "manual",
2649
+ latencyMs,
2650
+ errorCode: failure.code
2651
+ });
2652
+ return envelope(failure.code, failure.category, failure.message);
2653
+ }
2654
+ const verdict = outcome.verdict;
2655
+ const tierIndex = verdict.resultKind === "choice" && verdict.choice === "Noul" ? 0 : Math.min(verdict.tier, cap);
2656
+ const autoIndex = Math.min(verdict.automation, cap);
2657
+ const tier = tierOf(tierIndex);
2658
+ const automation = trunc.truncated ? "suggest-only" : automationOf(tier, autoIndex);
2659
+ const codepoints = verdict.codepoints > 0 ? verdict.codepoints : trunc.originalLength;
2660
+ safeRecord(deps, {
2661
+ sessionId: deps.sessionId,
2662
+ presetId: valid.presetId,
2663
+ text: valid.text,
2664
+ lang: valid.lang,
2665
+ truncated: trunc.truncated,
2666
+ originalLength: trunc.originalLength,
2667
+ resultKind: verdict.resultKind,
2668
+ questions: valid.questions,
2669
+ precheckHit: hit,
2670
+ ...verdict.choice !== void 0 ? { choice: verdict.choice } : {},
2671
+ ...verdict.score !== void 0 ? { score: verdict.score } : {},
2672
+ confidence: verdict.confidence,
2673
+ tier,
2674
+ automation,
2675
+ latencyMs
2676
+ });
2677
+ return {
2678
+ ok: true,
2679
+ provider: "official",
2680
+ appliedSource: valid.appliedSource,
2681
+ truncated: trunc.truncated,
2682
+ originalLength: trunc.originalLength,
2683
+ tier,
2684
+ automation,
2685
+ codepoints,
2686
+ retries: outcome.retries,
2687
+ latencyMs,
2688
+ resultKind: verdict.resultKind,
2689
+ ...verdict.choice !== void 0 ? { choice: verdict.choice } : {},
2690
+ ...verdict.score !== void 0 ? { score: verdict.score } : {},
2691
+ confidence: verdict.confidence
2692
+ };
2693
+ };
2694
+ try {
2695
+ return await limit(() => execute(direct), deps.signal);
2696
+ } catch (cause) {
2697
+ if (isAbortError(cause)) {
2698
+ return envelope("ABORTED", "aborted", "caller aborted");
2699
+ }
2700
+ throw cause;
2701
+ }
2702
+ }
2703
+ function listPresets(deps) {
2704
+ const frozen = FROZEN_PRESETS.map((preset) => ({
2705
+ id: preset.id,
2706
+ enabled: deps.isEnabled(preset.id),
2707
+ templateVersion: TEMPLATE_VERSION,
2708
+ automationCap: deps.capOf(preset.id),
2709
+ label: preset.label,
2710
+ description: preset.description,
2711
+ custom: false
2712
+ }));
2713
+ const customs = (deps.customs ?? []).map((preset) => ({
2714
+ id: preset.id,
2715
+ enabled: deps.isEnabled(preset.id),
2716
+ templateVersion: TEMPLATE_VERSION,
2717
+ automationCap: deps.capOf(preset.id),
2718
+ label: preset.label,
2719
+ description: preset.description,
2720
+ custom: true
2721
+ }));
2722
+ return [...frozen, ...customs];
2723
+ }
2724
+
2725
+ // lib/server/tools/impl/define.js
2726
+ function sessionOf(exec) {
2727
+ if (exec !== null && typeof exec === "object") {
2728
+ const rec = exec;
2729
+ const direct = rec["sessionId"];
2730
+ if (typeof direct === "string" && direct.length > 0)
2731
+ return direct;
2732
+ for (const key of ["session", "agent"]) {
2733
+ const nested = rec[key];
2734
+ if (nested !== null && typeof nested === "object") {
2735
+ const id = nested["id"];
2736
+ if (typeof id === "string" && id.length > 0)
2737
+ return id;
2738
+ }
2739
+ }
2740
+ }
2741
+ return "unknown";
2742
+ }
2743
+ function rootOf(exec) {
2744
+ if (exec !== null && typeof exec === "object") {
2745
+ const rec = exec;
2746
+ for (const key of ["cwd", "root", "workdir"]) {
2747
+ const value = rec[key];
2748
+ if (typeof value === "string" && value.length > 0)
2749
+ return value;
2750
+ }
2751
+ }
2752
+ return process.cwd();
2753
+ }
2754
+ function renderJson(_args, value) {
2755
+ return [{ type: "text", text: JSON.stringify(value) }];
2756
+ }
2757
+ function buildToolDefinitions(assembly) {
2758
+ const decideTool = {
2759
+ name: "ws_request_verdict",
2760
+ description: "WHEN you must decide rather than generate text, call this after ws_list_verdict_guides: the list tells you which preset is enabled and how to shape questions_override, this tool returns the verdict your code branches on. Ask the JEV SystemOne model for a calibrated judgment over text you supply. Ask in English when you can: state, instructions and options calibrate best in English; Chinese is fully supported (lang zh) but may return slightly lower confidence. USE for three kinds: pick one option from 2-10 candidates (general for a frozen choice, custom for your own choice), rate a plan 1-5 (plan-review for a frozen score, custom for your own score with 2-10 levels), or gate a risky change as safe or risky (risk-check), check text for leaked secrets (secret-leak), or ask any custom question set (custom). DO NOT use for open-ended reasoning, writing, or performing actions: this tool only judges and returns a verdict, acting on it is always your decision. You MUST supply questions_override on every call (1-20 questions; templates store no questions): choice questions need 2-10 options, score questions must not carry options but may carry levels (2-10 rubric strings, default 1-5, e.g. five levels 1-5 or two levels low-high rescaled to 1-5), question ids and preset_id must be lowercase ASCII letters, digits or hyphens; preset_id is one of the 5 frozen ids or a custom id from ws_list_verdict_guides; lang is en, zh, or unknown and defaults to unknown. Texts matching secret shapes (API keys, tokens, private-key blocks, password assignments) never leave the device and return choice human for a person to review; choice Noul likewise means abstention and forces tier none for a person to review. A disabled preset fails with PRESET_DISABLED: check availability first with ws_list_verdict_guides. Success returns choice or score plus confidence 0-1, tier (none/low/high, derived as confidence ≥0.8 high, ≥0.5 low, else none) and automation (manual/assisted/auto/suggest-only, advisory only: truncated inputs force suggest-only, except local-precheck routing which stays manual). Failures carry errorCode and category (for example NO_KEY, UPSTREAM, RATE_LIMITED, TIMEOUT). Without a configured key every call fails NO_KEY: ask the user to configure one in settings. Enable secret-leak only when you must check untrusted text for real secrets; it stays disabled by default and still runs local precheck first.",
2761
+ parameters: {
2762
+ type: "object",
2763
+ properties: {
2764
+ preset_id: {
2765
+ type: "string",
2766
+ description: "Which decision template to apply: one of the 5 frozen ids (general, secret-leak, plan-review, risk-check, custom) or a user-created custom preset id. Frozen ids are fixed; custom ids are the custom:true entries returned by ws_list_verdict_guides. general takes a 2-10-candidate choice; plan-review takes score questions with no options; risk-check takes a proceed-or-not choice (options like safe/risky); secret-leak takes a leaked-or-clean check and is disabled by default; custom takes any 1-20 questions you supply, with the first question driving the verdict."
2767
+ },
2768
+ state: {
2769
+ type: "object",
2770
+ description: "The content under review.",
2771
+ properties: {
2772
+ text: {
2773
+ type: "string",
2774
+ description: "The text to judge; must be non-blank. Over-long text is truncated (see truncated/originalLength in the verdict)."
2775
+ },
2776
+ lang: {
2777
+ type: "string",
2778
+ enum: ["en", "zh", "unknown"],
2779
+ default: "unknown",
2780
+ description: "Language of text (optional, defaults to unknown; en or zh when known)."
2781
+ }
2782
+ },
2783
+ required: ["text"]
2784
+ },
2785
+ questions_override: {
2786
+ type: "array",
2787
+ description: "Full question set for this call, 1-20 questions (required; templates store no questions). ids must be unique.",
2788
+ items: {
2789
+ type: "object",
2790
+ properties: {
2791
+ id: {
2792
+ type: "string",
2793
+ description: "Question id: ASCII lowercase letters, digits or hyphens, 1-64 chars."
2794
+ },
2795
+ text: {
2796
+ type: "string",
2797
+ description: "Question text: 1-255 chars, must be non-blank."
2798
+ },
2799
+ kind: {
2800
+ type: "string",
2801
+ enum: ["choice", "score"],
2802
+ description: "choice picks one option; score rates 1-5."
2803
+ },
2804
+ options: {
2805
+ type: "array",
2806
+ items: { type: "string" },
2807
+ description: "Choice only: 2-10 non-empty options (each 64 chars max). Forbidden on score questions."
2808
+ },
2809
+ levels: {
2810
+ type: "array",
2811
+ items: { type: "string" },
2812
+ description: "Score only: 2-10 rubric level descriptions (default 1-5 when omitted). Forbidden on choice questions."
2813
+ }
2814
+ },
2815
+ required: ["id", "text", "kind"]
2816
+ }
2817
+ }
2818
+ },
2819
+ required: ["preset_id", "state", "questions_override"]
2820
+ },
2821
+ output: {
2822
+ schema: { type: "object" },
2823
+ render: renderJson
2824
+ },
2825
+ isConcurrencySafe: () => true,
2826
+ execute: (args, exec) => {
2827
+ const base = assembly.depsFor(exec);
2828
+ return decide(args, base);
2829
+ }
2830
+ };
2831
+ const listTool = {
2832
+ name: "ws_list_verdict_guides",
2833
+ description: "WHEN you need to know which preset to use or how to shape questions_override, call this first; WHEN NOT to call it is when you already know the enabled preset and its asking conventions. List the frozen JEV preset catalogue with live enabled state (read-only: no network, no history). Each entry carries its English asking guide (how to shape questions_override for it), templateVersion (always 1) and automationCap (0 manual-only, 1 low, 2 high). Call this before ws_request_verdict to check a preset is enabled and to copy its asking conventions; custom:true entries are user-created presets you can also use as preset_id.",
2834
+ parameters: { type: "object", properties: {} },
2835
+ output: {
2836
+ schema: { type: "array" },
2837
+ render: renderJson
2838
+ },
2839
+ isConcurrencySafe: () => true,
2840
+ execute: () => Promise.resolve(listPresets(assembly.snapshot()))
2841
+ };
2842
+ return [decideTool, listTool];
2843
+ }
2844
+
2845
+ // lib/server/tools/impl/probe.js
2846
+ async function probeConnection(input) {
2847
+ if (input.key === void 0) {
2848
+ return { ok: false, errorCode: "NO_KEY", category: "no-key", message: "no api key" };
2849
+ }
2850
+ const started = Date.now();
2851
+ const outcome = await callWithRetry({
2852
+ model: JEV_MODEL,
2853
+ state: "connection probe",
2854
+ questions: toWireQuestions([
2855
+ { id: "probe", text: "Reply ok.", kind: "choice", options: ["ok", "fail"] }
2856
+ ])
2857
+ }, input.key, input.timeoutMs, input.fetchImpl ?? defaultFetchImpl());
2858
+ if (outcome.failure !== void 0 || outcome.verdict === void 0) {
2859
+ const failure = outcome.failure ?? {
2860
+ code: "UPSTREAM",
2861
+ category: "upstream",
2862
+ message: "unknown"
2863
+ };
2864
+ return {
2865
+ ok: false,
2866
+ errorCode: failure.code,
2867
+ category: failure.category,
2868
+ message: failure.message
2869
+ };
2870
+ }
2871
+ return { ok: true, latencyMs: Date.now() - started };
2872
+ }
2873
+
2874
+ // lib/server/upgrade/impl/steps.js
2875
+ function ensureSeeded(home, seed, io) {
2876
+ const created = [];
2877
+ const targets = [
2878
+ [configFile(home), seed.configJson],
2879
+ [presetsFile(home), seed.presetsJson],
2880
+ [secretsFile(home), seed.secretsJson]
2881
+ ];
2882
+ for (const [file, content] of targets) {
2883
+ if (!io.readTextSync(file).ok) {
2884
+ io.atomicWrite0600Sync(file, content);
2885
+ created.push(file);
2886
+ }
2887
+ }
2888
+ return created;
2889
+ }
2890
+
2891
+ // lib/server/upgrade/impl/service.js
2892
+ var STORAGE_TARGET = "0.2.6";
2893
+ function configPorts(deps) {
2894
+ return { io: deps.io, logger: deps.logger };
2895
+ }
2896
+ function migrate(deps) {
2897
+ const target = deps.targetOverride ?? STORAGE_TARGET;
2898
+ const raw = readStoredVersion(deps.home, configPorts(deps));
2899
+ const stored = raw === BASELINE_VERSION ? "0" : raw;
2900
+ const order = compareVersions(stored, target);
2901
+ if (order > 0) {
2902
+ throw new Error("upgrade[500]: unknown future storage version " + stored + " (target " + target + "): refusing to start — 请升级插件后再试");
2903
+ }
2904
+ if (order === 0)
2905
+ return { from: stored, to: target, seeded: [] };
2906
+ const seeded = ensureSeeded(deps.home, deps.seedDefaults(), deps.io);
2907
+ writeStoredVersion(deps.home, target, configPorts(deps));
2908
+ if (deps.logger.info !== void 0)
2909
+ deps.logger.info("dsh-decision-gateway: 存储已锚定 " + stored + " → " + target);
2910
+ return { from: stored, to: target, seeded };
2911
+ }
2912
+ function installUpgrade(deps) {
2913
+ return migrate(deps);
2914
+ }
2915
+
2916
+ // lib/index.js
2917
+ var name = "decision-gateway";
2918
+ var inject = ["webServer", "tools", "sessions", "sessionTitle"];
2919
+ function bindHost(ctx) {
2920
+ return {
2921
+ logger: ctx.logger,
2922
+ register: (route) => ctx.webServer.register(route),
2923
+ registerTool: (tool) => ctx.tools.register(tool),
2924
+ sessions: ctx.sessions,
2925
+ sessionTitle: ctx.sessionTitle
2926
+ };
2927
+ }
2928
+ function assemble(host, options) {
2929
+ const disposers = [];
2930
+ const home = options.home;
2931
+ const env = options.env ?? process.env;
2932
+ installUpgrade({
2933
+ io: {
2934
+ readJsonSync,
2935
+ readTextSync,
2936
+ atomicWrite0600Sync,
2937
+ listFilesSync
2938
+ },
2939
+ logger: host.logger,
2940
+ home,
2941
+ seedDefaults: () => {
2942
+ const defaults = buildDefaultConfig();
2943
+ return {
2944
+ configJson: JSON.stringify(defaults, null, 2) + "\n",
2945
+ presetsJson: JSON.stringify({ presets: defaults.presets }, null, 2) + "\n",
2946
+ secretsJson: JSON.stringify({}, null, 2) + "\n"
2947
+ };
2948
+ }
2949
+ });
2950
+ const storeIo = {
2951
+ readJsonSync,
2952
+ readTextSync,
2953
+ atomicWrite0600Sync
2954
+ };
2955
+ const configDeps = { io: storeIo, logger: host.logger };
2956
+ const historyDeps = {
2957
+ io: {
2958
+ readTextSync,
2959
+ atomicWrite0600Sync,
2960
+ listFilesSync,
2961
+ mtimeMs,
2962
+ removeFileSync,
2963
+ ensureDir0700
2964
+ },
2965
+ logger: host.logger
2966
+ };
2967
+ const live = () => loadState(home, configDeps);
2968
+ const customOf = (state, presetId) => state.customPresets.find((entry) => entry.id === presetId);
2969
+ const enabledOf = (state, presetId) => state.config.presets.find((entry) => entry.id === presetId)?.enabled ?? customOf(state, presetId)?.enabled ?? false;
2970
+ const capOfState = (state, presetId) => {
2971
+ const cap = state.config.presets.find((entry) => entry.id === presetId)?.automationCap ?? customOf(state, presetId)?.automationCap;
2972
+ return cap === 1 || cap === 2 ? cap : 0;
2973
+ };
2974
+ const customMapOf = (state) => new Map(state.customPresets.map((entry) => [entry.id, entry]));
2975
+ const titleOf = (state, presetId) => state.config.presets.find((entry) => entry.id === presetId) !== void 0 ? frozenPresetOf(presetId)?.label ?? presetId : customOf(state, presetId)?.label ?? presetId;
2976
+ const sessionTitleOf = (sessionId) => {
2977
+ try {
2978
+ const session = host.sessions?.get(sessionId);
2979
+ if (session === void 0)
2980
+ return {};
2981
+ const snapshot = host.sessionTitle?.get(session);
2982
+ const title = snapshot?.title;
2983
+ if (typeof title !== "string" || title.length === 0)
2984
+ return {};
2985
+ return { sessionTitle: title };
2986
+ } catch {
2987
+ return {};
2988
+ }
2989
+ };
2990
+ let gate = createSemaphore(live().config.connection.maxConcurrency);
2991
+ const gateFor = (maxConcurrency) => {
2992
+ gate = createSemaphore(maxConcurrency);
2993
+ return gate.run;
2994
+ };
2995
+ let lastConcurrency = live().config.connection.maxConcurrency;
2996
+ const resolveRoot = (exec, sessionId) => {
2997
+ try {
2998
+ const cwd = host.sessions?.get(sessionId)?.header.cwd;
2999
+ if (typeof cwd === "string" && cwd.length > 0)
3000
+ return cwd;
3001
+ } catch {
3002
+ }
3003
+ return rootOf(exec);
3004
+ };
3005
+ const depsFor = (exec) => {
3006
+ const state = live();
3007
+ const sessionId = sessionOf(exec);
3008
+ const root = resolveRoot(exec, sessionId);
3009
+ if (state.config.connection.maxConcurrency !== lastConcurrency) {
3010
+ lastConcurrency = state.config.connection.maxConcurrency;
3011
+ gateFor(lastConcurrency);
3012
+ }
3013
+ return {
3014
+ logger: host.logger,
3015
+ signal: exec.signal,
3016
+ connection: state.config.connection,
3017
+ isEnabled: (presetId) => enabledOf(state, presetId),
3018
+ capOf: (presetId) => capOfState(state, presetId),
3019
+ customPresets: customMapOf(state),
3020
+ resolveKey: () => resolveApiKey(state, env, configDeps),
3021
+ recordEvent: (event) => {
3022
+ const entry = assembleEntry(root, event.sessionId, event, Date.now());
3023
+ appendEntry(home, entry, state.config.history, historyDeps);
3024
+ },
3025
+ fetchImpl: options.fetchImpl,
3026
+ limit: gate.run,
3027
+ root,
3028
+ sessionId
3029
+ };
3030
+ };
3031
+ const apiDeps = {
3032
+ logger: host.logger,
3033
+ register: (route) => host.register(route),
3034
+ version: () => pluginVersion(),
3035
+ readConfig: () => toMaskedConfig(live()),
3036
+ writeConfig: (body) => {
3037
+ const flat = normalizePutEnvelope(body);
3038
+ if (!flat.ok) {
3039
+ throw new Error(JSON.stringify({
3040
+ status: 400,
3041
+ errorCode: flat.failure.errorCode,
3042
+ category: flat.failure.category,
3043
+ message: flat.failure.message
3044
+ }));
3045
+ }
3046
+ const checked = validatePutBody(flat.flat);
3047
+ if (!checked.ok) {
3048
+ throw new Error(JSON.stringify({
3049
+ status: 400,
3050
+ errorCode: checked.failure.errorCode,
3051
+ category: checked.failure.category,
3052
+ message: checked.failure.message
3053
+ }));
3054
+ }
3055
+ const current = live();
3056
+ const effectiveRef = checked.patch.apiKeyRef !== void 0 ? checked.patch.apiKeyRef : current.config.connection.apiKeyRef;
3057
+ if (checked.patch.apiKeyPlaintext !== void 0 && effectiveRef !== void 0 && effectiveRef !== null) {
3058
+ throw new Error(JSON.stringify({
3059
+ status: 400,
3060
+ errorCode: "MUTUALLY_EXCLUSIVE",
3061
+ category: "mutually-exclusive",
3062
+ message: "clear apiKeyRef (null) before setting plaintext"
3063
+ }));
3064
+ }
3065
+ return toMaskedConfig(savePatch(home, checked.patch, configDeps));
3066
+ },
3067
+ readPresets: () => {
3068
+ const state = live();
3069
+ return listPresets({
3070
+ isEnabled: (id) => enabledOf(state, id),
3071
+ capOf: (id) => capOfState(state, id),
3072
+ customs: state.customPresets
3073
+ }).map((item) => ({ ...item }));
3074
+ },
3075
+ readHistory: (query) => {
3076
+ const state = live();
3077
+ return queryEntries(home, query, historyDeps).map((entry) => ({
3078
+ ...entry,
3079
+ presetTitle: titleOf(state, entry.presetId),
3080
+ ...sessionTitleOf(entry.sessionId)
3081
+ }));
3082
+ },
3083
+ removeHistory: (query) => deleteSession(home, query, historyDeps),
3084
+ probeConnection: async () => {
3085
+ const state = live();
3086
+ const resolved = resolveApiKey(state, env, configDeps);
3087
+ return probeConnection({
3088
+ key: resolved.key,
3089
+ timeoutMs: state.config.connection.timeoutMs,
3090
+ fetchImpl: options.fetchImpl
3091
+ });
3092
+ }
3093
+ };
3094
+ for (const tool of buildToolDefinitions({
3095
+ depsFor,
3096
+ snapshot: () => {
3097
+ const state = live();
3098
+ return {
3099
+ isEnabled: (id) => enabledOf(state, id),
3100
+ capOf: (id) => capOfState(state, id),
3101
+ customs: state.customPresets
3102
+ };
3103
+ }
3104
+ })) {
3105
+ disposers.push(host.registerTool(tool));
3106
+ }
3107
+ for (const dispose of installApi(apiDeps))
3108
+ disposers.push(dispose);
3109
+ return disposers;
3110
+ }
3111
+ function apply(ctx, config = {}) {
3112
+ if (config.enabled === false)
3113
+ return;
3114
+ const host = bindHost(ctx);
3115
+ let disposers = [];
3116
+ try {
3117
+ disposers = assemble(host, config);
3118
+ } catch (cause) {
3119
+ for (const dispose of disposers.reverse()) {
3120
+ try {
3121
+ dispose();
3122
+ } catch {
3123
+ }
3124
+ }
3125
+ throw cause;
3126
+ }
3127
+ ctx.effect(() => () => {
3128
+ for (const dispose of disposers.reverse()) {
3129
+ try {
3130
+ dispose();
3131
+ } catch (cause) {
3132
+ host.logger.warn("dsh-decision-gateway: 卸载失败 —— " + (cause instanceof Error ? cause.message : String(cause)));
3133
+ }
3134
+ }
3135
+ }, "dsh-decision-gateway");
3136
+ }
3137
+ export {
3138
+ ROUTES,
3139
+ apply,
3140
+ inject,
3141
+ name
3142
+ };