@dsh-workbench/shared 0.2.14 → 0.2.16
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/api/dsh.d.ts +1 -0
- package/dist/api/host-write.d.ts +1 -1
- package/dist/components/FormRows.css +10 -4
- package/dist/components/Rows.css +12 -5
- package/dist/components/SelectField.css +20 -0
- package/dist/components/SelectField.d.ts +3 -1
- package/dist/components/SelectField.js +43 -25
- package/dist/components/SelectFieldNodes.d.ts +21 -0
- package/dist/components/SelectFieldNodes.js +17 -0
- package/dist/config/constants.d.ts +3 -0
- package/dist/config/constants.js +3 -0
- package/dist/host/http.d.ts +3 -0
- package/dist/host/http.js +10 -1
- package/dist/host/tool-kit.d.ts +1 -0
- package/dist/host/tool-kit.js +4 -0
- package/dist/host.d.ts +2 -0
- package/dist/host.js +2 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/index.d.ts +15 -1
- package/dist/i18n/zh.d.ts +14 -0
- package/dist/i18n/zh.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/board-fact.d.ts +13 -0
- package/dist/lib/board-fact.js +28 -0
- package/dist/lib/diy-debug.d.ts +39 -0
- package/dist/lib/diy-debug.js +70 -0
- package/package.json +1 -1
- package/src/api/dsh.ts +6 -1
- package/src/api/host-write.ts +5 -1
- package/src/components/FormRows.css +10 -4
- package/src/components/Rows.css +12 -5
- package/src/components/SelectField.css +20 -0
- package/src/components/SelectField.test.tsx +63 -0
- package/src/components/SelectField.tsx +56 -81
- package/src/components/SelectFieldNodes.tsx +77 -0
- package/src/config/constants.ts +5 -0
- package/src/host/http.ts +16 -3
- package/src/host/tool-kit.ts +5 -0
- package/src/host.ts +2 -0
- package/src/i18n/en.ts +15 -0
- package/src/i18n/zh.ts +15 -0
- package/src/index.ts +2 -0
- package/src/lib/board-fact.test.ts +86 -0
- package/src/lib/board-fact.ts +45 -0
- package/src/lib/diy-debug.test.ts +110 -0
- package/src/lib/diy-debug.ts +118 -0
package/src/host/tool-kit.ts
CHANGED
|
@@ -6,6 +6,11 @@ export function sessionWorkspace(exec: DshToolRunContext | undefined): string |
|
|
|
6
6
|
return typeof cwd === 'string' && cwd.trim() !== '' ? cwd.trim() : null;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function sessionIdOf(exec: DshToolRunContext | undefined): string | null {
|
|
10
|
+
const id = exec?.agent?.session?.id;
|
|
11
|
+
return typeof id === 'string' && id.trim() !== '' ? id.trim() : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export function textOutput(text: string): Array<Record<string, unknown>> {
|
|
10
15
|
return [{ type: 'text', text }];
|
|
11
16
|
}
|
package/src/host.ts
CHANGED
|
@@ -7,6 +7,8 @@ export * from './api/long-task.js';
|
|
|
7
7
|
export * from './lib/text.js';
|
|
8
8
|
export * from './lib/ui.js';
|
|
9
9
|
export * from './lib/panel-state.js';
|
|
10
|
+
export * from './lib/board-fact.js';
|
|
11
|
+
export * from './lib/diy-debug.js';
|
|
10
12
|
export * from './api/blocker-table.js';
|
|
11
13
|
export * from './host/browser-find.js';
|
|
12
14
|
export * from './host/browser-read.js';
|
package/src/i18n/en.ts
CHANGED
|
@@ -420,4 +420,19 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
420
420
|
'configSync.noSource': 'This machine has no other profile to sync from',
|
|
421
421
|
'configSync.emptyProviders': 'The workbench has no model provider yet — sync the llm- rows from some profile, or configure one under Models.',
|
|
422
422
|
'configSync.failed': 'The sync did not go through; try again.',
|
|
423
|
+
|
|
424
|
+
'debug.status': 'Workbench DIY debugging',
|
|
425
|
+
'debug.status.trouble': 'Workbench DIY debugging needs attention',
|
|
426
|
+
'debug.stalled': 'Workbench DIY debugging paused',
|
|
427
|
+
'debug.act.pause': 'Pause debugging',
|
|
428
|
+
'debug.act.resume': 'Resume debugging',
|
|
429
|
+
'debug.act.stop': 'Stop debugging',
|
|
430
|
+
'debug.act.stop.ask': 'Confirm stop',
|
|
431
|
+
'debug.card.here.title': 'Workbench DIY debugging is on',
|
|
432
|
+
'debug.card.here.note': 'Do not switch workspaces or touch the panel',
|
|
433
|
+
'debug.card.away.title': 'Workbench DIY debugging is on',
|
|
434
|
+
'debug.card.away.note': 'Switch to the Workbench DIY workspace',
|
|
435
|
+
'debug.card.paused.note': 'Paused',
|
|
436
|
+
'debug.card.hidden.note': 'Switch to the Workbench panel',
|
|
437
|
+
'debug.back': 'Back to the workshop screen',
|
|
423
438
|
};
|
package/src/i18n/zh.ts
CHANGED
|
@@ -415,6 +415,21 @@ export const zh = {
|
|
|
415
415
|
'configSync.noSource': '这台机器上只有工作台这一个 profile,没有可同步的来源',
|
|
416
416
|
'configSync.emptyProviders': '工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。',
|
|
417
417
|
'configSync.failed': '同步没成功,再试一次。',
|
|
418
|
+
|
|
419
|
+
'debug.status': '工作台 DIY 开发调试中',
|
|
420
|
+
'debug.status.trouble': '工作台 DIY 开发调试异常',
|
|
421
|
+
'debug.stalled': '工作台 DIY 调试暂停',
|
|
422
|
+
'debug.act.pause': '暂停调试',
|
|
423
|
+
'debug.act.resume': '继续调试',
|
|
424
|
+
'debug.act.stop': '结束调试',
|
|
425
|
+
'debug.act.stop.ask': '确认结束',
|
|
426
|
+
'debug.card.here.title': '当前工作台 DIY 开发调试中',
|
|
427
|
+
'debug.card.here.note': '请勿切换工作区和操作面板',
|
|
428
|
+
'debug.card.away.title': '工作台 DIY 开发调试中',
|
|
429
|
+
'debug.card.away.note': '请切换到工作台 DIY 工作区',
|
|
430
|
+
'debug.card.paused.note': '已暂停',
|
|
431
|
+
'debug.card.hidden.note': '请切换到工作台面板',
|
|
432
|
+
'debug.back': '回到工坊那一屏',
|
|
418
433
|
} as const;
|
|
419
434
|
|
|
420
435
|
export type WorkbenchMessages = typeof zh;
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,8 @@ export * from './i18n/zh.js';
|
|
|
45
45
|
export * from './lib/blocker-action.js';
|
|
46
46
|
export * from './lib/busy.js';
|
|
47
47
|
export * from './lib/form-problem.js';
|
|
48
|
+
export * from './lib/board-fact.js';
|
|
49
|
+
export * from './lib/diy-debug.js';
|
|
48
50
|
export * from './lib/panel-state.js';
|
|
49
51
|
export * from './lib/react.js';
|
|
50
52
|
export * from './lib/text.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { boardFactOf, boardSituation, insideWorkshop, type BoardProbe } from './board-fact.js';
|
|
3
|
+
|
|
4
|
+
const WORKSHOP = '/w/workbench/diy';
|
|
5
|
+
const FL0XIN = '/w/floxin-app';
|
|
6
|
+
|
|
7
|
+
const probe = (over: Partial<BoardProbe> = {}): BoardProbe => ({
|
|
8
|
+
workbench: true,
|
|
9
|
+
where: WORKSHOP,
|
|
10
|
+
bar: true,
|
|
11
|
+
board: WORKSHOP,
|
|
12
|
+
open: true,
|
|
13
|
+
drawn: true,
|
|
14
|
+
...over,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const gone = { bar: false, board: '', open: false, drawn: false } as const;
|
|
18
|
+
|
|
19
|
+
describe('板事实:一份判据,两边共用', () => {
|
|
20
|
+
it('★★ 画着的就是工坊那块板(含子路径)⇒ workshop', () => {
|
|
21
|
+
expect(boardFactOf(probe(), WORKSHOP)).toBe('workshop');
|
|
22
|
+
expect(boardFactOf(probe({ board: `${WORKSHOP}/packages` }), WORKSHOP)).toBe('workshop');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('★★ 画的是别的项目的板 ⇒ other(前缀像但没落在里面也算 other)', () => {
|
|
26
|
+
expect(boardFactOf(probe({ board: '/w/project' }), WORKSHOP)).toBe('other');
|
|
27
|
+
expect(boardFactOf(probe({ board: `${WORKSHOP}-other` }), WORKSHOP)).toBe('other');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('★★ 那块板没画在屏上 ⇒ hidden:不挂、没声明开着、量出来不可见,三条里任一条不成立都算', () => {
|
|
31
|
+
expect(boardFactOf(probe({ bar: false }), WORKSHOP)).toBe('hidden');
|
|
32
|
+
expect(boardFactOf(probe({ open: false }), WORKSHOP)).toBe('hidden');
|
|
33
|
+
expect(boardFactOf(probe({ drawn: false }), WORKSHOP)).toBe('hidden');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('★★ 工作台没答话(旧外壳 / 夹具页)⇒ unknown:放行,但不许当成"就是工坊板"', () => {
|
|
37
|
+
expect(boardFactOf(probe({ workbench: false }), WORKSHOP)).toBe('unknown');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('★ 画着板但说不清是哪块、或不知道工坊在哪 ⇒ unknown', () => {
|
|
41
|
+
expect(boardFactOf(probe({ board: '' }), WORKSHOP)).toBe('unknown');
|
|
42
|
+
expect(boardFactOf(probe(), null)).toBe('unknown');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('★★ 顺序要紧:没画在屏上,就别去猜是哪块板', () => {
|
|
46
|
+
expect(boardFactOf(probe({ board: WORKSHOP, drawn: false }), WORKSHOP)).toBe('hidden');
|
|
47
|
+
expect(boardFactOf(probe({ workbench: false, bar: false }), WORKSHOP)).toBe('unknown');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('干不了活是为什么:先看人在不在工坊,再看板画没画在屏上', () => {
|
|
52
|
+
it('★★ 在位 ⇒ ok(跟 boardFactOf === "workshop" 是同一个条件)', () => {
|
|
53
|
+
expect(boardSituation(probe(), WORKSHOP)).toBe('ok');
|
|
54
|
+
expect(boardSituation(probe({ board: `${WORKSHOP}/packages` }), WORKSHOP)).toBe('ok');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('★★ 用户根本不在工坊那个工作区 ⇒ off-workspace(哪怕右栏整个没挂也说得清)', () => {
|
|
58
|
+
expect(boardSituation(probe({ where: FL0XIN, ...gone }), WORKSHOP)).toBe('off-workspace');
|
|
59
|
+
expect(boardSituation(probe({ where: FL0XIN }), WORKSHOP)).toBe('ok');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('★★ 人在工坊、可那块板没画在屏上 ⇒ panel-closed', () => {
|
|
63
|
+
expect(boardSituation(probe({ ...gone }), WORKSHOP)).toBe('panel-closed');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('★★ 人在工坊、画着的却是别家的板 ⇒ other-board', () => {
|
|
67
|
+
expect(boardSituation(probe({ board: '/w/project' }), WORKSHOP)).toBe('other-board');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('★ 问不出来(旧外壳 / 夹具页)⇒ unknown:两边都放行、都别喊狼来了', () => {
|
|
71
|
+
expect(boardSituation(probe({ workbench: false }), WORKSHOP)).toBe('unknown');
|
|
72
|
+
expect(boardSituation(probe({ where: '', ...gone }), WORKSHOP)).toBe('panel-closed');
|
|
73
|
+
expect(boardSituation(probe({ board: '' }), WORKSHOP)).toBe('unknown');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('★★ 优先级:不在工坊这一条压过"面板没开"(先说更根本的那一步)', () => {
|
|
77
|
+
expect(boardSituation(probe({ where: FL0XIN, ...gone }), WORKSHOP)).toBe('off-workspace');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('★ 路径归属的纯规则:相同就是里面,前缀必须落在斜杠上', () => {
|
|
81
|
+
expect(insideWorkshop('/a/b', '/a/b')).toBe(true);
|
|
82
|
+
expect(insideWorkshop('/a/b', '/a/b/c')).toBe(true);
|
|
83
|
+
expect(insideWorkshop('/a/b', '/a/bc')).toBe(false);
|
|
84
|
+
expect(insideWorkshop('/a/b/', '/a/b/c')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type BoardFactState =
|
|
2
|
+
| 'workshop'
|
|
3
|
+
| 'other'
|
|
4
|
+
| 'hidden'
|
|
5
|
+
| 'unknown';
|
|
6
|
+
|
|
7
|
+
export type BoardSituation =
|
|
8
|
+
| 'ok'
|
|
9
|
+
| 'off-workspace'
|
|
10
|
+
| 'panel-closed'
|
|
11
|
+
| 'other-board'
|
|
12
|
+
| 'unknown';
|
|
13
|
+
|
|
14
|
+
export interface BoardProbe {
|
|
15
|
+
readonly workbench: boolean;
|
|
16
|
+
readonly where: string;
|
|
17
|
+
readonly bar: boolean;
|
|
18
|
+
readonly board: string;
|
|
19
|
+
readonly open: boolean;
|
|
20
|
+
readonly drawn: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function insideWorkshop(root: string, target: string): boolean {
|
|
24
|
+
if (target === root) return true;
|
|
25
|
+
const base = root.endsWith('/') ? root : `${root}/`;
|
|
26
|
+
return target.startsWith(base);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function boardFactOf(probe: BoardProbe, workshop: string | null): BoardFactState {
|
|
30
|
+
if (!probe.workbench) return 'unknown';
|
|
31
|
+
if (!probe.bar || !probe.open || !probe.drawn) return 'hidden';
|
|
32
|
+
if (probe.board === '' || workshop === null) return 'unknown';
|
|
33
|
+
return insideWorkshop(workshop, probe.board) ? 'workshop' : 'other';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function boardSituation(probe: BoardProbe, workshop: string | null): BoardSituation {
|
|
37
|
+
if (!probe.workbench || workshop === null) return 'unknown';
|
|
38
|
+
const drawn = probe.bar && probe.open && probe.drawn;
|
|
39
|
+
if (drawn) {
|
|
40
|
+
if (probe.board === '') return 'unknown';
|
|
41
|
+
return insideWorkshop(workshop, probe.board) ? 'ok' : 'other-board';
|
|
42
|
+
}
|
|
43
|
+
if (probe.where !== '' && !insideWorkshop(workshop, probe.where)) return 'off-workspace';
|
|
44
|
+
return 'panel-closed';
|
|
45
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEBUG_STALE_MS, createDebugStore, parseDebugIntent, type DebugPact } from './diy-debug.js';
|
|
3
|
+
|
|
4
|
+
const pact = (sessionId: string, now = 1000, unit = 'demo'): DebugPact => ({
|
|
5
|
+
sessionId,
|
|
6
|
+
workspace: '/w/diy',
|
|
7
|
+
board: '/w/diy',
|
|
8
|
+
unit,
|
|
9
|
+
name: unit,
|
|
10
|
+
now,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('调试态:一台机器一次一条,谁开始谁收工', () => {
|
|
14
|
+
it('★ 开始 ⇒ debugging,把目标、开始时间与会话都记下', () => {
|
|
15
|
+
const store = createDebugStore();
|
|
16
|
+
const move = store.begin(pact('s1', 1000));
|
|
17
|
+
|
|
18
|
+
expect(move.ok).toBe(true);
|
|
19
|
+
if (!move.ok) return;
|
|
20
|
+
expect(move.state.phase).toBe('debugging');
|
|
21
|
+
expect(move.state.unit).toBe('demo');
|
|
22
|
+
expect(move.state.startedAt).toBe(1000);
|
|
23
|
+
expect(store.live(1000)).toEqual(move.state);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('★★ 另一场调试占着就不许再开(一次一台机器只允许一场)', () => {
|
|
27
|
+
const store = createDebugStore();
|
|
28
|
+
store.begin(pact('s1', 1000));
|
|
29
|
+
|
|
30
|
+
const second = store.begin(pact('s2', 1200));
|
|
31
|
+
|
|
32
|
+
expect(second).toEqual({ ok: false, code: 'held-by-other' });
|
|
33
|
+
expect(store.live(1200)?.sessionId).toBe('s1');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('★ 同一个会话再来一次 ⇒ already-held(换目标要先收工,或接着看用 resume)', () => {
|
|
37
|
+
const store = createDebugStore();
|
|
38
|
+
store.begin(pact('s1', 1000));
|
|
39
|
+
|
|
40
|
+
expect(store.begin(pact('s1', 1100))).toEqual({ ok: false, code: 'already-held' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('★ 暂停 / 继续 / 收工的正常路径:每回都换 at,不动 startedAt', () => {
|
|
44
|
+
const store = createDebugStore();
|
|
45
|
+
store.begin(pact('s1', 1000));
|
|
46
|
+
|
|
47
|
+
const paused = store.pause('s1', 2000);
|
|
48
|
+
expect(paused.ok && paused.state.phase).toBe('paused');
|
|
49
|
+
expect(paused.ok && paused.state.at).toBe(2000);
|
|
50
|
+
|
|
51
|
+
const resumed = store.resume('s1', 3000);
|
|
52
|
+
expect(resumed.ok && resumed.state.phase).toBe('debugging');
|
|
53
|
+
expect(resumed.ok && resumed.state.startedAt).toBe(1000);
|
|
54
|
+
|
|
55
|
+
const stopped = store.stop('s1', 4000);
|
|
56
|
+
expect(stopped.ok && stopped.state.unit).toBe('demo');
|
|
57
|
+
expect(store.live(4000)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('★ 没开始就暂停 / 继续 / 收工 ⇒ none;暂停态里再暂停 ⇒ already-paused;调试中再继续 ⇒ already-debugging', () => {
|
|
61
|
+
const store = createDebugStore();
|
|
62
|
+
|
|
63
|
+
expect(store.pause('s1', 1000)).toEqual({ ok: false, code: 'none' });
|
|
64
|
+
expect(store.resume('s1', 1000)).toEqual({ ok: false, code: 'none' });
|
|
65
|
+
expect(store.stop('s1', 1000)).toEqual({ ok: false, code: 'none' });
|
|
66
|
+
|
|
67
|
+
store.begin(pact('s1', 1000));
|
|
68
|
+
expect(store.resume('s1', 1000)).toEqual({ ok: false, code: 'already-debugging' });
|
|
69
|
+
expect(store.pause('s1', 2000).ok).toBe(true);
|
|
70
|
+
expect(store.pause('s1', 2500)).toEqual({ ok: false, code: 'already-paused' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('★★ 别人不许动我的调试态(not-owner)—— 谁开始谁收工', () => {
|
|
74
|
+
const store = createDebugStore();
|
|
75
|
+
store.begin(pact('s1', 1000));
|
|
76
|
+
|
|
77
|
+
expect(store.pause('s2', 1500)).toEqual({ ok: false, code: 'not-owner' });
|
|
78
|
+
expect(store.resume('s2', 1500)).toEqual({ ok: false, code: 'not-owner' });
|
|
79
|
+
expect(store.stop('s2', 1500)).toEqual({ ok: false, code: 'not-owner' });
|
|
80
|
+
expect(store.live(1500)?.sessionId).toBe('s1');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('★★ 状态机里没有"自动落 paused"这回事:只有 agent 主动 pause / resume / stop(2026-10-17 夜口径)', () => {
|
|
84
|
+
const store = createDebugStore();
|
|
85
|
+
store.begin(pact('s1', 1000));
|
|
86
|
+
|
|
87
|
+
expect(Object.keys(store)).toEqual(['live', 'begin', 'pause', 'resume', 'stop']);
|
|
88
|
+
expect(store.live(2000)?.phase).toBe('debugging');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('★ 过期当它不存在(时间隔太久,别把上一场调试留给下一个人)', () => {
|
|
92
|
+
const store = createDebugStore();
|
|
93
|
+
store.begin(pact('s1', 1000));
|
|
94
|
+
|
|
95
|
+
expect(store.live(1000 + DEBUG_STALE_MS)).not.toBeNull();
|
|
96
|
+
expect(store.live(1001 + DEBUG_STALE_MS)).toBeNull();
|
|
97
|
+
expect(store.begin(pact('s2', 1001 + DEBUG_STALE_MS)).ok).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('用户在那一格上按的三个键:意图词表只此一份', () => {
|
|
102
|
+
it('★ 认得出三个,其余一律 null(别把别的字当成指令)', () => {
|
|
103
|
+
expect(parseDebugIntent('pause')).toBe('pause');
|
|
104
|
+
expect(parseDebugIntent('resume')).toBe('resume');
|
|
105
|
+
expect(parseDebugIntent('stop')).toBe('stop');
|
|
106
|
+
for (const bad of ['', 'PAUSE', 'Pause', 'kill', 'end', null, undefined, 1, {}, []]) {
|
|
107
|
+
expect(parseDebugIntent(bad)).toBeNull();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type DebugPhase = 'debugging' | 'paused';
|
|
2
|
+
|
|
3
|
+
export type DebugIntent = 'pause' | 'resume' | 'stop';
|
|
4
|
+
|
|
5
|
+
export function parseDebugIntent(value: unknown): DebugIntent | null {
|
|
6
|
+
return value === 'pause' || value === 'resume' || value === 'stop' ? value : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DEBUG_STALE_MS = 4 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export interface DebugState {
|
|
12
|
+
readonly phase: DebugPhase;
|
|
13
|
+
readonly sessionId: string;
|
|
14
|
+
readonly workspace: string;
|
|
15
|
+
readonly board: string;
|
|
16
|
+
readonly unit: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly startedAt: number;
|
|
19
|
+
readonly at: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DebugPact {
|
|
23
|
+
readonly sessionId: string;
|
|
24
|
+
readonly workspace: string;
|
|
25
|
+
readonly board: string;
|
|
26
|
+
readonly unit: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly now: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DebugRefusal =
|
|
32
|
+
| 'already-held'
|
|
33
|
+
| 'held-by-other'
|
|
34
|
+
| 'not-owner'
|
|
35
|
+
| 'none'
|
|
36
|
+
| 'already-paused'
|
|
37
|
+
| 'already-debugging';
|
|
38
|
+
|
|
39
|
+
export type DebugMove =
|
|
40
|
+
| { readonly ok: true; readonly state: DebugState }
|
|
41
|
+
| { readonly ok: false; readonly code: DebugRefusal };
|
|
42
|
+
|
|
43
|
+
export interface DebugStore {
|
|
44
|
+
live(now: number): DebugState | null;
|
|
45
|
+
begin(pact: DebugPact): DebugMove;
|
|
46
|
+
pause(sessionId: string, now: number): DebugMove;
|
|
47
|
+
resume(sessionId: string, now: number): DebugMove;
|
|
48
|
+
stop(sessionId: string, now: number): DebugMove;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function alive(state: DebugState | null, now: number): DebugState | null {
|
|
52
|
+
if (state === null) return null;
|
|
53
|
+
return now - state.at <= DEBUG_STALE_MS ? state : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ownerOf(state: DebugState | null, sessionId: string, now: number): DebugRefusal | DebugState {
|
|
57
|
+
const held = alive(state, now);
|
|
58
|
+
if (held === null) return 'none';
|
|
59
|
+
return held.sessionId === sessionId ? held : 'not-owner';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createDebugStore(): DebugStore {
|
|
63
|
+
let held: DebugState | null = null;
|
|
64
|
+
|
|
65
|
+
const live = (now: number): DebugState | null => alive(held, now);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
live,
|
|
69
|
+
|
|
70
|
+
begin(pact) {
|
|
71
|
+
const current = alive(held, pact.now);
|
|
72
|
+
if (current !== null) {
|
|
73
|
+
return { ok: false, code: current.sessionId === pact.sessionId ? 'already-held' : 'held-by-other' };
|
|
74
|
+
}
|
|
75
|
+
const state: DebugState = {
|
|
76
|
+
phase: 'debugging',
|
|
77
|
+
sessionId: pact.sessionId,
|
|
78
|
+
workspace: pact.workspace,
|
|
79
|
+
board: pact.board,
|
|
80
|
+
unit: pact.unit,
|
|
81
|
+
name: pact.name,
|
|
82
|
+
startedAt: pact.now,
|
|
83
|
+
at: pact.now,
|
|
84
|
+
};
|
|
85
|
+
held = state;
|
|
86
|
+
return { ok: true, state };
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
pause(sessionId, now) {
|
|
90
|
+
const found = ownerOf(held, sessionId, now);
|
|
91
|
+
if (typeof found === 'string') return { ok: false, code: found };
|
|
92
|
+
if (found.phase === 'paused') return { ok: false, code: 'already-paused' };
|
|
93
|
+
const state: DebugState = { ...found, phase: 'paused', at: now };
|
|
94
|
+
held = state;
|
|
95
|
+
return { ok: true, state };
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
resume(sessionId, now) {
|
|
99
|
+
const found = ownerOf(held, sessionId, now);
|
|
100
|
+
if (typeof found === 'string') return { ok: false, code: found };
|
|
101
|
+
if (found.phase === 'debugging') return { ok: false, code: 'already-debugging' };
|
|
102
|
+
const state: DebugState = { ...found, phase: 'debugging', at: now };
|
|
103
|
+
held = state;
|
|
104
|
+
return { ok: true, state };
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
stop(sessionId, now) {
|
|
108
|
+
const found = ownerOf(held, sessionId, now);
|
|
109
|
+
if (typeof found === 'string') return { ok: false, code: found };
|
|
110
|
+
held = null;
|
|
111
|
+
return { ok: true, state: found };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function debugOwnerText(state: DebugState): string {
|
|
117
|
+
return `会话 ${state.sessionId}(自 ${new Date(state.startedAt).toISOString().slice(11, 19)}Z 起在调试 ${state.unit})`;
|
|
118
|
+
}
|