@dsh-workbench/shared 0.1.9 → 0.1.11
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 +26 -3
- package/dist/api/dsh.d.ts +10 -0
- 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/tool-approval.d.ts +10 -1
- package/dist/host/tool-approval.js +24 -169
- package/dist/host/unit-face.d.ts +2 -1
- package/dist/host/unit-libs.d.ts +1 -1
- package/dist/i18n/en.js +20 -0
- package/dist/i18n/index.d.ts +21 -1
- package/dist/i18n/merge.d.ts +9 -0
- package/dist/i18n/merge.js +22 -0
- package/dist/i18n/zh.d.ts +20 -0
- package/dist/i18n/zh.js +20 -0
- 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 +27 -1
- package/src/api/dsh.ts +5 -0
- 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/tool-approval.ts +41 -187
- package/src/host/unit-face.ts +2 -1
- package/src/host/unit-libs.ts +1 -1
- package/src/i18n/en.ts +20 -0
- package/src/i18n/merge.test.ts +62 -1
- package/src/i18n/merge.ts +36 -0
- package/src/i18n/zh.ts +20 -0
- package/src/index.ts +1 -0
- package/src/lib/react.ts +2 -0
package/dist/i18n/merge.js
CHANGED
|
@@ -38,3 +38,25 @@ export function mergeMessages(base, contributions) {
|
|
|
38
38
|
}
|
|
39
39
|
return { zh, en };
|
|
40
40
|
}
|
|
41
|
+
function reasonOf(error) {
|
|
42
|
+
if (error instanceof Error)
|
|
43
|
+
return error.message;
|
|
44
|
+
return typeof error === 'string' ? error : String(error);
|
|
45
|
+
}
|
|
46
|
+
export function mergeMessagesSkipping(base, contributions) {
|
|
47
|
+
let dictionaries = base;
|
|
48
|
+
const accepted = [];
|
|
49
|
+
const problems = [];
|
|
50
|
+
for (const item of contributions) {
|
|
51
|
+
if (item.messages === undefined)
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
dictionaries = mergeMessages(base, [...accepted, item]);
|
|
55
|
+
accepted.push(item);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
problems.push({ unit: item.unit, reason: reasonOf(error) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { dictionaries, problems };
|
|
62
|
+
}
|
package/dist/i18n/zh.d.ts
CHANGED
|
@@ -7,6 +7,20 @@ export declare const zh: {
|
|
|
7
7
|
readonly 'project.none': "未选择项目";
|
|
8
8
|
readonly 'project.empty': "还没有项目。用下面的「添加项目」选一个目录。";
|
|
9
9
|
readonly 'project.add': "添加项目…";
|
|
10
|
+
readonly 'project.search.label': "搜索工作区";
|
|
11
|
+
readonly 'project.search.placeholder': "搜索工作区(目录名 / 中文名 / 路径)";
|
|
12
|
+
readonly 'project.search.empty': "没有匹配「{query}」的工作区。";
|
|
13
|
+
readonly 'project.rename.row': "改「{name}」的中文名";
|
|
14
|
+
readonly 'project.rename.current': "改当前工作区的中文名…";
|
|
15
|
+
readonly 'project.rename.title': "给「{name}」配一个中文名";
|
|
16
|
+
readonly 'project.rename.description': "显示在目录名后面。改的是工作区本身的名字。";
|
|
17
|
+
readonly 'project.rename.field': "中文名";
|
|
18
|
+
readonly 'project.rename.placeholder': "例如:工作台外壳";
|
|
19
|
+
readonly 'project.rename.hint': "留空 = 只显示目录名";
|
|
20
|
+
readonly 'project.rename.save': "保存";
|
|
21
|
+
readonly 'project.rename.saving': "保存中…";
|
|
22
|
+
readonly 'project.rename.cancel': "取消";
|
|
23
|
+
readonly 'project.rename.failed': "改名失败了,再试一次。";
|
|
10
24
|
readonly 'session.new': "新会话";
|
|
11
25
|
readonly 'session.new.label': "在当前项目新建会话";
|
|
12
26
|
readonly 'session.empty': "这个项目还没有会话。";
|
|
@@ -30,11 +44,13 @@ export declare const zh: {
|
|
|
30
44
|
readonly 'panels.enabled': "这个项目已启用";
|
|
31
45
|
readonly 'panels.disabled': "这个项目未启用";
|
|
32
46
|
readonly 'panels.choose': "选择模块";
|
|
47
|
+
readonly 'panels.buildModule': "造一个模块";
|
|
33
48
|
readonly 'panels.failed': "改配置失败了,再试一次。";
|
|
34
49
|
readonly 'panels.hintEffect': "停用 = agent 拿不到相关数据";
|
|
35
50
|
readonly 'panels.dragHint': "拖动调整顺序";
|
|
36
51
|
readonly 'panels.unknownStep': "这一层没有套件提供";
|
|
37
52
|
readonly 'panels.labelWithHint': "{label}({hint})";
|
|
53
|
+
readonly 'panels.dep.failed': "⚠ 起不来";
|
|
38
54
|
readonly 'panels.dep.missingPlugin': "⚠ 还没装好";
|
|
39
55
|
readonly 'panels.dep.missingCommands': "⚠ 需要更新";
|
|
40
56
|
readonly 'panels.dep.unchecked': "⚠ 检测不了";
|
|
@@ -228,6 +244,8 @@ export declare const zh: {
|
|
|
228
244
|
readonly 'blocker.exec.hint': "确认 opencli 已安装且在 PATH 里(which opencli)。";
|
|
229
245
|
readonly 'blocker.unknown.title': "出了点问题";
|
|
230
246
|
readonly 'panels.reading': "正在读取面板配置…";
|
|
247
|
+
readonly 'panels.cardBroken': "这个模块起不来:{unit}({reason})";
|
|
248
|
+
readonly 'panels.cardRetry': "重试";
|
|
231
249
|
readonly 'panels.empty': "面板里还没有模块";
|
|
232
250
|
readonly 'panels.emptyHint': "也可以直接跟 agent 说:「把某个模块加到面板」。";
|
|
233
251
|
readonly 'env.title': "准备工作台";
|
|
@@ -266,6 +284,8 @@ export declare const zh: {
|
|
|
266
284
|
readonly 'env.step.warm': "预热站点登录态";
|
|
267
285
|
readonly 'env.warm.done': "已预热 {n} 个站点";
|
|
268
286
|
readonly 'env.warm.short': "这次没热上 —— 不影响用,面板第一次读取会自己补";
|
|
287
|
+
readonly 'env.unit.failed': "这个模块起不来";
|
|
288
|
+
readonly 'env.unit.failedHint': "它自己装配时报了错,别的模块不受影响。";
|
|
269
289
|
readonly 'env.unit.missingPlugin': "这个模块还没装好";
|
|
270
290
|
readonly 'env.unit.missingCommandsHint': "更新之后就能用了。";
|
|
271
291
|
readonly 'env.unit.unchecked': "检测不了:拿不到执行通道";
|
package/dist/i18n/zh.js
CHANGED
|
@@ -7,6 +7,20 @@ export const zh = {
|
|
|
7
7
|
'project.none': '未选择项目',
|
|
8
8
|
'project.empty': '还没有项目。用下面的「添加项目」选一个目录。',
|
|
9
9
|
'project.add': '添加项目…',
|
|
10
|
+
'project.search.label': '搜索工作区',
|
|
11
|
+
'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
|
|
12
|
+
'project.search.empty': '没有匹配「{query}」的工作区。',
|
|
13
|
+
'project.rename.row': '改「{name}」的中文名',
|
|
14
|
+
'project.rename.current': '改当前工作区的中文名…',
|
|
15
|
+
'project.rename.title': '给「{name}」配一个中文名',
|
|
16
|
+
'project.rename.description': '显示在目录名后面。改的是工作区本身的名字。',
|
|
17
|
+
'project.rename.field': '中文名',
|
|
18
|
+
'project.rename.placeholder': '例如:工作台外壳',
|
|
19
|
+
'project.rename.hint': '留空 = 只显示目录名',
|
|
20
|
+
'project.rename.save': '保存',
|
|
21
|
+
'project.rename.saving': '保存中…',
|
|
22
|
+
'project.rename.cancel': '取消',
|
|
23
|
+
'project.rename.failed': '改名失败了,再试一次。',
|
|
10
24
|
'session.new': '新会话',
|
|
11
25
|
'session.new.label': '在当前项目新建会话',
|
|
12
26
|
'session.empty': '这个项目还没有会话。',
|
|
@@ -30,11 +44,13 @@ export const zh = {
|
|
|
30
44
|
'panels.enabled': '这个项目已启用',
|
|
31
45
|
'panels.disabled': '这个项目未启用',
|
|
32
46
|
'panels.choose': '选择模块',
|
|
47
|
+
'panels.buildModule': '造一个模块',
|
|
33
48
|
'panels.failed': '改配置失败了,再试一次。',
|
|
34
49
|
'panels.hintEffect': '停用 = agent 拿不到相关数据',
|
|
35
50
|
'panels.dragHint': '拖动调整顺序',
|
|
36
51
|
'panels.unknownStep': '这一层没有套件提供',
|
|
37
52
|
'panels.labelWithHint': '{label}({hint})',
|
|
53
|
+
'panels.dep.failed': '⚠ 起不来',
|
|
38
54
|
'panels.dep.missingPlugin': '⚠ 还没装好',
|
|
39
55
|
'panels.dep.missingCommands': '⚠ 需要更新',
|
|
40
56
|
'panels.dep.unchecked': '⚠ 检测不了',
|
|
@@ -228,6 +244,8 @@ export const zh = {
|
|
|
228
244
|
'blocker.exec.hint': '确认 opencli 已安装且在 PATH 里(which opencli)。',
|
|
229
245
|
'blocker.unknown.title': '出了点问题',
|
|
230
246
|
'panels.reading': '正在读取面板配置…',
|
|
247
|
+
'panels.cardBroken': '这个模块起不来:{unit}({reason})',
|
|
248
|
+
'panels.cardRetry': '重试',
|
|
231
249
|
'panels.empty': '面板里还没有模块',
|
|
232
250
|
'panels.emptyHint': '也可以直接跟 agent 说:「把某个模块加到面板」。',
|
|
233
251
|
'env.title': '准备工作台',
|
|
@@ -266,6 +284,8 @@ export const zh = {
|
|
|
266
284
|
'env.step.warm': '预热站点登录态',
|
|
267
285
|
'env.warm.done': '已预热 {n} 个站点',
|
|
268
286
|
'env.warm.short': '这次没热上 —— 不影响用,面板第一次读取会自己补',
|
|
287
|
+
'env.unit.failed': '这个模块起不来',
|
|
288
|
+
'env.unit.failedHint': '它自己装配时报了错,别的模块不受影响。',
|
|
269
289
|
'env.unit.missingPlugin': '这个模块还没装好',
|
|
270
290
|
'env.unit.missingCommandsHint': '更新之后就能用了。',
|
|
271
291
|
'env.unit.unchecked': '检测不了:拿不到执行通道',
|
package/dist/index.d.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/dist/index.js
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/dist/lib/react.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as ReactTypes from 'react';
|
|
2
2
|
export declare const h: typeof ReactTypes.createElement;
|
|
3
3
|
export declare const Fragment: ReactTypes.ExoticComponent<ReactTypes.FragmentProps>;
|
|
4
|
+
export declare const Component: typeof ReactTypes.Component;
|
|
4
5
|
export declare const useState: typeof ReactTypes.useState;
|
|
5
6
|
export declare const useEffect: typeof ReactTypes.useEffect;
|
|
6
7
|
export declare const useLayoutEffect: typeof ReactTypes.useLayoutEffect;
|
package/dist/lib/react.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const react = require('react');
|
|
2
2
|
export const h = react.createElement;
|
|
3
3
|
export const Fragment = react.Fragment;
|
|
4
|
+
export const Component = react.Component;
|
|
4
5
|
export const useState = react.useState;
|
|
5
6
|
export const useEffect = react.useEffect;
|
|
6
7
|
export const useLayoutEffect = react.useLayoutEffect;
|
package/package.json
CHANGED
|
@@ -141,6 +141,19 @@ describe('blockerOfCapability', () => {
|
|
|
141
141
|
expect(blockerOfCapability(capability({ status: 'ok', plugin: 'coding' }), t)).toBeUndefined();
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
it('★ 起不来(模块自己装配炸了)→ 只说"起不来" + 把宿主给的原因原样附上,不给装/卸的按钮', () => {
|
|
145
|
+
const blocker = blockerOfCapability(
|
|
146
|
+
capability({ status: 'failed', detail: '模块自己的 bug:kit.components.StateBlock 取不到' }),
|
|
147
|
+
t,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(blocker?.id).toBe('unit-mount-failed');
|
|
151
|
+
expect(blocker?.title).toBe('env.unit.failed');
|
|
152
|
+
expect(blocker?.detail).toBe('模块自己的 bug:kit.components.StateBlock 取不到');
|
|
153
|
+
expect(blocker?.hint).toBe('env.unit.failedHint');
|
|
154
|
+
expect(blocker?.actions.map((action) => action.kind)).toEqual(['retry']);
|
|
155
|
+
});
|
|
156
|
+
|
|
144
157
|
it('★ 没装好 → 标题只说"这个模块还没装好",**不点名插件**(用户不认识插件)', () => {
|
|
145
158
|
const blocker = blockerOfCapability(
|
|
146
159
|
capability({
|
package/src/api/blocker-table.ts
CHANGED
|
@@ -122,6 +122,17 @@ export function blockerOfCapability(capability: CapabilityPayload, t: Translate)
|
|
|
122
122
|
...(capability.command === undefined || sourceUnknown ? [] : [copyAction(t, capability.command)]),
|
|
123
123
|
];
|
|
124
124
|
|
|
125
|
+
if (capability.status === 'failed') {
|
|
126
|
+
return {
|
|
127
|
+
id: 'unit-mount-failed',
|
|
128
|
+
severity: 'block',
|
|
129
|
+
title: t('env.unit.failed'),
|
|
130
|
+
hint: t('env.unit.failedHint'),
|
|
131
|
+
...(capability.detail === undefined ? {} : { detail: capability.detail }),
|
|
132
|
+
actions: [retryAction(t)],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
if (capability.status === 'missing-plugin') {
|
|
126
137
|
return {
|
|
127
138
|
id: 'unit-missing-plugin',
|
package/src/api/dsh-client.ts
CHANGED
|
@@ -113,8 +113,15 @@ export interface DshUiWorkspace {
|
|
|
113
113
|
pickDirectory(): Promise<string | null>;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export interface DshWorkspaceWritten {
|
|
117
|
+
readonly workspaceId?: string;
|
|
118
|
+
readonly title?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
116
121
|
export interface DshWorkspaces {
|
|
117
|
-
|
|
122
|
+
readonly list: DshObservable<WorkspaceSnapshot>;
|
|
123
|
+
create(request: { path: string }): Promise<DshWorkspaceWritten | undefined>;
|
|
124
|
+
rename(workspaceId: string, title: string): Promise<DshWorkspaceWritten>;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
export interface DshReferenceInsert {
|
|
@@ -161,6 +168,12 @@ export interface DshConversationService {
|
|
|
161
168
|
export interface DshSessionsService {
|
|
162
169
|
readonly list: { getSnapshot(): SessionsSnapshot };
|
|
163
170
|
scope(sessionId: string): unknown | undefined;
|
|
171
|
+
create?(request: {
|
|
172
|
+
readonly workspaceId?: string;
|
|
173
|
+
readonly sessionId?: string;
|
|
174
|
+
readonly cwd?: string;
|
|
175
|
+
readonly agentPreset?: string;
|
|
176
|
+
}): Promise<unknown>;
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
export interface DshObservable<T> {
|
|
@@ -176,6 +189,18 @@ export interface DshSidebarRight {
|
|
|
176
189
|
openTab(kind: string, options?: unknown): void;
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
export interface DshSessionsRemote {
|
|
193
|
+
create(request: {
|
|
194
|
+
readonly workspaceId?: string;
|
|
195
|
+
readonly cwd?: string;
|
|
196
|
+
readonly agentPreset?: string;
|
|
197
|
+
}): Promise<{ readonly sessionId: string }>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface DshClientRemote {
|
|
201
|
+
readonly sessions: DshSessionsRemote;
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
export interface DshClientContext {
|
|
180
205
|
get<T = unknown>(name: string): T | undefined;
|
|
181
206
|
provide(name: string, value: unknown): void;
|
|
@@ -186,4 +211,5 @@ export interface DshClientContext {
|
|
|
186
211
|
readonly uiWorkspace: DshUiWorkspace;
|
|
187
212
|
readonly workspaces: DshWorkspaces;
|
|
188
213
|
readonly shortcuts: DshShortcutCatalog;
|
|
214
|
+
readonly remote: DshClientRemote;
|
|
189
215
|
}
|
package/src/api/dsh.ts
CHANGED
|
@@ -102,6 +102,7 @@ export interface DshHostContext {
|
|
|
102
102
|
get<T = unknown>(name: string): T | undefined;
|
|
103
103
|
provide(name: string, value: unknown): () => void;
|
|
104
104
|
effect(effect: () => void | (() => void), label?: string): () => void;
|
|
105
|
+
inject(names: readonly string[], effect: (scoped: DshHostContext) => void): () => void;
|
|
105
106
|
readonly logger?: DshLogger;
|
|
106
107
|
readonly webServer: { register(route: DshWebRoute): () => void };
|
|
107
108
|
}
|
|
@@ -130,8 +131,12 @@ export interface DshShell {
|
|
|
130
131
|
|
|
131
132
|
export interface DshWorkspaceRegistry {
|
|
132
133
|
get(id: string): { id: string; path?: string; title?: string } | undefined;
|
|
134
|
+
resolveByPath(path: string): Promise<{ readonly id: unknown } | undefined>;
|
|
135
|
+
create(path: string, title?: string): Promise<{ id?: string; path?: string; title?: string } | undefined>;
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
export type DshHomePath = (relative: string) => string;
|
|
139
|
+
|
|
135
140
|
export interface DshToolDefinition {
|
|
136
141
|
name: string;
|
|
137
142
|
description: string;
|
|
@@ -21,7 +21,7 @@ export interface PluginStatePayload {
|
|
|
21
21
|
|
|
22
22
|
export interface CapabilityPayload {
|
|
23
23
|
readonly unit: string;
|
|
24
|
-
readonly status: 'ok' | 'missing-commands' | 'missing-plugin' | 'unchecked';
|
|
24
|
+
readonly status: 'ok' | 'failed' | 'missing-commands' | 'missing-plugin' | 'unchecked';
|
|
25
25
|
readonly plugin?: string;
|
|
26
26
|
readonly required: readonly string[];
|
|
27
27
|
readonly missing: readonly string[];
|
|
@@ -2,6 +2,7 @@ import type { Translate } from '../api/dsh-client.js';
|
|
|
2
2
|
import type { ApprovalInput, UnitClientFace } from '../components/board-module.js';
|
|
3
3
|
import { h } from '../lib/react.js';
|
|
4
4
|
import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
5
|
+
import { CardBoundary } from './CardBoundary.js';
|
|
5
6
|
import './ApprovalDetail.css';
|
|
6
7
|
|
|
7
8
|
interface ToolCallRoot {
|
|
@@ -53,7 +54,14 @@ export function ApprovalDetail(props: ApprovalDetailProps) {
|
|
|
53
54
|
const command = typeof args.command === 'string' ? args.command : '';
|
|
54
55
|
|
|
55
56
|
const owner = (props.units ?? []).find((face) => face.approval?.tools.includes(input.tool) === true);
|
|
56
|
-
if (owner?.approval !== undefined)
|
|
57
|
+
if (owner?.approval !== undefined) {
|
|
58
|
+
const Render = owner.approval.render;
|
|
59
|
+
return (
|
|
60
|
+
<CardBoundary unit={owner.manifest.id} resetKey={props.callId ?? ''} t={t}>
|
|
61
|
+
<Render input={input} t={t} />
|
|
62
|
+
</CardBoundary>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
57
65
|
|
|
58
66
|
if (command === '') return null;
|
|
59
67
|
return (
|
|
@@ -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
|
+
}
|
|
@@ -22,222 +22,76 @@ export interface ApprovalCall {
|
|
|
22
22
|
readonly workspace?: string | undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export type ApprovalVerdict =
|
|
26
|
+
| { readonly kind: 'read' }
|
|
27
|
+
| {
|
|
28
|
+
readonly kind: 'ask';
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
readonly question: ApprovalDetail;
|
|
31
|
+
readonly fallback: ApprovalDetail;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ApprovalPolicy = (call: ApprovalCall) => ApprovalVerdict | undefined;
|
|
35
|
+
|
|
25
36
|
export interface ApprovalContext {
|
|
26
|
-
readonly titleOf?: ((id: string) => string | undefined) | undefined;
|
|
27
37
|
readonly writes?: boolean | undefined;
|
|
38
|
+
readonly policy?: ApprovalPolicy | undefined;
|
|
28
39
|
readonly detailOf?:
|
|
29
40
|
| ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>)
|
|
30
41
|
| undefined;
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
function messageOf(error: unknown): string {
|
|
45
|
+
return error instanceof Error ? error.message : String(error);
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
function compose(
|
|
34
49
|
detail: ApprovalDetail | undefined,
|
|
35
|
-
|
|
50
|
+
verdict: { readonly question: ApprovalDetail; readonly fallback: ApprovalDetail },
|
|
36
51
|
): ApprovalDetail {
|
|
37
|
-
if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '')
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function actionOf(call: ApprovalCall): { action?: string; id?: string } {
|
|
42
|
-
const raw = call.arguments;
|
|
43
|
-
if (typeof raw !== 'object' || raw === null) return {};
|
|
44
|
-
const value = raw as { action?: unknown; id?: unknown; unit?: unknown };
|
|
52
|
+
if (detail === undefined || typeof detail.zh !== 'string' || detail.zh.trim() === '') {
|
|
53
|
+
return verdict.fallback;
|
|
54
|
+
}
|
|
45
55
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const READ_ONLY_ACTIONS = new Set(['list', '']);
|
|
52
|
-
|
|
53
|
-
interface AskSpec {
|
|
54
|
-
readonly reason: string;
|
|
55
|
-
readonly generic: ApprovalDetail;
|
|
56
|
-
readonly question: ApprovalDetail;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type CallEffect =
|
|
60
|
-
| { readonly kind: 'ask'; readonly spec: AskSpec }
|
|
61
|
-
| { readonly kind: 'read' }
|
|
62
|
-
| { readonly kind: 'unknown' };
|
|
63
|
-
|
|
64
|
-
const READ: CallEffect = { kind: 'read' };
|
|
65
|
-
const UNKNOWN: CallEffect = { kind: 'unknown' };
|
|
66
|
-
|
|
67
|
-
function ask(spec: AskSpec): CallEffect {
|
|
68
|
-
return { kind: 'ask', spec };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function confirmed(call: ApprovalCall): boolean | undefined {
|
|
72
|
-
const raw = call.arguments;
|
|
73
|
-
if (typeof raw !== 'object' || raw === null) return undefined;
|
|
74
|
-
const value = (raw as { confirm?: unknown }).confirm;
|
|
75
|
-
return typeof value === 'boolean' ? value : undefined;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function effectOf(call: ApprovalCall, context: ApprovalContext): CallEffect {
|
|
79
|
-
const name = call.name ?? '';
|
|
80
|
-
const { action, id } = actionOf(call);
|
|
81
|
-
const readAction = action !== undefined && action !== '' && READ_ONLY_ACTIONS.has(action);
|
|
82
|
-
|
|
83
|
-
const what = (): string => {
|
|
84
|
-
if (id === undefined || id === '') return '一个模块';
|
|
85
|
-
return `「${context.titleOf?.(id) ?? id}」`;
|
|
56
|
+
en: `${detail.en} — ${verdict.question.en}`,
|
|
57
|
+
zh: `${detail.zh} —— ${verdict.question.zh}`,
|
|
86
58
|
};
|
|
87
|
-
|
|
88
|
-
if (name === 'workbench_cmdb_release') {
|
|
89
|
-
if (confirmed(call) !== true) return READ;
|
|
90
|
-
return ask({
|
|
91
|
-
reason: 'confirm=true releases a version to a real environment',
|
|
92
|
-
generic: {
|
|
93
|
-
en: 'The agent wants to deploy a version for real (confirm: true) — it deploys on the platform (non-production). Allow?',
|
|
94
|
-
zh: 'agent 想在非生产环境上**真的部署一个版本**(confirm: true)—— 会在平台上部署。允许吗?',
|
|
95
|
-
},
|
|
96
|
-
question: {
|
|
97
|
-
en: 'it deploys on the platform for real (non-production). Allow?',
|
|
98
|
-
zh: '会在平台上真的部署一次(非生产环境)。允许吗?',
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (name === 'workbench_coding_issue_comment' || name === 'workbench_coding_issue_transfer') {
|
|
104
|
-
if (confirmed(call) !== true) return READ;
|
|
105
|
-
const comment = name === 'workbench_coding_issue_comment';
|
|
106
|
-
return ask({
|
|
107
|
-
reason: comment
|
|
108
|
-
? 'confirm=true posts a comment on the platform'
|
|
109
|
-
: 'confirm=true moves an issue to another status on the platform',
|
|
110
|
-
generic: comment
|
|
111
|
-
? {
|
|
112
|
-
en: 'The agent wants to post a comment on the platform for real (confirm: true) — it writes on the platform. Allow?',
|
|
113
|
-
zh: 'agent 想**真的给一条缺陷加评论**(confirm: true)—— 会写平台数据。允许吗?',
|
|
114
|
-
}
|
|
115
|
-
: {
|
|
116
|
-
en: 'The agent wants to move an issue to another status for real (confirm: true) — it writes on the platform. Allow?',
|
|
117
|
-
zh: 'agent 想**真的把一条缺陷流转到别的状态**(confirm: true)—— 会写平台数据。允许吗?',
|
|
118
|
-
},
|
|
119
|
-
question: comment
|
|
120
|
-
? {
|
|
121
|
-
en: 'it posts that comment on the platform for real. Allow?',
|
|
122
|
-
zh: '会在平台上真的发表这条评论。允许吗?',
|
|
123
|
-
}
|
|
124
|
-
: {
|
|
125
|
-
en: 'it changes that issue status on the platform for real. Allow?',
|
|
126
|
-
zh: '会在平台上真的改这条缺陷的状态。允许吗?',
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (name === 'workbench_modules') {
|
|
132
|
-
if (readAction) return READ;
|
|
133
|
-
if (action === 'update-dependency') {
|
|
134
|
-
return ask({
|
|
135
|
-
reason: `workbench_modules update-dependency ${id ?? ''} upgrades a shared dependency on this machine`,
|
|
136
|
-
generic: {
|
|
137
|
-
en: `The agent wants to upgrade the shared dependency ${what()} (it changes what is installed on this machine). Allow?`,
|
|
138
|
-
zh: `agent 想升级共用依赖 ${what()}(会改这台机器上装的东西)。允许吗?`,
|
|
139
|
-
},
|
|
140
|
-
question: {
|
|
141
|
-
en: 'it upgrades that shared dependency on this machine. Allow?',
|
|
142
|
-
zh: `会升级共用依赖 ${what()}。允许吗?`,
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
const verb = action === 'install' ? '装' : action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '装 / 更新 / 卸载';
|
|
147
|
-
return ask({
|
|
148
|
-
reason: `workbench_modules ${action ?? '(unknown action)'} ${id ?? ''} changes plugins on this machine`.trim(),
|
|
149
|
-
generic: {
|
|
150
|
-
en: `The agent wants to ${verb === '装' ? 'install' : verb === '卸载' ? 'remove' : verb === '更新' ? 'update' : 'install/update/remove'} the module ${what()} (it changes plugins on this machine). Allow?`,
|
|
151
|
-
zh: `agent 想${verb}模块 ${what()}(会改这台机器上的插件)。允许吗?`,
|
|
152
|
-
},
|
|
153
|
-
question: {
|
|
154
|
-
en: 'it changes plugins on this machine. Allow?',
|
|
155
|
-
zh: '会改这台机器上的插件。允许吗?',
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (name !== 'workbench_panels') return UNKNOWN;
|
|
161
|
-
|
|
162
|
-
if (readAction) return READ;
|
|
163
|
-
|
|
164
|
-
if (action === 'hide') {
|
|
165
|
-
return ask({
|
|
166
|
-
reason: 'workbench_panels hide removes a module from the panel',
|
|
167
|
-
generic: {
|
|
168
|
-
en: `The agent wants to remove ${what()} from this project's panel (its agent tools go offline here too). Allow?`,
|
|
169
|
-
zh: `agent 想把 ${what()} 从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
|
|
170
|
-
},
|
|
171
|
-
question: {
|
|
172
|
-
en: 'it takes that module off this panel (its agent tools go offline here too). Allow?',
|
|
173
|
-
zh: `${what()}会从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`,
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
if (action === 'move') {
|
|
178
|
-
return ask({
|
|
179
|
-
reason: 'workbench_panels move reorders the panel',
|
|
180
|
-
generic: {
|
|
181
|
-
en: `The agent wants to reorder this project's panel (put ${what()} first). Allow?`,
|
|
182
|
-
zh: `agent 想调整这个项目面板的顺序(把 ${what()} 放前面)。允许吗?`,
|
|
183
|
-
},
|
|
184
|
-
question: {
|
|
185
|
-
en: `it reorders this panel (${what()} goes first). Allow?`,
|
|
186
|
-
zh: `会调整这个项目面板的顺序(${what()}放前面)。允许吗?`,
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
if (action === 'reset') {
|
|
191
|
-
return ask({
|
|
192
|
-
reason: 'workbench_panels reset clears the panel',
|
|
193
|
-
generic: {
|
|
194
|
-
en: "The agent wants to clear this project's panel (every module goes offline here). Allow?",
|
|
195
|
-
zh: 'agent 想清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
|
|
196
|
-
},
|
|
197
|
-
question: {
|
|
198
|
-
en: 'it clears this panel (every module goes offline here). Allow?',
|
|
199
|
-
zh: '会清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?',
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
return ask({
|
|
204
|
-
reason: `workbench_panels ${action ?? '(unknown action)'} changes the user's panel`,
|
|
205
|
-
generic: {
|
|
206
|
-
en: `The agent wants to add ${what()} to this project's panel. Allow?`,
|
|
207
|
-
zh: `agent 想把 ${what()} 加到**这个项目**的面板上。允许吗?`,
|
|
208
|
-
},
|
|
209
|
-
question: {
|
|
210
|
-
en: `it adds ${what()} to this project's panel. Allow?`,
|
|
211
|
-
zh: `会把 ${what()} 加到**这个项目**的面板上。允许吗?`,
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
59
|
}
|
|
215
60
|
|
|
216
61
|
export async function approvalFor(
|
|
217
62
|
call: ApprovalCall,
|
|
218
63
|
context: ApprovalContext = {},
|
|
219
64
|
): Promise<AskDecision | undefined> {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
65
|
+
if (context.writes !== true) return undefined;
|
|
66
|
+
|
|
67
|
+
let verdict: ApprovalVerdict | undefined;
|
|
68
|
+
try {
|
|
69
|
+
verdict = context.policy?.(call);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
kind: 'deny',
|
|
73
|
+
reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf(error)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (verdict?.kind === 'read') return undefined;
|
|
78
|
+
|
|
79
|
+
if (verdict?.kind !== 'ask') {
|
|
224
80
|
return {
|
|
225
81
|
kind: 'deny',
|
|
226
82
|
reason:
|
|
227
|
-
`${call.name ?? '这把工具'}
|
|
228
|
-
'
|
|
83
|
+
`${call.name ?? '这把工具'} 在清单里声明会写东西,但它的套件没认领这次调用 —— 停下:` +
|
|
84
|
+
'在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去',
|
|
229
85
|
};
|
|
230
86
|
}
|
|
231
|
-
const { spec } = effect;
|
|
232
87
|
|
|
233
88
|
let facts: ApprovalFacts | undefined;
|
|
234
89
|
try {
|
|
235
90
|
facts = await context.detailOf?.(call);
|
|
236
91
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${why}` };
|
|
92
|
+
return { kind: 'deny', reason: `拿不到"这次会发生什么",已经停下:${messageOf(error)}` };
|
|
239
93
|
}
|
|
240
94
|
if (facts !== undefined && 'blocked' in facts) return { kind: 'deny', reason: facts.blocked };
|
|
241
95
|
|
|
242
|
-
return { kind: 'ask', reason:
|
|
96
|
+
return { kind: 'ask', reason: verdict.reason, displayReason: compose(facts, verdict) };
|
|
243
97
|
}
|