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

20 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 若预期相反,会造成交互争议,需要产品确认

Open Questions

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

    • What we know: 语义已锁定为“区分引用文件与上传文件”。[VERIFIED: codebase grep]
    • What's unclear: 后端/其他消费端对字段命名是否有硬编码。
    • Recommendation: 在 Phase 6 Plan 的 Wave 0 先做一次全仓搜索与联调验证,确定最终命名再实现。
  2. 同名同路径尾段时的最终去歧义显示

    • What we know: 决策要求展示“文件名+类型+路径尾段”。[VERIFIED: codebase grep]
    • What's unclear: 极端冲突(路径尾段仍相同)时的 fallback 文案。
    • Recommendation: 增加 source 徽标artifact/upload作为第三级 disambiguation。

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 "chip limit" Wave 0
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/tests/e2e/at-reference-files.spec.ts — 覆盖 D-01~D-08 的主流程
  • frontend/src/core/threads/hooks.test.ts — 增加 uploads+refs 合并与 soft-fail 场景断言
  • frontend/src/core/messages/utils 相关测试 — 校验新增元信息对渲染兼容性

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 天)