#!/usr/bin/env python3 """ Blog Post Consultant Review Management Tool - Tracks review status of all blog posts (pending, approved, needs_revision) - Automatically partitions unapproved posts into batches (< 200k chars) - Injects background, constraints, persona rules, and purpose into each batch - Allows 1-command approval so approved posts are permanently skipped next time """ import os import sys import json import glob import re import argparse from datetime import datetime REPO_ROOT = "D:/dev/hoelee-blog" POSTS_DIR = os.path.join(REPO_ROOT, "src/content/posts") ZH_POSTS_DIR = os.path.join(POSTS_DIR, "zh") REVIEW_DIR = os.path.join(REPO_ROOT, "docs/consultant-review") BATCHES_DIR = os.path.join(REVIEW_DIR, "batches") STATUS_FILE = os.path.join(REVIEW_DIR, "audit-tracker.json") TRACKER_MD = os.path.join(REVIEW_DIR, "AUDIT-TRACKER.md") BRIEF_MD = os.path.join(REVIEW_DIR, "00-外部顾问审阅须知与背景规范.md") BACKGROUND_HEADER = """# 外部顾问审阅前置须知与工程背景规范 > **本文档目标**:本批次包含 `blog.hoelee.com` 的待审阅技术文章全文。请外部顾问在审阅正文与技术主张时,以此规范为最高评估准则。 ## 1. 站点定位与双重业务目的 - **受众 A(技术招聘官 / 资深技术主管)**:寻找具备 Senior 全栈 + DevOps + AI 智能体闭环能力的架构工程师。文章定位为**“不可伪造的工程实操交付证据(Proof of Expertise)”**,要求代码真实、日志可查、有架构权衡与技术深度。 - **受众 B(中小企业主 SME / 商业客户)**:寻找能提供系统研发、私有化部署、24/7 AI 获客系统、高可用基础设施的技术顾问。文章需清晰阐述业务痛点、商业 ROI、上线效率与交付保障。 ## 2. 核心人设铁律与工程硬约束(必须严格遵守) 1. **彻底杜绝业余爱好者/玩具口吻**: - ❌ **绝对禁用词**:`homelab`, `home lab`, `家用`, `小主机`, `玩具`, `折腾`。 - ✅ **标准替代词**:`the infrastructure I manage`(我管理的私有基础设施)、`self-hosted production node`(自托管生产节点)、`on-premise host`(私有化服务器)。 2. **严禁捏造虚假指标与性能数字**: - 所有 benchmark 数据必须有明确的测试基准与单设备限制。 - 严格区分“耗时减少比例”与“吞吐加速倍数”(例如耗时从 352s 降至 218s 是耗时减少 38.0%,吞吐提升 1.61×,严禁偷换概念混淆)。 - 时间戳精度与物理帧对齐:视频帧率上限(如 30 fps 单帧 33.3ms)不可妄称“微秒级精确”。 3. **去模板化叙事与真实物理锚点**: - 拒绝千篇一律的 AI 模板味。文章采用三大叙事架构:① 故障排查谜案(Symptom → False Lead → Kernel Evidence → Fix);② 商业交付复盘(Client Bottleneck → Trade-offs → State Machine → ROI);③ 架构演进对比(V1 Naive → Concurrency Crash → V2 Redesign)。 - 每篇文章必须包含至少一个物理真实世界锚点(主机真实硬件规格、生产维护窗口、真实业务场景)。 4. **代码与配置展示规范**: - 超过 20 行的配置或日志采用 HTML `
` 原生折叠,`` 必须精准标注文档名与行数。 ## 3. 顾问重点开刀维度 - [ ] **事实与数字可核性**:技术判断与性能数据是否自洽?有无过度夸大? - [ ] **人设与语调纯洁性**:有无遗漏的 hobbyist/homelab 口吻? - [ ] **双轨转化引导有效性**:是否有清晰的业务价值提炼与行动召唤(CTA)? - [ ] **代码与生产安全性**:有无未脱敏的凭据、绝对内部私密路径或不安全的权限配置? """ def load_or_init_tracker(): tracker = {} if os.path.exists(STATUS_FILE): with open(STATUS_FILE, "r", encoding="utf-8") as f: try: tracker = json.load(f) except Exception: tracker = {} # Scan all posts in POSTS_DIR en_files = sorted(glob.glob(os.path.join(POSTS_DIR, "*.md"))) for en_path in en_files: slug = os.path.splitext(os.path.basename(en_path))[0] zh_path = os.path.join(ZH_POSTS_DIR, f"{slug}.md") with open(en_path, "r", encoding="utf-8") as f: en_content = f.read() zh_content = "" if os.path.exists(zh_path): with open(zh_path, "r", encoding="utf-8") as f: zh_content = f.read() # Extract category, title, pubDate safely without regex multiline newlines category = "uncategorized" title = slug zh_title = slug pub_date = "unknown" for line in en_content.splitlines(): if line.startswith("category:"): category = line.split(":", 1)[1].strip().strip('"\'') elif line.startswith("title:"): title = line.split(":", 1)[1].strip().strip('"\'') elif line.startswith("pubDate:"): pub_date = line.split(":", 1)[1].strip().strip('"\'') for line in zh_content.splitlines(): if line.startswith("title:"): zh_title = line.split(":", 1)[1].strip().strip('"\'') if slug not in tracker: tracker[slug] = { "slug": slug, "title": title, "zh_title": zh_title, "category": category, "pub_date": pub_date, "status": "pending", # pending | approved | rejected | needs_revision "batch_id": None, "approved_at": None, "notes": "", "en_chars": len(en_content), "zh_chars": len(zh_content) } else: # Update dynamic fields tracker[slug]["title"] = title tracker[slug]["zh_title"] = zh_title tracker[slug]["category"] = category tracker[slug]["pub_date"] = pub_date tracker[slug]["en_chars"] = len(en_content) tracker[slug]["zh_chars"] = len(zh_content) save_tracker(tracker) return tracker def save_tracker(tracker): os.makedirs(REVIEW_DIR, exist_ok=True) with open(STATUS_FILE, "w", encoding="utf-8") as f: json.dump(tracker, f, ensure_ascii=False, indent=2) render_tracker_markdown(tracker) def render_tracker_markdown(tracker): total = len(tracker) approved = sum(1 for p in tracker.values() if p.get("status") == "approved") pending = total - approved lines = [ "# 全站文章顾问审阅状态总控台 (Audit Tracker)", "", f"> **最后同步时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ", f"> **全站总文章数**:{total} 篇 | **已审批完成**:{approved} 篇 ({approved/total*100:.1f}%) | **待审阅**:{pending} 篇", "", "## 一、操作命令速查", "- `python scripts/manage_review.py list` — 查看待审与已审概览", "- `python scripts/manage_review.py approve [--notes '顾问通过']` — 标记单篇通过", "- `python scripts/manage_review.py approve-batch ` — 标记整个批次通过", "- `python scripts/manage_review.py generate` — 重新分批(自动排除已审批文章,每批 < 200k 字)", "", "## 二、文章审核状态总表", "| # | 分类 | 文章 Slug | 中文标题 | 状态 | 所在批次 | 审批时间 | 顾问备注 |", "|---|---|---|---|:---:|:---:|:---:|---|" ] sorted_posts = sorted(tracker.values(), key=lambda x: (x.get("category", ""), x.get("slug", ""))) for idx, p in enumerate(sorted_posts, 1): status_icon = "✅ 已通过" if p.get("status") == "approved" else "⏳ 待审阅" if p.get("status") == "needs_revision": status_icon = "⚠️ 需修改" batch = p.get("batch_id") or "未分配" approved_time = p.get("approved_at") or "-" notes = (p.get("notes") or "").replace("|", "/") lines.append(f"| {idx} | `{p['category']}` | `{p['slug']}` | {p['zh_title']} | {status_icon} | {batch} | {approved_time} | {notes} |") with open(TRACKER_MD, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") def write_master_brief(): with open(BRIEF_MD, "w", encoding="utf-8") as f: f.write(BACKGROUND_HEADER + """ ## 4. 博客 7 大分类与内容策略参考 - `case-studies` (商业案例):求职与获客核心武器,以真实商业交付和解决瓶颈为核心。 - `devops` (私有基建):高并发网关、Docker、Traefik、存储阵列与监控,最强差异化资产。 - `ai` (人工智能与智能体):围绕自动化工作流、LiteLLM 网关、状态机防幻觉与本地私有大模型。 - `engineering` (系统架构与后端):Java/Spring、PHP/CodeIgniter、TS/React 的硬核实战。 - `tutorials` (技术教程):面向学习者的深度实战指南。 - `notes` (排障速记):轻量级踩坑手记与高频高价值 gotcha。 - `web3` (智能合约):Solidity/Foundry 严谨实践,诚实表达学习与验证阶段。 ## 5. 既有核心参考文档清单(仓库内置) - `docs/content-guide.md` — 博客内容定位、选题规范、反模式与 i18n 策略。 - `docs/post-publishing-sop-v2.md` — 5 步叙事模型、代码折叠与 Bento 卡片技术规范。 - `docs/consultant-review-2026-10-26-supplement.md` — 上一轮顾问诊断全景资料包。 """) def generate_batches(mode="bilingual", max_chars=180000): tracker = load_or_init_tracker() write_master_brief() os.makedirs(BATCHES_DIR, exist_ok=True) # Clean previous batch files for old_b in glob.glob(os.path.join(BATCHES_DIR, "batch-*.md")): try: os.remove(old_b) except Exception: pass # Filter unapproved posts pending_posts = [p for p in tracker.values() if p.get("status") != "approved"] if not pending_posts: print("All posts are approved! No new batches needed.") return # Sort deterministically: category, then slug pending_posts.sort(key=lambda x: (x.get("category", ""), x.get("slug", ""))) batches = [] current_batch = [] current_chars = 0 for p in pending_posts: slug = p['slug'] en_path = os.path.join(POSTS_DIR, f"{slug}.md") zh_path = os.path.join(ZH_POSTS_DIR, f"{slug}.md") with open(en_path, "r", encoding="utf-8") as f: en_text = f.read() zh_text = "" if os.path.exists(zh_path): with open(zh_path, "r", encoding="utf-8") as f: zh_text = f.read() if mode == "bilingual": item_chars = len(en_text) + len(zh_text) if item_chars > max_chars: # Split giant post into ZH and EN individual sub-batches if current_batch: batches.append(current_batch) current_batch = [] current_chars = 0 batches.append([{ "p": p, "mode": "zh_only", "slug": slug + "-zh", "content_zh": zh_text, "content_en": "", "chars": len(zh_text) }]) batches.append([{ "p": p, "mode": "en_only", "slug": slug + "-en", "content_zh": "", "content_en": en_text, "chars": len(en_text) }]) continue item = { "p": p, "mode": "bi", "slug": slug, "content_zh": zh_text, "content_en": en_text, "chars": item_chars } elif mode == "zh": item_chars = len(zh_text) item = { "p": p, "mode": "zh_only", "slug": slug, "content_zh": zh_text, "content_en": "", "chars": item_chars } else: # en item_chars = len(en_text) item = { "p": p, "mode": "en_only", "slug": slug, "content_zh": "", "content_en": en_text, "chars": item_chars } if current_chars + item_chars > max_chars and current_batch: batches.append(current_batch) current_batch = [item] current_chars = item_chars else: current_batch.append(item) current_chars += item_chars if current_batch: batches.append(current_batch) # Write each batch file for b_idx, batch_items in enumerate(batches, 1): batch_id = f"Batch-{b_idx:02d}" batch_char_sum = sum(x['chars'] for x in batch_items) batch_filename = f"batch-{b_idx:02d}.md" batch_filepath = os.path.join(BATCHES_DIR, batch_filename) lines = [ f"# 博客文章顾问审阅资料包 — {batch_id}", "", f"> **批次编号**:`{batch_id}` ", f"> **包含文章数**:{len(batch_items)} 篇 ", f"> **本批次总字符量**:{batch_char_sum:,} 字(严格限制 < 200,000 字) ", f"> **模式**:{mode.upper()} ", f"> **生成时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ", "", BACKGROUND_HEADER, "", "---", "", "## 本批次文章目录索引", "" ] for i, item in enumerate(batch_items, 1): p = item['p'] lines.append(f"{i}. [{p['category']}] **{p['zh_title']}** (`{item['slug']}`) — ~{item['chars']:,} 字符") lines.append("") lines.append("---") lines.append("") for i, item in enumerate(batch_items, 1): p = item['p'] slug = p['slug'] tracker[slug]["batch_id"] = batch_id lines.append(f"## 文章 {i:02d} / {len(batch_items):02d}:{p['zh_title']}") lines.append(f"- **Slug**: `{slug}`") lines.append(f"- **英文标题**: {p['title']}") lines.append(f"- **分类**: `{p['category']}` | **发布日期**: {p['pub_date']}") lines.append(f"- **线上地址**: https://blog.hoelee.com/posts/{slug}/ (EN) | https://blog.hoelee.com/posts/zh/{slug}/ (ZH)") lines.append(f"- **本篇审核状态**: ⏳ 待审核 (Pending)") lines.append("") lines.append("### ✍️ 顾问审阅意见区 (Review Feedback)") lines.append("```text") lines.append("【审阅结论】:[ ] 通过 [ ] 需微调 [ ] 需重构") lines.append("【事实与数字核对】:") lines.append("【人设与用词卫生】:") lines.append("【CRO与转化建议】:") lines.append("```") lines.append("") if item["content_zh"]: lines.append("### 🇨🇳 中文版原文 (Chinese Markdown Twin)") lines.append("````markdown") lines.append(item["content_zh"].strip()) lines.append("````") lines.append("") if item["content_en"]: lines.append("### 🇬🇧 英文版原文 (English Markdown Primary)") lines.append("````markdown") lines.append(item["content_en"].strip()) lines.append("````") lines.append("") lines.append("---") lines.append("") with open(batch_filepath, "w", encoding="utf-8") as bf: bf.write("\n".join(lines) + "\n") print(f"Generated {batch_id}: {len(batch_items)} posts, {batch_char_sum:,} chars -> {batch_filepath}") save_tracker(tracker) print(f"\n[OK] Generated {len(batches)} batches in {BATCHES_DIR}. Total pending posts processed: {len(pending_posts)}") def approve_posts(slugs, notes=None): tracker = load_or_init_tracker() now_str = datetime.now().strftime("%Y-%m-%d %H:%M") count = 0 for slug in slugs: if slug in tracker: tracker[slug]["status"] = "approved" tracker[slug]["approved_at"] = now_str if notes: tracker[slug]["notes"] = notes count += 1 print(f"Approved: {slug}") else: print(f"Warning: Slug '{slug}' not found in tracker.") save_tracker(tracker) print(f"Successfully marked {count} post(s) as approved.") def approve_batch(batch_num, notes=None): tracker = load_or_init_tracker() target_batch = f"Batch-{int(batch_num):02d}" matched_slugs = [slug for slug, p in tracker.items() if p.get("batch_id") == target_batch] if not matched_slugs: print(f"No posts found belonging to {target_batch}.") return approve_posts(matched_slugs, notes=notes or f"Approved via {target_batch}") def reset_posts(slugs): tracker = load_or_init_tracker() count = 0 for slug in slugs: if slug in tracker: tracker[slug]["status"] = "pending" tracker[slug]["approved_at"] = None tracker[slug]["notes"] = "" count += 1 print(f"Reset to pending: {slug}") save_tracker(tracker) print(f"Successfully reset {count} post(s).") def reset_all(): tracker = load_or_init_tracker() for slug in tracker: tracker[slug]["status"] = "pending" tracker[slug]["approved_at"] = None tracker[slug]["notes"] = "" save_tracker(tracker) print(f"Successfully reset all {len(tracker)} posts to pending.") def main(): parser = argparse.ArgumentParser(description="Blog Post Consultant Review Management") subparsers = parser.add_subparsers(dest="command") # init subparsers.add_parser("init", help="Initialize or update tracker from markdown files") # generate gen_p = subparsers.add_parser("generate", help="Generate review batches (< 200k chars each)") gen_p.add_argument("--mode", choices=["bilingual", "zh", "en"], default="bilingual", help="Content mode") gen_p.add_argument("--max-chars", type=int, default=180000, help="Max characters per batch") # approve app_p = subparsers.add_parser("approve", help="Mark slug(s) as approved") app_p.add_argument("slugs", nargs="+", help="Post slug(s) to approve") app_p.add_argument("--notes", default="Approved by consultant", help="Consultant review note") # approve-batch app_b = subparsers.add_parser("approve-batch", help="Mark all posts in a batch as approved") app_b.add_argument("batch_num", type=int, help="Batch number (e.g. 1)") app_b.add_argument("--notes", default="Approved by consultant", help="Consultant review note") # reset rst_p = subparsers.add_parser("reset", help="Reset slug(s) to pending") rst_p.add_argument("slugs", nargs="+", help="Post slug(s) to reset") # reset-all subparsers.add_parser("reset-all", help="Reset all posts to pending") # list subparsers.add_parser("list", help="List summary of review status") args = parser.parse_args() if args.command == "init": t = load_or_init_tracker() print(f"Tracker initialized with {len(t)} posts.") elif args.command == "generate": generate_batches(mode=args.mode, max_chars=args.max_chars) elif args.command == "approve": approve_posts(args.slugs, notes=args.notes) elif args.command == "approve-batch": approve_batch(args.batch_num, notes=args.notes) elif args.command == "reset": reset_posts(args.slugs) elif args.command == "reset-all": reset_all() elif args.command == "list" or not args.command: tracker = load_or_init_tracker() total = len(tracker) approved = sum(1 for p in tracker.values() if p.get("status") == "approved") pending = total - approved print(f"=== Blog Post Consultant Review Status ===") print(f"Total Posts: {total} | Approved: {approved} | Pending: {pending}") by_cat = {} for p in tracker.values(): cat = p['category'] by_cat.setdefault(cat, [0, 0]) if p.get('status') == 'approved': by_cat[cat][0] += 1 else: by_cat[cat][1] += 1 for cat, (app, pend) in sorted(by_cat.items()): print(f" {cat:15s}: {app:2d} approved / {pend:2d} pending") if __name__ == "__main__": main()