@dsh-workbench/shared 0.3.9 → 0.3.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.
Files changed (48) hide show
  1. package/dist/api/dsh-client.d.ts +1 -0
  2. package/dist/api/dsh.d.ts +18 -0
  3. package/dist/api/host-write.d.ts +0 -15
  4. package/dist/api/host-write.js +1 -35
  5. package/dist/api/unit-board.d.ts +0 -21
  6. package/dist/api/unit-board.js +1 -45
  7. package/dist/components/icons.d.ts +0 -6
  8. package/dist/components/icons.js +0 -12
  9. package/dist/config/storage.d.ts +0 -4
  10. package/dist/config/storage.js +0 -11
  11. package/dist/host/browser-find.js +1 -1
  12. package/dist/host/browser-read.js +5 -5
  13. package/dist/host/tool-approval.d.ts +12 -0
  14. package/dist/host/tool-approval.js +4 -0
  15. package/dist/i18n/en.js +40 -20
  16. package/dist/i18n/index.d.ts +41 -21
  17. package/dist/i18n/zh.d.ts +40 -20
  18. package/dist/i18n/zh.js +40 -20
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/lib/blocker-action.d.ts +0 -15
  22. package/dist/lib/blocker-action.js +1 -25
  23. package/dist/lib/react-types.d.ts +5 -0
  24. package/dist/lib/react-types.js +1 -0
  25. package/dist/lib/react.d.ts +14 -13
  26. package/dist/lib/react.js +11 -11
  27. package/package.json +1 -1
  28. package/src/api/dsh-client.ts +4 -1
  29. package/src/api/dsh.ts +16 -0
  30. package/src/api/host-write.ts +0 -42
  31. package/src/api/unit-board.ts +0 -45
  32. package/src/components/icons.tsx +0 -16
  33. package/src/config/storage.test.ts +1 -25
  34. package/src/config/storage.ts +0 -12
  35. package/src/host/__tests__/browser-find.test.ts +1 -1
  36. package/src/host/__tests__/browser-read.test.ts +1 -1
  37. package/src/host/browser-find.ts +1 -1
  38. package/src/host/browser-read.ts +5 -5
  39. package/src/host/tool-approval.ts +17 -0
  40. package/src/i18n/en.ts +42 -20
  41. package/src/i18n/zh.ts +42 -20
  42. package/src/index.ts +1 -1
  43. package/src/lib/blocker-action.ts +0 -27
  44. package/src/lib/react-types.ts +5 -0
  45. package/src/lib/react.ts +14 -13
  46. package/src/api/__tests__/host-write.test.ts +0 -75
  47. package/src/api/__tests__/unit-board.test.ts +0 -76
  48. package/src/lib/blocker-action.test.ts +0 -40
@@ -18,18 +18,3 @@ export interface BlockerLabelState {
18
18
  readonly running: string | null;
19
19
  readonly copied: string | null;
20
20
  }
