对话文件授权与桌面保存交互:RC 实施任务书
更新日期:2026-09-17(修订版 2:参考 Cherry Studio 的草稿附件与发送时入库流程)
适用项目:Infinia / FengYu 4.x,Vue 前端 + Electron 桌面 + Spring Boot 后端
编写时源码版本:4.0.0-rc.2(实施与发布时必须重新读取版本源)
状态:增量实施任务书;工作区已有部分相关实现,必须先审计差距。本文不证明现有实现正确、完整或获准发布。
1. 给实施 AI 的执行指令
请依据本文完成“RC 必做范围”,先检查真实源码,再按依赖顺序实施、补充行为测试、验证并更新用户文档。不要只输出建议,也不要只合并附件标签后宣布完成。
本文是一份待执行任务书。只有用户明确要求执行本文时,才开始修改应用代码。当前生成本文的请求仅授权创建方案文档。
执行时遵守以下规则:
- 阅读根目录及改动目录适用的
AGENTS.md,检查git status,保留用户已有修改。 - 本文中的文件路径用于定位,类名、接口名和数据结构建议不高于实际源码;先核实调用链和测试。
- 可以自行决定局部实现命名和拆分方式,但不得静默删减必做验收项。
- 不主动升级依赖、不改变插件公开协议、不顺带重写聊天或插件框架。
- 不自动提交、推送、打标签、发布或修改应用版本;这些操作需要用户另行提出。
- 修改插件契约或官方插件前读取
fengyu-plugin-dev;同步产品文档时读取docs-updater;只有实际执行发布时使用app-release。 - 若发现必做目标依赖大规模运行时重构,完成可独立验证的部分,报告具体阻塞、证据和剩余工作。不得把不完整实现描述为可发布。
- 本文不要求多代理执行。遵守当前会话对代理委派的限制。
最终交付至少包含:代码改动、必要测试、文档更新、验收结果、已知限制、RC 发布判断;未执行的检查必须明确列出。
2. 目标与 RC 决策
2.1 必须达到的用户结果
- 一个本地文件或文件夹只出现一次,不暴露内部插件 ID。
- 用户通过系统选择器添加草稿附件,选择阶段只记录描述,不生成插件授权;发送时创建只读副本,不再追加“授权给所有兼容插件”的确认卡片。
- 文件资源、输出位置和待处理操作属于具体对话;新增对话不继承它们。
- 旧对话后台执行不受新增、切换对话影响;回包不会落入另一个对话。
- 桌面生成结果直接保存到本地,提供打开和定位;不以“下载”作为桌面主流程。
- 后端拒绝跨作用域或已撤销资源引用;不依赖前端隐藏标签实现授权。
- 保存失败不能报告成功,也不能立即删除唯一的生成结果。
2.2 是否可以在 RC 修改
可以。授权跨对话残留应在正式版前修复;资源聚合、只读默认和本地保存闭环可以随修复进入下一 RC。
| 范围 | RC 决策 |
|---|---|
| 对话归属、异步竞争、统一回收、服务端校验 | 必做,正式版阻断项 |
| 单资源展示、删除冗余确认、资料与输出目录区分 | 必做 |
| 桌面保存、保存失败重试、打开与定位 | 必做;若无法验证,明确该交付仍未完成 |
| 任意文件原地编辑、目录实时同步、跨重启自动授权 | 不在本次范围 |
| 每对话独立 Worker、并发执行架构重构、逐工具动态沙箱重建 | 后续独立设计;不得混入本次 |
发布使用下一个未占用的 RC 编号。不得假定远端尚无 rc.3,不覆盖已分发 RC。新 RC 通过验收后才能评估正式版;不按等待天数替代测试证据。
3. 已核实的现状与代码入口
以下表格主要记录初始问题及定位入口,不代表当前文件仍保持原状。2026-09-17 工作区已存在相关未提交改动,务必以当前源码重新核实;具体增量入口见第 13 节。
| 入口 | 现状 / 检查重点 |
|---|---|
frontend/src/views/AiChat.vue | 先选择再确认;直接逐条展示 activeFiles 和 pluginId;异步选择与确认返回的归属 |
frontend/src/stores/aiSession.ts | 共享 activeFiles;busy 时跳过清理;空对话复用;聊天回包追加授权 |
frontend/src/api/client.ts、types.ts | 原生路径、上传、撤销和聊天 DTO;目录默认可写 |
frontend/src/mf/desktop.ts | 桌面检测、文件/目录选择器封装 |
frontend/src/i18n/{en,zh}.json | 添加资源、资源权限、保存状态、错误文案 |
FengYu/.../web/controller/AiFileController.java | 文件授权入口,对话作用域绑定 |
FengYu/.../web/controller/AiController.java | 客户端引用、文本路径引用、暂存输出及终止回收 |
FengYu/.../web/controller/ConversationController.java | 持久化、删除对话与资源清理关联 |
FengYu/.../ai/ChatFileGrantService.java | 每个兼容插件生成独立引用;文本路径授权;暂存输出导出 |
FengYu/.../ai/ChatFileContext.java、AiToolFileInjector.java | 当前轮资源注入与解析 |
FengYu/.../ai/config/AiToolRegistry.java | 插件调用入口、默认输出位置、Flow 上下文 |
FengYu/.../plugin/runtime/PluginFileGrantService.java | 不透明引用、快照、读写目录集合、撤销、授权版本 |
FengYu/.../plugin/runtime/PluginProcessManager.java | 按插件复用进程,权限版本变化触发重启,沙箱根目录聚合 |
desktop/electron/src/ipc/dialog.ts、window/preload.ts | 系统对话框、窄范围 IPC 桥接 |
frontend/src/components/agent/FlowChatPanel.vue、FlowRunDialog.vue | 共享文件 API 的其他调用方,必须兼容 |
上表中的 FengYu/.../ 表示 FengYu/src/main/java/fan/summer/fengyu/。
重要事实:
- 只读原生资源当前使用副本快照;不能承诺外部修改自动同步。
- 可写原生目录当前可以直接指向真实目录;本次聊天新交互优先使用宿主保存输出,避免直接扩大 Worker 的本地写权限。
FileRef目前主要验证插件、引用和权限属性;需要添加聊天作用域与本轮成员校验。- 底层可访问根目录按插件聚合;添加
conversationId不自动构成操作系统级跨对话隔离。 - 权限版本变化可能重启 Worker;Excel/Python 等多步操作状态必须保护。
- 部分暂存导出失败路径只记录日志,之后仍清理暂存;保存闭环必须改变这个行为。
4. 产品行为契约
4.1 资源入口
“+”菜单:
- 添加文件:系统文件选择器,默认只读。
- 添加资料文件夹:系统目录选择器,默认只读,明确包括子目录。
- 设置输出文件夹:系统目录选择器,授权宿主在该目录保存生成结果。
入口文案承担权限说明。输入文件/资料目录选择完成后只加入所属草稿的附件描述,不调用创建快照或插件授权的接口;发送时才创建资源副本。输出目录选择是独立的宿主保存授权,可当场登记,但不能创建 Worker 授权。取消选择不产生资源或授权,不再出现一次笼统的“批准发送”。不要求用户选择插件。
文件夹受现有数量、大小和路径安全限制;错误明确说明,禁止无提示截断。空输出目录必须支持,不能用“读取目录快照”的限制阻止选择空输出位置。
4.2 展示与交互
发送前:
[文件 contacts-template.csv · 只读 ×]
[文件夹 参考资料 · 只读 ×]
[保存到:报表输出 ▾]
发送后:
用户消息保留附件记录
输入区附近:[本对话资源 2 项 ▾] [保存到:报表输出 ▾]
输出结果:
报表.xlsx · 已保存
/用户选择的位置/报表.xlsx
[打开] [在 Finder 中显示]- 非 macOS 使用对应平台文案“在文件夹中显示”。
- 内部插件 ID 不出现于主界面;详情可显示实际使用过资源的工具名称。
- 详情展示来源、只读状态、快照说明、刷新、移除。
- 不同目录的同名文件保持独立,必要时显示父目录名消歧。
- 草稿移除表示取消添加;已发送后移除表示禁止后续访问,历史消息保留“已移除”记录。
- 不承诺移除能清除模型上下文或工具已经读取的数据。
- “请求批准”仍管理工具操作审批;切换批准模式不扩展资源范围。
4.3 新建与切换
- 新增对话显示空草稿、空资源、空输出位置和空待批准列表。
- 只有已加载、无消息、无草稿、无资源、无输出目标、无选择/上传操作、无执行任务的对话才可作为纯空白对话复用。
- 未加载历史消息的对话不能因为
turns.length === 0被判定为空白。 - 有附件的空草稿在新建时保留在原对话,不作为空白复用;明确删除原对话才清理。
- 普通切换保留原对话资源和草稿。所有弹窗、回包、批准动作绑定发起时对话。
- 沿用现有生成并发限制,不顺带支持多对话并行生成。
5. 数据模型与授权契约
5.1 推荐逻辑模型
名称可以调整,但各层职责不可合并为一个全局数组。
type ResourceRecord = {
resourceId: string
scopeId: string
name: string
kind: 'file' | 'directory'
purpose: 'input' | 'output'
access: 'read' | 'save-output'
revision: number
status: 'preparing' | 'ready' | 'revoked' | 'expired' | 'failed'
source: 'desktop-native' | 'browser-upload'
// 展示信息不是授权凭证;真实路径与引用由服务端管理。
}
type PendingOperation = {
operationId: string
scopeId: string
draftRevision: number
}
type TurnResourceLease = {
executionId: string
scopeId: string
resourceVersions: Array<{ resourceId: string; revision: number }>
}scopeId在选择资源前建立,推荐由后端生成并登记为当前应用会话拥有的作用域。- 数据库
conversationId可以后续关联,不使用全局空值作为所有未发送草稿的共同作用域。 - 本地 UI 数字 ID、数据库 ID、后端作用域 ID 分清用途;前端随意拼出的 ID 不自动代表所有权。
- RC 可保留“一个资源对应多个插件 FileRef”的内部结构,但这些引用由资源记录统一拥有和回收。
- 不再用
pluginId + 文件名作为资源替换依据。 - 原生资源身份由服务端规范化路径与来源判定;不要简单小写所有路径。浏览器资源使用上传资源 ID,不仅按文件名或大小去重。
5.2 API 设计要求
实施者根据当前路由风格设计确切路径,建议覆盖以下操作:
| 操作 | 契约 |
|---|---|
| 创建作用域 | 返回服务端登记的作用域标识;可关联尚未持久化的草稿 |
| 准备发送资源 | 选择阶段不调用;发送时接收作用域、发送事务 ID、附件描述,创建一组可回收的副本记录;提交后成为正式消息资源 |
| 列出资源 | 仅返回该作用域的资源与保存目标 |
| 刷新只读资源 | 成功后原子替换版本;失败保留可用旧版本 |
| 移除资源 | 幂等;先禁用后续使用,再按执行引用计数回收 |
| 发起聊天 | 携带作用域及资源 ID/版本;服务端解析 FileRef,拒绝其他作用域资源 |
| 保存结果 | 通过已登记产物及输出目标操作,不接受任意源路径作为可信产物 |
| 关闭作用域 | 禁止新操作,终止或安全收束执行,回收所有资源 |
客户端不得靠更换 scopeId 重绑一个已有资源。校验作用域属于当前认证上下文、资源归属、状态、版本和用途。若现有产品是单用户本地认证,沿用其身份模型,不凭空引入账户系统。
旧文件接口被 Flow 等模块共享:可以新增聊天专用聚合接口,或显式增加所有者类型。不能把所有旧请求默认归入“当前聊天”。Flow 仍按运行归属管理。旧聊天兼容入口不得绕过新校验;旧页面需要刷新时明确报错,不静默退回无作用域授权。
5.3 不变量
- 所有资源有明确逻辑所有者;消息/对话引用拥有副本,执行任务持有独立访问租约。副本所有权不能等同于 FileRef 生命周期。
- 页面切换不改变所有权。
- 一个执行固定使用发起时资源版本,刷新不改写正在执行的快照。
- 移除后不接受新调用;已开始的调用可安全结束,释放后物理清理。
- 撤销网络失败不等于后端已撤销:前端保留待清理状态/重试,后端提供过期回收。
- 授权不可由模型输出、工具响应、附件正文或引用文档中的指令创建。
- 用户明确提供的路径也必须进入同一资源登记流程。仅引用或讨论路径不得被解释为写入授权。
- “保存输出”不授予 Worker 对整个本地目录的删除、重命名或任意修改权限。
6. 异步与执行状态机
6.1 资源操作
发起选择(conversationKey, operationId, draftRevision)
→ 系统选择器返回文件描述
→ 检查原草稿与操作是否仍有效
→ 只向原草稿追加 DraftAttachment,不创建插件授权
→ 用户发送:固定草稿快照并创建服务端发送事务
→ 副本准备完成后提交原对话消息
→ 清理事务已消费的原草稿附件;失败保留草稿不要在回调中读取 activeId 决定归属。取消一个组件中的 pending 状态不足以取消网络请求,必须处理延迟成功回包。
新增、切换后仍允许有效选择完成到原草稿;删除或取消后的选择结果丢弃。发送准备产生的迟到资源由原事务接管或回收。网络重试用发送事务 ID 去重,避免重复复制文件或提交消息。
6.2 执行租约
准备并提交本轮消息资源 → 校验作用域与资源 → 获取执行租约 → 固定本轮引用 → 执行
→ 成功 / 失败 / 取消 / 超时 / 断连
→ 幂等释放租约
→ 回收无所有者、无执行持有的引用POST 成功但 SSE 从未连接、重复终止回调、取消与完成竞争都需要覆盖。不能依赖前端一定发送清理请求。
新对话应立刻呈现为空;旧任务仍写回原对话。全局 busy 若保留,应只表示现有执行限制,不用于决定文件属于谁。
6.3 Worker 与沙箱边界
本阶段先实现宿主级作用域校验,但必须同时审计运行时实际可访问目录。
- 不允许把所有闲置对话的可写本地目录长期加入同一插件 Worker。
- 输出目录由宿主写入,Worker 只访问受控暂存;这是本阶段避免扩大写权限的主要路径。
- 活动资源的授权创建、切换、撤销在安全执行边界处理;不要在多步任务中逐次修改授权集合触发进程重启。
- 若共享 Worker 会保留旧作用域的文件内容或根目录权限,必须在作用域切换的安全边界清理/重建,或明确当前隔离不能满足目标并阻止发布。
- 审计 Flow、插件 UI 与聊天共用同一 Worker 的情况。不能仅凭聊天全局 busy 推断系统不存在其他执行。
- 不在 RC 新增并发 Worker 池;复用已有调度/锁,必要时串行化冲突执行,并给出明确等待状态。
- 不宣称新增一个
scopeId字段就提供 OS 级或恶意插件隔离。报告实际实现的边界和测试证据。
7. 桌面输出保存闭环
7.1 输出目标
每个对话 RC 最多一个输出目标,初始为空。用户主动设置后复用;切换目标只影响后续执行,当前执行固定持有原目标。记住系统选择器上次所在目录只是一项便利设置,不能自动授权新对话。
未设置目标时:用户手势触发系统目录选择器,避免重复或后台突然弹窗。若生成已经开始,先保留暂存产物,在原对话展示“选择保存位置”。取消选择不销毁产物。
7.2 产物状态
generating → ready-to-save → saving → saved
↘ save-failed → retry → saving- 每个产物有服务端登记 ID、执行归属、逻辑名称、暂存位置、保存状态与最终位置。
- 插件不支持完整产物事件时,可在任务结束后从已知暂存根安全收集普通输出文件;拒绝越界与链接,不从任意模型文本路径推断可信产物。
- 先落盘并确认结果,再报告
saved。文件生成成功和目标保存成功必须分开。 - 只允许从本执行登记的暂存产物复制到已授权目标;防止
..、符号链接、目录替换和路径逃逸。 - 写入使用目标文件系统内的临时文件后替换等可靠方式;跨文件系统不能假定 rename 原子可用。失败不得破坏已有文件。
- 默认同名保留两份,原子地避免竞争覆盖;显式请求覆盖按批准模式处理,不能静默
REPLACE_EXISTING。 - 多文件保存按文件记录成功/失败,重试不重复覆盖已成功文件;不假装整个批次具备事务原子性。
- 取消不强制导出未完成结果;已完成产物可保留为未保存状态。删除对话时清理这些暂存产物。
7.3 失败保留与清理
失败/取消选址的结果应从普通“终止即删除”的 staging 生命周期中分离。选用应用管理的待保存产物目录及清单,避免启动清扫误删。
建议默认保留 7 天,并设置总容量限制和用户可见的清理入口;具体限额根据现有存储设置选取并文档化。容量不足时明确失败,不静默删除尚在重试的唯一结果。保存成功后清理暂存副本。
跨重启恢复待保存产物只恢复内容与状态,不恢复原目录写入授权。用户重新选择保存位置后才能重试。
7.4 Electron 桥接
增加或复用窄范围的本地产物操作:打开、在文件管理器中显示。同步 preload、前端声明、封装、IPC 和测试。
- 校验调用来源为可信应用渲染器;不能让任意插件 iframe 获取通用文件系统桥接。
- 使用可验证的产物登记信息解析路径;不要把 renderer 传入的任意字符串直接交给 shell。
- 本地打开只处理存在的受支持产物,拒绝 URL/自定义协议;脚本、可执行文件默认仅定位,不提供直接执行入口。
- 处理
openPath等返回的错误,定位/打开失败不能显示成功。 - 老 preload 缺少新方法时显示不可用或升级提示,不抛未处理异常,也不退回不受限 URL 打开。
- Web 端继续使用浏览器上传和保存能力,隐藏桌面专用入口;不得把本机路径当作浏览器可读取文件。
8. 按依赖顺序实施
阶段 A:复现与契约
- 阅读第 3 节入口、现有测试和实际 API DTO。
- 复现:添加未发送后新增、生成中新增、延迟授权后新增、同名文件替换。
- 梳理资源、执行、Worker、Flow 的当前所有权;确定作用域与产物 DTO。
- 先补会失败的行为测试,锁定跨对话与延迟回包问题。
完成条件:有可重复失败用例与明确状态转换,未修改公开插件协议。
阶段 B:后端归属与回收
- 引入作用域和聚合资源登记,支持首次发送前创建;本地选择可以先使用稳定的本地草稿标识,开始准备发送或设置输出目标时再取得服务端 scope。
- 输入选择只保留草稿描述;发送时对原生路径、浏览器上传及明确用户路径创建宿主资源副本,登记明确所有者。
- 分离副本所有权和插件引用;聊天入口、工具引用注入与实际解析校验作用域、本轮成员和资源版本。
- 统一幂等撤销、执行租约、异常与超时回收。
- 保持 Flow 运行授权语义,覆盖旧 API 调用方。
- 在安全边界处理 Worker 权限变化,验证多步工具状态。
完成条件:跨作用域引用与撤销后调用被服务端拒绝;资源不依赖前端才能回收。
阶段 C:前端归属与资源展示
- 把资源、草稿、输出位置、pending 操作移动到对话状态;去除共享数组的所有者角色。
- 当前资源列表改为活动对话的派生视图。
- 修复纯空白对话判定与异步回调归属;所有回包更新捕获的原对话。
- 一个资源一张标签,按资源整体移除;去掉插件 ID 与冗余确认卡片。
- 添加详情、快照说明、刷新、权限与错误状态,补中英文文案。
完成条件:前端验收矩阵通过,旧任务与新对话展示互不污染。
阶段 D:桌面保存与产物
- 输出目标与只读资料分开建模,接入系统目录选择器。
- 复用受控暂存,增加产物状态与宿主保存结果。
- 修复失败即清理问题,完成重试、重名和容量处理。
- 完成打开/定位的受限 IPC、能力检测和错误反馈。
- Web 保持兼容,桌面不出现主流程下载按钮。
完成条件:真实本地文件保存与失败恢复验证通过,权限不因设置输出目录扩大到 Worker。
阶段 E:回归、文档与交付
- 执行第 9、10 节检查;修复失败后只重跑受影响检查。
- 使用
docs-updater更新相关中英文用户文档及 CHANGELOG;生成镜像,不手改 changelog 镜像。 - 汇总实现与范围差异、测试结果、剩余风险、RC 判定。
- 保持未发布、未打标签;实际发布等待用户指令。
9. 必须覆盖的验收矩阵
| ID | 场景 | 预期 |
|---|---|---|
| A01 | CSV 兼容多个插件 | 只展示一个资源,整体移除回收全部引用 |
| A02 | 不同目录同名文件 | 资源独立,内容不互相替换 |
| A03 | 同一路径重复添加/重试 | 去重或刷新版本,不泄漏授权 |
| A04 | 已有附件但未发送时新增 | 新对话为空,原草稿资源保留在原对话 |
| A05 | 未加载历史消息时新增 | 不复用该历史对话为空白 |
| A06 | 生成中新增/切换 | 原执行和结果归属不变,新对话无旧权限 |
| A07 | 系统选择器/上传/授权延迟返回 | 回到原对话;已废弃操作的资源被回收 |
| A08 | 聊天接口延迟返回文本路径授权 | 不添加到当前新对话 |
| A09 | 选择期间删除原对话 | 回包不能复活对话或资源 |
| A10 | 刷新资源期间原任务执行 | 原任务使用旧版本,后续轮次使用新版本 |
| B01 | 跨作用域伪造/重用引用 | 服务端拒绝,不能仅前端过滤 |
| B02 | 移除后新工具调用 | 被拒绝;既有调用安全释放 |
| B03 | POST 后未连接 SSE | 超时回收,没有孤立执行租约 |
| B04 | 完成/取消/断连重复回调 | 幂等清理,不重复导出或提前删结果 |
| B05 | 撤销请求网络失败 | 显示状态、重试;服务端过期回收 |
| B06 | 附件正文/模型输出包含授权指令 | 不建立文件权限 |
| B07 | 只读资源被请求写入 | 被拒绝,不能靠工具参数提升权限 |
| B08 | 聊天与 Flow/插件 UI 共用 Worker | 没有跨作用域权限累积、错误重启或死锁 |
| B09 | 多步 Excel/Python 操作 | 不因中途权限变化丢失进程内状态 |
| C01 | 已有输出位置生成文件 | 直接保存本地,实际内容正确 |
| C02 | 无输出位置/取消选择 | 不循环弹窗,保留结果等待选择 |
| C03 | 空输出目录 | 可选择并成功保存 |
| C04 | 同名文件及并发冲突 | 不静默覆盖;保留两份或明确批准 |
| C05 | 无权限/磁盘满/目标被删除 | 标记失败,保留唯一结果,能重试 |
| C06 | 多文件部分保存失败 | 成功失败分开显示,重试不重复破坏成功项 |
| C07 | 路径穿越/符号链接/替换目标目录 | 拒绝越界写入 |
| C08 | 打开/定位及旧 preload | 平台行为正确,无未处理异常或任意路径入口 |
| C09 | 重启恢复历史与未保存产物 | 元数据/产物恢复;本地写权限不自动恢复 |
| D01 | 浏览器与 Flow 文件流程 | 不因聊天改造退化或被强制绑定当前对话 |
| D02 | macOS/Linux 桌面启动 | 现有稳定 E2E 通过 |
| D03 | 中英文、长文件名、窄窗口、键盘操作 | 可操作、可读、无明显布局溢出 |
单测应使用可控制的延迟 Promise、假时钟和显式状态,不依赖随机 sleep。文件系统测试使用临时目录,不能读取或写入用户真实资料。
10. 验证命令与证据
先阅读当前 package scripts、测试配置及 Maven 模块依赖,调整新增测试类名。以下是起点,不是假定已经执行的结果。
前端(工作目录 frontend/):
corepack yarn run test:unit src/stores/aiSession.test.ts
corepack yarn run typecheck
corepack yarn run build运行新增资源组件/状态测试;合入 RC 前执行完整 test:unit,以及被此次修改影响的其他测试脚本。
后端(仓库根目录):
./mvnw -f FengYu/pom.xml -Dtest=ChatFileGrantServiceTest,PluginFileGrantServiceTest,AiControllerChatGrantLeakTest,AiControllerFileContextTest test追加新增作用域、租约、保存失败恢复测试。若依赖需本地安装,按 README 使用 wrapper 补齐必要模块,不盲目运行全 reactor,也不以 -DskipTests 替代行为测试。
桌面(工作目录 desktop/electron/):
corepack yarn test
corepack yarn run build:ts
corepack yarn run test:e2e先核实 E2E 所需后端/前端构建。新增系统选择器测试采用受控替身;不要让真实系统弹窗或外部网址造成 CI 不稳定。不得启用 FENGYU_E2E_BROWSER_BRIDGE=1 作为普通发布测试的默认条件。
文档(工作目录 docs/,更新 CHANGELOG 后):
corepack yarn run sync:changelog
corepack yarn run build仓库根目录:
git diff --check实际发布另按 app-release 执行构建、审计、release contract、打包和 scripts/e2e-smoke.sh。裸 JAR smoke 不覆盖 Electron 链路;CI macOS/Linux 的稳定 launch E2E 必须通过。改了 electron-builder.yml 才需要同步相关包装契约断言,不能留下失配。
手动桌面验证至少记录:运行平台、应用构建、测试临时目录、选择/保存/失败重试结果,以及打开和定位的实际效果。没有测试的平台不得宣称已验证。
11. 历史数据、兼容与回滚
- 尽量使用向后兼容的可选资源元数据,不把进程内引用写进历史当作永久权限。
- 旧对话没有资源字段时正常加载为空;无稳定资源信息时显示旧附件不可用,不猜测路径恢复。
- 新字段若需要数据库迁移,使用仓库现有机制,提供旧数据测试;禁止删除或重写现有聊天记录。
- 应用热更新时考虑 SPA 与旧 preload 暂时不同步,用能力检测明确降级。
- RC 回滚保留原文件和聊天历史;资源缓存可以过期,但不能删除已保存用户产物。
- 不以回滚理由重启旧的跨对话共享授权路径。若新交互必须回退,授权归属修复应保持有效。
12. 完成标准与实施报告模板
以下条件全部满足才可标记本任务完成:
- [ ] RC 必做范围已实现,未把延后范围混入主交付。
- [ ] 新建/切换/删除/异步回包归属符合契约。
- [ ] 单资源展示、只读默认、输出目标分离生效。
- [ ] 后端跨作用域及撤销后调用测试通过。
- [ ] Worker、Flow 和多步工具状态经过实际回归。
- [ ] 本地保存、冲突、失败保留、重试、打开和定位闭环通过。
- [ ] 历史兼容、重启失效与未保存产物恢复符合设计。
- [ ] 中英文文档与 CHANGELOG 同步。
- [ ] 必要构建和检查有结果,未执行项如实披露。
- [ ] 未擅自提交、推送、改版本或发布。
实施 AI 最终报告格式:
已实现:说明用户可见结果及关键后端行为。
验证:列出实际执行的命令、结果和桌面场景。
授权边界:说明宿主校验、Worker 实际权限范围及残余限制。
范围差异:列出与任务书不同的实现和原因,不能省略未完成项。
RC 判断:可进入下一 RC / 不可进入,并给出具体证据或阻断项。
发布状态:未发布;若用户另行授权发布,引用相应发布结果。只有用户另行要求发布时,才读取最新版本和远端标签、按发布技能执行下一 RC。应用发布不修改独立插件工具链版本。
13. 修订版实施基线:必须增量改造现有代码
本节及后续章节明确修订版的技术契约。与此前方案的核心差别是:选择阶段只保存草稿描述;发送时创建宿主副本;每轮执行的 FileRef 单独建立和释放。 不能仍在选择时调用原来的 grant 接口,仅把界面文案改成附件。
2026-09-17 只读检查发现工作区已有下列改动,尚未在本次文档任务中验证:
ChatResourceScopeService:已有 scope、聚合资源、输出目标和 lease;addNative等目前会立即调用授权服务。ChatResourceController:已有聊天资源路由,需复用并调整语义,不另建第二套 scope 控制器。ChatArtifactStore、Electronipc/artifact.ts:已有产物管理与桥接,需逐条核对保存、归属、重启与清理契约。aiSession.ts:已有按对话的 draft/resources/scope 和附件操作计数;应扩展为独立待发送附件,不退回旧的全局数组。- 后端、前端、Electron 已有相关测试和中英文文档改动;保留并修正测试,不删除失败断言以迎合实现。
先输出简短差距表:目标 / 已有实现 / 缺口 / 修改入口 / 验证方式,然后实施。不能因为已有同名类就认定功能完成,也不能重置未提交修改来获得“干净基线”。当前代码状态只是审计入口,不是后续永久有效的事实。
13.1 Cherry 参考范围
参考提交:b112a46b0e2d93105540ddbc9234ce468e9e4b46,属于当时 main 源码,不代表其发布版。
- 附件选择按钮:原生多选后直接追加草稿。
- ComposerAttachment:轻量附件描述,和持久化文件记录分开。
- 发送时创建文件记录:发送时复制文件并关联消息。
- ChatComposer:按 topic/scope 管理草稿。
只借鉴职责拆分和交互,不复制其整套存储实现。其发送附件批处理源码也注明非原子失败可能残留,FengYu 必须按下一节补齐幂等与回收,不能照搬该限制。
14. 四类对象及明确存储责任
type DraftAttachment = {
attachmentId: string
conversationKey: string
kind: 'file' | 'directory'
name: string
source: 'desktop-native' | 'browser-file'
selectionHandle?: string // 推荐主进程登记的选择凭据,不是插件权限
displayPath?: string // 只用于展示,不构成服务端授权
selectedSize?: number
status: 'selected' | 'preparing' | 'failed' | 'unavailable'
}
type ResourceVersion = {
resourceId: string
scopeId: string
revision: number
snapshotId: string
status: 'prepared' | 'committed' | 'revoked' | 'expired'
}
type PreparedSend = {
sendId: string
scopeId: string
state: 'preparing' | 'prepared' | 'committed' | 'aborted' | 'expired'
resourceVersions: Array<{ resourceId: string; revision: number }>
}上面是逻辑类型,不要求逐字复制。浏览器 File 对象单独保存在会话内存映射中,不能 JSON 序列化后假装恢复了文件内容。
| 对象 | 存储位置 | 清理责任 |
|---|---|---|
| 草稿附件描述 | 对话草稿;浏览器二进制对象在内存 | 移除、发送成功后消费、删除草稿 |
| 宿主副本 | 独立资源存储;和 Worker runtime-files 清扫边界分开 | 无消息/对话引用且无执行持有后清理 |
| 执行 FileRef | 后端活动租约 | 执行终止幂等释放;不直接删除仍被消息引用的副本 |
| 生成产物 | 独立产物存储与状态清单 | 保存成功或按明确保留策略清理 |
实现要求:
- 使用仓库已有持久化机制保存附件元数据及消息关联,不能仅在会话内存显示已发送附件。
- 明确
releaseGrant与deleteSnapshot的不同职责。底层现有 owned 标志不足时补充资源所有者,不把仍引用中的副本交给通用 revoke 删除。 - 同一版本只创建一个逻辑副本;若既有沙箱要求插件专属物理副本,可以内部派生,但必须统一配额、归属和清理。禁止通过硬链接让可写 Worker 修改共享只读快照。
- 本轮结束释放权限后,对话资源仍可作为下一轮“继续”的输入重新派生权限。
- 跨重启保留副本与消息记录,但旧 FileRef 一律失效。恢复对话时资源显示待恢复访问;明确用户操作后重新建立引用,不自动恢复原目录写权限。
- 默认只读快照以发送准备期间读到的内容为准。发送前变化提示和发送失败时的原草稿必须保留;不承诺原文件实时同步。
- 对复制期间仍被外部程序修改的文件,检测可识别的大小/时间变化并拒绝或重试;不声称普通文件复制提供完整文件系统快照一致性。
15. 发送协议:避免重复消息、重复副本与草稿丢失
15.1 推荐操作
沿用现有 /api/ai/chat-resources 等命名空间,增加必要操作或在原接口中等价实现。不要把下列示意路径当成必须照抄的公开协议。
| 操作 | 输入 | 结果 |
|---|---|---|
| prepareSend | scope、sendId、原生选择凭据/上传内容、草稿附件 ID、已提交资源 ID/版本 | PreparedSend;新副本只归本事务所有,未向 Worker 授权 |
| commitSend / chat | scope、sendId、文本、固定资源集合、输出目标版本 | 唯一 message/stream/execution 标识;提交所有权并启动原对话执行 |
| getSendStatus | scope、sendId | 当前状态及已经生成的标识,供不确定结果恢复 |
| abortSend | scope、sendId | 未提交则回收;已提交不作为“取消任务”的替代接口 |
| cancelExecution | executionId | 沿用执行取消语义并释放 lease |
可以合并 prepare 与 chat 为服务端单一事务入口,但必须提供等价的幂等状态查询与不确定结果恢复能力。
15.2 完整顺序
- 捕获原对话、草稿版本、附件集合、文本和输出目标。生成一次性的
sendId。 - 防止重复点击产生第二次发送。准备期间可编辑新草稿,但不得修改已经捕获的发送载荷。
- 后端验证作用域和选择来源,对整个批次做路径、大小、数量、类型及配额校验。
- 创建宿主副本,按事务记录已完成条目;失败则回收本事务独占副本,不碰其他消息资源。
- 提交唯一用户消息及其资源关联;创建执行前确定本轮资源版本。
- 在安全调度边界获取执行租约、派生兼容插件的 FileRef、启动执行。
- 前端得到明确已接受结果后,只消费捕获的附件 ID 和对应草稿版本。不得把用户刚输入的新内容清空。
- 后续流失败也不撤销已经成功提交的消息;把失败展示在原消息上,允许显式重试执行。
- 执行结束释放 FileRef 与租约;副本依据消息引用保留。
15.3 幂等与异常规则
- 同一 sendId 同一载荷重复请求返回同一结果,不再次复制或发送模型请求。
- 同一 sendId 不同载荷拒绝,并返回可识别冲突错误。
- 连接超时不能被当作“未提交”。先查询状态,再决定重试或回收。
- 服务端已提交但前端未收到响应时,通过 sendId 找回消息和流,禁止新建重复消息。
- prepare 阶段失败保留草稿,标记具体失败附件;用户可移除失败附件后用新事务重试。
- prepare 已成功、前端退出或从未 commit:服务端 TTL 回收孤儿事务;活跃事务不能被清扫。
- 删除对话与 commit 竞争由服务端线性化:关闭后拒绝新提交,迟到副本安全回收。
- 提交后按 sendId 去重的记录至少覆盖客户端合法重试周期;不可在执行刚完成就删除。
- 不为了幂等把完整文件内容或敏感路径写进普通日志。
16. 原生选择与 Web 兼容的实现细节
16.1 桌面输入
- 扩展原生选择器支持文件多选;为旧调用方保留兼容封装或同步所有调用点。
- 推荐 Electron 主进程登记选择结果并返回不透明 selectionHandle 和展示元数据。其作用是证明选择来源,不开放 Worker 文件访问。
- 后端兑现选择凭据时通过可信桌面桥接校验,不信任 renderer 自称“用户选了此路径”。若复用当前可信路径机制,先确认其边界并记录理由,不能用可伪造字段代替凭据。
- 选择后立即进入草稿,禁止创建
FileRef、启动 Worker 或递归复制大型目录。 - 发送前主进程/后端重新校验存在性、真实路径与文件类型;失效凭据提示重新选择。
- 选择操作在对话切换后写回原草稿,在原对话删除后丢弃并释放凭据。
- 本地草稿持久化路径只能用于展示;重启后的旧 selectionHandle 不自动有效。
16.2 浏览器输入
- 文件选择保留
File对象,发送准备时才上传;不要求浏览器提供本地绝对路径。 - 大文件进度属于原发送事务。刷新页面后失去未发送 File 对象时显示“重新选择”,不能拿文件名自动匹配。
- 目录上传沿用相对路径验证;浏览器不能替代桌面输出目录授权,保留适合浏览器的保存行为。
- 不给 Web 输入创建永久可写目录权限,不把桌面 IPC 能力暴露给浏览器降级路径。
16.3 文本路径
明确用户要求处理的本地路径可在发送准备时进入同一管线,不另外调用旧的“即时全插件授权”逻辑。仅出现在引用段落、示例、附件正文、模型回复或工具结果里的路径不构成授权。
不要依靠更宽泛的正则表达式推断所有权限。若现有解析无法可靠区分用户意图,应生成待确认资源项或要求系统选择器,而不是静默授予写权限。整段用户请求“把结果保存到 X”也必须登记到当前作用域输出目标并走同一保存策略。
17. 增量实施拆分与退出条件
| 顺序 | 增量工作 | 本步退出条件 |
|---|---|---|
| 1 | 审计现有 scope/artifact/store 改动;写差距表 | 每个必做目标有真实代码入口,不重复造服务 |
| 2 | 增加 DraftAttachment,选择只写草稿;保留现有对话归属修复 | 选择/取消零插件 grant;新增对话为空 |
| 3 | 将资源存储从 grant 生命周期分离 | release lease 后副本仍可用;删除引用后回收正确 |
| 4 | 完成 prepare/commit/status/abort 等价协议 | 重复提交、超时和部分复制失败不泄漏、不重复发送 |
| 5 | 执行边界派生/回收 FileRef,接回现有 scope 校验 | 原任务状态正常;跨 scope 和撤销后调用被拒绝 |
| 6 | 消息持久化附件元数据与版本;清理仅消费已发送草稿 | 重启可见历史;新增编辑不被延迟回包清空 |
| 7 | 补齐输出目标、产物重试、打开/定位已有实现 | 成功真实落盘;失败保留;越界路径被拒绝 |
| 8 | 执行全矩阵、同步文档、给 RC 结论 | 所有阻断项有证据,未执行项明确披露 |
这些是逻辑实施批次,不授权创建 Git 提交。可以在兼容层中逐步迁移,但最终用户可达的聊天入口不得继续绕过新协议。Flow 旧授权保持其 run-scoped 行为;不得为统一代码而强行改成聊天 scope。
18. 修订版新增强制测试
第 9 节全部保留,另增加以下用例:
| ID | 场景 | 预期 |
|---|---|---|
| E01 | 选择文件但不发送 | grant 数量不增加,没有快照复制,没有 Worker 启动 |
| E02 | 选择后取消/移除 | 无后端资源残留;选择凭据释放 |
| E03 | 选择后原文件变化/被删除 | 按发送时版本或明确失败处理,草稿保留 |
| E04 | 第 N 个附件准备失败 | 本事务独占副本回收;前 N-1 个不会永久泄漏 |
| E05 | 同 sendId 重复 prepare/commit | 一个事务、一条消息、一次执行 |
| E06 | commit 已成功但响应丢失 | 状态查询恢复原结果,不重复发送 |
| E07 | 同 sendId 不同载荷 | 冲突拒绝,不覆盖已准备资源 |
| E08 | 用户发送后继续输入/添加附件 | 延迟成功回包只消费旧载荷,新草稿保留 |
| E09 | 本轮完成后说“继续” | 副本仍存在,重新建立本轮访问权限 |
| E10 | 执行完成但对话仍在 | 活动执行引用释放;消息拥有的副本不被删除 |
| E11 | 删除资源与执行释放竞争 | 逻辑撤销及时,物理回收只发生一次 |
| E12 | 重启时有未提交发送事务 | 按清单回收;不删除已提交消息副本 |
| E13 | 历史附件加载 | 展示名称/版本/状态;不复用旧 grant 或自动恢复本地写权限 |
| E14 | 再次执行已有保存产物 | 只有显式加入对话资源后可作为输入,不自动开放任意输出路径 |
| E15 | 在宿主重启/权限变化后复用 Worker | 不残留其他作用域本地访问;有状态任务不在执行中被重启 |
至少在测试中观测资源存储、grant 数量、实际模型/执行调用次数,而不只是断言 UI 文本。现有 scope 和 artifact 测试应扩展;新增发送事务、消息关联与副本所有者测试。所有测试使用临时目录与受控执行替身。
19. 修订版 RC 决策与最终交付要求
本方案可以作为下一 RC 的完整修复目标,但它包含状态与存储生命周期变更,不能当作纯 UI 调整发布。作用域校验、副本回收、发送幂等、Worker 状态和保存失败恢复属于正式版阻断项。
允许后移:实时目录同步、任意原地编辑、每对话独立 Worker、跨重启静默恢复授权。不能后移:选择不提前授权、发送失败保留草稿、已提交附件持久化、执行引用与副本解耦、跨对话校验。
如果时间不足,应延后正式版或明确缩减产品承诺并由用户决定范围,不能继续保留已知串权限问题。发布时读取当前 app 版本和远端标签,按发布技能选择未占用 RC;不要改动独立工具链版本。
实施报告还必须回答四个问题:
- 选择后、发送前,是否创建了任何插件访问权限?用测试说明。
- 一轮结束后,哪些对象被释放、哪些对象保留、谁负责最终清理?
- 发送响应丢失时,如何防止重复消息、重复副本和重复模型调用?
- 当前实现保证的是宿主级校验还是更强的 Worker/OS 隔离?有哪些实际验证?
给实施 AI 的启动请求:
请执行本任务书修订版 2 的完整 RC 必做范围。先审计并保留工作区现有改动,输出简短差距表,然后增量实施、测试并同步中英文文档。以“选择仅入草稿、发送时创建宿主副本、执行权限独立回收”为准。不要提交、推送、打标签、改版本或发布;最终报告验收结果与 RC 阻断项。