diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e08b3c9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.pdf binary +*.png binary diff --git a/.gitignore b/.gitignore index 63e98d1..d843e81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store tmp/ +__pycache__/ +*.pyc diff --git a/assets/v2/codex-operating-system.png b/assets/v2/codex-operating-system.png new file mode 100644 index 0000000..4621296 Binary files /dev/null and b/assets/v2/codex-operating-system.png differ diff --git a/assets/v2/delivery-loop.png b/assets/v2/delivery-loop.png new file mode 100644 index 0000000..ee747b8 Binary files /dev/null and b/assets/v2/delivery-loop.png differ diff --git a/assets/v2/ninety-day-roadmap.png b/assets/v2/ninety-day-roadmap.png new file mode 100644 index 0000000..886dd9e Binary files /dev/null and b/assets/v2/ninety-day-roadmap.png differ diff --git a/assets/v2/surface-map.png b/assets/v2/surface-map.png new file mode 100644 index 0000000..729fd1a Binary files /dev/null and b/assets/v2/surface-map.png differ diff --git a/docs/Everything-CodeX.md b/docs/Everything-CodeX.md index a0c5bfc..9c9e0a4 100644 --- a/docs/Everything-CodeX.md +++ b/docs/Everything-CodeX.md @@ -2,14 +2,30 @@ 副标题: 从熟练使用到 Top 1% Codex Operator 的完整学习材料 -版本: 2026-05-17, Asia/Singapore +版本: v2 draft, 2026-05-17, Asia/Singapore 适用对象: 想把 Codex 当成长期工程能力来训练的开发者、架构师、技术负责人、AI 工程效率负责人、准备对外讲授 Codex 工作法的人。 > 说明: OpenAI 官方名称是 Codex。本文沿用你给出的 "CodeX" 作为标题风格,但正文默认使用 "Codex"。 +![Everything CodeX operating system](../assets/v2/codex-operating-system.png) + --- +## 阅读地图 + +这版把第一版内容重新排成手册结构: 先建立心智模型,再搭建工作台,然后进入 workflow、自动化、安全、评估和授课。 + +| 章节组 | 你要学到什么 | 推荐产出 | +| --- | --- | --- | +| 0-4 | Codex 的心智模型、能力地图、模型选择、与 everything-claude-code 的迁移关系 | 你自己的 Codex 能力地图 | +| 5-10 | 工作台、AGENTS.md、config、rules、hooks、skills | 一个可复用项目模板 | +| 11-17 | MCP、subagents、worktrees、prompt、CLI、App/IDE/Cloud、GitHub 工作流 | 一套端到端研发流程 | +| 18-22 | 验证、memory、安全、经典模板、自动化 | 一套质量与安全闭环 | +| 23-27 | 90 天训练、评分表、授课大纲、个人 playbook、原则 | 可讲授的课程框架 | + +> 阅读建议: 不要按功能记忆。每读完一章,都问自己: 这部分应该沉淀成 AGENTS.md、skill、hook、rule、MCP,还是自动化脚本? + ## 0. 这份材料怎么学 这不是一份命令手册。它的目标是让你形成一套可复用、可验证、可教学的 Codex 工作系统。 @@ -68,6 +84,8 @@ everything-claude-code 给出的核心思路可以浓缩成一句话: 把 agent 截至 2026-05-17,Codex 的能力已经不是单点工具,而是多 surface 协作。 +![Codex surface map](../assets/v2/surface-map.png) + | Surface | 最适合做什么 | 你要掌握的重点 | | --- | --- | --- | | Codex App | 多项目、多线程、本地 worktree、review pane、automations、computer use、remote host | 把它当作 Codex 控制台 | @@ -953,6 +971,8 @@ Hooks 安全: ## 21. 经典工作流模板 +![Codex delivery loop](../assets/v2/delivery-loop.png) + ### 新功能 ```text @@ -1055,6 +1075,8 @@ Skill 适合复杂 playbook: ## 23. 90 天训练计划 +![90-day operator roadmap](../assets/v2/ninety-day-roadmap.png) + ### 第 1-7 天: 基础能力 目标: 熟练使用 Codex App、CLI、IDE extension。 @@ -1286,4 +1308,3 @@ Codex/ - OpenAI Codex Memories. https://developers.openai.com/codex/memories - OpenAI Codex Security. https://developers.openai.com/codex/security - OpenAI Help: Using Codex with your ChatGPT plan. https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan - diff --git a/docs/Everything-CodeX.pdf b/docs/Everything-CodeX.pdf index 8c622b8..48246df 100644 Binary files a/docs/Everything-CodeX.pdf and b/docs/Everything-CodeX.pdf differ diff --git a/scripts/create_v2_assets.py b/scripts/create_v2_assets.py new file mode 100644 index 0000000..c5e4ee3 --- /dev/null +++ b/scripts/create_v2_assets.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "assets" / "v2" +OUT.mkdir(parents=True, exist_ok=True) + +FONT_REG = "/System/Library/Fonts/Hiragino Sans GB.ttc" +FONT_BOLD = "/System/Library/Fonts/STHeiti Medium.ttc" + + +def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: + return ImageFont.truetype(FONT_BOLD if bold else FONT_REG, size=size) + + +def rounded(draw: ImageDraw.ImageDraw, box, radius, fill, outline=None, width=1): + draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width) + + +def multiline(draw: ImageDraw.ImageDraw, xy, text, fnt, fill, spacing=8): + draw.multiline_text(xy, text, font=fnt, fill=fill, spacing=spacing) + + +def save(img: Image.Image, name: str): + img.save(OUT / name, "PNG", optimize=True) + + +def codex_os(): + img = Image.new("RGB", (1600, 900), "#0f172a") + d = ImageDraw.Draw(img) + d.rectangle((0, 0, 1600, 900), fill="#0f172a") + d.ellipse((1050, -260, 1880, 570), fill="#172554") + d.ellipse((-260, 520, 540, 1210), fill="#0e7490") + + d.text((90, 80), "Codex Operating System", font=font(66, True), fill="#f8fafc") + d.text((94, 160), "把模型能力变成可持续复利的工程系统", font=font(34), fill="#cbd5e1") + + center = (560, 345, 1040, 595) + rounded(d, center, 42, "#f8fafc", "#dbeafe", 4) + d.text((665, 405), "Codex / GPT", font=font(54, True), fill="#0f172a") + d.text((645, 480), "Reasoning + Tools + Context", font=font(26), fill="#334155") + + cards = [ + ((90, 270, 390, 390), "AGENTS.md", "长期规则\n项目约束"), + ((90, 480, 390, 600), "Skills", "按需加载\n复用流程"), + ((1210, 270, 1510, 390), "MCP", "结构化工具\n外部系统"), + ((1210, 480, 1510, 600), "Hooks / Rules", "确定性护栏\n权限边界"), + ((420, 700, 720, 820), "Subagents", "并行探索\n专门角色"), + ((880, 700, 1180, 820), "Verification", "测试证据\n风险闭环"), + ] + for box, title, desc in cards: + rounded(d, box, 26, "#111827", "#38bdf8", 3) + d.text((box[0] + 28, box[1] + 24), title, font=font(30, True), fill="#e0f2fe") + d.text((box[0] + 28, box[1] + 66), desc, font=font(24), fill="#cbd5e1", spacing=4) + + d.text((90, 830), "Top 1% 的差异不是 prompt 更长,而是上下文、流程、验证、记忆被系统化。", font=font(28), fill="#d1fae5") + save(img, "codex-operating-system.png") + + +def surface_map(): + img = Image.new("RGB", (1600, 950), "#f8fafc") + d = ImageDraw.Draw(img) + d.rectangle((0, 0, 1600, 950), fill="#f8fafc") + d.text((90, 70), "Codex Surface Map", font=font(58, True), fill="#0f172a") + d.text((94, 142), "不同入口服务不同任务,不要把所有任务都塞进一个聊天窗口。", font=font(30), fill="#475569") + + headers = ["Surface", "最佳场景", "成熟用法"] + xs = [90, 410, 930] + widths = [280, 470, 580] + y = 230 + row_h = 86 + for i, h in enumerate(headers): + rounded(d, (xs[i], y, xs[i] + widths[i], y + 64), 16, "#0f172a") + d.text((xs[i] + 20, y + 16), h, font=font(28, True), fill="#f8fafc") + rows = [ + ("App", "多项目、多线程、review pane", "用作日常控制台和任务总览"), + ("CLI", "本地工程、日志、JSONL、自动化", "接入 shell、CI、脚本和 schema 输出"), + ("IDE", "打开文件上下文、局部修改", "边看边改,适合小范围高频协作"), + ("Cloud", "后台 issue、PR、CI 修复", "把明确任务交给远程工程师"), + ("GitHub", "PR review、@codex fix", "把 Codex 嵌入团队研发流程"), + ("Mobile", "跟进长任务、批准动作", "离开电脑也能接管后台任务"), + ] + y += 82 + for idx, row in enumerate(rows): + fill = "#ffffff" if idx % 2 == 0 else "#eef6ff" + for i, txt in enumerate(row): + d.rounded_rectangle((xs[i], y, xs[i] + widths[i], y + row_h - 10), radius=14, fill=fill, outline="#cbd5e1", width=2) + d.text((xs[i] + 20, y + 22), txt, font=font(25, i == 0), fill="#0f172a") + y += row_h + + d.text((90, 860), "原则: 能用 CLI 确定性解决的,不一定上 MCP;需要长期复用的,沉淀为 skill。", font=font(27), fill="#0f766e") + save(img, "surface-map.png") + + +def workflow_loop(): + img = Image.new("RGB", (1600, 850), "#fff7ed") + d = ImageDraw.Draw(img) + d.text((90, 70), "Codex Delivery Loop", font=font(58, True), fill="#111827") + d.text((94, 142), "每个任务都按 Explore -> Plan -> Implement -> Review -> Verify -> Capture 形成闭环。", font=font(30), fill="#475569") + steps = [ + ("Explore", "只读探索\n真实调用链"), + ("Plan", "拆阶段\n定义风险"), + ("Implement", "最小改动\nTDD 优先"), + ("Review", "owner-level\n找真实问题"), + ("Verify", "测试证据\n可复现结果"), + ("Capture", "沉淀 skill\n更新规则"), + ] + x0, y0 = 90, 330 + w, h, gap = 210, 180, 38 + colors = ["#0ea5e9", "#2563eb", "#7c3aed", "#db2777", "#ea580c", "#059669"] + for i, (title, desc) in enumerate(steps): + x = x0 + i * (w + gap) + rounded(d, (x, y0, x + w, y0 + h), 28, colors[i]) + d.text((x + 26, y0 + 30), title, font=font(30, True), fill="#ffffff") + d.text((x + 26, y0 + 82), desc, font=font(25), fill="#f8fafc", spacing=7) + if i < len(steps) - 1: + d.line((x + w + 8, y0 + h // 2, x + w + gap - 8, y0 + h // 2), fill="#334155", width=5) + d.polygon([(x + w + gap - 8, y0 + h // 2), (x + w + gap - 26, y0 + h // 2 - 12), (x + w + gap - 26, y0 + h // 2 + 12)], fill="#334155") + rounded(d, (170, 650, 1430, 755), 28, "#ffffff", "#fed7aa", 3) + d.text((220, 684), "Done Definition: 代码可运行、测试有证据、diff 可 review、剩余风险说清楚。", font=font(32, True), fill="#9a3412") + save(img, "delivery-loop.png") + + +def roadmap(): + img = Image.new("RGB", (1600, 850), "#ecfeff") + d = ImageDraw.Draw(img) + d.text((90, 70), "90-Day Operator Roadmap", font=font(58, True), fill="#0f172a") + d.text((94, 142), "从会用 Codex,到能把 Codex 讲给别人听。", font=font(30), fill="#475569") + phases = [ + ("Days 1-7", "基础能力", "App / CLI / IDE\nPrompt + 验证"), + ("Week 2", "配置能力", "AGENTS.md\nconfig + rules"), + ("Weeks 3-4", "工作流能力", "TDD / Review\nDomain skills"), + ("Month 2", "编排能力", "Subagents\nWorktrees + MCP"), + ("Month 3", "专家能力", "Evals\n课程与团队 playbook"), + ] + x, y = 110, 300 + for i, (time, title, desc) in enumerate(phases): + x1 = x + i * 292 + rounded(d, (x1, y, x1 + 248, y + 310), 30, "#ffffff", "#67e8f9", 4) + d.ellipse((x1 + 82, y - 48, x1 + 166, y + 36), fill="#0891b2") + d.text((x1 + 110, y - 32), str(i + 1), font=font(42, True), fill="#ffffff", anchor="mm") + d.text((x1 + 28, y + 58), time, font=font(25, True), fill="#155e75") + d.text((x1 + 28, y + 112), title, font=font(34, True), fill="#0f172a") + d.text((x1 + 28, y + 178), desc, font=font(25), fill="#334155", spacing=8) + d.text((110, 715), "验收标准: 你不仅能完成任务,还能解释失败、改造流程、沉淀成可复用资产。", font=font(31, True), fill="#0f766e") + save(img, "ninety-day-roadmap.png") + + +if __name__ == "__main__": + codex_os() + surface_map() + workflow_loop() + roadmap() + print(f"wrote {OUT}") diff --git a/scripts/render_handbook_pdf.py b/scripts/render_handbook_pdf.py new file mode 100644 index 0000000..535222f --- /dev/null +++ b/scripts/render_handbook_pdf.py @@ -0,0 +1,365 @@ +from __future__ import annotations + +import html +import re +import textwrap +from pathlib import Path + +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import cm +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.cidfonts import UnicodeCIDFont +from reportlab.platypus import ( + Image, + PageBreak, + Paragraph, + Preformatted, + SimpleDocTemplate, + Spacer, + Table, + TableStyle, +) +from PIL import Image as PILImage + + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "docs" / "Everything-CodeX.md" +OUT = ROOT / "docs" / "Everything-CodeX.pdf" + + +pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light")) +FONT = "STSong-Light" + + +def styles(): + base = getSampleStyleSheet() + return { + "title": ParagraphStyle( + "TitleCN", + parent=base["Title"], + fontName=FONT, + fontSize=31, + leading=38, + alignment=TA_CENTER, + textColor=colors.HexColor("#0f172a"), + spaceAfter=20, + wordWrap="CJK", + ), + "subtitle": ParagraphStyle( + "SubtitleCN", + parent=base["BodyText"], + fontName=FONT, + fontSize=11.5, + leading=17, + textColor=colors.HexColor("#334155"), + alignment=TA_CENTER, + wordWrap="CJK", + ), + "h2": ParagraphStyle( + "H2CN", + parent=base["Heading2"], + fontName=FONT, + fontSize=17, + leading=23, + textColor=colors.HexColor("#0f172a"), + spaceBefore=15, + spaceAfter=8, + wordWrap="CJK", + ), + "h3": ParagraphStyle( + "H3CN", + parent=base["Heading3"], + fontName=FONT, + fontSize=12.5, + leading=18, + textColor=colors.HexColor("#1e293b"), + spaceBefore=10, + spaceAfter=5, + wordWrap="CJK", + ), + "body": ParagraphStyle( + "BodyCN", + parent=base["BodyText"], + fontName=FONT, + fontSize=9.6, + leading=14.4, + textColor=colors.HexColor("#111827"), + spaceAfter=5.5, + alignment=TA_LEFT, + wordWrap="CJK", + ), + "small": ParagraphStyle( + "SmallCN", + parent=base["BodyText"], + fontName=FONT, + fontSize=8.1, + leading=11.5, + textColor=colors.HexColor("#1f2937"), + wordWrap="CJK", + ), + "bullet": ParagraphStyle( + "BulletCN", + parent=base["BodyText"], + fontName=FONT, + fontSize=9.3, + leading=13.8, + leftIndent=13, + firstLineIndent=-9, + spaceAfter=3.5, + textColor=colors.HexColor("#111827"), + wordWrap="CJK", + ), + "quote": ParagraphStyle( + "QuoteCN", + parent=base["BodyText"], + fontName=FONT, + fontSize=9.5, + leading=14.8, + leftIndent=12, + rightIndent=8, + borderColor=colors.HexColor("#93c5fd"), + borderWidth=1, + borderPadding=8, + backColor=colors.HexColor("#eff6ff"), + textColor=colors.HexColor("#1e3a8a"), + spaceBefore=4, + spaceAfter=8, + wordWrap="CJK", + ), + "code": ParagraphStyle( + "CodeCN", + fontName=FONT, + fontSize=7.2, + leading=9.4, + textColor=colors.HexColor("#111827"), + backColor=colors.HexColor("#f8fafc"), + borderColor=colors.HexColor("#cbd5e1"), + borderWidth=0.6, + borderPadding=6, + spaceBefore=4, + spaceAfter=8, + ), + } + + +STYLES = styles() + + +def inline(text: str) -> str: + text = html.escape(text) + text = re.sub(r"`([^`]+)`", r"\1", text) + text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) + text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text) + return text + + +def para(text: str, style: str = "body") -> Paragraph: + return Paragraph(inline(text), STYLES[style]) + + +def code_block(text: str) -> Preformatted: + out: list[str] = [] + for line in text.rstrip("\n").splitlines(): + if len(line) <= 92: + out.append(line) + else: + out.extend(textwrap.wrap(line, width=92, replace_whitespace=False, drop_whitespace=False)) + return Preformatted("\n".join(out), STYLES["code"]) + + +def image_flowable(rel_path: str): + path = (SRC.parent / rel_path).resolve() + if not path.exists(): + return para(f"[missing image: {rel_path}]", "quote") + max_w = A4[0] - 3.2 * cm + with PILImage.open(path) as im: + w, h = im.size + scale = min(max_w / w, 8.0 * cm / h) + return Image(str(path), width=w * scale, height=h * scale, hAlign="CENTER") + + +def table_from(lines: list[str]): + rows = [] + for line in lines: + stripped = line.strip() + if re.fullmatch(r"\|?[\s:|-]+\|?", stripped): + continue + cells = [c.strip() for c in stripped.strip("|").split("|")] + rows.append([Paragraph(inline(c), STYLES["small"]) for c in cells]) + if not rows: + return [] + col_count = max(len(r) for r in rows) + for row in rows: + while len(row) < col_count: + row.append(Paragraph("", STYLES["small"])) + available = A4[0] - 3.2 * cm + widths = [available / col_count] * col_count + table = Table(rows, colWidths=widths, repeatRows=1, hAlign="LEFT") + table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#e2e8f0")), + ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#cbd5e1")), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (-1, -1), 5), + ("RIGHTPADDING", (0, 0), (-1, -1), 5), + ("TOPPADDING", (0, 0), (-1, -1), 5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 5), + ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f8fafc")]), + ] + ) + ) + return [table, Spacer(1, 7)] + + +def parse(text: str): + lines = text.splitlines() + story = [] + i = 0 + in_code = False + code_lines: list[str] = [] + first_title = True + + while i < len(lines): + raw = lines[i] + stripped = raw.strip() + + if stripped.startswith("```"): + if in_code: + story.append(code_block("\n".join(code_lines))) + code_lines = [] + in_code = False + else: + in_code = True + i += 1 + continue + + if in_code: + code_lines.append(raw) + i += 1 + continue + + if not stripped: + story.append(Spacer(1, 3)) + i += 1 + continue + + if stripped == "---": + story.append(Spacer(1, 9)) + i += 1 + continue + + img_match = re.match(r"!\[[^\]]*\]\(([^)]+)\)", stripped) + if img_match: + image_path = img_match.group(1) + story.append(Spacer(1, 5)) + story.append(image_flowable(image_path)) + story.append(Spacer(1, 9)) + if "codex-operating-system" in image_path: + story.append(PageBreak()) + i += 1 + continue + + if stripped.startswith("|") and "|" in stripped[1:]: + tbl = [] + while i < len(lines) and lines[i].strip().startswith("|"): + tbl.append(lines[i]) + i += 1 + story.extend(table_from(tbl)) + continue + + if stripped.startswith("# "): + title = stripped[2:].strip() + if first_title: + story.append(Spacer(1, 2.0 * cm)) + story.append(para(title, "title")) + first_title = False + else: + story.append(PageBreak()) + story.append(para(title, "h2")) + i += 1 + continue + + if stripped.startswith("## "): + if not stripped.startswith("## 阅读地图") and not stripped.startswith("## 0."): + story.append(Spacer(1, 4)) + story.append(para(stripped[3:].strip(), "h2")) + i += 1 + continue + + if stripped.startswith("### "): + story.append(para(stripped[4:].strip(), "h3")) + i += 1 + continue + + if stripped.startswith(">"): + story.append(para(stripped.lstrip("> ").strip(), "quote")) + i += 1 + continue + + if re.match(r"^[-*] ", stripped): + story.append(para("- " + stripped[2:].strip(), "bullet")) + i += 1 + continue + + if re.match(r"^\d+\. ", stripped): + story.append(para(stripped, "bullet")) + i += 1 + continue + + parts = [stripped] + i += 1 + while i < len(lines): + nxt = lines[i].strip() + if ( + not nxt + or nxt == "---" + or nxt.startswith("#") + or nxt.startswith("```") + or nxt.startswith("|") + or nxt.startswith("> ") + or nxt.startswith("![") + or re.match(r"^[-*] ", nxt) + or re.match(r"^\d+\. ", nxt) + ): + break + parts.append(nxt) + i += 1 + if parts[0].startswith("副标题:") or parts[0].startswith("版本:") or parts[0].startswith("适用对象:"): + story.append(para(" ".join(parts), "subtitle")) + else: + story.append(para(" ".join(parts), "body")) + return story + + +def footer(canvas, doc): + canvas.saveState() + canvas.setFont(FONT, 8) + canvas.setFillColor(colors.HexColor("#64748b")) + canvas.line(1.6 * cm, 1.35 * cm, A4[0] - 1.6 * cm, 1.35 * cm) + canvas.drawString(1.6 * cm, 1.0 * cm, "Everything CodeX v2 draft") + canvas.drawRightString(A4[0] - 1.6 * cm, 1.0 * cm, str(doc.page)) + canvas.restoreState() + + +def main(): + story = parse(SRC.read_text(encoding="utf-8")) + doc = SimpleDocTemplate( + str(OUT), + pagesize=A4, + leftMargin=1.6 * cm, + rightMargin=1.6 * cm, + topMargin=1.55 * cm, + bottomMargin=1.75 * cm, + title="Everything CodeX v2", + author="tidus2005", + ) + doc.build(story, onFirstPage=footer, onLaterPages=footer) + print(OUT) + + +if __name__ == "__main__": + main()