@dsh-workbench/shared 0.1.18 → 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.
Files changed (46) hide show
  1. package/dist/api/unit.d.ts +1 -0
  2. package/dist/api/unit.js +3 -1
  3. package/dist/api/workbench-contract.d.ts +1 -1
  4. package/dist/components/CardBoundary.d.ts +1 -0
  5. package/dist/components/CardBoundary.js +3 -1
  6. package/dist/components/board-module.d.ts +1 -1
  7. package/dist/components/icons.d.ts +2 -0
  8. package/dist/components/icons.js +14 -0
  9. package/dist/config/constants.d.ts +3 -0
  10. package/dist/config/constants.js +3 -0
  11. package/dist/host/http.d.ts +2 -0
  12. package/dist/host/http.js +31 -1
  13. package/dist/host/provision.d.ts +1 -1
  14. package/dist/host/provision.js +5 -0
  15. package/dist/host/tool-approval.d.ts +5 -0
  16. package/dist/host/tool-approval.js +47 -9
  17. package/dist/host/unit-face.d.ts +1 -0
  18. package/dist/host.d.ts +1 -0
  19. package/dist/host.js +1 -0
  20. package/dist/i18n/en.js +30 -13
  21. package/dist/i18n/index.d.ts +31 -14
  22. package/dist/i18n/zh.d.ts +30 -13
  23. package/dist/i18n/zh.js +30 -13
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/lib/panel-state.d.ts +60 -0
  27. package/dist/lib/panel-state.js +146 -0
  28. package/package.json +1 -1
  29. package/src/api/__tests__/unit.test.ts +38 -0
  30. package/src/api/unit.ts +4 -0
  31. package/src/api/workbench-contract.ts +2 -1
  32. package/src/components/CardBoundary.tsx +4 -1
  33. package/src/components/board-module.ts +1 -1
  34. package/src/components/icons.tsx +38 -0
  35. package/src/config/constants.ts +6 -0
  36. package/src/host/__tests__/provision.test.ts +13 -0
  37. package/src/host/http.ts +30 -1
  38. package/src/host/provision.ts +3 -0
  39. package/src/host/tool-approval.ts +54 -8
  40. package/src/host/unit-face.ts +1 -0
  41. package/src/host.ts +1 -0
  42. package/src/i18n/en.ts +31 -13
  43. package/src/i18n/zh.ts +31 -13
  44. package/src/index.ts +1 -0
  45. package/src/lib/panel-state.test.ts +123 -0
  46. package/src/lib/panel-state.ts +209 -0
@@ -14,6 +14,7 @@ describe('provision 请求体:每类动作各有各的目标字段', () => {
14
14
  ['update-dependency', 'id'],
15
15
  ['update-plugin', 'name'],
16
16
  ['uninstall-plugin', 'name'],
17
+ ['update-workbench', 'name'],
17
18
  ]);
18
19
  });
19
20
 
@@ -73,6 +74,18 @@ describe('provision 请求体:每类动作各有各的目标字段', () => {
73
74
  });
74
75
  });
75
76
 
