Skip to content

feat(turns): P0 attachment support for images and PDFs (#306) - #313

Merged
PeterGuy326 merged 8 commits into
mainfrom
feat/306-turn-attachments
Sep 20, 2026
Merged

PeterGuy326 merged 8 commits into
mainfrom
feat/306-turn-attachments

Conversation

@sun-970

@sun-970 sun-970 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 对话回合支持图片(PNG/JPEG/WebP)和 PDF 附件
  • 用户可通过粘贴或文件选择器添加附件
  • 服务端存储附件并提取 PDF 文本(pdfjs-dist)
  • 附件上下文通过 input 拼接传递给引擎(Decision A2)
  • TurnRecord 新增可选 attachments 字段(向后兼容)

Changes

Shared (packages/shared)

  • attachments.ts: 类型定义和校验常量
  • errors.ts: 6 个附件错误码
  • api.ts: 附件上传/读取路由
  • turns.ts: TurnRecord 扩展

Server (apps/server)

  • attachments/store.ts: 原子写入存储
  • attachments/validate.ts: fail-closed 校验
  • attachments/extract-pdf.ts: PDF 文本提取
  • attachments/routes.ts: HTTP 端点
  • routes/turns.ts: 附件上下文构建(Decision A2)
  • turns/store.ts: 附件元数据持久化

Desktop (apps/desktop)

  • IPC 桥接:main.js, preload.js, session-ipc.cjs
  • TurnComposer.tsx: 附件卡片条、粘贴处理、Paperclip 按钮
  • TurnPanel.tsx: 附件上传状态管理
  • AttachmentCard.tsx: 附件卡片组件
  • message-actions.tsx: 历史附件展示
  • CSS 样式 + i18n 文案(zh/en)

Test plan

  • 粘贴 PNG 截图 → 看到附件卡片
  • 选择多页 PDF → 看到附件卡片
  • 发送带附件回合 → 引擎收到含附件上下文的 input
  • 回合历史显示附件信息
  • 纯文本回合不受影响(回归)
  • npm run check 全绿

Deferred

  • 拖拽投放、图片 OCR、引用跳转、附件生命周期 UI

Closes #306

🤖 Generated with [Qoder][https://qoder.com]

@sun-970

sun-970 commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

PR 清理:当前相对 main 为 CONFLICTING,verify CI 红(Node 24 ubuntu/macOS、Windows/macOS smoke 与 unsigned installers)。附件功能还没到可合状态,先转 Draft,避免挡参赛前的 PR 队列。需要继续做时再 rebase 到当前 main 并修 CI。

@sun-970
sun-970 marked this pull request as draft September 19, 2026 06:26

@waterbro-8 waterbro-8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前不能合并,先转 Draft。复核 exact merge CI 35191833784 与 diff 后有以下阻塞:

  1. 分支相对 main 为 CONFLICTING,必须先 rebase/merge 最新 main,保留已发布 CHANGELOG,不要在旧基线上继续补丁。
  2. package-layout 明确失败:apps/server/dist/src/routes/sessions.js 与 routes/turns.js 引入的 attachments/store.js、attachments/validate.js 未加入 apps/desktop/packaging/runtime-layout.cjs,导致 runtime manifest 和两端 staging/installer 失败。
  3. macOS/Windows/Ubuntu 的 renderer/staging 构建失败于旧基线 packages/shared/dist/position-id.js 的 browser createRequire;同步 main 后需重新确认,并跑完整 renderer/package CI。
  4. 上传读取边界缺少回归证据:需要断言 attachmentId 只能访问当前 session、元数据与文件的 id/mime/size 一致,拒绝符号链接/路径穿越;PDF 解析需有页数、文本字节和耗时/资源上限,失败不能把不可信文本当作成功提取。
  5. PR test plan 仍未执行。请补服务端上传/读取/限制/持久化测试、renderer 选择/粘贴/发送/历史测试,再更新验证记录后申请复审。

现有 body-size 修复可以单独拆成小 PR;附件功能建议以 rebase 后的独立提交继续。

@PeterGuy326

Copy link
Copy Markdown
Contributor

Status check from today's triage pass — this PR is still blocked and needs author action before it can move forward. Current blockers, in suggested order:

  1. Branch is CONFLICTING with main. main has moved (memory-plane R1 docs docs(memory): record RoleWeave memory-plane R1 design (#327) #345 merged today; more PRs are in the merge queue). Please rebase onto the latest main and keep the released CHANGELOG sections intact.
  2. Package layout failures. apps/server/dist/src/routes/sessions.js and routes/turns.js import attachments/store.js / attachments/validate.js, but they are not in apps/desktop/packaging/runtime-layout.cjs, which breaks the runtime manifest and both staging/installer jobs.
  3. Renderer/staging build failures on the old baseline (packages/shared/dist/position-id.js browser createRequire). After syncing main, please confirm these are gone and run the full CI.
  4. Missing regression evidence for upload/read boundaries: assertions that an attachmentId is only accessible within its own session, that metadata and file id/mime/size are consistent, that symlinks/path traversal are rejected, and that PDF parsing has page/text-byte/time caps and never treats untrusted text as a successful extraction.
  5. PR test plan not executed yet.

Suggested path: sync main first (it may resolve item 3), then fix the runtime-layout entries (item 2), then add the boundary tests (item 4) and run the documented test plan. Happy to re-review once a new head is up.

@waterbro-8 waterbro-8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — changes requested(exact head 9c5d09db38632ae8378c5e06b427a97372a0e950)

当前 PR 仍是 Draft/CONFLICTING,且 hosted CI 有失败;下面还有会影响数据边界与运行时安全的阻塞项。

Blockers

  1. 附件存储/读取没有 symlink 与稳定文件保护。 apps/server/src/attachments/store.ts:29-33,49-54,63-80 使用 mkdir({ recursive: true })、rename 和 readFile,没有逐级 lstat/禁止符号链接,也没有 O_NOFOLLOW/stable-read;meta.json 与 file 可被替换或重定向,且读取时没有验证它们仍属于同一附件。请补 real-directory chain、原子/稳定读取和 symlink/path traversal 回归测试。

  2. 上传和读取没有绑定到 server-owned session。 apps/server/src/attachments/routes.ts:60-67,101-114 只校验 UUID,就允许对不存在、已轮换或别的 session 创建/读取附件;这会产生可持续的 orphan storage,也绕过了“附件只能属于当前 session”的边界。上传和读取前应通过 ctx.sessionStore.get() 校验 workspace 中真实存在的 session(并按产品语义限制 active/当前 session)。

  3. PDF 解析没有资源上限。 apps/server/src/attachments/extract-pdf.ts:18-27 只限制页数为 200,没有文本总字节/单页字节/解析耗时或取消边界;压缩 PDF/超大文本层可以让 CPU、内存和后续 input 无界增长。MAX_META_BYTES 只在读取时检查,saveAttachment() 写入时仍可写入超大 meta.json。请在解析和持久化前冻结 page/text/time/metadata budgets,并添加恶意/超限 fixture。

  4. 10 MiB 文件在 renderer 中会因 spread 参数溢出而无法上传。 apps/desktop/renderer/src/turns/TurnPanel.tsx:234-236 使用 String.fromCharCode(...new Uint8Array(buffer));允许的最大文件是 10 MiB,但这种调用在大约几十万字节后就会抛 RangeError: Maximum call stack size exceeded。请改为分块 base64 编码(或 Blob/流式方案),并补最大尺寸测试。

  5. 非 session 的 /turns 会静默丢弃附件。 apps/server/src/routes/turns.ts:133-150 接受 attachmentIds,但 executeTurn() 只有在 session !== undefined 时才解析它们(apps/server/src/routes/turns.ts:347-356);调用 bare /turns 带附件会返回成功,却不会把附件放进 engine context 或 TurnRecord。请明确拒绝该组合,或实现所有允许路径的一致绑定;不能静默忽略。

  6. 运行时 manifest 未包含本 PR 新模块。 apps/desktop/packaging/runtime-layout.cjs:52-127,129-159 没有 attachments/{extract-pdf,routes,store,validate}.js 和 dist/attachments.js。exact-head CI 35191833784 已失败:package-layout 报这 5 个 unpackaged imports,staging/installer 也因此失败。请先同步最新 main,补 manifest,再重跑完整 CI。

Additional correctness

  • Buffer.from(body.dataBase64, "base64") 会容忍非法字符;当前 try/catch 不能证明输入是合法 base64,应做严格字符/填充/round-trip 校验。
  • apps/server 与 renderer 没有附件专项测试;PR body 的 test plan 仍全部未执行。至少需要覆盖 session 隔离、元数据与文件 id/mime/size 一致性、symlink/path traversal、非法 base64、PDF caps、最大文件 UI 上传和历史回读。

当前 CI 还受旧基线 packages/shared/dist/position-id.js 的 Vite createRequire 错误影响;rebase 后请确认该基线问题确实消失,而不是把它当作附件功能已通过。修复并推送新 head 后再请求复审。

@sun-970

sun-970 commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed fcf1af9444c950b510ddd82cd83d5fcb1a2a28df for the exact-head CHANGES_REQUESTED:

  • store: no symlink dirs/files, stable bounded reads, meta.id and file size must match
  • upload/read call sessionStore.get before IO
  • PDF: page + per-page/total text bytes + parse timeout; over-budget returns undefined (not fake success)
  • renderer: chunked base64, no fromCharCode(...10MiB)
  • bare /turns with attachmentIds is 400
  • runtime-layout includes attachment modules + dist/attachments.js
  • added apps/server/test/attachments-store.test.ts

Please re-review this head. Rebase onto latest main is still needed if the PR remains CONFLICTING (git fetch to github.com:443 failed from this runner).

@sun-970
sun-970 force-pushed the feat/306-turn-attachments branch from fcf1af9 to fa45562 Compare September 19, 2026 10:02
@sun-970

sun-970 commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (5ffdff1). Dropped two already-landed server commits (#309 consume-body / post-loop drain). Conflicts resolved by keeping main's gemini engine, Qoder login, workspace initialize, IME/caret newline, and #366 approval wiring, plus the attachment surface.

Head fa45562. Still Draft until CI is green.

sun-970 added a commit that referenced this pull request Sep 20, 2026
Keep #313 Draft. Rebase onto current main and put attachment modules in
the explicit runtime allowlist in walkFiles order so package-layout CI
passes.

task_ref: #313
@sun-970
sun-970 force-pushed the feat/306-turn-attachments branch from fa45562 to c098f14 Compare September 20, 2026 02:00
@sun-970

sun-970 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (23d7492, includes #390/#384/#393). Kept Draft.

Please re-run review on this head after CI.

sun-970 added a commit that referenced this pull request Sep 20, 2026
Value-importing @roleweave/shared pulled position-id/turns createRequire
into the Vite renderer bundle and failed smoke/installer builds.

task_ref: #313
@sun-970

sun-970 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up 7b76cdc: renderer no longer value-imports @roleweave/shared (that pulled createRequire from position-id/turns into Vite and failed smoke). Attachment constants now come from @roleweave/shared/attachments. Still Draft.

@sun-970
sun-970 marked this pull request as ready for review September 20, 2026 02:08
@sun-970
sun-970 requested a review from waterbro-8 September 20, 2026 02:30
@sun-970

sun-970 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

@waterbro-8 请对当前 head 7b76cdc 复审。你 9-19 07:31 的 CHANGES_REQUESTED 钉在 rebase 前的 9c5d09db,已经过期;这条 head 相对 main MERGEABLE,hosted CI 全绿。

对照你当时的 blockers(当前文件):

  1. symlink / 稳定读取 — apps/server/src/attachments/store.ts:lstat 禁 symlink、tmp+rename 原子写、readStableBoundedFile,meta.id / size 必须匹配。回归:apps/server/test/attachments-store.test.ts
  2. 绑定 server-owned session — attachments/routes.ts 上传/读取都先 sessionStore.get(workspace.dir, sessionId)
  3. PDF 预算 — extract-pdf.ts:页数 + 单页/总文本字节 + parse timeout;超预算返回 undefined(不当成功提取)。saveAttachment 写入前检查 MAX_META_BYTES
  4. 10 MiB spread — TurnPanel.tsx bytesToBase64 分块,不再 fromCharCode(...10MiB)
  5. bare /turns + attachmentIds — routes/turns.ts 无 session 时 400:attachmentIds require a personal session
  6. runtime manifest — runtime-layout.cjs 已按 walkFiles 收入 attachments 模块;Node 24 / smoke / installers 全绿
  7. 严格 base64 — decodeBase64Strict:charset + padding + round-trip
  8. Vite createRequire — renderer 改为 @roleweave/shared/attachments 子路径,不再 value-import 桶

请在 7b76cdc 上点一次。过了胡奕舟会合。

@PeterGuy326 PeterGuy326 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve the direction — the rebase onto current main is clean, the runtime-layout manifest now lists all four dist/src/attachments/*.js + dist/attachments.js, symlink hardening (assertNotSymlink at every level + readStableBoundedFile/decodeStableUtf8) and session binding (ctx.sessionStore.get() in both upload and read) are solid, and the renderer correctly uses the deep path @roleweave/shared/attachments. CI 11/11 green. But two substantive defects survive the green CI and should be fixed before merge:

  1. pdfjs-dist is not declared anywhere. apps/server/package.json deps = {"@roleweave/shared": "*"} only (and its description claims zero third-party runtime deps); root has no pdfjs dep; the diff only touches packages/shared/package.json. extract-pdf.ts loads it via Function('return import("pdfjs-dist/legacy/build/pdf.js")')() wrapped in try/catch → return undefined, so PDF text extraction always fails silently and extractedText is never set — the CHANGELOG/PR-body claim ships as dead code. It's also absent from the packaging manifest. Either declare pdfjs-dist (and add it to the runtime layout) or scope PDF-text extraction out of this P0.
  2. Byte-budget mismatch: ATTACHMENT_MAX_PDF_TEXT_BYTES = 512 KiB exceeds MAX_INPUT_BYTES = 256 KiB (thread-context.ts). buildAttachmentContext inlines page text into augmentedInput; budget = max(0, 256KiB − bytes(input)) then hits 0 and materializeThreadContext throws turn_request_invalid for any session with history — a large PDF makes the turn unusable. Align the two budgets.

Also: ATTACHMENT_MAX_TOTAL_BYTES (30 MiB) is enforced only in the renderer; server-side turn creation checks count (<=5) and single-file size but not the total. And test coverage is thin for +970 lines (one 38-line store test; nothing for routes, validate, extract-pdf, buildAttachmentContext, cross-session id rejection, or renderer behavior) — please add route + cross-session tests. Once pdfjs-dist is declared (or PDF-text scoped out), the budgets aligned, and tests added, this is good to merge after waterbro-8 re-reviews the current head (his CHANGES_REQUESTED is stale, from the pre-rebase head).

Add image (PNG/JPEG/WebP) and PDF attachment support to conversation
turns. Users can paste from clipboard or select files via the composer.

- Shared types and validation constants (packages/shared)
- Server attachment store with atomic writes under session directory
- PDF text extraction via pdfjs-dist (dynamic import, graceful fallback)
- Attachment upload/read HTTP endpoints + IPC bridge
- Attachment context injected into engine input (Decision A2: path
  manifest + extracted text, no envelope schema change)
- Composer UI: paperclip button, paste handler, attachment card strip
- Turn history displays saved attachment cards
- Additive optional `attachments` field on TurnRecord (backward compat)
- i18n for zh/en, CSS for attachment strip and cards

Deferred to follow-up: drag-drop, OCR, reference links, lifecycle UI.
Refs #306.

Reject symlinks, require sessionStore.get on upload/read, cap PDF
text/time, chunk renderer base64, refuse attachmentIds on bare /turns,
and ship attachment modules in the runtime manifest.
Keep #313 Draft. Rebase onto current main and put attachment modules in
the explicit runtime allowlist in walkFiles order so package-layout CI
passes.

task_ref: #313
Value-importing @roleweave/shared pulled position-id/turns createRequire
into the Vite renderer bundle and failed smoke/installer builds.

task_ref: #313
P0 keeps PDF as a stored attachment but does not extract text (no
pdfjs-dist). Engine context lists file paths only and must fit the
256 KiB input budget. Server turn creation now enforces total size.
Adds validate, context, route, and cross-session tests.

task_ref: #313
@sun-970
sun-970 force-pushed the feat/306-turn-attachments branch from 7b76cdc to 05daf49 Compare September 20, 2026 03:39
@sun-970

sun-970 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed PeterGuy326's 11:24 request-changes on head 05daf49 (rebased onto current main c41049a):

  1. pdfjs-dist — P0 不抽 PDF 文本。删了 extract-pdf.ts 和 runtime-layout 条目;上传不再 dynamic import。PDF 仍可当附件存,引擎上下文只带路径。
  2. 预算 — buildAttachmentContext 不再内联 extractedText,组装结果必须 ≤ MAX_INPUT_BYTES 256 KiB。
  3. 总量 — 建 turn 时 assertAttachmentBatch(个数 + 30 MiB 总量)。
  4. 测试 — attachments-validate / attachments-context / attachments-routes(非法 base64、跨 session 拒绝、bare /turns + attachmentIds 400)。本地 6/6 绿。

@waterbro-8 请对 05daf49 复审。

@waterbro-8 waterbro-8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review — Approve current head (not 验收)

Re-review of 05daf49 (after 7b76cdc). Prior waterbro-8 CHANGES_REQUESTED was on pre-rebase 9c5d09d and is stale.

Checked PeterGuy326’s two P1s on this head:

  • extract-pdf.ts / pdfjs-dist are gone from apps/server/src/attachments/ (store, routes, validate only). PDF stays a stored attachment; no silent dead extract.
  • ATTACHMENT_MAX_PDF_TEXT_BYTES is gone from packages/shared/src/attachments.ts, so it no longer exceeds MAX_INPUT_BYTES.

CI 11/11 green, MERGEABLE. Not merging — 胡奕舟 said he will squash after this re-review.

#313

waterbro-8
waterbro-8 previously approved these changes Sep 20, 2026

@waterbro-8 waterbro-8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Approve (code, not 产品验收)

Exact head 05daf49a286405dec8edf6ccc143e13ad98f2a42. MERGEABLE. Hosted CI 11/11 green.

Re-review of the post-rebase head (my prior CHANGES_REQUESTED was on pre-rebase 9c5d09db). Peter's REQUEST_CHANGES on 7b76cdc is also superseded by this commit.

Previously blocking items — now addressed

  • Session bind: upload/read both ctx.sessionStore.get(); missing session 404; cross-session read 400.
  • Symlink/stable read: assertNotSymlink on dir/file/meta; readStableBoundedFile + id/size match.
  • Strict base64: charset/padding + round-trip.
  • Bare /turns: attachmentIds without a personal session → 400, not silent drop.
  • Runtime inventory: dist/src/attachments/{routes,store,validate}.js + dist/attachments.js. extract-pdf.js removed with the extractor.
  • Renderer 10 MiB: chunked bytesToBase64 (0x8000), no spread fromCharCode.
  • pdfjs-dist: extract-pdf.ts deleted. P0 stores PDF, does not extract text. CHANGELOG matches. extractedText is not populated (routes test asserts undefined; context test asserts extracted pages never enter engine input).
  • Budgets: buildAttachmentContext path-manifest only, fail-closed if assembled UTF-8 > 256 KiB (MAX_INPUT_BYTES). Turn create calls assertAttachmentBatch (count ≤5, total ≤30 MiB).
  • Tests: attachments-routes (session bind, illegal base64, cross-session, bare /turns, session turn persist), attachments-validate, attachments-context (no inline PDF text + budget), existing store symlink test.

Non-blocking

  • ATTACHMENT_MAX_PDF_PAGES and extractedText remain on the shared type with no producer in this P0.
  • No renderer unit tests; server coverage is enough for this gate.

Not product验收. I am not merging (read on own PRs does not apply here; still leaving merge to 胡奕舟).

@sun-970

sun-970 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

@PeterGuy326 waterbro-8 已在当前 head 05daf49 Approve。你 11:24 的 Request changes 还钉在旧 head 7b76cdc,GitHub 因此仍 BLOCKED,他合不了。

那三条合前意见已在 05daf49 落地:P0 不抽 PDF 文本、上下文 ≤ 256 KiB、服务端总量校验 + 测试。请你 dismiss / 在新 head 上再点一次,过了就可以合。

sun-970 added a commit that referenced this pull request Sep 20, 2026
Refs #306.

Keep #405 approval-preview fingerprinting and locale keys, and retain
#313 attachment store/runtime/i18n. Merge main e53ab83 into 05daf49.
@PeterGuy326

Copy link
Copy Markdown
Contributor

Current head 05daf49 addresses the previous attachment blockers and has an independent current-head approval, but after #404/#405 merged the branch is now CONFLICTING with main. Please rebase onto latest main while preserving the attachment changes and rerun CI; I will re-review/merge once the new head is clean.

Refs #306.

Base the tree on current main so approval-preview.js exists, then overlay
attachment files. Keep preview fingerprinting and attachment validation.
@sun-970
sun-970 force-pushed the feat/306-turn-attachments branch from 5983492 to fa9c190 Compare September 20, 2026 06:11
Refs #306.

Base the tree on current main (#405 + #407) so approval-preview.js and
org reparent bindings remain. Overlay attachment files. Sort
SHARED_RUNTIME_FILES so attachments.js follows approvals.js. Keep both
#406 and #306 changelog entries.
@sun-970

sun-970 commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto latest main 920c6e6 (#405 + #407).

New head: 82450229790a35fc92ab565e464c84182140f10c

@PeterGuy326 your REQUEST_CHANGES is still pinned to old head 7b76cdc. I cannot dismiss it. Please re-review this head (or dismiss the stale review). GitHub stays BLOCKED until then.

@waterbro-8 please re-review after CI is green.

The previous merge dropped turn.send's key and appended attachment
strings after the locale object closer, which broke tsc.
@sun-970
sun-970 force-pushed the feat/306-turn-attachments branch from 9d01391 to 14ed4c8 Compare September 20, 2026 06:20

@PeterGuy326 PeterGuy326 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed current head 14ed4c8 after merging latest main. Previous blockers are resolved: undeclared PDF extraction is removed, attachment input uses the shared 256 KiB budget, server validation enforces 5 files / 10 MiB each / 30 MiB total, and route/context tests cover session binding and cross-session rejection. The merge also preserves the approved #405 integrity-bound preview implementation and locale keys. Local verification: build + UI typecheck passed; approval/attachment suites 30 passed, 1 optional engine test skipped; package-layout 9/9 passed. All 11 hosted checks are green. Approved.

@PeterGuy326
PeterGuy326 merged commit 5e407fb into main Sep 20, 2026
11 checks passed
@PeterGuy326
PeterGuy326 deleted the feat/306-turn-attachments branch September 20, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(turns): image/PDF attachments for turns with OCR and citations

3 participants