Skip to content

Commit c38fd9f

Browse files
committed
feat(cli): 添加工作日志功能和文档移动命令
- 新增 worklog 命令支持倒序列表、详情查看、内容记录和草稿生成 - 添加 docs 命令的 --move-from 和 --move-to 参数用于文件移动重命名 - 在项目启动信息中显示最近 3 条工作日志摘录 - 创建 worklog 模块包含相关数据模型和 API 接口定义 - 更新 CLI 参数解析器以支持新的命令选项 - 实现工作日志的增删改查和草稿生成功能
1 parent df07533 commit c38fd9f

10 files changed

Lines changed: 304 additions & 1 deletion

File tree

‎skills/bcode/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ feedback 的设计意图:**目标配了关联即可投递,不需要目标准
5151

5252
**讨论区**(想法 → 议题线程 → 转任务):`bcode discuss`(列表/详情含回复)、`--new --title`(发起,需 discuss 能力位)、`--reply/--reply-file`(回复)、`--convert`(转任务血缘互链)、`--archive`。
5353

54+
**工作日志**(项目演化记录):`bcode worklog`(倒序列表)、`--show <id>`(单条全量)、`--write --file`(记录,需 worklog 能力位)、`--draft --from --to`(从已完成任务/发布生成草稿,stdout 不落库,润色后 `--write --source tasks` 发布)。开工包只带 3 条摘录,完整历史走此命令。
55+
5456
**项目发布**(团队内分发):`bcode release`(列表 --channel)、`--create <v> --file f...`(建+传,maintainer)、`--download <v> [-o dir]`(下载;成员受限走 maintainer)、`--delete`。
5557

5658
**专题**(长期任务阶段化,全生命周期):`topic --create --title <t> [--goal/--acceptance/--doc-path]`(创建,默认自任执行)、`topic --phases <tid> --file plan.json`(PRD 拆解批量导入阶段,整体替换)、`topic --detail <id>`(阶段清单+最近交接)、`topic --work <tid> --phase <pid> --next`(推进)、`topic --log --action handoff`(交接摘要)、`topic --convert <tid> --phase <pid>`(阶段转任务)。

‎src/cli.rs‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const GROUPED_CATALOG: &str = "\
4242
notify 通知(--unread / --read <id> / --read-all / --watch SSE 实时流)
4343
4444
上下文消费
45-
docs [path] 文档中枢(读正文 / --list / --search;--write-file 写入)
45+
docs [path] 文档中枢(读正文 / --list / --search;--write-file 写入;--move-from/--move-to 移动)
4646
memory <key> 项目记忆(--set/--file 写;--global --propose 全局提案)
4747
memories 记忆列表(--prefix 前缀过滤)
4848
search <kw> 全局搜索(已获读权的项目范围)
@@ -62,6 +62,11 @@ QA 库与反馈
6262
release --download <v> [-o dir] 下载(--file 按名过滤)
6363
release --delete <v> 删除(级联清文件)
6464
65+
工作日志(项目演化记录)
66+
worklog 倒序列表(id/日期/作者/来源/首行)
67+
worklog --show <id> 单条完整内容;--write --file 记录(需能力位)
68+
worklog --draft --from --to 从已完成任务/发布生成草稿(stdout,不落库)
69+
6570
专题(长期任务阶段化)
6671
topic --create 创建专题(--title/--goal/--acceptance;默认自任执行)
6772
topic --phases <tid> --file f PRD 拆解批量导入阶段(整体替换)
@@ -201,6 +206,8 @@ pub enum Command {
201206
Discuss(DiscussArgs),
202207
/// 项目发布:create/upload/list/download/delete(本地打包推平台分发)
203208
Release(ReleaseArgs),
209+
/// 工作日志:list/show/write/draft(项目演化记录;写入需 worklog 能力位)
210+
Worklog(WorklogArgs),
204211
/// 测试执行:--run 包裹执行并上报 / --upload junit·dump 补传 / --cases pull·push 用例同步
205212
Test(TestArgs),
206213
}
@@ -397,6 +404,12 @@ pub struct DocsArgs {
397404
/// 写入:本地文件 → vault 目标路径(整文件覆盖,平台自动 .history 快照)
398405
#[arg(long)]
399406
pub write_file: Option<String>,
407+
/// 移动/重命名:源路径(与 --move-to 配对;目标目录自动创建,同名拒)
408+
#[arg(long, requires = "move_to")]
409+
pub move_from: Option<String>,
410+
/// 移动/重命名:目标路径
411+
#[arg(long)]
412+
pub move_to: Option<String>,
400413
}
401414

402415
#[derive(clap::Args)]
@@ -546,6 +559,40 @@ pub struct ReleaseArgs {
546559
pub delete: Option<String>,
547560
}
548561

