Update agent visibility fixes and local docs
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-21 02:01:56 +08:00
parent ccc2e45b12
commit fe8a8564e7
12 changed files with 2506 additions and 41 deletions

View File

@ -0,0 +1,332 @@
# AI 生图平台桶缓存落地对接清单
> 交付对象:AI 生图平台负责人
> 当前主接口域名:`https://sc.jiansun.vip`
> 当前资源主域名:`https://s3.jiansun.vip`
> 备用资源域名:`https://s3.yydd.ai`
> 文档时间:`2026-03-18`
## 1. 这次要做的事情
计划把新入口服务器 `180.97.215.57` 上独立数据盘中的 `180G` 空间,用作 AI 生图平台相关资源的**读取缓存**。
这件事的目标是:
- 提升 AI 生图平台读取图片资源时的稳定性
- 降低反复回源到生产机的压力
- 让热点资源第二次及之后访问更快
## 2. 这次不会变的东西
下面这些默认都**不变**:
- 桶本体位置不变,真实存储仍在生产机 `192.168.58.20:9000`
- AI 生图平台的接口入口仍然是 `https://sc.jiansun.vip`
- 对外资源主域名仍然是 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 先保留为备用域名,不作为默认对接地址
- 这次做的是“读缓存”,不是改上传链路,也不是迁桶
一句话说清楚:
**缓存只是在入口服务器上多放一份“最近常用内容的副本”,不是把桶搬走。**
## 3. 当前已经确认的事实
当前资源入口链路是:
```text
s3.jiansun.vip
-> 180.97.215.57
-> nginx 磁盘缓存
-> 127.0.0.1:29000
-> FRP
-> 192.168.58.20:9000
```
当前缓存盘配置已经到位:
- 缓存目录:`/data/nginx-cache/s3`
- 缓存上限:`180g`
- 当前入口机资源盘:`/data`,总容量约 `200G`
`2026-03-18` 实测真实桶链路时,使用了一个约 `145MB` 的真实桶对象做下载测试:
- 直回源 `127.0.0.1:29000`:约 `23.2 MB/s`
- 走 `https://s3.jiansun.vip` 第一次请求:`X-Cache-Status: MISS`,约 `22.7 MB/s`
- 同一个 URL 第二次请求:`X-Cache-Status: HIT`,约 `63.2 MB/s`
这说明:
- 首次访问主要受回源链路影响
- 命中缓存后,速度会明显提升
基于当前已验证结果,可以先明确 3 个结论:
- 这套 `180G` 读缓存方案本身是可行的
- 它适合承接 AI 生图平台里“会被重复访问”的热点资源
- 在不改业务入口、不迁桶的前提下,就可以先做灰度验证
## 4. 我还需要 AI 生图平台这边提供什么
为了把这 `180G` 真正用于 AI 生图平台的热点流量,而不是“缓存了一堆不重要的文件”,还需要下面这些信息。
### 4.1 需要确认的资源范围
请提供:
- AI 生图平台当前实际会读取的资源类型
- 这些资源对应的 URL 样例
- 最常访问的路径前缀
最好直接给:
- 近 3 天最常访问的 20 个资源 URL 样例
- 主要路径前缀清单
例如是否主要集中在:
- 生图输入参考图
- 生图结果图
- 放大图、变体图、蒙版图、压缩包等固定路径前缀
### 4.2 需要确认是否只使用一个主资源域名
这个点非常关键。
请确认:
- AI 生图平台后续是否统一只读 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 是否只作为备用 / 回切,不参与日常默认调用
推荐口径:
- 主用:`https://s3.jiansun.vip`
- 备用:`https://s3.yydd.ai`
如果两个域名同时长期对外使用,那么**同一份内容就可能同时出现两个公网 URL**。
### 4.3 需要确认文件是否“同 key 覆盖”
这个点决定缓存能不能放心加大。
请确认:
- 同一张图后续会不会“覆盖原来的对象 key”
- 还是每次生成新图都使用新文件名 / 新路径
最推荐的方式是:
- **新内容永远用新 key**
比如:
- `image-20260318-001.png`
- `image-20260318-002.png`
不推荐的方式是:
- 总是写回同一个 URL
比如始终覆盖:
- `latest.png`
原因很简单:
- 如果同一个 URL 对应的文件内容被覆盖了,缓存里可能暂时还是旧版本
- 这样就会出现“URL 没变,但内容短时间还是旧图”的情况
### 4.4 需要确认时效要求
请确认:
- 图片更新后,允许多久内出现旧图
- 删除后,是否允许短时间还能访问
- 是否存在“必须秒级生效”的路径
如果某些资源要求非常高的实时性,就不能和普通静态资源用完全同一套缓存策略。
### 4.5 需要确认流量规模
请提供一个大致量级即可,不需要特别精确。
需要知道:
- 峰值并发大概多少
- 每分钟 / 每小时大概多少请求
- 热门图片是否会被大量重复读取
- 单张图通常多大
- 是否有明显的突发高峰时段
### 4.6 需要一批联调样本
请提供:
- 5 到 10 个真实可访问的 AI 生图平台资源 URL
- 最好覆盖“结果图 / 参考图 / 较大文件”几类
这样我可以直接按真实资源做灰度验证,而不是拿不相关对象代替。
### 4.7 这次不需要 AI 生图平台配合改的内容
为了避免沟通误差,这里也明确一下当前**不要求**你们先改的东西:
- 不要求先改接口域名,接口仍然走 `https://sc.jiansun.vip`
- 不要求先迁桶,桶本体仍然在 `192.168.58.20:9000`
- 不要求先切到 `s3.yydd.ai`
- 不要求先改上传链路
这次优先要拿到的是“真实读流量信息”,不是让业务先大改。
## 5. 对正常使用有没有影响
正常情况下,**对用户是无感的**。
前提是满足下面两个条件:
- 对外主资源域名统一使用 `https://s3.jiansun.vip`
- 资源尽量使用“新内容新 key”的方式,不要反复覆盖同一个 URL
只要做到这两点,缓存的效果就是:
- 第一次访问走回源
- 后续重复访问直接命中入口缓存
- 用户看到的还是同一个公网 URL
## 6. 会不会出现“同一张图两个 URL”
### 情况 1:缓存本身
**不会。**
缓存不会凭空再生成一个给用户使用的新 URL。
用户访问的仍然是原来的公网地址,比如:
```text
https://s3.jiansun.vip/xxx.png
```
缓存只是入口服务器内部把这个 URL 对应的内容存到磁盘里。
也就是说:
- 对用户看到的 URL 不变
- 对程序调用的 URL 不变
- 只是入口机内部多了一份缓存文件
### 情况 2:两个域名同时都在用
**会。**
如果下面两个地址同时长期对外使用:
- `https://s3.jiansun.vip/xxx.png`
- `https://s3.yydd.ai/xxx.png`
那么从“公网 URL”角度看,就是两条 URL 指向了同一份内容。
所以当前建议非常明确:
- `s3.jiansun.vip` 主用
- `s3.yydd.ai` 备用
不要两个一起作为默认入口使用。
## 7. 会不会出现“缓存里一张图,桶里又是另一个 URL”
从用户角度看,**不会多出一个可见 URL**。
但有一个需要特别注意的现实问题:
### 如果同一个 URL 被反复覆盖内容
那么就可能出现:
- 桶里已经是新图
- 缓存里短时间还是旧图
这时候不是“两个 URL”,而是:
- **同一个 URL,在不同时间点可能先返回旧内容,再返回新内容**
这也是为什么我强烈建议:
- 新图使用新 key
- 不要反复覆盖同一个对象地址
如果业务上确实必须覆盖同一个 URL,那就必须额外做:
- 清缓存机制
- 或单独降低这类路径的缓存时间
## 8. 推荐的最终口径
建议 AI 生图平台后续按下面这套规则执行:
### 域名规则
- 主用资源域名:`https://s3.jiansun.vip`
- 备用资源域名:`https://s3.yydd.ai`
### URL 规则
- 对外只认主域名,不混用两个资源域名
- 新内容尽量使用新 key,不覆盖老 key
### 缓存规则
- 把 AI 生图平台的热点读流量优先收敛到 `s3.jiansun.vip`
- 入口机 `180G` 只做读缓存,不改桶本体位置
## 9. 现在就可以推进的下一步
如果 AI 生图平台负责人能补齐下面 4 项,我就可以继续往下收口:
1. 确认主用资源域名是否统一为 `s3.jiansun.vip`
2. 提供 AI 生图平台真实会读取的路径前缀或 URL 样例
3. 明确是否存在“同 key 覆盖”
4. 提供一组真实联调 URL 用于灰度测试
拿到这些信息后,我这边会继续做:
1. 按真实热点路径确认是否需要“分路径缓存策略”
2. 区分哪些资源适合长缓存,哪些资源要短缓存
3. 如果存在“同 key 覆盖”,评估是否需要补清缓存机制
4. 用真实 URL 做灰度验证,再决定是否正式放量
## 10. 建议负责人直接按下面模板回复
为了节省来回沟通时间,建议 AI 生图平台负责人直接按这个格式回:
```text
1. 主用资源域名:
例如:统一使用 https://s3.jiansun.vip
2. 是否保留备用域名:
例如:保留 https://s3.yydd.ai,仅用于备用,不作为默认入口
3. 主要读取的资源类型:
例如:生图结果图、参考图、放大图、变体图、压缩包
4. 主要路径前缀或 URL 样例:
例如:/gongxiangpan/ai-output/... 或 /gongxiangpan/ai-input/...
5. 是否存在同 key 覆盖:
例如:有 / 没有
6. 时效要求:
例如:更新后允许 5 分钟内旧图;某些路径要求秒级生效
7. 流量规模:
例如:高峰每分钟约多少次读取,热门图片是否会被重复访问
8. 联调样本:
例如:提供 5 到 10 个真实可访问 URL
```
---
如果只要一句最短版本,可以直接转给对方:
**这次不是迁桶,只是在 `180.97.215.57` 上给 `s3.jiansun.vip` 做 180G 读缓存。要继续推进,需要 AI 生图平台确认主用域名、真实热点路径、是否会覆盖同一 URL、以及给一组联调样本。只要统一用一个主域名,并尽量做到新内容新 key,缓存对用户基本无感,也不会凭空多出第二个 URL。**

