From a8a470481da7715c8487a2fd9086f1bba1c3e68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E7=82=8E?= <635735027@qq.com> Date: Sun, 12 Apr 2026 21:35:28 +0800 Subject: [PATCH] fix: align lineloss default periods with page semantics Default month/week deterministic lineloss requests to the source page's built-in time ranges while preserving explicit-period parsing and existing routing contracts. Co-Authored-By: Claude Sonnet 4.6 --- src/compat/tq_lineloss/period_resolver.rs | 89 +++++++-- tests/compat_runtime_test.rs | 223 ++++++++++++++++++++++ tests/deterministic_submit_test.rs | 85 ++++++++- 3 files changed, 379 insertions(+), 18 deletions(-) diff --git a/src/compat/tq_lineloss/period_resolver.rs b/src/compat/tq_lineloss/period_resolver.rs index 8c60f45..a986aee 100644 --- a/src/compat/tq_lineloss/period_resolver.rs +++ b/src/compat/tq_lineloss/period_resolver.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, Duration, NaiveDate}; +use chrono::{Datelike, Duration, Local, NaiveDate}; use serde_json::json; use super::contracts::{ @@ -37,7 +37,11 @@ fn resolve_month_period(input: &str) -> Result { }); } - Err(missing_period_prompt()) + if contains_explicit_month_period_hint(input) { + return Err(missing_period_prompt()); + } + + Ok(default_month_period()) } fn resolve_week_period(input: &str) -> Result { @@ -45,26 +49,83 @@ fn resolve_week_period(input: &str) -> Result { return Err(missing_week_year_prompt()); } - let Some((year, week)) = extract_year_week(input) else { - return Err(missing_period_prompt()); - }; + if let Some((year, week)) = extract_year_week(input) { + let Some(week_start) = week_start_date(year, week) else { + return Err(missing_period_prompt()); + }; + let week_end = week_start + Duration::days(6); - let Some(week_start) = week_start_date(year, week) else { - return Err(missing_period_prompt()); - }; - let week_end = week_start + Duration::days(6); + return Ok(ResolvedPeriod { + mode: PeriodMode::Week, + mode_code: "2".to_string(), + value: format!("{year}-W{week:02}"), + payload: json!({ + "tjzq": "week", + "level": "00", + "weekSfdate": week_start.format("%Y-%m-%d").to_string(), + "weekEfdate": week_end.format("%Y-%m-%d").to_string(), + }), + }); + } - Ok(ResolvedPeriod { + if contains_explicit_week_period_hint(input) { + return Err(missing_period_prompt()); + } + + Ok(default_week_period()) +} + +fn default_month_period() -> ResolvedPeriod { + let today = Local::now().date_naive(); + let (year, month) = if today.month() == 1 { + (today.year() - 1, 12) + } else { + (today.year(), today.month() - 1) + }; + let value = format!("{year}-{month:02}"); + + ResolvedPeriod { + mode: PeriodMode::Month, + mode_code: "1".to_string(), + value: value.clone(), + payload: json!({ "fdate": value }), + } +} + +fn default_week_period() -> ResolvedPeriod { + let today = Local::now().date_naive(); + let month_start = today.with_day(1).expect("current month should have day 1"); + let start = month_start.format("%Y-%m-%d").to_string(); + let end = today.format("%Y-%m-%d").to_string(); + + ResolvedPeriod { mode: PeriodMode::Week, mode_code: "2".to_string(), - value: format!("{year}-W{week:02}"), + value: format!("{start}至{end}"), payload: json!({ "tjzq": "week", "level": "00", - "weekSfdate": week_start.format("%Y-%m-%d").to_string(), - "weekEfdate": week_end.format("%Y-%m-%d").to_string(), + "weekSfdate": start, + "weekEfdate": end, }), - }) + } +} + +fn contains_explicit_month_period_hint(input: &str) -> bool { + let trimmed = input.replace("月累计", ""); + trimmed.contains('年') + || trimmed.contains('月') + || trimmed.contains('-') + || trimmed.chars().any(|ch| ch.is_ascii_digit()) +} + +fn contains_explicit_week_period_hint(input: &str) -> bool { + let trimmed = input.replace("周累计", ""); + trimmed.contains('年') + || trimmed.contains('第') + || trimmed.contains('周') + || trimmed.contains('-') + || trimmed.chars().any(|ch| ch.is_ascii_digit()) } fn extract_year_month_dash(input: &str) -> Option { diff --git a/tests/compat_runtime_test.rs b/tests/compat_runtime_test.rs index c4c4039..5ed59f9 100644 --- a/tests/compat_runtime_test.rs +++ b/tests/compat_runtime_test.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex, OnceLock}; use std::thread; use std::time::Duration; +use chrono::{Datelike, Local}; use common::MockTransport; use serde_json::{json, Value}; use sgclaw::agent::{ @@ -276,6 +277,24 @@ fn extract_generated_artifact_path(summary: &str, extension: &str) -> PathBuf { .expect("expected artifact path in task summary") } +fn expected_default_month() -> String { + let today = Local::now().date_naive(); + let (year, month) = if today.month() == 1 { + (today.year() - 1, 12) + } else { + (today.year(), today.month() - 1) + }; + format!("{year}-{month:02}") +} + +fn expected_default_week_range() -> (String, String, String) { + let today = Local::now().date_naive(); + let month_start = today.with_day(1).expect("current month should have day 1"); + let start = month_start.format("%Y-%m-%d").to_string(); + let end = today.format("%Y-%m-%d").to_string(); + (format!("{start}至{end}"), start, end) +} + #[test] fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); @@ -3686,6 +3705,210 @@ fn deterministic_lineloss_runtime_passes_canonical_args_to_browser_script_tool() })); } +#[test] +fn deterministic_lineloss_runtime_defaults_missing_month_period_to_page_semantics() { + let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); + let expected_month = expected_default_month(); + + let workspace_root = temp_workspace_root(); + let config_path = write_deepseek_config_with_skills_dir( + &workspace_root, + "deepseek-test-key", + "http://127.0.0.1:9", + "deepseek-chat", + Some(staged_lineloss_skills_root().to_str().unwrap()), + ); + let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); + + let transport = Arc::new(MockTransport::new(vec![success_browser_response( + 1, + json!({ + "text": { + "type": "report-artifact", + "report_name": "tq-lineloss-report", + "status": "ok", + "org": { + "label": "国网兰州供电公司", + "code": "62401" + }, + "period": { + "mode": "month", + "mode_code": "1", + "value": expected_month, + "payload": { + "fdate": expected_default_month() + } + }, + "rows": [ + { "ORG_NAME": "国网兰州供电公司", "LINE_LOSS_RATE": "3.21" } + ], + "counts": { + "rows": 1 + }, + "reasons": [] + } + }), + )])); + let browser_tool = BrowserPipeTool::new( + transport.clone(), + policy_for_domains(&["20.76.57.61"]), + vec![1, 2, 3, 4, 5, 6, 7, 8], + ) + .with_response_timeout(Duration::from_secs(1)); + + handle_browser_message_with_context( + transport.as_ref(), + &browser_tool, + &runtime_context, + BrowserMessage::SubmitTask { + instruction: "兰州公司 月累计。。。".to_string(), + conversation_id: String::new(), + messages: vec![], + page_url: "http://20.76.57.61:8080/#/lineloss".to_string(), + page_title: "台区线损报表".to_string(), + }, + ) + .unwrap(); + + let sent = transport.sent_messages(); + + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::TaskComplete { success, summary } + if *success + && summary.contains("tq-lineloss-report") + && summary.contains("国网兰州供电公司") + && summary.contains(&expected_default_month()) + && summary.contains("rows=1") + ) + })); + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::Command { + action, + params, + security, + .. + } + if action == &Action::Eval + && security.expected_domain == "20.76.57.61" + && params["script"].as_str().is_some_and(|script| + script.contains("\"period_mode\":\"month\"") + && script.contains("\"period_mode_code\":\"1\"") + && script.contains(&format!("\"period_value\":\"{}\"", expected_default_month())) + && script.contains("\"period_payload\":\"{") + && script.contains(&format!("\\\"fdate\\\":\\\"{}\\\"", expected_default_month())) + ) + ) + }), "sent messages were: {sent:?}"); +} + +#[test] +fn deterministic_lineloss_runtime_defaults_missing_week_period_to_page_semantics() { + let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); + let (expected_value, expected_start, expected_end) = expected_default_week_range(); + + let workspace_root = temp_workspace_root(); + let config_path = write_deepseek_config_with_skills_dir( + &workspace_root, + "deepseek-test-key", + "http://127.0.0.1:9", + "deepseek-chat", + Some(staged_lineloss_skills_root().to_str().unwrap()), + ); + let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); + + let transport = Arc::new(MockTransport::new(vec![success_browser_response( + 1, + json!({ + "text": { + "type": "report-artifact", + "report_name": "tq-lineloss-report", + "status": "ok", + "org": { + "label": "国网兰州供电公司", + "code": "62401" + }, + "period": { + "mode": "week", + "mode_code": "2", + "value": expected_value, + "payload": { + "tjzq": "week", + "level": "00", + "weekSfdate": expected_start, + "weekEfdate": expected_end + } + }, + "rows": [ + { "ORG_NAME3": "国网兰州供电公司", "LINELOSS_RATE": "3.21" } + ], + "counts": { + "rows": 1 + }, + "reasons": [] + } + }), + )])); + let browser_tool = BrowserPipeTool::new( + transport.clone(), + policy_for_domains(&["20.76.57.61"]), + vec![1, 2, 3, 4, 5, 6, 7, 8], + ) + .with_response_timeout(Duration::from_secs(1)); + + handle_browser_message_with_context( + transport.as_ref(), + &browser_tool, + &runtime_context, + BrowserMessage::SubmitTask { + instruction: "兰州公司 周累计。。。".to_string(), + conversation_id: String::new(), + messages: vec![], + page_url: "http://20.76.57.61:8080/#/lineloss".to_string(), + page_title: "台区线损报表".to_string(), + }, + ) + .unwrap(); + + let sent = transport.sent_messages(); + + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::TaskComplete { success, summary } + if *success + && summary.contains("tq-lineloss-report") + && summary.contains("国网兰州供电公司") + && summary.contains(&expected_value) + && summary.contains("rows=1") + ) + })); + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::Command { + action, + params, + security, + .. + } + if action == &Action::Eval + && security.expected_domain == "20.76.57.61" + && params["script"].as_str().is_some_and(|script| + script.contains("\"period_mode\":\"week\"") + && script.contains("\"period_mode_code\":\"2\"") + && script.contains(&format!("\"period_value\":\"{}\"", expected_value)) + && script.contains("\"period_payload\":\"{") + && script.contains(&format!("\\\"weekSfdate\\\":\\\"{}\\\"", expected_start)) + && script.contains(&format!("\\\"weekEfdate\\\":\\\"{}\\\"", expected_end)) + ) + ) + }), "sent messages were: {sent:?}"); +} + #[test] fn deterministic_lineloss_missing_company_prompt_skips_browser_execution() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); diff --git a/tests/deterministic_submit_test.rs b/tests/deterministic_submit_test.rs index 0149dbd..503657f 100644 --- a/tests/deterministic_submit_test.rs +++ b/tests/deterministic_submit_test.rs @@ -2,6 +2,7 @@ mod common; use std::path::PathBuf; +use chrono::{Datelike, Local}; use zeroclaw::skills::load_skills_from_directory; use sgclaw::compat::deterministic_submit::{ @@ -14,6 +15,24 @@ use sgclaw::compat::tq_lineloss::{ }; use sgclaw::runtime::is_zhihu_hotlist_task; +fn expected_default_month() -> String { + let today = Local::now().date_naive(); + let (year, month) = if today.month() == 1 { + (today.year() - 1, 12) + } else { + (today.year(), today.month() - 1) + }; + format!("{year}-{month:02}") +} + +fn expected_default_week_range() -> (String, String, String) { + let today = Local::now().date_naive(); + let month_start = today.with_day(1).expect("current month should have day 1"); + let start = month_start.format("%Y-%m-%d").to_string(); + let end = today.format("%Y-%m-%d").to_string(); + (format!("{start}至{end}"), start, end) +} + #[test] fn deterministic_submit_discovers_tq_lineloss_skill_contract() { let skills_root = PathBuf::from("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging/skills"); @@ -225,6 +244,43 @@ fn lineloss_period_resolver_parses_week_text() { assert_eq!(resolved.payload["weekEfdate"], "2026-03-22"); } +#[test] +fn lineloss_period_resolver_defaults_month_period_from_page_semantics() { + let expected_month = expected_default_month(); + + assert_eq!( + resolve_period("兰州公司 月累计").unwrap(), + ResolvedPeriod { + mode: PeriodMode::Month, + mode_code: "1".to_string(), + value: expected_month.clone(), + payload: serde_json::json!({ + "fdate": expected_month, + }), + } + ); +} + +#[test] +fn lineloss_period_resolver_defaults_week_period_from_page_semantics() { + let (expected_value, expected_start, expected_end) = expected_default_week_range(); + + assert_eq!( + resolve_period("兰州公司 周累计").unwrap(), + ResolvedPeriod { + mode: PeriodMode::Week, + mode_code: "2".to_string(), + value: expected_value, + payload: serde_json::json!({ + "tjzq": "week", + "level": "00", + "weekSfdate": expected_start, + "weekEfdate": expected_end, + }), + } + ); +} + #[test] fn lineloss_period_resolver_prompts_for_missing_year_on_week() { let summary = resolve_period("周累计 第12周") @@ -279,14 +335,35 @@ fn deterministic_lineloss_execution_plan_contains_canonical_args() { } #[test] -fn deterministic_lineloss_missing_period_does_not_reach_execution_plan() { +fn deterministic_lineloss_missing_period_uses_default_month_execution_plan() { + let expected_month = expected_default_month(); let decision = decide_deterministic_submit("兰州公司 月累计。。。", None, None); match decision { - DeterministicSubmitDecision::Prompt { summary } => { - assert!(summary.contains("周期") || summary.contains("时间") || summary.contains("2026-03")); + DeterministicSubmitDecision::Execute(plan) => { + assert_eq!(plan.period_mode, "month"); + assert_eq!(plan.period_mode_code, "1"); + assert_eq!(plan.period_value, expected_month); + assert!(plan.period_payload.contains("fdate")); } - other => panic!("expected missing-period prompt before execution, got {other:?}"), + other => panic!("expected missing month period to default into execution, got {other:?}"), + } +} + +#[test] +fn deterministic_lineloss_missing_period_uses_default_week_execution_plan() { + let (expected_value, expected_start, expected_end) = expected_default_week_range(); + let decision = decide_deterministic_submit("兰州公司 周累计。。。", None, None); + + match decision { + DeterministicSubmitDecision::Execute(plan) => { + assert_eq!(plan.period_mode, "week"); + assert_eq!(plan.period_mode_code, "2"); + assert_eq!(plan.period_value, expected_value); + assert!(plan.period_payload.contains(&expected_start)); + assert!(plan.period_payload.contains(&expected_end)); + } + other => panic!("expected missing week period to default into execution, got {other:?}"), } }