@serveryyswys/node.operit 0.0.0-stage → 0.1.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 (123) hide show
  1. package/BUILTINS.json +233 -0
  2. package/DESIGN.md +513 -0
  3. package/LICENSE +674 -0
  4. package/README.md +301 -2
  5. package/dist/assert/strict.d.ts +2 -0
  6. package/dist/assert/strict.js +7 -0
  7. package/dist/assert.d.ts +3 -0
  8. package/dist/assert.js +483 -0
  9. package/dist/buffer.d.ts +60 -0
  10. package/dist/buffer.js +524 -0
  11. package/dist/child_process.d.ts +56 -0
  12. package/dist/child_process.js +371 -0
  13. package/dist/constants.d.ts +2 -0
  14. package/dist/constants.js +19 -0
  15. package/dist/crypto.d.ts +76 -0
  16. package/dist/crypto.js +312 -0
  17. package/dist/dns/promises.d.ts +34 -0
  18. package/dist/dns/promises.js +190 -0
  19. package/dist/dns.d.ts +88 -0
  20. package/dist/dns.js +259 -0
  21. package/dist/events.d.ts +40 -0
  22. package/dist/events.js +237 -0
  23. package/dist/fs/promises.d.ts +159 -0
  24. package/dist/fs/promises.js +129 -0
  25. package/dist/fs.d.ts +221 -0
  26. package/dist/fs.js +944 -0
  27. package/dist/http.d.ts +92 -0
  28. package/dist/http.js +319 -0
  29. package/dist/https.d.ts +296 -0
  30. package/dist/https.js +50 -0
  31. package/dist/index.d.ts +2422 -0
  32. package/dist/index.js +57 -0
  33. package/dist/lib/bridge.d.ts +60 -0
  34. package/dist/lib/bridge.js +205 -0
  35. package/dist/lib/bytes.d.ts +20 -0
  36. package/dist/lib/bytes.js +239 -0
  37. package/dist/lib/child-process-types.d.ts +45 -0
  38. package/dist/lib/child-process-types.js +2 -0
  39. package/dist/lib/cwd.d.ts +2 -0
  40. package/dist/lib/cwd.js +32 -0
  41. package/dist/lib/deep-equal.d.ts +3 -0
  42. package/dist/lib/deep-equal.js +223 -0
  43. package/dist/lib/deflate.d.ts +2 -0
  44. package/dist/lib/deflate.js +234 -0
  45. package/dist/lib/dns-driver.d.ts +14 -0
  46. package/dist/lib/dns-driver.js +85 -0
  47. package/dist/lib/dns-types.d.ts +22 -0
  48. package/dist/lib/dns-types.js +2 -0
  49. package/dist/lib/errors.d.ts +48 -0
  50. package/dist/lib/errors.js +424 -0
  51. package/dist/lib/fs-driver.d.ts +70 -0
  52. package/dist/lib/fs-driver.js +364 -0
  53. package/dist/lib/fs-env.d.ts +30 -0
  54. package/dist/lib/fs-env.js +157 -0
  55. package/dist/lib/fs-types.d.ts +19 -0
  56. package/dist/lib/fs-types.js +2 -0
  57. package/dist/lib/hashes.d.ts +27 -0
  58. package/dist/lib/hashes.js +432 -0
  59. package/dist/lib/http-driver.d.ts +30 -0
  60. package/dist/lib/http-driver.js +72 -0
  61. package/dist/lib/http-types.d.ts +18 -0
  62. package/dist/lib/http-types.js +2 -0
  63. package/dist/lib/inflate.d.ts +6 -0
  64. package/dist/lib/inflate.js +284 -0
  65. package/dist/lib/path-types.d.ts +37 -0
  66. package/dist/lib/path-types.js +2 -0
  67. package/dist/lib/paths.d.ts +8 -0
  68. package/dist/lib/paths.js +106 -0
  69. package/dist/lib/perf-hooks-types.d.ts +18 -0
  70. package/dist/lib/perf-hooks-types.js +2 -0
  71. package/dist/lib/process-driver.d.ts +31 -0
  72. package/dist/lib/process-driver.js +102 -0
  73. package/dist/lib/punycode.d.ts +8 -0
  74. package/dist/lib/punycode.js +179 -0
  75. package/dist/lib/stream-types.d.ts +29 -0
  76. package/dist/lib/stream-types.js +2 -0
  77. package/dist/lib/timers-types.d.ts +18 -0
  78. package/dist/lib/timers-types.js +2 -0
  79. package/dist/lib/url-search-params.d.ts +26 -0
  80. package/dist/lib/url-search-params.js +281 -0
  81. package/dist/lib/url-types.d.ts +44 -0
  82. package/dist/lib/url-types.js +2 -0
  83. package/dist/lib/url-whatwg.d.ts +78 -0
  84. package/dist/lib/url-whatwg.js +857 -0
  85. package/dist/lib/zlib-types.d.ts +12 -0
  86. package/dist/lib/zlib-types.js +2 -0
  87. package/dist/module.d.ts +8 -0
  88. package/dist/module.js +72 -0
  89. package/dist/os.d.ts +80 -0
  90. package/dist/os.js +247 -0
  91. package/dist/path/posix.d.ts +2 -0
  92. package/dist/path/posix.js +7 -0
  93. package/dist/path/win32.d.ts +2 -0
  94. package/dist/path/win32.js +10 -0
  95. package/dist/path.d.ts +3 -0
  96. package/dist/path.js +1222 -0
  97. package/dist/perf_hooks.d.ts +36 -0
  98. package/dist/perf_hooks.js +144 -0
  99. package/dist/process.d.ts +69 -0
  100. package/dist/process.js +230 -0
  101. package/dist/querystring.d.ts +15 -0
  102. package/dist/querystring.js +209 -0
  103. package/dist/stream/promises.d.ts +5 -0
  104. package/dist/stream/promises.js +10 -0
  105. package/dist/stream.d.ts +84 -0
  106. package/dist/stream.js +875 -0
  107. package/dist/string_decoder.d.ts +15 -0
  108. package/dist/string_decoder.js +237 -0
  109. package/dist/timers/promises.d.ts +19 -0
  110. package/dist/timers/promises.js +74 -0
  111. package/dist/timers.d.ts +21 -0
  112. package/dist/timers.js +41 -0
  113. package/dist/tty.d.ts +9 -0
  114. package/dist/tty.js +25 -0
  115. package/dist/unsupported.d.ts +12 -0
  116. package/dist/unsupported.js +17 -0
  117. package/dist/url.d.ts +28 -0
  118. package/dist/url.js +486 -0
  119. package/dist/util.d.ts +74 -0
  120. package/dist/util.js +515 -0
  121. package/dist/zlib.d.ts +88 -0
  122. package/dist/zlib.js +371 -0
  123. package/package.json +29 -4
