@weiping/wingline-core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-table.d.ts +11 -0
- package/dist/audit-table.js +85 -0
- package/dist/audit.js +1 -1
- package/dist/commands/about.d.ts +13 -0
- package/dist/commands/about.js +12 -1
- package/dist/commands/autostart/sys.d.ts +3 -0
- package/dist/commands/autostart/sys.js +23 -0
- package/dist/commands/dispatcher/lifecycle.d.ts +4 -0
- package/dist/commands/dispatcher/lifecycle.js +7 -1
- package/dist/commands/down.js +3 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.js +106 -0
- package/dist/commands/launch.d.ts +10 -1
- package/dist/commands/launch.js +13 -1
- package/dist/commands/spec.d.ts +8 -6
- package/dist/commands/spec.js +66 -9
- package/dist/commands/task/plan.d.ts +1 -0
- package/dist/commands/task/plan.js +1 -1
- package/dist/commands/templates.d.ts +32 -0
- package/dist/commands/templates.js +120 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/orca/client.js +27 -9
- package/dist/orca/types.d.ts +6 -0
- package/dist/registry/registry.d.ts +7 -0
- package/dist/registry/registry.js +12 -0
- package/dist/server/api-read.js +44 -28
- package/dist/server/api-spec.js +43 -7
- package/dist/server/api-write.d.ts +86 -2
- package/dist/server/api-write.js +285 -12
- package/dist/server/index.js +4 -4
- package/dist/spec/doc.d.ts +13 -0
- package/dist/spec/doc.js +62 -0
- package/dist/spec/draft.d.ts +16 -0
- package/dist/spec/draft.js +15 -8
- package/dist/spec/edit.d.ts +21 -0
- package/dist/spec/edit.js +136 -0
- package/dist/spec/facade.d.ts +41 -0
- package/dist/spec/facade.js +68 -0
- package/dist/spec/load.d.ts +1 -0
- package/dist/spec/load.js +7 -6
- package/package.json +1 -1
package/dist/server/api-write.js
CHANGED
|
@@ -5,10 +5,50 @@ import { auditedCommands } from '../audit.js';
|
|
|
5
5
|
import { send } from '../commands/send.js';
|
|
6
6
|
import { createTask } from '../commands/task/create.js';
|
|
7
7
|
import { escalateTask, recoverTask, resolveGate } from '../commands/task/escalate.js';
|
|
8
|
-
import { reassignTask } from '../commands/task/claim.js';
|
|
8
|
+
import { claimTask, reassignTask, releaseTask } from '../commands/task/claim.js';
|
|
9
|
+
import { planTask } from '../commands/task/plan.js';
|
|
10
|
+
import { startTask } from '../commands/task/start.js';
|
|
11
|
+
import { doneTask } from '../commands/task/done.js';
|
|
9
12
|
import { freezePostCmd, hushCmd, unfreezePostCmd, unhushCmd } from '../commands/dispatcher/controls.js';
|
|
13
|
+
import { dispatcherDown, dispatcherUp } from '../commands/dispatcher/lifecycle.js';
|
|
10
14
|
import { dispatcherReply } from '../commands/dispatcher/inbox.js';
|
|
11
15
|
import { discardDraftCmd, saveSpecCmd } from '../commands/spec.js';
|
|
16
|
+
import { down } from '../commands/down.js';
|
|
17
|
+
import { restore } from '../commands/restore/index.js';
|
|
18
|
+
import { up } from '../commands/up.js';
|
|
19
|
+
import { initTeamCmd } from '../commands/init.js';
|
|
20
|
+
import { autostartInstall, autostartUninstall } from '../commands/autostart/index.js';
|
|
21
|
+
import { realAutostartSys } from '../commands/autostart/sys.js';
|
|
22
|
+
/** 布尔开关:只收 true / false(缺省放行)。字符串 "false" 不再当成真值。 */
|
|
23
|
+
function checkBool(key, val) {
|
|
24
|
+
if (val !== undefined && typeof val !== 'boolean') {
|
|
25
|
+
throw new WinglineError('input', 'invalid_param', `参数 ${key}=${val} 不合法`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** 毫秒数:只收有限数字(缺省放行)。 */
|
|
29
|
+
function checkNumber(key, val) {
|
|
30
|
+
if (val !== undefined && (typeof val !== 'number' || !Number.isFinite(val))) {
|
|
31
|
+
throw new WinglineError('input', 'invalid_param', `参数 ${key}=${val} 不合法`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** 岗位 → 会话 id 的字符串映射:对象、且每个值都是字符串。 */
|
|
35
|
+
function parseSessions(raw) {
|
|
36
|
+
if (raw === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const ok = !!raw && typeof raw === 'object' && !Array.isArray(raw) && Object.values(raw).every((v) => typeof v === 'string');
|
|
39
|
+
if (!ok)
|
|
40
|
+
throw new WinglineError('input', 'invalid_param', '参数 sessions 不合法');
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
/** 标签:字符串数组。 */
|
|
44
|
+
function parseTags(raw) {
|
|
45
|
+
if (raw === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (!Array.isArray(raw) || !raw.every((t) => typeof t === 'string')) {
|
|
48
|
+
throw new WinglineError('input', 'invalid_param', '参数 tags 不合法');
|
|
49
|
+
}
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
12
52
|
export const WRITE_ROUTES = [
|
|
13
53
|
{
|
|
14
54
|
pattern: '/api/teams/:team/send',
|
|
@@ -16,6 +56,7 @@ export const WRITE_ROUTES = [
|
|
|
16
56
|
fields: ['post', 'message', 'retryRequest'],
|
|
17
57
|
call: async (cmds, ctx) => {
|
|
18
58
|
checkParam('post', ctx.body.post);
|
|
59
|
+
checkParam('message', ctx.body.message);
|
|
19
60
|
const address = postAddress(ctx.body.post, ctx.params.team);
|
|
20
61
|
return cmds.send(ctx.deps, { address, message: ctx.body.message, retryRequest: ctx.body.retryRequest });
|
|
21
62
|
},
|
|
@@ -26,11 +67,12 @@ export const WRITE_ROUTES = [
|
|
|
26
67
|
fields: ['title', 'post', 'tags', 'parent', 'retryRequest'],
|
|
27
68
|
call: async (cmds, ctx) => {
|
|
28
69
|
const { title, post, tags, parent, retryRequest } = ctx.body;
|
|
29
|
-
|
|
70
|
+
checkParam('title', title);
|
|
71
|
+
if (post !== undefined)
|
|
30
72
|
checkParam('post', post);
|
|
31
|
-
if (parent)
|
|
73
|
+
if (parent !== undefined)
|
|
32
74
|
checkParam('id', parent);
|
|
33
|
-
return cmds.createTask(ctx.deps, { team: ctx.params.team, title, post: post ? postAddress(post, ctx.params.team) : undefined, tags, parent, retryRequest });
|
|
75
|
+
return cmds.createTask(ctx.deps, { team: ctx.params.team, title, post: post ? postAddress(post, ctx.params.team) : undefined, tags: parseTags(tags), parent, retryRequest });
|
|
34
76
|
},
|
|
35
77
|
},
|
|
36
78
|
{
|
|
@@ -39,6 +81,8 @@ export const WRITE_ROUTES = [
|
|
|
39
81
|
fields: ['reason', 'reopen', 'retryRequest'],
|
|
40
82
|
call: async (cmds, ctx) => {
|
|
41
83
|
const { reason, reopen, retryRequest } = ctx.body;
|
|
84
|
+
checkParam('reason', reason);
|
|
85
|
+
checkBool('reopen', reopen);
|
|
42
86
|
return cmds.escalateTask(ctx.deps, { id: ctx.params.id, reason, reopen, retryRequest });
|
|
43
87
|
},
|
|
44
88
|
},
|
|
@@ -60,14 +104,50 @@ export const WRITE_ROUTES = [
|
|
|
60
104
|
fields: ['retryRequest'],
|
|
61
105
|
call: async (cmds, ctx) => cmds.recoverTask(ctx.deps, { id: ctx.params.id }),
|
|
62
106
|
},
|
|
107
|
+
{
|
|
108
|
+
pattern: '/api/teams/:team/tasks/:id/claim',
|
|
109
|
+
check: ['team', 'id'],
|
|
110
|
+
fields: ['as', 'retryRequest'],
|
|
111
|
+
call: async (cmds, ctx) => cmds.claimTask(ctx.deps, { id: ctx.params.id, as: actorOf(ctx) }),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
pattern: '/api/teams/:team/tasks/:id/release',
|
|
115
|
+
check: ['team', 'id'],
|
|
116
|
+
fields: ['as', 'retryRequest'],
|
|
117
|
+
call: async (cmds, ctx) => cmds.releaseTask(ctx.deps, { id: ctx.params.id, as: actorOf(ctx) }),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
pattern: '/api/teams/:team/tasks/:id/plan',
|
|
121
|
+
check: ['team', 'id'],
|
|
122
|
+
fields: ['as', 'text', 'retryRequest'],
|
|
123
|
+
call: async (cmds, ctx) => {
|
|
124
|
+
const { text } = ctx.body;
|
|
125
|
+
if (typeof text !== 'string' || !text.trim())
|
|
126
|
+
throw new WinglineError('input', 'invalid_param', '参数 text 不合法');
|
|
127
|
+
return cmds.planTask(ctx.deps, { id: ctx.params.id, as: actorOf(ctx), text });
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
pattern: '/api/teams/:team/tasks/:id/start',
|
|
132
|
+
check: ['team', 'id'],
|
|
133
|
+
fields: ['as', 'retryRequest'],
|
|
134
|
+
call: async (cmds, ctx) => cmds.startTask(ctx.deps, { id: ctx.params.id, as: actorOf(ctx) }),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
pattern: '/api/teams/:team/tasks/:id/done',
|
|
138
|
+
check: ['team', 'id'],
|
|
139
|
+
fields: ['as', 'retryRequest'],
|
|
140
|
+
call: async (cmds, ctx) => cmds.doneTask(ctx.deps, { id: ctx.params.id, as: actorOf(ctx) }),
|
|
141
|
+
},
|
|
63
142
|
{
|
|
64
143
|
pattern: '/api/teams/:team/gates/:gateId/resolve',
|
|
65
144
|
check: ['team', 'gateId'],
|
|
66
145
|
fields: ['resolution', 'reassign', 'retryRequest'],
|
|
67
146
|
call: async (cmds, ctx) => {
|
|
68
147
|
const { resolution, reassign } = ctx.body;
|
|
69
|
-
|
|
70
|
-
|
|
148
|
+
checkParam('resolution', resolution);
|
|
149
|
+
if (reassign !== undefined)
|
|
150
|
+
checkParam('reassign', reassign);
|
|
71
151
|
const target = reassign ? (reassign.includes('@') ? reassign : (checkParam('post', reassign), postAddress(reassign, ctx.params.team))) : undefined;
|
|
72
152
|
return cmds.resolveGate(ctx.deps, { gateId: ctx.params.gateId, resolution, reassign: target });
|
|
73
153
|
},
|
|
@@ -76,7 +156,10 @@ export const WRITE_ROUTES = [
|
|
|
76
156
|
pattern: '/api/teams/:team/posts/:post/freeze',
|
|
77
157
|
check: ['team', 'post'],
|
|
78
158
|
fields: ['reason', 'retryRequest'],
|
|
79
|
-
call: async (cmds, ctx) =>
|
|
159
|
+
call: async (cmds, ctx) => {
|
|
160
|
+
checkParam('reason', ctx.body.reason);
|
|
161
|
+
return cmds.freezePostCmd(ctx.deps, { address: postAddress(ctx.params.post, ctx.params.team), reason: ctx.body.reason });
|
|
162
|
+
},
|
|
80
163
|
},
|
|
81
164
|
{
|
|
82
165
|
pattern: '/api/teams/:team/posts/:post/unfreeze',
|
|
@@ -88,7 +171,10 @@ export const WRITE_ROUTES = [
|
|
|
88
171
|
pattern: '/api/teams/:team/posts/:post/hush',
|
|
89
172
|
check: ['team', 'post'],
|
|
90
173
|
fields: ['forMs', 'retryRequest'],
|
|
91
|
-
call: async (cmds, ctx) =>
|
|
174
|
+
call: async (cmds, ctx) => {
|
|
175
|
+
checkNumber('forMs', ctx.body.forMs);
|
|
176
|
+
return cmds.hushCmd(ctx.deps, { address: postAddress(ctx.params.post, ctx.params.team), forMs: ctx.body.forMs });
|
|
177
|
+
},
|
|
92
178
|
},
|
|
93
179
|
{
|
|
94
180
|
pattern: '/api/teams/:team/posts/:post/unhush',
|
|
@@ -102,21 +188,23 @@ export const WRITE_ROUTES = [
|
|
|
102
188
|
fields: ['body', 'again', 'retryRequest'],
|
|
103
189
|
call: async (cmds, ctx) => {
|
|
104
190
|
const { body, again } = ctx.body;
|
|
191
|
+
checkParam('body', body);
|
|
192
|
+
checkBool('again', again);
|
|
105
193
|
return cmds.dispatcherReply(ctx.deps, { team: ctx.params.team, messageId: ctx.params.messageId, body, again });
|
|
106
194
|
},
|
|
107
195
|
},
|
|
108
196
|
{
|
|
109
197
|
pattern: '/api/teams/:team/spec',
|
|
110
198
|
check: ['team'],
|
|
111
|
-
fields: ['yaml', 'baseHash', 'running', 'retryRequest'],
|
|
199
|
+
fields: ['yaml', 'baseHash', 'running', 'files', 'retryRequest'],
|
|
112
200
|
call: async (cmds, ctx) => {
|
|
113
|
-
const { yaml, baseHash, running } = ctx.body;
|
|
201
|
+
const { yaml, baseHash, running, files } = ctx.body;
|
|
114
202
|
checkParam('yaml', yaml);
|
|
115
203
|
if (baseHash !== null)
|
|
116
204
|
checkParam('baseHash', baseHash);
|
|
117
205
|
if (typeof running !== 'boolean')
|
|
118
206
|
throw new WinglineError('input', 'invalid_param', '参数 running 不合法');
|
|
119
|
-
return cmds.saveSpecCmd(ctx.deps, { team: ctx.params.team, yaml, baseHash, running });
|
|
207
|
+
return cmds.saveSpecCmd(ctx.deps, { team: ctx.params.team, yaml, baseHash, running, files });
|
|
120
208
|
},
|
|
121
209
|
},
|
|
122
210
|
{
|
|
@@ -125,8 +213,102 @@ export const WRITE_ROUTES = [
|
|
|
125
213
|
fields: ['retryRequest'],
|
|
126
214
|
call: async (cmds, ctx) => cmds.discardDraftCmd(ctx.deps, { team: ctx.params.team }),
|
|
127
215
|
},
|
|
216
|
+
{
|
|
217
|
+
pattern: '/api/teams/:team/up',
|
|
218
|
+
check: ['team'],
|
|
219
|
+
fields: ['draft', 'newRun', 'adoptRun', 'retryRequest'],
|
|
220
|
+
call: async (cmds, ctx) => {
|
|
221
|
+
const { draft, newRun, adoptRun } = ctx.body;
|
|
222
|
+
checkBool('draft', draft);
|
|
223
|
+
checkBool('newRun', newRun);
|
|
224
|
+
if (adoptRun !== undefined)
|
|
225
|
+
checkParam('adoptRun', adoptRun);
|
|
226
|
+
return cmds.up(ctx.deps, { team: ctx.params.team, draft, newRun, adoptRun });
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
pattern: '/api/teams/:team/down',
|
|
231
|
+
check: ['team'],
|
|
232
|
+
fields: ['close', 'retryRequest'],
|
|
233
|
+
call: async (cmds, ctx) => {
|
|
234
|
+
checkBool('close', ctx.body.close);
|
|
235
|
+
return cmds.down(ctx.deps, { team: ctx.params.team, close: !!ctx.body.close });
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
pattern: '/api/teams/:team/restore',
|
|
240
|
+
check: ['team'],
|
|
241
|
+
fields: ['dryRun', 'noTasks', 'sessions', 'retryRequest'],
|
|
242
|
+
call: async (cmds, ctx) => {
|
|
243
|
+
const { dryRun, noTasks } = ctx.body;
|
|
244
|
+
checkBool('dryRun', dryRun);
|
|
245
|
+
checkBool('noTasks', noTasks);
|
|
246
|
+
return cmds.restore(ctx.deps, { team: ctx.params.team, dryRun: !!dryRun, noTasks: !!noTasks, sessions: parseSessions(ctx.body.sessions) });
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
pattern: '/api/teams/:team/dispatcher/up',
|
|
251
|
+
check: ['team'],
|
|
252
|
+
fields: ['retryRequest'],
|
|
253
|
+
call: async (cmds, ctx) => cmds.dispatcherUp(ctx.deps, { team: ctx.params.team }),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
pattern: '/api/teams/:team/dispatcher/down',
|
|
257
|
+
check: ['team'],
|
|
258
|
+
fields: ['close', 'retryRequest'],
|
|
259
|
+
call: async (cmds, ctx) => {
|
|
260
|
+
checkBool('close', ctx.body.close);
|
|
261
|
+
return cmds.dispatcherDown(ctx.deps, { team: ctx.params.team, close: ctx.body.close ?? true });
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
pattern: '/api/teams',
|
|
266
|
+
check: [],
|
|
267
|
+
fields: ['mode', 'name', 'repo', 'template', 'dir', 'specPath', 'retryRequest'],
|
|
268
|
+
call: async (cmds, ctx) => {
|
|
269
|
+
const { mode, name, repo, template, dir, specPath } = ctx.body;
|
|
270
|
+
if (mode !== 'template' && mode !== 'import')
|
|
271
|
+
throw new WinglineError('input', 'invalid_param', '参数 mode 不合法');
|
|
272
|
+
for (const key of ['name', 'repo', 'template', 'dir', 'specPath']) {
|
|
273
|
+
if (ctx.body[key] !== undefined)
|
|
274
|
+
checkParam(key, ctx.body[key]);
|
|
275
|
+
}
|
|
276
|
+
return cmds.initTeamCmd(ctx.deps, { mode, name, repo, template, dir, specPath });
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
pattern: '/api/autostart/install',
|
|
281
|
+
check: [],
|
|
282
|
+
fields: ['waitOrca', 'retryRequest'],
|
|
283
|
+
call: async (cmds, ctx) => {
|
|
284
|
+
if (ctx.body.waitOrca !== undefined)
|
|
285
|
+
checkParam('waitOrca', ctx.body.waitOrca);
|
|
286
|
+
return cmds.autostartInstall(ctx.deps, { waitOrca: ctx.body.waitOrca });
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
pattern: '/api/autostart/uninstall',
|
|
291
|
+
check: [],
|
|
292
|
+
fields: ['retryRequest'],
|
|
293
|
+
call: async (cmds, ctx) => cmds.autostartUninstall(ctx.deps),
|
|
294
|
+
},
|
|
128
295
|
];
|
|
129
296
|
const ofAddress = (a) => a.address.split('@')[1] ?? '-';
|
|
297
|
+
/** 任务执行流的执行者:必填;不带 @ 时按本团队岗位解释,带 @ 时团队后缀必须一致。 */
|
|
298
|
+
function actorOf(ctx) {
|
|
299
|
+
const raw = ctx.body.as;
|
|
300
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
301
|
+
throw new WinglineError('input', 'invalid_param', '参数 as 不合法');
|
|
302
|
+
if (!raw.includes('@')) {
|
|
303
|
+
checkParam('post', raw);
|
|
304
|
+
return postAddress(raw, ctx.params.team);
|
|
305
|
+
}
|
|
306
|
+
const [post, team] = raw.split('@');
|
|
307
|
+
checkParam('post', post);
|
|
308
|
+
if (team !== ctx.params.team)
|
|
309
|
+
throw new WinglineError('input', 'invalid_param', '执行者不属于该团队');
|
|
310
|
+
return raw;
|
|
311
|
+
}
|
|
130
312
|
const byTask = (idOf) => (_a, ctx) => ctx.deps.registry.getTaskExt(idOf(_a))?.team ?? '-';
|
|
131
313
|
const byGate = (gateId) => (_a, ctx) => ctx.deps.registry.getGate(gateId(_a))?.team ?? '-';
|
|
132
314
|
export const CONSOLE_AUDIT = {
|
|
@@ -165,6 +347,36 @@ export const CONSOLE_AUDIT = {
|
|
|
165
347
|
target: (a) => a.gateId,
|
|
166
348
|
summary: (a) => ({ resolution: a.resolution, reassign: a.reassign }),
|
|
167
349
|
},
|
|
350
|
+
claimTask: {
|
|
351
|
+
command: 'task claim',
|
|
352
|
+
team: byTask((a) => a.id),
|
|
353
|
+
target: (a) => a.id,
|
|
354
|
+
summary: (a) => ({ as: a.as }),
|
|
355
|
+
},
|
|
356
|
+
releaseTask: {
|
|
357
|
+
command: 'task release',
|
|
358
|
+
team: byTask((a) => a.id),
|
|
359
|
+
target: (a) => a.id,
|
|
360
|
+
summary: (a) => ({ as: a.as }),
|
|
361
|
+
},
|
|
362
|
+
planTask: {
|
|
363
|
+
command: 'task plan',
|
|
364
|
+
team: byTask((a) => a.id),
|
|
365
|
+
target: (a) => a.id,
|
|
366
|
+
summary: (a) => ({ as: a.as, text: a.text }),
|
|
367
|
+
},
|
|
368
|
+
startTask: {
|
|
369
|
+
command: 'task start',
|
|
370
|
+
team: byTask((a) => a.id),
|
|
371
|
+
target: (a) => a.id,
|
|
372
|
+
summary: (a) => ({ as: a.as }),
|
|
373
|
+
},
|
|
374
|
+
doneTask: {
|
|
375
|
+
command: 'task done',
|
|
376
|
+
team: byTask((a) => a.id),
|
|
377
|
+
target: (a) => a.id,
|
|
378
|
+
summary: (a) => ({ as: a.as }),
|
|
379
|
+
},
|
|
168
380
|
freezePostCmd: {
|
|
169
381
|
command: 'dispatcher freeze',
|
|
170
382
|
team: ofAddress,
|
|
@@ -209,8 +421,48 @@ export const CONSOLE_AUDIT = {
|
|
|
209
421
|
team: (a) => a.team,
|
|
210
422
|
summary: (_a, ctx) => ({ discarded: ctx.result?.discarded }),
|
|
211
423
|
},
|
|
424
|
+
up: {
|
|
425
|
+
command: 'up',
|
|
426
|
+
team: (a) => a.team,
|
|
427
|
+
summary: (a) => ({ draft: a.draft, newRun: a.newRun, adoptRun: a.adoptRun }),
|
|
428
|
+
},
|
|
429
|
+
down: {
|
|
430
|
+
command: 'down',
|
|
431
|
+
team: (a) => a.team,
|
|
432
|
+
summary: (a) => ({ close: a.close }),
|
|
433
|
+
},
|
|
434
|
+
restore: {
|
|
435
|
+
command: 'restore',
|
|
436
|
+
team: (a) => a.team,
|
|
437
|
+
skip: (a) => !!a.dryRun,
|
|
438
|
+
summary: (a) => ({ dryRun: a.dryRun, noTasks: a.noTasks, session: Object.keys(a.sessions ?? {}) }),
|
|
439
|
+
},
|
|
440
|
+
dispatcherUp: {
|
|
441
|
+
command: 'dispatcher up',
|
|
442
|
+
team: (a) => a.team,
|
|
443
|
+
},
|
|
444
|
+
dispatcherDown: {
|
|
445
|
+
command: 'dispatcher down',
|
|
446
|
+
team: (a) => a.team,
|
|
447
|
+
summary: (a) => ({ close: a.close }),
|
|
448
|
+
},
|
|
449
|
+
initTeamCmd: {
|
|
450
|
+
command: 'init',
|
|
451
|
+
// 新建团队没有 :team 路径参数:团队名从结果里取
|
|
452
|
+
team: (_a, ctx) => ctx.result?.team ?? '-',
|
|
453
|
+
summary: (a, ctx) => ({ mode: a.mode, template: a.template, created: ctx.result?.created }),
|
|
454
|
+
},
|
|
455
|
+
autostartInstall: {
|
|
456
|
+
command: 'autostart install',
|
|
457
|
+
team: () => '-',
|
|
458
|
+
summary: (a) => ({ waitOrca: a.waitOrca }),
|
|
459
|
+
},
|
|
460
|
+
autostartUninstall: {
|
|
461
|
+
command: 'autostart uninstall',
|
|
462
|
+
team: () => '-',
|
|
463
|
+
},
|
|
212
464
|
};
|
|
213
|
-
/** 控制台用 `console:`,TUI 用 `tui
|
|
465
|
+
/** 控制台用 `console:`,TUI 用 `tui:`,Orca 插件用 `plugin:`;三者共用同一张审计表,执行者都是 human。 */
|
|
214
466
|
export function auditedWriteCommands(prefix) {
|
|
215
467
|
const raw = {
|
|
216
468
|
send,
|
|
@@ -219,6 +471,12 @@ export function auditedWriteCommands(prefix) {
|
|
|
219
471
|
reassignTask,
|
|
220
472
|
recoverTask,
|
|
221
473
|
resolveGate,
|
|
474
|
+
// 任务执行流:控制台口径只透传 id/as(+plan 的 text),resumeEpoch 等其余参数不暴露
|
|
475
|
+
claimTask: (deps, a) => claimTask(deps, { id: a.id, as: a.as }),
|
|
476
|
+
releaseTask: (deps, a) => releaseTask(deps, { id: a.id, as: a.as }),
|
|
477
|
+
planTask: (deps, a) => planTask(deps, { id: a.id, as: a.as, text: a.text }),
|
|
478
|
+
startTask: (deps, a) => startTask(deps, { id: a.id, as: a.as }),
|
|
479
|
+
doneTask: (deps, a) => doneTask(deps, { id: a.id, as: a.as }),
|
|
222
480
|
freezePostCmd,
|
|
223
481
|
unfreezePostCmd,
|
|
224
482
|
hushCmd,
|
|
@@ -226,6 +484,21 @@ export function auditedWriteCommands(prefix) {
|
|
|
226
484
|
dispatcherReply,
|
|
227
485
|
saveSpecCmd,
|
|
228
486
|
discardDraftCmd,
|
|
487
|
+
// core 的 up 吃 specPath,控制台给的是 team 名:包装层先查注册表换算
|
|
488
|
+
up: async (deps, a) => {
|
|
489
|
+
const row = deps.registry.getTeam(a.team);
|
|
490
|
+
if (!row)
|
|
491
|
+
throw new WinglineError('input', 'team_unknown', `团队 ${a.team} 未登记`);
|
|
492
|
+
return up(deps, { specPath: row.specPath, draft: a.draft, newRun: a.newRun, adoptRun: a.adoptRun });
|
|
493
|
+
},
|
|
494
|
+
down: (deps, a) => down(deps, { team: a.team, close: !!a.close }),
|
|
495
|
+
restore: (deps, a) => restore(deps, { team: a.team, dryRun: !!a.dryRun, noTasks: !!a.noTasks, sessions: a.sessions }),
|
|
496
|
+
dispatcherUp: (deps, a) => dispatcherUp(deps, { team: a.team, node: a.node, script: a.script }),
|
|
497
|
+
dispatcherDown: (deps, a) => dispatcherDown(deps, { team: a.team, close: a.close ?? true }),
|
|
498
|
+
initTeamCmd,
|
|
499
|
+
// 系统宿主(launchd/systemd)只有真实执行时才能取:默认 realAutostartSys,测试经 cmds 注入替换
|
|
500
|
+
autostartInstall: (deps, a) => autostartInstall(deps, realAutostartSys(), { waitOrca: a.waitOrca }),
|
|
501
|
+
autostartUninstall: (deps) => autostartUninstall(deps, realAutostartSys()),
|
|
229
502
|
};
|
|
230
503
|
return auditedCommands(raw, CONSOLE_AUDIT, { prefix, actor: 'human' });
|
|
231
504
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -71,11 +71,11 @@ function respondError(res, err, warn, where) {
|
|
|
71
71
|
}
|
|
72
72
|
const BODY_LIMIT = 64 * 1024;
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* 规格编辑接口的请求体:规格 48 KB + 若干份 64 KB 以内的 .md 文档,JSON 转义后最坏再放大几倍,
|
|
75
|
+
* 所以不能套 64 KB 的通用上限。超过各自上限由 spec_too_large / doc_too_large 拒绝;
|
|
76
|
+
* 这里的 512 KB 只防止读入无限大的请求体。
|
|
77
77
|
*/
|
|
78
|
-
const SPEC_BODY_LIMIT =
|
|
78
|
+
const SPEC_BODY_LIMIT = 512 * 1024;
|
|
79
79
|
const SPEC_BODY_ROUTE = /^\/api\/teams\/[^/]+\/spec(?:\/validate)?$/;
|
|
80
80
|
const bodyLimitFor = (pathname) => (SPEC_BODY_ROUTE.test(pathname) ? SPEC_BODY_LIMIT : BODY_LIMIT);
|
|
81
81
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DOC_LIMIT_BYTES: number;
|
|
2
|
+
/**
|
|
3
|
+
* 控制台只允许写团队目录里被规格引用的 .md。路径来自不可信的请求体:
|
|
4
|
+
* 绝对路径、`..` 越界、非 .md、含 \0、软链接指向目录外,一律拒绝。
|
|
5
|
+
* shortcut: 新建文件时穿过本身是软链接的父目录不在检查之列(规格引用校验已把可写范围限到团队目录)
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveDocPath(specDir: string, rel: string): string;
|
|
8
|
+
/** 读文档正文;读不到(还没落盘、被删掉)一律返回空串,让编辑器拿到一个可编辑的初值。 */
|
|
9
|
+
export declare function readDocText(abs: string): string;
|
|
10
|
+
/** 原子写文档(照 saveSpec 的 writeAtomic:临时文件 + 改名,继承已有文件的权限位),并建出缺失的父目录。 */
|
|
11
|
+
export declare function writeDocAtomic(abs: string, content: string): void;
|
|
12
|
+
/** 从原始 YAML 对象里取 norms 与 posts[*].brief 引用的相对路径(去重);不是对象就返回空。 */
|
|
13
|
+
export declare function docRelsOf(parsed: unknown): string[];
|
package/dist/spec/doc.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { WinglineError } from '../errors.js';
|
|
4
|
+
import { writeAtomic } from './draft.js';
|
|
5
|
+
export const DOC_LIMIT_BYTES = 64 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* 控制台只允许写团队目录里被规格引用的 .md。路径来自不可信的请求体:
|
|
8
|
+
* 绝对路径、`..` 越界、非 .md、含 \0、软链接指向目录外,一律拒绝。
|
|
9
|
+
* shortcut: 新建文件时穿过本身是软链接的父目录不在检查之列(规格引用校验已把可写范围限到团队目录)
|
|
10
|
+
*/
|
|
11
|
+
export function resolveDocPath(specDir, rel) {
|
|
12
|
+
const bad = () => new WinglineError('input', 'doc_path_invalid', `文档路径 ${rel} 必须位于团队目录内且为 .md 文件`);
|
|
13
|
+
if (typeof rel !== 'string' || rel === '' || !rel.endsWith('.md') || rel.includes('\0'))
|
|
14
|
+
throw bad();
|
|
15
|
+
const abs = path.resolve(specDir, rel.replace(/\\/g, '/'));
|
|
16
|
+
const within = path.relative(specDir, abs);
|
|
17
|
+
if (within === '' || within.startsWith('..') || path.isAbsolute(within))
|
|
18
|
+
throw bad();
|
|
19
|
+
// 已存在的路径可能是软链接:解析后的真实位置必须仍在真实团队目录内
|
|
20
|
+
if (fs.existsSync(abs)) {
|
|
21
|
+
const real = path.relative(fs.realpathSync(specDir), fs.realpathSync(abs));
|
|
22
|
+
if (real === '' || real.startsWith('..') || path.isAbsolute(real))
|
|
23
|
+
throw bad();
|
|
24
|
+
}
|
|
25
|
+
return abs;
|
|
26
|
+
}
|
|
27
|
+
/** 读文档正文;读不到(还没落盘、被删掉)一律返回空串,让编辑器拿到一个可编辑的初值。 */
|
|
28
|
+
export function readDocText(abs) {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readFileSync(abs, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 原子写文档(照 saveSpec 的 writeAtomic:临时文件 + 改名,继承已有文件的权限位),并建出缺失的父目录。 */
|
|
37
|
+
export function writeDocAtomic(abs, content) {
|
|
38
|
+
if (Buffer.byteLength(content, 'utf8') > DOC_LIMIT_BYTES) {
|
|
39
|
+
throw new WinglineError('input', 'doc_too_large', `文档 ${path.basename(abs)} 超过 ${DOC_LIMIT_BYTES / 1024} KB 上限`);
|
|
40
|
+
}
|
|
41
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
42
|
+
writeAtomic(abs, content);
|
|
43
|
+
}
|
|
44
|
+
/** 从原始 YAML 对象里取 norms 与 posts[*].brief 引用的相对路径(去重);不是对象就返回空。 */
|
|
45
|
+
export function docRelsOf(parsed) {
|
|
46
|
+
if (!parsed || typeof parsed !== 'object')
|
|
47
|
+
return [];
|
|
48
|
+
const raw = parsed;
|
|
49
|
+
const rels = new Set();
|
|
50
|
+
if (typeof raw.norms === 'string')
|
|
51
|
+
rels.add(raw.norms);
|
|
52
|
+
if (Array.isArray(raw.posts)) {
|
|
53
|
+
for (const post of raw.posts) {
|
|
54
|
+
const brief = post?.brief;
|
|
55
|
+
if (Array.isArray(brief))
|
|
56
|
+
for (const b of brief)
|
|
57
|
+
if (typeof b === 'string')
|
|
58
|
+
rels.add(b);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...rels];
|
|
62
|
+
}
|
package/dist/spec/draft.d.ts
CHANGED
|
@@ -3,7 +3,23 @@ export declare const SPEC_MAX_BYTES: number;
|
|
|
3
3
|
export declare function draftPathOf(specPath: string): string;
|
|
4
4
|
export declare const textHash: (text: string) => string;
|
|
5
5
|
export declare function fileHash(file: string): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* 先写同目录临时文件再改名,避免留下半截文件。改名会换掉文件本身,所以:
|
|
8
|
+
* 符号链接要写到它指向的真实文件上(链接保持链接),已有文件的权限位要带过去。
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeAtomic(link: string, text: string): void;
|
|
6
11
|
export declare function assertSpecSize(text: string): void;
|
|
12
|
+
/** 保存前的冲突检查:目标文件当前内容既不等于新内容、也不等于加载时的 baseHash 时抛 spec_conflict。 */
|
|
13
|
+
export declare function checkSaveConflict(a: {
|
|
14
|
+
specPath: string;
|
|
15
|
+
text: string;
|
|
16
|
+
baseHash: string | null;
|
|
17
|
+
running: boolean;
|
|
18
|
+
}): {
|
|
19
|
+
target: string;
|
|
20
|
+
hash: string;
|
|
21
|
+
skip: boolean;
|
|
22
|
+
};
|
|
7
23
|
/** 保存规格:团队运行中写草稿,否则写正式规格。`running` 与注册表是否一致由调用方检查。 */
|
|
8
24
|
export declare function saveSpec(a: {
|
|
9
25
|
specPath: string;
|
package/dist/spec/draft.js
CHANGED
|
@@ -44,7 +44,7 @@ function modeOf(file) {
|
|
|
44
44
|
* 先写同目录临时文件再改名,避免留下半截文件。改名会换掉文件本身,所以:
|
|
45
45
|
* 符号链接要写到它指向的真实文件上(链接保持链接),已有文件的权限位要带过去。
|
|
46
46
|
*/
|
|
47
|
-
function writeAtomic(link, text) {
|
|
47
|
+
export function writeAtomic(link, text) {
|
|
48
48
|
const file = realTarget(link);
|
|
49
49
|
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
|
|
50
50
|
try {
|
|
@@ -64,6 +64,17 @@ export function assertSpecSize(text) {
|
|
|
64
64
|
throw new WinglineError('input', 'spec_too_large', `规格超过 ${SPEC_MAX_BYTES / 1024} KB`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
/** 保存前的冲突检查:目标文件当前内容既不等于新内容、也不等于加载时的 baseHash 时抛 spec_conflict。 */
|
|
68
|
+
export function checkSaveConflict(a) {
|
|
69
|
+
const target = a.running ? draftPathOf(a.specPath) : a.specPath;
|
|
70
|
+
const hash = textHash(a.text);
|
|
71
|
+
const current = fileHash(target);
|
|
72
|
+
if (current === hash)
|
|
73
|
+
return { target, hash, skip: true };
|
|
74
|
+
if (current !== a.baseHash)
|
|
75
|
+
throw new WinglineError('input', 'spec_conflict', `${path.basename(target)} 在加载后被改动过,请重新加载`);
|
|
76
|
+
return { target, hash, skip: false };
|
|
77
|
+
}
|
|
67
78
|
/** 保存规格:团队运行中写草稿,否则写正式规格。`running` 与注册表是否一致由调用方检查。 */
|
|
68
79
|
export function saveSpec(a) {
|
|
69
80
|
assertSpecSize(a.text);
|
|
@@ -71,14 +82,10 @@ export function saveSpec(a) {
|
|
|
71
82
|
if (errors.length > 0) {
|
|
72
83
|
throw new WinglineError('input', 'spec_invalid', errors[0].message, { detail: { errors } });
|
|
73
84
|
}
|
|
74
|
-
const target
|
|
75
|
-
const result = { wrote: a.running ? 'draft' : 'spec', path: target, hash
|
|
76
|
-
|
|
77
|
-
if (current === result.hash)
|
|
85
|
+
const { target, hash, skip } = checkSaveConflict(a);
|
|
86
|
+
const result = { wrote: a.running ? 'draft' : 'spec', path: target, hash };
|
|
87
|
+
if (skip)
|
|
78
88
|
return result;
|
|
79
|
-
if (current !== a.baseHash) {
|
|
80
|
-
throw new WinglineError('input', 'spec_conflict', `${path.basename(target)} 在加载后被改动过,请重新加载`);
|
|
81
|
-
}
|
|
82
89
|
writeAtomic(target, a.text);
|
|
83
90
|
return result;
|
|
84
91
|
}
|
package/dist/spec/edit.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
type Path = Array<string | number>;
|
|
2
2
|
/** 把表单的一处修改写回 YAML:`value === null` 删除该字段。 */
|
|
3
3
|
export declare function setField(text: string, path: Path, value: unknown): string;
|
|
4
|
+
type SpecPost = {
|
|
5
|
+
id: string;
|
|
6
|
+
agent: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
launch?: string;
|
|
9
|
+
worktree?: string;
|
|
10
|
+
brief?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type SpecEditOp = {
|
|
13
|
+
op?: 'set';
|
|
14
|
+
path: Path;
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
op: 'add_post';
|
|
18
|
+
post: SpecPost;
|
|
19
|
+
} | {
|
|
20
|
+
op: 'remove_post';
|
|
21
|
+
index: number;
|
|
22
|
+
};
|
|
23
|
+
/** 把团队编辑页的一处规格修改写回 YAML:op 缺省是 set(等价 setField),add_post / remove_post 增删岗位。 */
|
|
24
|
+
export declare function applySpecOp(text: string, op: SpecEditOp): string;
|
|
4
25
|
export {};
|