View File

@ -0,0 +1,247 @@
# AI 生图平台白底图接口文档
> 用途:AI 生图平台输入 `SKU` 后,直接从素材库获取该 `SKU` 对应的白底图。
> 当前主域名:`https://sc.jiansun.vip`
> 旧域名:`https://3d.yydd.ai`,保留兼容并跳转到新域名
## 1. 基础信息
- **接口基地址**:`https://sc.jiansun.vip/api/external/`
- **鉴权方式**:请求头 `X-API-Key`
- **返回格式**:`application/json`
- **适用范围**:仅适用于 `标准模型` 类型素材
## 2. 接口说明
### 2.1 按 SKU 查询白底图
**请求**
```http
GET /api/external/standard-model-white-background?sku={SKU}
X-API-Key: 5073534d7a59babbc45397a8eac5943b
```
**请求参数**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `sku` | string | 是 | SKU 名称。接口按大小写不敏感查询。 |
**请求头**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `X-API-Key` | string | 是 | 外部接口访问密钥 |
## 3. 成功响应
**响应示例**
```json
{
"success": true,
"data": {
"sku": "text001452",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"productModel": "TEXT0014",
"whiteBackground": {
"fixed": [
{
"key": "leftFront45",
"label": "左前45°",
"fileName": "左前45°.png",
"fileSize": 68,
"downloadUrl": "https://3d.yydd.ai/api/external/download?token=xxx"
},
{
"key": "rightFront45",
"label": "右前45°",
"fileName": "右前45°.png",
"fileSize": 68,
"downloadUrl": "https://3d.yydd.ai/api/external/download?token=xxx"
}
],
"other": [
{
"key": "other",
"label": "其他图片",
"fileName": "01-other-1.png",
"fileSize": 68,
"downloadUrl": "https://3d.yydd.ai/api/external/download?token=xxx"
}
]
},
"downloadUrls": {
"zip": "https://3d.yydd.ai/api/external/download?token=xxx"
}
}
}
```
### 3.1 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.sku` | string | 实际命中的 SKU 名称 |
| `data.materialId` | string | 素材 ID |
| `data.productModel` | string | 所属 SPU / 型号 |
| `data.whiteBackgroundUploaded` | boolean | 是否已有白底图 |
| `data.whiteBackgroundComplete` | boolean | 固定 8 张是否已补齐 |
| `data.whiteBackgroundFixedCount` | number | 当前已上传的固定角度图数量 |
| `data.whiteBackgroundOtherCount` | number | 当前已上传的 other 图片数量 |
| `data.whiteBackgroundCount` | number | 当前白底图总数 |
| `data.missingViews` | array | 当前仍缺少的固定角度图 |
| `data.whiteBackground.fixed` | array | 当前已有的固定角度白底图,按固定顺序返回已存在项 |
| `data.whiteBackground.other` | array | 其他图片,可为空数组 |
| `data.downloadUrls.zip` | string | 当前 SKU 白底图整包 ZIP 下载链接 |
### 3.2 `fixed` 固定图顺序
固定图返回顺序固定为:
1. `leftFront45`:左前45°
2. `rightFront45`:右前45°
3. `leftBack45`:左后45°
4. `rightBack45`:右后45°
5. `front`:正视图
6. `back`:后视图
7. `left`:左视图
8. `right`:右视图
### 3.3 单张图片对象字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `key` | string | 固定图为固定角度 key;其他图固定为 `other` |
| `label` | string | 图片说明 |
| `fileName` | string | 文件名 |
| `fileSize` | number | 文件大小,单位字节 |
| `downloadUrl` | string | 单张图片下载链接 |
## 4. 下载方式
### 4.1 下载单张白底图
直接请求响应里的 `downloadUrl` 即可:
```http
GET https://3d.yydd.ai/api/external/download?token=xxx
```
返回内容为对应图片文件,例如:
- `image/png`
- `image/jpeg`
### 4.2 下载整包白底图 ZIP
直接请求:
```http
GET https://3d.yydd.ai/api/external/download?token=xxx
```
当 token 对应的是白底图 ZIP 时,返回:
- `Content-Type: application/zip`
- 文件名:`{SKU}_白底图.zip`
## 5. 错误响应
### 5.1 SKU 不存在
```json
{
"success": false,
"error": {
"code": "SKU_NOT_FOUND",
"message": "未找到对应的标准模型 SKU"
}
}
```
### 5.2 SKU 数据重复
```json
{
"success": false,
"error": {
"code": "SKU_NOT_UNIQUE",
"message": "该 SKU 存在重复数据,暂无法对外提供白底图"
}
}
```
### 5.3 白底图不存在
```json
{
"success": false,
"error": {
"code": "WHITE_BACKGROUND_NOT_FOUND",
"message": "该 SKU 暂无白底图"
}
}
```
### 5.4 API Key 无效
```json
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "API Key 无效或缺失"
}
}
```
## 6. 接入建议
- **推荐主流程**:先查接口 → 拿到 `fixed` / `other` → 按需下载单张图
- 如果业务需要一次性拉全量,也可以直接使用 `downloadUrls.zip`
- `sku` 查询支持大小写不敏感,但建议调用方统一按大写传参,便于排查
- 接口不再要求固定 8 张必须补齐;只要该 SKU 已有任意白底图,就会返回当前已有图片
- 下载链接为带 token 的受控链接,建议由服务端调用,不建议直接暴露到前端页面
## 7. cURL 示例
### 7.1 查询白底图
```bash
curl -X GET \
"https://3d.yydd.ai/api/external/standard-model-white-background?sku=TEXT001452" \
-H "X-API-Key: 5073534d7a59babbc45397a8eac5943b"
```
### 7.2 下载单张图片
```bash
curl -L \
"https://3d.yydd.ai/api/external/download?token=xxx" \
-o left-front-45.png
```
### 7.3 下载整包 ZIP
```bash
curl -L \
"https://3d.yydd.ai/api/external/download?token=xxx" \
-o TEXT001452_白底图.zip
```
## 8. 当前联调结论
已验证:
- 旧任务平台接口 `GET /api/external/product-assets` 正常
- 旧下载接口 `GET /api/external/download?token=...` 正常
- 新白底图接口 `GET /api/external/standard-model-white-background` 正常
- 新白底图单图下载正常
如果后续还要继续开放更多素材接口,建议继续复用当前这套:
- `X-API-Key`
- `/api/external/*`
- token 下载链接

504
SPU详情接口文档.md Normal file
View File