21
- /**
22
- * 这颗钮此刻该不该按成等待态:这一轮忙还没收,或有一支正在跑。
23
- * @param busy - 这一轮忙是不是还在。
24
- * @param running - 正在跑的那一支的 label;没有就给 `null`。
25
- * @returns 该等待给 `true`,否则 `false`。
26
- */
27
- export declare function blockerPending(busy: boolean, running: string | null): boolean;
28
- /**
29
- * 这颗钮此刻该显示什么字:正在重试说「重试中…」,正在跑的那一支说「修复中…」,刚复制过的说「已复制」,其余原样。
30
- * @param action - 这颗钮的形状(行为 + 平时文案)。
31
- * @param state - 此刻的运行态。
32
- * @param t - i18n 取词函数。
33
- * @returns 要显示的文案;三种临时态都不成立时就是 `action.label`。
34
- */
35
- export declare function blockerActionLabel(action: BlockerActionShape, state: BlockerLabelState, t: BlockerLabelTranslate): string;
@@ -1,25 +1 @@
1
- /**
2
- * 这颗钮此刻该不该按成等待态:这一轮忙还没收,或有一支正在跑。
3
- * @param busy - 这一轮忙是不是还在。
4
- * @param running - 正在跑的那一支的 label;没有就给 `null`。
5
- * @returns 该等待给 `true`,否则 `false`。
6
- */
7
- export function blockerPending(busy, running) {
8
- return busy || running !== null;
9
- }
10
- /**
11
- * 这颗钮此刻该显示什么字:正在重试说「重试中…」,正在跑的那一支说「修复中…」,刚复制过的说「已复制」,其余原样。
12
- * @param action - 这颗钮的形状(行为 + 平时文案)。
13
- * @param state - 此刻的运行态。
14
- * @param t - i18n 取词函数。
15
- * @returns 要显示的文案;三种临时态都不成立时就是 `action.label`。
16
- */
17
- export function blockerActionLabel(action, state, t) {
18
- if (action.kind === 'retry' && state.retrying)
19
- return t('blocker.retry.running');
20
- if (state.running === action.label)
21
- return t('blocker.fix.running');
22
- if (action.kind === 'copy' && state.copied === action.text)
23
- return t('env.copied');
24
- return action.label;
25
- }
1
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 从 react 借来的三个类型(`ReactNode` / `CSSProperties` / `ChangeEvent`)—— 模块从这里拿,
3
+ * 就不必自己直接依赖 react。只取类型:编译期擦除,不进任何产物。
4
+ */
5
+ export type { ReactNode, CSSProperties, ChangeEvent } from 'react';
@@ -0,0 +1 @@
1
+ export {};
@@ -4,50 +4,51 @@ import type * as ReactTypes from 'react';
4
4
  */
5
5
  export declare const h: typeof ReactTypes.createElement;
6
6
  /**
7
- * React 的 `Fragment` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
7
+ * React 的 `Fragment` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
8
8
  */
9
9
  export declare const Fragment: ReactTypes.ExoticComponent<ReactTypes.FragmentProps>;
10
10
  /**
11
- * React 的 `Component` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
11
+ * React 的 `Component` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
12
12
  */
13
13
  export declare const Component: typeof ReactTypes.Component;
14
14
  /**
15
- * React 的 `createContext` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
15
+ * React 的 `createContext` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
16
16
  */
17
17
  export declare const createContext: typeof ReactTypes.createContext;
18
18
  /**
19
- * React 的 `useContext` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
19
+ * React 的 `useContext` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
20
20
  */
21
21
  export declare const useContext: typeof ReactTypes.useContext;
22
22
  /**
23
- * React 的 `useState` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
23
+ * React 的 `useState` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
24
24
  */
25
25
  export declare const useState: typeof ReactTypes.useState;
26
26
  /**
27
- * React 的 `useEffect` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
27
+ * React 的 `useEffect` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
28
28
  */
29
29
  export declare const useEffect: typeof ReactTypes.useEffect;
30
30
  /**
31
- * React 的 `useLayoutEffect` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
31
+ * React 的 `useLayoutEffect` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
32
32
  */
33
33
  export declare const useLayoutEffect: typeof ReactTypes.useLayoutEffect;
34
34
  /**
35
- * React 的 `useMemo` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
35
+ * React 的 `useMemo` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
36
36
  */
37
37
  export declare const useMemo: typeof ReactTypes.useMemo;
38
38
  /**
39
- * React 的 `useCallback` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
39
+ * React 的 `useCallback` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
40
40
  */
41
41
  export declare const useCallback: typeof ReactTypes.useCallback;
42
42
  /**
43
- * React 的 `useRef` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
43
+ * React 的 `useRef` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
44
44
  */
45
45
  export declare const useRef: typeof ReactTypes.useRef;
46
46
  /**
47
- * React 的 `useSyncExternalStore` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
47
+ * React 的 `useSyncExternalStore` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
48
48
  */
