ai-worklog 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/collect_work_log.py +116 -50
package/package.json
CHANGED
|
@@ -19,6 +19,80 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Optional
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
# ─── TUI 输出 ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
_COLOR = sys.stdout.isatty()
|
|
25
|
+
|
|
26
|
+
def _c(text: str, code: str) -> str:
|
|
27
|
+
return f"\033[{code}m{text}\033[0m" if _COLOR else text
|
|
28
|
+
|
|
29
|
+
def bold(t): return _c(t, "1")
|
|
30
|
+
def dim(t): return _c(t, "2")
|
|
31
|
+
def green(t): return _c(t, "32")
|
|
32
|
+
def cyan(t): return _c(t, "36")
|
|
33
|
+
def yellow(t): return _c(t, "33")
|
|
34
|
+
def gray(t): return _c(t, "90")
|
|
35
|
+
def red(t): return _c(t, "31")
|
|
36
|
+
def blue(t): return _c(t, "34")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_header(date_str: str, incremental: bool = False, prev_time: str = "") -> None:
|
|
40
|
+
line = f" ai-worklog · {date_str} "
|
|
41
|
+
w = max(len(line), 44)
|
|
42
|
+
print(f"\n╭{'─' * w}╮")
|
|
43
|
+
print(f"│{bold(line):<{w + (10 if _COLOR else 0)}}│")
|
|
44
|
+
print(f"╰{'─' * w}╯")
|
|
45
|
+
if incremental:
|
|
46
|
+
print(f"\n {yellow('↻')} 增量更新模式 {dim(f'上次生成于 {prev_time}')}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_step(msg: str) -> None:
|
|
50
|
+
print(f"\n{bold(cyan('●'))} {bold(msg)}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def print_ok(msg: str) -> None:
|
|
54
|
+
print(f" {green('✓')} {msg}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_info(msg: str) -> None:
|
|
58
|
+
print(f" {dim('│')} {dim(msg)}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_warn(msg: str) -> None:
|
|
62
|
+
print(f" {yellow('⚠')} {msg}", file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_fail(msg: str) -> None:
|
|
66
|
+
print(f" {red('✗')} {msg}", file=sys.stderr)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_project_tree(data: dict, is_sessions: bool = True) -> None:
|
|
70
|
+
"""打印项目列表(树形)"""
|
|
71
|
+
if not data:
|
|
72
|
+
print(f" {gray('└─')} {gray('(无记录)')}")
|
|
73
|
+
return
|
|
74
|
+
items = list(data.items())
|
|
75
|
+
max_len = max(len(p) for p in data)
|
|
76
|
+
for i, (proj, val) in enumerate(items):
|
|
77
|
+
connector = gray("└─") if i == len(items) - 1 else gray("├─")
|
|
78
|
+
if is_sessions:
|
|
79
|
+
n_sessions = len(val)
|
|
80
|
+
n_turns = sum(len(t) for t in val)
|
|
81
|
+
stats = gray(f"{n_sessions} 对话 {n_turns} 轮")
|
|
82
|
+
else:
|
|
83
|
+
stats = gray(f"{len(val)} 条")
|
|
84
|
+
print(f" {connector} {bold(proj.ljust(max_len))} {stats}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def print_footer(log_file: Path, total_projects: int, total_turns: int) -> None:
|
|
88
|
+
w = 46
|
|
89
|
+
print(f"\n{'─' * w}")
|
|
90
|
+
rel = str(log_file).replace(str(Path.home()), "~")
|
|
91
|
+
print(f" {green('✓')} {bold('完成')} {total_projects} 个项目 · {total_turns} 轮对话")
|
|
92
|
+
print(f" {dim(rel)}")
|
|
93
|
+
print(f"{'─' * w}\n")
|
|
94
|
+
|
|
95
|
+
|
|
22
96
|
# ─── 配置 ───────────────────────────────────────────────────────────────────
|
|
23
97
|
|
|
24
98
|
WORKLOG_CONFIG_FILE = Path.home() / ".config" / "worklog.json"
|
|
@@ -467,7 +541,6 @@ def save_log(target_date: str, content: str) -> Path:
|
|
|
467
541
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
468
542
|
log_file = log_dir / f"{target_date}.md"
|
|
469
543
|
log_file.write_text(content, encoding="utf-8")
|
|
470
|
-
print(f"日志已保存: {log_file}")
|
|
471
544
|
return log_file
|
|
472
545
|
|
|
473
546
|
|
|
@@ -636,67 +709,67 @@ def git_commit_and_push(log_file: Path, target_date: str, push: bool = True) ->
|
|
|
636
709
|
env["LC_ALL"] = "C" # 强制英文输出,保证字符串匹配不受本地化影响
|
|
637
710
|
r = subprocess.run(cmd, cwd=REPO_DIR, capture_output=True, text=True, env=env)
|
|
638
711
|
if check_err and r.returncode != 0:
|
|
639
|
-
|
|
712
|
+
print_fail(f"{' '.join(cmd[:2])}: {r.stderr.strip()}")
|
|
640
713
|
return r
|
|
641
714
|
|
|
715
|
+
print_step("同步 Git")
|
|
716
|
+
|
|
642
717
|
# ── 1. 初始化 git 仓库 ──────────────────────────────────────────────────
|
|
643
718
|
is_git = run(["git", "rev-parse", "--git-dir"], check_err=False).returncode == 0
|
|
644
719
|
if not is_git:
|
|
645
|
-
|
|
720
|
+
print_info("初始化仓库...")
|
|
646
721
|
if run(["git", "init"]).returncode != 0:
|
|
647
722
|
return
|
|
648
723
|
run(["git", "checkout", "-b", "main"], check_err=False)
|
|
724
|
+
print_ok("git init")
|
|
649
725
|
|
|
650
726
|
# ── 2. 检查并创建 GitLab 远程(无需 glab)────────────────────────────────
|
|
651
727
|
has_remote = run(["git", "remote", "get-url", "origin"], check_err=False).returncode == 0
|
|
652
728
|
if not has_remote:
|
|
653
729
|
repo_name = REPO_DIR.name
|
|
654
|
-
|
|
730
|
+
print_info(f"在 GitLab 创建公开项目 {repo_name} ...")
|
|
655
731
|
try:
|
|
656
732
|
cfg = load_gitlab_config()
|
|
657
733
|
ssh_url = gitlab_create_project(cfg, repo_name)
|
|
658
734
|
run(["git", "remote", "add", "origin", ssh_url])
|
|
659
|
-
|
|
735
|
+
print_ok(f"远程仓库 → {ssh_url}")
|
|
660
736
|
except Exception as e:
|
|
661
|
-
|
|
662
|
-
|
|
737
|
+
print_fail(f"GitLab 创建失败: {e}")
|
|
738
|
+
print_info("请手动: git remote add origin <url>")
|
|
663
739
|
return
|
|
664
740
|
|
|
665
741
|
# ── 3. 确保有初始提交 ─────────────────────────────────────────────────────
|
|
666
742
|
if run(["git", "rev-parse", "HEAD"], check_err=False).returncode != 0:
|
|
667
|
-
print("Git: 创建初始提交...")
|
|
668
743
|
run(["git", "add", "-A"])
|
|
669
744
|
run(["git", "commit", "-m", "init: 初始化工作日志仓库"])
|
|
670
745
|
|
|
671
746
|
# ── 4. 提交日志 ───────────────────────────────────────────────────────────
|
|
672
747
|
rel_path = log_file.relative_to(REPO_DIR)
|
|
673
|
-
print(f"Git: 添加 {rel_path}")
|
|
674
748
|
if run(["git", "add", str(rel_path)]).returncode != 0:
|
|
675
749
|
return
|
|
676
750
|
|
|
677
751
|
commit_msg = f"docs: 工作日志 {target_date}"
|
|
678
|
-
print(f"Git: 提交 '{commit_msg}'")
|
|
679
752
|
r = run(["git", "commit", "-m", commit_msg], check_err=False)
|
|
680
753
|
if r.returncode != 0:
|
|
681
754
|
if "nothing to commit" in r.stdout + r.stderr:
|
|
682
|
-
|
|
755
|
+
print_info("无新变更,跳过提交")
|
|
683
756
|
else:
|
|
684
|
-
|
|
757
|
+
print_fail(f"commit 失败: {r.stderr.strip()}")
|
|
685
758
|
return
|
|
686
|
-
|
|
759
|
+
else:
|
|
760
|
+
print_ok(f"commit: {dim(commit_msg)}")
|
|
687
761
|
|
|
688
762
|
# ── 5. 推送 ────────────────────────────────────────────────────────────────
|
|
689
763
|
if push:
|
|
690
|
-
print("Git: 推送到远程...")
|
|
691
764
|
r = run(["git", "push", "--set-upstream", "origin", "main"], check_err=False)
|
|
692
765
|
if r.returncode != 0:
|
|
693
766
|
r = run(["git", "push"])
|
|
694
767
|
if r.returncode == 0:
|
|
695
|
-
|
|
768
|
+
print_ok("push → origin/main")
|
|
696
769
|
else:
|
|
697
|
-
|
|
770
|
+
print_fail(f"push 失败: {r.stderr.strip()}")
|
|
698
771
|
else:
|
|
699
|
-
|
|
772
|
+
print_info("已跳过 push(--no-push)")
|
|
700
773
|
|
|
701
774
|
|
|
702
775
|
# ─── 主入口 ───────────────────────────────────────────────────────────────────
|
|
@@ -741,68 +814,61 @@ def main():
|
|
|
741
814
|
args = parser.parse_args()
|
|
742
815
|
|
|
743
816
|
target_date = parse_date_arg(args.date)
|
|
744
|
-
print(f"=== 收集 {target_date} 的工作日志 ===")
|
|
745
817
|
|
|
746
|
-
# 1.
|
|
818
|
+
# 1. 检查 checkpoint
|
|
747
819
|
checkpoint = load_checkpoint(target_date)
|
|
748
820
|
is_incremental = checkpoint is not None
|
|
749
821
|
prev_file_counts = checkpoint["file_counts"] if is_incremental else {}
|
|
822
|
+
prev_time = checkpoint["generated_at"].split("T")[-1] if is_incremental else ""
|
|
750
823
|
|
|
751
|
-
|
|
752
|
-
print(f"检测到已有日志(生成于 {checkpoint['generated_at']}),进入增量更新模式")
|
|
824
|
+
print_header(target_date, is_incremental, prev_time)
|
|
753
825
|
|
|
754
|
-
# 2.
|
|
755
|
-
|
|
756
|
-
claude_data, file_counts = collect_claude_sessions(
|
|
826
|
+
# 2. 收集 Claude Code 对话
|
|
827
|
+
print_step("收集 Claude Code 对话记录")
|
|
828
|
+
claude_data, file_counts = collect_claude_sessions(
|
|
829
|
+
target_date, prev_file_counts if is_incremental else None
|
|
830
|
+
)
|
|
831
|
+
print_project_tree(claude_data, is_sessions=True)
|
|
757
832
|
total_claude_turns = sum(sum(len(t) for t in s) for s in claude_data.values())
|
|
758
|
-
total_claude_sessions = sum(len(s) for s in claude_data.values())
|
|
759
|
-
label = "新增" if is_incremental else "找到"
|
|
760
|
-
print(f" {label} {len(claude_data)} 个项目,{total_claude_sessions} 个对话,{total_claude_turns} 轮交互")
|
|
761
833
|
|
|
762
|
-
|
|
834
|
+
# 3. 收集 Codex 对话
|
|
835
|
+
print_step("收集 Codex 对话记录")
|
|
763
836
|
codex_data = collect_codex_sessions(target_date)
|
|
837
|
+
print_project_tree(codex_data, is_sessions=False)
|
|
764
838
|
total_codex = sum(len(v) for v in codex_data.values())
|
|
765
|
-
|
|
839
|
+
|
|
840
|
+
total_projects = len(set(list(claude_data.keys()) + list(codex_data.keys())))
|
|
841
|
+
total_turns = total_claude_turns + total_codex
|
|
766
842
|
|
|
767
843
|
if args.dry_run:
|
|
768
|
-
print(f"\n[dry-run]
|
|
769
|
-
for proj, sessions in claude_data.items():
|
|
770
|
-
total_turns = sum(len(t) for t in sessions)
|
|
771
|
-
print(f" [Claude/{proj}] {len(sessions)} 个对话,{total_turns} 轮")
|
|
772
|
-
for si, turns in enumerate(sessions[:2], 1):
|
|
773
|
-
print(f" 对话{si}: {len(turns)} 轮")
|
|
774
|
-
for user_msg, asst_msg in turns[:2]:
|
|
775
|
-
print(f" [用户] {user_msg[:60]}")
|
|
776
|
-
if asst_msg:
|
|
777
|
-
print(f" [Claude] {asst_msg[:60]}")
|
|
778
|
-
for proj, msgs in codex_data.items():
|
|
779
|
-
print(f" [Codex/{proj}] {len(msgs)} 条")
|
|
780
|
-
for m in msgs[:2]:
|
|
781
|
-
print(f" - {m[:60]}")
|
|
844
|
+
print(f"\n{dim('[dry-run] 预览完毕,未调用 API')}\n")
|
|
782
845
|
return
|
|
783
846
|
|
|
784
|
-
#
|
|
847
|
+
# 4. 生成日志
|
|
785
848
|
if is_incremental:
|
|
786
849
|
if total_claude_turns == 0 and total_codex == 0:
|
|
787
|
-
print("
|
|
850
|
+
print(f"\n {yellow('─')} 没有新增内容,无需更新\n")
|
|
788
851
|
return
|
|
789
|
-
|
|
852
|
+
print_step(f"调用 {bold(MODEL)} 更新日志(增量)")
|
|
790
853
|
log_file_path = LOGS_DIR / target_date[:4] / f"{target_date}.md"
|
|
791
854
|
existing_log = log_file_path.read_text(encoding="utf-8") if log_file_path.exists() else ""
|
|
792
855
|
content = generate_incremental_update(target_date, existing_log, claude_data, codex_data)
|
|
793
856
|
else:
|
|
794
|
-
|
|
857
|
+
print_step(f"调用 {bold(MODEL)} 生成摘要")
|
|
795
858
|
content = generate_summary(target_date, claude_data, codex_data)
|
|
796
859
|
|
|
797
|
-
#
|
|
860
|
+
# 5. 保存文件 + checkpoint
|
|
861
|
+
print_step("保存日志")
|
|
798
862
|
log_file = save_log(target_date, content)
|
|
799
863
|
save_checkpoint(target_date, file_counts)
|
|
864
|
+
rel = str(log_file).replace(str(Path.home()), "~")
|
|
865
|
+
print_ok(rel)
|
|
800
866
|
|
|
801
|
-
#
|
|
867
|
+
# 6. Git 操作
|
|
802
868
|
if not args.no_git:
|
|
803
869
|
git_commit_and_push(log_file, target_date, push=not args.no_push)
|
|
804
870
|
|
|
805
|
-
|
|
871
|
+
print_footer(log_file, total_projects, total_turns)
|
|
806
872
|
|
|
807
873
|
|
|
808
874
|
if __name__ == "__main__":
|