@dsh-workbench/shared 0.3.5 → 0.3.7
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/context-ref.d.ts +0 -12
- package/dist/api/context-ref.js +0 -40
- package/dist/api/facts.d.ts +0 -6
- package/dist/api/facts.js +0 -8
- package/dist/api/long-task.d.ts +33 -4
- package/dist/api/long-task.js +34 -2
- package/dist/components/ConfirmBar.css +26 -0
- package/dist/components/ConfirmBar.d.ts +36 -0
- package/dist/components/ConfirmBar.js +26 -0
- package/dist/components/Form.d.ts +7 -5
- package/dist/components/Form.js +5 -3
- package/dist/components/FormNodes.d.ts +17 -13
- package/dist/components/FormNodes.js +13 -5
- package/dist/components/FormRows.css +31 -0
- package/dist/components/FormTypes.d.ts +39 -14
- package/dist/components/LongTaskRow.css +52 -0
- package/dist/components/LongTaskRow.d.ts +18 -2
- package/dist/components/LongTaskRow.js +33 -5
- package/dist/components/MockStrip.css +11 -1
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +2 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/board-fact.d.ts +8 -0
- package/dist/host/board-fact.js +16 -0
- package/dist/host/browser-read.js +2 -1
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/context-ref.d.ts +13 -0
- package/dist/host/context-ref.js +41 -0
- package/dist/host/diy-debug.d.ts +80 -0
- package/dist/host/diy-debug.js +88 -0
- package/dist/host/diy-record.d.ts +135 -0
- package/dist/host/diy-record.js +162 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/facts.d.ts +6 -0
- package/dist/host/facts.js +9 -0
- package/dist/host/git.js +1 -1
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +3 -3
- package/dist/host/long-task.d.ts +12 -0
- package/dist/host/long-task.js +34 -5
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/opencli-runtime.js +1 -1
- package/dist/host/opencli-window.js +1 -1
- package/dist/host/opencli.js +2 -1
- package/dist/host/panel-state.d.ts +120 -0
- package/dist/host/panel-state.js +176 -0
- package/dist/host/plugin-source.js +1 -1
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/shell-text.d.ts +13 -0
- package/dist/host/shell-text.js +20 -0
- package/dist/host/tool-approval.d.ts +1 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-declaration.d.ts +65 -0
- package/dist/host/unit-declaration.js +213 -0
- package/dist/host/unit-face.d.ts +1 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host.d.ts +7 -4
- package/dist/host.js +7 -4
- package/dist/i18n/en.js +6 -0
- package/dist/i18n/index.d.ts +7 -1
- package/dist/i18n/zh.d.ts +6 -0
- package/dist/i18n/zh.js +6 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/lib/board-fact.d.ts +0 -7
- package/dist/lib/board-fact.js +0 -15
- package/dist/lib/diy-record.d.ts +38 -1
- package/dist/lib/diy-record.js +66 -0
- package/dist/lib/text.d.ts +0 -13
- package/dist/lib/text.js +0 -20
- package/dist/testing/declaration.js +1 -1
- package/package.json +1 -1
- package/src/api/__tests__/facts.test.ts +0 -13
- package/src/api/__tests__/long-task.test.ts +41 -1
- package/src/api/context-ref.ts +0 -39
- package/src/api/facts.ts +0 -9
- package/src/api/long-task.ts +46 -4
- package/src/components/ConfirmBar.css +26 -0
- package/src/components/ConfirmBar.test.tsx +130 -0
- package/src/components/ConfirmBar.tsx +63 -0
- package/src/components/Form.test.tsx +15 -0
- package/src/components/Form.tsx +7 -5
- package/src/components/FormNodes.tsx +50 -26
- package/src/components/FormRows.css +31 -0
- package/src/components/FormRows.test.tsx +71 -1
- package/src/components/FormTypes.test.ts +31 -0
- package/src/components/FormTypes.ts +46 -15
- package/src/components/FormWiring.test.tsx +1 -0
- package/src/components/LongTaskRow.css +52 -0
- package/src/components/LongTaskRow.tsx +59 -4
- package/src/components/__tests__/long-task-row.test.ts +115 -0
- package/src/config/constants.ts +3 -0
- package/src/host/__tests__/board-fact.test.ts +47 -0
- package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
- package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
- package/src/host/__tests__/facts.test.ts +13 -0
- package/src/host/__tests__/long-task.test.ts +306 -0
- package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
- package/src/host/__tests__/preflight-route.test.ts +33 -0
- package/src/host/__tests__/shell-text.test.ts +44 -0
- package/src/host/board-fact.ts +14 -0
- package/src/host/browser-read.ts +2 -1
- package/src/host/context-ref.ts +40 -0
- package/src/{lib → host}/diy-record.ts +95 -1
- package/src/host/facts.ts +10 -0
- package/src/host/git.ts +1 -1
- package/src/host/http.ts +4 -4
- package/src/host/long-task.ts +52 -7
- package/src/host/opencli-runtime.ts +1 -1
- package/src/host/opencli-window.ts +1 -1
- package/src/host/opencli.ts +2 -1
- package/src/host/plugin-source.ts +1 -1
- package/src/host/shell-text.ts +21 -0
- package/src/host/tool-approval.ts +1 -0
- package/src/{api → host}/unit-declaration.ts +1 -1
- package/src/host/unit-face.ts +1 -0
- package/src/host.ts +7 -4
- package/src/i18n/en.ts +7 -0
- package/src/i18n/zh.ts +6 -0
- package/src/index.ts +1 -4
- package/src/lib/board-fact.test.ts +1 -33
- package/src/lib/board-fact.ts +0 -13
- package/src/lib/text.test.ts +1 -43
- package/src/lib/text.ts +0 -22
- package/src/testing/declaration.ts +1 -1
- package/dist/lib/lint-probe.d.ts +0 -1
- package/dist/lib/lint-probe.js +0 -6
- /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
- /package/src/{lib → host}/diy-debug.ts +0 -0
- /package/src/{lib → host}/panel-state.ts +0 -0
package/dist/host/long-task.js
CHANGED
|
@@ -3,11 +3,18 @@ const LIVE = new Set(['running', 'needs-login']);
|
|
|
3
3
|
const RECENT_MS = 10 * 60 * 1000;
|
|
4
4
|
export function createLongTaskRegistry(deps) {
|
|
5
5
|
const runs = new Map();
|
|
6
|
+
const controls = new Map();
|
|
6
7
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
7
8
|
let counter = 0;
|
|
8
9
|
const nextId = deps.nextId ?? (() => `run-${Date.now()}-${(counter += 1)}`);
|
|
9
10
|
const recentMs = deps.recentMs ?? RECENT_MS;
|
|
10
11
|
const listeners = new Set();
|
|
12
|
+
const live = (id) => {
|
|
13
|
+
const current = runs.get(id);
|
|
14
|
+
if (current === undefined || !LIVE.has(current.status))
|
|
15
|
+
return null;
|
|
16
|
+
return current;
|
|
17
|
+
};
|
|
11
18
|
const notify = (change) => {
|
|
12
19
|
for (const listener of [...listeners]) {
|
|
13
20
|
try {
|
|
@@ -46,7 +53,10 @@ export function createLongTaskRegistry(deps) {
|
|
|
46
53
|
return [...newest.values()]
|
|
47
54
|
.filter((snapshot) => LIVE.has(snapshot.status) || Date.parse(snapshot.at) >= floor)
|
|
48
55
|
.reverse()
|
|
49
|
-
.map((snapshot) => deps.view(snapshot));
|
|
56
|
+
.map((snapshot) => ({ ...deps.view(snapshot), id: snapshot.id }));
|
|
57
|
+
},
|
|
58
|
+
live() {
|
|
59
|
+
return [...runs.values()].filter((snapshot) => LIVE.has(snapshot.status));
|
|
50
60
|
},
|
|
51
61
|
start(options) {
|
|
52
62
|
const running = this.active(options.workspace, options.target);
|
|
@@ -63,11 +73,12 @@ export function createLongTaskRegistry(deps) {
|
|
|
63
73
|
steps: [],
|
|
64
74
|
startedAt,
|
|
65
75
|
at: startedAt,
|
|
76
|
+
...(options.session === undefined ? {} : { session: options.session }),
|
|
66
77
|
});
|
|
67
78
|
notify({ kind: 'start', snapshot: runs.get(id) });
|
|
68
79
|
const onStep = (step) => {
|
|
69
|
-
const current =
|
|
70
|
-
if (current ===
|
|
80
|
+
const current = live(id);
|
|
81
|
+
if (current === null)
|
|
71
82
|
return;
|
|
72
83
|
const steps = [...current.steps, `${step.kind === 'fail' ? '✗ ' : ''}${step.text}`].slice(-20);
|
|
73
84
|
const extra = deps.step?.(step, current) ?? {};
|
|
@@ -83,13 +94,21 @@ export function createLongTaskRegistry(deps) {
|
|
|
83
94
|
...(current.status === 'needs-login' ? { loginUrl: undefined } : {}),
|
|
84
95
|
}, { kind: 'step', step });
|
|
85
96
|
};
|
|
97
|
+
const control = new AbortController();
|
|
98
|
+
controls.set(id, control);
|
|
86
99
|
void deps
|
|
87
|
-
.run({ payload: options.payload, onStep })
|
|
100
|
+
.run({ payload: options.payload, onStep, signal: control.signal })
|
|
88
101
|
.then((result) => {
|
|
89
|
-
const current =
|
|
102
|
+
const current = live(id);
|
|
103
|
+
if (current === null)
|
|
104
|
+
return;
|
|
105
|
+
controls.delete(id);
|
|
90
106
|
patch(id, { status: 'ok', ...(deps.outcome?.(result, current) ?? {}), loginUrl: undefined }, { kind: 'settle' });
|
|
91
107
|
})
|
|
92
108
|
.catch((error) => {
|
|
109
|
+
if (live(id) === null)
|
|
110
|
+
return;
|
|
111
|
+
controls.delete(id);
|
|
93
112
|
patch(id, {
|
|
94
113
|
status: 'failed',
|
|
95
114
|
why: error instanceof Error ? error.message : String(error),
|
|
@@ -98,6 +117,15 @@ export function createLongTaskRegistry(deps) {
|
|
|
98
117
|
});
|
|
99
118
|
return runs.get(id);
|
|
100
119
|
},
|
|
120
|
+
stop(id, by) {
|
|
121
|
+
const current = live(id);
|
|
122
|
+
if (current === null)
|
|
123
|
+
return false;
|
|
124
|
+
controls.get(id)?.abort();
|
|
125
|
+
controls.delete(id);
|
|
126
|
+
patch(id, { ...(deps.stopped?.(current) ?? {}), status: 'stopped', loginUrl: undefined }, { kind: 'settle', by });
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
101
129
|
snapshot(id) {
|
|
102
130
|
return runs.get(id) ?? null;
|
|
103
131
|
},
|
|
@@ -136,6 +164,7 @@ export function createLongTaskHost(options) {
|
|
|
136
164
|
offs.push(registry.onChange((change) => {
|
|
137
165
|
const named = [runsRouteOf(options.unit), ...(facts?.(change) ?? [])];
|
|
138
166
|
options.bump([...new Set(named)]);
|
|
167
|
+
options.notify?.(change);
|
|
139
168
|
}));
|
|
140
169
|
return registry;
|
|
141
170
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DshHostContext } from '@dsh-workbench/shared';
|
|
2
|
+
import type { UnitManifest } from '@dsh-workbench/shared';
|
|
3
|
+
import { type RouteTable } from './http';
|
|
4
|
+
import type { ContextRenderer, UnitHostFace, UnitMountDeps, UnitMountResult, UnitCall } from './unit-face';
|
|
5
|
+
import { type UnitReadiness } from './unit-readiness';
|
|
6
|
+
export interface MountedUnit {
|
|
7
|
+
readonly face: UnitHostFace;
|
|
8
|
+
readonly result: UnitMountResult;
|
|
9
|
+
readonly readiness: UnitReadiness;
|
|
10
|
+
}
|
|
11
|
+
export interface UnitsPlan {
|
|
12
|
+
readonly mounted: readonly MountedUnit[];
|
|
13
|
+
readonly routes: RouteTable;
|
|
14
|
+
readonly gate: UnitMountResult['gate'];
|
|
15
|
+
readonly prefs: ReadonlyArray<{
|
|
16
|
+
readonly resource: string;
|
|
17
|
+
readonly unit: string;
|
|
18
|
+
handle(payload: unknown): unknown;
|
|
19
|
+
}>;
|
|
20
|
+
readonly actions: ReadonlyArray<{
|
|
21
|
+
readonly resource: string;
|
|
22
|
+
readonly unit: string;
|
|
23
|
+
handle(payload: unknown): unknown;
|
|
24
|
+
}>;
|
|
25
|
+
readonly provision: UnitMountResult['provision'];
|
|
26
|
+
readonly contextRenderers: ReadonlyMap<string, ContextRenderer>;
|
|
27
|
+
readonly owners: ReadonlyMap<string, string>;
|
|
28
|
+
readonly routesOfUnit: ReadonlyMap<string, readonly string[]>;
|
|
29
|
+
readonly factsOf: ReadonlyMap<string, (call: UnitCall) => readonly string[] | undefined>;
|
|
30
|
+
readonly skipped: ReadonlyArray<{
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly missing: readonly string[];
|
|
33
|
+
}>;
|
|
34
|
+
readonly warmReads: () => readonly (readonly string[])[];
|
|
35
|
+
readonly disposeLongTasks: () => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function mountUnits(faces: readonly UnitHostFace[], deps: {
|
|
38
|
+
ctx: DshHostContext;
|
|
39
|
+
logger?: {
|
|
40
|
+
info?(m: string): void;
|
|
41
|
+
warn?(m: string): void;
|
|
42
|
+
} | undefined;
|
|
43
|
+
manifests: () => readonly UnitManifest[];
|
|
44
|
+
opencliCommand?: string;
|
|
45
|
+
probe?: UnitMountDeps['probe'];
|
|
46
|
+
store?: UnitMountDeps['store'];
|
|
47
|
+
values?: UnitMountDeps['values'];
|
|
48
|
+
longTasks?: {
|
|
49
|
+
readonly scope: string;
|
|
50
|
+
readonly bump: (facts: readonly string[]) => void;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}): UnitsPlan;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { runsRouteOf } from '@dsh-workbench/shared';
|
|
2
|
+
import { param } from './http';
|
|
3
|
+
import { createLongTaskHost } from './long-task';
|
|
4
|
+
import { unitReadiness } from './unit-readiness';
|
|
5
|
+
import { createWorkspaceStore } from './workspace-store';
|
|
6
|
+
function withRunsRoute(result, unit) {
|
|
7
|
+
const runs = result.longTasks;
|
|
8
|
+
if (runs === undefined)
|
|
9
|
+
return result;
|
|
10
|
+
return {
|
|
11
|
+
...result,
|
|
12
|
+
routes: {
|
|
13
|
+
...result.routes,
|
|
14
|
+
[runsRouteOf(unit)]: (search) => {
|
|
15
|
+
const path = (param(search, 'path') ?? '').trim();
|
|
16
|
+
return { ok: true, runs: path === '' ? [] : runs.current(path) };
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function collectRoutes(mounted) {
|
|
22
|
+
const routes = {};
|
|
23
|
+
const owner = new Map();
|
|
24
|
+
const routesOfUnit = new Map();
|
|
25
|
+
for (const item of mounted) {
|
|
26
|
+
const unit = item.face.manifest.id;
|
|
27
|
+
for (const [key, handler] of Object.entries(item.result.routes)) {
|
|
28
|
+
if (key !== unit && !key.startsWith(`${unit}/`)) {
|
|
29
|
+
throw new Error(`路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— ` +
|
|
30
|
+
`装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`);
|
|
31
|
+
}
|
|
32
|
+
const previous = owner.get(key);
|
|
33
|
+
if (previous !== undefined) {
|
|
34
|
+
throw new Error(`路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`);
|
|
35
|
+
}
|
|
36
|
+
owner.set(key, unit);
|
|
37
|
+
routes[key] = handler;
|
|
38
|
+
const own = routesOfUnit.get(unit);
|
|
39
|
+
if (own === undefined)
|
|
40
|
+
routesOfUnit.set(unit, [key]);
|
|
41
|
+
else
|
|
42
|
+
own.push(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { routes, owners: owner, routesOfUnit };
|
|
46
|
+
}
|
|
47
|
+
function pickGate(mounted) {
|
|
48
|
+
const providers = mounted.filter((item) => item.result.gate !== undefined);
|
|
49
|
+
if (providers.length > 1) {
|
|
50
|
+
throw new Error(`启停闸门只能有一个提供者,现在是:${providers.map((item) => item.face.manifest.id).join('、')}`);
|
|
51
|
+
}
|
|
52
|
+
return providers[0]?.result.gate;
|
|
53
|
+
}
|
|
54
|
+
function pickProvision(mounted) {
|
|
55
|
+
const providers = mounted.filter((item) => item.result.provision !== undefined);
|
|
56
|
+
if (providers.length > 1) {
|
|
57
|
+
throw new Error(`机器供给只能有一个提供者,现在是:${providers.map((item) => item.face.manifest.id).join('、')}`);
|
|
58
|
+
}
|
|
59
|
+
return providers[0]?.result.provision;
|
|
60
|
+
}
|
|
61
|
+
function collectPrefs(mounted) {
|
|
62
|
+
const owner = new Map();
|
|
63
|
+
const prefs = [];
|
|
64
|
+
for (const item of mounted) {
|
|
65
|
+
const entry = item.result.prefs;
|
|
66
|
+
if (entry === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const previous = owner.get(entry.resource);
|
|
69
|
+
if (previous !== undefined) {
|
|
70
|
+
throw new Error(`偏好资源名冲突:${entry.resource} 由套件 ${previous} 与 ${item.face.manifest.id} 同时声明`);
|
|
71
|
+
}
|
|
72
|
+
owner.set(entry.resource, item.face.manifest.id);
|
|
73
|
+
prefs.push({ ...entry, unit: item.face.manifest.id });
|
|
74
|
+
}
|
|
75
|
+
return prefs;
|
|
76
|
+
}
|
|
77
|
+
function collectActions(mounted) {
|
|
78
|
+
const owner = new Map();
|
|
79
|
+
const actions = [];
|
|
80
|
+
for (const item of mounted) {
|
|
81
|
+
for (const entry of item.result.actions ?? []) {
|
|
82
|
+
const previous = owner.get(entry.resource);
|
|
83
|
+
if (previous !== undefined) {
|
|
84
|
+
throw new Error(`动作资源名冲突:${entry.resource} 由套件 ${previous} 与 ${item.face.manifest.id} 同时声明`);
|
|
85
|
+
}
|
|
86
|
+
owner.set(entry.resource, item.face.manifest.id);
|
|
87
|
+
actions.push({ ...entry, unit: item.face.manifest.id });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return actions;
|
|
91
|
+
}
|
|
92
|
+
function collectWarmReads(mounted) {
|
|
93
|
+
const declarations = mounted
|
|
94
|
+
.map((item) => item.result.warmReads)
|
|
95
|
+
.filter((declare) => declare !== undefined);
|
|
96
|
+
return () => {
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
const reads = [];
|
|
99
|
+
for (const read of declarations.flatMap((declare) => declare())) {
|
|
100
|
+
const key = read.join(' ');
|
|
101
|
+
if (seen.has(key))
|
|
102
|
+
continue;
|
|
103
|
+
seen.add(key);
|
|
104
|
+
reads.push(read);
|
|
105
|
+
}
|
|
106
|
+
return reads;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function collectContextRenderers(mounted) {
|
|
110
|
+
const table = new Map();
|
|
111
|
+
const owner = new Map();
|
|
112
|
+
for (const item of mounted) {
|
|
113
|
+
const context = item.result.context;
|
|
114
|
+
if (context === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
for (const kind of context.kinds) {
|
|
117
|
+
const previous = owner.get(kind);
|
|
118
|
+
if (previous !== undefined) {
|
|
119
|
+
throw new Error(`对话引用的 kind 冲突:${kind} 由套件 ${previous} 与 ${item.face.manifest.id} 同时认领`);
|
|
120
|
+
}
|
|
121
|
+
owner.set(kind, item.face.manifest.id);
|
|
122
|
+
table.set(kind, (ref) => context.render(ref));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return table;
|
|
126
|
+
}
|
|
127
|
+
export function mountUnits(faces, deps) {
|
|
128
|
+
const store = deps.store ?? createWorkspaceStore({ root: null });
|
|
129
|
+
const mounted = [];
|
|
130
|
+
const skipped = [];
|
|
131
|
+
const longTaskDisposers = [];
|
|
132
|
+
for (const face of faces) {
|
|
133
|
+
const readiness = unitReadiness(face.manifest, (dep) => deps.ctx.get(dep));
|
|
134
|
+
if (!readiness.canMount) {
|
|
135
|
+
skipped.push({ id: face.manifest.id, missing: readiness.missingHard });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const unit = face.manifest.id;
|
|
139
|
+
const longTasks = deps.longTasks === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: createLongTaskHost({ scope: deps.longTasks.scope, unit, bump: deps.longTasks.bump });
|
|
142
|
+
if (longTasks !== undefined)
|
|
143
|
+
longTaskDisposers.push(longTasks.dispose);
|
|
144
|
+
const result = face.mount({
|
|
145
|
+
ctx: deps.ctx,
|
|
146
|
+
logger: deps.logger,
|
|
147
|
+
manifests: deps.manifests,
|
|
148
|
+
...(deps.opencliCommand === undefined ? {} : { opencliCommand: deps.opencliCommand }),
|
|
149
|
+
...(deps.probe === undefined ? {} : { probe: deps.probe }),
|
|
150
|
+
...(deps.values === undefined ? {} : { values: deps.values }),
|
|
151
|
+
...(longTasks === undefined ? {} : { longTasks: longTasks.host }),
|
|
152
|
+
store,
|
|
153
|
+
});
|
|
154
|
+
mounted.push({ face, readiness, result: withRunsRoute(result, unit) });
|
|
155
|
+
}
|
|
156
|
+
const collected = collectRoutes(mounted);
|
|
157
|
+
const prefs = collectPrefs(mounted);
|
|
158
|
+
const actions = collectActions(mounted);
|
|
159
|
+
const owners = new Map(collected.owners);
|
|
160
|
+
for (const entry of [...prefs, ...actions])
|
|
161
|
+
owners.set(entry.resource, entry.unit);
|
|
162
|
+
const factsOf = new Map();
|
|
163
|
+
for (const item of mounted) {
|
|
164
|
+
const declare = item.result.factsOf;
|
|
165
|
+
if (declare !== undefined)
|
|
166
|
+
factsOf.set(item.face.manifest.id, declare);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
mounted,
|
|
170
|
+
routes: collected.routes,
|
|
171
|
+
owners,
|
|
172
|
+
routesOfUnit: collected.routesOfUnit,
|
|
173
|
+
factsOf,
|
|
174
|
+
gate: pickGate(mounted),
|
|
175
|
+
prefs,
|
|
176
|
+
actions,
|
|
177
|
+
provision: pickProvision(mounted),
|
|
178
|
+
contextRenderers: collectContextRenderers(mounted),
|
|
179
|
+
skipped,
|
|
180
|
+
warmReads: collectWarmReads(mounted),
|
|
181
|
+
disposeLongTasks: () => {
|
|
182
|
+
for (const dispose of longTaskDisposers.splice(0))
|
|
183
|
+
dispose();
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
package/dist/host/opencli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OPENCLI_MAX_CONCURRENT, OPENCLI_TIMEOUT_MS } from '../config/constants.js';
|
|
2
|
-
import { parseJsonLoose
|
|
2
|
+
import { parseJsonLoose } from '../lib/text.js';
|
|
3
|
+
import { quoteShellArg, tailLines } from './shell-text.js';
|
|
3
4
|
import { helpUrlOf, parseCliErrorEnvelope } from './cli-error.js';
|
|
4
5
|
import { backgroundPrefix } from './opencli-window.js';
|
|
5
6
|
import { runShell } from './shell.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一张面板卡片取数走到哪一步:`idle` 还没取、`loading` 取着、`ready` 拿到了、`error` 失败了。
|
|
3
|
+
*/
|
|
4
|
+
export type PanelCardPhase = 'idle' | 'loading' | 'ready' | 'error';
|
|
5
|
+
/**
|
|
6
|
+
* 一次取数失败:机器码 + 给人看的一句话。
|
|
7
|
+
*/
|
|
8
|
+
export interface PanelCardError {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 一张卡片最近一次取数的读数:哪个单元、哪条路由、什么相位、什么时候、失败原因。
|
|
14
|
+
*/
|
|
15
|
+
export interface PanelCardRead {
|
|
16
|
+
readonly unit: string;
|
|
17
|
+
readonly route: string;
|
|
18
|
+
readonly phase: PanelCardPhase;
|
|
19
|
+
readonly at: number;
|
|
20
|
+
readonly error?: PanelCardError;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 一张卡片崩了的读数:单元、原因、时间,以及同一原因连着崩了几次(窗口见 `PANEL_STATE_CRASH_WINDOW_MS`)。
|
|
24
|
+
*/
|
|
25
|
+
export interface PanelCardCrash {
|
|
26
|
+
readonly unit: string;
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
readonly at: number;
|
|
29
|
+
readonly count: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 一个单元此刻的读数:它的卡片、最近一次读成的时间(`okAt`,没成过给 `null`),以及崩了的那一条。
|
|
33
|
+
*/
|
|
34
|
+
export interface PanelUnitReport {
|
|
35
|
+
readonly unit: string;
|
|
36
|
+
readonly at: number;
|
|
37
|
+
readonly okAt: number | null;
|
|
38
|
+
readonly cards: readonly PanelCardRead[];
|
|
39
|
+
readonly crash?: PanelCardCrash;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 一个工作区此刻的全量读数(面板按工作区存):所有单元的读数 + 整块最近动过的时间。
|
|
43
|
+
*/
|
|
44
|
+
export interface PanelWorkspaceReport {
|
|
45
|
+
readonly workspace: string | null;
|
|
46
|
+
readonly at: number;
|
|
47
|
+
readonly units: readonly PanelUnitReport[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 客户端往上报的一批卡片读数(`POST` 请求体的形状)。
|
|
51
|
+
*/
|
|
52
|
+
export interface PanelCardsReport {
|
|
53
|
+
readonly workspace: string;
|
|
54
|
+
readonly cards: readonly PanelCardRead[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 客户端往上报的一次卡片崩溃(`POST` 请求体的形状)。
|
|
58
|
+
*/
|
|
59
|
+
export interface PanelCrashReport {
|
|
60
|
+
readonly workspace: string;
|
|
61
|
+
readonly unit: string;
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
readonly at: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 面板运行态的留存口(宿主持有一份):收卡片读数与崩溃,按工作区取快照。
|
|
67
|
+
*/
|
|
68
|
+
export interface PanelStateStore {
|
|
69
|
+
cards(report: PanelCardsReport): void;
|
|
70
|
+
crash(report: PanelCrashReport): void;
|
|
71
|
+
snapshot(workspace: string | null): PanelWorkspaceReport;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 留存的工作区上限(LRU,满 64 个丢最旧的)。
|
|
75
|
+
*/
|
|
76
|
+
export declare const PANEL_STATE_MAX_UNITS = 64;
|
|
77
|
+
/**
|
|
78
|
+
* 每个单元留存的卡片上限(按路由 LRU,满 12 张丢最旧的)。
|
|
79
|
+
*/
|
|
80
|
+
export declare const PANEL_STATE_MAX_CARDS = 12;
|
|
81
|
+
/**
|
|
82
|
+
* 一次上报最多读入的卡片条数(32 条之后的丢掉)。
|
|
83
|
+
*/
|
|
84
|
+
export declare const PANEL_STATE_MAX_REPORTS = 32;
|
|
85
|
+
/**
|
|
86
|
+
* 同一原因的连续崩溃算进同一次计数的窗口(10 分钟,毫秒)。
|
|
87
|
+
*/
|
|
88
|
+
export declare const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
89
|
+
/**
|
|
90
|
+
* 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
|
|
91
|
+
*/
|
|
92
|
+
export declare const PANEL_STATE_MAX_TEXT = 2000;
|
|
93
|
+
/**
|
|
94
|
+
* 上报解析的结果:`ok: true` 带收好的值,`ok: false` 带一句中文原因(直接回给上报方)。
|
|
95
|
+
*/
|
|
96
|
+
export type PanelParse<T> = {
|
|
97
|
+
readonly ok: true;
|
|
98
|
+
readonly value: T;
|
|
99
|
+
} | {
|
|
100
|
+
readonly ok: false;
|
|
101
|
+
readonly message: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 收一批卡片读数上报。
|
|
105
|
+
* @param payload - `POST` 的请求体。
|
|
106
|
+
* @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
|
|
107
|
+
* 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
|
|
108
|
+
*/
|
|
109
|
+
export declare function parseCardsReport(payload: unknown): PanelParse<PanelCardsReport>;
|
|
110
|
+
/**
|
|
111
|
+
* 收一次卡片崩溃上报。
|
|
112
|
+
* @param payload - `POST` 的请求体。
|
|
113
|
+
* @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
|
|
114
|
+
*/
|
|
115
|
+
export declare function parseCrashReport(payload: unknown): PanelParse<PanelCrashReport>;
|
|
116
|
+
/**
|
|
117
|
+
* 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
|
|
118
|
+
* @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
|
|
119
|
+
*/
|
|
120
|
+
export declare function createPanelStateStore(): PanelStateStore;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 留存的工作区上限(LRU,满 64 个丢最旧的)。
|
|
3
|
+
*/
|
|
4
|
+
export const PANEL_STATE_MAX_UNITS = 64;
|
|
5
|
+
/**
|
|
6
|
+
* 每个单元留存的卡片上限(按路由 LRU,满 12 张丢最旧的)。
|
|
7
|
+
*/
|
|
8
|
+
export const PANEL_STATE_MAX_CARDS = 12;
|
|
9
|
+
/**
|
|
10
|
+
* 一次上报最多读入的卡片条数(32 条之后的丢掉)。
|
|
11
|
+
*/
|
|
12
|
+
export const PANEL_STATE_MAX_REPORTS = 32;
|
|
13
|
+
/**
|
|
14
|
+
* 同一原因的连续崩溃算进同一次计数的窗口(10 分钟,毫秒)。
|
|
15
|
+
*/
|
|
16
|
+
export const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
17
|
+
/**
|
|
18
|
+
* 上报文本字段的截断长度(2000 字符;workspace / reason / message 共用)。
|
|
19
|
+
*/
|
|
20
|
+
export const PANEL_STATE_MAX_TEXT = 2000;
|
|
21
|
+
const PHASES = ['idle', 'loading', 'ready', 'error'];
|
|
22
|
+
function asRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function textOf(value, max) {
|
|
26
|
+
if (typeof value !== 'string')
|
|
27
|
+
return '';
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
30
|
+
}
|
|
31
|
+
function atOf(value) {
|
|
32
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
33
|
+
return null;
|
|
34
|
+
if (value < 0)
|
|
35
|
+
return null;
|
|
36
|
+
return Math.trunc(value);
|
|
37
|
+
}
|
|
38
|
+
function errorOf(raw) {
|
|
39
|
+
const record = asRecord(raw);
|
|
40
|
+
if (record === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const code = textOf(record['code'], 120);
|
|
43
|
+
const message = textOf(record['message'], PANEL_STATE_MAX_TEXT);
|
|
44
|
+
if (code === '' && message === '')
|
|
45
|
+
return undefined;
|
|
46
|
+
return { code: code === '' ? 'unknown' : code, message };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 收一批卡片读数上报。
|
|
50
|
+
* @param payload - `POST` 的请求体。
|
|
51
|
+
* @returns 至少收到一条能认的卡片才 `ok: true`;请求体不是对象、`workspace` 为空、`cards` 不是数组,
|
|
52
|
+
* 或一条都认不出(`unit` / `route` / `phase` / `at` 缺一)时 `ok: false` + 中文原因;认不出的单条静默跳过。
|
|
53
|
+
*/
|
|
54
|
+
export function parseCardsReport(payload) {
|
|
55
|
+
const record = asRecord(payload);
|
|
56
|
+
if (record === undefined)
|
|
57
|
+
return { ok: false, message: '请求体要是一个对象' };
|
|
58
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
59
|
+
if (workspace === '')
|
|
60
|
+
return { ok: false, message: 'workspace 不能是空的 —— 面板按工作区存' };
|
|
61
|
+
const raw = record['cards'];
|
|
62
|
+
if (!Array.isArray(raw))
|
|
63
|
+
return { ok: false, message: 'cards 要是一个数组' };
|
|
64
|
+
const cards = [];
|
|
65
|
+
for (const item of raw.slice(0, PANEL_STATE_MAX_REPORTS)) {
|
|
66
|
+
const card = asRecord(item);
|
|
67
|
+
if (card === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
const unit = textOf(card['unit'], 200);
|
|
70
|
+
const route = textOf(card['route'], 400);
|
|
71
|
+
const phase = textOf(card['phase'], 20);
|
|
72
|
+
const at = atOf(card['at']);
|
|
73
|
+
if (unit === '' || route === '' || at === null || !PHASES.includes(phase))
|
|
74
|
+
continue;
|
|
75
|
+
const error = errorOf(card['error']);
|
|
76
|
+
cards.push({ unit, route, phase, at, ...(error === undefined ? {} : { error }) });
|
|
77
|
+
}
|
|
78
|
+
if (cards.length === 0)
|
|
79
|
+
return { ok: false, message: 'cards 里没有一条能认的(unit / route / phase / at 都要有)' };
|
|
80
|
+
return { ok: true, value: { workspace, cards } };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 收一次卡片崩溃上报。
|
|
84
|
+
* @param payload - `POST` 的请求体。
|
|
85
|
+
* @returns `workspace` / `unit` / `reason` 都不能空,缺一给 `ok: false` + 中文原因;`at` 缺省或不合法时取当前时间。
|
|
86
|
+
*/
|
|
87
|
+
export function parseCrashReport(payload) {
|
|
88
|
+
const record = asRecord(payload);
|
|
89
|
+
if (record === undefined)
|
|
90
|
+
return { ok: false, message: '请求体要是一个对象' };
|
|
91
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
92
|
+
const unit = textOf(record['unit'], 200);
|
|
93
|
+
const reason = textOf(record['reason'], PANEL_STATE_MAX_TEXT);
|
|
94
|
+
if (workspace === '')
|
|
95
|
+
return { ok: false, message: 'workspace 不能是空的' };
|
|
96
|
+
if (unit === '')
|
|
97
|
+
return { ok: false, message: 'unit 不能是空的' };
|
|
98
|
+
if (reason === '')
|
|
99
|
+
return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
|
|
100
|
+
return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? Date.now() } };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 造一份面板运行态的留存(进程内存、按工作区分桶,带上面那几条上限的 LRU)。
|
|
104
|
+
* @returns 留存口:`cards` / `crash` 收上报,`snapshot(workspace)` 取全量;没存过的工作区(含 `null`)给一份空的。
|
|
105
|
+
*/
|
|
106
|
+
export function createPanelStateStore() {
|
|
107
|
+
const workspaces = new Map();
|
|
108
|
+
function trim(entry, max) {
|
|
109
|
+
while (entry.size > max) {
|
|
110
|
+
const oldest = entry.keys().next();
|
|
111
|
+
if (oldest.done === true)
|
|
112
|
+
return;
|
|
113
|
+
entry.delete(oldest.value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function workspaceEntryOf(workspace) {
|
|
117
|
+
const existing = workspaces.get(workspace);
|
|
118
|
+
if (existing !== undefined)
|
|
119
|
+
return existing;
|
|
120
|
+
const created = { at: 0, units: new Map() };
|
|
121
|
+
workspaces.set(workspace, created);
|
|
122
|
+
trim(workspaces, PANEL_STATE_MAX_UNITS);
|
|
123
|
+
return created;
|
|
124
|
+
}
|
|
125
|
+
function unitEntryOf(entry, unit) {
|
|
126
|
+
const existing = entry.units.get(unit);
|
|
127
|
+
if (existing !== undefined)
|
|
128
|
+
return existing;
|
|
129
|
+
const created = { at: 0, cards: new Map() };
|
|
130
|
+
entry.units.set(unit, created);
|
|
131
|
+
return created;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
cards(report) {
|
|
135
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
136
|
+
for (const card of report.cards) {
|
|
137
|
+
const unit = unitEntryOf(entry, card.unit);
|
|
138
|
+
unit.cards.delete(card.route);
|
|
139
|
+
unit.cards.set(card.route, card);
|
|
140
|
+
trim(unit.cards, PANEL_STATE_MAX_CARDS);
|
|
141
|
+
if (card.at > unit.at)
|
|
142
|
+
unit.at = card.at;
|
|
143
|
+
if (card.at > entry.at)
|
|
144
|
+
entry.at = card.at;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
crash(report) {
|
|
148
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
149
|
+
const unit = unitEntryOf(entry, report.unit);
|
|
150
|
+
const previous = unit.crash;
|
|
151
|
+
const counted = previous !== undefined &&
|
|
152
|
+
previous.reason === report.reason &&
|
|
153
|
+
report.at >= previous.at &&
|
|
154
|
+
report.at - previous.at <= PANEL_STATE_CRASH_WINDOW_MS;
|
|
155
|
+
unit.crash = { unit: report.unit, reason: report.reason, at: report.at, count: counted ? previous.count + 1 : 1 };
|
|
156
|
+
if (report.at > unit.at)
|
|
157
|
+
unit.at = report.at;
|
|
158
|
+
if (report.at > entry.at)
|
|
159
|
+
entry.at = report.at;
|
|
160
|
+
},
|
|
161
|
+
snapshot(workspace) {
|
|
162
|
+
if (workspace === null)
|
|
163
|
+
return { workspace: null, at: 0, units: [] };
|
|
164
|
+
const entry = workspaces.get(workspace);
|
|
165
|
+
if (entry === undefined)
|
|
166
|
+
return { workspace, at: 0, units: [] };
|
|
167
|
+
const units = [...entry.units.entries()].map(([unit, one]) => {
|
|
168
|
+
const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
|
|
169
|
+
const okAt = cards.reduce((best, card) => (card.phase === 'ready' && (best === null || card.at > best) ? card.at : best), null);
|
|
170
|
+
return { unit, at: one.at, okAt, cards, ...(one.crash === undefined ? {} : { crash: one.crash }) };
|
|
171
|
+
});
|
|
172
|
+
units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
|
|
173
|
+
return { workspace, at: entry.at, units };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|