generated-scene: add command-center automation semantics

This commit is contained in:
木炎
2026-05-06 15:22:49 +08:00
parent 6122b521a8
commit 1d586dbe27
7 changed files with 894 additions and 15 deletions

View File

@@ -13,13 +13,15 @@ use crate::generated_scene::analyzer::{
use crate::generated_scene::ir::{ use crate::generated_scene::ir::{
ApiEndpointIr, ArtifactContractIr, BootstrapIr, EnrichmentRequestIr, EvidenceIr, ExportPlanIr, ApiEndpointIr, ArtifactContractIr, BootstrapIr, EnrichmentRequestIr, EvidenceIr, ExportPlanIr,
LegacySceneInfoJson, MainRequestIr, MergeFieldMappingIr, MergePlanIr, ModeConditionIr, ModeIr, LegacySceneInfoJson, MainRequestIr, MergeFieldMappingIr, MergePlanIr, ModeConditionIr, ModeIr,
MonitoringActionWorkflowIr, MonitoringDeltaStateIr, MonitoringDependencyIr, MonitoringActionContractIr, MonitoringActionWorkflowIr, MonitoringDeltaStateIr,
MonitoringEncryptionResolutionIr, MonitoringOutputContractIr, MonitoringReadSliceIr, MonitoringDependencyIr, MonitoringEncryptionResolutionIr, MonitoringExecutionFlowIr,
MonitoringRuntimeContextIr, MonitoringSideEffectIr, MonitoringSideEffectPolicyIr, MonitoringExecutionStepIr, MonitoringIterationContractIr, MonitoringLogWriteContractIr,
MonitoringSidecarOutputIr, MonitoringStorageReadIr, MonitoringTimeoutContractIr, MonitoringOutputContractIr, MonitoringQueueTransitionRuleIr, MonitoringReadSliceIr,
NormalizeRulesIr, PaginationPlanIr, ParamIr, ReadinessGateIr, ReadinessIr, MonitoringResultStateMachineIr, MonitoringRuntimeContextIr, MonitoringSideEffectIr,
RequestFieldMappingIr, RuntimeDependencyIr, SceneIdDiagnosticsIr, SceneIr, ValidationHintsIr, MonitoringSideEffectPolicyIr, MonitoringSidecarOutputIr, MonitoringStorageReadIr,
WorkflowArchetype, WorkflowEvidenceIr, WorkflowStepIr, MonitoringTimeoutContractIr, NormalizeRulesIr, PaginationPlanIr, ParamIr, ReadinessGateIr,
ReadinessIr, RequestFieldMappingIr, RuntimeDependencyIr, SceneIdDiagnosticsIr, SceneIr,
ValidationHintsIr, WorkflowArchetype, WorkflowEvidenceIr, WorkflowStepIr,
}; };
use crate::generated_scene::lessons::{ use crate::generated_scene::lessons::{
load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS, load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
@@ -312,7 +314,13 @@ pub fn generate_scheduled_monitoring_action_skill_package(
let trigger_contract = let trigger_contract =
read_json_file(&request.trigger_contract_json, "scheduled monitoring trigger contract")?; read_json_file(&request.trigger_contract_json, "scheduled monitoring trigger contract")?;
let workflow = monitoring_workflow_from_contract(&ir_contract); let mut workflow = monitoring_workflow_from_contract(&ir_contract);
enrich_command_center_automation_semantics(
&request.scene_id,
&source_evidence,
&trigger_contract,
&mut workflow,
);
let scene_ir = monitoring_action_scene_ir( let scene_ir = monitoring_action_scene_ir(
&GenerateMonitoringActionPreviewRequest { &GenerateMonitoringActionPreviewRequest {
scene_id: request.scene_id.clone(), scene_id: request.scene_id.clone(),
@@ -1227,6 +1235,12 @@ fn monitoring_workflow_from_contract(contract: &Value) -> MonitoringActionWorkfl
}) })
.unwrap_or_else(|| monitoring_blocked_actions_from_platform_dependencies(platform_dependencies)), .unwrap_or_else(|| monitoring_blocked_actions_from_platform_dependencies(platform_dependencies)),
}, },
action_contracts: Vec::new(),
iteration_contract: None,
execution_flow: None,
result_state_machines: Vec::new(),
queue_transition_rules: Vec::new(),
log_write_contracts: Vec::new(),
archetype: contract["archetype"] archetype: contract["archetype"]
.as_str() .as_str()
.unwrap_or("marketing_gateway_monitor") .unwrap_or("marketing_gateway_monitor")
@@ -1234,6 +1248,278 @@ fn monitoring_workflow_from_contract(contract: &Value) -> MonitoringActionWorkfl
} }
} }
fn enrich_command_center_automation_semantics(
scene_id: &str,
source_evidence: &Value,
trigger_contract: &Value,
workflow: &mut MonitoringActionWorkflowIr,
) {
if !is_command_center_monitoring_scene(scene_id, &workflow.workflow_id) {
return;
}
let dispatch_dependency = workflow
.business_api_dependencies
.iter()
.find(|item| {
item.url.contains("repetCtrlSend")
|| item.classification == "dispatch_exception_order"
|| item.classification == "business_dispatch"
})
.cloned()
.unwrap_or_else(|| MonitoringDependencyIr {
name: "repetCtrlSend".to_string(),
url: "http://yxgateway.gs.sgcc.com.cn/emss-chargacctgf-paysrv-front/member/acctabnor/repetCtrlSend"
.to_string(),
classification: "dispatch_exception_order".to_string(),
side_effect: true,
blocked_by_default: true,
});
let dispose_log_dependency = workflow
.local_service_dependencies
.iter()
.find(|item| {
item.url.contains("setDisposeLog")
|| item.classification == "write_dispose_log"
|| item.classification == "dispose_log_write"
})
.cloned()
.unwrap_or_else(|| MonitoringDependencyIr {
name: "setDisposeLog".to_string(),
url: "http://localhost:13313/MonitorServices/setDisposeLog".to_string(),
classification: "write_dispose_log".to_string(),
side_effect: true,
blocked_by_default: true,
});
let queue_action_ref = trigger_contract["platformRuntimeCapabilities"]["hostActionBridge"]
.as_array()
.and_then(|items| {
items.iter().find_map(|item| {
if item["name"].as_str().unwrap_or_default() == "mac.exeTQueue" {
Some(item["name"].as_str().unwrap_or_default().to_string())
} else {
None
}
})
})
.unwrap_or_else(|| "mac.exeTQueue".to_string());
let dispatch_dependency_ref = if dispatch_dependency.url.is_empty() {
dispatch_dependency.classification.clone()
} else {
dispatch_dependency.url.clone()
};
let dispose_log_dependency_ref = if dispose_log_dependency.url.is_empty() {
dispose_log_dependency.classification.clone()
} else {
dispose_log_dependency.url.clone()
};
let queue_required_mode = trigger_contract["triggerContracts"]["queue"]["futureModes"]
.as_array()
.and_then(|items| items.first())
.and_then(Value::as_str)
.unwrap_or("queue_process")
.to_string();
let mut dispatch_field_bindings = Map::new();
dispatch_field_bindings.insert("chooseList".to_string(), json!(["$current_item"]));
dispatch_field_bindings.insert(
"itemIdentity".to_string(),
json!(["consNo", "custNo", "sendBeginTime", "createTime"]),
);
let mut dispose_log_field_bindings = Map::new();
dispose_log_field_bindings.insert(
"orderID".to_string(),
json!({
"from": ["current_item.consNo", "current_item.custNo", "current_item.sendBeginTime", "current_item.createTime"],
"strategy": "join_non_empty_with_underscore"
}),
);
dispose_log_field_bindings.insert(
"name".to_string(),
json!({
"from": ["current_item.orgName", "current_item.consName", "current_item.phone"],
"strategy": "first_non_empty"
}),
);
dispose_log_field_bindings.insert("time".to_string(), json!({"from": "runtime.now"}));
let action_contract = MonitoringActionContractIr {
action_id: "dispatch_exception_order".to_string(),
action_type: "business_dispatch".to_string(),
dependency_ref: dispatch_dependency_ref.clone(),
target_endpoint_or_host_call: if dispatch_dependency.url.is_empty() {
"repetCtrlSend".to_string()
} else {
dispatch_dependency.url.clone()
},
execution_context: "attached_page_http_post".to_string(),
input_source: "current_item".to_string(),
request_template: json!({
"busType": "03",
"chooseList": ["$current_item"]
}),
field_bindings: dispatch_field_bindings,
auth_binding: json!({
"source": "localStorage.markToken",
"targetField": "auth_token"
}),
encryption_binding: json!({
"mode": workflow.runtime_context.encryption_mode,
"payloadSource": "request_body_json"
}),
result_channel: "browser_callback_js_result".to_string(),
};
let iteration_contract = MonitoringIterationContractIr {
iteration_id: "pending_dispatch_loop".to_string(),
source_collection: source_evidence["queueDependencies"]
.as_array()
.and_then(|items| {
items.iter().find_map(|item| {
if item["name"].as_str().unwrap_or_default() == "pendingList" {
Some("pendingList".to_string())
} else {
None
}
})
})
.unwrap_or_else(|| "pendingList".to_string()),
iteration_mode: "sequential_per_item".to_string(),
item_alias: "current_item".to_string(),
on_empty_transition: "queue_continue_on_empty".to_string(),
on_item_complete_transition: "next_item".to_string(),
on_all_complete_transition: "queue_continue_on_done".to_string(),
};
let execution_flow = MonitoringExecutionFlowIr {
flow_id: "command_center_dispatch_preview_flow".to_string(),
entry_step: "dispatch_loop".to_string(),
steps: vec![
MonitoringExecutionStepIr {
step_id: "dispatch_loop".to_string(),
step_type: "iterate".to_string(),
iteration_ref: "pending_dispatch_loop".to_string(),
next_on_empty: "queue_continue_empty".to_string(),
next_on_done: "queue_continue_done".to_string(),
next_on_success: "dispatch_current_item".to_string(),
..MonitoringExecutionStepIr::default()
},
MonitoringExecutionStepIr {
step_id: "dispatch_current_item".to_string(),
step_type: "action".to_string(),
action_contract_ref: "dispatch_exception_order".to_string(),
next_on_success: "write_dispose_log".to_string(),
next_on_failure: "write_dispose_log".to_string(),
..MonitoringExecutionStepIr::default()
},
MonitoringExecutionStepIr {
step_id: "write_dispose_log".to_string(),
step_type: "log_write".to_string(),
log_write_contract_ref: "dispose_log_after_dispatch".to_string(),
next_on_success: "dispatch_loop".to_string(),
next_on_failure: "dispatch_loop".to_string(),
..MonitoringExecutionStepIr::default()
},
MonitoringExecutionStepIr {
step_id: "queue_continue_empty".to_string(),
step_type: "queue_transition".to_string(),
queue_transition_ref: "queue_continue_on_empty".to_string(),
next_on_success: "done".to_string(),
..MonitoringExecutionStepIr::default()
},
MonitoringExecutionStepIr {
step_id: "queue_continue_done".to_string(),
step_type: "queue_transition".to_string(),
queue_transition_ref: "queue_continue_on_done".to_string(),
next_on_success: "done".to_string(),
..MonitoringExecutionStepIr::default()
},
MonitoringExecutionStepIr {
step_id: "done".to_string(),
step_type: "terminal".to_string(),
..MonitoringExecutionStepIr::default()
},
],
};
let result_state_machine = MonitoringResultStateMachineIr {
state_machine_id: "dispatch_result_state_machine".to_string(),
action_contract_ref: "dispatch_exception_order".to_string(),
success_match: json!({
"allOf": [
{"field": "code", "equals": "00000"},
{"field": "message", "equals": "success"}
]
}),
failure_match: json!({
"fallback": "not_success_match"
}),
state_on_success: "success".to_string(),
state_on_failure: "failure".to_string(),
post_success_log_contract_ref: "dispose_log_after_dispatch".to_string(),
post_failure_log_contract_ref: "dispose_log_after_dispatch".to_string(),
continue_policy: "continue_next_item".to_string(),
};
let queue_transition_rules = vec![
MonitoringQueueTransitionRuleIr {
transition_id: "queue_continue_on_empty".to_string(),
queue_action_ref: queue_action_ref.clone(),
trigger_point: "on_empty_collection".to_string(),
required_mode: queue_required_mode.clone(),
blocked_by_default: true,
},
MonitoringQueueTransitionRuleIr {
transition_id: "queue_continue_on_done".to_string(),
queue_action_ref,
trigger_point: "on_all_items_done".to_string(),
required_mode: queue_required_mode,
blocked_by_default: true,
},
];
let log_write_contract = MonitoringLogWriteContractIr {
log_id: "dispose_log_after_dispatch".to_string(),
dependency_ref: dispose_log_dependency_ref,
target_endpoint_or_host_call: if dispose_log_dependency.url.is_empty() {
"setDisposeLog".to_string()
} else {
dispose_log_dependency.url.clone()
},
emit_phase: "after_dispatch_result".to_string(),
payload_template: json!({
"type": "fee_control_exception_dispose",
"orderID": "$derived.orderID",
"name": "$derived.name",
"time": "$runtime.now",
"state": "$dispatch.state"
}),
field_bindings: dispose_log_field_bindings,
state_binding: "dispatch_result_state".to_string(),
};
if workflow.action_contracts.is_empty() {
workflow.action_contracts = vec![action_contract];
}
if workflow.iteration_contract.is_none() {
workflow.iteration_contract = Some(iteration_contract);
}
if workflow.execution_flow.is_none() {
workflow.execution_flow = Some(execution_flow);
}
if workflow.result_state_machines.is_empty() {
workflow.result_state_machines = vec![result_state_machine];
}
if workflow.queue_transition_rules.is_empty() {
workflow.queue_transition_rules = queue_transition_rules;
}
if workflow.log_write_contracts.is_empty() {
workflow.log_write_contracts = vec![log_write_contract];
}
}
fn is_command_center_monitoring_scene(scene_id: &str, workflow_id: &str) -> bool {
scene_id == "command-center-fee-control-monitor"
|| workflow_id == "command_center_fee_control_monitoring_action"
}
fn monitoring_dependencies(value: &Value) -> Vec<MonitoringDependencyIr> { fn monitoring_dependencies(value: &Value) -> Vec<MonitoringDependencyIr> {
value value
.as_array() .as_array()
@@ -7257,14 +7543,28 @@ if (typeof module !== 'undefined') {{
} }
fn compile_scheduled_monitoring_action_plan_script(scene_ir: &SceneIr) -> String { fn compile_scheduled_monitoring_action_plan_script(scene_ir: &SceneIr) -> String {
let workflow = scene_ir
.monitoring_action_workflow
.as_ref()
.cloned()
.unwrap_or_default();
let blocked = scene_ir let blocked = scene_ir
.monitoring_action_workflow .monitoring_action_workflow
.as_ref() .as_ref()
.map(|workflow| workflow.side_effect_policy.blocked_call_signatures.clone()) .map(|workflow| workflow.side_effect_policy.blocked_call_signatures.clone())
.unwrap_or_default(); .unwrap_or_default();
let blocked_json = serde_json::to_string_pretty(&blocked).unwrap_or_else(|_| "[]".to_string()); let blocked_json = serde_json::to_string_pretty(&blocked).unwrap_or_else(|_| "[]".to_string());
let action_contracts_json =
serde_json::to_string_pretty(&workflow.action_contracts).unwrap_or_else(|_| "[]".to_string());
let queue_transitions_json = serde_json::to_string_pretty(&workflow.queue_transition_rules)
.unwrap_or_else(|_| "[]".to_string());
let log_write_contracts_json =
serde_json::to_string_pretty(&workflow.log_write_contracts).unwrap_or_else(|_| "[]".to_string());
format!( format!(
r#"const BLOCKED_CALL_SIGNATURES = {blocked_json}; r#"const BLOCKED_CALL_SIGNATURES = {blocked_json};
const ACTION_CONTRACTS = {action_contracts_json};
const QUEUE_TRANSITION_RULES = {queue_transitions_json};
const LOG_WRITE_CONTRACTS = {log_write_contracts_json};
function normalizeDecision(decision) {{ function normalizeDecision(decision) {{
if (typeof decision === 'string') {{ if (typeof decision === 'string') {{
@@ -7276,22 +7576,42 @@ function normalizeDecision(decision) {{
function buildActionPlan(decision = {{}}, args = {{}}) {{ function buildActionPlan(decision = {{}}, args = {{}}) {{
const source = normalizeDecision(decision); const source = normalizeDecision(decision);
const pendingList = Array.isArray(source.pendingList) ? source.pendingList : []; const pendingList = Array.isArray(source.pendingList) ? source.pendingList : [];
const dispatchContract = ACTION_CONTRACTS.find(item => item.actionId === 'dispatch_exception_order') || ACTION_CONTRACTS[0] || {{}};
const actionPlan = pendingList.map((item, index) => ({{ const actionPlan = pendingList.map((item, index) => ({{
itemId: String(item.id || item.consNo || item.custNo || item.workOrderId || index), itemId: String(item.id || item.consNo || item.custNo || item.workOrderId || index),
actionType: 'business_dispatch', actionType: dispatchContract.actionType || 'business_dispatch',
targetEndpointOrHostCall: 'repetCtrlSend', actionContractRef: dispatchContract.actionId || 'dispatch_exception_order',
targetEndpointOrHostCall: dispatchContract.targetEndpointOrHostCall || 'repetCtrlSend',
blockedByDefault: true, blockedByDefault: true,
requiresFutureGate: 'dispatch_gate', requiresFutureGate: 'dispatch_gate',
reason: 'scheduled monitoring preview only' reason: 'scheduled monitoring preview only'
}})); }}));
const queueTransitions = pendingList.length > 0
? QUEUE_TRANSITION_RULES.filter(item => item.transitionId === 'queue_continue_on_done')
: QUEUE_TRANSITION_RULES.filter(item => item.transitionId === 'queue_continue_on_empty');
const logWritePreview = pendingList.length > 0
? LOG_WRITE_CONTRACTS.map(item => ({{
logId: item.logId,
targetEndpointOrHostCall: item.targetEndpointOrHostCall,
emitPhase: item.emitPhase,
blockedByDefault: true
}}))
: [];
return {{ return {{
type: 'scheduled-monitoring-action-plan-preview', type: 'scheduled-monitoring-action-plan-preview',
status: 'action-plan-ok', status: 'action-plan-ok',
mode: args.mode || source.mode || 'monitor_only', mode: args.mode || source.mode || 'monitor_only',
actionPlan, actionPlan,
queueTransitions,
logWritePreview,
blockedSideEffects: {{ blockedSideEffects: {{
blockedCallSignatures: BLOCKED_CALL_SIGNATURES blockedCallSignatures: BLOCKED_CALL_SIGNATURES
}}, }},
summary: {{
action_plan_count: actionPlan.length,
queue_transition_count: queueTransitions.length,
log_write_preview_count: logWritePreview.length
}},
sideEffectCounters: {{ sideEffectCounters: {{
repetCtrlSend: 0, repetCtrlSend: 0,
sendMessages: 0, sendMessages: 0,
@@ -7308,7 +7628,10 @@ if (typeof module !== 'undefined') {{
module.exports = {{ buildActionPlan }}; module.exports = {{ buildActionPlan }};
}} }}
"#, "#,
blocked_json = blocked_json blocked_json = blocked_json,
action_contracts_json = action_contracts_json,
queue_transitions_json = queue_transitions_json,
log_write_contracts_json = log_write_contracts_json
) )
} }
@@ -8867,6 +9190,12 @@ fn scheduled_monitoring_generation_report(
"sidecarOutputs": workflow.runtime_context.output_contract.sidecar_outputs, "sidecarOutputs": workflow.runtime_context.output_contract.sidecar_outputs,
"deltaState": workflow.runtime_context.output_contract.delta_state, "deltaState": workflow.runtime_context.output_contract.delta_state,
}, },
"actionContracts": workflow.action_contracts,
"iterationContract": workflow.iteration_contract,
"executionFlow": workflow.execution_flow,
"resultStateMachines": workflow.result_state_machines,
"queueTransitionRules": workflow.queue_transition_rules,
"logWriteContracts": workflow.log_write_contracts,
"triggerContractStatus": trigger_contract["status"], "triggerContractStatus": trigger_contract["status"],
"readiness": scene_ir.readiness, "readiness": scene_ir.readiness,
}) })
@@ -9129,4 +9458,40 @@ mod tests {
assert!(rendered.contains("[[runtime_context.output_contract.sidecar_outputs]]")); assert!(rendered.contains("[[runtime_context.output_contract.sidecar_outputs]]"));
assert!(rendered.contains("archetype = \"business_page_report_monitor\"")); assert!(rendered.contains("archetype = \"business_page_report_monitor\""));
} }
#[test]
fn command_center_enrichment_backfills_missing_semantics_without_overwriting_existing_ones() {
let mut workflow = test_workflow("marketing_gateway_monitor");
workflow.workflow_id = "command_center_fee_control_monitoring_action".to_string();
workflow.action_contracts.push(MonitoringActionContractIr {
action_id: "preexisting_dispatch".to_string(),
..MonitoringActionContractIr::default()
});
enrich_command_center_automation_semantics(
"command-center-fee-control-monitor",
&json!({}),
&json!({
"triggerContracts": {
"queue": {
"futureModes": ["queue_process"]
}
},
"platformRuntimeCapabilities": {
"hostActionBridge": [
{ "name": "mac.exeTQueue" }
]
}
}),
&mut workflow,
);
assert_eq!(workflow.action_contracts.len(), 1);
assert_eq!(workflow.action_contracts[0].action_id, "preexisting_dispatch");
assert!(workflow.iteration_contract.is_some());
assert!(workflow.execution_flow.is_some());
assert_eq!(workflow.result_state_machines.len(), 1);
assert_eq!(workflow.queue_transition_rules.len(), 2);
assert_eq!(workflow.log_write_contracts.len(), 1);
}
} }

View File

@@ -477,6 +477,138 @@ pub struct MonitoringSideEffectPolicyIr {
pub blocked_actions: Vec<MonitoringSideEffectIr>, pub blocked_actions: Vec<MonitoringSideEffectIr>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringActionContractIr {
#[serde(rename = "actionId", default)]
pub action_id: String,
#[serde(rename = "actionType", default)]
pub action_type: String,
#[serde(rename = "dependencyRef", default)]
pub dependency_ref: String,
#[serde(rename = "targetEndpointOrHostCall", default)]
pub target_endpoint_or_host_call: String,
#[serde(rename = "executionContext", default)]
pub execution_context: String,
#[serde(rename = "inputSource", default)]
pub input_source: String,
#[serde(rename = "requestTemplate", default)]
pub request_template: Value,
#[serde(rename = "fieldBindings", default)]
pub field_bindings: Map<String, Value>,
#[serde(rename = "authBinding", default)]
pub auth_binding: Value,
#[serde(rename = "encryptionBinding", default)]
pub encryption_binding: Value,
#[serde(rename = "resultChannel", default)]
pub result_channel: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringIterationContractIr {
#[serde(rename = "iterationId", default)]
pub iteration_id: String,
#[serde(rename = "sourceCollection", default)]
pub source_collection: String,
#[serde(rename = "iterationMode", default)]
pub iteration_mode: String,
#[serde(rename = "itemAlias", default)]
pub item_alias: String,
#[serde(rename = "onEmptyTransition", default)]
pub on_empty_transition: String,
#[serde(rename = "onItemCompleteTransition", default)]
pub on_item_complete_transition: String,
#[serde(rename = "onAllCompleteTransition", default)]
pub on_all_complete_transition: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringExecutionStepIr {
#[serde(rename = "stepId", default)]
pub step_id: String,
#[serde(rename = "stepType", default)]
pub step_type: String,
#[serde(rename = "iterationRef", default)]
pub iteration_ref: String,
#[serde(rename = "actionContractRef", default)]
pub action_contract_ref: String,
#[serde(rename = "logWriteContractRef", default)]
pub log_write_contract_ref: String,
#[serde(rename = "queueTransitionRef", default)]
pub queue_transition_ref: String,
#[serde(rename = "nextOnSuccess", default)]
pub next_on_success: String,
#[serde(rename = "nextOnFailure", default)]
pub next_on_failure: String,
#[serde(rename = "nextOnEmpty", default)]
pub next_on_empty: String,
#[serde(rename = "nextOnDone", default)]
pub next_on_done: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringExecutionFlowIr {
#[serde(rename = "flowId", default)]
pub flow_id: String,
#[serde(rename = "entryStep", default)]
pub entry_step: String,
#[serde(rename = "steps", default)]
pub steps: Vec<MonitoringExecutionStepIr>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringResultStateMachineIr {
#[serde(rename = "stateMachineId", default)]
pub state_machine_id: String,
#[serde(rename = "actionContractRef", default)]
pub action_contract_ref: String,
#[serde(rename = "successMatch", default)]
pub success_match: Value,
#[serde(rename = "failureMatch", default)]
pub failure_match: Value,
#[serde(rename = "stateOnSuccess", default)]
pub state_on_success: String,
#[serde(rename = "stateOnFailure", default)]
pub state_on_failure: String,
#[serde(rename = "postSuccessLogContractRef", default)]
pub post_success_log_contract_ref: String,
#[serde(rename = "postFailureLogContractRef", default)]
pub post_failure_log_contract_ref: String,
#[serde(rename = "continuePolicy", default)]
pub continue_policy: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringQueueTransitionRuleIr {
#[serde(rename = "transitionId", default)]
pub transition_id: String,
#[serde(rename = "queueActionRef", default)]
pub queue_action_ref: String,
#[serde(rename = "triggerPoint", default)]
pub trigger_point: String,
#[serde(rename = "requiredMode", default)]
pub required_mode: String,
#[serde(rename = "blockedByDefault", default)]
pub blocked_by_default: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringLogWriteContractIr {
#[serde(rename = "logId", default)]
pub log_id: String,
#[serde(rename = "dependencyRef", default)]
pub dependency_ref: String,
#[serde(rename = "targetEndpointOrHostCall", default)]
pub target_endpoint_or_host_call: String,
#[serde(rename = "emitPhase", default)]
pub emit_phase: String,
#[serde(rename = "payloadTemplate", default)]
pub payload_template: Value,
#[serde(rename = "fieldBindings", default)]
pub field_bindings: Map<String, Value>,
#[serde(rename = "stateBinding", default)]
pub state_binding: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MonitoringActionWorkflowIr { pub struct MonitoringActionWorkflowIr {
#[serde(rename = "workflowId", default)] #[serde(rename = "workflowId", default)]
@@ -505,6 +637,18 @@ pub struct MonitoringActionWorkflowIr {
pub preview_schema: Vec<String>, pub preview_schema: Vec<String>,
#[serde(rename = "sideEffectPolicy", default)] #[serde(rename = "sideEffectPolicy", default)]
pub side_effect_policy: MonitoringSideEffectPolicyIr, pub side_effect_policy: MonitoringSideEffectPolicyIr,
#[serde(rename = "actionContracts", default)]
pub action_contracts: Vec<MonitoringActionContractIr>,
#[serde(rename = "iterationContract", default)]
pub iteration_contract: Option<MonitoringIterationContractIr>,
#[serde(rename = "executionFlow", default)]
pub execution_flow: Option<MonitoringExecutionFlowIr>,
#[serde(rename = "resultStateMachines", default)]
pub result_state_machines: Vec<MonitoringResultStateMachineIr>,
#[serde(rename = "queueTransitionRules", default)]
pub queue_transition_rules: Vec<MonitoringQueueTransitionRuleIr>,
#[serde(rename = "logWriteContracts", default)]
pub log_write_contracts: Vec<MonitoringLogWriteContractIr>,
#[serde(rename = "archetype", default = "default_monitoring_archetype")] #[serde(rename = "archetype", default = "default_monitoring_archetype")]
pub archetype: String, pub archetype: String,
} }

View File

@@ -1301,6 +1301,16 @@ fn run_scheduled_monitoring_skill_runtime(
.and_then(Value::as_array) .and_then(Value::as_array)
.cloned() .cloned()
.unwrap_or_default(); .unwrap_or_default();
let queue_transitions = action_plan_preview
.get("queueTransitions")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let log_write_preview = action_plan_preview
.get("logWritePreview")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let detect_root_cause = detect_status_root_cause(&json!({ let detect_root_cause = detect_status_root_cause(&json!({
"detectSnapshot": detect_snapshot.clone() "detectSnapshot": detect_snapshot.clone()
})); }));
@@ -1322,11 +1332,14 @@ fn run_scheduled_monitoring_skill_runtime(
"pending_count": pending_count, "pending_count": pending_count,
"notify_count": notify_candidates.len(), "notify_count": notify_candidates.len(),
"action_plan_count": action_plan.len(), "action_plan_count": action_plan.len(),
"queue_transition_count": queue_transitions.len(),
"detect_root_cause": detect_root_cause "detect_root_cause": detect_root_cause
}, },
"pendingList": pending_list, "pendingList": pending_list,
"notifyCandidates": notify_candidates, "notifyCandidates": notify_candidates,
"actionPlan": action_plan, "actionPlan": action_plan,
"queueTransitions": queue_transitions,
"logWritePreview": log_write_preview,
"blockedSideEffects": { "blockedSideEffects": {
"blockedCallSignatures": blocked_call_signatures "blockedCallSignatures": blocked_call_signatures
}, },

View File

@@ -44,7 +44,7 @@
], ],
"source": "scheduled_monitoring_action_trigger_runtime_contract" "source": "scheduled_monitoring_action_trigger_runtime_contract"
}, },
"finishedAt": "2026-04-26T05:30:42.489979300+00:00", "finishedAt": "2026-05-06T04:36:29.926950300+00:00",
"mode": "dry_run", "mode": "dry_run",
"previewArtifact": { "previewArtifact": {
"actionPlan": [ "actionPlan": [
@@ -112,7 +112,7 @@
"repetCtrlSend": 0, "repetCtrlSend": 0,
"sendMessages": 0 "sendMessages": 0
}, },
"startedAt": "2026-04-26T05:30:42.489979300+00:00", "startedAt": "2026-05-06T04:36:29.926950300+00:00",
"status": "dry-run-runtime-pass", "status": "dry-run-runtime-pass",
"triggerType": "queue", "triggerType": "queue",
"warnings": [ "warnings": [
@@ -158,7 +158,7 @@
], ],
"source": "scheduled_monitoring_action_trigger_runtime_contract" "source": "scheduled_monitoring_action_trigger_runtime_contract"
}, },
"finishedAt": "2026-04-26T05:30:42.260973600+00:00", "finishedAt": "2026-05-06T04:36:29.680658700+00:00",
"mode": "monitor_only", "mode": "monitor_only",
"previewArtifact": { "previewArtifact": {
"actionPlan": [ "actionPlan": [
@@ -226,7 +226,7 @@
"repetCtrlSend": 0, "repetCtrlSend": 0,
"sendMessages": 0 "sendMessages": 0
}, },
"startedAt": "2026-04-26T05:30:42.260973600+00:00", "startedAt": "2026-05-06T04:36:29.680658700+00:00",
"status": "dry-run-runtime-pass", "status": "dry-run-runtime-pass",
"triggerType": "scheduled", "triggerType": "scheduled",
"warnings": [ "warnings": [

View File

@@ -3893,6 +3893,84 @@ fn generator_emits_scheduled_monitoring_action_skill_package() {
assert!(test_status.success()); assert!(test_status.success());
} }
#[test]
fn command_center_materializes_automation_semantics_into_workflow_ir() {
let output_root = temp_workspace("sgclaw-command-center-automation-workflow-ir");
let skill_root = generate_scheduled_monitoring_action_skill_package(
GenerateScheduledMonitoringActionSkillRequest {
scene_id: "command-center-fee-control-monitor".to_string(),
scene_name: "指挥中心费控异常监测".to_string(),
output_root: output_root.clone(),
source_evidence_json: PathBuf::from(
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
),
ir_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
),
trigger_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
),
},
)
.unwrap();
let workflow_ir: serde_json::Value = serde_json::from_str(
&fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(),
)
.unwrap();
let action_contracts = workflow_ir["actionContracts"]
.as_array()
.expect("expected actionContracts array");
assert!(
action_contracts.iter().any(|item| {
item["targetEndpointOrHostCall"] == "repetCtrlSend"
|| item["actionId"] == "dispatch_exception_order"
}),
"expected action contract for repetCtrlSend dispatch"
);
assert_eq!(
workflow_ir["iterationContract"]["sourceCollection"],
"pendingList",
"expected iteration contract over pendingList"
);
assert_eq!(
workflow_ir["iterationContract"]["iterationMode"],
"sequential_per_item",
"expected sequential per-item iteration"
);
let queue_transition_rules = workflow_ir["queueTransitionRules"]
.as_array()
.expect("expected queueTransitionRules array");
assert!(
queue_transition_rules.iter().any(|item| {
item["triggerPoint"] == "on_empty_collection"
|| item["transitionId"] == "queue_continue_on_empty"
}),
"expected queue continue transition for empty collection"
);
assert!(
queue_transition_rules.iter().any(|item| {
item["triggerPoint"] == "on_all_items_done"
|| item["transitionId"] == "queue_continue_on_done"
}),
"expected queue continue transition for completed collection"
);
let log_write_contracts = workflow_ir["logWriteContracts"]
.as_array()
.expect("expected logWriteContracts array");
assert!(
log_write_contracts.iter().any(|item| {
item["targetEndpointOrHostCall"] == "setDisposeLog"
|| item["logId"] == "dispose_log_after_dispatch"
}),
"expected dispose-log contract"
);
}
#[test] #[test]
fn generator_preserves_localhost_dependency_as_host_runtime_evidence() { fn generator_preserves_localhost_dependency_as_host_runtime_evidence() {
let analysis = analyze_scene_source(Path::new( let analysis = analyze_scene_source(Path::new(

View File

@@ -737,6 +737,233 @@ fn binary_wiring_registry_backed_skill_executes_read_only_scripts_with_runtime_i
); );
} }
#[test]
fn command_center_preview_reflects_automation_semantics() {
let workspace = temp_workspace("sgclaw-command-center-preview-automation-semantics");
let trigger_path = workspace.join("scheduled-trigger.json");
let output_path = workspace.join("run-record.json");
let config_path = workspace.join("sgclaw_config.json");
let rules_path = workspace.join("resources").join("rules.json");
let materialization_root = workspace.join("materialized");
fs::create_dir_all(&materialization_root).unwrap();
write_json(
&trigger_path,
&scheduled_trigger_with_runtime_inputs("monitor_only"),
);
write_runtime_rules(&rules_path);
generate_scheduled_monitoring_action_skill_package(
GenerateScheduledMonitoringActionSkillRequest {
scene_id: "command-center-fee-control-monitor".to_string(),
scene_name: "command-center-fee-control-monitor".to_string(),
output_root: materialization_root.clone(),
source_evidence_json: PathBuf::from(
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
),
ir_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
),
trigger_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
),
},
)
.unwrap();
let detect_payload = json!({
"type": "scheduled-monitoring-detect-snapshot",
"report_name": "command-center-fee-control-monitor",
"status": "detect-ok",
"workflowId": "command_center_fee_control_monitoring_action",
"mode": "monitor_only",
"pendingList": [
{ "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" }
],
"inputs": {
"source": "browser_attached_live_read",
"queryAbnorList": [
{ "id": "A1", "consNo": "C1", "phone": "13800000000", "abnorType": "fee_control" }
],
"queryHistoryEnergyCharge": [],
"getMonitorLog": { "lastHandled": "2026-04-22T08:00:00Z" },
"getOtherIphones": { "holidaySwitch": "off" },
"getAllSubMgtOrgTreeByOrgCode": {}
},
"localStorageSnapshot": {
"loginUserInfo": "{\"orgNo\":\"62401\"}",
"markToken": "browser-token",
"yxClassList": "[{\"orgNo\":\"62401\"}]",
"zhzxFkycSendTime": "2026-04-22 08:00:00"
},
"readDiagnostics": {
"source": "browser_attached_live_read",
"businessGatewayReadAttempted": true,
"localhostReadAttempted": true,
"queryAbnorListCount": 1,
"queryHistoryEnergyChargeCount": 0
},
"dependencySnapshot": {
"businessReads": [],
"localReads": [],
"blockedLocalWrites": [],
"blockedCalls": ["repetCtrlSend"]
},
"sideEffectCounters": {
"repetCtrlSend": 0,
"sendMessages": 0,
"callOutLogin": 0,
"audioPlay": 0,
"exeTQueue": 0,
"productionLogWrite": 0
}
});
let (browser_ws_url, browser_server) =
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
write_browser_config(&config_path, &browser_ws_url);
let output = run_binary_with_skills_dir_and_config(
&trigger_path,
&materialization_root.join("skills"),
&config_path,
&workspace,
&output_path,
);
browser_server.join().unwrap();
assert!(
output.status.success(),
"stdout={}\nstderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
let preview_artifact = &record["previewArtifact"];
assert_eq!(
preview_artifact["actionPlan"][0]["actionContractRef"],
"dispatch_exception_order"
);
assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1);
assert_eq!(
preview_artifact["queueTransitions"][0]["transitionId"],
"queue_continue_on_done"
);
assert_eq!(
preview_artifact["logWritePreview"][0]["logId"],
"dispose_log_after_dispatch"
);
}
#[test]
fn command_center_empty_pending_list_does_not_emit_log_write_preview() {
let workspace = temp_workspace("sgclaw-command-center-empty-preview-semantics");
let trigger_path = workspace.join("scheduled-trigger.json");
let output_path = workspace.join("run-record.json");
let config_path = workspace.join("sgclaw_config.json");
let rules_path = workspace.join("resources").join("rules.json");
let materialization_root = workspace.join("materialized");
fs::create_dir_all(&materialization_root).unwrap();
write_json(
&trigger_path,
&scheduled_trigger_with_runtime_inputs("monitor_only"),
);
write_runtime_rules(&rules_path);
generate_scheduled_monitoring_action_skill_package(
GenerateScheduledMonitoringActionSkillRequest {
scene_id: "command-center-fee-control-monitor".to_string(),
scene_name: "command-center-fee-control-monitor".to_string(),
output_root: materialization_root.clone(),
source_evidence_json: PathBuf::from(
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
),
ir_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
),
trigger_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
),
},
)
.unwrap();
let detect_payload = json!({
"type": "scheduled-monitoring-detect-snapshot",
"report_name": "command-center-fee-control-monitor",
"status": "detect-ok",
"workflowId": "command_center_fee_control_monitoring_action",
"mode": "monitor_only",
"pendingList": [],
"inputs": {
"source": "browser_attached_live_read",
"queryAbnorList": [],
"queryHistoryEnergyCharge": [],
"getMonitorLog": {},
"getOtherIphones": {},
"getAllSubMgtOrgTreeByOrgCode": {}
},
"localStorageSnapshot": {
"loginUserInfo": "{\"orgNo\":\"62401\"}",
"markToken": "browser-token",
"yxClassList": "[{\"orgNo\":\"62401\"}]",
"zhzxFkycSendTime": "2026-04-22 08:00:00"
},
"readDiagnostics": {
"source": "browser_attached_live_read",
"businessGatewayReadAttempted": true,
"localhostReadAttempted": true,
"queryAbnorListCount": 0,
"queryHistoryEnergyChargeCount": 0
},
"dependencySnapshot": {
"businessReads": [],
"localReads": [],
"blockedLocalWrites": [],
"blockedCalls": ["repetCtrlSend"]
},
"sideEffectCounters": {
"repetCtrlSend": 0,
"sendMessages": 0,
"callOutLogin": 0,
"audioPlay": 0,
"exeTQueue": 0,
"productionLogWrite": 0
}
});
let (browser_ws_url, browser_server) =
start_callback_host_scheduled_monitoring_browser_server(detect_payload);
write_browser_config(&config_path, &browser_ws_url);
let output = run_binary_with_skills_dir_and_config(
&trigger_path,
&materialization_root.join("skills"),
&config_path,
&workspace,
&output_path,
);
browser_server.join().unwrap();
assert!(
output.status.success(),
"stdout={}\nstderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let record: Value = serde_json::from_str(&fs::read_to_string(output_path).unwrap()).unwrap();
let preview_artifact = &record["previewArtifact"];
assert_eq!(preview_artifact["summary"]["pending_count"], 0);
assert_eq!(preview_artifact["summary"]["queue_transition_count"], 1);
assert_eq!(
preview_artifact["queueTransitions"][0]["transitionId"],
"queue_continue_on_empty"
);
assert_eq!(
preview_artifact["logWritePreview"].as_array().unwrap().len(),
0
);
}
#[test] #[test]
fn binary_wiring_browser_attached_passes_platform_service_base_from_config() { fn binary_wiring_browser_attached_passes_platform_service_base_from_config() {
let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-platform-service-base"); let workspace = temp_workspace("sgclaw-scheduled-monitoring-binary-platform-service-base");

View File

@@ -279,6 +279,58 @@ fn fee_control_materialization_emits_timeout_contract_in_generated_artifacts() {
assert!(detect_script.contains("statusOnTimeout")); assert!(detect_script.contains("statusOnTimeout"));
} }
#[test]
fn command_center_workflow_ir_includes_automation_semantics() {
let root = temp_workspace("sgclaw-command-center-automation-semantics");
let skill_root = generate_scheduled_monitoring_action_skill_package(
GenerateScheduledMonitoringActionSkillRequest {
scene_id: "command-center-fee-control-monitor".to_string(),
scene_name: "command-center-fee-control-monitor".to_string(),
output_root: root.clone(),
source_evidence_json: PathBuf::from(
"tests/fixtures/generated_scene/monitoring_action_source_evidence_extraction_2026-04-21.json",
),
ir_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_ir_contract_2026-04-22.json",
),
trigger_contract_json: PathBuf::from(
"tests/fixtures/generated_scene/scheduled_monitoring_action_trigger_runtime_contract_2026-04-22.json",
),
},
)
.unwrap();
let workflow_ir: Value = serde_json::from_str(
&fs::read_to_string(skill_root.join("references/workflow-ir.json")).unwrap(),
)
.unwrap();
assert!(
workflow_ir.get("actionContracts").is_some(),
"workflow-ir.json must include actionContracts"
);
assert!(
workflow_ir.get("iterationContract").is_some(),
"workflow-ir.json must include iterationContract"
);
assert!(
workflow_ir.get("executionFlow").is_some(),
"workflow-ir.json must include executionFlow"
);
assert!(
workflow_ir.get("resultStateMachines").is_some(),
"workflow-ir.json must include resultStateMachines"
);
assert!(
workflow_ir.get("queueTransitionRules").is_some(),
"workflow-ir.json must include queueTransitionRules"
);
assert!(
workflow_ir.get("logWriteContracts").is_some(),
"workflow-ir.json must include logWriteContracts"
);
}
#[test] #[test]
fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() { fn available_balance_and_archive_materialization_emit_explicit_dependency_classification_in_references() {
let example_output = Command::new("cargo") let example_output = Command::new("cargo")