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,5 +1,5 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::browser::BrowserBackend;
|
||||
@@ -64,6 +64,10 @@ impl AgentRuntimeContext {
|
||||
.map_err(|err| PipeError::Protocol(err.to_string()))
|
||||
}
|
||||
|
||||
pub fn config_path(&self) -> Option<&Path> {
|
||||
self.config_path.as_deref()
|
||||
}
|
||||
|
||||
fn settings_source_label(&self) -> String {
|
||||
match &self.config_path {
|
||||
Some(path) if path.exists() => path.display().to_string(),
|
||||
|
||||
@@ -84,6 +84,13 @@ fn run() -> Result<(), String> {
|
||||
break;
|
||||
}
|
||||
ServiceMessage::Pong => {}
|
||||
ServiceMessage::ConfigUpdated { success, message } => {
|
||||
if success {
|
||||
println!("config updated: {message}");
|
||||
} else {
|
||||
eprintln!("config update failed: {message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Close(_) => {
|
||||
|
||||
@@ -436,14 +436,16 @@ fn build_eval_js(source_url: &str, script: &str) -> String {
|
||||
let events_url = escape_js_single_quoted(&events_endpoint_url(source_url));
|
||||
|
||||
format!(
|
||||
"(async function(){{try{{\
|
||||
var v=await (async function(){{return {script}}})();\
|
||||
if(v&&typeof v.then==='function'){{v=await v;}}\
|
||||
"(function(){{try{{\
|
||||
var v=(function(){{return {script}}})();\
|
||||
function _s(v){{\
|
||||
var t=(typeof v==='string')?v:JSON.stringify(v);\
|
||||
try{{callBackJsToCpp('{escaped_source_url}@_@'+window.location.href+'@_@{callback}@_@sgBrowserExcuteJsCodeByDomain@_@'+(t??''))}}catch(_){{}}\
|
||||
var j=JSON.stringify({{type:'callback',callback:'{callback}',request_url:'{escaped_source_url}',payload:{{value:(t??'')}}}});\
|
||||
try{{var r=new XMLHttpRequest();r.open('POST','{events_url}',true);r.setRequestHeader('Content-Type','application/json');r.send(j)}}catch(_){{}}\
|
||||
try{{navigator.sendBeacon('{events_url}',new Blob([j],{{type:'application/json'}}))}}catch(_){{}}\
|
||||
}}\
|
||||
if(v&&typeof v.then==='function'){{v.then(_s).catch(function(){{}});}}else{{_s(v);}}\
|
||||
}}catch(e){{}}}})()"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -234,11 +234,15 @@ fn execute_browser_script_impl(
|
||||
|
||||
let wrapped_script = wrap_browser_script(&script_body, &Value::Object(args.clone()));
|
||||
eprintln!("[execute_browser_script_impl] 包装后脚本长度: {} 字节", wrapped_script.len());
|
||||
eprintln!("[execute_browser_script_impl] 包装后脚本前500字符: {}",
|
||||
eprintln!("[execute_browser_script_impl] 包装后脚本前500字符: {}",
|
||||
if wrapped_script.len() > 500 { &wrapped_script[..500] } else { &wrapped_script });
|
||||
eprintln!("[execute_browser_script_impl] 调用 browser_tool.invoke(Action::Eval)...");
|
||||
|
||||
let target_url = format!("http://{}", expected_domain);
|
||||
|
||||
let target_url = args.get("target_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("http://{}", expected_domain));
|
||||
eprintln!("[execute_browser_script_impl] target_url: {}", target_url);
|
||||
let result = match browser_tool.invoke(
|
||||
Action::Eval,
|
||||
json!({
|
||||
|
||||
@@ -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('。') {
|
||||
|
||||
@@ -330,8 +330,7 @@ fn derive_expected_domain(task_context: &CompatTaskContext) -> Result<String, Pi
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
PipeError::Protocol(
|
||||
"direct submit skill requires page_url so expected_domain can be derived"
|
||||
.to_string(),
|
||||
"当前命令需要浏览器页面上下文才能执行。请在浏览器中打开目标页面后重试,或在指令末尾添加'。。。'使用确定性提交。".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
|
||||
223
src/compat/lineloss_xlsx_export.rs
Normal file
223
src/compat/lineloss_xlsx_export.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use zip::write::FileOptions;
|
||||
use zip::{CompressionMethod, ZipWriter};
|
||||
|
||||
pub struct LinelossExportRequest {
|
||||
pub sheet_name: String,
|
||||
pub column_defs: Vec<(String, String)>,
|
||||
pub rows: Vec<Map<String, Value>>,
|
||||
pub output_path: PathBuf,
|
||||
}
|
||||
|
||||
pub fn export_lineloss_xlsx(request: &LinelossExportRequest) -> anyhow::Result<PathBuf> {
|
||||
if request.rows.is_empty() {
|
||||
anyhow::bail!("rows must not be empty");
|
||||
}
|
||||
if request.column_defs.is_empty() {
|
||||
anyhow::bail!("column_defs must not be empty");
|
||||
}
|
||||
|
||||
let sheet_xml = build_worksheet_xml(&request.column_defs, &request.rows);
|
||||
|
||||
write_xlsx(
|
||||
&request.output_path,
|
||||
&request.sheet_name,
|
||||
&sheet_xml,
|
||||
)?;
|
||||
|
||||
Ok(request.output_path.clone())
|
||||
}
|
||||
|
||||
fn build_worksheet_xml(
|
||||
column_defs: &[(String, String)],
|
||||
rows: &[Map<String, Value>],
|
||||
) -> String {
|
||||
let mut xml_rows = Vec::with_capacity(rows.len() + 1);
|
||||
|
||||
// Header row (row 1)
|
||||
let header_cells: Vec<String> = column_defs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(col_idx, (_key, label))| {
|
||||
let col_letter = column_letter(col_idx);
|
||||
format!(
|
||||
"<c r=\"{col_letter}1\" t=\"inlineStr\"><is><t>{}</t></is></c>",
|
||||
xml_escape(label)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
xml_rows.push(format!("<row r=\"1\">{}</row>", header_cells.join("")));
|
||||
|
||||
// Data rows (row 2+)
|
||||
for (row_idx, row) in rows.iter().enumerate() {
|
||||
let excel_row = row_idx + 2;
|
||||
let cells: Vec<String> = column_defs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(col_idx, (key, _label))| {
|
||||
let col_letter = column_letter(col_idx);
|
||||
let value = row
|
||||
.get(key)
|
||||
.map(|v| value_to_string(v))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"<c r=\"{col_letter}{excel_row}\" t=\"inlineStr\"><is><t>{}</t></is></c>",
|
||||
xml_escape(&value)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
xml_rows.push(format!("<row r=\"{excel_row}\">{}</row>", cells.join("")));
|
||||
}
|
||||
|
||||
format!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\
|
||||
<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
|
||||
<sheetData>{}</sheetData>\
|
||||
</worksheet>",
|
||||
xml_rows.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn column_letter(index: usize) -> String {
|
||||
let mut result = String::new();
|
||||
let mut n = index;
|
||||
loop {
|
||||
result.insert(0, (b'A' + (n % 26) as u8) as char);
|
||||
if n < 26 {
|
||||
break;
|
||||
}
|
||||
n = n / 26 - 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn value_to_string(value: &Value) -> String {
|
||||
match value {
|
||||
Value::String(text) => text.clone(),
|
||||
Value::Number(number) => number.to_string(),
|
||||
Value::Bool(flag) => flag.to_string(),
|
||||
Value::Null => String::new(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn xml_escape(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn write_xlsx(output_path: &Path, sheet_name: &str, sheet_xml: &str) -> anyhow::Result<()> {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
if output_path.exists() {
|
||||
fs::remove_file(output_path)?;
|
||||
}
|
||||
|
||||
let file = fs::File::create(output_path)?;
|
||||
let mut zip = ZipWriter::new(file);
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
|
||||
zip.start_file("[Content_Types].xml", options)?;
|
||||
zip.write_all(content_types_xml().as_bytes())?;
|
||||
|
||||
zip.start_file("_rels/.rels", options)?;
|
||||
zip.write_all(root_rels_xml().as_bytes())?;
|
||||
|
||||
zip.start_file("docProps/app.xml", options)?;
|
||||
zip.write_all(app_xml().as_bytes())?;
|
||||
|
||||
zip.start_file("docProps/core.xml", options)?;
|
||||
zip.write_all(core_xml().as_bytes())?;
|
||||
|
||||
zip.start_file("xl/workbook.xml", options)?;
|
||||
zip.write_all(workbook_xml(&xml_escape(sheet_name)).as_bytes())?;
|
||||
|
||||
zip.start_file("xl/_rels/workbook.xml.rels", options)?;
|
||||
zip.write_all(workbook_rels_xml().as_bytes())?;
|
||||
|
||||
zip.start_file("xl/worksheets/sheet1.xml", options)?;
|
||||
zip.write_all(sheet_xml.as_bytes())?;
|
||||
|
||||
zip.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn content_types_xml() -> &'static str {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
|
||||
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
|
||||
</Types>"#
|
||||
}
|
||||
|
||||
fn root_rels_xml() -> &'static str {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
||||
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
|
||||
</Relationships>"#
|
||||
}
|
||||
|
||||
fn app_xml() -> &'static str {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
|
||||
<Application>sgClaw</Application>
|
||||
</Properties>"#
|
||||
}
|
||||
|
||||
fn core_xml() -> &'static str {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<dc:title>台区线损报表</dc:title>
|
||||
</cp:coreProperties>"#
|
||||
}
|
||||
|
||||
fn workbook_xml(sheet_name: &str) -> String {
|
||||
format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets>
|
||||
<sheet name="{sheet_name}" sheetId="1" r:id="rId1"/>
|
||||
</sheets>
|
||||
</workbook>"#
|
||||
)
|
||||
}
|
||||
|
||||
fn workbook_rels_xml() -> &'static str {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
||||
</Relationships>"#
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::column_letter;
|
||||
|
||||
#[test]
|
||||
fn column_letter_maps_indices_correctly() {
|
||||
assert_eq!(column_letter(0), "A");
|
||||
assert_eq!(column_letter(1), "B");
|
||||
assert_eq!(column_letter(6), "G");
|
||||
assert_eq!(column_letter(25), "Z");
|
||||
assert_eq!(column_letter(26), "AA");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod cron_adapter;
|
||||
pub mod deterministic_submit;
|
||||
pub mod direct_skill_runtime;
|
||||
pub mod event_bridge;
|
||||
pub mod lineloss_xlsx_export;
|
||||
pub mod memory_adapter;
|
||||
pub mod openxml_office_tool;
|
||||
pub mod orchestration;
|
||||
|
||||
@@ -5,29 +5,598 @@ pub(crate) struct OrgUnit {
|
||||
}
|
||||
|
||||
pub(crate) const ORG_UNITS: &[OrgUnit] = &[
|
||||
// ===== Province-level =====
|
||||
OrgUnit {
|
||||
label: "国网甘肃省电力公司",
|
||||
code: "62101",
|
||||
aliases: &["国网甘肃省电力公司", "甘肃省电力公司", "甘肃电力公司", "甘肃省公司"],
|
||||
},
|
||||
|
||||
// ===== City-level (lv=2) =====
|
||||
OrgUnit {
|
||||
label: "国网兰州供电公司",
|
||||
code: "62401",
|
||||
aliases: &["国网兰州供电公司", "兰州供电公司", "兰州公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网白银供电公司",
|
||||
code: "62402",
|
||||
aliases: &["国网白银供电公司", "白银供电公司", "白银公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网天水供电公司",
|
||||
code: "62403",
|
||||
aliases: &["国网天水供电公司", "天水供电公司", "天水公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网平凉供电公司",
|
||||
code: "62404",
|
||||
aliases: &["国网平凉供电公司", "平凉供电公司", "平凉公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网金昌供电公司",
|
||||
code: "62405",
|
||||
aliases: &["国网金昌供电公司", "金昌供电公司", "金昌公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网张掖供电公司",
|
||||
code: "62406",
|
||||
aliases: &["国网张掖供电公司", "张掖供电公司", "张掖公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网陇南供电公司",
|
||||
code: "62407",
|
||||
aliases: &["国网陇南供电公司", "陇南供电公司", "陇南公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网定西供电公司",
|
||||
code: "62408",
|
||||
aliases: &["国网定西供电公司", "定西供电公司", "定西公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网庆阳供电公司",
|
||||
code: "62409",
|
||||
aliases: &["国网庆阳供电公司", "庆阳供电公司", "庆阳公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网武威供电公司",
|
||||
code: "62410",
|
||||
aliases: &["国网武威供电公司", "武威供电公司", "武威公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网酒泉供电公司",
|
||||
code: "62411",
|
||||
aliases: &["国网酒泉供电公司", "酒泉供电公司", "酒泉公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网临夏供电公司",
|
||||
code: "62412",
|
||||
aliases: &["国网临夏供电公司", "临夏供电公司", "临夏公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网甘南供电公司",
|
||||
code: "62413",
|
||||
aliases: &["国网甘南供电公司", "甘南供电公司", "甘南公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网嘉峪关供电公司",
|
||||
code: "62414",
|
||||
aliases: &["国网嘉峪关供电公司", "嘉峪关供电公司", "嘉峪关公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网兰州新区供电公司",
|
||||
code: "62415",
|
||||
aliases: &["国网兰州新区供电公司", "兰州新区供电公司", "兰州新区公司"],
|
||||
},
|
||||
|
||||
// ===== 兰州供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "城关供电分公司",
|
||||
code: "6240108",
|
||||
aliases: &["城关供电分公司", "城关分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "七里河供电分公司",
|
||||
code: "6240109",
|
||||
aliases: &["七里河供电分公司", "七里河分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "西固供电分公司",
|
||||
code: "6240107",
|
||||
aliases: &["西固供电分公司", "西固分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "安宁供电分公司",
|
||||
code: "6240111",
|
||||
aliases: &["安宁供电分公司", "安宁分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "红古供电分公司",
|
||||
code: "6240102",
|
||||
aliases: &["红古供电分公司", "红古分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "东岗供电分公司",
|
||||
code: "6240110",
|
||||
aliases: &["东岗供电分公司", "东岗分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网永登县供电公司",
|
||||
code: "6240122",
|
||||
aliases: &["国网永登县供电公司", "永登县供电公司", "永登县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网榆中县供电公司",
|
||||
code: "6240121",
|
||||
aliases: &["国网榆中县供电公司", "榆中县供电公司", "榆中县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "榆中城关供电所",
|
||||
code: "624012108",
|
||||
aliases: &["榆中城关供电所"],
|
||||
label: "国网永靖县供电公司",
|
||||
code: "6240123",
|
||||
aliases: &["国网永靖县供电公司", "永靖县供电公司", "永靖县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "兰州客户服务中心",
|
||||
code: "6240101",
|
||||
aliases: &["兰州客户服务中心", "兰州客服中心"],
|
||||
},
|
||||
|
||||
// ===== 白银供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "城区供电分公司",
|
||||
code: "6240201",
|
||||
aliases: &["城区供电分公司", "城区分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网白银市城区供电分公司",
|
||||
code: "6240201",
|
||||
aliases: &["国网白银市城区供电分公司", "白银市城区供电分公司", "白银城区分公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网皋兰县供电公司",
|
||||
code: "6240223",
|
||||
aliases: &["国网皋兰县供电公司", "皋兰县供电公司", "皋兰县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网靖远县供电公司",
|
||||
code: "6240221",
|
||||
aliases: &["国网靖远县供电公司", "靖远县供电公司", "靖远县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网景泰县供电公司",
|
||||
code: "6240222",
|
||||
aliases: &["国网景泰县供电公司", "景泰县供电公司", "景泰县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网会宁县供电公司",
|
||||
code: "6240225",
|
||||
aliases: &["国网会宁县供电公司", "会宁县供电公司", "会宁县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网白银市平川区供电公司",
|
||||
code: "6240224",
|
||||
aliases: &["国网白银市平川区供电公司", "白银市平川区供电公司", "平川区供电公司", "平川区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "白银客户服务中心",
|
||||
code: "6240207",
|
||||
aliases: &["白银客户服务中心", "白银客服中心"],
|
||||
},
|
||||
|
||||
// ===== 天水供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网天水市秦州区供电公司",
|
||||
code: "6240323",
|
||||
aliases: &["国网天水市秦州区供电公司", "天水市秦州区供电公司", "秦州区供电公司", "秦州区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "秦州区供电公司",
|
||||
code: "6240323",
|
||||
aliases: &["秦州区供电公司", "秦州区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网天水市麦积区供电公司",
|
||||
code: "6240305",
|
||||
aliases: &["国网天水市麦积区供电公司", "天水市麦积区供电公司", "麦积区供电公司", "麦积区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网麦积区供电公司",
|
||||
code: "6240305",
|
||||
aliases: &["国网麦积区供电公司", "麦积区供电公司", "麦积区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网武山县供电公司",
|
||||
code: "6240321",
|
||||
aliases: &["国网武山县供电公司", "武山县供电公司", "武山县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "武山县供电公司",
|
||||
code: "6240321",
|
||||
aliases: &["武山县供电公司", "武山县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网甘谷县供电公司",
|
||||
code: "6240322",
|
||||
aliases: &["国网甘谷县供电公司", "甘谷县供电公司", "甘谷县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "甘谷县供电公司",
|
||||
code: "6240322",
|
||||
aliases: &["甘谷县供电公司", "甘谷县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网秦安县供电公司",
|
||||
code: "6240324",
|
||||
aliases: &["国网秦安县供电公司", "秦安县供电公司", "秦安县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "清水县供电公司",
|
||||
code: "6240325",
|
||||
aliases: &["清水县供电公司", "清水县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "张家川县供电公司",
|
||||
code: "6240326",
|
||||
aliases: &["张家川县供电公司", "张家川县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "天水客户服务中心",
|
||||
code: "6240306",
|
||||
aliases: &["天水客户服务中心", "天水客服中心"],
|
||||
},
|
||||
|
||||
// ===== 平凉供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网崇信县供电公司",
|
||||
code: "6240401",
|
||||
aliases: &["国网崇信县供电公司", "崇信县供电公司", "崇信县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网庄浪县供电公司",
|
||||
code: "6240402",
|
||||
aliases: &["国网庄浪县供电公司", "庄浪县供电公司", "庄浪县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网泾川县供电公司",
|
||||
code: "6240403",
|
||||
aliases: &["国网泾川县供电公司", "泾川县供电公司", "泾川县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网静宁县供电公司",
|
||||
code: "6240404",
|
||||
aliases: &["国网静宁县供电公司", "静宁县供电公司", "静宁县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网崆峒区供电公司",
|
||||
code: "6240405",
|
||||
aliases: &["国网崆峒区供电公司", "崆峒区供电公司", "崆峒区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网华亭市公司",
|
||||
code: "6240407",
|
||||
aliases: &["国网华亭市公司", "华亭市公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网灵台县供电公司",
|
||||
code: "6240408",
|
||||
aliases: &["国网灵台县供电公司", "灵台县供电公司", "灵台县公司"],
|
||||
},
|
||||
|
||||
// ===== 金昌供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "金川区供电公司",
|
||||
code: "6240522",
|
||||
aliases: &["金川区供电公司", "金川区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网永昌县供电公司",
|
||||
code: "6240523",
|
||||
aliases: &["国网永昌县供电公司", "永昌县供电公司", "永昌县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "城区供电服务中心",
|
||||
code: "6240505",
|
||||
aliases: &["城区供电服务中心"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "金昌客户服务中心",
|
||||
code: "6240507",
|
||||
aliases: &["金昌客户服务中心", "金昌客服中心"],
|
||||
},
|
||||
|
||||
// ===== 张掖供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网甘州区供电公司",
|
||||
code: "6240621",
|
||||
aliases: &["国网甘州区供电公司", "甘州区供电公司", "甘州区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "肃南县供电公司",
|
||||
code: "6240622",
|
||||
aliases: &["肃南县供电公司", "肃南县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网高台县供电公司",
|
||||
code: "6240623",
|
||||
aliases: &["国网高台县供电公司", "高台县供电公司", "高台县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网山丹县供电公司",
|
||||
code: "6240624",
|
||||
aliases: &["国网山丹县供电公司", "山丹县供电公司", "山丹县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网民乐县供电公司",
|
||||
code: "6240625",
|
||||
aliases: &["国网民乐县供电公司", "民乐县供电公司", "民乐县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网临泽县供电公司",
|
||||
code: "6240626",
|
||||
aliases: &["国网临泽县供电公司", "临泽县供电公司", "临泽县公司"],
|
||||
},
|
||||
|
||||
// ===== 陇南供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网武都区供电公司",
|
||||
code: "6240701",
|
||||
aliases: &["国网武都区供电公司", "武都区供电公司", "武都区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网宕昌县供电公司",
|
||||
code: "6240702",
|
||||
aliases: &["国网宕昌县供电公司", "宕昌县供电公司", "宕昌县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网文县供电公司",
|
||||
code: "6240703",
|
||||
aliases: &["国网文县供电公司", "文县供电公司", "文县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网康县供电公司",
|
||||
code: "6240704",
|
||||
aliases: &["国网康县供电公司", "康县供电公司", "康县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网西和县供电公司",
|
||||
code: "6240705",
|
||||
aliases: &["国网西和县供电公司", "西和县供电公司", "西和县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网礼县供电公司",
|
||||
code: "6240706",
|
||||
aliases: &["国网礼县供电公司", "礼县供电公司", "礼县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网成县供电公司",
|
||||
code: "6240707",
|
||||
aliases: &["国网成县供电公司", "成县供电公司", "成县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网徽县供电公司",
|
||||
code: "6240708",
|
||||
aliases: &["国网徽县供电公司", "徽县供电公司", "徽县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网两当县供电公司",
|
||||
code: "6240709",
|
||||
aliases: &["国网两当县供电公司", "两当县供电公司", "两当县公司"],
|
||||
},
|
||||
|
||||
// ===== 定西供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网定西市安定区供电公司",
|
||||
code: "6240801",
|
||||
aliases: &["国网定西市安定区供电公司", "定西市安定区供电公司", "安定区供电公司", "安定区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网通渭县供电公司",
|
||||
code: "6240802",
|
||||
aliases: &["国网通渭县供电公司", "通渭县供电公司", "通渭县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网陇西县供电公司",
|
||||
code: "6240803",
|
||||
aliases: &["国网陇西县供电公司", "陇西县供电公司", "陇西县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网渭源县供电公司",
|
||||
code: "6240804",
|
||||
aliases: &["国网渭源县供电公司", "渭源县供电公司", "渭源县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网临洮县供电公司",
|
||||
code: "6240805",
|
||||
aliases: &["国网临洮县供电公司", "临洮县供电公司", "临洮县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网漳县供电公司",
|
||||
code: "6240806",
|
||||
aliases: &["国网漳县供电公司", "漳县供电公司", "漳县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网岷县供电公司",
|
||||
code: "6240807",
|
||||
aliases: &["国网岷县供电公司", "岷县供电公司", "岷县公司"],
|
||||
},
|
||||
|
||||
// ===== 庆阳供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "西峰区供电公司",
|
||||
code: "6240901",
|
||||
aliases: &["西峰区供电公司", "西峰区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网庆城县供电公司",
|
||||
code: "6240902",
|
||||
aliases: &["国网庆城县供电公司", "庆城县供电公司", "庆城县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网正宁县供电公司",
|
||||
code: "6240903",
|
||||
aliases: &["国网正宁县供电公司", "正宁县供电公司", "正宁县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网镇原县供电公司",
|
||||
code: "6240904",
|
||||
aliases: &["国网镇原县供电公司", "镇原县供电公司", "镇原县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网环县供电公司",
|
||||
code: "6240905",
|
||||
aliases: &["国网环县供电公司", "环县供电公司", "环县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网华池县供电公司",
|
||||
code: "6240906",
|
||||
aliases: &["国网华池县供电公司", "华池县供电公司", "华池县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网合水县供电公司",
|
||||
code: "6240907",
|
||||
aliases: &["国网合水县供电公司", "合水县供电公司", "合水县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网宁县供电公司",
|
||||
code: "6240908",
|
||||
aliases: &["国网宁县供电公司", "宁县供电公司", "宁县公司"],
|
||||
},
|
||||
|
||||
// ===== 武威供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网古浪县供电公司",
|
||||
code: "6241001",
|
||||
aliases: &["国网古浪县供电公司", "古浪县供电公司", "古浪县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网凉州区供电公司",
|
||||
code: "6241002",
|
||||
aliases: &["国网凉州区供电公司", "凉州区供电公司", "凉州区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网民勤县供电公司",
|
||||
code: "6241003",
|
||||
aliases: &["国网民勤县供电公司", "民勤县供电公司", "民勤县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网天祝县供电公司",
|
||||
code: "6241004",
|
||||
aliases: &["国网天祝县供电公司", "天祝县供电公司", "天祝县公司"],
|
||||
},
|
||||
|
||||
// ===== 酒泉供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网酒泉市肃州区供电公司",
|
||||
code: "6241101",
|
||||
aliases: &["国网酒泉市肃州区供电公司", "酒泉市肃州区供电公司", "肃州区供电公司", "肃州区公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网金塔县供电公司",
|
||||
code: "6241102",
|
||||
aliases: &["国网金塔县供电公司", "金塔县供电公司", "金塔县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网玉门市供电公司",
|
||||
code: "6241103",
|
||||
aliases: &["国网玉门市供电公司", "玉门市供电公司", "玉门市公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网瓜州县供电公司",
|
||||
code: "6241104",
|
||||
aliases: &["国网瓜州县供电公司", "瓜州县供电公司", "瓜州县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网敦煌市供电公司",
|
||||
code: "6241105",
|
||||
aliases: &["国网敦煌市供电公司", "敦煌市供电公司", "敦煌市公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网肃北县供电公司",
|
||||
code: "6241106",
|
||||
aliases: &["国网肃北县供电公司", "肃北县供电公司", "肃北县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网阿克塞县供电公司",
|
||||
code: "6241107",
|
||||
aliases: &["国网阿克塞县供电公司", "阿克塞县供电公司", "阿克塞县公司"],
|
||||
},
|
||||
|
||||
// ===== 临夏供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "临夏市城关营业班",
|
||||
code: "6241201",
|
||||
aliases: &["临夏市城关营业班"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网临夏县供电公司",
|
||||
code: "6241202",
|
||||
aliases: &["国网临夏县供电公司", "临夏县供电公司", "临夏县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网东乡县供电公司",
|
||||
code: "6241203",
|
||||
aliases: &["国网东乡县供电公司", "东乡县供电公司", "东乡县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网和政县供电公司",
|
||||
code: "6241204",
|
||||
aliases: &["国网和政县供电公司", "和政县供电公司", "和政县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网广河县供电公司",
|
||||
code: "6241205",
|
||||
aliases: &["国网广河县供电公司", "广河县供电公司", "广河县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网积石山县供电公司",
|
||||
code: "6241206",
|
||||
aliases: &["国网积石山县供电公司", "积石山县供电公司", "积石山县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网康乐县供电公司",
|
||||
code: "6241207",
|
||||
aliases: &["国网康乐县供电公司", "康乐县供电公司", "康乐县公司"],
|
||||
},
|
||||
|
||||
// ===== 甘南供电公司 children (lv=3) =====
|
||||
OrgUnit {
|
||||
label: "国网合作市供电公司",
|
||||
code: "6241301",
|
||||
aliases: &["国网合作市供电公司", "合作市供电公司", "合作市公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网夏河县供电公司",
|
||||
code: "6241302",
|
||||
aliases: &["国网夏河县供电公司", "夏河县供电公司", "夏河县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网卓尼县供电公司",
|
||||
code: "6241303",
|
||||
aliases: &["国网卓尼县供电公司", "卓尼县供电公司", "卓尼县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网临潭县供电公司",
|
||||
code: "6241304",
|
||||
aliases: &["国网临潭县供电公司", "临潭县供电公司", "临潭县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网碌曲县供电公司",
|
||||
code: "6241305",
|
||||
aliases: &["国网碌曲县供电公司", "碌曲县供电公司", "碌曲县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网玛曲县供电公司",
|
||||
code: "6241306",
|
||||
aliases: &["国网玛曲县供电公司", "玛曲县供电公司", "玛曲县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网迭部县供电公司",
|
||||
code: "6241307",
|
||||
aliases: &["国网迭部县供电公司", "迭部县供电公司", "迭部县公司"],
|
||||
},
|
||||
OrgUnit {
|
||||
label: "国网舟曲县供电公司",
|
||||
code: "6241308",
|
||||
aliases: &["国网舟曲县供电公司", "舟曲县供电公司", "舟曲县公司"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod settings;
|
||||
|
||||
pub use crate::runtime::RuntimeProfile;
|
||||
|
||||
pub use settings::{
|
||||
BrowserBackend, ConfigError, DeepSeekSettings, OfficeBackend, PlannerMode, ProviderSettings,
|
||||
SgClawSettings, SkillsPromptMode,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::runtime::RuntimeProfile;
|
||||
@@ -200,6 +200,65 @@ impl SgClawSettings {
|
||||
.expect("active_provider should always resolve to a configured provider")
|
||||
}
|
||||
|
||||
fn to_serializable(&self) -> SerializableRawSgClawSettings {
|
||||
SerializableRawSgClawSettings {
|
||||
api_key: self.provider_api_key.clone(),
|
||||
base_url: self.provider_base_url.clone(),
|
||||
model: self.provider_model.clone(),
|
||||
skills_dir: self.skills_dir.as_ref().map(|p| p.to_string_lossy().into_owned()),
|
||||
direct_submit_skill: self.direct_submit_skill.clone(),
|
||||
skills_prompt_mode: Some(match self.skills_prompt_mode {
|
||||
SkillsPromptMode::Full => "full".to_string(),
|
||||
SkillsPromptMode::Compact => "compact".to_string(),
|
||||
}),
|
||||
runtime_profile: Some(match self.runtime_profile {
|
||||
RuntimeProfile::BrowserAttached => "browser-attached".to_string(),
|
||||
RuntimeProfile::BrowserHeavy => "browser-heavy".to_string(),
|
||||
RuntimeProfile::GeneralAssistant => "general-assistant".to_string(),
|
||||
}),
|
||||
planner_mode: Some(match self.planner_mode {
|
||||
PlannerMode::ZeroclawPlanFirst => "zeroclaw-plan-first".to_string(),
|
||||
PlannerMode::LegacyDeterministic => "legacy-deterministic".to_string(),
|
||||
}),
|
||||
active_provider: Some(self.active_provider.clone()),
|
||||
browser_backend: Some(match self.browser_backend {
|
||||
BrowserBackend::SuperRpa => "super-rpa".to_string(),
|
||||
BrowserBackend::AgentBrowser => "agent-browser".to_string(),
|
||||
BrowserBackend::RustNative => "rust-native".to_string(),
|
||||
BrowserBackend::ComputerUse => "computer-use".to_string(),
|
||||
BrowserBackend::Auto => "auto".to_string(),
|
||||
}),
|
||||
office_backend: Some(match self.office_backend {
|
||||
OfficeBackend::OpenXml => "openxml".to_string(),
|
||||
OfficeBackend::Disabled => "disabled".to_string(),
|
||||
}),
|
||||
browser_ws_url: self.browser_ws_url.clone(),
|
||||
service_ws_listen_addr: self.service_ws_listen_addr.clone(),
|
||||
providers: self
|
||||
.providers
|
||||
.iter()
|
||||
.map(|p| SerializableProviderSettings {
|
||||
id: p.id.clone(),
|
||||
provider: Some(p.provider.clone()),
|
||||
api_key: p.api_key.clone(),
|
||||
base_url: p.base_url.clone(),
|
||||
model: p.model.clone(),
|
||||
api_path: p.api_path.clone(),
|
||||
wire_api: p.wire_api.clone(),
|
||||
requires_openai_auth: p.requires_openai_auth,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_to_path(&self, path: &Path) -> Result<(), ConfigError> {
|
||||
let serializable = self.to_serializable();
|
||||
let json = serde_json::to_string_pretty(&serializable)
|
||||
.map_err(|err| ConfigError::ConfigParse(path.to_path_buf(), err.to_string()))?;
|
||||
std::fs::write(path, json)
|
||||
.map_err(|err| ConfigError::ConfigRead(path.to_path_buf(), err.to_string()))
|
||||
}
|
||||
|
||||
fn maybe_from_env() -> Result<Option<Self>, ConfigError> {
|
||||
let api_key = match std::env::var("DEEPSEEK_API_KEY") {
|
||||
Ok(value) => value,
|
||||
@@ -529,6 +588,54 @@ fn normalize_enum_token(raw: &str) -> String {
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializableRawSgClawSettings {
|
||||
#[serde(rename = "apiKey")]
|
||||
api_key: String,
|
||||
#[serde(rename = "baseUrl")]
|
||||
base_url: String,
|
||||
model: String,
|
||||
#[serde(rename = "skillsDir", skip_serializing_if = "Option::is_none")]
|
||||
skills_dir: Option<String>,
|
||||
#[serde(rename = "directSubmitSkill", skip_serializing_if = "Option::is_none")]
|
||||
direct_submit_skill: Option<String>,
|
||||
#[serde(rename = "skillsPromptMode", skip_serializing_if = "Option::is_none")]
|
||||
skills_prompt_mode: Option<String>,
|
||||
#[serde(rename = "runtimeProfile", skip_serializing_if = "Option::is_none")]
|
||||
runtime_profile: Option<String>,
|
||||
#[serde(rename = "plannerMode", skip_serializing_if = "Option::is_none")]
|
||||
planner_mode: Option<String>,
|
||||
#[serde(rename = "activeProvider", skip_serializing_if = "Option::is_none")]
|
||||
active_provider: Option<String>,
|
||||
#[serde(rename = "browserBackend", skip_serializing_if = "Option::is_none")]
|
||||
browser_backend: Option<String>,
|
||||
#[serde(rename = "officeBackend", skip_serializing_if = "Option::is_none")]
|
||||
office_backend: Option<String>,
|
||||
#[serde(rename = "browserWsUrl", skip_serializing_if = "Option::is_none")]
|
||||
browser_ws_url: Option<String>,
|
||||
#[serde(rename = "serviceWsListenAddr", skip_serializing_if = "Option::is_none")]
|
||||
service_ws_listen_addr: Option<String>,
|
||||
#[serde(default)]
|
||||
providers: Vec<SerializableProviderSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SerializableProviderSettings {
|
||||
id: String,
|
||||
provider: Option<String>,
|
||||
#[serde(rename = "apiKey")]
|
||||
api_key: String,
|
||||
#[serde(rename = "baseUrl", skip_serializing_if = "Option::is_none")]
|
||||
base_url: Option<String>,
|
||||
model: String,
|
||||
#[serde(rename = "apiPath", skip_serializing_if = "Option::is_none")]
|
||||
api_path: Option<String>,
|
||||
#[serde(rename = "wireApi", skip_serializing_if = "Option::is_none")]
|
||||
wire_api: Option<String>,
|
||||
#[serde(rename = "requiresOpenaiAuth")]
|
||||
requires_openai_auth: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawSgClawSettings {
|
||||
#[serde(rename = "apiKey", default)]
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::security::MacPolicy;
|
||||
const DEFAULT_BROWSER_WS_URL: &str = "ws://127.0.0.1:12345";
|
||||
const DEFAULT_SERVICE_WS_LISTEN_ADDR: &str = "127.0.0.1:42321";
|
||||
|
||||
pub use protocol::{ClientMessage, ServiceMessage};
|
||||
pub use protocol::{ClientMessage, ConfigUpdatePayload, ServiceMessage};
|
||||
pub use server::{ServiceEventSink, ServiceSession};
|
||||
|
||||
pub(crate) mod browser_ws_client {
|
||||
|
||||
@@ -3,6 +3,24 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::agent::SubmitTaskRequest;
|
||||
use crate::pipe::ConversationMessage;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConfigUpdatePayload {
|
||||
#[serde(rename = "apiKey", default)]
|
||||
pub api_key: Option<String>,
|
||||
#[serde(rename = "baseUrl", default)]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model: Option<String>,
|
||||
#[serde(rename = "skillsDir", default)]
|
||||
pub skills_dir: Option<String>,
|
||||
#[serde(rename = "directSubmitSkill", default)]
|
||||
pub direct_submit_skill: Option<String>,
|
||||
#[serde(rename = "runtimeProfile", default)]
|
||||
pub runtime_profile: Option<String>,
|
||||
#[serde(rename = "browserBackend", default)]
|
||||
pub browser_backend: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientMessage {
|
||||
@@ -21,6 +39,9 @@ pub enum ClientMessage {
|
||||
page_title: String,
|
||||
},
|
||||
Ping,
|
||||
UpdateConfig {
|
||||
config: ConfigUpdatePayload,
|
||||
},
|
||||
}
|
||||
|
||||
impl ClientMessage {
|
||||
@@ -39,7 +60,11 @@ impl ClientMessage {
|
||||
page_url: normalize_optional_field(page_url),
|
||||
page_title: normalize_optional_field(page_title),
|
||||
}),
|
||||
ClientMessage::Connect | ClientMessage::Start | ClientMessage::Stop | ClientMessage::Ping => None,
|
||||
ClientMessage::Connect
|
||||
| ClientMessage::Start
|
||||
| ClientMessage::Stop
|
||||
| ClientMessage::Ping
|
||||
| ClientMessage::UpdateConfig { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +77,10 @@ pub enum ServiceMessage {
|
||||
TaskComplete { success: bool, summary: String },
|
||||
Busy { message: String },
|
||||
Pong,
|
||||
ConfigUpdated {
|
||||
success: bool,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn normalize_optional_field(value: String) -> Option<String> {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -229,6 +231,52 @@ fn send_status_changed(sink: &ServiceEventSink, state: &str) -> Result<(), PipeE
|
||||
})
|
||||
}
|
||||
|
||||
fn update_config_file(config_path: &Path, config: crate::service::protocol::ConfigUpdatePayload) -> Result<(), String> {
|
||||
use crate::config::{BrowserBackend, RuntimeProfile, SgClawSettings};
|
||||
|
||||
let mut settings = SgClawSettings::load(Some(config_path))
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "无法读取现有配置".to_string())?;
|
||||
|
||||
if let Some(v) = config.api_key {
|
||||
settings.provider_api_key = v;
|
||||
}
|
||||
if let Some(v) = config.base_url {
|
||||
settings.provider_base_url = v;
|
||||
}
|
||||
if let Some(v) = config.model {
|
||||
settings.provider_model = v;
|
||||
}
|
||||
if let Some(v) = config.skills_dir {
|
||||
settings.skills_dir = Some(PathBuf::from(&v));
|
||||
}
|
||||
if let Some(v) = config.direct_submit_skill {
|
||||
settings.direct_submit_skill = Some(v);
|
||||
}
|
||||
if let Some(v) = config.runtime_profile {
|
||||
settings.runtime_profile = match v.as_str() {
|
||||
"browser-attached" => RuntimeProfile::BrowserAttached,
|
||||
"browser-heavy" => RuntimeProfile::BrowserHeavy,
|
||||
"general-assistant" => RuntimeProfile::GeneralAssistant,
|
||||
_ => return Err(format!("无效的 runtimeProfile: {}", v)),
|
||||
};
|
||||
}
|
||||
if let Some(v) = config.browser_backend {
|
||||
settings.browser_backend = match v.as_str() {
|
||||
"super-rpa" => BrowserBackend::SuperRpa,
|
||||
"agent-browser" => BrowserBackend::AgentBrowser,
|
||||
"rust-native" => BrowserBackend::RustNative,
|
||||
"computer-use" => BrowserBackend::ComputerUse,
|
||||
"auto" => BrowserBackend::Auto,
|
||||
_ => return Err(format!("无效的 browserBackend: {}", v)),
|
||||
};
|
||||
}
|
||||
|
||||
settings
|
||||
.save_to_path(config_path)
|
||||
.map_err(|e| format!("写入配置文件失败: {}", e))
|
||||
}
|
||||
|
||||
pub(crate) fn serve_client(
|
||||
context: &AgentRuntimeContext,
|
||||
session: &ServiceSession,
|
||||
@@ -334,6 +382,39 @@ pub(crate) fn serve_client(
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientMessage::UpdateConfig { config } => {
|
||||
let Some(config_path) = context.config_path() else {
|
||||
sink.send_service_message(ServiceMessage::ConfigUpdated {
|
||||
success: false,
|
||||
message: "未找到配置文件路径。请通过 --config-path 参数启动 sg_claw 后再使用此功能。".to_string(),
|
||||
})?;
|
||||
continue;
|
||||
};
|
||||
|
||||
if !config_path.exists() {
|
||||
sink.send_service_message(ServiceMessage::ConfigUpdated {
|
||||
success: false,
|
||||
message: format!("配置文件不存在: {}", config_path.display()),
|
||||
})?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = update_config_file(config_path, config);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
sink.send_service_message(ServiceMessage::ConfigUpdated {
|
||||
success: true,
|
||||
message: "配置已保存。重启 sg_claw 以应用新配置。".to_string(),
|
||||
})?;
|
||||
}
|
||||
Err(err) => {
|
||||
sink.send_service_message(ServiceMessage::ConfigUpdated {
|
||||
success: false,
|
||||
message: format!("保存配置失败: {}", err),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user