deerflow2/.planning/phases/06-/06-RESEARCH.md

21 KiB
Raw Blame History

Phase 6: 在输入框输入@时,可引用已生成文件和已上传附件 - Research

Researched: 2026-04-15
Domain: 聊天输入框 @ 文件引用thread 内 artifacts + uploads
Confidence: HIGH

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: 引用来源限定为“当前线程”的 artifacts + uploads,不做跨线程或全局文件池。
  • D-02: 输入 @ 即刻弹出候选面板;继续输入即进行过滤。
  • D-03: 选中文件后在输入框内展示为可删除标签chip而非纯文本 @文件名
  • D-04: 同名文件场景下,候选项展示“文件名 + 类型徽标 + 路径尾段”,避免歧义。
  • D-09: @ 触发后的文件选择面板必须使用 dropdown 组件实现(不使用自定义浮层替代)。
  • D-05: 复用 additional_kwargs.files 作为提交数据结构,不新增并行主结构。
  • D-06:files 项内增加来源/类型元信息(如 ref_kind / ref_source),用于区分“引用文件”与“上传文件”,保持与现有渲染链路兼容。
  • D-07: 采用软失败:引用项失效时自动剔除并给出 toast不阻止整条消息发送。
  • D-08: 每条消息最多允许 10 个引用文件,超限时给出提示并阻止继续添加。

Claude's Discretion

  • @ 候选面板的具体键盘交互细节上下选择、回车确认、Esc 关闭)的实现方式。
  • chip 的具体视觉样式与动画,不改变已确认交互语义。
  • ref_kind / ref_source 的精确字段命名(前提是语义清晰且不破坏现有消费逻辑)。

Deferred Ideas (OUT OF SCOPE)

  • 跨线程/全局文件引用能力(可作为后续独立 phase
  • 基于语义检索或标签检索的高级文件查找(超出本阶段范围)。 </user_constraints>

Project Constraints (from CLAUDE.md)

  • 仓库根目录未发现 CLAUDE.md,无额外项目级强制约束可继承。[VERIFIED: codebase grep]
  • 仓库根目录未发现 AGENTS.md,无额外项目级指令文件可继承。[VERIFIED: codebase grep]
  • 未发现 .claude/skills/.agents/skills/ 项目技能目录。[VERIFIED: codebase grep]

Summary

本阶段最稳妥方案是“仅在现有输入与提交链路上加一层 thread-scoped 引用状态”,不改后端主契约、不引入新存储:InputBox/PromptInputTextarea 负责 @ 触发与候选选择,useThreadStream.sendMessage 继续作为唯一提交汇总点,把“上传文件 + 引用文件”统一写入 additional_kwargs.files。[VERIFIED: codebase grep]

当前代码已具备三块可复用能力1) 输入框附件管理与提交 (PromptInput/PromptInputMessage)2) 当前线程 artifacts 来源 (thread.values.artifacts)3) 当前线程 uploads 查询 API (/api/threads/{threadId}/uploads/list);因此本 phase 核心是“状态拼接与交互补全”,而不是基础设施建设。[VERIFIED: codebase grep]

约束上最关键的是 D-09 与 D-05候选面板必须基于现有 dropdownRadix 封装)实现,且最终协议必须落到 additional_kwargs.files,这意味着应避免“独立 mention payload”或“自绘浮层”两类分叉实现。[VERIFIED: codebase grep][CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu]

Primary recommendation:InputBox 增加 referencedFileschip 状态)+ dropdown 候选层,在 useThreadStream 合并为单一 additional_kwargs.files 提交,并为失效引用执行发送前软剔除。[VERIFIED: codebase grep]

Standard Stack

Core

