use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Parser)]
#[command(name = "qtcloud-execute", version, about = "量潮执行云 CLI:云端任务管理(AI 辅助入口)")]
struct Cli {
#[arg(long, global = true)]
server: Option<String>,
#[arg(long, global = true)]
json: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Lists,
Tasks {
list_id: String,
#[arg(long)]
status: Option<String>,
},
Add {
list_id: String,
title: String,
#[arg(long)]
description: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
status: Option<String>,
},
Update {
list_id: String,
task_id: String,
#[arg(long)]
status: Option<String>,
#[arg(long)]
priority: Option<String>,
#[arg(long)]
description: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Task {
id: String,
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
status: String,
priority: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct List {
id: String,
name: String,
tasks: Vec<Task>,
}
#[derive(Debug, Clone, Deserialize)]
struct ListsResp {
lists: Vec<List>,
}
#[derive(Debug, Clone, Deserialize)]
struct TasksResp {
tasks: Vec<Task>,
}
const STATUSES: [&str; 4] = ["notStarted", "inProgress", "reviewing", "done"];
const PRIORITIES: [&str; 4] = ["urgent", "high", "medium", "low"];
fn exit_err(e: &str) -> ! {
eprintln!("错误: {e}");
std::process::exit(1);
}
const DEFAULT_API_BASE: &str = "https://api.quanttide.com/qtcloud-execute";
fn resolve_base(cli_server: &Option<String>) -> String {
if let Some(s) = cli_server {
if !s.trim().is_empty() {
return s.trim_end_matches('/').to_string();
}
}
match std::env::var("QTCLOUD_EXECUTE_API_BASE_URL") {
Ok(s) if !s.trim().is_empty() => s.trim_end_matches('/').to_string(),
_ => DEFAULT_API_BASE.to_string(),
}
}
fn http_json(method: &str, url: &str, body: Option<&str>) -> Result<serde_json::Value, String> {
let resp = match method {
"GET" => ureq::get(url).call(),
"PUT" => ureq::put(url)
.set("Content-Type", "application/json")
.send_string(body.unwrap_or("")),
_ => return Err(format!("不支持的 HTTP 方法: {method}")),
};
let resp = resp.map_err(|e| match e {
ureq::Error::Status(code, r) => format!("HTTP {code}: {}", r.into_string().unwrap_or_default()),
other => format!("请求 {url} 失败: {other}"),
})?;
let code = resp.status();
let text = resp
.into_string()
.map_err(|e| format!("读取响应失败: {e}"))?;
if (200..300).contains(&code) {
serde_json::from_str(&text).map_err(|e| format!("JSON 解析失败: {e}"))
} else {
Err(format!("HTTP {code}: {text}"))
}
}
fn http_tasks(base: &str, list_id: &str) -> Result<Vec<Task>, String> {
let v = http_json("GET", &format!("{base}/api/lists/{list_id}/tasks"), None)?;
let r: TasksResp = serde_json::from_value(v).map_err(|e| format!("解析任务响应失败: {e}"))?;
Ok(r.tasks)
}
fn check_status(status: &str) -> Result<(), String> {
if STATUSES.contains(&status) {
Ok(())
} else {
Err(format!("非法状态 `{status}`,可选:{}", STATUSES.join("/")))
}
}
fn check_priority(priority: &str) -> Result<(), String> {
if PRIORITIES.contains(&priority) {
Ok(())
} else {
Err(format!("非法优先级 `{priority}`,可选:{}", PRIORITIES.join("/")))
}
}
fn ensure_update_fields(status: Option<&str>, priority: Option<&str>, description: Option<&str>) {
if status.is_none() && priority.is_none() && description.is_none() {
exit_err("至少提供一项要更新的字段(--status/--priority/--description)");
}
if let Some(s) = status {
if let Err(e) = check_status(s) {
exit_err(&e);
}
}
if let Some(p) = priority {
if let Err(e) = check_priority(p) {
exit_err(&e);
}
}
}
fn lists(base: &str, json: bool) {
let v = http_json("GET", &format!("{base}/api/lists"), None).unwrap_or_else(|e| exit_err(&e));
if json {
println!("{}", serde_json::to_string(&v).unwrap());
return;
}
let r: ListsResp = serde_json::from_value(v).unwrap_or_else(|e| exit_err(&format!("解析清单失败: {e}")));
if r.lists.is_empty() {
println!("(暂无清单)服务器: {base}");
return;
}
println!("── 任务清单 ── {base}");
for l in &r.lists {
println!(" {:<12} {} ({} 个任务)", l.id, l.name, l.tasks.len());
}
}
fn tasks(base: &str, list_id: &str, status_filter: Option<&str>, json: bool) {
if let Some(s) = status_filter {
if let Err(e) = check_status(s) {
exit_err(&e);
}
}
let tasks = http_tasks(base, list_id).unwrap_or_else(|e| exit_err(&e));
if json {
let payload = if let Some(s) = status_filter {
let filtered: Vec<&Task> = tasks.iter().filter(|t| t.status == s).collect();
serde_json::json!({ "tasks": filtered })
} else {
serde_json::json!({ "tasks": tasks })
};
println!("{}", serde_json::to_string(&payload).unwrap());
return;
}
println!("── {list_id} ── {base}/api/lists/{list_id}/tasks");
let filtered: Vec<&Task> = tasks
.iter()
.filter(|t| status_filter.map(|s| t.status == s).unwrap_or(true))
.collect();
if filtered.is_empty() {
println!(" (暂无任务)");
return;
}
for t in &filtered {
println!(" {:<38} {:<11} {:<7}", t.title, t.status, t.priority);
if let Some(d) = &t.description {
println!(" {} {d}", t.id);
} else {
println!(" {}", t.id);
}
}
}
fn gen_task_id(list_id: &str, existing: &[String]) -> String {
let base = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
for i in 0..10000u64 {
let id = format!("{list_id}-{}", base + i);
if !existing.contains(&id) {
return id;
}
}
format!("{list_id}-{}", base)
}
fn add(
base: &str,
list_id: &str,
title: &str,
description: Option<&str>,
priority: Option<&str>,
status: Option<&str>,
json: bool,
) {
let priority = priority.unwrap_or("medium");
if let Err(e) = check_priority(priority) {
exit_err(&e);
}
let status = status.unwrap_or("notStarted");
if let Err(e) = check_status(status) {
exit_err(&e);
}
let existing = http_tasks(base, list_id).unwrap_or_else(|e| exit_err(&e));
let existing_ids: Vec<String> = existing.iter().map(|t| t.id.clone()).collect();
let task_id = gen_task_id(list_id, &existing_ids);
let task = Task {
id: task_id.clone(),
title: title.to_string(),
description: description.map(String::from),
status: status.to_string(),
priority: priority.to_string(),
};
let body = serde_json::to_string(&task).unwrap_or_else(|e| exit_err(&format!("序列化失败: {e}")));
let v = http_json("PUT", &format!("{base}/api/lists/{list_id}/tasks/{task_id}"), Some(&body))
.unwrap_or_else(|e| exit_err(&e));
if json {
println!("{}", serde_json::to_string(&v).unwrap());
} else {
println!("✓ 已新增任务 {task_id} → {title} ({status}/{priority}) @ {base}");
}
}
fn update(
base: &str,
list_id: &str,
task_id: &str,
status: Option<&str>,
priority: Option<&str>,
description: Option<&str>,
json: bool,
) {
ensure_update_fields(status, priority, description);
let tasks = http_tasks(base, list_id).unwrap_or_else(|e| exit_err(&e));
let mut task = tasks
.into_iter()
.find(|t| t.id == task_id)
.unwrap_or_else(|| exit_err(&format!("任务不存在: {task_id}(清单 {list_id})")));
let title = task.title.clone();
if let Some(s) = status {
task.status = s.to_string();
}
if let Some(p) = priority {
task.priority = p.to_string();
}
if let Some(d) = description {
task.description = Some(d.to_string());
}
let body = serde_json::to_string(&task).unwrap_or_else(|e| exit_err(&format!("序列化失败: {e}")));
let v = http_json("PUT", &format!("{base}/api/lists/{list_id}/tasks/{task_id}"), Some(&body))
.unwrap_or_else(|e| exit_err(&e));
if json {
println!("{}", serde_json::to_string(&v).unwrap());
} else {
println!("✓ 已更新任务 {task_id}:{title} @ {base}");
}
}
fn main() {
let cli = Cli::parse();
let base = resolve_base(&cli.server);
match &cli.command {
Command::Lists => lists(&base, cli.json),
Command::Tasks { list_id, status } => tasks(&base, list_id, status.as_deref(), cli.json),
Command::Add { list_id, title, description, priority, status } => add(
&base, list_id, title, description.as_deref(), priority.as_deref(), status.as_deref(), cli.json,
),
Command::Update { list_id, task_id, status, priority, description } => update(
&base, list_id, task_id, status.as_deref(), priority.as_deref(), description.as_deref(), cli.json,
),
}
}