feat: unify creation agent chat fill
This commit is contained in:
@@ -193,8 +193,10 @@ Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事:
|
|||||||
4. 会补缺,不会平均盘问
|
4. 会补缺,不会平均盘问
|
||||||
5. 进度基于真实锚点完成度,而不是机械轮次
|
5. 进度基于真实锚点完成度,而不是机械轮次
|
||||||
6. 当会话至少完成 `2` 轮后,工作区必须提供 `补充剩余关键字` 快捷动作。
|
6. 当会话至少完成 `2` 轮后,工作区必须提供 `补充剩余关键字` 快捷动作。
|
||||||
- 该动作只向 Agent 发送“请补充剩余关键字。”,由后端 Agent 根据当前锚点补齐缺失关键词。
|
- 该动作沿用 RPG 聊天链路,仍走发送消息接口,但请求体必须携带 `quickFillRequested: true`。
|
||||||
|
- 前端只发送“请补充剩余关键字。”作为本轮用户消息,由后端 Agent 根据当前锚点补齐缺失关键词。
|
||||||
- 前端不得自行推断成长阶梯、风险节奏或视觉母题,也不得直接改写锚点状态。
|
- 前端不得自行推断成长阶梯、风险节奏或视觉母题,也不得直接改写锚点状态。
|
||||||
|
- Agent 收到后必须进入自动补齐模式,不允许继续反问用户。
|
||||||
|
|
||||||
## 7.3 大鱼吃小鱼玩法的 4 个最小高杠杆锚点
|
## 7.3 大鱼吃小鱼玩法的 4 个最小高杠杆锚点
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# AI 原生自定义世界生成流程优化 PRD
|
# AI 原生自定义世界生成流程优化 PRD
|
||||||
|
|
||||||
更新时间:`2026-04-06`
|
更新时间:`2026-04-06`
|
||||||
|
|
||||||
@@ -274,6 +274,31 @@
|
|||||||
|
|
||||||
这样既能保证世界最小成型,又不会把创作者门槛抬高。
|
这样既能保证世界最小成型,又不会把创作者门槛抬高。
|
||||||
|
|
||||||
|
## 4.3.1 抽象统一“聊天补充设定”能力
|
||||||
|
|
||||||
|
RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都应走同一套“聊天补充设定”能力,而不是各自维护按钮、提交 payload 和自动补全规则。
|
||||||
|
|
||||||
|
统一能力需要覆盖:
|
||||||
|
|
||||||
|
1. 普通聊天提交:只携带用户输入文本与统一生成的 `clientMessageId`。
|
||||||
|
2. 总结当前设定:快捷动作统一展示为“总结当前设定”,但总结消息允许各玩法保留领域文案。
|
||||||
|
3. 补充剩余设定:快捷动作统一展示为“补充剩余设定”,并统一进入自动补全模式。
|
||||||
|
|
||||||
|
用户完成至少两轮对话后,工作台展示“补充剩余设定”快捷操作。点击后前端只发送一次聊天消息,不再弹出额外表单或追问面板:
|
||||||
|
|
||||||
|
- 消息文本固定为 `请补充剩余设定。`
|
||||||
|
- 请求必须携带 `quickFillRequested: true`
|
||||||
|
- `focusCardId` 固定为空,`selectedCardIds` 固定为空数组
|
||||||
|
|
||||||
|
后端各玩法 Agent 收到 `quickFillRequested: true` 后必须进入强制补全模式,并复用统一公共规则:
|
||||||
|
|
||||||
|
1. 不再继续向用户追问缺失设定。
|
||||||
|
2. 基于已有对话与已推断卡片自行补齐剩余关键字。
|
||||||
|
3. 输出可进入对应生成结果的完整结构。
|
||||||
|
4. 将进度推进到可提交状态,避免用户点击后仍停留在问答循环。
|
||||||
|
|
||||||
|
三入口只允许保留领域差异:聊天消息 ID 前缀、总结文案、最终生成按钮名称、领域 payload 额外字段、领域 prompt 对“缺失内容范围”的补充说明。除此之外,聊天动作的展示、触发语义与自动补全硬规则必须统一维护。
|
||||||
|
|
||||||
## 4.4 明确支持“锁定”
|
## 4.4 明确支持“锁定”
|
||||||
|
|
||||||
每张卡片、每个关键角色、每个关键地点都应支持锁定。
|
每张卡片、每个关键角色、每个关键地点都应支持锁定。
|
||||||
|
|||||||
@@ -202,8 +202,9 @@
|
|||||||
2. 每轮只追问当前最影响图片生成质量的 `1` 个问题。
|
2. 每轮只追问当前最影响图片生成质量的 `1` 个问题。
|
||||||
3. 当创作者已经说出足够信息时,优先总结,不重复追问。
|
3. 当创作者已经说出足够信息时,优先总结,不重复追问。
|
||||||
4. 当会话至少完成 `2` 轮后,工作区必须提供 `补充剩余关键字` 快捷动作。
|
4. 当会话至少完成 `2` 轮后,工作区必须提供 `补充剩余关键字` 快捷动作。
|
||||||
- 该动作只向 Agent 发送“请补充剩余关键字。”,不在前端补数据、不伪造锚点状态。
|
- 该动作沿用 RPG 聊天链路,仍走发送消息接口,但请求体必须携带 `quickFillRequested: true`。
|
||||||
- Agent 收到后应优先补齐仍为 `待补充` / 空值的锚点关键词,并保持每次回复清爽直接。
|
- 前端不补数据、不伪造锚点状态,只发送“请补充剩余关键字。”作为本轮用户消息。
|
||||||
|
- Agent 收到后必须进入自动补齐模式,优先补齐仍为 `待补充` / 空值的锚点关键词,不允许继续反问用户。
|
||||||
5. 在进入结果页前,至少确认:
|
5. 在进入结果页前,至少确认:
|
||||||
- 一句题材承诺
|
- 一句题材承诺
|
||||||
- 一个主要视觉主体
|
- 一个主要视觉主体
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type CreateBigFishSessionRequest = {
|
|||||||
export type SendBigFishMessageRequest = {
|
export type SendBigFishMessageRequest = {
|
||||||
clientMessageId: string;
|
clientMessageId: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
quickFillRequested?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BigFishActionId =
|
export type BigFishActionId =
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export interface CreatePuzzleAgentSessionResponse {
|
|||||||
export interface SendPuzzleAgentMessageRequest {
|
export interface SendPuzzleAgentMessageRequest {
|
||||||
clientMessageId: string;
|
clientMessageId: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
quickFillRequested?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendPuzzleAgentMessageResponse extends PuzzleAgentActionResponse {
|
export interface SendPuzzleAgentMessageResponse extends PuzzleAgentActionResponse {
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ pub async fn submit_big_fish_message(
|
|||||||
BigFishAgentTurnRequest {
|
BigFishAgentTurnRequest {
|
||||||
llm_client: state.llm_client(),
|
llm_client: state.llm_client(),
|
||||||
session: &submitted_session,
|
session: &submitted_session,
|
||||||
|
quick_fill_requested: payload.quick_fill_requested.unwrap_or(false),
|
||||||
},
|
},
|
||||||
|_| {},
|
|_| {},
|
||||||
)
|
)
|
||||||
@@ -285,11 +286,13 @@ pub async fn stream_big_fish_message(
|
|||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||||
})?;
|
})?;
|
||||||
|
let quick_fill_requested = payload.quick_fill_requested.unwrap_or(false);
|
||||||
let mut streamed_reply_text = String::new();
|
let mut streamed_reply_text = String::new();
|
||||||
let turn_result = run_big_fish_agent_turn(
|
let turn_result = run_big_fish_agent_turn(
|
||||||
BigFishAgentTurnRequest {
|
BigFishAgentTurnRequest {
|
||||||
llm_client: state.llm_client(),
|
llm_client: state.llm_client(),
|
||||||
session: &submitted_session,
|
session: &submitted_session,
|
||||||
|
quick_fill_requested,
|
||||||
},
|
},
|
||||||
|text| {
|
|text| {
|
||||||
streamed_reply_text = text.to_string();
|
streamed_reply_text = text.to_string();
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ use spacetime_client::{
|
|||||||
use crate::creation_agent_anchor_templates::{
|
use crate::creation_agent_anchor_templates::{
|
||||||
get_creation_agent_anchor_template, render_anchor_question_block,
|
get_creation_agent_anchor_template, render_anchor_question_block,
|
||||||
};
|
};
|
||||||
|
use crate::creation_agent_chat::render_quick_fill_extra_rules;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct BigFishAgentTurnRequest<'a> {
|
pub(crate) struct BigFishAgentTurnRequest<'a> {
|
||||||
pub llm_client: Option<&'a LlmClient>,
|
pub llm_client: Option<&'a LlmClient>,
|
||||||
pub session: &'a BigFishSessionRecord,
|
pub session: &'a BigFishSessionRecord,
|
||||||
|
pub quick_fill_requested: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -115,7 +117,7 @@ where
|
|||||||
let llm_client = request
|
let llm_client = request
|
||||||
.llm_client
|
.llm_client
|
||||||
.ok_or_else(|| BigFishAgentTurnError::new("当前模型不可用,请稍后重试。"))?;
|
.ok_or_else(|| BigFishAgentTurnError::new("当前模型不可用,请稍后重试。"))?;
|
||||||
let prompt = build_big_fish_agent_prompt(request.session);
|
let prompt = build_big_fish_agent_prompt(request.session, request.quick_fill_requested);
|
||||||
let mut latest_reply_text = String::new();
|
let mut latest_reply_text = String::new();
|
||||||
let response = llm_client
|
let response = llm_client
|
||||||
.stream_text(
|
.stream_text(
|
||||||
@@ -146,7 +148,11 @@ where
|
|||||||
Ok(BigFishAgentTurnResult {
|
Ok(BigFishAgentTurnResult {
|
||||||
assistant_reply_text: output.reply_text,
|
assistant_reply_text: output.reply_text,
|
||||||
stage: BigFishCreationStage::CollectingAnchors.as_str().to_string(),
|
stage: BigFishCreationStage::CollectingAnchors.as_str().to_string(),
|
||||||
progress_percent: output.progress_percent.min(100),
|
progress_percent: if request.quick_fill_requested {
|
||||||
|
100
|
||||||
|
} else {
|
||||||
|
output.progress_percent.min(100)
|
||||||
|
},
|
||||||
anchor_pack_json: serde_json::to_string(&output.next_anchor_pack)
|
anchor_pack_json: serde_json::to_string(&output.next_anchor_pack)
|
||||||
.unwrap_or_else(|_| "{}".to_string()),
|
.unwrap_or_else(|_| "{}".to_string()),
|
||||||
error_message: None,
|
error_message: None,
|
||||||
@@ -193,15 +199,33 @@ pub(crate) fn build_failed_finalize_record_input(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_big_fish_agent_prompt(session: &BigFishSessionRecord) -> String {
|
fn build_big_fish_agent_prompt(
|
||||||
|
session: &BigFishSessionRecord,
|
||||||
|
quick_fill_requested: bool,
|
||||||
|
) -> String {
|
||||||
let anchor_question_block = get_creation_agent_anchor_template("big_fish")
|
let anchor_question_block = get_creation_agent_anchor_template("big_fish")
|
||||||
.map(render_anchor_question_block)
|
.map(render_anchor_question_block)
|
||||||
.unwrap_or_else(|| "模板目标:收束成可玩的竖屏大鱼吃小鱼玩法草稿。".to_string());
|
.unwrap_or_else(|| "模板目标:收束成可玩的竖屏大鱼吃小鱼玩法草稿。".to_string());
|
||||||
|
let quick_fill_rules = if quick_fill_requested {
|
||||||
|
format!(
|
||||||
|
"\n\n{}",
|
||||||
|
render_quick_fill_extra_rules(
|
||||||
|
"当前玩法方向里的成长、生态、风险节奏等缺失关键词",
|
||||||
|
"不要要求用户再提供等级、鱼群、场景或节奏信息",
|
||||||
|
"输出完整 nextAnchorPack,直接补齐 value 为空或 status 为 missing 的项",
|
||||||
|
"生成结果页",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
format!(
|
format!(
|
||||||
"{anchor_question_block}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n当前 anchor pack:\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
|
"{anchor_question_block}{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动补充剩余关键字:{quick_fill_requested_text}\n\n当前 anchor pack:\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
|
||||||
anchor_question_block = anchor_question_block,
|
anchor_question_block = anchor_question_block,
|
||||||
|
quick_fill_rules = quick_fill_rules,
|
||||||
turn = session.current_turn.saturating_add(1),
|
turn = session.current_turn.saturating_add(1),
|
||||||
progress = session.progress_percent,
|
progress = session.progress_percent,
|
||||||
|
quick_fill_requested_text = if quick_fill_requested { "是" } else { "否" },
|
||||||
anchor_pack = serialize_record_anchor_pack(&session.anchor_pack),
|
anchor_pack = serialize_record_anchor_pack(&session.anchor_pack),
|
||||||
chat_history =
|
chat_history =
|
||||||
serde_json::to_string_pretty(&build_chat_history(session.messages.as_slice()))
|
serde_json::to_string_pretty(&build_chat_history(session.messages.as_slice()))
|
||||||
@@ -315,7 +339,7 @@ fn default_big_fish_anchor_label(field_name: &str) -> &'static str {
|
|||||||
|
|
||||||
fn serialize_record_anchor_pack(anchor_pack: &spacetime_client::BigFishAnchorPackRecord) -> String {
|
fn serialize_record_anchor_pack(anchor_pack: &spacetime_client::BigFishAnchorPackRecord) -> String {
|
||||||
serde_json::to_string_pretty(&map_big_fish_record_anchor_pack(anchor_pack))
|
serde_json::to_string_pretty(&map_big_fish_record_anchor_pack(anchor_pack))
|
||||||
.unwrap_or_else(|_| "{}".to_string())
|
.unwrap_or_else(|_| "{}".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_big_fish_record_anchor_pack(
|
fn map_big_fish_record_anchor_pack(
|
||||||
@@ -399,3 +423,70 @@ fn extract_reply_text_from_partial_json(text: &str) -> Option<String> {
|
|||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::build_big_fish_agent_prompt;
|
||||||
|
|
||||||
|
fn anchor_item(
|
||||||
|
key: &str,
|
||||||
|
label: &str,
|
||||||
|
value: &str,
|
||||||
|
status: &str,
|
||||||
|
) -> spacetime_client::BigFishAnchorItemRecord {
|
||||||
|
spacetime_client::BigFishAnchorItemRecord {
|
||||||
|
key: key.to_string(),
|
||||||
|
label: label.to_string(),
|
||||||
|
value: value.to_string(),
|
||||||
|
status: status.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_session_record() -> spacetime_client::BigFishSessionRecord {
|
||||||
|
spacetime_client::BigFishSessionRecord {
|
||||||
|
session_id: "big-fish-session-test".to_string(),
|
||||||
|
current_turn: 2,
|
||||||
|
progress_percent: 60,
|
||||||
|
stage: "collecting_anchors".to_string(),
|
||||||
|
anchor_pack: spacetime_client::BigFishAnchorPackRecord {
|
||||||
|
gameplay_promise: anchor_item(
|
||||||
|
"gameplayPromise",
|
||||||
|
"玩法承诺",
|
||||||
|
"微光小鱼逆袭深海巨兽",
|
||||||
|
"confirmed",
|
||||||
|
),
|
||||||
|
ecology_visual_theme: anchor_item(
|
||||||
|
"ecologyVisualTheme",
|
||||||
|
"生态视觉主题",
|
||||||
|
"幽蓝珊瑚海沟",
|
||||||
|
"confirmed",
|
||||||
|
),
|
||||||
|
growth_ladder: anchor_item("growthLadder", "成长阶梯", "", "missing"),
|
||||||
|
risk_tempo: anchor_item("riskTempo", "风险节奏", "", "missing"),
|
||||||
|
},
|
||||||
|
draft: None,
|
||||||
|
asset_slots: Vec::new(),
|
||||||
|
asset_coverage: spacetime_client::BigFishAssetCoverageRecord {
|
||||||
|
level_main_image_ready_count: 0,
|
||||||
|
level_motion_ready_count: 0,
|
||||||
|
background_ready: false,
|
||||||
|
required_level_count: 8,
|
||||||
|
publish_ready: false,
|
||||||
|
blockers: Vec::new(),
|
||||||
|
},
|
||||||
|
messages: Vec::new(),
|
||||||
|
last_assistant_reply: None,
|
||||||
|
publish_ready: false,
|
||||||
|
updated_at: "2026-04-24T10:00:00.000Z".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quick_fill_prompt_forbids_follow_up_questions() {
|
||||||
|
let prompt = build_big_fish_agent_prompt(&empty_session_record(), true);
|
||||||
|
|
||||||
|
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
|
||||||
|
assert!(prompt.contains("不要再继续提问"));
|
||||||
|
assert!(prompt.contains("progressPercent 直接输出为 100"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
25
server-rs/crates/api-server/src/creation_agent_chat.rs
Normal file
25
server-rs/crates/api-server/src/creation_agent_chat.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/// 共创聊天中“补充剩余关键字”的统一提示规则。
|
||||||
|
///
|
||||||
|
/// RPG、拼图、大鱼吃小鱼都通过聊天补充设定;这里集中维护点击补全后必须进入
|
||||||
|
/// 自行补齐、不可继续追问、进度推进到可提交状态的公共约束,避免各入口各写一套。
|
||||||
|
pub(crate) fn render_quick_fill_extra_rules(
|
||||||
|
acceptance_scope: &str,
|
||||||
|
forbidden_follow_up: &str,
|
||||||
|
completion_target: &str,
|
||||||
|
submit_hint: &str,
|
||||||
|
) -> String {
|
||||||
|
format!(
|
||||||
|
r#"用户刚刚主动要求你自动补充剩余关键字。
|
||||||
|
|
||||||
|
这表示用户接受你基于当前方向自行补完仍缺失的关键设定:{acceptance_scope}
|
||||||
|
|
||||||
|
本轮要求:
|
||||||
|
1. 不要再继续提问
|
||||||
|
2. {forbidden_follow_up}
|
||||||
|
3. 必须保留已有已确认内容,并直接补齐缺失或仍为空的关键项
|
||||||
|
4. 对你自行推断补齐的项,应标记或表达为系统推断;已有明确内容继续保持确认或锁定状态
|
||||||
|
5. progressPercent 直接输出为 100
|
||||||
|
6. {completion_target}
|
||||||
|
7. replyText 只做简短完成说明,引导用户可以{submit_hint},不能出现问号"#,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -508,7 +508,7 @@ pub async fn get_custom_world_works(
|
|||||||
|
|
||||||
pub async fn delete_custom_world_agent_session(
|
pub async fn delete_custom_world_agent_session(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AxumPath(session_id): AxumPath<String>,
|
Path(session_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
@@ -516,10 +516,7 @@ pub async fn delete_custom_world_agent_session(
|
|||||||
|
|
||||||
let items = state
|
let items = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.delete_custom_world_agent_session(
|
.delete_custom_world_agent_session(session_id, authenticated.claims().user_id().to_string())
|
||||||
session_id,
|
|
||||||
authenticated.claims().user_id().to_string(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
||||||
@@ -1121,7 +1118,7 @@ fn spawn_custom_world_draft_foundation_job(
|
|||||||
"底稿生成失败",
|
"底稿生成失败",
|
||||||
message.clone().as_str(),
|
message.clone().as_str(),
|
||||||
100,
|
100,
|
||||||
Some(message),
|
Some(message.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
@@ -1142,7 +1139,7 @@ fn spawn_custom_world_draft_foundation_job(
|
|||||||
"底稿素材生成失败",
|
"底稿素材生成失败",
|
||||||
message.as_str(),
|
message.as_str(),
|
||||||
100,
|
100,
|
||||||
Some(message),
|
Some(message.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
@@ -1158,7 +1155,7 @@ fn spawn_custom_world_draft_foundation_job(
|
|||||||
"底稿素材生成失败",
|
"底稿素材生成失败",
|
||||||
message.as_str(),
|
message.as_str(),
|
||||||
100,
|
100,
|
||||||
Some(message),
|
Some(message.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
@@ -1183,7 +1180,7 @@ fn spawn_custom_world_draft_foundation_job(
|
|||||||
"生成角色主形象失败",
|
"生成角色主形象失败",
|
||||||
message.as_str(),
|
message.as_str(),
|
||||||
100,
|
100,
|
||||||
Some(message),
|
Some(message.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
@@ -1207,7 +1204,7 @@ fn spawn_custom_world_draft_foundation_job(
|
|||||||
"生成幕背景图失败",
|
"生成幕背景图失败",
|
||||||
message.as_str(),
|
message.as_str(),
|
||||||
100,
|
100,
|
||||||
Some(message),
|
Some(message.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
@@ -1226,7 +1223,7 @@ fn spawn_custom_world_draft_foundation_job(
|
|||||||
"底稿素材写回失败",
|
"底稿素材写回失败",
|
||||||
message.as_str(),
|
message.as_str(),
|
||||||
100,
|
100,
|
||||||
Some(message),
|
Some(message.clone()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
@@ -1916,6 +1913,24 @@ fn custom_world_error_response(request_context: &RequestContext, error: AppError
|
|||||||
error.into_response_with_context(Some(request_context))
|
error.into_response_with_context(Some(request_context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_non_empty(
|
||||||
|
request_context: &RequestContext,
|
||||||
|
value: &str,
|
||||||
|
field_name: &str,
|
||||||
|
) -> Result<(), Response> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return Err(custom_world_error_response(
|
||||||
|
request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "custom-world",
|
||||||
|
"message": format!("{field_name} is required"),
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn custom_world_sse_json_event(event_name: &str, payload: Value) -> Result<Event, AppError> {
|
fn custom_world_sse_json_event(event_name: &str, payload: Value) -> Result<Event, AppError> {
|
||||||
Event::default()
|
Event::default()
|
||||||
.event(event_name)
|
.event(event_name)
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::{Value as JsonValue, json};
|
use serde_json::{Value as JsonValue, json};
|
||||||
|
|
||||||
use crate::custom_world_rpg_draft_prompts::{
|
use crate::custom_world_rpg_draft_prompts::{
|
||||||
BASE_SYSTEM_PROMPT, GLOBAL_HARD_RULES, OUTPUT_CONTRACT_REMINDER, QUICK_FILL_EXTRA_RULES,
|
BASE_SYSTEM_PROMPT, GLOBAL_HARD_RULES, OUTPUT_CONTRACT_REMINDER,
|
||||||
STATE_INFERENCE_OUTPUT_CONTRACT, STATE_INFERENCE_SYSTEM_PROMPT,
|
STATE_INFERENCE_OUTPUT_CONTRACT, STATE_INFERENCE_SYSTEM_PROMPT,
|
||||||
extract_reply_text_from_partial_json, mode_rules, parse_conversation_mode, parse_drift_risk,
|
extract_reply_text_from_partial_json, mode_rules, parse_conversation_mode, parse_drift_risk,
|
||||||
parse_json_response_text, parse_user_input_signal, render_chat_history_context,
|
parse_json_response_text, parse_user_input_signal, quick_fill_extra_rules,
|
||||||
render_current_anchor_context, render_dynamic_state_context, user_signal_rules,
|
render_chat_history_context, render_current_anchor_context, render_dynamic_state_context,
|
||||||
|
user_signal_rules,
|
||||||
};
|
};
|
||||||
use spacetime_client::{
|
use spacetime_client::{
|
||||||
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
|
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
|
||||||
@@ -799,7 +800,7 @@ fn build_eight_anchor_single_turn_prompt(
|
|||||||
user_signal_rules(dynamic_state.user_input_signal).to_string(),
|
user_signal_rules(dynamic_state.user_input_signal).to_string(),
|
||||||
];
|
];
|
||||||
if quick_fill_requested {
|
if quick_fill_requested {
|
||||||
blocks.push(QUICK_FILL_EXTRA_RULES.to_string());
|
blocks.push(quick_fill_extra_rules());
|
||||||
}
|
}
|
||||||
blocks.push(render_dynamic_state_context(dynamic_state));
|
blocks.push(render_dynamic_state_context(dynamic_state));
|
||||||
blocks.push(render_current_anchor_context(current_anchor_content));
|
blocks.push(render_current_anchor_context(current_anchor_content));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::creation_agent_chat::render_quick_fill_extra_rules;
|
||||||
use crate::custom_world_agent_turn::{
|
use crate::custom_world_agent_turn::{
|
||||||
EightAnchorContent, PromptConversationMode, PromptDriftRisk, PromptDynamicState,
|
EightAnchorContent, PromptConversationMode, PromptDriftRisk, PromptDynamicState,
|
||||||
PromptUserInputSignal,
|
PromptUserInputSignal,
|
||||||
@@ -41,15 +42,14 @@ pub(crate) const GLOBAL_HARD_RULES: &str = r#"全局硬约束:
|
|||||||
11. 你输出的 JSON 必须可以被直接解析。
|
11. 你输出的 JSON 必须可以被直接解析。
|
||||||
12. 输出字段顺序必须固定为:replyText、progressPercent、nextAnchorContent。"#;
|
12. 输出字段顺序必须固定为:replyText、progressPercent、nextAnchorContent。"#;
|
||||||
|
|
||||||
pub(crate) const QUICK_FILL_EXTRA_RULES: &str = r#"用户刚刚主动要求你自动补全剩余设定。
|
pub(crate) fn quick_fill_extra_rules() -> String {
|
||||||
|
render_quick_fill_extra_rules(
|
||||||
这表示用户接受你基于当前方向自动补完剩余设定。
|
"当前 RPG 世界方向里的剩余设定",
|
||||||
|
"不要要求用户再提供世界观、角色、冲突或禁忌信息",
|
||||||
本轮要求:
|
"直接输出一版尽量完整的设定结构",
|
||||||
1. 不要再继续提问
|
"进入“生成游戏设定草稿”",
|
||||||
2. 直接输出一版尽量完整的设定结构
|
)
|
||||||
3. progressPercent 直接输出为 100
|
}
|
||||||
4. replyText 要告诉用户现在可以进入“生成游戏设定草稿”"#;
|
|
||||||
|
|
||||||
pub(crate) const STATE_INFERENCE_SYSTEM_PROMPT: &str = r#"你是正式生成世界设定前的一步“创作状态识别器”。
|
pub(crate) const STATE_INFERENCE_SYSTEM_PROMPT: &str = r#"你是正式生成世界设定前的一步“创作状态识别器”。
|
||||||
你的职责不是直接生成新设定,而是先判断:下一轮正式生成应该用什么推进策略,尤其要判断 replyText 应该更偏确认、吸收、收束、纠偏,还是启发式提问。
|
你的职责不是直接生成新设定,而是先判断:下一轮正式生成应该用什么推进策略,尤其要判断 replyText 应该更偏确认、吸收、收束、纠偏,还是启发式提问。
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod character_animation_assets;
|
|||||||
mod character_visual_assets;
|
mod character_visual_assets;
|
||||||
mod config;
|
mod config;
|
||||||
mod creation_agent_anchor_templates;
|
mod creation_agent_anchor_templates;
|
||||||
|
mod creation_agent_chat;
|
||||||
mod custom_world;
|
mod custom_world;
|
||||||
mod custom_world_agent_entities;
|
mod custom_world_agent_entities;
|
||||||
mod custom_world_agent_turn;
|
mod custom_world_agent_turn;
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ pub async fn submit_puzzle_agent_message(
|
|||||||
PuzzleAgentTurnRequest {
|
PuzzleAgentTurnRequest {
|
||||||
llm_client: state.llm_client(),
|
llm_client: state.llm_client(),
|
||||||
session: &submitted_session,
|
session: &submitted_session,
|
||||||
|
quick_fill_requested: payload.quick_fill_requested.unwrap_or(false),
|
||||||
},
|
},
|
||||||
|_| {},
|
|_| {},
|
||||||
)
|
)
|
||||||
@@ -277,6 +278,7 @@ pub async fn stream_puzzle_agent_message(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||||
|
let quick_fill_requested = payload.quick_fill_requested.unwrap_or(false);
|
||||||
let session = state
|
let session = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.submit_puzzle_agent_message(PuzzleAgentMessageSubmitRecordInput {
|
.submit_puzzle_agent_message(PuzzleAgentMessageSubmitRecordInput {
|
||||||
@@ -304,6 +306,7 @@ pub async fn stream_puzzle_agent_message(
|
|||||||
PuzzleAgentTurnRequest {
|
PuzzleAgentTurnRequest {
|
||||||
llm_client: state.llm_client(),
|
llm_client: state.llm_client(),
|
||||||
session: &session,
|
session: &session,
|
||||||
|
quick_fill_requested,
|
||||||
},
|
},
|
||||||
move |text| {
|
move |text| {
|
||||||
let _ = reply_tx.send(text.to_string());
|
let _ = reply_tx.send(text.to_string());
|
||||||
@@ -1505,13 +1508,17 @@ struct GeneratedPuzzleAssetResponse {
|
|||||||
asset_id: String,
|
asset_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_puzzle_dashscope_settings(state: &AppState) -> Result<PuzzleDashScopeSettings, AppError> {
|
fn require_puzzle_dashscope_settings(
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<PuzzleDashScopeSettings, AppError> {
|
||||||
let base_url = state.config.dashscope_base_url.trim().trim_end_matches('/');
|
let base_url = state.config.dashscope_base_url.trim().trim_end_matches('/');
|
||||||
if base_url.is_empty() {
|
if base_url.is_empty() {
|
||||||
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
return Err(
|
||||||
"provider": "dashscope",
|
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||||
"reason": "DASHSCOPE_BASE_URL 未配置",
|
"provider": "dashscope",
|
||||||
})));
|
"reason": "DASHSCOPE_BASE_URL 未配置",
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let api_key = state
|
let api_key = state
|
||||||
@@ -1600,7 +1607,9 @@ async fn create_puzzle_text_to_image_generation(
|
|||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|error| map_puzzle_dashscope_request_error(format!("创建拼图图片生成任务失败:{error}")))?;
|
.map_err(|error| {
|
||||||
|
map_puzzle_dashscope_request_error(format!("创建拼图图片生成任务失败:{error}"))
|
||||||
|
})?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let response_text = response.text().await.map_err(|error| {
|
let response_text = response.text().await.map_err(|error| {
|
||||||
map_puzzle_dashscope_request_error(format!("读取拼图图片生成响应失败:{error}"))
|
map_puzzle_dashscope_request_error(format!("读取拼图图片生成响应失败:{error}"))
|
||||||
@@ -1642,7 +1651,8 @@ async fn create_puzzle_text_to_image_generation(
|
|||||||
"查询拼图图片生成任务失败",
|
"查询拼图图片生成任务失败",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let poll_payload = parse_puzzle_json_payload(poll_text.as_str(), "解析拼图图片生成任务响应失败")?;
|
let poll_payload =
|
||||||
|
parse_puzzle_json_payload(poll_text.as_str(), "解析拼图图片生成任务响应失败")?;
|
||||||
let task_status = find_first_puzzle_string_by_key(&poll_payload, "task_status")
|
let task_status = find_first_puzzle_string_by_key(&poll_payload, "task_status")
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -1650,13 +1660,18 @@ async fn create_puzzle_text_to_image_generation(
|
|||||||
if task_status == "SUCCEEDED" {
|
if task_status == "SUCCEEDED" {
|
||||||
let image_urls = extract_puzzle_image_urls(&poll_payload);
|
let image_urls = extract_puzzle_image_urls(&poll_payload);
|
||||||
if image_urls.is_empty() {
|
if image_urls.is_empty() {
|
||||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
return Err(
|
||||||
"provider": "dashscope",
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||||
"message": "拼图图片生成成功但未返回图片地址",
|
"provider": "dashscope",
|
||||||
})));
|
"message": "拼图图片生成成功但未返回图片地址",
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mut images = Vec::with_capacity(image_urls.len());
|
let mut images = Vec::with_capacity(image_urls.len());
|
||||||
for image_url in image_urls.into_iter().take(candidate_count.clamp(1, 2) as usize) {
|
for image_url in image_urls
|
||||||
|
.into_iter()
|
||||||
|
.take(candidate_count.clamp(1, 2) as usize)
|
||||||
|
{
|
||||||
images.push(download_puzzle_remote_image(http_client, image_url.as_str()).await?);
|
images.push(download_puzzle_remote_image(http_client, image_url.as_str()).await?);
|
||||||
}
|
}
|
||||||
return Ok(PuzzleGeneratedImages { task_id, images });
|
return Ok(PuzzleGeneratedImages { task_id, images });
|
||||||
@@ -1670,10 +1685,12 @@ async fn create_puzzle_text_to_image_generation(
|
|||||||
sleep(Duration::from_secs(2)).await;
|
sleep(Duration::from_secs(2)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
Err(
|
||||||
"provider": "dashscope",
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||||
"message": "拼图图片生成超时或未返回图片地址",
|
"provider": "dashscope",
|
||||||
})))
|
"message": "拼图图片生成超时或未返回图片地址",
|
||||||
|
})),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_puzzle_remote_image(
|
async fn download_puzzle_remote_image(
|
||||||
@@ -1694,11 +1711,13 @@ async fn download_puzzle_remote_image(
|
|||||||
map_puzzle_dashscope_request_error(format!("读取拼图正式图片内容失败:{error}"))
|
map_puzzle_dashscope_request_error(format!("读取拼图正式图片内容失败:{error}"))
|
||||||
})?;
|
})?;
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
return Err(
|
||||||
"provider": "dashscope",
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||||
"message": "下载拼图正式图片失败",
|
"provider": "dashscope",
|
||||||
"status": status.as_u16(),
|
"message": "下载拼图正式图片失败",
|
||||||
})));
|
"status": status.as_u16(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let mime_type = normalize_puzzle_downloaded_image_mime_type(content_type.as_str());
|
let mime_type = normalize_puzzle_downloaded_image_mime_type(content_type.as_str());
|
||||||
Ok(PuzzleDownloadedImage {
|
Ok(PuzzleDownloadedImage {
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ use spacetime_client::{
|
|||||||
use crate::creation_agent_anchor_templates::{
|
use crate::creation_agent_anchor_templates::{
|
||||||
get_creation_agent_anchor_template, render_anchor_question_block,
|
get_creation_agent_anchor_template, render_anchor_question_block,
|
||||||
};
|
};
|
||||||
|
use crate::creation_agent_chat::render_quick_fill_extra_rules;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct PuzzleAgentTurnRequest<'a> {
|
pub(crate) struct PuzzleAgentTurnRequest<'a> {
|
||||||
pub llm_client: Option<&'a LlmClient>,
|
pub llm_client: Option<&'a LlmClient>,
|
||||||
pub session: &'a PuzzleAgentSessionRecord,
|
pub session: &'a PuzzleAgentSessionRecord,
|
||||||
|
pub quick_fill_requested: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -122,7 +124,7 @@ where
|
|||||||
.llm_client
|
.llm_client
|
||||||
.ok_or_else(|| PuzzleAgentTurnError::new("当前模型不可用,请稍后重试。"))?;
|
.ok_or_else(|| PuzzleAgentTurnError::new("当前模型不可用,请稍后重试。"))?;
|
||||||
|
|
||||||
let prompt = build_puzzle_agent_prompt(request.session);
|
let prompt = build_puzzle_agent_prompt(request.session, request.quick_fill_requested);
|
||||||
let mut latest_reply_text = String::new();
|
let mut latest_reply_text = String::new();
|
||||||
let response = llm_client
|
let response = llm_client
|
||||||
.stream_text(
|
.stream_text(
|
||||||
@@ -155,7 +157,11 @@ where
|
|||||||
stage: resolve_puzzle_agent_stage(output.progress_percent)
|
stage: resolve_puzzle_agent_stage(output.progress_percent)
|
||||||
.as_str()
|
.as_str()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
progress_percent: output.progress_percent,
|
progress_percent: if request.quick_fill_requested {
|
||||||
|
100
|
||||||
|
} else {
|
||||||
|
output.progress_percent
|
||||||
|
},
|
||||||
anchor_pack_json: serde_json::to_string(&output.next_anchor_pack).unwrap_or_else(|_| {
|
anchor_pack_json: serde_json::to_string(&output.next_anchor_pack).unwrap_or_else(|_| {
|
||||||
serde_json::to_string(&empty_anchor_pack()).unwrap_or_else(|_| "{}".to_string())
|
serde_json::to_string(&empty_anchor_pack()).unwrap_or_else(|_| "{}".to_string())
|
||||||
}),
|
}),
|
||||||
@@ -207,15 +213,33 @@ pub(crate) fn build_failed_finalize_record_input(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_puzzle_agent_prompt(session: &PuzzleAgentSessionRecord) -> String {
|
fn build_puzzle_agent_prompt(
|
||||||
|
session: &PuzzleAgentSessionRecord,
|
||||||
|
quick_fill_requested: bool,
|
||||||
|
) -> String {
|
||||||
let anchor_question_block = get_creation_agent_anchor_template("puzzle")
|
let anchor_question_block = get_creation_agent_anchor_template("puzzle")
|
||||||
.map(render_anchor_question_block)
|
.map(render_anchor_question_block)
|
||||||
.unwrap_or_else(|| "模板目标:收束成可以发布为拼图关卡的视觉方案。".to_string());
|
.unwrap_or_else(|| "模板目标:收束成可以发布为拼图关卡的视觉方案。".to_string());
|
||||||
|
let quick_fill_rules = if quick_fill_requested {
|
||||||
|
format!(
|
||||||
|
"\n\n{}",
|
||||||
|
render_quick_fill_extra_rules(
|
||||||
|
"当前题材方向里的拼图关键词",
|
||||||
|
"不要要求用户再提供素材、风格或禁忌",
|
||||||
|
"输出完整 nextAnchorPack,直接补齐 value 为空或 status 为 missing 的项",
|
||||||
|
"生成结果页",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
format!(
|
format!(
|
||||||
"{anchor_question_block}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n当前 anchor pack:\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
|
"{anchor_question_block}{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动补充剩余关键字:{quick_fill_requested_text}\n\n当前 anchor pack:\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
|
||||||
anchor_question_block = anchor_question_block,
|
anchor_question_block = anchor_question_block,
|
||||||
|
quick_fill_rules = quick_fill_rules,
|
||||||
turn = session.current_turn.saturating_add(1),
|
turn = session.current_turn.saturating_add(1),
|
||||||
progress = session.progress_percent,
|
progress = session.progress_percent,
|
||||||
|
quick_fill_requested_text = if quick_fill_requested { "是" } else { "否" },
|
||||||
anchor_pack = serde_json::to_string_pretty(&map_record_anchor_pack(&session.anchor_pack))
|
anchor_pack = serde_json::to_string_pretty(&map_record_anchor_pack(&session.anchor_pack))
|
||||||
.unwrap_or_else(|_| "{}".to_string()),
|
.unwrap_or_else(|_| "{}".to_string()),
|
||||||
chat_history =
|
chat_history =
|
||||||
@@ -430,7 +454,57 @@ mod tests {
|
|||||||
use module_puzzle::PuzzleAnchorStatus;
|
use module_puzzle::PuzzleAnchorStatus;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{extract_reply_text_from_partial_json, parse_model_output};
|
use super::{
|
||||||
|
build_puzzle_agent_prompt, extract_reply_text_from_partial_json, parse_model_output,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn empty_session_record() -> spacetime_client::PuzzleAgentSessionRecord {
|
||||||
|
spacetime_client::PuzzleAgentSessionRecord {
|
||||||
|
session_id: "puzzle-session-test".to_string(),
|
||||||
|
current_turn: 2,
|
||||||
|
progress_percent: 60,
|
||||||
|
stage: "collecting_anchors".to_string(),
|
||||||
|
anchor_pack: spacetime_client::PuzzleAnchorPackRecord {
|
||||||
|
theme_promise: spacetime_client::PuzzleAnchorItemRecord {
|
||||||
|
key: "themePromise".to_string(),
|
||||||
|
label: "题材承诺".to_string(),
|
||||||
|
value: "雨夜猫咪遗迹".to_string(),
|
||||||
|
status: "confirmed".to_string(),
|
||||||
|
},
|
||||||
|
visual_subject: spacetime_client::PuzzleAnchorItemRecord {
|
||||||
|
key: "visualSubject".to_string(),
|
||||||
|
label: "画面主体".to_string(),
|
||||||
|
value: String::new(),
|
||||||
|
status: "missing".to_string(),
|
||||||
|
},
|
||||||
|
visual_mood: spacetime_client::PuzzleAnchorItemRecord {
|
||||||
|
key: "visualMood".to_string(),
|
||||||
|
label: "视觉气质".to_string(),
|
||||||
|
value: String::new(),
|
||||||
|
status: "missing".to_string(),
|
||||||
|
},
|
||||||
|
composition_hooks: spacetime_client::PuzzleAnchorItemRecord {
|
||||||
|
key: "compositionHooks".to_string(),
|
||||||
|
label: "拼图记忆点".to_string(),
|
||||||
|
value: String::new(),
|
||||||
|
status: "missing".to_string(),
|
||||||
|
},
|
||||||
|
tags_and_forbidden: spacetime_client::PuzzleAnchorItemRecord {
|
||||||
|
key: "tagsAndForbidden".to_string(),
|
||||||
|
label: "标签与禁忌".to_string(),
|
||||||
|
value: String::new(),
|
||||||
|
status: "missing".to_string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
draft: None,
|
||||||
|
messages: Vec::new(),
|
||||||
|
last_assistant_reply: None,
|
||||||
|
published_profile_id: None,
|
||||||
|
suggested_actions: Vec::new(),
|
||||||
|
result_preview: None,
|
||||||
|
updated_at: "2026-04-24T10:00:00.000Z".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extract_reply_text_from_partial_json_preserves_chinese_characters() {
|
fn extract_reply_text_from_partial_json_preserves_chinese_characters() {
|
||||||
@@ -496,4 +570,13 @@ mod tests {
|
|||||||
"雨夜、猫咪、神庙遗迹;禁止文字水印"
|
"雨夜、猫咪、神庙遗迹;禁止文字水印"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quick_fill_prompt_forbids_follow_up_questions() {
|
||||||
|
let prompt = build_puzzle_agent_prompt(&empty_session_record(), true);
|
||||||
|
|
||||||
|
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
|
||||||
|
assert!(prompt.contains("不要再继续提问"));
|
||||||
|
assert!(prompt.contains("progressPercent 直接输出为 100"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ pub struct CreateBigFishSessionRequest {
|
|||||||
pub struct SendBigFishMessageRequest {
|
pub struct SendBigFishMessageRequest {
|
||||||
pub client_message_id: String,
|
pub client_message_id: String,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub quick_fill_requested: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ pub struct CreatePuzzleAgentSessionRequest {
|
|||||||
pub struct SendPuzzleAgentMessageRequest {
|
pub struct SendPuzzleAgentMessageRequest {
|
||||||
pub client_message_id: String,
|
pub client_message_id: String,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub quick_fill_requested: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@@ -81,11 +81,12 @@ test('big fish workspace submits quick keyword fill request after two turns', as
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: '补充剩余关键字' }));
|
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
|
||||||
|
|
||||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
text: '请补充剩余关键字。',
|
text: '请补充剩余设定。',
|
||||||
|
quickFillRequested: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -100,5 +101,5 @@ test('big fish workspace hides keyword fill before two turns', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByRole('button', { name: '补充剩余关键字' })).toBeNull();
|
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import type {
|
|||||||
ExecuteBigFishActionRequest,
|
ExecuteBigFishActionRequest,
|
||||||
SendBigFishMessageRequest,
|
SendBigFishMessageRequest,
|
||||||
} from '../../../packages/shared/src/contracts/bigFish';
|
} from '../../../packages/shared/src/contracts/bigFish';
|
||||||
import { createCreationAgentClientMessageId } from '../../services/creation-agent';
|
import {
|
||||||
|
buildCreationAgentChatMessage,
|
||||||
|
createCreationAgentChatQuickActions,
|
||||||
|
createCreationAgentClientMessageId,
|
||||||
|
resolveCreationAgentQuickActionMessage,
|
||||||
|
} from '../../services/creation-agent';
|
||||||
import {
|
import {
|
||||||
type CreationAgentAnchorView,
|
type CreationAgentAnchorView,
|
||||||
type CreationAgentSessionView,
|
type CreationAgentSessionView,
|
||||||
@@ -84,35 +89,30 @@ export function BigFishAgentWorkspace({
|
|||||||
isStreamingReply={Boolean(streamingReplyText)}
|
isStreamingReply={Boolean(streamingReplyText)}
|
||||||
isBusy={isBusy}
|
isBusy={isBusy}
|
||||||
error={error}
|
error={error}
|
||||||
quickActions={[
|
quickActions={createCreationAgentChatQuickActions()}
|
||||||
{
|
|
||||||
key: 'summarize',
|
|
||||||
label: '总结当前设定',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quickFill',
|
|
||||||
label: '补充剩余关键字',
|
|
||||||
minTurn: 2,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onSubmitText={(text) => {
|
onSubmitText={(text) => {
|
||||||
onSubmitMessage({
|
onSubmitMessage(
|
||||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
buildCreationAgentChatMessage({
|
||||||
text,
|
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||||
});
|
text,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onPrimaryAction={() => {
|
onPrimaryAction={() => {
|
||||||
onExecuteAction({ action: 'big_fish_compile_draft' });
|
onExecuteAction({ action: 'big_fish_compile_draft' });
|
||||||
}}
|
}}
|
||||||
onQuickAction={(action) => {
|
onQuickAction={(action) => {
|
||||||
onSubmitMessage({
|
const quickActionMessage = resolveCreationAgentQuickActionMessage(
|
||||||
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
action.key,
|
||||||
text:
|
'请总结一下当前已经成形的大鱼吃小鱼设定。',
|
||||||
action.key === 'quickFill'
|
);
|
||||||
? '请补充剩余关键字。'
|
onSubmitMessage(
|
||||||
: '请总结一下当前已经成形的大鱼吃小鱼设定。',
|
buildCreationAgentChatMessage({
|
||||||
});
|
clientMessageId: createCreationAgentClientMessageId('big-fish'),
|
||||||
|
...quickActionMessage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { afterEach, expect, test, vi } from 'vitest';
|
import { afterEach, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createCreationAgentChatQuickActions } from '../../services/creation-agent';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type CreationAgentTheme,
|
type CreationAgentTheme,
|
||||||
CreationAgentWorkspace,
|
CreationAgentWorkspace,
|
||||||
@@ -243,17 +245,7 @@ test('creation agent workspace shows primary and progress actions at completed p
|
|||||||
loadingText="正在准备"
|
loadingText="正在准备"
|
||||||
composerPlaceholder="输入消息"
|
composerPlaceholder="输入消息"
|
||||||
primaryActionLabel="生成结果页"
|
primaryActionLabel="生成结果页"
|
||||||
quickActions={[
|
quickActions={createCreationAgentChatQuickActions()}
|
||||||
{
|
|
||||||
key: 'summarize',
|
|
||||||
label: '总结当前设定',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quickFill',
|
|
||||||
label: '补全剩余设定',
|
|
||||||
minTurn: 2,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onBack={() => {}}
|
onBack={() => {}}
|
||||||
onSubmitText={() => {}}
|
onSubmitText={() => {}}
|
||||||
onPrimaryAction={() => {}}
|
onPrimaryAction={() => {}}
|
||||||
@@ -262,7 +254,7 @@ test('creation agent workspace shows primary and progress actions at completed p
|
|||||||
|
|
||||||
expect(screen.getByRole('button', { name: '生成结果页' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: '生成结果页' })).toBeTruthy();
|
||||||
expect(screen.getByRole('button', { name: '总结当前设定' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: '总结当前设定' })).toBeTruthy();
|
||||||
expect(screen.getByRole('button', { name: '补全剩余设定' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: '补充剩余设定' })).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creation agent workspace hides hero copy area when title and summary are absent', () => {
|
test('creation agent workspace hides hero copy area when title and summary are absent', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@@ -108,11 +108,11 @@ test('workspace enables quick fill after at least two turns and submits quick fi
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: '补全剩余设定' }));
|
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
|
||||||
|
|
||||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
text: '请补全剩余设定。',
|
text: '请补充剩余设定。',
|
||||||
quickFillRequested: true,
|
quickFillRequested: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import type {
|
|||||||
SendCustomWorldAgentMessageRequest,
|
SendCustomWorldAgentMessageRequest,
|
||||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||||
import {
|
import {
|
||||||
|
buildCreationAgentChatMessage,
|
||||||
|
createCreationAgentChatQuickActions,
|
||||||
createCreationAgentClientMessageId,
|
createCreationAgentClientMessageId,
|
||||||
isCreationAgentOperationBusy,
|
isCreationAgentOperationBusy,
|
||||||
|
resolveCreationAgentQuickActionMessage,
|
||||||
} from '../../services/creation-agent';
|
} from '../../services/creation-agent';
|
||||||
import {
|
import {
|
||||||
type CreationAgentAnchorView,
|
type CreationAgentAnchorView,
|
||||||
@@ -166,13 +169,17 @@ export function CustomWorldAgentWorkspace({
|
|||||||
isCreationAgentOperationBusy(activeOperation) || isStreamingReply;
|
isCreationAgentOperationBusy(activeOperation) || isStreamingReply;
|
||||||
|
|
||||||
const submitMessage = (text: string, quickFillRequested = false) => {
|
const submitMessage = (text: string, quickFillRequested = false) => {
|
||||||
onSubmitMessage({
|
onSubmitMessage(
|
||||||
clientMessageId: createCreationAgentClientMessageId('custom-world'),
|
buildCreationAgentChatMessage({
|
||||||
text,
|
clientMessageId: createCreationAgentClientMessageId('custom-world'),
|
||||||
quickFillRequested,
|
text,
|
||||||
focusCardId: null,
|
quickFillRequested,
|
||||||
selectedCardIds: [],
|
extraPayload: {
|
||||||
});
|
focusCardId: null,
|
||||||
|
selectedCardIds: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -186,17 +193,7 @@ export function CustomWorldAgentWorkspace({
|
|||||||
streamingReplyText={streamingReplyText}
|
streamingReplyText={streamingReplyText}
|
||||||
isStreamingReply={isStreamingReply}
|
isStreamingReply={isStreamingReply}
|
||||||
isBusy={isBusy}
|
isBusy={isBusy}
|
||||||
quickActions={[
|
quickActions={createCreationAgentChatQuickActions()}
|
||||||
{
|
|
||||||
key: 'summarize',
|
|
||||||
label: '总结当前设定',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quickFill',
|
|
||||||
label: '补全剩余设定',
|
|
||||||
minTurn: 2,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onSubmitText={(text) => {
|
onSubmitText={(text) => {
|
||||||
submitMessage(text);
|
submitMessage(text);
|
||||||
@@ -207,12 +204,14 @@ export function CustomWorldAgentWorkspace({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onQuickAction={(action) => {
|
onQuickAction={(action) => {
|
||||||
if (action.key === 'quickFill') {
|
const quickActionMessage = resolveCreationAgentQuickActionMessage(
|
||||||
submitMessage('请补全剩余设定。', true);
|
action.key,
|
||||||
return;
|
'请总结一下当前已经成形的世界设定。',
|
||||||
}
|
);
|
||||||
|
submitMessage(
|
||||||
submitMessage('请总结一下当前已经成形的世界设定。');
|
quickActionMessage.text,
|
||||||
|
quickActionMessage.quickFillRequested,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@@ -80,11 +80,12 @@ test('puzzle workspace submits quick keyword fill request after two turns', asyn
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: '补充剩余关键字' }));
|
await user.click(screen.getByRole('button', { name: '补充剩余设定' }));
|
||||||
|
|
||||||
expect(onSubmitMessage).toHaveBeenCalledWith(
|
expect(onSubmitMessage).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
text: '请补充剩余关键字。',
|
text: '请补充剩余设定。',
|
||||||
|
quickFillRequested: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -99,5 +100,5 @@ test('puzzle workspace hides keyword fill before two turns', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByRole('button', { name: '补充剩余关键字' })).toBeNull();
|
expect(screen.queryByRole('button', { name: '补充剩余设定' })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import type {
|
|||||||
PuzzleAgentSessionSnapshot,
|
PuzzleAgentSessionSnapshot,
|
||||||
SendPuzzleAgentMessageRequest,
|
SendPuzzleAgentMessageRequest,
|
||||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||||
import { createCreationAgentClientMessageId } from '../../services/creation-agent';
|
import {
|
||||||
|
buildCreationAgentChatMessage,
|
||||||
|
createCreationAgentChatQuickActions,
|
||||||
|
createCreationAgentClientMessageId,
|
||||||
|
resolveCreationAgentQuickActionMessage,
|
||||||
|
} from '../../services/creation-agent';
|
||||||
import {
|
import {
|
||||||
type CreationAgentOperationView,
|
type CreationAgentOperationView,
|
||||||
type CreationAgentSessionView,
|
type CreationAgentSessionView,
|
||||||
@@ -100,35 +105,30 @@ export function PuzzleAgentWorkspace({
|
|||||||
isStreamingReply={Boolean(streamingReplyText)}
|
isStreamingReply={Boolean(streamingReplyText)}
|
||||||
isBusy={isBusy}
|
isBusy={isBusy}
|
||||||
error={error}
|
error={error}
|
||||||
quickActions={[
|
quickActions={createCreationAgentChatQuickActions()}
|
||||||
{
|
|
||||||
key: 'summarize',
|
|
||||||
label: '总结当前设定',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'quickFill',
|
|
||||||
label: '补充剩余关键字',
|
|
||||||
minTurn: 2,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onSubmitText={(text) => {
|
onSubmitText={(text) => {
|
||||||
onSubmitMessage({
|
onSubmitMessage(
|
||||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
buildCreationAgentChatMessage({
|
||||||
text,
|
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||||
});
|
text,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onPrimaryAction={() => {
|
onPrimaryAction={() => {
|
||||||
onExecuteAction({ action: 'compile_puzzle_draft' });
|
onExecuteAction({ action: 'compile_puzzle_draft' });
|
||||||
}}
|
}}
|
||||||
onQuickAction={(action) => {
|
onQuickAction={(action) => {
|
||||||
onSubmitMessage({
|
const quickActionMessage = resolveCreationAgentQuickActionMessage(
|
||||||
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
action.key,
|
||||||
text:
|
'请总结一下当前已经成形的拼图设定。',
|
||||||
action.key === 'quickFill'
|
);
|
||||||
? '请补充剩余关键字。'
|
onSubmitMessage(
|
||||||
: '请总结一下当前已经成形的拼图设定。',
|
buildCreationAgentChatMessage({
|
||||||
});
|
clientMessageId: createCreationAgentClientMessageId('puzzle'),
|
||||||
|
...quickActionMessage,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
58
src/services/creation-agent/creationAgentChat.test.ts
Normal file
58
src/services/creation-agent/creationAgentChat.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CREATION_AGENT_QUICK_FILL_MESSAGE,
|
||||||
|
buildCreationAgentChatMessage,
|
||||||
|
createCreationAgentChatQuickActions,
|
||||||
|
resolveCreationAgentQuickActionMessage,
|
||||||
|
} from './creationAgentChat';
|
||||||
|
|
||||||
|
test('creation agent chat exposes the unified summary and quick fill actions', () => {
|
||||||
|
expect(createCreationAgentChatQuickActions()).toEqual([
|
||||||
|
{
|
||||||
|
key: 'summarize',
|
||||||
|
label: '总结当前设定',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'quickFill',
|
||||||
|
label: '补充剩余设定',
|
||||||
|
minTurn: 2,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creation agent chat resolves quick actions through one message contract', () => {
|
||||||
|
expect(
|
||||||
|
resolveCreationAgentQuickActionMessage('quickFill', '请总结当前设定。'),
|
||||||
|
).toEqual({
|
||||||
|
text: CREATION_AGENT_QUICK_FILL_MESSAGE,
|
||||||
|
quickFillRequested: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveCreationAgentQuickActionMessage('summarize', '请总结当前设定。'),
|
||||||
|
).toEqual({
|
||||||
|
text: '请总结当前设定。',
|
||||||
|
quickFillRequested: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creation agent chat builds shared message payload with genre extras', () => {
|
||||||
|
expect(
|
||||||
|
buildCreationAgentChatMessage({
|
||||||
|
clientMessageId: 'message-1',
|
||||||
|
text: '请补充剩余设定。',
|
||||||
|
quickFillRequested: true,
|
||||||
|
extraPayload: {
|
||||||
|
focusCardId: null,
|
||||||
|
selectedCardIds: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
clientMessageId: 'message-1',
|
||||||
|
text: '请补充剩余设定。',
|
||||||
|
quickFillRequested: true,
|
||||||
|
focusCardId: null,
|
||||||
|
selectedCardIds: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
63
src/services/creation-agent/creationAgentChat.ts
Normal file
63
src/services/creation-agent/creationAgentChat.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
export const CREATION_AGENT_SUMMARY_ACTION_KEY = 'summarize';
|
||||||
|
export const CREATION_AGENT_QUICK_FILL_ACTION_KEY = 'quickFill';
|
||||||
|
|
||||||
|
export const CREATION_AGENT_SUMMARY_ACTION_LABEL = '总结当前设定';
|
||||||
|
export const CREATION_AGENT_QUICK_FILL_ACTION_LABEL = '补充剩余设定';
|
||||||
|
export const CREATION_AGENT_QUICK_FILL_MESSAGE = '请补充剩余设定。';
|
||||||
|
|
||||||
|
type CreationAgentChatQuickAction = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
minTurn?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreationAgentChatMessageBase = {
|
||||||
|
clientMessageId: string;
|
||||||
|
text: string;
|
||||||
|
quickFillRequested?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCreationAgentChatQuickActions(): CreationAgentChatQuickAction[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: CREATION_AGENT_SUMMARY_ACTION_KEY,
|
||||||
|
label: CREATION_AGENT_SUMMARY_ACTION_LABEL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: CREATION_AGENT_QUICK_FILL_ACTION_KEY,
|
||||||
|
label: CREATION_AGENT_QUICK_FILL_ACTION_LABEL,
|
||||||
|
minTurn: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCreationAgentQuickActionMessage(
|
||||||
|
actionKey: string,
|
||||||
|
summaryMessage: string,
|
||||||
|
) {
|
||||||
|
const quickFillRequested = actionKey === CREATION_AGENT_QUICK_FILL_ACTION_KEY;
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: quickFillRequested ? CREATION_AGENT_QUICK_FILL_MESSAGE : summaryMessage,
|
||||||
|
quickFillRequested,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCreationAgentChatMessage<TExtraPayload extends object = Record<string, never>>({
|
||||||
|
clientMessageId,
|
||||||
|
text,
|
||||||
|
quickFillRequested = false,
|
||||||
|
extraPayload,
|
||||||
|
}: {
|
||||||
|
clientMessageId: string;
|
||||||
|
text: string;
|
||||||
|
quickFillRequested?: boolean;
|
||||||
|
extraPayload?: TExtraPayload;
|
||||||
|
}): CreationAgentChatMessageBase & TExtraPayload {
|
||||||
|
return {
|
||||||
|
...(extraPayload ?? ({} as TExtraPayload)),
|
||||||
|
clientMessageId,
|
||||||
|
text,
|
||||||
|
quickFillRequested,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export * from './creationAgentChat';
|
||||||
export * from './creationAgentProgress';
|
export * from './creationAgentProgress';
|
||||||
export * from './creationAgentSse';
|
export * from './creationAgentSse';
|
||||||
|
|||||||
Reference in New Issue
Block a user