[P2][CR-F-P2] 前端杂项:按键冲突、重复 change 处理器、i18n、URL 参数无 allowlist、new Function、tsconfig #534

Open
opened 2026-09-29 06:44:01 +00:00 by troytt · 0 comments
Owner

来源:#516|优先级 P2|评审编号 F-P2(前端杂项)|基线 e475c2e

问题描述

这里收集了 6 个前端小问题。单看每个都不大,但它们有同一个特点:缺少统一入口。键盘事件、语言状态、页面参数、公式求值、类型检查配置,都是各写各的。

  1. Enter 既是"攻击"又是"打开聊天";三个键盘处理器各有一套过滤规则。
  2. #act 下拉框挂了两个 change 处理器,一次选择会发起两次方向相反的导航。
  3. i18n 靠 246 处内联 isEn ? … : …,语言状态有两个来源,入口 HTML 有三份几乎相同的拷贝。
  4. URL 参数没有 allowlist,可以让页面从任意来源加载数据、连接任意中继;页面也没有 CSP。
  5. 技能描述公式用正则改写后交给 new Function 求值,出错时静默返回 0。
  6. 一个 tsconfig 同时管浏览器代码、Node 脚本和测试,还把部分测试排除在类型检查之外。

除特别说明外,以下结论均为代码阅读得出,未单独复现。

证据

1. 按键冲突

  • KeyboardInput 把 Enter 当作攻击键(input.ts:39),把 Tab 当作地图键(input.ts:48)。

  • onKeyDown(input.ts:97-117)只看 event.code,不看 event.target。只要是它认识的键,一律 preventDefault()。

  • 聊天栏也在 window 上监听 Enter,用来打开聊天框(toolbar-chat-ui.ts:206-219)。

  • 结果:

    • 在 act-scene 里按 Enter 打开聊天时,角色同时开始攻击,直到松开 Enter。
    • 焦点在 <select> 上时,方向键和 Tab 仍被当作游戏输入并阻止默认行为:下拉框无法用键盘操作,Tab 也无法切换焦点。
    • 聊天框里打字不会漏进游戏,只是因为聊天框自己调用了 stopPropagation()(toolbar-chat-ui.ts:191-192)。
  • 三个 window 级 keydown 处理器,三套过滤规则:

    处理器 过滤规则
    KeyboardInput 不过滤
    toolbar-chat-ui 检查 document.activeElement 是否为 input / select / textarea
    HudManager(hud-manager.ts:718-719) 检查 e.target 是否为 input / select(不含 textarea)
  • 原版 1.13c 中 Enter 用来打开聊天输入框,键盘上并没有"攻击"键。KeyJ / Enter 攻击是项目为键盘演示模式加的。

2. #act 上的两个 change 处理器

位置 做法 对 level 参数
toolbar-chat-ui.ts:724-729 addEventListener('change'),然后赋值 location.search 删除
toolbar-chat-ui.ts:843-850 onchange,然后赋值 location.search 设为该 act 的第一个场景

一次 change 会连续发起两次导航,而且意图相反。最后停在哪个 URL,取决于浏览器怎么处理连续导航。

