Merge pull request 'chore(api-server): 外部模型与网关配置改为环境变量' (#6) from hermes/hermes-996d586b into master
Some checks failed
CI / verify (push) Has been cancelled

Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/6
This commit was merged in pull request #6.
This commit is contained in:
2026-05-07 16:48:41 +08:00
7 changed files with 256 additions and 53 deletions

View File

@@ -34,6 +34,19 @@ ALIYUN_SMS_RETURN_VERIFY_CODE="false"
VITE_AUTH_ALLOW_DEV_GUEST="false"
# api-server 非公共模型与可变网关配置(从 config.rs 默认值迁移到本地环境变量)
GENARRATIVE_LLM_PROVIDER="ark"
GENARRATIVE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
GENARRATIVE_LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
GENARRATIVE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
APIMART_BASE_URL="https://api.apimart.ai/v1"
APIMART_API_KEY=""
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
DASHSCOPE_SCENE_IMAGE_MODEL="wan2.2-t2i-flash"
DASHSCOPE_REFERENCE_IMAGE_MODEL="qwen-image-2.0"
DASHSCOPE_COVER_IMAGE_MODEL="wan2.2-t2i-flash"
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
# 启用服务端大模型调试日志(记录所有输入输出)
LLM_DEBUG_LOG="true"

View File

@@ -35,14 +35,25 @@ GENARRATIVE_LLM_MODEL=
GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=false
GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false
APIMART_BASE_URL=
APIMART_API_KEY=
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
ARK_CHARACTER_VIDEO_BASE_URL=
ARK_CHARACTER_VIDEO_API_KEY=
ARK_CHARACTER_VIDEO_MODEL=
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
DASHSCOPE_API_KEY=
DASHSCOPE_IMAGE_MODEL=wan2.7-image
DASHSCOPE_SCENE_IMAGE_MODEL=
DASHSCOPE_REFERENCE_IMAGE_MODEL=
DASHSCOPE_COVER_IMAGE_MODEL=
DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS=150000
DASHSCOPE_CHARACTER_VISUAL_MODEL=wan2.7-image-pro
DASHSCOPE_CHARACTER_IMAGE_SEQUENCE_MODEL=wan2.7-image-pro
DASHSCOPE_CHARACTER_REFERENCE_VIDEO_MODEL=wan2.7-r2v
DASHSCOPE_CHARACTER_MOTION_TRANSFER_MODEL=wan2.2-animate-move
DASHSCOPE_CHARACTER_VISUAL_MODEL=
DASHSCOPE_CHARACTER_IMAGE_SEQUENCE_MODEL=
DASHSCOPE_CHARACTER_REFERENCE_VIDEO_MODEL=
DASHSCOPE_CHARACTER_MOTION_TRANSFER_MODEL=
DASHSCOPE_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
SMS_AUTH_ENABLED=false

View File

@@ -0,0 +1,86 @@
# api-server 外部服务环境变量配置 2026-05-07
## 背景
`server-rs/crates/api-server/src/config.rs` 统一收口 api-server 启动配置。外部服务分为两类:
1. 公共服务:阿里云、腾讯云、微信等对外公开且接口域名稳定的服务。
2. 非公共服务团队自选模型网关、图片网关、视频模型、内部兼容服务等URL 与模型名可能随部署、供应商或账号策略变化。
本次约定:公共服务 URL 可以保留代码默认值;非公共服务的 URL 与模型名必须通过环境变量提供,不再在 `config.rs` 写死具体模型名称或私有网关地址。
## 公共服务默认值
以下默认值属于公共服务稳定接口,可继续保留在代码或示例环境中:
```text
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
ALIYUN_SMS_ENDPOINT=dypnsapi.aliyuncs.com
WECHAT_AUTHORIZE_ENDPOINT=https://open.weixin.qq.com/connect/qrconnect
WECHAT_ACCESS_TOKEN_ENDPOINT=https://api.weixin.qq.com/sns/oauth2/access_token
WECHAT_USER_INFO_ENDPOINT=https://api.weixin.qq.com/sns/userinfo
```
说明DashScope 属于阿里云公开服务,基础 URL 可保留;具体图片模型名不属于稳定公共接口,必须由环境变量配置。
## 非公共服务必配项
生产环境或真实联调使用到对应能力时,应显式配置以下变量:
```text
# 文本 LLM 网关
GENARRATIVE_LLM_PROVIDER=openai-compatible
GENARRATIVE_LLM_BASE_URL=
GENARRATIVE_LLM_API_KEY=
GENARRATIVE_LLM_MODEL=
# APIMart / OpenAI 兼容图片网关
APIMART_BASE_URL=
APIMART_API_KEY=
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
# DashScope 图片模型名
DASHSCOPE_SCENE_IMAGE_MODEL=
DASHSCOPE_REFERENCE_IMAGE_MODEL=
DASHSCOPE_COVER_IMAGE_MODEL=
# Ark / 角色视频模型网关
ARK_CHARACTER_VIDEO_BASE_URL=
ARK_CHARACTER_VIDEO_API_KEY=
ARK_CHARACTER_VIDEO_MODEL=
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
```
## 兼容变量
为降低部署切换成本,当前代码仍兼容部分历史变量:
```text
GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL
GENARRATIVE_LLM_MODEL / LLM_MODEL / VITE_LLM_MODEL
GENARRATIVE_LLM_API_KEY / LLM_API_KEY / ARK_API_KEY
DASHSCOPE_SCENE_IMAGE_MODEL / DASHSCOPE_IMAGE_MODEL
DASHSCOPE_REFERENCE_IMAGE_MODEL / DASHSCOPE_IMAGE_EDIT_MODEL
DASHSCOPE_COVER_IMAGE_MODEL / DASHSCOPE_IMAGE_MODEL
ARK_CHARACTER_VIDEO_BASE_URL / ARK_BASE_URL / GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL
ARK_CHARACTER_VIDEO_API_KEY / ARK_API_KEY / GENARRATIVE_LLM_API_KEY / LLM_API_KEY
ARK_CHARACTER_VIDEO_MODEL / DASHSCOPE_CHARACTER_VIDEO_MODEL
```
## 运行时行为
1. `AppConfig::default()` 不再包含具体非公共模型名或私有网关 URL。
2. `AppConfig::from_env()` 会从环境变量读取非公共模型名和 URL。
3. 文本 LLM provider 为 `ark` 且未配置 `GENARRATIVE_LLM_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
4. 角色视频 provider 复用 Ark 且未配置 `ARK_CHARACTER_VIDEO_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
5. 具体模型名缺失时不在配置层伪造默认模型,调用到对应能力时由下游配置校验返回缺配置错误。
## 示例文件
生产示例环境变量维护在:
```text
deploy/env/api-server.env.example
```
真实密钥、内部网关 URL 和具体模型名只应写入服务器 `/etc/genarrative/api-server.env` 或本地未提交的 `.env.local` / `.env.secrets.local`,不得提交到仓库。

View File

@@ -4,6 +4,7 @@
## 文档列表
- [API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md](./API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md):冻结 api-server 外部服务配置边界,公共服务 URL 可保留代码默认值,非公共模型名和私有网关 URL 统一通过环境变量注入。
- [PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md](./PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md):冻结个人任务与埋点系统首版方案,明确 `tracking_event``tracking_daily_stat``profile_task_config`、任务进度、领奖记录和光点钱包流水的边界。
- [SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md](./SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md):记录方洞挑战结果页图片槽位局部生成、洞口图历史素材、运行态拖拽与点击投放交互的修正口径。
- [MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md](./MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md):冻结 Maincloud 历史残留引用禁用策略,明确后续不得新增、运行或引用 `api-server:maincloud``GENARRATIVE_SPACETIME_MAINCLOUD_*` 和相关测试/文档口径。

View File

@@ -5,7 +5,6 @@ use platform_llm::{
DEFAULT_RETRY_BACKOFF_MS, LlmProvider,
};
const DEFAULT_LLM_MODEL: &str = "doubao-1-5-pro-32k-character-250715";
const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge";
const DEFAULT_AUTH_STORE_PATH: &str = "server-rs/.data/auth-store.json";
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
@@ -171,9 +170,9 @@ impl Default for AppConfig {
spacetime_pool_size: 4,
spacetime_procedure_timeout: Duration::from_secs(30),
llm_provider: LlmProvider::Ark,
llm_base_url: DEFAULT_ARK_BASE_URL.to_string(),
llm_base_url: String::new(),
llm_api_key: None,
llm_model: DEFAULT_LLM_MODEL.to_string(),
llm_model: String::new(),
llm_request_timeout_ms: DEFAULT_REQUEST_TIMEOUT_MS,
llm_max_retries: DEFAULT_MAX_RETRIES,
llm_retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
@@ -181,18 +180,18 @@ impl Default for AppConfig {
creation_agent_llm_web_search_enabled: true,
dashscope_base_url: "https://dashscope.aliyuncs.com/api/v1".to_string(),
dashscope_api_key: None,
dashscope_scene_image_model: "wan2.2-t2i-flash".to_string(),
dashscope_reference_image_model: "qwen-image-2.0".to_string(),
dashscope_cover_image_model: "wan2.2-t2i-flash".to_string(),
dashscope_scene_image_model: String::new(),
dashscope_reference_image_model: String::new(),
dashscope_cover_image_model: String::new(),
dashscope_image_request_timeout_ms: 150_000,
apimart_base_url: "https://api.apimart.ai/v1".to_string(),
apimart_base_url: String::new(),
apimart_api_key: None,
apimart_image_request_timeout_ms: 180_000,
draft_asset_generation_max_concurrent_requests: 4,
ark_character_video_base_url: DEFAULT_ARK_BASE_URL.to_string(),
ark_character_video_base_url: String::new(),
ark_character_video_api_key: None,
ark_character_video_request_timeout_ms: 420_000,
ark_character_video_model: "doubao-seedance-2-0-fast-260128".to_string(),
ark_character_video_model: String::new(),
character_animation_ffmpeg_path: "ffmpeg".to_string(),
character_animation_ffprobe_path: "ffprobe".to_string(),
character_animation_frame_extract_timeout_ms: 120_000,
@@ -456,6 +455,8 @@ impl AppConfig {
read_first_non_empty_env(&["GENARRATIVE_LLM_BASE_URL", "LLM_BASE_URL"])
{
config.llm_base_url = llm_base_url;
} else if config.llm_provider == LlmProvider::Ark {
config.llm_base_url = DEFAULT_ARK_BASE_URL.to_string();
}
config.llm_api_key =
@@ -557,6 +558,8 @@ impl AppConfig {
"LLM_BASE_URL",
]) {
config.ark_character_video_base_url = ark_character_video_base_url;
} else if config.llm_provider == LlmProvider::Ark {
config.ark_character_video_base_url = DEFAULT_ARK_BASE_URL.to_string();
}
config.ark_character_video_api_key = read_first_non_empty_env(&[
@@ -816,11 +819,95 @@ fn parse_positive_u16(raw: &str) -> Option<u16> {
#[cfg(test)]
mod tests {
use super::AppConfig;
use super::{AppConfig, LlmProvider};
use std::sync::{Mutex, OnceLock};
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
#[test]
fn default_keeps_non_public_model_and_base_url_empty() {
let config = AppConfig::default();
assert!(config.llm_model.is_empty());
assert!(config.llm_base_url.is_empty());
assert!(config.apimart_base_url.is_empty());
assert!(config.ark_character_video_base_url.is_empty());
assert!(config.ark_character_video_model.is_empty());
assert!(config.dashscope_scene_image_model.is_empty());
assert!(config.dashscope_reference_image_model.is_empty());
assert!(config.dashscope_cover_image_model.is_empty());
assert_eq!(
config.dashscope_base_url,
"https://dashscope.aliyuncs.com/api/v1"
);
assert_eq!(config.sms_endpoint, "dypnsapi.aliyuncs.com");
assert_eq!(
config.wechat_authorize_endpoint,
"https://open.weixin.qq.com/connect/qrconnect"
);
}
#[test]
fn from_env_reads_non_public_models_and_urls() {
let _guard = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.expect("env lock should not poison");
unsafe {
std::env::remove_var("GENARRATIVE_LLM_PROVIDER");
std::env::remove_var("GENARRATIVE_LLM_BASE_URL");
std::env::remove_var("GENARRATIVE_LLM_MODEL");
std::env::remove_var("APIMART_BASE_URL");
std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_COVER_IMAGE_MODEL");
std::env::remove_var("ARK_CHARACTER_VIDEO_BASE_URL");
std::env::remove_var("ARK_CHARACTER_VIDEO_MODEL");
std::env::set_var("GENARRATIVE_LLM_PROVIDER", "openai-compatible");
std::env::set_var(
"GENARRATIVE_LLM_BASE_URL",
"https://llm.internal.example/v1",
);
std::env::set_var("GENARRATIVE_LLM_MODEL", "internal-text-model");
std::env::set_var("APIMART_BASE_URL", "https://image.internal.example/v1");
std::env::set_var("DASHSCOPE_SCENE_IMAGE_MODEL", "scene-model");
std::env::set_var("DASHSCOPE_REFERENCE_IMAGE_MODEL", "reference-model");
std::env::set_var("DASHSCOPE_COVER_IMAGE_MODEL", "cover-model");
std::env::set_var(
"ARK_CHARACTER_VIDEO_BASE_URL",
"https://video.internal.example/v1",
);
std::env::set_var("ARK_CHARACTER_VIDEO_MODEL", "video-model");
}
let config = AppConfig::from_env();
assert_eq!(config.llm_provider, LlmProvider::OpenAiCompatible);
assert_eq!(config.llm_base_url, "https://llm.internal.example/v1");
assert_eq!(config.llm_model, "internal-text-model");
assert_eq!(config.apimart_base_url, "https://image.internal.example/v1");
assert_eq!(config.dashscope_scene_image_model, "scene-model");
assert_eq!(config.dashscope_reference_image_model, "reference-model");
assert_eq!(config.dashscope_cover_image_model, "cover-model");
assert_eq!(
config.ark_character_video_base_url,
"https://video.internal.example/v1"
);
assert_eq!(config.ark_character_video_model, "video-model");
unsafe {
std::env::remove_var("GENARRATIVE_LLM_PROVIDER");
std::env::remove_var("GENARRATIVE_LLM_BASE_URL");
std::env::remove_var("GENARRATIVE_LLM_MODEL");
std::env::remove_var("APIMART_BASE_URL");
std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL");
std::env::remove_var("DASHSCOPE_COVER_IMAGE_MODEL");
std::env::remove_var("ARK_CHARACTER_VIDEO_BASE_URL");
std::env::remove_var("ARK_CHARACTER_VIDEO_MODEL");
}
}
#[test]
fn from_env_reads_spacetime_pool_size() {
let _guard = ENV_LOCK

View File

@@ -28,9 +28,7 @@ use webp::Encoder as WebpEncoder;
use crate::{
api_response::json_success_body,
asset_billing::{
execute_billable_asset_operation, execute_billable_asset_operation_with_cost,
},
asset_billing::{execute_billable_asset_operation, execute_billable_asset_operation_with_cost},
auth::AuthenticatedAccessToken,
custom_world_result_prompts::{
build_result_entity_system_prompt, build_result_entity_user_prompt,
@@ -1390,7 +1388,9 @@ async fn create_ark_storyboard_to_video_task(
}))
.send()
.await
.map_err(|error| map_ark_video_request_error(format!("请求 Seedance 视频服务失败:{error}")))?;
.map_err(|error| {
map_ark_video_request_error(format!("请求 Seedance 视频服务失败:{error}"))
})?;
let status = response.status();
let text = response.text().await.map_err(|error| {
map_ark_video_request_error(format!("读取 Seedance 视频任务响应失败:{error}"))
@@ -1428,7 +1428,9 @@ async fn wait_for_ark_content_generation_task(
)
.send()
.await
.map_err(|error| map_ark_video_request_error(format!("查询 Seedance 视频任务失败:{error}")))?;
.map_err(|error| {
map_ark_video_request_error(format!("查询 Seedance 视频任务失败:{error}"))
})?;
let status = response.status();
let text = response.text().await.map_err(|error| {
map_ark_video_request_error(format!("读取 Seedance 视频任务响应失败:{error}"))
@@ -1447,11 +1449,13 @@ async fn wait_for_ark_content_generation_task(
extract_generation_task_status(&payload.payload).as_str(),
);
if is_completed_generation_task_status(normalized_status.as_str()) {
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": "开局 CG 视频任务完成但没有返回 video_url。",
"taskId": task_id,
})));
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": "开局 CG 视频任务完成但没有返回 video_url。",
"taskId": task_id,
})),
);
}
if is_failed_generation_task_status(normalized_status.as_str()) {
return Err(parse_ark_video_upstream_error(
@@ -1463,11 +1467,13 @@ async fn wait_for_ark_content_generation_task(
sleep(Duration::from_millis(ARK_VIDEO_TASK_POLL_INTERVAL_MS)).await;
}
Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": "开局 CG 视频生成超时,请稍后重试。",
"taskId": task_id,
})))
Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": "开局 CG 视频生成超时,请稍后重试。",
"taskId": task_id,
})),
)
}
async fn download_generated_video(
@@ -1492,11 +1498,13 @@ async fn download_generated_video(
.await
.map_err(|error| map_ark_video_request_error(format!("{fallback_message}{error}")))?;
if !status.is_success() {
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": fallback_message,
"status": status.as_u16(),
})));
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "ark",
"message": fallback_message,
"status": status.as_u16(),
})),
);
}
let normalized_mime_type = normalize_downloaded_video_mime_type(content_type.as_str());
@@ -1767,12 +1775,10 @@ fn normalize_opening_cg_request(profile: &Value) -> Result<NormalizedOpeningCgRe
})?;
let world_name = read_string_field(object, "name").unwrap_or_else(|| "未命名世界".to_string());
let profile_id = read_string_field(object, "id");
let world_tone = read_string_field(object, "tone").ok_or_else(|| {
missing_opening_cg_field_error("世界基调缺失,无法生成开局 CG。")
})?;
let world_summary = read_string_field(object, "summary").ok_or_else(|| {
missing_opening_cg_field_error("世界概述缺失,无法生成开局 CG。")
})?;
let world_tone = read_string_field(object, "tone")
.ok_or_else(|| missing_opening_cg_field_error("世界基调缺失,无法生成开局 CG。"))?;
let world_summary = read_string_field(object, "summary")
.ok_or_else(|| missing_opening_cg_field_error("世界概述缺失,无法生成开局 CG。"))?;
let core_conflicts = read_string_array_field(object, "coreConflicts");
if core_conflicts.is_empty() {
return Err(missing_opening_cg_field_error(
@@ -1785,9 +1791,8 @@ fn normalize_opening_cg_request(profile: &Value) -> Result<NormalizedOpeningCgRe
.and_then(|roles| roles.first())
.and_then(Value::as_object)
.ok_or_else(|| missing_opening_cg_field_error("缺少玩家扮演角色。"))?;
let player_role_image_src = read_string_field(player_role, "imageSrc").ok_or_else(|| {
missing_opening_cg_field_error("玩家扮演角色缺少角色参考图。")
})?;
let player_role_image_src = read_string_field(player_role, "imageSrc")
.ok_or_else(|| missing_opening_cg_field_error("玩家扮演角色缺少角色参考图。"))?;
let player_role_brief = build_opening_cg_player_role_brief(player_role);
let opening_scene_image_src = profile
.pointer("/sceneChapterBlueprints/0/acts/0/backgroundImageSrc")
@@ -1887,10 +1892,12 @@ fn require_ark_video_settings(state: &AppState) -> Result<ArkVideoSettings, AppE
.trim()
.trim_end_matches('/');
if base_url.is_empty() {
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
"provider": "ark",
"reason": "ARK_CHARACTER_VIDEO_BASE_URL 未配置",
})));
return Err(
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
"provider": "ark",
"reason": "ARK_CHARACTER_VIDEO_BASE_URL 未配置",
})),
);
}
let api_key = state
.config
@@ -3056,9 +3063,7 @@ fn normalize_downloaded_video_mime_type(content_type: &str) -> String {
.map(str::trim)
.unwrap_or("video/mp4");
match mime_type {
"video/mp4" | "video/quicktime" | "video/webm" | "video/x-msvideo" => {
mime_type.to_string()
}
"video/mp4" | "video/quicktime" | "video/webm" | "video/x-msvideo" => mime_type.to_string(),
_ => "video/mp4".to_string(),
}
}

View File

@@ -45,8 +45,8 @@ use shared_contracts::{
PutSquareHoleWorkRequest, RegenerateSquareHoleWorkImageRequest,
SquareHoleHoleOptionResponse as SquareHoleWorkHoleOptionResponse,
SquareHoleShapeOptionResponse as SquareHoleWorkShapeOptionResponse,
SquareHoleWorkDetailResponse, SquareHoleWorkMutationResponse, SquareHoleWorkProfileResponse,
SquareHoleWorkSummaryResponse, SquareHoleWorksResponse,
SquareHoleWorkDetailResponse, SquareHoleWorkMutationResponse,
SquareHoleWorkProfileResponse, SquareHoleWorkSummaryResponse, SquareHoleWorksResponse,
},
};
use shared_kernel::build_prefixed_uuid_id;