@ -0,0 +1,504 @@
# AI 生图平台素材接口文档
> 用途:AI 生图平台统一通过同一条接口,查询 `标准模型白底图` 或 `任务-详情整套预览下载链接`。
> 当前主域名:`https://sc.jiansun.vip`
> 当前资源公网域名:`https://s3.jiansun.vip`
> 旧应用域名:`https://3d.yydd.ai`(仅保留兼容跳转)
## 1. 基础信息
- 接口基地址:`https://sc.jiansun.vip/api/external/`
- 鉴权方式:请求头 `X-API-Key`
- 请求方式:`GET`
- 返回格式:`application/json`
- 主接口路径:`/api/external/standard-model-white-background`
- 默认资源类型:`white-background`
返回统一格式:
```json
{
"success": true,
"data": {}
}
```
失败时:
```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "错误说明"
}
}
```
## 2. 通用请求规则
### 2.1 请求头
| 参数 | 必填 | 说明 |
|---|---|---|
| `X-API-Key` | 是 | 外部接口访问密钥 |
### 2.2 查询参数
| 参数 | 必填 | 说明 |
|---|---|---|
| `resourceType` | 否 | 资源类型。可选 `white-background` 或 `task-detail`。不传时默认 `white-background` |
| `sku` | 条件必填 | `sku` 和 `spu` 二选一即可 |
| `spu` | 条件必填 | `sku` 和 `spu` 二选一即可 |
补充说明:
- `sku`、`spu` 都按大小写不敏感查询,建议调用方统一传大写。
- 如果 `sku` 和 `spu` 同时传入,接口优先按 `spu` 处理。
- `shopName` 当前与 `categoryName` 含义一致,都是“店铺”。
## 3. 白底图查询
### 3.1 适用场景
- `resourceType=white-background`
- 不传 `resourceType` 时也走这一分支,兼容老调用方
- 支持两种查询方式:
- `sku`:直接拿到该 SKU 的完整白底图信息
- `spu`:先拿到该 SPU 下全部 SKU 清单,再让用户选 SKU
---
### 3.2 按 SKU 查询白底图
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=white-background&sku={SKU}
X-API-Key: your-api-key
```
兼容老调用方式:
```http
GET /api/external/standard-model-white-background?sku={SKU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "white-background",
"sku": "TEXT001452",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"productModel": "TEXT0014",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"whiteBackgroundUploaded": true,
"whiteBackgroundComplete": false,
"whiteBackgroundFixedCount": 2,
"whiteBackgroundOtherCount": 1,
"whiteBackgroundCount": 3,
"missingViews": [
{
"key": "leftBack45",
"label": "左后45°"
}
],
"whiteBackground": {
"fixed": [
{
"key": "leftFront45",
"label": "左前45°",
"fileName": "左前45°.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx"
},
{
"key": "rightFront45",
"label": "右前45°",
"fileName": "右前45°.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=yyy"
}
],
"other": [
{
"key": "other-1",
"label": "其他图片1",
"fileName": "detail-1.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=zzz"
}
]
},
"downloadUrls": {
"zip": "https://sc.jiansun.vip/api/external/download?token=zip-token"
}
}
}
```
**字段说明**
| 字段 | 说明 |
|---|---|
| `resourceType` | 固定为 `white-background` |
| `sku` | 实际命中的 SKU |
| `materialId` | 标准模型素材 ID |
| `productModel` | 所属 SPU |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `whiteBackgroundUploaded` | 是否已有白底图 |
| `whiteBackgroundComplete` | 固定 8 个角度是否已补齐 |
| `whiteBackgroundFixedCount` | 已有固定角度图数量 |
| `whiteBackgroundOtherCount` | 已有 other 图数量 |
| `whiteBackgroundCount` | 白底图总数 |
| `missingViews` | 还缺少的固定角度图 |
| `whiteBackground.fixed` | 固定角度白底图列表 |
| `whiteBackground.other` | 其他白底图列表 |
| `downloadUrls.zip` | 当前 SKU 白底图 ZIP 下载链接 |
固定角度顺序固定为:
1. `leftFront45`
2. `rightFront45`
3. `leftBack45`
4. `rightBack45`
5. `front`
6. `back`
7. `left`
8. `right`
单张图片对象字段:
| 字段 | 说明 |
|---|---|
| `key` | 固定图为固定角度 key,other 图为 `other-*` |
| `label` | 图片说明 |
| `fileName` | 文件名 |
| `fileSize` | 文件大小,单位字节 |
| `downloadUrl` | 单张白底图下载链接 |
---
### 3.3 按 SPU 查询白底图 SKU 清单
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=white-background&spu={SPU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "white-background",
"spu": "TEXT0014",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"skuCount": 2,
"skuList": [
{
"skuName": "TEXT001452",
"skuId": "sku-id-1",
"hasWhiteBackground": true,
"whiteBackgroundCount": 2,
"previewImage": {
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx",
"fileName": "右前45°.png",
"fileSize": 123456
}
},
{
"skuName": "TEXT001453",
"skuId": "sku-id-2",
"hasWhiteBackground": false,
"whiteBackgroundCount": 0,
"previewImage": null
}
]
}
}
```
**字段说明**
| 字段 | 说明 |
|---|---|
| `resourceType` | 固定为 `white-background` |
| `spu` | 实际命中的 SPU |
| `materialId` | 标准模型素材 ID |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `skuCount` | 该 SPU 下 SKU 数量 |
| `skuList[].skuName` | SKU 名称 |
| `skuList[].skuId` | SKU 记录 ID |
| `skuList[].hasWhiteBackground` | 是否已有任意白底图 |
| `skuList[].whiteBackgroundCount` | 当前固定角度白底图数量 |
| `skuList[].previewImage` | 右前 45° 预览图;如果没有该角度则为 `null` |
说明:
- 这个接口主要给 AI 平台先展示 SKU 列表用。
- 选定 SKU 后,再调用“按 SKU 查询白底图”接口拿完整白底图信息。
## 4. 任务-详情整套查询
### 4.1 适用场景
- `resourceType=task-detail`
- 只返回:
- `materialType='任务-详情'`
- `isFullSet=true`
- `deletedAt=null`
的候选素材
- 返回的是**预览图直链数组**,不是 zip,也不是 token 下载链接
- 返回顺序按 `uploadedAt` 倒序,也就是最新上传的排前面
---
### 4.2 按 SPU 查询任务-详情整套候选
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=task-detail&spu={SPU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "task-detail",
"spu": "SPU001",
"sku": null,
"categoryName": "旗舰店",
"shopName": "旗舰店",
"candidateCount": 2,
"candidates": [
{
"materialId": "material-2",
"taskNo": "TASK202601141713525624",
"productModel": "SPU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"sceneName": "客厅",
"uploadedAt": "2026-03-18T10:00:00.000Z",
"isFullSet": true,
"downloadUrls": {
"main": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg"
],
"sku": [
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg"
],
"detail": [
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
],
"all": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
]
}
}
]
}
}
```
---
### 4.3 按 SKU 查询任务-详情整套候选
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=task-detail&sku={SKU}
X-API-Key: your-api-key
```
**处理逻辑**
- 先通过 `SKU -> 标准模型 SPU`
- 再按这个 SPU 查询所有整套 `任务-详情` 候选
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "task-detail",
"spu": "SPU001",
"sku": "SKU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"candidateCount": 1,
"candidates": [
{
"materialId": "material-2",
"taskNo": "TASK202601141713525624",
"productModel": "SPU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"sceneName": "客厅",
"uploadedAt": "2026-03-18T10:00:00.000Z",
"isFullSet": true,
"downloadUrls": {
"main": [],
"sku": [],
"detail": [],
"all": []
}
}
]
}
}
```
**候选素材字段说明**
| 字段 | 说明 |
|---|---|
| `materialId` | 任务-详情素材 ID |
| `taskNo` | 任务号 |
| `productModel` | 对应 SPU |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `sceneName` | 场景 |
| `uploadedAt` | 上传时间,ISO 8601 格式 |
| `isFullSet` | 是否整套,返回结果中固定为 `true` |
| `downloadUrls.main` | 主图预览直链数组 |
| `downloadUrls.sku` | SKU 图预览直链数组 |
| `downloadUrls.detail` | 详情图预览直链数组 |
| `downloadUrls.all` | `main + sku + detail` 合并后的数组 |
说明:
- `task-detail` 分支返回的是**全部候选**,不是只返回一条。
- 预览链接可以直接打开或下载。
- 历史 `任务-详情` 如果没有标记 `isFullSet=true`,不会被返回。
## 5. 下载说明
### 5.1 白底图
白底图返回的是下载链接:
- 单张图:`whiteBackground.fixed[*].downloadUrl` / `whiteBackground.other[*].downloadUrl`
- 整包 ZIP:`downloadUrls.zip`
调用方式:
```http
GET https://sc.jiansun.vip/api/external/download?token=xxx
```
### 5.2 任务-详情整套
任务-详情返回的是可直接访问的预览图直链数组:
- `downloadUrls.main`
- `downloadUrls.sku`
- `downloadUrls.detail`
- `downloadUrls.all`
这一分支**不返回 zip**,也**不需要再调下载 token 接口**。
## 6. 错误码
### 6.1 通用错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 401 | `INVALID_API_KEY` | API Key 无效或缺失 |
| 400 | `MISSING_PARAM` | 没有传 `sku` 或 `spu` |
| 400 | `RESOURCE_TYPE_INVALID` | `resourceType` 不是 `white-background` 或 `task-detail` |
| 500 | `SERVER_ERROR` | 服务器内部错误 |
### 6.2 白底图分支错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 404 | `SKU_NOT_FOUND` | 未找到对应的标准模型 SKU |
| 409 | `SKU_NOT_UNIQUE` | SKU 重复,暂无法对外提供白底图 |
| 404 | `SPU_NOT_FOUND` | 未找到对应的标准模型 SPU |
| 404 | `NO_SKUS` | 该 SPU 下没有 SKU 数据 |
| 404 | `WHITE_BACKGROUND_NOT_FOUND` | 该 SKU 暂无白底图 |
### 6.3 任务-详情分支错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 404 | `SPU_NOT_FOUND` | 未找到对应的标准模型 SPU |
| 404 | `SKU_NOT_FOUND` | 未找到对应的标准模型 SKU |
| 409 | `SKU_NOT_UNIQUE` | SKU 重复,暂无法对外提供任务-详情整套素材 |
| 404 | `TASK_DETAIL_NOT_FOUND` | 该 SPU 或该 SKU 所属 SPU 下暂无任务-详情素材 |
| 404 | `FULL_SET_NOT_FOUND` | 有任务-详情素材,但没有任何一条被标记为整套 |
## 7. cURL 示例
### 7.1 按 SKU 查询白底图
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?sku=TEXT001452" \
-H "X-API-Key: your-api-key"
```
### 7.2 按 SPU 查询白底图 SKU 清单
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=white-background&spu=TEXT0014" \
-H "X-API-Key: your-api-key"
```
### 7.3 按 SPU 查询任务-详情整套候选
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=task-detail&spu=SPU001" \
-H "X-API-Key: your-api-key"
```
### 7.4 按 SKU 查询任务-详情整套候选
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=task-detail&sku=SKU001" \
-H "X-API-Key: your-api-key"
```
## 8. 接入建议
- 老的白底图调用方可以不改,继续传 `sku` 即可。
- 如果是 AI 生图平台新流程,建议:
- 白底图:`spu -> sku -> 白底图详情`
- 任务-详情:直接按 `spu` 或 `sku` 查整套候选
- `task-detail` 返回多个候选时,建议调用方按 `uploadedAt` 再做一次前端展示排序或筛选。
- `task-detail` 使用的是预览图直链,不适合再按源文件 zip 的思路处理。
## 9. 当前联调状态
截至 `2026-03-18`,本地代码已完成并验证通过的内容:
- `/api/external/standard-model-white-background` 已支持 `white-background` 和 `task-detail`
- 不传 `resourceType` 时,白底图老调用方式保持兼容
- `task-detail` 已支持 `spu` / `sku` 两种查询
- 返回中已补充 `shopName`
- `任务-详情` 整套字段 `isFullSet` 已接入接口返回逻辑
- 本地构建 `npm run build` 已通过

View File

@ -1604,6 +1604,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
onOpenFileList={() => setPanelMode('fileList')}
selectedAgentPresetId={null}
chatScope="canvas"
agentEnabled={isAgentFeatureEnabled}
/>
</div>
) : (
@ -1654,22 +1655,24 @@ export default function CanvasEditorPage({ params }: PageProps) {
<TooltipContent side="top">平移 (H)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={isAgentMode ? "secondary" : "ghost"}
size="icon"
onClick={handleToggleAgentMode}
className={cn(
"h-9 w-9 rounded-xl",
isAgentMode && "bg-primary text-primary-foreground hover:bg-primary/90"
)}
>
<Wand2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">智能体模式</TooltipContent>
</Tooltip>
{isAgentFeatureEnabled ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={isAgentMode ? "secondary" : "ghost"}
size="icon"
onClick={handleToggleAgentMode}
className={cn(
"h-9 w-9 rounded-xl",
isAgentMode && "bg-primary text-primary-foreground hover:bg-primary/90"
)}
>
<Wand2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="top">智能体模式</TooltipContent>
</Tooltip>
) : null}
<Tooltip>
<TooltipTrigger asChild>

View File

@ -34,7 +34,11 @@ import {
} from "./ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export function AppSidebar({ user }: { user: User | undefined }) {
export function AppSidebar({
user,
}: {
user: User | undefined;
}) {
const router = useRouter();
const { setOpenMobile } = useSidebar();
const { mutate } = useSWRConfig();
@ -64,7 +68,7 @@ export function AppSidebar({ user }: { user: User | undefined }) {
<SidebarMenu>
<div className="flex flex-row items-center justify-between w-full">
{/* Logo removed */}
<div className="flex-1" />
<div className="flex-1" />
<div className="flex flex-row gap-1">
{user && (
<Tooltip>

View File

@ -120,6 +120,7 @@ interface CanvasChatPanelProps {
chatScope?: "canvas" | "agent";
workflowContext?: AgentWorkflowComposerContext | null;
presentation?: "panel" | "workspace";
agentEnabled?: boolean;
}
type VideoStoryboardStreamData =
@ -1706,12 +1707,13 @@ export function CanvasChatPanel({
chatScope = "canvas",
workflowContext = null,
presentation = "panel",
agentEnabled = true,
}: CanvasChatPanelProps) {
const isWorkspacePresentation = presentation === "workspace";
const { data: session } = useSession();
const [input, setInput] = useState("");
const [attachments, setAttachments] = useState<PickedObjectAttachment[]>([]);
const [selectedModelId, setSelectedModelId] = useState<string>("auto");
const [selectedModelId, setSelectedModelId] = useState<string>(agentEnabled ? "auto" : "");
const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false);
const [detailGenerationMode, setDetailGenerationMode] = useState<DetailGenerationMode>("classic");
const [isDetailGenerating, setIsDetailGenerating] = useState(false);
@ -2901,6 +2903,7 @@ export function CanvasChatPanel({
sortOrder: -999,
providerType: "auto",
supportsImageGen: true,
capabilities: null,
isMultimodal: false,
videoToolCapability: null,
imageGenConfig: modelsData?.agentModel?.imageGenConfig || undefined,
@ -2919,6 +2922,7 @@ export function CanvasChatPanel({
providerType: modelsData.agentModel.providerType,
supportsVision: modelsData.agentModel.supportsVision,
supportsImageGen: modelsData.agentModel.supportsImageGen,
capabilities: null,
isMultimodal: true,
videoToolCapability: null,
imageGenConfig: modelsData.agentModel.imageGenConfig || undefined,
@ -2928,7 +2932,7 @@ export function CanvasChatPanel({
// 所有可选模型(Agent模式 + 可视化模型)
const allModels = useMemo(() => {
const merged = [
SMART_AUTO_MODEL,
...(agentEnabled ? [SMART_AUTO_MODEL] : []),
...(multimodalAgentModel ? [multimodalAgentModel] : []),
...visualModels,
];
@ -2940,7 +2944,7 @@ export function CanvasChatPanel({
seen.add(model.id);
return true;
});
}, [SMART_AUTO_MODEL, multimodalAgentModel, visualModels]);
}, [SMART_AUTO_MODEL, agentEnabled, multimodalAgentModel, visualModels]);
const imageOnlyModels = useMemo(
() =>
visualModels.filter(
@ -3040,6 +3044,7 @@ export function CanvasChatPanel({
currentModel && isCanvasConversationOnlyModel(currentModel)
);
const isAgentModel =
agentEnabled &&
!isDetailGenerationMode &&
(!selectedModelId || selectedModelId === "auto" || isConversationModel);
const computedComposerMode = useMemo<"agent" | "image" | "video" | "detail">(() => {
@ -3052,8 +3057,8 @@ export function CanvasChatPanel({
"agent" | "image" | "video" | "detail" | null
>(null);
const agentSelectableModels = useMemo(
() => [SMART_AUTO_MODEL, ...conversationOnlyModels],
[SMART_AUTO_MODEL, conversationOnlyModels]
() => (agentEnabled ? [SMART_AUTO_MODEL, ...conversationOnlyModels] : conversationOnlyModels),
[SMART_AUTO_MODEL, agentEnabled, conversationOnlyModels]
);
const selectableModels = useMemo(() => {
if (isDetailGenerationMode) {
@ -3339,10 +3344,31 @@ export function CanvasChatPanel({
useEffect(() => {
if (!selectedModelId && siteSettings) {
currentModelIdRef.current = "auto";
setSelectedModelId("auto");
const nextModelId = agentEnabled ? "auto" : firstImageModelId || firstVideoModelId || "";
currentModelIdRef.current = nextModelId;
setSelectedModelId(nextModelId);
}
}, [selectedModelId, siteSettings]);
}, [agentEnabled, firstImageModelId, firstVideoModelId, selectedModelId, siteSettings]);
useEffect(() => {
if (agentEnabled) return;
if (selectedModelId !== "auto" && activeComposerMode !== "agent") return;
const fallbackModelId = firstImageModelId || firstVideoModelId || "";
currentModelIdRef.current = fallbackModelId;
setSelectedModelId(fallbackModelId);
setComposerModeOverride(firstVideoModelId && fallbackModelId === firstVideoModelId ? "video" : "image");
if (isDetailGenerationMode) {
setIsDetailGenerationMode(false);
}
}, [
activeComposerMode,
agentEnabled,
firstImageModelId,
firstVideoModelId,
isDetailGenerationMode,
selectedModelId,
]);
useEffect(() => {
if (!aiDetailGenerationEnabled && isDetailGenerationMode) {
@ -3357,6 +3383,7 @@ export function CanvasChatPanel({
}, [modelSelectValue, selectedModelId]);
useEffect(() => {
if (!agentEnabled) return;
if (activeComposerMode !== "agent") return;
if (!selectedAgentPresetId) return;
if (!selectedAgentPreset?.defaultModelId) return;
@ -3557,6 +3584,9 @@ export function CanvasChatPanel({
setComposerModeOverride(mode);
if (mode === "agent") {
if (!agentEnabled) {
return;
}
const preferredAgentModelId =
allModels.find(
(model) =>
@ -3574,8 +3604,9 @@ export function CanvasChatPanel({
toast.error("AI详情生成功能未开启");
return;
}
currentModelIdRef.current = "auto";
setSelectedModelId("auto");
const detailModelId = agentEnabled ? "auto" : "";
currentModelIdRef.current = detailModelId;
setSelectedModelId(detailModelId);
setIsDetailGenerationMode(true);
setIsDetailWorkbenchCollapsed(true);
return;
@ -3615,6 +3646,7 @@ export function CanvasChatPanel({
},
[
aiDetailGenerationEnabled,
agentEnabled,
allModels,
firstImageModelId,
firstVideoModelId,
@ -9942,17 +9974,19 @@ export function CanvasChatPanel({
sideOffset={10}
className="w-[156px] rounded-xl border border-[#d6d9df] bg-[#f4f5f7]/90 p-1.5 shadow-[0_8px_18px_rgba(15,23,42,0.14)]"
>
<DropdownMenuItem
onClick={() => handleSelectGenerationMode("agent")}
className={cn(
"h-8 cursor-pointer rounded-lg px-2 text-[13px] font-medium text-[#2e3238]",
activeComposerMode === "agent" ? "bg-white/80" : "hover:bg-white/70"
)}
>
<Sparkles className="h-4 w-4" />
<span className="flex-1">Agent</span>
{activeComposerMode === "agent" ? <Check className="h-4 w-4 text-[#3b82f6]" /> : null}
</DropdownMenuItem>
{agentEnabled ? (
<DropdownMenuItem
onClick={() => handleSelectGenerationMode("agent")}
className={cn(
"h-8 cursor-pointer rounded-lg px-2 text-[13px] font-medium text-[#2e3238]",
activeComposerMode === "agent" ? "bg-white/80" : "hover:bg-white/70"
)}
>
<Sparkles className="h-4 w-4" />
<span className="flex-1">Agent</span>
{activeComposerMode === "agent" ? <Check className="h-4 w-4 text-[#3b82f6]" /> : null}
</DropdownMenuItem>
) : null}
{aiDetailGenerationEnabled ? (
<DropdownMenuItem
onClick={() => handleSelectGenerationMode("detail")}

View File

@ -0,0 +1,514 @@
# AI生图平台桶缓存落地对接答复(基于本项目现状)
## 1. 文档目的
本文档用于回应本地清单 [AI生图平台桶缓存落地对接清单.md](/Users/lanmu/Desktop/ai-chatbot-main/AI生图平台桶缓存落地对接清单.md),并结合当前项目的真实实现,给出一份可以直接用于和基础设施、资源域名、桶存储负责人对齐的项目侧答复。
本文只描述“当前项目已实现的事实”和“基于这些事实的建议”,不把尚未上线的理想方案写成现状。
---
## 2. 一句话结论
**对本项目来说,只要 `https://s3.jiansun.vip` 继续作为统一对外资源主域名,并且入口服务器上的 180G 磁盘缓存只做“读缓存”而不改上传链路,那么项目应用层基本不需要改代码,就可以直接受益。**
原因很简单:
- 项目当前所有图片类资源最终都走统一存储服务上传
- 对外 URL 由存储配置里的 `publicUrl` 决定,而不是写死在业务代码里
- 项目默认就是“新内容新 key”,天然适合读缓存
- 当前没有依赖“对象覆盖后立刻强制失效”的核心链路
---
## 3. 当前项目真实存储链路
## 3.1 上传入口
项目中的图片上传与多数生图结果回存,都会走统一存储服务:
- 核心实现:`lib/storage/service.ts`
- 聊天上传接口:`app/(chat)/api/files/upload/route.ts`
统一存储服务的优先级如下:
1. 数据库中配置的默认 S3 兼容存储
2. 备用桶故障切换配置
3. 环境变量 S3
4. Vercel Blob 兜底
也就是说,项目本身已经是“多存储后端兼容”的。
## 3.2 对外 URL 生成方式
当前项目上传成功后,对外图片 URL 的生成规则是:
- 如果存储配置里设置了 `publicUrl`
- 返回:`publicUrl + /uploads/...`
- 如果没有设置 `publicUrl`
- 返回:`endpoint + /bucket + /uploads/...`
因此,对项目来说真正关键的不是“桶后面用不用缓存”,而是:
- 当前默认存储配置中的 `publicUrl` 是否统一指向 `https://s3.jiansun.vip`
如果是,那么项目对外发给前端、消息、相册、画布、详情页的资源 URL,就会自然统一使用这个域名。
## 3.3 当前资源 key 规则
项目统一上传时,文件 key 采用:
```text
uploads/{timestamp}-{random}.{ext}
```
特点:
- 路径前缀稳定:`/uploads/`
- 文件名默认唯一
- 默认不会覆盖旧对象
这对读缓存非常友好,因为:
- 同一张新图天然是新 URL
- 缓存不会因为“同 URL 内容被覆盖”而频繁失真
---
## 4. 本项目实际会被缓存命中的资源范围
基于当前代码,最适合接入入口机磁盘读缓存的资源,主要是这些:
## 4.1 用户上传图片
来源:
- 聊天区上传
- 画布素材上传
- 局部编辑、修图、扩图等中间结果回存
特点:
- 统一进 `uploads/...`
- 后续可能被多轮编辑、对话历史、画布再次引用
## 4.2 AI 生成结果图
来源:
- 聊天生图
- 编辑生成
- 扩图、增强、去水印、角度编辑等图像工作流
落库位置:
- `generatedImage.originalUrl`
- `generatedImage.thumbnailUrl`
这些 URL 会被以下场景重复读取:
- 聊天消息回放
- 相册页 `/gallery`
- 精选页
- 管理后台图片记录
- 无限画布节点
## 4.3 详情页生成相关图片
来源:
- AI详情主图
- AI详情追加评价晒图
- 详情历史归档封面图
特点:
- 一轮生成后可能被画布、存档、后续再次编辑反复读取
- 对重复访问更敏感,适合缓存
## 4.4 工作流中的中间结果图
如:
- 去背景结果
- 增强图
- 扩图结果
- 角度编辑结果
- 场景融合图
特点:
- 虽然是中间态,但在会话里会被持续回读
- 如果用户反复对比和继续编辑,也会产生热点访问
---
## 5. 对清单中关键问题的项目侧答复
## 5.1 关于“需要确认的资源范围”
基于项目实际情况,可以先明确:
### 当前最值得纳入缓存命中的路径前缀
首选:
- `/uploads/`
原因:
- 当前统一上传服务默认就写入这个前缀
- 这是用户上传图、生成图、处理结果图最稳定的公共前缀
### 当前最可能成为热点的资源类型
- 会话中反复引用的参考图
- 相册中的生成结果图
- AI详情主图与评价晒图
- 画布中的历史结果图
- 编辑工作流中多轮引用的中间结果图
### 项目侧当前无法直接从代码给出的内容
项目代码里目前**没有现成的“近 3 天 Top20 资源 URL 统计”接口**。
所以如果基础设施侧需要:
- 近 3 天最常访问的 20 个资源 URL
- 主要热点路径前缀
建议直接从:
- Nginx access log
- 对象存储访问日志
- 资源域名层日志
做统计,而不是从业务数据库里推。
## 5.2 关于“是否统一只使用一个主资源域名”
### 项目建议结论
建议统一:
- 主用:`https://s3.jiansun.vip`
- 备用:`https://s3.yydd.ai`
并且在项目的默认存储配置里:
- `publicUrl` 只配置成 `https://s3.jiansun.vip`
### 为什么对本项目很重要
因为当前项目会把图片 URL 持久化到很多地方:
- `generatedImage`
- 消息内容 markdown
- media asset
- 画布数据
- AI详情归档
如果两个公网域名都长期作为默认资源入口使用,就会出现:
- 同一份资源在数据库中可能留下两套公网 URL
- 前端、相册、对话、画布里都可能混用不同域名
这会带来:
- 缓存命中分散
- 数据治理复杂
- 后续域名切换和清理成本变高
### 项目侧结论
对当前项目来说:
- 可以保留 `https://s3.yydd.ai` 作为基础设施备用
- 但**不建议**把它配置成业务默认返回的资源 URL
## 5.3 关于“是否同 key 覆盖”
### 当前项目默认行为
**默认不是同 key 覆盖,而是新内容新 key。**
这是当前项目的真实实现。
统一上传时生成的路径形式为:
```text
uploads/{timestamp}-{random}.{ext}
```
### 这意味着什么
对于缓存层来说,这其实是最理想的模式:
- 新图 = 新 URL
- 不需要依赖强制 purge
- 缓存旧对象不会污染新对象
### 需要注意的例外
如果未来有人在对象存储层手工做:
- 同路径覆盖
- 同 key 重写
那么缓存仍可能返回旧内容。
但就当前项目默认代码路径而言,这不是主流行为。
## 5.4 关于“时效要求”
### 当前项目的大多数图片链路
当前项目大多数图片资源都适合“读缓存优先”:
- 新图默认新 URL
- 会话、相册、画布大多是读取已经生成好的资源
- 不依赖“同 URL 秒级替换内容”
### 当前少数需要特别注意的情况
1. 如果基础设施层后续做了“同 URL 覆盖”
- 会有缓存短时返回旧图的风险
2. 如果资源域名切换
- 旧 URL 已经持久化在数据库和消息里,不能只改 DNS 口径,还要考虑历史数据
### 项目侧建议
只要遵守:
- 新内容新 key
- 主资源域名单一化
那么当前项目对缓存一致性的压力并不高。
## 5.5 关于“流量规模”
### 项目代码层能确定的事实
当前项目存在大量图片重复读取场景:
- 聊天消息回放
- 相册分页
- 画布节点回显
- AI详情历史存档
- 后续编辑再次读取旧图
### 项目代码层目前不能直接给出的事实
项目中没有现成的资源访问量级面板来回答:
- 峰值并发
- 每分钟请求数
- 热点 URL 排行
- 资源命中曲线
这类数据仍建议由入口网关、Nginx、对象存储日志侧统计。
### 项目侧判断
从业务形态上看,本项目非常适合读缓存,因为它是明显的“生成后被多次回看、回显、继续编辑”的产品,不是“一次下载后永不再读”的冷资源型系统。
## 5.6 关于“联调样本”
项目侧建议联调用下面 3 类真实资源:
1. 用户上传原图
- 路径特征:`/uploads/...`
- 用于验证首次回源、二次命中
2. AI 生成结果图
- 包括 `originalUrl` 和 `thumbnailUrl`
- 用于验证相册、会话回放、画布回显的缓存效果
3. AI详情主图或晒单图
- 文件通常更大、读取更频繁
- 用于验证长图场景的稳定性
项目本身可以提供测试资源 URL,但如果要验证“真实热点价值”,仍建议从线上真实访问日志中抽样。
## 5.7 关于“这次不需要改的内容”
结合项目现状,这次确实**不需要先改**这些内容:
- 不需要改业务接口域名
- 不需要先迁桶
- 不需要改聊天上传接口
- 不需要改 AI 生图主流程
- 不需要改画布或相册读取逻辑
前提条件只有一个:
- 当前默认存储配置中的 `publicUrl` 继续统一返回主资源域名
---
## 6. 项目侧对“会不会有两个 URL”的明确答复
## 6.1 缓存本身不会制造第二个 URL
对本项目来说,只要缓存层放在:
- `https://s3.jiansun.vip`
前面,且仍返回同一个公网地址,那么:
- 会话里看到的 URL 不变
- 相册里拿到的 URL 不变
- 数据库里保存的 URL 不变
- 只是入口服务器内部多了一份磁盘缓存
所以:
**缓存本身不会让本项目出现第二个用户可见 URL。**
## 6.2 真正会导致“双 URL”的是双域名并用
如果业务层长期同时把下面两个域名都写进数据库或消息:
- `https://s3.jiansun.vip/...`
- `https://s3.yydd.ai/...`
那对本项目来说就真的是两套 URL 了。
这不仅影响缓存,也会影响:
- 相册聚合
- 画布历史复用
- 消息解析
- 存储迁移时的 URL 替换
因此项目侧建议非常明确:
- `s3.jiansun.vip` 作为唯一默认 `publicUrl`
- `s3.yydd.ai` 只做备用访问入口,不写入业务默认配置
---
## 7. 项目里已有的相关能力,基础设施侧可以放心利用
## 7.1 多存储后端兼容 `已具备`
当前项目支持:
- S3 / MinIO / 阿里云 OSS 风格配置
- 默认存储
- 备用桶故障迁移
## 7.2 阿里云 OSS 入口桶 + 延迟转存 `已具备`
当前项目已有:
- 上传优先进入阿里云 OSS 的配置
- 到期后转存到目标默认桶
- 转存后自动替换数据库中的 URL
- 到期后清理源对象
这说明项目本身已经有“入口桶”和“最终落地桶”的概念。
### 但要注意
这套 OSS 转存能力和你们现在要做的 **入口机读缓存** 不是一回事:
- OSS 转存:属于存储生命周期管理
- 入口机缓存:属于读路径加速
两者可以并存,而且不会冲突。
## 7.3 对外资源路径稳定 `已具备`
当前绝大多数图片都落在:
```text
/uploads/...
```
这很适合基础设施侧做:
- 路径维度的缓存策略
- 热点资源命中统计
- 单独限流和回源观测
## 7.4 Provider 读图适配 `已具备部分能力`
例如 Volcano Vision provider 中,已有把 `publicUrl` 形式的上传图 URL 重写成底层 endpoint/bucket 形式的逻辑,用于避免部分供应商抓取公开资源时失败。
这说明:
- 项目不是完全假设“所有供应商都直接从公网域名读图”
- 即便公网资源域名前面加了缓存,只要 `publicUrl` 配置一致,当前系统仍然能工作
---
## 8. 当前项目对缓存落地的建议
## 8.1 项目侧推荐落地方式
建议基础设施侧这样落:
1. 保持业务默认资源域名为 `https://s3.jiansun.vip`
2. 让 `s3.jiansun.vip` 前挂入口服务器读缓存
3. 缓存主要覆盖 `/uploads/` 路径
4. 不改上传链路,不改桶位置
5. 不在业务配置中默认使用 `https://s3.yydd.ai`
## 8.2 项目侧推荐前置检查项
在正式切量前,建议先确认:
1. 项目当前默认存储配置里的 `publicUrl` 是否为 `https://s3.jiansun.vip`
2. 历史数据库中是否已经混入了 `s3.yydd.ai` 资源 URL
3. Nginx 缓存是否正确返回命中头,如 `X-Cache-Status`
4. 是否只缓存 GET 读流量,不介入写入逻辑
5. 是否对超大文件和极低频资源设置合理淘汰策略
## 8.3 项目侧不建议的做法
1. 不建议把两个公网资源域名同时作为业务默认入口
2. 不建议让业务层开始复用同一个对象 key 覆盖新图
3. 不建议把“入口读缓存”和“迁桶/改上传链路”绑定在同一个窗口一起做
---
## 9. 项目侧当前仍缺少的支持
虽然项目整体适配缓存没有问题,但如果后面要长期运营缓存层,当前项目还缺这些观测能力:
1. 没有按资源 URL 维度的访问统计
2. 没有热点资源排行榜
3. 没有区分“上传成功量”和“资源读取量”的统一面板
4. 没有面向缓存层的 purge / 失效控制接口
这些都不是这次缓存落地的阻塞项,但属于后续优化方向。
---
## 10. 最终答复口径
如果要给 AI 生图平台负责人一个简短的项目侧结论,可以直接用下面这段:
> 基于我们当前项目实现,入口服务器上的 180G 磁盘缓存可以直接作为 `https://s3.jiansun.vip` 的读缓存使用,项目业务层基本不需要改造。因为我们当前图片资源统一经存储服务上传,对外 URL 由存储配置中的 `publicUrl` 决定,且默认采用 `uploads/{timestamp}-{random}.{ext}` 这种新内容新 key 的方式,天然适合缓存命中。
>
> 对项目最关键的前提只有两个:
> 1. 业务默认资源域名统一使用 `https://s3.jiansun.vip`;
> 2. 不要把 `https://s3.yydd.ai` 同时作为默认业务资源域名写入数据库或消息。
>
> 只要保持“主域名单一化 + 新内容新 key”,当前这套读缓存方案对我们的聊天生图、画布、相册、AI详情等链路都是正向收益,而且不会改变现有上传链路和真实桶位置。
---
## 11. 相关代码位置
如需进一步联调,可重点查看:
- 统一上传服务:`lib/storage/service.ts`
- 上传接口:`app/(chat)/api/files/upload/route.ts`
- OSS 转存任务:`lib/jobs/oss-image-migration-job.ts`
- OSS 转存 API:`app/api/jobs/migrate-oss-images/route.ts`
- 相册读取:`app/api/gallery/route.ts`
- 聊天图片落库:`app/(chat)/api/chat/route.ts`
- Volcano 读图 URL 改写:`lib/ai/providers/volcano-vision.ts`

