# Office UI Guide

本文件用于记录 `office UI` 相关的想法、讨论结论、约束和后续修正。

目标：
- 作为实现前的持续设计指南
- 作为后续页面开发时的范围边界
- 作为每次对话补充和纠偏的记录落点

约定：
- 后续在对话中凡是对 `office UI` 的想法有新增、修正、否定、细化，都继续更新本文件
- 这里记录的是当前共识，不代表已经完成实现

## 当前结论

`office UI` 不是传统意义上的“任务管理页”，更适合定义为：

一个面向长耗时、多 agent、多阶段任务的“运行态可视化总览页”。

当前开发阶段补充：
- 优先目标已切换为“稳定可用、稳定可演示”
- 当前阶段不再继续追求更复杂的像素美术细节
- 当前冻结项：
  - 四区地图结构
  - 当前家具资源方案
  - 当前角色资源方案
- 当前优先确保：
  - `/office/[taskId]` 真实可用
  - 角色状态有基础差异
  - 构建、测试、类型检查稳定

它借鉴 `Star-Office-UI` 的核心概念：
- 用办公室空间隐喻表达 agent 状态
- 让主 agent 和 sub-agent 的工作状态可视化
- 让用户快速感知“谁在做什么、是否卡住、任务进展到哪里”

但它不应直接替代现有结构化任务管理界面。

## 产品定位

建议定位：
- `office UI` = 总览层 / 态势层 / 趣味化运行时观察台

不建议定位：
- 唯一的任务管理入口
- 唯一的故障诊断入口
- 唯一的任务事实来源

原因：
- 办公室隐喻适合表达“状态”和“分工”
- 不适合单独承载依赖关系、失败原因、产物细节、真实阻塞点

## 核心设计思想

### 1. session、task、office 是三层关系

当前方向：
- `session` 用于维护长期对话上下文
- 用户在 session 中发起“让主 agent 干 XXX”即创建一个 `task`
- `office` 是这个 `task` 的运行态可视化现场

当前共识：
- 一个 session 可以包含多个 task
- 一个 task 对应一个临时 office
- task 结束后，office 解散，不作为长期容器保留

补充约束：
- office 解散指“退出活跃运行态视图”
- 不等于删除该 task 的历史事实
- task 完成后，仍应保留最小可回看的摘要、结果和产物入口

预期收益：
- 对话上下文和任务执行上下文分离
- office UI 可以只关注“当前这次任务在发生什么”
- 避免把整个 session 的历史信息都带进办公室画布

### 2. 办公室负责概览，不负责全部事实

办公室画布适合承载：
- 主 agent / sub-agent 在线状态
- 当前任务归属
- 工作区域映射
- 简化的进度感知
- 团队分工
- 当前办公室成员名单

办公室画布不应单独承载：
- 全量日志
- 复杂筛选
- 深层错误排查
- 完整产物浏览
- 精确任务编排

结论：
- 画布负责“扫一眼看懂”
- 结构化面板负责“点进去看清”

### 3. 趣味性可以有，但不能压过可读性

允许：
- 空间化布局
- 角色位置变化
- 轻量动效
- 有辨识度的视觉氛围

不建议：
- 过多持续动画
- 复杂装饰导致信息遮挡
- 仅为可爱而牺牲状态辨识度

## 页面建议结构

建议采用混合布局，而不是纯画布页。

### A. 办公室画布区

用于展示：
- 主 agent
- sub-agent
- 当前状态分布
- 所属任务或阶段
- 简化状态气泡

可能的空间分区示例：
- 接待区：待命 / 排队
- 工位区：执行中 / 处理中
- 会议区：同步 / 协作
- 研究区：检索 / 分析
- 故障区：异常 / 阻塞
- 完成区：已完成 / 待汇总

注意：
- 分区必须和真实状态枚举稳定映射
- 不能靠“视觉猜测”表达状态

### B. 任务看板区

用于展示当前办公室里的任务拆分情况，例如：
- 待开始
- 进行中
- 已完成
- 阻塞

要求：
- 看板是结构化事实，不是装饰物
- 每个任务卡应至少显示标题、负责人、状态、阶段、更新时间

### C. 进度区

可以有进度条，但要非常克制。

原则：
- 只有在后端有真实阶段数据时才显示百分比进度
- 如果没有可靠进度，只显示“阶段进度”而不是伪百分比

可以接受：
- `3 / 7` 子任务完成
- `当前阶段：资料检索`
- `已完成：需求分析 -> 分工 -> 编码中`

不建议：
- 没有依据的 `78%`
- 基于耗时猜测的 ETA

### D. 分工区

用于展示：
- 谁负责什么
- 主 agent 当前委派给了哪些 sub-agent
- 各 agent 当前所属阶段

### E. 办公室成员名单

用于展示：
- 当前 task 参与成员
- 角色类型
- 当前状态
- 最近更新时间

## 当前明确的产品判断

### 支持的判断

- 这个方向有价值
- 比纯日志或纯表格更适合“长耗时任务”的态势感知
- 对多 agent 协作特别有帮助
- 与 task 生命周期绑定是合理的
- 如果做得好，既能提高清晰度，也能提高使用乐趣

### 反对的判断

- 不应把它做成唯一任务管理页
- 不应让办公室隐喻替代真实任务数据
- 不应为了像素办公室效果而牺牲后台控制台的可操作性
- 不应直接照搬 `Star-Office-UI` 整套产品结构

## 关键风险

### 1. 隐喻过强，事实过弱

风险：
- 用户知道“谁在忙”，但不知道“为什么慢”

控制方式：
- 必须有结构化任务卡、状态、阶段、更新时间、负责人

### 2. 进度条失真

风险：
- 进度看起来很满，但实际任务没有可靠里程碑

控制方式：
- 没有真实进度数据就不用百分比

### 3. 页面职责过载

风险：
- 一个页面同时做总览、诊断、管理、配置，最后都做不强

控制方式：
- 明确 `office UI` 首先是总览页
- 诊断和深度操作应下钻到详细面板

### 4. 代码移植成本被低估

风险：
- `Star-Office-UI` 是完整产品，不是简单组件
- 直接搬代码的成本和适配复杂度可能高于重写

控制方式：
- 优先借鉴概念、布局语言和交互隐喻
- 代码层面只选择性复用可拆分部分

### 5. 资产许可风险

风险：
- 原项目代码和美术资产许可范围不同

控制方式：
- 直接复用图像和素材前，必须再次核对许可
- 有商用可能时，应准备自有素材方案 （暂时不商用）

