feat: Codex 工作区改动迁移(支付管理/用户余额/创作页等)
将 Codex 在工作区累积的改动整体落到 refactor/rust-backend: 支付管理与用户余额页、各创作 surface、website 等。排除 boosapi.db/config.toml 等敏感文件。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+122
-35
@@ -589,7 +589,8 @@ pub async fn execute_video_generation(
|
||||
}
|
||||
|
||||
fn comfy_video_template_id(route: &UpstreamRoute) -> Option<String> {
|
||||
route.base_url
|
||||
route
|
||||
.base_url
|
||||
.strip_prefix("comfy://")
|
||||
.or_else(|| route.base_url.strip_prefix("comfy-template://"))
|
||||
.or_else(|| route.model.strip_prefix("comfy://"))
|
||||
@@ -614,14 +615,16 @@ async fn execute_comfy_video_generation(
|
||||
let workflow = crate::workflow::storage::get_workflow(&state.db, template_id, None, true)
|
||||
.await
|
||||
.map_err(|err| AppError::BadRequest(format!("Comfy 视频模型模板不可用: {err}")))?;
|
||||
let mut prompt = workflow_prompt_from_graph(&workflow.graph)
|
||||
.ok_or_else(|| AppError::BadRequest("Comfy 视频模型模板缺少 prompt/workflow JSON".into()))?;
|
||||
let mut prompt = workflow_prompt_from_graph(&workflow.graph).ok_or_else(|| {
|
||||
AppError::BadRequest("Comfy 视频模型模板缺少 prompt/workflow JSON".into())
|
||||
})?;
|
||||
if prompt_contains_class_type(&prompt, "BoosAPITextToVideo") {
|
||||
return Err(AppError::BadRequest(
|
||||
"Comfy 视频模型模板不能包含 BoosAPITextToVideo 自定义节点,请选择真实 Comfy 视频节点模板".into(),
|
||||
));
|
||||
}
|
||||
let mut mapping = comfy_mapping_from_graph(&workflow.graph).unwrap_or_else(default_comfy_mapping);
|
||||
let mut mapping =
|
||||
comfy_mapping_from_graph(&workflow.graph).unwrap_or_else(default_comfy_mapping);
|
||||
if !mapping_has_node(&mapping, &["prompt", "positive", "text", "promptNode"]) {
|
||||
mapping = infer_comfy_mapping(&prompt);
|
||||
}
|
||||
@@ -644,7 +647,9 @@ async fn execute_comfy_video_generation(
|
||||
.or_else(|| response.pointer("/data/prompt_id"))
|
||||
.or_else(|| response.pointer("/data/promptId")),
|
||||
)
|
||||
.ok_or_else(|| AppError::UpstreamError(format!("Comfy response missing prompt_id: {response}")))?;
|
||||
.ok_or_else(|| {
|
||||
AppError::UpstreamError(format!("Comfy response missing prompt_id: {response}"))
|
||||
})?;
|
||||
|
||||
Ok(json!({
|
||||
"id": prompt_id,
|
||||
@@ -732,7 +737,8 @@ fn scalar_string(value: Option<&Value>) -> Option<String> {
|
||||
}
|
||||
|
||||
fn workflow_prompt_from_graph(graph: &Value) -> Option<Value> {
|
||||
graph.get("prompt")
|
||||
graph
|
||||
.get("prompt")
|
||||
.cloned()
|
||||
.or_else(|| graph.get("workflow").cloned())
|
||||
.or_else(|| graph.get("comfyPrompt").cloned())
|
||||
@@ -741,7 +747,8 @@ fn workflow_prompt_from_graph(graph: &Value) -> Option<Value> {
|
||||
}
|
||||
|
||||
fn comfy_mapping_from_graph(graph: &Value) -> Option<Value> {
|
||||
graph.pointer("/metadata/moneyPrinter/nodeMapping")
|
||||
graph
|
||||
.pointer("/metadata/moneyPrinter/nodeMapping")
|
||||
.or_else(|| graph.pointer("/moneyPrinter/nodeMapping"))
|
||||
.or_else(|| graph.pointer("/metadata/video/nodeMapping"))
|
||||
.or_else(|| graph.pointer("/video/nodeMapping"))
|
||||
@@ -772,11 +779,24 @@ fn graph_to_comfy_prompt(graph: &Value) -> Option<Value> {
|
||||
.unwrap_or_default();
|
||||
prompt.insert(id, json!({ "class_type": class_type, "inputs": inputs }));
|
||||
}
|
||||
for edge in graph.get("edges").and_then(Value::as_array).into_iter().flatten() {
|
||||
let Some(from) = edge.get("from").and_then(Value::as_str) else { continue; };
|
||||
let Some(to) = edge.get("to").and_then(Value::as_str) else { continue; };
|
||||
let Some((from_id, _from_port)) = from.split_once('.') else { continue; };
|
||||
let Some((to_id, to_field)) = to.split_once('.') else { continue; };
|
||||
for edge in graph
|
||||
.get("edges")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let Some(from) = edge.get("from").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let Some(to) = edge.get("to").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let Some((from_id, _from_port)) = from.split_once('.') else {
|
||||
continue;
|
||||
};
|
||||
let Some((to_id, to_field)) = to.split_once('.') else {
|
||||
continue;
|
||||
};
|
||||
if let Some(inputs) = prompt
|
||||
.get_mut(to_id)
|
||||
.and_then(Value::as_object_mut)
|
||||
@@ -806,7 +826,12 @@ fn mapping_value<'a>(mapping: &'a Value, keys: &[&str]) -> Option<&'a Value> {
|
||||
fn mapping_node_id(mapping: &Value, keys: &[&str]) -> Option<String> {
|
||||
mapping_value(mapping, keys).and_then(|value| {
|
||||
if value.is_object() {
|
||||
scalar_string(value.get("nodeId").or_else(|| value.get("node_id")).or(Some(value)))
|
||||
scalar_string(
|
||||
value
|
||||
.get("nodeId")
|
||||
.or_else(|| value.get("node_id"))
|
||||
.or(Some(value)),
|
||||
)
|
||||
} else {
|
||||
scalar_string(Some(value))
|
||||
}
|
||||
@@ -829,7 +854,10 @@ fn mapping_has_node(mapping: &Value, keys: &[&str]) -> bool {
|
||||
mapping_node_id(mapping, keys).is_some_and(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn first_node_by_class(prompt: &serde_json::Map<String, Value>, class_type: &str) -> Option<String> {
|
||||
fn first_node_by_class(
|
||||
prompt: &serde_json::Map<String, Value>,
|
||||
class_type: &str,
|
||||
) -> Option<String> {
|
||||
prompt
|
||||
.iter()
|
||||
.find(|(_, node)| node.get("class_type").and_then(Value::as_str) == Some(class_type))
|
||||
@@ -843,11 +871,22 @@ fn infer_comfy_mapping(prompt: &Value) -> Value {
|
||||
let prompt_node = object
|
||||
.iter()
|
||||
.find(|(_, node)| {
|
||||
let class_type = node.get("class_type").and_then(Value::as_str).unwrap_or_default().to_ascii_lowercase();
|
||||
let class_type = node
|
||||
.get("class_type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
let inputs = node
|
||||
.get("inputs")
|
||||
.and_then(Value::as_object)
|
||||
.map(|value| value.keys().cloned().collect::<Vec<_>>().join(" ").to_ascii_lowercase())
|
||||
.map(|value| {
|
||||
value
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.to_ascii_lowercase()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
class_type.contains("text")
|
||||
|| class_type.contains("prompt")
|
||||
@@ -875,19 +914,29 @@ fn infer_comfy_mapping(prompt: &Value) -> Value {
|
||||
|
||||
fn apply_node_input(prompt: &mut Value, node_id: &str, field: &str, value: Value) -> AppResult<()> {
|
||||
if node_id.trim().is_empty() {
|
||||
return Err(AppError::BadRequest("Comfy 视频模型节点映射包含空 nodeId".into()));
|
||||
return Err(AppError::BadRequest(
|
||||
"Comfy 视频模型节点映射包含空 nodeId".into(),
|
||||
));
|
||||
}
|
||||
let Some(node) = prompt.get_mut(node_id) else {
|
||||
return Err(AppError::BadRequest(format!("Comfy prompt missing node {node_id}")));
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Comfy prompt missing node {node_id}"
|
||||
)));
|
||||
};
|
||||
let Some(inputs) = node.get_mut("inputs").and_then(Value::as_object_mut) else {
|
||||
return Err(AppError::BadRequest(format!("Comfy node {node_id} has no inputs")));
|
||||
return Err(AppError::BadRequest(format!(
|
||||
"Comfy node {node_id} has no inputs"
|
||||
)));
|
||||
};
|
||||
inputs.insert(field.to_string(), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn inject_comfy_video_inputs(prompt: &mut Value, mapping: &Value, body: &Value) -> AppResult<String> {
|
||||
fn inject_comfy_video_inputs(
|
||||
prompt: &mut Value,
|
||||
mapping: &Value,
|
||||
body: &Value,
|
||||
) -> AppResult<String> {
|
||||
let video_prompt = scalar_string(body.get("prompt"))
|
||||
.ok_or_else(|| AppError::BadRequest("prompt is required for Comfy video models".into()))?;
|
||||
let prompt_node = mapping_node_id(mapping, &["prompt", "positive", "text", "promptNode"])
|
||||
@@ -895,27 +944,56 @@ fn inject_comfy_video_inputs(prompt: &mut Value, mapping: &Value, body: &Value)
|
||||
apply_node_input(
|
||||
prompt,
|
||||
&prompt_node,
|
||||
&mapping_field(mapping, &["prompt", "positive", "text", "promptNode"], "prompt"),
|
||||
&mapping_field(
|
||||
mapping,
|
||||
&["prompt", "positive", "text", "promptNode"],
|
||||
"prompt",
|
||||
),
|
||||
json!(video_prompt),
|
||||
)?;
|
||||
if let Some(negative_node) = mapping_node_id(mapping, &["negativePrompt", "negative", "negativeNode"]) {
|
||||
if let Some(negative_node) =
|
||||
mapping_node_id(mapping, &["negativePrompt", "negative", "negativeNode"])
|
||||
{
|
||||
let negative = scalar_string(body.get("negative_prompt").or_else(|| body.get("negative")))
|
||||
.unwrap_or_else(|| "low quality, blurry, distorted, watermark, text artifacts".to_string());
|
||||
.unwrap_or_else(|| {
|
||||
"low quality, blurry, distorted, watermark, text artifacts".to_string()
|
||||
});
|
||||
apply_node_input(
|
||||
prompt,
|
||||
&negative_node,
|
||||
&mapping_field(mapping, &["negativePrompt", "negative", "negativeNode"], "text"),
|
||||
&mapping_field(
|
||||
mapping,
|
||||
&["negativePrompt", "negative", "negativeNode"],
|
||||
"text",
|
||||
),
|
||||
json!(negative),
|
||||
)?;
|
||||
}
|
||||
for (keys, request_keys, default_field) in [
|
||||
(&["ratio", "ratioNode"][..], &["ratio", "aspectRatio", "aspect_ratio"][..], "ratio"),
|
||||
(&["resolution", "resolutionNode"][..], &["resolution", "size"][..], "resolution"),
|
||||
(&["duration", "durationNode"][..], &["duration"][..], "duration"),
|
||||
(
|
||||
&["ratio", "ratioNode"][..],
|
||||
&["ratio", "aspectRatio", "aspect_ratio"][..],
|
||||
"ratio",
|
||||
),
|
||||
(
|
||||
&["resolution", "resolutionNode"][..],
|
||||
&["resolution", "size"][..],
|
||||
"resolution",
|
||||
),
|
||||
(
|
||||
&["duration", "durationNode"][..],
|
||||
&["duration"][..],
|
||||
"duration",
|
||||
),
|
||||
] {
|
||||
if let Some(node_id) = mapping_node_id(mapping, keys) {
|
||||
if let Some(value) = request_keys.iter().find_map(|key| body.get(*key).cloned()) {
|
||||
apply_node_input(prompt, &node_id, &mapping_field(mapping, keys, default_field), value)?;
|
||||
apply_node_input(
|
||||
prompt,
|
||||
&node_id,
|
||||
&mapping_field(mapping, keys, default_field),
|
||||
value,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,9 +1007,13 @@ fn prompt_contains_class_type(prompt: &Value, class_type: &str) -> bool {
|
||||
.get("class_type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| value == class_type)
|
||||
|| object.values().any(|value| prompt_contains_class_type(value, class_type))
|
||||
|| object
|
||||
.values()
|
||||
.any(|value| prompt_contains_class_type(value, class_type))
|
||||
}
|
||||
Value::Array(items) => items.iter().any(|value| prompt_contains_class_type(value, class_type)),
|
||||
Value::Array(items) => items
|
||||
.iter()
|
||||
.any(|value| prompt_contains_class_type(value, class_type)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -1024,7 +1106,10 @@ fn comfy_failure_message(value: &Value) -> Option<String> {
|
||||
let status = scalar_string(value.get("status").or_else(|| value.get("state")))
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
if matches!(status.as_str(), "failed" | "error" | "cancelled" | "canceled") {
|
||||
if matches!(
|
||||
status.as_str(),
|
||||
"failed" | "error" | "cancelled" | "canceled"
|
||||
) {
|
||||
return Some(
|
||||
scalar_string(value.pointer("/execution_error/exception_message"))
|
||||
.or_else(|| scalar_string(value.get("error")))
|
||||
@@ -1038,10 +1123,12 @@ fn comfy_failure_message(value: &Value) -> Option<String> {
|
||||
fn comfy_output_objects(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
["videos", "gifs", "images"]
|
||||
.iter()
|
||||
.any(|key| object.get(*key).and_then(Value::as_array).is_some_and(|items| !items.is_empty()))
|
||||
|| object.values().any(comfy_output_objects)
|
||||
["videos", "gifs", "images"].iter().any(|key| {
|
||||
object
|
||||
.get(*key)
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|items| !items.is_empty())
|
||||
}) || object.values().any(comfy_output_objects)
|
||||
}
|
||||
Value::Array(items) => items.iter().any(comfy_output_objects),
|
||||
_ => false,
|
||||
|
||||
Reference in New Issue
Block a user