feat: service console auto-connect, settings panel, and batch of enhancements
- Auto-connect WebSocket on page load in service console - Settings modal for editing sgclaw_config.json (API key, base URL, model, skills dir, etc.) - UpdateConfig/ConfigUpdated protocol messages for remote config save - save_to_path() for SgClawSettings serialization - ConfigUpdated handler in sg_claw_client binary - Protocol serialization tests for new message types - HTML test assertions for auto-connect and settings UI - Additional pending changes: deterministic submit, org units, lineloss xlsx export, browser script tool, and docs 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::browser::BrowserBackend;
|
||||
use crate::compat::artifact_open::{open_exported_xlsx, PostExportOpen};
|
||||
use crate::compat::direct_skill_runtime::DirectSubmitOutcome;
|
||||
use crate::compat::lineloss_xlsx_export::{export_lineloss_xlsx, LinelossExportRequest};
|
||||
use crate::config::SgClawSettings;
|
||||
use crate::pipe::{BrowserPipeTool, PipeError, Transport};
|
||||
|
||||
@@ -13,6 +15,7 @@ pub struct DeterministicExecutionPlan {
|
||||
pub instruction: String,
|
||||
pub tool_name: String,
|
||||
pub expected_domain: String,
|
||||
pub target_url: String,
|
||||
pub org_label: String,
|
||||
pub org_code: String,
|
||||
pub period_mode: String,
|
||||
@@ -30,6 +33,7 @@ pub enum DeterministicSubmitDecision {
|
||||
|
||||
const DETERMINISTIC_SUFFIX: &str = "。。。";
|
||||
const LINELLOSS_HOST: &str = "20.76.57.61";
|
||||
const LINELLOSS_TARGET_URL: &str = "http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor";
|
||||
const LINELLOSS_TOOL: &str = "tq-lineloss-report.collect_lineloss";
|
||||
|
||||
pub fn decide_deterministic_submit(
|
||||
@@ -85,6 +89,7 @@ pub fn decide_deterministic_submit(
|
||||
instruction: normalized_instruction.to_string(),
|
||||
tool_name: LINELLOSS_TOOL.to_string(),
|
||||
expected_domain: LINELLOSS_HOST.to_string(),
|
||||
target_url: LINELLOSS_TARGET_URL.to_string(),
|
||||
org_label: resolved_org.label,
|
||||
org_code: resolved_org.code,
|
||||
period_mode: period_mode_name(&resolved_period.mode).to_string(),
|
||||
@@ -110,7 +115,8 @@ pub fn execute_deterministic_submit<T: Transport + 'static>(
|
||||
args,
|
||||
)?;
|
||||
|
||||
Ok(summarize_lineloss_output(&output))
|
||||
let export_path = try_export_lineloss_xlsx(&output, workspace_root);
|
||||
Ok(summarize_lineloss_output_with_export(&output, export_path.as_deref()))
|
||||
}
|
||||
|
||||
pub fn execute_deterministic_submit_with_browser_backend(
|
||||
@@ -129,7 +135,8 @@ pub fn execute_deterministic_submit_with_browser_backend(
|
||||
args,
|
||||
)?;
|
||||
|
||||
Ok(summarize_lineloss_output(&output))
|
||||
let export_path = try_export_lineloss_xlsx(&output, workspace_root);
|
||||
Ok(summarize_lineloss_output_with_export(&output, export_path.as_deref()))
|
||||
}
|
||||
|
||||
fn deterministic_submit_args(plan: &DeterministicExecutionPlan) -> Map<String, Value> {
|
||||
@@ -138,6 +145,10 @@ fn deterministic_submit_args(plan: &DeterministicExecutionPlan) -> Map<String, V
|
||||
"expected_domain".to_string(),
|
||||
Value::String(plan.expected_domain.clone()),
|
||||
);
|
||||
args.insert(
|
||||
"target_url".to_string(),
|
||||
Value::String(plan.target_url.clone()),
|
||||
);
|
||||
args.insert(
|
||||
"org_label".to_string(),
|
||||
Value::String(plan.org_label.clone()),
|
||||
@@ -256,6 +267,155 @@ fn summarize_lineloss_artifact(artifact: &Value) -> DirectSubmitOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_lineloss_output_with_export(output: &str, export_path: Option<&Path>) -> DirectSubmitOutcome {
|
||||
let mut outcome = summarize_lineloss_output(output);
|
||||
|
||||
if let Some(path) = export_path {
|
||||
outcome.summary.push_str(&format!(" export_path={}", path.display()));
|
||||
match open_exported_xlsx(path) {
|
||||
PostExportOpen::Opened => {
|
||||
outcome.summary.push_str(" 已自动打开Excel");
|
||||
}
|
||||
PostExportOpen::Failed(reason) => {
|
||||
outcome.summary.push_str(&format!(" 自动打开Excel失败: {}", reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outcome
|
||||
}
|
||||
|
||||
struct LinelossArtifactExportData {
|
||||
sheet_name: String,
|
||||
column_defs: Vec<(String, String)>,
|
||||
rows: Vec<Map<String, Value>>,
|
||||
}
|
||||
|
||||
fn extract_export_data(output: &str) -> Option<LinelossArtifactExportData> {
|
||||
let payload: Value = serde_json::from_str(output).ok()?;
|
||||
let artifact = payload
|
||||
.as_object()
|
||||
.and_then(|object| object.get("text"))
|
||||
.unwrap_or(&payload);
|
||||
let artifact = artifact.as_object()?;
|
||||
|
||||
if artifact.get("type").and_then(Value::as_str) != Some("report-artifact") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let status = artifact.get("status").and_then(Value::as_str).unwrap_or("");
|
||||
if !matches!(status, "ok" | "partial") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let rows = artifact
|
||||
.get("rows")
|
||||
.and_then(Value::as_array)?;
|
||||
if rows.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let rows: Vec<Map<String, Value>> = rows
|
||||
.iter()
|
||||
.filter_map(|row| row.as_object().cloned())
|
||||
.collect();
|
||||
if rows.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let column_defs: Vec<(String, String)> = artifact
|
||||
.get("column_defs")
|
||||
.and_then(Value::as_array)
|
||||
.map(|defs| {
|
||||
defs.iter()
|
||||
.filter_map(|def| {
|
||||
let arr = def.as_array()?;
|
||||
let key = arr.first()?.as_str()?.to_string();
|
||||
let label = arr.get(1)?.as_str()?.to_string();
|
||||
Some((key, label))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Fallback: if column_defs not in artifact, try "columns" array as keys
|
||||
let column_defs = if column_defs.is_empty() {
|
||||
let columns = artifact
|
||||
.get("columns")
|
||||
.and_then(Value::as_array)?;
|
||||
columns
|
||||
.iter()
|
||||
.filter_map(|col| {
|
||||
let key = col.as_str()?.to_string();
|
||||
Some((key.clone(), key))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
column_defs
|
||||
};
|
||||
|
||||
if column_defs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let org_label = artifact
|
||||
.get("org")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|org| org.get("label"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("lineloss");
|
||||
let period_mode = artifact
|
||||
.get("period")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|p| p.get("mode"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("month");
|
||||
let period_value = artifact
|
||||
.get("period")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|p| p.get("value"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let mode_label = if period_mode == "week" { "周度" } else { "月度" };
|
||||
let sheet_name = format!("{org_label}{mode_label}线损分析报表({period_value})");
|
||||
|
||||
Some(LinelossArtifactExportData {
|
||||
sheet_name,
|
||||
column_defs,
|
||||
rows,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_export_lineloss_xlsx(
|
||||
output: &str,
|
||||
workspace_root: &Path,
|
||||
) -> Option<PathBuf> {
|
||||
let data = extract_export_data(output)?;
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or_default();
|
||||
let out_dir = workspace_root.join("out");
|
||||
let output_path = out_dir.join(format!("tq-lineloss-{nanos}.xlsx"));
|
||||
|
||||
let request = LinelossExportRequest {
|
||||
sheet_name: data.sheet_name,
|
||||
column_defs: data.column_defs,
|
||||
rows: data.rows,
|
||||
output_path,
|
||||
};
|
||||
|
||||
match export_lineloss_xlsx(&request) {
|
||||
Ok(path) => {
|
||||
eprintln!("[deterministic_submit] XLSX exported to: {}", path.display());
|
||||
Some(path)
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[deterministic_submit] XLSX export failed: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_exact_deterministic_suffix(raw_instruction: &str) -> Option<&str> {
|
||||
let without_suffix = raw_instruction.strip_suffix(DETERMINISTIC_SUFFIX)?;
|
||||
if without_suffix.ends_with('。') {
|
||||
|
||||
Reference in New Issue
Block a user