## 实现原则

实现时建议遵守：
- 先做信息架构，再做美术包装
- 先做静态结构，再接实时数据
- 先保证状态映射准确，再增加趣味动效
- 先让用户看懂，再让页面有趣

## 与现有前端的关系

当前前端已经存在：
- 聊天工作台
- 执行过程展示
- 定时任务管理
- 智能体管理

因此 `office UI` 更像新增一个：
- `运行态总览页`
- 或者 `task 级运行态页面`

而不是替换这些已有页面。

## 适配现有数据结构的信息架构

当前前端已具备的关键数据：
- `sessionId`
- `sessions`
- `processRuns`
- `processEvents`
- `processArtifacts`
- `selectedRunId`
- run 的 `parent_run_id` 父子关系

现有数据模型说明：
- `session` 适合作为 task 的发起上下文，不适合直接等同办公室
- `ProcessRun` 更适合作为 `task / 子任务` 的基础执行实体
- `ProcessRun` 是最适合作为“任务实体”的基础对象
- `ProcessEvent` 是状态变化和过程流
- `ProcessArtifact` 是阶段产物和结果输出
- `parent_run_id` 可以推导主 agent 与 sub-agent 的委派关系

结论：
- `office UI` 第一版应围绕 `task -> run tree -> event/artifact` 构建
- `session` 只用于找到当前 task 的来源上下文
- 不要第一版就引入全新任务模型，除非现有结构明显不够

## 页面级信息架构

建议采用四层结构。

### 1. 办公室切换层

位置：
- 页面顶部或左侧一级导航区

作用：
- 切换不同 task 对应的办公室
- 展示当前办公室的摘要状态

建议展示字段：
- 办公室名称
- 对应 task 标题
- 最近活跃时间
- 运行中 agent 数
- 阻塞 / 错误数

数据来源建议：
- 当前 session 中可查看的 task 列表
- 当前 task 下筛选后的 `processRuns`

### 2. 办公室画布层

位置：
- 页面主视觉中心

作用：
- 用空间化方式展示当前 task 内主 agent / sub-agent 的实时分布

画布中的基本对象建议：
- 办公室房间 / 分区
- agent 角色卡
- 当前任务气泡
- 简化状态标识
- 高风险提醒标识

数据映射建议：
- 一个 `agent run` 对应一个画布中的“办公人员状态实例”
- 同一 agent 多个并发 run 时，需要有明确合并策略

第一版建议合并策略：
- 只显示每个 agent 最近一个活跃 run
- 若存在 `running` / `waiting`，优先显示活跃 run
- 已完成 run 不长期占据画布主位

### 3. 结构化事实层

位置：
- 画布右侧栏或下方主面板

作用：
- 承载不能只靠视觉隐喻表达的真实任务信息

建议包含 4 个模块：
- 任务看板
- 进度区
- 分工区
- 成员名单

### 4. 下钻详情层

位置：
- 抽屉、侧边详情面板或弹层

作用：
- 用户点击画布中的 agent、任务卡或异常标识后查看详情

建议展示：
- 当前 run 摘要
- 最近事件
- 当前状态
- 产物列表
- 失败原因
- 上下游关系

## 元素归位规则

本节用于明确什么应该放在办公室画布，什么不应该。

### 应该放在办公室画布里的元素

适合放入画布的前提：
- 用户需要扫一眼快速理解
- 信息可以被抽象成状态、位置、分工、风险
- 不依赖长文本解释

具体包括：
- 主 agent
- sub-agent
- agent 当前状态
- agent 当前负责的任务短标题
- 所在阶段的标签
- 阻塞 / 错误告警标记
- 办公室内的功能分区
- 简化进度感，例如“阶段 2/4”或“3 个子任务进行中”
- 当前在线成员总览

这些信息适合视觉化，因为：
- 强调态势而不是细节
- 适合空间映射
- 能快速建立“工作现场感”

### 不应该直接放在办公室画布里的元素

以下内容不适合直接塞进画布主体：
- 全量事件流
- 全量日志文本
- 复杂任务依赖图
- 多条件筛选器
- 大量操作按钮
- 大段失败堆栈
- 完整 artifact 内容
- 大量表格字段
- 精确排程配置

原因：
- 会破坏扫视效率
- 会让画布从“总览层”退化成“信息垃圾堆”
- 视觉隐喻会被结构化细节打散

结论：
- 画布里只保留高信号、低文本密度的信息
- 深度信息必须进入结构化面板或详情抽屉

## 现有数据到页面模块的映射建议

### 办公室

建议实体：
- `office = current task`

需要的基础字段：
- task 标题
- task 创建时间
- 最近活跃时间
- 所属 `sessionId`

如果 task 当前没有独立名称：
- 第一版可以显示为主 run 标题或默认编号

### 办公人员

建议实体：
- `office member = actor`

主要来源：
- `processRuns.actor_id`
- `processRuns.actor_name`
- `processRuns.actor_type`

注意：
- 成员名单应按 actor 聚合，而不是按 run 平铺
- 否则一个 agent 多次执行会在名单中重复出现

### 任务卡

建议实体：
- `task card = root run 或独立 run`

第一版推荐：
- 一个 office 绑定一个主 task
- 以 `root run` 作为该 task 的主执行单元
- 其 `member runs` 视为子任务 / 分工项

依据：
- 现有 `parent_run_id` 已足够支撑这种树状关系
- 现有聊天页中的 team group 逻辑已经在这么做

建议字段：
- `title`
- `status`
- `actor_name`
- `started_at`
- `finished_at`
- `summary`
- 子任务数量
- 最近更新时间

### 进度

当前数据模型下，最安全的进度表达方式：
- 按子任务完成数计算
- 按状态分布计算
- 按阶段标签计算

当前不安全的进度表达方式：
- 伪百分比
- 纯时间推算 ETA

第一版建议的进度来源优先级：
1. `metadata` 中显式阶段信息
2. `member runs` 的完成数量
3. `run status` 分布

### 分工

建议实体：
- `assignment = root run -> member runs`

展示方式：
- 主 agent
- 子 agent 列表
- 每个子 agent 的当前标题 / 阶段 / 状态

适合来源：
- `parent_run_id`
- `actor_name`
- `title`
- `status`

### 详情抽屉

建议实体：
- `detail drawer = selected run`

主要来源：
- `selectedRunId`
- 对应 run 的 `events`
- 对应 run 的 `artifacts`

这层应承接：
- 任务摘要
- 最近事件时间线
- 产物列表
- 失败信息

