Pie · Issue #38 引用页内内容 · v1 Design Preview

这份预览是 brainstorming skill 阶段的可视化交付物,覆盖 Issue #38 的 v1 设计全貌。每节顶部的 已锁 表示已经在 brainstorm 中由你确认;待复核 表示我有判断但希望你点头;v2 推迟 表示明确不做。

用 §10 末尾的 checklist 一次性勾完后,点页底 "看完,写 spec" 是个 UI 占位(不会真触发),你回复 "OK" 即可让我把 spec.md 写到 docs/specs/

§0brainstorm 决策摘要

5 道澄清问题的答案。每张卡是后续 §1-§7 决策的源点。

Q1v1 scope

A 文字 B 元素截图 D 引用面板。C 智能组件边界 / iframe / Canvas / OCR 全部 v2 推迟

Q2触发方式

划词 mouseup → 浮出 floating bubble,点 bubble 添加为文字 chip。元素引用走 sidepanel picker → 进入 hover-highlight pick 模式。

Q3元素表征

截图(走 Phase 5 image content block) + 结构化 metadata(role / accessibleName / textContent / outerHTML 截断),新增 <untrusted_page_element> wrapper 包裹 metadata。

Q4容量上限

v1 不设上限。但 image chip(元素截图 + 用户上传图)继续受 Phase 5 R13/R14 evict 不变量约束。BYOK cost 失控由 v1.1 reactive 收口

Q5架构选型

方案 1:常驻 content script,manifest 加 content_scripts 字段(matches: <all_urls>, run_at: document_idle)。单 module 同时承担 划词监听 / floating bubble / picker overlay / 元素抽取与截图。
不变量:常驻 script 只承担引用功能;click / type / snapshot 等 DOM 工具继续走 chrome.scripting.executeScript,零迁移。

§1架构总览 已锁

3 层组件 + 单向数据流。content script 不持久化,所有 chip state 活在 sidepanel useSession

PAGE · NEW content script — src/content/quote/
SelectionListener
document.addEventListener('mouseup') → 检测 window.getSelection() 非空 → 计算 bubble 锚点
FloatingBubble
Shadow DOM 隔离样式;点击后抽 textContent + range + sourceUrl,sendMessage 给 SW
ElementPicker
SW 启用时进入 hover-highlight 模式:mousemove 高亮元素 outline + 角标,click 选定,Esc 退出
ScreenshotExtractor
bbox + html2canvas-free 实现:用 chrome.tabs.captureVisibleTab + crop 走 SW 端(page 端只回报 bbox)
chrome.runtime.sendMessage type: "quote-captured" · type: "picker-element-bbox"
SERVICE WORKER · + thin QuoteBridge — src/background/quote-bridge.ts
QuoteBridge handler
收 quote-captured / picker-element-bbox → 校验 sender.tab.id → 派发给 active sidepanel port
Screenshot crop
picker-element-bbox 触发 chrome.tabs.captureVisibleTab(tabId) → OffscreenCanvas crop → JPEG q85,复用 Phase 5 image normalize util
Picker start/stop RPC
sidepanel → SW "picker:start(tabId)" → SW broadcast 给 content script enter picker mode;类似 stop / esc
不持久化
QuoteBridge 不写 storage,不进 session 持久层;chip 生命周期由 sidepanel 拥有
panel port 已有:新增 'quote-added' 消息类型 + 'picker-state' 消息类型
SIDEPANEL · existing — src/sidepanel/hooks/useSession.ts + Chat.tsx
useSession.quotes
Map<sessionId, Quote[]>(复用 M3-U6 per-session state 模型)
Composer chip 行
现有 image attach chip 行扩展:文字 chip / 元素 chip / image chip 共享同一容器
Pre-submit serialize
send 时把 quotes 序列化进 user message(text wrapper + image block + element wrapper),写完 quotes 清空
Picker button
composer 上 "拾取元素" 按钮 → 通过 SW RPC 启动 picker;按钮在 picker 进行中显 "拾取中… (Esc 取消)"

关键不变量

§2Content Script 实现 已锁

