Files
Genarrative/docs/technical/【技术方案】DirectProject本轮附件路径映射-2026-08-31.md
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

13 KiB
Raw Blame History

DirectProject 本轮附件路径映射

  • 日期:2026-08-31
  • 状态:现行合同(已按本文落地)
  • 问题:Gitea issue #212DirectProject 未消费用户上传权威文档)
  • 关联入口:PR #210「批准 GDD 回填做游戏入口」(feat/create_entrance,未合入时仍按该 PR 的调用链理解)
  • 原则:落地后代码简洁可维护,不为了 diff 最小而打补丁;附件一律同等对待,不给 GDD 开协议特例

0. 一句话

首页带进项目的附件已经复制并登记,但 Direct 首轮只拿到用户原文。本方案让 Direct 回合在发给 Codex 的 user prompt 末尾附上有界路径映射(原文件名 → 项目相对路径),不灌正文、不强制读取、不改做方案注入。

1. 目标与非目标

目标

  1. Direct 首轮知道本轮用户附件的原文件名、项目相对路径、媒体类型、大小和导入状态。
  2. 用户原文不被改写;路径映射是独立 sidecar。
  3. 图片、Markdown、其它文件走同一条协议。
  4. 做成游戏(PR #210:读 game/fast_gdd.md → 当成 fast_gdd.md 附件 → createHomeDraftAutomatically)自动吃到效果,因为那条链就是「带附件的 Direct 首轮」。

非目标

  • 不改 PR #210 的按钮、固定 prompt、startGameFromApprovedGdd、读 game/fast_gdd.md 的方式。
  • 不改 approvedGddRef、审批 receipt、策划项目里的 game/fast_gdd.md 投影。
  • 不改上传命名 assets/uploads/upload-<ts>-<name>
  • 不把附件全文拼进 prompt,不按扩展名决定是否读取。
  • 不把「没读到就阻断」做成门禁。
  • 不扫 manifest 里历史 kind=uploaded
  • 不做 native 读取审计;该项由 【技术方案】Direct回合行为审计账本-2026-08-31.md 承接。
  • 不改 DirectHome 在「无项目路径」时的现有文案和列表格式。
  • 不改 enterCreatedHomeProject 的空正文兜底句(与做方案共用)。

2. 现行断点

Home 附件 / 做成游戏 File(fast_gdd.md)
  → upload_local_asset
  → LauncherProjectContext.attachments          // 已有,只给资源画布
  → ProjectSupervisor                            // 无 attachments 字段
  → chat_with_game_creator_direct_codex
      { projectPath, prompt, clientTurnId, creationType? }

做成游戏的固定 prompt 仍写「附件中的 fast_gdd.md」,磁盘文件却是 assets/uploads/upload-<ts>-fast_gdd.md。映射没有进 Direct。

DirectHome 已有 { name, mediaType, size } 元数据注入,但标明「尚未打开项目,内容尚不可读取」。项目已落盘后这条元数据被丢掉。

3. 目标合同

3.1 唯一 DTO

Home 与 Project 共用一个附件结构,缺省字段表示 Home 现状:

{
  name: string;                 // 原文件名
  mediaType: string;
  size?: number;                // 缺省按 0
  localPath?: string;           // 仅已落入项目时出现
  status?: 'imported' | 'failed';
}
  • 不把 error 字符串送给模型。
  • 前端 LauncherImportedAttachment 继续给画布;invoke 前映射成上述瘦 DTO。
  • importHomeAttachmentsFile.size 写入可选 size,不改 upload_local_asset 返回值。

Rust

struct DirectCodexTurnAttachment {
    name: String,
    media_type: String,
    #[serde(default)]
    size: u64,
    #[serde(default)]
    local_path: Option<String>,
    #[serde(default)]
    status: Option<String>, // 只接受 imported | failed,其它忽略
}

DirectCodexHomeAttachment 删除,Home command 改用同一类型。现有 Home JSON(无 localPath / status)继续能反序列化。

3.2 渲染

一个函数 render_direct_codex_user_prompt(prompt, attachments) -> Result<String, String>

输入 输出
无附件 prompt.trim();若也空则 Err("聊天内容不能为空")
附件都没有 localPath 且都没有 status 保持现有 Home 文案与行格式,测试须逐字兼容
任一条有 localPathstatus Project 头 + Project 行格式

Home 行(禁止改字):

[首页附件说明:当前尚未打开项目,以下仅为附件元数据,附件内容尚不可读取]
- {name};类型:{mediaType};大小:{n} 字节

Project 头与行(禁止出现 GDD / 规格 / 权威 / 必须读取):

<用户原文>

[本轮用户附件:已复制到当前项目。请用「项目路径」读取;原文件名不是磁盘路径。]
- 原文件名:fast_gdd.md;项目路径:assets/uploads/upload-1788083777445-fast_gdd.md;类型:text/markdown;大小:7944 字节;状态:imported

规则:

  • 条数上限仍为 8,超出写 - 另有 N 个附件未展开
  • 名字清洗沿用 Home:basename、去掉控制字符、最多 160 字、空则「未命名附件」。
  • 媒体类型清洗沿用 Home。
  • localPath 只接受项目相对 POSIX 路径:无 ..、无盘符/根路径、首段不是 .agent / .git\ 归一为 /,最长 512;不合法则该条不输出路径,状态按 failed
  • 有附件时允许原文为空(Home 已如此)。Direct 内层若仍要求非空,在 command 边界先渲染再下传,避免空原文 + 有附件被拒。

不在 sidecar 里写「若用户要求按附件实施请先读取」。意图留在用户原文;做成游戏的固定 prompt 已经在说这件事。

3.3 谁渲染、谁看见

  • sidecar 只在进 Codex 前由 Rust 拼装
  • .agent/conversations/project.jsonl 继续写用户原文(现有 append_local_conversation_message)。
  • 工作台气泡继续显示 latch / 输入框原文,不把 sidecar 画进 UI。
  • 未完成首轮的 hydration 重放目前只带 prompt:本期不把附件写进 jsonl,进程重启后的未完成首轮可能丢映射。完整成功首轮不受影响。不为此新增会话 schema。

4. 数据流

Home 上传 / 做成游戏 File
  → upload_local_asset(已有)
  → LauncherProjectContext.attachments(已有)
  → ProjectSupervisor.initialAttachments
  → 首轮 latch(与 prompt、creationType 同级)
  → 仅 chat_with_game_creator_direct_codex.attachments
  → render_direct_codex_user_prompt
  → 现有 Direct turncwd = 项目根)

