@siming-org/core 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/index.d.ts +1696 -0
- package/dist/index.js +2096 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2096 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { z as z11 } from "zod";
|
|
4
|
+
|
|
5
|
+
// src/store/client.ts
|
|
6
|
+
import { MongoClient } from "mongodb";
|
|
7
|
+
var DEFAULT_OPTIONS = {
|
|
8
|
+
retryWrites: true
|
|
9
|
+
};
|
|
10
|
+
function dbFromUri(uri) {
|
|
11
|
+
const match = uri.match(/\/\/[^/]+\/([^?]+)/);
|
|
12
|
+
return match?.[1];
|
|
13
|
+
}
|
|
14
|
+
async function createMongoClient(uri = "mongodb://localhost:27017", options = {}) {
|
|
15
|
+
const client = new MongoClient(uri, { ...DEFAULT_OPTIONS, ...options });
|
|
16
|
+
await client.connect();
|
|
17
|
+
await client.db().command({ ping: 1 });
|
|
18
|
+
const defaultDb = dbFromUri(uri) ?? "siming";
|
|
19
|
+
return {
|
|
20
|
+
db: (name) => client.db(name ?? defaultDb),
|
|
21
|
+
close: () => client.close(),
|
|
22
|
+
_underlying: client
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function withTransaction(client, fn) {
|
|
26
|
+
const session = client._underlying.startSession();
|
|
27
|
+
try {
|
|
28
|
+
session.startTransaction();
|
|
29
|
+
const result = await fn(session);
|
|
30
|
+
await session.commitTransaction();
|
|
31
|
+
return result;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (session.inTransaction()) {
|
|
34
|
+
await session.abortTransaction();
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
} finally {
|
|
38
|
+
session.endSession();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/store/repos/base.ts
|
|
43
|
+
import { ObjectId } from "mongodb";
|
|
44
|
+
function toEntity(doc) {
|
|
45
|
+
const { _id, ...rest } = doc;
|
|
46
|
+
return { ...rest, id: _id.toString() };
|
|
47
|
+
}
|
|
48
|
+
function createRepo(collection, options = {}) {
|
|
49
|
+
const { nameField } = options;
|
|
50
|
+
return {
|
|
51
|
+
async list(filter = {}) {
|
|
52
|
+
const docs = await collection.find(filter).toArray();
|
|
53
|
+
return docs.map((doc) => toEntity(doc));
|
|
54
|
+
},
|
|
55
|
+
async getById(id) {
|
|
56
|
+
if (!ObjectId.isValid(id)) return null;
|
|
57
|
+
const doc = await collection.findOne({ _id: new ObjectId(id) });
|
|
58
|
+
return doc ? toEntity(doc) : null;
|
|
59
|
+
},
|
|
60
|
+
async getByName(name) {
|
|
61
|
+
if (!nameField) return null;
|
|
62
|
+
const doc = await collection.findOne({ [nameField]: name });
|
|
63
|
+
return doc ? toEntity(doc) : null;
|
|
64
|
+
},
|
|
65
|
+
async create(data) {
|
|
66
|
+
const now = /* @__PURE__ */ new Date();
|
|
67
|
+
const result = await collection.insertOne({ ...data, createdAt: now, updatedAt: now });
|
|
68
|
+
return { ...data, createdAt: now, updatedAt: now, id: result.insertedId.toString() };
|
|
69
|
+
},
|
|
70
|
+
async update(id, patch) {
|
|
71
|
+
if (!ObjectId.isValid(id)) return null;
|
|
72
|
+
const now = /* @__PURE__ */ new Date();
|
|
73
|
+
const result = await collection.findOneAndUpdate(
|
|
74
|
+
{ _id: new ObjectId(id) },
|
|
75
|
+
{ $set: { ...patch, updatedAt: now } },
|
|
76
|
+
{ returnDocument: "after" }
|
|
77
|
+
);
|
|
78
|
+
return result ? toEntity(result) : null;
|
|
79
|
+
},
|
|
80
|
+
async delete(id) {
|
|
81
|
+
if (!ObjectId.isValid(id)) return false;
|
|
82
|
+
const result = await collection.deleteOne({ _id: new ObjectId(id) });
|
|
83
|
+
return result.deletedCount > 0;
|
|
84
|
+
},
|
|
85
|
+
/** Raw collection access for entity-specific queries */
|
|
86
|
+
get _collection() {
|
|
87
|
+
return collection;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/errors.ts
|
|
93
|
+
var BIZ_CODE_MESSAGES = {
|
|
94
|
+
PROJECT_ARCHIVED: "\u9879\u76EE\u5DF2\u505C\u7528\uFF0C\u65E0\u6CD5\u6267\u884C\u8BE5\u64CD\u4F5C",
|
|
95
|
+
TEMPLATE_PROJECT_MISMATCH: "\u6A21\u677F\u5F52\u5C5E\u4E8E\u5176\u4ED6\u9879\u76EE\uFF0C\u65E0\u6CD5\u7528\u4E8E\u8BE5\u9879\u76EE",
|
|
96
|
+
TEMPLATE_PROJECT_IMMUTABLE: "\u6A21\u677F\u5F52\u5C5E\u9879\u76EE\u521B\u5EFA\u540E\u4E0D\u53EF\u53D8\u66F4",
|
|
97
|
+
SCOPE_IMMUTABLE: "\u4F5C\u7528\u57DF\u521B\u5EFA\u540E\u4E0D\u53EF\u53D8\u66F4",
|
|
98
|
+
TASK_PROJECT_IMMUTABLE: "\u4EFB\u52A1\u5F52\u5C5E\u9879\u76EE\u521B\u5EFA\u540E\u4E0D\u53EF\u53D8\u66F4",
|
|
99
|
+
DEFAULT_PROJECT_IMMUTABLE: "\u9ED8\u8BA4\u9879\u76EE\u4E0D\u53EF\u505C\u7528",
|
|
100
|
+
TEMPLATE_NAME_CONFLICT: "\u76EE\u6807\u9879\u76EE\u5185\u5DF2\u5B58\u5728\u540C\u540D\u6A21\u677F",
|
|
101
|
+
// 仅覆盖 404 分支(不存在);archived 走独立的 409 PROJECT_ARCHIVED,勿在此合并语义
|
|
102
|
+
PROJECT_NOT_FOUND: "\u9879\u76EE\u4E0D\u5B58\u5728",
|
|
103
|
+
AGENT_SKILL_SCOPE_CONFLICT: "Agent \u7ED1\u5B9A\u7684 Skill \u4F5C\u7528\u57DF\u4E0D\u517C\u5BB9\uFF08\u5168\u5C40 Agent \u4EC5\u53EF\u7ED1\u5B9A\u5168\u5C40 Skill\uFF09",
|
|
104
|
+
MODEL_CODE_EXISTS: "\u6A21\u578B code \u5DF2\u5B58\u5728",
|
|
105
|
+
MODEL_CODE_IMMUTABLE: "\u6A21\u578B code \u521B\u5EFA\u540E\u4E0D\u53EF\u53D8",
|
|
106
|
+
MODEL_ALIAS_IN_USE: "\u6A21\u578B\u6620\u5C04\u88AB Agent \u5F15\u7528\uFF0C\u7981\u6B62\u5220\u9664",
|
|
107
|
+
ENUM_ENTRY_BUILTIN: "\u5185\u7F6E\u679A\u4E3E\u503C\u7981\u6B62\u5220\u9664\u6216\u4FEE\u6539",
|
|
108
|
+
ENUM_ENTRY_IN_USE: "\u679A\u4E3E\u503C\u88AB\u8D44\u6E90\u5F15\u7528\uFF0C\u7981\u6B62\u5220\u9664",
|
|
109
|
+
ENUM_VALUE_CONFLICT: "\u679A\u4E3E\u503C\u5DF2\u5B58\u5728",
|
|
110
|
+
NAME_SCOPE_CONFLICT: "\u8BE5\u4F5C\u7528\u57DF\u5185\u5DF2\u5B58\u5728\u540C\u540D\u8D44\u6E90\uFF08\u5168\u5C40\u8D44\u6E90\u4E0E\u9879\u76EE\u8D44\u6E90\u4E0D\u53EF\u540C\u540D\uFF09",
|
|
111
|
+
TASK_DOC_NOT_SET: "\u4EFB\u52A1\u672A\u5173\u8054\u4EFB\u52A1\u6587\u4EF6",
|
|
112
|
+
TASK_DOC_NOT_FOUND: "\u4EFB\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728",
|
|
113
|
+
TASK_DOC_SECTION_NOT_FOUND: "\u4EFB\u52A1\u6587\u4EF6\u4E2D\u672A\u627E\u5230\u5339\u914D\u6BB5\u843D",
|
|
114
|
+
TASK_AT_PAUSE_POINT: "\u4EFB\u52A1\u6682\u505C\u4E8E\u5BA1\u6279\u70B9\uFF0C\u9700\u5148\u5B8C\u6210\u5BA1\u6279",
|
|
115
|
+
TASK_NOT_AT_PAUSE_POINT: "\u4EFB\u52A1\u4E0D\u5728\u5BA1\u6279\u70B9\uFF0C\u65E0\u9700\u5BA1\u6279",
|
|
116
|
+
// N020 D1:记录写端点错误码(路由层校验前置顺序 ①-⑥ 对应)
|
|
117
|
+
NODE_NOT_IN_INSTANCE: "\u8282\u70B9\u4E0D\u5728\u4EFB\u52A1 DAG \u5B9E\u4F8B\u4E2D\uFF08\u5148 task context \u67E5\u8282\u70B9\u6E05\u5355\uFF09",
|
|
118
|
+
CHECK_NOT_FOUND: "\u5B8C\u6210\u5224\u5B9A\u6761\u76EE\u4E0D\u5B58\u5728\uFF08id \u5931\u6548\u6216\u5DF2\u88AB\u8986\u76D6\uFF0C\u91CD\u65B0 task context \u67E5\u770B\u6761\u76EE\uFF09",
|
|
119
|
+
NODE_RECORD_NOT_WRITABLE: "\u8282\u70B9\u8BB0\u5F55\u4E0D\u53EF\u5199\uFF08\u8282\u70B9\u672A\u6FC0\u6D3B/\u5DF2\u8DF3\u8FC7\u2014\u2014\u6D41\u7A0B\u672A\u63A8\u8FDB\u5230\u8BE5\u8282\u70B9\uFF09",
|
|
120
|
+
TASK_TERMINATED: "\u4EFB\u52A1\u5DF2\u5B8C\u7ED3\uFF08completed/cancelled\uFF09\uFF0C\u7981\u6B62\u5199\u5165",
|
|
121
|
+
// T202608240003:类型化任务节点路径(创建时剪枝 + 类型×轨道入口校验)
|
|
122
|
+
TASK_PRUNE_GRAPH_INVALID: "\u526A\u679D\u540E DAG \u56FE\u4E0D\u5B8C\u6574\uFF08\u8282\u70B9\u6E05\u5355\u4E0E\u6A21\u677F\u7ED3\u6784\u4E0D\u517C\u5BB9\uFF0C\u89C1\u9519\u8BEF\u8BE6\u60C5\uFF09",
|
|
123
|
+
TASK_SKIP_NODE_NOT_FOUND: "skip \u6E05\u5355\u542B\u6A21\u677F\u4E0D\u5B58\u5728\u7684\u8282\u70B9 id\uFF08\u89C1\u9519\u8BEF\u8BE6\u60C5\u7684\u5408\u6CD5\u8282\u70B9\u6E05\u5355\uFF09",
|
|
124
|
+
TASK_TYPE_TRACK_MISMATCH: "\u4EFB\u52A1\u7C7B\u578B\u4E0E\u8F68\u9053\u7EC4\u5408\u975E\u6CD5\uFF08research \u8F68\u9053\u4E3A\u8C03\u7814\u7C7B\u578B\u4E13\u7528\u503C\uFF1BUI \u5FAE\u8C03\u4EC5\u9002\u7528 ui \u8F68\u9053\uFF09"
|
|
125
|
+
};
|
|
126
|
+
var AppError = class extends Error {
|
|
127
|
+
};
|
|
128
|
+
var NotFoundError = class extends AppError {
|
|
129
|
+
constructor(resource, id, options) {
|
|
130
|
+
super(options?.message ?? `${resource} not found: ${id}`);
|
|
131
|
+
this.resource = resource;
|
|
132
|
+
this.id = id;
|
|
133
|
+
this.name = "NotFoundError";
|
|
134
|
+
if (options?.bizCode) {
|
|
135
|
+
this.bizCode = options.bizCode;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
resource;
|
|
139
|
+
id;
|
|
140
|
+
statusCode = 404;
|
|
141
|
+
code = "not_found";
|
|
142
|
+
bizCode;
|
|
143
|
+
toResponse() {
|
|
144
|
+
return { error: this.bizCode ?? this.code, message: this.message, resource: this.resource, id: this.id };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var BadRequestError = class extends AppError {
|
|
148
|
+
constructor(message, field, options) {
|
|
149
|
+
super(options?.message ?? message);
|
|
150
|
+
this.field = field;
|
|
151
|
+
this.name = "BadRequestError";
|
|
152
|
+
if (options?.bizCode) {
|
|
153
|
+
this.bizCode = options.bizCode;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
field;
|
|
157
|
+
statusCode = 400;
|
|
158
|
+
code = "bad_request";
|
|
159
|
+
bizCode;
|
|
160
|
+
toResponse() {
|
|
161
|
+
return { error: this.bizCode ?? this.code, message: this.message, ...this.field ? { field: this.field } : {} };
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var ConflictError = class extends AppError {
|
|
165
|
+
constructor(resource, id, options) {
|
|
166
|
+
super(options?.message ?? `${resource} conflict: ${id}`);
|
|
167
|
+
this.resource = resource;
|
|
168
|
+
this.id = id;
|
|
169
|
+
this.name = "ConflictError";
|
|
170
|
+
if (options?.bizCode) {
|
|
171
|
+
this.bizCode = options.bizCode;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
resource;
|
|
175
|
+
id;
|
|
176
|
+
statusCode = 409;
|
|
177
|
+
code = "conflict";
|
|
178
|
+
bizCode;
|
|
179
|
+
toResponse() {
|
|
180
|
+
return { error: this.bizCode ?? this.code, message: this.message, resource: this.resource, id: this.id };
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var ValidationError = class extends AppError {
|
|
184
|
+
constructor(message, issues, options) {
|
|
185
|
+
super(options?.message ?? message);
|
|
186
|
+
this.issues = issues;
|
|
187
|
+
this.name = "ValidationError";
|
|
188
|
+
if (options?.bizCode) {
|
|
189
|
+
this.bizCode = options.bizCode;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
issues;
|
|
193
|
+
statusCode = 422;
|
|
194
|
+
code = "validation_error";
|
|
195
|
+
bizCode;
|
|
196
|
+
toResponse() {
|
|
197
|
+
return { error: this.bizCode ?? this.code, message: this.message, issues: this.issues };
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/store/repos/skill.repo.ts
|
|
202
|
+
function createSkillRepo(db) {
|
|
203
|
+
const base = createRepo(db.collection("skills"), { nameField: "name" });
|
|
204
|
+
return {
|
|
205
|
+
...base,
|
|
206
|
+
async createSkill(data) {
|
|
207
|
+
await assertNameScopeAvailable(base._collection, data.name, data.scope, data.projectId);
|
|
208
|
+
return base.create(data);
|
|
209
|
+
},
|
|
210
|
+
async updateByName(name, patch) {
|
|
211
|
+
const skill = await base.getByName(name);
|
|
212
|
+
if (!skill || !skill.id) return null;
|
|
213
|
+
return base.update(skill.id, patch);
|
|
214
|
+
},
|
|
215
|
+
async deleteByName(name) {
|
|
216
|
+
const skill = await base.getByName(name);
|
|
217
|
+
if (!skill || !skill.id) return false;
|
|
218
|
+
return base.delete(skill.id);
|
|
219
|
+
},
|
|
220
|
+
async updateByNameScoped(name, scope, projectId, patch) {
|
|
221
|
+
const skill = await this.getByNameScoped(name, scope, projectId);
|
|
222
|
+
if (!skill || !skill.id) return null;
|
|
223
|
+
return base.update(skill.id, patch);
|
|
224
|
+
},
|
|
225
|
+
async deleteByNameScoped(name, scope, projectId) {
|
|
226
|
+
const skill = await this.getByNameScoped(name, scope, projectId);
|
|
227
|
+
if (!skill || !skill.id) return false;
|
|
228
|
+
return base.delete(skill.id);
|
|
229
|
+
},
|
|
230
|
+
async getByNameScoped(name, scope, projectId) {
|
|
231
|
+
const doc = await base._collection.findOne(
|
|
232
|
+
scope === "project" ? { name, scope: "project", projectId } : { name, scope: "global" }
|
|
233
|
+
);
|
|
234
|
+
return doc ? toEntity(doc) : null;
|
|
235
|
+
},
|
|
236
|
+
async listSkillsByScope(projectId) {
|
|
237
|
+
return base.list({ $or: [{ scope: "global" }, { scope: "project", projectId }] });
|
|
238
|
+
},
|
|
239
|
+
async listSkillsByScopeFilter(scope, projectId) {
|
|
240
|
+
return base.list(scope === "project" ? { scope: "project", projectId } : { scope: "global" });
|
|
241
|
+
},
|
|
242
|
+
async listSkillsByNames(names, scope, projectId) {
|
|
243
|
+
if (names.length === 0) return /* @__PURE__ */ new Map();
|
|
244
|
+
const filter = scope === void 0 ? { name: { $in: names } } : scope === "project" ? { name: { $in: names }, $or: [{ scope: "global" }, { scope: "project", projectId }] } : { name: { $in: names }, scope: "global" };
|
|
245
|
+
const skills = await base.list(filter);
|
|
246
|
+
return new Map(skills.map((s) => [s.name, s]));
|
|
247
|
+
},
|
|
248
|
+
async copySkill(sourceName, source, target, newName) {
|
|
249
|
+
const src = await base._collection.findOne(
|
|
250
|
+
source.scope === "project" ? { name: sourceName, scope: "project", projectId: source.projectId } : { name: sourceName, scope: "global" }
|
|
251
|
+
);
|
|
252
|
+
if (!src) {
|
|
253
|
+
throw new ConflictError("skill", `copy source not found: ${sourceName}`);
|
|
254
|
+
}
|
|
255
|
+
const sourceEntity = toEntity(src);
|
|
256
|
+
const sameScopeDup = await base._collection.findOne(
|
|
257
|
+
target.scope === "project" ? { name: newName, scope: "project", projectId: target.projectId } : { name: newName, scope: "global" },
|
|
258
|
+
{ projection: { _id: 1 } }
|
|
259
|
+
);
|
|
260
|
+
if (sameScopeDup) {
|
|
261
|
+
throw new ConflictError("skill", `name already exists: ${newName}`);
|
|
262
|
+
}
|
|
263
|
+
await assertNameScopeAvailable(base._collection, newName, target.scope, target.projectId);
|
|
264
|
+
const { id: _sourceId, createdAt: _ca, updatedAt: _ua, projectId: _sourceProjectId, scope: _sourceScope, ...rest } = sourceEntity;
|
|
265
|
+
const copy = {
|
|
266
|
+
...JSON.parse(JSON.stringify(rest)),
|
|
267
|
+
name: newName,
|
|
268
|
+
scope: target.scope,
|
|
269
|
+
version: "1.0.0",
|
|
270
|
+
...target.scope === "project" && target.projectId ? { projectId: target.projectId } : {}
|
|
271
|
+
};
|
|
272
|
+
return base.create(copy);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function assertNameScopeAvailable(collection, name, scope, projectId) {
|
|
277
|
+
const clash = await collection.findOne(scope === "global" ? { name, scope: "project" } : { name, scope: "global" });
|
|
278
|
+
if (clash) {
|
|
279
|
+
throw new ConflictError("skill", name, {
|
|
280
|
+
bizCode: "NAME_SCOPE_CONFLICT",
|
|
281
|
+
message: BIZ_CODE_MESSAGES.NAME_SCOPE_CONFLICT
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/store/repos/agent.repo.ts
|
|
287
|
+
function createAgentRepo(db) {
|
|
288
|
+
const base = createRepo(db.collection("agents"), { nameField: "name" });
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
async createAgent(data) {
|
|
292
|
+
await assertNameScopeAvailable2(base._collection, data.name, data.scope, data.projectId);
|
|
293
|
+
return base.create(data);
|
|
294
|
+
},
|
|
295
|
+
async updateByName(name, patch) {
|
|
296
|
+
const agent = await base.getByName(name);
|
|
297
|
+
if (!agent || !agent.id) return null;
|
|
298
|
+
return base.update(agent.id, patch);
|
|
299
|
+
},
|
|
300
|
+
async deleteByName(name) {
|
|
301
|
+
const agent = await base.getByName(name);
|
|
302
|
+
if (!agent || !agent.id) return false;
|
|
303
|
+
return base.delete(agent.id);
|
|
304
|
+
},
|
|
305
|
+
async updateByNameScoped(name, scope, projectId, patch) {
|
|
306
|
+
const agent = await this.getByNameScoped(name, scope, projectId);
|
|
307
|
+
if (!agent || !agent.id) return null;
|
|
308
|
+
return base.update(agent.id, patch);
|
|
309
|
+
},
|
|
310
|
+
async deleteByNameScoped(name, scope, projectId) {
|
|
311
|
+
const agent = await this.getByNameScoped(name, scope, projectId);
|
|
312
|
+
if (!agent || !agent.id) return false;
|
|
313
|
+
return base.delete(agent.id);
|
|
314
|
+
},
|
|
315
|
+
async getByNameScoped(name, scope, projectId) {
|
|
316
|
+
const doc = await base._collection.findOne(
|
|
317
|
+
scope === "project" ? { name, scope: "project", projectId } : { name, scope: "global" }
|
|
318
|
+
);
|
|
319
|
+
return doc ? toEntity(doc) : null;
|
|
320
|
+
},
|
|
321
|
+
async listAgentsByScope(projectId) {
|
|
322
|
+
return base.list({ $or: [{ scope: "global" }, { scope: "project", projectId }] });
|
|
323
|
+
},
|
|
324
|
+
async listAgentsByScopeFilter(scope, projectId) {
|
|
325
|
+
return base.list(scope === "project" ? { scope: "project", projectId } : { scope: "global" });
|
|
326
|
+
},
|
|
327
|
+
async copyAgent(sourceName, source, target, newName, skillRepo) {
|
|
328
|
+
const src = await base._collection.findOne(
|
|
329
|
+
source.scope === "project" ? { name: sourceName, scope: "project", projectId: source.projectId } : { name: sourceName, scope: "global" }
|
|
330
|
+
);
|
|
331
|
+
if (!src) {
|
|
332
|
+
throw new ConflictError("agent", `copy source not found: ${sourceName}`);
|
|
333
|
+
}
|
|
334
|
+
const sourceEntity = toEntity(src);
|
|
335
|
+
const sameScopeDup = await base._collection.findOne(
|
|
336
|
+
target.scope === "project" ? { name: newName, scope: "project", projectId: target.projectId } : { name: newName, scope: "global" },
|
|
337
|
+
{ projection: { _id: 1 } }
|
|
338
|
+
);
|
|
339
|
+
if (sameScopeDup) {
|
|
340
|
+
throw new ConflictError("agent", `name already exists: ${newName}`);
|
|
341
|
+
}
|
|
342
|
+
await assertNameScopeAvailable2(base._collection, newName, target.scope, target.projectId);
|
|
343
|
+
await assertBoundSkillsCompatible(skillRepo, target.scope, target.projectId, src.boundSkills ?? []);
|
|
344
|
+
const { id: _sourceId, createdAt: _ca, updatedAt: _ua, projectId: _sourceProjectId, scope: _sourceScope, ...rest } = sourceEntity;
|
|
345
|
+
const copy = {
|
|
346
|
+
...JSON.parse(JSON.stringify(rest)),
|
|
347
|
+
name: newName,
|
|
348
|
+
scope: target.scope,
|
|
349
|
+
version: "1.0.0",
|
|
350
|
+
...target.scope === "project" && target.projectId ? { projectId: target.projectId } : {}
|
|
351
|
+
};
|
|
352
|
+
return base.create(copy);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async function assertNameScopeAvailable2(collection, name, scope, projectId) {
|
|
357
|
+
const clash = await collection.findOne(scope === "global" ? { name, scope: "project" } : { name, scope: "global" });
|
|
358
|
+
if (clash) {
|
|
359
|
+
throw new ConflictError("agent", name, {
|
|
360
|
+
bizCode: "NAME_SCOPE_CONFLICT",
|
|
361
|
+
message: BIZ_CODE_MESSAGES.NAME_SCOPE_CONFLICT
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function assertBoundSkillsCompatible(skillRepo, agentScope, agentProjectId, boundSkills) {
|
|
366
|
+
if (boundSkills.length === 0) return;
|
|
367
|
+
const all = await skillRepo.listSkillsByNames(boundSkills);
|
|
368
|
+
for (const name of boundSkills) {
|
|
369
|
+
const skill = all.get(name);
|
|
370
|
+
if (!skill) {
|
|
371
|
+
throw new BadRequestError(`boundSkill not found: ${name}`, "boundSkills");
|
|
372
|
+
}
|
|
373
|
+
const compatible = skill.scope === "global" || agentScope === "project" && skill.scope === "project" && skill.projectId === agentProjectId;
|
|
374
|
+
if (!compatible) {
|
|
375
|
+
throw new ConflictError("agent", name, {
|
|
376
|
+
bizCode: "AGENT_SKILL_SCOPE_CONFLICT",
|
|
377
|
+
message: BIZ_CODE_MESSAGES.AGENT_SKILL_SCOPE_CONFLICT
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/store/repos/dag-template.repo.ts
|
|
384
|
+
function createDagTemplateRepo(db) {
|
|
385
|
+
const base = createRepo(db.collection("dag_templates"));
|
|
386
|
+
return {
|
|
387
|
+
...base,
|
|
388
|
+
createDagTemplate(data) {
|
|
389
|
+
return base.create(data);
|
|
390
|
+
},
|
|
391
|
+
async listDagTemplates(filter) {
|
|
392
|
+
const query = {};
|
|
393
|
+
if (filter?.projectId) query.projectId = filter.projectId;
|
|
394
|
+
if (filter?.name) query.name = filter.name;
|
|
395
|
+
return base.list(query);
|
|
396
|
+
},
|
|
397
|
+
async copyDagTemplate(sourceId, targetProjectId, newName) {
|
|
398
|
+
const source = await base.getById(sourceId);
|
|
399
|
+
if (!source) {
|
|
400
|
+
throw new ConflictError("dagTemplate", `copy source not found: ${sourceId}`);
|
|
401
|
+
}
|
|
402
|
+
const dup = await base._collection.findOne(
|
|
403
|
+
{ projectId: targetProjectId, name: newName },
|
|
404
|
+
{ projection: { _id: 1 } }
|
|
405
|
+
);
|
|
406
|
+
if (dup) {
|
|
407
|
+
throw new ConflictError("dagTemplate", `name conflict in target project: ${newName}`);
|
|
408
|
+
}
|
|
409
|
+
const { id: _sourceId, createdAt: _ca, updatedAt: _ua, ...rest } = source;
|
|
410
|
+
const copy = {
|
|
411
|
+
...rest,
|
|
412
|
+
name: newName,
|
|
413
|
+
projectId: targetProjectId,
|
|
414
|
+
isDefault: false,
|
|
415
|
+
version: "1.0.0"
|
|
416
|
+
};
|
|
417
|
+
return base.create(copy);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/schemas/task.ts
|
|
423
|
+
import { z as z3 } from "zod";
|
|
424
|
+
|
|
425
|
+
// src/schemas/skill.ts
|
|
426
|
+
import { z } from "zod";
|
|
427
|
+
var SkillScopeSchema = z.enum(["global", "project"]);
|
|
428
|
+
var OBJECT_ID_HEX = /^[a-f0-9]{24}$/;
|
|
429
|
+
var SkillScopeRefine = (val, ctx) => {
|
|
430
|
+
if (val.scope === "project" && !val.projectId) {
|
|
431
|
+
ctx.addIssue({
|
|
432
|
+
code: "custom",
|
|
433
|
+
path: ["projectId"],
|
|
434
|
+
message: "scope=project \u65F6 projectId \u5FC5\u586B"
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
var SkillShapeSchema = z.object({
|
|
439
|
+
id: z.string().optional(),
|
|
440
|
+
name: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "kebab-case only"),
|
|
441
|
+
description: z.string(),
|
|
442
|
+
content: z.string(),
|
|
443
|
+
category: z.string(),
|
|
444
|
+
version: z.string().default("1.0.0"),
|
|
445
|
+
/** 作用域:global=跨项目复用(默认,仅作存量迁移读侧兜底)/ project=项目专用(N012 D3) */
|
|
446
|
+
scope: SkillScopeSchema.default("global"),
|
|
447
|
+
/** 仅 scope=project 时必填且为合法 ObjectId hex(superRefine 保证) */
|
|
448
|
+
projectId: z.string().regex(OBJECT_ID_HEX, "projectId must be a valid ObjectId hex").optional(),
|
|
449
|
+
createdAt: z.date().optional(),
|
|
450
|
+
updatedAt: z.date().optional()
|
|
451
|
+
});
|
|
452
|
+
var SkillSchema = SkillShapeSchema.superRefine(SkillScopeRefine);
|
|
453
|
+
var SkillCreateSchema = SkillShapeSchema.omit({
|
|
454
|
+
id: true,
|
|
455
|
+
createdAt: true,
|
|
456
|
+
updatedAt: true
|
|
457
|
+
}).extend({ scope: SkillScopeSchema }).superRefine(SkillScopeRefine);
|
|
458
|
+
var SkillUpdateSchema = SkillShapeSchema.omit({
|
|
459
|
+
id: true,
|
|
460
|
+
createdAt: true,
|
|
461
|
+
updatedAt: true
|
|
462
|
+
}).extend({
|
|
463
|
+
version: z.string().optional(),
|
|
464
|
+
scope: SkillScopeSchema.optional()
|
|
465
|
+
}).partial().superRefine(SkillScopeRefine);
|
|
466
|
+
var SkillCopySchema = z.object({
|
|
467
|
+
newName: z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "kebab-case only"),
|
|
468
|
+
newScope: SkillScopeSchema,
|
|
469
|
+
targetProjectId: z.string().regex(OBJECT_ID_HEX, "projectId must be a valid ObjectId hex").optional()
|
|
470
|
+
}).superRefine((val, ctx) => {
|
|
471
|
+
if (val.newScope === "project" && !val.targetProjectId) {
|
|
472
|
+
ctx.addIssue({
|
|
473
|
+
code: "custom",
|
|
474
|
+
path: ["targetProjectId"],
|
|
475
|
+
message: "newScope=project \u65F6 targetProjectId \u5FC5\u586B"
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// src/schemas/dag-template.ts
|
|
481
|
+
import { z as z2 } from "zod";
|
|
482
|
+
var DagNodePhaseSchema = z2.string();
|
|
483
|
+
var DagNodeTrackSchema = z2.string();
|
|
484
|
+
var DAG_PHASES = ["entry", "track", "test", "exit"];
|
|
485
|
+
var DAG_TRACKS = ["backend", "ui", "all"];
|
|
486
|
+
var NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
487
|
+
var DagNodeSchema = z2.object({
|
|
488
|
+
id: z2.string().regex(NODE_ID_PATTERN, "node id \u53EA\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u4E0B\u5212\u7EBF/\u8FDE\u5B57\u7B26\uFF08\u70B9\u8DEF\u5F84\u62FC\u63A5\u5B89\u5168\u524D\u63D0\uFF09"),
|
|
489
|
+
label: z2.string(),
|
|
490
|
+
phase: DagNodePhaseSchema,
|
|
491
|
+
track: DagNodeTrackSchema,
|
|
492
|
+
prompt: z2.string(),
|
|
493
|
+
skills: z2.array(z2.string()).default([])
|
|
494
|
+
});
|
|
495
|
+
var PausePointTypeSchema = z2.enum(["human_approval", "checkpoint"]);
|
|
496
|
+
var PAUSE_POINT_TYPES = ["human_approval", "checkpoint"];
|
|
497
|
+
var EdgePausePointBaseSchema = z2.object({
|
|
498
|
+
type: PausePointTypeSchema,
|
|
499
|
+
description: z2.string(),
|
|
500
|
+
autoResume: z2.boolean().default(false)
|
|
501
|
+
});
|
|
502
|
+
var EdgePausePointSchema = EdgePausePointBaseSchema.superRefine((pp, ctx) => {
|
|
503
|
+
if (pp.type === "human_approval" && pp.autoResume) {
|
|
504
|
+
ctx.addIssue({
|
|
505
|
+
code: "custom",
|
|
506
|
+
message: "edge pausePoint \u7EC4\u5408\u975E\u6CD5\uFF1Ahuman_approval \u5FC5\u987B autoResume=false\uFF08\u4EBA\u5DE5\u5BA1\u6279\u5FC5\u987B\u505C\u7559\u7B49\u4EBA\u51B3\u7B56\uFF09"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
if (pp.type === "checkpoint" && !pp.autoResume) {
|
|
510
|
+
ctx.addIssue({
|
|
511
|
+
code: "custom",
|
|
512
|
+
message: "edge pausePoint \u7EC4\u5408\u975E\u6CD5\uFF1Acheckpoint \u5FC5\u987B autoResume=true\uFF08\u68C0\u67E5\u70B9\u4EC5\u8BB0\u5F55\u4E0D\u505C\u7559\uFF0C\u5199\u6B7B\u5728\u7EC4\u5408\u7EA6\u675F\u91CC\uFF09"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
var DagEdgeSchema = z2.object({
|
|
517
|
+
from: z2.string(),
|
|
518
|
+
to: z2.string(),
|
|
519
|
+
condition: z2.string().optional(),
|
|
520
|
+
/** N017 D1:边暂停点(可选)。human_approval=人工审批门(advance 落 paused 等 approve);checkpoint=检查点(仅记录直接流转) */
|
|
521
|
+
pausePoint: EdgePausePointSchema.optional()
|
|
522
|
+
});
|
|
523
|
+
var DagTemplateSchema = z2.object({
|
|
524
|
+
id: z2.string().optional(),
|
|
525
|
+
name: z2.string(),
|
|
526
|
+
/** 归属项目 id(N012:实体层去哨兵 default,由路由层解析写入——校验项目存在且 active 后落库) */
|
|
527
|
+
projectId: z2.string(),
|
|
528
|
+
description: z2.string(),
|
|
529
|
+
nodes: z2.array(DagNodeSchema),
|
|
530
|
+
edges: z2.array(DagEdgeSchema),
|
|
531
|
+
/** UI 布局坐标(nodeId → {x,y})。UI 关注点,advance 引擎零感知;旧模板无此字段 → 前端 dagre 兜底布局(N008 D2) */
|
|
532
|
+
layout: z2.record(z2.string(), z2.object({ x: z2.number(), y: z2.number() })).optional(),
|
|
533
|
+
isDefault: z2.boolean().default(false),
|
|
534
|
+
version: z2.string().default("1.0.0"),
|
|
535
|
+
createdAt: z2.date().optional(),
|
|
536
|
+
updatedAt: z2.date().optional()
|
|
537
|
+
});
|
|
538
|
+
var DagTemplateCreateSchema = DagTemplateSchema.omit({
|
|
539
|
+
id: true,
|
|
540
|
+
createdAt: true,
|
|
541
|
+
updatedAt: true
|
|
542
|
+
});
|
|
543
|
+
var DagTemplateUpdateSchema = DagTemplateCreateSchema.extend({
|
|
544
|
+
isDefault: z2.boolean().optional(),
|
|
545
|
+
version: z2.string().optional()
|
|
546
|
+
}).partial();
|
|
547
|
+
var DagTemplateCopySchema = z2.object({
|
|
548
|
+
newName: z2.string().min(1),
|
|
549
|
+
targetProjectId: z2.string().regex(OBJECT_ID_HEX, "projectId must be a valid ObjectId hex")
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// src/schemas/task.ts
|
|
553
|
+
var TASK_TYPES = ["feature", "bugfix", "ui-tweak", "research"];
|
|
554
|
+
var ENTRY_ID_REGEX = /^[a-z0-9]{6}$/;
|
|
555
|
+
var CheckItemSchema = z3.object({
|
|
556
|
+
id: z3.string().regex(ENTRY_ID_REGEX),
|
|
557
|
+
item: z3.string().min(1),
|
|
558
|
+
passed: z3.boolean()
|
|
559
|
+
});
|
|
560
|
+
var ARTIFACT_TYPES = ["prd", "tech", "code", "test", "doc", "other"];
|
|
561
|
+
var ArtifactSchema = z3.object({
|
|
562
|
+
id: z3.string().regex(ENTRY_ID_REGEX),
|
|
563
|
+
type: z3.enum(ARTIFACT_TYPES),
|
|
564
|
+
path: z3.string().min(1),
|
|
565
|
+
note: z3.string().optional(),
|
|
566
|
+
content: z3.string().max(2e5).optional()
|
|
567
|
+
});
|
|
568
|
+
var ConfirmationSchema = z3.object({
|
|
569
|
+
id: z3.string().regex(ENTRY_ID_REGEX),
|
|
570
|
+
quote: z3.string().min(1).max(2e3),
|
|
571
|
+
at: z3.date()
|
|
572
|
+
});
|
|
573
|
+
var DecisionSchema = z3.object({
|
|
574
|
+
id: z3.string().regex(ENTRY_ID_REGEX),
|
|
575
|
+
topic: z3.string().min(1),
|
|
576
|
+
decision: z3.string().min(1)
|
|
577
|
+
});
|
|
578
|
+
var ReviewSummarySchema = z3.object({
|
|
579
|
+
verdict: z3.enum(["pass", "fail"]),
|
|
580
|
+
rounds: z3.number().int().min(1),
|
|
581
|
+
critical: z3.number().int().min(0)
|
|
582
|
+
});
|
|
583
|
+
var NodeRecordSchema = z3.object({
|
|
584
|
+
summary: z3.string().optional(),
|
|
585
|
+
checks: z3.array(CheckItemSchema).default([]),
|
|
586
|
+
artifacts: z3.array(ArtifactSchema).default([]),
|
|
587
|
+
confirmations: z3.array(ConfirmationSchema).default([]),
|
|
588
|
+
decisions: z3.array(DecisionSchema).default([]),
|
|
589
|
+
review: ReviewSummarySchema.optional()
|
|
590
|
+
});
|
|
591
|
+
var TaskDocContentSchema = z3.object({
|
|
592
|
+
what: z3.string().min(1),
|
|
593
|
+
why: z3.string().min(1),
|
|
594
|
+
acceptance: z3.array(z3.string()).default([]),
|
|
595
|
+
nonGoals: z3.array(z3.string()).default([]),
|
|
596
|
+
trackNote: z3.string().optional()
|
|
597
|
+
});
|
|
598
|
+
var ArchNoteSchema = z3.object({
|
|
599
|
+
id: z3.string().regex(ENTRY_ID_REGEX),
|
|
600
|
+
text: z3.string().min(1).max(2e3),
|
|
601
|
+
at: z3.date()
|
|
602
|
+
});
|
|
603
|
+
var NodeStatusSchema = z3.string();
|
|
604
|
+
var NODE_STATUSES = ["pending", "active", "completed", "skipped"];
|
|
605
|
+
var NodeStateSchema = z3.object({
|
|
606
|
+
status: NodeStatusSchema,
|
|
607
|
+
enteredAt: z3.date().nullable().default(null),
|
|
608
|
+
completedAt: z3.date().nullable().default(null)
|
|
609
|
+
});
|
|
610
|
+
var DagInstanceSchema = z3.object({
|
|
611
|
+
templateId: z3.string(),
|
|
612
|
+
templateVersion: z3.string(),
|
|
613
|
+
nodes: z3.array(z3.object({
|
|
614
|
+
id: z3.string(),
|
|
615
|
+
label: z3.string(),
|
|
616
|
+
// N016 F9:枚举放宽(注册表权威)
|
|
617
|
+
phase: z3.string(),
|
|
618
|
+
track: z3.string(),
|
|
619
|
+
prompt: z3.string(),
|
|
620
|
+
skills: z3.array(z3.string()).default([])
|
|
621
|
+
})),
|
|
622
|
+
// N017 D1:暂停点内联到边(human_approval=审批门 / checkpoint=检查点),顶层数组删除
|
|
623
|
+
edges: z3.array(z3.object({
|
|
624
|
+
from: z3.string(),
|
|
625
|
+
to: z3.string(),
|
|
626
|
+
condition: z3.string().optional(),
|
|
627
|
+
pausePoint: EdgePausePointBaseSchema.optional()
|
|
628
|
+
})),
|
|
629
|
+
prunedNodes: z3.array(z3.string()),
|
|
630
|
+
nodeStates: z3.record(z3.string(), NodeStateSchema)
|
|
631
|
+
});
|
|
632
|
+
var HistoryActionSchema = z3.enum(["entered", "advanced", "paused", "resumed", "approved", "rejected", "completed", "cancelled"]);
|
|
633
|
+
var HistoryEntrySchema = z3.object({
|
|
634
|
+
nodeId: z3.string(),
|
|
635
|
+
action: HistoryActionSchema,
|
|
636
|
+
timestamp: z3.date().default(() => /* @__PURE__ */ new Date()),
|
|
637
|
+
details: z3.record(z3.string(), z3.unknown()).default({})
|
|
638
|
+
});
|
|
639
|
+
var TaskStatusSchema = z3.string();
|
|
640
|
+
var TaskPhaseSchema = z3.string();
|
|
641
|
+
var TASK_STATUSES = ["active", "paused", "completed", "cancelled"];
|
|
642
|
+
var TASK_PHASES = ["entry", "track", "test", "exit"];
|
|
643
|
+
var TaskSchema = z3.object({
|
|
644
|
+
id: z3.string().optional(),
|
|
645
|
+
taskId: z3.string(),
|
|
646
|
+
title: z3.string(),
|
|
647
|
+
/**
|
|
648
|
+
* 任务类型标签(T202608240003):创建时确定、此后不可变(PATCH 白名单不含 type)。
|
|
649
|
+
* 入参与落库同源 enum——持久层防脏值,模板 PRD 调研变体判定依赖精确值。
|
|
650
|
+
*/
|
|
651
|
+
type: z3.enum(TASK_TYPES).optional(),
|
|
652
|
+
/** 归属项目 id(N012:实体层去哨兵 default,由 repo 显式写入——路由层解析:显式携带校验一致性 / 缺省继承模板 projectId) */
|
|
653
|
+
projectId: z3.string(),
|
|
654
|
+
dagTemplateId: z3.string(),
|
|
655
|
+
dagInstance: DagInstanceSchema,
|
|
656
|
+
currentNode: z3.string(),
|
|
657
|
+
currentPhase: TaskPhaseSchema,
|
|
658
|
+
status: TaskStatusSchema,
|
|
659
|
+
/** 暂停位置:from 节点 id(暂停点暂停,approve 经 findNextEdge 重推导边)/ null(手动暂停) */
|
|
660
|
+
pausedAt: z3.string().nullable().default(null),
|
|
661
|
+
track: z3.string(),
|
|
662
|
+
/**
|
|
663
|
+
* N020 D1:任务级结构化文档(未写时字段缺省——语义即「未写」,不做 null 占位)。
|
|
664
|
+
* 写入口:PATCH /api/tasks/:id/doc(字段级 $set + acceptance/nonGoals 条目 $push)。
|
|
665
|
+
*/
|
|
666
|
+
doc: TaskDocContentSchema.optional(),
|
|
667
|
+
/**
|
|
668
|
+
* N020 D1:节点级执行记录(nodeId → record)。$push 到不存在的嵌套路径时
|
|
669
|
+
* MongoDB 自动创建中间对象,故单条目字段可能先于容器整体存在——读侧对
|
|
670
|
+
* record 内数组字段按 `?? []` 容错(toEntity 不跑 parse 补 default)。
|
|
671
|
+
*/
|
|
672
|
+
nodeRecords: z3.record(z3.string(), NodeRecordSchema).default({}),
|
|
673
|
+
/** N020 D1:跨节点累积素材(架构信息收集条目,服务端生成 id/at) */
|
|
674
|
+
archNotes: z3.array(ArchNoteSchema).default([]),
|
|
675
|
+
history: z3.array(HistoryEntrySchema).default([]),
|
|
676
|
+
createdAt: z3.date().optional(),
|
|
677
|
+
updatedAt: z3.date().optional()
|
|
678
|
+
});
|
|
679
|
+
var TaskCreateInputSchema = z3.object({
|
|
680
|
+
title: z3.string(),
|
|
681
|
+
track: z3.string(),
|
|
682
|
+
dagTemplateId: z3.string(),
|
|
683
|
+
/**
|
|
684
|
+
* T202608240003 创建时剪枝:显式剔除的节点 id(在轨道自动剔除之外)。
|
|
685
|
+
* 清单来源约束 = task-create skill 的类型×轨道映射矩阵(skip 只允许来自映射,禁止自由发挥);
|
|
686
|
+
* 剪枝算法与图校验见 core/task/prune.ts。
|
|
687
|
+
*/
|
|
688
|
+
skipNodes: z3.array(z3.string().regex(NODE_ID_PATTERN, "skipNodes \u6761\u76EE\u987B\u4E3A\u5408\u6CD5\u8282\u70B9 id")).optional(),
|
|
689
|
+
/** 任务类型标签(见 TASK_TYPES;与 track 的合法组合由 server 入口校验) */
|
|
690
|
+
type: z3.enum(TASK_TYPES).optional(),
|
|
691
|
+
/** 归属项目:可选(缺省 → 继承模板 projectId,D2;显式携带且 ≠ 模板 → 409 TEMPLATE_PROJECT_MISMATCH) */
|
|
692
|
+
projectId: z3.string().regex(OBJECT_ID_HEX, "projectId must be a valid ObjectId hex").optional()
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// src/task/track-match.ts
|
|
696
|
+
function trackMatchSet(taskTrack) {
|
|
697
|
+
if (taskTrack === "mixed") return /* @__PURE__ */ new Set(["backend", "ui", "all"]);
|
|
698
|
+
return /* @__PURE__ */ new Set([taskTrack, "all"]);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/task/prompt-renderer.ts
|
|
702
|
+
function renderPrompt(template, task) {
|
|
703
|
+
return template.replaceAll("{{task.title}}", task.title).replaceAll("{{task.taskId}}", task.taskId).replaceAll("{{task.projectId}}", task.projectId).replaceAll("{{task.track}}", task.track).replaceAll("{{task.currentNode}}", task.currentNode);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/task/advance-engine.ts
|
|
707
|
+
async function advanceTask(deps, taskId, input) {
|
|
708
|
+
const task = await deps.getTask(taskId);
|
|
709
|
+
if (!task) throw new NotFoundError("task", taskId);
|
|
710
|
+
if (task.status === "paused" && task.pausedAt !== null) {
|
|
711
|
+
throw new BadRequestError(
|
|
712
|
+
`Task is paused at a pause point (after node '${task.pausedAt}'). Use approve (POST /api/tasks/${taskId}/approve) with decision approved|rejected.`,
|
|
713
|
+
void 0,
|
|
714
|
+
{ bizCode: "TASK_AT_PAUSE_POINT", message: `\u4EFB\u52A1\u6682\u505C\u4E8E\u5BA1\u6279\u70B9\uFF08${task.pausedAt} \u4E4B\u540E\uFF09\uFF0C\u9700\u5148\u8C03\u7528 approve \u5B8C\u6210\u5BA1\u6279` }
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (task.status !== "active") {
|
|
718
|
+
throw new BadRequestError(
|
|
719
|
+
`Task status is '${task.status}'. Use resume() for manually paused tasks.`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const currentNode = task.dagInstance.nodes.find((n) => n.id === task.currentNode);
|
|
723
|
+
if (!currentNode) throw new NotFoundError("node", task.currentNode);
|
|
724
|
+
const now = /* @__PURE__ */ new Date();
|
|
725
|
+
const existingState = task.dagInstance.nodeStates[currentNode.id];
|
|
726
|
+
const completedState = {
|
|
727
|
+
status: "completed",
|
|
728
|
+
enteredAt: existingState?.enteredAt ?? now,
|
|
729
|
+
completedAt: now
|
|
730
|
+
};
|
|
731
|
+
const noteDetails = input?.note ? { note: input.note } : {};
|
|
732
|
+
const wantsSummary = input?.summary !== void 0;
|
|
733
|
+
if (wantsSummary && !NODE_ID_PATTERN.test(currentNode.id)) {
|
|
734
|
+
throw new BadRequestError(
|
|
735
|
+
`node id '${currentNode.id}' \u542B\u975E\u6CD5\u5B57\u7B26\uFF0Csummary \u70B9\u8DEF\u5F84\u5199\u5165\u4E0D\u5B89\u5168\uFF08\u6A21\u677F\u5C42 pattern \u5E94\u5DF2\u62E6\u622A\u2014\u2014\u5B9E\u4F8B\u6570\u636E\u5F02\u5E38\uFF09`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
const summaryPatch = wantsSummary ? { [`nodeRecords.${currentNode.id}.summary`]: input.summary } : {};
|
|
739
|
+
const nextEdge = findNextEdge(
|
|
740
|
+
task.dagInstance.edges,
|
|
741
|
+
task.dagInstance.nodes,
|
|
742
|
+
currentNode.id,
|
|
743
|
+
task.track
|
|
744
|
+
);
|
|
745
|
+
if (!nextEdge) {
|
|
746
|
+
if (checkTermination(task.dagInstance.nodes, task.dagInstance.edges, task.dagInstance.nodeStates, completedState, currentNode.id, task.track)) {
|
|
747
|
+
const patch2 = {
|
|
748
|
+
status: "completed",
|
|
749
|
+
...summaryPatch,
|
|
750
|
+
history: [...task.history, mkHistory(currentNode.id, "completed", noteDetails)],
|
|
751
|
+
dagInstance: {
|
|
752
|
+
...task.dagInstance,
|
|
753
|
+
nodeStates: {
|
|
754
|
+
...task.dagInstance.nodeStates,
|
|
755
|
+
[currentNode.id]: completedState
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
const updated2 = await deps.updateTask(taskId, patch2, {
|
|
760
|
+
expectedStatus: task.status,
|
|
761
|
+
expectedCurrentNode: task.currentNode
|
|
762
|
+
});
|
|
763
|
+
return {
|
|
764
|
+
status: "completed",
|
|
765
|
+
task: toTaskPublic(updated2)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
throw new BadRequestError("No next node found but not all terminal nodes are completed");
|
|
769
|
+
}
|
|
770
|
+
const pausePoint = nextEdge.pausePoint;
|
|
771
|
+
if (pausePoint && pausePoint.type === "human_approval" && !pausePoint.autoResume) {
|
|
772
|
+
const patch2 = {
|
|
773
|
+
status: "paused",
|
|
774
|
+
pausedAt: currentNode.id,
|
|
775
|
+
...summaryPatch,
|
|
776
|
+
history: [
|
|
777
|
+
...task.history,
|
|
778
|
+
mkHistory(currentNode.id, "completed", noteDetails),
|
|
779
|
+
mkHistory(currentNode.id, "paused", { pausePoint: pausePoint.description })
|
|
780
|
+
],
|
|
781
|
+
dagInstance: {
|
|
782
|
+
...task.dagInstance,
|
|
783
|
+
nodeStates: {
|
|
784
|
+
...task.dagInstance.nodeStates,
|
|
785
|
+
[currentNode.id]: completedState
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
const updated2 = await deps.updateTask(taskId, patch2, {
|
|
790
|
+
expectedStatus: task.status,
|
|
791
|
+
expectedCurrentNode: task.currentNode
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
status: "paused",
|
|
795
|
+
task: toTaskPublic(updated2),
|
|
796
|
+
pausePoint,
|
|
797
|
+
guidance: pausePoint.description
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const nextNode = task.dagInstance.nodes.find((n) => n.id === nextEdge.to);
|
|
801
|
+
if (!nextNode) throw new NotFoundError("node", nextEdge.to);
|
|
802
|
+
const nextNodeState = {
|
|
803
|
+
status: "active",
|
|
804
|
+
enteredAt: now,
|
|
805
|
+
completedAt: null
|
|
806
|
+
};
|
|
807
|
+
const checkpointDetails = {
|
|
808
|
+
...pausePoint ? { checkpoint: pausePoint.description, autoResume: true } : {},
|
|
809
|
+
...noteDetails
|
|
810
|
+
};
|
|
811
|
+
const patch = {
|
|
812
|
+
currentNode: nextNode.id,
|
|
813
|
+
currentPhase: nextNode.phase,
|
|
814
|
+
status: "active",
|
|
815
|
+
pausedAt: null,
|
|
816
|
+
...summaryPatch,
|
|
817
|
+
history: [
|
|
818
|
+
...task.history,
|
|
819
|
+
mkHistory(currentNode.id, "completed", noteDetails),
|
|
820
|
+
mkHistory(nextNode.id, "advanced", checkpointDetails)
|
|
821
|
+
],
|
|
822
|
+
dagInstance: {
|
|
823
|
+
...task.dagInstance,
|
|
824
|
+
nodeStates: {
|
|
825
|
+
...task.dagInstance.nodeStates,
|
|
826
|
+
[currentNode.id]: completedState,
|
|
827
|
+
[nextNode.id]: nextNodeState
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const updated = await deps.updateTask(taskId, patch, {
|
|
832
|
+
expectedStatus: task.status,
|
|
833
|
+
expectedCurrentNode: task.currentNode
|
|
834
|
+
});
|
|
835
|
+
return {
|
|
836
|
+
status: "advanced",
|
|
837
|
+
task: toTaskPublic(updated),
|
|
838
|
+
nextNode: toNodeInfo(nextNode, updated)
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function approveTask(deps, taskId, request) {
|
|
842
|
+
const task = await deps.getTask(taskId);
|
|
843
|
+
if (!task) throw new NotFoundError("task", taskId);
|
|
844
|
+
if (task.status !== "paused" || task.pausedAt === null) {
|
|
845
|
+
throw new BadRequestError(
|
|
846
|
+
`Task is not paused at a pause point (status '${task.status}'). Use advance to complete the current node.`,
|
|
847
|
+
void 0,
|
|
848
|
+
{ bizCode: "TASK_NOT_AT_PAUSE_POINT", message: `\u4EFB\u52A1\u4E0D\u5728\u5BA1\u6279\u70B9\uFF08status '${task.status}'\uFF09\uFF0C\u65E0\u9700\u5BA1\u6279\u2014\u2014\u7528 advance \u63A8\u8FDB\u5F53\u524D\u8282\u70B9` }
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const pausedNodeId = task.pausedAt;
|
|
852
|
+
const nextEdge = findNextEdge(
|
|
853
|
+
task.dagInstance.edges,
|
|
854
|
+
task.dagInstance.nodes,
|
|
855
|
+
pausedNodeId,
|
|
856
|
+
task.track
|
|
857
|
+
);
|
|
858
|
+
if (!nextEdge) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`invariant violation: pause-point edge not found for node '${pausedNodeId}' (track '${task.track}') \u2014 task ${taskId}`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const commentDetails = request.comment ? { comment: request.comment } : {};
|
|
864
|
+
if (request.decision === "rejected") {
|
|
865
|
+
const patch2 = {
|
|
866
|
+
history: [...task.history, mkHistory(pausedNodeId, "rejected", commentDetails)]
|
|
867
|
+
};
|
|
868
|
+
const updated2 = await deps.updateTask(taskId, patch2, {
|
|
869
|
+
expectedStatus: task.status,
|
|
870
|
+
expectedCurrentNode: task.currentNode
|
|
871
|
+
});
|
|
872
|
+
return {
|
|
873
|
+
status: "rejected",
|
|
874
|
+
task: toTaskPublic(updated2),
|
|
875
|
+
guidance: "\u5BA1\u6279\u9A73\u56DE\uFF1A\u4EFB\u52A1\u4FDD\u6301\u6682\u505C\u3002\u5904\u7406\u9A73\u56DE\u610F\u89C1\u540E\u53EF\u518D\u6B21\u5BA1\u6279\uFF08approve approved\uFF09\u3002"
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const nextNode = task.dagInstance.nodes.find((n) => n.id === nextEdge.to);
|
|
879
|
+
if (!nextNode) throw new NotFoundError("node", nextEdge.to);
|
|
880
|
+
const now = /* @__PURE__ */ new Date();
|
|
881
|
+
const nextNodeState = {
|
|
882
|
+
status: "active",
|
|
883
|
+
enteredAt: now,
|
|
884
|
+
completedAt: null
|
|
885
|
+
};
|
|
886
|
+
const patch = {
|
|
887
|
+
currentNode: nextNode.id,
|
|
888
|
+
currentPhase: nextNode.phase,
|
|
889
|
+
status: "active",
|
|
890
|
+
pausedAt: null,
|
|
891
|
+
history: [
|
|
892
|
+
...task.history,
|
|
893
|
+
mkHistory(pausedNodeId, "approved", commentDetails),
|
|
894
|
+
mkHistory(nextNode.id, "advanced", {})
|
|
895
|
+
],
|
|
896
|
+
dagInstance: {
|
|
897
|
+
...task.dagInstance,
|
|
898
|
+
nodeStates: {
|
|
899
|
+
...task.dagInstance.nodeStates,
|
|
900
|
+
[nextNode.id]: nextNodeState
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
const updated = await deps.updateTask(taskId, patch, {
|
|
905
|
+
expectedStatus: task.status,
|
|
906
|
+
expectedCurrentNode: task.currentNode
|
|
907
|
+
});
|
|
908
|
+
return {
|
|
909
|
+
status: "advanced",
|
|
910
|
+
task: toTaskPublic(updated),
|
|
911
|
+
nextNode: toNodeInfo(nextNode, updated)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
async function pauseTask(deps, taskId, reason) {
|
|
915
|
+
const task = await deps.getTask(taskId);
|
|
916
|
+
if (!task) throw new NotFoundError("task", taskId);
|
|
917
|
+
if (task.status !== "active") {
|
|
918
|
+
throw new BadRequestError(`Cannot pause: task status is '${task.status}'`);
|
|
919
|
+
}
|
|
920
|
+
const patch = {
|
|
921
|
+
status: "paused",
|
|
922
|
+
pausedAt: null,
|
|
923
|
+
// null = manual pause (vs. nodeId = pause-point pause)
|
|
924
|
+
history: [...task.history, mkHistory(task.currentNode, "paused", { reason: reason ?? "manual" })]
|
|
925
|
+
};
|
|
926
|
+
return deps.updateTask(taskId, patch, {
|
|
927
|
+
expectedStatus: task.status,
|
|
928
|
+
expectedCurrentNode: task.currentNode
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
async function resumeTask(deps, taskId, decision) {
|
|
932
|
+
const task = await deps.getTask(taskId);
|
|
933
|
+
if (!task) throw new NotFoundError("task", taskId);
|
|
934
|
+
if (task.status !== "paused") {
|
|
935
|
+
throw new BadRequestError(`Cannot resume: task status is '${task.status}'`);
|
|
936
|
+
}
|
|
937
|
+
if (task.pausedAt !== null) {
|
|
938
|
+
throw new BadRequestError(
|
|
939
|
+
`Task is paused at a pause point (after node '${task.pausedAt}'). Use approve (POST /api/tasks/${taskId}/approve) instead.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
const patch = {
|
|
943
|
+
status: "active",
|
|
944
|
+
history: [...task.history, mkHistory(task.currentNode, "resumed", { decision: decision ?? "manual" })]
|
|
945
|
+
};
|
|
946
|
+
return deps.updateTask(taskId, patch, {
|
|
947
|
+
expectedStatus: task.status,
|
|
948
|
+
expectedCurrentNode: task.currentNode
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function findNextEdge(edges, nodes, fromNodeId, track) {
|
|
952
|
+
const outEdges = edges.filter((e) => e.from === fromNodeId);
|
|
953
|
+
const matchSet = trackMatchSet(track);
|
|
954
|
+
const matching = outEdges.filter((e) => {
|
|
955
|
+
const target = nodes.find((n) => n.id === e.to);
|
|
956
|
+
if (!target) return false;
|
|
957
|
+
return matchSet.has(target.track);
|
|
958
|
+
});
|
|
959
|
+
if (matching.length > 1) {
|
|
960
|
+
const byTargetTrack = /* @__PURE__ */ new Map();
|
|
961
|
+
for (const edge of matching) {
|
|
962
|
+
const target = nodes.find((n) => n.id === edge.to);
|
|
963
|
+
const key = target?.track ?? "";
|
|
964
|
+
const group = byTargetTrack.get(key) ?? [];
|
|
965
|
+
group.push(edge);
|
|
966
|
+
byTargetTrack.set(key, group);
|
|
967
|
+
}
|
|
968
|
+
for (const [targetTrack, group] of byTargetTrack) {
|
|
969
|
+
if (group.length > 1) {
|
|
970
|
+
throw new BadRequestError(
|
|
971
|
+
`Multiple matching edges from node "${fromNodeId}" for track "${track}" \u2014 DAG template has a same-track fork (target track "${targetTrack}")`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
const dedicated = matching.filter((e) => {
|
|
976
|
+
const target = nodes.find((n) => n.id === e.to);
|
|
977
|
+
return target?.track !== "all";
|
|
978
|
+
});
|
|
979
|
+
return dedicated[0] ?? matching[0];
|
|
980
|
+
}
|
|
981
|
+
return matching[0] ?? null;
|
|
982
|
+
}
|
|
983
|
+
function checkTermination(nodes, edges, nodeStates, upcomingCompletedState, upcomingCompletedNodeId, track) {
|
|
984
|
+
const matchSet = trackMatchSet(track);
|
|
985
|
+
const trackFilter = (n) => matchSet.has(n.track);
|
|
986
|
+
const terminalNodes = nodes.filter(
|
|
987
|
+
(n) => trackFilter(n) && !edges.some((e) => e.from === n.id)
|
|
988
|
+
);
|
|
989
|
+
return terminalNodes.every((n) => {
|
|
990
|
+
if (n.id === upcomingCompletedNodeId) {
|
|
991
|
+
return upcomingCompletedState.status === "completed" || upcomingCompletedState.status === "skipped";
|
|
992
|
+
}
|
|
993
|
+
const state = nodeStates[n.id];
|
|
994
|
+
return state?.status === "completed" || state?.status === "skipped";
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function toTaskPublic(task) {
|
|
998
|
+
return {
|
|
999
|
+
taskId: task.taskId,
|
|
1000
|
+
title: task.title,
|
|
1001
|
+
// T202608240003:type 进 context/advance 公共投影(模板 PRD 调研变体判定依赖 context 可见)
|
|
1002
|
+
...task.type !== void 0 ? { type: task.type } : {},
|
|
1003
|
+
currentNode: task.currentNode,
|
|
1004
|
+
currentPhase: task.currentPhase,
|
|
1005
|
+
status: task.status,
|
|
1006
|
+
pausedAt: task.pausedAt,
|
|
1007
|
+
track: task.track
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function toNodeInfo(node, task) {
|
|
1011
|
+
const nextEdge = findNextEdge(
|
|
1012
|
+
task.dagInstance.edges,
|
|
1013
|
+
task.dagInstance.nodes,
|
|
1014
|
+
node.id,
|
|
1015
|
+
task.track
|
|
1016
|
+
);
|
|
1017
|
+
return {
|
|
1018
|
+
nodeId: node.id,
|
|
1019
|
+
label: node.label,
|
|
1020
|
+
track: node.track,
|
|
1021
|
+
prompt: renderPrompt(node.prompt, task),
|
|
1022
|
+
skills: node.skills,
|
|
1023
|
+
upcomingPause: nextEdge?.pausePoint?.description ?? null
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function mkHistory(nodeId, action, details) {
|
|
1027
|
+
return { nodeId, action, timestamp: /* @__PURE__ */ new Date(), details };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/task/prune.ts
|
|
1031
|
+
function assertSkipNodesExist(template, skipNodeIds) {
|
|
1032
|
+
const known = new Set(template.nodes.map((n) => n.id));
|
|
1033
|
+
const unknown = skipNodeIds.filter((id) => !known.has(id));
|
|
1034
|
+
if (unknown.length > 0) {
|
|
1035
|
+
throw new BadRequestError(
|
|
1036
|
+
`skipNodes \u542B\u6A21\u677F\u4E0D\u5B58\u5728\u7684\u8282\u70B9 id\uFF1A${unknown.join(", ")}\u3002\u6A21\u677F\u300C${template.name}\u300D\u5408\u6CD5\u8282\u70B9\uFF1A${[...known].join(", ")}`,
|
|
1037
|
+
void 0,
|
|
1038
|
+
{ bizCode: "TASK_SKIP_NODE_NOT_FOUND" }
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function graphInvalid(reason) {
|
|
1043
|
+
throw new ValidationError(
|
|
1044
|
+
`\u526A\u679D\u540E DAG \u56FE\u4E0D\u5B8C\u6574\uFF1A${reason}`,
|
|
1045
|
+
[{ code: "custom", path: ["skipNodes"], message: reason }],
|
|
1046
|
+
{ bizCode: "TASK_PRUNE_GRAPH_INVALID", message: `\u526A\u679D\u540E DAG \u56FE\u4E0D\u5B8C\u6574\uFF08${reason}\uFF09\u2014\u2014\u68C0\u67E5 skip \u6E05\u5355\u4E0E\u6A21\u677F\u7ED3\u6784\u517C\u5BB9\u6027` }
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
function pruneInstance(template, taskTrack, skipNodeIds) {
|
|
1050
|
+
if (skipNodeIds.length > 0) assertSkipNodesExist(template, skipNodeIds);
|
|
1051
|
+
const skipSet = new Set(skipNodeIds);
|
|
1052
|
+
const matchSet = trackMatchSet(taskTrack);
|
|
1053
|
+
const keptNodes = template.nodes.filter((n) => !skipSet.has(n.id) && matchSet.has(n.track));
|
|
1054
|
+
const prunedNodes = template.nodes.filter((n) => skipSet.has(n.id) || !matchSet.has(n.track)).map((n) => n.id);
|
|
1055
|
+
const keptIds = new Set(keptNodes.map((n) => n.id));
|
|
1056
|
+
const keptEdges = template.edges.filter((e) => keptIds.has(e.from) && keptIds.has(e.to));
|
|
1057
|
+
const edges = keptEdges.map((e) => ({ ...e }));
|
|
1058
|
+
const templateTerminalIds = new Set(
|
|
1059
|
+
template.nodes.filter((n) => !template.edges.some((e) => e.from === n.id)).map((n) => n.id)
|
|
1060
|
+
);
|
|
1061
|
+
const outDegree = /* @__PURE__ */ new Map();
|
|
1062
|
+
for (const e of edges) outDegree.set(e.from, (outDegree.get(e.from) ?? 0) + 1);
|
|
1063
|
+
for (const node of keptNodes) {
|
|
1064
|
+
if (outDegree.get(node.id) !== void 0) continue;
|
|
1065
|
+
if (templateTerminalIds.has(node.id)) continue;
|
|
1066
|
+
const nearestActive = findNearestActiveDescendant(template, node.id, keptIds);
|
|
1067
|
+
if (!nearestActive) {
|
|
1068
|
+
graphInvalid(`\u8282\u70B9\u300C${node.id}\u300D\u526A\u679D\u540E\u51FA\u5EA6\u4E3A 0 \u4E14\u6CBF\u6A21\u677F\u540E\u4EE3\u65E0\u6FC0\u6D3B\u8282\u70B9\uFF08\u8DEF\u5F84\u65AD\u88C2\uFF09`);
|
|
1069
|
+
}
|
|
1070
|
+
const firstTemplateOutEdge = template.edges.find((e) => e.from === node.id);
|
|
1071
|
+
edges.push({
|
|
1072
|
+
from: node.id,
|
|
1073
|
+
to: nearestActive,
|
|
1074
|
+
...firstTemplateOutEdge?.pausePoint ? { pausePoint: { ...firstTemplateOutEdge.pausePoint } } : {}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
if (keptNodes.length < 2) {
|
|
1078
|
+
graphInvalid(`\u526A\u540E\u4EC5\u5269 ${keptNodes.length} \u4E2A\u8282\u70B9\uFF08\u987B \u2265 2\uFF09\u2014\u2014skip \u6E05\u5355/\u8F68\u9053\u5254\u9664\u8FC7\u91CD`);
|
|
1079
|
+
}
|
|
1080
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1081
|
+
for (const e of edges) inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
|
|
1082
|
+
const entryNodes = keptNodes.filter((n) => !inDegree.has(n.id));
|
|
1083
|
+
if (entryNodes.length !== 1) {
|
|
1084
|
+
graphInvalid(`\u526A\u540E\u5165\u5EA6 0 \u8282\u70B9 ${entryNodes.length} \u4E2A\uFF08\u987B\u6070\u597D 1 \u4E2A entry\uFF09\uFF1A${entryNodes.map((n) => n.id).join(", ") || "\u65E0"}`);
|
|
1085
|
+
}
|
|
1086
|
+
const entry = entryNodes[0];
|
|
1087
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1088
|
+
for (const e of edges) {
|
|
1089
|
+
const list = adjacency.get(e.from) ?? [];
|
|
1090
|
+
list.push(e.to);
|
|
1091
|
+
adjacency.set(e.from, list);
|
|
1092
|
+
}
|
|
1093
|
+
const visited = /* @__PURE__ */ new Set([entry.id]);
|
|
1094
|
+
const queue = [entry.id];
|
|
1095
|
+
while (queue.length > 0) {
|
|
1096
|
+
const cur = queue.shift();
|
|
1097
|
+
for (const next of adjacency.get(cur) ?? []) {
|
|
1098
|
+
if (!visited.has(next)) {
|
|
1099
|
+
visited.add(next);
|
|
1100
|
+
queue.push(next);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const unreachable = keptNodes.filter((n) => !visited.has(n.id));
|
|
1105
|
+
if (unreachable.length > 0) {
|
|
1106
|
+
graphInvalid(`\u8282\u70B9\u4E0D\u53EF\u8FBE\uFF08\u4ECE entry\u300C${entry.id}\u300D\uFF09\uFF1A${unreachable.map((n) => n.id).join(", ")}`);
|
|
1107
|
+
}
|
|
1108
|
+
const terminals = keptNodes.filter((n) => !adjacency.has(n.id) || (adjacency.get(n.id)?.length ?? 0) === 0);
|
|
1109
|
+
if (terminals.length === 0) {
|
|
1110
|
+
graphInvalid("\u526A\u540E\u65E0\u51FA\u5EA6 0 \u8282\u70B9\uFF08\u65E0 terminal\uFF0C\u6D41\u7A0B\u4E0D\u53EF\u6536\u655B\uFF09");
|
|
1111
|
+
}
|
|
1112
|
+
const dangling = keptNodes.filter(
|
|
1113
|
+
(n) => !terminals.some((t) => t.id === n.id) && (adjacency.get(n.id)?.length ?? 0) === 0
|
|
1114
|
+
);
|
|
1115
|
+
if (dangling.length > 0) {
|
|
1116
|
+
graphInvalid(`\u975E terminal \u8282\u70B9\u51FA\u5EA6\u4E3A 0\uFF1A${dangling.map((n) => n.id).join(", ")}`);
|
|
1117
|
+
}
|
|
1118
|
+
const visitedByEngine = /* @__PURE__ */ new Set([entry.id]);
|
|
1119
|
+
let cursor = entry.id;
|
|
1120
|
+
for (let hop = 0; hop <= keptNodes.length && cursor !== null; hop++) {
|
|
1121
|
+
if (hop === keptNodes.length) {
|
|
1122
|
+
graphInvalid("\u5F15\u64CE\u8DEF\u5F84\u6A21\u62DF\u8D85\u6B65\u6570\u4E0A\u9650\uFF08\u56FE\u4E2D\u5B58\u5728\u73AF\uFF09");
|
|
1123
|
+
}
|
|
1124
|
+
let nextEdge;
|
|
1125
|
+
try {
|
|
1126
|
+
nextEdge = findNextEdge(edges, keptNodes, cursor, taskTrack);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
const reason = err instanceof BadRequestError ? err.message : String(err);
|
|
1129
|
+
graphInvalid(`\u5F15\u64CE\u8DEF\u7531\u65E0\u6CD5\u6D88\u6B67\uFF08${reason}\uFF09`);
|
|
1130
|
+
}
|
|
1131
|
+
if (nextEdge === null) break;
|
|
1132
|
+
cursor = nextEdge.to;
|
|
1133
|
+
visitedByEngine.add(cursor);
|
|
1134
|
+
}
|
|
1135
|
+
const unreachableByEngine = keptNodes.filter((n) => !visitedByEngine.has(n.id));
|
|
1136
|
+
if (unreachableByEngine.length > 0) {
|
|
1137
|
+
graphInvalid(
|
|
1138
|
+
`\u6FC0\u6D3B\u8282\u70B9\u4E0D\u5728\u5F15\u64CE\u8DEF\u5F84\u4E0A\uFF08\u5C06\u6C38\u4E45 pending\uFF0C\u68C0\u67E5\u7C7B\u578B\xD7\u8F68\u9053\u4E0E\u6A21\u677F\u7ED3\u6784\u5339\u914D\u2014\u2014\u5982 mixed \u9700\u6A21\u677F\u542B\u94FE\u95F4\u4E32\u884C\u6865\u8FB9\u3001research \u9700 skip \u6389\u4E0D\u88AB\u884C\u8D70\u7684\u5206\u652F\uFF09\uFF1A${unreachableByEngine.map((n) => n.id).join(", ")}`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
return { nodes: keptNodes, edges, prunedNodes, entryNode: entry };
|
|
1142
|
+
}
|
|
1143
|
+
function findNearestActiveDescendant(template, fromNodeId, keptIds) {
|
|
1144
|
+
const templateAdjacency = /* @__PURE__ */ new Map();
|
|
1145
|
+
for (const e of template.edges) {
|
|
1146
|
+
const list = templateAdjacency.get(e.from) ?? [];
|
|
1147
|
+
list.push(e.to);
|
|
1148
|
+
templateAdjacency.set(e.from, list);
|
|
1149
|
+
}
|
|
1150
|
+
const visited = /* @__PURE__ */ new Set([fromNodeId]);
|
|
1151
|
+
let frontier = templateAdjacency.get(fromNodeId) ?? [];
|
|
1152
|
+
while (frontier.length > 0) {
|
|
1153
|
+
const next = [];
|
|
1154
|
+
for (const candidate of frontier) {
|
|
1155
|
+
if (visited.has(candidate)) continue;
|
|
1156
|
+
visited.add(candidate);
|
|
1157
|
+
if (keptIds.has(candidate)) return candidate;
|
|
1158
|
+
next.push(...templateAdjacency.get(candidate) ?? []);
|
|
1159
|
+
}
|
|
1160
|
+
frontier = next;
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/store/repos/task.repo.ts
|
|
1166
|
+
function createTaskRepo(db) {
|
|
1167
|
+
const base = createRepo(db.collection("tasks"));
|
|
1168
|
+
return {
|
|
1169
|
+
...base,
|
|
1170
|
+
async listTasks(filter) {
|
|
1171
|
+
const match = {};
|
|
1172
|
+
if (filter?.status) match.status = filter.status;
|
|
1173
|
+
if (filter?.track) match.track = filter.track;
|
|
1174
|
+
if (filter?.projectId) match.projectId = filter.projectId;
|
|
1175
|
+
const page = filter?.page ?? 1;
|
|
1176
|
+
const limit = filter?.limit ?? 20;
|
|
1177
|
+
const skip = (page - 1) * limit;
|
|
1178
|
+
const sortSpec = filter?.sort === "createdAt" ? { createdAt: -1 } : filter?.sort === "updatedAt" ? { updatedAt: -1 } : (
|
|
1179
|
+
// progress(默认):终态组(completed/cancelled,状态机无 archived)置底,
|
|
1180
|
+
// 组内终态按 updatedAt 倒排、非终态按进度倒排(sortProgress 键保证终态组内 updatedAt 生效)
|
|
1181
|
+
{ isTerminal: 1, sortProgress: -1, updatedAt: -1 }
|
|
1182
|
+
);
|
|
1183
|
+
const pipeline = [
|
|
1184
|
+
{ $match: match },
|
|
1185
|
+
{
|
|
1186
|
+
$addFields: {
|
|
1187
|
+
// 已完成节点数(nodeStates 中 status='completed')
|
|
1188
|
+
completedCount: {
|
|
1189
|
+
$size: {
|
|
1190
|
+
$filter: {
|
|
1191
|
+
input: { $objectToArray: "$dagInstance.nodeStates" },
|
|
1192
|
+
as: "st",
|
|
1193
|
+
cond: { $eq: ["$$st.v.status", "completed"] }
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
// 参与流转节点数 = nodes 长度 - skipped 节点数(裁剪节点不计分母)
|
|
1198
|
+
totalCount: {
|
|
1199
|
+
$subtract: [
|
|
1200
|
+
{ $size: "$dagInstance.nodes" },
|
|
1201
|
+
{
|
|
1202
|
+
$size: {
|
|
1203
|
+
$filter: {
|
|
1204
|
+
input: { $objectToArray: "$dagInstance.nodeStates" },
|
|
1205
|
+
as: "st",
|
|
1206
|
+
cond: { $eq: ["$$st.v.status", "skipped"] }
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
]
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
$addFields: {
|
|
1216
|
+
// 零除防护:totalCount=0 时进度置 0(MongoDB $divide 遇零除会报错)
|
|
1217
|
+
progressSort: {
|
|
1218
|
+
$cond: {
|
|
1219
|
+
if: { $eq: ["$totalCount", 0] },
|
|
1220
|
+
then: 0,
|
|
1221
|
+
else: { $divide: ["$completedCount", "$totalCount"] }
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
isTerminal: {
|
|
1225
|
+
$cond: { if: { $in: ["$status", ["completed", "cancelled"]] }, then: 1, else: 0 }
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
$addFields: {
|
|
1231
|
+
// 终态组组内不按进度排(PRD F5 行为 1:终态组按更新时间倒排)——组内进度键置 0
|
|
1232
|
+
sortProgress: { $cond: { if: { $eq: ["$isTerminal", 1] }, then: 0, else: "$progressSort" } }
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
{ $sort: sortSpec },
|
|
1236
|
+
{ $skip: skip },
|
|
1237
|
+
{ $limit: limit }
|
|
1238
|
+
];
|
|
1239
|
+
const [items, total] = await Promise.all([
|
|
1240
|
+
base._collection.aggregate(pipeline).toArray(),
|
|
1241
|
+
base._collection.countDocuments(match)
|
|
1242
|
+
]);
|
|
1243
|
+
return {
|
|
1244
|
+
// N017 D6:列表 = 概要——剥离 dagInstance(11 节点 prompt 全文)与 history;
|
|
1245
|
+
// createdAt/updatedAt 必须保留(web TaskTable 消费 createdAt 列且为默认降序排序键)
|
|
1246
|
+
items: items.map((doc) => {
|
|
1247
|
+
const task = toEntity(doc);
|
|
1248
|
+
const summary = {
|
|
1249
|
+
taskId: task.taskId,
|
|
1250
|
+
title: task.title,
|
|
1251
|
+
...task.type !== void 0 ? { type: task.type } : {},
|
|
1252
|
+
projectId: task.projectId,
|
|
1253
|
+
dagTemplateId: task.dagTemplateId,
|
|
1254
|
+
currentNode: task.currentNode,
|
|
1255
|
+
currentPhase: task.currentPhase,
|
|
1256
|
+
status: task.status,
|
|
1257
|
+
pausedAt: task.pausedAt,
|
|
1258
|
+
track: task.track,
|
|
1259
|
+
progress: { completed: doc.completedCount, total: doc.totalCount },
|
|
1260
|
+
...task.createdAt ? { createdAt: task.createdAt } : {},
|
|
1261
|
+
...task.updatedAt ? { updatedAt: task.updatedAt } : {}
|
|
1262
|
+
};
|
|
1263
|
+
return summary;
|
|
1264
|
+
}),
|
|
1265
|
+
total
|
|
1266
|
+
};
|
|
1267
|
+
},
|
|
1268
|
+
async getByTaskId(taskId) {
|
|
1269
|
+
const doc = await base._collection.findOne({ taskId });
|
|
1270
|
+
if (!doc) return null;
|
|
1271
|
+
return base.getById(doc._id.toString());
|
|
1272
|
+
},
|
|
1273
|
+
async updateTask(taskId, patch, cas) {
|
|
1274
|
+
for (const banned of ["nodeRecords", "doc", "archNotes"]) {
|
|
1275
|
+
if (banned in patch) {
|
|
1276
|
+
throw new BadRequestError(
|
|
1277
|
+
`updateTask \u7981\u6B62\u6574\u5BF9\u8C61\u5199\u5165 '${banned}'\u2014\u2014\u7528\u70B9\u8DEF\u5F84 key\uFF08\u5982 "nodeRecords.TECH.summary"\uFF09\u6216 updateTaskPaths`,
|
|
1278
|
+
banned
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const filter = { taskId };
|
|
1283
|
+
if (cas) {
|
|
1284
|
+
filter.status = cas.expectedStatus;
|
|
1285
|
+
filter.currentNode = cas.expectedCurrentNode;
|
|
1286
|
+
}
|
|
1287
|
+
const result = await base._collection.findOneAndUpdate(
|
|
1288
|
+
filter,
|
|
1289
|
+
{ $set: { ...patch, updatedAt: /* @__PURE__ */ new Date() } },
|
|
1290
|
+
{ returnDocument: "after" }
|
|
1291
|
+
);
|
|
1292
|
+
if (!result) {
|
|
1293
|
+
throw new ConflictError("task", `CAS mismatch or not found: ${taskId}`);
|
|
1294
|
+
}
|
|
1295
|
+
return toEntity(result);
|
|
1296
|
+
},
|
|
1297
|
+
/**
|
|
1298
|
+
* N020 D1:小步原子写——$set/$push/arrayFilters 在同一条 findOneAndUpdate
|
|
1299
|
+
* 内生效(含 updatedAt)。前置存在性/权限校验归路由层(repo 只管写)。
|
|
1300
|
+
*/
|
|
1301
|
+
async updateTaskPaths(taskId, update) {
|
|
1302
|
+
const operators = {
|
|
1303
|
+
$set: { ...update.sets ?? {}, updatedAt: /* @__PURE__ */ new Date() }
|
|
1304
|
+
};
|
|
1305
|
+
const pushes = update.pushes ?? {};
|
|
1306
|
+
if (Object.keys(pushes).length > 0) {
|
|
1307
|
+
operators.$push = pushes;
|
|
1308
|
+
}
|
|
1309
|
+
const result = await base._collection.findOneAndUpdate(
|
|
1310
|
+
{ taskId },
|
|
1311
|
+
operators,
|
|
1312
|
+
{
|
|
1313
|
+
returnDocument: "after",
|
|
1314
|
+
...update.arrayFilters ? { arrayFilters: update.arrayFilters } : {}
|
|
1315
|
+
}
|
|
1316
|
+
);
|
|
1317
|
+
if (!result) {
|
|
1318
|
+
throw new NotFoundError("task", taskId);
|
|
1319
|
+
}
|
|
1320
|
+
return toEntity(result);
|
|
1321
|
+
},
|
|
1322
|
+
async createTask(input, template, resolvedProjectId) {
|
|
1323
|
+
if (!input.track) {
|
|
1324
|
+
throw new BadRequestError(`track is required`);
|
|
1325
|
+
}
|
|
1326
|
+
const pruned = pruneInstance(template, input.track, input.skipNodes ?? []);
|
|
1327
|
+
const firstNode = pruned.entryNode;
|
|
1328
|
+
const dagInstance = {
|
|
1329
|
+
templateId: template.id ?? "",
|
|
1330
|
+
templateVersion: template.version,
|
|
1331
|
+
nodes: pruned.nodes.map(deepCopyNode),
|
|
1332
|
+
edges: pruned.edges.map(deepCopyEdge),
|
|
1333
|
+
prunedNodes: pruned.prunedNodes,
|
|
1334
|
+
nodeStates: initNodeStates(pruned.nodes, firstNode.id)
|
|
1335
|
+
};
|
|
1336
|
+
if (!template.id) {
|
|
1337
|
+
throw new BadRequestError("DagTemplate has no id");
|
|
1338
|
+
}
|
|
1339
|
+
let taskId = null;
|
|
1340
|
+
let seq = await base._collection.countDocuments({
|
|
1341
|
+
taskId: { $regex: `^T${getDatePrefix()}` }
|
|
1342
|
+
});
|
|
1343
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1344
|
+
seq++;
|
|
1345
|
+
taskId = generateTaskId(seq);
|
|
1346
|
+
const exists = await base._collection.findOne({ taskId }, { projection: { _id: 1 } });
|
|
1347
|
+
if (!exists) break;
|
|
1348
|
+
taskId = null;
|
|
1349
|
+
}
|
|
1350
|
+
if (!taskId) {
|
|
1351
|
+
throw new ConflictError("task", "taskId generation failed after 3 retries");
|
|
1352
|
+
}
|
|
1353
|
+
const now = /* @__PURE__ */ new Date();
|
|
1354
|
+
const task = {
|
|
1355
|
+
taskId,
|
|
1356
|
+
title: input.title,
|
|
1357
|
+
...input.type !== void 0 ? { type: input.type } : {},
|
|
1358
|
+
projectId: resolvedProjectId,
|
|
1359
|
+
dagTemplateId: template.id,
|
|
1360
|
+
dagInstance,
|
|
1361
|
+
currentNode: firstNode.id,
|
|
1362
|
+
currentPhase: firstNode.phase,
|
|
1363
|
+
status: "active",
|
|
1364
|
+
pausedAt: null,
|
|
1365
|
+
track: input.track,
|
|
1366
|
+
nodeRecords: {},
|
|
1367
|
+
archNotes: [],
|
|
1368
|
+
history: [{
|
|
1369
|
+
nodeId: firstNode.id,
|
|
1370
|
+
action: "entered",
|
|
1371
|
+
timestamp: now,
|
|
1372
|
+
details: {}
|
|
1373
|
+
}]
|
|
1374
|
+
};
|
|
1375
|
+
const result = TaskSchema.safeParse(task);
|
|
1376
|
+
if (!result.success) {
|
|
1377
|
+
throw new ValidationError("Task validation failed", result.error.issues);
|
|
1378
|
+
}
|
|
1379
|
+
return base.create(result.data);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function getDatePrefix() {
|
|
1384
|
+
const d = /* @__PURE__ */ new Date();
|
|
1385
|
+
return d.getFullYear().toString() + (d.getMonth() + 1).toString().padStart(2, "0") + d.getDate().toString().padStart(2, "0");
|
|
1386
|
+
}
|
|
1387
|
+
function generateTaskId(seq) {
|
|
1388
|
+
return `T${getDatePrefix()}${seq.toString().padStart(4, "0")}`;
|
|
1389
|
+
}
|
|
1390
|
+
function initNodeStates(nodes, firstNodeId) {
|
|
1391
|
+
const now = /* @__PURE__ */ new Date();
|
|
1392
|
+
const states = {};
|
|
1393
|
+
for (const node of nodes) {
|
|
1394
|
+
states[node.id] = node.id === firstNodeId ? { status: "active", enteredAt: now, completedAt: null } : { status: "pending", enteredAt: null, completedAt: null };
|
|
1395
|
+
}
|
|
1396
|
+
return states;
|
|
1397
|
+
}
|
|
1398
|
+
function deepCopyNode(node) {
|
|
1399
|
+
return {
|
|
1400
|
+
id: node.id,
|
|
1401
|
+
label: node.label,
|
|
1402
|
+
phase: node.phase,
|
|
1403
|
+
track: node.track,
|
|
1404
|
+
prompt: node.prompt,
|
|
1405
|
+
skills: [...node.skills]
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function deepCopyEdge(edge) {
|
|
1409
|
+
return {
|
|
1410
|
+
from: edge.from,
|
|
1411
|
+
to: edge.to,
|
|
1412
|
+
...edge.condition ? { condition: edge.condition } : {},
|
|
1413
|
+
...edge.pausePoint ? { pausePoint: { ...edge.pausePoint } } : {}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// src/store/repos/project.repo.ts
|
|
1418
|
+
import { ObjectId as ObjectId2 } from "mongodb";
|
|
1419
|
+
var DEFAULT_PROJECT_KEY = "default";
|
|
1420
|
+
function projectCreateBody(data, key) {
|
|
1421
|
+
return {
|
|
1422
|
+
key,
|
|
1423
|
+
name: data.name,
|
|
1424
|
+
...data.description ? { description: data.description } : {},
|
|
1425
|
+
status: "active"
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function generateProjectKey(name) {
|
|
1429
|
+
const kebab = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 31).replace(/-+$/g, "");
|
|
1430
|
+
if (kebab.length >= 2) return kebab;
|
|
1431
|
+
return `project-${new ObjectId2().toHexString().slice(0, 8)}`;
|
|
1432
|
+
}
|
|
1433
|
+
function createProjectRepo(db) {
|
|
1434
|
+
const base = createRepo(db.collection("projects"));
|
|
1435
|
+
return {
|
|
1436
|
+
...base,
|
|
1437
|
+
async getByKey(key) {
|
|
1438
|
+
const doc = await base._collection.findOne({ key });
|
|
1439
|
+
return doc ? toEntity(doc) : null;
|
|
1440
|
+
},
|
|
1441
|
+
async createProject(data) {
|
|
1442
|
+
if (data.key) {
|
|
1443
|
+
const dup = await base._collection.findOne({ key: data.key }, { projection: { _id: 1 } });
|
|
1444
|
+
if (dup) {
|
|
1445
|
+
throw new ConflictError("project", `key already exists: ${data.key}`);
|
|
1446
|
+
}
|
|
1447
|
+
return base.create({ ...projectCreateBody(data, data.key) });
|
|
1448
|
+
}
|
|
1449
|
+
const baseKey = generateProjectKey(data.name);
|
|
1450
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1451
|
+
const candidate = attempt === 0 ? baseKey : `${baseKey.slice(0, 28).replace(/-+$/g, "")}-${attempt + 1}`;
|
|
1452
|
+
const exists = await base._collection.findOne({ key: candidate }, { projection: { _id: 1 } });
|
|
1453
|
+
if (!exists) {
|
|
1454
|
+
return base.create({ ...projectCreateBody(data, candidate) });
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
throw new ConflictError("project", `key generation exhausted for name: ${data.name}`);
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/store/repos/model-alias.repo.ts
|
|
1463
|
+
import { MongoServerError } from "mongodb";
|
|
1464
|
+
var AGENTS_COLLECTION = "agents";
|
|
1465
|
+
var MODEL_FIELD = "model";
|
|
1466
|
+
function createModelAliasRepo(db) {
|
|
1467
|
+
const base = createRepo(db.collection("model_aliases"), { nameField: "code" });
|
|
1468
|
+
return {
|
|
1469
|
+
...base,
|
|
1470
|
+
async listWithRefCount() {
|
|
1471
|
+
const docs = await base._collection.aggregate([
|
|
1472
|
+
{
|
|
1473
|
+
$lookup: {
|
|
1474
|
+
from: AGENTS_COLLECTION,
|
|
1475
|
+
localField: "code",
|
|
1476
|
+
foreignField: MODEL_FIELD,
|
|
1477
|
+
as: "refs"
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
{ $addFields: { refCount: { $size: "$refs" } } },
|
|
1481
|
+
{ $project: { refs: 0 } }
|
|
1482
|
+
]).toArray();
|
|
1483
|
+
return docs.map((doc) => {
|
|
1484
|
+
const { refCount, ...rest } = doc;
|
|
1485
|
+
return { ...toEntity(rest), refCount };
|
|
1486
|
+
});
|
|
1487
|
+
},
|
|
1488
|
+
async getByCode(code) {
|
|
1489
|
+
return base.getByName(code);
|
|
1490
|
+
},
|
|
1491
|
+
async createModelAlias(data) {
|
|
1492
|
+
try {
|
|
1493
|
+
return await base.create(data);
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
if (err instanceof MongoServerError && err.code === 11e3) {
|
|
1496
|
+
throw new ConflictError("model-alias", data.code, {
|
|
1497
|
+
bizCode: "MODEL_CODE_EXISTS",
|
|
1498
|
+
message: BIZ_CODE_MESSAGES.MODEL_CODE_EXISTS
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
throw err;
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
async updateByCode(code, patch) {
|
|
1505
|
+
const alias = await base.getByName(code);
|
|
1506
|
+
if (!alias || !alias.id) return null;
|
|
1507
|
+
return base.update(alias.id, patch);
|
|
1508
|
+
},
|
|
1509
|
+
async deleteByCode(code) {
|
|
1510
|
+
const refCount = await db.collection(AGENTS_COLLECTION).countDocuments({ [MODEL_FIELD]: code });
|
|
1511
|
+
if (refCount > 0) {
|
|
1512
|
+
throw new ConflictError("model-alias", code, {
|
|
1513
|
+
bizCode: "MODEL_ALIAS_IN_USE",
|
|
1514
|
+
message: `\u6A21\u578B\u6620\u5C04\u88AB ${refCount} \u4E2A Agent \u5F15\u7528\uFF0C\u7981\u6B62\u5220\u9664`
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
const alias = await base.getByName(code);
|
|
1518
|
+
if (!alias || !alias.id) return false;
|
|
1519
|
+
return base.delete(alias.id);
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
async function assertModelAliasExists(repo, code) {
|
|
1524
|
+
const alias = await repo.getByCode(code);
|
|
1525
|
+
if (!alias) {
|
|
1526
|
+
throw new NotFoundError("model-alias", code);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/store/repos/enum-registry.repo.ts
|
|
1531
|
+
var REFERENCE_CHECKS = {
|
|
1532
|
+
dag_phase: { collection: "dag_templates", query: (v) => ({ "nodes.phase": v }) },
|
|
1533
|
+
dag_track: { collection: "dag_templates", query: (v) => ({ "nodes.track": v }) },
|
|
1534
|
+
pause_type: { collection: "dag_templates", query: (v) => ({ "edges.pausePoint.type": v }) },
|
|
1535
|
+
task_status: { collection: "tasks", query: (v) => ({ status: v }) },
|
|
1536
|
+
node_status: { collection: "tasks", query: (v) => ({ "dagInstance.nodeStates.status": v }) },
|
|
1537
|
+
skill_category: { collection: "skills", query: (v) => ({ category: v }) },
|
|
1538
|
+
scope: void 0
|
|
1539
|
+
};
|
|
1540
|
+
function createEnumRegistryRepo(db) {
|
|
1541
|
+
const base = createRepo(db.collection("enum_registry"), { nameField: "category" });
|
|
1542
|
+
return {
|
|
1543
|
+
...base,
|
|
1544
|
+
async listRegistries() {
|
|
1545
|
+
return base.list();
|
|
1546
|
+
},
|
|
1547
|
+
async getRegistry(category) {
|
|
1548
|
+
return base.getByName(category);
|
|
1549
|
+
},
|
|
1550
|
+
async getEntries(category) {
|
|
1551
|
+
const registry = await base.getByName(category);
|
|
1552
|
+
return registry?.entries ?? [];
|
|
1553
|
+
},
|
|
1554
|
+
async updateRegistry(category, patch) {
|
|
1555
|
+
const existing = await base.getByName(category);
|
|
1556
|
+
if (!existing || !existing.id) return null;
|
|
1557
|
+
const existingByValue = new Map(existing.entries.map((e) => [e.value, e]));
|
|
1558
|
+
const incomingByValue = new Map(patch.entries.map((e) => [e.value, e]));
|
|
1559
|
+
for (const prev of existing.entries) {
|
|
1560
|
+
if (prev.builtin && !incomingByValue.has(prev.value)) {
|
|
1561
|
+
throw new ConflictError("enum-registry", `${category}/${prev.value}`, {
|
|
1562
|
+
bizCode: "ENUM_ENTRY_BUILTIN",
|
|
1563
|
+
message: BIZ_CODE_MESSAGES.ENUM_ENTRY_BUILTIN
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
for (const incoming of patch.entries) {
|
|
1568
|
+
const prev = existingByValue.get(incoming.value);
|
|
1569
|
+
if (!prev && incoming.builtin) {
|
|
1570
|
+
throw new ConflictError("enum-registry", `${category}/${incoming.value}`, {
|
|
1571
|
+
bizCode: "ENUM_ENTRY_BUILTIN",
|
|
1572
|
+
message: BIZ_CODE_MESSAGES.ENUM_ENTRY_BUILTIN
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
if (prev && prev.builtin === false && incoming.builtin) {
|
|
1576
|
+
throw new ConflictError("enum-registry", `${category}/${incoming.value}`, {
|
|
1577
|
+
bizCode: "ENUM_ENTRY_BUILTIN",
|
|
1578
|
+
message: BIZ_CODE_MESSAGES.ENUM_ENTRY_BUILTIN
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
if (prev?.builtin === true && incoming.builtin !== true) {
|
|
1582
|
+
throw new ConflictError("enum-registry", `${category}/${incoming.value}`, {
|
|
1583
|
+
bizCode: "ENUM_ENTRY_BUILTIN",
|
|
1584
|
+
message: BIZ_CODE_MESSAGES.ENUM_ENTRY_BUILTIN
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
if (prev?.active === true && incoming.active === false) {
|
|
1588
|
+
const ref = REFERENCE_CHECKS[category];
|
|
1589
|
+
if (ref) {
|
|
1590
|
+
const count = await db.collection(ref.collection).countDocuments(ref.query(incoming.value));
|
|
1591
|
+
if (count > 0) {
|
|
1592
|
+
throw new ConflictError("enum-registry", `${category}/${incoming.value}`, {
|
|
1593
|
+
bizCode: "ENUM_ENTRY_IN_USE",
|
|
1594
|
+
message: `${BIZ_CODE_MESSAGES.ENUM_ENTRY_IN_USE}\uFF08${count} \u4E2A\u8D44\u6E90\u5F15\u7528\uFF09`
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1601
|
+
for (const e of patch.entries) {
|
|
1602
|
+
if (seen.has(e.value)) {
|
|
1603
|
+
throw new ConflictError("enum-registry", `${category}/${e.value}`, {
|
|
1604
|
+
bizCode: "ENUM_VALUE_CONFLICT",
|
|
1605
|
+
message: BIZ_CODE_MESSAGES.ENUM_VALUE_CONFLICT
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
seen.add(e.value);
|
|
1609
|
+
}
|
|
1610
|
+
return base.update(existing.id, { entries: patch.entries });
|
|
1611
|
+
},
|
|
1612
|
+
async deleteEntry(category, value) {
|
|
1613
|
+
const existing = await base.getByName(category);
|
|
1614
|
+
if (!existing || !existing.id) return false;
|
|
1615
|
+
const entry = existing.entries.find((e) => e.value === value);
|
|
1616
|
+
if (!entry) throw new NotFoundError("enum-entry", `${category}/${value}`);
|
|
1617
|
+
if (entry.builtin) {
|
|
1618
|
+
throw new ConflictError("enum-registry", `${category}/${value}`, {
|
|
1619
|
+
bizCode: "ENUM_ENTRY_BUILTIN",
|
|
1620
|
+
message: BIZ_CODE_MESSAGES.ENUM_ENTRY_BUILTIN
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
const ref = REFERENCE_CHECKS[category];
|
|
1624
|
+
if (ref) {
|
|
1625
|
+
const count = await db.collection(ref.collection).countDocuments(ref.query(value));
|
|
1626
|
+
if (count > 0) {
|
|
1627
|
+
throw new ConflictError("enum-registry", `${category}/${value}`, {
|
|
1628
|
+
bizCode: "ENUM_ENTRY_IN_USE",
|
|
1629
|
+
message: `${BIZ_CODE_MESSAGES.ENUM_ENTRY_IN_USE}\uFF08${count} \u4E2A\u8D44\u6E90\u5F15\u7528\uFF09`
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const remaining = existing.entries.filter((e) => e.value !== value);
|
|
1634
|
+
return await base.update(existing.id, { entries: remaining }) !== null;
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/schemas/agent.ts
|
|
1640
|
+
import { z as z5 } from "zod";
|
|
1641
|
+
|
|
1642
|
+
// src/schemas/model-alias.ts
|
|
1643
|
+
import { z as z4 } from "zod";
|
|
1644
|
+
var SIMING_CODE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1645
|
+
var ModelAliasShapeSchema = z4.object({
|
|
1646
|
+
id: z4.string().optional(),
|
|
1647
|
+
/** siming code:创建后不可变(PUT 显式拒绝 MODEL_CODE_IMMUTABLE,非静默剥离) */
|
|
1648
|
+
code: z4.string().regex(SIMING_CODE_REGEX, "code \u5FC5\u987B\u4E3A kebab-case\uFF08\u5982 primary\u3001fast-model\uFF09"),
|
|
1649
|
+
/** 展示名(如「主力模型」) */
|
|
1650
|
+
name: z4.string().min(1, "\u5C55\u793A\u540D\u4E0D\u80FD\u4E3A\u7A7A"),
|
|
1651
|
+
/** 真实模型标识(如 zhipuai/glm-5.3;改此处 = 引用方全局替换生效) */
|
|
1652
|
+
realModel: z4.string().min(1, "\u771F\u5B9E\u6A21\u578B\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A"),
|
|
1653
|
+
createdAt: z4.date().optional(),
|
|
1654
|
+
updatedAt: z4.date().optional()
|
|
1655
|
+
});
|
|
1656
|
+
var ModelAliasSchema = ModelAliasShapeSchema;
|
|
1657
|
+
var ModelAliasCreateSchema = ModelAliasShapeSchema.omit({
|
|
1658
|
+
id: true,
|
|
1659
|
+
createdAt: true,
|
|
1660
|
+
updatedAt: true
|
|
1661
|
+
});
|
|
1662
|
+
var ModelAliasUpdateSchema = z4.object({
|
|
1663
|
+
code: z4.string().regex(SIMING_CODE_REGEX).optional(),
|
|
1664
|
+
name: z4.string().min(1).optional(),
|
|
1665
|
+
realModel: z4.string().min(1).optional()
|
|
1666
|
+
});
|
|
1667
|
+
var ModelAliasWithRefCountSchema = ModelAliasShapeSchema.extend({
|
|
1668
|
+
refCount: z4.number().int().min(0)
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
// src/schemas/agent.ts
|
|
1672
|
+
var AgentScopeSchema = z5.enum(["global", "project"]);
|
|
1673
|
+
var AgentScopeRefine = (val, ctx) => {
|
|
1674
|
+
if (val.scope === "project" && !val.projectId) {
|
|
1675
|
+
ctx.addIssue({
|
|
1676
|
+
code: "custom",
|
|
1677
|
+
path: ["projectId"],
|
|
1678
|
+
message: "scope=project \u65F6 projectId \u5FC5\u586B"
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
var AgentShapeSchema = z5.object({
|
|
1683
|
+
id: z5.string().optional(),
|
|
1684
|
+
name: z5.string(),
|
|
1685
|
+
description: z5.string(),
|
|
1686
|
+
systemPrompt: z5.string(),
|
|
1687
|
+
boundSkills: z5.array(z5.string()).default([]),
|
|
1688
|
+
model: z5.string(),
|
|
1689
|
+
/** 资产版本(N015 F13):install/export 版本对比依据;default 仅读侧兜底(createRepo 不做读侧解析,存量回填走 M6 迁移) */
|
|
1690
|
+
version: z5.string().default("1.0.0"),
|
|
1691
|
+
tools: z5.array(z5.string()).default([]),
|
|
1692
|
+
permissions: z5.array(z5.string()).default([]),
|
|
1693
|
+
/** 作用域:global=跨项目复用(默认,仅作存量迁移读侧兜底)/ project=项目专用(N012 D3) */
|
|
1694
|
+
scope: AgentScopeSchema.default("global"),
|
|
1695
|
+
/** 仅 scope=project 时必填且为合法 ObjectId hex(superRefine 保证) */
|
|
1696
|
+
projectId: z5.string().regex(OBJECT_ID_HEX, "projectId must be a valid ObjectId hex").optional(),
|
|
1697
|
+
createdAt: z5.date().optional(),
|
|
1698
|
+
updatedAt: z5.date().optional()
|
|
1699
|
+
});
|
|
1700
|
+
var AgentSchema = AgentShapeSchema.superRefine(AgentScopeRefine);
|
|
1701
|
+
var AgentCreateSchema = AgentShapeSchema.omit({
|
|
1702
|
+
id: true,
|
|
1703
|
+
createdAt: true,
|
|
1704
|
+
updatedAt: true
|
|
1705
|
+
}).extend({ scope: AgentScopeSchema }).extend({ model: z5.string().regex(SIMING_CODE_REGEX, "model \u5FC5\u987B\u662F siming code\uFF08kebab-case\uFF09") }).superRefine(AgentScopeRefine);
|
|
1706
|
+
var AgentUpdateSchema = AgentShapeSchema.omit({
|
|
1707
|
+
id: true,
|
|
1708
|
+
createdAt: true,
|
|
1709
|
+
updatedAt: true
|
|
1710
|
+
}).extend({
|
|
1711
|
+
boundSkills: z5.array(z5.string()).optional(),
|
|
1712
|
+
// N015:version 放行(export apply 版本递增随 PUT 携带);optional 剥离实体层 default 避免 partial 空 body 注入
|
|
1713
|
+
version: z5.string().optional(),
|
|
1714
|
+
tools: z5.array(z5.string()).optional(),
|
|
1715
|
+
permissions: z5.array(z5.string()).optional(),
|
|
1716
|
+
scope: AgentScopeSchema.optional(),
|
|
1717
|
+
// N013:model 收紧为 siming code 格式(存在性校验在 handler 层;undefined = 不更新该字段)
|
|
1718
|
+
model: z5.string().regex(SIMING_CODE_REGEX, "model \u5FC5\u987B\u662F siming code\uFF08kebab-case\uFF09").optional()
|
|
1719
|
+
}).partial().superRefine(AgentScopeRefine);
|
|
1720
|
+
var AgentCopySchema = z5.object({
|
|
1721
|
+
newName: z5.string(),
|
|
1722
|
+
newScope: AgentScopeSchema,
|
|
1723
|
+
targetProjectId: z5.string().regex(OBJECT_ID_HEX, "projectId must be a valid ObjectId hex").optional()
|
|
1724
|
+
}).superRefine((val, ctx) => {
|
|
1725
|
+
if (val.newScope === "project" && !val.targetProjectId) {
|
|
1726
|
+
ctx.addIssue({
|
|
1727
|
+
code: "custom",
|
|
1728
|
+
path: ["targetProjectId"],
|
|
1729
|
+
message: "newScope=project \u65F6 targetProjectId \u5FC5\u586B"
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
// src/schemas/advance.ts
|
|
1735
|
+
import { z as z6 } from "zod";
|
|
1736
|
+
var AdvanceRequestSchema = z6.object({
|
|
1737
|
+
note: z6.string().optional(),
|
|
1738
|
+
/** min(1).max(2000) 与 record summary 端点对齐(同一 $set 目标,两入口校验强度一致) */
|
|
1739
|
+
summary: z6.string().min(1).max(2e3).optional()
|
|
1740
|
+
});
|
|
1741
|
+
var ApproveRequestSchema = z6.object({
|
|
1742
|
+
decision: z6.enum(["approved", "rejected"]),
|
|
1743
|
+
comment: z6.string().optional()
|
|
1744
|
+
});
|
|
1745
|
+
var TaskPublicSchema = z6.object({
|
|
1746
|
+
taskId: z6.string(),
|
|
1747
|
+
title: z6.string(),
|
|
1748
|
+
// T202608240003:任务类型标签(缺省 = 裸任务/旧任务;模板 PRD 调研变体判定消费)
|
|
1749
|
+
type: z6.enum(TASK_TYPES).optional(),
|
|
1750
|
+
currentNode: z6.string(),
|
|
1751
|
+
currentPhase: z6.string(),
|
|
1752
|
+
status: z6.string(),
|
|
1753
|
+
pausedAt: z6.string().nullable(),
|
|
1754
|
+
track: z6.string()
|
|
1755
|
+
});
|
|
1756
|
+
var NodeInfoSchema = z6.object({
|
|
1757
|
+
nodeId: z6.string(),
|
|
1758
|
+
label: z6.string(),
|
|
1759
|
+
track: z6.string(),
|
|
1760
|
+
prompt: z6.string(),
|
|
1761
|
+
skills: z6.array(z6.string()),
|
|
1762
|
+
upcomingPause: z6.string().nullable()
|
|
1763
|
+
});
|
|
1764
|
+
var AdvanceResponseSchema = z6.discriminatedUnion("status", [
|
|
1765
|
+
z6.object({
|
|
1766
|
+
status: z6.literal("advanced"),
|
|
1767
|
+
task: TaskPublicSchema,
|
|
1768
|
+
nextNode: NodeInfoSchema
|
|
1769
|
+
}),
|
|
1770
|
+
z6.object({
|
|
1771
|
+
status: z6.literal("paused"),
|
|
1772
|
+
task: TaskPublicSchema,
|
|
1773
|
+
pausePoint: EdgePausePointSchema,
|
|
1774
|
+
guidance: z6.string()
|
|
1775
|
+
}),
|
|
1776
|
+
z6.object({
|
|
1777
|
+
status: z6.literal("completed"),
|
|
1778
|
+
task: TaskPublicSchema
|
|
1779
|
+
})
|
|
1780
|
+
]);
|
|
1781
|
+
var ApproveResponseSchema = z6.discriminatedUnion("status", [
|
|
1782
|
+
z6.object({
|
|
1783
|
+
status: z6.literal("advanced"),
|
|
1784
|
+
task: TaskPublicSchema,
|
|
1785
|
+
nextNode: NodeInfoSchema
|
|
1786
|
+
}),
|
|
1787
|
+
z6.object({
|
|
1788
|
+
status: z6.literal("rejected"),
|
|
1789
|
+
task: TaskPublicSchema,
|
|
1790
|
+
guidance: z6.string()
|
|
1791
|
+
}),
|
|
1792
|
+
// 「completed」分支当前引擎不可达(approve 从 paused 流转必有下一节点,技术方案 §2.2
|
|
1793
|
+
// 已删 paused→completed 直达路径)——按方案 §2.1 契约保留作前向兼容预留
|
|
1794
|
+
z6.object({
|
|
1795
|
+
status: z6.literal("completed"),
|
|
1796
|
+
task: TaskPublicSchema
|
|
1797
|
+
})
|
|
1798
|
+
]);
|
|
1799
|
+
var PauseRequestSchema = z6.object({
|
|
1800
|
+
reason: z6.string().optional()
|
|
1801
|
+
});
|
|
1802
|
+
var ResumeRequestSchema = z6.object({
|
|
1803
|
+
decision: z6.string().optional()
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
// src/schemas/task-doc.ts
|
|
1807
|
+
import { z as z7 } from "zod";
|
|
1808
|
+
var TaskContextSchema = z7.object({
|
|
1809
|
+
task: z7.object({
|
|
1810
|
+
taskId: z7.string(),
|
|
1811
|
+
title: z7.string(),
|
|
1812
|
+
// T202608240003:与 TaskPublicSchema 同步(toTaskPublic 运行时已产出,schema 声明漂移会导致 typed 消费方 strip 掉 type)
|
|
1813
|
+
type: z7.enum(TASK_TYPES).optional(),
|
|
1814
|
+
currentNode: z7.string(),
|
|
1815
|
+
currentPhase: z7.string(),
|
|
1816
|
+
status: z7.string(),
|
|
1817
|
+
pausedAt: z7.string().nullable(),
|
|
1818
|
+
track: z7.string()
|
|
1819
|
+
}),
|
|
1820
|
+
currentNode: z7.object({
|
|
1821
|
+
nodeId: z7.string(),
|
|
1822
|
+
label: z7.string(),
|
|
1823
|
+
phase: z7.string(),
|
|
1824
|
+
track: z7.string(),
|
|
1825
|
+
/** renderPrompt 渲染后(AI 推进任务的直接输入) */
|
|
1826
|
+
prompt: z7.string(),
|
|
1827
|
+
skills: z7.array(z7.string()),
|
|
1828
|
+
/** 完成当前节点后出边上的暂停点描述(经 findNextEdge 按轨选边) */
|
|
1829
|
+
upcomingPause: z7.string().nullable()
|
|
1830
|
+
}).nullable(),
|
|
1831
|
+
pausedAtEdge: z7.object({
|
|
1832
|
+
from: z7.string(),
|
|
1833
|
+
to: z7.string(),
|
|
1834
|
+
pausePoint: EdgePausePointBaseSchema
|
|
1835
|
+
}).nullable(),
|
|
1836
|
+
/** 全节点实时状态清单 */
|
|
1837
|
+
nodes: z7.array(z7.object({
|
|
1838
|
+
nodeId: z7.string(),
|
|
1839
|
+
label: z7.string(),
|
|
1840
|
+
status: z7.string(),
|
|
1841
|
+
enteredAt: z7.date().nullable(),
|
|
1842
|
+
completedAt: z7.date().nullable()
|
|
1843
|
+
})),
|
|
1844
|
+
/** N020 D1:任务级结构化文档(未写时 null) */
|
|
1845
|
+
taskDoc: TaskDocContentSchema.nullable(),
|
|
1846
|
+
/** N020 D1:节点执行记录(仅含已写记录的 nodeId) */
|
|
1847
|
+
nodeRecords: z7.record(z7.string(), NodeRecordSchema),
|
|
1848
|
+
/** N020 D1:跨节点累积素材(架构信息收集条目) */
|
|
1849
|
+
archNotes: z7.array(ArchNoteSchema)
|
|
1850
|
+
});
|
|
1851
|
+
var TaskSummarySchema = z7.object({
|
|
1852
|
+
taskId: z7.string(),
|
|
1853
|
+
title: z7.string(),
|
|
1854
|
+
// T202608240003:任务类型标签(缺省 = 裸任务/旧任务)
|
|
1855
|
+
type: z7.enum(TASK_TYPES).optional(),
|
|
1856
|
+
projectId: z7.string(),
|
|
1857
|
+
dagTemplateId: z7.string(),
|
|
1858
|
+
currentNode: z7.string(),
|
|
1859
|
+
currentPhase: z7.string(),
|
|
1860
|
+
status: z7.string(),
|
|
1861
|
+
pausedAt: z7.string().nullable(),
|
|
1862
|
+
track: z7.string(),
|
|
1863
|
+
progress: z7.object({
|
|
1864
|
+
completed: z7.number(),
|
|
1865
|
+
total: z7.number()
|
|
1866
|
+
}),
|
|
1867
|
+
createdAt: z7.date().optional(),
|
|
1868
|
+
updatedAt: z7.date().optional()
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// src/schemas/project.ts
|
|
1872
|
+
import { z as z8 } from "zod";
|
|
1873
|
+
var ProjectStatusSchema = z8.enum(["active", "archived"]);
|
|
1874
|
+
var ProjectSchema = z8.object({
|
|
1875
|
+
id: z8.string().optional(),
|
|
1876
|
+
key: z8.string().regex(/^[a-z0-9][a-z0-9-]{1,30}$/, "key \u5FC5\u987B\u4E3A kebab-case\uFF082-31 \u5B57\u7B26\uFF09"),
|
|
1877
|
+
name: z8.string().min(1).max(50),
|
|
1878
|
+
description: z8.string().max(500).optional(),
|
|
1879
|
+
status: ProjectStatusSchema.default("active"),
|
|
1880
|
+
createdAt: z8.date().optional(),
|
|
1881
|
+
updatedAt: z8.date().optional()
|
|
1882
|
+
});
|
|
1883
|
+
var ProjectCreateSchema = z8.object({
|
|
1884
|
+
key: z8.string().regex(/^[a-z0-9][a-z0-9-]{1,30}$/, "key \u5FC5\u987B\u4E3A kebab-case\uFF082-31 \u5B57\u7B26\uFF09").optional(),
|
|
1885
|
+
name: z8.string().min(1).max(50),
|
|
1886
|
+
description: z8.string().max(500).optional()
|
|
1887
|
+
});
|
|
1888
|
+
var ProjectUpdateSchema = z8.object({
|
|
1889
|
+
name: z8.string().min(1).max(50).optional(),
|
|
1890
|
+
description: z8.string().max(500).optional(),
|
|
1891
|
+
status: ProjectStatusSchema.optional()
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
// src/schemas/enum-registry.ts
|
|
1895
|
+
import { z as z9 } from "zod";
|
|
1896
|
+
var EnumRegistryCategorySchema = z9.enum([
|
|
1897
|
+
"dag_phase",
|
|
1898
|
+
"dag_track",
|
|
1899
|
+
"pause_type",
|
|
1900
|
+
"task_status",
|
|
1901
|
+
"node_status",
|
|
1902
|
+
"skill_category",
|
|
1903
|
+
"scope"
|
|
1904
|
+
]);
|
|
1905
|
+
var EnumEntrySchema = z9.object({
|
|
1906
|
+
/** 领域层存库的原始枚举值(不可臆造;builtin 值 engine/DB 强依赖;kebab 约束与 skill name 对齐,保证 DELETE /entries/:value 可寻址) */
|
|
1907
|
+
value: z9.string().regex(/^[a-z0-9][a-z0-9_-]*$/, "value \u4EC5\u652F\u6301\u5C0F\u5199\u5B57\u6BCD/\u6570\u5B57/\u4E0B\u5212\u7EBF/\u8FDE\u5B57\u7B26\uFF08kebab-case\uFF09"),
|
|
1908
|
+
/** 展示文案(消费端下拉直接取用) */
|
|
1909
|
+
label: z9.string().min(1, "label \u4E0D\u80FD\u4E3A\u7A7A"),
|
|
1910
|
+
/** 可选语义色 token(如 --success / --warning),消费端映射到 UI */
|
|
1911
|
+
color: z9.string().optional(),
|
|
1912
|
+
/** 下拉排序(升序) */
|
|
1913
|
+
order: z9.number().int().default(0),
|
|
1914
|
+
/** 内置值:禁删 + label/color 可改但 value 不可改(D8);seed 时标记 */
|
|
1915
|
+
builtin: z9.boolean().default(false),
|
|
1916
|
+
/** 启停:false = 下拉禁用展示(历史数据仍可读,新选择不可用) */
|
|
1917
|
+
active: z9.boolean().default(true)
|
|
1918
|
+
});
|
|
1919
|
+
var EnumRegistrySchema = z9.object({
|
|
1920
|
+
id: z9.string().optional(),
|
|
1921
|
+
category: EnumRegistryCategorySchema,
|
|
1922
|
+
entries: z9.array(EnumEntrySchema).default([]),
|
|
1923
|
+
updatedAt: z9.date().optional()
|
|
1924
|
+
});
|
|
1925
|
+
var EnumRegistryUpdateSchema = z9.object({
|
|
1926
|
+
entries: z9.array(EnumEntrySchema)
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
// src/config/server-config.ts
|
|
1930
|
+
import { z as z10 } from "zod";
|
|
1931
|
+
var SimingLogLevelSchema = z10.enum(["debug", "info", "warn", "error"]);
|
|
1932
|
+
var SimingConfigSchema = z10.object({
|
|
1933
|
+
mongoUri: z10.string().min(1).default("mongodb://localhost:27017"),
|
|
1934
|
+
port: z10.coerce.number().int().min(1).max(65535).default(7777),
|
|
1935
|
+
/**
|
|
1936
|
+
* N018 D8:默认仅绑定本机回环(安全默认);远程/局域接入(F7)显式配置 host。
|
|
1937
|
+
* 超出 PRD V1 配置清单的新增项,依据见技术方案 §3.4「超范围扩展标注」。
|
|
1938
|
+
*/
|
|
1939
|
+
host: z10.string().min(1).default("127.0.0.1"),
|
|
1940
|
+
logLevel: SimingLogLevelSchema.default("info"),
|
|
1941
|
+
/**
|
|
1942
|
+
* F6 预留字段(V2 远程部署):宽松形态——出现即接受、形状不校验、运行时不读取。
|
|
1943
|
+
* 严格满足 PRD「配置中出现预留字段时不报错」;V2 落地时收紧为类型化 schema。
|
|
1944
|
+
*/
|
|
1945
|
+
auth: z10.unknown().optional(),
|
|
1946
|
+
cors: z10.unknown().optional()
|
|
1947
|
+
});
|
|
1948
|
+
var SIMING_CONFIG_KEYS = ["mongoUri", "port", "host", "logLevel"];
|
|
1949
|
+
var SIMING_CONFIG_ENV_KEYS = {
|
|
1950
|
+
mongoUri: "SIMING_MONGO_URI",
|
|
1951
|
+
port: "PORT",
|
|
1952
|
+
host: "SIMING_HOST",
|
|
1953
|
+
logLevel: "SIMING_LOG_LEVEL"
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
// src/task/entry-id.ts
|
|
1957
|
+
var ENTRY_ID_LENGTH = 6;
|
|
1958
|
+
var BASE36_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1959
|
+
function generateEntryId(existing) {
|
|
1960
|
+
const taken = new Set(existing);
|
|
1961
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
1962
|
+
let id = "";
|
|
1963
|
+
for (let i = 0; i < ENTRY_ID_LENGTH; i++) {
|
|
1964
|
+
id += BASE36_ALPHABET[Math.floor(Math.random() * BASE36_ALPHABET.length)];
|
|
1965
|
+
}
|
|
1966
|
+
if (!taken.has(id)) return id;
|
|
1967
|
+
}
|
|
1968
|
+
throw new Error(`entry id generation failed after 32 attempts (existing: ${existing.length})`);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/index.ts
|
|
1972
|
+
var PackageJsonSchema = z11.object({ version: z11.string().min(1) });
|
|
1973
|
+
var packageJsonParsed = PackageJsonSchema.safeParse(
|
|
1974
|
+
JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"))
|
|
1975
|
+
);
|
|
1976
|
+
if (!packageJsonParsed.success) {
|
|
1977
|
+
throw new Error("core VERSION \u521D\u59CB\u5316\u5931\u8D25\uFF1A\u5305\u6839 package.json \u7F3A\u5C11\u5408\u6CD5 version \u5B57\u6BB5");
|
|
1978
|
+
}
|
|
1979
|
+
var VERSION = packageJsonParsed.data.version;
|
|
1980
|
+
function ping() {
|
|
1981
|
+
return "pong";
|
|
1982
|
+
}
|
|
1983
|
+
export {
|
|
1984
|
+
ARTIFACT_TYPES,
|
|
1985
|
+
AdvanceRequestSchema,
|
|
1986
|
+
AdvanceResponseSchema,
|
|
1987
|
+
AgentCopySchema,
|
|
1988
|
+
AgentCreateSchema,
|
|
1989
|
+
AgentSchema,
|
|
1990
|
+
AgentScopeSchema,
|
|
1991
|
+
AgentUpdateSchema,
|
|
1992
|
+
AppError,
|
|
1993
|
+
ApproveRequestSchema,
|
|
1994
|
+
ApproveResponseSchema,
|
|
1995
|
+
ArchNoteSchema,
|
|
1996
|
+
ArtifactSchema,
|
|
1997
|
+
BIZ_CODE_MESSAGES,
|
|
1998
|
+
BadRequestError,
|
|
1999
|
+
CheckItemSchema,
|
|
2000
|
+
ConfirmationSchema,
|
|
2001
|
+
ConflictError,
|
|
2002
|
+
DAG_PHASES,
|
|
2003
|
+
DAG_TRACKS,
|
|
2004
|
+
DEFAULT_PROJECT_KEY,
|
|
2005
|
+
DagEdgeSchema,
|
|
2006
|
+
DagInstanceSchema,
|
|
2007
|
+
DagNodePhaseSchema,
|
|
2008
|
+
DagNodeSchema,
|
|
2009
|
+
DagNodeTrackSchema,
|
|
2010
|
+
DagTemplateCopySchema,
|
|
2011
|
+
DagTemplateCreateSchema,
|
|
2012
|
+
DagTemplateSchema,
|
|
2013
|
+
DagTemplateUpdateSchema,
|
|
2014
|
+
DecisionSchema,
|
|
2015
|
+
ENTRY_ID_REGEX,
|
|
2016
|
+
EdgePausePointBaseSchema,
|
|
2017
|
+
EdgePausePointSchema,
|
|
2018
|
+
EnumEntrySchema,
|
|
2019
|
+
EnumRegistryCategorySchema,
|
|
2020
|
+
EnumRegistrySchema,
|
|
2021
|
+
EnumRegistryUpdateSchema,
|
|
2022
|
+
HistoryActionSchema,
|
|
2023
|
+
HistoryEntrySchema,
|
|
2024
|
+
ModelAliasCreateSchema,
|
|
2025
|
+
ModelAliasSchema,
|
|
2026
|
+
ModelAliasShapeSchema,
|
|
2027
|
+
ModelAliasUpdateSchema,
|
|
2028
|
+
ModelAliasWithRefCountSchema,
|
|
2029
|
+
NODE_ID_PATTERN,
|
|
2030
|
+
NODE_STATUSES,
|
|
2031
|
+
NodeInfoSchema,
|
|
2032
|
+
NodeRecordSchema,
|
|
2033
|
+
NodeStateSchema,
|
|
2034
|
+
NodeStatusSchema,
|
|
2035
|
+
NotFoundError,
|
|
2036
|
+
OBJECT_ID_HEX,
|
|
2037
|
+
PAUSE_POINT_TYPES,
|
|
2038
|
+
PausePointTypeSchema,
|
|
2039
|
+
PauseRequestSchema,
|
|
2040
|
+
ProjectCreateSchema,
|
|
2041
|
+
ProjectSchema,
|
|
2042
|
+
ProjectStatusSchema,
|
|
2043
|
+
ProjectUpdateSchema,
|
|
2044
|
+
ResumeRequestSchema,
|
|
2045
|
+
ReviewSummarySchema,
|
|
2046
|
+
SIMING_CODE_REGEX,
|
|
2047
|
+
SIMING_CONFIG_ENV_KEYS,
|
|
2048
|
+
SIMING_CONFIG_KEYS,
|
|
2049
|
+
SimingConfigSchema,
|
|
2050
|
+
SimingLogLevelSchema,
|
|
2051
|
+
SkillCopySchema,
|
|
2052
|
+
SkillCreateSchema,
|
|
2053
|
+
SkillSchema,
|
|
2054
|
+
SkillScopeSchema,
|
|
2055
|
+
SkillUpdateSchema,
|
|
2056
|
+
TASK_PHASES,
|
|
2057
|
+
TASK_STATUSES,
|
|
2058
|
+
TASK_TYPES,
|
|
2059
|
+
TaskContextSchema,
|
|
2060
|
+
TaskCreateInputSchema,
|
|
2061
|
+
TaskDocContentSchema,
|
|
2062
|
+
TaskPhaseSchema,
|
|
2063
|
+
TaskPublicSchema,
|
|
2064
|
+
TaskSchema,
|
|
2065
|
+
TaskStatusSchema,
|
|
2066
|
+
TaskSummarySchema,
|
|
2067
|
+
VERSION,
|
|
2068
|
+
ValidationError,
|
|
2069
|
+
advanceTask,
|
|
2070
|
+
approveTask,
|
|
2071
|
+
assertBoundSkillsCompatible,
|
|
2072
|
+
assertModelAliasExists,
|
|
2073
|
+
checkTermination,
|
|
2074
|
+
createAgentRepo,
|
|
2075
|
+
createDagTemplateRepo,
|
|
2076
|
+
createEnumRegistryRepo,
|
|
2077
|
+
createModelAliasRepo,
|
|
2078
|
+
createMongoClient,
|
|
2079
|
+
createProjectRepo,
|
|
2080
|
+
createRepo,
|
|
2081
|
+
createSkillRepo,
|
|
2082
|
+
createTaskRepo,
|
|
2083
|
+
findNextEdge,
|
|
2084
|
+
generateEntryId,
|
|
2085
|
+
generateProjectKey,
|
|
2086
|
+
mkHistory,
|
|
2087
|
+
pauseTask,
|
|
2088
|
+
ping,
|
|
2089
|
+
pruneInstance,
|
|
2090
|
+
renderPrompt,
|
|
2091
|
+
resumeTask,
|
|
2092
|
+
toNodeInfo,
|
|
2093
|
+
toTaskPublic,
|
|
2094
|
+
trackMatchSet,
|
|
2095
|
+
withTransaction
|
|
2096
|
+
};
|