package/DESIGN.md ADDED
@@ -0,0 +1,513 @@
1
+ # node.operit 设计文档
2
+
3
+ > 本文是设计依据,不是使用说明。使用说明见 [README.md](README.md);
4
+ > 机器可读的契约见 [BUILTINS.json](BUILTINS.json)。
5
+
6
+ ## 1. 定位
7
+
8
+ 两个东西,职责严格分开:
9
+
10
+ | | **OperitPackageManager**(OPM,自建 npm) | **node.operit** |
11
+ |---|---|---|
12
+ | 形态 | 本身是一个 toolpkg,纯 JS,跑在 Operit 的 QuickJS 里 | 自建源上的一个普通包,CommonJS 可加载 |
13
+ | 定位 | **解析 / 分发层**,给 AI 提供工具 | **能力层** |
14
+ | 职责 | 下载、算真实入口、重写裸名、生成清单、打成 toolpkg | 补齐 Node 核心能力与语义 |
15
+ | 依赖方向 | 用运行时的原始 API 干活,**不依赖** node.operit | 只依赖宿主 Java bridge + 内建 `lodash/uuid/axios` |
16
+
17
+ `opm init` 时自动把 node.operit 下载到 `<项目>/node_modules/@serveryyswys/node.operit/`,
18
+ 版本按项目锁定。node.operit 是**被生产出来的项目的运行期依赖**,不是 OperitPackageManager 的运行期依赖。
19
+
20
+ ## 2. 运行时的硬事实
21
+
22
+ 以下全部来自 Operit 源码核对,是整套设计的依据。
23
+
24
+ ### 2.1 模块系统
25
+
26
+ - **`require` 只有一处实现**:[JsExecutionScriptBuilder.kt:1262](../Operit-src/app/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsExecutionScriptBuilder.kt:1262) 的 `requireInternal`
27
+ - **唯一执行入口**:`__operitExecuteScriptFunction`(`TOOLPKG_EXECUTION_ENTRY_FUNCTION`),普通包与 toolpkg 共用
28
+ - **裸名处理**:
29
+
30
+ | 请求 | 行为 |
31
+ |---|---|
32
+ | `require('lodash')` | → `root._`,运行时的**迷你 lodash**,只有 8 个方法 |
33
+ | `require('uuid')` | → 手写的 `v4()` |
34
+ | `require('axios')` | → `get`/`post` 包到 `toolCall('http_request')` |
35
+ | **其他裸名** | → **静默 `return {}`**,不报错 |
36
+
37
+ - **解析规则**(`resolveModulePath` / `buildCandidatePaths`):
38
+ - 只有相对路径与绝对路径会走解析;**没有 `node_modules` 上溯**
39
+ - **没有 `package.json` 的 `main` / `exports` 解析**
40
+ - 候选只有 5 个:`x`、`x.js`、`x.json`、`x/index.js`、`x/index.json`
41
+ - **模块源码来自 toolpkg 归档**:`NativeInterface.readToolPkgTextResource(packageTarget, path)`
42
+ - **toolpkg 会被解压到磁盘**:`<app filesDir>/toolpkg_cache/<packageName>/`;`extractZipEntriesFromExternal` 是**无过滤的全量 unzip**([ToolPkgParser.kt:1669](../Operit-src/app/src/main/java/com/ai/assistance/operit/core/tools/packTool/ToolPkgParser.kt:1669))
43
+ - **`new Function` 与 `eval` 可用**:Operit 自己的模块工厂就是在用([:297](../Operit-src/app/src/main/java/com/ai/assistance/operit/core/tools/javascript/JsExecutionScriptBuilder.kt:297))
44
+ - **模块实例缓存**是运行时全局的 `root.__operitModuleInstanceCache`,key 里含 `packageTarget` 与源码 hash
45
+ → **同一包的模块实例会跨 call 复用**,模块级状态会跨工具调用保留
46
+
47
+ ### 2.1.1 运行时注入的全局(重要)
48
+
49
+ `JsLibraries.kt` 会把下面这些挂到全局:`_`、`dataUtils`、`Icons`、`Tools`、`Java`、`Kotlin`、
50
+ `CryptoJS`、`pako`、`Jimp`、`UINode`、`Android`、`OkHttp3` 等。
51
+
52
+ **但 `CryptoJS` 与 `pako` 都不是真库,而是极薄的"原生桥" shim:**
53
+
54
+ | 全局 | 真实能力 |
55
+ |---|---|
56
+ | `CryptoJS` | 仅 `MD5(string)` 与 `AES.decrypt`;没有 SHA 系列、HMAC、PBKDF2 |
57
+ | `pako` | 仅 `inflate(base64String, { to: 'string' })`;没有 deflate/gzip、不接受字节 |
58
+
59
+ 对应的原生入口 `NativeInterface.crypto(algorithm, operation, args)` 也只认 `md5` 与 `aes/decrypt`。
60
+
61
+ **结论:不能用它们实现 `crypto` / `zlib`。** 两者都改成了纯 JS 实现
62
+ (`lib/hashes.ts`、`lib/inflate.ts`、`lib/deflate.ts`),这样反而能跟 Node 全量对拍 ——
63
+ `zlib` 是**双向**验证:Node 压的我们解、我们压的 Node 解。
64
+
65
+ ### 2.2 由事实直接推出的三条结论
66
+
67
+ 1. **裸名检索 `node_modules` 是效果,不是运行时能力**。它只能由 OperitPackageManager 在打包期重写出来。
68
+ 2. **`node_modules` 必须打进 toolpkg**。sdcard 上的文件永远 `require` 不到。
69
+ 3. **漏重写的裸名是静默 `{}`,漏打包的路径是显式抛错**。两者失败模式不同,构建期要分别校验。
70
+
71
+ ### 2.3 Java bridge 的能力边界
72
+
73
+ 返回值经 `JsJavaBridgeDelegates.toJsonCompatibleValue` 转换:
74
+
75
+ | 返回值 | 转成 |
76
+ |---|---|
77
+ | 基本类型 / `Char` / `Enum` / `Class` | 直通(`Float`→`Double`,`Char`→`String`) |
78
+ | `Map` / `Iterable` / **数组(含 `byte[]`)** | 递归展开成 JSON |
79
+ | 其他对象 | **`{ __javaHandle, __javaClass }` 句柄** |
80
+
81
+ - **`byte[]` 会被逐元素展开成 JSON 数字数组** —— 1MB 文件会膨胀成 MB 级文本。
82
+ 所以 `Buffer` 与 `fs` 的二进制**不走 `byte[]`**,走字符串通道(见 §6.3)。
83
+ - 参数方向只接受 JSON 值,基础类型外一律强转。
84
+ - 长生命周期 Java 对象可以用句柄持有,因此 `stream` 这类是可行的。
85
+
86
+ ## 3. 契约
87
+
88
+ ### 3.1 一个内置名 = 一个子路径
89
+
90
+ `BUILTINS.json` 是唯一真源,OperitPackageManager 读它生成重写规则:
91
+
92
+ ```json
93
+ {
94
+ "distDir": "dist",
95
+ "subpathTemplate": "<distDir>/<subpath>.js",
96
+ "builtins": {
97
+ "path": { "subpath": "path", "status": "ready" },
98
+ "fs/promises": { "subpath": "fs/promises", "status": "planned" },
99
+ "vm": { "status": "unsupported", "reason": "..." }
100
+ }
101
+ }
102
+ ```
103
+
104
+ | status | 重写器应当 |
105
+ |---|---|
106
+ | `ready` | 映射到 `<distDir>/<subpath>.js` |
107
+ | `partial` | 可以映射,但要读 `note` 评估;偏差范围由测试守 |
108
+ | `planned` | **报构建错误**,不得静默映射 |
109
+ | `unsupported` | **报构建错误**(或按 `unsupportedEntry` 的调用形式输出) |
110
+
111
+ 子路径保持 Node 的目录结构,不压平:`fs/promises` → `dist/fs/promises.js`。
112
+ 这样 `node.operit/<内置名>` 与 Node 的内置名一一对应,映射规则可以机械化生成。
113
+
114
+ ### 3.2 点号包名规则(必须记住)
115
+
116
+ `buildCandidatePaths` 第一句:
117
+
118
+ ```js
119
+ if (/\.[a-z0-9]+$/i.test(normalized)) return [normalized];
120
+ ```
121
+
122
+ 包名里的 `.operit` 正好命中这个正则,于是:
123
+
124
+ - ❌ `require('.../node_modules/@serveryyswys/node.operit')` → 只试这一个候选 → 解析失败,**不会回落到 index.js**
125
+ - ✅ `require('.../node_modules/@serveryyswys/node.operit/dist/index.js')`
126
+ - ✅ `require('.../node_modules/@serveryyswys/node.operit/dist/path')` → 结尾不是 `.ext`,候选里有 `path.js`
127
+
128
+ **结论:重写输出一律显式带 `.js`,永不产出"目录名等价于入口"的形式。**
129
+
130
+ ### 3.3 加载链路
131
+
132
+ ```
133
+ opm init <project>
134
+ ├─ 生成项目骨架
135
+ └─ 下载 node.operit → <project>/node_modules/@serveryyswys/node.operit/
136
+ opm install <dep>
137
+ ├─ 下载 dep → <project>/node_modules/
138
+ ├─ 重写:裸名 → <rel>/node_modules/@serveryyswys/node.operit/dist/<subpath>.js
139
+ │ 解 main/exports、补扩展名、node_modules 上溯
140
+ └─ 打成 .toolpkg(java.util.zip 或 Tools.Files.zip)
141
+ 运行时
142
+ └─ 解压到 toolpkg_cache/<包名>/ → 原生相对 require 按路径直接读
143
+ ```
144
+
145
+ ## 4. 开发者规范
146
+
147
+ > **"把裸名重写成相对路径" 和 "把 `node_modules` 打进 toolpkg" 是同一件事的两半,缺一半就是废包。**
148
+
149
+ 1. `node_modules` 放**项目顶层**,不放 `dist/` 里(运行时不做任何上溯)
150
+ 2. 进 toolpkg 的必须是**运行期可达闭包**:`dependencies` 全量 + 嵌套 `node_modules`;
151
+ `devDependencies` / `src/` / `tsconfig.json` / 测试一律不进
152
+ —— 因为解包是无过滤全量 unzip,**打进去多少字节,每台设备就落多少字节**
153
+ 3. `package.json` / `package-lock.json` 一并打包(保留 npm 语义与私有源配置)
154
+ 4. **node.operit 不可被裁剪**
155
+ 5. 只要依赖碰到 Node 核心模块,就必须引入 node.operit
156
+
157
+ **建议 OperitPackageManager 做的**:重写时记下每个生成路径,打包前逐条验证在归档中存在,缺一条 fail build;
158
+ 并产出 `pack-manifest` 让体积成本可见。
159
+
160
+ ## 5. 项目布局
161
+
162
+ ```
163
+ <项目>/
164
+ manifest.json Operit 清单
165
+ dist/ Operit 规范的发行目录
166
+ src/ 开发目录
167
+ package.json
168
+ package-lock.json
169
+ tsconfig.json
170
+ node_modules/ ← 必须在顶层
171
+ node.operit/
172
+ <deps>/
173
+ ```
174
+
175
+ ## 6. host 层
176
+
177
+ ### 6.1 `lib/bridge.ts` —— 唯一接触 Java bridge 的地方
178
+
179
+ - 后端接口 `JavaBridgeBackend`,Operit 上的实现走 `NativeInterface.java*`,桌面测试注入 mock
180
+ - `unwrap()` 解 `{ success, data?, error?, message? }`;`success === false` 抛**中间态** Java 错误
181
+ (带 `code` 但还没有 `syscall` / `path`)
182
+ - `handleOf()` 把 `{ __javaHandle }` 或裸 handle 统一成字符串
183
+ - **它不知道 fs,也不知道路径语义** —— 那些属于上层
184
+
185
+ ### 6.2 `lib/errors.ts` —— 错误码的唯一翻译层
186
+
187
+ 每个错误对象必须带 `code`:三方库普遍靠 `code` 分支(`existsSync`、`mkdirp`、`tar`),
188
+ 没有 `code` 的实现等于跑不通。
189
+
190
+ Java 异常 → Node 错误码(**消息特征优先于类名精确表**,因为 `java.io.IOException` 是通用类):
191
+
192
+ | 线索 | code |
193
+ |---|---|
194
+ | 消息含 "No space left on device" | `ENOSPC` |
195
+ | 消息含 "Read-only file system" | `EROFS` |
196
+ | 消息含 "Too many open files" | `EMFILE` |
197
+ | 消息含 "Is a directory" / "Not a directory" | `EISDIR` / `ENOTDIR` |
198
+ | `FileNotFoundException` / `NoSuchFileException` | `ENOENT` |
199
+ | `AccessDeniedException` / `SecurityException` | `EACCES` |
200
+ | `FileAlreadyExistsException` | `EEXIST` |
201
+ | `NotDirectoryException` | `ENOTDIR` |
202
+ | `DirectoryNotEmptyException` | `ENOTEMPTY` |
203
+ | `FileSystemLoopException` | `ELOOP` |
204
+ | 其余 | `EIO` |
205
+
206
+ 库自身的状态用 `ONJ_*` 命名空间,不污染 POSIX 码空间。
207
+
208
+ ### 6.3 `lib/bytes.ts` —— 二进制通道的唯一开关
209
+
210
+ 因为 `byte[]` 会被展开成数字数组,二进制走字符串通道:
211
+
212
+ | 通道 | 体积 | 说明 |
213
+ |---|---|---|
214
+ | `base64` | 1.33x | 纯 ASCII,任何传输都安全 —— **默认** |
215
+ | `latin1` | 1.00x | 依赖 JSON 字符串保持码点不变 |
216
+
217
+ `encode()` / `decode()` 是唯一切口,`fs` 与 `Buffer` 只调它们。
218
+ QuickJS 不保证有 `TextEncoder` / `TextDecoder`,UTF-8 自行实现,非法序列按 U+FFFD 处理。
219
+
220
+ ### 6.4 `lib/paths.ts` 与 `lib/cwd.ts`
221
+
222
+ `paths` 集中了所有"路径根从哪来"的知识,**分成公开面与内部面**:
223
+
224
+ - 公开面(Operit 的 `types/index.d.ts` 已声明):`getPluginConfigDir`、`OPERIT_DOWNLOAD_DIR`、`OPERIT_CLEAN_ON_EXIT_DIR`
225
+ - 内部面(源码里真实存在、d.ts 未声明):`getPluginConfigDir('')` 会回落到当前 call 的包名 —— `packageName()` 依赖它
226
+
227
+ `cwd` 独立成模块,是为了打破 `path` ↔ `fs` 的循环依赖:`path` 只读,`fs` / `process` 只写。
228
+
229
+ ## 7. `fs` 环境状态机(设计)
230
+
231
+ ### 7.1 模型
232
+
233
+ **环境 = 物理根 + cwd + 可用能力**。`process.cwd()` 跟着环境走,相对路径的解析基准也就跟着走 ——
234
+ 状态机不是给 `fs` 打的补丁,而是路径语义的唯一来源。
235
+
236
+ ### 7.2 状态与事件
237
+
238
+ ```
239
+ UNBOUND ──首次使用──▶ BOUND(sdcard) # 默认
240
+ BOUND(A) ──use(B)──▶ BOUND(B)
241
+ BOUND(A) ──with(B, fn)──▶ BOUND(B) ──fn 同步返回──▶ BOUND(A)
242
+ BOUND(*) ──reset()──▶ UNBOUND
243
+ ```
244
+
245
+ | 事件 | 守卫 | 失败码 |
246
+ |---|---|---|
247
+ | `use(name)` | 未注册 → 拒绝 | `ONJ_UNKNOWN_ENV` |
248
+ | `use(name)` | 根不存在 / 无权限 / 能力探测失败 → 拒绝 | `ONJ_ENV_UNAVAILABLE` |
249
+ | `with(name, fn)` | `fn` 返回 thenable → **拒绝** | `ONJ_ENV_ASYNC_SCOPE` |
250
+ | `define(name, spec)` | 不许覆盖内置环境名 | `ONJ_UNKNOWN_ENV` |
251
+ | `reset()` | 无 | |
252
+
253
+ `with()` 的同步约束是最重要的一条红线:否则 `await` 期间环境会被别的代码看到。
254
+
255
+ ### 7.3 环境清单(初版)
256
+
257
+ | 名 | 根 | 来源 |
258
+ |---|---|---|
259
+ | `sdcard` | `/sdcard` | 默认 |
260
+ | `config` | `getPluginConfigDir()` | 公开全局 |
261
+ | `app` | `getFilesDir()` | bridge + `javaGetApplicationContext` |
262
+
263
+ ### 7.4 跨调用持久化(必须写进文档)
264
+
265
+ 模块实例缓存按 `packageTarget` 复用(§2.1),所以 **`fs` 的环境状态会跨工具调用保留**。
266
+ 这不是 bug,但会让"上一次调用把环境设成 linux、这一次忘了设"变成静默的行为漂移。
267
+ 对策:`reset()` 可用;文档明确要求长生命周期代码用 `with()` 而不是 `use()`。
268
+
269
+ ### 7.5 形态取舍
270
+
271
+ - `fs` 的 Node 方法签名保持原样,环境作为**独立的 `fs.env` 命名空间** —— 读 `fs` 的代码不会看到多余参数
272
+ - 另给 `createFs(env)` 工厂作为逃生口,应付同时操作两个环境的场景
273
+ - 默认实例仍是全局单例,因为三方库只会调全局 `fs`
274
+ - `..` 逃逸出根目录默认禁止,可逐环境放开
275
+
276
+ ## 8. 模块清单与后端
277
+
278
+ | 档 | 内容 | 后端 |
279
+ |---|---|---|
280
+ | **纯 JS** | `path`、`querystring`、`string_decoder`、`events`、`assert`、`util`、`url`、`timers`、`perf_hooks`、`process`(骨架) | 无宿主依赖,本地可测 |
281
+ | **Java bridge** | `fs`、`os`、`crypto`、`zlib`、`child_process`、`dns`、`net`、`http` | `java.io/nio/security/util.zip/net`、`android.os.Build` |
282
+ | **可复用内建** | `zlib` ← pako、`crypto` 部分 ← CryptoJS、`http` ← axios | 内建裸模块 |
283
+ | **不作** | `vm`、`worker_threads`、`cluster`、`async_hooks`、`inspector`、`repl` | 见 `unsupported.js` |
284
+
285
+ `Buffer` 是隐性阻塞项:它决定 `string_decoder` / `stream` / `fs` / `crypto` 能不能做对,
286
+ 而它完全取决于 §6.3 的通道选择。
287
+
288
+ ## 9. 测试策略
289
+
290
+ ### 一条反直觉的发现:深比较比的是 constructor
291
+
292
+ `Object.getPrototypeOf` 看起来才该是深比较的依据,但实测 Node 不是这么做的:
293
+
294
+ | 比较 | 结果 |
295
+ | --- | --- |
296
+ | `Object.create({i:1})` + own 与 `{}` | **相等**(原型不同,constructor 都是 Object) |
297
+ | `Object.create(null)` 与 `{}` | 不等(前者没有 constructor) |
298
+ | `new A()` 与 `new B()` | 不等 |
299
+ | `{__proto__: Array.prototype}` 与 `[]` | 不等(constructor 相同,但有数组类型闸) |
300
+
301
+ 所以规则是「**沿原型链取 constructor 比较**」外加「数组与非数组永不相等」,
302
+ 再单独补一条数组长度检查(`[]` 与 `new Array(2)` 的自有键都是空的,但不等)。
303
+
304
+ 这个点如果按直觉写,会在 `Object.create` 与无原型对象上静默给出相反答案 ——
305
+ 所以 `lib/deep-equal.ts` 用 4225 对语料(含循环引用、Map/Set、TypedArray、符号键、±0、NaN、
306
+ 稀疏数组、无原型对象)与 `util.isDeepStrictEqual` 逐对比对。
307
+
308
+ ### 另一条反直觉的发现:旧版 url.parse 按「协议是否 slashed」分派
309
+
310
+ `url.parse` 是手写状态机,规则不统一,实测出来的几条:
311
+
312
+ | 输入 | 结果 |
313
+ | --- | --- |
314
+ | `http:example.com` | **不当主机**(http 是 slashed 协议,缺 `//` 就不认),pathname = `example.com` |
315
+ | `a:b` | **当主机**(a 不是 slashed 协议),host = `b` |
316
+ | `mailto:someone@example.com` | 当主机,auth = `someone`,host = `example.com` |
317
+ | `foo://bar` | 有 `//` 就当主机,但 pathname 不补 `/`(foo 不是 slashed 协议) |
318
+ | `file:host` | href = `file://host` —— `file:` **无条件**带 `//`,其它 slashed 协议要有主机才带 |
319
+ | `parse('http://x', true).query` | 没有查询串时是**空对象**(无原型),不是 `null` |
320
+
321
+ 这些如果按"直觉上更合理"的方式写,会在 `a:b`、`file:host` 这类输入上静默给出不同结果。
322
+ `src/url.ts` 里每条都标了出处,测试用 40 条语料与 Node 逐字段对拍(parse 字段、query 对象、
323
+ slashesDenoteHost、format、format(parse(x)) 往返、resolve)。
324
+
325
+ ### OperitPackageManager 打包期要做什么(实测得出)
326
+
327
+ 这一节是从"真装一个 npm 包跑通"的过程里逼出来的,是交给 OperitPackageManager 的施工图。
328
+
329
+ **根本前提**:Operit 的 `require` **没有 node_modules 查找**。
330
+ `resolveModulePath` 对不以 `.` 或 `/` 开头的请求原样返回,
331
+ `requireInternal` 对 `lodash`/`uuid`/`axios` 之外的一律 `return {}` —— **静默**。
332
+ 所以打包期必须把所有裸名都变成相对路径,**不只是 Node 内建**:
333
+
334
+ | 情况 | 处理 |
335
+ | --- | --- |
336
+ | Node 内建名 | → `node_modules/@serveryyswys/node.operit/dist/<subpath>.js` |
337
+ | npm 包名 | → 该包入口文件(`main`/`exports` 必须在打包期解析) |
338
+ | `lodash` / `uuid` / `axios` | 保持原样(Operit 自带) |
339
+ | 相对路径的 `.json` | 换成包装模块 `module.exports = <json>` |
340
+ | 契约表里 planned / unsupported | **构建失败**,不能留到运行期变 `{}` |
341
+
342
+ 另外三条同样是踩出来的:
343
+
344
+ - **扫描 require 要跳过注释与字符串**。用正则扫会把 node.operit 自己文档注释里的
345
+ `require('<项目>/node_modules/@serveryyswys/node.operit/index.js')` 当成真依赖。
346
+ - **只处理从入口可达的文件**。npm 包里常有 `tests/` 引用没装的 devDependency
347
+ (`safer-buffer/tests.js` 就 require `tape`),不可达的文件不该让构建失败。
348
+ - **始终写显式 `.js`**。`buildCandidatePaths` 用 `/\.[a-z0-9]+$/i` 判断有无扩展名,
349
+ 而 `node.operit` 这个名字自带一个点。
350
+
351
+ 这些规则连同验证方式都写进了 `BUILTINS.json` 的 `packing` 段。
352
+
353
+ ### 一条设计判断:http 为什么建在工具之上
354
+
355
+ 实现 `http` 有两条路:直接用 Java 的 socket / `HttpURLConnection`,或者走宿主的
356
+ `toolCall('http_request')`。
357
+
358
+ 选了后者。理由不是技术难度,而是**权限模型**:`http_request` 是 Operit 注册的工具
359
+ (`ToolRegistration.kt:1967`),走用户的工具审批流程。用裸 socket 另起一套,
360
+ 插件就能绕过用户"是否允许联网"的决定 —— 那是库作者不该替用户做的选择。
361
+
362
+ 于是 `http.get` / `http.request` 保持 Node 的 API 形状,底下发的是同一个工具调用。
363
+ 代价写在契约表里:请求体只能是文本、`abort()` 取消不掉已经在宿主侧跑的请求、
364
+ 没有连接池与 HTTP/2。服务端(`createServer`)则直接显式抛 `ONJ_UNSUPPORTED`。
365
+
366
+ ### 一次自我印证的错误(JSON)
367
+
368
+ 我在文档里写过"`.json` 的 require 会被静默吃掉,因为 Operit 把文本直接交给 `new Function`"。
369
+ **这是错的**,而错的方式值得记下来:
370
+
371
+ 1. 读 `JsExecutionScriptBuilder.kt` 时**漏看**了 `executeModule` 里的 `.json` 分支;
372
+ 2. 据此写运行时模拟器 —— 让 JSON 走 `new Function`,导出自然是 `{}`;
373
+ 3. 模拟器"复现"了我以为的现象,于是我把结论写进 README 和 BUILTINS;
374
+ 4. 此后每一轮集成验证都"印证"它,因为验证跑的就是那个模拟器。
375
+
376
+ **测试无法证伪一个与它同源的错误假设。** 整条链条里没有一个独立信源。
377
+
378
+ 后来是用户在真机报告里把这条**当成"来自 README"引用**,我才回去逐行核对源码,
379
+ 发现 :1231 明明有 `JSON.parse`。修正后直接验证:`require('./data.json')` 得到 `[1,2,3]`。
380
+
381
+ 教训:引用源码行号时必须贴出那几行,而不是凭印象写结论;
382
+ 凡是"运行时会怎样"的断言,都要有一个**不依赖我自己实现**的核对动作。
383
+
384
+ ### 运行时的模块解析语义(真机核实)
385
+
386
+ 真机报告(单文件 `.js` 沙盒包探针)确认了几条此前只从源码推断的事实,
387
+ 其中两条改变了打包建议的表述:
388
+
389
+ - **`require` 只读 toolpkg 归档**(`readToolPkgTextResource(toolPkgId, path)`,:675),
390
+ **完全不碰文件系统**。所以把 `node_modules` 铺在 `/sdcard` 上,
391
+ 运行期永远 require 不到 —— 这与"有没有重写裸名"是两件不相干的事。
392
+ 是"没进归档"先失败,不是"没重写"先失败。
393
+ - **单文件 `.js` 沙盒包不是合法宿主**:`screenPath` 为空、又没有归档资源,
394
+ 连包自己旁边的 `./x.js` 都解析不到(真机实测全部 `Cannot resolve … from "<root>"`)。
395
+
396
+ 完整的语义表(含源码行号)在 README 的「运行时的模块解析语义」一节,
397
+ 机器可读版本在 `BUILTINS.json` 的 `runtimeResolution`。
398
+
399
+ ### 一条实测出来的契约要求:process / Buffer 要注入成全局
400
+
401
+ 第一版 README 写的是"OperitPackageManager 在入口文件注入 `var process = require(...)`"。扩展集成测试
402
+ (加了 `debug` / `supports-color` 之后)证明这不够:
403
+
404
+ ```
405
+ 无 process 全局: supports-color require 时抛 "process is not defined"
406
+ 注入 process 全局: supportsColorKeys=["stderr","stdout","supportsColor"], stdout=false ← 与 Node 完全一致
407
+ ```
408
+
409
+ 真实包探测的是**全局**(`const {env} = process`、`typeof process === 'undefined'`),
410
+ 而不是某个模块作用域里的变量。所以契约改成:把 `process`(与 `Buffer`)挂到全局上。
411
+
412
+ 这一条是"只对拍 API"永远发现不了的:API 层面 `require('process')` 完全正确,
413
+ 问题出在别人怎么用它。
414
+
415
+ ### 一条刻意的取舍:net 不做
416
+
417
+ `net` 需要裸 TCP/UDP socket。技术上用 `java.net.Socket` 能实现,但**没有做**。
418
+
419
+ 理由与 http 那条一致:网络访问在 Operit 里是**工具能力**(走用户审批),
420
+ 用裸 socket 另起一套等于绕过审批模型。`http`/`https` 已经建在 `toolCall('http_request')`
421
+ 之上,是"既能用 Node API、又不改变权限语义"的做法;`net` 没有对应的工具可挂,
422
+ 所以宁可明确标为不支持。
423
+
424
+ 因此 `BUILTINS.json` 的 `unsupported` 语义被明确成两类原因:平台不成立,与刻意不做。
425
+
426
+ ### 一条平台判断:dns 只做 lookup
427
+
428
+ `dns.resolveMx` / `resolveTxt` 这些要做 DNS **记录级**查询。Java 侧对应的能力是 JNDI 的
429
+ DNS provider(`com.sun.jndi.dns`),而它不是 Android 平台的一部分。
430
+
431
+ 所以 `dns` 只实现 `lookup` 一族(getaddrinfo,也就是绝大多数代码真正用的那部分),
432
+ `resolve*` 与 `reverse` **显式抛 `ONJ_UNSUPPORTED`** 并说明原因。
433
+ 这与 `net` / `http.createServer` 的处理一致:平台不成立的能力给明确拒绝,
434
+ 不给一个"能调用但结果不对"的空壳。
435
+
436
+ ### 一条踩过的坑:假绿
437
+
438
+ 测试入口是 async 的,如果某个场景的 Promise 永不 resolve,事件循环会变空,
439
+ **Node 会静默退 0**,看起来像"全部通过"。这个坑真实发生过一次
440
+ (`Transform` 漏调 `_final`,导致 `end` 永不触发)。
441
+
442
+ 现在有两道防线:
443
+
444
+ 1. **看门狗**:单模块超过 30s 未完成即置失败码;
445
+ 2. **汇总落盘**:`test/last-run.txt` 记录最终计数与失败详情,
446
+ 因为长输出在管道里可能被截断,stdout 不一定能看到结论。
447
+
448
+ 除此之外,每个模块完成后会打印自己的断言条数 —— 数量异常下降本身就是信号。
449
+
450
+ ### 开发机的沙箱限制
451
+
452
+ 本机(DSH 沙箱)**禁止带管道 stdio 起子进程**,Node 的 `spawnSync`/`exec` 会直接 `EPERM`。
453
+ 因此 `child_process` 的测试不能用"真跑命令"的驱动,改成**契约测试**:
454
+
455
+ - mock 驱动记录收到的 argv,断言**交给宿主的是不是 shell + 单参数**(这正是与 Java bridge 的契约);
456
+ - Node 语义层(退出码、错误码、maxBuffer、回调参数、事件与流)用固定返回值断言。
457
+
458
+ 换句话说:能验证的是"我们交给宿主什么"和"我们怎么解释结果",
459
+ **真正起进程这一步只能在真机上验证**。
460
+
461
+ - **本地 Node 对拍**:纯逻辑模块直接与 Node 自身实现比对。
462
+ `path` 的 posix 部分是**全量对拍、一字不差**;win32 是真实场景硬断言 + 全量分歧普查(防扩大)
463
+ - **bridge mock**:`bridge.setBackend()` 注入桌面实现,使 `fs` 这类宿主模块也能本地测
464
+ - **真机验证**:Java bridge 的实际返回形状、`getPluginConfigDir('')` 的回落行为、
465
+ `new Function` / `eval` 的可用性、二进制通道的实测开销 —— 这些必须在 Operit 上跑一次
466
+
467
+ ## 10. 进度
468
+
469
+ 已完成并通过本机对拍:
470
+
471
+ - [x] `path` / `path/posix`(posix 与 Node 全量对拍一字不差)
472
+ - [x] `path/win32`(partial,分歧率 1.45%,见 README)
473
+ - [x] `Buffer`(含 Node 的 base64 容错规则、ascii 编解码不对称行为)
474
+ - [x] `fs` 环境状态机 + 同步面 + `fs/promises`
475
+ (Node 语义层与本机 Node 的 `fs` 逐项对拍通过;Java 驱动待真机验证)
476
+ - [x] `events` / `querystring` / `string_decoder`(与 Node 对拍通过)
477
+ - [x] `util`(partial:`inspect` 的折行算法未照搬)
478
+
479
+ - [x] `os` / `process`(partial:取值来自 Android,用 mock backend 钉死 Java 侧契约)
480
+ - [x] `crypto`(partial:6 个哈希算法 + HMAC + PBKDF2 与 Node 全量对拍;分组密码/签名未实现)
481
+ - [x] `zlib`(partial:完整 DEFLATE 解压 + 定长 Huffman 压缩,与 Node 双向对拍)
482
+ - [x] `stream` / `stream/promises`(partial:9 个场景与 Node 对拍一致,并解锁了 zlib 的流式接口)
483
+ - [x] `fs.createReadStream` / `fs.createWriteStream`(与 Node 对拍;读流无 fd,因此也不是真流式内存)
484
+ - [x] `child_process`(partial:exec/execFile/spawn 及其同步版、错误形态与事件已实现;
485
+ 同步宿主决定了它没有真并发)
486
+ - [x] fd 系列 + 回调形式 + `fs/promises.FileHandle`(与 Node 对拍)
487
+ - [x] `assert` / `assert/strict`(+ 共享的 `lib/deep-equal.ts`,语料零分歧)
488
+ - [x] `url`:WHATWG `URL`(含 setter 与 searchParams 活视图)+ `URLSearchParams` +
489
+ 旧版 `parse`/`format`/`resolve` + file URL + punycode
490
+ - [x] `timers` / `timers/promises` / `perf_hooks` / `constants` / `module` / `tty`
491
+ - [x] 端到端集成验证(真实 npm 包 + 忠实复刻的运行时模拟器 + 与 Node 差分比对)
492
+ - [x] `os` / `process` / `util` / `events` / `querystring` / `string_decoder` / `crypto` /
493
+ `zlib` / `stream` / `buffer` / `path`
494
+
495
+ ### 契约里仍未实现的条目
496
+
497
+ `BUILTINS.json` 中标为 planned 的还有这些,按建议顺序:
498
+
499
+ 1. **需要宿主**:`net` / `http` / `https` / `dns`(可基于 Java Socket / HttpURLConnection)
500
+ 2. **明确不支持**:`vm` / `worker_threads` / `cluster` / `async_hooks` / `inspector` / `repl`
501
+ 3. **意义有限**:`readline`(沙箱没有真实 stdin)
502
+
503
+ ### 只剩真机验证
504
+
505
+ 模块面已经铺满契约中"可实现"的部分。剩下的收尾是 §9 第三条那份清单:
506
+ bridge 的实际返回形状、`getPluginConfigDir('')` 的回落行为、`new Function` / `eval` 的可用性、
507
+ 二进制通道(base64 vs latin1)的实测开销、`ProcessBuilder` 与 `RandomAccessFile` 的可用性。
508
+
509
+ 这些都必须跑在 Operit 上,桌面测不了。
510
+ - [ ] `crypto` / `zlib` / `stream` / `child_process`
511
+ - [ ] fd 系列(`open`/`read`/`write`/`close`)与流式 API
512
+ - [ ] 真机验证清单(§9 第三条):bridge 返回形状、`getPluginConfigDir('')` 回落、`new Function` 可用性、二进制通道实测开销
513
+ - [ ] 二进制通道实测后定稿(`base64` vs `latin1`)