## 第一版页面布局建议

推荐布局：

### 顶部

- task office 切换器
- 当前办公室摘要
- 总体状态统计

### 中部左侧

- 办公室画布

### 中部右侧

- 任务看板
- 进度区
- 分工区

### 底部或右侧抽屉

- 成员名单
- 任务详情
- 事件与产物

这样的好处：
- 左边负责“看态势”
- 右边负责“看事实”
- 抽屉负责“看细节”

## 第一版强约束

为了避免首版失控，建议强约束如下：

- 一个 task 对应一个办公室
- office 生命周期跟随 task，task 结束后 office 解散
- 画布只展示 agent 和简化任务态势，不展示全量任务文字
- 任务看板只展示 root run 及其子任务摘要
- 进度不用伪百分比
- 详情只对单个 run 下钻
- 不在首版加入自由拖拽装修系统
- 不在首版复刻像素资产编辑系统

## 当前建议的数据派生逻辑

建议在前端增加一层 office view model，而不是直接把 store 数据原样灌进 UI。

建议派生对象：
- `OfficeView`
- `OfficeMemberView`
- `OfficeTaskView`
- `OfficeAssignmentView`
- `OfficeAlertView`

建议派生过程：
1. 先识别当前 task 的 root run
2. 收集该 root run 及其子树中的 runs / events / artifacts
3. 将 `runs` 按 `parent_run_id` 组织成树
4. 按 `actor_id` 聚合成员状态
5. 从状态和 metadata 推导画布分区
6. 将异常和阻塞单独提取为 alert

这样做的好处：
- 避免页面组件直接承担复杂归并逻辑
- 便于后续替换视觉方案而不动数据组织

当前 task 的推荐判定规则：
- 用户在 session 中发起一次主请求，即创建一个主 task
- 主 agent 开始执行，到主 agent 向用户给出最终回复为止，视为该 task 生命周期
- 该期间产生的 root run 及其子 runs 构成一个 office
- task 结束后，office 从活跃列表移除，但 task 摘要不应丢失

## Task 识别规则

这一节用于把“什么算一个 task”说清楚，否则 office UI 很容易做成不稳定状态机。

### task 定义

建议定义：
- task = 用户在 session 中发起的一次主请求所触发的完整执行周期

起点：
- 主 agent 接收到该次用户请求
- 对应 root run 被创建

终点：
- 主 agent 针对该次请求输出最终用户可见回复
- 对应 root run 进入终态：`done` / `error` / `cancelled`

一个 task 内可以包含：
- 主 agent 自身执行
- sub-agent 委派
- MCP 调用
- 中间产物
- 中间进展事件

一个 task 不应跨越：
- 多次独立用户请求
- 不同 session
- 已结束后再次重新触发的新一轮执行

### 第一版 task 主键建议

首选：
- `taskId = root run_id`

原因：
- 当前数据里最稳定的执行主键就是 `run_id`
- `parent_run_id` 已能自然形成 task 树
- 前端不需要首版额外等后端提供新 ID

### 第一版 task 归属规则

推荐规则：
1. 找到 `parent_run_id = null` 的主 run
2. 将该 run 及其所有子孙 runs 视为一个 task
3. 该 task 的 office 生命周期跟随 root run 生命周期

边界情况：
- 如果一个用户请求触发多个并列 root run，第一版必须选一个明确策略

第一版推荐策略：
- 默认一个用户请求只允许一个主 root run
- 如果出现多个并列 root run，先按时间拆成多个 task
- 不在首版支持“一个 task 多个 root run 合并”

### task 状态映射建议

建议 task 状态从 root run 派生：
- `waiting` -> 待启动
- `queued` -> 排队中
- `running` -> 进行中
- `done` -> 已完成
- `error` -> 失败
- `cancelled` -> 已取消

补充派生状态：
- `blocked`：仅当存在明确阻塞信号时额外推导，不要乱推

### task 标题生成建议

优先级：
1. root run.title
2. 用户该轮请求的首句摘要
3. 默认标题，例如 `Task 2026-03-24 14:32`

要求：
- 标题应短
- 可用于列表和 office 顶部摘要
- 不依赖长文本解析

## 路由与入口建议

这一节用于明确 `office UI` 在当前前端中的落点。

### 页面关系

建议关系：
- `session page` 负责对话
- `office page` 负责展示某个 task 的运行态

两者是关联页面，不是同页双模式。

### 路由建议

第一版推荐：
- 列表页：`/office`
- 详情页：`/office/[taskId]`

路由职责：
- `/office`
  - 查看活跃 office 列表
  - 查看最近结束 task 摘要
  - 支持从 session 跳转过来的当前 task 高亮
- `/office/[taskId]`
  - 查看某个 task 的办公室画布
  - 查看对应结构化面板和详情抽屉

### 入口建议

首选入口：
- 聊天页在主 agent 开始执行后，出现“查看任务现场”入口
- 点击后跳转 `/office/[taskId]`

辅助入口：
- 顶部导航新增 `Office`
- 进入 `/office` 查看当前活跃任务与最近任务

不建议的入口方式：
- 强制把用户从聊天页自动跳走
- 每次任务启动都弹窗打断对话

### 与现有导航的关系

建议：
- 顶部导航增加 `Office`
- 但不要把 `Office` 放成比 `对话` 更中心的一级任务入口

原因：
- 主工作流仍然是对话发起任务
- `office UI` 是运行态观察层，不是主输入层

## 首版页面线框

本节提供实现时可直接参考的结构。

### `/office` 列表页

建议结构：
- 顶部标题区
- 活跃 office 列表
- 最近结束 task 列表
- 过滤栏：全部 / 进行中 / 失败 / 最近完成

每个卡片建议包含：
- task 标题
- 来源 session 标识
- 主 agent 名称
- 当前状态
- 最近活跃时间
- sub-agent 数量
- 错误 / 告警数
- “进入办公室”按钮

页面目标：
- 让用户快速找到正在跑的 task
- 让用户快速回看最近结束的 task

### `/office/[taskId]` 详情页

建议结构：
- 顶部摘要条
- 主画布区
- 右侧事实面板
- 底部或侧边详情抽屉

#### 顶部摘要条

建议包含：
- task 标题
- task 状态
- 来源 session 入口
- 创建时间 / 运行时长
- 返回对话按钮

#### 主画布区

建议包含：
- 办公室背景分区
- 主 agent 卡
- sub-agent 卡
- 状态气泡
- 简化分工连线或区域归属

#### 右侧事实面板

