@weiping/wingline-core 0.1.0 → 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/deps.d.ts +2 -0
- package/dist/commands/deps.js +2 -0
- package/dist/commands/dispatcher/attention.d.ts +2 -0
- package/dist/commands/dispatcher/attention.js +10 -9
- package/dist/commands/dispatcher/controls.d.ts +1 -1
- package/dist/commands/dispatcher/controls.js +5 -3
- package/dist/commands/dispatcher/lifecycle.d.ts +4 -0
- package/dist/commands/dispatcher/lifecycle.js +7 -1
- package/dist/commands/dispatcher/loop.js +2 -0
- 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 -2
- package/dist/commands/launch.js +14 -3
- package/dist/commands/restore/index.js +6 -0
- package/dist/commands/restore/posts.d.ts +1 -1
- package/dist/commands/restore/posts.js +17 -17
- package/dist/commands/restore/resume-command.d.ts +0 -1
- package/dist/commands/restore/resume-command.js +0 -3
- package/dist/commands/send.js +1 -2
- package/dist/commands/spec.d.ts +8 -6
- package/dist/commands/spec.js +66 -9
- package/dist/commands/task/gate-exec.d.ts +5 -1
- package/dist/commands/task/gate-exec.js +29 -5
- package/dist/commands/task/notify.d.ts +1 -1
- package/dist/commands/task/notify.js +1 -1
- package/dist/commands/task/plan.d.ts +1 -0
- package/dist/commands/task/plan.js +1 -1
- package/dist/commands/task/query.d.ts +2 -0
- package/dist/commands/task/query.js +11 -2
- 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 +20 -0
- package/dist/registry/registry.js +35 -2
- package/dist/server/api-read.js +55 -37
- 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 +14 -6
- package/dist/server/sse.js +22 -10
- 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/dist/spec/plan-change.js +4 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,18 +9,22 @@ export * from './orca/types.js';
|
|
|
9
9
|
export * from './spec/hash.js';
|
|
10
10
|
export * from './spec/load.js';
|
|
11
11
|
export * from './spec/draft.js';
|
|
12
|
+
export * from './spec/doc.js';
|
|
12
13
|
export * from './spec/edit.js';
|
|
13
14
|
export * from './spec/plan-change.js';
|
|
15
|
+
export * from './spec/facade.js';
|
|
14
16
|
export * from './commands/spec.js';
|
|
15
17
|
export * from './commands/about.js';
|
|
16
18
|
export * from './registry/registry.js';
|
|
17
19
|
export * from './brief/render.js';
|
|
18
20
|
export * from './audit.js';
|
|
21
|
+
export * from './audit-table.js';
|
|
19
22
|
export * from './commands/deps.js';
|
|
20
23
|
export * from './commands/locate.js';
|
|
21
24
|
export * from './commands/up.js';
|
|
22
25
|
export * from './commands/restore/index.js';
|
|
23
26
|
export * from './commands/autostart/index.js';
|
|
27
|
+
export * from './commands/autostart/sys.js';
|
|
24
28
|
export * from './commands/version-range.js';
|
|
25
29
|
export * from './commands/ps.js';
|
|
26
30
|
export * from './commands/send.js';
|
|
@@ -48,9 +52,12 @@ export * from './commands/task/escalate.js';
|
|
|
48
52
|
export { HEARTBEAT_STALE_MS, HEARTBEAT_TAKEOVER_MS, dispatcherHealth, dispatcherLaunchCommand, dispatcherUp, dispatcherDown, } from './commands/dispatcher/lifecycle.js';
|
|
49
53
|
export * from './commands/dispatcher/loop.js';
|
|
50
54
|
export * from './commands/dispatcher/attention.js';
|
|
55
|
+
export * from './commands/dispatcher/signal-summary.js';
|
|
51
56
|
export * from './commands/dispatcher/controls.js';
|
|
52
57
|
export * from './commands/dispatcher/inbox.js';
|
|
53
58
|
export * from './commands/dispatcher/outlets.js';
|
|
54
59
|
export * from './commands/dispatcher/signals.js';
|
|
55
60
|
export * from './commands/dispatcher/advance.js';
|
|
56
61
|
export * from './commands/dispatcher/orca-sync.js';
|
|
62
|
+
export * from './commands/templates.js';
|
|
63
|
+
export * from './commands/init.js';
|
package/dist/orca/client.js
CHANGED
|
@@ -29,6 +29,8 @@ function tabTitles(node, found = new Map()) {
|
|
|
29
29
|
}
|
|
30
30
|
const retryArgs = (a) => (a.retryRequest ? ['--retry-request', a.retryRequest] : []);
|
|
31
31
|
const replayedOf = (r) => r.mutation?.replayed ?? false;
|
|
32
|
+
/** run-list 每页 10 条;翻够这么多页还没完,就当找不全(`wl up` 会按这个提示走 --adopt-run)。 */
|
|
33
|
+
const RUN_LIST_MAX_PAGES = 50;
|
|
32
34
|
const toTask = (t) => ({
|
|
33
35
|
id: t.id,
|
|
34
36
|
status: t.status,
|
|
@@ -39,27 +41,35 @@ const toTask = (t) => ({
|
|
|
39
41
|
dispatchId: t.dispatch_id ?? null,
|
|
40
42
|
result: t.result ?? null,
|
|
41
43
|
});
|
|
44
|
+
// Orca 1.4.251 起 run-* 的简版回执只剩 { runId, objective }(没有 coordinator_handle),
|
|
45
|
+
// --full 才给完整记录;下面这几个调用都要 --full,否则协调者句柄读不出来。
|
|
42
46
|
function runMethods(runner) {
|
|
43
47
|
return {
|
|
44
48
|
async runCreate(a) {
|
|
45
|
-
const r = await runner.json(['orchestration', 'run-create', '--objective', a.objective, '--from', a.from, ...retryArgs(a)]);
|
|
49
|
+
const r = await runner.json(['orchestration', 'run-create', '--full', '--objective', a.objective, '--from', a.from, ...retryArgs(a)]);
|
|
46
50
|
return { id: r.run.id, coordinatorHandle: r.run.coordinator_handle, replayed: replayedOf(r) };
|
|
47
51
|
},
|
|
48
52
|
async runUse(a) {
|
|
49
|
-
const r = await runner.json(['orchestration', 'run-use', '--id', a.id, '--from', a.from]);
|
|
53
|
+
const r = await runner.json(['orchestration', 'run-use', '--full', '--id', a.id, '--from', a.from]);
|
|
50
54
|
return { coordinatorHandle: r.run.coordinator_handle };
|
|
51
55
|
},
|
|
52
56
|
async runShow(a) {
|
|
53
|
-
const r = await runner.json(['orchestration', 'run-show', '--id', a.id]);
|
|
57
|
+
const r = await runner.json(['orchestration', 'run-show', '--full', '--id', a.id]);
|
|
54
58
|
return { coordinatorHandle: r.run.coordinator_handle };
|
|
55
59
|
},
|
|
56
60
|
async runList() {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
const runs = [];
|
|
62
|
+
let cursor;
|
|
63
|
+
for (let page = 0; page < RUN_LIST_MAX_PAGES; page++) {
|
|
64
|
+
const r = await runner.json(['orchestration', 'run-list', '--full', ...(cursor ? ['--cursor', cursor] : [])]);
|
|
65
|
+
runs.push(...r.runs.map((x) => ({ id: x.id, objective: x.objective, coordinatorHandle: x.coordinator_handle, createdAt: x.created_at })));
|
|
66
|
+
// 还有下一页就得接着翻:按 objective 找回 Run 会漏掉后面的候选
|
|
67
|
+
const next = r.nextCursor ?? r.page?.nextCursor ?? undefined;
|
|
68
|
+
if (!next || next === cursor)
|
|
69
|
+
return runs;
|
|
70
|
+
cursor = next;
|
|
61
71
|
}
|
|
62
|
-
|
|
72
|
+
throw new WinglineError('orca', 'run_list_truncated', 'run-list 分页超过上限,无法确认没有其他候选 Run', { hint: '用 wl up --adopt-run <id> 指定要找回的 Run' });
|
|
63
73
|
},
|
|
64
74
|
async requestShow(a) {
|
|
65
75
|
const r = await runner.json(['orchestration', 'request-show', '--request', a.request]);
|
|
@@ -166,10 +176,14 @@ function inboxMethods(runner) {
|
|
|
166
176
|
};
|
|
167
177
|
},
|
|
168
178
|
async reply(a) {
|
|
179
|
+
// Orca 1.4.251 起回执是 { messageId };旧版是 { message: { id } }。两种都认。
|
|
169
180
|
const r = await runner.json([
|
|
170
181
|
'orchestration', 'reply', '--id', a.id, '--body', a.body, '--run', a.run, '--from', a.from,
|
|
171
182
|
]);
|
|
172
|
-
|
|
183
|
+
const messageId = r.message?.id ?? r.messageId;
|
|
184
|
+
if (!messageId)
|
|
185
|
+
throw new WinglineError('orca', 'reply_shape_unknown', 'Orca 的回复回执里没有 messageId');
|
|
186
|
+
return { messageId, duplicate: r.duplicate ?? false };
|
|
173
187
|
},
|
|
174
188
|
async workerList(a) {
|
|
175
189
|
const r = await runner.json(['orchestration', 'worker-list', '--run', a.run]);
|
|
@@ -243,6 +257,10 @@ function terminalMethods(runner) {
|
|
|
243
257
|
const r = await runner.json(['terminal', 'close', '--terminal', a.handle]);
|
|
244
258
|
return { ptyKilled: r.close.ptyKilled };
|
|
245
259
|
},
|
|
260
|
+
async terminalSwitch(a) {
|
|
261
|
+
const r = await runner.json(['terminal', 'switch', '--terminal', a.handle]);
|
|
262
|
+
return { navigated: r.focus.navigated };
|
|
263
|
+
},
|
|
246
264
|
};
|
|
247
265
|
}
|
|
248
266
|
/**
|
package/dist/orca/types.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface OrcaClient {
|
|
|
113
113
|
terminalClose(a: {
|
|
114
114
|
handle: string;
|
|
115
115
|
}): Promise<CloseResult>;
|
|
116
|
+
/** 把 Orca 窗口切到某个终端标签页(UI 导航,不改变终端本身)。 */
|
|
117
|
+
terminalSwitch(a: {
|
|
118
|
+
handle: string;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
navigated: boolean;
|
|
121
|
+
}>;
|
|
116
122
|
runCreate(a: {
|
|
117
123
|
objective: string;
|
|
118
124
|
from: string;
|
|
@@ -101,6 +101,8 @@ export interface PatrolStateRow {
|
|
|
101
101
|
detail?: Record<string, unknown> | null;
|
|
102
102
|
}
|
|
103
103
|
export interface FrozenRow {
|
|
104
|
+
/** 冻结行自带的团队名:被冻结的可以是没登记的终端,所以别从地址里反推。 */
|
|
105
|
+
team: string;
|
|
104
106
|
post: string;
|
|
105
107
|
reason: string;
|
|
106
108
|
signal: string | null;
|
|
@@ -170,6 +172,13 @@ export declare class Registry {
|
|
|
170
172
|
/** 换 id 与参数;给了 expectedOld 时是比较后交换:旧 id 已被别的进程换掉则不动,返回 false。 */
|
|
171
173
|
reissueIntent(id: string, retryRequest: string, args: unknown, expectedOld?: string): boolean;
|
|
172
174
|
dropRequest(id: string): void;
|
|
175
|
+
/**
|
|
176
|
+
* 岗位生命周期结束(down --close / 终端已消失)时,把该岗位未完成的请求行结为 done。
|
|
177
|
+
* 不结账的话,下次 up 的 nextRequestId 会沿用在 Orca 侧早已完成的旧行(本地还是 uncertain),
|
|
178
|
+
* beginRequest 与 Orca 的完成记录互相矛盾,up 卡死在 request_already_done。
|
|
179
|
+
* 返回结账的行数。
|
|
180
|
+
*/
|
|
181
|
+
finalizePostRequests(team: string, post: string): number;
|
|
173
182
|
listOpenRequests(prefix: string): Array<{
|
|
174
183
|
id: string;
|
|
175
184
|
state: RequestState;
|
|
@@ -283,6 +292,11 @@ export declare class Registry {
|
|
|
283
292
|
id: number;
|
|
284
293
|
at: string;
|
|
285
294
|
}) | null;
|
|
295
|
+
/** 该信号主体最近一次 notify 事件(不受巡查事件保留窗口影响);task_id 为 NULL 的主体与空串比较。 */
|
|
296
|
+
lastNotifyEventOf(team: string, post: string, signal: string, taskId: string): (PatrolEvent & {
|
|
297
|
+
id: number;
|
|
298
|
+
at: string;
|
|
299
|
+
}) | null;
|
|
286
300
|
appendAudit(r: Omit<AuditRow, 'id' | 'at' | 'detail'> & {
|
|
287
301
|
at?: string;
|
|
288
302
|
detail?: unknown;
|
|
@@ -322,6 +336,12 @@ export declare class Registry {
|
|
|
322
336
|
/** team 是否写过 actions 之一的事件。 */
|
|
323
337
|
hasPatrolEventOf(team: string, actions: readonly string[]): boolean;
|
|
324
338
|
prunePatrolEvents(before: string): number;
|
|
339
|
+
/**
|
|
340
|
+
* 已结束状态的日常清理:已清除的巡查行、已回答的提问、已定案且过保留期的任务账本行。
|
|
341
|
+
* 不动:pending/uncertain 行(可能还在重放)、代数键(restore:*、<team>:<post>:*,删了会让代数从头数起)、
|
|
342
|
+
* done:*:delivered:* 交付行(解冻时按冻结时间点数被跳过的任务)。
|
|
343
|
+
*/
|
|
344
|
+
pruneFinishedState(before: string): void;
|
|
325
345
|
/** finished_at > after(null 表示不限)的已完成判定,按 finished_at 升序。 */
|
|
326
346
|
listFinishedVerdictsAfter(post: string, after: string | null): DoneVerdictRow[];
|
|
327
347
|
/** 2A done 写下的交付记录(requests 里 done:%:delivered:<target> 且 done 的行)。 */
|
|
@@ -74,7 +74,7 @@ const patrolRow = (r) => ({
|
|
|
74
74
|
detail: r.detail === null || r.detail === undefined ? null : JSON.parse(r.detail),
|
|
75
75
|
});
|
|
76
76
|
const frozenRow = (r) => ({
|
|
77
|
-
post: r.post, reason: r.reason, signal: r.signal, frozenAt: r.frozen_at,
|
|
77
|
+
team: r.team, post: r.post, reason: r.reason, signal: r.signal, frozenAt: r.frozen_at,
|
|
78
78
|
});
|
|
79
79
|
const questionRow = (r) => ({
|
|
80
80
|
team: r.team,
|
|
@@ -264,6 +264,18 @@ export class Registry {
|
|
|
264
264
|
dropRequest(id) {
|
|
265
265
|
this.db.prepare('DELETE FROM requests WHERE id = ?').run(id);
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* 岗位生命周期结束(down --close / 终端已消失)时,把该岗位未完成的请求行结为 done。
|
|
269
|
+
* 不结账的话,下次 up 的 nextRequestId 会沿用在 Orca 侧早已完成的旧行(本地还是 uncertain),
|
|
270
|
+
* beginRequest 与 Orca 的完成记录互相矛盾,up 卡死在 request_already_done。
|
|
271
|
+
* 返回结账的行数。
|
|
272
|
+
*/
|
|
273
|
+
finalizePostRequests(team, post) {
|
|
274
|
+
const open = this.listOpenRequests(`${team}:${post}:`);
|
|
275
|
+
for (const r of open)
|
|
276
|
+
this.finishRequest(r.id, 'done', { closed: true });
|
|
277
|
+
return open.length;
|
|
278
|
+
}
|
|
267
279
|
listOpenRequests(prefix) {
|
|
268
280
|
const rows = this.db
|
|
269
281
|
.prepare("SELECT id, state, updated_at, args FROM requests WHERE state IN ('pending','uncertain') AND substr(id, 1, ?) = ? ORDER BY id")
|
|
@@ -466,7 +478,9 @@ export class Registry {
|
|
|
466
478
|
}
|
|
467
479
|
/** unfreeze 用:该岗位所有巡查行回到 level 0 并标记已清除(spec 第 7 节)。画面信号的行不动:解冻不该清掉用量暂停,也不该在提示仍留屏时让它重新通知一次。 */
|
|
468
480
|
resetPatrolForPost(post, at) {
|
|
469
|
-
this.db
|
|
481
|
+
this.db
|
|
482
|
+
.prepare("UPDATE patrol_state SET level = 0, cleared_at = ?, last_nudged_at = NULL, notify_count = 0, next_due_at = NULL WHERE post = ? AND signal NOT IN ('context_low', 'usage_limit')")
|
|
483
|
+
.run(at, post);
|
|
470
484
|
}
|
|
471
485
|
freezePost(a) {
|
|
472
486
|
this.db
|
|
@@ -552,6 +566,13 @@ export class Registry {
|
|
|
552
566
|
const r = this.db.prepare(`SELECT * FROM patrol_events WHERE team = ? AND action IN (${marks}) ORDER BY id DESC LIMIT 1`).get(team, ...actions);
|
|
553
567
|
return r ? eventRow(r) : null;
|
|
554
568
|
}
|
|
569
|
+
/** 该信号主体最近一次 notify 事件(不受巡查事件保留窗口影响);task_id 为 NULL 的主体与空串比较。 */
|
|
570
|
+
lastNotifyEventOf(team, post, signal, taskId) {
|
|
571
|
+
const r = this.db
|
|
572
|
+
.prepare("SELECT * FROM patrol_events WHERE team = ? AND action = 'notify' AND post = ? AND signal = ? AND COALESCE(task_id, '') = ? ORDER BY id DESC LIMIT 1")
|
|
573
|
+
.get(team, post, signal, taskId);
|
|
574
|
+
return r ? eventRow(r) : null;
|
|
575
|
+
}
|
|
555
576
|
// —— 阶段 3A:审计事件、团队任务日志、时间线游标 ——
|
|
556
577
|
// 三者共用 pageRows 的分页语义:id 升序;不带 afterId 取最新 limit 条;带 afterId 取游标之后最早的 limit 条。
|
|
557
578
|
appendAudit(r) {
|
|
@@ -627,6 +648,18 @@ export class Registry {
|
|
|
627
648
|
prunePatrolEvents(before) {
|
|
628
649
|
return Number(this.db.prepare('DELETE FROM patrol_events WHERE at < ?').run(before).changes);
|
|
629
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* 已结束状态的日常清理:已清除的巡查行、已回答的提问、已定案且过保留期的任务账本行。
|
|
653
|
+
* 不动:pending/uncertain 行(可能还在重放)、代数键(restore:*、<team>:<post>:*,删了会让代数从头数起)、
|
|
654
|
+
* done:*:delivered:* 交付行(解冻时按冻结时间点数被跳过的任务)。
|
|
655
|
+
*/
|
|
656
|
+
pruneFinishedState(before) {
|
|
657
|
+
this.db.prepare('DELETE FROM patrol_state WHERE cleared_at IS NOT NULL AND cleared_at < ?').run(before);
|
|
658
|
+
this.db.prepare('DELETE FROM questions WHERE answered_at IS NOT NULL AND answered_at < ?').run(before);
|
|
659
|
+
this.db.prepare(`DELETE FROM requests WHERE state = 'done' AND updated_at < ?
|
|
660
|
+
AND (id LIKE 'dispatch:%' OR id LIKE 'recover:%' OR id LIKE 'resolve:%' OR id LIKE 'oneshot:%'
|
|
661
|
+
OR (id LIKE 'done:%' AND id NOT LIKE 'done:%:delivered:%'))`).run(before);
|
|
662
|
+
}
|
|
630
663
|
/** finished_at > after(null 表示不限)的已完成判定,按 finished_at 升序。 */
|
|
631
664
|
listFinishedVerdictsAfter(post, after) {
|
|
632
665
|
const sql = 'SELECT * FROM task_done WHERE post = ? AND finished_at IS NOT NULL AND finished_at > ? ORDER BY finished_at, task_id';
|
package/dist/server/api-read.js
CHANGED
|
@@ -1,58 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { checkParam } from './http.js';
|
|
3
|
+
import { WinglineError } from '../errors.js';
|
|
2
4
|
import { ps } from '../commands/ps.js';
|
|
3
5
|
import { dispatcherHealth } from '../commands/dispatcher/lifecycle.js';
|
|
4
|
-
import { attention, dispatcherStatus } from '../commands/dispatcher/attention.js';
|
|
6
|
+
import { attention, dispatcherStatus, pendingCount } from '../commands/dispatcher/attention.js';
|
|
5
7
|
import { signalSummary } from '../commands/dispatcher/signal-summary.js';
|
|
6
8
|
import { listTasks, showTask } from '../commands/task/query.js';
|
|
7
9
|
import { listStale } from '../commands/task/query.js';
|
|
8
10
|
import { readTimeline } from '../commands/log.js';
|
|
9
11
|
import { postAddress } from '../commands/deps.js';
|
|
10
12
|
import { plainText } from '../view/plain-text.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
import { draftPathOf } from '../spec/draft.js';
|
|
14
|
+
/** 总览摘要:未启动团队(runId === null)不碰 Orca——ps() 的 preflight 会抛错,直接给注册表口径(spec 3.3.4)。 */
|
|
15
|
+
async function teamSummary(ctx, row) {
|
|
16
|
+
const base = { name: row.name, status: row.status, hasDraft: fs.existsSync(draftPathOf(row.specPath)) };
|
|
17
|
+
const dispatcher = dispatcherHealth(ctx.deps, row.name).state;
|
|
18
|
+
if (row.runId === null) {
|
|
19
|
+
return { ...base, postsCount: ctx.deps.registry.listPosts(row.name).length, dispatcher, attentionCount: 0 };
|
|
20
|
+
}
|
|
21
|
+
const posts = await ps(ctx.deps, { team: row.name });
|
|
22
|
+
const att = await attention(ctx.deps, row.name);
|
|
23
|
+
return { ...base, postsCount: posts.length, dispatcher, attentionCount: pendingCount(att) };
|
|
24
|
+
}
|
|
25
|
+
/** 未启动 / Orca 不可达时的离线岗位:全部字段来自注册表,handle 一律为 null(spec 3.3.4)。 */
|
|
26
|
+
function offlinePosts(posts) {
|
|
27
|
+
return posts.map((p) => ({
|
|
28
|
+
address: p.address, agent: p.agent, status: 'offline', handle: null, worktree: p.worktree, isLead: p.isLead, sessionId: p.sessionId,
|
|
29
|
+
}));
|
|
27
30
|
}
|
|
28
31
|
function registerTeamRoutes(r) {
|
|
29
32
|
// GET /api/teams
|
|
30
33
|
r.get('/api/teams', async (ctx) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
return { teams: result };
|
|
34
|
+
const rows = ctx.deps.registry.listTeams();
|
|
35
|
+
return { teams: await Promise.all(rows.map((row) => teamSummary(ctx, row))) };
|
|
34
36
|
});
|
|
35
37
|
// GET /api/teams/:team
|
|
36
38
|
r.get('/api/teams/:team', async (ctx) => {
|
|
37
39
|
const team = ctx.params.team;
|
|
38
40
|
checkParam('team', team);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
41
|
+
const { registry } = ctx.deps;
|
|
42
|
+
const row = registry.getTeam(team);
|
|
43
|
+
if (!row)
|
|
44
|
+
throw new WinglineError('input', 'team_unknown', `没有团队 ${team}`, { hint: '先执行 wl up <team.yaml>' });
|
|
45
|
+
const base = { team, runId: row.runId, status: row.status, specPath: row.specPath, hasDraft: fs.existsSync(draftPathOf(row.specPath)) };
|
|
46
|
+
// 从未启动:不调 ps()/dispatcherStatus(),Orca 一次都不碰(spec 3.3.4)
|
|
47
|
+
if (row.runId === null) {
|
|
45
48
|
return {
|
|
46
|
-
...
|
|
47
|
-
|
|
49
|
+
...base, neverStarted: true,
|
|
50
|
+
dispatcher: { health: { state: 'never' }, frozen: 0, hushed: 0, attentionItems: 0 },
|
|
51
|
+
posts: offlinePosts(registry.listPosts(team)),
|
|
48
52
|
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
}
|
|
54
|
+
let posts;
|
|
55
|
+
let orcaUnavailable;
|
|
56
|
+
try {
|
|
57
|
+
posts = (await ps(ctx.deps, { team })).map((p) => ({ ...p, sessionId: registry.getPost(p.address)?.sessionId ?? null }));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Orca 不可达:降级为离线岗位列表并附原因,其余照常返回
|
|
61
|
+
if (!(err instanceof WinglineError) || err.kind !== 'orca')
|
|
62
|
+
throw err;
|
|
63
|
+
orcaUnavailable = err.message;
|
|
64
|
+
posts = offlinePosts(registry.listPosts(team));
|
|
65
|
+
}
|
|
66
|
+
return { ...base, ...(orcaUnavailable !== undefined ? { orcaUnavailable } : {}), dispatcher: await dispatcherStatus(ctx.deps, team), posts };
|
|
56
67
|
});
|
|
57
68
|
// GET /api/teams/:team/attention
|
|
58
69
|
r.get('/api/teams/:team/attention', async (ctx) => {
|
|
@@ -140,7 +151,7 @@ function registerPostRoutes(r) {
|
|
|
140
151
|
const posts = await ps(ctx.deps, { team });
|
|
141
152
|
const pRow = posts.find((p) => p.address === address);
|
|
142
153
|
if (!pRow) {
|
|
143
|
-
throw new
|
|
154
|
+
throw new WinglineError('input', 'invalid_param', `岗位 ${address} 不存在`);
|
|
144
155
|
}
|
|
145
156
|
return await buildPostDetail(ctx, team, address, pRow);
|
|
146
157
|
});
|
|
@@ -154,8 +165,15 @@ function registerLogRoute(r) {
|
|
|
154
165
|
const post = ctx.query.get('post') ?? undefined;
|
|
155
166
|
const since = ctx.query.get('since') ?? undefined;
|
|
156
167
|
const source = ctx.query.get('source') ?? undefined;
|
|
168
|
+
// 与 wl log 的 --limit 同一口径:非正整数按用法错报,而不是 NaN 静默读空
|
|
157
169
|
const limitStr = ctx.query.get('limit');
|
|
158
|
-
|
|
170
|
+
let limit;
|
|
171
|
+
if (limitStr !== null) {
|
|
172
|
+
limit = Number(limitStr);
|
|
173
|
+
if (!(Number.isInteger(limit) && limit >= 1)) {
|
|
174
|
+
throw new WinglineError('input', 'limit_invalid', `limit ${limitStr} 无效:应为正整数`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
159
177
|
return await readTimeline(ctx.deps, { team, task, post, since, source, limit });
|
|
160
178
|
});
|
|
161
179
|
}
|
package/dist/server/api-spec.js
CHANGED
|
@@ -12,14 +12,45 @@ function assertFields(body, allowed) {
|
|
|
12
12
|
throw new WinglineError('input', 'unknown_field', `未知字段 ${k}`);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
16
|
+
/** set 的三种形态:{ path, value }(op 缺省)、{ op: 'add_post', post }、{ op: 'remove_post', index }。 */
|
|
15
17
|
function parseSet(raw) {
|
|
16
18
|
if (raw === undefined)
|
|
17
19
|
return undefined;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const bad = new WinglineError('input', 'invalid_param', '参数 set 不合法');
|
|
21
|
+
if (!isObj(raw))
|
|
22
|
+
throw bad;
|
|
23
|
+
if (raw.op === 'add_post') {
|
|
24
|
+
const p = raw.post;
|
|
25
|
+
if (!isObj(p) || typeof p.id !== 'string' || !p.id || typeof p.agent !== 'string' || !p.agent)
|
|
26
|
+
throw bad;
|
|
27
|
+
if (p.brief !== undefined && (!Array.isArray(p.brief) || !p.brief.every((b) => typeof b === 'string')))
|
|
28
|
+
throw bad;
|
|
29
|
+
for (const k of ['model', 'launch', 'worktree']) {
|
|
30
|
+
if (p[k] !== undefined && typeof p[k] !== 'string')
|
|
31
|
+
throw bad;
|
|
32
|
+
}
|
|
33
|
+
return { op: 'add_post', post: p };
|
|
34
|
+
}
|
|
35
|
+
if (raw.op === 'remove_post') {
|
|
36
|
+
if (!Number.isInteger(raw.index) || raw.index < 0)
|
|
37
|
+
throw bad;
|
|
38
|
+
return { op: 'remove_post', index: raw.index };
|
|
39
|
+
}
|
|
40
|
+
if (raw.op !== undefined && raw.op !== 'set')
|
|
41
|
+
throw bad;
|
|
42
|
+
const ok = Array.isArray(raw.path) && raw.path.length > 0 && raw.path.every((p) => typeof p === 'string' || Number.isInteger(p));
|
|
43
|
+
if (!ok || !('value' in raw))
|
|
44
|
+
throw bad;
|
|
45
|
+
return { path: raw.path, value: raw.value };
|
|
46
|
+
}
|
|
47
|
+
function parseFiles(raw) {
|
|
48
|
+
if (raw === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (!isObj(raw) || !Object.values(raw).every((v) => typeof v === 'string')) {
|
|
51
|
+
throw new WinglineError('input', 'invalid_param', '参数 files 不合法');
|
|
52
|
+
}
|
|
53
|
+
return raw;
|
|
23
54
|
}
|
|
24
55
|
/** 编辑器与“关于”页的只读 / 校验接口。validate 虽是 POST,但不写文件、不进审计。 */
|
|
25
56
|
export function registerSpecRoutes(r, o) {
|
|
@@ -29,9 +60,14 @@ export function registerSpecRoutes(r, o) {
|
|
|
29
60
|
});
|
|
30
61
|
r.post('/api/teams/:team/spec/validate', async (ctx) => {
|
|
31
62
|
checkParam('team', ctx.params.team);
|
|
32
|
-
assertFields(ctx.body, ['yaml', 'set']);
|
|
63
|
+
assertFields(ctx.body, ['yaml', 'set', 'files']);
|
|
33
64
|
checkParam('yaml', ctx.body.yaml);
|
|
34
|
-
return validateSpecEdit(ctx.deps, {
|
|
65
|
+
return validateSpecEdit(ctx.deps, {
|
|
66
|
+
team: ctx.params.team,
|
|
67
|
+
yaml: ctx.body.yaml,
|
|
68
|
+
set: parseSet(ctx.body.set),
|
|
69
|
+
files: parseFiles(ctx.body.files),
|
|
70
|
+
});
|
|
35
71
|
});
|
|
36
72
|
r.get('/api/about', async (ctx) => about(ctx.deps, { version: o.version, consoleFile: readConsoleFile(ctx.deps.home) }));
|
|
37
73
|
}
|
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
import type { HandlerContext, Router } from './http.js';
|
|
2
|
+
import { type CommandDeps } from '../commands/deps.js';
|
|
2
3
|
import { type AuditTable } from '../audit.js';
|
|
3
4
|
import { send } from '../commands/send.js';
|
|
4
5
|
import { createTask } from '../commands/task/create.js';
|
|
5
6
|
import { escalateTask, recoverTask, resolveGate } from '../commands/task/escalate.js';
|
|
6
7
|
import { reassignTask } from '../commands/task/claim.js';
|
|
8
|
+
import { type DoneResult } from '../commands/task/done.js';
|
|
9
|
+
import type { Delivery } from '../commands/task/dispatch.js';
|
|
7
10
|
import { freezePostCmd, hushCmd, unfreezePostCmd, unhushCmd } from '../commands/dispatcher/controls.js';
|
|
8
11
|
import { dispatcherReply } from '../commands/dispatcher/inbox.js';
|
|
9
12
|
import { discardDraftCmd, saveSpecCmd } from '../commands/spec.js';
|
|
13
|
+
import { type UpResult } from '../commands/up.js';
|
|
14
|
+
import type { DownResult } from '../commands/down.js';
|
|
15
|
+
import type { RestoreResult } from '../commands/restore/index.js';
|
|
16
|
+
import { initTeamCmd } from '../commands/init.js';
|
|
17
|
+
/** 控制台口径的参数:带 team 名(审计表靠它取团队),开关在路由层已归一成 boolean。 */
|
|
18
|
+
export interface ConsoleUpArgs {
|
|
19
|
+
team: string;
|
|
20
|
+
draft?: boolean;
|
|
21
|
+
newRun?: boolean;
|
|
22
|
+
adoptRun?: string;
|
|
23
|
+
}
|
|
24
|
+
/** 控制台 / 插件的调度员启动参数;node/script 供插件 worker 等非 wingline 宿主覆盖 self 命令。 */
|
|
25
|
+
export interface ConsoleDispatcherUpArgs {
|
|
26
|
+
team: string;
|
|
27
|
+
node?: string;
|
|
28
|
+
script?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ConsoleDownArgs {
|
|
31
|
+
team: string;
|
|
32
|
+
close?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface ConsoleRestoreArgs {
|
|
35
|
+
team: string;
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
noTasks?: boolean;
|
|
38
|
+
sessions?: Record<string, string>;
|
|
39
|
+
}
|
|
10
40
|
export interface WriteCommands {
|
|
11
41
|
send: typeof send;
|
|
12
42
|
createTask: typeof createTask;
|
|
@@ -14,6 +44,31 @@ export interface WriteCommands {
|
|
|
14
44
|
reassignTask: typeof reassignTask;
|
|
15
45
|
recoverTask: typeof recoverTask;
|
|
16
46
|
resolveGate: typeof resolveGate;
|
|
47
|
+
claimTask: (deps: CommandDeps, a: {
|
|
48
|
+
id: string;
|
|
49
|
+
as?: string;
|
|
50
|
+
}) => Promise<void>;
|
|
51
|
+
releaseTask: (deps: CommandDeps, a: {
|
|
52
|
+
id: string;
|
|
53
|
+
as?: string;
|
|
54
|
+
}) => Promise<void>;
|
|
55
|
+
planTask: (deps: CommandDeps, a: {
|
|
56
|
+
id: string;
|
|
57
|
+
as?: string;
|
|
58
|
+
text: string;
|
|
59
|
+
}) => Promise<void>;
|
|
60
|
+
startTask: (deps: CommandDeps, a: {
|
|
61
|
+
id: string;
|
|
62
|
+
as?: string;
|
|
63
|
+
}) => Promise<{
|
|
64
|
+
delivery: Delivery;
|
|
65
|
+
reason?: string;
|
|
66
|
+
already?: true;
|
|
67
|
+
}>;
|
|
68
|
+
doneTask: (deps: CommandDeps, a: {
|
|
69
|
+
id: string;
|
|
70
|
+
as?: string;
|
|
71
|
+
}) => Promise<DoneResult>;
|
|
17
72
|
freezePostCmd: typeof freezePostCmd;
|
|
18
73
|
unfreezePostCmd: typeof unfreezePostCmd;
|
|
19
74
|
hushCmd: typeof hushCmd;
|
|
@@ -21,6 +76,35 @@ export interface WriteCommands {
|
|
|
21
76
|
dispatcherReply: typeof dispatcherReply;
|
|
22
77
|
saveSpecCmd: typeof saveSpecCmd;
|
|
23
78
|
discardDraftCmd: typeof discardDraftCmd;
|
|
79
|
+
up: (deps: CommandDeps, a: ConsoleUpArgs) => Promise<UpResult>;
|
|
80
|
+
down: (deps: CommandDeps, a: ConsoleDownArgs) => Promise<DownResult>;
|
|
81
|
+
restore: (deps: CommandDeps, a: ConsoleRestoreArgs) => Promise<RestoreResult>;
|
|
82
|
+
dispatcherUp: (deps: CommandDeps, a: ConsoleDispatcherUpArgs) => Promise<{
|
|
83
|
+
address: string;
|
|
84
|
+
outcome: 'already_running' | 'started';
|
|
85
|
+
handle?: string;
|
|
86
|
+
}>;
|
|
87
|
+
dispatcherDown: (deps: CommandDeps, a: {
|
|
88
|
+
team: string;
|
|
89
|
+
close?: boolean;
|
|
90
|
+
}) => Promise<{
|
|
91
|
+
address: string;
|
|
92
|
+
outcome: 'noop' | 'closed' | 'already_gone';
|
|
93
|
+
}>;
|
|
94
|
+
initTeamCmd: typeof initTeamCmd;
|
|
95
|
+
autostartInstall: (deps: CommandDeps, a: {
|
|
96
|
+
waitOrca?: string;
|
|
97
|
+
}) => Promise<{
|
|
98
|
+
platform: string;
|
|
99
|
+
file: string;
|
|
100
|
+
command: string[];
|
|
101
|
+
logPath: string;
|
|
102
|
+
waitOrca: string;
|
|
103
|
+
}>;
|
|
104
|
+
autostartUninstall: (deps: CommandDeps) => Promise<{
|
|
105
|
+
file: string;
|
|
106
|
+
removed: boolean;
|
|
107
|
+
}>;
|
|
24
108
|
}
|
|
25
109
|
export interface WriteRouteSpec {
|
|
26
110
|
pattern: string;
|
|
@@ -30,7 +114,7 @@ export interface WriteRouteSpec {
|
|
|
30
114
|
}
|
|
31
115
|
export declare const WRITE_ROUTES: WriteRouteSpec[];
|
|
32
116
|
export declare const CONSOLE_AUDIT: AuditTable<WriteCommands>;
|
|
33
|
-
/** 控制台用 `console:`,TUI 用 `tui
|
|
34
|
-
export declare function auditedWriteCommands(prefix: 'console:' | 'tui:'): WriteCommands;
|
|
117
|
+
/** 控制台用 `console:`,TUI 用 `tui:`,Orca 插件用 `plugin:`;三者共用同一张审计表,执行者都是 human。 */
|
|
118
|
+
export declare function auditedWriteCommands(prefix: 'console:' | 'tui:' | 'plugin:'): WriteCommands;
|
|
35
119
|
export declare const consoleCommands: () => WriteCommands;
|
|
36
120
|
export declare function registerWriteRoutes(r: Router, cmds: WriteCommands): void;
|