Supervisor / 做方案首轮忽略 attachments,行为不变。

后续工作台手打消息不带 attachments。附件是这一轮带来的,不是项目终身上下文。

5. 代码落地(按最终结构,不按最小补丁)

5.1 Rust

新增 apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_attachments.rs

  • DTO、清洗、上限常量、render_direct_codex_user_prompt
  • 单元测试(见第 6 节)

agent.rs 增加 mod direct_codex_attachments

direct_runtime.rs

  • 删除 Home 专用 struct / sanitizer / render_direct_codex_home_user_prompt
  • run_direct_game_creator_home_turn 改为调用共享渲染
  • chat_with_game_creator_direct_codex 增加 attachments: Option<Vec<DirectCodexTurnAttachment>>,先渲染再调用现有 run_direct_game_creator_turn_at_with_creation_type_and_emitter
  • 把现有 Home 渲染测试迁到新文件;本文件不再保留一份平行实现

不要把 attachments 顺着 inner turn / emitter / CLI 往下传。CLI run_direct_game_creator_turn_at 不变。

5.2 前端

model.ts ProjectSupervisorComponentProps 增加:

initialAttachments?: LauncherImportedAttachment[];

WorkspaceLauncher.tsx

initialAttachments={currentProjectContext.attachments}

App.tsx

  • props / latch 增加 attachments(默认 []
  • executeChatAgentReply 不要再加第 5 个位置参数,收成:
{
  prompt: string;
  clientTurnId?: string;
  creationType?: HomeCreationType | null;
  attachments?: DirectCodexTurnAttachment[];
}
  • Direct invoke:有 creationType 才写该字段(现有);attachments?.length 才写 attachments
  • Supervisor 分支完全不读 attachments
  • 现有 executeChatAgentReply(prompt) / 恢复未完成 turn 的调用改为对象形式,不传 attachments

瘦映射不要写在 1 万行的 App.tsx 里,放到例如 apps/ai-game-creator-shell/src/features/app-shell/directCodexTurnAttachments.tstoDirectCodexTurnAttachments(imported),去掉 error,空 localPath 不输出该键。

useHomeProjectCreation.tsimportHomeAttachments 写入 size: attachment.file.size。不改 initialPrompt 兜底句,不改做成游戏(即便本分支尚未合入 PR #210,也不预埋 GDD 字段)。

types.tsLauncherImportedAttachment 增加可选 size?: number

5.3 文档

落地提交时(不是本方案文件自身):

  • 本文件标为现行合同
  • docs/README.mddocs/project-memory/shared-memory/document-map.md 增加条目
  • decision-log.md 记一条:Direct 本轮附件只映射路径,不灌正文、不区别 GDD
  • 不把 issue #212 技术说明改写成「已修复」,等代码合入后再改状态

6. 测试

Rust(新文件)

  1. 无附件:原文 trim 后原样返回;空原文报错。
  2. Home 形态(无 path、无 status):与迁过来的两条现有测试逐字一致(含路径剥离、非法 mediaType、8 条上限)。
  3. Project 形态:原文保留;含原名与 assets/uploads/...断言不得出现「GDD」「规格」「权威」
  4. 同一列表里 png 与 md 行格式相同(只是 name/path/type 不同)。
  5. status=failed 且无 path:有状态、无项目路径、无 error 正文。
  6. 非法 localPath../.agent/x、绝对路径)不出现在输出中。
  7. 空原文 + 有附件:成功,且含 Project 头。

前端

  1. home.suite.ts「imports home attachments…」:Direct invoke 必须带 attachments,其中 name角色参考.pnglocalPath 为 upload 返回路径、status: 'imported'。用 png 证明不是 md 特例。
  2. 无附件的 Direct invoke 仍不得出现 attachments 键(或等价:不传该字段)。
  3. planningStartMode 首轮仍走 Supervisorchat_with_game_creator_agent 的 payload 不含附件 sidecar。
  4. 工作台后发的普通消息:chat_with_game_creator_direct_codex 只有 projectPath/prompt/clientTurnId(及既有 creationType 规则),不带 attachments。
  5. 若本分支已能跑 PR #210 的 home.suite / plan-gdd 做成游戏用例:只断言它仍调用 createHomeDraftAutomatically / 仍使用原固定 prompt不要给做成游戏加第二条附件协议。sidecar 由通用 Direct 断言覆盖。

不测

  • 不把「必须生成弹幕射击」写成单测。
  • 不测 native file.read 是否进 agent.db

7. 做成游戏为什么不用改

PR #210 startGameFromApprovedGdd

  1. 读策划项目 game/fast_gdd.md
  2. new File([content], 'fast_gdd.md')
  3. createHomeDraftAutomatically({ creationType: 'game', prompt: APPROVED_GDD_BUILD_PROMPT, attachments }, 'direct-build')

之后与首页拖一个 md 完全相同。sidecar 见到的是「原文件名 fast_gdd.md + 新项目 assets/uploads/upload-…-fast_gdd.md」。固定 prompt 继续说「读附件中的 fast_gdd.md」,映射补上真实路径。

8. 验收

  1. 首页做游戏:上传任意文本或图片 + 一句话,Direct 首轮 prompt 含原名和 assets/uploads/...
  2. 做成游戏(PR #210 合入后或该分支上):固定 prompt 一字不改,同时出现改写后的项目路径。
  3. 做方案首轮:Supervisor 行为与现在一致,无 sidecar。
  4. 无附件:Direct 入参与现在一致。
  5. npm run check:encodinggit diff --check、相关 home.suite / Direct Rust 测试通过。

9. 实现顺序

  1. Rust 共享渲染 + 迁 Home 测试 + Direct command 接 attachments
  2. 前端 latch / invoke / 映射 / size
  3. home.suite 附件断言
  4. encoding 与定向测试
  5. 合入时补 decision-log 与文档索引