|
| 1 | +//! 工作日志域命令:项目演化的第一手记录(人类手写 + agent 草稿生成)。 |
| 2 | +//! 开工包只带最近 3 条摘录——完整历史走本命令族消费。 |
| 3 | +//! 写入需平台 worklog 能力位;draft 从已完成任务/发布生成草稿不落库。 |
| 4 | +
|
| 5 | +use anyhow::{Context, Result, anyhow}; |
| 6 | +use serde_json::json; |
| 7 | + |
| 8 | +use super::project_ctx; |
| 9 | +use crate::cli::WorklogArgs; |
| 10 | +use crate::client::encode_query; |
| 11 | +use crate::config::Config; |
| 12 | +use crate::model::worklog::{WorklogDraft, WorklogItem, WorklogList}; |
| 13 | +use crate::output::{Out, pad_display}; |
| 14 | + |
| 15 | +/// `bcode worklog`:缺省列表(倒序,id/日期/作者/来源/首行)。 |
| 16 | +/// `--show <id>` 单条完整内容(直连详情端点); |
| 17 | +/// `--write [--file <path>|--content <s>]` 写入(source 缺省 manual); |
| 18 | +/// `--draft --from --to` 生成草稿到 stdout(不落库;润色后 --write --source tasks 发布)。 |
| 19 | +pub async fn worklog(cfg: &Config, profile: &str, a: &WorklogArgs, out: &Out) -> Result<()> { |
| 20 | + let ctx = project_ctx(cfg, profile).await?; |
| 21 | + let pid = ctx.project.project_id; |
| 22 | + |
| 23 | + if let Some(id) = a.show { |
| 24 | + let item: WorklogItem = ctx.client.get_as(&format!("/v1/worklogs/{id}")).await?; |
| 25 | + render_item(&item, out, true); |
| 26 | + out.emit_value(&serde_json::to_value(&item)?); |
| 27 | + return Ok(()); |
| 28 | + } |
| 29 | + |
| 30 | + if a.write { |
| 31 | + let content = match (a.file.as_deref(), a.content.as_deref()) { |
| 32 | + (Some(f), _) => std::fs::read_to_string(f).with_context(|| format!("读取 {f} 失败"))?, |
| 33 | + (None, Some(c)) => c.to_string(), |
| 34 | + (None, None) => return Err(bail_anyhow()), |
| 35 | + }; |
| 36 | + let source = a.source.clone().unwrap_or_else(|| "manual".into()); |
| 37 | + if !matches!(source.as_str(), "manual" | "tasks") { |
| 38 | + return Err(anyhow!( |
| 39 | + "--source 仅支持 manual / tasks(tasks=草稿润色后发布)" |
| 40 | + )); |
| 41 | + } |
| 42 | + let created: crate::model::task::CreatedId = ctx |
| 43 | + .client |
| 44 | + .post_as( |
| 45 | + &format!("/v1/projects/{pid}/worklogs"), |
| 46 | + json!({ "content": content, "source": source }), |
| 47 | + ) |
| 48 | + .await?; |
| 49 | + out.kv("已记录", &format!("工作日志 #{}", created.id)); |
| 50 | + out.emit_value(&json!({ "written": true, "worklog_id": created.id })); |
| 51 | + return Ok(()); |
| 52 | + } |
| 53 | + |
| 54 | + if a.draft { |
| 55 | + let from = a |
| 56 | + .from |
| 57 | + .as_deref() |
| 58 | + .ok_or_else(|| anyhow!("--draft 需要 --from YYYY-MM-DD(配 --to,范围上限 92 天)"))?; |
| 59 | + let to = |
| 60 | + a.to.as_deref() |
| 61 | + .ok_or_else(|| anyhow!("--draft 需要 --to YYYY-MM-DD"))?; |
| 62 | + let d: WorklogDraft = ctx |
| 63 | + .client |
| 64 | + .get_as(&format!( |
| 65 | + "/v1/projects/{pid}/worklogs/draft?from={}&to={}", |
| 66 | + encode_query(from), |
| 67 | + encode_query(to) |
| 68 | + )) |
| 69 | + .await?; |
| 70 | + if !out.json { |
| 71 | + // 草稿直出 stdout:便于重定向润色(> draft.md 后编辑再 --write) |
| 72 | + println!("{}", d.content); |
| 73 | + eprintln!( |
| 74 | + "(草稿:{} 个任务 / {} 个发布;润色后 bcode worklog --write --file x.md --source tasks 发布)", |
| 75 | + d.task_count, d.release_count |
| 76 | + ); |
| 77 | + } |
| 78 | + out.emit_value(&serde_json::to_value(&d)?); |
| 79 | + return Ok(()); |
| 80 | + } |
| 81 | + |
| 82 | + // 缺省:列表(倒序分页) |
| 83 | + let page = a.page.unwrap_or(1); |
| 84 | + let size = a.size.unwrap_or(20); |
| 85 | + let list: WorklogList = ctx |
| 86 | + .sessioned_get(&format!( |
| 87 | + "/v1/projects/{pid}/worklogs?pageNum={page}&pageSize={size}" |
| 88 | + )) |
| 89 | + .await?; |
| 90 | + if list.list.is_empty() { |
| 91 | + out.line("(无工作日志)"); |
| 92 | + } |
| 93 | + for item in &list.list { |
| 94 | + let author = if item.author_type == "ai" { |
| 95 | + "[AI] " |
| 96 | + } else { |
| 97 | + "" |
| 98 | + }; |
| 99 | + let src = if item.source == "tasks" { |
| 100 | + "草稿" |
| 101 | + } else { |
| 102 | + "手写" |
| 103 | + }; |
| 104 | + out.line(&format!( |
| 105 | + " {} {author}{} [{}] {}({})", |
| 106 | + pad_display(&format!("#{}", item.id), 6), |
| 107 | + item.author_name, |
| 108 | + src, |
| 109 | + item.created_at, |
| 110 | + first_line(&item.content) |
| 111 | + )); |
| 112 | + } |
| 113 | + out.line(&format!("(共 {} 条)", list.total)); |
| 114 | + out.emit_value(&serde_json::to_value(&list)?); |
| 115 | + Ok(()) |
| 116 | +} |
| 117 | + |
| 118 | +fn render_item(item: &WorklogItem, out: &Out, full: bool) { |
| 119 | + let author = if item.author_type == "ai" { |
| 120 | + "[AI] " |
| 121 | + } else { |
| 122 | + "" |
| 123 | + }; |
| 124 | + let src = if item.source == "tasks" { |
| 125 | + "草稿" |
| 126 | + } else { |
| 127 | + "手写" |
| 128 | + }; |
| 129 | + out.line(&format!( |
| 130 | + "#{} [{}] {author}{}({})", |
| 131 | + item.id, src, item.author_name, item.created_at |
| 132 | + )); |
| 133 | + if full { |
| 134 | + out.line(""); |
| 135 | + out.line(&item.content); |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +fn first_line(s: &str) -> String { |
| 140 | + let l = s.lines().next().unwrap_or(""); |
| 141 | + let mut cut: String = l.chars().take(60).collect(); |
| 142 | + if l.chars().count() > 60 { |
| 143 | + cut.push('…'); |
| 144 | + } |
| 145 | + cut |
| 146 | +} |
| 147 | + |
| 148 | +fn bail_anyhow() -> anyhow::Error { |
| 149 | + anyhow!("--write 需要 --file <路径> 或 --content <文本>") |
| 150 | +} |
0 commit comments