49
49
  export declare const useSyncExternalStore: typeof ReactTypes.useSyncExternalStore;
50
50
  /**
51
- * 从 react 借来的三个类型(`ReactNode` / `CSSProperties` / `ChangeEvent`)—— 模块从这里拿,就不必自己直接依赖 react。
51
+ * 借来的三个类型从 `react-types.ts` 转一手:那份是**公开面**(模块 `import type` 从根入口拿),
52
+ * 组件内部照旧从这里引,别在两处各写一遍名字。
52
53
  */
53
- export type { ReactNode, CSSProperties, ChangeEvent } from 'react';
54
+ export type { ReactNode, CSSProperties, ChangeEvent } from './react-types.js';
package/dist/lib/react.js CHANGED
@@ -4,46 +4,46 @@ const react = require('react');
4
4
  */
5
5
  export const h = react.createElement;
6
6
  /**
7
- * React 的 `Fragment` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
7
+ * React 的 `Fragment` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
8
8
  */
9
9
  export const Fragment = react.Fragment;
10
10
  /**
11
- * React 的 `Component` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
11
+ * React 的 `Component` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
12
12
  */
13
13
  export const Component = react.Component;
14
14
  /**
15
- * React 的 `createContext` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
15
+ * React 的 `createContext` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
16
16
  */
17
17
  export const createContext = react.createContext;
18
18
  /**
19
- * React 的 `useContext` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
19
+ * React 的 `useContext` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
20
20
  */
21
21
  export const useContext = react.useContext;
22
22
  /**
23
- * React 的 `useState` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
23
+ * React 的 `useState` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
24
24
  */
25
25
  export const useState = react.useState;
26
26
  /**
27
- * React 的 `useEffect` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
27
+ * React 的 `useEffect` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
28
28
  */
29
29
  export const useEffect = react.useEffect;
30
30
  /**
31
- * React 的 `useLayoutEffect` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
31
+ * React 的 `useLayoutEffect` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
32
32
  */
33
33
  export const useLayoutEffect = react.useLayoutEffect;
34
34
  /**
35
- * React 的 `useMemo` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
35
+ * React 的 `useMemo` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
36
36
  */
37
37
  export const useMemo = react.useMemo;
38
38
  /**
39
- * React 的 `useCallback` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
39
+ * React 的 `useCallback` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
40
40
  */
41
41
  export const useCallback = react.useCallback;
42
42
  /**
43
- * React 的 `useRef` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
43
+ * React 的 `useRef` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
44
44
  */
45
45
  export const useRef = react.useRef;
46
46
  /**
47
- * React 的 `useSyncExternalStore` 原样再导出 —— 模块从这里拿,就不必自己直接依赖 react。
47
+ * React 的 `useSyncExternalStore` 原样再导出 —— 共享包的哑组件用它;模块写 JSX 一律直接 import react(这一份不进公开入口)。
48
48
  */
