1
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("&", "&")
|
||||
}
|
||||
|
||||
fn normalize_document_text(value: &str) -> String {
|
||||
value
|
||||
.trim_start_matches('\u{feff}')
|
||||
|
||||
1434
server-rs/crates/api-server/src/creative_agent.rs
Normal file
1434
server-rs/crates/api-server/src/creative_agent.rs
Normal file
File diff suppressed because it is too large
Load Diff
69
server-rs/crates/api-server/src/creative_agent_sse.rs
Normal file
69
server-rs/crates/api-server/src/creative_agent_sse.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
@@ -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", "请求参数不合法"),
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
690
server-rs/crates/api-server/src/prompt/visual_novel.rs
Normal file
690
server-rs/crates/api-server/src/prompt/visual_novel.rs
Normal 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. 需要玩家选择时必须输出 choice,choice 内每项必须有 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()));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1767
server-rs/crates/api-server/src/visual_novel.rs
Normal file
1767
server-rs/crates/api-server/src/visual_novel.rs
Normal file
File diff suppressed because it is too large
Load Diff
15
server-rs/crates/module-creative-agent/Cargo.toml
Normal file
15
server-rs/crates/module-creative-agent/Cargo.toml
Normal 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 }
|
||||
225
server-rs/crates/module-creative-agent/src/application.rs
Normal file
225
server-rs/crates/module-creative-agent/src/application.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
89
server-rs/crates/module-creative-agent/src/commands.rs
Normal file
89
server-rs/crates/module-creative-agent/src/commands.rs
Normal 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,
|
||||
}
|
||||
187
server-rs/crates/module-creative-agent/src/domain.rs
Normal file
187
server-rs/crates/module-creative-agent/src/domain.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
35
server-rs/crates/module-creative-agent/src/errors.rs
Normal file
35
server-rs/crates/module-creative-agent/src/errors.rs
Normal 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 {}
|
||||
9
server-rs/crates/module-creative-agent/src/lib.rs
Normal file
9
server-rs/crates/module-creative-agent/src/lib.rs
Normal 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::*;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()),
|
||||
|
||||
208
server-rs/crates/module-puzzle/src/creative_templates.rs
Normal file
208
server-rs/crates/module-puzzle/src/creative_templates.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
529
server-rs/crates/module-puzzle/src/creative_tools.rs
Normal file
529
server-rs/crates/module-puzzle/src/creative_tools.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
server-rs/crates/module-visual-novel/Cargo.toml
Normal file
15
server-rs/crates/module-visual-novel/Cargo.toml
Normal 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 }
|
||||
935
server-rs/crates/module-visual-novel/src/application.rs
Normal file
935
server-rs/crates/module-visual-novel/src/application.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
390
server-rs/crates/module-visual-novel/src/domain.rs
Normal file
390
server-rs/crates/module-visual-novel/src/domain.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
41
server-rs/crates/module-visual-novel/src/errors.rs
Normal file
41
server-rs/crates/module-visual-novel/src/errors.rs
Normal 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 {}
|
||||
7
server-rs/crates/module-visual-novel/src/lib.rs
Normal file
7
server-rs/crates/module-visual-novel/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod application;
|
||||
mod domain;
|
||||
mod errors;
|
||||
|
||||
pub use application::*;
|
||||
pub use domain::*;
|
||||
pub use errors::*;
|
||||
16
server-rs/crates/platform-agent/Cargo.toml
Normal file
16
server-rs/crates/platform-agent/Cargo.toml
Normal 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"] }
|
||||
59
server-rs/crates/platform-agent/src/apimart_gpt5_adapter.rs
Normal file
59
server-rs/crates/platform-agent/src/apimart_gpt5_adapter.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
54
server-rs/crates/platform-agent/src/callbacks.rs
Normal file
54
server-rs/crates/platform-agent/src/callbacks.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
40
server-rs/crates/platform-agent/src/error.rs
Normal file
40
server-rs/crates/platform-agent/src/error.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
49
server-rs/crates/platform-agent/src/function_agent.rs
Normal file
49
server-rs/crates/platform-agent/src/function_agent.rs
Normal 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,
|
||||
})?
|
||||
}
|
||||
}
|
||||
177
server-rs/crates/platform-agent/src/langchain_adapter.rs
Normal file
177
server-rs/crates/platform-agent/src/langchain_adapter.rs
Normal 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("系统提示"));
|
||||
}
|
||||
}
|
||||
23
server-rs/crates/platform-agent/src/lib.rs
Normal file
23
server-rs/crates/platform-agent/src/lib.rs
Normal 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};
|
||||
12
server-rs/crates/platform-agent/src/output_parser.rs
Normal file
12
server-rs/crates/platform-agent/src/output_parser.rs
Normal 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}"))
|
||||
})
|
||||
}
|
||||
112
server-rs/crates/platform-agent/src/puzzle_phase1_agent.rs
Normal file
112
server-rs/crates/platform-agent/src/puzzle_phase1_agent.rs
Normal 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",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
116
server-rs/crates/platform-agent/src/tool_registry.rs
Normal file
116
server-rs/crates/platform-agent/src/tool_registry.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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` / 业务模块任务里另行实现
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
563
server-rs/crates/shared-contracts/src/creative_agent.rs
Normal file
563
server-rs/crates/shared-contracts/src/creative_agent.rs
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
687
server-rs/crates/shared-contracts/src/visual_novel.rs
Normal file
687
server-rs/crates/shared-contracts/src/visual_novel.rs
Normal 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!("门铃响了。"));
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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
Reference in New Issue
Block a user