dsh-oh-my-terminal 0.2.2

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 (56) hide show
  1. package/CLAUDE.md +167 -0
  2. package/LICENSE +218 -0
  3. package/README.md +103 -0
  4. package/README.zh.md +101 -0
  5. package/cordis.patch.yml +3 -0
  6. package/docs/git-workflow.md +227 -0
  7. package/docs/type_script_style.md +392 -0
  8. package/lib/client.js +11894 -0
  9. package/lib/index.js +1296 -0
  10. package/lib/xterm.css +285 -0
  11. package/package.json +60 -0
  12. package/pnpm-workspace.yaml +3 -0
  13. package/scripts/build.ts +60 -0
  14. package/src/client/clipboard.ts +148 -0
  15. package/src/client/dropdown.tsx +172 -0
  16. package/src/client/hooks.ts +20 -0
  17. package/src/client/icons.tsx +148 -0
  18. package/src/client/settings/api.ts +190 -0
  19. package/src/client/settings/card.tsx +393 -0
  20. package/src/client/settings/profile-table.tsx +652 -0
  21. package/src/client/settings/store.ts +260 -0
  22. package/src/client/settings/styles.ts +75 -0
  23. package/src/client/settings/types.ts +115 -0
  24. package/src/client/shortcut-bridge.ts +100 -0
  25. package/src/client/side-list.tsx +310 -0
  26. package/src/client/styles.ts +211 -0
  27. package/src/client/term-pane.tsx +282 -0
  28. package/src/client/terminal/reducer.ts +200 -0
  29. package/src/client/terminal/use-panel-geometry.ts +138 -0
  30. package/src/client/terminal/use-tabs.ts +386 -0
  31. package/src/client/terminal/use-terminal-state.ts +119 -0
  32. package/src/client/types.ts +144 -0
  33. package/src/client.tsx +492 -0
  34. package/src/constants.ts +108 -0
  35. package/src/index.ts +539 -0
  36. package/src/logger.ts +115 -0
  37. package/src/persistence.ts +319 -0
  38. package/src/platform.ts +173 -0
  39. package/src/routes.ts +457 -0
  40. package/src/server-command.ts +55 -0
  41. package/src/settings/bridge.ts +341 -0
  42. package/src/settings/index.ts +73 -0
  43. package/src/settings/namespace.ts +51 -0
  44. package/src/settings/patch.ts +101 -0
  45. package/src/shortcut.ts +142 -0
  46. package/src/terminal/detect.ts +146 -0
  47. package/src/terminal/index.ts +44 -0
  48. package/src/terminal/kinds.ts +179 -0
  49. package/src/terminal/resolve.ts +97 -0
  50. package/src/terminal/store.ts +268 -0
  51. package/src/ws-handler.ts +164 -0
  52. package/tests/unit/config-schema.test.ts +119 -0
  53. package/tests/unit/server-command.test.ts +188 -0
  54. package/tests/unit/shortcut-bridge.test.ts +83 -0
  55. package/tests/unit/shortcut.test.ts +371 -0
  56. package/tsconfig.json +21 -0