3. i18n

  • src/ 中有 246 处 isEn ? … : …,分布在 13 个文件里。算上其他按语言判断的三元表达式,约 320 处,分布在 24 个文件里。
  • 最多的三个文件(宽口径):
    • scene/backend/skill-caster.ts:65 处(后端模块)
    • ui/world-panels.ts:62 处
    • ui/hotkeys.ts:38 处
  • 语言状态有两个来源:HudManager.lang 和 getTblLang()(lang.ts:84-86)。
    • 例如 skill-caster.ts:115:(hudManager?.lang ?? getTblLang()) === 'ENG'。
    • 后端模块为了拿到语言,要去读 UI 对象(见 #526)。
  • 三份入口 HTML:acts.html(133 行)、acts-zh.html(132 行)、acts-en.html(132 行)。
    • acts.html 和 acts-zh.html 只差 1 行。
    • 中英文版只差 37 行,全是界面文字。
  • 译名手写,与 .tbl 不一致:

4. URL 参数没有 allowlist

页面 参数 用途
act-scene(act-scene.ts:775-783) pack、base 从 ${pack}/index.json 加载资源包和角色资源
net-scene(net-scene.ts:554-557) data、mpq、ws 加载 fixture 目录;fetch(mpq) 后按 MPQ 解析(net-scene.ts:571-577);连接哪个中继
  • 这些值都不校验来源。一条构造好的链接就能:
    • 让页面从任意服务器加载资源包或 MPQ;
    • 把联机流量导向任意中继。中继一方可以伪造输入,见 #518。
  • 目前没找到把这些数据写进 innerHTML 的路径(例如 net-scene.ts:486-488 只插入数字),所以暂不构成 XSS。
  • 5 个 HTML 入口都没有 CSP,缺少纵深防御。

5. new Function 求值技能描述公式

  • evalSkillDescFormula(skill-calc-engine.ts:577)的做法是:先用正则把公式改写成 JS 表达式,再交给 new Function('Math', 'return ' + s)(skill-calc-engine.ts:642-648)。

  • skill-calc-engine 被 hud-manager、skill-tree-panel、class-starter-profiles 等生产代码引用。

  • 静默兜底,违反 AGENTS.md 的 fail-fast 约定:

    • sklvl(...)、miss(...) 直接被替换成 0(skill-calc-engine.ts:607-609);
    • 求值出错时,catch 返回 0;
    • Number(res) || 0 把 NaN 也变成 0。

    结果是 tooltip 上的数字错了,也不会有任何报错。

  • 安全与 CSP:

    • 公式目前来自静态的 src/data/skills-catalog.ts,所以现在还不是注入漏洞。
    • 但如果以后改为从 MPQ 读取(而 ?mpq= 可以指定任意 MPQ),这里就会变成任意代码执行。
    • new Function 要求 CSP 放开 'unsafe-eval',和第 4 点要加的 CSP 冲突。
  • 仓库里已经有正规的求值器:calc-ast.ts 的 evaluateCalc(calc-ast.ts:574)。它先分词再递归下降解析,文件头列出了它覆盖的 1.13c 表中 5 个边界情况。

  • skill-tree.ts 里还有一处 new Function(skill-tree.ts:4765)。这个文件只被测试引用,src/ 里没有任何引用。

6. tsconfig

  • include 同时包含 src、scripts、vite.config.ts 和 tests(tsconfig.json:18)。
  • 所有代码都启用了 types: ["node"](tsconfig.json:16)。浏览器代码里误用 process、Buffer、node:fs 也能通过类型检查,问题要到运行时才暴露。
    • 现有的例子:src/mpq/file-source.ts 直接 import('node:fs/promises')(file-source.ts:17)。
  • tests/e2e-drop-parity(5 个文件)和 tests/fail-fast-drop.test.ts 被排除在类型检查之外(tsconfig.json:19-22),但 vitest 仍然会运行它们。
    • 用同样的编译选项单独检查,这些文件有 20 个类型错误。
    • 其中一个错误让一条用例的断言永远不会执行,详见 #535。

根因

  • 前端没有统一入口:键盘事件、语言、页面配置、公式求值都各自实现。
  • 类型检查的边界从没按"浏览器 / Node / 测试"划分过。

修复指南

  1. 统一键盘分发:
    • 全局只保留一个 window 级 keydown 监听器(例如 InputRouter),按优先级分发:可编辑控件 → 聊天 → HUD 快捷键 → 游戏输入。
    • 用同一个 isEditableTarget(e.target) 判断可编辑控件,覆盖 input、textarea、select 和 contenteditable。焦点在可编辑控件上时,游戏输入不处理,也不 preventDefault。
    • 从 ATTACK_KEYS 里去掉 Enter,和原版一致:Enter 只用来打开聊天。KeyJ 如果只是演示和测试用的,就放到显式开关后面。
  2. #act 只保留一个 change 处理器:
    • 删除 L724 的 addEventListener('change'),或者把两段逻辑合并成一个函数;
    • .onchange = 统一改成 addEventListener,并接入 #533 的 Disposables。
  3. i18n 键值表:
    • 新建 src/i18n/strings.ts,按键管理界面文字,例如 t('toolbar.class')。
    • 游戏内的名称(职业、技能、物品、怪物)一律从 .tbl 取,不在代码或 HTML 里手写译名。如果项目决定显示简体中文,就做一个有记录的转换层,而不是逐条手写。
    • 语言状态只保留 src/i18n/lang.ts 一个来源,删除 HudManager.lang。后端不再读语言,文字格式化放到前端。
    • 三个 acts*.html 合并成一个模板。语言由 URL 参数或 navigator.language 决定,文字在运行时填充。
    • 分批进行:先把后端(skill-caster.ts)里的文字移出去,再按文件逐个替换。
  4. URL 参数 allowlist 与 CSP:
    • 用一个 readPageConfig() 集中解析页面参数,每个参数都有类型和取值范围。
    • pack、base、data、mpq 只接受同源路径,即 new URL(value, location.href).origin === location.origin。不合法就报错并拒绝加载,不要静默回退到默认值。
    • ws 只接受配置好的中继地址列表,开发环境可以放行 localhost。
    • 给 5 个 HTML 入口加 CSP,至少包含 default-src 'self'; script-src 'self'; connect-src 'self' <中继地址>,不开 'unsafe-eval'。
  5. 用 calc-ast 替换 new Function:
    • 让 evalSkillDescFormula 复用 calc-ast 的分词和解析,只把符号解析(lvl、par1..8、ln12、skill('…'.blvl) 等)接到 SKILLS_CATALOG 上。
    • 不认识的函数或符号直接报错,不要静默替换成 0。如果 1.13c 的 D2Common 对某个符号确实返回 0,要在代码里注明出处,写法可以参考 calc-ast.ts:566-568。
    • 删除 skill-tree.ts 里仅供测试的 new Function。
    • 用 ESLint 的 no-new-func 固化(#537)。
  6. 拆分 tsconfig:
    • tsconfig.base.json:公共选项。
    • tsconfig.app.json:src/**,只用 DOM 类型,不引入 node。
    • tsconfig.node.json:scripts/**、vite.config.ts,引入 node。
    • tsconfig.test.json:tests/**。
    • 用 project references 和 tsc -b 一次检查全部;npm run typecheck 改为调用它。
    • src/mpq/file-source.ts 这类只在 Node 下运行的代码,移到 Node 侧或放进 node 配置。
    • 取消对测试文件的排除,修掉那 20 个类型错误。

验收标准

  • act-scene 里按 Enter 只打开聊天,不触发攻击。
  • 焦点在 <select> 或 <input> 上时,方向键、Tab 不被游戏拦截;有对应的 DOM 事件测试。
  • 全仓只有一个 window 级 keydown 监听器。
  • 对 #act 做一次 change,只触发一次导航。
  • src/ 中没有 isEn ? … : …;只剩一个 acts.html;游戏内名称全部来自 .tbl。(可以分多个 PR 完成。)
  • 非同源的 pack / base / data / mpq,以及不在列表里的 ws,都会被拒绝并给出明确的错误。
  • 5 个入口都配置了 CSP,且不含 'unsafe-eval'。
  • src/ 中没有 new Function。技能描述公式用 calc-ast 求值,并且有测试覆盖原来会被替换成 0 的 sklvl(...) / miss(...)。
  • tsconfig 拆分完成,src 不再引入 node 类型,不再排除任何测试文件。
  • npm run typecheck 0 error;npx vitest run 全部通过。

相关

  • #526:后端读取 hudManager.lang。
  • #533:Disposables 和监听器的生命周期。
  • #518:ws 被指向恶意中继时的输入伪造。
  • #537:no-new-func 规则。
  • #535:被排除在类型检查之外的测试。
> 来源:#516|优先级 P2|评审编号 F-P2(前端杂项)|基线 `e475c2e` ## 问题描述 这里收集了 6 个前端小问题。单看每个都不大,但它们有同一个特点:**缺少统一入口**。键盘事件、语言状态、页面参数、公式求值、类型检查配置,都是各写各的。 1. Enter 既是"攻击"又是"打开聊天";三个键盘处理器各有一套过滤规则。 2. `#act` 下拉框挂了两个 change 处理器,一次选择会发起两次方向相反的导航。 3. i18n 靠 246 处内联 `isEn ? … : …`,语言状态有两个来源,入口 HTML 有三份几乎相同的拷贝。 4. URL 参数没有 allowlist,可以让页面从任意来源加载数据、连接任意中继;页面也没有 CSP。 5. 技能描述公式用正则改写后交给 `new Function` 求值,出错时静默返回 0。 6. 一个 tsconfig 同时管浏览器代码、Node 脚本和测试,还把部分测试排除在类型检查之外。 除特别说明外,以下结论均为代码阅读得出,未单独复现。 ## 证据 ### 1. 按键冲突 - `KeyboardInput` 把 `Enter` 当作攻击键([input.ts:39](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/sim/input.ts#L39)),把 `Tab` 当作地图键([input.ts:48](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/sim/input.ts#L48))。 - `onKeyDown`([input.ts:97-117](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/sim/input.ts#L97-L117))只看 `event.code`,不看 `event.target`。只要是它认识的键,一律 `preventDefault()`。 - 聊天栏也在 `window` 上监听 Enter,用来打开聊天框([toolbar-chat-ui.ts:206-219](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/toolbar-chat-ui.ts#L206-L219))。 - 结果: - 在 act-scene 里按 Enter 打开聊天时,角色同时开始攻击,直到松开 Enter。 - 焦点在 `<select>` 上时,方向键和 Tab 仍被当作游戏输入并阻止默认行为:下拉框无法用键盘操作,Tab 也无法切换焦点。 - 聊天框里打字不会漏进游戏,只是因为聊天框自己调用了 `stopPropagation()`([toolbar-chat-ui.ts:191-192](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/toolbar-chat-ui.ts#L191-L192))。 - 三个 `window` 级 keydown 处理器,三套过滤规则: | 处理器 | 过滤规则 | |---|---| | `KeyboardInput` | 不过滤 | | `toolbar-chat-ui` | 检查 `document.activeElement` 是否为 input / select / textarea | | `HudManager`([hud-manager.ts:718-719](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/ui/hud-manager.ts#L718-L719)) | 检查 `e.target` 是否为 input / select(不含 textarea) | - 原版 1.13c 中 Enter 用来打开聊天输入框,键盘上并没有"攻击"键。`KeyJ` / `Enter` 攻击是项目为键盘演示模式加的。 ### 2. `#act` 上的两个 change 处理器 | 位置 | 做法 | 对 `level` 参数 | |---|---|---| | [toolbar-chat-ui.ts:724-729](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/toolbar-chat-ui.ts#L724-L729) | `addEventListener('change')`,然后赋值 `location.search` | **删除** | | [toolbar-chat-ui.ts:843-850](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/frontend/toolbar-chat-ui.ts#L843-L850) | `onchange`,然后赋值 `location.search` | **设为**该 act 的第一个场景 | 一次 change 会连续发起两次导航,而且意图相反。最后停在哪个 URL,取决于浏览器怎么处理连续导航。 ### 3. i18n - `src/` 中有 246 处 `isEn ? … : …`,分布在 13 个文件里。算上其他按语言判断的三元表达式,约 320 处,分布在 24 个文件里。 - 最多的三个文件(宽口径): - `scene/backend/skill-caster.ts`:65 处(后端模块) - `ui/world-panels.ts`:62 处 - `ui/hotkeys.ts`:38 处 - **语言状态有两个来源**:`HudManager.lang` 和 `getTblLang()`([lang.ts:84-86](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/i18n/lang.ts#L84-L86))。 - 例如 [skill-caster.ts:115](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/backend/skill-caster.ts#L115):`(hudManager?.lang ?? getTblLang()) === 'ENG'`。 - 后端模块为了拿到语言,要去读 UI 对象(见 #526)。 - **三份入口 HTML**:`acts.html`(133 行)、`acts-zh.html`(132 行)、`acts-en.html`(132 行)。 - `acts.html` 和 `acts-zh.html` 只差 1 行。 - 中英文版只差 37 行,全是界面文字。 - **译名手写,与 `.tbl` 不一致**: - 职业名直接写在 HTML 里([acts-zh.html:79-86](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/acts-zh.html#L79-L86)、[acts-en.html:79-86](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/acts-en.html#L79-L86))。 - 而从官方 `.tbl` 生成的字典里已经有这些名字,例如 `CANONICAL_CHI_TBL_DICT` 的 `Druid`([canonical-tbl-strings.ts:4569](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/data/canonical-tbl-strings.ts#L4569))。 - 两边用字不同:HTML 写的是"德鲁伊",表里是"德魯依"。 ### 4. URL 参数没有 allowlist | 页面 | 参数 | 用途 | |---|---|---| | act-scene([act-scene.ts:775-783](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/act-scene.ts#L775-L783)) | `pack`、`base` | 从 `${pack}/index.json` 加载资源包和角色资源 | | net-scene([net-scene.ts:554-557](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/net-scene.ts#L554-L557)) | `data`、`mpq`、`ws` | 加载 fixture 目录;`fetch(mpq)` 后按 MPQ 解析([net-scene.ts:571-577](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/net-scene.ts#L571-L577));连接哪个中继 | - 这些值都不校验来源。一条构造好的链接就能: - 让页面从任意服务器加载资源包或 MPQ; - 把联机流量导向任意中继。中继一方可以伪造输入,见 #518。 - 目前没找到把这些数据写进 `innerHTML` 的路径(例如 [net-scene.ts:486-488](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/scene/net-scene.ts#L486-L488) 只插入数字),所以暂不构成 XSS。 - 5 个 HTML 入口都没有 CSP,缺少纵深防御。 ### 5. `new Function` 求值技能描述公式 - `evalSkillDescFormula`([skill-calc-engine.ts:577](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/skill-calc-engine.ts#L577))的做法是:先用正则把公式改写成 JS 表达式,再交给 `new Function('Math', 'return ' + s)`([skill-calc-engine.ts:642-648](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/skill-calc-engine.ts#L642-L648))。 - `skill-calc-engine` 被 `hud-manager`、`skill-tree-panel`、`class-starter-profiles` 等生产代码引用。 - **静默兜底**,违反 `AGENTS.md` 的 fail-fast 约定: - `sklvl(...)`、`miss(...)` 直接被替换成 `0`([skill-calc-engine.ts:607-609](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/skill-calc-engine.ts#L607-L609)); - 求值出错时,`catch` 返回 `0`; - `Number(res) || 0` 把 NaN 也变成 0。 结果是 tooltip 上的数字错了,也不会有任何报错。 - **安全与 CSP**: - 公式目前来自静态的 `src/data/skills-catalog.ts`,所以现在还不是注入漏洞。 - 但如果以后改为从 MPQ 读取(而 `?mpq=` 可以指定任意 MPQ),这里就会变成任意代码执行。 - `new Function` 要求 CSP 放开 `'unsafe-eval'`,和第 4 点要加的 CSP 冲突。 - **仓库里已经有正规的求值器**:`calc-ast.ts` 的 `evaluateCalc`([calc-ast.ts:574](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine/calc-ast.ts#L574))。它先分词再递归下降解析,文件头列出了它覆盖的 1.13c 表中 5 个边界情况。 - `skill-tree.ts` 里还有一处 `new Function`([skill-tree.ts:4765](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/skill-tree.ts#L4765))。这个文件只被测试引用,`src/` 里没有任何引用。 ### 6. tsconfig - `include` 同时包含 `src`、`scripts`、`vite.config.ts` 和 `tests`([tsconfig.json:18](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/tsconfig.json#L18))。 - 所有代码都启用了 `types: ["node"]`([tsconfig.json:16](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/tsconfig.json#L16))。浏览器代码里误用 `process`、`Buffer`、`node:fs` 也能通过类型检查,问题要到运行时才暴露。 - 现有的例子:`src/mpq/file-source.ts` 直接 `import('node:fs/promises')`([file-source.ts:17](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/mpq/file-source.ts#L17))。 - `tests/e2e-drop-parity`(5 个文件)和 `tests/fail-fast-drop.test.ts` 被排除在类型检查之外([tsconfig.json:19-22](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/tsconfig.json#L19-L22)),但 vitest 仍然会运行它们。 - 用同样的编译选项单独检查,这些文件有 20 个类型错误。 - 其中一个错误让一条用例的断言永远不会执行,详见 #535。 ## 根因 - 前端没有统一入口:键盘事件、语言、页面配置、公式求值都各自实现。 - 类型检查的边界从没按"浏览器 / Node / 测试"划分过。 ## 修复指南 1. **统一键盘分发**: - 全局只保留一个 `window` 级 keydown 监听器(例如 `InputRouter`),按优先级分发:可编辑控件 → 聊天 → HUD 快捷键 → 游戏输入。 - 用同一个 `isEditableTarget(e.target)` 判断可编辑控件,覆盖 input、textarea、select 和 `contenteditable`。焦点在可编辑控件上时,游戏输入不处理,也不 `preventDefault`。 - 从 `ATTACK_KEYS` 里去掉 `Enter`,和原版一致:Enter 只用来打开聊天。`KeyJ` 如果只是演示和测试用的,就放到显式开关后面。 2. **`#act` 只保留一个 change 处理器**: - 删除 L724 的 `addEventListener('change')`,或者把两段逻辑合并成一个函数; - `.onchange = ` 统一改成 `addEventListener`,并接入 #533 的 `Disposables`。 3. **i18n 键值表**: - 新建 `src/i18n/strings.ts`,按键管理界面文字,例如 `t('toolbar.class')`。 - 游戏内的名称(职业、技能、物品、怪物)一律从 `.tbl` 取,不在代码或 HTML 里手写译名。如果项目决定显示简体中文,就做一个有记录的转换层,而不是逐条手写。 - 语言状态只保留 `src/i18n/lang.ts` 一个来源,删除 `HudManager.lang`。后端不再读语言,文字格式化放到前端。 - 三个 `acts*.html` 合并成一个模板。语言由 URL 参数或 `navigator.language` 决定,文字在运行时填充。 - 分批进行:先把后端(`skill-caster.ts`)里的文字移出去,再按文件逐个替换。 4. **URL 参数 allowlist 与 CSP**: - 用一个 `readPageConfig()` 集中解析页面参数,每个参数都有类型和取值范围。 - `pack`、`base`、`data`、`mpq` 只接受同源路径,即 `new URL(value, location.href).origin === location.origin`。不合法就报错并拒绝加载,不要静默回退到默认值。 - `ws` 只接受配置好的中继地址列表,开发环境可以放行 `localhost`。 - 给 5 个 HTML 入口加 CSP,至少包含 `default-src 'self'; script-src 'self'; connect-src 'self' <中继地址>`,不开 `'unsafe-eval'`。 5. **用 `calc-ast` 替换 `new Function`**: - 让 `evalSkillDescFormula` 复用 `calc-ast` 的分词和解析,只把符号解析(`lvl`、`par1..8`、`ln12`、`skill('…'.blvl)` 等)接到 `SKILLS_CATALOG` 上。 - 不认识的函数或符号直接报错,不要静默替换成 0。如果 1.13c 的 D2Common 对某个符号确实返回 0,要在代码里注明出处,写法可以参考 [calc-ast.ts:566-568](https://git.projectdiablo2.cn/troytt/diablo2-web/src/commit/e475c2e6d8a6e85eabb607525d9d87beca9fbee3/src/game/engine/calc-ast.ts#L566-L568)。 - 删除 `skill-tree.ts` 里仅供测试的 `new Function`。 - 用 ESLint 的 `no-new-func` 固化(#537)。 6. **拆分 tsconfig**: - `tsconfig.base.json`:公共选项。 - `tsconfig.app.json`:`src/**`,只用 DOM 类型,不引入 `node`。 - `tsconfig.node.json`:`scripts/**`、`vite.config.ts`,引入 `node`。 - `tsconfig.test.json`:`tests/**`。 - 用 project references 和 `tsc -b` 一次检查全部;`npm run typecheck` 改为调用它。 - `src/mpq/file-source.ts` 这类只在 Node 下运行的代码,移到 Node 侧或放进 node 配置。 - 取消对测试文件的排除,修掉那 20 个类型错误。 ## 验收标准 - [ ] act-scene 里按 Enter 只打开聊天,不触发攻击。 - [ ] 焦点在 `<select>` 或 `<input>` 上时,方向键、Tab 不被游戏拦截;有对应的 DOM 事件测试。 - [ ] 全仓只有一个 `window` 级 keydown 监听器。 - [ ] 对 `#act` 做一次 change,只触发一次导航。 - [ ] `src/` 中没有 `isEn ? … : …`;只剩一个 `acts.html`;游戏内名称全部来自 `.tbl`。(可以分多个 PR 完成。) - [ ] 非同源的 `pack` / `base` / `data` / `mpq`,以及不在列表里的 `ws`,都会被拒绝并给出明确的错误。 - [ ] 5 个入口都配置了 CSP,且不含 `'unsafe-eval'`。 - [ ] `src/` 中没有 `new Function`。技能描述公式用 `calc-ast` 求值,并且有测试覆盖原来会被替换成 0 的 `sklvl(...)` / `miss(...)`。 - [ ] tsconfig 拆分完成,`src` 不再引入 `node` 类型,不再排除任何测试文件。 - [ ] `npm run typecheck` 0 error;`npx vitest run` 全部通过。 ## 相关 - #526:后端读取 `hudManager.lang`。 - #533:`Disposables` 和监听器的生命周期。 - #518:`ws` 被指向恶意中继时的输入伪造。 - #537:`no-new-func` 规则。 - #535:被排除在类型检查之外的测试。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#534
No description provided.