Library Version Purpose Why Standard
@radix-ui/react-dropdown-menu 2.1.16 (project) / 2.1.16 (latest) @ 候选弹层、焦点管理、键盘导航 仓库已封装 components/ui/dropdown-menu.tsx,且官方支持完整键盘导航与焦点管理。[VERIFIED: npm registry][VERIFIED: codebase grep][CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu]
@tanstack/react-query 5.90.17 (project) / 5.99.0 (latest) 复用 uploads 列表查询缓存与失效机制 现有 useUploadedFiles 已标准化 thread 级文件查询,不应手写请求状态机。[VERIFIED: npm registry][VERIFIED: codebase grep]
sonner 2.0.7 (project) / 2.0.7 (latest) 软失败 toast引用失效/超限) 现有错误提示链路已统一使用 toast.error,保持一致性最小回归。[VERIFIED: npm registry][VERIFIED: codebase grep]

Supporting

Library Version Purpose When to Use
react 19.0.0 (project) / 19.2.5 (latest) 输入态、候选态、chip 态管理 本 phase 只做组件内状态扩展,不做 React 升级。[VERIFIED: npm registry][VERIFIED: codebase grep]
Internal: PromptInput + useThreadStream current repo 输入与提交主链路 所有 @ 行为应挂接在该链路,避免并行提交路径。[VERIFIED: codebase grep]

Alternatives Considered

Instead of Could Use Tradeoff
Dropdown 组件 自定义绝对定位浮层 违背 D-09且会重复处理焦点/键盘/关闭行为。[VERIFIED: codebase grep]
additional_kwargs.files 统一提交 新增 mentions 顶层字段 违背 D-05增加后端与渲染兼容风险。[VERIFIED: codebase grep]
thread 范围候选 全局文件池检索 违背 D-01范围失控并引入权限语义。[VERIFIED: codebase grep]

Installation:

# 本 phase 无需新增依赖

Version verification:

  • npm view @radix-ui/react-dropdown-menu version time --json → latest 2.1.16。[VERIFIED: npm registry]
  • npm view @tanstack/react-query version time --json → latest 5.99.0(项目当前 5.90.17)。[VERIFIED: npm registry]
  • npm view sonner version time --json → latest 2.0.7。[VERIFIED: npm registry]
  • npm view react version time --json → latest stable 19.2.5(项目当前 19.0.0)。[VERIFIED: npm registry]

Architecture Patterns

frontend/src/components/workspace/
├── input-box.tsx                  # @ 触发、候选 dropdown、chip 交互
frontend/src/components/ai-elements/
├── prompt-input.tsx               # 输入事件钩子onChange/onKeyDown扩展点
frontend/src/core/threads/
├── hooks.ts                       # 发送前合并 uploads + refs -> additional_kwargs.files
frontend/src/core/messages/
├── utils.ts                       # FileInMessage 类型扩展与兼容解析

Pattern 1: Thread-Scoped Candidate Aggregation

What: 候选集合 = thread.values.artifacts + useUploadedFiles(threadId),在前端归一为统一候选结构(含 displayName/type/pathTail/source)。[VERIFIED: codebase grep]
When to use: 每次输入框出现 @ 触发态时。
Example:

// Source: frontend/src/components/workspace/chats/chat-box.tsx
// Source: frontend/src/core/uploads/hooks.ts
const artifactPaths = thread.values.artifacts ?? [];
const { data: uploads } = useUploadedFiles(threadId);
const candidates = normalizeCandidates(artifactPaths, uploads?.files ?? []);

Pattern 2: Chip State Separate from Raw Text

What: @ 选择结果保存在独立 referencedFiles 状态,不把 @xxx 文本作为真实提交依据。
When to use: 处理删除、去重、同名文件 disambiguation、上限控制。
Example:

type ReferencedFile = {
  key: string; // source + path
  filename: string;
  path: string;
  ref_source: "artifact" | "upload";
  ref_kind: "mention";
};

Pattern 3: Single Submit Envelope

What: 发送前把“已上传附件 + 引用文件”统一组装为 additional_kwargs.files
When to use: useThreadStream.sendMessagethread.submit 前。
Example:

