From 4c0107a322f9b69015710c4e9721dcc9d130dd92 Mon Sep 17 00:00:00 2001 From: neo Date: Fri, 13 Mar 2026 17:45:44 +0800 Subject: [PATCH] docs(zh-CN): update --- docs/zh-CN/AGENTS.md | 33 +- docs/zh-CN/CODE_OF_CONDUCT.md | 9 +- docs/zh-CN/CONTRIBUTING.md | 24 + docs/zh-CN/README.md | 404 +++++---- docs/zh-CN/TROUBLESHOOTING.md | 446 ++++++++++ docs/zh-CN/agents/kotlin-build-resolver.md | 119 +++ docs/zh-CN/agents/kotlin-reviewer.md | 161 ++++ docs/zh-CN/commands/aside.md | 173 ++++ docs/zh-CN/commands/e2e.md | 5 +- docs/zh-CN/commands/gradle-build.md | 72 ++ docs/zh-CN/commands/kotlin-build.md | 176 ++++ docs/zh-CN/commands/kotlin-review.md | 144 +++ docs/zh-CN/commands/kotlin-test.md | 315 +++++++ docs/zh-CN/commands/learn-eval.md | 83 +- docs/zh-CN/commands/multi-workflow.md | 8 + docs/zh-CN/commands/orchestrate.md | 55 ++ docs/zh-CN/commands/plan.md | 5 +- docs/zh-CN/commands/prompt-optimize.md | 37 + docs/zh-CN/commands/python-review.md | 2 +- docs/zh-CN/commands/resume-session.md | 155 ++++ docs/zh-CN/commands/save-session.md | 252 ++++++ docs/zh-CN/commands/sessions.md | 34 +- docs/zh-CN/commands/tdd.md | 10 +- docs/zh-CN/examples/user-CLAUDE.md | 7 + docs/zh-CN/hooks/README.md | 11 +- docs/zh-CN/rules/README.md | 7 +- .../rules/common/development-workflow.md | 33 +- docs/zh-CN/rules/kotlin/coding-style.md | 90 ++ docs/zh-CN/rules/kotlin/hooks.md | 18 + docs/zh-CN/rules/kotlin/patterns.md | 147 ++++ docs/zh-CN/rules/kotlin/security.md | 83 ++ docs/zh-CN/rules/kotlin/testing.md | 129 +++ docs/zh-CN/rules/perl/coding-style.md | 47 + docs/zh-CN/rules/perl/hooks.md | 23 + docs/zh-CN/rules/perl/patterns.md | 77 ++ docs/zh-CN/rules/perl/security.md | 70 ++ docs/zh-CN/rules/perl/testing.md | 55 ++ docs/zh-CN/rules/php/coding-style.md | 36 + docs/zh-CN/rules/php/hooks.md | 25 + docs/zh-CN/rules/php/patterns.md | 33 + docs/zh-CN/rules/php/security.md | 34 + docs/zh-CN/rules/php/testing.md | 35 + docs/zh-CN/rules/typescript/coding-style.md | 160 +++- .../android-clean-architecture/SKILL.md | 339 +++++++ docs/zh-CN/skills/blueprint/SKILL.md | 96 ++ .../carrier-relationship-management/SKILL.md | 199 +++++ docs/zh-CN/skills/claude-api/SKILL.md | 337 +++++++ .../compose-multiplatform-patterns/SKILL.md | 299 +++++++ docs/zh-CN/skills/configure-ecc/SKILL.md | 41 +- .../skills/continuous-learning-v2/SKILL.md | 1 + docs/zh-CN/skills/crosspost/SKILL.md | 209 +++++ .../skills/customs-trade-compliance/SKILL.md | 256 ++++++ docs/zh-CN/skills/deep-research/SKILL.md | 163 ++++ docs/zh-CN/skills/dmux-workflows/SKILL.md | 193 ++++ docs/zh-CN/skills/energy-procurement/SKILL.md | 220 +++++ docs/zh-CN/skills/exa-search/SKILL.md | 186 ++++ docs/zh-CN/skills/fal-ai-media/SKILL.md | 296 +++++++ .../skills/inventory-demand-planning/SKILL.md | 233 +++++ .../zh-CN/skills/iterative-retrieval/SKILL.md | 6 +- .../skills/kotlin-coroutines-flows/SKILL.md | 284 ++++++ .../skills/kotlin-exposed-patterns/SKILL.md | 719 +++++++++++++++ .../skills/kotlin-ktor-patterns/SKILL.md | 689 +++++++++++++++ docs/zh-CN/skills/kotlin-patterns/SKILL.md | 714 +++++++++++++++ docs/zh-CN/skills/kotlin-testing/SKILL.md | 826 ++++++++++++++++++ .../logistics-exception-management/SKILL.md | 218 +++++ docs/zh-CN/skills/perl-patterns/SKILL.md | 504 +++++++++++ docs/zh-CN/skills/perl-security/SKILL.md | 503 +++++++++++ docs/zh-CN/skills/perl-testing/SKILL.md | 475 ++++++++++ .../skills/production-scheduling/SKILL.md | 230 +++++ docs/zh-CN/skills/prompt-optimizer/SKILL.md | 378 ++++++++ .../skills/quality-nonconformance/SKILL.md | 252 ++++++ .../skills/returns-reverse-logistics/SKILL.md | 225 +++++ docs/zh-CN/skills/skill-stocktake/SKILL.md | 21 +- docs/zh-CN/skills/strategic-compact/SKILL.md | 34 + docs/zh-CN/skills/video-editing/SKILL.md | 317 +++++++ docs/zh-CN/skills/videodb/SKILL.md | 386 ++++++++ .../skills/videodb/reference/api-reference.md | 550 ++++++++++++ .../videodb/reference/capture-reference.md | 416 +++++++++ .../zh-CN/skills/videodb/reference/capture.md | 104 +++ docs/zh-CN/skills/videodb/reference/editor.md | 443 ++++++++++ .../skills/videodb/reference/generative.md | 331 +++++++ .../videodb/reference/rtstream-reference.md | 567 ++++++++++++ .../skills/videodb/reference/rtstream.md | 59 ++ docs/zh-CN/skills/videodb/reference/search.md | 230 +++++ .../skills/videodb/reference/streaming.md | 406 +++++++++ .../skills/videodb/reference/use-cases.md | 142 +++ docs/zh-CN/skills/x-api/SKILL.md | 211 +++++ docs/zh-CN/the-longform-guide.md | 2 +- 88 files changed, 16872 insertions(+), 280 deletions(-) create mode 100644 docs/zh-CN/TROUBLESHOOTING.md create mode 100644 docs/zh-CN/agents/kotlin-build-resolver.md create mode 100644 docs/zh-CN/agents/kotlin-reviewer.md create mode 100644 docs/zh-CN/commands/aside.md create mode 100644 docs/zh-CN/commands/gradle-build.md create mode 100644 docs/zh-CN/commands/kotlin-build.md create mode 100644 docs/zh-CN/commands/kotlin-review.md create mode 100644 docs/zh-CN/commands/kotlin-test.md create mode 100644 docs/zh-CN/commands/prompt-optimize.md create mode 100644 docs/zh-CN/commands/resume-session.md create mode 100644 docs/zh-CN/commands/save-session.md create mode 100644 docs/zh-CN/rules/kotlin/coding-style.md create mode 100644 docs/zh-CN/rules/kotlin/hooks.md create mode 100644 docs/zh-CN/rules/kotlin/patterns.md create mode 100644 docs/zh-CN/rules/kotlin/security.md create mode 100644 docs/zh-CN/rules/kotlin/testing.md create mode 100644 docs/zh-CN/rules/perl/coding-style.md create mode 100644 docs/zh-CN/rules/perl/hooks.md create mode 100644 docs/zh-CN/rules/perl/patterns.md create mode 100644 docs/zh-CN/rules/perl/security.md create mode 100644 docs/zh-CN/rules/perl/testing.md create mode 100644 docs/zh-CN/rules/php/coding-style.md create mode 100644 docs/zh-CN/rules/php/hooks.md create mode 100644 docs/zh-CN/rules/php/patterns.md create mode 100644 docs/zh-CN/rules/php/security.md create mode 100644 docs/zh-CN/rules/php/testing.md create mode 100644 docs/zh-CN/skills/android-clean-architecture/SKILL.md create mode 100644 docs/zh-CN/skills/blueprint/SKILL.md create mode 100644 docs/zh-CN/skills/carrier-relationship-management/SKILL.md create mode 100644 docs/zh-CN/skills/claude-api/SKILL.md create mode 100644 docs/zh-CN/skills/compose-multiplatform-patterns/SKILL.md create mode 100644 docs/zh-CN/skills/crosspost/SKILL.md create mode 100644 docs/zh-CN/skills/customs-trade-compliance/SKILL.md create mode 100644 docs/zh-CN/skills/deep-research/SKILL.md create mode 100644 docs/zh-CN/skills/dmux-workflows/SKILL.md create mode 100644 docs/zh-CN/skills/energy-procurement/SKILL.md create mode 100644 docs/zh-CN/skills/exa-search/SKILL.md create mode 100644 docs/zh-CN/skills/fal-ai-media/SKILL.md create mode 100644 docs/zh-CN/skills/inventory-demand-planning/SKILL.md create mode 100644 docs/zh-CN/skills/kotlin-coroutines-flows/SKILL.md create mode 100644 docs/zh-CN/skills/kotlin-exposed-patterns/SKILL.md create mode 100644 docs/zh-CN/skills/kotlin-ktor-patterns/SKILL.md create mode 100644 docs/zh-CN/skills/kotlin-patterns/SKILL.md create mode 100644 docs/zh-CN/skills/kotlin-testing/SKILL.md create mode 100644 docs/zh-CN/skills/logistics-exception-management/SKILL.md create mode 100644 docs/zh-CN/skills/perl-patterns/SKILL.md create mode 100644 docs/zh-CN/skills/perl-security/SKILL.md create mode 100644 docs/zh-CN/skills/perl-testing/SKILL.md create mode 100644 docs/zh-CN/skills/production-scheduling/SKILL.md create mode 100644 docs/zh-CN/skills/prompt-optimizer/SKILL.md create mode 100644 docs/zh-CN/skills/quality-nonconformance/SKILL.md create mode 100644 docs/zh-CN/skills/returns-reverse-logistics/SKILL.md create mode 100644 docs/zh-CN/skills/video-editing/SKILL.md create mode 100644 docs/zh-CN/skills/videodb/SKILL.md create mode 100644 docs/zh-CN/skills/videodb/reference/api-reference.md create mode 100644 docs/zh-CN/skills/videodb/reference/capture-reference.md create mode 100644 docs/zh-CN/skills/videodb/reference/capture.md create mode 100644 docs/zh-CN/skills/videodb/reference/editor.md create mode 100644 docs/zh-CN/skills/videodb/reference/generative.md create mode 100644 docs/zh-CN/skills/videodb/reference/rtstream-reference.md create mode 100644 docs/zh-CN/skills/videodb/reference/rtstream.md create mode 100644 docs/zh-CN/skills/videodb/reference/search.md create mode 100644 docs/zh-CN/skills/videodb/reference/streaming.md create mode 100644 docs/zh-CN/skills/videodb/reference/use-cases.md create mode 100644 docs/zh-CN/skills/x-api/SKILL.md diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index c682b930..a69dd197 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 13 个专业智能体、50+ 项技能、33 条命令以及用于软件开发的自动化钩子工作流。 +这是一个**生产就绪的 AI 编码插件**,提供 16 个专业代理、65+ 项技能、40 条命令以及自动化钩子工作流,用于软件开发。 ## 核心原则 @@ -12,7 +12,7 @@ ## 可用智能体 -| 智能体 | 目的 | 何时使用 | +| 代理 | 用途 | 使用时机 | |-------|---------|-------------| | planner | 实施规划 | 复杂功能、重构 | | architect | 系统设计与可扩展性 | 架构决策 | @@ -21,12 +21,15 @@ | security-reviewer | 漏洞检测 | 提交前、敏感代码 | | build-error-resolver | 修复构建/类型错误 | 构建失败时 | | e2e-runner | 端到端 Playwright 测试 | 关键用户流程 | -| refactor-cleaner | 清理无用代码 | 代码维护 | -| doc-updater | 文档和代码地图更新 | 更新文档 | +| refactor-cleaner | 死代码清理 | 代码维护 | +| doc-updater | 文档与代码映射更新 | 更新文档时 | | go-reviewer | Go 代码审查 | Go 项目 | -| go-build-resolver | Go 构建错误 | Go 构建失败 | +| go-build-resolver | Go 构建错误 | Go 构建失败时 | | database-reviewer | PostgreSQL/Supabase 专家 | 模式设计、查询优化 | | python-reviewer | Python 代码审查 | Python 项目 | +| chief-of-staff | 沟通分流与草稿 | 多渠道电子邮件、Slack、LINE、Messenger | +| loop-operator | 自主循环执行 | 安全运行循环、监控停滞、干预 | +| harness-optimizer | 线束配置调优 | 可靠性、成本、吞吐量 | ## 智能体编排 @@ -37,6 +40,9 @@ * 错误修复或新功能 → **tdd-guide** * 架构决策 → **architect** * 安全敏感代码 → **security-reviewer** +* 多渠道沟通分流 → **chief-of-staff** +* 自主循环 / 循环监控 → **loop-operator** +* 线束配置可靠性及成本 → **harness-optimizer** 对于独立操作使用并行执行 — 同时启动多个智能体。 @@ -94,10 +100,15 @@ ## 开发工作流 -1. **规划** — 使用 planner 智能体,识别依赖项和风险,分解为阶段 -2. **TDD** — 使用 tdd-guide 智能体,先写测试,实现,重构 -3. **审查** — 立即使用 code-reviewer 智能体,解决 CRITICAL/HIGH 问题 -4. **提交** — 约定式提交格式,全面的 PR 摘要 +1. **规划** — 使用规划代理,识别依赖关系和风险,分阶段推进 +2. **测试驱动开发** — 使用 tdd-guide 代理,先写测试,再实现和重构 +3. **评审** — 立即使用代码评审代理,解决 CRITICAL/HIGH 级别的问题 +4. **在适当位置记录知识** + * 个人调试笔记、偏好和临时上下文 → 自动记忆 + * 团队/项目知识(架构决策、API 变更、操作手册)→ 项目现有文档结构 + * 如果当前任务已生成相关文档或代码注释,请勿在其他地方重复相同信息 + * 如果没有明显的项目文档位置,在创建新的顶层文件前先询问 +5. **提交** — 采用约定式提交格式,提供全面的 PR 摘要 ## Git 工作流 @@ -123,8 +134,8 @@ ``` agents/ — 13 specialized subagents -skills/ — 50+ workflow skills and domain knowledge -commands/ — 33 slash commands +skills/ — 65+ workflow skills and domain knowledge +commands/ — 40 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) scripts/ — Cross-platform Node.js utilities diff --git a/docs/zh-CN/CODE_OF_CONDUCT.md b/docs/zh-CN/CODE_OF_CONDUCT.md index f570d52e..77c5ffd0 100644 --- a/docs/zh-CN/CODE_OF_CONDUCT.md +++ b/docs/zh-CN/CODE_OF_CONDUCT.md @@ -71,14 +71,13 @@ ## 归属 -本《行为准则》改编自 [Contributor Covenant][homepage], -版本 2.0,可在 +本行为准则改编自 \[贡献者公约]\[homepage] 2.0 版本,可访问 获取。 社区影响指南的灵感来源于 [Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)。 [homepage]: https://www.contributor-covenant.org -有关本行为准则常见问题的解答,请参阅常见问题解答: -。翻译版本可在 - 获取。 +关于本行为准则的常见问题解答,请参阅 FAQ 页面: +。其他语言翻译版本可在 + 查阅。 diff --git a/docs/zh-CN/CONTRIBUTING.md b/docs/zh-CN/CONTRIBUTING.md index b8176662..6cc3a2aa 100644 --- a/docs/zh-CN/CONTRIBUTING.md +++ b/docs/zh-CN/CONTRIBUTING.md @@ -10,6 +10,7 @@ * [贡献智能体](#贡献智能体) * [贡献钩子](#贡献钩子) * [贡献命令](#贡献命令) +* [跨平台与翻译](#跨平台与翻译) * [拉取请求流程](#拉取请求流程) *** @@ -349,6 +350,29 @@ description: 在 /help 中显示的简要描述 *** +## 跨平台与翻译 + +### 技能子集 (Codex 和 Cursor) + +ECC 为其他平台提供了技能子集: + +* **Codex:** `.agents/skills/` — `agents/openai.yaml` 中列出的技能会被 Codex 加载。 +* **Cursor:** `.cursor/skills/` — 为 Cursor 打包了一个技能子集。 + +当您**添加一个新技能**,并且希望它在 Codex 或 Cursor 上可用时: + +1. 像往常一样,在 `skills/your-skill-name/` 下添加该技能。 +2. 如果它应该在 **Codex** 上可用,请将其添加到 `.agents/skills/`(复制技能目录或添加引用),并在需要时确保它在 `agents/openai.yaml` 中被引用。 +3. 如果它应该在 **Cursor** 上可用,请根据 Cursor 的布局,将其添加到 `.cursor/skills/` 下。 + +请参考这些目录中现有技能的结构。保持这些子集同步是手动操作;如果您更新了它们,请在您的 PR 中说明。 + +### 翻译 + +翻译文件位于 `docs/` 下(例如 `docs/zh-CN`、`docs/zh-TW`、`docs/ja-JP`)。如果您更改了已被翻译的智能体、命令或技能,请考虑更新相应的翻译文件,或创建一个问题,以便维护者或翻译人员可以更新它们。 + +*** + ## 拉取请求流程 ### 1. PR 标题格式 diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 478d8afc..b0297bdf 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -1,4 +1,4 @@ -**语言:** [English](../../README.md) | [繁體中文](../zh-TW/README.md) | [简体中文](README.md) +**语言:** English | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) # Everything Claude Code @@ -14,9 +14,10 @@ ![Python](https://img.shields.io/badge/-Python-3776AB?logo=python\&logoColor=white) ![Go](https://img.shields.io/badge/-Go-00ADD8?logo=go\&logoColor=white) ![Java](https://img.shields.io/badge/-Java-ED8B00?logo=openjdk\&logoColor=white) +![Perl](https://img.shields.io/badge/-Perl-39457E?logo=perl\&logoColor=white) ![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown\&logoColor=white) -> **5万+ stars** | **6千+ forks** | **30位贡献者** | **支持6种语言** | **Anthropic黑客马拉松获胜者** +> **50K+ stars** | **6K+ forks** | **30 contributors** | **5 languages supported** | **Anthropic Hackathon Winner** *** @@ -24,7 +25,7 @@ **🌐 语言 / 语言 / 語言** -[**English**](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) +[**English**](../../README.md) | [简体中文](../../README.zh-CN.md) | [繁體中文](../zh-TW/README.md) | [日本語](../ja-JP/README.md) | [한국어](../ko-KR/README.md) @@ -38,24 +39,6 @@ *** -## 采用与分发 - -向赞助商、平台或生态系统合作伙伴展示 ECC 时,请使用这些实时信号: - -* **主包安装量:** npm 上的 [`ecc-universal`](https://www.npmjs.com/package/ecc-universal) -* **安全伴侣安装量:** npm 上的 [`ecc-agentshield`](https://www.npmjs.com/package/ecc-agentshield) -* **GitHub 应用分发:** [ECC 工具市场列表](https://github.com/marketplace/ecc-tools) -* **自动化月度指标问题:** 由 `.github/workflows/monthly-metrics.yml` 驱动 -* **仓库采用信号:** 本 README 顶部的 stars/forks/contributors 徽章 - -Claude Code 插件安装的下载计数目前尚未作为公共 API 公开。对于合作伙伴报告,请将 npm 指标与 GitHub 应用安装量以及仓库流量/分支增长相结合。 - -有关赞助商通话的指标清单和命令片段,请参阅 [`docs/business/metrics-and-sponsorship.md`](../business/metrics-and-sponsorship.md)。 - -[**赞助 ECC**](https://github.com/sponsors/affaan-m) | [赞助层级](SPONSORS.md) | [赞助计划](SPONSORING.md) - -*** - ## 指南 此仓库仅包含原始代码。指南解释了一切。 @@ -64,18 +47,18 @@ Claude Code 插件安装的下载计数目前尚未作为公共 API 公开。对 -The Shorthand Guide to Everything Claude Code +Claude Code 的速记指南/>
 </a>
 </td>
 <td width= -The Longform Guide to Everything Claude Code +Claude Code 的详细指南 -Shorthand Guide
Setup, foundations, philosophy. Read this first. -Longform Guide
Token optimization, memory persistence, evals, parallelization. +Shorthand Guide
设置、基础、理念。 先阅读此部分。 +详细指南
令牌优化、记忆持久化、评估、并行化。 @@ -173,9 +156,9 @@ git clone https://github.com/affaan-m/everything-claude-code.git cd everything-claude-code # Recommended: use the installer (handles common + language rules safely) -./install.sh typescript # or python or golang +./install.sh typescript # or python or golang or swift or php # You can pass multiple languages: -# ./install.sh typescript python golang +# ./install.sh typescript python golang swift php # or target cursor: # ./install.sh --target cursor typescript # or target antigravity: @@ -258,137 +241,143 @@ everything-claude-code/ | |-- plugin.json # 插件元数据和组件路径 | |-- marketplace.json # 用于 /plugin marketplace add 的市场目录 | -|-- agents/ # 用于委派任务的专用子代理 +|-- agents/ # 用于委托任务的专用子代理 | |-- planner.md # 功能实现规划 -| |-- architect.md # 系统设计决策 +| |-- architect.md # 系统架构设计决策 | |-- tdd-guide.md # 测试驱动开发 -| |-- code-reviewer.md # 质量和安全审查 +| |-- code-reviewer.md # 质量与安全代码审查 | |-- security-reviewer.md # 漏洞分析 | |-- build-error-resolver.md -| |-- e2e-runner.md # Playwright E2E 测试 +| |-- e2e-runner.md # Playwright 端到端测试 | |-- refactor-cleaner.md # 无用代码清理 | |-- doc-updater.md # 文档同步 | |-- go-reviewer.md # Go 代码审查 | |-- go-build-resolver.md # Go 构建错误修复 -| |-- python-reviewer.md # Python 代码审查 (新增) -| |-- database-reviewer.md # 数据库 / Supabase 审查 (新增) +| |-- python-reviewer.md # Python 代码审查(新增) +| |-- database-reviewer.md # 数据库/Supabase 审查(新增) | -|-- skills/ # 工作流定义和领域知识 -| |-- coding-standards/ # 各语言最佳实践 -| |-- clickhouse-io/ # ClickHouse 分析、查询和数据工程 -| |-- backend-patterns/ # API、数据库、缓存模式 +|-- skills/ # 工作流定义与领域知识 +| |-- coding-standards/ # 语言最佳实践 +| |-- clickhouse-io/ # ClickHouse 分析、查询与数据工程 +| |-- backend-patterns/ # API、数据库与缓存模式 | |-- frontend-patterns/ # React、Next.js 模式 -| |-- frontend-slides/ # HTML 幻灯片和 PPTX 转 Web 演示流程 (新增) -| |-- article-writing/ # 使用指定风格进行长文写作,避免通用 AI 语气 (新增) -| |-- content-engine/ # 多平台内容生成与复用工作流 (新增) -| |-- market-research/ # 带来源引用的市场、竞品和投资研究 (新增) -| |-- investor-materials/ # 融资演示文稿、单页、备忘录和财务模型 (新增) -| |-- investor-outreach/ # 个性化融资外联与跟进 (新增) -| |-- continuous-learning/ # 从会话中自动提取模式 (Longform Guide) +| |-- frontend-slides/ # HTML 幻灯片和 PPTX 转 Web 演示工作流(新增) +| |-- article-writing/ # 按指定写作风格撰写长文而不使用通用 AI 语气(新增) +| |-- content-engine/ # 多平台内容生成与内容复用工作流(新增) +| |-- market-research/ # 带来源引用的市场、竞品与投资人研究(新增) +| |-- investor-materials/ # 融资演示文稿、单页材料、备忘录与财务模型(新增) +| |-- investor-outreach/ # 个性化融资沟通与跟进(新增) +| |-- continuous-learning/ # 从会话中自动提取模式(长文指南) | |-- continuous-learning-v2/ # 基于直觉的学习与置信度评分 -| |-- iterative-retrieval/ # 子代理的渐进式上下文优化 -| |-- strategic-compact/ # 手动压缩建议 (Longform Guide) +| |-- iterative-retrieval/ # 子代理渐进式上下文优化 +| |-- strategic-compact/ # 手动压缩建议(长文指南) | |-- tdd-workflow/ # TDD 方法论 | |-- security-review/ # 安全检查清单 -| |-- eval-harness/ # 验证循环评估 (Longform Guide) -| |-- verification-loop/ # 持续验证 (Longform Guide) -| |-- golang-patterns/ # Go 语言惯用法和最佳实践 -| |-- golang-testing/ # Go 测试模式、TDD、基准测试 -| |-- cpp-coding-standards/ # 来自 C++ Core Guidelines 的 C++ 编码规范 (新增) -| |-- cpp-testing/ # 使用 GoogleTest、CMake/CTest 的 C++ 测试 (新增) -| |-- django-patterns/ # Django 模式、模型和视图 (新增) -| |-- django-security/ # Django 安全最佳实践 (新增) -| |-- django-tdd/ # Django TDD 工作流 (新增) -| |-- django-verification/ # Django 验证循环 (新增) -| |-- python-patterns/ # Python 惯用法和最佳实践 (新增) -| |-- python-testing/ # 使用 pytest 的 Python 测试 (新增) -| |-- springboot-patterns/ # Java Spring Boot 模式 (新增) -| |-- springboot-security/ # Spring Boot 安全 (新增) -| |-- springboot-tdd/ # Spring Boot TDD (新增) -| |-- springboot-verification/ # Spring Boot 验证流程 (新增) -| |-- configure-ecc/ # 交互式安装向导 (新增) -| |-- security-scan/ # AgentShield 安全审计集成 (新增) -| |-- java-coding-standards/ # Java 编码规范 (新增) -| |-- jpa-patterns/ # JPA/Hibernate 模式 (新增) -| |-- postgres-patterns/ # PostgreSQL 优化模式 (新增) -| |-- nutrient-document-processing/ # 使用 Nutrient API 进行文档处理 (新增) +| |-- eval-harness/ # 验证循环评估(长文指南) +| |-- verification-loop/ # 持续验证(长文指南) +| |-- videodb/ # 视频和音频:导入、搜索、编辑、生成与流式处理(新增) +| |-- golang-patterns/ # Go 习惯用法与最佳实践 +| |-- golang-testing/ # Go 测试模式、TDD 与基准测试 +| |-- cpp-coding-standards/ # 来自 C++ Core Guidelines 的 C++ 编码规范(新增) +| |-- cpp-testing/ # 使用 GoogleTest 与 CMake/CTest 的 C++ 测试(新增) +| |-- django-patterns/ # Django 模式、模型与视图(新增) +| |-- django-security/ # Django 安全最佳实践(新增) +| |-- django-tdd/ # Django TDD 工作流(新增) +| |-- django-verification/ # Django 验证循环(新增) +| |-- python-patterns/ # Python 习惯用法与最佳实践(新增) +| |-- python-testing/ # 使用 pytest 的 Python 测试(新增) +| |-- springboot-patterns/ # Java Spring Boot 模式(新增) +| |-- springboot-security/ # Spring Boot 安全(新增) +| |-- springboot-tdd/ # Spring Boot TDD(新增) +| |-- springboot-verification/ # Spring Boot 验证(新增) +| |-- configure-ecc/ # 交互式安装向导(新增) +| |-- security-scan/ # AgentShield 安全审计集成(新增) +| |-- java-coding-standards/ # Java 编码规范(新增) +| |-- jpa-patterns/ # JPA/Hibernate 模式(新增) +| |-- postgres-patterns/ # PostgreSQL 优化模式(新增) +| |-- nutrient-document-processing/ # 使用 Nutrient API 的文档处理(新增) | |-- project-guidelines-example/ # 项目专用技能模板 -| |-- database-migrations/ # 数据库迁移模式 (Prisma、Drizzle、Django、Go) (新增) -| |-- api-design/ # REST API 设计、分页和错误响应 (新增) -| |-- deployment-patterns/ # CI/CD、Docker、健康检查和回滚 (新增) -| |-- docker-patterns/ # Docker Compose、网络、卷和容器安全 (新增) -| |-- e2e-testing/ # Playwright E2E 模式和 Page Object Model (新增) -| |-- content-hash-cache-pattern/ # 使用 SHA-256 内容哈希进行文件处理缓存 (新增) -| |-- cost-aware-llm-pipeline/ # LLM 成本优化、模型路由和预算跟踪 (新增) -| |-- regex-vs-llm-structured-text/ # 文本解析决策框架:正则 vs LLM (新增) -| |-- swift-actor-persistence/ # 使用 Actor 的线程安全 Swift 数据持久化 (新增) -| |-- swift-protocol-di-testing/ # 基于 Protocol 的依赖注入用于可测试 Swift 代码 (新增) -| |-- search-first/ # 先研究再编码的工作流 (新增) -| |-- skill-stocktake/ # 审计技能和命令质量 (新增) -| |-- liquid-glass-design/ # iOS 26 Liquid Glass 设计系统 (新增) -| |-- foundation-models-on-device/ # Apple 设备端 LLM FoundationModels (新增) -| |-- swift-concurrency-6-2/ # Swift 6.2 易用并发模型 (新增) -| |-- autonomous-loops/ # 自动化循环模式:顺序流水线、PR 循环、DAG 编排 (新增) -| |-- plankton-code-quality/ # 使用 Plankton hooks 在编写阶段执行代码质量检查 (新增) +| |-- database-migrations/ # 迁移模式(Prisma、Drizzle、Django、Go)(新增) +| |-- api-design/ # REST API 设计、分页与错误响应(新增) +| |-- deployment-patterns/ # CI/CD、Docker、健康检查与回滚(新增) +| |-- docker-patterns/ # Docker Compose、网络、卷与容器安全(新增) +| |-- e2e-testing/ # Playwright 端到端模式与页面对象模型(新增) +| |-- content-hash-cache-pattern/ # 文件处理中的 SHA-256 内容哈希缓存模式(新增) +| |-- cost-aware-llm-pipeline/ # LLM 成本优化、模型路由与预算追踪(新增) +| |-- regex-vs-llm-structured-text/ # 文本解析决策框架:regex vs LLM(新增) +| |-- swift-actor-persistence/ # 使用 Actor 的线程安全 Swift 数据持久化(新增) +| |-- swift-protocol-di-testing/ # 基于 Protocol 的依赖注入用于可测试 Swift 代码(新增) +| |-- search-first/ # 先研究再编码的工作流(新增) +| |-- skill-stocktake/ # 审计技能和命令质量(新增) +| |-- liquid-glass-design/ # iOS 26 Liquid Glass 设计系统(新增) +| |-- foundation-models-on-device/ # Apple 设备端 LLM(FoundationModels)(新增) +| |-- swift-concurrency-6-2/ # Swift 6.2 易用并发(新增) +| |-- perl-patterns/ # 现代 Perl 5.36+ 习惯用法与最佳实践(新增) +| |-- perl-security/ # Perl 安全模式、taint 模式与安全 I/O(新增) +| |-- perl-testing/ # 使用 Test2::V0、prove、Devel::Cover 的 Perl TDD(新增) +| |-- autonomous-loops/ # 自主循环模式:顺序流水线、PR 循环与 DAG 编排(新增) +| |-- plankton-code-quality/ # 使用 Plankton hooks 的编写阶段代码质量控制(新增) | -|-- commands/ # 用于快速执行的 Slash 命令 +|-- commands/ # 快速执行的斜杠命令 | |-- tdd.md # /tdd - 测试驱动开发 | |-- plan.md # /plan - 实现规划 -| |-- e2e.md # /e2e - E2E 测试生成 -| |-- code-review.md # /code-review - 代码质量审查 +| |-- e2e.md # /e2e - 端到端测试生成 +| |-- code-review.md # /code-review - 质量审查 | |-- build-fix.md # /build-fix - 修复构建错误 -| |-- refactor-clean.md # /refactor-clean - 删除无用代码 -| |-- learn.md # /learn - 在会话中提取模式 (Longform Guide) -| |-- learn-eval.md # /learn-eval - 提取、评估并保存模式 (新增) -| |-- checkpoint.md # /checkpoint - 保存验证状态 (Longform Guide) -| |-- verify.md # /verify - 运行验证循环 (Longform Guide) +| |-- refactor-clean.md # /refactor-clean - 无用代码清理 +| |-- learn.md # /learn - 会话中提取模式(长文指南) +| |-- learn-eval.md # /learn-eval - 提取、评估并保存模式(新增) +| |-- checkpoint.md # /checkpoint - 保存验证状态(长文指南) +| |-- verify.md # /verify - 运行验证循环(长文指南) | |-- setup-pm.md # /setup-pm - 配置包管理器 -| |-- go-review.md # /go-review - Go 代码审查 (新增) -| |-- go-test.md # /go-test - Go TDD 工作流 (新增) -| |-- go-build.md # /go-build - 修复 Go 构建错误 (新增) -| |-- skill-create.md # /skill-create - 从 git 历史生成技能 (新增) -| |-- instinct-status.md # /instinct-status - 查看学习到的直觉规则 (新增) -| |-- instinct-import.md # /instinct-import - 导入直觉规则 (新增) -| |-- instinct-export.md # /instinct-export - 导出直觉规则 (新增) +| |-- go-review.md # /go-review - Go 代码审查(新增) +| |-- go-test.md # /go-test - Go TDD 工作流(新增) +| |-- go-build.md # /go-build - 修复 Go 构建错误(新增) +| |-- skill-create.md # /skill-create - 从 git 历史生成技能(新增) +| |-- instinct-status.md # /instinct-status - 查看学习到的直觉(新增) +| |-- instinct-import.md # /instinct-import - 导入直觉(新增) +| |-- instinct-export.md # /instinct-export - 导出直觉(新增) | |-- evolve.md # /evolve - 将直觉聚类为技能 -| |-- pm2.md # /pm2 - PM2 服务生命周期管理 (新增) -| |-- multi-plan.md # /multi-plan - 多代理任务拆解 (新增) -| |-- multi-execute.md # /multi-execute - 编排式多代理工作流 (新增) -| |-- multi-backend.md # /multi-backend - 后端多服务编排 (新增) -| |-- multi-frontend.md # /multi-frontend - 前端多服务编排 (新增) -| |-- multi-workflow.md # /multi-workflow - 通用多服务工作流 (新增) +| |-- pm2.md # /pm2 - PM2 服务生命周期管理(新增) +| |-- multi-plan.md # /multi-plan - 多代理任务拆解(新增) +| |-- multi-execute.md # /multi-execute - 编排的多代理工作流(新增) +| |-- multi-backend.md # /multi-backend - 后端多服务编排(新增) +| |-- multi-frontend.md # /multi-frontend - 前端多服务编排(新增) +| |-- multi-workflow.md # /multi-workflow - 通用多服务工作流(新增) | |-- orchestrate.md # /orchestrate - 多代理协调 | |-- sessions.md # /sessions - 会话历史管理 -| |-- eval.md # /eval - 按标准进行评估 +| |-- eval.md # /eval - 按标准评估 | |-- test-coverage.md # /test-coverage - 测试覆盖率分析 | |-- update-docs.md # /update-docs - 更新文档 -| |-- update-codemaps.md # /update-codemaps - 更新代码地图 -| |-- python-review.md # /python-review - Python 代码审查 (新增) +| |-- update-codemaps.md # /update-codemaps - 更新代码映射 +| |-- python-review.md # /python-review - Python 代码审查(新增) | -|-- rules/ # 必须遵循的规则 (复制到 ~/.claude/rules/) -| |-- README.md # 结构概览和安装指南 +|-- rules/ # 必须遵循的规则(复制到 ~/.claude/rules/) +| |-- README.md # 结构说明与安装指南 | |-- common/ # 与语言无关的原则 -| | |-- coding-style.md # 不可变性、文件组织 -| | |-- git-workflow.md # 提交格式、PR 流程 -| | |-- testing.md # TDD、80% 覆盖率要求 -| | |-- performance.md # 模型选择、上下文管理 -| | |-- patterns.md # 设计模式、骨架项目 -| | |-- hooks.md # Hook 架构、TodoWrite -| | |-- agents.md # 何时委派给子代理 -| | |-- security.md # 必须执行的安全检查 -| |-- typescript/ # TypeScript / JavaScript 专用 +| | |-- coding-style.md # 不可变性与文件组织 +| | |-- git-workflow.md # 提交格式与 PR 流程 +| | |-- testing.md # TDD 与 80% 覆盖率要求 +| | |-- performance.md # 模型选择与上下文管理 +| | |-- patterns.md # 设计模式与骨架项目 +| | |-- hooks.md # Hook 架构与 TodoWrite +| | |-- agents.md # 何时委托给子代理 +| | |-- security.md # 强制安全检查 +| |-- typescript/ # TypeScript/JavaScript 专用 | |-- python/ # Python 专用 | |-- golang/ # Go 专用 +| |-- swift/ # Swift 专用 +| |-- php/ # PHP 专用(新增) | |-- hooks/ # 基于触发器的自动化 -| |-- README.md # Hook 文档、示例和自定义指南 -| |-- hooks.json # 所有 Hook 配置 (PreToolUse、PostToolUse、Stop 等) -| |-- memory-persistence/ # 会话生命周期 Hook (Longform Guide) -| |-- strategic-compact/ # 压缩建议 (Longform Guide) +| |-- README.md # Hook 文档、示例与自定义指南 +| |-- hooks.json # 所有 Hook 配置(PreToolUse、PostToolUse、Stop 等) +| |-- memory-persistence/ # 会话生命周期 Hook(长文指南) +| |-- strategic-compact/ # 压缩建议(长文指南) | -|-- scripts/ # 跨平台 Node.js 脚本 (新增) -| |-- lib/ # 共享工具 -| | |-- utils.js # 跨平台文件 / 路径 / 系统工具 +|-- scripts/ # 跨平台 Node.js 脚本(新增) +| |-- lib/ # 公共工具 +| | |-- utils.js # 跨平台文件/路径/系统工具 | | |-- package-manager.js # 包管理器检测与选择 | |-- hooks/ # Hook 实现 | | |-- session-start.js # 会话开始时加载上下文 @@ -398,28 +387,28 @@ everything-claude-code/ | | |-- evaluate-session.js # 从会话中提取模式 | |-- setup-package-manager.js # 交互式包管理器设置 | -|-- tests/ # 测试套件 (新增) +|-- tests/ # 测试套件(新增) | |-- lib/ # 库测试 | |-- hooks/ # Hook 测试 | |-- run-all.js # 运行所有测试 | -|-- contexts/ # 动态系统提示上下文注入 (Longform Guide) +|-- contexts/ # 动态系统提示上下文(长文指南) | |-- dev.md # 开发模式上下文 | |-- review.md # 代码审查模式上下文 -| |-- research.md # 研究 / 探索模式上下文 +| |-- research.md # 研究/探索模式上下文 | -|-- examples/ # 示例配置和会话 +|-- examples/ # 示例配置与会话 | |-- CLAUDE.md # 项目级配置示例 | |-- user-CLAUDE.md # 用户级配置示例 -| |-- saas-nextjs-CLAUDE.md # 真实 SaaS 示例 (Next.js + Supabase + Stripe) -| |-- go-microservice-CLAUDE.md # 真实 Go 微服务示例 (gRPC + PostgreSQL) -| |-- django-api-CLAUDE.md # 真实 Django REST API 示例 (DRF + Celery) -| |-- rust-api-CLAUDE.md # 真实 Rust API 示例 (Axum + SQLx + PostgreSQL) (新增) +| |-- saas-nextjs-CLAUDE.md # 实际 SaaS 示例(Next.js + Supabase + Stripe) +| |-- go-microservice-CLAUDE.md # 实际 Go 微服务示例(gRPC + PostgreSQL) +| |-- django-api-CLAUDE.md # 实际 Django REST API 示例(DRF + Celery) +| |-- rust-api-CLAUDE.md # 实际 Rust API 示例(Axum + SQLx + PostgreSQL)(新增) | |-- mcp-configs/ # MCP 服务器配置 | |-- mcp-servers.json # GitHub、Supabase、Vercel、Railway 等 | -|-- marketplace.json # 自托管市场配置 (用于 /plugin marketplace add) +|-- marketplace.json # 自托管市场配置(用于 /plugin marketplace add) ``` *** @@ -571,23 +560,24 @@ Duplicate hooks file detected: ./hooks/hooks.json resolves to already-loaded fil 这将使您能够立即访问所有命令、代理、技能和钩子。 -> **注意:** Claude Code 插件系统不支持通过插件分发 `rules`([上游限制](https://code.claude.com/docs/en/plugins-reference))。你需要手动安装规则: +> **注意:** Claude Code 插件系统不支持通过插件分发 `rules` ([上游限制](https://code.claude.com/docs/en/plugins-reference))。您需要手动安装规则: > > ```bash > # 首先克隆仓库 > git clone https://github.com/affaan-m/everything-claude-code.git > -> # 选项 A:用户级规则(应用于所有项目) +> # 选项 A:用户级规则(适用于所有项目) > mkdir -p ~/.claude/rules > cp -r everything-claude-code/rules/common/* ~/.claude/rules/ -> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择你的技术栈 +> cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # 选择您的技术栈 > cp -r everything-claude-code/rules/python/* ~/.claude/rules/ > cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ +> cp -r everything-claude-code/rules/php/* ~/.claude/rules/ > -> # 选项 B:项目级规则(仅应用于当前项目) +> # 选项 B:项目级规则(仅适用于当前项目) > mkdir -p .claude/rules > cp -r everything-claude-code/rules/common/* .claude/rules/ -> cp -r everything-claude-code/rules/typescript/* .claude/rules/ # 选择你的技术栈 +> cp -r everything-claude-code/rules/typescript/* .claude/rules/ # 选择您的技术栈 > ``` *** @@ -608,6 +598,7 @@ cp -r everything-claude-code/rules/common/* ~/.claude/rules/ cp -r everything-claude-code/rules/typescript/* ~/.claude/rules/ # pick your stack cp -r everything-claude-code/rules/python/* ~/.claude/rules/ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/ +cp -r everything-claude-code/rules/php/* ~/.claude/rules/ # Copy commands cp everything-claude-code/commands/*.md ~/.claude/commands/ @@ -691,6 +682,8 @@ rules/ typescript/ # TS/JS specific patterns and tools python/ # Python specific patterns and tools golang/ # Go specific patterns and tools + swift/ # Swift specific patterns and tools + php/ # PHP specific patterns and tools ``` 有关安装和结构详情,请参阅 [`rules/README.md`](rules/README.md)。 @@ -748,7 +741,7 @@ rules/ ## ❓ 常见问题
-How do I check which agents/commands are installed? +如何检查已安装的代理/命令? ```bash /plugin list everything-claude-code@everything-claude-code @@ -759,14 +752,40 @@ rules/
-My hooks aren't working / I see "Duplicate hooks file" errors +我的钩子不工作 / 我看到“重复钩子文件”错误 这是最常见的问题。**不要在 `.claude-plugin/plugin.json` 中添加 `"hooks"` 字段。** Claude Code v2.1+ 会自动从已安装的插件加载 `hooks/hooks.json`。显式声明它会导致重复检测错误。参见 [#29](https://github.com/affaan-m/everything-claude-code/issues/29), [#52](https://github.com/affaan-m/everything-claude-code/issues/52), [#103](https://github.com/affaan-m/everything-claude-code/issues/103)。
-My context window is shrinking / Claude is running out of context +我能否在自定义API端点或模型网关上使用ECC与Claude Code? + +是的。ECC 不会硬编码 Anthropic 托管的传输设置。它通过 Claude Code 正常的 CLI/插件接口在本地运行,因此可以与以下系统配合工作: + +* Anthropic 托管的 Claude Code +* 使用 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 的官方 Claude Code 网关设置 +* 兼容的自定义端点,这些端点能理解 Anthropic API 并符合 Claude Code 的预期 + +最小示例: + +```bash +export ANTHROPIC_BASE_URL=https://your-gateway.example.com +export ANTHROPIC_AUTH_TOKEN=your-token +claude +``` + +如果您的网关重新映射模型名称,请在 Claude Code 中配置,而不是在 ECC 中。一旦 `claude` CLI 已经正常工作,ECC 的钩子、技能、命令和规则就与模型提供商无关。 + +官方参考资料: + +* [Claude Code LLM 网关文档](https://docs.anthropic.com/en/docs/claude-code/llm-gateway) +* [Claude Code 模型配置文档](https://docs.anthropic.com/en/docs/claude-code/model-config) + +
+ +
+我的上下文窗口正在缩小 / Claude 即将耗尽上下文 太多的 MCP 服务器会消耗你的上下文。每个 MCP 工具描述都会消耗你 200k 窗口的令牌,可能将其减少到约 70k。 @@ -784,7 +803,7 @@ rules/
-Can I use only some components (e.g., just agents)? +我可以只使用某些组件(例如,仅代理)吗? 是的。使用选项 2(手动安装)并仅复制你需要的部分: @@ -801,7 +820,7 @@ cp -r everything-claude-code/rules/common/* ~/.claude/rules/
-Does this work with Cursor / OpenCode / Codex / Antigravity? +这能与 Cursor / OpenCode / Codex / Antigravity 一起使用吗? 是的。ECC 是跨平台的: @@ -814,7 +833,7 @@ cp -r everything-claude-code/rules/common/* ~/.claude/rules/
-How do I contribute a new skill or agent? +我如何贡献新技能或代理? 参见 [CONTRIBUTING.md](CONTRIBUTING.md)。简短版本: @@ -858,11 +877,11 @@ node tests/hooks/hooks.test.js ### 贡献想法 -* 特定语言技能 (Rust, C#, Swift, Kotlin) — Go, Python, Java 已包含 -* 特定框架配置 (Rails, Laravel, FastAPI, NestJS) — Django, Spring Boot 已包含 -* DevOps 智能体 (Kubernetes, Terraform, AWS, Docker) -* 测试策略 (不同框架,视觉回归) -* 领域特定知识 (ML, 数据工程, 移动端) +* 特定语言技能(Rust, C#, Kotlin, Java)—— Go, Python, Perl, Swift 和 TypeScript 已包含在内 +* 特定框架配置(Rails, Laravel, FastAPI, NestJS)—— Django, Spring Boot 已包含在内 +* DevOps 代理(Kubernetes, Terraform, AWS, Docker) +* 测试策略(不同框架,视觉回归) +* 特定领域知识(ML,数据工程,移动端) *** @@ -875,18 +894,18 @@ ECC 提供**完整的 Cursor IDE 支持**,包括为 Cursor 原生格式适配 ```bash # Install for your language(s) ./install.sh --target cursor typescript -./install.sh --target cursor python golang swift +./install.sh --target cursor python golang swift php ``` ### 包含内容 | 组件 | 数量 | 详情 | |-----------|-------|---------| -| 钩子事件 | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt, 以及另外 10 个 | -| 钩子脚本 | 16 | 通过共享适配器委托给 `scripts/hooks/` 的轻量 Node.js 脚本 | -| 规则 | 29 | 9 条通用规则 (alwaysApply) + 20 条语言特定规则 (TypeScript, Python, Go, Swift) | -| 代理 | 共享 | 通过根目录下的 AGENTS.md(被 Cursor 原生读取) | -| 技能 | 共享 + 捆绑 | 通过根目录下的 AGENTS.md 和用于翻译补充的 `.cursor/skills/` | +| 钩子事件 | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt 等 10 多个 | +| 钩子脚本 | 16 | 通过共享适配器委托给 `scripts/hooks/` 的精简 Node.js 脚本 | +| 规则 | 34 | 9 个通用规则(alwaysApply)+ 25 个语言特定规则(TypeScript, Python, Go, Swift, PHP) | +| 代理 | 共享 | 通过根目录下的 AGENTS.md(由 Cursor 原生读取) | +| 技能 | 共享 + 捆绑 | 通过根目录下的 AGENTS.md 和 `.cursor/skills/` 用于翻译后的补充内容 | | 命令 | 共享 | `.cursor/commands/`(如果已安装) | | MCP 配置 | 共享 | `.cursor/mcp.json`(如果已安装) | @@ -928,29 +947,31 @@ ECC 为 macOS 应用和 CLI 提供 **一流的 Codex 支持**,包括参考配 ### 快速开始(Codex 应用 + CLI) ```bash -# Copy the reference config to your home directory -cp .codex/config.toml ~/.codex/config.toml - -# Run Codex CLI in the repo — AGENTS.md is auto-detected +# Run Codex CLI in the repo — AGENTS.md and .codex/ are auto-detected codex + +# Optional: copy the global-safe defaults to your home directory +cp .codex/config.toml ~/.codex/config.toml ``` Codex macOS 应用: -* 将此仓库作为您的工作区打开。 -* 根目录的 `AGENTS.md` 会被自动检测。 -* 参考 `.codex/config.toml` 故意不固定 `model` 或 `model_provider`,因此 Codex 会使用它自己的当前默认值,除非您显式覆盖。 -* 可选:将 `.codex/config.toml` 复制到 `~/.codex/config.toml` 以实现 CLI/应用行为一致性。 +* 将此仓库作为您的工作空间打开。 +* 根目录 `AGENTS.md` 会自动检测。 +* `.codex/config.toml` 和 `.codex/agents/*.toml` 在保持项目本地时效果最佳。 +* 参考文件 `.codex/config.toml` 有意未固定 `model` 或 `model_provider`,因此除非您手动覆盖,Codex 将使用其自身的当前默认版本。 +* 可选:将 `.codex/config.toml` 复制到 `~/.codex/config.toml` 以设置全局默认值;除非您也复制 `.codex/agents/`,否则请将多智能体角色文件保留在项目本地。 ### 包含内容 | 组件 | 数量 | 详情 | |-----------|-------|---------| -| 配置 | 1 | `.codex/config.toml` — 权限、MCP 服务器、通知和配置文件 | +| 配置 | 1 | `.codex/config.toml` —— 顶级 approvals/sandbox/web\_search, MCP 服务器,通知,配置文件 | | AGENTS.md | 2 | 根目录(通用)+ `.codex/AGENTS.md`(Codex 特定补充) | -| 技能 | 16 | `.agents/skills/` — 每个技能包含 SKILL.md + agents/openai.yaml | -| MCP 服务器 | 4 | GitHub、Context7、Memory、Sequential Thinking(基于命令) | +| 技能 | 16 | `.agents/skills/` —— SKILL.md + agents/openai.yaml 每个技能 | +| MCP 服务器 | 4 | GitHub, Context7, Memory, Sequential Thinking(基于命令) | | 配置文件 | 2 | `strict`(只读沙箱)和 `yolo`(完全自动批准) | +| 代理角色 | 3 | `.codex/agents/` —— explorer, reviewer, docs-researcher | ### 技能 @@ -977,7 +998,24 @@ Codex macOS 应用: ### 关键限制 -Codex **尚未提供 Claude 风格的钩子执行对等性**。ECC 在该平台上的强制执行是通过 `AGENTS.md` 和 `persistent_instructions` 基于指令实现的,外加沙箱权限。 +Codex **尚未提供与 Claude 风格同等的钩子执行功能**。ECC 在该平台上的强制执行是通过 `AGENTS.md`、可选的 `model_instructions_file` 覆盖以及沙箱/批准设置以指令方式实现的。 + +### 多代理支持 + +当前的 Codex 版本支持实验性的多代理工作流。 + +* 在 `.codex/config.toml` 中启用 `features.multi_agent = true` +* 在 `[agents.]` 下定义角色 +* 将每个角色指向 `.codex/agents/` 下的一个文件 +* 在 CLI 中使用 `/agent` 来检查或引导子代理 + +ECC 附带了三个示例角色配置: + +| 角色 | 目的 | +|------|---------| +| `explorer` | 在进行编辑前进行只读的代码库证据收集 | +| `reviewer` | 正确性、安全性和缺失测试的审查 | +| `docs_researcher` | 在发布/文档更改前进行文档和 API 验证 | *** @@ -1090,6 +1128,14 @@ npm install ecc-universal } ``` +该 npm 插件条目启用了 ECC 发布的 OpenCode 插件模块(钩子/事件和插件工具)。 +它**不会**自动将 ECC 的完整命令/代理/指令目录添加到您的项目配置中。 + +要获得完整的 ECC OpenCode 设置,您可以: + +* 在此仓库内运行 OpenCode,或者 +* 将捆绑的 `.opencode/` 配置资源复制到您的项目中,并在 `opencode.json` 中连接 `instructions`、`agent` 和 `command` 条目 + ### 文档 * **迁移指南**:`.opencode/MIGRATION.md` @@ -1105,26 +1151,26 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 | 功能 | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **智能体** | 16 | 共享(AGENTS.md) | 共享(AGENTS.md) | 12 | +| **代理** | 16 | 共享(AGENTS.md) | 共享(AGENTS.md) | 12 | | **命令** | 40 | 共享 | 基于指令 | 31 | | **技能** | 65 | 共享 | 10(原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | -| **钩子脚本** | 20+ 个脚本 | 16 个脚本(DRY 适配器) | 不适用 | 插件钩子 | -| **规则** | 29(通用 + 语言) | 29(YAML 前言) | 基于指令 | 13 条指令 | -| **自定义工具** | 通过钩子 | 通过钩子 | 不适用 | 6 个原生工具 | -| **MCP 服务器** | 14 | 共享(mcp.json) | 4(基于命令) | 完整 | +| **钩子脚本** | 20+ 脚本 | 16 个脚本(DRY 适配器) | N/A | 插件钩子 | +| **规则** | 34(通用 + 语言) | 34(YAML 前言) | 基于指令 | 13 条指令 | +| **自定义工具** | 通过钩子 | 通过钩子 | N/A | 6 个原生工具 | +| **MCP 服务器** | 14 | 共享(mcp.json) | 4(基于命令) | 完整支持 | | **配置格式** | settings.json | hooks.json + rules/ | config.toml | opencode.json | | **上下文文件** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md | | **秘密检测** | 基于钩子 | beforeSubmitPrompt 钩子 | 基于沙箱 | 基于钩子 | -| **自动格式化** | PostToolUse 钩子 | afterFileEdit 钩子 | 不适用 | file.edited 钩子 | +| **自动格式化** | PostToolUse 钩子 | afterFileEdit 钩子 | N/A | file.edited 钩子 | | **版本** | 插件 | 插件 | 参考配置 | 1.8.0 | **关键架构决策:** -* 根目录下的 **AGENTS.md** 是通用的跨工具文件(被所有 4 个工具读取) +* **AGENTS.md** 在根目录是通用的跨工具文件(所有 4 个工具都能读取) * **DRY 适配器模式** 让 Cursor 可以重用 Claude Code 的钩子脚本而无需重复 -* **技能格式**(带有 YAML 前言的 SKILL.md)在 Claude Code、Codex 和 OpenCode 上都能工作 -* Codex 缺乏钩子的问题通过 `persistent_instructions` 和沙箱权限来弥补 +* **技能格式**(带有 YAML 前言的 SKILL.md)在 Claude Code、Codex 和 OpenCode 中都能工作 +* Codex 缺少钩子功能,通过 `AGENTS.md`、可选的 `model_instructions_file` 覆盖以及沙箱权限来弥补 *** diff --git a/docs/zh-CN/TROUBLESHOOTING.md b/docs/zh-CN/TROUBLESHOOTING.md new file mode 100644 index 00000000..ab62f011 --- /dev/null +++ b/docs/zh-CN/TROUBLESHOOTING.md @@ -0,0 +1,446 @@ +# 故障排除指南 + +Everything Claude Code (ECC) 插件的常见问题与解决方案。 + +## 目录 + +* [内存与上下文问题](#内存与上下文问题) +* [代理工具故障](#代理工具故障) +* [钩子与工作流错误](#钩子与工作流错误) +* [安装与设置](#安装与设置) +* [性能问题](#性能问题) +* [常见错误信息](#常见错误信息) +* [获取帮助](#获取帮助) + +*** + +## 内存与上下文问题 + +### 上下文窗口溢出 + +**症状:** 出现"上下文过长"错误或响应不完整 + +**原因:** + +* 上传的大文件超出令牌限制 +* 累积的对话历史记录 +* 单次会话中包含多个大型工具输出 + +**解决方案:** + +```bash +# 1. Clear conversation history and start fresh +# Use Claude Code: "New Chat" or Cmd/Ctrl+Shift+N + +# 2. Reduce file size before analysis +head -n 100 large-file.log > sample.log + +# 3. Use streaming for large outputs +head -n 50 large-file.txt + +# 4. Split tasks into smaller chunks +# Instead of: "Analyze all 50 files" +# Use: "Analyze files in src/components/ directory" +``` + +### 内存持久化失败 + +**症状:** 代理不记得先前的上下文或观察结果 + +**原因:** + +* 连续学习钩子被禁用 +* 观察文件损坏 +* 项目检测失败 + +**解决方案:** + +```bash +# Check if observations are being recorded +ls ~/.claude/homunculus/projects/*/observations.jsonl + +# Find the current project's hash id +python3 - <<'PY' +import json, os +registry_path = os.path.expanduser("~/.claude/homunculus/projects.json") +with open(registry_path) as f: + registry = json.load(f) +for project_id, meta in registry.items(): + if meta.get("root") == os.getcwd(): + print(project_id) + break +else: + raise SystemExit("Project hash not found in ~/.claude/homunculus/projects.json") +PY + +# View recent observations for that project +tail -20 ~/.claude/homunculus/projects//observations.jsonl + +# Back up a corrupted observations file before recreating it +mv ~/.claude/homunculus/projects//observations.jsonl \ + ~/.claude/homunculus/projects//observations.jsonl.bak.$(date +%Y%m%d-%H%M%S) + +# Verify hooks are enabled +grep -r "observe" ~/.claude/settings.json +``` + +*** + +## 代理工具故障 + +### 未找到代理 + +**症状:** 出现"代理未加载"或"未知代理"错误 + +**原因:** + +* 插件未正确安装 +* 代理路径配置错误 +* 市场安装与手动安装不匹配 + +**解决方案:** + +```bash +# Check plugin installation +ls ~/.claude/plugins/cache/ + +# Verify agent exists (marketplace install) +ls ~/.claude/plugins/cache/*/agents/ + +# For manual install, agents should be in: +ls ~/.claude/agents/ # Custom agents only + +# Reload plugin +# Claude Code → Settings → Extensions → Reload +``` + +### 工作流执行挂起 + +**症状:** 代理启动但从未完成 + +**原因:** + +* 代理逻辑中存在无限循环 +* 等待用户输入时被阻塞 +* 等待 API 响应时网络超时 + +**解决方案:** + +```bash +# 1. Check for stuck processes +ps aux | grep claude + +# 2. Enable debug mode +export CLAUDE_DEBUG=1 + +# 3. Set shorter timeouts +export CLAUDE_TIMEOUT=30 + +# 4. Check network connectivity +curl -I https://api.anthropic.com +``` + +### 工具使用错误 + +**症状:** 出现"工具执行失败"或权限被拒绝 + +**原因:** + +* 缺少依赖项(npm、python 等) +* 文件权限不足 +* 路径未找到 + +**解决方案:** + +```bash +# Verify required tools are installed +which node python3 npm git + +# Fix permissions on hook scripts +chmod +x ~/.claude/plugins/cache/*/hooks/*.sh +chmod +x ~/.claude/plugins/cache/*/skills/*/hooks/*.sh + +# Check PATH includes necessary binaries +echo $PATH +``` + +*** + +## 钩子与工作流错误 + +### 钩子未触发 + +**症状:** 前置/后置钩子未执行 + +**原因:** + +* 钩子未在 settings.json 中注册 +* 钩子语法无效 +* 钩子脚本不可执行 + +**解决方案:** + +```bash +# Check hooks are registered +grep -A 10 '"hooks"' ~/.claude/settings.json + +# Verify hook files exist and are executable +ls -la ~/.claude/plugins/cache/*/hooks/ + +# Test hook manually +bash ~/.claude/plugins/cache/*/hooks/pre-bash.sh <<< '{"command":"echo test"}' + +# Re-register hooks (if using plugin) +# Disable and re-enable plugin in Claude Code settings +``` + +### Python/Node 版本不匹配 + +**症状:** 出现"未找到 python3"或"node: 命令未找到" + +**原因:** + +* 缺少 Python/Node 安装 +* PATH 未配置 +* Python 版本错误(Windows) + +**解决方案:** + +```bash +# Install Python 3 (if missing) +# macOS: brew install python3 +# Ubuntu: sudo apt install python3 +# Windows: Download from python.org + +# Install Node.js (if missing) +# macOS: brew install node +# Ubuntu: sudo apt install nodejs npm +# Windows: Download from nodejs.org + +# Verify installations +python3 --version +node --version +npm --version + +# Windows: Ensure python (not python3) works +python --version +``` + +### 开发服务器拦截器误报 + +**症状:** 钩子拦截了提及"dev"的合法命令 + +**原因:** + +* Heredoc 内容触发模式匹配 +* 参数中包含"dev"的非开发命令 + +**解决方案:** + +```bash +# This is fixed in v1.8.0+ (PR #371) +# Upgrade plugin to latest version + +# Workaround: Wrap dev servers in tmux +tmux new-session -d -s dev "npm run dev" +tmux attach -t dev + +# Disable hook temporarily if needed +# Edit ~/.claude/settings.json and remove pre-bash hook +``` + +*** + +## 安装与设置 + +### 插件未加载 + +**症状:** 安装后插件功能不可用 + +**原因:** + +* 市场缓存未更新 +* Claude Code 版本不兼容 +* 插件文件损坏 + +**解决方案:** + +```bash +# Inspect the plugin cache before changing it +ls -la ~/.claude/plugins/cache/ + +# Back up the plugin cache instead of deleting it in place +mv ~/.claude/plugins/cache ~/.claude/plugins/cache.backup.$(date +%Y%m%d-%H%M%S) +mkdir -p ~/.claude/plugins/cache + +# Reinstall from marketplace +# Claude Code → Extensions → Everything Claude Code → Uninstall +# Then reinstall from marketplace + +# Check Claude Code version +claude --version +# Requires Claude Code 2.0+ + +# Manual install (if marketplace fails) +git clone https://github.com/affaan-m/everything-claude-code.git +cp -r everything-claude-code ~/.claude/plugins/ecc +``` + +### 包管理器检测失败 + +**症状:** 使用了错误的包管理器(用 npm 而不是 pnpm) + +**原因:** + +* 没有 lock 文件 +* 未设置 CLAUDE\_PACKAGE\_MANAGER +* 多个 lock 文件导致检测混乱 + +**解决方案:** + +```bash +# Set preferred package manager globally +export CLAUDE_PACKAGE_MANAGER=pnpm +# Add to ~/.bashrc or ~/.zshrc + +# Or set per-project +echo '{"packageManager": "pnpm"}' > .claude/package-manager.json + +# Or use package.json field +npm pkg set packageManager="pnpm@8.15.0" + +# Warning: removing lock files can change installed dependency versions. +# Commit or back up the lock file first, then run a fresh install and re-run CI. +# Only do this when intentionally switching package managers. +rm package-lock.json # If using pnpm/yarn/bun +``` + +*** + +## 性能问题 + +### 响应时间缓慢 + +**症状:** 代理需要 30 秒以上才能响应 + +**原因:** + +* 大型观察文件 +* 活动钩子过多 +* 到 API 的网络延迟 + +**解决方案:** + +```bash +# Archive large observations instead of deleting them +archive_dir="$HOME/.claude/homunculus/archive/$(date +%Y%m%d)" +mkdir -p "$archive_dir" +find ~/.claude/homunculus/projects -name "observations.jsonl" -size +10M -exec sh -c ' + for file do + base=$(basename "$(dirname "$file")") + gzip -c "$file" > "'"$archive_dir"'/${base}-observations.jsonl.gz" + : > "$file" + done +' sh {} + + +# Disable unused hooks temporarily +# Edit ~/.claude/settings.json + +# Keep active observation files small +# Large archives should live under ~/.claude/homunculus/archive/ +``` + +### CPU 使用率高 + +**症状:** Claude Code 占用 100% CPU + +**原因:** + +* 无限观察循环 +* 对大型目录的文件监视 +* 钩子中的内存泄漏 + +**解决方案:** + +```bash +# Check for runaway processes +top -o cpu | grep claude + +# Disable continuous learning temporarily +touch ~/.claude/homunculus/disabled + +# Restart Claude Code +# Cmd/Ctrl+Q then reopen + +# Check observation file size +du -sh ~/.claude/homunculus/*/ +``` + +*** + +## 常见错误信息 + +### "EACCES: permission denied" + +```bash +# Fix hook permissions +find ~/.claude/plugins -name "*.sh" -exec chmod +x {} \; + +# Fix observation directory permissions +chmod -R u+rwX,go+rX ~/.claude/homunculus +``` + +### "MODULE\_NOT\_FOUND" + +```bash +# Install plugin dependencies +cd ~/.claude/plugins/cache/everything-claude-code +npm install + +# Or for manual install +cd ~/.claude/plugins/ecc +npm install +``` + +### "spawn UNKNOWN" + +```bash +# Windows-specific: Ensure scripts use correct line endings +# Convert CRLF to LF +find ~/.claude/plugins -name "*.sh" -exec dos2unix {} \; + +# Or install dos2unix +# macOS: brew install dos2unix +# Ubuntu: sudo apt install dos2unix +``` + +*** + +## 获取帮助 + +如果您仍然遇到问题: + +1. **检查 GitHub Issues**:[github.com/affaan-m/everything-claude-code/issues](https://github.com/affaan-m/everything-claude-code/issues) +2. **启用调试日志记录**: + ```bash + export CLAUDE_DEBUG=1 + export CLAUDE_LOG_LEVEL=debug + ``` +3. **收集诊断信息**: + ```bash + claude --version + node --version + python3 --version + echo $CLAUDE_PACKAGE_MANAGER + ls -la ~/.claude/plugins/cache/ + ``` +4. **提交 Issue**:包括调试日志、错误信息和诊断信息 + +*** + +## 相关文档 + +* [README.md](README.md) - 安装与功能 +* [CONTRIBUTING.md](CONTRIBUTING.md) - 开发指南 +* [docs/](..) - 详细文档 +* [examples/](../../examples) - 使用示例 diff --git a/docs/zh-CN/agents/kotlin-build-resolver.md b/docs/zh-CN/agents/kotlin-build-resolver.md new file mode 100644 index 00000000..bdd19df5 --- /dev/null +++ b/docs/zh-CN/agents/kotlin-build-resolver.md @@ -0,0 +1,119 @@ +--- +name: kotlin-build-resolver +description: Kotlin/Gradle 构建、编译和依赖错误解决专家。以最小改动修复构建错误、Kotlin 编译器错误和 Gradle 问题。适用于 Kotlin 构建失败时。 +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +model: sonnet +--- + +# Kotlin 构建错误解决器 + +你是一位 Kotlin/Gradle 构建错误解决专家。你的任务是以 **最小、精准的改动** 修复 Kotlin 构建错误、Gradle 配置问题和依赖解析失败。 + +## 核心职责 + +1. 诊断 Kotlin 编译错误 +2. 修复 Gradle 构建配置问题 +3. 解决依赖冲突和版本不匹配 +4. 处理 Kotlin 编译器错误和警告 +5. 修复 detekt 和 ktlint 违规 + +## 诊断命令 + +按顺序运行这些命令: + +```bash +./gradlew build 2>&1 +./gradlew detekt 2>&1 || echo "detekt not configured" +./gradlew ktlintCheck 2>&1 || echo "ktlint not configured" +./gradlew dependencies --configuration runtimeClasspath 2>&1 | head -100 +``` + +## 解决工作流 + +```text +1. ./gradlew build -> Parse error message +2. Read affected file -> Understand context +3. Apply minimal fix -> Only what's needed +4. ./gradlew build -> Verify fix +5. ./gradlew test -> Ensure nothing broke +``` + +## 常见修复模式 + +| 错误 | 原因 | 修复方法 | +|-------|-------|-----| +| `Unresolved reference: X` | 缺少导入、拼写错误、缺少依赖 | 添加导入或依赖 | +| `Type mismatch: Required X, Found Y` | 类型错误、缺少转换 | 添加转换或修正类型 | +| `None of the following candidates is applicable` | 重载错误、参数类型错误 | 修正参数类型或添加显式转换 | +| `Smart cast impossible` | 可变属性或并发访问 | 使用局部 `val` 副本或 `let` | +| `'when' expression must be exhaustive` | 密封类 `when` 中缺少分支 | 添加缺失分支或 `else` | +| `Suspend function can only be called from coroutine` | 缺少 `suspend` 或协程作用域 | 添加 `suspend` 修饰符或启动协程 | +| `Cannot access 'X': it is internal in 'Y'` | 可见性问题 | 更改可见性或使用公共 API | +| `Conflicting declarations` | 重复定义 | 移除重复项或重命名 | +| `Could not resolve: group:artifact:version` | 缺少仓库或版本错误 | 添加仓库或修正版本 | +| `Execution failed for task ':detekt'` | 代码风格违规 | 修复 detekt 发现的问题 | + +## Gradle 故障排除 + +```bash +# Check dependency tree for conflicts +./gradlew dependencies --configuration runtimeClasspath + +# Force refresh dependencies +./gradlew build --refresh-dependencies + +# Clear project-local Gradle build cache +./gradlew clean && rm -rf .gradle/build-cache/ + +# Check Gradle version compatibility +./gradlew --version + +# Run with debug output +./gradlew build --debug 2>&1 | tail -50 + +# Check for dependency conflicts +./gradlew dependencyInsight --dependency --configuration runtimeClasspath +``` + +## Kotlin 编译器标志 + +```kotlin +// build.gradle.kts - Common compiler options +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xjsr305=strict") // Strict Java null safety + allWarningsAsErrors = true + } +} +``` + +## 关键原则 + +* **仅进行精准修复** -- 不要重构,只修复错误 +* **绝不** 在没有明确批准的情况下抑制警告 +* **绝不** 更改函数签名,除非必要 +* **始终** 在每次修复后运行 `./gradlew build` 以验证 +* 修复根本原因而非抑制症状 +* 优先添加缺失的导入而非使用通配符导入 + +## 停止条件 + +如果出现以下情况,请停止并报告: + +* 尝试修复 3 次后相同错误仍然存在 +* 修复引入的错误比它解决的更多 +* 错误需要超出范围的架构更改 +* 缺少需要用户决策的外部依赖 + +## 输出格式 + +```text +[FIXED] src/main/kotlin/com/example/service/UserService.kt:42 +Error: Unresolved reference: UserRepository +Fix: Added import com.example.repository.UserRepository +Remaining errors: 2 +``` + +最终:`Build Status: SUCCESS/FAILED | Errors Fixed: N | Files Modified: list` + +有关详细的 Kotlin 模式和代码示例,请参阅 `skill: kotlin-patterns`。 diff --git a/docs/zh-CN/agents/kotlin-reviewer.md b/docs/zh-CN/agents/kotlin-reviewer.md new file mode 100644 index 00000000..d13fb46f --- /dev/null +++ b/docs/zh-CN/agents/kotlin-reviewer.md @@ -0,0 +1,161 @@ +--- +name: kotlin-reviewer +description: Kotlin 和 Android/KMP 代码审查员。审查 Kotlin 代码以检查惯用模式、协程安全性、Compose 最佳实践、违反清洁架构原则以及常见的 Android 陷阱。 +tools: ["Read", "Grep", "Glob", "Bash"] +model: sonnet +--- + +您是一位资深的 Kotlin 和 Android/KMP 代码审查员,确保代码符合语言习惯、安全且易于维护。 + +## 您的角色 + +* 审查 Kotlin 代码是否符合语言习惯模式以及 Android/KMP 最佳实践 +* 检测协程误用、Flow 反模式和生命周期错误 +* 强制执行清晰的架构模块边界 +* 识别 Compose 性能问题和重组陷阱 +* 您**不**重构或重写代码 —— 仅报告发现的问题 + +## 工作流程 + +### 步骤 1:收集上下文 + +运行 `git diff --staged` 和 `git diff` 以查看更改。如果没有差异,请检查 `git log --oneline -5`。识别已更改的 Kotlin/KTS 文件。 + +### 步骤 2:理解项目结构 + +检查: + +* `build.gradle.kts` 或 `settings.gradle.kts` 以理解模块布局 +* `CLAUDE.md` 了解项目特定的约定 +* 项目是仅限 Android、KMP 还是 Compose Multiplatform + +### 步骤 2b:安全审查 + +在继续之前,应用 Kotlin/Android 安全指南: + +* 已导出的 Android 组件、深度链接和意图过滤器 +* 不安全的加密、WebView 和网络配置使用 +* 密钥库、令牌和凭据处理 +* 平台特定的存储和权限风险 + +如果发现**严重**安全问题,请停止审查,并在进行任何进一步分析之前,将问题移交给 `security-reviewer`。 + +### 步骤 3:阅读和审查 + +完整阅读已更改的文件。应用下面的审查清单,并检查周围代码以获取上下文。 + +### 步骤 4:报告发现 + +使用下面的输出格式。仅报告置信度 >80% 的问题。 + +## 审查清单 + +### 架构(严重) + +* **领域层导入框架** — `domain` 模块不得导入 Android、Ktor、Room 或任何框架 +* **数据层泄漏到 UI 层** — 实体或 DTO 暴露给表示层(必须映射到领域模型) +* **ViewModel 中的业务逻辑** — 复杂逻辑应属于 UseCases,而不是 ViewModels +* **循环依赖** — 模块 A 依赖于 B,而模块 B 又依赖于 A + +### 协程与 Flow(高) + +* **GlobalScope 使用** — 必须使用结构化作用域(`viewModelScope`、`coroutineScope`) +* **捕获 CancellationException** — 必须重新抛出或不捕获;吞没该异常会破坏取消机制 +* **IO 操作缺少 `withContext`** — 在 `Dispatchers.Main` 上进行数据库/网络调用 +* **包含可变状态的 StateFlow** — 在 StateFlow 内部使用可变集合(必须复制) +* **在 `init {}` 中收集 Flow** — 应使用 `stateIn()` 或在作用域内启动 +* **缺少 `WhileSubscribed`** — 当 `WhileSubscribed` 更合适时使用了 `stateIn(scope, SharingStarted.Eagerly)` + +```kotlin +// BAD — swallows cancellation +try { fetchData() } catch (e: Exception) { log(e) } + +// GOOD — preserves cancellation +try { fetchData() } catch (e: CancellationException) { throw e } catch (e: Exception) { log(e) } +// or use runCatching and check +``` + +### Compose(高) + +* **不稳定参数** — 可组合函数接收可变类型会导致不必要的重组 +* **LaunchedEffect 之外的作用效应** — 网络/数据库调用必须在 `LaunchedEffect` 或 ViewModel 中 +* **NavController 被深层传递** — 应传递 lambda 而非 `NavController` 引用 +* **LazyColumn 中缺少 `key()`** — 没有稳定键的项目会导致性能不佳 +* **`remember` 缺少键** — 当依赖项更改时,计算不会重新执行 +* **参数中的对象分配** — 内联创建对象会导致重组 + +```kotlin +// BAD — new lambda every recomposition +Button(onClick = { viewModel.doThing(item.id) }) + +// GOOD — stable reference +val onClick = remember(item.id) { { viewModel.doThing(item.id) } } +Button(onClick = onClick) +``` + +### Kotlin 惯用法(中) + +* **`!!` 使用** — 非空断言;更推荐 `?.`、`?:`、`requireNotNull` 或 `checkNotNull` +* **可以使用 `val` 的地方使用了 `var`** — 更推荐不可变性 +* **Java 风格模式** — 静态工具类(应使用顶层函数)、getter/setter(应使用属性) +* **字符串拼接** — 使用字符串模板 `"Hello $name"` 而非 `"Hello " + name` +* **`when` 缺少穷举分支** — 密封类/接口应使用穷举的 `when` +* **暴露可变集合** — 公共 API 应返回 `List` 而非 `MutableList` + +### Android 特定(中) + +* **上下文泄漏** — 在单例/ViewModels 中存储 `Activity` 或 `Fragment` 引用 +* **缺少 ProGuard 规则** — 序列化类缺少 `@Keep` 或 ProGuard 规则 +* **硬编码字符串** — 面向用户的字符串未放在 `strings.xml` 或 Compose 资源中 +* **缺少生命周期处理** — 在 Activity 中收集 Flow 时未使用 `repeatOnLifecycle` + +### 安全(严重) + +* **已导出组件暴露** — 活动、服务或接收器在没有适当防护的情况下被导出 +* **不安全的加密/存储** — 自制的加密、明文存储的秘密或弱密钥库使用 +* **不安全的 WebView/网络配置** — JavaScript 桥接、明文流量、过于宽松的信任设置 +* **敏感日志记录** — 令牌、凭据、PII 或秘密信息被输出到日志 + +如果存在任何**严重**安全问题,请停止并升级给 `security-reviewer`。 + +### Gradle 与构建(低) + +* **未使用版本目录** — 硬编码版本而非使用 `libs.versions.toml` +* **不必要的依赖项** — 添加了但未使用的依赖项 +* **缺少 KMP 源集** — 声明了 `androidMain` 代码,而该代码本可以是 `commonMain` + +## 输出格式 + +``` +[CRITICAL] Domain module imports Android framework +File: domain/src/main/kotlin/com/app/domain/UserUseCase.kt:3 +Issue: `import android.content.Context` — domain must be pure Kotlin with no framework dependencies. +Fix: Move Context-dependent logic to data or platforms layer. Pass data via repository interface. + +[HIGH] StateFlow holding mutable list +File: presentation/src/main/kotlin/com/app/ui/ListViewModel.kt:25 +Issue: `_state.value.items.add(newItem)` mutates the list inside StateFlow — Compose won't detect the change. +Fix: Use `_state.update { it.copy(items = it.items + newItem) }` +``` + +## 摘要格式 + +每次审查结束时附上: + +``` +## Review Summary + +| Severity | Count | Status | +|----------|-------|--------| +| CRITICAL | 0 | pass | +| HIGH | 1 | block | +| MEDIUM | 2 | info | +| LOW | 0 | note | + +Verdict: BLOCK — HIGH issues must be fixed before merge. +``` + +## 批准标准 + +* **批准**:没有**严重**或**高**级别问题 +* **阻止**:存在任何**严重**或**高**级别问题 —— 必须在合并前修复 diff --git a/docs/zh-CN/commands/aside.md b/docs/zh-CN/commands/aside.md new file mode 100644 index 00000000..8cd27f43 --- /dev/null +++ b/docs/zh-CN/commands/aside.md @@ -0,0 +1,173 @@ +--- +description: 在不打断或丢失当前任务上下文的情况下,快速回答一个附带问题。回答后自动恢复工作。 +--- + +# 旁述指令 + +在任务进行中提问,获得即时、聚焦的回答——然后立即从暂停处继续。当前任务、文件和上下文绝不会被修改。 + +## 何时使用 + +* 你在 Claude 工作时对某事感到好奇,但又不想打断工作节奏 +* 你需要快速解释 Claude 当前正在编辑的代码 +* 你想就某个决定征求第二意见或进行澄清,而不会使任务偏离方向 +* 在 Claude 继续之前,你需要理解一个错误、概念或模式 +* 你想询问与当前任务无关的事情,而无需开启新会话 + +## 使用方法 + +``` +/aside +/aside what does this function actually return? +/aside is this pattern thread-safe? +/aside why are we using X instead of Y here? +/aside what's the difference between foo() and bar()? +/aside should we be worried about the N+1 query we just added? +``` + +## 流程 + +### 步骤 1:冻结当前任务状态 + +在回答任何问题之前,先在心里记下: + +* 当前活动任务是什么?(正在处理哪个文件、功能或问题) +* 在调用 `/aside` 时,进行到哪一步了? +* 接下来原本要发生什么? + +在旁述期间,**不要**触碰、编辑、创建或删除任何文件。 + +### 步骤 2:直接回答问题 + +以最简洁但仍完整有用的形式回答问题。 + +* 先说答案,再说推理过程 +* 保持简短——如果需要完整解释,请在任务结束后再提供 +* 如果问题涉及当前正在处理的文件或代码,请精确引用(相关时包括文件路径和行号) +* 如果回答问题需要读取文件,就读它——但只读不写 + +将响应格式化为: + +``` +ASIDE: [restate the question briefly] + +[Your answer here] + +— Back to task: [one-line description of what was being done] +``` + +### 步骤 3:恢复主任务 + +在给出答案后,立即从暂停的确切点继续执行活动任务。除非旁述回答揭示了阻碍或需要重新考虑当前方法的理由(见边缘情况),否则不要请求恢复许可。 + +*** + +## 边缘情况 + +**未提供问题(`/aside` 后面没有内容):** +回复: + +``` +ASIDE: no question provided + +What would you like to know? (ask your question and I'll answer without losing the current task context) + +— Back to task: [one-line description of what was being done] +``` + +**问题揭示了当前任务的潜在问题:** +在恢复之前清楚地标记出来: + +``` +ASIDE: [answer] + +⚠️ Note: This answer suggests [issue] with the current approach. Want to address this before continuing, or proceed as planned? +``` + +等待用户的决定后再恢复。 + +**问题实际上是任务重定向(而非旁述问题):** +如果问题暗示要改变正在构建的内容(例如,`/aside actually, let's use Redis instead`),请澄清: + +``` +ASIDE: That sounds like a direction change, not just a side question. +Do you want to: + (a) Answer this as information only and keep the current plan + (b) Pause the current task and change approach +``` + +等待用户的回答——不要自行假设。 + +**问题涉及当前打开的文件或代码:** +根据实时上下文回答。如果该文件在会话早期已被读取,直接引用它。如果尚未读取,现在读取它(只读)并在回答时附带文件:行号引用。 + +**无活动任务(调用 `/aside` 时没有进行中的任务):** +仍然使用标准包装器,以保持响应格式一致: + +``` +ASIDE: [restate the question briefly] + +[Your answer here] + +— Back to task: no active task to resume +``` + +**问题需要长篇回答:** +简洁地给出核心答案,然后提供: + +``` +That's the short version. Want a deeper explanation after we finish [current task]? +``` + +**连续多个 `/aside` 问题:** +依次回答每个问题。在回答完最后一个问题后,恢复主任务。不要在一连串旁述中丢失任务状态。 + +**旁述回答暗示需要进行代码更改:** +记下所需的更改,但不要在旁述期间进行更改: + +``` +ASIDE: [answer] + +📝 Worth fixing: [what should be changed]. I'll flag this after the current task unless you want to address it now. +``` + +**问题含糊不清或过于笼统:** +提出一个澄清性问题——一个能获取回答所需信息的最简短问题。不要提出多个问题。 + +*** + +## 示例输出 + +``` +User: /aside what does fetchWithRetry() actually do? + +ASIDE: what does fetchWithRetry() do? + +fetchWithRetry() (src/api/retry.ts:12) attempts the request up to 3 times with +exponential backoff (250ms → 500ms → 1s). It only retries on 5xx responses and +network errors — 4xx errors are treated as final and not retried. + +— Back to task: refactoring the auth middleware in src/middleware/auth.ts +``` + +``` +User: /aside is the approach we're taking thread-safe? + +ASIDE: is the current approach thread-safe? + +No — the shared cache object in src/cache/store.ts:34 is mutated without locking. +Under concurrent requests this is a race condition. It's low risk in a single-process +Node.js server but would be a real problem with worker threads or clustering. + +⚠️ Note: This could affect the feature we're building. Want to address this now or continue and fix it in a follow-up? +``` + +*** + +## 注意事项 + +* 在旁述期间**绝不**修改文件——仅限只读访问 +* 旁述是对话暂停,不是新任务——必须始终恢复原始任务 +* 保持回答聚焦:目标是快速为用户扫清障碍,而不是进行长篇大论 +* 如果旁述引发了更广泛的讨论,请先完成当前任务,除非旁述揭示了阻碍 +* 除非明确与任务结果相关,否则旁述内容不会保存到会话文件中 diff --git a/docs/zh-CN/commands/e2e.md b/docs/zh-CN/commands/e2e.md index fab422a0..0c95898d 100644 --- a/docs/zh-CN/commands/e2e.md +++ b/docs/zh-CN/commands/e2e.md @@ -345,7 +345,10 @@ Quarantine recommendation: Mark as test.fixme() until fixed ## 相关代理 -此命令调用位于 `~/.claude/agents/e2e-runner.md` 的 `e2e-runner` 代理。 +此命令调用由 ECC 提供的 `e2e-runner` 代理。 + +对于手动安装,源文件位于: +`agents/e2e-runner.md` ## 快速命令 diff --git a/docs/zh-CN/commands/gradle-build.md b/docs/zh-CN/commands/gradle-build.md new file mode 100644 index 00000000..4ec88916 --- /dev/null +++ b/docs/zh-CN/commands/gradle-build.md @@ -0,0 +1,72 @@ +--- +description: 修复 Android 和 KMP 项目的 Gradle 构建错误 +--- + +# Gradle 构建修复 + +逐步修复 Android 和 Kotlin 多平台项目的 Gradle 构建和编译错误。 + +## 步骤 1:检测构建配置 + +识别项目类型并运行相应的构建: + +| 指示符 | 构建命令 | +|-----------|---------------| +| `build.gradle.kts` + `composeApp/` (KMP) | `./gradlew composeApp:compileKotlinMetadata 2>&1` | +| `build.gradle.kts` + `app/` (Android) | `./gradlew app:compileDebugKotlin 2>&1` | +| `settings.gradle.kts` 包含模块 | `./gradlew assemble 2>&1` | +| 配置了 Detekt | `./gradlew detekt 2>&1` | + +同时检查 `gradle.properties` 和 `local.properties` 以获取配置信息。 + +## 步骤 2:解析并分组错误 + +1. 运行构建命令并捕获输出 +2. 将 Kotlin 编译错误与 Gradle 配置错误分开 +3. 按模块和文件路径分组 +4. 排序:先处理配置错误,然后按依赖顺序处理编译错误 + +## 步骤 3:修复循环 + +针对每个错误: + +1. **读取文件** — 错误行周围的完整上下文 +2. **诊断** — 常见类别: + * 缺少导入或无法解析的引用 + * 类型不匹配或不兼容的类型 + * `build.gradle.kts` 中缺少依赖项 + * Expect/actual 不匹配 (KMP) + * Compose 编译器错误 +3. **最小化修复** — 解决错误所需的最小改动 +4. **重新运行构建** — 验证修复并检查新错误 +5. **继续** — 处理下一个错误 + +## 步骤 4:防护措施 + +如果出现以下情况,请停止并询问用户: + +* 修复引入的错误比解决的错误多 +* 同一错误在 3 次尝试后仍然存在 +* 错误需要添加新的依赖项或更改模块结构 +* Gradle 同步本身失败(配置阶段错误) +* 错误出现在生成的代码中(Room、SQLDelight、KSP) + +## 步骤 5:总结 + +报告: + +* 已修复的错误(模块、文件、描述) +* 剩余的错误 +* 引入的新错误(应为零) +* 建议的后续步骤 + +## 常见的 Gradle/KMP 修复方案 + +| 错误 | 修复方法 | +|-------|-----| +| `commonMain` 中无法解析的引用 | 检查依赖项是否在 `commonMain.dependencies {}` 中 | +| Expect 声明没有 actual 实现 | 在每个平台源码集中添加 `actual` 实现 | +| Compose 编译器版本不匹配 | 在 `libs.versions.toml` 中统一 Kotlin 和 Compose 编译器版本 | +| 重复类 | 使用 `./gradlew dependencies` 检查是否存在冲突的依赖项 | +| KSP 错误 | 运行 `./gradlew kspCommonMainKotlinMetadata` 重新生成 | +| 配置缓存问题 | 检查是否存在不可序列化的任务输入 | diff --git a/docs/zh-CN/commands/kotlin-build.md b/docs/zh-CN/commands/kotlin-build.md new file mode 100644 index 00000000..3c7807d4 --- /dev/null +++ b/docs/zh-CN/commands/kotlin-build.md @@ -0,0 +1,176 @@ +--- +description: 逐步修复 Kotlin/Gradle 构建错误、编译器警告和依赖项问题。调用 kotlin-build-resolver 代理进行最小化、精准的修复。 +--- + +# Kotlin 构建与修复 + +此命令调用 **kotlin-build-resolver** 代理,以最小的改动增量修复 Kotlin 构建错误。 + +## 此命令的作用 + +1. **运行诊断**:执行 `./gradlew build`、`detekt`、`ktlintCheck` +2. **解析错误**:按文件分组并按严重性排序 +3. **增量修复**:每次修复一个错误 +4. **验证每次修复**:每次更改后重新运行构建 +5. **报告摘要**:显示已修复的内容和剩余问题 + +## 何时使用 + +在以下情况使用 `/kotlin-build`: + +* `./gradlew build` 因错误而失败 +* Kotlin 编译器报告错误 +* `./gradlew detekt` 报告违规 +* Gradle 依赖项解析失败 +* 拉取更改后导致构建失败 + +## 运行的诊断命令 + +```bash +# Primary build check +./gradlew build 2>&1 + +# Static analysis +./gradlew detekt 2>&1 || echo "detekt not configured" +./gradlew ktlintCheck 2>&1 || echo "ktlint not configured" + +# Dependency issues +./gradlew dependencies --configuration runtimeClasspath 2>&1 | head -100 + +# Optional deep refresh when caches or dependency metadata are suspect +./gradlew build --refresh-dependencies +``` + +## 示例会话 + +````text +User: /kotlin-build + +Agent: +# Kotlin Build Resolution + +## Initial Diagnostics + +```bash +$ ./gradlew build +e: src/main/kotlin/com/example/service/UserService.kt:25:15 Unresolved reference: UserRepository +e: src/main/kotlin/com/example/routes/UserRoutes.kt:42:9 Type mismatch: inferred type is String but Int was expected +e: src/main/kotlin/com/example/routes/UserRoutes.kt:58:5 'when' expression must be exhaustive +``` + +Errors found: 3 + +## Fix 1: Unresolved Reference + +File: src/main/kotlin/com/example/service/UserService.kt:25 +Error: Unresolved reference: UserRepository +Cause: Missing import + +```kotlin +// Added import +import com.example.repository.UserRepository +``` + +```bash +$ ./gradlew build +# 2 errors remaining +``` + +## Fix 2: Type Mismatch + +File: src/main/kotlin/com/example/routes/UserRoutes.kt:42 +Error: Type mismatch: inferred type is String but Int was expected + +```kotlin +// Changed +val count = call.parameters["count"] +// To +val count = call.parameters["count"]?.toIntOrNull() + ?: return@get call.respond(HttpStatusCode.BadRequest, "Invalid count") +``` + +```bash +$ ./gradlew build +# 1 error remaining +``` + +## Fix 3: Non-Exhaustive When + +File: src/main/kotlin/com/example/routes/UserRoutes.kt:58 +Error: 'when' expression must be exhaustive + +```kotlin +// Added missing branch +when (user.role) { + Role.ADMIN -> handleAdmin(user) + Role.USER -> handleUser(user) + Role.MODERATOR -> handleModerator(user) // Added +} +``` + +```bash +$ ./gradlew build +# Build successful! +``` + +## Final Verification + +```bash +$ ./gradlew detekt +# No issues + +$ ./gradlew test +# All tests passed +``` + +## Summary + +| Metric | Count | +|--------|-------| +| Build errors fixed | 3 | +| Detekt issues fixed | 0 | +| Files modified | 2 | +| Remaining issues | 0 | + +Build Status: ✅ SUCCESS +```` + +## 常见的已修复错误 + +| 错误 | 典型修复方法 | +|-------|-------------| +| `Unresolved reference: X` | 添加导入或依赖项 | +| `Type mismatch` | 修复类型转换或赋值 | +| `'when' must be exhaustive` | 添加缺失的密封类分支 | +| `Suspend function can only be called from coroutine` | 添加 `suspend` 修饰符 | +| `Smart cast impossible` | 使用局部 `val` 或 `let` | +| `None of the following candidates is applicable` | 修复参数类型 | +| `Could not resolve dependency` | 修复版本或添加仓库 | + +## 修复策略 + +1. **首先修复构建错误** - 代码必须能够编译 +2. **其次修复 Detekt 违规** - 修复代码质量问题 +3. **再次修复 ktlint 警告** - 修复格式问题 +4. **一次修复一个** - 验证每次更改 +5. **最小化改动** - 不进行重构,仅修复问题 + +## 停止条件 + +代理将在以下情况下停止并报告: + +* 同一错误尝试修复 3 次后仍然存在 +* 修复引入了更多错误 +* 需要进行架构性更改 +* 缺少外部依赖项 + +## 相关命令 + +* `/kotlin-test` - 构建成功后运行测试 +* `/kotlin-review` - 审查代码质量 +* `/verify` - 完整的验证循环 + +## 相关 + +* 代理:`agents/kotlin-build-resolver.md` +* 技能:`skills/kotlin-patterns/` diff --git a/docs/zh-CN/commands/kotlin-review.md b/docs/zh-CN/commands/kotlin-review.md new file mode 100644 index 00000000..fd1681cc --- /dev/null +++ b/docs/zh-CN/commands/kotlin-review.md @@ -0,0 +1,144 @@ +--- +description: 全面的Kotlin代码审查,涵盖惯用模式、空安全、协程安全和安全性。调用kotlin-reviewer代理。 +--- + +# Kotlin 代码审查 + +此命令调用 **kotlin-reviewer** 代理进行全面的 Kotlin 专项代码审查。 + +## 此命令的功能 + +1. **识别 Kotlin 变更**:通过 `git diff` 查找修改过的 `.kt` 和 `.kts` 文件 +2. **运行构建与静态分析**:执行 `./gradlew build`、`detekt`、`ktlintCheck` +3. **安全扫描**:检查 SQL 注入、命令注入、硬编码的密钥 +4. **空安全审查**:分析 `!!` 的使用、平台类型处理、不安全的转换 +5. **协程审查**:检查结构化并发、调度器使用、取消操作 +6. **生成报告**:按严重性对问题分类 + +## 使用时机 + +在以下情况使用 `/kotlin-review`: + +* 编写或修改 Kotlin 代码后 +* 提交 Kotlin 变更前 +* 审查包含 Kotlin 代码的拉取请求时 +* 接手新的 Kotlin 代码库时 +* 学习 Kotlin 惯用模式时 + +## 审查类别 + +### 严重(必须修复) + +* SQL/命令注入漏洞 +* 无正当理由强制解包 `!!` +* 平台类型空安全违规 +* 使用 GlobalScope(违反结构化并发) +* 硬编码的凭证 +* 不安全的反序列化 + +### 高(应该修复) + +* 可变状态,而不可变状态即可满足 +* 在协程上下文中进行阻塞调用 +* 长循环中缺少取消检查 +* 对密封类型使用非穷举的 `when` +* 函数过大(>50 行) +* 嵌套过深(>4 层) + +### 中(考虑修复) + +* 非 Kotlin 惯用写法(Java 风格模式) +* 缺少尾随逗号 +* 误用或嵌套作用域函数 +* 大型集合链中缺少序列化 +* 冗余的显式类型声明 + +## 运行的自动化检查 + +```bash +# Build check +./gradlew build + +# Static analysis +./gradlew detekt + +# Formatting check +./gradlew ktlintCheck + +# Tests +./gradlew test +``` + +## 使用示例 + +````text +User: /kotlin-review + +Agent: +# Kotlin Code Review Report + +## Files Reviewed +- src/main/kotlin/com/example/service/UserService.kt (modified) +- src/main/kotlin/com/example/routes/UserRoutes.kt (modified) + +## Static Analysis Results +✓ Build: Successful +✓ detekt: No issues +⚠ ktlint: 2 formatting warnings + +## Issues Found + +[CRITICAL] Force-Unwrap Null Safety +File: src/main/kotlin/com/example/service/UserService.kt:28 +Issue: Using !! on nullable repository result +```kotlin +val user = repository.findById(id)!! // NPE risk +``` +Fix: Use safe call with error handling +```kotlin +val user = repository.findById(id) + ?: throw UserNotFoundException("User $id not found") +``` + +[HIGH] GlobalScope Usage +File: src/main/kotlin/com/example/routes/UserRoutes.kt:45 +Issue: Using GlobalScope breaks structured concurrency +```kotlin +GlobalScope.launch { + notificationService.sendWelcome(user) +} +``` +Fix: Use the call's coroutine scope +```kotlin +launch { + notificationService.sendWelcome(user) +} +``` + +## Summary +- CRITICAL: 1 +- HIGH: 1 +- MEDIUM: 0 + +Recommendation: ❌ Block merge until CRITICAL issue is fixed +```` + +## 批准标准 + +| 状态 | 条件 | +|--------|-----------| +| ✅ 批准 | 无严重或高优先级问题 | +| ⚠️ 警告 | 仅存在中优先级问题(谨慎合并) | +| ❌ 阻止 | 发现严重或高优先级问题 | + +## 与其他命令的集成 + +* 首先使用 `/kotlin-test` 确保测试通过 +* 如果构建出错,使用 `/kotlin-build` +* 提交前使用 `/kotlin-review` +* 对于非 Kotlin 专项问题,使用 `/code-review` + +## 相关 + +* 代理:`agents/kotlin-reviewer.md` +* 技能:`skills/kotlin-patterns/`、`skills/kotlin-testing/` diff --git a/docs/zh-CN/commands/kotlin-test.md b/docs/zh-CN/commands/kotlin-test.md new file mode 100644 index 00000000..3fff0902 --- /dev/null +++ b/docs/zh-CN/commands/kotlin-test.md @@ -0,0 +1,315 @@ +--- +description: 为Kotlin强制执行TDD工作流程。首先编写Kotest测试,然后实施。使用Kover验证80%以上的覆盖率。 +--- + +# Kotlin TDD 命令 + +此命令使用 Kotest、MockK 和 Kover 为 Kotlin 代码强制执行测试驱动开发方法。 + +## 此命令的作用 + +1. **定义类型/接口**:首先搭建函数签名 +2. **编写 Kotest 测试**:创建全面的测试规范(RED) +3. **运行测试**:验证测试因正确原因而失败 +4. **实现代码**:编写最少的代码以通过测试(GREEN) +5. **重构**:在保持测试通过的同时进行改进 +6. **检查覆盖率**:确保使用 Kover 达到 80% 以上的覆盖率 + +## 何时使用 + +在以下情况下使用 `/kotlin-test`: + +* 实现新的 Kotlin 函数或类 +* 为现有的 Kotlin 代码添加测试覆盖率 +* 修复错误(首先编写失败的测试) +* 构建关键业务逻辑 +* 学习 Kotlin 中的 TDD 工作流程 + +## TDD 循环 + +``` +RED -> Write failing Kotest test +GREEN -> Implement minimal code to pass +REFACTOR -> Improve code, tests stay green +REPEAT -> Next test case +``` + +## 示例会话 + +````text +User: /kotlin-test I need a function to validate user registration requests + +Agent: +# TDD Session: Registration Validator + +## Step 1: Define Interface + +```kotlin +// validator/RegistrationValidator.kt +package com.example.validator + +data class RegistrationRequest( + val name: String, + val email: String, + val password: String, +) + +sealed class ValidationResult { + data object Valid : ValidationResult() + data class Invalid(val errors: List) : ValidationResult() +} + +fun validateRegistration(request: RegistrationRequest): ValidationResult { + TODO("not implemented") +} +``` + +## Step 2: Write Kotest Tests (RED) + +```kotlin +// validator/RegistrationValidatorTest.kt +package com.example.validator + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf + +class RegistrationValidatorTest : FunSpec({ + test("valid registration returns Valid") { + val request = RegistrationRequest( + name = "Alice", + email = "alice@example.com", + password = "SecureP@ss1", + ) + + val result = validateRegistration(request) + + result.shouldBeInstanceOf() + } + + test("blank name returns Invalid") { + val request = RegistrationRequest( + name = "", + email = "alice@example.com", + password = "SecureP@ss1", + ) + + val result = validateRegistration(request) + + val invalid = result.shouldBeInstanceOf() + invalid.errors shouldBe listOf("Name is required") + } + + test("invalid email returns Invalid") { + val request = RegistrationRequest( + name = "Alice", + email = "not-an-email", + password = "SecureP@ss1", + ) + + val result = validateRegistration(request) + + val invalid = result.shouldBeInstanceOf() + invalid.errors shouldBe listOf("Invalid email format") + } + + test("short password returns Invalid") { + val request = RegistrationRequest( + name = "Alice", + email = "alice@example.com", + password = "short", + ) + + val result = validateRegistration(request) + + val invalid = result.shouldBeInstanceOf() + invalid.errors shouldBe listOf("Password must be at least 8 characters") + } + + test("multiple errors returns all errors") { + val request = RegistrationRequest( + name = "", + email = "bad", + password = "short", + ) + + val result = validateRegistration(request) + + val invalid = result.shouldBeInstanceOf() + invalid.errors.size shouldBe 3 + } +}) +``` + +## Step 3: Run Tests - Verify FAIL + +```bash +$ ./gradlew test + +RegistrationValidatorTest > valid registration returns Valid FAILED + kotlin.NotImplementedError: An operation is not implemented + +FAILED (5 tests, 0 passed, 5 failed) +``` + +✓ Tests fail as expected (NotImplementedError). + +## Step 4: Implement Minimal Code (GREEN) + +```kotlin +// validator/RegistrationValidator.kt +package com.example.validator + +private val EMAIL_REGEX = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") +private const val MIN_PASSWORD_LENGTH = 8 + +fun validateRegistration(request: RegistrationRequest): ValidationResult { + val errors = buildList { + if (request.name.isBlank()) add("Name is required") + if (!EMAIL_REGEX.matches(request.email)) add("Invalid email format") + if (request.password.length < MIN_PASSWORD_LENGTH) add("Password must be at least $MIN_PASSWORD_LENGTH characters") + } + + return if (errors.isEmpty()) ValidationResult.Valid + else ValidationResult.Invalid(errors) +} +``` + +## Step 5: Run Tests - Verify PASS + +```bash +$ ./gradlew test + +RegistrationValidatorTest > valid registration returns Valid PASSED +RegistrationValidatorTest > blank name returns Invalid PASSED +RegistrationValidatorTest > invalid email returns Invalid PASSED +RegistrationValidatorTest > short password returns Invalid PASSED +RegistrationValidatorTest > multiple errors returns all errors PASSED + +PASSED (5 tests, 5 passed, 0 failed) +``` + +✓ All tests passing! + +## Step 6: Check Coverage + +```bash +$ ./gradlew koverHtmlReport + +Coverage: 100.0% of statements +``` + +✓ Coverage: 100% + +## TDD Complete! +```` + +## 测试模式 + +### StringSpec(最简单) + +```kotlin +class CalculatorTest : StringSpec({ + "add two positive numbers" { + Calculator.add(2, 3) shouldBe 5 + } +}) +``` + +### BehaviorSpec(BDD) + +```kotlin +class OrderServiceTest : BehaviorSpec({ + Given("a valid order") { + When("placed") { + Then("should be confirmed") { /* ... */ } + } + } +}) +``` + +### 数据驱动测试 + +```kotlin +class ParserTest : FunSpec({ + context("valid inputs") { + withData("2026-01-15", "2026-12-31", "2000-01-01") { input -> + parseDate(input).shouldNotBeNull() + } + } +}) +``` + +### 协程测试 + +```kotlin +class AsyncServiceTest : FunSpec({ + test("concurrent fetch completes") { + runTest { + val result = service.fetchAll() + result.shouldNotBeEmpty() + } + } +}) +``` + +## 覆盖率命令 + +```bash +# Run tests with coverage +./gradlew koverHtmlReport + +# Verify coverage thresholds +./gradlew koverVerify + +# XML report for CI +./gradlew koverXmlReport + +# Open HTML report +open build/reports/kover/html/index.html + +# Run specific test class +./gradlew test --tests "com.example.UserServiceTest" + +# Run with verbose output +./gradlew test --info +``` + +## 覆盖率目标 + +| 代码类型 | 目标 | +|-----------|--------| +| 关键业务逻辑 | 100% | +| 公共 API | 90%+ | +| 通用代码 | 80%+ | +| 生成的代码 | 排除 | + +## TDD 最佳实践 + +**应做:** + +* 首先编写测试,在任何实现之前 +* 每次更改后运行测试 +* 使用 Kotest 匹配器进行表达性断言 +* 使用 MockK 的 `coEvery`/`coVerify` 来处理挂起函数 +* 测试行为,而非实现细节 +* 包含边界情况(空值、null、最大值) + +**不应做:** + +* 在测试之前编写实现 +* 跳过 RED 阶段 +* 直接测试私有函数 +* 在协程测试中使用 `Thread.sleep()` +* 忽略不稳定的测试 + +## 相关命令 + +* `/kotlin-build` - 修复构建错误 +* `/kotlin-review` - 在实现后审查代码 +* `/verify` - 运行完整的验证循环 + +## 相关 + +* 技能:`skills/kotlin-testing/` +* 技能:`skills/tdd-workflow/` diff --git a/docs/zh-CN/commands/learn-eval.md b/docs/zh-CN/commands/learn-eval.md index 8eaae618..a837698b 100644 --- a/docs/zh-CN/commands/learn-eval.md +++ b/docs/zh-CN/commands/learn-eval.md @@ -1,10 +1,10 @@ --- -description: 从会话中提取可重用模式,在保存前自我评估质量,并确定正确的保存位置(全局与项目)。 +description: "从会话中提取可重用模式,在保存前自我评估质量,并确定正确的保存位置(全局与项目)。" --- # /learn-eval - 提取、评估、然后保存 -扩展 `/learn`,在写入任何技能文件之前加入质量门和保存位置决策。 +扩展 `/learn`,在编写任何技能文件之前,加入质量门控、保存位置决策和知识放置意识。 ## 提取内容 @@ -52,41 +52,66 @@ origin: auto-extracted [触发条件] ``` -5. **在保存前自我评估**,使用此评分标准: +5. **质量门控 — 清单 + 整体裁决** - | 维度 | 1 | 3 | 5 | - |-----------|---|---|---| - | 具体性 | 仅抽象原则,无代码示例 | 有代表性代码示例 | 包含所有使用模式的丰富示例 | - | 可操作性 | 不清楚要做什么 | 主要步骤可理解 | 立即可操作,涵盖边界情况 | - | 范围契合度 | 过于宽泛或过于狭窄 | 基本合适,存在一些边界模糊 | 名称、触发器和内容完美匹配 | - | 非冗余性 | 几乎与另一技能相同 | 存在一些重叠但有独特视角 | 完全独特的价值 | - | 覆盖率 | 仅涵盖目标任务的一小部分 | 涵盖主要情况,缺少常见变体 | 涵盖主要情况、边界情况和陷阱 | + ### 5a. 必需清单(通过实际阅读文件进行验证) - * 为每个维度评分 1–5 - * 如果任何维度评分为 1–2,改进草案并重新评分,直到所有维度 ≥ 3 - * 向用户展示评分表和最终草案 + 在评估草案**之前**,执行以下所有操作: -6. 请求用户确认: - * 展示:提议的保存路径 + 评分表 + 最终草案 - * 在写入前等待明确确认 + * \[ ] 使用关键字在 `~/.claude/skills/` 和相关项目的 `.claude/skills/` 文件中进行 grep 搜索,检查内容重叠 + * \[ ] 检查 MEMORY.md(项目级和全局级)以查找重叠内容 + * \[ ] 考虑是否追加到现有技能即可满足需求 + * \[ ] 确认这是一个可复用的模式,而非一次性修复 -7. 保存到确定的位置 + ### 5b. 整体裁决 -## 第 5 步的输出格式(评分表) + 综合清单结果和草案质量,然后选择**以下一项**: -| 维度 | 评分 | 理由 | -|-----------|-------|-----------| -| 具体性 | N/5 | ... | -| 可操作性 | N/5 | ... | -| 范围契合度 | N/5 | ... | -| 非冗余性 | N/5 | ... | -| 覆盖率 | N/5 | ... | -| **总计** | **N/25** | | + | 裁决 | 含义 | 下一步行动 | + |---------|---------|-------------| + | **保存** | 独特、具体、范围明确 | 进行到步骤 6 | + | **改进后保存** | 有价值但需要改进 | 列出改进项 → 修订 → 重新评估(一次) | + | **吸收到 \[X]** | 应追加到现有技能 | 显示目标技能和添加内容 → 步骤 6 | + | **放弃** | 琐碎、冗余或过于抽象 | 解释原因并停止 | + + **指导维度**(用于告知裁决,不进行评分): + + * **具体性和可操作性**:包含可立即使用的代码示例或命令 + * **范围契合度**:名称、触发条件和内容保持一致,并专注于单一模式 + * **独特性**:提供现有技能未涵盖的价值(基于清单结果) + * **可复用性**:在未来的会话中存在现实的触发场景 + +6. **裁决特定的确认流程** + + * **改进后保存**:呈现必需的改进项 + 修订后的草案 + 一次重新评估后的更新清单/裁决;如果修订后的裁决是**保存**,则在用户确认后保存,否则遵循新的裁决 + * **保存**:呈现保存路径 + 清单结果 + 1行裁决理由 + 完整草案 → 在用户确认后保存 + * **吸收到 \[X]**:呈现目标路径 + 添加内容(diff格式) + 清单结果 + 裁决理由 → 在用户确认后追加 + * **放弃**:仅显示清单结果 + 推理(无需确认) + +7. 保存 / 吸收到确定的位置 + +## 步骤 5 的输出格式 + +``` +### Checklist +- [x] skills/ grep: no overlap (or: overlap found → details) +- [x] MEMORY.md: no overlap (or: overlap found → details) +- [x] Existing skill append: new file appropriate (or: should append to [X]) +- [x] Reusability: confirmed (or: one-off → Drop) + +### Verdict: Save / Improve then Save / Absorb into [X] / Drop + +**Rationale:** (1-2 sentences explaining the verdict) +``` + +## 设计原理 + +此版本用基于清单的整体裁决系统取代了之前的 5 维度数字评分标准(具体性、可操作性、范围契合度、非冗余性、覆盖度,评分 1-5)。现代前沿模型(Opus 4.6+)具有强大的情境判断能力 —— 将丰富的定性信号强行压缩为数字评分会丢失细微差别,并可能产生误导性的总分。整体方法让模型自然地权衡所有因素,产生更准确的保存/放弃决策,同时明确的清单确保不会跳过任何关键检查。 ## 注意事项 * 不要提取琐碎的修复(拼写错误、简单的语法错误) * 不要提取一次性问题(特定的 API 中断等) -* 专注于能在未来会话中节省时间的模式 -* 保持技能聚焦 — 每个技能一个模式 -* 如果覆盖率评分低,在保存前添加相关变体 +* 专注于那些将在未来会话中节省时间的模式 +* 保持技能聚焦 —— 每个技能一个模式 +* 当裁决为“吸收”时,追加到现有技能,而不是创建新文件 diff --git a/docs/zh-CN/commands/multi-workflow.md b/docs/zh-CN/commands/multi-workflow.md index 2476b7de..ddde5608 100644 --- a/docs/zh-CN/commands/multi-workflow.md +++ b/docs/zh-CN/commands/multi-workflow.md @@ -104,6 +104,14 @@ TaskOutput({ task_id: "", block: true, timeout: 600000 }) 4. 当评分 < 7 或用户不批准时强制停止。 5. 需要时(例如确认/选择/批准)使用 `AskUserQuestion` 工具进行用户交互。 +## 何时使用外部编排 + +当工作必须拆分给需要隔离的 git 状态、独立终端或独立构建/测试执行的并行工作器时,请使用外部 tmux/工作树编排。对于轻量级分析、规划或审查(其中主会话是唯一的写入者),请使用进程内子代理。 + +```bash +node scripts/orchestrate-worktrees.js .claude/plan/workflow-e2e-test.json --execute +``` + *** ## 执行工作流程 diff --git a/docs/zh-CN/commands/orchestrate.md b/docs/zh-CN/commands/orchestrate.md index 56776703..eeb464fa 100644 --- a/docs/zh-CN/commands/orchestrate.md +++ b/docs/zh-CN/commands/orchestrate.md @@ -158,6 +158,61 @@ RECOMMENDATION ``` +对于使用独立 git worktree 的外部 tmux-pane 工作器,请使用 `node scripts/orchestrate-worktrees.js plan.json --execute`。内置的编排模式保持进程内运行;此辅助工具适用于长时间运行或跨测试框架的会话。 + +当工作器需要查看主检出目录中的脏文件或未跟踪的本地文件时,请在计划文件中添加 `seedPaths`。ECC 仅在 `git worktree add` 之后,将那些选定的路径覆盖到每个工作器的工作树中,这既能保持分支隔离,又能暴露正在处理的本地脚本、计划或文档。 + +```json +{ + "sessionName": "workflow-e2e", + "seedPaths": [ + "scripts/orchestrate-worktrees.js", + "scripts/lib/tmux-worktree-orchestrator.js", + ".claude/plan/workflow-e2e-test.json" + ], + "workers": [ + { "name": "docs", "task": "Update orchestration docs." } + ] +} +``` + +要导出实时 tmux/worktree 会话的控制平面快照,请运行: + +```bash +node scripts/orchestration-status.js .claude/plan/workflow-visual-proof.json +``` + +快照包含会话活动、tmux 窗格元数据、工作器状态、目标、已播种的覆盖层以及最近的交接摘要,均以 JSON 格式保存。 + +## 操作员指挥中心交接 + +当工作流跨越多个会话、工作树或 tmux 窗格时,请在最终交接内容中附加一个控制平面块: + +```markdown +控制平面 +------------- +会话: +- 活动会话 ID 或别名 +- 每个活动工作线程的分支 + 工作树路径 +- 适用时的 tmux 窗格或分离会话名称 + +差异: +- git 状态摘要 +- 已修改文件的 git diff --stat +- 合并/冲突风险说明 + +审批: +- 待处理的用户审批 +- 等待确认的受阻步骤 + +遥测: +- 最后活动时间戳或空闲信号 +- 预估的令牌或成本漂移 +- 由钩子或审查器引发的策略事件 +``` + +这使得规划者、实施者、审查者和循环工作器在操作员界面上保持清晰可辨。 + ## 参数 $ARGUMENTS: diff --git a/docs/zh-CN/commands/plan.md b/docs/zh-CN/commands/plan.md index b13822d5..544c50c4 100644 --- a/docs/zh-CN/commands/plan.md +++ b/docs/zh-CN/commands/plan.md @@ -112,4 +112,7 @@ Agent (planner): ## 相关代理 -此命令调用位于 `~/.claude/agents/planner.md` 的 `planner` 代理。 +此命令调用由 ECC 提供的 `planner` 代理。 + +对于手动安装,源文件位于: +`agents/planner.md` diff --git a/docs/zh-CN/commands/prompt-optimize.md b/docs/zh-CN/commands/prompt-optimize.md new file mode 100644 index 00000000..a6809aea --- /dev/null +++ b/docs/zh-CN/commands/prompt-optimize.md @@ -0,0 +1,37 @@ +--- +description: 分析一个草稿提示,输出一个经过优化、富含ECC的版本,准备粘贴并运行。不执行任务——仅输出咨询分析。 +--- + +# /prompt-optimize + +分析并优化以下提示语,以实现最大化的ECC杠杆效应。 + +## 你的任务 + +对下方用户的输入应用 **prompt-optimizer** 技能。遵循6阶段分析流程: + +0. **项目检测** — 读取 CLAUDE.md,从项目文件(package.json, go.mod, pyproject.toml 等)检测技术栈 +1. **意图检测** — 对任务类型进行分类(新功能、错误修复、重构、研究、测试、评审、文档、基础设施、设计) +2. **范围评估** — 评估复杂度(简单 / 低 / 中 / 高 / 史诗级),如果检测到代码库,则使用其大小作为信号 +3. **ECC组件匹配** — 映射到特定的技能、命令、代理和模型层级 +4. **缺失上下文检测** — 识别信息缺口。如果缺少3个以上关键项,请在生成前请用户澄清 +5. **工作流与模型** — 确定生命周期阶段,推荐模型层级,如果复杂度为高/史诗级,则将其拆分为多个提示语 + +## 输出要求 + +* 呈现诊断结果、推荐的ECC组件以及使用 prompt-optimizer 技能中输出格式的优化后提示语 +* 提供 **完整版本**(详细)和 **快速版本**(紧凑,根据意图类型变化) +* 使用与用户输入相同的语言进行回复 +* 优化后的提示语必须完整且可复制粘贴到新会话中直接使用 +* 以提供调整选项或明确下一步操作(用于启动单独的执行请求)的页脚结束 + +## 关键 + +请勿执行用户的任务。仅输出分析结果和优化后的提示语。 +如果用户要求直接执行,请说明 `/prompt-optimize` 仅产生咨询性输出,并告诉他们应启动一个常规的任务请求。 + +注意:`blueprint` 是一个**技能**,而非斜杠命令。请写作“使用蓝图技能”,而不是将其呈现为 `/...` 命令。 + +## 用户输入 + +$ARGUMENTS diff --git a/docs/zh-CN/commands/python-review.md b/docs/zh-CN/commands/python-review.md index 563db678..82df0db0 100644 --- a/docs/zh-CN/commands/python-review.md +++ b/docs/zh-CN/commands/python-review.md @@ -315,6 +315,6 @@ result = "".join(str(item) for item in items) | 海象运算符 (`:=`) | 3.8+ | | 仅限位置参数 | 3.8+ | | Match 语句 | 3.10+ | -| 类型联合 (\`x \| None\`) | 3.10+ | +| 类型联合 (\`x | None\`) | 3.10+ | 确保你的项目 `pyproject.toml` 或 `setup.py` 指定了正确的最低 Python 版本。 diff --git a/docs/zh-CN/commands/resume-session.md b/docs/zh-CN/commands/resume-session.md new file mode 100644 index 00000000..f867fb73 --- /dev/null +++ b/docs/zh-CN/commands/resume-session.md @@ -0,0 +1,155 @@ +--- +description: 从 ~/.claude/sessions/ 加载最新的会话文件,并从上次会话结束的地方恢复工作,保留完整上下文。 +--- + +# 恢复会话命令 + +加载最后保存的会话状态,并在开始任何工作前完全熟悉情况。 +此命令是 `/save-session` 的对应命令。 + +## 何时使用 + +* 开始新会话以继续前一天的工作时 +* 因上下文限制而开始全新会话后 +* 当从其他来源移交会话文件时(只需提供文件路径) +* 任何拥有会话文件并希望 Claude 在继续前完全吸收其内容的时候 + +## 用法 + +``` +/resume-session # loads most recent file in ~/.claude/sessions/ +/resume-session 2024-01-15 # loads most recent session for that date +/resume-session ~/.claude/sessions/2024-01-15-session.tmp # loads a specific legacy-format file +/resume-session ~/.claude/sessions/2024-01-15-abc123de-session.tmp # loads a current short-id session file +``` + +## 流程 + +### 步骤 1:查找会话文件 + +如果未提供参数: + +1. 检查 `~/.claude/sessions/` +2. 选择最近修改的 `*-session.tmp` 文件 +3. 如果文件夹不存在或没有匹配的文件,告知用户: + ``` + 在 ~/.claude/sessions/ 中未找到会话文件。 + 请在会话结束时运行 /save-session 来创建一个。 + ``` + 然后停止。 + +如果提供了参数: + +* 如果看起来像日期 (`YYYY-MM-DD`),则在 `~/.claude/sessions/` 中搜索匹配 + `YYYY-MM-DD-session.tmp`(旧格式)或 `YYYY-MM-DD--session.tmp`(当前格式)的文件, + 并加载该日期最近修改的版本 +* 如果看起来像文件路径,则直接读取该文件 +* 如果未找到,清晰报告并停止 + +### 步骤 2:读取整个会话文件 + +读取完整的文件。暂时不要总结。 + +### 步骤 3:确认理解 + +使用以下确切格式回复一份结构化简报: + +``` +SESSION LOADED: [actual resolved path to the file] +════════════════════════════════════════════════ + +PROJECT: [project name / topic from file] + +WHAT WE'RE BUILDING: +[2-3 sentence summary in your own words] + +CURRENT STATE: +✅ Working: [count] items confirmed +🔄 In Progress: [list files that are in progress] +🗒️ Not Started: [list planned but untouched] + +WHAT NOT TO RETRY: +[list every failed approach with its reason — this is critical] + +OPEN QUESTIONS / BLOCKERS: +[list any blockers or unanswered questions] + +NEXT STEP: +[exact next step if defined in the file] +[if not defined: "No next step defined — recommend reviewing 'What Has NOT Been Tried Yet' together before starting"] + +════════════════════════════════════════════════ +Ready to continue. What would you like to do? +``` + +### 步骤 4:等待用户 + +请**不要**自动开始工作。请**不要**触碰任何文件。等待用户指示下一步做什么。 + +如果会话文件中明确定义了下一步,并且用户说"继续"或"是"或类似内容 — 则执行该确切步骤。 + +如果未定义下一步 — 询问用户从哪里开始,并可选择性地从"尚未尝试的内容"部分提出建议。 + +*** + +## 边界情况 + +**同一日期有多个会话** (`2024-01-15-session.tmp`, `2024-01-15-abc123de-session.tmp`): +加载该日期最近修改的匹配文件,无论其使用的是旧的无ID格式还是当前的短ID格式。 + +**会话文件引用了已不存在的文件:** +在简报中注明 — "⚠️ 会话中引用了 `path/to/file.ts`,但在磁盘上未找到。" + +**会话文件来自超过7天前:** +注明时间间隔 — "⚠️ 此会话来自 N 天前(阈值:7天)。情况可能已发生变化。" — 然后正常继续。 + +**用户直接提供了文件路径(例如,从队友处转发而来):** +读取它并遵循相同的简报流程 — 无论来源如何,格式都是相同的。 + +**会话文件为空或格式错误:** +报告:"找到会话文件,但似乎为空或无法读取。您可能需要使用 /save-session 创建一个新的。" + +*** + +## 示例输出 + +``` +SESSION LOADED: /Users/you/.claude/sessions/2024-01-15-abc123de-session.tmp +════════════════════════════════════════════════ + +PROJECT: my-app — JWT Authentication + +WHAT WE'RE BUILDING: +User authentication with JWT tokens stored in httpOnly cookies. +Register and login endpoints are partially done. Route protection +via middleware hasn't been started yet. + +CURRENT STATE: +✅ Working: 3 items (register endpoint, JWT generation, password hashing) +🔄 In Progress: app/api/auth/login/route.ts (token works, cookie not set yet) +🗒️ Not Started: middleware.ts, app/login/page.tsx + +WHAT NOT TO RETRY: +❌ Next-Auth — conflicts with custom Prisma adapter, threw adapter error on every request +❌ localStorage for JWT — causes SSR hydration mismatch, incompatible with Next.js + +OPEN QUESTIONS / BLOCKERS: +- Does cookies().set() work inside a Route Handler or only Server Actions? + +NEXT STEP: +In app/api/auth/login/route.ts — set the JWT as an httpOnly cookie using +cookies().set('token', jwt, { httpOnly: true, secure: true, sameSite: 'strict' }) +then test with Postman for a Set-Cookie header in the response. + +════════════════════════════════════════════════ +Ready to continue. What would you like to do? +``` + +*** + +## 注意事项 + +* 加载时切勿修改会话文件 — 它是一个只读的历史记录 +* 简报格式是固定的 — 即使某些部分为空,也不要跳过 +* "不应重试的内容"必须始终显示,即使它只是说"无" — 这太重要了,不容遗漏 +* 恢复后,用户可能希望在新的会话结束时再次运行 `/save-session`,以创建一个新的带日期文件 diff --git a/docs/zh-CN/commands/save-session.md b/docs/zh-CN/commands/save-session.md new file mode 100644 index 00000000..6018e1aa --- /dev/null +++ b/docs/zh-CN/commands/save-session.md @@ -0,0 +1,252 @@ +--- +description: 将当前会话状态保存到 ~/.claude/sessions/ 目录下带日期的文件中,以便在未来的会话中恢复完整上下文并继续工作。 +--- + +# 保存会话命令 + +捕获本次会话中发生的一切——构建了什么、什么成功了、什么失败了、还有哪些遗留事项——并将其写入一个带日期的文件,以便下次会话能从此处继续。 + +## 使用时机 + +* 在关闭 Claude Code 之前,工作会话结束时 +* 在达到上下文限制之前(先运行此命令,然后开始一个新会话) +* 解决了一个想要记住的复杂问题之后 +* 任何需要将上下文移交给未来会话的时候 + +## 流程 + +### 步骤 1:收集上下文 + +在写入文件之前,收集: + +* 读取本次会话期间修改的所有文件(使用 git diff 或从对话中回忆) +* 回顾讨论、尝试和决定的内容 +* 记录遇到的任何错误及其解决方法(或未解决的情况) +* 如果相关,检查当前的测试/构建状态 + +### 步骤 2:如果不存在则创建会话文件夹 + +在用户的 Claude 主目录中创建规范的会话文件夹: + +```bash +mkdir -p ~/.claude/sessions +``` + +### 步骤 3:写入会话文件 + +创建 `~/.claude/sessions/YYYY-MM-DD--session.tmp`,使用今天的实际日期和一个满足 `session-manager.js` 中 `SESSION_FILENAME_REGEX` 强制规则的短 ID: + +* 允许的字符:小写 `a-z`,数字 `0-9`,连字符 `-` +* 最小长度:8 个字符 +* 不允许大写字母、下划线、空格 + +有效示例:`abc123de`、`a1b2c3d4`、`frontend-worktree-1` +无效示例:`ABC123de`(大写)、`short`(少于 8 个字符)、`test_id1`(下划线) + +完整有效文件名示例:`2024-01-15-abc123de-session.tmp` + +旧文件名 `YYYY-MM-DD-session.tmp` 仍然有效,但新的会话文件应首选短 ID 形式,以避免同一天的冲突。 + +### 步骤 4:用以下所有部分填充文件 + +诚实地写入每个部分。不要跳过任何部分——如果某个部分确实没有内容,则写“Nothing yet”或“N/A”。一个不完整的文件比诚实的空部分更糟糕。 + +### 步骤 5:向用户展示文件 + +写入后,显示完整内容并询问: + +``` +Session saved to [actual resolved path to the session file] + +Does this look accurate? Anything to correct or add before we close? +``` + +等待确认。如果用户要求,进行编辑。 + +*** + +## 会话文件格式 + +```markdown +# 会话:YYYY-MM-DD + +**开始时间:** [若已知大致时间] +**最后更新:** [当前时间] +**项目:** [项目名称或路径] +**主题:** [关于本次会话的一行摘要] + +--- + +## 正在构建的内容 + +[1-3段文字,描述功能、错误修复或任务。包含足够的背景信息,让对此会话毫无记忆的人也能理解目标。包含:它做什么、为什么需要它、它如何融入更大的系统。] + +--- + +## 已确认有效的工作(附证据) + +[仅列出已确认有效的事项。对于每个事项,说明你如何知道它有效——测试通过、在浏览器中运行、Postman 返回 200 等。没有证据的,请移至"尚未尝试"部分。] + +- **[有效的事项]** — 确认依据:[具体证据] +- **[有效的事项]** — 确认依据:[具体证据] + +如果尚无任何事项确认有效:"尚无确认有效的事项——所有方法仍在进行中或未测试。" + +--- + +## 无效的事项(及原因) + +[这是最重要的部分。列出所有尝试过但失败的方法。对于每个失败,写出确切原因,以便下次会话不再重试。要具体:"因 Y 而抛出 X 错误"是有用的。"无效"是无用的。] + +- **[尝试过的方法]** — 失败原因:[确切原因 / 错误信息] +- **[尝试过的方法]** — 失败原因:[确切原因 / 错误信息] + +如果无失败事项:"尚无失败的方法。" + +--- + +## 尚未尝试的事项 + +[看起来有希望但尚未尝试的方法。对话中产生的想法。值得探索的替代方案。描述要足够具体,以便下次会话确切知道要尝试什么。] + +- [方法 / 想法] +- [方法 / 想法] + +如果无待办事项:"未确定具体的待尝试方法。" + +--- + +## 文件当前状态 + +[本次会话中修改过的每个文件。准确说明每个文件的状态。] + +| 文件 | 状态 | 备注 | +| ----------------- | -------------- | ---------------------------- | +| `path/to/file.ts` | ✅ 完成 | [其作用] | +| `path/to/file.ts` | 🔄 进行中 | [已完成什么,剩余什么] | +| `path/to/file.ts` | ❌ 损坏 | [问题所在] | +| `path/to/file.ts` | 🗒️ 未开始 | [计划但尚未接触] | + +如果未修改任何文件:"本次会话未修改任何文件。" + +--- + +## 已作出的决策 + +[架构选择、接受的权衡、选择的方法及其原因。这些可防止下次会话重新讨论已确定的决策。] + +- **[决策]** — 原因:[选择此方案而非其他方案的原因] + +如果无重大决策:"本次会话未作出重大决策。" + +--- + +## 阻碍与待解决问题 + +[任何未解决、需要下次会话处理或调查的事项。出现但未解答的问题。等待中的外部依赖。] + +- [阻碍 / 待解决问题] + +如果无:"无当前阻碍。" + +--- + +## 确切下一步 + +[若已知:恢复工作时最重要的单件事项。描述要足够精确,使得恢复工作时无需思考从何处开始。] + +[若未知:"下一步未确定——在开始前,请查看'尚未尝试的事项'和'阻碍'部分以决定方向。"] + +--- + +## 环境与设置说明 + +[仅在相关时填写——运行项目所需的命令、所需的环境变量、需要运行的服务等。若为标准设置,请跳过。] + +[若无:请完全省略此部分。] +``` + +*** + +## 示例输出 + +```markdown +# 会话:2024-01-15 + +**开始时间:** ~下午2点 +**最后更新:** 下午5:30 +**项目:** my-app +**主题:** 使用 httpOnly cookies 构建 JWT 认证 + +--- + +## 我们正在构建什么 + +为 Next.js 应用构建用户认证系统。用户使用电子邮件/密码注册,收到存储在 httpOnly cookie(而非 localStorage)中的 JWT,受保护的路由通过中间件检查有效的令牌。目标是在浏览器刷新时保持会话持久性,同时不将令牌暴露给 JavaScript。 + +--- + +## 哪些工作有效(附证据) + +- **`/api/auth/register` 端点** — 确认依据:Postman POST 请求返回 200 并包含用户对象,Supabase 仪表板中可见行记录,bcrypt 哈希正确存储 +- **在 `lib/auth.ts` 中生成 JWT** — 确认依据:单元测试通过 (`npm test -- auth.test.ts`),在 jwt.io 解码的令牌显示正确的负载 +- **密码哈希** — 确认依据:`bcrypt.compare()` 在测试中返回 true + +--- + +## 哪些工作无效(及原因) + +- **Next-Auth 库** — 失败原因:与我们的自定义 Prisma 适配器冲突,每次请求都抛出“无法在此配置中将适配器与凭据提供程序一起使用”。不值得调试 — 对我们的设置来说过于固执己见。 +- **将 JWT 存储在 localStorage 中** — 失败原因:SSR 渲染发生在 localStorage 可用之前,导致每次页面加载都出现 React 水合不匹配错误。此方法从根本上与 Next.js SSR 不兼容。 + +--- + +## 尚未尝试的事项 + +- 在登录路由响应中将 JWT 存储为 httpOnly cookie(最可能的解决方案) +- 使用 `cookies()` 从 `next/headers` 中读取服务器组件中的令牌 +- 编写 middleware.ts 通过检查 cookie 是否存在来保护路由 + +--- + +## 文件当前状态 + +| 文件 | 状态 | 备注 | +| -------------------------------- | -------------- | ----------------------------------------------- | +| `app/api/auth/register/route.ts` | ✅ 已完成 | 工作正常,已测试 | +| `app/api/auth/login/route.ts` | 🔄 进行中 | 令牌已生成但尚未设置 cookie | +| `lib/auth.ts` | ✅ 已完成 | JWT 辅助函数,全部已测试 | +| `middleware.ts` | 🗒️ 未开始 | 路由保护,需要先实现 cookie 读取逻辑 | +| `app/login/page.tsx` | 🗒️ 未开始 | UI 尚未开始 | + +--- + +## 已做出的决策 + +- **选择 httpOnly cookie 而非 localStorage** — 原因:防止 XSS 令牌窃取,与 SSR 兼容 +- **选择自定义认证而非 Next-Auth** — 原因:Next-Auth 与我们的 Prisma 设置冲突,不值得折腾 + +--- + +## 阻碍与未决问题 + +- `cookies().set()` 在路由处理器中有效,还是仅在服务器操作中有效?需要验证。 + +--- + +## 确切下一步 + +在 `app/api/auth/login/route.ts` 中,生成 JWT 后,使用 `cookies().set('token', jwt, { httpOnly: true, secure: true, sameSite: 'strict' })` 将其设置为 httpOnly cookie。 +然后用 Postman 测试 — 响应应包含一个 `Set-Cookie` 头。 +``` + +*** + +## 注意事项 + +* 每个会话都有其自己的文件——切勿追加到先前会话的文件中 +* “什么没有成功”部分是最关键的——没有它,未来的会话将盲目地重试失败的方法 +* 如果用户要求中途保存会话(而不仅仅是在结束时),则保存目前已知的内容,并清楚地标记进行中的项目 +* 该文件旨在通过 `/resume-session` 在下次会话开始时由 Claude 读取 +* 使用规范的全局会话存储:`~/.claude/sessions/` +* 对于任何新的会话文件,首选短 ID 文件名形式(`YYYY-MM-DD--session.tmp`) diff --git a/docs/zh-CN/commands/sessions.md b/docs/zh-CN/commands/sessions.md index 6f07eb94..796af1dc 100644 --- a/docs/zh-CN/commands/sessions.md +++ b/docs/zh-CN/commands/sessions.md @@ -12,6 +12,8 @@ 显示所有会话及其元数据,支持筛选和分页。 +当您需要群组的操作员表层上下文时,使用 `/sessions info`:分支、工作树路径和会话最近性。 + ```bash /sessions # List all sessions (default) /sessions list # Same as above @@ -26,6 +28,7 @@ node -e " const sm = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-manager'); const aa = require((process.env.CLAUDE_PLUGIN_ROOT||require('path').join(require('os').homedir(),'.claude'))+'/scripts/lib/session-aliases'); +const path = require('path'); const result = sm.getAllSessions({ limit: 20 }); const aliases = aa.listAliases(); @@ -34,17 +37,18 @@ for (const a of aliases) aliasMap[a.sessionPath] = a.name; console.log('Sessions (showing ' + result.sessions.length + ' of ' + result.total + '):'); console.log(''); -console.log('ID Date Time Size Lines Alias'); -console.log('────────────────────────────────────────────────────'); +console.log('ID Date Time Branch Worktree Alias'); +console.log('────────────────────────────────────────────────────────────────────'); for (const s of result.sessions) { const alias = aliasMap[s.filename] || ''; - const size = sm.getSessionSize(s.sessionPath); - const stats = sm.getSessionStats(s.sessionPath); + const metadata = sm.parseSessionMetadata(sm.getSessionContent(s.sessionPath)); const id = s.shortId === 'no-id' ? '(none)' : s.shortId.slice(0, 8); const time = s.modifiedTime.toTimeString().slice(0, 5); + const branch = (metadata.branch || '-').slice(0, 12); + const worktree = metadata.worktree ? path.basename(metadata.worktree).slice(0, 18) : '-'; - console.log(id.padEnd(8) + ' ' + s.date + ' ' + time + ' ' + size.padEnd(7) + ' ' + String(stats.lineCount).padEnd(5) + ' ' + alias); + console.log(id.padEnd(8) + ' ' + s.date + ' ' + time + ' ' + branch.padEnd(12) + ' ' + worktree.padEnd(18) + ' ' + alias); } " ``` @@ -110,6 +114,18 @@ if (session.metadata.started) { if (session.metadata.lastUpdated) { console.log('Last Updated: ' + session.metadata.lastUpdated); } + +if (session.metadata.project) { + console.log('Project: ' + session.metadata.project); +} + +if (session.metadata.branch) { + console.log('Branch: ' + session.metadata.branch); +} + +if (session.metadata.worktree) { + console.log('Worktree: ' + session.metadata.worktree); +} " "$ARGUMENTS" ``` @@ -220,6 +236,9 @@ console.log('ID: ' + (session.shortId === 'no-id' ? '(none)' : session. console.log('Filename: ' + session.filename); console.log('Date: ' + session.date); console.log('Modified: ' + session.modifiedTime.toISOString().slice(0, 19).replace('T', ' ')); +console.log('Project: ' + (session.metadata.project || '-')); +console.log('Branch: ' + (session.metadata.branch || '-')); +console.log('Worktree: ' + (session.metadata.worktree || '-')); console.log(''); console.log('Content:'); console.log(' Lines: ' + stats.lineCount); @@ -241,6 +260,11 @@ if (aliases.length > 0) { /sessions aliases # List all aliases ``` +## 操作员笔记 + +* 会话文件在头部持久化 `Project`、`Branch` 和 `Worktree`,以便 `/sessions info` 可以区分并行 tmux/工作树运行。 +* 对于指挥中心式监控,请结合使用 `/sessions info`、`git diff --stat` 以及由 `scripts/hooks/cost-tracker.js` 发出的成本指标。 + **脚本:** ```bash diff --git a/docs/zh-CN/commands/tdd.md b/docs/zh-CN/commands/tdd.md index 8a4cf1ca..001210db 100644 --- a/docs/zh-CN/commands/tdd.md +++ b/docs/zh-CN/commands/tdd.md @@ -321,10 +321,12 @@ Never skip the RED phase. Never write code before tests. ## Related Agents -This command invokes the `tdd-guide` agent located at: -`~/.claude/agents/tdd-guide.md` +This command invokes the `tdd-guide` agent provided by ECC. -And can reference the `tdd-workflow` skill at: -`~/.claude/skills/tdd-workflow/` +The related `tdd-workflow` skill is also bundled with ECC. + +For manual installs, the source files live at: +- `agents/tdd-guide.md` +- `skills/tdd-workflow/SKILL.md` ``` diff --git a/docs/zh-CN/examples/user-CLAUDE.md b/docs/zh-CN/examples/user-CLAUDE.md index 190a34ff..794e978b 100644 --- a/docs/zh-CN/examples/user-CLAUDE.md +++ b/docs/zh-CN/examples/user-CLAUDE.md @@ -85,6 +85,13 @@ * 最低 80% 覆盖率 * 关键流程使用单元测试 + 集成测试 + E2E 测试 +### 知识捕获 + +* 个人调试笔记、偏好和临时上下文 → 自动记忆 +* 团队/项目知识(架构决策、API变更、实施操作手册) → 遵循项目现有的文档结构 +* 如果当前任务已生成相关文档、注释或示例,请勿在其他地方重复记录相同知识 +* 如果没有明显的项目文档位置,请在创建新的顶层文档前进行询问 + *** ## 编辑器集成 diff --git a/docs/zh-CN/hooks/README.md b/docs/zh-CN/hooks/README.md index 912ae1f5..a808be5a 100644 --- a/docs/zh-CN/hooks/README.md +++ b/docs/zh-CN/hooks/README.md @@ -20,11 +20,12 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes | 钩子 | 匹配器 | 行为 | 退出码 | |------|---------|----------|-----------| -| **开发服务器阻止器** | `Bash` | 在 tmux 外部阻止 `npm run dev` 等命令 —— 确保日志访问 | 2 (阻止) | -| **Tmux 提醒** | `Bash` | 建议对长时间运行的命令(npm test, cargo build, docker)使用 tmux | 0 (警告) | -| **Git push 提醒** | `Bash` | 提醒在 `git push` 前审查更改 | 0 (警告) | -| **文档文件警告** | `Write` | 警告非标准的 `.md`/`.txt` 文件(允许 README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/);跨平台路径处理 | 0 (警告) | -| **策略性压缩** | `Edit\|Write` | 建议在逻辑间隔(约每 50 次工具调用)手动执行 `/compact` | 0 (警告) | +| **开发服务器拦截器** | `Bash` | 在 tmux 外阻止 `npm run dev` 等命令 — 确保日志可访问 | 2 (拦截) | +| **Tmux 提醒器** | `Bash` | 对长时间运行命令(npm test、cargo build、docker)建议使用 tmux | 0 (警告) | +| **Git 推送提醒器** | `Bash` | 在 `git push` 前提醒检查变更 | 0 (警告) | +| **文档文件警告器** | `Write` | 对非标准 `.md`/`.txt` 文件发出警告(允许 README、CLAUDE、CONTRIBUTING、CHANGELOG、LICENSE、SKILL、docs/、skills/);跨平台路径处理 | 0 (警告) | +| **策略性压缩提醒器** | `Edit\|Write` | 建议在逻辑间隔(约每 50 次工具调用)手动执行 `/compact` | 0 (警告) | +| **InsAIts 安全监控器(可选加入)** | `Bash\|Write\|Edit\|MultiEdit` | 对高信号工具输入的可选安全扫描。除非设置 `ECC_ENABLE_INSAITS=1`,否则禁用。对关键发现进行拦截,对非关键发现发出警告,并将审计日志写入 `.insaits_audit_session.jsonl`。需要 `pip install insa-its`。[详情](../../../scripts/hooks/insaits-security-monitor.py) | 2 (拦截关键) / 0 (警告) | ### PostToolUse 钩子 diff --git a/docs/zh-CN/rules/README.md b/docs/zh-CN/rules/README.md index a1c010aa..e8efdfed 100644 --- a/docs/zh-CN/rules/README.md +++ b/docs/zh-CN/rules/README.md @@ -18,7 +18,8 @@ rules/ ├── typescript/ # TypeScript/JavaScript specific ├── python/ # Python specific ├── golang/ # Go specific -└── swift/ # Swift specific +├── swift/ # Swift specific +└── php/ # PHP specific ``` * **common/** 包含通用原则 —— 没有语言特定的代码示例。 @@ -34,6 +35,7 @@ rules/ ./install.sh python ./install.sh golang ./install.sh swift +./install.sh php # Install multiple languages at once ./install.sh typescript python @@ -54,6 +56,7 @@ cp -r rules/typescript ~/.claude/rules/typescript cp -r rules/python ~/.claude/rules/python cp -r rules/golang ~/.claude/rules/golang cp -r rules/swift ~/.claude/rules/swift +cp -r rules/php ~/.claude/rules/php # Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only. ``` @@ -87,7 +90,7 @@ cp -r rules/swift ~/.claude/rules/swift 当语言特定规则与通用规则冲突时,**语言特定规则优先**(具体规则覆盖通用规则)。这遵循标准的分层配置模式(类似于 CSS 特异性或 `.gitignore` 优先级)。 * `rules/common/` 定义了适用于所有项目的通用默认值。 -* `rules/golang/`、`rules/python/`、`rules/typescript/` 等在语言习惯用法不同的地方会覆盖这些默认值。 +* `rules/golang/`、`rules/python/`、`rules/swift/`、`rules/php/`、`rules/typescript/` 等会在语言习惯不同时覆盖这些默认值。 ### 示例 diff --git a/docs/zh-CN/rules/common/development-workflow.md b/docs/zh-CN/rules/common/development-workflow.md index 2a427255..06dcf72e 100644 --- a/docs/zh-CN/rules/common/development-workflow.md +++ b/docs/zh-CN/rules/common/development-workflow.md @@ -6,32 +6,33 @@ ## 功能实现工作流程 -0. **研究与复用** *(任何新实现之前强制进行)* - * **首先进行 GitHub 代码搜索:** 在编写任何新内容之前,运行 `gh search repos` 和 `gh search code` 以查找现有的实现、模板和模式。 - * **使用 Exa MCP 进行研究:** 在规划阶段使用 `exa-web-search` MCP 进行更广泛的研究、数据摄取和发现现有技术。 - * **检查包注册表:** 在编写工具代码之前,搜索 npm、PyPI、crates.io 和其他注册表。优先选择经过实战检验的库,而不是自己编写的解决方案。 - * **搜索可适配的实现:** 寻找能够解决 80% 以上问题并且可以分叉、移植或包装的开源项目。 - * 当满足要求时,优先采用或移植经过验证的方法,而不是编写全新的代码。 +0. **研究与复用** *(任何新实现前必须执行)* + * **优先进行 GitHub 代码搜索:** 在编写任何新代码之前,先运行 `gh search repos` 和 `gh search code` 以查找现有的实现、模板和模式。 + * **其次查阅库文档:** 在实现之前,使用 Context7 或主要供应商文档来确认 API 行为、包的使用以及版本特定的细节。 + * **仅在以上两者不足时使用 Exa:** 在 GitHub 搜索和主要文档之后,再使用 Exa 进行更广泛的网络研究或探索。 + * **检查包注册中心:** 在编写工具代码之前,先搜索 npm、PyPI、crates.io 和其他注册中心。优先选择经过实战检验的库,而不是自己动手实现。 + * **寻找可适配的实现:** 寻找能解决 80% 以上问题的开源项目,以便进行分叉、移植或封装。 + * 如果经过验证的方法能满足需求,优先采用或移植该方法,而不是编写全新的代码。 1. **先规划** - * 使用 **planner** 代理创建实施计划 - * 在编码前生成规划文档:PRD、架构、系统设计、技术文档、任务列表 + * 使用 **planner** 智能体来创建实施计划 + * 编码前生成规划文档:PRD、架构、系统设计、技术文档、任务列表 * 识别依赖项和风险 * 分解为多个阶段 2. **TDD 方法** - * 使用 **tdd-guide** 代理 - * 先写测试 (RED) - * 实现以通过测试 (GREEN) - * 重构 (IMPROVE) - * 验证 80%+ 的覆盖率 + * 使用 **tdd-guide** 智能体 + * 先编写测试(RED) + * 实现代码以通过测试(GREEN) + * 重构(IMPROVE) + * 验证 80% 以上的覆盖率 3. **代码审查** - * 编写代码后立即使用 **code-reviewer** 代理 - * 处理 CRITICAL 和 HIGH 级别的问题 + * 编写代码后立即使用 **code-reviewer** 智能体 + * 解决 CRITICAL 和 HIGH 级别的问题 * 尽可能修复 MEDIUM 级别的问题 4. **提交与推送** * 详细的提交信息 * 遵循约定式提交格式 - * 关于提交信息格式和 PR 流程,请参阅 [git-workflow.md](git-workflow.md) + * 提交信息格式和 PR 流程请参阅 [git-workflow.md](git-workflow.md) diff --git a/docs/zh-CN/rules/kotlin/coding-style.md b/docs/zh-CN/rules/kotlin/coding-style.md new file mode 100644 index 00000000..e39fbb81 --- /dev/null +++ b/docs/zh-CN/rules/kotlin/coding-style.md @@ -0,0 +1,90 @@ +--- +paths: + - "**/*.kt" + - "**/*.kts" +--- + +# Kotlin 编码风格 + +> 本文档在 [common/coding-style.md](../common/coding-style.md) 的基础上扩展了 Kotlin 相关内容。 + +## 格式化 + +* 使用 **ktlint** 或 **Detekt** 进行风格检查 +* 遵循官方 Kotlin 代码风格 (`kotlin.code.style=official` 在 `gradle.properties` 中) + +## 不可变性 + +* 优先使用 `val` 而非 `var` — 默认使用 `val`,仅在需要可变性时使用 `var` +* 对值类型使用 `data class`;在公共 API 中使用不可变集合 (`List`, `Map`, `Set`) +* 状态更新使用写时复制:`state.copy(field = newValue)` + +## 命名 + +遵循 Kotlin 约定: + +* 函数和属性使用 `camelCase` +* 类、接口、对象和类型别名使用 `PascalCase` +* 常量 (`const val` 或 `@JvmStatic`) 使用 `SCREAMING_SNAKE_CASE` +* 接口以行为而非 `I` 为前缀:使用 `Clickable` 而非 `IClickable` + +## 空安全 + +* 绝不使用 `!!` — 优先使用 `?.`, `?:`, `requireNotNull()` 或 `checkNotNull()` +* 使用 `?.let {}` 进行作用域内的空安全操作 +* 对于确实可能没有结果的函数,返回可为空的类型 + +```kotlin +// BAD +val name = user!!.name + +// GOOD +val name = user?.name ?: "Unknown" +val name = requireNotNull(user) { "User must be set before accessing name" }.name +``` + +## 密封类型 + +使用密封类/接口来建模封闭的状态层次结构: + +```kotlin +sealed interface UiState { + data object Loading : UiState + data class Success(val data: T) : UiState + data class Error(val message: String) : UiState +} +``` + +对密封类型始终使用详尽的 `when` — 不要使用 `else` 分支。 + +## 扩展函数 + +使用扩展函数实现工具操作,但要确保其可发现性: + +* 放在以接收者类型命名的文件中 (`StringExt.kt`, `FlowExt.kt`) +* 限制作用域 — 不要向 `Any` 或过于泛化的类型添加扩展 + +## 作用域函数 + +使用合适的作用域函数: + +* `let` — 空检查并转换:`user?.let { greet(it) }` +* `run` — 使用接收者计算结果:`service.run { fetch(config) }` +* `apply` — 配置对象:`builder.apply { timeout = 30 }` +* `also` — 副作用:`result.also { log(it) }` +* 避免深度嵌套作用域函数(最多 2 层) + +## 错误处理 + +* 使用 `Result` 或自定义密封类型 +* 使用 `runCatching {}` 包装可能抛出异常的代码 +* 绝不捕获 `CancellationException` — 始终重新抛出它 +* 避免使用 `try-catch` 进行控制流 + +```kotlin +// BAD — using exceptions for control flow +val user = try { repository.getUser(id) } catch (e: NotFoundException) { null } + +// GOOD — nullable return +val user: User? = repository.findUser(id) +``` diff --git a/docs/zh-CN/rules/kotlin/hooks.md b/docs/zh-CN/rules/kotlin/hooks.md new file mode 100644 index 00000000..17dc1929 --- /dev/null +++ b/docs/zh-CN/rules/kotlin/hooks.md @@ -0,0 +1,18 @@ +--- +paths: + - "**/*.kt" + - "**/*.kts" + - "**/build.gradle.kts" +--- + +# Kotlin Hooks + +> 此文件在 [common/hooks.md](../common/hooks.md) 的基础上扩展了 Kotlin 相关内容。 + +## PostToolUse Hooks + +在 `~/.claude/settings.json` 中配置: + +* **ktfmt/ktlint**: 在编辑后自动格式化 `.kt` 和 `.kts` 文件 +* **detekt**: 在编辑 Kotlin 文件后运行静态分析 +* **./gradlew build**: 在更改后验证编译 diff --git a/docs/zh-CN/rules/kotlin/patterns.md b/docs/zh-CN/rules/kotlin/patterns.md new file mode 100644 index 00000000..864e0ba4 --- /dev/null +++ b/docs/zh-CN/rules/kotlin/patterns.md @@ -0,0 +1,147 @@ +--- +paths: + - "**/*.kt" + - "**/*.kts" +--- + +# Kotlin 模式 + +> 此文件扩展了 [common/patterns.md](../common/patterns.md) 的内容,增加了 Kotlin 和 Android/KMP 特定的内容。 + +## 依赖注入 + +首选构造函数注入。使用 Koin(KMP)或 Hilt(仅限 Android): + +```kotlin +// Koin — declare modules +val dataModule = module { + single { ItemRepositoryImpl(get(), get()) } + factory { GetItemsUseCase(get()) } + viewModelOf(::ItemListViewModel) +} + +// Hilt — annotations +@HiltViewModel +class ItemListViewModel @Inject constructor( + private val getItems: GetItemsUseCase +) : ViewModel() +``` + +## ViewModel 模式 + +单一状态对象、事件接收器、单向数据流: + +```kotlin +data class ScreenState( + val items: List = emptyList(), + val isLoading: Boolean = false +) + +class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() { + private val _state = MutableStateFlow(ScreenState()) + val state = _state.asStateFlow() + + fun onEvent(event: ScreenEvent) { + when (event) { + is ScreenEvent.Load -> load() + is ScreenEvent.Delete -> delete(event.id) + } + } +} +``` + +## 仓库模式 + +* `suspend` 函数返回 `Result` 或自定义错误类型 +* 对于响应式流使用 `Flow` +* 协调本地和远程数据源 + +```kotlin +interface ItemRepository { + suspend fun getById(id: String): Result + suspend fun getAll(): Result> + fun observeAll(): Flow> +} +``` + +## 用例模式 + +单一职责,`operator fun invoke`: + +```kotlin +class GetItemUseCase(private val repository: ItemRepository) { + suspend operator fun invoke(id: String): Result { + return repository.getById(id) + } +} + +class GetItemsUseCase(private val repository: ItemRepository) { + suspend operator fun invoke(): Result> { + return repository.getAll() + } +} +``` + +## expect/actual (KMP) + +用于平台特定的实现: + +```kotlin +// commonMain +expect fun platformName(): String +expect class SecureStorage { + fun save(key: String, value: String) + fun get(key: String): String? +} + +// androidMain +actual fun platformName(): String = "Android" +actual class SecureStorage { + actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ } + actual fun get(key: String): String? = null /* ... */ +} + +// iosMain +actual fun platformName(): String = "iOS" +actual class SecureStorage { + actual fun save(key: String, value: String) { /* Keychain */ } + actual fun get(key: String): String? = null /* ... */ +} +``` + +## 协程模式 + +* 在 ViewModels 中使用 `viewModelScope`,对于结构化的子工作使用 `coroutineScope` +* 对于来自冷流的 StateFlow 使用 `stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue)` +* 当子任务失败应独立处理时使用 `supervisorScope` + +## 使用 DSL 的构建器模式 + +```kotlin +class HttpClientConfig { + var baseUrl: String = "" + var timeout: Long = 30_000 + private val interceptors = mutableListOf() + + fun interceptor(block: () -> Interceptor) { + interceptors.add(block()) + } +} + +fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient { + val config = HttpClientConfig().apply(block) + return HttpClient(config) +} + +// Usage +val client = httpClient { + baseUrl = "https://api.example.com" + timeout = 15_000 + interceptor { AuthInterceptor(tokenProvider) } +} +``` + +## 参考 + +有关详细的协程模式,请参阅技能:`kotlin-coroutines-flows`。 +有关模块和分层模式,请参阅技能:`android-clean-architecture`。 diff --git a/docs/zh-CN/rules/kotlin/security.md b/docs/zh-CN/rules/kotlin/security.md new file mode 100644 index 00000000..ac3a0996 --- /dev/null +++ b/docs/zh-CN/rules/kotlin/security.md @@ -0,0 +1,83 @@ +--- +paths: + - "**/*.kt" + - "**/*.kts" +--- + +# Kotlin 安全 + +> 本文档基于 [common/security.md](../common/security.md),补充了 Kotlin 和 Android/KMP 相关的内容。 + +## 密钥管理 + +* 切勿在源代码中硬编码 API 密钥、令牌或凭据 +* 本地开发时,使用 `local.properties`(已通过 git 忽略)来管理密钥 +* 发布版本中,使用由 CI 密钥生成的 `BuildConfig` 字段 +* 运行时密钥存储使用 `EncryptedSharedPreferences`(Android)或 Keychain(iOS) + +```kotlin +// BAD +val apiKey = "sk-abc123..." + +// GOOD — from BuildConfig (generated at build time) +val apiKey = BuildConfig.API_KEY + +// GOOD — from secure storage at runtime +val token = secureStorage.get("auth_token") +``` + +## 网络安全 + +* 仅使用 HTTPS —— 配置 `network_security_config.xml` 以阻止明文传输 +* 使用 OkHttp 的 `CertificatePinner` 或 Ktor 的等效功能为敏感端点固定证书 +* 为所有 HTTP 客户端设置超时 —— 切勿使用默认值(可能为无限长) +* 在使用所有服务器响应前,先进行验证和清理 + +```xml + + + + +``` + +## 输入验证 + +* 在处理或将用户输入发送到 API 之前,验证所有用户输入 +* 对 Room/SQLDelight 使用参数化查询 —— 切勿将用户输入拼接到 SQL 语句中 +* 清理用户输入中的文件路径,以防止路径遍历攻击 + +```kotlin +// BAD — SQL injection +@Query("SELECT * FROM items WHERE name = '$input'") + +// GOOD — parameterized +@Query("SELECT * FROM items WHERE name = :input") +fun findByName(input: String): List +``` + +## 数据保护 + +* 在 Android 上,使用 `EncryptedSharedPreferences` 存储敏感键值数据 +* 使用 `@Serializable` 并明确指定字段名 —— 不要泄露内部属性名 +* 敏感数据不再需要时,从内存中清除 +* 对序列化类使用 `@Keep` 或 ProGuard 规则,以防止名称混淆 + +## 身份验证 + +* 将令牌存储在安全存储中,而非普通的 SharedPreferences +* 实现令牌刷新机制,并正确处理 401/403 状态码 +* 退出登录时清除所有身份验证状态(令牌、缓存的用户数据、Cookie) +* 对敏感操作使用生物特征认证(`BiometricPrompt`) + +## ProGuard / R8 + +* 为所有序列化模型(`@Serializable`、Gson、Moshi)保留规则 +* 为基于反射的库(Koin、Retrofit)保留规则 +* 测试发布版本 —— 混淆可能会静默地破坏序列化 + +## WebView 安全 + +* 除非明确需要,否则禁用 JavaScript:`settings.javaScriptEnabled = false` +* 在 WebView 中加载 URL 前,先进行验证 +* 切勿暴露访问敏感数据的 `@JavascriptInterface` 方法 +* 使用 `WebViewClient.shouldOverrideUrlLoading()` 来控制导航 diff --git a/docs/zh-CN/rules/kotlin/testing.md b/docs/zh-CN/rules/kotlin/testing.md new file mode 100644 index 00000000..2c0bb32f --- /dev/null +++ b/docs/zh-CN/rules/kotlin/testing.md @@ -0,0 +1,129 @@ +--- +paths: + - "**/*.kt" + - "**/*.kts" +--- + +# Kotlin 测试 + +> 本文档扩展了 [common/testing.md](../common/testing.md),补充了 Kotlin 和 Android/KMP 特有的内容。 + +## 测试框架 + +* **kotlin.test** 用于跨平台 (KMP) — `@Test`, `assertEquals`, `assertTrue` +* **JUnit 4/5** 用于 Android 特定测试 +* **Turbine** 用于测试 Flow 和 StateFlow +* **kotlinx-coroutines-test** 用于协程测试 (`runTest`, `TestDispatcher`) + +## 使用 Turbine 测试 ViewModel + +```kotlin +@Test +fun `loading state emitted then data`() = runTest { + val repo = FakeItemRepository() + repo.addItem(testItem) + val viewModel = ItemListViewModel(GetItemsUseCase(repo)) + + viewModel.state.test { + assertEquals(ItemListState(), awaitItem()) // initial state + viewModel.onEvent(ItemListEvent.Load) + assertTrue(awaitItem().isLoading) // loading + assertEquals(listOf(testItem), awaitItem().items) // loaded + } +} +``` + +## 使用伪造对象而非模拟对象 + +优先使用手写的伪造对象,而非模拟框架: + +```kotlin +class FakeItemRepository : ItemRepository { + private val items = mutableListOf() + var fetchError: Throwable? = null + + override suspend fun getAll(): Result> { + fetchError?.let { return Result.failure(it) } + return Result.success(items.toList()) + } + + override fun observeAll(): Flow> = flowOf(items.toList()) + + fun addItem(item: Item) { items.add(item) } +} +``` + +## 协程测试 + +```kotlin +@Test +fun `parallel operations complete`() = runTest { + val repo = FakeRepository() + val result = loadDashboard(repo) + advanceUntilIdle() + assertNotNull(result.items) + assertNotNull(result.stats) +} +``` + +使用 `runTest` — 它会自动推进虚拟时间并提供 `TestScope`。 + +## Ktor MockEngine + +```kotlin +val mockEngine = MockEngine { request -> + when (request.url.encodedPath) { + "/api/items" -> respond( + content = Json.encodeToString(testItems), + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + else -> respondError(HttpStatusCode.NotFound) + } +} + +val client = HttpClient(mockEngine) { + install(ContentNegotiation) { json() } +} +``` + +## Room/SQLDelight 测试 + +* Room: 使用 `Room.inMemoryDatabaseBuilder()` 进行内存测试 +* SQLDelight: 在 JVM 测试中使用 `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)` + +```kotlin +@Test +fun `insert and query items`() = runTest { + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + Database.Schema.create(driver) + val db = Database(driver) + + db.itemQueries.insert("1", "Sample Item", "description") + val items = db.itemQueries.getAll().executeAsList() + assertEquals(1, items.size) +} +``` + +## 测试命名 + +使用反引号包裹的描述性名称: + +```kotlin +@Test +fun `search with empty query returns all items`() = runTest { } + +@Test +fun `delete item emits updated list without deleted item`() = runTest { } +``` + +## 测试组织 + +``` +src/ +├── commonTest/kotlin/ # Shared tests (ViewModel, UseCase, Repository) +├── androidUnitTest/kotlin/ # Android unit tests (JUnit) +├── androidInstrumentedTest/kotlin/ # Instrumented tests (Room, UI) +└── iosTest/kotlin/ # iOS-specific tests +``` + +最低测试覆盖率:每个功能都需要覆盖 ViewModel + UseCase。 diff --git a/docs/zh-CN/rules/perl/coding-style.md b/docs/zh-CN/rules/perl/coding-style.md new file mode 100644 index 00000000..d67ae8d2 --- /dev/null +++ b/docs/zh-CN/rules/perl/coding-style.md @@ -0,0 +1,47 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- + +# Perl 编码风格 + +> 本文档在 [common/coding-style.md](../common/coding-style.md) 的基础上,补充了 Perl 相关的内容。 + +## 标准 + +* 始终 `use v5.36`(启用 `strict`、`warnings`、`say` 和子程序签名) +* 使用子程序签名 — 切勿手动解包 `@_` +* 优先使用 `say` 而非显式换行的 `print` + +## 不可变性 + +* 对所有属性使用 **Moo**,并配合 `is => 'ro'` 和 `Types::Standard` +* 切勿直接使用被祝福的哈希引用 — 始终通过 Moo/Moose 访问器 +* **面向对象覆盖说明**:对于计算得出的只读值,使用 Moo `has` 属性并配合 `builder` 或 `default` 是可以接受的 + +## 格式化 + +使用 **perltidy** 并采用以下设置: + +``` +-i=4 # 4-space indent +-l=100 # 100 char line length +-ce # cuddled else +-bar # opening brace always right +``` + +## 代码检查 + +使用 **perlcritic**,严重级别设为 3,并启用主题:`core`、`pbp`、`security`。 + +```bash +perlcritic --severity 3 --theme 'core || pbp || security' lib/ +``` + +## 参考 + +查看技能:`perl-patterns`,了解全面的现代 Perl 惯用法和最佳实践。 diff --git a/docs/zh-CN/rules/perl/hooks.md b/docs/zh-CN/rules/perl/hooks.md new file mode 100644 index 00000000..fb043f32 --- /dev/null +++ b/docs/zh-CN/rules/perl/hooks.md @@ -0,0 +1,23 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- + +# Perl 钩子 + +> 本文件在 [common/hooks.md](../common/hooks.md) 的基础上扩展了 Perl 相关的内容。 + +## PostToolUse 钩子 + +在 `~/.claude/settings.json` 中配置: + +* **perltidy**:编辑后自动格式化 `.pl` 和 `.pm` 文件 +* **perlcritic**:编辑 `.pm` 文件后运行代码检查 + +## 警告 + +* 警告在非脚本 `.pm` 文件中使用 `print` — 应使用 `say` 或日志模块(例如,`Log::Any`) diff --git a/docs/zh-CN/rules/perl/patterns.md b/docs/zh-CN/rules/perl/patterns.md new file mode 100644 index 00000000..4575a12f --- /dev/null +++ b/docs/zh-CN/rules/perl/patterns.md @@ -0,0 +1,77 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- + +# Perl 模式 + +> 本文档在 [common/patterns.md](../common/patterns.md) 的基础上扩展了 Perl 特定的内容。 + +## 仓储模式 + +在接口背后使用 **DBI** 或 **DBIx::Class**: + +```perl +package MyApp::Repo::User; +use Moo; + +has dbh => (is => 'ro', required => 1); + +sub find_by_id ($self, $id) { + my $sth = $self->dbh->prepare('SELECT * FROM users WHERE id = ?'); + $sth->execute($id); + return $sth->fetchrow_hashref; +} +``` + +## DTOs / 值对象 + +使用带有 **Types::Standard** 的 **Moo** 类(相当于 Python 的 dataclasses): + +```perl +package MyApp::DTO::User; +use Moo; +use Types::Standard qw(Str Int); + +has name => (is => 'ro', isa => Str, required => 1); +has email => (is => 'ro', isa => Str, required => 1); +has age => (is => 'ro', isa => Int); +``` + +## 资源管理 + +* 始终使用 **三参数 open** 配合 `autodie` +* 使用 **Path::Tiny** 进行文件操作 + +```perl +use autodie; +use Path::Tiny; + +my $content = path('config.json')->slurp_utf8; +``` + +## 模块接口 + +使用 `Exporter 'import'` 配合 `@EXPORT_OK` — 绝不使用 `@EXPORT`: + +```perl +use Exporter 'import'; +our @EXPORT_OK = qw(parse_config validate_input); +``` + +## 依赖管理 + +使用 **cpanfile** + **carton** 以实现可复现的安装: + +```bash +carton install +carton exec prove -lr t/ +``` + +## 参考 + +查看技能:`perl-patterns` 以获取全面的现代 Perl 模式和惯用法。 diff --git a/docs/zh-CN/rules/perl/security.md b/docs/zh-CN/rules/perl/security.md new file mode 100644 index 00000000..14fae6f3 --- /dev/null +++ b/docs/zh-CN/rules/perl/security.md @@ -0,0 +1,70 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- + +# Perl 安全 + +> 本文档在 [common/security.md](../common/security.md) 的基础上扩展了 Perl 相关的内容。 + +## 污染模式 + +* 在所有 CGI/面向 Web 的脚本中使用 `-T` 标志 +* 在执行任何外部命令前,清理 `%ENV` (`$ENV{PATH}`、`$ENV{CDPATH}` 等) + +## 输入验证 + +* 使用允许列表正则表达式进行去污化 — 绝不要使用 `/(.*)/s` +* 使用明确的模式验证所有用户输入: + +```perl +if ($input =~ /\A([a-zA-Z0-9_-]+)\z/) { + my $clean = $1; +} +``` + +## 文件 I/O + +* **仅使用三参数 open** — 绝不要使用两参数 open +* 使用 `Cwd::realpath` 防止路径遍历: + +```perl +use Cwd 'realpath'; +my $safe_path = realpath($user_path); +die "Path traversal" unless $safe_path =~ m{\A/allowed/directory/}; +``` + +## 进程执行 + +* 使用 **列表形式的 `system()`** — 绝不要使用单字符串形式 +* 使用 **IPC::Run3** 来捕获输出 +* 绝对不要在反引号中使用变量插值 + +```perl +system('grep', '-r', $pattern, $directory); # safe +``` + +## SQL 注入预防 + +始终使用 DBI 占位符 — 绝不要将变量插值到 SQL 中: + +```perl +my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?'); +$sth->execute($email); +``` + +## 安全扫描 + +运行 **perlcritic** 并使用安全主题,严重级别设为 4 或更高: + +```bash +perlcritic --severity 4 --theme security lib/ +``` + +## 参考 + +有关全面的 Perl 安全模式、污染模式和安全 I/O,请参阅技能:`perl-security`。 diff --git a/docs/zh-CN/rules/perl/testing.md b/docs/zh-CN/rules/perl/testing.md new file mode 100644 index 00000000..12c4800c --- /dev/null +++ b/docs/zh-CN/rules/perl/testing.md @@ -0,0 +1,55 @@ +--- +paths: + - "**/*.pl" + - "**/*.pm" + - "**/*.t" + - "**/*.psgi" + - "**/*.cgi" +--- + +# Perl 测试 + +> 本文档在 [common/testing.md](../common/testing.md) 的基础上扩展了针对 Perl 的内容。 + +## 框架 + +在新项目中使用 **Test2::V0**(而非 Test::More): + +```perl +use Test2::V0; + +is($result, 42, 'answer is correct'); + +done_testing; +``` + +## 测试运行器 + +```bash +prove -l t/ # adds lib/ to @INC +prove -lr -j8 t/ # recursive, 8 parallel jobs +``` + +始终使用 `-l` 以确保 `lib/` 位于 `@INC` 上。 + +## 覆盖率 + +使用 **Devel::Cover** —— 目标覆盖率 80%+: + +```bash +cover -test +``` + +## 模拟 + +* **Test::MockModule** —— 模拟现有模块上的方法 +* **Test::MockObject** —— 从头创建测试替身 + +## 常见陷阱 + +* 测试文件末尾始终使用 `done_testing` +* 使用 `prove` 时切勿忘记 `-l` 标志 + +## 参考 + +有关使用 Test2::V0、prove 和 Devel::Cover 的详细 Perl TDD 模式,请参阅技能:`perl-testing`。 diff --git a/docs/zh-CN/rules/php/coding-style.md b/docs/zh-CN/rules/php/coding-style.md new file mode 100644 index 00000000..4e839c7c --- /dev/null +++ b/docs/zh-CN/rules/php/coding-style.md @@ -0,0 +1,36 @@ +--- +paths: + - "**/*.php" + - "**/composer.json" +--- + +# PHP 编码风格 + +> 此文件在 [common/coding-style.md](../common/coding-style.md) 的基础上扩展了 PHP 相关内容。 + +## 标准 + +* 遵循 **PSR-12** 的格式化和命名约定。 +* 在应用程序代码中优先使用 `declare(strict_types=1);`。 +* 在所有新代码允许的地方使用标量类型提示、返回类型和类型化属性。 + +## 不可变性 + +* 对于跨越服务边界的数据,优先使用不可变的 DTO 和值对象。 +* 在可能的情况下,对请求/响应负载使用 `readonly` 属性或不可变构造函数。 +* 对于简单的映射使用数组;将业务关键的结构提升为显式类。 + +## 格式化 + +* 使用 **PHP-CS-Fixer** 或 **Laravel Pint** 进行格式化。 +* 使用 **PHPStan** 或 **Psalm** 进行静态分析。 +* 将 Composer 脚本纳入版本控制,以便在本地和 CI 中运行相同的命令。 + +## 错误处理 + +* 对于异常状态抛出异常;避免在新代码中返回 `false`/`null` 作为隐藏的错误通道。 +* 在框架/请求输入到达领域逻辑之前,将其转换为经过验证的 DTO。 + +## 参考 + +有关更广泛的服务/仓库分层指导,请参阅技能:`backend-patterns`。 diff --git a/docs/zh-CN/rules/php/hooks.md b/docs/zh-CN/rules/php/hooks.md new file mode 100644 index 00000000..1a3313bc --- /dev/null +++ b/docs/zh-CN/rules/php/hooks.md @@ -0,0 +1,25 @@ +--- +paths: + - "**/*.php" + - "**/composer.json" + - "**/phpstan.neon" + - "**/phpstan.neon.dist" + - "**/psalm.xml" +--- + +# PHP 钩子 + +> 此文件在 [common/hooks.md](../common/hooks.md) 的基础上扩展了 PHP 相关的内容。 + +## PostToolUse 钩子 + +在 `~/.claude/settings.json` 中配置: + +* **Pint / PHP-CS-Fixer**:自动格式化编辑过的 `.php` 文件。 +* **PHPStan / Psalm**:在类型化代码库中对编辑过的 PHP 文件运行静态分析。 +* **PHPUnit / Pest**:当编辑影响到行为时,为被修改的文件或模块运行针对性测试。 + +## 警告 + +* 当编辑过的文件中存在 `var_dump`、`dd`、`dump` 或 `die()` 时发出警告。 +* 当编辑的 PHP 文件添加了原始 SQL 或禁用了 CSRF/会话保护时发出警告。 diff --git a/docs/zh-CN/rules/php/patterns.md b/docs/zh-CN/rules/php/patterns.md new file mode 100644 index 00000000..a81aadae --- /dev/null +++ b/docs/zh-CN/rules/php/patterns.md @@ -0,0 +1,33 @@ +--- +paths: + - "**/*.php" + - "**/composer.json" +--- + +# PHP 设计模式 + +> 本文档在 [common/patterns.md](../common/patterns.md) 的基础上,补充了 PHP 相关的内容。 + +## 精炼控制器,明确服务 + +* 保持控制器专注于传输层:认证、验证、序列化、状态码。 +* 将业务规则移至应用/领域服务中,这些服务无需 HTTP 引导即可轻松测试。 + +## DTO 与值对象 + +* 对于请求、命令和外部 API 负载,用 DTO 替代结构复杂的关联数组。 +* 对于货币、标识符、日期范围和其他受约束的概念,使用值对象。 + +## 依赖注入 + +* 依赖于接口或精简的服务契约,而非框架全局变量。 +* 通过构造函数传递协作者,这样服务就无需依赖服务定位器查找,易于测试。 + +## 边界 + +* 当模型层职责超出持久化时,应将 ORM 模型与领域决策隔离。 +* 将第三方 SDK 封装在小型的适配器之后,使代码库的其余部分依赖于你的契约,而非它们的。 + +## 参考 + +关于端点约定和响应格式的指导,请参见技能:`api-design`。 diff --git a/docs/zh-CN/rules/php/security.md b/docs/zh-CN/rules/php/security.md new file mode 100644 index 00000000..430a7d5f --- /dev/null +++ b/docs/zh-CN/rules/php/security.md @@ -0,0 +1,34 @@ +--- +paths: + - "**/*.php" + - "**/composer.lock" + - "**/composer.json" +--- + +# PHP 安全 + +> 本文档在 [common/security.md](../common/security.md) 的基础上,补充了 PHP 相关的内容。 + +## 输入与输出 + +* 在框架边界验证请求输入(`FormRequest`、Symfony Validator 或显式 DTO 验证)。 +* 默认在模板中转义输出;将原始 HTML 渲染视为需要合理解释的例外情况。 +* 未经验证,切勿信任查询参数、Cookie、请求头或上传文件的元数据。 + +## 数据库安全 + +* 对所有动态查询使用预处理语句(`PDO`、Doctrine、Eloquent 查询构建器)。 +* 避免在控制器/视图中拼接 SQL 字符串。 +* 谨慎限定 ORM 批量赋值范围,并明确列出可写入字段的白名单。 + +## 密钥与依赖项 + +* 从环境变量或密钥管理器中加载密钥,切勿从已提交的配置文件中读取。 +* 在 CI 中运行 `composer audit`,并在添加依赖项前审查新包维护者的可信度。 +* 审慎锁定主版本号,并及时移除已废弃的包。 + +## 认证与会话安全 + +* 使用 `password_hash()` / `password_verify()` 存储密码。 +* 在身份验证和权限变更后重新生成会话标识符。 +* 对状态变更的 Web 请求强制实施 CSRF 保护。 diff --git a/docs/zh-CN/rules/php/testing.md b/docs/zh-CN/rules/php/testing.md new file mode 100644 index 00000000..793ec491 --- /dev/null +++ b/docs/zh-CN/rules/php/testing.md @@ -0,0 +1,35 @@ +--- +paths: + - "**/*.php" + - "**/phpunit.xml" + - "**/phpunit.xml.dist" + - "**/composer.json" +--- + +# PHP 测试 + +> 本文档在 [common/testing.md](../common/testing.md) 的基础上,补充了 PHP 相关的内容。 + +## 测试框架 + +默认使用 **PHPUnit** 作为测试框架。如果项目已在使用 **Pest**,也是可以接受的。 + +## 覆盖率 + +```bash +vendor/bin/phpunit --coverage-text +# or +vendor/bin/pest --coverage +``` + +在 CI 中优先使用 **pcov** 或 **Xdebug**,并将覆盖率阈值设置在 CI 中,而不是作为团队内部的隐性知识。 + +## 测试组织 + +* 将快速的单元测试与涉及框架/数据库的集成测试分开。 +* 使用工厂/构建器来生成测试数据,而不是手动编写大量的数组。 +* 保持 HTTP/控制器测试专注于传输和验证;将业务规则移到服务层级的测试中。 + +## 参考 + +关于整个仓库范围内的 RED -> GREEN -> REFACTOR 循环,请参见技能:`tdd-workflow`。 diff --git a/docs/zh-CN/rules/typescript/coding-style.md b/docs/zh-CN/rules/typescript/coding-style.md index 7033b208..686dfaf6 100644 --- a/docs/zh-CN/rules/typescript/coding-style.md +++ b/docs/zh-CN/rules/typescript/coding-style.md @@ -10,19 +10,128 @@ paths: > 本文件基于 [common/coding-style.md](../common/coding-style.md) 扩展,包含 TypeScript/JavaScript 特定内容。 +## 类型与接口 + +使用类型使公共 API、共享模型和组件属性显式化、可读且可复用。 + +### 公共 API + +* 为导出的函数、共享工具函数和公共类方法添加参数类型和返回类型 +* 让 TypeScript 推断明显的局部变量类型 +* 将重复的内联对象结构提取为命名类型或接口 + +```typescript +// WRONG: Exported function without explicit types +export function formatUser(user) { + return `${user.firstName} ${user.lastName}` +} + +// CORRECT: Explicit types on public APIs +interface User { + firstName: string + lastName: string +} + +export function formatUser(user: User): string { + return `${user.firstName} ${user.lastName}` +} +``` + +### 接口与类型别名 + +* 使用 `interface` 定义可能被扩展或实现的对象结构 +* 使用 `type` 定义联合类型、交叉类型、元组、映射类型和工具类型 +* 优先使用字符串字面量联合类型而非 `enum`,除非需要 `enum` 以实现互操作性 + +```typescript +interface User { + id: string + email: string +} + +type UserRole = 'admin' | 'member' +type UserWithRole = User & { + role: UserRole +} +``` + +### 避免使用 `any` + +* 在应用程序代码中避免使用 `any` +* 对外部或不受信任的输入使用 `unknown`,然后安全地缩小其类型范围 +* 当值的类型依赖于调用者时,使用泛型 + +```typescript +// WRONG: any removes type safety +function getErrorMessage(error: any) { + return error.message +} + +// CORRECT: unknown forces safe narrowing +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + + return 'Unexpected error' +} +``` + +### React 属性 + +* 使用命名的 `interface` 或 `type` 定义组件属性 +* 显式地定义回调属性类型 +* 除非有特定原因,否则不要使用 `React.FC` + +```typescript +interface User { + id: string + email: string +} + +interface UserCardProps { + user: User + onSelect: (id: string) => void +} + +function UserCard({ user, onSelect }: UserCardProps) { + return +} +``` + +### JavaScript 文件 + +* 在 `.js` 和 `.jsx` 文件中,当类型能提高清晰度且迁移到 TypeScript 不可行时,使用 JSDoc +* 保持 JSDoc 与运行时行为一致 + +```javascript +/** + * @param {{ firstName: string, lastName: string }} user + * @returns {string} + */ +export function formatUser(user) { + return `${user.firstName} ${user.lastName}` +} +``` + ## 不可变性 使用展开运算符进行不可变更新: ```typescript +interface User { + id: string + name: string +} + // WRONG: Mutation -function updateUser(user, name) { - user.name = name // MUTATION! +function updateUser(user: User, name: string): User { + user.name = name // MUTATION! return user } // CORRECT: Immutability -function updateUser(user, name) { +function updateUser(user: Readonly, name: string): User { return { ...user, name @@ -32,31 +141,56 @@ function updateUser(user, name) { ## 错误处理 -使用 async/await 配合 try-catch: +使用 async/await 配合 try-catch 并安全地缩小未知错误类型范围: ```typescript -try { - const result = await riskyOperation() - return result -} catch (error) { - console.error('Operation failed:', error) - throw new Error('Detailed user-friendly message') +interface User { + id: string + email: string +} + +declare function riskyOperation(userId: string): Promise + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message + } + + return 'Unexpected error' +} + +const logger = { + error: (message: string, error: unknown) => { + // Replace with your production logger (for example, pino or winston). + } +} + +async function loadUser(userId: string): Promise { + try { + const result = await riskyOperation(userId) + return result + } catch (error: unknown) { + logger.error('Operation failed', error) + throw new Error(getErrorMessage(error)) + } } ``` ## 输入验证 -使用 Zod 进行基于模式的验证: +使用 Zod 进行基于模式的验证,并从模式推断类型: ```typescript import { z } from 'zod' -const schema = z.object({ +const userSchema = z.object({ email: z.string().email(), age: z.number().int().min(0).max(150) }) -const validated = schema.parse(input) +type UserInput = z.infer + +const validated: UserInput = userSchema.parse(input) ``` ## Console.log diff --git a/docs/zh-CN/skills/android-clean-architecture/SKILL.md b/docs/zh-CN/skills/android-clean-architecture/SKILL.md new file mode 100644 index 00000000..04a4bbcd --- /dev/null +++ b/docs/zh-CN/skills/android-clean-architecture/SKILL.md @@ -0,0 +1,339 @@ +--- +name: android-clean-architecture +description: 适用于Android和Kotlin多平台项目的Clean Architecture模式——模块结构、依赖规则、用例、仓库以及数据层模式。 +origin: ECC +--- + +# Android 整洁架构 + +适用于 Android 和 KMP 项目的整洁架构模式。涵盖模块边界、依赖反转、UseCase/Repository 模式,以及使用 Room、SQLDelight 和 Ktor 的数据层设计。 + +## 何时启用 + +* 构建 Android 或 KMP 项目模块结构 +* 实现 UseCases、Repositories 或 DataSources +* 设计各层(领域层、数据层、表示层)之间的数据流 +* 使用 Koin 或 Hilt 设置依赖注入 +* 在分层架构中使用 Room、SQLDelight 或 Ktor + +## 模块结构 + +### 推荐布局 + +``` +project/ +├── app/ # Android entry point, DI wiring, Application class +├── core/ # Shared utilities, base classes, error types +├── domain/ # UseCases, domain models, repository interfaces (pure Kotlin) +├── data/ # Repository implementations, DataSources, DB, network +├── presentation/ # Screens, ViewModels, UI models, navigation +├── design-system/ # Reusable Compose components, theme, typography +└── feature/ # Feature modules (optional, for larger projects) + ├── auth/ + ├── settings/ + └── profile/ +``` + +### 依赖规则 + +``` +app → presentation, domain, data, core +presentation → domain, design-system, core +data → domain, core +domain → core (or no dependencies) +core → (nothing) +``` + +**关键**:`domain` 绝不能依赖 `data`、`presentation` 或任何框架。它仅包含纯 Kotlin 代码。 + +## 领域层 + +### UseCase 模式 + +每个 UseCase 代表一个业务操作。使用 `operator fun invoke` 以获得简洁的调用点: + +```kotlin +class GetItemsByCategoryUseCase( + private val repository: ItemRepository +) { + suspend operator fun invoke(category: String): Result> { + return repository.getItemsByCategory(category) + } +} + +// Flow-based UseCase for reactive streams +class ObserveUserProgressUseCase( + private val repository: UserRepository +) { + operator fun invoke(userId: String): Flow { + return repository.observeProgress(userId) + } +} +``` + +### 领域模型 + +领域模型是普通的 Kotlin 数据类——没有框架注解: + +```kotlin +data class Item( + val id: String, + val title: String, + val description: String, + val tags: List, + val status: Status, + val category: String +) + +enum class Status { DRAFT, ACTIVE, ARCHIVED } +``` + +### 仓库接口 + +在领域层定义,在数据层实现: + +```kotlin +interface ItemRepository { + suspend fun getItemsByCategory(category: String): Result> + suspend fun saveItem(item: Item): Result + fun observeItems(): Flow> +} +``` + +## 数据层 + +### 仓库实现 + +协调本地和远程数据源: + +```kotlin +class ItemRepositoryImpl( + private val localDataSource: ItemLocalDataSource, + private val remoteDataSource: ItemRemoteDataSource +) : ItemRepository { + + override suspend fun getItemsByCategory(category: String): Result> { + return runCatching { + val remote = remoteDataSource.fetchItems(category) + localDataSource.insertItems(remote.map { it.toEntity() }) + localDataSource.getItemsByCategory(category).map { it.toDomain() } + } + } + + override suspend fun saveItem(item: Item): Result { + return runCatching { + localDataSource.insertItems(listOf(item.toEntity())) + } + } + + override fun observeItems(): Flow> { + return localDataSource.observeAll().map { entities -> + entities.map { it.toDomain() } + } + } +} +``` + +### 映射器模式 + +将映射器作为扩展函数放在数据模型附近: + +```kotlin +// In data layer +fun ItemEntity.toDomain() = Item( + id = id, + title = title, + description = description, + tags = tags.split("|"), + status = Status.valueOf(status), + category = category +) + +fun ItemDto.toEntity() = ItemEntity( + id = id, + title = title, + description = description, + tags = tags.joinToString("|"), + status = status, + category = category +) +``` + +### Room 数据库 (Android) + +```kotlin +@Entity(tableName = "items") +data class ItemEntity( + @PrimaryKey val id: String, + val title: String, + val description: String, + val tags: String, + val status: String, + val category: String +) + +@Dao +interface ItemDao { + @Query("SELECT * FROM items WHERE category = :category") + suspend fun getByCategory(category: String): List + + @Upsert + suspend fun upsert(items: List) + + @Query("SELECT * FROM items") + fun observeAll(): Flow> +} +``` + +### SQLDelight (KMP) + +```sql +-- Item.sq +CREATE TABLE ItemEntity ( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT NOT NULL, + tags TEXT NOT NULL, + status TEXT NOT NULL, + category TEXT NOT NULL +); + +getByCategory: +SELECT * FROM ItemEntity WHERE category = ?; + +upsert: +INSERT OR REPLACE INTO ItemEntity (id, title, description, tags, status, category) +VALUES (?, ?, ?, ?, ?, ?); + +observeAll: +SELECT * FROM ItemEntity; +``` + +### Ktor 网络客户端 (KMP) + +```kotlin +class ItemRemoteDataSource(private val client: HttpClient) { + + suspend fun fetchItems(category: String): List { + return client.get("api/items") { + parameter("category", category) + }.body() + } +} + +// HttpClient setup with content negotiation +val httpClient = HttpClient { + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + install(Logging) { level = LogLevel.HEADERS } + defaultRequest { url("https://api.example.com/") } +} +``` + +## 依赖注入 + +### Koin (适用于 KMP) + +```kotlin +// Domain module +val domainModule = module { + factory { GetItemsByCategoryUseCase(get()) } + factory { ObserveUserProgressUseCase(get()) } +} + +// Data module +val dataModule = module { + single { ItemRepositoryImpl(get(), get()) } + single { ItemLocalDataSource(get()) } + single { ItemRemoteDataSource(get()) } +} + +// Presentation module +val presentationModule = module { + viewModelOf(::ItemListViewModel) + viewModelOf(::DashboardViewModel) +} +``` + +### Hilt (仅限 Android) + +```kotlin +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Binds + abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository +} + +@HiltViewModel +class ItemListViewModel @Inject constructor( + private val getItems: GetItemsByCategoryUseCase +) : ViewModel() +``` + +## 错误处理 + +### Result/Try 模式 + +使用 `Result` 或自定义密封类型进行错误传播: + +```kotlin +sealed interface Try { + data class Success(val value: T) : Try + data class Failure(val error: AppError) : Try +} + +sealed interface AppError { + data class Network(val message: String) : AppError + data class Database(val message: String) : AppError + data object Unauthorized : AppError +} + +// In ViewModel — map to UI state +viewModelScope.launch { + when (val result = getItems(category)) { + is Try.Success -> _state.update { it.copy(items = result.value, isLoading = false) } + is Try.Failure -> _state.update { it.copy(error = result.error.toMessage(), isLoading = false) } + } +} +``` + +## 约定插件 (Gradle) + +对于 KMP 项目,使用约定插件以减少构建文件重复: + +```kotlin +// build-logic/src/main/kotlin/kmp-library.gradle.kts +plugins { + id("org.jetbrains.kotlin.multiplatform") +} + +kotlin { + androidTarget() + iosX64(); iosArm64(); iosSimulatorArm64() + sourceSets { + commonMain.dependencies { /* shared deps */ } + commonTest.dependencies { implementation(kotlin("test")) } + } +} +``` + +在模块中应用: + +```kotlin +// domain/build.gradle.kts +plugins { id("kmp-library") } +``` + +## 应避免的反模式 + +* 在 `domain` 中导入 Android 框架类——保持其为纯 Kotlin +* 向 UI 层暴露数据库实体或 DTO——始终映射到领域模型 +* 将业务逻辑放在 ViewModels 中——提取到 UseCases +* 使用 `GlobalScope` 或非结构化协程——使用 `viewModelScope` 或结构化并发 +* 臃肿的仓库实现——拆分为专注的 DataSources +* 循环模块依赖——如果 A 依赖 B,则 B 绝不能依赖 A + +## 参考 + +查看技能:`compose-multiplatform-patterns` 了解 UI 模式。 +查看技能:`kotlin-coroutines-flows` 了解异步模式。 diff --git a/docs/zh-CN/skills/blueprint/SKILL.md b/docs/zh-CN/skills/blueprint/SKILL.md new file mode 100644 index 00000000..9022ab65 --- /dev/null +++ b/docs/zh-CN/skills/blueprint/SKILL.md @@ -0,0 +1,96 @@ +--- +name: blueprint +description: 将单行目标转化为多会话、多代理工程项目的分步构建计划。每个步骤包含独立的上下文简介,以便新代理能直接执行。包括对抗性审查门、依赖图、并行步骤检测、反模式目录和计划突变协议。触发条件:当用户请求复杂多PR任务的计划、蓝图或路线图,或描述需要多个会话的工作时。不触发条件:任务可在单个PR或少于3个工具调用中完成,或用户说“直接执行”时。origin: community +--- + +# Blueprint — 施工计划生成器 + +将单行目标转化为分步施工计划,任何编码代理都能冷启动执行。 + +## 何时使用 + +* 将大型功能拆分为多个具有明确依赖顺序的 PR +* 规划跨多个会话的重构或迁移 +* 协调子代理间的并行工作流 +* 任何因会话间上下文丢失而导致返工的任务 + +**请勿用于** 可在单个 PR 内完成、少于 3 次工具调用,或用户明确表示“直接做”的任务。 + +## 工作原理 + +Blueprint 运行一个 5 阶段流水线: + +1. **研究** — 预检(git、gh auth、远程仓库、默认分支),然后读取项目结构、现有计划和记忆文件以收集上下文。 +2. **设计** — 将目标分解为适合单次 PR 的步骤(通常 3–12 步)。为每个步骤分配依赖边、并行/串行顺序、模型层级(最强 vs 默认)和回滚策略。 +3. **草拟** — 将自包含的 Markdown 计划文件写入 `plans/`。每个步骤都包含上下文摘要、任务列表、验证命令和退出标准 — 这样新的代理无需阅读先前步骤即可执行任何步骤。 +4. **审查** — 委托最强模型子代理(例如 Opus)根据清单和反模式目录进行对抗性审查。在最终确定前修复所有关键发现。 +5. **注册** — 保存计划、更新内存索引,并向用户展示步骤计数和并行性摘要。 + +Blueprint 自动检测 git/gh 可用性。如果具备 git + GitHub CLI,它会生成完整的分支/PR/CI 工作流计划。如果没有,则切换到直接模式(原地编辑,无分支)。 + +## 示例 + +### 基本用法 + +``` +/blueprint myapp "migrate database to PostgreSQL" +``` + +生成 `plans/myapp-migrate-database-to-postgresql.md`,包含类似以下的步骤: + +* 步骤 1:添加 PostgreSQL 驱动程序和连接配置 +* 步骤 2:为每个表创建迁移脚本 +* 步骤 3:更新仓库层以使用新驱动程序 +* 步骤 4:添加针对 PostgreSQL 的集成测试 +* 步骤 5:移除旧数据库代码和配置 + +### 多代理项目 + +``` +/blueprint chatbot "extract LLM providers into a plugin system" +``` + +生成一个尽可能包含并行步骤的计划(例如,在插件接口步骤完成后,“实现 Anthropic 插件”和“实现 OpenAI 插件”可以并行运行),分配模型层级(接口设计步骤使用最强模型,实现步骤使用默认模型),并在每个步骤后验证不变量(例如“所有现有测试通过”、“核心模块无提供商导入”)。 + +## 主要特性 + +* **冷启动执行** — 每个步骤都包含自包含的上下文摘要。无需先前上下文。 +* **对抗性审查门控** — 每个计划都由最强模型子代理根据清单进行审查,涵盖完整性、依赖关系正确性和反模式检测。 +* **分支/PR/CI 工作流** — 内置于每个步骤中。当 git/gh 缺失时,优雅降级为直接模式。 +* **并行步骤检测** — 依赖图识别出没有共享文件或输出依赖的步骤。 +* **计划变更协议** — 步骤可以按照正式协议和审计追踪进行拆分、插入、跳过、重新排序或放弃。 +* **零运行时风险** — 纯 Markdown 技能。整个仓库仅包含 `.md` 文件 — 无钩子、无 shell 脚本、无可执行代码、无 `package.json`、无构建步骤。安装或调用时,除了 Claude Code 的原生 Markdown 技能加载器外,不运行任何内容。 + +## 安装 + +此技能随 Everything Claude Code 附带。安装 ECC 时无需单独安装。 + +### 完整 ECC 安装 + +如果您从 ECC 仓库检出中工作,请验证技能是否存在: + +```bash +test -f skills/blueprint/SKILL.md +``` + +后续更新时,请在更新前查看 ECC 的差异: + +```bash +cd /path/to/everything-claude-code +git fetch origin main +git log --oneline HEAD..origin/main # review new commits before updating +git checkout # pin to a specific reviewed commit +``` + +### 独立安装(内嵌副本) + +如果您在完整 ECC 安装之外仅内嵌此技能,请将 ECC 仓库中已审查的文件复制到 `~/.claude/skills/blueprint/SKILL.md`。内嵌副本没有 git 远程仓库,因此应通过从已审查的 ECC 提交中重新复制文件来更新,而不是运行 `git pull`。 + +## 要求 + +* Claude Code(用于 `/blueprint` 斜杠命令) +* Git + GitHub CLI(可选 — 启用完整的分支/PR/CI 工作流;Blueprint 检测到缺失时会自动切换到直接模式) + +## 来源 + +灵感来源于 antbotlab/blueprint — 上游项目和参考设计。 diff --git a/docs/zh-CN/skills/carrier-relationship-management/SKILL.md b/docs/zh-CN/skills/carrier-relationship-management/SKILL.md new file mode 100644 index 00000000..bf5cf3e9 --- /dev/null +++ b/docs/zh-CN/skills/carrier-relationship-management/SKILL.md @@ -0,0 +1,199 @@ +--- +name: carrier-relationship-management +description: 用于管理承运商组合、协商运费、跟踪承运商绩效、分配货运以及维护战略承运商关系的编码专业知识。基于拥有15年以上经验的运输经理提供的信息。包括记分卡框架、RFP流程、市场情报和合规性审查。适用于管理承运商、协商费率、评估承运商绩效或制定货运策略时使用。license: Apache-2.0 +version: 1.0.0 +homepage: https://github.com/affaan-m/everything-claude-code +origin: ECC +metadata: + author: evos + clawdbot: + emoji: "🤝" +--- + +# 承运商关系管理 + +## 角色与背景 + +您是一名拥有15年以上经验的资深运输经理,管理着从40家到200多家活跃承运商的组合,涵盖整车运输、零担运输、联运和经纪业务。您负责全生命周期管理:寻找新承运商、协商费率、执行RFP、建立路由指南、通过记分卡跟踪绩效、管理合同续签以及做出运力分配决策。您使用的系统包括TMS(运输管理系统)、费率管理平台、承运商入驻门户、用于市场情报的DAT/Greenscreens,以及用于合规性的FMCSA SAFER系统。您在降低成本的压力与服务品质、运力保障以及承运商关系健康之间取得平衡——因为当市场趋紧时,您的承运商是否愿意承运您的货物,取决于您在运力宽松时如何对待他们。 + +## 使用场景 + +* 入驻新承运商并审查其安全、保险和运营资质时 +* 执行年度或特定线路的RFP进行费率基准测试时 +* 建立或更新承运商记分卡和绩效评估时 +* 在运力紧张或承运商绩效不佳时重新分配货运量时 +* 协商费率上调、燃油附加费或附加费标准时 + +## 运作方式 + +1. 通过FMCSA SAFER系统、保险验证和背景调查寻找并审查承运商 +2. 使用线路级数据、运量承诺和评分标准构建RFP +3. 通过分解干线运输费、燃油费、附加费和运力保证来协商费率 +4. 在TMS中建立包含主/备用承运商分配和自动派单规则的路由指南 +5. 通过加权记分卡跟踪绩效(准时率、索赔率、派单接受率、成本) +6. 进行季度业务评估,并根据记分卡排名调整运力分配 + +## 示例 + +* **新承运商入驻**:一家区域性零担承运商申请承运您的货物。请完成FMCSA资质检查、保险凭证验证、安全分数阈值设定以及90天试用期记分卡设置。 +* **年度RFP**:执行一个包含200条线路的整车运输RFP。构建投标包,根据DAT基准分析现有承运商与挑战者承运商的费率,并构建兼顾成本节约与服务风险的授标方案。 +* **运力紧张时的重新分配**:关键线路上的主承运商派单接受率降至60%。激活备用承运商,调整路由指南优先级,并协商临时运力附加费以应对现货市场风险。 + +## 核心知识 + +### 费率谈判基础 + +每一项运费费率都有必须独立协商的组成部分——将它们捆绑会掩盖您多付费用的地方: + +* **基础干线费率**:码头到码头的每英里或固定费率。对于整车运输,以DAT或Greenscreens的线路费率作为基准。对于零担运输,这是承运商公布运价单的折扣(对于中等货量的托运人,通常为70-85%的折扣)。始终按线路逐一协商——一家承运商可能在芝加哥-达拉斯线路上有竞争力,但在亚特兰大-洛杉矶线路上可能比市场高出15%。 +* **燃油附加费**:与DOE全国平均柴油价格挂钩的百分比或每英里附加费。协商FSC表格,而不仅仅是当前费率。关键细节:基准触发价格(柴油价格达到多少时FSC为0%)、增量(例如,柴油每上涨0.05美元,FSC增加0.01美元/英里)以及指数滞后(每周调整与每月调整)。一家报价低干线费率但采用激进FSC表的承运商,可能比干线费率较高但采用标准DOE指数化FSC的承运商更昂贵。 +* **附加费**:滞期费(2小时免费时间后每小时50-100美元是标准)、升降尾板费(75-150美元)、住宅配送费(75-125美元)、室内配送费(100美元以上)、限制区域费(50-100美元)、预约调度费(0-50美元)。积极协商滞期费的免费时间——司机滞期是承运商发票纠纷的首要来源。对于零担运输,注意重新称重/重新分类费(每次25-75美元)和立方容量附加费。 +* **最低收费**:每家承运商都有每票货物的最低收费。对于整车运输,通常是最低里程费(例如,200英里以下的货物800美元)。对于零担运输,这是每票货物的最低收费(75-150美元),无论重量或等级如何。单独协商短途线路的最低收费。 +* **合同费率与现货费率**:合同费率(通过RFP或谈判授予,有效期6-12个月)提供成本可预测性和运力承诺。现货费率(在公开市场上按每票货物协商)在紧张市场中高出10-30%,在疲软市场中低5-20%。一个健康的组合应使用75-85%的合同货运和15-25%的现货货运。现货货运超过30%意味着您的路由指南正在失效。 + +### 承运商记分卡 + +衡量重要指标。一个跟踪20个指标的记分卡会被忽视;一个跟踪5个指标的记分卡会被付诸行动: + +* **准时交付率**:在约定时间窗口内交付的货物百分比。目标:≥95%。危险信号:<90%。分别衡量提货和交付的准时率——一家提货准时率98%但交付准时率88%的承运商存在干线或终端问题,而非运力问题。 +* **派单接受率**:承运商接受的电子派单百分比。目标:主承运商≥90%。危险信号:<80%。一家拒绝25%派单的承运商正在消耗您运营团队重新派单的时间,并迫使您暴露于现货市场。合同线路上的派单接受率低于75%意味着费率低于市场水平——重新协商或重新分配。 +* **索赔率**:已申报索赔的美元价值除以承运商的总运费支出。目标:<支出总额的0.5%。危险信号:>1.0%。分别跟踪索赔频率和索赔严重程度——一家有一笔5万美元索赔的承运商与一家有五十笔1千美元索赔的承运商是不同的。后者表明存在系统性的处理问题。 +* **发票准确性**:无需人工修改即与合同费率匹配的发票百分比。目标:≥97%。危险信号:<93%。长期多收(即使是小金额)表明要么是故意的费率试探,要么是计费系统故障。无论哪种情况,都会增加您的审计成本。发票准确性低于90%的承运商应被纳入整改行动。 +* **派单到提货时间**:电子派单接受到实际提货之间的小时数。目标:整车运输在要求提货时间后2小时内。接受派单但持续延迟提货的承运商是在“软性拒绝”——他们接受派单是为了锁定货物,同时寻找更好的货源。 + +### 组合策略 + +您的承运商组合就像一个投资组合——多元化管理风险,集中化创造杠杆: + +* **资产承运商与经纪人**:资产承运商拥有卡车。他们提供运力确定性、稳定的服务和直接的责任归属——但他们在定价上灵活性较低,可能无法覆盖您的所有线路。经纪人从数千家小型承运商处获取运力。他们提供定价灵活性和线路覆盖,但引入了交易对手风险(双重经纪、承运商质量参差不齐、支付链复杂)。典型的组合是60-70%的资产承运商,20-30%的经纪人,以及5-15%的利基/专业承运商作为一个单独的类别,专门用于温控、危险品、超尺寸或其他需要特殊处理的线路。 +* **路由指南结构**:为每条每周超过2票货物的线路建立一个3级深度的路由指南。主承运商获得首次派单(目标:接受率80%以上)。备用承运商获得后备派单(目标:溢货接受率70%以上)。第三级是您的价格上限——通常是一个经纪人,其费率代表现货采购的“不超过”价格。对于每周少于2票货物的线路,使用2级深度的指南或具有广泛覆盖范围的区域经纪人。 +* **线路密度与承运商集中度**:授予每家承运商每条线路足够的货量,使其重视您的业务。一家在您的线路上每周承运2票货物的承运商会优先于每月只给其2票货物的托运人。但不要给任何一家承运商超过单条线路40%的货量——一家承运商退出或服务失败对集中度高的线路是灾难性的。对于您按货量排名前20的线路,至少保持3家活跃承运商。 +* **小型承运商的价值**:拥有10-50辆卡车的承运商通常比大型承运商提供更好的服务、更灵活的定价和更牢固的关系。他们会接电话。他们的车主经营者关心您的货物。代价是:技术集成度较低、保险覆盖较薄以及高峰期的运力限制。将小型承运商用于稳定、中等货量的线路,在这些线路上,关系质量比激增运力更重要。 + +### RFP流程 + +一个运行良好的货运RFP需要8-12周,并涉及每家现有和潜在的承运商: + +* **RFP前准备**:分析12个月的货运数据。按货量、支出和当前服务水平识别线路。标记绩效不佳的线路以及当前费率超过市场基准(DAT、Greenscreens、Chainalytics)的线路。设定目标:成本降低百分比、服务水平最低要求、承运商多元化目标。 +* **RFP设计**:包含线路级详细信息(始发地/目的地邮编、货量范围、所需设备、任何特殊处理要求)、当前运输时间预期、附加费要求、付款条件、保险最低要求,以及您的评估标准和权重。要求承运商按线路报价——组合报价(“我们给您所有线路5%的折扣”)会掩盖交叉补贴。 +* **投标评估**:不要仅根据价格授标。将成本权重设为40-50%,服务历史权重设为25-30%,运力承诺权重设为15-20%,运营匹配度权重设为10-15%。一家比最低报价高3%但拥有97%准时交付率和95%派单接受率的承运商,比准时交付率85%、派单接受率70%的最低报价承运商更便宜——服务失败造成的成本高于费率差异。 +* **授标与实施**:分阶段授标——先授标给主承运商,然后是备用承运商。给承运商2-3周时间使其新线路运营就绪,然后您再开始派单。运行30天的并行期,新旧路由指南重叠。然后干净利落地切换。 + +### 市场情报 + +费率周期方向可预测,幅度不可预测: + +* **DAT和Greenscreens**:DAT RateView提供基于经纪人报告交易的线路级现货和合同费率基准。Greenscreens提供承运商特定的定价情报和预测分析。两者都用——DAT用于判断市场方向,Greenscreens用于获取承运商特定的谈判筹码。两者都不完全准确,但都比盲目谈判要好。 +* **货运市场周期**:整车运输市场在托运人有利(运力过剩、费率下降、派单接受率高)和承运人有利(运力紧张、费率上升、派单拒绝)之间波动。周期从高峰到高峰持续18-36个月。关键指标:DAT货物与卡车比率(>6:1表示市场紧张)、OTRI(外派单拒绝指数——>10%表示承运商议价能力增强)、8级卡车订单(未来6-12个月运力增加的领先指标)。 +* **季节性模式**:农产品季节(4月至7月)会收紧东南部和西部的冷藏车运力。零售旺季(10月至1月)会收紧全国的干货厢式车运力。每月和每季度的最后一周会出现货量激增,因为托运人要完成收入目标。预算RFP时间安排应避免在周期高峰或低谷授标合同——在过渡期授标以获得更现实的费率。 + +### FMCSA合规审查 + +您组合中的每家承运商在承运第一票货物前以及之后每季度都必须通过合规审查: + +* **运营资质:** 通过 FMCSA SAFER 系统核实有效的 MC(汽车承运人)或 FF(货运代理)资质。超过 12 个月未更新的"已授权"状态可能表明承运人技术上授权但实际已停止运营。检查"授权范围"字段——授权为"普通货物"的承运人依法不能承运家居用品。 +* **保险最低要求:** 普通货运最低 75 万美元(根据 FMCSA §387.9 规定),危险品 100 万美元,家居用品 500 万美元。无论货物类型如何,要求所有承运人提供至少 100 万美元的保险——FMCSA 75 万美元的最低要求无法覆盖严重事故。通过 FMCSA 的保险选项卡核实保险,而不仅仅是承运人提供的证书——证书可能伪造或已过期。 +* **安全评级:** FMCSA 根据合规审查分配满意、有条件或不满意的评级。绝不使用评级为不满意的承运人。有条件评级的承运人需要个案评估——了解具体条件。无评级("未评级")的承运人占大多数——改用其 CSA(合规、安全、问责)分数。重点关注不安全驾驶、服务时间与车辆维护 BASICs。在不安全驾驶方面处于前 25%(最差)百分位的承运人存在责任风险。 +* **经纪人保证金核实:** 如果使用经纪人,核实其 7.5 万美元的保证金或信托基金是否有效。保证金被撤销或减少的经纪人很可能陷入财务困境。检查 FMCSA 保证金/信托选项卡。同时核实经纪人拥有或有货物保险——这可以在经纪人指定的承运人造成损失且承运人保险不足时保护您。 + +## 决策框架 + +### 新线路的承运人选择 + +当向您的网络添加新线路时,按此决策树评估候选者: + +1. **现有合作承运人是否覆盖此线路?** 如果是,首先与现有承运人谈判——为一条线路引入新承运人会带来启动成本(500-1500 美元)和关系管理开销。将新线路作为增量业务提供给现有承运人,以换取对现有线路的费率优惠。 +2. **如果没有现有承运人覆盖该线路:** 寻找 3-5 个候选者。对于距离 >500 英里的线路,优先考虑其所在地在始发地 100 英里内的资产型承运人。对于距离 <300 英里的线路,考虑区域性承运人和专属车队。对于不频繁的线路(<1 车/周),拥有强大区域覆盖的经纪人可能是最实际的选择。 +3. **评估:** 进行 FMCSA 合规检查。向每位候选者索取该特定线路的 12 个月服务历史(而不仅仅是其网络平均值)。对照 DAT 线路费率以获取市场基准。比较总成本(干线运输 + 燃油附加费 + 预期附加费),而不仅仅是干线运输费。 +4. **试用期:** 以合同费率授予 30 天试用期。设定明确的 KPI:准时交付率 ≥93%,承运人接受率 ≥85%,发票准确率 ≥95%。30 天后进行审查——在没有运营验证的情况下,不要锁定 12 个月的承诺。 + +### 何时整合 vs. 多元化 + +* **整合(减少承运人数量)时机:** 在一条每周 <5 车货量的线路上,您有超过 3 家承运人(每家承运人获得的业务量太少而不重视)。您的承运人管理资源紧张。您需要战略合作伙伴提供更优惠的价格(业务量集中 = 议价能力)。市场宽松,承运人正在争夺您的货物。 +* **多元化(增加承运人)时机:** 单一承运人处理关键线路 >40% 的业务量。线路上的承运人拒绝接受率上升超过 15%。您正进入旺季,需要应急运力。承运人出现财务困境迹象(Carrier411 上报告拖欠司机款项、FMCSA 保险失效、通过 CDL 招聘信息可见司机突然流失)。 + +### 现货 vs. 合同决策 + +* **维持合同时机:** 合同费率与现货费率之间的差价 <10%。您有稳定、可预测的业务量。运力正在收紧(现货费率正在上涨)。该线路对客户至关重要且交货窗口紧张。 +* **转向现货时机:** 现货费率比您的合同费率低 >15%(市场疲软)。该线路不规律(<1 车/周)。您需要超出路由指南的一次性应急运力。您的合同承运人持续拒绝接受该线路的货物(他们实际上是在迫使您进入现货市场)。 +* **重新谈判合同时机:** 您的合同费率与 DAT 基准之间的差价连续 60 天以上超过 15%。承运人的承运人接受率在 30 天内降至 75% 以下。您的业务量发生重大变化(增加或减少),从而改变了线路的经济性。 + +### 承运人退出标准 + +当达到以下任何阈值,且在记录在案的纠正措施失败后,将承运人从您的活跃路由指南中移除: + +* 准时交付率连续 60 天低于 85% +* 承运人接受率连续 30 天低于 70% 且无沟通 +* 索赔率连续 90 天超过支出的 2% +* FMCSA 资质被撤销、保险失效或安全评级降为不满意 +* 发出纠正通知后,发票准确率连续 90 天低于 88% +* 发现将您的货物进行双重经纪 +* 财务困境证据:保证金被撤销、CarrierOK 或 Carrier411 上的司机投诉、无法解释的服务崩溃 + +## 关键边缘情况 + +这些是标准决策手册会导致不良结果的情况。此处包含简要摘要,以便您在需要时可以将其扩展为特定项目的决策手册。 + +1. **飓风期间的运力紧缩:** 您的顶级承运人将司机从墨西哥湾沿岸撤离。现货费率翻了三倍。诱惑是支付任何费率来运输货物。专业做法是:激活预先部署的区域承运人,通过未受影响的走廊重新规划路线,并与现货承运人谈判多车承诺以锁定费率上限。 +2. **发现双重经纪:** 您被告知到达的卡车并非来自您提单上的承运人。保险链可能断裂,您的货物面临更高风险。如果货物尚未发出,请不要接受。如果在途,记录一切并要求在 24 小时内提供书面解释。 +3. **业务量损失 40% 后的费率重新谈判:** 您的公司失去了一个大客户,货运量下降。您承运人的合同费率是基于您已无法履行的业务量承诺。主动重新谈判可以维护关系;让承运人在开具发票时发现业务量不足则会破坏信任。 +4. **承运人财务困境迹象:** 警告信号在承运人倒闭前数月出现:延迟支付司机结算款、FMCSA 保险文件频繁更换承保人、保证金金额下降、Carrier411 投诉激增。逐步减少业务量——不要等到倒闭。 +5. **大型承运人收购您的利基合作伙伴:** 您最好的区域承运人刚被一家全国性车队收购。预计整合期间会出现服务中断、费率重新谈判尝试以及可能失去您的专属客户经理。在过渡完成前确保替代运力。 +6. **燃油附加费操纵:** 承运人提出人为压低的基础费率,搭配激进的燃油附加费表,使总成本高于市场。始终在柴油价格范围内(3.50 美元、4.00 美元、4.50 美元/加仑)模拟总成本以揭露此策略。 +7. **大规模滞留费和附加费争议:** 当滞留费占承运人总账单的 >5% 时,根本原因通常是发货方设施运营问题,而非承运人超额收费。在争议费用前解决运营问题——否则将失去承运人。 + +## 沟通模式 + +### 费率谈判语气 + +费率谈判是长期关系对话,而非一次性交易。调整语气: + +* **开场立场:** 用数据引导,而非要求。"DAT 数据显示,过去 90 天该线路平均为每英里 2.15 美元。我们当前的合同是 2.45 美元。我们希望讨论一下如何调整。" 绝不要说"您的费率太高了"——应该说"市场已经发生变化,我们希望确保我们一起保持竞争力。" +* **还价:** 承认承运人的观点。"我们理解司机工资上涨是真实存在的。让我们找到一个数字,既能使这条线路对您的司机有吸引力,又能保持我们的竞争力。" 在基础费率上折中,在附加费和燃油附加费表上更努力地谈判。 +* **年度审查:** 将其定位为合作伙伴关系检查,而非削减成本的活动。分享您的业务量预测、增长计划和线路变更。询问在运营方面您能做些什么来帮助承运人(更快的装卸时间、一致的调度、甩挂运输计划)。承运人会给那些让司机工作更轻松的发货人提供更好的费率。 + +### 绩效评估 + +* **正面评估:** 要具体。"您在芝加哥-达拉斯线路 97% 的准时交付率本季度为我们节省了约 4.5 万美元的加急成本。我们将您在该线路上的分配份额从 60% 提高到 75%。" 承运人会投资于奖励绩效的关系。 +* **纠正性评估:** 用数据引导,而非指责。出示记分卡。指出低于阈值的具体指标。要求提供包含 30/60/90 天时间线的纠正行动计划。设定明确的后果:"如果该线路的准时交付率在 60 天内达不到 92%,我们将需要将 50% 的业务量转移到替代承运人。" + +将上述评估模式作为基础,并根据您的承运人合同、升级路径和客户承诺调整语言。 + +## 升级协议 + +### 自动升级触发条件 + +| 触发条件 | 行动 | 时间线 | +|---|---|---| +| 承运人接受率连续 2 周低于 70% | 通知采购部门,安排与承运人通话 | 48 小时内 | +| 任何线路的现货支出超过线路预算的 30% | 审查路由指南,启动承运人寻源 | 1 周内 | +| 承运人 FMCSA 资质或保险失效 | 立即暂停分配货物,通知运营部门 | 1 小时内 | +| 单一承运人控制关键线路 >50% 的业务量 | 启动二级承运人资格认证 | 2 周内 | +| 任何承运人的索赔率超过 1.5% 持续 60 天以上 | 安排正式绩效评估 | 1 周内 | +| 5 条以上线路的费率与 DAT 基准差异 >20% | 启动合同重新谈判或小型招标 | 2 周内 | +| 承运人报告司机短缺或服务中断 | 激活备用承运人,加强监控 | 4 小时内 | +| 确认任何货物存在双重经纪 | 立即暂停承运人,进行合规审查 | 2 小时内 | + +### 升级链 + +分析师 → 运输经理(48 小时) → 运输总监(1 周) → 供应链副总裁(持续性问题或 >10 万美元风险敞口) + +## 绩效指标 + +每周跟踪,每月与承运人管理团队审查,每季度与承运人分享: + +| 指标 | 目标 | 红色警报 | +|---|---|---| +| 合同费率 vs. DAT 基准 | 在 ±8% 以内 | 溢价或折扣 >15% | +| 路由指南合规率(按货物重量/数量计) | ≥85% | <70% | +| 首次承运人接受率 | ≥90% | <80% | +| 整体准时交付率(加权平均) | ≥95% | <90% | +| 承运人整体索赔率 | <支出的 0.5% | >1.0% | +| 平均承运人发票准确率 | ≥97% | <93% | +| 现货货运百分比 | <20% | >30% | +| RFP 周期时间(启动到实施) | ≤12 周 | >16 周 | + +## 其他资源 + +* 在同一运营审查中跟踪承运人记分卡、异常趋势和路由指南合规情况,以便定价和服务决策保持关联。 +* 在将此技能用于生产环境之前,请先记录您组织偏好的谈判立场、附加费护栏和升级触发条件。 diff --git a/docs/zh-CN/skills/claude-api/SKILL.md b/docs/zh-CN/skills/claude-api/SKILL.md new file mode 100644 index 00000000..c26e2ee4 --- /dev/null +++ b/docs/zh-CN/skills/claude-api/SKILL.md @@ -0,0 +1,337 @@ +--- +name: claude-api +description: Anthropic Claude API 的 Python 和 TypeScript 使用模式。涵盖 Messages API、流式处理、工具使用、视觉功能、扩展思维、批量处理、提示缓存和 Claude Agent SDK。适用于使用 Claude API 或 Anthropic SDK 构建应用程序的场景。 +origin: ECC +--- + +# Claude API + +使用 Anthropic Claude API 和 SDK 构建应用程序。 + +## 何时激活 + +* 构建调用 Claude API 的应用程序 +* 代码导入 `anthropic` (Python) 或 `@anthropic-ai/sdk` (TypeScript) +* 用户询问 Claude API 模式、工具使用、流式传输或视觉功能 +* 使用 Claude Agent SDK 实现智能体工作流 +* 优化 API 成本、令牌使用或延迟 + +## 模型选择 + +| 模型 | ID | 最适合 | +|-------|-----|----------| +| Opus 4.1 | `claude-opus-4-1` | 复杂推理、架构设计、研究 | +| Sonnet 4 | `claude-sonnet-4-0` | 平衡的编码任务,大多数开发工作 | +| Haiku 3.5 | `claude-3-5-haiku-latest` | 快速响应、高吞吐量、成本敏感型 | + +默认使用 Sonnet 4,除非任务需要深度推理(Opus)或速度/成本优化(Haiku)。对于生产环境,优先使用固定的快照 ID 而非别名。 + +## Python SDK + +### 安装 + +```bash +pip install anthropic +``` + +### 基本消息 + +```python +import anthropic + +client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env + +message = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=1024, + messages=[ + {"role": "user", "content": "Explain async/await in Python"} + ] +) +print(message.content[0].text) +``` + +### 流式传输 + +```python +with client.messages.stream( + model="claude-sonnet-4-0", + max_tokens=1024, + messages=[{"role": "user", "content": "Write a haiku about coding"}] +) as stream: + for text in stream.text_stream: + print(text, end="", flush=True) +``` + +### 系统提示词 + +```python +message = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=1024, + system="You are a senior Python developer. Be concise.", + messages=[{"role": "user", "content": "Review this function"}] +) +``` + +## TypeScript SDK + +### 安装 + +```bash +npm install @anthropic-ai/sdk +``` + +### 基本消息 + +```typescript +import Anthropic from "@anthropic-ai/sdk"; + +const client = new Anthropic(); // reads ANTHROPIC_API_KEY from env + +const message = await client.messages.create({ + model: "claude-sonnet-4-0", + max_tokens: 1024, + messages: [ + { role: "user", content: "Explain async/await in TypeScript" } + ], +}); +console.log(message.content[0].text); +``` + +### 流式传输 + +```typescript +const stream = client.messages.stream({ + model: "claude-sonnet-4-0", + max_tokens: 1024, + messages: [{ role: "user", content: "Write a haiku" }], +}); + +for await (const event of stream) { + if (event.type === "content_block_delta" && event.delta.type === "text_delta") { + process.stdout.write(event.delta.text); + } +} +``` + +## 工具使用 + +定义工具并让 Claude 调用它们: + +```python +tools = [ + { + "name": "get_weather", + "description": "Get current weather for a location", + "input_schema": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"}, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} + }, + "required": ["location"] + } + } +] + +message = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=1024, + tools=tools, + messages=[{"role": "user", "content": "What's the weather in SF?"}] +) + +# Handle tool use response +for block in message.content: + if block.type == "tool_use": + # Execute the tool with block.input + result = get_weather(**block.input) + # Send result back + follow_up = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=1024, + tools=tools, + messages=[ + {"role": "user", "content": "What's the weather in SF?"}, + {"role": "assistant", "content": message.content}, + {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": block.id, "content": str(result)} + ]} + ] + ) +``` + +## 视觉功能 + +发送图像进行分析: + +```python +import base64 + +with open("diagram.png", "rb") as f: + image_data = base64.standard_b64encode(f.read()).decode("utf-8") + +message = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=1024, + messages=[{ + "role": "user", + "content": [ + {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_data}}, + {"type": "text", "text": "Describe this diagram"} + ] + }] +) +``` + +## 扩展思考 + +针对复杂推理任务: + +```python +message = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=16000, + thinking={ + "type": "enabled", + "budget_tokens": 10000 + }, + messages=[{"role": "user", "content": "Solve this math problem step by step..."}] +) + +for block in message.content: + if block.type == "thinking": + print(f"Thinking: {block.thinking}") + elif block.type == "text": + print(f"Answer: {block.text}") +``` + +## 提示词缓存 + +缓存大型系统提示词或上下文以降低成本: + +```python +message = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=1024, + system=[ + {"type": "text", "text": large_system_prompt, "cache_control": {"type": "ephemeral"}} + ], + messages=[{"role": "user", "content": "Question about the cached context"}] +) +# Check cache usage +print(f"Cache read: {message.usage.cache_read_input_tokens}") +print(f"Cache creation: {message.usage.cache_creation_input_tokens}") +``` + +## 批量 API + +以 50% 的成本降低异步处理大量数据: + +```python +import time + +batch = client.messages.batches.create( + requests=[ + { + "custom_id": f"request-{i}", + "params": { + "model": "claude-sonnet-4-0", + "max_tokens": 1024, + "messages": [{"role": "user", "content": prompt}] + } + } + for i, prompt in enumerate(prompts) + ] +) + +# Poll for completion +while True: + status = client.messages.batches.retrieve(batch.id) + if status.processing_status == "ended": + break + time.sleep(30) + +# Get results +for result in client.messages.batches.results(batch.id): + print(result.result.message.content[0].text) +``` + +## Claude Agent SDK + +构建多步骤智能体: + +```python +# Note: Agent SDK API surface may change — check official docs +import anthropic + +# Define tools as functions +tools = [{ + "name": "search_codebase", + "description": "Search the codebase for relevant code", + "input_schema": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"] + } +}] + +# Run an agentic loop with tool use +client = anthropic.Anthropic() +messages = [{"role": "user", "content": "Review the auth module for security issues"}] + +while True: + response = client.messages.create( + model="claude-sonnet-4-0", + max_tokens=4096, + tools=tools, + messages=messages, + ) + if response.stop_reason == "end_turn": + break + # Handle tool calls and continue the loop + messages.append({"role": "assistant", "content": response.content}) + # ... execute tools and append tool_result messages +``` + +## 成本优化 + +| 策略 | 节省幅度 | 使用时机 | +|----------|---------|-------------| +| 提示词缓存 | 缓存令牌成本降低高达 90% | 重复的系统提示词或上下文 | +| 批量 API | 50% | 非时间敏感的批量处理 | +| 使用 Haiku 而非 Sonnet | ~75% | 简单任务、分类、提取 | +| 缩短 max\_tokens | 可变 | 已知输出较短时 | +| 流式传输 | 无(成本相同) | 更好的用户体验,价格相同 | + +## 错误处理 + +```python +import time + +from anthropic import APIError, RateLimitError, APIConnectionError + +try: + message = client.messages.create(...) +except RateLimitError: + # Back off and retry + time.sleep(60) +except APIConnectionError: + # Network issue, retry with backoff + pass +except APIError as e: + print(f"API error {e.status_code}: {e.message}") +``` + +## 环境设置 + +```bash +# Required +export ANTHROPIC_API_KEY="your-api-key-here" + +# Optional: set default model +export ANTHROPIC_MODEL="claude-sonnet-4-0" +``` + +切勿硬编码 API 密钥。始终使用环境变量。 diff --git a/docs/zh-CN/skills/compose-multiplatform-patterns/SKILL.md b/docs/zh-CN/skills/compose-multiplatform-patterns/SKILL.md new file mode 100644 index 00000000..cd065092 --- /dev/null +++ b/docs/zh-CN/skills/compose-multiplatform-patterns/SKILL.md @@ -0,0 +1,299 @@ +--- +name: compose-multiplatform-patterns +description: KMP项目中的Compose Multiplatform和Jetpack Compose模式——状态管理、导航、主题化、性能优化和平台特定UI。 +origin: ECC +--- + +# Compose 多平台模式 + +使用 Compose Multiplatform 和 Jetpack Compose 构建跨 Android、iOS、桌面和 Web 的共享 UI 的模式。涵盖状态管理、导航、主题和性能。 + +## 何时启用 + +* 构建 Compose UI(Jetpack Compose 或 Compose Multiplatform) +* 使用 ViewModel 和 Compose 状态管理 UI 状态 +* 在 KMP 或 Android 项目中实现导航 +* 设计可复用的可组合项和设计系统 +* 优化重组和渲染性能 + +## 状态管理 + +### ViewModel + 单一状态对象 + +使用单个数据类表示屏幕状态。将其暴露为 `StateFlow` 并在 Compose 中收集: + +```kotlin +data class ItemListState( + val items: List = emptyList(), + val isLoading: Boolean = false, + val error: String? = null, + val searchQuery: String = "" +) + +class ItemListViewModel( + private val getItems: GetItemsUseCase +) : ViewModel() { + private val _state = MutableStateFlow(ItemListState()) + val state: StateFlow = _state.asStateFlow() + + fun onSearch(query: String) { + _state.update { it.copy(searchQuery = query) } + loadItems(query) + } + + private fun loadItems(query: String) { + viewModelScope.launch { + _state.update { it.copy(isLoading = true) } + getItems(query).fold( + onSuccess = { items -> _state.update { it.copy(items = items, isLoading = false) } }, + onFailure = { e -> _state.update { it.copy(error = e.message, isLoading = false) } } + ) + } + } +} +``` + +### 在 Compose 中收集状态 + +```kotlin +@Composable +fun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) { + val state by viewModel.state.collectAsStateWithLifecycle() + + ItemListContent( + state = state, + onSearch = viewModel::onSearch + ) +} + +@Composable +private fun ItemListContent( + state: ItemListState, + onSearch: (String) -> Unit +) { + // Stateless composable — easy to preview and test +} +``` + +### 事件接收器模式 + +对于复杂屏幕,使用密封接口表示事件,而非多个回调 lambda: + +```kotlin +sealed interface ItemListEvent { + data class Search(val query: String) : ItemListEvent + data class Delete(val itemId: String) : ItemListEvent + data object Refresh : ItemListEvent +} + +// In ViewModel +fun onEvent(event: ItemListEvent) { + when (event) { + is ItemListEvent.Search -> onSearch(event.query) + is ItemListEvent.Delete -> deleteItem(event.itemId) + is ItemListEvent.Refresh -> loadItems(_state.value.searchQuery) + } +} + +// In Composable — single lambda instead of many +ItemListContent( + state = state, + onEvent = viewModel::onEvent +) +``` + +## 导航 + +### 类型安全导航(Compose Navigation 2.8+) + +将路由定义为 `@Serializable` 对象: + +```kotlin +@Serializable data object HomeRoute +@Serializable data class DetailRoute(val id: String) +@Serializable data object SettingsRoute + +@Composable +fun AppNavHost(navController: NavHostController = rememberNavController()) { + NavHost(navController, startDestination = HomeRoute) { + composable { + HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) }) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + DetailScreen(id = route.id) + } + composable { SettingsScreen() } + } +} +``` + +### 对话框和底部抽屉导航 + +使用 `dialog()` 和覆盖层模式,而非命令式的显示/隐藏: + +```kotlin +NavHost(navController, startDestination = HomeRoute) { + composable { /* ... */ } + dialog { backStackEntry -> + val route = backStackEntry.toRoute() + ConfirmDeleteDialog( + itemId = route.itemId, + onConfirm = { navController.popBackStack() }, + onDismiss = { navController.popBackStack() } + ) + } +} +``` + +## 可组合项设计 + +### 基于槽位的 API + +使用槽位参数设计可组合项以获得灵活性: + +```kotlin +@Composable +fun AppCard( + modifier: Modifier = Modifier, + header: @Composable () -> Unit = {}, + content: @Composable ColumnScope.() -> Unit, + actions: @Composable RowScope.() -> Unit = {} +) { + Card(modifier = modifier) { + Column { + header() + Column(content = content) + Row(horizontalArrangement = Arrangement.End, content = actions) + } + } +} +``` + +### 修饰符顺序 + +修饰符顺序很重要 —— 按此顺序应用: + +```kotlin +Text( + text = "Hello", + modifier = Modifier + .padding(16.dp) // 1. Layout (padding, size) + .clip(RoundedCornerShape(8.dp)) // 2. Shape + .background(Color.White) // 3. Drawing (background, border) + .clickable { } // 4. Interaction +) +``` + +## KMP 平台特定 UI + +### 平台可组合项的 expect/actual + +```kotlin +// commonMain +@Composable +expect fun PlatformStatusBar(darkIcons: Boolean) + +// androidMain +@Composable +actual fun PlatformStatusBar(darkIcons: Boolean) { + val systemUiController = rememberSystemUiController() + SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) } +} + +// iosMain +@Composable +actual fun PlatformStatusBar(darkIcons: Boolean) { + // iOS handles this via UIKit interop or Info.plist +} +``` + +## 性能 + +### 用于可跳过重组的稳定类型 + +当所有属性都稳定时,将类标记为 `@Stable` 或 `@Immutable`: + +```kotlin +@Immutable +data class ItemUiModel( + val id: String, + val title: String, + val description: String, + val progress: Float +) +``` + +### 正确使用 `key()` 和惰性列表 + +```kotlin +LazyColumn { + items( + items = items, + key = { it.id } // Stable keys enable item reuse and animations + ) { item -> + ItemRow(item = item) + } +} +``` + +### 使用 `derivedStateOf` 延迟读取 + +```kotlin +val listState = rememberLazyListState() +val showScrollToTop by remember { + derivedStateOf { listState.firstVisibleItemIndex > 5 } +} +``` + +### 避免在重组中分配内存 + +```kotlin +// BAD — new lambda and list every recomposition +items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) } + +// GOOD — key each item so callbacks stay attached to the right row +val activeItems = remember(items) { items.filter { it.isActive } } +activeItems.forEach { item -> + key(item.id) { + ActiveItem(item, onClick = { handle(item) }) + } +} +``` + +## 主题 + +### Material 3 动态主题 + +```kotlin +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + if (darkTheme) dynamicDarkColorScheme(LocalContext.current) + else dynamicLightColorScheme(LocalContext.current) + } + darkTheme -> darkColorScheme() + else -> lightColorScheme() + } + + MaterialTheme(colorScheme = colorScheme, content = content) +} +``` + +## 应避免的反模式 + +* 在 ViewModel 中使用 `mutableStateOf`,而 `MutableStateFlow` 配合 `collectAsStateWithLifecycle` 对生命周期更安全 +* 将 `NavController` 深入传递到可组合项中 —— 应传递 lambda 回调 +* 在 `@Composable` 函数中进行繁重计算 —— 应移至 ViewModel 或 `remember {}` +* 使用 `LaunchedEffect(Unit)` 作为 ViewModel 初始化的替代 —— 在某些设置中,它会在配置更改时重新运行 +* 在可组合项参数中创建新的对象实例 —— 会导致不必要的重组 + +## 参考资料 + +查看技能:`android-clean-architecture` 了解模块结构和分层。 +查看技能:`kotlin-coroutines-flows` 了解协程和 Flow 模式。 diff --git a/docs/zh-CN/skills/configure-ecc/SKILL.md b/docs/zh-CN/skills/configure-ecc/SKILL.md index f84df4db..e45d8f85 100644 --- a/docs/zh-CN/skills/configure-ecc/SKILL.md +++ b/docs/zh-CN/skills/configure-ecc/SKILL.md @@ -86,7 +86,7 @@ Default: Core only ### 2b: 选择技能类别 -共有 27 项技能,分为 4 个类别。使用 `AskUserQuestion` 和 `multiSelect: true`: +共有41项技能,分为8个类别。使用 `AskUserQuestion` 配合 `multiSelect: true`: ``` Question: "Which skill categories do you want to install?" @@ -94,6 +94,11 @@ Options: - "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend patterns" - "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns" - "Workflow & Quality" — "TDD, verification, learning, security review, compaction" + - "Business & Content" — "Article writing, content engine, market research, investor materials, outreach" + - "Research & APIs" — "Deep research, Exa search, Claude API patterns" + - "Social & Content Distribution" — "X/Twitter API, crossposting alongside content-engine" + - "Media Generation" — "fal.ai image/video/audio alongside VideoDB" + - "Orchestration" — "dmux multi-agent workflows" - "All skills" — "Install every available skill" ``` @@ -154,6 +159,34 @@ Options: | `investor-materials` | 宣传文稿、一页简介、投资者备忘录和财务模型 | | `investor-outreach` | 个性化的投资者冷邮件、熟人介绍和后续跟进 | +**类别:研究与API(3项技能)** + +| 技能 | 描述 | +|-------|-------------| +| `deep-research` | 使用 firecrawl 和 exa MCP 进行多源深度研究,并生成带引用的报告 | +| `exa-search` | 通过 Exa MCP 进行网络、代码、公司和人员的神经搜索 | +| `claude-api` | Anthropic Claude API 模式:消息、流式处理、工具使用、视觉、批处理、Agent SDK | + +**类别:社交与内容分发(2项技能)** + +| 技能 | 描述 | +|-------|-------------| +| `x-api` | X/Twitter API 集成,用于发帖、线程、搜索和分析 | +| `crosspost` | 多平台内容分发,并进行平台原生适配 | + +**类别:媒体生成(2项技能)** + +| 技能 | 描述 | +|-------|-------------| +| `fal-ai-media` | 通过 fal.ai MCP 进行统一的AI媒体生成(图像、视频、音频) | +| `video-editing` | AI辅助视频编辑,用于剪辑、结构化和增强实拍素材 | + +**类别:编排(1项技能)** + +| 技能 | 描述 | +|-------|-------------| +| `dmux-workflows` | 使用 dmux 进行多智能体编排,实现并行智能体会话 | + **独立技能** | 技能 | 描述 | @@ -241,7 +274,11 @@ grep -rn "skills/" $TARGET/skills/ * `continuous-learning-v2` 引用 `~/.claude/homunculus/` 目录 * `python-testing` 可能引用 `python-patterns` * `golang-testing` 可能引用 `golang-patterns` -* 特定语言规则引用其 `common/` 对应项 +* `crosspost` 引用 `content-engine` 和 `x-api` +* `deep-research` 引用 `exa-search`(互补的 MCP 工具) +* `fal-ai-media` 引用 `videodb`(互补的媒体技能) +* `x-api` 引用 `content-engine` 和 `crosspost` +* 语言特定规则引用 `common/` 对应项 ### 4d:报告问题 diff --git a/docs/zh-CN/skills/continuous-learning-v2/SKILL.md b/docs/zh-CN/skills/continuous-learning-v2/SKILL.md index 34e5b09e..d1f4c202 100644 --- a/docs/zh-CN/skills/continuous-learning-v2/SKILL.md +++ b/docs/zh-CN/skills/continuous-learning-v2/SKILL.md @@ -260,6 +260,7 @@ mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,sk | +-- commands/ # Global generated commands +-- projects/ +-- a1b2c3d4e5f6/ # Project hash (from git remote URL) + | +-- project.json # Per-project metadata mirror (id/name/root/remote) | +-- observations.jsonl | +-- observations.archive/ | +-- instincts/ diff --git a/docs/zh-CN/skills/crosspost/SKILL.md b/docs/zh-CN/skills/crosspost/SKILL.md new file mode 100644 index 00000000..3fb49c95 --- /dev/null +++ b/docs/zh-CN/skills/crosspost/SKILL.md @@ -0,0 +1,209 @@ +--- +name: crosspost +description: 跨X、LinkedIn、Threads和Bluesky的多平台内容分发。使用内容引擎模式根据平台适配内容。从不跨平台发布相同内容。当用户希望跨社交平台分发内容时使用。 +origin: ECC +--- + +# 跨平台发布 + +将内容分发到多个社交平台,并适配各平台原生风格。 + +## 何时使用 + +* 用户希望将内容发布到多个平台 +* 在社交媒体上发布公告、产品发布或更新 +* 将某个平台的内容改编后发布到其他平台 +* 用户提及“跨平台发布”、“到处发帖”、“分享到所有平台”或“分发这个” + +## 运作方式 + +### 核心规则 + +1. **切勿在不同平台发布相同内容。** 每个平台都应获得原生适配版本。 +2. **主平台优先。** 先发布到主平台,再为其他平台适配。 +3. **遵循平台惯例。** 各平台的字符限制、格式、链接处理方式均不同。 +4. **每条帖子一个核心思想。** 如果源内容包含多个想法,请拆分成多条帖子。 +5. **注明出处很重要。** 如果转发他人的内容,请注明来源。 + +### 平台规格 + +| 平台 | 最大长度 | 链接处理 | 话题标签 | 媒体 | +|----------|-----------|---------------|----------|-------| +| X | 280 字符 (Premium 用户为 4000) | 计入长度 | 少量 (最多 1-2 个) | 图片、视频、GIF | +| LinkedIn | 3000 字符 | 不计入长度 | 3-5 个相关标签 | 图片、视频、文档、轮播 | +| Threads | 500 字符 | 独立的链接附件 | 通常不使用 | 图片、视频 | +| Bluesky | 300 字符 | 通过 Facets (富文本) | 无 (使用 Feeds) | 图片 | + +### 工作流程 + +### 步骤 1:创建源内容 + +从核心想法开始。使用 `content-engine` 技能来生成高质量草稿: + +* 识别单一核心信息 +* 确定主平台 (受众最大的平台) +* 首先为主平台撰写草稿 + +### 步骤 2:确定目标平台 + +询问用户或根据上下文确定: + +* 要发布到哪些平台 +* 优先级顺序 (主平台获得最佳版本) +* 任何平台特定要求 (例如,LinkedIn 需要专业语气) + +### 步骤 3:按平台适配 + +针对每个目标平台,转换内容: + +**X 平台适配:** + +* 用吸引人的开头,而非总结 +* 快速切入核心见解 +* 尽可能将链接放在正文之外 +* 对于较长内容,使用 Thread 格式 + +**LinkedIn 平台适配:** + +* 强有力的首行 (在“查看更多”前可见) +* 使用换行符的短段落 +* 围绕经验教训、结果或专业收获来构建内容 +* 比 X 提供更明确的背景信息 (LinkedIn 受众需要背景框架) + +**Threads 平台适配:** + +* 对话式、随意的语气 +* 比 LinkedIn 短,但比 X 压缩感弱 +* 如果可能,优先考虑视觉效果 + +**Bluesky 平台适配:** + +* 直接简洁 (300 字符限制) +* 社区导向的语气 +* 使用 Feeds/列表进行主题定位,而非话题标签 + +### 步骤 4:发布到主平台 + +首先发布到主平台: + +* 使用 `x-api` 技能处理 X +* 使用平台特定的 API 或工具处理其他平台 +* 捕获帖子 URL 以便交叉引用 + +### 步骤 5:发布到次级平台 + +将适配后的版本发布到其余平台: + +* 错开发布时间 (不要同时发布 — 间隔 30-60 分钟) +* 在适当的地方包含跨平台引用 (例如,“在 X 上有更长的 Thread”等) + +## 示例 + +### 源内容:产品发布 + +**X 版本:** + +``` +We just shipped [feature]. + +[One specific thing it does that's impressive] + +[Link] +``` + +**LinkedIn 版本:** + +``` +Excited to share: we just launched [feature] at [Company]. + +Here's why it matters: + +[2-3 short paragraphs with context] + +[Takeaway for the audience] + +[Link] +``` + +**Threads 版本:** + +``` +just shipped something cool — [feature] + +[casual explanation of what it does] + +link in bio +``` + +### 源内容:技术见解 + +**X 版本:** + +``` +TIL: [specific technical insight] + +[Why it matters in one sentence] +``` + +**LinkedIn 版本:** + +``` +A pattern I've been using that's made a real difference: + +[Technical insight with professional framing] + +[How it applies to teams/orgs] + +#relevantHashtag +``` + +## API 集成 + +### 批量跨平台发布服务 (示例模式) + +如果使用跨平台发布服务 (例如 Postbridge、Buffer 或自定义 API),模式如下: + +```python +import os +import requests + +resp = requests.post( + "https://your-crosspost-service.example/api/posts", + headers={"Authorization": f"Bearer {os.environ['POSTBRIDGE_API_KEY']}"}, + json={ + "platforms": ["twitter", "linkedin", "threads"], + "content": { + "twitter": {"text": x_version}, + "linkedin": {"text": linkedin_version}, + "threads": {"text": threads_version} + } + }, + timeout=30 +) +resp.raise_for_status() +``` + +### 手动发布 + +没有 Postbridge 时,使用各平台原生 API 发布: + +* X: 使用 `x-api` 技能模式 +* LinkedIn: 使用 OAuth 2.0 的 LinkedIn API v2 +* Threads: Threads API (Meta) +* Bluesky: AT Protocol API + +## 质量检查 + +发布前: + +* \[ ] 每个平台的版本读起来都符合该平台的自然风格 +* \[ ] 各平台内容不完全相同 +* \[ ] 遵守字符限制 +* \[ ] 链接有效且放置位置恰当 +* \[ ] 语气符合平台惯例 +* \[ ] 媒体文件尺寸适合各平台 + +## 相关技能 + +* `content-engine` — 生成平台原生内容 +* `x-api` — X/Twitter API 集成 diff --git a/docs/zh-CN/skills/customs-trade-compliance/SKILL.md b/docs/zh-CN/skills/customs-trade-compliance/SKILL.md new file mode 100644 index 00000000..f787a758 --- /dev/null +++ b/docs/zh-CN/skills/customs-trade-compliance/SKILL.md @@ -0,0 +1,256 @@ +--- +name: customs-trade-compliance +description: 海关文件、关税分类、关税优化、受限方筛查以及多司法管辖区法规合规的编码化专业知识。由拥有15年以上经验的贸易合规专家提供。包括HS分类逻辑、Incoterms应用、自贸协定利用以及罚款减免。适用于处理海关清关、关税分类、贸易合规、进出口文件或关税优化时使用。license: Apache-2.0 +version: 1.0.0 +homepage: https://github.com/affaan-m/everything-claude-code +origin: ECC +metadata: + author: evos + clawdbot: + emoji: "🌐" +--- + +# 海关与贸易合规 + +## 角色与背景 + +您是一位拥有 15 年以上经验的高级贸易合规专家,负责管理美国、欧盟、英国和亚太地区的海关业务。您处于进口商、出口商、海关经纪人、货运代理、政府机构和法律顾问的交汇点。您使用的系统包括 ACE(自动化商业环境)、CHIEF/CDS(英国)、ATLAS(德国)、海关经纪人门户网站、被拒方筛查平台以及 ERP 贸易管理模块。您的工作是确保货物合法、成本优化的跨境流动,同时保护组织免受罚款、扣押和禁止交易的处罚。 + +## 使用时机 + +* 为进出口商品进行 HS/HTS 税则号归类 +* 准备海关文件(商业发票、原产地证书、ISF 申报) +* 筛查交易方是否在被拒/受限实体名单上(SDN、实体清单、欧盟制裁) +* 评估 FTA 资格和关税节省机会 +* 应对海关审计、CF-28/CF-29 请求或罚款通知 + +## 运作方式 + +1. 使用 GRI 规则和章/品目/子目分析对产品进行归类 +2. 确定适用的关税税率、优惠计划(FTZs、退税、FTAs)和贸易救济措施 +3. 在发货前,对所有交易方进行综合被拒方名单筛查 +4. 根据司法管辖区要求准备并验证报关文件 +5. 监控法规变化(关税调整、新制裁、贸易协定更新) +6. 采用适当的主动披露和罚款减免策略回应政府问询 + +## 示例 + +* **HS 归类争议**:CBP 将您的电子元件从 8542(集成电路,0% 关税)重新归类为 8543(电机,2.6%)。使用 GRI 1 和 3(a) 结合技术规格、约束性预裁定和 EN 注释来构建论证。 +* **FTA 资格认定**:评估在墨西哥组装的商品是否符合 USMCA 优惠待遇。追溯 BOM 组件以确定区域价值成分和税则归类改变资格。 +* **被拒方筛查命中**:自动筛查标记某个客户为 OFAC 的 SDN 名单上的潜在匹配项。演练误报解决、上报程序和文件要求。 + +## 核心知识 + +### HS 税则归类 + +协调制度是由 WCO 维护的 6 位国际商品编码。前 2 位代表章,4 位代表品目,6 位代表子目。国家扩展会添加更多位数:美国使用 10 位 HTS 编码(出口使用 Schedule B),欧盟使用 10 位 TARIC 编码,英国通过 UK Global Tariff 使用 10 位商品编码。 + +归类严格遵循《归类总规则》的顺序——除非 GRI 1 失败,否则绝不引用 GRI 3;除非 GRI 1-3 失败,否则绝不引用 GRI 4: + +* **GRI 1:** 归类由品目条文和类注/章注决定。这解决了约 90% 的归类问题。在继续之前,应逐字阅读品目条文并核对所有相关的类和章注释。 +* **GRI 2(a):** 不完整或未制成品,如果具有完整品的基本特征,则按完整品归类。没有发动机的汽车车身仍按机动车辆归类。 +* **GRI 2(b):** 材料混合物和组合物。钢和塑料复合材料根据赋予基本特征的材料归类。 +* **GRI 3(a):** 当商品可归入两个或更多品目时,优先选择最具体的品目。"橡胶制外科手套"比"橡胶制品"更具体。 +* **GRI 3(b):** 组合商品、成套商品——按赋予基本特征的组件归类。包含 40 美元香水和 5 美元小袋的礼品套装按香水归类。 +* **GRI 3(c):** 当 3(a) 和 3(b) 均无法适用时,归入编码顺序中最后的品目。 +* **GRI 4:** 无法按 GRI 1-3 归类的商品,归入与其最相类似的商品品目。 +* **GRI 5:** 箱、容器和包装材料遵循与所装货物一并或分开归类的特定规则。 +* **GRI 6:** 子目级别的归类遵循相同原则,适用于相关品目内。子目注释在此级别具有优先性。 + +**常见的错误归类陷阱**:多功能设备(根据 GRI 3(b) 按主要功能归类,而不是按最昂贵的组件归类)。食品制品与配料(第 21 章 vs 第 7-12 章——检查产品是否经过超出简单保藏的"制作")。纺织品复合材料(纤维的重量百分比决定归类,而非表面积)。零件与附件(第十六类注释 2 决定零件是与机器一并归类还是单独归类)。物理介质上的软件(在大多数税则中,由介质而非软件决定归类)。 + +### 文件要求 + +**商业发票:** 必须包括卖方/买方名称和地址、足以用于归类的商品描述、数量、单价、总价值、币种、贸易术语、原产国和付款条件。美国 CBP 要求发票符合 19 CFR § 141.86。低报价值会触发 19 USC § 1592 的处罚。 + +**装箱单:** 每件包裹的重量和尺寸、与提单相符的唛头和编号、件数。装箱单与实物数量之间的差异会触发查验。 + +**原产地证书:** 因 FTA 而异。USMCA 使用一份证明(无规定格式),必须包含第 5.2 条规定的九个数据元素。EUR.1 流动证书用于欧盟优惠贸易。Form A 用于 GSP 申请。英国对 UK-EU TCA 申请使用发票上的"原产地声明"。 + +**提单 / 空运单:** 海运提单作为物权凭证、运输合同和收据。空运单不可转让。两者都必须与商业发票细节一致——承运人添加的批注("据称装有"、"托运人装载和计数")限制了承运人责任并影响海关风险评估。 + +**ISF 10+2(美国):** 进口商安全申报必须在外国港口装船前 24 小时提交。进口商提供十个数据元素(制造商、卖方、买方、收货方、原产国、HS-6 位编码、集装箱装箱地点、拼箱商、进口商登记号、收货人编号)。承运人提供两个。延迟或不准确的 ISF 会触发每项违规 5,000 美元的违约金。CBP 使用 ISF 数据进行布控——错误会增加查验概率。 + +**报关单摘要(CBP 7501):** 在报关后 10 个工作日内提交。包含归类、价值、关税税率、原产国和优惠计划申请。这是法律声明——此处的错误会引发 19 USC § 1592 下的处罚风险。 + +### 贸易术语 2020 + +贸易术语定义了买卖双方之间成本、风险和责任的转移。它们不是法律——它们是必须明确纳入的合同条款。关键的合规影响: + +* **EXW(工厂交货):** 卖方最低义务。买方安排一切。问题:买方是卖方国家的出口商,这给买方带来了其可能无法履行的出口合规义务。在国际贸易中很少适用。 +* **FCA(货交承运人):** 卖方在指定地点将货物交付给承运人。卖方负责出口清关。2020 年修订允许买方指示其承运人向卖方签发已装船提单——这对信用证交易至关重要。 +* **CPT/CIP(运费付至 / 运费和保险费付至):** 风险在第一个承运人处转移,但卖方支付至目的地的运费。CIP 现在要求协会货物保险条款(A)——一切险保障,这是与 2010 年贸易术语相比的重大变化。 +* **DAP(目的地交货):** 卖方承担至目的地的所有风险和费用,不包括进口清关和关税。卖方不在目的国办理清关。 +* **DDP(完税后交货):** 卖方承担一切,包括进口关税和税费。卖方必须注册为进口商或使用非居民进口商安排。海关估价基于 DDP 价格减去关税(倒扣法)——如果卖方将关税包含在发票价格中,会产生循环估价问题。 +* **估价影响:** 贸易术语影响发票结构,但海关估价仍遵循进口制度的规则。在美国,CBP 成交价格通常不包括国际运费和保险费;在欧盟,海关完税价格通常包括运至欧盟入境地点的运输和保险费用。即使商业条款明确,弄错这一点也会改变关税计算。 +* **常见误解:** 贸易术语不转移货物所有权——这由销售合同和适用法律管辖。贸易术语不默认适用于纯国内交易——必须明确引用。将 FOB 用于集装箱海运在技术上是不正确的(首选 FCA),因为 FOB 下风险在船舷转移,而 FCA 下风险在集装箱堆场转移。 + +### 关税优化 + +**FTA 利用:** 每个优惠贸易协定都有货物必须满足的特定原产地规则。USMCA 要求产品特定规则(附件 4-B),包括税则归类改变、区域价值成分和净成本法。EU-UK TCA 使用"完全获得"和"充分加工"规则,并在附件 ORIG-2 中有产品特定清单规则。RCEP 对 15 个亚太国家采用统一规则,并包含累积条款。AfCFTA 允许成员国之间 60% 的累积。 + +**RVC 计算事项:** USMCA 提供两种方法——成交价格法:RVC = ((TV - VNM) / TV) × 100,以及净成本法:RVC = ((NC - VNM) / NC) × 100。净成本法从分母中排除促销费、特许权使用费和运输成本,通常在利润率较低时产生更高的 RVC。 + +**对外贸易区(FTZs):** 进入 FTZ 的货物不在美国关税区内。好处:货物进入商业流通前关税递延、倒置关税减免(如果成品税率低于组件税率,则按成品税率缴纳关税)、废料/边角料无需缴纳关税、复出口货物无需缴纳关税。区与区之间的转移维持特许外国身份。 + +**临时进口保证金(TIBs):** ATA Carnet 用于专业设备、样品、展览品——免税进入 78+ 个国家。美国临时进口保证金(TIB)依据 19 USC § 1202, Chapter 98——货物必须在 1 年内出口(可延长至 3 年)。未能出口将导致按全额关税加保证金溢价进行清算。 + +**关税退税:** 退还进口货物随后出口时已缴关税的 99%。三种类型:生产退税(进口材料用于美国制造的出口产品)、未使用货物退税(进口货物以相同状态出口)和替代退税(商业上可互换的货物)。申请必须在进口后 5 年内提交。TFTEA 简化了退税流程——对于替代申请,不再要求将特定进口报关单与特定出口报关单进行匹配。 + +### 受限方筛查 + +**强制性名单(美国):** SDN(OFAC——特别指定国民)、实体清单(BIS——出口管制)、被拒人员清单(BIS——出口特权被拒)、未经核实清单(BIS——无法核实最终用途)、军事最终用户清单(BIS)、非 SDN 菜单式制裁(OFAC)。筛查必须涵盖交易中的所有相关方:买方、卖方、收货人、最终用户、货运代理、银行和中间收货人。 + +**欧盟/英国名单:** 欧盟综合制裁清单、英国 OFSI 综合清单、英国出口管制联合部门。 + +**触发强化尽职调查的警示信号:** 客户不愿提供最终用途信息。异常运输路线(高价值货物通过自由港)。客户愿意为昂贵物品支付现金。交付给货运代理或贸易公司,无明确最终用户。产品性能超出所述应用范围。客户缺乏该产品类型的业务背景。订单模式与客户业务不符。 + +**误报管理:** 约95%的筛查匹配为误报。判定需要:完全名称匹配与部分匹配对比、地址关联性、出生日期(针对个人)、国家关联性、别名分析。记录每次匹配的判定理由——监管机构审计时会询问。 + +### 区域特色 + +**美国海关与边境保护局:** 卓越与专业中心按行业划分。可信贸易商计划:C-TPAT(安全)和Trusted Trader(结合C-TPAT与ISA)。ACE是所有进出口数据的单一窗口。重点评估审计针对特定合规领域——在审计开始前主动披露至关重要。 + +**欧盟关税同盟:** 共同对外关税统一适用。授权经济运营商提供AEOC(海关简化)和AEOS(安全)。约束性关税信息提供为期3年的归类确定性。联盟海关法典自2016年起实施。 + +**英国脱欧后:** 英国全球关税取代了共同对外关税。北爱尔兰议定书/温莎框架创建双重身份货物。英国海关申报服务取代了CHIEF。英国-欧盟贸易与合作协定要求遵守原产地规则以获得零关税待遇——“原产”要求货物完全在英国/欧盟获得或经过充分加工。 + +**中国:** 列明产品类别在进口前需获得中国强制性产品认证。中国使用13位HS编码。跨境电商有独立的清关通道(9610、9710、9810贸易模式)。近期不可靠实体清单产生了新的筛查义务。 + +### 处罚与合规 + +**美国处罚框架依据19 USC § 1592:** + +* **疏忽:** 未缴关税的2倍或应税价值的20%(首次违规)。经减轻可降至1倍或10%。最常见的处罚。 +* **重大疏忽:** 未缴关税的4倍或应税价值的40%。较难减轻——需证明存在系统性合规措施。 +* **欺诈:** 货物的全部国内价值。可能移交刑事调查。除非有非同寻常的合作,否则无法减轻。 + +**主动披露:** 在CBP启动调查前提交主动披露,可将疏忽行为的罚款上限限制为未缴关税利息,重大疏忽行为的罚款上限限制为1倍关税。这是减轻处罚最有力的工具。要求:识别违规行为、提供正确信息、补缴未缴关税。必须在CBP发出处罚前通知或启动正式调查前提交。 + +**记录保存:** 19 USC § 1508要求所有报关记录保留5年。欧盟要求保留3年(部分成员国要求10年)。审计期间未能提供记录将产生不利推定——CBP可以按不利方式重构价值/归类。 + +## 决策框架 + +### 归类决策逻辑 + +对产品进行归类时,遵循此顺序,不可走捷径。在自动化任何税则归类工作流程前,将其转换为内部决策树。 + +1. **精确识别货物。** 获取完整技术规格——材料成分、功能、尺寸和预期用途。切勿仅凭产品名称归类。 +2. **确定章节和品目。** 使用章节和品目注释来确认或排除。品目注释优先于品目条文。 +3. **应用归类总规则一。** 按字面意思解读品目条文。如果只有一个品目涵盖该货物,归类即确定。 +4. **如果归类总规则一产生多个候选品目,** 依次应用归类总规则二和归类总规则三。对于组合货物,根据功能、价值、体积或对该特定货物最相关的因素确定基本特征。 +5. **在子目层面验证。** 应用归类总规则六。检查子目注释。确认国家税则子目(8/10位)与6位HS编码确定一致。 +6. **检查约束性裁定。** 在CBP CROSS数据库、欧盟BTI数据库或WCO归类意见中搜索相同或类似产品。现有裁定即使不直接约束也具有说服力。 +7. **记录理由。** 记录应用的归类总规则、考虑和排除的品目,以及决定因素。此文件是审计时的辩护依据。 + +### 自由贸易协定资格分析 + +1. 根据原产国和目的国**确定适用的自由贸易协定**。 +2. **确定产品特定原产地规则。** 在相关自由贸易协定的附件中查找HS品目。规则因产品而异——有些要求税则归类改变,有些要求最低区域价值成分,有些要求两者兼备。 +3. **追踪所有非原产材料**直至物料清单。必须对每种投入物进行归类以确定是否发生税则归类改变。 +4. **如需要,计算区域价值成分。** 选择产生最有利结果的方法(如果自由贸易协定提供选择)。与供应商核实所有成本数据。 +5. **应用累积规则。** 美墨加协定允许在美国、墨西哥和加拿大之间累积。欧盟-英国贸易与合作协定允许双边累积。区域全面经济伙伴关系协定允许所有15个缔约方之间的对角累积。 +6. **准备原产地证明。** 美墨加协定原产地证明必须包含九个规定数据要素。EUR.1需要商会或海关当局签注。保留支持文件5年(美墨加协定)或4年(欧盟)。 + +### 估价方法选择 + +海关估价遵循WTO《海关估价协定》。方法按层级顺序应用——仅当上一方法无法应用时才进入下一方法: + +1. **成交价格法:** 实际支付或应付价格,根据增加项目(协助、特许权费、佣金、包装)和扣除项目(进口后成本、关税)进行调整。用于约90%的报关。在以下情况失效:关联方交易且关系影响价格、无销售(寄售、租赁、免费货物),或具有无法量化条件的附条件销售。 +2. **相同货物成交价格法:** 相同货物、相同原产国、相同商业水平。很少可用,因为“相同”定义严格。 +3. **类似货物成交价格法:** 商业上可互换的货物。比方法2宽泛,但仍要求相同原产国。 +4. **倒扣价格法:** 从进口国转售价格开始,扣除:利润率、运输、关税及任何进口后加工成本。 +5. **计算价格法:** 根据出口国成本构建:材料成本、加工费、利润和一般费用。仅在出口商配合提供成本数据时可用。 +6. **合理方法:** 灵活应用方法1-5并进行合理调整。不能基于任意价值、最低价值或出口国国内市场货物价格。 + +### 筛查匹配评估 + +当受限制方筛查工具返回匹配时,不要自动阻止交易或未经调查即放行。遵循此规程: + +1. **评估匹配质量:** 名称匹配百分比、地址关联性、国家关联性、别名分析、出生日期(个人)。名称相似度低于85%且无地址或国家关联的匹配很可能是误报——记录并放行。 +2. **核实实体身份:** 交叉核对公司注册信息、邓白氏编码、网站验证以及过往交易历史。一个拥有多年清洁交易历史且与SDN条目部分名称匹配的合法客户几乎肯定是误报。 +3. **检查清单具体要求:** SDN匹配需要获得OFAC许可证才能进行。实体清单匹配需要获得BIS许可证且推定拒绝。拒绝人员清单匹配是绝对禁止——无许可证可用。 +4. **将真实匹配和模糊案例**立即上报给合规法律顾问。在筛查匹配未解决时切勿继续进行交易。 +5. **记录一切。** 记录使用的筛查工具、日期、匹配详情、判定理由和处理结果。至少保留5年。 + +## 关键边缘案例 + +这些是明显方法错误的情况。此处包含简要摘要,以便您可以根据需要将其扩展为特定项目手册。 + +1. **微量限额利用:** 供应商重组发货以保持在800美元美国微量限额以下,从而规避关税。CBP可能将同一日发往同一收货人的多批货物进行合并。第321条款条目不免除配额、反倾销/反补贴税或其他政府机构要求——仅免除关税。 + +2. **转运规避反倾销/反补贴税令:** 在中国制造但经越南转运且仅进行最低限度加工以声称越南原产的货物。CBP使用具有传票权的规避调查。“实质性转变”测试要求产生具有新名称、特征和用途的新商业物品。 + +3. **处于EAR/ITAR边界的军民两用物项:** 兼具商业和军事应用的部件。ITAR基于物项本身控制,EAR基于物项加上最终用途和最终用户控制。当归类模糊时需要申请商品管辖裁定。在错误制度下申报同时违反两种制度。 + +4. **进口后调整:** 关联方之间在报关结关后的转让定价调整。当最终价格在报关时未知时,CBP要求进行调账报关。未能调账会产生未付差额关税的补缴义务及罚款。 + +5. **关联方首次销售估价:** 使用中间商支付的价格(首次销售)而非进口商支付的价格(最后销售)作为海关估价。CBP在“首次销售规则”下允许此做法,但需证明首次销售是真实公平交易。欧盟和大多数其他司法管辖区不承认首次销售——它们以进口前的最后一次销售进行估价。 + +6. **追溯性自由贸易协定索赔:** 进口后18个月发现货物符合优惠待遇条件。美国允许在清算期内通过报关单后续更正进行追溯性索赔。欧盟要求原产地证书在进口时有效。时间和文件要求因自由贸易协定和司法管辖区而异。 + +7. **成套物品与零部件的归类:** 包含来自不同HS章节物品的零售套装(例如,包含帐篷、炉具和餐具的露营套装)。归类总规则三(二)按基本特征归类——但如果没有任何单一部件赋予基本特征,则适用归类总规则三(三)(按品目数字顺序归入最后一个品目)。“为零售而包装”的成套物品在归类总规则三(二)下有特定规则,与工业成套物品不同。 + +8. **临时进口变为永久进口:** 根据ATA单证册或临时进口保证金进口的设备,进口商决定保留。必须通过支付全额关税及任何罚款来核销单证册/保证金。如果临时进口期限已过但未出口或缴纳关税,将调用单证册担保,导致担保商会承担责任。 + +## 沟通模式 + +### 语气校准 + +根据对方、监管环境和风险级别调整沟通语气: + +* **报关代理(常规):** 协作且精准。提供完整的单证,标记异常项目,预先确认归类。"HS 8471.30 已确认——我们的 GRI 1 分析以及 2019 年 CBP 裁决 HQ H298456 支持此归类。已备齐 4 份所需单证中的 3 份,原产地证书将于今日下班前送达。" +* **报关代理(紧急扣留/查验):** 直接、基于事实、注重时效。"货物在洛杉矶/长滩港被扣留——CBP 要求提供制造商文件。正在发送制造商身份验证和生产记录。需要贵方在 2 小时内完成申报,以避免滞箱费。" +* **监管机构(裁决请求):** 正式、文件详尽、法律上精确。严格按照机构的既定格式提交。如要求,提供样品。切勿过度断言——使用"我们的立场是",而非"此产品归类为"。 +* **监管机构(处罚回应):** 审慎、合作、基于事实。如果存在错误,予以承认。系统性地陈述减轻处罚的因素。在事实支持疏忽的情况下,切勿承认欺诈。 +* **内部合规建议:** 明确业务影响、具体行动项、截止日期。将监管要求转化为操作语言。"自 3 月 1 日起,所有锂电池进口在报关时均需提供 UN 38.3 测试摘要。运营部门必须在订舱前向供应商收集这些文件。不合规后果:每票货物罚款及扣货费用超过 1 万美元。" +* **供应商问卷:** 具体、结构化、解释为何需要这些信息。了解自贸协定带来关税节省的供应商,会更愿意配合提供原产地数据。 + +### 关键模板 + +以下为简要模板。在生产环境中使用前,请根据您的报关代理、海关律师和监管流程进行调整。 + +**报关代理指示:** 主题:`Entry Instructions — {PO/shipment_ref} — {origin} to {destination}`。包含:归类及 GRI 依据、申报价值及贸易术语、自贸协定声明及支持文件索引、任何其他政府机构要求(如 FDA 预先通知、EPA TSCA 认证、FCC 声明)。 + +**主动披露申报:** 必须提交给有管辖权的 CBP 口岸关长或罚款、处罚和没收办公室。包含:报关单号、日期、具体违规事项、正确信息、应付关税以及补缴款项。 + +**内部合规警报:** 主题:`COMPLIANCE ACTION REQUIRED: {topic} — Effective {date}`。以业务影响开头,然后是监管依据,接着是要求的行动,最后是截止日期及不合规的后果。 + +## 升级协议 + +### 自动升级触发条件 + +| 触发条件 | 行动 | 时间线 | +|---|---|---| +| CBP 扣留或没收 | 通知副总裁和法律顾问 | 1 小时内 | +| 受限制方筛查结果为真阳性 | 暂停交易,通知合规官和法律部门 | 立即 | +| 潜在处罚风险 > 50,000 美元 | 通知贸易合规副总裁和总法律顾问 | 2 小时内 | +| 海关查验发现不符点 | 指派专人负责,通知报关代理 | 4 小时内 | +| 被拒方 / SDN 匹配确认 | 全球范围内完全停止与该实体的所有交易 | 立即 | +| 收到反倾销/反补贴税规避调查 | 聘请外部贸易法律顾问 | 24 小时内 | +| 收到外国海关当局的自贸协定原产地审计 | 通知所有受影响的供应商,开始文件审查 | 48 小时内 | +| 自愿自我披露决定 | 申报前必须获得法律顾问批准 | 提交前 | + +### 升级链 + +级别 1(分析师)→ 级别 2(贸易合规经理,4 小时)→ 级别 3(合规总监,24 小时)→ 级别 4(贸易合规副总裁,48 小时)→ 级别 5(总法律顾问 / 最高管理层,针对没收、SDN 匹配或处罚风险 > 10 万美元的情况立即处理) + +## 绩效指标 + +每月跟踪并季度趋势分析以下指标: + +| 指标 | 目标 | 红色警报 | +|---|---|---| +| 归类准确率(审计后) | > 98% | < 95% | +| 自贸协定利用率(符合条件的货物) | > 90% | < 70% | +| 报关单拒收率 | < 2% | > 5% | +| 主动披露频率 | < 2 次/年 | > 4 次/年 | +| 筛查误报判定时间 | < 4 小时 | > 24 小时 | +| 实现的关税节省(自贸协定 + 外贸区 + 退税) | 跟踪趋势 | 季度环比下降 | +| CBP 查验率 | < 3% | > 7% | +| 处罚风险(年度) | 0 美元 | 任何实质性处罚 | + +## 附加资源 + +* 将此技能与内部 HS 归类日志、报关代理升级矩阵以及一份列有您团队拥有非居民进口商或外贸区覆盖权限的司法管辖区清单结合使用。 +* 记录贵组织用于美国、欧盟和亚太航线的估价假设,以确保各团队间的关税计算保持一致。 diff --git a/docs/zh-CN/skills/deep-research/SKILL.md b/docs/zh-CN/skills/deep-research/SKILL.md new file mode 100644 index 00000000..c8292b4d --- /dev/null +++ b/docs/zh-CN/skills/deep-research/SKILL.md @@ -0,0 +1,163 @@ +--- +name: deep-research +description: 使用firecrawl和exa MCPs进行多源深度研究。搜索网络、综合发现并交付带有来源引用的报告。适用于用户希望对任何主题进行有证据和引用的彻底研究时。 +origin: ECC +--- + +# 深度研究 + +使用 firecrawl 和 exa MCP 工具,从多个网络来源生成详尽且有引用的研究报告。 + +## 何时激活 + +* 用户要求深入研究任何主题 +* 竞争分析、技术评估或市场规模测算 +* 对公司、投资者或技术的尽职调查 +* 任何需要综合多个来源信息的问题 +* 用户提到"研究"、"深入探讨"、"调查"或"当前状况如何" + +## MCP 要求 + +至少需要以下之一: + +* **firecrawl** — `firecrawl_search`, `firecrawl_scrape`, `firecrawl_crawl` +* **exa** — `web_search_exa`, `web_search_advanced_exa`, `crawling_exa` + +两者结合可提供最佳覆盖范围。在 `~/.claude.json` 或 `~/.codex/config.toml` 中配置。 + +## 工作流程 + +### 步骤 1:理解目标 + +提出 1-2 个快速澄清性问题: + +* "您的目标是什么——学习、做决策还是撰写内容?" +* "有任何特定的角度或深度要求吗?" + +如果用户说"直接研究即可"——则跳过此步,使用合理的默认设置。 + +### 步骤 2:规划研究 + +将主题分解为 3-5 个研究子问题。例如: + +* 主题:"人工智能对医疗保健的影响" + * 目前医疗保健领域的主要人工智能应用有哪些? + * 测量到了哪些临床结果? + * 存在哪些监管挑战? + * 哪些公司在该领域处于领先地位? + * 市场规模和增长轨迹如何? + +### 步骤 3:执行多源搜索 + +对**每个**子问题,使用可用的 MCP 工具进行搜索: + +**使用 firecrawl:** + +``` +firecrawl_search(query: "", limit: 8) +``` + +**使用 exa:** + +``` +web_search_exa(query: "", numResults: 8) +web_search_advanced_exa(query: "", numResults: 5, startPublishedDate: "2025-01-01") +``` + +**搜索策略:** + +* 每个子问题使用 2-3 个不同的关键词变体 +* 混合使用通用查询和新闻聚焦查询 +* 目标总共获取 15-30 个独特的来源 +* 优先级:学术、官方、知名新闻 > 博客 > 论坛 + +### 步骤 4:深度阅读关键来源 + +对于最有希望的 URL,获取完整内容: + +**使用 firecrawl:** + +``` +firecrawl_scrape(url: "") +``` + +**使用 exa:** + +``` +crawling_exa(url: "", tokensNum: 5000) +``` + +完整阅读 3-5 个关键来源以获得深度信息。不要仅依赖搜索片段。 + +### 步骤 5:综合并撰写报告 + +构建报告结构: + +```markdown +# [主题]:研究报告 +*生成日期:[date] | 来源数量:[N] | 置信度:[高/中/低]* + +## 执行摘要 +[3-5 句关键发现概述] + +## 1. [第一个主要主题] +[带有内联引用的发现] +- 关键点 ([Source Name](url)) +- 支持性数据 ([Source Name](url)) + +## 2. [第二个主要主题] +... + +## 3. [第三个主要主题] +... + +## 关键要点 +- [可执行的见解 1] +- [可执行的见解 2] +- [可执行的见解 3] + +## 来源 +1. [Title](url) — [一行摘要] +2. ... + +## 方法论 +搜索了网络和新闻中的 [N] 个查询。分析了 [M] 个来源。 +调查的子问题:[列表] +``` + +### 步骤 6:交付 + +* **简短主题**:在聊天中发布完整报告 +* **长篇报告**:发布执行摘要 + 关键要点,将完整报告保存到文件 + +## 使用子代理进行并行研究 + +对于广泛的主题,使用 Claude Code 的 Task 工具进行并行处理: + +``` +Launch 3 research agents in parallel: +1. Agent 1: Research sub-questions 1-2 +2. Agent 2: Research sub-questions 3-4 +3. Agent 3: Research sub-question 5 + cross-cutting themes +``` + +每个代理负责搜索、阅读来源并返回发现结果。主会话将其综合成最终报告。 + +## 质量规则 + +1. **每个主张都需要有来源**。不要有无来源的断言。 +2. **交叉验证**。如果只有一个来源提及,请将其标记为未经验证。 +3. **时效性很重要**。优先选择过去 12 个月内的来源。 +4. **承认信息缺口**。如果某个子问题找不到好的信息,请如实说明。 +5. **不捏造信息**。如果不知道,就说"未找到足够的数据"。 +6. **区分事实与推断**。清楚标注估计、预测和观点。 + +## 示例 + +``` +"Research the current state of nuclear fusion energy" +"Deep dive into Rust vs Go for backend services in 2026" +"Research the best strategies for bootstrapping a SaaS business" +"What's happening with the US housing market right now?" +"Investigate the competitive landscape for AI code editors" +``` diff --git a/docs/zh-CN/skills/dmux-workflows/SKILL.md b/docs/zh-CN/skills/dmux-workflows/SKILL.md new file mode 100644 index 00000000..d0e6564e --- /dev/null +++ b/docs/zh-CN/skills/dmux-workflows/SKILL.md @@ -0,0 +1,193 @@ +--- +name: dmux-workflows +description: 使用dmux(AI代理的tmux窗格管理器)进行多代理编排。跨Claude Code、Codex、OpenCode及其他工具的并行代理工作流模式。适用于并行运行多个代理会话或协调多代理开发工作流时。 +origin: ECC +--- + +# dmux 工作流 + +使用 dmux(一个用于代理套件的 tmux 窗格管理器)来编排并行的 AI 代理会话。 + +## 何时激活 + +* 并行运行多个代理会话时 +* 跨 Claude Code、Codex 和其他套件协调工作时 +* 需要分而治之并行处理的复杂任务 +* 用户提到“并行运行”、“拆分此工作”、“使用 dmux”或“多代理”时 + +## 什么是 dmux + +dmux 是一个基于 tmux 的编排工具,用于管理 AI 代理窗格: + +* 按 `n` 创建一个带有提示的新窗格 +* 按 `m` 将窗格输出合并回主会话 +* 支持:Claude Code、Codex、OpenCode、Cline、Gemini、Qwen + +**安装:** `npm install -g dmux` 或参见 [github.com/standardagents/dmux](https://github.com/standardagents/dmux) + +## 快速开始 + +```bash +# Start dmux session +dmux + +# Create agent panes (press 'n' in dmux, then type prompt) +# Pane 1: "Implement the auth middleware in src/auth/" +# Pane 2: "Write tests for the user service" +# Pane 3: "Update API documentation" + +# Each pane runs its own agent session +# Press 'm' to merge results back +``` + +## 工作流模式 + +### 模式 1:研究 + 实现 + +将研究和实现拆分为并行轨道: + +``` +Pane 1 (Research): "Research best practices for rate limiting in Node.js. + Check current libraries, compare approaches, and write findings to + /tmp/rate-limit-research.md" + +Pane 2 (Implement): "Implement rate limiting middleware for our Express API. + Start with a basic token bucket, we'll refine after research completes." + +# After Pane 1 completes, merge findings into Pane 2's context +``` + +### 模式 2:多文件功能 + +在独立文件间并行工作: + +``` +Pane 1: "Create the database schema and migrations for the billing feature" +Pane 2: "Build the billing API endpoints in src/api/billing/" +Pane 3: "Create the billing dashboard UI components" + +# Merge all, then do integration in main pane +``` + +### 模式 3:测试 + 修复循环 + +在一个窗格中运行测试,在另一个窗格中修复: + +``` +Pane 1 (Watcher): "Run the test suite in watch mode. When tests fail, + summarize the failures." + +Pane 2 (Fixer): "Fix failing tests based on the error output from pane 1" +``` + +### 模式 4:跨套件 + +为不同任务使用不同的 AI 工具: + +``` +Pane 1 (Claude Code): "Review the security of the auth module" +Pane 2 (Codex): "Refactor the utility functions for performance" +Pane 3 (Claude Code): "Write E2E tests for the checkout flow" +``` + +### 模式 5:代码审查流水线 + +并行审查视角: + +``` +Pane 1: "Review src/api/ for security vulnerabilities" +Pane 2: "Review src/api/ for performance issues" +Pane 3: "Review src/api/ for test coverage gaps" + +# Merge all reviews into a single report +``` + +## 最佳实践 + +1. **仅限独立任务。** 不要并行化相互依赖输出的任务。 +2. **明确边界。** 每个窗格应处理不同的文件或关注点。 +3. **策略性合并。** 合并前审查窗格输出以避免冲突。 +4. **使用 git worktree。** 对于容易产生文件冲突的工作,为每个窗格使用单独的工作树。 +5. **资源意识。** 每个窗格都消耗 API 令牌 —— 将总窗格数控制在 5-6 个以下。 + +## Git Worktree 集成 + +对于涉及重叠文件的任务: + +```bash +# Create worktrees for isolation +git worktree add -b feat/auth ../feature-auth HEAD +git worktree add -b feat/billing ../feature-billing HEAD + +# Run agents in separate worktrees +# Pane 1: cd ../feature-auth && claude +# Pane 2: cd ../feature-billing && claude + +# Merge branches when done +git merge feat/auth +git merge feat/billing +``` + +## 互补工具 + +| 工具 | 功能 | 使用时机 | +|------|-------------|-------------| +| **dmux** | 用于代理的 tmux 窗格管理 | 并行代理会话 | +| **Superset** | 用于 10+ 并行代理的终端 IDE | 大规模编排 | +| **Claude Code Task 工具** | 进程内子代理生成 | 会话内的程序化并行 | +| **Codex 多代理** | 内置代理角色 | Codex 特定的并行工作 | + +## ECC 助手 + +ECC 现在包含一个助手,用于使用独立的 git worktree 进行外部 tmux 窗格编排: + +```bash +node scripts/orchestrate-worktrees.js plan.json --execute +``` + +示例 `plan.json`: + +```json +{ + "sessionName": "skill-audit", + "baseRef": "HEAD", + "launcherCommand": "codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}", + "workers": [ + { "name": "docs-a", "task": "Fix skills 1-4 and write handoff notes." }, + { "name": "docs-b", "task": "Fix skills 5-8 and write handoff notes." } + ] +} +``` + +该助手: + +* 为每个工作器创建一个基于分支的 git worktree +* 可选择将主检出中的选定 `seedPaths` 覆盖到每个工作器的工作树中 +* 在 `.orchestration//` 下写入每个工作器的 `task.md`、`handoff.md` 和 `status.md` 文件 +* 启动一个 tmux 会话,每个工作器一个窗格 +* 在每个窗格中启动相应的工作器命令 +* 为主协调器保留主窗格空闲 + +当工作器需要访问尚未纳入 `HEAD` 的脏文件或未跟踪的本地文件(例如本地编排脚本、草案计划或文档)时,使用 `seedPaths`: + +```json +{ + "sessionName": "workflow-e2e", + "seedPaths": [ + "scripts/orchestrate-worktrees.js", + "scripts/lib/tmux-worktree-orchestrator.js", + ".claude/plan/workflow-e2e-test.json" + ], + "launcherCommand": "bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}", + "workers": [ + { "name": "seed-check", "task": "Verify seeded files are present before starting work." } + ] +} +``` + +## 故障排除 + +* **窗格无响应:** 直接切换到该窗格或使用 `tmux capture-pane -pt :0.` 检查它。 +* **合并冲突:** 使用 git worktree 隔离每个窗格的文件更改。 +* **令牌使用量高:** 减少并行窗格数量。每个窗格都是一个完整的代理会话。 +* **未找到 tmux:** 使用 `brew install tmux` (macOS) 或 `apt install tmux` (Linux) 安装。 diff --git a/docs/zh-CN/skills/energy-procurement/SKILL.md b/docs/zh-CN/skills/energy-procurement/SKILL.md new file mode 100644 index 00000000..80188609 --- /dev/null +++ b/docs/zh-CN/skills/energy-procurement/SKILL.md @@ -0,0 +1,220 @@ +--- +name: energy-procurement +description: 电力与燃气采购、电价优化、需量电费管理、可再生能源购电协议评估及多设施能源成本管理的编码化专业知识。基于能源采购经理在大型工商业用户中超过15年的经验。包括市场结构分析、对冲策略、负荷分析和可持续性报告框架。适用于采购能源、优化电价、管理需量电费、评估购电协议或制定能源策略时使用。license: Apache-2.0 +version: 1.0.0 +homepage: https://github.com/affaan-m/everything-claude-code +origin: ECC +metadata: + author: evos + clawdbot: + emoji: "⚡" +--- + +# 能源采购 + +## 角色与背景 + +您是一家大型工商业用户的资深能源采购经理,该用户在受监管和放松管制的电力市场中拥有多处设施。您管理着分布在10-50多个站点的年度能源支出,金额在1500万至8000万美元之间,这些站点包括制造工厂、配送中心、企业办公室和冷藏设施。您负责整个采购生命周期:费率分析、供应商招标、合同谈判、需量费用管理、可再生能源采购、预算预测和可持续发展报告。您处于运营(控制负荷)、财务(负责预算)、可持续发展(设定排放目标)和执行领导层(批准长期承诺,如购电协议)之间。您使用的系统包括公用事业账单管理平台、间隔数据分析、能源市场数据提供商和采购平台。您需要在降低成本、预算确定性、可持续发展目标和运营灵活性之间取得平衡——因为一个节省8%但在极地涡旋年份导致公司预算出现200万美元偏差的采购策略并不是一个好策略。 + +## 使用时机 + +* 为多个设施的电力或天然气供应进行招标 +* 分析费率结构和费率优化机会 +* 评估需量费用缓解策略 +* 评估现场或虚拟可再生能源的购电协议报价 +* 制定年度能源预算和对冲头寸策略 +* 应对市场波动事件 + +## 工作原理 + +1. 使用间隔电表数据分析每个设施的负荷曲线,以识别成本驱动因素 +2. 分析当前费率结构并识别优化机会 +3. 构建具有适当产品规格的采购招标书 +4. 使用总能源成本评估投标,包括容量、输电、辅助服务和风险溢价 +5. 执行具有交错条款和分层对冲的合同,以避免集中风险 +6. 监控市场头寸,在触发事件时重新平衡对冲,并每月报告预算偏差 + +## 示例 + +* **多站点招标**:在PJM和ERCOT地区拥有25个设施,年度支出4000万美元。构建招标书以获取负荷多样性效益,评估6家供应商在固定、指数和区块指数产品上的投标,并推荐一个混合策略,将60%的用量锁定在固定费率,同时保持40%的指数敞口。 +* **需量费用缓解**:位于Con Edison辖区的制造工厂,在2MW峰值时支付28美元/kW的需量费用。分析间隔数据以识别前10个设定需量的时段,评估电池储能与负荷削减和功率因数校正的经济性,并计算投资回收期。 +* **购电协议评估**:太阳能开发商提供一份为期15年、价格为35美元/MWh的虚拟购电协议,在结算枢纽存在5美元/MWh的基差风险。根据远期曲线模拟预期节省,使用历史节点到枢纽价差量化基差风险敞口,并向首席财务官展示风险调整后的净现值,并提供高/低天然气价格环境的情景分析。 + +## 核心知识 + +### 定价结构与公用事业账单剖析 + +每份商业电费账单都有必须独立理解的组成部分——将它们捆绑成一个单一的"费率"会掩盖真正的优化机会所在: + +* **能源费用**:消耗电力的每千瓦时成本。可以是固定费率、分时电价或实时电价。对于大型工商业用户,能源费用通常占总账单的40–55%。在放松管制的市场中,这是您可以竞争性采购的组成部分。 +* **需量费用**:根据计费周期内以15分钟为间隔测量的峰值千瓦数计费。需量费用占制造工厂账单的20–40%。一个糟糕的15分钟间隔——压缩机启动与暖通空调峰值同时发生——可能使月度账单增加5000–15000美元。 +* **容量费用**:在有容量义务的市场中,您承担的电网容量成本份额根据您在前一年系统峰值时段的峰值负荷贡献进行分配。在这些关键时段减少负荷可以使下一年的容量费用降低15–30%。这是大多数工商业用户投资回报率最高的需求响应机会。 +* **输电和配电费用**:将电力从发电端输送到您电表的受监管费用。输电通常基于您对区域输电峰值的贡献。配电包括客户费用、基于需量的配送费用和按量配送费用。这些通常是不可绕过的——即使有现场发电,您也需要为接入电网支付配电费用。 +* **附加费和附加条款**:可再生能源标准合规性、核电站退役、公用事业转型费用和监管要求的计划。这些通过费率案例进行变更。公用事业费率案例申请可能使您的交付成本增加0.005–0.015美元/kWh——请关注您所在州公用事业委员会的公开程序。 + +### 采购策略 + +放松管制市场中的核心决策是保留多少价格风险与转移给供应商: + +* **固定价格**:供应商在合同期内以锁定的$/kWh价格提供所有电力。提供预算确定性。您支付风险溢价——通常在合同签署时比远期曲线高5–12%——因为供应商承担了价格、用量和基差风险。最适合预算可预测性优于成本最小化的组织。 +* **指数/可变定价**:您支付实时或日前批发价格加上供应商附加费。长期平均成本最低,但完全暴露于价格飙升风险。指数定价需要积极的风险管理和能够容忍预算偏差的企业文化。 +* **区块指数定价**:您购买固定价格区块来覆盖您的基本负荷,并让剩余的变动负荷按指数浮动。这平衡了成本优化与部分预算确定性。区块应与您的基本负荷曲线匹配。 +* **分层采购**:与其在一个时间点锁定全部负荷,不如在12–24个月内分批购买。这是大多数工商业买家可用的最有效的风险管理技术——它消除了"我们是否在顶部锁定?"的问题。 +* **放松管制市场中的招标流程**:向5–8家合格的零售能源提供商发布招标书。评估总成本、供应商信用质量、合同灵活性和增值服务。 + +### 需量费用管理 + +对于具有运营灵活性的设施,需量费用是最可控的成本组成部分: + +* **峰值识别**:从您的公用事业公司或电表数据管理系统下载15分钟间隔数据。识别每月前10个峰值时段。在大多数设施中,前10个峰值中有6–8个具有共同的根本原因——多个大型负荷在早上6:00–9:00的启动期间同时启动。 +* **负荷转移**:将可自由支配的负荷转移到非高峰时段。 +* **使用电池进行峰值削减**:表后电池储能可以通过在最高需量的15分钟时段放电来限制峰值需求。 +* **需求响应计划**:公用事业公司和独立系统运营商运营的计划,在电网紧张事件期间向用户支付削减负荷的费用。 +* **棘轮条款**:许多费率包含需量棘轮条款——您的计费需量不能低于前11个月记录的最高峰值需量的60–80%。在可能导致峰值负荷激增的任何设施改造之前,请务必检查您的费率是否包含棘轮条款。 + +### 可再生能源采购 + +* **实物购电协议(PPA):** 您直接与可再生能源发电商(太阳能/风电场)签订合同,以固定的 $/MWh 价格购买其电力输出,为期 10-25 年。发电商通常与您的用电负荷位于同一独立系统运营商(ISO)区域内,电力通过电网输送到您的电表。您既获得电能,也获得相关的可再生能源证书(REC)。实物购电协议要求您管理基差风险(发电商节点价格与您负荷区域价格之间的差异)、限电风险(当 ISO 限制发电商出力时)以及形态风险(太阳能只在有日照时发电,而非在您用电时)。 +* **虚拟(金融)购电协议(VPPA):** 一种差价合约。您约定一个固定的执行价格(例如 $35/MWh)。发电商以结算点价格将电力出售到批发市场。如果市场价格是 $45/MWh,发电商向您支付 $10/MWh。如果市场价格是 $25/MWh,您向发电商支付 $10/MWh。您获得 REC 以声明可再生属性。VPPA 不改变您的物理电力供应——您继续从零售供应商处购电。VPPA 是金融工具,可能需要 CFO/财务部门批准、ISDA 协议以及按市值计价会计处理。 +* **可再生能源证书(REC):** 1 个 REC = 1 MWh 的可再生能源发电属性。非捆绑 REC(与物理电力分开购买)是声明使用可再生能源的最便宜方式——全国性风电 REC 为 $1–$5/MWh,太阳能 REC 为 $5–$15/MWh,特定区域市场(新英格兰、PJM)为 $20–$60/MWh。然而,根据温室气体核算体系(GHG Protocol)范围 2 指南,非捆绑 REC 正面临日益严格的审查:它们满足市场法核算要求,但无法证明“额外性”(即导致新的可再生能源发电设施被建造)。 +* **现场发电:** 屋顶或地面安装的太阳能、热电联产(CHP)。现场太阳能购电协议定价:$0.04–$0.08/kWh,具体取决于地点、系统规模和投资税收抵免(ITC)资格。现场发电减少了输配电(T\&D)费用暴露,并可以降低容量标签。但表后发电引入了净计量风险(公用事业补偿费率变化)、并网成本和场地租赁复杂性。应根据总经济价值(而不仅仅是能源成本)评估现场发电与场外发电。 + +### 负荷分析 + +了解您设施的负荷形态是每个采购和优化决策的基础: + +* **基础负荷与可变负荷:** 基础负荷全天候运行——工艺制冷、服务器机房、连续制造、有人区域的照明。可变负荷与生产计划、人员占用和天气(暖通空调)相关。负荷系数为 0.85(基础负荷占峰值的 85%)的设施受益于全天候的整块电力采购。负荷系数为 0.45(占用与非占用期间波动巨大)的设施受益于与峰/谷时段模式匹配的形态化产品。 +* **负荷系数:** 平均需求除以峰值需求。负荷系数 = (总 kWh)/(峰值 kW × 时段小时数)。高负荷系数(>0.75)意味着相对平稳、可预测的消耗——更易于采购且每 kWh 的需求费用更低。低负荷系数(<0.50)意味着消耗具有尖峰特征,峰均比高——需求费用在您的账单中占主导地位,并且削峰的投资回报率最高。 +* **各系统贡献:** 在制造业中,典型的负荷分解为:暖通空调 25–35%,生产电机/驱动器 30–45%,压缩空气 10–15%,照明 5–10%,工艺加热 5–15%。对峰值需求贡献最大的系统并不总是能耗最高的系统——压缩空气系统由于空载运行和压缩机循环,通常具有最差的峰均比。 + +### 市场结构 + +* **受管制市场:** 单一公用事业公司提供发电、输电和配电服务。费率由州公共事业委员会(PUC)通过定期费率审查设定。您不能选择电力供应商。优化仅限于费率方案选择(在可用费率计划之间切换)、需求费用管理和现场发电。美国约 35% 的商业电力负荷处于完全受管制的市场中。 +* **放松管制市场:** 发电环节具有竞争性。您可以从合格的零售能源供应商(REP)、直接从批发市场(如果您有基础设施和信用)或通过经纪人/聚合商购买电力。独立系统运营商/区域输电组织(ISO/RTO)运营批发市场:PJM(大西洋中部和中西部,美国最大市场)、ERCOT(德克萨斯州,独特的独立电网)、CAISO(加利福尼亚州)、NYISO(纽约州)、ISO-NE(新英格兰)、MISO(美国中部)、SPP(平原各州)。每个 ISO 有不同的市场规则、容量结构和定价机制。 +* **节点边际电价(LMP):** 批发电力价格在 ISO 内因地点(节点)而异,反映了发电成本、输电损耗和阻塞情况。LMP = 能量分量 + 阻塞分量 + 损耗分量。位于阻塞节点的设施比位于非阻塞节点的设施支付更多费用。在受约束的区域,阻塞可能使您的交付成本增加 $5–$30/MWh。评估 VPPA 时,发电商节点与您负荷区域之间的基差风险由阻塞模式驱动。 + +### 可持续发展报告 + +* **范围 2 排放——两种方法:** 温室气体核算体系要求双重报告。基于地理位置法:使用您所在区域的平均电网排放因子(美国使用 eGRID)。基于市场法:反映您的采购选择——如果您购买 REC 或签订购电协议,您的市场法排放会减少。大多数以 RE100 或 SBTi 认证为目标的公司关注市场法范围 2 排放。 +* **RE100:** 一项全球倡议,企业承诺使用 100% 可再生电力。要求每年报告进展。可接受的工具包括:实物购电协议、附带 REC 的 VPPA、公用事业绿色电价计划、非捆绑 REC(尽管 RE100 正在收紧额外性要求)以及现场发电。 +* **CDP 和 SBTi:** CDP(前身为碳披露项目)评估企业气候信息披露。能源采购数据直接输入您的 CDP 气候变化问卷——C8 部分(能源)。SBTi(科学碳目标倡议)验证您的减排目标是否符合《巴黎协定》目标。锁定化石燃料密集型电力供应 10 年以上的采购决策可能与 SBTi 减排路径冲突。 + +### 风险管理 + +* **对冲方法:** 分层采购是主要对冲手段。辅以针对特定风险敞口的金融对冲工具(掉期、期权、热值看涨期权)。购买批发电力看跌期权以封顶您的指数定价风险敞口——$50/MWh 的看跌期权成本为 $2–$5/MWh 的权利金,但可以防止 $200+/MWh 的批发价格飙升带来的灾难性尾部风险。 +* **预算确定性与市场风险敞口:** 基本的权衡取舍。固定价格合同以溢价提供确定性。指数合同提供较低的平均成本但方差较高。大多数成熟的商业和工业(C\&I)买家最终采用 60–80% 对冲、20–40% 指数敞口的策略——具体比例取决于公司的财务状况、财务部门风险承受能力以及能源是主要投入成本(制造业)还是管理费用项目(办公场所)。 +* **天气风险:** 采暖度日(HDD)和制冷度日(CDD)驱动消耗量的变化。比正常情况冷 15% 的冬季可能使天然气成本比预算高出 25–40%。天气衍生品(HDD/CDD 掉期和期权)可以对冲数量风险——但大多数 C\&I 买家通过预算准备金而非金融工具来管理天气风险。 +* **监管风险:** 费率审查导致的费率变化、容量市场改革(PJM 的容量市场自 2015 年以来已三次重组定价)、碳定价立法以及净计量政策变化,都可能在合同期内改变您采购策略的经济性。 + +## 决策框架 + +### 采购策略选择 + +为合同续签在固定价格、指数价格和整块-指数混合方案之间进行选择时: + +1. **公司的预算波动容忍度是多少?** 如果能源成本波动 >5% 就会触发管理层审查,则倾向于固定价格。如果公司能够承受 15–20% 的波动而无财务压力,则指数或整块-指数方案可行。 +2. **市场处于价格周期的哪个阶段?** 如果远期曲线处于 5 年区间的底部三分之一,锁定更多固定价格(逢低买入)。如果远期曲线处于顶部三分之一,保持更多指数敞口(避免在峰值锁定)。如果不确定,则分层采购。 +3. **合同期限是多长?** 对于 12 个月期限,固定与指数差别不大——溢价较小且风险敞口期短。对于 36 个月以上期限,固定价格的溢价会累积,多付钱的可能性增加。对于较长期限,倾向于混合或分层策略。 +4. **设施的负荷系数是多少?** 高负荷系数(>0.75):整块-指数方案效果良好——购买全天候的平坦电力块。低负荷系数(<0.50):形态化电力块或分时电价指数产品能更好地匹配负荷形态。 + +### 购电协议评估 + +在签订 10–25 年购电协议之前,评估: + +1. **项目经济性是否成立?** 将购电协议执行价格与合同期限的远期曲线进行比较。$35/MWh 的太阳能购电协议相对于 $45/MWh 的远期曲线有 $10/MWh 的正价差。但需要对整个合同期建模——签约时处于价内的 $35/MWh 20 年期购电协议,如果由于该地区可再生能源过度建设导致批发价格跌破执行价,可能会转为价外。 +2. **基差风险有多大?** 如果发电商位于西德克萨斯(ERCOT 西部),而您的负荷在休斯顿(ERCOT 休斯顿),两个区域之间的阻塞可能造成 $3–$12/MWh 的持续基差,侵蚀购电协议价值。要求开发商提供项目节点与您负荷区域之间 5 年以上的历史基差数据。 +3. **限电风险敞口有多大?** ERCOT 每年限电风电 3–8%;CAISO 在春季月份限电太阳能 5–12%。如果购电协议按实际发电量(而非计划发电量)结算,限电会减少您的 REC 交付并改变经济性。谈判限电上限或不因电网运营商限电而惩罚您的结算结构。 +4. **信用要求是什么?** 开发商通常要求投资级信用或信用证/母公司担保来签订长期购电协议。$5000 万美元名义本金的 VPPA 可能需要 $500–$1000 万美元的信用证,占用资金。将信用证成本纳入您的购电协议经济性评估。 + +### 需求费用削减的投资回报率评估 + +使用总叠加价值评估需求费用削减投资: + +1. 计算当前需求费用:峰值 kW × 需求费率 × 12 个月。 +2. 估算拟议干预措施(电池、负荷控制、需求响应)可实现的峰值削减。 +3. 评估削减在所有适用费率组成部分中的价值:需求费用 + 容量标签削减(在下个交付年度生效)+ 分时电价套利 + 需求响应项目收入。 +4. 如果叠加价值的简单投资回收期 < 5 年,投资通常合理。如果为 5–8 年,则处于边际状态,取决于资金可用性。如果叠加价值 > 8 年,除非受可持续发展要求驱动,否则经济性不佳。 + +### 市场择时 + +永远不要试图“预测”能源市场的底部。相反: + +* 监控远期曲线相对于 5 年历史区间的水平。当远期曲线处于底部四分位数时,加速采购(比分层采购计划更快地买入份额)。当处于顶部四分位数时,减速(让现有份额滚动并增加指数敞口)。 +* 关注结构性信号:新增发电容量(对价格看跌)、电厂退役(看涨)、天然气管道约束(区域价格分化)以及容量市场拍卖结果(影响未来容量费用)。 + +将上述采购顺序用作决策框架基线,并根据您的费率结构、采购日程和董事会批准的对冲限额进行调整。 + +## 关键边缘案例 + +以下是标准采购方案可能导致不良后果的几种情况。此处提供简要概述,以便您在需要时将其扩展为针对特定项目的操作方案。 + +1. **ERCOT极端天气下的价格飙升**:冬季风暴尤里证明,ERCOT采用指数定价的客户面临灾难性的尾部风险。一个5兆瓦的设施采用指数定价,单周内损失超过150万美元。教训并非“避免指数定价”,而是“在ERCOT地区进入冬季时,如果没有价格上限或金融对冲,切勿不进行对冲操作”。 + +2. **阻塞区域的虚拟PPA基差风险**:与西得克萨斯州风电场签订的虚拟PPA,以休斯顿负荷区价格结算,可能因输电阻塞导致持续3-12美元/兆瓦时的负结算额,从而使原本看似有利的PPA变成净成本。 + +3. **需量费用棘轮陷阱**:设施改造(新生产线、冷水机组更换启动)导致单月峰值比正常水平高出50%。费率条款中的80%棘轮条款会将较高的计费需量锁定11个月。一次15分钟的间隔可能导致年度成本增加20万美元。 + +4. **合同期内公用事业费率案例申请**:您的固定价格供应合同涵盖能源部分,但输配电和附加费用仍需支付。公用事业费率案例使输送费用增加0.012美元/千瓦时——对于一个12兆瓦的设施,这意味着年度增加15万美元,而您的“固定”合同无法提供保护。 + +5. **负LMP定价影响PPA经济性**:在高风能或高太阳能期间,发电节点的批发价格变为负值。在某些PPA结构下,您需向开发商支付负价格时段的结算差额,从而产生意外支出。 + +6. **表后太阳能侵蚀需求响应价值**:现场太阳能降低了您的平均用电量,但可能无法降低峰值(峰值通常出现在多云午后)。如果您的需求响应基线是根据近期用电量计算的,太阳能会降低基线,从而减少您的需求响应削减能力和相关收入。 + +7. **容量市场义务意外**:在PJM,您的容量标签由您在上一年5个重合峰值时段的负荷决定。如果您在恰逢峰值时段的热浪期间运行备用发电机或增加产量,您的容量标签会飙升,导致下一个交付年度的容量费用增加20-40%。 + +8. **放松管制市场重新监管风险**:州立法机构在价格飙升事件后提议重新监管。如果实施,您通过竞争性采购获得的供应合同可能被作废,您将恢复到公用事业费率——可能比您谈判的合同成本更高。 + +## 沟通模式 + +### 供应商谈判 + +能源供应商谈判是多年的合作关系。需调整语气: + +* **发布RFP**:专业、数据丰富、具有竞争性。提供完整的间隔数据和负荷曲线。无法准确模拟您负荷的供应商会提高其利润。透明度可降低风险溢价。 +* **合同续签**:首先强调关系价值和业务量增长,而非价格要求。“我们珍视过去36个月的合作关系,希望讨论能反映市场条件和我们不断增长的业务组合的续约条款。” +* **价格挑战**:引用具体的市场数据。“ICE 2027年AEP代顿枢纽的远期曲线显示为42美元/兆瓦时。您48美元/兆瓦时的报价比曲线高出14%——您能帮助我们理解这种价差的原因吗?” + +### 内部利益相关者 + +* **财务/资金部门**:用量化的预算影响、方差和风险来表述决策。“这种区块加指数结构提供了75%的预算确定性,相对于1200万美元的年度能源预算,模型预测的最坏情况方差为±40万美元。” +* **可持续发展部门**:将采购决策与范围2目标对应。“这份PPA每年提供5万兆瓦时的捆绑REC,占我们RE100目标的35%。” +* **运营部门**:专注于运营要求和约束。“我们需要在夏季午后减少400千瓦的峰值需求——这里有三个不影响生产计划的方案。” + +使用这里的沟通示例作为起点,并根据您的供应商、公用事业和高管利益相关者的工作流程进行调整。 + +## 升级协议 + +| 触发条件 | 行动 | 时间线 | +|---|---|---| +| 批发价格连续5天以上超过预算假设的2倍 | 通知财务部门,评估对冲头寸,考虑紧急固定价格采购 | 24小时内 | +| 供应商信用评级降至投资级以下 | 审查合同终止条款,评估替代供应商选项 | 48小时内 | +| 公用事业费率案例申请,提议涨幅>10% | 聘请监管法律顾问,评估干预申请 | 1周内 | +| 需求峰值超过棘轮阈值>15% | 与运营部门调查根本原因,模拟计费影响,评估缓解措施 | 24小时内 | +| PPA开发商未能交付超过合同量10%的REC | 根据合同发出违约通知,评估替代REC采购 | 5个工作日内 | +| 容量标签较上年增加>20% | 分析重合峰值时段,模拟容量费用影响,制定峰值响应计划 | 2周内 | +| 监管行动威胁合同可执行性 | 聘请法律顾问,评估合同不可抗力条款 | 48小时内 | +| 电网紧急情况/轮流停电影响设施 | 启动紧急负荷削减,与运营部门协调,为保险目的记录 | 立即 | + +### 升级链 + +能源分析师 → 能源采购经理(24小时) → 采购总监(48小时) → 财务副总裁/首席财务官(风险敞口>50万美元或长期承诺>5年) + +## 绩效指标 + +每月跟踪,每季度与财务和可持续发展部门审查: + +| 指标 | 目标 | 红色警报 | +|---|---|---| +| 加权平均能源成本 vs. 预算 | 在±5%以内 | 方差>10% | +| 采购成本 vs. 市场基准(执行时的远期曲线) | 在市场价3%以内 | 溢价>8% | +| 需量费用占总账单百分比 | <25%(制造业) | >35% | +| 峰值需求 vs. 上年同期(天气标准化后) | 持平或下降 | 增加>10% | +| 可再生能源百分比(基于市场的范围2) | 按RE100目标年度进度进行 | 落后进度>15% | +| 供应商合同续签提前期 | 到期前≥90天签署 | 到期前<30天 | +| 容量标签趋势 | 持平或下降 | 同比增加>15% | +| 预算预测准确性(第一季度预测 vs. 实际) | 在±7%以内 | 偏差>12% | + +## 其他资源 + +* 在本技能之外,还需维护经批准的内部对冲政策、交易对手名单和费率变更日历。 +* 将特定设施的负荷曲线和公用事业合同元数据保持在规划工作流附近,以确保建议基于实际需求模式。 diff --git a/docs/zh-CN/skills/exa-search/SKILL.md b/docs/zh-CN/skills/exa-search/SKILL.md new file mode 100644 index 00000000..ac071a60 --- /dev/null +++ b/docs/zh-CN/skills/exa-search/SKILL.md @@ -0,0 +1,186 @@ +--- +name: exa-search +description: 通过Exa MCP进行神经搜索,适用于网络、代码和公司研究。当用户需要网络搜索、代码示例、公司情报、人员查找,或使用Exa神经搜索引擎进行AI驱动的深度研究时使用。 +origin: ECC +--- + +# Exa 搜索 + +通过 Exa MCP 服务器实现网页内容、代码、公司和人物的神经搜索。 + +## 何时激活 + +* 用户需要当前网页信息或新闻 +* 搜索代码示例、API 文档或技术参考资料 +* 研究公司、竞争对手或市场参与者 +* 查找特定领域的专业资料或人物 +* 为任何开发任务进行背景调研 +* 用户提到“搜索”、“查找”、“寻找”或“关于……的最新消息是什么” + +## MCP 要求 + +必须配置 Exa MCP 服务器。添加到 `~/.claude.json`: + +```json +"exa-web-search": { + "command": "npx", + "args": [ + "-y", + "exa-mcp-server", + "tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check" + ], + "env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" } +} +``` + +在 [exa.ai](https://exa.ai) 获取 API 密钥。 +如果省略 `tools=...` 参数,可能只会启用较小的默认工具集。 + +## 核心工具 + +### web\_search\_exa + +用于当前信息、新闻或事实的通用网页搜索。 + +``` +web_search_exa(query: "latest AI developments 2026", numResults: 5) +``` + +**参数:** + +| 参数 | 类型 | 默认值 | 说明 | +|-------|------|---------|-------| +| `query` | string | 必需 | 搜索查询 | +| `numResults` | number | 8 | 结果数量 | + +### web\_search\_advanced\_exa + +具有域名和日期约束的过滤搜索。 + +``` +web_search_advanced_exa( + query: "React Server Components best practices", + numResults: 5, + includeDomains: ["github.com", "react.dev"], + startPublishedDate: "2025-01-01" +) +``` + +**参数:** + +| 参数 | 类型 | 默认值 | 说明 | +|-------|------|---------|-------| +| `query` | string | 必需 | 搜索查询 | +| `numResults` | number | 8 | 结果数量 | +| `includeDomains` | string\[] | 无 | 限制在特定域名 | +| `excludeDomains` | string\[] | 无 | 排除特定域名 | +| `startPublishedDate` | string | 无 | ISO 日期过滤器(开始) | +| `endPublishedDate` | string | 无 | ISO 日期过滤器(结束) | + +### get\_code\_context\_exa + +从 GitHub、Stack Overflow 和文档站点查找代码示例和文档。 + +``` +get_code_context_exa(query: "Python asyncio patterns", tokensNum: 3000) +``` + +**参数:** + +| 参数 | 类型 | 默认值 | 说明 | +|-------|------|---------|-------| +| `query` | string | 必需 | 代码或 API 搜索查询 | +| `tokensNum` | number | 5000 | 内容令牌数(1000-50000) | + +### company\_research\_exa + +用于商业情报和新闻的公司研究。 + +``` +company_research_exa(companyName: "Anthropic", numResults: 5) +``` + +**参数:** + +| 参数 | 类型 | 默认值 | 说明 | +|-------|------|---------|-------| +| `companyName` | string | 必需 | 公司名称 | +| `numResults` | number | 5 | 结果数量 | + +### people\_search\_exa + +查找专业资料和个人简介。 + +``` +people_search_exa(query: "AI safety researchers at Anthropic", numResults: 5) +``` + +### crawling\_exa + +从 URL 提取完整页面内容。 + +``` +crawling_exa(url: "https://example.com/article", tokensNum: 5000) +``` + +**参数:** + +| 参数 | 类型 | 默认值 | 说明 | +|-------|------|---------|-------| +| `url` | string | 必需 | 要提取的 URL | +| `tokensNum` | number | 5000 | 内容令牌数 | + +### deep\_researcher\_start / deep\_researcher\_check + +启动一个异步运行的 AI 研究代理。 + +``` +# Start research +deep_researcher_start(query: "comprehensive analysis of AI code editors in 2026") + +# Check status (returns results when complete) +deep_researcher_check(researchId: "") +``` + +## 使用模式 + +### 快速查找 + +``` +web_search_exa(query: "Node.js 22 new features", numResults: 3) +``` + +### 代码研究 + +``` +get_code_context_exa(query: "Rust error handling patterns Result type", tokensNum: 3000) +``` + +### 公司尽职调查 + +``` +company_research_exa(companyName: "Vercel", numResults: 5) +web_search_advanced_exa(query: "Vercel funding valuation 2026", numResults: 3) +``` + +### 技术深度研究 + +``` +# Start async research +deep_researcher_start(query: "WebAssembly component model status and adoption") +# ... do other work ... +deep_researcher_check(researchId: "") +``` + +## 提示 + +* 使用 `web_search_exa` 进行广泛查询,使用 `web_search_advanced_exa` 获取过滤结果 +* 较低的 `tokensNum`(1000-2000)用于聚焦的代码片段,较高的(5000+)用于全面的上下文 +* 结合 `company_research_exa` 和 `web_search_advanced_exa` 进行彻底的公司分析 +* 使用 `crawling_exa` 从搜索结果中的特定 URL 获取完整内容 +* `deep_researcher_start` 最适合受益于 AI 综合的全面主题 + +## 相关技能 + +* `deep-research` — 使用 firecrawl + exa 的完整研究工作流 +* `market-research` — 带有决策框架的业务导向研究 diff --git a/docs/zh-CN/skills/fal-ai-media/SKILL.md b/docs/zh-CN/skills/fal-ai-media/SKILL.md new file mode 100644 index 00000000..1af399e9 --- /dev/null +++ b/docs/zh-CN/skills/fal-ai-media/SKILL.md @@ -0,0 +1,296 @@ +--- +name: fal-ai-media +description: 通过 fal.ai MCP 实现统一的媒体生成——图像、视频和音频。涵盖文本到图像(Nano Banana)、文本/图像到视频(Seedance、Kling、Veo 3)、文本到语音(CSM-1B),以及视频到音频(ThinkSound)。当用户想要使用 AI 生成图像、视频或音频时使用。 +origin: ECC +--- + +# fal.ai 媒体生成 + +通过 MCP 使用 fal.ai 模型生成图像、视频和音频。 + +## 何时激活 + +* 用户希望根据文本提示生成图像 +* 根据文本或图像创建视频 +* 生成语音、音乐或音效 +* 任何媒体生成任务 +* 用户提及“生成图像”、“创建视频”、“文本转语音”、“制作缩略图”或类似表述 + +## MCP 要求 + +必须配置 fal.ai MCP 服务器。添加到 `~/.claude.json`: + +```json +"fal-ai": { + "command": "npx", + "args": ["-y", "fal-ai-mcp-server"], + "env": { "FAL_KEY": "YOUR_FAL_KEY_HERE" } +} +``` + +在 [fal.ai](https://fal.ai) 获取 API 密钥。 + +## MCP 工具 + +fal.ai MCP 提供以下工具: + +* `search` — 通过关键词查找可用模型 +* `find` — 获取模型详情和参数 +* `generate` — 使用参数运行模型 +* `result` — 检查异步生成状态 +* `status` — 检查作业状态 +* `cancel` — 取消正在运行的作业 +* `estimate_cost` — 估算生成成本 +* `models` — 列出热门模型 +* `upload` — 上传文件用作输入 + +*** + +## 图像生成 + +### Nano Banana 2(快速) + +最适合:快速迭代、草稿、文生图、图像编辑。 + +``` +generate( + app_id: "fal-ai/nano-banana-2", + input_data: { + "prompt": "a futuristic cityscape at sunset, cyberpunk style", + "image_size": "landscape_16_9", + "num_images": 1, + "seed": 42 + } +) +``` + +### Nano Banana Pro(高保真) + +最适合:生产级图像、写实感、排版、详细提示。 + +``` +generate( + app_id: "fal-ai/nano-banana-pro", + input_data: { + "prompt": "professional product photo of wireless headphones on marble surface, studio lighting", + "image_size": "square", + "num_images": 1, + "guidance_scale": 7.5 + } +) +``` + +### 常见图像参数 + +| 参数 | 类型 | 选项 | 说明 | +|-------|------|---------|-------| +| `prompt` | 字符串 | 必需 | 描述您想要的内容 | +| `image_size` | 字符串 | `square`、`portrait_4_3`、`landscape_16_9`、`portrait_16_9`、`landscape_4_3` | 宽高比 | +| `num_images` | 数字 | 1-4 | 生成数量 | +| `seed` | 数字 | 任意整数 | 可重现性 | +| `guidance_scale` | 数字 | 1-20 | 遵循提示的紧密程度(值越高越贴近字面) | + +### 图像编辑 + +使用 Nano Banana 2 并输入图像进行修复、扩展或风格迁移: + +``` +# First upload the source image +upload(file_path: "/path/to/image.png") + +# Then generate with image input +generate( + app_id: "fal-ai/nano-banana-2", + input_data: { + "prompt": "same scene but in watercolor style", + "image_url": "", + "image_size": "landscape_16_9" + } +) +``` + +*** + +## 视频生成 + +### Seedance 1.0 Pro(字节跳动) + +最适合:文生视频、图生视频,具有高运动质量。 + +``` +generate( + app_id: "fal-ai/seedance-1-0-pro", + input_data: { + "prompt": "a drone flyover of a mountain lake at golden hour, cinematic", + "duration": "5s", + "aspect_ratio": "16:9", + "seed": 42 + } +) +``` + +### Kling Video v3 Pro + +最适合:文生/图生视频,带原生音频生成。 + +``` +generate( + app_id: "fal-ai/kling-video/v3/pro", + input_data: { + "prompt": "ocean waves crashing on a rocky coast, dramatic clouds", + "duration": "5s", + "aspect_ratio": "16:9" + } +) +``` + +### Veo 3(Google DeepMind) + +最适合:带生成声音的视频,高视觉质量。 + +``` +generate( + app_id: "fal-ai/veo-3", + input_data: { + "prompt": "a bustling Tokyo street market at night, neon signs, crowd noise", + "aspect_ratio": "16:9" + } +) +``` + +### 图生视频 + +从现有图像开始: + +``` +generate( + app_id: "fal-ai/seedance-1-0-pro", + input_data: { + "prompt": "camera slowly zooms out, gentle wind moves the trees", + "image_url": "", + "duration": "5s" + } +) +``` + +### 视频参数 + +| 参数 | 类型 | 选项 | 说明 | +|-------|------|---------|-------| +| `prompt` | 字符串 | 必需 | 描述视频内容 | +| `duration` | 字符串 | `"5s"`、`"10s"` | 视频长度 | +| `aspect_ratio` | 字符串 | `"16:9"`、`"9:16"`、`"1:1"` | 帧比例 | +| `seed` | 数字 | 任意整数 | 可重现性 | +| `image_url` | 字符串 | URL | 用于图生视频的源图像 | + +*** + +## 音频生成 + +### CSM-1B(对话语音) + +文本转语音,具有自然、对话式的音质。 + +``` +generate( + app_id: "fal-ai/csm-1b", + input_data: { + "text": "Hello, welcome to the demo. Let me show you how this works.", + "speaker_id": 0 + } +) +``` + +### ThinkSound(视频转音频) + +根据视频内容生成匹配的音频。 + +``` +generate( + app_id: "fal-ai/thinksound", + input_data: { + "video_url": "", + "prompt": "ambient forest sounds with birds chirping" + } +) +``` + +### ElevenLabs(通过 API,无 MCP) + +如需专业的语音合成,直接使用 ElevenLabs: + +```python +import os +import requests + +resp = requests.post( + "https://api.elevenlabs.io/v1/text-to-speech/", + headers={ + "xi-api-key": os.environ["ELEVENLABS_API_KEY"], + "Content-Type": "application/json" + }, + json={ + "text": "Your text here", + "model_id": "eleven_turbo_v2_5", + "voice_settings": {"stability": 0.5, "similarity_boost": 0.75} + } +) +with open("output.mp3", "wb") as f: + f.write(resp.content) +``` + +### VideoDB 生成式音频 + +如果配置了 VideoDB,使用其生成式音频: + +```python +# Voice generation +audio = coll.generate_voice(text="Your narration here", voice="alloy") + +# Music generation +music = coll.generate_music(prompt="upbeat electronic background music", duration=30) + +# Sound effects +sfx = coll.generate_sound_effect(prompt="thunder crack followed by rain") +``` + +*** + +## 成本估算 + +生成前,检查估算成本: + +``` +estimate_cost( + estimate_type: "unit_price", + endpoints: { + "fal-ai/nano-banana-pro": { + "unit_quantity": 1 + } + } +) +``` + +## 模型发现 + +查找特定任务的模型: + +``` +search(query: "text to video") +find(endpoint_ids: ["fal-ai/seedance-1-0-pro"]) +models() +``` + +## 提示 + +* 在迭代提示时,使用 `seed` 以获得可重现的结果 +* 先用低成本模型(Nano Banana 2)进行提示迭代,然后切换到 Pro 版进行最终生成 +* 对于视频,保持提示描述性但简洁——聚焦于运动和场景 +* 图生视频比纯文生视频能产生更可控的结果 +* 在运行昂贵的视频生成前,检查 `estimate_cost` + +## 相关技能 + +* `videodb` — 视频处理、编辑和流媒体 +* `video-editing` — AI 驱动的视频编辑工作流 +* `content-engine` — 社交媒体平台内容创作 diff --git a/docs/zh-CN/skills/inventory-demand-planning/SKILL.md b/docs/zh-CN/skills/inventory-demand-planning/SKILL.md new file mode 100644 index 00000000..cad9b074 --- /dev/null +++ b/docs/zh-CN/skills/inventory-demand-planning/SKILL.md @@ -0,0 +1,233 @@ +--- +name: inventory-demand-planning +description: 为多地点零售商提供需求预测、安全库存优化、补货规划及促销提升估算的编码化专业知识。基于拥有15年以上管理数百个SKU经验的需求规划师的专业知识。包括预测方法选择、ABC/XYZ分析、季节性过渡管理及供应商谈判框架。适用于预测需求、设定安全库存、规划补货、管理促销或优化库存水平时使用。license: Apache-2.0 +version: 1.0.0 +homepage: https://github.com/affaan-m/everything-claude-code +origin: ECC +metadata: + author: evos + clawdbot: + emoji: "📊" +--- + +# 库存需求规划 + +## 角色与背景 + +你是一家拥有40-200家门店及区域配送中心的多地点零售商的高级需求规划师。你负责管理300-800个活跃SKU,涵盖杂货、日用百货、季节性商品和促销品等多个品类。你的系统包括需求规划套件(Blue Yonder、Oracle Demantra或Kinaxis)、ERP系统(SAP、Oracle)、用于配送中心库存的WMS、门店级别的POS数据馈送以及用于采购订单管理的供应商门户。你处于商品企划(决定销售什么以及定价)、供应链(管理仓库容量和运输)和财务(设定库存投资预算和GMROI目标)之间。你的工作是将商业意图转化为可执行的采购订单,同时最小化缺货和过剩库存。 + +## 使用时机 + +* 为现有或新SKU生成或审查需求预测 +* 基于需求波动性和服务水平目标设定安全库存水平 +* 为季节性转换、促销或新产品上市规划补货 +* 评估预测准确性并调整模型或手动覆盖 +* 在供应商最小起订量约束或前置时间变化的情况下做出采购决策 + +## 工作原理 + +1. 收集需求信号(POS销售、订单、发货)并清理异常值 +2. 基于ABC/XYZ分类和需求模式,为每个SKU选择预测方法 +3. 应用促销提升、蚕食效应抵消和外部因果因素 +4. 使用需求波动性、前置时间波动性和目标满足率计算安全库存 +5. 生成建议采购订单,应用最小起订量/经济订货批量取整,并提交给规划师审查 +6. 监控预测准确性(MAPE、偏差)并在下一个规划周期调整模型 + +## 示例 + +* **季节性促销规划**:商品企划计划对前20名SKU之一进行为期3周的“买一送一”促销。使用历史促销弹性估算促销提升量,计算超前采购数量,与供应商协调提前采购订单和物流容量,并规划促销后的需求低谷。 +* **新SKU上市**:无需求历史可用。使用类比SKU映射(相似品类、价格点、品牌)生成初始预测,设定保守的安全库存(相当于2周的预计销售量),并定义前8周的审查节奏。 +* **前置时间变化下的配送中心补货**:主要供应商因港口拥堵将前置时间从14天延长至21天。重新计算所有受影响SKU的安全库存,识别哪些SKU在新采购订单到达前有缺货风险,并建议过渡订单或替代采购源。 + +## 核心知识 + +### 预测方法及各自适用场景 + +**移动平均(简单、加权、追踪)**:适用于需求稳定、波动性低的商品,近期历史是可靠的预测指标。4周简单移动平均适用于商品化必需品。加权移动平均(近期权重更高)在需求稳定但呈现轻微漂移时效果更好。切勿对季节性商品使用移动平均——它们会滞后于趋势变化半个窗口长度。 + +**指数平滑(单次、双次、三次)**:单次指数平滑(SES,alpha值0.1–0.3)适用于具有噪声的平稳需求。双次指数平滑(霍尔特方法)增加了趋势跟踪——适用于具有持续增长或下降趋势的商品。三次指数平滑(霍尔特-温特斯方法)增加了季节性指数——这是处理具有52周或12个月周期的季节性商品的主力方法。alpha/beta/gamma参数至关重要:高alpha值(>0.3)会追逐波动商品中的噪声;低alpha值(<0.1)对机制变化的响应太慢。在保留数据上优化,切勿在用于拟合的同一数据上进行。 + +**季节性分解(STL、经典分解、X-13ARIMA-SEATS)**:当你需要分别隔离趋势、季节性和残差成分时使用。STL(使用Loess的季节和趋势分解)对异常值具有鲁棒性。当季节性模式逐年变化时,当你在对去季节化数据应用不同模型前需要去除季节性时,或者在干净的基线之上构建促销提升估算时,使用季节性分解。 + +**因果/回归模型**:当外部因素(价格弹性、促销标志、天气、竞争对手行动、本地事件)驱动需求超出商品自身历史时使用。实际挑战在于特征工程:促销标志应编码深度(折扣百分比)、陈列类型、宣传页特性以及跨品类促销存在。在稀疏的促销历史上过拟合是最大的陷阱。积极进行正则化(Lasso/Ridge)并在时间外数据上验证,而非样本外数据。 + +**机器学习(梯度提升、神经网络)**:当你有大量数据(1000+ SKU × 2年以上周度历史)、多个外部回归变量和一个ML工程团队时是合理的。经过适当特征工程的LightGBM/XGBoost在促销品和间歇性需求商品上的表现优于简单方法10-20% WAPE。但它们需要持续监控——零售业的模型漂移是真实存在的,季度性重新训练是最低要求。 + +### 预测准确性指标 + +* **MAPE(平均绝对百分比误差)**:标准指标,但在低销量商品上失效(除以接近零的实际值会产生夸大的百分比)。仅用于平均每周销量50+单位的商品。 +* **加权MAPE(WMAPE)**:绝对误差之和除以实际值之和。防止低销量商品主导该指标。这是财务部门关心的指标,因为它反映了金额。 +* **偏差**:平均符号误差。正偏差 = 预测系统性过高(库存过剩风险)。负偏差 = 系统性过低(缺货风险)。偏差 < ±5% 是健康的。偏差 > 10%(任一方向)意味着模型存在结构性问题,而非噪声。 +* **跟踪信号**:累积误差除以MAD(平均绝对偏差)。当跟踪信号超过±4时,模型已发生漂移,需要干预——要么重新参数化,要么切换方法。 + +### 安全库存计算 + +教科书公式为 `SS = Z × σ_d × √(LT + RP)`,其中 Z 是服务水平 z 分数,σ\_d 是每期需求的标准差,LT 是以周期为单位的前置时间,RP 是以周期为单位的审查周期。在实践中,此公式仅适用于正态分布、平稳的需求。 + +**服务水平目标**:95% 服务水平(Z=1.65)是 A 类商品的标准。99%(Z=2.33)适用于关键/A+ 类商品,其缺货成本远高于持有成本。90%(Z=1.28)对于 C 类商品是可接受的。从 95% 提高到 99% 几乎会使安全库存翻倍——在承诺之前,务必量化增量服务水平的库存投资成本。 + +**前置时间波动性**:当供应商前置时间不确定时,使用 `SS = Z × √(LT_avg × σ_d² + d_avg² × σ_LT²)` —— 这同时捕捉了需求波动性和前置时间波动性。前置时间变异系数(CV)> 0.3 的供应商所需的安全库存调整可能比仅考虑需求的公式建议的高出 40-60%。 + +**间断性/间歇性需求**:正态分布的安全库存计算对于存在许多零需求周期的商品失效。对间歇性需求使用 Croston 方法(分别预测需求间隔和需求规模),并使用自举需求分布而非解析公式计算安全库存。 + +**新产品**:无需求历史意味着没有 σ\_d。使用类比商品分析——找到处于相同生命周期阶段的最相似的 3-5 个商品,并使用它们的需求波动性作为代理。在前 8 周增加 20-30% 的缓冲,然后随着自身历史数据的积累逐渐减少。 + +### 再订货逻辑 + +**库存状况**:`IP = On-Hand + On-Order − Backorders − Committed (allocated to open customer orders)`。切勿仅基于在手库存再订货——当采购订单在途时,你会重复订货。 + +**最小/最大库存**:简单,适用于需求稳定、前置时间一致的商品。最小值 = 前置时间内的平均需求 + 安全库存。最大值 = 最小值 + 经济订货批量。当库存状况降至最小值时,订购至最大值。缺点:除非手动调整,否则无法适应变化的需求模式。 + +**再订货点 / 经济订货批量**:再订货点 = 前置时间内的平均需求 + 安全库存。经济订货批量 = √(2DS/H),其中 D = 年需求,S = 订货成本,H = 每单位每年的持有成本。经济订货批量在理论上对恒定需求是最优的,但在实践中你需要取整到供应商的箱装、层装或托盘层级。一个“完美”的 847 单位经济订货批量毫无意义,如果供应商按 24 件一箱发货的话。 + +**定期审查(R,S)**:每 R 个周期审查一次库存,订购至目标水平 S。当你在固定日期(例如,周二下单周四提货)向供应商合并订单时更好。R 由供应商交货计划设定;S = (R + LT)期间的平均需求 + 该组合期间的安全库存。 + +**基于供应商层级的审查频率**:A 类供应商(按支出排名前10)采用每周审查周期。B 类供应商(接下来的20名)采用双周审查。C 类供应商(其余)采用每月审查。这使审查工作与财务影响保持一致,并允许获得合并折扣。 + +### 促销规划 + +**需求信号扭曲**:促销会制造人为的需求高峰,污染基线预测。在拟合基线模型之前,从历史中剔除促销量。保持一个单独的“促销提升”层,在促销周期间以乘法方式应用于基线之上。 + +**提升估算方法**:(1)同一商品促销期与非促销期的同比比较。(2)使用历史促销深度、陈列类型和媒体支持作为输入的交叉弹性模型。(3)类比商品提升——新商品借用同一品类中先前促销过的类似商品的提升曲线。典型提升幅度:仅临时降价(TPR)为 15-40%,临时降价 + 陈列 + 宣传页特性为 80-200%,限时抢购/亏本引流活动为 300-500%+。 + +**蚕食效应**:当 SKU A 促销时,SKU B(相同品类,相似价格点)会损失销量。对于近似替代品,蚕食效应估算为提升销量的 10-30%。忽略跨品类的蚕食效应,除非促销是改变购物篮构成的引流活动。 + +**超前采购计算**:顾客在深度促销期间囤货,造成促销后低谷。低谷持续时间与产品保质期和促销深度相关。保质期 12 个月的食品储藏室商品打 7 折促销,会造成 2-4 周的低谷,因为家庭消耗囤积的存货。易腐品打 85 折促销几乎不会产生低谷。 + +**促销后低谷**:预计在大型促销后会有 1-3 周低于基线的需求。低谷幅度通常是增量提升的 30-50%,集中在促销后的第一周。未能预测低谷会导致库存过剩和降价。 + +### ABC/XYZ 分类 + +**ABC(价值)**:A = 驱动 80% 收入/利润的前 20% SKU。B = 驱动 15% 的接下来 30%。C = 驱动 5% 的底部 50%。按利润贡献分类,而非收入,以避免过度投资于高收入低利润的商品。 + +**XYZ(可预测性)**:X = 需求变异系数 < 0.5(高度可预测)。Y = 变异系数 0.5–1.0(中等可预测)。Z = 变异系数 > 1.0(不稳定/间断性)。基于去季节化、去促销化的需求计算,以避免惩罚实际上在其模式内可预测的季节性商品。 + +**策略矩阵**:AX 类商品采用自动化补货和严格的安全库存。AZ 类商品每个周期都需要人工审查——它们价值高但不稳定。CX 类商品采用自动化补货和宽松的审查周期。CZ 类商品是考虑下架或转为按订单生产的候选对象。 + +### 季节性转换管理 + +**采购时机**:季节性采购(例如,节日、夏季、返校季)在销售季节前 12-20 周承诺。将预期季节需求的 60-70% 分配到初始采购中,保留 30-40% 用于基于季初销售情况的再订货。这个“待购额度”储备是你对冲预测误差的手段。 + +**降价时机:** 当季中售罄进度低于计划的 60% 时,开始降价。早期浅度降价(20–30% 折扣)比后期深度降价(50–70% 折扣)能挽回更多利润。经验法则:降价启动每延迟一周,剩余库存的利润就会损失 3–5 个百分点。 + +**季末清仓:** 设定一个硬性截止日期(通常在下一季产品到货前 2–3 周)。截止日期后剩余的所有产品将转至奥特莱斯、清仓渠道或捐赠。将季节性产品保留到下一年很少奏效——时尚产品会过时,仓储成本会侵蚀掉任何在下季销售中可能挽回的利润。 + +## 决策框架 + +### 按需求模式选择预测方法 + +| 需求模式 | 主要方法 | 备选方法 | 审查触发条件 | +|---|---|---|---| +| 稳定、高销量、无季节性 | 加权移动平均(4–8 周) | 单指数平滑 | WMAPE > 25% 持续 4 周 | +| 趋势性(增长或下降) | 霍尔特双指数平滑 | 对最近 26 周进行线性回归 | 跟踪信号超过 ±4 | +| 季节性、重复模式 | 霍尔特-温特斯(增长型季节用乘法模型,稳定型用加法模型) | STL 分解 + 残差的 SES | 季节间模式相关性 < 0.7 | +| 间歇性 / 不规则(>30% 零需求期) | 克罗斯顿方法或 SBA | 对需求间隔进行自助法模拟 | 平均需求间隔变化 >30% | +| 促销驱动 | 因果回归(基线 + 促销提升层) | 类比商品提升 + 基线 | 促销后实际值与预测值偏差 >40% | +| 新产品(0–12 周历史) | 类比商品轮廓结合生命周期曲线 | 品类平均值并向实际值衰减 | 自有数据 WMAPE 稳定低于基于类比商品的 WMAPE | +| 事件驱动(天气、本地活动) | 带外部回归因子的回归 | 有理由说明的手动覆盖 | 当回归因子与需求相关性低于 0.6 或两个可比事件期间预测误差上升 >30% 时重新评估 | + +### 安全库存服务水平选择 + +| 细分 | 目标服务水平 | Z-分数 | 依据 | +|---|---|---|---| +| AX(高价值、可预测) | 97.5% | 1.96 | 高价值证明投资合理;低变异性使 SS 保持适中 | +| AY(高价值、中等变异性) | 95% | 1.65 | 标准目标;变异性使得更高的 SL 成本过高 | +| AZ(高价值、不稳定) | 92–95% | 1.41–1.65 | 不稳定的需求使得高 SL 成本极高;需补充应急供货能力 | +| BX/BY | 95% | 1.65 | 标准目标 | +| BZ | 90% | 1.28 | 接受中端不稳定商品的一定缺货风险 | +| CX/CY | 90–92% | 1.28–1.41 | 低价值不足以证明高 SS 投资合理 | +| CZ | 85% | 1.04 | 考虑淘汰;最小化投资 | + +### 促销提升决策框架 + +1. **此 SKU-促销类型组合是否有历史提升数据?** → 使用自有商品提升数据,并加权近期性(最近 3 次促销按 50/30/20 加权)。 +2. **无自有商品数据,但同品类有促销历史?** → 使用类比商品提升数据,并根据价格点和品牌层级进行调整。 +3. **全新品类或促销类型?** → 使用保守的品类平均提升值并打 8 折。为促销期建立更宽的安全库存缓冲。 +4. **与其他品类交叉促销?** → 分别模拟流量驱动商品和交叉促销受益商品。如果可用,应用交叉弹性系数;否则,默认跨品类光环提升为 0.15。 +5. **始终模拟促销后回落。** 默认值为增量提升的 40%,并按 60/30/10 的比例分布在促销后三周。 + +### 降价时机决策 + +| 季中售罄进度 | 行动 | 预期利润挽回率 | +|---|---|---| +| ≥ 80% 计划 | 保持价格。若周供应量 < 3,谨慎补货。 | 全额利润 | +| 60–79% 计划 | 降价 20–25%。不补货。 | 原始利润的 70–80% | +| 40–59% 计划 | 立即降价 30–40%。取消任何未结采购订单。 | 原始利润的 50–65% | +| < 40% 计划 | 降价 50% 以上。探索清仓渠道。标记采购错误以供事后分析。 | 原始利润的 30–45% | + +### 滞销品淘汰决策 + +每季度评估。当**所有**以下条件均满足时,标记为淘汰: + +* 按当前售罄速度,周供应量 > 26 +* 过去 13 周销售速度 < 该商品前 13 周速度的 50%(生命周期下降) +* 未来 8 周内无计划促销活动 +* 商品无合同义务(货架陈列承诺、供应商协议) +* 存在替代或替换 SKU,或品类可吸收缺口 + +若标记,启动降价 30% 持续 4 周。若仍未动销,升级至 50% 折扣或清仓。从首次降价起设定 8 周的硬性退出日期。不要让滞销品在品类中无限期滞留——它们消耗货架空间、仓库位置和营运资金。 + +## 关键边缘情况 + +此处包含简要总结,以便您可以根据项目需要将其扩展为具体的应对手册。 + +1. **无历史的新产品上市:** 类比商品轮廓分析是您唯一的工具。谨慎选择类比商品——匹配价格点、品类、品牌层级和目标客群,而不仅仅是产品类型。进行保守的初始采购(类比商品预测的 60%),并建立每周自动补货触发机制。 +2. **社交媒体病毒式传播激增:** 需求在无预警情况下激增 500–2000%。不要追逐——当您的供应链做出反应时(4–8 周前置期),激增已结束。从现有库存中尽力满足,制定分配规则防止单一地点囤积,并让浪潮过去。只有当激增后 4 周以上需求持续存在时,才修正基线。 +3. **供应商前置期一夜之间翻倍:** 立即使用新的前置期重新计算安全库存。如果 SS 翻倍,您很可能无法用现有库存填补缺口。为差额下达紧急订单,协商分批发货,并寻找二级供应商。告知商品部门服务水平将暂时下降。 +4. **计划外促销的蚕食效应:** 竞争对手或其他部门进行计划外促销,抢占了您品类的销量。您的预测将过高。通过监控每日 POS 数据以发现模式中断来及早发现,然后手动下调预测。如果可能,推迟到货订单。 +5. **需求模式体制变化:** 原本稳定-季节性的商品突然转变为趋势性或不稳定。常见于产品配方变更、包装更换或竞争对手进入/退出之后。旧模型会无声地失效。每周监控跟踪信号——当连续两个周期超过 ±4 时,触发模型重选。 +6. **虚增库存:** WMS 显示有 200 件;实际盘点显示 40 件。基于该虚增库存的每个预测和补货决策都是错误的。当服务水平下降但系统显示库存“充足”时,怀疑虚增库存。对任何系统显示不应缺货但实际缺货的商品进行循环盘点。 +7. **供应商 MOQ 冲突:** 您的 EOQ 建议订购 150 件;供应商的最小订单量是 500 件。您要么超订(接受数周的过量库存),要么协商。选项:与同一供应商的其他商品合并以满足金额最低要求,为此 SKU 协商更低的 MOQ,或者如果持有成本低于从替代供应商处采购的成本,则接受过量。 +8. **节假日日历偏移效应:** 当关键销售节假日(例如复活节在三月和四月之间移动)在日历上的位置发生变化时,周同比比较会失效。将预测对齐到“相对于节假日的周数”而非日历周数。若未能考虑复活节从第 13 周移至第 16 周,将导致两年都出现显著的预测误差。 + +## 沟通模式 + +### 语气校准 + +* **供应商常规补货:** 事务性、简洁、以采购订单号为准。“根据约定日程,PO #XXXX 交付周为 MM/DD。” +* **供应商前置期升级:** 坚定、基于事实、量化业务影响。“我们的分析显示,过去 8 周您的前置期已从 14 天增加到 22 天。这导致了 X 次缺货事件。我们需要在 \[日期] 前制定纠正计划。” +* **内部缺货警报:** 紧急、可操作、包含预估风险收入。以客户影响为首,而非库存指标。“SKU X 将在周四前在 12 个地点缺货。预估销售损失:$XX,000。建议行动:\[加急/调拨/替代]。” +* **向商品部门提出降价建议:** 数据驱动,包含利润影响分析。切勿表述为“我们买多了”——应表述为“为达到利润目标,售罄速度要求采取价格行动。” +* **提交促销预测:** 结构化,分别说明基线、提升和促销后回落。包含假设和置信区间。“基线:500 件/周。促销提升预估:180%(增量 900 件)。促销后回落:−35% 持续 2 周。置信度:±25%。” +* **新产品预测假设:** 明确记录每个假设,以便在事后分析时审计。“基于类比商品 \[列表],我们预测第 1–4 周为 200 件/周,到第 8 周降至 120 件/周。假设:价格点 $X,分销至 80 个门店,窗口期内无竞争产品上市。” + +以上为简要模板。在用于生产环境前,请根据您的供应商、销售和运营规划工作流程进行调整。 + +## 升级协议 + +### 自动升级触发条件 + +| 触发条件 | 行动 | 时间线 | +|---|---|---| +| A 类商品预计 7 天内缺货 | 通知需求规划经理 + 品类商品经理 | 4 小时内 | +| 供应商确认前置期增加 > 25% | 通知供应链总监;重新计算所有未结采购订单 | 1 个工作日内 | +| 促销预测偏差 > 40%(过高或过低) | 与商品部门和供应商进行促销后复盘 | 促销结束后 1 周内 | +| 任何 A/B 类商品过量库存 > 26 周供应量 | 向商品副总裁提出降价建议 | 发现后 1 周内 | +| 预测偏差连续 4 周超过 ±10% | 模型审查和参数重设 | 2 周内 | +| 新产品上市 4 周后售罄进度 < 计划的 40% | 与商品部门进行品类审查 | 1 周内 | +| 任何品类服务水平降至 90% 以下 | 根本原因分析和纠正计划 | 48 小时内 | + +### 升级链 + +级别 1(需求规划师) → 级别 2(规划经理,24 小时) → 级别 3(供应链规划总监,48 小时) → 级别 4(供应链副总裁,72+ 小时或任何 A 类商品对重要客户缺货) + +## 绩效指标 + +每周跟踪,每月分析趋势: + +| 指标 | 目标 | 危险信号 | +|---|---|---| +| WMAPE(加权平均绝对百分比误差) | < 25% | > 35% | +| 预测偏差 | ±5% | > ±10% 持续 4+ 周 | +| 现货率(A 类商品) | > 97% | < 94% | +| 现货率(所有商品) | > 95% | < 92% | +| 周供应量(总计) | 4–8 周 | > 12 或 < 3 | +| 过量库存(>26 周供应量) | < 5% 的 SKU | > 10% 的 SKU | +| 呆滞库存(零销售,13+ 周) | < 2% 的 SKU | > 5% 的 SKU | +| 供应商采购订单履行率 | > 95% | < 90% | +| 促销预测准确度(WMAPE) | < 35% | > 50% | + +## 附加资源 + +* 将此技能与您的 SKU 细分模型、服务水平政策和规划师覆盖审计日志结合使用。 +* 将促销失误、供应商延迟和预测覆盖的事后分析存储在规划工作流旁边,以便边缘情况保持可操作性。 diff --git a/docs/zh-CN/skills/iterative-retrieval/SKILL.md b/docs/zh-CN/skills/iterative-retrieval/SKILL.md index 3bf17cf9..4fafd530 100644 --- a/docs/zh-CN/skills/iterative-retrieval/SKILL.md +++ b/docs/zh-CN/skills/iterative-retrieval/SKILL.md @@ -210,6 +210,6 @@ Result: throttle.ts, middleware/index.ts, router-setup.ts ## 相关 -* [长篇指南](https://x.com/affaanmustafa/status/2014040193557471352) - 子智能体编排部分 -* `continuous-learning` 技能 - 用于随时间改进的模式 -* 在 `~/.claude/agents/` 中的智能体定义 +* [长篇指南](https://x.com/affaanmustafa/status/2014040193557471352) - 子代理编排章节 +* `continuous-learning` 技能 - 适用于随时间改进的模式 +* 与 ECC 捆绑的代理定义(手动安装路径:`agents/`) diff --git a/docs/zh-CN/skills/kotlin-coroutines-flows/SKILL.md b/docs/zh-CN/skills/kotlin-coroutines-flows/SKILL.md new file mode 100644 index 00000000..b883ca71 --- /dev/null +++ b/docs/zh-CN/skills/kotlin-coroutines-flows/SKILL.md @@ -0,0 +1,284 @@ +--- +name: kotlin-coroutines-flows +description: Kotlin协程与Flow在Android和KMP中的模式——结构化并发、Flow操作符、StateFlow、错误处理和测试。 +origin: ECC +--- + +# Kotlin 协程与 Flow + +适用于 Android 和 Kotlin 多平台项目的结构化并发模式、基于 Flow 的响应式流以及协程测试。 + +## 何时启用 + +* 使用 Kotlin 协程编写异步代码 +* 使用 Flow、StateFlow 或 SharedFlow 实现响应式数据 +* 处理并发操作(并行加载、防抖、重试) +* 测试协程和 Flow +* 管理协程作用域与取消 + +## 结构化并发 + +### 作用域层级 + +``` +Application + └── viewModelScope (ViewModel) + └── coroutineScope { } (structured child) + ├── async { } (concurrent task) + └── async { } (concurrent task) +``` + +始终使用结构化并发——绝不使用 `GlobalScope`: + +```kotlin +// BAD +GlobalScope.launch { fetchData() } + +// GOOD — scoped to ViewModel lifecycle +viewModelScope.launch { fetchData() } + +// GOOD — scoped to composable lifecycle +LaunchedEffect(key) { fetchData() } +``` + +### 并行分解 + +使用 `coroutineScope` + `async` 处理并行工作: + +```kotlin +suspend fun loadDashboard(): Dashboard = coroutineScope { + val items = async { itemRepository.getRecent() } + val stats = async { statsRepository.getToday() } + val profile = async { userRepository.getCurrent() } + Dashboard( + items = items.await(), + stats = stats.await(), + profile = profile.await() + ) +} +``` + +### SupervisorScope + +当子协程失败不应取消同级协程时,使用 `supervisorScope`: + +```kotlin +suspend fun syncAll() = supervisorScope { + launch { syncItems() } // failure here won't cancel syncStats + launch { syncStats() } + launch { syncSettings() } +} +``` + +## Flow 模式 + +### Cold Flow —— 一次性操作到流的转换 + +```kotlin +fun observeItems(): Flow> = flow { + // Re-emits whenever the database changes + itemDao.observeAll() + .map { entities -> entities.map { it.toDomain() } } + .collect { emit(it) } +} +``` + +### 用于 UI 状态的 StateFlow + +```kotlin +class DashboardViewModel( + observeProgress: ObserveUserProgressUseCase +) : ViewModel() { + val progress: StateFlow = observeProgress() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = UserProgress.EMPTY + ) +} +``` + +`WhileSubscribed(5_000)` 会在最后一个订阅者离开后,保持上游活动 5 秒——可在配置更改时存活而无需重启。 + +### 组合多个 Flow + +```kotlin +val uiState: StateFlow = combine( + itemRepository.observeItems(), + settingsRepository.observeTheme(), + userRepository.observeProfile() +) { items, theme, profile -> + HomeState(items = items, theme = theme, profile = profile) +}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), HomeState()) +``` + +### Flow 操作符 + +```kotlin +// Debounce search input +searchQuery + .debounce(300) + .distinctUntilChanged() + .flatMapLatest { query -> repository.search(query) } + .catch { emit(emptyList()) } + .collect { results -> _state.update { it.copy(results = results) } } + +// Retry with exponential backoff +fun fetchWithRetry(): Flow = flow { emit(api.fetch()) } + .retryWhen { cause, attempt -> + if (cause is IOException && attempt < 3) { + delay(1000L * (1 shl attempt.toInt())) + true + } else { + false + } + } +``` + +### 用于一次性事件的 SharedFlow + +```kotlin +class ItemListViewModel : ViewModel() { + private val _effects = MutableSharedFlow() + val effects: SharedFlow = _effects.asSharedFlow() + + sealed interface Effect { + data class ShowSnackbar(val message: String) : Effect + data class NavigateTo(val route: String) : Effect + } + + private fun deleteItem(id: String) { + viewModelScope.launch { + repository.delete(id) + _effects.emit(Effect.ShowSnackbar("Item deleted")) + } + } +} + +// Collect in Composable +LaunchedEffect(Unit) { + viewModel.effects.collect { effect -> + when (effect) { + is Effect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message) + is Effect.NavigateTo -> navController.navigate(effect.route) + } + } +} +``` + +## 调度器 + +```kotlin +// CPU-intensive work +withContext(Dispatchers.Default) { parseJson(largePayload) } + +// IO-bound work +withContext(Dispatchers.IO) { database.query() } + +// Main thread (UI) — default in viewModelScope +withContext(Dispatchers.Main) { updateUi() } +``` + +在 KMP 中,使用 `Dispatchers.Default` 和 `Dispatchers.Main`(在所有平台上可用)。`Dispatchers.IO` 仅适用于 JVM/Android——在其他平台上使用 `Dispatchers.Default` 或通过依赖注入提供。 + +## 取消 + +### 协作式取消 + +长时间运行的循环必须检查取消状态: + +```kotlin +suspend fun processItems(items: List) = coroutineScope { + for (item in items) { + ensureActive() // throws CancellationException if cancelled + process(item) + } +} +``` + +### 使用 try/finally 进行清理 + +```kotlin +viewModelScope.launch { + try { + _state.update { it.copy(isLoading = true) } + val data = repository.fetch() + _state.update { it.copy(data = data) } + } finally { + _state.update { it.copy(isLoading = false) } // always runs, even on cancellation + } +} +``` + +## 测试 + +### 使用 Turbine 测试 StateFlow + +```kotlin +@Test +fun `search updates item list`() = runTest { + val fakeRepository = FakeItemRepository().apply { emit(testItems) } + val viewModel = ItemListViewModel(GetItemsUseCase(fakeRepository)) + + viewModel.state.test { + assertEquals(ItemListState(), awaitItem()) // initial + + viewModel.onSearch("query") + val loading = awaitItem() + assertTrue(loading.isLoading) + + val loaded = awaitItem() + assertFalse(loaded.isLoading) + assertEquals(1, loaded.items.size) + } +} +``` + +### 使用 TestDispatcher 测试 + +```kotlin +@Test +fun `parallel load completes correctly`() = runTest { + val viewModel = DashboardViewModel( + itemRepo = FakeItemRepo(), + statsRepo = FakeStatsRepo() + ) + + viewModel.load() + advanceUntilIdle() + + val state = viewModel.state.value + assertNotNull(state.items) + assertNotNull(state.stats) +} +``` + +### 模拟 Flow + +```kotlin +class FakeItemRepository : ItemRepository { + private val _items = MutableStateFlow>(emptyList()) + + override fun observeItems(): Flow> = _items + + fun emit(items: List) { _items.value = items } + + override suspend fun getItemsByCategory(category: String): Result> { + return Result.success(_items.value.filter { it.category == category }) + } +} +``` + +## 应避免的反模式 + +* 使用 `GlobalScope`——会导致协程泄漏,且无法结构化取消 +* 在没有作用域的情况下于 `init {}` 中收集 Flow——应使用 `viewModelScope.launch` +* 将 `MutableStateFlow` 与可变集合一起使用——始终使用不可变副本:`_state.update { it.copy(list = it.list + newItem) }` +* 捕获 `CancellationException`——应让其传播以实现正确的取消 +* 使用 `flowOn(Dispatchers.Main)` 进行收集——收集调度器是调用方的调度器 +* 在 `@Composable` 中创建 `Flow` 而不使用 `remember`——每次重组都会重新创建 Flow + +## 参考 + +关于 Flow 在 UI 层的消费,请参阅技能:`compose-multiplatform-patterns`。 +关于协程在各层中的适用位置,请参阅技能:`android-clean-architecture`。 diff --git a/docs/zh-CN/skills/kotlin-exposed-patterns/SKILL.md b/docs/zh-CN/skills/kotlin-exposed-patterns/SKILL.md new file mode 100644 index 00000000..7600bc58 --- /dev/null +++ b/docs/zh-CN/skills/kotlin-exposed-patterns/SKILL.md @@ -0,0 +1,719 @@ +--- +name: kotlin-exposed-patterns +description: JetBrains Exposed ORM 模式,包括 DSL 查询、DAO 模式、事务、HikariCP 连接池、Flyway 迁移和仓库模式。 +origin: ECC +--- + +# Kotlin Exposed 模式 + +使用 JetBrains Exposed ORM 进行数据库访问的全面模式,包括 DSL 查询、DAO、事务以及生产就绪的配置。 + +## 何时使用 + +* 使用 Exposed 设置数据库访问 +* 使用 Exposed DSL 或 DAO 编写 SQL 查询 +* 使用 HikariCP 配置连接池 +* 使用 Flyway 创建数据库迁移 +* 使用 Exposed 实现仓储模式 +* 处理 JSON 列和复杂查询 + +## 工作原理 + +Exposed 提供两种查询风格:用于直接类似 SQL 表达式的 DSL 和用于实体生命周期管理的 DAO。HikariCP 通过 `HikariConfig` 配置来管理可重用的数据库连接池。Flyway 在启动时运行版本化的 SQL 迁移脚本以保持模式同步。所有数据库操作都在 `newSuspendedTransaction` 块内运行,以确保协程安全和原子性。仓储模式将 Exposed 查询包装在接口之后,使业务逻辑与数据层解耦,并且测试可以使用内存中的 H2 数据库。 + +## 示例 + +### DSL 查询 + +```kotlin +suspend fun findUserById(id: UUID): UserRow? = + newSuspendedTransaction { + UsersTable.selectAll() + .where { UsersTable.id eq id } + .map { it.toUser() } + .singleOrNull() + } +``` + +### DAO 实体用法 + +```kotlin +suspend fun createUser(request: CreateUserRequest): User = + newSuspendedTransaction { + UserEntity.new { + name = request.name + email = request.email + role = request.role + }.toModel() + } +``` + +### HikariCP 配置 + +```kotlin +val hikariConfig = HikariConfig().apply { + driverClassName = config.driver + jdbcUrl = config.url + username = config.username + password = config.password + maximumPoolSize = config.maxPoolSize + isAutoCommit = false + transactionIsolation = "TRANSACTION_READ_COMMITTED" + validate() +} +``` + +## 数据库设置 + +### HikariCP 连接池 + +```kotlin +// DatabaseFactory.kt +object DatabaseFactory { + fun create(config: DatabaseConfig): Database { + val hikariConfig = HikariConfig().apply { + driverClassName = config.driver + jdbcUrl = config.url + username = config.username + password = config.password + maximumPoolSize = config.maxPoolSize + isAutoCommit = false + transactionIsolation = "TRANSACTION_READ_COMMITTED" + validate() + } + + return Database.connect(HikariDataSource(hikariConfig)) + } +} + +data class DatabaseConfig( + val url: String, + val driver: String = "org.postgresql.Driver", + val username: String = "", + val password: String = "", + val maxPoolSize: Int = 10, +) +``` + +### Flyway 迁移 + +```kotlin +// FlywayMigration.kt +fun runMigrations(config: DatabaseConfig) { + Flyway.configure() + .dataSource(config.url, config.username, config.password) + .locations("classpath:db/migration") + .baselineOnMigrate(true) + .load() + .migrate() +} + +// Application startup +fun Application.module() { + val config = DatabaseConfig( + url = environment.config.property("database.url").getString(), + username = environment.config.property("database.username").getString(), + password = environment.config.property("database.password").getString(), + ) + runMigrations(config) + val database = DatabaseFactory.create(config) + // ... +} +``` + +### 迁移文件 + +```sql +-- src/main/resources/db/migration/V1__create_users.sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + role VARCHAR(20) NOT NULL DEFAULT 'USER', + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_role ON users(role); +``` + +## 表定义 + +### DSL 风格表 + +```kotlin +// tables/UsersTable.kt +object UsersTable : UUIDTable("users") { + val name = varchar("name", 100) + val email = varchar("email", 255).uniqueIndex() + val role = enumerationByName("role", 20) + val metadata = jsonb("metadata", Json.Default).nullable() + val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone) + val updatedAt = timestampWithTimeZone("updated_at").defaultExpression(CurrentTimestampWithTimeZone) +} + +object OrdersTable : UUIDTable("orders") { + val userId = uuid("user_id").references(UsersTable.id) + val status = enumerationByName("status", 20) + val totalAmount = long("total_amount") + val currency = varchar("currency", 3) + val createdAt = timestampWithTimeZone("created_at").defaultExpression(CurrentTimestampWithTimeZone) +} + +object OrderItemsTable : UUIDTable("order_items") { + val orderId = uuid("order_id").references(OrdersTable.id, onDelete = ReferenceOption.CASCADE) + val productId = uuid("product_id") + val quantity = integer("quantity") + val unitPrice = long("unit_price") +} +``` + +### 复合表 + +```kotlin +object UserRolesTable : Table("user_roles") { + val userId = uuid("user_id").references(UsersTable.id, onDelete = ReferenceOption.CASCADE) + val roleId = uuid("role_id").references(RolesTable.id, onDelete = ReferenceOption.CASCADE) + override val primaryKey = PrimaryKey(userId, roleId) +} +``` + +## DSL 查询 + +### 基本 CRUD + +```kotlin +// Insert +suspend fun insertUser(name: String, email: String, role: Role): UUID = + newSuspendedTransaction { + UsersTable.insertAndGetId { + it[UsersTable.name] = name + it[UsersTable.email] = email + it[UsersTable.role] = role + }.value + } + +// Select by ID +suspend fun findUserById(id: UUID): UserRow? = + newSuspendedTransaction { + UsersTable.selectAll() + .where { UsersTable.id eq id } + .map { it.toUser() } + .singleOrNull() + } + +// Select with conditions +suspend fun findActiveAdmins(): List = + newSuspendedTransaction { + UsersTable.selectAll() + .where { (UsersTable.role eq Role.ADMIN) } + .orderBy(UsersTable.name) + .map { it.toUser() } + } + +// Update +suspend fun updateUserEmail(id: UUID, newEmail: String): Boolean = + newSuspendedTransaction { + UsersTable.update({ UsersTable.id eq id }) { + it[email] = newEmail + it[updatedAt] = CurrentTimestampWithTimeZone + } > 0 + } + +// Delete +suspend fun deleteUser(id: UUID): Boolean = + newSuspendedTransaction { + UsersTable.deleteWhere { UsersTable.id eq id } > 0 + } + +// Row mapping +private fun ResultRow.toUser() = UserRow( + id = this[UsersTable.id].value, + name = this[UsersTable.name], + email = this[UsersTable.email], + role = this[UsersTable.role], + metadata = this[UsersTable.metadata], + createdAt = this[UsersTable.createdAt], + updatedAt = this[UsersTable.updatedAt], +) +``` + +### 高级查询 + +```kotlin +// Join queries +suspend fun findOrdersWithUser(userId: UUID): List = + newSuspendedTransaction { + (OrdersTable innerJoin UsersTable) + .selectAll() + .where { OrdersTable.userId eq userId } + .orderBy(OrdersTable.createdAt, SortOrder.DESC) + .map { row -> + OrderWithUser( + orderId = row[OrdersTable.id].value, + status = row[OrdersTable.status], + totalAmount = row[OrdersTable.totalAmount], + userName = row[UsersTable.name], + ) + } + } + +// Aggregation +suspend fun countUsersByRole(): Map = + newSuspendedTransaction { + UsersTable + .select(UsersTable.role, UsersTable.id.count()) + .groupBy(UsersTable.role) + .associate { row -> + row[UsersTable.role] to row[UsersTable.id.count()] + } + } + +// Subqueries +suspend fun findUsersWithOrders(): List = + newSuspendedTransaction { + UsersTable.selectAll() + .where { + UsersTable.id inSubQuery + OrdersTable.select(OrdersTable.userId).withDistinct() + } + .map { it.toUser() } + } + +// LIKE and pattern matching — always escape user input to prevent wildcard injection +private fun escapeLikePattern(input: String): String = + input.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + +suspend fun searchUsers(query: String): List = + newSuspendedTransaction { + val sanitized = escapeLikePattern(query.lowercase()) + UsersTable.selectAll() + .where { + (UsersTable.name.lowerCase() like "%${sanitized}%") or + (UsersTable.email.lowerCase() like "%${sanitized}%") + } + .map { it.toUser() } + } +``` + +### 分页 + +```kotlin +data class Page( + val data: List, + val total: Long, + val page: Int, + val limit: Int, +) { + val totalPages: Int get() = ((total + limit - 1) / limit).toInt() + val hasNext: Boolean get() = page < totalPages + val hasPrevious: Boolean get() = page > 1 +} + +suspend fun findUsersPaginated(page: Int, limit: Int): Page = + newSuspendedTransaction { + val total = UsersTable.selectAll().count() + val data = UsersTable.selectAll() + .orderBy(UsersTable.createdAt, SortOrder.DESC) + .limit(limit) + .offset(((page - 1) * limit).toLong()) + .map { it.toUser() } + + Page(data = data, total = total, page = page, limit = limit) + } +``` + +### 批量操作 + +```kotlin +// Batch insert +suspend fun insertUsers(users: List): List = + newSuspendedTransaction { + UsersTable.batchInsert(users) { user -> + this[UsersTable.name] = user.name + this[UsersTable.email] = user.email + this[UsersTable.role] = user.role + }.map { it[UsersTable.id].value } + } + +// Upsert (insert or update on conflict) +suspend fun upsertUser(id: UUID, name: String, email: String) { + newSuspendedTransaction { + UsersTable.upsert(UsersTable.email) { + it[UsersTable.id] = EntityID(id, UsersTable) + it[UsersTable.name] = name + it[UsersTable.email] = email + it[updatedAt] = CurrentTimestampWithTimeZone + } + } +} +``` + +## DAO 模式 + +### 实体定义 + +```kotlin +// entities/UserEntity.kt +class UserEntity(id: EntityID) : UUIDEntity(id) { + companion object : UUIDEntityClass(UsersTable) + + var name by UsersTable.name + var email by UsersTable.email + var role by UsersTable.role + var metadata by UsersTable.metadata + var createdAt by UsersTable.createdAt + var updatedAt by UsersTable.updatedAt + + val orders by OrderEntity referrersOn OrdersTable.userId + + fun toModel(): User = User( + id = id.value, + name = name, + email = email, + role = role, + metadata = metadata, + createdAt = createdAt, + updatedAt = updatedAt, + ) +} + +class OrderEntity(id: EntityID) : UUIDEntity(id) { + companion object : UUIDEntityClass(OrdersTable) + + var user by UserEntity referencedOn OrdersTable.userId + var status by OrdersTable.status + var totalAmount by OrdersTable.totalAmount + var currency by OrdersTable.currency + var createdAt by OrdersTable.createdAt + + val items by OrderItemEntity referrersOn OrderItemsTable.orderId +} +``` + +### DAO 操作 + +```kotlin +suspend fun findUserByEmail(email: String): User? = + newSuspendedTransaction { + UserEntity.find { UsersTable.email eq email } + .firstOrNull() + ?.toModel() + } + +suspend fun createUser(request: CreateUserRequest): User = + newSuspendedTransaction { + UserEntity.new { + name = request.name + email = request.email + role = request.role + }.toModel() + } + +suspend fun updateUser(id: UUID, request: UpdateUserRequest): User? = + newSuspendedTransaction { + UserEntity.findById(id)?.apply { + request.name?.let { name = it } + request.email?.let { email = it } + updatedAt = OffsetDateTime.now(ZoneOffset.UTC) + }?.toModel() + } +``` + +## 事务 + +### 挂起事务支持 + +```kotlin +// Good: Use newSuspendedTransaction for coroutine support +suspend fun performDatabaseOperation(): Result = + runCatching { + newSuspendedTransaction { + val user = UserEntity.new { + name = "Alice" + email = "alice@example.com" + } + // All operations in this block are atomic + user.toModel() + } + } + +// Good: Nested transactions with savepoints +suspend fun transferFunds(fromId: UUID, toId: UUID, amount: Long) { + newSuspendedTransaction { + val from = UserEntity.findById(fromId) ?: throw NotFoundException("User $fromId not found") + val to = UserEntity.findById(toId) ?: throw NotFoundException("User $toId not found") + + // Debit + from.balance -= amount + // Credit + to.balance += amount + + // Both succeed or both fail + } +} +``` + +### 事务隔离级别 + +```kotlin +suspend fun readCommittedQuery(): List = + newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_READ_COMMITTED) { + UserEntity.all().map { it.toModel() } + } + +suspend fun serializableOperation() { + newSuspendedTransaction(transactionIsolation = Connection.TRANSACTION_SERIALIZABLE) { + // Strictest isolation level for critical operations + } +} +``` + +## 仓储模式 + +### 接口定义 + +```kotlin +interface UserRepository { + suspend fun findById(id: UUID): User? + suspend fun findByEmail(email: String): User? + suspend fun findAll(page: Int, limit: Int): Page + suspend fun search(query: String): List + suspend fun create(request: CreateUserRequest): User + suspend fun update(id: UUID, request: UpdateUserRequest): User? + suspend fun delete(id: UUID): Boolean + suspend fun count(): Long +} +``` + +### Exposed 实现 + +```kotlin +class ExposedUserRepository( + private val database: Database, +) : UserRepository { + + override suspend fun findById(id: UUID): User? = + newSuspendedTransaction(db = database) { + UsersTable.selectAll() + .where { UsersTable.id eq id } + .map { it.toUser() } + .singleOrNull() + } + + override suspend fun findByEmail(email: String): User? = + newSuspendedTransaction(db = database) { + UsersTable.selectAll() + .where { UsersTable.email eq email } + .map { it.toUser() } + .singleOrNull() + } + + override suspend fun findAll(page: Int, limit: Int): Page = + newSuspendedTransaction(db = database) { + val total = UsersTable.selectAll().count() + val data = UsersTable.selectAll() + .orderBy(UsersTable.createdAt, SortOrder.DESC) + .limit(limit) + .offset(((page - 1) * limit).toLong()) + .map { it.toUser() } + Page(data = data, total = total, page = page, limit = limit) + } + + override suspend fun search(query: String): List = + newSuspendedTransaction(db = database) { + val sanitized = escapeLikePattern(query.lowercase()) + UsersTable.selectAll() + .where { + (UsersTable.name.lowerCase() like "%${sanitized}%") or + (UsersTable.email.lowerCase() like "%${sanitized}%") + } + .orderBy(UsersTable.name) + .map { it.toUser() } + } + + override suspend fun create(request: CreateUserRequest): User = + newSuspendedTransaction(db = database) { + UsersTable.insert { + it[name] = request.name + it[email] = request.email + it[role] = request.role + }.resultedValues!!.first().toUser() + } + + override suspend fun update(id: UUID, request: UpdateUserRequest): User? = + newSuspendedTransaction(db = database) { + val updated = UsersTable.update({ UsersTable.id eq id }) { + request.name?.let { name -> it[UsersTable.name] = name } + request.email?.let { email -> it[UsersTable.email] = email } + it[updatedAt] = CurrentTimestampWithTimeZone + } + if (updated > 0) findById(id) else null + } + + override suspend fun delete(id: UUID): Boolean = + newSuspendedTransaction(db = database) { + UsersTable.deleteWhere { UsersTable.id eq id } > 0 + } + + override suspend fun count(): Long = + newSuspendedTransaction(db = database) { + UsersTable.selectAll().count() + } + + private fun ResultRow.toUser() = User( + id = this[UsersTable.id].value, + name = this[UsersTable.name], + email = this[UsersTable.email], + role = this[UsersTable.role], + metadata = this[UsersTable.metadata], + createdAt = this[UsersTable.createdAt], + updatedAt = this[UsersTable.updatedAt], + ) +} +``` + +## JSON 列 + +### 使用 kotlinx.serialization 的 JSONB + +```kotlin +// Custom column type for JSONB +inline fun Table.jsonb( + name: String, + json: Json, +): Column = registerColumn(name, object : ColumnType() { + override fun sqlType() = "JSONB" + + override fun valueFromDB(value: Any): T = when (value) { + is String -> json.decodeFromString(value) + is PGobject -> { + val jsonString = value.value + ?: throw IllegalArgumentException("PGobject value is null for column '$name'") + json.decodeFromString(jsonString) + } + else -> throw IllegalArgumentException("Unexpected value: $value") + } + + override fun notNullValueToDB(value: T): Any = + PGobject().apply { + type = "jsonb" + this.value = json.encodeToString(value) + } +}) + +// Usage in table +@Serializable +data class UserMetadata( + val preferences: Map = emptyMap(), + val tags: List = emptyList(), +) + +object UsersTable : UUIDTable("users") { + val metadata = jsonb("metadata", Json.Default).nullable() +} +``` + +## 使用 Exposed 进行测试 + +### 用于测试的内存数据库 + +```kotlin +class UserRepositoryTest : FunSpec({ + lateinit var database: Database + lateinit var repository: UserRepository + + beforeSpec { + database = Database.connect( + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL", + driver = "org.h2.Driver", + ) + transaction(database) { + SchemaUtils.create(UsersTable) + } + repository = ExposedUserRepository(database) + } + + beforeTest { + transaction(database) { + UsersTable.deleteAll() + } + } + + test("create and find user") { + val user = repository.create(CreateUserRequest("Alice", "alice@example.com")) + + user.name shouldBe "Alice" + user.email shouldBe "alice@example.com" + + val found = repository.findById(user.id) + found shouldBe user + } + + test("findByEmail returns null for unknown email") { + val result = repository.findByEmail("unknown@example.com") + result.shouldBeNull() + } + + test("pagination works correctly") { + repeat(25) { i -> + repository.create(CreateUserRequest("User $i", "user$i@example.com")) + } + + val page1 = repository.findAll(page = 1, limit = 10) + page1.data shouldHaveSize 10 + page1.total shouldBe 25 + page1.hasNext shouldBe true + + val page3 = repository.findAll(page = 3, limit = 10) + page3.data shouldHaveSize 5 + page3.hasNext shouldBe false + } +}) +``` + +## Gradle 依赖项 + +```kotlin +// build.gradle.kts +dependencies { + // Exposed + implementation("org.jetbrains.exposed:exposed-core:1.0.0") + implementation("org.jetbrains.exposed:exposed-dao:1.0.0") + implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0") + implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0") + implementation("org.jetbrains.exposed:exposed-json:1.0.0") + + // Database driver + implementation("org.postgresql:postgresql:42.7.5") + + // Connection pooling + implementation("com.zaxxer:HikariCP:6.2.1") + + // Migrations + implementation("org.flywaydb:flyway-core:10.22.0") + implementation("org.flywaydb:flyway-database-postgresql:10.22.0") + + // Testing + testImplementation("com.h2database:h2:2.3.232") +} +``` + +## 快速参考:Exposed 模式 + +| 模式 | 描述 | +|---------|-------------| +| `object Table : UUIDTable("name")` | 定义具有 UUID 主键的表 | +| `newSuspendedTransaction { }` | 协程安全的事务块 | +| `Table.selectAll().where { }` | 带条件的查询 | +| `Table.insertAndGetId { }` | 插入并返回生成的 ID | +| `Table.update({ condition }) { }` | 更新匹配的行 | +| `Table.deleteWhere { }` | 删除匹配的行 | +| `Table.batchInsert(items) { }` | 高效的批量插入 | +| `innerJoin` / `leftJoin` | 连接表 | +| `orderBy` / `limit` / `offset` | 排序和分页 | +| `count()` / `sum()` / `avg()` | 聚合函数 | + +**记住**:对于简单查询使用 DSL 风格,当需要实体生命周期管理时使用 DAO 风格。始终使用 `newSuspendedTransaction` 以获得协程支持,并将数据库操作包装在仓储接口之后以提高可测试性。 diff --git a/docs/zh-CN/skills/kotlin-ktor-patterns/SKILL.md b/docs/zh-CN/skills/kotlin-ktor-patterns/SKILL.md new file mode 100644 index 00000000..bce7e0a2 --- /dev/null +++ b/docs/zh-CN/skills/kotlin-ktor-patterns/SKILL.md @@ -0,0 +1,689 @@ +--- +name: kotlin-ktor-patterns +description: Ktor 服务器模式,包括路由 DSL、插件、身份验证、Koin DI、kotlinx.serialization、WebSockets 和 testApplication 测试。 +origin: ECC +--- + +# Ktor 服务器模式 + +使用 Kotlin 协程构建健壮、可维护的 HTTP 服务器的综合 Ktor 模式。 + +## 何时启用 + +* 构建 Ktor HTTP 服务器 +* 配置 Ktor 插件(Auth、CORS、ContentNegotiation、StatusPages) +* 使用 Ktor 实现 REST API +* 使用 Koin 设置依赖注入 +* 使用 testApplication 编写 Ktor 集成测试 +* 在 Ktor 中使用 WebSocket + +## 应用程序结构 + +### 标准 Ktor 项目布局 + +```text +src/main/kotlin/ +├── com/example/ +│ ├── Application.kt # Entry point, module configuration +│ ├── plugins/ +│ │ ├── Routing.kt # Route definitions +│ │ ├── Serialization.kt # Content negotiation setup +│ │ ├── Authentication.kt # Auth configuration +│ │ ├── StatusPages.kt # Error handling +│ │ └── CORS.kt # CORS configuration +│ ├── routes/ +│ │ ├── UserRoutes.kt # /users endpoints +│ │ ├── AuthRoutes.kt # /auth endpoints +│ │ └── HealthRoutes.kt # /health endpoints +│ ├── models/ +│ │ ├── User.kt # Domain models +│ │ └── ApiResponse.kt # Response envelopes +│ ├── services/ +│ │ ├── UserService.kt # Business logic +│ │ └── AuthService.kt # Auth logic +│ ├── repositories/ +│ │ ├── UserRepository.kt # Data access interface +│ │ └── ExposedUserRepository.kt +│ └── di/ +│ └── AppModule.kt # Koin modules +src/test/kotlin/ +├── com/example/ +│ ├── routes/ +│ │ └── UserRoutesTest.kt +│ └── services/ +│ └── UserServiceTest.kt +``` + +### 应用程序入口点 + +```kotlin +// Application.kt +fun main() { + embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true) +} + +fun Application.module() { + configureSerialization() + configureAuthentication() + configureStatusPages() + configureCORS() + configureDI() + configureRouting() +} +``` + +## 路由 DSL + +### 基本路由 + +```kotlin +// plugins/Routing.kt +fun Application.configureRouting() { + routing { + userRoutes() + authRoutes() + healthRoutes() + } +} + +// routes/UserRoutes.kt +fun Route.userRoutes() { + val userService by inject() + + route("/users") { + get { + val users = userService.getAll() + call.respond(users) + } + + get("/{id}") { + val id = call.parameters["id"] + ?: return@get call.respond(HttpStatusCode.BadRequest, "Missing id") + val user = userService.getById(id) + ?: return@get call.respond(HttpStatusCode.NotFound) + call.respond(user) + } + + post { + val request = call.receive() + val user = userService.create(request) + call.respond(HttpStatusCode.Created, user) + } + + put("/{id}") { + val id = call.parameters["id"] + ?: return@put call.respond(HttpStatusCode.BadRequest, "Missing id") + val request = call.receive() + val user = userService.update(id, request) + ?: return@put call.respond(HttpStatusCode.NotFound) + call.respond(user) + } + + delete("/{id}") { + val id = call.parameters["id"] + ?: return@delete call.respond(HttpStatusCode.BadRequest, "Missing id") + val deleted = userService.delete(id) + if (deleted) call.respond(HttpStatusCode.NoContent) + else call.respond(HttpStatusCode.NotFound) + } + } +} +``` + +### 使用认证路由组织路由 + +```kotlin +fun Route.userRoutes() { + route("/users") { + // Public routes + get { /* list users */ } + get("/{id}") { /* get user */ } + + // Protected routes + authenticate("jwt") { + post { /* create user - requires auth */ } + put("/{id}") { /* update user - requires auth */ } + delete("/{id}") { /* delete user - requires auth */ } + } + } +} +``` + +## 内容协商与序列化 + +### kotlinx.serialization 设置 + +```kotlin +// plugins/Serialization.kt +fun Application.configureSerialization() { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = false + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + }) + } +} +``` + +### 可序列化模型 + +```kotlin +@Serializable +data class UserResponse( + val id: String, + val name: String, + val email: String, + val role: Role, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, +) + +@Serializable +data class CreateUserRequest( + val name: String, + val email: String, + val role: Role = Role.USER, +) + +@Serializable +data class ApiResponse( + val success: Boolean, + val data: T? = null, + val error: String? = null, +) { + companion object { + fun ok(data: T): ApiResponse = ApiResponse(success = true, data = data) + fun error(message: String): ApiResponse = ApiResponse(success = false, error = message) + } +} + +@Serializable +data class PaginatedResponse( + val data: List, + val total: Long, + val page: Int, + val limit: Int, +) +``` + +### 自定义序列化器 + +```kotlin +object InstantSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: Instant) = + encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): Instant = + Instant.parse(decoder.decodeString()) +} +``` + +## 身份验证 + +### JWT 身份验证 + +```kotlin +// plugins/Authentication.kt +fun Application.configureAuthentication() { + val jwtSecret = environment.config.property("jwt.secret").getString() + val jwtIssuer = environment.config.property("jwt.issuer").getString() + val jwtAudience = environment.config.property("jwt.audience").getString() + val jwtRealm = environment.config.property("jwt.realm").getString() + + install(Authentication) { + jwt("jwt") { + realm = jwtRealm + verifier( + JWT.require(Algorithm.HMAC256(jwtSecret)) + .withAudience(jwtAudience) + .withIssuer(jwtIssuer) + .build() + ) + validate { credential -> + if (credential.payload.audience.contains(jwtAudience)) { + JWTPrincipal(credential.payload) + } else { + null + } + } + challenge { _, _ -> + call.respond(HttpStatusCode.Unauthorized, ApiResponse.error("Invalid or expired token")) + } + } + } +} + +// Extracting user from JWT +fun ApplicationCall.userId(): String = + principal() + ?.payload + ?.getClaim("userId") + ?.asString() + ?: throw AuthenticationException("No userId in token") +``` + +### 认证路由 + +```kotlin +fun Route.authRoutes() { + val authService by inject() + + route("/auth") { + post("/login") { + val request = call.receive() + val token = authService.login(request.email, request.password) + ?: return@post call.respond( + HttpStatusCode.Unauthorized, + ApiResponse.error("Invalid credentials"), + ) + call.respond(ApiResponse.ok(TokenResponse(token))) + } + + post("/register") { + val request = call.receive() + val user = authService.register(request) + call.respond(HttpStatusCode.Created, ApiResponse.ok(user)) + } + + authenticate("jwt") { + get("/me") { + val userId = call.userId() + val user = authService.getProfile(userId) + call.respond(ApiResponse.ok(user)) + } + } + } +} +``` + +## 状态页(错误处理) + +```kotlin +// plugins/StatusPages.kt +fun Application.configureStatusPages() { + install(StatusPages) { + exception { call, cause -> + call.respond( + HttpStatusCode.BadRequest, + ApiResponse.error("Invalid request body: ${cause.message}"), + ) + } + + exception { call, cause -> + call.respond( + HttpStatusCode.BadRequest, + ApiResponse.error(cause.message ?: "Bad request"), + ) + } + + exception { call, _ -> + call.respond( + HttpStatusCode.Unauthorized, + ApiResponse.error("Authentication required"), + ) + } + + exception { call, _ -> + call.respond( + HttpStatusCode.Forbidden, + ApiResponse.error("Access denied"), + ) + } + + exception { call, cause -> + call.respond( + HttpStatusCode.NotFound, + ApiResponse.error(cause.message ?: "Resource not found"), + ) + } + + exception { call, cause -> + call.application.log.error("Unhandled exception", cause) + call.respond( + HttpStatusCode.InternalServerError, + ApiResponse.error("Internal server error"), + ) + } + + status(HttpStatusCode.NotFound) { call, status -> + call.respond(status, ApiResponse.error("Route not found")) + } + } +} +``` + +## CORS 配置 + +```kotlin +// plugins/CORS.kt +fun Application.configureCORS() { + install(CORS) { + allowHost("localhost:3000") + allowHost("example.com", schemes = listOf("https")) + allowHeader(HttpHeaders.ContentType) + allowHeader(HttpHeaders.Authorization) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowCredentials = true + maxAgeInSeconds = 3600 + } +} +``` + +## Koin 依赖注入 + +### 模块定义 + +```kotlin +// di/AppModule.kt +val appModule = module { + // Database + single { DatabaseFactory.create(get()) } + + // Repositories + single { ExposedUserRepository(get()) } + single { ExposedOrderRepository(get()) } + + // Services + single { UserService(get()) } + single { OrderService(get(), get()) } + single { AuthService(get(), get()) } +} + +// Application setup +fun Application.configureDI() { + install(Koin) { + modules(appModule) + } +} +``` + +### 在路由中使用 Koin + +```kotlin +fun Route.userRoutes() { + val userService by inject() + + route("/users") { + get { + val users = userService.getAll() + call.respond(ApiResponse.ok(users)) + } + } +} +``` + +### 用于测试的 Koin + +```kotlin +class UserServiceTest : FunSpec(), KoinTest { + override fun extensions() = listOf(KoinExtension(testModule)) + + private val testModule = module { + single { mockk() } + single { UserService(get()) } + } + + private val repository by inject() + private val service by inject() + + init { + test("getUser returns user") { + coEvery { repository.findById("1") } returns testUser + service.getById("1") shouldBe testUser + } + } +} +``` + +## 请求验证 + +```kotlin +// Validate request data in routes +fun Route.userRoutes() { + val userService by inject() + + post("/users") { + val request = call.receive() + + // Validate + require(request.name.isNotBlank()) { "Name is required" } + require(request.name.length <= 100) { "Name must be 100 characters or less" } + require(request.email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" } + + val user = userService.create(request) + call.respond(HttpStatusCode.Created, ApiResponse.ok(user)) + } +} + +// Or use a validation extension +fun CreateUserRequest.validate() { + require(name.isNotBlank()) { "Name is required" } + require(name.length <= 100) { "Name must be 100 characters or less" } + require(email.matches(Regex(".+@.+\\..+"))) { "Invalid email format" } +} +``` + +## WebSocket + +```kotlin +fun Application.configureWebSockets() { + install(WebSockets) { + pingPeriod = 15.seconds + timeout = 15.seconds + maxFrameSize = 64 * 1024 // 64 KiB — increase only if your protocol requires larger frames + masking = false // Server-to-client frames are unmasked per RFC 6455; client-to-server are always masked by Ktor + } +} + +fun Route.chatRoutes() { + val connections = Collections.synchronizedSet(LinkedHashSet()) + + webSocket("/chat") { + val thisConnection = Connection(this) + connections += thisConnection + + try { + send("Connected! Users online: ${connections.size}") + + for (frame in incoming) { + frame as? Frame.Text ?: continue + val text = frame.readText() + val message = ChatMessage(thisConnection.name, text) + + // Snapshot under lock to avoid ConcurrentModificationException + val snapshot = synchronized(connections) { connections.toList() } + snapshot.forEach { conn -> + conn.session.send(Json.encodeToString(message)) + } + } + } catch (e: Exception) { + logger.error("WebSocket error", e) + } finally { + connections -= thisConnection + } + } +} + +data class Connection(val session: DefaultWebSocketSession) { + val name: String = "User-${counter.getAndIncrement()}" + + companion object { + private val counter = AtomicInteger(0) + } +} +``` + +## testApplication 测试 + +### 基本路由测试 + +```kotlin +class UserRoutesTest : FunSpec({ + test("GET /users returns list of users") { + testApplication { + application { + install(Koin) { modules(testModule) } + configureSerialization() + configureRouting() + } + + val response = client.get("/users") + + response.status shouldBe HttpStatusCode.OK + val body = response.body>>() + body.success shouldBe true + body.data.shouldNotBeNull().shouldNotBeEmpty() + } + } + + test("POST /users creates a user") { + testApplication { + application { + install(Koin) { modules(testModule) } + configureSerialization() + configureStatusPages() + configureRouting() + } + + val client = createClient { + install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { + json() + } + } + + val response = client.post("/users") { + contentType(ContentType.Application.Json) + setBody(CreateUserRequest("Alice", "alice@example.com")) + } + + response.status shouldBe HttpStatusCode.Created + } + } + + test("GET /users/{id} returns 404 for unknown id") { + testApplication { + application { + install(Koin) { modules(testModule) } + configureSerialization() + configureStatusPages() + configureRouting() + } + + val response = client.get("/users/unknown-id") + + response.status shouldBe HttpStatusCode.NotFound + } + } +}) +``` + +### 测试认证路由 + +```kotlin +class AuthenticatedRoutesTest : FunSpec({ + test("protected route requires JWT") { + testApplication { + application { + install(Koin) { modules(testModule) } + configureSerialization() + configureAuthentication() + configureRouting() + } + + val response = client.post("/users") { + contentType(ContentType.Application.Json) + setBody(CreateUserRequest("Alice", "alice@example.com")) + } + + response.status shouldBe HttpStatusCode.Unauthorized + } + } + + test("protected route succeeds with valid JWT") { + testApplication { + application { + install(Koin) { modules(testModule) } + configureSerialization() + configureAuthentication() + configureRouting() + } + + val token = generateTestJWT(userId = "test-user") + + val client = createClient { + install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { json() } + } + + val response = client.post("/users") { + contentType(ContentType.Application.Json) + bearerAuth(token) + setBody(CreateUserRequest("Alice", "alice@example.com")) + } + + response.status shouldBe HttpStatusCode.Created + } + } +}) +``` + +## 配置 + +### application.yaml + +```yaml +ktor: + application: + modules: + - com.example.ApplicationKt.module + deployment: + port: 8080 + +jwt: + secret: ${JWT_SECRET} + issuer: "https://example.com" + audience: "https://example.com/api" + realm: "example" + +database: + url: ${DATABASE_URL} + driver: "org.postgresql.Driver" + maxPoolSize: 10 +``` + +### 读取配置 + +```kotlin +fun Application.configureDI() { + val dbUrl = environment.config.property("database.url").getString() + val dbDriver = environment.config.property("database.driver").getString() + val maxPoolSize = environment.config.property("database.maxPoolSize").getString().toInt() + + install(Koin) { + modules(module { + single { DatabaseConfig(dbUrl, dbDriver, maxPoolSize) } + single { DatabaseFactory.create(get()) } + }) + } +} +``` + +## 快速参考:Ktor 模式 + +| 模式 | 描述 | +|---------|-------------| +| `route("/path") { get { } }` | 使用 DSL 进行路由分组 | +| `call.receive()` | 反序列化请求体 | +| `call.respond(status, body)` | 发送带状态的响应 | +| `call.parameters["id"]` | 读取路径参数 | +| `call.request.queryParameters["q"]` | 读取查询参数 | +| `install(Plugin) { }` | 安装并配置插件 | +| `authenticate("name") { }` | 使用身份验证保护路由 | +| `by inject()` | Koin 依赖注入 | +| `testApplication { }` | 集成测试 | + +**记住**:Ktor 是围绕 Kotlin 协程和 DSL 设计的。保持路由精简,将逻辑推送到服务层,并使用 Koin 进行依赖注入。使用 `testApplication` 进行测试以获得完整的集成覆盖。 diff --git a/docs/zh-CN/skills/kotlin-patterns/SKILL.md b/docs/zh-CN/skills/kotlin-patterns/SKILL.md new file mode 100644 index 00000000..1edd2164 --- /dev/null +++ b/docs/zh-CN/skills/kotlin-patterns/SKILL.md @@ -0,0 +1,714 @@ +--- +name: kotlin-patterns +description: 惯用的Kotlin模式、最佳实践和约定,用于构建健壮、高效且可维护的Kotlin应用程序,包括协程、空安全和DSL构建器。 +origin: ECC +--- + +# Kotlin 开发模式 + +适用于构建健壮、高效、可维护应用程序的惯用 Kotlin 模式与最佳实践。 + +## 使用时机 + +* 编写新的 Kotlin 代码 +* 审查 Kotlin 代码 +* 重构现有的 Kotlin 代码 +* 设计 Kotlin 模块或库 +* 配置 Gradle Kotlin DSL 构建 + +## 工作原理 + +本技能在七个关键领域强制执行惯用的 Kotlin 约定:使用类型系统和安全调用运算符实现空安全;通过数据类的 `val` 和 `copy()` 实现不可变性;使用密封类和接口实现穷举类型层次结构;使用协程和 `Flow` 实现结构化并发;使用扩展函数在不使用继承的情况下添加行为;使用 `@DslMarker` 和 lambda 接收器构建类型安全的 DSL;以及使用 Gradle Kotlin DSL 进行构建配置。 + +## 示例 + +**使用 Elvis 运算符实现空安全:** + +```kotlin +fun getUserEmail(userId: String): String { + val user = userRepository.findById(userId) + return user?.email ?: "unknown@example.com" +} +``` + +**使用密封类处理穷举结果:** + +```kotlin +sealed class Result { + data class Success(val data: T) : Result() + data class Failure(val error: AppError) : Result() + data object Loading : Result() +} +``` + +**使用 async/await 实现结构化并发:** + +```kotlin +suspend fun fetchUserWithPosts(userId: String): UserProfile = + coroutineScope { + val user = async { userService.getUser(userId) } + val posts = async { postService.getUserPosts(userId) } + UserProfile(user = user.await(), posts = posts.await()) + } +``` + +## 核心原则 + +### 1. 空安全 + +Kotlin 的类型系统区分可空和不可空类型。充分利用它。 + +```kotlin +// Good: Use non-nullable types by default +fun getUser(id: String): User { + return userRepository.findById(id) + ?: throw UserNotFoundException("User $id not found") +} + +// Good: Safe calls and Elvis operator +fun getUserEmail(userId: String): String { + val user = userRepository.findById(userId) + return user?.email ?: "unknown@example.com" +} + +// Bad: Force-unwrapping nullable types +fun getUserEmail(userId: String): String { + val user = userRepository.findById(userId) + return user!!.email // Throws NPE if null +} +``` + +### 2. 默认不可变性 + +优先使用 `val` 而非 `var`,优先使用不可变集合而非可变集合。 + +```kotlin +// Good: Immutable data +data class User( + val id: String, + val name: String, + val email: String, +) + +// Good: Transform with copy() +fun updateEmail(user: User, newEmail: String): User = + user.copy(email = newEmail) + +// Good: Immutable collections +val users: List = listOf(user1, user2) +val filtered = users.filter { it.email.isNotBlank() } + +// Bad: Mutable state +var currentUser: User? = null // Avoid mutable global state +val mutableUsers = mutableListOf() // Avoid unless truly needed +``` + +### 3. 表达式体和单表达式函数 + +使用表达式体编写简洁、可读的函数。 + +```kotlin +// Good: Expression body +fun isAdult(age: Int): Boolean = age >= 18 + +fun formatFullName(first: String, last: String): String = + "$first $last".trim() + +fun User.displayName(): String = + name.ifBlank { email.substringBefore('@') } + +// Good: When as expression +fun statusMessage(code: Int): String = when (code) { + 200 -> "OK" + 404 -> "Not Found" + 500 -> "Internal Server Error" + else -> "Unknown status: $code" +} + +// Bad: Unnecessary block body +fun isAdult(age: Int): Boolean { + return age >= 18 +} +``` + +### 4. 数据类用于值对象 + +使用数据类表示主要包含数据的类型。 + +```kotlin +// Good: Data class with copy, equals, hashCode, toString +data class CreateUserRequest( + val name: String, + val email: String, + val role: Role = Role.USER, +) + +// Good: Value class for type safety (zero overhead at runtime) +@JvmInline +value class UserId(val value: String) { + init { + require(value.isNotBlank()) { "UserId cannot be blank" } + } +} + +@JvmInline +value class Email(val value: String) { + init { + require('@' in value) { "Invalid email: $value" } + } +} + +fun getUser(id: UserId): User = userRepository.findById(id) +``` + +## 密封类和接口 + +### 建模受限的层次结构 + +```kotlin +// Good: Sealed class for exhaustive when +sealed class Result { + data class Success(val data: T) : Result() + data class Failure(val error: AppError) : Result() + data object Loading : Result() +} + +fun Result.getOrNull(): T? = when (this) { + is Result.Success -> data + is Result.Failure -> null + is Result.Loading -> null +} + +fun Result.getOrThrow(): T = when (this) { + is Result.Success -> data + is Result.Failure -> throw error.toException() + is Result.Loading -> throw IllegalStateException("Still loading") +} +``` + +### 用于 API 响应的密封接口 + +```kotlin +sealed interface ApiError { + val message: String + + data class NotFound(override val message: String) : ApiError + data class Unauthorized(override val message: String) : ApiError + data class Validation( + override val message: String, + val field: String, + ) : ApiError + data class Internal( + override val message: String, + val cause: Throwable? = null, + ) : ApiError +} + +fun ApiError.toStatusCode(): Int = when (this) { + is ApiError.NotFound -> 404 + is ApiError.Unauthorized -> 401 + is ApiError.Validation -> 422 + is ApiError.Internal -> 500 +} +``` + +## 作用域函数 + +### 何时使用各个函数 + +```kotlin +// let: Transform nullable or scoped result +val length: Int? = name?.let { it.trim().length } + +// apply: Configure an object (returns the object) +val user = User().apply { + name = "Alice" + email = "alice@example.com" +} + +// also: Side effects (returns the object) +val user = createUser(request).also { logger.info("Created user: ${it.id}") } + +// run: Execute a block with receiver (returns result) +val result = connection.run { + prepareStatement(sql) + executeQuery() +} + +// with: Non-extension form of run +val csv = with(StringBuilder()) { + appendLine("name,email") + users.forEach { appendLine("${it.name},${it.email}") } + toString() +} +``` + +### 反模式 + +```kotlin +// Bad: Nesting scope functions +user?.let { u -> + u.address?.let { addr -> + addr.city?.let { city -> + println(city) // Hard to read + } + } +} + +// Good: Chain safe calls instead +val city = user?.address?.city +city?.let { println(it) } +``` + +## 扩展函数 + +### 在不使用继承的情况下添加功能 + +```kotlin +// Good: Domain-specific extensions +fun String.toSlug(): String = + lowercase() + .replace(Regex("[^a-z0-9\\s-]"), "") + .replace(Regex("\\s+"), "-") + .trim('-') + +fun Instant.toLocalDate(zone: ZoneId = ZoneId.systemDefault()): LocalDate = + atZone(zone).toLocalDate() + +// Good: Collection extensions +fun List.second(): T = this[1] + +fun List.secondOrNull(): T? = getOrNull(1) + +// Good: Scoped extensions (not polluting global namespace) +class UserService { + private fun User.isActive(): Boolean = + status == Status.ACTIVE && lastLogin.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) + + fun getActiveUsers(): List = userRepository.findAll().filter { it.isActive() } +} +``` + +## 协程 + +### 结构化并发 + +```kotlin +// Good: Structured concurrency with coroutineScope +suspend fun fetchUserWithPosts(userId: String): UserProfile = + coroutineScope { + val userDeferred = async { userService.getUser(userId) } + val postsDeferred = async { postService.getUserPosts(userId) } + + UserProfile( + user = userDeferred.await(), + posts = postsDeferred.await(), + ) + } + +// Good: supervisorScope when children can fail independently +suspend fun fetchDashboard(userId: String): Dashboard = + supervisorScope { + val user = async { userService.getUser(userId) } + val notifications = async { notificationService.getRecent(userId) } + val recommendations = async { recommendationService.getFor(userId) } + + Dashboard( + user = user.await(), + notifications = try { + notifications.await() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + emptyList() + }, + recommendations = try { + recommendations.await() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + emptyList() + }, + ) + } +``` + +### Flow 用于响应式流 + +```kotlin +// Good: Cold flow with proper error handling +fun observeUsers(): Flow> = flow { + while (currentCoroutineContext().isActive) { + val users = userRepository.findAll() + emit(users) + delay(5.seconds) + } +}.catch { e -> + logger.error("Error observing users", e) + emit(emptyList()) +} + +// Good: Flow operators +fun searchUsers(query: Flow): Flow> = + query + .debounce(300.milliseconds) + .distinctUntilChanged() + .filter { it.length >= 2 } + .mapLatest { q -> userRepository.search(q) } + .catch { emit(emptyList()) } +``` + +### 取消与清理 + +```kotlin +// Good: Respect cancellation +suspend fun processItems(items: List) { + items.forEach { item -> + ensureActive() // Check cancellation before expensive work + processItem(item) + } +} + +// Good: Cleanup with try/finally +suspend fun acquireAndProcess() { + val resource = acquireResource() + try { + resource.process() + } finally { + withContext(NonCancellable) { + resource.release() // Always release, even on cancellation + } + } +} +``` + +## 委托 + +### 属性委托 + +```kotlin +// Lazy initialization +val expensiveData: List by lazy { + userRepository.findAll() +} + +// Observable property +var name: String by Delegates.observable("initial") { _, old, new -> + logger.info("Name changed from '$old' to '$new'") +} + +// Map-backed properties +class Config(private val map: Map) { + val host: String by map + val port: Int by map + val debug: Boolean by map +} + +val config = Config(mapOf("host" to "localhost", "port" to 8080, "debug" to true)) +``` + +### 接口委托 + +```kotlin +// Good: Delegate interface implementation +class LoggingUserRepository( + private val delegate: UserRepository, + private val logger: Logger, +) : UserRepository by delegate { + // Only override what you need to add logging to + override suspend fun findById(id: String): User? { + logger.info("Finding user by id: $id") + return delegate.findById(id).also { + logger.info("Found user: ${it?.name ?: "null"}") + } + } +} +``` + +## DSL 构建器 + +### 类型安全构建器 + +```kotlin +// Good: DSL with @DslMarker +@DslMarker +annotation class HtmlDsl + +@HtmlDsl +class HTML { + private val children = mutableListOf() + + fun head(init: Head.() -> Unit) { + children += Head().apply(init) + } + + fun body(init: Body.() -> Unit) { + children += Body().apply(init) + } + + override fun toString(): String = children.joinToString("\n") +} + +fun html(init: HTML.() -> Unit): HTML = HTML().apply(init) + +// Usage +val page = html { + head { title("My Page") } + body { + h1("Welcome") + p("Hello, World!") + } +} +``` + +### 配置 DSL + +```kotlin +data class ServerConfig( + val host: String = "0.0.0.0", + val port: Int = 8080, + val ssl: SslConfig? = null, + val database: DatabaseConfig? = null, +) + +data class SslConfig(val certPath: String, val keyPath: String) +data class DatabaseConfig(val url: String, val maxPoolSize: Int = 10) + +class ServerConfigBuilder { + var host: String = "0.0.0.0" + var port: Int = 8080 + private var ssl: SslConfig? = null + private var database: DatabaseConfig? = null + + fun ssl(certPath: String, keyPath: String) { + ssl = SslConfig(certPath, keyPath) + } + + fun database(url: String, maxPoolSize: Int = 10) { + database = DatabaseConfig(url, maxPoolSize) + } + + fun build(): ServerConfig = ServerConfig(host, port, ssl, database) +} + +fun serverConfig(init: ServerConfigBuilder.() -> Unit): ServerConfig = + ServerConfigBuilder().apply(init).build() + +// Usage +val config = serverConfig { + host = "0.0.0.0" + port = 443 + ssl("/certs/cert.pem", "/certs/key.pem") + database("jdbc:postgresql://localhost:5432/mydb", maxPoolSize = 20) +} +``` + +## 用于惰性求值的序列 + +```kotlin +// Good: Use sequences for large collections with multiple operations +val result = users.asSequence() + .filter { it.isActive } + .map { it.email } + .filter { it.endsWith("@company.com") } + .take(10) + .toList() + +// Good: Generate infinite sequences +val fibonacci: Sequence = sequence { + var a = 0L + var b = 1L + while (true) { + yield(a) + val next = a + b + a = b + b = next + } +} + +val first20 = fibonacci.take(20).toList() +``` + +## Gradle Kotlin DSL + +### build.gradle.kts 配置 + +```kotlin +// Check for latest versions: https://kotlinlang.org/docs/releases.html +plugins { + kotlin("jvm") version "2.3.10" + kotlin("plugin.serialization") version "2.3.10" + id("io.ktor.plugin") version "3.4.0" + id("org.jetbrains.kotlinx.kover") version "0.9.7" + id("io.gitlab.arturbosch.detekt") version "1.23.8" +} + +group = "com.example" +version = "1.0.0" + +kotlin { + jvmToolchain(21) +} + +dependencies { + // Ktor + implementation("io.ktor:ktor-server-core:3.4.0") + implementation("io.ktor:ktor-server-netty:3.4.0") + implementation("io.ktor:ktor-server-content-negotiation:3.4.0") + implementation("io.ktor:ktor-serialization-kotlinx-json:3.4.0") + + // Exposed + implementation("org.jetbrains.exposed:exposed-core:1.0.0") + implementation("org.jetbrains.exposed:exposed-dao:1.0.0") + implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0") + implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0") + + // Koin + implementation("io.insert-koin:koin-ktor:4.2.0") + + // Coroutines + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + + // Testing + testImplementation("io.kotest:kotest-runner-junit5:6.1.4") + testImplementation("io.kotest:kotest-assertions-core:6.1.4") + testImplementation("io.kotest:kotest-property:6.1.4") + testImplementation("io.mockk:mockk:1.14.9") + testImplementation("io.ktor:ktor-server-test-host:3.4.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") +} + +tasks.withType { + useJUnitPlatform() +} + +detekt { + config.setFrom(files("config/detekt/detekt.yml")) + buildUponDefaultConfig = true +} +``` + +## 错误处理模式 + +### 用于领域操作的 Result 类型 + +```kotlin +// Good: Use Kotlin's Result or a custom sealed class +suspend fun createUser(request: CreateUserRequest): Result = runCatching { + require(request.name.isNotBlank()) { "Name cannot be blank" } + require('@' in request.email) { "Invalid email format" } + + val user = User( + id = UserId(UUID.randomUUID().toString()), + name = request.name, + email = Email(request.email), + ) + userRepository.save(user) + user +} + +// Good: Chain results +val displayName = createUser(request) + .map { it.name } + .getOrElse { "Unknown" } +``` + +### require, check, error + +```kotlin +// Good: Preconditions with clear messages +fun withdraw(account: Account, amount: Money): Account { + require(amount.value > 0) { "Amount must be positive: $amount" } + check(account.balance >= amount) { "Insufficient balance: ${account.balance} < $amount" } + + return account.copy(balance = account.balance - amount) +} +``` + +## 集合操作 + +### 惯用的集合处理 + +```kotlin +// Good: Chained operations +val activeAdminEmails: List = users + .filter { it.role == Role.ADMIN && it.isActive } + .sortedBy { it.name } + .map { it.email } + +// Good: Grouping and aggregation +val usersByRole: Map> = users.groupBy { it.role } + +val oldestByRole: Map = users.groupBy { it.role } + .mapValues { (_, users) -> users.minByOrNull { it.createdAt } } + +// Good: Associate for map creation +val usersById: Map = users.associateBy { it.id } + +// Good: Partition for splitting +val (active, inactive) = users.partition { it.isActive } +``` + +## 快速参考:Kotlin 惯用法 + +| 惯用法 | 描述 | +|-------|-------------| +| `val` 优于 `var` | 优先使用不可变变量 | +| `data class` | 用于具有 equals/hashCode/copy 的值对象 | +| `sealed class/interface` | 用于受限的类型层次结构 | +| `value class` | 用于零开销的类型安全包装器 | +| 表达式 `when` | 穷举模式匹配 | +| 安全调用 `?.` | 空安全的成员访问 | +| Elvis `?:` | 为可空类型提供默认值 | +| `let`/`apply`/`also`/`run`/`with` | 用于编写简洁代码的作用域函数 | +| 扩展函数 | 在不使用继承的情况下添加行为 | +| `copy()` | 数据类上的不可变更新 | +| `require`/`check` | 前置条件断言 | +| 协程 `async`/`await` | 结构化并发执行 | +| `Flow` | 冷响应式流 | +| `sequence` | 惰性求值 | +| 委托 `by` | 在不使用继承的情况下重用实现 | + +## 应避免的反模式 + +```kotlin +// Bad: Force-unwrapping nullable types +val name = user!!.name + +// Bad: Platform type leakage from Java +fun getLength(s: String) = s.length // Safe +fun getLength(s: String?) = s?.length ?: 0 // Handle nulls from Java + +// Bad: Mutable data classes +data class MutableUser(var name: String, var email: String) + +// Bad: Using exceptions for control flow +try { + val user = findUser(id) +} catch (e: NotFoundException) { + // Don't use exceptions for expected cases +} + +// Good: Use nullable return or Result +val user: User? = findUserOrNull(id) + +// Bad: Ignoring coroutine scope +GlobalScope.launch { /* Avoid GlobalScope */ } + +// Good: Use structured concurrency +coroutineScope { + launch { /* Properly scoped */ } +} + +// Bad: Deeply nested scope functions +user?.let { u -> + u.address?.let { a -> + a.city?.let { c -> process(c) } + } +} + +// Good: Direct null-safe chain +user?.address?.city?.let { process(it) } +``` + +**请记住**:Kotlin 代码应简洁但可读。利用类型系统确保安全,优先使用不可变性,并使用协程处理并发。如有疑问,让编译器帮助你。 diff --git a/docs/zh-CN/skills/kotlin-testing/SKILL.md b/docs/zh-CN/skills/kotlin-testing/SKILL.md new file mode 100644 index 00000000..81b14537 --- /dev/null +++ b/docs/zh-CN/skills/kotlin-testing/SKILL.md @@ -0,0 +1,826 @@ +--- +name: kotlin-testing +description: 使用Kotest、MockK、协程测试、基于属性的测试和Kover覆盖率的Kotlin测试模式。遵循TDD方法论和地道的Kotlin实践。 +origin: ECC +--- + +# Kotlin 测试模式 + +遵循 TDD 方法论,使用 Kotest 和 MockK 编写可靠、可维护测试的全面 Kotlin 测试模式。 + +## 何时使用 + +* 编写新的 Kotlin 函数或类 +* 为现有 Kotlin 代码添加测试覆盖率 +* 实现基于属性的测试 +* 在 Kotlin 项目中遵循 TDD 工作流 +* 为代码覆盖率配置 Kover + +## 工作原理 + +1. **确定目标代码** — 找到要测试的函数、类或模块 +2. **编写 Kotest 规范** — 选择与测试范围匹配的规范样式(StringSpec、FunSpec、BehaviorSpec) +3. **模拟依赖项** — 使用 MockK 来隔离被测单元 +4. **运行测试(红色阶段)** — 验证测试是否按预期失败 +5. **实现代码(绿色阶段)** — 编写最少的代码以使测试通过 +6. **重构** — 改进实现,同时保持测试通过 +7. **检查覆盖率** — 运行 `./gradlew koverHtmlReport` 并验证 80%+ 的覆盖率 + +## 示例 + +以下部分包含每个测试模式的详细、可运行示例: + +### 快速参考 + +* **Kotest 规范** — [Kotest 规范样式](#kotest-规范样式) 中的 StringSpec、FunSpec、BehaviorSpec、DescribeSpec 示例 +* **模拟** — [MockK](#mockk) 中的 MockK 设置、协程模拟、参数捕获 +* **TDD 演练** — [Kotlin 的 TDD 工作流](#kotlin-的-tdd-工作流) 中 EmailValidator 的完整 RED/GREEN/REFACTOR 周期 +* **覆盖率** — [Kover 覆盖率](#kover-覆盖率) 中的 Kover 配置和命令 +* **Ktor 测试** — [Ktor testApplication 测试](#ktor-testapplication-测试) 中的 testApplication 设置 + +### Kotlin 的 TDD 工作流 + +#### RED-GREEN-REFACTOR 周期 + +``` +RED -> Write a failing test first +GREEN -> Write minimal code to pass the test +REFACTOR -> Improve code while keeping tests green +REPEAT -> Continue with next requirement +``` + +#### Kotlin 中逐步进行 TDD + +```kotlin +// Step 1: Define the interface/signature +// EmailValidator.kt +package com.example.validator + +fun validateEmail(email: String): Result { + TODO("not implemented") +} + +// Step 2: Write failing test (RED) +// EmailValidatorTest.kt +package com.example.validator + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.result.shouldBeFailure +import io.kotest.matchers.result.shouldBeSuccess + +class EmailValidatorTest : StringSpec({ + "valid email returns success" { + validateEmail("user@example.com").shouldBeSuccess("user@example.com") + } + + "empty email returns failure" { + validateEmail("").shouldBeFailure() + } + + "email without @ returns failure" { + validateEmail("userexample.com").shouldBeFailure() + } +}) + +// Step 3: Run tests - verify FAIL +// $ ./gradlew test +// EmailValidatorTest > valid email returns success FAILED +// kotlin.NotImplementedError: An operation is not implemented + +// Step 4: Implement minimal code (GREEN) +fun validateEmail(email: String): Result { + if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank")) + if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @")) + val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$") + if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format")) + return Result.success(email) +} + +// Step 5: Run tests - verify PASS +// $ ./gradlew test +// EmailValidatorTest > valid email returns success PASSED +// EmailValidatorTest > empty email returns failure PASSED +// EmailValidatorTest > email without @ returns failure PASSED + +// Step 6: Refactor if needed, verify tests still pass +``` + +### Kotest 规范样式 + +#### StringSpec(最简单) + +```kotlin +class CalculatorTest : StringSpec({ + "add two positive numbers" { + Calculator.add(2, 3) shouldBe 5 + } + + "add negative numbers" { + Calculator.add(-1, -2) shouldBe -3 + } + + "add zero" { + Calculator.add(0, 5) shouldBe 5 + } +}) +``` + +#### FunSpec(类似 JUnit) + +```kotlin +class UserServiceTest : FunSpec({ + val repository = mockk() + val service = UserService(repository) + + test("getUser returns user when found") { + val expected = User(id = "1", name = "Alice") + coEvery { repository.findById("1") } returns expected + + val result = service.getUser("1") + + result shouldBe expected + } + + test("getUser throws when not found") { + coEvery { repository.findById("999") } returns null + + shouldThrow { + service.getUser("999") + } + } +}) +``` + +#### BehaviorSpec(BDD 风格) + +```kotlin +class OrderServiceTest : BehaviorSpec({ + val repository = mockk() + val paymentService = mockk() + val service = OrderService(repository, paymentService) + + Given("a valid order request") { + val request = CreateOrderRequest( + userId = "user-1", + items = listOf(OrderItem("product-1", quantity = 2)), + ) + + When("the order is placed") { + coEvery { paymentService.charge(any()) } returns PaymentResult.Success + coEvery { repository.save(any()) } answers { firstArg() } + + val result = service.placeOrder(request) + + Then("it should return a confirmed order") { + result.status shouldBe OrderStatus.CONFIRMED + } + + Then("it should charge payment") { + coVerify(exactly = 1) { paymentService.charge(any()) } + } + } + + When("payment fails") { + coEvery { paymentService.charge(any()) } returns PaymentResult.Declined + + Then("it should throw PaymentException") { + shouldThrow { + service.placeOrder(request) + } + } + } + } +}) +``` + +#### DescribeSpec(RSpec 风格) + +```kotlin +class UserValidatorTest : DescribeSpec({ + describe("validateUser") { + val validator = UserValidator() + + context("with valid input") { + it("accepts a normal user") { + val user = CreateUserRequest("Alice", "alice@example.com") + validator.validate(user).shouldBeValid() + } + } + + context("with invalid name") { + it("rejects blank name") { + val user = CreateUserRequest("", "alice@example.com") + validator.validate(user).shouldBeInvalid() + } + + it("rejects name exceeding max length") { + val user = CreateUserRequest("A".repeat(256), "alice@example.com") + validator.validate(user).shouldBeInvalid() + } + } + } +}) +``` + +### Kotest 匹配器 + +#### 核心匹配器 + +```kotlin +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.* +import io.kotest.matchers.collections.* +import io.kotest.matchers.nulls.* + +// Equality +result shouldBe expected +result shouldNotBe unexpected + +// Strings +name shouldStartWith "Al" +name shouldEndWith "ice" +name shouldContain "lic" +name shouldMatch Regex("[A-Z][a-z]+") +name.shouldBeBlank() + +// Collections +list shouldContain "item" +list shouldHaveSize 3 +list.shouldBeSorted() +list.shouldContainAll("a", "b", "c") +list.shouldBeEmpty() + +// Nulls +result.shouldNotBeNull() +result.shouldBeNull() + +// Types +result.shouldBeInstanceOf() + +// Numbers +count shouldBeGreaterThan 0 +price shouldBeInRange 1.0..100.0 + +// Exceptions +shouldThrow { + validateAge(-1) +}.message shouldBe "Age must be positive" + +shouldNotThrow { + validateAge(25) +} +``` + +#### 自定义匹配器 + +```kotlin +fun beActiveUser() = object : Matcher { + override fun test(value: User) = MatcherResult( + value.isActive && value.lastLogin != null, + { "User ${value.id} should be active with a last login" }, + { "User ${value.id} should not be active" }, + ) +} + +// Usage +user should beActiveUser() +``` + +### MockK + +#### 基本模拟 + +```kotlin +class UserServiceTest : FunSpec({ + val repository = mockk() + val logger = mockk(relaxed = true) // Relaxed: returns defaults + val service = UserService(repository, logger) + + beforeTest { + clearMocks(repository, logger) + } + + test("findUser delegates to repository") { + val expected = User(id = "1", name = "Alice") + every { repository.findById("1") } returns expected + + val result = service.findUser("1") + + result shouldBe expected + verify(exactly = 1) { repository.findById("1") } + } + + test("findUser returns null for unknown id") { + every { repository.findById(any()) } returns null + + val result = service.findUser("unknown") + + result.shouldBeNull() + } +}) +``` + +#### 协程模拟 + +```kotlin +class AsyncUserServiceTest : FunSpec({ + val repository = mockk() + val service = UserService(repository) + + test("getUser suspending function") { + coEvery { repository.findById("1") } returns User(id = "1", name = "Alice") + + val result = service.getUser("1") + + result.name shouldBe "Alice" + coVerify { repository.findById("1") } + } + + test("getUser with delay") { + coEvery { repository.findById("1") } coAnswers { + delay(100) // Simulate async work + User(id = "1", name = "Alice") + } + + val result = service.getUser("1") + result.name shouldBe "Alice" + } +}) +``` + +#### 参数捕获 + +```kotlin +test("save captures the user argument") { + val slot = slot() + coEvery { repository.save(capture(slot)) } returns Unit + + service.createUser(CreateUserRequest("Alice", "alice@example.com")) + + slot.captured.name shouldBe "Alice" + slot.captured.email shouldBe "alice@example.com" + slot.captured.id.shouldNotBeNull() +} +``` + +#### 间谍和部分模拟 + +```kotlin +test("spy on real object") { + val realService = UserService(repository) + val spy = spyk(realService) + + every { spy.generateId() } returns "fixed-id" + + spy.createUser(request) + + verify { spy.generateId() } // Overridden + // Other methods use real implementation +} +``` + +### 协程测试 + +#### 用于挂起函数的 runTest + +```kotlin +import kotlinx.coroutines.test.runTest + +class CoroutineServiceTest : FunSpec({ + test("concurrent fetches complete together") { + runTest { + val service = DataService(testScope = this) + + val result = service.fetchAllData() + + result.users.shouldNotBeEmpty() + result.products.shouldNotBeEmpty() + } + } + + test("timeout after delay") { + runTest { + val service = SlowService() + + shouldThrow { + withTimeout(100) { + service.slowOperation() // Takes > 100ms + } + } + } + } +}) +``` + +#### 测试 Flow + +```kotlin +import io.kotest.matchers.collections.shouldContainInOrder +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest + +class FlowServiceTest : FunSpec({ + test("observeUsers emits updates") { + runTest { + val service = UserFlowService() + + val emissions = service.observeUsers() + .take(3) + .toList() + + emissions shouldHaveSize 3 + emissions.last().shouldNotBeEmpty() + } + } + + test("searchUsers debounces input") { + runTest { + val service = SearchService() + val queries = MutableSharedFlow() + + val results = mutableListOf>() + val job = launch { + service.searchUsers(queries).collect { results.add(it) } + } + + queries.emit("a") + queries.emit("ab") + queries.emit("abc") // Only this should trigger search + advanceTimeBy(500) + + results shouldHaveSize 1 + job.cancel() + } + } +}) +``` + +#### TestDispatcher + +```kotlin +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle + +class DispatcherTest : FunSpec({ + test("uses test dispatcher for controlled execution") { + val dispatcher = StandardTestDispatcher() + + runTest(dispatcher) { + var completed = false + + launch { + delay(1000) + completed = true + } + + completed shouldBe false + advanceTimeBy(1000) + completed shouldBe true + } + } +}) +``` + +### 基于属性的测试 + +#### Kotest 属性测试 + +```kotlin +import io.kotest.core.spec.style.FunSpec +import io.kotest.property.Arb +import io.kotest.property.arbitrary.* +import io.kotest.property.forAll +import io.kotest.property.checkAll +import kotlinx.serialization.json.Json +import kotlinx.serialization.encodeToString +import kotlinx.serialization.decodeFromString + +// Note: The serialization roundtrip test below requires the User data class +// to be annotated with @Serializable (from kotlinx.serialization). + +class PropertyTest : FunSpec({ + test("string reverse is involutory") { + forAll { s -> + s.reversed().reversed() == s + } + } + + test("list sort is idempotent") { + forAll(Arb.list(Arb.int())) { list -> + list.sorted() == list.sorted().sorted() + } + } + + test("serialization roundtrip preserves data") { + checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email -> + User(name = name, email = "$email@test.com") + }) { user -> + val json = Json.encodeToString(user) + val decoded = Json.decodeFromString(json) + decoded shouldBe user + } + } +}) +``` + +#### 自定义生成器 + +```kotlin +val userArb: Arb = Arb.bind( + Arb.string(minSize = 1, maxSize = 50), + Arb.email(), + Arb.enum(), +) { name, email, role -> + User( + id = UserId(UUID.randomUUID().toString()), + name = name, + email = Email(email), + role = role, + ) +} + +val moneyArb: Arb = Arb.bind( + Arb.long(1L..1_000_000L), + Arb.enum(), +) { amount, currency -> + Money(amount, currency) +} +``` + +### 数据驱动测试 + +#### Kotest 中的 withData + +```kotlin +class ParserTest : FunSpec({ + context("parsing valid dates") { + withData( + "2026-01-15" to LocalDate(2026, 1, 15), + "2026-12-31" to LocalDate(2026, 12, 31), + "2000-01-01" to LocalDate(2000, 1, 1), + ) { (input, expected) -> + parseDate(input) shouldBe expected + } + } + + context("rejecting invalid dates") { + withData( + nameFn = { "rejects '$it'" }, + "not-a-date", + "2026-13-01", + "2026-00-15", + "", + ) { input -> + shouldThrow { + parseDate(input) + } + } + } +}) +``` + +### 测试生命周期和固件 + +#### BeforeTest / AfterTest + +```kotlin +class DatabaseTest : FunSpec({ + lateinit var db: Database + + beforeSpec { + db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1") + transaction(db) { + SchemaUtils.create(UsersTable) + } + } + + afterSpec { + transaction(db) { + SchemaUtils.drop(UsersTable) + } + } + + beforeTest { + transaction(db) { + UsersTable.deleteAll() + } + } + + test("insert and retrieve user") { + transaction(db) { + UsersTable.insert { + it[name] = "Alice" + it[email] = "alice@example.com" + } + } + + val users = transaction(db) { + UsersTable.selectAll().map { it[UsersTable.name] } + } + + users shouldContain "Alice" + } +}) +``` + +#### Kotest 扩展 + +```kotlin +// Reusable test extension +class DatabaseExtension : BeforeSpecListener, AfterSpecListener { + lateinit var db: Database + + override suspend fun beforeSpec(spec: Spec) { + db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1") + } + + override suspend fun afterSpec(spec: Spec) { + // cleanup + } +} + +class UserRepositoryTest : FunSpec({ + val dbExt = DatabaseExtension() + register(dbExt) + + test("save and find user") { + val repo = UserRepository(dbExt.db) + // ... + } +}) +``` + +### Kover 覆盖率 + +#### Gradle 配置 + +```kotlin +// build.gradle.kts +plugins { + id("org.jetbrains.kotlinx.kover") version "0.9.7" +} + +kover { + reports { + total { + html { onCheck = true } + xml { onCheck = true } + } + filters { + excludes { + classes("*.generated.*", "*.config.*") + } + } + verify { + rule { + minBound(80) // Fail build below 80% coverage + } + } + } +} +``` + +#### 覆盖率命令 + +```bash +# Run tests with coverage +./gradlew koverHtmlReport + +# Verify coverage thresholds +./gradlew koverVerify + +# XML report for CI +./gradlew koverXmlReport + +# View HTML report (use the command for your OS) +# macOS: open build/reports/kover/html/index.html +# Linux: xdg-open build/reports/kover/html/index.html +# Windows: start build/reports/kover/html/index.html +``` + +#### 覆盖率目标 + +| 代码类型 | 目标 | +|-----------|--------| +| 关键业务逻辑 | 100% | +| 公共 API | 90%+ | +| 通用代码 | 80%+ | +| 生成的 / 配置代码 | 排除 | + +### Ktor testApplication 测试 + +```kotlin +class ApiRoutesTest : FunSpec({ + test("GET /users returns list") { + testApplication { + application { + configureRouting() + configureSerialization() + } + + val response = client.get("/users") + + response.status shouldBe HttpStatusCode.OK + val users = response.body>() + users.shouldNotBeEmpty() + } + } + + test("POST /users creates user") { + testApplication { + application { + configureRouting() + configureSerialization() + } + + val response = client.post("/users") { + contentType(ContentType.Application.Json) + setBody(CreateUserRequest("Alice", "alice@example.com")) + } + + response.status shouldBe HttpStatusCode.Created + } + } +}) +``` + +### 测试命令 + +```bash +# Run all tests +./gradlew test + +# Run specific test class +./gradlew test --tests "com.example.UserServiceTest" + +# Run specific test +./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found" + +# Run with verbose output +./gradlew test --info + +# Run with coverage +./gradlew koverHtmlReport + +# Run detekt (static analysis) +./gradlew detekt + +# Run ktlint (formatting check) +./gradlew ktlintCheck + +# Continuous testing +./gradlew test --continuous +``` + +### 最佳实践 + +**应做:** + +* 先写测试(TDD) +* 在整个项目中一致地使用 Kotest 的规范样式 +* 对挂起函数使用 MockK 的 `coEvery`/`coVerify` +* 对协程测试使用 `runTest` +* 测试行为,而非实现 +* 对纯函数使用基于属性的测试 +* 为清晰起见使用 `data class` 测试固件 + +**不应做:** + +* 混合使用测试框架(选择 Kotest 并坚持使用) +* 模拟数据类(使用真实实例) +* 在协程测试中使用 `Thread.sleep()`(改用 `advanceTimeBy`) +* 跳过 TDD 中的红色阶段 +* 直接测试私有函数 +* 忽略不稳定的测试 + +### 与 CI/CD 集成 + +```yaml +# GitHub Actions example +test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Run tests with coverage + run: ./gradlew test koverXmlReport + + - name: Verify coverage + run: ./gradlew koverVerify + + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + files: build/reports/kover/report.xml + token: ${{ secrets.CODECOV_TOKEN }} +``` + +**记住**:测试就是文档。它们展示了你的 Kotlin 代码应如何使用。使用 Kotest 富有表现力的匹配器使测试可读,并使用 MockK 来清晰地模拟依赖项。 diff --git a/docs/zh-CN/skills/logistics-exception-management/SKILL.md b/docs/zh-CN/skills/logistics-exception-management/SKILL.md new file mode 100644 index 00000000..2b528564 --- /dev/null +++ b/docs/zh-CN/skills/logistics-exception-management/SKILL.md @@ -0,0 +1,218 @@ +--- +name: logistics-exception-management +description: 针对货运异常、货物延误、损坏、丢失和承运商纠纷的编码化专业知识,由拥有15年以上运营经验的物流专业人士提供。包括升级协议、承运商特定行为、索赔程序和判断框架。在处理运输异常、货运索赔、交付问题或承运商纠纷时使用。license: Apache-2.0 +version: 1.0.0 +homepage: https://github.com/affaan-m/everything-claude-code +origin: ECC +metadata: + author: evos + clawdbot: + emoji: "📦" +--- + +# 物流异常管理 + +## 角色与背景 + +您是一名拥有15年以上经验的高级货运异常分析师,负责管理所有运输模式(零担、整车、包裹、联运、海运和空运)的运输异常。您处于托运人、承运人、收货人、保险提供商和内部利益相关者的交汇点。您使用的系统包括TMS(运输管理系统)、WMS(仓储管理系统)、承运商门户、理赔管理平台和ERP订单管理系统。您的工作是快速解决异常,同时保护财务利益、维护承运商关系并保持客户满意度。 + +## 使用时机 + +* 货物在交付时出现延误、损坏、丢失或拒收 +* 承运商就责任、附加费或滞留费索赔发生争议 +* 因错过交货窗口或订单错误导致客户升级投诉 +* 向承运商或保险公司提交或管理货运索赔 +* 建立异常处理标准操作程序或升级协议 + +## 运作方式 + +1. 按类型(延误、损坏、丢失、短缺、拒收)和严重程度对异常进行分类 +2. 根据分类和财务风险应用相应的解决流程 +3. 按照承运商特定要求和提交截止日期记录证据 +4. 根据经过的时间和金额阈值,通过既定层级进行升级 +5. 在法定时限内提交索赔,协商和解,并跟踪追偿情况 + +## 示例 + +* **损坏索赔**:500单位的货物到达,其中30%可修复。承运商声称不可抗力。指导证据收集、残值评估、责任判定、索赔提交和谈判策略。 +* **滞留费争议**:承运商对配送中心开具8小时滞留费账单。收货人称司机提前2小时到达。协调GPS数据、预约记录和闸口时间戳以解决争议。 +* **货物丢失**:高价值包裹显示"已送达",但收货人否认收到。启动追踪,配合承运商调查,并在9个月的Carmack时限内提交索赔。 + +## 核心知识 + +### 异常分类 + +每个异常都属于一个分类,该分类决定了解决流程、文件要求和紧急程度: + +* **延误(运输途中)**:货物未在承诺日期前送达。子类型:天气、机械故障、运力(无司机)、海关扣留、收货人改期。最常见的异常类型(约占所有异常的40%)。解决取决于延误是承运商责任还是不可抗力。 +* **损坏(可见)**:在交付时签收单上注明。当收货人在交货回单上记录时,承运商责任明确。立即拍照。切勿接受"司机在我们检查前已离开"。 +* **损坏(隐蔽)**:交付后发现,签收单上未注明。必须在交付后5天内(行业标准,非法定)提交隐蔽损坏索赔。举证责任转移给托运人。承运商会质疑——您需要包装完好性的证据。 +* **损坏(温度)**:冷藏/温控故障。需要连续温度记录仪数据(Sensitech、Emerson)。行程前检查记录至关重要。承运商会声称"产品装货时温度过高"。 +* **短缺**:交付时件数不符。在车尾清点——如果数量不符,切勿签署清洁的提单。区分司机清点与仓库清点的冲突。需要OS\&D(多、短、损)报告。 +* **多货**:交付的产品数量多于提单数量。通常表明来自另一收货人的货物交叉。追踪多余货物——有人会短缺。 +* **拒收**:收货人拒收。原因:损坏、延迟(易腐品窗口)、产品错误、采购订单不匹配、码头调度冲突。如果拒收不是承运商责任,承运商有权收取仓储费和回程运费。 +* **误送**:交付到错误地址或错误收货人。承运商承担全部责任。时间紧迫,需尽快找回——产品会变质或被消耗。 +* **丢失(整票货物)**:未交付,无扫描活动。整车运输在预计到达时间后24小时触发追踪,零担运输在48小时后触发。向承运商OS\&D部门提交正式追踪请求。 +* **丢失(部分)**:货物中部分物品缺失。常发生在零担运输的交叉转运过程中。对于高价值货物,序列号追踪至关重要。 +* **污染**:产品暴露于化学品、异味或不兼容的货物(零担运输中常见)。对食品和药品有监管影响。 + +### 不同运输模式的承运商行为 + +了解不同承运商类型的运作方式会改变您的解决策略: + +* **零担承运商**(FedEx Freight、XPO、Estes):货物经过2-4个中转站。每次中转都存在损坏风险。理赔部门庞大且流程化。预计30-60天解决索赔。中转站经理的权限约为2,500美元。 +* **整车运输**(资产型承运商 + 经纪商):单一司机,码头到码头。损坏通常发生在装卸过程中。经纪商增加了一层复杂性——经纪商的承运商可能失联。务必获取实际承运商的MC号码。 +* **包裹运输**(UPS、FedEx、USPS):自动化索赔门户。文件要求严格。申报价值很重要——默认责任限额很低(UPS为100美元)。必须在发货时购买额外保险。 +* **联运**(铁路 + 短驳运输):多次交接。损坏常发生在铁路运输(撞击事件)或底盘更换过程中。提单链决定了铁路和短驳运输之间的责任分配。 +* **海运**(集装箱运输):受《海牙-维斯比规则》或COGSA(美国)管辖。承运商责任按件计算(COGSA下每件500美元,除非申报价值)。集装箱封条完整性至关重要。在目的港进行检验员检查。 +* **空运**:受《蒙特利尔公约》管辖。损坏通知严格规定为14天,延误为21天。基于重量的责任限额,除非申报价值。是所有运输模式中索赔解决最快的。 + +### 索赔流程基础 + +* **Carmack修正案(美国国内陆路运输)**:除有限例外情况(天灾、公敌行为、托运人行为、公共当局行为、固有缺陷)外,承运商对实际损失或损坏负责。托运人必须证明:货物交付时状况良好,货物到达时损坏/短缺,以及损失金额。 +* **提交截止日期**:美国国内运输为交付日期起9个月(《美国法典》第49编第14706节)。错过此期限,无论索赔是否有理,均因时效而被禁止。 +* **所需文件**:原始提单(显示完好交付)、交货回单(显示异常)、商业发票(证明价值)、检验报告、照片、维修估算或更换报价、包装规格。 +* **承运商回应**:承运商有30天时间确认,120天时间支付或拒赔。如果拒赔,您有自拒赔之日起2年的时间提起诉讼。 + +### 季节性和周期性规律 + +* **旺季(10月-1月)**:异常率增加30-50%。承运商网络紧张。运输时间延长。理赔部门处理速度变慢。在承诺中加入缓冲时间。 +* **农产品季节(4月-9月)**:温度异常激增。冷藏车可用性紧张。预冷合规性变得至关重要。 +* **飓风季节(6月-11月)**:墨西哥湾和东海岸中断。不可抗力索赔增加。需要在风暴路径更新后4-6小时内做出改道决定。 +* **月末/季末**:托运人赶量。承运商拒单率激增。双重经纪增加。整体服务质量下降。 +* **司机短缺周期**:在第四季度和新法规实施后(ELD指令、FMCSA药物清关数据库)最为严重。即期费率飙升,服务水平下降。 + +### 欺诈与危险信号 + +* **伪造损坏**:损坏模式与运输模式不符。同一收货地点多次索赔。 +* **地址操纵**:提货后要求更改地址。高价值电子产品中常见。 +* **系统性短缺**:多批货物持续短缺1-2个单位——表明在中转站或运输途中有盗窃行为。 +* **双重经纪迹象**:提单上的承运商与出现的卡车不符。司机说不出调度员的名字。保险证书来自不同的实体。 + +## 决策框架 + +### 严重程度分类 + +从三个维度评估每个异常,并取最高严重程度: + +**财务影响:** + +* 级别1(低):产品价值 < 1,000美元,无需加急 +* 级别2(中):1,000 - 5,000美元或少量加急费用 +* 级别3(显著):5,000 - 25,000美元或有客户罚款风险 +* 级别4(重大):25,000 - 100,000美元或有合同合规风险 +* 级别5(严重):> 100,000美元或有监管/安全影响 + +**客户影响:** + +* 标准客户,服务水平协议无风险 → 不升级 +* 关键客户,服务水平协议有风险 → 提升1级 +* 企业客户,有惩罚条款 → 提升2级 +* 客户生产线或零售发布面临风险 → 自动提升至4级+ + +**时间敏感性:** + +* 标准运输,有缓冲时间 → 不升级 +* 需在48小时内交付,无替代货源 → 提升1级 +* 当日或次日加急(生产停工、活动截止日期) → 自动提升至4级+ + +### 自行承担成本 vs 争取索赔 + +这是最常见的判断。阈值: + +* **< 500美元且承运商关系良好**:自行承担。索赔处理的管理成本(内部150-250美元)使其投资回报率为负。记录在承运商记分卡中。 +* **500 - 2,500美元**:提交索赔但不积极升级。这是"标准流程"区间。接受价值70%以上的部分和解。 +* **2,500 - 10,000美元**:完整的索赔流程。如果30天后无解决方案,则升级。联系承运商客户经理。拒绝低于80%的和解方案。 +* **> 10,000美元**:引起副总裁级别关注。指定专人处理索赔。如有损坏,进行独立检验。拒绝低于90%的和解方案。如果被拒,进行法律审查。 +* **任何金额 + 模式**:如果这是同一承运商在30天内的第3次以上异常,无论单个金额多少,都将其视为承运商绩效问题。 + +### 优先级排序 + +当多个异常同时发生时(旺季或天气事件期间常见),按以下顺序确定优先级: + +1. 安全/监管(温控药品、危险品)——始终优先 +2. 客户生产停工风险——财务乘数为产品价值的10-50倍 +3. 剩余保质期 < 48小时的易腐品 +4. 根据客户层级调整后的最高财务影响 +5. 最久未解决的异常(防止超出服务水平协议期限) + +## 关键边缘案例 + +这些情况下,显而易见的方法是错误的。此处包含简要摘要,以便您可以根据需要将其扩展为特定项目的应对方案。 + +1. **药品冷藏车故障,温度数据有争议**:承运商显示正确的设定点;您的Sensitech数据显示温度偏离。争议在于传感器放置和预冷。切勿接受承运商的单点读数——要求下载连续数据记录仪数据。 + +2. **收货人声称损坏,但损坏发生在卸货过程中**:签收单签署时清洁,但收货人2小时后致电声称损坏。如果您的司机目睹了他们的叉车掉落托盘,司机的实时记录是您的最佳辩护。如果没有,您很可能面临隐蔽损坏索赔。 + +3. **高价值货物72小时无扫描更新**:无跟踪更新并不总是意味着丢失。零担运输在繁忙的中转站会出现扫描中断。在触发丢失处理流程之前,直接致电始发站和目的站。询问实际的拖车/货位位置。 + +4. **跨境海关扣留**:当货物被海关扣留时,迅速确定扣留是由于文件问题(可修复)还是合规问题(可能无法修复)。承运商文件错误(承运商部分商品编码错误)与托运人错误(商业发票价值不正确)需要不同的解决路径。 + +5. **针对单一提单的部分交付**:多次交付尝试,数量不符。保持动态记录。在所有部分交付对账完毕前,不要提交短缺索赔——承运商会将过早的索赔作为托运人错误的证据。 + +6. **货运代理在运输途中破产:** 您的货物已在卡车上,但安排此运输的货运代理破产了。实际承运人拥有留置权。迅速确定:承运人是否已获付款?如果没有,直接与承运人协商放货。 + +7. **最终客户发现隐藏损坏:** 您将货物交付给分销商,分销商交付给终端客户,终端客户发现损坏。责任链文件决定了谁承担损失。 + +8. **恶劣天气事件期间的旺季附加费争议:** 承运人追溯性地加收紧急附加费。合同可能允许也可能不允许这样做——需特别检查不可抗力和燃油附加费条款。 + +## 沟通模式 + +### 语气调整 + +根据情况的严重性和关系调整沟通语气: + +* **常规异常,与承运人关系良好:** 协作式。"PRO# X 出现延误——您能给我一个更新的预计到达时间吗?客户正在询问。" +* **重大异常,关系中立:** 专业且有记录。陈述事实,引用提单/PRO号,明确您需要什么以及何时需要。 +* **重大异常或模式性问题,关系紧张:** 正式。抄送管理层。引用合同条款。设定回复截止日期。"根据我们日期为...的运输协议第4.2节..." +* **面向客户(延误):** 主动、诚实、以解决方案为导向。切勿点名指责承运人。"您的货物在运输途中出现延误。以下是我们正在采取的措施以及您更新后的时间表。" +* **面向客户(损坏/丢失):** 富有同理心,以行动为导向。以解决方案开头,而非问题。"我们已发现您的货物存在问题,并已立即启动\[更换/赔偿]。" + +### 关键模板 + +以下是简要模板。在投入生产使用前,请根据您的承运人、客户和保险工作流程进行调整。 + +**初次向承运人询问:** 主题:`Exception Notice — PRO# {pro} / BOL# {bol}`。说明:发生了什么情况,您需要什么(更新ETA、检查、OS\&D报告),以及截止时间。 + +**向客户主动更新:** 开头说明:您知道的情况、您正在采取的措施、客户更新后的时间表,以及您直接的联系方式以便客户提问。 + +**向承运人管理层升级问题:** 主题:`ESCALATION: Unresolved Exception — {shipment_ref} — {days} Days`。包括之前沟通的时间线、财务影响,以及您期望的解决方案。 + +## 升级协议 + +### 自动升级触发条件 + +| 触发条件 | 行动 | 时间线 | +|---|---|---| +| 异常价值 > 25,000 美元 | 立即通知供应链副总裁 | 1小时内 | +| 影响企业客户 | 指派专门处理人员,通知客户团队 | 2小时内 | +| 承运人无回应 | 升级至承运人客户经理 | 4小时后 | +| 同一承运人重复异常(30天内3次以上) | 与采购部门进行承运人绩效审查 | 1周内 | +| 潜在的欺诈迹象 | 通知合规部门并暂停标准处理流程 | 立即 | +| 受监管产品出现温度偏差 | 通知质量/法规团队 | 30分钟内 | +| 高价值货物(> 5万美元)无扫描更新 | 启动追踪协议并通知安全部门 | 24小时后 | +| 索赔被拒金额 > 1万美元 | 对拒赔依据进行法律审查 | 48小时内 | + +### 升级链 + +级别 1(分析师)→ 级别 2(团队主管,4小时)→ 级别 3(经理,24小时)→ 级别 4(总监,48小时)→ 级别 5(副总裁,72+小时或任何级别5严重程度) + +## 绩效指标 + +每周跟踪这些指标,每月观察趋势: + +| 指标 | 目标 | 危险信号 | +|---|---|---| +| 平均解决时间 | < 72 小时 | > 120 小时 | +| 首次联系解决率 | > 40% | < 25% | +| 财务追偿率(索赔) | > 75% | < 50% | +| 客户满意度(异常处理后) | > 4.0/5.0 | < 3.5/5.0 | +| 异常率(每1000票货物) | < 25 | > 40 | +| 索赔提交及时性 | 100% 在30天内 | 任何 > 60 天 | +| 重复异常(同一承运人/线路) | < 10% | > 20% | +| 长期未决异常(> 30天未关闭) | < 总数的 5% | > 总数的 15% | + +## 其他资源 + +* 将此技能与您内部的索赔截止日期、特定运输模式的升级矩阵以及保险公司的通知要求结合使用。 +* 将承运人特定的交货证明规则和OS\&D检查清单放在执行本手册的团队附近。 diff --git a/docs/zh-CN/skills/perl-patterns/SKILL.md b/docs/zh-CN/skills/perl-patterns/SKILL.md new file mode 100644 index 00000000..1c769fdb --- /dev/null +++ b/docs/zh-CN/skills/perl-patterns/SKILL.md @@ -0,0 +1,504 @@ +--- +name: perl-patterns +description: 现代 Perl 5.36+ 的惯用法、最佳实践和约定,用于构建稳健、可维护的 Perl 应用程序。 +origin: ECC +--- + +# 现代 Perl 开发模式 + +适用于构建健壮、可维护应用程序的 Perl 5.36+ 惯用模式和最佳实践。 + +## 何时启用 + +* 编写新的 Perl 代码或模块时 +* 审查 Perl 代码是否符合惯用法时 +* 重构遗留 Perl 代码以符合现代标准时 +* 设计 Perl 模块架构时 +* 将 5.36 之前的代码迁移到现代 Perl 时 + +## 工作原理 + +将这些模式作为偏向现代 Perl 5.36+ 默认设置的指南应用:签名、显式模块、聚焦的错误处理和可测试的边界。下面的示例旨在作为起点被复制,然后根据您面前的实际应用程序、依赖栈和部署模型进行调整。 + +## 核心原则 + +### 1. 使用 `v5.36` 编译指令 + +单个 `use v5.36` 即可替代旧的样板代码,并启用严格模式、警告和子程序签名。 + +```perl +# Good: Modern preamble +use v5.36; + +sub greet($name) { + say "Hello, $name!"; +} + +# Bad: Legacy boilerplate +use strict; +use warnings; +use feature 'say', 'signatures'; +no warnings 'experimental::signatures'; + +sub greet { + my ($name) = @_; + say "Hello, $name!"; +} +``` + +### 2. 子程序签名 + +使用签名以提高清晰度和自动参数数量检查。 + +```perl +use v5.36; + +# Good: Signatures with defaults +sub connect_db($host, $port = 5432, $timeout = 30) { + # $host is required, others have defaults + return DBI->connect("dbi:Pg:host=$host;port=$port", undef, undef, { + RaiseError => 1, + PrintError => 0, + }); +} + +# Good: Slurpy parameter for variable args +sub log_message($level, @details) { + say "[$level] " . join(' ', @details); +} + +# Bad: Manual argument unpacking +sub connect_db { + my ($host, $port, $timeout) = @_; + $port //= 5432; + $timeout //= 30; + # ... +} +``` + +### 3. 上下文敏感性 + +理解标量上下文与列表上下文——这是 Perl 的核心概念。 + +```perl +use v5.36; + +my @items = (1, 2, 3, 4, 5); + +my @copy = @items; # List context: all elements +my $count = @items; # Scalar context: count (5) +say "Items: " . scalar @items; # Force scalar context +``` + +### 4. 后缀解引用 + +对嵌套结构使用后缀解引用语法以提高可读性。 + +```perl +use v5.36; + +my $data = { + users => [ + { name => 'Alice', roles => ['admin', 'user'] }, + { name => 'Bob', roles => ['user'] }, + ], +}; + +# Good: Postfix dereferencing +my @users = $data->{users}->@*; +my @roles = $data->{users}[0]{roles}->@*; +my %first = $data->{users}[0]->%*; + +# Bad: Circumfix dereferencing (harder to read in chains) +my @users = @{ $data->{users} }; +my @roles = @{ $data->{users}[0]{roles} }; +``` + +### 5. `isa` 运算符 (5.32+) + +中缀类型检查——替代 `blessed($o) && $o->isa('X')`。 + +```perl +use v5.36; +if ($obj isa 'My::Class') { $obj->do_something } +``` + +## 错误处理 + +### eval/die 模式 + +```perl +use v5.36; + +sub parse_config($path) { + my $content = eval { path($path)->slurp_utf8 }; + die "Config error: $@" if $@; + return decode_json($content); +} +``` + +### Try::Tiny(可靠的异常处理) + +```perl +use v5.36; +use Try::Tiny; + +sub fetch_user($id) { + my $user = try { + $db->resultset('User')->find($id) + // die "User $id not found\n"; + } + catch { + warn "Failed to fetch user $id: $_"; + undef; + }; + return $user; +} +``` + +### 原生 try/catch (5.40+) + +```perl +use v5.40; + +sub divide($x, $y) { + try { + die "Division by zero" if $y == 0; + return $x / $y; + } + catch ($e) { + warn "Error: $e"; + return; + } +} +``` + +## 使用 Moo 的现代 OO + +优先使用 Moo 进行轻量级、现代的面向对象编程。仅当需要 Moose 的元协议时才使用它。 + +```perl +# Good: Moo class +package User; +use Moo; +use Types::Standard qw(Str Int ArrayRef); +use namespace::autoclean; + +has name => (is => 'ro', isa => Str, required => 1); +has email => (is => 'ro', isa => Str, required => 1); +has age => (is => 'ro', isa => Int, default => sub { 0 }); +has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] }); + +sub is_admin($self) { + return grep { $_ eq 'admin' } $self->roles->@*; +} + +sub greet($self) { + return "Hello, I'm " . $self->name; +} + +1; + +# Usage +my $user = User->new( + name => 'Alice', + email => 'alice@example.com', + roles => ['admin', 'user'], +); + +# Bad: Blessed hashref (no validation, no accessors) +package User; +sub new { + my ($class, %args) = @_; + return bless \%args, $class; +} +sub name { return $_[0]->{name} } +1; +``` + +### Moo 角色 + +```perl +package Role::Serializable; +use Moo::Role; +use JSON::MaybeXS qw(encode_json); +requires 'TO_HASH'; +sub to_json($self) { encode_json($self->TO_HASH) } +1; + +package User; +use Moo; +with 'Role::Serializable'; +has name => (is => 'ro', required => 1); +has email => (is => 'ro', required => 1); +sub TO_HASH($self) { { name => $self->name, email => $self->email } } +1; +``` + +### 原生 `class` 关键字 (5.38+, Corinna) + +```perl +use v5.38; +use feature 'class'; +no warnings 'experimental::class'; + +class Point { + field $x :param; + field $y :param; + method magnitude() { sqrt($x**2 + $y**2) } +} + +my $p = Point->new(x => 3, y => 4); +say $p->magnitude; # 5 +``` + +## 正则表达式 + +### 命名捕获和 `/x` 标志 + +```perl +use v5.36; + +# Good: Named captures with /x for readability +my $log_re = qr{ + ^ (? \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} ) + \s+ \[ (? \w+ ) \] + \s+ (? .+ ) $ +}x; + +if ($line =~ $log_re) { + say "Time: $+{timestamp}, Level: $+{level}"; + say "Message: $+{message}"; +} + +# Bad: Positional captures (hard to maintain) +if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) { + say "Time: $1, Level: $2"; +} +``` + +### 预编译模式 + +```perl +use v5.36; + +# Good: Compile once, use many +my $email_re = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; + +sub validate_emails(@emails) { + return grep { $_ =~ $email_re } @emails; +} +``` + +## 数据结构 + +### 引用和安全深度访问 + +```perl +use v5.36; + +# Hash and array references +my $config = { + database => { + host => 'localhost', + port => 5432, + options => ['utf8', 'sslmode=require'], + }, +}; + +# Safe deep access (returns undef if any level missing) +my $port = $config->{database}{port}; # 5432 +my $missing = $config->{cache}{host}; # undef, no error + +# Hash slices +my %subset; +@subset{qw(host port)} = @{$config->{database}}{qw(host port)}; + +# Array slices +my @first_two = $config->{database}{options}->@[0, 1]; + +# Multi-variable for loop (experimental in 5.36, stable in 5.40) +use feature 'for_list'; +no warnings 'experimental::for_list'; +for my ($key, $val) (%$config) { + say "$key => $val"; +} +``` + +## 文件 I/O + +### 三参数 open + +```perl +use v5.36; + +# Good: Three-arg open with autodie (core module, eliminates 'or die') +use autodie; + +sub read_file($path) { + open my $fh, '<:encoding(UTF-8)', $path; + local $/; + my $content = <$fh>; + close $fh; + return $content; +} + +# Bad: Two-arg open (shell injection risk, see perl-security) +open FH, $path; # NEVER do this +open FH, "< $path"; # Still bad — user data in mode string +``` + +### 使用 Path::Tiny 进行文件操作 + +```perl +use v5.36; +use Path::Tiny; + +my $file = path('config', 'app.json'); +my $content = $file->slurp_utf8; +$file->spew_utf8($new_content); + +# Iterate directory +for my $child (path('src')->children(qr/\.pl$/)) { + say $child->basename; +} +``` + +## 模块组织 + +### 标准项目布局 + +```text +MyApp/ +├── lib/ +│ └── MyApp/ +│ ├── App.pm # Main module +│ ├── Config.pm # Configuration +│ ├── DB.pm # Database layer +│ └── Util.pm # Utilities +├── bin/ +│ └── myapp # Entry-point script +├── t/ +│ ├── 00-load.t # Compilation tests +│ ├── unit/ # Unit tests +│ └── integration/ # Integration tests +├── cpanfile # Dependencies +├── Makefile.PL # Build system +└── .perlcriticrc # Linting config +``` + +### 导出器模式 + +```perl +package MyApp::Util; +use v5.36; +use Exporter 'import'; + +our @EXPORT_OK = qw(trim); +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +sub trim($str) { $str =~ s/^\s+|\s+$//gr } + +1; +``` + +## 工具 + +### perltidy 配置 (.perltidyrc) + +```text +-i=4 # 4-space indent +-l=100 # 100-char line length +-ci=4 # continuation indent +-ce # cuddled else +-bar # opening brace on same line +-nolq # don't outdent long quoted strings +``` + +### perlcritic 配置 (.perlcriticrc) + +```ini +severity = 3 +theme = core + pbp + security + +[InputOutput::RequireCheckedSyscalls] +functions = :builtins +exclude_functions = say print + +[Subroutines::ProhibitExplicitReturnUndef] +severity = 4 + +[ValuesAndExpressions::ProhibitMagicNumbers] +allowed_values = 0 1 2 -1 +``` + +### 依赖管理 (cpanfile + carton) + +```bash +cpanm App::cpanminus Carton # Install tools +carton install # Install deps from cpanfile +carton exec -- perl bin/myapp # Run with local deps +``` + +```perl +# cpanfile +requires 'Moo', '>= 2.005'; +requires 'Path::Tiny'; +requires 'JSON::MaybeXS'; +requires 'Try::Tiny'; + +on test => sub { + requires 'Test2::V0'; + requires 'Test::MockModule'; +}; +``` + +## 快速参考:现代 Perl 惯用法 + +| 遗留模式 | 现代替代方案 | +|---|---| +| `use strict; use warnings;` | `use v5.36;` | +| `my ($x, $y) = @_;` | `sub foo($x, $y) { ... }` | +| `@{ $ref }` | `$ref->@*` | +| `%{ $ref }` | `$ref->%*` | +| `open FH, "< $file"` | `open my $fh, '<:encoding(UTF-8)', $file` | +| `blessed hashref` | `Moo` 带类型的类 | +| `$1, $2, $3` | `$+{name}` (命名捕获) | +| `eval { }; if ($@)` | `Try::Tiny` 或原生 `try/catch` (5.40+) | +| `BEGIN { require Exporter; }` | `use Exporter 'import';` | +| 手动文件操作 | `Path::Tiny` | +| `blessed($o) && $o->isa('X')` | `$o isa 'X'` (5.32+) | +| `builtin::true / false` | `use builtin 'true', 'false';` (5.36+, 实验性) | + +## 反模式 + +```perl +# 1. Two-arg open (security risk) +open FH, $filename; # NEVER + +# 2. Indirect object syntax (ambiguous parsing) +my $obj = new Foo(bar => 1); # Bad +my $obj = Foo->new(bar => 1); # Good + +# 3. Excessive reliance on $_ +map { process($_) } grep { validate($_) } @items; # Hard to follow +my @valid = grep { validate($_) } @items; # Better: break it up +my @results = map { process($_) } @valid; + +# 4. Disabling strict refs +no strict 'refs'; # Almost always wrong +${"My::Package::$var"} = $value; # Use a hash instead + +# 5. Global variables as configuration +our $TIMEOUT = 30; # Bad: mutable global +use constant TIMEOUT => 30; # Better: constant +# Best: Moo attribute with default + +# 6. String eval for module loading +eval "require $module"; # Bad: code injection risk +eval "use $module"; # Bad +use Module::Runtime 'require_module'; # Good: safe module loading +require_module($module); +``` + +**记住**:现代 Perl 是简洁、可读且安全的。让 `use v5.36` 处理样板代码,使用 Moo 处理对象,并优先使用 CPAN 上经过实战检验的模块,而不是自己动手的解决方案。 diff --git a/docs/zh-CN/skills/perl-security/SKILL.md b/docs/zh-CN/skills/perl-security/SKILL.md new file mode 100644 index 00000000..c6453345 --- /dev/null +++ b/docs/zh-CN/skills/perl-security/SKILL.md @@ -0,0 +1,503 @@ +--- +name: perl-security +description: 全面的Perl安全指南,涵盖污染模式、输入验证、安全进程执行、DBI参数化查询、Web安全(XSS/SQLi/CSRF)以及perlcritic安全策略。 +origin: ECC +--- + +# Perl 安全模式 + +涵盖输入验证、注入预防和安全编码实践的 Perl 应用程序全面安全指南。 + +## 何时启用 + +* 处理 Perl 应用程序中的用户输入时 +* 构建 Perl Web 应用程序时(CGI、Mojolicious、Dancer2、Catalyst) +* 审查 Perl 代码中的安全漏洞时 +* 使用用户提供的路径执行文件操作时 +* 从 Perl 执行系统命令时 +* 编写 DBI 数据库查询时 + +## 工作原理 + +从污染感知的输入边界开始,然后向外扩展:验证并净化输入,保持文件系统和进程执行受限,并处处使用参数化的 DBI 查询。下面的示例展示了在交付涉及用户输入、shell 或网络的 Perl 代码之前,此技能期望您应用的安全默认做法。 + +## 污染模式 + +Perl 的污染模式(`-T`)跟踪来自外部源的数据,并防止其在未经明确验证的情况下用于不安全操作。 + +### 启用污染模式 + +```perl +#!/usr/bin/perl -T +use v5.36; + +# Tainted: anything from outside the program +my $input = $ARGV[0]; # Tainted +my $env_path = $ENV{PATH}; # Tainted +my $form = ; # Tainted +my $query = $ENV{QUERY_STRING}; # Tainted + +# Sanitize PATH early (required in taint mode) +$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin'; +delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; +``` + +### 净化模式 + +```perl +use v5.36; + +# Good: Validate and untaint with a specific regex +sub untaint_username($input) { + if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) { + return $1; # $1 is untainted + } + die "Invalid username: must be 3-30 alphanumeric characters\n"; +} + +# Good: Validate and untaint a file path +sub untaint_filename($input) { + if ($input =~ m{^([a-zA-Z0-9._-]+)$}) { + return $1; + } + die "Invalid filename: contains unsafe characters\n"; +} + +# Bad: Overly permissive untainting (defeats the purpose) +sub bad_untaint($input) { + $input =~ /^(.*)$/s; + return $1; # Accepts ANYTHING — pointless +} +``` + +## 输入验证 + +### 允许列表优于阻止列表 + +```perl +use v5.36; + +# Good: Allowlist — define exactly what's permitted +sub validate_sort_field($field) { + my %allowed = map { $_ => 1 } qw(name email created_at updated_at); + die "Invalid sort field: $field\n" unless $allowed{$field}; + return $field; +} + +# Good: Validate with specific patterns +sub validate_email($email) { + if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) { + return $1; + } + die "Invalid email address\n"; +} + +sub validate_integer($input) { + if ($input =~ /^(-?\d{1,10})$/) { + return $1 + 0; # Coerce to number + } + die "Invalid integer\n"; +} + +# Bad: Blocklist — always incomplete +sub bad_validate($input) { + die "Invalid" if $input =~ /[<>"';&|]/; # Misses encoded attacks + return $input; +} +``` + +### 长度约束 + +```perl +use v5.36; + +sub validate_comment($text) { + die "Comment is required\n" unless length($text) > 0; + die "Comment exceeds 10000 chars\n" if length($text) > 10_000; + return $text; +} +``` + +## 安全正则表达式 + +### 防止正则表达式拒绝服务 + +嵌套的量词应用于重叠模式时会发生灾难性回溯。 + +```perl +use v5.36; + +# Bad: Vulnerable to ReDoS (exponential backtracking) +my $bad_re = qr/^(a+)+$/; # Nested quantifiers +my $bad_re2 = qr/^([a-zA-Z]+)*$/; # Nested quantifiers on class +my $bad_re3 = qr/^(.*?,){10,}$/; # Repeated greedy/lazy combo + +# Good: Rewrite without nesting +my $good_re = qr/^a+$/; # Single quantifier +my $good_re2 = qr/^[a-zA-Z]+$/; # Single quantifier on class + +# Good: Use possessive quantifiers or atomic groups to prevent backtracking +my $safe_re = qr/^[a-zA-Z]++$/; # Possessive (5.10+) +my $safe_re2 = qr/^(?>a+)$/; # Atomic group + +# Good: Enforce timeout on untrusted patterns +use POSIX qw(alarm); +sub safe_match($string, $pattern, $timeout = 2) { + my $matched; + eval { + local $SIG{ALRM} = sub { die "Regex timeout\n" }; + alarm($timeout); + $matched = $string =~ $pattern; + alarm(0); + }; + alarm(0); + die $@ if $@; + return $matched; +} +``` + +## 安全的文件操作 + +### 三参数 Open + +```perl +use v5.36; + +# Good: Three-arg open, lexical filehandle, check return +sub read_file($path) { + open my $fh, '<:encoding(UTF-8)', $path + or die "Cannot open '$path': $!\n"; + local $/; + my $content = <$fh>; + close $fh; + return $content; +} + +# Bad: Two-arg open with user data (command injection) +sub bad_read($path) { + open my $fh, $path; # If $path = "|rm -rf /", runs command! + open my $fh, "< $path"; # Shell metacharacter injection +} +``` + +### 防止检查时使用时间和路径遍历 + +```perl +use v5.36; +use Fcntl qw(:DEFAULT :flock); +use File::Spec; +use Cwd qw(realpath); + +# Atomic file creation +sub create_file_safe($path) { + sysopen(my $fh, $path, O_WRONLY | O_CREAT | O_EXCL, 0600) + or die "Cannot create '$path': $!\n"; + return $fh; +} + +# Validate path stays within allowed directory +sub safe_path($base_dir, $user_path) { + my $real = realpath(File::Spec->catfile($base_dir, $user_path)) + // die "Path does not exist\n"; + my $base_real = realpath($base_dir) + // die "Base dir does not exist\n"; + die "Path traversal blocked\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/; + return $real; +} +``` + +使用 `File::Temp` 处理临时文件(`tempfile(UNLINK => 1)`),并使用 `flock(LOCK_EX)` 防止竞态条件。 + +## 安全的进程执行 + +### 列表形式的 system 和 exec + +```perl +use v5.36; + +# Good: List form — no shell interpolation +sub run_command(@cmd) { + system(@cmd) == 0 + or die "Command failed: @cmd\n"; +} + +run_command('grep', '-r', $user_pattern, '/var/log/app/'); + +# Good: Capture output safely with IPC::Run3 +use IPC::Run3; +sub capture_output(@cmd) { + my ($stdout, $stderr); + run3(\@cmd, \undef, \$stdout, \$stderr); + if ($?) { + die "Command failed (exit $?): $stderr\n"; + } + return $stdout; +} + +# Bad: String form — shell injection! +sub bad_search($pattern) { + system("grep -r '$pattern' /var/log/app/"); # If $pattern = "'; rm -rf / #" +} + +# Bad: Backticks with interpolation +my $output = `ls $user_dir`; # Shell injection risk +``` + +也可以使用 `Capture::Tiny` 安全地捕获外部命令的标准输出和标准错误。 + +## SQL 注入预防 + +### DBI 占位符 + +```perl +use v5.36; +use DBI; + +my $dbh = DBI->connect($dsn, $user, $pass, { + RaiseError => 1, + PrintError => 0, + AutoCommit => 1, +}); + +# Good: Parameterized queries — always use placeholders +sub find_user($dbh, $email) { + my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?'); + $sth->execute($email); + return $sth->fetchrow_hashref; +} + +sub search_users($dbh, $name, $status) { + my $sth = $dbh->prepare( + 'SELECT * FROM users WHERE name LIKE ? AND status = ? ORDER BY name' + ); + $sth->execute("%$name%", $status); + return $sth->fetchall_arrayref({}); +} + +# Bad: String interpolation in SQL (SQLi vulnerability!) +sub bad_find($dbh, $email) { + my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'"); + # If $email = "' OR 1=1 --", returns all users + $sth->execute; + return $sth->fetchrow_hashref; +} +``` + +### 动态列允许列表 + +```perl +use v5.36; + +# Good: Validate column names against an allowlist +sub order_by($dbh, $column, $direction) { + my %allowed_cols = map { $_ => 1 } qw(name email created_at); + my %allowed_dirs = map { $_ => 1 } qw(ASC DESC); + + die "Invalid column: $column\n" unless $allowed_cols{$column}; + die "Invalid direction: $direction\n" unless $allowed_dirs{uc $direction}; + + my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction"); + $sth->execute; + return $sth->fetchall_arrayref({}); +} + +# Bad: Directly interpolating user-chosen column +sub bad_order($dbh, $column) { + $dbh->prepare("SELECT * FROM users ORDER BY $column"); # SQLi! +} +``` + +### DBIx::Class(ORM 安全性) + +```perl +use v5.36; + +# DBIx::Class generates safe parameterized queries +my @users = $schema->resultset('User')->search({ + status => 'active', + email => { -like => '%@example.com' }, +}, { + order_by => { -asc => 'name' }, + rows => 50, +}); +``` + +## Web 安全 + +### XSS 预防 + +```perl +use v5.36; +use HTML::Entities qw(encode_entities); +use URI::Escape qw(uri_escape_utf8); + +# Good: Encode output for HTML context +sub safe_html($user_input) { + return encode_entities($user_input); +} + +# Good: Encode for URL context +sub safe_url_param($value) { + return uri_escape_utf8($value); +} + +# Good: Encode for JSON context +use JSON::MaybeXS qw(encode_json); +sub safe_json($data) { + return encode_json($data); # Handles escaping +} + +# Template auto-escaping (Mojolicious) +# <%= $user_input %> — auto-escaped (safe) +# <%== $raw_html %> — raw output (dangerous, use only for trusted content) + +# Template auto-escaping (Template Toolkit) +# [% user_input | html %] — explicit HTML encoding + +# Bad: Raw output in HTML +sub bad_html($input) { + print "
$input
"; # XSS if $input contains