49
49
  export const useSyncExternalStore = react.useSyncExternalStore;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -254,7 +254,10 @@ export interface DshConversationService {
254
254
  * 浏览器侧的会话服务:快照加按 id 取作用域;create 是可选写口,不是每个 dsh 版本都有。
255
255
  */
256
256
  export interface DshSessionsService {
257
- readonly list: { getSnapshot(): SessionsSnapshot };
257
+ readonly list: {
258
+ getSnapshot(): SessionsSnapshot;
259
+ subscribe?(listener: () => void): () => void;
260
+ };
258
261
  scope(sessionId: string): unknown | undefined;
259
262
  create?(request: {
260
263
  readonly workspaceId?: string;
package/src/api/dsh.ts CHANGED
@@ -229,6 +229,22 @@ export interface DshToolRunContext {
229
229
  | undefined;
230
230
  }
231
231
 
232
+ /**
233
+ * dsh 的审批服务(服务名 `approval`):读一个会话此刻生效的审批策略。
234
+ * 方法可选:服务缺席或老版本没有它时,调用方必须当成「不知道」而不是「已关闭」。
235
+ */
236
+ export interface DshApprovalService {
237
+ effectivePolicy?(session: unknown): 'ask' | 'never' | undefined;
238
+ }
239
+
240
+ /**
241
+ * dsh 的沙箱策略服务(服务名 `sandboxPolicy`):按会话解析这次调用生效的文件边界。
242
+ * 方法可选:服务缺席或老版本没有它时,调用方必须当成「不知道」而不是「完全放行」。
243
+ */
244
+ export interface DshSandboxPolicy {
245
+ resolve?(request?: { readonly session?: unknown }): { readonly mode?: string } | undefined;
246
+ }
247
+
232
248
  /**
233
249
  * dsh 的工具表;register 回的清理函数负责在插件卸载时摘掉这条工具。
234
250
  */
@@ -1,5 +1,3 @@
1
- import type { ApiResponse } from './workbench-client.js';
2
-
3
1
  /**
4
2
  * 宿主写口的五条通道(偏好、动作、供给、工坊调试、工坊录制),是写路由的第一段。
5
3
  */
@@ -10,17 +8,6 @@ export type WriteChannel =
10
8
  | 'diy-debug'
11
9
  | 'diy-record';
12
10
 
13
- /**
14
- * 由通道与资源拼出写路由。
15
- * @param channel 写通道。
16
- * @param resource 该通道下的资源名。
17
- * @returns 路由字符串;provision 只有一段,其余是 channel/resource。
18
- */
19
- export function writeRoute(channel: WriteChannel, resource: string): string {
20
- if (channel === 'provision') return 'provision';
21
- return `${channel}/${resource}`;
22
- }
23
-
24
11
  /**
25
12
  * 写通道统一的结果:成了带值,没成带一句能给人看的话(可选明细与帮助链接)。
26
13
  */
@@ -33,32 +20,3 @@ export type WriteOutcome<T> =
33
20
  readonly helpUrl?: string;
34
21
  };
35
22
 
36
- /**
37
- * 把写通道的响应翻成 WriteOutcome。
38
- * @param response 写口回的响应;null 表示请求本身没成(网络断了,宿主没答)。
39
- * @param networkText 拿不到任何服务端文案时兜底的那句。
40
- * @returns 失败时 message 优先用服务端自己的话,没有才用响应正文或 networkText。
41
- */
42
- export function writeOutcomeOf<T>(response: ApiResponse<T> | null, networkText: string): WriteOutcome<T> {
43
- if (response === null) return { ok: false, message: networkText };
44
- if (!response.ok) {
45
- const box = response as {
46
- readonly error?:
47
- | { readonly message?: unknown; readonly detail?: unknown; readonly helpUrl?: unknown }
48
- | undefined;
49
- readonly text?: unknown;
50
- };
51
- const own = typeof box.error?.message === 'string' && box.error.message !== '' ? box.error.message : undefined;
52
- const fallback = typeof box.text === 'string' && box.text !== '' ? box.text : networkText;
53
- const detail = typeof box.error?.detail === 'string' ? box.error.detail : undefined;
54
- const helpUrl =
55
- typeof box.error?.helpUrl === 'string' && box.error.helpUrl !== '' ? box.error.helpUrl : undefined;
56
- return {
57
- ok: false,
58
- message: own ?? fallback,
59
- ...(detail === undefined ? {} : { detail }),
60
- ...(helpUrl === undefined ? {} : { helpUrl }),
61
- };
62
- }
63
- return { ok: true, value: response };
64
- }
@@ -1,6 +1,4 @@
1
1
  import type { ModuleBlocker } from './blocker.js';
2
- import type { CapabilityPayload } from './workbench-contract.js';
3
- import type { ApiError } from './workbench-client.js';
4
2
 
5
3
  /**
6
4
  * 卡片闸的两种档:capability 按环境读数判,none 无条件放行。
@@ -16,18 +14,6 @@ export interface UnitGate {
16
14
  readonly gated: boolean;
17
15
  }
18
16
 
19
- /**
20
- * 判这张卡现在能不能画。
21
- * @param capability 环境读数;还没读到给 null。
22
- * @param mode 闸档。
23
- * @returns mode 为 none 或读数还没到都放行(先放行让它读);读到明确不通过(ok 与 unchecked 之外)才拦。
24
- */
25
- export function gateOf(capability: CapabilityPayload | null, mode: GateMode): UnitGate {
26
- if (mode === 'none') return { gated: true };
27
- if (capability === null) return { gated: true };
28
- return { gated: capability.status === 'ok' || capability.status === 'unchecked' };
29
- }
30
-
31
17
  /**
32
18
  * 一个单元攒下的卡住态:blocker 顶掉卡片,notice 只提示;组装方产出,卡片消费。
33
19
  */
@@ -36,34 +22,3 @@ export interface UnitBlockers {
36
22
  readonly notice?: ModuleBlocker;
37
23
  }
38
24
 
39
- /**
40
- * 把候选卡住态分档,每档只留第一个。
41
- * @param candidates 候选(可能含 undefined),按优先级从高到低给。
42
- * @returns 分出的 blocker 与 notice;已经有 blocker 时不带 notice(挡住就不必再提示)。
43
- */
44
- export function splitBlocker(candidates: readonly (ModuleBlocker | undefined)[]): UnitBlockers {
45
- let blocker: ModuleBlocker | undefined;
46
- let notice: ModuleBlocker | undefined;
47
- for (const candidate of candidates) {
48
- if (candidate === undefined) continue;
49
- if (candidate.severity === 'block') {
50
- blocker ??= candidate;
51
- continue;
52
- }
53
- notice ??= candidate;
54
- }
55
- return {
56
- ...(blocker === undefined ? {} : { blocker }),
57
- ...(blocker !== undefined || notice === undefined ? {} : { notice }),
58
- };
59
- }
60
-
61
- /**
62
- * 由读口错误算出「出错搭车强探」的防抖键:同一个键只自动重探一次。
63
- * @param error 读口错误;undefined 表示这次没有错误。
64
- * @returns 键字符串;空 code 归一到 unknown。
65
- */
66
- export function probeKeyOf(error: ApiError | undefined): string | null {
67
- if (error === undefined) return null;
68
- return error.code === '' ? 'unknown' : error.code;
69
- }
@@ -117,22 +117,6 @@ export function IconNpm({ size = 16 }: IconProps) {
117
117
  );
118
118
  }
119
119
 
120
- /**
121
- * 四宫格:"模块"这个概念的通用单色图标(侧栏、面板那颗钮、市场节的标题);默认 16px。
122
- * @param props.size - 边长像素,同时给 `width` 与 `height`。
123
- * @returns 一枚 `aria-hidden` 的 svg。
124
- */
125
- export function IconModules({ size = 16 }: IconProps) {
126
- return (
127
- <svg width={size} height={size} viewBox="0 0 16 16" fill="none" aria-hidden="true">
128
- <rect x="2.5" y="2.5" width="4.6" height="4.6" rx="1.2" stroke="currentColor" strokeWidth="1.2" />
129
- <rect x="8.9" y="2.5" width="4.6" height="4.6" rx="1.2" stroke="currentColor" strokeWidth="1.2" />
130
- <rect x="2.5" y="8.9" width="4.6" height="4.6" rx="1.2" stroke="currentColor" strokeWidth="1.2" />
131
- <rect x="8.9" y="8.9" width="4.6" height="4.6" rx="1.2" stroke="currentColor" strokeWidth="1.2" />
132
- </svg>
133
- );
134
- }
135
-
136
120
  /**
137
121
  * 模块类型标:渐变方块 + 白四宫格,用在"这是个模块"的行首;品牌物,颜色不跟主题走。默认 16px。
138
122
  * @param props.size - 边长像素,同时给 `width` 与 `height`。
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import { readFlag, readText, writeFlag, writeText } from './storage.js';
2
+ import { readText, writeText } from './storage.js';
3
3
 
4
4
  function fakeStorage(options: { throwOnWrite?: boolean } = {}) {
5
5
  const data = new Map<string, string>();
@@ -44,30 +44,6 @@ describe('storage', () => {
44
44
  expect(fake.data.has('k')).toBe(false);
45
45
  });
46
46
 
47
- it('没存过时读回 null / 默认值', () => {
48
- useStorage();
49
- expect(readText('missing')).toBeNull();
50
- expect(readFlag('missing', true)).toBe(true);
51
- expect(readFlag('missing', false)).toBe(false);
52
- });
53
-
54
- it('布尔开关按字面量往返', () => {
55
- useStorage();
56
- writeFlag('on', true);
57
- writeFlag('off', false);
58
- expect(readFlag('on', false)).toBe(true);
59
- expect(readFlag('off', true)).toBe(false);
60
- });
61
-
62
- it('只有字面量 "true" 算真:脏数据不因为"非空"就把开关打开', () => {
63
- useStorage();
64
- writeText('weird', 'yes');
65
- writeText('one', '1');
66
- expect(readFlag('weird', true)).toBe(false);
67
- expect(readFlag('one', true)).toBe(false);
68
- expect(readFlag('never', true)).toBe(true);
69
- });
70
-
71
47
  it('localStorage 抛异常时静默降级(隐私模式不该让面板崩掉)', () => {
72
48
  useStorage({ throwOnWrite: true });
73
49
  expect(() => writeText('k', 'v')).not.toThrow();
@@ -15,15 +15,3 @@ export function writeText(key: string, value: string | null): void {
15
15
  } catch {
16
16
  }
17
17
  }
18
-
19
- /** 读一个布尔开关:键不存在时返回 `fallback`,只有存进去的 `'true'` 算真。 */
20
- export function readFlag(key: string, fallback: boolean): boolean {
21
- const raw = readText(key);
22
- if (raw === null) return fallback;
23
- return raw === 'true';
24
- }
25
-
26
- /** 把一个布尔开关写进 localStorage(存 `'true'` / `'false'`);失败静默。 */
27
- export function writeFlag(key: string, value: boolean): void {
28
- writeText(key, value ? 'true' : 'false');
29
- }
@@ -217,7 +217,7 @@ describe('browserFind · 找到与找不到', () => {
217
217
  expect(out.ok).toBe(false);
218
218
  expect(out.code).toBe('cli-exec-failed');
219
219
  expect(out.detail).toContain('ENOENT');
220
- expect(out.hint).toContain('opencli');
220
+ expect(out.hint).toContain('后台服务');
221
221
  });
222
222
 
223
223
  it('session / command / limit / textMax 都能覆盖(命令行每个 token 都加引号)', async () => {
@@ -189,7 +189,7 @@ describe('browserRead · 命令这一层的问题', () => {
189
189
  const { shell } = fakeShell([{ stdout: 'not json at all' }]);
190
190
  const out = await browserRead({ shell, script, ...FAST });
191
191
  expect(out.code).toBe('cli-exec-failed');
192
- expect(out.message).toContain('不是 JSON');
192
+ expect(out.message).toContain('格式不对');
193
193
  });
194
194
  });
195
195
 
@@ -229,7 +229,7 @@ export async function browserFind(options: BrowserFindOptions): Promise<BrowserF
229
229
  return {
230
230
  kind: 'failed',
231
231
  detail: error instanceof Error ? error.message : String(error),
232
- hint: '确认这台机器上 opencli 能跑(工作台会自己把它挂好)',
232
+ hint: '确认后台服务能跑起来,再重试',
233
233
  };
234
234
  }
235
235
  };
