docs: 四文档同步 0.1.36——CSP 红线纪律

AGENTS:版本头 0.1.36(三问题处置/arith 根因)/测试 7011/新增 CSP 红线(严禁 Function()/eval——一律 arith)/
打包版≠开发版(CSP 分野)环境坑 + dev:web 浏览器通道说明(Tabbit 劫持端口用 WSL IP 绕过)
README:测试数 102/7011 + 版本沿革 0.1.36 行
MOD_GUIDE:版本头/尾注 0.1.36 + produceExpr 改用 arith 求值说明(表达式无脚本化能力)
MEMORY:时间线 0.1.36 + 测试数 + 新增 8e 决策(CSP 血案全纪录/军规/真实错误文字教训)
This commit is contained in:
2026-08-24 22:38:37 +08:00
parent 41cffdfdb6
commit 2984d05647
3 changed files with 15 additions and 6 deletions
+3
View File
@@ -31,6 +31,8 @@ SMOKE_TEST=1 SMOKE_SHOTS_DIR=/tmp/opencode/shots npx electron ... # 附
- **`npm rebuild electron esbuild` 会删掉 `node_modules/electron/dist/electron`**Linux 二进制),修复: - **`npm rebuild electron esbuild` 会删掉 `node_modules/electron/dist/electron`**Linux 二进制),修复:
`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node node_modules/electron/install.js` `ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node node_modules/electron/install.js`
- 私有 registry 的 `_auth` token 只在用户级 `~/.npmrc`;项目 `.npmrc` 只保留 registry 映射,不要把 token 写进仓库。 - 私有 registry 的 `_auth` token 只在用户级 `~/.npmrc`;项目 `.npmrc` 只保留 registry 映射,不要把 token 写进仓库。
- **打包版≠开发版(CSP 分野)**devvite server 无 CSP)能跑不代表打包版能跑——CSP 性 bug(如 0.1.36 unsafe-eval)只出现在 out/;改完务必 build +(可能的话)真实 GUI 冒烟。
- **WSL 无显示环境调试**`npm run dev:web`vite.web.config.ts,端口 5179 + host 0.0.0.0)纯浏览器通道——Windows 浏览器开 `http://<WSL_IP>:5179`window.api(导出/导入/MOD 扫描)缺失自动跳过,其余全功能等价(存档走浏览器 OPFS)。**注意**:部分浏览器(如 TabbitDance 后台 node)会劫持 localhost 端口段(5173/5179 全 502)——用 WSL 网卡 IP 绕过。
- Linux 无 emoji 字体:**所有图标一律用汉字印章字符**renderer/ui/styles.css 的 `.s-icon/.res-icon/.bld-icon`),不要引入 emoji。 - Linux 无 emoji 字体:**所有图标一律用汉字印章字符**renderer/ui/styles.css 的 `.s-icon/.res-icon/.bld-icon`),不要引入 emoji。
## 架构(0.1.14 大重构后,目录与路径以此为准) ## 架构(0.1.14 大重构后,目录与路径以此为准)
@@ -57,6 +59,7 @@ src/main/ Electron 主进程(app:// 协议 + IPC 存
- **统一随机**:引擎只经 `World.rng`UI 播种用 `RngHub.rollSeed()`、音效白噪用 `RngHub.audioNoise01()`**不要**在逻辑里引入 Math.random()(引擎域 0 处)。渲染层 canvas 粒子散布的 Math.random 不进引擎状态,可留。 - **统一随机**:引擎只经 `World.rng`UI 播种用 `RngHub.rollSeed()`、音效白噪用 `RngHub.audioNoise01()`**不要**在逻辑里引入 Math.random()(引擎域 0 处)。渲染层 canvas 粒子散布的 Math.random 不进引擎状态,可留。
- **引擎单线程定论**(0.1.21 实测):`advanceMonth` 2160 月平均 **0.05ms/tick**——无 Worker 化必要;拖帧热点在 UI 渲染与存档(都已在 0.1.20/0.1.21 修)。 - **引擎单线程定论**(0.1.21 实测):`advanceMonth` 2160 月平均 **0.05ms/tick**——无 Worker 化必要;拖帧热点在 UI 渲染与存档(都已在 0.1.20/0.1.21 修)。
- **事件闸**:pending 事件唯一写入入口是 `events.ts``fire(w, id, priority)`(0=普通遇占放弃 / 1=高优 raid·渡劫·大比可顶替被占者,被顶替者入 `eventQueue` 下月兑现不丢失)。**禁止直写 `state.pendingEvent`**(渡劫/破境丹均已改走 fire)。`applyEventChoice` 前置校验 `pendingEvent===id`(陈旧 Modal 二次结算防线)。 - **事件闸**:pending 事件唯一写入入口是 `events.ts``fire(w, id, priority)`(0=普通遇占放弃 / 1=高优 raid·渡劫·大比可顶替被占者,被顶替者入 `eventQueue` 下月兑现不丢失)。**禁止直写 `state.pendingEvent`**(渡劫/破境丹均已改走 fire)。`applyEventChoice` 前置校验 `pendingEvent===id`(陈旧 Modal 二次结算防线)。
- **禁 eval/字符串求值(0.1.36 CSP 红线)**:打包 renderer 的 CSP 禁 unsafe-eval——**严禁 `Function()`/`eval()` 求值表达式**(业务字符串一律走 `data/arith.ts` 纯解析器:数字/L/四则/括号/幂/负号白名单,非法 NaN 不抛;bonusOf/produceExpr/MOD 表达式已全量替换)。开发模式无 CSP 常掩盖此问题——凡是新表达式求值必须用 arith。
- **特效/音效语义出口**`WorldEventBus.onFx?(em: FxEmit)` + `World.emitFx(kind, source?)`(零 rng 消耗);UI 的 `makeBus` 桥接 FxGate`onLog kind` 音效映射保留作兜底。 - **特效/音效语义出口**`WorldEventBus.onFx?(em: FxEmit)` + `World.emitFx(kind, source?)`(零 rng 消耗);UI 的 `makeBus` 桥接 FxGate`onLog kind` 音效映射保留作兜底。
- **插件架构**0.1.8 起,路径已迁 engine/runtime|kernel):`kernel/plugin.ts` 协议、`runtime/pluginManager.ts` 安装管线(依赖校验/异常回滚/追踪型上下文——注销时钩子全摘)、`plugin-bootstrap.ts` 三核心插件(core-systems/core-data/core-events protected)。事件多池 `world.eventPools`**findEvent 必须带 world 参数查池**。能力卡 12 张经 `viaCap` 注册;tournament/tribulation/apprentice/season 四卡各自内部检查 `w.sysEnabled`(无独立钩子)。 - **插件架构**0.1.8 起,路径已迁 engine/runtime|kernel):`kernel/plugin.ts` 协议、`runtime/pluginManager.ts` 安装管线(依赖校验/异常回滚/追踪型上下文——注销时钩子全摘)、`plugin-bootstrap.ts` 三核心插件(core-systems/core-data/core-events protected)。事件多池 `world.eventPools`**findEvent 必须带 world 参数查池**。能力卡 12 张经 `viaCap` 注册;tournament/tribulation/apprentice/season 四卡各自内部检查 `w.sysEnabled`(无独立钩子)。
- **MOD 红线(0.1.26 规范)**MOD 只含 JSON 零脚本(`docs/MOD_GUIDE.md`);**禁止新增 rng 消耗**(全局随机序列);不改核心时序(扩展走五口:事件池/模板/词库/灾因/配方);`modPreflight`+`validateMod` 先行(id 冲突用 `findEvent` 权威检测);MOD 只影响**新开档**世界(老档世界变形成档记录);MOD 状态随 `state.plugins` 持久化(对应 `worldGen.npcs` 落档保证读档永稳)。 - **MOD 红线(0.1.26 规范)**MOD 只含 JSON 零脚本(`docs/MOD_GUIDE.md`);**禁止新增 rng 消耗**(全局随机序列);不改核心时序(扩展走五口:事件池/模板/词库/灾因/配方);`modPreflight`+`validateMod` 先行(id 冲突用 `findEvent` 权威检测);MOD 只影响**新开档**世界(老档世界变形成档记录);MOD 状态随 `state.plugins` 持久化(对应 `worldGen.npcs` 落档保证读档永稳)。
+4 -4
View File
@@ -1,6 +1,6 @@
# MOD 开发教程 · Chronicle of the Immortal Clan # MOD 开发教程 · Chronicle of the Immortal Clan
> 版本 **0.1.35·《涤荡·向新》** · MOD_SCHEMA_VERSION = 1 · 全口纯 JSON(零脚本) > 版本 **0.1.36·《CSP 根因》** · MOD_SCHEMA_VERSION = 1 · 全口纯 JSON(零脚本)
> **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点、示例都写在下面。 > **本教程即全部所需**:你不需要读任何源码或类型文件——所有 MOD 能用的字段、取值、默认值、坑点、示例都写在下面。
> 照例动手即可:最快 5 分钟做出你的第一个 MOD。 > 照例动手即可:最快 5 分钟做出你的第一个 MOD。
@@ -650,7 +650,7 @@ macOS: ~/Library/Application Support/Electron/mods/
"buildings": [ "buildings": [
{ "id": "b-chalu", "name": "茶园", "icon": "茗", "desc": "清茶待客,悦人悦己。", "kind": "produce", { "id": "b-chalu", "name": "茶园", "icon": "茗", "desc": "清茶待客,悦人悦己。", "kind": "produce",
"maxLevel": 3, "maxLevel": 3,
"produceExpr": { "lingcao": "4 * L" }, // L=等级——月产表达式(支持 + - * / 与 ^) "produceExpr": { "lingcao": "4 * L" }, // L=等级——月产表达式(支持 + - * / ( ) 与 ^
"upgradeStones": 60, "upgradeMineral": 8 } // 升级费=基价 ×1.6^(L-1) / 灵矿×L "upgradeStones": 60, "upgradeMineral": 8 } // 升级费=基价 ×1.6^(L-1) / 灵矿×L
] ]
``` ```
@@ -661,7 +661,7 @@ macOS: ~/Library/Application Support/Electron/mods/
| `name` / `icon` / `desc` | ✅ | 显示(icon 单字汉字符号) | | `name` / `icon` / `desc` | ✅ | 显示(icon 单字汉字符号) |
| `kind` | ✅ | `produce`(月产)\| `function`(加成——function 型建议少碰,数据面以 produceExpr 为主) | | `kind` | ✅ | `produce`(月产)\| `function`(加成——function 型建议少碰,数据面以 produceExpr 为主) |
| `maxLevel` | ✅ | 最高等级 | | `maxLevel` | ✅ | 最高等级 |
| `produceExpr` | produce 必填 | 月产表达式:`{ 资源id: "公式" }`——公式中 `L`=当前等级;数学符号白名单+ - * / 与 ^,其它忽略) | | `produceExpr` | produce 必填 | 月产表达式:`{ 资源id: "公式" }`——公式中 `L`=当前等级;数学符号(+ - * / ( ) ^)——**0.1.36 起由纯解析器 arith 求值(CSP 禁 eval——表达式无脚本化能力,字母/函数名一律视为非法),引擎内置与 MOD 表达式同一求值器** |
| `upgradeStones` | ✅ | 升级灵石基价(1.6^(L-1) 递增) | | `upgradeStones` | ✅ | 升级灵石基价(1.6^(L-1) 递增) |
| `upgradeMineral` | ✅ | 升级灵矿(×L | | `upgradeMineral` | ✅ | 升级灵矿(×L |
@@ -950,4 +950,4 @@ macOS: ~/Library/Application Support/Electron/mods/
> 0.1.34 功法注:四流派效果 crit/guard/trib/secret 全 optional——缺省零行为,金钟罩安全;元素时节共鸣 4 季同频(土系恒应)。 > 0.1.34 功法注:四流派效果 crit/guard/trib/secret 全 optional——缺省零行为,金钟罩安全;元素时节共鸣 4 季同频(土系恒应)。
> 0.1.35 涤荡注:无存档迁移(游戏未发布——旧档随版本演进失效属预期);`patch` 层按插件归属回滚;validate 覆盖 §12 全部规则。 > 0.1.35 涤荡注:无存档迁移(游戏未发布——旧档随版本演进失效属预期);`patch` 层按插件归属回滚;validate 覆盖 §12 全部规则。
<sub>⚔️ 本文档随游戏版本更新(0.1.35)。字段若有新增,此处先行。</sub> <sub>⚔️ 本文档随游戏版本更新(0.1.36)。字段若有新增,此处先行。</sub>
+8 -2
View File
@@ -49,6 +49,7 @@
| 0.1.33 | 闭环·流光 | 物品生态闭环(符箓开战生效/僵尸物品归位/灾年入产——A1-A3 高危断链修复);引擎止血 15 项(tribBoost 清零/assistAlly 先校验/回声空池守卫/版本透传/noAutoCreate 等);UI 动效打磨(Modal 淡入/DriftLayer 节点清理/季节色对月/rAF 停帧节能);**测试 5009→5230** | | 0.1.33 | 闭环·流光 | 物品生态闭环(符箓开战生效/僵尸物品归位/灾年入产——A1-A3 高危断链修复);引擎止血 15 项(tribBoost 清零/assistAlly 先校验/回声空池守卫/版本透传/noAutoCreate 等);UI 动效打磨(Modal 淡入/DriftLayer 节点清理/季节色对月/rAF 停帧节能);**测试 5009→5230** |
| 0.1.34 | 万象归元 | 功法流派化(crit/guard/trib/secret 四效果 ×6 新功 ×元素时节共鸣)+ 世界自演化二层(warFatigue 温度冷降/换代恩义半衰/断供应急×1.5/新秘境种子派生);插件卡 12→14worldsim 观察/tech 功法);MOD 功法效果口+patch 数值层(0.5~2 mult 校验/卸载复原);**测试 5230→6000** | | 0.1.34 | 万象归元 | 功法流派化(crit/guard/trib/secret 四效果 ×6 新功 ×元素时节共鸣)+ 世界自演化二层(warFatigue 温度冷降/换代恩义半衰/断供应急×1.5/新秘境种子派生);插件卡 12→14worldsim 观察/tech 功法);MOD 功法效果口+patch 数值层(0.5~2 mult 校验/卸载复原);**测试 5230→6000** |
| 0.1.35 | 荡涤·向新 | 全量审计 17 缺陷歼灭(wonder 全链/boss 释放/patch 归属/拒单门/衰减/transition);**兼容层整体铲除**migrate 迁移链 55 行+版本戳 8 处+chronicle 死表+__state/seed 三元+normalize 兼容批 55 行(未发布不向下兼容——validateLoadedState 结构校验替代)+domain 5 兼容 optional 收紧;**测试 6000→7000** | | 0.1.35 | 荡涤·向新 | 全量审计 17 缺陷歼灭(wonder 全链/boss 释放/patch 归属/拒单门/衰减/transition);**兼容层整体铲除**migrate 迁移链 55 行+版本戳 8 处+chronicle 死表+__state/seed 三元+normalize 兼容批 55 行(未发布不向下兼容——validateLoadedState 结构校验替代)+domain 5 兼容 optional 收紧;**测试 6000→7000** |
| 0.1.36 | CSP 根因 | 三问题处置:成员弹框双排名(壳/内容双渲染已修);**推进崩溃+定制白屏同一根因——打包 renderer CSP 禁 unsafe-evalbonusOf/produceExpr/MOD 表达式三处 Function() 求值全被拦**→ 新建 arith.ts 纯解析器(令牌化+递归下降)全量替换(零 eval);toast 根错可见+出险双保险;ErrorBoundary 防白屏;dev:web 浏览器通道;存储真链测试(MetonaSqlark memory 方言);**测试 7007→7011** |
## 三、关键架构决策(及为什么) ## 三、关键架构决策(及为什么)
@@ -90,6 +91,11 @@
- **新物产入池即活**:仅定义物品不够——只有进了 POOL_BASE/MARKET_IDS 才参与世界呼吸/NPC 贸易/灾年超卖(0.1.32 四同步(四新增键入池)是关键,否则"只躺仓库")。 - **新物产入池即活**:仅定义物品不够——只有进了 POOL_BASE/MARKET_IDS 才参与世界呼吸/NPC 贸易/灾年超卖(0.1.32 四同步(四新增键入池)是关键,否则"只躺仓库")。
- **消耗类物品的纪律**:符箓/灵酿效果必须**零 rng 消耗**(自动消耗实现——金钟罩安全);新增效果尽量走"已有结算点"(渡劫/战斗结束时挂钩)。 - **消耗类物品的纪律**:符箓/灵酿效果必须**零 rng 消耗**(自动消耗实现——金钟罩安全);新增效果尽量走"已有结算点"(渡劫/战斗结束时挂钩)。
### 8e. CSP 红线(0.1.36 血案)
- **打包 renderer CSP 禁 unsafe-eval**:任何 `Function()/eval()` 字符串求值在开发模式(vite 无 CSP)可跑、打包版必炸——0.1.36 的"推进崩溃+定制白屏"双 bug 即此(bonusOf 每月月率+craft 渲染同源)。
- 军规:**新增表达式求值一律走 `data/arith.ts`**(白名单解析器——数字/L/四则/括号/幂/负号;字母/函数名→NaN 不抛);自查步骤:`rg "Function\(|eval\(" src/` 应为零。
- 教训:出 bug 先把**真实错误文字**拿到手(0.1.36 给 toast/ErrorBoundary 加了根错可见)——比猜快一个数量级。
### 8d. 未发布不兼容铁律(0.1.35) ### 8d. 未发布不兼容铁律(0.1.35)
- **游戏未正式发布 = 零存档迁移义务**:迁移链/版本戳/旧档兜底一票删除;防呆靠结构校验(保存=最新结构;读=字段断言)+ World 构造 normalize(仅防护语义)。 - **游戏未正式发布 = 零存档迁移义务**:迁移链/版本戳/旧档兜底一票删除;防呆靠结构校验(保存=最新结构;读=字段断言)+ World 构造 normalize(仅防护语义)。
- **新档结构唯一权威**:新字段初始责任在 initSim/creation——normalize 只做正当监管(npcs 重注册/headId 修复/era 校验),不做「为旧档补字段」。 - **新档结构唯一权威**:新字段初始责任在 initSim/creation——normalize 只做正当监管(npcs 重注册/headId 修复/era 校验),不做「为旧档补字段」。
@@ -145,7 +151,7 @@
7. MOD 目录形态仅支持单文件(目录/分包/依赖多个 .cotymod 自动解析留待)。 7. MOD 目录形态仅支持单文件(目录/分包/依赖多个 .cotymod 自动解析留待)。
8. CALAMITY 与 WORLDSTIM 数值表的大规模注入(多 MOD 叠加平衡)未做。 8. CALAMITY 与 WORLDSTIM 数值表的大规模注入(多 MOD 叠加平衡)未做。
9. NPC"中期势力轮替"曲线(newborn 频度/覆灭线)仍单参数,可寻参数化。 9. NPC"中期势力轮替"曲线(newborn 频度/覆灭线)仍单参数,可寻参数化。
10. **测试规模 7000**101 套件矩阵域),后续新增机制优先在对应矩阵域追加参(seed×组合),避免重开大文件。 10. **测试规模 7011**102 套件矩阵域),后续新增机制优先在对应矩阵域追加参(seed×组合),避免重开大文件。
11. **缓做项(0.1.28 审计定案)**declineYears 盛世误触/闪烁——重平衡归 0.1.29 独立调参(避免二次基线漂移);CALAMITY_FAMILY 独立口收益低(灾因口已覆盖);ERA_DURA/REGION_TIDE 松弛版数值口 0.1.29+(必须走"默认优先+聚合"模式)。 11. **缓做项(0.1.28 审计定案)**declineYears 盛世误触/闪烁——重平衡归 0.1.29 独立调参(避免二次基线漂移);CALAMITY_FAMILY 独立口收益低(灾因口已覆盖);ERA_DURA/REGION_TIDE 松弛版数值口 0.1.29+(必须走"默认优先+聚合"模式)。
## 八、如何开始修改(给下一个智能体的路径) ## 八、如何开始修改(给下一个智能体的路径)
@@ -156,7 +162,7 @@
改结构:注册进 clocks/plugins;世界循环参数唯一权威 = worldsim-data.ts 改结构:注册进 clocks/plugins;世界循环参数唯一权威 = worldsim-data.ts
→ 5. 改 UI:先改 storerevision)再改组件;纯逻辑放 game/engine 可测 → 5. 改 UI:先改 storerevision)再改组件;纯逻辑放 game/engine 可测
→ 6. 加测试:双金钟罩不动,新增用例挂 tests/xxx-0.1.x.test.ts(按主题命名) → 6. 加测试:双金钟罩不动,新增用例挂 tests/xxx-0.1.x.test.ts(按主题命名)
→ 7. 提交前:npm test101 套件 7000++ typecheck + GUI 冒烟需 WSLg 在线)+ 更新三份文档 + MOD_GUIDE(触 MOD 时) → 7. 提交前:npm test102 套件 7011++ typecheck + GUI 冒烟需 WSLg 在线)+ 更新三份文档 + MOD_GUIDE(触 MOD 时)
``` ```
**一句话原则**:先复现、再小改、后金钟罩。改坏确定性比改错玩法更危险;改数值去 worldsim-data,改流程去 clocks,改事件去 fire()。 **一句话原则**:先复现、再小改、后金钟罩。改坏确定性比改错玩法更危险;改数值去 worldsim-data,改流程去 clocks,改事件去 fire()。