View File

@ -0,0 +1,362 @@
# AI生图平台桶缓存落地补充回复(基于近期日志与项目现状)
## 1. 说明
这份补充回复用于回应对方最新提出的 5 个信息项,信息来源分为两类:
- 项目当前代码与存储实现
- 最近 3 天业务侧可观测日志样本
需要特别说明:
- 下面的“热点路径 / URL 样例 / 请求量级”主要来自应用服务器的 Nginx 日志中,对 `/api/image-proxy?url=...` 这一类读图请求的观测样本
- 它可以反映当前业务真实在读哪些桶资源,但**不等于**资源域名 `s3.jiansun.vip` 的全量公网访问统计
如果后续要做最终缓存策略定稿,仍建议把资源域名入口侧的 Nginx / 对象存储访问日志也一起纳入。
---
## 2. 对最新 5 个问题的直接答复
## 2.1 主资源域名
### 我们项目侧的明确口径
建议后续默认统一使用:
- 主用:`https://s3.jiansun.vip`
- 备用 / 回切:`https://s3.yydd.ai`
也就是:
- `https://s3.yydd.ai` 不再作为日常默认资源域名写入业务数据库
- 仅保留为备用访问入口或异常切换入口
### 但当前线上现状需要说明
从近期应用侧 Nginx 日志样本看,**当前仍存在双域名并用现象**:
- `s3.jiansun.vip`:`129` 次业务读图样本
- `s3.yydd.ai`:`182` 次业务读图样本
这说明当前历史数据或部分读取链路里,`s3.yydd.ai` 还没有完全退出。
### 对缓存落地的建议
建议按下面方式收敛:
1. 从现在开始,业务默认只写入 `https://s3.jiansun.vip`
2. `https://s3.yydd.ai` 仅保留备用,不再作为默认返回 URL
3. 后续再分阶段清理历史数据库 / 消息 / 画布中遗留的 `s3.yydd.ai` 资源引用
---
## 2.2 真实热点路径
### 当前最明确的热点路径前缀
从项目代码与近期日志样本结合看,当前最明确的热点路径集中在:
```text
/gongxiangpan/uploads/
```
更具体地说,当前应用侧反复读取的资源普遍是:
```text
https://s3.jiansun.vip/gongxiangpan/uploads/{filename}
```
或历史残留:
```text
https://s3.yydd.ai/gongxiangpan/uploads/{filename}
```
### 为什么判断它是热点
因为项目当前所有这类资源会被反复用于:
- 聊天消息回放
- 相册页查看
- 无限画布节点回显
- AI详情生成后的详情图回读
- 图片编辑工作流中的二次引用
### 近期日志里观测到的热点对象样本
按“去掉尺寸参数后的基础对象 URL”聚合,近期样本里重复读取较多的对象包括:
1. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846370425-vkah9g.png`,样本中出现 `18` 次
2. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846444732-r8bzni.jpg`,样本中出现 `17` 次
3. `https://s3.jiansun.vip/gongxiangpan/uploads/1773847125480-d8y8sq.png`,样本中出现 `11` 次
4. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846185500-0rpjwm.png`,样本中出现 `10` 次
历史备用域名上的热点对象也能看到:
5. `https://s3.yydd.ai/gongxiangpan/uploads/1771897462521-toeu12.jpg`,样本中出现 `9` 次
6. `https://s3.yydd.ai/gongxiangpan/uploads/1771897607531-fm9w05.png`,样本中出现 `9` 次
### 当前业务实际读取形态
当前大量读取不是直接浏览器裸读对象 URL,而是经由:
```text
/api/image-proxy?url=https://s3.jiansun.vip/gongxiangpan/uploads/...
```
并附带不同渲染尺寸参数,例如:
- `w=64`
- `w=720`
- `w=756`
- `w=758`
- `w=960`
- `w=1000`
- 个别还有 `w=2200`
这意味着当前业务访问热点实际上有两层:
1. 原始对象路径热点:`/gongxiangpan/uploads/...`
2. 尺寸变体热点:同一对象会被不同宽度反复请求
---
## 2.3 联调样本 URL
下面给出一批近期日志中真实出现过的、可用于联调的对象 URL 样本。
### A. `s3.jiansun.vip` 主域名样本
1. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846370425-vkah9g.png`
2. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846444732-r8bzni.jpg`
3. `https://s3.jiansun.vip/gongxiangpan/uploads/1773847125480-d8y8sq.png`
4. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846185500-0rpjwm.png`
5. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846584708-oxjgyr.png`
6. `https://s3.jiansun.vip/gongxiangpan/uploads/1773846626436-8lspf3.jpeg`
7. `https://s3.jiansun.vip/gongxiangpan/uploads/1773848127368-dw0s0a.png`
8. `https://s3.jiansun.vip/gongxiangpan/uploads/1773848264041-2l07om.png`
9. `https://s3.jiansun.vip/gongxiangpan/uploads/1773848987321-s19v08.png`
10. `https://s3.jiansun.vip/gongxiangpan/uploads/1773849085019-w9cded.jpg`
### B. `s3.yydd.ai` 历史残留样本
如果你们需要验证备用域名或历史链路,也可以使用这些样本:
1. `https://s3.yydd.ai/gongxiangpan/uploads/1771897462521-toeu12.jpg`
2. `https://s3.yydd.ai/gongxiangpan/uploads/1771897607531-fm9w05.png`
3. `https://s3.yydd.ai/gongxiangpan/uploads/1771897836086-4uri8r.jpg`
4. `https://s3.yydd.ai/gongxiangpan/uploads/1771897858792-olbfdc.jpg`
5. `https://s3.yydd.ai/gongxiangpan/uploads/1771897892910-kjszzi.jpg`
### C. 样本覆盖说明
这批样本已经覆盖:
- `png`
- `jpg`
- `jpeg`
- 主域名资源
- 历史备用域名资源
- 会被多次重复读取的热点对象
如果你们还需要“明确的大文件样本”,建议再由资源域名入口日志侧补充带原始 `Content-Length` 的对象清单,因为应用侧日志只能看到尺寸请求,不直接记录对象文件大小。
---
## 2.4 是否存在同 key 覆盖
### 当前项目应用侧结论
**当前应用默认链路中,不存在“同 key 覆盖写新内容”的设计。**
项目统一上传时,文件 key 规则是:
```text
uploads/{timestamp}-{random}.{ext}
```
也就是说:
- 新内容默认新 key
- 新 URL 默认新对象
- 这对缓存非常友好
### 当前项目里没有发现的情况
在当前默认代码路径里,没有发现下面这种模式:
```text
始终覆盖同一个 latest.png / result.png / current.jpg
```
### 需要额外说明的边界
如果后续存储层或外部脚本手工做了:
- 同对象路径覆盖
- 同 key 重写
那么缓存仍会出现“同 URL 短时间返回旧内容”的风险。
但就项目当前应用代码而言,这不是主流写法。
---
## 2.5 时效和流量口径
## A. 图片更新后允许旧图保留多久
### 项目侧真实情况
对当前默认上传链路来说:
- 因为新内容默认新 key
- 所以绝大多数图片并不存在“同一个 URL 更新后必须立刻看到新内容”的问题
也就是说:
- **项目应用侧默认可以接受对象级相对长一点的缓存**
- 因为新图会用新 URL 呈现
### 项目侧建议口径
对 `/gongxiangpan/uploads/` 下这类对象资源,项目侧建议优先按“静态对象”理解,而不是按“频繁同 URL 热更新对象”理解。
## B. 是否存在必须秒级生效的路径
### 当前项目侧结论
就当前应用默认链路来看:
- **没有发现必须依赖“同一对象 URL 秒级更新生效”的核心路径**
需要秒级变化的更多是:
- 业务页面本身
- 会话状态
- 画布数据
而不是上传后的对象 URL 本身。
### 需要保守处理的例外
如果后续出现人工覆盖对象、后台替换同 URL 内容,那就属于另外一类策略,需要额外补清缓存机制。
## C. 峰值请求量级大概多少
### 当前能给出的项目侧样本
基于最近 3 个日志文件中,对 `/api/image-proxy` 读取桶对象 URL 的样本统计:
- 观测到 `s3.jiansun.vip` 资源样本请求:`129`
- 观测到 `s3.yydd.ai` 资源样本请求:`182`
- 两个域名合计样本:`311`
按分钟聚合,近期样本中可见的高点大约是:
- `11 req/min`
- `9 req/min`
- `8 req/min`
### 这个量级该怎么理解
这只能算“应用层近期可见样本”,不能等同于资源域名全量公网访问峰值。
但它已经足以说明:
- 热门资源存在重复读取
- 资源访问不是纯一次性下载
- 入口缓存会有实际收益
## D. 热门资源是否会被重复读取
### 明确结论
**会,而且已经在日志样本里看到同一对象被重复读取。**
例如近期样本中:
- 单个对象可被重复读取 `10-18` 次
- 同一对象还会以不同宽度参数重复请求
这说明缓存非常有价值,尤其适用于:
- AI详情长图
- 画布多次回显
- 聊天结果回看
- 相册缩略图与大图切换
---
## 3. 对缓存策略落地的项目侧建议
基于上面的真实情况,项目侧建议如下:
### 适合长缓存的路径
优先建议:
- `https://s3.jiansun.vip/gongxiangpan/uploads/*`
理由:
- 默认新内容新 key
- 热门对象会被重复读取
- 当前没有应用层同 URL 秒级覆盖诉求
### 需要保守处理的情况
如果你们后续发现这类链路存在:
- 人工覆盖对象
- 通过管理脚本复写原对象
- 同 URL 替换内容
则这些路径应转为:
- 短缓存
- 或配合 purge 机制
### 当前最重要的动作优先级
1. 明确主域名收敛到 `s3.jiansun.vip`
2. 把 `s3.yydd.ai` 定位为备用,不再做默认日常入口
3. 先对 `/gongxiangpan/uploads/` 做缓存策略落地
4. 再根据真实热点对象与尺寸参数情况优化缓存分配
---
## 4. 可直接发给对方的简版回复
下面这段可以直接作为对外补充答复:
> 补充我们项目侧的明确口径如下:
>
> 1. 主资源域名建议统一收敛为 `https://s3.jiansun.vip`,`https://s3.yydd.ai` 仅保留为备用 / 回切入口,不再参与默认日常调用。
> 2. 结合当前项目代码和最近业务日志样本,热点资源主要集中在 `/gongxiangpan/uploads/` 前缀下,且当前业务读取大量表现为 `/api/image-proxy?url=https://s3.jiansun.vip/gongxiangpan/uploads/...` 这种对对象资源的重复访问。
> 3. 当前应用默认采用 `uploads/{timestamp}-{random}.{ext}` 的新内容新 key 方式,不存在应用层默认的“同 key 覆盖写新内容”设计。
> 4. 从应用侧最近 3 天样本看,同一对象存在明显重复读取,单对象可重复访问 10-18 次,样本高点在双位数每分钟,因此读缓存对我们这类场景是有效的。
> 5. 对当前项目而言,绝大多数对象资源不依赖“同 URL 秒级内容更新”,因此 `/gongxiangpan/uploads/` 这类对象路径更适合按静态对象长缓存来设计。
>
> 我们这边已整理了一批可联调的真实 URL 样本,可直接用于你们后续灰度验证。
---
## 5. 附:本次补充结论与上一份答复的关系
上一份文档:
- [AI生图平台桶缓存落地对接答复-基于项目现状.md](/Users/lanmu/Desktop/ai-chatbot-main/docs/AI生图平台桶缓存落地对接答复-基于项目现状.md)
适合回答:
- 项目整体是否适合缓存
- 为什么基本不用改业务代码
- 为什么要单域名
本份文档适合回答:
- 对方点名索要的 5 项补充信息
- 近期日志里实际观察到的热点 URL 与流量样本
- 当前双域名残留现状