// Source: frontend/src/core/threads/hooks.ts
const filesForSubmit = [...uploadedFiles, ...referencedFiles].slice(0, 10);
await thread.submit({
  messages: [{ type: "human", content, additional_kwargs: { files: filesForSubmit } }],
});

Pattern 4: Soft-Fail on Stale References

What: 提交前校验引用项是否仍存在;失效则自动移除并 toast不中断文本发送。
When to use: 后端提交前最后一步校验。
Example:

const { validRefs, staleRefs } = validateRefs(referencedFiles, latestCandidates);
if (staleRefs.length) toast.error("部分引用已失效,已自动移除");

Anti-Patterns to Avoid

  • 自定义浮层替代 dropdown: 违反 D-09并引入焦点逃逸/关闭行为缺陷风险。[VERIFIED: codebase grep]
  • 把引用仅编码进纯文本 @文件名: 无法稳定区分同名文件,且删除/失效处理困难。[VERIFIED: codebase grep]
  • 新增并行提交结构(如 mentions: 与当前渲染和兼容链路分叉,违反 D-05。[VERIFIED: codebase grep]

Don't Hand-Roll

Problem Don't Build Use Instead Why
候选面板交互 自写键盘导航/焦点环 DropdownMenu (Radix) 官方能力已覆盖焦点与键盘导航,重造成本高且易出无障碍缺陷。[CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu]
线程文件查询缓存 手写 fetch + useEffect 缓存层 useUploadedFiles + React Query 现有 query key 与失效逻辑已稳定使用于 uploads 领域。[VERIFIED: codebase grep]
文件渲染协议 新建消息文件协议 复用 additional_kwargs.files 现有 message-list-itemmessages/utils 已消费该结构。[VERIFIED: codebase grep]

Key insight: 本 phase 的复杂度主要来自“交互状态一致性”不是“API 能力缺失”;复用现有协议可显著降低回归面。[VERIFIED: codebase grep]

Common Pitfalls

Pitfall 1: IME 输入法与 @ 触发冲突

What goes wrong: 中文输入组合态误触发候选面板。
Why it happens: 仅监听按键,不区分 isComposing
How to avoid: 与现有 Enter 逻辑一致,基于 isComposing / nativeEvent.isComposing 保护 @ 触发。[VERIFIED: codebase grep]
Warning signs: 中文拼写期间面板闪烁或误选。

Pitfall 2: 同名文件引用歧义

What goes wrong: report.md 来自 artifact 还是 upload 无法区分。
Why it happens: 候选展示缺少 path/source。
How to avoid: 候选项固定显示“文件名 + 类型 + 路径尾段(或来源标签)”。[VERIFIED: codebase grep]
Warning signs: 选中后 chip 文案无法回溯来源。

Pitfall 3: 发送时覆盖已有上传文件

What goes wrong: 引用文件写入后把上传文件挤掉。
Why it happens: 覆盖赋值而非合并数组。
How to avoid:hooks.ts 保持统一 mergeuploads first, refs append, 统一上限)。[VERIFIED: codebase grep]
Warning signs: 上传成功但消息只显示引用 chip。

Pitfall 4: 失效引用阻断发送

What goes wrong: 单个引用失效导致整条消息失败。
Why it happens: 抛异常中断提交。
How to avoid: 执行 D-07 软失败策略:剔除失效项 + toast + 继续发送文本。[VERIFIED: codebase grep] Warning signs: 用户可复现“删了附件后消息无法发送”。

Pitfall 5: Backspace 删除行为冲突

What goes wrong: 空输入框按退格时,附件与引用 chip 删除顺序混乱。
Why it happens: 当前 Backspace 已绑定附件删除,需要定义 chip 优先级。[VERIFIED: codebase grep]
How to avoid: 统一规则(建议:先删引用 chip再删附件。[ASSUMED]
Warning signs: 用户感觉“按一次退格删错对象”。

Code Examples

Verified patterns from official sources:

1) Dropdown 基础结构(用于 @ 候选)

