@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const CHECKER_LAUNCH = 'codex --model gpt-6-astra -c model_reasoning_effort="high"';
|
|
2
|
+
export const TEAM_TEMPLATES = {
|
|
3
|
+
duo: {
|
|
4
|
+
id: 'duo',
|
|
5
|
+
label: '双人协作',
|
|
6
|
+
summary: 'builder 负责实现,checker 负责独立审查',
|
|
7
|
+
lead: 'builder',
|
|
8
|
+
posts: [
|
|
9
|
+
{ id: 'builder', role: 'builder', agent: 'claude', model: 'opus', brief: 'briefs/builder.md' },
|
|
10
|
+
{ id: 'checker', role: 'checker', agent: 'codex', launch: CHECKER_LAUNCH, brief: 'briefs/checker.md' },
|
|
11
|
+
],
|
|
12
|
+
links: { builder: { reviewed_by: ['checker'] } },
|
|
13
|
+
},
|
|
14
|
+
solo: {
|
|
15
|
+
id: 'solo',
|
|
16
|
+
label: '单人',
|
|
17
|
+
summary: '单岗位 builder 独立完成实现与自检',
|
|
18
|
+
lead: 'builder',
|
|
19
|
+
posts: [
|
|
20
|
+
{ id: 'builder', role: 'builder', agent: 'claude', model: 'opus', brief: 'briefs/builder.md' },
|
|
21
|
+
],
|
|
22
|
+
links: {},
|
|
23
|
+
},
|
|
24
|
+
trio: {
|
|
25
|
+
id: 'trio',
|
|
26
|
+
label: '三人流水线',
|
|
27
|
+
summary: 'planner 规划拆解,移交 builder 实现,checker 负责审查',
|
|
28
|
+
lead: 'planner',
|
|
29
|
+
posts: [
|
|
30
|
+
{ id: 'planner', role: 'planner', agent: 'claude', model: 'opus', brief: 'briefs/planner.md' },
|
|
31
|
+
{ id: 'builder', role: 'builder', agent: 'claude', model: 'opus', brief: 'briefs/builder.md' },
|
|
32
|
+
{ id: 'checker', role: 'checker', agent: 'codex', launch: CHECKER_LAUNCH, brief: 'briefs/checker.md' },
|
|
33
|
+
],
|
|
34
|
+
links: {
|
|
35
|
+
planner: { hands_to: ['builder'] },
|
|
36
|
+
builder: { reviewed_by: ['checker'] },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export const ROLE_BRIEFS = {
|
|
41
|
+
builder: (team, _post) => [
|
|
42
|
+
`你是 ${team} 团队的实现岗,负责把需求落成代码。`,
|
|
43
|
+
'',
|
|
44
|
+
'- 先读懂需求和现有代码,再说明方案,再实现。',
|
|
45
|
+
'- 优先写测试,再写实现;每次改动保持测试通过。',
|
|
46
|
+
`- 完成一个可审查的改动后,用 \`wl send checker@${team} "<摘要和文件列表>"\` 请审查岗审查。`,
|
|
47
|
+
'- 收到审查意见后,逐条回应:修改,或说明为什么不改。',
|
|
48
|
+
'- 需求不清时向牵头岗或用户确认,不要自行扩大范围。',
|
|
49
|
+
].join('\n'),
|
|
50
|
+
checker: (team, _post) => [
|
|
51
|
+
`你是 ${team} 团队的审查岗,负责独立审查改动。`,
|
|
52
|
+
'',
|
|
53
|
+
'- 收到审查请求后,先看改动本身和相关测试,再给意见。',
|
|
54
|
+
'- 重点看:正确性、边界情况、测试是否真的覆盖了行为、是否引入不必要的复杂度。',
|
|
55
|
+
`- 意见分级:必须修改 / 建议修改 / 仅供参考;用 \`wl send builder@${team} "<意见>"\` 回复。`,
|
|
56
|
+
'- 你可以运行测试和只读命令,但不要直接改实现岗的代码;需要改动时把建议发给对方。',
|
|
57
|
+
'- 没有问题时明确回复"通过",并说明你检查了什么。',
|
|
58
|
+
].join('\n'),
|
|
59
|
+
planner: (team, _post) => [
|
|
60
|
+
`你是 ${team} 团队的规划岗,负责拆解需求与验收标准。`,
|
|
61
|
+
'',
|
|
62
|
+
'- 先读懂需求与现有代码结构,给出清晰的分步方案与边界条件。',
|
|
63
|
+
'- 将拆解好的子任务与验收标准写清楚,避免模糊描述。',
|
|
64
|
+
`- 方案就绪后,用 \`wl send builder@${team} "<方案与任务说明>"\` 移交实现岗。`,
|
|
65
|
+
'- 实现或审查过程中遇到范围变更时,及时更新方案并同步相关岗位。',
|
|
66
|
+
'- 拿不准的产品或架构取舍先向用户确认,不要替用户拍板。',
|
|
67
|
+
].join('\n'),
|
|
68
|
+
};
|
|
69
|
+
export function renderTeamYaml(t, a) {
|
|
70
|
+
const repo = a.repo.startsWith('path:') ? a.repo : `path:${a.repo}`;
|
|
71
|
+
const lines = [
|
|
72
|
+
'version: 1',
|
|
73
|
+
`name: ${a.name}`,
|
|
74
|
+
`repo: ${JSON.stringify(repo)}`,
|
|
75
|
+
'norms: NORMS.md',
|
|
76
|
+
`lead: ${t.lead}`,
|
|
77
|
+
'posts:',
|
|
78
|
+
];
|
|
79
|
+
for (const p of t.posts) {
|
|
80
|
+
lines.push(` - id: ${p.id}`, ` agent: ${p.agent}`);
|
|
81
|
+
if (p.model)
|
|
82
|
+
lines.push(` model: ${p.model}`);
|
|
83
|
+
if (p.launch)
|
|
84
|
+
lines.push(` launch: '${p.launch}'`);
|
|
85
|
+
lines.push(' worktree: current', ` brief: [${p.brief}]`);
|
|
86
|
+
}
|
|
87
|
+
const linkEntries = Object.entries(t.links);
|
|
88
|
+
if (linkEntries.length > 0) {
|
|
89
|
+
lines.push('links:');
|
|
90
|
+
for (const [post, rels] of linkEntries) {
|
|
91
|
+
lines.push(` ${post}:`);
|
|
92
|
+
for (const [rel, targets] of Object.entries(rels)) {
|
|
93
|
+
lines.push(` ${rel}: [${targets.join(', ')}]`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return `${lines.join('\n')}\n`;
|
|
98
|
+
}
|
|
99
|
+
export function renderNorms(a) {
|
|
100
|
+
const lead = a.posts[0] ?? 'builder';
|
|
101
|
+
const peer = a.posts[1] ?? lead;
|
|
102
|
+
return [
|
|
103
|
+
`# ${a.team} 团队规范`,
|
|
104
|
+
'',
|
|
105
|
+
`在岗角色:${a.posts.join('、')}。`,
|
|
106
|
+
'',
|
|
107
|
+
'1. 改动前先用一两句话说明你打算怎么做,再动手。',
|
|
108
|
+
'2. 改完先自己跑测试,确认通过再汇报。',
|
|
109
|
+
`3. 需要其他岗位审查或回答时,用 \`wl send <地址> "<消息>"\` 联系,地址形如 \`${peer}@${a.team}\`。`,
|
|
110
|
+
'4. 消息写清楚:改了什么、在哪个文件、希望对方做什么。',
|
|
111
|
+
'5. 不修改其他岗位正在改的文件;拿不准时先问。',
|
|
112
|
+
'6. 小步提交,提交信息说明为什么改。',
|
|
113
|
+
`7. 遇到无法判断的事(需求不清、权限、破坏性操作),停下来问牵头岗 \`${lead}@${a.team}\`,不要猜。`,
|
|
114
|
+
'8. 收到消息后尽快回复;暂时无法处理时说明原因。',
|
|
115
|
+
'',
|
|
116
|
+
].join('\n');
|
|
117
|
+
}
|
|
118
|
+
export function renderBrief(role, a) {
|
|
119
|
+
return `# ${a.post} 岗位说明\n\n${ROLE_BRIEFS[role](a.team, a.post)}\n`;
|
|
120
|
+
}
|
package/dist/index.d.ts
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, type DispatcherHealth, type UpArgs as DispatcherUpArgs, } 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/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;
|
|
@@ -172,6 +172,13 @@ export declare class Registry {
|
|
|
172
172
|
/** 换 id 与参数;给了 expectedOld 时是比较后交换:旧 id 已被别的进程换掉则不动,返回 false。 */
|
|
173
173
|
reissueIntent(id: string, retryRequest: string, args: unknown, expectedOld?: string): boolean;
|
|
174
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;
|
|
175
182
|
listOpenRequests(prefix: string): Array<{
|
|
176
183
|
id: string;
|
|
177
184
|
state: RequestState;
|
|
@@ -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")
|
package/dist/server/api-read.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { checkParam } from './http.js';
|
|
2
3
|
import { WinglineError } from '../errors.js';
|
|
3
4
|
import { ps } from '../commands/ps.js';
|
|
@@ -9,45 +10,60 @@ import { listStale } from '../commands/task/query.js';
|
|
|
9
10
|
import { readTimeline } from '../commands/log.js';
|
|
10
11
|
import { postAddress } from '../commands/deps.js';
|
|
11
12
|
import { plainText } from '../view/plain-text.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
postsCount:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
}));
|
|
22
30
|
}
|
|
23
31
|
function registerTeamRoutes(r) {
|
|
24
32
|
// GET /api/teams
|
|
25
33
|
r.get('/api/teams', async (ctx) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return { teams: result };
|
|
34
|
+
const rows = ctx.deps.registry.listTeams();
|
|
35
|
+
return { teams: await Promise.all(rows.map((row) => teamSummary(ctx, row))) };
|
|
29
36
|
});
|
|
30
37
|
// GET /api/teams/:team
|
|
31
38
|
r.get('/api/teams/:team', async (ctx) => {
|
|
32
39
|
const team = ctx.params.team;
|
|
33
40
|
checkParam('team', team);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
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) {
|
|
40
48
|
return {
|
|
41
|
-
...
|
|
42
|
-
|
|
49
|
+
...base, neverStarted: true,
|
|
50
|
+
dispatcher: { health: { state: 'never' }, frozen: 0, hushed: 0, attentionItems: 0 },
|
|
51
|
+
posts: offlinePosts(registry.listPosts(team)),
|
|
43
52
|
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 };
|
|
51
67
|
});
|
|
52
68
|
// GET /api/teams/:team/attention
|
|
53
69
|
r.get('/api/teams/:team/attention', async (ctx) => {
|
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;
|