View File

@ -0,0 +1,206 @@
# 智能体页面 UI 优化文档
## 优化概述
本次优化对智能体(Agent)页面进行了全面的 UI 重构,采用简洁、高级、细腻的设计理念,提升用户体验和视觉品质。
---
## 优化范围
### 1. 智能体卡片 (AgentPresetCardNew)
**位置**: `components/agents/AgentPresetCardNew.tsx`
**优化内容**:
- ✅ 采用圆角卡片设计(rounded-2xl)
- ✅ 添加 Framer Motion 入场动画( stagger 效果)
- ✅ 悬浮时卡片上浮效果(y: -4)
- ✅ 选中状态添加顶部渐变指示条
- ✅ 图片悬浮缩放效果(scale: 1.05)
- ✅ 操作按钮悬浮淡入效果
- ✅ 类型标签使用图标+文字组合
- ✅ 信息层级更清晰(名称/描述/发布人/时间)
**视觉效果**:
- 柔和的阴影系统
- 渐变选中指示器
- 流畅的悬浮动效
---
### 2. 智能体面板 (AgentPresetPanelNew)
**位置**: `components/agents/AgentPresetPanelNew.tsx`
**优化内容**:
- ✅ 全新头部设计(渐变图标 + 标题)
- ✅ 搜索框圆角优化(rounded-xl)
- ✅ 空状态插画式提示
- ✅ 区块标题带计数徽章
- ✅ AnimatePresence 列表动画
- ✅ 响应式网格布局(1-2列自适应)
**交互改进**:
- 卡片删除/添加时的平滑过渡
- 搜索实时过滤动画
---
### 3. 工作流面板 (AgentWorkflowPanelNew)
**位置**: `components/agents/AgentWorkflowPanelNew.tsx`
**优化内容**:
- ✅ 步骤进度条可视化
- ✅ 步骤卡片全新设计(左侧选中指示条)
- ✅ 选中动画(缩放 + 勾选标记)
- ✅ 步骤序号徽章
- ✅ Prompt 预览区域优化
- ✅ 当前选择摘要卡片
**交互改进**:
- 步骤卡片悬浮上浮
- 选中状态平滑过渡
- 进度条动画
---
### 4. 生图工作台 (AgentGenerationWorkspaceNew)
**位置**: `components/agents/AgentGenerationWorkspaceNew.tsx`
**优化内容**:
- ✅ 全新左右分栏布局
- ✅ 图片槽位圆角卡片设计
- ✅ 槽位悬浮动画(缩放 + 删除按钮)
- ✅ 结果展示区全屏沉浸式设计
- ✅ 缩略图选中环效果
- ✅ 全屏预览遮罩层
**交互改进**:
- 图片槽位悬浮显示删除按钮
- 结果缩略图悬浮放大
- 全屏预览淡入动画
- 生成状态平滑切换
---
## 设计系统
### 色彩系统
```css
/* 主色调 - 靛蓝 */
--primary: 238 83% 66%; /* #6366f1 */
--primary-subtle: 238 100% 97%; /* #eef2ff */
/* 背景色 */
--background: 0 0% 98%; /* #fafafa */
--surface: 0 0% 100%; /* #ffffff */
/* 文字色 */
--text-primary: 222 47% 11%; /* #0f172a */
--text-secondary: 215 16% 47%; /* #64748b */
```
### 阴影系统
```css
--shadow-card: 0 0 0 1px rgb(0 0 0 / 0.03), 0 2px 8px rgb(0 0 0 / 0.04);
--shadow-card-hover: 0 0 0 1px rgb(0 0 0 / 0.05), 0 12px 32px rgb(0 0 0 / 0.08);
```
### 圆角系统
```css
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-2xl: 32px;
```
### 动画规范
```typescript
// 入场动画
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: { delay: i * 0.05, duration: 0.3 }
})
};
// 悬浮动画
whileHover={{ y: -4, scale: 1.01 }}
// 选中动画
animate={{ scale: [0.95, 1.02, 1] }}
transition={{ duration: 0.3 }}
```
---
## 文件变更列表
### 新增文件
1. `components/agents/AgentPresetCardNew.tsx` - 智能体卡片新组件
2. `components/agents/AgentPresetPanelNew.tsx` - 智能体面板新组件
3. `components/agents/AgentWorkflowPanelNew.tsx` - 工作流面板新组件
4. `components/agents/AgentGenerationWorkspaceNew.tsx` - 生图工作台新组件
5. `components/agents/AgentPanelHeader.tsx` - 面板头部组件
6. `components/agents/AgentSectionHeader.tsx` - 区块头部组件
7. `components/agents/index-new.ts` - 新组件统一导出
### 修改文件
1. `app/canvas/[id]/page.tsx` - 画布页面(更新导入和组件使用)
---
## 技术实现
### 动画库
- **Framer Motion**: 负责所有入场动画、悬浮动画、过渡效果
### 保持不变的
- ✅ 所有 API 调用(使用原有的 services/agents)
- ✅ 所有类型定义(复用 types/agent)
- ✅ 状态管理逻辑(完全保持一致)
- ✅ 后端接口调用(完全保持一致)
---
## 使用方式
新组件已经整合到画布页面中,访问智能体模式时会自动使用新 UI:
```
/canvas/[id]?mode=agent
```
如需在开发环境对比新旧版本,可以临时修改导入:
```typescript
// 旧版本
import { AgentPresetPanel } from "@/components/agents/AgentPresetPanel";
// 新版本
import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew";
```
---
## 后续优化建议
1. **暗黑模式优化** - 进一步完善暗黑模式下的色彩对比度
2. **响应式优化** - 针对平板设备优化布局
3. **无障碍支持** - 添加 ARIA 标签和键盘导航
4. **性能优化** - 图片懒加载、虚拟滚动(列表很长时)
5. **微交互** - 添加更多细腻的反馈动画(按钮点击、加载状态等)
---
## 效果预览
优化后的智能体页面具有以下特点:
1. **视觉层次清晰** - 通过阴影、间距、色彩区分信息层级
2. **动效细腻流畅** - 所有交互都有平滑的过渡效果
3. **现代简洁风格** - 去除多余装饰,聚焦内容本身
4. **一致性** - 所有组件遵循统一的设计系统
5. **专业感** - 适合企业级应用场景的视觉品质

