@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.
Files changed (63) hide show
  1. package/dist/audit-table.d.ts +11 -0
  2. package/dist/audit-table.js +85 -0
  3. package/dist/audit.js +1 -1
  4. package/dist/commands/about.d.ts +13 -0
  5. package/dist/commands/about.js +12 -1
  6. package/dist/commands/autostart/sys.d.ts +3 -0
  7. package/dist/commands/autostart/sys.js +23 -0
  8. package/dist/commands/deps.d.ts +2 -0
  9. package/dist/commands/deps.js +2 -0
  10. package/dist/commands/dispatcher/attention.d.ts +2 -0
  11. package/dist/commands/dispatcher/attention.js +10 -9
  12. package/dist/commands/dispatcher/controls.d.ts +1 -1
  13. package/dist/commands/dispatcher/controls.js +5 -3
  14. package/dist/commands/dispatcher/lifecycle.d.ts +4 -0
  15. package/dist/commands/dispatcher/lifecycle.js +7 -1
  16. package/dist/commands/dispatcher/loop.js +2 -0
  17. package/dist/commands/down.js +3 -0
  18. package/dist/commands/init.d.ts +17 -0
  19. package/dist/commands/init.js +106 -0
  20. package/dist/commands/launch.d.ts +10 -2
  21. package/dist/commands/launch.js +14 -3
  22. package/dist/commands/restore/index.js +6 -0
  23. package/dist/commands/restore/posts.d.ts +1 -1
  24. package/dist/commands/restore/posts.js +17 -17
  25. package/dist/commands/restore/resume-command.d.ts +0 -1
  26. package/dist/commands/restore/resume-command.js +0 -3
  27. package/dist/commands/send.js +1 -2
  28. package/dist/commands/spec.d.ts +8 -6
  29. package/dist/commands/spec.js +66 -9
  30. package/dist/commands/task/gate-exec.d.ts +5 -1
  31. package/dist/commands/task/gate-exec.js +29 -5
  32. package/dist/commands/task/notify.d.ts +1 -1
  33. package/dist/commands/task/notify.js +1 -1
  34. package/dist/commands/task/plan.d.ts +1 -0
  35. package/dist/commands/task/plan.js +1 -1
  36. package/dist/commands/task/query.d.ts +2 -0
  37. package/dist/commands/task/query.js +11 -2
  38. package/dist/commands/templates.d.ts +32 -0
  39. package/dist/commands/templates.js +120 -0
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.js +7 -0
  42. package/dist/orca/client.js +27 -9
  43. package/dist/orca/types.d.ts +6 -0
  44. package/dist/registry/registry.d.ts +20 -0
  45. package/dist/registry/registry.js +35 -2
  46. package/dist/server/api-read.js +55 -37
  47. package/dist/server/api-spec.js +43 -7
  48. package/dist/server/api-write.d.ts +86 -2
  49. package/dist/server/api-write.js +285 -12
  50. package/dist/server/index.js +14 -6
  51. package/dist/server/sse.js +22 -10
  52. package/dist/spec/doc.d.ts +13 -0
  53. package/dist/spec/doc.js +62 -0
  54. package/dist/spec/draft.d.ts +16 -0
  55. package/dist/spec/draft.js +15 -8
  56. package/dist/spec/edit.d.ts +21 -0
  57. package/dist/spec/edit.js +136 -0
  58. package/dist/spec/facade.d.ts +41 -0
  59. package/dist/spec/facade.js +68 -0
  60. package/dist/spec/load.d.ts +1 -0
  61. package/dist/spec/load.js +7 -6
  62. package/dist/spec/plan-change.js +4 -2
  63. 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';
@@ -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 r = await runner.json(['orchestration', 'run-list']);
58
- // 还有下一页就不能当成"找全了":按 objective 找回 Run 会漏掉后面的候选
59
- if (r.nextCursor ?? r.page?.nextCursor) {
60
- throw new WinglineError('orca', 'run_list_truncated', 'run-list 结果不止一页,无法确认没有其他候选 Run', { hint: '用 wl up --adopt-run <id> 指定要找回的 Run' });
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
- return r.runs.map((x) => ({ id: x.id, objective: x.objective, coordinatorHandle: x.coordinator_handle, createdAt: x.created_at }));
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
- return { messageId: r.message.id, duplicate: r.duplicate ?? false };
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
  /**
@@ -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.prepare("UPDATE patrol_state SET level = 0, cleared_at = ?, last_nudged_at = NULL WHERE post = ? AND signal NOT IN ('context_low', 'usage_limit')").run(at, post);
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';
@@ -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
- async function teamSummary(ctx, name) {
12
- const posts = await ps(ctx.deps, { team: name });
13
- const disp = dispatcherHealth(ctx.deps, name).state;
14
- const att = await attention(ctx.deps, name);
15
- const attentionCount = (disp === 'stale' ? 1 : 0) +
16
- att.frozen.length +
17
- att.signals.length +
18
- att.questions.length +
19
- att.stale.length +
20
- att.warnings.length;
21
- return {
22
- name,
23
- postsCount: posts.length,
24
- dispatcher: disp,
25
- attentionCount,
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 teams = ctx.deps.registry.listTeams().map((t) => t.name);
32
- const result = await Promise.all(teams.map((name) => teamSummary(ctx, name)));
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 posts = await ps(ctx.deps, { team });
40
- const disp = await dispatcherStatus(ctx.deps, team);
41
- const runId = ctx.deps.registry.getTeam(team)?.runId ?? null;
42
- // 移除 sessionMarker,只留 sessionId
43
- const cleanPosts = posts.map((p) => {
44
- const row = ctx.deps.registry.getPost(p.address);
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
- ...p,
47
- sessionId: row?.sessionId ?? null,
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
- return {
51
- team,
52
- runId,
53
- dispatcher: disp,
54
- posts: cleanPosts,
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 (await import('../errors.js')).WinglineError('input', 'invalid_param', `岗位 ${address} 不存在`);
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
- const limit = limitStr ? parseInt(limitStr, 10) : undefined;
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
  }
@@ -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 s = raw;
19
- const ok = s && typeof s === 'object' && Array.isArray(s.path) && s.path.length > 0 && s.path.every((p) => typeof p === 'string' || Number.isInteger(p));
20
- if (!ok || !('value' in s))
21
- throw new WinglineError('input', 'invalid_param', '参数 set 不合法');
22
- return { path: s.path, value: s.value };
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, { team: ctx.params.team, yaml: ctx.body.yaml, set: parseSet(ctx.body.set) });
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:`;两者共用同一张审计表,执行者都是 human。 */
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;