@siming-org/server 0.3.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/LICENSE +21 -0
- package/dist/chunk-T7QAGG73.js +1558 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +14 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +38 -0
- package/package.json +42 -0
- package/web-dist/assets/index-B_ZlCIgd.css +1 -0
- package/web-dist/assets/index-hBW7IJvz.js +121 -0
- package/web-dist/index.html +14 -0
|
@@ -0,0 +1,1558 @@
|
|
|
1
|
+
// src/static.ts
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
|
+
import { join, resolve, sep } from "path";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
var ASSETS_PREFIX = "/assets/";
|
|
6
|
+
var API_NAMESPACES = ["/api", "/health"];
|
|
7
|
+
function isApiNamespace(pathname) {
|
|
8
|
+
return API_NAMESPACES.some((ns) => pathname === ns || pathname.startsWith(`${ns}/`));
|
|
9
|
+
}
|
|
10
|
+
var MIME_TYPES = {
|
|
11
|
+
html: "text/html; charset=utf-8",
|
|
12
|
+
js: "text/javascript; charset=utf-8",
|
|
13
|
+
mjs: "text/javascript; charset=utf-8",
|
|
14
|
+
css: "text/css; charset=utf-8",
|
|
15
|
+
json: "application/json; charset=utf-8",
|
|
16
|
+
map: "application/json",
|
|
17
|
+
svg: "image/svg+xml",
|
|
18
|
+
png: "image/png",
|
|
19
|
+
jpg: "image/jpeg",
|
|
20
|
+
jpeg: "image/jpeg",
|
|
21
|
+
gif: "image/gif",
|
|
22
|
+
webp: "image/webp",
|
|
23
|
+
avif: "image/avif",
|
|
24
|
+
ico: "image/x-icon",
|
|
25
|
+
woff: "font/woff",
|
|
26
|
+
woff2: "font/woff2",
|
|
27
|
+
ttf: "font/ttf",
|
|
28
|
+
otf: "font/otf",
|
|
29
|
+
txt: "text/plain; charset=utf-8",
|
|
30
|
+
wasm: "application/wasm"
|
|
31
|
+
};
|
|
32
|
+
function contentTypeFor(filePath) {
|
|
33
|
+
const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".") + 1).toLowerCase() : "";
|
|
34
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
35
|
+
}
|
|
36
|
+
function resolveWebDistRoot() {
|
|
37
|
+
const base = import.meta.dirname;
|
|
38
|
+
const candidates = [
|
|
39
|
+
join(base, "..", "web-dist"),
|
|
40
|
+
// dist 布局:包根/web-dist;src 布局:包根/web-dist(均命中)
|
|
41
|
+
join(base, "..", "..", "web", "dist")
|
|
42
|
+
// dist/src 布局回退:repo packages/web/dist
|
|
43
|
+
];
|
|
44
|
+
for (const dir of candidates) {
|
|
45
|
+
if (existsSync(join(dir, "index.html"))) return dir;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function resolveSafe(webRoot, urlPathname) {
|
|
50
|
+
const segments = urlPathname.split("/").filter((s) => s.length > 0);
|
|
51
|
+
if (segments.includes("..")) return null;
|
|
52
|
+
const target = resolve(webRoot, ...segments);
|
|
53
|
+
if (target !== webRoot && !target.startsWith(webRoot + sep)) return null;
|
|
54
|
+
return target;
|
|
55
|
+
}
|
|
56
|
+
function serveFile(absPath, cacheControl) {
|
|
57
|
+
const body = readFileSync(absPath);
|
|
58
|
+
return new Response(body, {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { "Content-Type": contentTypeFor(absPath), "Cache-Control": cacheControl }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function createStaticRoutes(webRoot) {
|
|
64
|
+
const app = new Hono();
|
|
65
|
+
app.get("*", async (c) => {
|
|
66
|
+
const pathname = c.req.path;
|
|
67
|
+
if (isApiNamespace(pathname)) {
|
|
68
|
+
return c.notFound();
|
|
69
|
+
}
|
|
70
|
+
if (webRoot === null) {
|
|
71
|
+
return c.json(
|
|
72
|
+
{
|
|
73
|
+
error: "web_ui_not_built",
|
|
74
|
+
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"
|
|
75
|
+
},
|
|
76
|
+
503
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
let decoded;
|
|
80
|
+
try {
|
|
81
|
+
decoded = decodeURIComponent(pathname);
|
|
82
|
+
} catch {
|
|
83
|
+
return c.json({ error: "bad_path", message: "URL \u8DEF\u5F84\u7F16\u7801\u975E\u6CD5" }, 400);
|
|
84
|
+
}
|
|
85
|
+
const absPath = resolveSafe(webRoot, decoded === "/" ? "" : decoded);
|
|
86
|
+
if (absPath === null) {
|
|
87
|
+
return c.json({ error: "bad_path", message: "\u8DEF\u5F84\u4E0D\u5408\u6CD5" }, 403);
|
|
88
|
+
}
|
|
89
|
+
const st = statSync(absPath, { throwIfNoEntry: false });
|
|
90
|
+
if (st?.isFile()) {
|
|
91
|
+
const cache = decoded.startsWith(ASSETS_PREFIX) ? "public, max-age=31536000, immutable" : "no-cache";
|
|
92
|
+
return serveFile(absPath, cache);
|
|
93
|
+
}
|
|
94
|
+
if (decoded.startsWith(ASSETS_PREFIX)) {
|
|
95
|
+
return c.notFound();
|
|
96
|
+
}
|
|
97
|
+
const indexPath = join(webRoot, "index.html");
|
|
98
|
+
if (existsSync(indexPath)) {
|
|
99
|
+
return serveFile(indexPath, "no-cache");
|
|
100
|
+
}
|
|
101
|
+
return c.notFound();
|
|
102
|
+
});
|
|
103
|
+
return app;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/app.ts
|
|
107
|
+
import { Hono as Hono11 } from "hono";
|
|
108
|
+
|
|
109
|
+
// src/errors.ts
|
|
110
|
+
import { AppError } from "@siming-org/core";
|
|
111
|
+
function setupErrorHandler(app) {
|
|
112
|
+
app.onError((err, c) => {
|
|
113
|
+
if (err instanceof AppError) {
|
|
114
|
+
const body = JSON.stringify(err.toResponse());
|
|
115
|
+
return new Response(body, { status: err.statusCode, headers: { "Content-Type": "application/json" } });
|
|
116
|
+
}
|
|
117
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === 11e3) {
|
|
118
|
+
return c.json({ error: "conflict", message: "duplicate key" }, 409);
|
|
119
|
+
}
|
|
120
|
+
console.error("Unhandled error:", err);
|
|
121
|
+
return c.json(
|
|
122
|
+
{ error: "internal_error", message: "An unexpected error occurred" },
|
|
123
|
+
500
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/routes/index.ts
|
|
129
|
+
import { Hono as Hono10 } from "hono";
|
|
130
|
+
|
|
131
|
+
// src/routes/health.ts
|
|
132
|
+
import { Hono as Hono2 } from "hono";
|
|
133
|
+
import { ping, VERSION } from "@siming-org/core";
|
|
134
|
+
function createHealth(client) {
|
|
135
|
+
const app = new Hono2();
|
|
136
|
+
app.get("/health", async (c) => {
|
|
137
|
+
let mongoStatus = "ok";
|
|
138
|
+
try {
|
|
139
|
+
await client.db().command({ ping: 1 });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error(`[health] mongo ping failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
142
|
+
mongoStatus = "error";
|
|
143
|
+
}
|
|
144
|
+
const status = mongoStatus === "ok" ? "ok" : "degraded";
|
|
145
|
+
return c.json({
|
|
146
|
+
status,
|
|
147
|
+
version: VERSION,
|
|
148
|
+
pong: ping(),
|
|
149
|
+
mongo: { status: mongoStatus }
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
return app;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/routes/project.routes.ts
|
|
156
|
+
import { Hono as Hono3 } from "hono";
|
|
157
|
+
import { zValidator } from "@hono/zod-validator";
|
|
158
|
+
import {
|
|
159
|
+
createProjectRepo,
|
|
160
|
+
ProjectCreateSchema,
|
|
161
|
+
ProjectUpdateSchema,
|
|
162
|
+
NotFoundError,
|
|
163
|
+
ConflictError,
|
|
164
|
+
BIZ_CODE_MESSAGES,
|
|
165
|
+
DEFAULT_PROJECT_KEY
|
|
166
|
+
} from "@siming-org/core";
|
|
167
|
+
async function loadProjectForWrite(projectRepo, projectId) {
|
|
168
|
+
const project = await projectRepo.getById(projectId);
|
|
169
|
+
if (!project) {
|
|
170
|
+
throw new NotFoundError("project", projectId, {
|
|
171
|
+
bizCode: "PROJECT_NOT_FOUND",
|
|
172
|
+
message: BIZ_CODE_MESSAGES.PROJECT_NOT_FOUND
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (project.status === "archived") {
|
|
176
|
+
throw new ConflictError("project", projectId, {
|
|
177
|
+
bizCode: "PROJECT_ARCHIVED",
|
|
178
|
+
message: BIZ_CODE_MESSAGES.PROJECT_ARCHIVED
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return project;
|
|
182
|
+
}
|
|
183
|
+
function createProjectRoutes(client) {
|
|
184
|
+
const app = new Hono3();
|
|
185
|
+
const repo = createProjectRepo(client.db());
|
|
186
|
+
app.get("/api/projects", async (c) => {
|
|
187
|
+
const status = c.req.query("status");
|
|
188
|
+
const projects = await repo.list();
|
|
189
|
+
return c.json(status ? projects.filter((p) => p.status === status) : projects);
|
|
190
|
+
});
|
|
191
|
+
app.get("/api/projects/:id", async (c) => {
|
|
192
|
+
const id = c.req.param("id");
|
|
193
|
+
const project = await repo.getById(id);
|
|
194
|
+
if (!project) throw new NotFoundError("project", id);
|
|
195
|
+
return c.json(project);
|
|
196
|
+
});
|
|
197
|
+
app.post("/api/projects", zValidator("json", ProjectCreateSchema), async (c) => {
|
|
198
|
+
const data = c.req.valid("json");
|
|
199
|
+
return c.json(await repo.createProject(data), 201);
|
|
200
|
+
});
|
|
201
|
+
app.put("/api/projects/:id", zValidator("json", ProjectUpdateSchema), async (c) => {
|
|
202
|
+
const id = c.req.param("id");
|
|
203
|
+
const data = c.req.valid("json");
|
|
204
|
+
const existing = await repo.getById(id);
|
|
205
|
+
if (!existing) throw new NotFoundError("project", id);
|
|
206
|
+
if (data.status === "archived" && existing.key === DEFAULT_PROJECT_KEY) {
|
|
207
|
+
throw new ConflictError("project", id, {
|
|
208
|
+
bizCode: "DEFAULT_PROJECT_IMMUTABLE",
|
|
209
|
+
message: BIZ_CODE_MESSAGES.DEFAULT_PROJECT_IMMUTABLE
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (data.name && data.name !== existing.name) {
|
|
213
|
+
const dup = await repo._collection.findOne({ name: data.name }, { projection: { _id: 1 } });
|
|
214
|
+
if (dup) throw new ConflictError("project", `name conflict: ${data.name}`);
|
|
215
|
+
}
|
|
216
|
+
const patch = {};
|
|
217
|
+
if (data.name !== void 0) patch.name = data.name;
|
|
218
|
+
if (data.description !== void 0) patch.description = data.description;
|
|
219
|
+
if (data.status !== void 0) patch.status = data.status;
|
|
220
|
+
const project = await repo.update(id, patch);
|
|
221
|
+
if (!project) throw new NotFoundError("project", id);
|
|
222
|
+
return c.json(project);
|
|
223
|
+
});
|
|
224
|
+
return app;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/routes/skill.routes.ts
|
|
228
|
+
import { Hono as Hono4 } from "hono";
|
|
229
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
230
|
+
import { z } from "zod";
|
|
231
|
+
import {
|
|
232
|
+
createSkillRepo,
|
|
233
|
+
createProjectRepo as createProjectRepo2,
|
|
234
|
+
createEnumRegistryRepo,
|
|
235
|
+
SkillCreateSchema,
|
|
236
|
+
SkillUpdateSchema,
|
|
237
|
+
SkillCopySchema,
|
|
238
|
+
OBJECT_ID_HEX,
|
|
239
|
+
NotFoundError as NotFoundError2,
|
|
240
|
+
ConflictError as ConflictError2,
|
|
241
|
+
BadRequestError,
|
|
242
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES2
|
|
243
|
+
} from "@siming-org/core";
|
|
244
|
+
var SkillListQuerySchema = z.object({
|
|
245
|
+
projectId: z.string().optional(),
|
|
246
|
+
scope: z.enum(["global", "project"]).optional()
|
|
247
|
+
}).refine((q) => !(q.scope === "project" && !q.projectId), {
|
|
248
|
+
message: "scope=project requires projectId",
|
|
249
|
+
path: ["projectId"]
|
|
250
|
+
});
|
|
251
|
+
var SkillByNameQuerySchema = z.object({
|
|
252
|
+
scope: z.enum(["global", "project"]).optional(),
|
|
253
|
+
projectId: z.string().regex(OBJECT_ID_HEX).optional()
|
|
254
|
+
}).superRefine((q, ctx) => {
|
|
255
|
+
if (q.scope === "project" && !q.projectId) {
|
|
256
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "scope=project requires projectId", path: ["projectId"] });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
function createSkillRoutes(client) {
|
|
260
|
+
const app = new Hono4();
|
|
261
|
+
const repo = createSkillRepo(client.db());
|
|
262
|
+
const projectRepo = createProjectRepo2(client.db());
|
|
263
|
+
const enumRegistryRepo = createEnumRegistryRepo(client.db());
|
|
264
|
+
const assertCategoryValid = async (category) => {
|
|
265
|
+
const categories = await enumRegistryRepo.getEntries("skill_category");
|
|
266
|
+
if (!categories.some((e) => e.active && e.value === category)) {
|
|
267
|
+
const validValues = categories.filter((e) => e.active).map((e) => e.value).join(", ");
|
|
268
|
+
throw new BadRequestError(
|
|
269
|
+
`category '${category}' is not a valid active skill_category value. Valid values: ${validValues}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
app.get("/api/skills", zValidator2("query", SkillListQuerySchema), async (c) => {
|
|
274
|
+
const q = c.req.valid("query");
|
|
275
|
+
if (q.scope) {
|
|
276
|
+
return c.json(await repo.listSkillsByScopeFilter(q.scope, q.projectId));
|
|
277
|
+
}
|
|
278
|
+
return c.json(q.projectId ? await repo.listSkillsByScope(q.projectId) : await repo.list());
|
|
279
|
+
});
|
|
280
|
+
app.get("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), async (c) => {
|
|
281
|
+
const name = c.req.param("name");
|
|
282
|
+
const q = c.req.valid("query");
|
|
283
|
+
const skill = await resolveAssetByScope(repo, name, q);
|
|
284
|
+
if (!skill) throw new NotFoundError2("skill", name);
|
|
285
|
+
return c.json(skill);
|
|
286
|
+
});
|
|
287
|
+
app.post("/api/skills", zValidator2("json", SkillCreateSchema), async (c) => {
|
|
288
|
+
const data = c.req.valid("json");
|
|
289
|
+
await assertCategoryValid(data.category);
|
|
290
|
+
if (data.scope === "project") await loadProjectForWrite(projectRepo, data.projectId);
|
|
291
|
+
return c.json(await repo.createSkill(data), 201);
|
|
292
|
+
});
|
|
293
|
+
app.put("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", SkillUpdateSchema), async (c) => {
|
|
294
|
+
const name = c.req.param("name");
|
|
295
|
+
const data = c.req.valid("json");
|
|
296
|
+
const q = c.req.valid("query");
|
|
297
|
+
const existing = await resolveAssetByScope(repo, name, q);
|
|
298
|
+
if (!existing) throw new NotFoundError2("skill", name);
|
|
299
|
+
if (data.category !== void 0) await assertCategoryValid(data.category);
|
|
300
|
+
if (isScopeMutation(existing, data)) {
|
|
301
|
+
throw new ConflictError2("skill", name, {
|
|
302
|
+
bizCode: "SCOPE_IMMUTABLE",
|
|
303
|
+
message: BIZ_CODE_MESSAGES2.SCOPE_IMMUTABLE
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const effectiveProjectId = data.projectId ?? existing.projectId;
|
|
307
|
+
if ((data.scope ?? existing.scope) === "project" && effectiveProjectId) {
|
|
308
|
+
await loadProjectForWrite(projectRepo, effectiveProjectId);
|
|
309
|
+
}
|
|
310
|
+
const skill = await repo.updateByNameScoped(name, q.scope ?? "global", q.projectId, data);
|
|
311
|
+
if (!skill) throw new NotFoundError2("skill", name);
|
|
312
|
+
return c.json(skill);
|
|
313
|
+
});
|
|
314
|
+
app.post("/api/skills/:name/copy", zValidator2("query", SkillByNameQuerySchema), zValidator2("json", SkillCopySchema), async (c) => {
|
|
315
|
+
const name = c.req.param("name");
|
|
316
|
+
const input = c.req.valid("json");
|
|
317
|
+
const q = c.req.valid("query");
|
|
318
|
+
const existing = await resolveAssetByScope(repo, name, q);
|
|
319
|
+
if (!existing) throw new NotFoundError2("skill", name);
|
|
320
|
+
if (input.newScope === "project") {
|
|
321
|
+
await loadProjectForWrite(projectRepo, input.targetProjectId);
|
|
322
|
+
}
|
|
323
|
+
const copy = await repo.copySkill(
|
|
324
|
+
name,
|
|
325
|
+
{ scope: q.scope ?? "global", ...q.projectId ? { projectId: q.projectId } : {} },
|
|
326
|
+
{ scope: input.newScope, ...input.targetProjectId ? { projectId: input.targetProjectId } : {} },
|
|
327
|
+
input.newName
|
|
328
|
+
);
|
|
329
|
+
return c.json(copy, 201);
|
|
330
|
+
});
|
|
331
|
+
app.delete("/api/skills/:name", zValidator2("query", SkillByNameQuerySchema), async (c) => {
|
|
332
|
+
const name = c.req.param("name");
|
|
333
|
+
const q = c.req.valid("query");
|
|
334
|
+
const existing = await resolveAssetByScope(repo, name, q);
|
|
335
|
+
if (!existing) throw new NotFoundError2("skill", name);
|
|
336
|
+
const deleted = await repo.deleteByNameScoped(name, q.scope ?? "global", q.projectId);
|
|
337
|
+
if (!deleted) throw new NotFoundError2("skill", name);
|
|
338
|
+
return c.body(null, 204);
|
|
339
|
+
});
|
|
340
|
+
return app;
|
|
341
|
+
}
|
|
342
|
+
function isScopeMutation(existing, patch) {
|
|
343
|
+
if (patch.scope !== void 0 && patch.scope !== existing.scope) return true;
|
|
344
|
+
if (patch.projectId !== void 0 && patch.projectId !== existing.projectId) return true;
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
async function resolveAssetByScope(repo, name, q) {
|
|
348
|
+
return repo.getByNameScoped(name, q.scope ?? "global", q.projectId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/routes/agent.routes.ts
|
|
352
|
+
import { Hono as Hono5 } from "hono";
|
|
353
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
354
|
+
import { z as z2 } from "zod";
|
|
355
|
+
import {
|
|
356
|
+
createAgentRepo,
|
|
357
|
+
createSkillRepo as createSkillRepo2,
|
|
358
|
+
createProjectRepo as createProjectRepo3,
|
|
359
|
+
createModelAliasRepo,
|
|
360
|
+
AgentCreateSchema,
|
|
361
|
+
AgentUpdateSchema,
|
|
362
|
+
AgentCopySchema,
|
|
363
|
+
NotFoundError as NotFoundError3,
|
|
364
|
+
ConflictError as ConflictError3,
|
|
365
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES3,
|
|
366
|
+
assertBoundSkillsCompatible,
|
|
367
|
+
assertModelAliasExists
|
|
368
|
+
} from "@siming-org/core";
|
|
369
|
+
var AgentListQuerySchema = z2.object({
|
|
370
|
+
projectId: z2.string().optional(),
|
|
371
|
+
scope: z2.enum(["global", "project"]).optional()
|
|
372
|
+
}).refine((q) => !(q.scope === "project" && !q.projectId), {
|
|
373
|
+
message: "scope=project requires projectId",
|
|
374
|
+
path: ["projectId"]
|
|
375
|
+
});
|
|
376
|
+
function createAgentRoutes(client) {
|
|
377
|
+
const app = new Hono5();
|
|
378
|
+
const repo = createAgentRepo(client.db());
|
|
379
|
+
const skillRepo = createSkillRepo2(client.db());
|
|
380
|
+
const projectRepo = createProjectRepo3(client.db());
|
|
381
|
+
const aliasRepo = createModelAliasRepo(client.db());
|
|
382
|
+
app.get("/api/agents", zValidator3("query", AgentListQuerySchema), async (c) => {
|
|
383
|
+
const q = c.req.valid("query");
|
|
384
|
+
if (q.scope) {
|
|
385
|
+
return c.json(await repo.listAgentsByScopeFilter(q.scope, q.projectId));
|
|
386
|
+
}
|
|
387
|
+
return c.json(q.projectId ? await repo.listAgentsByScope(q.projectId) : await repo.list());
|
|
388
|
+
});
|
|
389
|
+
app.get("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), async (c) => {
|
|
390
|
+
const name = c.req.param("name");
|
|
391
|
+
const q = c.req.valid("query");
|
|
392
|
+
const agent = await resolveAssetByScope(repo, name, q);
|
|
393
|
+
if (!agent) throw new NotFoundError3("agent", name);
|
|
394
|
+
return c.json(agent);
|
|
395
|
+
});
|
|
396
|
+
app.post("/api/agents", zValidator3("json", AgentCreateSchema), async (c) => {
|
|
397
|
+
const data = c.req.valid("json");
|
|
398
|
+
if (data.scope === "project") await loadProjectForWrite(projectRepo, data.projectId);
|
|
399
|
+
await assertBoundSkillsCompatible(skillRepo, data.scope, data.projectId, data.boundSkills);
|
|
400
|
+
await assertModelAliasExists(aliasRepo, data.model);
|
|
401
|
+
return c.json(await repo.createAgent(data), 201);
|
|
402
|
+
});
|
|
403
|
+
app.put("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AgentUpdateSchema), async (c) => {
|
|
404
|
+
const name = c.req.param("name");
|
|
405
|
+
const data = c.req.valid("json");
|
|
406
|
+
const q = c.req.valid("query");
|
|
407
|
+
const existing = await resolveAssetByScope(repo, name, q);
|
|
408
|
+
if (!existing) throw new NotFoundError3("agent", name);
|
|
409
|
+
if (isScopeMutation(existing, data)) {
|
|
410
|
+
throw new ConflictError3("agent", name, {
|
|
411
|
+
bizCode: "SCOPE_IMMUTABLE",
|
|
412
|
+
message: BIZ_CODE_MESSAGES3.SCOPE_IMMUTABLE
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const effectiveProjectId = data.projectId ?? existing.projectId;
|
|
416
|
+
if ((data.scope ?? existing.scope) === "project" && effectiveProjectId) {
|
|
417
|
+
await loadProjectForWrite(projectRepo, effectiveProjectId);
|
|
418
|
+
}
|
|
419
|
+
if (data.boundSkills !== void 0) {
|
|
420
|
+
await assertBoundSkillsCompatible(
|
|
421
|
+
skillRepo,
|
|
422
|
+
existing.scope,
|
|
423
|
+
effectiveProjectId,
|
|
424
|
+
data.boundSkills
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (data.model !== void 0) {
|
|
428
|
+
await assertModelAliasExists(aliasRepo, data.model);
|
|
429
|
+
}
|
|
430
|
+
const agent = await repo.updateByNameScoped(name, q.scope ?? "global", q.projectId, data);
|
|
431
|
+
if (!agent) throw new NotFoundError3("agent", name);
|
|
432
|
+
return c.json(agent);
|
|
433
|
+
});
|
|
434
|
+
app.post("/api/agents/:name/copy", zValidator3("query", SkillByNameQuerySchema), zValidator3("json", AgentCopySchema), async (c) => {
|
|
435
|
+
const name = c.req.param("name");
|
|
436
|
+
const input = c.req.valid("json");
|
|
437
|
+
const q = c.req.valid("query");
|
|
438
|
+
const existing = await resolveAssetByScope(repo, name, q);
|
|
439
|
+
if (!existing) throw new NotFoundError3("agent", name);
|
|
440
|
+
if (input.newScope === "project") {
|
|
441
|
+
await loadProjectForWrite(projectRepo, input.targetProjectId);
|
|
442
|
+
}
|
|
443
|
+
const copy = await repo.copyAgent(
|
|
444
|
+
name,
|
|
445
|
+
{ scope: q.scope ?? "global", ...q.projectId ? { projectId: q.projectId } : {} },
|
|
446
|
+
{ scope: input.newScope, ...input.targetProjectId ? { projectId: input.targetProjectId } : {} },
|
|
447
|
+
input.newName,
|
|
448
|
+
skillRepo
|
|
449
|
+
);
|
|
450
|
+
return c.json(copy, 201);
|
|
451
|
+
});
|
|
452
|
+
app.delete("/api/agents/:name", zValidator3("query", SkillByNameQuerySchema), async (c) => {
|
|
453
|
+
const name = c.req.param("name");
|
|
454
|
+
const q = c.req.valid("query");
|
|
455
|
+
const existing = await resolveAssetByScope(repo, name, q);
|
|
456
|
+
if (!existing) throw new NotFoundError3("agent", name);
|
|
457
|
+
const deleted = await repo.deleteByNameScoped(name, q.scope ?? "global", q.projectId);
|
|
458
|
+
if (!deleted) throw new NotFoundError3("agent", name);
|
|
459
|
+
return c.body(null, 204);
|
|
460
|
+
});
|
|
461
|
+
return app;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/routes/model-alias.routes.ts
|
|
465
|
+
import { Hono as Hono6 } from "hono";
|
|
466
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
467
|
+
import {
|
|
468
|
+
createModelAliasRepo as createModelAliasRepo2,
|
|
469
|
+
ModelAliasCreateSchema,
|
|
470
|
+
ModelAliasUpdateSchema,
|
|
471
|
+
NotFoundError as NotFoundError4,
|
|
472
|
+
ConflictError as ConflictError4,
|
|
473
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES4
|
|
474
|
+
} from "@siming-org/core";
|
|
475
|
+
function createModelAliasRoutes(client) {
|
|
476
|
+
const app = new Hono6();
|
|
477
|
+
const repo = createModelAliasRepo2(client.db());
|
|
478
|
+
app.get("/api/model-aliases", async (c) => {
|
|
479
|
+
return c.json(await repo.listWithRefCount());
|
|
480
|
+
});
|
|
481
|
+
app.post("/api/model-aliases", zValidator4("json", ModelAliasCreateSchema), async (c) => {
|
|
482
|
+
const data = c.req.valid("json");
|
|
483
|
+
const dup = await repo.getByCode(data.code);
|
|
484
|
+
if (dup) {
|
|
485
|
+
throw new ConflictError4("model-alias", data.code, {
|
|
486
|
+
bizCode: "MODEL_CODE_EXISTS",
|
|
487
|
+
message: BIZ_CODE_MESSAGES4.MODEL_CODE_EXISTS
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return c.json(await repo.createModelAlias(data), 201);
|
|
491
|
+
});
|
|
492
|
+
app.put("/api/model-aliases/:code", zValidator4("json", ModelAliasUpdateSchema), async (c) => {
|
|
493
|
+
const code = c.req.param("code");
|
|
494
|
+
const data = c.req.valid("json");
|
|
495
|
+
const existing = await repo.getByCode(code);
|
|
496
|
+
if (!existing) throw new NotFoundError4("model-alias", code);
|
|
497
|
+
if (data.code !== void 0 && data.code !== code) {
|
|
498
|
+
throw new ConflictError4("model-alias", code, {
|
|
499
|
+
bizCode: "MODEL_CODE_IMMUTABLE",
|
|
500
|
+
message: BIZ_CODE_MESSAGES4.MODEL_CODE_IMMUTABLE
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
const patch = {};
|
|
504
|
+
if (data.name !== void 0) patch.name = data.name;
|
|
505
|
+
if (data.realModel !== void 0) patch.realModel = data.realModel;
|
|
506
|
+
const alias = await repo.updateByCode(code, patch);
|
|
507
|
+
if (!alias) throw new NotFoundError4("model-alias", code);
|
|
508
|
+
return c.json(alias);
|
|
509
|
+
});
|
|
510
|
+
app.delete("/api/model-aliases/:code", async (c) => {
|
|
511
|
+
const code = c.req.param("code");
|
|
512
|
+
const deleted = await repo.deleteByCode(code);
|
|
513
|
+
if (!deleted) throw new NotFoundError4("model-alias", code);
|
|
514
|
+
return c.body(null, 204);
|
|
515
|
+
});
|
|
516
|
+
return app;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/routes/dag-template.routes.ts
|
|
520
|
+
import { Hono as Hono7 } from "hono";
|
|
521
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
522
|
+
import { z as z3 } from "zod";
|
|
523
|
+
import {
|
|
524
|
+
createDagTemplateRepo,
|
|
525
|
+
createProjectRepo as createProjectRepo4,
|
|
526
|
+
DagTemplateCreateSchema,
|
|
527
|
+
DagTemplateCopySchema,
|
|
528
|
+
DagTemplateUpdateSchema,
|
|
529
|
+
NotFoundError as NotFoundError5,
|
|
530
|
+
ConflictError as ConflictError5,
|
|
531
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES5
|
|
532
|
+
} from "@siming-org/core";
|
|
533
|
+
var DagTemplateListQuerySchema = z3.object({
|
|
534
|
+
projectId: z3.string().optional(),
|
|
535
|
+
name: z3.string().optional()
|
|
536
|
+
});
|
|
537
|
+
function createDagTemplateRoutes(client) {
|
|
538
|
+
const app = new Hono7();
|
|
539
|
+
const repo = createDagTemplateRepo(client.db());
|
|
540
|
+
const projectRepo = createProjectRepo4(client.db());
|
|
541
|
+
app.get("/api/dag/templates", zValidator5("query", DagTemplateListQuerySchema), async (c) => {
|
|
542
|
+
const q = c.req.valid("query");
|
|
543
|
+
return c.json(
|
|
544
|
+
await repo.listDagTemplates({
|
|
545
|
+
...q.projectId ? { projectId: q.projectId } : {},
|
|
546
|
+
...q.name ? { name: q.name } : {}
|
|
547
|
+
})
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
app.get("/api/dag/templates/:id", async (c) => {
|
|
551
|
+
const id = c.req.param("id");
|
|
552
|
+
const template = await repo.getById(id);
|
|
553
|
+
if (!template) throw new NotFoundError5("dag-template", id);
|
|
554
|
+
return c.json(template);
|
|
555
|
+
});
|
|
556
|
+
app.post("/api/dag/templates", zValidator5("json", DagTemplateCreateSchema), async (c) => {
|
|
557
|
+
const data = c.req.valid("json");
|
|
558
|
+
await loadProjectForWrite(projectRepo, data.projectId);
|
|
559
|
+
return c.json(await repo.createDagTemplate(data), 201);
|
|
560
|
+
});
|
|
561
|
+
app.put("/api/dag/templates/:id", zValidator5("json", DagTemplateUpdateSchema), async (c) => {
|
|
562
|
+
const id = c.req.param("id");
|
|
563
|
+
const data = c.req.valid("json");
|
|
564
|
+
const existing = await repo.getById(id);
|
|
565
|
+
if (!existing) throw new NotFoundError5("dag-template", id);
|
|
566
|
+
if (data.projectId !== void 0 && data.projectId !== existing.projectId) {
|
|
567
|
+
throw new ConflictError5("dag-template", id, {
|
|
568
|
+
bizCode: "TEMPLATE_PROJECT_IMMUTABLE",
|
|
569
|
+
message: BIZ_CODE_MESSAGES5.TEMPLATE_PROJECT_IMMUTABLE
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
await loadProjectForWrite(projectRepo, existing.projectId);
|
|
573
|
+
if (data.name !== void 0 && data.name !== existing.name) {
|
|
574
|
+
const dup = await repo._collection.findOne(
|
|
575
|
+
{ projectId: existing.projectId, name: data.name },
|
|
576
|
+
{ projection: { _id: 1 } }
|
|
577
|
+
);
|
|
578
|
+
if (dup) {
|
|
579
|
+
throw new ConflictError5("dag-template", `name conflict: ${data.name}`, {
|
|
580
|
+
bizCode: "TEMPLATE_NAME_CONFLICT",
|
|
581
|
+
message: BIZ_CODE_MESSAGES5.TEMPLATE_NAME_CONFLICT
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const template = await repo.update(id, data);
|
|
586
|
+
if (!template) throw new NotFoundError5("dag-template", id);
|
|
587
|
+
return c.json(template);
|
|
588
|
+
});
|
|
589
|
+
app.post("/api/dag/templates/:id/copy", zValidator5("json", DagTemplateCopySchema), async (c) => {
|
|
590
|
+
const id = c.req.param("id");
|
|
591
|
+
const input = c.req.valid("json");
|
|
592
|
+
const existing = await repo.getById(id);
|
|
593
|
+
if (!existing) throw new NotFoundError5("dag-template", id);
|
|
594
|
+
await loadProjectForWrite(projectRepo, input.targetProjectId);
|
|
595
|
+
const dup = await repo._collection.findOne(
|
|
596
|
+
{ projectId: input.targetProjectId, name: input.newName },
|
|
597
|
+
{ projection: { _id: 1 } }
|
|
598
|
+
);
|
|
599
|
+
if (dup) {
|
|
600
|
+
throw new ConflictError5("dag-template", `name conflict: ${input.newName}`, {
|
|
601
|
+
bizCode: "TEMPLATE_NAME_CONFLICT",
|
|
602
|
+
message: BIZ_CODE_MESSAGES5.TEMPLATE_NAME_CONFLICT
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const copy = await repo.copyDagTemplate(id, input.targetProjectId, input.newName);
|
|
606
|
+
return c.json(copy, 201);
|
|
607
|
+
});
|
|
608
|
+
app.delete("/api/dag/templates/:id", async (c) => {
|
|
609
|
+
const id = c.req.param("id");
|
|
610
|
+
const deleted = await repo.delete(id);
|
|
611
|
+
if (!deleted) throw new NotFoundError5("dag-template", id);
|
|
612
|
+
return c.body(null, 204);
|
|
613
|
+
});
|
|
614
|
+
return app;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/routes/task.routes.ts
|
|
618
|
+
import { Hono as Hono8 } from "hono";
|
|
619
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
620
|
+
import { z as z4 } from "zod";
|
|
621
|
+
import {
|
|
622
|
+
createTaskRepo,
|
|
623
|
+
createDagTemplateRepo as createDagTemplateRepo2,
|
|
624
|
+
createProjectRepo as createProjectRepo5,
|
|
625
|
+
TaskCreateInputSchema,
|
|
626
|
+
AdvanceRequestSchema,
|
|
627
|
+
ApproveRequestSchema,
|
|
628
|
+
PauseRequestSchema,
|
|
629
|
+
ResumeRequestSchema,
|
|
630
|
+
ARTIFACT_TYPES,
|
|
631
|
+
NODE_ID_PATTERN,
|
|
632
|
+
createEnumRegistryRepo as createEnumRegistryRepo2,
|
|
633
|
+
NotFoundError as NotFoundError6,
|
|
634
|
+
ConflictError as ConflictError6,
|
|
635
|
+
BadRequestError as BadRequestError2,
|
|
636
|
+
ValidationError,
|
|
637
|
+
BIZ_CODE_MESSAGES as BIZ_CODE_MESSAGES6,
|
|
638
|
+
advanceTask,
|
|
639
|
+
approveTask,
|
|
640
|
+
pauseTask,
|
|
641
|
+
resumeTask,
|
|
642
|
+
renderPrompt,
|
|
643
|
+
toNodeInfo,
|
|
644
|
+
toTaskPublic,
|
|
645
|
+
findNextEdge,
|
|
646
|
+
generateEntryId
|
|
647
|
+
} from "@siming-org/core";
|
|
648
|
+
var TaskListQuerySchema = z4.object({
|
|
649
|
+
status: z4.string().optional(),
|
|
650
|
+
track: z4.string().optional(),
|
|
651
|
+
projectId: z4.string().optional(),
|
|
652
|
+
page: z4.coerce.number().int().min(1).default(1),
|
|
653
|
+
limit: z4.coerce.number().int().min(1).max(500).default(20),
|
|
654
|
+
sort: z4.enum(["progress", "createdAt", "updatedAt"]).optional()
|
|
655
|
+
});
|
|
656
|
+
var TaskPatchSchema = z4.object({
|
|
657
|
+
title: z4.string().min(1).optional()
|
|
658
|
+
});
|
|
659
|
+
var TaskDocSetSchema = z4.object({
|
|
660
|
+
what: z4.string().min(1).max(2e3).optional(),
|
|
661
|
+
why: z4.string().min(1).max(2e3).optional(),
|
|
662
|
+
trackNote: z4.string().max(2e3).optional()
|
|
663
|
+
});
|
|
664
|
+
var TextItemSchema = z4.object({ text: z4.string().min(1).max(2e3) });
|
|
665
|
+
var RecordSummarySchema = z4.object({ summary: z4.string().min(1).max(2e3) });
|
|
666
|
+
var CheckAddSchema = z4.object({ item: z4.string().min(1).max(2e3), passed: z4.boolean().optional() });
|
|
667
|
+
var CheckPatchSchema = z4.object({ passed: z4.boolean() });
|
|
668
|
+
var ArtifactAddSchema = z4.object({
|
|
669
|
+
type: z4.enum(ARTIFACT_TYPES),
|
|
670
|
+
path: z4.string().min(1).max(2e3),
|
|
671
|
+
note: z4.string().max(2e3).optional(),
|
|
672
|
+
/** 全文快照(CLI --file 读文件后传入;≤200k——续跑会话凭 context 自足) */
|
|
673
|
+
content: z4.string().max(2e5).optional()
|
|
674
|
+
});
|
|
675
|
+
var ConfirmAddSchema = z4.object({ quote: z4.string().min(1).max(2e3) });
|
|
676
|
+
var DecisionAddSchema = z4.object({
|
|
677
|
+
topic: z4.string().min(1).max(2e3),
|
|
678
|
+
decision: z4.string().min(1).max(2e3)
|
|
679
|
+
});
|
|
680
|
+
var ReviewSetSchema = z4.object({
|
|
681
|
+
verdict: z4.enum(["pass", "fail"]),
|
|
682
|
+
rounds: z4.number().int().min(1),
|
|
683
|
+
critical: z4.number().int().min(0)
|
|
684
|
+
});
|
|
685
|
+
function normalizeTaskDoc(doc) {
|
|
686
|
+
if (!doc) return null;
|
|
687
|
+
return { ...doc, acceptance: doc.acceptance ?? [], nonGoals: doc.nonGoals ?? [] };
|
|
688
|
+
}
|
|
689
|
+
function normalizeNodeRecords(records) {
|
|
690
|
+
const src = records ?? {};
|
|
691
|
+
const out = {};
|
|
692
|
+
for (const [nodeId, record] of Object.entries(src)) {
|
|
693
|
+
if (!record) continue;
|
|
694
|
+
out[nodeId] = {
|
|
695
|
+
...record,
|
|
696
|
+
checks: record.checks ?? [],
|
|
697
|
+
artifacts: record.artifacts ?? [],
|
|
698
|
+
confirmations: record.confirmations ?? [],
|
|
699
|
+
decisions: record.decisions ?? []
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
function createTaskRoutes(client) {
|
|
705
|
+
const app = new Hono8();
|
|
706
|
+
const repo = createTaskRepo(client.db());
|
|
707
|
+
const templateRepo = createDagTemplateRepo2(client.db());
|
|
708
|
+
const projectRepo = createProjectRepo5(client.db());
|
|
709
|
+
const enumRegistryRepo = createEnumRegistryRepo2(client.db());
|
|
710
|
+
app.get("/api/tasks", zValidator6("query", TaskListQuerySchema), async (c) => {
|
|
711
|
+
const q = c.req.valid("query");
|
|
712
|
+
const { items, total } = await repo.listTasks({
|
|
713
|
+
...q.status ? { status: q.status } : {},
|
|
714
|
+
...q.track ? { track: q.track } : {},
|
|
715
|
+
...q.projectId ? { projectId: q.projectId } : {},
|
|
716
|
+
page: q.page,
|
|
717
|
+
limit: q.limit,
|
|
718
|
+
...q.sort ? { sort: q.sort } : {}
|
|
719
|
+
});
|
|
720
|
+
return c.json({ items, total, page: q.page, limit: q.limit });
|
|
721
|
+
});
|
|
722
|
+
app.get("/api/tasks/:taskId", async (c) => {
|
|
723
|
+
const taskId = c.req.param("taskId");
|
|
724
|
+
const task = await repo.getByTaskId(taskId);
|
|
725
|
+
if (!task) throw new NotFoundError6("task", taskId);
|
|
726
|
+
return c.json(task);
|
|
727
|
+
});
|
|
728
|
+
app.post("/api/tasks", zValidator6("json", TaskCreateInputSchema), async (c) => {
|
|
729
|
+
const input = c.req.valid("json");
|
|
730
|
+
const dagTracks = (await enumRegistryRepo.getEntries("dag_track")).filter(
|
|
731
|
+
(e) => e.active && e.value !== "all"
|
|
732
|
+
);
|
|
733
|
+
if (!dagTracks.some((e) => e.value === input.track)) {
|
|
734
|
+
const validValues = dagTracks.map((e) => e.value).join(", ");
|
|
735
|
+
throw new BadRequestError2(
|
|
736
|
+
`track '${input.track}' is not a valid active dag_track value. Valid values: ${validValues}`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
assertTypeTrackCompatible(input.type, input.track);
|
|
740
|
+
const template = await templateRepo.getById(input.dagTemplateId);
|
|
741
|
+
if (!template) throw new NotFoundError6("dag-template", input.dagTemplateId);
|
|
742
|
+
if (input.projectId && input.projectId !== template.projectId) {
|
|
743
|
+
throw new ConflictError6("task", input.dagTemplateId, {
|
|
744
|
+
bizCode: "TEMPLATE_PROJECT_MISMATCH",
|
|
745
|
+
message: BIZ_CODE_MESSAGES6.TEMPLATE_PROJECT_MISMATCH
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
const resolvedProjectId = input.projectId ?? template.projectId;
|
|
749
|
+
await loadProjectForWrite(projectRepo, resolvedProjectId);
|
|
750
|
+
return c.json(await repo.createTask(input, template, resolvedProjectId), 201);
|
|
751
|
+
});
|
|
752
|
+
app.patch("/api/tasks/:taskId", zValidator6("json", TaskPatchSchema), async (c) => {
|
|
753
|
+
const taskId = c.req.param("taskId");
|
|
754
|
+
const body = c.req.valid("json");
|
|
755
|
+
await assertTaskProjectActive(repo, projectRepo, taskId);
|
|
756
|
+
const patch = {
|
|
757
|
+
...body.title !== void 0 ? { title: body.title } : {}
|
|
758
|
+
};
|
|
759
|
+
return c.json(await repo.updateTask(taskId, patch));
|
|
760
|
+
});
|
|
761
|
+
app.post("/api/tasks/:taskId/advance", zValidator6("json", AdvanceRequestSchema), async (c) => {
|
|
762
|
+
const taskId = c.req.param("taskId");
|
|
763
|
+
const body = c.req.valid("json");
|
|
764
|
+
await assertTaskProjectActive(repo, projectRepo, taskId);
|
|
765
|
+
const deps = mkDeps(repo);
|
|
766
|
+
const result = await advanceTask(deps, taskId, {
|
|
767
|
+
...body.note !== void 0 ? { note: body.note } : {},
|
|
768
|
+
...body.summary !== void 0 ? { summary: body.summary } : {}
|
|
769
|
+
});
|
|
770
|
+
return c.json(result, 200);
|
|
771
|
+
});
|
|
772
|
+
app.post("/api/tasks/:taskId/approve", zValidator6("json", ApproveRequestSchema), async (c) => {
|
|
773
|
+
const taskId = c.req.param("taskId");
|
|
774
|
+
const body = c.req.valid("json");
|
|
775
|
+
await assertTaskProjectActive(repo, projectRepo, taskId);
|
|
776
|
+
const deps = mkDeps(repo);
|
|
777
|
+
const result = await approveTask(deps, taskId, body);
|
|
778
|
+
return c.json(result, 200);
|
|
779
|
+
});
|
|
780
|
+
app.post("/api/tasks/:taskId/pause", zValidator6("json", PauseRequestSchema), async (c) => {
|
|
781
|
+
const taskId = c.req.param("taskId");
|
|
782
|
+
const body = c.req.valid("json");
|
|
783
|
+
await assertTaskProjectActive(repo, projectRepo, taskId);
|
|
784
|
+
const result = await pauseTask(mkDeps(repo), taskId, body.reason);
|
|
785
|
+
return c.json(result, 200);
|
|
786
|
+
});
|
|
787
|
+
app.post("/api/tasks/:taskId/resume", zValidator6("json", ResumeRequestSchema), async (c) => {
|
|
788
|
+
const taskId = c.req.param("taskId");
|
|
789
|
+
const body = c.req.valid("json");
|
|
790
|
+
await assertTaskProjectActive(repo, projectRepo, taskId);
|
|
791
|
+
const result = await resumeTask(mkDeps(repo), taskId, body.decision);
|
|
792
|
+
return c.json(result, 200);
|
|
793
|
+
});
|
|
794
|
+
app.get("/api/tasks/:taskId/history", async (c) => {
|
|
795
|
+
const taskId = c.req.param("taskId");
|
|
796
|
+
const task = await repo.getByTaskId(taskId);
|
|
797
|
+
if (!task) throw new NotFoundError6("task", taskId);
|
|
798
|
+
return c.json(task.history);
|
|
799
|
+
});
|
|
800
|
+
app.get("/api/tasks/:taskId/node/:nodeId", async (c) => {
|
|
801
|
+
const taskId = c.req.param("taskId");
|
|
802
|
+
const nodeId = c.req.param("nodeId");
|
|
803
|
+
const task = await repo.getByTaskId(taskId);
|
|
804
|
+
if (!task) throw new NotFoundError6("task", taskId);
|
|
805
|
+
const node = task.dagInstance.nodes.find((n) => n.id === nodeId);
|
|
806
|
+
if (!node) throw new NotFoundError6("node", nodeId);
|
|
807
|
+
return c.json({ ...node, prompt: renderPrompt(node.prompt, task) });
|
|
808
|
+
});
|
|
809
|
+
app.get("/api/tasks/:taskId/context", async (c) => {
|
|
810
|
+
const taskId = c.req.param("taskId");
|
|
811
|
+
const task = await repo.getByTaskId(taskId);
|
|
812
|
+
if (!task) throw new NotFoundError6("task", taskId);
|
|
813
|
+
const currentNodeActive = task.status === "active" ? task.dagInstance.nodes.find((n) => n.id === task.currentNode) : void 0;
|
|
814
|
+
let pausedAtEdge = null;
|
|
815
|
+
if (task.status === "paused" && task.pausedAt !== null) {
|
|
816
|
+
const edge = findNextEdge(task.dagInstance.edges, task.dagInstance.nodes, task.pausedAt, task.track);
|
|
817
|
+
if (edge?.pausePoint) {
|
|
818
|
+
pausedAtEdge = { from: edge.from, to: edge.to, pausePoint: edge.pausePoint };
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const context = {
|
|
822
|
+
task: toTaskPublic(task),
|
|
823
|
+
currentNode: currentNodeActive ? {
|
|
824
|
+
...toNodeInfo(currentNodeActive, task),
|
|
825
|
+
phase: currentNodeActive.phase
|
|
826
|
+
} : null,
|
|
827
|
+
pausedAtEdge,
|
|
828
|
+
nodes: task.dagInstance.nodes.map((n) => {
|
|
829
|
+
const state = task.dagInstance.nodeStates[n.id];
|
|
830
|
+
return {
|
|
831
|
+
nodeId: n.id,
|
|
832
|
+
label: n.label,
|
|
833
|
+
status: state?.status ?? "pending",
|
|
834
|
+
enteredAt: state?.enteredAt ?? null,
|
|
835
|
+
completedAt: state?.completedAt ?? null
|
|
836
|
+
};
|
|
837
|
+
}),
|
|
838
|
+
// N020 D1:结构化全景(断点续跑数据源);?? 容错迁移前旧文档与小步写入的部分形态
|
|
839
|
+
// (toEntity 不跑 parse 补 default——点路径 $set/$push 只写触及字段,record/doc 内数组
|
|
840
|
+
// 可能缺键,读侧按字段归一到 schema 目标形状,Web/CLI 消费方拿到的恒为完整形)
|
|
841
|
+
taskDoc: normalizeTaskDoc(task.doc),
|
|
842
|
+
nodeRecords: normalizeNodeRecords(task.nodeRecords),
|
|
843
|
+
archNotes: task.archNotes ?? []
|
|
844
|
+
};
|
|
845
|
+
return c.json(context);
|
|
846
|
+
});
|
|
847
|
+
app.patch("/api/tasks/:taskId/doc", zValidator6("json", TaskDocSetSchema), async (c) => {
|
|
848
|
+
const taskId = c.req.param("taskId");
|
|
849
|
+
const body = c.req.valid("json");
|
|
850
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId);
|
|
851
|
+
const sets = {};
|
|
852
|
+
if (body.what !== void 0) sets["doc.what"] = body.what;
|
|
853
|
+
if (body.why !== void 0) sets["doc.why"] = body.why;
|
|
854
|
+
if (body.trackNote !== void 0) sets["doc.trackNote"] = body.trackNote;
|
|
855
|
+
return c.json(await repo.updateTaskPaths(taskId, { sets }));
|
|
856
|
+
});
|
|
857
|
+
app.post("/api/tasks/:taskId/doc/acceptance", zValidator6("json", TextItemSchema), async (c) => {
|
|
858
|
+
const taskId = c.req.param("taskId");
|
|
859
|
+
const body = c.req.valid("json");
|
|
860
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId);
|
|
861
|
+
return c.json(await repo.updateTaskPaths(taskId, { pushes: { "doc.acceptance": body.text } }));
|
|
862
|
+
});
|
|
863
|
+
app.post("/api/tasks/:taskId/doc/non-goal", zValidator6("json", TextItemSchema), async (c) => {
|
|
864
|
+
const taskId = c.req.param("taskId");
|
|
865
|
+
const body = c.req.valid("json");
|
|
866
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId);
|
|
867
|
+
return c.json(await repo.updateTaskPaths(taskId, { pushes: { "doc.nonGoals": body.text } }));
|
|
868
|
+
});
|
|
869
|
+
app.patch("/api/tasks/:taskId/records/:nodeId/summary", zValidator6("json", RecordSummarySchema), async (c) => {
|
|
870
|
+
const taskId = c.req.param("taskId");
|
|
871
|
+
const nodeId = c.req.param("nodeId");
|
|
872
|
+
const body = c.req.valid("json");
|
|
873
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
874
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
875
|
+
sets: { [`nodeRecords.${nodeId}.summary`]: body.summary }
|
|
876
|
+
}));
|
|
877
|
+
});
|
|
878
|
+
app.post("/api/tasks/:taskId/records/:nodeId/checks", zValidator6("json", CheckAddSchema), async (c) => {
|
|
879
|
+
const taskId = c.req.param("taskId");
|
|
880
|
+
const nodeId = c.req.param("nodeId");
|
|
881
|
+
const body = c.req.valid("json");
|
|
882
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
883
|
+
const existing = (task.nodeRecords ?? {})[nodeId]?.checks ?? [];
|
|
884
|
+
const check = {
|
|
885
|
+
id: generateEntryId(existing.map((ch) => ch.id)),
|
|
886
|
+
item: body.item,
|
|
887
|
+
passed: body.passed ?? true
|
|
888
|
+
};
|
|
889
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
890
|
+
pushes: { [`nodeRecords.${nodeId}.checks`]: check }
|
|
891
|
+
}));
|
|
892
|
+
});
|
|
893
|
+
app.patch("/api/tasks/:taskId/records/:nodeId/checks/:checkId", zValidator6("json", CheckPatchSchema), async (c) => {
|
|
894
|
+
const taskId = c.req.param("taskId");
|
|
895
|
+
const nodeId = c.req.param("nodeId");
|
|
896
|
+
const checkId = c.req.param("checkId");
|
|
897
|
+
const body = c.req.valid("json");
|
|
898
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
899
|
+
const check = ((task.nodeRecords ?? {})[nodeId]?.checks ?? []).find((ch) => ch.id === checkId);
|
|
900
|
+
if (!check) {
|
|
901
|
+
throw new NotFoundError6("check", checkId, {
|
|
902
|
+
bizCode: "CHECK_NOT_FOUND",
|
|
903
|
+
message: `${BIZ_CODE_MESSAGES6.CHECK_NOT_FOUND}\uFF08node ${nodeId}, check ${checkId}\uFF09`
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
907
|
+
sets: { [`nodeRecords.${nodeId}.checks.$[e].passed`]: body.passed },
|
|
908
|
+
arrayFilters: [{ "e.id": checkId }]
|
|
909
|
+
}));
|
|
910
|
+
});
|
|
911
|
+
app.post("/api/tasks/:taskId/records/:nodeId/artifacts", zValidator6("json", ArtifactAddSchema), async (c) => {
|
|
912
|
+
const taskId = c.req.param("taskId");
|
|
913
|
+
const nodeId = c.req.param("nodeId");
|
|
914
|
+
const body = c.req.valid("json");
|
|
915
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
916
|
+
const artifact = {
|
|
917
|
+
id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.artifacts ?? []).map((a) => a.id)),
|
|
918
|
+
type: body.type,
|
|
919
|
+
path: body.path,
|
|
920
|
+
...body.note !== void 0 ? { note: body.note } : {},
|
|
921
|
+
...body.content !== void 0 ? { content: body.content } : {}
|
|
922
|
+
};
|
|
923
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
924
|
+
pushes: { [`nodeRecords.${nodeId}.artifacts`]: artifact }
|
|
925
|
+
}));
|
|
926
|
+
});
|
|
927
|
+
app.post("/api/tasks/:taskId/records/:nodeId/confirmations", zValidator6("json", ConfirmAddSchema), async (c) => {
|
|
928
|
+
const taskId = c.req.param("taskId");
|
|
929
|
+
const nodeId = c.req.param("nodeId");
|
|
930
|
+
const body = c.req.valid("json");
|
|
931
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
932
|
+
const confirmation = {
|
|
933
|
+
id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.confirmations ?? []).map((cf) => cf.id)),
|
|
934
|
+
quote: body.quote,
|
|
935
|
+
at: /* @__PURE__ */ new Date()
|
|
936
|
+
};
|
|
937
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
938
|
+
pushes: { [`nodeRecords.${nodeId}.confirmations`]: confirmation }
|
|
939
|
+
}));
|
|
940
|
+
});
|
|
941
|
+
app.post("/api/tasks/:taskId/records/:nodeId/decisions", zValidator6("json", DecisionAddSchema), async (c) => {
|
|
942
|
+
const taskId = c.req.param("taskId");
|
|
943
|
+
const nodeId = c.req.param("nodeId");
|
|
944
|
+
const body = c.req.valid("json");
|
|
945
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
946
|
+
const decision = {
|
|
947
|
+
id: generateEntryId(((task.nodeRecords ?? {})[nodeId]?.decisions ?? []).map((d) => d.id)),
|
|
948
|
+
topic: body.topic,
|
|
949
|
+
decision: body.decision
|
|
950
|
+
};
|
|
951
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
952
|
+
pushes: { [`nodeRecords.${nodeId}.decisions`]: decision }
|
|
953
|
+
}));
|
|
954
|
+
});
|
|
955
|
+
app.put("/api/tasks/:taskId/records/:nodeId/review", zValidator6("json", ReviewSetSchema), async (c) => {
|
|
956
|
+
const taskId = c.req.param("taskId");
|
|
957
|
+
const nodeId = c.req.param("nodeId");
|
|
958
|
+
const body = c.req.valid("json");
|
|
959
|
+
await loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId);
|
|
960
|
+
return c.json(await repo.updateTaskPaths(taskId, {
|
|
961
|
+
sets: { [`nodeRecords.${nodeId}.review`]: body }
|
|
962
|
+
}));
|
|
963
|
+
});
|
|
964
|
+
app.post("/api/tasks/:taskId/archnotes", zValidator6("json", TextItemSchema), async (c) => {
|
|
965
|
+
const taskId = c.req.param("taskId");
|
|
966
|
+
const body = c.req.valid("json");
|
|
967
|
+
const task = await loadTaskForRecordWrite(repo, projectRepo, taskId);
|
|
968
|
+
const note = { id: generateEntryId((task.archNotes ?? []).map((n) => n.id)), text: body.text, at: /* @__PURE__ */ new Date() };
|
|
969
|
+
return c.json(await repo.updateTaskPaths(taskId, { pushes: { archNotes: note } }));
|
|
970
|
+
});
|
|
971
|
+
return app;
|
|
972
|
+
}
|
|
973
|
+
function assertTypeTrackCompatible(type, track) {
|
|
974
|
+
const mismatch = (reason) => new ValidationError(
|
|
975
|
+
reason,
|
|
976
|
+
[{ code: "custom", path: ["type"], message: reason }],
|
|
977
|
+
{ bizCode: "TASK_TYPE_TRACK_MISMATCH", message: reason }
|
|
978
|
+
);
|
|
979
|
+
if (track === "research") {
|
|
980
|
+
if (type !== "research") {
|
|
981
|
+
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`);
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (type === void 0) return;
|
|
986
|
+
if (type === "research") {
|
|
987
|
+
throw mismatch(`type=research \u987B\u914D track=research\uFF08\u5F53\u524D track=${track}\uFF09`);
|
|
988
|
+
}
|
|
989
|
+
if (type === "ui-tweak" && track !== "ui") {
|
|
990
|
+
throw mismatch(`UI \u5FAE\u8C03\uFF08ui-tweak\uFF09\u4EC5\u9002\u7528 ui \u8F68\u9053\uFF08\u5F53\u524D track=${track}\uFF09`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function mkDeps(repo) {
|
|
994
|
+
return {
|
|
995
|
+
getTask: repo.getByTaskId.bind(repo),
|
|
996
|
+
updateTask: repo.updateTask.bind(repo)
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
async function assertTaskProjectActive(repo, projectRepo, taskId) {
|
|
1000
|
+
const task = await repo.getByTaskId(taskId);
|
|
1001
|
+
if (!task) throw new NotFoundError6("task", taskId);
|
|
1002
|
+
await loadProjectForWrite(projectRepo, task.projectId);
|
|
1003
|
+
}
|
|
1004
|
+
async function loadTaskForRecordWrite(repo, projectRepo, taskId, nodeId) {
|
|
1005
|
+
const task = await repo.getByTaskId(taskId);
|
|
1006
|
+
if (!task) throw new NotFoundError6("task", taskId);
|
|
1007
|
+
await loadProjectForWrite(projectRepo, task.projectId);
|
|
1008
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
1009
|
+
throw new BadRequestError2(
|
|
1010
|
+
`Task '${taskId}' is ${task.status} \u2014 task records are read-only after termination`,
|
|
1011
|
+
void 0,
|
|
1012
|
+
{ bizCode: "TASK_TERMINATED", message: `${BIZ_CODE_MESSAGES6.TASK_TERMINATED}\uFF08status: ${task.status}\uFF09` }
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
if (nodeId === void 0) return task;
|
|
1016
|
+
if (!NODE_ID_PATTERN.test(nodeId)) {
|
|
1017
|
+
throw new BadRequestError2(`nodeId '${nodeId}' \u975E\u6CD5\uFF08\u53EA\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u4E0B\u5212\u7EBF/\u8FDE\u5B57\u7B26\uFF09`, "nodeId");
|
|
1018
|
+
}
|
|
1019
|
+
const node = task.dagInstance.nodes.find((n) => n.id === nodeId);
|
|
1020
|
+
if (!node) {
|
|
1021
|
+
throw new NotFoundError6("node", nodeId, {
|
|
1022
|
+
bizCode: "NODE_NOT_IN_INSTANCE",
|
|
1023
|
+
message: `${BIZ_CODE_MESSAGES6.NODE_NOT_IN_INSTANCE}\uFF08nodeId: ${nodeId}\uFF09`
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
const state = task.dagInstance.nodeStates[nodeId];
|
|
1027
|
+
if (state?.status !== "active" && state?.status !== "completed") {
|
|
1028
|
+
throw new BadRequestError2(
|
|
1029
|
+
`Node '${nodeId}' record is not writable (state '${state?.status ?? "pending"}') \u2014 flow has not reached this node`,
|
|
1030
|
+
void 0,
|
|
1031
|
+
{
|
|
1032
|
+
bizCode: "NODE_RECORD_NOT_WRITABLE",
|
|
1033
|
+
message: `${BIZ_CODE_MESSAGES6.NODE_RECORD_NOT_WRITABLE}\uFF08${nodeId}: ${state?.status ?? "pending"}\uFF09`
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
return task;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/routes/settings.routes.ts
|
|
1041
|
+
import { Hono as Hono9 } from "hono";
|
|
1042
|
+
import { zValidator as zValidator7 } from "@hono/zod-validator";
|
|
1043
|
+
import { z as z5 } from "zod";
|
|
1044
|
+
import {
|
|
1045
|
+
createEnumRegistryRepo as createEnumRegistryRepo3,
|
|
1046
|
+
EnumRegistryUpdateSchema,
|
|
1047
|
+
NotFoundError as NotFoundError7
|
|
1048
|
+
} from "@siming-org/core";
|
|
1049
|
+
function createSettingsRoutes(client) {
|
|
1050
|
+
const app = new Hono9();
|
|
1051
|
+
const repo = createEnumRegistryRepo3(client.db());
|
|
1052
|
+
app.get("/api/settings/enums", async (c) => {
|
|
1053
|
+
return c.json(await repo.listRegistries());
|
|
1054
|
+
});
|
|
1055
|
+
app.get(
|
|
1056
|
+
"/api/settings/enums/:category",
|
|
1057
|
+
zValidator7("param", z5.object({ category: z5.string() })),
|
|
1058
|
+
async (c) => {
|
|
1059
|
+
const { category } = c.req.valid("param");
|
|
1060
|
+
const registry = await repo.getRegistry(category);
|
|
1061
|
+
if (!registry) throw new NotFoundError7("enum-registry", category);
|
|
1062
|
+
return c.json(registry);
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
app.put(
|
|
1066
|
+
"/api/settings/enums/:category",
|
|
1067
|
+
zValidator7("param", z5.object({ category: z5.string() })),
|
|
1068
|
+
zValidator7("json", EnumRegistryUpdateSchema),
|
|
1069
|
+
async (c) => {
|
|
1070
|
+
const { category } = c.req.valid("param");
|
|
1071
|
+
const data = c.req.valid("json");
|
|
1072
|
+
const registry = await repo.updateRegistry(category, data);
|
|
1073
|
+
if (!registry) throw new NotFoundError7("enum-registry", category);
|
|
1074
|
+
return c.json(registry);
|
|
1075
|
+
}
|
|
1076
|
+
);
|
|
1077
|
+
app.delete(
|
|
1078
|
+
"/api/settings/enums/:category/entries/:value",
|
|
1079
|
+
zValidator7("param", z5.object({ category: z5.string(), value: z5.string().min(1) })),
|
|
1080
|
+
async (c) => {
|
|
1081
|
+
const { category, value } = c.req.valid("param");
|
|
1082
|
+
const deleted = await repo.deleteEntry(category, value);
|
|
1083
|
+
if (!deleted) throw new NotFoundError7("enum-registry", category);
|
|
1084
|
+
return c.body(null, 204);
|
|
1085
|
+
}
|
|
1086
|
+
);
|
|
1087
|
+
return app;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/routes/index.ts
|
|
1091
|
+
function createRoutes(client) {
|
|
1092
|
+
const router = new Hono10();
|
|
1093
|
+
router.route("/", createHealth(client));
|
|
1094
|
+
router.route("/", createProjectRoutes(client));
|
|
1095
|
+
router.route("/", createSkillRoutes(client));
|
|
1096
|
+
router.route("/", createAgentRoutes(client));
|
|
1097
|
+
router.route("/", createModelAliasRoutes(client));
|
|
1098
|
+
router.route("/", createDagTemplateRoutes(client));
|
|
1099
|
+
router.route("/", createTaskRoutes(client));
|
|
1100
|
+
router.route("/", createSettingsRoutes(client));
|
|
1101
|
+
return router;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/app.ts
|
|
1105
|
+
function createApp(client, opts = {}) {
|
|
1106
|
+
const app = new Hono11();
|
|
1107
|
+
setupErrorHandler(app);
|
|
1108
|
+
app.route("/", createRoutes(client));
|
|
1109
|
+
const webRoot = opts.webRoot !== void 0 ? opts.webRoot : resolveWebDistRoot();
|
|
1110
|
+
app.route("/", createStaticRoutes(webRoot));
|
|
1111
|
+
return app;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/start-server.ts
|
|
1115
|
+
import { serve } from "@hono/node-server";
|
|
1116
|
+
import { createMongoClient } from "@siming-org/core";
|
|
1117
|
+
|
|
1118
|
+
// src/ensure-collections.ts
|
|
1119
|
+
import { EnumRegistrySchema } from "@siming-org/core";
|
|
1120
|
+
async function ensureCollections(client) {
|
|
1121
|
+
const db = client.db();
|
|
1122
|
+
await Promise.all([
|
|
1123
|
+
ensureProjectCollection(db),
|
|
1124
|
+
ensureSkillCollection(db),
|
|
1125
|
+
ensureAgentCollection(db),
|
|
1126
|
+
ensureDagTemplateCollection(db),
|
|
1127
|
+
ensureTaskCollection(db),
|
|
1128
|
+
ensureModelAliasCollection(db),
|
|
1129
|
+
ensureEnumRegistryCollection(db)
|
|
1130
|
+
]);
|
|
1131
|
+
await migrateProjectSentinelData(db);
|
|
1132
|
+
await migrateGateRemovalData(db);
|
|
1133
|
+
await migrateTaskRecordData(db);
|
|
1134
|
+
await migrateDagTrackTaskValues(db);
|
|
1135
|
+
await ensurePostMigrationIndexes(db);
|
|
1136
|
+
}
|
|
1137
|
+
var ENUM_REGISTRY_SEEDS = {
|
|
1138
|
+
dag_phase: [
|
|
1139
|
+
{ value: "entry", label: "\u5165\u53E3", builtin: false },
|
|
1140
|
+
{ value: "track", label: "\u8F68\u9053", builtin: false },
|
|
1141
|
+
{ value: "test", label: "\u6D4B\u8BD5", builtin: false },
|
|
1142
|
+
{ value: "exit", label: "\u51FA\u53E3", builtin: false }
|
|
1143
|
+
],
|
|
1144
|
+
dag_track: [
|
|
1145
|
+
{ value: "backend", label: "\u540E\u7AEF", builtin: true },
|
|
1146
|
+
{ value: "ui", label: "\u524D\u7AEF", builtin: true },
|
|
1147
|
+
{ value: "all", label: "\u5168\u90E8", builtin: true },
|
|
1148
|
+
// T202608240003:任务级轨道专用值(非节点 track 值)——mixed=双链串行、research=调研任务;
|
|
1149
|
+
// 存量库 doc 已存在(seed 类缺失才插入),由 M11 迁移合并(缺 M11 会测试绿+生产 400)
|
|
1150
|
+
{ value: "mixed", label: "\u6DF7\u5408\uFF08\u524D\u540E\u7AEF\uFF09", builtin: true },
|
|
1151
|
+
{ value: "research", label: "\u8C03\u7814\uFF08\u5185\u90E8\u503C\uFF0C\u968F\u7C7B\u578B\u81EA\u52A8\u8BBE\u7F6E\uFF09", builtin: true }
|
|
1152
|
+
],
|
|
1153
|
+
pause_type: [
|
|
1154
|
+
{ value: "human_approval", label: "\u4EBA\u5DE5\u5BA1\u6279", builtin: false },
|
|
1155
|
+
{ value: "checkpoint", label: "\u68C0\u67E5\u70B9", builtin: false }
|
|
1156
|
+
],
|
|
1157
|
+
task_status: [
|
|
1158
|
+
{ value: "active", label: "\u8FDB\u884C\u4E2D", builtin: true },
|
|
1159
|
+
{ value: "paused", label: "\u6682\u505C", builtin: true },
|
|
1160
|
+
{ value: "completed", label: "\u5DF2\u5B8C\u6210", builtin: true },
|
|
1161
|
+
{ value: "cancelled", label: "\u5DF2\u53D6\u6D88", builtin: true }
|
|
1162
|
+
],
|
|
1163
|
+
node_status: [
|
|
1164
|
+
{ value: "pending", label: "\u5F85\u6267\u884C", builtin: true },
|
|
1165
|
+
{ value: "active", label: "\u8FDB\u884C\u4E2D", builtin: true },
|
|
1166
|
+
{ value: "completed", label: "\u5DF2\u5B8C\u6210", builtin: true },
|
|
1167
|
+
{ value: "skipped", label: "\u5DF2\u8DF3\u8FC7", builtin: true }
|
|
1168
|
+
],
|
|
1169
|
+
skill_category: [
|
|
1170
|
+
{ value: "process", label: "\u6D41\u7A0B", builtin: false },
|
|
1171
|
+
{ value: "domain", label: "\u9886\u57DF", builtin: false },
|
|
1172
|
+
{ value: "tooling", label: "\u5DE5\u5177", builtin: false }
|
|
1173
|
+
],
|
|
1174
|
+
scope: [
|
|
1175
|
+
{ value: "global", label: "\u5168\u5C40", builtin: true },
|
|
1176
|
+
{ value: "project", label: "\u9879\u76EE", builtin: true }
|
|
1177
|
+
]
|
|
1178
|
+
};
|
|
1179
|
+
function buildSeedEntries(seeds) {
|
|
1180
|
+
return seeds.map((s, i) => ({
|
|
1181
|
+
value: s.value,
|
|
1182
|
+
label: s.label,
|
|
1183
|
+
builtin: s.builtin,
|
|
1184
|
+
order: i,
|
|
1185
|
+
active: true,
|
|
1186
|
+
...s.color ? { color: s.color } : {}
|
|
1187
|
+
}));
|
|
1188
|
+
}
|
|
1189
|
+
async function ensureEnumRegistryCollection(db) {
|
|
1190
|
+
const collections = await db.listCollections({ name: "enum_registry" }).toArray();
|
|
1191
|
+
if (collections.length === 0) {
|
|
1192
|
+
await db.createCollection("enum_registry");
|
|
1193
|
+
}
|
|
1194
|
+
await db.collection("enum_registry").createIndex({ category: 1 }, { unique: true });
|
|
1195
|
+
const coll = db.collection("enum_registry");
|
|
1196
|
+
for (const [category, seeds] of Object.entries(ENUM_REGISTRY_SEEDS)) {
|
|
1197
|
+
const existing = await coll.findOne({ category });
|
|
1198
|
+
if (!existing) {
|
|
1199
|
+
const doc = {
|
|
1200
|
+
category,
|
|
1201
|
+
entries: buildSeedEntries(seeds),
|
|
1202
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1203
|
+
};
|
|
1204
|
+
await coll.insertOne(doc);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function ensureModelAliasCollection(db) {
|
|
1209
|
+
const validator = { $jsonSchema: {
|
|
1210
|
+
bsonType: "object",
|
|
1211
|
+
required: ["code", "name", "realModel"],
|
|
1212
|
+
properties: {
|
|
1213
|
+
code: { bsonType: "string", pattern: "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
|
1214
|
+
name: { bsonType: "string" },
|
|
1215
|
+
realModel: { bsonType: "string" },
|
|
1216
|
+
createdAt: { bsonType: "date" },
|
|
1217
|
+
updatedAt: { bsonType: "date" }
|
|
1218
|
+
}
|
|
1219
|
+
} };
|
|
1220
|
+
const collections = await db.listCollections({ name: "model_aliases" }).toArray();
|
|
1221
|
+
if (collections.length === 0) {
|
|
1222
|
+
await db.createCollection("model_aliases", { validator });
|
|
1223
|
+
}
|
|
1224
|
+
await db.collection("model_aliases").createIndex({ code: 1 }, { unique: true });
|
|
1225
|
+
}
|
|
1226
|
+
async function ensureProjectCollection(db) {
|
|
1227
|
+
const collections = await db.listCollections({ name: "projects" }).toArray();
|
|
1228
|
+
if (collections.length === 0) {
|
|
1229
|
+
await db.createCollection("projects", { validator: projectValidator() });
|
|
1230
|
+
}
|
|
1231
|
+
await db.collection("projects").createIndex({ key: 1 }, { unique: true });
|
|
1232
|
+
await db.collection("projects").createIndex({ name: 1 }, { unique: true });
|
|
1233
|
+
await db.collection("projects").createIndex({ status: 1 });
|
|
1234
|
+
}
|
|
1235
|
+
function projectValidator() {
|
|
1236
|
+
return { $jsonSchema: {
|
|
1237
|
+
bsonType: "object",
|
|
1238
|
+
required: ["key", "name", "status"],
|
|
1239
|
+
properties: {
|
|
1240
|
+
key: { bsonType: "string", pattern: "^[a-z0-9][a-z0-9-]{1,30}$" },
|
|
1241
|
+
name: { bsonType: "string" },
|
|
1242
|
+
description: { bsonType: "string" },
|
|
1243
|
+
status: { enum: ["active", "archived"] },
|
|
1244
|
+
createdAt: { bsonType: "date" },
|
|
1245
|
+
updatedAt: { bsonType: "date" }
|
|
1246
|
+
}
|
|
1247
|
+
} };
|
|
1248
|
+
}
|
|
1249
|
+
async function ensureSkillCollection(db) {
|
|
1250
|
+
const validator = { $jsonSchema: {
|
|
1251
|
+
bsonType: "object",
|
|
1252
|
+
required: ["name", "description", "content", "category", "scope"],
|
|
1253
|
+
properties: {
|
|
1254
|
+
// name 不设 pattern:create 侧 kebab 由应用层 Zod 把守(SkillCreateSchema),DB 层放宽以允许
|
|
1255
|
+
// validator 部署前的 CJK 存量记录被更新(否则 findOneAndUpdate 触发 code 121 拒绝合法编辑,N013 SKILL_EDIT_008)
|
|
1256
|
+
name: { bsonType: "string", minLength: 1 },
|
|
1257
|
+
description: { bsonType: "string" },
|
|
1258
|
+
content: { bsonType: "string" },
|
|
1259
|
+
// N016 F9:category 放宽为任意字符串(枚举注册表为运行时权威,DB 只做类型兜底)
|
|
1260
|
+
category: { bsonType: "string" },
|
|
1261
|
+
// N012 F7:作用域字段(全局 / 项目专用);projectId 仅 scope=project 携带
|
|
1262
|
+
scope: { enum: ["global", "project"] },
|
|
1263
|
+
projectId: { bsonType: "string" },
|
|
1264
|
+
version: { bsonType: "string" },
|
|
1265
|
+
createdAt: { bsonType: "date" },
|
|
1266
|
+
updatedAt: { bsonType: "date" }
|
|
1267
|
+
}
|
|
1268
|
+
} };
|
|
1269
|
+
const collections = await db.listCollections({ name: "skills" }).toArray();
|
|
1270
|
+
if (collections.length === 0) {
|
|
1271
|
+
await db.createCollection("skills", { validator });
|
|
1272
|
+
} else {
|
|
1273
|
+
await db.command({ collMod: "skills", validationLevel: "strict", validator });
|
|
1274
|
+
}
|
|
1275
|
+
await db.collection("skills").createIndex({ scope: 1, projectId: 1 });
|
|
1276
|
+
}
|
|
1277
|
+
async function ensureAgentCollection(db) {
|
|
1278
|
+
const validator = { $jsonSchema: {
|
|
1279
|
+
bsonType: "object",
|
|
1280
|
+
required: ["name", "description", "systemPrompt", "model", "scope"],
|
|
1281
|
+
properties: {
|
|
1282
|
+
name: { bsonType: "string" },
|
|
1283
|
+
description: { bsonType: "string" },
|
|
1284
|
+
systemPrompt: { bsonType: "string" },
|
|
1285
|
+
boundSkills: { bsonType: "array", items: { bsonType: "string" } },
|
|
1286
|
+
model: { bsonType: "string" },
|
|
1287
|
+
// N015:agent 资产版本(F13 版本对比);存量回填走 M6 迁移
|
|
1288
|
+
version: { bsonType: "string" },
|
|
1289
|
+
tools: { bsonType: "array", items: { bsonType: "string" } },
|
|
1290
|
+
permissions: { bsonType: "array", items: { bsonType: "string" } },
|
|
1291
|
+
scope: { enum: ["global", "project"] },
|
|
1292
|
+
projectId: { bsonType: "string" },
|
|
1293
|
+
createdAt: { bsonType: "date" },
|
|
1294
|
+
updatedAt: { bsonType: "date" }
|
|
1295
|
+
}
|
|
1296
|
+
} };
|
|
1297
|
+
const collections = await db.listCollections({ name: "agents" }).toArray();
|
|
1298
|
+
if (collections.length === 0) {
|
|
1299
|
+
await db.createCollection("agents", { validator });
|
|
1300
|
+
} else {
|
|
1301
|
+
await db.command({ collMod: "agents", validationLevel: "strict", validator });
|
|
1302
|
+
}
|
|
1303
|
+
await db.collection("agents").createIndex({ scope: 1, projectId: 1 });
|
|
1304
|
+
}
|
|
1305
|
+
async function ensureDagTemplateCollection(db) {
|
|
1306
|
+
const validator = { $jsonSchema: {
|
|
1307
|
+
bsonType: "object",
|
|
1308
|
+
// N017 F8:pausePoints 顶层字段退役(暂停点内联 edges.pausePoint)
|
|
1309
|
+
required: ["name", "projectId", "description", "nodes", "edges", "isDefault", "version"],
|
|
1310
|
+
properties: {
|
|
1311
|
+
name: { bsonType: "string" },
|
|
1312
|
+
projectId: { bsonType: "string" },
|
|
1313
|
+
description: { bsonType: "string" },
|
|
1314
|
+
nodes: { bsonType: "array" },
|
|
1315
|
+
edges: { bsonType: "array" },
|
|
1316
|
+
isDefault: { bsonType: "bool" },
|
|
1317
|
+
version: { bsonType: "string" },
|
|
1318
|
+
createdAt: { bsonType: "date" },
|
|
1319
|
+
updatedAt: { bsonType: "date" }
|
|
1320
|
+
}
|
|
1321
|
+
} };
|
|
1322
|
+
const collections = await db.listCollections({ name: "dag_templates" }).toArray();
|
|
1323
|
+
if (collections.length === 0) {
|
|
1324
|
+
await db.createCollection("dag_templates", { validator });
|
|
1325
|
+
} else {
|
|
1326
|
+
await db.command({ collMod: "dag_templates", validationLevel: "strict", validator });
|
|
1327
|
+
}
|
|
1328
|
+
await db.collection("dag_templates").createIndex({ projectId: 1 });
|
|
1329
|
+
await db.collection("dag_templates").createIndex({ isDefault: 1 });
|
|
1330
|
+
}
|
|
1331
|
+
async function ensureTaskCollection(db) {
|
|
1332
|
+
const validator = { $jsonSchema: {
|
|
1333
|
+
bsonType: "object",
|
|
1334
|
+
required: ["taskId", "title", "projectId", "dagTemplateId", "dagInstance", "currentNode", "currentPhase", "status", "track", "history"],
|
|
1335
|
+
properties: {
|
|
1336
|
+
taskId: { bsonType: "string" },
|
|
1337
|
+
title: { bsonType: "string" },
|
|
1338
|
+
projectId: { bsonType: "string" },
|
|
1339
|
+
dagTemplateId: { bsonType: "string" },
|
|
1340
|
+
dagInstance: { bsonType: "object" },
|
|
1341
|
+
currentNode: { bsonType: "string" },
|
|
1342
|
+
// N016 F06:枚举放宽(D3 注册表权威)——currentPhase/status 不再 DB 枚举硬限,由注册表/应用层把守
|
|
1343
|
+
currentPhase: { bsonType: "string" },
|
|
1344
|
+
status: { bsonType: "string" },
|
|
1345
|
+
pausedAt: { bsonType: ["string", "null"] },
|
|
1346
|
+
track: { bsonType: "string" },
|
|
1347
|
+
// T202608240003:任务类型标签(可选,类型兜底——取值域归应用层 z.enum)
|
|
1348
|
+
type: { bsonType: "string" },
|
|
1349
|
+
// N020 D1:任务信息结构化三字段(可选——存量任务未迁移前缺省合法,M10 补齐)
|
|
1350
|
+
doc: { bsonType: "object" },
|
|
1351
|
+
nodeRecords: { bsonType: "object" },
|
|
1352
|
+
archNotes: { bsonType: "array" },
|
|
1353
|
+
history: { bsonType: "array" },
|
|
1354
|
+
createdAt: { bsonType: "date" },
|
|
1355
|
+
updatedAt: { bsonType: "date" }
|
|
1356
|
+
}
|
|
1357
|
+
} };
|
|
1358
|
+
const collections = await db.listCollections({ name: "tasks" }).toArray();
|
|
1359
|
+
if (collections.length === 0) {
|
|
1360
|
+
await db.createCollection("tasks", { validator });
|
|
1361
|
+
} else {
|
|
1362
|
+
await db.command({ collMod: "tasks", validationLevel: "strict", validator });
|
|
1363
|
+
}
|
|
1364
|
+
await db.collection("tasks").createIndex({ taskId: 1 }, { unique: true });
|
|
1365
|
+
await db.collection("tasks").createIndex({ status: 1 });
|
|
1366
|
+
await db.collection("tasks").createIndex({ track: 1 });
|
|
1367
|
+
}
|
|
1368
|
+
async function migrateProjectSentinelData(db) {
|
|
1369
|
+
const projects = db.collection("projects");
|
|
1370
|
+
let defaultProject = await projects.findOne({ key: "default" });
|
|
1371
|
+
if (!defaultProject) {
|
|
1372
|
+
const now = /* @__PURE__ */ new Date();
|
|
1373
|
+
await projects.insertOne({
|
|
1374
|
+
key: "default",
|
|
1375
|
+
name: "\u9ED8\u8BA4\u9879\u76EE",
|
|
1376
|
+
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",
|
|
1377
|
+
status: "active",
|
|
1378
|
+
createdAt: now,
|
|
1379
|
+
updatedAt: now
|
|
1380
|
+
});
|
|
1381
|
+
defaultProject = await projects.findOne({ key: "default" });
|
|
1382
|
+
}
|
|
1383
|
+
if (!defaultProject) {
|
|
1384
|
+
throw new Error("N012 M1 \u8FC1\u79FB\u5931\u8D25\uFF1A\u9ED8\u8BA4\u9879\u76EE seed \u540E\u4ECD\u65E0\u6CD5\u8BFB\u53D6");
|
|
1385
|
+
}
|
|
1386
|
+
const defaultProjectId = defaultProject._id.toString();
|
|
1387
|
+
await db.collection("tasks").updateMany({ projectId: "default" }, { $set: { projectId: defaultProjectId } }, { bypassDocumentValidation: true });
|
|
1388
|
+
await db.collection("dag_templates").updateMany({ projectId: "default" }, { $set: { projectId: defaultProjectId } }, { bypassDocumentValidation: true });
|
|
1389
|
+
await db.collection("skills").updateMany({ scope: { $exists: false } }, { $set: { scope: "global" } });
|
|
1390
|
+
await db.collection("agents").updateMany({ scope: { $exists: false } }, { $set: { scope: "global" } });
|
|
1391
|
+
await db.collection("agents").updateMany({ version: { $exists: false } }, { $set: { version: "1.0.0" } });
|
|
1392
|
+
}
|
|
1393
|
+
async function migrateGateRemovalData(db) {
|
|
1394
|
+
await db.collection("enum_registry").deleteOne({ category: "gate_type" });
|
|
1395
|
+
const legacyTemplates = await db.collection("dag_templates").find(
|
|
1396
|
+
{ $or: [{ "nodes.gates": { $exists: true } }, { pausePoints: { $exists: true } }] },
|
|
1397
|
+
{ projection: { name: 1 } }
|
|
1398
|
+
).toArray();
|
|
1399
|
+
if (legacyTemplates.length > 0) {
|
|
1400
|
+
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(", ")}`);
|
|
1401
|
+
await db.collection("dag_templates").deleteMany({
|
|
1402
|
+
_id: { $in: legacyTemplates.map((t) => t._id) }
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
const legacyTasks = await db.collection("tasks").find(
|
|
1406
|
+
{ "dagInstance.pausePoints": { $exists: true } },
|
|
1407
|
+
{ projection: { taskId: 1 } }
|
|
1408
|
+
).toArray();
|
|
1409
|
+
if (legacyTasks.length > 0) {
|
|
1410
|
+
console.log(`[siming] N017 M9\uFF1A\u5220\u9664\u65E7\u7ED3\u6784\u4EFB\u52A1 ${legacyTasks.length} \u4E2A\uFF1A${legacyTasks.map((t) => t.taskId).join(", ")}`);
|
|
1411
|
+
await db.collection("tasks").deleteMany({
|
|
1412
|
+
_id: { $in: legacyTasks.map((t) => t._id) }
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
async function migrateTaskRecordData(db) {
|
|
1417
|
+
await db.collection("tasks").updateMany(
|
|
1418
|
+
{ nodeRecords: { $exists: false } },
|
|
1419
|
+
{ $set: { nodeRecords: {} } },
|
|
1420
|
+
{ bypassDocumentValidation: true }
|
|
1421
|
+
);
|
|
1422
|
+
await db.collection("tasks").updateMany(
|
|
1423
|
+
{ archNotes: { $exists: false } },
|
|
1424
|
+
{ $set: { archNotes: [] } },
|
|
1425
|
+
{ bypassDocumentValidation: true }
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
async function migrateDagTrackTaskValues(db) {
|
|
1429
|
+
const coll = db.collection("enum_registry");
|
|
1430
|
+
const doc = await coll.findOne({ category: "dag_track" });
|
|
1431
|
+
if (!doc) return;
|
|
1432
|
+
const parsed = EnumRegistrySchema.safeParse({ category: "dag_track", ...doc, entries: doc.entries ?? [] });
|
|
1433
|
+
if (!parsed.success) {
|
|
1434
|
+
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`);
|
|
1435
|
+
}
|
|
1436
|
+
const entries = parsed.data.entries;
|
|
1437
|
+
const existingValues = new Set(entries.map((e) => e.value));
|
|
1438
|
+
const maxOrder = entries.reduce((max, e) => Math.max(max, e.order), -1);
|
|
1439
|
+
const additions = ENUM_REGISTRY_SEEDS.dag_track.filter((s) => !existingValues.has(s.value)).map((s, i) => ({
|
|
1440
|
+
value: s.value,
|
|
1441
|
+
label: s.label,
|
|
1442
|
+
builtin: s.builtin,
|
|
1443
|
+
order: maxOrder + 1 + i,
|
|
1444
|
+
active: true
|
|
1445
|
+
}));
|
|
1446
|
+
if (additions.length === 0) return;
|
|
1447
|
+
const result = await coll.updateOne(
|
|
1448
|
+
{ _id: doc._id },
|
|
1449
|
+
{ $set: { entries: [...entries, ...additions], updatedAt: /* @__PURE__ */ new Date() } }
|
|
1450
|
+
);
|
|
1451
|
+
if (result.matchedCount === 0) {
|
|
1452
|
+
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");
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function ensurePostMigrationIndexes(db) {
|
|
1456
|
+
const tasks = db.collection("tasks");
|
|
1457
|
+
const taskIndexes = await tasks.listIndexes().toArray();
|
|
1458
|
+
const legacySingle = taskIndexes.find(
|
|
1459
|
+
(idx) => idx.name === "projectId_1" && Object.keys(idx.key).length === 1
|
|
1460
|
+
);
|
|
1461
|
+
if (legacySingle) {
|
|
1462
|
+
await tasks.dropIndex("projectId_1");
|
|
1463
|
+
}
|
|
1464
|
+
await tasks.createIndex({ projectId: 1, status: 1 });
|
|
1465
|
+
const templates = db.collection("dag_templates");
|
|
1466
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1467
|
+
const docs = await templates.find({}, { projection: { projectId: 1, name: 1 } }).toArray();
|
|
1468
|
+
for (const doc of docs) {
|
|
1469
|
+
const key = `${String(doc.projectId)}\0${String(doc.name)}`;
|
|
1470
|
+
const ids = seen.get(key) ?? [];
|
|
1471
|
+
ids.push(doc._id.toString());
|
|
1472
|
+
seen.set(key, ids);
|
|
1473
|
+
}
|
|
1474
|
+
const conflicts = [...seen.entries()].filter(([, ids]) => ids.length > 1);
|
|
1475
|
+
if (conflicts.length > 0) {
|
|
1476
|
+
const detail = conflicts.map(([key, ids]) => {
|
|
1477
|
+
const [projectId, name] = key.split("\0");
|
|
1478
|
+
return `projectId=${projectId} name="${name}" \xD7${ids.length} (_id: ${ids.join(", ")})`;
|
|
1479
|
+
}).join("; ");
|
|
1480
|
+
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}`);
|
|
1481
|
+
}
|
|
1482
|
+
await templates.createIndex({ projectId: 1, name: 1 }, { unique: true });
|
|
1483
|
+
await db.collection("agents").createIndex({ model: 1 });
|
|
1484
|
+
await migrateNameUniquenessIndexes(db);
|
|
1485
|
+
}
|
|
1486
|
+
async function migrateNameUniquenessIndexes(db) {
|
|
1487
|
+
const assertNoProjectNameConflict = async (collectionName) => {
|
|
1488
|
+
const coll = db.collection(collectionName);
|
|
1489
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1490
|
+
const docs = await coll.find({ scope: "project" }, { projection: { projectId: 1, name: 1 } }).toArray();
|
|
1491
|
+
for (const doc of docs) {
|
|
1492
|
+
const key = `${String(doc.projectId)}\0${String(doc.name)}`;
|
|
1493
|
+
const ids = seen.get(key) ?? [];
|
|
1494
|
+
ids.push(doc._id.toString());
|
|
1495
|
+
seen.set(key, ids);
|
|
1496
|
+
}
|
|
1497
|
+
const conflicts = [...seen.entries()].filter(([, ids]) => ids.length > 1);
|
|
1498
|
+
if (conflicts.length > 0) {
|
|
1499
|
+
const detail = conflicts.map(([key, ids]) => {
|
|
1500
|
+
const [projectId, name] = key.split("\0");
|
|
1501
|
+
return `projectId=${projectId} name="${name}" \xD7${ids.length} (_id: ${ids.join(", ")})`;
|
|
1502
|
+
}).join("; ");
|
|
1503
|
+
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}`);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
for (const name of ["skills", "agents"]) {
|
|
1507
|
+
const coll = db.collection(name);
|
|
1508
|
+
const indexes = await coll.listIndexes().toArray();
|
|
1509
|
+
const legacyNameUnique = indexes.find(
|
|
1510
|
+
(idx) => idx.name === "name_1" && idx.unique === true && Object.keys(idx.key).length === 1 && // N016 F04:partial 索引名也是 name_1,必须排除(否则每次启动误判 legacy → drop+重建 churn)
|
|
1511
|
+
idx.partialFilterExpression === void 0
|
|
1512
|
+
);
|
|
1513
|
+
if (legacyNameUnique) {
|
|
1514
|
+
await coll.dropIndex("name_1");
|
|
1515
|
+
}
|
|
1516
|
+
await assertNoProjectNameConflict(name);
|
|
1517
|
+
await coll.createIndex(
|
|
1518
|
+
{ name: 1 },
|
|
1519
|
+
{ unique: true, partialFilterExpression: { scope: "global" } }
|
|
1520
|
+
);
|
|
1521
|
+
await coll.createIndex(
|
|
1522
|
+
{ projectId: 1, name: 1 },
|
|
1523
|
+
{ unique: true, partialFilterExpression: { scope: "project" } }
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/start-server.ts
|
|
1529
|
+
var MONGO_PROBE_TIMEOUT_MS = 5e3;
|
|
1530
|
+
async function startServer(config) {
|
|
1531
|
+
const client = await createMongoClient(config.mongoUri, {
|
|
1532
|
+
serverSelectionTimeoutMS: MONGO_PROBE_TIMEOUT_MS
|
|
1533
|
+
});
|
|
1534
|
+
await ensureCollections(client);
|
|
1535
|
+
const webRoot = resolveWebDistRoot();
|
|
1536
|
+
const app = createApp(client, { webRoot });
|
|
1537
|
+
let port = config.port;
|
|
1538
|
+
const server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
|
|
1539
|
+
port = info.port;
|
|
1540
|
+
console.log(`[siming] server listening on http://${info.address}:${info.port}`);
|
|
1541
|
+
});
|
|
1542
|
+
console.log(webRoot === null ? "[siming] web ui not built \u2014 static disabled (API only)" : `[siming] web ui root: ${webRoot}`);
|
|
1543
|
+
console.log(`[siming] config: port=${config.port} host=${config.host} logLevel=${config.logLevel}`);
|
|
1544
|
+
const stop = async () => {
|
|
1545
|
+
console.log("[siming] shutting down...");
|
|
1546
|
+
server.close();
|
|
1547
|
+
await client.close();
|
|
1548
|
+
};
|
|
1549
|
+
return { server, client, host: config.host, port, stop };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
export {
|
|
1553
|
+
resolveWebDistRoot,
|
|
1554
|
+
createStaticRoutes,
|
|
1555
|
+
createApp,
|
|
1556
|
+
MONGO_PROBE_TIMEOUT_MS,
|
|
1557
|
+
startServer
|
|
1558
|
+
};
|