@@ -140,8 +140,8 @@ async function evalOnce(
140
140
  result: {
141
141
  ok: false,
142
142
  code: 'cli-exec-failed',
143
- message: '读不了:opencli 起不来',
144
- hint: '确认这台机器上 opencli 能跑(工作台会自己把它挂好)',
143
+ message: '读不了:后台服务起不来',
144
+ hint: '确认后台服务能跑起来,再重试',
145
145
  detail: error instanceof Error ? error.message : String(error),
146
146
  },
147
147
  };
@@ -179,8 +179,8 @@ async function evalOnce(
179
179
  result: {
180
180
  ok: false,
181
181
  code: 'cli-exec-failed',
182
- message: '读到了、但不是 JSON',
183
- hint: '脚本必须回 JSON',
182
+ message: '读到的格式不对',
183
+ hint: '让 agent 检查一下这条读',
184
184
  detail: tailLines(attempt.stdout, 4),
185
185
  },
186
186
  };
@@ -210,7 +210,7 @@ async function openOnce(
210
210
  ok: false,
211
211
  code: 'cli-exec-failed',
212
212
  message: '打不开那个页面',
213
- hint: '确认 opencli 能跑起来',
213
+ hint: '确认后台服务能跑起来,再重试',
214
214
  detail: error instanceof Error ? error.message : String(error),
215
215
  helpUrl: url,
216
216
  };