建议包含 4 个模块：
- 任务看板
- 当前进度
- 分工面板
- 成员名单

#### 底部或侧边详情抽屉

点击对象后展示：
- run 摘要
- 最近事件
- 产物列表
- 错误信息

## 首版组件清单

建议先拆成以下组件，而不是把整个页面写成单文件。

### 页面级组件

- `OfficeListPage`
- `OfficeDetailPage`

### 数据组装层

- `buildOfficeView(taskId, storeState)`
- `buildOfficeTaskList(sessionId?, storeState)`

### 列表页组件

- `OfficeTaskCard`
- `OfficeTaskStatusBadge`
- `OfficeTaskFilterBar`

### 详情页组件

- `OfficeHeader`
- `OfficeCanvas`
- `OfficeZones`
- `OfficeAgentCard`
- `OfficeTaskBoard`
- `OfficeProgressPanel`
- `OfficeAssignmentPanel`
- `OfficeRosterPanel`
- `OfficeDetailDrawer`

### 公共派生组件

- `TaskDuration`
- `TaskStageBadge`
- `AgentStateDot`
- `OfficeAlertPill`

## 首版交互原则

### 列表页

- 单击卡片进入 office 详情
- 已结束 task 默认折叠到“最近任务”区
- 活跃 task 永远优先展示在上方

### 详情页

- 点击 agent 卡，打开对应 run 详情
- 点击任务卡，定位到对应分工或详情
- 点击 session 入口，返回聊天页对应会话

### 任务结束时

- 如果用户正在 office 详情页内，不强制跳走
- 页面显示“任务已完成，办公室已解散”
- 保留摘要和结果查看能力
- 从活跃状态切换为历史状态

## 当前实现建议

如果按现有代码结构推进，推荐顺序：

1. 先新增 `/office` 和 `/office/[taskId]` 路由骨架
2. 先做 `OfficeView` 派生函数
3. 先接真实 `processRuns / processEvents / processArtifacts`
4. 再做静态办公室画布
5. 最后补动效和视觉包装

这样可以避免先做出一套漂亮但没有稳定数据归属的空画布。

## OfficeView 类型设计

这一节定义首版前端建议使用的 view model，目的是隔离原始 store 数据和页面 UI。

### 顶层对象

```ts
type OfficeTaskStatus =
  | 'queued'
  | 'running'
  | 'waiting'
  | 'done'
  | 'error'
  | 'cancelled'
  | 'blocked';

type OfficeZoneId =
  | 'reception'
  | 'workspace'
  | 'collab'
  | 'research'
  | 'alert'
  | 'done';

interface OfficeView {
  officeId: string;
  taskId: string;
  sessionId: string | null;
  title: string;
  status: OfficeTaskStatus;
  createdAt: string;
  updatedAt: string;
  finishedAt: string | null;
  durationMs: number | null;
  sourceSessionLabel: string;
  rootRunId: string;
  rootActorName: string;
  currentStageLabel: string | null;
  progress: OfficeProgressView;
  stats: OfficeStatsView;
  alerts: OfficeAlertView[];
  zones: OfficeZoneView[];
  members: OfficeMemberView[];
  tasks: OfficeTaskView[];
  assignments: OfficeAssignmentView[];
  detailRunIds: string[];
}
```

说明：
- `officeId` 和 `taskId` 第一版可以相同，均使用 `rootRunId`
- `status` 以 root run 为主，再允许前端推导 `blocked`
- `detailRunIds` 用于详情抽屉的可选对象列表

### 进度对象

```ts
interface OfficeProgressView {
  mode: 'stage' | 'ratio' | 'status';
  label: string;
  value: number | null;
  max: number | null;
  stageLabel: string | null;
}
```

约束：
- `value/max` 只在有可靠分子分母时填写
- 没有可靠比值时只显示 `stageLabel`

### 统计对象

```ts
interface OfficeStatsView {
  totalRuns: number;
  activeRuns: number;
  doneRuns: number;
  errorRuns: number;
  cancelledRuns: number;
  memberCount: number;
  artifactCount: number;
}
```

### 分区对象

```ts
interface OfficeZoneView {
  id: OfficeZoneId;
  label: string;
  memberIds: string[];
  taskIds: string[];
  tone: 'neutral' | 'info' | 'warn' | 'danger' | 'success';
}
```

### 成员对象

```ts
interface OfficeMemberView {
  memberId: string;
  actorId: string;
  actorName: string;
  actorType: 'agent' | 'mcp' | 'system';
  status: OfficeTaskStatus;
  zoneId: OfficeZoneId;
  currentRunId: string;
  currentTitle: string;
  stageLabel: string | null;
  summary: string | null;
  startedAt: string | null;
  updatedAt: string | null;
  finishedAt: string | null;
  childRunIds: string[];
  artifactCount: number;
  isPrimary: boolean;
}
```

设计原则：
- `member` 是按 actor 聚合，不是简单复制 run
- `currentRunId` 指向该 actor 当前最值得展示的 run

### 任务对象

```ts
interface OfficeTaskView {
  taskId: string;
  runId: string;
  parentRunId: string | null;
  actorId: string;
  actorName: string;
  actorType: 'agent' | 'mcp' | 'system';
  title: string;
  status: OfficeTaskStatus;
  stageLabel: string | null;
  summary: string | null;
  startedAt: string;
  updatedAt: string;
  finishedAt: string | null;
  childTaskIds: string[];
  artifactCount: number;
  errorText: string | null;
  isRoot: boolean;
}
```

说明：
- `OfficeTaskView` 面向任务看板
- `taskId` 第一版可直接等于 `runId`

### 分工对象

```ts
interface OfficeAssignmentView {
  ownerRunId: string;
  ownerActorName: string;
  assigneeRunIds: string[];
  assigneeActorNames: string[];
  label: string;
}
```

### 告警对象

```ts
interface OfficeAlertView {
  id: string;
  level: 'info' | 'warn' | 'error';
  title: string;
  description: string | null;
  runId: string | null;
  actorId: string | null;
  createdAt: string;
}
```

## buildOfficeView 派生规则

这一节定义如何从现有 store 数据推导 `OfficeView`。

### 输入

建议输入：

```ts
interface BuildOfficeViewInput {
  taskId: string;
  sessions: Session[];
  processRuns: ProcessRun[];
  processEvents: ProcessEvent[];
  processArtifacts: ProcessArtifact[];
}
```

### 第一步：定位 root run

