chore: rebuild release package

This commit is contained in:
shiyue
2026-07-18 10:14:38 +08:00
parent fa15b54469
commit a76de06c47
112 changed files with 8858 additions and 399 deletions

View File

@@ -20,7 +20,8 @@
8. 空的 `wait/close` 协作工具不得把已有子代理数量重置为 0普通工具调用不得进入子代理状态合并路径。
9. 当历史中找不到原 `spawnToolId` 时,服务端应将 child 状态合并到最近的协作工具记录,保证刷新后仍可恢复关闭卡片;不得选择普通工具。
10. 原生 `subAgentActivity` 必须归一化进同一子代理卡片:`agentThreadId` 作为线程 ID`agentPath` 作为角色/标题来源,活动 `kind` 映射为状态;随后空 `wait` 复用这些状态,原始活动工具条不重复显示。
11.`subAgentActivity` 没有携带原始 prompt保守显示可读角色标题,不伪造任务简介;若同线程的其他事件提供 prompt则继续按既有优先级保留简介。
11.`subAgentActivity` 没有携带原始 prompt应显示可读角色标题和明确标注的兜底任务说明;若同线程的其他事件提供 prompt则继续按既有优先级保留真实简介。
12. 产品要求所有子代理统一为“标题 + 简介”富卡片。无 prompt 时,从 `agentPath` basename/task name 生成可读标题,并显示兜底任务说明;不得继续展示裸 snake_case 名称或空白简介区域。
## 视觉要求
@@ -39,6 +40,7 @@
- 关闭空载荷场景保留原标题、简介、线程 ID并显示关闭状态与非零数量。
- 页面刷新和历史重渲染后仍满足上述关闭卡片要求。
- 仅包含 `subAgentActivity + 空 wait` 的历史消息显示非零子代理富卡片,标题来自 `agentPath`,且不再同时显示 `subAgentActivity` 原始工具条或 `ID call_...`
- 无 prompt 的历史子代理也显示可读标题与一行兜底简介,布局层级与有 prompt 的富卡片一致。
- JavaScript 语法检查及项目回归测试通过。
## 范围

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/frontend/index.md","reason":"前端规范入口与交付检查"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"静态检查、回归和代码审查约束"}
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/theme-architecture.md","reason":"核对主题链路、最小改动面和验证命令"}
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/asset-slicing.md","reason":"核对资产尺寸、裁剪框、透明边缘和背景构图"}

View File

@@ -0,0 +1,5 @@
{"file":".trellis/spec/frontend/index.md","reason":"前端规范入口与质量检查清单"}
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"保持现有原生 DOM/CSS 组件模式与可访问性"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"前端实现和测试质量约束"}
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/theme-architecture.md","reason":"主题注册、持久化、CSS 隔离和回归链路"}
{"file":".trellis/tasks/07-17-gilded-wasteland-theme/research/asset-slicing.md","reason":"背景定位、图标裁剪坐标、透明化与 manifest 方案"}

View File

@@ -0,0 +1,70 @@
# 暗金荒野主题技术设计
## 实施结论
沿用现有纯前端 `THEME_OPTIONS + html[data-theme] + CSS 变量` 架构,不新增依赖、不改服务端。原 Warframe 主题 `gilded` 恢复为可见选项;新主题使用独立 ID `wasteland`,以用户背景图作为全画布主视觉,并用半透明黑铁面板和旧金描边覆盖现有工作台。
## 主题契约
- 默认主题仍为 `washi`
- `gilded`:保留现有 Warframe Wasteland 注册、四图 welcome 舞台和样式;删除 `hidden: true`
- `wasteland`新增可见注册label 为“暗金荒野”swatches 为 `#050707/#171511/#c49a5a/#6f783f`
- 删除 `public/index.html``gilded -> washi` 的首屏迁移;不新增任何主题迁移。
- `normalizeTheme()` 与 picker 保留现有统一逻辑,两个主题都应正常持久化和选择。
- cache bust 更新为 `20260717-wasteland-theme`
## 资产方案
-`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp` 原样复制到 `public/assets/themes/wasteland/background.webp`
-`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp` 的 32px 档裁切 17 个常用图标,生成 `public/assets/themes/wasteland/icons/*.png`
- 图标输出统一使用 64×64 透明 PNG 画布;源主体先按研究文档给出的 bbox 裁剪,再用亮度/色相 mask 去掉暗色背景,保留旧金高光和 status 的绿色中心,边缘做 1px 以内羽化。
- 创建 `public/assets/themes/wasteland/icons/manifest.json`,每项记录 `source``card``variant``bbox``output`
- 至少在新主题的 new chat、search、attachment、send、stop、settings 等可见控件中实际使用切分图标;其余图标作为主题资产集供弹层和后续控件复用。
## CSS 设计
### 变量层
新增 `html[data-theme='wasteland']`
- 页面:`#070807`、黑铁半透明面板 `rgba(13,15,14,.82)`
- 文字:主文字 `#efe8d8`、次文字 `#cfc3aa`、弱文字 `#9e927d`
- 强调:旧金 `#c49a5a`,状态绿 `#6f783f`,危险余烬 `#a65b3d`
- 角切:复用单一 `--wasteland-cut`,不要给所有容器叠加厚边框。
### 主视觉与层级
- `body` 使用多层遮罩 + `background.webp``background-size: cover`、桌面 `background-position: center`;人物保持在右半区。
- `.app``.chat-main``.messages-wrap` 允许背景透出;侧栏、顶栏、消息、输入区使用不同透明度的不透明化玻璃面板,正文区域对比度优先。
- 左侧遮罩更深,人物核心区域只叠轻微暖金径向光;移动端提高整体遮罩并使用 `background-position: 70% 50%`
- 现有 `.welcome-visual` 仍只属于 `gilded``wasteland` 不复用 Warframe 四图舞台。
### 组件与交互
- 侧栏/顶栏/输入区使用细旧金分隔线,不做大面积亮金渐变。
- 用户消息使用暗棕黑,助手消息使用黑铁;工具调用块用左侧旧金标记强化层级。
- 主按钮仅在 hover/focus 时出现短促微光;列表 hover 位移不超过 2px。
- 主题切入使用 320420ms 淡入,按钮/边框反馈 140180ms。
- `prefers-reduced-motion: reduce` 下禁用 `wasteland` 的动画与过渡。
- 不改现有布局结构、不新增营销 hero、卡片只用于实际交互对象。
## 业务文件最小改动
- `public/app.js`:只在 `THEME_OPTIONS` 附近移除 `gilded.hidden` 并新增 `wasteland`;不要触碰后半文件的子代理卡片改动。
- `public/index.html`:删除 3 行 `gilded` 迁移,更新 cache bust必要时只给现有静态控件增加主题图标所需的无障碍钩子。
- `public/style.css`:在变量区新增 `wasteland` 变量并加入共享暗色 selector在文件尾追加独立 `wasteland` 组件层,不重写 `gilded` 区块。
- `scripts/regression.js`:保留现有 Warframe 资产检查,改写“隐藏/迁移”断言;新增 Wasteland 资产清单、ID/选择器/背景/图标/对比度/reduced-motion/cache bust 契约和 `wasteland-theme` target。
- `server.js`:不修改,现有 WebP/PNG MIME 已满足。
## 验证
1. `git diff --check`
2. `npm run regression -- --target gilded-theme`
3. `npm run regression -- --target wasteland-theme`
4. `npm run regression`
5. 浏览器桌面视口 1672×941、1440×900以及窄屏 390×844验证两个主题均可选择、刷新持久化、背景/图标加载、文本可读、无横向溢出。
## 研究引用
- `research/theme-architecture.md`主题注册、持久化、CSS 分层、回归入口和脏文件边界。
- `research/asset-slicing.md`背景构图、图标网格、17 个 bbox、透明化算法和输出格式。

View File

@@ -0,0 +1,57 @@
# 新增暗金荒野主题
## Goal
为 cc-web 新增一个独立的“暗金荒野”视觉主题,忠实吸收用户参考图中的黑铁、旧金、余烬、切角边框和暗黑骑士主视觉,同时保留现有 Warframe 主题供即时切换对比。
## What I already know
- 用户明确要求新建主题,不直接覆盖当前 Warframe。
- 用户提供一张完整界面参考、一张 1672×941 背景图和一张 1672×941 图标板,并授权自行切图。
- 主题必须覆盖现有 cc-web 工作台,而不是单独制作静态展示页。
- 工作区有其他未提交改动,实施必须精确合并。
## Assumptions
- 本轮保持现有默认主题不变,仅在主题选择器中新增选项。
- 原 Warframe 主题保留内部标识 `gilded`;新主题显示名“暗金荒野”,内部标识固定为 `wasteland`
- 主题保持现有信息结构与业务交互,不新增功能入口。
## Requirements
- 新主题可在现有设置/主题选择入口中选择并持久化。
- 现有 `gilded`Warframe Wasteland主题移除隐藏限制和启动迁移继续可选用户可在它与 `wasteland`(暗金荒野)之间来回切换对比。
- 使用用户提供的暗黑骑士图作为全局主背景,并保证聊天文本、导航和弹层可读。
- 从图标板切出主题所需的代表性图标,作为本地资产接入。
- 侧栏、顶栏、消息、工具调用块、输入区、按钮、表单、弹层和滚动条使用统一暗金设计语言。
- 桌面与窄屏布局均不得退化;动效遵守 `prefers-reduced-motion`
## Acceptance Criteria
- [ ] 主题选择器中可看到“暗金荒野”和原 Warframe 两个独立选项。
- [ ] 切换至暗金荒野后刷新页面,主题仍保持。
- [ ] 用户提供的背景和切分图标均由项目本地静态资源加载。
- [ ] 主题在常见桌面与移动视口无明显遮挡、溢出或不可读文本。
- [ ] 主题切换、关键悬停和聚焦有一致且克制的视觉反馈。
- [ ] 现有自动化回归通过,并补充新主题注册/资源相关断言。
## Definition of Done
- 静态检查和相关回归测试通过。
- 至少完成桌面和窄屏两种视口的浏览器视觉验收。
- 不覆盖用户现有未提交改动,不引入额外前端依赖。
- 临时 TODO CSV 清理Trellis 与持久化计划记录收口。
## Out of Scope
- 重构现有应用信息架构或业务功能。
- 删除、重命名或替换原 Warframe 主题。
- 生成新的 AI 背景图或建立通用图标编辑器。
## Technical Notes
- 优先使用 codebase-memory-mcp 定位主题注册、持久化和测试调用链,再用 `rg`/`sed` 核验行号与未索引静态内容。
- 前端视觉遵循 `frontend-skill`:以强主视觉为锚点,避免卡片堆叠与无意义装饰,最多一个主强调色。
- 主题 ID 契约:`gilded` 只对应旧 Warframe`wasteland` 只对应新暗金荒野,默认 `washi` 不变。
- 源资产路径:`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp`(背景)、`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp`(图标板)、`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`(完整参考)。
- 目标资产路径:`public/assets/themes/wasteland/background.webp``public/assets/themes/wasteland/icons/*.png`

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

View File