// Source: https://www.radix-ui.com/primitives/docs/components/dropdown-menu
<DropdownMenu open={open} onOpenChange={setOpen}>
  <DropdownMenuTrigger asChild>
    <button type="button">Trigger</button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" sideOffset={4}>
    {items.map((item) => (
      <DropdownMenuItem key={item.key} onSelect={() => select(item)}>
        {item.label}
      </DropdownMenuItem>
    ))}
  </DropdownMenuContent>
</DropdownMenu>

2) 现有提交结构(需保持兼容)

// Source: frontend/src/core/threads/hooks.ts
await thread.submit({
  messages: [
    {
      type: "human",
      content: [{ type: "text", text }],
      additional_kwargs: filesForSubmit.length > 0 ? { files: filesForSubmit } : {},
    },
  ],
});

3) 现有消息文件消费(需兼容)

// Source: frontend/src/components/workspace/messages/message-list-item.tsx
const files = message.additional_kwargs?.files;
if (Array.isArray(files) && files.length > 0) {
  return <RichFilesList files={files as FileInMessage[]} threadId={threadId} />;
}

State of the Art

Old Approach Current Approach When Changed Impact
从消息正文解析 <uploaded_files> 标签 优先使用 additional_kwargs.files 结构化字段;仅保留正文解析作为兼容回退 精确时间未知(代码中已存在回退逻辑) 新功能应继续写结构化字段,避免文本协议漂移。[VERIFIED: codebase grep]

Deprecated/outdated:

  • 仅依赖 <uploaded_files> 文本标签作为主数据源:当前属于兼容路径,不应作为新功能主路径。[VERIFIED: codebase grep]

Assumptions Log

# Claim Section Risk if Wrong
A1 后端对 additional_kwargs.files 中新增 ref_kind/ref_source 字段是前向兼容(忽略或透传) Architecture Patterns / Standard Stack 若不兼容,将导致提交失败或渲染异常
A2 空输入框 Backspace 的“先删引用 chip 再删附件”顺序是更符合用户预期的规则 Common Pitfalls 若预期相反,会造成交互争议,需要产品确认

Resolved Questions

  1. ref_kind/ref_source 的最终字段名与枚举值

    • Resolution: 保持 ref_kind: "mention"ref_source: "artifact" | "upload",不再改名。
    • Why resolved: Phase 6 已有计划与验证链路都围绕这两个字段展开,且提交契约仍固定落在 additional_kwargs.files,符合 D-05/D-06。[VERIFIED: 06-01-PLAN, 06-VERIFICATION]
    • Planning impact: gap-closure 只允许补强验证与 UI 去歧义,不再重新设计字段名。
  2. 同名同路径尾段时的最终去歧义显示

    • Resolution: 固定为“文件名 + 类型徽标 + 路径尾段”,若路径尾段仍冲突,再附加 source 徽标作为第四层提示,但不替代“类型”维度。
    • Why resolved: 这与锁定决策 D-04 完全对齐,也正是 06-05 要关闭的 verification gap。
    • Planning impact: 06-05 必须在候选与已选引用预览中都兑现该展示合同,不允许回退为仅 pathTail/ref_source

Environment Availability

Dependency Required By Available Version Fallback
Node.js 前端构建/测试 v24.14.0
npm registry 校验/脚本 11.9.0
pnpm 项目脚本执行 10.32.1 npm(不推荐,锁文件不同)
Playwright CLI E2E 验证 1.59.1 仅做单测/静态检查(覆盖不足)
Frontend dev server (127.0.0.1:3000) 本地 E2E 运行 启动 pnpm --dir frontend dev
Backend API (127.0.0.1:8000) uploads/artifacts 联调 启动后端服务或使用 mock 断言

Missing dependencies with no fallback:

  • CLI 工具均可用)。[VERIFIED: local command]

Missing dependencies with fallback:

  • 本地前后端服务当前未运行,可通过启动命令补齐。[VERIFIED: local command]

Validation Architecture

Test Framework