规则：
- `taskId` 第一版默认等于 `rootRunId`
- 在 `processRuns` 中找到 `run_id === taskId` 的 run
- 若找不到则返回 `null`

失败处理：
- 详情页展示 “任务不存在或已失效”

### 第二步：收集 task 子树

规则：
- 从 root run 出发
- 递归收集所有 `parent_run_id === 当前 run_id` 的子孙 runs
- 形成当前 task 的完整 run tree

派生结果：
- `taskRuns`
- `taskRunIds`

### 第三步：收集 events / artifacts

规则：
- `taskEvents = processEvents.filter(event => taskRunIds.has(event.run_id))`
- `taskArtifacts = processArtifacts.filter(artifact => taskRunIds.has(artifact.run_id))`

### 第四步：派生 task 更新时间

优先级：
1. taskEvents 中最新 `created_at`
2. taskArtifacts 中最新 `created_at`
3. root run.finished_at
4. root run.started_at

### 第五步：派生 task 状态

规则：
- 默认直接取 root run.status
- 只有在存在明确阻塞信号时才提升为 `blocked`

第一版阻塞信号建议：
- root run.status 为 `waiting`
- 且在一段阈值时间内没有新的 progress / artifact / status 更新

注意：
- `blocked` 是前端推导态，不是后端事实态
- 第一版如果证据不足，可以先不启用 `blocked`

### 第六步：派生阶段标签

优先级建议：
1. `rootRun.metadata.stage_label`
2. 最近一个相关 event 的 `metadata.stage_label`
3. 基于状态的回退文案，例如“执行中”“等待中”“已完成”

第一版要求：
- 没有可靠阶段字段就显示空，不要硬猜复杂阶段

### 第七步：派生成员列表

规则：
- 按 `actor_id` 分组 taskRuns
- 每组选择“当前展示 run”

当前展示 run 选择优先级：
1. `running`
2. `waiting`
3. `queued`
4. 最近更新的非终态 run
5. 最近完成的终态 run

然后派生：
- `status`
- `zoneId`
- `currentTitle`
- `artifactCount`
- `childRunIds`

### 第八步：派生任务看板

规则：
- 将 taskRuns 全量转为 `OfficeTaskView`
- 保留树形关系 `parentRunId -> childTaskIds`
- root run 置 `isRoot = true`

排序建议：
1. `isRoot`
2. 非终态优先
3. 最近更新时间倒序

### 第九步：派生分工关系

规则：
- 对每个有子 run 的 run 生成一条 `OfficeAssignmentView`
- owner = 当前 run
- assignee = 直接 children

### 第十步：派生告警

第一版告警来源建议：
- `run.status === error`
- 最近 `run_status` / `run_finished` 事件中有错误摘要
- 长时间 `waiting`

输出原则：
- 少而准
- 不在首页堆满低价值提醒

### 第十一步：派生分区

第一版分区映射建议：
- `queued` -> `reception`
- `waiting` -> `collab`
- `running`
  - actor_type 为 `mcp` 时可放 `research`
  - 其他默认放 `workspace`
- `error` -> `alert`
- `cancelled` -> `alert`
- `done` -> `done`

注意：
- 分区映射是 UI 规则，不是业务事实
- 必须保持稳定，不能频繁变动语义

### 第十二步：派生进度

优先级建议：
1. 若存在明确阶段总数与当前阶段，使用 `mode = ratio`
2. 若仅能得到子任务完成数，使用 `mode = ratio`
3. 若只能得到当前阶段标签，使用 `mode = stage`
4. 否则使用 `mode = status`

第一版推荐公式：
- `value = doneRuns`
- `max = totalRuns`

注意：
- 这只是“子任务完成比例”，不是业务整体真实完成度
- 文案必须明确，例如“已完成子任务 3 / 7”

## 字段来源对照

### 来自 ProcessRun

- `run_id`
- `parent_run_id`
- `session_id`
- `actor_id`
- `actor_name`
- `actor_type`
- `title`
- `status`
- `started_at`
- `finished_at`
- `summary`
- `metadata`

### 来自 ProcessEvent

- `created_at`
- `text`
- `status`
- `kind`
- `metadata`

用途：
- 更新时间
- 阶段标签
- 告警摘要
- 最近进展

### 来自 ProcessArtifact

- `artifact_id`
- `run_id`
- `title`
- `artifact_type`
- `created_at`

用途：
- 产物数量
- 最近活跃时间
- 结果完成感

### 来自 Session

- `key`
- `created_at`
- `updated_at`

用途：
- office 返回入口
- source session label

## 当前实现提醒

### 不要在类型层做的事

- 不要把办公室坐标直接写死进 `OfficeView`
- 不要把动画状态混进数据模型
- 不要把完整 event 文本直接塞进成员对象

### 可以在 UI 层处理的事

- agent 卡的像素样式
- 分区背景样式
- 动画和入场过渡
- hover / selection / drawer open 状态

## 当前明确不该做的事

第一版不要做：
- 在画布上塞完整聊天消息
- 在画布上展示原始 event 列表
- 让一个 agent 因多个 run 复制出多个角色分身且没有解释
- 使用没有依据的任务百分比
- 把任务配置、任务编辑、故障排查都塞到同一屏

## 本次补充结论

本次新增共识：
- `session` 和 `office UI` 是分开的页面
- `session` 负责维护对话
- `task` 是用户在 session 中发起的一次主 agent 执行请求
- `office` 是 task 的临时运行态空间，不是 session 的长期容器
- `office UI` 第一版应直接建立在现有 `processRuns + processEvents + processArtifacts` 之上
- `root run / member runs` 结构适合作为任务看板和分工区的基础
- 办公室画布只表达态势，不承载深度细节
- 深度细节通过结构化面板和详情抽屉承接
- 页面应按“概览层 -> 事实层 -> 详情层”分层，而不是全部平铺

## 暂定非目标

当前不把以下内容设为首期目标：
- 完整项目管理系统
- 甘特图级别依赖编排
- 精确工时统计
- 复杂权限编排
- 复刻 `Star-Office-UI` 的全部玩法和资产系统

## 后续需要继续明确的问题

- 办公室切换是放在聊天页内，还是独立路由页
- 一个 session 中多个 task 的切换入口应该如何暴露
- 任务看板的数据源来自 process runs、cron、会话消息，还是新的聚合接口
- “分工” 是自动从委派关系推导，还是允许人工编辑
- 进度计算是否需要后端显式上报阶段
- 画布是否允许拖拽布局，还是固定布局
- 是否采用像素风，还是只保留办公室隐喻、不使用像素素材