@@ -0,0 +1,280 @@
# 暗金荒野主题资产切片研究
生成时间2026-07-17T16:41:28+08:00
## 输入资产
- 背景图:`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp`
- 图标板:`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp`
- 完整界面参考:`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`
三张图尺寸均为 `1672x941`,格式为 RGB WebP无 alpha 通道。
坐标约定:本文所有裁剪框均使用 `[x, y, w, h]`,其中 `x+w` / `y+h` 是右边界 / 下边界的排他坐标。由于源图是有损 WebP边缘估算建议按 `±3px` 处理;真正导出时可再外扩 `2-4px` 做抗锯齿和阴影保留。
## 背景图定位与遮罩建议
### 视觉结构
- 左侧 `x=0-740` 是较干净的暗色烟雾区,适合承载侧边栏、搜索框、会话列表和中左侧主内容。
- 右侧 `x=920-1560, y=80-760` 是人物、法阵和权杖主体,视觉权重高,不适合直接压大段正文。
- 下方 `y=780-941` 有烟尘、碎石和暗部,可放输入框,但输入框自身需要较实的半透明面板。
- 背景平均亮度很低,左侧安全区采样亮度约 `22/255`,右侧人物区约 `38/255`;遮罩目标不是继续压暗整图,而是稳定文本和边框对比。
### 推荐 CSS
桌面端推荐保留人物在右中部,同时让左侧和主聊天区域更稳:
```css
.theme-gilded-wasteland {
position: relative;
min-height: 100vh;
color: var(--gilded-text);
background-color: #050707;
overflow: hidden;
}
.theme-gilded-wasteland::before {
content: "";
position: fixed;
inset: 0;
z-index: -2;
background-image: url("/assets/theme/gilded-wasteland/background.webp");
background-size: cover;
background-position: 62% 50%;
background-repeat: no-repeat;
}
.theme-gilded-wasteland::after {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
linear-gradient(
90deg,
rgba(4, 6, 6, 0.94) 0%,
rgba(4, 6, 6, 0.86) 18%,
rgba(4, 6, 6, 0.58) 48%,
rgba(4, 6, 6, 0.30) 70%,
rgba(4, 6, 6, 0.62) 100%
),
linear-gradient(
180deg,
rgba(0, 0, 0, 0.40) 0%,
rgba(0, 0, 0, 0.08) 24%,
rgba(0, 0, 0, 0.16) 70%,
rgba(0, 0, 0, 0.70) 100%
),
radial-gradient(
ellipse at 74% 34%,
rgba(216, 168, 92, 0.12) 0%,
rgba(216, 168, 92, 0.04) 22%,
transparent 42%
);
}
@media (max-width: 720px) {
.theme-gilded-wasteland::before {
background-position: 70% 50%;
}
.theme-gilded-wasteland::after {
background:
linear-gradient(90deg, rgba(4, 6, 6, 0.92), rgba(4, 6, 6, 0.76)),
linear-gradient(180deg, rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.72));
}
}
```
### 定位取值
- 常规桌面:`background-position: 62% 50%`,人物仍在右侧,左中区域更可读。
- 参考图同尺寸复刻:`background-position: center center` 也可用,因为参考图本身没有裁切。
- 侧边栏更宽或主面板偏左时:可调到 `64% 50%`,避免人物头部过早进入主消息卡片。
- 移动端:建议 `70% 50%` 并提高遮罩不透明度,优先保证内容可读。
## 图标板网格边界
图标板不是透明 sprite而是「暗色背景 + 金色边框 + 标题 + 三规格图标 + 尺寸标签」的展示板。建议把它作为切片源,不要直接拿整板做 CSS sprite。
### 列边界
| 列 | x0 | x1 | 宽度 |
| --- | ---: | ---: | ---: |
| C1 | 50 | 295 | 245 |
| C2 | 315 | 562 | 247 |
| C3 | 582 | 829 | 247 |
| C4 | 846 | 1092 | 246 |
| C5 | 1111 | 1358 | 247 |
| C6 | 1377 | 1621 | 244 |
### 行边界
| 行 | y0 | y1 | 高度 |
| --- | ---: | ---: | ---: |
| R1 | 50 | 187 | 137 |
| R2 | 201 | 339 | 138 |
| R3 | 354 | 490 | 136 |
| R4 | 506 | 641 | 135 |
| R5 | 655 | 786 | 131 |
| R6 | 798 | 927 | 129 |
### 卡片矩阵
| 卡片 | 名称 | 整卡裁剪框 `[x,y,w,h]` |
| --- | --- | --- |
| R1C1 | new chat | `[50,50,245,137]` |
| R1C2 | dropdown | `[315,50,247,137]` |
| R1C3 | search | `[582,50,247,137]` |
| R1C4 | shortcut | `[846,50,246,137]` |
| R1C5 | collapse / expand | `[1111,50,247,137]` |
| R1C6 | add (circle) | `[1377,50,244,137]` |
| R2C1 | theme / star | `[50,201,245,138]` |
| R2C2 | ai swirl | `[315,201,247,138]` |
| R2C3 | status (online) | `[582,201,247,138]` |
| R2C4 | loading | `[846,201,246,138]` |
| R2C5 | attachment | `[1111,201,247,138]` |
| R2C6 | send | `[1377,201,244,138]` |
| R3C1 | stop | `[50,354,245,136]` |
| R3C2 | settings | `[315,354,247,136]` |
| R3C3 | user / profile | `[582,354,247,136]` |
| R3C4 | terminal / prompt | `[846,354,246,136]` |
| R3C5 | copy | `[1111,354,247,136]` |
| R3C6 | delete | `[1377,354,244,136]` |
| R4C1 | refresh | `[50,506,245,135]` |
| R4C2 | folder | `[315,506,247,135]` |
| R4C3 | chat bubble | `[582,506,247,135]` |
| R4C4 | notification | `[846,506,246,135]` |
| R4C5 | upload | `[1111,506,247,135]` |
| R4C6 | download | `[1377,506,244,135]` |
| R5C1 | info | `[50,655,245,131]` |
| R5C2 | warning | `[315,655,247,131]` |
| R5C3 | close | `[582,655,247,131]` |
| R5C4 | check | `[846,655,246,131]` |
| R5C5 | lock | `[1111,655,247,131]` |
| R5C6 | eye / visibility | `[1377,655,244,131]` |
| R6C1 | fullscreen | `[50,798,245,129]` |
## 常用图标推荐裁剪框
默认建议切 32px 规格,因为它能向下缩放到 24px / 16px且保留更多金属高光和阴影。下表是「图标主体裁剪框」不包含卡片标题、尺寸标签和外框。
| 图标语义 | 来源卡片 | 32px 主体框 `[x,y,w,h]` | 备注 |
| --- | --- | --- | --- |
| new chat | R1C1 | `[204,85,69,65]` | 十字星形,外扩较多以保留长尖角 |
| search | R1C3 | `[746,86,62,62]` | 放大镜手柄在右下,透明化时保留阴影 |
| dropdown | R1C2 | `[480,98,59,45]` | 三角图标高度较小,可居中放入 32px 容器 |
| theme / star | R2C1 | `[204,236,70,65]` | 星芒很长,不建议再紧缩 |
| ai swirl | R2C2 | `[476,236,65,65]` | 旋涡近似正方形 |
| status / online | R2C3 | `[749,242,53,53]` | 需要把绿色填充计入 mask |
| attachment | R2C5 | `[1277,236,49,60]` | 回形针纵向更高,容器内垂直居中 |
| send | R2C6 | `[1533,237,63,57]` | 纸飞机尖角在右上 |
| stop | R3C1 | `[211,392,56,56]` | 方形停止按钮 |
| settings | R3C2 | `[476,389,65,62]` | 齿轮外齿需要保留 |
| user / profile | R3C3 | `[743,389,65,62]` | 头像圆框 |
| terminal / prompt | R3C4 | `[1009,393,59,55]` | 矩形外框有阴影 |
| copy | R3C5 | `[1278,392,48,56]` | 双页图标偏窄 |
| refresh | R4C1 | `[211,541,57,57]` | 圆箭头 |
| chat bubble | R4C3 | `[745,541,62,55]` | 气泡尾巴在左下 |
| close | R5C3 | `[749,690,53,53]` | X 形,适合放入 32px 方形容器 |
| check | R5C4 | `[1010,693,56,47]` | 勾号高度较小,容器内垂直居中 |
### 16px / 24px / 32px 自动裁剪规则
整卡内三个规格的位置基本稳定,推荐通过卡片边界自动切,而不是手工维护所有坐标。
1. 先按上文卡片矩阵定位目标卡片。
2. 在卡片内部建立三个横向检测带:
```text
16px: [card.x + 18, card.x + 70]
24px: [card.x + 82, card.x + 145]
32px: [card.x + 152, card.x + card.w - 20]
```
3. 纵向检测带取:
```text
[card.y + 35, min(card.y + card.h - 35, card.y + 100)]
```
4. 对检测带内像素建立图标 mask
```text
gold = r > 75 && g > 48 && b < 90 && r >= g - 8 && r - b > 22
pale = r > 135 && g > 105 && b < 95 && r - b > 35
green = g > 70 && r < 95 && b < 80 && g - r > 8
mask = gold || pale || green
```
5. 对 mask 像素取最小外接矩形,再外扩 `4px` 得到主体裁剪框。
6. 如果要导出统一尺寸图标,把主体框透明化后放入固定画布:
| 源规格 | 推荐输出画布 | 用途 |
| --- | ---: | --- |
| 16px | `20x20``24x24` | 列表小图标、状态点 |
| 24px | `32x32` | 常规按钮 |
| 32px | `40x40``48x48` | 主操作按钮、头像旁标识 |
### 自动检测边界算法
卡片边界可以用金色边缘峰值检测自动恢复,适合后续批量生成 manifest。
```text
1. 读取 RGB WebP。
2. 计算相邻像素梯度:
edge = abs(r-r0) + abs(g-g0) + abs(b-b0)
3. 对 edge > 18 的像素分别累加到 x/y 投影。
4. 对投影做半径 1-2px 的平滑。
5. 在 x 轴选取强峰并按宽度 239-247px 配对,得到左右边界:
[50,295], [315,562], [582,829], [846,1092], [1111,1358], [1377,1621]
6. 在 y 轴选取强峰并按高度 129-138px 配对,得到上下边界:
[50,187], [201,339], [354,490], [506,641], [655,786], [798,927]
7. 用卡片位置矩阵给每个 cell 赋语义标签。
8. 在 cell 内按 16/24/32 检测带取主体 mask 外接框。
```
## 透明化与输出格式方案
### 图标透明化
源图标板是 RGB WebP背景和图标都已经混合压缩不能直接得到完美 alpha。推荐流程
1. 从图标板按主体框裁剪,裁剪时外扩 `2-4px`
2. 从裁剪框边缘做暗背景 flood-fill把与边缘连通且亮度低、饱和度低的区域标记为背景。
3. 对非背景像素按亮度和色相生成 alpha
- 金色 / 浅金高光:保留。
- 绿色状态图标:单独保留,避免被金色阈值漏掉。
- 暗色压缩噪点alpha 压低到 `0`
4. 对 alpha 做轻微 `1px` 羽化,减少 WebP 黑边。
5. 把图标居中放入统一画布,按钮内再用 CSS 控制 `width/height`
建议保留一份 manifest记录源图、源框和输出文件名后续可重复生成
```json
{
"search": {
"source": "43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp",
"card": "R1C3",
"variant": "32px",
"bbox": [746, 86, 62, 62],
"output": "icons/search-32.png"
}
}
```
### 输出格式
- 背景:保留 WebP 或转 AVIF/WebP 两套,作为 CSS 背景图,不需要透明通道。
- 图标主格式:透明 PNG。它对 alpha 最稳,适合切片后人工复核和 UI 直接引用。
- 图标 Web 优化:可额外导出 lossless WebP使用 `image-set()` 或构建工具按浏览器能力选择。
- 不建议:直接用原始图标板做 CSS sprite。它包含展示卡片、标题、尺寸标签和暗纹理背景透明化与 hover/filter 都会困难。
### 质量检查点
- 用深色、浅色、半透明按钮背景分别检查黑边。
-`status / online` 单独检查绿色中心是否完整。
-`new chat``theme / star``send` 检查尖角是否被裁掉。
-`dropdown``check` 这类非方形图标,导出后应放入统一画布居中,而不是强行拉伸。

View File

