This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -17,6 +17,7 @@ module-assets = { workspace = true, features = ["server-service"] }
module-auth = { workspace = true }
module-big-fish = { workspace = true }
module-combat = { workspace = true }
module-creative-agent = { workspace = true }
module-custom-world = { workspace = true }
module-inventory = { workspace = true }
module-match3d = { workspace = true }
@@ -27,6 +28,8 @@ module-runtime-story = { workspace = true }
module-runtime-item = { workspace = true }
module-square-hole = { workspace = true }
module-story = { workspace = true }
module-visual-novel = { workspace = true }
platform-agent = { workspace = true }
platform-auth = { workspace = true }
platform-llm = { workspace = true }
platform-oss = { workspace = true }
@@ -44,6 +47,7 @@ tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
zip = { workspace = true, features = ["deflate"] }
[dev-dependencies]
base64 = { workspace = true }

View File

@@ -49,6 +49,11 @@ use crate::{
generate_character_visual, get_character_visual_job, publish_character_visual,
},
creation_agent_document_input::parse_creation_agent_document_input,
creative_agent::{
cancel_creative_agent_session, confirm_creative_puzzle_template,
create_creative_agent_session, get_creative_agent_session, stream_creative_agent_message,
stream_creative_draft_edit,
},
custom_world::{
create_custom_world_agent_session, delete_custom_world_agent_session,
delete_custom_world_library_profile, execute_custom_world_agent_action,
@@ -140,6 +145,14 @@ use crate::{
begin_story_runtime_session, begin_story_session, continue_story,
get_story_runtime_projection, get_story_session_state, resolve_story_runtime_action,
},
visual_novel::{
compile_visual_novel_session, create_visual_novel_session, delete_visual_novel_work,
execute_visual_novel_action, get_visual_novel_run, get_visual_novel_session,
get_visual_novel_work, list_visual_novel_gallery, list_visual_novel_history,
list_visual_novel_works, publish_visual_novel_work, regenerate_visual_novel_run,
start_visual_novel_run, stream_visual_novel_action, stream_visual_novel_message,
submit_visual_novel_message, update_visual_novel_work,
},
wechat_auth::{bind_wechat_phone, handle_wechat_callback, start_wechat_login},
};
@@ -1014,6 +1027,8 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.merge(creative_agent_router(state.clone()))
.merge(visual_novel_router(state.clone()))
.route(
"/api/runtime/puzzle/onboarding/generate",
post(generate_puzzle_onboarding_work).layer(DefaultBodyLimit::max(
@@ -1428,6 +1443,173 @@ pub fn build_router(state: AppState) -> Router {
.with_state(state)
}
fn creative_agent_router(state: AppState) -> Router<AppState> {
Router::new()
.route(
"/api/runtime/creative-agent/sessions",
post(create_creative_agent_session)
// 中文注释:创意 Agent 首轮允许携带参考图 URL/Data URL沿用拼图参考图入口上限。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/creative-agent/sessions/{session_id}",
get(get_creative_agent_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/creative-agent/sessions/{session_id}/messages/stream",
post(stream_creative_agent_message)
// 中文注释message stream 同样可能带图片素材,避免默认 JSON limit 过早拒绝。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/creative-agent/sessions/{session_id}/confirm-template",
post(confirm_creative_puzzle_template).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/creative-agent/sessions/{session_id}/draft-edits/stream",
post(stream_creative_draft_edit)
// 中文注释:草稿编辑会携带当前 puzzle draft JSON保持和拼图草稿入口一致的 body 上限。
.layer(DefaultBodyLimit::max(
PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/creative-agent/sessions/{session_id}/cancel",
post(cancel_creative_agent_session)
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
)
}
fn visual_novel_router(state: AppState) -> Router<AppState> {
Router::new()
.route(
"/api/creation/visual-novel/sessions",
post(create_visual_novel_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/sessions/{session_id}",
get(get_visual_novel_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/sessions/{session_id}/messages",
post(submit_visual_novel_message).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/sessions/{session_id}/messages/stream",
post(stream_visual_novel_message).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/sessions/{session_id}/actions",
post(execute_visual_novel_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/sessions/{session_id}/compile",
post(compile_visual_novel_session).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/works",
get(list_visual_novel_works).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/works/{profile_id}",
get(get_visual_novel_work)
.put(update_visual_novel_work)
.patch(update_visual_novel_work)
.delete(delete_visual_novel_work)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/visual-novel/works/{profile_id}/publish",
post(publish_visual_novel_work).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/visual-novel/gallery",
get(list_visual_novel_gallery),
)
.route(
"/api/runtime/visual-novel/works/{profile_id}/runs",
post(start_visual_novel_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/visual-novel/runs/{run_id}",
get(get_visual_novel_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/visual-novel/runs/{run_id}/actions/stream",
post(stream_visual_novel_action).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/visual-novel/runs/{run_id}/history",
get(list_visual_novel_history).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/visual-novel/runs/{run_id}/regenerate",
post(regenerate_visual_novel_run)
.route_layer(middleware::from_fn_with_state(state, require_bearer_auth)),
)
}
#[cfg(test)]
mod tests {
use axum::{
@@ -1450,6 +1632,8 @@ mod tests {
use super::build_router;
const TEST_PASSWORD: &str = "secret123";
const INTERNAL_TEST_SECRET: &str = "test-internal-secret";
async fn seed_phone_user_with_password(
state: &AppState,
phone_number: &str,
@@ -1507,6 +1691,43 @@ mod tests {
.expect("password login request should succeed")
}
fn build_internal_creative_agent_app() -> Router {
let mut config = AppConfig::default();
config.internal_api_secret = Some(INTERNAL_TEST_SECRET.to_string());
build_router(AppState::new(config).expect("state should build"))
}
fn internal_creative_agent_request(method: &str, uri: &str, body: Value) -> Request<Body> {
Request::builder()
.method(method)
.uri(uri)
.header("content-type", "application/json")
.header("x-genarrative-authenticated-user-id", "user-creative-test")
.header("x-genarrative-internal-api-secret", INTERNAL_TEST_SECRET)
.body(Body::from(body.to_string()))
.expect("creative agent request should build")
}
async fn read_json_response(response: axum::response::Response) -> Value {
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
serde_json::from_slice(&body).expect("response body should be valid json")
}
async fn read_text_response(response: axum::response::Response) -> String {
let body = response
.into_body()
.collect()
.await
.expect("response body should collect")
.to_bytes();
String::from_utf8(body.to_vec()).expect("response body should be utf8")
}
#[tokio::test]
async fn healthz_returns_legacy_compatible_payload_and_headers() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -1600,6 +1821,244 @@ mod tests {
);
}
#[tokio::test]
async fn creative_agent_draft_edit_rejects_unconfirmed_template_session() {
let app = build_internal_creative_agent_app();
let create_response = app
.clone()
.oneshot(internal_creative_agent_request(
"POST",
"/api/runtime/creative-agent/sessions",
serde_json::json!({
"text": "做一个生日拼图",
"entryContext": "creation_home"
}),
))
.await
.expect("create session request should succeed");
assert_eq!(create_response.status(), StatusCode::OK);
let create_payload = read_json_response(create_response).await;
let session_id = create_payload["session"]["sessionId"]
.as_str()
.expect("session id should exist");
let edit_response = app
.clone()
.oneshot(internal_creative_agent_request(
"POST",
&format!("/api/runtime/creative-agent/sessions/{session_id}/draft-edits/stream"),
serde_json::json!({
"clientMessageId": "creative-edit-test",
"instruction": "把标题改轻松一点",
"targetPuzzleSessionId": "puzzle-session-unconfirmed",
"currentDraft": {
"workTitle": "旧标题",
"workDescription": "旧描述",
"summary": "旧描述",
"themeTags": ["创意", "拼图", "灵感"],
"levels": [{
"levelId": "puzzle-level-1",
"levelName": "第一关",
"pictureDescription": "旧图面",
"pictureReference": null,
"generationStatus": "idle",
"candidates": []
}]
}
}),
))
.await
.expect("draft edit request should be handled");
assert_eq!(edit_response.status(), StatusCode::BAD_REQUEST);
let edit_payload = read_json_response(edit_response).await;
assert_eq!(
edit_payload["error"]["details"]["message"],
Value::String("尚未绑定拼图草稿".to_string())
);
let session_response = app
.oneshot(internal_creative_agent_request(
"GET",
&format!("/api/runtime/creative-agent/sessions/{session_id}"),
Value::Null,
))
.await
.expect("get session request should succeed");
let session_payload = read_json_response(session_response).await;
assert_eq!(session_payload["session"]["targetBinding"], Value::Null);
}
#[tokio::test]
async fn creative_agent_message_stream_returns_template_confirmation_events() {
let app = build_internal_creative_agent_app();
let create_response = app
.clone()
.oneshot(internal_creative_agent_request(
"POST",
"/api/runtime/creative-agent/sessions",
serde_json::json!({
"text": "做一个生日拼图",
"entryContext": "creation_home"
}),
))
.await
.expect("create session request should succeed");
assert_eq!(create_response.status(), StatusCode::OK);
let create_payload = read_json_response(create_response).await;
let session_id = create_payload["session"]["sessionId"]
.as_str()
.expect("session id should exist");
let stream_response = app
.clone()
.oneshot(internal_creative_agent_request(
"POST",
&format!("/api/runtime/creative-agent/sessions/{session_id}/messages/stream"),
serde_json::json!({
"clientMessageId": "creative-message-stream-test",
"content": [{
"type": "input_text",
"text": "做一个温暖的生日拼图"
}]
}),
))
.await
.expect("message stream request should be handled");
assert_eq!(stream_response.status(), StatusCode::OK);
assert_eq!(
stream_response
.headers()
.get("content-type")
.and_then(|value| value.to_str().ok()),
Some("text/event-stream")
);
let stream_body = read_text_response(stream_response).await;
assert!(stream_body.contains("event: stage"));
assert!(stream_body.contains("event: tool_started"));
assert!(stream_body.contains("event: tool_completed"));
assert!(stream_body.contains("event: puzzle_template_catalog"));
assert!(!stream_body.contains("event: puzzle_template_selection"));
assert!(!stream_body.contains("event: puzzle_cost_range"));
assert!(stream_body.contains("event: done"));
let tool_started_id = stream_body
.lines()
.skip_while(|line| *line != "event: tool_started")
.nth(1)
.and_then(|line| line.strip_prefix("data: "))
.and_then(|data| serde_json::from_str::<Value>(data).ok())
.and_then(|payload| payload["toolCallId"].as_str().map(ToString::to_string))
.expect("tool_started should include toolCallId");
let tool_completed_id = stream_body
.lines()
.skip_while(|line| *line != "event: tool_completed")
.nth(1)
.and_then(|line| line.strip_prefix("data: "))
.and_then(|data| serde_json::from_str::<Value>(data).ok())
.and_then(|payload| payload["toolCallId"].as_str().map(ToString::to_string))
.expect("tool_completed should include toolCallId");
assert_eq!(tool_started_id, tool_completed_id);
let session_response = app
.oneshot(internal_creative_agent_request(
"GET",
&format!("/api/runtime/creative-agent/sessions/{session_id}"),
Value::Null,
))
.await
.expect("get session request should succeed");
let session_payload = read_json_response(session_response).await;
assert_eq!(
session_payload["session"]["stage"],
Value::String("waiting_template_confirmation".to_string())
);
assert_eq!(
session_payload["session"]["puzzleTemplateSelection"],
Value::Null
);
assert!(
session_payload["session"]["puzzleTemplateCatalog"]
.as_array()
.map(|templates| templates.len() >= 3)
.unwrap_or(false)
);
}
#[tokio::test]
async fn creative_agent_confirm_template_rejects_non_puzzle_template() {
let app = build_internal_creative_agent_app();
let create_response = app
.clone()
.oneshot(internal_creative_agent_request(
"POST",
"/api/runtime/creative-agent/sessions",
serde_json::json!({
"text": "做一个角色扮演开场",
"entryContext": "creation_home"
}),
))
.await
.expect("create session request should succeed");
assert_eq!(create_response.status(), StatusCode::OK);
let create_payload = read_json_response(create_response).await;
let session_id = create_payload["session"]["sessionId"]
.as_str()
.expect("session id should exist");
let confirm_response = app
.clone()
.oneshot(internal_creative_agent_request(
"POST",
&format!("/api/runtime/creative-agent/sessions/{session_id}/confirm-template"),
serde_json::json!({
"selection": {
"templateId": "rpg.unsupported",
"title": "RPG",
"reason": "用户想创建 RPG",
"costRange": {
"minPoints": 2,
"maxPoints": 12,
"pricingUnit": "point",
"reason": "按关卡数和每关图片生成次数估算,实际扣费以后端任务结算为准"
},
"supportedLevelMode": "single_or_multi",
"selectedLevelMode": "single_level",
"plannedLevelCount": 1,
"requiresUserConfirmation": true
}
}),
))
.await
.expect("confirm template request should be handled");
assert_eq!(confirm_response.status(), StatusCode::BAD_REQUEST);
let confirm_payload = read_json_response(confirm_response).await;
assert_eq!(
confirm_payload["error"]["details"]["provider"],
Value::String("module-puzzle".to_string())
);
let session_response = app
.oneshot(internal_creative_agent_request(
"GET",
&format!("/api/runtime/creative-agent/sessions/{session_id}"),
Value::Null,
))
.await
.expect("get session request should succeed");
let session_payload = read_json_response(session_response).await;
assert_eq!(
session_payload["session"]["stage"],
Value::String("idle".to_string())
);
assert_eq!(session_payload["session"]["targetBinding"], Value::Null);
}
#[tokio::test]
async fn runtime_story_legacy_routes_are_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -3982,4 +4441,58 @@ mod tests {
assert_eq!(debug_response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn visual_novel_creation_route_requires_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/creation/visual-novel/sessions")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"sourceMode": "idea",
"seedText": "雨夜书店",
"sourceAssetIds": []
})
.to_string(),
))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn visual_novel_forbidden_playback_routes_are_not_mounted() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let legacy_playback_segment = concat!("re", "play");
for path in [
format!("/api/creation/visual-novel/{legacy_playback_segment}"),
format!("/api/runtime/visual-novel/{legacy_playback_segment}"),
format!("/api/runtime/visual-novel/{legacy_playback_segment}s"),
format!("/api/visual/{legacy_playback_segment}"),
format!("/api/galgame/{legacy_playback_segment}"),
format!("/api/txt/{legacy_playback_segment}"),
] {
let response = app
.clone()
.oneshot(
Request::builder()
.uri(path.as_str())
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{path}");
}
}
}

View File

@@ -191,6 +191,7 @@ fn allows_internal_forwarded_auth(path: &str) -> bool {
// Node 代理已经完成平台账号 JWT 校验Rust 运行时只信任这些明确的内部转发路径。
path.starts_with("/api/runtime/big-fish/")
|| path.starts_with("/api/runtime/chat/")
|| path.starts_with("/api/runtime/creative-agent/")
|| path.starts_with("/api/runtime/puzzle/")
}
@@ -287,6 +288,9 @@ mod tests {
assert!(allows_internal_forwarded_auth(
"/api/runtime/chat/npc/turn/stream"
));
assert!(allows_internal_forwarded_auth(
"/api/runtime/creative-agent/sessions"
));
assert!(allows_internal_forwarded_auth("/api/runtime/puzzle/works"));
assert!(!allows_internal_forwarded_auth("/api/auth/me"));
}

View File

@@ -1,3 +1,5 @@
use std::io::{Cursor, Read};
use axum::{Json, extract::Extension, http::StatusCode};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use serde_json::{Value, json};
@@ -12,7 +14,7 @@ use crate::{
const MAX_DOCUMENT_INPUT_BYTES: usize = 256 * 1024;
const MAX_DOCUMENT_INPUT_BASE64_CHARS: usize = 360 * 1024;
const SUPPORTED_DOCUMENT_EXTENSIONS: &[&str] = &["txt", "md", "markdown", "csv", "json"];
const SUPPORTED_DOCUMENT_EXTENSIONS: &[&str] = &["txt", "md", "markdown", "docx", "csv", "json"];
pub async fn parse_creation_agent_document_input(
Extension(request_context): Extension<RequestContext>,
@@ -58,12 +60,8 @@ pub async fn parse_creation_agent_document_input(
);
}
let text = String::from_utf8(decoded.clone()).map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"message": "暂时只支持 UTF-8 文本文档,请转换编码后再上传。",
"field": "contentBase64",
}))
})?;
let extension = document_extension(&file_name)?;
let text = decode_document_text(&decoded, extension.as_str())?;
let normalized_text = normalize_document_text(&text);
if normalized_text.trim().is_empty() {
@@ -88,6 +86,7 @@ pub async fn parse_creation_agent_document_input(
.map(str::to_string),
size_bytes: decoded.len(),
text: normalized_text,
source_asset_id: None,
},
},
))
@@ -115,11 +114,7 @@ fn normalize_file_name(value: &str) -> Result<String, AppError> {
}
fn ensure_supported_extension(file_name: &str) -> Result<(), AppError> {
let extension = file_name
.rsplit_once('.')
.map(|(_, extension)| extension.trim().to_ascii_lowercase())
.filter(|extension| !extension.is_empty())
.ok_or_else(|| unsupported_document_error(file_name))?;
let extension = document_extension(file_name)?;
if !SUPPORTED_DOCUMENT_EXTENSIONS.contains(&extension.as_str()) {
return Err(unsupported_document_error(file_name));
@@ -128,15 +123,100 @@ fn ensure_supported_extension(file_name: &str) -> Result<(), AppError> {
Ok(())
}
fn document_extension(file_name: &str) -> Result<String, AppError> {
file_name
.rsplit_once('.')
.map(|(_, extension)| extension.trim().to_ascii_lowercase())
.filter(|extension| !extension.is_empty())
.ok_or_else(|| unsupported_document_error(file_name))
}
fn unsupported_document_error(file_name: &str) -> AppError {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"message": "暂时只支持 txt、md、csv、json 文本文档。",
"message": "暂时只支持 txt、md、docx、csv、json 文档。",
"field": "fileName",
"fileName": file_name,
"supportedExtensions": SUPPORTED_DOCUMENT_EXTENSIONS,
}))
}
fn decode_document_text(bytes: &[u8], extension: &str) -> Result<String, AppError> {
if extension == "docx" {
return extract_docx_text(bytes);
}
String::from_utf8(bytes.to_vec()).map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"message": "暂时只支持 UTF-8 文本文档,请转换编码后再上传。",
"field": "contentBase64",
}))
})
}
fn extract_docx_text(bytes: &[u8]) -> Result<String, AppError> {
let reader = Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(reader).map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"message": "docx 文档结构无效,请重新选择文件。",
"field": "contentBase64",
}))
})?;
let mut document_xml = String::new();
archive
.by_name("word/document.xml")
.map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"message": "docx 文档缺少正文内容。",
"field": "contentBase64",
}))
})?
.read_to_string(&mut document_xml)
.map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"message": "docx 文档正文读取失败。",
"field": "contentBase64",
}))
})?;
Ok(extract_docx_visible_text(document_xml.as_str()))
}
fn extract_docx_visible_text(xml: &str) -> String {
let mut output = String::new();
let mut cursor = 0usize;
while let Some(start_offset) = xml[cursor..].find("<w:t") {
let start = cursor + start_offset;
let Some(tag_end_offset) = xml[start..].find('>') else {
break;
};
let text_start = start + tag_end_offset + 1;
let Some(end_offset) = xml[text_start..].find("</w:t>") else {
break;
};
let text_end = text_start + end_offset;
output.push_str(&decode_xml_text(&xml[text_start..text_end]));
cursor = text_end + "</w:t>".len();
if let Some(next_break) = xml[cursor..].find("<w:br") {
if next_break == 0 {
output.push('\n');
}
}
}
output
}
fn decode_xml_text(value: &str) -> String {
value
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&apos;", "'")
.replace("&amp;", "&")
}
fn normalize_document_text(value: &str) -> String {
value
.trim_start_matches('\u{feff}')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
use axum::response::sse::Event;
use serde::Serialize;
use serde_json::{Value, json};
use shared_contracts::creative_agent::{CreativeAgentErrorEvent, CreativeAgentSseEventType};
pub fn creative_sse_json_event<T>(event: CreativeAgentSseEventType, payload: T) -> Event
where
T: Serialize,
{
let event_name = creative_event_name(event);
match serde_json::to_value(payload)
.ok()
.and_then(|value| Event::default().event(event_name).json_data(value).ok())
{
Some(event) => event,
None => creative_sse_error_event(None, "SSE_SERIALIZE_FAILED", "SSE payload 序列化失败"),
}
}
pub fn creative_sse_json_value_event(event_name: &str, payload: Value) -> Event {
Event::default()
.event(event_name)
.json_data(payload)
.unwrap_or_else(|_| {
creative_sse_error_event(None, "SSE_SERIALIZE_FAILED", "SSE payload 序列化失败")
})
}
pub fn creative_sse_error_event(
session_id: Option<String>,
code: impl Into<String>,
message: impl Into<String>,
) -> Event {
let payload = serde_json::to_string(&CreativeAgentErrorEvent {
session_id,
code: code.into(),
message: message.into(),
recoverable: false,
})
.unwrap_or_else(|_| {
json!({
"sessionId": null,
"code": "SSE_ERROR_SERIALIZE_FAILED",
"message": "SSE 错误事件序列化失败",
"recoverable": false,
})
.to_string()
});
Event::default().event("error").data(payload)
}
fn creative_event_name(event: CreativeAgentSseEventType) -> &'static str {
match event {
CreativeAgentSseEventType::Stage => "stage",
CreativeAgentSseEventType::AgentMessageDelta => "agent_message_delta",
CreativeAgentSseEventType::ThoughtSummaryDelta => "thought_summary_delta",
CreativeAgentSseEventType::PuzzleTemplateCatalog => "puzzle_template_catalog",
CreativeAgentSseEventType::PuzzleTemplateSelection => "puzzle_template_selection",
CreativeAgentSseEventType::PuzzleCostRange => "puzzle_cost_range",
CreativeAgentSseEventType::PuzzleLevelPlan => "puzzle_level_plan",
CreativeAgentSseEventType::ToolStarted => "tool_started",
CreativeAgentSseEventType::ToolCompleted => "tool_completed",
CreativeAgentSseEventType::Reflection => "reflection",
CreativeAgentSseEventType::TargetSession => "target_session",
CreativeAgentSseEventType::Session => "session",
CreativeAgentSseEventType::Error => "error",
CreativeAgentSseEventType::Done => "done",
}
}

View File