## 更新记录

### 2026-03-24

首次创建，记录当前共识：
- 借鉴 `Star-Office-UI` 的办公室可视化概念
- 目标是长耗时、多 agent 任务的运行态总览
- session 和 office UI 分离
- session 用于维护对话
- task 是一次主 agent 执行周期
- 一个 task 对应一个临时 office，task 结束则 office 解散
- 办公室中可包含任务看板、进度、分工、人员名单
- 但办公室页不应替代结构化任务管理和详细诊断视图

同日实现进展：
- 已新增 `/office` 列表页骨架
- 已新增 `/office/[taskId]` 详情页骨架
- 已在顶部导航加入 `Office` 入口
- 已实现 `buildOfficeView` 和 `buildOfficeTaskList`
- 第一版页面已接入真实的 `processRuns / processEvents / processArtifacts` 数据
- 已补 `OfficeView` / `OfficeTaskList` 的单元测试
- 已在聊天页加入直达当前 task 对应 `/office/[taskId]` 的入口
- 已对 office 详情页的画布分区做第一轮视觉打磨

同日导航结构修正：
- 一级导航将 `Office` 和 `定时任务` 合并为 `任务管理`
- `任务管理` 默认跳转到 `/office`
- `任务管理` 内部再分两个子页：
  - `Office`
  - `定时任务`
- `Office` 列表页、`Office` 详情页、`定时任务` 页共享同一组二级页签
- 路由保持不变：
  - `/office`
  - `/office/[taskId]`
  - `/cron`

同日视觉方向补充：
- `Office` 画布应继续向“像素风游戏感”靠拢
- 目标不是简单套一层像素贴图，而是让页面更像一个可观察的 2D 办公室场景
- 第一优先级是空间感、角色感、状态感
- 第二优先级才是像素装饰、像素字体、像素动效
- 第一版技术选型改为 `Phaser`
- `Phaser` 只负责办公室场景画布
- 结构化面板、任务看板、详情抽屉仍保留在 React 页面层
- 已把 `/office/[taskId]` 进一步改成更接近 `Star-Office-UI` 的舞台式布局
- 页面主结构调整为：上方像素办公室舞台 + 下方像素面板 + 右侧结构化任务板

像素风画布的实现原则：
- 画布先像一个房间，再像一个后台页面
- 成员先像场景角色，再像数据卡片
- 状态先通过位置、朝向、动作、灯光表达，再补文字标签
- 仍然必须保留结构化信息入口，不能为了游戏感牺牲可读性

第一版建议做的视觉元素：
- 固定视角办公室平面布局
- 像素化地板、隔断、会议桌、工位、服务器角、归档区
- 每个 agent 使用统一规则的小型像素角色牌
- 用状态色和状态动作表达：
  - `running`：轻微闪动、桌面亮起
  - `waiting`：低饱和、静止
  - `blocked`：红色告警灯或感叹号
  - `done`：柔和高亮或归档动作
- 用连接线或小型任务浮标表达主 agent 与 sub-agent 的委派关系

第一版不要做的伪像素化处理：
- 整屏强行加像素滤镜
- 用大量像素素材堆满背景
- 为了复古感牺牲文字可读性
- 把所有信息都做成悬浮气泡导致画面很吵
- 让所有成员持续运动，破坏扫描效率

推荐的实现顺序：
1. 先把画布从“分区卡片网格”改成“房间式布局”
2. 再把成员卡改成带角色感的场景单位
3. 再补状态动画、灯光、任务浮标
4. 最后才决定是否引入真正的像素素材或像素字体

### 2026-03-25

像素风实现路线补充：
- 当前版本虽然使用了 `Phaser`，但主要仍是程序化矩形和几何块体，观感更接近“示意图”而不是“像素游戏房间”
- 与 `Star-Office-UI` 的核心差距不在是否使用 `Phaser`，而在是否采用了完整的像素游戏生产方式
- 后续不应继续主要依赖 `graphics` / `rectangle` 拼场景，而应转向 `tileset + sprite + atlas + 低分辨率放大`

像素风的 6 个必要层级：
1. 低分辨率逻辑画布，再整数倍放大
2. 统一调色板，而不是自由配色
3. 像素素材驱动，而不是几何图形驱动
4. 固定房间语法和明确空间分区
5. 克制的帧动画和状态动画
6. Phaser 世界层与 React / DOM 信息面板分离

建议工具链：
- 场景引擎：`Phaser`
- 角色 / 家具 / 图块绘制：`Aseprite`
- 平铺地图编辑：`Tiled`
- 最终资源形式：`tileset png + sprite sheet png + json atlas`

建议目录结构：
- `public/office/tiles/`
- `public/office/sprites/`
- `public/office/atlas/`
- `public/office/maps/`
- `components/office/phaser/`

重做顺序：
1. 先确定逻辑分辨率，例如 `400x225` 或 `480x270`
2. 先定义办公室专用调色板，控制主色、阴影色、灯光色、状态色
3. 先做地板、墙体、隔断、桌椅、服务器、归档箱的 tileset
4. 再做 agent 角色的最小动画集：
   - idle
   - walk
   - type
   - blocked
   - done
5. 用 `Tiled` 产出固定房间地图，而不是继续在代码里硬编码大量图形
6. 在 `Phaser` 中加载 tilemap、tileset、sprite atlas，按图层拼装办公室
7. 把运行状态映射成“房间位置 + 动画 + 状态浮标”，而不是主要靠文字标签
8. React 页面继续只承载：
   - 昨日小记
   - 任务看板
   - 办公人员名单
   - 详情抽屉

当前代码层面的明确改造方向：
- [components/office/OfficePhaserCanvas.tsx](/home/ivan/xuan/nano_project/app-instance/frontend/components/office/OfficePhaserCanvas.tsx) 不应继续扩展矩形绘制细节
- 下一版应拆成：
  - 场景资源加载
  - tilemap 创建
  - 成员 sprite 实例化
  - 动画注册
  - run -> scene state 映射
- 角色、家具、房间应优先做成资源文件，而不是继续写死在 TSX 内

明确的非目标：
- 不靠整页像素滤镜制造“伪像素感”
- 不靠像素字体替代真正的像素场景
- 不在没有素材体系前继续微调现有程序化块体配色

本轮进一步确定：
- `Office` 像素画布的世界坐标固定为 `400x225`
- 内部渲染分辨率提升为 `800x450`
- 最终显示采用整数倍缩放，不做非整数拉伸
- 调色板方向参考 `openclaw-theme-stardew` 的冬季主题
- 视觉氛围关键词：
  - 冬夜深蓝
  - 冰霜蓝灰
  - 雪白文字
  - 壁炉橙点缀

