From e475dff6b00bf36be9b25fe7e3f677f5ae0b6492 Mon Sep 17 00:00:00 2001 From: urzeye <20869204+urzeye@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:42:14 +0800 Subject: [PATCH 01/17] docs(audit): plan project remediation --- docs/developer/project-audit-2026-07-10.md | 443 ++++++++++++++++++ ...-07-10-project-audit-remediation-design.md | 70 +++ .../2026-07-10-project-audit-remediation.md | 198 ++++++++ 3 files changed, 711 insertions(+) create mode 100644 docs/developer/project-audit-2026-07-10.md create mode 100644 docs/plans/2026-07-10-project-audit-remediation-design.md create mode 100644 docs/plans/2026-07-10-project-audit-remediation.md diff --git a/docs/developer/project-audit-2026-07-10.md b/docs/developer/project-audit-2026-07-10.md new file mode 100644 index 000000000..2f1058953 --- /dev/null +++ b/docs/developer/project-audit-2026-07-10.md @@ -0,0 +1,443 @@ +# Ophel Atlas 全项目架构、性能与 UI/UX 审查 + +> 审查日期:2026-07-10 +> 审查范围:全站共享架构、浏览器扩展与油猴双平台、核心生命周期、状态与存储、Shadow DOM/主题、通用 UI/UX、可访问性、构建体积、死代码与兼容层。 +> 不在范围:仅影响单个站点选择器或单个站点 DOM 适配的兼容问题。 + +## 1. 结论 + +项目当前可以通过格式、类型和两种生产构建,但仍存在数个会影响所有站点或整个平台的结构性问题。最需要优先处理的是: + +1. 扩展端两个内容脚本入口重复打包全部站点适配器和 11 种语言,单页需解析约 7.49 MB 原始 JavaScript。 +2. 多套弹窗、菜单和队列 UI 通过 Portal 跳出 Shadow DOM,动态主题变量没有随之跨出,深色、自定义和 24 套预置主题会退回局部硬编码 fallback。 +3. 油猴端在所有站点永久保留全页 `MutationObserver` 和 2 秒轮询;选中提示词后还存在永久逐帧布局测量。 +4. Options 页面在 600–800 px 宽度出现横向溢出和标签文字被压成竖排;设置开关没有可访问名称和可见键盘焦点。 +5. 核心管理器的生命周期契约没有统一落地,已经出现无法释放的订阅、不可取消的延迟任务和存储写入竞态。 + +本次没有确认 P0 级别的稳定必现崩溃、全量数据损坏或安全漏洞。确认问题分布为: + +| 级别 | 数量 | 含义 | +| --- | ---: | --- | +| P1 | 6 | 应优先进入近期版本,已造成明显性能、主题或可访问性问题 | +| P2 | 10 | 中期必须修复,存在竞态、泄漏、长期维护或一致性风险 | +| P3 | 5 | 清理和治理项,可随相关模块修改一起完成 | + +## 2. 审查方法与证据 + +### 2.1 静态与构建检查 + +已运行: + +- `pnpm format:check`:通过。 +- `pnpm lint:check`:通过退出码,但有 110 条 warning。 +- `pnpm typecheck`:通过。 +- `pnpm build`:通过。 +- `pnpm build:userscript`:通过。 + +当前没有正式测试体系,因此“构建通过”不能证明运行时生命周期、主题、键盘操作或窄屏布局正确。 + +### 2.2 视觉与键盘冒烟 + +使用生产构建的 Options 页面做了 1440×900、800×700、600×700 三档浏览器检查,并对原生 checkbox 执行键盘聚焦。 + +确认结果: + +- 1440 px 下结构可用,但内容密度偏低,右侧存在较大空白。 +- 800 px 下二级标签开始被强制折为两行。 +- 600 px 下固定 228 px 侧边栏挤压主内容,二级标签接近竖排,右侧标签被裁切,页面产生横向溢出。 +- 聚焦设置开关的 checkbox 后,没有可见焦点环;无障碍树中的 checkbox 也没有名称。 + +### 2.3 构建体积快照 + +本次生产构建测得: + +| 产物 | 原始大小 | gzip | +| --- | ---: | ---: | +| `main.*.js` | 3,336,340 B | 849,872 B | +| `ui-entry.*.js` | 4,158,496 B | 1,056,090 B | +| 两个扩展内容脚本合计 | 7,494,836 B | 1,905,962 B | +| `ophel.user.js` | 1,860,986 B | 482,797 B | + +扩展资源从本地加载,不直接产生网络下载成本,但 JavaScript 的解析、编译、初始化和内存占用仍发生在每个支持站点页面。 + +## 3. P1:优先修复 + +### P1-01 两个扩展入口重复打包全部适配器和语言包 + +**范围:所有扩展端支持站点;油猴端也受单体 bundle 影响。** + +证据: + +- `src/contents/main.ts` 导入 `getAdapter()`。 +- `src/components/App.tsx:10` 和 `src/components/App.tsx:407` 再次导入并调用 `getAdapter()`。 +- `src/adapters/index.ts:7-22` 同步导入全部 15 个站点适配器,并在模块加载时全部实例化。 +- `src/locales/resources.ts:5-15` 同步导入全部 11 种语言。 +- 构建产物中 `main.*.js` 和 `ui-entry.*.js` 均包含各适配器类及多语言文本。 + +影响: + +- 每个页面都解析与当前站点无关的 14 个适配器。 +- `main` 和 `ui-entry` 是独立内容脚本,适配器与语言资源被重复打包、重复解析。 +- 适配器源文件本身很大,例如 Gemini、ChatGPT、AI Studio 分别约 4,900、3,411、3,276 行,重复成本明显。 +- 扩展端两个入口合计约 7.49 MB 原始 JS;这比局部 DOM 微优化更值得优先处理。 + +建议: + +1. 将站点识别拆成轻量 hostname/path 路由,不要先构造全部适配器再逐个 `match()`。 +2. 按站点动态导入适配器,或由构建脚本为不同 match 组生成独立入口。 +3. 评估合并 `main.ts` 与 CSUI 初始化,使核心逻辑和 React UI 共享同一适配器实例与模块图。 +4. 扩展端语言资源按当前语言加载,至少避免 11 份完整字典同时进入两个入口。 + +验收标准: + +- 单个站点产物不再包含其他站点的大型适配器实现。 +- `main` 与 UI 不再各自携带完整适配器和语言字典。 +- 记录修改前后的 raw/gzip 大小和页面初始化耗时。 + +### P1-02 Portal 跨出 Shadow DOM 后丢失主题变量,形成隐式第二主题系统 + +**范围:所有站点的通用弹窗、菜单、加载遮罩、提示词弹窗和队列 UI。** + +证据: + +- `src/core/theme-manager.ts:760-794` 只把动态变量写入 Shadow Root 内的 `:host` 和 `#gh-theme-vars`。 +- `src/components/ui/Dialog.tsx:99-100,163` 把样式注入 `document.head`,再把对话框 Portal 到 `document.body`。 +- `src/components/ConversationDialogs.tsx:92-101,165` 使用相同模式。 +- `ConversationMenus`、`LoadingOverlay`、Prompt Import/Editor/Preview 等也 Portal 到 `document.body`。 +- `QueueOverlay.tsx:516` 的目标查找使用 `document.querySelector(".gh-root")`;该 API 无法穿透 Shadow DOM,因此实际会回退到 `document.body`。 +- 普通组件 CSS import 会生成页面级 content-script CSS;这能让队列样式“看起来存在”,但同时绕开 Shadow DOM 隔离和动态主题变量。 + +影响: + +- 深色主题、自定义主题和非默认预置主题中的覆盖层会使用白色、灰色、蓝色等 fallback,而不是当前 `--gh-*` 变量。 +- 页面级 CSS 和 DOM 节点污染宿主页面,增加样式冲突和 z-index 冲突风险。 +- 主题兼容依赖 fallback,违反 `DESIGN.md` 的单一主题源和 24 套主题兼容要求。 +- 同一功能在 Options 页面与内容面板中可能表现不同,因为 Options 的变量在文档根可用,而面板变量只存在于 Shadow Host。 + +建议: + +- 建立一个 Shadow Root 内的统一 overlay portal 容器,所有面板弹窗、菜单、tooltip、loading 和 queue 默认渲染到该容器。 +- 只有必须覆盖宿主页面且不能受 Shadow Host 几何影响的元素才进入 `document.body`;这类元素应挂到独立 portal host,并由 `ThemeManager` 显式镜像变量。 +- 删除组件内各自向 `document.head` 注入样式的实现,统一样式入口和清理生命周期。 + +验收标准: + +- 24 套主题与自定义主题下,弹窗、菜单、加载层、队列和预览均读取同一组动态变量。 +- 内容面板功能不再依赖页面级 `.gh-*` CSS 才能显示正确。 + +### P1-03 油猴端在所有站点永久观察整个文档并同时轮询 + +**范围:所有油猴端支持站点。** + +证据:`src/platform/userscript/entry.tsx:437-448` 在 UI 挂载后: + +- 对 `document.documentElement` 建立 `{ childList: true, subtree: true }` 的永久 `MutationObserver`。 +- 同时每 2 秒执行一次 `shadowHost.isConnected` 检查。 +- 两者只在页面 unload 时清理。 + +影响: + +- AI 流式生成会产生大量 DOM 变更,observer callback 即使只做连接检查,也会被高频调度。 +- Observer 与 interval 完成同一“防宿主被移除”任务,属于重复监控。 +- 该逻辑不是单站点兼容,而是对全部油猴站点无条件开启。 + +建议: + +- 默认只观察 Shadow Host 的直接父节点 `{ childList: true, subtree: false }`。 +- 稳定挂载后停止重试定时器;只在确认 host 被移除时短时重启。 +- 不同时长期保留全树 Observer 和 interval。 + +### P1-04 选中提示词后永久逐帧测量布局 + +**范围:所有站点的提示词选择条。** + +证据:`src/components/SelectedPromptBar.tsx:146-151` 在 `title` 存在期间递归调用 `requestAnimationFrame(trackPosition)`;每帧会执行: + +- `adapter.getTextareaElement()`; +- 必要时向上遍历并调用 `getComputedStyle()`; +- `getBoundingClientRect()`; +- React state 比较与可能更新。 + +影响: + +- 用户选中一个提示词但暂不发送时,会持续以显示器帧率工作。 +- 布局读取可能触发同步 layout,在复杂站点和长对话中造成 CPU、耗电与滚动卡顿。 +- 代码已经有 `ResizeObserver` 和 window resize 监听,逐帧循环属于过度兜底。 + +建议: + +- 用 `ResizeObserver`、滚动事件的 rAF 节流、`visualViewport` 和有限时长 transition 跟踪替代永久循环。 +- 若必须追踪宿主动画,只在检测到几何变化后维持短窗口,连续若干帧稳定后停止。 + +### P1-05 Options 页面缺少整体响应式策略 + +**范围:独立 Options、权限请求窗口和窄窗口使用场景。** + +证据: + +- `src/styles/settings.css:44-46` 将侧边栏固定为 228 px 且不收缩。 +- `src/styles/settings.css:538-543` 内容区固定使用 24×32 px padding。 +- 现有 `@media` 只处理主题卡片、About 局部和 reduced motion,没有处理整体 sidebar/content/tab 导航。 +- 浏览器实测:800 px 时二级标签明显换行;600 px 时标签近似竖排、右侧内容被裁切并产生横向滚动。 + +影响: + +- 权限请求页或用户缩窄 Options 窗口时,导航难以扫描和点击。 +- 中文、德文、俄文等较长标签更容易被压缩。 +- 与 `DESIGN.md` 中“稳定网格、可扫读、点击目标足够大”的要求不一致。 + +建议: + +- 设定 Options 的响应式断点:窄屏下收起侧边栏为图标栏或顶部下拉导航。 +- 二级 tab 改为横向滚动、下拉或允许有控制的两行布局,不应逐字折行。 +- 设置行在窄屏下切为上下布局,控件保持最小可操作宽度。 + +### P1-06 通用设置开关没有可访问名称和可见焦点 + +**范围:至少 52 个 `ToggleRow` 设置项。** + +证据: + +- `src/components/ui/Switch.tsx:37-41` 使用原生 checkbox,但设置为 `opacity: 0; width: 0; height: 0`。 +- `Switch` 没有 `aria-label`、`aria-labelledby` 或可关联的 `id`。 +- `src/tabs/options/components/SettingRow.tsx:52,73-89` 的文字是普通 `div`,没有 `