master #14
@@ -119,6 +119,11 @@ RPG_LLM_WEB_SEARCH_ENABLED="true"
|
||||
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
|
||||
|
||||
# Server-side APIMart image generation config for optional puzzle image models.
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY="YOUR_APIMART_API_KEY"
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# 阿里云 OSS 配置。
|
||||
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
|
||||
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
- protocol: `/responses`
|
||||
- web_search: 开启时映射为 `tools: [{ "type": "web_search", "max_keyword": 3 }]`
|
||||
|
||||
联网搜索是模板创作的增强能力,不是 Agent 对话主链的硬依赖。若上游 Responses SSE 返回 `ToolNotOpen` 或“账号未开通 web search”类错误,`creation_agent_llm_turn` 必须使用同一 system/user prompt 自动降级为无联网搜索重试一次;只有重试仍失败时才向玩法层返回“生成失败”。这样 RPG Agent、大鱼 Agent、拼图 Agent 都能在未开通搜索插件的环境中继续完成基础 JSON turn。
|
||||
|
||||
覆盖入口:
|
||||
|
||||
1. `creation_agent_llm_turn.rs`
|
||||
@@ -74,6 +76,7 @@ Responses 非流式解析优先读取 `output_text`,再兼容 `output[].conten
|
||||
1. RPG 运行时 LLM 请求在代码层显式带 `doubao-seed-character-251128`。
|
||||
2. 创作模板 LLM 请求在代码层显式带 `deepseek-v3-2-251201` 与 Responses 协议。
|
||||
3. `platform-llm` 单测覆盖 Responses 非流式、Responses SSE、Responses web_search tools 请求体。
|
||||
4. `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml` 通过。
|
||||
5. `cargo test -p api-server creation_agent_llm_turn --manifest-path server-rs/Cargo.toml` 通过。
|
||||
6. 修改后按项目约束使用 `npm run api-server:maincloud` 重新启动后端,并执行相应自动测试。
|
||||
4. `api-server` 单测覆盖创作 Agent 对 `ToolNotOpen` / 未开通 web search 错误的识别,确保可触发无搜索重试。
|
||||
5. `cargo test -p platform-llm --manifest-path server-rs/Cargo.toml` 通过。
|
||||
6. `cargo test -p api-server creation_agent_llm_turn --manifest-path server-rs/Cargo.toml` 通过。
|
||||
7. 修改后按项目约束使用 `npm run api-server:maincloud` 重新启动后端,并执行相应自动测试。
|
||||
|
||||
33
docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md
Normal file
33
docs/technical/NEW_WORK_ENTRY_CONFIG_2026-05-01.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 新建作品入口配置说明 2026-05-01
|
||||
|
||||
## 背景
|
||||
|
||||
创作中心顶部“新建作品”入口和平台创作类型弹层都依赖同一组玩法模板。此前入口开放状态、隐藏状态和中文文案集中写在 `src/components/platform-entry/platformEntryCreationTypes.ts` 与入口组件中,后续切换玩法开放节奏时容易出现多个入口不一致。
|
||||
|
||||
## 落地规则
|
||||
|
||||
1. 新建作品入口配置统一放在 `src/config/newWorkEntryConfig.ts`。
|
||||
2. `visible` 控制玩法是否展示在新建作品入口和创作类型弹层中。
|
||||
3. `open` 控制玩法是否允许点击创建;`open: false` 时入口保持展示但禁用。
|
||||
4. `title`、`subtitle`、`badge` 控制玩法卡片文案。
|
||||
5. `startCard` 控制创作中心顶部新建作品模块的标题、辅助文案和移动端角标文案。
|
||||
6. `typeModal` 控制平台创作类型弹层标题和描述。
|
||||
7. 入口排序仍遵循“可创建玩法在前,未开放玩法在后”;同组内部沿用配置顺序。
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 玩法 | 展示 | 开放 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 角色扮演 | 是 | 是 | 点击后进入 RPG Agent 共创工作台 |
|
||||
| 大鱼吃小鱼 | 否 | 是 | 功能仍保留,不在新建作品入口展示 |
|
||||
| 拼图 | 是 | 是 | 当前唯一可见且可创建入口 |
|
||||
| 抓大鹅 | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| AIRP | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
| 视觉小说 | 是 | 否 | 保留入口,显示敬请期待 |
|
||||
|
||||
## 验收
|
||||
|
||||
1. 修改 `src/config/newWorkEntryConfig.ts` 后,创作中心顶部卡带和平台创作类型弹层应同步变化。
|
||||
2. 隐藏玩法不触发入口预加载,也不出现在新建作品入口中。
|
||||
3. 未开放玩法点击态保持禁用,不应进入鉴权或创建会话链路。
|
||||
4. 已开放玩法点击后必须进入对应创建链路;若用户未登录,先走登录保护。
|
||||
@@ -0,0 +1,69 @@
|
||||
# 拼图 APIMart 图片模型路由接入 2026-05-01
|
||||
|
||||
## 背景
|
||||
|
||||
拼图创作已收口为填表式流程,首图生成和结果页关卡重新生成都由 `server-rs/crates/api-server/src/puzzle.rs` 执行外部图片 I/O,再把正式图写入 OSS 与 SpacetimeDB。新的模型选择只影响图片生成上游,不改变 SpacetimeDB 表结构、拼图草稿结构或前端运行时规则。
|
||||
|
||||
本轮参考 APIMart 文档:
|
||||
|
||||
1. `https://docs.apimart.ai/cn/api-reference/images/gpt-image-2/generation`
|
||||
2. `https://docs.apimart.ai/cn/api-reference/images/gemini-3.1-flash/generation`
|
||||
|
||||
两条文档均指向 OpenAI 兼容风格的图片生成入口:`POST https://api.apimart.ai/v1/images/generations`,头部使用 `Authorization: Bearer {APIMART_API_KEY}`。请求体至少包含 `model`、`prompt`、`n`、`size`。返回体按 OpenAI images 兼容格式优先读取 `data[].url`,若供应商返回异步任务结构,则继续按 `task_id` / `tasks/{task_id}` 轮询并提取图片 URL。
|
||||
|
||||
## 模型选项
|
||||
|
||||
拼图图片生成支持三个选项:
|
||||
|
||||
| 前端显示 | 请求值 | 上游 |
|
||||
| --- | --- | --- |
|
||||
| 原模型 | `original` | 继续使用现有 DashScope 文生图 / 图生图链路 |
|
||||
| `gpt-image-2` | `gpt-image-2` | APIMart `/v1/images/generations` |
|
||||
| `nanobanana2` | `gemini-3.1-flash-image-preview` | APIMart `/v1/images/generations` |
|
||||
|
||||
默认值为 `original`。前端只负责展示和传递所选模型,不能把模型路由逻辑、上游请求体拼装或 API Key 暴露到浏览器。
|
||||
|
||||
## 前端交互
|
||||
|
||||
1. 拼图创作表单的“画面描述”输入框左下角显示当前调用模型。
|
||||
2. 拼图结果页关卡详情的“画面描述”输入框左下角同样显示当前调用模型。
|
||||
3. 点击模型标识弹出轻量选择菜单,支持 `原模型`、`gpt-image-2`、`nanobanana2` 三项。
|
||||
4. 菜单只是模型选择控件,不写入说明性规则文案。
|
||||
5. 参考图入口继续在画面描述输入框右下角,模型选择在左下角,两者不得遮挡文本。
|
||||
6. 表单创建 session、自动保存表单草稿、首图生成失败重试时都要保留当前模型选择。
|
||||
7. 结果页关卡重新生成时将当前模型随 `generate_puzzle_images` action 传给后端。
|
||||
|
||||
## 后端路由
|
||||
|
||||
1. `CreatePuzzleAgentSessionRequest` 与 `ExecutePuzzleAgentActionRequest` 增加可选 `imageModel` 字段;该字段不进入 SpacetimeDB reducer 输入结构。
|
||||
2. `compile_puzzle_draft_with_initial_cover` 与 `generate_puzzle_image_candidates` 增加图片模型参数。
|
||||
3. `imageModel` 归一化规则:
|
||||
- 空值、`original` 或未知值统一回落为原 DashScope 链路;
|
||||
- `gpt-image-2` 走 APIMart;
|
||||
- `gemini-3.1-flash-image-preview` 走 APIMart,前端显示名为 `nanobanana2`。
|
||||
4. APIMart 文生图和图生图共用 `POST /v1/images/generations`。有参考图时,后端将参考图 Data URL 作为 `image_urls` 数组传入;若上游不接受该字段,错误按上游失败返回,不在前端降级伪造结果。
|
||||
5. APIMart 尺寸使用文档要求的比例写法 `1:1`;原 DashScope 继续使用像素写法 `1024*1024`。`gemini-3.1-flash-image-preview` 额外带 `resolution = "1K"`,对齐约 1024px 的拼图正方形素材。
|
||||
6. APIMart 生成成功后仍下载远程图片,沿用现有 OSS 私有对象、`asset_object` 和 `asset_entity_binding` 写入流程。
|
||||
7. APIMart 错误统一映射为 `502 UPSTREAM_ERROR`,`details.provider = "apimart"`,保留上游状态码、业务 message 和截断后的 raw excerpt。
|
||||
|
||||
## 环境变量
|
||||
|
||||
新增服务端环境变量:
|
||||
|
||||
```text
|
||||
APIMART_BASE_URL="https://api.apimart.ai/v1"
|
||||
APIMART_API_KEY="YOUR_APIMART_API_KEY"
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
`APIMART_API_KEY` 只能存在于本地或部署环境,不写入 Git 跟踪文件。若选择 APIMart 模型但缺少 key,后端返回服务不可用错误,前端展示现有错误面板。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 创作表单和关卡详情的画面描述框左下角能切换 `原模型`、`gpt-image-2`、`nanobanana2`。
|
||||
2. 点击“生成草稿”时,后端首图生成使用当前表单选择的模型。
|
||||
3. 点击“生成画面 / 重新生成画面”时,后端当前关卡图片生成使用关卡详情选择的模型。
|
||||
4. 选择 `original` 时,现有 DashScope 请求体、尺寸和错误处理保持兼容。
|
||||
5. 选择 APIMart 模型时,请求 `POST {APIMART_BASE_URL}/images/generations`,使用 `Authorization: Bearer {APIMART_API_KEY}`,`model` 等于请求值,`size = 1:1`。
|
||||
6. 不改 SpacetimeDB 表结构,因此无需更新 `migration.rs` 或重新生成 bindings。
|
||||
7. 后端改动后运行对应 Rust 测试,并按项目约束用 `npm run api-server:maincloud` 重启验证。
|
||||
@@ -5,6 +5,8 @@
|
||||
## 文档列表
|
||||
|
||||
- [PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md](./PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md):冻结当前对外中文命名,产品展示名统一为“百梦”,消费单位为“光点”,公开账号标识为“百梦号”,创作侧称谓为“百梦主”。
|
||||
- [NEW_WORK_ENTRY_CONFIG_2026-05-01.md](./NEW_WORK_ENTRY_CONFIG_2026-05-01.md):记录新建作品入口开放状态、隐藏状态和展示文案统一收口到 `src/config/newWorkEntryConfig.ts` 的配置规则。
|
||||
- [PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md](./PUZZLE_APIMART_IMAGE_MODEL_ROUTING_2026-05-01.md):记录拼图图片生成接入 APIMart `gpt-image-2`、`gemini-3.1-flash-image-preview`,以及画面描述框内模型切换与后端路由边界。
|
||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
||||
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。
|
||||
- [LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md](./LLM_MODEL_ROUTING_RPG_AND_CREATION_2026-04-30.md):冻结 RPG 运行时剧情推理使用 `doubao-seed-character-251128` 的 `/chat/completions`,以及所有模板创作大模型推理使用 `deepseek-v3-2-251201` 的 `/responses`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,131 @@
|
||||
event: response.created
|
||||
data: {"type":"response.created","response":{"created_at":1777642663,"id":"resp_021777642661795d15b81097ef4f266330d7ca8118c9494434eb3","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777901861},"sequence_number":0}
|
||||
|
||||
event: response.in_progress
|
||||
data: {"type":"response.in_progress","response":{"created_at":1777642663,"id":"resp_021777642661795d15b81097ef4f266330d7ca8118c9494434eb3","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777901861},"sequence_number":1}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","role":"assistant","status":"in_progress","id":"msg_02177764266332000000000000000000000ffffac1550372ec101"},"sequence_number":2}
|
||||
|
||||
event: response.content_part.added
|
||||
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"part":{"type":"output_text","text":""},"sequence_number":3}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"我需要","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":4}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"先","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":5}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"了解","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":6}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"一些","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":7}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"关于","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":8}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":9}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"玩具","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":10}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"王国","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":11}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":12}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"的","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":13}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"创作","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":14}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"背景","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":15}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"和","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":16}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"灵感","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":17}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"来源","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":18}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":19}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"以便","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":20}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"更好地","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":21}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"帮助你","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":22}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"构建","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":23}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"这个","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":24}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"游戏","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":25}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"世界","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":26}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"。","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":27}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"让我","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":28}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"搜索","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":29}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"一些","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":30}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"相关信息","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":31}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"。\n\n","item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"sequence_number":32}
|
||||
|
||||
event: response.output_text.done
|
||||
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"text":"我需要先了解一些关于\"玩具王国\"的创作背景和灵感来源,以便更好地帮助你构建这个游戏世界。让我搜索一些相关信息。\n\n","sequence_number":33}
|
||||
|
||||
event: response.content_part.done
|
||||
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_02177764266332000000000000000000000ffffac1550372ec101","output_index":0,"part":{"type":"output_text","text":"我需要先了解一些关于\"玩具王国\"的创作背景和灵感来源,以便更好地帮助你构建这个游戏世界。让我搜索一些相关信息。\n\n"},"sequence_number":34}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要先了解一些关于\"玩具王国\"的创作背景和灵感来源,以便更好地帮助你构建这个游戏世界。让我搜索一些相关信息。\n\n"}],"status":"completed","id":"msg_02177764266332000000000000000000000ffffac1550372ec101"},"sequence_number":35}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":1,"item":{"type":"web_search_call","status":"in_progress","id":"ws_02177764266480300000000000000000000ffffac155037f1bc06"},"sequence_number":36}
|
||||
|
||||
event: response.web_search_call.in_progress
|
||||
data: {"type":"response.web_search_call.in_progress","item_id":"ws_02177764266480300000000000000000000ffffac155037f1bc06","output_index":1,"sequence_number":37}
|
||||
|
||||
event: response.web_search_call.searching
|
||||
data: {"type":"response.web_search_call.searching","item_id":"ws_02177764266480300000000000000000000ffffac155037f1bc06","output_index":1,"sequence_number":38}
|
||||
|
||||
event: response.web_search_call.completed
|
||||
data: {"type":"response.web_search_call.completed","item_id":"ws_02177764266480300000000000000000000ffffac155037f1bc06","output_index":1,"sequence_number":39}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":1,"item":{"type":"web_search_call","action":{"query":"玩具王国 游戏设定 世界观;玩具题材游戏 设计灵感;王国主题玩具世界","type":"search"},"status":"completed","id":"ws_02177764266480300000000000000000000ffffac155037f1bc06"},"sequence_number":40}
|
||||
|
||||
event: error
|
||||
data: {"type":"error","code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin","param":"","sequence_number":41}
|
||||
|
||||
event: response.failed
|
||||
data: {"type":"response.failed","response":{"created_at":1777642663,"error":{"code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin"},"id":"resp_021777642661795d15b81097ef4f266330d7ca8118c9494434eb3","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要先了解一些关于\"玩具王国\"的创作背景和灵感来源,以便更好地帮助你构建这个游戏世界。让我搜索一些相关信息。\n\n"}],"status":"completed","id":"msg_02177764266332000000000000000000000ffffac1550372ec101"},{"type":"web_search_call","action":{"query":"玩具王国 游戏设定 世界观;玩具题材游戏 设计灵感;王国主题玩具世界","type":"search"},"status":"completed","id":"ws_02177764266480300000000000000000000ffffac155037f1bc06"}],"service_tier":"default","status":"failed","tools":[{"type":"web_search","max_keyword":3}],"usage":{"input_tokens":1791,"output_tokens":122,"total_tokens":1913,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0}},"caching":{"type":"disabled"},"store":true,"expire_at":1777901861},"sequence_number":42}
|
||||
|
||||
data: [DONE]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,117 @@
|
||||
event: response.created
|
||||
data: {"type":"response.created","response":{"created_at":1777644754,"id":"resp_02177764475392548aafbb39a6fef3e9b38f2299115036678748a","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777903953},"sequence_number":0}
|
||||
|
||||
event: response.in_progress
|
||||
data: {"type":"response.in_progress","response":{"created_at":1777644754,"id":"resp_02177764475392548aafbb39a6fef3e9b38f2299115036678748a","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777903953},"sequence_number":1}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","role":"assistant","status":"in_progress","id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d"},"sequence_number":2}
|
||||
|
||||
event: response.content_part.added
|
||||
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"part":{"type":"output_text","text":""},"sequence_number":3}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"我需要","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":4}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"先","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":5}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"搜索","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":6}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"一些","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":7}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"关于","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":8}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":9}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"玩具","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":10}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"王国","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":11}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":12}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"的相关","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":13}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"信息","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":14}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":15}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"以便","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":16}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"更好地","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":17}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"理解","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":18}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"这个概念","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":19}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":20}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"为","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":21}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"我们的","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":22}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"共创","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":23}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"提供","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":24}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"更有","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":25}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"价值的","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":26}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"参考","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":27}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"。\n\n","item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"sequence_number":28}
|
||||
|
||||
event: response.output_text.done
|
||||
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"text":"我需要先搜索一些关于\"玩具王国\"的相关信息,以便更好地理解这个概念,为我们的共创提供更有价值的参考。\n\n","sequence_number":29}
|
||||
|
||||
event: response.content_part.done
|
||||
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d","output_index":0,"part":{"type":"output_text","text":"我需要先搜索一些关于\"玩具王国\"的相关信息,以便更好地理解这个概念,为我们的共创提供更有价值的参考。\n\n"},"sequence_number":30}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要先搜索一些关于\"玩具王国\"的相关信息,以便更好地理解这个概念,为我们的共创提供更有价值的参考。\n\n"}],"status":"completed","id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d"},"sequence_number":31}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":1,"item":{"type":"web_search_call","status":"in_progress","id":"ws_02177764475609200000000000000000000ffffac15dae8e54616"},"sequence_number":32}
|
||||
|
||||
event: response.web_search_call.in_progress
|
||||
data: {"type":"response.web_search_call.in_progress","item_id":"ws_02177764475609200000000000000000000ffffac15dae8e54616","output_index":1,"sequence_number":33}
|
||||
|
||||
event: response.web_search_call.searching
|
||||
data: {"type":"response.web_search_call.searching","item_id":"ws_02177764475609200000000000000000000ffffac15dae8e54616","output_index":1,"sequence_number":34}
|
||||
|
||||
event: response.web_search_call.completed
|
||||
data: {"type":"response.web_search_call.completed","item_id":"ws_02177764475609200000000000000000000ffffac15dae8e54616","output_index":1,"sequence_number":35}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":1,"item":{"type":"web_search_call","action":{"query":"玩具王国 游戏设定;玩具王国 世界观;玩具主题 游戏","type":"search"},"status":"completed","id":"ws_02177764475609200000000000000000000ffffac15dae8e54616"},"sequence_number":36}
|
||||
|
||||
event: error
|
||||
data: {"type":"error","code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin","param":"","sequence_number":37}
|
||||
|
||||
event: response.failed
|
||||
data: {"type":"response.failed","response":{"created_at":1777644754,"error":{"code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin"},"id":"resp_02177764475392548aafbb39a6fef3e9b38f2299115036678748a","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要先搜索一些关于\"玩具王国\"的相关信息,以便更好地理解这个概念,为我们的共创提供更有价值的参考。\n\n"}],"status":"completed","id":"msg_02177764475480800000000000000000000ffffac15dae87fba6d"},{"type":"web_search_call","action":{"query":"玩具王国 游戏设定;玩具王国 世界观;玩具主题 游戏","type":"search"},"status":"completed","id":"ws_02177764475609200000000000000000000ffffac15dae8e54616"}],"service_tier":"default","status":"failed","tools":[{"type":"web_search","max_keyword":3}],"usage":{"input_tokens":1808,"output_tokens":120,"total_tokens":1928,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0}},"caching":{"type":"disabled"},"store":true,"expire_at":1777903953},"sequence_number":38}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,117 @@
|
||||
event: response.created
|
||||
data: {"type":"response.created","response":{"created_at":1777644794,"id":"resp_02177764479383248aafbb39a6fef3e9b38f22991150366ae7197","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777903993},"sequence_number":0}
|
||||
|
||||
event: response.in_progress
|
||||
data: {"type":"response.in_progress","response":{"created_at":1777644794,"id":"resp_02177764479383248aafbb39a6fef3e9b38f22991150366ae7197","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777903993},"sequence_number":1}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","role":"assistant","status":"in_progress","id":"msg_02177764479470200000000000000000000ffffac1549fec91d99"},"sequence_number":2}
|
||||
|
||||
event: response.content_part.added
|
||||
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"part":{"type":"output_text","text":""},"sequence_number":3}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"我需要","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":4}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"先","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":5}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"搜索","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":6}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"一些","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":7}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"关于","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":8}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":9}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"玩具","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":10}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"王国","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":11}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":12}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"和","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":13}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":14}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"误","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":15}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"闯入","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":16}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"的人类","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":17}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"孩子","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":18}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"\"","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":19}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"的","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":20}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"创意","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":21}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"灵感","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":22}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":23}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"来","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":24}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"帮助我们","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":25}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"更好地","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":26}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"构建","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":27}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"这个世界","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":28}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"。\n\n","item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"sequence_number":29}
|
||||
|
||||
event: response.output_text.done
|
||||
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"text":"我需要先搜索一些关于\"玩具王国\"和\"误闯入的人类孩子\"的创意灵感,来帮助我们更好地构建这个世界。\n\n","sequence_number":30}
|
||||
|
||||
event: response.content_part.done
|
||||
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_02177764479470200000000000000000000ffffac1549fec91d99","output_index":0,"part":{"type":"output_text","text":"我需要先搜索一些关于\"玩具王国\"和\"误闯入的人类孩子\"的创意灵感,来帮助我们更好地构建这个世界。\n\n"},"sequence_number":31}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要先搜索一些关于\"玩具王国\"和\"误闯入的人类孩子\"的创意灵感,来帮助我们更好地构建这个世界。\n\n"}],"status":"completed","id":"msg_02177764479470200000000000000000000ffffac1549fec91d99"},"sequence_number":32}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":1,"item":{"type":"web_search_call","status":"in_progress","id":"ws_02177764479607400000000000000000000ffffac1549fee3e1df"},"sequence_number":33}
|
||||
|
||||
event: response.web_search_call.in_progress
|
||||
data: {"type":"response.web_search_call.in_progress","item_id":"ws_02177764479607400000000000000000000ffffac1549fee3e1df","output_index":1,"sequence_number":34}
|
||||
|
||||
event: response.web_search_call.searching
|
||||
data: {"type":"response.web_search_call.searching","item_id":"ws_02177764479607400000000000000000000ffffac1549fee3e1df","output_index":1,"sequence_number":35}
|
||||
|
||||
event: response.web_search_call.completed
|
||||
data: {"type":"response.web_search_call.completed","item_id":"ws_02177764479607400000000000000000000ffffac1549fee3e1df","output_index":1,"sequence_number":36}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":1,"item":{"type":"web_search_call","action":{"query":"玩具王国创意设定;人类孩子误入奇幻世界;玩具主题游戏世界观","type":"search"},"status":"completed","id":"ws_02177764479607400000000000000000000ffffac1549fee3e1df"},"sequence_number":37}
|
||||
|
||||
event: error
|
||||
data: {"type":"error","code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin","param":"","sequence_number":38}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,155 @@
|
||||
event: response.created
|
||||
data: {"type":"response.created","response":{"created_at":1777644839,"id":"resp_02177764483844428a04e35a69bbadd2802274f74f3d7f05d787b","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777904038},"sequence_number":0}
|
||||
|
||||
event: response.in_progress
|
||||
data: {"type":"response.in_progress","response":{"created_at":1777644839,"id":"resp_02177764483844428a04e35a69bbadd2802274f74f3d7f05d787b","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","service_tier":"default","tools":[{"type":"web_search","max_keyword":3}],"caching":{"type":"disabled"},"store":true,"expire_at":1777904038},"sequence_number":1}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","role":"assistant","status":"in_progress","id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581"},"sequence_number":2}
|
||||
|
||||
event: response.content_part.added
|
||||
data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"part":{"type":"output_text","text":""},"sequence_number":3}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"我需要","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":4}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"基于","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":5}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"当前的","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":6}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"玩具","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":7}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"王国","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":8}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"设定","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":9}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":10}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"为用户","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":11}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"补","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":12}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"全","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":13}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"剩余的","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":14}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"设定","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":15}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"内容","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":16}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"。","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":17}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"让我","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":18}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"先","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":19}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"搜索","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":20}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"一些","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":21}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"关于","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":22}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"玩具","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":23}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"王国","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":24}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"、","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":25}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"童话","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":26}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"设定","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":27}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"、","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":28}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"儿童","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":29}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"奇幻","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":30}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"世界的","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":31}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"参考","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":32}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"信息","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":33}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":34}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"以便","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":35}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"更好地","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":36}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"完善","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":37}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"这个","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":38}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"设定","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":39}
|
||||
|
||||
event: response.output_text.delta
|
||||
data: {"type":"response.output_text.delta","content_index":0,"delta":"。\n\n","item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"sequence_number":40}
|
||||
|
||||
event: response.output_text.done
|
||||
data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"text":"我需要基于当前的玩具王国设定,为用户补全剩余的设定内容。让我先搜索一些关于玩具王国、童话设定、儿童奇幻世界的参考信息,以便更好地完善这个设定。\n\n","sequence_number":41}
|
||||
|
||||
event: response.content_part.done
|
||||
data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581","output_index":0,"part":{"type":"output_text","text":"我需要基于当前的玩具王国设定,为用户补全剩余的设定内容。让我先搜索一些关于玩具王国、童话设定、儿童奇幻世界的参考信息,以便更好地完善这个设定。\n\n"},"sequence_number":42}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要基于当前的玩具王国设定,为用户补全剩余的设定内容。让我先搜索一些关于玩具王国、童话设定、儿童奇幻世界的参考信息,以便更好地完善这个设定。\n\n"}],"status":"completed","id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581"},"sequence_number":43}
|
||||
|
||||
event: response.output_item.added
|
||||
data: {"type":"response.output_item.added","output_index":1,"item":{"type":"web_search_call","status":"in_progress","id":"ws_02177764484182900000000000000000000ffffac151e4ca524c5"},"sequence_number":44}
|
||||
|
||||
event: response.web_search_call.in_progress
|
||||
data: {"type":"response.web_search_call.in_progress","item_id":"ws_02177764484182900000000000000000000ffffac151e4ca524c5","output_index":1,"sequence_number":45}
|
||||
|
||||
event: response.web_search_call.searching
|
||||
data: {"type":"response.web_search_call.searching","item_id":"ws_02177764484182900000000000000000000ffffac151e4ca524c5","output_index":1,"sequence_number":46}
|
||||
|
||||
event: response.web_search_call.completed
|
||||
data: {"type":"response.web_search_call.completed","item_id":"ws_02177764484182900000000000000000000ffffac151e4ca524c5","output_index":1,"sequence_number":47}
|
||||
|
||||
event: response.output_item.done
|
||||
data: {"type":"response.output_item.done","output_index":1,"item":{"type":"web_search_call","action":{"query":"玩具王国 童话设定;儿童奇幻世界 游戏设定;玩具主题 RPG 游戏","type":"search"},"status":"completed","id":"ws_02177764484182900000000000000000000ffffac151e4ca524c5"},"sequence_number":48}
|
||||
|
||||
event: error
|
||||
data: {"type":"error","code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin","param":"","sequence_number":49}
|
||||
|
||||
event: response.failed
|
||||
data: {"type":"response.failed","response":{"created_at":1777644839,"error":{"code":"ToolNotOpen","message":"Your account has not activated web search. You may activate it at https://console.volcengine.com/common-buy/CC_content_plugin"},"id":"resp_02177764483844428a04e35a69bbadd2802274f74f3d7f05d787b","max_output_tokens":32768,"model":"deepseek-v3-2-251201","object":"response","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"我需要基于当前的玩具王国设定,为用户补全剩余的设定内容。让我先搜索一些关于玩具王国、童话设定、儿童奇幻世界的参考信息,以便更好地完善这个设定。\n\n"}],"status":"completed","id":"msg_02177764483994400000000000000000000ffffac151e4c1d2581"},{"type":"web_search_call","action":{"query":"玩具王国 童话设定;儿童奇幻世界 游戏设定;玩具主题 RPG 游戏","type":"search"},"status":"completed","id":"ws_02177764484182900000000000000000000ffffac151e4ca524c5"}],"service_tier":"default","status":"failed","tools":[{"type":"web_search","max_keyword":3}],"usage":{"input_tokens":2173,"output_tokens":130,"total_tokens":2303,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0}},"caching":{"type":"disabled"},"store":true,"expire_at":1777904038},"sequence_number":50}
|
||||
|
||||
data: [DONE]
|
||||
|
||||
@@ -46,6 +46,7 @@ export type PuzzleAgentActionRequest =
|
||||
workTitle?: string;
|
||||
workDescription?: string;
|
||||
pictureDescription?: string;
|
||||
imageModel?: string | null;
|
||||
}
|
||||
| {
|
||||
action: 'compile_puzzle_draft';
|
||||
@@ -54,6 +55,7 @@ export type PuzzleAgentActionRequest =
|
||||
workDescription?: string;
|
||||
pictureDescription?: string;
|
||||
referenceImageSrc?: string | null;
|
||||
imageModel?: string | null;
|
||||
candidateCount?: number;
|
||||
}
|
||||
| {
|
||||
@@ -61,6 +63,7 @@ export type PuzzleAgentActionRequest =
|
||||
levelId?: string | null;
|
||||
promptText?: string | null;
|
||||
referenceImageSrc?: string | null;
|
||||
imageModel?: string | null;
|
||||
candidateCount?: number;
|
||||
levelsJson?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { PuzzleAgentActionResponse, PuzzleAgentSuggestedAction } from './puzzleAgentActions';
|
||||
import type {
|
||||
PuzzleAgentActionResponse,
|
||||
PuzzleAgentSuggestedAction,
|
||||
} from './puzzleAgentActions';
|
||||
import type { PuzzleAnchorPack, PuzzleResultDraft } from './puzzleAgentDraft';
|
||||
import type { PuzzleResultPreviewEnvelope } from './puzzleResultPreview';
|
||||
|
||||
@@ -47,6 +50,7 @@ export interface CreatePuzzleAgentSessionRequest {
|
||||
workDescription?: string;
|
||||
pictureDescription?: string;
|
||||
referenceImageSrc?: string | null;
|
||||
imageModel?: string | null;
|
||||
}
|
||||
|
||||
export interface CreatePuzzleAgentSessionResponse {
|
||||
@@ -59,6 +63,7 @@ export interface SendPuzzleAgentMessageRequest {
|
||||
quickFillRequested?: boolean;
|
||||
}
|
||||
|
||||
export interface SendPuzzleAgentMessageResponse extends PuzzleAgentActionResponse {
|
||||
export interface SendPuzzleAgentMessageResponse
|
||||
extends PuzzleAgentActionResponse {
|
||||
session: PuzzleAgentSessionSnapshot;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@ pub struct AppConfig {
|
||||
pub dashscope_reference_image_model: String,
|
||||
pub dashscope_cover_image_model: String,
|
||||
pub dashscope_image_request_timeout_ms: u64,
|
||||
pub apimart_base_url: String,
|
||||
pub apimart_api_key: Option<String>,
|
||||
pub apimart_image_request_timeout_ms: u64,
|
||||
pub draft_asset_generation_max_concurrent_requests: usize,
|
||||
pub ark_character_video_base_url: String,
|
||||
pub ark_character_video_api_key: Option<String>,
|
||||
@@ -182,6 +185,9 @@ impl Default for AppConfig {
|
||||
dashscope_reference_image_model: "qwen-image-2.0".to_string(),
|
||||
dashscope_cover_image_model: "wan2.2-t2i-flash".to_string(),
|
||||
dashscope_image_request_timeout_ms: 150_000,
|
||||
apimart_base_url: "https://api.apimart.ai/v1".to_string(),
|
||||
apimart_api_key: None,
|
||||
apimart_image_request_timeout_ms: 180_000,
|
||||
draft_asset_generation_max_concurrent_requests: 4,
|
||||
ark_character_video_base_url: DEFAULT_ARK_BASE_URL.to_string(),
|
||||
ark_character_video_api_key: None,
|
||||
@@ -530,6 +536,18 @@ impl AppConfig {
|
||||
config.dashscope_image_request_timeout_ms = dashscope_image_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(apimart_base_url) = read_first_non_empty_env(&["APIMART_BASE_URL"]) {
|
||||
config.apimart_base_url = apimart_base_url;
|
||||
}
|
||||
|
||||
config.apimart_api_key = read_first_non_empty_env(&["APIMART_API_KEY"]);
|
||||
|
||||
if let Some(apimart_image_request_timeout_ms) =
|
||||
read_first_positive_u64_env(&["APIMART_IMAGE_REQUEST_TIMEOUT_MS"])
|
||||
{
|
||||
config.apimart_image_request_timeout_ms = apimart_image_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(max_concurrent_requests) = read_first_usize_env(&[
|
||||
"GENARRATIVE_DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS",
|
||||
"DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmStreamDelta, LlmTextRequest};
|
||||
use platform_llm::{LlmClient, LlmError, LlmMessage, LlmStreamDelta, LlmTextRequest};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
||||
@@ -33,10 +33,63 @@ where
|
||||
{
|
||||
let llm_client =
|
||||
llm_client.ok_or_else(|| build_error(messages.model_unavailable.to_string()))?;
|
||||
let user_prompt = user_prompt.into();
|
||||
let turn_output = match request_stream_creation_agent_json_turn(
|
||||
llm_client,
|
||||
system_prompt.clone(),
|
||||
user_prompt.clone(),
|
||||
enable_web_search,
|
||||
&mut on_reply_update,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(turn_output) => Ok(turn_output),
|
||||
Err(CreationAgentJsonTurnFailure::Stream(error))
|
||||
if enable_web_search && is_web_search_tool_unavailable(&error) =>
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %error,
|
||||
"创作 Agent 联网搜索插件不可用,自动降级为无联网搜索重试"
|
||||
);
|
||||
request_stream_creation_agent_json_turn(
|
||||
llm_client,
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
false,
|
||||
&mut on_reply_update,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
|
||||
turn_output.map_err(|error| match error {
|
||||
CreationAgentJsonTurnFailure::Stream(_) => {
|
||||
build_error(messages.generation_failed.to_string())
|
||||
}
|
||||
CreationAgentJsonTurnFailure::Parse => build_error(messages.parse_failed.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
enum CreationAgentJsonTurnFailure {
|
||||
Stream(LlmError),
|
||||
Parse,
|
||||
}
|
||||
|
||||
async fn request_stream_creation_agent_json_turn<F>(
|
||||
llm_client: &LlmClient,
|
||||
system_prompt: String,
|
||||
user_prompt: String,
|
||||
enable_web_search: bool,
|
||||
on_reply_update: &mut F,
|
||||
) -> Result<CreationAgentJsonTurnOutput, CreationAgentJsonTurnFailure>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
let mut latest_reply_text = String::new();
|
||||
let response = llm_client
|
||||
.stream_text(
|
||||
build_creation_agent_llm_request(system_prompt, user_prompt.into(), enable_web_search),
|
||||
build_creation_agent_llm_request(system_prompt, user_prompt, enable_web_search),
|
||||
|delta: &LlmStreamDelta| {
|
||||
if let Some(reply_progress) =
|
||||
extract_reply_text_from_partial_json(delta.accumulated_text.as_str())
|
||||
@@ -48,9 +101,9 @@ where
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| build_error(messages.generation_failed.to_string()))?;
|
||||
.map_err(CreationAgentJsonTurnFailure::Stream)?;
|
||||
let parsed = parse_json_response_text(response.content.as_str())
|
||||
.map_err(|_| build_error(messages.parse_failed.to_string()))?;
|
||||
.map_err(|_| CreationAgentJsonTurnFailure::Parse)?;
|
||||
let reply_text = read_reply_text(&parsed);
|
||||
if let Some(reply_text) = reply_text.as_deref()
|
||||
&& reply_text != latest_reply_text
|
||||
@@ -61,6 +114,13 @@ where
|
||||
Ok(CreationAgentJsonTurnOutput { parsed })
|
||||
}
|
||||
|
||||
fn is_web_search_tool_unavailable(error: &LlmError) -> bool {
|
||||
let message = error.to_string();
|
||||
message.contains("ToolNotOpen")
|
||||
|| message.contains("has not activated web search")
|
||||
|| message.contains("未开通")
|
||||
}
|
||||
|
||||
fn build_creation_agent_llm_request(
|
||||
system_prompt: String,
|
||||
user_prompt: String,
|
||||
@@ -168,11 +228,23 @@ fn read_reply_text(parsed: &JsonValue) -> Option<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
net::TcpListener,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::{Duration as StdDuration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use platform_llm::{LlmConfig, LlmProvider};
|
||||
|
||||
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
|
||||
|
||||
use super::{
|
||||
build_creation_agent_llm_request, extract_reply_text_from_partial_json,
|
||||
parse_json_response_text,
|
||||
CreationAgentLlmTurnErrorMessages, build_creation_agent_llm_request,
|
||||
extract_reply_text_from_partial_json, is_web_search_tool_unavailable,
|
||||
parse_json_response_text, stream_creation_agent_json_turn,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -202,4 +274,214 @@ mod tests {
|
||||
assert_eq!(request.protocol, platform_llm::LlmTextProtocol::Responses);
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_upstream_web_search_tool_unavailable_error() {
|
||||
let error = platform_llm::LlmError::Upstream {
|
||||
status_code: 502,
|
||||
message: "Your account has not activated web search. code=ToolNotOpen".to_string(),
|
||||
};
|
||||
|
||||
assert!(is_web_search_tool_unavailable(&error));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream_turn_retries_without_web_search_when_tool_is_unavailable() {
|
||||
let log_dir = std::env::temp_dir().join(format!(
|
||||
"api-server-creation-agent-raw-log-test-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time should be after epoch")
|
||||
.as_nanos()
|
||||
));
|
||||
unsafe {
|
||||
std::env::set_var("LLM_RAW_LOG_DIR", &log_dir);
|
||||
}
|
||||
let success_json = serde_json::json!({
|
||||
"replyText": "好,我们先把玩具王国定住。",
|
||||
"progressPercent": 12,
|
||||
"nextAnchorContent": {
|
||||
"worldPromise": "玩具王国初步方向",
|
||||
"playerFantasy": null,
|
||||
"themeBoundary": null,
|
||||
"playerEntryPoint": null,
|
||||
"coreConflict": null,
|
||||
"keyRelationships": null,
|
||||
"hiddenLines": null,
|
||||
"iconicElements": null
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let server = spawn_capturing_mock_server(vec![
|
||||
MockResponse {
|
||||
body: concat!(
|
||||
"data: {\"type\":\"error\",\"code\":\"ToolNotOpen\",\"message\":\"Your account has not activated web search.\"}\n\n",
|
||||
"data: [DONE]\n\n"
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
MockResponse {
|
||||
body: format!(
|
||||
"data: {}\n\n",
|
||||
serde_json::json!({
|
||||
"type": "response.output_text.delta",
|
||||
"delta": success_json
|
||||
})
|
||||
) + "data: {\"type\":\"response.completed\"}\n\n",
|
||||
},
|
||||
]);
|
||||
let config = LlmConfig::new(
|
||||
LlmProvider::Ark,
|
||||
server.base_url,
|
||||
"test-key".to_string(),
|
||||
"test-model".to_string(),
|
||||
30_000,
|
||||
0,
|
||||
1,
|
||||
)
|
||||
.expect("LLM config should build");
|
||||
let llm_client = platform_llm::LlmClient::new(config).expect("LLM client should build");
|
||||
let mut visible_replies = Vec::new();
|
||||
|
||||
let output = stream_creation_agent_json_turn(
|
||||
Some(&llm_client),
|
||||
"系统提示".to_string(),
|
||||
"用户提示",
|
||||
true,
|
||||
CreationAgentLlmTurnErrorMessages {
|
||||
model_unavailable: "模型不可用",
|
||||
generation_failed: "生成失败",
|
||||
parse_failed: "解析失败",
|
||||
},
|
||||
|text| visible_replies.push(text.to_string()),
|
||||
|message| message,
|
||||
)
|
||||
.await
|
||||
.expect("web search fallback should succeed");
|
||||
|
||||
assert_eq!(
|
||||
output.parsed["replyText"].as_str(),
|
||||
Some("好,我们先把玩具王国定住。")
|
||||
);
|
||||
assert_eq!(visible_replies, vec!["好,我们先把玩具王国定住。"]);
|
||||
|
||||
let requests = server.requests.lock().expect("requests lock").clone();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert!(requests[0].contains("\"tools\""));
|
||||
assert!(requests[0].contains("\"web_search\""));
|
||||
assert!(!requests[1].contains("\"tools\""));
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("LLM_RAW_LOG_DIR");
|
||||
}
|
||||
if log_dir.exists() {
|
||||
fs::remove_dir_all(log_dir).expect("temporary LLM raw log dir should be removed");
|
||||
}
|
||||
}
|
||||
|
||||
struct MockResponse {
|
||||
body: String,
|
||||
}
|
||||
|
||||
struct CapturingMockServer {
|
||||
base_url: String,
|
||||
requests: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
fn spawn_capturing_mock_server(responses: Vec<MockResponse>) -> CapturingMockServer {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
|
||||
let address = listener.local_addr().expect("listener should have addr");
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let requests_for_thread = Arc::clone(&requests);
|
||||
|
||||
thread::spawn(move || {
|
||||
for response in responses {
|
||||
let (mut stream, _) = listener.accept().expect("request should connect");
|
||||
let request_text = read_request(&mut stream);
|
||||
requests_for_thread
|
||||
.lock()
|
||||
.expect("requests lock")
|
||||
.push(request_text);
|
||||
write_sse_response(&mut stream, response);
|
||||
}
|
||||
});
|
||||
|
||||
CapturingMockServer {
|
||||
base_url: format!("http://{address}"),
|
||||
requests,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_request(stream: &mut std::net::TcpStream) -> String {
|
||||
stream
|
||||
.set_read_timeout(Some(StdDuration::from_secs(1)))
|
||||
.expect("read timeout should be set");
|
||||
let mut buffer = Vec::new();
|
||||
let mut chunk = [0_u8; 1024];
|
||||
let mut expected_total = None;
|
||||
|
||||
loop {
|
||||
match stream.read(&mut chunk) {
|
||||
Ok(0) => break,
|
||||
Ok(bytes_read) => {
|
||||
buffer.extend_from_slice(&chunk[..bytes_read]);
|
||||
|
||||
if expected_total.is_none()
|
||||
&& let Some(header_end) = find_header_end(&buffer)
|
||||
{
|
||||
let content_length =
|
||||
read_content_length(&buffer[..header_end]).unwrap_or(0);
|
||||
expected_total = Some(header_end + content_length);
|
||||
}
|
||||
|
||||
if let Some(total_bytes) = expected_total
|
||||
&& buffer.len() >= total_bytes
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error)
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| error.kind() == std::io::ErrorKind::TimedOut =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Err(error) => panic!("mock server failed to read request: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
String::from_utf8_lossy(buffer.as_slice()).to_string()
|
||||
}
|
||||
|
||||
fn write_sse_response(stream: &mut std::net::TcpStream, response: MockResponse) {
|
||||
let raw_response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
response.body.len(),
|
||||
response.body
|
||||
);
|
||||
|
||||
stream
|
||||
.write_all(raw_response.as_bytes())
|
||||
.expect("mock response should be written");
|
||||
stream.flush().expect("mock response should flush");
|
||||
}
|
||||
|
||||
fn find_header_end(buffer: &[u8]) -> Option<usize> {
|
||||
buffer
|
||||
.windows(4)
|
||||
.position(|window| window == b"\r\n\r\n")
|
||||
.map(|index| index + 4)
|
||||
}
|
||||
|
||||
fn read_content_length(headers: &[u8]) -> Option<usize> {
|
||||
let text = String::from_utf8_lossy(headers);
|
||||
text.lines().find_map(|line| {
|
||||
let (name, value) = line.split_once(':')?;
|
||||
if name.eq_ignore_ascii_case("content-length") {
|
||||
return value.trim().parse::<usize>().ok();
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,13 @@ const PUZZLE_WORKS_PROVIDER: &str = "puzzle-works";
|
||||
const PUZZLE_GALLERY_PROVIDER: &str = "puzzle-gallery";
|
||||
const PUZZLE_RUNTIME_PROVIDER: &str = "puzzle-runtime";
|
||||
const PUZZLE_TEXT_TO_IMAGE_MODEL: &str = "wan2.2-t2i-flash";
|
||||
const PUZZLE_IMAGE_MODEL_ORIGINAL: &str = "original";
|
||||
const PUZZLE_IMAGE_MODEL_GPT_IMAGE_2: &str = "gpt-image-2";
|
||||
const PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW: &str = "gemini-3.1-flash-image-preview";
|
||||
const PUZZLE_ENTITY_KIND: &str = "puzzle_work";
|
||||
const PUZZLE_GENERATED_IMAGE_SIZE: &str = "1024*1024";
|
||||
const PUZZLE_APIMART_GENERATED_IMAGE_SIZE: &str = "1:1";
|
||||
const PUZZLE_APIMART_GEMINI_RESOLUTION: &str = "1K";
|
||||
|
||||
pub async fn create_puzzle_agent_session(
|
||||
State(state): State<AppState>,
|
||||
@@ -463,6 +468,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
session_id = %session_id,
|
||||
owner_user_id = %owner_user_id,
|
||||
action = %action,
|
||||
image_model = payload.image_model.as_deref().unwrap_or(PUZZLE_IMAGE_MODEL_ORIGINAL),
|
||||
prompt_chars = payload
|
||||
.prompt_text
|
||||
.as_deref()
|
||||
@@ -508,6 +514,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
owner_user_id.clone(),
|
||||
prompt_text,
|
||||
payload.reference_image_src.as_deref(),
|
||||
payload.image_model.as_deref(),
|
||||
now,
|
||||
)
|
||||
.await
|
||||
@@ -627,6 +634,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
&target_level.level_name,
|
||||
&prompt,
|
||||
payload.reference_image_src.as_deref(),
|
||||
payload.image_model.as_deref(),
|
||||
candidate_count,
|
||||
candidate_start_index,
|
||||
)
|
||||
@@ -2406,6 +2414,7 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
owner_user_id: String,
|
||||
prompt_text: Option<&str>,
|
||||
reference_image_src: Option<&str>,
|
||||
image_model: Option<&str>,
|
||||
now: i64,
|
||||
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||
let compiled_session = state
|
||||
@@ -2431,6 +2440,7 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
&target_level.level_name,
|
||||
&image_prompt,
|
||||
reference_image_src,
|
||||
image_model,
|
||||
1,
|
||||
target_level.candidates.len(),
|
||||
)
|
||||
@@ -2544,7 +2554,12 @@ fn is_missing_puzzle_form_draft_procedure_error(error: &SpacetimeClientError) ->
|
||||
|
||||
fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
let message = error.to_string();
|
||||
let provider = if message.contains("DashScope") || message.contains("dashscope") {
|
||||
let provider = if message.contains("APIMart")
|
||||
|| message.contains("apimart")
|
||||
|| message.contains("APIMART")
|
||||
{
|
||||
"apimart"
|
||||
} else if message.contains("DashScope") || message.contains("dashscope") {
|
||||
"dashscope"
|
||||
} else if message.contains("OSS") || message.contains("oss") || message.contains("参考图") {
|
||||
"puzzle-assets"
|
||||
@@ -2556,6 +2571,9 @@ fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
|| message.contains("上游")
|
||||
|| message.contains("DashScope")
|
||||
|| message.contains("dashscope")
|
||||
|| message.contains("APIMart")
|
||||
|| message.contains("apimart")
|
||||
|| message.contains("APIMART")
|
||||
|| message.contains("参考图")
|
||||
|| message.contains("图片")
|
||||
|| message.contains("OSS")
|
||||
@@ -2648,17 +2666,18 @@ async fn generate_puzzle_image_candidates(
|
||||
level_name: &str,
|
||||
prompt: &str,
|
||||
reference_image_src: Option<&str>,
|
||||
image_model: Option<&str>,
|
||||
candidate_count: u32,
|
||||
candidate_start_index: usize,
|
||||
) -> Result<Vec<PuzzleGeneratedImageCandidateRecord>, String> {
|
||||
let count = candidate_count.clamp(1, 1);
|
||||
let settings =
|
||||
require_puzzle_dashscope_settings(state).map_err(map_puzzle_generation_app_error)?;
|
||||
let http_client =
|
||||
build_puzzle_dashscope_http_client(&settings).map_err(map_puzzle_generation_app_error)?;
|
||||
let resolved_model = resolve_puzzle_image_model(image_model);
|
||||
let actual_prompt = build_puzzle_image_prompt(level_name, prompt);
|
||||
let http_client = build_puzzle_image_http_client(state, resolved_model)
|
||||
.map_err(map_puzzle_generation_app_error)?;
|
||||
tracing::info!(
|
||||
provider = "dashscope",
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
prompt_chars = prompt.chars().count(),
|
||||
@@ -2680,29 +2699,50 @@ async fn generate_puzzle_image_candidates(
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
// 中文注释:SpacetimeDB reducer 不能做外部 I/O,参考图读取与 DashScope 图生图都必须停留在 api-server。
|
||||
// 中文注释:SpacetimeDB reducer 不能做外部 I/O,参考图读取与外部生图都必须停留在 api-server。
|
||||
// 中文注释:拼图作品资产统一按 1:1 正方形生成,前端运行时也按正方形棋盘切块承载。
|
||||
let generated = match reference_image.as_deref() {
|
||||
Some(reference_image) => {
|
||||
create_puzzle_image_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image,
|
||||
)
|
||||
.await
|
||||
let generated = match resolved_model {
|
||||
PuzzleImageModel::Original => {
|
||||
let settings = require_puzzle_dashscope_settings(state)
|
||||
.map_err(map_puzzle_generation_app_error)?;
|
||||
match reference_image.as_deref() {
|
||||
Some(reference_image) => {
|
||||
create_puzzle_image_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
create_puzzle_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
create_puzzle_text_to_image_generation(
|
||||
PuzzleImageModel::GptImage2 | PuzzleImageModel::Gemini31FlashPreview => {
|
||||
let settings =
|
||||
require_puzzle_apimart_settings(state).map_err(map_puzzle_generation_app_error)?;
|
||||
create_puzzle_apimart_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
resolved_model,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_GENERATED_IMAGE_SIZE,
|
||||
PUZZLE_APIMART_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2733,7 +2773,7 @@ async fn generate_puzzle_image_candidates(
|
||||
asset_id: asset.asset_id,
|
||||
prompt: prompt.to_string(),
|
||||
actual_prompt: Some(actual_prompt.clone()),
|
||||
source_type: "generated".to_string(),
|
||||
source_type: resolved_model.candidate_source_type().to_string(),
|
||||
// 单图生成结果总是直接成为当前正式图。
|
||||
selected: index == 0,
|
||||
});
|
||||
@@ -2823,6 +2863,7 @@ async fn build_local_next_puzzle_run(
|
||||
&draft.level_name,
|
||||
&draft.summary,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
draft.candidates.len(),
|
||||
)
|
||||
@@ -3577,6 +3618,7 @@ mod tests {
|
||||
#[test]
|
||||
fn puzzle_generated_image_size_is_square_1_1() {
|
||||
assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "1024*1024");
|
||||
assert_eq!(PUZZLE_APIMART_GENERATED_IMAGE_SIZE, "1:1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3598,6 +3640,30 @@ mod tests {
|
||||
assert_eq!(body["parameters"]["n"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_apimart_request_uses_selected_model_and_reference_images() {
|
||||
let body = build_puzzle_apimart_image_request_body(
|
||||
PuzzleImageModel::Gemini31FlashPreview,
|
||||
"一只猫在雨夜灯牌下回头。",
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_APIMART_GENERATED_IMAGE_SIZE,
|
||||
4,
|
||||
Some("data:image/png;base64,abcd"),
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW);
|
||||
assert_eq!(body["size"], PUZZLE_APIMART_GENERATED_IMAGE_SIZE);
|
||||
assert_eq!(body["resolution"], PUZZLE_APIMART_GEMINI_RESOLUTION);
|
||||
assert_eq!(body["n"], 1);
|
||||
assert_eq!(body["image_urls"][0], "data:image/png;base64,abcd");
|
||||
assert!(
|
||||
body["prompt"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("文字水印")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_dashscope_upstream_error_keeps_status_and_raw_excerpt() {
|
||||
let error = map_puzzle_dashscope_upstream_error(
|
||||
@@ -3639,6 +3705,44 @@ struct PuzzleDashScopeSettings {
|
||||
request_timeout_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum PuzzleImageModel {
|
||||
Original,
|
||||
GptImage2,
|
||||
Gemini31FlashPreview,
|
||||
}
|
||||
|
||||
impl PuzzleImageModel {
|
||||
fn provider_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Original => "dashscope",
|
||||
Self::GptImage2 | Self::Gemini31FlashPreview => "apimart",
|
||||
}
|
||||
}
|
||||
|
||||
fn request_model_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Original => PUZZLE_TEXT_TO_IMAGE_MODEL,
|
||||
Self::GptImage2 => PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
|
||||
Self::Gemini31FlashPreview => PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW,
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_source_type(self) -> &'static str {
|
||||
match self {
|
||||
Self::Original => "generated",
|
||||
Self::GptImage2 => "generated:gpt-image-2",
|
||||
Self::Gemini31FlashPreview => "generated:nanobanana2",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PuzzleApimartSettings {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
request_timeout_ms: u64,
|
||||
}
|
||||
|
||||
struct PuzzleGeneratedImages {
|
||||
task_id: String,
|
||||
images: Vec<PuzzleDownloadedImage>,
|
||||
@@ -3694,16 +3798,69 @@ fn require_puzzle_dashscope_settings(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_puzzle_dashscope_http_client(
|
||||
settings: &PuzzleDashScopeSettings,
|
||||
fn resolve_puzzle_image_model(value: Option<&str>) -> PuzzleImageModel {
|
||||
match value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(PUZZLE_IMAGE_MODEL_ORIGINAL)
|
||||
{
|
||||
PUZZLE_IMAGE_MODEL_GPT_IMAGE_2 => PuzzleImageModel::GptImage2,
|
||||
PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW => PuzzleImageModel::Gemini31FlashPreview,
|
||||
_ => PuzzleImageModel::Original,
|
||||
}
|
||||
}
|
||||
|
||||
fn require_puzzle_apimart_settings(state: &AppState) -> Result<PuzzleApimartSettings, AppError> {
|
||||
let base_url = state.config.apimart_base_url.trim().trim_end_matches('/');
|
||||
if base_url.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"reason": "APIMART_BASE_URL 未配置",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
let api_key = state
|
||||
.config
|
||||
.apimart_api_key
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"reason": "APIMART_API_KEY 未配置",
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(PuzzleApimartSettings {
|
||||
base_url: base_url.to_string(),
|
||||
api_key: api_key.to_string(),
|
||||
request_timeout_ms: state.config.apimart_image_request_timeout_ms.max(1),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_puzzle_image_http_client(
|
||||
state: &AppState,
|
||||
image_model: PuzzleImageModel,
|
||||
) -> Result<reqwest::Client, AppError> {
|
||||
let (provider, request_timeout_ms) = match image_model {
|
||||
PuzzleImageModel::Original => {
|
||||
("dashscope", state.config.dashscope_image_request_timeout_ms)
|
||||
}
|
||||
PuzzleImageModel::GptImage2 | PuzzleImageModel::Gemini31FlashPreview => {
|
||||
("apimart", state.config.apimart_image_request_timeout_ms)
|
||||
}
|
||||
};
|
||||
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(settings.request_timeout_ms))
|
||||
.timeout(Duration::from_millis(request_timeout_ms.max(1)))
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": "dashscope",
|
||||
"message": format!("构造拼图 DashScope HTTP 客户端失败:{error}"),
|
||||
"provider": provider,
|
||||
"message": format!("构造拼图图片生成 HTTP 客户端失败:{error}"),
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -3866,6 +4023,229 @@ fn build_puzzle_text_to_image_request_body(
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_puzzle_apimart_image_generation(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &PuzzleApimartSettings,
|
||||
image_model: PuzzleImageModel,
|
||||
prompt: &str,
|
||||
negative_prompt: &str,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_image: Option<&str>,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let request_body = build_puzzle_apimart_image_request_body(
|
||||
image_model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
candidate_count,
|
||||
reference_image,
|
||||
);
|
||||
let response = http_client
|
||||
.post(format!("{}/images/generations", settings.base_url))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("Bearer {}", settings.api_key),
|
||||
)
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!("创建拼图 APIMart 图片生成任务失败:{error}"))
|
||||
})?;
|
||||
let status = response.status();
|
||||
let response_text = response.text().await.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!("读取拼图 APIMart 图片生成响应失败:{error}"))
|
||||
})?;
|
||||
if !status.is_success() {
|
||||
return Err(map_puzzle_apimart_upstream_error(
|
||||
status,
|
||||
response_text.as_str(),
|
||||
"创建拼图 APIMart 图片生成任务失败",
|
||||
));
|
||||
}
|
||||
|
||||
let payload =
|
||||
parse_puzzle_json_payload(response_text.as_str(), "解析拼图 APIMart 图片生成响应失败")?;
|
||||
let image_urls = extract_puzzle_image_urls(&payload);
|
||||
if !image_urls.is_empty() {
|
||||
return download_puzzle_images_from_urls(
|
||||
http_client,
|
||||
format!("apimart-{}", current_utc_micros()),
|
||||
image_urls,
|
||||
candidate_count,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let task_id = extract_puzzle_task_id(&payload).ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "拼图 APIMart 图片生成未返回 task_id 或图片地址",
|
||||
}))
|
||||
})?;
|
||||
|
||||
wait_puzzle_apimart_generated_images(
|
||||
http_client,
|
||||
settings,
|
||||
task_id.as_str(),
|
||||
candidate_count,
|
||||
"拼图 APIMart 图片生成任务失败",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn build_puzzle_apimart_image_request_body(
|
||||
image_model: PuzzleImageModel,
|
||||
prompt: &str,
|
||||
negative_prompt: &str,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_image: Option<&str>,
|
||||
) -> Value {
|
||||
let mut body = Map::from_iter([
|
||||
(
|
||||
"model".to_string(),
|
||||
Value::String(image_model.request_model_name().to_string()),
|
||||
),
|
||||
(
|
||||
"prompt".to_string(),
|
||||
Value::String(build_puzzle_apimart_prompt(prompt, negative_prompt)),
|
||||
),
|
||||
("n".to_string(), json!(candidate_count.clamp(1, 1))),
|
||||
("size".to_string(), Value::String(size.to_string())),
|
||||
]);
|
||||
body.insert(
|
||||
"resolution".to_string(),
|
||||
Value::String(
|
||||
match image_model {
|
||||
PuzzleImageModel::Gemini31FlashPreview => PUZZLE_APIMART_GEMINI_RESOLUTION,
|
||||
_ => "1k",
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
|
||||
if let Some(reference_image) = reference_image
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
body.insert("image_urls".to_string(), json!([reference_image]));
|
||||
}
|
||||
|
||||
Value::Object(body)
|
||||
}
|
||||
|
||||
fn build_puzzle_apimart_prompt(prompt: &str, negative_prompt: &str) -> String {
|
||||
let prompt = prompt.trim();
|
||||
let negative_prompt = negative_prompt.trim();
|
||||
if negative_prompt.is_empty() {
|
||||
return prompt.to_string();
|
||||
}
|
||||
|
||||
format!("{prompt}\n避免:{negative_prompt}")
|
||||
}
|
||||
|
||||
async fn wait_puzzle_apimart_generated_images(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &PuzzleApimartSettings,
|
||||
task_id: &str,
|
||||
candidate_count: u32,
|
||||
failure_message: &str,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let deadline = Instant::now() + Duration::from_millis(settings.request_timeout_ms);
|
||||
|
||||
while Instant::now() < deadline {
|
||||
let poll_response = http_client
|
||||
.get(format!("{}/tasks/{}", settings.base_url, task_id))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("Bearer {}", settings.api_key),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!(
|
||||
"查询拼图 APIMart 图片生成任务失败:{error}"
|
||||
))
|
||||
})?;
|
||||
let poll_status = poll_response.status();
|
||||
let poll_text = poll_response.text().await.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!(
|
||||
"读取拼图 APIMart 图片生成任务响应失败:{error}"
|
||||
))
|
||||
})?;
|
||||
if !poll_status.is_success() {
|
||||
return Err(map_puzzle_apimart_upstream_error(
|
||||
poll_status,
|
||||
poll_text.as_str(),
|
||||
"查询拼图 APIMart 图片生成任务失败",
|
||||
));
|
||||
}
|
||||
|
||||
let poll_payload =
|
||||
parse_puzzle_json_payload(poll_text.as_str(), "解析拼图 APIMart 图片生成任务响应失败")?;
|
||||
let task_status = find_first_puzzle_string_by_key(&poll_payload, "status")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if matches!(task_status.as_str(), "completed" | "succeeded" | "success") {
|
||||
let image_urls = extract_puzzle_image_urls(&poll_payload);
|
||||
if image_urls.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "拼图 APIMart 图片生成成功但未返回图片地址",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return download_puzzle_images_from_urls(
|
||||
http_client,
|
||||
task_id.to_string(),
|
||||
image_urls,
|
||||
candidate_count,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if matches!(
|
||||
task_status.as_str(),
|
||||
"failed" | "error" | "canceled" | "cancelled"
|
||||
) {
|
||||
return Err(map_puzzle_apimart_upstream_error(
|
||||
poll_status,
|
||||
poll_text.as_str(),
|
||||
failure_message,
|
||||
));
|
||||
}
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
|
||||
Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "拼图 APIMart 图片生成超时或未返回图片地址",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
async fn download_puzzle_images_from_urls(
|
||||
http_client: &reqwest::Client,
|
||||
task_id: String,
|
||||
image_urls: Vec<String>,
|
||||
candidate_count: u32,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let mut images = Vec::with_capacity(candidate_count.clamp(1, 1) as usize);
|
||||
for image_url in image_urls
|
||||
.into_iter()
|
||||
.take(candidate_count.clamp(1, 1) as usize)
|
||||
{
|
||||
images.push(download_puzzle_remote_image(http_client, image_url.as_str()).await?);
|
||||
}
|
||||
Ok(PuzzleGeneratedImages { task_id, images })
|
||||
}
|
||||
|
||||
async fn resolve_puzzle_reference_image_as_data_url(
|
||||
state: &AppState,
|
||||
http_client: &reqwest::Client,
|
||||
@@ -4427,6 +4807,36 @@ fn map_puzzle_dashscope_upstream_error(
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_puzzle_apimart_request_error(message: String) -> AppError {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_puzzle_apimart_upstream_error(
|
||||
upstream_status: reqwest::StatusCode,
|
||||
raw_text: &str,
|
||||
fallback_message: &str,
|
||||
) -> AppError {
|
||||
let message = parse_puzzle_api_error_message(raw_text, fallback_message);
|
||||
let raw_excerpt = trim_puzzle_upstream_excerpt(raw_text, 800);
|
||||
tracing::warn!(
|
||||
provider = "apimart",
|
||||
upstream_status = upstream_status.as_u16(),
|
||||
message = %message,
|
||||
raw_excerpt = %raw_excerpt,
|
||||
"拼图 APIMart 上游请求失败"
|
||||
);
|
||||
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"upstreamStatus": upstream_status.as_u16(),
|
||||
"message": message,
|
||||
"rawExcerpt": raw_excerpt,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_puzzle_api_error_message(raw_text: &str, fallback_message: &str) -> String {
|
||||
let trimmed = raw_text.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
||||
@@ -2540,8 +2540,7 @@ fn format_rfc3339(value: OffsetDateTime) -> Result<String, String> {
|
||||
}
|
||||
|
||||
fn current_auth_user_created_at() -> String {
|
||||
format_rfc3339(OffsetDateTime::now_utc())
|
||||
.unwrap_or_else(|_| default_auth_user_created_at())
|
||||
format_rfc3339(OffsetDateTime::now_utc()).unwrap_or_else(|_| default_auth_user_created_at())
|
||||
}
|
||||
|
||||
fn default_auth_user_created_at() -> String {
|
||||
|
||||
@@ -3465,10 +3465,7 @@ pub fn puzzle_point_incentive_claimable_points(total_half_points: u64, claimed_p
|
||||
.saturating_sub(claimed_points)
|
||||
}
|
||||
|
||||
pub fn puzzle_point_incentive_total_after_spend(
|
||||
total_half_points: u64,
|
||||
spent_points: u64,
|
||||
) -> u64 {
|
||||
pub fn puzzle_point_incentive_total_after_spend(total_half_points: u64, spent_points: u64) -> u64 {
|
||||
total_half_points.saturating_add(spent_points)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct CreatePuzzleAgentSessionRequest {
|
||||
pub picture_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -33,6 +35,8 @@ pub struct ExecutePuzzleAgentActionRequest {
|
||||
#[serde(default)]
|
||||
pub reference_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub image_model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub candidate_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub candidate_id: Option<String>,
|
||||
|
||||
@@ -108,11 +108,10 @@ test('creation hub reflects updated draft title summary and counts after rerende
|
||||
const puzzleButton = screen.getByRole('button', { name: /拼图.*创意礼物/u });
|
||||
const match3dButton = screen.getByRole('button', { name: /抓大鹅/u });
|
||||
expect(
|
||||
puzzleButton.compareDocumentPosition(rpgButton) &
|
||||
rpgButton.compareDocumentPosition(puzzleButton) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
).toBeTruthy();
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(within(rpgButton).getAllByText('敬请期待').length).toBeGreaterThan(0);
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
expect((match3dButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(
|
||||
within(match3dButton).getAllByText('敬请期待').length,
|
||||
|
||||
@@ -43,7 +43,6 @@ test('creation hub draft card renders compiled work summary fields', () => {
|
||||
expect(html).toContain('玩家是失职返乡的守灯人');
|
||||
expect(html).toContain('守灯会与沉船商盟争夺航道解释权');
|
||||
expect(html).toContain('角色扮演');
|
||||
expect(html).toContain('敬请期待');
|
||||
expect(html).toContain('拼图');
|
||||
expect(html).toContain('创意礼物,生活分享');
|
||||
expect(html).not.toContain('大鱼吃小鱼');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import { NEW_WORK_ENTRY_CONFIG } from '../../config/newWorkEntryConfig';
|
||||
import {
|
||||
getVisiblePlatformCreationTypes,
|
||||
type PlatformCreationTypeId,
|
||||
@@ -27,13 +28,15 @@ export function CustomWorldCreationStartCard({
|
||||
<div className="relative z-10 space-y-2.5 sm:space-y-4 xl:space-y-3">
|
||||
<div className="flex items-center justify-between gap-3 xl:items-end">
|
||||
<div className="text-xl font-black leading-none text-white sm:text-3xl xl:text-2xl">
|
||||
新建作品
|
||||
{NEW_WORK_ENTRY_CONFIG.startCard.title}
|
||||
</div>
|
||||
<div className="hidden text-sm leading-6 text-zinc-200/88 sm:block xl:text-xs xl:leading-5">
|
||||
直接选择游戏创作模板,立刻进入对应的共创工作台。
|
||||
{NEW_WORK_ENTRY_CONFIG.startCard.description}
|
||||
</div>
|
||||
<span className="platform-pill platform-pill--neutral shrink-0 border-white/25 bg-white/14 px-2.5 text-xs text-white sm:hidden">
|
||||
{busy ? '正在开启' : '选择模板'}
|
||||
{busy
|
||||
? NEW_WORK_ENTRY_CONFIG.startCard.busyBadge
|
||||
: NEW_WORK_ENTRY_CONFIG.startCard.idleBadge}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import { NEW_WORK_ENTRY_CONFIG } from '../../config/newWorkEntryConfig';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import { getVisiblePlatformCreationTypes } from './platformEntryCreationTypes';
|
||||
|
||||
@@ -86,8 +87,8 @@ export function PlatformEntryCreationTypeModal({
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={isOpen}
|
||||
title="选择创作类型"
|
||||
description="先选玩法类型,再进入对应创作工作台。"
|
||||
title={NEW_WORK_ENTRY_CONFIG.typeModal.title}
|
||||
description={NEW_WORK_ENTRY_CONFIG.typeModal.description}
|
||||
onClose={onClose}
|
||||
closeDisabled={isBusy}
|
||||
size="lg"
|
||||
|
||||
@@ -655,6 +655,7 @@ function buildPuzzleCompileActionFromFormPayload(
|
||||
...(workDescription ? { workDescription } : {}),
|
||||
...(pictureDescription ? { pictureDescription } : {}),
|
||||
referenceImageSrc: payload?.referenceImageSrc || null,
|
||||
imageModel: payload?.imageModel ?? null,
|
||||
candidateCount: 1,
|
||||
};
|
||||
}
|
||||
@@ -687,6 +688,7 @@ function buildPuzzleFormPayloadFromSession(
|
||||
workDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc: null,
|
||||
imageModel: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -714,6 +716,10 @@ function buildPuzzleFormPayloadFromAction(
|
||||
payload.action === 'compile_puzzle_draft'
|
||||
? (payload.referenceImageSrc ?? null)
|
||||
: null,
|
||||
imageModel:
|
||||
payload.action === 'compile_puzzle_draft'
|
||||
? (payload.imageModel ?? null)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -941,8 +947,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [match3dProfile, setMatch3DProfile] =
|
||||
useState<Match3DWorkProfile | null>(null);
|
||||
const [match3dRun, setMatch3DRun] = useState<Match3DRunSnapshot | null>(null);
|
||||
const [match3dRuntimeReturnStage, setMatch3DRuntimeReturnStage] =
|
||||
useState<'match3d-result' | 'work-detail'>('match3d-result');
|
||||
const [match3dRuntimeReturnStage, setMatch3DRuntimeReturnStage] = useState<
|
||||
'match3d-result' | 'work-detail'
|
||||
>('match3d-result');
|
||||
const [isMatch3DLoadingLibrary, setIsMatch3DLoadingLibrary] = useState(false);
|
||||
const [bigFishRun, setBigFishRun] =
|
||||
useState<BigFishRuntimeSnapshotResponse | null>(null);
|
||||
@@ -993,8 +1000,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [claimingPuzzlePointIncentiveProfileId, setClaimingPuzzlePointIncentiveProfileId] =
|
||||
useState<string | null>(null);
|
||||
const [
|
||||
claimingPuzzlePointIncentiveProfileId,
|
||||
setClaimingPuzzlePointIncentiveProfileId,
|
||||
] = useState<string | null>(null);
|
||||
const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish');
|
||||
const [profilePlayStats, setProfilePlayStats] =
|
||||
useState<ProfilePlayStatsResponse | null>(null);
|
||||
@@ -1099,7 +1108,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
return galleryResponse.items;
|
||||
} catch (error) {
|
||||
setMatch3DGalleryEntries([]);
|
||||
setMatch3DError(resolveMatch3DErrorMessage(error, '读取抓大鹅广场失败。'));
|
||||
setMatch3DError(
|
||||
resolveMatch3DErrorMessage(error, '读取抓大鹅广场失败。'),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}, [resolveMatch3DErrorMessage]);
|
||||
@@ -1293,7 +1304,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
return mergePlatformPublicGalleryEntries(
|
||||
platformBootstrap.publishedGalleryEntries,
|
||||
[...bigFishPublicEntries, ...match3dPublicEntries, ...puzzlePublicEntries],
|
||||
[
|
||||
...bigFishPublicEntries,
|
||||
...match3dPublicEntries,
|
||||
...puzzlePublicEntries,
|
||||
],
|
||||
).slice(0, 6);
|
||||
}, [
|
||||
isBigFishCreationVisible,
|
||||
@@ -1665,24 +1680,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
await bigFishFlow.openWorkspace();
|
||||
}, [bigFishFlow]);
|
||||
|
||||
const openMatch3DAgentWorkspace = useCallback(async () => {
|
||||
setMatch3DSession(null);
|
||||
setMatch3DProfile(null);
|
||||
setMatch3DRun(null);
|
||||
setMatch3DError(null);
|
||||
setStreamingMatch3DReplyText('');
|
||||
setIsStreamingMatch3DReply(false);
|
||||
await match3dFlow.openWorkspace();
|
||||
}, [
|
||||
match3dFlow,
|
||||
setIsStreamingMatch3DReply,
|
||||
setMatch3DError,
|
||||
setMatch3DProfile,
|
||||
setMatch3DRun,
|
||||
setMatch3DSession,
|
||||
setStreamingMatch3DReplyText,
|
||||
]);
|
||||
|
||||
const openPuzzleAgentWorkspace = useCallback(async () => {
|
||||
setPuzzleRun(null);
|
||||
setPuzzleOperation(null);
|
||||
@@ -1729,6 +1726,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
workTitle: payload.workTitle ?? payload.seedText ?? '',
|
||||
workDescription: payload.workDescription ?? '',
|
||||
pictureDescription: payload.pictureDescription ?? '',
|
||||
imageModel: payload.imageModel ?? null,
|
||||
});
|
||||
setPuzzleOperation(response.operation);
|
||||
puzzleFlow.setSession(response.session);
|
||||
@@ -1826,12 +1824,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const handleCreationHubCreateType = useCallback(
|
||||
(type: PlatformCreationTypeId) => {
|
||||
if (
|
||||
type === 'rpg' ||
|
||||
type === 'match3d' ||
|
||||
type === 'airp' ||
|
||||
type === 'visual-novel'
|
||||
) {
|
||||
if (type === 'match3d' || type === 'airp' || type === 'visual-novel') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1839,6 +1832,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'rpg') {
|
||||
runProtectedAction(() => {
|
||||
void sessionController.openRpgAgentWorkspace();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'big-fish') {
|
||||
runProtectedAction(() => {
|
||||
void openBigFishAgentWorkspace();
|
||||
@@ -1857,6 +1857,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
openPuzzleAgentWorkspace,
|
||||
prepareCreationLaunch,
|
||||
runProtectedAction,
|
||||
sessionController,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2332,8 +2333,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
propKind === 'extendTime'
|
||||
? extendLocalPuzzleTime(currentRun)
|
||||
: propKind === 'freezeTime'
|
||||
? applyLocalPuzzleFreezeTime(currentRun)
|
||||
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
|
||||
? applyLocalPuzzleFreezeTime(currentRun)
|
||||
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
|
||||
puzzleRunRef.current = nextRun;
|
||||
setPuzzleRun(nextRun);
|
||||
return nextRun;
|
||||
@@ -2367,7 +2368,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
selectedPuzzleDetail,
|
||||
);
|
||||
if (isLocalPuzzleRun(puzzleRun)) {
|
||||
const nextRun = restartLocalPuzzleLevel(puzzleRunRef.current ?? puzzleRun);
|
||||
const nextRun = restartLocalPuzzleLevel(
|
||||
puzzleRunRef.current ?? puzzleRun,
|
||||
);
|
||||
puzzleRunRef.current = nextRun;
|
||||
setPuzzleRun(nextRun);
|
||||
return;
|
||||
@@ -2531,68 +2534,68 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleError,
|
||||
]);
|
||||
|
||||
const advancePuzzleLevel = useCallback(async (target?: {
|
||||
profileId?: string;
|
||||
levelId?: string | null;
|
||||
}) => {
|
||||
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLevel = puzzleRun.currentLevel;
|
||||
if (!currentLevel || currentLevel.status !== 'cleared') {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPuzzleBusy(true);
|
||||
setIsPuzzleNextLevelGenerating(true);
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const targetProfileId = target?.profileId?.trim();
|
||||
if (
|
||||
targetProfileId &&
|
||||
targetProfileId !== currentLevel.profileId &&
|
||||
puzzleRun.nextLevelMode === 'similarWorks'
|
||||
) {
|
||||
await startPuzzleRunFromProfile(
|
||||
targetProfileId,
|
||||
'puzzle-gallery-detail',
|
||||
undefined,
|
||||
false,
|
||||
null,
|
||||
);
|
||||
const advancePuzzleLevel = useCallback(
|
||||
async (target?: { profileId?: string; levelId?: string | null }) => {
|
||||
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
|
||||
return;
|
||||
}
|
||||
const { run } = isLocalPuzzleRun(puzzleRun)
|
||||
? await advanceLocalPuzzleNextLevel({
|
||||
run: puzzleRun,
|
||||
sourceSessionId:
|
||||
selectedPuzzleDetail?.sourceSessionId ??
|
||||
puzzleSession?.sessionId ??
|
||||
null,
|
||||
})
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId);
|
||||
setPuzzleRun(run);
|
||||
if (!isLocalPuzzleRun(puzzleRun)) {
|
||||
void platformBootstrap.refreshSaveArchives();
|
||||
|
||||
const currentLevel = puzzleRun.currentLevel;
|
||||
if (!currentLevel || currentLevel.status !== 'cleared') {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
|
||||
} finally {
|
||||
setIsPuzzleNextLevelGenerating(false);
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
}, [
|
||||
isPuzzleBusy,
|
||||
isPuzzleLeaderboardBusy,
|
||||
platformBootstrap,
|
||||
puzzleRun,
|
||||
puzzleSession,
|
||||
resolvePuzzleErrorMessage,
|
||||
selectedPuzzleDetail,
|
||||
startPuzzleRunFromProfile,
|
||||
]);
|
||||
|
||||
setIsPuzzleBusy(true);
|
||||
setIsPuzzleNextLevelGenerating(true);
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const targetProfileId = target?.profileId?.trim();
|
||||
if (
|
||||
targetProfileId &&
|
||||
targetProfileId !== currentLevel.profileId &&
|
||||
puzzleRun.nextLevelMode === 'similarWorks'
|
||||
) {
|
||||
await startPuzzleRunFromProfile(
|
||||
targetProfileId,
|
||||
'puzzle-gallery-detail',
|
||||
undefined,
|
||||
false,
|
||||
null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { run } = isLocalPuzzleRun(puzzleRun)
|
||||
? await advanceLocalPuzzleNextLevel({
|
||||
run: puzzleRun,
|
||||
sourceSessionId:
|
||||
selectedPuzzleDetail?.sourceSessionId ??
|
||||
puzzleSession?.sessionId ??
|
||||
null,
|
||||
})
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId);
|
||||
setPuzzleRun(run);
|
||||
if (!isLocalPuzzleRun(puzzleRun)) {
|
||||
void platformBootstrap.refreshSaveArchives();
|
||||
}
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
|
||||
} finally {
|
||||
setIsPuzzleNextLevelGenerating(false);
|
||||
setIsPuzzleBusy(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
isPuzzleBusy,
|
||||
isPuzzleLeaderboardBusy,
|
||||
platformBootstrap,
|
||||
puzzleRun,
|
||||
puzzleSession,
|
||||
resolvePuzzleErrorMessage,
|
||||
selectedPuzzleDetail,
|
||||
startPuzzleRunFromProfile,
|
||||
],
|
||||
);
|
||||
|
||||
const leaveAgentWorkspace = useCallback(() => {
|
||||
enterCreateTab();
|
||||
@@ -2935,14 +2938,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
.then((response) => {
|
||||
const updatedWork = response.item;
|
||||
setPuzzleWorks((current) =>
|
||||
current.map((item) =>
|
||||
mergePuzzleWorkSummary(item, updatedWork),
|
||||
),
|
||||
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
|
||||
);
|
||||
setPuzzleGalleryEntries((current) =>
|
||||
current.map((item) =>
|
||||
mergePuzzleWorkSummary(item, updatedWork),
|
||||
),
|
||||
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
|
||||
);
|
||||
setSelectedPuzzleDetail((current) =>
|
||||
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
|
||||
@@ -3316,7 +3315,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const restoredSession = await match3dFlow.restoreDraft(item.sourceSessionId);
|
||||
const restoredSession = await match3dFlow.restoreDraft(
|
||||
item.sourceSessionId,
|
||||
);
|
||||
if (!restoredSession) {
|
||||
await refreshMatch3DShelf().catch(() => undefined);
|
||||
return;
|
||||
@@ -3395,7 +3396,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (isPuzzleGalleryEntry(selectedPublicWorkDetail)) {
|
||||
const work = mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail);
|
||||
if (!work) {
|
||||
setPublicWorkDetailError('当前拼图作品信息不完整,暂时无法进入玩法。');
|
||||
setPublicWorkDetailError(
|
||||
'当前拼图作品信息不完整,暂时无法进入玩法。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPublicWorkDetailError(null);
|
||||
@@ -3411,7 +3414,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (isMatch3DGalleryEntry(selectedPublicWorkDetail)) {
|
||||
const work = mapPublicWorkDetailToMatch3DWork(selectedPublicWorkDetail);
|
||||
if (!work) {
|
||||
setPublicWorkDetailError('当前抓大鹅作品信息不完整,暂时无法进入玩法。');
|
||||
setPublicWorkDetailError(
|
||||
'当前抓大鹅作品信息不完整,暂时无法进入玩法。',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setPublicWorkDetailError(null);
|
||||
@@ -4487,7 +4492,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载抓大鹅共创工作区..." />}
|
||||
fallback={
|
||||
<LazyPanelFallback label="正在加载抓大鹅共创工作区..." />
|
||||
}
|
||||
>
|
||||
<Match3DAgentWorkspace
|
||||
session={match3dSession}
|
||||
@@ -4520,7 +4527,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<Match3DResultView
|
||||
profile={
|
||||
match3dProfile ?? buildMatch3DProfileFromSession(match3dSession)!
|
||||
match3dProfile ??
|
||||
buildMatch3DProfileFromSession(match3dSession)!
|
||||
}
|
||||
draft={match3dSession.draft}
|
||||
isBusy={isMatch3DBusy}
|
||||
@@ -4537,7 +4545,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
refreshMatch3DShelf(),
|
||||
refreshMatch3DGallery(),
|
||||
]);
|
||||
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(profile));
|
||||
openPublicWorkDetail(
|
||||
mapMatch3DWorkToPublicWorkDetail(profile),
|
||||
);
|
||||
}}
|
||||
onStartTestRun={(profile) => {
|
||||
setMatch3DProfile(profile);
|
||||
@@ -4565,7 +4575,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
error={match3dError}
|
||||
onBack={() => {
|
||||
if (match3dRun?.runId && match3dRun.status === 'running') {
|
||||
void stopMatch3DRun(match3dRun.runId).catch(() => undefined);
|
||||
void stopMatch3DRun(match3dRun.runId).catch(
|
||||
() => undefined,
|
||||
);
|
||||
}
|
||||
setSelectionStage(match3dRuntimeReturnStage);
|
||||
}}
|
||||
@@ -4596,7 +4608,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
onClickItem={(payload) => {
|
||||
const runId = payload.runId ?? match3dRun?.runId;
|
||||
if (!runId) {
|
||||
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
||||
return Promise.reject(
|
||||
new Error('抓大鹅运行态缺少 runId。'),
|
||||
);
|
||||
}
|
||||
return clickMatch3DItem(runId, payload);
|
||||
}}
|
||||
@@ -5084,7 +5098,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
setShowCreationTypeModal(false);
|
||||
}}
|
||||
onSelectRpg={() => {
|
||||
// RPG 创作入口当前为敬请期待;保留回调防御,避免旧入口绕过锁定态。
|
||||
runProtectedAction(() => {
|
||||
void sessionController.openRpgAgentWorkspace();
|
||||
});
|
||||
}}
|
||||
onSelectBigFish={() => {
|
||||
runProtectedAction(() => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { NEW_WORK_ENTRY_CONFIG } from '../../config/newWorkEntryConfig';
|
||||
import {
|
||||
getVisiblePlatformCreationTypes,
|
||||
isPlatformCreationTypeVisible,
|
||||
PLATFORM_CREATION_TYPES,
|
||||
} from './platformEntryCreationTypes';
|
||||
|
||||
test('platform creation types are derived from new work entry config', () => {
|
||||
const puzzleConfig = NEW_WORK_ENTRY_CONFIG.creationTypes.find(
|
||||
(item) => item.id === 'puzzle',
|
||||
);
|
||||
|
||||
expect(puzzleConfig).toBeTruthy();
|
||||
expect(PLATFORM_CREATION_TYPES).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'puzzle',
|
||||
title: puzzleConfig?.title,
|
||||
subtitle: puzzleConfig?.subtitle,
|
||||
badge: puzzleConfig?.badge,
|
||||
locked: false,
|
||||
hidden: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('new work entry config controls visibility and open order', () => {
|
||||
const visibleIds = getVisiblePlatformCreationTypes().map((item) => item.id);
|
||||
|
||||
expect(isPlatformCreationTypeVisible('big-fish')).toBe(false);
|
||||
expect(visibleIds).not.toContain('big-fish');
|
||||
expect(visibleIds[0]).toBe('rpg');
|
||||
expect(visibleIds).toEqual([
|
||||
'rpg',
|
||||
'puzzle',
|
||||
'match3d',
|
||||
'airp',
|
||||
'visual-novel',
|
||||
]);
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
export type PlatformCreationTypeId =
|
||||
| 'rpg'
|
||||
| 'big-fish'
|
||||
| 'match3d'
|
||||
| 'puzzle'
|
||||
| 'airp'
|
||||
| 'visual-novel';
|
||||
import {
|
||||
NEW_WORK_ENTRY_CONFIG,
|
||||
type NewWorkEntryCreationTypeId,
|
||||
} from '../../config/newWorkEntryConfig';
|
||||
|
||||
export type PlatformCreationTypeId = NewWorkEntryCreationTypeId;
|
||||
|
||||
export type PlatformCreationTypeCard = {
|
||||
id: PlatformCreationTypeId;
|
||||
@@ -39,51 +38,15 @@ export function isPlatformCreationTypeVisible(id: PlatformCreationTypeId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创作页与类型弹层共用同一份模板元数据,避免多入口文案和可用状态漂移。
|
||||
* 创作页与类型弹层共用同一份新建作品入口配置,避免多入口文案和开放状态漂移。
|
||||
* `hidden` 只控制平台入口是否展示,不影响既有玩法链路和路由能力。
|
||||
*/
|
||||
export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] = [
|
||||
{
|
||||
id: 'rpg',
|
||||
title: '角色扮演',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
id: 'big-fish',
|
||||
title: '大鱼吃小鱼',
|
||||
subtitle: '实时成长玩法',
|
||||
badge: '可创建',
|
||||
locked: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
id: 'puzzle',
|
||||
title: '拼图',
|
||||
subtitle: '创意礼物,生活分享',
|
||||
badge: '可创建',
|
||||
locked: false,
|
||||
},
|
||||
{
|
||||
id: 'match3d',
|
||||
title: '抓大鹅',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
id: 'airp',
|
||||
title: 'AIRP',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel',
|
||||
title: '视觉小说',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
locked: true,
|
||||
},
|
||||
];
|
||||
export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] =
|
||||
NEW_WORK_ENTRY_CONFIG.creationTypes.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
badge: item.badge,
|
||||
locked: !item.open,
|
||||
hidden: !item.visible,
|
||||
}));
|
||||
|
||||
@@ -100,6 +100,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
|
||||
workDescription: '一套雨夜猫街主题拼图。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
imageModel: 'original',
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
|
||||
expect(screen.queryByText('旧会话消息不再渲染为聊天入口。')).toBeNull();
|
||||
@@ -129,10 +130,44 @@ test('puzzle workspace falls back to compile action for restored sessions', () =
|
||||
workDescription: '雾港遗迹拼图',
|
||||
pictureDescription: '潮雾中的灯塔与断桥',
|
||||
referenceImageSrc: null,
|
||||
imageModel: 'original',
|
||||
candidateCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle workspace switches the image model from the description box', () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
session={null}
|
||||
onBack={() => {}}
|
||||
onSubmitMessage={() => {}}
|
||||
onExecuteAction={() => {}}
|
||||
onCreateFromForm={onCreateFromForm}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('作品名称'), {
|
||||
target: { value: '暖灯猫街' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('作品描述'), {
|
||||
target: { value: '一套雨夜猫街主题拼图。' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('画面描述'), {
|
||||
target: { value: '一只猫在雨夜灯牌下回头。' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '图片模型' }));
|
||||
fireEvent.click(screen.getByRole('menuitemradio', { name: 'nanobanana2' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /生成草稿/u }));
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
imageModel: 'gemini-3.1-flash-image-preview',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('puzzle workspace restores form draft fields and autosaves edits', () => {
|
||||
vi.useFakeTimers();
|
||||
const onAutoSaveForm = vi.fn();
|
||||
@@ -208,5 +243,6 @@ test('puzzle workspace restores form draft fields and autosaves edits', () => {
|
||||
workDescription: '旧街雨夜的拼图草稿。',
|
||||
pictureDescription: '旧街灯牌下的猫和发光雨伞。',
|
||||
referenceImageSrc: null,
|
||||
imageModel: 'original',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,12 @@ import type {
|
||||
SendPuzzleAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
|
||||
import {
|
||||
normalizePuzzleImageModel,
|
||||
PUZZLE_IMAGE_MODEL_ORIGINAL,
|
||||
type PuzzleImageModelId,
|
||||
} from './puzzleImageModelOptions';
|
||||
import { PuzzleImageModelPicker } from './PuzzleImageModelPicker';
|
||||
|
||||
type PuzzleAgentWorkspaceProps = {
|
||||
session: PuzzleAgentSessionSnapshot | null;
|
||||
@@ -27,6 +33,7 @@ type PuzzleFormState = {
|
||||
pictureDescription: string;
|
||||
referenceImageSrc: string;
|
||||
referenceImageLabel: string;
|
||||
imageModel: PuzzleImageModelId;
|
||||
};
|
||||
|
||||
const EMPTY_FORM_STATE: PuzzleFormState = {
|
||||
@@ -35,6 +42,7 @@ const EMPTY_FORM_STATE: PuzzleFormState = {
|
||||
pictureDescription: '',
|
||||
referenceImageSrc: '',
|
||||
referenceImageLabel: '',
|
||||
imageModel: PUZZLE_IMAGE_MODEL_ORIGINAL,
|
||||
};
|
||||
|
||||
function resolveInitialFormState(
|
||||
@@ -51,6 +59,7 @@ function resolveInitialFormState(
|
||||
referenceImageLabel: initialFormPayload?.referenceImageSrc
|
||||
? '已选择参考图'
|
||||
: '',
|
||||
imageModel: normalizePuzzleImageModel(initialFormPayload?.imageModel),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,6 +73,7 @@ function resolveInitialFormState(
|
||||
referenceImageLabel: initialFormPayload.referenceImageSrc
|
||||
? '已选择参考图'
|
||||
: '',
|
||||
imageModel: normalizePuzzleImageModel(initialFormPayload.imageModel),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,6 +97,7 @@ function resolveInitialFormState(
|
||||
session.draft?.summary || session.anchorPack.visualSubject.value || '',
|
||||
referenceImageSrc: '',
|
||||
referenceImageLabel: '',
|
||||
imageModel: PUZZLE_IMAGE_MODEL_ORIGINAL,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -151,9 +162,11 @@ export function PuzzleAgentWorkspace({
|
||||
workDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc: formState.referenceImageSrc || null,
|
||||
imageModel: formState.imageModel,
|
||||
}),
|
||||
[
|
||||
formState.referenceImageSrc,
|
||||
formState.imageModel,
|
||||
pictureDescription,
|
||||
workDescription,
|
||||
workTitle,
|
||||
@@ -163,6 +176,7 @@ export function PuzzleAgentWorkspace({
|
||||
autosavePayload.workTitle,
|
||||
autosavePayload.workDescription,
|
||||
autosavePayload.pictureDescription,
|
||||
autosavePayload.imageModel,
|
||||
]);
|
||||
const lastAutosaveSignatureRef = useRef(autosaveSignature);
|
||||
const autosaveSessionIdRef = useRef(session?.sessionId ?? null);
|
||||
@@ -240,6 +254,7 @@ export function PuzzleAgentWorkspace({
|
||||
workDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc: formState.referenceImageSrc || null,
|
||||
imageModel: formState.imageModel,
|
||||
};
|
||||
|
||||
if (!session && onCreateFromForm) {
|
||||
@@ -254,6 +269,7 @@ export function PuzzleAgentWorkspace({
|
||||
workDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc: formState.referenceImageSrc || null,
|
||||
imageModel: formState.imageModel,
|
||||
candidateCount: 1,
|
||||
});
|
||||
};
|
||||
@@ -332,6 +348,16 @@ export function PuzzleAgentWorkspace({
|
||||
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="画面描述"
|
||||
/>
|
||||
<PuzzleImageModelPicker
|
||||
value={formState.imageModel}
|
||||
disabled={isBusy}
|
||||
onChange={(imageModel) =>
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
imageModel,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
className={`absolute bottom-3 right-3 inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
title={
|
||||
|
||||
84
src/components/puzzle-agent/PuzzleImageModelPicker.tsx
Normal file
84
src/components/puzzle-agent/PuzzleImageModelPicker.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
getPuzzleImageModelLabel,
|
||||
normalizePuzzleImageModel,
|
||||
PUZZLE_IMAGE_MODEL_OPTIONS,
|
||||
type PuzzleImageModelId,
|
||||
} from './puzzleImageModelOptions';
|
||||
|
||||
type PuzzleImageModelPickerProps = {
|
||||
value: PuzzleImageModelId;
|
||||
disabled?: boolean;
|
||||
onChange: (value: PuzzleImageModelId) => void;
|
||||
};
|
||||
|
||||
export function PuzzleImageModelPicker({
|
||||
value,
|
||||
disabled = false,
|
||||
onChange,
|
||||
}: PuzzleImageModelPickerProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||
const normalizedValue = normalizePuzzleImageModel(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!rootRef.current?.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('pointerdown', handlePointerDown);
|
||||
return () => window.removeEventListener('pointerdown', handlePointerDown);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="absolute bottom-3 left-3 z-10">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen((current) => !current)}
|
||||
className={`inline-flex min-h-8 max-w-[10rem] items-center rounded-full border border-[var(--platform-subpanel-border)] bg-white/96 px-3 text-[11px] font-bold text-[var(--platform-text-strong)] shadow-sm transition hover:bg-[var(--platform-subpanel-fill)] ${disabled ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isOpen}
|
||||
aria-label="图片模型"
|
||||
title="图片模型"
|
||||
>
|
||||
<span className="truncate">
|
||||
{getPuzzleImageModelLabel(normalizedValue)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen ? (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute bottom-10 left-0 min-w-[11rem] overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/98 p-1 shadow-[0_16px_40px_rgba(0,0,0,0.18)]"
|
||||
>
|
||||
{PUZZLE_IMAGE_MODEL_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={option.id === normalizedValue}
|
||||
onClick={() => {
|
||||
onChange(option.id);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`block min-h-9 w-full rounded-[0.8rem] px-3 text-left text-xs font-bold transition ${
|
||||
option.id === normalizedValue
|
||||
? 'bg-amber-100/80 text-amber-800'
|
||||
: 'text-[var(--platform-text-base)] hover:bg-[var(--platform-subpanel-fill)]'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
33
src/components/puzzle-agent/puzzleImageModelOptions.ts
Normal file
33
src/components/puzzle-agent/puzzleImageModelOptions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const PUZZLE_IMAGE_MODEL_ORIGINAL = 'original';
|
||||
export const PUZZLE_IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2';
|
||||
export const PUZZLE_IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
|
||||
|
||||
export type PuzzleImageModelId =
|
||||
| typeof PUZZLE_IMAGE_MODEL_ORIGINAL
|
||||
| typeof PUZZLE_IMAGE_MODEL_GPT_IMAGE_2
|
||||
| typeof PUZZLE_IMAGE_MODEL_NANOBANANA2;
|
||||
|
||||
export const PUZZLE_IMAGE_MODEL_OPTIONS: Array<{
|
||||
id: PuzzleImageModelId;
|
||||
label: string;
|
||||
}> = [
|
||||
{ id: PUZZLE_IMAGE_MODEL_ORIGINAL, label: '原模型' },
|
||||
{ id: PUZZLE_IMAGE_MODEL_GPT_IMAGE_2, label: 'gpt-image-2' },
|
||||
{ id: PUZZLE_IMAGE_MODEL_NANOBANANA2, label: 'nanobanana2' },
|
||||
];
|
||||
|
||||
export function normalizePuzzleImageModel(
|
||||
value: string | null | undefined,
|
||||
): PuzzleImageModelId {
|
||||
return (
|
||||
PUZZLE_IMAGE_MODEL_OPTIONS.find((option) => option.id === value)?.id ??
|
||||
PUZZLE_IMAGE_MODEL_ORIGINAL
|
||||
);
|
||||
}
|
||||
|
||||
export function getPuzzleImageModelLabel(model: PuzzleImageModelId) {
|
||||
return (
|
||||
PUZZLE_IMAGE_MODEL_OPTIONS.find((option) => option.id === model)?.label ??
|
||||
'原模型'
|
||||
);
|
||||
}
|
||||
@@ -232,13 +232,16 @@ describe('PuzzleResultView', () => {
|
||||
fireEvent.change(within(dialog).getByLabelText('画面描述'), {
|
||||
target: { value: '一只猫在雨夜灯牌下回头。' },
|
||||
});
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: /重新生成画面/u }));
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: /重新生成画面/u }),
|
||||
);
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith({
|
||||
action: 'generate_puzzle_images',
|
||||
levelId: 'puzzle-level-1',
|
||||
promptText: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: undefined,
|
||||
imageModel: 'original',
|
||||
candidateCount: 1,
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
@@ -295,9 +298,13 @@ describe('PuzzleResultView', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /新增关卡/u }));
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
expect(within(dialog).getByRole('button', { name: /生成画面/u })).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: /生成画面/u }),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).queryByText('画面图')).toBeNull();
|
||||
expect(within(dialog).queryByRole('button', { name: /关卡测试/u })).toBeNull();
|
||||
expect(
|
||||
within(dialog).queryByRole('button', { name: /关卡测试/u }),
|
||||
).toBeNull();
|
||||
fireEvent.click(screen.getByLabelText('关闭'));
|
||||
expect(screen.getAllByText('第2关').length).toBeGreaterThan(0);
|
||||
|
||||
@@ -358,6 +365,7 @@ describe('PuzzleResultView', () => {
|
||||
levelId: 'puzzle-level-1775000000000-2',
|
||||
promptText: '新关卡里有一座发光钟楼。',
|
||||
referenceImageSrc: undefined,
|
||||
imageModel: 'original',
|
||||
candidateCount: 1,
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
@@ -457,8 +465,38 @@ describe('PuzzleResultView', () => {
|
||||
levelId: 'puzzle-level-1',
|
||||
promptText: '屋檐下的猫与暖灯街角。',
|
||||
referenceImageSrc: '/generated-puzzle-assets/history/image.png',
|
||||
imageModel: 'original',
|
||||
candidateCount: 1,
|
||||
levelsJson: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
test('passes the selected image model when regenerating a level image', () => {
|
||||
const onExecuteAction = vi.fn();
|
||||
|
||||
render(
|
||||
<PuzzleResultView
|
||||
session={createSession()}
|
||||
onBack={() => {}}
|
||||
onExecuteAction={onExecuteAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('雨夜猫街'));
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '图片模型' }));
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('menuitemradio', { name: 'gpt-image-2' }),
|
||||
);
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: /重新生成画面/u }),
|
||||
);
|
||||
|
||||
expect(onExecuteAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'generate_puzzle_images',
|
||||
imageModel: 'gpt-image-2',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,11 @@ import {
|
||||
} from '../../services/puzzle-works/puzzleAssetClient';
|
||||
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import {
|
||||
PUZZLE_IMAGE_MODEL_ORIGINAL,
|
||||
type PuzzleImageModelId,
|
||||
} from '../puzzle-agent/puzzleImageModelOptions';
|
||||
import { PuzzleImageModelPicker } from '../puzzle-agent/PuzzleImageModelPicker';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type PuzzleResultViewProps = {
|
||||
@@ -80,7 +85,9 @@ function resolveLevelFormalImageSrc(level: PuzzleDraftLevel) {
|
||||
);
|
||||
}
|
||||
|
||||
function buildFallbackLevelFromDraft(draft: PuzzleResultDraft): PuzzleDraftLevel {
|
||||
function buildFallbackLevelFromDraft(
|
||||
draft: PuzzleResultDraft,
|
||||
): PuzzleDraftLevel {
|
||||
return {
|
||||
levelId: 'puzzle-level-1',
|
||||
levelName: draft.levelName || '',
|
||||
@@ -143,7 +150,9 @@ function createDraftEditState(draft: PuzzleResultDraft): DraftEditState {
|
||||
};
|
||||
}
|
||||
|
||||
function createBlankPuzzleLevel(existingLevels: PuzzleDraftLevel[]): PuzzleDraftLevel {
|
||||
function createBlankPuzzleLevel(
|
||||
existingLevels: PuzzleDraftLevel[],
|
||||
): PuzzleDraftLevel {
|
||||
const nextIndex = existingLevels.length + 1;
|
||||
return {
|
||||
levelId: `puzzle-level-${Date.now()}-${nextIndex}`,
|
||||
@@ -200,7 +209,9 @@ function buildPublishReady(
|
||||
...(levels.length > 0 ? [] : ['至少需要一个拼图关卡。']),
|
||||
...levels.flatMap((level, index) => [
|
||||
...(level.levelName.trim() ? [] : [`第${index + 1}关名称不能为空。`]),
|
||||
...(resolveLevelFormalImageSrc(level) ? [] : [`第${index + 1}关缺少正式图。`]),
|
||||
...(resolveLevelFormalImageSrc(level)
|
||||
? []
|
||||
: [`第${index + 1}关缺少正式图。`]),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -574,6 +585,7 @@ function PuzzleLevelDetailDialog({
|
||||
levelId: string,
|
||||
promptText?: string | null,
|
||||
referenceImageSrc?: string | null,
|
||||
imageModel?: PuzzleImageModelId | null,
|
||||
) => void;
|
||||
onLevelChange: (nextLevel: PuzzleDraftLevel) => void;
|
||||
onStartTestRun?: (level: PuzzleDraftLevel) => void;
|
||||
@@ -581,8 +593,13 @@ function PuzzleLevelDetailDialog({
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const [referenceImageSrc, setReferenceImageSrc] = useState('');
|
||||
const [referenceImageLabel, setReferenceImageLabel] = useState('');
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(null);
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isHistoryPickerOpen, setIsHistoryPickerOpen] = useState(false);
|
||||
const [imageModel, setImageModel] = useState<PuzzleImageModelId>(
|
||||
PUZZLE_IMAGE_MODEL_ORIGINAL,
|
||||
);
|
||||
const formalImageSrc = resolveLevelFormalImageSrc(level);
|
||||
const hasFormalImage = Boolean(formalImageSrc);
|
||||
|
||||
@@ -704,6 +721,11 @@ function PuzzleLevelDetailDialog({
|
||||
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
|
||||
aria-label="画面描述"
|
||||
/>
|
||||
<PuzzleImageModelPicker
|
||||
value={imageModel}
|
||||
disabled={isBusy}
|
||||
onChange={setImageModel}
|
||||
/>
|
||||
<label
|
||||
className={`absolute bottom-3 right-3 inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
|
||||
title={referenceImageSrc ? '更换参考图' : '添加参考图'}
|
||||
@@ -787,6 +809,7 @@ function PuzzleLevelDetailDialog({
|
||||
level.levelId,
|
||||
level.pictureDescription.trim() || undefined,
|
||||
referenceImageSrc || undefined,
|
||||
imageModel,
|
||||
);
|
||||
}}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-amber-600 px-4 py-3 text-sm font-bold text-white disabled:opacity-45"
|
||||
@@ -836,7 +859,9 @@ function PuzzlePublishDialog({
|
||||
}) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const primaryLevel = editState.levels[0] ?? null;
|
||||
const formalImageSrc = primaryLevel ? resolveLevelFormalImageSrc(primaryLevel) : '';
|
||||
const formalImageSrc = primaryLevel
|
||||
? resolveLevelFormalImageSrc(primaryLevel)
|
||||
: '';
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
@@ -1180,7 +1205,9 @@ export function PuzzleResultView({
|
||||
return syncDraftFromEditState(draft, editState);
|
||||
}, [draft, editState]);
|
||||
const primaryLevel = editState?.levels[0] ?? null;
|
||||
const primaryImageSrc = primaryLevel ? resolveLevelFormalImageSrc(primaryLevel) : '';
|
||||
const primaryImageSrc = primaryLevel
|
||||
? resolveLevelFormalImageSrc(primaryLevel)
|
||||
: '';
|
||||
const imageRefreshKey = `${session.updatedAt}:${primaryImageSrc}:${editState?.levels.length ?? 0}`;
|
||||
const activeLevel =
|
||||
editState?.levels.find((level) => level.levelId === activeLevelId) ?? null;
|
||||
@@ -1201,7 +1228,8 @@ export function PuzzleResultView({
|
||||
pictureDescription: level.pictureDescription.trim(),
|
||||
})),
|
||||
};
|
||||
const originalState = savedEditStateRef.current ?? createDraftEditState(draft);
|
||||
const originalState =
|
||||
savedEditStateRef.current ?? createDraftEditState(draft);
|
||||
const changed =
|
||||
JSON.stringify(normalizedState) !== JSON.stringify(originalState);
|
||||
|
||||
@@ -1386,12 +1414,13 @@ export function PuzzleResultView({
|
||||
isBusy={isBusy}
|
||||
level={activeLevel}
|
||||
onClose={() => setActiveLevelId(null)}
|
||||
onGenerate={(levelId, promptText, referenceImageSrc) => {
|
||||
onGenerate={(levelId, promptText, referenceImageSrc, imageModel) => {
|
||||
onExecuteAction({
|
||||
action: 'generate_puzzle_images',
|
||||
levelId,
|
||||
promptText,
|
||||
referenceImageSrc,
|
||||
imageModel: imageModel ?? PUZZLE_IMAGE_MODEL_ORIGINAL,
|
||||
candidateCount: 1,
|
||||
levelsJson: JSON.stringify(editState.levels),
|
||||
});
|
||||
|
||||
@@ -1708,7 +1708,7 @@ beforeEach(() => {
|
||||
vi.mocked(streamRpgCreationMessage).mockResolvedValue(mockSession);
|
||||
});
|
||||
|
||||
test('create hub keeps RPG, AIRP and visual novel locked', async () => {
|
||||
test('create hub opens RPG while keeping AIRP and visual novel locked', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
@@ -1724,9 +1724,13 @@ test('create hub keeps RPG, AIRP and visual novel locked', async () => {
|
||||
expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true);
|
||||
const rpgButton = screen.getByRole('button', { name: /角色扮演/u });
|
||||
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(within(rpgButton).getAllByText('敬请期待').length).toBeGreaterThan(0);
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(rpgButton);
|
||||
|
||||
expect(createRpgCreationSession).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('platform create hub does not prefetch hidden big fish platform data', async () => {
|
||||
@@ -2437,7 +2441,7 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('selecting locked RPG creation while logged out does not route through requireAuth', async () => {
|
||||
test('selecting RPG creation while logged out routes through requireAuth', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
|
||||
@@ -2454,9 +2458,9 @@ test('selecting locked RPG creation while logged out does not route through requ
|
||||
await openCreationHub(user);
|
||||
const rpgButton = await screen.findByRole('button', { name: /角色扮演/u });
|
||||
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(rpgButton);
|
||||
expect(requireAuth).not.toHaveBeenCalled();
|
||||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2582,16 +2586,16 @@ test('new creation entry maps raw bearer token errors to user-facing auth copy',
|
||||
await openCreationHub(user);
|
||||
const rpgButton = screen.getByRole('button', { name: /角色扮演/u });
|
||||
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((rpgButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(rpgButton);
|
||||
|
||||
expect(listPuzzleWorks).toHaveBeenCalled();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(createRpgCreationSession).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).queryByText(
|
||||
await within(getPlatformTabPanel('create')).findByText(
|
||||
'当前登录状态已失效,请重新登录后继续。',
|
||||
),
|
||||
).toBeNull();
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
69
src/config/newWorkEntryConfig.ts
Normal file
69
src/config/newWorkEntryConfig.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 新建作品入口配置。
|
||||
* 修改入口开放状态、隐藏状态和展示文案时,优先调整本文件,避免多入口文案漂移。
|
||||
*/
|
||||
export const NEW_WORK_ENTRY_CONFIG = {
|
||||
startCard: {
|
||||
title: '新建作品',
|
||||
description: '直接选择游戏创作模板,立刻进入对应的共创工作台。',
|
||||
idleBadge: '选择模板',
|
||||
busyBadge: '正在开启',
|
||||
},
|
||||
typeModal: {
|
||||
title: '选择创作类型',
|
||||
description: '先选玩法类型,再进入对应创作工作台。',
|
||||
},
|
||||
creationTypes: [
|
||||
{
|
||||
id: 'rpg',
|
||||
title: '角色扮演',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
visible: true,
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
id: 'big-fish',
|
||||
title: '大鱼吃小鱼',
|
||||
subtitle: '实时成长玩法',
|
||||
badge: '可创建',
|
||||
visible: false,
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
id: 'puzzle',
|
||||
title: '拼图',
|
||||
subtitle: '创意礼物,生活分享',
|
||||
badge: '可创建',
|
||||
visible: true,
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
id: 'match3d',
|
||||
title: '抓大鹅',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
visible: true,
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
id: 'airp',
|
||||
title: 'AIRP',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
visible: true,
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
id: 'visual-novel',
|
||||
title: '视觉小说',
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
visible: true,
|
||||
open: false,
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
export type NewWorkEntryCreationTypeId =
|
||||
(typeof NEW_WORK_ENTRY_CONFIG.creationTypes)[number]['id'];
|
||||
Reference in New Issue
Block a user