划词 → Floating Bubble

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultrices urna eget elit ornare, vitae malesuada nisi rhoncus. Donec efficitur, urna a tincidunt fermentum, ante elit cursus risus, ut vehicula nibh nulla nec lectus.
添加为引用
  • 监听 mouseup + selectionchange
  • 非空 selection(trim 后 ≥ 1 字符)→ 计算锚点:selection 结束位置上方(避开输入光标 / 鼠标轨迹)
  • Shadow DOM 隔离样式(防站点 CSS 污染)
  • 点击后 chrome.runtime.sendMessage({type: 'quote-captured', kind: 'text', payload: {text, sourceUrl}})
  • 同 selection 多次点击 = 多次添加 chip(让用户决定,不去重)

Picker 模式 → 元素选取

card
Create issue (hovered)
card
点击元素以引用 · Esc 取消
  • sidepanel 按"拾取元素" → SW broadcast → content script 进入 picker 模式
  • mousemove 命中元素:加 outline 高亮 + 顶部角标显 <tag> · accessibleName
  • click → 抽 bbox + role + accessibleName + textContent + outerHTML(截断 1000 字)
  • Esc / 右键 / 再次点 sidepanel 按钮 → 退出 picker 模式
  • 截图本身在 SW 端 crop:page 只回 bbox,SW 调 captureVisibleTab 后 crop

载入策略

"content_scripts": [{
  "matches": ["<all_urls>"],
  "js": ["src/content/quote/index.ts"],
  "run_at": "document_idle",
  "all_frames": false,            // v1 不进 iframe
  "match_origin_as_fallback": false
}]

SPA 兼容

CSP 兼容

禁注入名单

v1 暂不维护 deny-list。CSP 严格站点(GitHub / Stripe / GMail 等)实测后补 exclude_matches。Pie 现有的 <all_urls> host_permission 在这些站点没遇到拒绝问题。

§3SW QuoteBridge 模块 已锁

thin module,只做路由 + 截图 crop。不持久化、不参与 agent loop。

消息协议

方向typepayload用途
content → SW quote-text-captured { text, sourceUrl } 用户在页面点 bubble 后
content → SW quote-element-captured { bbox, role, accessibleName, textContent, outerHTMLTruncated, sourceUrl } picker click 后;SW 收到后再 captureVisibleTab + crop
SW → sidepanel port quote-added { id, kind, ...payload, sourceTabId, imageDataUrl? } chip 投递;imageDataUrl 仅元素 chip 携带
sidepanel → SW picker:start { tabId } 启动 picker 模式
sidepanel → SW picker:stop { tabId } 取消 picker 模式
SW → content picker:enter / picker:exit { } 广播给指定 tab 的 content script

不变量

§4Sidepanel UI 已锁 视觉细节待复核

Composer chip 行 mockup

"Vivamus ultrices urna eget elit ornare...× button · "Create issue"× 🖼screenshot-3.jpg×

chip 视觉规则

Picker 启动 UX

useSession state 字段

type Quote =
  | { id: string; kind: "text"; text: string; sourceUrl: string; sourceTabId: number }
  | { id: string; kind: "element"; role: string; accessibleName: string;
      textContent: string; outerHTMLTruncated: string;
      imageDataUrl: string;            // 元素 bbox crop JPEG
      sourceUrl: string; sourceTabId: number };

type SessionUIState = {
  // ... existing
  quotes: Quote[];                     // 不写 storage
};

§5LLM wire 协议 已锁

新增 2 个 untrusted wrapper,注册到 untrusted-wrappers.tsKNOWN_WRAPPERS 列表,复用全套 closing-tag confusable sanitize 防御。元素 chip 的截图走 Phase 5 已有的 image content block。

新增 wrapper

wrapper用途内容
untrusted_page_quote 文字 chip 用户选中的纯文本
untrusted_page_element 元素 chip 的结构化 metadata(截图独立走 image block) role / name / textContent / outerHTML truncated

序列化时序

send 时按 chip 添加顺序构造 user message content array:先 image content blocks(元素截图 + 用户上传图按 chip 顺序),再 text content block(含所有 wrapper + 纯文本输入)。

