@siming-org/server 0.3.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.
- package/dist/chunk-XZWVPHDG.js +2980 -0
- package/dist/index.d.ts +39 -2
- package/dist/index.js +3 -1
- package/dist/main.js +43 -3
- package/package.json +2 -2
- package/web-dist/assets/index-CDjWDFDb.js +150 -0
- package/web-dist/assets/index-DRm1IYKE.css +1 -0
- package/web-dist/index.html +2 -2
- package/dist/chunk-T7QAGG73.js +0 -1558
- package/web-dist/assets/index-B_ZlCIgd.css +0 -1
- package/web-dist/assets/index-hBW7IJvz.js +0 -121
|
@@ -0,0 +1,2980 @@
|
|
|
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
|
+
|
|
111
|
+
// src/static.ts
|
|
112
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
113
|
+
import { join, resolve, sep } from "path";
|
|
114
|
+
import { Hono } from "hono";
|
|
115
|
+
var ASSETS_PREFIX = "/assets/";
|
|
116
|
+
var API_NAMESPACES = ["/api", "/health"];
|
|
117
|
+
function isApiNamespace(pathname) {
|
|
118
|
+
return API_NAMESPACES.some((ns) => pathname === ns || pathname.startsWith(`${ns}/`));
|
|
119
|
+
}
|
|
120
|
+
var MIME_TYPES = {
|
|
121
|
+
html: "text/html; charset=utf-8",
|
|
122
|
+
js: "text/javascript; charset=utf-8",
|
|
123
|
+
mjs: "text/javascript; charset=utf-8",
|
|
124
|
+
css: "text/css; charset=utf-8",
|
|
125
|
+
json: "application/json; charset=utf-8",
|
|
126
|
+
map: "application/json",
|
|
127
|
+
svg: "image/svg+xml",
|
|
128
|
+
png: "image/png",
|
|
129
|
+
jpg: "image/jpeg",
|
|
130
|
+
jpeg: "image/jpeg",
|
|
131
|
+
gif: "image/gif",
|
|
132
|
+
webp: "image/webp",
|
|
133
|
+
avif: "image/avif",
|
|
134
|
+
ico: "image/x-icon",
|
|
135
|
+
woff: "font/woff",
|
|
136
|
+
woff2: "font/woff2",
|
|
137
|
+
ttf: "font/ttf",
|
|
138
|
+
otf: "font/otf",
|
|
139
|
+
txt: "text/plain; charset=utf-8",
|
|
140
|
+
wasm: "application/wasm"
|
|
141
|
+
};
|
|
142
|
+
function contentTypeFor(filePath) {
|
|
143
|
+
const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".") + 1).toLowerCase() : "";
|
|
144
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
145
|
+
}
|
|
146
|
+
function resolveWebDistRoot() {
|
|
147
|
+
const base = import.meta.dirname;
|
|
148
|
+
const candidates = [
|
|
149
|
+
join(base, "..", "web-dist"),
|
|
150
|
+
// dist 布局:包根/web-dist;src 布局:包根/web-dist(均命中)
|
|
151
|
+
join(base, "..", "..", "web", "dist")
|
|
152
|
+
// dist/src 布局回退:repo packages/web/dist
|
|
153
|
+
];
|
|
154
|
+
for (const dir of candidates) {
|
|
155
|
+
if (existsSync(join(dir, "index.html"))) return dir;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function resolveSafe(webRoot, urlPathname) {
|
|
160
|
+
const segments = urlPathname.split("/").filter((s) => s.length > 0);
|
|
161
|
+
if (segments.includes("..")) return null;
|
|
162
|
+
const target = resolve(webRoot, ...segments);
|
|
163
|
+
if (target !== webRoot && !target.startsWith(webRoot + sep)) return null;
|
|
164
|
+
return target;
|
|
165
|
+
}
|
|
166
|
+
function serveFile(absPath, cacheControl) {
|
|
167
|
+
const body = readFileSync(absPath);
|
|
168
|
+
return new Response(body, {
|
|
169
|
+
status: 200,
|
|
170
|
+
headers: { "Content-Type": contentTypeFor(absPath), "Cache-Control": cacheControl }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function createStaticRoutes(webRoot) {
|
|
174
|
+
const app = new Hono();
|
|
175
|
+
app.get("*", async (c) => {
|
|
176
|
+
const pathname = c.req.path;
|
|
177
|
+
if (isApiNamespace(pathname)) {
|
|
178
|
+
return c.notFound();
|
|
179
|
+
}
|
|
180
|
+
if (webRoot === null) {
|
|
181
|
+
return c.json(
|
|
182
|
+
{
|
|
183
|
+
error: "web_ui_not_built",
|
|
184
|
+
message: "Web UI \u672A\u6784\u5EFA\uFF0C\u672C\u670D\u52A1\u4EC5\u63D0\u4F9B API\u3002\u5982\u5728 siming \u4ED3\u5E93\u5185\uFF1A\u6267\u884C pnpm --filter @siming-org/web build \u540E\u91CD\u542F\uFF1B\u5982\u9700\u5B8C\u6574\u7BA1\u7406\u754C\u9762\uFF1A\u901A\u8FC7 npm \u5B89\u88C5\u542B web-dist \u7684 @siming-org/server \u5206\u53D1\u7248\u672C"
|
|
185
|
+
},
|
|
186
|
+
503
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
let decoded;
|
|
190
|
+
try {
|
|
191
|
+
decoded = decodeURIComponent(pathname);
|
|
192
|
+
} catch {
|
|
193
|
+
return c.json({ error: "bad_path", message: "URL \u8DEF\u5F84\u7F16\u7801\u975E\u6CD5" }, 400);
|
|
194
|
+
}
|
|
195
|
+
const absPath = resolveSafe(webRoot, decoded === "/" ? "" : decoded);
|
|
196
|
+
if (absPath === null) {
|
|
197
|
+
return c.json({ error: "bad_path", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5" }, 403);
|
|
198
|
+
}
|
|
199
|
+
const st = statSync(absPath, { throwIfNoEntry: false });
|
|
200
|
+
if (st?.isFile()) {
|
|
201
|
+
const cache = decoded.startsWith(ASSETS_PREFIX) ? "public, max-age=31536000, immutable" : "no-cache";
|
|
202
|
+
return serveFile(absPath, cache);
|
|
203
|
+
}
|
|
204
|
+
if (decoded.startsWith(ASSETS_PREFIX)) {
|
|
205
|
+
return c.notFound();
|
|
206
|
+
}
|
|
207
|
+
const indexPath = join(webRoot, "index.html");
|
|
208
|
+
if (existsSync(indexPath)) {
|
|
209
|
+
return serveFile(indexPath, "no-cache");
|
|
210
|
+
}
|
|
211
|
+
return c.notFound();
|
|
212
|
+
});
|
|
213
|
+
return app;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/app.ts
|
|
217
|
+
import { Hono as Hono15 } from "hono";
|
|
218
|
+
|
|
219
|
+
// src/errors.ts
|
|
220
|
+
import { AppError } from "@siming-org/core";
|
|
221
|
+
function setupErrorHandler(app) {
|
|
222
|
+
app.onError((err, c) => {
|
|
223
|
+
if (err instanceof AppError) {
|
|
224
|
+
const body = JSON.stringify(err.toResponse());
|
|
225
|
+
return new Response(body, { status: err.statusCode, headers: { "Content-Type": "application/json" } });
|
|
226
|
+
}
|
|
227
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === 11e3) {
|
|
228
|
+
return c.json({ error: "conflict", message: "duplicate key" }, 409);
|
|
229
|
+
}
|
|
230
|
+
console.error("Unhandled error:", err);
|
|
231
|
+
return c.json(
|
|
232
|
+
{ error: "internal_error", message: "An unexpected error occurred" },
|
|
233
|
+
500
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/routes/index.ts
|
|
239
|
+
import { Hono as Hono13 } from "hono";
|
|
240
|
+
|
|
241
|
+
// src/routes/health.ts
|
|
242
|
+
import { Hono as Hono2 } from "hono";
|
|
243
|
+
import { ping, VERSION } from "@siming-org/core";
|
|
244
|
+
function createHealth(client) {
|
|
245
|
+
const app = new Hono2();
|
|
246
|
+
app.get("/health", async (c) => {
|
|
247
|
+
let mongoStatus = "ok";
|
|
248
|
+
try {
|
|
249
|
+
await client.db().command({ ping: 1 });
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.error(`[health] mongo ping failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
252
|
+
mongoStatus = "error";
|
|
253
|
+
}
|
|
254
|
+
const status = mongoStatus === "ok" ? "ok" : "degraded";
|
|
255
|
+
return c.json({
|
|
256
|
+
status,
|
|
257
|
+
version: VERSION,
|
|
258
|
+
pong: ping(),
|
|
259
|
+
mongo: { status: mongoStatus }
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
return app;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/routes/project.routes.ts
|
|
266
|
+
import { Hono as Hono3 } from "hono";
|
|
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
|
|
300
|
+
import {
|
|
301
|
+
createProjectRepo,
|
|
302
|
+
ProjectCreateSchema,
|
|
303
|
+
ProjectUpdateSchema,
|
|
304
|
+
NotFoundError as NotFoundError2,
|
|
305
|
+
ConflictError,
|
|
306
|
+
BIZ_CODE_MESSAGES,
|
|
307
|
+
DEFAULT_PROJECT_KEY
|
|
308
|
+
} from "@siming-org/core";
|
|
309
|
+
async function loadProjectForWrite(projectRepo, projectId) {
|
|
310
|
+
const project = await projectRepo.getById(projectId);
|
|
311
|
+
if (!project) {
|
|
312
|
+
throw new NotFoundError2("project", projectId, {
|
|
313
|
+
bizCode: "PROJECT_NOT_FOUND",
|
|
314
|
+
message: BIZ_CODE_MESSAGES.PROJECT_NOT_FOUND
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (project.status === "archived") {
|
|
318
|
+
throw new ConflictError("project", projectId, {
|
|
319
|
+
bizCode: "PROJECT_ARCHIVED",
|
|
320
|
+
message: BIZ_CODE_MESSAGES.PROJECT_ARCHIVED
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return project;
|
|
324
|
+
}
|
|
325
|
+
function createProjectRoutes(client) {
|
|
326
|
+
const app = new Hono3();
|
|
327
|
+
const repo = createProjectRepo(client.db());
|
|
328
|
+
app.get("/api/projects", async (c) => {
|
|
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
|
+
}
|
|
335
|
+
const projects = await repo.list();
|
|
336
|
+
return c.json(status ? projects.filter((p) => p.status === status) : projects);
|
|
337
|
+
});
|
|
338
|
+
app.get("/api/projects/:id", async (c) => {
|
|
339
|
+
const id = c.req.param("id");
|
|
340
|
+
const project = await repo.getById(id);
|
|
341
|
+
if (!project) throw new NotFoundError2("project", id);
|
|
342
|
+
assertProjectScope(c, project.id ?? id);
|
|
343
|
+
return c.json(project);
|
|
344
|
+
});
|
|
345
|
+
app.post("/api/projects", zValidator("json", ProjectCreateSchema), async (c) => {
|
|
346
|
+
const data = c.req.valid("json");
|
|
347
|
+
return c.json(await repo.createProject(data), 201);
|
|
348
|
+
});
|
|
349
|
+
app.put("/api/projects/:id", zValidator("json", ProjectUpdateSchema), async (c) => {
|
|
350
|
+
const id = c.req.param("id");
|
|
351
|
+
const data = c.req.valid("json");
|
|
352
|
+
const existing = await repo.getById(id);
|
|
353
|
+
if (!existing) throw new NotFoundError2("project", id);
|
|
354
|
+
if (data.status === "archived" && existing.key === DEFAULT_PROJECT_KEY) {
|
|
355
|
+
throw new ConflictError("project", id, {
|
|
356
|
+
bizCode: "DEFAULT_PROJECT_IMMUTABLE",
|
|
357
|
+
message: BIZ_CODE_MESSAGES.DEFAULT_PROJECT_IMMUTABLE
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (data.name && data.name !== existing.name) {
|
|
361
|
+
const dup = await repo._collection.findOne({ name: data.name }, { projection: { _id: 1 } });
|
|
362
|
+
if (dup) throw new ConflictError("project", `name conflict: ${data.name}`);
|
|
363
|
+
}
|
|
364
|
+
const patch = {};
|
|
365
|
+
if (data.name !== void 0) patch.name = data.name;
|
|
366
|
+
if (data.description !== void 0) patch.description = data.description;
|
|
367
|
+
if (data.status !== void 0) patch.status = data.status;
|
|
368
|
+
const project = await repo.update(id, patch);
|
|
369
|
+
if (!project) throw new NotFoundError2("project", id);
|
|
370
|
+
return c.json(project);
|
|
371
|
+
});
|
|
372
|
+
return app;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/routes/skill.routes.ts
|
|
376
|
+
import { Hono as Hono4 } from "hono";
|
|
377
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
378
|
+
import { z } from "zod";
|
|
379
|
+
import {
|
|
380
|
+
createSkillRepo,
|
|
381
|
+
createProjectRepo as createProjectRepo2,
|
|
382
|
+
createEnumRegistryRepo,
|
|
383
|
+
SkillCreateSchema,
|
|
384
|
+
SkillUpdateSchema,
|
|
385
|
+
SkillCopySchema,
|
|
386
|
+
AssetSetEnabledSchema,
|
|
387
|
+
OBJECT_ID_HEX,
|
|
388
|
+
assertReferencesDeletable,
|
|
389
|
+
assertReferenceAggregateLimit,
|
|
390
|
+
NotFoundError as NotFoundError3,
|
|
391
|
+
ConflictError as ConflictError2,
|
|
392
|
+
BadRequestError,
|
|
393
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES2
|
|
394
|
+
} from "@siming-org/core";
|
|
395
|
+
var SkillListQuerySchema = z.object({
|
|
396
|
+
projectId: z.string().optional(),
|
|
397
|
+
scope: z.enum(["global", "project"]).optional(),
|
|
398
|
+
q: z.string().trim().min(1).max(100).optional(),
|
|
399
|
+
includeDisabled: z.stringbool().optional()
|
|
400
|
+
}).refine((q) => !(q.scope === "project" && !q.projectId), {
|
|
401
|
+
message: "scope=project requires projectId",
|
|
402
|
+
path: ["projectId"]
|
|
403
|
+
});
|
|
404
|
+
var SkillByNameQuerySchema = z.object({
|
|
405
|
+
scope: z.enum(["global", "project"]).optional(),
|
|
406
|
+
projectId: z.string().regex(OBJECT_ID_HEX).optional(),
|
|
407
|
+
includeDisabled: z.stringbool().optional()
|
|
408
|
+
}).superRefine((q, ctx) => {
|
|
409
|
+
if (q.scope === "project" && !q.projectId) {
|
|
410
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "scope=project requires projectId", path: ["projectId"] });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
function skillToListItem(skill) {
|
|
414
|
+
const { references, ...rest } = skill;
|
|
415
|
+
return { ...rest, referenceCount: references?.length ?? 0 };
|
|
416
|
+
}
|
|
417
|
+
function createSkillRoutes(client) {
|
|
418
|
+
const app = new Hono4();
|
|
419
|
+
const repo = createSkillRepo(client.db());
|
|
420
|
+
const projectRepo = createProjectRepo2(client.db());
|
|
421
|
+
const enumRegistryRepo = createEnumRegistryRepo(client.db());
|
|
422
|
+
const assertCategoryValid = async (category) => {
|
|
423
|
+
const categories = await enumRegistryRepo.getEntries("skill_category");
|
|
424
|
+
if (!categories.some((e) => e.active && e.value === category)) {
|
|
425
|
+
const validValues = categories.filter((e) => e.active).map((e) => e.value).join(", ");
|
|
426
|
+
throw new BadRequestError(
|
|
427
|
+
`category '${category}' is not a valid active skill_category value. Valid values: ${validValues}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
app.get("/api/skills", zValidator2("query", SkillListQuerySchema), async (c) => {
|
|
432
|
+
const q = c.req.valid("query");
|
|
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
|
+
}
|
|
438
|
+
if (q.scope) {
|
|
439
|
+
return c.json((await repo.listSkillsByScopeFilter(q.scope, q.projectId, opts)).map(skillToListItem));
|
|
440
|
+
}
|
|
441
|
+
return c.json(
|
|
442
|
+
(q.projectId ? await repo.listSkillsByScope(q.projectId, opts) : await repo.listSkills(opts)).map(skillToListItem)
|
|
443
|
+
);
|
|
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
|
+
});
|
|
450
|
+
app.get("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), async (c) => {
|
|
451
|
+
const name = c.req.param("name");
|
|
452
|
+
const q = c.req.valid("query");
|
|
453
|
+
const skill = await resolveAssetByScope(repo, name, q, { includeDisabled: q.includeDisabled });
|
|
454
|
+
if (!skill) throw new NotFoundError3("skill", name);
|
|
455
|
+
assertProjectVisible(c, skill.projectId);
|
|
456
|
+
return c.json(skill);
|
|
457
|
+
});
|
|
458
|
+
app.post("/api/skills", zValidator2("json", SkillCreateSchema), async (c) => {
|
|
459
|
+
const data = c.req.valid("json");
|
|
460
|
+
await assertCategoryValid(data.category);
|
|
461
|
+
if (data.scope === "project") {
|
|
462
|
+
assertProjectScope(c, data.projectId);
|
|
463
|
+
await loadProjectForWrite(projectRepo, data.projectId);
|
|
464
|
+
} else {
|
|
465
|
+
requireAdminLike(c);
|
|
466
|
+
}
|
|
467
|
+
return c.json(await repo.createSkill(data), 201);
|
|
468
|
+
});
|
|
469
|
+
app.put("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", SkillUpdateSchema), async (c) => {
|
|
470
|
+
const name = c.req.param("name");
|
|
471
|
+
const data = c.req.valid("json");
|
|
472
|
+
const q = c.req.valid("query");
|
|
473
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
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
|
+
}
|
|
480
|
+
if (data.category !== void 0) await assertCategoryValid(data.category);
|
|
481
|
+
if (isScopeMutation(existing, data)) {
|
|
482
|
+
throw new ConflictError2("skill", name, {
|
|
483
|
+
bizCode: "SCOPE_IMMUTABLE",
|
|
484
|
+
message: BIZ_CODE_MESSAGES2.SCOPE_IMMUTABLE
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const effectiveProjectId = data.projectId ?? existing.projectId;
|
|
488
|
+
if ((data.scope ?? existing.scope) === "project" && effectiveProjectId) {
|
|
489
|
+
await loadProjectForWrite(projectRepo, effectiveProjectId);
|
|
490
|
+
}
|
|
491
|
+
assertReferencesDeletable({
|
|
492
|
+
resource: "skill",
|
|
493
|
+
name,
|
|
494
|
+
existingReferences: existing.references,
|
|
495
|
+
nextReferences: data.references,
|
|
496
|
+
nextMainBody: data.content,
|
|
497
|
+
currentMainBody: existing.content
|
|
498
|
+
});
|
|
499
|
+
assertReferenceAggregateLimit({
|
|
500
|
+
effectiveMainBody: data.content ?? existing.content,
|
|
501
|
+
nextReferences: data.references
|
|
502
|
+
});
|
|
503
|
+
const skill = await repo.updateByNameScoped(name, q.scope ?? "global", q.projectId, data);
|
|
504
|
+
if (!skill) throw new NotFoundError3("skill", name);
|
|
505
|
+
return c.json(skill);
|
|
506
|
+
});
|
|
507
|
+
app.post("/api/skills/:name/copy", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", SkillCopySchema), async (c) => {
|
|
508
|
+
const name = c.req.param("name");
|
|
509
|
+
const input = c.req.valid("json");
|
|
510
|
+
const q = c.req.valid("query");
|
|
511
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
512
|
+
if (!existing) throw new NotFoundError3("skill", name);
|
|
513
|
+
assertProjectVisible(c, existing.projectId);
|
|
514
|
+
if (input.newScope === "project") {
|
|
515
|
+
assertProjectScope(c, input.targetProjectId);
|
|
516
|
+
await loadProjectForWrite(projectRepo, input.targetProjectId);
|
|
517
|
+
} else {
|
|
518
|
+
requireAdminLike(c);
|
|
519
|
+
}
|
|
520
|
+
const copy = await repo.copySkill(
|
|
521
|
+
name,
|
|
522
|
+
{ scope: q.scope ?? "global", ...q.projectId ? { projectId: q.projectId } : {} },
|
|
523
|
+
{ scope: input.newScope, ...input.targetProjectId ? { projectId: input.targetProjectId } : {} },
|
|
524
|
+
input.newName
|
|
525
|
+
);
|
|
526
|
+
return c.json(copy, 201);
|
|
527
|
+
});
|
|
528
|
+
app.delete("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), async (c) => {
|
|
529
|
+
const name = c.req.param("name");
|
|
530
|
+
const q = c.req.valid("query");
|
|
531
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
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
|
+
}
|
|
538
|
+
const deleted = await repo.deleteByNameScoped(name, q.scope ?? "global", q.projectId);
|
|
539
|
+
if (!deleted) throw new NotFoundError3("skill", name);
|
|
540
|
+
return c.body(null, 204);
|
|
541
|
+
});
|
|
542
|
+
app.post("/api/skills/:name/enabled", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", AssetSetEnabledSchema), async (c) => {
|
|
543
|
+
const name = c.req.param("name");
|
|
544
|
+
const body = c.req.valid("json");
|
|
545
|
+
const q = c.req.valid("query");
|
|
546
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
547
|
+
if (!existing) throw new NotFoundError3("skill", name);
|
|
548
|
+
const scope = q.scope ?? "global";
|
|
549
|
+
if (scope === "global") {
|
|
550
|
+
requireAdminLike(c);
|
|
551
|
+
} else {
|
|
552
|
+
assertProjectScope(c, existing.projectId ?? "");
|
|
553
|
+
await loadProjectForWrite(projectRepo, existing.projectId ?? q.projectId);
|
|
554
|
+
}
|
|
555
|
+
const skill = await repo.updateByNameScoped(name, scope, q.projectId, { enabled: body.enabled });
|
|
556
|
+
if (!skill) throw new NotFoundError3("skill", name);
|
|
557
|
+
return c.json({ name: skill.name, enabled: skill.enabled ?? true });
|
|
558
|
+
});
|
|
559
|
+
return app;
|
|
560
|
+
}
|
|
561
|
+
function isScopeMutation(existing, patch) {
|
|
562
|
+
if (patch.scope !== void 0 && patch.scope !== existing.scope) return true;
|
|
563
|
+
if (patch.projectId !== void 0 && patch.projectId !== existing.projectId) return true;
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
async function resolveAssetByScope(repo, name, q, opts) {
|
|
567
|
+
return repo.getByNameScoped(name, q.scope ?? "global", q.projectId, opts);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/routes/agent.routes.ts
|
|
571
|
+
import { Hono as Hono5 } from "hono";
|
|
572
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
573
|
+
import { z as z2 } from "zod";
|
|
574
|
+
import {
|
|
575
|
+
bumpPatch,
|
|
576
|
+
createAgentRepo,
|
|
577
|
+
createSkillRepo as createSkillRepo2,
|
|
578
|
+
createProjectRepo as createProjectRepo3,
|
|
579
|
+
createModelAliasRepo,
|
|
580
|
+
AgentCreateSchema,
|
|
581
|
+
AgentUpdateSchema,
|
|
582
|
+
AgentCopySchema,
|
|
583
|
+
AssetSetEnabledSchema as AssetSetEnabledSchema2,
|
|
584
|
+
assertReferencesDeletable as assertReferencesDeletable2,
|
|
585
|
+
assertReferenceAggregateLimit as assertReferenceAggregateLimit2,
|
|
586
|
+
NotFoundError as NotFoundError4,
|
|
587
|
+
ConflictError as ConflictError3,
|
|
588
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES3,
|
|
589
|
+
assertBoundSkillsCompatible,
|
|
590
|
+
assertModelAliasExists
|
|
591
|
+
} from "@siming-org/core";
|
|
592
|
+
function agentToListItem(agent) {
|
|
593
|
+
const { references, ...rest } = agent;
|
|
594
|
+
return { ...rest, referenceCount: references?.length ?? 0 };
|
|
595
|
+
}
|
|
596
|
+
var AgentListQuerySchema = z2.object({
|
|
597
|
+
projectId: z2.string().optional(),
|
|
598
|
+
scope: z2.enum(["global", "project"]).optional(),
|
|
599
|
+
q: z2.string().trim().min(1).max(100).optional(),
|
|
600
|
+
includeDisabled: z2.stringbool().optional()
|
|
601
|
+
}).refine((q) => !(q.scope === "project" && !q.projectId), {
|
|
602
|
+
message: "scope=project requires projectId",
|
|
603
|
+
path: ["projectId"]
|
|
604
|
+
});
|
|
605
|
+
function createAgentRoutes(client) {
|
|
606
|
+
const app = new Hono5();
|
|
607
|
+
const repo = createAgentRepo(client.db());
|
|
608
|
+
const skillRepo = createSkillRepo2(client.db());
|
|
609
|
+
const projectRepo = createProjectRepo3(client.db());
|
|
610
|
+
const aliasRepo = createModelAliasRepo(client.db());
|
|
611
|
+
app.get("/api/agents", zValidator3("query", AgentListQuerySchema), async (c) => {
|
|
612
|
+
const q = c.req.valid("query");
|
|
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
|
+
}
|
|
618
|
+
if (q.scope) {
|
|
619
|
+
return c.json((await repo.listAgentsByScopeFilter(q.scope, q.projectId, opts)).map(agentToListItem));
|
|
620
|
+
}
|
|
621
|
+
return c.json(
|
|
622
|
+
(q.projectId ? await repo.listAgentsByScope(q.projectId, opts) : await repo.listAgents(opts)).map(agentToListItem)
|
|
623
|
+
);
|
|
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
|
+
});
|
|
630
|
+
app.get("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), async (c) => {
|
|
631
|
+
const name = c.req.param("name");
|
|
632
|
+
const q = c.req.valid("query");
|
|
633
|
+
const agent = await resolveAssetByScope(repo, name, q, { includeDisabled: q.includeDisabled });
|
|
634
|
+
if (!agent) throw new NotFoundError4("agent", name);
|
|
635
|
+
assertProjectVisible(c, agent.projectId);
|
|
636
|
+
return c.json(agent);
|
|
637
|
+
});
|
|
638
|
+
app.post("/api/agents", zValidator3("json", AgentCreateSchema), async (c) => {
|
|
639
|
+
const data = c.req.valid("json");
|
|
640
|
+
if (data.scope === "project") {
|
|
641
|
+
assertProjectScope(c, data.projectId);
|
|
642
|
+
await loadProjectForWrite(projectRepo, data.projectId);
|
|
643
|
+
} else {
|
|
644
|
+
requireAdminLike(c);
|
|
645
|
+
}
|
|
646
|
+
await assertBoundSkillsCompatible(skillRepo, data.scope, data.projectId, data.boundSkills);
|
|
647
|
+
await assertModelAliasExists(aliasRepo, data.model);
|
|
648
|
+
return c.json(await repo.createAgent(data), 201);
|
|
649
|
+
});
|
|
650
|
+
app.put("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AgentUpdateSchema), async (c) => {
|
|
651
|
+
const name = c.req.param("name");
|
|
652
|
+
const data = c.req.valid("json");
|
|
653
|
+
const q = c.req.valid("query");
|
|
654
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
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
|
+
}
|
|
661
|
+
let payload = data;
|
|
662
|
+
if (data["function"] !== void 0 && data["function"] !== existing["function"]) {
|
|
663
|
+
payload = { ...data, version: bumpPatch(data.version ?? existing.version) };
|
|
664
|
+
}
|
|
665
|
+
if (isScopeMutation(existing, data)) {
|
|
666
|
+
throw new ConflictError3("agent", name, {
|
|
667
|
+
bizCode: "SCOPE_IMMUTABLE",
|
|
668
|
+
message: BIZ_CODE_MESSAGES3.SCOPE_IMMUTABLE
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const effectiveProjectId = data.projectId ?? existing.projectId;
|
|
672
|
+
if ((data.scope ?? existing.scope) === "project" && effectiveProjectId) {
|
|
673
|
+
await loadProjectForWrite(projectRepo, effectiveProjectId);
|
|
674
|
+
}
|
|
675
|
+
if (data.boundSkills !== void 0) {
|
|
676
|
+
await assertBoundSkillsCompatible(
|
|
677
|
+
skillRepo,
|
|
678
|
+
existing.scope,
|
|
679
|
+
effectiveProjectId,
|
|
680
|
+
data.boundSkills
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (data.model !== void 0) {
|
|
684
|
+
await assertModelAliasExists(aliasRepo, data.model);
|
|
685
|
+
}
|
|
686
|
+
assertReferencesDeletable2({
|
|
687
|
+
resource: "agent",
|
|
688
|
+
name,
|
|
689
|
+
existingReferences: existing.references,
|
|
690
|
+
nextReferences: data.references,
|
|
691
|
+
nextMainBody: data.systemPrompt,
|
|
692
|
+
currentMainBody: existing.systemPrompt
|
|
693
|
+
});
|
|
694
|
+
assertReferenceAggregateLimit2({
|
|
695
|
+
effectiveMainBody: data.systemPrompt ?? existing.systemPrompt,
|
|
696
|
+
nextReferences: data.references
|
|
697
|
+
});
|
|
698
|
+
const agent = await repo.updateByNameScoped(name, q.scope ?? "global", q.projectId, payload);
|
|
699
|
+
if (!agent) throw new NotFoundError4("agent", name);
|
|
700
|
+
return c.json(agent);
|
|
701
|
+
});
|
|
702
|
+
app.post("/api/agents/:name/copy", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AgentCopySchema), async (c) => {
|
|
703
|
+
const name = c.req.param("name");
|
|
704
|
+
const input = c.req.valid("json");
|
|
705
|
+
const q = c.req.valid("query");
|
|
706
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
707
|
+
if (!existing) throw new NotFoundError4("agent", name);
|
|
708
|
+
assertProjectVisible(c, existing.projectId);
|
|
709
|
+
if (input.newScope === "project") {
|
|
710
|
+
assertProjectScope(c, input.targetProjectId);
|
|
711
|
+
await loadProjectForWrite(projectRepo, input.targetProjectId);
|
|
712
|
+
} else {
|
|
713
|
+
requireAdminLike(c);
|
|
714
|
+
}
|
|
715
|
+
const copy = await repo.copyAgent(
|
|
716
|
+
name,
|
|
717
|
+
{ scope: q.scope ?? "global", ...q.projectId ? { projectId: q.projectId } : {} },
|
|
718
|
+
{ scope: input.newScope, ...input.targetProjectId ? { projectId: input.targetProjectId } : {} },
|
|
719
|
+
input.newName,
|
|
720
|
+
skillRepo
|
|
721
|
+
);
|
|
722
|
+
return c.json(copy, 201);
|
|
723
|
+
});
|
|
724
|
+
app.delete("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), async (c) => {
|
|
725
|
+
const name = c.req.param("name");
|
|
726
|
+
const q = c.req.valid("query");
|
|
727
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
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
|
+
}
|
|
734
|
+
const deleted = await repo.deleteByNameScoped(name, q.scope ?? "global", q.projectId);
|
|
735
|
+
if (!deleted) throw new NotFoundError4("agent", name);
|
|
736
|
+
return c.body(null, 204);
|
|
737
|
+
});
|
|
738
|
+
app.post("/api/agents/:name/enabled", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AssetSetEnabledSchema2), async (c) => {
|
|
739
|
+
const name = c.req.param("name");
|
|
740
|
+
const body = c.req.valid("json");
|
|
741
|
+
const q = c.req.valid("query");
|
|
742
|
+
const existing = await resolveAssetByScope(repo, name, q, { includeDisabled: true });
|
|
743
|
+
if (!existing) throw new NotFoundError4("agent", name);
|
|
744
|
+
const scope = q.scope ?? "global";
|
|
745
|
+
if (scope === "global") {
|
|
746
|
+
requireAdminLike(c);
|
|
747
|
+
} else {
|
|
748
|
+
assertProjectScope(c, existing.projectId ?? "");
|
|
749
|
+
await loadProjectForWrite(projectRepo, existing.projectId ?? q.projectId);
|
|
750
|
+
}
|
|
751
|
+
const agent = await repo.updateByNameScoped(name, scope, q.projectId, { enabled: body.enabled });
|
|
752
|
+
if (!agent) throw new NotFoundError4("agent", name);
|
|
753
|
+
return c.json({ name: agent.name, enabled: agent.enabled ?? true });
|
|
754
|
+
});
|
|
755
|
+
return app;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/routes/model-alias.routes.ts
|
|
759
|
+
import { Hono as Hono6 } from "hono";
|
|
760
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
761
|
+
import {
|
|
762
|
+
createModelAliasRepo as createModelAliasRepo2,
|
|
763
|
+
ModelAliasCreateSchema,
|
|
764
|
+
ModelAliasUpdateSchema,
|
|
765
|
+
NotFoundError as NotFoundError5,
|
|
766
|
+
ConflictError as ConflictError4,
|
|
767
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES4
|
|
768
|
+
} from "@siming-org/core";
|
|
769
|
+
function createModelAliasRoutes(client) {
|
|
770
|
+
const app = new Hono6();
|
|
771
|
+
const repo = createModelAliasRepo2(client.db());
|
|
772
|
+
app.get("/api/model-aliases", async (c) => {
|
|
773
|
+
return c.json(await repo.listWithRefCount());
|
|
774
|
+
});
|
|
775
|
+
app.post("/api/model-aliases", zValidator4("json", ModelAliasCreateSchema), async (c) => {
|
|
776
|
+
const data = c.req.valid("json");
|
|
777
|
+
const dup = await repo.getByCode(data.code);
|
|
778
|
+
if (dup) {
|
|
779
|
+
throw new ConflictError4("model-alias", data.code, {
|
|
780
|
+
bizCode: "MODEL_CODE_EXISTS",
|
|
781
|
+
message: BIZ_CODE_MESSAGES4.MODEL_CODE_EXISTS
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return c.json(await repo.createModelAlias(data), 201);
|
|
785
|
+
});
|
|
786
|
+
app.put("/api/model-aliases/:code", zValidator4("json", ModelAliasUpdateSchema), async (c) => {
|
|
787
|
+
const code = c.req.param("code");
|
|
788
|
+
const data = c.req.valid("json");
|
|
789
|
+
const existing = await repo.getByCode(code);
|
|
790
|
+
if (!existing) throw new NotFoundError5("model-alias", code);
|
|
791
|
+
if (data.code !== void 0 && data.code !== code) {
|
|
792
|
+
throw new ConflictError4("model-alias", code, {
|
|
793
|
+
bizCode: "MODEL_CODE_IMMUTABLE",
|
|
794
|
+
message: BIZ_CODE_MESSAGES4.MODEL_CODE_IMMUTABLE
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
const patch = {};
|
|
798
|
+
if (data.name !== void 0) patch.name = data.name;
|
|
799
|
+
if (data.realModel !== void 0) patch.realModel = data.realModel;
|
|
800
|
+
const alias = await repo.updateByCode(code, patch);
|
|
801
|
+
if (!alias) throw new NotFoundError5("model-alias", code);
|
|
802
|
+
return c.json(alias);
|
|
803
|
+
});
|
|
804
|
+
app.delete("/api/model-aliases/:code", async (c) => {
|
|
805
|
+
const code = c.req.param("code");
|
|
806
|
+
const deleted = await repo.deleteByCode(code);
|
|
807
|
+
if (!deleted) throw new NotFoundError5("model-alias", code);
|
|
808
|
+
return c.body(null, 204);
|
|
809
|
+
});
|
|
810
|
+
return app;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/routes/dag-template.routes.ts
|
|
814
|
+
import { Hono as Hono7 } from "hono";
|
|
815
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
816
|
+
import { z as z3 } from "zod";
|
|
817
|
+
import {
|
|
818
|
+
createDagTemplateRepo,
|
|
819
|
+
createProjectRepo as createProjectRepo4,
|
|
820
|
+
createSkillRepo as createSkillRepo3,
|
|
821
|
+
createAgentRepo as createAgentRepo2,
|
|
822
|
+
createModelAliasRepo as createModelAliasRepo3,
|
|
823
|
+
createNodePresetRepo,
|
|
824
|
+
DagTemplateCreateSchema,
|
|
825
|
+
DagTemplateCopySchema,
|
|
826
|
+
DagTemplateUpdateSchema,
|
|
827
|
+
AssetSetEnabledSchema as AssetSetEnabledSchema3,
|
|
828
|
+
ImportPlanRequestSchema,
|
|
829
|
+
ImportApplyRequestSchema,
|
|
830
|
+
UpgradeApplyRequestSchema,
|
|
831
|
+
assertUniqueNodeIds,
|
|
832
|
+
buildUpgradePlan,
|
|
833
|
+
applyUpgradeDecisions,
|
|
834
|
+
composeExportBundle,
|
|
835
|
+
buildImportPlan,
|
|
836
|
+
applyImport,
|
|
837
|
+
NotFoundError as NotFoundError7,
|
|
838
|
+
ConflictError as ConflictError5,
|
|
839
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES5
|
|
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
|
|
859
|
+
var DagTemplateListQuerySchema = z3.object({
|
|
860
|
+
projectId: z3.string().optional(),
|
|
861
|
+
name: z3.string().optional(),
|
|
862
|
+
code: z3.string().optional(),
|
|
863
|
+
includeDisabled: z3.stringbool().optional()
|
|
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";
|
|
866
|
+
function createDagTemplateRoutes(client) {
|
|
867
|
+
const app = new Hono7();
|
|
868
|
+
const repo = createDagTemplateRepo(client.db());
|
|
869
|
+
const projectRepo = createProjectRepo4(client.db());
|
|
870
|
+
const nodePresetRepo = createNodePresetRepo(client.db());
|
|
871
|
+
const transferDeps = {
|
|
872
|
+
templateRepo: repo,
|
|
873
|
+
skillRepo: createSkillRepo3(client.db()),
|
|
874
|
+
agentRepo: createAgentRepo2(client.db()),
|
|
875
|
+
modelAliasRepo: createModelAliasRepo3(client.db()),
|
|
876
|
+
nodePresetRepo
|
|
877
|
+
};
|
|
878
|
+
app.get("/api/dag/templates", zValidator5("query", DagTemplateListQuerySchema), async (c) => {
|
|
879
|
+
const q = c.req.valid("query");
|
|
880
|
+
const projectFilter = resolveProjectFilter(c, q.projectId);
|
|
881
|
+
return c.json(
|
|
882
|
+
await repo.listDagTemplates({
|
|
883
|
+
...projectFilter ? { projectId: projectFilter } : {},
|
|
884
|
+
...q.name ? { name: q.name } : {},
|
|
885
|
+
...q.code ? { code: q.code } : {},
|
|
886
|
+
...q.includeDisabled !== void 0 ? { includeDisabled: q.includeDisabled } : {}
|
|
887
|
+
})
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
app.get("/api/dag/templates/:id", async (c) => {
|
|
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);
|
|
894
|
+
return c.json(template);
|
|
895
|
+
});
|
|
896
|
+
app.post("/api/dag/templates/:id/enabled", zValidator5("json", AssetSetEnabledSchema3), async (c) => {
|
|
897
|
+
const ref = c.req.param("id");
|
|
898
|
+
const body = c.req.valid("json");
|
|
899
|
+
const existing = await resolveTemplateOr404(repo, ref, resolveProjectFilter(c, c.req.query("projectId")), QUERY_PROJECT_HINT);
|
|
900
|
+
assertProjectScope(c, existing.projectId);
|
|
901
|
+
await loadProjectForWrite(projectRepo, existing.projectId);
|
|
902
|
+
const template = await repo.update(existing.id, { enabled: body.enabled });
|
|
903
|
+
if (!template) throw new NotFoundError7("dag-template", existing.id);
|
|
904
|
+
return c.json({ id: template.id, name: template.name, enabled: template.enabled ?? true });
|
|
905
|
+
});
|
|
906
|
+
app.post("/api/dag/templates", zValidator5("json", DagTemplateCreateSchema), async (c) => {
|
|
907
|
+
const data = c.req.valid("json");
|
|
908
|
+
assertProjectScope(c, data.projectId);
|
|
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
|
+
}
|
|
923
|
+
return c.json(await repo.createDagTemplate(data), 201);
|
|
924
|
+
});
|
|
925
|
+
app.put("/api/dag/templates/:id", zValidator5("json", DagTemplateUpdateSchema), async (c) => {
|
|
926
|
+
const ref = c.req.param("id");
|
|
927
|
+
const data = c.req.valid("json");
|
|
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, {
|
|
939
|
+
bizCode: "TEMPLATE_PROJECT_IMMUTABLE",
|
|
940
|
+
message: BIZ_CODE_MESSAGES5.TEMPLATE_PROJECT_IMMUTABLE
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
await loadProjectForWrite(projectRepo, existing.projectId);
|
|
944
|
+
if (data.nodes !== void 0) {
|
|
945
|
+
assertUniqueNodeIds(data.nodes);
|
|
946
|
+
}
|
|
947
|
+
if (writable.name !== void 0 && writable.name !== existing.name) {
|
|
948
|
+
const dup = await repo._collection.findOne(
|
|
949
|
+
{ projectId: existing.projectId, name: writable.name },
|
|
950
|
+
{ projection: { _id: 1 } }
|
|
951
|
+
);
|
|
952
|
+
if (dup) {
|
|
953
|
+
throw new ConflictError5("dag-template", `name conflict: ${writable.name}`, {
|
|
954
|
+
bizCode: "TEMPLATE_NAME_CONFLICT",
|
|
955
|
+
message: BIZ_CODE_MESSAGES5.TEMPLATE_NAME_CONFLICT
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const template = await repo.update(existing.id, writable);
|
|
960
|
+
if (!template) throw new NotFoundError7("dag-template", existing.id);
|
|
961
|
+
return c.json(template);
|
|
962
|
+
});
|
|
963
|
+
app.post("/api/dag/templates/:id/copy", zValidator5("json", DagTemplateCopySchema), async (c) => {
|
|
964
|
+
const ref = c.req.param("id");
|
|
965
|
+
const input = c.req.valid("json");
|
|
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);
|
|
969
|
+
await loadProjectForWrite(projectRepo, input.targetProjectId);
|
|
970
|
+
const dup = await repo._collection.findOne(
|
|
971
|
+
{ projectId: input.targetProjectId, name: input.newName },
|
|
972
|
+
{ projection: { _id: 1 } }
|
|
973
|
+
);
|
|
974
|
+
if (dup) {
|
|
975
|
+
throw new ConflictError5("dag-template", `name conflict: ${input.newName}`, {
|
|
976
|
+
bizCode: "TEMPLATE_NAME_CONFLICT",
|
|
977
|
+
message: BIZ_CODE_MESSAGES5.TEMPLATE_NAME_CONFLICT
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const copy = await repo.copyDagTemplate(existing.id, input.targetProjectId, input.newName, input.newCode);
|
|
981
|
+
return c.json(copy, 201);
|
|
982
|
+
});
|
|
983
|
+
app.get("/api/dag/templates/:id/export", async (c) => {
|
|
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));
|
|
988
|
+
});
|
|
989
|
+
app.post("/api/dag/templates/import/plan", zValidator5("json", ImportPlanRequestSchema), async (c) => {
|
|
990
|
+
const body = c.req.valid("json");
|
|
991
|
+
assertProjectScope(c, body.targetProjectId);
|
|
992
|
+
await loadProjectForWrite(projectRepo, body.targetProjectId);
|
|
993
|
+
return c.json(await buildImportPlan(transferDeps, body.targetProjectId, body.bundle));
|
|
994
|
+
});
|
|
995
|
+
app.post("/api/dag/templates/import/apply", zValidator5("json", ImportApplyRequestSchema), async (c) => {
|
|
996
|
+
const body = c.req.valid("json");
|
|
997
|
+
assertProjectScope(c, body.targetProjectId);
|
|
998
|
+
await loadProjectForWrite(projectRepo, body.targetProjectId);
|
|
999
|
+
return c.json(await applyImport(transferDeps, body.targetProjectId, body.bundle, body.decisions));
|
|
1000
|
+
});
|
|
1001
|
+
app.get("/api/dag/templates/:id/upgrade/plan", async (c) => {
|
|
1002
|
+
const id = c.req.param("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);
|
|
1034
|
+
return c.body(null, 204);
|
|
1035
|
+
});
|
|
1036
|
+
return app;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/routes/node-preset.routes.ts
|
|
1040
|
+
import { Hono as Hono8 } from "hono";
|
|
1041
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
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";
|
|
1259
|
+
import {
|
|
1260
|
+
createTaskRepo,
|
|
1261
|
+
createDagTemplateRepo as createDagTemplateRepo2,
|
|
1262
|
+
createProjectRepo as createProjectRepo7,
|
|
1263
|
+
TaskCreateInputSchema,
|
|
1264
|
+
AdvanceRequestSchema,
|
|
1265
|
+
ApproveRequestSchema,
|
|
1266
|
+
PauseRequestSchema,
|
|
1267
|
+
ResumeRequestSchema,
|
|
1268
|
+
CancelRequestSchema,
|
|
1269
|
+
taskProgress,
|
|
1270
|
+
excerpt,
|
|
1271
|
+
ContextViewSchema,
|
|
1272
|
+
CONTEXT_VIEWS,
|
|
1273
|
+
ARTIFACT_TYPES,
|
|
1274
|
+
NODE_ID_PATTERN,
|
|
1275
|
+
createEnumRegistryRepo as createEnumRegistryRepo2,
|
|
1276
|
+
NotFoundError as NotFoundError9,
|
|
1277
|
+
ConflictError as ConflictError7,
|
|
1278
|
+
BadRequestError as BadRequestError2,
|
|
1279
|
+
ValidationError as ValidationError2,
|
|
1280
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES7,
|
|
1281
|
+
advanceTask,
|
|
1282
|
+
approveTask,
|
|
1283
|
+
pauseTask,
|
|
1284
|
+
resumeTask,
|
|
1285
|
+
cancelTask,
|
|
1286
|
+
renderPrompt,
|
|
1287
|
+
toNodeInfo,
|
|
1288
|
+
toTaskPublic,
|
|
1289
|
+
findNextEdge,
|
|
1290
|
+
generateEntryId
|
|
1291
|
+
} from "@siming-org/core";
|
|
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()
|
|
1300
|
+
});
|
|
1301
|
+
var TaskPatchSchema = z5.object({
|
|
1302
|
+
title: z5.string().min(1).optional()
|
|
1303
|
+
});
|
|
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()
|
|
1308
|
+
});
|
|
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(),
|
|
1317
|
+
/** 全文快照(CLI --file 读文件后传入;≤200k——续跑会话凭 context 自足) */
|
|
1318
|
+
content: z5.string().max(2e5).optional()
|
|
1319
|
+
});
|
|
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)
|
|
1324
|
+
});
|
|
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)
|
|
1329
|
+
});
|
|
1330
|
+
function normalizeTaskDoc(doc) {
|
|
1331
|
+
if (!doc) return null;
|
|
1332
|
+
return { ...doc, acceptance: doc.acceptance ?? [], nonGoals: doc.nonGoals ?? [] };
|
|
1333
|
+
}
|
|
1334
|
+
function normalizeNodeRecords(records) {
|
|
1335
|
+
const src = records ?? {};
|
|
1336
|
+
const out = {};
|
|
1337
|
+
for (const [nodeId, record] of Object.entries(src)) {
|
|
1338
|
+
if (!record) continue;
|
|
1339
|
+
out[nodeId] = {
|
|
1340
|
+
...record,
|
|
1341
|
+
checks: record.checks ?? [],
|
|
1342
|
+
artifacts: record.artifacts ?? [],
|
|
1343
|
+
confirmations: record.confirmations ?? [],
|
|
1344
|
+
decisions: record.decisions ?? []
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
return out;
|
|
1348
|
+
}
|
|
1349
|
+
function stripArtifactContent(records) {
|
|
1350
|
+
const out = {};
|
|
1351
|
+
for (const [nodeId, record] of Object.entries(records)) {
|
|
1352
|
+
out[nodeId] = {
|
|
1353
|
+
...record,
|
|
1354
|
+
artifacts: record.artifacts.map((a) => {
|
|
1355
|
+
if (a.content === void 0) return a;
|
|
1356
|
+
const { content: _stripped, ...rest } = a;
|
|
1357
|
+
return rest;
|
|
1358
|
+
})
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
return out;
|
|
1362
|
+
}
|
|
1363
|
+
function writeAck(entity, updated, entry, echo) {
|
|
1364
|
+
return {
|
|
1365
|
+
taskId: entity.taskId,
|
|
1366
|
+
updated,
|
|
1367
|
+
...entry !== void 0 ? { entry } : {},
|
|
1368
|
+
...echo !== void 0 ? { echo } : {},
|
|
1369
|
+
updatedAt: entity.updatedAt ?? /* @__PURE__ */ new Date(),
|
|
1370
|
+
task: { ...toTaskPublic(entity), projectId: entity.projectId, progress: taskProgress(entity) }
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function createTaskRoutes(client) {
|
|
1374
|
+
const app = new Hono10();
|
|
1375
|
+
const repo = createTaskRepo(client.db());
|
|
1376
|
+
const templateRepo = createDagTemplateRepo2(client.db());
|
|
1377
|
+
const projectRepo = createProjectRepo7(client.db());
|
|
1378
|
+
const enumRegistryRepo = createEnumRegistryRepo2(client.db());
|
|
1379
|
+
app.get("/api/tasks", zValidator8("query", TaskListQuerySchema), async (c) => {
|
|
1380
|
+
const q = c.req.valid("query");
|
|
1381
|
+
const { items, total } = await repo.listTasks({
|
|
1382
|
+
...q.status ? { status: q.status } : {},
|
|
1383
|
+
...q.track ? { track: q.track } : {},
|
|
1384
|
+
// T202608310001:project 上下文强制覆写 projectId 过滤("只能看到自己的")
|
|
1385
|
+
...resolveProjectFilter(c, q.projectId) ? { projectId: resolveProjectFilter(c, q.projectId) } : {},
|
|
1386
|
+
...q.q !== void 0 ? { q: q.q } : {},
|
|
1387
|
+
page: q.page,
|
|
1388
|
+
limit: q.limit,
|
|
1389
|
+
...q.sort ? { sort: q.sort } : {}
|
|
1390
|
+
});
|
|
1391
|
+
return c.json({ items, total, page: q.page, limit: q.limit });
|
|
1392
|
+
});
|
|
1393
|
+
app.get("/api/tasks/:taskId", async (c) => {
|
|
1394
|
+
const taskId = c.req.param("taskId");
|
|
1395
|
+
const task = await repo.getByTaskId(taskId);
|
|
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
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
app.post("/api/tasks", zValidator8("json", TaskCreateInputSchema), async (c) => {
|
|
1407
|
+
const input = c.req.valid("json");
|
|
1408
|
+
const dagTracks = (await enumRegistryRepo.getEntries("dag_track")).filter(
|
|
1409
|
+
(e) => e.active && e.value !== "all"
|
|
1410
|
+
);
|
|
1411
|
+
if (!dagTracks.some((e) => e.value === input.track)) {
|
|
1412
|
+
const validValues = dagTracks.map((e) => e.value).join(", ");
|
|
1413
|
+
throw new BadRequestError2(
|
|
1414
|
+
`track '${input.track}' is not a valid active dag_track value. Valid values: ${validValues}`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
assertTypeTrackCompatible(input.type, input.track);
|
|
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
|
+
);
|
|
1424
|
+
if (template.enabled === false) {
|
|
1425
|
+
throw new ConflictError7("task", input.dagTemplateId, {
|
|
1426
|
+
bizCode: "TEMPLATE_DISABLED",
|
|
1427
|
+
message: BIZ_CODE_MESSAGES7.TEMPLATE_DISABLED
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
if (input.projectId && input.projectId !== template.projectId) {
|
|
1431
|
+
throw new ConflictError7("task", input.dagTemplateId, {
|
|
1432
|
+
bizCode: "TEMPLATE_PROJECT_MISMATCH",
|
|
1433
|
+
message: BIZ_CODE_MESSAGES7.TEMPLATE_PROJECT_MISMATCH
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
const resolvedProjectId = input.projectId ?? template.projectId;
|
|
1437
|
+
assertProjectScope(c, resolvedProjectId);
|
|
1438
|
+
await loadProjectForWrite(projectRepo, resolvedProjectId);
|
|
1439
|
+
const created = await repo.createTask(input, template, resolvedProjectId);
|
|
1440
|
+
const firstActive = created.dagInstance.nodes.find(
|
|
1441
|
+
(n) => created.dagInstance.nodeStates[n.id]?.status === "active"
|
|
1442
|
+
);
|
|
1443
|
+
if (!firstActive) {
|
|
1444
|
+
throw new BadRequestError2(
|
|
1445
|
+
`task has no active node to start (all nodes pruned by skipNodes); \u6A21\u677F ${template.name} \u526A\u679D\u540E\u65E0\u53EF\u5F00\u5DE5\u8282\u70B9\uFF0C\u521B\u5EFA\u88AB\u62D2\u7EDD`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const response = {
|
|
1449
|
+
task: { ...toTaskPublic(created), projectId: created.projectId, progress: taskProgress(created) },
|
|
1450
|
+
firstNode: toNodeInfo(firstActive, created)
|
|
1451
|
+
};
|
|
1452
|
+
return c.json(response, 201);
|
|
1453
|
+
});
|
|
1454
|
+
app.patch("/api/tasks/:taskId", zValidator8("json", TaskPatchSchema), async (c) => {
|
|
1455
|
+
const taskId = c.req.param("taskId");
|
|
1456
|
+
const body = c.req.valid("json");
|
|
1457
|
+
if (body.title === void 0) {
|
|
1458
|
+
throw new BadRequestError2("PATCH /api/tasks/:taskId \u9700\u8981\u81F3\u5C11\u4E00\u4E2A\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08title\uFF09");
|
|
1459
|
+
}
|
|
1460
|
+
await assertTaskProjectActive(repo, projectRepo, taskId, c);
|
|
1461
|
+
const patch = { title: body.title };
|
|
1462
|
+
const updatedTask = await repo.updateTask(taskId, patch);
|
|
1463
|
+
return c.json(
|
|
1464
|
+
writeAck(updatedTask, ["title"], void 0, [{ path: "title", excerpt: excerpt(updatedTask.title) }])
|
|
1465
|
+
);
|
|
1466
|
+
});
|
|
1467
|
+
app.post("/api/tasks/:taskId/advance", zValidator8("json", AdvanceRequestSchema), async (c) => {
|
|
1468
|
+
const taskId = c.req.param("taskId");
|
|
1469
|
+
const body = c.req.valid("json");
|
|
1470
|
+
await assertTaskProjectActive(repo, projectRepo, taskId, c);
|
|
1471
|
+
const deps = mkDeps(repo);
|
|
1472
|
+
const result = await advanceTask(deps, taskId, {
|
|
1473
|
+
...body.note !== void 0 ? { note: body.note } : {},
|
|
1474
|
+
...body.summary !== void 0 ? { summary: body.summary } : {}
|
|
1475
|
+
});
|
|
1476
|
+
return c.json(result, 200);
|
|
1477
|
+
});
|
|
1478
|
+
app.post("/api/tasks/:taskId/approve", zValidator8("json", ApproveRequestSchema), async (c) => {
|
|
1479
|
+
const taskId = c.req.param("taskId");
|
|
1480
|
+
const body = c.req.valid("json");
|
|
1481
|
+
await assertTaskProjectActive(repo, projectRepo, taskId, c);
|
|
1482
|
+
const deps = mkDeps(repo);
|
|
1483
|
+
const result = await approveTask(deps, taskId, body);
|
|
1484
|
+
return c.json(result, 200);
|
|
1485
|
+
});
|
|
1486
|
+
app.post("/api/tasks/:taskId/pause", zValidator8("json", PauseRequestSchema), async (c) => {
|
|
1487
|
+
const taskId = c.req.param("taskId");
|
|
1488
|
+
const body = c.req.valid("json");
|
|
1489
|
+
await assertTaskProjectActive(repo, projectRepo, taskId, c);
|
|
1490
|
+
const result = await pauseTask(mkDeps(repo), taskId, body.reason);
|
|
1491
|
+
return c.json(writeAck(result, ["status", "pausedAt"], void 0, [{ path: "status", excerpt: result.status }]));
|
|
1492
|
+
});
|
|
1493
|
+
app.post("/api/tasks/:taskId/resume", zValidator8("json", ResumeRequestSchema), async (c) => {
|
|
1494
|
+
const taskId = c.req.param("taskId");
|
|
1495
|
+
const body = c.req.valid("json");
|
|
1496
|
+
await assertTaskProjectActive(repo, projectRepo, taskId, c);
|
|
1497
|
+
const result = await resumeTask(mkDeps(repo), taskId, body.decision);
|
|
1498
|
+
return c.json(writeAck(result, ["status"], void 0, [{ path: "status", excerpt: `${result.status}\uFF08\u5DF2\u6062\u590D\u624B\u52A8\u6682\u505C\uFF09` }]));
|
|
1499
|
+
});
|
|
1500
|
+
app.post("/api/tasks/:taskId/cancel", zValidator8("json", CancelRequestSchema), async (c) => {
|
|
1501
|
+
const taskId = c.req.param("taskId");
|
|
1502
|
+
const body = c.req.valid("json");
|
|
1503
|
+
await assertTaskProjectActive(repo, projectRepo, taskId, c);
|
|
1504
|
+
const result = await cancelTask(mkDeps(repo), taskId, body.reason);
|
|
1505
|
+
return c.json(
|
|
1506
|
+
writeAck(
|
|
1507
|
+
result,
|
|
1508
|
+
["status", "pausedAt"],
|
|
1509
|
+
void 0,
|
|
1510
|
+
[
|
|
1511
|
+
{ path: "status", excerpt: result.status },
|
|
1512
|
+
...body.reason !== void 0 ? [{ path: "history.reason", excerpt: excerpt(body.reason) }] : []
|
|
1513
|
+
]
|
|
1514
|
+
)
|
|
1515
|
+
);
|
|
1516
|
+
});
|
|
1517
|
+
app.get("/api/tasks/:taskId/history", async (c) => {
|
|
1518
|
+
const taskId = c.req.param("taskId");
|
|
1519
|
+
const task = await repo.getByTaskId(taskId);
|
|
1520
|
+
if (!task) throw new NotFoundError9("task", taskId);
|
|
1521
|
+
assertProjectScope(c, task.projectId);
|
|
1522
|
+
return c.json(task.history);
|
|
1523
|
+
});
|
|
1524
|
+
app.get("/api/tasks/:taskId/node/:nodeId", async (c) => {
|
|
1525
|
+
const taskId = c.req.param("taskId");
|
|
1526
|
+
const nodeId = c.req.param("nodeId");
|
|
1527
|
+
const task = await repo.getByTaskId(taskId);
|
|
1528
|
+
if (!task) throw new NotFoundError9("task", taskId);
|
|
1529
|
+
assertProjectScope(c, task.projectId);
|
|
1530
|
+
const node = task.dagInstance.nodes.find((n) => n.id === nodeId);
|
|
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
|
+
});
|
|
1538
|
+
});
|
|
1539
|
+
app.get("/api/tasks/:taskId/context", async (c) => {
|
|
1540
|
+
const taskId = c.req.param("taskId");
|
|
1541
|
+
const viewR = ContextViewSchema.safeParse(c.req.query("view") ?? "basic");
|
|
1542
|
+
if (!viewR.success) {
|
|
1543
|
+
throw new BadRequestError2(
|
|
1544
|
+
`context view '${c.req.query("view")}' is invalid. Valid values: ${CONTEXT_VIEWS.join(", ")}`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
const view = viewR.data;
|
|
1548
|
+
const task = await repo.getByTaskId(taskId);
|
|
1549
|
+
if (!task) throw new NotFoundError9("task", taskId);
|
|
1550
|
+
assertProjectScope(c, task.projectId);
|
|
1551
|
+
const currentNodeActive = task.status === "active" ? task.dagInstance.nodes.find((n) => n.id === task.currentNode) : void 0;
|
|
1552
|
+
let pausedAtEdge = null;
|
|
1553
|
+
if (task.status === "paused" && task.pausedAt !== null) {
|
|
1554
|
+
const edge = findNextEdge(task.dagInstance.edges, task.dagInstance.nodes, task.pausedAt, task.track);
|
|
1555
|
+
if (edge?.pausePoint) {
|
|
1556
|
+
pausedAtEdge = { from: edge.from, to: edge.to, pausePoint: edge.pausePoint };
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const context = {
|
|
1560
|
+
task: toTaskPublic(task),
|
|
1561
|
+
currentNode: currentNodeActive ? {
|
|
1562
|
+
...toNodeInfo(currentNodeActive, task),
|
|
1563
|
+
phase: currentNodeActive.phase
|
|
1564
|
+
} : null,
|
|
1565
|
+
pausedAtEdge,
|
|
1566
|
+
nodes: task.dagInstance.nodes.map((n) => {
|
|
1567
|
+
const state = task.dagInstance.nodeStates[n.id];
|
|
1568
|
+
return {
|
|
1569
|
+
nodeId: n.id,
|
|
1570
|
+
label: n.label,
|
|
1571
|
+
status: state?.status ?? "pending",
|
|
1572
|
+
enteredAt: state?.enteredAt ?? null,
|
|
1573
|
+
completedAt: state?.completedAt ?? null
|
|
1574
|
+
};
|
|
1575
|
+
}),
|
|
1576
|
+
// N020 D1:结构化全景(断点续跑数据源);?? 容错迁移前旧文档与小步写入的部分形态
|
|
1577
|
+
// (toEntity 不跑 parse 补 default——点路径 $set/$push 只写触及字段,record/doc 内数组
|
|
1578
|
+
// 可能缺键,读侧按字段归一到 schema 目标形状,Web/CLI 消费方拿到的恒为完整形)
|
|
1579
|
+
taskDoc: normalizeTaskDoc(task.doc),
|
|
1580
|
+
nodeRecords: view === "basic" ? stripArtifactContent(normalizeNodeRecords(task.nodeRecords)) : normalizeNodeRecords(task.nodeRecords),
|
|
1581
|
+
archNotes: task.archNotes ?? []
|
|
1582
|
+
};
|
|
1583
|
+
return c.json(context);
|
|
1584
|
+
});
|
|
1585
|
+
app.patch("/api/tasks/:taskId/doc", zValidator8("json", TaskDocSetSchema), async (c) => {
|
|
1586
|
+
const taskId = c.req.param("taskId");
|
|
1587
|
+
const body = c.req.valid("json");
|
|
1588
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
|
|
1589
|
+
const sets = {};
|
|
1590
|
+
if (body.what !== void 0) sets["doc.what"] = body.what;
|
|
1591
|
+
if (body.why !== void 0) sets["doc.why"] = body.why;
|
|
1592
|
+
if (body.trackNote !== void 0) sets["doc.trackNote"] = body.trackNote;
|
|
1593
|
+
const updatedDoc = await repo.updateTaskPaths(taskId, { sets });
|
|
1594
|
+
return c.json(
|
|
1595
|
+
writeAck(
|
|
1596
|
+
updatedDoc,
|
|
1597
|
+
Object.keys(sets),
|
|
1598
|
+
void 0,
|
|
1599
|
+
Object.entries(sets).map(([path, value]) => ({ path, excerpt: excerpt(String(value)) }))
|
|
1600
|
+
)
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
app.post("/api/tasks/:taskId/doc/acceptance", zValidator8("json", TextItemSchema), async (c) => {
|
|
1604
|
+
const taskId = c.req.param("taskId");
|
|
1605
|
+
const body = c.req.valid("json");
|
|
1606
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
|
|
1607
|
+
const updatedAcc = await repo.updateTaskPaths(taskId, { pushes: { "doc.acceptance": body.text } });
|
|
1608
|
+
return c.json(
|
|
1609
|
+
writeAck(updatedAcc, ["doc.acceptance"], void 0, [
|
|
1610
|
+
{ path: "doc.acceptance", excerpt: excerpt(body.text) }
|
|
1611
|
+
])
|
|
1612
|
+
);
|
|
1613
|
+
});
|
|
1614
|
+
app.post("/api/tasks/:taskId/doc/non-goal", zValidator8("json", TextItemSchema), async (c) => {
|
|
1615
|
+
const taskId = c.req.param("taskId");
|
|
1616
|
+
const body = c.req.valid("json");
|
|
1617
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
|
|
1618
|
+
const updatedNg = await repo.updateTaskPaths(taskId, { pushes: { "doc.nonGoals": body.text } });
|
|
1619
|
+
return c.json(
|
|
1620
|
+
writeAck(updatedNg, ["doc.nonGoals"], void 0, [{ path: "doc.nonGoals", excerpt: excerpt(body.text) }])
|
|
1621
|
+
);
|
|
1622
|
+
});
|
|
1623
|
+
app.patch("/api/tasks/:taskId/records/:nodeId/summary", zValidator8("json", RecordSummarySchema), async (c) => {
|
|
1624
|
+
const taskId = c.req.param("taskId");
|
|
1625
|
+
const nodeId = c.req.param("nodeId");
|
|
1626
|
+
const body = c.req.valid("json");
|
|
1627
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1628
|
+
const updatedSummary = await repo.updateTaskPaths(taskId, {
|
|
1629
|
+
sets: { [`nodeRecords.${nodeId}.summary`]: body.summary }
|
|
1630
|
+
});
|
|
1631
|
+
return c.json(
|
|
1632
|
+
writeAck(updatedSummary, [`nodeRecords.${nodeId}.summary`], void 0, [
|
|
1633
|
+
{ path: `nodeRecords.${nodeId}.summary`, excerpt: excerpt(body.summary) }
|
|
1634
|
+
])
|
|
1635
|
+
);
|
|
1636
|
+
});
|
|
1637
|
+
app.post("/api/tasks/:taskId/records/:nodeId/checks", zValidator8("json", CheckAddSchema), async (c) => {
|
|
1638
|
+
const taskId = c.req.param("taskId");
|
|
1639
|
+
const nodeId = c.req.param("nodeId");
|
|
1640
|
+
const body = c.req.valid("json");
|
|
1641
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1642
|
+
const existing = (task.nodeRecords ?? {})[nodeId]?.checks ?? [];
|
|
1643
|
+
const check = {
|
|
1644
|
+
id: generateEntryId(existing.map((ch) => ch.id)),
|
|
1645
|
+
item: body.item,
|
|
1646
|
+
passed: body.passed ?? true
|
|
1647
|
+
};
|
|
1648
|
+
const updatedCheck = await repo.updateTaskPaths(taskId, {
|
|
1649
|
+
pushes: { [`nodeRecords.${nodeId}.checks`]: check }
|
|
1650
|
+
});
|
|
1651
|
+
return c.json(
|
|
1652
|
+
writeAck(
|
|
1653
|
+
updatedCheck,
|
|
1654
|
+
[`nodeRecords.${nodeId}.checks`],
|
|
1655
|
+
{ id: check.id, kind: "check" },
|
|
1656
|
+
[{ path: `nodeRecords.${nodeId}.checks`, excerpt: excerpt(`${check.item}\uFF08passed=${String(check.passed)}\uFF09`) }]
|
|
1657
|
+
)
|
|
1658
|
+
);
|
|
1659
|
+
});
|
|
1660
|
+
app.patch("/api/tasks/:taskId/records/:nodeId/checks/:checkId", zValidator8("json", CheckPatchSchema), async (c) => {
|
|
1661
|
+
const taskId = c.req.param("taskId");
|
|
1662
|
+
const nodeId = c.req.param("nodeId");
|
|
1663
|
+
const checkId = c.req.param("checkId");
|
|
1664
|
+
const body = c.req.valid("json");
|
|
1665
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1666
|
+
const check = ((task.nodeRecords ?? {})[nodeId]?.checks ?? []).find((ch) => ch.id === checkId);
|
|
1667
|
+
if (!check) {
|
|
1668
|
+
throw new NotFoundError9("check", checkId, {
|
|
1669
|
+
bizCode: "CHECK_NOT_FOUND",
|
|
1670
|
+
message: `${BIZ_CODE_MESSAGES7.CHECK_NOT_FOUND}\uFF08node ${nodeId}, check ${checkId}\uFF09`
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
const updatedFlip = await repo.updateTaskPaths(taskId, {
|
|
1674
|
+
sets: { [`nodeRecords.${nodeId}.checks.$[e].passed`]: body.passed },
|
|
1675
|
+
arrayFilters: [{ "e.id": checkId }]
|
|
1676
|
+
});
|
|
1677
|
+
return c.json(
|
|
1678
|
+
writeAck(updatedFlip, [`nodeRecords.${nodeId}.checks.${checkId}.passed`], void 0, [
|
|
1679
|
+
{ path: `nodeRecords.${nodeId}.checks.${checkId}.passed`, excerpt: String(body.passed) }
|
|
1680
|
+
])
|
|
1681
|
+
);
|
|
1682
|
+
});
|
|
1683
|
+
app.post("/api/tasks/:taskId/records/:nodeId/artifacts", zValidator8("json", ArtifactAddSchema), async (c) => {
|
|
1684
|
+
const taskId = c.req.param("taskId");
|
|
1685
|
+
const nodeId = c.req.param("nodeId");
|
|
1686
|
+
const body = c.req.valid("json");
|
|
1687
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1688
|
+
const artifact = {
|
|
1689
|
+
id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.artifacts ?? []).map((a) => a.id)),
|
|
1690
|
+
type: body.type,
|
|
1691
|
+
path: body.path,
|
|
1692
|
+
...body.note !== void 0 ? { note: body.note } : {},
|
|
1693
|
+
...body.content !== void 0 ? { content: body.content } : {}
|
|
1694
|
+
};
|
|
1695
|
+
const updatedArtifact = await repo.updateTaskPaths(taskId, {
|
|
1696
|
+
pushes: { [`nodeRecords.${nodeId}.artifacts`]: artifact }
|
|
1697
|
+
});
|
|
1698
|
+
return c.json(
|
|
1699
|
+
writeAck(
|
|
1700
|
+
updatedArtifact,
|
|
1701
|
+
[`nodeRecords.${nodeId}.artifacts`],
|
|
1702
|
+
{ id: artifact.id, kind: "artifact" },
|
|
1703
|
+
[
|
|
1704
|
+
{
|
|
1705
|
+
path: `nodeRecords.${nodeId}.artifacts`,
|
|
1706
|
+
excerpt: excerpt(
|
|
1707
|
+
`${artifact.type} ${artifact.path}${body.content !== void 0 ? `\uFF08\u542B\u5168\u6587\u5FEB\u7167 ${body.content.length} \u5B57\u7B26\uFF09` : ""}`
|
|
1708
|
+
)
|
|
1709
|
+
}
|
|
1710
|
+
]
|
|
1711
|
+
)
|
|
1712
|
+
);
|
|
1713
|
+
});
|
|
1714
|
+
app.post("/api/tasks/:taskId/records/:nodeId/confirmations", zValidator8("json", ConfirmAddSchema), async (c) => {
|
|
1715
|
+
const taskId = c.req.param("taskId");
|
|
1716
|
+
const nodeId = c.req.param("nodeId");
|
|
1717
|
+
const body = c.req.valid("json");
|
|
1718
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1719
|
+
const confirmation = {
|
|
1720
|
+
id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.confirmations ?? []).map((cf) => cf.id)),
|
|
1721
|
+
quote: body.quote,
|
|
1722
|
+
at: /* @__PURE__ */ new Date()
|
|
1723
|
+
};
|
|
1724
|
+
const updatedConfirm = await repo.updateTaskPaths(taskId, {
|
|
1725
|
+
pushes: { [`nodeRecords.${nodeId}.confirmations`]: confirmation }
|
|
1726
|
+
});
|
|
1727
|
+
return c.json(
|
|
1728
|
+
writeAck(updatedConfirm, [`nodeRecords.${nodeId}.confirmations`], { id: confirmation.id, kind: "confirmation" }, [
|
|
1729
|
+
{ path: `nodeRecords.${nodeId}.confirmations`, excerpt: excerpt(confirmation.quote) }
|
|
1730
|
+
])
|
|
1731
|
+
);
|
|
1732
|
+
});
|
|
1733
|
+
app.post("/api/tasks/:taskId/records/:nodeId/decisions", zValidator8("json", DecisionAddSchema), async (c) => {
|
|
1734
|
+
const taskId = c.req.param("taskId");
|
|
1735
|
+
const nodeId = c.req.param("nodeId");
|
|
1736
|
+
const body = c.req.valid("json");
|
|
1737
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1738
|
+
const decision = {
|
|
1739
|
+
id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.decisions ?? []).map((d) => d.id)),
|
|
1740
|
+
topic: body.topic,
|
|
1741
|
+
decision: body.decision
|
|
1742
|
+
};
|
|
1743
|
+
const updatedDecision = await repo.updateTaskPaths(taskId, {
|
|
1744
|
+
pushes: { [`nodeRecords.${nodeId}.decisions`]: decision }
|
|
1745
|
+
});
|
|
1746
|
+
return c.json(
|
|
1747
|
+
writeAck(updatedDecision, [`nodeRecords.${nodeId}.decisions`], { id: decision.id, kind: "decision" }, [
|
|
1748
|
+
{ path: `nodeRecords.${nodeId}.decisions`, excerpt: excerpt(`${decision.topic}: ${decision.decision}`) }
|
|
1749
|
+
])
|
|
1750
|
+
);
|
|
1751
|
+
});
|
|
1752
|
+
app.put("/api/tasks/:taskId/records/:nodeId/review", zValidator8("json", ReviewSetSchema), async (c) => {
|
|
1753
|
+
const taskId = c.req.param("taskId");
|
|
1754
|
+
const nodeId = c.req.param("nodeId");
|
|
1755
|
+
const body = c.req.valid("json");
|
|
1756
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c);
|
|
1757
|
+
const updatedReview = await repo.updateTaskPaths(taskId, {
|
|
1758
|
+
sets: { [`nodeRecords.${nodeId}.review`]: body }
|
|
1759
|
+
});
|
|
1760
|
+
return c.json(
|
|
1761
|
+
writeAck(updatedReview, [`nodeRecords.${nodeId}.review`], void 0, [
|
|
1762
|
+
{ path: `nodeRecords.${nodeId}.review`, excerpt: `${body.verdict}\uFF08rounds=${String(body.rounds)}, critical=${String(body.critical)}\uFF09` }
|
|
1763
|
+
])
|
|
1764
|
+
);
|
|
1765
|
+
});
|
|
1766
|
+
app.post("/api/tasks/:taskId/archnotes", zValidator8("json", TextItemSchema), async (c) => {
|
|
1767
|
+
const taskId = c.req.param("taskId");
|
|
1768
|
+
const body = c.req.valid("json");
|
|
1769
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, void 0, c);
|
|
1770
|
+
const note = { id: generateEntryId((task.archNotes ?? []).map((n) => n.id)), text: body.text, at: /* @__PURE__ */ new Date() };
|
|
1771
|
+
const updatedNote = await repo.updateTaskPaths(taskId, { pushes: { archNotes: note } });
|
|
1772
|
+
return c.json(
|
|
1773
|
+
writeAck(updatedNote, ["archNotes"], { id: note.id, kind: "archnote" }, [
|
|
1774
|
+
{ path: "archNotes", excerpt: excerpt(note.text) }
|
|
1775
|
+
])
|
|
1776
|
+
);
|
|
1777
|
+
});
|
|
1778
|
+
return app;
|
|
1779
|
+
}
|
|
1780
|
+
function assertTypeTrackCompatible(type, track) {
|
|
1781
|
+
const mismatch = (reason) => new ValidationError2(
|
|
1782
|
+
reason,
|
|
1783
|
+
[{ code: "custom", path: ["type"], message: reason }],
|
|
1784
|
+
{ bizCode: "TASK_TYPE_TRACK_MISMATCH", message: reason }
|
|
1785
|
+
);
|
|
1786
|
+
if (track === "research") {
|
|
1787
|
+
if (type !== "research") {
|
|
1788
|
+
throw mismatch(`track=research \u4E3A\u8C03\u7814\u7C7B\u578B\u4E13\u7528\u8F68\u9053\u503C\uFF0C\u987B\u914D --type research\uFF08\u5F53\u524D type=${type ?? "\u672A\u6307\u5B9A"}\uFF09`);
|
|
1789
|
+
}
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (type === void 0) return;
|
|
1793
|
+
if (type === "research") {
|
|
1794
|
+
throw mismatch(`type=research \u987B\u914D track=research\uFF08\u5F53\u524D track=${track}\uFF09`);
|
|
1795
|
+
}
|
|
1796
|
+
if (type === "ui-tweak" && track !== "ui") {
|
|
1797
|
+
throw mismatch(`UI \u5FAE\u8C03\uFF08ui-tweak\uFF09\u4EC5\u9002\u7528 ui \u8F68\u9053\uFF08\u5F53\u524D track=${track}\uFF09`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function mkDeps(repo) {
|
|
1801
|
+
return {
|
|
1802
|
+
getTask: repo.getByTaskId.bind(repo),
|
|
1803
|
+
updateTask: repo.updateTask.bind(repo)
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
async function assertTaskProjectActive(repo, projectRepo, taskId, c) {
|
|
1807
|
+
const task = await repo.getByTaskId(taskId);
|
|
1808
|
+
if (!task) throw new NotFoundError9("task", taskId);
|
|
1809
|
+
assertProjectScope(c, task.projectId);
|
|
1810
|
+
await loadProjectForWrite(projectRepo, task.projectId);
|
|
1811
|
+
}
|
|
1812
|
+
async function loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId, c) {
|
|
1813
|
+
const task = await repo.getByTaskId(taskId);
|
|
1814
|
+
if (!task) throw new NotFoundError9("task", taskId);
|
|
1815
|
+
assertProjectScope(c, task.projectId);
|
|
1816
|
+
await loadProjectForWrite(projectRepo, task.projectId);
|
|
1817
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
1818
|
+
throw new BadRequestError2(
|
|
1819
|
+
`Task '${taskId}' is ${task.status} \u2014 task records are read-only after termination`,
|
|
1820
|
+
void 0,
|
|
1821
|
+
{ bizCode: "TASK_TERMINATED", message: `${BIZ_CODE_MESSAGES7.TASK_TERMINATED}\uFF08status: ${task.status}\uFF09` }
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
if (nodeId === void 0) return task;
|
|
1825
|
+
if (!NODE_ID_PATTERN.test(nodeId)) {
|
|
1826
|
+
throw new BadRequestError2(`nodeId '${nodeId}' \u975E\u6CD5\uFF08\u53EA\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u4E0B\u5212\u7EBF/\u8FDE\u5B57\u7B26\uFF09`, "nodeId");
|
|
1827
|
+
}
|
|
1828
|
+
const node = task.dagInstance.nodes.find((n) => n.id === nodeId);
|
|
1829
|
+
if (!node) {
|
|
1830
|
+
throw new NotFoundError9("node", nodeId, {
|
|
1831
|
+
bizCode: "NODE_NOT_IN_INSTANCE",
|
|
1832
|
+
message: `${BIZ_CODE_MESSAGES7.NODE_NOT_IN_INSTANCE}\uFF08nodeId: ${nodeId}\uFF09`
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
const state = task.dagInstance.nodeStates[nodeId];
|
|
1836
|
+
if (state?.status !== "active" && state?.status !== "completed") {
|
|
1837
|
+
throw new BadRequestError2(
|
|
1838
|
+
`Node '${nodeId}' record is not writable (state '${state?.status ?? "pending"}') \u2014 flow has not reached this node`,
|
|
1839
|
+
void 0,
|
|
1840
|
+
{
|
|
1841
|
+
bizCode: "NODE_RECORD_NOT_WRITABLE",
|
|
1842
|
+
message: `${BIZ_CODE_MESSAGES7.NODE_RECORD_NOT_WRITABLE}\uFF08${nodeId}: ${state?.status ?? "pending"}\uFF09`
|
|
1843
|
+
}
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
return task;
|
|
1847
|
+
}
|
|
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
|
+
|
|
2063
|
+
// src/routes/settings.routes.ts
|
|
2064
|
+
import { Hono as Hono12 } from "hono";
|
|
2065
|
+
import { zValidator as zValidator10 } from "@hono/zod-validator";
|
|
2066
|
+
import { z as z7 } from "zod";
|
|
2067
|
+
import {
|
|
2068
|
+
createEnumRegistryRepo as createEnumRegistryRepo3,
|
|
2069
|
+
EnumRegistryUpdateSchema,
|
|
2070
|
+
NotFoundError as NotFoundError10
|
|
2071
|
+
} from "@siming-org/core";
|
|
2072
|
+
function createSettingsRoutes(client) {
|
|
2073
|
+
const app = new Hono12();
|
|
2074
|
+
const repo = createEnumRegistryRepo3(client.db());
|
|
2075
|
+
app.get("/api/settings/enums", async (c) => {
|
|
2076
|
+
return c.json(await repo.listRegistries());
|
|
2077
|
+
});
|
|
2078
|
+
app.get(
|
|
2079
|
+
"/api/settings/enums/:category",
|
|
2080
|
+
zValidator10("param", z7.object({ category: z7.string() })),
|
|
2081
|
+
async (c) => {
|
|
2082
|
+
const { category } = c.req.valid("param");
|
|
2083
|
+
const registry = await repo.getRegistry(category);
|
|
2084
|
+
if (!registry) throw new NotFoundError10("enum-registry", category);
|
|
2085
|
+
return c.json(registry);
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
2088
|
+
app.put(
|
|
2089
|
+
"/api/settings/enums/:category",
|
|
2090
|
+
zValidator10("param", z7.object({ category: z7.string() })),
|
|
2091
|
+
zValidator10("json", EnumRegistryUpdateSchema),
|
|
2092
|
+
async (c) => {
|
|
2093
|
+
const { category } = c.req.valid("param");
|
|
2094
|
+
const data = c.req.valid("json");
|
|
2095
|
+
const registry = await repo.updateRegistry(category, data);
|
|
2096
|
+
if (!registry) throw new NotFoundError10("enum-registry", category);
|
|
2097
|
+
return c.json(registry);
|
|
2098
|
+
}
|
|
2099
|
+
);
|
|
2100
|
+
app.delete(
|
|
2101
|
+
"/api/settings/enums/:category/entries/:value",
|
|
2102
|
+
zValidator10("param", z7.object({ category: z7.string(), value: z7.string().min(1) })),
|
|
2103
|
+
async (c) => {
|
|
2104
|
+
const { category, value } = c.req.valid("param");
|
|
2105
|
+
const deleted = await repo.deleteEntry(category, value);
|
|
2106
|
+
if (!deleted) throw new NotFoundError10("enum-registry", category);
|
|
2107
|
+
return c.body(null, 204);
|
|
2108
|
+
}
|
|
2109
|
+
);
|
|
2110
|
+
return app;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/routes/index.ts
|
|
2114
|
+
function createRoutes(client) {
|
|
2115
|
+
const router = new Hono13();
|
|
2116
|
+
router.route("/", createHealth(client));
|
|
2117
|
+
router.route("/", createProjectRoutes(client));
|
|
2118
|
+
router.route("/", createSkillRoutes(client));
|
|
2119
|
+
router.route("/", createAgentRoutes(client));
|
|
2120
|
+
router.route("/", createModelAliasRoutes(client));
|
|
2121
|
+
router.route("/", createDagTemplateRoutes(client));
|
|
2122
|
+
router.route("/", createNodePresetRoutes(client));
|
|
2123
|
+
router.route("/", createNodeLibraryRoutes(client));
|
|
2124
|
+
router.route("/", createTaskRoutes(client));
|
|
2125
|
+
router.route("/", createTaskBatchRoutes(client));
|
|
2126
|
+
router.route("/", createSettingsRoutes(client));
|
|
2127
|
+
return router;
|
|
2128
|
+
}
|
|
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
|
+
|
|
2295
|
+
// src/app.ts
|
|
2296
|
+
function createApp(client, opts = {}) {
|
|
2297
|
+
const app = new Hono15();
|
|
2298
|
+
setupErrorHandler(app);
|
|
2299
|
+
const authEnabled = opts.authEnabled ?? false;
|
|
2300
|
+
app.use("/api/*", createAuthMiddleware(client, { authEnabled }));
|
|
2301
|
+
app.use("/health/*", createAuthMiddleware(client, { authEnabled }));
|
|
2302
|
+
app.route("/", createRoutes(client));
|
|
2303
|
+
app.route("/", createAuthRoutes(client, { authEnabled }));
|
|
2304
|
+
const webRoot = opts.webRoot !== void 0 ? opts.webRoot : resolveWebDistRoot();
|
|
2305
|
+
app.route("/", createStaticRoutes(webRoot));
|
|
2306
|
+
return app;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/start-server.ts
|
|
2310
|
+
import { serve } from "@hono/node-server";
|
|
2311
|
+
import { createMongoClient } from "@siming-org/core";
|
|
2312
|
+
|
|
2313
|
+
// src/ensure-collections.ts
|
|
2314
|
+
import { EnumRegistrySchema, bumpPatch as bumpPatch2, createAuthSessionRepo as createAuthSessionRepo3, buildTemplateCode, isLegalTemplateCode } from "@siming-org/core";
|
|
2315
|
+
async function ensureCollections(client) {
|
|
2316
|
+
const db = client.db();
|
|
2317
|
+
await Promise.all([
|
|
2318
|
+
ensureProjectCollection(db),
|
|
2319
|
+
ensureSkillCollection(db),
|
|
2320
|
+
ensureAgentCollection(db),
|
|
2321
|
+
ensureDagTemplateCollection(db),
|
|
2322
|
+
ensureTaskCollection(db),
|
|
2323
|
+
ensureModelAliasCollection(db),
|
|
2324
|
+
ensureEnumRegistryCollection(db),
|
|
2325
|
+
ensureAuthCollections(db),
|
|
2326
|
+
ensureNodePresetCollection(db),
|
|
2327
|
+
ensureNodeLibraryCollection(db)
|
|
2328
|
+
]);
|
|
2329
|
+
await migrateProjectSentinelData(db);
|
|
2330
|
+
await migrateGateRemovalData(db);
|
|
2331
|
+
await migrateTaskRecordData(db);
|
|
2332
|
+
await migrateDagTrackTaskValues(db);
|
|
2333
|
+
await migrateAgentFunctionBackfill(db);
|
|
2334
|
+
await migrateDagTemplateCode(db);
|
|
2335
|
+
await ensurePostMigrationIndexes(db);
|
|
2336
|
+
}
|
|
2337
|
+
var ENUM_REGISTRY_SEEDS = {
|
|
2338
|
+
dag_phase: [
|
|
2339
|
+
{ value: "entry", label: "\u5165\u53E3", builtin: false },
|
|
2340
|
+
{ value: "track", label: "\u8F68\u9053", builtin: false },
|
|
2341
|
+
{ value: "test", label: "\u6D4B\u8BD5", builtin: false },
|
|
2342
|
+
{ value: "exit", label: "\u51FA\u53E3", builtin: false }
|
|
2343
|
+
],
|
|
2344
|
+
dag_track: [
|
|
2345
|
+
{ value: "backend", label: "\u540E\u7AEF", builtin: true },
|
|
2346
|
+
{ value: "ui", label: "\u524D\u7AEF", builtin: true },
|
|
2347
|
+
{ value: "all", label: "\u5168\u90E8", builtin: true },
|
|
2348
|
+
// T202608240003:任务级轨道专用值(非节点 track 值)——mixed=双链串行、research=调研任务;
|
|
2349
|
+
// 存量库 doc 已存在(seed 类缺失才插入),由 M11 迁移合并(缺 M11 会测试绿+生产 400)
|
|
2350
|
+
{ value: "mixed", label: "\u6DF7\u5408\uFF08\u524D\u540E\u7AEF\uFF09", builtin: true },
|
|
2351
|
+
{ value: "research", label: "\u8C03\u7814\uFF08\u5185\u90E8\u503C\uFF0C\u968F\u7C7B\u578B\u81EA\u52A8\u8BBE\u7F6E\uFF09", builtin: true }
|
|
2352
|
+
],
|
|
2353
|
+
pause_type: [
|
|
2354
|
+
{ value: "human_approval", label: "\u4EBA\u5DE5\u5BA1\u6279", builtin: false },
|
|
2355
|
+
{ value: "checkpoint", label: "\u68C0\u67E5\u70B9", builtin: false }
|
|
2356
|
+
],
|
|
2357
|
+
task_status: [
|
|
2358
|
+
{ value: "active", label: "\u8FDB\u884C\u4E2D", builtin: true },
|
|
2359
|
+
{ value: "paused", label: "\u6682\u505C", builtin: true },
|
|
2360
|
+
{ value: "completed", label: "\u5DF2\u5B8C\u6210", builtin: true },
|
|
2361
|
+
{ value: "cancelled", label: "\u5DF2\u53D6\u6D88", builtin: true }
|
|
2362
|
+
],
|
|
2363
|
+
node_status: [
|
|
2364
|
+
{ value: "pending", label: "\u5F85\u6267\u884C", builtin: true },
|
|
2365
|
+
{ value: "active", label: "\u8FDB\u884C\u4E2D", builtin: true },
|
|
2366
|
+
{ value: "completed", label: "\u5DF2\u5B8C\u6210", builtin: true },
|
|
2367
|
+
{ value: "skipped", label: "\u5DF2\u8DF3\u8FC7", builtin: true }
|
|
2368
|
+
],
|
|
2369
|
+
skill_category: [
|
|
2370
|
+
{ value: "process", label: "\u6D41\u7A0B", builtin: false },
|
|
2371
|
+
{ value: "domain", label: "\u9886\u57DF", builtin: false },
|
|
2372
|
+
{ value: "tooling", label: "\u5DE5\u5177", builtin: false }
|
|
2373
|
+
],
|
|
2374
|
+
scope: [
|
|
2375
|
+
{ value: "global", label: "\u5168\u5C40", builtin: true },
|
|
2376
|
+
{ value: "project", label: "\u9879\u76EE", builtin: true }
|
|
2377
|
+
],
|
|
2378
|
+
// T202608260002 C3:schema 固定枚举为消费权威(AgentFunctionSchema),registry 仅展示(仿 pause_type 双轨)——
|
|
2379
|
+
// web 后续经 useEnumEntries('agent_function') 消费;两值 builtin(writer 落盘注入按 reviewer 判定,承重字段)
|
|
2380
|
+
agent_function: [
|
|
2381
|
+
{ value: "reviewer", label: "\u5BA1\u67E5\uFF08\u843D\u76D8\u6CE8\u5165\u4FE1\u606F\u8FB9\u754C\uFF09", builtin: true },
|
|
2382
|
+
{ value: "executor", label: "\u6267\u884C/\u901A\u7528\uFF08\u9ED8\u8BA4\uFF09", builtin: true }
|
|
2383
|
+
]
|
|
2384
|
+
};
|
|
2385
|
+
function buildSeedEntries(seeds) {
|
|
2386
|
+
return seeds.map((s, i) => ({
|
|
2387
|
+
value: s.value,
|
|
2388
|
+
label: s.label,
|
|
2389
|
+
builtin: s.builtin,
|
|
2390
|
+
order: i,
|
|
2391
|
+
active: true,
|
|
2392
|
+
...s.color ? { color: s.color } : {}
|
|
2393
|
+
}));
|
|
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
|
+
}
|
|
2446
|
+
async function ensureEnumRegistryCollection(db) {
|
|
2447
|
+
const collections = await db.listCollections({ name: "enum_registry" }).toArray();
|
|
2448
|
+
if (collections.length === 0) {
|
|
2449
|
+
await db.createCollection("enum_registry");
|
|
2450
|
+
}
|
|
2451
|
+
await db.collection("enum_registry").createIndex({ category: 1 }, { unique: true });
|
|
2452
|
+
const coll = db.collection("enum_registry");
|
|
2453
|
+
for (const [category, seeds] of Object.entries(ENUM_REGISTRY_SEEDS)) {
|
|
2454
|
+
const existing = await coll.findOne({ category });
|
|
2455
|
+
if (!existing) {
|
|
2456
|
+
const doc = {
|
|
2457
|
+
category,
|
|
2458
|
+
entries: buildSeedEntries(seeds),
|
|
2459
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2460
|
+
};
|
|
2461
|
+
await coll.insertOne(doc);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async function ensureModelAliasCollection(db) {
|
|
2466
|
+
const validator = { $jsonSchema: {
|
|
2467
|
+
bsonType: "object",
|
|
2468
|
+
required: ["code", "name", "realModel"],
|
|
2469
|
+
properties: {
|
|
2470
|
+
code: { bsonType: "string", pattern: "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
|
2471
|
+
name: { bsonType: "string" },
|
|
2472
|
+
realModel: { bsonType: "string" },
|
|
2473
|
+
createdAt: { bsonType: "date" },
|
|
2474
|
+
updatedAt: { bsonType: "date" }
|
|
2475
|
+
}
|
|
2476
|
+
} };
|
|
2477
|
+
const collections = await db.listCollections({ name: "model_aliases" }).toArray();
|
|
2478
|
+
if (collections.length === 0) {
|
|
2479
|
+
await db.createCollection("model_aliases", { validator });
|
|
2480
|
+
}
|
|
2481
|
+
await db.collection("model_aliases").createIndex({ code: 1 }, { unique: true });
|
|
2482
|
+
}
|
|
2483
|
+
async function ensureProjectCollection(db) {
|
|
2484
|
+
const collections = await db.listCollections({ name: "projects" }).toArray();
|
|
2485
|
+
if (collections.length === 0) {
|
|
2486
|
+
await db.createCollection("projects", { validator: projectValidator() });
|
|
2487
|
+
}
|
|
2488
|
+
await db.collection("projects").createIndex({ key: 1 }, { unique: true });
|
|
2489
|
+
await db.collection("projects").createIndex({ name: 1 }, { unique: true });
|
|
2490
|
+
await db.collection("projects").createIndex({ status: 1 });
|
|
2491
|
+
}
|
|
2492
|
+
function projectValidator() {
|
|
2493
|
+
return { $jsonSchema: {
|
|
2494
|
+
bsonType: "object",
|
|
2495
|
+
required: ["key", "name", "status"],
|
|
2496
|
+
properties: {
|
|
2497
|
+
key: { bsonType: "string", pattern: "^[a-z0-9][a-z0-9-]{1,30}$" },
|
|
2498
|
+
name: { bsonType: "string" },
|
|
2499
|
+
description: { bsonType: "string" },
|
|
2500
|
+
status: { enum: ["active", "archived"] },
|
|
2501
|
+
createdAt: { bsonType: "date" },
|
|
2502
|
+
updatedAt: { bsonType: "date" }
|
|
2503
|
+
}
|
|
2504
|
+
} };
|
|
2505
|
+
}
|
|
2506
|
+
async function ensureSkillCollection(db) {
|
|
2507
|
+
const validator = { $jsonSchema: {
|
|
2508
|
+
bsonType: "object",
|
|
2509
|
+
required: ["name", "description", "content", "category", "scope"],
|
|
2510
|
+
properties: {
|
|
2511
|
+
// name 不设 pattern:create 侧 kebab 由应用层 Zod 把守(SkillCreateSchema),DB 层放宽以允许
|
|
2512
|
+
// validator 部署前的 CJK 存量记录被更新(否则 findOneAndUpdate 触发 code 121 拒绝合法编辑,N013 SKILL_EDIT_008)
|
|
2513
|
+
name: { bsonType: "string", minLength: 1 },
|
|
2514
|
+
description: { bsonType: "string" },
|
|
2515
|
+
content: { bsonType: "string" },
|
|
2516
|
+
// N016 F9:category 放宽为任意字符串(枚举注册表为运行时权威,DB 只做类型兜底)
|
|
2517
|
+
category: { bsonType: "string" },
|
|
2518
|
+
// T202608270002:引用文档(可选,存量单文件资产无此字段合法)——路径规则/上限归应用层 Zod,DB 只做类型级声明
|
|
2519
|
+
references: {
|
|
2520
|
+
bsonType: "array",
|
|
2521
|
+
items: {
|
|
2522
|
+
bsonType: "object",
|
|
2523
|
+
required: ["path", "content"],
|
|
2524
|
+
properties: {
|
|
2525
|
+
path: { bsonType: "string", minLength: 1 },
|
|
2526
|
+
content: { bsonType: "string" }
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
},
|
|
2530
|
+
// N012 F7:作用域字段(全局 / 项目专用);projectId 仅 scope=project 携带
|
|
2531
|
+
scope: { enum: ["global", "project"] },
|
|
2532
|
+
projectId: { bsonType: "string" },
|
|
2533
|
+
version: { bsonType: "string" },
|
|
2534
|
+
// T202608290001:资产启停(可选——enabled === false 即停用,缺失 = 启用,存量零迁移)
|
|
2535
|
+
enabled: { bsonType: "bool" },
|
|
2536
|
+
createdAt: { bsonType: "date" },
|
|
2537
|
+
updatedAt: { bsonType: "date" }
|
|
2538
|
+
}
|
|
2539
|
+
} };
|
|
2540
|
+
const collections = await db.listCollections({ name: "skills" }).toArray();
|
|
2541
|
+
if (collections.length === 0) {
|
|
2542
|
+
await db.createCollection("skills", { validator });
|
|
2543
|
+
} else {
|
|
2544
|
+
await db.command({ collMod: "skills", validationLevel: "strict", validator });
|
|
2545
|
+
}
|
|
2546
|
+
await db.collection("skills").createIndex({ scope: 1, projectId: 1 });
|
|
2547
|
+
}
|
|
2548
|
+
async function ensureAgentCollection(db) {
|
|
2549
|
+
const validator = { $jsonSchema: {
|
|
2550
|
+
bsonType: "object",
|
|
2551
|
+
required: ["name", "description", "systemPrompt", "model", "scope"],
|
|
2552
|
+
properties: {
|
|
2553
|
+
name: { bsonType: "string" },
|
|
2554
|
+
description: { bsonType: "string" },
|
|
2555
|
+
systemPrompt: { bsonType: "string" },
|
|
2556
|
+
boundSkills: { bsonType: "array", items: { bsonType: "string" } },
|
|
2557
|
+
model: { bsonType: "string" },
|
|
2558
|
+
// N015:agent 资产版本(F13 版本对比);存量回填走 M6 迁移
|
|
2559
|
+
version: { bsonType: "string" },
|
|
2560
|
+
tools: { bsonType: "array", items: { bsonType: "string" } },
|
|
2561
|
+
permissions: { bsonType: "array", items: { bsonType: "string" } },
|
|
2562
|
+
// T202608270002:引用文档(可选,存量单文件资产无此字段合法)——路径规则/上限归应用层 Zod,DB 只做类型级声明
|
|
2563
|
+
references: {
|
|
2564
|
+
bsonType: "array",
|
|
2565
|
+
items: {
|
|
2566
|
+
bsonType: "object",
|
|
2567
|
+
required: ["path", "content"],
|
|
2568
|
+
properties: {
|
|
2569
|
+
path: { bsonType: "string", minLength: 1 },
|
|
2570
|
+
content: { bsonType: "string" }
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
},
|
|
2574
|
+
scope: { enum: ["global", "project"] },
|
|
2575
|
+
projectId: { bsonType: "string" },
|
|
2576
|
+
// T202608260002:功能类型(可选——存量缺省合法,M12 回填)——取值域归应用层 Zod,DB 只做类型兜底
|
|
2577
|
+
"function": { bsonType: "string" },
|
|
2578
|
+
// T202608290001:资产启停(可选——enabled === false 即停用,缺失 = 启用,存量零迁移)
|
|
2579
|
+
enabled: { bsonType: "bool" },
|
|
2580
|
+
createdAt: { bsonType: "date" },
|
|
2581
|
+
updatedAt: { bsonType: "date" }
|
|
2582
|
+
}
|
|
2583
|
+
} };
|
|
2584
|
+
const collections = await db.listCollections({ name: "agents" }).toArray();
|
|
2585
|
+
if (collections.length === 0) {
|
|
2586
|
+
await db.createCollection("agents", { validator });
|
|
2587
|
+
} else {
|
|
2588
|
+
await db.command({ collMod: "agents", validationLevel: "strict", validator });
|
|
2589
|
+
}
|
|
2590
|
+
await db.collection("agents").createIndex({ scope: 1, projectId: 1 });
|
|
2591
|
+
}
|
|
2592
|
+
async function ensureDagTemplateCollection(db) {
|
|
2593
|
+
const validator = { $jsonSchema: {
|
|
2594
|
+
bsonType: "object",
|
|
2595
|
+
// N017 F8:pausePoints 顶层字段退役(暂停点内联 edges.pausePoint)
|
|
2596
|
+
required: ["name", "projectId", "description", "nodes", "edges", "isDefault", "version"],
|
|
2597
|
+
properties: {
|
|
2598
|
+
name: { bsonType: "string" },
|
|
2599
|
+
projectId: { bsonType: "string" },
|
|
2600
|
+
// T202609020002:业务 code(可选——存量迁移前缺失合法,M13 回填;格式规则归应用层 Zod,
|
|
2601
|
+
// DB 只做类型兜底,pattern 禁令见技术架构「DB validator 职责边界」)
|
|
2602
|
+
code: { bsonType: "string" },
|
|
2603
|
+
description: { bsonType: "string" },
|
|
2604
|
+
nodes: { bsonType: "array" },
|
|
2605
|
+
edges: { bsonType: "array" },
|
|
2606
|
+
isDefault: { bsonType: "bool" },
|
|
2607
|
+
version: { bsonType: "string" },
|
|
2608
|
+
// T202608290001:模板启停(可选——enabled === false 即停用,缺失 = 启用,存量零迁移)
|
|
2609
|
+
enabled: { bsonType: "bool" },
|
|
2610
|
+
createdAt: { bsonType: "date" },
|
|
2611
|
+
updatedAt: { bsonType: "date" }
|
|
2612
|
+
}
|
|
2613
|
+
} };
|
|
2614
|
+
const collections = await db.listCollections({ name: "dag_templates" }).toArray();
|
|
2615
|
+
if (collections.length === 0) {
|
|
2616
|
+
await db.createCollection("dag_templates", { validator });
|
|
2617
|
+
} else {
|
|
2618
|
+
await db.command({ collMod: "dag_templates", validationLevel: "strict", validator });
|
|
2619
|
+
}
|
|
2620
|
+
await db.collection("dag_templates").createIndex({ projectId: 1 });
|
|
2621
|
+
await db.collection("dag_templates").createIndex({ isDefault: 1 });
|
|
2622
|
+
}
|
|
2623
|
+
async function ensureTaskCollection(db) {
|
|
2624
|
+
const validator = { $jsonSchema: {
|
|
2625
|
+
bsonType: "object",
|
|
2626
|
+
required: ["taskId", "title", "projectId", "dagTemplateId", "dagInstance", "currentNode", "currentPhase", "status", "track", "history"],
|
|
2627
|
+
properties: {
|
|
2628
|
+
taskId: { bsonType: "string" },
|
|
2629
|
+
title: { bsonType: "string" },
|
|
2630
|
+
projectId: { bsonType: "string" },
|
|
2631
|
+
dagTemplateId: { bsonType: "string" },
|
|
2632
|
+
dagInstance: { bsonType: "object" },
|
|
2633
|
+
currentNode: { bsonType: "string" },
|
|
2634
|
+
// N016 F06:枚举放宽(D3 注册表权威)——currentPhase/status 不再 DB 枚举硬限,由注册表/应用层把守
|
|
2635
|
+
currentPhase: { bsonType: "string" },
|
|
2636
|
+
status: { bsonType: "string" },
|
|
2637
|
+
pausedAt: { bsonType: ["string", "null"] },
|
|
2638
|
+
track: { bsonType: "string" },
|
|
2639
|
+
// T202608240003:任务类型标签(可选,类型兜底——取值域归应用层 z.enum)
|
|
2640
|
+
type: { bsonType: "string" },
|
|
2641
|
+
// N020 D1:任务信息结构化三字段(可选——存量任务未迁移前缺省合法,M10 补齐)
|
|
2642
|
+
doc: { bsonType: "object" },
|
|
2643
|
+
nodeRecords: { bsonType: "object" },
|
|
2644
|
+
archNotes: { bsonType: "array" },
|
|
2645
|
+
history: { bsonType: "array" },
|
|
2646
|
+
createdAt: { bsonType: "date" },
|
|
2647
|
+
updatedAt: { bsonType: "date" }
|
|
2648
|
+
}
|
|
2649
|
+
} };
|
|
2650
|
+
const collections = await db.listCollections({ name: "tasks" }).toArray();
|
|
2651
|
+
if (collections.length === 0) {
|
|
2652
|
+
await db.createCollection("tasks", { validator });
|
|
2653
|
+
} else {
|
|
2654
|
+
await db.command({ collMod: "tasks", validationLevel: "strict", validator });
|
|
2655
|
+
}
|
|
2656
|
+
await db.collection("tasks").createIndex({ taskId: 1 }, { unique: true });
|
|
2657
|
+
await db.collection("tasks").createIndex({ status: 1 });
|
|
2658
|
+
await db.collection("tasks").createIndex({ track: 1 });
|
|
2659
|
+
}
|
|
2660
|
+
async function migrateProjectSentinelData(db) {
|
|
2661
|
+
const projects = db.collection("projects");
|
|
2662
|
+
let defaultProject = await projects.findOne({ key: "default" });
|
|
2663
|
+
if (!defaultProject) {
|
|
2664
|
+
const now = /* @__PURE__ */ new Date();
|
|
2665
|
+
await projects.insertOne({
|
|
2666
|
+
key: "default",
|
|
2667
|
+
name: "\u9ED8\u8BA4\u9879\u76EE",
|
|
2668
|
+
description: "\u7CFB\u7EDF\u5185\u7F6E\u9879\u76EE\uFF08\u5B58\u91CF\u8D44\u4EA7\u4E0E\u672A\u6307\u5B9A\u5F52\u5C5E\u8D44\u4EA7\u7684\u843D\u70B9\uFF0C\u4E0D\u53EF\u505C\u7528\uFF09",
|
|
2669
|
+
status: "active",
|
|
2670
|
+
createdAt: now,
|
|
2671
|
+
updatedAt: now
|
|
2672
|
+
});
|
|
2673
|
+
defaultProject = await projects.findOne({ key: "default" });
|
|
2674
|
+
}
|
|
2675
|
+
if (!defaultProject) {
|
|
2676
|
+
throw new Error("N012 M1 \u8FC1\u79FB\u5931\u8D25\uFF1A\u9ED8\u8BA4\u9879\u76EE seed \u540E\u4ECD\u65E0\u6CD5\u8BFB\u53D6");
|
|
2677
|
+
}
|
|
2678
|
+
const defaultProjectId = defaultProject._id.toString();
|
|
2679
|
+
await db.collection("tasks").updateMany({ projectId: "default" }, { $set: { projectId: defaultProjectId } }, { bypassDocumentValidation: true });
|
|
2680
|
+
await db.collection("dag_templates").updateMany({ projectId: "default" }, { $set: { projectId: defaultProjectId } }, { bypassDocumentValidation: true });
|
|
2681
|
+
await db.collection("skills").updateMany({ scope: { $exists: false } }, { $set: { scope: "global" } });
|
|
2682
|
+
await db.collection("agents").updateMany({ scope: { $exists: false } }, { $set: { scope: "global" } });
|
|
2683
|
+
await db.collection("agents").updateMany({ version: { $exists: false } }, { $set: { version: "1.0.0" } });
|
|
2684
|
+
}
|
|
2685
|
+
async function migrateGateRemovalData(db) {
|
|
2686
|
+
await db.collection("enum_registry").deleteOne({ category: "gate_type" });
|
|
2687
|
+
const legacyTemplates = await db.collection("dag_templates").find(
|
|
2688
|
+
{ $or: [{ "nodes.gates": { $exists: true } }, { pausePoints: { $exists: true } }] },
|
|
2689
|
+
{ projection: { name: 1 } }
|
|
2690
|
+
).toArray();
|
|
2691
|
+
if (legacyTemplates.length > 0) {
|
|
2692
|
+
console.log(`[siming] N017 M8\uFF1A\u5220\u9664\u65E7\u7ED3\u6784 DAG \u6A21\u677F ${legacyTemplates.length} \u4E2A\uFF08\u91CD\u5BFC\u65B0 YAML\uFF09\uFF1A${legacyTemplates.map((t) => `${t.name}(${t._id})`).join(", ")}`);
|
|
2693
|
+
await db.collection("dag_templates").deleteMany({
|
|
2694
|
+
_id: { $in: legacyTemplates.map((t) => t._id) }
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
const legacyTasks = await db.collection("tasks").find(
|
|
2698
|
+
{ "dagInstance.pausePoints": { $exists: true } },
|
|
2699
|
+
{ projection: { taskId: 1 } }
|
|
2700
|
+
).toArray();
|
|
2701
|
+
if (legacyTasks.length > 0) {
|
|
2702
|
+
console.log(`[siming] N017 M9\uFF1A\u5220\u9664\u65E7\u7ED3\u6784\u4EFB\u52A1 ${legacyTasks.length} \u4E2A\uFF1A${legacyTasks.map((t) => t.taskId).join(", ")}`);
|
|
2703
|
+
await db.collection("tasks").deleteMany({
|
|
2704
|
+
_id: { $in: legacyTasks.map((t) => t._id) }
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
async function migrateTaskRecordData(db) {
|
|
2709
|
+
await db.collection("tasks").updateMany(
|
|
2710
|
+
{ nodeRecords: { $exists: false } },
|
|
2711
|
+
{ $set: { nodeRecords: {} } },
|
|
2712
|
+
{ bypassDocumentValidation: true }
|
|
2713
|
+
);
|
|
2714
|
+
await db.collection("tasks").updateMany(
|
|
2715
|
+
{ archNotes: { $exists: false } },
|
|
2716
|
+
{ $set: { archNotes: [] } },
|
|
2717
|
+
{ bypassDocumentValidation: true }
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
async function migrateDagTrackTaskValues(db) {
|
|
2721
|
+
const coll = db.collection("enum_registry");
|
|
2722
|
+
const doc = await coll.findOne({ category: "dag_track" });
|
|
2723
|
+
if (!doc) return;
|
|
2724
|
+
const parsed = EnumRegistrySchema.safeParse({ category: "dag_track", ...doc, entries: doc.entries ?? [] });
|
|
2725
|
+
if (!parsed.success) {
|
|
2726
|
+
throw new Error(`T202608240003 M11 \u8FC1\u79FB\u5931\u8D25\uFF1A\u5B58\u91CF dag_track entries \u5F62\u72B6\u975E\u6CD5\uFF08${JSON.stringify(parsed.error.issues[0]?.message)}\uFF09`);
|
|
2727
|
+
}
|
|
2728
|
+
const entries = parsed.data.entries;
|
|
2729
|
+
const existingValues = new Set(entries.map((e) => e.value));
|
|
2730
|
+
const maxOrder = entries.reduce((max, e) => Math.max(max, e.order), -1);
|
|
2731
|
+
const additions = ENUM_REGISTRY_SEEDS.dag_track.filter((s) => !existingValues.has(s.value)).map((s, i) => ({
|
|
2732
|
+
value: s.value,
|
|
2733
|
+
label: s.label,
|
|
2734
|
+
builtin: s.builtin,
|
|
2735
|
+
order: maxOrder + 1 + i,
|
|
2736
|
+
active: true
|
|
2737
|
+
}));
|
|
2738
|
+
if (additions.length === 0) return;
|
|
2739
|
+
const result = await coll.updateOne(
|
|
2740
|
+
{ _id: doc._id },
|
|
2741
|
+
{ $set: { entries: [...entries, ...additions], updatedAt: /* @__PURE__ */ new Date() } }
|
|
2742
|
+
);
|
|
2743
|
+
if (result.matchedCount === 0) {
|
|
2744
|
+
throw new Error("T202608240003 M11 \u8FC1\u79FB\u5931\u8D25\uFF1Adag_track doc \u66F4\u65B0\u672A\u547D\u4E2D\uFF08\u542F\u52A8\u671F\u4E0D\u5E94\u53D1\u751F\uFF09");
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
async function migrateAgentFunctionBackfill(db) {
|
|
2748
|
+
const coll = db.collection("agents");
|
|
2749
|
+
const docs = await coll.find({ "function": { $exists: false } }, { projection: { version: 1 } }).toArray();
|
|
2750
|
+
if (docs.length === 0) return;
|
|
2751
|
+
console.log(`[siming] T202608260002 M12\uFF1Aagents.function \u56DE\u586B ${docs.length} \u4E2A\uFF08executor + version bump\uFF0C\u9A71\u52A8\u5DF2\u88C5\u73AF\u5883\u91CD\u5199\uFF09`);
|
|
2752
|
+
for (const doc of docs) {
|
|
2753
|
+
const baseVersion = typeof doc.version === "string" ? doc.version : "1.0.0";
|
|
2754
|
+
const result = await coll.updateOne(
|
|
2755
|
+
{ _id: doc._id, "function": { $exists: false } },
|
|
2756
|
+
{ $set: { "function": "executor", version: bumpPatch2(baseVersion) } }
|
|
2757
|
+
);
|
|
2758
|
+
if (result.matchedCount === 0) {
|
|
2759
|
+
throw new Error("T202608260002 M12 \u8FC1\u79FB\u5931\u8D25\uFF1Aagents.function \u56DE\u586B\u672A\u547D\u4E2D\uFF08\u542F\u52A8\u671F\u5355\u8FDB\u7A0B\u4E32\u884C\uFF0C\u4E0D\u5E94\u53D1\u751F\uFF09");
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
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
|
+
}
|
|
2852
|
+
async function ensurePostMigrationIndexes(db) {
|
|
2853
|
+
const tasks = db.collection("tasks");
|
|
2854
|
+
const taskIndexes = await tasks.listIndexes().toArray();
|
|
2855
|
+
const legacySingle = taskIndexes.find(
|
|
2856
|
+
(idx) => idx.name === "projectId_1" && Object.keys(idx.key).length === 1
|
|
2857
|
+
);
|
|
2858
|
+
if (legacySingle) {
|
|
2859
|
+
await tasks.dropIndex("projectId_1");
|
|
2860
|
+
}
|
|
2861
|
+
await tasks.createIndex({ projectId: 1, status: 1 });
|
|
2862
|
+
const templates = db.collection("dag_templates");
|
|
2863
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2864
|
+
const docs = await templates.find({}, { projection: { projectId: 1, name: 1 } }).toArray();
|
|
2865
|
+
for (const doc of docs) {
|
|
2866
|
+
const key = `${String(doc.projectId)}\0${String(doc.name)}`;
|
|
2867
|
+
const ids = seen.get(key) ?? [];
|
|
2868
|
+
ids.push(doc._id.toString());
|
|
2869
|
+
seen.set(key, ids);
|
|
2870
|
+
}
|
|
2871
|
+
const conflicts = [...seen.entries()].filter(([, ids]) => ids.length > 1);
|
|
2872
|
+
if (conflicts.length > 0) {
|
|
2873
|
+
const detail = conflicts.map(([key, ids]) => {
|
|
2874
|
+
const [projectId, name] = key.split("\0");
|
|
2875
|
+
return `projectId=${projectId} name="${name}" \xD7${ids.length} (_id: ${ids.join(", ")})`;
|
|
2876
|
+
}).join("; ");
|
|
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}`);
|
|
2878
|
+
}
|
|
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
|
+
);
|
|
2904
|
+
await db.collection("agents").createIndex({ model: 1 });
|
|
2905
|
+
await migrateNameUniquenessIndexes(db);
|
|
2906
|
+
}
|
|
2907
|
+
async function migrateNameUniquenessIndexes(db) {
|
|
2908
|
+
const assertNoProjectNameConflict = async (collectionName) => {
|
|
2909
|
+
const coll = db.collection(collectionName);
|
|
2910
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2911
|
+
const docs = await coll.find({ scope: "project" }, { projection: { projectId: 1, name: 1 } }).toArray();
|
|
2912
|
+
for (const doc of docs) {
|
|
2913
|
+
const key = `${String(doc.projectId)}\0${String(doc.name)}`;
|
|
2914
|
+
const ids = seen.get(key) ?? [];
|
|
2915
|
+
ids.push(doc._id.toString());
|
|
2916
|
+
seen.set(key, ids);
|
|
2917
|
+
}
|
|
2918
|
+
const conflicts = [...seen.entries()].filter(([, ids]) => ids.length > 1);
|
|
2919
|
+
if (conflicts.length > 0) {
|
|
2920
|
+
const detail = conflicts.map(([key, ids]) => {
|
|
2921
|
+
const [projectId, name] = key.split("\0");
|
|
2922
|
+
return `projectId=${projectId} name="${name}" \xD7${ids.length} (_id: ${ids.join(", ")})`;
|
|
2923
|
+
}).join("; ");
|
|
2924
|
+
throw new Error(`${collectionName} \u5B58\u5728\u540C\u9879\u76EE\u91CD\u540D\u8D44\u4EA7\uFF0C\u65E0\u6CD5\u5EFA\u7ACB (projectId,name) \u552F\u4E00\u7D22\u5F15\uFF0C\u8BF7\u5148\u4EBA\u5DE5\u5F52\u5E76\uFF1A${detail}`);
|
|
2925
|
+
}
|
|
2926
|
+
};
|
|
2927
|
+
for (const name of ["skills", "agents"]) {
|
|
2928
|
+
const coll = db.collection(name);
|
|
2929
|
+
const indexes = await coll.listIndexes().toArray();
|
|
2930
|
+
const legacyNameUnique = indexes.find(
|
|
2931
|
+
(idx) => idx.name === "name_1" && idx.unique === true && Object.keys(idx.key).length === 1 && // N016 F04:partial 索引名也是 name_1,必须排除(否则每次启动误判 legacy → drop+重建 churn)
|
|
2932
|
+
idx.partialFilterExpression === void 0
|
|
2933
|
+
);
|
|
2934
|
+
if (legacyNameUnique) {
|
|
2935
|
+
await coll.dropIndex("name_1");
|
|
2936
|
+
}
|
|
2937
|
+
await assertNoProjectNameConflict(name);
|
|
2938
|
+
await coll.createIndex(
|
|
2939
|
+
{ name: 1 },
|
|
2940
|
+
{ unique: true, partialFilterExpression: { scope: "global" } }
|
|
2941
|
+
);
|
|
2942
|
+
await coll.createIndex(
|
|
2943
|
+
{ projectId: 1, name: 1 },
|
|
2944
|
+
{ unique: true, partialFilterExpression: { scope: "project" } }
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// src/start-server.ts
|
|
2950
|
+
var MONGO_PROBE_TIMEOUT_MS = 5e3;
|
|
2951
|
+
async function startServer(config) {
|
|
2952
|
+
const client = await createMongoClient(config.mongoUri, {
|
|
2953
|
+
serverSelectionTimeoutMS: MONGO_PROBE_TIMEOUT_MS
|
|
2954
|
+
});
|
|
2955
|
+
await ensureCollections(client);
|
|
2956
|
+
const webRoot = resolveWebDistRoot();
|
|
2957
|
+
const app = createApp(client, { webRoot, authEnabled: config.auth.enabled });
|
|
2958
|
+
let port = config.port;
|
|
2959
|
+
const server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
|
|
2960
|
+
port = info.port;
|
|
2961
|
+
console.log(`[siming] server listening on http://${info.address}:${info.port}`);
|
|
2962
|
+
});
|
|
2963
|
+
console.log(webRoot === null ? "[siming] web ui not built \u2014 static disabled (API only)" : `[siming] web ui root: ${webRoot}`);
|
|
2964
|
+
console.log(`[siming] config: port=${config.port} host=${config.host} logLevel=${config.logLevel}`);
|
|
2965
|
+
const stop = async () => {
|
|
2966
|
+
console.log("[siming] shutting down...");
|
|
2967
|
+
server.close();
|
|
2968
|
+
await client.close();
|
|
2969
|
+
};
|
|
2970
|
+
return { server, client, host: config.host, port, stop };
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
export {
|
|
2974
|
+
ADMIN_ONLY_ROUTES,
|
|
2975
|
+
resolveWebDistRoot,
|
|
2976
|
+
createStaticRoutes,
|
|
2977
|
+
createApp,
|
|
2978
|
+
MONGO_PROBE_TIMEOUT_MS,
|
|
2979
|
+
startServer
|
|
2980
|
+
};
|