@siming-org/server 0.4.0 → 0.5.0

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.
@@ -1,3 +1,113 @@
1
+ // src/middleware/auth.ts
2
+ import {
3
+ createAuthTokenRepo,
4
+ createAuthSessionRepo,
5
+ hashToken,
6
+ tokenTypeFromValue,
7
+ ForbiddenError,
8
+ UnauthorizedError
9
+ } from "@siming-org/core";
10
+
11
+ // src/middleware/auth-cookie.ts
12
+ var SESSION_COOKIE_NAME = "siming_session";
13
+
14
+ // src/middleware/auth.ts
15
+ var SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
16
+ var AUTH_EXEMPT_ROUTES = /* @__PURE__ */ new Set(["GET /health", "GET /api/auth/status"]);
17
+ function isWhoamiRoute(method, path) {
18
+ return (method === "GET" || method === "POST") && path === "/api/auth/whoami";
19
+ }
20
+ var AUTH_FLOW_ROUTES = /* @__PURE__ */ new Set(["POST /api/auth/login", "POST /api/auth/setup"]);
21
+ var ADMIN_ONLY_ROUTES = [
22
+ { method: "POST", pattern: /^\/api\/projects$/ },
23
+ { method: "PUT", pattern: /^\/api\/projects\/[^/]+$/ },
24
+ { method: "PUT", pattern: /^\/api\/settings\/enums\/[^/]+$/ },
25
+ { method: "DELETE", pattern: /^\/api\/settings\/enums\/[^/]+\/entries\/[^/]+$/ },
26
+ { method: "POST", pattern: /^\/api\/model-aliases$/ },
27
+ { method: "PUT", pattern: /^\/api\/model-aliases\/[^/]+$/ },
28
+ { method: "DELETE", pattern: /^\/api\/model-aliases\/[^/]+$/ },
29
+ // Token 管理全 admin
30
+ { method: "GET", pattern: /^\/api\/auth\/tokens(\/.*)?$/ },
31
+ { method: "POST", pattern: /^\/api\/auth\/tokens(\/.*)?$/ }
32
+ ];
33
+ function createAuthMiddleware(client, deps) {
34
+ const tokenRepo = createAuthTokenRepo(client.db());
35
+ const sessionRepo = createAuthSessionRepo(client.db());
36
+ const parseCredential = async (c) => {
37
+ const authHeader = c.req.header("Authorization");
38
+ if (authHeader !== void 0 && authHeader.startsWith("Bearer ")) {
39
+ const token = authHeader.slice("Bearer ".length).trim();
40
+ const type = tokenTypeFromValue(token);
41
+ if (type === null) return null;
42
+ const matched = await tokenRepo.findByHash(hashToken(token));
43
+ if (!matched) return null;
44
+ if (matched.type === "admin") return { kind: "admin" };
45
+ return { kind: "project", projectId: matched.projectId ?? "" };
46
+ }
47
+ const sessionId = getCookie(c, SESSION_COOKIE_NAME);
48
+ if (sessionId !== void 0) {
49
+ const session = await sessionRepo.findValidSession(sessionId);
50
+ if (session) return { kind: "admin" };
51
+ }
52
+ return null;
53
+ };
54
+ return async (c, next) => {
55
+ const method = c.req.method;
56
+ const path = c.req.path;
57
+ if (isWhoamiRoute(method, path)) {
58
+ const parsed2 = deps.authEnabled ? await parseCredential(c) : null;
59
+ c.set("auth", parsed2 === null ? { kind: deps.authEnabled ? "none" : "open" } : parsed2);
60
+ await next();
61
+ return;
62
+ }
63
+ if (AUTH_EXEMPT_ROUTES.has(`${method} ${path}`) || AUTH_FLOW_ROUTES.has(`${method} ${path}`)) {
64
+ c.set("auth", { kind: "open" });
65
+ await next();
66
+ return;
67
+ }
68
+ if (!deps.authEnabled) {
69
+ c.set("auth", { kind: "open" });
70
+ await next();
71
+ return;
72
+ }
73
+ const parsed = await parseCredential(c);
74
+ if (parsed === null) {
75
+ const authHeader = c.req.header("Authorization");
76
+ if (authHeader !== void 0 && authHeader.startsWith("Bearer ")) {
77
+ throw new UnauthorizedError("AUTH_TOKEN_INVALID");
78
+ }
79
+ throw new UnauthorizedError("AUTH_REQUIRED");
80
+ }
81
+ const auth = parsed.kind === "admin" ? { kind: "admin" } : { kind: "project", projectId: parsed.projectId ?? "" };
82
+ if (auth.kind === "project") {
83
+ for (const rule of ADMIN_ONLY_ROUTES) {
84
+ if (rule.method === method && rule.pattern.test(path)) {
85
+ throw new ForbiddenError();
86
+ }
87
+ }
88
+ }
89
+ c.set("auth", auth);
90
+ await next();
91
+ };
92
+ }
93
+ function getCookie(c, name) {
94
+ const cookieHeader = c.req.header("Cookie");
95
+ if (cookieHeader === void 0) return void 0;
96
+ for (const part of cookieHeader.split(";")) {
97
+ const eq = part.indexOf("=");
98
+ if (eq === -1) continue;
99
+ if (part.slice(0, eq).trim() === name) {
100
+ const raw = part.slice(eq + 1).trim();
101
+ try {
102
+ return decodeURIComponent(raw);
103
+ } catch {
104
+ return raw;
105
+ }
106
+ }
107
+ }
108
+ return void 0;
109
+ }
110
+
1
111
  // src/static.ts
2
112
  import { existsSync, readFileSync, statSync } from "fs";
3
113
  import { join, resolve, sep } from "path";
@@ -104,7 +214,7 @@ function createStaticRoutes(webRoot) {
104
214
  }
105
215
 
106
216
  // src/app.ts
107
- import { Hono as Hono11 } from "hono";
217
+ import { Hono as Hono15 } from "hono";
108
218
 
109
219
  // src/errors.ts
110
220
  import { AppError } from "@siming-org/core";
@@ -126,7 +236,7 @@ function setupErrorHandler(app) {
126
236
  }
127
237
 
128
238
  // src/routes/index.ts
129
- import { Hono as Hono10 } from "hono";
239
+ import { Hono as Hono13 } from "hono";
130
240
 
131
241
  // src/routes/health.ts
132
242
  import { Hono as Hono2 } from "hono";