562+
#[derive(clap::Args)]
563+
pub struct WorklogArgs {
564+
/// 单条完整内容(直连详情端点)
565+
#[arg(long)]
566+
pub show: Option<i64>,
567+
/// 写入一条(--file 或 --content;agent 需 worklog 能力位)
568+
#[arg(long)]
569+
pub write: bool,
570+
/// 写入内容(与 --file 互斥使用)
571+
#[arg(long)]
572+
pub content: Option<String>,
573+
/// 写入内容从本地 UTF-8 文件读取
574+
#[arg(long)]
575+
pub file: Option<String>,
576+
/// 来源:manual 手写(缺省)/ tasks 草稿润色后发布
577+
#[arg(long)]
578+
pub source: Option<String>,
579+
/// 生成草稿到 stdout(不落库;配 --from/--to,上限 92 天)
580+
#[arg(long)]
581+
pub draft: bool,
582+
/// 起始日期 YYYY-MM-DD(含)
583+
#[arg(long)]
584+
pub from: Option<String>,
585+
/// 结束日期 YYYY-MM-DD(含)
586+
#[arg(long)]
587+
pub to: Option<String>,
588+
/// 列表页码
589+
#[arg(long)]
590+
pub page: Option<i64>,
591+
/// 列表每页条数
592+
#[arg(long)]
593+
pub size: Option<i64>,
594+
}
595+
549596
#[derive(clap::Args)]
550597
pub struct QaArgs {
551598
/// 搜索关键词(缺省列出全部,按命中数降序)

‎src/cmd/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod tasks;
1515
pub mod test;
1616
pub mod topic;
1717
pub mod vault;
18+
pub mod worklog;
1819

1920
use anyhow::{Result, anyhow};
2021
use serde::de::DeserializeOwned;

‎src/cmd/project.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,26 @@ fn display_kickoff(boot: &SessionCreated, out: &Out) {
304304
));
305305
}
306306
out.line("");
307+
out.line(&format!(
308+
"── 最近工作日志({} 条摘录)── 完整历史 bcode worklog",
309+
boot.worklogs.len()
310+
));
311+
if boot.worklogs.is_empty() {
312+
out.line(" (无)");
313+
}
314+
for w in &boot.worklogs {
315+
let author = if w.author_type == "ai" { "[AI] " } else { "" };
316+
let src = if w.source == "tasks" {
317+
"草稿"
318+
} else {
319+
"手写"
320+
};
321+
out.line(&format!(
322+
" #{} [{}] {author}{}:{}",
323+
w.id, src, w.author, w.excerpt
324+
));
325+
}
326+
out.line("");
307327
out.line(&format!(
308328
"── 进行中专题({})── bcode topic work 推进",
309329
boot.active_topics.len()

‎src/cmd/vault.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::output::Out;
2020
/// 会话推导项目,X-Session 即上下文)。
2121
pub async fn docs(cfg: &Config, profile: &str, a: &crate::cli::DocsArgs, out: &Out) -> Result<()> {
2222
let ctx = project_ctx(cfg, profile).await?;
23+
let pid = ctx.project.project_id;
2324

2425
// vault 内搜索(免参别名)
2526
if let Some(kw) = &a.search {
@@ -44,6 +45,20 @@ pub async fn docs(cfg: &Config, profile: &str, a: &crate::cli::DocsArgs, out: &O
4445
return Ok(());
4546
}
4647

48+
// 移动/重命名(知识库整理:归类、归档——agent 交付后的整理入口)。
49+
// 平台暂无免参 move 别名,走项目端点(.bc/project 指向)
50+
if let (Some(from), Some(to)) = (&a.move_from, &a.move_to) {
51+
ctx.client
52+
.post(
53+
&format!("/v1/projects/{pid}/docs/file/move"),
54+
json!({ "from": from, "to": to }),
55+
)
56+
.await?;
57+
out.kv("已移动", &format!("{from} → {to}"));
58+
out.emit_value(&json!({ "moved": true, "from": from, "to": to }));
59+
return Ok(());
60+
}
61+
4762
// 写通道:本地文件 → vault 路径(免参别名)
4863
if let Some(local) = &a.write_file {
4964
let target = a.path.as_deref().ok_or_else(|| {

‎src/cmd/worklog.rs‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
}

‎src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ async fn main() {
9696
Command::Topic(a) => cmd::topic::topic(&cfg, &profile, &a, &out).await,
9797
Command::Discuss(a) => cmd::discuss::discuss(&cfg, &profile, &a, &out).await,
9898
Command::Release(a) => cmd::release::release(&cfg, &profile, &a, &out).await,
99+
Command::Worklog(a) => cmd::worklog::worklog(&cfg, &profile, &a, &out).await,
99100
Command::Test(a) => cmd::test::test(&cfg, &profile, &a, &out).await,
100101
};
101102

‎src/model/agent.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ pub struct SessionCreated {
5353
/// 高频 QA(按命中数前 5——遇到问题先查 QA 库再问人)
5454
#[serde(default, deserialize_with = "null_to_default")]
5555
pub top_qas: Vec<QaBrief>,
56+
/// 最近 3 条工作日志摘录(项目演化背景);完整历史走 worklog 命令族
57+
#[serde(default, deserialize_with = "null_to_default")]
58+
pub worklogs: Vec<WorklogBrief>,
5659
}
5760

5861
#[derive(Debug, Serialize, Deserialize)]
@@ -103,6 +106,24 @@ pub struct TopicBrief {
103106
pub last_handoff: String,
104107
}
105108

109+
/// 工作日志摘录(开工包,仅 3 条 + 首行截断)
110+
#[derive(Debug, Serialize, Deserialize)]
111+
#[serde(rename_all = "camelCase")]
112+
pub struct WorklogBrief {
113+
#[serde(default)]
114+
pub id: i64,
115+
#[serde(default)]
116+
pub author: String,
117+
#[serde(default)]
118+
pub author_type: String,
119+
#[serde(default)]
120+
pub source: String,
121+
#[serde(default)]
122+
pub excerpt: String,
123+
#[serde(default)]
124+
pub created_at: String,
125+
}
126+
106127
/// QA 摘要(开工包 TopQas)
107128
#[derive(Debug, Serialize, Deserialize)]
108129
#[serde(rename_all = "camelCase")]

‎src/model/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod qa;
1111
pub mod task;
1212
pub mod test;
1313
pub mod topic;
14+
pub mod worklog;
1415

1516
use serde::Deserialize;
1617

0 commit comments

Comments
 (0)