冬季基础调色板：
- 背景主色：`#1a2433`
- 背景次色：`#232f42`
- 背景抬升：`#2c3a52`
- 卡片/家具中间色：`#3a4a62`
- 边框色：`#4a5a72`
- 边框高亮：`#5a7092`
- 文本主色：`#e8f0f8`
- 文本强色：`#f5faff`
- 弱文本：`#8aa0b8`
- 主强调色：`#ff8a65`
- 强调高亮：`#ffab91`
- 焦点/冷光：`#90caf9`

场景层建议的派生色：
- 地板深色：`#243041`
- 地板亮格：`#31425b`
- 墙体阴影：`#17202d`
- 窗外夜色：`#101827`
- 屏幕冷光：`#8fd3ff`
- 完成态绿：`#78c27a`
- 阻塞态红：`#d96c75`
- 等待态米黄：`#d8c79a`

第一批素材最小集合：
- 房间 tileset
  - 墙体
  - 墙角
  - 地板
  - 地毯
  - 窗户
- 家具 sprites
  - 工位桌
  - 椅子
  - 会议桌
  - 沙发
  - 茶几
  - 服务器柜
  - 归档箱
  - 植物
  - 灯
- 角色 sprites
  - 主 agent
  - 通用 sub-agent
  - 访客 / 等待角色
- 状态 sprites
  - 感叹号
  - 任务气泡
  - 屏幕闪烁
  - 告警灯
  - 完成勾标

角色第一版动画最小集合：
- `idle`
- `walk`
- `type`
- `blocked`
- `done`

素材制作顺序：
1. 先做房间 tileset，再做家具
2. 家具完成后先在 `Tiled` 里搭出固定办公室
3. 最后再做角色和状态动画
4. 没有房间和家具前，不先做复杂角色细节

第一批素材规格书：

基础尺寸约定：
- 基础 tile 尺寸统一为 `16x16`
- 大件家具优先按 `16` 的倍数出图，避免后续拼接时出现半格对齐
- 角色单帧基准尺寸先定为 `16x24`
- 小型状态图标基准尺寸先定为 `8x8` 或 `12x12`
- UI 面板仍由 React / CSS 承载，不纳入本批像素素材制作

地图网格约定：
- 逻辑分辨率：`400x225`
- 地图绘制网格：`16x16`
- 可视区域理论格数约为：
  - 宽 `25` 格
  - 高 `14` 格
- 允许顶部或底部保留少量非满格安全边距，但房间主体尽量在整数格内完成

tileset 规格：
- 文件：`public/office/tiles/office-winter-tileset.png`
- 单格：`16x16`
- 首版建议内容：
  - 墙面浅色 tile
  - 墙面阴影 tile
  - 内墙/隔断 tile
  - 地板深色 tile
  - 地板亮色 tile
  - 地毯边缘 tile
  - 地毯中心 tile
  - 窗框 tile
  - 窗外夜景 tile
  - 踢脚线 / 边框 tile

家具 sprite 规格：
- 文件目录：`public/office/sprites/furniture/`
- 命名规则：
  - `desk-workstation.png`
  - `chair-office.png`
  - `table-meeting.png`
  - `sofa-2seat.png`
  - `table-coffee.png`
  - `rack-server.png`
  - `crate-archive.png`
  - `lamp-floor.png`
  - `plant-office.png`
- 建议尺寸：
  - 工位桌：`32x16`
  - 椅子：`16x16`
  - 会议桌：`32x24`
  - 双人沙发：`32x16`
  - 茶几：`16x16`
  - 服务器柜：`16x32`
  - 归档箱：`16x16`
  - 落地灯：`16x32`
  - 植物：`16x24`

角色 sprite 规格：
- 文件目录：`public/office/sprites/agents/`
- 命名规则：
  - `agent-main.png`
  - `agent-worker.png`
  - `agent-visitor.png`
- 单帧尺寸：`16x24`
- 第一版朝向：
  - 先只做正面 / 背面 / 侧面三向
- 第一版动画帧数建议：
  - `idle`: `2` 帧
  - `walk`: `4` 帧
  - `type`: `2` 帧
  - `blocked`: `2` 帧
  - `done`: `2` 帧

状态图标规格：
- 文件目录：`public/office/sprites/status/`
- 命名规则：
  - `icon-alert.png`
  - `icon-task.png`
  - `icon-done.png`
  - `icon-wait.png`
  - `light-warning.png`
- 建议尺寸：
  - 感叹号：`8x8`
  - 任务气泡：`12x12`
  - 完成标记：`8x8`
  - 等待气泡：`8x8`
  - 告警灯：`8x8`

第一张办公室地图必须包含的区域：
- 左侧工位区
- 中央协作区
- 右侧服务器 / 监控区
- 下侧归档区
- 上侧窗户 / 夜景区

第一张办公室地图不做的内容：
- 多房间切换
- 可拖拽家具
- 复杂门禁
- 楼梯 / 二层结构
- 动态天气

Tiled 第一版图层建议：
- `bg-floor`
- `bg-rug`
- `walls`
- `windows`
- `furniture-base`
- `furniture-top`
- `markers`
- `collision`

第一批最先开画的 10 个资产：
1. `floor-dark`
2. `floor-light`
3. `wall-main`
4. `window-night`
5. `desk-workstation`
6. `chair-office`
7. `sofa-2seat`
8. `rack-server`
9. `crate-archive`
10. `agent-worker`

实现约束：
- 所有像素图禁止后期模糊缩放
- 输出时保留原始整数像素，不做抗锯齿
- 家具和角色优先保证轮廓清晰，再补内部细节
- 首版宁可资产少，也不要为了“丰富”而拉杂

仓库骨架已创建：
- [public/office/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/README.md)
- [public/office/tiles/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/tiles/README.md)
- [public/office/sprites/furniture/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/sprites/furniture/README.md)
- [public/office/sprites/agents/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/sprites/agents/README.md)
- [public/office/sprites/status/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/sprites/status/README.md)
- [public/office/atlas/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/atlas/README.md)
- [public/office/maps/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/maps/README.md)
- [public/office/maps/office-winter-v1-sketch.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/maps/office-winter-v1-sketch.md)
- [public/office/maps/office-winter-v1-grid.txt](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/maps/office-winter-v1-grid.txt)
- [public/office/maps/office-winter-v1.tmj](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/maps/office-winter-v1.tmj)