View File

@ -0,0 +1,259 @@
# 智能体深色主题 UI 文档
## 设计参考
本次深色主题设计参考了「纳米漫剧流水线」页面的视觉风格,具有以下特点:
- **纯黑背景** - 深邃的黑色背景 (#000000)
- **卡片网格** - 固定尺寸的卡片,整齐排列
- **渐变遮罩** - 图片底部渐变遮罩,确保文字可读
- **中央播放按钮** - 悬浮时中央显示播放/使用按钮
- **响应式网格** - 2-6 列自适应布局
---
## 组件列表
### 1. AgentPresetCardDark
**文件**: `components/agents/AgentPresetCardDark.tsx`
**特点**:
- 4:3 固定比例卡片
- 纯黑背景配渐变遮罩
- 顶部类型标签和可见性标签
- 中央悬浮播放按钮
- 底部信息栏(标题、作者、槽位数)
- 选中状态发光效果
**视觉效果**:
```
┌─────────────────────────┐
│ [类型标签] [可见性] │
│ │
│ ┌─────────────┐ │
│ │ ▶️ 播放 │ │ <- 悬浮时显示
│ └─────────────┘ │
│ │
│ 标题名称 │
│ 作者 · 槽位 · 时间 │
└─────────────────────────┘
```
---
### 2. AgentPresetPanelDark
**文件**: `components/agents/AgentPresetPanelDark.tsx`
**布局结构**:
- 顶部固定导航栏(Logo + 搜索 + 新建按钮)
- 最大宽度 1800px,两侧充足留白
- 网格布局:2-6 列自适应
- 区块标题带计数徽章
**响应式断点**:
```
2xl: 6列 (≥1536px)
xl: 5列 (≥1280px)
lg: 4列 (≥1024px)
sm: 3列 (≥640px)
default: 2列
```
---
### 3. AgentWorkflowPanelDark
**文件**: `components/agents/AgentWorkflowPanelDark.tsx`
**布局**:
- 顶部进度指示器
- 步骤选项卡片网格(16:10 比例)
- 当前选择摘要
- Prompt 预览区
**交互**:
- 步骤选项卡片悬浮上浮
- 选中状态勾选标记
- 进度条动画
---
### 4. AgentGenerationWorkspaceDark
**文件**: `components/agents/AgentGenerationWorkspaceDark.tsx`
**布局**:
- 左右分栏(380px + 自适应)
- 左侧:图片槽位 + 提示词输入 + 参数设置
- 右侧:结果展示区 + 缩略图列表
**特点**:
- 图片槽位圆角卡片
- 生成结果全屏展示
- 缩略图横向滚动
- 全屏预览模态框
---
## 色彩系统
### 背景色
```css
bg-black /* 主背景 #000000 */
bg-zinc-900 /* 卡片背景 #18181b */
bg-white/[0.02] /* 面板背景 微透 */
bg-white/5 /* 输入框背景 */
```
### 边框色
```css
border-white/5 /* 细边框 */
border-white/10 /* 选中边框 */
border-white/20 /* 悬浮边框 */
```
### 文字色
```css
text-white /* 主标题 */
text-white/80 /* 副标题 */
text-white/60 /* 描述文字 */
text-white/40 /* 占位文字 */
```
### 强调色
```css
/* 生图 - 紫罗兰 */
from-violet-500 to-purple-600
/* 视频 - 琥珀 */
from-amber-500 to-orange-600
/* 生文 - 翡翠 */
from-emerald-500 to-teal-600
```
---
## 阴影与光效
### 卡片阴影
```css
/* 默认 */
ring-1 ring-white/5
/* 悬浮 */
ring-1 ring-white/20
hover:ring-white/20
/* 选中 */
ring-2 ring-white/30
shadow-[0_0_40px_rgba(255,255,255,0.1)]
```
### 按钮光效
```css
/* 白色按钮 */
bg-white text-black
hover:shadow-[0_0_30px_rgba(255,255,255,0.3)]
/* 渐变按钮 */
bg-gradient-to-r from-violet-600 to-purple-600
```
---
## 动画规范
### 入场动画
```typescript
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.08 }}
```
### 悬浮动画
```typescript
whileHover={{ y: -8, scale: 1.02 }}
transition={{ duration: 0.5 }}
```
### 选中动画
```typescript
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3 }}
```
---
## 使用方式
### 在画布页面中使用
```typescript
// 深色主题组件已自动整合到智能体模式
// 访问 /canvas/[id]?mode=agent 即可看到深色主题
// 手动使用组件
import { AgentPresetPanelDark } from "@/components/agents/AgentPresetPanelDark";
<AgentPresetPanelDark
selectedPresetId={selectedId}
onSelectPreset={handleSelect}
/>
```
### 组件切换
如需在深浅主题间切换,可修改 `app/canvas/[id]/page.tsx` 中的导入:
```typescript
// 浅色主题
import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew";
// 深色主题
import { AgentPresetPanelDark } from "@/components/agents/AgentPresetPanelDark";
```
---
## 文件清单
```
components/agents/
├── AgentPresetCardDark.tsx # 深色卡片
├── AgentPresetPanelDark.tsx # 深色面板
├── AgentWorkflowPanelDark.tsx # 深色工作流
├── AgentGenerationWorkspaceDark.tsx # 深色工作台
├── AgentPageDark.tsx # 深色页面容器
└── index-dark.ts # 统一导出
```
---
## 与原设计的对比
| 特性 | 原浅色设计 | 新深色设计 |
|------|-----------|-----------|
| 背景 | 白色/浅灰 | 纯黑 |
| 卡片 | 白底阴影 | 黑底边框 |
| 强调色 | 靛蓝 | 紫罗兰/渐变 |
| 选中效果 | 蓝边 | 白边+发光 |
| 悬浮效果 | 上浮+阴影 | 上浮+边框高亮 |
| 整体风格 | 清爽明亮 | 沉浸高级 |
---
## 注意事项
1. **图片质量** - 深色背景下图片质量更明显,建议使用高质量预览图
2. **对比度** - 文字使用白色/高透明度确保可读性
3. **加载状态** - 深色背景下的加载动画更明显
4. **空状态** - 空状态使用白色/20% 透明度图标
---
## 后续优化方向
1. 添加暗黑/明亮模式切换按钮
2. 支持自定义主题色
3. 添加更多入场动画变体
4. 优化移动端触摸体验

2
next-env.d.ts vendored
View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

File diff suppressed because one or more lines are too long