77
+ it('更新工作台自己按包名认目标(界面把它自己那个包名交上来)', () => {
78
+ expect(parseProvisionBody({ action: 'update-workbench', name: ' @dsh-workbench/core ' })).toEqual({
79
+ ok: true,
80
+ request: { action: 'update-workbench', name: '@dsh-workbench/core' },
81
+ });
82
+ expect(parseProvisionBody({ action: 'update-workbench', unit: 'panels' })).toEqual({
83
+ ok: false,
84
+ code: 'workbench/no-name',
85
+ message: '缺少 name',
86
+ });
87
+ });
88
+
76
89
  it('拿错字段不算数(spec 动作给了 unit 也要打回)', () => {
77
90
  expect(parseProvisionBody({ action: 'install-bundle', unit: 'issues' })).toEqual({
78
91
  ok: false,
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 { ACTIONS_PREFIX, API_PREFIX, PLUGIN_ID, PREFS_PREFIX, PROVISION_PREFIX } from '../config/constants.js';
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
+ }
@@ -13,6 +13,7 @@ export const PROVISION_ACTIONS = [
13
13
  'update-dependency',
14
14
  'update-plugin',
15
15
  'uninstall-plugin',
16
+ 'update-workbench',
16
17
  ] as const satisfies readonly ProvisionAction[];
17
18
 
18
19
  export const DEPENDENCY_ACTIONS = ['update-dependency'] as const satisfies readonly ProvisionAction[];
@@ -35,6 +36,7 @@ export function provisionTargetOf(action: ProvisionAction): ProvisionTarget {
35
36
  if (action === 'install-bundle') return 'spec';
36
37
  if (action === 'remove-bundle') return 'name';
37
38
  if (action === 'update-plugin' || action === 'uninstall-plugin') return 'name';
39
+ if (action === 'update-workbench') return 'name';
38
40
  if (action === 'cancel-install') return 'requestId';
39
41
  return 'unit';
40
42
  }
@@ -78,6 +80,7 @@ function requestOf(action: ProvisionAction, value: string): ProvisionRequest {
78
80
  if (action === 'install-bundle') return { action, spec: value };
79
81
  if (action === 'remove-bundle') return { action, name: value };
80
82
  if (action === 'update-plugin' || action === 'uninstall-plugin') return { action, name: value };
83
+ if (action === 'update-workbench') return { action, name: value };
81
84
  if (action === 'cancel-install') return { action, requestId: value };
82
85
  return { action, unit: value };
83
86
  }
@@ -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
- if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '') {
53
- return verdict.fallback;
54
- }
55
- return {
56
- en: `${detail.en} — ${verdict.question.en}`,
57
- zh: `${detail.zh} —— ${verdict.question.zh}`,
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 { kind: 'ask', reason: verdict.reason, displayReason: compose(facts, verdict) };
138
+ return {
139
+ kind: 'ask',
140
+ reason: verdict.reason,
141
+ displayReason: compose(facts, verdict, approvalNoteOf(call)),
142
+ };
97
143
  }
@@ -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,13 +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.reset': 'Reset',
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',
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',
49
61
  'panels.enabled': 'enabled in this project',
50
62
  'panels.disabled': 'not enabled in this project',
51
63
  'panels.choose': 'Choose modules',
52
64
  'panels.buildModule': 'Build a module',
53
65
  'panels.failed': 'Could not save the change. Try again.',
54
- 'panels.hintEffect': 'Disabling = the agent cannot reach that data',
55
66
  'panels.dragHint': 'Drag to reorder',
56
67
  'panels.unknownStep': 'No suite provides this view',
57
68
  'panels.labelWithHint': '{label} ({hint})',
@@ -65,7 +76,9 @@ export const en: Record<keyof WorkbenchMessages, string> = {
65
76
  'panels.noneInstalled': 'None of these modules has its plugin installed yet',
66
77
  'panels.noneInstalledHint': 'Once installed they come back here, and you pick which go on the panel.',
67
78
  'panels.market.installed': 'installed',
68
- 'panels.market.outdated': 'needs update',
79
+ 'panels.market.notActive': 'not active',
80
+ 'panels.market.notUsable': 'unusable',
81
+ 'panels.market.broken': 'broken',
69
82
  'panels.market.hasNew': 'New version',
70
83
  'panels.market.missing': 'not installed',
71
84
  'panels.market.unchecked': 'unknown',
@@ -73,7 +86,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
73
86
  'panels.market.sectionInstalled': 'Installed modules',
74
87
  'panels.market.sectionPending': 'Not installed',
75
88
  'panels.market.sectionShared': 'Shared dependencies',
76
- 'panels.market.sharedUsedBy': 'Used by {modules}',
89
+ 'panels.market.sharedUsedBy': 'Used by module {modules}',
77
90
  'panels.market.listSeparator': ', ',
78
91
  'panels.market.sharedMissing': 'not installed',
79
92
  'panels.market.sharedOutdated': 'update available',
@@ -97,22 +110,24 @@ export const en: Record<keyof WorkbenchMessages, string> = {
97
110
  'panels.market.confirmNo': 'Cancel',
98
111
  'panels.market.failed': 'Failed: {reason}',
99
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).',
100
116
  'panels.market.noSource': 'This module declares no install method — install it yourself',
101
117
  'panels.market.badSource': 'The install method is incomplete — install it yourself',
102
118
  'panels.market.empty': 'No modules available',
103
- 'panels.market.sectionLocal': 'Local modules',
104
- 'panels.market.sectionUnlisted': 'Not from the market',
105
119
  'panels.market.sectionPlugins': 'Installed opencli plugins',
106
120
  'panels.market.moduleOutdated': 'update available',
107
- 'panels.market.moduleRefused': 'This module was built for an older version of the workbench, so it does not work here. Update it, or uninstall.',
121
+ 'panels.market.moduleRefused': 'This module does not match this workbench, so it cannot be used for now.',
108
122
  'panels.market.moduleRefusedDetail': 'Why it cannot be used: {reason}',
109
- 'panels.market.moduleRestartHint': 'installed but not active — restart dsh once to bring it up',
123
+ 'panels.market.moduleRestartHint': 'restart dsh once to bring it up',
124
+ 'panels.market.workbenchName': 'Workbench',
125
+ 'panels.market.workbenchLocal': 'This is the copy from your working repository; it does not come from the registry.',
110
126
  'panels.market.pluginKeptInUse': 'Files kept for now: {names} still uses it.',
111
127
  'panels.market.pluginKeptUnsure': 'Files kept for now: some modules on this machine do not say what they use, so it is unclear who needs it.',
112
128
  'panels.market.pluginKeptHeld': 'Files kept: this one is yours, and a module letting go will not remove it.',
113
129
  'panels.market.pluginKeptUnnamed': 'Files kept for now: this one cannot be identified as a plugin, so it is unclear who needs it.',
114
- 'panels.market.pluginAlsoKnownAs': 'Its package is {pkg} (updates and uninstall use that)',
115
- 'panels.market.pluginBroken': 'This install is broken: it cannot be identified as a plugin. Uninstall it, or install it again by package name above.',
130
+ 'panels.market.pluginBroken': 'This install is broken: it cannot be identified as a plugin. Uninstall it, or tell the agent to reinstall it by package name with workbench_modules (the install-bundle action).',
116
131
  'panels.market.restartNeeded': 'The new version is installed — restart once to pick it up.',
117
132
  'panels.market.foot': 'After installing, enable it in the panel page of that project.',
118
133
  'panels.market.recheck': 'Check for updates',
@@ -121,7 +136,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
121
136
  'panels.market.justNow': 'just now',
122
137
  'panels.market.minutesAgo': '{n} min ago',
123
138
  'panels.market.hoursAgo': '{n} h ago',
124
- 'panels.market.add.description': 'Enter a package name, a Git URL, or a path to a local directory.',
139
+ "panels.market.add.description": "Enter a module or opencli plugin package name, Git repository URL, or local directory path.",
125
140
  'panels.market.add.specPlaceholder': 'e.g. @dsh-workbench/module-hello',
126
141
  'panels.market.add.guideToggle': 'Install guide and examples',
127
142
  'panels.market.add.guideHide': 'Hide guide',
@@ -161,11 +176,14 @@ export const en: Record<keyof WorkbenchMessages, string> = {
161
176
  'panels.market.add.problemNotFound': 'No such module found',
162
177
  'panels.market.add.problemNotPackage': 'That path does not exist or is not a valid module package',
163
178
  'panels.market.add.problemNotBundle':
164
- 'This package cannot be installed here: {reason}. If it is actually an opencli plugin, use its package name (for example opencli-plugin-cmdb) or a local folder — a git URL or tarball does not reveal what it is.',
179
+ 'This package cannot be installed here: {reason}. This entry recognises workbench modules and opencli plugins by package name, and installs either; but a git URL or a tarball does not reveal what it is — if it is really one of those two, use its package name or a local folder instead.',
165
180
  'panels.market.add.problemNotWorkbenchModule': 'This package does not match the workbench you are running, so installing it would not work. Pick another version, or ask the module author.',
166
181
  'panels.market.add.problemNotModuleOrPlugin': 'This package is neither a workbench module nor an opencli plugin, so installing it would do nothing. Pick another version, or ask the package author.',
167
182
  'panels.market.add.failureNotModuleOrPlugin': 'This package is neither a workbench module nor an opencli plugin, so installing it would do nothing',
168
183
  'panels.market.add.installedPlugin': 'Installed: the opencli plugin {name}',
184
+ 'panels.market.add.pluginPreview':
185
+ 'This is an opencli plugin: it goes into the opencli plugin area and works as soon as it is installed — no restart.',
186
+ 'panels.market.add.pluginAlias': 'Plugin name: {name}',
169
187
  'panels.market.add.problemNetwork': 'Cannot reach the module source; check the network and retry',
170
188
  'panels.market.add.problemUnknown': 'Cannot fetch module information: {reason}',
171
189
  'panels.market.add.failureNetwork': 'The network connection failed',
@@ -196,7 +214,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
196
214
  'panels.market.add.registryPnpm': "pnpm's own registry",
197
215
  'panels.market.add.check': 'Check',
198
216
  'panels.market.add.checking': 'Checking…',
199
- 'panels.market.add.classifying': 'Working out what it is…',
217
+ 'panels.market.add.classifying': 'Checking…',
200
218
  'panels.market.add.change': 'Edit',
201
219
  'panels.market.add.previewTitle': 'About to install',
202
220
  'panels.market.add.fieldName': 'Package',
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,13 +45,22 @@ export const zh = {
43
45
  'time.days': '{n} 天前',
44
46
 
45
47
  'panels.title': '面板模块',
46
- 'panels.reset': '恢复默认',
48
+ 'panels.groupBoardless': '无看板模块',
49
+ 'panels.groupPlugins': 'opencli 插件',
50
+ 'panels.market.defaultHint': '打开后所有工作区默认开启,也可在工作区面板配置中单独关闭',
51
+ 'panels.market.defaultLabel': '全局默认开启',
52
+ 'panels.market.defaultLocked': '这份现在用不了,先更新或卸载',
53
+ 'panels.followAll': '全部恢复默认',
54
+ 'panels.followAllConfirmTitle': '全部恢复默认?',
55
+ 'panels.followAllConfirmDetail': '这个工作区里改过的每一行都会清掉,之后都跟随全局。',
56
+ 'panels.followAllConfirmAck': '我明白:清掉之后,想再按这个工作区配,得一行一行重新改。',
57
+ 'panels.followAllConfirmYes': '恢复默认',
58
+ 'panels.followAllConfirmNo': '取消',
47
59
  'panels.enabled': '这个项目已启用',
48
60
  'panels.disabled': '这个项目未启用',
49
61
  'panels.choose': '选择模块',
50
62
  'panels.buildModule': '造一个模块',
51
63
  'panels.failed': '改配置失败了,再试一次。',
52
- 'panels.hintEffect': '停用 = agent 拿不到相关数据',
53
64
  'panels.dragHint': '拖动调整顺序',
54
65
  'panels.unknownStep': '这一层没有套件提供',
55
66
  'panels.labelWithHint': '{label}({hint})',
@@ -63,7 +74,9 @@ export const zh = {
63
74
  'panels.noneInstalled': '这些模块要的插件都还没装',
64
75
  'panels.noneInstalledHint': '装好之后它们会回到这里,再由你决定放哪几个上板。',
65
76
  'panels.market.installed': '已安装',
66
- 'panels.market.outdated': '要更新',
77
+ 'panels.market.notActive': '未生效',
78
+ 'panels.market.notUsable': '不可用',
79
+ 'panels.market.broken': '装坏了',
67
80
  'panels.market.hasNew': '有新版本',
68
81
  'panels.market.missing': '未装',
69
82
  'panels.market.unchecked': '状态未知',
@@ -71,7 +84,7 @@ export const zh = {
71
84
  'panels.market.sectionInstalled': '已安装模块',
72
85
  'panels.market.sectionPending': '未安装',
73
86
  'panels.market.sectionShared': '共用依赖',
74
- 'panels.market.sharedUsedBy': '供 {modules} 使用',
87
+ 'panels.market.sharedUsedBy': '供 {modules} 模块使用',
75
88
  'panels.market.listSeparator': '、',
76
89
  'panels.market.sharedMissing': '未装',
77
90
  'panels.market.sharedOutdated': '有新版',
@@ -93,22 +106,24 @@ export const zh = {
93
106
  'panels.market.confirmNo': '取消',
94
107
  'panels.market.failed': '没成功:{reason}',
95
108
  'panels.market.needEnv': '先在启动工作台的终端里设 {env}',
109
+ 'panels.market.needCommands': '缺这几条命令:{commands}',
110
+ 'panels.market.localMark': '本地',
111
+ 'panels.market.localHint': '本机那份(不从源上取)。',
96
112
  'panels.market.noSource': '这个模块没声明怎么装,只能自己动手',
97
113
  'panels.market.badSource': '安装方式没写全,只能自己动手',
98
114
  'panels.market.empty': '暂无可用模块',
99
- 'panels.market.sectionLocal': '本地模块',
100
- 'panels.market.sectionUnlisted': '不是从市场装的',
101
115
  'panels.market.sectionPlugins': '已安装 opencli 插件',
102
116
  'panels.market.moduleOutdated': '有新版',
103
- 'panels.market.moduleRefused': '这个模块是给旧版本的工作台写的,现在用不了。更新它,或者卸载。',
117
+ 'panels.market.moduleRefused': '模块版本与工作台不匹配,暂时无法使用',
104
118
  'panels.market.moduleRefusedDetail': '装不上的原因:{reason}',
105
- 'panels.market.moduleRestartHint': '装好了但还没生效 —— 重启一次 dsh 就会出来',
119
+ 'panels.market.moduleRestartHint': '重启后生效',
120
+ 'panels.market.workbenchName': '工作台',
121
+ 'panels.market.workbenchLocal': '这份是本仓正在开发的那一份,不从源上取。',
106
122
  'panels.market.pluginKeptInUse': '文件先留着:{names} 还要用它。',
107
123
  'panels.market.pluginKeptUnsure': '文件先留着:这台机器上还有模块看不出用了什么,说不准谁在用它。',
108
124
  'panels.market.pluginKeptHeld': '文件留着:你自己装的那一份,模块放手不动它。',
109
125
  'panels.market.pluginKeptUnnamed': '文件先留着:看不出这是哪个插件,说不准谁在用它。',
110
- 'panels.market.pluginAlsoKnownAs': '包名是 {pkg}(更新与卸载按它来)',
111
- 'panels.market.pluginBroken': '这份装坏了:看不出它是个插件。可以卸载,或者去上面按包名再装一次。',
126
+ 'panels.market.pluginBroken': '这份装坏了:看不出它是个插件。可以卸载,或者直接跟 agent 说:用 workbench_modules 的 install-bundle 按包名再装一次。',
112
127
  'panels.market.restartNeeded': '新版装好了,重启一次才生效。',
113
128
  'panels.market.foot': '装好后到那个项目的面板页勾选它,模块才会出现。',
114
129
  'panels.market.recheck': '重新检查更新',
@@ -117,7 +132,7 @@ export const zh = {
117
132
  'panels.market.justNow': '刚刚',
118
133
  'panels.market.minutesAgo': '{n} 分钟前',
119
134
  'panels.market.hoursAgo': '{n} 小时前',
120
- 'panels.market.add.description': '输入模块的包名、Git 仓库地址或本地目录路径。',
135
+ 'panels.market.add.description': '输入模块或 opencli 插件的包名、Git 仓库地址或本地目录路径。',
121
136
  'panels.market.add.specPlaceholder': '例如 @dsh-workbench/module-hello',
122
137
  'panels.market.add.guideToggle': '模块安装引导和示例',
123
138
  'panels.market.add.guideHide': '收起引导',
@@ -157,11 +172,14 @@ export const zh = {
157
172
  'panels.market.add.problemNotFound': '未找到相关模块',
158
173
  'panels.market.add.problemNotPackage': '该路径不存在或不是有效的模块包',
159
174
  'panels.market.add.problemNotBundle':
160
- '这个包装不进这里:{reason}。要是它其实是个 opencli 插件,请改用包名(例如 opencli-plugin-cmdb)或者本地目录 —— 从 git 地址和 tarball 判断不出它是什么。',
175
+ '这个包装不进这里:{reason}。这个入口按包名认工作台模块和 opencli 插件,两种都能装;但 git 地址和 tarball 判断不出它是什么 —— 要是它其实是这两样之一,请改填包名或本地目录。',
161
176
  'panels.market.add.problemNotWorkbenchModule': '这个包跟现在的工作台对不上,装上也用不了。换一个版本,或者找这个模块的作者。',
162
177
  'panels.market.add.problemNotModuleOrPlugin': '这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上。换一个版本,或者找这个包的作者。',
163
178
  'panels.market.add.failureNotModuleOrPlugin': '这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上',
164
179
  'panels.market.add.installedPlugin': '装上了:opencli 插件 {name}',
180
+ 'panels.market.add.pluginPreview':
181
+ '这是一份 opencli 插件:装进 opencli 的插件区,装完就能用,不用重启。',
182
+ 'panels.market.add.pluginAlias': '插件名:{name}',
165
183
  'panels.market.add.problemNetwork': '无法连接模块源,请检查网络后重试',
166
184
  'panels.market.add.problemUnknown': '无法获取模块信息:{reason}',
167
185
  'panels.market.add.failureNetwork': '网络连接失败',
@@ -192,7 +210,7 @@ export const zh = {
192
210
  'panels.market.add.registryPnpm': 'pnpm 自己配的源',
193
211
  'panels.market.add.check': '预检',
194
212
  'panels.market.add.checking': '正在预检…',
195
- 'panels.market.add.classifying': '判定中…',
213
+ 'panels.market.add.classifying': '检测中…',
196
214
  'panels.market.add.change': '改一下',
197
215
  'panels.market.add.previewTitle': '将要安装',
198
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
+ });