Property Value
Framework Playwright 1.59.1 + existing unit tests (*.test.ts/.mjs)
Config file frontend/playwright.config.ts
Quick run command pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts
Full suite command pnpm --dir frontend test:e2e

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
D-01/D-02 @ 仅展示当前线程候选并可过滤 e2e pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts -g "@候选" Wave 0
D-03/D-08 选中后显示 chip最多 10 个 e2e pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts -g "DF-INPUT-009" 由 06-05 落地
D-05/D-06 提交落入 additional_kwargs.files 且含来源元信息 unit/integration pnpm --dir frontend node --test frontend/src/core/threads/hooks.test.ts (需扩展用例)
D-07 失效引用软失败,不阻断发送 e2e pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts -g "stale ref" Wave 0

Sampling Rate

  • Per task commit: pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts
  • Per wave merge: pnpm --dir frontend test:e2e
  • Phase gate: Full suite green before /gsd-verify-work

Wave 0 Gaps

  • frontend/src/core/threads/hooks.test.ts — 已覆盖 uploads+refs 合并与 soft-fail 场景断言06-01 / 06-03
  • frontend/tests/e2e/input-and-compose.spec.ts — 已作为主 E2E 文件承接 D-01~D-0806-05 继续补稳 DF-INPUT-008/009。
  • frontend/src/core/messages/utils.ts 契约验证 — 由 06-01 类型契约与 hooks 单测共同覆盖,不再拆独立测试文件。

Security Domain

Applicable ASVS Categories

ASVS Category Applies Standard Control
V2 Authentication no 由现有会话体系负责(本 phase 不新增认证机制)
V3 Session Management no 复用现有线程会话
V4 Access Control yes 严格 thread 范围候选来源artifacts/uploads with threadId
V5 Input Validation yes 前端仅提交候选池中的受控文件元数据,不信任自由文本路径
V6 Cryptography no 本 phase 不引入加密实现

Known Threat Patterns for frontend mention-reference flow

Pattern STRIDE Standard Mitigation
跨线程文件枚举IDOR Information Disclosure 候选源仅取当前 threadId 的 artifacts/uploads禁止全局检索
客户端伪造文件路径 Tampering 提交前按候选池二次校验,失效项软剔除
文件名注入 UI异常字符 Tampering 渲染时只做文本展示,不执行 HTML沿用现有 React 转义
超量引用导致 UI/消息膨胀 Denial of Service 强制上限 10 并阻止继续添加

Sources

Primary (HIGH confidence)

  • frontend/src/components/workspace/input-box.tsx - 输入框组合、提交入口、附件 UI。[VERIFIED: codebase grep]
  • frontend/src/components/ai-elements/prompt-input.tsx - 文本/附件状态、键盘行为、PromptInputMessage。[VERIFIED: codebase grep]
  • frontend/src/core/threads/hooks.ts - additional_kwargs.files 提交与上传流程。[VERIFIED: codebase grep]
  • frontend/src/components/workspace/messages/message-list-item.tsx - additional_kwargs.files 渲染消费。[VERIFIED: codebase grep]
  • frontend/src/core/messages/utils.ts - FileInMessage 与兼容解析(含 <uploaded_files> 回退)。[VERIFIED: codebase grep]
  • frontend/src/core/uploads/api.ts / frontend/src/core/uploads/hooks.ts - 当前线程 uploads API 与 query 封装。[VERIFIED: codebase grep]
  • npm registry (npm view ...) - 版本与发布时间校验。[VERIFIED: npm registry]

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

  • 无。

Metadata

Confidence breakdown:

  • Standard stack: HIGH - 主要基于仓库现有依赖与 npm registry 实时校验。
  • Architecture: HIGH - 关键链路(输入->提交->渲染)均在代码中可直接定位。
  • Pitfalls: MEDIUM - 大部分可由现有行为推导,个别交互优先级仍需产品确认。

Research date: 2026-04-15
Valid until: 2026-05-1530 天)