better-dsh 0.2.2-a → 0.2.2-c
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/docs/50_test-reports/2026-09-06-write/345/267/245/345/205/267sandbox/345/215/207/347/272/247/351/200/217/344/274/240bug/345/244/215/345/217/221/345/217/212/346/214/202/350/265/267-/344/272/213/344/273/266/346/212/245/345/221/212.md +43 -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/01-cordis-runtime/bun-compile-cordis-runtime-bootstrap-research.md +536 -0
- package/docs/60_exploration-and-research/01-cordis-runtime/cordis-customization-and-override-mechanics.md +418 -0
- package/docs/60_exploration-and-research/01-cordis-runtime/cordis-research.md +350 -0
- package/docs/60_exploration-and-research/01-cordis-runtime/dsh-cordis-hotplug-mcp-patch-research.md +265 -0
- package/docs/60_exploration-and-research/02-dsh-webui/dsh-web-profile-package-map.md +186 -0
- package/docs/60_exploration-and-research/02-dsh-webui/dsh-web-ui-slot-system-research.md +310 -0
- package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-backend-data-inventory.md +633 -0
- package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-strip-boundary-research.md +300 -0
- package/docs/60_exploration-and-research/02-dsh-webui/dsh-webui-wire-appendix.md +3729 -0
- package/docs/60_exploration-and-research/02-dsh-webui/web-frontend-composability-research.md +191 -0
- package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/capture-live-turn.json +1 -0
- package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/observed-endpoints.json +224 -0
- package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/remote-inventory.json +110954 -0
- package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/served-index-sample.html +47 -0
- package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/session-events.json +9411 -0
- package/docs/60_exploration-and-research/02-dsh-webui/webui-wire-data/ws-frame-examples.json +20 -0
- package/docs/60_exploration-and-research/03-mobile-ios/dsh-mobile-spa-ios-input-experience-research.md +160 -0
- package/docs/60_exploration-and-research/03-mobile-ios/ios-chat-app-bridge-research.md +324 -0
- package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-jsonl-mapping.md +1532 -0
- package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode-failed.json +46 -0
- package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode1.json +1124 -0
- package/docs/60_exploration-and-research/04-session-storage/alpha5-compaction-sample/episode2.json +1240 -0
- package/docs/60_exploration-and-research/05-dashr-dev/plugin-development.md +148 -0
- package/docs/60_exploration-and-research/05-dashr-dev/upstream-alignment.md +102 -0
- package/docs/60_exploration-and-research/README.md +87 -0
- package/docs/60_exploration-and-research/bun-compile-cordis-runtime-bootstrap-research.md +348 -0
- package/docs/60_exploration-and-research/dsh-mobile-spa-ios-input-experience-research.md +160 -0
- package/lib/index.d.ts +9 -1
- package/lib/index.js +258 -6
- package/package.json +1 -1
- package/docs/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 +0 -110
- package/docs/adr/0001-bridge-tool-layer-not-service-layer.md +0 -14
- package/docs/adr/0002-masking-is-presentation-only.md +0 -15
- package/docs/plans/A2A-messaging-channel-test-archive.md +0 -256
- package/docs/plans/code-mode-vs-rlm-ipython-comparison.md +0 -137
- package/docs/plans/dashr-blueprint-review.md +0 -201
- package/docs/plans/dashr-blueprint.md +0 -561
- package/docs/plans/dashr-compaction-window-and-archive.md +0 -307
- package/docs/plans/dashr-profile-layer-feasibility.md +0 -367
- package/docs/plans/dashr-sandbox-escalation-semantics-gap.md +0 -171
- package/docs/plans/dashr-security-sandbox-analysis.md +0 -187
- package/docs/plans/dashr-surface-invariant-and-omp-imports.md +0 -97
- package/docs/plans/ipython-kernel-interactive-interface-test-report.md +0 -152
- package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft.md +0 -146
- package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v3.md +0 -50
- package/docs/plans/kernel-refactoring/Dash-IPython-Control-Prompt-draft_v4.md +0 -79
- package/docs/plans/kernel-refactoring/Dash-vs-PrimeAgent-systemprompt-toolcatalog-comparison.md +0 -138
- package/docs/plans/kernel-refactoring/RLM-system-prompt-injection-gap-report.md +0 -161
- package/docs/plans/kernel-refactoring/V0.1.5-development-plan.md +0 -109
- package/docs/plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_dsh.md +0 -50
- package/docs/plans/kernel-refactoring/actinoable-surface-to-llm-in-agent-runtime_prime.md +0 -113
- package/docs/plans/recallable-compaction.md +0 -147
- package/docs/plans/spike-tag-repro.mjs +0 -102
- package/docs/plans/upstream-analysis.md +0 -128
- package/docs/v0.1.8-improved-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -142
- package/docs/v0.1.8-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -193
- package/docs/v0.1.8b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -96
- package/docs/v0.1.8c-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -127
- package/docs/v0.1.8d-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -150
- package/docs/v0.1.8d_artifacts/README.md +0 -138
- package/docs/v0.1.8d_artifacts/code-mode-repl-only.observation.md +0 -74
- package/docs/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.jsonl +0 -3890
- package/docs/v0.1.8d_artifacts/dsh-session-session-4a293388-9ae1-474b-87a0-9e17bb556d94.w-sample-0435.jsonl +0 -544
- package/docs/v0.1.8d_artifacts/functions.json +0 -592
- package/docs/v0.1.8d_artifacts/skills-catalog.snapshot.md +0 -30
- package/docs/v0.1.8d_artifacts/tools-sdk.output-schemas.json +0 -1236
- package/docs/v0.1.8d_artifacts/tools-sdk.python.txt +0 -592
- package/docs/v0.1.8d_artifacts/tools-sdk.typescript.txt +0 -516
- package/docs/v0.1.8d_artifacts/wire-vs-transcription.diff.md +0 -54
- package/docs/v0.1.8e-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -224
- package/docs/v0.1.9a-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -168
- package/docs/v0.2.0b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -123
- package/docs/v0.2.0b_artifacts/f2probe/Cargo.lock +0 -7
- package/docs/v0.2.0b_artifacts/f2probe/Cargo.toml +0 -6
- package/docs/v0.2.0b_artifacts/f2probe/src/bin/messy.rs +0 -8
- package/docs/v0.2.0b_artifacts/f2probe/src/main.rs +0 -4
- package/docs/v0.2.0b_artifacts/hashline-probe.md +0 -5
- package/docs/v0.2.0b_artifacts/slowprobe/Cargo.lock +0 -7
- package/docs/v0.2.0b_artifacts/slowprobe/Cargo.toml +0 -7
- package/docs/v0.2.0b_artifacts/slowprobe/build.rs +0 -4
- package/docs/v0.2.0b_artifacts/slowprobe/src/main.rs +0 -13
- package/docs/v0.2.1-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -110
- package/docs/v0.2.1b-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -86
- package/docs/v0.2.1c-/345/256/236/346/265/213/346/212/245/345/221/212.md +0 -66
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# Bun 编译自包含二进制 × Cordis 运行时自举 — Feasibility 研究
|
|
2
|
+
|
|
3
|
+
> 记录:2026-09-03 · 一手核验:upstream checkout `dsh-v0.1.2-alpha.5` vendored loader/include/hmr 源码 +
|
|
4
|
+
> `packages/boot/app-boot` 源码 + Bun 官方 executables 文档(2026-09 抓取)+ oven-sh/bun#11732(API 实查
|
|
5
|
+
> state=open)+ vercel/turborepo#11900 patch(2026-02 合并的生产级修复)+ azu/bun-build-dynamic-import repro。
|
|
6
|
+
> 本文是 [cordis-research.md](./cordis-research.md) §4.2 的深化:那一节给了"可行但有工程成本"的一行结论,
|
|
7
|
+
> 本文把"成本"拆到确切的机制、确切的失败面、和确切的缓解配方。
|
|
8
|
+
|
|
9
|
+
**范围声明(owner 已裁决,不再争论)**:node-pty 等 native addon 对 Bun 预编译的不兼容是已知项,开发具体
|
|
10
|
+
工具时 exclude 或换 `Bun.Terminal`,本文不展开(见 cordis-research.md §4.2 的政策)。本文只回答一个问题:
|
|
11
|
+
**Cordis 的运行时自举——扫配置、解析包名、动态 import 插件——在 `bun build --compile` 的自包含二进制里
|
|
12
|
+
会发生什么,能不能救,怎么救。**
|
|
13
|
+
**会发生什么,能不能救,怎么救。**
|
|
14
|
+
>
|
|
15
|
+
> **修订 v2(2026-09-03 同日,owner 边界重述)**:划分线不是"第一方/第三方插件",而是 **Bun 预编译界**——
|
|
16
|
+
> 编译期可枚举集(含 vendored 第三方,不问出身)vs 编译后运行时动态集。据此新增方案 E(Bun 静态核 +
|
|
17
|
+
> Node sidecar 双运行时)、§2 B10–B12(OpenClaw / better-sqlite3 / execa 行为级 divergence 实证)。
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 0. 结论速览
|
|
21
|
+
|
|
22
|
+
**Feasible,但"纯自包含单文件"和"保留全部运行时模块动态性"二者不可兼得——这不是 Bun 的 bug,是 bundler
|
|
23
|
+
与 plugin-host 的本质张力(Deno/esbuild/webpack 同款)。正确的目标形态是"编译核心 + 分层插件解析策略":**
|
|
24
|
+
|
|
25
|
+
- **编译期已知集 → 构建期冻结**(codegen 静态注册表,全部 bundle 进二进制,字面量动态 import 保懒加载)。
|
|
26
|
+
"已知集"不问作者出身:第一方插件、工具自有组件、vendored 依赖、依赖树里的第三方包——凡 build 时已在
|
|
27
|
+
模块图内者皆是。Cordis 的动态性分两层:**配置树动态性**(yml 行的 insert/patch/config/inject/disabled,
|
|
28
|
+
运行时解释,完全保留)与**模块集动态性**(import 哪个包,冻结侧冻结)。这条边界同时是 **shim 可行性
|
|
29
|
+
的边界**:已知集内任何 Bun 不兼容都是 build-time 工程项(alias/shim/exclude,图在我们手里);无界集内
|
|
30
|
+
没有收敛解。对工具发行,冻结核恰是 reproducibility 收益。
|
|
31
|
+
- **运行时动态集(编译后才到达的插件,天然 TS/Node 形态)→ 承接路线三条**:B(Bun 运行时内磁盘解析 +
|
|
32
|
+
walker patch,Turborepo 生产配方)、E(Node sidecar 真实运行时承接,§4-E)、C(runtime bundling
|
|
33
|
+
桥接)。用户侧只解包不做版本解析的原则不变。
|
|
34
|
+
"closed packaged runtimes" 概念、"Built bins need the Loader's native helper for bare plugin
|
|
35
|
+
specifiers" 的自述——upstream 作者已经在想打包形态,且所有动态 import 收敛在**我们自己 vendor 的两个
|
|
36
|
+
choke point** 上,不是黑盒。
|
|
37
|
+
- 真正阴险的不是"找不到包"(fail-loud,好修),而是**双实例身份问题**(磁盘插件 import `@deepseek-ai/*`
|
|
38
|
+
解析到磁盘副本 → 两个 cordis/Fiber 并存)。有解(external 共享树 / virtual namespace 桥接),但必须作为
|
|
39
|
+
build 纪律 + boot 时断言来执行。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 1. "自举"到底指什么 — Cordis 运行时加载面盘点(源码级)
|
|
44
|
+
|
|
45
|
+
把"运行时扫描 + 动态 import"拆成六个具体机制,每个的 bundle 敏感性完全不同:
|
|
46
|
+
|
|
47
|
+
### 1.1 Boot 链与 baseUrl 锚定
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
bin.js(薄壳)→ boot() 【packages/boot/app-boot/src/index.ts:777】
|
|
51
|
+
├─ new Context() # 框架基底
|
|
52
|
+
├─ await ctx.plugin(Loader) # vendored loader 挂载
|
|
53
|
+
├─ ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/' # index.ts:784
|
|
54
|
+
└─ mountRootInclude(ctx, configPath, patches, bareModuleBaseUrl) # index.ts:789
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`baseUrl` = **配置文件所在目录**(profile 目录)。相对路径插件条目(`./foo`)以它为锚 → 这是纯磁盘锚定,
|
|
58
|
+
编译二进制下 fs 照常工作,**不受影响**。
|
|
59
|
+
|
|
60
|
+
### 1.2 `tree.import` — 所有插件模块 import 的收敛点
|
|
61
|
+
|
|
62
|
+
`vendor/loader/src/config/tree.ts:145`,三分支:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import(name) {
|
|
66
|
+
if (name.startsWith('cordis:')) {
|
|
67
|
+
return this.ctx.loader.builtins[name.slice(7)] // ① 内存表,零 fs
|
|
68
|
+
}
|
|
69
|
+
if (this.ctx.loader.internal) {
|
|
70
|
+
return await this.ctx.loader.internal.import(name, this.ctx.baseUrl!, {}) // ② Node 内部 loader
|
|
71
|
+
} else if (name.startsWith('.')) {
|
|
72
|
+
return await import(new URL(name, this.ctx.baseUrl).href) // ③a 相对 → 绝对 file URL
|
|
73
|
+
} else {
|
|
74
|
+
return await import(name) // ③b 裸包名,不可分析
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **① `cordis:` builtins**(include/group 等):静态注册的内存表,bundle 安全 ✅
|
|
80
|
+
- **② Node internals 深集成**(见 1.3):Bun 下拿不到 → 走兜底,**graceful 降级 ✅**
|
|
81
|
+
- **③a 相对名**:先展开成绝对 file URL 再 import → 磁盘锚定,Bun 运行时可 import 磁盘 JS/TS ✅
|
|
82
|
+
- **③b 裸包名**:`await import(name)`,specifier 来自 yml/patch 行,**完全不可静态分析** ⚠️ —— 用户说的
|
|
83
|
+
"最要命"就是这一行(以及 1.4 的同款)
|
|
84
|
+
|
|
85
|
+
### 1.3 Node internals 深集成(loader 的 `internal`)与 HMR
|
|
86
|
+
|
|
87
|
+
`vendor/loader/src/internal.ts` 的 `ModuleLoader.fromInternal()`:
|
|
88
|
+
|
|
89
|
+
- 门槛:`process.versions.node` major ≥ 22,且要拿到 `internal/modules/esm/loader` 的
|
|
90
|
+
`getOrInitializeCascadedLoader()` —— 靠 `--expose-internals` execArgv **或**
|
|
91
|
+
`require('node-addon-require-builtin')`(一个 native addon)。
|
|
92
|
+
- 拿不到 → `fromInternal()` 返回 `undefined` → loader 全链走 ③ 兜底。**这是文档化的降级路径,不是崩**。
|
|
93
|
+
- 唯一硬依赖者是 HMR(`vendor/hmr/src/index.ts:121`:`--expose-internals is required for HMR service`,
|
|
94
|
+
它直接操作 Node 内部 ESM `loadCache` 做模块驱逐)。**HMR 是 dev-only 面,dev 线继续用 Node 跑即可**;
|
|
95
|
+
编译二进制(本来就是 ship 形态)丢 HMR 无损。
|
|
96
|
+
|
|
97
|
+
### 1.4 `mountRootInclude` 的裸包名 seam —— upstream 已经在为打包形态留口
|
|
98
|
+
|
|
99
|
+
`packages/boot/app-boot/src/index.ts:501-518`:当传了 `bareModuleBaseUrl`,root include 换成
|
|
100
|
+
`HostResolvedRootInclude`,把裸包名的解析基点从"配置工程"改指"**installed-host base**":
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
if (internal === undefined) return super.import(specifier, getOuterStack)
|
|
104
|
+
return internal.import(specifier, bareModuleBaseUrl, {})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
docstring(index.ts:745-757)原文值得照抄,因为它证明 upstream 对本研究的主题已有预设计:
|
|
108
|
+
|
|
109
|
+
> bare package names resolve there by default or against an explicit `bareModuleBaseUrl` **for closed
|
|
110
|
+
> packaged runtimes** … use it when the host, rather than the configuration project, **owns the complete
|
|
111
|
+
> plugin set**. … Built bins **need the Loader's native helper for bare plugin specifiers**; relative
|
|
112
|
+
> specifiers do not. … The package build **embeds Include while leaving Loader external**, so the built
|
|
113
|
+
> include tree and host **share one Loader peer**.
|
|
114
|
+
|
|
115
|
+
三句话三个信号:(a) "闭包打包运行时"已是设计词汇;(b) built bin + 裸包名 = 已知难点,当前答案是 Node
|
|
116
|
+
native helper(Bun 下没有 → 正是我们要替换的 seam);(c) 构建已经用 "external peer 保单实例" 的纪律
|
|
117
|
+
(Include 内嵌、Loader 留 external 共享)——**双实例问题不是新问题,是这个纪律的推广**。
|
|
118
|
+
|
|
119
|
+
### 1.5 配置层的其余机制(全部 bundle 友好)
|
|
120
|
+
|
|
121
|
+
- `!!js` 表达式 = `new Function('ctx','expr',…)`(`vendor/loader/src/config/utils.ts:5`),纯 JS,无
|
|
122
|
+
`node:vm`,Bun 支持 ✅
|
|
123
|
+
- YAML/patch 行读写 = fs + yaml,`--dump-config` 预览同理 ✅
|
|
124
|
+
- profile 目录 = `package.json`(out-of-tree 插件 deps + `dsh.profile.bundles` 列序)+
|
|
125
|
+
`cordis.patch.yml` + pnpm `node_modules`(`packages/boot/app-boot/src/profile.ts:5-19`)——纯磁盘数据
|
|
126
|
+
结构,二进制照读 ✅
|
|
127
|
+
|
|
128
|
+
### 1.6 盘点结论
|
|
129
|
+
|
|
130
|
+
**"Cordis 会运行时自举"作为 barrier,其实精确化为两个 choke point 上的裸包名动态 import**:
|
|
131
|
+
`Tree.import` ③b 和 `HostResolvedRootInclude` 的裸名分支。两者都在我们自己 vendor/维护的代码里
|
|
132
|
+
(vendor/loader + app-boot),可patch面极小。其余自举面(builtins、相对路径、yml/patch/!!js、baseUrl)
|
|
133
|
+
在编译二进制下要么天然安全,要么走文档化降级。**barrier 真实存在,但它是"两个函数的裸包名分支",不是
|
|
134
|
+
"框架级黑盒"。**
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 2. Bun `--compile` 的模块解析事实(2026-09 现状)
|
|
139
|
+
|
|
140
|
+
全部带来源;这些是本研究的硬地基:
|
|
141
|
+
|
|
142
|
+
| # | 事实 | 来源 |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| B1 | 编译产物 = 全部被 import 的模块(含字面量动态 import,配 `--splitting` 保懒加载 chunk)+ **完整 Bun 运行时**;built-in Bun/Node API 全支持 | [Bun executables docs](https://bun.sh/docs/bundler/executables)(splitting 示例本身就是 `await import("./lazy.ts")` 编译后免磁盘可用) |
|
|
145
|
+
| B2 | **非可分析动态 import 的裸包名**:运行时从导入者的虚拟位置 `/$bunfs/root/…` 向上找 node_modules → 必败:`Cannot find package "rambda" from "/$bunfs/root/bun-example"`(`import.meta.resolve` 同败) | [azu/bun-build-dynamic-import](https://github.com/azu/bun-build-dynamic-import) repro |
|
|
146
|
+
| B3 | 请求"用 flag 收编非可分析动态 import"的 issue **至今 open**(enhancement/bundler,7 评论,2024-06 开,2025-11 仍有活动,无里程碑);Deno 同问题已用 `--include <path>` 解决 | [oven-sh/bun#11732](https://github.com/oven-sh/bun/issues/11732) |
|
|
147
|
+
| B4 | 编译产物内 `createRequire().resolve()` **不可靠**(不沿 node_modules 祖先上溯)→ 生产级 workaround = 手写 node_modules 目录 walker,把**绝对路径**喂回去,Node builtin 标 `external: true` | [vercel/turborepo#11900](https://github.com/vercel/turborepo/pull/11900)(fix #11882,2026-02) |
|
|
148
|
+
| B5 | 相对路径若不在 bundle 内 → **从进程 cwd 读磁盘**,不存在则报错(Worker/SQLite 章节明示 cwd 锚定语义) | [Bun executables docs](https://bun.sh/docs/bundler/executables) |
|
|
149
|
+
| B6 | 内嵌运行时是完整的:编译产物可 `BUN_BE_BUN=1` 直接当 **bun CLI** 用(install/run/打包),即二进制自带转译器与包管理器——**用户侧可以完全不装 Node/pnpm** | 同上(v1.2.16+) |
|
|
150
|
+
| B7 | `Bun.build` 在编译产物内可用 → **运行时打包**模式成立:Turborepo 在编译产物里对用户磁盘上的 TS 配置现场 `Bun.build`,并用 onResolve/onLoad **virtual namespace 把二进制内置模块桥接给用户代码**(`BINARY_MODULES`),node builtin 走 external | [vercel/turborepo#11900](https://github.com/vercel/turborepo/pull/11900) |
|
|
151
|
+
| B8 | `--asset ./dir` 可整树内嵌,运行时经 `import.meta.dir`(虚拟根 `/$bunfs`)+ `node:fs`(readdirSync 等)可达;`with {type:"file"}` 内嵌文件同理;`Bun.isStandaloneExecutable` 可探测编译态;bytecode/sourcemap/minify/交叉编译齐备;`--compile` 不支持 `--outdir/--public-path/--no-bundle` | 同上 |
|
|
152
|
+
| B9 | `.env`/`bunfig.toml` 运行时自动加载(默认开),tsconfig/package.json 默认不加载(可开) | 同上 |
|
|
153
|
+
| B10 | **行为级 divergence(隐性、不可枚举)**:Bun 的 spawn 会消费 `encoding` 选项 → execa 的 `encoding:"buffer"` 组合直接抛 `ERR_UNKNOWN_ENCODING`(stable 1.3.14 仍复现);`bun install` 解不了 pnpm workspace 布局。这类不兼容**踩到才知道**,无静态清单可穷举 | [openclaw#114256](https://github.com/openclaw/openclaw/pull/114256)(2026-07 merged)引 [oven-sh/bun#36049](https://github.com/oven-sh/bun/issues/36049) |
|
|
154
|
+
| B11 | `node:sqlite`:Bun ≤1.3.x **不提供**;1.4.0 canary(**Rust 重写线**)起提供(`DatabaseSync` 实测可用)。388k★ 的 OpenClaw 接法 = `process.getBuiltinModule('node:sqlite')` **feature-probe,不做品牌/版本门** | [openclaw#114256](https://github.com/openclaw/openclaw/pull/114256) |
|
|
155
|
+
| B12 | `better-sqlite3`:可跑但**需重编译**;且有真实 N-API crash 服务启动失败案例,社区以 `bun:sqlite` 规避——native addon 在 Bun 下的"能用"是逐版本、逐包的灰色地带 | [oven-sh/bun#16050](https://github.com/oven-sh/bun/issues/16050)、[opencode-telegram-bridge#34](https://github.com/gabriel-trigo/opencode-telegram-bridge/issues/34)、[OmniRoute#11468](https://github.com/diegosouzapw/OmniRoute/pull/11468) |
|
|
156
|
+
推论:**Bun 编译世界的模块解析 = "bundle 内虚拟根(`/$bunfs`)+ 磁盘(绝对路径/cwd 相对)"两界**。裸包名
|
|
157
|
+
只在磁盘界有 node_modules 语义,而 bundled 模块住在虚拟界 → B2 必败。这就是 1.2 ③b 在编译产物下的死因。
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 3. 冲突矩阵:Cordis 自举面 × Bun compile
|
|
162
|
+
|
|
163
|
+
| Cordis 机制(§1) | Node 语义 | Bun compile 语义 | 差距判定 |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| `cordis:` builtins(include/group) | 内存表 | bundle 内 ✅ | 无 |
|
|
166
|
+
| 相对名插件(`./x` → baseUrl 绝对 URL) | 磁盘 import | 磁盘 import(运行时转译 TS 可用,B1/B6/B7) | 无 |
|
|
167
|
+
| **裸包名(`Tree.import` ③b / `HostResolvedRootInclude`)** | node_modules 分层上溯(①→④ 层) | **B2 必败**(虚拟根无 node_modules);`createRequire` 也不可靠(B4) | **核心差距,§4-B patch** |
|
|
168
|
+
| `internal`(Node cascaded loader) | 经 native addon/`--expose-internals` 取 internals | 取不到 → `fromInternal()`=undefined → **文档化降级**到兜底 import | 无阻断(丢深集成) |
|
|
169
|
+
| HMR(loadCache 驱逐) | 硬依赖 internals | 直接抛 `--expose-internals is required` | **放弃于编译形态**;dev 线保留 Node |
|
|
170
|
+
| `!!js` / yml / patch 行 / `--dump-config` | new Function + fs + yaml | 同左,全支持 | 无 |
|
|
171
|
+
| profile 目录(package.json/bundles/pnpm 树) | 磁盘数据结构 | 磁盘照读(B5/B9) | 无 |
|
|
172
|
+
| **磁盘插件 runtime-import `@deepseek-ai/*` peers**(14/14 实证,见 AGENTS.md §一) | host ②③ 层单一物理副本 → 单实例 | 解析到磁盘副本或失败 → **双实例/断裂风险** | **§5 专节,比 B2 阴险** |
|
|
173
|
+
| `dsh plugin add` → pnpm 子进程 | 用户侧需 Node+pnpm | 可改 `BUN_BE_BUN=1` 自身当包管理器(B6) | 运营面机会(§6) |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 4. 缓解方案空间(按"自包含性 × 动态性"光谱)
|
|
178
|
+
|
|
179
|
+
### 方案 A:Frozen composition — 构建期 codegen 静态插件注册表(编译期已知集)
|
|
180
|
+
|
|
181
|
+
- **机制**:构建期枚举已知集(第一方 bundle `dsh-base`/`dsh-web-app`、工具自有组件、vendored 与依赖树第三方——凡 build 时在图内者,不问出身),生成
|
|
182
|
+
`plugins.generated.ts`:`{ 'dsh-base': () => import('<literal path>'), … }`(**字面量** → 可分析 →
|
|
183
|
+
被 bundle,`--splitting` 下每个插件是懒加载 chunk,B1)。patch `Tree.import`/root include:
|
|
184
|
+
**registry-first,磁盘-fallback**。二进制内建 `Loader` 身份天然单实例(bundler 去重)。
|
|
185
|
+
- **得到**:真·单文件;启动即 fail-loud 审计(`assertEntriesLoaded`)语义不变;`cordis.patch.yml` 整条
|
|
186
|
+
patch 线**原样可用**——patch 是配置层,id 覆盖/config/inject/`!!js` 全在运行时解释。Cordis 面向用户的
|
|
187
|
+
组合动态性(配置树)一点没冻。
|
|
188
|
+
- **失去**:二进制内置插件不可热插拔(本来就无此需求);内置集升级 = 重新发二进制(对工具发行恰是
|
|
189
|
+
reproducibility 收益)。
|
|
190
|
+
- **成本**:codegen 脚本 + loader 一处 patch(registry 查询优先)。低。
|
|
191
|
+
|
|
192
|
+
### 方案 B:Hybrid closed runtime — 二进制核 + 磁盘 profile 树(第三方插件)
|
|
193
|
+
|
|
194
|
+
- **机制**:二进制 = Bun 运行时 + harness 核 + vendored loader;`$DSH_HOME/profiles/<name>/` 磁盘树照旧
|
|
195
|
+
(package.json + node_modules)。**裸包名 patch**(在 `HostResolvedRootInclude.import` 的
|
|
196
|
+
`internal === undefined` 分支 + `Tree.import` ③b):手写 node_modules walker(从 `baseUrl` /
|
|
197
|
+
`bareModuleBaseUrl` 指向的 profile 目录上溯)→ 得绝对路径 → `pathToFileURL().href` → `import()`。
|
|
198
|
+
这就是 Turborepo #11900 的生产配方(B4),一行不差地适用:他们的场景(编译产物加载用户磁盘配置、配置
|
|
199
|
+
import npm 包)与 Cordis loader 加载磁盘插件**同构**。
|
|
200
|
+
- **得到**:第三方插件生态全保留——用户 `dsh plugin add X` 装进 profile 目录,重启即挂载;相对名/裸名/
|
|
201
|
+
`cordis:` 全通。**用户侧无版本解析**:发布物是"预解析好的树"(CI 里 pnpm 已经算完 lockfile),或运行时
|
|
202
|
+
`BUN_BE_BUN=1` 自装(B6)。
|
|
203
|
+
- **失去**:不再是单文件(二进制 + DSH_HOME 树)——但"避免用户侧 Node 依赖/版本解析"的原始动机完整保住。
|
|
204
|
+
- **成本**:walker patch(~50 行,Turborepo patch 可参考)+ 解析锚点测试。中低。
|
|
205
|
+
|
|
206
|
+
### 方案 C:Runtime bundling — Turborepo 全套(virtual namespace 桥接,B 方案进阶)
|
|
207
|
+
|
|
208
|
+
- **机制**(B7):磁盘插件不直接 `import()`,而是现场 `Bun.build`:onResolve 对裸包名先查磁盘 walker;
|
|
209
|
+
查不到再查 `BINARY_MODULES`(二进制内嵌的 `@deepseek-ai/*` 等 host 包)→ 转向 virtual namespace,
|
|
210
|
+
onLoad 把内嵌模块**桥接**给插件代码。
|
|
211
|
+
- **得到**:磁盘插件可以直接 import host 包且**单实例**(§5 的最优解);比 B 多了对外部插件的完整身份控制。
|
|
212
|
+
- **失去**:桥接层是自维护面(CJS/ESM 边界、export 形状、加载延迟);复杂度显著高于 B。
|
|
213
|
+
- **定位**:B 跑通后的按需升级,不是首选项。
|
|
214
|
+
|
|
215
|
+
### 方案 D:等上游 — `--include` flag(#11732)/ Deno 式收编
|
|
216
|
+
|
|
217
|
+
Deno 用 `--include` 解决了同构问题,Bun issue 两岁半仍 open(B3)。**不作为路径,只作观察项**:一旦落地,
|
|
218
|
+
A 的 codegen 可换成声明式 `--include` 清单,B 的磁盘集也可预收编。
|
|
219
|
+
|
|
220
|
+
### 方案 E:Node sidecar — Bun 静态核 + IPC + 真 Node 运行时承接动态面(owner 提案,v2 新增)
|
|
221
|
+
|
|
222
|
+
- **动机**:动态集的无界性使"Bun 运行时内兼容一切 Node 插件"不收敛——B10 类**行为级 divergence**(连
|
|
223
|
+
`child_process` 的 option 组合都能翻车)没有静态清单可穷举,只有踩到才知道。唯一"完全兼容 Node 生态"
|
|
224
|
+
的东西是 Node 本身 → 动态面整体路由给真实 Node sidecar,Bun 侧只保静态核。
|
|
225
|
+
- **先例校准(没有听起来那么"没人做过")**:家族先例 = VS Code Extension Host(主进程 + 扩展宿主进程 +
|
|
226
|
+
RPC 化 API 面)、Claude Code(bun 单文件 + MCP 子进程生态——动态面全在 bun 体外跑,即 E-seam 形态的
|
|
227
|
+
大规模存在证明)、dsh 自己的 fd3 code-runtime(cordis-research.md §5 桥表)。**真正没人 ship 过的只有
|
|
228
|
+
一块:跨进程 Cordis context bridge**(inject-epoch 反应式、fiber disposal 传播、事件全序跨边界)——
|
|
229
|
+
cordis-research.md §7.2(b) 已判"研究级、非免费午餐"。
|
|
230
|
+
- **三个子形态,成本差一个量级**:
|
|
231
|
+
- **E-seam**:动态组件经工具面接入(MCP/ACP/subprocess 工具),不进 context graph。现有原语直接可用,
|
|
232
|
+
成本≈0;代价是动态组件不是"Cordis 公民"(无 inject/services/events)。
|
|
233
|
+
- **E-subtree**:动态插件挂 Node 侧真实 cordis+loader(磁盘树),整组作为一个 remote group 桥回核心
|
|
234
|
+
图;服务/事件在**组边界**显式代理。桥面收窄为接口清单,工程可控——"野心方案"的可交付版本。
|
|
235
|
+
- **E-full**:双 context 全语义融合(任意插件可挂任意侧、inject 跨边界反应)。研究级,月级成本,不建议
|
|
236
|
+
作首发目标。
|
|
237
|
+
- **架构红利**:双实例问题被**驯化**——两个 context 是设计而非事故(§5 身份风险在边界上显式化);
|
|
238
|
+
sidecar 用磁盘树真 cordis,身份天然一致。
|
|
239
|
+
- **成本/风险**:IPC 管道不贵(dsh 有 sdk-jsonrpc/acp/fd3 库存),贵在 Cordis 语义保真(E-full 的
|
|
240
|
+
inject-epoch 跨边界);artifact 变"Bun 核 + pinned node + 插件树"(约两份运行时体积);**若多数用户
|
|
241
|
+
最终都要 sidecar,Bun 简洁性论证反转**——这是必须用真实插件集先测的决策变量;另 Bun 1.4 起 runtime
|
|
242
|
+
本身在 Rust 重写线上(B11),把 C 类深度桥接押在其上要计入成熟度风险。
|
|
243
|
+
- **Node 从哪来**:随包 pinned node(免用户安装与版本解析,原始动机保全)或 tiered——默认单文件,检测到
|
|
244
|
+
动态插件需求才要求/下载 sidecar。
|
|
245
|
+
|
|
246
|
+
### 推荐(v2):**B 与 E 是升级关系,不是二选一**
|
|
247
|
+
|
|
248
|
+
loader 做**双运行时路由**——patch 行声明(或 probe)`runtime: bun|node`:纯 JS/TS 动态插件在 Bun 运行时
|
|
249
|
+
内直接跑(B 的 walker patch 覆盖大多数);带 native / 踩 B10 类雷的插件路由到 Node sidecar(E-subtree
|
|
250
|
+
起步)。无动态插件 = 单文件(tiered)。路由判定用 OpenClaw 验证过的 **feature-probe** 模式
|
|
251
|
+
(`process.getBuiltinModule` / 试载探测),不做品牌/版本门。C(runtime bundling 桥接)降级为 B 的可选
|
|
252
|
+
增强;D(#11732)保持观察。
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 5. 双实例身份问题(比"找不到包"更阴险,单独一节)
|
|
257
|
+
|
|
258
|
+
**现象**:磁盘插件(B/C 世界)运行时 `import '@deepseek-ai/cordis'`(better-dsh 实证 14 个 host 包是
|
|
259
|
+
**运行期 import**,非 type-only,见 AGENTS.md §一)。Node 部署下 ②③ 层的单一物理副本保证插件与 host 拿到
|
|
260
|
+
**同一个模块实例**。编译世界里 host 的副本在 `/$bunfs` 内,磁盘插件的解析只能落在磁盘 → 两个 cordis 并存。
|
|
261
|
+
|
|
262
|
+
**为什么致命**:cordis 的 `RegistryService` 按 callback 身份键控、`ReflectService` 沿 fiber 祖先解析、
|
|
263
|
+
`ctx` proxy 与 `instanceof`/`symbols.isolate` 边界判定全部依赖**跨模块单实例**。双实例不一定立刻崩——
|
|
264
|
+
更坏:半工作状态(事件两套、fiber 图断裂、卸载链丢失),恰好踩中 cordis-research.md §2 "两份 cordis 身份
|
|
265
|
+
风险" 的老坑。
|
|
266
|
+
|
|
267
|
+
**解法(按纪律强度排序)**:
|
|
268
|
+
|
|
269
|
+
1. **external 共享树**(B 的正统解):身份关键包(`@deepseek-ai/cordis`、vendored loader、
|
|
270
|
+
`cosmokit`、`schemastery`)在二进制 build 里标 **`--external`**,运行时从磁盘共享树加载——host 与磁盘
|
|
271
|
+
插件解析到同一物理副本。app-boot 已有同构先例:"embeds Include while **leaving Loader external**, so
|
|
272
|
+
the built include tree and host **share one Loader peer**"(§1.4 引文)——把这个纪律从"Loader 一个包"
|
|
273
|
+
推广到"身份关键包清单"即可。代价:这批包必须随 DSH_HOME 树一起 ship(树本来就要 ship,边际成本≈0)。
|
|
274
|
+
2. **virtual namespace 桥接**(C):二进制内嵌包经 runtime bundling 桥给插件,单实例由 build plugin 保证。
|
|
275
|
+
3. **boot 断言**(无论选哪条):启动时 probe 单实例(如 `ctx.loader` 与插件侧 `import` 得到的
|
|
276
|
+
`Loader`/`Context` 同源),fail-loud 拒绝双实例启动——把隐性半工作态变成显性启动错误,符合 dsh 的
|
|
277
|
+
fail-loud 哲学。
|
|
278
|
+
4. **方案 E 的驯化红利**:若动态面走 Node sidecar,Node 侧用磁盘树里的真 cordis——双 context 是显式设计
|
|
279
|
+
而非事故,身份一致性在边界两侧各自成立;本节三条纪律只适用于"Bun 进程内同时存在双侧副本"的 B/C 世界。
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 6. 运营面(简述)
|
|
283
|
+
|
|
284
|
+
- **用户安装面**:单文件(A)或"二进制+解包树"(B),均无 Node 版本协商、无 pnpm hoisting 分层、无
|
|
285
|
+
用户侧供应链年龄门(`minimumReleaseAge` 是用户 install 相位的 pnpm 策略——版本解析已在 CI 完成即消解)。
|
|
286
|
+
这正对 AGENTS.md 里记录的 0.2.2 发布日 install 被拦一类运营痛点。
|
|
287
|
+
- **`dsh plugin add`**:现走 pnpm 子进程(AGENTS.md §一)。Bun 世界两条路:`BUN_BE_BUN=1 ./dsh install`
|
|
288
|
+
(B6,二进制自身即包管理器,用户零依赖)或继续要求 pnpm(锁 pnpm 生态语义:年龄门、hoisted 模型)。
|
|
289
|
+
注意 bun install ≠ pnpm(lockfile/hoist/策略引擎均不同)——切换是生态决策不是纯技术决策,**列为开放
|
|
290
|
+
问题**。
|
|
291
|
+
- **dev/test 线不变**:4999 源码级实例、HMR、`--expose-internals` 全部留在 Node 轨道(§1.3);编译形态只
|
|
292
|
+
是 ship 面的新成员。两轨道同源(cordis-research.md §4.2 的"195 包照旧 tsdown 构建,只在装配层用 Bun")。
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## 7. 判决
|
|
297
|
+
|
|
298
|
+
1. **Barrier 2(自举)是真的,但被高估为"框架级";实测是"两个 choke point 的裸包名分支"**,且 loader 是
|
|
299
|
+
自家 vendor 代码、upstream 已有 `bareModuleBaseUrl`/"closed packaged runtime" 的设计预留。可patch面小、
|
|
300
|
+
边界清晰。
|
|
301
|
+
2. **Bun 侧的失败模式有生产级先例与配方**:B2/B4(azu repro、Turborepo #11900)不仅确诊了"编译产物内
|
|
302
|
+
裸包名/`createRequire` 必败",还交付了被验证的 workaround(手写 walker + 绝对路径 + external builtin
|
|
303
|
+
+ virtual namespace)。
|
|
304
|
+
3. **A(冻结已知集)+ B(磁盘承接动态集)是基线形态**;**E(Node sidecar)按真实插件集的 Bun 通过率
|
|
305
|
+
决定是首发件还是后备件**(§4-E 路由器模式:`runtime: bun|node` 双路由 + feature-probe)。Deno 的
|
|
306
|
+
`--include` 是该张力的生态级参照物;Bun 尚未跟进(#11732 open)。
|
|
307
|
+
4. **必须带着 §5 的身份纪律上线**:external 共享树 + boot 单实例断言,否则双实例会把问题变成最难查的
|
|
308
|
+
半工作态。
|
|
309
|
+
5. **PoC 清单**(按依赖序,预计一天内可跑完判定):
|
|
310
|
+
a. 最小 cordis app(vendored core + loader + 一个静态插件 + 一行 yml)`bun build --compile` → 预期
|
|
311
|
+
boot 成功(core 零 native、`new Function`/fs/yaml 全通)。
|
|
312
|
+
b. 加一行裸包名条目 + 磁盘 node_modules → 复现 B2 报错 → 打 walker patch(Turborepo 配方)→ 复通。
|
|
313
|
+
c. 双实例 probe:磁盘插件 import `@deepseek-ai/cordis`,断言与 host 同实例 → 验证 external 共享树。
|
|
314
|
+
d. A 的 codegen registry(bund bundles 列表生成)→ registry-first → 懒加载 chunk + fail-loud 审计。
|
|
315
|
+
e. `BUN_BE_BUN=1 ./dsh install` 在 profile 目录装一个真插件(运营面冒烟)。
|
|
316
|
+
f. 双运行时路由率测定:拿 3–5 个真实目标动态插件在 Bun 运行时试载(`process.getBuiltinModule` probe
|
|
317
|
+
+ 试 import + native 探测)→ 通过率决定 E-sidecar 是首发件还是后备件。
|
|
318
|
+
g. E-subtree 最小桥:Node 侧起真 loader 挂一个 group,服务/事件经组边界代理回 Bun 核——验证桥面接口
|
|
319
|
+
清单是否收敛(§4-E 的可交付性判定)。
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 来源
|
|
324
|
+
|
|
325
|
+
- 本仓一手源码:`upstream/deepseek-harness`(`dsh-v0.1.2-alpha.5`)`vendor/loader/src/{internal.ts,
|
|
326
|
+
config/tree.ts, config/utils.ts, index.ts}`、`vendor/hmr/src/index.ts`、
|
|
327
|
+
`packages/boot/app-boot/src/{index.ts, profile.ts}`
|
|
328
|
+
- [Bun — Single-file executable(2026-09)](https://bun.sh/docs/bundler/executables):bundled modules +
|
|
329
|
+
运行时、cwd 锚定(Worker/SQLite)、`/$bunfs` 内嵌文件与 `--asset` 目录树、`BUN_BE_BUN=1`、
|
|
330
|
+
`--splitting`、bytecode/sourcemap、不支持项清单
|
|
331
|
+
- [azu/bun-build-dynamic-import](https://github.com/azu/bun-build-dynamic-import):非可分析动态 import 在
|
|
332
|
+
编译产物的 `Cannot find package … from "/$bunfs/root/…"` repro
|
|
333
|
+
- [oven-sh/bun#11732](https://github.com/oven-sh/bun/issues/11732):`--include` flag 请求,state=open
|
|
334
|
+
(API 实查 2026-09-03);issue 正文引 Deno `--include` 先例
|
|
335
|
+
- [vercel/turborepo#11900](https://github.com/vercel/turborepo/pull/11900)(fix #11882,2026-02):
|
|
336
|
+
编译产物内 `createRequire().resolve()` 不可靠 → 手写 node_modules walker;运行时 `Bun.build` 用户配置 +
|
|
337
|
+
`BINARY_MODULES` virtual namespace 桥接 + node builtin `external: true`;含回归测试
|
|
338
|
+
- [openclaw/openclaw#114256](https://github.com/openclaw/openclaw/pull/114256)(2026-07 merged):388k★ TS
|
|
339
|
+
工具加实验性 Bun 支持实录——node:sqlite 在 1.3.x 缺、1.4.0 canary(Rust 重写线)提供;execa
|
|
340
|
+
`encoding:"buffer"` 触发 #36049;接法 = `process.getBuiltinModule` feature-probe 而非品牌门
|
|
341
|
+
- [oven-sh/bun#36049](https://github.com/oven-sh/bun/issues/36049)(spawn `encoding` divergence,stable
|
|
342
|
+
1.3.14 复现)、[oven-sh/bun#16050](https://github.com/oven-sh/bun/issues/16050)(better-sqlite3 需重编译)、
|
|
343
|
+
[opencode-telegram-bridge#34](https://github.com/gabriel-trigo/opencode-telegram-bridge/issues/34)
|
|
344
|
+
(better-sqlite3 N-API crash 实案)、[OmniRoute#11468](https://github.com/diegosouzapw/OmniRoute/pull/11468)
|
|
345
|
+
(以 bun:sqlite 规避 N-API crash)
|
|
346
|
+
- 本仓既有研究:[cordis-research.md](./cordis-research.md) §2(no privileged core / bootstrap 链)、§4.2
|
|
347
|
+
(Bun 编译首判:native addon 政策、Claude Code 先例、tsdown 线不动);AGENTS.md §一(①→④ 解析分层、
|
|
348
|
+
14/14 运行期 host import、供应链年龄门运营史)
|
|
@@ -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 文档)
|
package/lib/index.d.ts
CHANGED
|
@@ -547,7 +547,6 @@ type ToolCallId = ToolExecutionInput['callId'];
|
|
|
547
547
|
interface WebTrustConfig {
|
|
548
548
|
/** Hostnames this operator declares their own devices' pages run on. */
|
|
549
549
|
trustedPageAuthorities?: readonly string[];
|
|
550
|
-
/** Mobile responsiveness knobs (client half consumes via page global). */
|
|
551
550
|
mobile?: {
|
|
552
551
|
enabled?: boolean;
|
|
553
552
|
breakpoint?: number;
|
|
@@ -556,6 +555,15 @@ interface WebTrustConfig {
|
|
|
556
555
|
leftEdgeBandPx?: number;
|
|
557
556
|
rightZoneRatio?: number;
|
|
558
557
|
swipeVelocityPxPerMs?: number;
|
|
558
|
+
/**
|
|
559
|
+
* iOS focus auto-zoom suppression mode (change
|
|
560
|
+
* `2026-09-03-ios-focus-zoom-suppression`): `'meta'` (default) rewrites
|
|
561
|
+
* the viewport meta on iOS-class narrow viewports; `'off'` is the escape
|
|
562
|
+
* hatch. `'font'` (16px floor, solution A) is a RESERVED value slot,
|
|
563
|
+
* deliberately not in the schema enum — unimplemented values fail loud
|
|
564
|
+
* at config load rather than no-op silently.
|
|
565
|
+
*/
|
|
566
|
+
zoomGuard?: 'meta' | 'off';
|
|
559
567
|
};
|
|
560
568
|
}
|
|
561
569
|
//#endregion
|