@@ -75,9 +75,17 @@ export interface ApprovalCall {
75
75
  readonly session?: string | undefined;
76
76
  }
77
77
 
78
+ /**
79
+ * 套件对一次写调用给出的判定:`read` 只是看看、`allow` 会写但本套件不问、`deny` 这一次根本跑不成
80
+ * (连问都不该问,`reason` 原样交给 agent)、`ask` 要用户点头(`question` 接在明细后,`fallback` 是没明细时的整句)。
81
+ */
78
82
  export type ApprovalVerdict =
79
83
  | { readonly kind: 'read' }
80
84
  | { readonly kind: 'allow' }
85
+ | {
86
+ readonly kind: 'deny';
87
+ readonly reason: string;
88
+ }
81
89
  | {
82
90
  readonly kind: 'ask';
83
91
  readonly reason: string;
@@ -87,10 +95,15 @@ export type ApprovalVerdict =
87
95
 
88
96
  export type ApprovalPolicy = (call: ApprovalCall) => ApprovalVerdict | undefined;
89
97
 
98
+ /**
99
+ * 闸门的输入:`skipAsk` 为真表示会话已经不要审批提示(完全权限)—— 认领检查、`{blocked}` 与套件自己的
100
+ * `deny` 照旧,只是不再返回 `ask`;dsh 在 `approval: never` 下会把 `ask` 直接拒掉。
101
+ */
90
102
  export interface ApprovalContext {
91
103
  readonly writes?: boolean | undefined;
92
104
  readonly policy?: ApprovalPolicy | undefined;
93
105
  readonly declaredNote?: boolean | undefined;
106
+ readonly skipAsk?: boolean | undefined;
94
107
  readonly detailOf?:
95
108
  | ((call: ApprovalCall) => ApprovalFacts | undefined | Promise<ApprovalFacts | undefined>)
96
109
  | undefined;
@@ -134,6 +147,8 @@ export async function approvalFor(
134
147
 
135
148
  if (verdict?.kind === 'read' || verdict?.kind === 'allow') return undefined;
136
149
 
150
+ if (verdict?.kind === 'deny') return { kind: 'deny', reason: verdict.reason };
151
+
137
152
  if (verdict?.kind !== 'ask') {
138
153
  return {
139
154
  kind: 'deny',
@@ -151,6 +166,8 @@ export async function approvalFor(
151
166
  }
152
167
  if (facts !== undefined && 'blocked' in facts) return { kind: 'deny', reason: facts.blocked };
153
168
 
169
+ if (context.skipAsk === true) return undefined;
170
+
154
171
  return {
155
172
  kind: 'ask',
156
173
  reason: verdict.reason,