@@ -155,11 +265,43 @@ function createHealth(client) {
155
265
  // src/routes/project.routes.ts
156
266
  import { Hono as Hono3 } from "hono";
157
267
  import { zValidator } from "@hono/zod-validator";
268
+
269
+ // src/routes/auth-guard.ts
270
+ import { NotFoundError, ForbiddenError as ForbiddenError2 } from "@siming-org/core";
271
+ function getAuth(c) {
272
+ const auth = c.get("auth");
273
+ return auth ?? { kind: "open" };
274
+ }
275
+ function assertProjectVisible(c, resourceProjectId) {
276
+ const auth = getAuth(c);
277
+ if (auth.kind === "project" && resourceProjectId !== void 0 && resourceProjectId !== "" && resourceProjectId !== auth.projectId) {
278
+ throw new NotFoundError("resource", resourceProjectId);
279
+ }
280
+ }
281
+ function assertProjectScope(c, resourceProjectId) {
282
+ const auth = getAuth(c);
283
+ if (auth.kind === "project" && resourceProjectId !== auth.projectId) {
284
+ throw new NotFoundError("resource", resourceProjectId);
285
+ }
286
+ }
287
+ function requireAdminLike(c) {
288
+ const auth = getAuth(c);
289
+ if (auth.kind === "project") {
290
+ throw new ForbiddenError2();
291
+ }
292
+ }
293
+ function resolveProjectFilter(c, requested) {
294
+ const auth = getAuth(c);
295
+ if (auth.kind === "project") return auth.projectId;
296
+ return requested;
297
+ }
298
+
299
+ // src/routes/project.routes.ts
158
300
  import {
159
301
  createProjectRepo,
160
302
  ProjectCreateSchema,
161
303
  ProjectUpdateSchema,
162
- NotFoundError,
304
+ NotFoundError as NotFoundError2,
163
305
  ConflictError,
164
306
  BIZ_CODE_MESSAGES,
165
307
  DEFAULT_PROJECT_KEY
@@ -167,7 +309,7 @@ import {
167
309
  async function loadProjectForWrite(projectRepo, projectId) {
168
310
  const project = await projectRepo.getById(projectId);
169
311
  if (!project) {
170
- throw new NotFoundError("project", projectId, {
312
+ throw new NotFoundError2("project", projectId, {
171
313
  bizCode: "PROJECT_NOT_FOUND",
172
314
  message: BIZ_CODE_MESSAGES.PROJECT_NOT_FOUND
173
315
  });
@@ -185,13 +327,19 @@ function createProjectRoutes(client) {
185
327
  const repo = createProjectRepo(client.db());
186
328
  app.get("/api/projects", async (c) => {
187
329
  const status = c.req.query("status");
330
+ const auth = getAuth(c);
331
+ if (auth.kind === "project") {
332
+ const own = await repo.getById(auth.projectId);
333
+ return c.json(own ? [own] : []);
334
+ }
188
335
  const projects = await repo.list();
189
336
  return c.json(status ? projects.filter((p) => p.status === status) : projects);
190
337
  });
191
338
  app.get("/api/projects/:id", async (c) => {
192
339
  const id = c.req.param("id");
193
340
  const project = await repo.getById(id);
194
- if (!project) throw new NotFoundError("project", id);
341
+ if (!project) throw new NotFoundError2("project", id);
342
+ assertProjectScope(c, project.id ?? id);
195
343
  return c.json(project);
196
344
  });
197
345
  app.post("/api/projects", zValidator("json", ProjectCreateSchema), async (c) => {
@@ -202,7 +350,7 @@ function createProjectRoutes(client) {
202
350
  const id = c.req.param("id");
203
351
  const data = c.req.valid("json");
204
352
  const existing = await repo.getById(id);
205
- if (!existing) throw new NotFoundError("project", id);
353
+ if (!existing) throw new NotFoundError2("project", id);
206
354
  if (data.status === "archived" && existing.key === DEFAULT_PROJECT_KEY) {
207
355
  throw new ConflictError("project", id, {
208
356
  bizCode: "DEFAULT_PROJECT_IMMUTABLE",
@@ -218,7 +366,7 @@ function createProjectRoutes(client) {
218
366
  if (data.description !== void 0) patch.description = data.description;
219
367
  if (data.status !== void 0) patch.status = data.status;
220
368
  const project = await repo.update(id, patch);
221
- if (!project) throw new NotFoundError("project", id);
369
+ if (!project) throw new NotFoundError2("project", id);
222
370
  return c.json(project);
223
371
  });
224
372
  return app;
@@ -239,7 +387,7 @@ import {
239
387
  OBJECT_ID_HEX,
240
388
  assertReferencesDeletable,
241
389
  assertReferenceAggregateLimit,
242
- NotFoundError as NotFoundError2,
390
+ NotFoundError as NotFoundError3,
243
391
  ConflictError as ConflictError2,
244
392
  BadRequestError,
245
393
  BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES2
@@ -283,6 +431,10 @@ function createSkillRoutes(client) {
283
431
  app.get("/api/skills", zValidator2("query", SkillListQuerySchema), async (c) => {
284
432
  const q = c.req.valid("query");
285
433
  const opts = { includeDisabled: q.includeDisabled, q: q.q };
434
+ const auth = getAuth(c);
435
+ if (auth.kind === "project") {
436
+ return c.json((await repo.listSkillsByScope(auth.projectId, opts)).map(skillToListItem));
437
+ }
286
438
  if (q.scope) {
287
439
  return c.json((await repo.listSkillsByScopeFilter(q.scope, q.projectId, opts)).map(skillToListItem));
288
440
  }
@@ -290,17 +442,28 @@ function createSkillRoutes(client) {
290
442
  (q.projectId ? await repo.listSkillsByScope(q.projectId, opts) : await repo.listSkills(opts)).map(skillToListItem)
291
443
  );
292
444
  });
445
+ app.get("/api/skills/:name/scope-availability", zValidator2("query", z.object({ scope: z.enum(["global", "project"]) })), async (c) => {
446
+ const name = c.req.param("name");
447
+ const { scope } = c.req.valid("query");
448
+ return c.json({ name, scope, available: !await repo.hasNameScopeConflict(name, scope) });
449
+ });
293
450
  app.get("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), async (c) => {
294
451
  const name = c.req.param("name");
295
452
  const q = c.req.valid("query");
296
453
  const skill = await resolveAssetByScope(repo, name, q, { includeDisabled: q.includeDisabled });
297
- if (!skill) throw new NotFoundError2("skill", name);
454
+ if (!skill) throw new NotFoundError3("skill", name);
455
+ assertProjectVisible(c, skill.projectId);
298
456
  return c.json(skill);
299
457
  });
300
458
  app.post("/api/skills", zValidator2("json", SkillCreateSchema), async (c) => {
301
459
  const data = c.req.valid("json");
302
460
  await assertCategoryValid(data.category);
303
- if (data.scope === "project") await loadProjectForWrite(projectRepo, data.projectId);
461
+ if (data.scope === "project") {
462
+ assertProjectScope(c, data.projectId);
463
+ await loadProjectForWrite(projectRepo, data.projectId);
464
+ } else {
465
+ requireAdminLike(c);
466
+ }
304
467
  return c.json(await repo.createSkill(data), 201);
305
468
  });
306
469
  app.put("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", SkillUpdateSchema), async (c) => {
@@ -308,7 +471,12 @@ function createSkillRoutes(client) {
308
471
  const data = c.req.valid("json");
309
472
  const q = c.req.valid("query");
310
473
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
311
- if (!existing) throw new NotFoundError2("skill", name);
474
+ if (!existing) throw new NotFoundError3("skill", name);
475
+ if ((q.scope ?? "global") === "global") {
476
+ requireAdminLike(c);
477
+ } else {
478
+ assertProjectScope(c, existing.projectId ?? "");
479
+ }
312
480
  if (data.category !== void 0) await assertCategoryValid(data.category);
313
481
  if (isScopeMutation(existing, data)) {
314
482
  throw new ConflictError2("skill", name, {
@@ -333,7 +501,7 @@ function createSkillRoutes(client) {
333
501
  nextReferences: data.references
334
502
  });
335
503
  const skill = await repo.updateByNameScoped(name, q.scope ?? "global", q.projectId, data);
336
- if (!skill) throw new NotFoundError2("skill", name);
504
+ if (!skill) throw new NotFoundError3("skill", name);
337
505
  return c.json(skill);
338
506
  });
339
507
  app.post("/api/skills/:name/copy", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", SkillCopySchema), async (c) => {
@@ -341,9 +509,13 @@ function createSkillRoutes(client) {
341
509
  const input = c.req.valid("json");
342
510
  const q = c.req.valid("query");
343
511
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
344
- if (!existing) throw new NotFoundError2("skill", name);
512
+ if (!existing) throw new NotFoundError3("skill", name);
513
+ assertProjectVisible(c, existing.projectId);
345
514
  if (input.newScope === "project") {
515
+ assertProjectScope(c, input.targetProjectId);
346
516
  await loadProjectForWrite(projectRepo, input.targetProjectId);
517
+ } else {
518
+ requireAdminLike(c);
347
519
  }
348
520
  const copy = await repo.copySkill(
349
521
  name,
@@ -357,9 +529,14 @@ function createSkillRoutes(client) {
357
529
  const name = c.req.param("name");
358
530
  const q = c.req.valid("query");
359
531
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
360
- if (!existing) throw new NotFoundError2("skill", name);
532
+ if (!existing) throw new NotFoundError3("skill", name);
533
+ if ((q.scope ?? "global") === "global") {
534
+ requireAdminLike(c);
535
+ } else {
536
+ assertProjectScope(c, existing.projectId ?? "");
537
+ }
361
538
  const deleted = await repo.deleteByNameScoped(name, q.scope ?? "global", q.projectId);
362
- if (!deleted) throw new NotFoundError2("skill", name);
539
+ if (!deleted) throw new NotFoundError3("skill", name);
363
540
  return c.body(null, 204);
364
541
  });
365
542
  app.post("/api/skills/:name/enabled", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", AssetSetEnabledSchema), async (c) => {
@@ -367,13 +544,16 @@ function createSkillRoutes(client) {
367
544
  const body = c.req.valid("json");
368
545
  const q = c.req.valid("query");
369
546
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
370
- if (!existing) throw new NotFoundError2("skill", name);
547
+ if (!existing) throw new NotFoundError3("skill", name);
371
548
  const scope = q.scope ?? "global";
372
- if (scope === "project") {
549
+ if (scope === "global") {
550
+ requireAdminLike(c);
551
+ } else {
552
+ assertProjectScope(c, existing.projectId ?? "");
373
553
  await loadProjectForWrite(projectRepo, existing.projectId ?? q.projectId);
374
554
  }
375
555
  const skill = await repo.updateByNameScoped(name, scope, q.projectId, { enabled: body.enabled });
376
- if (!skill) throw new NotFoundError2("skill", name);
556
+ if (!skill) throw new NotFoundError3("skill", name);
377
557
  return c.json({ name: skill.name, enabled: skill.enabled ?? true });
378
558
  });
379
559
  return app;
@@ -403,7 +583,7 @@ import {
403
583
  AssetSetEnabledSchema as AssetSetEnabledSchema2,
404
584
  assertReferencesDeletable as assertReferencesDeletable2,
405
585
  assertReferenceAggregateLimit as assertReferenceAggregateLimit2,
406
- NotFoundError as NotFoundError3,
586
+ NotFoundError as NotFoundError4,
407
587
  ConflictError as ConflictError3,
408
588
  BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES3,
409
589
  assertBoundSkillsCompatible,
@@ -431,6 +611,10 @@ function createAgentRoutes(client) {
431
611
  app.get("/api/agents", zValidator3("query", AgentListQuerySchema), async (c) => {
432
612
  const q = c.req.valid("query");
433
613
  const opts = { includeDisabled: q.includeDisabled, q: q.q };
614
+ const auth = getAuth(c);
615
+ if (auth.kind === "project") {
616
+ return c.json((await repo.listAgentsByScope(auth.projectId, opts)).map(agentToListItem));
617
+ }
434
618
  if (q.scope) {
435
619
  return c.json((await repo.listAgentsByScopeFilter(q.scope, q.projectId, opts)).map(agentToListItem));
436
620
  }
@@ -438,16 +622,27 @@ function createAgentRoutes(client) {
438
622
  (q.projectId ? await repo.listAgentsByScope(q.projectId, opts) : await repo.listAgents(opts)).map(agentToListItem)
439
623
  );
440
624
  });
625
+ app.get("/api/agents/:name/scope-availability", zValidator3("query", z2.object({ scope: z2.enum(["global", "project"]) })), async (c) => {
626
+ const name = c.req.param("name");
627
+ const { scope } = c.req.valid("query");
628
+ return c.json({ name, scope, available: !await repo.hasNameScopeConflict(name, scope) });
629
+ });
441
630
  app.get("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), async (c) => {
442
631
  const name = c.req.param("name");
443
632
  const q = c.req.valid("query");
444
633
  const agent = await resolveAssetByScope(repo, name, q, { includeDisabled: q.includeDisabled });
445
- if (!agent) throw new NotFoundError3("agent", name);
634
+ if (!agent) throw new NotFoundError4("agent", name);
635
+ assertProjectVisible(c, agent.projectId);
446
636
  return c.json(agent);
447
637
  });
448
638
  app.post("/api/agents", zValidator3("json", AgentCreateSchema), async (c) => {
449
639
  const data = c.req.valid("json");
450
- if (data.scope === "project") await loadProjectForWrite(projectRepo, data.projectId);
640
+ if (data.scope === "project") {
641
+ assertProjectScope(c, data.projectId);
642
+ await loadProjectForWrite(projectRepo, data.projectId);
643
+ } else {
644
+ requireAdminLike(c);
645
+ }
451
646
  await assertBoundSkillsCompatible(skillRepo, data.scope, data.projectId, data.boundSkills);
452
647
  await assertModelAliasExists(aliasRepo, data.model);
453
648
  return c.json(await repo.createAgent(data), 201);
@@ -457,7 +652,12 @@ function createAgentRoutes(client) {
457
652
  const data = c.req.valid("json");
458
653
  const q = c.req.valid("query");
459
654
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
460
- if (!existing) throw new NotFoundError3("agent", name);
655
+ if (!existing) throw new NotFoundError4("agent", name);
656
+ if ((q.scope ?? "global") === "global") {
657
+ requireAdminLike(c);
658
+ } else {
659
+ assertProjectScope(c, existing.projectId ?? "");
660
+ }
461
661
  let payload = data;
462
662
  if (data["function"] !== void 0 && data["function"] !== existing["function"]) {
463
663
  payload = { ...data, version: bumpPatch(data.version ?? existing.version) };
@@ -496,7 +696,7 @@ function createAgentRoutes(client) {
496
696
  nextReferences: data.references
497
697
  });
498
698
  const agent = await repo.updateByNameScoped(name, q.scope ?? "global", q.projectId, payload);
499
- if (!agent) throw new NotFoundError3("agent", name);
699
+ if (!agent) throw new NotFoundError4("agent", name);
500
700
  return c.json(agent);
501
701
  });
502
702
  app.post("/api/agents/:name/copy", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AgentCopySchema), async (c) => {
@@ -504,9 +704,13 @@ function createAgentRoutes(client) {
504
704
  const input = c.req.valid("json");
505
705
  const q = c.req.valid("query");
506
706
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
507
- if (!existing) throw new NotFoundError3("agent", name);
707
+ if (!existing) throw new NotFoundError4("agent", name);
708
+ assertProjectVisible(c, existing.projectId);
508
709
  if (input.newScope === "project") {
710
+ assertProjectScope(c, input.targetProjectId);
509
711
  await loadProjectForWrite(projectRepo, input.targetProjectId);
712
+ } else {
713
+ requireAdminLike(c);
510
714
  }
511
715
  const copy = await repo.copyAgent(
512
716
  name,
@@ -521,9 +725,14 @@ function createAgentRoutes(client) {
521
725
  const name = c.req.param("name");
522
726
  const q = c.req.valid("query");
523
727
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
524
- if (!existing) throw new NotFoundError3("agent", name);
728
+ if (!existing) throw new NotFoundError4("agent", name);
729
+ if ((q.scope ?? "global") === "global") {
730
+ requireAdminLike(c);
731
+ } else {
732
+ assertProjectScope(c, existing.projectId ?? "");
733
+ }
525
734
  const deleted = await repo.deleteByNameScoped(name, q.scope ?? "global", q.projectId);
526
- if (!deleted) throw new NotFoundError3("agent", name);
735
+ if (!deleted) throw new NotFoundError4("agent", name);
527
736
  return c.body(null, 204);
528
737
  });
529
738
  app.post("/api/agents/:name/enabled", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AssetSetEnabledSchema2), async (c) => {
@@ -531,13 +740,16 @@ function createAgentRoutes(client) {
531
740
  const body = c.req.valid("json");
532
741
  const q = c.req.valid("query");
533
742
  const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
534
- if (!existing) throw new NotFoundError3("agent", name);
743
+ if (!existing) throw new NotFoundError4("agent", name);
535
744
  const scope = q.scope ?? "global";
536
- if (scope === "project") {
745
+ if (scope === "global") {
746
+ requireAdminLike(c);
747
+ } else {
748
+ assertProjectScope(c, existing.projectId ?? "");
537
749
  await loadProjectForWrite(projectRepo, existing.projectId ?? q.projectId);
538
750
  }
539
751
  const agent = await repo.updateByNameScoped(name, scope, q.projectId, { enabled: body.enabled });
540
- if (!agent) throw new NotFoundError3("agent", name);
752
+ if (!agent) throw new NotFoundError4("agent", name);
541
753
  return c.json({ name: agent.name, enabled: agent.enabled ?? true });
542
754
  });
543
755
  return app;
@@ -550,7 +762,7 @@ import {
550
762
  createModelAliasRepo as createModelAliasRepo2,
551
763
  ModelAliasCreateSchema,
552
764
  ModelAliasUpdateSchema,
553
- NotFoundError as NotFoundError4,
765
+ NotFoundError as NotFoundError5,
554
766
  ConflictError as ConflictError4,
555
767
  BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES4
556
768
  } from "@siming-org/core";
@@ -575,7 +787,7 @@ function createModelAliasRoutes(client) {
575
787
  const code = c.req.param("code");
576
788
  const data = c.req.valid("json");
577
789
  const existing = await repo.getByCode(code);
578
- if (!existing) throw new NotFoundError4("model-alias", code);
790
+ if (!existing) throw new NotFoundError5("model-alias", code);
579
791
  if (data.code !== void 0 && data.code !== code) {
580
792
  throw new ConflictError4("model-alias", code, {
581
793
  bizCode: "MODEL_CODE_IMMUTABLE",
@@ -586,13 +798,13 @@ function createModelAliasRoutes(client) {
586
798
  if (data.name !== void 0) patch.name = data.name;
587
799
  if (data.realModel !== void 0) patch.realModel = data.realModel;
588
800
  const alias = await repo.updateByCode(code, patch);
589
- if (!alias) throw new NotFoundError4("model-alias", code);
801
+ if (!alias) throw new NotFoundError5("model-alias", code);
590
802
  return c.json(alias);
591
803
  });
592
804
  app.delete("/api/model-aliases/:code", async (c) => {
593
805
  const code = c.req.param("code");
594
806
  const deleted = await repo.deleteByCode(code);
595
- if (!deleted) throw new NotFoundError4("model-alias", code);
807
+ if (!deleted) throw new NotFoundError5("model-alias", code);
596
808
  return c.body(null, 204);
597
809
  });
598
810
  return app;
@@ -608,98 +820,152 @@ import {
608
820
  createSkillRepo as createSkillRepo3,
609
821
  createAgentRepo as createAgentRepo2,
610
822
  createModelAliasRepo as createModelAliasRepo3,
823
+ createNodePresetRepo,
611
824
  DagTemplateCreateSchema,
612
825
  DagTemplateCopySchema,
613
826
  DagTemplateUpdateSchema,
614
827
  AssetSetEnabledSchema as AssetSetEnabledSchema3,
615
828
  ImportPlanRequestSchema,
616
829
  ImportApplyRequestSchema,
830
+ UpgradeApplyRequestSchema,
831
+ assertUniqueNodeIds,
832
+ buildUpgradePlan,
833
+ applyUpgradeDecisions,
617
834
  composeExportBundle,
618
835
  buildImportPlan,
619
836
  applyImport,
620
- NotFoundError as NotFoundError5,
837
+ NotFoundError as NotFoundError7,
621
838
  ConflictError as ConflictError5,
622
839
  BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES5
623
840
  } from "@siming-org/core";
841
+
842
+ // src/routes/template-ref.ts
843
+ import { NotFoundError as NotFoundError6 } from "@siming-org/core";
844
+ var TEMPLATE_CANDIDATE_LIMIT = 5;
845
+ async function resolveTemplateOr404(repo, ref, projectCtx, refHint) {
846
+ const template = await repo.resolveTemplateByRef(ref, projectCtx);
847
+ if (!template || template.id === void 0) {
848
+ const message = !/^[a-f0-9]{24}$/.test(ref) && projectCtx === void 0 ? `dag-template not found: ${ref}${refHint}` : `dag-template not found: ${ref}`;
849
+ const err = new NotFoundError6("dag-template", ref, { message });
850
+ if (projectCtx !== void 0) {
851
+ err.withTemplateCandidates(await repo.findSimilarTemplates(projectCtx, ref, TEMPLATE_CANDIDATE_LIMIT));
852
+ }
853
+ throw err;
854
+ }
855
+ return template;
856
+ }
857
+
858
+ // src/routes/dag-template.routes.ts
624
859
  var DagTemplateListQuerySchema = z3.object({
625
860
  projectId: z3.string().optional(),
626
861
  name: z3.string().optional(),
862
+ code: z3.string().optional(),
627
863
  includeDisabled: z3.stringbool().optional()
628
864
  });
865
+ var QUERY_PROJECT_HINT = "\uFF08code \u5F62\u6001\u5BFB\u5740\u9700 ?projectId= \u6307\u5B9A\u9879\u76EE\u2014\u2014code \u9879\u76EE\u5185\u552F\u4E00\uFF0C\u65E0\u9879\u76EE\u57DF\u4E0D\u53EF\u6D88\u6B67\uFF1B\u6216\u6539\u4F20 24 \u4F4D\u6570\u636E\u5E93 id\uFF09";
629
866
  function createDagTemplateRoutes(client) {
630
867
  const app = new Hono7();
631
868
  const repo = createDagTemplateRepo(client.db());
632
869
  const projectRepo = createProjectRepo4(client.db());
870
+ const nodePresetRepo = createNodePresetRepo(client.db());
633
871
  const transferDeps = {
634
872
  templateRepo: repo,
635
873
  skillRepo: createSkillRepo3(client.db()),
636
874
  agentRepo: createAgentRepo2(client.db()),
637
- modelAliasRepo: createModelAliasRepo3(client.db())
875
+ modelAliasRepo: createModelAliasRepo3(client.db()),
876
+ nodePresetRepo
638
877
  };
639
878
  app.get("/api/dag/templates", zValidator5("query", DagTemplateListQuerySchema), async (c) => {
640
879
  const q = c.req.valid("query");
880
+ const projectFilter = resolveProjectFilter(c, q.projectId);
641
881
  return c.json(
642
882
  await repo.listDagTemplates({
643
- ...q.projectId ? { projectId: q.projectId } : {},
883
+ ...projectFilter ? { projectId: projectFilter } : {},
644
884
  ...q.name ? { name: q.name } : {},
885
+ ...q.code ? { code: q.code } : {},
645
886
  ...q.includeDisabled !== void 0 ? { includeDisabled: q.includeDisabled } : {}
646
887
  })
647
888
  );
648
889
  });
649
890
  app.get("/api/dag/templates/:id", async (c) => {
650
- const id = c.req.param("id");
651
- const template = await repo.getById(id);
652
- if (!template) throw new NotFoundError5("dag-template", id);
891
+ const ref = c.req.param("id");
892
+ const template = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
893
+ assertProjectScope(c, template.projectId);
653
894
  return c.json(template);
654
895
  });
655
896
  app.post("/api/dag/templates/:id/enabled", zValidator5("json", AssetSetEnabledSchema3), async (c) => {
656
- const id = c.req.param("id");
897
+ const ref = c.req.param("id");
657
898
  const body = c.req.valid("json");
658
- const existing = await repo.getById(id);
659
- if (!existing) throw new NotFoundError5("dag-template", id);
899
+ const existing = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
900
+ assertProjectScope(c, existing.projectId);
660
901
  await loadProjectForWrite(projectRepo, existing.projectId);
661
- const template = await repo.update(id, { enabled: body.enabled });
662
- if (!template) throw new NotFoundError5("dag-template", id);
902
+ const template = await repo.update(existing.id, { enabled: body.enabled });
903
+ if (!template) throw new NotFoundError7("dag-template", existing.id);
663
904
  return c.json({ id: template.id, name: template.name, enabled: template.enabled ?? true });
664
905
  });
665
906
  app.post("/api/dag/templates", zValidator5("json", DagTemplateCreateSchema), async (c) => {
666
907
  const data = c.req.valid("json");
908
+ assertProjectScope(c, data.projectId);
667
909
  await loadProjectForWrite(projectRepo, data.projectId);
910
+ assertUniqueNodeIds(data.nodes);
911
+ if (data.code !== void 0) {
912
+ const dup = await repo._collection.findOne(
913
+ { projectId: data.projectId, code: data.code },
914
+ { projection: { _id: 1, name: 1 } }
915
+ );
916
+ if (dup) {
917
+ throw new ConflictError5("dag-template", `code conflict: ${data.code}`, {
918
+ bizCode: "TEMPLATE_CODE_CONFLICT",
919
+ message: `${BIZ_CODE_MESSAGES5.TEMPLATE_CODE_CONFLICT}\uFF08\u51B2\u7A81\u6A21\u677F\uFF1A${String(dup.name)}\uFF09`
920
+ });
921
+ }
922
+ }
668
923
  return c.json(await repo.createDagTemplate(data), 201);
669
924
  });
670
925
  app.put("/api/dag/templates/:id", zValidator5("json", DagTemplateUpdateSchema), async (c) => {
671
- const id = c.req.param("id");
926
+ const ref = c.req.param("id");
672
927
  const data = c.req.valid("json");
673
- const existing = await repo.getById(id);
674
- if (!existing) throw new NotFoundError5("dag-template", id);
675
- if (data.projectId !== void 0 && data.projectId !== existing.projectId) {
676
- throw new ConflictError5("dag-template", id, {
928
+ const existing = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
929
+ assertProjectScope(c, existing.projectId);
930
+ if (data.code !== void 0 && data.code !== existing.code) {
931
+ throw new ConflictError5("dag-template", ref, {
932
+ bizCode: "TEMPLATE_CODE_IMMUTABLE",
933
+ message: BIZ_CODE_MESSAGES5.TEMPLATE_CODE_IMMUTABLE
934
+ });
935
+ }
936
+ const { code: _codeStrip, ...writable } = data;
937
+ if (writable.projectId !== void 0 && writable.projectId !== existing.projectId) {
938
+ throw new ConflictError5("dag-template", ref, {
677
939
  bizCode: "TEMPLATE_PROJECT_IMMUTABLE",
678
940
  message: BIZ_CODE_MESSAGES5.TEMPLATE_PROJECT_IMMUTABLE
679
941
  });
680
942
  }
681
943
  await loadProjectForWrite(projectRepo, existing.projectId);
682
- if (data.name !== void 0 && data.name !== existing.name) {
944
+ if (data.nodes !== void 0) {
945
+ assertUniqueNodeIds(data.nodes);
946
+ }
947
+ if (writable.name !== void 0 && writable.name !== existing.name) {
683
948
  const dup = await repo._collection.findOne(
684
- { projectId: existing.projectId, name: data.name },
949
+ { projectId: existing.projectId, name: writable.name },
685
950
  { projection: { _id: 1 } }
686
951
  );
687
952
  if (dup) {
688
- throw new ConflictError5("dag-template", `name conflict: ${data.name}`, {
953
+ throw new ConflictError5("dag-template", `name conflict: ${writable.name}`, {
689
954
  bizCode: "TEMPLATE_NAME_CONFLICT",
690
955
  message: BIZ_CODE_MESSAGES5.TEMPLATE_NAME_CONFLICT
691
956
  });
692
957
  }
693
958
  }
694
- const template = await repo.update(id, data);
695
- if (!template) throw new NotFoundError5("dag-template", id);
959
+ const template = await repo.update(existing.id, writable);
960
+ if (!template) throw new NotFoundError7("dag-template", existing.id);
696
961
  return c.json(template);
697
962
  });
698
963
  app.post("/api/dag/templates/:id/copy", zValidator5("json", DagTemplateCopySchema), async (c) => {
699
- const id = c.req.param("id");
964
+ const ref = c.req.param("id");
700
965
  const input = c.req.valid("json");
701
- const existing = await repo.getById(id);
702
- if (!existing) throw new NotFoundError5("dag-template", id);
966
+ const existing = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
967
+ assertProjectScope(c, existing.projectId);
968
+ assertProjectScope(c, input.targetProjectId);
703
969
  await loadProjectForWrite(projectRepo, input.targetProjectId);
704
970
  const dup = await repo._collection.findOne(
705
971
  { projectId: input.targetProjectId, name: input.newName },
@@ -711,40 +977,289 @@ function createDagTemplateRoutes(client) {
711
977
  message: BIZ_CODE_MESSAGES5.TEMPLATE_NAME_CONFLICT
712
978
  });
713
979
  }
714
- const copy = await repo.copyDagTemplate(id, input.targetProjectId, input.newName);
980
+ const copy = await repo.copyDagTemplate(existing.id, input.targetProjectId, input.newName, input.newCode);
715
981
  return c.json(copy, 201);
716
982
  });
717
983
  app.get("/api/dag/templates/:id/export", async (c) => {
718
- const id = c.req.param("id");
719
- return c.json(await composeExportBundle(transferDeps, id));
984
+ const ref = c.req.param("id");
985
+ const template = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
986
+ assertProjectScope(c, template.projectId);
987
+ return c.json(await composeExportBundle(transferDeps, template.id));
720
988
  });
721
989
  app.post("/api/dag/templates/import/plan", zValidator5("json", ImportPlanRequestSchema), async (c) => {
722
990
  const body = c.req.valid("json");
991
+ assertProjectScope(c, body.targetProjectId);
723
992
  await loadProjectForWrite(projectRepo, body.targetProjectId);
724
993
  return c.json(await buildImportPlan(transferDeps, body.targetProjectId, body.bundle));
725
994
  });
726
995
  app.post("/api/dag/templates/import/apply", zValidator5("json", ImportApplyRequestSchema), async (c) => {
727
996
  const body = c.req.valid("json");
997
+ assertProjectScope(c, body.targetProjectId);
728
998
  await loadProjectForWrite(projectRepo, body.targetProjectId);
729
999
  return c.json(await applyImport(transferDeps, body.targetProjectId, body.bundle, body.decisions));
730
1000
  });
731
- app.delete("/api/dag/templates/:id", async (c) => {
1001
+ app.get("/api/dag/templates/:id/upgrade/plan", async (c) => {
732
1002
  const id = c.req.param("id");
733
- const deleted = await repo.delete(id);
734
- if (!deleted) throw new NotFoundError5("dag-template", id);
1003
+ const template = await repo.getById(id);
1004
+ if (!template) throw new NotFoundError7("dag-template", id);
1005
+ assertProjectScope(c, template.projectId);
1006
+ const codes = [...new Set(template.nodes.flatMap((n) => n.sourcePreset ? [n.sourcePreset.code] : []))];
1007
+ const presetsByCode = await nodePresetRepo.listByCodes(codes);
1008
+ return c.json(buildUpgradePlan(template, presetsByCode));
1009
+ });
1010
+ app.post("/api/dag/templates/:id/upgrade/apply", zValidator5("json", UpgradeApplyRequestSchema), async (c) => {
1011
+ const id = c.req.param("id");
1012
+ const body = c.req.valid("json");
1013
+ const template = await repo.getById(id);
1014
+ if (!template) throw new NotFoundError7("dag-template", id);
1015
+ assertProjectScope(c, template.projectId);
1016
+ await loadProjectForWrite(projectRepo, template.projectId);
1017
+ const codes = [...new Set(template.nodes.flatMap((n) => n.sourcePreset ? [n.sourcePreset.code] : []))];
1018
+ const presetsByCode = await nodePresetRepo.listByCodes(codes);
1019
+ const outcome = applyUpgradeDecisions(template, presetsByCode, body.decisions);
1020
+ let version = template.version;
1021
+ if (outcome.nextVersion !== null) {
1022
+ const updated = await repo.update(id, { nodes: outcome.nodes, version: outcome.nextVersion });
1023
+ if (!updated) throw new NotFoundError7("dag-template", id);
1024
+ version = updated.version;
1025
+ }
1026
+ return c.json({ template: { id, version }, results: outcome.results });
1027
+ });
1028
+ app.delete("/api/dag/templates/:id", async (c) => {
1029
+ const ref = c.req.param("id");
1030
+ const existing = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
1031
+ assertProjectScope(c, existing.projectId);
1032
+ const deleted = await repo.delete(existing.id);
1033
+ if (!deleted) throw new NotFoundError7("dag-template", existing.id);
735
1034
  return c.body(null, 204);
736
1035
  });
737
1036
  return app;
738
1037
  }
739
1038
 
740
- // src/routes/task.routes.ts
1039
+ // src/routes/node-preset.routes.ts
741
1040
  import { Hono as Hono8 } from "hono";
742
1041
  import { zValidator as zValidator6 } from "@hono/zod-validator";
743
1042
  import { z as z4 } from "zod";
1043
+ import {
1044
+ createNodePresetRepo as createNodePresetRepo2,
1045
+ createProjectRepo as createProjectRepo5,
1046
+ NodePresetCreateSchema,
1047
+ NodePresetUpdateSchema,
1048
+ NodePresetCopySchema,
1049
+ AssetSetEnabledSchema as AssetSetEnabledSchema4,
1050
+ NODE_PRESET_CODE_PATTERN,
1051
+ computeNodeContentHash,
1052
+ NotFoundError as NotFoundError8,
1053
+ ConflictError as ConflictError6,
1054
+ ValidationError,
1055
+ BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES6
1056
+ } from "@siming-org/core";
1057
+ var NodePresetListQuerySchema = z4.object({
1058
+ projectId: z4.string().optional(),
1059
+ scope: z4.enum(["global", "project"]).optional(),
1060
+ q: z4.string().trim().min(1).max(100).optional(),
1061
+ includeDisabled: z4.stringbool().optional()
1062
+ }).superRefine((val, ctx) => {
1063
+ if (val.scope === "project" && val.projectId === void 0) {
1064
+ ctx.addIssue({ code: "custom", path: ["projectId"], message: "scope=project \u65F6 projectId \u5FC5\u586B" });
1065
+ }
1066
+ });
1067
+ var NodePresetByCodeQuerySchema = z4.object({
1068
+ includeDisabled: z4.stringbool().optional()
1069
+ });
1070
+ function assertCodeShape(code) {
1071
+ if (!NODE_PRESET_CODE_PATTERN.test(code)) {
1072
+ throw new NotFoundError8("node-preset", code);
1073
+ }
1074
+ }
1075
+ function nodePresetToListItem(preset) {
1076
+ const { prompt: _prompt, ...rest } = preset;
1077
+ return { ...rest, agents: preset.agents ?? [] };
1078
+ }
1079
+ var CONTENT_FIELDS = ["nodeId", "phase", "track", "prompt", "skills", "agents", "label"];
1080
+ function isContentChange(existing, data) {
1081
+ return CONTENT_FIELDS.some((field) => {
1082
+ const next = data[field];
1083
+ if (next === void 0) return false;
1084
+ const current = existing[field];
1085
+ if (field === "skills" || field === "agents") {
1086
+ const currentItems = current ?? [];
1087
+ return JSON.stringify([...next].sort()) !== JSON.stringify([...currentItems].sort());
1088
+ }
1089
+ return next !== current;
1090
+ });
1091
+ }
1092
+ function createNodePresetRoutes(client) {
1093
+ const app = new Hono8();
1094
+ const repo = createNodePresetRepo2(client.db());
1095
+ const projectRepo = createProjectRepo5(client.db());
1096
+ app.get("/api/node-presets", zValidator6("query", NodePresetListQuerySchema), async (c) => {
1097
+ const q = c.req.valid("query");
1098
+ const auth = getAuth(c);
1099
+ const filter = { scope: q.scope, projectId: q.projectId, q: q.q, includeDisabled: q.includeDisabled };
1100
+ if (auth.kind === "project") {
1101
+ return c.json(
1102
+ (await repo.listNodePresets({ ...filter, scope: void 0, projectId: auth.projectId })).map(nodePresetToListItem)
1103
+ );
1104
+ }
1105
+ return c.json((await repo.listNodePresets(filter)).map(nodePresetToListItem));
1106
+ });
1107
+ app.get("/api/node-presets/:code/availability", async (c) => {
1108
+ const code = c.req.param("code");
1109
+ assertCodeShape(code);
1110
+ return c.json({ code, available: !await repo.isCodeOccupied(code) });
1111
+ });
1112
+ app.get("/api/node-presets/:code", zValidator6("query", NodePresetByCodeQuerySchema), async (c) => {
1113
+ const code = c.req.param("code");
1114
+ const q = c.req.valid("query");
1115
+ assertCodeShape(code);
1116
+ const preset = await repo.getByCode(code, { includeDisabled: q.includeDisabled });
1117
+ if (!preset) throw new NotFoundError8("node-preset", code);
1118
+ assertProjectVisible(c, preset.projectId);
1119
+ return c.json({ ...preset, agents: preset.agents ?? [], contentHash: computeNodeContentHash(preset) });
1120
+ });
1121
+ app.post("/api/node-presets", zValidator6("json", NodePresetCreateSchema), async (c) => {
1122
+ const data = c.req.valid("json");
1123
+ if (data.scope === "project") {
1124
+ assertProjectScope(c, data.projectId);
1125
+ await loadProjectForWrite(projectRepo, data.projectId);
1126
+ } else {
1127
+ requireAdminLike(c);
1128
+ }
1129
+ const created = await repo.createNodePreset(data);
1130
+ return c.json({ ...created, agents: created.agents ?? [] }, 201);
1131
+ });
1132
+ app.put("/api/node-presets/:code", zValidator6("query", NodePresetByCodeQuerySchema), zValidator6("json", NodePresetUpdateSchema), async (c) => {
1133
+ const code = c.req.param("code");
1134
+ const data = c.req.valid("json");
1135
+ assertCodeShape(code);
1136
+ const existing = await repo.getByCode(code, { includeDisabled: true });
1137
+ if (!existing) throw new NotFoundError8("node-preset", code);
1138
+ if (existing.scope === "global") {
1139
+ requireAdminLike(c);
1140
+ } else {
1141
+ assertProjectScope(c, existing.projectId ?? "");
1142
+ await loadProjectForWrite(projectRepo, existing.projectId ?? "");
1143
+ }
1144
+ if (data.code !== void 0 && data.code !== existing.code) {
1145
+ throw new ConflictError6("node-preset", code, {
1146
+ bizCode: "NODE_PRESET_CODE_IMMUTABLE",
1147
+ message: BIZ_CODE_MESSAGES6.NODE_PRESET_CODE_IMMUTABLE
1148
+ });
1149
+ }
1150
+ if (isScopeMutation(existing, data)) {
1151
+ throw new ConflictError6("node-preset", code, {
1152
+ bizCode: "SCOPE_IMMUTABLE",
1153
+ message: BIZ_CODE_MESSAGES6.SCOPE_IMMUTABLE
1154
+ });
1155
+ }
1156
+ if (isContentChange(existing, data) && (data.version === void 0 || data.version === existing.version)) {
1157
+ throw new ValidationError(
1158
+ "\u8282\u70B9\u5185\u5BB9\u53D8\u66F4\u5FC5\u987B\u540C\u6B65\u63D0\u5347\u7248\u672C\u53F7\uFF08\u643A\u5E26\u53D8\u66F4\u5B57\u6BB5\u65F6 version \u5FC5\u586B\u4E14\u4E0D\u5F97\u4E0E\u73B0\u503C\u76F8\u540C\uFF09",
1159
+ [{ path: ["version"], message: "\u5185\u5BB9\u53D8\u66F4\u672A\u5347\u7248" }],
1160
+ {
1161
+ bizCode: "NODE_PRESET_VERSION_REQUIRED",
1162
+ message: BIZ_CODE_MESSAGES6.NODE_PRESET_VERSION_REQUIRED
1163
+ }
1164
+ );
1165
+ }
1166
+ const preset = await repo.updateByCode(code, data);
1167
+ if (!preset) throw new NotFoundError8("node-preset", code);
1168
+ return c.json({ ...preset, agents: preset.agents ?? [] });
1169
+ });
1170
+ app.delete("/api/node-presets/:code", zValidator6("query", NodePresetByCodeQuerySchema), async (c) => {
1171
+ const code = c.req.param("code");
1172
+ assertCodeShape(code);
1173
+ const existing = await repo.getByCode(code, { includeDisabled: true });
1174
+ if (!existing) throw new NotFoundError8("node-preset", code);
1175
+ if (existing.scope === "global") {
1176
+ requireAdminLike(c);
1177
+ } else {
1178
+ assertProjectScope(c, existing.projectId ?? "");
1179
+ }
1180
+ const deleted = await repo.deleteByCode(code);
1181
+ if (!deleted) throw new NotFoundError8("node-preset", code);
1182
+ return c.body(null, 204);
1183
+ });
1184
+ app.post("/api/node-presets/:code/enabled", zValidator6("query", NodePresetByCodeQuerySchema), zValidator6("json", AssetSetEnabledSchema4), async (c) => {
1185
+ const code = c.req.param("code");
1186
+ const body = c.req.valid("json");
1187
+ assertCodeShape(code);
1188
+ const existing = await repo.getByCode(code, { includeDisabled: true });
1189
+ if (!existing) throw new NotFoundError8("node-preset", code);
1190
+ if (existing.scope === "global") {
1191
+ requireAdminLike(c);
1192
+ } else {
1193
+ assertProjectScope(c, existing.projectId ?? "");
1194
+ await loadProjectForWrite(projectRepo, existing.projectId ?? "");
1195
+ }
1196
+ const preset = await repo.updateByCode(code, { enabled: body.enabled });
1197
+ if (!preset) throw new NotFoundError8("node-preset", code);
1198
+ return c.json({ code: preset.code, enabled: preset.enabled ?? true });
1199
+ });
1200
+ app.post("/api/node-presets/:code/copy", zValidator6("query", NodePresetByCodeQuerySchema), zValidator6("json", NodePresetCopySchema), async (c) => {
1201
+ const code = c.req.param("code");
1202
+ const input = c.req.valid("json");
1203
+ assertCodeShape(code);
1204
+ const existing = await repo.getByCode(code, { includeDisabled: true });
1205
+ if (!existing) throw new NotFoundError8("node-preset", code);
1206
+ assertProjectVisible(c, existing.projectId);
1207
+ if (input.newScope === "project") {
1208
+ assertProjectScope(c, input.targetProjectId);
1209
+ await loadProjectForWrite(projectRepo, input.targetProjectId);
1210
+ } else {
1211
+ requireAdminLike(c);
1212
+ }
1213
+ const copy = await repo.copyNodePreset(
1214
+ code,
1215
+ { scope: input.newScope, ...input.targetProjectId ? { projectId: input.targetProjectId } : {} },
1216
+ input
1217
+ );
1218
+ return c.json({ ...copy, agents: copy.agents ?? [] }, 201);
1219
+ });
1220
+ return app;
1221
+ }
1222
+
1223
+ // src/routes/node-library.routes.ts
1224
+ import { Hono as Hono9 } from "hono";
1225
+ import { zValidator as zValidator7 } from "@hono/zod-validator";
1226
+ import {
1227
+ createNodeLibraryRepo,
1228
+ createProjectRepo as createProjectRepo6,
1229
+ NodeLibraryUpsertSchema
1230
+ } from "@siming-org/core";
1231
+ function createNodeLibraryRoutes(client) {
1232
+ const app = new Hono9();
1233
+ const repo = createNodeLibraryRepo(client.db());
1234
+ const projectRepo = createProjectRepo6(client.db());
1235
+ app.get("/api/node-libraries", async (c) => {
1236
+ const auth = getAuth(c);
1237
+ if (auth.kind === "project") {
1238
+ return c.json(await repo.listNodeLibraries({ projectId: auth.projectId }));
1239
+ }
1240
+ return c.json(await repo.listNodeLibraries());
1241
+ });
1242
+ app.post("/api/node-libraries", zValidator7("json", NodeLibraryUpsertSchema), async (c) => {
1243
+ const data = c.req.valid("json");
1244
+ if (data.scope === "project") {
1245
+ assertProjectScope(c, data.projectId);
1246
+ await loadProjectForWrite(projectRepo, data.projectId);
1247
+ } else {
1248
+ requireAdminLike(c);
1249
+ }
1250
+ return c.json(await repo.upsertNodeLibrary(data));
1251
+ });
1252
+ return app;
1253
+ }
1254
+
1255
+ // src/routes/task.routes.ts
1256
+ import { Hono as Hono10 } from "hono";
1257
+ import { zValidator as zValidator8 } from "@hono/zod-validator";
1258
+ import { z as z5 } from "zod";
744
1259
  import {
745
1260
  createTaskRepo,
746
1261
  createDagTemplateRepo as createDagTemplateRepo2,
747
- createProjectRepo as createProjectRepo5,
1262
+ createProjectRepo as createProjectRepo7,
748
1263
  TaskCreateInputSchema,
749
1264
  AdvanceRequestSchema,
750
1265
  ApproveRequestSchema,
@@ -758,11 +1273,11 @@ import {
758
1273
  ARTIFACT_TYPES,
759
1274
  NODE_ID_PATTERN,
760
1275
  createEnumRegistryRepo as createEnumRegistryRepo2,
761
- NotFoundError as NotFoundError6,
762
- ConflictError as ConflictError6,
1276
+ NotFoundError as NotFoundError9,
1277
+ ConflictError as ConflictError7,
763
1278
  BadRequestError as BadRequestError2,
764
- ValidationError,
765
- BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES6,
1279
+ ValidationError as ValidationError2,
1280
+ BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES7,
766
1281
  advanceTask,
767
1282
  approveTask,
768
1283
  pauseTask,
@@ -774,43 +1289,43 @@ import {
774
1289
  findNextEdge,
775
1290
  generateEntryId
776
1291
  } from "@siming-org/core";
777
- var TaskListQuerySchema = z4.object({
778
- status: z4.string().optional(),
779
- track: z4.string().optional(),
780
- projectId: z4.string().optional(),
781
- q: z4.string().trim().min(1).max(100).optional(),
782
- page: z4.coerce.number().int().min(1).default(1),
783
- limit: z4.coerce.number().int().min(1).max(500).default(20),
784
- sort: z4.enum(["progress", "createdAt", "updatedAt"]).optional()
1292
+ var TaskListQuerySchema = z5.object({
1293
+ status: z5.string().optional(),
1294
+ track: z5.string().optional(),
1295
+ projectId: z5.string().optional(),
1296
+ q: z5.string().trim().min(1).max(100).optional(),
1297
+ page: z5.coerce.number().int().min(1).default(1),
1298
+ limit: z5.coerce.number().int().min(1).max(500).default(20),
1299
+ sort: z5.enum(["progress", "createdAt", "updatedAt"]).optional()
785
1300
  });
786
- var TaskPatchSchema = z4.object({
787
- title: z4.string().min(1).optional()
1301
+ var TaskPatchSchema = z5.object({
1302
+ title: z5.string().min(1).optional()
788
1303
  });
789
- var TaskDocSetSchema = z4.object({
790
- what: z4.string().min(1).max(2e3).optional(),
791
- why: z4.string().min(1).max(2e3).optional(),
792
- trackNote: z4.string().max(2e3).optional()
1304
+ var TaskDocSetSchema = z5.object({
1305
+ what: z5.string().min(1).max(2e3).optional(),
1306
+ why: z5.string().min(1).max(2e3).optional(),
1307
+ trackNote: z5.string().max(2e3).optional()
793
1308
  });
794
- var TextItemSchema = z4.object({ text: z4.string().min(1).max(2e3) });
795
- var RecordSummarySchema = z4.object({ summary: z4.string().min(1).max(2e3) });
796
- var CheckAddSchema = z4.object({ item: z4.string().min(1).max(2e3), passed: z4.boolean().optional() });
797
- var CheckPatchSchema = z4.object({ passed: z4.boolean() });
798
- var ArtifactAddSchema = z4.object({
799
- type: z4.enum(ARTIFACT_TYPES),
800
- path: z4.string().min(1).max(2e3),
801
- note: z4.string().max(2e3).optional(),
1309
+ var TextItemSchema = z5.object({ text: z5.string().min(1).max(2e3) });
1310
+ var RecordSummarySchema = z5.object({ summary: z5.string().min(1).max(2e3) });
1311
+ var CheckAddSchema = z5.object({ item: z5.string().min(1).max(2e3), passed: z5.boolean().optional() });
1312
+ var CheckPatchSchema = z5.object({ passed: z5.boolean() });
1313
+ var ArtifactAddSchema = z5.object({
1314
+ type: z5.enum(ARTIFACT_TYPES),
1315
+ path: z5.string().min(1).max(2e3),
1316
+ note: z5.string().max(2e3).optional(),
802
1317
  /** 全文快照(CLI --file 读文件后传入;≤200k——续跑会话凭 context 自足) */
803
- content: z4.string().max(2e5).optional()
1318
+ content: z5.string().max(2e5).optional()
804
1319
  });
805
- var ConfirmAddSchema = z4.object({ quote: z4.string().min(1).max(2e3) });
806
- var DecisionAddSchema = z4.object({
807
- topic: z4.string().min(1).max(2e3),
808
- decision: z4.string().min(1).max(2e3)
1320
+ var ConfirmAddSchema = z5.object({ quote: z5.string().min(1).max(2e3) });
1321
+ var DecisionAddSchema = z5.object({
1322
+ topic: z5.string().min(1).max(2e3),
1323
+ decision: z5.string().min(1).max(2e3)
809
1324
  });
810
- var ReviewSetSchema = z4.object({
811
- verdict: z4.enum(["pass", "fail"]),
812
- rounds: z4.number().int().min(1),
813
- critical: z4.number().int().min(0)
1325
+ var ReviewSetSchema = z5.object({
1326
+ verdict: z5.enum(["pass", "fail"]),
1327
+ rounds: z5.number().int().min(1),
1328
+ critical: z5.number().int().min(0)
814
1329
  });
815
1330
  function normalizeTaskDoc(doc) {
816
1331
  if (!doc) return null;
@@ -856,17 +1371,18 @@ function writeAck(entity, updated, entry, echo) {
856
1371
  };
857
1372
  }
858
1373
  function createTaskRoutes(client) {
859
- const app = new Hono8();
1374
+ const app = new Hono10();
860
1375
  const repo = createTaskRepo(client.db());
861
1376
  const templateRepo = createDagTemplateRepo2(client.db());
862
- const projectRepo = createProjectRepo5(client.db());
1377
+ const projectRepo = createProjectRepo7(client.db());
863
1378
  const enumRegistryRepo = createEnumRegistryRepo2(client.db());
864
- app.get("/api/tasks", zValidator6("query", TaskListQuerySchema), async (c) => {
1379
+ app.get("/api/tasks", zValidator8("query", TaskListQuerySchema), async (c) => {
865
1380
  const q = c.req.valid("query");
866
1381
  const { items, total } = await repo.listTasks({
867
1382
  ...q.status ? { status: q.status } : {},
868
1383
  ...q.track ? { track: q.track } : {},
869
- ...q.projectId ? { projectId: q.projectId } : {},
1384
+ // T202608310001:project 上下文强制覆写 projectId 过滤("只能看到自己的")
1385
+ ...resolveProjectFilter(c, q.projectId) ? { projectId: resolveProjectFilter(c, q.projectId) } : {},
870
1386
  ...q.q !== void 0 ? { q: q.q } : {},
871
1387
  page: q.page,
872
1388
  limit: q.limit,
@@ -877,10 +1393,17 @@ function createTaskRoutes(client) {
877
1393
  app.get("/api/tasks/:taskId", async (c) => {
878
1394
  const taskId = c.req.param("taskId");
879
1395
  const task = await repo.getByTaskId(taskId);
880
- if (!task) throw new NotFoundError6("task", taskId);
881
- return c.json(task);
1396
+ if (!task) throw new NotFoundError9("task", taskId);
1397
+ assertProjectScope(c, task.projectId);
1398
+ return c.json({
1399
+ ...task,
1400
+ dagInstance: {
1401
+ ...task.dagInstance,
1402
+ nodes: task.dagInstance.nodes.map((node) => ({ ...node, agents: node.agents ?? [] }))
1403
+ }
1404
+ });
882
1405
  });
883
- app.post("/api/tasks", zValidator6("json", TaskCreateInputSchema), async (c) => {
1406
+ app.post("/api/tasks", zValidator8("json", TaskCreateInputSchema), async (c) => {
884
1407
  const input = c.req.valid("json");
885
1408
  const dagTracks = (await enumRegistryRepo.getEntries("dag_track")).filter(
886
1409
  (e) => e.active && e.value !== "all"
@@ -892,21 +1415,26 @@ function createTaskRoutes(client) {
892
1415
  );
893
1416
  }
894
1417
  assertTypeTrackCompatible(input.type, input.track);
895
- const template = await templateRepo.getById(input.dagTemplateId);
896
- if (!template) throw new NotFoundError6("dag-template", input.dagTemplateId);
1418
+ const template = await resolveTemplateOr404(
1419
+ templateRepo,
1420
+ input.dagTemplateId,
1421
+ resolveProjectFilter(c, input.projectId),
1422
+ "\uFF08code \u5F62\u6001\u5BFB\u5740\u9700 body \u643A\u5E26 projectId \u6307\u5B9A\u9879\u76EE\u2014\u2014code \u9879\u76EE\u5185\u552F\u4E00\uFF0C\u65E0\u9879\u76EE\u57DF\u4E0D\u53EF\u6D88\u6B67\uFF1B\u6216\u6539\u4F20 24 \u4F4D\u6570\u636E\u5E93 id\uFF09"
1423
+ );
897
1424
  if (template.enabled === false) {
898
- throw new ConflictError6("task", input.dagTemplateId, {
1425
+ throw new ConflictError7("task", input.dagTemplateId, {
899
1426
  bizCode: "TEMPLATE_DISABLED",
900
- message: BIZ_CODE_MESSAGES6.TEMPLATE_DISABLED
1427
+ message: BIZ_CODE_MESSAGES7.TEMPLATE_DISABLED
901
1428
  });
902
1429
  }
903
1430
  if (input.projectId && input.projectId !== template.projectId) {
904
- throw new ConflictError6("task", input.dagTemplateId, {
1431
+ throw new ConflictError7("task", input.dagTemplateId, {
905
1432
  bizCode: "TEMPLATE_PROJECT_MISMATCH",
906
- message: BIZ_CODE_MESSAGES6.TEMPLATE_PROJECT_MISMATCH
1433
+ message: BIZ_CODE_MESSAGES7.TEMPLATE_PROJECT_MISMATCH
907
1434
  });
908
1435
  }
909
1436
  const resolvedProjectId = input.projectId ?? template.projectId;
1437
+ assertProjectScope(c, resolvedProjectId);
910
1438
  await loadProjectForWrite(projectRepo, resolvedProjectId);
911
1439
  const created = await repo.createTask(input, template, resolvedProjectId);
912
1440
  const firstActive = created.dagInstance.nodes.find(
@@ -923,23 +1451,23 @@ function createTaskRoutes(client) {
923
1451
  };
924
1452
  return c.json(response, 201);
925
1453
  });
926
- app.patch("/api/tasks/:taskId", zValidator6("json", TaskPatchSchema), async (c) => {
1454
+ app.patch("/api/tasks/:taskId", zValidator8("json", TaskPatchSchema), async (c) => {
927
1455
  const taskId = c.req.param("taskId");
928
1456
  const body = c.req.valid("json");
929
1457
  if (body.title === void 0) {
930
1458
  throw new BadRequestError2("PATCH /api/tasks/:taskId \u9700\u8981\u81F3\u5C11\u4E00\u4E2A\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08title\uFF09");
931
1459
  }
932
- await assertTaskProjectActive(repo, projectRepo, taskId);
1460
+ await assertTaskProjectActive(repo, projectRepo, taskId, c);
933
1461
  const patch = { title: body.title };
934
1462
  const updatedTask = await repo.updateTask(taskId, patch);
935
1463
  return c.json(
936
1464
  writeAck(updatedTask, ["title"], void 0, [{ path: "title", excerpt: excerpt(updatedTask.title) }])
937
1465
  );
938
1466
  });
939
- app.post("/api/tasks/:taskId/advance", zValidator6("json", AdvanceRequestSchema), async (c) => {
1467
+ app.post("/api/tasks/:taskId/advance", zValidator8("json", AdvanceRequestSchema), async (c) => {
940
1468
  const taskId = c.req.param("taskId");
941
1469
  const body = c.req.valid("json");
942
- await assertTaskProjectActive(repo, projectRepo, taskId);
1470
+ await assertTaskProjectActive(repo, projectRepo, taskId, c);
943
1471
  const deps = mkDeps(repo);
944
1472
  const result = await advanceTask(deps, taskId, {
945
1473
  ...body.note !== void 0 ? { note: body.note } : {},
@@ -947,32 +1475,32 @@ function createTaskRoutes(client) {
947
1475
  });
948
1476
  return c.json(result, 200);
949
1477
  });
950
- app.post("/api/tasks/:taskId/approve", zValidator6("json", ApproveRequestSchema), async (c) => {
1478
+ app.post("/api/tasks/:taskId/approve", zValidator8("json", ApproveRequestSchema), async (c) => {
951
1479
  const taskId = c.req.param("taskId");
952
1480
  const body = c.req.valid("json");
953
- await assertTaskProjectActive(repo, projectRepo, taskId);
1481
+ await assertTaskProjectActive(repo, projectRepo, taskId, c);
954
1482
  const deps = mkDeps(repo);
955
1483
  const result = await approveTask(deps, taskId, body);
956
1484
  return c.json(result, 200);
957
1485
  });
958
- app.post("/api/tasks/:taskId/pause", zValidator6("json", PauseRequestSchema), async (c) => {
1486
+ app.post("/api/tasks/:taskId/pause", zValidator8("json", PauseRequestSchema), async (c) => {
959
1487
  const taskId = c.req.param("taskId");
960
1488
  const body = c.req.valid("json");
961
- await assertTaskProjectActive(repo, projectRepo, taskId);
1489
+ await assertTaskProjectActive(repo, projectRepo, taskId, c);
962
1490
  const result = await pauseTask(mkDeps(repo), taskId, body.reason);
963
1491
  return c.json(writeAck(result, ["status", "pausedAt"], void 0, [{ path: "status", excerpt: result.status }]));
964
1492
  });
965
- app.post("/api/tasks/:taskId/resume", zValidator6("json", ResumeRequestSchema), async (c) => {
1493
+ app.post("/api/tasks/:taskId/resume", zValidator8("json", ResumeRequestSchema), async (c) => {
966
1494
  const taskId = c.req.param("taskId");
967
1495
  const body = c.req.valid("json");
968
- await assertTaskProjectActive(repo, projectRepo, taskId);
1496
+ await assertTaskProjectActive(repo, projectRepo, taskId, c);
969
1497
  const result = await resumeTask(mkDeps(repo), taskId, body.decision);
970
1498
  return c.json(writeAck(result, ["status"], void 0, [{ path: "status", excerpt: `${result.status}\uFF08\u5DF2\u6062\u590D\u624B\u52A8\u6682\u505C\uFF09` }]));
971
1499
  });
972
- app.post("/api/tasks/:taskId/cancel", zValidator6("json", CancelRequestSchema), async (c) => {
1500
+ app.post("/api/tasks/:taskId/cancel", zValidator8("json", CancelRequestSchema), async (c) => {
973
1501
  const taskId = c.req.param("taskId");
974
1502
  const body = c.req.valid("json");
975
- await assertTaskProjectActive(repo, projectRepo, taskId);
1503
+ await assertTaskProjectActive(repo, projectRepo, taskId, c);
976
1504
  const result = await cancelTask(mkDeps(repo), taskId, body.reason);
977
1505
  return c.json(
978
1506
  writeAck(
@@ -989,17 +1517,24 @@ function createTaskRoutes(client) {
989
1517
  app.get("/api/tasks/:taskId/history", async (c) => {
990
1518
  const taskId = c.req.param("taskId");
991
1519
  const task = await repo.getByTaskId(taskId);
992
- if (!task) throw new NotFoundError6("task", taskId);
1520
+ if (!task) throw new NotFoundError9("task", taskId);
1521
+ assertProjectScope(c, task.projectId);
993
1522
  return c.json(task.history);
994
1523
  });
995
1524
  app.get("/api/tasks/:taskId/node/:nodeId", async (c) => {
996
1525
  const taskId = c.req.param("taskId");
997
1526
  const nodeId = c.req.param("nodeId");
998
1527
  const task = await repo.getByTaskId(taskId);
999
- if (!task) throw new NotFoundError6("task", taskId);
1528
+ if (!task) throw new NotFoundError9("task", taskId);
1529
+ assertProjectScope(c, task.projectId);
1000
1530
  const node = task.dagInstance.nodes.find((n) => n.id === nodeId);
1001
- if (!node) throw new NotFoundError6("node", nodeId);
1002
- return c.json({ ...node, prompt: renderPrompt(node.prompt, task) });
1531
+ if (!node) throw new NotFoundError9("node", nodeId);
1532
+ return c.json({
1533
+ ...node,
1534
+ skills: node.skills ?? [],
1535
+ agents: node.agents ?? [],
1536
+ prompt: renderPrompt(node.prompt, task)
1537
+ });
1003
1538
  });
1004
1539
  app.get("/api/tasks/:taskId/context", async (c) => {
1005
1540
  const taskId = c.req.param("taskId");
@@ -1011,7 +1546,8 @@ function createTaskRoutes(client) {
1011
1546
  }
1012
1547
  const view = viewR.data;
1013
1548
  const task = await repo.getByTaskId(taskId);
1014
- if (!task) throw new NotFoundError6("task", taskId);
1549
+ if (!task) throw new NotFoundError9("task", taskId);
1550
+ assertProjectScope(c, task.projectId);
1015
1551
  const currentNodeActive = task.status === "active" ? task.dagInstance.nodes.find((n) => n.id === task.currentNode) : void 0;
1016
1552
  let pausedAtEdge = null;
1017
1553
  if (task.status === "paused" && task.pausedAt !== null) {
@@ -1046,10 +1582,10 @@ function createTaskRoutes(client) {
1046
1582
  };
1047
1583
  return c.json(context);
1048
1584
  });
1049
- app.patch("/api/tasks/:taskId/doc", zValidator6("json", TaskDocSetSchema), async (c) => {
1585
+ app.patch("/api/tasks/:taskId/doc", zValidator8("json", TaskDocSetSchema), async (c) => {
1050
1586
  const taskId = c.req.param("taskId");
1051
1587
  const body = c.req.valid("json");
1052
- await loadTaskForRecordWrite(repo, projectRepo, taskId);
1588
+ await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
1053
1589
  const sets = {};
1054
1590
  if (body.what !== void 0) sets["doc.what"] = body.what;
1055
1591
  if (body.why !== void 0) sets["doc.why"] = body.why;
@@ -1064,10 +1600,10 @@ function createTaskRoutes(client) {
1064
1600
  )
1065
1601
  );
1066
1602
  });
1067
- app.post("/api/tasks/:taskId/doc/acceptance", zValidator6("json", TextItemSchema), async (c) => {
1603
+ app.post("/api/tasks/:taskId/doc/acceptance", zValidator8("json", TextItemSchema), async (c) => {
1068
1604
  const taskId = c.req.param("taskId");
1069
1605
  const body = c.req.valid("json");
1070
- await loadTaskForRecordWrite(repo, projectRepo, taskId);
1606
+ await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
1071
1607
  const updatedAcc = await repo.updateTaskPaths(taskId, { pushes: { "doc.acceptance": body.text } });
1072
1608
  return c.json(
1073
1609
  writeAck(updatedAcc, ["doc.acceptance"], void 0, [
@@ -1075,20 +1611,20 @@ function createTaskRoutes(client) {
1075
1611
  ])
1076
1612
  );
1077
1613
  });
1078
- app.post("/api/tasks/:taskId/doc/non-goal", zValidator6("json", TextItemSchema), async (c) => {
1614
+ app.post("/api/tasks/:taskId/doc/non-goal", zValidator8("json", TextItemSchema), async (c) => {
1079
1615
  const taskId = c.req.param("taskId");
1080
1616
  const body = c.req.valid("json");
1081
- await loadTaskForRecordWrite(repo, projectRepo, taskId);
1617
+ await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
1082
1618
  const updatedNg = await repo.updateTaskPaths(taskId, { pushes: { "doc.nonGoals": body.text } });
1083
1619
  return c.json(
1084
1620
  writeAck(updatedNg, ["doc.nonGoals"], void 0, [{ path: "doc.nonGoals", excerpt: excerpt(body.text) }])
1085
1621
  );
1086
1622
  });
1087
- app.patch("/api/tasks/:taskId/records/:nodeId/summary", zValidator6("json", RecordSummarySchema), async (c) => {
1623
+ app.patch("/api/tasks/:taskId/records/:nodeId/summary", zValidator8("json", RecordSummarySchema), async (c) => {
1088
1624
  const taskId = c.req.param("taskId");
1089
1625
  const nodeId = c.req.param("nodeId");
1090
1626
  const body = c.req.valid("json");
1091
- await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1627
+ await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1092
1628
  const updatedSummary = await repo.updateTaskPaths(taskId, {
1093
1629
  sets: { [`nodeRecords.${nodeId}.summary`]: body.summary }
1094
1630
  });
@@ -1098,11 +1634,11 @@ function createTaskRoutes(client) {
1098
1634
  ])
1099
1635
  );
1100
1636
  });
1101
- app.post("/api/tasks/:taskId/records/:nodeId/checks", zValidator6("json", CheckAddSchema), async (c) => {
1637
+ app.post("/api/tasks/:taskId/records/:nodeId/checks", zValidator8("json", CheckAddSchema), async (c) => {
1102
1638
  const taskId = c.req.param("taskId");
1103
1639
  const nodeId = c.req.param("nodeId");
1104
1640
  const body = c.req.valid("json");
1105
- const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1641
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1106
1642
  const existing = (task.nodeRecords ?? {})[nodeId]?.checks ?? [];
1107
1643
  const check = {
1108
1644
  id: generateEntryId(existing.map((ch) => ch.id)),
@@ -1121,17 +1657,17 @@ function createTaskRoutes(client) {
1121
1657
  )
1122
1658
  );
1123
1659
  });
1124
- app.patch("/api/tasks/:taskId/records/:nodeId/checks/:checkId", zValidator6("json", CheckPatchSchema), async (c) => {
1660
+ app.patch("/api/tasks/:taskId/records/:nodeId/checks/:checkId", zValidator8("json", CheckPatchSchema), async (c) => {
1125
1661
  const taskId = c.req.param("taskId");
1126
1662
  const nodeId = c.req.param("nodeId");
1127
1663
  const checkId = c.req.param("checkId");
1128
1664
  const body = c.req.valid("json");
1129
- const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1665
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1130
1666
  const check = ((task.nodeRecords ?? {})[nodeId]?.checks ?? []).find((ch) => ch.id === checkId);
1131
1667
  if (!check) {
1132
- throw new NotFoundError6("check", checkId, {
1668
+ throw new NotFoundError9("check", checkId, {
1133
1669
  bizCode: "CHECK_NOT_FOUND",
1134
- message: `${BIZ_CODE_MESSAGES6.CHECK_NOT_FOUND}\uFF08node ${nodeId}, check ${checkId}\uFF09`
1670
+ message: `${BIZ_CODE_MESSAGES7.CHECK_NOT_FOUND}\uFF08node ${nodeId}, check ${checkId}\uFF09`
1135
1671
  });
1136
1672
  }
1137
1673
  const updatedFlip = await repo.updateTaskPaths(taskId, {
@@ -1144,11 +1680,11 @@ function createTaskRoutes(client) {
1144
1680
  ])
1145
1681
  );
1146
1682
  });
1147
- app.post("/api/tasks/:taskId/records/:nodeId/artifacts", zValidator6("json", ArtifactAddSchema), async (c) => {
1683
+ app.post("/api/tasks/:taskId/records/:nodeId/artifacts", zValidator8("json", ArtifactAddSchema), async (c) => {
1148
1684
  const taskId = c.req.param("taskId");
1149
1685
  const nodeId = c.req.param("nodeId");
1150
1686
  const body = c.req.valid("json");
1151
- const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1687
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1152
1688
  const artifact = {
1153
1689
  id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.artifacts ?? []).map((a) => a.id)),
1154
1690
  type: body.type,
@@ -1175,11 +1711,11 @@ function createTaskRoutes(client) {
1175
1711
  )
1176
1712
  );
1177
1713
  });
1178
- app.post("/api/tasks/:taskId/records/:nodeId/confirmations", zValidator6("json", ConfirmAddSchema), async (c) => {
1714
+ app.post("/api/tasks/:taskId/records/:nodeId/confirmations", zValidator8("json", ConfirmAddSchema), async (c) => {
1179
1715
  const taskId = c.req.param("taskId");
1180
1716
  const nodeId = c.req.param("nodeId");
1181
1717
  const body = c.req.valid("json");
1182
- const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1718
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1183
1719
  const confirmation = {
1184
1720
  id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.confirmations ?? []).map((cf) => cf.id)),
1185
1721
  quote: body.quote,
@@ -1194,11 +1730,11 @@ function createTaskRoutes(client) {
1194
1730
  ])
1195
1731
  );
1196
1732
  });
1197
- app.post("/api/tasks/:taskId/records/:nodeId/decisions", zValidator6("json", DecisionAddSchema), async (c) => {
1733
+ app.post("/api/tasks/:taskId/records/:nodeId/decisions", zValidator8("json", DecisionAddSchema), async (c) => {
1198
1734
  const taskId = c.req.param("taskId");
1199
1735
  const nodeId = c.req.param("nodeId");
1200
1736
  const body = c.req.valid("json");
1201
- const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1737
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1202
1738
  const decision = {
1203
1739
  id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.decisions ?? []).map((d) => d.id)),
1204
1740
  topic: body.topic,
@@ -1213,11 +1749,11 @@ function createTaskRoutes(client) {
1213
1749
  ])
1214
1750
  );
1215
1751
  });
1216
- app.put("/api/tasks/:taskId/records/:nodeId/review", zValidator6("json", ReviewSetSchema), async (c) => {
1752
+ app.put("/api/tasks/:taskId/records/:nodeId/review", zValidator8("json", ReviewSetSchema), async (c) => {
1217
1753
  const taskId = c.req.param("taskId");
1218
1754
  const nodeId = c.req.param("nodeId");
1219
1755
  const body = c.req.valid("json");
1220
- await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
1756
+ await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1221
1757
  const updatedReview = await repo.updateTaskPaths(taskId, {
1222
1758
  sets: { [`nodeRecords.${nodeId}.review`]: body }
1223
1759
  });
@@ -1227,10 +1763,10 @@ function createTaskRoutes(client) {
1227
1763
  ])
1228
1764
  );
1229
1765
  });
1230
- app.post("/api/tasks/:taskId/archnotes", zValidator6("json", TextItemSchema), async (c) => {
1766
+ app.post("/api/tasks/:taskId/archnotes", zValidator8("json", TextItemSchema), async (c) => {
1231
1767
  const taskId = c.req.param("taskId");
1232
1768
  const body = c.req.valid("json");
1233
- const task = await loadTaskForRecordWrite(repo, projectRepo, taskId);
1769
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
1234
1770
  const note = { id: generateEntryId((task.archNotes ?? []).map((n) => n.id)), text: body.text, at: /* @__PURE__ */ new Date() };
1235
1771
  const updatedNote = await repo.updateTaskPaths(taskId, { pushes: { archNotes: note } });
1236
1772
  return c.json(
@@ -1242,7 +1778,7 @@ function createTaskRoutes(client) {
1242
1778
  return app;
1243
1779
  }
1244
1780
  function assertTypeTrackCompatible(type, track) {
1245
- const mismatch = (reason) => new ValidationError(
1781
+ const mismatch = (reason) => new ValidationError2(
1246
1782
  reason,
1247
1783
  [{ code: "custom", path: ["type"], message: reason }],
1248
1784
  { bizCode: "TASK_TYPE_TRACK_MISMATCH", message: reason }
@@ -1267,20 +1803,22 @@ function mkDeps(repo) {
1267
1803
  updateTask: repo.updateTask.bind(repo)
1268
1804
  };
1269
1805
  }
1270
- async function assertTaskProjectActive(repo, projectRepo, taskId) {
1806
+ async function assertTaskProjectActive(repo, projectRepo, taskId, c) {
1271
1807
  const task = await repo.getByTaskId(taskId);
1272
- if (!task) throw new NotFoundError6("task", taskId);
1808
+ if (!task) throw new NotFoundError9("task", taskId);
1809
+ assertProjectScope(c, task.projectId);
1273
1810
  await loadProjectForWrite(projectRepo, task.projectId);
1274
1811
  }
1275
- async function loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId) {
1812
+ async function loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c) {
1276
1813
  const task = await repo.getByTaskId(taskId);
1277
- if (!task) throw new NotFoundError6("task", taskId);
1814
+ if (!task) throw new NotFoundError9("task", taskId);
1815
+ assertProjectScope(c, task.projectId);
1278
1816
  await loadProjectForWrite(projectRepo, task.projectId);
1279
1817
  if (task.status === "completed" || task.status === "cancelled") {
1280
1818
  throw new BadRequestError2(
1281
1819
  `Task '${taskId}' is ${task.status} \u2014 task records are read-only after termination`,
1282
1820
  void 0,
1283
- { bizCode: "TASK_TERMINATED", message: `${BIZ_CODE_MESSAGES6.TASK_TERMINATED}\uFF08status: ${task.status}\uFF09` }
1821
+ { bizCode: "TASK_TERMINATED", message: `${BIZ_CODE_MESSAGES7.TASK_TERMINATED}\uFF08status: ${task.status}\uFF09` }
1284
1822
  );
1285
1823
  }
1286
1824
  if (nodeId === void 0) return task;
@@ -1289,9 +1827,9 @@ async function loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId) {
1289
1827
  }
1290
1828
  const node = task.dagInstance.nodes.find((n) => n.id === nodeId);
1291
1829
  if (!node) {
1292
- throw new NotFoundError6("node", nodeId, {
1830
+ throw new NotFoundError9("node", nodeId, {
1293
1831
  bizCode: "NODE_NOT_IN_INSTANCE",
1294
- message: `${BIZ_CODE_MESSAGES6.NODE_NOT_IN_INSTANCE}\uFF08nodeId: ${nodeId}\uFF09`
1832
+ message: `${BIZ_CODE_MESSAGES7.NODE_NOT_IN_INSTANCE}\uFF08nodeId: ${nodeId}\uFF09`
1295
1833
  });
1296
1834
  }
1297
1835
  const state = task.dagInstance.nodeStates[nodeId];
@@ -1301,57 +1839,271 @@ async function loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId) {
1301
1839
  void 0,
1302
1840
  {
1303
1841
  bizCode: "NODE_RECORD_NOT_WRITABLE",
1304
- message: `${BIZ_CODE_MESSAGES6.NODE_RECORD_NOT_WRITABLE}\uFF08${nodeId}: ${state?.status ?? "pending"}\uFF09`
1842
+ message: `${BIZ_CODE_MESSAGES7.NODE_RECORD_NOT_WRITABLE}\uFF08${nodeId}: ${state?.status ?? "pending"}\uFF09`
1305
1843
  }
1306
1844
  );
1307
1845
  }
1308
1846
  return task;
1309
1847
  }
1310
1848
 
1849
+ // src/routes/task-batch.routes.ts
1850
+ import { Hono as Hono11 } from "hono";
1851
+ import { zValidator as zValidator9 } from "@hono/zod-validator";
1852
+ import { z as z6 } from "zod";
1853
+ import {
1854
+ createTaskRepo as createTaskRepo2,
1855
+ createProjectRepo as createProjectRepo8,
1856
+ ValidationError as ValidationError3,
1857
+ BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES8,
1858
+ toTaskPublic as toTaskPublic2,
1859
+ taskProgress as taskProgress2,
1860
+ generateEntryId as generateEntryId2
1861
+ } from "@siming-org/core";
1862
+ var BATCH_LIMIT = 50;
1863
+ var ARTIFACT_BATCH_CONTENT_LIMIT = 2e6;
1864
+ var BatchTextSchema = z6.string().min(1).max(2e3);
1865
+ var ObjectItemsSchema = z6.object({ items: z6.array(z6.record(z6.string(), z6.unknown())) });
1866
+ var StringItemsSchema = z6.object({ items: z6.array(z6.string()) });
1867
+ function zodIssueToMessage(issue) {
1868
+ const shape = issue;
1869
+ switch (issue.code) {
1870
+ case "too_small":
1871
+ return `\u957F\u5EA6\u4E0D\u8DB3\uFF08\u6700\u5C0F ${String(shape.minimum)}\uFF09`;
1872
+ case "too_big":
1873
+ return `\u8D85\u51FA\u4E0A\u9650\uFF08\u6700\u5927 ${String(shape.maximum)}\uFF09`;
1874
+ case "invalid_type":
1875
+ return `\u7C7B\u578B\u4E0D\u5339\u914D\uFF08\u671F\u671B ${String(shape.expected)}\uFF09`;
1876
+ case "invalid_value":
1877
+ return `\u503C\u4E0D\u5408\u6CD5\uFF08${issue.message}\uFF09`;
1878
+ default:
1879
+ return issue.message;
1880
+ }
1881
+ }
1882
+ function partitionItems(items, schema) {
1883
+ const ok = [];
1884
+ const bad = [];
1885
+ items.forEach((item, index) => {
1886
+ const r = schema.safeParse(item);
1887
+ if (r.success) {
1888
+ ok.push({ index, data: r.data });
1889
+ } else {
1890
+ const issue = r.error.issues[0];
1891
+ const loc = issue !== void 0 && issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
1892
+ bad.push({ index, message: `${loc}${issue !== void 0 ? zodIssueToMessage(issue) : "\u6761\u76EE\u683C\u5F0F\u4E0D\u5408\u6CD5"}` });
1893
+ }
1894
+ });
1895
+ return { ok, bad };
1896
+ }
1897
+ function assertBatchSize(items) {
1898
+ if (items.length === 0) {
1899
+ throw new ValidationError3(BIZ_CODE_MESSAGES8.BATCH_EMPTY, [], { bizCode: "BATCH_EMPTY" });
1900
+ }
1901
+ if (items.length > BATCH_LIMIT) {
1902
+ throw new ValidationError3(BIZ_CODE_MESSAGES8.BATCH_LIMIT_EXCEEDED, [], {
1903
+ bizCode: "BATCH_LIMIT_EXCEEDED"
1904
+ });
1905
+ }
1906
+ }
1907
+ function batchAck(entity, updated, entries, failures) {
1908
+ const applied = entries.length;
1909
+ return {
1910
+ taskId: entity.taskId,
1911
+ applied,
1912
+ updated: applied > 0 ? updated : [],
1913
+ entries,
1914
+ failures,
1915
+ updatedAt: entity.updatedAt ?? /* @__PURE__ */ new Date(),
1916
+ task: { ...toTaskPublic2(entity), projectId: entity.projectId, progress: taskProgress2(entity) }
1917
+ };
1918
+ }
1919
+ function createTaskBatchRoutes(client) {
1920
+ const app = new Hono11();
1921
+ const repo = createTaskRepo2(client.db());
1922
+ const projectRepo = createProjectRepo8(client.db());
1923
+ app.post("/api/tasks/:taskId/records/:nodeId/checks/batch", zValidator9("json", ObjectItemsSchema), async (c) => {
1924
+ const taskId = c.req.param("taskId");
1925
+ const nodeId = c.req.param("nodeId");
1926
+ const { items } = c.req.valid("json");
1927
+ assertBatchSize(items);
1928
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1929
+ const { ok, bad } = partitionItems(items, CheckAddSchema);
1930
+ const idSeq = ((task.nodeRecords ?? {})[nodeId]?.checks ?? []).map((ch) => ch.id);
1931
+ const checks = [];
1932
+ const entries = [];
1933
+ for (const { index, data } of ok) {
1934
+ const check = { id: generateEntryId2(idSeq), item: data.item, passed: data.passed ?? true };
1935
+ idSeq.push(check.id);
1936
+ checks.push(check);
1937
+ entries.push({ index, id: check.id, kind: "check" });
1938
+ }
1939
+ if (checks.length === 0) {
1940
+ return c.json(batchAck(task, [`nodeRecords.${nodeId}.checks`], entries, bad));
1941
+ }
1942
+ const updated = await repo.updateTaskPaths(taskId, {
1943
+ pushes: { [`nodeRecords.${nodeId}.checks`]: { $each: checks } }
1944
+ });
1945
+ return c.json(batchAck(updated, [`nodeRecords.${nodeId}.checks`], entries, bad));
1946
+ });
1947
+ app.post("/api/tasks/:taskId/records/:nodeId/artifacts/batch", zValidator9("json", ObjectItemsSchema), async (c) => {
1948
+ const taskId = c.req.param("taskId");
1949
+ const nodeId = c.req.param("nodeId");
1950
+ const { items } = c.req.valid("json");
1951
+ assertBatchSize(items);
1952
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1953
+ const { ok, bad } = partitionItems(items, ArtifactAddSchema);
1954
+ const totalContent = ok.reduce((sum, { data }) => sum + (data.content?.length ?? 0), 0);
1955
+ if (totalContent > ARTIFACT_BATCH_CONTENT_LIMIT) {
1956
+ throw new ValidationError3(BIZ_CODE_MESSAGES8.BATCH_PAYLOAD_TOO_LARGE, [], {
1957
+ bizCode: "BATCH_PAYLOAD_TOO_LARGE"
1958
+ });
1959
+ }
1960
+ const idSeq = ((task.nodeRecords ?? {})[nodeId]?.artifacts ?? []).map((a) => a.id);
1961
+ const artifacts = [];
1962
+ const entries = [];
1963
+ for (const { index, data } of ok) {
1964
+ const artifact = {
1965
+ id: generateEntryId2(idSeq),
1966
+ type: data.type,
1967
+ path: data.path,
1968
+ ...data.note !== void 0 ? { note: data.note } : {},
1969
+ ...data.content !== void 0 ? { content: data.content } : {}
1970
+ };
1971
+ idSeq.push(artifact.id);
1972
+ artifacts.push(artifact);
1973
+ entries.push({ index, id: artifact.id, kind: "artifact" });
1974
+ }
1975
+ if (artifacts.length === 0) {
1976
+ return c.json(batchAck(task, [`nodeRecords.${nodeId}.artifacts`], entries, bad));
1977
+ }
1978
+ const updated = await repo.updateTaskPaths(taskId, {
1979
+ pushes: { [`nodeRecords.${nodeId}.artifacts`]: { $each: artifacts } }
1980
+ });
1981
+ return c.json(batchAck(updated, [`nodeRecords.${nodeId}.artifacts`], entries, bad));
1982
+ });
1983
+ app.post("/api/tasks/:taskId/records/:nodeId/decisions/batch", zValidator9("json", ObjectItemsSchema), async (c) => {
1984
+ const taskId = c.req.param("taskId");
1985
+ const nodeId = c.req.param("nodeId");
1986
+ const { items } = c.req.valid("json");
1987
+ assertBatchSize(items);
1988
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
1989
+ const { ok, bad } = partitionItems(items, DecisionAddSchema);
1990
+ const idSeq = ((task.nodeRecords ?? {})[nodeId]?.decisions ?? []).map((d) => d.id);
1991
+ const decisions = [];
1992
+ const entries = [];
1993
+ for (const { index, data } of ok) {
1994
+ const decision = { id: generateEntryId2(idSeq), topic: data.topic, decision: data.decision };
1995
+ idSeq.push(decision.id);
1996
+ decisions.push(decision);
1997
+ entries.push({ index, id: decision.id, kind: "decision" });
1998
+ }
1999
+ if (decisions.length === 0) {
2000
+ return c.json(batchAck(task, [`nodeRecords.${nodeId}.decisions`], entries, bad));
2001
+ }
2002
+ const updated = await repo.updateTaskPaths(taskId, {
2003
+ pushes: { [`nodeRecords.${nodeId}.decisions`]: { $each: decisions } }
2004
+ });
2005
+ return c.json(batchAck(updated, [`nodeRecords.${nodeId}.decisions`], entries, bad));
2006
+ });
2007
+ app.post("/api/tasks/:taskId/doc/acceptance/batch", zValidator9("json", StringItemsSchema), async (c) => {
2008
+ const taskId = c.req.param("taskId");
2009
+ const { items } = c.req.valid("json");
2010
+ assertBatchSize(items);
2011
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
2012
+ const { ok, bad } = partitionItems(items, BatchTextSchema);
2013
+ const entries = ok.map(({ index }) => ({ index }));
2014
+ if (ok.length === 0) {
2015
+ return c.json(batchAck(task, ["doc.acceptance"], entries, bad));
2016
+ }
2017
+ const updated = await repo.updateTaskPaths(taskId, {
2018
+ pushes: { "doc.acceptance": { $each: ok.map(({ data }) => data) } }
2019
+ });
2020
+ return c.json(batchAck(updated, ["doc.acceptance"], entries, bad));
2021
+ });
2022
+ app.post("/api/tasks/:taskId/doc/non-goal/batch", zValidator9("json", StringItemsSchema), async (c) => {
2023
+ const taskId = c.req.param("taskId");
2024
+ const { items } = c.req.valid("json");
2025
+ assertBatchSize(items);
2026
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
2027
+ const { ok, bad } = partitionItems(items, BatchTextSchema);
2028
+ const entries = ok.map(({ index }) => ({ index }));
2029
+ if (ok.length === 0) {
2030
+ return c.json(batchAck(task, ["doc.nonGoals"], entries, bad));
2031
+ }
2032
+ const updated = await repo.updateTaskPaths(taskId, {
2033
+ pushes: { "doc.nonGoals": { $each: ok.map(({ data }) => data) } }
2034
+ });
2035
+ return c.json(batchAck(updated, ["doc.nonGoals"], entries, bad));
2036
+ });
2037
+ app.post("/api/tasks/:taskId/archnotes/batch", zValidator9("json", StringItemsSchema), async (c) => {
2038
+ const taskId = c.req.param("taskId");
2039
+ const { items } = c.req.valid("json");
2040
+ assertBatchSize(items);
2041
+ const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
2042
+ const { ok, bad } = partitionItems(items, BatchTextSchema);
2043
+ const idSeq = (task.archNotes ?? []).map((n) => n.id);
2044
+ const notes = [];
2045
+ const entries = [];
2046
+ for (const { index, data } of ok) {
2047
+ const note = { id: generateEntryId2(idSeq), text: data, at: /* @__PURE__ */ new Date() };
2048
+ idSeq.push(note.id);
2049
+ notes.push(note);
2050
+ entries.push({ index, id: note.id, kind: "archnote" });
2051
+ }
2052
+ if (notes.length === 0) {
2053
+ return c.json(batchAck(task, ["archNotes"], entries, bad));
2054
+ }
2055
+ const updated = await repo.updateTaskPaths(taskId, {
2056
+ pushes: { archNotes: { $each: notes } }
2057
+ });
2058
+ return c.json(batchAck(updated, ["archNotes"], entries, bad));
2059
+ });
2060
+ return app;
2061
+ }
2062
+
1311
2063
  // src/routes/settings.routes.ts
1312
- import { Hono as Hono9 } from "hono";
1313
- import { zValidator as zValidator7 } from "@hono/zod-validator";
1314
- import { z as z5 } from "zod";
2064
+ import { Hono as Hono12 } from "hono";
2065
+ import { zValidator as zValidator10 } from "@hono/zod-validator";
2066
+ import { z as z7 } from "zod";
1315
2067
  import {
1316
2068
  createEnumRegistryRepo as createEnumRegistryRepo3,
1317
2069
  EnumRegistryUpdateSchema,
1318
- NotFoundError as NotFoundError7
2070
+ NotFoundError as NotFoundError10
1319
2071
  } from "@siming-org/core";
1320
2072
  function createSettingsRoutes(client) {
1321
- const app = new Hono9();
2073
+ const app = new Hono12();
1322
2074
  const repo = createEnumRegistryRepo3(client.db());
1323
2075
  app.get("/api/settings/enums", async (c) => {
1324
2076
  return c.json(await repo.listRegistries());
1325
2077
  });
1326
2078
  app.get(
1327
2079
  "/api/settings/enums/:category",
1328
- zValidator7("param", z5.object({ category: z5.string() })),
2080
+ zValidator10("param", z7.object({ category: z7.string() })),
1329
2081
  async (c) => {
1330
2082
  const { category } = c.req.valid("param");
1331
2083
  const registry = await repo.getRegistry(category);
1332
- if (!registry) throw new NotFoundError7("enum-registry", category);
2084
+ if (!registry) throw new NotFoundError10("enum-registry", category);
1333
2085
  return c.json(registry);
1334
2086
  }
1335
2087
  );
1336
2088
  app.put(
1337
2089
  "/api/settings/enums/:category",
1338
- zValidator7("param", z5.object({ category: z5.string() })),
1339
- zValidator7("json", EnumRegistryUpdateSchema),
2090
+ zValidator10("param", z7.object({ category: z7.string() })),
2091
+ zValidator10("json", EnumRegistryUpdateSchema),
1340
2092
  async (c) => {
1341
2093
  const { category } = c.req.valid("param");
1342
2094
  const data = c.req.valid("json");
1343
2095
  const registry = await repo.updateRegistry(category, data);
1344
- if (!registry) throw new NotFoundError7("enum-registry", category);
2096
+ if (!registry) throw new NotFoundError10("enum-registry", category);
1345
2097
  return c.json(registry);
1346
2098
  }
1347
2099
  );
1348
2100
  app.delete(
1349
2101
  "/api/settings/enums/:category/entries/:value",
1350
- zValidator7("param", z5.object({ category: z5.string(), value: z5.string().min(1) })),
2102
+ zValidator10("param", z7.object({ category: z7.string(), value: z7.string().min(1) })),
1351
2103
  async (c) => {
1352
2104
  const { category, value } = c.req.valid("param");
1353
2105
  const deleted = await repo.deleteEntry(category, value);
1354
- if (!deleted) throw new NotFoundError7("enum-registry", category);
2106
+ if (!deleted) throw new NotFoundError10("enum-registry", category);
1355
2107
  return c.body(null, 204);
1356
2108
  }
1357
2109
  );
@@ -1360,23 +2112,195 @@ function createSettingsRoutes(client) {
1360
2112
 
1361
2113
  // src/routes/index.ts
1362
2114
  function createRoutes(client) {
1363
- const router = new Hono10();
2115
+ const router = new Hono13();
1364
2116
  router.route("/", createHealth(client));
1365
2117
  router.route("/", createProjectRoutes(client));
1366
2118
  router.route("/", createSkillRoutes(client));
1367
2119
  router.route("/", createAgentRoutes(client));
1368
2120
  router.route("/", createModelAliasRoutes(client));
1369
2121
  router.route("/", createDagTemplateRoutes(client));
2122
+ router.route("/", createNodePresetRoutes(client));
2123
+ router.route("/", createNodeLibraryRoutes(client));
1370
2124
  router.route("/", createTaskRoutes(client));
2125
+ router.route("/", createTaskBatchRoutes(client));
1371
2126
  router.route("/", createSettingsRoutes(client));
1372
2127
  return router;
1373
2128
  }
1374
2129
 
2130
+ // src/routes/auth.routes.ts
2131
+ import { Hono as Hono14 } from "hono";
2132
+ import { zValidator as zValidator11 } from "@hono/zod-validator";
2133
+ import {
2134
+ createAuthAccountRepo,
2135
+ createAuthTokenRepo as createAuthTokenRepo2,
2136
+ createAuthSessionRepo as createAuthSessionRepo2,
2137
+ createProjectRepo as createProjectRepo9,
2138
+ hashPassword,
2139
+ verifyPassword,
2140
+ generateToken,
2141
+ generateSessionId,
2142
+ hashToken as hashToken2,
2143
+ AuthAccountCreateSchema,
2144
+ LoginSchema,
2145
+ PasswordChangeSchema,
2146
+ ConflictError as ConflictError8,
2147
+ NotFoundError as NotFoundError11
2148
+ } from "@siming-org/core";
2149
+ var DUMMY_PASSWORD_HASH = await hashPassword("timing-equalizer-dummy");
2150
+ function createAuthRoutes(client, deps) {
2151
+ const app = new Hono14();
2152
+ const accountRepo = createAuthAccountRepo(client.db());
2153
+ const tokenRepo = createAuthTokenRepo2(client.db());
2154
+ const sessionRepo = createAuthSessionRepo2(client.db());
2155
+ const projectRepo = createProjectRepo9(client.db());
2156
+ function setSessionCookie(c, sessionId) {
2157
+ c.header("Set-Cookie", `${SESSION_COOKIE_NAME}=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.floor(SESSION_TTL_MS / 1e3)}`);
2158
+ }
2159
+ function clearSessionCookie(c) {
2160
+ c.header("Set-Cookie", `${SESSION_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
2161
+ }
2162
+ app.get("/api/auth/status", async (c) => {
2163
+ const account = await accountRepo.getAccount();
2164
+ return c.json({ authEnabled: deps.authEnabled, needsSetup: deps.authEnabled && account === null });
2165
+ });
2166
+ const whoamiHandler = async (c) => {
2167
+ const auth = getAuth(c);
2168
+ const base = { authEnabled: deps.authEnabled, server: { url: new URL(c.req.url).origin } };
2169
+ if (!deps.authEnabled || auth.kind === "open" || auth.kind === "none") {
2170
+ return c.json({ ...base, identity: "none", project: null });
2171
+ }
2172
+ if (auth.kind === "admin") {
2173
+ return c.json({ ...base, identity: "admin", project: null });
2174
+ }
2175
+ const project = await projectRepo.getById(auth.projectId);
2176
+ return c.json({
2177
+ ...base,
2178
+ identity: "project",
2179
+ project: project ? { id: project.id ?? auth.projectId, key: project.key, name: project.name } : null
2180
+ });
2181
+ };
2182
+ app.get("/api/auth/whoami", whoamiHandler);
2183
+ app.post("/api/auth/whoami", whoamiHandler);
2184
+ const v422 = (schema) => zValidator11("json", schema, (result, c) => {
2185
+ if (!result.success) {
2186
+ return c.json({ error: "validation_error", message: "\u683C\u5F0F\u4E0D\u7B26", issues: result.error.issues }, 422);
2187
+ }
2188
+ return void 0;
2189
+ });
2190
+ app.post("/api/auth/setup", v422(AuthAccountCreateSchema), async (c) => {
2191
+ const body = c.req.valid("json");
2192
+ const existing = await accountRepo.getAccount();
2193
+ if (existing) {
2194
+ throw new ConflictError8("auth-account", existing.id ?? "unknown", {
2195
+ bizCode: "AUTH_ACCOUNT_EXISTS",
2196
+ message: "\u7BA1\u7406\u5458\u8D26\u53F7\u5DF2\u521D\u59CB\u5316\uFF08\u65E0\u9700\u91CD\u590D\u521B\u5EFA\uFF09"
2197
+ });
2198
+ }
2199
+ const passwordHash = await hashPassword(body.password);
2200
+ const account = await accountRepo.createAccount(body.username, passwordHash);
2201
+ const sessionId = generateSessionId();
2202
+ await sessionRepo.createSession(sessionId, account.id ?? "", new Date(Date.now() + SESSION_TTL_MS));
2203
+ setSessionCookie(c, sessionId);
2204
+ return c.json({ username: account.username });
2205
+ });
2206
+ app.post("/api/auth/login", zValidator11("json", LoginSchema), async (c) => {
2207
+ const body = c.req.valid("json");
2208
+ const account = await accountRepo.getAccount();
2209
+ if (!account || account.username !== body.username) {
2210
+ await verifyPassword(body.password, DUMMY_PASSWORD_HASH);
2211
+ return c.json({ error: "AUTH_INVALID_CREDENTIALS", message: "\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF" }, 401);
2212
+ }
2213
+ const ok = await verifyPassword(body.password, account.passwordHash);
2214
+ if (!ok) {
2215
+ return c.json({ error: "AUTH_INVALID_CREDENTIALS", message: "\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF" }, 401);
2216
+ }
2217
+ const sessionId = generateSessionId();
2218
+ await sessionRepo.createSession(sessionId, account.id ?? "", new Date(Date.now() + SESSION_TTL_MS));
2219
+ setSessionCookie(c, sessionId);
2220
+ return c.json({ username: account.username });
2221
+ });
2222
+ app.post("/api/auth/logout", async (c) => {
2223
+ const sessionId = readSessionCookie(c);
2224
+ if (sessionId !== void 0) {
2225
+ await sessionRepo.deleteBySessionId(sessionId);
2226
+ }
2227
+ clearSessionCookie(c);
2228
+ return c.body(null, 204);
2229
+ });
2230
+ app.get("/api/auth/me", async (c) => {
2231
+ const auth = getAuth(c);
2232
+ if (auth.kind !== "admin") {
2233
+ return c.json({ error: "AUTH_REQUIRED", message: "\u9700\u8981\u767B\u5F55" }, 401);
2234
+ }
2235
+ const account = await accountRepo.getAccount();
2236
+ return c.json({ username: account?.username ?? "" });
2237
+ });
2238
+ app.put("/api/auth/password", v422(PasswordChangeSchema), async (c) => {
2239
+ const auth = getAuth(c);
2240
+ if (auth.kind !== "admin") {
2241
+ return c.json({ error: "AUTH_REQUIRED", message: "\u9700\u8981\u767B\u5F55" }, 401);
2242
+ }
2243
+ const body = c.req.valid("json");
2244
+ const account = await accountRepo.getAccount();
2245
+ if (!account) {
2246
+ return c.json({ error: "AUTH_REQUIRED", message: "\u9700\u8981\u767B\u5F55" }, 401);
2247
+ }
2248
+ const ok = await verifyPassword(body.currentPassword, account.passwordHash);
2249
+ if (!ok) {
2250
+ return c.json({ error: "AUTH_INVALID_CREDENTIALS", message: "\u8D26\u53F7\u6216\u5BC6\u7801\u9519\u8BEF" }, 401);
2251
+ }
2252
+ await accountRepo.updatePassword(account.id ?? "", await hashPassword(body.newPassword));
2253
+ return c.body(null, 204);
2254
+ });
2255
+ app.post("/api/auth/tokens/admin", async (c) => {
2256
+ const token = generateToken("admin");
2257
+ await tokenRepo.upsertToken("admin", hashToken2(token));
2258
+ return c.json({ token, type: "admin" }, 201);
2259
+ });
2260
+ app.get("/api/auth/tokens/admin", async (c) => {
2261
+ const status = await tokenRepo.getTokenStatus("admin");
2262
+ return c.json({ type: "admin", hasToken: status.hasToken, createdAt: status.createdAt });
2263
+ });
2264
+ app.get("/api/auth/tokens/project", async (c) => {
2265
+ const projects = await projectRepo.list();
2266
+ const statuses = await tokenRepo.listProjectTokenStatuses(projects.map((p) => p.id ?? ""));
2267
+ const items = projects.map((p) => {
2268
+ const st = statuses.get(p.id ?? "");
2269
+ return { type: "project", projectId: p.id, hasToken: st?.hasToken ?? false, createdAt: st?.createdAt ?? null };
2270
+ });
2271
+ return c.json({ items, total: items.length, page: 1, limit: items.length });
2272
+ });
2273
+ app.post("/api/auth/tokens/project/:projectId", async (c) => {
2274
+ const projectId = c.req.param("projectId");
2275
+ const project = await projectRepo.getById(projectId);
2276
+ if (!project) throw new NotFoundError11("project", projectId);
2277
+ if (project.status === "archived") {
2278
+ throw new ConflictError8("project", projectId, { bizCode: "PROJECT_ARCHIVED" });
2279
+ }
2280
+ const token = generateToken("project");
2281
+ await tokenRepo.upsertToken("project", hashToken2(token), projectId);
2282
+ return c.json({ token, type: "project", projectId }, 201);
2283
+ });
2284
+ app.get("/api/auth/tokens/project/:projectId", async (c) => {
2285
+ const projectId = c.req.param("projectId");
2286
+ const status = await tokenRepo.getTokenStatus("project", projectId);
2287
+ return c.json({ type: "project", projectId, hasToken: status.hasToken, createdAt: status.createdAt });
2288
+ });
2289
+ return app;
2290
+ }
2291
+ function readSessionCookie(c) {
2292
+ return getCookie(c, SESSION_COOKIE_NAME);
2293
+ }
2294
+
1375
2295
  // src/app.ts
1376
2296
  function createApp(client, opts = {}) {
1377
- const app = new Hono11();
2297
+ const app = new Hono15();
1378
2298
  setupErrorHandler(app);
2299
+ const authEnabled = opts.authEnabled ?? false;
2300
+ app.use("/api/*", createAuthMiddleware(client, { authEnabled }));
2301
+ app.use("/health/*", createAuthMiddleware(client, { authEnabled }));
1379
2302
  app.route("/", createRoutes(client));
2303
+ app.route("/", createAuthRoutes(client, { authEnabled }));
1380
2304
  const webRoot = opts.webRoot !== void 0 ? opts.webRoot : resolveWebDistRoot();
1381
2305
  app.route("/", createStaticRoutes(webRoot));
1382
2306
  return app;
@@ -1387,7 +2311,7 @@ import { serve } from "@hono/node-server";
1387
2311
  import { createMongoClient } from "@siming-org/core";
1388
2312
 
1389
2313
  // src/ensure-collections.ts
1390
- import { EnumRegistrySchema, bumpPatch as bumpPatch2 } from "@siming-org/core";
2314
+ import { EnumRegistrySchema, bumpPatch as bumpPatch2, createAuthSessionRepo as createAuthSessionRepo3, buildTemplateCode, isLegalTemplateCode } from "@siming-org/core";
1391
2315
  async function ensureCollections(client) {
1392
2316
  const db = client.db();
1393
2317
  await Promise.all([
@@ -1397,13 +2321,17 @@ async function ensureCollections(client) {
1397
2321
  ensureDagTemplateCollection(db),
1398
2322
  ensureTaskCollection(db),
1399
2323
  ensureModelAliasCollection(db),
1400
- ensureEnumRegistryCollection(db)
2324
+ ensureEnumRegistryCollection(db),
2325
+ ensureAuthCollections(db),
2326
+ ensureNodePresetCollection(db),
2327
+ ensureNodeLibraryCollection(db)
1401
2328
  ]);
1402
2329
  await migrateProjectSentinelData(db);
1403
2330
  await migrateGateRemovalData(db);
1404
2331
  await migrateTaskRecordData(db);
1405
2332
  await migrateDagTrackTaskValues(db);
1406
2333
  await migrateAgentFunctionBackfill(db);
2334
+ await migrateDagTemplateCode(db);
1407
2335
  await ensurePostMigrationIndexes(db);
1408
2336
  }
1409
2337
  var ENUM_REGISTRY_SEEDS = {
@@ -1464,6 +2392,57 @@ function buildSeedEntries(seeds) {
1464
2392
  ...s.color ? { color: s.color } : {}
1465
2393
  }));
1466
2394
  }
2395
+ async function ensureAuthCollections(db) {
2396
+ const accountValidator = { $jsonSchema: {
2397
+ bsonType: "object",
2398
+ required: ["username", "passwordHash"],
2399
+ properties: {
2400
+ username: { bsonType: "string", minLength: 1 },
2401
+ passwordHash: { bsonType: "string", minLength: 1 },
2402
+ createdAt: { bsonType: "date" },
2403
+ updatedAt: { bsonType: "date" }
2404
+ }
2405
+ } };
2406
+ const collections = await db.listCollections({ name: "auth_accounts" }).toArray();
2407
+ if (collections.length === 0) {
2408
+ await db.createCollection("auth_accounts", { validator: accountValidator });
2409
+ }
2410
+ await db.collection("auth_accounts").createIndex({ username: 1 }, { unique: true });
2411
+ const tokenValidator = { $jsonSchema: {
2412
+ bsonType: "object",
2413
+ required: ["tokenHash", "type"],
2414
+ properties: {
2415
+ tokenHash: { bsonType: "string" },
2416
+ type: { enum: ["admin", "project"] },
2417
+ projectId: { bsonType: "string" },
2418
+ createdAt: { bsonType: "date" },
2419
+ updatedAt: { bsonType: "date" }
2420
+ }
2421
+ } };
2422
+ const tokenColls = await db.listCollections({ name: "auth_tokens" }).toArray();
2423
+ if (tokenColls.length === 0) {
2424
+ await db.createCollection("auth_tokens", { validator: tokenValidator });
2425
+ }
2426
+ await db.collection("auth_tokens").createIndex({ tokenHash: 1 }, { unique: true });
2427
+ await db.collection("auth_tokens").createIndex(
2428
+ { projectId: 1 },
2429
+ { unique: true, partialFilterExpression: { type: "project" } }
2430
+ );
2431
+ await db.collection("auth_tokens").createIndex(
2432
+ { type: 1 },
2433
+ { unique: true, partialFilterExpression: { type: "admin" } }
2434
+ );
2435
+ const sessionColls = await db.listCollections({ name: "auth_sessions" }).toArray();
2436
+ if (sessionColls.length === 0) {
2437
+ await db.createCollection("auth_sessions");
2438
+ }
2439
+ await db.collection("auth_sessions").createIndex({ sessionId: 1 }, { unique: true });
2440
+ await db.collection("auth_sessions").createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
2441
+ const removed = await createAuthSessionRepo3(db).deleteExpiredSessions();
2442
+ if (removed > 0) {
2443
+ console.log(`[siming] auth: \u6E05\u626B\u8FC7\u671F\u4F1A\u8BDD ${String(removed)} \u4E2A`);
2444
+ }
2445
+ }
1467
2446
  async function ensureEnumRegistryCollection(db) {
1468
2447
  const collections = await db.listCollections({ name: "enum_registry" }).toArray();
1469
2448
  if (collections.length === 0) {
@@ -1618,6 +2597,9 @@ async function ensureDagTemplateCollection(db) {
1618
2597
  properties: {
1619
2598
  name: { bsonType: "string" },
1620
2599
  projectId: { bsonType: "string" },
2600
+ // T202609020002:业务 code(可选——存量迁移前缺失合法,M13 回填;格式规则归应用层 Zod,
2601
+ // DB 只做类型兜底,pattern 禁令见技术架构「DB validator 职责边界」)
2602
+ code: { bsonType: "string" },
1621
2603
  description: { bsonType: "string" },
1622
2604
  nodes: { bsonType: "array" },
1623
2605
  edges: { bsonType: "array" },
@@ -1778,6 +2760,95 @@ async function migrateAgentFunctionBackfill(db) {
1778
2760
  }
1779
2761
  }
1780
2762
  }
2763
+ async function ensureNodePresetCollection(db) {
2764
+ const validator = { $jsonSchema: {
2765
+ bsonType: "object",
2766
+ required: ["code", "label", "nodeId", "phase", "track", "prompt", "skills", "scope", "version"],
2767
+ properties: {
2768
+ code: { bsonType: "string" },
2769
+ label: { bsonType: "string", minLength: 1 },
2770
+ nodeId: { bsonType: "string" },
2771
+ phase: { bsonType: "string" },
2772
+ track: { bsonType: "string" },
2773
+ prompt: { bsonType: "string" },
2774
+ skills: { bsonType: "array", items: { bsonType: "string" } },
2775
+ agents: { bsonType: "array", items: { bsonType: "string" } },
2776
+ scope: { enum: ["global", "project"] },
2777
+ projectId: { bsonType: "string" },
2778
+ description: { bsonType: "string" },
2779
+ version: { bsonType: "string" },
2780
+ source: { bsonType: "object" },
2781
+ enabled: { bsonType: "bool" },
2782
+ createdAt: { bsonType: "date" },
2783
+ updatedAt: { bsonType: "date" }
2784
+ }
2785
+ } };
2786
+ const collections = await db.listCollections({ name: "node_presets" }).toArray();
2787
+ if (collections.length === 0) {
2788
+ await db.createCollection("node_presets", { validator });
2789
+ } else {
2790
+ await db.command({ collMod: "node_presets", validationLevel: "strict", validator });
2791
+ }
2792
+ await db.collection("node_presets").createIndex({ code: 1 }, { unique: true });
2793
+ await db.collection("node_presets").createIndex({ scope: 1, projectId: 1 });
2794
+ }
2795
+ async function ensureNodeLibraryCollection(db) {
2796
+ const validator = { $jsonSchema: {
2797
+ bsonType: "object",
2798
+ required: ["name", "version", "scope", "nodes", "compositions"],
2799
+ properties: {
2800
+ name: { bsonType: "string", minLength: 1 },
2801
+ version: { bsonType: "string" },
2802
+ scope: { enum: ["global", "project"] },
2803
+ projectId: { bsonType: "string" },
2804
+ nodes: { bsonType: "array", items: { bsonType: "string" } },
2805
+ compositions: { bsonType: "array" },
2806
+ installedAt: { bsonType: "date" },
2807
+ createdAt: { bsonType: "date" },
2808
+ updatedAt: { bsonType: "date" }
2809
+ }
2810
+ } };
2811
+ const collections = await db.listCollections({ name: "node_libraries" }).toArray();
2812
+ if (collections.length === 0) {
2813
+ await db.createCollection("node_libraries", { validator });
2814
+ }
2815
+ await db.collection("node_libraries").createIndex({ name: 1 }, { unique: true });
2816
+ await db.collection("node_libraries").createIndex({ scope: 1, projectId: 1 });
2817
+ }
2818
+ async function migrateDagTemplateCode(db) {
2819
+ const coll = db.collection("dag_templates");
2820
+ const docs = await coll.find({}, { projection: { projectId: 1, name: 1, code: 1 } }).toArray();
2821
+ const takenByProject = /* @__PURE__ */ new Map();
2822
+ for (const doc of docs) {
2823
+ const projectId = String(doc.projectId);
2824
+ if (typeof doc.code === "string" && isLegalTemplateCode(doc.code)) {
2825
+ const taken = takenByProject.get(projectId) ?? /* @__PURE__ */ new Set();
2826
+ taken.add(doc.code);
2827
+ takenByProject.set(projectId, taken);
2828
+ }
2829
+ }
2830
+ let migrated = 0;
2831
+ for (const doc of docs) {
2832
+ const projectId = String(doc.projectId);
2833
+ if (typeof doc.code === "string" && isLegalTemplateCode(doc.code)) continue;
2834
+ const taken = takenByProject.get(projectId) ?? /* @__PURE__ */ new Set();
2835
+ const code = buildTemplateCode(String(doc.name ?? ""), taken);
2836
+ taken.add(code);
2837
+ takenByProject.set(projectId, taken);
2838
+ const result = await coll.updateOne(
2839
+ { _id: doc._id },
2840
+ { $set: { code } },
2841
+ { bypassDocumentValidation: true }
2842
+ );
2843
+ if (result.matchedCount === 0) {
2844
+ throw new Error("T202609020002 M13 \u8FC1\u79FB\u5931\u8D25\uFF1Acode \u56DE\u586B\u672A\u547D\u4E2D\uFF08\u542F\u52A8\u671F\u5355\u8FDB\u7A0B\u4E32\u884C\uFF0C\u4E0D\u5E94\u53D1\u751F\uFF09");
2845
+ }
2846
+ migrated += 1;
2847
+ }
2848
+ if (migrated > 0) {
2849
+ console.log(`[siming] T202609020002 M13\uFF1Adag_templates.code \u56DE\u586B ${migrated} \u4E2A`);
2850
+ }
2851
+ }
1781
2852
  async function ensurePostMigrationIndexes(db) {
1782
2853
  const tasks = db.collection("tasks");
1783
2854
  const taskIndexes = await tasks.listIndexes().toArray();
@@ -1806,6 +2877,30 @@ async function ensurePostMigrationIndexes(db) {
1806
2877
  throw new Error(`dag_templates \u5B58\u5728\u540C\u9879\u76EE\u91CD\u540D\u6A21\u677F\uFF0C\u65E0\u6CD5\u5EFA\u7ACB (projectId,name) \u552F\u4E00\u7D22\u5F15\uFF0C\u8BF7\u5148\u4EBA\u5DE5\u5F52\u5E76\uFF1A${detail}`);
1807
2878
  }
1808
2879
  await templates.createIndex({ projectId: 1, name: 1 }, { unique: true });
2880
+ const seenCodes = /* @__PURE__ */ new Map();
2881
+ const codeDocs = await templates.find(
2882
+ { code: { $exists: true } },
2883
+ { projection: { projectId: 1, code: 1 } }
2884
+ ).toArray();
2885
+ for (const doc of codeDocs) {
2886
+ if (typeof doc.code !== "string") continue;
2887
+ const key = `${String(doc.projectId)}\0${doc.code}`;
2888
+ const ids = seenCodes.get(key) ?? [];
2889
+ ids.push(doc._id.toString());
2890
+ seenCodes.set(key, ids);
2891
+ }
2892
+ const codeConflicts = [...seenCodes.entries()].filter(([, ids]) => ids.length > 1);
2893
+ if (codeConflicts.length > 0) {
2894
+ const detail = codeConflicts.map(([key, ids]) => {
2895
+ const [projectId, code] = key.split("\0");
2896
+ return `projectId=${projectId} code="${code}" \xD7${ids.length} (_id: ${ids.join(", ")})`;
2897
+ }).join("; ");
2898
+ throw new Error(`dag_templates \u5B58\u5728\u540C\u9879\u76EE\u91CD\u590D code\uFF0C\u65E0\u6CD5\u5EFA\u7ACB (projectId,code) \u552F\u4E00\u7D22\u5F15\uFF0C\u8BF7\u5148\u4EBA\u5DE5\u5F52\u5E76\uFF1A${detail}`);
2899
+ }
2900
+ await templates.createIndex(
2901
+ { projectId: 1, code: 1 },
2902
+ { unique: true, partialFilterExpression: { code: { $type: "string" } } }
2903
+ );
1809
2904
  await db.collection("agents").createIndex({ model: 1 });
1810
2905
  await migrateNameUniquenessIndexes(db);
1811
2906
  }
@@ -1859,7 +2954,7 @@ async function startServer(config) {
1859
2954
  });
1860
2955
  await ensureCollections(client);
1861
2956
  const webRoot = resolveWebDistRoot();
1862
- const app = createApp(client, { webRoot });
2957
+ const app = createApp(client, { webRoot, authEnabled: config.auth.enabled });
1863
2958
  let port = config.port;
1864
2959
  const server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
1865
2960
  port = info.port;
@@ -1876,6 +2971,7 @@ async function startServer(config) {
1876
2971
  }
1877
2972
 
1878
2973
  export {
2974
+ ADMIN_ONLY_ROUTES,
1879
2975
  resolveWebDistRoot,
1880
2976
  createStaticRoutes,
1881
2977
  createApp,