@dsh-workbench/shared 0.2.0 → 0.2.1
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 +3 -0
- package/dist/config/constants.js +3 -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 +6 -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/dist/i18n/zh.js
CHANGED
|
@@ -7,6 +7,8 @@ export const zh = {
|
|
|
7
7
|
'project.none': '未选择项目',
|
|
8
8
|
'project.empty': '还没有项目。用下面的「添加项目」选一个目录。',
|
|
9
9
|
'project.add': '添加项目…',
|
|
10
|
+
'project.add.busy': '正在新建工作区…',
|
|
11
|
+
'project.add.failed': '新建工作区没成功',
|
|
10
12
|
'project.search.label': '搜索工作区',
|
|
11
13
|
'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
|
|
12
14
|
'project.search.empty': '没有匹配「{query}」的工作区。',
|
|
@@ -40,18 +42,22 @@ export const zh = {
|
|
|
40
42
|
'time.hours': '{n} 小时前',
|
|
41
43
|
'time.days': '{n} 天前',
|
|
42
44
|
'panels.title': '面板模块',
|
|
43
|
-
'panels.
|
|
44
|
-
'panels.
|
|
45
|
-
'panels.market.
|
|
46
|
-
'panels.
|
|
47
|
-
'panels.
|
|
45
|
+
'panels.groupBoardless': '无看板模块',
|
|
46
|
+
'panels.groupPlugins': 'opencli 插件',
|
|
47
|
+
'panels.market.defaultHint': '打开后所有工作区默认开启,也可在工作区面板配置中单独关闭',
|
|
48
|
+
'panels.market.defaultLabel': '全局默认开启',
|
|
49
|
+
'panels.market.defaultLocked': '这份现在用不了,先更新或卸载',
|
|
48
50
|
'panels.followAll': '全部恢复默认',
|
|
51
|
+
'panels.followAllConfirmTitle': '全部恢复默认?',
|
|
52
|
+
'panels.followAllConfirmDetail': '这个工作区里改过的每一行都会清掉,之后都跟随全局。',
|
|
53
|
+
'panels.followAllConfirmAck': '我明白:清掉之后,想再按这个工作区配,得一行一行重新改。',
|
|
54
|
+
'panels.followAllConfirmYes': '恢复默认',
|
|
55
|
+
'panels.followAllConfirmNo': '取消',
|
|
49
56
|
'panels.enabled': '这个项目已启用',
|
|
50
57
|
'panels.disabled': '这个项目未启用',
|
|
51
58
|
'panels.choose': '选择模块',
|
|
52
59
|
'panels.buildModule': '造一个模块',
|
|
53
60
|
'panels.failed': '改配置失败了,再试一次。',
|
|
54
|
-
'panels.hintEffect': '停用 = agent 拿不到相关数据',
|
|
55
61
|
'panels.dragHint': '拖动调整顺序',
|
|
56
62
|
'panels.unknownStep': '这一层没有套件提供',
|
|
57
63
|
'panels.labelWithHint': '{label}({hint})',
|
|
@@ -64,9 +70,10 @@ export const zh = {
|
|
|
64
70
|
'panels.browseMarket': '浏览模块市场',
|
|
65
71
|
'panels.noneInstalled': '这些模块要的插件都还没装',
|
|
66
72
|
'panels.noneInstalledHint': '装好之后它们会回到这里,再由你决定放哪几个上板。',
|
|
67
|
-
'panels.rowPlugin': '插件',
|
|
68
73
|
'panels.market.installed': '已安装',
|
|
69
|
-
'panels.market.
|
|
74
|
+
'panels.market.notActive': '未生效',
|
|
75
|
+
'panels.market.notUsable': '不可用',
|
|
76
|
+
'panels.market.broken': '装坏了',
|
|
70
77
|
'panels.market.hasNew': '有新版本',
|
|
71
78
|
'panels.market.missing': '未装',
|
|
72
79
|
'panels.market.unchecked': '状态未知',
|
|
@@ -96,16 +103,17 @@ export const zh = {
|
|
|
96
103
|
'panels.market.confirmNo': '取消',
|
|
97
104
|
'panels.market.failed': '没成功:{reason}',
|
|
98
105
|
'panels.market.needEnv': '先在启动工作台的终端里设 {env}',
|
|
106
|
+
'panels.market.needCommands': '缺这几条命令:{commands}',
|
|
107
|
+
'panels.market.localMark': '本地',
|
|
108
|
+
'panels.market.localHint': '本机那份(不从源上取)。',
|
|
99
109
|
'panels.market.noSource': '这个模块没声明怎么装,只能自己动手',
|
|
100
110
|
'panels.market.badSource': '安装方式没写全,只能自己动手',
|
|
101
111
|
'panels.market.empty': '暂无可用模块',
|
|
102
|
-
'panels.market.sectionLocal': '本地模块',
|
|
103
|
-
'panels.market.sectionUnlisted': '不是从市场装的',
|
|
104
112
|
'panels.market.sectionPlugins': '已安装 opencli 插件',
|
|
105
113
|
'panels.market.moduleOutdated': '有新版',
|
|
106
|
-
'panels.market.moduleRefused': '
|
|
114
|
+
'panels.market.moduleRefused': '模块版本与工作台不匹配,暂时无法使用',
|
|
107
115
|
'panels.market.moduleRefusedDetail': '装不上的原因:{reason}',
|
|
108
|
-
'panels.market.moduleRestartHint': '
|
|
116
|
+
'panels.market.moduleRestartHint': '重启后生效',
|
|
109
117
|
'panels.market.workbenchName': '工作台',
|
|
110
118
|
'panels.market.workbenchLocal': '这份是本仓正在开发的那一份,不从源上取。',
|
|
111
119
|
'panels.market.pluginKeptInUse': '文件先留着:{names} 还要用它。',
|
|
@@ -197,7 +205,7 @@ export const zh = {
|
|
|
197
205
|
'panels.market.add.registryPnpm': 'pnpm 自己配的源',
|
|
198
206
|
'panels.market.add.check': '预检',
|
|
199
207
|
'panels.market.add.checking': '正在预检…',
|
|
200
|
-
'panels.market.add.classifying': '
|
|
208
|
+
'panels.market.add.classifying': '检测中…',
|
|
201
209
|
'panels.market.add.change': '改一下',
|
|
202
210
|
'panels.market.add.previewTitle': '将要安装',
|
|
203
211
|
'panels.market.add.fieldName': '包名',
|
package/dist/index.d.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';
|
package/dist/index.js
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,60 @@
|
|
|
1
|
+
export type PanelCardPhase = 'idle' | 'loading' | 'ready' | 'error';
|
|
2
|
+
export interface PanelCardError {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PanelCardRead {
|
|
7
|
+
readonly unit: string;
|
|
8
|
+
readonly route: string;
|
|
9
|
+
readonly phase: PanelCardPhase;
|
|
10
|
+
readonly at: number;
|
|
11
|
+
readonly error?: PanelCardError;
|
|
12
|
+
}
|
|
13
|
+
export interface PanelCardCrash {
|
|
14
|
+
readonly unit: string;
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
readonly at: number;
|
|
17
|
+
readonly count: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PanelUnitReport {
|
|
20
|
+
readonly unit: string;
|
|
21
|
+
readonly at: number;
|
|
22
|
+
readonly okAt: number | null;
|
|
23
|
+
readonly cards: readonly PanelCardRead[];
|
|
24
|
+
readonly crash?: PanelCardCrash;
|
|
25
|
+
}
|
|
26
|
+
export interface PanelWorkspaceReport {
|
|
27
|
+
readonly workspace: string | null;
|
|
28
|
+
readonly at: number;
|
|
29
|
+
readonly units: readonly PanelUnitReport[];
|
|
30
|
+
}
|
|
31
|
+
export interface PanelCardsReport {
|
|
32
|
+
readonly workspace: string;
|
|
33
|
+
readonly cards: readonly PanelCardRead[];
|
|
34
|
+
}
|
|
35
|
+
export interface PanelCrashReport {
|
|
36
|
+
readonly workspace: string;
|
|
37
|
+
readonly unit: string;
|
|
38
|
+
readonly reason: string;
|
|
39
|
+
readonly at: number;
|
|
40
|
+
}
|
|
41
|
+
export interface PanelStateStore {
|
|
42
|
+
cards(report: PanelCardsReport): void;
|
|
43
|
+
crash(report: PanelCrashReport): void;
|
|
44
|
+
snapshot(workspace: string | null): PanelWorkspaceReport;
|
|
45
|
+
}
|
|
46
|
+
export declare const PANEL_STATE_MAX_UNITS = 64;
|
|
47
|
+
export declare const PANEL_STATE_MAX_CARDS = 12;
|
|
48
|
+
export declare const PANEL_STATE_MAX_REPORTS = 32;
|
|
49
|
+
export declare const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
50
|
+
export declare const PANEL_STATE_MAX_TEXT = 2000;
|
|
51
|
+
export type PanelParse<T> = {
|
|
52
|
+
readonly ok: true;
|
|
53
|
+
readonly value: T;
|
|
54
|
+
} | {
|
|
55
|
+
readonly ok: false;
|
|
56
|
+
readonly message: string;
|
|
57
|
+
};
|
|
58
|
+
export declare function parseCardsReport(payload: unknown): PanelParse<PanelCardsReport>;
|
|
59
|
+
export declare function parseCrashReport(payload: unknown): PanelParse<PanelCrashReport>;
|
|
60
|
+
export declare function createPanelStateStore(): PanelStateStore;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const PANEL_STATE_MAX_UNITS = 64;
|
|
2
|
+
export const PANEL_STATE_MAX_CARDS = 12;
|
|
3
|
+
export const PANEL_STATE_MAX_REPORTS = 32;
|
|
4
|
+
export const PANEL_STATE_CRASH_WINDOW_MS = 600000;
|
|
5
|
+
export const PANEL_STATE_MAX_TEXT = 2000;
|
|
6
|
+
const PHASES = ['idle', 'loading', 'ready', 'error'];
|
|
7
|
+
function asRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
function textOf(value, max) {
|
|
11
|
+
if (typeof value !== 'string')
|
|
12
|
+
return '';
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
15
|
+
}
|
|
16
|
+
function atOf(value) {
|
|
17
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
18
|
+
return null;
|
|
19
|
+
if (value < 0)
|
|
20
|
+
return null;
|
|
21
|
+
return Math.trunc(value);
|
|
22
|
+
}
|
|
23
|
+
function errorOf(raw) {
|
|
24
|
+
const record = asRecord(raw);
|
|
25
|
+
if (record === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const code = textOf(record['code'], 120);
|
|
28
|
+
const message = textOf(record['message'], PANEL_STATE_MAX_TEXT);
|
|
29
|
+
if (code === '' && message === '')
|
|
30
|
+
return undefined;
|
|
31
|
+
return { code: code === '' ? 'unknown' : code, message };
|
|
32
|
+
}
|
|
33
|
+
export function parseCardsReport(payload) {
|
|
34
|
+
const record = asRecord(payload);
|
|
35
|
+
if (record === undefined)
|
|
36
|
+
return { ok: false, message: '请求体要是一个对象' };
|
|
37
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
38
|
+
if (workspace === '')
|
|
39
|
+
return { ok: false, message: 'workspace 不能是空的 —— 面板按工作区存' };
|
|
40
|
+
const raw = record['cards'];
|
|
41
|
+
if (!Array.isArray(raw))
|
|
42
|
+
return { ok: false, message: 'cards 要是一个数组' };
|
|
43
|
+
const cards = [];
|
|
44
|
+
for (const item of raw.slice(0, PANEL_STATE_MAX_REPORTS)) {
|
|
45
|
+
const card = asRecord(item);
|
|
46
|
+
if (card === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
const unit = textOf(card['unit'], 200);
|
|
49
|
+
const route = textOf(card['route'], 400);
|
|
50
|
+
const phase = textOf(card['phase'], 20);
|
|
51
|
+
const at = atOf(card['at']);
|
|
52
|
+
if (unit === '' || route === '' || at === null || !PHASES.includes(phase))
|
|
53
|
+
continue;
|
|
54
|
+
const error = errorOf(card['error']);
|
|
55
|
+
cards.push({ unit, route, phase, at, ...(error === undefined ? {} : { error }) });
|
|
56
|
+
}
|
|
57
|
+
if (cards.length === 0)
|
|
58
|
+
return { ok: false, message: 'cards 里没有一条能认的(unit / route / phase / at 都要有)' };
|
|
59
|
+
return { ok: true, value: { workspace, cards } };
|
|
60
|
+
}
|
|
61
|
+
export function parseCrashReport(payload) {
|
|
62
|
+
const record = asRecord(payload);
|
|
63
|
+
if (record === undefined)
|
|
64
|
+
return { ok: false, message: '请求体要是一个对象' };
|
|
65
|
+
const workspace = textOf(record['workspace'], PANEL_STATE_MAX_TEXT);
|
|
66
|
+
const unit = textOf(record['unit'], 200);
|
|
67
|
+
const reason = textOf(record['reason'], PANEL_STATE_MAX_TEXT);
|
|
68
|
+
if (workspace === '')
|
|
69
|
+
return { ok: false, message: 'workspace 不能是空的' };
|
|
70
|
+
if (unit === '')
|
|
71
|
+
return { ok: false, message: 'unit 不能是空的' };
|
|
72
|
+
if (reason === '')
|
|
73
|
+
return { ok: false, message: 'reason 不能是空的(没有原因的崩溃没法查)' };
|
|
74
|
+
return { ok: true, value: { workspace, unit, reason, at: atOf(record['at']) ?? Date.now() } };
|
|
75
|
+
}
|
|
76
|
+
export function createPanelStateStore() {
|
|
77
|
+
const workspaces = new Map();
|
|
78
|
+
function trim(entry, max) {
|
|
79
|
+
while (entry.size > max) {
|
|
80
|
+
const oldest = entry.keys().next();
|
|
81
|
+
if (oldest.done === true)
|
|
82
|
+
return;
|
|
83
|
+
entry.delete(oldest.value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function workspaceEntryOf(workspace) {
|
|
87
|
+
const existing = workspaces.get(workspace);
|
|
88
|
+
if (existing !== undefined)
|
|
89
|
+
return existing;
|
|
90
|
+
const created = { at: 0, units: new Map() };
|
|
91
|
+
workspaces.set(workspace, created);
|
|
92
|
+
trim(workspaces, PANEL_STATE_MAX_UNITS);
|
|
93
|
+
return created;
|
|
94
|
+
}
|
|
95
|
+
function unitEntryOf(entry, unit) {
|
|
96
|
+
const existing = entry.units.get(unit);
|
|
97
|
+
if (existing !== undefined)
|
|
98
|
+
return existing;
|
|
99
|
+
const created = { at: 0, cards: new Map() };
|
|
100
|
+
entry.units.set(unit, created);
|
|
101
|
+
return created;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
cards(report) {
|
|
105
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
106
|
+
for (const card of report.cards) {
|
|
107
|
+
const unit = unitEntryOf(entry, card.unit);
|
|
108
|
+
unit.cards.delete(card.route);
|
|
109
|
+
unit.cards.set(card.route, card);
|
|
110
|
+
trim(unit.cards, PANEL_STATE_MAX_CARDS);
|
|
111
|
+
if (card.at > unit.at)
|
|
112
|
+
unit.at = card.at;
|
|
113
|
+
if (card.at > entry.at)
|
|
114
|
+
entry.at = card.at;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
crash(report) {
|
|
118
|
+
const entry = workspaceEntryOf(report.workspace);
|
|
119
|
+
const unit = unitEntryOf(entry, report.unit);
|
|
120
|
+
const previous = unit.crash;
|
|
121
|
+
const counted = previous !== undefined &&
|
|
122
|
+
previous.reason === report.reason &&
|
|
123
|
+
report.at >= previous.at &&
|
|
124
|
+
report.at - previous.at <= PANEL_STATE_CRASH_WINDOW_MS;
|
|
125
|
+
unit.crash = { unit: report.unit, reason: report.reason, at: report.at, count: counted ? previous.count + 1 : 1 };
|
|
126
|
+
if (report.at > unit.at)
|
|
127
|
+
unit.at = report.at;
|
|
128
|
+
if (report.at > entry.at)
|
|
129
|
+
entry.at = report.at;
|
|
130
|
+
},
|
|
131
|
+
snapshot(workspace) {
|
|
132
|
+
if (workspace === null)
|
|
133
|
+
return { workspace: null, at: 0, units: [] };
|
|
134
|
+
const entry = workspaces.get(workspace);
|
|
135
|
+
if (entry === undefined)
|
|
136
|
+
return { workspace, at: 0, units: [] };
|
|
137
|
+
const units = [...entry.units.entries()].map(([unit, one]) => {
|
|
138
|
+
const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
|
|
139
|
+
const okAt = cards.reduce((best, card) => (card.phase === 'ready' && (best === null || card.at > best) ? card.at : best), null);
|
|
140
|
+
return { unit, at: one.at, okAt, cards, ...(one.crash === undefined ? {} : { crash: one.crash }) };
|
|
141
|
+
});
|
|
142
|
+
units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
|
|
143
|
+
return { workspace, at: entry.at, units };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { hasEnvSupply, type UnitManifest } from '../unit.js';
|
|
3
|
+
|
|
4
|
+
function manifest(env?: UnitManifest['env']): UnitManifest {
|
|
5
|
+
return {
|
|
6
|
+
id: 'issues',
|
|
7
|
+
card: { titleKey: 'issues.mine', order: 10 },
|
|
8
|
+
tools: [],
|
|
9
|
+
host: { inject: [] },
|
|
10
|
+
...(env === undefined ? {} : { env }),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('hasEnvSupply(缺来源时那条回退口径,宿主与浏览器共用一份)', () => {
|
|
15
|
+
it('声明了插件或库 → 有供给', () => {
|
|
16
|
+
expect(hasEnvSupply(manifest({ plugins: [{ name: 'coding', commands: ['x'] }] }))).toBe(true);
|
|
17
|
+
expect(
|
|
18
|
+
hasEnvSupply(
|
|
19
|
+
manifest({
|
|
20
|
+
libs: [
|
|
21
|
+
{
|
|
22
|
+
name: 'opencli-workflow',
|
|
23
|
+
registryEnv: 'WB_NPM',
|
|
24
|
+
registryDefault: 'https://registry.example/',
|
|
25
|
+
purpose: '夹具',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('★ 只有基座命令(无卡片模块那种)不算供给 —— 它不是市场门槛,只是缺来源时的回退', () => {
|
|
34
|
+
expect(hasEnvSupply(manifest({ command: 'gitops' }))).toBe(false);
|
|
35
|
+
expect(hasEnvSupply(manifest({ plugins: [], libs: [] }))).toBe(false);
|
|
36
|
+
expect(hasEnvSupply(manifest())).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
package/src/api/unit.ts
CHANGED
|
@@ -55,3 +55,7 @@ export interface UnitManifest {
|
|
|
55
55
|
readonly host: UnitHostNeeds;
|
|
56
56
|
readonly env?: UnitEnvNeeds;
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
export function hasEnvSupply(manifest: UnitManifest): boolean {
|
|
60
|
+
return (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0;
|
|
61
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface CardBoundaryProps {
|
|
|
8
8
|
readonly unit: string;
|
|
9
9
|
readonly resetKey: string;
|
|
10
10
|
readonly t: Translate;
|
|
11
|
+
readonly onFailure?: ((reason: string) => void) | undefined;
|
|
11
12
|
readonly children?: ReactNode | undefined;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -28,7 +29,9 @@ export class CardBoundary extends Component<CardBoundaryProps, CardBoundaryState
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
override componentDidCatch(error: unknown): void {
|
|
31
|
-
|
|
32
|
+
const reason = errorReasonOf(error);
|
|
33
|
+
console.error(`[dsh-workbench] 模块 ${this.props.unit} 渲染失败:${reason}`, error);
|
|
34
|
+
this.props.onFailure?.(reason);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
override componentDidUpdate(previous: CardBoundaryProps): void {
|
package/src/config/constants.ts
CHANGED
|
@@ -9,6 +9,10 @@ export const ACTIONS_PREFIX = `${API_PREFIX}/actions`;
|
|
|
9
9
|
|
|
10
10
|
export const PROVISION_PREFIX = `${API_PREFIX}/provision`;
|
|
11
11
|
|
|
12
|
+
export const PANEL_STATE_PATH = "panel-state";
|
|
13
|
+
|
|
14
|
+
export const PANEL_STATE_PREFIX = `${API_PREFIX}/${PANEL_STATE_PATH}`;
|
|
15
|
+
|
|
12
16
|
export const OPENCLI_PIN = "1.8.8";
|
|
13
17
|
|
|
14
18
|
export const CODING_ISSUES_SERVICE = "workbenchCodingIssues";
|
|
@@ -17,6 +21,8 @@ export const PANELS_SERVICE = "workbenchPanels";
|
|
|
17
21
|
|
|
18
22
|
export const UNITS_SERVICE = "workbenchUnits";
|
|
19
23
|
|
|
24
|
+
export const PANEL_STATE_SERVICE = "workbenchPanelState";
|
|
25
|
+
|
|
20
26
|
export const PANEL_LIST_LIMIT = 50;
|
|
21
27
|
|
|
22
28
|
export const OPENCLI_TIMEOUT_MS = 120_000;
|
package/src/host/http.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DshHostContext, DshRequest, DshResponse } from '../api/dsh.js';
|
|
2
2
|
import { unitFactScope } from '../api/facts.js';
|
|
3
|
-
import {
|
|
3
|
+
import { parseCardsReport, parseCrashReport, type PanelStateStore } from '../lib/panel-state.js';
|
|
4
|
+
import { ACTIONS_PREFIX, API_PREFIX, PANEL_STATE_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
|
|
4
5
|
import {
|
|
5
6
|
parseProvisionBody,
|
|
6
7
|
type ProvisionHandler,
|
|
@@ -298,3 +299,31 @@ export function registerActions(
|
|
|
298
299
|
...(wrote === undefined ? {} : { wrote }),
|
|
299
300
|
});
|
|
300
301
|
}
|
|
302
|
+
|
|
303
|
+
export function registerPanelState(ctx: DshHostContext, store: PanelStateStore): () => void {
|
|
304
|
+
return registerPost(ctx, {
|
|
305
|
+
prefix: PANEL_STATE_PREFIX,
|
|
306
|
+
what: '面板运行态',
|
|
307
|
+
refusedCode: 'report-refused',
|
|
308
|
+
resources: [
|
|
309
|
+
{
|
|
310
|
+
resource: 'cards',
|
|
311
|
+
handle(payload) {
|
|
312
|
+
const parsed = parseCardsReport(payload);
|
|
313
|
+
if (!parsed.ok) return { ok: false, text: parsed.message };
|
|
314
|
+
store.cards(parsed.value);
|
|
315
|
+
return { ok: true, at: Date.now() };
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
resource: 'crash',
|
|
320
|
+
handle(payload) {
|
|
321
|
+
const parsed = parseCrashReport(payload);
|
|
322
|
+
if (!parsed.ok) return { ok: false, text: parsed.message };
|
|
323
|
+
store.crash(parsed.value);
|
|
324
|
+
return { ok: true, at: Date.now() };
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
}
|
|
@@ -16,6 +16,45 @@ export interface ApprovalDetail {
|
|
|
16
16
|
|
|
17
17
|
export type ApprovalFacts = ApprovalDetail | { readonly blocked: string };
|
|
18
18
|
|
|
19
|
+
export const APPROVAL_NOTE_ARG = 'approval_note';
|
|
20
|
+
|
|
21
|
+
export const APPROVAL_NOTE_LIMIT = 160;
|
|
22
|
+
|
|
23
|
+
const NOTE_SCHEMA: Readonly<Record<string, unknown>> = {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: '一句话说明你这次想拿它干什么(要点头的那几通会显示在审批屏上)。',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function withApprovalNote(
|
|
29
|
+
parameters: Record<string, unknown>,
|
|
30
|
+
options: { readonly required?: boolean | undefined } = {},
|
|
31
|
+
): Record<string, unknown> {
|
|
32
|
+
const properties = parameters['properties'];
|
|
33
|
+
if (typeof properties !== 'object' || properties === null) return parameters;
|
|
34
|
+
const map = properties as Record<string, unknown>;
|
|
35
|
+
if (APPROVAL_NOTE_ARG in map) return parameters;
|
|
36
|
+
|
|
37
|
+
const declared = Array.isArray(parameters['required']) ? parameters['required'] : [];
|
|
38
|
+
const kept = declared.filter((name) => name !== APPROVAL_NOTE_ARG);
|
|
39
|
+
const required = options.required === true ? [...kept, APPROVAL_NOTE_ARG] : kept;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...parameters,
|
|
43
|
+
properties: { ...map, [APPROVAL_NOTE_ARG]: NOTE_SCHEMA },
|
|
44
|
+
...(required.length === 0 ? {} : { required }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function approvalNoteOf(call: ApprovalCall): string | undefined {
|
|
49
|
+
const raw = call.arguments;
|
|
50
|
+
const value =
|
|
51
|
+
typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>)[APPROVAL_NOTE_ARG] : undefined;
|
|
52
|
+
if (typeof value !== 'string') return undefined;
|
|
53
|
+
const line = value.replace(/\s+/g, ' ').trim();
|
|
54
|
+
if (line === '') return undefined;
|
|
55
|
+
return line.length > APPROVAL_NOTE_LIMIT ? `${line.slice(0, APPROVAL_NOTE_LIMIT)}…` : line;
|
|
56
|
+
}
|
|
57
|
+
|
|
19
58
|
export interface ApprovalCall {
|
|
20
59
|
readonly name?: string | undefined;
|
|
21
60
|
readonly arguments?: unknown;
|
|
@@ -48,14 +87,17 @@ function messageOf(error: unknown): string {
|
|
|
48
87
|
function compose(
|
|
49
88
|
detail: ApprovalDetail | undefined,
|
|
50
89
|
verdict: { readonly question: ApprovalDetail; readonly fallback: ApprovalDetail },
|
|
90
|
+
note: string | undefined,
|
|
51
91
|
): ApprovalDetail {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
const base =
|
|
93
|
+
detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === ''
|
|
94
|
+
? verdict.fallback
|
|
95
|
+
: {
|
|
96
|
+
en: `${detail.en} — ${verdict.question.en}`,
|
|
97
|
+
zh: `${detail.zh} —— ${verdict.question.zh}`,
|
|
98
|
+
};
|
|
99
|
+
if (note === undefined) return base;
|
|
100
|
+
return { en: `${base.en} — ${note}`, zh: `${base.zh} —— ${note}` };
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
export async function approvalFor(
|
|
@@ -93,5 +135,9 @@ export async function approvalFor(
|
|
|
93
135
|
}
|
|
94
136
|
if (facts !== undefined && 'blocked' in facts) return { kind: 'deny', reason: facts.blocked };
|
|
95
137
|
|
|
96
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
kind: 'ask',
|
|
140
|
+
reason: verdict.reason,
|
|
141
|
+
displayReason: compose(facts, verdict, approvalNoteOf(call)),
|
|
142
|
+
};
|
|
97
143
|
}
|
package/src/host/unit-face.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface UnitMountDeps {
|
|
|
14
14
|
readonly logger?: { info?(message: string): void; warn?(message: string): void } | undefined;
|
|
15
15
|
readonly manifests: () => readonly UnitManifest[];
|
|
16
16
|
readonly ownedUnits: () => readonly string[];
|
|
17
|
+
readonly installable?: (() => readonly string[]) | undefined;
|
|
17
18
|
readonly opencliCommand?: string;
|
|
18
19
|
readonly store: WorkspaceStore;
|
|
19
20
|
readonly values?: ValueHandoff | undefined;
|
package/src/host.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './api/facts.js';
|
|
|
6
6
|
export * from './api/long-task.js';
|
|
7
7
|
export * from './lib/text.js';
|
|
8
8
|
export * from './lib/ui.js';
|
|
9
|
+
export * from './lib/panel-state.js';
|
|
9
10
|
export * from './api/blocker-table.js';
|
|
10
11
|
export * from './host/cli-error.js';
|
|
11
12
|
export * from './host/data-root.js';
|
package/src/i18n/en.ts
CHANGED
|
@@ -10,6 +10,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
10
10
|
'project.none': 'No project',
|
|
11
11
|
'project.empty': 'No projects yet. Use “Add project” below.',
|
|
12
12
|
'project.add': 'Add project…',
|
|
13
|
+
'project.add.busy': 'Creating the workspace…',
|
|
14
|
+
'project.add.failed': 'Creating the workspace failed',
|
|
13
15
|
'project.search.label': 'Search workspaces',
|
|
14
16
|
'project.search.placeholder': 'Search workspaces (folder / name / path)',
|
|
15
17
|
'project.search.empty': 'No workspace matches “{query}”.',
|
|
@@ -45,18 +47,22 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
45
47
|
'time.days': '{n} d ago',
|
|
46
48
|
|
|
47
49
|
'panels.title': 'Panel modules',
|
|
48
|
-
'panels.
|
|
49
|
-
'panels.
|
|
50
|
-
'panels.market.
|
|
51
|
-
'panels.
|
|
52
|
-
'panels.
|
|
50
|
+
'panels.groupBoardless': 'No-board modules',
|
|
51
|
+
'panels.groupPlugins': 'opencli plugins',
|
|
52
|
+
'panels.market.defaultHint': 'When on, it is on by default in every workspace; you can also turn it off for a single workspace in Panel setup.',
|
|
53
|
+
'panels.market.defaultLabel': 'On by default everywhere',
|
|
54
|
+
'panels.market.defaultLocked': 'Unusable right now — update or uninstall it first',
|
|
53
55
|
'panels.followAll': 'Restore the default everywhere',
|
|
56
|
+
'panels.followAllConfirmTitle': 'Restore the default everywhere?',
|
|
57
|
+
'panels.followAllConfirmDetail': 'Every row changed in this workspace goes back to following the global default.',
|
|
58
|
+
'panels.followAllConfirmAck': 'I understand: to set them up for this workspace again, I will have to change them row by row.',
|
|
59
|
+
'panels.followAllConfirmYes': 'Restore',
|
|
60
|
+
'panels.followAllConfirmNo': 'Cancel',
|
|
54
61
|
'panels.enabled': 'enabled in this project',
|
|
55
62
|
'panels.disabled': 'not enabled in this project',
|
|
56
63
|
'panels.choose': 'Choose modules',
|
|
57
64
|
'panels.buildModule': 'Build a module',
|
|
58
65
|
'panels.failed': 'Could not save the change. Try again.',
|
|
59
|
-
'panels.hintEffect': 'Disabling = the agent cannot reach that data',
|
|
60
66
|
'panels.dragHint': 'Drag to reorder',
|
|
61
67
|
'panels.unknownStep': 'No suite provides this view',
|
|
62
68
|
'panels.labelWithHint': '{label} ({hint})',
|
|
@@ -69,9 +75,10 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
69
75
|
'panels.browseMarket': 'Browse the module market',
|
|
70
76
|
'panels.noneInstalled': 'None of these modules has its plugin installed yet',
|
|
71
77
|
'panels.noneInstalledHint': 'Once installed they come back here, and you pick which go on the panel.',
|
|
72
|
-
'panels.rowPlugin': 'Plugin',
|
|
73
78
|
'panels.market.installed': 'installed',
|
|
74
|
-
'panels.market.
|
|
79
|
+
'panels.market.notActive': 'not active',
|
|
80
|
+
'panels.market.notUsable': 'unusable',
|
|
81
|
+
'panels.market.broken': 'broken',
|
|
75
82
|
'panels.market.hasNew': 'New version',
|
|
76
83
|
'panels.market.missing': 'not installed',
|
|
77
84
|
'panels.market.unchecked': 'unknown',
|
|
@@ -103,16 +110,17 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
103
110
|
'panels.market.confirmNo': 'Cancel',
|
|
104
111
|
'panels.market.failed': 'Failed: {reason}',
|
|
105
112
|
'panels.market.needEnv': 'Set {env} in the terminal that started the workbench first',
|
|
113
|
+
'panels.market.needCommands': 'Missing commands: {commands}',
|
|
114
|
+
'panels.market.localMark': 'local',
|
|
115
|
+
'panels.market.localHint': 'This is your machine’s own copy (not taken from the registry).',
|
|
106
116
|
'panels.market.noSource': 'This module declares no install method — install it yourself',
|
|
107
117
|
'panels.market.badSource': 'The install method is incomplete — install it yourself',
|
|
108
118
|
'panels.market.empty': 'No modules available',
|
|
109
|
-
'panels.market.sectionLocal': 'Local modules',
|
|
110
|
-
'panels.market.sectionUnlisted': 'Not from the market',
|
|
111
119
|
'panels.market.sectionPlugins': 'Installed opencli plugins',
|
|
112
120
|
'panels.market.moduleOutdated': 'update available',
|
|
113
|
-
'panels.market.moduleRefused': 'This module
|
|
121
|
+
'panels.market.moduleRefused': 'This module does not match this workbench, so it cannot be used for now.',
|
|
114
122
|
'panels.market.moduleRefusedDetail': 'Why it cannot be used: {reason}',
|
|
115
|
-
'panels.market.moduleRestartHint': '
|
|
123
|
+
'panels.market.moduleRestartHint': 'restart dsh once to bring it up',
|
|
116
124
|
'panels.market.workbenchName': 'Workbench',
|
|
117
125
|
'panels.market.workbenchLocal': 'This is the copy from your working repository; it does not come from the registry.',
|
|
118
126
|
'panels.market.pluginKeptInUse': 'Files kept for now: {names} still uses it.',
|
|
@@ -206,7 +214,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
206
214
|
'panels.market.add.registryPnpm': "pnpm's own registry",
|
|
207
215
|
'panels.market.add.check': 'Check',
|
|
208
216
|
'panels.market.add.checking': 'Checking…',
|
|
209
|
-
'panels.market.add.classifying': '
|
|
217
|
+
'panels.market.add.classifying': 'Checking…',
|
|
210
218
|
'panels.market.add.change': 'Edit',
|
|
211
219
|
'panels.market.add.previewTitle': 'About to install',
|
|
212
220
|
'panels.market.add.fieldName': 'Package',
|