@dsh-workbench/shared 0.1.8 → 0.1.10
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/blocker-table.js +10 -0
- package/dist/api/dsh-client.d.ts +6 -3
- package/dist/api/workbench-contract.d.ts +1 -1
- package/dist/components/ApprovalDetail.js +6 -2
- package/dist/components/CardBoundary.d.ts +21 -0
- package/dist/components/CardBoundary.js +34 -0
- package/dist/host/unit-libs.d.ts +1 -1
- package/dist/i18n/en.js +20 -1
- package/dist/i18n/index.d.ts +21 -2
- package/dist/i18n/merge.d.ts +9 -0
- package/dist/i18n/merge.js +22 -0
- package/dist/i18n/zh.d.ts +20 -1
- package/dist/i18n/zh.js +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/react.d.ts +1 -0
- package/dist/lib/react.js +1 -0
- package/package.json +1 -1
- package/src/api/__tests__/blocker-table.test.ts +13 -0
- package/src/api/blocker-table.ts +11 -0
- package/src/api/dsh-client.ts +7 -1
- package/src/api/workbench-contract.ts +1 -1
- package/src/components/ApprovalDetail.tsx +9 -1
- package/src/components/CardBoundary.tsx +56 -0
- package/src/host/unit-libs.ts +1 -1
- package/src/i18n/en.ts +20 -1
- package/src/i18n/merge.test.ts +62 -1
- package/src/i18n/merge.ts +36 -0
- package/src/i18n/zh.ts +20 -1
- package/src/index.ts +1 -0
- package/src/lib/react.ts +2 -0
- package/dist/components/StateDot.d.ts +0 -5
- package/dist/components/StateDot.js +0 -10
- package/dist/host/__tests__/fakes.d.ts +0 -41
- package/dist/host/__tests__/fakes.js +0 -76
- package/dist/host/__tests__/preflight-fixtures.d.ts +0 -19
- package/dist/host/__tests__/preflight-fixtures.js +0 -47
- package/dist/host/context-inject.d.ts +0 -24
- package/dist/host/context-inject.js +0 -74
- package/dist/host/events.d.ts +0 -9
- package/dist/host/events.js +0 -70
- package/dist/host/mount-units.d.ts +0 -52
- package/dist/host/mount-units.js +0 -186
- package/dist/host/preflight.d.ts +0 -58
- package/dist/host/preflight.js +0 -233
- package/dist/host/probe-cache.d.ts +0 -6
- package/dist/host/probe-cache.js +0 -19
- package/dist/host/probe-libs.d.ts +0 -45
- package/dist/host/probe-libs.js +0 -61
- package/dist/host/probe-units.d.ts +0 -17
- package/dist/host/probe-units.js +0 -102
- package/dist/host/probe-verdict.d.ts +0 -7
- package/dist/host/probe-verdict.js +0 -143
- package/dist/host/tool-scope.d.ts +0 -32
- package/dist/host/tool-scope.js +0 -84
- package/dist/host/unit-readiness.d.ts +0 -7
- package/dist/host/unit-readiness.js +0 -5
- package/dist/host/unit-registry.d.ts +0 -9
- package/dist/host/unit-registry.js +0 -33
- package/dist/host/version-range.d.ts +0 -2
- package/dist/host/version-range.js +0 -73
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
2
|
+
import type { Translate } from '../api/dsh-client.js';
|
|
3
|
+
import type { ReactNode } from '../lib/react.js';
|
|
4
|
+
import { Component, h } from '../lib/react.js';
|
|
5
|
+
import { Note } from './Note.js';
|
|
6
|
+
|
|
7
|
+
export interface CardBoundaryProps {
|
|
8
|
+
readonly unit: string;
|
|
9
|
+
readonly resetKey: string;
|
|
10
|
+
readonly t: Translate;
|
|
11
|
+
readonly children?: ReactNode | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CardBoundaryState {
|
|
15
|
+
readonly reason: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function errorReasonOf(error: unknown): string {
|
|
19
|
+
if (error instanceof Error) return error.message;
|
|
20
|
+
return typeof error === 'string' ? error : JSON.stringify(error) ?? String(error);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CardBoundary extends Component<CardBoundaryProps, CardBoundaryState> {
|
|
24
|
+
override state: CardBoundaryState = { reason: null };
|
|
25
|
+
|
|
26
|
+
static getDerivedStateFromError(error: unknown): CardBoundaryState {
|
|
27
|
+
return { reason: errorReasonOf(error) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override componentDidCatch(error: unknown): void {
|
|
31
|
+
console.error(`[dsh-workbench] 模块 ${this.props.unit} 渲染失败:${errorReasonOf(error)}`, error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override componentDidUpdate(previous: CardBoundaryProps): void {
|
|
35
|
+
if (previous.resetKey !== this.props.resetKey && this.state.reason !== null) {
|
|
36
|
+
this.setState({ reason: null });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private readonly retry = (): void => {
|
|
41
|
+
this.setState({ reason: null });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
override render(): ReactNode {
|
|
45
|
+
const { reason } = this.state;
|
|
46
|
+
if (reason === null) return this.props.children;
|
|
47
|
+
return (
|
|
48
|
+
<Note tone="problem">
|
|
49
|
+
<span>{this.props.t('panels.cardBroken', { unit: this.props.unit, reason })}</span>{' '}
|
|
50
|
+
<Button type="button" variant="outline" size="sm" onClick={this.retry}>
|
|
51
|
+
{this.props.t('panels.cardRetry')}
|
|
52
|
+
</Button>
|
|
53
|
+
</Note>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/host/unit-libs.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface UnitPluginState {
|
|
|
27
27
|
|
|
28
28
|
export interface UnitCapability {
|
|
29
29
|
readonly unit: string;
|
|
30
|
-
readonly status: "ok" | "missing-commands" | "missing-plugin" | "unchecked";
|
|
30
|
+
readonly status: "ok" | "failed" | "missing-commands" | "missing-plugin" | "unchecked";
|
|
31
31
|
readonly plugin?: string;
|
|
32
32
|
readonly required: readonly string[];
|
|
33
33
|
readonly missing: readonly string[];
|
package/src/i18n/en.ts
CHANGED
|
@@ -10,6 +10,20 @@ 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.search.label': 'Search workspaces',
|
|
14
|
+
'project.search.placeholder': 'Search workspaces (folder / name / path)',
|
|
15
|
+
'project.search.empty': 'No workspace matches “{query}”.',
|
|
16
|
+
'project.rename.row': 'Rename “{name}”',
|
|
17
|
+
'project.rename.current': 'Give this workspace a display name…',
|
|
18
|
+
'project.rename.title': 'Give “{name}” a display name',
|
|
19
|
+
'project.rename.description': 'Shown after the folder name. It renames the workspace itself.',
|
|
20
|
+
'project.rename.field': 'Display name',
|
|
21
|
+
'project.rename.placeholder': 'e.g. Workbench shell',
|
|
22
|
+
'project.rename.hint': 'Empty = folder name only',
|
|
23
|
+
'project.rename.save': 'Save',
|
|
24
|
+
'project.rename.saving': 'Saving…',
|
|
25
|
+
'project.rename.cancel': 'Cancel',
|
|
26
|
+
'project.rename.failed': 'Rename failed. Try again.',
|
|
13
27
|
'session.new': 'New session',
|
|
14
28
|
'session.new.label': 'Start a session in the current project',
|
|
15
29
|
'session.empty': 'No sessions in this project yet.',
|
|
@@ -40,6 +54,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
40
54
|
'panels.dragHint': 'Drag to reorder',
|
|
41
55
|
'panels.unknownStep': 'No suite provides this view',
|
|
42
56
|
'panels.labelWithHint': '{label} ({hint})',
|
|
57
|
+
'panels.dep.failed': '⚠ failed to start',
|
|
43
58
|
'panels.dep.missingPlugin': '⚠ not ready yet',
|
|
44
59
|
'panels.dep.missingCommands': '⚠ needs an update',
|
|
45
60
|
'panels.dep.unchecked': '⚠ cannot check',
|
|
@@ -86,7 +101,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
86
101
|
'panels.market.empty': 'No modules available',
|
|
87
102
|
'panels.market.installedSection': 'Installed',
|
|
88
103
|
'panels.market.moduleOutdated': 'update available',
|
|
89
|
-
'panels.market.moduleRestartHint': '
|
|
104
|
+
'panels.market.moduleRestartHint': 'installed but not active — restart dsh once to bring it up',
|
|
90
105
|
'panels.market.restartNeeded': 'The new version is installed — restart once to pick it up.',
|
|
91
106
|
'panels.market.foot': 'After installing, enable it in the panel page of that project.',
|
|
92
107
|
'panels.market.recheck': 'Check for updates',
|
|
@@ -235,6 +250,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
235
250
|
'blocker.exec.hint': 'Make sure opencli is installed and on PATH (which opencli).',
|
|
236
251
|
'blocker.unknown.title': 'Something went wrong',
|
|
237
252
|
'panels.reading': 'Reading the panel setup…',
|
|
253
|
+
'panels.cardBroken': 'This module will not render: {unit} ({reason})',
|
|
254
|
+
'panels.cardRetry': 'Retry',
|
|
238
255
|
'panels.empty': 'No modules on the panel yet',
|
|
239
256
|
'panels.emptyHint': 'You can also just tell the agent: “add a module to the panel”.',
|
|
240
257
|
'env.title': 'Preparing the workbench',
|
|
@@ -273,6 +290,8 @@ export const en: Record<keyof WorkbenchMessages, string> = {
|
|
|
273
290
|
'env.step.warm': 'Warming up site sessions',
|
|
274
291
|
'env.warm.done': '{n} site(s) warmed up',
|
|
275
292
|
'env.warm.short': 'Not warmed this time — harmless; the panel heals on first read',
|
|
293
|
+
'env.unit.failed': 'This module cannot start',
|
|
294
|
+
'env.unit.failedHint': 'It failed while assembling itself; other modules are unaffected.',
|
|
276
295
|
'env.unit.missingPlugin': 'This module is not ready yet',
|
|
277
296
|
'env.unit.missingCommandsHint': 'Updating it should fix this.',
|
|
278
297
|
'env.unit.unchecked': 'Cannot check: no shell channel',
|
package/src/i18n/merge.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { mergeMessages } from './merge.js';
|
|
2
|
+
import { mergeMessages, mergeMessagesSkipping } from './merge.js';
|
|
3
3
|
|
|
4
4
|
const base = { zh: { 'board.title': '看板' }, en: { 'board.title': 'Board' } };
|
|
5
5
|
|
|
@@ -58,3 +58,64 @@ describe('mergeMessages', () => {
|
|
|
58
58
|
).toThrow(/是空的/);
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
describe('mergeMessagesSkipping', () => {
|
|
63
|
+
const good = {
|
|
64
|
+
unit: 'issues',
|
|
65
|
+
messages: { zh: { 'issues.mine': '我的缺陷' }, en: { 'issues.mine': 'My defects' } },
|
|
66
|
+
};
|
|
67
|
+
const bad = {
|
|
68
|
+
unit: 'cmdb',
|
|
69
|
+
messages: { zh: { 'wrong.prefix': '错了' }, en: { 'wrong.prefix': 'nope' } },
|
|
70
|
+
};
|
|
71
|
+
const later = {
|
|
72
|
+
unit: 'builds',
|
|
73
|
+
messages: { zh: { 'builds.recent': '最近构建' }, en: { 'builds.recent': 'Recent builds' } },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
it('★ 一份坏文案只废它自己:好的照并、坏的报出来', () => {
|
|
77
|
+
const { dictionaries, problems } = mergeMessagesSkipping(base, [good, bad, later]);
|
|
78
|
+
|
|
79
|
+
expect(dictionaries.zh).toEqual({
|
|
80
|
+
'board.title': '看板',
|
|
81
|
+
'issues.mine': '我的缺陷',
|
|
82
|
+
'builds.recent': '最近构建',
|
|
83
|
+
});
|
|
84
|
+
expect(dictionaries.en['builds.recent']).toBe('Recent builds');
|
|
85
|
+
expect(problems).toHaveLength(1);
|
|
86
|
+
expect(problems[0]?.unit).toBe('cmdb');
|
|
87
|
+
expect(problems[0]?.reason).toMatch(/必须以 "cmdb\." 开头/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('★ 坏的在前、好的在后:后面那份不受影响(不会因为前者坏就整批拒收)', () => {
|
|
91
|
+
const { dictionaries, problems } = mergeMessagesSkipping(base, [bad, good]);
|
|
92
|
+
|
|
93
|
+
expect(dictionaries.zh['issues.mine']).toBe('我的缺陷');
|
|
94
|
+
expect(problems.map((item) => item.unit)).toEqual(['cmdb']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('都好时与 mergeMessages 逐字一致、没有问题', () => {
|
|
98
|
+
const { dictionaries, problems } = mergeMessagesSkipping(base, [good, later]);
|
|
99
|
+
|
|
100
|
+
expect(dictionaries).toEqual(mergeMessages(base, [good, later]));
|
|
101
|
+
expect(problems).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('与外壳自己的键撞名的那份被跳过,且报的是撞名的双方', () => {
|
|
105
|
+
const clash = {
|
|
106
|
+
unit: 'board',
|
|
107
|
+
messages: { zh: { 'board.title': '看板改' }, en: { 'board.title': 'Board!' } },
|
|
108
|
+
};
|
|
109
|
+
const { dictionaries, problems } = mergeMessagesSkipping(base, [clash]);
|
|
110
|
+
|
|
111
|
+
expect(dictionaries.zh['board.title']).toBe('看板');
|
|
112
|
+
expect(problems[0]?.reason).toMatch(/文案键冲突:board\.title 由 外壳 与 board/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('没带文案的那份直接跳过,不算问题', () => {
|
|
116
|
+
const { dictionaries, problems } = mergeMessagesSkipping(base, [{ unit: 'nothing' }]);
|
|
117
|
+
|
|
118
|
+
expect(dictionaries).toEqual(base);
|
|
119
|
+
expect(problems).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/i18n/merge.ts
CHANGED
|
@@ -52,3 +52,39 @@ export function mergeMessages(
|
|
|
52
52
|
|
|
53
53
|
return { zh, en };
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export interface MessagesProblem {
|
|
57
|
+
readonly unit: string;
|
|
58
|
+
readonly reason: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MessagesMerge {
|
|
62
|
+
readonly dictionaries: Dictionaries;
|
|
63
|
+
readonly problems: readonly MessagesProblem[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function reasonOf(error: unknown): string {
|
|
67
|
+
if (error instanceof Error) return error.message;
|
|
68
|
+
return typeof error === 'string' ? error : String(error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function mergeMessagesSkipping(
|
|
72
|
+
base: Dictionaries,
|
|
73
|
+
contributions: readonly MessagesContribution[],
|
|
74
|
+
): MessagesMerge {
|
|
75
|
+
let dictionaries = base;
|
|
76
|
+
const accepted: MessagesContribution[] = [];
|
|
77
|
+
const problems: MessagesProblem[] = [];
|
|
78
|
+
|
|
79
|
+
for (const item of contributions) {
|
|
80
|
+
if (item.messages === undefined) continue;
|
|
81
|
+
try {
|
|
82
|
+
dictionaries = mergeMessages(base, [...accepted, item]);
|
|
83
|
+
accepted.push(item);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
problems.push({ unit: item.unit, reason: reasonOf(error) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { dictionaries, problems };
|
|
90
|
+
}
|
package/src/i18n/zh.ts
CHANGED
|
@@ -8,6 +8,20 @@ export const zh = {
|
|
|
8
8
|
'project.none': '未选择项目',
|
|
9
9
|
'project.empty': '还没有项目。用下面的「添加项目」选一个目录。',
|
|
10
10
|
'project.add': '添加项目…',
|
|
11
|
+
'project.search.label': '搜索工作区',
|
|
12
|
+
'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
|
|
13
|
+
'project.search.empty': '没有匹配「{query}」的工作区。',
|
|
14
|
+
'project.rename.row': '改「{name}」的中文名',
|
|
15
|
+
'project.rename.current': '改当前工作区的中文名…',
|
|
16
|
+
'project.rename.title': '给「{name}」配一个中文名',
|
|
17
|
+
'project.rename.description': '显示在目录名后面。改的是工作区本身的名字。',
|
|
18
|
+
'project.rename.field': '中文名',
|
|
19
|
+
'project.rename.placeholder': '例如:工作台外壳',
|
|
20
|
+
'project.rename.hint': '留空 = 只显示目录名',
|
|
21
|
+
'project.rename.save': '保存',
|
|
22
|
+
'project.rename.saving': '保存中…',
|
|
23
|
+
'project.rename.cancel': '取消',
|
|
24
|
+
'project.rename.failed': '改名失败了,再试一次。',
|
|
11
25
|
'session.new': '新会话',
|
|
12
26
|
'session.new.label': '在当前项目新建会话',
|
|
13
27
|
'session.empty': '这个项目还没有会话。',
|
|
@@ -38,6 +52,7 @@ export const zh = {
|
|
|
38
52
|
'panels.dragHint': '拖动调整顺序',
|
|
39
53
|
'panels.unknownStep': '这一层没有套件提供',
|
|
40
54
|
'panels.labelWithHint': '{label}({hint})',
|
|
55
|
+
'panels.dep.failed': '⚠ 起不来',
|
|
41
56
|
'panels.dep.missingPlugin': '⚠ 还没装好',
|
|
42
57
|
'panels.dep.missingCommands': '⚠ 需要更新',
|
|
43
58
|
'panels.dep.unchecked': '⚠ 检测不了',
|
|
@@ -82,7 +97,7 @@ export const zh = {
|
|
|
82
97
|
'panels.market.empty': '暂无可用模块',
|
|
83
98
|
'panels.market.installedSection': '已装',
|
|
84
99
|
'panels.market.moduleOutdated': '有新版',
|
|
85
|
-
'panels.market.moduleRestartHint': '
|
|
100
|
+
'panels.market.moduleRestartHint': '装好了但还没生效 —— 重启一次 dsh 就会出来',
|
|
86
101
|
'panels.market.restartNeeded': '新版装好了,重启一次才生效。',
|
|
87
102
|
'panels.market.foot': '装好后到那个项目的面板页勾选它,模块才会出现。',
|
|
88
103
|
'panels.market.recheck': '重新检查更新',
|
|
@@ -231,6 +246,8 @@ export const zh = {
|
|
|
231
246
|
'blocker.exec.hint': '确认 opencli 已安装且在 PATH 里(which opencli)。',
|
|
232
247
|
'blocker.unknown.title': '出了点问题',
|
|
233
248
|
'panels.reading': '正在读取面板配置…',
|
|
249
|
+
'panels.cardBroken': '这个模块起不来:{unit}({reason})',
|
|
250
|
+
'panels.cardRetry': '重试',
|
|
234
251
|
'panels.empty': '面板里还没有模块',
|
|
235
252
|
'panels.emptyHint': '也可以直接跟 agent 说:「把某个模块加到面板」。',
|
|
236
253
|
'env.title': '准备工作台',
|
|
@@ -269,6 +286,8 @@ export const zh = {
|
|
|
269
286
|
'env.step.warm': '预热站点登录态',
|
|
270
287
|
'env.warm.done': '已预热 {n} 个站点',
|
|
271
288
|
'env.warm.short': '这次没热上 —— 不影响用,面板第一次读取会自己补',
|
|
289
|
+
'env.unit.failed': '这个模块起不来',
|
|
290
|
+
'env.unit.failedHint': '它自己装配时报了错,别的模块不受影响。',
|
|
272
291
|
'env.unit.missingPlugin': '这个模块还没装好',
|
|
273
292
|
'env.unit.missingCommandsHint': '更新之后就能用了。',
|
|
274
293
|
'env.unit.unchecked': '检测不了:拿不到执行通道',
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './components/Activity.js';
|
|
|
14
14
|
export * from './components/ApprovalDetail.js';
|
|
15
15
|
export * from './components/ApprovalPlan.js';
|
|
16
16
|
export * from './components/Card.js';
|
|
17
|
+
export * from './components/CardBoundary.js';
|
|
17
18
|
export * from './components/CardHead.js';
|
|
18
19
|
export * from './components/ChoiceGroup.js';
|
|
19
20
|
export * from './components/LongTaskRow.js';
|
package/src/lib/react.ts
CHANGED
|
@@ -6,6 +6,8 @@ export const h = react.createElement;
|
|
|
6
6
|
|
|
7
7
|
export const Fragment = react.Fragment;
|
|
8
8
|
|
|
9
|
+
export const Component = react.Component;
|
|
10
|
+
|
|
9
11
|
export const useState = react.useState;
|
|
10
12
|
export const useEffect = react.useEffect;
|
|
11
13
|
export const useLayoutEffect = react.useLayoutEffect;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { h } from '../lib/react.js';
|
|
2
|
-
export function StateDot({ state, size }) {
|
|
3
|
-
if (state === 'ongoing') {
|
|
4
|
-
return (h("svg", { className: "wb-loading", viewBox: "0 0 24 24", width: size, height: size, "aria-hidden": "true", style: { display: 'block' } },
|
|
5
|
-
h("g", { className: "wb-loadingMotion" },
|
|
6
|
-
h("circle", { className: "wb-loadingTrack", cx: 12, cy: 12, r: 9.5 }),
|
|
7
|
-
h("circle", { className: "wb-loadingArc", cx: 12, cy: 12, r: 9.5 }))));
|
|
8
|
-
}
|
|
9
|
-
return (h("span", { className: "wb-stateDot", "data-state": state, style: { width: size, height: size }, "aria-hidden": "true" }));
|
|
10
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { DshShell } from '../../api/dsh.js';
|
|
2
|
-
import type { Opencli } from '../../host/opencli.js';
|
|
3
|
-
export interface ShellCall {
|
|
4
|
-
command: string;
|
|
5
|
-
workdir?: string;
|
|
6
|
-
timeoutMs?: number;
|
|
7
|
-
onExpiry?: string;
|
|
8
|
-
stdoutMaxBytes?: number;
|
|
9
|
-
}
|
|
10
|
-
export interface ShellReply {
|
|
11
|
-
stdout?: string;
|
|
12
|
-
stderr?: string;
|
|
13
|
-
exitCode?: number | null;
|
|
14
|
-
timedOut?: boolean;
|
|
15
|
-
aborted?: boolean;
|
|
16
|
-
failWith?: Error;
|
|
17
|
-
}
|
|
18
|
-
export declare function createFakeShell(replies?: ShellReply[] | ((call: ShellCall, index: number) => ShellReply)): {
|
|
19
|
-
shell: DshShell;
|
|
20
|
-
calls: ShellCall[];
|
|
21
|
-
peak: () => number;
|
|
22
|
-
commands: () => string[];
|
|
23
|
-
};
|
|
24
|
-
export interface OpencliCall {
|
|
25
|
-
args: readonly string[];
|
|
26
|
-
key?: string;
|
|
27
|
-
workdir?: string;
|
|
28
|
-
refresh?: boolean;
|
|
29
|
-
}
|
|
30
|
-
export declare function createFakeOpencli(replies?: Array<unknown | Error> | ((call: OpencliCall, index: number) => unknown | Error)): {
|
|
31
|
-
opencli: Opencli;
|
|
32
|
-
calls: OpencliCall[];
|
|
33
|
-
keys: () => (string | undefined)[];
|
|
34
|
-
};
|
|
35
|
-
export declare function createFakeLogger(): {
|
|
36
|
-
logger: {
|
|
37
|
-
info: () => void;
|
|
38
|
-
warn: (message: string) => void;
|
|
39
|
-
};
|
|
40
|
-
warnings: string[];
|
|
41
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
const tick = () => delay(0);
|
|
3
|
-
export function createFakeShell(replies = []) {
|
|
4
|
-
const calls = [];
|
|
5
|
-
let running = 0;
|
|
6
|
-
let peak = 0;
|
|
7
|
-
const replyAt = (index) => typeof replies === 'function' ? replies(calls[index], index) : (replies[index] ?? {});
|
|
8
|
-
const shell = {
|
|
9
|
-
resolve(request) {
|
|
10
|
-
calls.push(request);
|
|
11
|
-
return request;
|
|
12
|
-
},
|
|
13
|
-
async execute() {
|
|
14
|
-
const index = calls.length - 1;
|
|
15
|
-
const reply = replyAt(index);
|
|
16
|
-
if (reply.failWith !== undefined)
|
|
17
|
-
throw reply.failWith;
|
|
18
|
-
running += 1;
|
|
19
|
-
peak = Math.max(peak, running);
|
|
20
|
-
return {
|
|
21
|
-
async result() {
|
|
22
|
-
await tick();
|
|
23
|
-
running -= 1;
|
|
24
|
-
return {
|
|
25
|
-
exitCode: reply.exitCode ?? 0,
|
|
26
|
-
timedOut: reply.timedOut ?? false,
|
|
27
|
-
aborted: reply.aborted ?? false,
|
|
28
|
-
stdout: { text: reply.stdout ?? '', truncated: false },
|
|
29
|
-
stderr: { text: reply.stderr ?? '', truncated: false },
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
return {
|
|
36
|
-
shell,
|
|
37
|
-
calls,
|
|
38
|
-
peak: () => peak,
|
|
39
|
-
commands: () => calls.map((call) => call.command),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
export function createFakeOpencli(replies = []) {
|
|
43
|
-
const calls = [];
|
|
44
|
-
const opencli = {
|
|
45
|
-
async json(args, options = {}) {
|
|
46
|
-
const call = {
|
|
47
|
-
args,
|
|
48
|
-
...(options.key === undefined ? {} : { key: options.key }),
|
|
49
|
-
...(options.workdir === undefined ? {} : { workdir: options.workdir }),
|
|
50
|
-
};
|
|
51
|
-
calls.push(call);
|
|
52
|
-
const index = calls.length - 1;
|
|
53
|
-
const reply = typeof replies === 'function' ? replies(call, index) : (replies[index] ?? []);
|
|
54
|
-
if (reply instanceof Error)
|
|
55
|
-
throw reply;
|
|
56
|
-
return { value: reply, at: Date.now() };
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
return {
|
|
60
|
-
opencli,
|
|
61
|
-
calls,
|
|
62
|
-
keys: () => calls.map((call) => call.key),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export function createFakeLogger() {
|
|
66
|
-
const warnings = [];
|
|
67
|
-
return {
|
|
68
|
-
logger: {
|
|
69
|
-
info: () => { },
|
|
70
|
-
warn: (message) => {
|
|
71
|
-
warnings.push(message);
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
warnings,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { createFakeShell } from './fakes';
|
|
2
|
-
export declare const GOOD_BRIDGE_REPORT: string;
|
|
3
|
-
export declare const ALL_OK: {
|
|
4
|
-
stdout: string;
|
|
5
|
-
}[];
|
|
6
|
-
export declare const STALE_DAEMON_BRIDGE: string;
|
|
7
|
-
export declare const BROKEN_BRIDGE: string;
|
|
8
|
-
export declare function build(replies: Parameters<typeof createFakeShell>[0], install?: () => Promise<{
|
|
9
|
-
ok: boolean;
|
|
10
|
-
detail: string;
|
|
11
|
-
}>, warmReads?: () => readonly (readonly string[])[]): {
|
|
12
|
-
fake: {
|
|
13
|
-
shell: import("../..").DshShell;
|
|
14
|
-
calls: import("./fakes").ShellCall[];
|
|
15
|
-
peak: () => number;
|
|
16
|
-
commands: () => string[];
|
|
17
|
-
};
|
|
18
|
-
preflight: import("../preflight").Preflight;
|
|
19
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { createPreflight } from '../preflight';
|
|
2
|
-
import { createFakeShell } from './fakes';
|
|
3
|
-
export const GOOD_BRIDGE_REPORT = [
|
|
4
|
-
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
5
|
-
'',
|
|
6
|
-
'[OK] Daemon: running on port 19825 (v1.8.7)',
|
|
7
|
-
'[OK] Extension: connected (v1.0.24)',
|
|
8
|
-
'[OK] Connectivity: connected in 0.0s',
|
|
9
|
-
'',
|
|
10
|
-
'Everything looks good!',
|
|
11
|
-
].join('\n');
|
|
12
|
-
export const ALL_OK = [{ stdout: '1.8.8\n' }, { stdout: GOOD_BRIDGE_REPORT }];
|
|
13
|
-
export const STALE_DAEMON_BRIDGE = [
|
|
14
|
-
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
15
|
-
'',
|
|
16
|
-
'[STALE] Daemon: running on port 19825 (v1.8.8 != CLI v1.8.7)',
|
|
17
|
-
'[OK] Extension: connected (v1.0.24)',
|
|
18
|
-
'[OK] Connectivity: connected in 0.0s',
|
|
19
|
-
'',
|
|
20
|
-
'Issues:',
|
|
21
|
-
' • Stale daemon detected: daemon v1.8.8 != CLI v1.8.7.',
|
|
22
|
-
' Run: opencli daemon restart',
|
|
23
|
-
].join('\n');
|
|
24
|
-
export const BROKEN_BRIDGE = [
|
|
25
|
-
'opencli v1.8.7 doctor (node v24.15.0)',
|
|
26
|
-
'',
|
|
27
|
-
'[MISSING] Daemon: not running',
|
|
28
|
-
'[MISSING] Extension: not connected',
|
|
29
|
-
'[FAIL] Connectivity: failed (daemon not reachable)',
|
|
30
|
-
'',
|
|
31
|
-
'Issues:',
|
|
32
|
-
" • Daemon is not running. It should start automatically when you run an opencli browser command.",
|
|
33
|
-
' • Daemon is running but the Chrome/Chromium extension is not connected.',
|
|
34
|
-
'If the extension is not installed:',
|
|
35
|
-
' 1. Download from https://github.com/jackwener/opencli/releases',
|
|
36
|
-
' 2. Open chrome://extensions/ → Enable Developer Mode',
|
|
37
|
-
' 3. Click "Load unpacked" → select the extension folder',
|
|
38
|
-
].join('\n');
|
|
39
|
-
export function build(replies, install, warmReads) {
|
|
40
|
-
const fake = createFakeShell(replies);
|
|
41
|
-
const preflight = createPreflight({
|
|
42
|
-
shell: fake.shell,
|
|
43
|
-
...(install === undefined ? {} : { install }),
|
|
44
|
-
...(warmReads === undefined ? {} : { warmReads }),
|
|
45
|
-
});
|
|
46
|
-
return { fake, preflight };
|
|
47
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type ContextRef } from '@dsh-workbench/shared';
|
|
2
|
-
import type { ContextRenderer } from './unit-face';
|
|
3
|
-
export declare const CONTEXT_SOURCE_KIND = "workbench";
|
|
4
|
-
export interface ScannableMessage {
|
|
5
|
-
readonly source?: {
|
|
6
|
-
readonly kind?: string;
|
|
7
|
-
};
|
|
8
|
-
readonly content?: readonly {
|
|
9
|
-
readonly type?: string;
|
|
10
|
-
readonly text?: string;
|
|
11
|
-
}[];
|
|
12
|
-
}
|
|
13
|
-
export type { ContextRenderer };
|
|
14
|
-
export declare function collectContextRefs(messages: readonly ScannableMessage[]): ContextRef[];
|
|
15
|
-
export declare function buildContextMessage(text: string, summary: string): unknown;
|
|
16
|
-
export interface ContextInjector {
|
|
17
|
-
inject(messages: readonly ScannableMessage[], agent: unknown): Promise<unknown | undefined>;
|
|
18
|
-
}
|
|
19
|
-
export declare function createContextInjector(options: {
|
|
20
|
-
renderers: ReadonlyMap<string, ContextRenderer>;
|
|
21
|
-
logger?: {
|
|
22
|
-
warn?(message: string): void;
|
|
23
|
-
} | undefined;
|
|
24
|
-
}): ContextInjector;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { encodeRef, parseMarkers } from '@dsh-workbench/shared';
|
|
3
|
-
export const CONTEXT_SOURCE_KIND = 'workbench';
|
|
4
|
-
export function collectContextRefs(messages) {
|
|
5
|
-
const seen = new Set();
|
|
6
|
-
const refs = [];
|
|
7
|
-
for (const message of messages) {
|
|
8
|
-
if (message.source?.kind !== 'user')
|
|
9
|
-
continue;
|
|
10
|
-
for (const block of message.content ?? []) {
|
|
11
|
-
if (block.type !== 'text' || typeof block.text !== 'string')
|
|
12
|
-
continue;
|
|
13
|
-
for (const ref of parseMarkers(block.text)) {
|
|
14
|
-
const key = encodeRef(ref);
|
|
15
|
-
if (seen.has(key))
|
|
16
|
-
continue;
|
|
17
|
-
seen.add(key);
|
|
18
|
-
refs.push(ref);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return refs;
|
|
23
|
-
}
|
|
24
|
-
export function buildContextMessage(text, summary) {
|
|
25
|
-
return {
|
|
26
|
-
role: 'user',
|
|
27
|
-
id: randomUUID(),
|
|
28
|
-
content: [{ type: 'text', text }],
|
|
29
|
-
source: { kind: CONTEXT_SOURCE_KIND, form: 'notice', summary },
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
export function createContextInjector(options) {
|
|
33
|
-
const done = new WeakMap();
|
|
34
|
-
return {
|
|
35
|
-
async inject(messages, agent) {
|
|
36
|
-
const refs = collectContextRefs(messages);
|
|
37
|
-
if (refs.length === 0)
|
|
38
|
-
return undefined;
|
|
39
|
-
const key = typeof agent === 'object' && agent !== null ? agent : undefined;
|
|
40
|
-
const seen = key === undefined ? undefined : (done.get(key) ?? new Set());
|
|
41
|
-
if (key !== undefined && seen !== undefined)
|
|
42
|
-
done.set(key, seen);
|
|
43
|
-
const fresh = [];
|
|
44
|
-
for (const ref of refs) {
|
|
45
|
-
const encoded = encodeRef(ref);
|
|
46
|
-
if (seen?.has(encoded) === true)
|
|
47
|
-
continue;
|
|
48
|
-
seen?.add(encoded);
|
|
49
|
-
fresh.push(ref);
|
|
50
|
-
}
|
|
51
|
-
if (fresh.length === 0)
|
|
52
|
-
return undefined;
|
|
53
|
-
const texts = [];
|
|
54
|
-
for (const ref of fresh) {
|
|
55
|
-
const render = options.renderers.get(ref.kind);
|
|
56
|
-
if (render === undefined)
|
|
57
|
-
continue;
|
|
58
|
-
let text;
|
|
59
|
-
try {
|
|
60
|
-
text = await render(ref);
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
options.logger?.warn?.(`[dsh-workbench] 渲染对话引用失败:${String(error)}`);
|
|
64
|
-
text = `工作台面板引用了一条内容(${encodeRef(ref)}),但当前渲染不出来(项目未选中或数据源不可用)。`;
|
|
65
|
-
}
|
|
66
|
-
if (text !== '')
|
|
67
|
-
texts.push(text);
|
|
68
|
-
}
|
|
69
|
-
if (texts.length === 0)
|
|
70
|
-
return undefined;
|
|
71
|
-
return buildContextMessage(texts.join('\n\n'), `工作台引用:${fresh.length} 条`);
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
package/dist/host/events.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { DshHostContext, DshRequest, DshResponse } from '@dsh-workbench/shared';
|
|
2
|
-
import type { FactLog } from './facts';
|
|
3
|
-
export interface FactStream {
|
|
4
|
-
handler(req: DshRequest, res: DshResponse): void;
|
|
5
|
-
closeAll(): void;
|
|
6
|
-
size(): number;
|
|
7
|
-
}
|
|
8
|
-
export declare function createFactStream(log: FactLog): FactStream;
|
|
9
|
-
export declare function registerFactStream(ctx: DshHostContext, log: FactLog): () => void;
|