@@ -0,0 +1,498 @@
# 客户端本地埋点与主站入库契约
Version: 0.10
Status: 当前方案,已确认口径作为实施依据;第 11 节保留待定稿建议,尚未实施
Date: 2026-09-21
需求来源:[Game Agent 埋点设计原始方案 ](./【需求来源】GameAgent埋点设计原始方案-2026-09-05.md );原始方案与后续已确认决策有差异时,以本文为准。
## 1. 交付目标与范围
交付结果:在 Game Agent 与现役 Design Agent 客户端的真实业务节点产生有明确语义的产品事件,以明文 JSONL 批次持久化在应用级目录;后续主站按统一事件合同入库。
本阶段验收以本地记录为准:字段来源可追溯、事件不会冒充业务成功、正常重放不重复、跨账号不串归属、埋点故障不影响正常操作。没有上传成功、服务端去重或报表已可用的承诺。
优先级:
- 必须项:字段合同、身份与因果关联、原方案 13 类事件逐项对照(采集其中 12 类,暂不采集 session_timeout)、新增策划进度快照事件、本地未闭合会话标记、本地 JSONL 持久化、静默失败、定向验证。本版合计启用 13 类事件。
- 风险项:项目目标身份稳定性、跨账号在途操作、多窗口前台时间、崩溃未闭合区间、revision 与保存事实、重复回调。
- 可选项:无;本阶段不扩充分析维度或细粒度点击事件。
本阶段不实现文件加密、上传定时器、HTTP 接口、数据库表、服务端聚合或后台看板。不读取或批量转换已有对话历史为埋点,不改变现有 Agent 产物与恢复账本的职责。
2026-09-21 存储范围确认:用户决定本阶段先不做加密,直接保存明文事件批次;无公钥配置、密钥生成或轮换前置要求。以后若启用加密,另行定义文件格式与接收兼容合同。
2026-09-21 产品口径确认:一个项目就是一个创作目标。项目内全部创作请求、澄清、审批、重试及跨会话继续均属于同一目标;不再根据消息内容划分新目标。
2026-09-21 异常会话口径确认:下次启动时,确认旧实例已退出且旧会话没有正常结束标记,再将本地会话标为 incomplete(不完整)。不设置周期心跳或检查点定时器、不等待超时阈值、不补记 session_timeout,不补造退出时间或未闭合前台时长。
2026-09-21 执行终态口径确认:等待澄清或审批算本次 run 正常完成,通过 end_reason 区分;用户取消和进程崩溃不计入 agent_run_failed,不进入已知成功/失败终态的分母。
2026-09-21 本地保留策略确认:埋点队列及相关索引最多保留 7 天、总量上限 20 MiB。任一条件达到即按第 7.3 节清理,未上传的数据也适用;不影响项目、Agent 对话或创作成果。本版尚不上传,数据不会无限保留等待上传功能上线。
2026-09-21 本地封存时间确认:从当前批次第一条事件进入开始,5 分钟到期后封存落盘;没有事件不生成空批次。该时间与后续每 15 分钟上传的周期独立,500 条及 1 MiB 的批量上限仍为草案建议。
2026-09-21 策划进度采集确认:不设固定采集周期;项目打开成功、相关策划状态成功持久化时触发,从 `.agent/design-agent/session.json` 提取白名单状态快照,内容无变化不重复记录;项目切换或关闭前尽力补采。5 分钟是封存落盘周期,后续 15 分钟是上传周期,均不是采集周期。不为策划埋点专门增加文件 revision 机制。
用户已确定的后续要求:上传至主站数据库;客户端每 15 分钟上传一次;绑定真实用户与业务标识;成功后删除对应待上传副本;失败无弹窗、不阻塞正常进程;失败是否重试可选。本草案建议后续失败保留至下一个周期重试,不做立即重试;这是建议,不是已实现行为。
本文是团队共享的埋点技术方案唯一维护入口,原始需求副本仅用于追溯。正式编码前,按仓库规范驱动工作流在 `docs/project-memory/plans/` 补齐里程碑规范及当前里程碑实施计划,并完成对应评审;本文的建议项不因迁入仓库而自动视为已确认。本次仅交付文档。
## 2. 已核实的现有记录与复用边界
| 现有记录 | 当前事实 | 本阶段复用方式 |
| --- | --- | --- |
| `.agent/conversations/project.jsonl` | Game Agent 正式对话历史,包含正文 | 复用正式受理、终态的业务入口;不复制正文到埋点 |
| `.agent/runtime/direct-codex/turns/<clientTurnId>.jsonl` | 回合工具审计,条目有上限,写入可失败 | 参考执行事实;不能把其条数作为完整产品事件数量 |
| `.agent/agent.db` | 逐行 JSON 本地索引与审计,非 SQLite | 保持原用途,不作为待上传队列 |
| `.agent/design-agent/session.json` | 策划会话、对话、工具结果、阶段、审批、当前回合与恢复状态 | 状态保存成功后触发,提取白名单进度快照;不上传完整会话文件 |
| `design_artifacts/` | 正式策划成果 | 保持原文件保存行为,本版不为统计新增 revision;文件内容不进入事件 |
| 应用级 `direct-executions/` 、`direct-delivery/` | 执行控制与交付状态 | 关联正式执行生命周期;不额外复制一套业务状态机 |
旧 Planning V1/V2 与 Fast GDD 不作为新埋点接线目标。主站已有 HTTP tracking 也不等于本方案事件已经被接收或保存。
## 3. 总体链路与非阻塞原则
``` text
真实业务节点产生不可变事件候选(冻结身份、时间与业务引用)
→ 有界内存队列,非阻塞投递
→ 单个后台写入器校验,在有界内存中组成 JSON 批次
→ 序列化为明文 JSONL,写入事件文件与最小控制元数据
→ 应用级本地待上传目录,批次不可变
→ 后续阶段:15 分钟上传 JSON 事件批次、主站入库、整批确认后删除
```
- 业务线程不等待磁盘写入、文件清理或网络。队列满时丢弃本次埋点并增加本地计数,不拖慢业务。
- 写入失败、权限异常、格式错误、磁盘满:埋点失败静默处理,不向业务调用返回错误、不显示弹窗、不回滚已经成功的操作。
- 字段缺失不能通过伪造值满足校验。整条事件不合规时不写入正式事件文件,在内部记录事件名、原因码及丢弃数量,不记录正文或凭据。
- 捕获时生成 event_id 与 event_time,后台排队延迟不得改变事件发生时间。
- 正常退出仅尽力排空已经在队列中的事件,不新增等待上传或等待写盘的退出门禁。
- 这一取舍允许“业务已成功,但埋点尚未持久化就进程退出”的丢数窗口。不能同时宣称完全不等待与绝对零丢数;本方案优先满足用户要求的不阻塞。
## 4. 公共事件合同
### 4.1 序列化规则
- UTF-8、无 BOM;字段名统一 snake_case。本地使用 JSONL,每行一个完整公共事件对象,按第 7 节组织批次文件。
- 新写入事件固定 `schema_version: 1` ;语义或类型发生不兼容变化时升版本,不能悄悄改变旧字段含义。
- 顶层字段全部显式出现;不适用的可空字段为 null,不使用空串、0、`unknown` 或设备 ID 冒充真实业务身份。
- properties 是按 event_name 定义的封闭对象,只允许本方案列出的字段;必填字段不可缺少,可选字段不可得时省略。
- 公共字段不在 properties 中重复。所有 ID 为字符串,不将数据库整数 ID 转为可能丢精度的 JavaScript number。
- 时间为 UTC ISO 8601,毫秒精度;时长和计数为非负整数,限制在 JavaScript 安全整数范围。时间倒退、跨重启无法可靠计算时写 null 或省略,不写负数或猜测值。
### 4.2 字段定义
| 字段 | JSON 类型 | 来源与约束 |
| --- | --- | --- |
| schema_version | integer | 固定 1;本文新增,供本地读取与后续服务端识别版本 |
| event_id | string | 客户端 UUID v4,单条事件唯一;重试、复制批次与恢复重放沿用原值 |
| event_name | string | 仅允许第 6 节本版启用的 13 个英文事件名(原方案 12 类加 design_progress_snapshot);session_timeout 不在本版写入白名单中 |
| event_time | string | 业务事实发生时捕获的 UTC 时间;不能以重启检测时间冒充旧会话的退出时间 |
| user_id | string/null | 由已确认的平台登录会话取得;匿名明确 null,不从邮箱、设备或项目所有者猜测 |
| editor_session_id | string | 埋点专用 UUID;不复用 Runtime sessionId、threadId 或 clientTurnId |
| project_id | string/null | 项目 manifest 的稳定 ID;第 6 节规定项目事件必须非空 |
| creative_task_id | string/null | 项目对应的唯一创作目标 ID;有任务关联时直接等于该事件的 project_id,规则见第 5 节;不另造随机任务身份 |
| agent_run_id | string/null | 一次受理的 Agent 执行 ID;仅 run 终态事件填写 |
| agent_turn_id | string/null | 真实底层技术回合标识,可选;禁止把 run ID 复制进来凑字段 |
| status | string/null | 按本版事件固定映射为 success、failed 或 null;不能任意填写;本地会话 incomplete 不属于本字段 |
| error_code | string/null | 仅 agent_run_failed 非空,来自稳定错误类别映射;其余为 null |
| source | string | 仅 editor、direct、design_agent、asset_canvas、resource_editor、ui_editor、manual、system;表示事实产生模块 |
| client_version | string | 从实际运行的应用版本元数据读取,不使用示例值或空串 |
| properties | object | 对应事件的专属字段,允许空对象 |
source 新增 design_agent 以表达现役策划实现;不使用旧 supervisor 标签代替。尚未接线的来源不能因枚举存在而被认为已覆盖。
### 4.3 用户归属与主站目标
- 用户与平台地址一并在事件发生时冻结。队列的本地路由元数据保存 `destination_origin` (经规范化的平台协议、主机、端口,无路径、查询串与凭据)和 user_id;它不是额外的产品指标。
- 同一批文件只包含同一个 destination_origin 和 user_id 的事件。使用不含用户标识的随机批次文件名;路由元数据不保存 Token、Cookie 或 API Key。文件内容与路由元数据均为明文,其采集边界见第 7.4 节。
- 账号 A 的在途任务结束事件继续归属发起执行时的 A;不得在后台写入时读取当前 B 账号并替换。
- 账号切换、登录与注销:闭合旧身份下的 focus 区间,原子切换埋点身份后按实际窗口状态开启新区间。编辑器会话可保持不变,按用户统计时必须按事件归属拆分,不能将整个会话时长归给最后登录者。
- 匿名事件保持 user_id=null;不因之后登录自动回填。缺乏可解析的平台目标时可以以 destination_origin=null 本地保存,后续禁止默认发给当前任意服务器,待上传阶段明确处理策略。
- 后续上传必须验证凭据主体与批次所属账号、平台一致。无法认证 A 时保留 A 的数据,不使用 B 的凭据代传;匿名接收规则在上传阶段明确。
- 本地 user_id 只是待验证声明,后续服务端不能不核验鉴权就信任客户端填报的用户身份。
## 5. ID、因果关系与执行语义
### 5.1 编辑器会话与多窗口
一个 GUI 应用实例对应一个 editor_session_id,从可用初始化到该实例实际退出。新进程新 ID;WebView 重载不重建会话。Runner 继续在后台运行不等于编辑器仍有前台会话。
同实例多个可交互编辑器窗口的前台区间取并集,不累加重叠时长。以宿主查询的窗口总体状态为准,聚合窗口切换;不同应用实例独立记会话。后台 Runner 自己不产生 editor_session_start。
### 5.2 项目与创作目标一一对应
- 用户已确认:一个项目就是一个目标。同项目内修改需求、改变方向、普通追加、澄清、审批和用户重试都不产生新目标;只有不同项目才对应不同目标。
- 建议采用最简单的字段映射:creative_task_id 直接复用 manifest 的 project_id 值,保留两个字段的契约名称但不维护第二份随机 ID 或映射表。该等值映射是本文实现方案,不表示 clientTurnId、agent_run_id 也可复用项目 ID。
- 项目改名、移动目录、重开、客户端重启、换账号以及从 Design Agent 转到 Game Agent 均不重建目标 ID。另存为新项目时应使用业务实际生成的新 project_id;复制目录但仍保留相同 project_id 的情况按同一项目身份处理,埋点不自行修正项目身份。
- creative_task_submit 的拟定采集口径保持每个目标首次受理创作请求时一次;仅创建或打开项目不算提交。之后的执行用多个 run 表达,不把消息条数算成目标数。此事件频次属于草案口径,需与第 11 节一并评审。
- 首次提交事实随项目已有受理记录或最小标记持久保存,不能只存在 React 状态或会过期的上传索引中。上传后清理批次不清除目标已提交事实,避免重开项目重复首次提交。
- 现有项目的目标 ID 可由真实 project_id 直接确定,但不补发历史事件。无法证明是首次创作请求时,不将首次启用埋点或第一次遇见旧项目冒充首次提交;后续 run、revision 等仍可记录并关联项目目标。
- 不新增“新目标/继续目标”选择器,不使用模型、关键词、消息间隔或最近任务推断目标归属。所有任务相关事件校验 creative_task_id == project_id。
### 5.3 Agent 执行
- 一次用户指令或明确继续动作被 Runtime 受理后生成 agent_run_id,执行至正常返回、最终失败、取消或中断。一次任务可以包含多个 run。
- 同一执行内部的 Provider 重试不创建新 run;相同受理请求重放复用原 run ID。
- 用户主动重试形成新的 run,沿用任务 ID。retry_index 定义为该任务的用户主动重试序号:初始执行为 0,主动重试递增;普通澄清/审批继续不递增。它不表示 Provider 的请求重试次数。
- 正常等待澄清或审批结束本次 run,以 completed + end_reason 区分;用户答复后新建 run。completed 表示本次执行正常收束,不代表整个任务完成或用户满意。
- 用户取消、崩溃、结果不确定不伪造 completed 或 failed。原方案未覆盖这些 run 终态,本版不新增对应终态事件,明确它们不进入成功/失败分母。
- duration_ms 用单调时钟测量本次执行,含内部等待与自动重试,不含等用户答复的间隔;跨进程恢复无法可靠累加时为 null。
### 5.4 下游因果关联
project_revision_created、preview_ready、project_save 已知所属项目,因此 creative_task_id 填同一 project_id,包括人工编辑与保存。这个关联只表示项目目标归属,不表示修改由 Agent 造成;实际来源由 source 与 revision_source/save_source 表达。其 agent_run_id 与 agent_turn_id 按原需求保持 null,不将“当前运行的 Agent”作为推定因果。
执行上下文冻结 project_id、creative_task_id、agent_run_id、用户与平台身份;后续不能从当前选中项目重新取值。run 的 output_change_detected 必须根据本次执行可归因的有效修改判定,不能只比较全局 revision 前后值,因为其他窗口也可能改动项目。
## 6. 事件合同(原方案启用 12 类,新增 1 类策划进度快照)
本节 properties 中“可选”字段在不可得时省略;未标可选的字段必须存在。公共 status、ID 与 source 的赋值同时受第 4、5 节约束。
### 6.1 editor_session_start
- 触发:GUI 完成可用初始化,宿主首次确认会话开始;不是每次组件挂载。
- 公共字段:status=success; project_id 仅在已成功打开项目时填写;任务/run/turn ID 均为 null; source=editor。
- properties: entry_source 为 direct_launch / project_association / app_restore; first_project_id 为已确认首次项目 ID 或 null。之后打开项目不回写本事件。
- 去重事实键:editor_session_id + event_name。
### 6.2 editor_session_end
- 触发:应用实际正常退出,先尝试闭合前台区间;关闭项目窗口但应用仍运行、退出被取消时不记。
- 公共字段:status=success; project_id 为最后成功活动项目或 null;任务/run/turn ID 均 null; source=editor。
- properties: end_reason=user_exit / app_restart; session_duration_ms 为可靠时长或 null; last_project_id 为稳定 ID 或 null。
- 去重事实键:editor_session_id + event_name。正常退出事件也可能因不等待后台写盘而丢失,不能据此声称进程必然崩溃。
### 6.3 session_timeout(本版暂不采集)
- 按用户确认,本版不产生该事件,也不新增其他异常退出产品事件替代它。后续若需要服务端异常会话统计,另行定义接收合同。
- 下次启动在后台检查本地旧会话:仅当缺少正常结束标记,且能够确认所属旧实例已退出时,将旧会话标为 incomplete。
- 旧实例仍在运行则不处理;无法确认进程归属或存活状态时保持原记录,不猜测退出。实例确认需防止 PID 被复用,复用宿主实例锁或 PID 与进程启动身份的组合,不仅检查 PID 数字。
- 发现即可标记,不要求等待 30 分钟,不维护 60 秒检查点定时器,也不添加启动等待门禁。
- incomplete 只表达记录不完整,不认定原因是崩溃。标记保持旧会话及原用户归属,不改成当前登录用户;重复启动处理保持幂等。
- 不补记 editor_session_end、editor_focus_end、session_timeout,不改写已封存的历史事件。未闭合 focus 区间不计入完整前台时长,已正确配对的历史区间仍可计入。
- 用户不再启动时,旧记录保持未闭合;主站后续只可将缺结束或缺配对记录视为不完整,不能从上传缺席推出确定退出时间。当前本地 incomplete 标记不随事件批次上传。
### 6.4 editor_focus_start
- 触发:实例的可交互窗口总体状态从无前台变为有前台;启动时已获得焦点也需要记录。
- 公共字段:status=null; project_id 为开始时实际活动项目或 null; source=editor;任务/run/turn ID 均 null。
- properties: focus_interval_id( UUID);focus_reason=initial_focus / window_focus / restore / account_change; active_project_id(可为 null)。
- focus_interval_id 是本文新增的事件专属关联 ID,确保存在丢数与乱序时仍能准确配对;不新增顶层业务 ID。
- 项目切换不拆分区间,因此不能用 active_project_id 把整个区间归为某个项目的编辑时长。
- 去重事实键:focus_interval_id + event_name。
### 6.5 editor_focus_end
- 触发:总体前台状态关闭、最小化、可观测的锁屏/休眠或正常退出。锁屏/休眠监听能力必须实际验证,无法观察的情况标为统计限制。
- 公共字段:status=null; source=editor; project_id 为结束时活动项目;任务/run/turn ID 均 null。
- properties:原 focus_interval_id; blur_reason=window_blur / minimized / app_exit / system_suspend / account_change; focus_duration_ms 为单调时钟时长或 null; active_project_id(可为 null)。
- 重复 blur 不重复结束。缺 start 的 end 或缺 end 的 start 都不组成有效配对,不凭空补齐另一端。
- 去重事实键:focus_interval_id + event_name。
### 6.6 project_create_success
- 触发:新项目初始化完成,manifest 已持久化且稳定 project_id 已确认;覆盖自动创建与用户选目录创建。
- 公共字段:status=success; project_id 必填;source=editor;任务/run/turn ID 均 null。
- properties: creation_source=home_game / home_design / template / selected_directory; project_template_id 可选,仅填真实模板稳定 ID。
- 打开已有项目、幂等初始化不记创建;文件夹刚建立但后续失败不记成功。
- 去重事实键:project_id + 本次创建操作 ID + event_name,沿用业务操作身份,不读取路径当项目 ID。
- 只有成功事件,不能计算创建成功率;创建尝试数不在本版范围。
### 6.7 project_open
- 触发:一次显式导航或启动恢复成功加载项目并进入工作区;后台读取与同页面刷新不记。
- 公共字段:status=success; project_id 必填;source=editor;任务/run/turn ID 均 null。
- properties: open_source=create / picker / recent / app_restore / project_association; is_first_open 为 boolean 或 null。
- is_first_open 口径为“当前账号在本安装可观察范围内首次成功打开此项目”,不是全平台首次。使用本地有界索引保存该事实;无历史覆盖证据、旧项目首次遇到或索引清理后填 null,不将未查到当 true。新创建并首次进入可确认为 true。
- 去重事实键:本次工作区打开操作 ID + event_name;之后真正重新打开必须有新的操作 ID。
### 6.8 creative_task_submit
- 触发:项目目标的首次创作请求通过基本校验并被业务受理,且首次提交事实可确认;不是创建项目、点击按钮、未提交草稿或请求重放。
- 公共字段:status=success; project_id、creative_task_id 必填;source=direct / design_agent; run/turn ID 均 null。
- properties:空对象。公共字段不重复,不增加 Prompt、长度、附件或任务分类。
- 去重事实键:creative_task_id + event_name;同项目的澄清、审批、追加和重试只创建后续 run。项目级首次提交标记跨应用会话及批次清理保留。
- creative_task_id 必须等于 project_id;同一项目先策划再开发也不再记第二次目标首次提交。
### 6.9 agent_run_completed
- 触发:本次执行达到确定正常终态;现役 Direct 与 Design 在各自正式生命周期接线,不以“返回一个字符串”或 HTTP 200 代替业务成功。
- 公共字段:status=success; project_id、creative_task_id、agent_run_id 必填;agent_turn_id 可空;source=direct / design_agent。
- properties: agent_type=game_agent / design_agent; run_source=user_submit / user_continue / clarification / approval / user_retry; duration_ms( integer/null);retry_index( integer);output_change_detected( boolean/null);revision_id 可选;end_reason=finished / waiting_for_user / waiting_for_approval。
- output_change_detected=true 必须有本次归属的有效变更证据;false 必须完成检测且确认无变化;无可靠证据填 null。revision_id 仅填本次归属的最新已提交 revision。
- 策划阶段快照不能作为文件内容变化证据;Design 执行若没有独立可靠的成果变更检测,output_change_detected=null 并省略 revision_id,不因阶段推进而填 true,也不因阶段没变而填 false。
- 去重事实键:agent_run_id + terminal;同一个 run 最多一个正常终态,不因 GUI 与 Rust 双写而重复。
### 6.10 agent_run_failed
- 触发:业务已经判定本次执行最终失败;内部重试尚在继续时不记。
- 公共字段:status=failed;同 completed 的 ID 要求;error_code 必填。
- properties:同 completed 的公共执行属性;end_reason=failed。失败前可能已产生有效修改,因此 output_change_detected 可以为 true。
- error_code v1 白名单:provider_auth_failed、provider_rate_limited、provider_unavailable、provider_timeout、provider_invalid_response、local_io_failed、runtime_failed、runtime_error_unclassified。
- 错误码由结构化错误种类映射;无法分类才使用 runtime_error_unclassified。不能通过异常正文猜分类,不保存错误正文、文件路径或堆栈。普通工具失败被 Agent 修复后完成不记 run_failed。
- 去重事实键:agent_run_id + terminal。取消与异常中断不冒充 failed;本版 run 完成率仅比较已观察到的 success/failed。
### 6.11 project_revision_created
- 触发:代码、资源、UI 等成果确实发生有效变化,且现有业务修改与正式 revision 都成功提交。存储损坏或不确定提交不记成功。
- 公共字段:status=success; project_id 必填;creative_task_id 与 project_id 相同;run/turn ID 为 null; source 使用实际修改模块。
- properties: revision_id(将项目内真实 revision 转为字符串,不使用 event_id);revision_source=agent / asset_canvas / resource_editor / ui_editor / manual_edit / system_projection; change_kind=code / asset / ui / design_document / mixed; files_changed_count 可选。
- 只更新聊天历史、访问时间或运行状态不属于有效变化。system_projection 仅在投影真实业务成果时允许,纯修复缓存/同步元数据不进入有效变化率。
- 当前 Direct 的 outputs_changed 与 manifest_requires_sync 要区分;不能给 revision 递增函数加一个无条件事件后就宣称满足语义。
- Design Agent 本版改用第 6.14 节的进度快照,不为了埋点给策划写文件、补丁或删除补建 revision。不得把阶段名、turn_index、session.updated_at 或审批结果填成 revision_id;仅当已有业务实际产生正式 revision 时,才可按本事件原条件记录。
- 去重事实键:project_id + revision_id + event_name。一个原子 revision 只记一次;多来源合并提交按真实提交范围填写,禁止任选最后一个来源。
- 外部编辑器的修改仅在宿主已有变化确认与 revision 提交时覆盖;本版不新建全盘监听,不宣称能看到所有磁盘改动。
### 6.12 preview_ready
- 触发:宿主对本次预览实例的实际入口执行访问检查成功,且确认检查期间项目版本仍匹配;Web 以真实入口成功响应且内容存在为最低可访问判据,其他引擎需各自真实 ready 信号。
- 公共字段:status=success; project_id 必填;creative_task_id 与 project_id 相同;run/turn ID 为 null; source 为实际发起模块。
- properties: preview_source=user / agent / auto_restore; preview_version 为该实例实际服务的项目 revision 字符串;ready_duration_ms 可选。
- 当前代码只启动监听、写 Running、返回 URL,不能直接产生本事件。探测失败或版本漂移不产生 ready;不记录本地 URL、端口或绝对路径。
- 去重事实键:项目 + 预览实例 ID + preview_version + event_name;轮询与重复回调不重复。同版本真正重开预览可产生新事件,漏斗按任务去重。
- ready 只证明可访问,不证明 JS 无异常、游戏通关或用户满意。仅策划文档项目不强行产生游戏预览事件。
### 6.13 project_save
- 触发:一次面向用户成果的逻辑保存操作已全部成功落盘,包括手动保存、业务自动保存或完整 checkpoint;多文件保存按一次操作记,不按底层 write 次数记。
- 公共字段:status=success; project_id 必填;creative_task_id 与 project_id 相同;run/turn ID 为 null; source 为实际保存模块。
- properties: save_source=manual / auto / checkpoint; revision_id 可选,取本次保存结果对应的真实 revision。
- Agent 会话检查点、审计记录、埋点写入、配置保存、云快照同步不算项目保存。独立素材导出也不自动等于项目保存。
- 无变化的自动保存不记;用户显式保存成功可记,但不能同时伪造 revision。失败或部分写入不记成功。
- 去重事实键:项目 + 保存操作 ID + event_name。自动保存不表示用户认可;看板必须按 save_source 区分。
### 6.14 design_progress_snapshot
- 用途:记录采集时实际观察到的策划进度,不是审批操作流水,也不证明文件内容已改变。
- 唯一事实来源:对应项目 `.agent/design-agent/session.json` 已成功持久化的状态。不读取 `.debug` ,不解析自然语言,不上传整个 session 文件。
- 触发:项目打开成功后采初始快照;阶段推进、提交审批、审批通过/拒绝、进入或结束等待澄清的状态成功保存后触发;切换或关闭项目时尽力补采。没有固定采集定时器,不等 5 分钟才读取。
- 普通模型输出、工具调用、对话保存、重试计数变化,若未改变白名单进度状态,不产生本事件。没有策划会话的项目跳过,不创建伪会话或默认初始阶段。
- 公共字段:status=null; source=design_agent; project_id 必填,校验会话 projectId 与项目 manifest 一致;creative_task_id=project_id; agent_run_id 与 agent_turn_id 为 null。editor_session_id 仍是编辑器会话,不使用策划 sessionId 替代。
- event_time 是成功读取该快照的观察时间,不回填为阶段真实转换时间。用户归属按触发上下文冻结:业务推进使用该次执行的用户,打开/关闭观察使用该操作的用户;不把快照中的历史阶段声称为当前观察者完成。
properties 白名单:
| 字段 | 类型 | 来源与语义 |
| --- | --- | --- |
| design_session_id | string | 原 sessionId,用于区分同项目重置后的策划会话;不改变项目目标 ID |
| current_phase | string | 原 currentPhase,仅 concept / top_design / architecture / systems / tdd / consultant |
| approved_phases | string[] | 原 approvedPhases;按固定阶段顺序规范化,无重复;不从 current_phase 推算 |
| pending_approval | object/null | 原 pendingApproval 提取 request_id、phase;没有待审批时为 null |
| pending_clarification | object/null | 原 pendingClarification 仅提取 request_id;不采问题正文和选项;没有时为 null |
| capture_reason | string | project_open / state_persisted / project_leave,表示观察触发方式,不冒充具体审批动作 |
- 不采 history、messages、commands、工具参数/结果、Prompt、错误正文、产物正文或路径。类型异常、未知阶段、项目身份不匹配或读取失败时静默跳过并计数,不填默认值。
- 比较范围为同平台、同用户、同编辑器会话、同项目与同 design_session_id 下最近一次已接收快照。比较 current_phase、approved_phases、pending_approval、pending_clarification;排除 event_time、capture_reason 及文件更新时间,避免普通会话写入反复触发。
- 第一次观察记录一次;同观察范围内状态相同则不重复。新编辑器会话或新用户需要自己的初始观察。只与相邻快照比较,不做永久内容去重:A→B→A 必须可记录三次,新的审批 request_id 也构成变化。
- 快照事件使用独立 event_id;重复回调的相邻相同状态被抑制,已排队事件的后续落盘/上传重试沿用原 ID。队列拒绝时不更新“已接收快照”,以便后续触发仍有机会采集。
- 读取和投递不阻塞正常业务。采集必须绑定触发时的项目路径和身份,不能在执行异步读取时改用当前选中项目。异步读取时状态可能已经推进,事件诚实记录实际读到的最新状态,不伪造漏掉的中间审批/澄清动作;本方案不承诺完整操作审计。
- 关闭项目不会删除已进入应用级队列的快照;整个客户端退出则尽力提前封存,强退仍适用未落盘丢数限制。
## 7. 本地存储、恢复与去重
### 7.1 位置与文件格式
使用客户端已解析的应用级配置目录下 `analytics/` ;正式 Windows 默认基于 `%APPDATA%/world.genarrative.ai-game-creator/` ,显式 config-dir 覆盖时随之变化。不放在项目目录或 `.debug` 中。
``` text
analytics/
instances/<editor_session_id>/
session.json 本地生命周期标记和恢复所需关联
batches/<batch_id>/
meta.json 最小路由、幂等与清理元数据
events.jsonl 每行一条事件的明文批次,封存后不可变
```
batch_id 为随机 UUID;每个实例由单个后台写入器负责。批次在内存中组装,在同文件系统的临时批次目录写 meta.json 与 events.jsonl,写入完成后原子发布整个目录。未来上传只处理已发布批次,不读取在写临时目录。账号或目标平台变化时切新批次。
已确认时间条件:从当前批次第一条事件进入开始计时,5 分钟到期后由后台写入器封存落盘;后续事件进入新批次,没有事件时不产生空批次。仍建议批次达到 500 条或 1 MiB 序列化事件大小时提前封存,两个批量上限尚待评审。正常退出尽力封存,不新增阻塞退出的等待。
这些是本地写入参数,与未来 15 分钟上传周期独立。封存前原始事件只存在有界内存,强杀或断电可能损失最近约 5 分钟尚未封存的数据;后台调度或写盘延迟可能扩大窗口,不承诺严格的最大丢数时长。实现时作为内部常量,不增加用户设置界面。
session.json 保存会话 owner(含可验证的实例身份)、原用户/平台归属、lifecycle_state( active / closed / incomplete)、未闭合 focus 及必要的幂等事实关联;原子替换。生命周期状态与会话开始、身份/focus 变化、正常退出等实际节点一起由后台写入,不设周期检查点。
下次启动识别旧实例退出后,只将符合第 6.3 节条件的 active 会话改为 incomplete; closed 和 incomplete 保持不变。可以保存本地 incomplete_detected_at 表示发现时间,它不是退出时间,不进入公共事件字段。记录缺失或损坏时不猜测重建完整会话。正常退出标记和事件均为尽力持久化,缺失可能来自写盘失败,不能据此确定为崩溃。
session.json 是本地恢复元数据,不是待上传事件;事件文件不能代替其中的实例存活身份和生命周期标记。用户下次启动前没有机会作出的标记,不视为已经完成。
### 7.2 去重与重放
- 第 6 节的事实键是本地逻辑幂等依据,不新增到公共 envelope。首次接收事实时分配 event_id;队列、批次及恢复保留同一身份。
- 普通重放从已有业务操作/任务记录取得关联 ID,不按相同正文去重;相同文本可能是两次合法操作。
- 幂等关联持久化与写入器串行执行。meta.json 保存批次的 event_id 清单和事实键摘要到 event_id 的映射,不复制事件正文。恢复从已发布批次元数据重建索引;批次成功发布但索引尚未更新时,不能重新生成 UUID 追加同一事实。
- 无法证明原事件身份时不补造历史事件,保留丢数限制;不为每条埋点给业务事务增加同步磁盘门禁。
- 重启保留完整已发布批次,清理未发布临时批次。读取时逐行解析并校验事件及元数据一致性;截断、坏行或身份冲突导致的损坏批次隔离并计数,不清空其他批次,也不自动改写事件或重新生成 event_id。
- meta.json 记录格式版本、批次身份、路由、创建时间、事件数量及幂等关联。事件白名单校验不等于防篡改认证;后续服务端仍须独立鉴权和校验。
- 正常退出标记缺失只是“未闭合”,不能证明具体退出原因;后续 incomplete 标记也不得被解释为已确定的 crash。
### 7.3 容量与生命周期
已确认:本地埋点队列及相关索引整体最多 20 MiB、最多保留 7 天。超过 7 天的批次按期清理;总量超过 20 MiB 时,从最旧封存批次开始清理及其专属索引,直到回到上限内。任一条件达到即执行,未上传批次也适用。清理记录丢弃条数;写入器轮转当前批次后仍无法满足上限则丢弃新事件。损坏文件同样计入限额,不无限隔离积压。
清理只作用于埋点队列及相关索引,不删除项目、Agent 对话、业务恢复记录或创作成果。清理由后台执行,失败静默,不阻塞正常客户端操作。
实例生命周期标记、打开历史索引与批次幂等映射均需有界清理;索引过期后不承诺识别无限久远的重放或首次打开。不可因此将未知 is_first_open 填 true。项目级首次创作提交标记跟随项目业务记录,不随埋点批次保留期清理。
本阶段没有成功上传清理,正常数据只因容量/保留期策略清理;不得启动一个空上传器并将数据标成“已上传”。这也意味着本阶段积累的数据不保证一直保留到上传功能上线。
### 7.4 数据最小化与文件边界
- 本地 events.jsonl 为可直接读取的明文,不承诺文件保密或防用户修改。本阶段不引入加密库、密钥配置或文件加密开关。
- 事件只包含本方案白名单字段;session.json 与 meta.json 只保存路由、实例身份、生命周期、幂等和清理所需元数据。
- 不采集 Prompt、对话正文、工具参数/结果、代码、文件正文、Token、Cookie、API Key 或异常堆栈;不因取消加密扩大采集范围。
- 正式事件写入 analytics 队列,不在普通应用日志、诊断包中重复输出完整事件正文。目录沿用现有应用私有目录权限。
- 后续网络传输仍使用 HTTPS,并由主站验证用户身份与字段。文件不加密不等于取消传输安全或服务端校验;本阶段无上传行为。
## 8. 为后续主站入库预留的合同
### 8.1 保持事件与传输分离
本地事件 envelope 保持第 4 至 6 节合同,不添加 uploaded、retry_count、last_upload_error 等会随传输变化的业务字段。批次目标、发送进度与确认信息留在传输元数据。
后续批量请求固定包含 schema_version(整数 1)、batch_id(批次 UUID)、destination_origin(规范平台 origin)、user_id(真实用户 ID 字符串或 JSON null)、events(从 JSONL 解析得到的事件对象数组)。全批事件用户必须一致;本地路由只作发送提示,不能作为服务端归属的唯一证据。
后续客户端通过 HTTPS 上传 JSON 批次,传输体类型为 application/json;保持各事件的原 ID、时间和字段值。本阶段不实现该请求,不虚构 URL 或物理表名。
为简化客户端文件管理,后续保留整批确认语义:服务端只有在全批事件已可靠保存,或按 event_id 去重确认内容相同时,才返回 `acknowledged_batch_ids` 。客户端核对响应来自预期主站且 batch_id 属于本次请求后,删除对应整个批次目录。
部分入库后失败时不确认整批;客户端保留原事件批次并在后续重发全批,由服务端按 event_id 去重。无需为了全批确认强制采用一个数据库大事务,但必须可靠判断全批完成。相同 event_id 不同内容为冲突,不能覆盖原记录或谎报确认。
本版不实现逐事件确认后的部分文件重写。永久无效批次的拒收/过期清理不得记为上传成功,也不能因为 HTTP 200、收到字节或仅解析成功就删除本地数据。
### 8.2 主站接收的最低语义
- 按 schema_version + event_name 校验字段类型、必填项、枚举、事件与 status 的组合;properties 不作为任意 JSON 垃圾桶。
- 任务相关事件要求 creative_task_id == project_id;项目目标首次提交只能按已确认的项目首次提交事实记录,不能在接收端把每个新用户的第一次消息再算成新目标。
- 限制请求体积与事件数量,校验 JSON 格式、批次身份和字段版本;解析或校验失败不确认该批次。请求鉴权和 HTTPS 独立于本地文件存储格式。
- user_id 与鉴权主体必须一致;项目 ID 是客户端本地项目的关联标识,不因此赋予主站项目权限。
- event_id 是全局唯一去重键。导入时保留原 ID、发生时间、关联字段和客户端版本,不重生成事件身份。
- 服务端单独生成 received_at,用于观察延迟;不覆盖 event_time。
- 不要求会话、任务、run 与 revision 按顺序到达,不因父事件丢失或批次乱序拒绝所有子事件;关联不完整需可识别。
- 查询维度需要支持 user_id、project_id、creative_task_id、agent_run_id、event_name、event_time 和 client_version。主站物理表、索引和现有 tracking 适配在后续阶段核查后确定。
- 本地匿名事件、未知平台、未知 schema 版本不能静默改写归属或格式后入库。
- 未来每 15 分钟触发后台上传、同一客户端同时最多一个上传任务;成功清理、失败静默、不阻塞退出。重试策略、请求上限和超时在上传里程碑定义。
### 8.3 可用指标的限制
- 匿名事件不纳入按用户的留存;稳定用户也要等主站接收链路验证后才能出正式留存。
- 创作目标数就是项目目标数,不是消息数或执行次数;目标首次提交漏斗按有 creative_task_submit 的项目去重。旧项目缺失首次提交事件时不回填,应明确其不在该漏斗 cohort 中。
- 同目标可能跨天、跨会话、跨账号并经历许多 run。回访创作可由后续 run、revision、project_open 观察,不能只用首次 creative_task_submit 判断用户是否继续创作;每会话目标首次提交数不再代表每会话交互次数。
- 人工修改、预览和保存归入相同项目目标,但不能据此宣传为 Agent 导致的成果;run 的 output_change_detected 仍需要本次执行的真实变化证据。
- Game Agent 与 Design Agent 的预览适用性不同;不能把所有策划任务算入“游戏预览失败”的分母。
- 策划进度按 design_progress_snapshot 的阶段、已批准阶段与等待状态统计,不依赖文件 revision。打开旧项目得到的是一次当前观察,不能当成这次用户新完成阶段;不由观察时间计算精确阶段耗时或审批操作次数。
- 快照是进度观测,原方案的 revision 有效变化率不能把快照数加进分子;策划进度与 Game Agent 成果变化分别统计。
- 自动保存、checkpoint 与手动保存分开解释;focus 仅为前台时长。
- 只采集创建成功事件无法计算创建成功率;不采 task_type 就不能按任务类型分组;不能拿 agent_type 冒充 task_type。
- 取消、崩溃与未闭合 run 未纳入失败事件,本版完成率是已知终态样本的完成率,不是所有提交的完成率。
## 9. 合规事件示例
以下是字段形状示例,ID、版本和时间均为示例值,生产必须从对应事实取得。
``` json
{
"schema_version" : 1 ,
"event_id" : "708cc064-e1ad-46d1-a26d-181432eef8aa" ,
"event_name" : "agent_run_completed" ,
"event_time" : "2026-09-21T10:30:00.000Z" ,
"user_id" : "123" ,
"editor_session_id" : "a5d5e098-e515-4b28-a27a-6018d47b55dd" ,
"project_id" : "gameagent-example-project" ,
"creative_task_id" : "gameagent-example-project" ,
"agent_run_id" : "36c70186-93c7-4e2d-ad07-80d6b91a1371" ,
"agent_turn_id" : null ,
"status" : "success" ,
"error_code" : null ,
"source" : "design_agent" ,
"client_version" : "0.1.0" ,
"properties" : {
"agent_type" : "design_agent" ,
"run_source" : "user_submit" ,
"duration_ms" : 32000 ,
"retry_index" : 0 ,
"output_change_detected" : false ,
"end_reason" : "waiting_for_user"
}
}
```
该例表示:本次策划执行正常停在等待用户澄清,已确认没有成果变化;不表示整个任务完成,不记录 revision_id。
## 10. 实施拆分与验收依据
本节仅列待评审的里程碑范围,不授权编码,不创建上传实现。
| 里程碑 | 范围 | 退出判据 |
| --- | --- | --- |
| A 数据合同与本地队列 | 强类型事件、捕获身份、JSONL 批次、有界异步写入与恢复、静默失败 | 字段校验、JSONL 读取、跨账号、不阻塞、损坏隔离、容量与幂等测试通过 |
| B 会话、窗口与项目 | start/end/focus/create/open,以及下次启动的本地 incomplete 标记 | 多窗口、重载、最小化、正常退出、旧实例存活识别、异常未闭合、项目重复打开测试与桌面 smoke |
| C 两类 Agent 与成果链路 | submit/run、策划进度快照、已有真实 revision、preview/save | 成功失败与等待、持久化触发快照与去重、重试关联、无变化、版本漂移、真实访问与保存边界全部可核对;不新建策划文件 revision 机制 |
| 后续独立阶段 | 上传主站与查询 | 另立规范;不在本版实施范围 |
实施前为选定里程碑生成单独实现计划,前一个里程碑评审/验收后再推进下一项。
最小验收矩阵:
| 场景 | 必须成立的结果 |
| --- | --- |
| 正常完整创作 | 读取本地 JSONL 批次得到原方案完整链路;事件 ID 各自唯一,关联 ID 同语义一致 |
| 策划澄清/审批/继续 | 同一 task、多次 run;等待不误报失败,不重复 task_submit |
| 策划状态保存与采集 | 保存成功后读取白名单;保存失败不伪造新状态;普通对话保存而阶段状态不变时不重复 |
| 策划项目 5 分钟内打开又关闭 | 打开时采初始快照,期间相关持久化触发采集;切换/关闭尽力补采,队列不随项目关闭消失 |
| 策划快照 A→B→A 与连续相同状态 | 前者三个观察都可记录;后者去重;采集时间仅为观察时间,不冒充审批发生时间 |
| 策划文件缺失、损坏、身份不符或快速切项目 | 静默跳过,不填伪默认阶段;异步结果不绑定到当前其他项目或其他用户 |
| 项目重开、改名、移动、切账号及策划转开发 | project_id 不变时 creative_task_id 恒定且等于 project_id;首次提交不因批次删除再产生 |
| 不同项目与旧项目首次观测 | 不同 project_id 对应不同目标;旧项目不补造历史首次提交,后续事件仍可关联目标 |
| 自动请求重试/用户重试 | 前者原 run,后者新 run; retry_index 口径一致 |
| 同请求重放/双回调 | 同事实不重复;故意重复相同文本的新操作不被误去重 |
| 纯聊天/相同内容保存 | 不生成有效 revision; output_change_detected 不误填 true |
| 失败前已有文件变化 | 可同时有 revision 和 run_failed,不因失败隐藏真实变更 |
| URL 返回但入口失败 | 没有 preview_ready |
| 保存失败/部分提交 | 没有 project_save success |
| A 发起后切 B | A 在途结果不归 B,B 的新操作归 B;平台分区不串 |
| 匿名后登录 | 历史匿名事件不回填用户;focus 归属正确切段 |
| 多窗口/最小化/重载 | 同实例不重复 session_start,前台并集不重计 |
| 强杀与下次启动 | 确认旧实例退出且旧会话未闭合后标记 incomplete;不伪造结束时间、run 失败、完整前台时长或 timeout 事件 |
| 旧实例仍运行、身份不确定或 PID 复用 | 不误标存活或归属不明的旧会话;再次启动不重复改变已确定的 incomplete 状态 |
| 队列满/只读目录/磁盘失败 | 正常创建、对话、保存仍继续,无弹窗;内部可观察丢弃计数 |
| 批次发布中断与重启 | 保留完整已发布事件批次,未发布临时目录可清理;不重新生成已发布事件 ID |
| JSONL 截断、坏行或身份冲突 | 损坏批次隔离并计数,不改写事实、不清空其他批次、不阻塞业务 |
| 数据最小化 | 正式事件和元数据符合白名单,日志不重复完整事件;无 Prompt、凭据或工具正文等禁止字段 |
| 本阶段运行 | 不因埋点发起 HTTP,不创建 15 分钟上传定时器或周期会话检查点,不补发 timeout,不删除“假确认”数据;本地批次封存定时不受影响 |
主要代码核查入口(相对仓库根目录):
- 生命周期与配置:`apps/ai-game-creator-shell/src-tauri/src/main.rs` 、`config.rs` 、`platform_session.rs` 。
- 项目创建与打开:`apps/ai-game-creator-shell/src-tauri/src/commands.rs` 、`apps/ai-game-creator-shell/src/App.tsx` 。
- Direct 执行与审计:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs` 、`agent/direct_runtime/mod.rs` 、`agent/direct_codex_audit.rs` 。
- Design 执行与持久化:`apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs` 、`agent/runtime_protocol/design_session.rs` 、`agent/design_tools.rs` 。
- revision: `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs` ,结合各实际写入调用方。
- 预览与保存:`apps/ai-game-creator-shell/src-tauri/src/preview.rs` 、`ui_editor/persistence.rs` 、`project/checkpoint.rs` 。
上述为定位线索,不表示这些函数已经完成埋点接入;正式实现必须再次核对当时源码。
## 11. 需要评审的口径与原需求差异
| 项目 | 本草案建议 | 未确认时的限制 |
| --- | --- | --- |
| 目标边界(已确认) | 用户已确认一个项目就是一个目标;方案据此令 creative_task_id=project_id | 不再需要消息意图分类或新增目标选择交互 |
| creative_task_submit 次数(草案建议) | 每个项目目标首次受理创作请求时一次,之后通过 run 表达继续执行 | 目标数不代表消息数;缺历史提交的旧项目不得伪造首次提交 |
| 异常会话处理(已确认) | 下次启动确认旧实例已退出且未正常结束,标记本地 incomplete;本版不采集 session_timeout | 无周期检查点、无超时阈值、无补造退出/前台时长;未再次启动则保持未闭合 |
| 等待用户(已确认) | completed + end_reason,后续继续为新 run | run 完成不是任务完成 |
| 取消与崩溃 run(已确认) | 本版不映射到失败事件 | 成功率仅覆盖已知正常/失败终态 |
| is_first_open | 本安装当前账号可观察范围,未知为 null | 不能声称全平台首次打开 |
| output_change_detected / duration_ms | 证据不足允许 null | 原方案未明确可空性,不能用 false/0 伪造确定值 |
| 策划进度(已确认) | 从持久化 session 提取白名单快照,打开/相关状态保存成功后触发,切换或关闭尽力补采,无变化去重 | 无定期采集;5 分钟仅用于封存;不能冒充完整操作流水或文件变更 |
| revision | Game Agent 与其他已有正式 revision 路径按真实变化采集;策划进度用快照,不为埋点新建策划 revision | 不把阶段、审批或快照当作文件 revision;剔除纯元数据同步 |
| 本地保留(已确认) | 最多 7 天或总量 20 MiB,任一条件达到即清理;超量先清最旧批次 | 未上传数据也会清理,本版不保证保留至上传功能上线;不删除项目及 Agent 业务数据 |
| 本地封存时间(已确认) | 当前批次首条事件进入后 5 分钟封存;无事件不生成空批次 | 与 15 分钟上传独立;强退可能丢失尚未落盘的一批事件 |
| 本地批量上限(草案建议) | 达到 500 条或 1 MiB 序列化事件大小提前封存 | 两个上限尚待评审,不影响已确认的 5 分钟时间条件 |
| 字段补充 | schema_version、focus_interval_id、run end_reason; incomplete 仅是本地生命周期状态 | 需与后续接收端按同版合同实现,不任意扩展 properties,不将本地状态冒充产品事件 |
| 本地格式(已确认) | 本阶段不加密,使用明文 JSONL 批次 | 无密钥前置要求;原字段白名单与数据最小化要求不变 |
| 未来确认删除 | 整批确认、整批删除,失败重发原事件批次 | 服务端仍需逐事件去重;暂不增加逐事件部分重写机制 |
## 12. 本次方案验证状态
已做:原产品方案逐项对照;核对现役 Direct/Design 持久化、项目创建、revision、预览、UI 保存与 checkpoint 的源码入口。本文新增合同和参数均以草案标识,不作为已经上线的事实。
本次将需求来源和当前技术方案纳入仓库并更新团队文档入口,不修改业务代码、数据库或网络行为;未运行客户端功能测试、真实窗口 smoke、主站 API 或数据库验证。文档编码、示例 JSON 与仓库文档检查结果随交付说明提供。