@dsh-workbench/shared 0.2.0 → 0.2.2
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/unit.d.ts +1 -0
- package/dist/api/unit.js +3 -1
- package/dist/components/CardBoundary.d.ts +1 -0
- package/dist/components/CardBoundary.js +3 -1
- package/dist/config/constants.d.ts +4 -0
- package/dist/config/constants.js +7 -0
- package/dist/host/http.d.ts +2 -0
- package/dist/host/http.js +31 -1
- package/dist/host/tool-approval.d.ts +5 -0
- package/dist/host/tool-approval.js +47 -9
- package/dist/host/unit-face.d.ts +1 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/i18n/en.js +21 -13
- package/dist/i18n/index.d.ts +22 -14
- package/dist/i18n/zh.d.ts +21 -13
- package/dist/i18n/zh.js +21 -13
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/panel-state.d.ts +60 -0
- package/dist/lib/panel-state.js +146 -0
- package/package.json +1 -1
- package/src/api/__tests__/unit.test.ts +38 -0
- package/src/api/unit.ts +4 -0
- package/src/components/CardBoundary.tsx +4 -1
- package/src/config/constants.ts +11 -0
- package/src/host/http.ts +30 -1
- package/src/host/tool-approval.ts +54 -8
- package/src/host/unit-face.ts +1 -0
- package/src/host.ts +1 -0
- package/src/i18n/en.ts +21 -13
- package/src/i18n/zh.ts +21 -13
- package/src/index.ts +1 -0
- package/src/lib/panel-state.test.ts +123 -0
- package/src/lib/panel-state.ts +209 -0
package/src/i18n/zh.ts
CHANGED
|
@@ -8,6 +8,8 @@ export const zh = {
|
|
|
8
8
|
'project.none': '未选择项目',
|
|
9
9
|
'project.empty': '还没有项目。用下面的「添加项目」选一个目录。',
|
|
10
10
|
'project.add': '添加项目…',
|
|
11
|
+
'project.add.busy': '正在新建工作区…',
|
|
12
|
+
'project.add.failed': '新建工作区没成功',
|
|
11
13
|
'project.search.label': '搜索工作区',
|
|
12
14
|
'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
|
|
13
15
|
'project.search.empty': '没有匹配「{query}」的工作区。',
|
|
@@ -43,18 +45,22 @@ export const zh = {
|
|
|
43
45
|
'time.days': '{n} 天前',
|
|
44
46
|
|
|
45
47
|
'panels.title': '面板模块',
|
|
46
|
-
'panels.
|
|
47
|
-
'panels.
|
|
48
|
-
'panels.market.
|
|
49
|
-
'panels.
|
|
50
|
-
'panels.
|
|
48
|
+
'panels.groupBoardless': '无看板模块',
|
|
49
|
+
'panels.groupPlugins': 'opencli 插件',
|
|
50
|
+
'panels.market.defaultHint': '打开后所有工作区默认开启,也可在工作区面板配置中单独关闭',
|
|
51
|
+
'panels.market.defaultLabel': '全局默认开启',
|
|
52
|
+
'panels.market.defaultLocked': '这份现在用不了,先更新或卸载',
|
|
51
53
|
'panels.followAll': '全部恢复默认',
|
|
54
|
+
'panels.followAllConfirmTitle': '全部恢复默认?',
|
|
55
|
+
'panels.followAllConfirmDetail': '这个工作区里改过的每一行都会清掉,之后都跟随全局。',
|
|
56
|
+
'panels.followAllConfirmAck': '我明白:清掉之后,想再按这个工作区配,得一行一行重新改。',
|
|
57
|
+
'panels.followAllConfirmYes': '恢复默认',
|
|
58
|
+
'panels.followAllConfirmNo': '取消',
|
|
52
59
|
'panels.enabled': '这个项目已启用',
|
|
53
60
|
'panels.disabled': '这个项目未启用',
|
|
54
61
|
'panels.choose': '选择模块',
|
|
55
62
|
'panels.buildModule': '造一个模块',
|
|
56
63
|
'panels.failed': '改配置失败了,再试一次。',
|
|
57
|
-
'panels.hintEffect': '停用 = agent 拿不到相关数据',
|
|
58
64
|
'panels.dragHint': '拖动调整顺序',
|
|
59
65
|
'panels.unknownStep': '这一层没有套件提供',
|
|
60
66
|
'panels.labelWithHint': '{label}({hint})',
|
|
@@ -67,9 +73,10 @@ export const zh = {
|
|
|
67
73
|
'panels.browseMarket': '浏览模块市场',
|
|
68
74
|
'panels.noneInstalled': '这些模块要的插件都还没装',
|
|
69
75
|
'panels.noneInstalledHint': '装好之后它们会回到这里,再由你决定放哪几个上板。',
|
|
70
|
-
'panels.rowPlugin': '插件',
|
|
71
76
|
'panels.market.installed': '已安装',
|
|
72
|
-
'panels.market.
|
|
77
|
+
'panels.market.notActive': '未生效',
|
|
78
|
+
'panels.market.notUsable': '不可用',
|
|
79
|
+
'panels.market.broken': '装坏了',
|
|
73
80
|
'panels.market.hasNew': '有新版本',
|
|
74
81
|
'panels.market.missing': '未装',
|
|
75
82
|
'panels.market.unchecked': '状态未知',
|
|
@@ -99,16 +106,17 @@ export const zh = {
|
|
|
99
106
|
'panels.market.confirmNo': '取消',
|
|
100
107
|
'panels.market.failed': '没成功:{reason}',
|
|
101
108
|
'panels.market.needEnv': '先在启动工作台的终端里设 {env}',
|
|
109
|
+
'panels.market.needCommands': '缺这几条命令:{commands}',
|
|
110
|
+
'panels.market.localMark': '本地',
|
|
111
|
+
'panels.market.localHint': '本机那份(不从源上取)。',
|
|
102
112
|
'panels.market.noSource': '这个模块没声明怎么装,只能自己动手',
|
|
103
113
|
'panels.market.badSource': '安装方式没写全,只能自己动手',
|
|
104
114
|
'panels.market.empty': '暂无可用模块',
|
|
105
|
-
'panels.market.sectionLocal': '本地模块',
|
|
106
|
-
'panels.market.sectionUnlisted': '不是从市场装的',
|
|
107
115
|
'panels.market.sectionPlugins': '已安装 opencli 插件',
|
|
108
116
|
'panels.market.moduleOutdated': '有新版',
|
|
109
|
-
'panels.market.moduleRefused': '
|
|
117
|
+
'panels.market.moduleRefused': '模块版本与工作台不匹配,暂时无法使用',
|
|
110
118
|
'panels.market.moduleRefusedDetail': '装不上的原因:{reason}',
|
|
111
|
-
'panels.market.moduleRestartHint': '
|
|
119
|
+
'panels.market.moduleRestartHint': '重启后生效',
|
|
112
120
|
'panels.market.workbenchName': '工作台',
|
|
113
121
|
'panels.market.workbenchLocal': '这份是本仓正在开发的那一份,不从源上取。',
|
|
114
122
|
'panels.market.pluginKeptInUse': '文件先留着:{names} 还要用它。',
|
|
@@ -202,7 +210,7 @@ export const zh = {
|
|
|
202
210
|
'panels.market.add.registryPnpm': 'pnpm 自己配的源',
|
|
203
211
|
'panels.market.add.check': '预检',
|
|
204
212
|
'panels.market.add.checking': '正在预检…',
|
|
205
|
-
'panels.market.add.classifying': '
|
|
213
|
+
'panels.market.add.classifying': '检测中…',
|
|
206
214
|
'panels.market.add.change': '改一下',
|
|
207
215
|
'panels.market.add.previewTitle': '将要安装',
|
|
208
216
|
'panels.market.add.fieldName': '包名',
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export * from './i18n/merge.js';
|
|
|
40
40
|
export * from './i18n/zh.js';
|
|
41
41
|
export * from './lib/blocker-action.js';
|
|
42
42
|
export * from './lib/busy.js';
|
|
43
|
+
export * from './lib/panel-state.js';
|
|
43
44
|
export * from './lib/react.js';
|
|
44
45
|
export * from './lib/text.js';
|
|
45
46
|
export * from './lib/ui.js';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
PANEL_STATE_CRASH_WINDOW_MS,
|
|
4
|
+
PANEL_STATE_MAX_CARDS,
|
|
5
|
+
createPanelStateStore,
|
|
6
|
+
parseCardsReport,
|
|
7
|
+
parseCrashReport,
|
|
8
|
+
} from './panel-state';
|
|
9
|
+
|
|
10
|
+
function card(unit: string, route: string, phase: 'idle' | 'loading' | 'ready' | 'error', at: number) {
|
|
11
|
+
return { unit, route, phase, at };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('parseCardsReport', () => {
|
|
15
|
+
it('收下一条能认的上报', () => {
|
|
16
|
+
const parsed = parseCardsReport({
|
|
17
|
+
workspace: '/w/a',
|
|
18
|
+
cards: [{ unit: 'demo', route: 'demo/overview', phase: 'ready', at: 10 }],
|
|
19
|
+
});
|
|
20
|
+
expect(parsed.ok).toBe(true);
|
|
21
|
+
if (!parsed.ok) return;
|
|
22
|
+
expect(parsed.value.cards).toEqual([{ unit: 'demo', route: 'demo/overview', phase: 'ready', at: 10 }]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('不认的字段直接丢掉,认不出任何一条就拒收(不当成"面板是空的")', () => {
|
|
26
|
+
const parsed = parseCardsReport({
|
|
27
|
+
workspace: '/w/a',
|
|
28
|
+
cards: [{ unit: 'demo', route: '', phase: 'ready', at: 1 }, { unit: 'demo', phase: 'weird' }],
|
|
29
|
+
});
|
|
30
|
+
expect(parsed.ok).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('没有 workspace 就拒收(面板运行态是按工作区存的)', () => {
|
|
34
|
+
expect(parseCardsReport({ cards: [card('demo', 'demo/o', 'ready', 1)] }).ok).toBe(false);
|
|
35
|
+
expect(parseCardsReport({ workspace: ' ', cards: [] }).ok).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('错误码与正文都留着,空错误对象不算错误', () => {
|
|
39
|
+
const parsed = parseCardsReport({
|
|
40
|
+
workspace: '/w/a',
|
|
41
|
+
cards: [
|
|
42
|
+
{ unit: 'demo', route: 'demo/o', phase: 'error', at: 3, error: { code: 'host-500', message: '宿主炸了' } },
|
|
43
|
+
{ unit: 'demo', route: 'demo/p', phase: 'ready', at: 4, error: {} },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
expect(parsed.ok).toBe(true);
|
|
47
|
+
if (!parsed.ok) return;
|
|
48
|
+
expect(parsed.value.cards[0]?.error).toEqual({ code: 'host-500', message: '宿主炸了' });
|
|
49
|
+
expect(parsed.value.cards[1]?.error).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('parseCrashReport', () => {
|
|
54
|
+
it('没有原因的崩溃不收(收了也查不了)', () => {
|
|
55
|
+
expect(parseCrashReport({ workspace: '/w/a', unit: 'demo', reason: ' ' }).ok).toBe(false);
|
|
56
|
+
expect(parseCrashReport({ workspace: '/w/a', reason: 'boom' }).ok).toBe(false);
|
|
57
|
+
expect(parseCrashReport({ unit: 'demo', reason: 'boom' }).ok).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('没给时间就按收到的那一刻算', () => {
|
|
61
|
+
const parsed = parseCrashReport({ workspace: '/w/a', unit: 'demo', reason: 'boom' });
|
|
62
|
+
expect(parsed.ok).toBe(true);
|
|
63
|
+
if (!parsed.ok) return;
|
|
64
|
+
expect(parsed.value.at).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('createPanelStateStore', () => {
|
|
69
|
+
it('按工作区 + unit + route 记最新一条(同一 route 后报的盖掉先报的)', () => {
|
|
70
|
+
const store = createPanelStateStore();
|
|
71
|
+
store.cards({ workspace: '/w/a', cards: [card('demo', 'demo/o', 'loading', 1)] });
|
|
72
|
+
store.cards({ workspace: '/w/a', cards: [card('demo', 'demo/o', 'ready', 2)] });
|
|
73
|
+
const snapshot = store.snapshot('/w/a');
|
|
74
|
+
expect(snapshot.units).toHaveLength(1);
|
|
75
|
+
expect(snapshot.units[0]?.cards).toEqual([{ unit: 'demo', route: 'demo/o', phase: 'ready', at: 2 }]);
|
|
76
|
+
expect(snapshot.units[0]?.okAt).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('别的工作区的上报不出来(面板按工作区分家)', () => {
|
|
80
|
+
const store = createPanelStateStore();
|
|
81
|
+
store.cards({ workspace: '/w/a', cards: [card('demo', 'demo/o', 'ready', 5)] });
|
|
82
|
+
expect(store.snapshot('/w/b').units).toEqual([]);
|
|
83
|
+
expect(store.snapshot(null)).toEqual({ workspace: null, at: 0, units: [] });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('没读到过数据时 okAt 是 null("读到过"不能靠 unit 有上报就算)', () => {
|
|
87
|
+
const store = createPanelStateStore();
|
|
88
|
+
store.cards({ workspace: '/w/a', cards: [card('demo', 'demo/o', 'error', 7)] });
|
|
89
|
+
expect(store.snapshot('/w/a').units[0]?.okAt).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('同一个 unit 反复崩只留最新一条 + 次数(不刷屏)', () => {
|
|
93
|
+
const store = createPanelStateStore();
|
|
94
|
+
store.crash({ workspace: '/w/a', unit: 'demo', reason: 'boom', at: 100 });
|
|
95
|
+
store.crash({ workspace: '/w/a', unit: 'demo', reason: 'boom', at: 200 });
|
|
96
|
+
const crash = store.snapshot('/w/a').units[0]?.crash;
|
|
97
|
+
expect(crash).toEqual({ unit: 'demo', reason: 'boom', at: 200, count: 2 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('换了原因、或隔太久,就重新数', () => {
|
|
101
|
+
const store = createPanelStateStore();
|
|
102
|
+
store.crash({ workspace: '/w/a', unit: 'demo', reason: 'boom', at: 100 });
|
|
103
|
+
store.crash({ workspace: '/w/a', unit: 'demo', reason: 'other', at: 200 });
|
|
104
|
+
expect(store.snapshot('/w/a').units[0]?.crash?.count).toBe(1);
|
|
105
|
+
store.crash({ workspace: '/w/a', unit: 'demo', reason: 'other', at: 200 + PANEL_STATE_CRASH_WINDOW_MS + 1 });
|
|
106
|
+
expect(store.snapshot('/w/a').units[0]?.crash?.count).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('一个 unit 的卡最多留这么多条(长命的页面不会把它撑成日志)', () => {
|
|
110
|
+
const store = createPanelStateStore();
|
|
111
|
+
for (let index = 0; index < PANEL_STATE_MAX_CARDS + 4; index += 1) {
|
|
112
|
+
store.cards({ workspace: '/w/a', cards: [card('demo', `demo/r${String(index)}`, 'ready', index + 1)] });
|
|
113
|
+
}
|
|
114
|
+
expect(store.snapshot('/w/a').units[0]?.cards.length).toBe(PANEL_STATE_MAX_CARDS);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('两份 store 各记各的(工厂不是单例)', () => {
|
|
118
|
+
const one = createPanelStateStore();
|
|
119
|
+
const two = createPanelStateStore();
|
|
120
|
+
one.cards({ workspace: '/w/a', cards: [card('demo', 'demo/o', 'ready', 1)] });
|
|
121
|
+
expect(two.snapshot('/w/a').units).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
export type PanelCardPhase = 'idle' | 'loading' | 'ready' | 'error';
|
|
2
|
+
|
|
3
|
+
export interface PanelCardError {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PanelCardRead {
|
|
9
|
+
readonly unit: string;
|
|
10
|
+
readonly route: string;
|
|
11
|
+
readonly phase: PanelCardPhase;
|
|
12
|
+
readonly at: number;
|
|
13
|
+
readonly error?: PanelCardError;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PanelCardCrash {
|
|
17
|
+
readonly unit: string;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
readonly at: number;
|
|
20
|
+
readonly count: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PanelUnitReport {
|
|
24
|
+
readonly unit: string;
|
|
25
|
+
readonly at: number;
|
|
26
|
+
readonly okAt: number | null;
|
|
27
|
+
readonly cards: readonly PanelCardRead[];
|
|
28
|
+
readonly crash?: PanelCardCrash;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PanelWorkspaceReport {
|
|
32
|
+
readonly workspace: string | null;
|
|
33
|
+
readonly at: number;
|
|
34
|
+
readonly units: readonly PanelUnitReport[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PanelCardsReport {
|
|
38
|
+
readonly workspace: string;
|
|
39
|
+
readonly cards: readonly PanelCardRead[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface PanelCrashReport {
|
|
43
|
+
readonly workspace: string;
|
|
44
|
+
readonly unit: string;
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
readonly at: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PanelStateStore {
|
|
50
|
+
cards(report: PanelCardsReport): void;
|
|
51
|
+
crash(report: PanelCrashReport): void;
|
|
52
|
+
snapshot(workspace: string | null): PanelWorkspaceReport;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const PANEL_STATE_MAX_UNITS = 64;
|
|
56
|
+
export const PANEL_STATE_MAX_CARDS = 12;
|
|
57
|
+
export const PANEL_STATE_MAX_REPORTS = 32;
|
|
58
|
+
export const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
59
|
+
export const PANEL_STATE_MAX_TEXT = 2000;
|
|
60
|
+
|
|
61
|
+
const PHASES: readonly PanelCardPhase[] = ['idle', 'loading', 'ready', 'error'];
|
|
62
|
+
|
|
63
|
+
interface UnitEntry {
|
|
64
|
+
at: number;
|
|
65
|
+
cards: Map<string, PanelCardRead>;
|
|
66
|
+
crash?: PanelCardCrash;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface WorkspaceEntry {
|
|
70
|
+
at: number;
|
|
71
|
+
units: Map<string, UnitEntry>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PanelParse<T> =
|
|
75
|
+
| { readonly ok: true; readonly value: T }
|
|
76
|
+
| { readonly ok: false; readonly message: string };
|
|
77
|
+
|
|
78
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
79
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function textOf(value: unknown, max: number): string {
|
|
83
|
+
if (typeof value !== 'string') return '';
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function atOf(value: unknown): number | null {
|
|
89
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
|
90
|
+
if (value < 0) return null;
|
|
91
|
+
return Math.trunc(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function errorOf(raw: unknown): PanelCardError | undefined {
|
|
95
|
+
const record = asRecord(raw);
|
|
96
|
+
if (record === undefined) return undefined;
|
|
97
|
+
const code = textOf(record['code'], 120);
|
|
98
|
+
const message = textOf(record['message'], PANEL_STATE_MAX_TEXT);
|
|
99
|
+
if (code === '' && message === '') return undefined;
|
|
100
|
+
return { code: code === '' ? 'unknown' : code, message };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseCardsReport(payload: unknown): PanelParse<PanelCardsReport> {
|
|
104
|
+
const record = asRecord(payload);
|
|
105
|
+
if (record === undefined) return { ok: false, message: '请求体要是一个对象' };
|
|
106
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
107
|
+
if (workspace === '') return { ok: false, message: 'workspace 不能是空的 —— 面板按工作区存' };
|
|
108
|
+
const raw = record['cards'];
|
|
109
|
+
if (!Array.isArray(raw)) return { ok: false, message: 'cards 要是一个数组' };
|
|
110
|
+
const cards: PanelCardRead[] = [];
|
|
111
|
+
for (const item of raw.slice(0, PANEL_STATE_MAX_REPORTS)) {
|
|
112
|
+
const card = asRecord(item);
|
|
113
|
+
if (card === undefined) continue;
|
|
114
|
+
const unit = textOf(card['unit'], 200);
|
|
115
|
+
const route = textOf(card['route'], 400);
|
|
116
|
+
const phase = textOf(card['phase'], 20) as PanelCardPhase;
|
|
117
|
+
const at = atOf(card['at']);
|
|
118
|
+
if (unit === '' || route === '' || at === null || !PHASES.includes(phase)) continue;
|
|
119
|
+
const error = errorOf(card['error']);
|
|
120
|
+
cards.push({ unit, route, phase, at, ...(error === undefined ? {} : { error }) });
|
|
121
|
+
}
|
|
122
|
+
if (cards.length === 0) return { ok: false, message: 'cards 里没有一条能认的(unit / route / phase / at 都要有)' };
|
|
123
|
+
return { ok: true, value: { workspace, cards } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function parseCrashReport(payload: unknown): PanelParse<PanelCrashReport> {
|
|
127
|
+
const record = asRecord(payload);
|
|
128
|
+
if (record === undefined) return { ok: false, message: '请求体要是一个对象' };
|
|
129
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
130
|
+
const unit = textOf(record['unit'], 200);
|
|
131
|
+
const reason = textOf(record['reason'], PANEL_STATE_MAX_TEXT);
|
|
132
|
+
if (workspace === '') return { ok: false, message: 'workspace 不能是空的' };
|
|
133
|
+
if (unit === '') return { ok: false, message: 'unit 不能是空的' };
|
|
134
|
+
if (reason === '') return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
|
|
135
|
+
return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? Date.now() } };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createPanelStateStore(): PanelStateStore {
|
|
139
|
+
const workspaces = new Map<string, WorkspaceEntry>();
|
|
140
|
+
|
|
141
|
+
function trim(entry: Map<string, unknown>, max: number): void {
|
|
142
|
+
while (entry.size > max) {
|
|
143
|
+
const oldest = entry.keys().next();
|
|
144
|
+
if (oldest.done === true) return;
|
|
145
|
+
entry.delete(oldest.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function workspaceEntryOf(workspace: string): WorkspaceEntry {
|
|
150
|
+
const existing = workspaces.get(workspace);
|
|
151
|
+
if (existing !== undefined) return existing;
|
|
152
|
+
const created: WorkspaceEntry = { at: 0, units: new Map() };
|
|
153
|
+
workspaces.set(workspace, created);
|
|
154
|
+
trim(workspaces, PANEL_STATE_MAX_UNITS);
|
|
155
|
+
return created;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unitEntryOf(entry: WorkspaceEntry, unit: string): UnitEntry {
|
|
159
|
+
const existing = entry.units.get(unit);
|
|
160
|
+
if (existing !== undefined) return existing;
|
|
161
|
+
const created: UnitEntry = { at: 0, cards: new Map() };
|
|
162
|
+
entry.units.set(unit, created);
|
|
163
|
+
return created;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
cards(report) {
|
|
168
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
169
|
+
for (const card of report.cards) {
|
|
170
|
+
const unit = unitEntryOf(entry, card.unit);
|
|
171
|
+
unit.cards.delete(card.route);
|
|
172
|
+
unit.cards.set(card.route, card);
|
|
173
|
+
trim(unit.cards, PANEL_STATE_MAX_CARDS);
|
|
174
|
+
if (card.at > unit.at) unit.at = card.at;
|
|
175
|
+
if (card.at > entry.at) entry.at = card.at;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
crash(report) {
|
|
180
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
181
|
+
const unit = unitEntryOf(entry, report.unit);
|
|
182
|
+
const previous = unit.crash;
|
|
183
|
+
const counted =
|
|
184
|
+
previous !== undefined &&
|
|
185
|
+
previous.reason === report.reason &&
|
|
186
|
+
report.at >= previous.at &&
|
|
187
|
+
report.at - previous.at <= PANEL_STATE_CRASH_WINDOW_MS;
|
|
188
|
+
unit.crash = { unit: report.unit, reason: report.reason, at: report.at, count: counted ? previous.count + 1 : 1 };
|
|
189
|
+
if (report.at > unit.at) unit.at = report.at;
|
|
190
|
+
if (report.at > entry.at) entry.at = report.at;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
snapshot(workspace) {
|
|
194
|
+
if (workspace === null) return { workspace: null, at: 0, units: [] };
|
|
195
|
+
const entry = workspaces.get(workspace);
|
|
196
|
+
if (entry === undefined) return { workspace, at: 0, units: [] };
|
|
197
|
+
const units = [...entry.units.entries()].map(([unit, one]): PanelUnitReport => {
|
|
198
|
+
const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
|
|
199
|
+
const okAt = cards.reduce<number | null>(
|
|
200
|
+
(best, card) => (card.phase === 'ready' && (best === null || card.at > best) ? card.at : best),
|
|
201
|
+
null,
|
|
202
|
+
);
|
|
203
|
+
return { unit, at: one.at, okAt, cards, ...(one.crash === undefined ? {} : { crash: one.crash }) };
|
|
204
|
+
});
|
|
205
|
+
units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
|
|
206
|
+
return { workspace, at: entry.at, units };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|