@@ -0,0 +1,227 @@
1
+ # Git 分支管理与提交规范
2
+
3
+ 本文档是 `dsh-oh-my-terminal` 仓库的 Git 工作流约定。AI agent 与人类开发者在本仓库中创建分支、提交代码、合并变更时**必须**遵循本规范。
4
+
5
+ ---
6
+
7
+ ## 一、分支模型
8
+
9
+ 本项目采用**短生命周期特性分支 + 主干发布**模型(参考 Trunk-Based Development),适配 AI agent 并行协作场景。
10
+
11
+ ### 1.1 长期分支
12
+
13
+ | 分支 | 用途 | 保护规则 |
14
+ |------|------|---------|
15
+ | `master` | 发布分支,始终可部署 | 只接受从 `develop` 的 `--no-ff` merge |
16
+ | `develop` | 集成分支,所有特性在此汇聚 | 只接受从特性分支的 `--no-ff` merge |
17
+
18
+ **核心约束:合并方向单向——`develop → master`,禁止反向合并。**
19
+
20
+ ```
21
+ 特性分支 ──→ develop ──→ master
22
+ ↑
23
+ (唯一入口)
24
+ ```
25
+
26
+ - ✅ `git checkout master && git merge develop --no-ff`
27
+ - ❌ `git checkout develop && git merge master`(禁止)
28
+
29
+ 当 master 上有 develop 没有的 commit 时(如 hotfix),应通过 cherry-pick 或重新在 develop 上做相同改动来同步,而非反向 merge。
30
+
31
+ ### 1.2 特性分支
32
+
33
+ 命名格式:`<type>/<short-description>`
34
+
35
+ | 前缀 | 用途 | 示例 |
36
+ |------|------|------|
37
+ | `feat/` | 新功能 | `feat/multi-tab-switching` |
38
+ | `fix/` | Bug 修复 | `fix/websocket-leak` |
39
+ | `refactor/` | 重构(不改行为) | `refactor/extract-shortcut-module` |
40
+ | `docs/` | 纯文档更新 | `docs/update-readme` |
41
+ | `test/` | 纯测试补充 | `test/shortcut-unit` |
42
+ | `chore/` | 构建/工具/配置 | `chore/upgrade-pnpm` |
43
+
44
+ **生命周期规则:**
45
+
46
+ 1. 从 `develop` 的最新 HEAD 创建:`git checkout develop && git branch <name>`
47
+ 2. 完成后以 `--no-ff` merge 回 `develop`
48
+ 3. merge 后立即删除本地特性分支
49
+ 4. 特性分支**不直接 merge 到 master**
50
+ 5. 特性分支之间**不互相 merge**——如有依赖,先合并上游分支到 develop,再从 develop rebase
51
+
52
+ ### 1.3 AI Agent 分支约定
53
+
54
+ AI agent 在本仓库工作时,除遵循上述规则外,还需注意:
55
+
56
+ 1. **每个 agent 任务使用独立分支**:避免多个 agent 在同一分支上并发写入导致冲突
57
+ 2. **写作用户不重叠**:Lead 分配任务时确保不同 agent 修改的文件集合不相交
58
+ 3. **agent 不自行 merge 到 develop/master**:agent 完成工作后提交到特性分支,由 Lead 审核并执行 merge
59
+ 4. **agent 不做反向 merge**:如果 agent 需要同步其他分支的改动,应通知 Lead 处理
60
+ 5. **特性分支命名加编号**:便于追踪,如 `refactor/1-extract-shortcut`
61
+
62
+ ### 1.4 发布流程
63
+
64
+ ```
65
+ develop (稳定) ──merge──→ master ──tag──→ v0.x.y
66
+ ```
67
+
68
+ 1. 确认 develop 上所有改动已通过 typecheck、单元测试
69
+ 2. `git checkout master && git merge develop --no-ff -m "merge: develop → master — <摘要>"`
70
+ 3. 如需版本号变更,在 master 上修改 `package.json` 并提交
71
+ 4. `git tag -a v0.x.y -m "v0.x.y: <发布摘要>"`
72
+ 5. push master 和 tag
73
+
74
+ ---
75
+
76
+ ## 二、提交规范
77
+
78
+ 采用 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/v1.0.0/) 规范,提交信息一律使用**中文描述**。
79
+
80
+ ### 2.1 格式
81
+
82
+ ```
83
+ <类型>(<范围>): <中文描述>
84
+
85
+ [可选正文]
86
+
87
+ [可选脚注]
88
+ ```
89
+
90
+ ### 2.2 类型
91
+
92
+ | 类型 | 含义 | SemVer 对应 |
93
+ |------|------|------------|
94
+ | `feat` | 新功能 | MINOR |
95
+ | `fix` | Bug 修复 | PATCH |
96
+ | `refactor` | 重构(不改外部行为) | — |
97
+ | `docs` | 文档更新 | — |
98
+ | `test` | 测试增删改 | — |
99
+ | `chore` | 构建/工具/配置/依赖 | — |
100
+ | `perf` | 性能优化 | PATCH |
101
+ | `style` | 代码格式(不影响逻辑) | — |
102
+ | `ci` | CI/CD 配置 | — |
103
+ | `revert` | 回滚 | — |
104
+
105
+ ### 2.3 范围(scope)
106
+
107
+ 使用模块名,与项目架构对齐:
108
+
109
+ | 范围 | 对应模块 |
110
+ |------|---------|
111
+ | `index` | src/index.ts |
112
+ | `client` | src/client.tsx |
113
+ | `persistence` | src/persistence.ts |
114
+ | `platform` | src/platform.ts |
115
+ | `constants` | src/constants.ts |
116
+ | `server-command` | src/server-command.ts |
117
+ | `shortcut` | src/shortcut.ts |
118
+ | `logger` | src/logger.ts |
119
+ | `scripts` | scripts/ |
120
+ | `tests` | tests/ |
121
+
122
+ 省略范围也可以,但当改动集中在单个模块时推荐标注。
123
+
124
+ ### 2.4 描述要求
125
+
126
+ - 使用中文,简洁明了(≤ 50 字为佳)
127
+ - 祈使语气:"提取…"、"修复…"、"新增…",不用"了"、"过"等过去时态助词
128
+ - 不以句号结尾
129
+ - 首字母不大写(中文无此概念)
130
+
131
+ ### 2.5 正文与脚注
132
+
133
+ - 正文用空行与标题分隔,解释"为什么"而非"做了什么"
134
+ - 破坏性变更必须在标题加 `!` 或在脚注写 `BREAKING CHANGE:`
135
+ - 引用 issue/PR 用 `Refs: #123` 脚注
136
+
137
+ ### 2.6 示例
138
+
139
+ ```
140
+ refactor(shortcut): 提取 shortcut.ts 独立模块,消除 index.ts 重复解析代码
141
+
142
+ 快捷键解析逻辑从 index.ts 提取到独立 shortcut.ts,便于单元测试与复用。
143
+
144
+ Refs: #12
145
+ ```
146
+
147
+ ```
148
+ fix(client): WebSocket 关闭时未清理 pty 导致进程泄漏
149
+
150
+ 面板关闭只断开 WebSocket 未 kill pty,导致 @lydell/node-pty 进程成为孤儿
151
+ 持续占用终端。
152
+
153
+ Refs: #287
154
+ ```
155
+
156
+ ```
157
+ feat!(index): 支持多 shell 实例并发
158
+
159
+ BREAKING CHANGE: TerminalSession 构造函数新增 shellIndex 参数,
160
+ 单实例初始化方式不再可用。
161
+ ```
162
+
163
+ ```
164
+ docs: 更新 README 配置说明
165
+ ```
166
+
167
+ ```
168
+ test(shortcut): 为 shortcut.ts 添加单元测试
169
+
170
+ 覆盖基本功能、修饰键组合、特殊字符、边界情况。
171
+ ```
172
+
173
+ ### 2.7 Merge Commit 格式
174
+
175
+ merge commit 也遵循约定式提交格式:
176
+
177
+ ```
178
+ merge: develop → master — <本次合并的摘要>
179
+ ```
180
+
181
+ 或带范围:
182
+
183
+ ```
184
+ merge(shortcut): develop → master — 快捷键解析模块提取
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 三、Agent 工作流检查清单
190
+
191
+ AI agent 在本仓库执行代码变更时,按以下清单自检:
192
+
193
+ ### 开始前
194
+
195
+ - [ ] 确认当前在正确的特性分支上(不在 master/develop 上直接改)
196
+ - [ ] 确认特性分支从 develop 最新 HEAD 创建
197
+ - [ ] 确认 Lead 分配的写作用户与其他 agent 不重叠
198
+
199
+ ### 工作中
200
+
201
+ - [ ] 每个逻辑独立的改动单独 commit(不攒大提交)
202
+ - [ ] commit message 符合 Conventional Commits 格式
203
+ - [ ] 中文描述,注释用中文
204
+ - [ ] 运行 `pnpm run typecheck` 确认零错误
205
+ - [ ] 提交前运行 `pnpm run build` 更新 lib/ 构建产物,并将 lib/ 变更一并提交
206
+
207
+ ### 完成后
208
+
209
+ - [ ] 所有改动已 commit 到特性分支
210
+ - [ ] 向 Lead 报告完成状态、commit hash、验证结果
211
+ - [ ] **不自行 merge 到 develop 或 master**
212
+ - [ ] **不做 master → develop 的反向 merge**
213
+
214
+ ---
215
+
216
+ ## 四、禁止事项
217
+
218
+ | 禁止操作 | 原因 |
219
+ |---------|------|
220
+ | 直接在 master/develop 上 commit | 所有改动必须经特性分支 |
221
+ | master → develop 的 merge | 破坏单向合并规则,产生混乱历史 |
222
+ | 特性分支之间互相 merge | 应通过 develop 中转 |
223
+ | force push 已推送的分支 | 除非 Lead 明确要求重写历史 |
224
+ | 使用 `git merge --squash` | 保留完整提交历史,merge 时用 `--no-ff` |
225
+ | 提交含 `.claude/`、`.agents/`、`agent/`、`tasks/` 的文件 | 已在 .gitignore 中排除 |
226
+ | 提交信息用英文 | 本仓库一律中文 |
227
+ | 提交 src/ 改动时不带 lib/ 更新 | lib/ 是构建产物,必须与源码同步提交 |
@@ -0,0 +1,392 @@
1
+ # TypeScript 编程规范(AI 编程约定)
2
+
3
+ 本文档是 `dsh-oh-my-terminal` 仓库的代码风格与工程约定。AI 与人类开发者写任何 TypeScript 代码前都应遵循本规范。
4
+
5
+ 核心原则:**模块化、工程化、注释完备**。新代码要读起来像仓库里已有的代码,而不是像另一个人写的。
6
+
7
+ ---
8
+
9
+ ## 一、注释规范
10
+
11
+ 注释一律用**中文**。注释解释"为什么"和"约束是什么",不重复代码已经说清楚的"做了什么"。
12
+
13
+ ### 1.1 文件头(必需)
14
+
15
+ 每个 `.ts` 文件开头必须有文件级 JSDoc,包含 `@file` 和 `@description`。`@description` 要写清这个模块在整体架构中的位置和职责边界,而非罗列函数名。
16
+
17
+ 涉及协议、安全、跨模块契约的文件,在 `@description` 之后追加分节说明(流程、约束、兼容性要求)。
18
+
19
+ ```typescript
20
+ /**
21
+ * @file 宿主半终端管理模块
22
+ * @description 管理 @lydell/node-pty 进程的创建、销毁与 WebSocket 数据转发。
23
+ * pty 进程与 WebSocket 一一绑定,面板关闭时同时销毁两者。
24
+ *
25
+ * 安全约束:
26
+ * - WebSocket 升级只走已鉴权通道,未鉴权连接直接拒绝
27
+ * - 终端输出数据帧绝不写日志(用户会话内容)
28
+ * - socket error 监听器必须在任何 destroy 之前挂上,未处理 error 事件会直接掀翻进程
29
+ */
30
+ ```
31
+
32
+ 插件主入口这类需要被外部理解的模块,额外标注 `@module`。
33
+
34
+ ### 1.2 接口与类型(必需)
35
+
36
+ 接口本身要有 JSDoc 说明用途;**每个字段都要有单行 `/** */` 注释**。不要用 `//` 行注释代替字段文档,前者不会被 IDE 悬浮提示采集。
37
+
38
+ ```typescript
39
+ /** 终端会话配置 */
40
+ export interface TerminalSessionConfig {
41
+ /** shell 命令(如 bash、powershell) */
42
+ shellCommand: string;
43
+ /** shell 启动参数 */
44
+ shellArgs: string[];
45
+ /** 工作目录,省略时用进程 cwd */
46
+ cwd?: string;
47
+ /** 初始列数 */
48
+ cols: number;
49
+ /** 初始行数 */
50
+ rows: number;
51
+ }
52
+ ```
53
+
54
+ 字段含义有歧义时(单位、是否可空、默认值、取值范围)必须在注释里写明:
55
+
56
+ ```typescript
57
+ /** WebSocket 心跳间隔(毫秒) */
58
+ heartbeatIntervalMs?: number;
59
+ ```
60
+
61
+ 类型别名同样要有注释:
62
+
63
+ ```typescript
64
+ /** 终端面板展开状态标签 */
65
+ export type PanelState = 'collapsed' | 'expanding' | 'expanded' | 'collapsing';
66
+ ```
67
+
68
+ ### 1.3 类(必需)
69
+
70
+ 类的 JSDoc 说明它的职责和设计取舍。设计上有多个要点时用列表展开,让后来者不必读完整个类才知道边界在哪。
71
+
72
+ ```typescript
73
+ /**
74
+ * 终端会话管理器:一个 pty 进程 + 一个 WebSocket 的生命周期绑定。
75
+ *
76
+ * 核心设计:
77
+ * - pty 与 WebSocket 一一绑定,任一关闭时同时销毁另一个
78
+ * - 数据双向转发:pty 输出 → WebSocket 帧,WebSocket 帧 → pty 输入
79
+ * - 面板关闭不销毁会话(持久化),仅断开 WebSocket;重新打开时复用 pty
80
+ */
81
+ export class TerminalSession {
82
+ ```
83
+
84
+ 构造函数参数用 `@param` 标注。公开的 getter 用单行注释说明取值时机:
85
+
86
+ ```typescript
87
+ /** pty 进程是否仍存活;`spawn()` 之后可用 */
88
+ get isAlive(): boolean {
89
+ ```
90
+
91
+ ### 1.4 函数与方法(必需)
92
+
93
+ 所有函数——包括 `private` 方法和模块内的辅助函数——都要有 JSDoc。公开 API 必须写全 `@param` 和 `@returns`;会抛错的写 `@throws`。
94
+
95
+ ```typescript
96
+ /**
97
+ * 解析快捷键字符串为可匹配的键位组合
98
+ *
99
+ * 快捷键格式:"Ctrl+`" 或 "Cmd+Shift+P"(修饰键 + 主键,加号分隔)
100
+ * 修饰键不区分大小写,主键区分(字母键转大写匹配 KeyboardEvent.key)。
101
+ *
102
+ * @param shortcut - 快捷键字符串
103
+ * @returns 可与 KeyboardEvent 匹配的键位组合,解析失败返回 null
104
+ */
105
+ export function parseShortcut(shortcut: string): ParsedShortcut | null {
106
+ ```
107
+
108
+ 一行就说得清的私有方法可以用单行 JSDoc:
109
+
110
+ ```typescript
111
+ /** 断言 pty 进程仍然存活 */
112
+ private assertAlive(): void {
113
+ ```
114
+
115
+ `@param` 用 ` - ` 连接参数名和说明,保持全仓库一致。
116
+
117
+ ### 1.5 函数体内注释
118
+
119
+ 关键步骤用编号注释串起流程,让人能顺着读完主干:
120
+
121
+ ```typescript
122
+ // 1. 解析快捷键配置
123
+ // 2. 注册全局 keydown 监听器
124
+ // 3. 匹配命中时切换面板状态
125
+ ```
126
+
127
+ 以下三种情况**必须**写行内注释:
128
+
129
+ - **绕过常规做法的地方**——写清为什么
130
+
131
+ ```typescript
132
+ // 不用 pty.kill():Windows 上 ConPTY 有时无法干净终止子进程
133
+ // WebSocket 关闭码 1000 是正常关闭,不作为异常处理
134
+ ```
135
+
136
+ - **与外部系统的契约**——写清对方的行为
137
+
138
+ ```typescript
139
+ // @lydell/node-pty 的 onData 回调在进程输出时触发,高频调用
140
+ // xterm.js 的 write 接受 Uint8Array 或 string,二进制帧用 Uint8Array 更高效
141
+ ```
142
+
143
+ - **空的 catch 块**——必须说明为什么可以忽略,不允许留空白 `catch {}`
144
+
145
+ ```typescript
146
+ } catch { /* 面板已卸载,写状态失败可忽略 */ }
147
+ } catch { /* pty 已退出,销毁时再报错无意义 */ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 二、模块化
153
+
154
+ ### 2.1 一个文件一个职责
155
+
156
+ 文件名用 kebab-case,名字直接反映职责:`server-command.ts`、`shortcut.ts`。
157
+
158
+ 单文件超过约 600 行就该考虑拆分。拆分沿职责边界切,不要按"太长了"随意切半。
159
+
160
+ ### 2.2 分层与依赖方向
161
+
162
+ 依赖必须单向向下,不允许反向或环形引用:
163
+
164
+ ```
165
+ 宿主半 index.ts(入口 + 路由分发 + pty 生命周期 + WebSocket 升级 + settings 集成)
166
+ 持久化层 persistence.ts(SessionStore:日志落盘/清理、元数据读写、启动恢复)
167
+ 平台适配层 platform.ts(PlatformAdapter 接口 + POSIX/Windows 适配器)
168
+ 浏览器半 client.tsx(xterm 面板 + 多 tab + 快捷键 + 拖拽 + 剪贴板 + 自定义 Hooks)
169
+ 常量 constants.ts(协议/尺寸/快捷键/环境变量/文件名常量)
170
+ 工具函数 server-command.ts、shortcut.ts、logger.ts
171
+ ```
172
+
173
+ 依赖方向:`index.ts` 依赖 `constants.ts`、`platform.ts`、`persistence.ts`、`server-command.ts`、`logger.ts`;`persistence.ts` 依赖 `constants.ts` 与 `logger.ts`;`client.tsx` 依赖 `shortcut.ts` 与 `logger.ts`,独立运行在浏览器侧。两半经 WebSocket 通信,不直接 import。
174
+
175
+ ### 2.3 导出约定
176
+
177
+ - 只用**命名导出**,不用 `export default`
178
+ - `src/index.ts` 是宿主半的对外入口(Cordis 插件 `apply`/`inject`/`name`)
179
+ - 内部辅助函数不导出;仅为测试而导出是不可接受的理由——测试走公开 API
180
+
181
+ ```typescript
182
+ export { parseShortcut } from './shortcut.js';
183
+ export type { ParsedShortcut } from './shortcut.js';
184
+ ```
185
+
186
+ ### 2.4 导入约定
187
+
188
+ - `src/` 内的相对导入**一律带 `.js` 后缀**(ESM 要求),即使源文件是 `.ts`
189
+ - `tests/` 下的脚本用 `.ts` 后缀(靠 `allowImportingTsExtensions`)
190
+ - Node 内置模块带 `node:` 前缀:`node:fs`、`node:path`、`node:crypto`
191
+ - 只用于类型的导入写 `import type`
192
+ - 导入顺序:第三方 → Node 内置 → 本仓库模块
193
+ - 避免运行时动态 `import()`,除非确实要延迟加载重依赖并在注释里说明原因。本仓库唯一的例外是宿主半的 `@lydell/node-pty`:它带原生绑定,顶层静态导入一旦失败会让整个模块在求值期不可导入,懒加载把失败收敛到会话创建那一步
194
+
195
+ ```typescript
196
+ // @lydell/node-pty 是上面那条的例外:懒加载,故不出现在导入块里
197
+ import { WebSocketServer } from 'ws';
198
+ import { readFileSync } from 'node:fs';
199
+ import { parseShortcut } from './shortcut.js';
200
+ import type { ParsedShortcut } from './shortcut.js';
201
+ ```
202
+
203
+ ### 2.5 状态封装
204
+
205
+ 模块级可变状态只用于确实进程唯一的东西(配置缓存),且必须配套提供刷新/失效函数:
206
+
207
+ ```typescript
208
+ let cachedConfig: TerminalConfig | undefined;
209
+
210
+ /** 刷新缓存(配置变更后调用) */
211
+ export function refreshConfig(): void {
212
+ cachedConfig = undefined;
213
+ }
214
+ ```
215
+
216
+ 其余状态放进类的 `private` 字段。对外暴露只读视图,不要把可变引用递出去:
217
+
218
+ ```typescript
219
+ get sessions(): readonly TerminalSession[] { return this._sessions; }
220
+ get currentSession(): TerminalSession | undefined {
221
+ return this._currentIndex >= 0 ? this._sessions[this._currentIndex] : undefined;
222
+ }
223
+ ```
224
+
225
+ ---
226
+
227
+ ## 三、类型安全
228
+
229
+ `tsconfig.json` 已开 `strict` + `noImplicitAny`,不要放宽。
230
+
231
+ - **不用 `any` 逃避类型**。第三方库确实缺类型时,在使用点就近断言并注释说明,不要让 `any` 顺着调用链扩散。
232
+ - **不用 `@ts-ignore`**;万不得已用 `@ts-expect-error` 并写明原因。
233
+ - **不用非空断言 `!` 绕过检查**,改用显式判断并抛出带上下文的错误。
234
+ - **可选属性用条件展开**,避免把 `undefined` 显式写进对象(配合 `exactOptionalPropertyTypes` 风格):
235
+
236
+ ```typescript
237
+ ...(config.shellArgs ? { shellArgs: config.shellArgs } : {}),
238
+ ...(config.cwd ? { cwd: config.cwd } : {}),
239
+ ```
240
+
241
+ - **联合字面量优先于 enum**:`type PanelState = 'collapsed' | 'expanded'`
242
+ - **枚举值到配置的映射用 `Record` 收口**,新增成员时编译器会提醒补齐:
243
+
244
+ ```typescript
245
+ const SHELL_DEFAULTS: Record<NodeJS.Platform, string> = { /* ... */ };
246
+ ```
247
+
248
+ ### 3.1 外部数据必须校验
249
+
250
+ - **插件 Config 用 schemastery**(`@deepseek-ai/schemastery`,zod 风格 API;dsh loader 拒绝真 zod),全字段给 default(3.18 无 enum/optional API)
251
+ - **跨进程边界的其余数据**(WebSocket 消息帧、命令行输入)用校验函数与容错解析——解析失败要么明确报错要么明确降级,不静默吞掉
252
+ - **形状同步靠 `import type`**:宿主半与浏览器半共享的类型从公共模块类型导入,编译期强制同步,不手写第二份
253
+
254
+ ---
255
+
256
+ ## 四、错误处理
257
+
258
+ - **错误消息用中文**,且必须带定位信息:哪个会话、哪个路由、期望值与实际值。
259
+
260
+ ```typescript
261
+ throw new Error(`终端会话 ${sessionId} 启动失败:pty 进程立即退出,退出码 ${code}`);
262
+ throw new TerminalError('SPAWN_FAILED', `shell 命令 ${shell} 不存在`, { sessionId });
263
+ ```
264
+
265
+ - **需要携带错误码时定义专用错误类**,并设置 `name`:
266
+
267
+ ```typescript
268
+ /** 终端操作错误:保留错误码 */
269
+ export class TerminalError extends Error {
270
+ constructor(message: string, readonly code?: string) {
271
+ super(message);
272
+ this.name = 'TerminalError';
273
+ }
274
+ }
275
+ ```
276
+
277
+ - **`catch (e: unknown)` 后统一归一化**,不要假设捕获到的是 `Error`:
278
+
279
+ ```typescript
280
+ const msg = error instanceof Error ? error.message : String(error);
281
+ ```
282
+
283
+ - **资源释放用 `try/finally`**,失败路径也要清理 WebSocket 连接、pty 进程、定时器:
284
+
285
+ ```typescript
286
+ const pty = spawn(shell, args, opts);
287
+ try {
288
+ await setupWebSocket(pty, ws);
289
+ } finally {
290
+ pty.kill();
291
+ }
292
+ ```
293
+
294
+ - **后台定时器一律 `unref()`**,避免阻止进程退出:
295
+
296
+ ```typescript
297
+ this.heartbeat = setInterval(/* ... */, interval);
298
+ this.heartbeat.unref();
299
+ ```
300
+
301
+ - **有意忽略的 Promise 用 `void` 标注**,表明不是漏写 `await`:`void this.ready.catch(() => {});`
302
+
303
+ ---
304
+
305
+ ## 五、异步
306
+
307
+ - 一律 `async/await`,不写 `.then()` 链
308
+ - 包装回调式 API 时用 `new Promise`,并**同时处理 `error` 事件**,不要只接成功回调
309
+ - 事件监听器要成对清理,用 `once` 或在 `finally`/`cleanup` 里 `off`
310
+
311
+ ```typescript
312
+ await new Promise<void>((resolve, reject) => {
313
+ const cleanup = (): void => { pty.off('exit', onExit); ws.off('error', onError); };
314
+ const onExit = (): void => { cleanup(); resolve(); };
315
+ const onError = (err: Error): void => { cleanup(); reject(err); };
316
+ pty.once('exit', onExit);
317
+ ws.once('error', onError);
318
+ });
319
+ ```
320
+
321
+ - 超时用 `AbortSignal.timeout()` / `AbortSignal.any()`,不要自己搭 `setTimeout` 竞速
322
+ - 可取消的长操作接受可选 `signal?: AbortSignal` 参数,入口处 `signal?.throwIfAborted()`
323
+
324
+ ---
325
+
326
+ ## 六、命名与格式
327
+
328
+ | 对象 | 约定 | 示例 |
329
+ |---|---|---|
330
+ | 文件 | kebab-case | `server-command.ts` |
331
+ | 类 / 接口 / 类型 | PascalCase | `TerminalSession`、`ParsedShortcut` |
332
+ | 函数 / 变量 / 方法 | camelCase | `parseShortcut`、`createSession` |
333
+ | 模块级常量 | UPPER_SNAKE_CASE | `ROUTE_PREFIX`、`DEFAULT_COLS` |
334
+ | 私有字段 | camelCase;与 getter 同名时前缀 `_` | `private _state` + `get state()` |
335
+ | 布尔值 | `is`/`has`/`enabled` 前缀或后缀 | `isAlive`、`hasResize` |
336
+
337
+ - 缩进 2 空格,单引号,语句末加分号
338
+ - 数字字面量用下划线分隔:`30_000`、`64 * 1024`
339
+ - 用 `===`,不用 `==`
340
+ - 默认值用 `??`,不用 `||`(除了确实要把空串一起兜掉的场合)
341
+ - 显式写返回类型,包括 `: void`
342
+ - 提前 return / continue 收窄分支,避免深层嵌套
343
+ - **不要引入魔法数字**。超时、重试次数、终端尺寸都提成具名常量放文件顶部:
344
+
345
+ ```typescript
346
+ /** 默认终端列数 */
347
+ const DEFAULT_COLS = 80;
348
+ /** 默认终端行数 */
349
+ const DEFAULT_ROWS = 24;
350
+ ```
351
+
352
+ ---
353
+
354
+ ## 七、工程化约束
355
+
356
+ 1. **插件形态必须构建。** dsh loader 经纯 ESM import 加载插件、不走 tsx,所以必须用 `scripts/build.ts` 产出 `lib/`(含 `index.js` + `client.js` + `xterm.css`)。`lib/` 纳入版本控制——**禁止声明生命周期脚本**(`prepare`/`postinstall` 等一概不加),pnpm 11 对声明了安装类脚本的 git-hosted 包直接报 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,拦截只看 `package.json` 字段、不看脚本内容。选依赖同理——`@lydell/node-pty` 的 `package.json` 连 `scripts` 字段都不存在,pnpm 10/11/12 都不会对它做构建授权判断。构建由开发者手动 `pnpm run build`,产物随源码一起提交。
357
+ 2. **改代码后跑类型检查**:`pnpm run typecheck`。不要让类型错误总数变多。
358
+ 3. **新增运行时依赖必须写进 `package.json`**,版本锁定或用窄范围。`node_modules` 里有不等于已声明。
359
+ 4. **协议与命名常量不可单方面修改**:路由前缀(`/api/dsh-oh-my-terminal`)、slash 命令名、插件 id 改动必须同步 cordis.patch.yml 与构建脚本,并在注释里标明兼容性影响。
360
+ 5. **跨平台**:`@lydell/node-pty` 自动按平台选 ConPTY(Windows)或 openpty(POSIX),不需要手动处理平台差异。该包版本精确钉在 `1.1.0`(上游 `latest` 指向 beta),六个平台子包自带 N-API 二进制,安装期不编译。
361
+ 6. **不落明文凭据**:日志和错误消息不打印密钥、口令内容。
362
+ 7. **终端数据绝不进日志**:pty 输出与 WebSocket 数据帧是用户会话内容,不写日志。
363
+ 8. **日志统一使用 `src/logger.ts` 的 `createLogger`**。不直接调用 `console.*`。
364
+ 每个模块在文件顶部创建日志器:`const log = createLogger('模块名');`,模块名用
365
+ kebab-case 取自文件名(如 `'terminal-host'`、`'terminal-client'`)。级别语义:
366
+ debug(开发排查)、info(关键流程节点)、warn(可恢复异常/降级)、error(不可恢复失败)。
367
+ 输出格式固定为 `[时间戳] [级别] [模块] 内容`,便于跨模块日志检索与过滤。
368
+ Node 环境下 `console.info/warn/error` 默认输出到 stderr;浏览器端输出到 DevTools
369
+ Console——两端 API 一致,无需环境判断。附加数据可选第二参数,自动 JSON 序列化。
370
+
371
+ ```typescript
372
+ import { createLogger } from './logger.js';
373
+ const log = createLogger('terminal-host');
374
+ log.info('创建会话', { id, cols, rows });
375
+ log.error('持久化失败', err);
376
+ ```
377
+
378
+ **高频路径日志纪律**:WebSocket 消息转发、pty onData 等周期性代码路径中,
379
+ 只在状态变化时输出 info 日志;重复性诊断信息用 debug 级别。避免每条数据帧
380
+ 都输出日志。
381
+
382
+ ---
383
+
384
+ ## 八、提交信息
385
+
386
+ 中文,一句话说清做了什么。修 bug 用 `修复:` 前缀,重构用 `重构:` 前缀。
387
+
388
+ ```
389
+ feat: 添加多标签终端会话切换功能
390
+ 修复: WebSocket 关闭时未清理 pty 导致进程泄漏
391
+ 重构: 提取 shortcut.ts 独立快捷键解析模块
392
+ ```