better-dsh 0.0.0 → 0.2.2-b
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/LICENSE +24 -0
- package/README.md +294 -4
- package/control-prompt.md +37 -0
- package/cordis.patch.yml +53 -0
- package/docs/00_adr/0001-bridge-tool-layer-not-service-layer.md +14 -0
- package/docs/00_adr/0002-masking-is-presentation-only.md +15 -0
- package/docs/10_plans/A2A-messaging-channel-test-archive.md +256 -0
- package/docs/10_plans/code-mode-vs-rlm-ipython-comparison.md +137 -0
- package/docs/10_plans/dashr-blueprint-review.md +201 -0
- package/docs/10_plans/dashr-blueprint.md +561 -0
- package/docs/10_plans/dashr-compaction-window-and-archive.md +307 -0
- package/docs/10_plans/dashr-profile-layer-feasibility.md +367 -0
- package/docs/10_plans/dashr-sandbox-escalation-semantics-gap.md +171 -0
- package/docs/10_plans/dashr-security-sandbox-analysis.md +187 -0
- package/docs/10_plans/dashr-surface-invariant-and-omp-imports.md +97 -0
- package/docs/10_plans/ipython-kernel-interactive-interface-test-report.md +152 -0
- package/docs/10_plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft.md +146 -0
- package/docs/10_plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v3.md +50 -0
- package/docs/10_plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v4.md +79 -0
- package/docs/10_plans/kernel-refactoring/Dash-vs-PrimeAgent-systemprompt-toolcatalog-comparison.md +138 -0
- package/docs/10_plans/kernel-refactoring/RLM-system-prompt-injection-gap-report.md +161 -0
- package/docs/10_plans/kernel-refactoring/V0.1.5-development-plan.md +109 -0
- package/docs/10_plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_dsh.md +50 -0
- package/docs/10_plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_prime.md +113 -0
- package/docs/10_plans/recallable-compaction.md +147 -0
- package/docs/10_plans/spike-tag-repro.mjs +102 -0
- package/docs/10_plans/upstream-analysis.md +128 -0
- package/docs/50_test-reports/REPL-/345/267/245/345/205/267/350/260/203/347/224/250-/346/210/252/346/226/255/350/257/212/346/226/255.md +110 -0
- package/docs/50_test-reports/kernel-provisioning.md +44 -0
- package/docs/50_test-reports/repl-kernel-provisioning-test-report.md +87 -0
- package/docs/50_test-reports/upstream-dsh-0.1.2-alpha.5-local-test-report.md +81 -0
- package/docs/50_test-reports/upstream-dsh-0.1.2-alpha.5-report.md +93 -0
- package/docs/50_test-reports/v0.1.8-improved-/345/256/236/346/265/213/346/212/245/345/221/212.md +142 -0
- package/docs/50_test-reports/v0.1.8-/345/256/236/346/265/213/346/212/245/345/221/212.md +193 -0
- package/docs/50_test-reports/v0.1.8b-/345/256/236/346/265/213/346/212/245/345/221/212.md +96 -0
- package/docs/50_test-reports/v0.1.8c-/345/256/236/346/265/213/346/212/245/345/221/212.md +127 -0
- package/docs/50_test-reports/v0.1.8d-/345/256/236/346/265/213/346/212/245/345/221/212.md +150 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/README.md +138 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/code-mode-repl-only.observation.md +74 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.jsonl +3890 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.w-sample-0435.jsonl +544 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/functions.json +592 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/skills-catalog.snapshot.md +30 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/tools-sdk.output-schemas.json +1236 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/tools-sdk.python.txt +592 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/tools-sdk.typescript.txt +516 -0
- package/docs/50_test-reports/v0.1.8d_artifacts/wire-vs-transcription.diff.md +54 -0
- package/docs/50_test-reports/v0.1.8e-/345/256/236/346/265/213/346/212/245/345/221/212.md +224 -0
- package/docs/50_test-reports/v0.1.9a-/345/256/236/346/265/213/346/212/245/345/221/212.md +168 -0
- package/docs/50_test-reports/v0.2.0b-/345/256/236/346/265/213/346/212/245/345/221/212.md +123 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/Cargo.lock +7 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/Cargo.toml +6 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/src/bin/messy.rs +8 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/f2probe/src/main.rs +4 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/hashline-probe.md +5 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/Cargo.lock +7 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/Cargo.toml +7 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/build.rs +4 -0
- package/docs/50_test-reports/v0.2.0b_artifacts/slowprobe/src/main.rs +13 -0
- package/docs/50_test-reports/v0.2.1-/345/256/236/346/265/213/346/212/245/345/221/212.md +110 -0
- package/docs/50_test-reports/v0.2.1b-/345/256/236/346/265/213/346/212/245/345/221/212.md +86 -0
- package/docs/50_test-reports/v0.2.1c-/345/256/236/346/265/213/346/212/245/345/221/212.md +66 -0
- package/docs/50_test-reports/v0.2.1d-/345/256/236/346/265/213/346/212/245/345/221/212.md +67 -0
- package/docs/50_test-reports/v0.2.1e-P1-/345/256/236/346/265/213/346/212/245/345/221/212.md +136 -0
- package/docs/50_test-reports/v0.2.1ef-dev-audit-report.md +73 -0
- package/docs/50_test-reports/v0.2.1f-plugin-shipped-ui-patches/345/256/236/346/265/213/346/212/245/345/221/212.md +102 -0
- package/docs/50_test-reports/v0.2.4-ios-focus-zoom-suppression/345/256/236/346/265/213/346/212/245/345/221/212.md +158 -0
- package/docs/60_exploration-and-research/bun-compile-cordis-runtime-bootstrap-research.md +348 -0
- package/docs/60_exploration-and-research/cordis-research.md +350 -0
- package/docs/60_exploration-and-research/dsh-mobile-spa-ios-input-experience-research.md +160 -0
- package/docs/60_exploration-and-research/dsh-web-profile-package-map.md +186 -0
- package/docs/60_exploration-and-research/dsh-web-ui-slot-system-research.md +310 -0
- package/docs/60_exploration-and-research/dsh-webui-strip-boundary-research.md +300 -0
- package/docs/60_exploration-and-research/ios-chat-app-bridge-research.md +324 -0
- package/docs/60_exploration-and-research/web-frontend-composability-research.md +191 -0
- package/docs/distro-blueprint.md +81 -0
- package/docs/dsh-webUI-with-rlm-mode.png +0 -0
- package/docs/repositioning-and-rebranding.md +102 -0
- package/lib/client/index.js +473 -0
- package/lib/index.d.ts +744 -0
- package/lib/index.js +11768 -0
- package/lib/kernel-env-hxaihi9C.js +195 -0
- package/lib/kernel-env.d.ts +80 -0
- package/lib/kernel-env.js +3 -0
- package/lib/py-sdk-BCaOGYz7.d.ts +125 -0
- package/lib/py-sdk-CbgYiX8O.js +691 -0
- package/lib/py-sdk.d.ts +2 -0
- package/lib/py-sdk.js +3 -0
- package/package.json +325 -4
- package/scripts/kernel-provision.mjs +35 -0
- package/index.js +0 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# Cordis 框架研究
|
|
2
|
+
|
|
3
|
+
> 记录:2026-08-21 · 一手核验:vendored `@deepseek-ai/cordis` 4.x(本地 4.0.1)源码 +
|
|
4
|
+
> `github.com/cordiverse/cordis`(6770★)+ `github.com/cordiverse/paper`(2532★)+
|
|
5
|
+
> dsh 官方 docs(`master` @ 2026-08-19)+ dsh `vendor/README.md`。
|
|
6
|
+
> 本文是 Cordis 单侧研究(与 Dash 解耦后的文档;Dash 侧见 `dash-research.md`)。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. 总览与时空可组合性
|
|
11
|
+
|
|
12
|
+
### 一句话
|
|
13
|
+
|
|
14
|
+
**Cordis 是"时空可组合性的元框架"(A Meta-Framework of Spatiotemporal Composability)**:
|
|
15
|
+
一个 TypeScript 插件框架,把经典类型论的 *effect/coeffect* 对偶落成两套运行时机制——
|
|
16
|
+
**可逆效应(Revertible Effects,时间维)** 与 **反应式余效应(Reactive Coeffects,空间维)**,
|
|
17
|
+
使插件"插得上也拔得下、依赖变化自动激活/停用",全程免重启进程。
|
|
18
|
+
|
|
19
|
+
### 血统与时间线
|
|
20
|
+
|
|
21
|
+
- 作者 **Shigma(施一凡)**,Koishi 聊天机器人框架作者(2020-01 首版)。2023 年已写
|
|
22
|
+
《可逆的插件系统》设计文(koishi.chat cookbook)——论文雏形,非学术论文。
|
|
23
|
+
- **2026-08-13**:论文 + dsh 开源同日。论文 *A Programming Paradigm for Spatiotemporal
|
|
24
|
+
Composability*,DeepSeek × 北大,88 页,`github.com/cordiverse/paper`("Draft of August
|
|
25
|
+
13, 2026",active revision)。
|
|
26
|
+
- `Cordis` = 拉丁语"心";Koishi 的一切都从 Cordis 开始。全程先工程后理论,DeepSeek 参与
|
|
27
|
+
是 2026 年的事。
|
|
28
|
+
|
|
29
|
+
### 理论支柱:effect / coeffect 对偶 → 运行时机制
|
|
30
|
+
|
|
31
|
+
| 维度 | 论文概念 | 工程语义 | 解决的问题 |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| **时间维** | **Revertible Effects** | 每次上下文修改配显式逆函数,叠成撤销链;卸载时反向执行 | 插件"插得上拔不下"(状态残留、伤及运行中组件) |
|
|
34
|
+
| **空间维** | **Reactive Coeffects** | 组件声明依赖 → 自动拓扑编排;依赖齐→ACTIVE,缺→INACTIVE;提供者撤走→依赖者先停;回归→自动恢复 | 补丁式依赖维护 / 循环依赖 / 手写编排代码 |
|
|
35
|
+
|
|
36
|
+
并统一 effect context 与 coeffect context 为**单一 context 类型**(这即"编程范式"),再组合成
|
|
37
|
+
**component(组件)**,给出**动态组合的演算**,其元理论把时空可组合性从单组件推广到整个
|
|
38
|
+
交错组件系统。
|
|
39
|
+
|
|
40
|
+
### 作为代码的实现(源码级取证)
|
|
41
|
+
|
|
42
|
+
核心包 9 个源文件(`context/service/fiber/events/registry/reflect/logger/utils/index`,
|
|
43
|
+
**实测 2693 行**)。四个构件落地时空可组合性:
|
|
44
|
+
|
|
45
|
+
- **`Context`** = 运行时代理(proxy):属性读取走服务解析器(`ctx.tools`/`ctx.llm` 由服务名
|
|
46
|
+
解析,非 import 具体实现)。`extend()`/`isolate()`/`intercept()` 创建作用域子 context。
|
|
47
|
+
- **`Fiber`** = 一个插件应用的运行时。持有 `_disposables`(DisposableList)、`inject`(依赖
|
|
48
|
+
声明)、`store`(已解析实现)、`state`(FiberState)、`epoch`(依赖满足度签名)。
|
|
49
|
+
- **时间维** `Fiber.effect(execute, label)`:`execute` 立即执行,产出 disposer 被收集;
|
|
50
|
+
disposer 被调 **或** fiber 卸载时,disposers **逆注册序(LIFO)** 执行(
|
|
51
|
+
`disposables.splice(0).reverse()`)。
|
|
52
|
+
- **空间维** `inject` + `_checkImpl` + `_refresh` + `epoch`:遍历每个依赖,任一缺失 →
|
|
53
|
+
`epoch=INACTIVE` → `_unload()`(停用);全部存在 → `epoch=':uid:uid...'` → `_reload()`
|
|
54
|
+
(激活);**提供者 fiber uid 变化** → epoch 变化 → 依赖者先 unload 再 reload。
|
|
55
|
+
- **`Service`** = 服务基类:构造函数 `super(ctx, name)` 即 `ctx.reflect.provide(...)` 注册,
|
|
56
|
+
随所属 fiber 卸载自动注销。
|
|
57
|
+
- **`EventsService`** = 事件总线,五种派发模式 `emit/parallel/serial/bail/waterfall`
|
|
58
|
+
(waterfall 是 around-middleware,`next()` 委派、不调则短路)。
|
|
59
|
+
|
|
60
|
+
### 生产验证与自认局限
|
|
61
|
+
|
|
62
|
+
Koishi:4 年、4000+ 社区插件、作者互不相识,唯一协调机制 = 反应式余效应;切存储后端 /
|
|
63
|
+
重连 IM 适配器时仅依赖变化的插件重激活。论文自认局限:仅 Koishi 一生态验证、仅 TS 单语言
|
|
64
|
+
数据、无与其他替代架构的直接对比。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. 非特权核心(No Privileged Core)
|
|
69
|
+
|
|
70
|
+
### 你的理解的校验(逐点)
|
|
71
|
+
|
|
72
|
+
你的 mental model 基本正确,有两处需要精确化:
|
|
73
|
+
|
|
74
|
+
> "基底 binary 可以看作一个入口"
|
|
75
|
+
|
|
76
|
+
✅ 精确。`dsh` 这个 binary 是 `apps/cli` 产出的 `lib/bin.js`,官方自述为 **"a thin
|
|
77
|
+
self-executing composition"**(薄的自执行组合)——它只做一件事:调 `dsh-app-boot` 的
|
|
78
|
+
`boot()`。
|
|
79
|
+
|
|
80
|
+
> "核心自己自举(bootstrap)为一个 plugin"
|
|
81
|
+
|
|
82
|
+
⚠️ 半对。**框架基底(Context + reflect/registry/events/logger 四服务)不是 plugin,它就是
|
|
83
|
+
框架本身**——不是"自举成 plugin",而是"进程里那个被 `new Context()` 造出来的根容器"。
|
|
84
|
+
真正被"自举/挂载"的是 **App 的插件树**(dsh 的 ~195 个插件),它们经 Loader 从
|
|
85
|
+
`dsh-base` 的 `cordis.patch.yml` 逐行 insert 到根 context 上。
|
|
86
|
+
|
|
87
|
+
> "如果你基于 Cordis 写了另一个 plugin,让它自举为一个核心也是可以的"
|
|
88
|
+
|
|
89
|
+
✅ 对,且措辞可更准:你不是"写一个 plugin 当核心",而是**写一个 Cordis App(一个插件树 +
|
|
90
|
+
一个薄 bin)**,它与 Dash 地位完全平等。Dash 只是"基于 Cordis 的 App 特例"。
|
|
91
|
+
|
|
92
|
+
> "Dash 第一个自举的 plugin 就成了核心"
|
|
93
|
+
|
|
94
|
+
⚠️ 需要修正。**没有"第一个插件成为核心"这回事**。"no privileged core" 的准确含义是:
|
|
95
|
+
**框架之外没有任何硬编码的、不可替换的模块**。看起来像"核心"的东西(`session`/
|
|
96
|
+
`system-prompt`/`tools`/`agent`/`agent-loop` 这六件"脊柱")**全是 `dsh-base` 的
|
|
97
|
+
`cordis.patch.yml` 里的普通插件行**,可被上层 patch 按 id 整行替换。所谓"核心"只是
|
|
98
|
+
"恰好提供了别人 inject 的基础服务的那个插件",并非特权位。
|
|
99
|
+
|
|
100
|
+
### Bootstrap 链(源码取证)
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
dsh 二进制(lib/bin.js,薄壳)
|
|
104
|
+
└─ boot(binName, configPath, ...) # dsh-app-boot
|
|
105
|
+
├─ new Context() # 根容器(框架基底,非 plugin)
|
|
106
|
+
├─ 注册 cordis:include + cordis:group builtins
|
|
107
|
+
├─ 安装 Loader
|
|
108
|
+
├─ mountRootInclude(...) # 挂载 cordis.yml 配置树
|
|
109
|
+
│ └─ 逐层叠加:bundle patch 层 → profile patch → home patch → --patch
|
|
110
|
+
│ └─ 每行 = 一个 plugin(含 dsh-base 的 agent-loop 等"核心")
|
|
111
|
+
├─ assertEntriesLoaded / Activated # 全部解析/激活,否则 fail loud
|
|
112
|
+
└─ 返回根 Context
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**关键证据**:`dsh-app-boot` 的 `boot()` 注释明言 "Create the root context, install Loader,
|
|
116
|
+
... mount and await the include tree ... return the root context"。核心(框架)与 App
|
|
117
|
+
(插件树)的边界就在 `new Context()` 这一行:它之上全是可 patch 的插件,它本身才是唯一的
|
|
118
|
+
"内核"。
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 3. 架构结构测绘
|
|
123
|
+
|
|
124
|
+
### Monorepo 包结构(`cordiverse/cordis`,9 包)
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
cordis/
|
|
128
|
+
└── packages/
|
|
129
|
+
├── core/ → 发布为 `cordis`:Context/Fiber/Service/Events/Registry/Reflect/Logger
|
|
130
|
+
├── loader/ → `@cordisjs/plugin-loader`:声明式配置加载(YAML/JSON 行 → 插件树)
|
|
131
|
+
├── include/ → `@cordisjs/plugin-include`:配置子树挂载 + `!!js` 表达式
|
|
132
|
+
├── hmr/ → `@cordisjs/plugin-hmr`:热模块替换(保存 → 仅重应用该插件)
|
|
133
|
+
├── group/ → `@cordisjs/plugin-group`:插件分组(isolate realm)
|
|
134
|
+
├── timer/ → `@cordisjs/plugin-timer`:定时服务
|
|
135
|
+
├── create/ → 脚手架(Node 22+)
|
|
136
|
+
├── logger-console/ → 控制台日志后端
|
|
137
|
+
└── utils/ → 共享工具
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 核心组件与关系
|
|
141
|
+
|
|
142
|
+
```mermaid
|
|
143
|
+
flowchart TD
|
|
144
|
+
C["Context<br/>运行时 proxy"] --> RF["ReflectService<br/>provide/inject 服务解析器"]
|
|
145
|
+
C --> RG["RegistryService<br/>plugin 注册 + Inject"]
|
|
146
|
+
C --> EV["EventsService<br/>emit/parallel/serial/bail/waterfall"]
|
|
147
|
+
C --> LG["LoggerService"]
|
|
148
|
+
RG -->|"ctx.plugin() → Fiber"| F["Fiber<br/>一个插件实例的运行时"]
|
|
149
|
+
RF -->|"super(ctx,name) 注册"| S["Service<br/>服务基类"]
|
|
150
|
+
S -->|"随 fiber 卸载自动注销"| F
|
|
151
|
+
F -->|"effect() 收集 disposer → 逆序撤销(时间维)"| UT["utils / DisposableList"]
|
|
152
|
+
F -->|"inject + epoch → 激活/停用(空间维)"| RF
|
|
153
|
+
L["loader<br/>import() 模块 → ctx.registry.plugin"] --> RG
|
|
154
|
+
I["include<br/>配置子树挂载"] --> L
|
|
155
|
+
H["hmr<br/>热重载"] --> F
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 4. 开发语言与打包编译
|
|
161
|
+
|
|
162
|
+
### 4.1 进程内约束(TS-only,Node ESM)
|
|
163
|
+
|
|
164
|
+
- `cordis/package.json`:`"type": "module"`、`"main": "lib/index.js"`;依赖仅
|
|
165
|
+
`@standard-schema/spec` + `cosmokit`——**零 native addon、零 WASM、零 child_process**。
|
|
166
|
+
- 插件入口只有三种形状:`Function(ctx, config)` / `Constructor` / `Object{apply}`,必须求值
|
|
167
|
+
为 JS 可调用对象。
|
|
168
|
+
- 插件加载:`tree.import(specifier)` → `unwrapExports()` → `ctx.registry.plugin(...)`。
|
|
169
|
+
`ModuleFormat = 'builtin'|'commonjs'|'json'|'module'|'wasm'`(仅 Node loader 格式)。
|
|
170
|
+
|
|
171
|
+
### 4.2 打包编译引擎:能否换成 Bun
|
|
172
|
+
|
|
173
|
+
**结论:Cordis 核心(零 native 依赖)可 trivial 编译成 Bun 单文件可执行;dsh 全量表面
|
|
174
|
+
"可行但有工程成本",不是"被卡死"。Claude Code 就是现成先例。**
|
|
175
|
+
|
|
176
|
+
| 项 | Bun 支持 | 说明 |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `bun build --compile` 单文件可执行 | ✅ | 内嵌 Bun 运行时,客户端无需装 Node;`--target=bun-linux-x64` 交叉编译 |
|
|
179
|
+
| `node:child_process`(spawn/PTY) | ✅ | 全支持 |
|
|
180
|
+
| `node:worker_threads` | ✅(小缺口) | postMessage/SharedArrayBuffer 支持;resourceLimits/execArgv 部分 |
|
|
181
|
+
| `node-pty`(native addon) | ❌ | 历史性坏(oven-sh/bun#7362);Bun 官方替代 = **Bun.Terminal**(v1.3.5 起) |
|
|
182
|
+
| `node-addon-landlock-run`(N-API) | ⚠️ | N-API 可加载,但 `.node` 须**静态 require + 逐 target 预编译**才能嵌入 `--compile` |
|
|
183
|
+
| tsdown(Rolldown+Oxc) | ⚠️ | 是 Rolldown+Oxc(非 esbuild);Bun 下运行是 experimental,跑 bundler 需 Node 22+ |
|
|
184
|
+
|
|
185
|
+
**Claude Code 先例(前提确认为真)**:Anthropic 自 ~v2.1.113 起把 Claude Code 以
|
|
186
|
+
**Bun 编译的 standalone 原生二进制**发布(`curl`/brew/winget 装的就是它;npm 包只是下载并
|
|
187
|
+
链接同一二进制的 wrapper,运行时不碰 Node)。**Anthropic 2025-12 收购了 Bun**,部分原因就是
|
|
188
|
+
Claude Code 以这种方式 ship。所以"用 Bun 预编译 + 直接 ship binary"不是假设,是存在证明。
|
|
189
|
+
|
|
190
|
+
**推荐落地路径**(与 Claude Code 一致):**195 个 npm 包继续用 tsdown 在 Node CI 构建
|
|
191
|
+
(不动),只在最终 app 装配 + 编译阶段用 `bun build --compile`**。硬阻塞只有两个:
|
|
192
|
+
`node-pty` → 换 `Bun.Terminal`;`node-addon-landlock-run` → 逐 OS/arch 预编译并静态 require。
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 5. 异构代码衔接层(Hetero-Language Bridge)
|
|
197
|
+
|
|
198
|
+
### 结论:官方**已经实现**了"异构语言成为插件模块",形式 = 进程边界 + IPC 桥
|
|
199
|
+
|
|
200
|
+
你对"其实是不是 subprocess 倒无所谓,只要逻辑层自洽、实现无感"的判断是对的——Cordis 的
|
|
201
|
+
"everything is plugin" 抽象**确实自洽**:从 Cordis 视角看,`PythonCodeRuntime` 就是一个
|
|
202
|
+
普通 Service provider;它内部 spawn 一个 Python 子进程、用 fd3 桥接,是藏在 `ctx.codeRuntime`
|
|
203
|
+
缝隙背后的实现细节,消费者无感。
|
|
204
|
+
|
|
205
|
+
### 官方实现(`dsh-code-runtime-python` 的 fd3 帧协议)
|
|
206
|
+
|
|
207
|
+
仓库里**真实实现**(非 stub):`packages/code-runtime/code-runtime-python/`(`src/index.ts`
|
|
208
|
+
+ `py/protocol.py` + e2e tests)。机制:
|
|
209
|
+
|
|
210
|
+
- 每个 model program 跑在**全新 `python3 -I` 子进程**里;`stdio: [pipe, pipe, pipe, pipe]`
|
|
211
|
+
的第 4 项 = **fd 3**,作为 framed-JSON 通道;stdout/stderr 留给程序自己的输出。
|
|
212
|
+
- **帧 = fd 3 上的 JSON-lines**(每行一个 JSON 对象)。子→宿主:`boot-ack`/`call`/`log`/
|
|
213
|
+
`done`;宿主→子:`boot`(首帧)/`run`(`boot-ack` 后)/每 `call` 一个 `reply`。
|
|
214
|
+
- **宿主把每帧都当敌对输入**(`validateChildFrame` 逐字段校验 + 重建):model 代码对 fd 3
|
|
215
|
+
有完全访问权、可伪造任意帧,所以进站的 forged 字段被丢弃、非有限 call id 不会回显。
|
|
216
|
+
- **无损 JSON codec**(无 `JSON.stringify` 深度限制;迭代遍历;超安全整数走 `BigInt`;
|
|
217
|
+
字节计量),保证 `CodeJsonValue` 深度无界也能过线。
|
|
218
|
+
- `py/protocol.py` 是 TS `src/protocol.ts` 的镜像:`TypedDict` 形状 + `PROTOCOL_FD = 3` +
|
|
219
|
+
`log_truncation_marker`(**字节级一致**);`protocol-mirror.e2e.ts` 起真实 `python3` 断言
|
|
220
|
+
两侧字段名/必填性不漂移(曾因 round-12 三次字段漂移而加此守卫)。
|
|
221
|
+
|
|
222
|
+
TS 侧 `src/index.ts` 把 `PythonCodeRuntime` 注册为 `ctx.codeRuntime` 的 provider——**这就是
|
|
223
|
+
"Python 成为 Cordis 插件"的官方答案:TS 宿主侧插件 + 外语子进程 + fd3 JSON-lines IPC**。
|
|
224
|
+
|
|
225
|
+
### 全部异构缝隙(进程边界是唯一的跨语言方式)
|
|
226
|
+
|
|
227
|
+
| 缝隙 | 协议 | 边界 | 可达语言 |
|
|
228
|
+
|---|---|---|---|
|
|
229
|
+
| `ctx.subprocess`(subprocess-local) | stdio + 进程组 | `node:child_process`+node-pty | 任意可执行 |
|
|
230
|
+
| `ctx.shell`(bash/pwsh-local) | `bash -c`/pwsh | 出进程 | 任意命令 |
|
|
231
|
+
| `dsh-mcp-client` | MCP = JSON-RPC 2.0(stdio / Streamable HTTP) | 出进程/远程 | 语言无关(Python MCP SDK 成熟) |
|
|
232
|
+
| `ctx.codeRuntime`(code-runtime-python) | fd3 framed-JSON-lines | Python 子进程 | Python |
|
|
233
|
+
| `dsh-sandbox`/`fs-sandbox`/`bash-sandbox` | 策略包装 subprocess/fs | 出进程 | 底层缝隙可达的任意语言 |
|
|
234
|
+
| `dsh-typert-*` | Typert RPC 装饰器 + endpoint registry | 进程内 TS 反射 | 仅 TS |
|
|
235
|
+
|
|
236
|
+
> 注意:默认 `web` profile 只 bundle `dsh-code-runtime-worker-thread`(TS 后端);Python 后端
|
|
237
|
+
> 在仓库里但不随默认 profile 发布,需显式安装。
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 6. Rust 重写
|
|
242
|
+
|
|
243
|
+
### 结论:核心重写**已有人做完**;但"Rust Cordis 兼容纯 TS 插件"不是进程内可行的,"万能"的
|
|
244
|
+
真相是"TS 插件变成又一种异构语言桥"
|
|
245
|
+
|
|
246
|
+
- **Cordis 源码量**:核心实测 **2693 行** TS(9 文件);上游仓库总 2590 KB(含 docs/tests)。
|
|
247
|
+
- **`cordis-rs` 已经存在**(`docs.rs/cordis-rs`):`@deepseek-ai/cordis` 4.x 的 Rust 移植,
|
|
248
|
+
证明**原生核心(作用域 DI / 生命周期拥有的效应 / Fiber 状态机 / 事件总线 / registry /
|
|
249
|
+
reflect / logger)能干净地移植到 Rust**。Rust 插件走 `plugin_sync`/`plugin_async`,无 JS 引擎。
|
|
250
|
+
- **异构语言插件**:当前最佳实践 = **WASM Component Model(wasmtime + WIT/bindgen)**;
|
|
251
|
+
**Extism** 作为便捷多语言 PDK 层(Rust/Python/Go/JS/TS/C#/Zig/C/C++)。
|
|
252
|
+
- **"兼容纯 TS Cordis 插件"的真相**:**进程内不现实**。现有 Cordis/dsh 插件是 Node 程序
|
|
253
|
+
(`node:child_process`/`worker_threads`/`node-pty` native addon/ESM import `cordis`/ctx proxy/
|
|
254
|
+
inject epoch/热重载)。**没有任何嵌入式 JS 引擎**能提供这套 Node 面——`deno_core`(V8) 默认
|
|
255
|
+
无 Node built-ins;QuickJS/Boa 无 Node API;Extism JS PDK(QuickJS-ng in WASM)明确无
|
|
256
|
+
事件循环/`child_process`/Worker/fs/net、sync-per-export。因此纯 TS 插件必须**在真实
|
|
257
|
+
Node/Bun 运行时里出进程跑,走 RPC 桥**——即第 5 节的同一种异构桥,恰好是"又多一种插件类型"。
|
|
258
|
+
|
|
259
|
+
| 机制 | 跨语言 | 能跑现有 JS 插件? | 先例 |
|
|
260
|
+
|---|---|---|---|
|
|
261
|
+
| 原生 Rust 核心(cordis-rs) | 仅 Rust | ❌ | cordis-rs(已 ship) |
|
|
262
|
+
| WASM Component Model(wasmtime) | ✅(WIT 多语言) | ❌ | wasmtime 生态 |
|
|
263
|
+
| Extism PDK | ✅(Rust/Py/Go/JS/C#...) | ❌(JS PDK 无 Node 面) | Extism |
|
|
264
|
+
| 嵌入式 V8(deno_core)/QuickJS/Boa | 仅 JS 子集 | ⚠️ 须重实现 `cordis` + ctx proxy + inject epoch | 无先例 |
|
|
265
|
+
| **出进程 Node/Bun + RPC 桥** | ✅(任意语言) | ✅(真实 Node/Bun 跑 TS 插件) | 第 5 节 fd3 / MCP |
|
|
266
|
+
|
|
267
|
+
**诚实成本**:原生核心 8–15k LOC / 4–8 周达到对等;**JS 互操作是真正耗时数月的硬层**
|
|
268
|
+
(要么在嵌入式引擎里重实现 Node API 面——无先例,`napi-rs` 是反方向;要么让响应式
|
|
269
|
+
inject-epoch 语义跨进程边界保持一致)。所以正确的心智模型不是"Rust 版 Cordis 万能兼容 TS",
|
|
270
|
+
而是 **原生 Rust 核心 + TS 插件作为又一种异构语言类型(出进程 Node/Bun + 桥)**。
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 7. 进程内框架与 Recursive Cordis
|
|
275
|
+
|
|
276
|
+
### 7.1 进程内框架的含义(承接你未完成的 1.5)
|
|
277
|
+
|
|
278
|
+
Cordis 是**单进程内**的框架:`Context` 是一个进程内的 proxy,服务解析、事件派发、效应撤销
|
|
279
|
+
全部在内存中。这意味着**一切跨进程/跨机器的组合都必须显式引入 IPC**——Cordis 自己不提供
|
|
280
|
+
"远程 context 镜像"(把一个远程进程的服务透明地当作本地 `inject` 到的服务)。
|
|
281
|
+
|
|
282
|
+
### 7.2 Recursive Cordis:include 子树 vs alien-binary + IPC 融合
|
|
283
|
+
|
|
284
|
+
你说的两种方案,可行性截然不同:
|
|
285
|
+
|
|
286
|
+
**(a) include 子树(直接加载 Dash 插件)——原生、零 IPC、已实现。**
|
|
287
|
+
Cordis Multica 作为唯一基底启动 `new Context()`,用 `cordis:include` 把 Dash 的 agent 运行时
|
|
288
|
+
插件直接挂到同一根 context 下(`dsh-app-boot` 的 `mountRootInclude` + dsh 自己 per-session
|
|
289
|
+
preset 机制就是同一机制)。Dash 自己的 Cordis 基座**不启动**,Dash 插件直接挂在 Multica 下。
|
|
290
|
+
**这就是你说的"直接加载 Dash plugin 就行"——正确,这是默认该走的路。**
|
|
291
|
+
|
|
292
|
+
**(b) alien-binary-plugin(启动一个完整 Dash)+ IPC 融合——可行但"融合"是净新增工作。**
|
|
293
|
+
"写一个 plugin 去 spawn 一个完整 Dash binary"本身**trivial**:一个 inject `ctx.subprocess` 的
|
|
294
|
+
插件即可。但难点在**"IPC 间的 Cordis 融合"**——把两个 Cordis context 的服务/事件在进程边界
|
|
295
|
+
上桥接起来。**Cordis 没有现成的跨进程 context 桥**:dsh 的跨进程原语(`dsh-sdk-jsonrpc-server`
|
|
296
|
+
/sdk-client、`dsh-acp`、`dsh-api-remotes`/api-gateway)暴露的是**一个面**(如 agent 面),不是
|
|
297
|
+
**context 本身**(任意服务/事件)。所以:
|
|
298
|
+
|
|
299
|
+
- "大家都是 Cordis,IPC 更容易"在**语义层**成立(共享事件/服务词汇,设计桥时有共同语言);
|
|
300
|
+
- 在**机制层**不成立:桥本身是净新增,官方未 ship。
|
|
301
|
+
|
|
302
|
+
**判定**:两条路不矛盾。默认走 (a)(零 IPC、原生、已实现);(b) 只在"必须让一个完整 Dash
|
|
303
|
+
独立成进程/独立沙箱/独立升级"时才有意义,且要自建 context-bridge 协议(可复用 sdk/acp/
|
|
304
|
+
typert 原语)。当前阶段 (b) 是研究级工作,不是免费午餐。
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## 8. 同类竞品调研
|
|
309
|
+
|
|
310
|
+
真正**同时**达到时间维(运行时保证的逆函数撤销)+ 空间维(响应式依赖自动激活/停用)的,
|
|
311
|
+
只有 **Cordis、其直系祖先 Koishi、经典对手 OSGi**。
|
|
312
|
+
|
|
313
|
+
| 框架 | 语言 | 时间维 | 空间维 | 免重启卸载 |
|
|
314
|
+
|---|---|---|---|---|
|
|
315
|
+
| **Cordis** | TS | ✅ `Fiber.effect()` LIFO 撤销 | ✅ `inject`+epoch | ✅ |
|
|
316
|
+
| **Koishi** | TS | ✅ `fork.dispose()` 逆序撤销 | ✅ 服务 DI | ✅ 热重载 |
|
|
317
|
+
| **OSGi + DS** | Java | ✅* bundle stop + `@Deactivate` | ✅ DS `bind/unbind/reconfigure` | ✅ |
|
|
318
|
+
| Effect-TS | TS | ✅ Scope/LIFO finalizers | ❌ 无响应式服务激活 | 部分 |
|
|
319
|
+
| VS Code Extension Host | TS | ❌ 手动 dispose,无法原地重置 | ❌ activation 一次性 | ❌ 须宿主重载 |
|
|
320
|
+
| Inversify / NestJS / Angular | TS | ❌ OnDestroy 是 shutdown 语义 | ❌ 容器图静态 | ❌ |
|
|
321
|
+
| Spring(core) | Java | ❌ Spring-DM 已移除 | ❌ `@RefreshScope` 非响应式激活 | ❌ |
|
|
322
|
+
| .NET(MEF/Autofac) | C# | 部分(程序集无法卸载) | 部分 recomposition | ❌ 须 AppDomain |
|
|
323
|
+
| Umzug(迁移) | JS | ✅* 仅时间维(up/down) | ❌ | N/A |
|
|
324
|
+
| webpack/Tapable、Rollup、esbuild | JS | N/A 构建期 | ❌ | N/A |
|
|
325
|
+
|
|
326
|
+
**判定**:OSGi 是 Cordis 之前唯一同构的双支柱系统(且免重启卸载);Cordis 的差异化不在
|
|
327
|
+
"有没有"而在**严谨度 + 人体工学**——每个 ctx 变更都带运行时追踪的逆函数、逆序重放(路径无关/
|
|
328
|
+
合流性),OSGi 的 `deactivate()` 可靠但依赖作者纪律。Koishi 是直系祖先。Effect-TS 只到时间维;
|
|
329
|
+
VS Code 是论文要反对的反例;Nest/Angular/Inversify/Spring/MEF 是空间维或 teardown-only;
|
|
330
|
+
Umzug 是相邻的时间维-only;bundler 是构建期。**Cordis 的主张在"类"上不唯一,但在托管语言里
|
|
331
|
+
是形式化根基最干净、免重启实现最彻底的一个。**
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 来源
|
|
336
|
+
|
|
337
|
+
- `github.com/cordiverse/cordis`(TypeScript/MIT,`packages/{core,create,group,hmr,include,
|
|
338
|
+
loader,logger-console,timer,utils}`)
|
|
339
|
+
- `github.com/cordiverse/paper` —— *A Programming Paradigm for Spatiotemporal Composability*
|
|
340
|
+
(Draft 2026-08-13)
|
|
341
|
+
- dsh 仓库 `vendor/README.md`(vendored 清单 + 18 项本地修改 + sync 流程)、
|
|
342
|
+
`packages/boot/app-boot/README.md`(bootstrap)、
|
|
343
|
+
`.agents/notes/.../2026-07-31-code-runtime-python-fd3-protocol.md` + `py/protocol.py`(fd3)
|
|
344
|
+
- Vendored 源码 `~/.dsh/profiles/node_modules/@deepseek-ai/cordis/src/*.ts`(@4.0.1,实测 2693 行)
|
|
345
|
+
- Bun 官方 docs(`bun build --compile` / Node-API / Bun.Terminal)、`anthropic.com/news/
|
|
346
|
+
anthropic-acquires-bun`、Claude Code quickstart
|
|
347
|
+
- `docs.rs/cordis-rs`、`arroyo.dev/blog/rust-plugin-systems`、`extism.org`、`wasmtime`、
|
|
348
|
+
`deno_core` / `rquickjs` / `Boa`、`tartanllama.xyz/posts/wasm-plugins`
|
|
349
|
+
- 竞品:OSGi Core Spec 9.0、`github.com/koishijs/koishi`、Effect v3 Scope、VS Code extension
|
|
350
|
+
anatomy
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# DSH Web UI 移动端(iOS Safari)输入体验研究 — focus 放大 / 键盘遮蔽(v2)
|
|
2
|
+
|
|
3
|
+
- 日期:2026-09-03(v1 初稿;v2 同日晚修订,含 TypingMind 逆向实录与裁决更新)
|
|
4
|
+
- 范围:DSH Web UI(`upstream/deepseek-harness` checkout,tag `dsh-v0.1.2-alpha.5`)在 iOS Safari 上的输入体验。**遵 user 2026-09-03 裁决:只解决 ①focus 放大、②虚拟键盘遮挡两个问题;左栏挤压会话区(原 D4)no-go** —— 那是框架级问题,改它是无底洞;侧栏弹出时内容完整即可,隐藏侧栏后自然回到会话区,挤压是暂时性的,由它去。
|
|
5
|
+
- 参照物:typingmind.com(逆向实录见 §2);上游源码逐行取证;WebKit Bugzilla 现状核查(2026-09-03)。
|
|
6
|
+
- 关联:`docs/50_test-reports/v0.2.1f-plugin-shipped-ui-patches实测报告.md`(手势/mobile CSS 已发布态)、`ios-chat-app-bridge-research.md`(native 壳路线)。逆向工作产物:`work/typingmind-re/`(case: `work/typingmind-web-re`,reverse-skill offline-sample)。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 0. 结论(TL;DR)
|
|
11
|
+
|
|
12
|
+
两个目标问题全部可在 better-dsh 插件内闭环,**零上游改动**。v2 关键更新:
|
|
13
|
+
|
|
14
|
+
1. **16px 论据已从"推断"升级为"实测"**:TypingMind 聊天输入框 **手机 16px / 桌面 14px**(CDP 活体测量,§2.3)—— 它不是"字体小也没事",而是刻意在手机端维持 16px、桌面才降到 14px(Tailwind `text-base sm:text-sm`)。你看它"字也不大"是桌面印象。
|
|
15
|
+
2. **行业存在两条正路**(§3.2):A) 移动端字号地板 16px(TypingMind 现行);B) JS 在 iOS 窄屏把 `user-scalable` 翻成 `no`(**iOS 10+ 并不禁双指缩放,只杀 focus 自动放大**;Discourse 曾用 A 后整体迁移到 B,原因是 A 的视觉膨胀)。DSH 选 A/B/A+B 是待讨论的决策点。
|
|
16
|
+
3. **键盘问题存在两层事实**(§3.3):浏览器内 Safari = 键盘 overlay 无 opt-out(WebKit 259770 仍 NEW),必须 visualViewport shim;**PWA standalone 态 = 引擎原生 resize(innerHeight/dvh 随键盘收缩)**,无需 shim —— 这就是 TypingMind 零键盘代码的原因(它的推荐移动形态是 PWA)。DSH manifest 已是 fullscreen,"推荐 PWA + 保留浏览器内 shim"可作组合策略。
|
|
17
|
+
4. **动态岛假设有真实对应物但不是本症状的机制**(§4):WebKit 300523(iOS 26.0 仅动态岛机型,键盘关闭/滚动后 viewport 上移数像素侵入安全区,26.1 beta 已修,应用侧无法绕过)—— 证明"动态岛参与 viewport 计算出错"这类 bug 存在,但其症状是**几像素上移**,不是 120% 宽度放大;放大是 font-size 机制(16/14≈1.14 起步,与观察值吻合)。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. 症状与根因(v1 取证维持有效,摘要)
|
|
22
|
+
|
|
23
|
+
### D1 — focus/JS 定位输入框 → 页面放大到 115–120%
|
|
24
|
+
|
|
25
|
+
- viewport:`apps/web/index.html:5` = `width=device-width, initial-scale=1`,全仓无 `maximum-scale`/`user-scalable` 处理。
|
|
26
|
+
- 字号:composer `.card { font-size: var(--dsh-content-font-size, 14px) }`(`InputBar.module.css:55`,`.input` 继承;contenteditable 锚点 `[data-composer-input]`);`--dsh-content-font-size` 由 `ui-theme/src/boot-theme.ts:21` 写 body,**默认 14px**;permission/model 原生 `<select>` 13px。全仓可聚焦控件 13–14px,全部低于 iOS 16px 阈值 → focus 必放大。16/14 ≈ 1.14,与观察到的 115–120% 吻合。
|
|
27
|
+
|
|
28
|
+
### D2 — 键盘弹出时页面不上推,input 被 overlay 遮住
|
|
29
|
+
|
|
30
|
+
- WebKit 未实现 `interactive-widget`([bug 259770](https://bugs.webkit.org/show_bug.cgi?id=259770),2026-09-03 核查仍 NEW/P2/Nobody)→ iOS 浏览器内键盘 overlay layout viewport,无 opt-out。
|
|
31
|
+
- DSH 布局:`html/body/#root {height:100%}`(`client/web/src/base.css:6`)+ `.frame` grid overflow hidden,composer 在文档流底部 → Safari 只做不可控 page pan,经常 pan 不到位。
|
|
32
|
+
- 唯一引擎 API:`window.visualViewport`(`resize`/`scroll` + `height`/`offsetTop`/`scale`)。`100dvh` 无济于事(响应工具栏不响应键盘)。
|
|
33
|
+
|
|
34
|
+
### D3 — 切会话自动 focus(D1+D2 的连锁触发器,保留在方案内待裁决)
|
|
35
|
+
|
|
36
|
+
`InputBar.tsx` unlock effect:`useEffect(..., [locked, sessionId, editor])` → `editor.getRootElement()?.focus()` —— 注释原文 "Unlock (mount / session switch) returns focus to the box"。切会话/首载 hero 必触发程序化聚焦 = 无人请求的键盘 + 放大。桌面这是特性(键盘用户续打),移动端是 bug。**user 裁决聚焦两症状,D3 正是两症状在"切会话"场景的共同触发层**,修它属于两症状的修复范围,但是否要"移动端切会话后不聚焦"仍留作决策点(§6)。
|
|
37
|
+
|
|
38
|
+
### ~~D4 — 左栏挤压~~(no-go,user 2026-09-03 裁决)
|
|
39
|
+
|
|
40
|
+
不再处理。机制留档备查:`narrowExpanded` 仅跨 1024 断点清除;`computeColumns` sidebar 永不让步(`SIDEBAR_MIN=264`),center 吸收全部赤字。v1 里的 M4(点击 treeitem 自动收栏)随之撤销。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. TypingMind 逆向实录(2026-09-03,work/typingmind-re)
|
|
45
|
+
|
|
46
|
+
### 2.1 分发形态定性:无 DMG,现行 = PWA only
|
|
47
|
+
|
|
48
|
+
官方 install 文档(docs.typingmind.com/install-typingmind-app)明示安装方式 = **PWA**:桌面 Chrome/Edge 地址栏安装图标、iOS Safari Add to Home Screen,"No app store, no download required"。历史上的 macOS app(changelog "MacOS app v1.15.0",Setapp 渠道)已非现行分发。GitHub `typingmind/typingmind` 是 issue/docs 门面,应用本体闭源。**结论:web bundle(typingmind.com 的 Next.js chunks + PWA 全家桶)就是完整 app package** —— 逆向它 = 逆向完整应用。
|
|
49
|
+
|
|
50
|
+
### 2.2 静态扫描(149 个 JS chunk ≈11MB + 4 个 CSS,样本 tarball 已存 case)
|
|
51
|
+
|
|
52
|
+
| 检索 | 结果 |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `visualViewport` | 仅 1 处,Floating UI 定位库内部偏移计算 —— **无键盘 shim** |
|
|
55
|
+
| `maximum-scale` / `user-scalable` / viewport 改写 | **无**(JS 与 CSS 均零命中) |
|
|
56
|
+
| `fontSize:"16px"` JS | 2 处 = Prism 代码高亮主题(噪音) |
|
|
57
|
+
| `safe-area-inset` | **真实使用**:CSS `env(safe-area-inset-bottom/left/right)`;JS 侧 workspace bar 高度 `calc(58px + env(safe-area-inset-bottom))`(chunk 3a4r…)—— 标准全面屏适配通道 |
|
|
58
|
+
| "dynamic island"/"notch" | 零真实命中(唯一 "notch" 是用户评价文案) |
|
|
59
|
+
| 表单基线 | Tailwind Forms 全局:`[type=text],…,textarea,select { font-size:1rem }` = 16px(无 html 根字号覆写) |
|
|
60
|
+
| PWA | manifest `display:standalone`;全套 iPhone/iPad splash;`apple-mobile-web-app-capable` |
|
|
61
|
+
|
|
62
|
+
### 2.3 活体测量(CDP 双宽度,google-chrome --remote-debugging-port + Node 22 原生 WebSocket,`Emulation.setDeviceMetricsOverride`)
|
|
63
|
+
|
|
64
|
+
主输入框 `<textarea id="chat-input-textbox">`,类名含 `text-base ... sm:text-sm`:
|
|
65
|
+
|
|
66
|
+
| viewport | computed font-size(#chat-input-textbox) | 机制 |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| 390×844(手机) | **16px** | `text-base`(1rem)生效 |
|
|
69
|
+
| 1280×900(桌面) | **14px** | `sm:text-sm`(≥640px 才降档) |
|
|
70
|
+
|
|
71
|
+
同页实测:viewport meta 活体值 `initial-scale=1, viewport-fit=cover`;搜索框 16px;根字号 16px(未覆写)。
|
|
72
|
+
|
|
73
|
+
**结论:TypingMind 对 focus 放大的对策 = 手机端输入面 16px(Tailwind 响应式降档手法)+ 不动 viewport meta + 无任何 JS 键盘/缩放处理。** 它的移动端键盘体验依赖 PWA standalone 的引擎原生行为(§3.3)。
|
|
74
|
+
|
|
75
|
+
### 2.4 行业演化旁证:Discourse PR #30877
|
|
76
|
+
|
|
77
|
+
Discourse 曾实现方案 A:`--font-size-ios-input: max(1em, 16px)`(其注释原话 "inputs/textareas in iOS need to be at least 16px to avoid triggering zoom on focus"),后整体替换为方案 B:iOS 上 JS 把 `user-scalable=yes` 翻成 `no`,注释原话:"**In iOS Safari, setting user-scalable=no doesn't actually prevent the user from zooming in. But, it does prevent the annoying 'auto zoom' when focussing input fields with small font-sizes.**" —— 迁移动机是 A 造成输入框视觉膨胀。两条路都被大型产品实证有效。
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. 机制结论与充要性(v2 修正)
|
|
82
|
+
|
|
83
|
+
### 3.1 放大机制的准确表述
|
|
84
|
+
|
|
85
|
+
iOS Safari(默认配置)在 `initial-scale=1` 且未禁缩放时,对 computed font-size **< 16px** 的可聚焦控件(input/select/textarea/contenteditable)在 focus 时自动放大 visual viewport 至文本 ≥16px 可读级。这是充分条件级的行业共识(TypingMind 刻意工程 + Discourse 注释 + 大量社区文献),且数值与 DSH 症状吻合(14px→×1.14)。**充要性的诚实边界**:
|
|
86
|
+
- 16px 在**默认 Safari 配置**下充分;非绝对必要(maximum-scale/user-scalable=no 亦阻断)。
|
|
87
|
+
- 例外残存:用户系统级辅助功能(更大文本、Safari 每站 Page Zoom 设置)可抬高实际阈值或残留缩放;`<select>` 聚焦在个别 iOS 版本有独立报告(如 SO #64076385 "not prevented with 16px",403 未能取全文,标题即反例存在性证明)。**这恰是 A+B 双保险的理由**(§6 决策点 1)。
|
|
88
|
+
|
|
89
|
+
### 3.2 方案空间(放大问题)
|
|
90
|
+
|
|
91
|
+
| 方案 | 手段 | 代价 | 先例 |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| **A 字号地板** | 窄屏 CSS:`[data-composer-input],[data-composer-placeholder],input,select,textarea { font-size: max(16px, var(--dsh-content-font-size,14px)) }` | 输入面视觉变大(13/14→16px);对字号偏好用户保序 | TypingMind 现行;Discourse v1 |
|
|
94
|
+
| **B 禁缩放标记** | 窄屏 JS 改写 viewport meta 追加 `maximum-scale=1, user-scalable=no` | iOS 10+ **不禁双指缩放**(Discourse 注释实证),只杀 focus 自动放大;桌面/Android 不动 | Discourse v2(现行) |
|
|
95
|
+
| A+B | 地板兜字义,标记兜例外 | 叠加 | —— |
|
|
96
|
+
|
|
97
|
+
B 的实现要点:只在 narrow + touch 检测下改写(避免桌面与 Android 误伤),boot script 早期执行(先于任何 focus)。
|
|
98
|
+
|
|
99
|
+
### 3.3 键盘机制的两层事实(v2 关键更新)
|
|
100
|
+
|
|
101
|
+
- **浏览器内 Safari**:键盘 overlay,无 opt-out(259770),必须 `visualViewport` shim(v1 M2 方案维持):`intrusion = innerHeight − visualViewport.height`,>阈值且 `scale≈1` 时以 `--dashr-vvh` 收缩 `#root` + `scrollTo(0,0)` 抗 pan。
|
|
102
|
+
- **PWA standalone(Add to Home Screen)**:dev.to 2026-07 实测文(iOS 17/18):键盘弹出时 **`window.innerHeight`、`visualViewport.height`、`100dvh` 全部收缩**(引擎原生 resize,等价 `resizes-content`),`interactive-widget` 在 standalone 被忽略。已知 bug:**键盘关闭后 viewport 卡在小尺寸不恢复**(直到杀进程);社区解法 = blur 后 140ms 对全高元素做 `display:none→reflow→restore` 翻转强制重测(配 backdrop-filter 遮罩隐藏闪跳)。**TypingMind 零键盘代码成立的原因 = 其推荐移动形态是 PWA standalone**。DSH 的 manifest 已是 `display:fullscreen`,具备同路线条件。
|
|
103
|
+
- 策略组合(待讨论):浏览器内 Safari 用户 → M2 shim;PWA 用户 → 引擎原生 + 可选 viewport-stuck 自愈;是否把"装成 PWA"作为官方推荐移动用法(对齐 typingmind)是产品决策点(§6 决策点 3)。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 4. 动态岛假设验证(user 2026-09-03 提出方向)
|
|
108
|
+
|
|
109
|
+
**方向部分成立 —— 动态岛确实参与了一类真实 viewport bug,但不是本症状的机制:**
|
|
110
|
+
|
|
111
|
+
- [WebKit bug 300523](https://bugs.webkit.org/show_bug.cgi?id=300523)(REGRESSION, iOS 26.0,iPhone 15 Pro 实测 100% 复现,非动态岛机型 iPhone 13 Pro Max iOS 18 不复现):键盘关闭或滚动/重渲染后,Safari **错误计算 visual viewport 高度,内容上移数像素侵入动态岛安全区**(fixed/sticky 头部漂移)。`viewport-fit=cover/contain`、`env(safe-area-inset-top)` padding、visualViewport JS 重算**均无法绕过**;Simon Fraser 确认 iOS 26.1 beta 已修。
|
|
112
|
+
- 该 bug 的症状是**纵向几像素漂移**,不是横向 115–120% 放大,且只在 iOS 26.0 存在(26.1 已修)。DSH 若在 iOS 26.0 真机观察到顶栏上漂数像素,即此 bug,升级即愈,应用侧无动作空间。
|
|
113
|
+
- **机制结论**:布局视口宽度由 viewport meta 决定(390pt 机型 = 390 CSS px),动态岛裁剪通过 `env(safe-area-inset-*)` 暴露、不改变布局宽度与缩放比;"focus 后重新拿 2000px 物理高度再按旧比例放大"无证据支持(按此假设放大应与焦点控件字号无关,而 TypingMind 16px 输入框在同一机型上不放大 —— 反证)。**120% 放大维持 font-size 机制定性**;动态岛类 bug 作为独立 bug class 记录在案。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 5. 实现方案(v2,全部 better-dsh 插件增量,零上游改动;待批准后动工)
|
|
118
|
+
|
|
119
|
+
配置通道沿用 `__DASHR_MOBILE__`(`web-trust.ts` boot script)扩键:`zoomGuard: 'font' | 'meta' | 'both' | 'off'`(默认待裁决)、`keyboardShim: true`、`focusGate: true`(若裁决保留 D3 修复)。
|
|
120
|
+
|
|
121
|
+
- **M1 放大防护**(对应 §3.2 A/B,二选一或叠加,boot script 装 B、claimStyles 装 A)。
|
|
122
|
+
- **M2 浏览器内键盘 shim**(§3.3;visualViewport → `--dashr-vvh` 收缩 `#root`;阈值 + scale guard + rAF 合帧)。
|
|
123
|
+
- **M2b standalone viewport 自愈**(可选):blur 后 display-flip 重测,防 PWA 态卡小 viewport。
|
|
124
|
+
- **M3 focus gate**(D3 触发层,裁决点 2):boot script 包 `HTMLElement.prototype.focus`,只拦窄屏 `[data-composer-input]` 的非用户发起聚焦(pointerdown 在 `[data-composer-card]`/弹层内放行)。若裁决"移动端切会话保留自动聚焦",则 M3 撤销,症状由 M1+M2 兜底(键盘弹出但可见、不放大)。
|
|
125
|
+
- ~~M4 自动收左栏~~ —— **撤销**(D4 no-go)。
|
|
126
|
+
|
|
127
|
+
桌面零影响(全部 narrow-gated);验证计划维持 v1 §5(4999 预演 + client spec + 真机 iOS 清单),真机清单新增:iOS 26.0 顶栏上漂观察项(对照 300523)、PWA standalone 态键盘 + 卡死自愈验证。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 6. 待讨论决策点(user 明确先讨论后开发)
|
|
132
|
+
|
|
133
|
+
1. **放大防护选型**:A(字号地板,视觉变大)/ B(user-scalable=no 标记,iOS 10+ 不禁双指)/ A+B 双保险。倾向建议:**B 为主 + A 只保 composer**(B 零视觉扰动且 Discourse 实证;composer 16px 同时改善手机可读性)——待你裁决。
|
|
134
|
+
2. **切会话自动聚焦(D3)**:移动端是否取消?(取消 = 切会话后纯净阅读态;保留 = 现状行为,靠 M1+M2 兜底症状。)
|
|
135
|
+
3. **移动端官方形态**:是否把"Add to Home Screen(PWA standalone)"作为推荐用法(键盘问题在引擎层消失,对齐 typingmind 路线)?浏览器内 Safari 用户仍由 M2 覆盖。
|
|
136
|
+
4. M2b(standalone 卡死自愈)是否纳入首版。
|
|
137
|
+
|
|
138
|
+
## 7. 证据索引(v2 增补)
|
|
139
|
+
|
|
140
|
+
| 事实 | 位置/来源 |
|
|
141
|
+
|---|---|
|
|
142
|
+
| TypingMind 分发 = PWA only(无 DMG) | docs.typingmind.com/install-typingmind-app(2026-09-03) |
|
|
143
|
+
| 输入框 16px@390 / 14px@1280(实测) | CDP 活体测量,脚本 `work/typingmind-re/measure.mjs`,样本 `work/typingmind-re/typingmind-web-bundle.tar.gz` |
|
|
144
|
+
| Tailwind Forms 基线 1rem=16px | 其 CSS chunk(case 存档) |
|
|
145
|
+
| 无键盘 shim / 无 viewport 改写 | bundle 全量 grep(case 存档) |
|
|
146
|
+
| safe-area = env() 标准通道 | 其 CSS + chunk 3a4r…(case 存档) |
|
|
147
|
+
| Discourse A→B 迁移及 B 不禁双指缩放 | Discourse PR #30877 diff(注释原话) |
|
|
148
|
+
| standalone PWA 键盘 resize + 卡死 bug + display-flip 自愈 | dev.to/cederhook 2026-07(iOS 17/18 实测) |
|
|
149
|
+
| 动态岛 viewport bug(上移数像素,26.1 修) | WebKit bug 300523 |
|
|
150
|
+
| 浏览器内无 interactive-widget | WebKit bug 259770(仍 NEW/Nobody) |
|
|
151
|
+
| DSH 侧全部源码锚点 | 见 v1 §6(`apps/web/index.html:5`、`InputBar.module.css:55`、`boot-theme.ts:21`、`InputBar.tsx` unlock effect、`base.css:6`、`columns.ts`、`stores.ts`) |
|
|
152
|
+
|
|
153
|
+
## 8. 参考文献
|
|
154
|
+
|
|
155
|
+
- [WebKit Bug 259770 – interactive-widget](https://bugs.webkit.org/show_bug.cgi?id=259770) · [WebKit Bug 300523 – Dynamic Island viewport shift](https://bugs.webkit.org/show_bug.cgi?id=300523)
|
|
156
|
+
- [Discourse PR #30877 – Replace font-size-ios-input workaround](https://github.com/discourse/discourse/pull/30877)
|
|
157
|
+
- [Fixing the iOS standalone-PWA keyboard bug (dev.to, 2026-07)](https://dev.to/cederhook/fixing-the-ios-standalone-pwa-keyboard-bug-that-shrinks-your-viewport-for-good-63d)
|
|
158
|
+
- [Chromium: viewport resize behavior](https://developer.chrome.com/blog/viewport-resize-behavior/) · [CSS Viewport §interactive-widget](https://drafts.csswg.org/css-viewport-1/#interactive-widget-section)
|
|
159
|
+
- [TIL: Avoid text-sm on inputs (guidefari)](https://guidefari.com/safari-ios-input-zoom/) · [SO #64076385 – 16px 反例存在性](https://stackoverflow.com/questions/64076385/input-zoom-on-iphone-safari-not-prevented-with-16px)
|
|
160
|
+
- typingmind.com(bundle/manifest/viewport 实测);docs.typingmind.com(install 文档)
|