feat(turns): P0 attachment support for images and PDFs (#306) - #313
Conversation
|
PR 清理:当前相对 |
waterbro-8
left a comment
There was a problem hiding this comment.
当前不能合并,先转 Draft。复核 exact merge CI 35191833784 与 diff 后有以下阻塞:
- 分支相对 main 为 CONFLICTING,必须先 rebase/merge 最新 main,保留已发布 CHANGELOG,不要在旧基线上继续补丁。
- 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 失败。 - macOS/Windows/Ubuntu 的 renderer/staging 构建失败于旧基线
packages/shared/dist/position-id.js的 browsercreateRequire;同步 main 后需重新确认,并跑完整 renderer/package CI。 - 上传读取边界缺少回归证据:需要断言 attachmentId 只能访问当前 session、元数据与文件的 id/mime/size 一致,拒绝符号链接/路径穿越;PDF 解析需有页数、文本字节和耗时/资源上限,失败不能把不可信文本当作成功提取。
- PR test plan 仍未执行。请补服务端上传/读取/限制/持久化测试、renderer 选择/粘贴/发送/历史测试,再更新验证记录后申请复审。
现有 body-size 修复可以单独拆成小 PR;附件功能建议以 rebase 后的独立提交继续。
|
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:
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
left a comment
There was a problem hiding this comment.
Review — changes requested(exact head 9c5d09db38632ae8378c5e06b427a97372a0e950)
当前 PR 仍是 Draft/CONFLICTING,且 hosted CI 有失败;下面还有会影响数据边界与运行时安全的阻塞项。
Blockers
-
附件存储/读取没有 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 回归测试。 -
上传和读取没有绑定到 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)。 -
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。 -
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/流式方案),并补最大尺寸测试。 -
非 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。请明确拒绝该组合,或实现所有允许路径的一致绑定;不能静默忽略。 -
运行时 manifest 未包含本 PR 新模块。
apps/desktop/packaging/runtime-layout.cjs:52-127,129-159没有attachments/{extract-pdf,routes,store,validate}.js和dist/attachments.js。exact-head CI35191833784已失败: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 后再请求复审。
|
Pushed
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). |
fcf1af9 to
fa45562
Compare
|
Rebased onto current Head |
fa45562 to
c098f14
Compare
|
Rebased onto current
Please re-run review on this head after CI. |
Value-importing @roleweave/shared pulled position-id/turns createRequire into the Vite renderer bundle and failed smoke/installer builds. task_ref: #313
|
Follow-up |
|
@waterbro-8 请对当前 head 对照你当时的 blockers(当前文件):
请在 |
PeterGuy326
left a comment
There was a problem hiding this comment.
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:
pdfjs-distis not declared anywhere.apps/server/package.jsondeps ={"@roleweave/shared": "*"}only (and its description claims zero third-party runtime deps); root has no pdfjs dep; the diff only touchespackages/shared/package.json.extract-pdf.tsloads it viaFunction('return import("pdfjs-dist/legacy/build/pdf.js")')()wrapped in try/catch →return undefined, so PDF text extraction always fails silently andextractedTextis never set — the CHANGELOG/PR-body claim ships as dead code. It's also absent from the packaging manifest. Either declarepdfjs-dist(and add it to the runtime layout) or scope PDF-text extraction out of this P0.- Byte-budget mismatch:
ATTACHMENT_MAX_PDF_TEXT_BYTES= 512 KiB exceedsMAX_INPUT_BYTES= 256 KiB (thread-context.ts).buildAttachmentContextinlines page text intoaugmentedInput;budget = max(0, 256KiB − bytes(input))then hits 0 andmaterializeThreadContextthrowsturn_request_invalidfor 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.
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
7b76cdc to
05daf49
Compare
|
Addressed PeterGuy326's 11:24 request-changes on head
@waterbro-8 请对 |
waterbro-8
left a comment
There was a problem hiding this comment.
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-distare gone fromapps/server/src/attachments/(store, routes, validate only). PDF stays a stored attachment; no silent dead extract.ATTACHMENT_MAX_PDF_TEXT_BYTESis gone frompackages/shared/src/attachments.ts, so it no longer exceedsMAX_INPUT_BYTES.
CI 11/11 green, MERGEABLE. Not merging — 胡奕舟 said he will squash after this re-review.
waterbro-8
left a comment
There was a problem hiding this comment.
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:
assertNotSymlinkon dir/file/meta;readStableBoundedFile+ id/size match. - Strict base64: charset/padding + round-trip.
- Bare
/turns:attachmentIdswithout a personal session → 400, not silent drop. - Runtime inventory:
dist/src/attachments/{routes,store,validate}.js+dist/attachments.js.extract-pdf.jsremoved with the extractor. - Renderer 10 MiB: chunked
bytesToBase64(0x8000), no spreadfromCharCode. - pdfjs-dist:
extract-pdf.tsdeleted. P0 stores PDF, does not extract text. CHANGELOG matches.extractedTextis not populated (routes test assertsundefined; context test asserts extracted pages never enter engine input). - Budgets:
buildAttachmentContextpath-manifest only, fail-closed if assembled UTF-8 > 256 KiB (MAX_INPUT_BYTES). Turn create callsassertAttachmentBatch(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_PAGESandextractedTextremain 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 胡奕舟).
|
@PeterGuy326 waterbro-8 已在当前 head 那三条合前意见已在 |
|
Current head |
Refs #306. Base the tree on current main so approval-preview.js exists, then overlay attachment files. Keep preview fingerprinting and attachment validation.
5983492 to
fa9c190
Compare
|
Rebased onto latest main New head:
@PeterGuy326 your REQUEST_CHANGES is still pinned to old head @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.
9d01391 to
14ed4c8
Compare
PeterGuy326
left a comment
There was a problem hiding this comment.
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.
Summary
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)main.js,preload.js,session-ipc.cjsTurnComposer.tsx: 附件卡片条、粘贴处理、Paperclip 按钮TurnPanel.tsx: 附件上传状态管理AttachmentCard.tsx: 附件卡片组件message-actions.tsx: 历史附件展示Test plan
npm run check全绿Deferred
Closes #306
🤖 Generated with [Qoder][https://qoder.com]