@@ -0,0 +1,294 @@
#!/usr/bin/env python3
"""从用户提供的设计稿中提取 Wasteland 边框与实际尺寸图标。"""
from pathlib import Path
import json
from PIL import Image, ImageFilter
ROOT = Path('/home/cc-web')
REFERENCE = ROOT / 'sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp'
ICON_SHEET = ROOT / 'sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp'
WELCOME_MATERIAL = ROOT / 'sessions/_attachments/2b88b81d-c5a8-4d92-a518-afeea691eade.webp'
ASSET_ROOT = ROOT / 'public/assets/themes/wasteland'
FRAME_DIR = ASSET_ROOT / 'frames'
ICON_DIR = ASSET_ROOT / 'icons/ui'
TEXTURE_DIR = ASSET_ROOT / 'textures'
def gold_alpha(pixel):
r, g, b = pixel
warm = max(0, min(r - b, g - b + 8))
light = max(0, (r + g) // 2 - 24)
if r < 38 or g < 27 or warm < 8:
return 0
return max(0, min(238, warm * 4 + light * 2))
def icon_alpha(pixel):
r, g, b = pixel
gold = r > 44 and g > 31 and r - b > 15 and g - b > 7
pale = r > 118 and g > 86 and r - b > 28
green = g > 58 and g - r > 5 and g - b > 10
if not (gold or pale or green):
return 0
if green:
score = (g - min(r, b)) * 4 + g
else:
score = (r - b) * 3 + (g - b) * 2 + (r + g) // 4
return max(0, min(255, score))
def crop_frame(source, name, box, zone):
crop = source.crop(box).convert('RGB')
width, height = crop.size
rgba = Image.new('RGBA', crop.size, (0, 0, 0, 0))
src = crop.load()
dst = rgba.load()
for y in range(height):
for x in range(width):
if not zone(x, y, width, height):
continue
alpha = gold_alpha(src[x, y])
if alpha:
r, g, b = src[x, y]
dst[x, y] = (r, g, b, alpha)
rgba.save(FRAME_DIR / f'{name}.png')
return {'name': name, 'source_box': list(box), 'size': [width, height]}
def extract_welcome_card(material):
"""从烘焙棋盘格的素材板中分离欢迎卡金属框和干净暗纹理。"""
frame_box = (70, 0, 1165, 875)
crop = material.crop(frame_box).convert('RGB')
frame = Image.new('RGBA', crop.size, (0, 0, 0, 0))
src = crop.load()
dst = frame.load()
for y in range(crop.height):
source_y = y + frame_box[1]
for x in range(crop.width):
source_x = x + frame_box[0]
# 730px 以下只保留主框的中央下坠徽记,排除素材板的其他组件。
if source_y >= 730 and not 500 <= source_x <= 780:
continue
alpha = gold_alpha(src[x, y])
if alpha < 18:
continue
r, g, b = src[x, y]
dst[x, y] = (r, g, b, alpha)
frame.save(FRAME_DIR / 'welcome-card.png')
surface_box = (350, 250, 950, 600)
surface = material.crop(surface_box).convert('RGB')
surface.save(TEXTURE_DIR / 'welcome-card-surface.webp', quality=92, method=6)
return [
{
'name': 'welcome-card',
'source': WELCOME_MATERIAL.name,
'source_box': list(frame_box),
'size': list(frame.size),
'output': 'frames/welcome-card.png',
},
{
'name': 'welcome-card-surface',
'source': WELCOME_MATERIAL.name,
'source_box': list(surface_box),
'size': list(surface.size),
'output': 'textures/welcome-card-surface.webp',
},
]
def slot_box(card, variant):
x, y, width, height = card
bands = {
'16': (x + 14, x + 74),
'24': (x + 76, x + 150),
'32': (x + 146, x + width - 12),
}
sx0, sx1 = bands[variant]
return sx0, y + 30, sx1, min(y + 105, y + height - 24)
def crop_icon(sheet, name, card, variant, canvas):
sx0, sy0, sx1, sy1 = slot_box(card, variant)
region = sheet.crop((sx0, sy0, sx1, sy1)).convert('RGB')
width, height = region.size
alpha = Image.new('L', region.size, 0)
ap = alpha.load()
rp = region.load()
points = []
for y in range(height):
for x in range(width):
value = icon_alpha(rp[x, y])
if value:
ap[x, y] = value
points.append((x, y))
if not points:
raise RuntimeError(f'没有检测到图标:{name}')
left = max(0, min(p[0] for p in points) - 3)
top = max(0, min(p[1] for p in points) - 3)
right = min(width, max(p[0] for p in points) + 4)
bottom = min(height, max(p[1] for p in points) + 4)
icon = region.crop((left, top, right, bottom)).convert('RGBA')
icon.putalpha(alpha.crop((left, top, right, bottom)).filter(ImageFilter.GaussianBlur(0.18)))
target_w, target_h = canvas
scale = min((target_w - 2) / icon.width, (target_h - 2) / icon.height)
resized = icon.resize(
(max(1, round(icon.width * scale)), max(1, round(icon.height * scale))),
Image.Resampling.LANCZOS,
)
output = Image.new('RGBA', canvas, (0, 0, 0, 0))
output.alpha_composite(resized, ((target_w - resized.width) // 2, (target_h - resized.height) // 2))
# 输入区图标按有效像素重心居中,避免纸飞机等非对称轮廓在按钮里视觉偏移。
if name in {'note', 'queue', 'send', 'stop'}:
alpha = output.getchannel('A')
pixels = alpha.load()
total = weighted_x = weighted_y = 0
for py in range(target_h):
for px in range(target_w):
value = pixels[px, py]
if not value:
continue
total += value
weighted_x += px * value
weighted_y += py * value
if total:
offset_x = round((target_w - 1) / 2 - weighted_x / total)
offset_y = round((target_h - 1) / 2 - weighted_y / total)
if offset_x or offset_y:
centered = Image.new('RGBA', canvas, (0, 0, 0, 0))
centered.alpha_composite(output, (offset_x, offset_y))
output = centered
output.save(ICON_DIR / f'{name}.png')
return {
'name': name,
'variant': variant,
'slot_box': [sx0, sy0, sx1 - sx0, sy1 - sy0],
'detected_box': [sx0 + left, sy0 + top, right - left, bottom - top],
'size': [target_w, target_h],
}
def main():
FRAME_DIR.mkdir(parents=True, exist_ok=True)
ICON_DIR.mkdir(parents=True, exist_ok=True)
TEXTURE_DIR.mkdir(parents=True, exist_ok=True)
reference = Image.open(REFERENCE).convert('RGB')
sheet = Image.open(ICON_SHEET).convert('RGB')
welcome_material = Image.open(WELCOME_MATERIAL).convert('RGB')
border_zone = lambda x, y, w, h: x < 8 or x >= w - 8 or y < 8 or y >= h - 8
frames = [
crop_frame(reference, 'new-chat', (15, 9, 304, 58), lambda x, y, w, h: border_zone(x, y, w, h) or 225 <= x <= 239),
crop_frame(reference, 'search', (15, 67, 304, 103), border_zone),
crop_frame(reference, 'project', (15, 117, 304, 164), border_zone),
crop_frame(reference, 'active-session', (15, 163, 304, 209), lambda x, y, w, h: x < 12 or x >= w - 7 or y < 7 or y >= h - 7),
crop_frame(
reference,
'composer',
(557, 830, 1364, 931),
lambda x, y, w, h: (
(y < 10)
or (x < 10 and y < 77)
or (x >= w - 10 and y < 77)
or (64 <= y < 79)
or (w // 2 - 56 <= x <= w // 2 + 56 and y >= 58)
),
),
crop_frame(reference, 'composer-action', (1211, 845, 1257, 891), border_zone),
crop_frame(reference, 'tool-call', (421, 603, 1381, 664), border_zone),
]
# 设计稿输入框含两层顶线。UI 叠加时只保留内侧主蚀刻线,
# 同时对称收紧左右 5px使主竖线贴合真实容器边界徽记保持几何中心不变。
composer_path = FRAME_DIR / 'composer.png'
composer = Image.open(composer_path).convert('RGBA')
composer = composer.crop((5, 5, composer.width - 5, composer.height))
composer = composer.resize((807, 101), Image.Resampling.LANCZOS)
# 设计稿原始裁区的中心徽记视觉中心比画布中心向右约 8px。
# 只校正下半部装饰层,保留左右框边和顶部线的原始几何位置。
centered_composer = composer.copy()
ornament_region = (96, 50, 711, 101)
centered_composer.paste((0, 0, 0, 0), ornament_region)
centered_composer.alpha_composite(composer.crop(ornament_region), (88, 50))
composer = centered_composer
composer.save(composer_path)
# 输入框不能把 807×101 的整图同时做横纵拉伸:窄窗口会让底部徽记
# 与真实 74px 框体错层。拆出只含设计稿顶部/侧边的框壳和独立徽记,
# 由 CSS 分别控制尺寸,保证任意宽度下都只出现一条边框。
shell_height = 74
shell = Image.new('RGBA', (composer.width, shell_height), (0, 0, 0, 0))
composer_pixels = composer.load()
shell_pixels = shell.load()
for y in range(shell_height):
for x in range(composer.width):
if y < 14 or x < 14 or x >= composer.width - 14:
shell_pixels[x, y] = composer_pixels[x, y]
shell.save(FRAME_DIR / 'composer-shell.png')
ornament_box = (130, 50, 677, 101)
composer.crop(ornament_box).save(FRAME_DIR / 'composer-ornament.png')
frames.extend([
{
'name': 'composer-shell',
'derived_from': 'composer.png',
'source_box': [0, 0, composer.width, shell_height],
'size': [composer.width, shell_height],
},
{
'name': 'composer-ornament',
'derived_from': 'composer.png',
'source_box': [
ornament_box[0],
ornament_box[1],
ornament_box[2] - ornament_box[0],
ornament_box[3] - ornament_box[1],
],
'size': [ornament_box[2] - ornament_box[0], ornament_box[3] - ornament_box[1]],
},
])
frames.extend(extract_welcome_card(welcome_material))
cards = {
'search': (582, 50, 247, 137),
'dropdown': (315, 50, 247, 137),
'theme': (50, 201, 245, 138),
'ai': (315, 201, 247, 138),
'status': (582, 201, 247, 138),
'attachment': (1111, 201, 247, 138),
'send': (1377, 201, 244, 138),
'queue': (846, 201, 246, 138),
'stop': (50, 354, 245, 136),
'settings': (315, 354, 247, 136),
'user': (582, 354, 247, 136),
'terminal': (846, 354, 246, 136),
'note': (582, 506, 247, 138),
}
specs = [
('search', '16', (20, 20)),
('dropdown', '16', (14, 10)),
('theme', '24', (26, 26)),
('ai', '32', (36, 36)),
('status', '16', (16, 16)),
('attachment', '24', (26, 28)),
('send', '32', (30, 30)),
('queue', '32', (30, 30)),
('stop', '24', (26, 26)),
('settings', '16', (20, 20)),
('user', '32', (36, 36)),
('terminal', '16', (20, 18)),
('note', '32', (36, 36)),
]
icons = [crop_icon(sheet, name, cards[name], variant, canvas) for name, variant, canvas in specs]
(FRAME_DIR / 'manifest.json').write_text(json.dumps(frames, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
(ICON_DIR / 'manifest.json').write_text(json.dumps(icons, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,346 @@
# Wasteland 图标重做审计
审计时间2026-07-17
输入资产:
- 图标板:`sessions/_attachments/43bd9ed6-2ba6-4c9d-bd9d-25b068cb4711.webp`
- 完整界面参考:`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`
- 现有图标:`public/assets/themes/wasteland/icons/*.png`
- 当前样式:`public/style.css`
结论:主 UI 控件不建议继续直接使用现有原色 PNG。推荐改为“清理后的 24px 单色 mask + CSS 暗金材质”,再按控件容器设置 16/18/20/24px 的显示尺寸。`status` 这类带绿色状态语义、头像/主题装饰类图标可以保留原色,但要重新裁切对应规格。
## 1. 参考界面实际显示尺寸
图标板原尺寸为 `1672x941`,每张卡片标注了 `16px / 24px / 32px` 三档逻辑使用尺寸。需要注意:标注是设计目标尺寸,不等同于源图中的物理像素;当前切片来自 32px 档展示图,源图里主体通常有 49-70px 宽高。
完整参考图同为 `1672x941`。按原尺寸查看和局部放大后,主要控件的实际显示尺寸约为:
| 区域 | 控件 | 参考图容器尺寸 | 图标视觉主体 | 对齐观察 |
| --- | --- | ---: | ---: | --- |
| 侧栏顶部 | 新会话主按钮 | `229x48` | `18-20px` | 图标+文字整体居中,图标不贴左边 |
| 侧栏顶部 | 新会话下拉按钮 | `51x48` | `10-12px` 宽、`7-9px` 高 | 小三角居中 |
| 侧栏顶部 | 搜索框 | `288x34` | `18-20px` | 图标左距约 `13px`,图标到文字约 `10px` |
| 项目列表 | 折叠/展开三角 | 行高约 `44-46px` | `10-12px` | 左侧垂直居中 |
| 项目列表 | 新建圆形加号 | 约 `28-30px` 圆按钮 | `14-16px` | 圆按钮居中,线条要比搜索更实 |
| 输入区上方 | 运行状态/模式 chip | 高约 `30-31px` | 状态点约 `8px`,下拉约 `8px` | 文字与图标同基线 |
| 输入区 | 输入框外框 | 约 `804x70` | 附件 `22-24px` | 附件没有明显方形底,靠左居中 |
| 输入区右侧 | 发送/队列/停止按钮 | 约 `42x42` | 发送 `24-28px`,停止 `17-20px` | 三个按钮等宽等高,间距约 `6px` |
| 消息卡右下 | 小动作按钮 | `29-30px` | `14-16px` | 适合单色小图标,不适合复杂材质 PNG |
| 侧栏底部 | 设置按钮 | 可点击区约 `34px` 高 | `18px` | 与文字标签水平对齐 |
| 侧栏底部 | 主题/星芒入口 | 可点击区约 `34px` 高 | `22-24px` | 星芒比齿轮略大才接近参考 |
当前 `public/style.css` 中 Wasteland 图标相关尺寸:
- `.new-chat-btn::before``24x24`
- `.session-search::before``20x20`
- `.attach-btn::before` / `.send-btn::before` / `.abort-btn::before` / `.settings-btn::before`:默认 `22x22`
- `.settings-btn``34x34`
- `:is(.settings-select, .mode-select)` 下拉背景:`14x14`
- 移动端 `.attach-btn/.send-btn/.abort-btn::before`:降到 `20x20`
主要偏差:参考图里“主按钮图标”和“输入区发送图标”不是同一套尺寸;当前 CSS 把大部分伪元素统一为 `22x22`,导致小动作按钮偏脏、大按钮又不够有冲击。
## 2. 现有 PNG 为什么显得糊、脏、比例怪
现有 PNG 全部是 `64x64 RGBA`,但它们不是干净矢量导出的图标,而是从有损 WebP 展示板里裁 32px 档,再透明化/统一画布得到的位图。抽检全部 17 个 PNG 后,问题集中在以下几点。
### 2.1 缩放链路过长
当前路径大致是:
```text
有损 WebP 展示板
→ 裁 32px 档的放大截图区域
→ 统一输出 64x64 PNG
→ CSS 再缩到 14/20/22/24px
```
浏览器最后会把 64px 的半透明阴影、暗边、高光全部平均到 20-24px。图标不会变“精致”反而会把阴影糊成灰边。
### 2.2 alpha 过软,缺少清晰实心主体
PNG 的非透明像素几乎都是半透明像素。统计结果显示:
- 多数图标的半透明像素占比在 `96%-100%`
- `stop.png` 也有约 `92%` 半透明像素
- 非透明像素平均 alpha 普遍只有 `67-142/255`
这说明边缘和主体都偏软。缩小时,暗边会吞掉金色高光,尤其在黑铁背景上更明显。
### 2.3 有效主体尺寸不一致
代表性 PNG 的 alpha 边界如下:
| 图标 | PNG 画布 | alpha 主体边界 | 视觉问题 |
| --- | ---: | ---: | --- |
| `new-chat.png` | `64x64` | `55x54` | 星芒长尖角多,缩小后中心笔画变弱 |
| `search.png` | `64x64` | `54x54` | 圆环+手柄复杂20px 时手柄容易糊 |
| `attachment.png` | `64x64` | `42x54` | 窄高图标,被统一方形缩放后显得瘦 |
| `send.png` | `64x64` | `54x49` | 纸飞机横向强22px 时尖角仍可辨但高光变灰 |
| `stop.png` | `64x64` | `54x54` | 填充和外框过重,小尺寸像脏方块 |
| `settings.png` | `64x64` | `54x53` | 齿轮齿太多22px 以下信息密度过高 |
| `dropdown.png` | `64x64` | `54x40` | 横宽纵矮,放进 `14x14` 背景后实际只有约 `14x10` |
同样放进 `22x22` 伪元素时,`attachment` 会显瘦,`stop/settings` 会显重,`dropdown` 会显矮,整体视觉重量不统一。
### 2.4 原色 PNG 不能响应 CSS 颜色状态
当前 hover/focus 主要改变 `color``background-color``border-color`。PNG 作为 `background-image` 不会跟随 `color` 改变,所以 hover 后按钮变亮,但图标仍是暗铜色,造成“按钮亮、图标脏”的割裂。
## 3. 逐控件建议尺寸与对齐
建议把“容器尺寸”和“图标主体尺寸”分开控制,不再一刀切 `22x22`
| 控件 | 当前落点 | 建议桌面尺寸 | 建议移动尺寸 | 对齐建议 |
| --- | --- | ---: | ---: | --- |
| 新会话 | `.new-chat-btn::before` | `20x20` | `18x18` | `.new-chat-btn` 改回图标+文字整体居中,`gap: 10px` |
| 新会话下拉 | `.new-chat-arrow::before` | `12x8``14x9` | `12x8` | 按钮 `48-52px` 宽,三角居中;隐藏原文本 `▾` |
| 搜索 | `.session-search::before` | `18x18``20x20` | `18x18` | `left: 13px`,输入框 `padding-left: 42px` |
| 附件 | `.attach-btn::before` | `24x24` | `22x22` | 40px 按钮内居中;不要用实心按钮底色压住图标 |
| 发送 | `.send-btn::before` | `24x24`,主发送可到 `26x26` | `22x22` | 桌面建议按钮 `40-42px`,纸飞机略向右上视觉居中 |
| 停止 | `.abort-btn::before` | `18x18``20x20` | `18x18` | 与发送按钮同容器,但图标主体必须小一档 |
| 设置 | `.settings-btn::before` | `18x18` | `18x18` | 34px 点击区内居中;避免 22px 齿轮过密 |
| 下拉 select | `:is(.settings-select, .mode-select)` | `10x7``12x8` | `10x7` | `background-position: right 12px center`;不要用 `14x14` 方形 PNG |
补充建议:
- 消息卡角落动作按钮:容器 `30x30`,图标只用 `14-16px` mask。
- 项目折叠三角:使用 `10-12px` mask不要复用 24px/32px 原色 PNG。
- 状态点/在线图标:如果要保留绿色,应单独重裁 `16px``20px` 原色图,不建议统一金色 mask。
## 4. 三种方案对比与推荐
| 方案 | 优点 | 缺点 | 适用范围 | 结论 |
| --- | --- | --- | --- | --- |
| 原色 PNG | 最接近图标板的金属纹理;不需要改 DOM可保留绿色状态、头像等多色信息 | 当前资产来自有损截图且 alpha 过软;缩到 20px 会糊hover/focus 不能自动变色;每个尺寸都要维护独立图片 | 装饰图标、状态图标、头像/主题入口、较大 32px 展示 | 不推荐作为主 UI 控件方案 |
| CSS mask | 形状和材质分离;可用 CSS 控制金色、hover、禁用、危险色小尺寸更干净同一 mask 可按控件调尺寸 | 会损失原图内部金属纹理;如果直接拿现有 PNG 当 mask软 alpha 仍会残留;严格做好需要清理 mask 资产 | new-chat/search/attach/send/stop/settings/dropdown 等交互控件 | 推荐主路径 |
| 重新裁 24px | 保留原色质感;比 64px 再缩小更稳;实现成本中等 | 仍是位图16px 以下会糊hover 不能换材质;不同控件仍要分别调视觉重量 | 必须保留原色材质的 24px 常规按钮 | 可作为过渡或备用 |
推荐路径:
1. 主交互控件使用 CSS mask但不要直接依赖当前软 alpha PNG先从图标板重做清晰的 24px 单色 mask或从现有 PNG 做阈值化/锐化后生成 `icons/masks/*.png`
2. `new-chat/search/attach/send/stop/settings/dropdown` 全部走 mask + CSS 暗金材质。
3. `status/user/theme/ai` 这类多色或装饰语义保留原色,但分别重裁 `16/24/32px`,不要再统一 64px 后缩放。
## 5. CSS mask 规则建议
下面是基于当前 DOM 的精确 selector 规则。路径可以先指向现有 PNG 做验证;正式落地时建议替换为清理后的 mask 资产,例如 `assets/themes/wasteland/icons/masks/send.png`
### 5.1 通用金色材质
```css
html[data-theme='wasteland'] {
--waste-icon-gold:
radial-gradient(circle at 32% 22%, rgba(255, 244, 196, 0.98) 0 10%, transparent 32%),
linear-gradient(135deg, #fff0bd 0%, #dfbd82 28%, #c49a5a 48%, #8f592b 72%, #f0d7aa 100%);
--waste-icon-gold-muted:
linear-gradient(135deg, #ead7ad 0%, #c49a5a 44%, #7b4c27 100%);
--waste-icon-danger:
linear-gradient(135deg, #f1b68d 0%, #b96849 48%, #6f2f24 100%);
--waste-icon-shadow:
drop-shadow(0 0 2px rgba(223, 189, 130, 0.42))
drop-shadow(0 1px 1px rgba(0, 0, 0, 0.82));
}
html[data-theme='wasteland'] :is(
.new-chat-btn,
.new-chat-arrow,
.attach-btn,
.send-btn,
.abort-btn,
.settings-btn
)::before,
html[data-theme='wasteland'] .session-search::before {
content: '';
display: block;
flex: 0 0 auto;
background: var(--waste-icon-gold);
filter: var(--waste-icon-shadow);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-size: contain;
mask-size: contain;
}
```
### 5.2 new-chat
```css
html[data-theme='wasteland'] .new-chat-btn {
justify-content: center;
gap: 10px;
padding-inline: 16px;
}
html[data-theme='wasteland'] .new-chat-btn .new-chat-glyph {
display: none;
}
html[data-theme='wasteland'] .new-chat-btn::before {
width: 20px;
height: 20px;
-webkit-mask-image: url('assets/themes/wasteland/icons/new-chat.png');
mask-image: url('assets/themes/wasteland/icons/new-chat.png');
}
```
### 5.3 search
```css
html[data-theme='wasteland'] .session-search::before {
position: absolute;
left: 13px;
top: 50%;
z-index: 1;
width: 18px;
height: 18px;
transform: translateY(-50%);
pointer-events: none;
-webkit-mask-image: url('assets/themes/wasteland/icons/search.png');
mask-image: url('assets/themes/wasteland/icons/search.png');
}
html[data-theme='wasteland'] .session-search-input {
padding-left: 42px;
}
```
### 5.4 attach
```css
html[data-theme='wasteland'] .attach-btn::before {
width: 24px;
height: 24px;
-webkit-mask-image: url('assets/themes/wasteland/icons/attachment.png');
mask-image: url('assets/themes/wasteland/icons/attachment.png');
}
```
### 5.5 send
```css
html[data-theme='wasteland'] .send-btn::before {
width: 24px;
height: 24px;
-webkit-mask-image: url('assets/themes/wasteland/icons/send.png');
mask-image: url('assets/themes/wasteland/icons/send.png');
}
html[data-theme='wasteland'] .send-btn:hover::before {
background: var(--waste-icon-gold);
filter:
drop-shadow(0 0 4px rgba(223, 189, 130, 0.58))
drop-shadow(0 1px 1px rgba(0, 0, 0, 0.82));
}
```
### 5.6 stop
```css
html[data-theme='wasteland'] .abort-btn::before {
width: 18px;
height: 18px;
background: var(--waste-icon-danger);
-webkit-mask-image: url('assets/themes/wasteland/icons/stop.png');
mask-image: url('assets/themes/wasteland/icons/stop.png');
}
```
### 5.7 settings
```css
html[data-theme='wasteland'] .settings-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
font-size: 0;
}
html[data-theme='wasteland'] .settings-btn::before {
width: 18px;
height: 18px;
-webkit-mask-image: url('assets/themes/wasteland/icons/settings.png');
mask-image: url('assets/themes/wasteland/icons/settings.png');
}
```
### 5.8 dropdown
`new-chat-arrow` 是普通按钮,可以直接用 mask
```css
html[data-theme='wasteland'] .new-chat-arrow {
font-size: 0;
line-height: 0;
}
html[data-theme='wasteland'] .new-chat-arrow::before {
width: 12px;
height: 8px;
background: var(--waste-icon-gold-muted);
-webkit-mask-image: url('assets/themes/wasteland/icons/dropdown.png');
mask-image: url('assets/themes/wasteland/icons/dropdown.png');
}
```
`settings-select``mode-select` 是原生 `select`,当前 DOM 没有外层专用 wrapper不能安全地只给箭头使用 `::before` mask。当前结构下推荐继续用金色 SVG 背景;如果强制用 CSS mask需要新增 wrapper 或自定义 select 结构。
当前 DOM 兼容规则:
```css
html[data-theme='wasteland'] :is(.settings-select, .mode-select) {
appearance: none;
-webkit-appearance: none;
background-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.25 6 6.75 11 1.25Z' fill='%23dfbd82' stroke='%238f592b' stroke-width='0.8'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 12px 8px;
}
html[data-theme='wasteland'] .settings-select {
background-position: right 12px center;
}
html[data-theme='wasteland'] .mode-select {
background-position: right 10px center;
}
```
若后续允许改 DOM建议把 select 放进 `.select-shell`,由 `.select-shell::after` 承载 dropdown mask这样文本和箭头可以完全分层。
### 5.9 移动端尺寸
```css
@media (max-width: 560px) {
html[data-theme='wasteland'] .new-chat-btn::before,
html[data-theme='wasteland'] .session-search::before,
html[data-theme='wasteland'] .settings-btn::before {
width: 18px;
height: 18px;
}
html[data-theme='wasteland'] :is(.attach-btn, .send-btn)::before {
width: 22px;
height: 22px;
}
html[data-theme='wasteland'] .abort-btn::before {
width: 18px;
height: 18px;
}
}
```
## 6. 实施优先级
1. 先把 `new-chat/search/attach/send/stop/settings/new-chat-arrow` 改成 mask 方案,解决主路径视觉脏和 hover 不一致。
2. 同步把 select 下拉从 `dropdown.png` 改成金色 SVG 背景,尺寸降到 `12x8``10x7`
3. 再生成清理后的 mask 资产,替换当前 PNG mask 源。清理标准是:去掉暗色投影,只保留形状 alpha小尺寸下需要比原图更粗、更少内部细节。
4. 最后再决定是否为 `status/user/theme/ai` 重裁原色 `16/24/32px`,这些不应阻塞主交互控件修复。

View File

@@ -0,0 +1,312 @@
# 主题架构研究:新增并存 Warframe/Wasteland 主题
## 结论
cc-web 的主题系统是一个纯前端、CSS 变量驱动的架构:
- 主题注册和设置候选统一来自 `public/app.js``THEME_OPTIONS`
- 当前主题持久化在 `localStorage['cc-web-theme']`DOM 状态写到 `html[data-theme]`
- `public/index.html` 在 CSS 加载前执行 bootstrap负责首屏避免闪烁当前还会把历史 `gilded` 主动迁移回 `washi`
- 样式主体在 `public/style.css`:先用 `html[data-theme='<id>']` 定义语义变量,再用共享暗色 completion selector 与主题专属组件层覆盖具体 UI。
- Warframe 当前 ID 是 `gilded`,但已 `hidden: true`,因此不会出现在设置页,也不会被 `normalizeTheme()` 视为有效持久化主题。
如果目标是“新增一个可并存、可选择的 Warframe/Wasteland 主题”,最稳妥的路径是新增一个可见 ID例如 `wasteland`),保留 `gilded` 作为隐藏历史/实验 ID 或迁移别名,避免直接复用当前已被首屏 bootstrap 迁移掉的 `gilded`
## codebase-memory 检索记录
| 项 | 结果 |
| --- | --- |
| 项目 | `home-cc-web` |
| 索引状态 | `ready` |
| 主要检索 | `theme settings data theme localStorage``gilded warframe regression theme assets CSS` |
| 关键符号 | `normalizeTheme``applyTheme``buildThemePickerHtml``mountThemePicker``openThemeSubpage``assertFrontendGildedThemeContract` |
| 调用链核验 | `applyTheme <- mountThemePicker <- openThemeSubpage``assertFrontendGildedThemeContract <- main` |
## 主题注册列表
主题注册点在 `public/app.js:109``THEME_OPTIONS`。每个主题对象包含:
| 字段 | 用途 |
| --- | --- |
| `value` | 写入 `html[data-theme]``localStorage` 的稳定主题 ID |
| `label` | 设置界面展示名 |
| `desc` | 设置界面说明文案 |
| `swatches` | 设置主题卡片色块 |
| `hidden` | 设置候选过滤与持久化合法性过滤 |
当前注册列表:
| ID | Label | 可见性 | 位置 |
| --- | --- | --- | --- |
| `washi` | `Washi Warm` | 可见 | `public/app.js:111` |
| `coolvibe` | `CoolVibe Light` | 可见 | `public/app.js:117` |
| `editorial` | `Editorial Sand` | 可见 | `public/app.js:123` |
| `sage` | `Sage Console` | 可见 | `public/app.js:129` |
| `ink` | `Ink Focus` | 可见 | `public/app.js:135` |
| `dawn` | `Dawn Studio` | 可见 | `public/app.js:141` |
| `carbon` | `Carbon Mint` | 可见暗色 | `public/app.js:147` |
| `nocturne` | `Nocturne Teal` | 可见暗色 | `public/app.js:153` |
| `cinder` | `Cinder Rose` | 可见暗色 | `public/app.js:159` |
| `gilded` | `Warframe Wasteland` | 隐藏 | `public/app.js:165` |
`gilded` 当前注册为:
- `label: 'Warframe Wasteland'`
- `desc: '四幅 Warframe 画作融合成 Orokin 档案界面...'`
- `swatches: ['#070a0b', '#d8c38f', '#78edf0', '#d86b4c']`
- `hidden: true`
关键含义:只加 CSS 不会让主题进入设置页;必须保证 `THEME_OPTIONS` 中有对应 ID且没有 `hidden: true`
## 主题持久化与 DOM 绑定
| 链路 | 实现 | 位置 |
| --- | --- | --- |
| 首屏读取 | `localStorage.getItem('cc-web-theme') || 'washi'` | `public/index.html:17` |
| 首屏 DOM 绑定 | `document.documentElement.dataset.theme = theme` | `public/index.html:23` |
| 运行时初始状态 | `document.documentElement.dataset.theme || localStorage.getItem(...) || 'washi'` | `public/app.js:200` |
| 合法性过滤 | `THEME_OPTIONS.some((item) => item.value === theme && !item.hidden) ? theme : 'washi'` | `public/app.js:1013` |
| 运行时应用 | 写 `document.documentElement.dataset.theme` | `public/app.js:1030` |
| 运行时持久化 | 写 `localStorage.setItem('cc-web-theme', currentTheme)` | `public/app.js:1031` |
| 摘要刷新 | `[data-theme-summary]` 同步当前 label | `public/app.js:1021` |
当前有一个特别重要的迁移逻辑:
```js
if (theme === 'gilded') {
theme = 'washi';
localStorage.setItem('cc-web-theme', theme);
}
```
位置:`public/index.html:18`
这意味着用户浏览器里即使已有 `gilded`,首屏也会被改回 `washi`。新增并存主题时有两种策略:
| 策略 | 做法 | 适用场景 |
| --- | --- | --- |
| 新 ID 并存 | 新增 `wasteland``gilded` 继续隐藏或迁移到 `wasteland` | 推荐,避免污染已有隐藏/历史语义 |
| 复活 `gilded` | 删除 `hidden: true`,移除 `index.html``gilded -> washi` 迁移 | 只适合确认不再需要隐藏 Warframe 时 |
## 设置界面主题候选
设置页不维护第二份主题列表,候选直接来自 `THEME_OPTIONS`
| 入口 | 实现 | 位置 |
| --- | --- | --- |
| 设置主面板外观入口 | `buildAppearanceSettingsHtml()` 输出 `data-open-theme-page` 和当前主题摘要 | `public/app.js:1084` |
| 入口事件绑定 | `mountAppearanceSettings()` 监听 `data-open-theme-page`,打开主题子页 | `public/app.js:1109` |
| 主题卡片 HTML | `buildThemePickerHtml()` | `public/app.js:1055` |
| 候选过滤 | `THEME_OPTIONS.filter((theme) => !theme.hidden).map(...)` | `public/app.js:1060` |
| 点击切换 | `mountThemePicker()` 调用 `applyTheme(button.dataset.themeValue)` | `public/app.js:1073` |
| 子页挂载 | `openThemeSubpage()` 生成 overlay挂载 picker | `public/app.js:1248` |
结论:新增可见主题只需在 `THEME_OPTIONS` 中加入非 hidden 条目,设置界面会自动出现;如果只想保留隐藏实验主题,设置候选不会展示。
## Warframe 主题样式与资产
### 静态资产
当前 Warframe 资产位于 `public/assets/themes/`
| 文件 | 尺寸/格式约束 | 来源于回归断言 |
| --- | --- | --- |
| `gilded-wasteland.png` | PNG1000x1500SHA-256 固定 | `scripts/regression.js:18` |
| `warframe-ember-squad.jpg` | JPEG480x673SHA-256 固定 | `scripts/regression.js:25` |
| `warframe-orokin-figure.jpg` | JPEG480x672SHA-256 固定 | `scripts/regression.js:32` |
| `warframe-sacrifice.jpg` | JPEG480x672SHA-256 固定 | `scripts/regression.js:39` |
静态服务器 MIME 映射支持 JPEG
- `.jpg -> image/jpeg``server.js:1289`
- `.jpeg -> image/jpeg``server.js:1290`
- 读取静态文件时用 `MIME_TYPES[ext]``Content-Type``server.js:6281`
如果新主题继续使用 WebP/PNG/JPEG通常不需要改 `server.js`;若引入 AVIF、字体、视频等新类型需要补 MIME 与回归断言。
### Welcome DOM
Warframe welcome 图层同时存在于动态和静态首屏:
| 文件 | 位置 | 内容 |
| --- | --- | --- |
| `public/app.js` | `public/app.js:320` | `buildWelcomeMarkup(agent)` 动态生成 `.welcome-copy` + `.welcome-visual` |
| `public/index.html` | `public/index.html:91` | 静态首屏 welcome DOM |
图层类名:
- `.warframe-visual-core`
- `.warframe-shard--wasteland`
- `.warframe-shard--sacrifice`
- `.warframe-shard--orokin`
- `.warframe-shard--ember`
- `.warframe-visual-axis`
- `.warframe-visual-code`
关键隔离点:通用主题默认隐藏 `.welcome-visual`
- `.welcome-art, .welcome-visual { display: none; }``public/style.css:2029`
- `html:not([data-theme='gilded']) .welcome-visual { display: none !important; }``public/style.css:6558`
- 只有 `html[data-theme='gilded'] .welcome-msg > .welcome-copy + .welcome-visual[aria-hidden]` 才显示视觉舞台:`public/style.css:6689`
如果新增 `wasteland` 并存主题,需要把这些选择器复制/泛化到 `html[data-theme='wasteland']`,或者用 `:is(html[data-theme='gilded'], html[data-theme='wasteland'])` 包住 Warframe 专属层。
### CSS 分层
当前 `style.css` 的可复用模式是四层:
| 层 | 作用 | 位置 |
| --- | --- | --- |
| 语义变量层 | `html[data-theme='gilded']` 定义颜色、面板、Warframe token | `public/style.css:683` |
| 共享暗色 completion 层 | 用 `:is(carbon, nocturne, cinder, gilded)` 给暗色主题补齐通用组件 | `public/style.css:745``public/style.css:6140` |
| 基础 UI 组件层 | `.theme-grid``.theme-card` 等设置 UI | `public/style.css:4323` |
| Warframe 专属组件层 | welcome 舞台、四图布局、Orokin 面板、暗金交互 | `public/style.css:6557` |
Warframe 专属层中的关键设计:
- 常规工作区不把图片铺到消息背后,`.chat-main` 只用渐变和 `--page-background``public/style.css:6598`
- welcome 页面用 `.messages:has(> .welcome-msg)` 限定视觉舞台:`public/style.css:6605`
- 四个真实 `<img>` 图层由 CSS 定位裁切,不用 CSS `url(...)` 壁纸:`public/style.css:6739`
- 输入、搜索、面板使用不透明 obsidian surfaces降低图片干扰`public/style.css:6965`
- 选择器 chevron、focus ring 复用 `--warframe-energy``public/style.css:6975``public/style.css:7175`
- 用户/助手消息和附件卡有专门的不透明覆盖:`public/style.css:7188``public/style.css:7222`
- 移动布局在 `1060px``560px` 两档重新堆叠四图:`public/style.css:7316``public/style.css:7373`
- `prefers-reduced-motion` 禁用 gilded 动画和 transition`public/style.css:7444`
## 相关回归测试
主题相关回归集中在 `scripts/regression.js`
| 检查 | 位置 |
| --- | --- |
| 资产清单与 hash/尺寸 | `scripts/regression.js:16` |
| 主题契约函数 | `scripts/regression.js:685` |
| target 入口 `gilded-theme` | `scripts/regression.js:2204` |
| full regression 会调用该主题契约 | `scripts/regression.js:2219` |
`assertFrontendGildedThemeContract()` 覆盖的关键面:
- `gilded` 已注册且 `hidden: true`
- `normalizeTheme()` 会排除 hidden 主题
- 主题 picker 排除 hidden 主题
- `index.html` 首屏会把 `gilded` 迁移回 `washi`
- JPEG MIME 映射存在
- Warframe label、swatches、desc 正确
- 动态与静态 welcome DOM 都有四个 Warframe 图层
- 不保留旧 `.welcome-art` 单图 placeholder
- 项目 UI 不引入 `cdn.displate.com``pinterest.com``pinimg.com`
- 本地资产 hash、格式、尺寸固定
- gilded CSS 变量块不含 `url(...)`
- 共享暗色 selector 必须包含 `gilded`
- Warframe 专属层必须包含 welcome scope、真实图片节点、无远程 URL、移动布局和 reduced-motion
- 关键文本对比度满足断言阈值
- `style.css` / `app.js` cache-busting 参数仍是 `20260717-warframe-hidden`
并存新主题时,需要同步调整这份测试:
| 目标 | 测试调整 |
| --- | --- |
| 新 ID `wasteland` 可见 | 断言 `value: 'wasteland'` 且不含 `hidden: true` |
| 保留 legacy `gilded` | 断言 `gilded` 隐藏,且 `index.html` 可按策略迁移到 `wasteland``washi` |
| CSS 复用 | 断言共享暗色 selector 包含 `wasteland` |
| 专属层泛化 | 断言 Warframe selectors 同时覆盖 `gilded`/`wasteland` 或只覆盖新 ID |
| 资产变更 | 更新 `GILDED_THEME_ASSETS` 或新增 `WASTELAND_THEME_ASSETS` |
| cache bust | 修改 `index.html``style.css?v=...``app.js?v=...` 后更新断言 |
## 新增并存主题的最小改动面
### 推荐:新增可见 `wasteland`,保留 `gilded` 隐藏
| 文件 | 最少改动 |
| --- | --- |
| `public/app.js` | 在 `THEME_OPTIONS` 新增 `value: 'wasteland'` 可见条目;可保留 `gilded hidden` 作为 legacy如复用 welcome DOM无需改 `buildWelcomeMarkup()` |
| `public/index.html` | 更新首屏迁移策略:历史 `gilded` 可迁移到 `wasteland`;设置新的 cache-busting query |
| `public/style.css` | 新增 `html[data-theme='wasteland']` 变量块;把共享暗色 selectors 纳入 `wasteland`;把 Warframe 专属层 selectors 从 `gilded` 泛化到 `gilded/wasteland` 或迁移为 `wasteland` |
| `public/assets/themes/*` | 若复用现有四图则不变;若换图则新增本地资产,不使用远程 URL |
| `scripts/regression.js` | 新增/调整主题契约,覆盖新 ID、可见性、迁移、CSS selector、资产 hash/尺寸、cache bust |
| `server.js` | 仅在引入新 MIME 类型时修改;当前 PNG/JPEG 已覆盖 |
### 可选:直接复活 `gilded`
| 文件 | 最少改动 |
| --- | --- |
| `public/app.js` | 删除 `hidden: true`,让 picker 与 `normalizeTheme()` 接受它 |
| `public/index.html` | 删除或反转 `gilded -> washi` 迁移 |
| `scripts/regression.js` | 删除“隐藏主题”相关断言改成“gilded 可见且可持久化” |
| `public/style.css` | 当前大部分 CSS 可复用,按需求微调 |
不推荐直接复活的原因:当前测试和 bootstrap 已明确把 `gilded` 定义为隐藏状态;复活会同时改变持久化、首屏、设置候选和回归契约,语义冲击更大。
## 可复用隔离模式
| 模式 | 价值 | 复用建议 |
| --- | --- | --- |
| 主题 ID 只写 `data-theme` | 让 CSS 完全接管视觉差异 | 新主题不要创建额外 body class |
| `THEME_OPTIONS` 单源 | 设置候选、label、swatch 同源 | 不要在设置页硬编码第二份列表 |
| `hidden` 双重隔离 | 同时从 picker 和持久化合法性排除 | legacy/实验主题可继续用 |
| 首屏 bootstrap | CSS 前写 `data-theme` 避免闪烁 | 新主题必须能被 bootstrap 识别或迁移 |
| 语义变量优先 | 大部分组件吃 `--bg-*``--text-*``--accent` | 新主题先补变量,再补组件特例 |
| 共享暗色 completion selector | 暗色主题共用大量控件适配 | 新暗色主题必须加入 `:is(...)` 列表 |
| 专属组件层后置 | 高风险视觉只在主题 selector 下生效 | Warframe 视觉不要污染普通主题 |
| 默认隐藏视觉舞台 | 非目标主题不会露出 Warframe DOM | 新主题要显式打开 `.welcome-visual` |
| 真实 `<img>` 资产节点 | 可被回归测试验证 hash/尺寸,避免远程 wallpaper | 继续保留本地资产和 `img` DOM |
| 回归防远程 URL | 防止参考站点进入产品 UI | 新资产必须落本地并断言 |
## 潜在脏文件冲突
当前工作区已经有大量未提交内容。本次研究只应新增该文档;后续实现必须先确认这些脏文件归属,避免覆盖他人工作。
### 已修改的跟踪文件
| 文件 | 与主题关系 |
| --- | --- |
| `public/app.js` | 高相关主题注册、picker、welcome DOM |
| `public/index.html` | 高相关:首屏主题 bootstrap、静态 welcome、cache bust |
| `public/style.css` | 高相关:主题变量和 Warframe CSS |
| `scripts/regression.js` | 高相关:主题回归契约 |
| `server.js` | 中相关:静态 MIME |
| `dist-exe/cc-web-bun-linux-x64-baseline.tar.gz` | 发布包脏文件,主题实现一般不应直接改 |
| `.planning/subagent-card-metadata/*``.trellis/tasks/07-11-subagent-card-metadata/prd.md` | 其他任务痕迹,避免误动 |
### 未跟踪目录/文件
| 路径 | 说明 |
| --- | --- |
| `.trellis/tasks/07-17-gilded-wasteland-theme/` | 当前任务目录未跟踪,本研究文档会落在这里 |
| `.trellis/tasks/07-17-warframe-gallery-redesign/` | 相关但独立任务 |
| `public/assets/themes/` | Warframe 资产目录未跟踪 |
| `.planning/dark-gilded-theme/` 等多个 `.planning/*` | 历史/并行规划产物 |
| `新增暗金荒野主题 TO DO list.csv``重做 Warframe 饱满主题 TO DO list.csv` | 临时 TODO CSV后续实现前需确认是否可清理 |
## 验证命令
建议按从轻到重执行:
```bash
# 1. 只跑主题契约
npm run regression -- --target gilded-theme
# 2. 跑全量回归
npm run regression
# 3. 检查补丁空白错误
git diff --check
# 4. 检查主题相关文件变更面
git diff -- public/app.js public/index.html public/style.css scripts/regression.js server.js
```
如果要做浏览器视觉验证:
```bash
# 启动服务
npm start
# 本地确认服务可用
curl http://127.0.0.1:<port>/
# Playwright 远程环境访问时,优先使用 11.144.144.* 网段地址
hostname -I
```
注意:本次研究未运行回归测试;以上命令是后续实现/验收阶段应执行的验证路径。

View File

@@ -0,0 +1,374 @@
# 暗金荒野视觉返工审计
审计对象:
- 参考图:`sessions/_attachments/f01c1388-b603-434a-b226-2ff5771d4925.webp`
- 背景图:`sessions/_attachments/094d7907-875d-41f4-9985-992c87c9b9cc.webp`
- 当前 CSS`public/style.css`,重点为 `/* === 暗金荒野` 到文件末尾
- 当前 mock`.planning/gilded-wasteland-theme/visual-preview.html`
审计结论:当前版本的问题不是主题元素不够多,而是过度把所有组件做成厚金边、硬切角、重阴影卡片。参考图的核心语言是“全画布骑士背景 + 半透明黑铁薄面板 + 1px 旧金蚀刻线 + 局部角饰”,需要删重而不是继续加装饰。
## 1. 参考图 1672x941 像素比例
以下尺寸为按原尺寸图像检查后的近似像素坐标,误差约 2-6px适合作为 CSS 目标值。
| 区域 | 参考图坐标 | 像素尺寸 | 视口占比 | 当前 CSS/Mock 主要偏差 |
|---|---:|---:|---:|---|
| 左侧栏整体 | `x=0-316, y=0-941` | `316x941` | 宽 `18.9%`,高 `100%` | 当前 `--sidebar-width: 280px`,仅 `16.7%`,明显偏窄 |
| 主工作区 | `x=316-1672, y=0-941` | `1356x941` | 宽 `81.1%` | 当前主区更早暴露背景,但侧栏比例不对 |
| 顶栏 | `x=316-1672, y=0-57` | `1356x57` | 高 `6.1%` | 当前 `--header-height: 52px`,略矮 |
| 侧栏顶部按钮组 | `x=15-303, y=10-57` | `288x47` | 侧栏内宽 `91.1%` | 当前按钮形状可接近,但金边过亮、切角太统一 |
| 新会话主按钮 | `x=15-245, y=10-57` | `230x47` | 侧栏宽 `72.8%` | 当前宽度受 280px 侧栏影响,整体缩窄 |
| 新会话下拉 | `x=250-303, y=10-57` | `53x47` | 侧栏宽 `16.8%` | 当前可以保留分裂按钮,但边框需要降亮度 |
| 搜索框 | `x=15-303, y=68-102` | `288x34` | 侧栏宽 `91.1%` | 当前 `clip-path` 切角不应扩散到搜索框 |
| 会话列表主区 | `x=15-303, y=118-870` | `288x752` | 高 `79.9%` | 当前 active item 过厚、阴影过明显 |
| 侧栏底栏 | `x=0-316, y=870-941` | `316x71` | 高 `7.5%` | 当前底栏暗度可保留,但不要成为厚卡片 |
| 消息可视带 | `x=316-1672, y=57-795` | `1356x738` | 高 `78.4%` | 当前消息卡片堆叠感更强 |
| 第一条 assistant 面板 | `x=400-1043, y=87-431` | `643x344` | 宽 `38.5%`,高 `36.6%` | 当前 max-width 接近,但黑底和切角过重 |
| 用户气泡 | `x=1152-1572, y=453-516` | `420x63` | 宽 `25.1%`,高 `6.7%` | 当前用户气泡过暗、阴影厚 |
| 第二条 assistant 面板 | `x=400-1397, y=516-774` | `997x258` | 宽 `59.6%`,高 `27.4%` | 当前 tool-call 产生太多内部卡片层 |
| 输入区总高度 | `x=316-1672, y=795-941` | `1356x146` | 高 `15.5%` | 当前 `.input-area` 作为整条暗底带太明显 |
| 模式按钮行 | `x=563-899, y=795-824` | `336x29` | 主区宽 `24.8%` | 当前状态胶囊可保留,边框和填充应更轻 |
| 输入框外框 | `x=557-1363, y=832-905` | `806x73` | 宽 `48.2%`,高 `7.8%` | 当前 max-width `800px` 接近,但高度 `54px` 偏矮,阴影偏重 |
建议直接落地的布局变量:
```css
html[data-theme='wasteland'] {
--sidebar-width: 316px;
--header-height: 57px;
}
```
移动端不应照搬 316px可继续使用 `min(88vw, 320px)`,但桌面 mock 必须先贴近参考图。
## 2. 边框、透明度、切角和面板层级
### 边框线宽
| 元素类型 | 参考图特征 | 建议 CSS 值 | 当前问题 |
|---|---|---|---|
| 全局旧金线 | 大多是 1px抗锯齿后局部看似 2px | `1px solid rgba(185, 139, 75, 0.28)` | 当前多处用 `rgba(..., 0.52-0.64)`,视觉太亮 |
| 高亮角线 | 只在角、按钮边缘、active 状态短暂增强 | `rgba(223, 180, 108, 0.44-0.58)` | 当前整条边都亮,不像蚀刻线 |
| 内发亮线 | 很弱,只用于面板顶部/内侧 | `inset 0 1px rgba(255, 230, 180, 0.04-0.08)` | 当前叠加了大阴影,层级失真 |
| active 会话 | 1px 金线 + 微弱背景,不是粗卡片 | `border-color: rgba(196,154,90,.46)` | 当前 `inset 3px` + 外阴影太重 |
| tool-call 内框 | 1px 细线,嵌在消息面板内 | `border-color: rgba(185,139,75,.30)` | 当前 `border-left: 2px``clip-path` 抢层级 |
### 透明度层级
参考图不是纯黑 UI而是让背景纹理穿过面板。建议把层级收敛为 4 档:
| 层级 | 用途 | 建议值 |
|---|---|---|
| 页面遮罩 | 保证左侧可读,右侧人物可见 | 左侧最高 `0.72`,人物区 `0.04-0.18`,底部 `0.44` |
| 侧栏大底 | 黑铁主面板 | `rgba(5, 7, 7, 0.72-0.78)` |
| 消息/输入面板 | 半透明工作区卡面 | `rgba(8, 10, 10, 0.62-0.76)` |
| 小按钮/标签 | 状态和工具按钮 | `rgba(12, 14, 12, 0.48-0.66)` |
当前 CSS 中这些值偏重:
- `body` 左侧遮罩 `0.96/0.90/0.68`,会把背景压成黑墙。
- `.sidebar` 背景 `rgba(7,9,8,0.92)`,比参考图更像实心抽屉。
- `.msg.assistant` 背景 `rgba(12,15,13,0.88)`,压掉背景纹理。
- `.input-wrapper` 背景 `rgba(8,10,9,0.91)`,过实。
- `.wasteland-panel-strong` `0.93` 加上 `--shadow-strong`,弹层会变成厚卡片。
### 切角位置
参考图的切角是“局部外框语言”,不是全组件形状规则。
| 区域 | 参考图切角 | 建议 |
|---|---|---|
| 新会话主按钮/下拉按钮 | 顶部左右约 `6-8px` 斜切,底边更克制 | 可保留小切角 |
| 输入框外框 | 外框角部约 `6-8px`,并带底部中心装饰 | 可保留局部角饰,但不要把内容裁掉 |
| 消息面板 | 基本是轻圆角矩形,角部仅有细线高光 | 删除 `clip-path`,用 `border-radius: 8px` |
| 会话列表项 | active 项有边框和微弱纹理,不是每一项都硬切 | 删除普通项 `clip-path` |
| 搜索框/状态胶囊 | 小圆角/轻边框 | 删除切角 |
| tool-call | 内嵌细线框,局部角线即可 | 删除 `clip-path` 和厚左边 |
| 弹层/菜单 | 可使用轻角饰,但不能整块重卡片 | 优先用 border/pseudo corner不用全局裁切 |
核心建议:不要继续依赖 `--wasteland-cut` 做全局形状。只保留一个小尺寸切角变量给主按钮和输入框外框,其余组件回到圆角或直角薄线。
## 3. 人物/背景可见范围与遮罩梯度
背景图与项目资源一致,`public/assets/themes/wasteland/background.webp` 和给定背景附件 hash 一致。背景本身就是 1672x941桌面 `cover center` 不会裁切,这是好事。
参考图中人物可见范围:
| 背景元素 | 参考图大致范围 | 视觉要求 |
|---|---:|---|
| 骑士头盔/头肩 | `x=1180-1370, y=85-265` | 必须清晰可辨,不能被右侧遮罩压黑 |
| 胸甲主体 | `x=1110-1415, y=190-620` | 是第一视觉锚点,应保留高光轮廓 |
| 法杖/右臂 | `x=1375-1535, y=330-610` | 需要在用户气泡后仍可见 |
| 披风左延展 | `x=660-1230, y=270-730` | 应穿过消息面板背后,增强深度 |
| 下摆/地面 | `x=1000-1510, y=610-941` | 输入区背后应能隐约看到,不要全黑 |
| 圆环符文 | `x=910-1580, y=15-655` | 右半背景层次的关键,不应被径向金色光斑覆盖 |
参考图遮罩规律:
1. 左侧 0-320px 需要更暗,但不是全黑,仍能看到纹理。
2. 中间 400-1000px 是消息阅读区,面板负责可读性,页面遮罩不应过重。
3. 人物区域 1050-1500px 遮罩最轻,保留胸甲和头肩轮廓。
4. 底部 800-941px 有自然暗角,但输入框外部仍可透出地面和披风。
5. 人工火星不应叠加到很显眼,背景图已有足够颗粒和火星。
建议替换 `body` 背景遮罩:
```css
html[data-theme='wasteland'] body {
background-color: #080a09;
background-image:
linear-gradient(
90deg,
rgba(3, 5, 5, 0.72) 0%,
rgba(3, 5, 5, 0.58) 18%,
rgba(3, 5, 5, 0.34) 43%,
rgba(3, 5, 5, 0.08) 66%,
rgba(3, 5, 5, 0.18) 100%
),
linear-gradient(
180deg,
rgba(0, 0, 0, 0.18) 0%,
rgba(0, 0, 0, 0.02) 32%,
rgba(0, 0, 0, 0.12) 72%,
rgba(0, 0, 0, 0.44) 100%
),
url('assets/themes/wasteland/background.webp');
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-attachment: fixed;
}
```
建议删除当前 `body::after` 人工火星层。若必须保留,`opacity` 不应超过 `0.18`,且不要动画漂移。
## 4. 当前 CSS 最不相似的 10 项
| 排名 | 影响 | 当前位置 | 不相似点 | 返工方向 |
|---:|---|---|---|---|
| 1 | 极高 | `style.css:7498-7502` | 页面遮罩太黑,左侧 `0.96`、中部 `0.68`、底部 `0.74`,人物和纹理被压扁 | 降低遮罩,人物区保持 `0.04-0.18` |
| 2 | 极高 | `style.css:805``7589-7594``7664-7665``7722-7723``7739-7744``7775-7780``7831-7838``7942-7948``7998-8009` | `clip-path` 被扩散到消息、弹层、输入、会话项、tool-call、按钮、搜索框和欢迎卡片 | 只给新会话按钮、输入外框少量角饰;其余删除 |
| 3 | 极高 | `style.css:7831-7838` | 主按钮边框 `rgba(...,0.64)` 加切角和阴影,变成厚金属按钮 | 边框降到 `0.42-0.48`,阴影删掉,角饰局部化 |
| 4 | 高 | `style.css:7541-7548` | 侧栏 `0.92` 实心黑 + `18px 48px` 侧向阴影,参考图更轻、更透 | 侧栏背景降到 `0.72-0.78`,只留内侧细线 |
| 5 | 高 | `style.css:7566-7572` | 顶栏和输入区阴影过强,形成上下厚压条 | 顶栏保留底线;输入区背景透明、无整条阴影 |
| 6 | 高 | `style.css:7584-7617` | 消息面板硬切角、暗底 `0.88-0.90`、外阴影重,不像参考图的薄线半透明面板 | 圆角 `8px`,背景 `0.64-0.70`,删除外阴影 |
| 7 | 高 | `style.css:7716-7726` | 输入框高度 `54px` 偏矮,背景 `0.91` 偏实,阴影厚;参考图为约 `806x73` 的长金属薄框 | `max-width:806px``min-height:72px`,背景 `0.76`,轻内线 |
| 8 | 中高 | `style.css:7739-7759` | 会话项每条切角active 有 `inset 3px` 和外阴影,列表太卡片化 | 普通项无边框active 仅 1px 旧金线和轻背景 |
| 9 | 中高 | `style.css:7775-7807` | tool-call 独立卡片化,`border-left:2px`、切角、阴影抢走消息层级 | 作为消息内部嵌套细线框,去左粗边和阴影 |
| 10 | 中 | `style.css:7913-7968` | 统一 22-24px bitmap pseudo-icon 并隐藏 SVG图标偏大偏实 | 图标降到 `16-18px`,优先保留线性符号或降低 bitmap opacity |
补充布局偏差:
- `style.css:31-32` 当前全局 `--sidebar-width: 280px``--header-height: 52px`,参考图约为 `316px``57px`
- mock 使用 `html data-theme="wasteland"` 并直接引入当前 CSS因此该比例偏差会直接出现在预览里。
## 5. 可直接落成 CSS 的具体值
### 5.1 主题变量
```css
html[data-theme='wasteland'] {
--sidebar-width: 316px;
--header-height: 57px;
--wasteland-iron: #080a09;
--wasteland-panel: rgba(8, 10, 10, 0.66);
--wasteland-panel-soft: rgba(8, 10, 10, 0.52);
--wasteland-panel-strong: rgba(7, 9, 9, 0.74);
--wasteland-gold: #b98b4b;
--wasteland-gold-bright: #dfb46c;
--wasteland-line: rgba(185, 139, 75, 0.30);
--wasteland-line-strong: rgba(223, 180, 108, 0.44);
--wasteland-corner: rgba(231, 185, 109, 0.58);
--shadow-strong: 0 14px 34px rgba(0, 0, 0, 0.30);
}
```
### 5.2 侧栏和顶部
```css
html[data-theme='wasteland'] .sidebar {
background:
linear-gradient(180deg, rgba(185, 139, 75, 0.035), transparent 24%),
rgba(5, 7, 7, 0.76);
border-right: 1px solid rgba(185, 139, 75, 0.32);
box-shadow: inset -1px 0 rgba(255, 230, 180, 0.08);
backdrop-filter: blur(4px) saturate(0.9);
}
html[data-theme='wasteland'] :is(.sidebar-header, .sidebar-footer, .chat-header) {
background: rgba(7, 9, 9, 0.46);
border-color: rgba(185, 139, 75, 0.26);
backdrop-filter: blur(3px) saturate(0.9);
}
html[data-theme='wasteland'] .chat-header {
box-shadow: none;
}
```
### 5.3 消息面板
```css
html[data-theme='wasteland'] .messages {
padding-top: 29px;
padding-bottom: 20px;
}
html[data-theme='wasteland'] .msg {
max-width: min(74%, 997px);
}
html[data-theme='wasteland'] .msg.user {
max-width: min(42%, 480px);
}
html[data-theme='wasteland'] .msg-bubble {
border: 1px solid rgba(185, 139, 75, 0.28);
border-radius: 8px;
clip-path: none;
backdrop-filter: blur(4px) saturate(0.9);
box-shadow: inset 0 1px rgba(255, 230, 180, 0.05);
}
html[data-theme='wasteland'] :is(
.msg.assistant .msg-bubble,
.msg.cross-conversation .msg-bubble,
.msg.cross-conversation-reply .msg-bubble,
.msg.system .msg-bubble[data-tone='info']
) {
background: rgba(8, 10, 10, 0.66);
border-color: rgba(185, 139, 75, 0.28);
box-shadow: inset 0 1px rgba(255, 230, 180, 0.05);
}
html[data-theme='wasteland'] .msg.user .msg-bubble {
background: rgba(38, 28, 19, 0.62);
border-color: rgba(196, 154, 90, 0.42);
box-shadow: inset 0 1px rgba(255, 230, 180, 0.06);
}
```
### 5.4 输入区
```css
html[data-theme='wasteland'] .input-area {
background: transparent;
border-top: 0;
box-shadow: none;
padding: 10px 16px 36px;
}
html[data-theme='wasteland'] .input-wrapper {
position: relative;
width: min(806px, calc(100% - 48px));
max-width: 806px;
min-height: 72px;
padding: 10px 14px;
border: 1px solid rgba(185, 139, 75, 0.46);
border-radius: 6px;
clip-path: none;
background: rgba(7, 8, 8, 0.76);
box-shadow:
inset 0 0 0 1px rgba(255, 230, 180, 0.06),
0 6px 18px rgba(0, 0, 0, 0.18);
backdrop-filter: blur(4px) saturate(0.9);
}
html[data-theme='wasteland'] .input-wrapper::after {
content: '';
position: absolute;
left: 50%;
bottom: -22px;
width: 62px;
height: 28px;
transform: translateX(-50%);
background:
linear-gradient(135deg, transparent 42%, rgba(223, 180, 108, 0.58) 43% 47%, transparent 48%),
linear-gradient(45deg, transparent 42%, rgba(223, 180, 108, 0.58) 43% 47%, transparent 48%);
opacity: 0.72;
pointer-events: none;
}
```
### 5.5 侧栏项、按钮和 tool-call
```css
html[data-theme='wasteland'] .session-item {
border: 1px solid transparent;
border-radius: 4px;
clip-path: none;
box-shadow: none;
}
html[data-theme='wasteland'] .session-item:hover {
background: rgba(185, 139, 75, 0.08);
border-color: rgba(185, 139, 75, 0.22);
transform: none;
}
html[data-theme='wasteland'] .session-item.active {
background: rgba(43, 31, 20, 0.50);
border-color: rgba(196, 154, 90, 0.46);
box-shadow: inset 1px 0 rgba(223, 180, 108, 0.48);
}
html[data-theme='wasteland'] :is(.tool-call, .ccweb-prompt-card) {
border: 1px solid rgba(185, 139, 75, 0.30);
border-radius: 4px;
background: rgba(8, 10, 10, 0.58);
clip-path: none;
box-shadow: none;
}
html[data-theme='wasteland'] :is(.new-chat-btn, .new-chat-arrow) {
min-height: 48px;
border: 1px solid rgba(196, 154, 90, 0.48);
border-radius: 2px;
background: rgba(10, 12, 11, 0.68);
color: #dfbd82;
box-shadow: inset 0 1px rgba(255, 230, 180, 0.07);
}
html[data-theme='wasteland'] :is(.attach-btn, .send-btn, .abort-btn)::before {
width: 18px;
height: 18px;
opacity: 0.82;
}
```
## 6. 建议删除或替换的当前规则
优先删除这些规则,而不是在后面继续覆盖:
| 当前位置 | 建议动作 | 原因 |
|---|---|---|
| `style.css:7498-7502` | 替换整个 `background-image` | 当前遮罩太黑,径向金光也会污染符文圆环 |
| `style.css:7509-7522` | 删除 `body::after` | 人工火星层太显眼,且动画会让背景像装饰层 |
| `style.css:7546` | 删除侧栏大外阴影 | 参考图没有厚侧投影 |
| `style.css:7566-7572` | 删除顶栏/输入区外阴影 | 上下压条感过强 |
| `style.css:7589-7594` | 删除 `border-radius:0``clip-path`,降低 blur | 消息面板应轻圆角、半透明 |
| `style.css:7596-7617` | 替换用户/assistant 背景和阴影 | 当前背景太实、阴影太厚 |
| `style.css:7659-7667` | 删除弹层全局 `clip-path``--shadow-strong` 套用 | 弹层变成厚金属卡片 |
| `style.css:7670-7714` | 拆分大范围组件组 | 所有组件共用一个卡片底会形成卡片堆叠 |
| `style.css:7716-7726` | 替换输入框规则 | 高度、背景透明度、阴影、切角都偏离 |
| `style.css:7728-7736` | 降低 focus/drag 阴影 | 参考图状态变化克制,不应出现 3px 光圈 |
| `style.css:7739-7759` | 删除普通会话项切角、hover 位移、active 厚阴影 | 列表应是薄线面板,不是卡片列表 |
| `style.css:7775-7781` | 删除 tool-call 左粗边、切角和阴影 | tool-call 应嵌在消息面板内部 |
| `style.css:7821-7858` | 替换按钮组厚金边、clip 和 hover 光圈 | 当前最像“游戏按钮皮肤”,不似参考图薄金属线 |
| `style.css:7913-7968` | 缩小或移除统一 pseudo bitmap 图标规则 | 图标过大过实,且隐藏 SVG 后细线感丢失 |
| `style.css:7986-8010` | 删除欢迎卡片厚框、切角和大阴影 | 欢迎页不能另起一套卡片重装饰 |
| `style.css:8068-8071` | 删除 `wastelandEmberDrift` | 若删除 `body::after`,该动画也应删除 |
| `style.css:8074-8087` | 替换移动端重遮罩和 `0.97` 侧栏背景 | 移动端也不应变成纯黑抽屉 |
## 7. 执行顺序建议
1. 先改布局变量:`--sidebar-width:316px``--header-height:57px`
2. 再改 `body` 遮罩,确保骑士主体、符文圆环、披风透出来。
3. 删除全组件 `clip-path`,只保留主按钮和输入框的局部角饰。
4. 降低所有面板背景 alpha 和外阴影,统一回 1px 旧金线。
5. 最后再处理图标尺寸和 active/hover 状态。
如果只做配色微调不删厚边、全局切角和重阴影mock 仍会偏离参考图。

View File

@@ -0,0 +1,26 @@
{
"id": "gilded-wasteland-theme",
"name": "gilded-wasteland-theme",
"title": "新增暗金荒野主题",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "shiyue",
"assignee": "shiyue",
"createdAt": "2026-07-17",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,2 @@
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"审查前后端附件格式、验证责任、持久化和协议输入是否一致。"}
{"file":".trellis/tasks/07-17-runtime-image-send/research/runtime-image-input.md","reason":"核对协议事实、测试预期和已有工作树改动保护。"}

View File

@@ -0,0 +1,2 @@
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"改动跨越 composer、WebSocket、附件解析、会话持久化和 app-server 协议,需要保持边界数据一致。"}
{"file":".trellis/tasks/07-17-runtime-image-send/research/runtime-image-input.md","reason":"包含本机 turn/steer 协议、附件安全边界、mock 测试能力和工作树冲突约束。"}

View File

@@ -0,0 +1,35 @@
# 技术设计
## 数据流
```text
上传接口返回附件元数据
├─ 普通 composer → submitUserMessage(text, attachments)
├─ 排队 → queueItem.attachments → drain → submitUserMessage
└─ 运行中插入 → WebSocket message.attachments
handleMessage 统一解析
resolvedAttachments含服务端 path
savedAttachments无服务端 path
handleCodexAppSteerMessage
├─ 会话历史保存 savedAttachments
├─ turn/steer 使用 resolvedAttachments
└─ stale fallback 使用 resolvedAttachments
```
## 安全边界
- 前端只发送附件 ID 与公开元数据。
- 服务端通过 `resolveMessageAttachments` 重新读取元数据文件、检查 storageState 与真实文件存在性。
- `localImage.path` 只能来自服务端解析结果。
- 会话 JSON 只保存 `savedAttachments`,不泄露服务端本地路径。
## 边界情况
- 纯图片:允许;标题/提示回退为图片文件名。
- 部分附件失效:保持现有语义,只发送仍可解析的附件。
- 所有附件失效且无文本:返回既有 `attachment_unavailable`/`empty_message` 错误。
- 删除队列项:清理该项附件,避免孤儿上传。
- stale steer旧 assistant 输出仍按 timestamp + content 插到已持久化用户消息之前。

View File

@@ -0,0 +1,58 @@
# 修复排队与运行中图片发送
## 背景
cc-web 已支持普通消息附带图片,也已通过 Codex app-server 的 `UserInput.localImage` 向新 turn 发送图片。但当前前端会拒绝排队消息与运行中插入消息携带图片,后端 steer 路径也会再次拒绝附件。
## 目标
在不改变现有纯文本、slash 指令和笔记模式语义的前提下,让现有图片附件可以:
1. 随排队消息保存,并在当前 turn 结束后发送。
2. 随 Codex App 运行中消息通过 `turn/steer` 插入。
3. 在 stale turn 自动转为新 turn 时继续保留。
4. 在会话历史与界面中保持与模型实际输入一致。
## 功能要求
### 排队发送
- 队列项保存创建时的附件元数据快照,不能依赖全局 composer 附件状态。
- 文本或附件至少存在一个即可入队;纯图片队列允许发送。
- 含 slash 的文本仍按现有规则禁止排队。
- 入队成功后清空 composer 文本和附件托盘。
- 队列卡片显示附件摘要,使用户能确认附件仍在队列中。
- 卡片内编辑只修改文本并保留附件。
- 删除未发送队列项时清理其上传附件。
- 出队时调用普通发送入口并传入附件数组。
### Codex App 运行中插入
- 前端运行中插入消息必须携带附件数组,用户气泡也显示相同附件。
- 插入成功提交后清空 composer 文本与附件托盘。
- 后端复用 `handleMessage` 已解析的安全附件,不信任前端路径。
- 持久化用户消息时保存规范化附件元数据。
- `turn/steer.input` 同时包含文本与 `localImage`;允许纯图片插入。
- 无文本时,系统提示使用图片文件名生成可读摘要。
- app-server 返回 `no active turn to steer`replacement turn 必须继续使用同一批已解析附件。
## 非目标
- 不支持图片以外的任意文件附件。
- 不延长附件 TTL也不保证超出既有 TTL 后仍可发送。
- 不改变 slash 指令、笔记模式、Claude/Codex CLI 进程模式的行为。
- 不重构现有附件存储体系。
## 验收标准
- 源码中不再存在排队图片和 Codex App 运行中图片的主动拒绝分支。
- 排队项保存附件,出队发送时附件不丢失,删除时清理附件。
- 成功 `turn/steer` 的 mock 输出能观察到 `localImage`
- steer 用户历史包含原始附件文件名和可用状态。
- stale turn replacement 输出能观察到同一图片,历史中的用户消息只出现一次且保留附件。
- 新增 `node scripts/regression.js --target runtime-image-send` 在 60 秒内通过。
- `node --check public/app.js``node --check server.js``node --check scripts/regression.js` 通过。
## 改动保护
工作树中已有主题与回归相关未提交改动。实现必须使用局部补丁,禁止覆盖或格式化无关区域。

View File

@@ -0,0 +1,22 @@
# 运行中图片输入调研
## 协议事实
- 本机 `codex-cli 0.144.1` 生成的 `TurnSteerParams` Schema 定义 `input: UserInput[]`
- `UserInput` 联合类型包含 `{ type: "localImage", path, detail? }`
- cc-web 的普通 Codex App turn 已通过 `codexAppInputFromMessage` 使用这一结构。
## 代码事实
- `handleMessage` 在选择普通 turn 或 steer 之前已经调用 `resolveMessageAttachments`
- `resolveMessageAttachments` 只返回元数据记录存在、未过期且真实文件仍存在的附件。
- `sanitizeMessageForPersist` 对历史附件统一调用 `normalizeMessageAttachments`
- mock app-server 的 `textFromInput` 会把 `localImage` 显示为 `[image:<basename>]`,适合协议回归断言。
- stale completion 使用用户消息的 timestamp + content 定位插入点,保存附件不会破坏顺序。
## 工作树约束
- `public/app.js` 有其他主题相关改动。
- `scripts/regression.js` 有大量其他回归新增。
- `server.js` 现有未提交差异仅涉及 jpg/jpeg 静态 MIME。
- 所有修改必须限制在相关函数与测试邻近区域。

View File

@@ -0,0 +1,26 @@
{
"id": "runtime-image-send",
"name": "runtime-image-send",
"title": "修复排队与运行中图片发送",
"description": "",
"status": "planning",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "shiyue",
"assignee": "shiyue",
"createdAt": "2026-07-17",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,4 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
{"file": ".trellis/spec/frontend/index.md", "reason": "前端质量检查规范"}
{"file": ".planning/warframe-theme-art-direction/task_plan.md", "reason": "四图主题和回归验收标准"}
{"file": ".planning/warframe-rich-theme/task_plan.md", "reason": "新单主视觉、真实快捷入口、三档截图与回归验收"}

View File

@@ -0,0 +1,4 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
{"file": ".trellis/spec/frontend/index.md", "reason": "前端结构、样式和响应式实现规范"}
{"file": ".planning/warframe-theme-art-direction/task_plan.md", "reason": "已审查通过的主题构图、素材与验收契约"}
{"file": ".planning/warframe-rich-theme/task_plan.md", "reason": "已审查通过的饱满 Warframe 主题策略、结构与截图验收"}

View File

@@ -0,0 +1,33 @@
# Warframe 饱满主题重做
## 背景
上一版多图方案已因“海报拼贴、抽象、主题不饱满”被用户否定并隐藏。本轮参考用户提供的
三套高完成度 Codex 主题,从单一横向主视觉和全页面视觉语言重新实现。参考图中的功能
入口只用于判断视觉密度,不复制到 cc-web也不改变现有输入行为。
## 需求
1. 基于原始荒野角色,通过裁切、定位和渐隐融合为无拼贴的横向连续主视觉,原图不被覆盖。
2. 其他三张 Warframe 画作只提供材质/光色参考,不作为独立海报摆放。
3. 欢迎态保留原有标题、说明和输入区,不新增快捷入口、预设提示词或点击填充逻辑。
4. 导航、按钮、输入框、会话项、菜单和聊天表面采用统一 Orokin 图形与材质语言。
5. 聊天正文保持不透明高对比表面;非 gilded 主题不受影响。
6. 通过三档 Firefox 截图审查后才取消隐藏并交付。
## 验收
- 新主视觉本地资源返回 200 和正确图片类型hash/尺寸固定。
- 欢迎态和聊天态都具备统一主题语言,输入内容与交互保持原样。
- 1440、1024、390 三档截图通过;专项与完整回归通过。
- 视觉审查确认不是海报拼贴、纯换色或大面积空洞。
## 原始素材映射
- 原图:`public/assets/themes/gilded-wasteland.png`,保持原文件与 hash最大主画面。
- `/tmp/warframe-candidate-1.jpg``warframe-ember-squad.jpg`,赤红战场切片。
- `/tmp/warframe-candidate-2.jpg``warframe-orokin-figure.jpg`,冷青人物前景。
- `/tmp/warframe-candidate-3.jpg``warframe-sacrifice.jpg`,暗青金色中轴深景。
`scripts/regression.js` 需改为新单主视觉、本地资产、无主题专属快捷交互、主题覆盖、隐藏至
截图通过、正文无图片背景和移动重排契约,不得继续保留旧四图拼贴断言。

View File

@@ -0,0 +1,26 @@
{
"id": "warframe-gallery-redesign",
"name": "warframe-gallery-redesign",
"title": "Warframe 单主视觉主题重构",
"description": "",
"status": "completed",
"dev_type": null,
"scope": "public/app.js public/index.html public/style.css public/assets/themes scripts/regression.js",
"package": null,
"priority": "P2",
"creator": "shiyue",
"assignee": "shiyue",
"createdAt": "2026-07-17",
"completedAt": "2026-07-17",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}