ai-worklog 1.0.4 → 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 +248 -69
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"
|
|
@@ -102,28 +176,42 @@ def utc_to_local_date(utc_ts: str) -> Optional[str]:
|
|
|
102
176
|
|
|
103
177
|
# ─── Claude Code 数据收集 ─────────────────────────────────────────────────────
|
|
104
178
|
|
|
105
|
-
def collect_claude_sessions(
|
|
179
|
+
def collect_claude_sessions(
|
|
180
|
+
target_date: str,
|
|
181
|
+
prev_file_counts: Optional[dict[str, int]] = None,
|
|
182
|
+
) -> tuple[dict[str, list[list[tuple[str, str]]]], dict[str, int]]:
|
|
106
183
|
"""
|
|
107
|
-
遍历 ~/.claude/projects/ 下所有 jsonl
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
184
|
+
遍历 ~/.claude/projects/ 下所有 jsonl,提取当天的对话轮次对。
|
|
185
|
+
|
|
186
|
+
prev_file_counts: 上次生成时各文件已处理的轮次数。
|
|
187
|
+
传入时只返回新增轮次(增量模式)。
|
|
111
188
|
|
|
112
|
-
返回:
|
|
113
|
-
|
|
189
|
+
返回:
|
|
190
|
+
sessions: {project_name: [session, ...]},session = [(user, assistant), ...]
|
|
191
|
+
file_counts: {jsonl_file_path: 本次处理的总轮次数}(用于保存 checkpoint)
|
|
114
192
|
"""
|
|
115
|
-
|
|
193
|
+
sessions: dict[str, list[list[tuple[str, str]]]] = {}
|
|
194
|
+
file_counts: dict[str, int] = {}
|
|
116
195
|
|
|
117
196
|
if not CLAUDE_PROJECTS_DIR.exists():
|
|
118
|
-
return
|
|
197
|
+
return sessions, file_counts
|
|
119
198
|
|
|
120
199
|
for project_dir in CLAUDE_PROJECTS_DIR.iterdir():
|
|
121
200
|
if not project_dir.is_dir():
|
|
122
201
|
continue
|
|
123
202
|
for jsonl_file in project_dir.glob("*.jsonl"):
|
|
124
|
-
_parse_claude_jsonl(jsonl_file, target_date
|
|
203
|
+
turns, cwd = _parse_claude_jsonl(jsonl_file, target_date)
|
|
204
|
+
file_key = str(jsonl_file)
|
|
205
|
+
file_counts[file_key] = len(turns)
|
|
125
206
|
|
|
126
|
-
|
|
207
|
+
prev = (prev_file_counts or {}).get(file_key, 0)
|
|
208
|
+
new_turns = turns[prev:] # 增量模式下只取新增部分;首次 prev=0 取全部
|
|
209
|
+
|
|
210
|
+
if new_turns:
|
|
211
|
+
project_name = os.path.basename(cwd) if cwd else "unknown"
|
|
212
|
+
sessions.setdefault(project_name, []).append(new_turns)
|
|
213
|
+
|
|
214
|
+
return sessions, file_counts
|
|
127
215
|
|
|
128
216
|
|
|
129
217
|
def _extract_user_text(content) -> str:
|
|
@@ -153,14 +241,14 @@ def _extract_assistant_text(content) -> str:
|
|
|
153
241
|
|
|
154
242
|
|
|
155
243
|
def _parse_claude_jsonl(
|
|
156
|
-
jsonl_file: Path, target_date: str
|
|
157
|
-
) ->
|
|
244
|
+
jsonl_file: Path, target_date: str
|
|
245
|
+
) -> tuple[list[tuple[str, str]], str]:
|
|
158
246
|
"""
|
|
159
247
|
解析单个 Claude Code jsonl 文件。
|
|
160
|
-
|
|
248
|
+
返回 (当天所有轮次对, cwd)。
|
|
161
249
|
Claude 回复只取 text 块,跳过 thinking 和 tool_use。
|
|
162
250
|
"""
|
|
163
|
-
|
|
251
|
+
turns: list[tuple[str, str]] = []
|
|
164
252
|
pending_user: str = ""
|
|
165
253
|
pending_user_date: str = ""
|
|
166
254
|
pending_assistant: str = ""
|
|
@@ -190,7 +278,7 @@ def _parse_claude_jsonl(
|
|
|
190
278
|
|
|
191
279
|
# 保存上一个 turn(日期匹配才入列)
|
|
192
280
|
if pending_user and pending_user_date == target_date:
|
|
193
|
-
|
|
281
|
+
turns.append((pending_user, pending_assistant))
|
|
194
282
|
|
|
195
283
|
# 开始新 turn
|
|
196
284
|
ts = entry.get("timestamp", "")
|
|
@@ -205,19 +293,17 @@ def _parse_claude_jsonl(
|
|
|
205
293
|
entry.get("message", {}).get("content", [])
|
|
206
294
|
)
|
|
207
295
|
if assistant_text:
|
|
208
|
-
pending_assistant = assistant_text #
|
|
296
|
+
pending_assistant = assistant_text # 多步工具调用后取最终回复
|
|
209
297
|
|
|
210
298
|
# 最后一个 pending turn
|
|
211
299
|
if pending_user and pending_user_date == target_date:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if session_turns:
|
|
215
|
-
project_name = os.path.basename(cwd) if cwd else "unknown"
|
|
216
|
-
results.setdefault(project_name, []).append(session_turns)
|
|
300
|
+
turns.append((pending_user, pending_assistant))
|
|
217
301
|
|
|
218
302
|
except Exception as e:
|
|
219
303
|
print(f"警告: 解析 {jsonl_file} 失败: {e}", file=sys.stderr)
|
|
220
304
|
|
|
305
|
+
return turns, cwd
|
|
306
|
+
|
|
221
307
|
|
|
222
308
|
# ─── Codex 数据收集 ───────────────────────────────────────────────────────────
|
|
223
309
|
|
|
@@ -455,10 +541,93 @@ def save_log(target_date: str, content: str) -> Path:
|
|
|
455
541
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
456
542
|
log_file = log_dir / f"{target_date}.md"
|
|
457
543
|
log_file.write_text(content, encoding="utf-8")
|
|
458
|
-
print(f"日志已保存: {log_file}")
|
|
459
544
|
return log_file
|
|
460
545
|
|
|
461
546
|
|
|
547
|
+
# ─── Checkpoint(增量更新支持)────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
def _checkpoint_path(target_date: str) -> Path:
|
|
550
|
+
year = target_date[:4]
|
|
551
|
+
return LOGS_DIR / year / f"{target_date}.meta.json"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def load_checkpoint(target_date: str) -> Optional[dict]:
|
|
555
|
+
"""读取上次生成的 checkpoint,不存在或解析失败返回 None"""
|
|
556
|
+
path = _checkpoint_path(target_date)
|
|
557
|
+
if not path.exists():
|
|
558
|
+
return None
|
|
559
|
+
try:
|
|
560
|
+
return json.loads(path.read_text())
|
|
561
|
+
except Exception:
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def save_checkpoint(target_date: str, file_counts: dict[str, int]) -> None:
|
|
566
|
+
"""保存本次处理的 checkpoint(各文件已处理轮次数)"""
|
|
567
|
+
path = _checkpoint_path(target_date)
|
|
568
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
569
|
+
data = {
|
|
570
|
+
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
|
571
|
+
"file_counts": file_counts,
|
|
572
|
+
}
|
|
573
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def generate_incremental_update(
|
|
577
|
+
target_date: str,
|
|
578
|
+
existing_log: str,
|
|
579
|
+
new_claude_data: dict[str, list[list[tuple[str, str]]]],
|
|
580
|
+
new_codex_data: dict[str, list[str]],
|
|
581
|
+
) -> str:
|
|
582
|
+
"""基于已有日志 + 新增对话,调用 AI 生成更新后的完整日志"""
|
|
583
|
+
|
|
584
|
+
def trim(text: str, max_chars: int) -> str:
|
|
585
|
+
return text if len(text) <= max_chars else text[:max_chars] + "…"
|
|
586
|
+
|
|
587
|
+
project_sections = []
|
|
588
|
+
for proj_name, sessions in new_claude_data.items():
|
|
589
|
+
parts = []
|
|
590
|
+
for si, turns in enumerate(sessions, 1):
|
|
591
|
+
parts.append(f" [新增对话 {si}]({len(turns)} 轮)")
|
|
592
|
+
for user_msg, asst_msg in turns:
|
|
593
|
+
parts.append(f" [用户] {trim(user_msg, 200)}")
|
|
594
|
+
if asst_msg:
|
|
595
|
+
parts.append(f" [Claude] {trim(asst_msg, 150)}")
|
|
596
|
+
total_turns = sum(len(t) for t in sessions)
|
|
597
|
+
project_sections.append(
|
|
598
|
+
f"项目: {proj_name}(新增 {len(sessions)} 个对话 {total_turns} 轮)\n" + "\n".join(parts)
|
|
599
|
+
)
|
|
600
|
+
for proj_name, msgs in new_codex_data.items():
|
|
601
|
+
parts = [f" [Codex] {trim(m, 200)}" for m in msgs]
|
|
602
|
+
project_sections.append(f"项目: {proj_name}(Codex 新增 {len(msgs)} 条)\n" + "\n".join(parts))
|
|
603
|
+
|
|
604
|
+
new_data_text = "\n\n".join(project_sections)
|
|
605
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
606
|
+
|
|
607
|
+
prompt = f"""以下是今天已生成的工作日志:
|
|
608
|
+
|
|
609
|
+
{existing_log}
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
以下是在上次生成之后新增的 AI 对话记录:
|
|
613
|
+
|
|
614
|
+
{new_data_text}
|
|
615
|
+
|
|
616
|
+
请在已有日志基础上进行更新,要求:
|
|
617
|
+
1. 更新「今日概览」中的统计数字(累计到最新)
|
|
618
|
+
2. 在对应项目的「主要工作」中追加新工作内容(保留原有内容,不要重复)
|
|
619
|
+
3. 如有新项目,新增对应章节
|
|
620
|
+
4. 更新「今日总结」以反映全天整体工作
|
|
621
|
+
5. 将页脚的生成时间改为 {now_str}
|
|
622
|
+
6. 只输出完整的更新后日志,不要加任何额外说明"""
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
return _call_api_claude_cli(prompt)
|
|
626
|
+
except Exception as e:
|
|
627
|
+
print(f"API 请求失败: {e}", file=sys.stderr)
|
|
628
|
+
return existing_log # 失败时保留原有日志
|
|
629
|
+
|
|
630
|
+
|
|
462
631
|
GITLAB_HOST = "gitcode.lingjingai.cn"
|
|
463
632
|
|
|
464
633
|
|
|
@@ -540,67 +709,67 @@ def git_commit_and_push(log_file: Path, target_date: str, push: bool = True) ->
|
|
|
540
709
|
env["LC_ALL"] = "C" # 强制英文输出,保证字符串匹配不受本地化影响
|
|
541
710
|
r = subprocess.run(cmd, cwd=REPO_DIR, capture_output=True, text=True, env=env)
|
|
542
711
|
if check_err and r.returncode != 0:
|
|
543
|
-
|
|
712
|
+
print_fail(f"{' '.join(cmd[:2])}: {r.stderr.strip()}")
|
|
544
713
|
return r
|
|
545
714
|
|
|
715
|
+
print_step("同步 Git")
|
|
716
|
+
|
|
546
717
|
# ── 1. 初始化 git 仓库 ──────────────────────────────────────────────────
|
|
547
718
|
is_git = run(["git", "rev-parse", "--git-dir"], check_err=False).returncode == 0
|
|
548
719
|
if not is_git:
|
|
549
|
-
|
|
720
|
+
print_info("初始化仓库...")
|
|
550
721
|
if run(["git", "init"]).returncode != 0:
|
|
551
722
|
return
|
|
552
723
|
run(["git", "checkout", "-b", "main"], check_err=False)
|
|
724
|
+
print_ok("git init")
|
|
553
725
|
|
|
554
726
|
# ── 2. 检查并创建 GitLab 远程(无需 glab)────────────────────────────────
|
|
555
727
|
has_remote = run(["git", "remote", "get-url", "origin"], check_err=False).returncode == 0
|
|
556
728
|
if not has_remote:
|
|
557
729
|
repo_name = REPO_DIR.name
|
|
558
|
-
|
|
730
|
+
print_info(f"在 GitLab 创建公开项目 {repo_name} ...")
|
|
559
731
|
try:
|
|
560
732
|
cfg = load_gitlab_config()
|
|
561
733
|
ssh_url = gitlab_create_project(cfg, repo_name)
|
|
562
734
|
run(["git", "remote", "add", "origin", ssh_url])
|
|
563
|
-
|
|
735
|
+
print_ok(f"远程仓库 → {ssh_url}")
|
|
564
736
|
except Exception as e:
|
|
565
|
-
|
|
566
|
-
|
|
737
|
+
print_fail(f"GitLab 创建失败: {e}")
|
|
738
|
+
print_info("请手动: git remote add origin <url>")
|
|
567
739
|
return
|
|
568
740
|
|
|
569
741
|
# ── 3. 确保有初始提交 ─────────────────────────────────────────────────────
|
|
570
742
|
if run(["git", "rev-parse", "HEAD"], check_err=False).returncode != 0:
|
|
571
|
-
print("Git: 创建初始提交...")
|
|
572
743
|
run(["git", "add", "-A"])
|
|
573
744
|
run(["git", "commit", "-m", "init: 初始化工作日志仓库"])
|
|
574
745
|
|
|
575
746
|
# ── 4. 提交日志 ───────────────────────────────────────────────────────────
|
|
576
747
|
rel_path = log_file.relative_to(REPO_DIR)
|
|
577
|
-
print(f"Git: 添加 {rel_path}")
|
|
578
748
|
if run(["git", "add", str(rel_path)]).returncode != 0:
|
|
579
749
|
return
|
|
580
750
|
|
|
581
751
|
commit_msg = f"docs: 工作日志 {target_date}"
|
|
582
|
-
print(f"Git: 提交 '{commit_msg}'")
|
|
583
752
|
r = run(["git", "commit", "-m", commit_msg], check_err=False)
|
|
584
753
|
if r.returncode != 0:
|
|
585
754
|
if "nothing to commit" in r.stdout + r.stderr:
|
|
586
|
-
|
|
755
|
+
print_info("无新变更,跳过提交")
|
|
587
756
|
else:
|
|
588
|
-
|
|
757
|
+
print_fail(f"commit 失败: {r.stderr.strip()}")
|
|
589
758
|
return
|
|
590
|
-
|
|
759
|
+
else:
|
|
760
|
+
print_ok(f"commit: {dim(commit_msg)}")
|
|
591
761
|
|
|
592
762
|
# ── 5. 推送 ────────────────────────────────────────────────────────────────
|
|
593
763
|
if push:
|
|
594
|
-
print("Git: 推送到远程...")
|
|
595
764
|
r = run(["git", "push", "--set-upstream", "origin", "main"], check_err=False)
|
|
596
765
|
if r.returncode != 0:
|
|
597
766
|
r = run(["git", "push"])
|
|
598
767
|
if r.returncode == 0:
|
|
599
|
-
|
|
768
|
+
print_ok("push → origin/main")
|
|
600
769
|
else:
|
|
601
|
-
|
|
770
|
+
print_fail(f"push 失败: {r.stderr.strip()}")
|
|
602
771
|
else:
|
|
603
|
-
|
|
772
|
+
print_info("已跳过 push(--no-push)")
|
|
604
773
|
|
|
605
774
|
|
|
606
775
|
# ─── 主入口 ───────────────────────────────────────────────────────────────────
|
|
@@ -645,51 +814,61 @@ def main():
|
|
|
645
814
|
args = parser.parse_args()
|
|
646
815
|
|
|
647
816
|
target_date = parse_date_arg(args.date)
|
|
648
|
-
print(f"=== 收集 {target_date} 的工作日志 ===")
|
|
649
817
|
|
|
650
|
-
# 1.
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
818
|
+
# 1. 检查 checkpoint
|
|
819
|
+
checkpoint = load_checkpoint(target_date)
|
|
820
|
+
is_incremental = checkpoint is not None
|
|
821
|
+
prev_file_counts = checkpoint["file_counts"] if is_incremental else {}
|
|
822
|
+
prev_time = checkpoint["generated_at"].split("T")[-1] if is_incremental else ""
|
|
823
|
+
|
|
824
|
+
print_header(target_date, is_incremental, prev_time)
|
|
825
|
+
|
|
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
|
|
655
830
|
)
|
|
656
|
-
|
|
657
|
-
|
|
831
|
+
print_project_tree(claude_data, is_sessions=True)
|
|
832
|
+
total_claude_turns = sum(sum(len(t) for t in s) for s in claude_data.values())
|
|
658
833
|
|
|
659
|
-
|
|
834
|
+
# 3. 收集 Codex 对话
|
|
835
|
+
print_step("收集 Codex 对话记录")
|
|
660
836
|
codex_data = collect_codex_sessions(target_date)
|
|
837
|
+
print_project_tree(codex_data, is_sessions=False)
|
|
661
838
|
total_codex = sum(len(v) for v in codex_data.values())
|
|
662
|
-
|
|
839
|
+
|
|
840
|
+
total_projects = len(set(list(claude_data.keys()) + list(codex_data.keys())))
|
|
841
|
+
total_turns = total_claude_turns + total_codex
|
|
663
842
|
|
|
664
843
|
if args.dry_run:
|
|
665
|
-
print("\n[dry-run]
|
|
666
|
-
for proj, sessions in claude_data.items():
|
|
667
|
-
total_turns = sum(len(t) for t in sessions)
|
|
668
|
-
print(f" [Claude/{proj}] {len(sessions)} 个对话,{total_turns} 轮")
|
|
669
|
-
for si, turns in enumerate(sessions[:2], 1):
|
|
670
|
-
print(f" 对话{si}: {len(turns)} 轮")
|
|
671
|
-
for user_msg, asst_msg in turns[:2]:
|
|
672
|
-
print(f" [用户] {user_msg[:60]}")
|
|
673
|
-
if asst_msg:
|
|
674
|
-
print(f" [Claude] {asst_msg[:60]}")
|
|
675
|
-
for proj, msgs in codex_data.items():
|
|
676
|
-
print(f" [Codex/{proj}] {len(msgs)} 条")
|
|
677
|
-
for m in msgs[:2]:
|
|
678
|
-
print(f" - {m[:60]}")
|
|
844
|
+
print(f"\n{dim('[dry-run] 预览完毕,未调用 API')}\n")
|
|
679
845
|
return
|
|
680
846
|
|
|
681
|
-
#
|
|
682
|
-
|
|
683
|
-
|
|
847
|
+
# 4. 生成日志
|
|
848
|
+
if is_incremental:
|
|
849
|
+
if total_claude_turns == 0 and total_codex == 0:
|
|
850
|
+
print(f"\n {yellow('─')} 没有新增内容,无需更新\n")
|
|
851
|
+
return
|
|
852
|
+
print_step(f"调用 {bold(MODEL)} 更新日志(增量)")
|
|
853
|
+
log_file_path = LOGS_DIR / target_date[:4] / f"{target_date}.md"
|
|
854
|
+
existing_log = log_file_path.read_text(encoding="utf-8") if log_file_path.exists() else ""
|
|
855
|
+
content = generate_incremental_update(target_date, existing_log, claude_data, codex_data)
|
|
856
|
+
else:
|
|
857
|
+
print_step(f"调用 {bold(MODEL)} 生成摘要")
|
|
858
|
+
content = generate_summary(target_date, claude_data, codex_data)
|
|
684
859
|
|
|
685
|
-
#
|
|
860
|
+
# 5. 保存文件 + checkpoint
|
|
861
|
+
print_step("保存日志")
|
|
686
862
|
log_file = save_log(target_date, content)
|
|
863
|
+
save_checkpoint(target_date, file_counts)
|
|
864
|
+
rel = str(log_file).replace(str(Path.home()), "~")
|
|
865
|
+
print_ok(rel)
|
|
687
866
|
|
|
688
|
-
#
|
|
867
|
+
# 6. Git 操作
|
|
689
868
|
if not args.no_git:
|
|
690
869
|
git_commit_and_push(log_file, target_date, push=not args.no_push)
|
|
691
870
|
|
|
692
|
-
|
|
871
|
+
print_footer(log_file, total_projects, total_turns)
|
|
693
872
|
|
|
694
873
|
|
|
695
874
|
if __name__ == "__main__":
|