dsh-oh-my-terminal 0.2.3 → 0.2.5
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/README.md +40 -28
- package/README.zh.md +40 -28
- package/lib/index.js +21 -9
- package/package.json +6 -7
- package/src/index.ts +9 -2
- package/src/settings/index.ts +59 -23
- package/tests/unit/settings-integration.test.ts +50 -17
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-oh-my-terminal
|
|
2
2
|
|
|
3
|
-
[](package.json)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
6
|
|
|
@@ -13,10 +13,11 @@ A bottom terminal panel plugin for DSH Web GUI. Powered by `@lydell/node-pty`, i
|
|
|
13
13
|
- **Multiple terminal sessions**: manage several sessions simultaneously; a side list replaces the traditional horizontal tab bar, with right-click rename support
|
|
14
14
|
- **Split terminals**: horizontal splits within the same group; a VSCode-style dropdown next to the `+` button (new / split / new by kind)
|
|
15
15
|
- **Session persistence**: sessions survive panel close/reopen and are automatically restored on startup
|
|
16
|
-
- **Configurable shortcut**: toggle the panel with a keyboard shortcut;
|
|
16
|
+
- **Configurable shortcut**: toggle the panel with a keyboard shortcut; integrates with the host shortcut system and the panel label updates to reflect the current binding
|
|
17
17
|
- **Resizable panel**: drag the top edge of the panel to adjust its height
|
|
18
18
|
- **Cross-platform**: Windows ConPTY and POSIX openpty are selected automatically by `@lydell/node-pty`; pre-compiled binaries ship with the package, nothing to compile
|
|
19
|
-
- **
|
|
19
|
+
- **Profile-driven terminals**: a configurable terminal profile table (type / name / path) drives new-terminal creation; profiles are auto-detected at startup and can be added, renamed, and edited in the settings UI
|
|
20
|
+
- **Configurable fonts**: font family, size, and line height are adjustable and hot-reload on open terminals
|
|
20
21
|
|
|
21
22
|
## Installation
|
|
22
23
|
|
|
@@ -30,16 +31,22 @@ dsh plugin --profile web add github:btsd321/dsh-oh-my-terminal
|
|
|
30
31
|
|
|
31
32
|
| Option | Description | Default |
|
|
32
33
|
|---|---|---|
|
|
33
|
-
| `toggleShortcut` | Shortcut to toggle the terminal panel (legacy host only; see below) | `` Ctrl+` `` |
|
|
34
|
-
| `
|
|
34
|
+
| `toggleShortcut` | Shortcut to toggle the terminal panel (legacy host only; see below) | `` Ctrl+Shift+` `` |
|
|
35
|
+
| `fontFamily` | Terminal font family (CSS `font-family` string); empty = built-in default stack with CJK fallback | `''` |
|
|
36
|
+
| `fontSize` | Terminal font size in pixels | `12.5` |
|
|
37
|
+
| `lineHeight` | Terminal line height multiplier | `1.25` |
|
|
38
|
+
| `terminalProfiles` | Terminal profile table (JSON array); empty = use auto-detected $PATH terminals | `''` |
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
### Terminal profiles
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
The terminal profile table is a JSON array of `{ id, type, name, path, origin }` entries:
|
|
39
43
|
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
44
|
+
- **type**: determines spawn semantics — `pwsh` / `powershell` / `cmd` / `bash` / `zsh` / `fish` / `gitbash` / `nushell` / `custom`
|
|
45
|
+
- **name**: display name shown in the dropdown menu (user-editable)
|
|
46
|
+
- **path**: executable path; empty = resolve by `type` in `$PATH`
|
|
47
|
+
- **origin**: `auto` (startup-detected) or `user` (manually added)
|
|
48
|
+
|
|
49
|
+
Auto-detected profiles (`origin: auto`) cannot be deleted and their path is read-only, but the name is always editable. The profile table is editable in the settings UI with inline editing, add-row, and delete support.
|
|
43
50
|
|
|
44
51
|
## Development
|
|
45
52
|
|
|
@@ -75,25 +82,30 @@ The host side lazy-loads the package (`await import()` on first session creation
|
|
|
75
82
|
|
|
76
83
|
```
|
|
77
84
|
src/
|
|
78
|
-
├── index.ts
|
|
79
|
-
├── routes.ts
|
|
80
|
-
├── ws-handler.ts
|
|
81
|
-
├── client.tsx
|
|
85
|
+
├── index.ts # Host entry (Cordis plugin + settings + session lifecycle)
|
|
86
|
+
├── routes.ts # HTTP route handlers
|
|
87
|
+
├── ws-handler.ts # WebSocket handler (pty data forwarding)
|
|
88
|
+
├── client.tsx # Browser entry (React components + plugin registration)
|
|
82
89
|
├── client/
|
|
83
|
-
│ ├── types.ts
|
|
84
|
-
│ ├── hooks.ts
|
|
85
|
-
│ ├── term-pane.tsx
|
|
86
|
-
│ ├── dropdown.tsx
|
|
87
|
-
│ ├── side-list.tsx
|
|
88
|
-
│ ├── styles.ts
|
|
89
|
-
│ ├── icons.tsx
|
|
90
|
-
│
|
|
91
|
-
├──
|
|
92
|
-
├──
|
|
93
|
-
|
|
94
|
-
├──
|
|
95
|
-
├──
|
|
96
|
-
|
|
90
|
+
│ ├── types.ts # Shared types
|
|
91
|
+
│ ├── hooks.ts # useReducer state management + custom hooks
|
|
92
|
+
│ ├── term-pane.tsx # xterm.js terminal pane component
|
|
93
|
+
│ ├── dropdown.tsx # Dropdown menu next to the + button
|
|
94
|
+
│ ├── side-list.tsx # Right-side terminal list panel
|
|
95
|
+
│ ├── styles.ts # CSS constants + Campbell dark theme
|
|
96
|
+
│ ├── icons.tsx # SVG icon components
|
|
97
|
+
│ ├── clipboard.ts # Clipboard utility functions
|
|
98
|
+
│ ├── shortcut-bridge.ts # Shortcuts service integration (command registration + toggle bridge)
|
|
99
|
+
│ ├── settings/ # Settings UI (types / store / card / profile-table / api / styles)
|
|
100
|
+
│ └── terminal/ # Terminal tab state (use-tabs / reducer / use-terminal-state / geometry)
|
|
101
|
+
├── persistence.ts # Session persistence (log storage, metadata, startup restore)
|
|
102
|
+
├── platform.ts # Platform adapter (POSIX / Windows + shell detection)
|
|
103
|
+
├── terminal/ # Terminal kinds, detection, resolution, profile store
|
|
104
|
+
├── settings/ # Settings bridge (namespace, bridge routes, patch ops)
|
|
105
|
+
├── constants.ts # Protocol, size, shortcut, and env var constants
|
|
106
|
+
├── server-command.ts # Command-line parsing utilities
|
|
107
|
+
├── shortcut.ts # Shortcut parsing utilities
|
|
108
|
+
└── logger.ts # Structured logger
|
|
97
109
|
```
|
|
98
110
|
|
|
99
111
|
The host side and browser side communicate over WebSocket at `/api/dsh-oh-my-terminal` and do not import each other directly.
|
package/README.zh.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-oh-my-terminal
|
|
2
2
|
|
|
3
|
-
[](package.json)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
6
|
|
|
@@ -11,10 +11,11 @@ DSH Web GUI 的底部终端面板插件,基于 `@lydell/node-pty` 提供多终
|
|
|
11
11
|
- **多标签终端**:同时管理多个会话,右侧列表替代水平 tab 栏,支持右键重命名
|
|
12
12
|
- **拆分终端**:同组内水平并排,+号旁下拉菜单(新建 / 拆分 / 按种类新建),VSCode 风格操作
|
|
13
13
|
- **会话持久化**:面板收起后会话保持存活,重新打开后自动恢复
|
|
14
|
-
-
|
|
14
|
+
- **快捷键开关**:可配置快捷键呼出/收起面板;接入宿主统一快捷键系统,在设置界面改键后标签跟随更新
|
|
15
15
|
- **拖拽调高**:拖拽面板上边缘调整高度
|
|
16
16
|
- **跨平台**:Windows ConPTY / POSIX openpty,由 `@lydell/node-pty` 自动选择,二进制随 tarball 分发,安装即用
|
|
17
|
-
-
|
|
17
|
+
- **配置表驱动终端**:终端配置表(type / name / path)驱动新建终端;启动时自动探测 $PATH 中的终端,可在设置界面新增、改名、编辑路径
|
|
18
|
+
- **字体可配置**:字体族、字号、行高可配置,配置变更对已打开终端即时生效
|
|
18
19
|
|
|
19
20
|
## 安装
|
|
20
21
|
|
|
@@ -28,16 +29,22 @@ dsh plugin --profile web add github:btsd321/dsh-oh-my-terminal
|
|
|
28
29
|
|
|
29
30
|
| 配置项 | 说明 | 默认值 |
|
|
30
31
|
|---|---|---|
|
|
31
|
-
| `toggleShortcut` | 呼出/收起终端面板的快捷键(仅旧宿主生效,见下方适配说明) | `` Ctrl+` `` |
|
|
32
|
-
| `
|
|
32
|
+
| `toggleShortcut` | 呼出/收起终端面板的快捷键(仅旧宿主生效,见下方适配说明) | `` Ctrl+Shift+` `` |
|
|
33
|
+
| `fontFamily` | 终端字体族(CSS `font-family` 串);留空使用内置默认字体栈(含 CJK 回退) | `''` |
|
|
34
|
+
| `fontSize` | 终端字号(像素) | `12.5` |
|
|
35
|
+
| `lineHeight` | 终端行高倍数 | `1.25` |
|
|
36
|
+
| `terminalProfiles` | 终端配置表(JSON 数组);留空 = 用启动时探测到的 $PATH 终端 | `''` |
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
### 终端配置表
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
终端配置表是 `{ id, type, name, path, origin }` 条目的 JSON 数组:
|
|
37
41
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
42
|
+
- **type**:决定 spawn 语义 — `pwsh` / `powershell` / `cmd` / `bash` / `zsh` / `fish` / `gitbash` / `nushell` / `custom`
|
|
43
|
+
- **name**:下拉菜单显示名(用户可改)
|
|
44
|
+
- **path**:可执行文件路径;留空 = 按 type 在 `$PATH` 中解析
|
|
45
|
+
- **origin**:`auto`(启动探测)/ `user`(手动新增)
|
|
46
|
+
|
|
47
|
+
自动探测项(`origin: auto`)不可删除、路径不可改,但名称始终可编辑。配置表可在设置界面内 inline 编辑、新增行、删除。
|
|
41
48
|
|
|
42
49
|
## 开发
|
|
43
50
|
|
|
@@ -73,25 +80,30 @@ pnpm exec tsx --test tests/unit/*.test.ts
|
|
|
73
80
|
|
|
74
81
|
```
|
|
75
82
|
src/
|
|
76
|
-
├── index.ts
|
|
77
|
-
├── routes.ts
|
|
78
|
-
├── ws-handler.ts
|
|
79
|
-
├── client.tsx
|
|
83
|
+
├── index.ts # 宿主半入口(Cordis 插件 + settings + 会话生命周期)
|
|
84
|
+
├── routes.ts # HTTP 路由处理器
|
|
85
|
+
├── ws-handler.ts # WebSocket 处理器(pty 数据转发)
|
|
86
|
+
├── client.tsx # 浏览器半入口(React 组件 + 插件注册)
|
|
80
87
|
├── client/
|
|
81
|
-
│ ├── types.ts
|
|
82
|
-
│ ├── hooks.ts
|
|
83
|
-
│ ├── term-pane.tsx
|
|
84
|
-
│ ├── dropdown.tsx
|
|
85
|
-
│ ├── side-list.tsx
|
|
86
|
-
│ ├── styles.ts
|
|
87
|
-
│ ├── icons.tsx
|
|
88
|
-
│
|
|
89
|
-
├──
|
|
90
|
-
├──
|
|
91
|
-
|
|
92
|
-
├──
|
|
93
|
-
├──
|
|
94
|
-
|
|
88
|
+
│ ├── types.ts # 共享类型
|
|
89
|
+
│ ├── hooks.ts # useReducer 状态管理 + 自定义 Hooks
|
|
90
|
+
│ ├── term-pane.tsx # xterm.js 终端面板组件
|
|
91
|
+
│ ├── dropdown.tsx # +号旁下拉菜单
|
|
92
|
+
│ ├── side-list.tsx # 右侧终端列表
|
|
93
|
+
│ ├── styles.ts # CSS 常量 + Campbell 暗色主题
|
|
94
|
+
│ ├── icons.tsx # SVG 图标组件
|
|
95
|
+
│ ├── clipboard.ts # 剪贴板工具函数
|
|
96
|
+
│ ├── shortcut-bridge.ts # shortcuts 服务接入桥(命令注册 + 面板切换回调)
|
|
97
|
+
│ ├── settings/ # 设置界面(types / store / card / profile-table / api / styles)
|
|
98
|
+
│ └── terminal/ # 终端标签页状态(use-tabs / reducer / use-terminal-state / geometry)
|
|
99
|
+
├── persistence.ts # 会话持久化层(日志落盘 / 元数据 / 启动恢复)
|
|
100
|
+
├── platform.ts # 平台适配层(POSIX / Windows + shell 探测)
|
|
101
|
+
├── terminal/ # 终端种类、探测、解析、配置表校验
|
|
102
|
+
├── settings/ # Settings 桥接层(命名空间 / 路由 / patch 操作)
|
|
103
|
+
├── constants.ts # 协议 / 尺寸 / 快捷键 / 环境变量常量
|
|
104
|
+
├── server-command.ts # 命令行解析工具
|
|
105
|
+
├── shortcut.ts # 快捷键解析工具
|
|
106
|
+
└── logger.ts # 统一日志工具
|
|
95
107
|
```
|
|
96
108
|
|
|
97
109
|
宿主半与浏览器半经 WebSocket 通信,路由前缀 `/api/dsh-oh-my-terminal`,不直接 import。
|
package/lib/index.js
CHANGED
|
@@ -1014,17 +1014,28 @@ function createSettingsBridgeRoutes(settings, namespace, runtimeProfiles) {
|
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
1016
|
// src/settings/index.ts
|
|
1017
|
-
function
|
|
1017
|
+
function registerSettingsNamespace(ctx, schema) {
|
|
1018
1018
|
ctx.inject(["settings"], (sctx) => {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1019
|
+
const facade = sctx.settings;
|
|
1020
|
+
if (typeof facade.configure === "function") {
|
|
1021
|
+
const configure = facade.configure;
|
|
1022
|
+
sctx.effect(() => {
|
|
1023
|
+
const cleanup = configure.call(facade, { auto: false }, ctx.fiber);
|
|
1024
|
+
return typeof cleanup === "function" ? cleanup : () => void 0;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof facade.register === "function") {
|
|
1028
|
+
try {
|
|
1029
|
+
facade.register(SETTINGS_NS, schema);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1032
|
+
if (!message.includes("already registered")) throw error;
|
|
1033
|
+
}
|
|
1026
1034
|
}
|
|
1027
1035
|
});
|
|
1036
|
+
}
|
|
1037
|
+
function registerSettingsIntegration(ctx, schema, runtimeProfiles) {
|
|
1038
|
+
registerSettingsNamespace(ctx, schema);
|
|
1028
1039
|
ctx.inject(["webServer", "settings"], (sctx) => {
|
|
1029
1040
|
sctx.effect(() => {
|
|
1030
1041
|
const routes = createSettingsBridgeRoutes(sctx.settings, SETTINGS_NS, runtimeProfiles);
|
|
@@ -1146,7 +1157,7 @@ function apply(ctx, config) {
|
|
|
1146
1157
|
ctx.inject(["settings"], (sctx) => {
|
|
1147
1158
|
const settings = sctx.settings;
|
|
1148
1159
|
if (typeof settings.update !== "function") return;
|
|
1149
|
-
void settings.update(
|
|
1160
|
+
void settings.update(SETTINGS_NS, { terminalProfiles: payload }).then(() => {
|
|
1150
1161
|
log3.info(`\u7EC8\u7AEF\u914D\u7F6E\u8868\u5DF2\u521D\u59CB\u5316\uFF1A\u63A2\u6D4B\u5230 ${merged.length} \u4E2A\u7EC8\u7AEF`);
|
|
1151
1162
|
}).catch((error) => {
|
|
1152
1163
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -1156,6 +1167,7 @@ function apply(ctx, config) {
|
|
|
1156
1167
|
}
|
|
1157
1168
|
return merged;
|
|
1158
1169
|
}
|
|
1170
|
+
registerSettingsNamespace(ctx, Config);
|
|
1159
1171
|
const cachedProfiles = prepareProfiles();
|
|
1160
1172
|
const sessions = /* @__PURE__ */ new Map();
|
|
1161
1173
|
const upgradeDisposers = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-oh-my-terminal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "DSH Web GUI 底部终端面板插件——node-pty 驱动的多标签交互式终端(Windows ConPTY / POSIX openpty)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -35,12 +35,11 @@
|
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh-brand": "0.
|
|
39
|
-
"@deepseek-ai/dsh-client-shortcuts": "0.
|
|
40
|
-
"@deepseek-ai/dsh-client-store": "0.
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/dsh-settings": "0.1.7-rc.2",
|
|
38
|
+
"@deepseek-ai/dsh-brand": "0.2.0-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-client-shortcuts": "0.2.0-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.1",
|
|
44
43
|
"@types/node": "^22.0.0",
|
|
45
44
|
"@types/react": "^18.2.0",
|
|
46
45
|
"@types/ws": "^8.5.0",
|
package/src/index.ts
CHANGED
|
@@ -61,7 +61,7 @@ import { createWsHandlers } from './ws-handler.js';
|
|
|
61
61
|
import { createRouteHandler } from './routes.js';
|
|
62
62
|
import type { CreateSessionOptions } from './routes.js';
|
|
63
63
|
import { makeId, getSessionCounter, resolveSpawn, resolveSessionCwd } from './routes.js';
|
|
64
|
-
import { registerSettingsIntegration } from './settings/index.js';
|
|
64
|
+
import { registerSettingsIntegration, registerSettingsNamespace, SETTINGS_NS } from './settings/index.js';
|
|
65
65
|
import type { TerminalProfile } from './terminal/index.js';
|
|
66
66
|
import { detectTerminalProfiles, mergeProfiles } from './terminal/index.js';
|
|
67
67
|
|
|
@@ -317,7 +317,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
317
317
|
ctx.inject(['settings'], (sctx) => {
|
|
318
318
|
const settings = sctx.settings as { update?: (ns: string, patch: object) => Promise<void> };
|
|
319
319
|
if (typeof settings.update !== 'function') return;
|
|
320
|
-
|
|
320
|
+
// 命名空间必须与 settings 集成注册的 SETTINGS_NS 逐字一致——
|
|
321
|
+
// 此前误用包名派生串(PKG_NAME 去前缀 = 'oh-my-terminal'),与注册的
|
|
322
|
+
// 'terminal-panel' 对不上,首次落盘恒报「namespace not registered」
|
|
323
|
+
void settings.update(SETTINGS_NS, { terminalProfiles: payload })
|
|
321
324
|
.then(() => { log.info(`终端配置表已初始化:探测到 ${merged.length} 个终端`); })
|
|
322
325
|
.catch((error: unknown) => {
|
|
323
326
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -329,6 +332,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
/** 启动时求值的终端配置表(探测 + 合并结果) */
|
|
335
|
+
// 命名空间注册必须先于 prepareProfiles 的首跑写回(时序竞态:写回早于注册会
|
|
336
|
+
// 报「namespace is not registered」)——故在此提前注册,settings/index.ts
|
|
337
|
+
// 内部保留幂等兜底
|
|
338
|
+
registerSettingsNamespace(ctx, Config);
|
|
332
339
|
const cachedProfiles: TerminalProfile[] = prepareProfiles();
|
|
333
340
|
|
|
334
341
|
/** id -> 会话记录 */
|
package/src/settings/index.ts
CHANGED
|
@@ -43,38 +43,74 @@ export { createSettingsBridgeRoutes } from './bridge.js';
|
|
|
43
43
|
* @param runtimeProfiles - 运行时终端配置表(探测+合并后的完整数组),传给 bridge
|
|
44
44
|
* 供 describe 端点覆盖持久化的 terminalProfiles 值
|
|
45
45
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 注册 settings 命名空间(幂等,须先于任何 settings 读写)。
|
|
48
|
+
*
|
|
49
|
+
* 命名空间注册是 settings 服务注册表的唯一入口;首次配置落盘(prepareProfiles
|
|
50
|
+
* 的首跑写回)等读写都依赖它——必须在 apply 早期调用本函数,晚于首次写回会
|
|
51
|
+
* 报「namespace is not registered」(时序竞态)。重复注册(热重载、重复 apply、
|
|
52
|
+
* 以及 registerSettingsIntegration 的兜底调用)抛 "already registered",作为幂等
|
|
53
|
+
* 情形吞掉;其余注册失败(如存储段损坏)如实上抛,不把失败包装成成功。
|
|
54
|
+
*
|
|
55
|
+
* @param ctx - cordis 上下文
|
|
56
|
+
* @param schema - 插件 Config 的 schemastery schema
|
|
57
|
+
*/
|
|
58
|
+
export function registerSettingsNamespace(ctx: Context, schema: unknown): void {
|
|
59
|
+
// 注册命名空间 +(可选)关闭自动模式——软探测,服务不存在时不激活
|
|
52
60
|
ctx.inject(['settings'], (sctx) => {
|
|
53
|
-
|
|
61
|
+
/** settings 服务能力面(不同宿主版本暴露面不同,逐能力探测) */
|
|
62
|
+
interface SettingsFacade {
|
|
63
|
+
/** 新宿主可选能力:关闭自动写回;旧宿主(0.1.5 系)无此方法 */
|
|
64
|
+
configure?: (options: { auto: boolean }, fiber?: unknown) => void;
|
|
65
|
+
/**
|
|
66
|
+
* 注册命名空间 schema;重复注册抛 "already registered"。
|
|
67
|
+
* 可选——宿主 SettingsForms 靠 cordis loader entries 发现命名空间,
|
|
68
|
+
* 不一定提供此方法(如 0.1.7-rc.1 的 SettingsForms 无 register)。
|
|
69
|
+
*/
|
|
70
|
+
register?: (ns: string, schema: unknown) => unknown;
|
|
71
|
+
}
|
|
72
|
+
const facade = sctx.settings as unknown as SettingsFacade;
|
|
73
|
+
|
|
74
|
+
// configure 仅在宿主提供时调用——0.1.5 系宿主的 settings 服务没有该方法,
|
|
75
|
+
// 直接调用会 TypeError 并中断本回调的后续初始化(包括下面的命名空间注册):
|
|
54
76
|
// - auto=true:settings 服务自动从 profile patch 读配置并写回插件 Config
|
|
55
77
|
// - auto=false:插件自行管理配置(本插件的 Config 字段都是 volatile 引用,
|
|
56
78
|
// 由 cordis loader 注入,无需 settings 服务二次写入)
|
|
57
79
|
//
|
|
58
80
|
// fiber 参数绑定配置生命周期到当前 effect——effect 销毁时配置自动清理
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
interface SettingsRegistrar {
|
|
67
|
-
/** 注册命名空间 schema;重复注册抛 "already registered" */
|
|
68
|
-
register(ns: string, schema: unknown): unknown;
|
|
81
|
+
if (typeof facade.configure === 'function') {
|
|
82
|
+
const configure = facade.configure;
|
|
83
|
+
sctx.effect(() => {
|
|
84
|
+
const cleanup = configure.call(facade, { auto: false }, ctx.fiber);
|
|
85
|
+
// effect 要求回调返回清理函数;configure 的返回值即其注册的清理效应
|
|
86
|
+
return typeof cleanup === 'function' ? cleanup : (): void => undefined;
|
|
87
|
+
});
|
|
69
88
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
|
|
90
|
+
// register 同样逐能力探测——宿主 SettingsForms 靠 cordis loader entries
|
|
91
|
+
// (configEditor.configuration())发现命名空间,不一定提供 register 方法
|
|
92
|
+
// (0.1.7-rc.1 的 SettingsForms 无此方法)。方法不存在时跳过,不抛 TypeError;
|
|
93
|
+
// 方法存在时重复注册(热重载、重复 apply)抛 "already registered",作为幂等
|
|
94
|
+
// 情形吞掉;其余注册失败(如存储段损坏)如实上抛,不把失败包装成成功。
|
|
95
|
+
if (typeof facade.register === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
facade.register(SETTINGS_NS, schema);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
if (!message.includes('already registered')) throw error;
|
|
101
|
+
}
|
|
76
102
|
}
|
|
77
103
|
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerSettingsIntegration(
|
|
107
|
+
ctx: Context,
|
|
108
|
+
schema: unknown,
|
|
109
|
+
runtimeProfiles?: unknown[],
|
|
110
|
+
): void {
|
|
111
|
+
// 0. 命名空间注册兜底(幂等)——正常由调用方在 apply 早期先行调用
|
|
112
|
+
// registerSettingsNamespace,此处重复注册被吞
|
|
113
|
+
registerSettingsNamespace(ctx, schema);
|
|
78
114
|
|
|
79
115
|
// 2. 注册 settings bridge 路由(describe + mutate)——软探测,服务不存在时不激活
|
|
80
116
|
ctx.inject(['webServer', 'settings'], (sctx) => {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file settings 集成注册单元测试
|
|
3
3
|
* @description 回归锁定 registerSettingsIntegration 的命名空间注册契约:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 注册是 settings 服务注册表的唯一入口,缺注册时 describe/mutate
|
|
5
|
+
* 恒报「命名空间不存在」,前端配置卡片静默不渲染(无报错、无入口)。
|
|
6
|
+
* 同时锁定宿主兼容面:旧宿主(0.1.5 系)的 settings 服务没有
|
|
7
|
+
* configure 方法,集成不得因此抛错中断注册——曾因无守卫调用
|
|
8
|
+
* configure 抛 TypeError,把随后的 register 一并挡掉。
|
|
7
9
|
* 本测试用假 settings 服务断言:注册确实发生且 schema 原样透传、
|
|
8
|
-
*
|
|
10
|
+
* 无 configure 的宿主不炸、有 configure 时才调用、重复注册幂等、
|
|
11
|
+
* 其余注册失败如实上抛(不把失败包装成成功)。
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { describe, it } from 'node:test';
|
|
@@ -21,19 +24,27 @@ interface RegisterCall {
|
|
|
21
24
|
schema: unknown;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
/** 假 settings
|
|
25
|
-
function makeFakeSettings(options: {
|
|
27
|
+
/** 假 settings 服务:可选 configure/register 模拟不同宿主版本的暴露面 */
|
|
28
|
+
function makeFakeSettings(options: {
|
|
29
|
+
withConfigure?: boolean;
|
|
30
|
+
withRegister?: boolean;
|
|
31
|
+
failRegisterWith?: Error;
|
|
32
|
+
} = {}): {
|
|
26
33
|
settings: Record<string, unknown>;
|
|
27
34
|
registerCalls: RegisterCall[];
|
|
28
35
|
configureCalls: unknown[];
|
|
29
36
|
} {
|
|
30
37
|
const registerCalls: RegisterCall[] = [];
|
|
31
38
|
const configureCalls: unknown[] = [];
|
|
32
|
-
const settings = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const settings: Record<string, unknown> = {
|
|
40
|
+
describe: async (): Promise<unknown> => [],
|
|
41
|
+
mutate: async (): Promise<unknown> => undefined,
|
|
42
|
+
writable: true,
|
|
43
|
+
};
|
|
44
|
+
// register 可选——0.1.7-rc.1 的 SettingsForms 靠 cordis loader entries 发现
|
|
45
|
+
// 命名空间,不提供 register 方法
|
|
46
|
+
if (options.withRegister !== false) {
|
|
47
|
+
settings.register = (ns: string, schema: unknown): unknown => {
|
|
37
48
|
if (options.failRegisterWith !== undefined) throw options.failRegisterWith;
|
|
38
49
|
registerCalls.push({ ns, schema });
|
|
39
50
|
return {
|
|
@@ -42,10 +53,13 @@ function makeFakeSettings(options: { failRegisterWith?: Error } = {}): {
|
|
|
42
53
|
update: async (): Promise<void> => undefined,
|
|
43
54
|
replace: async (): Promise<void> => undefined,
|
|
44
55
|
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (options.withConfigure === true) {
|
|
59
|
+
settings.configure = (value: unknown): void => {
|
|
60
|
+
configureCalls.push(value);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
49
63
|
return { settings, registerCalls, configureCalls };
|
|
50
64
|
}
|
|
51
65
|
|
|
@@ -68,14 +82,24 @@ function makeFakeContext(settings: unknown): Context {
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
describe('settings 集成注册', () => {
|
|
71
|
-
it('
|
|
85
|
+
it('旧宿主(无 configure)下仍完成命名空间注册,不抛错', () => {
|
|
72
86
|
const { settings, registerCalls, configureCalls } = makeFakeSettings();
|
|
73
87
|
const schema = { marker: 'config-schema' };
|
|
74
|
-
|
|
88
|
+
assert.doesNotThrow(() => {
|
|
89
|
+
registerSettingsIntegration(makeFakeContext(settings), schema, []);
|
|
90
|
+
});
|
|
75
91
|
assert.equal(registerCalls.length, 1);
|
|
76
92
|
assert.equal(registerCalls[0]?.ns, SETTINGS_NS);
|
|
77
93
|
assert.equal(registerCalls[0]?.schema, schema);
|
|
94
|
+
assert.deepEqual(configureCalls, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('新宿主(有 configure)时调用 auto:false 并照常注册', () => {
|
|
98
|
+
const { settings, registerCalls, configureCalls } = makeFakeSettings({ withConfigure: true });
|
|
99
|
+
registerSettingsIntegration(makeFakeContext(settings), { marker: 'x' });
|
|
78
100
|
assert.deepEqual(configureCalls, [{ auto: false }]);
|
|
101
|
+
assert.equal(registerCalls.length, 1);
|
|
102
|
+
assert.equal(registerCalls[0]?.ns, SETTINGS_NS);
|
|
79
103
|
});
|
|
80
104
|
|
|
81
105
|
it('重复注册视为幂等情形,吞掉 already registered 不上抛', () => {
|
|
@@ -96,4 +120,13 @@ describe('settings 集成注册', () => {
|
|
|
96
120
|
/存储段损坏/u,
|
|
97
121
|
);
|
|
98
122
|
});
|
|
123
|
+
|
|
124
|
+
it('宿主无 register 方法时不抛 TypeError(靠 cordis loader entries 发现命名空间)', () => {
|
|
125
|
+
const { settings, registerCalls } = makeFakeSettings({ withRegister: false });
|
|
126
|
+
assert.doesNotThrow(() => {
|
|
127
|
+
registerSettingsIntegration(makeFakeContext(settings), { marker: 'x' });
|
|
128
|
+
});
|
|
129
|
+
// 宿主无 register——registerCalls 为空,但不抛错
|
|
130
|
+
assert.equal(registerCalls.length, 0);
|
|
131
|
+
});
|
|
99
132
|
});
|