@@ -89,6 +89,12 @@ impl IntoResponse for AppError {
}
}
impl From<AppError> for Response {
fn from(error: AppError) -> Self {
error.into_response()
}
}
fn resolve_http_error(status_code: StatusCode) -> (&'static str, &'static str) {
match status_code {
StatusCode::BAD_REQUEST => ("BAD_REQUEST", "请求参数不合法"),

View File

@@ -23,6 +23,8 @@ mod creation_agent_anchor_templates;
mod creation_agent_chat;
mod creation_agent_document_input;
mod creation_agent_llm_turn;
mod creative_agent;
mod creative_agent_sse;
mod custom_world;
mod custom_world_agent_entities;
mod custom_world_agent_turn;
@@ -66,11 +68,13 @@ mod square_hole_agent_turn;
mod state;
mod story_battles;
mod story_sessions;
mod visual_novel;
mod wechat_auth;
mod wechat_provider;
mod work_author;
use shared_logging::init_tracing;
use std::{collections::HashSet, env, fs, io, panic, thread};
use tokio::net::TcpListener;
use tokio::runtime::Builder as TokioRuntimeBuilder;
use tracing::info;
@@ -79,30 +83,30 @@ use crate::{app::build_router, config::AppConfig, state::AppState};
const API_SERVER_STARTUP_STACK_SIZE_BYTES: usize = 32 * 1024 * 1024;
fn main() -> Result<(), std::io::Error> {
fn main() -> Result<(), io::Error> {
// Windows 本地调试下 Axum 路由树和启动恢复链较重,显式放大启动线程栈,避免 debug 构建在进入监听前栈溢出。
std::thread::Builder::new()
let server_thread = thread::Builder::new()
.name("api-server-bootstrap".to_string())
.stack_size(API_SERVER_STARTUP_STACK_SIZE_BYTES)
.spawn(run_api_server_with_runtime)?
.join()
.map_err(|_| std::io::Error::other("api-server 启动线程异常退出"))?
.spawn(|| {
TokioRuntimeBuilder::new_multi_thread()
.enable_all()
.thread_name("api-server-worker")
.thread_stack_size(API_SERVER_STARTUP_STACK_SIZE_BYTES)
.build()?
.block_on(run_server())
})?;
match server_thread.join() {
Ok(result) => result,
Err(payload) => panic::resume_unwind(payload),
}
}
fn run_api_server_with_runtime() -> Result<(), std::io::Error> {
TokioRuntimeBuilder::new_multi_thread()
.enable_all()
.thread_name("api-server-worker")
.thread_stack_size(API_SERVER_STARTUP_STACK_SIZE_BYTES)
.build()?
.block_on(run_api_server())
}
async fn run_api_server() -> Result<(), std::io::Error> {
// 运行本地开发与联调时,优先从仓库根目录加载本地变量,避免手工逐项导出 OSS / APIMart 配置。
let _ = dotenvy::from_filename(".env");
let _ = dotenvy::from_filename(".env.local");
let _ = dotenvy::from_filename(".env.secrets.local");
async fn run_server() -> Result<(), io::Error> {
// 运行本地开发与联调时,优先从仓库根目录加载本地变量。
// 只尊重外层 shell 先注入的变量;.env.local 需要能覆盖 .env。
load_local_env_files();
// 统一先从配置对象读取监听地址,避免后续把环境变量读取散落到入口和路由层。
let config = AppConfig::from_env();
@@ -120,3 +124,92 @@ async fn run_api_server() -> Result<(), std::io::Error> {
axum::serve(listener, router).await
}
fn load_local_env_files() {
let shell_env_keys = env::vars().map(|(key, _)| key).collect::<HashSet<_>>();
for path in [".env", ".env.local", ".env.secrets.local"] {
load_env_file(path, &shell_env_keys);
}
}
fn load_env_file(path: &str, shell_env_keys: &HashSet<String>) {
let Ok(raw_text) = fs::read_to_string(path) else {
return;
};
let raw_text = raw_text.trim_start_matches('\u{feff}');
for raw_line in raw_text.split('\n') {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((raw_key, raw_value)) = line.split_once('=') else {
continue;
};
let key = raw_key.trim().trim_start_matches('\u{feff}');
if !is_valid_env_key(key) || shell_env_keys.contains(key) {
continue;
}
// 这里只在启动前、Tokio runtime 创建前写入进程环境,避免并发读写 env。
unsafe {
env::set_var(key, strip_env_value(raw_value));
}
}
}
fn strip_env_value(raw_value: &str) -> String {
let value = raw_value.trim_end_matches('\r');
if value.len() >= 2 {
let bytes = value.as_bytes();
let first = bytes[0];
let last = bytes[value.len() - 1];
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
return value[1..value.len() - 1].to_string();
}
}
value.to_string()
}
fn is_valid_env_key(key: &str) -> bool {
let mut chars = key.chars();
match chars.next() {
Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
#[cfg(test)]
mod tests {
use super::{is_valid_env_key, strip_env_value};
#[test]
fn strip_env_value_removes_wrapping_quotes() {
assert_eq!(strip_env_value("\"true\""), "true");
assert_eq!(strip_env_value("'aliyun'"), "aliyun");
assert_eq!(strip_env_value("plain\r"), "plain");
}
#[test]
fn load_env_key_can_strip_utf8_bom_prefix() {
let key = "\u{feff}SMS_AUTH_ENABLED"
.trim()
.trim_start_matches('\u{feff}');
assert_eq!(key, "SMS_AUTH_ENABLED");
}
#[test]
fn is_valid_env_key_accepts_dotenv_key_subset() {
assert!(is_valid_env_key("SMS_AUTH_ENABLED"));
assert!(is_valid_env_key("_LOCAL_KEY_1"));
assert!(!is_valid_env_key("1_BAD"));
assert!(!is_valid_env_key("BAD-KEY"));
}
}

View File

@@ -5,6 +5,7 @@ pub(crate) mod puzzle;
pub(crate) mod rpg;
pub(crate) mod scene_background;
pub(crate) mod square_hole;
pub(crate) mod visual_novel;
pub(crate) use rpg::agent_chat;
pub(crate) use rpg::foundation_draft;

View File

@@ -0,0 +1,690 @@
#![allow(dead_code)]
use platform_llm::{LlmMessage, LlmTextRequest};
use serde_json::{Value as JsonValue, json};
use shared_contracts::visual_novel::{VisualNovelResultDraft, VisualNovelRuntimeStep};
use crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL;
pub(crate) const VISUAL_NOVEL_CREATION_SYSTEM_PROMPT: &str = r#"你是百梦平台内的视觉小说模板创作导演。
你的任务是把用户的一句话、文档摘要或空白创建意图,生成一份可以进入结果页继续编辑的 VisualNovelResultDraft。
硬约束:
1. 只能输出一个 JSON 对象,不要输出 Markdown、代码块、解释或 UI 规则说明。
2. 输出内容必须是中文视觉小说底稿,补齐世界观、玩家身份、角色、场景、剧情阶段和开场。
3. 每个角色必须有可生成立绘的 appearance每个场景必须有可生成背景图的 description。
4. sourceMode 必须沿用输入的 idea、document 或 blank。
5. 图片、音乐、文档只能写平台资产引用或 null不能写大段 data URL。
6. 不要输出旧 TXT 播放记录、分享播放包、外部商业、运营、活动、展示横幅、交易或独立账号字段。
7. 不要发明第二套存档、发布、钱包、广场或资产系统。
8. publishReady 只有在 opening 场景、主要角色、剧情阶段和 2 到 4 个 initialChoices 都齐备时才可以为 true。
"#;
pub(crate) const VISUAL_NOVEL_RUNTIME_GM_SYSTEM_PROMPT: &str = r#"你是百梦视觉小说运行时 GM。
你的任务是读取作品底稿、当前 run snapshot、玩家动作和最近历史然后输出下一轮 VisualNovelRuntimeStep[]。
硬约束:
1. 只能输出一个 JSON 数组不要输出对象包裹、Markdown、代码块、解释或 UI 规则说明。
2. 每轮 step 数量不能超过输入的 maxAssistantStepCountPerTurn。
3. 场景变化必须先输出 scene_change。
4. 旁白使用 narration角色说话使用 dialogue转场使用 transition。
5. 需要玩家选择时必须输出 choicechoice 内每项必须有 choiceId 和 text。
6. 关键剧情事实变化使用 flag数值倾向变化使用 metric。
7. 不要让前端从 raw_text 猜业务 step不要输出未定义 step 类型。
8. 不要输出旧 TXT 播放记录、分享播放包、屏幕记录、外部商业、运营、活动或独立保存元数据。
"#;
pub(crate) const VISUAL_NOVEL_REPAIR_SYSTEM_PROMPT: &str = r#"你是视觉小说结构化输出修复器。
你的任务是把上一次模型输出修复为目标 JSON 契约。
硬约束:
1. 只能输出目标 JSON不要解释错误原因。
2. 不能新增目标契约之外的字段。
3. 不要把普通历史、运行事件或 raw_text 改写成旧 TXT 播放包、屏幕记录或分享片段。
4. 如果原文缺失必要信息,只补最小可运行占位值,并保持中文内容。
"#;
const VISUAL_NOVEL_CREATION_OUTPUT_CONTRACT: &str = r#"{
"profileId": null,
"workTitle": "",
"workDescription": "",
"workTags": [],
"coverImageSrc": null,
"sourceMode": "idea",
"sourceAssetIds": [],
"world": {
"title": "",
"summary": "",
"background": "",
"premise": "",
"literaryStyle": "",
"playerRole": "",
"defaultTone": ""
},
"characters": [
{
"characterId": "char-main-1",
"name": "",
"gender": null,
"role": "main",
"appearance": "",
"personality": "",
"tone": "",
"background": "",
"relationshipToPlayer": "",
"imageAssets": [],
"defaultExpression": null,
"isPlayerVisible": false
}
],
"scenes": [
{
"sceneId": "scene-opening",
"name": "",
"description": "",
"backgroundImageSrc": null,
"musicSrc": null,
"ambientSoundSrc": null,
"availability": "opening",
"phaseIds": []
}
],
"storyPhases": [
{
"phaseId": "phase-opening",
"title": "",
"goal": "",
"summary": "",
"entryCondition": "",
"exitCondition": "",
"sceneIds": ["scene-opening"],
"characterIds": ["char-main-1"],
"suggestedChoices": []
}
],
"opening": {
"sceneId": "scene-opening",
"narration": "",
"speakerCharacterId": null,
"firstDialogue": null,
"initialChoices": [
{ "choiceId": "choice-opening-1", "text": "", "actionHint": null },
{ "choiceId": "choice-opening-2", "text": "", "actionHint": null }
]
},
"runtimeConfig": {
"textModeEnabled": true,
"defaultTextMode": false,
"maxHistoryEntries": 80,
"maxAssistantStepCountPerTurn": 8,
"allowFreeTextAction": true,
"allowHistoryRegeneration": true,
"attributePanelMode": "off",
"saveArchiveEnabled": true
},
"publishReady": false,
"validationIssues": [],
"updatedAt": "ISO-8601"
}"#;
const VISUAL_NOVEL_RUNTIME_OUTPUT_CONTRACT: &str = r#"[
{ "type": "scene_change", "sceneId": "scene-opening", "backgroundImageSrc": null, "musicSrc": null },
{ "type": "narration", "text": "" },
{ "type": "dialogue", "characterId": "char-main-1", "characterName": "", "expression": null, "text": "" },
{ "type": "transition", "transitionKind": "fade", "text": null },
{ "type": "flag", "key": "", "value": true },
{ "type": "metric", "key": "", "delta": 1 },
{ "type": "choice", "choices": [{ "choiceId": "choice-next-1", "text": "", "actionHint": null }] }
]"#;
#[derive(Clone, Debug)]
pub(crate) struct VisualNovelCreationPromptParams<'a> {
pub(crate) source_mode: &'a str,
pub(crate) seed_text: Option<&'a str>,
pub(crate) source_asset_ids: &'a [String],
pub(crate) document_summary: Option<&'a str>,
pub(crate) current_draft: Option<&'a JsonValue>,
pub(crate) recent_messages: &'a [JsonValue],
pub(crate) now_iso: &'a str,
}
#[derive(Clone, Debug)]
pub(crate) struct VisualNovelRuntimePromptParams<'a> {
pub(crate) work_profile: &'a JsonValue,
pub(crate) run_snapshot: &'a JsonValue,
pub(crate) runtime_action: &'a JsonValue,
pub(crate) recent_history: &'a [JsonValue],
pub(crate) max_assistant_step_count_per_turn: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum VisualNovelRepairTarget {
ResultDraft,
RuntimeSteps,
}
impl VisualNovelRepairTarget {
fn label(self) -> &'static str {
match self {
Self::ResultDraft => "VisualNovelResultDraft",
Self::RuntimeSteps => "VisualNovelRuntimeStep[]",
}
}
fn contract(self) -> &'static str {
match self {
Self::ResultDraft => VISUAL_NOVEL_CREATION_OUTPUT_CONTRACT,
Self::RuntimeSteps => VISUAL_NOVEL_RUNTIME_OUTPUT_CONTRACT,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct VisualNovelRepairPromptParams<'a> {
pub(crate) target: VisualNovelRepairTarget,
pub(crate) raw_text: &'a str,
pub(crate) parse_error: &'a str,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct VisualNovelPromptParseFailure {
pub(crate) target: VisualNovelRepairTarget,
pub(crate) message: String,
}
impl VisualNovelPromptParseFailure {
pub(crate) fn retryable_message(&self) -> String {
format!(
"{} 输出结构不可解析,可重试或进入 repair{}",
self.target.label(),
self.message
)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct VisualNovelToolDescriptor {
pub(crate) name: &'static str,
pub(crate) description: &'static str,
pub(crate) input_schema: JsonValue,
}
pub(crate) fn build_visual_novel_creation_user_prompt(
params: VisualNovelCreationPromptParams<'_>,
) -> String {
json!({
"task": "generate_visual_novel_result_draft",
"sourceMode": params.source_mode,
"seedText": params.seed_text.unwrap_or("").trim(),
"sourceAssetIds": params.source_asset_ids,
"documentSummary": params.document_summary.unwrap_or("").trim(),
"currentDraft": params.current_draft,
"recentMessages": params.recent_messages,
"nowIso": params.now_iso,
"draftRequirements": {
"mainCharacters": "3 到 6 个,至少 1 个非玩家主要角色",
"scenes": "3 到 8 个,至少 1 个 opening 场景",
"storyPhases": "3 到 6 个,第一阶段可从 opening 进入",
"initialChoices": "2 到 4 个",
"runtimeConfigDefaults": "沿用契约默认值attributePanelMode 默认为 off"
},
"outputContract": VISUAL_NOVEL_CREATION_OUTPUT_CONTRACT
})
.to_string()
}
pub(crate) fn build_visual_novel_runtime_user_prompt(
params: VisualNovelRuntimePromptParams<'_>,
) -> String {
json!({
"task": "generate_visual_novel_runtime_steps",
"workProfile": params.work_profile,
"runSnapshot": params.run_snapshot,
"runtimeAction": params.runtime_action,
"recentHistory": params.recent_history,
"maxAssistantStepCountPerTurn": params.max_assistant_step_count_per_turn,
"runtimeRules": [
"只以 step 数组作为正式业务输出",
"当前选择项必须来自 runSnapshot.availableChoices 或由本轮 choice step 重新给出",
"如果玩家自由输入改变事实,必须用 flag 或 metric 表达可持久化变化",
"不要在输出中夹带 raw_text、debug、prompt、historyPlayback 或平台运营字段"
],
"outputContract": VISUAL_NOVEL_RUNTIME_OUTPUT_CONTRACT
})
.to_string()
}
pub(crate) fn build_visual_novel_repair_user_prompt(
params: VisualNovelRepairPromptParams<'_>,
) -> String {
json!({
"task": "repair_visual_novel_structured_output",
"target": params.target.label(),
"parseError": params.parse_error,
"rawText": params.raw_text,
"outputContract": params.target.contract()
})
.to_string()
}
pub(crate) fn build_visual_novel_creation_llm_request(
params: VisualNovelCreationPromptParams<'_>,
enable_web_search: bool,
) -> LlmTextRequest {
LlmTextRequest::new(vec![
LlmMessage::system(VISUAL_NOVEL_CREATION_SYSTEM_PROMPT),
LlmMessage::user(build_visual_novel_creation_user_prompt(params)),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
.with_web_search(enable_web_search)
}
pub(crate) fn build_visual_novel_runtime_llm_request(
params: VisualNovelRuntimePromptParams<'_>,
) -> LlmTextRequest {
LlmTextRequest::new(vec![
LlmMessage::system(VISUAL_NOVEL_RUNTIME_GM_SYSTEM_PROMPT),
LlmMessage::user(build_visual_novel_runtime_user_prompt(params)),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
}
pub(crate) fn build_visual_novel_repair_llm_request(
params: VisualNovelRepairPromptParams<'_>,
) -> LlmTextRequest {
LlmTextRequest::new(vec![
LlmMessage::system(VISUAL_NOVEL_REPAIR_SYSTEM_PROMPT),
LlmMessage::user(build_visual_novel_repair_user_prompt(params)),
])
.with_model(CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api()
}
pub(crate) fn visual_novel_tool_descriptors() -> Vec<VisualNovelToolDescriptor> {
vec![
VisualNovelToolDescriptor {
name: "visual_novel_apply_creation_action",
description: "执行视觉小说创作 action写回 VisualNovelResultDraft 或编译平台 work profile 草稿。",
input_schema: json!({
"type": "object",
"required": ["kind"],
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"enum": [
"generate_draft",
"patch_world",
"patch_character",
"patch_scene",
"patch_story_phase",
"compile_work_profile"
]
},
"targetId": { "type": ["string", "null"] },
"payload": { "type": "object", "additionalProperties": true }
}
}),
},
VisualNovelToolDescriptor {
name: "visual_novel_generate_image_asset",
description: "为视觉小说角色立绘或场景背景生成图片,并返回平台资产引用。",
input_schema: json!({
"type": "object",
"required": ["kind", "targetId", "prompt"],
"additionalProperties": false,
"properties": {
"kind": {
"type": "string",
"enum": ["generate_scene_image", "generate_character_image"]
},
"targetId": { "type": "string", "minLength": 1 },
"prompt": { "type": "string", "minLength": 1 },
"styleHints": { "type": "array", "items": { "type": "string" } },
"sourceImageAssetId": { "type": ["string", "null"] }
}
}),
},
]
}
pub(crate) fn parse_visual_novel_result_draft_fixture(
text: &str,
) -> Result<VisualNovelResultDraft, VisualNovelPromptParseFailure> {
let value = extract_json_root(
text,
JsonRootShape::Object,
VisualNovelRepairTarget::ResultDraft,
)?;
serde_json::from_value(value).map_err(|error| VisualNovelPromptParseFailure {
target: VisualNovelRepairTarget::ResultDraft,
message: error.to_string(),
})
}
pub(crate) fn parse_visual_novel_runtime_steps_fixture(
text: &str,
) -> Result<Vec<VisualNovelRuntimeStep>, VisualNovelPromptParseFailure> {
let value = extract_json_root(
text,
JsonRootShape::Array,
VisualNovelRepairTarget::RuntimeSteps,
)?;
serde_json::from_value(value).map_err(|error| VisualNovelPromptParseFailure {
target: VisualNovelRepairTarget::RuntimeSteps,
message: error.to_string(),
})
}
#[derive(Clone, Copy)]
enum JsonRootShape {
Object,
Array,
}
fn extract_json_root(
text: &str,
shape: JsonRootShape,
target: VisualNovelRepairTarget,
) -> Result<JsonValue, VisualNovelPromptParseFailure> {
let trimmed = strip_json_code_fence(text.trim());
if let Ok(value) = serde_json::from_str::<JsonValue>(trimmed) {
return Ok(value);
}
let (start_char, end_char) = match shape {
JsonRootShape::Object => ('{', '}'),
JsonRootShape::Array => ('[', ']'),
};
let start = trimmed.find(start_char);
let end = trimmed.rfind(end_char);
match (start, end) {
(Some(start), Some(end)) if end > start => {
serde_json::from_str::<JsonValue>(&trimmed[start..=end]).map_err(|error| {
VisualNovelPromptParseFailure {
target,
message: error.to_string(),
}
})
}
_ => Err(VisualNovelPromptParseFailure {
target,
message: format!("未找到目标 JSON {}", target.label()),
}),
}
}
fn strip_json_code_fence(text: &str) -> &str {
let trimmed = text.trim();
if !trimmed.starts_with("```") {
return trimmed;
}
let without_start = trimmed
.strip_prefix("```json")
.or_else(|| trimmed.strip_prefix("```JSON"))
.or_else(|| trimmed.strip_prefix("```"))
.unwrap_or(trimmed)
.trim();
without_start
.strip_suffix("```")
.unwrap_or(without_start)
.trim()
}
#[cfg(test)]
mod tests {
use platform_llm::LlmTextProtocol;
use serde_json::json;
use super::*;
fn source_asset_ids() -> Vec<String> {
vec!["asset-doc-1".to_string()]
}
fn creation_params<'a>(source_asset_ids: &'a [String]) -> VisualNovelCreationPromptParams<'a> {
VisualNovelCreationPromptParams {
source_mode: "idea",
seed_text: Some("雨夜里,只在午夜出现的书店会归还人们遗失的名字。"),
source_asset_ids,
document_summary: None,
current_draft: None,
recent_messages: &[],
now_iso: "2026-05-05T12:00:00Z",
}
}
fn runtime_params<'a>(
work_profile: &'a JsonValue,
run_snapshot: &'a JsonValue,
runtime_action: &'a JsonValue,
) -> VisualNovelRuntimePromptParams<'a> {
VisualNovelRuntimePromptParams {
work_profile,
run_snapshot,
runtime_action,
recent_history: &[],
max_assistant_step_count_per_turn: 8,
}
}
fn sample_draft() -> JsonValue {
json!({
"profileId": null,
"workTitle": "雨夜书店",
"workDescription": "一名失去名字的读者在午夜书店寻找真相。",
"workTags": ["悬疑", "治愈"],
"coverImageSrc": null,
"sourceMode": "idea",
"sourceAssetIds": [],
"world": {
"title": "雨夜书店",
"summary": "午夜书店会收留遗失名字的人。",
"background": "旧城区尽头有一家只在雨夜开门的书店,书架保存着人们遗忘的片段。",
"premise": "玩家要在天亮前找回自己的名字。",
"literaryStyle": "细腻、克制、轻悬疑",
"playerRole": "失去名字的读者",
"defaultTone": "雨夜、温柔、隐秘"
},
"characters": [
{
"characterId": "char-keeper",
"name": "林栖",
"gender": "",
"role": "main",
"appearance": "银灰短发,深绿围裙,手中常拿一盏铜灯,适合半身立绘。",
"personality": "温和但不轻易透露真相",
"tone": "低声、像在翻旧书",
"background": "午夜书店的看守者。",
"relationshipToPlayer": "知道玩家名字的一部分。",
"imageAssets": [],
"defaultExpression": "calm",
"isPlayerVisible": false
}
],
"scenes": [
{
"sceneId": "scene-bookstore",
"name": "午夜书店",
"description": "窄巷尽头的木门半开,暖黄灯光落在潮湿石板上,室内书架高而幽深。",
"backgroundImageSrc": null,
"musicSrc": null,
"ambientSoundSrc": null,
"availability": "opening",
"phaseIds": ["phase-opening"]
}
],
"storyPhases": [
{
"phaseId": "phase-opening",
"title": "失名之夜",
"goal": "确认玩家为何失去名字",
"summary": "玩家进入书店,与林栖第一次交谈。",
"entryCondition": "opening",
"exitCondition": "找到第一张名字书签",
"sceneIds": ["scene-bookstore"],
"characterIds": ["char-keeper"],
"suggestedChoices": ["询问书店来历", "查看柜台上的旧书"]
}
],
"opening": {
"sceneId": "scene-bookstore",
"narration": "雨水顺着伞尖落下时,你发现门牌上的字正在一点点亮起。",
"speakerCharacterId": "char-keeper",
"firstDialogue": "你终于来了。名字丢失的人,总会先听见这场雨。",
"initialChoices": [
{ "choiceId": "choice-ask-name", "text": "询问自己的名字在哪里", "actionHint": "向林栖确认线索" },
{ "choiceId": "choice-look-book", "text": "查看柜台上的旧书", "actionHint": "寻找名字书签" }
]
},
"runtimeConfig": {
"textModeEnabled": true,
"defaultTextMode": false,
"maxHistoryEntries": 80,
"maxAssistantStepCountPerTurn": 8,
"allowFreeTextAction": true,
"allowHistoryRegeneration": true,
"attributePanelMode": "off",
"saveArchiveEnabled": true
},
"publishReady": true,
"validationIssues": [],
"updatedAt": "2026-05-05T12:00:00Z"
})
}
#[test]
fn creation_fixture_parses_as_visual_novel_result_draft() {
let raw_text = format!("模型输出如下:\n{}", sample_draft());
let draft = parse_visual_novel_result_draft_fixture(raw_text.as_str())
.expect("draft fixture should parse");
assert_eq!(draft.work_title, "雨夜书店");
assert_eq!(draft.characters[0].character_id, "char-keeper");
assert_eq!(draft.opening.initial_choices.len(), 2);
}
#[test]
fn runtime_fixture_parses_as_typed_steps() {
let raw_text = json!([
{ "type": "scene_change", "sceneId": "scene-bookstore", "backgroundImageSrc": null, "musicSrc": null },
{ "type": "narration", "text": "门铃轻响,雨声像被书页吸走。" },
{ "type": "dialogue", "characterId": "char-keeper", "characterName": "林栖", "expression": "calm", "text": "先别急着找答案,先告诉我你还记得什么。" },
{ "type": "flag", "key": "met_keeper", "value": true },
{ "type": "metric", "key": "keeper_trust", "delta": 1 },
{
"type": "choice",
"choices": [
{ "choiceId": "choice-tell-memory", "text": "说出最后记得的街名", "actionHint": "提供线索" },
{ "choiceId": "choice-stay-silent", "text": "保持沉默观察她", "actionHint": "观察林栖反应" }
]
}
])
.to_string();
let steps = parse_visual_novel_runtime_steps_fixture(raw_text.as_str())
.expect("runtime fixture should parse");
assert_eq!(steps.len(), 6);
assert!(matches!(
steps[0],
VisualNovelRuntimeStep::SceneChange { .. }
));
assert!(matches!(steps[5], VisualNovelRuntimeStep::Choice { .. }));
}
#[test]
fn bad_runtime_output_can_enter_repair_prompt() {
let failure = parse_visual_novel_runtime_steps_fixture("林栖说:欢迎来到书店。")
.expect_err("bad output should fail");
let retryable_message = failure.retryable_message();
let repair_prompt = build_visual_novel_repair_user_prompt(VisualNovelRepairPromptParams {
target: failure.target,
raw_text: "林栖说:欢迎来到书店。",
parse_error: failure.message.as_str(),
});
assert!(retryable_message.contains("可重试"));
assert!(repair_prompt.contains("VisualNovelRuntimeStep[]"));
assert!(repair_prompt.contains("林栖说"));
assert!(repair_prompt.contains("scene_change"));
}
#[test]
fn llm_requests_use_responses_template_model() {
let asset_ids = source_asset_ids();
let creation_request =
build_visual_novel_creation_llm_request(creation_params(asset_ids.as_slice()), true);
assert_eq!(
creation_request.model.as_deref(),
Some(CREATION_TEMPLATE_LLM_MODEL)
);
assert_eq!(creation_request.protocol, LlmTextProtocol::Responses);
assert!(creation_request.enable_web_search);
assert!(
creation_request.messages[0]
.content
.contains("VisualNovelResultDraft")
);
assert!(
creation_request.messages[1]
.content
.contains("sourceAssetIds")
);
let work_profile = sample_draft();
let run_snapshot = json!({ "runId": "run-1", "availableChoices": [] });
let runtime_action = json!({ "actionKind": "continue", "clientEventId": "event-1" });
let runtime_request = build_visual_novel_runtime_llm_request(runtime_params(
&work_profile,
&run_snapshot,
&runtime_action,
));
assert_eq!(
runtime_request.model.as_deref(),
Some(CREATION_TEMPLATE_LLM_MODEL)
);
assert_eq!(runtime_request.protocol, LlmTextProtocol::Responses);
assert!(!runtime_request.enable_web_search);
assert!(
runtime_request.messages[0]
.content
.contains("VisualNovelRuntimeStep[]")
);
}
#[test]
fn prompts_and_tools_guard_against_external_platform_fields() {
assert!(VISUAL_NOVEL_CREATION_SYSTEM_PROMPT.contains("外部商业"));
assert!(VISUAL_NOVEL_CREATION_SYSTEM_PROMPT.contains("独立账号"));
assert!(VISUAL_NOVEL_RUNTIME_GM_SYSTEM_PROMPT.contains("独立保存"));
let tools = visual_novel_tool_descriptors();
let tool_payload = serde_json::to_string(&json!(
tools
.iter()
.map(|tool| json!({
"name": tool.name,
"description": tool.description,
"inputSchema": tool.input_schema,
}))
.collect::<Vec<_>>()
))
.expect("tools should serialize");
assert!(tool_payload.contains("generate_scene_image"));
assert!(tool_payload.contains("generate_character_image"));
assert!(tool_payload.contains("compile_work_profile"));
let legacy_playback_marker = format!("{}{}", "re", "play");
assert!(!tool_payload.contains(&legacy_playback_marker));
assert!(!tool_payload.contains(&legacy_playback_marker.to_uppercase()));
}
}

View File

@@ -219,6 +219,7 @@ pub async fn generate_puzzle_onboarding_work(
level_id: "onboarding-level-1".to_string(),
level_name: level_name.clone(),
picture_description: prompt_text.clone(),
picture_reference: None,
candidates,
selected_candidate_id: Some(selected.candidate_id.clone()),
cover_image_src: Some(selected.image_src.clone()),
@@ -706,6 +707,7 @@ pub async fn execute_puzzle_agent_action(
);
let (operation_type, phase_label, phase_detail, session) = match action.as_str() {
"compile_puzzle_draft" => {
let ai_redraw = payload.ai_redraw.unwrap_or(true);
let prompt_text = payload
.picture_description
.as_deref()
@@ -725,33 +727,49 @@ pub async fn execute_puzzle_agent_action(
Ok(next_session_id) => next_session_id,
Err(response) => return Err(response),
};
let session = execute_billable_asset_operation_with_cost(
&state,
&owner_user_id,
"puzzle_initial_image",
&billing_asset_id,
PUZZLE_IMAGE_GENERATION_POINTS_COST,
async {
compile_puzzle_draft_with_initial_cover(
&state,
compile_session_id.clone(),
owner_user_id.clone(),
prompt_text,
payload.reference_image_src.as_deref(),
payload.image_model.as_deref(),
now,
)
.await
},
)
.await
let session = if ai_redraw {
execute_billable_asset_operation_with_cost(
&state,
&owner_user_id,
"puzzle_initial_image",
&billing_asset_id,
PUZZLE_IMAGE_GENERATION_POINTS_COST,
async {
compile_puzzle_draft_with_initial_cover(
&state,
compile_session_id.clone(),
owner_user_id.clone(),
prompt_text,
payload.reference_image_src.as_deref(),
payload.image_model.as_deref(),
now,
)
.await
},
)
.await
} else {
compile_puzzle_draft_with_uploaded_cover(
&state,
compile_session_id.clone(),
owner_user_id.clone(),
prompt_text,
payload.reference_image_src.as_deref(),
now,
)
.await
}
.map_err(|error| {
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
});
(
"compile_puzzle_draft",
"首关拼图草稿",
"已编译首关草稿、生成首关画面并写入正式草稿。",
if ai_redraw {
"已编译首关草稿、生成首关画面并写入正式草稿。"
} else {
"已编译首关草稿,并直接应用上传图片为第一关图片。"
},
session,
)
}
@@ -1980,6 +1998,7 @@ fn map_puzzle_draft_level_response(level: PuzzleDraftLevelRecord) -> PuzzleDraft
level_id: level.level_id,
level_name: level.level_name,
picture_description: level.picture_description,
picture_reference: level.picture_reference,
candidates: level
.candidates
.into_iter()
@@ -2519,6 +2538,7 @@ fn parse_puzzle_level_records_from_module_json(
level_id: level.level_id,
level_name: level.level_name,
picture_description: level.picture_description,
picture_reference: level.picture_reference,
candidates: level
.candidates
.into_iter()
@@ -2685,6 +2705,7 @@ fn serialize_puzzle_levels_response(
"level_id": level.level_id,
"level_name": level.level_name,
"picture_description": level.picture_description,
"picture_reference": level.picture_reference,
"candidates": level
.candidates
.iter()
@@ -3076,6 +3097,163 @@ async fn compile_puzzle_draft_with_initial_cover(
}
}
async fn compile_puzzle_draft_with_uploaded_cover(
state: &AppState,
session_id: String,
owner_user_id: String,
prompt_text: Option<&str>,
reference_image_src: Option<&str>,
now: i64,
) -> Result<PuzzleAgentSessionRecord, AppError> {
let uploaded_image_src = reference_image_src
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
"field": "referenceImageSrc",
"message": "关闭 AI 重绘时必须上传拼图图片。",
}))
})?;
let uploaded_image = parse_puzzle_image_data_url(uploaded_image_src).ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
"field": "referenceImageSrc",
"message": "关闭 AI 重绘时上传图必须是图片 Data URL。",
}))
})?;
let compiled_session = state
.spacetime_client()
.compile_puzzle_agent_draft(session_id.clone(), owner_user_id.clone(), now)
.await
.map_err(map_puzzle_compile_error)?;
let draft = compiled_session.draft.clone().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
"message": "拼图结果页草稿尚未生成",
}))
})?;
let mut target_level = select_puzzle_level_for_api(&draft, None)?;
let fallback_level_name = target_level.level_name.clone();
let generated_level_name =
generate_puzzle_first_level_name(state, &target_level.picture_description).await;
target_level.level_name = generated_level_name.clone();
let levels_json_with_generated_name = Some(serialize_puzzle_level_records_for_module(
&build_puzzle_levels_with_primary_name(&draft, &target_level),
)?);
let image_prompt = resolve_puzzle_draft_cover_prompt(
prompt_text,
&target_level.picture_description,
&draft.summary,
);
// 中文注释:关闭 AI 重绘时不请求 APIMart也不进入光点扣费流程上传图直接成为首关正式图候选。
let candidate_id = format!(
"{}-candidate-{}",
compiled_session.session_id,
target_level.candidates.len() + 1
);
let persisted_upload = persist_puzzle_generated_asset(
state,
owner_user_id.as_str(),
&compiled_session.session_id,
&target_level.level_name,
candidate_id.as_str(),
"uploaded-direct",
PuzzleDownloadedImage {
extension: puzzle_mime_to_extension(uploaded_image.mime_type.as_str()).to_string(),
mime_type: normalize_puzzle_downloaded_image_mime_type(
uploaded_image.mime_type.as_str(),
),
bytes: uploaded_image.bytes,
},
current_utc_micros(),
)
.await?;
let candidate = PuzzleGeneratedImageCandidateRecord {
candidate_id: candidate_id.clone(),
image_src: persisted_upload.image_src,
asset_id: persisted_upload.asset_id,
prompt: image_prompt,
actual_prompt: None,
source_type: "uploaded".to_string(),
selected: true,
};
let candidates_json = serde_json::to_string(&vec![to_puzzle_generated_image_candidate(
&candidate,
)])
.map_err(|error| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
"message": format!("拼图上传图候选序列化失败:{error}"),
}))
})?;
let (saved_session, save_used_fallback) = state
.spacetime_client()
.save_puzzle_generated_images(PuzzleGeneratedImagesSaveRecordInput {
session_id: compiled_session.session_id.clone(),
owner_user_id: owner_user_id.clone(),
level_id: Some(target_level.level_id.clone()),
levels_json: levels_json_with_generated_name,
candidates_json,
saved_at_micros: current_utc_micros(),
})
.await
.map_err(map_puzzle_client_error)
.map(|session| (session, false))
.or_else(|error| {
if is_spacetimedb_connectivity_app_error(&error) {
tracing::warn!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %compiled_session.session_id,
owner_user_id = %owner_user_id,
message = %error.body_text(),
"拼图上传图草稿回写不可用,降级返回本地快照"
);
let session = apply_generated_puzzle_candidates_to_session_snapshot(
apply_generated_puzzle_first_level_name_to_session_snapshot(
compiled_session.clone(),
target_level.level_id.as_str(),
generated_level_name.as_str(),
fallback_level_name.as_str(),
now,
),
target_level.level_id.as_str(),
vec![candidate.clone()],
now,
);
Ok((session, true))
} else {
Err(error)
}
})?;
if save_used_fallback {
return Ok(saved_session);
}
match state
.spacetime_client()
.select_puzzle_cover_image(PuzzleSelectCoverImageRecordInput {
session_id,
owner_user_id,
level_id: Some(target_level.level_id),
candidate_id,
selected_at_micros: current_utc_micros(),
})
.await
{
Ok(session) => Ok(session),
Err(error) if should_skip_asset_operation_billing_for_connectivity(&error) => {
tracing::warn!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %saved_session.session_id,
error = %error,
"拼图上传图选定回写因 SpacetimeDB 连接不可用而降级使用已保存快照"
);
Ok(saved_session)
}
Err(error) => Err(map_puzzle_client_error(error)),
}
}
fn apply_generated_puzzle_candidates_to_session_snapshot(
mut session: PuzzleAgentSessionRecord,
target_level_id: &str,

View File

@@ -1,7 +1,9 @@
use std::{error::Error, fmt, sync::Arc};
#[cfg(test)]
use std::{collections::HashMap, sync::Mutex};
use std::{
collections::HashMap,
error::Error,
fmt,
sync::{Arc, Mutex},
};
use module_ai::{AiTaskService, InMemoryAiTaskStore};
use module_auth::{
@@ -11,14 +13,16 @@ use module_auth::{
use module_runtime::RuntimeSnapshotRecord;
#[cfg(test)]
use module_runtime::{SAVE_SNAPSHOT_VERSION, format_utc_micros};
use platform_agent::MockLangChainRustAgentExecutor;
use platform_auth::{
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus, JwtConfig, JwtError,
RefreshCookieConfig, RefreshCookieError, RefreshCookieSameSite, SmsAuthConfig, SmsAuthProvider,
SmsAuthProviderKind, SmsProviderError, WechatProvider, sign_access_token, verify_access_token,
};
use platform_llm::{LlmClient, LlmConfig, LlmError};
use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider};
use platform_oss::{OssClient, OssConfig, OssError};
use serde_json::Value;
use shared_contracts::creative_agent::CreativeAgentSessionSnapshot;
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError};
use time::OffsetDateTime;
use tracing::{info, warn};
@@ -51,11 +55,22 @@ pub struct AppState {
ai_task_service: AiTaskService,
spacetime_client: SpacetimeClient,
llm_client: Option<LlmClient>,
creative_agent_gpt5_client: Option<LlmClient>,
creative_agent_executor: Arc<MockLangChainRustAgentExecutor>,
// Phase 1 任务 E 的 creative session facade 暂存在 api-server。
// creative_agent_* 表由任务 D 收口后,这里只保留读写 facade。
creative_agent_sessions: Arc<Mutex<HashMap<String, CreativeAgentSessionRuntimeRecord>>>,
#[cfg(test)]
// 测试环境允许在未启动 SpacetimeDB 时,用内存快照兜底当前 runtime story 回归链。
test_runtime_snapshot_store: Arc<Mutex<HashMap<String, RuntimeSnapshotRecord>>>,
}
#[derive(Clone, Debug)]
struct CreativeAgentSessionRuntimeRecord {
owner_user_id: String,
snapshot: CreativeAgentSessionSnapshot,
}
// 后台管理员运行态独立于普通玩家登录体系,只从环境变量构造。
#[derive(Clone, Debug)]
pub struct AdminRuntime {
@@ -167,6 +182,7 @@ impl AppState {
procedure_timeout: config.spacetime_procedure_timeout,
});
let llm_client = build_llm_client(&config)?;
let creative_agent_gpt5_client = build_creative_agent_gpt5_client(&config)?;
Ok(Self {
config,
@@ -185,6 +201,9 @@ impl AppState {
ai_task_service,
spacetime_client,
llm_client,
creative_agent_gpt5_client,
creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor),
creative_agent_sessions: Arc::new(Mutex::new(HashMap::new())),
#[cfg(test)]
test_runtime_snapshot_store: Arc::new(Mutex::new(HashMap::new())),
})
@@ -336,6 +355,44 @@ impl AppState {
self.llm_client.as_ref()
}
pub fn creative_agent_gpt5_client(&self) -> Option<&LlmClient> {
self.creative_agent_gpt5_client.as_ref()
}
pub fn creative_agent_executor(&self) -> Arc<MockLangChainRustAgentExecutor> {
self.creative_agent_executor.clone()
}
pub fn get_creative_agent_session(
&self,
session_id: &str,
owner_user_id: &str,
) -> Option<CreativeAgentSessionSnapshot> {
self.creative_agent_sessions
.lock()
.expect("creative agent session store should lock")
.get(session_id)
.filter(|record| record.owner_user_id == owner_user_id)
.map(|record| record.snapshot.clone())
}
pub fn put_creative_agent_session(
&self,
owner_user_id: String,
session: CreativeAgentSessionSnapshot,
) {
self.creative_agent_sessions
.lock()
.expect("creative agent session store should lock")
.insert(
session.session_id.clone(),
CreativeAgentSessionRuntimeRecord {
owner_user_id,
snapshot: session,
},
);
}
pub async fn get_runtime_snapshot_record(
&self,
user_id: String,
@@ -710,6 +767,31 @@ fn build_llm_client(config: &AppConfig) -> Result<Option<LlmClient>, AppStateIni
Ok(Some(LlmClient::new(llm_config)?))
}
fn build_creative_agent_gpt5_client(
config: &AppConfig,
) -> Result<Option<LlmClient>, AppStateInitError> {
let Some(api_key) = config
.apimart_api_key
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
else {
return Ok(None);
};
let llm_config = LlmConfig::new(
LlmProvider::OpenAiCompatible,
config.apimart_base_url.clone(),
api_key.to_string(),
platform_agent::CREATIVE_AGENT_GPT5_MODEL.to_string(),
config.apimart_image_request_timeout_ms,
0,
config.llm_retry_backoff_ms,
)?;
Ok(Some(LlmClient::new(llm_config)?))
}
// 只有在用户名和密码都已配置时才启用后台,避免半配置状态暴露伪入口。
fn build_admin_runtime(
config: &AppConfig,
@@ -783,5 +865,28 @@ mod tests {
let state = AppState::new(AppConfig::default()).expect("state should build");
assert!(state.llm_client().is_none());
assert!(state.creative_agent_gpt5_client().is_none());
}
#[test]
fn app_state_builds_creative_agent_gpt5_client_from_apimart_settings() {
let mut config = AppConfig::default();
config.llm_api_key = None;
config.apimart_base_url = "https://api.apimart.test/v1".to_string();
config.apimart_api_key = Some("apimart-key".to_string());
let state = AppState::new(config).expect("state should build");
let client = state
.creative_agent_gpt5_client()
.expect("creative agent gpt5 client should exist");
assert_eq!(
client.config().model(),
platform_agent::CREATIVE_AGENT_GPT5_MODEL
);
assert_eq!(
client.config().responses_url(),
"https://api.apimart.test/v1/responses"
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
[package]
name = "module-creative-agent"
edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = []
spacetime-types = ["dep:spacetimedb"]
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
shared-kernel = { workspace = true }
spacetimedb = { workspace = true, optional = true }

View File

@@ -0,0 +1,225 @@
use shared_kernel::{normalize_optional_string, normalize_required_string};
use crate::{
CreativeAgentError, CreativeAgentMessageAppendInput, CreativeAgentMessageKind,
CreativeAgentMessageRole, CreativeAgentStage, CreativeAgentStageUpdateInput,
CreativeAgentTargetBindInput, CreativeAgentTemplateConfirmInput, CreativeTargetPlayType,
};
pub fn validate_create_session(
session_id: &str,
owner_user_id: &str,
) -> Result<(String, String), CreativeAgentError> {
let session_id =
normalize_required_string(session_id).ok_or(CreativeAgentError::MissingSessionId)?;
let owner_user_id =
normalize_required_string(owner_user_id).ok_or(CreativeAgentError::MissingOwnerUserId)?;
Ok((session_id, owner_user_id))
}
pub fn validate_append_message(
input: &CreativeAgentMessageAppendInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
if normalize_required_string(&input.message_id).is_none() {
return Err(CreativeAgentError::MissingMessageId);
}
if normalize_required_string(&input.text).is_none() {
return Err(CreativeAgentError::MissingMessageText);
}
Ok(())
}
pub fn validate_stage_update(
current: CreativeAgentStage,
input: &CreativeAgentStageUpdateInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
validate_stage_transition(current, input.stage)
}
pub fn validate_template_confirmation(
current: CreativeAgentStage,
input: &CreativeAgentTemplateConfirmInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
if normalize_required_string(&input.template_selection_json).is_none() {
return Err(CreativeAgentError::MissingTemplateSelection);
}
if !input.template_selection_json.contains("\"costRange\"")
&& !input.template_selection_json.contains("\"cost_range\"")
{
return Err(CreativeAgentError::MissingCostRange);
}
validate_stage_transition(current, CreativeAgentStage::PlanningPuzzleLevels)
}
pub fn validate_target_binding(
current_stage: CreativeAgentStage,
template_selection_json: Option<&str>,
input: &CreativeAgentTargetBindInput,
) -> Result<(), CreativeAgentError> {
validate_create_session(&input.session_id, &input.owner_user_id)?;
if input.play_type != CreativeTargetPlayType::Puzzle {
return Err(CreativeAgentError::UnsupportedTargetPlayType);
}
if normalize_required_string(&input.target_session_id).is_none() {
return Err(CreativeAgentError::MissingTargetSessionId);
}
if normalize_optional_string(template_selection_json.map(str::to_string)).is_none() {
return Err(CreativeAgentError::TemplateNotConfirmed);
}
// 中文注释:绑定目标 session 是“草稿已创建”的持久化标记,只允许在行动链路之后发生。
if !matches!(
current_stage,
CreativeAgentStage::PlanningPuzzleLevels
| CreativeAgentStage::Acting
| CreativeAgentStage::Reflecting
| CreativeAgentStage::Collaborating
| CreativeAgentStage::TargetReady
) {
return Err(CreativeAgentError::InvalidStageTransition);
}
Ok(())
}
pub fn validate_stage_transition(
current: CreativeAgentStage,
next: CreativeAgentStage,
) -> Result<(), CreativeAgentError> {
if current == next {
return Ok(());
}
if matches!(
next,
CreativeAgentStage::Failed | CreativeAgentStage::WaitingUser
) {
return Ok(());
}
let allowed = matches!(
(current, next),
(CreativeAgentStage::Idle, CreativeAgentStage::Perceiving)
| (
CreativeAgentStage::Idle,
CreativeAgentStage::SelectingPuzzleTemplate
)
| (CreativeAgentStage::Perceiving, CreativeAgentStage::Thinking)
| (
CreativeAgentStage::Thinking,
CreativeAgentStage::Remembering
)
| (
CreativeAgentStage::Thinking,
CreativeAgentStage::SelectingPuzzleTemplate
)
| (
CreativeAgentStage::Remembering,
CreativeAgentStage::SelectingPuzzleTemplate
)
| (
CreativeAgentStage::SelectingPuzzleTemplate,
CreativeAgentStage::WaitingTemplateConfirmation
)
| (
CreativeAgentStage::WaitingTemplateConfirmation,
CreativeAgentStage::PlanningPuzzleLevels
)
| (
CreativeAgentStage::PlanningPuzzleLevels,
CreativeAgentStage::Acting
)
| (CreativeAgentStage::Acting, CreativeAgentStage::Reflecting)
| (
CreativeAgentStage::Reflecting,
CreativeAgentStage::Collaborating
)
| (
CreativeAgentStage::Collaborating,
CreativeAgentStage::Acting
)
| (
CreativeAgentStage::Reflecting,
CreativeAgentStage::TargetReady
)
| (CreativeAgentStage::Acting, CreativeAgentStage::TargetReady)
| (
CreativeAgentStage::PlanningPuzzleLevels,
CreativeAgentStage::TargetReady
)
);
if allowed {
Ok(())
} else {
Err(CreativeAgentError::InvalidStageTransition)
}
}
pub fn normalize_message_role(value: CreativeAgentMessageRole) -> &'static str {
value.as_str()
}
pub fn normalize_message_kind(value: CreativeAgentMessageKind) -> &'static str {
value.as_str()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CreativeAgentTargetBindInput, CreativeTargetStage};
#[test]
fn template_confirmation_requires_cost_range() {
let input = CreativeAgentTemplateConfirmInput {
session_id: "creative-session-1".to_string(),
owner_user_id: "user-1".to_string(),
template_selection_json: r#"{"templateId":"puzzle.default-creative"}"#.to_string(),
updated_at_micros: 1,
};
assert_eq!(
validate_template_confirmation(CreativeAgentStage::WaitingTemplateConfirmation, &input,),
Err(CreativeAgentError::MissingCostRange)
);
}
#[test]
fn target_binding_requires_confirmed_template() {
let input = CreativeAgentTargetBindInput {
binding_id: "creative-binding-1".to_string(),
session_id: "creative-session-1".to_string(),
owner_user_id: "user-1".to_string(),
play_type: CreativeTargetPlayType::Puzzle,
target_session_id: "puzzle-session-1".to_string(),
target_stage: CreativeTargetStage::PuzzleResult,
result_profile_id: None,
created_at_micros: 1,
};
assert_eq!(
validate_target_binding(CreativeAgentStage::Acting, None, &input),
Err(CreativeAgentError::TemplateNotConfirmed)
);
}
#[test]
fn phase1_stage_path_allows_template_to_target_ready() {
assert!(
validate_stage_transition(
CreativeAgentStage::WaitingTemplateConfirmation,
CreativeAgentStage::PlanningPuzzleLevels,
)
.is_ok()
);
assert!(
validate_stage_transition(
CreativeAgentStage::PlanningPuzzleLevels,
CreativeAgentStage::Acting
)
.is_ok()
);
assert!(
validate_stage_transition(CreativeAgentStage::Acting, CreativeAgentStage::TargetReady)
.is_ok()
);
}
}

View File

@@ -0,0 +1,89 @@
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
use crate::{
CreativeAgentMessageKind, CreativeAgentMessageRole, CreativeAgentStage,
CreativeInputSummarySnapshot, CreativeTargetPlayType, CreativeTargetStage,
};
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionCreateInput {
pub session_id: String,
pub owner_user_id: String,
pub input_summary: CreativeInputSummarySnapshot,
pub welcome_message_id: Option<String>,
pub welcome_message_text: Option<String>,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionGetInput {
pub session_id: String,
pub owner_user_id: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentMessageAppendInput {
pub session_id: String,
pub owner_user_id: String,
pub message_id: String,
pub role: CreativeAgentMessageRole,
pub kind: CreativeAgentMessageKind,
pub text: String,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentStageUpdateInput {
pub session_id: String,
pub owner_user_id: String,
pub stage: CreativeAgentStage,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentTemplateConfirmInput {
pub session_id: String,
pub owner_user_id: String,
pub template_selection_json: String,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentLevelPlanSaveInput {
pub session_id: String,
pub owner_user_id: String,
pub level_plan_json: String,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentTargetBindInput {
pub binding_id: String,
pub session_id: String,
pub owner_user_id: String,
pub play_type: CreativeTargetPlayType,
pub target_session_id: String,
pub target_stage: CreativeTargetStage,
pub result_profile_id: Option<String>,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentFinalizeInput {
pub session_id: String,
pub owner_user_id: String,
pub stage: CreativeAgentStage,
pub assistant_message_id: Option<String>,
pub assistant_message_text: Option<String>,
pub updated_at_micros: i64,
}

View File

@@ -0,0 +1,187 @@
//! 创意互动 Agent 领域模型。
//!
//! 本 crate 只描述会话、阶段、消息和目标绑定的纯领域事实LLM、SSE、
//! 图片生成和 SpacetimeDB 写表均留在外层 adapter。
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
pub const CREATIVE_AGENT_SESSION_ID_PREFIX: &str = "creative-session-";
pub const CREATIVE_AGENT_MESSAGE_ID_PREFIX: &str = "creative-message-";
pub const CREATIVE_AGENT_BINDING_ID_PREFIX: &str = "creative-binding-";
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeAgentStage {
Idle,
Perceiving,
Thinking,
Remembering,
SelectingPuzzleTemplate,
WaitingTemplateConfirmation,
PlanningPuzzleLevels,
Acting,
Reflecting,
Collaborating,
TargetReady,
WaitingUser,
Failed,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeAgentMessageRole {
User,
Assistant,
System,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeAgentMessageKind {
Chat,
Stage,
ActionResult,
Warning,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeTargetPlayType {
Puzzle,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CreativeTargetStage {
PuzzleAgentWorkspace,
PuzzleResult,
PuzzleRuntime,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeInputImageSnapshot {
pub asset_id: Option<String>,
pub read_url: Option<String>,
pub thumbnail_url: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub summary: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeInputSummarySnapshot {
pub text: Option<String>,
pub entry_context: String,
pub images: Vec<CreativeInputImageSnapshot>,
pub material_summary: Option<String>,
pub unsupported_capabilities_json: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentMessageSnapshot {
pub message_id: String,
pub session_id: String,
pub role: CreativeAgentMessageRole,
pub kind: CreativeAgentMessageKind,
pub text: String,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentTargetBindingSnapshot {
pub binding_id: String,
pub session_id: String,
pub play_type: CreativeTargetPlayType,
pub target_session_id: String,
pub target_stage: CreativeTargetStage,
pub result_profile_id: Option<String>,
pub created_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionSnapshot {
pub session_id: String,
pub owner_user_id: String,
pub stage: CreativeAgentStage,
pub input_summary: CreativeInputSummarySnapshot,
pub messages: Vec<CreativeAgentMessageSnapshot>,
pub puzzle_template_selection_json: Option<String>,
pub puzzle_image_generation_plan_json: Option<String>,
pub target_binding: Option<CreativeAgentTargetBindingSnapshot>,
pub created_at_micros: i64,
pub updated_at_micros: i64,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreativeAgentSessionProcedureResult {
pub ok: bool,
pub session_json: Option<String>,
pub error_message: Option<String>,
}
impl CreativeAgentStage {
pub fn as_str(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Perceiving => "perceiving",
Self::Thinking => "thinking",
Self::Remembering => "remembering",
Self::SelectingPuzzleTemplate => "selecting_puzzle_template",
Self::WaitingTemplateConfirmation => "waiting_template_confirmation",
Self::PlanningPuzzleLevels => "planning_puzzle_levels",
Self::Acting => "acting",
Self::Reflecting => "reflecting",
Self::Collaborating => "collaborating",
Self::TargetReady => "target_ready",
Self::WaitingUser => "waiting_user",
Self::Failed => "failed",
}
}
}
impl CreativeAgentMessageRole {
pub fn as_str(self) -> &'static str {
match self {
Self::User => "user",
Self::Assistant => "assistant",
Self::System => "system",
}
}
}
impl CreativeAgentMessageKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Chat => "chat",
Self::Stage => "stage",
Self::ActionResult => "action_result",
Self::Warning => "warning",
}
}
}
impl CreativeTargetPlayType {
pub fn as_str(self) -> &'static str {
match self {
Self::Puzzle => "puzzle",
}
}
}
impl CreativeTargetStage {
pub fn as_str(self) -> &'static str {
match self {
Self::PuzzleAgentWorkspace => "puzzle-agent-workspace",
Self::PuzzleResult => "puzzle-result",
Self::PuzzleRuntime => "puzzle-runtime",
}
}
}

View File

@@ -0,0 +1,35 @@
use std::{error::Error, fmt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CreativeAgentError {
MissingSessionId,
MissingOwnerUserId,
MissingMessageId,
MissingMessageText,
MissingTemplateSelection,
MissingCostRange,
MissingTargetSessionId,
InvalidStageTransition,
TemplateNotConfirmed,
UnsupportedTargetPlayType,
}
impl fmt::Display for CreativeAgentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::MissingSessionId => "creative session_id 缺失",
Self::MissingOwnerUserId => "creative owner_user_id 缺失",
Self::MissingMessageId => "creative message_id 缺失",
Self::MissingMessageText => "creative message text 缺失",
Self::MissingTemplateSelection => "拼图模板选择缺失",
Self::MissingCostRange => "拼图模板积分范围缺失",
Self::MissingTargetSessionId => "目标拼图 session 缺失",
Self::InvalidStageTransition => "创意 Agent 阶段迁移不合法",
Self::TemplateNotConfirmed => "拼图模板未确认,不能创建草稿",
Self::UnsupportedTargetPlayType => "Phase 1 只允许绑定拼图 target",
};
write!(f, "{message}")
}
}
impl Error for CreativeAgentError {}

View File

@@ -0,0 +1,9 @@
mod application;
mod commands;
mod domain;
mod errors;
pub use application::*;
pub use commands::*;
pub use domain::*;
pub use errors::*;

View File

@@ -5,10 +5,10 @@ use crate::{
MATCH3D_BLOCK_VISUAL_KEYS, MATCH3D_BOARD_CENTER, MATCH3D_BOARD_RADIUS,
MATCH3D_BOARD_SAFE_MARGIN, MATCH3D_DEFAULT_DURATION_LIMIT_MS, MATCH3D_ITEMS_PER_CLEAR,
MATCH3D_MAX_DIFFICULTY, MATCH3D_MAX_ITEM_TYPE_COUNT, MATCH3D_MIN_DIFFICULTY,
MATCH3D_TRAY_SLOT_COUNT, Match3DClickConfirmation, Match3DClickInput,
Match3DClickRejectReason, Match3DCreatorConfig, Match3DFailureReason, Match3DFieldError,
Match3DItemSnapshot, Match3DItemState, Match3DPublicationStatus, Match3DResultDraft,
Match3DRunSnapshot, Match3DRunStatus, Match3DTraySlot, Match3DWorkProfile,
MATCH3D_TRAY_SLOT_COUNT, Match3DClickConfirmation, Match3DClickInput, Match3DClickRejectReason,
Match3DCreatorConfig, Match3DFailureReason, Match3DFieldError, Match3DItemSnapshot,
Match3DItemState, Match3DPublicationStatus, Match3DResultDraft, Match3DRunSnapshot,
Match3DRunStatus, Match3DTraySlot, Match3DWorkProfile,
};
#[derive(Clone, Copy)]
@@ -358,10 +358,7 @@ fn resolve_size_tier_plan(item_type_count: usize) -> Vec<Match3DSizeTierRule> {
(exact_count.floor() as usize, exact_count.fract(), *rule)
})
.collect::<Vec<_>>();
let mut assigned_count = plans
.iter()
.map(|(count, _, _)| *count)
.sum::<usize>();
let mut assigned_count = plans.iter().map(|(count, _, _)| *count).sum::<usize>();
let mut remainder_order = (0..plans.len()).collect::<Vec<_>>();
remainder_order.sort_by(|left, right| {
plans[*right]
@@ -802,9 +799,11 @@ mod tests {
}
assert_eq!(radii_by_visual_key.len(), 25);
assert!(radii_by_visual_key.values().all(|radii| {
radii.iter().all(|radius| radius == &radii[0])
}));
assert!(
radii_by_visual_key
.values()
.all(|radii| { radii.iter().all(|radius| radius == &radii[0]) })
);
}
#[test]
@@ -824,7 +823,11 @@ mod tests {
.iter()
.map(|item| item.visual_key.as_str())
.collect::<Vec<_>>();
assert!(visual_keys.iter().all(|visual_key| visual_key.starts_with("block-")));
assert!(
visual_keys
.iter()
.all(|visual_key| visual_key.starts_with("block-"))
);
for item in &run.items {
let dx = item.x - MATCH3D_BOARD_CENTER;

View File

@@ -10,5 +10,6 @@ spacetime-types = ["dep:spacetimedb"]
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
shared-kernel = { workspace = true }
spacetimedb = { workspace = true, optional = true }

View File

@@ -185,6 +185,7 @@ pub fn compile_result_draft_from_seed(
level_id: "puzzle-level-1".to_string(),
level_name: level_name.clone(),
picture_description,
picture_reference: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
@@ -240,6 +241,7 @@ pub fn build_form_draft_from_parts(
level_id: "puzzle-level-1".to_string(),
level_name: String::new(),
picture_description: picture_description.clone().unwrap_or_default(),
picture_reference: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
@@ -344,6 +346,7 @@ pub fn normalize_puzzle_draft(mut draft: PuzzleResultDraft) -> PuzzleResultDraft
&draft.anchor_pack.visual_subject.value,
&draft.summary,
),
picture_reference: None,
candidates: draft.candidates.clone(),
selected_candidate_id: draft.selected_candidate_id.clone(),
cover_image_src: draft.cover_image_src.clone(),
@@ -429,6 +432,7 @@ pub fn append_blank_puzzle_level(draft: &PuzzleResultDraft) -> PuzzleResultDraft
next_index,
),
picture_description,
picture_reference: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
@@ -671,10 +675,12 @@ pub fn normalize_puzzle_levels(
.unwrap_or_else(|| format!("puzzle-level-{}", index + 1));
let picture_description = normalize_required_string(&level.picture_description)
.unwrap_or_else(|| format!("{}关画面", index + 1));
let picture_reference = level.picture_reference.and_then(normalize_required_string);
let level_name = normalize_required_string(&level.level_name).unwrap_or_default();
level.level_id = level_id;
level.level_name = level_name;
level.picture_description = picture_description;
level.picture_reference = picture_reference;
level.generation_status = normalize_required_string(&level.generation_status)
.unwrap_or_else(|| "idle".to_string());
normalized_levels.push(level);
@@ -2791,6 +2797,7 @@ mod tests {
level_id: "puzzle-level-1".to_string(),
level_name: format!("{profile_id} 关"),
picture_description: "summary".to_string(),
picture_reference: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: Some("/cover.png".to_string()),
@@ -3004,6 +3011,7 @@ mod tests {
level_id: "puzzle-level-1".to_string(),
level_name: "第一关".to_string(),
picture_description: "第一关画面".to_string(),
picture_reference: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: Some("/level-1.png".to_string()),
@@ -3014,6 +3022,7 @@ mod tests {
level_id: "puzzle-level-2".to_string(),
level_name: "第二关".to_string(),
picture_description: "第二关画面".to_string(),
picture_reference: None,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: Some("/level-2.png".to_string()),

View File

@@ -0,0 +1,208 @@
//! 拼图创意 Agent 模板协议。
//!
//! 这里只保存拼图模块自己的模板事实HTTP / SSE 展示字段由 api-server
//! 再映射到 shared-contracts避免通用 Agent 复制拼图模板规则。
use serde::{Deserialize, Serialize};
pub const PUZZLE_PHASE1_TEMPLATE_ID: &str = "puzzle.default-creative";
pub const PUZZLE_PHASE1_TEMPLATE_TITLE: &str = "创意拼图";
pub const PUZZLE_FAMILY_KEEPSTAKE_TEMPLATE_ID: &str = "puzzle.family-keepsake";
pub const PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID: &str = "puzzle.travel-memory";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PuzzleCreativePricingUnit {
Point,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleCreativeSupportedLevelMode {
Single,
Multi,
SingleOrMulti,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleCreativeLevelGenerationMode {
SingleLevel,
MultiLevel,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeCostRange {
pub min_points: u32,
pub max_points: u32,
pub pricing_unit: PuzzleCreativePricingUnit,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PuzzleCreativeDraftEditableFieldPath {
#[serde(rename = "workTitle")]
WorkTitle,
#[serde(rename = "workDescription")]
WorkDescription,
#[serde(rename = "workTags")]
WorkTags,
#[serde(rename = "levels[].levelName")]
LevelName,
#[serde(rename = "levels[].pictureDescription")]
LevelPictureDescription,
#[serde(rename = "levels[].pictureReference")]
LevelPictureReference,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeImageGenerationPolicy {
pub allow_uploaded_image_directly: bool,
pub allow_generated_images: bool,
pub allow_per_level_reference_image: bool,
pub default_candidate_count_per_level: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateProtocol {
pub template_id: String,
pub title: String,
pub summary: String,
pub preview_image_src: Option<String>,
pub supported_level_mode: PuzzleCreativeSupportedLevelMode,
pub min_level_count: u32,
pub max_level_count: u32,
pub default_level_count: u32,
pub cost_range: PuzzleCreativeCostRange,
pub required_draft_fields: Vec<PuzzleCreativeDraftEditableFieldPath>,
pub image_policy: PuzzleCreativeImageGenerationPolicy,
}
fn shared_required_draft_fields() -> Vec<PuzzleCreativeDraftEditableFieldPath> {
vec![
PuzzleCreativeDraftEditableFieldPath::WorkTitle,
PuzzleCreativeDraftEditableFieldPath::WorkDescription,
PuzzleCreativeDraftEditableFieldPath::WorkTags,
PuzzleCreativeDraftEditableFieldPath::LevelName,
PuzzleCreativeDraftEditableFieldPath::LevelPictureDescription,
PuzzleCreativeDraftEditableFieldPath::LevelPictureReference,
]
}
fn shared_image_policy() -> PuzzleCreativeImageGenerationPolicy {
PuzzleCreativeImageGenerationPolicy {
allow_uploaded_image_directly: true,
allow_generated_images: true,
allow_per_level_reference_image: true,
default_candidate_count_per_level: 1,
}
}
fn build_template(
template_id: &str,
title: &str,
summary: &str,
default_level_count: u32,
min_points: u32,
max_points: u32,
reason: &str,
) -> PuzzleCreativeTemplateProtocol {
PuzzleCreativeTemplateProtocol {
template_id: template_id.to_string(),
title: title.to_string(),
summary: summary.to_string(),
preview_image_src: None,
supported_level_mode: PuzzleCreativeSupportedLevelMode::SingleOrMulti,
min_level_count: 1,
max_level_count: 6,
default_level_count,
cost_range: PuzzleCreativeCostRange {
min_points,
max_points,
pricing_unit: PuzzleCreativePricingUnit::Point,
reason: reason.to_string(),
},
required_draft_fields: shared_required_draft_fields(),
image_policy: shared_image_policy(),
}
}
pub fn retrieve_puzzle_template_catalog() -> Vec<PuzzleCreativeTemplateProtocol> {
vec![
build_template(
PUZZLE_PHASE1_TEMPLATE_ID,
PUZZLE_PHASE1_TEMPLATE_TITLE,
"把图文灵感整理成可编辑、可试玩的拼图草稿。",
1,
2,
12,
"按关卡数和每关图片生成次数估算,实际扣费以后端任务结算为准",
),
build_template(
PUZZLE_FAMILY_KEEPSTAKE_TEMPLATE_ID,
"家庭纪念拼图",
"把合影、节日或成长瞬间做成温暖的纪念拼图。",
3,
4,
14,
"按纪念主题多关卡和图片候选估算,实际扣费以后端任务结算为准",
),
build_template(
PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID,
"旅行记忆拼图",
"把一次出行拆成地点、风景和故事节点拼图。",
3,
4,
16,
"按旅行节点和每关图片生成次数估算,实际扣费以后端任务结算为准",
),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phase1_template_contains_cost_range_and_editable_fields() {
let template = retrieve_puzzle_template_catalog()
.into_iter()
.find(|template| template.template_id == PUZZLE_PHASE1_TEMPLATE_ID)
.expect("template should exist");
assert_eq!(template.template_id, PUZZLE_PHASE1_TEMPLATE_ID);
assert_eq!(template.cost_range.min_points, 2);
assert_eq!(template.cost_range.max_points, 12);
assert!(
template
.required_draft_fields
.contains(&PuzzleCreativeDraftEditableFieldPath::LevelPictureReference)
);
}
#[test]
fn catalog_exposes_multiple_phase1_puzzle_subtemplates() {
let catalog = retrieve_puzzle_template_catalog();
assert!(catalog.len() >= 3);
assert!(
catalog
.iter()
.any(|template| template.template_id == PUZZLE_FAMILY_KEEPSTAKE_TEMPLATE_ID)
);
assert!(
catalog
.iter()
.any(|template| template.template_id == PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID)
);
assert!(
catalog
.iter()
.all(|template| template.supported_level_mode
== PuzzleCreativeSupportedLevelMode::SingleOrMulti)
);
}
}

View File

@@ -0,0 +1,529 @@
//! 拼图创意 Agent 草稿工具。
//!
//! 通用 Agent 只能把模型输出交给这些工具;字段归一化、模板关卡数和可编辑
//! 字段白名单都收口在拼图模块,避免 api-server 复制草稿业务规则。
use serde::{Deserialize, Serialize};
use serde_json::Value;
use shared_kernel::{normalize_required_string, normalize_string_list};
use crate::{
application::{
build_form_anchor_pack, build_result_preview, normalize_puzzle_draft,
normalize_puzzle_levels, sync_primary_level_fields,
},
creative_templates::{
PuzzleCreativeCostRange, PuzzleCreativeDraftEditableFieldPath,
PuzzleCreativeLevelGenerationMode, PuzzleCreativeSupportedLevelMode,
PuzzleCreativeTemplateProtocol, retrieve_puzzle_template_catalog,
},
domain::{
PUZZLE_MAX_TAG_COUNT, PUZZLE_MIN_TAG_COUNT, PuzzleDraftLevel, PuzzleFormDraft,
PuzzleResultDraft,
},
errors::PuzzleFieldError,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreativePuzzleLevelDraftInput {
pub level_name: String,
pub picture_description: String,
#[serde(default)]
pub picture_reference: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreativePuzzleDraftToolInput {
pub template_id: String,
pub template_cost_range: PuzzleCreativeCostRange,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
pub levels: Vec<CreativePuzzleLevelDraftInput>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateSelection {
pub template_id: String,
pub title: String,
pub reason: String,
pub cost_range: PuzzleCreativeCostRange,
pub supported_level_mode: PuzzleCreativeSupportedLevelMode,
pub selected_level_mode: PuzzleCreativeLevelGenerationMode,
pub planned_level_count: u32,
pub requires_user_confirmation: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleLevelImagePlanInput {
pub template_id: String,
pub selected_level_mode: PuzzleCreativeLevelGenerationMode,
pub levels: Vec<CreativePuzzleLevelDraftInput>,
pub cost_range: PuzzleCreativeCostRange,
#[serde(default)]
pub candidate_count_per_level: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleImageGenerationPlanLevel {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub image_prompt: String,
#[serde(default)]
pub picture_reference: Option<String>,
pub candidate_count: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleImageGenerationPlan {
pub mode: PuzzleCreativeLevelGenerationMode,
pub template_id: String,
pub estimated_cost_range: PuzzleCreativeCostRange,
pub levels: Vec<PuzzleImageGenerationPlanLevel>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleDraftFieldPatchOperation {
Set,
Append,
Replace,
Remove,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleDraftFieldPatch {
pub field_path: PuzzleCreativeDraftEditableFieldPath,
pub operation: PuzzleDraftFieldPatchOperation,
#[serde(default)]
pub level_id: Option<String>,
pub value: Value,
pub rationale: String,
}
pub fn validate_puzzle_template_selection(
selection: &PuzzleCreativeTemplateSelection,
) -> Result<(), PuzzleFieldError> {
let template = resolve_phase1_template(&selection.template_id)?;
if selection.cost_range != template.cost_range
|| selection.supported_level_mode != template.supported_level_mode
|| !selection.requires_user_confirmation
{
return Err(PuzzleFieldError::InvalidOperation);
}
validate_level_count(
selection.planned_level_count,
&selection.selected_level_mode,
&template,
)
}
pub fn build_puzzle_draft_from_creative_fields(
input: CreativePuzzleDraftToolInput,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
let template = resolve_phase1_template(&input.template_id)?;
if input.template_cost_range != template.cost_range {
return Err(PuzzleFieldError::InvalidOperation);
}
validate_level_count(
input.levels.len() as u32,
&if input.levels.len() > 1 {
PuzzleCreativeLevelGenerationMode::MultiLevel
} else {
PuzzleCreativeLevelGenerationMode::SingleLevel
},
&template,
)?;
let work_title =
normalize_required_string(&input.work_title).ok_or(PuzzleFieldError::MissingText)?;
let work_description =
normalize_required_string(&input.work_description).ok_or(PuzzleFieldError::MissingText)?;
let tags = normalize_theme_tags_for_creative(input.work_tags)?;
let anchor_pack = build_form_anchor_pack(
work_title.as_str(),
input
.levels
.first()
.map(|level| level.picture_description.as_str())
.unwrap_or(work_description.as_str()),
);
let levels = input
.levels
.into_iter()
.enumerate()
.map(|(index, level)| {
let picture_description = normalize_required_string(&level.picture_description)
.ok_or(PuzzleFieldError::MissingText)?;
Ok(PuzzleDraftLevel {
level_id: format!("puzzle-level-{}", index + 1),
level_name: normalize_required_string(&level.level_name)
.unwrap_or_else(|| format!("{}", index + 1)),
picture_description,
picture_reference: level.picture_reference.and_then(normalize_required_string),
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
})
})
.collect::<Result<Vec<_>, PuzzleFieldError>>()?;
let mut draft = PuzzleResultDraft {
work_title: work_title.clone(),
work_description: work_description.clone(),
level_name: levels
.first()
.map(|level| level.level_name.clone())
.unwrap_or_default(),
summary: work_description.clone(),
theme_tags: tags,
forbidden_directives: Vec::new(),
creator_intent: None,
anchor_pack,
candidates: Vec::new(),
selected_candidate_id: None,
cover_image_src: None,
cover_asset_id: None,
generation_status: "idle".to_string(),
levels,
form_draft: Some(PuzzleFormDraft {
work_title: Some(work_title),
work_description: Some(work_description),
picture_description: None,
}),
};
sync_primary_level_fields(&mut draft);
Ok(normalize_puzzle_draft(draft))
}
pub fn plan_puzzle_level_images(
input: PuzzleLevelImagePlanInput,
) -> Result<PuzzleImageGenerationPlan, PuzzleFieldError> {
let template = resolve_phase1_template(&input.template_id)?;
validate_level_count(
input.levels.len() as u32,
&input.selected_level_mode,
&template,
)?;
if input.cost_range != template.cost_range {
return Err(PuzzleFieldError::InvalidOperation);
}
let candidate_count = input.candidate_count_per_level.unwrap_or(1).clamp(1, 1);
let levels = input
.levels
.into_iter()
.enumerate()
.map(|(index, level)| {
let picture_description = normalize_required_string(&level.picture_description)
.ok_or(PuzzleFieldError::MissingText)?;
let level_name = normalize_required_string(&level.level_name)
.unwrap_or_else(|| format!("{}", index + 1));
Ok(PuzzleImageGenerationPlanLevel {
level_id: format!("puzzle-level-{}", index + 1),
image_prompt: build_level_image_prompt(&level_name, &picture_description),
level_name,
picture_description,
picture_reference: level.picture_reference.and_then(normalize_required_string),
candidate_count,
})
})
.collect::<Result<Vec<_>, PuzzleFieldError>>()?;
Ok(PuzzleImageGenerationPlan {
mode: input.selected_level_mode,
template_id: input.template_id,
estimated_cost_range: input.cost_range,
levels,
})
}
pub fn apply_puzzle_draft_field_patch(
draft: PuzzleResultDraft,
patch: PuzzleDraftFieldPatch,
) -> Result<PuzzleResultDraft, PuzzleFieldError> {
if patch.operation != PuzzleDraftFieldPatchOperation::Set
&& patch.operation != PuzzleDraftFieldPatchOperation::Replace
{
return Err(PuzzleFieldError::InvalidOperation);
}
let mut next_draft = normalize_puzzle_draft(draft);
match patch.field_path {
PuzzleCreativeDraftEditableFieldPath::WorkTitle => {
next_draft.work_title = value_as_required_string(&patch.value)?;
}
PuzzleCreativeDraftEditableFieldPath::WorkDescription => {
next_draft.work_description = value_as_required_string(&patch.value)?;
}
PuzzleCreativeDraftEditableFieldPath::WorkTags => {
next_draft.theme_tags =
normalize_theme_tags_for_creative(value_as_string_list(&patch.value)?)?;
}
PuzzleCreativeDraftEditableFieldPath::LevelName => {
let level = mutable_level_for_patch(&mut next_draft, patch.level_id.as_deref())?;
level.level_name = value_as_required_string(&patch.value)?;
}
PuzzleCreativeDraftEditableFieldPath::LevelPictureDescription => {
let level = mutable_level_for_patch(&mut next_draft, patch.level_id.as_deref())?;
level.picture_description = value_as_required_string(&patch.value)?;
}
PuzzleCreativeDraftEditableFieldPath::LevelPictureReference => {
let level = mutable_level_for_patch(&mut next_draft, patch.level_id.as_deref())?;
level.picture_reference = value_as_optional_string(&patch.value);
}
}
let levels = normalize_puzzle_levels(next_draft.levels.clone(), &next_draft.theme_tags)?;
next_draft.levels = levels;
sync_primary_level_fields(&mut next_draft);
let _ = build_result_preview(&next_draft, Some("百梦主"));
Ok(next_draft)
}
fn resolve_phase1_template(
template_id: &str,
) -> Result<PuzzleCreativeTemplateProtocol, PuzzleFieldError> {
let normalized_template_id =
normalize_required_string(template_id).ok_or(PuzzleFieldError::InvalidOperation)?;
retrieve_puzzle_template_catalog()
.into_iter()
.find(|template| template.template_id == normalized_template_id)
.ok_or(PuzzleFieldError::InvalidOperation)
}
fn validate_level_count(
count: u32,
mode: &PuzzleCreativeLevelGenerationMode,
template: &PuzzleCreativeTemplateProtocol,
) -> Result<(), PuzzleFieldError> {
if count < template.min_level_count || count > template.max_level_count {
return Err(PuzzleFieldError::InvalidOperation);
}
if matches!(mode, PuzzleCreativeLevelGenerationMode::SingleLevel) && count != 1 {
return Err(PuzzleFieldError::InvalidOperation);
}
if matches!(mode, PuzzleCreativeLevelGenerationMode::MultiLevel) && count < 2 {
return Err(PuzzleFieldError::InvalidOperation);
}
Ok(())
}
fn normalize_theme_tags_for_creative(values: Vec<String>) -> Result<Vec<String>, PuzzleFieldError> {
let mut tags = Vec::new();
for tag in normalize_string_list(values) {
if !tags.contains(&tag) {
tags.push(tag);
}
if tags.len() >= PUZZLE_MAX_TAG_COUNT {
break;
}
}
if tags.len() < PUZZLE_MIN_TAG_COUNT {
return Err(PuzzleFieldError::InvalidTagCount);
}
Ok(tags)
}
fn build_level_image_prompt(level_name: &str, picture_description: &str) -> String {
format!("{level_name}{picture_description}。清晰主体,适合拼图切块。")
}
fn mutable_level_for_patch<'a>(
draft: &'a mut PuzzleResultDraft,
level_id: Option<&str>,
) -> Result<&'a mut PuzzleDraftLevel, PuzzleFieldError> {
if let Some(level_id) = level_id.and_then(normalize_required_string) {
return draft
.levels
.iter_mut()
.find(|level| level.level_id == level_id)
.ok_or(PuzzleFieldError::InvalidOperation);
}
draft
.levels
.first_mut()
.ok_or(PuzzleFieldError::InvalidOperation)
}
fn value_as_required_string(value: &Value) -> Result<String, PuzzleFieldError> {
value
.as_str()
.and_then(normalize_required_string)
.ok_or(PuzzleFieldError::MissingText)
}
fn value_as_optional_string(value: &Value) -> Option<String> {
value.as_str().and_then(normalize_required_string)
}
fn value_as_string_list(value: &Value) -> Result<Vec<String>, PuzzleFieldError> {
value
.as_array()
.map(|values| {
values
.iter()
.filter_map(|value| value.as_str().map(ToString::to_string))
.collect::<Vec<_>>()
})
.ok_or(PuzzleFieldError::InvalidOperation)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::creative_templates::{PUZZLE_PHASE1_TEMPLATE_ID, PuzzleCreativePricingUnit};
fn cost_range() -> PuzzleCreativeCostRange {
PuzzleCreativeCostRange {
min_points: 2,
max_points: 12,
pricing_unit: PuzzleCreativePricingUnit::Point,
reason: "按关卡数和每关图片生成次数估算,实际扣费以后端任务结算为准".to_string(),
}
}
#[test]
fn creative_draft_builds_single_level_with_summary_and_plan() {
let input_level = CreativePuzzleLevelDraftInput {
level_name: "第一关".to_string(),
picture_description: "生日蛋糕和朋友合影。".to_string(),
picture_reference: Some("https://assets.example.test/birthday.png".to_string()),
};
let draft = build_puzzle_draft_from_creative_fields(CreativePuzzleDraftToolInput {
template_id: PUZZLE_PHASE1_TEMPLATE_ID.to_string(),
template_cost_range: cost_range(),
work_title: "生日拼图".to_string(),
work_description: "把生日照片做成一关拼图。".to_string(),
work_tags: vec!["生日".to_string(), "朋友".to_string(), "纪念".to_string()],
levels: vec![input_level.clone()],
})
.expect("single level draft should build");
let plan = plan_puzzle_level_images(PuzzleLevelImagePlanInput {
template_id: PUZZLE_PHASE1_TEMPLATE_ID.to_string(),
selected_level_mode: PuzzleCreativeLevelGenerationMode::SingleLevel,
levels: vec![input_level],
cost_range: cost_range(),
candidate_count_per_level: Some(3),
})
.expect("single level image plan should build");
assert_eq!(draft.work_title, "生日拼图");
assert_eq!(draft.work_description, "把生日照片做成一关拼图。");
assert_eq!(draft.summary, "把生日照片做成一关拼图。");
assert_eq!(draft.level_name, "第一关");
assert_eq!(draft.levels.len(), 1);
assert_eq!(
draft.levels[0].picture_reference.as_deref(),
Some("https://assets.example.test/birthday.png")
);
assert_eq!(plan.mode, PuzzleCreativeLevelGenerationMode::SingleLevel);
assert_eq!(plan.levels.len(), 1);
assert_eq!(plan.levels[0].candidate_count, 1);
assert!(plan.levels[0].image_prompt.contains("生日蛋糕和朋友合影"));
}
#[test]
fn creative_draft_builds_multi_level_picture_references() {
let draft = build_puzzle_draft_from_creative_fields(CreativePuzzleDraftToolInput {
template_id: PUZZLE_PHASE1_TEMPLATE_ID.to_string(),
template_cost_range: cost_range(),
work_title: "旅行拼图".to_string(),
work_description: "把旅行照片做成系列拼图。".to_string(),
work_tags: vec!["旅行".to_string(), "照片".to_string(), "纪念".to_string()],
levels: vec![
CreativePuzzleLevelDraftInput {
level_name: "第一站".to_string(),
picture_description: "海边合影".to_string(),
picture_reference: Some("asset-1".to_string()),
},
CreativePuzzleLevelDraftInput {
level_name: "第二站".to_string(),
picture_description: "山顶日落".to_string(),
picture_reference: Some("asset-2".to_string()),
},
],
})
.expect("draft should build");
assert_eq!(draft.work_title, "旅行拼图");
assert_eq!(draft.theme_tags, vec!["旅行", "照片", "纪念"]);
assert_eq!(draft.levels.len(), 2);
assert_eq!(
draft.levels[1].picture_reference.as_deref(),
Some("asset-2")
);
}
#[test]
fn creative_draft_accepts_catalog_subtemplate_id() {
let draft = build_puzzle_draft_from_creative_fields(CreativePuzzleDraftToolInput {
template_id: crate::creative_templates::PUZZLE_TRAVEL_MEMORY_TEMPLATE_ID.to_string(),
template_cost_range: PuzzleCreativeCostRange {
min_points: 4,
max_points: 16,
pricing_unit: PuzzleCreativePricingUnit::Point,
reason: "按旅行节点和每关图片生成次数估算,实际扣费以后端任务结算为准"
.to_string(),
},
work_title: "旅行记忆".to_string(),
work_description: "把旅行照片做成系列拼图。".to_string(),
work_tags: vec!["旅行".to_string(), "照片".to_string(), "纪念".to_string()],
levels: vec![
CreativePuzzleLevelDraftInput {
level_name: "第一站".to_string(),
picture_description: "海边合影".to_string(),
picture_reference: Some("asset-1".to_string()),
},
CreativePuzzleLevelDraftInput {
level_name: "第二站".to_string(),
picture_description: "山顶日落".to_string(),
picture_reference: Some("asset-2".to_string()),
},
],
})
.expect("subtemplate draft should build");
assert_eq!(draft.work_title, "旅行记忆");
assert_eq!(draft.levels.len(), 2);
}
#[test]
fn draft_patch_rejects_non_whitelisted_operation() {
let draft = build_puzzle_draft_from_creative_fields(CreativePuzzleDraftToolInput {
template_id: PUZZLE_PHASE1_TEMPLATE_ID.to_string(),
template_cost_range: cost_range(),
work_title: "旅行拼图".to_string(),
work_description: "把旅行照片做成系列拼图。".to_string(),
work_tags: vec!["旅行".to_string(), "照片".to_string(), "纪念".to_string()],
levels: vec![CreativePuzzleLevelDraftInput {
level_name: "第一站".to_string(),
picture_description: "海边合影".to_string(),
picture_reference: None,
}],
})
.expect("draft should build");
let error = apply_puzzle_draft_field_patch(
draft,
PuzzleDraftFieldPatch {
field_path: PuzzleCreativeDraftEditableFieldPath::WorkTitle,
operation: PuzzleDraftFieldPatchOperation::Remove,
level_id: None,
value: Value::Null,
rationale: "测试".to_string(),
},
)
.expect_err("remove should be rejected");
assert_eq!(error, PuzzleFieldError::InvalidOperation);
}
}

View File

@@ -129,6 +129,8 @@ pub struct PuzzleDraftLevel {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
#[serde(default)]
pub picture_reference: Option<String>,
pub candidates: Vec<PuzzleGeneratedImageCandidate>,
pub selected_candidate_id: Option<String>,
pub cover_image_src: Option<String>,

View File

@@ -1,11 +1,15 @@
mod application;
mod commands;
mod creative_templates;
mod creative_tools;
mod domain;
mod errors;
mod events;
pub use application::*;
pub use commands::*;
pub use creative_templates::*;
pub use creative_tools::*;
pub use domain::*;
pub use errors::*;
pub use events::*;

View File

@@ -3,7 +3,12 @@ use module_runtime::{
aggregate_runtime_tracking_daily_stats,
};
fn stat(event_key: &str, scope_id: &str, day_key: i64, count: u32) -> RuntimeAnalyticsDailyStatSnapshot {
fn stat(
event_key: &str,
scope_id: &str,
day_key: i64,
count: u32,
) -> RuntimeAnalyticsDailyStatSnapshot {
RuntimeAnalyticsDailyStatSnapshot {
event_key: event_key.to_string(),
scope_kind: RuntimeTrackingScopeKind::User,
@@ -55,7 +60,17 @@ fn aggregates_daily_stats_by_month_quarter_and_year_bucket() {
"user-1",
AnalyticsGranularity::Month,
);
assert_eq!(month.iter().map(|bucket| (&bucket.bucket_key, bucket.value)).collect::<Vec<_>>(), vec![(&"202604".to_string(), 5), (&"202605".to_string(), 5), (&"202612".to_string(), 7)]);
assert_eq!(
month
.iter()
.map(|bucket| (&bucket.bucket_key, bucket.value))
.collect::<Vec<_>>(),
vec![
(&"202604".to_string(), 5),
(&"202605".to_string(), 5),
(&"202612".to_string(), 7)
]
);
assert_eq!(month[0].bucket_start_date_key, 20_544);
assert_eq!(month[0].bucket_end_date_key, 20_573);
@@ -66,7 +81,13 @@ fn aggregates_daily_stats_by_month_quarter_and_year_bucket() {
"user-1",
AnalyticsGranularity::Quarter,
);
assert_eq!(quarter.iter().map(|bucket| (&bucket.bucket_key, bucket.value)).collect::<Vec<_>>(), vec![(&"20262".to_string(), 10), (&"20264".to_string(), 7)]);
assert_eq!(
quarter
.iter()
.map(|bucket| (&bucket.bucket_key, bucket.value))
.collect::<Vec<_>>(),
vec![(&"20262".to_string(), 10), (&"20264".to_string(), 7)]
);
let year = aggregate_runtime_tracking_daily_stats(
stats,

View File

@@ -0,0 +1,15 @@
[package]
name = "module-visual-novel"
edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = []
spacetime-types = ["dep:spacetimedb"]
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
shared-kernel = { workspace = true }
spacetimedb = { workspace = true, optional = true }

View File

@@ -0,0 +1,935 @@
use std::collections::{BTreeMap, BTreeSet};
use serde::Deserialize;
use shared_kernel::{normalize_required_string, normalize_string_list};
use crate::{
VISUAL_NOVEL_MAX_INITIAL_CHOICE_COUNT, VISUAL_NOVEL_MIN_INITIAL_CHOICE_COUNT,
VisualNovelCharacterRole, VisualNovelChoiceDraft, VisualNovelDomainError, VisualNovelFlagValue,
VisualNovelHistoryEntry, VisualNovelHistorySource, VisualNovelResultDraft,
VisualNovelRunSnapshot, VisualNovelRunStatus, VisualNovelRuntimeAction,
VisualNovelRuntimeActionKind, VisualNovelRuntimeStep, VisualNovelSaveArchiveState,
VisualNovelSceneAvailability, VisualNovelTransitionKind, VisualNovelValidationIssue,
VisualNovelValidationSeverity, VisualNovelWorkProfile,
};
pub fn validate_visual_novel_draft(
draft: &VisualNovelResultDraft,
) -> Vec<VisualNovelValidationIssue> {
let mut issues = Vec::new();
push_missing(
&mut issues,
"workTitle",
&draft.work_title,
"作品标题不能为空",
);
push_missing(
&mut issues,
"workDescription",
&draft.work_description,
"作品简介不能为空",
);
push_missing(
&mut issues,
"world.summary",
&draft.world.summary,
"世界观摘要不能为空",
);
push_missing(
&mut issues,
"world.playerRole",
&draft.world.player_role,
"玩家身份不能为空",
);
if !draft.characters.iter().any(|character| {
matches!(
character.role,
VisualNovelCharacterRole::Main | VisualNovelCharacterRole::Supporting
) && !character.is_player_visible
&& normalize_required_string(&character.name).is_some()
}) {
push_issue(
&mut issues,
"characters",
"MISSING_NON_PLAYER_MAIN_CHARACTER",
"至少需要 1 个非玩家主要角色",
);
}
for character in &draft.characters {
if normalize_required_string(&character.character_id).is_none() {
push_issue(
&mut issues,
"characters[].characterId",
"MISSING_CHARACTER_ID",
"角色 ID 不能为空",
);
}
if normalize_required_string(&character.name).is_none() {
push_issue(
&mut issues,
"characters[].name",
"MISSING_CHARACTER_NAME",
"角色名称不能为空",
);
}
for asset in &character.image_assets {
if normalize_required_string(&asset.asset_id).is_none()
|| normalize_required_string(&asset.image_src).is_none()
{
push_issue(
&mut issues,
"characters[].imageAssets",
"INVALID_CHARACTER_IMAGE_ASSET",
"角色立绘必须使用有效的平台资产引用或受控图片 URL",
);
}
}
}
let scene_ids = draft
.scenes
.iter()
.map(|scene| scene.scene_id.as_str())
.collect::<BTreeSet<_>>();
let phase_ids = draft
.story_phases
.iter()
.map(|phase| phase.phase_id.as_str())
.collect::<BTreeSet<_>>();
if !draft
.scenes
.iter()
.any(|scene| scene.availability == VisualNovelSceneAvailability::Opening)
{
push_issue(
&mut issues,
"scenes",
"MISSING_OPENING_SCENE",
"至少需要 1 个 opening 场景",
);
}
for scene in &draft.scenes {
if normalize_required_string(&scene.scene_id).is_none() {
push_issue(
&mut issues,
"scenes[].sceneId",
"MISSING_SCENE_ID",
"场景 ID 不能为空",
);
}
if normalize_required_string(&scene.name).is_none() {
push_issue(
&mut issues,
"scenes[].name",
"MISSING_SCENE_NAME",
"场景名称不能为空",
);
}
if scene.availability == VisualNovelSceneAvailability::PhaseLocked
&& scene.phase_ids.is_empty()
{
push_issue(
&mut issues,
"scenes[].phaseIds",
"MISSING_PHASE_LOCKED_SCENE_PHASE",
"阶段锁定场景必须绑定剧情阶段",
);
}
for phase_id in &scene.phase_ids {
if !phase_ids.contains(phase_id.as_str()) {
push_issue(
&mut issues,
"scenes[].phaseIds",
"UNKNOWN_SCENE_PHASE_ID",
"场景绑定了不存在的剧情阶段",
);
}
}
}
if draft.story_phases.is_empty() {
push_issue(
&mut issues,
"storyPhases",
"MISSING_STORY_PHASE",
"至少需要 1 个剧情阶段",
);
}
for phase in &draft.story_phases {
if normalize_required_string(&phase.phase_id).is_none() {
push_issue(
&mut issues,
"storyPhases[].phaseId",
"MISSING_PHASE_ID",
"剧情阶段 ID 不能为空",
);
}
if normalize_required_string(&phase.title).is_none() {
push_issue(
&mut issues,
"storyPhases[].title",
"MISSING_PHASE_TITLE",
"剧情阶段标题不能为空",
);
}
if phase.scene_ids.is_empty() && phase.character_ids.is_empty() {
push_issue(
&mut issues,
"storyPhases[]",
"PHASE_WITHOUT_SCENE_OR_CHARACTER",
"每个剧情阶段至少绑定一个场景或角色",
);
}
for scene_id in &phase.scene_ids {
if !scene_ids.contains(scene_id.as_str()) {
push_issue(
&mut issues,
"storyPhases[].sceneIds",
"UNKNOWN_PHASE_SCENE_ID",
"剧情阶段绑定了不存在的场景",
);
}
}
}
match draft.opening.scene_id.as_ref() {
Some(scene_id) if scene_ids.contains(scene_id.as_str()) => {}
_ => push_issue(
&mut issues,
"opening.sceneId",
"INVALID_OPENING_SCENE",
"开场场景必须指向有效场景",
),
}
push_missing(
&mut issues,
"opening.narration",
&draft.opening.narration,
"开场旁白不能为空",
);
let choice_count = draft.opening.initial_choices.len();
if !(VISUAL_NOVEL_MIN_INITIAL_CHOICE_COUNT..=VISUAL_NOVEL_MAX_INITIAL_CHOICE_COUNT)
.contains(&choice_count)
{
push_issue(
&mut issues,
"opening.initialChoices",
"INVALID_INITIAL_CHOICE_COUNT",
"初始选项必须为 2 到 4 个",
);
}
for choice in &draft.opening.initial_choices {
if normalize_required_string(&choice.choice_id).is_none()
|| normalize_required_string(&choice.text).is_none()
{
push_issue(
&mut issues,
"opening.initialChoices[]",
"INVALID_INITIAL_CHOICE",
"初始选项 ID 和文本不能为空",
);
}
}
issues
}
pub fn compile_visual_novel_profile(
draft: &VisualNovelResultDraft,
) -> Result<VisualNovelWorkProfile, VisualNovelDomainError> {
let profile_id = normalize_required_string(draft.profile_id.as_deref().unwrap_or(""))
.ok_or(VisualNovelDomainError::MissingProfileId)?;
let mut normalized_draft = draft.clone();
normalized_draft.work_tags = normalize_string_list(normalized_draft.work_tags);
normalized_draft.validation_issues = validate_visual_novel_draft(&normalized_draft);
normalized_draft.publish_ready = normalized_draft.validation_issues.is_empty();
Ok(VisualNovelWorkProfile {
profile_id,
work_title: normalized_draft.work_title.clone(),
work_description: normalized_draft.work_description.clone(),
work_tags: normalized_draft.work_tags.clone(),
cover_image_src: normalized_draft.cover_image_src.clone(),
source_mode: normalized_draft.source_mode,
draft: normalized_draft,
})
}
pub fn parse_runtime_steps(
model_output: &str,
) -> Result<Vec<VisualNovelRuntimeStep>, VisualNovelDomainError> {
let text =
normalize_required_string(model_output).ok_or(VisualNovelDomainError::InvalidJson)?;
let steps = serde_json::from_str::<Vec<RuntimeStepInput>>(&text)
.or_else(|_| serde_json::from_str::<RuntimeStepsEnvelope>(&text).map(|value| value.steps))
.map_err(|_| VisualNovelDomainError::InvalidJson)?;
let parsed = steps
.into_iter()
.map(RuntimeStepInput::try_into)
.collect::<Result<Vec<_>, _>>()?;
if parsed.is_empty() {
return Err(VisualNovelDomainError::EmptyRuntimeSteps);
}
Ok(parsed)
}
pub fn apply_runtime_steps(
snapshot: &VisualNovelRunSnapshot,
steps: &[VisualNovelRuntimeStep],
history_entry_id: &str,
created_at: &str,
) -> Result<VisualNovelRunSnapshot, VisualNovelDomainError> {
if steps.is_empty() {
return Err(VisualNovelDomainError::EmptyRuntimeSteps);
}
let history_entry_id = normalize_required_string(history_entry_id)
.ok_or(VisualNovelDomainError::InvalidRuntimeStep)?;
let created_at =
normalize_required_string(created_at).ok_or(VisualNovelDomainError::InvalidRuntimeStep)?;
let mut next = snapshot.clone();
let max_history_entries = snapshot.history.len().saturating_add(1);
for step in steps {
apply_step_to_snapshot(&mut next, step)?;
}
let turn_index = next
.history
.last()
.map(|entry| entry.turn_index.saturating_add(1))
.unwrap_or(0);
next.history.push(VisualNovelHistoryEntry {
entry_id: history_entry_id,
run_id: next.run_id.clone(),
turn_index,
source: VisualNovelHistorySource::Assistant,
action_text: None,
steps: steps.to_vec(),
snapshot_before_hash: snapshot.history.last().and_then(|entry| {
entry
.snapshot_after_hash
.clone()
.or_else(|| Some(format!("turn-{}", entry.turn_index)))
}),
snapshot_after_hash: Some(format!("turn-{turn_index}")),
created_at: created_at.clone(),
});
if next.history.len() > max_history_entries {
next.history.remove(0);
}
next.updated_at = created_at;
Ok(next)
}
pub fn build_runtime_prompt_context(
profile: &VisualNovelWorkProfile,
snapshot: &VisualNovelRunSnapshot,
action: &VisualNovelRuntimeAction,
) -> Result<String, VisualNovelDomainError> {
validate_runtime_action(snapshot, action, &profile.draft.runtime_config)?;
let action_text = resolve_action_text(snapshot, action)?;
let scene_name = snapshot
.current_scene_id
.as_ref()
.and_then(|scene_id| {
profile
.draft
.scenes
.iter()
.find(|scene| scene.scene_id == *scene_id)
})
.map(|scene| scene.name.as_str())
.unwrap_or("未指定场景");
Ok(format!(
"作品:{}\n世界观:{}\n玩家身份:{}\n当前场景:{}\n玩家行动:{}",
profile.work_title,
profile.draft.world.summary,
profile.draft.world.player_role,
scene_name,
action_text,
))
}
pub fn regenerate_from_history(
snapshot: &VisualNovelRunSnapshot,
history_entry_id: &str,
allow_history_regeneration: bool,
updated_at: &str,
) -> Result<VisualNovelRunSnapshot, VisualNovelDomainError> {
if !allow_history_regeneration {
return Err(VisualNovelDomainError::HistoryRegenerationDisabled);
}
let history_entry_id = normalize_required_string(history_entry_id)
.ok_or(VisualNovelDomainError::HistoryEntryNotFound)?;
let target_index = snapshot
.history
.iter()
.position(|entry| entry.entry_id == history_entry_id)
.ok_or(VisualNovelDomainError::HistoryEntryNotFound)?;
if snapshot.history[target_index].source != VisualNovelHistorySource::Assistant {
return Err(VisualNovelDomainError::InvalidHistorySource);
}
let mut next = snapshot.clone();
next.history.truncate(target_index);
next.current_scene_id = None;
next.visible_character_ids.clear();
next.available_choices.clear();
next.flags.clear();
next.metrics.clear();
let steps_to_restore = next
.history
.iter()
.flat_map(|entry| entry.steps.clone())
.collect::<Vec<_>>();
for step in steps_to_restore {
apply_step_to_snapshot(&mut next, &step)?;
}
next.updated_at = updated_at.to_string();
Ok(next)
}
pub fn build_save_archive_state(snapshot: &VisualNovelRunSnapshot) -> VisualNovelSaveArchiveState {
VisualNovelSaveArchiveState {
runtime_kind: "visual-novel".to_string(),
profile_id: snapshot.profile_id.clone(),
run_id: snapshot.run_id.clone(),
current_scene_id: snapshot.current_scene_id.clone(),
current_phase_id: snapshot.current_phase_id.clone(),
history_cursor: snapshot
.history
.last()
.map(|entry| entry.turn_index)
.unwrap_or(0),
snapshot_hash: snapshot
.history
.last()
.and_then(|entry| entry.snapshot_after_hash.clone()),
}
}
pub fn validate_runtime_action(
snapshot: &VisualNovelRunSnapshot,
action: &VisualNovelRuntimeAction,
config: &crate::VisualNovelRuntimeConfigDraft,
) -> Result<(), VisualNovelDomainError> {
if normalize_required_string(&action.client_event_id).is_none() {
return Err(VisualNovelDomainError::MissingClientEventId);
}
match action.action_kind {
VisualNovelRuntimeActionKind::Choice => {
let choice_id = action
.choice_id
.as_ref()
.and_then(normalize_required_string)
.ok_or(VisualNovelDomainError::InvalidChoiceId)?;
if snapshot
.available_choices
.iter()
.any(|choice| choice.choice_id == choice_id)
{
Ok(())
} else {
Err(VisualNovelDomainError::InvalidChoiceId)
}
}
VisualNovelRuntimeActionKind::FreeText => {
if !config.allow_free_text_action {
return Err(VisualNovelDomainError::FreeTextDisabled);
}
action
.text
.as_ref()
.and_then(normalize_required_string)
.map(|_| ())
.ok_or(VisualNovelDomainError::MissingActionText)
}
VisualNovelRuntimeActionKind::Continue => Ok(()),
}
}
fn apply_step_to_snapshot(
snapshot: &mut VisualNovelRunSnapshot,
step: &VisualNovelRuntimeStep,
) -> Result<(), VisualNovelDomainError> {
match step {
VisualNovelRuntimeStep::SceneChange { scene_id, .. } => {
let scene_id = normalize_required_string(scene_id)
.ok_or(VisualNovelDomainError::InvalidRuntimeStep)?;
snapshot.current_scene_id = Some(scene_id);
snapshot.visible_character_ids.clear();
}
VisualNovelRuntimeStep::Narration { text } => {
if normalize_required_string(text).is_none() {
return Err(VisualNovelDomainError::InvalidRuntimeStep);
}
}
VisualNovelRuntimeStep::Dialogue {
character_id,
character_name,
text,
..
} => {
let character_id = normalize_required_string(character_id)
.ok_or(VisualNovelDomainError::InvalidRuntimeStep)?;
if normalize_required_string(character_name).is_none()
|| normalize_required_string(text).is_none()
{
return Err(VisualNovelDomainError::InvalidRuntimeStep);
}
if !snapshot.visible_character_ids.contains(&character_id) {
snapshot.visible_character_ids.push(character_id);
}
}
VisualNovelRuntimeStep::Transition { .. } => {}
VisualNovelRuntimeStep::Choice { choices } => {
if choices.is_empty() {
return Err(VisualNovelDomainError::InvalidRuntimeStep);
}
for choice in choices {
if normalize_required_string(&choice.choice_id).is_none()
|| normalize_required_string(&choice.text).is_none()
{
return Err(VisualNovelDomainError::InvalidRuntimeStep);
}
}
snapshot.available_choices = choices.clone();
}
VisualNovelRuntimeStep::Flag { key, value } => {
let key =
normalize_required_string(key).ok_or(VisualNovelDomainError::InvalidRuntimeStep)?;
snapshot.flags.insert(key, value.clone());
}
VisualNovelRuntimeStep::Metric { key, delta } => {
let key =
normalize_required_string(key).ok_or(VisualNovelDomainError::InvalidRuntimeStep)?;
let entry = snapshot.metrics.entry(key).or_insert(0.0);
*entry += *delta;
}
}
Ok(())
}
fn resolve_action_text(
snapshot: &VisualNovelRunSnapshot,
action: &VisualNovelRuntimeAction,
) -> Result<String, VisualNovelDomainError> {
match action.action_kind {
VisualNovelRuntimeActionKind::Choice => {
let choice_id = action
.choice_id
.as_ref()
.and_then(normalize_required_string)
.ok_or(VisualNovelDomainError::InvalidChoiceId)?;
snapshot
.available_choices
.iter()
.find(|choice| choice.choice_id == choice_id)
.map(|choice| choice.text.clone())
.ok_or(VisualNovelDomainError::InvalidChoiceId)
}
VisualNovelRuntimeActionKind::FreeText => action
.text
.as_ref()
.and_then(normalize_required_string)
.ok_or(VisualNovelDomainError::MissingActionText),
VisualNovelRuntimeActionKind::Continue => Ok("继续".to_string()),
}
}
fn push_missing(
issues: &mut Vec<VisualNovelValidationIssue>,
path: &str,
value: &str,
message: &str,
) {
if normalize_required_string(value).is_none() {
let code = format!("MISSING_{}", path_to_code(path));
push_issue(issues, path, &code, message);
}
}
fn push_issue(issues: &mut Vec<VisualNovelValidationIssue>, path: &str, code: &str, message: &str) {
issues.push(VisualNovelValidationIssue {
issue_id: format!("vn-issue-{}", issues.len() + 1),
code: code.to_string(),
severity: VisualNovelValidationSeverity::Error,
path: path.to_string(),
message: message.to_string(),
});
}
fn path_to_code(path: &str) -> String {
path.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() {
ch.to_ascii_uppercase()
} else {
'_'
}
})
.collect()
}
#[derive(Debug, Deserialize)]
struct RuntimeStepsEnvelope {
steps: Vec<RuntimeStepInput>,
}
#[derive(Debug, Deserialize)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
enum RuntimeStepInput {
SceneChange {
scene_id: String,
background_image_src: Option<String>,
music_src: Option<String>,
},
Narration {
text: String,
},
Dialogue {
character_id: String,
character_name: String,
expression: Option<String>,
text: String,
},
Transition {
transition_kind: VisualNovelTransitionKind,
text: Option<String>,
},
Choice {
choices: Vec<VisualNovelChoiceDraft>,
},
Flag {
key: String,
value: VisualNovelFlagValue,
},
Metric {
key: String,
delta: f64,
},
}
impl TryFrom<RuntimeStepInput> for VisualNovelRuntimeStep {
type Error = VisualNovelDomainError;
fn try_from(value: RuntimeStepInput) -> Result<Self, Self::Error> {
let step = match value {
RuntimeStepInput::SceneChange {
scene_id,
background_image_src,
music_src,
} => VisualNovelRuntimeStep::SceneChange {
scene_id,
background_image_src,
music_src,
},
RuntimeStepInput::Narration { text } => VisualNovelRuntimeStep::Narration { text },
RuntimeStepInput::Dialogue {
character_id,
character_name,
expression,
text,
} => VisualNovelRuntimeStep::Dialogue {
character_id,
character_name,
expression,
text,
},
RuntimeStepInput::Transition {
transition_kind,
text,
} => VisualNovelRuntimeStep::Transition {
transition_kind,
text,
},
RuntimeStepInput::Choice { choices } => VisualNovelRuntimeStep::Choice { choices },
RuntimeStepInput::Flag { key, value } => VisualNovelRuntimeStep::Flag { key, value },
RuntimeStepInput::Metric { key, delta } => {
VisualNovelRuntimeStep::Metric { key, delta }
}
};
let mut probe = empty_run_for_validation();
apply_step_to_snapshot(&mut probe, &step)?;
Ok(step)
}
}
fn empty_run_for_validation() -> VisualNovelRunSnapshot {
VisualNovelRunSnapshot {
run_id: "vn-run-validation".to_string(),
owner_user_id: "user-validation".to_string(),
profile_id: "vn-profile-validation".to_string(),
mode: crate::VisualNovelRunMode::Test,
status: VisualNovelRunStatus::Active,
current_scene_id: None,
current_phase_id: None,
visible_character_ids: Vec::new(),
flags: BTreeMap::new(),
metrics: BTreeMap::new(),
history: Vec::new(),
available_choices: Vec::new(),
text_mode_enabled: true,
created_at: "2026-05-05T00:00:00Z".to_string(),
updated_at: "2026-05-05T00:00:00Z".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
VisualNovelAttributePanelMode, VisualNovelCharacterDraft, VisualNovelOpeningDraft,
VisualNovelRunMode, VisualNovelRuntimeConfigDraft, VisualNovelSceneDraft,
VisualNovelSourceMode, VisualNovelStoryPhaseDraft, VisualNovelWorldDraft,
};
fn valid_draft() -> VisualNovelResultDraft {
VisualNovelResultDraft {
profile_id: Some("vn-profile-1".to_string()),
work_title: "雨夜书店".to_string(),
work_description: "在午夜书店里找回名字的视觉小说。".to_string(),
work_tags: vec!["悬疑".to_string(), "都市奇谈".to_string()],
cover_image_src: None,
source_mode: VisualNovelSourceMode::Idea,
source_asset_ids: Vec::new(),
world: VisualNovelWorldDraft {
title: "雨夜书店".to_string(),
summary: "主角在雨夜进入一间只在午夜出现的书店。".to_string(),
background: "旧街区里隐藏着交换记忆的书店。".to_string(),
premise: "找回遗失的名字。".to_string(),
literary_style: "细腻、轻悬疑".to_string(),
player_role: "误入书店的读者".to_string(),
default_tone: "克制而温柔".to_string(),
},
characters: vec![VisualNovelCharacterDraft {
character_id: "char-clerk".to_string(),
name: "店员".to_string(),
gender: None,
role: VisualNovelCharacterRole::Main,
appearance: "银灰色长发,黑色围裙。".to_string(),
personality: "温和但回避关键问题。".to_string(),
tone: "轻声慢语。".to_string(),
background: "守着午夜书店的人。".to_string(),
relationship_to_player: "向玩家递出第一本书。".to_string(),
image_assets: Vec::new(),
default_expression: None,
is_player_visible: false,
}],
scenes: vec![VisualNovelSceneDraft {
scene_id: "scene-bookstore".to_string(),
name: "午夜书店".to_string(),
description: "暖灯、旧木地板和窗外雨声。".to_string(),
background_image_src: None,
music_src: None,
ambient_sound_src: None,
availability: VisualNovelSceneAvailability::Opening,
phase_ids: vec!["phase-opening".to_string()],
}],
story_phases: vec![VisualNovelStoryPhaseDraft {
phase_id: "phase-opening".to_string(),
title: "进入书店".to_string(),
goal: "理解书店规则。".to_string(),
summary: "玩家第一次见到店员。".to_string(),
entry_condition: "开场".to_string(),
exit_condition: "选择第一本书".to_string(),
scene_ids: vec!["scene-bookstore".to_string()],
character_ids: vec!["char-clerk".to_string()],
suggested_choices: vec!["询问店员".to_string()],
}],
opening: VisualNovelOpeningDraft {
scene_id: Some("scene-bookstore".to_string()),
narration: "门铃响起,雨声被关在门外。".to_string(),
speaker_character_id: Some("char-clerk".to_string()),
first_dialogue: Some("欢迎回来。".to_string()),
initial_choices: vec![
VisualNovelChoiceDraft {
choice_id: "choice-ask".to_string(),
text: "询问店员为什么说回来".to_string(),
action_hint: None,
},
VisualNovelChoiceDraft {
choice_id: "choice-look".to_string(),
text: "环顾书店".to_string(),
action_hint: None,
},
],
},
runtime_config: VisualNovelRuntimeConfigDraft {
text_mode_enabled: true,
default_text_mode: false,
max_history_entries: 80,
max_assistant_step_count_per_turn: 8,
allow_free_text_action: true,
allow_history_regeneration: true,
attribute_panel_mode: VisualNovelAttributePanelMode::Off,
save_archive_enabled: true,
},
publish_ready: false,
validation_issues: Vec::new(),
updated_at: "2026-05-05T00:00:00Z".to_string(),
}
}
fn empty_run() -> VisualNovelRunSnapshot {
VisualNovelRunSnapshot {
run_id: "vn-run-1".to_string(),
owner_user_id: "user-1".to_string(),
profile_id: "vn-profile-1".to_string(),
mode: VisualNovelRunMode::Test,
status: VisualNovelRunStatus::Active,
current_scene_id: None,
current_phase_id: None,
visible_character_ids: Vec::new(),
flags: BTreeMap::new(),
metrics: BTreeMap::new(),
history: Vec::new(),
available_choices: Vec::new(),
text_mode_enabled: true,
created_at: "2026-05-05T00:00:00Z".to_string(),
updated_at: "2026-05-05T00:00:00Z".to_string(),
}
}
#[test]
fn publish_validation_catches_missing_opening_requirements() {
let mut draft = valid_draft();
draft.opening.scene_id = Some("missing-scene".to_string());
draft.opening.initial_choices = vec![VisualNovelChoiceDraft {
choice_id: "only-one".to_string(),
text: "只有一个选项".to_string(),
action_hint: None,
}];
let issues = validate_visual_novel_draft(&draft);
assert!(
issues
.iter()
.any(|issue| issue.code == "INVALID_OPENING_SCENE")
);
assert!(
issues
.iter()
.any(|issue| issue.code == "INVALID_INITIAL_CHOICE_COUNT")
);
}
#[test]
fn valid_draft_compiles_to_profile() {
let profile = compile_visual_novel_profile(&valid_draft()).expect("profile");
assert_eq!(profile.profile_id, "vn-profile-1");
assert!(profile.draft.publish_ready);
assert!(profile.draft.validation_issues.is_empty());
}
#[test]
fn step_parser_rejects_empty_dialogue_text() {
let error = parse_runtime_steps(
r#"[{"type":"dialogue","characterId":"char-a","characterName":"A","text":" "}]"#,
)
.expect_err("blank dialogue text should fail");
assert_eq!(error, VisualNovelDomainError::InvalidRuntimeStep);
}
#[test]
fn apply_runtime_steps_advances_scene_character_choices_and_metrics() {
let steps = parse_runtime_steps(
r#"{
"steps": [
{"type":"scene_change","sceneId":"scene-bookstore","backgroundImageSrc":null,"musicSrc":null},
{"type":"dialogue","characterId":"char-clerk","characterName":"店员","expression":null,"text":"欢迎回来。"},
{"type":"choice","choices":[{"choiceId":"choice-ask","text":"询问","actionHint":null}]},
{"type":"flag","key":"metClerk","value":true},
{"type":"metric","key":"curiosity","delta":2}
]
}"#,
)
.expect("steps");
let next =
apply_runtime_steps(&empty_run(), &steps, "vn-history-1", "2026-05-05T00:00:01Z")
.expect("run");
assert_eq!(next.current_scene_id.as_deref(), Some("scene-bookstore"));
assert_eq!(next.visible_character_ids, vec!["char-clerk".to_string()]);
assert_eq!(next.available_choices[0].choice_id, "choice-ask");
assert_eq!(
next.flags.get("metClerk"),
Some(&VisualNovelFlagValue::Bool(true))
);
assert_eq!(next.metrics.get("curiosity"), Some(&2.0));
assert_eq!(next.history.len(), 1);
}
#[test]
fn regeneration_truncates_after_assistant_history_node() {
let first = apply_runtime_steps(
&empty_run(),
&[VisualNovelRuntimeStep::SceneChange {
scene_id: "scene-a".to_string(),
background_image_src: None,
music_src: None,
}],
"vn-history-1",
"2026-05-05T00:00:01Z",
)
.expect("first");
let second = apply_runtime_steps(
&first,
&[VisualNovelRuntimeStep::SceneChange {
scene_id: "scene-b".to_string(),
background_image_src: None,
music_src: None,
}],
"vn-history-2",
"2026-05-05T00:00:02Z",
)
.expect("second");
let regenerated =
regenerate_from_history(&second, "vn-history-2", true, "2026-05-05T00:00:03Z")
.expect("regenerated");
assert_eq!(regenerated.history.len(), 1);
assert_eq!(regenerated.current_scene_id.as_deref(), Some("scene-a"));
}
#[test]
fn save_archive_state_uses_platform_runtime_kind() {
let run = apply_runtime_steps(
&empty_run(),
&[VisualNovelRuntimeStep::SceneChange {
scene_id: "scene-bookstore".to_string(),
background_image_src: None,
music_src: None,
}],
"vn-history-1",
"2026-05-05T00:00:01Z",
)
.expect("run");
let archive_state = build_save_archive_state(&run);
assert_eq!(archive_state.runtime_kind, "visual-novel");
assert_eq!(archive_state.profile_id, "vn-profile-1");
assert_eq!(archive_state.run_id, "vn-run-1");
assert_eq!(
archive_state.current_scene_id.as_deref(),
Some("scene-bookstore")
);
assert_eq!(archive_state.history_cursor, 0);
assert_eq!(archive_state.snapshot_hash.as_deref(), Some("turn-0"));
}
}

View File

@@ -0,0 +1,390 @@
//! 视觉小说模板的纯领域模型。
//!
//! 本 crate 只负责草稿校验、运行时 step 解析、状态推进、历史重生成边界
//! 和平台统一存档状态构造HTTP、SpacetimeDB、LLM、OSS 均由外层 adapter 处理。
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
use std::collections::BTreeMap;
pub const VISUAL_NOVEL_PROFILE_ID_PREFIX: &str = "vn-profile-";
pub const VISUAL_NOVEL_RUN_ID_PREFIX: &str = "vn-run-";
pub const VISUAL_NOVEL_HISTORY_ID_PREFIX: &str = "vn-history-";
pub const VISUAL_NOVEL_DEFAULT_MAX_HISTORY_ENTRIES: u32 = 80;
pub const VISUAL_NOVEL_DEFAULT_MAX_ASSISTANT_STEP_COUNT_PER_TURN: u32 = 8;
pub const VISUAL_NOVEL_MIN_INITIAL_CHOICE_COUNT: usize = 2;
pub const VISUAL_NOVEL_MAX_INITIAL_CHOICE_COUNT: usize = 4;
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelSourceMode {
Idea,
Document,
Blank,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelCharacterRole {
Protagonist,
Main,
Supporting,
Antagonist,
Background,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAssetSource {
PlatformAsset,
Generated,
External,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelSceneAvailability {
Opening,
Always,
PhaseLocked,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAttributePanelMode {
Off,
PlatformWhitelist,
TemplateConfig,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelValidationSeverity {
Error,
Warning,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelRunMode {
Test,
Play,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelRunStatus {
Active,
Completed,
Failed,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelRuntimeActionKind {
Choice,
FreeText,
Continue,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelTransitionKind {
Fade,
Cut,
Flash,
None,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelHistorySource {
Player,
Assistant,
System,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VisualNovelFlagValue {
String(String),
Number(f64),
Bool(bool),
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelValidationIssue {
pub issue_id: String,
pub code: String,
pub severity: VisualNovelValidationSeverity,
pub path: String,
pub message: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelChoiceDraft {
pub choice_id: String,
pub text: String,
pub action_hint: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCharacterImageAsset {
pub asset_id: String,
pub image_src: String,
pub expression: Option<String>,
pub source: VisualNovelAssetSource,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorldDraft {
pub title: String,
pub summary: String,
pub background: String,
pub premise: String,
pub literary_style: String,
pub player_role: String,
pub default_tone: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCharacterDraft {
pub character_id: String,
pub name: String,
pub gender: Option<String>,
pub role: VisualNovelCharacterRole,
pub appearance: String,
pub personality: String,
pub tone: String,
pub background: String,
pub relationship_to_player: String,
pub image_assets: Vec<VisualNovelCharacterImageAsset>,
pub default_expression: Option<String>,
pub is_player_visible: bool,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSceneDraft {
pub scene_id: String,
pub name: String,
pub description: String,
pub background_image_src: Option<String>,
pub music_src: Option<String>,
pub ambient_sound_src: Option<String>,
pub availability: VisualNovelSceneAvailability,
pub phase_ids: Vec<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelStoryPhaseDraft {
pub phase_id: String,
pub title: String,
pub goal: String,
pub summary: String,
pub entry_condition: String,
pub exit_condition: String,
pub scene_ids: Vec<String>,
pub character_ids: Vec<String>,
pub suggested_choices: Vec<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelOpeningDraft {
pub scene_id: Option<String>,
pub narration: String,
pub speaker_character_id: Option<String>,
pub first_dialogue: Option<String>,
pub initial_choices: Vec<VisualNovelChoiceDraft>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRuntimeConfigDraft {
pub text_mode_enabled: bool,
pub default_text_mode: bool,
pub max_history_entries: u32,
pub max_assistant_step_count_per_turn: u32,
pub allow_free_text_action: bool,
pub allow_history_regeneration: bool,
pub attribute_panel_mode: VisualNovelAttributePanelMode,
pub save_archive_enabled: bool,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelResultDraft {
pub profile_id: Option<String>,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub source_mode: VisualNovelSourceMode,
pub source_asset_ids: Vec<String>,
pub world: VisualNovelWorldDraft,
pub characters: Vec<VisualNovelCharacterDraft>,
pub scenes: Vec<VisualNovelSceneDraft>,
pub story_phases: Vec<VisualNovelStoryPhaseDraft>,
pub opening: VisualNovelOpeningDraft,
pub runtime_config: VisualNovelRuntimeConfigDraft,
pub publish_ready: bool,
pub validation_issues: Vec<VisualNovelValidationIssue>,
pub updated_at: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkProfile {
pub profile_id: String,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
pub cover_image_src: Option<String>,
pub source_mode: VisualNovelSourceMode,
pub draft: VisualNovelResultDraft,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelRuntimeStep {
SceneChange {
scene_id: String,
background_image_src: Option<String>,
music_src: Option<String>,
},
Narration {
text: String,
},
Dialogue {
character_id: String,
character_name: String,
expression: Option<String>,
text: String,
},
Transition {
transition_kind: VisualNovelTransitionKind,
text: Option<String>,
},
Choice {
choices: Vec<VisualNovelChoiceDraft>,
},
Flag {
key: String,
value: VisualNovelFlagValue,
},
Metric {
key: String,
delta: f64,
},
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelHistoryEntry {
pub entry_id: String,
pub run_id: String,
pub turn_index: u32,
pub source: VisualNovelHistorySource,
pub action_text: Option<String>,
pub steps: Vec<VisualNovelRuntimeStep>,
pub snapshot_before_hash: Option<String>,
pub snapshot_after_hash: Option<String>,
pub created_at: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRunSnapshot {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: VisualNovelRunMode,
pub status: VisualNovelRunStatus,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub visible_character_ids: Vec<String>,
pub flags: BTreeMap<String, VisualNovelFlagValue>,
pub metrics: BTreeMap<String, f64>,
pub history: Vec<VisualNovelHistoryEntry>,
pub available_choices: Vec<VisualNovelChoiceDraft>,
pub text_mode_enabled: bool,
pub created_at: String,
pub updated_at: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRuntimeAction {
pub action_kind: VisualNovelRuntimeActionKind,
pub choice_id: Option<String>,
pub text: Option<String>,
pub client_event_id: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSaveArchiveState {
pub runtime_kind: String,
pub profile_id: String,
pub run_id: String,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub history_cursor: u32,
pub snapshot_hash: Option<String>,
}
impl Default for VisualNovelRuntimeConfigDraft {
fn default() -> Self {
Self {
text_mode_enabled: true,
default_text_mode: false,
max_history_entries: VISUAL_NOVEL_DEFAULT_MAX_HISTORY_ENTRIES,
max_assistant_step_count_per_turn:
VISUAL_NOVEL_DEFAULT_MAX_ASSISTANT_STEP_COUNT_PER_TURN,
allow_free_text_action: true,
allow_history_regeneration: true,
attribute_panel_mode: VisualNovelAttributePanelMode::Off,
save_archive_enabled: true,
}
}
}

View File

@@ -0,0 +1,41 @@
use std::{error::Error, fmt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VisualNovelDomainError {
MissingProfileId,
MissingRunId,
MissingOwnerUserId,
MissingClientEventId,
MissingActionText,
InvalidChoiceId,
FreeTextDisabled,
HistoryRegenerationDisabled,
HistoryEntryNotFound,
InvalidHistorySource,
InvalidRuntimeStep,
InvalidJson,
EmptyRuntimeSteps,
}
impl fmt::Display for VisualNovelDomainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::MissingProfileId => "visual novel profile_id 缺失",
Self::MissingRunId => "visual novel run_id 缺失",
Self::MissingOwnerUserId => "visual novel owner_user_id 缺失",
Self::MissingClientEventId => "visual novel client_event_id 缺失",
Self::MissingActionText => "visual novel 行动文本缺失",
Self::InvalidChoiceId => "visual novel choice_id 不合法",
Self::FreeTextDisabled => "visual novel 当前作品未开启自由行动",
Self::HistoryRegenerationDisabled => "visual novel 当前作品未开启历史重生成",
Self::HistoryEntryNotFound => "visual novel history entry 不存在",
Self::InvalidHistorySource => "visual novel 只能从助手历史节点重生成",
Self::InvalidRuntimeStep => "visual novel runtime step 不合法",
Self::InvalidJson => "visual novel JSON 解析失败",
Self::EmptyRuntimeSteps => "visual novel runtime step 不能为空",
};
write!(f, "{message}")
}
}
impl Error for VisualNovelDomainError {}

View File

@@ -0,0 +1,7 @@
mod application;
mod domain;
mod errors;
pub use application::*;
pub use domain::*;
pub use errors::*;

View File

@@ -0,0 +1,16 @@
[package]
name = "platform-agent"
edition.workspace = true
version.workspace = true
license.workspace = true
[dependencies]
async-trait = { workspace = true }
langchainrust = { workspace = true }
platform-llm = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["time"] }
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }

View File

@@ -0,0 +1,59 @@
use platform_llm::{
LlmClient, LlmMessage, LlmMessageContentPart, LlmMessageRole, LlmTextProtocol, LlmTextRequest,
LlmTextResponse,
};
use crate::error::PlatformAgentError;
pub const CREATIVE_AGENT_GPT5_MODEL: &str = "gpt-5";
#[derive(Clone)]
pub struct Gpt5ResponsesAgentClient {
llm_client: LlmClient,
}
impl Gpt5ResponsesAgentClient {
pub fn new(llm_client: LlmClient) -> Self {
Self { llm_client }
}
pub async fn request(
&self,
system_prompt: impl Into<String>,
user_text: impl Into<String>,
image_urls: Vec<String>,
) -> Result<LlmTextResponse, PlatformAgentError> {
let request = build_gpt5_multimodal_request(system_prompt, user_text, image_urls);
self.llm_client
.request_text(request)
.await
.map_err(Into::into)
}
}
pub fn build_gpt5_multimodal_request(
system_prompt: impl Into<String>,
user_text: impl Into<String>,
image_urls: Vec<String>,
) -> LlmTextRequest {
let mut user_parts = vec![LlmMessageContentPart::InputText {
text: user_text.into(),
}];
user_parts.extend(
image_urls
.into_iter()
.map(|image_url| LlmMessageContentPart::InputImage { image_url }),
);
LlmTextRequest {
model: Some(CREATIVE_AGENT_GPT5_MODEL.to_string()),
messages: vec![
LlmMessage::new(LlmMessageRole::System, system_prompt.into()),
LlmMessage::multimodal(LlmMessageRole::User, user_parts),
],
max_tokens: None,
request_timeout_ms: None,
enable_web_search: false,
protocol: LlmTextProtocol::Responses,
}
}

View File

@@ -0,0 +1,54 @@
use std::sync::Arc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CreativeAgentCallbackKind {
Stage,
ToolStarted,
ToolCompleted,
ModelRequestStarted,
ModelRequestCompleted,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreativeAgentCallbackEvent {
pub kind: CreativeAgentCallbackKind,
pub label: String,
pub detail: Option<String>,
}
type CreativeAgentCallbackFn = Arc<dyn Fn(CreativeAgentCallbackEvent) + Send + Sync>;
#[derive(Clone, Default)]
pub struct CreativeAgentCallbacks {
on_event: Option<CreativeAgentCallbackFn>,
}
impl CreativeAgentCallbacks {
pub fn new<F>(on_event: F) -> Self
where
F: Fn(CreativeAgentCallbackEvent) + Send + Sync + 'static,
{
Self {
on_event: Some(Arc::new(on_event)),
}
}
pub fn noop() -> Self {
Self::default()
}
pub fn emit(&self, event: CreativeAgentCallbackEvent) {
if let Some(on_event) = &self.on_event {
on_event(event);
}
}
pub fn stage(&self, label: impl Into<String>) {
self.emit(CreativeAgentCallbackEvent {
kind: CreativeAgentCallbackKind::Stage,
label: label.into(),
detail: None,
});
}
}

View File

@@ -0,0 +1,40 @@
use std::{error::Error, fmt};
#[derive(Debug, PartialEq, Eq)]
pub enum PlatformAgentError {
InvalidInput(String),
ToolNotFound(String),
ToolExecution(String),
ToolBudgetExceeded { max_tool_calls: usize },
Timeout { timeout_ms: u64 },
Llm(String),
LangChain(String),
OutputParse(String),
}
impl fmt::Display for PlatformAgentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidInput(message)
| Self::ToolExecution(message)
| Self::Llm(message)
| Self::LangChain(message)
| Self::OutputParse(message) => write!(f, "{message}"),
Self::ToolNotFound(name) => write!(f, "Agent 工具未注册:{name}"),
Self::ToolBudgetExceeded { max_tool_calls } => {
write!(f, "Agent 工具调用次数超过限制:{max_tool_calls}")
}
Self::Timeout { timeout_ms } => {
write!(f, "Agent 执行超时:{timeout_ms}ms")
}
}
}
}
impl Error for PlatformAgentError {}
impl From<platform_llm::LlmError> for PlatformAgentError {
fn from(error: platform_llm::LlmError) -> Self {
Self::Llm(error.to_string())
}
}

View File

@@ -0,0 +1,49 @@
use std::{future::Future, time::Duration};
use tokio::time::timeout;
use crate::error::PlatformAgentError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FunctionAgentLimits {
pub max_tool_calls: usize,
pub timeout_ms: u64,
}
impl Default for FunctionAgentLimits {
fn default() -> Self {
Self {
max_tool_calls: 8,
timeout_ms: 30_000,
}
}
}
impl FunctionAgentLimits {
pub fn validate(&self) -> Result<(), PlatformAgentError> {
if self.max_tool_calls == 0 {
return Err(PlatformAgentError::InvalidInput(
"Agent max_tool_calls 必须大于 0".to_string(),
));
}
if self.timeout_ms == 0 {
return Err(PlatformAgentError::InvalidInput(
"Agent timeout_ms 必须大于 0".to_string(),
));
}
Ok(())
}
pub async fn run_with_timeout<F, T>(&self, future: F) -> Result<T, PlatformAgentError>
where
F: Future<Output = Result<T, PlatformAgentError>>,
{
self.validate()?;
timeout(Duration::from_millis(self.timeout_ms), future)
.await
.map_err(|_| PlatformAgentError::Timeout {
timeout_ms: self.timeout_ms,
})?
}
}

View File

@@ -0,0 +1,177 @@
use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use langchainrust::{
AgentAction, AgentError, AgentExecutor, AgentFinish, AgentOutput, AgentStep, BaseAgent,
BaseTool, FunctionCallingAgent, OpenAIChat, OpenAIConfig, ToolError, ToolInput,
};
use serde_json::json;
use crate::{
apimart_gpt5_adapter::CREATIVE_AGENT_GPT5_MODEL, error::PlatformAgentError,
function_agent::FunctionAgentLimits,
};
pub struct LangChainRustAdapter {
limits: FunctionAgentLimits,
}
impl LangChainRustAdapter {
pub fn new(limits: FunctionAgentLimits) -> Result<Self, PlatformAgentError> {
limits.validate()?;
Ok(Self { limits })
}
pub fn limits(&self) -> &FunctionAgentLimits {
&self.limits
}
pub fn build_function_calling_agent(
&self,
api_key: impl Into<String>,
base_url: impl Into<String>,
tools: Vec<Arc<dyn BaseTool>>,
system_prompt: Option<String>,
) -> FunctionCallingAgent {
let config = OpenAIConfig::new(api_key)
.with_base_url(base_url)
.with_model(CREATIVE_AGENT_GPT5_MODEL);
let llm = OpenAIChat::new(config);
FunctionCallingAgent::new(llm, tools, system_prompt)
}
pub async fn execute_minimal_tool_call(
&self,
tool_name: impl Into<String>,
input: serde_json::Value,
) -> Result<String, PlatformAgentError> {
let tool_name = tool_name.into();
let tools: Vec<Arc<dyn BaseTool>> =
vec![Arc::new(EchoLangChainTool::new(tool_name.clone()))];
let agent = Arc::new(ScriptedLangChainToolAgent::new(tool_name, input));
let executor =
AgentExecutor::new(agent, tools).with_max_iterations(self.limits.max_tool_calls);
self.limits
.run_with_timeout(async move {
executor
.invoke("执行最小工具调用".to_string())
.await
.map_err(|error| PlatformAgentError::LangChain(error.to_string()))
})
.await
}
}
struct ScriptedLangChainToolAgent {
tool_name: String,
input: serde_json::Value,
}
impl ScriptedLangChainToolAgent {
fn new(tool_name: String, input: serde_json::Value) -> Self {
Self { tool_name, input }
}
}
#[async_trait]
impl BaseAgent for ScriptedLangChainToolAgent {
async fn plan(
&self,
intermediate_steps: &[AgentStep],
_inputs: &HashMap<String, String>,
) -> Result<AgentOutput, AgentError> {
if let Some(step) = intermediate_steps.last() {
return Ok(AgentOutput::Finish(AgentFinish::new(
step.observation.clone(),
String::new(),
)));
}
Ok(AgentOutput::Action(AgentAction {
tool: self.tool_name.clone(),
tool_input: ToolInput::Object(self.input.clone()),
log: "scripted-call-1".to_string(),
}))
}
fn get_allowed_tools(&self) -> Option<Vec<&str>> {
Some(vec![self.tool_name.as_str()])
}
}
struct EchoLangChainTool {
name: String,
}
impl EchoLangChainTool {
fn new(name: String) -> Self {
Self { name }
}
}
#[async_trait]
impl BaseTool for EchoLangChainTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"用于验证 LangChain-Rust AgentExecutor 能执行最小工具调用。"
}
async fn run(&self, input: String) -> Result<String, ToolError> {
Ok(json!({
"ok": true,
"tool": self.name,
"input": input,
})
.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn langchain_adapter_executes_minimal_tool_call() {
let adapter = LangChainRustAdapter::new(FunctionAgentLimits {
max_tool_calls: 2,
timeout_ms: 1_000,
})
.expect("limits should be valid");
let output = adapter
.execute_minimal_tool_call("retrieve_puzzle_template_catalog", json!({"query": "拼图"}))
.await
.expect("langchain executor should run tool");
let parsed: serde_json::Value =
serde_json::from_str(output.as_str()).expect("tool output should be json");
assert_eq!(parsed["ok"], true);
assert_eq!(parsed["tool"], "retrieve_puzzle_template_catalog");
assert!(
parsed["input"]
.as_str()
.unwrap_or_default()
.contains("拼图")
);
}
#[test]
fn function_calling_agent_uses_gpt5_config_without_calling_network() {
let adapter = LangChainRustAdapter::new(FunctionAgentLimits::default())
.expect("limits should be valid");
let agent = adapter.build_function_calling_agent(
"test-key",
"http://127.0.0.1:9/v1",
Vec::new(),
Some("系统提示".to_string()),
);
let debug_text = format!("{agent:?}");
assert!(debug_text.contains("FunctionCallingAgent"));
assert!(debug_text.contains("系统提示"));
}
}

View File

@@ -0,0 +1,23 @@
pub mod apimart_gpt5_adapter;
pub mod callbacks;
pub mod error;
pub mod function_agent;
pub mod langchain_adapter;
pub mod output_parser;
pub mod puzzle_phase1_agent;
pub mod tool_registry;
pub use apimart_gpt5_adapter::{
CREATIVE_AGENT_GPT5_MODEL, Gpt5ResponsesAgentClient, build_gpt5_multimodal_request,
};
pub use callbacks::{
CreativeAgentCallbackEvent, CreativeAgentCallbackKind, CreativeAgentCallbacks,
};
pub use error::PlatformAgentError;
pub use function_agent::FunctionAgentLimits;
pub use langchain_adapter::LangChainRustAdapter;
pub use puzzle_phase1_agent::{
CreativeAgentExecutor, MockLangChainRustAgentExecutor, PuzzlePhase1AgentInput,
PuzzlePhase1AgentOutput,
};
pub use tool_registry::{CreativeAgentTool, CreativeAgentToolRegistry, ToolExecutionBudget};

View File

@@ -0,0 +1,12 @@
use serde::de::DeserializeOwned;
use crate::error::PlatformAgentError;
pub fn parse_json_output<T>(raw_text: &str) -> Result<T, PlatformAgentError>
where
T: DeserializeOwned,
{
serde_json::from_str(raw_text).map_err(|error| {
PlatformAgentError::OutputParse(format!("解析 Agent JSON 输出失败:{error}"))
})
}

View File

@@ -0,0 +1,112 @@
use async_trait::async_trait;
use crate::{
callbacks::CreativeAgentCallbacks, error::PlatformAgentError,
function_agent::FunctionAgentLimits,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzlePhase1AgentInput {
pub session_id: String,
pub user_text: String,
pub image_urls: Vec<String>,
pub limits: FunctionAgentLimits,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PuzzlePhase1AgentOutput {
pub session_id: String,
pub model: String,
pub final_message: String,
pub tool_call_count: usize,
}
#[async_trait]
pub trait CreativeAgentExecutor: Send + Sync {
async fn run_puzzle_phase1(
&self,
input: PuzzlePhase1AgentInput,
callbacks: CreativeAgentCallbacks,
) -> Result<PuzzlePhase1AgentOutput, PlatformAgentError>;
}
#[derive(Clone, Debug, Default)]
pub struct MockLangChainRustAgentExecutor;
#[async_trait]
impl CreativeAgentExecutor for MockLangChainRustAgentExecutor {
async fn run_puzzle_phase1(
&self,
input: PuzzlePhase1AgentInput,
callbacks: CreativeAgentCallbacks,
) -> Result<PuzzlePhase1AgentOutput, PlatformAgentError> {
input.limits.validate()?;
if input.session_id.trim().is_empty() {
return Err(PlatformAgentError::InvalidInput(
"creative session_id 不能为空".to_string(),
));
}
if input.user_text.trim().is_empty() && input.image_urls.is_empty() {
return Err(PlatformAgentError::InvalidInput(
"创意 Agent 输入文本和图片不能同时为空".to_string(),
));
}
callbacks.stage("perceiving");
callbacks.stage("thinking");
callbacks.stage("remembering");
callbacks.stage("selecting_puzzle_template");
callbacks.stage("waiting_template_confirmation");
Ok(PuzzlePhase1AgentOutput {
session_id: input.session_id,
model: crate::apimart_gpt5_adapter::CREATIVE_AGENT_GPT5_MODEL.to_string(),
final_message: "已完成创意 Agent Phase 1 mock 执行,可进入模板确认。".to_string(),
tool_call_count: 0,
})
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
#[tokio::test]
async fn mock_executor_keeps_gpt5_model_and_emits_stages() {
let events = Arc::new(Mutex::new(Vec::new()));
let events_clone = events.clone();
let callbacks = CreativeAgentCallbacks::new(move |event| {
events_clone.lock().expect("events lock").push(event.label);
});
let output = MockLangChainRustAgentExecutor
.run_puzzle_phase1(
PuzzlePhase1AgentInput {
session_id: "creative-session-test".to_string(),
user_text: "做一个家庭纪念拼图".to_string(),
image_urls: vec!["https://example.com/ref.png".to_string()],
limits: FunctionAgentLimits::default(),
},
callbacks,
)
.await
.expect("mock executor should succeed");
assert_eq!(
output.model,
crate::apimart_gpt5_adapter::CREATIVE_AGENT_GPT5_MODEL
);
assert_eq!(
events.lock().expect("events lock").as_slice(),
[
"perceiving",
"thinking",
"remembering",
"selecting_puzzle_template",
"waiting_template_confirmation",
]
);
}
}

View File

@@ -0,0 +1,116 @@
use std::{
collections::HashMap,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use async_trait::async_trait;
use serde_json::Value;
use crate::{
callbacks::{CreativeAgentCallbackEvent, CreativeAgentCallbackKind, CreativeAgentCallbacks},
error::PlatformAgentError,
};
#[async_trait]
pub trait CreativeAgentTool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> Option<Value> {
None
}
async fn call(&self, input: Value) -> Result<Value, PlatformAgentError>;
}
#[derive(Debug)]
pub struct ToolExecutionBudget {
max_tool_calls: usize,
used_tool_calls: AtomicUsize,
}
impl ToolExecutionBudget {
pub fn new(max_tool_calls: usize) -> Result<Self, PlatformAgentError> {
if max_tool_calls == 0 {
return Err(PlatformAgentError::InvalidInput(
"Agent max_tool_calls 必须大于 0".to_string(),
));
}
Ok(Self {
max_tool_calls,
used_tool_calls: AtomicUsize::new(0),
})
}
pub fn reserve_call(&self) -> Result<(), PlatformAgentError> {
let previous = self.used_tool_calls.fetch_add(1, Ordering::SeqCst);
if previous >= self.max_tool_calls {
return Err(PlatformAgentError::ToolBudgetExceeded {
max_tool_calls: self.max_tool_calls,
});
}
Ok(())
}
}
#[derive(Default)]
pub struct CreativeAgentToolRegistry {
tools: HashMap<String, Arc<dyn CreativeAgentTool>>,
}
impl CreativeAgentToolRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<T>(&mut self, tool: T)
where
T: CreativeAgentTool + 'static,
{
self.tools.insert(tool.name().to_string(), Arc::new(tool));
}
pub fn len(&self) -> usize {
self.tools.len()
}
pub fn is_empty(&self) -> bool {
self.tools.is_empty()
}
pub async fn execute(
&self,
name: &str,
input: Value,
budget: &ToolExecutionBudget,
callbacks: &CreativeAgentCallbacks,
) -> Result<Value, PlatformAgentError> {
budget.reserve_call()?;
let tool = self
.tools
.get(name)
.ok_or_else(|| PlatformAgentError::ToolNotFound(name.to_string()))?;
callbacks.emit(CreativeAgentCallbackEvent {
kind: CreativeAgentCallbackKind::ToolStarted,
label: name.to_string(),
detail: None,
});
let result = tool.call(input).await;
callbacks.emit(CreativeAgentCallbackEvent {
kind: if result.is_ok() {
CreativeAgentCallbackKind::ToolCompleted
} else {
CreativeAgentCallbackKind::Error
},
label: name.to_string(),
detail: result.as_ref().err().map(ToString::to_string),
});
result
}
}

View File

@@ -16,7 +16,7 @@
当前实现只覆盖“文本 chat completion”主链不提前混入媒体生成和业务编排
1. 支持 OpenAI 兼容格式的 JSON 请求与 SSE 增量响应
2. 支持按 provider 打标签,但不把业务 prompt、SSE 转发和模块状态写回放进本 crate
2. 支持按 provider 打标签,但不把业务 prompt、SSE 转发和模块状态写回本 crate
3. `DashScope` 当前只通过“调用方显式提供兼容文本网关 base url”的方式接入不复用图像 API
4. 角色动画、图片、视频、资产轮询仍留在后续 `platform-llm` / `platform-oss` / 业务模块任务里另行实现

View File

@@ -57,7 +57,18 @@ pub enum LlmMessageRole {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LlmMessage {
pub role: LlmMessageRole,
// 中文注释:保留纯文本字段兼容 Chat Completions 和既有调用Responses 多模态请求读取 content_parts。
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content_parts: Vec<LlmMessageContentPart>,
}
// Responses 多模态内容块。字段名按上游 OpenAI 兼容协议保持 snake_case。
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LlmMessageContentPart {
InputText { text: String },
InputImage { image_url: String },
}
// 文本补全请求冻结为“消息列表 + 可选模型覆盖 + 可选 max_tokens”最小闭环。
@@ -179,10 +190,10 @@ struct ResponsesInputMessage {
}
#[derive(Serialize)]
struct ResponsesInputContentPart {
#[serde(rename = "type")]
part_type: &'static str,
text: String,
#[serde(tag = "type", rename_all = "snake_case")]
enum ResponsesInputContentPart {
InputText { text: String },
InputImage { image_url: String },
}
#[derive(Serialize)]
@@ -398,6 +409,7 @@ impl LlmMessage {
Self {
role,
content: content.into(),
content_parts: Vec::new(),
}
}
@@ -412,6 +424,39 @@ impl LlmMessage {
pub fn assistant(content: impl Into<String>) -> Self {
Self::new(LlmMessageRole::Assistant, content)
}
pub fn multimodal(role: LlmMessageRole, content_parts: Vec<LlmMessageContentPart>) -> Self {
let content = content_parts
.iter()
.filter_map(|part| match part {
LlmMessageContentPart::InputText { text } => Some(text.as_str()),
LlmMessageContentPart::InputImage { .. } => None,
})
.collect::<Vec<_>>()
.join("\n");
Self {
role,
content,
content_parts,
}
}
pub fn user_multimodal(content_parts: Vec<LlmMessageContentPart>) -> Self {
Self::multimodal(LlmMessageRole::User, content_parts)
}
pub fn with_image_url(mut self, image_url: impl Into<String>) -> Self {
if self.content_parts.is_empty() && !self.content.trim().is_empty() {
self.content_parts.push(LlmMessageContentPart::InputText {
text: self.content.clone(),
});
}
self.content_parts.push(LlmMessageContentPart::InputImage {
image_url: image_url.into(),
});
self
}
}
impl LlmTextRequest {
@@ -466,9 +511,27 @@ impl LlmTextRequest {
}
for message in &self.messages {
if message.content.trim().is_empty() {
let has_text = !message.content.trim().is_empty()
|| message.content_parts.iter().any(|part| match part {
LlmMessageContentPart::InputText { text } => !text.trim().is_empty(),
LlmMessageContentPart::InputImage { .. } => false,
});
let has_image = message.content_parts.iter().any(|part| match part {
LlmMessageContentPart::InputImage { image_url } => !image_url.trim().is_empty(),
LlmMessageContentPart::InputText { .. } => false,
});
if !has_text && !has_image {
return Err(LlmError::InvalidRequest(
"LLM message.content 不能为空".to_string(),
"LLM message content 不能为空".to_string(),
));
}
if message.content_parts.iter().any(|part| match part {
LlmMessageContentPart::InputText { text } => text.trim().is_empty(),
LlmMessageContentPart::InputImage { image_url } => image_url.trim().is_empty(),
}) {
return Err(LlmError::InvalidRequest(
"LLM message content part 不能为空".to_string(),
));
}
}
@@ -1073,10 +1136,30 @@ fn map_responses_input_messages(messages: &[LlmMessage]) -> Vec<ResponsesInputMe
LlmMessageRole::User => "user",
LlmMessageRole::Assistant => "assistant",
},
content: vec![ResponsesInputContentPart {
part_type: "input_text",
text: message.content.clone(),
}],
content: map_responses_content_parts(message),
})
.collect()
}
fn map_responses_content_parts(message: &LlmMessage) -> Vec<ResponsesInputContentPart> {
if message.content_parts.is_empty() {
return vec![ResponsesInputContentPart::InputText {
text: message.content.clone(),
}];
}
message
.content_parts
.iter()
.map(|part| match part {
LlmMessageContentPart::InputText { text } => {
ResponsesInputContentPart::InputText { text: text.clone() }
}
LlmMessageContentPart::InputImage { image_url } => {
ResponsesInputContentPart::InputImage {
image_url: image_url.clone(),
}
}
})
.collect()
}
@@ -1764,6 +1847,64 @@ mod tests {
);
}
#[tokio::test]
async fn responses_multimodal_request_sends_input_text_and_input_image() {
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
let address = listener.local_addr().expect("listener should have addr");
let server_handle = thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("request should connect");
let request_text = read_request(&mut stream);
write_response(
&mut stream,
MockResponse {
status_line: "200 OK",
content_type: "application/json; charset=utf-8",
body: r#"{"id":"resp_multimodal","model":"gpt-5","output_text":"","status":"completed"}"#.to_string(),
extra_headers: Vec::new(),
},
);
request_text
});
let client = build_test_client(format!("http://{address}"), 0);
let response = client
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system("你是创意互动内容生成 Agent"),
LlmMessage::user_multimodal(vec![
LlmMessageContentPart::InputText {
text: "把这张图做成拼图".to_string(),
},
LlmMessageContentPart::InputImage {
image_url: "https://example.com/ref.png".to_string(),
},
]),
])
.with_model("gpt-5")
.with_responses_api(),
)
.await
.expect("responses multimodal request_text should succeed");
let request_text = server_handle.join().expect("server thread should join");
let request_body = request_text
.split("\r\n\r\n")
.nth(1)
.expect("request body should exist");
let request_json: serde_json::Value =
serde_json::from_str(request_body).expect("request body should be json");
assert_eq!(response.model, "gpt-5");
assert_eq!(request_json["model"], serde_json::json!("gpt-5"));
assert_eq!(
request_json["input"][1]["content"],
serde_json::json!([
{ "type": "input_text", "text": "把这张图做成拼图" },
{ "type": "input_image", "image_url": "https://example.com/ref.png" }
])
);
}
#[tokio::test]
async fn stream_text_accumulates_sse_response() {
let server_url = spawn_mock_server(vec![MockResponse {

View File

@@ -17,6 +17,8 @@ pub struct CreationAgentDocumentInputPayload {
pub content_type: Option<String>,
pub size_bytes: usize,
pub text: String,
#[serde(default)]
pub source_asset_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]

View File

@@ -0,0 +1,563 @@
use crate::puzzle_creative_template::{
PuzzleCreativeTemplateProtocol, PuzzleCreativeTemplateSelection, PuzzleDraftFieldPatch,
PuzzleImageGenerationPlan, PuzzleTemplateCostRange,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CreativeAgentStage {
Idle,
Perceiving,
Thinking,
Remembering,
SelectingPuzzleTemplate,
WaitingTemplateConfirmation,
PlanningPuzzleLevels,
Acting,
Reflecting,
Collaborating,
TargetReady,
WaitingUser,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CreativeAgentEntryContext {
CreationHome,
PuzzleWorkspace,
GalleryRemix,
DraftRestore,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CreativeAgentMessageRole {
User,
Assistant,
System,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CreativeAgentMessageKind {
Chat,
Stage,
ActionResult,
Warning,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CreativeAgentInputPartType {
InputText,
InputImage,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentInputPart {
#[serde(rename = "type")]
pub part_type: CreativeAgentInputPartType,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub image_url: Option<String>,
#[serde(default)]
pub asset_id: Option<String>,
#[serde(default)]
pub thumbnail_url: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeImageInput {
pub asset_id: String,
pub read_url: String,
#[serde(default)]
pub thumbnail_url: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeImageSummary {
#[serde(default)]
pub asset_id: Option<String>,
#[serde(default)]
pub read_url: Option<String>,
#[serde(default)]
pub thumbnail_url: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
#[serde(default)]
pub summary: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CreativeUnsupportedPlayType {
Rpg,
#[serde(rename = "match3d")]
Match3d,
BigFish,
SquareHole,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CreativeCapabilityStatus {
Unsupported,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeUnsupportedCapability {
pub play_type: CreativeUnsupportedPlayType,
pub title: String,
pub status: CreativeCapabilityStatus,
pub reason: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeInputSummary {
#[serde(default)]
pub text: Option<String>,
pub entry_context: CreativeAgentEntryContext,
pub images: Vec<CreativeImageSummary>,
#[serde(default)]
pub material_summary: Option<String>,
pub unsupported_capabilities: Vec<CreativeUnsupportedCapability>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentMessage {
pub id: String,
pub role: CreativeAgentMessageRole,
pub kind: CreativeAgentMessageKind,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CreativeTargetPlayType {
Puzzle,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum CreativeTargetStage {
PuzzleAgentWorkspace,
PuzzleResult,
PuzzleRuntime,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeTargetSessionBinding {
pub play_type: CreativeTargetPlayType,
pub target_session_id: String,
pub target_stage: CreativeTargetStage,
#[serde(default)]
pub result_profile_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentSessionSnapshot {
pub session_id: String,
pub stage: CreativeAgentStage,
pub input_summary: CreativeInputSummary,
pub messages: Vec<CreativeAgentMessage>,
#[serde(default)]
pub puzzle_template_catalog: Vec<PuzzleCreativeTemplateProtocol>,
#[serde(default)]
pub puzzle_template_selection: Option<PuzzleCreativeTemplateSelection>,
#[serde(default)]
pub puzzle_image_generation_plan: Option<PuzzleImageGenerationPlan>,
#[serde(default)]
pub target_binding: Option<CreativeTargetSessionBinding>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateCreativeAgentSessionRequest {
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub images: Vec<CreativeImageInput>,
#[serde(default)]
pub entry_context: Option<CreativeAgentEntryContext>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentSessionResponse {
pub session: CreativeAgentSessionSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StreamCreativeAgentMessageRequest {
pub client_message_id: String,
pub content: Vec<CreativeAgentInputPart>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmCreativePuzzleTemplateRequest {
pub selection: PuzzleCreativeTemplateSelection,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeDraftEditStreamRequest {
pub client_message_id: String,
pub instruction: String,
pub target_puzzle_session_id: String,
pub current_draft: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeDraftEditResult {
pub edit_instructions: Vec<PuzzleDraftFieldPatch>,
pub session: CreativeAgentSessionSnapshot,
pub puzzle_session: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CreativeAgentSseEventType {
Stage,
AgentMessageDelta,
ThoughtSummaryDelta,
PuzzleTemplateCatalog,
PuzzleTemplateSelection,
PuzzleCostRange,
PuzzleLevelPlan,
ToolStarted,
ToolCompleted,
Reflection,
TargetSession,
Session,
Error,
Done,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentSseEnvelope {
pub event: CreativeAgentSseEventType,
pub data: serde_json::Value,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentStageEvent {
pub session_id: String,
pub stage: CreativeAgentStage,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentMessageDeltaEvent {
pub session_id: String,
pub message_id: String,
pub role: CreativeAgentMessageRole,
pub kind: CreativeAgentMessageKind,
pub text_delta: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentThoughtSummaryDeltaEvent {
pub session_id: String,
pub thought_id: String,
pub text_delta: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentTemplateCatalogEvent {
pub session_id: String,
pub templates: Vec<PuzzleCreativeTemplateProtocol>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentTemplateSelectionEvent {
pub session_id: String,
pub selection: PuzzleCreativeTemplateSelection,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentCostRangeEvent {
pub session_id: String,
pub cost_range: PuzzleTemplateCostRange,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentLevelPlanEvent {
pub session_id: String,
pub plan: PuzzleImageGenerationPlan,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentToolEvent {
pub session_id: String,
pub tool_call_id: String,
pub tool_name: String,
#[serde(default)]
pub summary: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentReflectionEvent {
pub session_id: String,
pub pass: bool,
pub summary: String,
pub warnings: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentTargetSessionEvent {
pub session_id: String,
pub binding: CreativeTargetSessionBinding,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentErrorEvent {
#[serde(default)]
pub session_id: Option<String>,
pub code: String,
pub message: String,
pub recoverable: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativeAgentDoneEvent {
pub session_id: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::puzzle_creative_template::{
PuzzleCreativeTemplateProtocol, PuzzleDraftEditableFieldPath, PuzzleLevelGenerationMode,
PuzzleSupportedLevelMode, PuzzleTemplateImageGenerationPolicy, PuzzleTemplatePricingUnit,
};
use serde_json::json;
fn cost_range() -> PuzzleTemplateCostRange {
PuzzleTemplateCostRange {
min_points: 2,
max_points: 12,
pricing_unit: PuzzleTemplatePricingUnit::Point,
reason: "按关卡数和每关图片生成次数估算".to_string(),
}
}
fn template_selection() -> PuzzleCreativeTemplateSelection {
PuzzleCreativeTemplateSelection {
template_id: "puzzle.default-creative".to_string(),
title: "创意拼图".to_string(),
reason: "素材适合拆成可试玩的拼图关卡".to_string(),
cost_range: cost_range(),
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
selected_level_mode: PuzzleLevelGenerationMode::SingleLevel,
planned_level_count: 1,
requires_user_confirmation: true,
}
}
fn template_protocol() -> PuzzleCreativeTemplateProtocol {
PuzzleCreativeTemplateProtocol {
template_id: "puzzle.default-creative".to_string(),
title: "创意拼图".to_string(),
summary: "把图文灵感做成拼图".to_string(),
preview_image_src: None,
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
min_level_count: 1,
max_level_count: 6,
default_level_count: 1,
cost_range: cost_range(),
required_draft_fields: vec![PuzzleDraftEditableFieldPath::WorkTitle],
image_policy: PuzzleTemplateImageGenerationPolicy {
allow_uploaded_image_directly: true,
allow_generated_images: true,
allow_per_level_reference_image: true,
default_candidate_count_per_level: 1,
},
}
}
#[test]
fn creative_agent_session_snapshot_uses_camel_case() {
let snapshot = CreativeAgentSessionSnapshot {
session_id: "creative-session-1".to_string(),
stage: CreativeAgentStage::WaitingTemplateConfirmation,
input_summary: CreativeInputSummary {
text: Some("把这张旅行照做成拼图".to_string()),
entry_context: CreativeAgentEntryContext::CreationHome,
images: vec![CreativeImageSummary {
asset_id: Some("asset-1".to_string()),
read_url: Some("https://example.test/image.png".to_string()),
thumbnail_url: None,
width: Some(1024),
height: Some(768),
summary: Some("一张旅行照片".to_string()),
}],
material_summary: Some("旅行纪念素材".to_string()),
unsupported_capabilities: vec![CreativeUnsupportedCapability {
play_type: CreativeUnsupportedPlayType::BigFish,
title: "大鱼吃小鱼".to_string(),
status: CreativeCapabilityStatus::Unsupported,
reason: "Phase 1 只开放拼图模板".to_string(),
}],
},
messages: vec![CreativeAgentMessage {
id: "message-1".to_string(),
role: CreativeAgentMessageRole::Assistant,
kind: CreativeAgentMessageKind::Chat,
text: "我会先选择拼图模板。".to_string(),
created_at: "2026-05-05T00:00:00Z".to_string(),
}],
puzzle_template_catalog: vec![template_protocol()],
puzzle_template_selection: Some(template_selection()),
puzzle_image_generation_plan: None,
target_binding: Some(CreativeTargetSessionBinding {
play_type: CreativeTargetPlayType::Puzzle,
target_session_id: "puzzle-session-1".to_string(),
target_stage: CreativeTargetStage::PuzzleResult,
result_profile_id: None,
}),
updated_at: "2026-05-05T00:00:01Z".to_string(),
};
let payload = serde_json::to_value(&snapshot).expect("snapshot should serialize");
assert_eq!(payload["sessionId"], json!("creative-session-1"));
assert_eq!(payload["stage"], json!("waiting_template_confirmation"));
assert_eq!(
payload["inputSummary"]["entryContext"],
json!("creation_home")
);
assert_eq!(
payload["inputSummary"]["unsupportedCapabilities"][0]["playType"],
json!("big_fish")
);
assert_eq!(
payload["puzzleTemplateCatalog"][0]["templateId"],
json!("puzzle.default-creative")
);
assert_eq!(
payload["puzzleTemplateSelection"]["selectedLevelMode"],
json!("single_level")
);
assert_eq!(
payload["targetBinding"]["targetStage"],
json!("puzzle-result")
);
let decoded: CreativeAgentSessionSnapshot =
serde_json::from_value(payload).expect("snapshot should deserialize");
assert_eq!(decoded, snapshot);
}
#[test]
fn creative_agent_sse_events_serialize_event_names() {
let event = CreativeAgentSseEnvelope {
event: CreativeAgentSseEventType::PuzzleTemplateSelection,
data: serde_json::to_value(CreativeAgentTemplateSelectionEvent {
session_id: "creative-session-1".to_string(),
selection: template_selection(),
})
.expect("event data should serialize"),
};
let payload = serde_json::to_value(event).expect("event should serialize");
assert_eq!(payload["event"], json!("puzzle_template_selection"));
assert_eq!(
payload["data"]["selection"]["costRange"]["pricingUnit"],
json!("point")
);
let thought_event = CreativeAgentSseEnvelope {
event: CreativeAgentSseEventType::ThoughtSummaryDelta,
data: serde_json::to_value(CreativeAgentThoughtSummaryDeltaEvent {
session_id: "creative-session-1".to_string(),
thought_id: "thought-1".to_string(),
text_delta: "正在理解素材".to_string(),
})
.expect("event data should serialize"),
};
let thought_payload = serde_json::to_value(thought_event).expect("event should serialize");
assert_eq!(thought_payload["event"], json!("thought_summary_delta"));
assert_eq!(thought_payload["data"]["thoughtId"], json!("thought-1"));
let catalog_event = CreativeAgentSseEnvelope {
event: CreativeAgentSseEventType::PuzzleTemplateCatalog,
data: serde_json::to_value(CreativeAgentTemplateCatalogEvent {
session_id: "creative-session-1".to_string(),
templates: vec![template_protocol()],
})
.expect("event data should serialize"),
};
let catalog_payload =
serde_json::to_value(catalog_event).expect("event should serialize");
assert_eq!(catalog_payload["event"], json!("puzzle_template_catalog"));
assert_eq!(
catalog_payload["data"]["templates"][0]["templateId"],
json!("puzzle.default-creative")
);
}
#[test]
fn creative_agent_multimodal_parts_keep_image_url_camel_case() {
let request = StreamCreativeAgentMessageRequest {
client_message_id: "client-message-1".to_string(),
content: vec![
CreativeAgentInputPart {
part_type: CreativeAgentInputPartType::InputText,
text: Some("做一张拼图".to_string()),
image_url: None,
asset_id: None,
thumbnail_url: None,
},
CreativeAgentInputPart {
part_type: CreativeAgentInputPartType::InputImage,
text: None,
image_url: Some("https://example.test/image.png".to_string()),
asset_id: Some("asset-1".to_string()),
thumbnail_url: None,
},
],
};
let payload = serde_json::to_value(request).expect("request should serialize");
assert_eq!(payload["clientMessageId"], json!("client-message-1"));
assert_eq!(payload["content"][0]["type"], json!("input_text"));
assert_eq!(payload["content"][1]["type"], json!("input_image"));
assert_eq!(
payload["content"][1]["imageUrl"],
json!("https://example.test/image.png")
);
}
}

View File

@@ -6,11 +6,13 @@ pub mod auth;
pub mod big_fish;
pub mod big_fish_works;
pub mod creation_agent_document_input;
pub mod creative_agent;
pub mod llm;
pub mod match3d_agent;
pub mod match3d_runtime;
pub mod match3d_works;
pub mod puzzle_agent;
pub mod puzzle_creative_template;
pub mod puzzle_gallery;
pub mod puzzle_runtime;
pub mod puzzle_works;
@@ -20,3 +22,4 @@ pub mod square_hole_agent;
pub mod square_hole_runtime;
pub mod square_hole_works;
pub mod story;
pub mod visual_novel;

View File

@@ -15,6 +15,8 @@ pub struct CreatePuzzleAgentSessionRequest {
pub reference_image_src: Option<String>,
#[serde(default)]
pub image_model: Option<String>,
#[serde(default)]
pub ai_redraw: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -37,6 +39,8 @@ pub struct ExecutePuzzleAgentActionRequest {
#[serde(default)]
pub image_model: Option<String>,
#[serde(default)]
pub ai_redraw: Option<bool>,
#[serde(default)]
pub candidate_count: Option<u32>,
#[serde(default)]
pub candidate_id: Option<String>,
@@ -145,6 +149,8 @@ pub struct PuzzleDraftLevelResponse {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
#[serde(default)]
pub picture_reference: Option<String>,
pub candidates: Vec<PuzzleGeneratedImageCandidateResponse>,
#[serde(default)]
pub selected_candidate_id: Option<String>,

View File

@@ -0,0 +1,230 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PuzzleTemplatePricingUnit {
Point,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleSupportedLevelMode {
Single,
Multi,
SingleOrMulti,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleLevelGenerationMode {
SingleLevel,
MultiLevel,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleTemplateCostRange {
pub min_points: u32,
pub max_points: u32,
pub pricing_unit: PuzzleTemplatePricingUnit,
pub reason: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PuzzleDraftEditableFieldPath {
#[serde(rename = "workTitle")]
WorkTitle,
#[serde(rename = "workDescription")]
WorkDescription,
#[serde(rename = "workTags")]
WorkTags,
#[serde(rename = "levels[].levelName")]
LevelName,
#[serde(rename = "levels[].pictureDescription")]
LevelPictureDescription,
#[serde(rename = "levels[].pictureReference")]
LevelPictureReference,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleTemplateImageGenerationPolicy {
pub allow_uploaded_image_directly: bool,
pub allow_generated_images: bool,
pub allow_per_level_reference_image: bool,
pub default_candidate_count_per_level: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateProtocol {
pub template_id: String,
pub title: String,
pub summary: String,
#[serde(default)]
pub preview_image_src: Option<String>,
pub supported_level_mode: PuzzleSupportedLevelMode,
pub min_level_count: u32,
pub max_level_count: u32,
pub default_level_count: u32,
pub cost_range: PuzzleTemplateCostRange,
pub required_draft_fields: Vec<PuzzleDraftEditableFieldPath>,
pub image_policy: PuzzleTemplateImageGenerationPolicy,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleCreativeTemplateSelection {
pub template_id: String,
pub title: String,
pub reason: String,
pub cost_range: PuzzleTemplateCostRange,
pub supported_level_mode: PuzzleSupportedLevelMode,
pub selected_level_mode: PuzzleLevelGenerationMode,
pub planned_level_count: u32,
pub requires_user_confirmation: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativePuzzleLevelDraftInput {
pub level_name: String,
pub picture_description: String,
/// 任务 A 冻结Phase 1 采用正式字段方案,后续拼图草稿落地需补正式 pictureReference 字段。
#[serde(default)]
pub picture_reference: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreativePuzzleDraftToolInput {
pub template_id: String,
pub template_cost_range: PuzzleTemplateCostRange,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
pub levels: Vec<CreativePuzzleLevelDraftInput>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleImageGenerationPlanLevel {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub image_prompt: String,
#[serde(default)]
pub picture_reference: Option<String>,
pub candidate_count: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleImageGenerationPlan {
pub mode: PuzzleLevelGenerationMode,
pub template_id: String,
pub estimated_cost_range: PuzzleTemplateCostRange,
pub levels: Vec<PuzzleImageGenerationPlanLevel>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PuzzleDraftFieldPatchOperation {
Set,
Append,
Replace,
Remove,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleDraftFieldPatch {
pub field_path: PuzzleDraftEditableFieldPath,
pub operation: PuzzleDraftFieldPatchOperation,
#[serde(default)]
pub level_id: Option<String>,
pub value: serde_json::Value,
pub rationale: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn cost_range() -> PuzzleTemplateCostRange {
PuzzleTemplateCostRange {
min_points: 2,
max_points: 12,
pricing_unit: PuzzleTemplatePricingUnit::Point,
reason: "按关卡数和每关图片生成次数估算".to_string(),
}
}
#[test]
fn creative_agent_puzzle_template_protocol_uses_camel_case() {
let payload = serde_json::to_value(PuzzleCreativeTemplateProtocol {
template_id: "puzzle.default-creative".to_string(),
title: "创意拼图".to_string(),
summary: "把图文灵感做成拼图".to_string(),
preview_image_src: None,
supported_level_mode: PuzzleSupportedLevelMode::SingleOrMulti,
min_level_count: 1,
max_level_count: 6,
default_level_count: 1,
cost_range: cost_range(),
required_draft_fields: vec![
PuzzleDraftEditableFieldPath::WorkTitle,
PuzzleDraftEditableFieldPath::LevelPictureReference,
],
image_policy: PuzzleTemplateImageGenerationPolicy {
allow_uploaded_image_directly: true,
allow_generated_images: true,
allow_per_level_reference_image: true,
default_candidate_count_per_level: 1,
},
})
.expect("template should serialize");
assert_eq!(payload["templateId"], json!("puzzle.default-creative"));
assert_eq!(payload["previewImageSrc"], json!(null));
assert_eq!(payload["supportedLevelMode"], json!("single_or_multi"));
assert_eq!(payload["costRange"]["pricingUnit"], json!("point"));
assert_eq!(
payload["requiredDraftFields"],
json!(["workTitle", "levels[].pictureReference"])
);
assert_eq!(
payload["imagePolicy"]["allowPerLevelReferenceImage"],
json!(true)
);
}
#[test]
fn creative_agent_puzzle_image_plan_roundtrips() {
let plan = PuzzleImageGenerationPlan {
mode: PuzzleLevelGenerationMode::MultiLevel,
template_id: "puzzle.default-creative".to_string(),
estimated_cost_range: cost_range(),
levels: vec![PuzzleImageGenerationPlanLevel {
level_id: "level-1".to_string(),
level_name: "第一关".to_string(),
picture_description: "温暖的家庭照片".to_string(),
image_prompt: "pixel puzzle, warm family photo".to_string(),
picture_reference: Some("asset-ref-1".to_string()),
candidate_count: 1,
}],
};
let payload = serde_json::to_value(&plan).expect("plan should serialize");
assert_eq!(payload["mode"], json!("multi_level"));
assert_eq!(
payload["levels"][0]["pictureReference"],
json!("asset-ref-1")
);
let decoded: PuzzleImageGenerationPlan =
serde_json::from_value(payload).expect("plan should deserialize");
assert_eq!(decoded, plan);
}
}

View File

@@ -0,0 +1,687 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelSourceMode {
Idea,
Document,
Blank,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelCharacterRole {
Protagonist,
Main,
Supporting,
Antagonist,
Background,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAssetSource {
PlatformAsset,
Generated,
External,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelSceneAvailability {
Opening,
Always,
PhaseLocked,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAttributePanelMode {
Off,
PlatformWhitelist,
TemplateConfig,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelValidationSeverity {
Error,
Warning,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelAgentStatus {
Collecting,
Drafting,
Ready,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelAgentMessageRole {
User,
Assistant,
System,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAgentMessageKind {
Chat,
Summary,
ActionResult,
Warning,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelAgentActionKind {
GenerateDraft,
PatchWorld,
PatchCharacter,
PatchScene,
PatchStoryPhase,
GenerateSceneImage,
GenerateCharacterImage,
CompileWorkProfile,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelAgentPhase {
Perception,
Reasoning,
Drafting,
Reflection,
Finalizing,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelRunMode {
Test,
Play,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelRunStatus {
Active,
Completed,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VisualNovelRuntimeActionKind {
Choice,
FreeText,
Continue,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelTransitionKind {
Fade,
Cut,
Flash,
None,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisualNovelHistorySource {
Player,
Assistant,
System,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum VisualNovelFlagValue {
String(String),
Number(f64),
Bool(bool),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelDraftPatch {
pub path: String,
pub op: String,
#[serde(default)]
pub value: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelValidationIssue {
pub issue_id: String,
pub code: String,
pub severity: VisualNovelValidationSeverity,
pub path: String,
pub message: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelChoiceDraft {
pub choice_id: String,
pub text: String,
#[serde(default)]
pub action_hint: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCharacterImageAsset {
pub asset_id: String,
pub image_src: String,
#[serde(default)]
pub expression: Option<String>,
pub source: VisualNovelAssetSource,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorldDraft {
pub title: String,
pub summary: String,
pub background: String,
pub premise: String,
pub literary_style: String,
pub player_role: String,
pub default_tone: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCharacterDraft {
pub character_id: String,
pub name: String,
#[serde(default)]
pub gender: Option<String>,
pub role: VisualNovelCharacterRole,
pub appearance: String,
pub personality: String,
pub tone: String,
pub background: String,
pub relationship_to_player: String,
pub image_assets: Vec<VisualNovelCharacterImageAsset>,
#[serde(default)]
pub default_expression: Option<String>,
pub is_player_visible: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSceneDraft {
pub scene_id: String,
pub name: String,
pub description: String,
#[serde(default)]
pub background_image_src: Option<String>,
#[serde(default)]
pub music_src: Option<String>,
#[serde(default)]
pub ambient_sound_src: Option<String>,
pub availability: VisualNovelSceneAvailability,
pub phase_ids: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelStoryPhaseDraft {
pub phase_id: String,
pub title: String,
pub goal: String,
pub summary: String,
pub entry_condition: String,
pub exit_condition: String,
pub scene_ids: Vec<String>,
pub character_ids: Vec<String>,
pub suggested_choices: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelOpeningDraft {
#[serde(default)]
pub scene_id: Option<String>,
pub narration: String,
#[serde(default)]
pub speaker_character_id: Option<String>,
#[serde(default)]
pub first_dialogue: Option<String>,
pub initial_choices: Vec<VisualNovelChoiceDraft>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRuntimeConfigDraft {
pub text_mode_enabled: bool,
pub default_text_mode: bool,
pub max_history_entries: u32,
pub max_assistant_step_count_per_turn: u32,
pub allow_free_text_action: bool,
pub allow_history_regeneration: bool,
pub attribute_panel_mode: VisualNovelAttributePanelMode,
pub save_archive_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelResultDraft {
#[serde(default)]
pub profile_id: Option<String>,
pub work_title: String,
pub work_description: String,
pub work_tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
pub source_mode: VisualNovelSourceMode,
pub source_asset_ids: Vec<String>,
pub world: VisualNovelWorldDraft,
pub characters: Vec<VisualNovelCharacterDraft>,
pub scenes: Vec<VisualNovelSceneDraft>,
pub story_phases: Vec<VisualNovelStoryPhaseDraft>,
pub opening: VisualNovelOpeningDraft,
pub runtime_config: VisualNovelRuntimeConfigDraft,
pub publish_ready: bool,
pub validation_issues: Vec<VisualNovelValidationIssue>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelAgentMessage {
pub id: String,
pub role: VisualNovelAgentMessageRole,
pub kind: VisualNovelAgentMessageKind,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelAgentPendingAction {
pub action_id: String,
pub kind: VisualNovelAgentActionKind,
pub label: String,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub payload: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelAgentSessionSnapshot {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: VisualNovelSourceMode,
pub status: VisualNovelAgentStatus,
pub messages: Vec<VisualNovelAgentMessage>,
#[serde(default)]
pub draft: Option<VisualNovelResultDraft>,
#[serde(default)]
pub pending_action: Option<VisualNovelAgentPendingAction>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateVisualNovelSessionRequest {
pub source_mode: VisualNovelSourceMode,
#[serde(default)]
pub seed_text: Option<String>,
#[serde(default)]
pub source_asset_ids: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSessionResponse {
pub session: VisualNovelAgentSessionSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkSummary {
pub runtime_kind: String,
pub profile_id: String,
pub owner_user_id: String,
pub title: String,
pub description: String,
#[serde(default)]
pub cover_image_src: Option<String>,
pub tags: Vec<String>,
pub publish_status: String,
pub publish_ready: bool,
pub play_count: u32,
pub updated_at: String,
#[serde(default)]
pub published_at: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkDetail {
pub work_id: String,
pub summary: VisualNovelWorkSummary,
#[serde(default)]
pub source_session_id: Option<String>,
pub author_display_name: String,
pub source_asset_ids: Vec<String>,
pub draft: VisualNovelResultDraft,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorksResponse {
pub works: Vec<VisualNovelWorkSummary>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelWorkResponse {
pub work: VisualNovelWorkDetail,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UpdateVisualNovelWorkRequest {
pub draft: VisualNovelResultDraft,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelCompileResponse {
pub session: VisualNovelAgentSessionSnapshot,
pub work: VisualNovelWorkDetail,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SendVisualNovelMessageRequest {
pub client_message_id: String,
pub text: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteVisualNovelAgentActionRequest {
#[serde(default)]
pub action_id: Option<String>,
pub kind: VisualNovelAgentActionKind,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub payload: Option<Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelAgentStreamEvent {
Start {
session_id: String,
},
Phase {
phase: VisualNovelAgentPhase,
},
TextDelta {
text: String,
},
DraftPatch {
patch: VisualNovelDraftPatch,
},
ActionRequired {
action: VisualNovelAgentPendingAction,
},
Complete {
session: VisualNovelAgentSessionSnapshot,
},
Error {
message: String,
retryable: bool,
},
Done {},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelRuntimeStep {
SceneChange {
scene_id: String,
#[serde(default)]
background_image_src: Option<String>,
#[serde(default)]
music_src: Option<String>,
},
Narration {
text: String,
},
Dialogue {
character_id: String,
character_name: String,
#[serde(default)]
expression: Option<String>,
text: String,
},
Transition {
transition_kind: VisualNovelTransitionKind,
#[serde(default)]
text: Option<String>,
},
Choice {
choices: Vec<VisualNovelChoiceDraft>,
},
Flag {
key: String,
value: VisualNovelFlagValue,
},
Metric {
key: String,
delta: f64,
},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelHistoryEntry {
pub entry_id: String,
pub run_id: String,
pub turn_index: u32,
pub source: VisualNovelHistorySource,
#[serde(default)]
pub action_text: Option<String>,
pub steps: Vec<VisualNovelRuntimeStep>,
#[serde(default)]
pub snapshot_before_hash: Option<String>,
#[serde(default)]
pub snapshot_after_hash: Option<String>,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRunSnapshot {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: VisualNovelRunMode,
pub status: VisualNovelRunStatus,
#[serde(default)]
pub current_scene_id: Option<String>,
#[serde(default)]
pub current_phase_id: Option<String>,
pub visible_character_ids: Vec<String>,
pub flags: BTreeMap<String, VisualNovelFlagValue>,
pub metrics: BTreeMap<String, f64>,
pub history: Vec<VisualNovelHistoryEntry>,
pub available_choices: Vec<VisualNovelChoiceDraft>,
pub text_mode_enabled: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRuntimeActionRequest {
pub action_kind: VisualNovelRuntimeActionKind,
#[serde(default)]
pub choice_id: Option<String>,
#[serde(default)]
pub text: Option<String>,
pub client_event_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelStartRunRequest {
pub profile_id: String,
pub mode: VisualNovelRunMode,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRunResponse {
pub run: VisualNovelRunSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelHistoryResponse {
pub history: Vec<VisualNovelHistoryEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelRegenerateRequest {
pub history_entry_id: String,
pub client_event_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualNovelSaveArchiveState {
pub runtime_kind: String,
pub profile_id: String,
pub run_id: String,
#[serde(default)]
pub current_scene_id: Option<String>,
#[serde(default)]
pub current_phase_id: Option<String>,
pub history_cursor: u32,
#[serde(default)]
pub snapshot_hash: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "type",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
pub enum VisualNovelRuntimeStreamEvent {
Start { run_id: String },
RawText { text: String },
Step { step: VisualNovelRuntimeStep },
Snapshot { run: VisualNovelRunSnapshot },
Complete { run: VisualNovelRunSnapshot },
Error { message: String, retryable: bool },
Done {},
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn result_draft_and_runtime_step_use_contract_case() {
let draft = VisualNovelResultDraft {
profile_id: Some("vn-profile-1".to_string()),
work_title: "雨夜书店".to_string(),
work_description: "一段视觉小说测试底稿".to_string(),
work_tags: vec!["悬疑".to_string()],
cover_image_src: None,
source_mode: VisualNovelSourceMode::Idea,
source_asset_ids: Vec::new(),
world: VisualNovelWorldDraft {
title: "雨夜书店".to_string(),
summary: "主角在雨夜进入一间只在午夜出现的书店。".to_string(),
background: "城市边缘的旧街区。".to_string(),
premise: "找回遗失的名字。".to_string(),
literary_style: "细腻、轻悬疑".to_string(),
player_role: "误入书店的读者".to_string(),
default_tone: "克制而温柔".to_string(),
},
characters: Vec::new(),
scenes: Vec::new(),
story_phases: Vec::new(),
opening: VisualNovelOpeningDraft {
scene_id: None,
narration: "雨声落下。".to_string(),
speaker_character_id: None,
first_dialogue: None,
initial_choices: Vec::new(),
},
runtime_config: VisualNovelRuntimeConfigDraft {
text_mode_enabled: true,
default_text_mode: false,
max_history_entries: 80,
max_assistant_step_count_per_turn: 8,
allow_free_text_action: true,
allow_history_regeneration: true,
attribute_panel_mode: VisualNovelAttributePanelMode::Off,
save_archive_enabled: true,
},
publish_ready: false,
validation_issues: Vec::new(),
updated_at: "2026-05-05T00:00:00Z".to_string(),
};
let payload = serde_json::to_value(draft).expect("draft should serialize");
assert_eq!(payload["profileId"], json!("vn-profile-1"));
assert_eq!(payload["sourceMode"], json!("idea"));
assert_eq!(payload["runtimeConfig"]["attributePanelMode"], json!("off"));
let step = VisualNovelRuntimeStep::SceneChange {
scene_id: "scene-1".to_string(),
background_image_src: None,
music_src: None,
};
let step_payload = serde_json::to_value(step).expect("step should serialize");
assert_eq!(step_payload["type"], json!("scene_change"));
assert_eq!(step_payload["sceneId"], json!("scene-1"));
}
#[test]
fn runtime_stream_event_uses_tagged_envelope() {
let event = VisualNovelRuntimeStreamEvent::Step {
step: VisualNovelRuntimeStep::Narration {
text: "门铃响了。".to_string(),
},
};
let payload = serde_json::to_value(event).expect("event should serialize");
assert_eq!(payload["type"], json!("step"));
assert_eq!(payload["step"]["type"], json!("narration"));
assert_eq!(payload["step"]["text"], json!("门铃响了。"));
}
}

View File

@@ -19,6 +19,7 @@ module-runtime-story = { workspace = true }
module-runtime-item = { workspace = true }
module-square-hole = { workspace = true }
module-story = { workspace = true }
module-visual-novel = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
shared-contracts = { workspace = true }

View File

@@ -60,6 +60,13 @@ pub use mapper::{
SquareHoleRunRecord, SquareHoleRunRestartRecordInput, SquareHoleRunStartRecordInput,
SquareHoleRunStopRecordInput, SquareHoleRunTimeUpRecordInput, SquareHoleShapeOptionRecord,
SquareHoleShapeSnapshotRecord, SquareHoleWorkProfileRecord, SquareHoleWorkUpdateRecordInput,
VisualNovelAgentMessageFinalizeRecordInput, VisualNovelAgentMessageRecord,
VisualNovelAgentMessageSubmitRecordInput, VisualNovelAgentSessionCreateRecordInput,
VisualNovelAgentSessionRecord, VisualNovelHistoryEntryRecord,
VisualNovelHistoryEntryRecordInput, VisualNovelRunRecord, VisualNovelRunSnapshotRecordInput,
VisualNovelRunStartRecordInput, VisualNovelRuntimeEventRecord,
VisualNovelRuntimeEventRecordInput, VisualNovelWorkCompileRecordInput,
VisualNovelWorkProfileRecord, VisualNovelWorkUpdateRecordInput,
};
pub mod ai;
@@ -76,6 +83,7 @@ pub mod runtime;
pub mod square_hole;
pub mod story;
pub mod story_runtime;
pub mod visual_novel;
use std::{
error::Error,

View File

@@ -1672,6 +1672,118 @@ pub(crate) fn map_square_hole_drop_shape_procedure_result(
})
}
pub(crate) fn map_visual_novel_agent_session_procedure_result(
result: VisualNovelAgentSessionProcedureResult,
) -> Result<VisualNovelAgentSessionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session_json = result
.session_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel agent session 快照"))?;
let session =
serde_json::from_str::<VisualNovelAgentSessionJsonRecord>(&session_json).map_err(
|error| {
SpacetimeClientError::Runtime(format!(
"visual novel session_json 非法: {error}"
))
},
)?;
Ok(map_visual_novel_agent_session_snapshot(session))
}
pub(crate) fn map_visual_novel_work_procedure_result(
result: VisualNovelWorkProcedureResult,
) -> Result<VisualNovelWorkProfileRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let work_json = result
.work_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel work 快照"))?;
let work = serde_json::from_str::<VisualNovelWorkJsonRecord>(&work_json).map_err(|error| {
SpacetimeClientError::Runtime(format!("visual novel work_json 非法: {error}"))
})?;
Ok(map_visual_novel_work_snapshot(work))
}
pub(crate) fn map_visual_novel_works_procedure_result(
result: VisualNovelWorksProcedureResult,
) -> Result<Vec<VisualNovelWorkProfileRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let items_json = result
.items_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel works 快照"))?;
let items = serde_json::from_str::<Vec<VisualNovelWorkJsonRecord>>(&items_json).map_err(
|error| {
SpacetimeClientError::Runtime(format!("visual novel works items_json 非法: {error}"))
},
)?;
Ok(items.into_iter().map(map_visual_novel_work_snapshot).collect())
}
pub(crate) fn map_visual_novel_run_procedure_result(
result: VisualNovelRunProcedureResult,
) -> Result<VisualNovelRunRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run_json = result
.run_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel run 快照"))?;
let run = serde_json::from_str::<VisualNovelRunJsonRecord>(&run_json).map_err(|error| {
SpacetimeClientError::Runtime(format!("visual novel run_json 非法: {error}"))
})?;
Ok(map_visual_novel_run_snapshot(run))
}
pub(crate) fn map_visual_novel_history_procedure_result(
result: VisualNovelHistoryProcedureResult,
) -> Result<Vec<VisualNovelHistoryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let items_json = result
.items_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel history 快照"))?;
let items = serde_json::from_str::<Vec<VisualNovelHistoryEntryJsonRecord>>(&items_json)
.map_err(|error| {
SpacetimeClientError::Runtime(format!("visual novel history items_json 非法: {error}"))
})?;
Ok(items.into_iter().map(map_visual_novel_history_entry).collect())
}
pub(crate) fn map_visual_novel_runtime_event_procedure_result(
result: VisualNovelRuntimeEventProcedureResult,
) -> Result<VisualNovelRuntimeEventRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let event_json = result
.event_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel runtime event 快照"))?;
let event = serde_json::from_str::<VisualNovelRuntimeEventJsonRecord>(&event_json).map_err(
|error| {
SpacetimeClientError::Runtime(format!("visual novel event_json 非法: {error}"))
},
)?;
Ok(map_visual_novel_runtime_event(event))
}
pub(crate) fn map_story_session_procedure_result(
result: StorySessionProcedureResult,
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
@@ -2667,6 +2779,7 @@ pub(crate) fn map_puzzle_draft_level(snapshot: DomainPuzzleDraftLevel) -> Puzzle
level_id: snapshot.level_id,
level_name: snapshot.level_name,
picture_description: snapshot.picture_description,
picture_reference: snapshot.picture_reference,
candidates: snapshot
.candidates
.into_iter()
@@ -3175,6 +3288,127 @@ fn build_square_hole_anchor_item(
}
}
fn map_visual_novel_agent_session_snapshot(
snapshot: VisualNovelAgentSessionJsonRecord,
) -> VisualNovelAgentSessionRecord {
VisualNovelAgentSessionRecord {
session_id: snapshot.session_id,
owner_user_id: snapshot.owner_user_id,
source_mode: snapshot.source_mode,
status: snapshot.status,
seed_text: snapshot.seed_text,
source_asset_ids: snapshot.source_asset_ids,
current_turn: snapshot.current_turn,
progress_percent: snapshot.progress_percent,
messages: snapshot
.messages
.into_iter()
.map(map_visual_novel_agent_message)
.collect(),
draft: snapshot.draft,
pending_action: snapshot.pending_action,
last_assistant_reply: snapshot.last_assistant_reply,
published_profile_id: snapshot.published_profile_id,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_visual_novel_agent_message(
snapshot: VisualNovelAgentMessageJsonRecord,
) -> VisualNovelAgentMessageRecord {
VisualNovelAgentMessageRecord {
message_id: snapshot.message_id,
session_id: snapshot.session_id,
role: snapshot.role,
kind: snapshot.kind,
text: snapshot.text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
fn map_visual_novel_work_snapshot(
snapshot: VisualNovelWorkJsonRecord,
) -> VisualNovelWorkProfileRecord {
VisualNovelWorkProfileRecord {
work_id: snapshot.work_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
source_session_id: snapshot.source_session_id,
author_display_name: snapshot.author_display_name,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
tags: snapshot.tags,
cover_image_src: snapshot.cover_image_src,
source_asset_ids: snapshot.source_asset_ids,
draft: snapshot.draft,
publication_status: snapshot.publication_status,
publish_ready: snapshot.publish_ready,
play_count: snapshot.play_count,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
}
}
fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunJsonRecord) -> VisualNovelRunRecord {
VisualNovelRunRecord {
run_id: snapshot.run_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
mode: snapshot.mode,
status: snapshot.status,
current_scene_id: snapshot.current_scene_id,
current_phase_id: snapshot.current_phase_id,
visible_character_ids: snapshot.visible_character_ids,
flags: snapshot.flags,
metrics: snapshot.metrics,
history: snapshot
.history
.into_iter()
.map(map_visual_novel_history_entry)
.collect(),
available_choices: snapshot.available_choices,
text_mode_enabled: snapshot.text_mode_enabled,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_visual_novel_history_entry(
snapshot: VisualNovelHistoryEntryJsonRecord,
) -> VisualNovelHistoryEntryRecord {
VisualNovelHistoryEntryRecord {
entry_id: snapshot.entry_id,
run_id: snapshot.run_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
turn_index: snapshot.turn_index,
source: snapshot.source,
action_text: snapshot.action_text,
steps: snapshot.steps,
snapshot_before_hash: snapshot.snapshot_before_hash,
snapshot_after_hash: snapshot.snapshot_after_hash,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
fn map_visual_novel_runtime_event(
snapshot: VisualNovelRuntimeEventJsonRecord,
) -> VisualNovelRuntimeEventRecord {
VisualNovelRuntimeEventRecord {
event_id: snapshot.event_id,
run_id: snapshot.run_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
event_kind: snapshot.event_kind,
client_event_id: snapshot.client_event_id,
history_entry_id: snapshot.history_entry_id,
payload: snapshot.payload,
occurred_at: format_timestamp_micros(snapshot.occurred_at_micros),
}
}
fn normalize_match3d_stage(value: &str) -> &str {
match value {
"Collecting" | "collecting" | "collecting_config" => "collecting_config",
@@ -6105,6 +6339,323 @@ pub struct SquareHoleRunTimeUpRecordInput {
pub finished_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelAgentSessionCreateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub seed_text: String,
pub source_asset_ids_json: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub draft_json: Option<String>,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelAgentMessageSubmitRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub user_message_id: String,
pub user_message_text: String,
pub submitted_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelAgentMessageFinalizeRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub draft_json: Option<String>,
pub pending_action_json: Option<String>,
pub status: String,
pub progress_percent: u32,
pub updated_at_micros: i64,
pub error_message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelWorkCompileRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub work_id: Option<String>,
pub author_display_name: String,
pub work_title: Option<String>,
pub work_description: Option<String>,
pub tags_json: Option<String>,
pub cover_image_src: Option<String>,
pub compiled_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelWorkUpdateRecordInput {
pub profile_id: String,
pub owner_user_id: String,
pub work_title: String,
pub work_description: String,
pub tags_json: String,
pub cover_image_src: Option<String>,
pub source_asset_ids_json: String,
pub draft_json: String,
pub publish_ready: bool,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelRunStartRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: String,
pub snapshot_json: Option<String>,
pub started_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelRunSnapshotRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub status: String,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub visible_character_ids_json: String,
pub flags_json: String,
pub metrics_json: String,
pub available_choices_json: String,
pub text_mode_enabled: bool,
pub snapshot_json: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelHistoryEntryRecordInput {
pub entry_id: String,
pub run_id: String,
pub owner_user_id: String,
pub turn_index: u32,
pub source: String,
pub action_text: Option<String>,
pub steps_json: String,
pub snapshot_before_hash: Option<String>,
pub snapshot_after_hash: Option<String>,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelRuntimeEventRecordInput {
pub event_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: Option<String>,
pub event_kind: String,
pub client_event_id: Option<String>,
pub history_entry_id: Option<String>,
pub payload_json: String,
pub occurred_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelAgentMessageRecord {
pub message_id: String,
pub session_id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelAgentSessionRecord {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub status: String,
pub seed_text: String,
pub source_asset_ids: Vec<String>,
pub current_turn: u32,
pub progress_percent: u32,
pub messages: Vec<VisualNovelAgentMessageRecord>,
pub draft: Option<serde_json::Value>,
pub pending_action: Option<serde_json::Value>,
pub last_assistant_reply: Option<String>,
pub published_profile_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelWorkProfileRecord {
pub work_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub tags: Vec<String>,
pub cover_image_src: Option<String>,
pub source_asset_ids: Vec<String>,
pub draft: serde_json::Value,
pub publication_status: String,
pub publish_ready: bool,
pub play_count: u32,
pub created_at: String,
pub updated_at: String,
pub published_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelHistoryEntryRecord {
pub entry_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub turn_index: u32,
pub source: String,
pub action_text: Option<String>,
pub steps: serde_json::Value,
pub snapshot_before_hash: Option<String>,
pub snapshot_after_hash: Option<String>,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelRunRecord {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: String,
pub status: String,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub visible_character_ids: Vec<String>,
pub flags: serde_json::Value,
pub metrics: serde_json::Value,
pub history: Vec<VisualNovelHistoryEntryRecord>,
pub available_choices: serde_json::Value,
pub text_mode_enabled: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelRuntimeEventRecord {
pub event_id: String,
pub run_id: Option<String>,
pub owner_user_id: String,
pub profile_id: Option<String>,
pub event_kind: String,
pub client_event_id: Option<String>,
pub history_entry_id: Option<String>,
pub payload: serde_json::Value,
pub occurred_at: String,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct VisualNovelAgentMessageJsonRecord {
message_id: String,
session_id: String,
role: String,
kind: String,
text: String,
created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct VisualNovelAgentSessionJsonRecord {
session_id: String,
owner_user_id: String,
source_mode: String,
status: String,
seed_text: String,
source_asset_ids: Vec<String>,
current_turn: u32,
progress_percent: u32,
messages: Vec<VisualNovelAgentMessageJsonRecord>,
draft: Option<serde_json::Value>,
pending_action: Option<serde_json::Value>,
last_assistant_reply: Option<String>,
published_profile_id: Option<String>,
created_at_micros: i64,
updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct VisualNovelWorkJsonRecord {
work_id: String,
profile_id: String,
owner_user_id: String,
source_session_id: Option<String>,
author_display_name: String,
work_title: String,
work_description: String,
tags: Vec<String>,
cover_image_src: Option<String>,
source_asset_ids: Vec<String>,
draft: serde_json::Value,
publication_status: String,
publish_ready: bool,
play_count: u32,
created_at_micros: i64,
updated_at_micros: i64,
published_at_micros: Option<i64>,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct VisualNovelHistoryEntryJsonRecord {
entry_id: String,
run_id: String,
owner_user_id: String,
profile_id: String,
turn_index: u32,
source: String,
action_text: Option<String>,
steps: serde_json::Value,
snapshot_before_hash: Option<String>,
snapshot_after_hash: Option<String>,
created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct VisualNovelRunJsonRecord {
run_id: String,
owner_user_id: String,
profile_id: String,
mode: String,
status: String,
current_scene_id: Option<String>,
current_phase_id: Option<String>,
visible_character_ids: Vec<String>,
flags: serde_json::Value,
metrics: serde_json::Value,
history: Vec<VisualNovelHistoryEntryJsonRecord>,
available_choices: serde_json::Value,
text_mode_enabled: bool,
created_at_micros: i64,
updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct VisualNovelRuntimeEventJsonRecord {
event_id: String,
run_id: Option<String>,
owner_user_id: String,
profile_id: Option<String>,
event_kind: String,
client_event_id: Option<String>,
history_entry_id: Option<String>,
payload: serde_json::Value,
occurred_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAnchorItemRecord {
pub key: String,
@@ -6552,6 +7103,7 @@ pub struct PuzzleDraftLevelRecord {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub picture_reference: Option<String>,
pub candidates: Vec<PuzzleGeneratedImageCandidateRecord>,
pub selected_candidate_id: Option<String>,
pub cover_image_src: Option<String>,

View File

@@ -0,0 +1,62 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_history_procedure_result_type::VisualNovelHistoryProcedureResult;
use super::visual_novel_runtime_history_append_input_type::VisualNovelRuntimeHistoryAppendInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AppendVisualNovelRuntimeHistoryEntryArgs {
pub input: VisualNovelRuntimeHistoryAppendInput,
}
impl __sdk::InModule for AppendVisualNovelRuntimeHistoryEntryArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `append_visual_novel_runtime_history_entry`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait append_visual_novel_runtime_history_entry {
fn append_visual_novel_runtime_history_entry(
&self,
input: VisualNovelRuntimeHistoryAppendInput,
) {
self.append_visual_novel_runtime_history_entry_then(input, |_, _| {});
}
fn append_visual_novel_runtime_history_entry_then(
&self,
input: VisualNovelRuntimeHistoryAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl append_visual_novel_runtime_history_entry for super::RemoteProcedures {
fn append_visual_novel_runtime_history_entry_then(
&self,
input: VisualNovelRuntimeHistoryAppendInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelHistoryProcedureResult>(
"append_visual_novel_runtime_history_entry",
AppendVisualNovelRuntimeHistoryEntryArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult;
use super::visual_novel_work_compile_input_type::VisualNovelWorkCompileInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct CompileVisualNovelWorkProfileArgs {
pub input: VisualNovelWorkCompileInput,
}
impl __sdk::InModule for CompileVisualNovelWorkProfileArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `compile_visual_novel_work_profile`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait compile_visual_novel_work_profile {
fn compile_visual_novel_work_profile(&self, input: VisualNovelWorkCompileInput) {
self.compile_visual_novel_work_profile_then(input, |_, _| {});
}
fn compile_visual_novel_work_profile_then(
&self,
input: VisualNovelWorkCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl compile_visual_novel_work_profile for super::RemoteProcedures {
fn compile_visual_novel_work_profile_then(
&self,
input: VisualNovelWorkCompileInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelAgentSessionProcedureResult>(
"compile_visual_novel_work_profile",
CompileVisualNovelWorkProfileArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_agent_session_create_input_type::VisualNovelAgentSessionCreateInput;
use super::visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct CreateVisualNovelAgentSessionArgs {
pub input: VisualNovelAgentSessionCreateInput,
}
impl __sdk::InModule for CreateVisualNovelAgentSessionArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `create_visual_novel_agent_session`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait create_visual_novel_agent_session {
fn create_visual_novel_agent_session(&self, input: VisualNovelAgentSessionCreateInput) {
self.create_visual_novel_agent_session_then(input, |_, _| {});
}
fn create_visual_novel_agent_session_then(
&self,
input: VisualNovelAgentSessionCreateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl create_visual_novel_agent_session for super::RemoteProcedures {
fn create_visual_novel_agent_session_then(
&self,
input: VisualNovelAgentSessionCreateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelAgentSessionProcedureResult>(
"create_visual_novel_agent_session",
CreateVisualNovelAgentSessionArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_work_delete_input_type::VisualNovelWorkDeleteInput;
use super::visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct DeleteVisualNovelWorkArgs {
pub input: VisualNovelWorkDeleteInput,
}
impl __sdk::InModule for DeleteVisualNovelWorkArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `delete_visual_novel_work`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait delete_visual_novel_work {
fn delete_visual_novel_work(&self, input: VisualNovelWorkDeleteInput) {
self.delete_visual_novel_work_then(input, |_, _| {});
}
fn delete_visual_novel_work_then(
&self,
input: VisualNovelWorkDeleteInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorksProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl delete_visual_novel_work for super::RemoteProcedures {
fn delete_visual_novel_work_then(
&self,
input: VisualNovelWorkDeleteInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorksProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelWorksProcedureResult>(
"delete_visual_novel_work",
DeleteVisualNovelWorkArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,62 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_agent_message_finalize_input_type::VisualNovelAgentMessageFinalizeInput;
use super::visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct FinalizeVisualNovelAgentMessageTurnArgs {
pub input: VisualNovelAgentMessageFinalizeInput,
}
impl __sdk::InModule for FinalizeVisualNovelAgentMessageTurnArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `finalize_visual_novel_agent_message_turn`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait finalize_visual_novel_agent_message_turn {
fn finalize_visual_novel_agent_message_turn(
&self,
input: VisualNovelAgentMessageFinalizeInput,
) {
self.finalize_visual_novel_agent_message_turn_then(input, |_, _| {});
}
fn finalize_visual_novel_agent_message_turn_then(
&self,
input: VisualNovelAgentMessageFinalizeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl finalize_visual_novel_agent_message_turn for super::RemoteProcedures {
fn finalize_visual_novel_agent_message_turn_then(
&self,
input: VisualNovelAgentMessageFinalizeInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelAgentSessionProcedureResult>(
"finalize_visual_novel_agent_message_turn",
FinalizeVisualNovelAgentMessageTurnArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_agent_session_get_input_type::VisualNovelAgentSessionGetInput;
use super::visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetVisualNovelAgentSessionArgs {
pub input: VisualNovelAgentSessionGetInput,
}
impl __sdk::InModule for GetVisualNovelAgentSessionArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_visual_novel_agent_session`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_visual_novel_agent_session {
fn get_visual_novel_agent_session(&self, input: VisualNovelAgentSessionGetInput) {
self.get_visual_novel_agent_session_then(input, |_, _| {});
}
fn get_visual_novel_agent_session_then(
&self,
input: VisualNovelAgentSessionGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_visual_novel_agent_session for super::RemoteProcedures {
fn get_visual_novel_agent_session_then(
&self,
input: VisualNovelAgentSessionGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelAgentSessionProcedureResult>(
"get_visual_novel_agent_session",
GetVisualNovelAgentSessionArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_run_get_input_type::VisualNovelRunGetInput;
use super::visual_novel_run_procedure_result_type::VisualNovelRunProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetVisualNovelRunArgs {
pub input: VisualNovelRunGetInput,
}
impl __sdk::InModule for GetVisualNovelRunArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_visual_novel_run`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_visual_novel_run {
fn get_visual_novel_run(&self, input: VisualNovelRunGetInput) {
self.get_visual_novel_run_then(input, |_, _| {});
}
fn get_visual_novel_run_then(
&self,
input: VisualNovelRunGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_visual_novel_run for super::RemoteProcedures {
fn get_visual_novel_run_then(
&self,
input: VisualNovelRunGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelRunProcedureResult>(
"get_visual_novel_run",
GetVisualNovelRunArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_work_get_input_type::VisualNovelWorkGetInput;
use super::visual_novel_work_procedure_result_type::VisualNovelWorkProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetVisualNovelWorkDetailArgs {
pub input: VisualNovelWorkGetInput,
}
impl __sdk::InModule for GetVisualNovelWorkDetailArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_visual_novel_work_detail`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_visual_novel_work_detail {
fn get_visual_novel_work_detail(&self, input: VisualNovelWorkGetInput) {
self.get_visual_novel_work_detail_then(input, |_, _| {});
}
fn get_visual_novel_work_detail_then(
&self,
input: VisualNovelWorkGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_visual_novel_work_detail for super::RemoteProcedures {
fn get_visual_novel_work_detail_then(
&self,
input: VisualNovelWorkGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelWorkProcedureResult>(
"get_visual_novel_work_detail",
GetVisualNovelWorkDetailArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_history_procedure_result_type::VisualNovelHistoryProcedureResult;
use super::visual_novel_runtime_history_list_input_type::VisualNovelRuntimeHistoryListInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct ListVisualNovelRuntimeHistoryArgs {
pub input: VisualNovelRuntimeHistoryListInput,
}
impl __sdk::InModule for ListVisualNovelRuntimeHistoryArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `list_visual_novel_runtime_history`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait list_visual_novel_runtime_history {
fn list_visual_novel_runtime_history(&self, input: VisualNovelRuntimeHistoryListInput) {
self.list_visual_novel_runtime_history_then(input, |_, _| {});
}
fn list_visual_novel_runtime_history_then(
&self,
input: VisualNovelRuntimeHistoryListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl list_visual_novel_runtime_history for super::RemoteProcedures {
fn list_visual_novel_runtime_history_then(
&self,
input: VisualNovelRuntimeHistoryListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelHistoryProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelHistoryProcedureResult>(
"list_visual_novel_runtime_history",
ListVisualNovelRuntimeHistoryArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_works_list_input_type::VisualNovelWorksListInput;
use super::visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct ListVisualNovelWorksArgs {
pub input: VisualNovelWorksListInput,
}
impl __sdk::InModule for ListVisualNovelWorksArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `list_visual_novel_works`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait list_visual_novel_works {
fn list_visual_novel_works(&self, input: VisualNovelWorksListInput) {
self.list_visual_novel_works_then(input, |_, _| {});
}
fn list_visual_novel_works_then(
&self,
input: VisualNovelWorksListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorksProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl list_visual_novel_works for super::RemoteProcedures {
fn list_visual_novel_works_then(
&self,
input: VisualNovelWorksListInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorksProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelWorksProcedureResult>(
"list_visual_novel_works",
ListVisualNovelWorksArgs { input },
__callback,
);
}
}

View File

@@ -1,7 +1,7 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
// This was generated using spacetimedb cli version 2.1.0 (commit 10a4779b1338eff3708493d87496b51842a7c412).
// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d).
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
@@ -57,6 +57,7 @@ pub mod analytics_granularity_type;
pub mod analytics_metric_query_input_type;
pub mod analytics_metric_query_procedure_result_type;
pub mod append_ai_text_chunk_and_return_procedure;
pub mod append_visual_novel_runtime_history_entry_procedure;
pub mod apply_chapter_progression_ledger_entry_and_return_procedure;
pub mod apply_chapter_progression_ledger_entry_reducer;
pub mod apply_inventory_mutation_reducer;
@@ -167,6 +168,7 @@ pub mod compile_custom_world_published_profile_procedure;
pub mod compile_match_3_d_draft_procedure;
pub mod compile_puzzle_agent_draft_procedure;
pub mod compile_square_hole_draft_procedure;
pub mod compile_visual_novel_work_profile_procedure;
pub mod complete_ai_stage_and_return_procedure;
pub mod complete_ai_task_and_return_procedure;
pub mod confirm_asset_object_and_return_procedure;
@@ -185,6 +187,7 @@ pub mod create_match_3_d_agent_session_procedure;
pub mod create_profile_recharge_order_and_return_procedure;
pub mod create_puzzle_agent_session_procedure;
pub mod create_square_hole_agent_session_procedure;
pub mod create_visual_novel_agent_session_procedure;
pub mod custom_world_agent_action_execute_input_type;
pub mod custom_world_agent_action_execute_result_type;
pub mod custom_world_agent_card_detail_get_input_type;
@@ -268,6 +271,7 @@ pub mod delete_match_3_d_work_procedure;
pub mod delete_puzzle_work_procedure;
pub mod delete_runtime_snapshot_and_return_procedure;
pub mod delete_square_hole_work_procedure;
pub mod delete_visual_novel_work_procedure;
pub mod drag_puzzle_piece_or_group_procedure;
pub mod drop_square_hole_shape_procedure;
pub mod ensure_analytics_date_dimension_for_date_reducer;
@@ -281,6 +285,7 @@ pub mod finalize_custom_world_agent_message_turn_procedure;
pub mod finalize_match_3_d_agent_message_turn_procedure;
pub mod finalize_puzzle_agent_message_turn_procedure;
pub mod finalize_square_hole_agent_message_turn_procedure;
pub mod finalize_visual_novel_agent_message_turn_procedure;
pub mod finish_match_3_d_time_up_procedure;
pub mod finish_square_hole_time_up_procedure;
pub mod generate_big_fish_asset_procedure;
@@ -315,6 +320,9 @@ pub mod get_square_hole_agent_session_procedure;
pub mod get_square_hole_run_procedure;
pub mod get_square_hole_work_detail_procedure;
pub mod get_story_session_state_procedure;
pub mod get_visual_novel_agent_session_procedure;
pub mod get_visual_novel_run_procedure;
pub mod get_visual_novel_work_detail_procedure;
pub mod grant_inventory_item_input_type;
pub mod grant_new_user_registration_wallet_reward_procedure;
pub mod grant_player_progression_experience_and_return_procedure;
@@ -346,6 +354,8 @@ pub mod list_profile_wallet_ledger_procedure;
pub mod list_puzzle_gallery_procedure;
pub mod list_puzzle_works_procedure;
pub mod list_square_hole_works_procedure;
pub mod list_visual_novel_runtime_history_procedure;
pub mod list_visual_novel_works_procedure;
pub mod match_3_d_agent_message_finalize_input_type;
pub mod match_3_d_agent_message_row_type;
pub mod match_3_d_agent_message_submit_input_type;
@@ -434,6 +444,7 @@ pub mod publish_custom_world_world_procedure;
pub mod publish_match_3_d_work_procedure;
pub mod publish_puzzle_work_procedure;
pub mod publish_square_hole_work_procedure;
pub mod publish_visual_novel_work_procedure;
pub mod put_database_migration_import_chunk_procedure;
pub mod puzzle_agent_message_finalize_input_type;
pub mod puzzle_agent_message_kind_type;
@@ -515,6 +526,7 @@ pub mod record_big_fish_play_procedure;
pub mod record_custom_world_profile_like_procedure;
pub mod record_custom_world_profile_play_procedure;
pub mod record_puzzle_work_like_procedure;
pub mod record_visual_novel_runtime_event_procedure;
pub mod redeem_profile_referral_invite_code_procedure;
pub mod redeem_profile_reward_code_procedure;
pub mod refresh_session_table;
@@ -682,6 +694,7 @@ pub mod start_big_fish_run_procedure;
pub mod start_match_3_d_run_procedure;
pub mod start_puzzle_run_procedure;
pub mod start_square_hole_run_procedure;
pub mod start_visual_novel_run_procedure;
pub mod stop_match_3_d_run_procedure;
pub mod stop_square_hole_run_procedure;
pub mod story_continue_input_type;
@@ -704,6 +717,7 @@ pub mod submit_match_3_d_agent_message_procedure;
pub mod submit_puzzle_agent_message_procedure;
pub mod submit_puzzle_leaderboard_entry_procedure;
pub mod submit_square_hole_agent_message_procedure;
pub mod submit_visual_novel_agent_message_procedure;
pub mod swap_puzzle_pieces_procedure;
pub mod tracking_daily_stat_table;
pub mod tracking_daily_stat_type;
@@ -723,6 +737,7 @@ pub mod update_match_3_d_work_procedure;
pub mod update_puzzle_run_pause_procedure;
pub mod update_puzzle_work_procedure;
pub mod update_square_hole_work_procedure;
pub mod update_visual_novel_work_procedure;
pub mod upsert_auth_store_snapshot_procedure;
pub mod upsert_chapter_progression_and_return_procedure;
pub mod upsert_chapter_progression_reducer;
@@ -734,11 +749,46 @@ pub mod upsert_npc_state_reducer;
pub mod upsert_platform_browse_history_and_return_procedure;
pub mod upsert_runtime_setting_and_return_procedure;
pub mod upsert_runtime_snapshot_and_return_procedure;
pub mod upsert_visual_novel_run_snapshot_procedure;
pub mod use_puzzle_runtime_prop_procedure;
pub mod user_account_table;
pub mod user_account_type;
pub mod user_browse_history_table;
pub mod user_browse_history_type;
pub mod visual_novel_agent_message_finalize_input_type;
pub mod visual_novel_agent_message_row_type;
pub mod visual_novel_agent_message_submit_input_type;
pub mod visual_novel_agent_message_table;
pub mod visual_novel_agent_session_create_input_type;
pub mod visual_novel_agent_session_get_input_type;
pub mod visual_novel_agent_session_procedure_result_type;
pub mod visual_novel_agent_session_row_type;
pub mod visual_novel_agent_session_table;
pub mod visual_novel_history_procedure_result_type;
pub mod visual_novel_run_get_input_type;
pub mod visual_novel_run_procedure_result_type;
pub mod visual_novel_run_snapshot_upsert_input_type;
pub mod visual_novel_run_start_input_type;
pub mod visual_novel_runtime_event_procedure_result_type;
pub mod visual_novel_runtime_event_record_input_type;
pub mod visual_novel_runtime_event_table;
pub mod visual_novel_runtime_event_type;
pub mod visual_novel_runtime_history_append_input_type;
pub mod visual_novel_runtime_history_entry_row_type;
pub mod visual_novel_runtime_history_entry_table;
pub mod visual_novel_runtime_history_list_input_type;
pub mod visual_novel_runtime_run_row_type;
pub mod visual_novel_runtime_run_table;
pub mod visual_novel_work_compile_input_type;
pub mod visual_novel_work_delete_input_type;
pub mod visual_novel_work_get_input_type;
pub mod visual_novel_work_procedure_result_type;
pub mod visual_novel_work_profile_row_type;
pub mod visual_novel_work_profile_table;
pub mod visual_novel_work_publish_input_type;
pub mod visual_novel_work_update_input_type;
pub mod visual_novel_works_list_input_type;
pub mod visual_novel_works_procedure_result_type;
pub use accept_quest_reducer::accept_quest;
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
@@ -791,6 +841,7 @@ pub use analytics_granularity_type::AnalyticsGranularity;
pub use analytics_metric_query_input_type::AnalyticsMetricQueryInput;
pub use analytics_metric_query_procedure_result_type::AnalyticsMetricQueryProcedureResult;
pub use append_ai_text_chunk_and_return_procedure::append_ai_text_chunk_and_return;
pub use append_visual_novel_runtime_history_entry_procedure::append_visual_novel_runtime_history_entry;
pub use apply_chapter_progression_ledger_entry_and_return_procedure::apply_chapter_progression_ledger_entry_and_return;
pub use apply_chapter_progression_ledger_entry_reducer::apply_chapter_progression_ledger_entry;
pub use apply_inventory_mutation_reducer::apply_inventory_mutation;
@@ -901,6 +952,7 @@ pub use compile_custom_world_published_profile_procedure::compile_custom_world_p
pub use compile_match_3_d_draft_procedure::compile_match_3_d_draft;
pub use compile_puzzle_agent_draft_procedure::compile_puzzle_agent_draft;
pub use compile_square_hole_draft_procedure::compile_square_hole_draft;
pub use compile_visual_novel_work_profile_procedure::compile_visual_novel_work_profile;
pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return;
pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return;
pub use confirm_asset_object_and_return_procedure::confirm_asset_object_and_return;
@@ -919,6 +971,7 @@ pub use create_match_3_d_agent_session_procedure::create_match_3_d_agent_session
pub use create_profile_recharge_order_and_return_procedure::create_profile_recharge_order_and_return;
pub use create_puzzle_agent_session_procedure::create_puzzle_agent_session;
pub use create_square_hole_agent_session_procedure::create_square_hole_agent_session;
pub use create_visual_novel_agent_session_procedure::create_visual_novel_agent_session;
pub use custom_world_agent_action_execute_input_type::CustomWorldAgentActionExecuteInput;
pub use custom_world_agent_action_execute_result_type::CustomWorldAgentActionExecuteResult;
pub use custom_world_agent_card_detail_get_input_type::CustomWorldAgentCardDetailGetInput;
@@ -1002,6 +1055,7 @@ pub use delete_match_3_d_work_procedure::delete_match_3_d_work;
pub use delete_puzzle_work_procedure::delete_puzzle_work;
pub use delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return;
pub use delete_square_hole_work_procedure::delete_square_hole_work;
pub use delete_visual_novel_work_procedure::delete_visual_novel_work;
pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group;
pub use drop_square_hole_shape_procedure::drop_square_hole_shape;
pub use ensure_analytics_date_dimension_for_date_reducer::ensure_analytics_date_dimension_for_date;
@@ -1015,6 +1069,7 @@ pub use finalize_custom_world_agent_message_turn_procedure::finalize_custom_worl
pub use finalize_match_3_d_agent_message_turn_procedure::finalize_match_3_d_agent_message_turn;
pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn;
pub use finalize_square_hole_agent_message_turn_procedure::finalize_square_hole_agent_message_turn;
pub use finalize_visual_novel_agent_message_turn_procedure::finalize_visual_novel_agent_message_turn;
pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up;
pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up;
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
@@ -1049,6 +1104,9 @@ pub use get_square_hole_agent_session_procedure::get_square_hole_agent_session;
pub use get_square_hole_run_procedure::get_square_hole_run;
pub use get_square_hole_work_detail_procedure::get_square_hole_work_detail;
pub use get_story_session_state_procedure::get_story_session_state;
pub use get_visual_novel_agent_session_procedure::get_visual_novel_agent_session;
pub use get_visual_novel_run_procedure::get_visual_novel_run;
pub use get_visual_novel_work_detail_procedure::get_visual_novel_work_detail;
pub use grant_inventory_item_input_type::GrantInventoryItemInput;
pub use grant_new_user_registration_wallet_reward_procedure::grant_new_user_registration_wallet_reward;
pub use grant_player_progression_experience_and_return_procedure::grant_player_progression_experience_and_return;
@@ -1080,6 +1138,8 @@ pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
pub use list_puzzle_gallery_procedure::list_puzzle_gallery;
pub use list_puzzle_works_procedure::list_puzzle_works;
pub use list_square_hole_works_procedure::list_square_hole_works;
pub use list_visual_novel_runtime_history_procedure::list_visual_novel_runtime_history;
pub use list_visual_novel_works_procedure::list_visual_novel_works;
pub use match_3_d_agent_message_finalize_input_type::Match3DAgentMessageFinalizeInput;
pub use match_3_d_agent_message_row_type::Match3DAgentMessageRow;
pub use match_3_d_agent_message_submit_input_type::Match3DAgentMessageSubmitInput;
@@ -1168,6 +1228,7 @@ pub use publish_custom_world_world_procedure::publish_custom_world_world;
pub use publish_match_3_d_work_procedure::publish_match_3_d_work;
pub use publish_puzzle_work_procedure::publish_puzzle_work;
pub use publish_square_hole_work_procedure::publish_square_hole_work;
pub use publish_visual_novel_work_procedure::publish_visual_novel_work;
pub use put_database_migration_import_chunk_procedure::put_database_migration_import_chunk;
pub use puzzle_agent_message_finalize_input_type::PuzzleAgentMessageFinalizeInput;
pub use puzzle_agent_message_kind_type::PuzzleAgentMessageKind;
@@ -1249,6 +1310,7 @@ pub use record_big_fish_play_procedure::record_big_fish_play;
pub use record_custom_world_profile_like_procedure::record_custom_world_profile_like;
pub use record_custom_world_profile_play_procedure::record_custom_world_profile_play;
pub use record_puzzle_work_like_procedure::record_puzzle_work_like;
pub use record_visual_novel_runtime_event_procedure::record_visual_novel_runtime_event;
pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code;
pub use redeem_profile_reward_code_procedure::redeem_profile_reward_code;
pub use refresh_session_table::*;
@@ -1416,6 +1478,7 @@ pub use start_big_fish_run_procedure::start_big_fish_run;
pub use start_match_3_d_run_procedure::start_match_3_d_run;
pub use start_puzzle_run_procedure::start_puzzle_run;
pub use start_square_hole_run_procedure::start_square_hole_run;
pub use start_visual_novel_run_procedure::start_visual_novel_run;
pub use stop_match_3_d_run_procedure::stop_match_3_d_run;
pub use stop_square_hole_run_procedure::stop_square_hole_run;
pub use story_continue_input_type::StoryContinueInput;
@@ -1438,6 +1501,7 @@ pub use submit_match_3_d_agent_message_procedure::submit_match_3_d_agent_message
pub use submit_puzzle_agent_message_procedure::submit_puzzle_agent_message;
pub use submit_puzzle_leaderboard_entry_procedure::submit_puzzle_leaderboard_entry;
pub use submit_square_hole_agent_message_procedure::submit_square_hole_agent_message;
pub use submit_visual_novel_agent_message_procedure::submit_visual_novel_agent_message;
pub use swap_puzzle_pieces_procedure::swap_puzzle_pieces;
pub use tracking_daily_stat_table::*;
pub use tracking_daily_stat_type::TrackingDailyStat;
@@ -1457,6 +1521,7 @@ pub use update_match_3_d_work_procedure::update_match_3_d_work;
pub use update_puzzle_run_pause_procedure::update_puzzle_run_pause;
pub use update_puzzle_work_procedure::update_puzzle_work;
pub use update_square_hole_work_procedure::update_square_hole_work;
pub use update_visual_novel_work_procedure::update_visual_novel_work;
pub use upsert_auth_store_snapshot_procedure::upsert_auth_store_snapshot;
pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return;
pub use upsert_chapter_progression_reducer::upsert_chapter_progression;
@@ -1468,11 +1533,46 @@ pub use upsert_npc_state_reducer::upsert_npc_state;
pub use upsert_platform_browse_history_and_return_procedure::upsert_platform_browse_history_and_return;
pub use upsert_runtime_setting_and_return_procedure::upsert_runtime_setting_and_return;
pub use upsert_runtime_snapshot_and_return_procedure::upsert_runtime_snapshot_and_return;
pub use upsert_visual_novel_run_snapshot_procedure::upsert_visual_novel_run_snapshot;
pub use use_puzzle_runtime_prop_procedure::use_puzzle_runtime_prop;
pub use user_account_table::*;
pub use user_account_type::UserAccount;
pub use user_browse_history_table::*;
pub use user_browse_history_type::UserBrowseHistory;
pub use visual_novel_agent_message_finalize_input_type::VisualNovelAgentMessageFinalizeInput;
pub use visual_novel_agent_message_row_type::VisualNovelAgentMessageRow;
pub use visual_novel_agent_message_submit_input_type::VisualNovelAgentMessageSubmitInput;
pub use visual_novel_agent_message_table::*;
pub use visual_novel_agent_session_create_input_type::VisualNovelAgentSessionCreateInput;
pub use visual_novel_agent_session_get_input_type::VisualNovelAgentSessionGetInput;
pub use visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult;
pub use visual_novel_agent_session_row_type::VisualNovelAgentSessionRow;
pub use visual_novel_agent_session_table::*;
pub use visual_novel_history_procedure_result_type::VisualNovelHistoryProcedureResult;
pub use visual_novel_run_get_input_type::VisualNovelRunGetInput;
pub use visual_novel_run_procedure_result_type::VisualNovelRunProcedureResult;
pub use visual_novel_run_snapshot_upsert_input_type::VisualNovelRunSnapshotUpsertInput;
pub use visual_novel_run_start_input_type::VisualNovelRunStartInput;
pub use visual_novel_runtime_event_procedure_result_type::VisualNovelRuntimeEventProcedureResult;
pub use visual_novel_runtime_event_record_input_type::VisualNovelRuntimeEventRecordInput;
pub use visual_novel_runtime_event_table::*;
pub use visual_novel_runtime_event_type::VisualNovelRuntimeEvent;
pub use visual_novel_runtime_history_append_input_type::VisualNovelRuntimeHistoryAppendInput;
pub use visual_novel_runtime_history_entry_row_type::VisualNovelRuntimeHistoryEntryRow;
pub use visual_novel_runtime_history_entry_table::*;
pub use visual_novel_runtime_history_list_input_type::VisualNovelRuntimeHistoryListInput;
pub use visual_novel_runtime_run_row_type::VisualNovelRuntimeRunRow;
pub use visual_novel_runtime_run_table::*;
pub use visual_novel_work_compile_input_type::VisualNovelWorkCompileInput;
pub use visual_novel_work_delete_input_type::VisualNovelWorkDeleteInput;
pub use visual_novel_work_get_input_type::VisualNovelWorkGetInput;
pub use visual_novel_work_procedure_result_type::VisualNovelWorkProcedureResult;
pub use visual_novel_work_profile_row_type::VisualNovelWorkProfileRow;
pub use visual_novel_work_profile_table::*;
pub use visual_novel_work_publish_input_type::VisualNovelWorkPublishInput;
pub use visual_novel_work_update_input_type::VisualNovelWorkUpdateInput;
pub use visual_novel_works_list_input_type::VisualNovelWorksListInput;
pub use visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult;
#[derive(Clone, PartialEq, Debug)]
@@ -1817,6 +1917,12 @@ pub struct DbUpdate {
treasure_record: __sdk::TableUpdate<TreasureRecord>,
user_account: __sdk::TableUpdate<UserAccount>,
user_browse_history: __sdk::TableUpdate<UserBrowseHistory>,
visual_novel_agent_message: __sdk::TableUpdate<VisualNovelAgentMessageRow>,
visual_novel_agent_session: __sdk::TableUpdate<VisualNovelAgentSessionRow>,
visual_novel_runtime_event: __sdk::TableUpdate<VisualNovelRuntimeEvent>,
visual_novel_runtime_history_entry: __sdk::TableUpdate<VisualNovelRuntimeHistoryEntryRow>,
visual_novel_runtime_run: __sdk::TableUpdate<VisualNovelRuntimeRunRow>,
visual_novel_work_profile: __sdk::TableUpdate<VisualNovelWorkProfileRow>,
}
impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
@@ -2040,6 +2146,26 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"user_browse_history" => db_update
.user_browse_history
.append(user_browse_history_table::parse_table_update(table_update)?),
"visual_novel_agent_message" => db_update.visual_novel_agent_message.append(
visual_novel_agent_message_table::parse_table_update(table_update)?,
),
"visual_novel_agent_session" => db_update.visual_novel_agent_session.append(
visual_novel_agent_session_table::parse_table_update(table_update)?,
),
"visual_novel_runtime_event" => db_update.visual_novel_runtime_event.append(
visual_novel_runtime_event_table::parse_table_update(table_update)?,
),
"visual_novel_runtime_history_entry" => {
db_update.visual_novel_runtime_history_entry.append(
visual_novel_runtime_history_entry_table::parse_table_update(table_update)?,
)
}
"visual_novel_runtime_run" => db_update.visual_novel_runtime_run.append(
visual_novel_runtime_run_table::parse_table_update(table_update)?,
),
"visual_novel_work_profile" => db_update.visual_novel_work_profile.append(
visual_novel_work_profile_table::parse_table_update(table_update)?,
),
unknown => {
return Err(__sdk::InternalError::unknown_name(
@@ -2415,6 +2541,37 @@ impl __sdk::DbUpdate for DbUpdate {
&self.user_browse_history,
)
.with_updates_by_pk(|row| &row.browse_history_id);
diff.visual_novel_agent_message = cache
.apply_diff_to_table::<VisualNovelAgentMessageRow>(
"visual_novel_agent_message",
&self.visual_novel_agent_message,
)
.with_updates_by_pk(|row| &row.message_id);
diff.visual_novel_agent_session = cache
.apply_diff_to_table::<VisualNovelAgentSessionRow>(
"visual_novel_agent_session",
&self.visual_novel_agent_session,
)
.with_updates_by_pk(|row| &row.session_id);
diff.visual_novel_runtime_event = self.visual_novel_runtime_event.into_event_diff();
diff.visual_novel_runtime_history_entry = cache
.apply_diff_to_table::<VisualNovelRuntimeHistoryEntryRow>(
"visual_novel_runtime_history_entry",
&self.visual_novel_runtime_history_entry,
)
.with_updates_by_pk(|row| &row.entry_id);
diff.visual_novel_runtime_run = cache
.apply_diff_to_table::<VisualNovelRuntimeRunRow>(
"visual_novel_runtime_run",
&self.visual_novel_runtime_run,
)
.with_updates_by_pk(|row| &row.run_id);
diff.visual_novel_work_profile = cache
.apply_diff_to_table::<VisualNovelWorkProfileRow>(
"visual_novel_work_profile",
&self.visual_novel_work_profile,
)
.with_updates_by_pk(|row| &row.profile_id);
diff
}
@@ -2635,6 +2792,24 @@ impl __sdk::DbUpdate for DbUpdate {
"user_browse_history" => db_update
.user_browse_history
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"visual_novel_agent_message" => db_update
.visual_novel_agent_message
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"visual_novel_agent_session" => db_update
.visual_novel_agent_session
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"visual_novel_runtime_event" => db_update
.visual_novel_runtime_event
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"visual_novel_runtime_history_entry" => db_update
.visual_novel_runtime_history_entry
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"visual_novel_runtime_run" => db_update
.visual_novel_runtime_run
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"visual_novel_work_profile" => db_update
.visual_novel_work_profile
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
unknown => {
return Err(
__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(),
@@ -2861,6 +3036,24 @@ impl __sdk::DbUpdate for DbUpdate {
"user_browse_history" => db_update
.user_browse_history
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"visual_novel_agent_message" => db_update
.visual_novel_agent_message
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"visual_novel_agent_session" => db_update
.visual_novel_agent_session
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"visual_novel_runtime_event" => db_update
.visual_novel_runtime_event
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"visual_novel_runtime_history_entry" => db_update
.visual_novel_runtime_history_entry
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"visual_novel_runtime_run" => db_update
.visual_novel_runtime_run
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"visual_novel_work_profile" => db_update
.visual_novel_work_profile
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
unknown => {
return Err(
__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into(),
@@ -2947,6 +3140,13 @@ pub struct AppliedDiff<'r> {
treasure_record: __sdk::TableAppliedDiff<'r, TreasureRecord>,
user_account: __sdk::TableAppliedDiff<'r, UserAccount>,
user_browse_history: __sdk::TableAppliedDiff<'r, UserBrowseHistory>,
visual_novel_agent_message: __sdk::TableAppliedDiff<'r, VisualNovelAgentMessageRow>,
visual_novel_agent_session: __sdk::TableAppliedDiff<'r, VisualNovelAgentSessionRow>,
visual_novel_runtime_event: __sdk::TableAppliedDiff<'r, VisualNovelRuntimeEvent>,
visual_novel_runtime_history_entry:
__sdk::TableAppliedDiff<'r, VisualNovelRuntimeHistoryEntryRow>,
visual_novel_runtime_run: __sdk::TableAppliedDiff<'r, VisualNovelRuntimeRunRow>,
visual_novel_work_profile: __sdk::TableAppliedDiff<'r, VisualNovelWorkProfileRow>,
__unused: std::marker::PhantomData<&'r ()>,
}
@@ -3295,6 +3495,36 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.user_browse_history,
event,
);
callbacks.invoke_table_row_callbacks::<VisualNovelAgentMessageRow>(
"visual_novel_agent_message",
&self.visual_novel_agent_message,
event,
);
callbacks.invoke_table_row_callbacks::<VisualNovelAgentSessionRow>(
"visual_novel_agent_session",
&self.visual_novel_agent_session,
event,
);
callbacks.invoke_table_row_callbacks::<VisualNovelRuntimeEvent>(
"visual_novel_runtime_event",
&self.visual_novel_runtime_event,
event,
);
callbacks.invoke_table_row_callbacks::<VisualNovelRuntimeHistoryEntryRow>(
"visual_novel_runtime_history_entry",
&self.visual_novel_runtime_history_entry,
event,
);
callbacks.invoke_table_row_callbacks::<VisualNovelRuntimeRunRow>(
"visual_novel_runtime_run",
&self.visual_novel_runtime_run,
event,
);
callbacks.invoke_table_row_callbacks::<VisualNovelWorkProfileRow>(
"visual_novel_work_profile",
&self.visual_novel_work_profile,
event,
);
}
}
@@ -4026,6 +4256,12 @@ impl __sdk::SpacetimeModule for RemoteModule {
treasure_record_table::register_table(client_cache);
user_account_table::register_table(client_cache);
user_browse_history_table::register_table(client_cache);
visual_novel_agent_message_table::register_table(client_cache);
visual_novel_agent_session_table::register_table(client_cache);
visual_novel_runtime_event_table::register_table(client_cache);
visual_novel_runtime_history_entry_table::register_table(client_cache);
visual_novel_runtime_run_table::register_table(client_cache);
visual_novel_work_profile_table::register_table(client_cache);
}
const ALL_TABLE_NAMES: &'static [&'static str] = &[
"ai_result_reference",
@@ -4099,5 +4335,11 @@ impl __sdk::SpacetimeModule for RemoteModule {
"treasure_record",
"user_account",
"user_browse_history",
"visual_novel_agent_message",
"visual_novel_agent_session",
"visual_novel_runtime_event",
"visual_novel_runtime_history_entry",
"visual_novel_runtime_run",
"visual_novel_work_profile",
];
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_work_procedure_result_type::VisualNovelWorkProcedureResult;
use super::visual_novel_work_publish_input_type::VisualNovelWorkPublishInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct PublishVisualNovelWorkArgs {
pub input: VisualNovelWorkPublishInput,
}
impl __sdk::InModule for PublishVisualNovelWorkArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `publish_visual_novel_work`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait publish_visual_novel_work {
fn publish_visual_novel_work(&self, input: VisualNovelWorkPublishInput) {
self.publish_visual_novel_work_then(input, |_, _| {});
}
fn publish_visual_novel_work_then(
&self,
input: VisualNovelWorkPublishInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl publish_visual_novel_work for super::RemoteProcedures {
fn publish_visual_novel_work_then(
&self,
input: VisualNovelWorkPublishInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelWorkProcedureResult>(
"publish_visual_novel_work",
PublishVisualNovelWorkArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_runtime_event_procedure_result_type::VisualNovelRuntimeEventProcedureResult;
use super::visual_novel_runtime_event_record_input_type::VisualNovelRuntimeEventRecordInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct RecordVisualNovelRuntimeEventArgs {
pub input: VisualNovelRuntimeEventRecordInput,
}
impl __sdk::InModule for RecordVisualNovelRuntimeEventArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `record_visual_novel_runtime_event`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait record_visual_novel_runtime_event {
fn record_visual_novel_runtime_event(&self, input: VisualNovelRuntimeEventRecordInput) {
self.record_visual_novel_runtime_event_then(input, |_, _| {});
}
fn record_visual_novel_runtime_event_then(
&self,
input: VisualNovelRuntimeEventRecordInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRuntimeEventProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl record_visual_novel_runtime_event for super::RemoteProcedures {
fn record_visual_novel_runtime_event_then(
&self,
input: VisualNovelRuntimeEventRecordInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRuntimeEventProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelRuntimeEventProcedureResult>(
"record_visual_novel_runtime_event",
RecordVisualNovelRuntimeEventArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_run_procedure_result_type::VisualNovelRunProcedureResult;
use super::visual_novel_run_start_input_type::VisualNovelRunStartInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct StartVisualNovelRunArgs {
pub input: VisualNovelRunStartInput,
}
impl __sdk::InModule for StartVisualNovelRunArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `start_visual_novel_run`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait start_visual_novel_run {
fn start_visual_novel_run(&self, input: VisualNovelRunStartInput) {
self.start_visual_novel_run_then(input, |_, _| {});
}
fn start_visual_novel_run_then(
&self,
input: VisualNovelRunStartInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl start_visual_novel_run for super::RemoteProcedures {
fn start_visual_novel_run_then(
&self,
input: VisualNovelRunStartInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelRunProcedureResult>(
"start_visual_novel_run",
StartVisualNovelRunArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_agent_message_submit_input_type::VisualNovelAgentMessageSubmitInput;
use super::visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct SubmitVisualNovelAgentMessageArgs {
pub input: VisualNovelAgentMessageSubmitInput,
}
impl __sdk::InModule for SubmitVisualNovelAgentMessageArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `submit_visual_novel_agent_message`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait submit_visual_novel_agent_message {
fn submit_visual_novel_agent_message(&self, input: VisualNovelAgentMessageSubmitInput) {
self.submit_visual_novel_agent_message_then(input, |_, _| {});
}
fn submit_visual_novel_agent_message_then(
&self,
input: VisualNovelAgentMessageSubmitInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl submit_visual_novel_agent_message for super::RemoteProcedures {
fn submit_visual_novel_agent_message_then(
&self,
input: VisualNovelAgentMessageSubmitInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelAgentSessionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelAgentSessionProcedureResult>(
"submit_visual_novel_agent_message",
SubmitVisualNovelAgentMessageArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_work_procedure_result_type::VisualNovelWorkProcedureResult;
use super::visual_novel_work_update_input_type::VisualNovelWorkUpdateInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct UpdateVisualNovelWorkArgs {
pub input: VisualNovelWorkUpdateInput,
}
impl __sdk::InModule for UpdateVisualNovelWorkArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `update_visual_novel_work`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait update_visual_novel_work {
fn update_visual_novel_work(&self, input: VisualNovelWorkUpdateInput) {
self.update_visual_novel_work_then(input, |_, _| {});
}
fn update_visual_novel_work_then(
&self,
input: VisualNovelWorkUpdateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl update_visual_novel_work for super::RemoteProcedures {
fn update_visual_novel_work_then(
&self,
input: VisualNovelWorkUpdateInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelWorkProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelWorkProcedureResult>(
"update_visual_novel_work",
UpdateVisualNovelWorkArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,59 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::visual_novel_run_procedure_result_type::VisualNovelRunProcedureResult;
use super::visual_novel_run_snapshot_upsert_input_type::VisualNovelRunSnapshotUpsertInput;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct UpsertVisualNovelRunSnapshotArgs {
pub input: VisualNovelRunSnapshotUpsertInput,
}
impl __sdk::InModule for UpsertVisualNovelRunSnapshotArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `upsert_visual_novel_run_snapshot`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait upsert_visual_novel_run_snapshot {
fn upsert_visual_novel_run_snapshot(&self, input: VisualNovelRunSnapshotUpsertInput) {
self.upsert_visual_novel_run_snapshot_then(input, |_, _| {});
}
fn upsert_visual_novel_run_snapshot_then(
&self,
input: VisualNovelRunSnapshotUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl upsert_visual_novel_run_snapshot for super::RemoteProcedures {
fn upsert_visual_novel_run_snapshot_then(
&self,
input: VisualNovelRunSnapshotUpsertInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<VisualNovelRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, VisualNovelRunProcedureResult>(
"upsert_visual_novel_run_snapshot",
UpsertVisualNovelRunSnapshotArgs { input },
__callback,
);
}
}

View File

@@ -0,0 +1,24 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentMessageFinalizeInput {
pub session_id: String,
pub owner_user_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub draft_json: Option<String>,
pub pending_action_json: Option<String>,
pub status: String,
pub progress_percent: u32,
pub updated_at_micros: i64,
pub error_message: Option<String>,
}
impl __sdk::InModule for VisualNovelAgentMessageFinalizeInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,66 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentMessageRow {
pub message_id: String,
pub session_id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: __sdk::Timestamp,
}
impl __sdk::InModule for VisualNovelAgentMessageRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `VisualNovelAgentMessageRow`.
///
/// Provides typed access to columns for query building.
pub struct VisualNovelAgentMessageRowCols {
pub message_id: __sdk::__query_builder::Col<VisualNovelAgentMessageRow, String>,
pub session_id: __sdk::__query_builder::Col<VisualNovelAgentMessageRow, String>,
pub role: __sdk::__query_builder::Col<VisualNovelAgentMessageRow, String>,
pub kind: __sdk::__query_builder::Col<VisualNovelAgentMessageRow, String>,
pub text: __sdk::__query_builder::Col<VisualNovelAgentMessageRow, String>,
pub created_at: __sdk::__query_builder::Col<VisualNovelAgentMessageRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for VisualNovelAgentMessageRow {
type Cols = VisualNovelAgentMessageRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
VisualNovelAgentMessageRowCols {
message_id: __sdk::__query_builder::Col::new(table_name, "message_id"),
session_id: __sdk::__query_builder::Col::new(table_name, "session_id"),
role: __sdk::__query_builder::Col::new(table_name, "role"),
kind: __sdk::__query_builder::Col::new(table_name, "kind"),
text: __sdk::__query_builder::Col::new(table_name, "text"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
}
}
}
/// Indexed column accessor struct for the table `VisualNovelAgentMessageRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct VisualNovelAgentMessageRowIxCols {
pub message_id: __sdk::__query_builder::IxCol<VisualNovelAgentMessageRow, String>,
pub session_id: __sdk::__query_builder::IxCol<VisualNovelAgentMessageRow, String>,
}
impl __sdk::__query_builder::HasIxCols for VisualNovelAgentMessageRow {
type IxCols = VisualNovelAgentMessageRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
VisualNovelAgentMessageRowIxCols {
message_id: __sdk::__query_builder::IxCol::new(table_name, "message_id"),
session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for VisualNovelAgentMessageRow {}

View File

@@ -0,0 +1,19 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentMessageSubmitInput {
pub session_id: String,
pub owner_user_id: String,
pub user_message_id: String,
pub user_message_text: String,
pub submitted_at_micros: i64,
}
impl __sdk::InModule for VisualNovelAgentMessageSubmitInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,166 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::visual_novel_agent_message_row_type::VisualNovelAgentMessageRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `visual_novel_agent_message`.
///
/// Obtain a handle from the [`VisualNovelAgentMessageTableAccess::visual_novel_agent_message`] method on [`super::RemoteTables`],
/// like `ctx.db.visual_novel_agent_message()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_agent_message().on_insert(...)`.
pub struct VisualNovelAgentMessageTableHandle<'ctx> {
imp: __sdk::TableHandle<VisualNovelAgentMessageRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `visual_novel_agent_message`.
///
/// Implemented for [`super::RemoteTables`].
pub trait VisualNovelAgentMessageTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`VisualNovelAgentMessageTableHandle`], which mediates access to the table `visual_novel_agent_message`.
fn visual_novel_agent_message(&self) -> VisualNovelAgentMessageTableHandle<'_>;
}
impl VisualNovelAgentMessageTableAccess for super::RemoteTables {
fn visual_novel_agent_message(&self) -> VisualNovelAgentMessageTableHandle<'_> {
VisualNovelAgentMessageTableHandle {
imp: self
.imp
.get_table::<VisualNovelAgentMessageRow>("visual_novel_agent_message"),
ctx: std::marker::PhantomData,
}
}
}
pub struct VisualNovelAgentMessageInsertCallbackId(__sdk::CallbackId);
pub struct VisualNovelAgentMessageDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for VisualNovelAgentMessageTableHandle<'ctx> {
type Row = VisualNovelAgentMessageRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = VisualNovelAgentMessageRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = VisualNovelAgentMessageInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelAgentMessageInsertCallbackId {
VisualNovelAgentMessageInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: VisualNovelAgentMessageInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = VisualNovelAgentMessageDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelAgentMessageDeleteCallbackId {
VisualNovelAgentMessageDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: VisualNovelAgentMessageDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct VisualNovelAgentMessageUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for VisualNovelAgentMessageTableHandle<'ctx> {
type UpdateCallbackId = VisualNovelAgentMessageUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> VisualNovelAgentMessageUpdateCallbackId {
VisualNovelAgentMessageUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: VisualNovelAgentMessageUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `message_id` unique index on the table `visual_novel_agent_message`,
/// which allows point queries on the field of the same name
/// via the [`VisualNovelAgentMessageMessageIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_agent_message().message_id().find(...)`.
pub struct VisualNovelAgentMessageMessageIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<VisualNovelAgentMessageRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> VisualNovelAgentMessageTableHandle<'ctx> {
/// Get a handle on the `message_id` unique index on the table `visual_novel_agent_message`.
pub fn message_id(&self) -> VisualNovelAgentMessageMessageIdUnique<'ctx> {
VisualNovelAgentMessageMessageIdUnique {
imp: self.imp.get_unique_constraint::<String>("message_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> VisualNovelAgentMessageMessageIdUnique<'ctx> {
/// Find the subscribed row whose `message_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<VisualNovelAgentMessageRow> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<VisualNovelAgentMessageRow>("visual_novel_agent_message");
_table.add_unique_constraint::<String>("message_id", |row| &row.message_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<VisualNovelAgentMessageRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<VisualNovelAgentMessageRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `VisualNovelAgentMessageRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait visual_novel_agent_messageQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `VisualNovelAgentMessageRow`.
fn visual_novel_agent_message(
&self,
) -> __sdk::__query_builder::Table<VisualNovelAgentMessageRow>;
}
impl visual_novel_agent_messageQueryTableAccess for __sdk::QueryTableAccessor {
fn visual_novel_agent_message(
&self,
) -> __sdk::__query_builder::Table<VisualNovelAgentMessageRow> {
__sdk::__query_builder::Table::new("visual_novel_agent_message")
}
}

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentSessionCreateInput {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub seed_text: String,
pub source_asset_ids_json: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub draft_json: Option<String>,
pub created_at_micros: i64,
}
impl __sdk::InModule for VisualNovelAgentSessionCreateInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,16 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentSessionGetInput {
pub session_id: String,
pub owner_user_id: String,
}
impl __sdk::InModule for VisualNovelAgentSessionGetInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentSessionProcedureResult {
pub ok: bool,
pub session_json: Option<String>,
pub error_message: Option<String>,
}
impl __sdk::InModule for VisualNovelAgentSessionProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,102 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelAgentSessionRow {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub status: String,
pub seed_text: String,
pub source_asset_ids_json: String,
pub current_turn: u32,
pub progress_percent: u32,
pub draft_json: String,
pub pending_action_json: String,
pub last_assistant_reply: String,
pub published_profile_id: String,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for VisualNovelAgentSessionRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `VisualNovelAgentSessionRow`.
///
/// Provides typed access to columns for query building.
pub struct VisualNovelAgentSessionRowCols {
pub session_id: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub source_mode: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub status: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub seed_text: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub source_asset_ids_json: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub current_turn: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, u32>,
pub progress_percent: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, u32>,
pub draft_json: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub pending_action_json: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub last_assistant_reply: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub published_profile_id: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, String>,
pub created_at: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<VisualNovelAgentSessionRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for VisualNovelAgentSessionRow {
type Cols = VisualNovelAgentSessionRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
VisualNovelAgentSessionRowCols {
session_id: __sdk::__query_builder::Col::new(table_name, "session_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
source_mode: __sdk::__query_builder::Col::new(table_name, "source_mode"),
status: __sdk::__query_builder::Col::new(table_name, "status"),
seed_text: __sdk::__query_builder::Col::new(table_name, "seed_text"),
source_asset_ids_json: __sdk::__query_builder::Col::new(
table_name,
"source_asset_ids_json",
),
current_turn: __sdk::__query_builder::Col::new(table_name, "current_turn"),
progress_percent: __sdk::__query_builder::Col::new(table_name, "progress_percent"),
draft_json: __sdk::__query_builder::Col::new(table_name, "draft_json"),
pending_action_json: __sdk::__query_builder::Col::new(
table_name,
"pending_action_json",
),
last_assistant_reply: __sdk::__query_builder::Col::new(
table_name,
"last_assistant_reply",
),
published_profile_id: __sdk::__query_builder::Col::new(
table_name,
"published_profile_id",
),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `VisualNovelAgentSessionRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct VisualNovelAgentSessionRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<VisualNovelAgentSessionRow, String>,
pub session_id: __sdk::__query_builder::IxCol<VisualNovelAgentSessionRow, String>,
}
impl __sdk::__query_builder::HasIxCols for VisualNovelAgentSessionRow {
type IxCols = VisualNovelAgentSessionRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
VisualNovelAgentSessionRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for VisualNovelAgentSessionRow {}

View File

@@ -0,0 +1,166 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::visual_novel_agent_session_row_type::VisualNovelAgentSessionRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `visual_novel_agent_session`.
///
/// Obtain a handle from the [`VisualNovelAgentSessionTableAccess::visual_novel_agent_session`] method on [`super::RemoteTables`],
/// like `ctx.db.visual_novel_agent_session()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_agent_session().on_insert(...)`.
pub struct VisualNovelAgentSessionTableHandle<'ctx> {
imp: __sdk::TableHandle<VisualNovelAgentSessionRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `visual_novel_agent_session`.
///
/// Implemented for [`super::RemoteTables`].
pub trait VisualNovelAgentSessionTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`VisualNovelAgentSessionTableHandle`], which mediates access to the table `visual_novel_agent_session`.
fn visual_novel_agent_session(&self) -> VisualNovelAgentSessionTableHandle<'_>;
}
impl VisualNovelAgentSessionTableAccess for super::RemoteTables {
fn visual_novel_agent_session(&self) -> VisualNovelAgentSessionTableHandle<'_> {
VisualNovelAgentSessionTableHandle {
imp: self
.imp
.get_table::<VisualNovelAgentSessionRow>("visual_novel_agent_session"),
ctx: std::marker::PhantomData,
}
}
}
pub struct VisualNovelAgentSessionInsertCallbackId(__sdk::CallbackId);
pub struct VisualNovelAgentSessionDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for VisualNovelAgentSessionTableHandle<'ctx> {
type Row = VisualNovelAgentSessionRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = VisualNovelAgentSessionRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = VisualNovelAgentSessionInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelAgentSessionInsertCallbackId {
VisualNovelAgentSessionInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: VisualNovelAgentSessionInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = VisualNovelAgentSessionDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelAgentSessionDeleteCallbackId {
VisualNovelAgentSessionDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: VisualNovelAgentSessionDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct VisualNovelAgentSessionUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for VisualNovelAgentSessionTableHandle<'ctx> {
type UpdateCallbackId = VisualNovelAgentSessionUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> VisualNovelAgentSessionUpdateCallbackId {
VisualNovelAgentSessionUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: VisualNovelAgentSessionUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `session_id` unique index on the table `visual_novel_agent_session`,
/// which allows point queries on the field of the same name
/// via the [`VisualNovelAgentSessionSessionIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_agent_session().session_id().find(...)`.
pub struct VisualNovelAgentSessionSessionIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<VisualNovelAgentSessionRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> VisualNovelAgentSessionTableHandle<'ctx> {
/// Get a handle on the `session_id` unique index on the table `visual_novel_agent_session`.
pub fn session_id(&self) -> VisualNovelAgentSessionSessionIdUnique<'ctx> {
VisualNovelAgentSessionSessionIdUnique {
imp: self.imp.get_unique_constraint::<String>("session_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> VisualNovelAgentSessionSessionIdUnique<'ctx> {
/// Find the subscribed row whose `session_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<VisualNovelAgentSessionRow> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<VisualNovelAgentSessionRow>("visual_novel_agent_session");
_table.add_unique_constraint::<String>("session_id", |row| &row.session_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<VisualNovelAgentSessionRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<VisualNovelAgentSessionRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `VisualNovelAgentSessionRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait visual_novel_agent_sessionQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `VisualNovelAgentSessionRow`.
fn visual_novel_agent_session(
&self,
) -> __sdk::__query_builder::Table<VisualNovelAgentSessionRow>;
}
impl visual_novel_agent_sessionQueryTableAccess for __sdk::QueryTableAccessor {
fn visual_novel_agent_session(
&self,
) -> __sdk::__query_builder::Table<VisualNovelAgentSessionRow> {
__sdk::__query_builder::Table::new("visual_novel_agent_session")
}
}

View File

@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelHistoryProcedureResult {
pub ok: bool,
pub items_json: Option<String>,
pub error_message: Option<String>,
}
impl __sdk::InModule for VisualNovelHistoryProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,16 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRunGetInput {
pub run_id: String,
pub owner_user_id: String,
}
impl __sdk::InModule for VisualNovelRunGetInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRunProcedureResult {
pub ok: bool,
pub run_json: Option<String>,
pub error_message: Option<String>,
}
impl __sdk::InModule for VisualNovelRunProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,26 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRunSnapshotUpsertInput {
pub run_id: String,
pub owner_user_id: String,
pub status: String,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub visible_character_ids_json: String,
pub flags_json: String,
pub metrics_json: String,
pub available_choices_json: String,
pub text_mode_enabled: bool,
pub snapshot_json: Option<String>,
pub updated_at_micros: i64,
}
impl __sdk::InModule for VisualNovelRunSnapshotUpsertInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,20 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRunStartInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: String,
pub snapshot_json: Option<String>,
pub started_at_micros: i64,
}
impl __sdk::InModule for VisualNovelRunStartInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeEventProcedureResult {
pub ok: bool,
pub event_json: Option<String>,
pub error_message: Option<String>,
}
impl __sdk::InModule for VisualNovelRuntimeEventProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,23 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeEventRecordInput {
pub event_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: Option<String>,
pub event_kind: String,
pub client_event_id: Option<String>,
pub history_entry_id: Option<String>,
pub payload_json: String,
pub occurred_at_micros: i64,
}
impl __sdk::InModule for VisualNovelRuntimeEventRecordInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,101 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::visual_novel_runtime_event_type::VisualNovelRuntimeEvent;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `visual_novel_runtime_event`.
///
/// Obtain a handle from the [`VisualNovelRuntimeEventTableAccess::visual_novel_runtime_event`] method on [`super::RemoteTables`],
/// like `ctx.db.visual_novel_runtime_event()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_runtime_event().on_insert(...)`.
pub struct VisualNovelRuntimeEventTableHandle<'ctx> {
imp: __sdk::TableHandle<VisualNovelRuntimeEvent>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `visual_novel_runtime_event`.
///
/// Implemented for [`super::RemoteTables`].
pub trait VisualNovelRuntimeEventTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`VisualNovelRuntimeEventTableHandle`], which mediates access to the table `visual_novel_runtime_event`.
fn visual_novel_runtime_event(&self) -> VisualNovelRuntimeEventTableHandle<'_>;
}
impl VisualNovelRuntimeEventTableAccess for super::RemoteTables {
fn visual_novel_runtime_event(&self) -> VisualNovelRuntimeEventTableHandle<'_> {
VisualNovelRuntimeEventTableHandle {
imp: self
.imp
.get_table::<VisualNovelRuntimeEvent>("visual_novel_runtime_event"),
ctx: std::marker::PhantomData,
}
}
}
pub struct VisualNovelRuntimeEventInsertCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::EventTable for VisualNovelRuntimeEventTableHandle<'ctx> {
type Row = VisualNovelRuntimeEvent;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = VisualNovelRuntimeEvent> + '_ {
self.imp.iter()
}
type InsertCallbackId = VisualNovelRuntimeEventInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeEventInsertCallbackId {
VisualNovelRuntimeEventInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: VisualNovelRuntimeEventInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<VisualNovelRuntimeEvent>("visual_novel_runtime_event");
_table.add_unique_constraint::<String>("event_id", |row| &row.event_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<VisualNovelRuntimeEvent>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<VisualNovelRuntimeEvent>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `VisualNovelRuntimeEvent`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait visual_novel_runtime_eventQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `VisualNovelRuntimeEvent`.
fn visual_novel_runtime_event(&self) -> __sdk::__query_builder::Table<VisualNovelRuntimeEvent>;
}
impl visual_novel_runtime_eventQueryTableAccess for __sdk::QueryTableAccessor {
fn visual_novel_runtime_event(&self) -> __sdk::__query_builder::Table<VisualNovelRuntimeEvent> {
__sdk::__query_builder::Table::new("visual_novel_runtime_event")
}
}

View File

@@ -0,0 +1,75 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeEvent {
pub event_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub event_kind: String,
pub client_event_id: String,
pub history_entry_id: String,
pub payload_json: String,
pub occurred_at: __sdk::Timestamp,
}
impl __sdk::InModule for VisualNovelRuntimeEvent {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `VisualNovelRuntimeEvent`.
///
/// Provides typed access to columns for query building.
pub struct VisualNovelRuntimeEventCols {
pub event_id: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub run_id: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub owner_user_id: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub profile_id: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub event_kind: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub client_event_id: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub history_entry_id: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub payload_json: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, String>,
pub occurred_at: __sdk::__query_builder::Col<VisualNovelRuntimeEvent, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for VisualNovelRuntimeEvent {
type Cols = VisualNovelRuntimeEventCols;
fn cols(table_name: &'static str) -> Self::Cols {
VisualNovelRuntimeEventCols {
event_id: __sdk::__query_builder::Col::new(table_name, "event_id"),
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"),
event_kind: __sdk::__query_builder::Col::new(table_name, "event_kind"),
client_event_id: __sdk::__query_builder::Col::new(table_name, "client_event_id"),
history_entry_id: __sdk::__query_builder::Col::new(table_name, "history_entry_id"),
payload_json: __sdk::__query_builder::Col::new(table_name, "payload_json"),
occurred_at: __sdk::__query_builder::Col::new(table_name, "occurred_at"),
}
}
}
/// Indexed column accessor struct for the table `VisualNovelRuntimeEvent`.
///
/// Provides typed access to indexed columns for query building.
pub struct VisualNovelRuntimeEventIxCols {
pub event_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeEvent, String>,
pub owner_user_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeEvent, String>,
pub run_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeEvent, String>,
}
impl __sdk::__query_builder::HasIxCols for VisualNovelRuntimeEvent {
type IxCols = VisualNovelRuntimeEventIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
VisualNovelRuntimeEventIxCols {
event_id: __sdk::__query_builder::IxCol::new(table_name, "event_id"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"),
}
}
}

View File

@@ -0,0 +1,24 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeHistoryAppendInput {
pub entry_id: String,
pub run_id: String,
pub owner_user_id: String,
pub turn_index: u32,
pub source: String,
pub action_text: Option<String>,
pub steps_json: String,
pub snapshot_before_hash: Option<String>,
pub snapshot_after_hash: Option<String>,
pub created_at_micros: i64,
}
impl __sdk::InModule for VisualNovelRuntimeHistoryAppendInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,91 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeHistoryEntryRow {
pub entry_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub turn_index: u32,
pub source: String,
pub action_text: String,
pub steps_json: String,
pub snapshot_before_hash: String,
pub snapshot_after_hash: String,
pub created_at: __sdk::Timestamp,
}
impl __sdk::InModule for VisualNovelRuntimeHistoryEntryRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `VisualNovelRuntimeHistoryEntryRow`.
///
/// Provides typed access to columns for query building.
pub struct VisualNovelRuntimeHistoryEntryRowCols {
pub entry_id: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub run_id: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub profile_id: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub turn_index: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, u32>,
pub source: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub action_text: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub steps_json: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub snapshot_before_hash:
__sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub snapshot_after_hash: __sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, String>,
pub created_at:
__sdk::__query_builder::Col<VisualNovelRuntimeHistoryEntryRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for VisualNovelRuntimeHistoryEntryRow {
type Cols = VisualNovelRuntimeHistoryEntryRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
VisualNovelRuntimeHistoryEntryRowCols {
entry_id: __sdk::__query_builder::Col::new(table_name, "entry_id"),
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"),
turn_index: __sdk::__query_builder::Col::new(table_name, "turn_index"),
source: __sdk::__query_builder::Col::new(table_name, "source"),
action_text: __sdk::__query_builder::Col::new(table_name, "action_text"),
steps_json: __sdk::__query_builder::Col::new(table_name, "steps_json"),
snapshot_before_hash: __sdk::__query_builder::Col::new(
table_name,
"snapshot_before_hash",
),
snapshot_after_hash: __sdk::__query_builder::Col::new(
table_name,
"snapshot_after_hash",
),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
}
}
}
/// Indexed column accessor struct for the table `VisualNovelRuntimeHistoryEntryRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct VisualNovelRuntimeHistoryEntryRowIxCols {
pub entry_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeHistoryEntryRow, String>,
pub owner_user_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeHistoryEntryRow, String>,
pub run_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeHistoryEntryRow, String>,
}
impl __sdk::__query_builder::HasIxCols for VisualNovelRuntimeHistoryEntryRow {
type IxCols = VisualNovelRuntimeHistoryEntryRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
VisualNovelRuntimeHistoryEntryRowIxCols {
entry_id: __sdk::__query_builder::IxCol::new(table_name, "entry_id"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for VisualNovelRuntimeHistoryEntryRow {}

View File

@@ -0,0 +1,170 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::visual_novel_runtime_history_entry_row_type::VisualNovelRuntimeHistoryEntryRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `visual_novel_runtime_history_entry`.
///
/// Obtain a handle from the [`VisualNovelRuntimeHistoryEntryTableAccess::visual_novel_runtime_history_entry`] method on [`super::RemoteTables`],
/// like `ctx.db.visual_novel_runtime_history_entry()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_runtime_history_entry().on_insert(...)`.
pub struct VisualNovelRuntimeHistoryEntryTableHandle<'ctx> {
imp: __sdk::TableHandle<VisualNovelRuntimeHistoryEntryRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `visual_novel_runtime_history_entry`.
///
/// Implemented for [`super::RemoteTables`].
pub trait VisualNovelRuntimeHistoryEntryTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`VisualNovelRuntimeHistoryEntryTableHandle`], which mediates access to the table `visual_novel_runtime_history_entry`.
fn visual_novel_runtime_history_entry(&self) -> VisualNovelRuntimeHistoryEntryTableHandle<'_>;
}
impl VisualNovelRuntimeHistoryEntryTableAccess for super::RemoteTables {
fn visual_novel_runtime_history_entry(&self) -> VisualNovelRuntimeHistoryEntryTableHandle<'_> {
VisualNovelRuntimeHistoryEntryTableHandle {
imp: self.imp.get_table::<VisualNovelRuntimeHistoryEntryRow>(
"visual_novel_runtime_history_entry",
),
ctx: std::marker::PhantomData,
}
}
}
pub struct VisualNovelRuntimeHistoryEntryInsertCallbackId(__sdk::CallbackId);
pub struct VisualNovelRuntimeHistoryEntryDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for VisualNovelRuntimeHistoryEntryTableHandle<'ctx> {
type Row = VisualNovelRuntimeHistoryEntryRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = VisualNovelRuntimeHistoryEntryRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = VisualNovelRuntimeHistoryEntryInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeHistoryEntryInsertCallbackId {
VisualNovelRuntimeHistoryEntryInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: VisualNovelRuntimeHistoryEntryInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = VisualNovelRuntimeHistoryEntryDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeHistoryEntryDeleteCallbackId {
VisualNovelRuntimeHistoryEntryDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: VisualNovelRuntimeHistoryEntryDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct VisualNovelRuntimeHistoryEntryUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for VisualNovelRuntimeHistoryEntryTableHandle<'ctx> {
type UpdateCallbackId = VisualNovelRuntimeHistoryEntryUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeHistoryEntryUpdateCallbackId {
VisualNovelRuntimeHistoryEntryUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: VisualNovelRuntimeHistoryEntryUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `entry_id` unique index on the table `visual_novel_runtime_history_entry`,
/// which allows point queries on the field of the same name
/// via the [`VisualNovelRuntimeHistoryEntryEntryIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_runtime_history_entry().entry_id().find(...)`.
pub struct VisualNovelRuntimeHistoryEntryEntryIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<VisualNovelRuntimeHistoryEntryRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> VisualNovelRuntimeHistoryEntryTableHandle<'ctx> {
/// Get a handle on the `entry_id` unique index on the table `visual_novel_runtime_history_entry`.
pub fn entry_id(&self) -> VisualNovelRuntimeHistoryEntryEntryIdUnique<'ctx> {
VisualNovelRuntimeHistoryEntryEntryIdUnique {
imp: self.imp.get_unique_constraint::<String>("entry_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> VisualNovelRuntimeHistoryEntryEntryIdUnique<'ctx> {
/// Find the subscribed row whose `entry_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<VisualNovelRuntimeHistoryEntryRow> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<VisualNovelRuntimeHistoryEntryRow>(
"visual_novel_runtime_history_entry",
);
_table.add_unique_constraint::<String>("entry_id", |row| &row.entry_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<VisualNovelRuntimeHistoryEntryRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<VisualNovelRuntimeHistoryEntryRow>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `VisualNovelRuntimeHistoryEntryRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait visual_novel_runtime_history_entryQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `VisualNovelRuntimeHistoryEntryRow`.
fn visual_novel_runtime_history_entry(
&self,
) -> __sdk::__query_builder::Table<VisualNovelRuntimeHistoryEntryRow>;
}
impl visual_novel_runtime_history_entryQueryTableAccess for __sdk::QueryTableAccessor {
fn visual_novel_runtime_history_entry(
&self,
) -> __sdk::__query_builder::Table<VisualNovelRuntimeHistoryEntryRow> {
__sdk::__query_builder::Table::new("visual_novel_runtime_history_entry")
}
}

View File

@@ -0,0 +1,16 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeHistoryListInput {
pub run_id: String,
pub owner_user_id: String,
}
impl __sdk::InModule for VisualNovelRuntimeHistoryListInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,101 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelRuntimeRunRow {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: String,
pub status: String,
pub current_scene_id: String,
pub current_phase_id: String,
pub visible_character_ids_json: String,
pub flags_json: String,
pub metrics_json: String,
pub available_choices_json: String,
pub text_mode_enabled: bool,
pub snapshot_json: String,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for VisualNovelRuntimeRunRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `VisualNovelRuntimeRunRow`.
///
/// Provides typed access to columns for query building.
pub struct VisualNovelRuntimeRunRowCols {
pub run_id: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub profile_id: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub mode: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub status: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub current_scene_id: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub current_phase_id: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub visible_character_ids_json: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub flags_json: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub metrics_json: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub available_choices_json: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub text_mode_enabled: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, bool>,
pub snapshot_json: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, String>,
pub created_at: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<VisualNovelRuntimeRunRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for VisualNovelRuntimeRunRow {
type Cols = VisualNovelRuntimeRunRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
VisualNovelRuntimeRunRowCols {
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"),
mode: __sdk::__query_builder::Col::new(table_name, "mode"),
status: __sdk::__query_builder::Col::new(table_name, "status"),
current_scene_id: __sdk::__query_builder::Col::new(table_name, "current_scene_id"),
current_phase_id: __sdk::__query_builder::Col::new(table_name, "current_phase_id"),
visible_character_ids_json: __sdk::__query_builder::Col::new(
table_name,
"visible_character_ids_json",
),
flags_json: __sdk::__query_builder::Col::new(table_name, "flags_json"),
metrics_json: __sdk::__query_builder::Col::new(table_name, "metrics_json"),
available_choices_json: __sdk::__query_builder::Col::new(
table_name,
"available_choices_json",
),
text_mode_enabled: __sdk::__query_builder::Col::new(table_name, "text_mode_enabled"),
snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_json"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `VisualNovelRuntimeRunRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct VisualNovelRuntimeRunRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeRunRow, String>,
pub profile_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeRunRow, String>,
pub run_id: __sdk::__query_builder::IxCol<VisualNovelRuntimeRunRow, String>,
}
impl __sdk::__query_builder::HasIxCols for VisualNovelRuntimeRunRow {
type IxCols = VisualNovelRuntimeRunRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
VisualNovelRuntimeRunRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"),
run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for VisualNovelRuntimeRunRow {}

View File

@@ -0,0 +1,162 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::visual_novel_runtime_run_row_type::VisualNovelRuntimeRunRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `visual_novel_runtime_run`.
///
/// Obtain a handle from the [`VisualNovelRuntimeRunTableAccess::visual_novel_runtime_run`] method on [`super::RemoteTables`],
/// like `ctx.db.visual_novel_runtime_run()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_runtime_run().on_insert(...)`.
pub struct VisualNovelRuntimeRunTableHandle<'ctx> {
imp: __sdk::TableHandle<VisualNovelRuntimeRunRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `visual_novel_runtime_run`.
///
/// Implemented for [`super::RemoteTables`].
pub trait VisualNovelRuntimeRunTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`VisualNovelRuntimeRunTableHandle`], which mediates access to the table `visual_novel_runtime_run`.
fn visual_novel_runtime_run(&self) -> VisualNovelRuntimeRunTableHandle<'_>;
}
impl VisualNovelRuntimeRunTableAccess for super::RemoteTables {
fn visual_novel_runtime_run(&self) -> VisualNovelRuntimeRunTableHandle<'_> {
VisualNovelRuntimeRunTableHandle {
imp: self
.imp
.get_table::<VisualNovelRuntimeRunRow>("visual_novel_runtime_run"),
ctx: std::marker::PhantomData,
}
}
}
pub struct VisualNovelRuntimeRunInsertCallbackId(__sdk::CallbackId);
pub struct VisualNovelRuntimeRunDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for VisualNovelRuntimeRunTableHandle<'ctx> {
type Row = VisualNovelRuntimeRunRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = VisualNovelRuntimeRunRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = VisualNovelRuntimeRunInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeRunInsertCallbackId {
VisualNovelRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: VisualNovelRuntimeRunInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = VisualNovelRuntimeRunDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeRunDeleteCallbackId {
VisualNovelRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: VisualNovelRuntimeRunDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct VisualNovelRuntimeRunUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for VisualNovelRuntimeRunTableHandle<'ctx> {
type UpdateCallbackId = VisualNovelRuntimeRunUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> VisualNovelRuntimeRunUpdateCallbackId {
VisualNovelRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: VisualNovelRuntimeRunUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `run_id` unique index on the table `visual_novel_runtime_run`,
/// which allows point queries on the field of the same name
/// via the [`VisualNovelRuntimeRunRunIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_runtime_run().run_id().find(...)`.
pub struct VisualNovelRuntimeRunRunIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<VisualNovelRuntimeRunRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> VisualNovelRuntimeRunTableHandle<'ctx> {
/// Get a handle on the `run_id` unique index on the table `visual_novel_runtime_run`.
pub fn run_id(&self) -> VisualNovelRuntimeRunRunIdUnique<'ctx> {
VisualNovelRuntimeRunRunIdUnique {
imp: self.imp.get_unique_constraint::<String>("run_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> VisualNovelRuntimeRunRunIdUnique<'ctx> {
/// Find the subscribed row whose `run_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<VisualNovelRuntimeRunRow> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<VisualNovelRuntimeRunRow>("visual_novel_runtime_run");
_table.add_unique_constraint::<String>("run_id", |row| &row.run_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<VisualNovelRuntimeRunRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<VisualNovelRuntimeRunRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `VisualNovelRuntimeRunRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait visual_novel_runtime_runQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `VisualNovelRuntimeRunRow`.
fn visual_novel_runtime_run(&self) -> __sdk::__query_builder::Table<VisualNovelRuntimeRunRow>;
}
impl visual_novel_runtime_runQueryTableAccess for __sdk::QueryTableAccessor {
fn visual_novel_runtime_run(&self) -> __sdk::__query_builder::Table<VisualNovelRuntimeRunRow> {
__sdk::__query_builder::Table::new("visual_novel_runtime_run")
}
}

View File

@@ -0,0 +1,24 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelWorkCompileInput {
pub session_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub work_id: Option<String>,
pub author_display_name: String,
pub work_title: Option<String>,
pub work_description: Option<String>,
pub tags_json: Option<String>,
pub cover_image_src: Option<String>,
pub compiled_at_micros: i64,
}
impl __sdk::InModule for VisualNovelWorkCompileInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,16 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelWorkDeleteInput {
pub profile_id: String,
pub owner_user_id: String,
}
impl __sdk::InModule for VisualNovelWorkDeleteInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,16 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelWorkGetInput {
pub profile_id: String,
pub owner_user_id: String,
}
impl __sdk::InModule for VisualNovelWorkGetInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,17 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelWorkProcedureResult {
pub ok: bool,
pub work_json: Option<String>,
pub error_message: Option<String>,
}
impl __sdk::InModule for VisualNovelWorkProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,111 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct VisualNovelWorkProfileRow {
pub profile_id: String,
pub work_id: String,
pub owner_user_id: String,
pub source_session_id: String,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub tags_json: String,
pub cover_image_src: String,
pub source_asset_ids_json: String,
pub draft_json: String,
pub publication_status: String,
pub publish_ready: bool,
pub play_count: u32,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
pub published_at: Option<__sdk::Timestamp>,
}
impl __sdk::InModule for VisualNovelWorkProfileRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `VisualNovelWorkProfileRow`.
///
/// Provides typed access to columns for query building.
pub struct VisualNovelWorkProfileRowCols {
pub profile_id: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub work_id: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub source_session_id: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub author_display_name: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub work_title: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub work_description: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub tags_json: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub cover_image_src: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub source_asset_ids_json: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub draft_json: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub publication_status: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, String>,
pub publish_ready: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, bool>,
pub play_count: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, u32>,
pub created_at: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, __sdk::Timestamp>,
pub published_at:
__sdk::__query_builder::Col<VisualNovelWorkProfileRow, Option<__sdk::Timestamp>>,
}
impl __sdk::__query_builder::HasCols for VisualNovelWorkProfileRow {
type Cols = VisualNovelWorkProfileRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
VisualNovelWorkProfileRowCols {
profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"),
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"),
author_display_name: __sdk::__query_builder::Col::new(
table_name,
"author_display_name",
),
work_title: __sdk::__query_builder::Col::new(table_name, "work_title"),
work_description: __sdk::__query_builder::Col::new(table_name, "work_description"),
tags_json: __sdk::__query_builder::Col::new(table_name, "tags_json"),
cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"),
source_asset_ids_json: __sdk::__query_builder::Col::new(
table_name,
"source_asset_ids_json",
),
draft_json: __sdk::__query_builder::Col::new(table_name, "draft_json"),
publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"),
publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"),
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
}
}
}
/// Indexed column accessor struct for the table `VisualNovelWorkProfileRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct VisualNovelWorkProfileRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<VisualNovelWorkProfileRow, String>,
pub profile_id: __sdk::__query_builder::IxCol<VisualNovelWorkProfileRow, String>,
pub publication_status: __sdk::__query_builder::IxCol<VisualNovelWorkProfileRow, String>,
}
impl __sdk::__query_builder::HasIxCols for VisualNovelWorkProfileRow {
type IxCols = VisualNovelWorkProfileRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
VisualNovelWorkProfileRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
profile_id: __sdk::__query_builder::IxCol::new(table_name, "profile_id"),
publication_status: __sdk::__query_builder::IxCol::new(
table_name,
"publication_status",
),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for VisualNovelWorkProfileRow {}

View File

@@ -0,0 +1,165 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::visual_novel_work_profile_row_type::VisualNovelWorkProfileRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `visual_novel_work_profile`.
///
/// Obtain a handle from the [`VisualNovelWorkProfileTableAccess::visual_novel_work_profile`] method on [`super::RemoteTables`],
/// like `ctx.db.visual_novel_work_profile()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_work_profile().on_insert(...)`.
pub struct VisualNovelWorkProfileTableHandle<'ctx> {
imp: __sdk::TableHandle<VisualNovelWorkProfileRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `visual_novel_work_profile`.
///
/// Implemented for [`super::RemoteTables`].
pub trait VisualNovelWorkProfileTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`VisualNovelWorkProfileTableHandle`], which mediates access to the table `visual_novel_work_profile`.
fn visual_novel_work_profile(&self) -> VisualNovelWorkProfileTableHandle<'_>;
}
impl VisualNovelWorkProfileTableAccess for super::RemoteTables {
fn visual_novel_work_profile(&self) -> VisualNovelWorkProfileTableHandle<'_> {
VisualNovelWorkProfileTableHandle {
imp: self
.imp
.get_table::<VisualNovelWorkProfileRow>("visual_novel_work_profile"),
ctx: std::marker::PhantomData,
}
}
}
pub struct VisualNovelWorkProfileInsertCallbackId(__sdk::CallbackId);
pub struct VisualNovelWorkProfileDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for VisualNovelWorkProfileTableHandle<'ctx> {
type Row = VisualNovelWorkProfileRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = VisualNovelWorkProfileRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = VisualNovelWorkProfileInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelWorkProfileInsertCallbackId {
VisualNovelWorkProfileInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: VisualNovelWorkProfileInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = VisualNovelWorkProfileDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> VisualNovelWorkProfileDeleteCallbackId {
VisualNovelWorkProfileDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: VisualNovelWorkProfileDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct VisualNovelWorkProfileUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for VisualNovelWorkProfileTableHandle<'ctx> {
type UpdateCallbackId = VisualNovelWorkProfileUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> VisualNovelWorkProfileUpdateCallbackId {
VisualNovelWorkProfileUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: VisualNovelWorkProfileUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `profile_id` unique index on the table `visual_novel_work_profile`,
/// which allows point queries on the field of the same name
/// via the [`VisualNovelWorkProfileProfileIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.visual_novel_work_profile().profile_id().find(...)`.
pub struct VisualNovelWorkProfileProfileIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<VisualNovelWorkProfileRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> VisualNovelWorkProfileTableHandle<'ctx> {
/// Get a handle on the `profile_id` unique index on the table `visual_novel_work_profile`.
pub fn profile_id(&self) -> VisualNovelWorkProfileProfileIdUnique<'ctx> {
VisualNovelWorkProfileProfileIdUnique {
imp: self.imp.get_unique_constraint::<String>("profile_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> VisualNovelWorkProfileProfileIdUnique<'ctx> {
/// Find the subscribed row whose `profile_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<VisualNovelWorkProfileRow> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table =
client_cache.get_or_make_table::<VisualNovelWorkProfileRow>("visual_novel_work_profile");
_table.add_unique_constraint::<String>("profile_id", |row| &row.profile_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<VisualNovelWorkProfileRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<VisualNovelWorkProfileRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `VisualNovelWorkProfileRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait visual_novel_work_profileQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `VisualNovelWorkProfileRow`.
fn visual_novel_work_profile(&self)
-> __sdk::__query_builder::Table<VisualNovelWorkProfileRow>;
}
impl visual_novel_work_profileQueryTableAccess for __sdk::QueryTableAccessor {
fn visual_novel_work_profile(
&self,
) -> __sdk::__query_builder::Table<VisualNovelWorkProfileRow> {
__sdk::__query_builder::Table::new("visual_novel_work_profile")
}
}

Some files were not shown because too many files have changed in this diff Show More