占位 Tiled 地图已生成：
- 使用 `25x14` 网格和 `16x16` tile
- 直接引用未来的 `../tiles/office-winter-tileset.png`
- 图层已预拆为：
  - `bg-floor`
  - `bg-rug`
  - `walls`
  - `windows`
  - `markers`
  - `furniture-anchors`
  - `collision`
- 家具、服务器、归档箱、装饰点暂时先落为 `object layer anchors`
- 后续真实素材接入时，可优先替换 `tileset` 和 `furniture-anchors` 的渲染映射，不必重画地图结构

首版占位 tileset 已生成：
- [public/office/tiles/office-winter-tileset.png](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/tiles/office-winter-tileset.png)
- [public/office/tiles/office-winter-tileset-preview.png](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/tiles/office-winter-tileset-preview.png)
- 当前是“可用占位图块”，目标是：
  - 让 `Tiled` 和 `Phaser` 不再处于缺图状态
  - 让布局讨论从纯色块进入“基础像素质感”阶段
  - 后续真实美术可以逐格替换，不影响现有地图编号

当前地图预览已切换为真实 tileset 拼接：
- [public/office/maps/office-winter-v1-preview.png](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/maps/office-winter-v1-preview.png)

已引入 `pixel-agents` 资产作为当前内用原型素材库：
- [public/office/vendor/pixel-agents/README.md](/home/ivan/xuan/nano_project/app-instance/frontend/public/office/vendor/pixel-agents/README.md)
- 资产已整体落入：
  - `public/office/vendor/pixel-agents/assets/furniture/`
  - `public/office/vendor/pixel-agents/assets/floors/`
  - `public/office/vendor/pixel-agents/assets/walls/`
  - `public/office/vendor/pixel-agents/assets/characters/`
- 当前已经在 `OfficePhaserCanvas` 中接入的是真实家具 sprite，不再只显示锚点框
- 当前已映射的主要锚点类型：
  - `desk-anchor`
  - `chair-anchor`
  - `sofa-anchor`
  - `coffee-anchor`
  - `meeting-anchor`
  - `server-anchor`
  - `archive-anchor`
  - `decor-anchor`
- `pixel-agents` 角色素材也已接入当前运行态画布
- 当前做法是：
  - 每个成员按顺序分配一套角色皮肤
  - 先裁切站立帧作为第一版展示
  - 状态变化仍通过徽标、灯光、告警符号和轻微动画表达
- 下一阶段再考虑把角色切到真正的多帧动画：
  - `idle`
  - `walk`
  - `type`
  - `blocked`
  - `done`

第一版 tileset 逐格设计表：

文件约定：
- 文件名：`public/office/tiles/office-winter-tileset.png`
- 单格尺寸：`16x16`
- 第一版先做 `4 x 4` 共 `16` 格
- 编号顺序按从左到右、从上到下

首版 16 格 tile 清单：
1. `tile-00-wall-main`
   - 主墙面
   - 用于绝大多数内墙和外墙主体
2. `tile-01-wall-shadow`
   - 主墙面的阴影变体
   - 用于墙角、隔断背光面
3. `tile-02-wall-trim-top`
   - 墙顶部边线 / 踢脚线风格装饰
   - 用于形成冬季室内木质边框感
4. `tile-03-window-night`
   - 窗框 + 夜色
   - 允许少量雪蓝高光

5. `tile-04-floor-dark`
   - 深色地板主格
   - 为办公室主走道和大部分可行走区域提供底色
6. `tile-05-floor-light`
   - 地板亮格
   - 与深格交替，形成轻微棋盘层次
7. `tile-06-floor-shadow`
   - 地板压暗格
   - 用于家具下方、墙边和角落
8. `tile-07-rug-center`
   - 地毯中心格
   - 适合中部休息区

9. `tile-08-rug-edge-top`
   - 地毯上边缘
10. `tile-09-rug-edge-bottom`
   - 地毯下边缘
11. `tile-10-rug-edge-left`
   - 地毯左边缘
12. `tile-11-rug-edge-right`
   - 地毯右边缘

13. `tile-12-corner-inner`
   - 内转角
   - 用于墙体或地毯转角收边
14. `tile-13-corner-outer`
   - 外转角
   - 用于房间边界和隔断端点
15. `tile-14-border-accent`
   - 装饰边框格
   - 用于壁炉橙 / 冷蓝点缀的少量节奏
16. `tile-15-placeholder-debug`
   - 调试占位格
   - 首版用于 Tiled 中快速查错，不进入最终视觉

每格绘制要求：
- 所有 tile 都必须在 `16x16` 内完成，不允许跨格画主体
- 第一版每格内部色彩控制在 `3-5` 个主色内
- 轮廓优先用较深色，不用纯黑硬描整圈
- 高光只落在上边或左上角，统一光源方向
- 地板和墙面的纹理必须克制，不能把 tile 画成噪点块

首版 tileset 排版建议：
- 第 1 行：墙体相关
- 第 2 行：地板相关
- 第 3 行：地毯相关
- 第 4 行：角落 / 边框 / 调试格

首版绘制优先级：
1. `tile-04-floor-dark`
2. `tile-05-floor-light`
3. `tile-00-wall-main`
4. `tile-03-window-night`
5. `tile-07-rug-center`
6. `tile-08/09/10/11-rug-edge-*`

首版完成标准：
- 能在 `Tiled` 里先拼出完整房间外轮廓
- 能区分墙、地板、窗户、地毯 4 个层次
- 即使还没有家具和角色，也能看出“冬季办公室”的空间氛围

最新运行时视觉约束：
- 场景里不显示分区标题
- 场景里不显示彩色分区框
- 分区只用于成员落点和状态映射，不在画布上显式标注

最新机器人表现约束：
- 机器人整体尺寸上调一档
- 主 agent 比普通成员再大一档
- running 态不使用走路朝向，统一正面朝下
- 运行感主要靠轻微上下浮动和状态灯，不靠横向行走
- 运行时角色必须使用 spritesheet frame 渲染，不允许再用整张角色图错误裁切
- `public/office/maps/office-winter-v1-scene-preview.png` 必须跟随当前地图、家具和角色资源重导，不能长期作为过期效果图保留

最新画布简化约束：
- 场景顶部左侧标题条移除
- 场景顶部右侧阶段条移除
- 场景底部 focus/点击提示牌匾移除

最新运行态规则修正：
- 已完成成员不单独停留在完成区，而是返回讨论区（collab）