实际发给 LLM 的 user message(Anthropic shape)
{
  "role": "user",
  "content": [
    { "type": "image", "source": { "type": "base64", ... } },   // 元素 chip 截图
    { "type": "image", "source": { "type": "base64", ... } },   // 用户上传图
    {
      "type": "text",
      "text": "<untrusted_page_quote source_url=\"https://example.com/docs\">
Vivamus ultrices urna eget elit ornare, vitae malesuada nisi rhoncus.
</untrusted_page_quote>

<untrusted_page_element source_url=\"https://github.com/foo/bar/issues\"
                        role=\"button\" name=\"Create issue\">
text_content: \"New issue\"
outer_html: \"&lt;button class=\\\"Button--primary\\\"&gt;New issue&lt;/button&gt;\"
</untrusted_page_element>

帮我看下这个按钮点了会发生什么"
    }
  ]
}

不变量

§6错误处理 / 边界 已锁

容错

明确不防的

§7测试策略 已锁 cross-layer 必做

按 CLAUDE.md "cross-layer integration test 模板" feedback:任何跨 panel↔SW 新 wire 字段必须有 wire→DisplayMessage 透传 regression test。本 feature 含 6 个新 wire type,cross-layer 是 P0 不可省。

测试范围关键 case
content unit SelectionListener / FloatingBubble / ElementPicker / ScreenshotExtractor mouseup 触发 bubble;空 selection 不触发;Shadow DOM 隔离;Esc 退 picker
SW unit QuoteBridge 路由 + 截图 crop null sender 拒绝;captureVisibleTab 失败 → imageDataUrl: null
sidepanel unit useSession quote state + Composer chip 行 + Pre-submit serialize chip 加 / 删 / send 清空;per-session 隔离;3 类 chip 视觉
cross-layer integration content → SW → panel → DisplayMessage 透传 quote-text-captured 全链路;quote-element-captured 含截图全链路;切 session 中断 picker
wire shape 序列化为 LLM user message image 块顺序;wrapper sanitize 双角度(confusable / 嵌套);纯文字 + chip 并存
untrusted-wrappers 新增 2 wrapper 复用现有 sanitize regression:KNOWN_WRAPPERS 列表 build-time check(同 risk.ts 模式)

§8v1 明确不做 v2 推迟

C 智能组件边界高亮
划词时自动识别"最小父组件"边界并高亮。需要 heuristic 算法(React fiber root 探测 / 语义化标签识别 / aria-label boundary),单独 v2 项。v1 划词只用浏览器原生 selection。
iframe 内容引用
v1 all_frames: false。iframe 跨域 + same-origin policy + nested frames 复杂度高;待 v2 评估。
Canvas / OCR 兜底
Canvas 内 "文字"在 DOM 不可见,需 OCR。OCR pipeline 独立工程(client-side tesseract.js vs server-side),与 BYOK 模型成本叠加风险。v2 单独评估。
chip 持久化
v1 send 后清空,切 session / SW 重启 / panel 关闭 → 清空。持久化需要 SW storage write + R13 evict 协调,与 chip "一次性引用"语义冲突。
chip 容量上限
brainstorm Q4 决议:v1 不设。BYOK cost 失控由 v1.1 reactive 收口。image chip 仍受 Phase 5 R13/R14 evict。
划词后自动展开 side panel
issue 第 3 条提到 "选中内容后打开 side panel"。chrome.sidePanel API 仅允许 user gesture 触发 open,content script mouseup 不算 user gesture(实测 / 文档双重确认才能保证)。v1 假设 sidepanel 已开(与 Pie 当前使用习惯一致);v1.1 评估是否走 action.openSidePanel + tabs onUpdated 钩子。
引用历史
用户多次引用同一段文本不去重,不存档。"看历史引用过什么"属于 v2 范畴。

§9待你复核 / 一次性确认

brainstorm 中 我有判断但希望你点头 的设计点。勾完 = 进 spec.md。


下一步

勾完 §9 12 条 + 点底部"看完,写 spec" → 我把 spec.md 写到 docs/specs/2026-05-14-issue-38-page-content-reference-design.md 并 commit;之后再走 spec self-review + 用户复核 + writing-plans。

本预览文件路径:docs/specs/2026-05-14-issue-38-preview.html(spec.md 写完后保留为可交互辅助;如需删除告诉我)

12 项待复核 — 勾完后回复 OK