mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Merge pull request #428 from zdocapp/zh-CN-pr
docs(zh-CN): sync Chinese docs with latest upstream changes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -71,14 +71,13 @@
|
||||
|
||||
## 归属
|
||||
|
||||
本《行为准则》改编自 [Contributor Covenant][homepage],
|
||||
版本 2.0,可在
|
||||
本行为准则改编自 \[贡献者公约]\[homepage] 2.0 版本,可访问
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html> 获取。
|
||||
|
||||
社区影响指南的灵感来源于 [Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)。
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
有关本行为准则常见问题的解答,请参阅常见问题解答:
|
||||
<https://www.contributor-covenant.org/faq>。翻译版本可在
|
||||
<https://www.contributor-covenant.org/translations> 获取。
|
||||
关于本行为准则的常见问题解答,请参阅 FAQ 页面:
|
||||
<https://www.contributor-covenant.org/faq>。其他语言翻译版本可在
|
||||
<https://www.contributor-covenant.org/translations> 查阅。
|
||||
|
||||
@@ -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 标题格式
|
||||
|
||||
@@ -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 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> **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)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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 公开。对
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<a href="https://x.com/affaanmustafa/status/2012378465664745795">
|
||||
<img src="https://github.com/user-attachments/assets/1a471488-59cc-425b-8345-5245c7efbcef" alt="The Shorthand Guide to Everything Claude Code" />
|
||||
<img src="https://github.com/user-attachments/assets/1a471488-59cc-425b-8345-5245c7efbcef" alt="Claude Code 的速记指南/>
|
||||
</a>
|
||||
</td>
|
||||
<td width="50%">
|
||||
<a href="https://x.com/affaanmustafa/status/2014040193557471352">
|
||||
<img src="https://github.com/user-attachments/assets/c9ca43bc-b149-427f-b551-af6840c368f0" alt="The Longform Guide to Everything Claude Code" />
|
||||
<img src="https://github.com/user-attachments/assets/c9ca43bc-b149-427f-b551-af6840c368f0" alt="Claude Code 的详细指南" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>Shorthand Guide</b><br/>Setup, foundations, philosophy. <b>Read this first.</b></td>
|
||||
<td align="center"><b>Longform Guide</b><br/>Token optimization, memory persistence, evals, parallelization.</td>
|
||||
<td align="center"><b>Shorthand Guide</b><br/>设置、基础、理念。 <b>先阅读此部分。</b></td>
|
||||
<td align="center"><b>详细指南</b><br/>令牌优化、记忆持久化、评估、并行化。</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -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/
|
||||
## ❓ 常见问题
|
||||
|
||||
<details>
|
||||
<summary><b>How do I check which agents/commands are installed?</b></summary>
|
||||
<summary><b>如何检查已安装的代理/命令?</b></summary>
|
||||
|
||||
```bash
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
@@ -759,14 +752,40 @@ rules/
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>My hooks aren't working / I see "Duplicate hooks file" errors</b></summary>
|
||||
<summary><b>我的钩子不工作 / 我看到“重复钩子文件”错误</b></summary>
|
||||
|
||||
这是最常见的问题。**不要在 `.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)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>My context window is shrinking / Claude is running out of context</b></summary>
|
||||
<summary><b>我能否在自定义API端点或模型网关上使用ECC与Claude Code?</b></summary>
|
||||
|
||||
是的。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)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>我的上下文窗口正在缩小 / Claude 即将耗尽上下文</b></summary>
|
||||
|
||||
太多的 MCP 服务器会消耗你的上下文。每个 MCP 工具描述都会消耗你 200k 窗口的令牌,可能将其减少到约 70k。
|
||||
|
||||
@@ -784,7 +803,7 @@ rules/
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Can I use only some components (e.g., just agents)?</b></summary>
|
||||
<summary><b>我可以只使用某些组件(例如,仅代理)吗?</b></summary>
|
||||
|
||||
是的。使用选项 2(手动安装)并仅复制你需要的部分:
|
||||
|
||||
@@ -801,7 +820,7 @@ cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Does this work with Cursor / OpenCode / Codex / Antigravity?</b></summary>
|
||||
<summary><b>这能与 Cursor / OpenCode / Codex / Antigravity 一起使用吗?</b></summary>
|
||||
|
||||
是的。ECC 是跨平台的:
|
||||
|
||||
@@ -814,7 +833,7 @@ cp -r everything-claude-code/rules/common/* ~/.claude/rules/
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>How do I contribute a new skill or agent?</b></summary>
|
||||
<summary><b>我如何贡献新技能或代理?</b></summary>
|
||||
|
||||
参见 [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.<name>]` 下定义角色
|
||||
* 将每个角色指向 `.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` 覆盖以及沙箱权限来弥补
|
||||
|
||||
***
|
||||
|
||||
|
||||
446
docs/zh-CN/TROUBLESHOOTING.md
Normal file
446
docs/zh-CN/TROUBLESHOOTING.md
Normal file
@@ -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/<project-hash>/observations.jsonl
|
||||
|
||||
# Back up a corrupted observations file before recreating it
|
||||
mv ~/.claude/homunculus/projects/<project-hash>/observations.jsonl \
|
||||
~/.claude/homunculus/projects/<project-hash>/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) - 使用示例
|
||||
119
docs/zh-CN/agents/kotlin-build-resolver.md
Normal file
119
docs/zh-CN/agents/kotlin-build-resolver.md
Normal file
@@ -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 <name> --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`。
|
||||
161
docs/zh-CN/agents/kotlin-reviewer.md
Normal file
161
docs/zh-CN/agents/kotlin-reviewer.md
Normal file
@@ -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.
|
||||
```
|
||||
|
||||
## 批准标准
|
||||
|
||||
* **批准**:没有**严重**或**高**级别问题
|
||||
* **阻止**:存在任何**严重**或**高**级别问题 —— 必须在合并前修复
|
||||
173
docs/zh-CN/commands/aside.md
Normal file
173
docs/zh-CN/commands/aside.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
description: 在不打断或丢失当前任务上下文的情况下,快速回答一个附带问题。回答后自动恢复工作。
|
||||
---
|
||||
|
||||
# 旁述指令
|
||||
|
||||
在任务进行中提问,获得即时、聚焦的回答——然后立即从暂停处继续。当前任务、文件和上下文绝不会被修改。
|
||||
|
||||
## 何时使用
|
||||
|
||||
* 你在 Claude 工作时对某事感到好奇,但又不想打断工作节奏
|
||||
* 你需要快速解释 Claude 当前正在编辑的代码
|
||||
* 你想就某个决定征求第二意见或进行澄清,而不会使任务偏离方向
|
||||
* 在 Claude 继续之前,你需要理解一个错误、概念或模式
|
||||
* 你想询问与当前任务无关的事情,而无需开启新会话
|
||||
|
||||
## 使用方法
|
||||
|
||||
```
|
||||
/aside <your question>
|
||||
/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?
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 注意事项
|
||||
|
||||
* 在旁述期间**绝不**修改文件——仅限只读访问
|
||||
* 旁述是对话暂停,不是新任务——必须始终恢复原始任务
|
||||
* 保持回答聚焦:目标是快速为用户扫清障碍,而不是进行长篇大论
|
||||
* 如果旁述引发了更广泛的讨论,请先完成当前任务,除非旁述揭示了阻碍
|
||||
* 除非明确与任务结果相关,否则旁述内容不会保存到会话文件中
|
||||
@@ -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`
|
||||
|
||||
## 快速命令
|
||||
|
||||
|
||||
72
docs/zh-CN/commands/gradle-build.md
Normal file
72
docs/zh-CN/commands/gradle-build.md
Normal file
@@ -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` 重新生成 |
|
||||
| 配置缓存问题 | 检查是否存在不可序列化的任务输入 |
|
||||
176
docs/zh-CN/commands/kotlin-build.md
Normal file
176
docs/zh-CN/commands/kotlin-build.md
Normal file
@@ -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/`
|
||||
144
docs/zh-CN/commands/kotlin-review.md
Normal file
144
docs/zh-CN/commands/kotlin-review.md
Normal file
@@ -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/`
|
||||
315
docs/zh-CN/commands/kotlin-test.md
Normal file
315
docs/zh-CN/commands/kotlin-test.md
Normal file
@@ -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<String>) : 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<ValidationResult.Valid>()
|
||||
}
|
||||
|
||||
test("blank name returns Invalid") {
|
||||
val request = RegistrationRequest(
|
||||
name = "",
|
||||
email = "alice@example.com",
|
||||
password = "SecureP@ss1",
|
||||
)
|
||||
|
||||
val result = validateRegistration(request)
|
||||
|
||||
val invalid = result.shouldBeInstanceOf<ValidationResult.Invalid>()
|
||||
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<ValidationResult.Invalid>()
|
||||
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<ValidationResult.Invalid>()
|
||||
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<ValidationResult.Invalid>()
|
||||
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/`
|
||||
@@ -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 中断等)
|
||||
* 专注于能在未来会话中节省时间的模式
|
||||
* 保持技能聚焦 — 每个技能一个模式
|
||||
* 如果覆盖率评分低,在保存前添加相关变体
|
||||
* 专注于那些将在未来会话中节省时间的模式
|
||||
* 保持技能聚焦 —— 每个技能一个模式
|
||||
* 当裁决为“吸收”时,追加到现有技能,而不是创建新文件
|
||||
|
||||
@@ -104,6 +104,14 @@ TaskOutput({ task_id: "<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
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 执行工作流程
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -112,4 +112,7 @@ Agent (planner):
|
||||
|
||||
## 相关代理
|
||||
|
||||
此命令调用位于 `~/.claude/agents/planner.md` 的 `planner` 代理。
|
||||
此命令调用由 ECC 提供的 `planner` 代理。
|
||||
|
||||
对于手动安装,源文件位于:
|
||||
`agents/planner.md`
|
||||
|
||||
37
docs/zh-CN/commands/prompt-optimize.md
Normal file
37
docs/zh-CN/commands/prompt-optimize.md
Normal file
@@ -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
|
||||
@@ -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 版本。
|
||||
|
||||
155
docs/zh-CN/commands/resume-session.md
Normal file
155
docs/zh-CN/commands/resume-session.md
Normal file
@@ -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-<shortid>-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`,以创建一个新的带日期文件
|
||||
252
docs/zh-CN/commands/save-session.md
Normal file
252
docs/zh-CN/commands/save-session.md
Normal file
@@ -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-<short-id>-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-<short-id>-session.tmp`)
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
```
|
||||
|
||||
@@ -85,6 +85,13 @@
|
||||
* 最低 80% 覆盖率
|
||||
* 关键流程使用单元测试 + 集成测试 + E2E 测试
|
||||
|
||||
### 知识捕获
|
||||
|
||||
* 个人调试笔记、偏好和临时上下文 → 自动记忆
|
||||
* 团队/项目知识(架构决策、API变更、实施操作手册) → 遵循项目现有的文档结构
|
||||
* 如果当前任务已生成相关文档、注释或示例,请勿在其他地方重复记录相同知识
|
||||
* 如果没有明显的项目文档位置,请在创建新的顶层文档前进行询问
|
||||
|
||||
***
|
||||
|
||||
## 编辑器集成
|
||||
|
||||
@@ -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 钩子
|
||||
|
||||
|
||||
@@ -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/` 等会在语言习惯不同时覆盖这些默认值。
|
||||
|
||||
### 示例
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
90
docs/zh-CN/rules/kotlin/coding-style.md
Normal file
90
docs/zh-CN/rules/kotlin/coding-style.md
Normal file
@@ -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<out T> {
|
||||
data object Loading : UiState<Nothing>
|
||||
data class Success<T>(val data: T) : UiState<T>
|
||||
data class Error(val message: String) : UiState<Nothing>
|
||||
}
|
||||
```
|
||||
|
||||
对密封类型始终使用详尽的 `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<T>` 或自定义密封类型
|
||||
* 使用 `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)
|
||||
```
|
||||
18
docs/zh-CN/rules/kotlin/hooks.md
Normal file
18
docs/zh-CN/rules/kotlin/hooks.md
Normal file
@@ -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**: 在更改后验证编译
|
||||
147
docs/zh-CN/rules/kotlin/patterns.md
Normal file
147
docs/zh-CN/rules/kotlin/patterns.md
Normal file
@@ -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<ItemRepository> { 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<Item> = 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<T>` 或自定义错误类型
|
||||
* 对于响应式流使用 `Flow`
|
||||
* 协调本地和远程数据源
|
||||
|
||||
```kotlin
|
||||
interface ItemRepository {
|
||||
suspend fun getById(id: String): Result<Item>
|
||||
suspend fun getAll(): Result<List<Item>>
|
||||
fun observeAll(): Flow<List<Item>>
|
||||
}
|
||||
```
|
||||
|
||||
## 用例模式
|
||||
|
||||
单一职责,`operator fun invoke`:
|
||||
|
||||
```kotlin
|
||||
class GetItemUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(id: String): Result<Item> {
|
||||
return repository.getById(id)
|
||||
}
|
||||
}
|
||||
|
||||
class GetItemsUseCase(private val repository: ItemRepository) {
|
||||
suspend operator fun invoke(): Result<List<Item>> {
|
||||
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<Interceptor>()
|
||||
|
||||
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`。
|
||||
83
docs/zh-CN/rules/kotlin/security.md
Normal file
83
docs/zh-CN/rules/kotlin/security.md
Normal file
@@ -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
|
||||
<!-- res/xml/network_security_config.xml -->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
</network-security-config>
|
||||
```
|
||||
|
||||
## 输入验证
|
||||
|
||||
* 在处理或将用户输入发送到 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<ItemEntity>
|
||||
```
|
||||
|
||||
## 数据保护
|
||||
|
||||
* 在 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()` 来控制导航
|
||||
129
docs/zh-CN/rules/kotlin/testing.md
Normal file
129
docs/zh-CN/rules/kotlin/testing.md
Normal file
@@ -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<Item>()
|
||||
var fetchError: Throwable? = null
|
||||
|
||||
override suspend fun getAll(): Result<List<Item>> {
|
||||
fetchError?.let { return Result.failure(it) }
|
||||
return Result.success(items.toList())
|
||||
}
|
||||
|
||||
override fun observeAll(): Flow<List<Item>> = 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。
|
||||
47
docs/zh-CN/rules/perl/coding-style.md
Normal file
47
docs/zh-CN/rules/perl/coding-style.md
Normal file
@@ -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 惯用法和最佳实践。
|
||||
23
docs/zh-CN/rules/perl/hooks.md
Normal file
23
docs/zh-CN/rules/perl/hooks.md
Normal file
@@ -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`)
|
||||
77
docs/zh-CN/rules/perl/patterns.md
Normal file
77
docs/zh-CN/rules/perl/patterns.md
Normal file
@@ -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 模式和惯用法。
|
||||
70
docs/zh-CN/rules/perl/security.md
Normal file
70
docs/zh-CN/rules/perl/security.md
Normal file
@@ -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`。
|
||||
55
docs/zh-CN/rules/perl/testing.md
Normal file
55
docs/zh-CN/rules/perl/testing.md
Normal file
@@ -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`。
|
||||
36
docs/zh-CN/rules/php/coding-style.md
Normal file
36
docs/zh-CN/rules/php/coding-style.md
Normal file
@@ -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`。
|
||||
25
docs/zh-CN/rules/php/hooks.md
Normal file
25
docs/zh-CN/rules/php/hooks.md
Normal file
@@ -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/会话保护时发出警告。
|
||||
33
docs/zh-CN/rules/php/patterns.md
Normal file
33
docs/zh-CN/rules/php/patterns.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
paths:
|
||||
- "**/*.php"
|
||||
- "**/composer.json"
|
||||
---
|
||||
|
||||
# PHP 设计模式
|
||||
|
||||
> 本文档在 [common/patterns.md](../common/patterns.md) 的基础上,补充了 PHP 相关的内容。
|
||||
|
||||
## 精炼控制器,明确服务
|
||||
|
||||
* 保持控制器专注于传输层:认证、验证、序列化、状态码。
|
||||
* 将业务规则移至应用/领域服务中,这些服务无需 HTTP 引导即可轻松测试。
|
||||
|
||||
## DTO 与值对象
|
||||
|
||||
* 对于请求、命令和外部 API 负载,用 DTO 替代结构复杂的关联数组。
|
||||
* 对于货币、标识符、日期范围和其他受约束的概念,使用值对象。
|
||||
|
||||
## 依赖注入
|
||||
|
||||
* 依赖于接口或精简的服务契约,而非框架全局变量。
|
||||
* 通过构造函数传递协作者,这样服务就无需依赖服务定位器查找,易于测试。
|
||||
|
||||
## 边界
|
||||
|
||||
* 当模型层职责超出持久化时,应将 ORM 模型与领域决策隔离。
|
||||
* 将第三方 SDK 封装在小型的适配器之后,使代码库的其余部分依赖于你的契约,而非它们的。
|
||||
|
||||
## 参考
|
||||
|
||||
关于端点约定和响应格式的指导,请参见技能:`api-design`。
|
||||
34
docs/zh-CN/rules/php/security.md
Normal file
34
docs/zh-CN/rules/php/security.md
Normal file
@@ -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 保护。
|
||||
35
docs/zh-CN/rules/php/testing.md
Normal file
35
docs/zh-CN/rules/php/testing.md
Normal file
@@ -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`。
|
||||
@@ -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 <button onClick={() => onSelect(user.id)}>{user.email}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### 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<User>, 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<User>
|
||||
|
||||
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<User> {
|
||||
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<typeof userSchema>
|
||||
|
||||
const validated: UserInput = userSchema.parse(input)
|
||||
```
|
||||
|
||||
## Console.log
|
||||
|
||||
339
docs/zh-CN/skills/android-clean-architecture/SKILL.md
Normal file
339
docs/zh-CN/skills/android-clean-architecture/SKILL.md
Normal file
@@ -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<List<Item>> {
|
||||
return repository.getItemsByCategory(category)
|
||||
}
|
||||
}
|
||||
|
||||
// Flow-based UseCase for reactive streams
|
||||
class ObserveUserProgressUseCase(
|
||||
private val repository: UserRepository
|
||||
) {
|
||||
operator fun invoke(userId: String): Flow<UserProgress> {
|
||||
return repository.observeProgress(userId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 领域模型
|
||||
|
||||
领域模型是普通的 Kotlin 数据类——没有框架注解:
|
||||
|
||||
```kotlin
|
||||
data class Item(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val tags: List<String>,
|
||||
val status: Status,
|
||||
val category: String
|
||||
)
|
||||
|
||||
enum class Status { DRAFT, ACTIVE, ARCHIVED }
|
||||
```
|
||||
|
||||
### 仓库接口
|
||||
|
||||
在领域层定义,在数据层实现:
|
||||
|
||||
```kotlin
|
||||
interface ItemRepository {
|
||||
suspend fun getItemsByCategory(category: String): Result<List<Item>>
|
||||
suspend fun saveItem(item: Item): Result<Unit>
|
||||
fun observeItems(): Flow<List<Item>>
|
||||
}
|
||||
```
|
||||
|
||||
## 数据层
|
||||
|
||||
### 仓库实现
|
||||
|
||||
协调本地和远程数据源:
|
||||
|
||||
```kotlin
|
||||
class ItemRepositoryImpl(
|
||||
private val localDataSource: ItemLocalDataSource,
|
||||
private val remoteDataSource: ItemRemoteDataSource
|
||||
) : ItemRepository {
|
||||
|
||||
override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
|
||||
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<Unit> {
|
||||
return runCatching {
|
||||
localDataSource.insertItems(listOf(item.toEntity()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeItems(): Flow<List<Item>> {
|
||||
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<ItemEntity>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(items: List<ItemEntity>)
|
||||
|
||||
@Query("SELECT * FROM items")
|
||||
fun observeAll(): Flow<List<ItemEntity>>
|
||||
}
|
||||
```
|
||||
|
||||
### 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<ItemDto> {
|
||||
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<ItemRepository> { 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<T>` 或自定义密封类型进行错误传播:
|
||||
|
||||
```kotlin
|
||||
sealed interface Try<out T> {
|
||||
data class Success<T>(val value: T) : Try<T>
|
||||
data class Failure(val error: AppError) : Try<Nothing>
|
||||
}
|
||||
|
||||
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` 了解异步模式。
|
||||
96
docs/zh-CN/skills/blueprint/SKILL.md
Normal file
96
docs/zh-CN/skills/blueprint/SKILL.md
Normal file
@@ -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 <reviewed-full-sha> # 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 — 上游项目和参考设计。
|
||||
199
docs/zh-CN/skills/carrier-relationship-management/SKILL.md
Normal file
199
docs/zh-CN/skills/carrier-relationship-management/SKILL.md
Normal file
@@ -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 周 |
|
||||
|
||||
## 其他资源
|
||||
|
||||
* 在同一运营审查中跟踪承运人记分卡、异常趋势和路由指南合规情况,以便定价和服务决策保持关联。
|
||||
* 在将此技能用于生产环境之前,请先记录您组织偏好的谈判立场、附加费护栏和升级触发条件。
|
||||
337
docs/zh-CN/skills/claude-api/SKILL.md
Normal file
337
docs/zh-CN/skills/claude-api/SKILL.md
Normal file
@@ -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 密钥。始终使用环境变量。
|
||||
299
docs/zh-CN/skills/compose-multiplatform-patterns/SKILL.md
Normal file
299
docs/zh-CN/skills/compose-multiplatform-patterns/SKILL.md
Normal file
@@ -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<Item> = 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<ItemListState> = _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<HomeRoute> {
|
||||
HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) })
|
||||
}
|
||||
composable<DetailRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<DetailRoute>()
|
||||
DetailScreen(id = route.id)
|
||||
}
|
||||
composable<SettingsRoute> { SettingsScreen() }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 对话框和底部抽屉导航
|
||||
|
||||
使用 `dialog()` 和覆盖层模式,而非命令式的显示/隐藏:
|
||||
|
||||
```kotlin
|
||||
NavHost(navController, startDestination = HomeRoute) {
|
||||
composable<HomeRoute> { /* ... */ }
|
||||
dialog<ConfirmDeleteRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<ConfirmDeleteRoute>()
|
||||
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 模式。
|
||||
@@ -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:报告问题
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
209
docs/zh-CN/skills/crosspost/SKILL.md
Normal file
209
docs/zh-CN/skills/crosspost/SKILL.md
Normal file
@@ -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 集成
|
||||
256
docs/zh-CN/skills/customs-trade-compliance/SKILL.md
Normal file
256
docs/zh-CN/skills/customs-trade-compliance/SKILL.md
Normal file
@@ -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 归类日志、报关代理升级矩阵以及一份列有您团队拥有非居民进口商或外贸区覆盖权限的司法管辖区清单结合使用。
|
||||
* 记录贵组织用于美国、欧盟和亚太航线的估价假设,以确保各团队间的关税计算保持一致。
|
||||
163
docs/zh-CN/skills/deep-research/SKILL.md
Normal file
163
docs/zh-CN/skills/deep-research/SKILL.md
Normal file
@@ -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: "<sub-question keywords>", limit: 8)
|
||||
```
|
||||
|
||||
**使用 exa:**
|
||||
|
||||
```
|
||||
web_search_exa(query: "<sub-question keywords>", numResults: 8)
|
||||
web_search_advanced_exa(query: "<keywords>", numResults: 5, startPublishedDate: "2025-01-01")
|
||||
```
|
||||
|
||||
**搜索策略:**
|
||||
|
||||
* 每个子问题使用 2-3 个不同的关键词变体
|
||||
* 混合使用通用查询和新闻聚焦查询
|
||||
* 目标总共获取 15-30 个独特的来源
|
||||
* 优先级:学术、官方、知名新闻 > 博客 > 论坛
|
||||
|
||||
### 步骤 4:深度阅读关键来源
|
||||
|
||||
对于最有希望的 URL,获取完整内容:
|
||||
|
||||
**使用 firecrawl:**
|
||||
|
||||
```
|
||||
firecrawl_scrape(url: "<url>")
|
||||
```
|
||||
|
||||
**使用 exa:**
|
||||
|
||||
```
|
||||
crawling_exa(url: "<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"
|
||||
```
|
||||
193
docs/zh-CN/skills/dmux-workflows/SKILL.md
Normal file
193
docs/zh-CN/skills/dmux-workflows/SKILL.md
Normal file
@@ -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/<session>/` 下写入每个工作器的 `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 <session>:0.<pane-index>` 检查它。
|
||||
* **合并冲突:** 使用 git worktree 隔离每个窗格的文件更改。
|
||||
* **令牌使用量高:** 减少并行窗格数量。每个窗格都是一个完整的代理会话。
|
||||
* **未找到 tmux:** 使用 `brew install tmux` (macOS) 或 `apt install tmux` (Linux) 安装。
|
||||
220
docs/zh-CN/skills/energy-procurement/SKILL.md
Normal file
220
docs/zh-CN/skills/energy-procurement/SKILL.md
Normal file
@@ -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% |
|
||||
|
||||
## 其他资源
|
||||
|
||||
* 在本技能之外,还需维护经批准的内部对冲政策、交易对手名单和费率变更日历。
|
||||
* 将特定设施的负荷曲线和公用事业合同元数据保持在规划工作流附近,以确保建议基于实际需求模式。
|
||||
186
docs/zh-CN/skills/exa-search/SKILL.md
Normal file
186
docs/zh-CN/skills/exa-search/SKILL.md
Normal file
@@ -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: "<id from start>")
|
||||
```
|
||||
|
||||
## 使用模式
|
||||
|
||||
### 快速查找
|
||||
|
||||
```
|
||||
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: "<id>")
|
||||
```
|
||||
|
||||
## 提示
|
||||
|
||||
* 使用 `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` — 带有决策框架的业务导向研究
|
||||
296
docs/zh-CN/skills/fal-ai-media/SKILL.md
Normal file
296
docs/zh-CN/skills/fal-ai-media/SKILL.md
Normal file
@@ -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": "<uploaded_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": "<uploaded_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": "<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/<voice_id>",
|
||||
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` — 社交媒体平台内容创作
|
||||
233
docs/zh-CN/skills/inventory-demand-planning/SKILL.md
Normal file
233
docs/zh-CN/skills/inventory-demand-planning/SKILL.md
Normal file
@@ -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 细分模型、服务水平政策和规划师覆盖审计日志结合使用。
|
||||
* 将促销失误、供应商延迟和预测覆盖的事后分析存储在规划工作流旁边,以便边缘情况保持可操作性。
|
||||
@@ -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/`)
|
||||
|
||||
284
docs/zh-CN/skills/kotlin-coroutines-flows/SKILL.md
Normal file
284
docs/zh-CN/skills/kotlin-coroutines-flows/SKILL.md
Normal file
@@ -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<List<Item>> = 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<UserProgress> = observeProgress()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = UserProgress.EMPTY
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`WhileSubscribed(5_000)` 会在最后一个订阅者离开后,保持上游活动 5 秒——可在配置更改时存活而无需重启。
|
||||
|
||||
### 组合多个 Flow
|
||||
|
||||
```kotlin
|
||||
val uiState: StateFlow<HomeState> = 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<Data> = 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<Effect>()
|
||||
val effects: SharedFlow<Effect> = _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<Item>) = 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<List<Item>>(emptyList())
|
||||
|
||||
override fun observeItems(): Flow<List<Item>> = _items
|
||||
|
||||
fun emit(items: List<Item>) { _items.value = items }
|
||||
|
||||
override suspend fun getItemsByCategory(category: String): Result<List<Item>> {
|
||||
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`。
|
||||
719
docs/zh-CN/skills/kotlin-exposed-patterns/SKILL.md
Normal file
719
docs/zh-CN/skills/kotlin-exposed-patterns/SKILL.md
Normal file
@@ -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>("role", 20)
|
||||
val metadata = jsonb<UserMetadata>("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<OrderStatus>("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<UserRow> =
|
||||
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<OrderWithUser> =
|
||||
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<Role, Long> =
|
||||
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<UserRow> =
|
||||
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<UserRow> =
|
||||
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<T>(
|
||||
val data: List<T>,
|
||||
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<UserRow> =
|
||||
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<CreateUserRequest>): List<UUID> =
|
||||
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<UUID>) : UUIDEntity(id) {
|
||||
companion object : UUIDEntityClass<UserEntity>(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<UUID>) : UUIDEntity(id) {
|
||||
companion object : UUIDEntityClass<OrderEntity>(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<User> =
|
||||
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<User> =
|
||||
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<User>
|
||||
suspend fun search(query: String): List<User>
|
||||
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<User> =
|
||||
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<User> =
|
||||
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 <reified T : Any> Table.jsonb(
|
||||
name: String,
|
||||
json: Json,
|
||||
): Column<T> = registerColumn(name, object : ColumnType<T>() {
|
||||
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<String, String> = emptyMap(),
|
||||
val tags: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
object UsersTable : UUIDTable("users") {
|
||||
val metadata = jsonb<UserMetadata>("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` 以获得协程支持,并将数据库操作包装在仓储接口之后以提高可测试性。
|
||||
689
docs/zh-CN/skills/kotlin-ktor-patterns/SKILL.md
Normal file
689
docs/zh-CN/skills/kotlin-ktor-patterns/SKILL.md
Normal file
@@ -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<UserService>()
|
||||
|
||||
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<CreateUserRequest>()
|
||||
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<UpdateUserRequest>()
|
||||
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<T>(
|
||||
val success: Boolean,
|
||||
val data: T? = null,
|
||||
val error: String? = null,
|
||||
) {
|
||||
companion object {
|
||||
fun <T> ok(data: T): ApiResponse<T> = ApiResponse(success = true, data = data)
|
||||
fun <T> error(message: String): ApiResponse<T> = ApiResponse(success = false, error = message)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PaginatedResponse<T>(
|
||||
val data: List<T>,
|
||||
val total: Long,
|
||||
val page: Int,
|
||||
val limit: Int,
|
||||
)
|
||||
```
|
||||
|
||||
### 自定义序列化器
|
||||
|
||||
```kotlin
|
||||
object InstantSerializer : KSerializer<Instant> {
|
||||
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<Unit>("Invalid or expired token"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extracting user from JWT
|
||||
fun ApplicationCall.userId(): String =
|
||||
principal<JWTPrincipal>()
|
||||
?.payload
|
||||
?.getClaim("userId")
|
||||
?.asString()
|
||||
?: throw AuthenticationException("No userId in token")
|
||||
```
|
||||
|
||||
### 认证路由
|
||||
|
||||
```kotlin
|
||||
fun Route.authRoutes() {
|
||||
val authService by inject<AuthService>()
|
||||
|
||||
route("/auth") {
|
||||
post("/login") {
|
||||
val request = call.receive<LoginRequest>()
|
||||
val token = authService.login(request.email, request.password)
|
||||
?: return@post call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
ApiResponse.error<Unit>("Invalid credentials"),
|
||||
)
|
||||
call.respond(ApiResponse.ok(TokenResponse(token)))
|
||||
}
|
||||
|
||||
post("/register") {
|
||||
val request = call.receive<RegisterRequest>()
|
||||
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<ContentTransformationException> { call, cause ->
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("Invalid request body: ${cause.message}"),
|
||||
)
|
||||
}
|
||||
|
||||
exception<IllegalArgumentException> { call, cause ->
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>(cause.message ?: "Bad request"),
|
||||
)
|
||||
}
|
||||
|
||||
exception<AuthenticationException> { call, _ ->
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
ApiResponse.error<Unit>("Authentication required"),
|
||||
)
|
||||
}
|
||||
|
||||
exception<AuthorizationException> { call, _ ->
|
||||
call.respond(
|
||||
HttpStatusCode.Forbidden,
|
||||
ApiResponse.error<Unit>("Access denied"),
|
||||
)
|
||||
}
|
||||
|
||||
exception<NotFoundException> { call, cause ->
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
ApiResponse.error<Unit>(cause.message ?: "Resource not found"),
|
||||
)
|
||||
}
|
||||
|
||||
exception<Throwable> { call, cause ->
|
||||
call.application.log.error("Unhandled exception", cause)
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse.error<Unit>("Internal server error"),
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.NotFound) { call, status ->
|
||||
call.respond(status, ApiResponse.error<Unit>("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<Database> { DatabaseFactory.create(get()) }
|
||||
|
||||
// Repositories
|
||||
single<UserRepository> { ExposedUserRepository(get()) }
|
||||
single<OrderRepository> { 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<UserService>()
|
||||
|
||||
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<UserRepository> { mockk() }
|
||||
single { UserService(get()) }
|
||||
}
|
||||
|
||||
private val repository by inject<UserRepository>()
|
||||
private val service by inject<UserService>()
|
||||
|
||||
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<UserService>()
|
||||
|
||||
post("/users") {
|
||||
val request = call.receive<CreateUserRequest>()
|
||||
|
||||
// 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<Connection>(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<ApiResponse<List<UserResponse>>>()
|
||||
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<T>()` | 反序列化请求体 |
|
||||
| `call.respond(status, body)` | 发送带状态的响应 |
|
||||
| `call.parameters["id"]` | 读取路径参数 |
|
||||
| `call.request.queryParameters["q"]` | 读取查询参数 |
|
||||
| `install(Plugin) { }` | 安装并配置插件 |
|
||||
| `authenticate("name") { }` | 使用身份验证保护路由 |
|
||||
| `by inject<T>()` | Koin 依赖注入 |
|
||||
| `testApplication { }` | 集成测试 |
|
||||
|
||||
**记住**:Ktor 是围绕 Kotlin 协程和 DSL 设计的。保持路由精简,将逻辑推送到服务层,并使用 Koin 进行依赖注入。使用 `testApplication` 进行测试以获得完整的集成覆盖。
|
||||
714
docs/zh-CN/skills/kotlin-patterns/SKILL.md
Normal file
714
docs/zh-CN/skills/kotlin-patterns/SKILL.md
Normal file
@@ -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<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>()
|
||||
data class Failure(val error: AppError) : Result<Nothing>()
|
||||
data object Loading : Result<Nothing>()
|
||||
}
|
||||
```
|
||||
|
||||
**使用 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<User> = listOf(user1, user2)
|
||||
val filtered = users.filter { it.email.isNotBlank() }
|
||||
|
||||
// Bad: Mutable state
|
||||
var currentUser: User? = null // Avoid mutable global state
|
||||
val mutableUsers = mutableListOf<User>() // 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<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>()
|
||||
data class Failure(val error: AppError) : Result<Nothing>()
|
||||
data object Loading : Result<Nothing>()
|
||||
}
|
||||
|
||||
fun <T> Result<T>.getOrNull(): T? = when (this) {
|
||||
is Result.Success -> data
|
||||
is Result.Failure -> null
|
||||
is Result.Loading -> null
|
||||
}
|
||||
|
||||
fun <T> Result<T>.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 <T> List<T>.second(): T = this[1]
|
||||
|
||||
fun <T> List<T>.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<User> = 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<List<User>> = 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<String>): Flow<List<User>> =
|
||||
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<Item>) {
|
||||
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<User> 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<String, Any?>) {
|
||||
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<Element>()
|
||||
|
||||
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<Long> = 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<Test> {
|
||||
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<User> = 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<String> = users
|
||||
.filter { it.role == Role.ADMIN && it.isActive }
|
||||
.sortedBy { it.name }
|
||||
.map { it.email }
|
||||
|
||||
// Good: Grouping and aggregation
|
||||
val usersByRole: Map<Role, List<User>> = users.groupBy { it.role }
|
||||
|
||||
val oldestByRole: Map<Role, User?> = users.groupBy { it.role }
|
||||
.mapValues { (_, users) -> users.minByOrNull { it.createdAt } }
|
||||
|
||||
// Good: Associate for map creation
|
||||
val usersById: Map<UserId, User> = 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 代码应简洁但可读。利用类型系统确保安全,优先使用不可变性,并使用协程处理并发。如有疑问,让编译器帮助你。
|
||||
826
docs/zh-CN/skills/kotlin-testing/SKILL.md
Normal file
826
docs/zh-CN/skills/kotlin-testing/SKILL.md
Normal file
@@ -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<String> {
|
||||
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<String> {
|
||||
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<UserRepository>()
|
||||
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<UserNotFoundException> {
|
||||
service.getUser("999")
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### BehaviorSpec(BDD 风格)
|
||||
|
||||
```kotlin
|
||||
class OrderServiceTest : BehaviorSpec({
|
||||
val repository = mockk<OrderRepository>()
|
||||
val paymentService = mockk<PaymentService>()
|
||||
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<PaymentException> {
|
||||
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<User>()
|
||||
|
||||
// Numbers
|
||||
count shouldBeGreaterThan 0
|
||||
price shouldBeInRange 1.0..100.0
|
||||
|
||||
// Exceptions
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
validateAge(-1)
|
||||
}.message shouldBe "Age must be positive"
|
||||
|
||||
shouldNotThrow<Exception> {
|
||||
validateAge(25)
|
||||
}
|
||||
```
|
||||
|
||||
#### 自定义匹配器
|
||||
|
||||
```kotlin
|
||||
fun beActiveUser() = object : Matcher<User> {
|
||||
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<UserRepository>()
|
||||
val logger = mockk<Logger>(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<UserRepository>()
|
||||
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<User>()
|
||||
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<TimeoutCancellationException> {
|
||||
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<String>()
|
||||
|
||||
val results = mutableListOf<List<User>>()
|
||||
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<String> { 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<User>(json)
|
||||
decoded shouldBe user
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 自定义生成器
|
||||
|
||||
```kotlin
|
||||
val userArb: Arb<User> = Arb.bind(
|
||||
Arb.string(minSize = 1, maxSize = 50),
|
||||
Arb.email(),
|
||||
Arb.enum<Role>(),
|
||||
) { name, email, role ->
|
||||
User(
|
||||
id = UserId(UUID.randomUUID().toString()),
|
||||
name = name,
|
||||
email = Email(email),
|
||||
role = role,
|
||||
)
|
||||
}
|
||||
|
||||
val moneyArb: Arb<Money> = Arb.bind(
|
||||
Arb.long(1L..1_000_000L),
|
||||
Arb.enum<Currency>(),
|
||||
) { 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<DateParseException> {
|
||||
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<List<UserResponse>>()
|
||||
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 来清晰地模拟依赖项。
|
||||
218
docs/zh-CN/skills/logistics-exception-management/SKILL.md
Normal file
218
docs/zh-CN/skills/logistics-exception-management/SKILL.md
Normal file
@@ -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检查清单放在执行本手册的团队附近。
|
||||
504
docs/zh-CN/skills/perl-patterns/SKILL.md
Normal file
504
docs/zh-CN/skills/perl-patterns/SKILL.md
Normal file
@@ -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{
|
||||
^ (?<timestamp> \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} )
|
||||
\s+ \[ (?<level> \w+ ) \]
|
||||
\s+ (?<message> .+ ) $
|
||||
}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 上经过实战检验的模块,而不是自己动手的解决方案。
|
||||
503
docs/zh-CN/skills/perl-security/SKILL.md
Normal file
503
docs/zh-CN/skills/perl-security/SKILL.md
Normal file
@@ -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 = <STDIN>; # 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 "<div>$input</div>"; # XSS if $input contains <script>
|
||||
}
|
||||
```
|
||||
|
||||
### CSRF 保护
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Crypt::URandom qw(urandom);
|
||||
use MIME::Base64 qw(encode_base64url);
|
||||
|
||||
sub generate_csrf_token() {
|
||||
return encode_base64url(urandom(32));
|
||||
}
|
||||
```
|
||||
|
||||
验证令牌时使用恒定时间比较。大多数 Web 框架(Mojolicious、Dancer2、Catalyst)都提供内置的 CSRF 保护——优先使用这些而非自行实现的解决方案。
|
||||
|
||||
### 会话和标头安全
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
|
||||
# Mojolicious session + headers
|
||||
$app->secrets(['long-random-secret-rotated-regularly']);
|
||||
$app->sessions->secure(1); # HTTPS only
|
||||
$app->sessions->samesite('Lax');
|
||||
|
||||
$app->hook(after_dispatch => sub ($c) {
|
||||
$c->res->headers->header('X-Content-Type-Options' => 'nosniff');
|
||||
$c->res->headers->header('X-Frame-Options' => 'DENY');
|
||||
$c->res->headers->header('Content-Security-Policy' => "default-src 'self'");
|
||||
$c->res->headers->header('Strict-Transport-Security' => 'max-age=31536000; includeSubDomains');
|
||||
});
|
||||
```
|
||||
|
||||
## 输出编码
|
||||
|
||||
始终根据上下文对输出进行编码:HTML 使用 `HTML::Entities::encode_entities()`,URL 使用 `URI::Escape::uri_escape_utf8()`,JSON 使用 `JSON::MaybeXS::encode_json()`。
|
||||
|
||||
## CPAN 模块安全
|
||||
|
||||
* **固定版本** 在 cpanfile 中:`requires 'DBI', '== 1.643';`
|
||||
* **优先使用维护中的模块**:在 MetaCPAN 上检查最新发布版本
|
||||
* **最小化依赖项**:每个依赖项都是一个攻击面
|
||||
|
||||
## 安全工具
|
||||
|
||||
### perlcritic 安全策略
|
||||
|
||||
```ini
|
||||
# .perlcriticrc — security-focused configuration
|
||||
severity = 3
|
||||
theme = security + core
|
||||
|
||||
# Require three-arg open
|
||||
[InputOutput::RequireThreeArgOpen]
|
||||
severity = 5
|
||||
|
||||
# Require checked system calls
|
||||
[InputOutput::RequireCheckedSyscalls]
|
||||
functions = :builtins
|
||||
severity = 4
|
||||
|
||||
# Prohibit string eval
|
||||
[BuiltinFunctions::ProhibitStringyEval]
|
||||
severity = 5
|
||||
|
||||
# Prohibit backtick operators
|
||||
[InputOutput::ProhibitBacktickOperators]
|
||||
severity = 4
|
||||
|
||||
# Require taint checking in CGI
|
||||
[Modules::RequireTaintChecking]
|
||||
severity = 5
|
||||
|
||||
# Prohibit two-arg open
|
||||
[InputOutput::ProhibitTwoArgOpen]
|
||||
severity = 5
|
||||
|
||||
# Prohibit bare-word filehandles
|
||||
[InputOutput::ProhibitBarewordFileHandles]
|
||||
severity = 5
|
||||
```
|
||||
|
||||
### 运行 perlcritic
|
||||
|
||||
```bash
|
||||
# Check a file
|
||||
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
|
||||
|
||||
# Check entire project
|
||||
perlcritic --severity 3 --theme security lib/
|
||||
|
||||
# CI integration
|
||||
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
|
||||
```
|
||||
|
||||
## 快速安全检查清单
|
||||
|
||||
| 检查项 | 需验证的内容 |
|
||||
|---|---|
|
||||
| 污染模式 | CGI/web 脚本上使用 `-T` 标志 |
|
||||
| 输入验证 | 允许列表模式,长度限制 |
|
||||
| 文件操作 | 三参数 open,路径遍历检查 |
|
||||
| 进程执行 | 列表形式的 system,无 shell 插值 |
|
||||
| SQL 查询 | DBI 占位符,绝不插值 |
|
||||
| HTML 输出 | `encode_entities()`,模板自动转义 |
|
||||
| CSRF 令牌 | 生成令牌,并在状态更改请求时验证 |
|
||||
| 会话配置 | 安全、HttpOnly、SameSite Cookie |
|
||||
| HTTP 标头 | CSP、X-Frame-Options、HSTS |
|
||||
| 依赖项 | 固定版本,已审计模块 |
|
||||
| 正则表达式安全 | 无嵌套量词,锚定模式 |
|
||||
| 错误消息 | 不向用户泄露堆栈跟踪或路径 |
|
||||
|
||||
## 反模式
|
||||
|
||||
```perl
|
||||
# 1. Two-arg open with user data (command injection)
|
||||
open my $fh, $user_input; # CRITICAL vulnerability
|
||||
|
||||
# 2. String-form system (shell injection)
|
||||
system("convert $user_file output.png"); # CRITICAL vulnerability
|
||||
|
||||
# 3. SQL string interpolation
|
||||
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
|
||||
|
||||
# 4. eval with user input (code injection)
|
||||
eval $user_code; # Remote code execution
|
||||
|
||||
# 5. Trusting $ENV without sanitizing
|
||||
my $path = $ENV{UPLOAD_DIR}; # Could be manipulated
|
||||
system("ls $path"); # Double vulnerability
|
||||
|
||||
# 6. Disabling taint without validation
|
||||
($input) = $input =~ /(.*)/s; # Lazy untaint — defeats purpose
|
||||
|
||||
# 7. Raw user data in HTML
|
||||
print "<div>Welcome, $username!</div>"; # XSS
|
||||
|
||||
# 8. Unvalidated redirects
|
||||
print $cgi->redirect($user_url); # Open redirect
|
||||
```
|
||||
|
||||
**请记住**:Perl 的灵活性很强大,但需要纪律。对面向 Web 的代码使用污染模式,使用允许列表验证所有输入,对每个查询使用 DBI 占位符,并根据上下文对所有输出进行编码。纵深防御——绝不依赖单一防护层。
|
||||
475
docs/zh-CN/skills/perl-testing/SKILL.md
Normal file
475
docs/zh-CN/skills/perl-testing/SKILL.md
Normal file
@@ -0,0 +1,475 @@
|
||||
---
|
||||
name: perl-testing
|
||||
description: 使用Test2::V0、Test::More、prove runner、模拟、Devel::Cover覆盖率和TDD方法的Perl测试模式。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Perl 测试模式
|
||||
|
||||
使用 Test2::V0、Test::More、prove 和 TDD 方法论为 Perl 应用程序提供全面的测试策略。
|
||||
|
||||
## 何时激活
|
||||
|
||||
* 编写新的 Perl 代码(遵循 TDD:红、绿、重构)
|
||||
* 为 Perl 模块或应用程序设计测试套件
|
||||
* 审查 Perl 测试覆盖率
|
||||
* 设置 Perl 测试基础设施
|
||||
* 将测试从 Test::More 迁移到 Test2::V0
|
||||
* 调试失败的 Perl 测试
|
||||
|
||||
## TDD 工作流程
|
||||
|
||||
始终遵循 RED-GREEN-REFACTOR 循环。
|
||||
|
||||
```perl
|
||||
# Step 1: RED — Write a failing test
|
||||
# t/unit/calculator.t
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
|
||||
use lib 'lib';
|
||||
use Calculator;
|
||||
|
||||
subtest 'addition' => sub {
|
||||
my $calc = Calculator->new;
|
||||
is($calc->add(2, 3), 5, 'adds two numbers');
|
||||
is($calc->add(-1, 1), 0, 'handles negatives');
|
||||
};
|
||||
|
||||
done_testing;
|
||||
|
||||
# Step 2: GREEN — Write minimal implementation
|
||||
# lib/Calculator.pm
|
||||
package Calculator;
|
||||
use v5.36;
|
||||
use Moo;
|
||||
|
||||
sub add($self, $a, $b) {
|
||||
return $a + $b;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
# Step 3: REFACTOR — Improve while tests stay green
|
||||
# Run: prove -lv t/unit/calculator.t
|
||||
```
|
||||
|
||||
## Test::More 基础
|
||||
|
||||
标准的 Perl 测试模块 —— 广泛使用,随核心发行。
|
||||
|
||||
### 基本断言
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test::More;
|
||||
|
||||
# Plan upfront or use done_testing
|
||||
# plan tests => 5; # Fixed plan (optional)
|
||||
|
||||
# Equality
|
||||
is($result, 42, 'returns correct value');
|
||||
isnt($result, 0, 'not zero');
|
||||
|
||||
# Boolean
|
||||
ok($user->is_active, 'user is active');
|
||||
ok(!$user->is_banned, 'user is not banned');
|
||||
|
||||
# Deep comparison
|
||||
is_deeply(
|
||||
$got,
|
||||
{ name => 'Alice', roles => ['admin'] },
|
||||
'returns expected structure'
|
||||
);
|
||||
|
||||
# Pattern matching
|
||||
like($error, qr/not found/i, 'error mentions not found');
|
||||
unlike($output, qr/password/, 'output hides password');
|
||||
|
||||
# Type check
|
||||
isa_ok($obj, 'MyApp::User');
|
||||
can_ok($obj, 'save', 'delete');
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
### SKIP 和 TODO
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test::More;
|
||||
|
||||
# Skip tests conditionally
|
||||
SKIP: {
|
||||
skip 'No database configured', 2 unless $ENV{TEST_DB};
|
||||
|
||||
my $db = connect_db();
|
||||
ok($db->ping, 'database is reachable');
|
||||
is($db->version, '15', 'correct PostgreSQL version');
|
||||
}
|
||||
|
||||
# Mark expected failures
|
||||
TODO: {
|
||||
local $TODO = 'Caching not yet implemented';
|
||||
is($cache->get('key'), 'value', 'cache returns value');
|
||||
}
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
## Test2::V0 现代框架
|
||||
|
||||
Test2::V0 是 Test::More 的现代替代品 —— 更丰富的断言、更好的诊断和可扩展性。
|
||||
|
||||
### 为什么选择 Test2?
|
||||
|
||||
* 使用哈希/数组构建器进行卓越的深层比较
|
||||
* 失败时提供更好的诊断输出
|
||||
* 具有更清晰作用域的子测试
|
||||
* 可通过 Test2::Tools::\* 插件扩展
|
||||
* 与 Test::More 测试向后兼容
|
||||
|
||||
### 使用构建器进行深层比较
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
|
||||
# Hash builder — check partial structure
|
||||
is(
|
||||
$user->to_hash,
|
||||
hash {
|
||||
field name => 'Alice';
|
||||
field email => match(qr/\@example\.com$/);
|
||||
field age => validator(sub { $_ >= 18 });
|
||||
# Ignore other fields
|
||||
etc();
|
||||
},
|
||||
'user has expected fields'
|
||||
);
|
||||
|
||||
# Array builder
|
||||
is(
|
||||
$result,
|
||||
array {
|
||||
item 'first';
|
||||
item match(qr/^second/);
|
||||
item DNE(); # Does Not Exist — verify no extra items
|
||||
},
|
||||
'result matches expected list'
|
||||
);
|
||||
|
||||
# Bag — order-independent comparison
|
||||
is(
|
||||
$tags,
|
||||
bag {
|
||||
item 'perl';
|
||||
item 'testing';
|
||||
item 'tdd';
|
||||
},
|
||||
'has all required tags regardless of order'
|
||||
);
|
||||
```
|
||||
|
||||
### 子测试
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
|
||||
subtest 'User creation' => sub {
|
||||
my $user = User->new(name => 'Alice', email => 'alice@example.com');
|
||||
ok($user, 'user object created');
|
||||
is($user->name, 'Alice', 'name is set');
|
||||
is($user->email, 'alice@example.com', 'email is set');
|
||||
};
|
||||
|
||||
subtest 'User validation' => sub {
|
||||
my $warnings = warns {
|
||||
User->new(name => '', email => 'bad');
|
||||
};
|
||||
ok($warnings, 'warns on invalid data');
|
||||
};
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
### 使用 Test2 进行异常测试
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
|
||||
# Test that code dies
|
||||
like(
|
||||
dies { divide(10, 0) },
|
||||
qr/Division by zero/,
|
||||
'dies on division by zero'
|
||||
);
|
||||
|
||||
# Test that code lives
|
||||
ok(lives { divide(10, 2) }, 'division succeeds') or note($@);
|
||||
|
||||
# Combined pattern
|
||||
subtest 'error handling' => sub {
|
||||
ok(lives { parse_config('valid.json') }, 'valid config parses');
|
||||
like(
|
||||
dies { parse_config('missing.json') },
|
||||
qr/Cannot open/,
|
||||
'missing file dies with message'
|
||||
);
|
||||
};
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
## 测试组织与 prove
|
||||
|
||||
### 目录结构
|
||||
|
||||
```text
|
||||
t/
|
||||
├── 00-load.t # Verify modules compile
|
||||
├── 01-basic.t # Core functionality
|
||||
├── unit/
|
||||
│ ├── config.t # Unit tests by module
|
||||
│ ├── user.t
|
||||
│ └── util.t
|
||||
├── integration/
|
||||
│ ├── database.t
|
||||
│ └── api.t
|
||||
├── lib/
|
||||
│ └── TestHelper.pm # Shared test utilities
|
||||
└── fixtures/
|
||||
├── config.json # Test data files
|
||||
└── users.csv
|
||||
```
|
||||
|
||||
### prove 命令
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
prove -l t/
|
||||
|
||||
# Verbose output
|
||||
prove -lv t/
|
||||
|
||||
# Run specific test
|
||||
prove -lv t/unit/user.t
|
||||
|
||||
# Recursive search
|
||||
prove -lr t/
|
||||
|
||||
# Parallel execution (8 jobs)
|
||||
prove -lr -j8 t/
|
||||
|
||||
# Run only failing tests from last run
|
||||
prove -l --state=failed t/
|
||||
|
||||
# Colored output with timer
|
||||
prove -l --color --timer t/
|
||||
|
||||
# TAP output for CI
|
||||
prove -l --formatter TAP::Formatter::JUnit t/ > results.xml
|
||||
```
|
||||
|
||||
### .proverc 配置
|
||||
|
||||
```text
|
||||
-l
|
||||
--color
|
||||
--timer
|
||||
-r
|
||||
-j4
|
||||
--state=save
|
||||
```
|
||||
|
||||
## 夹具与设置/拆卸
|
||||
|
||||
### 子测试隔离
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
use File::Temp qw(tempdir);
|
||||
use Path::Tiny;
|
||||
|
||||
subtest 'file processing' => sub {
|
||||
# Setup
|
||||
my $dir = tempdir(CLEANUP => 1);
|
||||
my $file = path($dir, 'input.txt');
|
||||
$file->spew_utf8("line1\nline2\nline3\n");
|
||||
|
||||
# Test
|
||||
my $result = process_file("$file");
|
||||
is($result->{line_count}, 3, 'counts lines');
|
||||
|
||||
# Teardown happens automatically (CLEANUP => 1)
|
||||
};
|
||||
```
|
||||
|
||||
### 共享测试助手
|
||||
|
||||
将可重用的助手放在 `t/lib/TestHelper.pm` 中,并通过 `use lib 't/lib'` 加载。通过 `Exporter` 导出工厂函数,例如 `create_test_db()`、`create_temp_dir()` 和 `fixture_path()`。
|
||||
|
||||
## 模拟
|
||||
|
||||
### Test::MockModule
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
use Test::MockModule;
|
||||
|
||||
subtest 'mock external API' => sub {
|
||||
my $mock = Test::MockModule->new('MyApp::API');
|
||||
|
||||
# Good: Mock returns controlled data
|
||||
$mock->mock(fetch_user => sub ($self, $id) {
|
||||
return { id => $id, name => 'Mock User', email => 'mock@test.com' };
|
||||
});
|
||||
|
||||
my $api = MyApp::API->new;
|
||||
my $user = $api->fetch_user(42);
|
||||
is($user->{name}, 'Mock User', 'returns mocked user');
|
||||
|
||||
# Verify call count
|
||||
my $call_count = 0;
|
||||
$mock->mock(fetch_user => sub { $call_count++; return {} });
|
||||
$api->fetch_user(1);
|
||||
$api->fetch_user(2);
|
||||
is($call_count, 2, 'fetch_user called twice');
|
||||
|
||||
# Mock is automatically restored when $mock goes out of scope
|
||||
};
|
||||
|
||||
# Bad: Monkey-patching without restoration
|
||||
# *MyApp::API::fetch_user = sub { ... }; # NEVER — leaks across tests
|
||||
```
|
||||
|
||||
对于轻量级的模拟对象,使用 `Test::MockObject` 创建可注入的测试替身,使用 `->mock()` 并验证调用 `->called_ok()`。
|
||||
|
||||
## 使用 Devel::Cover 进行覆盖率分析
|
||||
|
||||
### 运行覆盖率分析
|
||||
|
||||
```bash
|
||||
# Basic coverage report
|
||||
cover -test
|
||||
|
||||
# Or step by step
|
||||
perl -MDevel::Cover -Ilib t/unit/user.t
|
||||
cover
|
||||
|
||||
# HTML report
|
||||
cover -report html
|
||||
open cover_db/coverage.html
|
||||
|
||||
# Specific thresholds
|
||||
cover -test -report text | grep 'Total'
|
||||
|
||||
# CI-friendly: fail under threshold
|
||||
cover -test && cover -report text -select '^lib/' \
|
||||
| perl -ne 'if (/Total.*?(\d+\.\d+)/) { exit 1 if $1 < 80 }'
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
对数据库测试使用内存中的 SQLite,对 API 测试模拟 HTTP::Tiny。
|
||||
|
||||
```perl
|
||||
use v5.36;
|
||||
use Test2::V0;
|
||||
use DBI;
|
||||
|
||||
subtest 'database integration' => sub {
|
||||
my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', {
|
||||
RaiseError => 1,
|
||||
});
|
||||
$dbh->do('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
|
||||
$dbh->prepare('INSERT INTO users (name) VALUES (?)')->execute('Alice');
|
||||
my $row = $dbh->selectrow_hashref('SELECT * FROM users WHERE name = ?', undef, 'Alice');
|
||||
is($row->{name}, 'Alice', 'inserted and retrieved user');
|
||||
};
|
||||
|
||||
done_testing;
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 应做事项
|
||||
|
||||
* **遵循 TDD**:在实现之前编写测试(红-绿-重构)
|
||||
* **使用 Test2::V0**:现代断言,更好的诊断
|
||||
* **使用子测试**:分组相关断言,隔离状态
|
||||
* **模拟外部依赖**:网络、数据库、文件系统
|
||||
* **使用 `prove -l`**:始终将 lib/ 包含在 `@INC` 中
|
||||
* **清晰命名测试**:`'user login with invalid password fails'`
|
||||
* **测试边界情况**:空字符串、undef、零、边界值
|
||||
* **目标 80%+ 覆盖率**:专注于业务逻辑路径
|
||||
* **保持测试快速**:模拟 I/O,使用内存数据库
|
||||
|
||||
### 禁止事项
|
||||
|
||||
* **不要测试实现**:测试行为和输出,而非内部细节
|
||||
* **不要在子测试之间共享状态**:每个子测试都应是独立的
|
||||
* **不要跳过 `done_testing`**:确保所有计划的测试都已运行
|
||||
* **不要过度模拟**:仅模拟边界,而非被测试的代码
|
||||
* **不要在新项目中使用 `Test::More`**:首选 Test2::V0
|
||||
* **不要忽略测试失败**:所有测试必须在合并前通过
|
||||
* **不要测试 CPAN 模块**:相信库能正常工作
|
||||
* **不要编写脆弱的测试**:避免过度具体的字符串匹配
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 任务 | 命令 / 模式 |
|
||||
|---|---|
|
||||
| 运行所有测试 | `prove -lr t/` |
|
||||
| 详细运行单个测试 | `prove -lv t/unit/user.t` |
|
||||
| 并行测试运行 | `prove -lr -j8 t/` |
|
||||
| 覆盖率报告 | `cover -test && cover -report html` |
|
||||
| 测试相等性 | `is($got, $expected, 'label')` |
|
||||
| 深层比较 | `is($got, hash { field k => 'v'; etc() }, 'label')` |
|
||||
| 测试异常 | `like(dies { ... }, qr/msg/, 'label')` |
|
||||
| 测试无异常 | `ok(lives { ... }, 'label')` |
|
||||
| 模拟一个方法 | `Test::MockModule->new('Pkg')->mock(m => sub { ... })` |
|
||||
| 跳过测试 | `SKIP: { skip 'reason', $count unless $cond; ... }` |
|
||||
| TODO 测试 | `TODO: { local $TODO = 'reason'; ... }` |
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### 忘记 `done_testing`
|
||||
|
||||
```perl
|
||||
# Bad: Test file runs but doesn't verify all tests executed
|
||||
use Test2::V0;
|
||||
is(1, 1, 'works');
|
||||
# Missing done_testing — silent bugs if test code is skipped
|
||||
|
||||
# Good: Always end with done_testing
|
||||
use Test2::V0;
|
||||
is(1, 1, 'works');
|
||||
done_testing;
|
||||
```
|
||||
|
||||
### 缺少 `-l` 标志
|
||||
|
||||
```bash
|
||||
# Bad: Modules in lib/ not found
|
||||
prove t/unit/user.t
|
||||
# Can't locate MyApp/User.pm in @INC
|
||||
|
||||
# Good: Include lib/ in @INC
|
||||
prove -l t/unit/user.t
|
||||
```
|
||||
|
||||
### 过度模拟
|
||||
|
||||
模拟*依赖项*,而非被测试的代码。如果你的测试只验证模拟返回了你告诉它的内容,那么它什么也没测试。
|
||||
|
||||
### 测试污染
|
||||
|
||||
在子测试内部使用 `my` 变量 —— 永远不要用 `our` —— 以防止状态在测试之间泄漏。
|
||||
|
||||
**记住**:测试是你的安全网。保持它们快速、专注和独立。新项目使用 Test2::V0,运行使用 prove,问责使用 Devel::Cover。
|
||||
230
docs/zh-CN/skills/production-scheduling/SKILL.md
Normal file
230
docs/zh-CN/skills/production-scheduling/SKILL.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
name: production-scheduling
|
||||
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: "🏭"
|
||||
---
|
||||
|
||||
# 生产排程
|
||||
|
||||
## 角色与背景
|
||||
|
||||
您是一家离散型和批量生产工厂的高级生产排程员,该工厂运营着3-8条生产线,每班有50-300名直接劳动力。您负责管理跨越工作中心(包括机加工、装配、精加工和包装)的作业排序、产线平衡、换产优化和中断响应。您的系统包括ERP(SAP PP、Oracle Manufacturing 或 Epicor)、有限产能排程工具(Preactor、PlanetTogether 或 Opcenter APS)、用于车间执行和实时报告的MES,以及用于维护协调的CMMS。您处于生产管理(负责产出目标和人员配置)、计划(从MRP下发工单)、质量(控制产品放行)和维护(负责设备可用性)之间。您的工作是将一组具有交货日期、工艺路线和物料清单的工单,转化为分钟级的执行序列,以在满足客户交付承诺、劳动力规则和质量要求的同时,最大化瓶颈环节的产出。
|
||||
|
||||
## 何时使用
|
||||
|
||||
* 生产订单在受约束的工作中心上竞争资源
|
||||
* 中断(故障、短缺、缺勤)需要快速重新排序
|
||||
* 换产和批量生产的权衡需要明确的经济决策
|
||||
* 需要将新工单插入现有排程而不破坏已承诺的作业
|
||||
* 班次级别的瓶颈变化需要重新分配鼓点资源
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. 使用OEE数据和产能利用率识别系统约束(瓶颈)
|
||||
2. 按优先级对需求进行分类:逾期、约束资源供料作业和剩余作业
|
||||
3. 使用适合产品组合的派工规则(最早交货期、最短加工时间或考虑换产的EDD)对作业进行排序
|
||||
4. 利用换产矩阵和最近邻启发式算法配合2-opt改进来优化换产顺序
|
||||
5. 锁定一个稳定窗口(通常为24-48小时),以防止已承诺作业的排程频繁变动
|
||||
6. 发生中断时重新排程,仅对未锁定的作业重新排序;将更新后的排程发布到MES
|
||||
|
||||
## 示例
|
||||
|
||||
* **瓶颈设备故障**:2号线数控机床停机4小时。识别哪些作业在排队,评估哪些可以重新路由到3号线(替代工艺路线),哪些必须等待,以及如何对剩余队列重新排序,以最小化所有受影响订单的总延误时间。
|
||||
* **批量生产与混流生产决策**:一条产线上有来自4个产品系列的15个作业,系列间换产需要45分钟。使用换产成本和持有成本计算交叉点,确定批量生产(换产次数少,在制品多)优于混流生产(换产次数多,在制品少)的临界点。
|
||||
* **紧急插单**:销售部门承诺了一个交货期为2天的紧急订单,而本周排程已满。评估排程松弛时间,确定哪些现有作业可以承受一个班次的延迟而不错过其交货期,并在不破坏冻结窗口的情况下插入紧急订单。
|
||||
|
||||
## 核心知识
|
||||
|
||||
### 排程基础
|
||||
|
||||
**顺推排程与倒推排程**:顺推排程从物料可用日期开始,按顺序安排工序以找到最早完成日期。倒推排程从客户交货日期开始,向后推算以找到最晚允许开始日期。在实践中,默认使用倒推排程以保持灵活性并最小化在制品,当倒推计算显示最晚开始日期已经过去时,则切换到顺推排程——该工单已经延迟开始,需要从今天开始加急处理。
|
||||
|
||||
**有限产能与无限产能**:MRP运行无限产能计划——它假设每个工作中心都有无限的产能,并将超负荷标记出来供排程员手动解决。有限产能排程(FCS)尊重实际资源可用性:机器数量、班次模式、维护窗口和工装约束。切勿将MRP生成的排程视为可执行排程,除非已通过有限产能逻辑验证。MRP告诉您*需要*制造什么;FCS告诉您*何时*可以实际制造。
|
||||
|
||||
**鼓-缓冲-绳(DBR)与约束理论**:鼓是约束资源——相对于需求而言,过剩产能最少的工作中心。缓冲是保护约束资源免受上游物料短缺影响的时间缓冲(而非库存缓冲)。绳是限制新工作进入系统的释放机制,其速度与约束资源的处理速度相匹配。通过比较每个工作中心的负荷工时与可用工时来识别约束;利用率比率最高(>85%)的那个就是您的鼓。所有其他排程决策都应服从于保持鼓的供料和运行。在约束资源上损失一分钟,整个工厂就损失一分钟;在非约束资源上损失一分钟,如果缓冲时间能吸收它,则没有任何成本。
|
||||
|
||||
**准时化排序**:在混流装配环境中,平衡生产序列以最小化部件消耗率的变化。使用平准化逻辑:如果每班次生产模型A、B、C的比例为3:2:1,理想的序列是A-B-A-C-A-B,而不是AAA-BB-C。平衡的排序平滑了上游需求,减少了部件安全库存,并防止了"班末赶工"现象(最困难的工作被推到最后一小时)。
|
||||
|
||||
**MRP失效的情况**:MRP假设固定的提前期、无限的产能和完美的物料清单准确性。当出现以下情况时,它会失效:(a)提前期依赖于队列,在负荷轻时可压缩,负荷重时会延长;(b)多个工单竞争同一受约束资源;(c)换产时间依赖于顺序;(d)良率损失导致固定投入产生可变产出。排程员必须弥补所有这四种情况。
|
||||
|
||||
### 换产优化
|
||||
|
||||
**SMED方法论(单分钟快速换模)**:新乡重夫的框架将换产活动分为外部(可以在机器仍在运行上一个作业时完成)和内部(必须在机器停止时完成)。第一阶段:记录当前换产过程,并将每个要素分类为内部或外部。第二阶段:尽可能将内部要素转化为外部要素(预置工具、预热模具、预混材料)。第三阶段:简化剩余的内部要素(快速释放夹具、标准化模具高度、颜色编码连接)。第四阶段:通过防错和首件验证夹具消除调整。典型结果:仅通过第一阶段和第二阶段,换产时间即可减少40-60%。
|
||||
|
||||
**颜色/尺寸排序**:在喷漆、涂层、印刷和纺织操作中,按从浅到深、从小到大或从简单到复杂的顺序安排作业,以最大限度地减少运行之间的清洁工作。从浅到深的油漆顺序可能只需要5分钟的冲洗;从深到浅则需要30分钟的完全净化。将这些依赖于顺序的换产时间记录在换产矩阵中,并输入到排程算法中。
|
||||
|
||||
**批量生产与混流生产排程**:批量生产将所有属于同一产品系列的作业分组到一次运行中,最大限度地减少了总换产次数,但增加了在制品和提前期。混流生产交错生产产品以减少提前期和在制品,但会产生更多的换产。正确的平衡取决于换产成本与持有成本之比。当换产时间长且成本高(>60分钟,>500美元的废品和产出损失)时,倾向于批量生产。当换产速度快(<15分钟)或客户订单模式要求短提前期时,倾向于混流生产。
|
||||
|
||||
**换产成本 vs. 库存持有成本 vs. 交付权衡**:每个排程决策都涉及这种三方面的权衡。更长的批量生产减少了换产成本,但增加了周期库存,并可能导致非批量产品的交货期延误。较短的批量生产提高了交付响应能力,但增加了换产频率。经济交叉点是边际换产成本等于额外周期库存单位的边际持有成本之处。计算它,不要猜测。
|
||||
|
||||
### 瓶颈管理
|
||||
|
||||
**识别真正的约束 vs. 在制品堆积之处**:在制品在工作中心前堆积并不一定意味着该工作中心是约束。在制品堆积可能是因为上游工作中心批量投放,因为共享资源(起重机、叉车、检验员)造成了人为队列,或者因为排程规则导致下游物料短缺。真正的约束是所需工时与可用工时比率最高的资源。通过检查来验证:如果您在该工作中心增加一小时的产能,工厂产出会增加吗?如果是,它就是约束。
|
||||
|
||||
**缓冲管理**:在DBR中,时间缓冲通常是约束工序生产提前期的50%。监控缓冲渗透:绿色区域(缓冲消耗<33%)意味着约束得到良好保护;黄色区域(33-67%)触发对延迟到达的上游工作的加急;红色区域(>67%)触发管理层立即关注,并可能在上游工序安排加班。几周内的缓冲渗透趋势揭示了长期问题:持续的黄色意味着上游可靠性正在下降。
|
||||
|
||||
**从属原则**:非约束资源的排程应服务于约束资源,而不是最大化其自身的利用率。当约束资源以85%的利用率运行时,将非约束资源以100%的利用率运行会产生过剩的在制品,而不会增加产出。有意在非约束资源上安排空闲时间,以匹配约束资源的消耗率。
|
||||
|
||||
**检测移动的瓶颈**:随着产品组合变化、设备退化或人员班次变动,约束可能在各个工作中心之间移动。在白班是瓶颈的工作中心(运行高换产产品)可能在夜班不是瓶颈(运行长周期产品)。按产品组合每周监控利用率比率。当约束转移时,整个排程逻辑必须随之转移——新的鼓决定了节奏。
|
||||
|
||||
### 中断响应
|
||||
|
||||
**机器故障**:立即行动:(1)与维护部门评估维修时间估计;(2)确定故障机器是否是约束;(3)如果是约束,计算每小时的产出损失并启动应急计划——在备用设备上加班、外包或重新排序以优先处理利润率最高的作业。如果不是约束,评估缓冲渗透——如果缓冲是绿色的,则不对排程采取任何行动;如果是黄色或红色,则加急上游工作到替代工艺路线。
|
||||
|
||||
**物料短缺**:检查替代材料、替代物料清单和部分装配选项。如果某个组件短缺,您能否将子装配件装配到缺少组件之前,然后稍后完成(配套策略)?升级到采购部门以加急交付。重新排序排程,将不需要短缺物料的作业提前,保持约束资源运行。
|
||||
|
||||
**质量扣留**:当一批产品被质量扣留时,它对排程是不可见的——它不能发货,也不能被下游消耗。立即重新运行排程,排除被扣留的库存。如果被扣留的批次是供应给客户承诺的,评估替代来源:安全库存、来自其他工单的在制品库存,或加急生产替代批次。
|
||||
|
||||
**缺勤**:在有认证操作员要求的情况下,一名操作员缺勤可能使整条生产线瘫痪。维护一个交叉培训矩阵,显示哪些操作员在哪些设备上获得认证。当发生缺勤时,首先检查缺失的操作员是否操作约束资源——如果是,重新分配最合格的备用人员。如果缺失的操作员操作非约束资源,评估缓冲时间是否能吸收延迟,然后再从其他区域调配备用人员。
|
||||
|
||||
**重新排序框架:** 当发生中断时,应用以下优先级逻辑:(1) 首要保护瓶颈资源正常运行时间,(2) 按客户层级和违约风险顺序保护客户承诺,(3) 最小化新序列的总换产成本,(4) 在剩余可用操作员间均衡劳动负荷。重新排序,在30分钟内传达新计划,并在允许进一步更改前锁定至少4小时。
|
||||
|
||||
### 劳动力管理
|
||||
|
||||
**班次模式:** 常见模式包括3×8(三个8小时班次,24/5或24/7)、2×12(两个12小时班次,通常轮换休息日)和4×10(四个10小时日班,仅限日间作业)。每种模式对加班规则、交接班质量和疲劳相关错误率的影响不同。12小时班次减少了交接次数,但在第10-12小时增加了错误率。在排程中需考虑这一点:不要在12小时班次的最后2小时安排关键的首件检验或复杂的换产。
|
||||
|
||||
**技能矩阵:** 维护操作员 × 工作中心 × 认证等级(学员、合格、专家)的矩阵。排程可行性取决于此矩阵——如果某个班次没有合格的操作员,那么派往数控车床的工单就是不可行的。排程工具应将劳动力作为与机器并列的约束条件。
|
||||
|
||||
**交叉培训投资回报率:** 每增加一名在瓶颈工作中心获得认证的操作员,都会降低因缺勤导致瓶颈资源闲置的概率。量化计算:如果瓶颈资源每小时产生5000美元的产出,平均缺勤率为8%,那么仅有2名合格操作员与拥有4名合格操作员相比,每年预期的产出损失差异超过20万美元。
|
||||
|
||||
**工会规则与加班:** 许多制造环境对加班分配(按资历)、班次间强制休息时间(通常8-10小时)以及跨部门临时调动有合同约束。这些是排程算法必须遵守的硬性约束。违反工会规则可能引发申诉,其成本远超原本试图节省的生产成本。
|
||||
|
||||
### OEE — 整体设备效率
|
||||
|
||||
**计算:** OEE = 时间开动率 × 性能开动率 × 合格品率。时间开动率 = (计划生产时间 − 停机时间) / 计划生产时间。性能开动率 = (理想周期时间 × 总产量) / 运行时间。合格品率 = 合格品数量 / 总产量。世界级OEE为85%以上;典型的离散制造业在55–65%之间。
|
||||
|
||||
**计划与非计划停机:** 在某些OEE标准中,计划停机(计划性维护、换产、休息)不计入时间开动率的分母,而在另一些标准中则计入。当需要跨工厂比较或为资本扩张提供理由时,使用TEEP(完全有效生产率)——TEEP包含所有日历时间。
|
||||
|
||||
**时间开动率损失:** 故障和非计划停机。通过预防性维护、预测性维护(振动分析、热成像)和TPM操作员日常点检来解决。目标:非计划停机时间 < 计划时间的5%。
|
||||
|
||||
**性能开动率损失:** 速度损失和微停机。一台额定产能为100件/小时的机器以85件/小时运行,则有15%的性能损失。常见原因:物料供给不一致、刀具磨损、传感器误触发和操作员犹豫。按作业跟踪实际周期时间与标准周期时间。
|
||||
|
||||
**合格品率损失:** 废品和返工。瓶颈工序的首检合格率低于95%会直接降低有效产能。优先改进瓶颈工序的质量——瓶颈工序2%的合格率提升,其带来的产出增益等同于2%的产能扩张。
|
||||
|
||||
### ERP/MES交互模式
|
||||
|
||||
**SAP PP / Oracle Manufacturing 生产计划流程:** 需求以销售订单或预测消耗的形式进入,驱动MPS(主生产计划),MPS通过MRP分解为按工作中心划分的带有物料需求的计划订单。计划员将计划订单转换为生产订单,进行排序,并通过MES发布到车间。反馈从MES(工序确认、废品报告、工时记录)流回ERP,以更新订单状态和库存。
|
||||
|
||||
**工单管理:** 工单包含工艺路线(带工作中心、准备时间和运行时间的工序序列)、BOM(所需组件)和到期日。计划员的工作是将每个工序分配到特定资源的特定时间段,同时尊重资源产能、物料可用性和依赖约束(工序20必须在工序10完成后才能开始)。
|
||||
|
||||
**车间报告与计划-实际差异:** MES捕获实际开始/结束时间、实际产量、废品数量和停机原因。计划与MES实际值之间的差距即为"计划依从性"指标。健康的计划依从性 > 90%的作业在计划开始时间±1小时内开始。持续存在的差距表明,要么排程参数(准备时间、运行速率、良率系数)有误,要么车间未遵循排序。
|
||||
|
||||
**闭环:** 每个班次,在工序级别比较计划与实际。用实际值更新计划,对剩余计划期重新排序,并发布更新后的计划。这种"滚动重排"节奏使计划保持现实性而非理想化。最糟糕的失效模式是计划偏离现实并被车间忽视——一旦操作员不再信任计划,计划就失去了作用。
|
||||
|
||||
## 决策框架
|
||||
|
||||
### 作业优先级排序
|
||||
|
||||
当多个作业竞争同一资源时,应用此决策树:
|
||||
|
||||
1. **是否有任何作业已逾期或若不立即处理将错过到期日?** → 首先安排逾期作业,按客户违约风险排序(合同违约金 > 声誉损害 > 内部KPI影响)。
|
||||
2. **是否有任何作业正在供给瓶颈且瓶颈缓冲处于黄区或红区?** → 接下来安排供给瓶颈的作业,以防止瓶颈资源闲置。
|
||||
3. **在剩余作业中,应用适合产品组合的调度规则:**
|
||||
* 高多样性、小批量:使用**最早到期日**以最小化最大延迟。
|
||||
* 长周期、少品种:使用**最短加工时间**以最小化平均流程时间和在制品。
|
||||
* 混合型,且存在序列相关准备时间:使用**考虑准备时间的最早到期日**——在考虑准备时间的提前量下使用最早到期日,当交换相邻作业可节省>30分钟准备时间且不导致逾期时,则进行交换。
|
||||
4. **平局决胜:** 客户层级更高的胜出。如果层级相同,则利润率更高的作业胜出。
|
||||
|
||||
### 换产顺序优化
|
||||
|
||||
1. **建立换产矩阵:** 针对每对产品(A→B, B→A, A→C等),记录换产时间(分钟)和换产成本(人工 + 废品 + 产出损失)。
|
||||
2. **识别强制性顺序约束:** 某些转换是被禁止的(食品中的过敏原交叉污染,化学品中的危险物料排序)。这些是硬性约束,不可优化。
|
||||
3. **应用最近邻启发式作为基线:** 从当前产品开始,选择换产时间最小的下一个产品。这给出一个可行的初始序列。
|
||||
4. **通过2-opt交换进行改进:** 交换相邻作业对;如果总换产时间减少且不违反到期日,则保留交换。
|
||||
5. **根据到期日进行验证:** 将优化后的序列放入排程中运行。如果任何作业错过到期日,即使增加总换产时间也要将其提前插入。遵守到期日优先于换产优化。
|
||||
|
||||
### 中断后重新排序
|
||||
|
||||
当中断使当前计划失效时:
|
||||
|
||||
1. **评估影响窗口:** 中断的资源不可用多少小时/班次?它是否是瓶颈?
|
||||
2. **冻结已承诺的工作:** 除非物理上不可能,否则不应移动已在进行中或距开始时间2小时内的作业。
|
||||
3. **重新排序剩余作业:** 对未冻结的所有作业应用上述作业优先级框架,使用更新后的资源可用性。
|
||||
4. **30分钟内沟通:** 将修订后的计划发布给所有受影响的工作中心、主管和物料搬运工。
|
||||
5. **设置稳定性锁定:** 至少4小时内(或直到下一班次开始)不允许进一步更改计划,除非发生新的中断。持续重新排序比原始中断造成更多混乱。
|
||||
|
||||
### 瓶颈识别
|
||||
|
||||
1. **拉取过去2周所有工作中心的利用率报告**(按班次,而非平均值)。
|
||||
2. **按利用率比**(负荷小时数 / 可用小时数)**排序**。排名最高的工作中心是疑似瓶颈。
|
||||
3. **进行因果验证:** 增加该工作中心一小时的产能是否会提高工厂总产出?如果其下游工作中心在该工作中心停机时总是闲置,那么答案是肯定的。
|
||||
4. **检查模式是否变化:** 如果排名最高的工作中心在不同班次或不同周之间发生变化,则存在由产品组合驱动的动态瓶颈。在这种情况下,应根据每个班次的产品组合来安排该班次的*瓶颈*,而不是基于周平均值。
|
||||
5. **区分人工瓶颈:** 因上游批量投放导致在制品堆积而显得超负荷的工作中心并非真正的瓶颈——它是上游排程不佳的受害者。在为受害者增加产能之前,先修复上游的投放速率。
|
||||
|
||||
## 关键边缘案例
|
||||
|
||||
此处包含简要总结,以便您可以根据需要将其扩展为针对特定项目的操作手册。
|
||||
|
||||
1. **班次中动态瓶颈转移:** 产品组合变化导致瓶颈从机加工转移到装配。早上6点最优的计划到上午10点就错了。需要实时利用率监控和班次内重新排序授权。
|
||||
|
||||
2. **受监管工序的认证操作员缺勤:** 一项FDA监管的涂覆操作需要特定的操作员认证。唯一认证的夜班操作员请病假。该生产线无法合法运行。激活交叉培训矩阵,如果允许则呼叫认证的日班操作员加班,或者关闭受监管的工序并重新安排非监管工作的路线。
|
||||
|
||||
3. **来自一级客户的竞争性紧急订单:** 两家顶级汽车OEM客户都要求加急交付。满足其中一家会延迟另一家。需要商业决策输入——哪家客户关系具有更高的违约风险或战略价值?计划员识别权衡;管理层做决定。
|
||||
|
||||
4. **BOM错误导致的MRP虚假需求:** BOM清单错误导致MRP生成了未被实际消耗的组件的计划订单。计划员看到一个背后没有真实需求的工单。通过交叉引用MRP生成的需求与实际销售订单和预测消耗来检测。标记并搁置——不要安排虚假需求。
|
||||
|
||||
5. **影响下游的在制品质量扣留:** 在200个部分完成的组件上发现油漆缺陷。这些组件原计划明天供给最终装配瓶颈。除非从早期阶段加急替换在制品或使用替代工艺路线,否则瓶颈将闲置。
|
||||
|
||||
6. **瓶颈设备故障:** 最具破坏性的中断。瓶颈每分钟的停机时间都等于整个工厂的产出损失。触发即时维护响应,如果可用则激活替代路线,并通知订单面临风险的客户。
|
||||
|
||||
7. **供应商在运行中途交付错误物料:** 一批钢材到货,但合金规格错误。已用此物料备料的作业无法进行。隔离该物料,重新排序以提前使用不同合金的作业,并升级至采购部门寻求紧急替换。
|
||||
|
||||
8. **生产开始后客户订单变更:** 客户在工作进行过程中修改数量或规格。评估已完工作的沉没成本、返工可行性以及对共享相同资源的其他作业的影响。部分完工暂停可能比报废和重新开始成本更低。
|
||||
|
||||
## 沟通模式
|
||||
|
||||
### 语气校准
|
||||
|
||||
* **每日计划发布:** 清晰、结构化、无歧义。作业顺序、开始时间、产线分配、操作员分配。使用表格格式。车间不阅读段落。
|
||||
* **计划变更通知:** 紧急标题、变更原因、受影响的特定作业、新的顺序和时间。"立即生效"或"于\[时间]生效"。
|
||||
* **中断升级:** 首先说明影响程度(损失的约束工时数、受影响的客户订单数量),然后是原因、提议的应对措施,最后是管理层需要做出的决策。
|
||||
* **加班请求:** 量化业务依据——加班成本与错过交付的成本。包括工会规则合规性。"请求周六上午CNC操作员(3人)4小时自愿加班。成本:$1,200。不加班的风险收入:$45,000。"
|
||||
* **客户交付影响通知:** 切勿让客户感到意外。一旦可能出现延迟,立即通知新的预计日期、根本原因(不归咎于内部团队)以及恢复计划。"由于设备问题,订单#12345将于\[新日期]发货,而非原定的\[原日期]。我们正在安排加班以尽量减少延迟。"
|
||||
* **维护协调:** 请求的具体时间窗口、选择该时间的业务理由、推迟维护的影响。"请求3号线在周二06:00–10:00进行预防性维护。这避开了周四的换产高峰。推迟到周五之后存在非计划性故障的风险——振动读数已呈上升趋势进入警戒区。"
|
||||
|
||||
以上为简要模板。在用于生产环境前,请根据您的工厂、计划员和客户承诺流程进行调整。
|
||||
|
||||
## 升级协议
|
||||
|
||||
### 自动升级触发器
|
||||
|
||||
| 触发器 | 行动 | 时间线 |
|
||||
|---|---|---|
|
||||
| 约束工作中心意外停机 > 30 分钟 | 通知生产经理 + 维护经理 | 立即 |
|
||||
| 计划遵守率一个班次内低于 80% | 与班次主管进行根本原因分析 | 4 小时内 |
|
||||
| 客户订单预计错过承诺发货日期 | 通知销售和客户服务部门,并提供修订后的预计到达时间 | 发现后 2 小时内 |
|
||||
| 加班需求超过周预算 > 20% | 将成本效益分析上报给工厂经理 | 1 个工作日内 |
|
||||
| 约束工序的OEE连续3个班次低于 65% | 触发重点改进活动(维护 + 工程 + 计划) | 1 周内 |
|
||||
| 约束工序的质量合格率低于 93% | 与质量工程部门联合审查 | 24 小时内 |
|
||||
| MRP生成的负载在下周超过有限产能 > 15% | 与计划和生产管理部门召开产能会议 | 超负荷周开始前 2 天 |
|
||||
|
||||
### 升级链
|
||||
|
||||
级别 1(生产计划员)→ 级别 2(生产经理/班次主管,约束问题30分钟,非约束问题4小时)→ 级别 3(工厂经理,影响客户的问题2小时)→ 级别 4(运营副总裁,影响多个客户或与安全相关的计划变更需当日处理)
|
||||
|
||||
## 绩效指标
|
||||
|
||||
按班次跟踪并每周统计趋势:
|
||||
|
||||
| 指标 | 目标 | 红色警报 |
|
||||
|---|---|---|
|
||||
| 计划遵守率(作业在±1小时内开始) | > 90% | < 80% |
|
||||
| 准时交付率(按客户承诺日期) | > 95% | < 90% |
|
||||
| 约束工序的综合设备效率 | > 75% | < 65% |
|
||||
| 换产时间 vs. 标准 | < 标准时间的 110% | > 标准时间的 130% |
|
||||
| 在制品天数(总在制品价值 / 每日销售成本) | < 5 天 | > 8 天 |
|
||||
| 约束工序利用率(实际生产时间 / 可用时间) | > 85% | < 75% |
|
||||
| 约束工序一次合格率 | > 97% | < 93% |
|
||||
| 非计划停机时间(占计划时间的百分比) | < 5% | > 10% |
|
||||
| 人工利用率(直接工时 / 可用工时) | 80–90% | < 70% 或 > 95% |
|
||||
|
||||
## 补充资源
|
||||
|
||||
* 将此技能与您的约束层次结构、计划冻结窗口策略和加急批准阈值结合使用。
|
||||
* 在工作流程旁记录实际计划遵守失败情况及根本原因,以便排序规则随时间改进。
|
||||
378
docs/zh-CN/skills/prompt-optimizer/SKILL.md
Normal file
378
docs/zh-CN/skills/prompt-optimizer/SKILL.md
Normal file
@@ -0,0 +1,378 @@
|
||||
---
|
||||
name: prompt-optimizer
|
||||
description: 分析原始提示,识别意图和差距,匹配ECC组件(技能/命令/代理/钩子),并输出一个可直接粘贴的优化提示。仅提供咨询角色——绝不自行执行任务。触发时机:当用户说“优化提示”、“改进我的提示”、“如何编写提示”、“帮我优化这个指令”或明确要求提高提示质量时。中文等效表达同样触发:“优化prompt”、“改进prompt”、“怎么写prompt”、“帮我优化这个指令”。不触发时机:当用户希望直接执行任务,或说“直接做”时。不触发时机:当用户说“优化代码”、“优化性能”、“optimize performance”、“optimize this code”时——这些是重构/性能优化任务,而非提示优化。origin: community
|
||||
metadata:
|
||||
author: YannJY02
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Prompt 优化器
|
||||
|
||||
分析一个草稿提示,对其进行评估,匹配到 ECC 生态系统组件,并输出一个完整的优化提示供用户复制粘贴并运行。
|
||||
|
||||
## 何时使用
|
||||
|
||||
* 用户说“优化这个提示”、“改进我的提示”、“重写这个提示”
|
||||
* 用户说“帮我写一个更好的提示来...”
|
||||
* 用户说“询问 Claude Code 的...最佳方式是什么?”
|
||||
* 用户说“优化prompt”、“改进prompt”、“怎么写prompt”、“帮我优化这个指令”
|
||||
* 用户粘贴一个草稿提示并要求反馈或改进
|
||||
* 用户说“我不知道如何为此编写提示”
|
||||
* 用户说“我应该如何使用 ECC 来...”
|
||||
* 用户明确调用 `/prompt-optimize`
|
||||
|
||||
### 不要用于
|
||||
|
||||
* 用户希望直接执行任务(直接执行即可)
|
||||
* 用户说“优化代码”、“优化性能”、“optimize this code”、“optimize performance”——这些是重构任务,不是提示优化
|
||||
* 用户询问 ECC 配置(改用 `configure-ecc`)
|
||||
* 用户想要技能清单(改用 `skill-stocktake`)
|
||||
* 用户说“直接做”或“just do it”
|
||||
|
||||
## 工作原理
|
||||
|
||||
**仅提供建议——不要执行用户的任务。**
|
||||
|
||||
不要编写代码、创建文件、运行命令或采取任何实现行动。你的**唯一**输出是分析加上一个优化后的提示。
|
||||
|
||||
如果用户说“直接做”、“just do it”或“不要优化,直接执行”,不要在此技能内切换到实现模式。告诉用户此技能只生成优化提示,并指示他们如果要执行任务,请提出正常的任务请求。
|
||||
|
||||
按顺序运行这个 6 阶段流程。使用下面的输出格式呈现结果。
|
||||
|
||||
### 分析流程
|
||||
|
||||
### 阶段 0:项目检测
|
||||
|
||||
在分析提示之前,检测当前项目上下文:
|
||||
|
||||
1. 检查工作目录中是否存在 `CLAUDE.md`——读取它以了解项目惯例
|
||||
2. 从项目文件中检测技术栈:
|
||||
* `package.json` → Node.js / TypeScript / React / Next.js
|
||||
* `go.mod` → Go
|
||||
* `pyproject.toml` / `requirements.txt` → Python
|
||||
* `Cargo.toml` → Rust
|
||||
* `build.gradle` / `pom.xml` → Java / Kotlin / Spring Boot
|
||||
* `Package.swift` → Swift
|
||||
* `Gemfile` → Ruby
|
||||
* `composer.json` → PHP
|
||||
* `*.csproj` / `*.sln` → .NET
|
||||
* `Makefile` / `CMakeLists.txt` → C / C++
|
||||
* `cpanfile` / `Makefile.PL` → Perl
|
||||
3. 记录检测到的技术栈,用于阶段 3 和阶段 4
|
||||
|
||||
如果未找到项目文件(例如,提示是抽象的或用于新项目),则跳过检测并在阶段 4 标记“技术栈未知”。
|
||||
|
||||
### 阶段 1:意图检测
|
||||
|
||||
将用户的任务分类为一个或多个类别:
|
||||
|
||||
| 类别 | 信号词 | 示例 |
|
||||
|----------|-------------|---------|
|
||||
| 新功能 | build, create, add, implement, 创建, 实现, 添加 | "Build a login page" |
|
||||
| 错误修复 | fix, broken, not working, error, 修复, 报错 | "Fix the auth flow" |
|
||||
| 重构 | refactor, clean up, restructure, 重构, 整理 | "Refactor the API layer" |
|
||||
| 研究 | how to, what is, explore, investigate, 怎么, 如何 | "How to add SSO" |
|
||||
| 测试 | test, coverage, verify, 测试, 覆盖率 | "Add tests for the cart" |
|
||||
| 审查 | review, audit, check, 审查, 检查 | "Review my PR" |
|
||||
| 文档 | document, update docs, 文档 | "Update the API docs" |
|
||||
| 基础设施 | deploy, CI, docker, database, 部署, 数据库 | "Set up CI/CD pipeline" |
|
||||
| 设计 | design, architecture, plan, 设计, 架构 | "Design the data model" |
|
||||
|
||||
### 阶段 2:范围评估
|
||||
|
||||
如果阶段 0 检测到项目,则使用代码库大小作为信号。否则,仅根据提示描述进行估算,并将估算标记为不确定。
|
||||
|
||||
| 范围 | 启发式判断 | 编排 |
|
||||
|-------|-----------|---------------|
|
||||
| 微小 | 单个文件,< 50 行 | 直接执行 |
|
||||
| 低 | 单个组件或模块 | 单个命令或技能 |
|
||||
| 中 | 多个组件,同一领域 | 命令链 + /verify |
|
||||
| 高 | 跨领域,5+ 个文件 | 先使用 /plan,然后分阶段执行 |
|
||||
| 史诗级 | 多会话,多 PR,架构性变更 | 使用蓝图技能制定多会话计划 |
|
||||
|
||||
### 阶段 3:ECC 组件匹配
|
||||
|
||||
将意图 + 范围 + 技术栈(来自阶段 0)映射到特定的 ECC 组件。
|
||||
|
||||
#### 按意图类型
|
||||
|
||||
| 意图 | 命令 | 技能 | 代理 |
|
||||
|--------|----------|--------|--------|
|
||||
| 新功能 | /plan, /tdd, /code-review, /verify | tdd-workflow, verification-loop | planner, tdd-guide, code-reviewer |
|
||||
| 错误修复 | /tdd, /build-fix, /verify | tdd-workflow | tdd-guide, build-error-resolver |
|
||||
| 重构 | /refactor-clean, /code-review, /verify | verification-loop | refactor-cleaner, code-reviewer |
|
||||
| 研究 | /plan | search-first, iterative-retrieval | — |
|
||||
| 测试 | /tdd, /e2e, /test-coverage | tdd-workflow, e2e-testing | tdd-guide, e2e-runner |
|
||||
| 审查 | /code-review | security-review | code-reviewer, security-reviewer |
|
||||
| 文档 | /update-docs, /update-codemaps | — | doc-updater |
|
||||
| 基础设施 | /plan, /verify | docker-patterns, deployment-patterns, database-migrations | architect |
|
||||
| 设计 (中-高) | /plan | — | planner, architect |
|
||||
| 设计 (史诗级) | — | blueprint (作为技能调用) | planner, architect |
|
||||
|
||||
#### 按技术栈
|
||||
|
||||
| 技术栈 | 要添加的技能 | 代理 |
|
||||
|------------|--------------|-------|
|
||||
| Python / Django | django-patterns, django-tdd, django-security, django-verification, python-patterns, python-testing | python-reviewer |
|
||||
| Go | golang-patterns, golang-testing | go-reviewer, go-build-resolver |
|
||||
| Spring Boot / Java | springboot-patterns, springboot-tdd, springboot-security, springboot-verification, java-coding-standards, jpa-patterns | code-reviewer |
|
||||
| Kotlin / Android | kotlin-coroutines-flows, compose-multiplatform-patterns, android-clean-architecture | kotlin-reviewer |
|
||||
| TypeScript / React | frontend-patterns, backend-patterns, coding-standards | code-reviewer |
|
||||
| Swift / iOS | swiftui-patterns, swift-concurrency-6-2, swift-actor-persistence, swift-protocol-di-testing | code-reviewer |
|
||||
| PostgreSQL | postgres-patterns, database-migrations | database-reviewer |
|
||||
| Perl | perl-patterns, perl-testing, perl-security | code-reviewer |
|
||||
| C++ | cpp-coding-standards, cpp-testing | code-reviewer |
|
||||
| 其他 / 未列出 | coding-standards (通用) | code-reviewer |
|
||||
|
||||
### 阶段 4:缺失上下文检测
|
||||
|
||||
扫描提示中缺失的关键信息。检查每个项目,并标记是阶段 0 自动检测到的还是用户必须提供的:
|
||||
|
||||
* \[ ] **技术栈** —— 阶段 0 检测到的,还是用户必须指定?
|
||||
* \[ ] **目标范围** —— 提到了文件、目录或模块吗?
|
||||
* \[ ] **验收标准** —— 如何知道任务已完成?
|
||||
* \[ ] **错误处理** —— 是否考虑了边界情况和故障模式?
|
||||
* \[ ] **安全要求** —— 身份验证、输入验证、密钥?
|
||||
* \[ ] **测试期望** —— 单元测试、集成测试、E2E?
|
||||
* \[ ] **性能约束** —— 负载、延迟、资源限制?
|
||||
* \[ ] **UI/UX 要求** —— 设计规范、响应式、无障碍访问?(如果是前端)
|
||||
* \[ ] **数据库变更** —— 模式、迁移、索引?(如果是数据层)
|
||||
* \[ ] **现有模式** —— 要遵循的参考文件或惯例?
|
||||
* \[ ] **范围边界** —— 什么**不要**做?
|
||||
|
||||
**如果缺少 3 个以上关键项目**,则在生成优化提示之前询问用户最多 3 个澄清问题。然后将答案纳入优化提示中。
|
||||
|
||||
### 阶段 5:工作流和模型推荐
|
||||
|
||||
确定此提示在开发生命周期中的位置:
|
||||
|
||||
```
|
||||
Research → Plan → Implement (TDD) → Review → Verify → Commit
|
||||
```
|
||||
|
||||
对于中等级别及以上的任务,始终以 /plan 开始。对于史诗级任务,使用蓝图技能。
|
||||
|
||||
**模型推荐**(包含在输出中):
|
||||
|
||||
| 范围 | 推荐模型 | 理由 |
|
||||
|-------|------------------|-----------|
|
||||
| 微小-低 | Sonnet 4.6 | 快速、成本效益高,适合简单任务 |
|
||||
| 中 | Sonnet 4.6 | 标准工作的最佳编码模型 |
|
||||
| 高 | Sonnet 4.6 (主) + Opus 4.6 (规划) | Opus 用于架构,Sonnet 用于实现 |
|
||||
| 史诗级 | Opus 4.6 (蓝图) + Sonnet 4.6 (执行) | 深度推理用于多会话规划 |
|
||||
|
||||
**多提示拆分**(针对高/史诗级范围):
|
||||
|
||||
对于超出单个会话的任务,拆分为顺序提示:
|
||||
|
||||
* 提示 1:研究 + 计划(使用 search-first 技能,然后 /plan)
|
||||
* 提示 2-N:每个提示实现一个阶段(每个阶段以 /verify 结束)
|
||||
* 最终提示:集成测试 + 跨所有阶段的 /code-review
|
||||
* 使用 /save-session 和 /resume-session 在会话之间保存上下文
|
||||
|
||||
***
|
||||
|
||||
## 输出格式
|
||||
|
||||
按照此确切结构呈现你的分析。使用与用户输入相同的语言进行回应。
|
||||
|
||||
### 第 1 部分:提示诊断
|
||||
|
||||
**优点:** 列出原始提示做得好的地方。
|
||||
|
||||
**问题:**
|
||||
|
||||
| 问题 | 影响 | 建议的修复方法 |
|
||||
|-------|--------|---------------|
|
||||
| (问题) | (后果) | (如何修复) |
|
||||
|
||||
**需要澄清:** 用户应回答的问题编号列表。如果阶段 0 自动检测到答案,请陈述该答案而不是提问。
|
||||
|
||||
### 第 2 部分:推荐的 ECC 组件
|
||||
|
||||
| 类型 | 组件 | 目的 |
|
||||
|------|-----------|---------|
|
||||
| 命令 | /plan | 编码前规划架构 |
|
||||
| 技能 | tdd-workflow | TDD 方法指导 |
|
||||
| 代理 | code-reviewer | 实施后审查 |
|
||||
| 模型 | Sonnet 4.6 | 针对此范围的推荐模型 |
|
||||
|
||||
### 第 3 部分:优化提示 —— 完整版本
|
||||
|
||||
在单个围栏代码块内呈现完整的优化提示。该提示必须是自包含的,可以复制粘贴。包括:
|
||||
|
||||
* 清晰的任务描述和上下文
|
||||
* 技术栈(检测到的或指定的)
|
||||
* 在正确工作流阶段调用的 /command
|
||||
* 验收标准
|
||||
* 验证步骤
|
||||
* 范围边界(什么**不要**做)
|
||||
|
||||
对于引用蓝图的项目,写成:“使用蓝图技能来...”(而不是 `/blueprint`,因为蓝图是技能,不是命令)。
|
||||
|
||||
### 第 4 部分:优化提示 —— 快速版本
|
||||
|
||||
为有经验的 ECC 用户提供的紧凑版本。根据意图类型而变化:
|
||||
|
||||
| 意图 | 快速模式 |
|
||||
|--------|--------------|
|
||||
| 新功能 | `/plan [feature]. /tdd to implement. /code-review. /verify.` |
|
||||
| 错误修复 | `/tdd — write failing test for [bug]. Fix to green. /verify.` |
|
||||
| 重构 | `/refactor-clean [scope]. /code-review. /verify.` |
|
||||
| 研究 | `Use search-first skill for [topic]. /plan based on findings.` |
|
||||
| 测试 | `/tdd [module]. /e2e for critical flows. /test-coverage.` |
|
||||
| 审查 | `/code-review. Then use security-reviewer agent.` |
|
||||
| 文档 | `/update-docs. /update-codemaps.` |
|
||||
| 史诗级 | `Use blueprint skill for "[objective]". Execute phases with /verify gates.` |
|
||||
|
||||
### 第 5 部分:改进理由
|
||||
|
||||
| 改进 | 理由 |
|
||||
|-------------|--------|
|
||||
| (添加了什么) | (为什么重要) |
|
||||
|
||||
### 页脚
|
||||
|
||||
> 不符合你的需求?告诉我需要调整什么,或者如果你想执行任务而不是优化提示,请提出正常的任务请求。
|
||||
|
||||
***
|
||||
|
||||
## 示例
|
||||
|
||||
### 触发示例
|
||||
|
||||
* "Optimize this prompt for ECC"
|
||||
* "Rewrite this prompt so Claude Code uses the right commands"
|
||||
* "帮我优化这个指令"
|
||||
* "How should I prompt ECC for this task?"
|
||||
|
||||
### 示例 1:模糊的中文提示(检测到项目)
|
||||
|
||||
**用户输入:**
|
||||
|
||||
```
|
||||
帮我写一个用户登录页面
|
||||
```
|
||||
|
||||
**阶段 0 检测到:** `package.json`,使用 Next.js 15, TypeScript, Tailwind CSS
|
||||
|
||||
**优化提示(完整):**
|
||||
|
||||
```
|
||||
使用项目现有技术栈(Next.js 15 + TypeScript + Tailwind CSS)实现用户登录页面。
|
||||
|
||||
技术要求:
|
||||
- 沿用项目现有的组件结构和路由约定
|
||||
- 表单验证使用项目中已有的验证方案(检查是否已用 Zod/Yup/其他)
|
||||
- 认证方式:沿用项目现有认证方案(如无,默认 JWT)
|
||||
- 包含:邮箱/密码登录表单、表单验证、错误提示、加载状态、响应式布局
|
||||
|
||||
工作流:
|
||||
1. /plan 先规划组件结构和认证流程,参考现有页面的模式
|
||||
2. /tdd 测试先行:编写登录表单的单元测试和认证流程的集成测试
|
||||
3. 实现登录页面和认证逻辑
|
||||
4. /code-review 审查实现
|
||||
5. /verify 验证所有测试通过且页面正常渲染
|
||||
|
||||
安全要求:
|
||||
- 密码不明文传输
|
||||
- 防止暴力破解(rate limiting)
|
||||
- XSS 防护
|
||||
- CSRF token
|
||||
|
||||
验收标准:
|
||||
- 所有测试通过,覆盖率 80%+
|
||||
- 页面在移动端和桌面端正常渲染
|
||||
- 登录成功跳转到 dashboard,失败显示错误信息
|
||||
|
||||
不要做:
|
||||
- 不要实现注册页面
|
||||
- 不要实现忘记密码功能
|
||||
- 不要修改现有的路由结构
|
||||
```
|
||||
|
||||
### 示例 2:中等英文提示
|
||||
|
||||
**用户输入:**
|
||||
|
||||
```
|
||||
Add a REST API endpoint for user profile updates with validation
|
||||
```
|
||||
|
||||
**阶段 0 检测到:** `go.mod`,使用 Go 1.22, Chi router
|
||||
|
||||
**优化提示(完整):**
|
||||
|
||||
```
|
||||
Add a REST API endpoint for user profile updates (PATCH /api/users/:id).
|
||||
|
||||
Tech stack: Go 1.22 + Chi router (detected from project)
|
||||
|
||||
Requirements:
|
||||
- PATCH /api/users/:id — partial update of user profile
|
||||
- Input validation for fields: name, email, avatar_url, bio
|
||||
- Auth: require valid token, users can only update own profile
|
||||
- Return 200 with updated user on success
|
||||
- Return 400 with validation errors on invalid input
|
||||
- Return 401/403 for auth failures
|
||||
- Follow existing API patterns in the codebase
|
||||
|
||||
Workflow:
|
||||
1. /plan the endpoint structure, middleware chain, and validation logic
|
||||
2. /tdd — write table-driven tests for success, validation failure, auth failure, not-found
|
||||
3. Implement following existing handler patterns
|
||||
4. /go-review
|
||||
5. /verify — run full test suite, confirm no regressions
|
||||
|
||||
Do not:
|
||||
- Modify existing endpoints
|
||||
- Change the database schema (use existing user table)
|
||||
- Add new dependencies without checking existing ones first (use search-first skill)
|
||||
```
|
||||
|
||||
### 示例 3:史诗级项目
|
||||
|
||||
**用户输入:**
|
||||
|
||||
```
|
||||
Migrate our monolith to microservices
|
||||
```
|
||||
|
||||
**优化提示(完整):**
|
||||
|
||||
```
|
||||
Use the blueprint skill to plan: "Migrate monolith to microservices architecture"
|
||||
|
||||
Before executing, answer these questions in the blueprint:
|
||||
1. Which domain boundaries exist in the current monolith?
|
||||
2. Which service should be extracted first (lowest coupling)?
|
||||
3. Communication pattern: REST APIs, gRPC, or event-driven (Kafka/RabbitMQ)?
|
||||
4. Database strategy: shared DB initially or database-per-service from start?
|
||||
5. Deployment target: Kubernetes, Docker Compose, or serverless?
|
||||
|
||||
The blueprint should produce phases like:
|
||||
- Phase 1: Identify service boundaries and create domain map
|
||||
- Phase 2: Set up infrastructure (API gateway, service mesh, CI/CD per service)
|
||||
- Phase 3: Extract first service (strangler fig pattern)
|
||||
- Phase 4: Verify with integration tests, then extract next service
|
||||
- Phase N: Decommission monolith
|
||||
|
||||
Each phase = 1 PR, with /verify gates between phases.
|
||||
Use /save-session between phases. Use /resume-session to continue.
|
||||
Use git worktrees for parallel service extraction when dependencies allow.
|
||||
|
||||
Recommended: Opus 4.6 for blueprint planning, Sonnet 4.6 for phase execution.
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 相关组件
|
||||
|
||||
| 组件 | 何时引用 |
|
||||
|-----------|------------------|
|
||||
| `configure-ecc` | 用户尚未设置 ECC |
|
||||
| `skill-stocktake` | 审计安装了哪些组件(使用它而不是硬编码的目录) |
|
||||
| `search-first` | 优化提示中的研究阶段 |
|
||||
| `blueprint` | 史诗级范围的优化提示(作为技能调用,而非命令) |
|
||||
| `strategic-compact` | 长会话上下文管理 |
|
||||
| `cost-aware-llm-pipeline` | Token 优化推荐 |
|
||||
252
docs/zh-CN/skills/quality-nonconformance/SKILL.md
Normal file
252
docs/zh-CN/skills/quality-nonconformance/SKILL.md
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
name: quality-nonconformance
|
||||
description: 为受监管制造业中的质量控制、不合格调查、根本原因分析、纠正措施和供应商质量管理提供编码化专业知识。基于在FDA、IATF 16949和AS9100环境中拥有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年以上受监管制造环境经验的高级质量工程师——涉及FDA 21 CFR 820(医疗器械)、IATF 16949(汽车)、AS9100(航空航天)和ISO 13485(医疗器械)。您管理从不合格品入厂检验到最终处置的完整生命周期。您使用的系统包括QMS(eQMS平台,如MasterControl、ETQ、Veeva)、SPC软件(Minitab、InfinityQS)、ERP(SAP QM、Oracle Quality)、CMM和计量设备,以及供应商门户。您处于制造、工程、采购、法规和客户质量的交汇点。您的判断直接影响产品安全、法规合规性、生产吞吐量和供应商关系。
|
||||
|
||||
## 使用时机
|
||||
|
||||
* 调查入厂检验、过程中或最终测试中出现的不合格品(NCR)
|
||||
* 使用5个为什么、石川图或故障树方法进行根本原因分析
|
||||
* 确定不合格品的处置方式(按现状使用、返工、报废、退回供应商)
|
||||
* 创建或评审CAPA(纠正与预防措施)计划
|
||||
* 解读SPC数据和控制图信号以评估过程稳定性
|
||||
* 准备或回应法规审核发现项
|
||||
|
||||
## 运作方式
|
||||
|
||||
1. 通过检验、SPC警报或客户投诉发现不合格品
|
||||
2. 立即隔离受影响物料(隔离、生产暂停、停止发货)
|
||||
3. 根据安全影响和法规要求对严重程度进行分类(严重、主要、次要)
|
||||
4. 使用适合复杂程度的结构化方法调查根本原因
|
||||
5. 基于工程评估、法规限制和经济效益确定处置方式
|
||||
6. 实施纠正措施,验证有效性,并附上证据关闭CAPA
|
||||
|
||||
## 示例
|
||||
|
||||
* **入厂检验失败**:一批10,000个注塑组件在二级AQL抽样中不合格。缺陷是某个关键功能特征的尺寸偏差为+0.15mm。演练隔离、通知供应商、根本原因调查(模具磨损)、跳批暂停和SCAR签发。
|
||||
* **SPC信号解读**:灌装线上的X-bar图显示连续9个点高于中心线(西电规则2)。过程仍处于规格限内。确定是停止生产线(调查可查明原因)还是继续生产(并解释为什么“符合规格”不等于“受控”)。
|
||||
* **客户投诉CAPA**:汽车OEM客户报告500个单元中有3个现场故障,均具有相同的故障模式。构建8D报告,执行故障树分析,识别最终测试中的逃逸点,并为纠正措施设计验证测试。
|
||||
|
||||
## 核心知识
|
||||
|
||||
### NCR生命周期
|
||||
|
||||
每个不合格品都遵循一个受控的生命周期。跳过步骤会产生审核发现项和法规风险:
|
||||
|
||||
* **识别**:任何人都可以发起。记录:谁发现的、在哪里(入厂、过程中、最终、现场)、违反了哪个标准/规范、影响数量、批次可追溯性。立即标记或隔离不合格品物料——无一例外。在指定的MRB区域进行物理隔离并贴上红标签或保留标签。在ERP中进行电子保留以防止无意中发货。
|
||||
* **记录**:根据您的QMS编号方案分配NCR编号。链接到零件号、版本、采购单/工单、违反的规范条款、测量数据(实际值 vs. 公差)、照片和检验员ID。对于FDA监管的产品,记录必须满足21 CFR 820.90;对于汽车行业,需满足IATF 16949 §8.7。
|
||||
* **调查**:确定范围——这是一个孤立的问题还是系统性的批次问题?检查上游和下游:同一供应商发货的其他批次、同一生产运行的其他单元、同一时期的在制品和成品库存。必须在开始根本原因分析之前采取隔离措施。
|
||||
* **通过MRB(物料评审委员会)处置**:MRB通常包括质量、工程和制造代表。对于航空航天(AS9100),客户可能需要参与。处置选项:
|
||||
* **按现状使用**:零件不符合图纸但在功能上可接受。需要工程理由(让步/偏差)。在航空航天领域,需要客户根据AS9100 §8.7.1批准。在汽车领域,通常需要通知客户。记录理由——“因为我们需要这些零件”不是正当理由。
|
||||
* **返工**:使用批准的返工程序使零件符合要求。返工指令必须记录在案,返工后的零件必须按照原始规范重新检验。跟踪返工成本。
|
||||
* **修理**:零件将不完全符合原始规格,但将被修复为可用。需要工程处置,并且通常需要客户让步。与返工不同——修理接受永久性偏差。
|
||||
* **退回供应商(RTV)**:发出供应商纠正措施请求(SCAR)或CAR。借记通知单或更换采购单。在约定的时间范围内跟踪供应商响应。更新供应商记分卡。
|
||||
* **报废**:记录报废数量、成本、批次可追溯性以及授权的报废批准(通常需要超过一定金额阈度的管理层签字)。对于序列化或安全关键零件,需见证销毁。
|
||||
|
||||
### 根本原因分析
|
||||
|
||||
在症状层面停止是质量调查中最常见的失败模式:
|
||||
|
||||
* **5个为什么**:简单,适用于直接的过程故障。局限性:假设单一的线性因果链。在处理复杂的多因素问题时失效。每个“为什么”必须用数据而非观点来验证——“为什么尺寸漂移?”→“因为工具磨损了”只有在测量了工具磨损后才有效。
|
||||
* **石川图(鱼骨图)**:使用6M框架(人、机、料、法、测、环)。强制考虑所有潜在原因类别。作为头脑风暴框架最有用,可防止过早地集中于单一原因。其本身不是根本原因工具——它产生需要验证的假设。
|
||||
* **故障树分析(FTA)**:自上而下,演绎法。从故障事件开始,使用AND/OR逻辑门分解为促成原因。当有故障率数据时可以进行量化。在航空航天(AS9100)和医疗器械(ISO 14971风险分析)环境中是必需或预期的。最严谨的方法,但资源密集。
|
||||
* **8D方法论**:基于团队的、结构化的问题解决方法。D0:症状识别和应急响应。D1:团队组建。D2:问题定义(是/不是)。D3:临时遏制。D4:根本原因识别(在8D内使用鱼骨图+5个为什么)。D5:纠正措施选择。D6:实施。D7:防止再发生。D8:团队表彰。汽车OEM(通用、福特、Stellantis)期望针对重大的供应商质量问题提交8D报告。
|
||||
* **表明您在症状层面停止的危险信号**:您的“根本原因”包含“错误”一词(人为错误从来不是根本原因——为什么系统允许了错误?),您的纠正措施是“重新培训操作员”(仅靠培训是最弱的纠正措施),或者您的根本原因只是问题陈述的改写。
|
||||
|
||||
### CAPA系统
|
||||
|
||||
CAPA是法规的支柱。FDA引用CAPA缺陷的次数多于任何其他子系统:
|
||||
|
||||
* **启动**:并非每个NCR都需要CAPA。触发因素:重复的不合格品(相同故障模式3次以上)、客户投诉、审核发现项、现场故障、趋势分析(SPC信号)、法规观察项。过度启动CAPA会稀释资源并造成积压。启动不足则会产生审核发现项。
|
||||
* **纠正措施 vs. 预防措施**:纠正措施针对已存在的不合格品并防止其再次发生。预防措施针对尚未发生的潜在不合格品——通常通过趋势分析、风险评估或未遂事件识别。FDA期望两者都有;不要混淆它们。
|
||||
* **撰写有效的CAPA**:措施必须具体、可衡量,并针对已验证的根本原因。不好的例子:“改进检验程序。”好的例子:“在工位12增加扭矩验证步骤,使用校准的扭矩扳手(±2%),记录在流转单检查表WI-4401 Rev C上,于2025-04-15前生效。”每个CAPA必须有一个负责人、一个目标日期和明确的完成证据。
|
||||
* **有效性验证 vs. 有效性确认**:验证确认措施按计划实施(我们安装了防错夹具吗?)。确认确认措施确实防止了再次发生(在90天的生产数据中,缺陷率是否降至零?)。FDA期望两者兼备。在验证阶段关闭CAPA而未进行确认是常见的审核发现项。
|
||||
* **关闭标准**:纠正措施已实施且有效的客观证据。最低有效性监控期:过程变更90天,材料变更3个生产批次,或系统变更的下一个审核周期。记录有效性数据——图表、拒收率、审核结果。
|
||||
* **法规期望**:FDA 21 CFR 820.198(投诉处理)和820.90(不合格品)输入到820.100(CAPA)。IATF 16949 §10.2.3-10.2.6。AS9100 §10.2。ISO 13485 §8.5.2-8.5.3。每个标准都有具体的文件记录和时限期望。
|
||||
|
||||
### 统计过程控制(SPC)
|
||||
|
||||
SPC将信号与噪音分离。误读图表比根本不使用图表造成更多问题:
|
||||
|
||||
* **图表选择**:X-bar/R用于具有子组的连续数据(n=2-10)。X-bar/S用于子组 n>10。单值-移动极差图(I-MR)用于子组 n=1 的连续数据(批次过程、破坏性测试)。p图用于不合格品比例(可变样本量)。np图用于不合格品数量(固定样本量)。c图用于单位缺陷数(固定机会区域)。u图用于单位缺陷数(可变机会区域)。
|
||||
* **能力指数**:Cp衡量过程散布与规格宽度的对比(潜在能力)。Cpk根据中心位置进行调整(实际能力)。Pp/Ppk使用总变差(长期)与Cp/Cpk(使用子组内变差,短期)对比。一个Cp=2.0但Cpk=0.8的过程是有能力的但未居中——修正均值,而非变差。汽车行业(IATF 16949)通常要求已建立过程的Cpk ≥ 1.33,新过程的Ppk ≥ 1.67。
|
||||
* **西电规则(超出控制限的信号)**:规则1:一个点超出3σ。规则2:连续9个点位于中心线同一侧。规则3:连续6个点持续上升或下降。规则4:连续14个点交替上下。规则1要求立即采取行动。规则2-4表明存在系统性原因,需要在过程超出规格限之前进行调查。
|
||||
* **过度调整问题**:通过调整过程来应对普通原因变异会增加变异性——这就是干预。如果图表显示过程稳定且在控制限内,但个别点“看起来偏高”,请不要调整。仅针对西电规则确认的特殊原因信号进行调整。
|
||||
* **普通原因 vs. 特殊原因**:普通原因变异是过程固有的——减少它需要根本性的过程变更(更好的设备、不同的材料、环境控制)。特殊原因变异可归因于特定事件——磨损的工具、新的原材料批次、第二班未经培训的操作员。SPC的主要功能是快速检测特殊原因。
|
||||
|
||||
### 入厂检验
|
||||
|
||||
* **AQL抽样方案(ANSI/ASQ Z1.4 / ISO 2859-1):** 确定检验水平(I、II、III——II级为标准水平)、批量、AQL值以及样本量字码。加严检验:连续5批中有2批被拒收后转换。正常检验:默认状态。放宽检验:连续10批被接收且生产稳定后转换。致命缺陷:AQL = 0,并采用相应的样本量。主要缺陷:通常AQL为1.0-2.5。次要缺陷:通常AQL为2.5-6.5。
|
||||
* **LTPD(批容许不良品率):** 抽样方案设计为要拒收的缺陷水平。AQL保护生产者(拒收好批的风险低)。LTPD保护消费者(接收坏批的风险低)。理解双方对于向管理层传达检验风险至关重要。
|
||||
* **跳批检验资格:** 供应商证明质量持续稳定(通常在正常检验下连续10批以上被接收)后,可将检验频率降低为每2批、3批或5批检验一次。任何一批被拒收则立即恢复原检验频率。需要正式的资格标准和文件化的决策。
|
||||
* **符合性证书依赖:** 何时信任供应商的CoC与执行来料检验:新供应商 = 始终检验;有历史的合格供应商 = CoC + 减少验证;关键/安全尺寸 = 无论历史如何,始终检验。依赖CoC需要文件化的协议和定期审核验证(审核供应商的最终检验过程,而不仅仅是文件)。
|
||||
|
||||
### 供应商质量管理
|
||||
|
||||
* **审核方法:** 过程审核评估工作执行方式(观察、访谈、抽样)。体系审核评估质量管理体系符合性(文件审查、记录抽样)。产品审核验证特定产品特性。使用基于风险的审核计划——高风险供应商每年一次,中等风险每两年一次,低风险每三年一次,外加基于原因的审核。体系评估采用通知审核;存在绩效问题时,过程验证可采用不通知审核。
|
||||
* **供应商记分卡:** 衡量PPM(每百万件不良品数)、准时交付率、SCAR响应时间、SCAR有效性(复发率)以及批接收率。根据业务影响对指标进行加权。每季度分享记分卡。分数驱动检验水平调整、业务分配和ASL状态。
|
||||
* **纠正措施要求(CARs/SCARs):** 针对每个重大不符合项或重复的轻微不符合项发布。要求进行8D或等效的根本原因分析。设定响应期限(通常初始响应为10个工作日,完整的纠正措施计划为30天)。跟进有效性验证。
|
||||
* **合格供应商名单(ASL):** 加入需要资格认证(首件检验、能力研究、体系审核)。维护需要持续的绩效满足记分卡阈值。移除是一项重大的商业决策,需要采购、工程和质量部门达成一致,并制定过渡计划。临时状态(有条件批准)对于处于改进计划中的供应商很有用。
|
||||
* **开发与切换决策:** 供应商开发(投资于培训、过程改进、工装)在以下情况下有意义:供应商具有独特能力,切换成本高,合作关系在其他方面良好,且质量差距是可以解决的。在以下情况下切换有意义:供应商不愿投资,尽管有CAR但质量趋势恶化,或者存在其他合格来源且总质量成本更低。
|
||||
|
||||
### 法规框架
|
||||
|
||||
* **FDA 21 CFR 820 (QSR):** 涵盖医疗器械质量体系。关键章节:820.90(不合格品),820.100(CAPA),820.198(投诉处理),820.250(统计技术)。FDA审核员特别关注CAPA体系的有效性、投诉趋势以及根本原因分析是否严谨。
|
||||
* **IATF 16949(汽车):** 在ISO 9001基础上增加了客户特定要求。控制计划、PPAP(生产件批准程序)、MSA(测量系统分析)、8D报告、特殊特性管理。过程变更和不合格品处置需要通知客户。
|
||||
* **AS9100(航空航天):** 增加了产品安全、仿冒件预防、配置管理、首件检验(按AS9102)和关键特性管理的要求。使用原样处置需要客户批准。OASIS数据库用于供应商管理。
|
||||
* **ISO 13485(医疗器械):** 与FDA QSR协调一致,但符合欧洲法规要求。强调风险管理(ISO 14971)、可追溯性和设计控制。临床调查要求反馈到不合格品管理。
|
||||
* **控制计划:** 为每个过程步骤定义检验特性、方法、频率、样本量、反应计划以及责任方。IATF 16949要求,也是普遍的良好实践。必须是过程变更时更新的活文件。
|
||||
|
||||
### 质量成本
|
||||
|
||||
使用朱兰的COQ模型构建质量投资的商业案例:
|
||||
|
||||
* **预防成本:** 培训、过程验证、设计评审、供应商资格认证、SPC实施、防错夹具。通常占总COQ的5-10%。这里每投资1美元可避免10-100美元的故障成本。
|
||||
* **鉴定成本:** 来料检验、过程检验、最终检验、测试、校准、审核成本。通常占总COQ的20-25%。
|
||||
* **内部故障成本:** 报废、返工、重新检验、MRB处理、因不合格品导致的生产延误、根本原因调查人力。通常占总COQ的25-40%。
|
||||
* **外部故障成本:** 客户退货、保修索赔、现场服务、召回、法规行动、责任风险、声誉损害。通常占总COQ的25-40%,但最具波动性且单次事件成本最高。
|
||||
|
||||
## 决策框架
|
||||
|
||||
### NCR处置决策逻辑
|
||||
|
||||
按此顺序评估——适用的第一条路径决定处置方式:
|
||||
|
||||
1. **安全/法规关键性:** 如果不合格品影响安全关键特性或法规要求 → 不得按原样使用。如果可能,返工至完全符合要求,否则报废。未经正式的工程风险评估和(如要求)法规通知,不得有例外。
|
||||
2. **客户特定要求:** 如果客户规范严于设计规范,且零件符合设计但不符合客户要求 → 处置前联系客户获取让步。汽车和航空航天客户有明确的让步流程。
|
||||
3. **功能影响:** 工程评估不合格品是否影响形状、配合或功能。若无功能影响且在材料评审权限内 → 按原样使用,并附有文件化的工程理由。若存在功能影响 → 返工或报废。
|
||||
4. **可返工性:** 如果零件可以通过批准的返工程序恢复至完全符合要求 → 返工。比较返工成本与更换成本。如果返工成本超过更换成本的60%,通常报废更经济。
|
||||
5. **供应商责任:** 如果不合格品由供应商造成 → 退货并附SCAR。例外:如果生产不能等待更换零件,可能需要按原样使用或返工,并向供应商追索成本。
|
||||
|
||||
### RCA方法选择
|
||||
|
||||
* **单一事件,简单因果链:** 5个为什么。预算:1-2小时。
|
||||
* **单一事件,多个潜在原因类别:** 石川图 + 对最可能分支进行5个为什么分析。预算:4-8小时。
|
||||
* **反复出现的问题,过程相关:** 8D,需要完整团队。预算:D0-D8阶段总计20-40小时。
|
||||
* **安全关键或高严重性事件:** 故障树分析,需定量风险评估。预算:40-80小时。航空航天产品安全事件和医疗器械上市后分析需要。
|
||||
* **客户强制要求的格式:** 使用客户要求的任何格式(大多数汽车主机厂强制要求8D)。
|
||||
|
||||
### CAPA有效性验证
|
||||
|
||||
关闭任何CAPA前,验证:
|
||||
|
||||
1. **实施证据:** 证明行动已完成的文件化证据(更新的作业指导书及修订版次、已安装的夹具及验证记录、修改的检验计划及生效日期)。
|
||||
2. **监控期数据:** 至少90天的生产数据、连续3批生产批次或一个完整的审核周期——以提供最有意义的证据为准。
|
||||
3. **复发检查:** 监控期内特定失效模式零复发。如果复发,则CAPA无效——重新打开并重新调查。不要为同一问题关闭并开启新的CAPA。
|
||||
4. **先导指标审查:** 除了具体失效,相关指标是否有所改善?(例如,该过程的总体PPM、该产品系列的客户投诉率)。
|
||||
|
||||
### 检验水平调整
|
||||
|
||||
| 条件 | 行动 |
|
||||
|---|---|
|
||||
| 新供应商,前5批 | 加严检验(III级或100%) |
|
||||
| 正常检验下连续10批以上被接收 | 获得放宽或跳批检验资格 |
|
||||
| 放宽检验下1批被拒收 | 立即恢复到正常检验 |
|
||||
| 正常检验下连续5批中有2批被拒收 | 切换到加严检验 |
|
||||
| 加严检验下连续5批被接收 | 恢复到正常检验 |
|
||||
| 加严检验下连续10批被拒收 | 暂停供应商;上报采购部门 |
|
||||
| 客户投诉追溯到来料 | 无论当前水平如何,恢复到加严检验 |
|
||||
|
||||
### 供应商纠正措施升级
|
||||
|
||||
| 阶段 | 触发条件 | 行动 | 时间线 |
|
||||
|---|---|---|---|
|
||||
| 第1级:发出SCAR | 单一重大不符合项或90天内3次以上轻微不符合项 | 正式的SCAR,要求8D响应 | 10天内响应,30天内实施 |
|
||||
| 第2级:供应商观察期 | SCAR未及时响应,或纠正措施无效 | 增加检验,供应商处于试用期,通知采购部门 | 60天内证明改进 |
|
||||
| 第3级:受控发货 | 观察期内持续出现质量故障 | 供应商每次发货必须提交检验数据;或由第三方在供应商处进行分选,费用由供应商承担 | 90天内证明持续改进 |
|
||||
| 第4级:新来源资格认证 | 受控发货期间无改善 | 启动替代供应商资格认证;减少业务分配 | 资格认证时间线(视行业而定,3-12个月) |
|
||||
| 第5级:从ASL移除 | 未能改善或不愿投资 | 正式从合格供应商名单中移除;转移所有零件 | 最终采购订单下达前完成过渡 |
|
||||
|
||||
## 关键边缘情况
|
||||
|
||||
这些情况中,显而易见的处理方法是错误的。此处包含简要总结,以便您可以根据需要将其扩展为项目特定的操作手册。
|
||||
|
||||
1. **客户报告的现场故障,内部未检测到:** 您的检验和测试通过了该批次,但客户现场数据显示故障。本能反应是质疑客户的数据——请抵制这种想法。检查您的检验计划是否覆盖了实际的失效模式。通常,现场故障暴露的是测试覆盖范围的缺口,而不是测试执行错误。
|
||||
|
||||
2. **供应商审核发现伪造的符合性证书:** 供应商一直在提交带有伪造测试数据的CoC。立即隔离该供应商的所有物料,包括在制品和成品。这在航空航天领域(根据AS9100仿冒件预防要求)和医疗器械领域可能是需要上报法规部门的事件。响应的规模由遏制范围决定,而非单个NCR。
|
||||
|
||||
3. **SPC显示过程受控,但客户投诉在增加:** 控制图稳定在控制限内,但客户的装配过程对您规格内的变异很敏感。您的过程在数字上是"有能力的",但能力不足。这需要与客户协作以了解真正的功能要求,而不仅仅是规格审查。
|
||||
|
||||
4. **已发货产品发现的不合格:** 遏制措施必须延伸到客户的库存、在制品,甚至可能包括客户的客户。通知速度取决于安全风险——安全关键问题需要立即通知客户,其他情况可按标准流程紧急处理。
|
||||
|
||||
5. **仅解决症状而非根本原因的CAPA:** 缺陷在CAPA关闭后复发。在重新开启CAPA前,核查原始的根本原因分析——如果根本原因是“操作员失误”,纠正措施是“再培训”,那么无论是根本原因还是措施都是不充分的。重新进行根本原因分析,并假设首次调查是不充分的。
|
||||
|
||||
6. **单一不合格存在多个根本原因:** 一个单一缺陷是由机器磨损、材料批次差异和测量系统限制共同作用导致的。5 Whys方法强制要求单一链条——使用石川图或故障树分析来捕捉这种相互作用。纠正措施必须针对所有促成原因;仅修复其中一个可能降低发生频率,但无法消除失效模式。
|
||||
|
||||
7. **无法按需复现的间歇性缺陷:** 无法复现 ≠ 不存在。增加样本量和监控频率。检查环境相关性(班次、环境温度、湿度、相邻设备的振动)。变异分量研究(包含嵌套因子的测量系统分析)可以揭示间歇性测量系统的贡献。
|
||||
|
||||
8. **在监管审核中发现的不合格:** 不要试图淡化或辩解。承认发现的问题,在审核回复中记录,并像对待任何NCR一样处理——进行正式调查、根本原因分析和CAPA。审核员会专门测试您的系统是否能发现他们找到的问题;展示一个强有力的回应比假装这是异常情况更有价值。
|
||||
|
||||
## 沟通模式
|
||||
|
||||
### 语气调整
|
||||
|
||||
根据情况的严重程度和受众调整沟通语气:
|
||||
|
||||
* **常规NCR,内部团队:** 直接且客观。“NCR-2025-0412:零件7832-A的来料批次4471外径测量值为12.52mm,而规格为12.45±0.05mm。50个抽样件中有18个超出规格。材料已隔离在MRB笼3号仓。”
|
||||
* **重大NCR,向管理层报告:** 首先总结影响——生产影响、客户风险、财务损失——然后是细节。管理者需要先知道这意味着什么,然后才需要知道发生了什么。
|
||||
* **供应商通知(SCAR):** 专业、具体且有记录。说明不合格、违反的规格、影响,以及期望的回复格式和时限。切勿指责;让数据说话。
|
||||
* **客户通知(已发货产品的不合格):** 首先说明已知情况、已采取的措施(遏制)、客户需要做什么,以及全面解决的时间表。透明建立信任;拖延则破坏信任。
|
||||
* **监管回复(审核发现):** 客观、负责,并按照监管期望(例如FDA 483表回复格式)结构化。承认观察项,描述调查,说明纠正措施,提供实施和有效性的证据。
|
||||
|
||||
### 关键模板
|
||||
|
||||
以下是简要模板。在使用前,请根据您的MRB、供应商质量和CAPA工作流程进行调整。
|
||||
|
||||
**NCR通知(内部):** 主题:`NCR-{number}: {part_number} — {defect_summary}`。说明:发现的问题、违反的规格、受影响的数量、当前遏制状态以及范围的初步评估。
|
||||
|
||||
**给供应商的SCAR:** 主题:`SCAR-{number}: Non-Conformance on PO# {po_number} — Response Required by {date}`。包含:零件号、批次、规格、测量数据、受影响数量、影响说明、期望的回复格式。
|
||||
|
||||
**客户质量通知:** 首先说明:已采取的遏制措施、产品可追溯性(批次/序列号)、建议客户采取的行动、纠正措施时间表,以及可直接联系的质量工程师。
|
||||
|
||||
## 升级协议
|
||||
|
||||
### 自动升级触发条件
|
||||
|
||||
| 触发条件 | 行动 | 时间表 |
|
||||
|---|---|---|
|
||||
| 安全关键不合格 | 立即通知质量副总裁和法规事务部门 | 1小时内 |
|
||||
| 现场失效或客户投诉 | 指定专门调查员,通知客户团队 | 4小时内 |
|
||||
| 重复NCR(相同失效模式,3次以上发生) | 强制启动CAPA,管理层评审 | 24小时内 |
|
||||
| 供应商伪造文件 | 隔离所有供应商材料,通知法规和法律部门 | 立即 |
|
||||
| 已发货产品的不合格 | 启动客户通知协议,进行遏制 | 4小时内 |
|
||||
| 审核发现(外部) | 管理层评审,制定回复计划 | 48小时内 |
|
||||
| CAPA逾期超过目标日期30天 | 升级至质量总监以分配资源 | 1周内 |
|
||||
| NCR积压超过50项未关闭 | 流程评审,资源分配,管理层简报 | 1周内 |
|
||||
|
||||
### 升级链
|
||||
|
||||
级别1(质量工程师) → 级别2(质量主管,4小时) → 级别3(质量经理,24小时) → 级别4(质量总监,48小时) → 级别5(质量副总裁,72+小时 或 任何安全关键事件)
|
||||
|
||||
## 绩效指标
|
||||
|
||||
每周跟踪这些指标,并每月进行趋势分析:
|
||||
|
||||
| 指标 | 目标 | 红色警报 |
|
||||
|---|---|---|
|
||||
| NCR关闭时间(中位数) | < 15个工作日 | > 30个工作日 |
|
||||
| CAPA按时关闭率 | > 90% | < 75% |
|
||||
| CAPA有效率(未复发) | > 85% | < 70% |
|
||||
| 供应商PPM(来料) | < 500 PPM | > 2,000 PPM |
|
||||
| 质量成本(占收入百分比) | < 3% | > 5% |
|
||||
| 内部缺陷率(过程中) | < 1,000 PPM | > 5,000 PPM |
|
||||
| 客户投诉率(每百万件) | < 50 | > 200 |
|
||||
| 超期NCR(> 30天未关闭) | < 总数的10% | > 总数的25% |
|
||||
|
||||
## 其他资源
|
||||
|
||||
* 将此技能与您的NCR模板、处置权限矩阵和SPC规则集结合使用,以确保调查人员每次使用相同的定义。
|
||||
* 在使用工作流进行生产前,请将CAPA关闭标准和有效性检查证据要求放在工作流旁边。
|
||||
225
docs/zh-CN/skills/returns-reverse-logistics/SKILL.md
Normal file
225
docs/zh-CN/skills/returns-reverse-logistics/SKILL.md
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
name: returns-reverse-logistics
|
||||
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年以上经验的高级退货运营经理,负责处理零售、电子商务和全渠道环境下的完整退货生命周期。您的职责范围涵盖退货授权(RMA)、收货与检验、状况分级、处置路径规划、退款与信用处理、欺诈检测、供应商回收(RTV)以及保修索赔管理。您使用的系统包括OMS(订单管理系统)、WMS(仓库管理系统)、RMS(退货管理系统)、CRM、欺诈检测平台和供应商门户。您在客户满意度与利润保护、处理速度与检验准确性、欺诈预防与误判客户摩擦之间寻求平衡。
|
||||
|
||||
## 何时使用
|
||||
|
||||
* 处理退货请求并确定RMA资格
|
||||
* 检查退回商品并分配状况等级以进行处置
|
||||
* 规划处置决策路径(重新上架、翻新、清仓、报废、退给供应商)
|
||||
* 调查退货欺诈模式或退货政策滥用行为
|
||||
* 管理保修索赔和供应商回收扣款
|
||||
|
||||
## 运作方式
|
||||
|
||||
1. 接收退货请求,并根据退货政策(时间窗口、状况、品类限制)验证资格
|
||||
2. 根据商品价值和退货原因,发放带有预付标签或自提点投递说明的RMA
|
||||
3. 在退货中心接收并检查商品;分配状况等级(A至D)
|
||||
4. 根据回收经济性(重新上架利润 vs. 清仓 vs. 报废成本)规划至最优处置渠道
|
||||
5. 根据政策处理退款或换货;标记异常情况以供欺诈审查
|
||||
6. 汇总可向供应商追回的退货,并在合同规定窗口内提交RTV索赔
|
||||
|
||||
## 示例
|
||||
|
||||
* **高价值电子产品退货**:客户退回一台价值1200美元的笔记本电脑,声称"有缺陷"。检验发现外观损坏与缺陷声明不符。演练分级、翻新成本评估、处置路径规划(翻新并以70%回收率转售 vs. 以85%回收率退给供应商),以及欺诈标记评估。
|
||||
* **系列退货者检测**:客户账户显示在6个月内23个订单的退货率为47%。根据欺诈指标分析模式,计算净利润贡献,并推荐政策行动(警告、限制退货或标记账户)。
|
||||
* **保修索赔纠纷**:客户在12个月保修期的第11个月提出保修索赔。产品显示有使用不当的迹象。整理证据材料,应用制造商保修排除标准,并起草客户沟通函。
|
||||
|
||||
## 核心知识
|
||||
|
||||
### 退货政策逻辑
|
||||
|
||||
每次退货都始于政策评估。政策引擎必须考虑重叠且有时相互冲突的规则:
|
||||
|
||||
* **标准退货窗口**:大多数一般商品通常为收货后30天。电子产品通常为15天。易腐品不可退货。家具/床垫为30-90天,并有特定状况要求。延长的假日窗口(11月1日至12月31日的购买可在1月31日前退货)会造成退货潮,并在1月中旬达到高峰。
|
||||
* **状况要求**:大多数政策要求原始包装完好、所有配件齐全、且无使用痕迹(超出合理检查范围)。"合理检查"是纠纷所在——移除笔记本电脑屏幕保护膜的客户技术上改变了产品,但这是正常的开箱行为。
|
||||
* **收据和购买凭证**:通过信用卡、会员号或电话号码查找POS交易记录已基本取代纸质收据。礼品收据赋予持有人按购买价换货或获得店铺积分的权利,而非现金退款。无收据退货设有限额(通常每笔交易50-75美元,滚动12个月内3次),并按近期最低售价退款。
|
||||
* **重新上架费**:适用于已开封的电子产品(15%)、特殊订购商品(20-25%)以及需要协调退货运输的大型/笨重物品。对有缺陷产品或配送错误的商品予以免除。为维护客户关系而免除的决定需要利润意识——在一件利润率为28%、价值300美元的商品上免除45美元的重新上架费,其实际成本比看起来更高。
|
||||
* **跨渠道退货**:线上下单、店内退货(BORIS)是客户期望但操作复杂的流程。线上价格可能与店内价格不同。退款应与原始购买价格匹配,而非当前货架价格。库存系统必须能够接受商品退回店内库存,或标记为退回配送中心。
|
||||
* **国际退货**:关税退税资格要求提供在法定窗口内(通常为3-5年,视国家而定)再出口的证明。对于低成本商品,退货运输成本通常超过商品价值——当运费超过商品价值的40%时,提供"免退货退款"。退货商品的海关申报文件与原始出口文件不同。
|
||||
* **例外情况**:价格匹配退货(客户发现更便宜的价格)、超出窗口但因情有可原的买家悔恨、保修期外的缺陷产品,以及忠诚度等级覆盖(顶级客户获得延长的窗口期和费用减免)都需要判断框架,而非僵化的规则。
|
||||
|
||||
### 检验与分级
|
||||
|
||||
退回产品需要一致的分级,以驱动处置决策。速度与准确性之间存在矛盾——30秒的目视检查能处理大量商品,但会遗漏外观缺陷;5分钟的功能测试能发现所有问题,但会造成规模瓶颈:
|
||||
|
||||
* **A级(如新)**:原始包装完好,所有配件齐全,无使用痕迹,通过功能测试。可作为新品或"开箱"商品重新上架,实现全额利润回收(原零售价的85-100%)。目标检验时间:45-90秒。
|
||||
* **B级(良好)**:轻微外观磨损,原始包装可能损坏或缺少外封套,所有配件齐全,功能完好。可作为"开箱"或"翻新"商品重新上架,价格为零售价的60-80%。可能需要重新包装(每件2-5美元)。目标检验时间:90-180秒。
|
||||
* **C级(一般)**:可见磨损、划痕或轻微损坏。缺少价值低于单位价值10%的配件。功能正常但外观受损。通过二级渠道(奥特莱斯、市场平台、清仓)以零售价的30-50%销售。如果翻新成本 < 回收价值的20%,则可进行翻新。
|
||||
* **D级(残次/零件)**:功能故障、严重损坏或缺少关键部件。可作为零件或材料回收,价值为零售价的5-15%。如果零件回收不可行,则送至回收或销毁。
|
||||
|
||||
分级标准因品类而异。消费电子产品需要进行功能测试(开机、屏幕检查、连接性),每件增加2-4分钟。服装检验侧重于污渍、气味、面料拉伸和缺失标签——经验丰富的检验员使用"一臂距离嗅探测试"和紫外线灯检测污渍。由于卫生法规限制,化妆品和个人护理用品一旦开封几乎无法重新上架。
|
||||
|
||||
### 处置决策树
|
||||
|
||||
处置是退货要么回收价值要么侵蚀利润的环节。路径决策由经济性驱动:
|
||||
|
||||
* **作为新品重新上架**:仅限包装完整的A级商品。产品必须通过任何要求的功能/安全测试。重新贴标或重新密封可能引发监管问题(FTC关于"以旧充新"的执法)。最适合重新上架成本(每件3-8美元)相对于回收价值微不足道的高利润商品。
|
||||
* **重新包装并作为"开箱"商品销售**:包装损坏的A级商品或B级商品。重新包装成本(5-15美元,视复杂程度而定)必须通过开箱价与下一级渠道之间的利润差来证明其合理性。电子产品和家电是理想选择。
|
||||
* **翻新**:当翻新成本 < 翻新后售价的40%,且存在翻新销售渠道(认证翻新计划、制造商直销店)时,经济上可行。常见于高端电子产品、电动工具和家电。需要专用的翻新站、备件库存和重新测试能力。
|
||||
* **清仓**:C级和部分B级商品,其中重新包装/翻新不合理。清仓渠道包括托盘拍卖(B-Stock、DirectLiquidation、Bulq)、批发清算商(服装按磅计价,电子产品按件计价)和区域清算商。回收率:零售价的5-20%。关键洞察:在托盘中混合品类会破坏价值——电子产品/服装/家居用品托盘按最低品类价格出售。
|
||||
* **捐赠**:按公允市场价值(FMV)可进行税前扣除。当FMV > 清仓回收价值且公司有足够的税负来利用抵扣时,比清仓更有价值。品牌保护:限制捐赠可能最终进入折扣渠道、损害品牌定位的贴牌产品。
|
||||
* **销毁**:适用于召回产品、在退货流中发现假冒产品、有监管处置要求的产品(电池、需符合WEEE规定的电子产品、危险品),以及任何二级市场存在都不可接受的品牌商品。需要销毁证明以符合合规和税务文件要求。
|
||||
|
||||
### 欺诈检测
|
||||
|
||||
退货欺诈每年给美国零售商造成240亿美元以上的损失。挑战在于检测而不给合法客户制造障碍:
|
||||
|
||||
* **衣橱欺诈(穿后退货)**:客户购买服装或配饰,穿着参加活动后退货。指标:退货集中在节假日/活动前后、有除臭剂残留、衣领有化妆品痕迹、褶皱/拉伸与"试穿"不符的面料。对策:紫外线灯检查化妆品痕迹、使用客户未被指示移除的RFID防盗标签(如果标签缺失,则说明商品曾被穿着)。
|
||||
* **收据欺诈**:使用拾获、盗窃或伪造的收据将盗窃的商品退回以换取现金。随着数字收据查询取代纸质收据,此类欺诈在减少,但仍有发生。对策:所有现金退款均需身份证件,退货需匹配原始支付方式,限制每张身份证的无收据退货次数。
|
||||
* **调包欺诈(退货调换)**:将假冒、更便宜或损坏的商品放入已购商品的包装中退回。常见于电子产品(将旧手机放入新手机盒中退回)和化妆品(用更便宜的产品重新填充容器)。对策:退货时验证序列号,检查重量是否与预期产品重量一致,在退款前对高价值商品进行详细检查。
|
||||
* **系列退货者**:退货率 > 购买量的30%或年退货额 > 5000美元的客户。并非所有人都是欺诈——有些人是真的犹豫不决或进行"套购"(购买多个尺码试穿)。按以下维度细分:退货原因一致性、退货时产品状况、退货后的净终身价值。一个购买5万美元、退货1.8万美元(退货率36%)但净收入3.2万美元的客户,其价值高于一个购买1.5万美元、零退货的客户。
|
||||
* **套购**:有意订购多个尺码/颜色,计划退回大部分。合法的购物行为,但在规模上变得成本高昂。通过合身技术(尺码推荐工具、AR试穿)、宽松的换货政策(免费换货、退货收取重新上架费)以及教育而非惩罚来解决。
|
||||
* **价格套利**:在促销/折扣期间购买,然后在不同地点或时间按全价退货以获取差价。政策必须将退款与实际购买价格挂钩,无论当前售价如何。跨渠道退货是主要途径。
|
||||
* **有组织零售犯罪(ORC)**:跨多个商店/身份协调的盗窃-退货操作。指标:同一地址多个身份证件的高价值退货、常被盗窃品类(电子产品、化妆品、保健品)的退货、地理聚集性。向防损(LP)团队报告——这超出了标准退货运营的范围。
|
||||
|
||||
### 供应商回收
|
||||
|
||||
并非所有退货都是客户的错。有缺陷的产品、履行错误和质量问题都存在向供应商追索成本的路径:
|
||||
|
||||
* **退还给供应商(RTV):** 在供应商保修期或缺陷索赔窗口内退回的有缺陷产品。流程:积累缺陷单位(各供应商的最低RTV发货门槛不同,通常在200-500美元之间),获取RTV授权编号,发货至供应商指定的退货设施,跟踪退款发放。常见失败原因:让符合RTV条件的产品在退货仓库中存放超过供应商的索赔窗口期(通常为收货后90天)。
|
||||
* **缺陷索赔:** 当缺陷率超过供应商协议阈值(通常为2-5%)时,就超出部分提出正式的缺陷索赔。需要缺陷记录文件(照片、检查记录、按SKU汇总的客户投诉数据)。供应商会提出异议——你的数据质量决定了你的追索成功率。
|
||||
* **供应商扣款:** 对于供应商造成的问题(从供应商配送中心发错货、产品标签错误、包装故障),扣回全部成本,包括退货运输和处理人工费。需要制定供应商合规计划,并公布标准和处罚细则。
|
||||
* **退款 vs 换货 vs 核销:** 如果供应商有偿付能力且响应迅速,则争取退款。如果供应商在海外且收款困难,则协商换货。如果索赔金额较小(< 200美元)且供应商是关键供应商,可考虑核销并在下一次合同谈判中注明。
|
||||
|
||||
### 保修管理
|
||||
|
||||
保修索赔与退货不同,遵循不同的工作流程:
|
||||
|
||||
* **保修 vs 退货:** 退货是客户行使撤销购买的权利(通常在30天内,任何原因均可)。保修索赔是客户在保修覆盖期内(90天至终身)报告产品缺陷。不同的系统、不同的政策、不同的财务处理方式。
|
||||
* **制造商 vs 零售商责任:** 零售商通常负责退货窗口期。制造商负责保修期。灰色地带:在保修期内反复出现故障的"柠檬"产品——客户要求退款,制造商提供维修,零售商陷入两难。
|
||||
* **延长保修/保护计划:** 在销售点销售,利润率为30-60%。针对延长保修的索赔由保修提供商(通常是第三方)处理。零售商的角色是协助提出索赔,而非处理索赔。常见投诉:客户无法区分零售商的退货政策、制造商保修和延长保修覆盖范围。
|
||||
|
||||
## 决策框架
|
||||
|
||||
### 按品类和状况分类处置
|
||||
|
||||
| 品类 | A级 | B级 | C级 | D级 |
|
||||
|---|---|---|---|---|
|
||||
| 消费电子 | 重新上架(先测试) | 开箱/翻新 | 若投资回报率 > 40%则翻新,否则清算 | 零件回收或电子垃圾处理 |
|
||||
| 服装 | 若标签完好则重新上架 | 重新包装/折扣店 | 按重量清算 | 纺织品回收 |
|
||||
| 家居与家具 | 重新上架 | 开箱折扣 | 清算(本地,避免运输) | 捐赠或销毁 |
|
||||
| 健康与美容 | 若密封则重新上架 | 销毁(法规要求) | 销毁 | 销毁 |
|
||||
| 图书与媒体 | 重新上架 | 重新上架(折扣) | 清算 | 回收 |
|
||||
| 体育用品 | 重新上架 | 开箱 | 若翻新成本 < 价值的25%则翻新 | 零件回收或捐赠 |
|
||||
| 玩具与游戏 | 若密封则重新上架 | 开箱 | 清算 | 若符合安全标准则捐赠 |
|
||||
|
||||
### 欺诈评分模型
|
||||
|
||||
为每次退货评分0-100分。65分以上标记为需审核,80分以上暂缓退款:
|
||||
|
||||
| 信号 | 分值 | 备注 |
|
||||
|---|---|---|
|
||||
| 退货率 > 30%(滚动12个月) | +15 | 根据品类标准调整 |
|
||||
| 收货后48小时内退货 | +5 | 可能是合理的"对比购物" |
|
||||
| 高价值电子产品,序列号不匹配 | +40 | 几乎确定是调包欺诈 |
|
||||
| 退货原因在发起和收货时不一致 | +10 | 不一致标记 |
|
||||
| 同一周内多次退货 | +10 | 与退货率信号累计 |
|
||||
| 退货地址与发货地址不同 | +10 | 礼品退货除外 |
|
||||
| 产品重量与预期相差 > 5% | +25 | 调包或缺少部件 |
|
||||
| 客户账户使用时间 < 30天 | +10 | 新账户风险 |
|
||||
| 无收据退货 | +15 | 收据欺诈风险较高 |
|
||||
| 属于高损耗率品类的商品 | +5 | 电子产品、化妆品、设计师服装 |
|
||||
|
||||
### 供应商追索投资回报率
|
||||
|
||||
在以下情况下进行供应商追索:`(Expected credit × probability of collection) > (Labor cost + shipping cost + relationship cost)`。经验法则:
|
||||
|
||||
* 索赔 > 500美元:必须追索。即使在50%的收款概率下,计算也成立。
|
||||
* 索赔 200-500美元:如果供应商有可操作的RTV计划且可以批量发货,则追索。
|
||||
* 索赔 < 200美元:累积到达到阈值,或用于抵扣下一个采购订单。不要单独发货单个单位。
|
||||
* 海外供应商:将最低阈值提高到1,000美元。预期处理时间增加30%。
|
||||
|
||||
### 退货政策例外情况处理逻辑
|
||||
|
||||
当退货超出标准政策时,按以下顺序评估:
|
||||
|
||||
1. **产品是否有缺陷?** 如果是,则无论窗口期或状况如何,都应接受。有缺陷的产品是公司的问题,不是客户的问题。
|
||||
2. **这是否是高价值客户?**(按客户终身价值排名前10%)如果是,则接受并按标准退款。保留客户的账目几乎总是支持例外处理。
|
||||
3. **请求对中立的观察者来说是否合理?** 客户在3月份退回11月购买的冬装(4个月,超出30天窗口期)是可以理解的。客户在12月份退回6月购买的泳装则不那么合理。
|
||||
4. **处置结果是什么?** 如果产品可以重新上架(A级),例外处理的成本微乎其微——批准。如果是C级或更差,例外处理会损失实际的利润。
|
||||
5. **批准是否会带来先例风险?** 针对有记录情况的一次性例外处理很少会产生先例。公开的例外处理(社交媒体投诉)总是会产生先例。
|
||||
|
||||
## 关键边缘案例
|
||||
|
||||
这些是标准工作流程无法处理的情况。此处包含简要摘要,以便您可以根据需要将其扩展为特定项目的操作手册。
|
||||
|
||||
1. **固件被擦除的高价值电子产品:** 客户退回一台声称有缺陷的笔记本电脑,但设备已被恢复出厂设置,并显示有6个月的电池循环计数。该设备被大量使用,现在却作为"缺陷"产品退回——评级必须超越干净的软件状态。
|
||||
2. **包装不当的危险品退货:** 客户退回含有锂电池或化学品的产品,但没有使用所需的DOT包装。接收会产生监管责任;拒绝会产生客户服务问题。产品不能通过标准包裹退货运输返回。
|
||||
3. **涉及关税的跨境退货:** 国际客户退回一件已支付关税的出口产品。关税退税申请需要客户没有的特定文件。退货运输成本可能超过产品价值。
|
||||
4. **内容创作后的网红批量退货:** 社交媒体网红购买20多件商品,创作内容后,除一件外全部退回。技术上符合政策,但品牌价值已被提取。重新上架的挑战加剧,因为开箱视频展示了完全相同的商品。
|
||||
5. **客户修改后的产品保修索赔:** 客户更换了产品中的某个部件(例如,升级了笔记本电脑的RAM),然后声称另一个无关部件(例如,屏幕故障)存在保修缺陷。该修改可能使所声称的缺陷不在保修范围内,也可能不影响。
|
||||
6. **既是高价值客户又是频繁退货者:** 年消费额8万美元且退货率为42%的客户。禁止其退货会失去一个盈利客户;接受其行为会鼓励其继续。需要超越简单退货率的细致入微的客户细分。
|
||||
7. **召回产品的退货:** 客户退回一件正在积极安全召回的产品。标准退货流程是错误的——召回产品应遵循召回计划,而非退货计划。混在一起会产生责任和报告错误。
|
||||
8. **礼品收据退货且当前价格高于购买价格:** 礼品接收者持礼品收据前来退货。该商品现在的售价比送礼者支付的价格高出30美元。政策规定按购买价格退款,但客户看到的是货架价格并期望获得该金额。
|
||||
|
||||
## 沟通模式
|
||||
|
||||
### 语气调整
|
||||
|
||||
* **标准退款确认:** 热情、高效。首先说明解决方案金额和时间,而不是流程。
|
||||
* **拒绝退货:** 富有同理心但清晰明了。解释具体政策,提供替代方案(换货、店铺积分、保修索赔),提供升级路径。永远不要让客户没有选择。
|
||||
* **欺诈调查暂缓:** 中立、客观。"我们需要更多时间来处理您的退货"——永远不要对客户说"欺诈"或"调查"。提供时间线。内部沟通是记录欺诈指标的地方。
|
||||
* **重新上架费说明:** 透明。解释费用涵盖的内容(检查、重新包装、价值损失),并在处理前确认净退款金额,以免产生意外。
|
||||
* **供应商RTV索赔:** 专业、基于证据。包括缺陷数据、照片、按SKU分类的退货量,并引用供应商协议中涵盖缺陷索赔的条款。
|
||||
|
||||
### 关键模板
|
||||
|
||||
简要模板如下。在投入生产使用前,请根据您的欺诈、客户体验和逆向物流工作流程进行调整。
|
||||
|
||||
**RMA批准:** 主题:`Return Approved — Order #{order_id}`。提供:RMA编号、退货运输说明、预期退款时间线、状况要求。
|
||||
|
||||
**退款确认:** 首先说明金额:"您${amount}的退款已处理至您的\[支付方式]。请允许\[X]个工作日。"
|
||||
|
||||
**欺诈暂缓通知:** "您的退货正在由我们的处理团队审核。我们预计在\[X]个工作日内提供更新。感谢您的耐心等待。"
|
||||
|
||||
## 升级协议
|
||||
|
||||
### 自动升级触发条件
|
||||
|
||||
| 触发条件 | 行动 | 时间线 |
|
||||
|---|---|---|
|
||||
| 退货价值 > 5,000美元(单件商品) | 退款前需主管批准 | 处理前 |
|
||||
| 欺诈评分 ≥ 80 | 暂缓退款,转交欺诈审核团队 | 立即 |
|
||||
| 客户同时提出信用卡拒付 | 停止退货处理,与支付团队协调 | 1小时内 |
|
||||
| 产品被识别为召回产品 | 转交召回协调员,不作为标准退货处理 | 立即 |
|
||||
| 供应商对某SKU的缺陷率超过5% | 通知商品和供应商管理部门 | 24小时内 |
|
||||
| 同一客户在12个月内提出第三次政策例外请求 | 批准前需经理审核 | 处理前 |
|
||||
| 退货流中疑似出现假冒产品 | 从处理中撤出,拍照,通知防损和品牌保护部门 | 立即 |
|
||||
| 退货涉及受管制产品(药品、危险品、医疗器械) | 转交合规团队 | 立即 |
|
||||
|
||||
### 升级链条
|
||||
|
||||
级别1(退货专员) → 级别2(团队主管,2小时) → 级别3(退货经理,8小时) → 级别4(运营总监,24小时) → 级别5(副总裁,48+小时或任何单件商品退货 > 25,000美元)
|
||||
|
||||
## 绩效指标
|
||||
|
||||
| 指标 | 目标 | 危险信号 |
|
||||
|---|---|---|
|
||||
| 退货处理时间(收货到退款) | < 48小时 | > 96小时 |
|
||||
| 检查准确率(审计中的等级一致性) | > 95% | < 88% |
|
||||
| 重新上架率(退货中作为新品/开箱品重新上架的比例) | > 45% | < 30% |
|
||||
| 欺诈检测率(确认的欺诈被捕获的比例) | > 80% | < 60% |
|
||||
| 误报率(被标记的合法退货比例) | < 3% | > 8% |
|
||||
| 供应商追索率(追回金额 / 符合条件金额) | > 70% | < 45% |
|
||||
| 客户满意度(退货后CSAT) | > 4.2/5.0 | < 3.5/5.0 |
|
||||
| 单次退货处理成本 | < $8.00 | > $15.00 |
|
||||
|
||||
## 其他资源
|
||||
|
||||
* 在将此技能投入生产使用前,请将其与你的评分标准、欺诈审查阈值和退款授权矩阵配对。
|
||||
* 将补货标准、危险品退货处理和清算规则交由负责执行决策的运营团队就近保管。
|
||||
@@ -72,8 +72,25 @@ Scanning:
|
||||
|
||||
### 第 2 阶段 — 质量评估
|
||||
|
||||
启动一个 Task 工具子代理(**Explore 代理,模型:opus**),提供完整的清单和检查清单。
|
||||
子代理读取每个技能,应用检查清单,并返回每个技能的 JSON:
|
||||
启动一个 **通用代理** 工具子代理,并使用完整的清单和检查项:
|
||||
|
||||
```text
|
||||
Agent(
|
||||
subagent_type="general-purpose",
|
||||
prompt="
|
||||
Evaluate the following skill inventory against the checklist.
|
||||
|
||||
[INVENTORY]
|
||||
|
||||
[CHECKLIST]
|
||||
|
||||
Return JSON for each skill:
|
||||
{ \"verdict\": \"Keep\"|\"Improve\"|\"Update\"|\"Retire\"|\"Merge into [X]\", \"reason\": \"...\" }
|
||||
"
|
||||
)
|
||||
```
|
||||
|
||||
子代理读取每项技能,应用检查项,并返回每项技能的 JSON 结果:
|
||||
|
||||
`{ "verdict": "Keep"|"Improve"|"Update"|"Retire"|"Merge into [X]", "reason": "..." }`
|
||||
|
||||
|
||||
@@ -99,6 +99,40 @@ origin: ECC
|
||||
5. **压缩前写入** — 在压缩前将重要上下文保存到文件或记忆中
|
||||
6. **使用带摘要的 `/compact`** — 添加自定义消息:`/compact Focus on implementing auth middleware next`
|
||||
|
||||
## 令牌优化模式
|
||||
|
||||
### 触发表惰性加载
|
||||
|
||||
不在会话开始时加载完整的技能内容,而是使用一个将关键词映射到技能路径的触发表。技能仅在触发时加载,可将基线上下文减少 50% 以上:
|
||||
|
||||
| 触发词 | 技能 | 加载时机 |
|
||||
|---------|-------|-----------|
|
||||
| "test", "tdd", "coverage" | tdd-workflow | 用户提及测试时 |
|
||||
| "security", "auth", "xss" | security-review | 涉及安全相关工作时 |
|
||||
| "deploy", "ci/cd" | deployment-patterns | 涉及部署上下文时 |
|
||||
|
||||
### 上下文组合感知
|
||||
|
||||
监控哪些内容正在消耗你的上下文窗口:
|
||||
|
||||
* **CLAUDE.md 文件** — 始终加载,需保持精简
|
||||
* **已加载技能** — 每个技能增加 1-5K 令牌
|
||||
* **对话历史** — 随每次交流增长
|
||||
* **工具结果** — 文件读取、搜索结果会增加体积
|
||||
|
||||
### 重复指令检测
|
||||
|
||||
常见的重复上下文来源:
|
||||
|
||||
* 相同的规则同时出现在 `~/.claude/rules/` 和项目 `.claude/rules/` 中
|
||||
* 技能重复了 CLAUDE.md 的指令
|
||||
* 多个技能覆盖了重叠的领域
|
||||
|
||||
### 上下文优化工具
|
||||
|
||||
* `token-optimizer` MCP — 通过内容去重实现 95% 以上的自动令牌减少
|
||||
* `context-mode` — 上下文虚拟化(已演示从 315KB 减少到 5.4KB)
|
||||
|
||||
## 相关
|
||||
|
||||
* [长篇指南](https://x.com/affaanmustafa/status/2014040193557471352) — Token 优化部分
|
||||
|
||||
317
docs/zh-CN/skills/video-editing/SKILL.md
Normal file
317
docs/zh-CN/skills/video-editing/SKILL.md
Normal file
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: video-editing
|
||||
description: AI辅助的视频编辑工作流程,用于剪辑、构建和增强实拍素材。涵盖从原始拍摄到FFmpeg、Remotion、ElevenLabs、fal.ai,再到Descript或CapCut最终润色的完整流程。适用于用户想要编辑视频、剪辑素材、制作vlog或构建视频内容的情况。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# 视频编辑
|
||||
|
||||
针对真实素材的AI辅助编辑。非根据提示生成。快速编辑现有视频。
|
||||
|
||||
## 何时激活
|
||||
|
||||
* 用户想要编辑、剪辑或构建视频素材
|
||||
* 将长录制内容转化为短视频内容
|
||||
* 从原始素材构建vlog、教程或演示视频
|
||||
* 为现有视频添加叠加层、字幕、音乐或画外音
|
||||
* 为不同平台(YouTube、TikTok、Instagram)重新构图视频
|
||||
* 用户提到“编辑视频”、“剪辑这个素材”、“制作vlog”或“视频工作流”
|
||||
|
||||
## 核心理念
|
||||
|
||||
当你不再要求AI创建整个视频,而是开始使用它来压缩、构建和增强真实素材时,AI视频编辑就变得有用了。价值不在于生成。价值在于压缩。
|
||||
|
||||
## 处理流程
|
||||
|
||||
```
|
||||
Screen Studio / raw footage
|
||||
→ Claude / Codex
|
||||
→ FFmpeg
|
||||
→ Remotion
|
||||
→ ElevenLabs / fal.ai
|
||||
→ Descript or CapCut
|
||||
```
|
||||
|
||||
每个层级都有特定的工作。不要跳过层级。不要试图让一个工具完成所有事情。
|
||||
|
||||
## 层级 1:采集(Screen Studio / 原始素材)
|
||||
|
||||
收集源材料:
|
||||
|
||||
* **Screen Studio**:用于应用演示、编码会话、浏览器工作流程的精致屏幕录制
|
||||
* **原始摄像机素材**:vlog素材、采访、活动录制
|
||||
* **通过VideoDB的桌面采集**:具有实时上下文的会话录制(参见 `videodb` 技能)
|
||||
|
||||
输出:准备进行组织的原始文件。
|
||||
|
||||
## 层级 2:组织(Claude / Codex)
|
||||
|
||||
使用Claude Code或Codex进行:
|
||||
|
||||
* **转录和标记**:生成转录稿,识别主题和要点
|
||||
* **规划结构**:决定保留内容、剪切内容、确定顺序
|
||||
* **识别无效片段**:查找停顿、离题、重复拍摄
|
||||
* **生成编辑决策列表**:用于剪辑的时间戳、保留的片段
|
||||
* **搭建FFmpeg和Remotion代码**:生成命令和合成
|
||||
|
||||
```
|
||||
Example prompt:
|
||||
"Here's the transcript of a 4-hour recording. Identify the 8 strongest segments
|
||||
for a 24-minute vlog. Give me FFmpeg cut commands for each segment."
|
||||
```
|
||||
|
||||
此层级关乎结构,而非最终的创意品味。
|
||||
|
||||
## 层级 3:确定性剪辑(FFmpeg)
|
||||
|
||||
FFmpeg处理枯燥但关键的工作:分割、修剪、连接和预处理。
|
||||
|
||||
### 按时间戳提取片段
|
||||
|
||||
```bash
|
||||
ffmpeg -i raw.mp4 -ss 00:12:30 -to 00:15:45 -c copy segment_01.mp4
|
||||
```
|
||||
|
||||
### 根据编辑决策列表批量剪辑
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# cuts.txt: start,end,label
|
||||
while IFS=, read -r start end label; do
|
||||
ffmpeg -i raw.mp4 -ss "$start" -to "$end" -c copy "segments/${label}.mp4"
|
||||
done < cuts.txt
|
||||
```
|
||||
|
||||
### 连接片段
|
||||
|
||||
```bash
|
||||
# Create file list
|
||||
for f in segments/*.mp4; do echo "file '$f'"; done > concat.txt
|
||||
ffmpeg -f concat -safe 0 -i concat.txt -c copy assembled.mp4
|
||||
```
|
||||
|
||||
### 创建代理文件以加速编辑
|
||||
|
||||
```bash
|
||||
ffmpeg -i raw.mp4 -vf "scale=960:-2" -c:v libx264 -preset ultrafast -crf 28 proxy.mp4
|
||||
```
|
||||
|
||||
### 提取音频用于转录
|
||||
|
||||
```bash
|
||||
ffmpeg -i raw.mp4 -vn -acodec pcm_s16le -ar 16000 audio.wav
|
||||
```
|
||||
|
||||
### 标准化音频电平
|
||||
|
||||
```bash
|
||||
ffmpeg -i segment.mp4 -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:v copy normalized.mp4
|
||||
```
|
||||
|
||||
## 层级 4:可编程合成(Remotion)
|
||||
|
||||
Remotion将编辑问题转化为可组合的代码。用它来处理传统编辑器让工作变得痛苦的事情:
|
||||
|
||||
### 何时使用Remotion
|
||||
|
||||
* 叠加层:文本、图像、品牌标识、下三分之一字幕
|
||||
* 数据可视化:图表、统计数据、动画数字
|
||||
* 动态图形:转场、解说动画
|
||||
* 可组合场景:跨视频可重复使用的模板
|
||||
* 产品演示:带注释的截图、UI高亮
|
||||
|
||||
### 基本的Remotion合成
|
||||
|
||||
```tsx
|
||||
import { AbsoluteFill, Sequence, Video, useCurrentFrame } from "remotion";
|
||||
|
||||
export const VlogComposition: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
|
||||
return (
|
||||
<AbsoluteFill>
|
||||
{/* Main footage */}
|
||||
<Sequence from={0} durationInFrames={300}>
|
||||
<Video src="/segments/intro.mp4" />
|
||||
</Sequence>
|
||||
|
||||
{/* Title overlay */}
|
||||
<Sequence from={30} durationInFrames={90}>
|
||||
<AbsoluteFill style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<h1 style={{
|
||||
fontSize: 72,
|
||||
color: "white",
|
||||
textShadow: "2px 2px 8px rgba(0,0,0,0.8)",
|
||||
}}>
|
||||
The AI Editing Stack
|
||||
</h1>
|
||||
</AbsoluteFill>
|
||||
</Sequence>
|
||||
|
||||
{/* Next segment */}
|
||||
<Sequence from={300} durationInFrames={450}>
|
||||
<Video src="/segments/demo.mp4" />
|
||||
</Sequence>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 渲染输出
|
||||
|
||||
```bash
|
||||
npx remotion render src/index.ts VlogComposition output.mp4
|
||||
```
|
||||
|
||||
有关详细模式和API参考,请参阅[Remotion文档](https://www.remotion.dev/docs)。
|
||||
|
||||
## 层级 5:生成资产(ElevenLabs / fal.ai)
|
||||
|
||||
仅生成所需内容。不要生成整个视频。
|
||||
|
||||
### 使用ElevenLabs进行画外音
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
|
||||
resp = requests.post(
|
||||
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
|
||||
headers={
|
||||
"xi-api-key": os.environ["ELEVENLABS_API_KEY"],
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"text": "Your narration text here",
|
||||
"model_id": "eleven_turbo_v2_5",
|
||||
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
|
||||
}
|
||||
)
|
||||
with open("voiceover.mp3", "wb") as f:
|
||||
f.write(resp.content)
|
||||
```
|
||||
|
||||
### 使用fal.ai生成音乐和音效
|
||||
|
||||
使用 `fal-ai-media` 技能进行:
|
||||
|
||||
* 背景音乐生成
|
||||
* 音效(用于视频转音频的ThinkSound模型)
|
||||
* 转场音效
|
||||
|
||||
### 使用fal.ai生成视觉效果
|
||||
|
||||
用于不存在的插入镜头、缩略图或B-roll素材:
|
||||
|
||||
```
|
||||
generate(app_id: "fal-ai/nano-banana-pro", input_data: {
|
||||
"prompt": "professional thumbnail for tech vlog, dark background, code on screen",
|
||||
"image_size": "landscape_16_9"
|
||||
})
|
||||
```
|
||||
|
||||
### VideoDB生成式音频
|
||||
|
||||
如果配置了VideoDB:
|
||||
|
||||
```python
|
||||
voiceover = coll.generate_voice(text="Narration here", voice="alloy")
|
||||
music = coll.generate_music(prompt="lo-fi background for coding vlog", duration=120)
|
||||
sfx = coll.generate_sound_effect(prompt="subtle whoosh transition")
|
||||
```
|
||||
|
||||
## 层级 6:最终润色(Descript / CapCut)
|
||||
|
||||
最后一层由人工完成。使用传统编辑器进行:
|
||||
|
||||
* **节奏调整**:调整感觉太快或太慢的剪辑
|
||||
* **字幕**:自动生成,然后手动清理
|
||||
* **色彩分级**:基本校正和氛围调整
|
||||
* **最终音频混音**:平衡人声、音乐和音效的电平
|
||||
* **导出**:平台特定的格式和质量设置
|
||||
|
||||
品味体现在此。AI清理重复性工作。你做出最终决定。
|
||||
|
||||
## 社交媒体重新构图
|
||||
|
||||
不同平台需要不同的宽高比:
|
||||
|
||||
| 平台 | 宽高比 | 分辨率 |
|
||||
|----------|-------------|------------|
|
||||
| YouTube | 16:9 | 1920x1080 |
|
||||
| TikTok / Reels | 9:16 | 1080x1920 |
|
||||
| Instagram Feed | 1:1 | 1080x1080 |
|
||||
| X / Twitter | 16:9 或 1:1 | 1280x720 或 720x720 |
|
||||
|
||||
### 使用FFmpeg重新构图
|
||||
|
||||
```bash
|
||||
# 16:9 to 9:16 (center crop)
|
||||
ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" vertical.mp4
|
||||
|
||||
# 16:9 to 1:1 (center crop)
|
||||
ffmpeg -i input.mp4 -vf "crop=ih:ih,scale=1080:1080" square.mp4
|
||||
```
|
||||
|
||||
### 使用VideoDB重新构图
|
||||
|
||||
```python
|
||||
from videodb import ReframeMode
|
||||
|
||||
# Smart reframe (AI-guided subject tracking)
|
||||
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
|
||||
```
|
||||
|
||||
## 场景检测与自动剪辑
|
||||
|
||||
### FFmpeg场景检测
|
||||
|
||||
```bash
|
||||
# Detect scene changes (threshold 0.3 = moderate sensitivity)
|
||||
ffmpeg -i input.mp4 -vf "select='gt(scene,0.3)',showinfo" -vsync vfr -f null - 2>&1 | grep showinfo
|
||||
```
|
||||
|
||||
### 用于自动剪辑的静音检测
|
||||
|
||||
```bash
|
||||
# Find silent segments (useful for cutting dead air)
|
||||
ffmpeg -i input.mp4 -af silencedetect=noise=-30dB:d=2 -f null - 2>&1 | grep silence
|
||||
```
|
||||
|
||||
### 精彩片段提取
|
||||
|
||||
使用Claude分析转录稿 + 场景时间戳:
|
||||
|
||||
```
|
||||
"Given this transcript with timestamps and these scene change points,
|
||||
identify the 5 most engaging 30-second clips for social media."
|
||||
```
|
||||
|
||||
## 每个工具最擅长什么
|
||||
|
||||
| 工具 | 优势 | 劣势 |
|
||||
|------|----------|----------|
|
||||
| Claude / Codex | 组织、规划、代码生成 | 不是创意品味层 |
|
||||
| FFmpeg | 确定性剪辑、批量处理、格式转换 | 无可视化编辑UI |
|
||||
| Remotion | 可编程叠加层、可组合场景、可重复使用模板 | 对非开发者有学习曲线 |
|
||||
| Screen Studio | 即时获得精致的屏幕录制 | 仅限屏幕采集 |
|
||||
| ElevenLabs | 人声、旁白、音乐、音效 | 不是工作流程的核心 |
|
||||
| Descript / CapCut | 最终节奏调整、字幕、润色 | 手动操作,不可自动化 |
|
||||
|
||||
## 关键原则
|
||||
|
||||
1. **编辑,而非生成。** 此工作流程用于剪辑真实素材,而非根据提示创建。
|
||||
2. **先结构,后风格。** 在接触任何视觉元素之前,先在层级2确定好故事结构。
|
||||
3. **FFmpeg是支柱。** 枯燥但关键。长素材在此变得易于管理。
|
||||
4. **Remotion用于可重复性。** 如果你会多次执行某项操作,就将其制作成Remotion组件。
|
||||
5. **选择性生成。** 仅对不存在的资产使用AI生成,而非所有内容。
|
||||
6. **品味是最后一层。** AI清理重复性工作。你做出最终的创意决定。
|
||||
|
||||
## 相关技能
|
||||
|
||||
* `fal-ai-media` — AI图像、视频和音频生成
|
||||
* `videodb` — 服务器端视频处理、索引和流媒体
|
||||
* `content-engine` — 平台原生内容分发
|
||||
386
docs/zh-CN/skills/videodb/SKILL.md
Normal file
386
docs/zh-CN/skills/videodb/SKILL.md
Normal file
@@ -0,0 +1,386 @@
|
||||
---
|
||||
name: videodb
|
||||
description: 视频与音频的查看、理解与行动。查看:从本地文件、URL、RTSP/直播源或实时录制桌面获取内容;返回实时上下文和可播放流链接。理解:提取帧,构建视觉/语义/时间索引,并通过时间戳和自动剪辑搜索片段。行动:转码和标准化(编解码器、帧率、分辨率、宽高比),执行时间线编辑(字幕、文本/图像叠加、品牌化、音频叠加、配音、翻译),生成媒体资源(图像、音频、视频),并为直播流或桌面捕获的事件创建实时警报。
|
||||
origin: ECC
|
||||
allowed-tools: Read Grep Glob Bash(python:*)
|
||||
argument-hint: "[task description]"
|
||||
---
|
||||
|
||||
# VideoDB 技能
|
||||
|
||||
**针对视频、直播流和桌面会话的感知 + 记忆 + 操作。**
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 桌面感知
|
||||
|
||||
* 启动/停止**桌面会话**,捕获**屏幕、麦克风和系统音频**
|
||||
* 流式传输**实时上下文**并存储**片段式会话记忆**
|
||||
* 对所说的内容和屏幕上发生的事情运行**实时警报/触发器**
|
||||
* 生成**会话摘要**、可搜索的时间线和**可播放的证据链接**
|
||||
|
||||
### 视频摄取 + 流
|
||||
|
||||
* 摄取**文件或URL**并返回**可播放的网络流链接**
|
||||
* 转码/标准化:**编解码器、比特率、帧率、分辨率、宽高比**
|
||||
|
||||
### 索引 + 搜索(时间戳 + 证据)
|
||||
|
||||
* 构建**视觉**、**语音**和**关键词**索引
|
||||
* 搜索并返回带有**时间戳**和**可播放证据**的精确时刻
|
||||
* 从搜索结果自动创建**片段**
|
||||
|
||||
### 时间线编辑 + 生成
|
||||
|
||||
* 字幕:**生成**、**翻译**、**烧录**
|
||||
* 叠加层:**文本/图片/品牌标识**,动态字幕
|
||||
* 音频:**背景音乐**、**画外音**、**配音**
|
||||
* 通过**时间线操作**进行程序化合成和导出
|
||||
|
||||
### 直播流(RTSP)+ 监控
|
||||
|
||||
* 连接**RTSP/实时流**
|
||||
* 运行**实时视觉和语音理解**,并为监控工作流发出**事件/警报**
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 常见输入
|
||||
|
||||
* 本地**文件路径**、公共**URL**或**RTSP URL**
|
||||
* 桌面捕获请求:**启动 / 停止 / 总结会话**
|
||||
* 期望的操作:获取理解上下文、转码规格、索引规格、搜索查询、片段范围、时间线编辑、警报规则
|
||||
|
||||
### 常见输出
|
||||
|
||||
* **流URL**
|
||||
* 带有**时间戳**和**证据链接**的搜索结果
|
||||
* 生成的资产:字幕、音频、图片、片段
|
||||
* 用于直播流的**事件/警报负载**
|
||||
* 桌面**会话摘要**和记忆条目
|
||||
|
||||
### 运行 Python 代码
|
||||
|
||||
在运行任何 VideoDB 代码之前,请切换到项目目录并加载环境变量:
|
||||
|
||||
```python
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(".env")
|
||||
|
||||
import videodb
|
||||
conn = videodb.connect()
|
||||
```
|
||||
|
||||
这会从以下位置读取 `VIDEO_DB_API_KEY`:
|
||||
|
||||
1. 环境变量(如果已导出)
|
||||
2. 项目当前目录中的 `.env` 文件
|
||||
|
||||
如果密钥缺失,`videodb.connect()` 会自动引发 `AuthenticationError`。
|
||||
|
||||
当简短的內联命令有效时,不要编写脚本文件。
|
||||
|
||||
编写內联 Python (`python -c "..."`) 时,始终使用格式正确的代码——使用分号分隔语句并保持可读性。对于任何超过约3条语句的内容,请改用 heredoc:
|
||||
|
||||
```bash
|
||||
python << 'EOF'
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(".env")
|
||||
|
||||
import videodb
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
print(f"Videos: {len(coll.get_videos())}")
|
||||
EOF
|
||||
```
|
||||
|
||||
### 设置
|
||||
|
||||
当用户要求“设置 videodb”或类似操作时:
|
||||
|
||||
### 1. 安装 SDK
|
||||
|
||||
```bash
|
||||
pip install "videodb[capture]" python-dotenv
|
||||
```
|
||||
|
||||
如果在 Linux 上 `videodb[capture]` 失败,请安装不带捕获扩展的版本:
|
||||
|
||||
```bash
|
||||
pip install videodb python-dotenv
|
||||
```
|
||||
|
||||
### 2. 配置 API 密钥
|
||||
|
||||
用户必须使用**任一**方法设置 `VIDEO_DB_API_KEY`:
|
||||
|
||||
* **在终端中导出**(在启动 Claude 之前):`export VIDEO_DB_API_KEY=your-key`
|
||||
* **项目 `.env` 文件**:将 `VIDEO_DB_API_KEY=your-key` 保存在项目的 `.env` 文件中
|
||||
|
||||
免费获取 API 密钥,请访问 [console.videodb.io](https://console.videodb.io)(50 次免费上传,无需信用卡)。
|
||||
|
||||
**请勿**自行读取、写入或处理 API 密钥。始终让用户设置。
|
||||
|
||||
### 快速参考
|
||||
|
||||
### 上传媒体
|
||||
|
||||
```python
|
||||
# URL
|
||||
video = coll.upload(url="https://example.com/video.mp4")
|
||||
|
||||
# YouTube
|
||||
video = coll.upload(url="https://www.youtube.com/watch?v=VIDEO_ID")
|
||||
|
||||
# Local file
|
||||
video = coll.upload(file_path="/path/to/video.mp4")
|
||||
```
|
||||
|
||||
### 转录 + 字幕
|
||||
|
||||
```python
|
||||
# force=True skips the error if the video is already indexed
|
||||
video.index_spoken_words(force=True)
|
||||
text = video.get_transcript_text()
|
||||
stream_url = video.add_subtitle()
|
||||
```
|
||||
|
||||
### 在视频内搜索
|
||||
|
||||
```python
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# search() raises InvalidRequestError when no results are found.
|
||||
# Always wrap in try/except and treat "No results found" as empty.
|
||||
try:
|
||||
results = video.search("product demo")
|
||||
shots = results.get_shots()
|
||||
stream_url = results.compile()
|
||||
except InvalidRequestError as e:
|
||||
if "No results found" in str(e):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
### 场景搜索
|
||||
|
||||
```python
|
||||
import re
|
||||
from videodb import SearchType, IndexType, SceneExtractionType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
# index_scenes() has no force parameter — it raises an error if a scene
|
||||
# index already exists. Extract the existing index ID from the error.
|
||||
try:
|
||||
scene_index_id = video.index_scenes(
|
||||
extraction_type=SceneExtractionType.shot_based,
|
||||
prompt="Describe the visual content in this scene.",
|
||||
)
|
||||
except Exception as e:
|
||||
match = re.search(r"id\s+([a-f0-9]+)", str(e))
|
||||
if match:
|
||||
scene_index_id = match.group(1)
|
||||
else:
|
||||
raise
|
||||
|
||||
# Use score_threshold to filter low-relevance noise (recommended: 0.3+)
|
||||
try:
|
||||
results = video.search(
|
||||
query="person writing on a whiteboard",
|
||||
search_type=SearchType.semantic,
|
||||
index_type=IndexType.scene,
|
||||
scene_index_id=scene_index_id,
|
||||
score_threshold=0.3,
|
||||
)
|
||||
shots = results.get_shots()
|
||||
stream_url = results.compile()
|
||||
except InvalidRequestError as e:
|
||||
if "No results found" in str(e):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
### 时间线编辑
|
||||
|
||||
**重要提示:** 在构建时间线之前,请务必验证时间戳:
|
||||
|
||||
* `start` 必须 >= 0(负值会被静默接受,但会产生损坏的输出)
|
||||
* `start` 必须 < `end`
|
||||
* `end` 必须 <= `video.length`
|
||||
|
||||
```python
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
timeline = Timeline(conn)
|
||||
timeline.add_inline(VideoAsset(asset_id=video.id, start=10, end=30))
|
||||
timeline.add_overlay(0, TextAsset(text="The End", duration=3, style=TextStyle(fontsize=36)))
|
||||
stream_url = timeline.generate_stream()
|
||||
```
|
||||
|
||||
### 转码视频(分辨率 / 质量更改)
|
||||
|
||||
```python
|
||||
from videodb import TranscodeMode, VideoConfig, AudioConfig
|
||||
|
||||
# Change resolution, quality, or aspect ratio server-side
|
||||
job_id = conn.transcode(
|
||||
source="https://example.com/video.mp4",
|
||||
callback_url="https://example.com/webhook",
|
||||
mode=TranscodeMode.economy,
|
||||
video_config=VideoConfig(resolution=720, quality=23, aspect_ratio="16:9"),
|
||||
audio_config=AudioConfig(mute=False),
|
||||
)
|
||||
```
|
||||
|
||||
### 调整宽高比(适用于社交平台)
|
||||
|
||||
**警告:** `reframe()` 是一项缓慢的服务器端操作。对于长视频,可能需要几分钟,并可能超时。最佳实践:
|
||||
|
||||
* 尽可能使用 `start`/`end` 限制为短片段
|
||||
* 对于全长视频,使用 `callback_url` 进行异步处理
|
||||
* 先在 `Timeline` 上修剪视频,然后调整较短结果的宽高比
|
||||
|
||||
```python
|
||||
from videodb import ReframeMode
|
||||
|
||||
# Always prefer reframing a short segment:
|
||||
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
|
||||
|
||||
# Async reframe for full-length videos (returns None, result via webhook):
|
||||
video.reframe(target="vertical", callback_url="https://example.com/webhook")
|
||||
|
||||
# Presets: "vertical" (9:16), "square" (1:1), "landscape" (16:9)
|
||||
reframed = video.reframe(start=0, end=60, target="square")
|
||||
|
||||
# Custom dimensions
|
||||
reframed = video.reframe(start=0, end=60, target={"width": 1280, "height": 720})
|
||||
```
|
||||
|
||||
### 生成式媒体
|
||||
|
||||
```python
|
||||
image = coll.generate_image(
|
||||
prompt="a sunset over mountains",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```python
|
||||
from videodb.exceptions import AuthenticationError, InvalidRequestError
|
||||
|
||||
try:
|
||||
conn = videodb.connect()
|
||||
except AuthenticationError:
|
||||
print("Check your VIDEO_DB_API_KEY")
|
||||
|
||||
try:
|
||||
video = coll.upload(url="https://example.com/video.mp4")
|
||||
except InvalidRequestError as e:
|
||||
print(f"Upload failed: {e}")
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 场景 | 错误信息 | 解决方案 |
|
||||
|----------|--------------|----------|
|
||||
| 为已索引的视频建立索引 | `Spoken word index for video already exists` | 使用 `video.index_spoken_words(force=True)` 跳过已索引的情况 |
|
||||
| 场景索引已存在 | `Scene index with id XXXX already exists` | 使用 `re.search(r"id\s+([a-f0-9]+)", str(e))` 从错误中提取现有的 `scene_index_id` |
|
||||
| 搜索无匹配项 | `InvalidRequestError: No results found` | 捕获异常并视为空结果 (`shots = []`) |
|
||||
| 调整宽高比超时 | 长视频上无限期阻塞 | 使用 `start`/`end` 限制片段,或传递 `callback_url` 进行异步处理 |
|
||||
| Timeline 上的负时间戳 | 静默产生损坏的流 | 在创建 `VideoAsset` 之前,始终验证 `start >= 0` |
|
||||
| `generate_video()` / `create_collection()` 失败 | `Operation not allowed` 或 `maximum limit` | 计划限制的功能——告知用户关于计划限制 |
|
||||
|
||||
## 示例
|
||||
|
||||
### 规范提示
|
||||
|
||||
* "开始桌面捕获,并在密码字段出现时发出警报。"
|
||||
* "记录我的会话并在结束时生成可操作的摘要。"
|
||||
* "摄取此文件并返回可播放的流链接。"
|
||||
* "为此文件夹建立索引,并找到每个有人的场景,返回时间戳。"
|
||||
* "生成字幕,将其烧录进去,并添加轻背景音乐。"
|
||||
* "连接此 RTSP URL,并在有人进入区域时发出警报。"
|
||||
|
||||
### 屏幕录制(桌面捕获)
|
||||
|
||||
使用 `ws_listener.py` 在录制会话期间捕获 WebSocket 事件。桌面捕获仅支持 **macOS**。
|
||||
|
||||
#### 快速开始
|
||||
|
||||
1. **选择状态目录**:`STATE_DIR="${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}"`
|
||||
2. **启动监听器**:`VIDEODB_EVENTS_DIR="$STATE_DIR" python scripts/ws_listener.py --clear "$STATE_DIR" &`
|
||||
3. **获取 WebSocket ID**:`cat "$STATE_DIR/videodb_ws_id"`
|
||||
4. **运行捕获代码**(完整工作流程请参阅 reference/capture.md)
|
||||
5. **事件写入**:`$STATE_DIR/videodb_events.jsonl`
|
||||
|
||||
每当开始新的捕获运行时,请使用 `--clear`,以免过时的转录和视觉事件泄露到新会话中。
|
||||
|
||||
#### 查询事件
|
||||
|
||||
```python
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
events_dir = Path(os.environ.get("VIDEODB_EVENTS_DIR", Path.home() / ".local" / "state" / "videodb"))
|
||||
events_file = events_dir / "videodb_events.jsonl"
|
||||
events = []
|
||||
|
||||
if events_file.exists():
|
||||
with events_file.open(encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
transcripts = [e["data"]["text"] for e in events if e.get("channel") == "transcript"]
|
||||
cutoff = time.time() - 300
|
||||
recent_visual = [
|
||||
e for e in events
|
||||
if e.get("channel") == "visual_index" and e["unix_ts"] > cutoff
|
||||
]
|
||||
```
|
||||
|
||||
## 附加文档
|
||||
|
||||
参考文档位于与此 SKILL.md 文件相邻的 `reference/` 目录中。如果需要,请使用 Glob 工具来定位。
|
||||
|
||||
* [reference/api-reference.md](reference/api-reference.md) - 完整的 VideoDB Python SDK API 参考
|
||||
* [reference/search.md](reference/search.md) - 视频搜索深入指南(口语词和基于场景的)
|
||||
* [reference/editor.md](reference/editor.md) - 时间线编辑、资产和合成
|
||||
* [reference/streaming.md](reference/streaming.md) - HLS 流和即时播放
|
||||
* [reference/generative.md](reference/generative.md) - AI 驱动的媒体生成(图像、视频、音频)
|
||||
* [reference/rtstream.md](reference/rtstream.md) - 直播流摄取工作流程(RTSP/RTMP)
|
||||
* [reference/rtstream-reference.md](reference/rtstream-reference.md) - RTStream SDK 方法和 AI 管道
|
||||
* [reference/capture.md](reference/capture.md) - 桌面捕获工作流程
|
||||
* [reference/capture-reference.md](reference/capture-reference.md) - Capture SDK 和 WebSocket 事件
|
||||
* [reference/use-cases.md](reference/use-cases.md) - 常见的视频处理模式和示例
|
||||
|
||||
**当 VideoDB 支持该操作时,不要使用 ffmpeg、moviepy 或本地编码工具。** 以下所有操作均由 VideoDB 在服务器端处理——修剪、合并片段、叠加音频或音乐、添加字幕、文本/图像叠加层、转码、分辨率更改、宽高比转换、为平台要求调整大小、转录和媒体生成。仅当 reference/editor.md 中“限制”部分列出的操作(转场、速度变化、裁剪/缩放、色彩分级、音量混合)时,才回退到本地工具。
|
||||
|
||||
### 何时使用什么
|
||||
|
||||
| 问题 | VideoDB 解决方案 |
|
||||
|---------|-----------------|
|
||||
| 平台拒绝视频宽高比或分辨率 | 使用 `VideoConfig` 的 `video.reframe()` 或 `conn.transcode()` |
|
||||
| 需要为 Twitter/Instagram/TikTok 调整视频大小 | `video.reframe(target="vertical")` 或 `target="square"` |
|
||||
| 需要更改分辨率(例如 1080p → 720p) | 使用 `VideoConfig(resolution=720)` 的 `conn.transcode()` |
|
||||
| 需要在视频上叠加音频/音乐 | 在 `Timeline` 上使用 `AudioAsset` |
|
||||
| 需要添加字幕 | `video.add_subtitle()` 或 `CaptionAsset` |
|
||||
| 需要合并/修剪片段 | 在 `Timeline` 上使用 `VideoAsset` |
|
||||
| 需要生成画外音、音乐或音效 | `coll.generate_voice()`、`generate_music()`、`generate_sound_effect()` |
|
||||
|
||||
## 来源
|
||||
|
||||
此技能的参考材料在 `skills/videodb/reference/` 下本地提供。
|
||||
请使用上面的本地副本,而不是在运行时遵循外部存储库链接。
|
||||
|
||||
**维护者:** [VideoDB](https://www.videodb.io/)
|
||||
550
docs/zh-CN/skills/videodb/reference/api-reference.md
Normal file
550
docs/zh-CN/skills/videodb/reference/api-reference.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# 完整 API 参考
|
||||
|
||||
VideoDB 技能参考材料。关于使用指南和工作流选择,请从 [../SKILL.md](../SKILL.md) 开始。
|
||||
|
||||
## 连接
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect(
|
||||
api_key="your-api-key", # or set VIDEO_DB_API_KEY env var
|
||||
base_url=None, # custom API endpoint (optional)
|
||||
)
|
||||
```
|
||||
|
||||
**返回:** `Connection` 对象
|
||||
|
||||
### 连接方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `conn.get_collection(collection_id="default")` | `Collection` | 获取集合(若无 ID 则获取默认集合) |
|
||||
| `conn.get_collections()` | `list[Collection]` | 列出所有集合 |
|
||||
| `conn.create_collection(name, description, is_public=False)` | `Collection` | 创建新集合 |
|
||||
| `conn.update_collection(id, name, description)` | `Collection` | 更新集合 |
|
||||
| `conn.check_usage()` | `dict` | 获取账户使用统计 |
|
||||
| `conn.upload(source, media_type, name, ...)` | `Video\|Audio\|Image` | 上传到默认集合 |
|
||||
| `conn.record_meeting(meeting_url, bot_name, ...)` | `Meeting` | 录制会议 |
|
||||
| `conn.create_capture_session(...)` | `CaptureSession` | 创建捕获会话(见 [capture-reference.md](capture-reference.md)) |
|
||||
| `conn.youtube_search(query, result_threshold, duration)` | `list[dict]` | 搜索 YouTube |
|
||||
| `conn.transcode(source, callback_url, mode, ...)` | `str` | 转码视频(返回作业 ID) |
|
||||
| `conn.get_transcode_details(job_id)` | `dict` | 获取转码作业状态和详情 |
|
||||
| `conn.connect_websocket(collection_id)` | `WebSocketConnection` | 连接到 WebSocket(见 [capture-reference.md](capture-reference.md)) |
|
||||
|
||||
### 转码
|
||||
|
||||
使用自定义分辨率、质量和音频设置从 URL 转码视频。处理在服务器端进行——无需本地 ffmpeg。
|
||||
|
||||
```python
|
||||
from videodb import TranscodeMode, VideoConfig, AudioConfig
|
||||
|
||||
job_id = conn.transcode(
|
||||
source="https://example.com/video.mp4",
|
||||
callback_url="https://example.com/webhook",
|
||||
mode=TranscodeMode.economy,
|
||||
video_config=VideoConfig(resolution=720, quality=23),
|
||||
audio_config=AudioConfig(mute=False),
|
||||
)
|
||||
```
|
||||
|
||||
#### transcode 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `source` | `str` | 必需 | 要转码的视频 URL(最好是可下载的 URL) |
|
||||
| `callback_url` | `str` | 必需 | 转码完成时接收回调的 URL |
|
||||
| `mode` | `TranscodeMode` | `TranscodeMode.economy` | 转码速度:`economy` 或 `lightning` |
|
||||
| `video_config` | `VideoConfig` | `VideoConfig()` | 视频编码设置 |
|
||||
| `audio_config` | `AudioConfig` | `AudioConfig()` | 音频编码设置 |
|
||||
|
||||
返回一个作业 ID (`str`)。使用 `conn.get_transcode_details(job_id)` 来检查作业状态。
|
||||
|
||||
```python
|
||||
details = conn.get_transcode_details(job_id)
|
||||
```
|
||||
|
||||
#### VideoConfig
|
||||
|
||||
```python
|
||||
from videodb import VideoConfig, ResizeMode
|
||||
|
||||
config = VideoConfig(
|
||||
resolution=720, # Target resolution height (e.g. 480, 720, 1080)
|
||||
quality=23, # Encoding quality (lower = better, default 23)
|
||||
framerate=30, # Target framerate
|
||||
aspect_ratio="16:9", # Target aspect ratio
|
||||
resize_mode=ResizeMode.crop, # How to fit: crop, fit, or pad
|
||||
)
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 默认值 | 描述 |
|
||||
|-------|------|---------|-------------|
|
||||
| `resolution` | `int\|None` | `None` | 目标分辨率高度(像素) |
|
||||
| `quality` | `int` | `23` | 编码质量(值越低,质量越高) |
|
||||
| `framerate` | `int\|None` | `None` | 目标帧率 |
|
||||
| `aspect_ratio` | `str\|None` | `None` | 目标宽高比(例如 `"16:9"`, `"9:16"`) |
|
||||
| `resize_mode` | `str` | `ResizeMode.crop` | 调整大小策略:`crop`, `fit`, 或 `pad` |
|
||||
|
||||
#### AudioConfig
|
||||
|
||||
```python
|
||||
from videodb import AudioConfig
|
||||
|
||||
config = AudioConfig(mute=False)
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 默认值 | 描述 |
|
||||
|-------|------|---------|-------------|
|
||||
| `mute` | `bool` | `False` | 静音音轨 |
|
||||
|
||||
## 集合
|
||||
|
||||
```python
|
||||
coll = conn.get_collection()
|
||||
```
|
||||
|
||||
### 集合方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `coll.get_videos()` | `list[Video]` | 列出所有视频 |
|
||||
| `coll.get_video(video_id)` | `Video` | 获取特定视频 |
|
||||
| `coll.get_audios()` | `list[Audio]` | 列出所有音频 |
|
||||
| `coll.get_audio(audio_id)` | `Audio` | 获取特定音频 |
|
||||
| `coll.get_images()` | `list[Image]` | 列出所有图像 |
|
||||
| `coll.get_image(image_id)` | `Image` | 获取特定图像 |
|
||||
| `coll.upload(url=None, file_path=None, media_type=None, name=None)` | `Video\|Audio\|Image` | 上传媒体 |
|
||||
| `coll.search(query, search_type, index_type, score_threshold, namespace, scene_index_id, ...)` | `SearchResult` | 在集合中搜索(仅语义搜索;关键词和场景搜索会引发 `NotImplementedError`) |
|
||||
| `coll.generate_image(prompt, aspect_ratio="1:1")` | `Image` | 使用 AI 生成图像 |
|
||||
| `coll.generate_video(prompt, duration=5)` | `Video` | 使用 AI 生成视频 |
|
||||
| `coll.generate_music(prompt, duration=5)` | `Audio` | 使用 AI 生成音乐 |
|
||||
| `coll.generate_sound_effect(prompt, duration=2)` | `Audio` | 生成音效 |
|
||||
| `coll.generate_voice(text, voice_name="Default")` | `Audio` | 从文本生成语音 |
|
||||
| `coll.generate_text(prompt, model_name="basic", response_type="text")` | `dict` | LLM 文本生成——通过 `["output"]` 访问结果 |
|
||||
| `coll.dub_video(video_id, language_code)` | `Video` | 将视频配音为另一种语言 |
|
||||
| `coll.record_meeting(meeting_url, bot_name, ...)` | `Meeting` | 录制实时会议 |
|
||||
| `coll.create_capture_session(...)` | `CaptureSession` | 创建捕获会话(见 [capture-reference.md](capture-reference.md)) |
|
||||
| `coll.get_capture_session(...)` | `CaptureSession` | 检索捕获会话(见 [capture-reference.md](capture-reference.md)) |
|
||||
| `coll.connect_rtstream(url, name, ...)` | `RTStream` | 连接到实时流(见 [rtstream-reference.md](rtstream-reference.md)) |
|
||||
| `coll.make_public()` | `None` | 使集合公开 |
|
||||
| `coll.make_private()` | `None` | 使集合私有 |
|
||||
| `coll.delete_video(video_id)` | `None` | 删除视频 |
|
||||
| `coll.delete_audio(audio_id)` | `None` | 删除音频 |
|
||||
| `coll.delete_image(image_id)` | `None` | 删除图像 |
|
||||
| `coll.delete()` | `None` | 删除集合 |
|
||||
|
||||
### 上传参数
|
||||
|
||||
```python
|
||||
video = coll.upload(
|
||||
url=None, # Remote URL (HTTP, YouTube)
|
||||
file_path=None, # Local file path
|
||||
media_type=None, # "video", "audio", or "image" (auto-detected if omitted)
|
||||
name=None, # Custom name for the media
|
||||
description=None, # Description
|
||||
callback_url=None, # Webhook URL for async notification
|
||||
)
|
||||
```
|
||||
|
||||
## 视频对象
|
||||
|
||||
```python
|
||||
video = coll.get_video(video_id)
|
||||
```
|
||||
|
||||
### 视频属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `video.id` | `str` | 唯一视频 ID |
|
||||
| `video.collection_id` | `str` | 父集合 ID |
|
||||
| `video.name` | `str` | 视频名称 |
|
||||
| `video.description` | `str` | 视频描述 |
|
||||
| `video.length` | `float` | 时长(秒) |
|
||||
| `video.stream_url` | `str` | 默认流 URL |
|
||||
| `video.player_url` | `str` | 播放器嵌入 URL |
|
||||
| `video.thumbnail_url` | `str` | 缩略图 URL |
|
||||
|
||||
### 视频方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `video.generate_stream(timeline=None)` | `str` | 生成流 URL(可选的 `[(start, end)]` 元组时间线) |
|
||||
| `video.play()` | `str` | 在浏览器中打开流,返回播放器 URL |
|
||||
| `video.index_spoken_words(language_code=None, force=False)` | `None` | 为语音搜索建立索引。使用 `force=True` 在已建立索引时跳过。 |
|
||||
| `video.index_scenes(extraction_type, prompt, extraction_config, metadata, model_name, name, scenes, callback_url)` | `str` | 索引视觉场景(返回 scene\_index\_id) |
|
||||
| `video.index_visuals(prompt, batch_config, ...)` | `str` | 索引视觉内容(返回 scene\_index\_id) |
|
||||
| `video.index_audio(prompt, model_name, ...)` | `str` | 使用 LLM 索引音频(返回 scene\_index\_id) |
|
||||
| `video.get_transcript(start=None, end=None)` | `list[dict]` | 获取带时间戳的转录稿 |
|
||||
| `video.get_transcript_text(start=None, end=None)` | `str` | 获取完整转录文本 |
|
||||
| `video.generate_transcript(force=None)` | `dict` | 生成转录稿 |
|
||||
| `video.translate_transcript(language, additional_notes)` | `list[dict]` | 翻译转录稿 |
|
||||
| `video.search(query, search_type, index_type, filter, **kwargs)` | `SearchResult` | 在视频内搜索 |
|
||||
| `video.add_subtitle(style=SubtitleStyle())` | `str` | 添加字幕(返回流 URL) |
|
||||
| `video.generate_thumbnail(time=None)` | `str\|Image` | 生成缩略图 |
|
||||
| `video.get_thumbnails()` | `list[Image]` | 获取所有缩略图 |
|
||||
| `video.extract_scenes(extraction_type, extraction_config)` | `SceneCollection` | 提取场景 |
|
||||
| `video.reframe(start, end, target, mode, callback_url)` | `Video\|None` | 调整视频宽高比 |
|
||||
| `video.clip(prompt, content_type, model_name)` | `str` | 根据提示生成剪辑(返回流 URL) |
|
||||
| `video.insert_video(video, timestamp)` | `str` | 在时间戳处插入视频 |
|
||||
| `video.download(name=None)` | `dict` | 下载视频 |
|
||||
| `video.delete()` | `None` | 删除视频 |
|
||||
|
||||
### 调整宽高比
|
||||
|
||||
将视频转换为不同的宽高比,可选智能对象跟踪。处理在服务器端进行。
|
||||
|
||||
> **警告:** 调整宽高比是缓慢的服务器端操作。对于长视频可能需要几分钟,并可能超时。始终使用 `start`/`end` 来限制片段,或传递 `callback_url` 进行异步处理。
|
||||
|
||||
```python
|
||||
from videodb import ReframeMode
|
||||
|
||||
# Always prefer short segments to avoid timeouts:
|
||||
reframed = video.reframe(start=0, end=60, target="vertical", mode=ReframeMode.smart)
|
||||
|
||||
# Async reframe for full-length videos (returns None, result via webhook):
|
||||
video.reframe(target="vertical", callback_url="https://example.com/webhook")
|
||||
|
||||
# Custom dimensions
|
||||
reframed = video.reframe(start=0, end=60, target={"width": 1080, "height": 1080})
|
||||
```
|
||||
|
||||
#### reframe 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `start` | `float\|None` | `None` | 开始时间(秒)(None = 开始) |
|
||||
| `end` | `float\|None` | `None` | 结束时间(秒)(None = 视频结束) |
|
||||
| `target` | `str\|dict` | `"vertical"` | 预设字符串(`"vertical"`, `"square"`, `"landscape"`)或 `{"width": int, "height": int}` |
|
||||
| `mode` | `str` | `ReframeMode.smart` | `"simple"`(中心裁剪)或 `"smart"`(对象跟踪) |
|
||||
| `callback_url` | `str\|None` | `None` | 异步通知的 Webhook URL |
|
||||
|
||||
当未提供 `callback_url` 时返回 `Video` 对象,否则返回 `None`。
|
||||
|
||||
## 音频对象
|
||||
|
||||
```python
|
||||
audio = coll.get_audio(audio_id)
|
||||
```
|
||||
|
||||
### 音频属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `audio.id` | `str` | 唯一音频 ID |
|
||||
| `audio.collection_id` | `str` | 父集合 ID |
|
||||
| `audio.name` | `str` | 音频名称 |
|
||||
| `audio.length` | `float` | 时长(秒) |
|
||||
|
||||
### 音频方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `audio.generate_url()` | `str` | 生成用于播放的签名 URL |
|
||||
| `audio.get_transcript(start=None, end=None)` | `list[dict]` | 获取带时间戳的转录稿 |
|
||||
| `audio.get_transcript_text(start=None, end=None)` | `str` | 获取完整转录文本 |
|
||||
| `audio.generate_transcript(force=None)` | `dict` | 生成转录稿 |
|
||||
| `audio.delete()` | `None` | 删除音频 |
|
||||
|
||||
## 图像对象
|
||||
|
||||
```python
|
||||
image = coll.get_image(image_id)
|
||||
```
|
||||
|
||||
### 图像属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `image.id` | `str` | 唯一图像 ID |
|
||||
| `image.collection_id` | `str` | 父集合 ID |
|
||||
| `image.name` | `str` | 图像名称 |
|
||||
| `image.url` | `str\|None` | 图像 URL(对于生成的图像可能为 `None`——请改用 `generate_url()`) |
|
||||
|
||||
### 图像方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `image.generate_url()` | `str` | 生成签名 URL |
|
||||
| `image.delete()` | `None` | 删除图像 |
|
||||
|
||||
## 时间线与编辑器
|
||||
|
||||
### 时间线
|
||||
|
||||
```python
|
||||
from videodb.timeline import Timeline
|
||||
|
||||
timeline = Timeline(conn)
|
||||
```
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `timeline.add_inline(asset)` | `None` | 在主轨道上顺序添加 `VideoAsset` |
|
||||
| `timeline.add_overlay(start, asset)` | `None` | 在时间戳处叠加 `AudioAsset`、`ImageAsset` 或 `TextAsset` |
|
||||
| `timeline.generate_stream()` | `str` | 编译并获取流 URL |
|
||||
|
||||
### 资产类型
|
||||
|
||||
#### VideoAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import VideoAsset
|
||||
|
||||
asset = VideoAsset(
|
||||
asset_id=video.id,
|
||||
start=0, # trim start (seconds)
|
||||
end=None, # trim end (seconds, None = full)
|
||||
)
|
||||
```
|
||||
|
||||
#### AudioAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import AudioAsset
|
||||
|
||||
asset = AudioAsset(
|
||||
asset_id=audio.id,
|
||||
start=0,
|
||||
end=None,
|
||||
disable_other_tracks=True, # mute original audio when True
|
||||
fade_in_duration=0, # seconds (max 5)
|
||||
fade_out_duration=0, # seconds (max 5)
|
||||
)
|
||||
```
|
||||
|
||||
#### ImageAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import ImageAsset
|
||||
|
||||
asset = ImageAsset(
|
||||
asset_id=image.id,
|
||||
duration=None, # display duration (seconds)
|
||||
width=100, # display width
|
||||
height=100, # display height
|
||||
x=80, # horizontal position (px from left)
|
||||
y=20, # vertical position (px from top)
|
||||
)
|
||||
```
|
||||
|
||||
#### TextAsset
|
||||
|
||||
```python
|
||||
from videodb.asset import TextAsset, TextStyle
|
||||
|
||||
asset = TextAsset(
|
||||
text="Hello World",
|
||||
duration=5,
|
||||
style=TextStyle(
|
||||
fontsize=24,
|
||||
fontcolor="black",
|
||||
boxcolor="white", # background box colour
|
||||
alpha=1.0,
|
||||
font="Sans",
|
||||
text_align="T", # text alignment within box
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
#### CaptionAsset(编辑器 API)
|
||||
|
||||
CaptionAsset 属于编辑器 API,它有自己的时间线、轨道和剪辑系统:
|
||||
|
||||
```python
|
||||
from videodb.editor import CaptionAsset, FontStyling
|
||||
|
||||
asset = CaptionAsset(
|
||||
src="auto", # "auto" or base64 ASS string
|
||||
font=FontStyling(name="Clear Sans", size=30),
|
||||
primary_color="&H00FFFFFF",
|
||||
)
|
||||
```
|
||||
|
||||
完整的 CaptionAsset 用法请见 [editor.md](../../../../../skills/videodb/reference/editor.md#caption-overlays) 中的编辑器 API。
|
||||
|
||||
## 视频搜索参数
|
||||
|
||||
```python
|
||||
results = video.search(
|
||||
query="your query",
|
||||
search_type=SearchType.semantic, # semantic, keyword, or scene
|
||||
index_type=IndexType.spoken_word, # spoken_word or scene
|
||||
result_threshold=None, # max number of results
|
||||
score_threshold=None, # minimum relevance score
|
||||
dynamic_score_percentage=None, # percentage of dynamic score
|
||||
scene_index_id=None, # target a specific scene index (pass via **kwargs)
|
||||
filter=[], # metadata filters for scene search
|
||||
)
|
||||
```
|
||||
|
||||
> **注意:** `filter` 是 `video.search()` 中的一个显式命名参数。`scene_index_id` 通过 `**kwargs` 传递给 API。
|
||||
>
|
||||
> **重要:** `video.search()` 在没有匹配项时会引发 `InvalidRequestError`,并附带消息 `"No results found"`。请始终将搜索调用包装在 try/except 中。对于场景搜索,请使用 `score_threshold=0.3` 或更高值来过滤低相关性的噪声。
|
||||
|
||||
对于场景搜索,请使用 `search_type=SearchType.semantic` 并设置 `index_type=IndexType.scene`。当针对特定场景索引时,传递 `scene_index_id`。详情请参阅 [search.md](search.md)。
|
||||
|
||||
## SearchResult 对象
|
||||
|
||||
```python
|
||||
results = video.search("query", search_type=SearchType.semantic)
|
||||
```
|
||||
|
||||
| 方法 | 返回值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `results.get_shots()` | `list[Shot]` | 获取匹配的片段列表 |
|
||||
| `results.compile()` | `str` | 将所有镜头编译为流 URL |
|
||||
| `results.play()` | `str` | 在浏览器中打开编译后的流 |
|
||||
|
||||
### Shot 属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `shot.video_id` | `str` | 源视频 ID |
|
||||
| `shot.video_length` | `float` | 源视频时长 |
|
||||
| `shot.video_title` | `str` | 源视频标题 |
|
||||
| `shot.start` | `float` | 开始时间(秒) |
|
||||
| `shot.end` | `float` | 结束时间(秒) |
|
||||
| `shot.text` | `str` | 匹配的文本内容 |
|
||||
| `shot.search_score` | `float` | 搜索相关性分数 |
|
||||
|
||||
| 方法 | 返回值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `shot.generate_stream()` | `str` | 流式传输此特定镜头 |
|
||||
| `shot.play()` | `str` | 在浏览器中打开镜头流 |
|
||||
|
||||
## Meeting 对象
|
||||
|
||||
```python
|
||||
meeting = coll.record_meeting(
|
||||
meeting_url="https://meet.google.com/...",
|
||||
bot_name="Bot",
|
||||
callback_url=None, # Webhook URL for status updates
|
||||
callback_data=None, # Optional dict passed through to callbacks
|
||||
time_zone="UTC", # Time zone for the meeting
|
||||
)
|
||||
```
|
||||
|
||||
### Meeting 属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `meeting.id` | `str` | 唯一会议 ID |
|
||||
| `meeting.collection_id` | `str` | 父集合 ID |
|
||||
| `meeting.status` | `str` | 当前状态 |
|
||||
| `meeting.video_id` | `str` | 录制视频 ID(完成后) |
|
||||
| `meeting.bot_name` | `str` | 机器人名称 |
|
||||
| `meeting.meeting_title` | `str` | 会议标题 |
|
||||
| `meeting.meeting_url` | `str` | 会议 URL |
|
||||
| `meeting.speaker_timeline` | `dict` | 发言人时间线数据 |
|
||||
| `meeting.is_active` | `bool` | 如果正在初始化或处理中则为真 |
|
||||
| `meeting.is_completed` | `bool` | 如果已完成则为真 |
|
||||
|
||||
### Meeting 方法
|
||||
|
||||
| 方法 | 返回值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `meeting.refresh()` | `Meeting` | 从服务器刷新数据 |
|
||||
| `meeting.wait_for_status(target_status, timeout=14400, interval=120)` | `bool` | 轮询直到达到指定状态 |
|
||||
|
||||
## RTStream 与 Capture
|
||||
|
||||
关于 RTStream(实时摄取、索引、转录),请参阅 [rtstream-reference.md](rtstream-reference.md)。
|
||||
|
||||
关于捕获会话(桌面录制、CaptureClient、频道),请参阅 [capture-reference.md](capture-reference.md)。
|
||||
|
||||
## 枚举与常量
|
||||
|
||||
### SearchType
|
||||
|
||||
```python
|
||||
from videodb import SearchType
|
||||
|
||||
SearchType.semantic # Natural language semantic search
|
||||
SearchType.keyword # Exact keyword matching
|
||||
SearchType.scene # Visual scene search (may require paid plan)
|
||||
SearchType.llm # LLM-powered search
|
||||
```
|
||||
|
||||
### SceneExtractionType
|
||||
|
||||
```python
|
||||
from videodb import SceneExtractionType
|
||||
|
||||
SceneExtractionType.shot_based # Automatic shot boundary detection
|
||||
SceneExtractionType.time_based # Fixed time interval extraction
|
||||
SceneExtractionType.transcript # Transcript-based scene extraction
|
||||
```
|
||||
|
||||
### SubtitleStyle
|
||||
|
||||
```python
|
||||
from videodb import SubtitleStyle
|
||||
|
||||
style = SubtitleStyle(
|
||||
font_name="Arial",
|
||||
font_size=18,
|
||||
primary_colour="&H00FFFFFF",
|
||||
bold=False,
|
||||
# ... see SubtitleStyle for all options
|
||||
)
|
||||
video.add_subtitle(style=style)
|
||||
```
|
||||
|
||||
### SubtitleAlignment 与 SubtitleBorderStyle
|
||||
|
||||
```python
|
||||
from videodb import SubtitleAlignment, SubtitleBorderStyle
|
||||
```
|
||||
|
||||
### TextStyle
|
||||
|
||||
```python
|
||||
from videodb import TextStyle
|
||||
# or: from videodb.asset import TextStyle
|
||||
|
||||
style = TextStyle(
|
||||
fontsize=24,
|
||||
fontcolor="black",
|
||||
boxcolor="white",
|
||||
font="Sans",
|
||||
text_align="T",
|
||||
alpha=1.0,
|
||||
)
|
||||
```
|
||||
|
||||
### 其他常量
|
||||
|
||||
```python
|
||||
from videodb import (
|
||||
IndexType, # spoken_word, scene
|
||||
MediaType, # video, audio, image
|
||||
Segmenter, # word, sentence, time
|
||||
SegmentationType, # sentence, llm
|
||||
TranscodeMode, # economy, lightning
|
||||
ResizeMode, # crop, fit, pad
|
||||
ReframeMode, # simple, smart
|
||||
RTStreamChannelType,
|
||||
)
|
||||
```
|
||||
|
||||
## 异常
|
||||
|
||||
```python
|
||||
from videodb.exceptions import (
|
||||
AuthenticationError, # Invalid or missing API key
|
||||
InvalidRequestError, # Bad parameters or malformed request
|
||||
RequestTimeoutError, # Request timed out
|
||||
SearchError, # Search operation failure (e.g. not indexed)
|
||||
VideodbError, # Base exception for all VideoDB errors
|
||||
)
|
||||
```
|
||||
|
||||
| 异常 | 常见原因 |
|
||||
|-----------|-------------|
|
||||
| `AuthenticationError` | 缺少或无效的 `VIDEO_DB_API_KEY` |
|
||||
| `InvalidRequestError` | 无效 URL、不支持的格式、错误参数 |
|
||||
| `RequestTimeoutError` | 服务器响应时间过长 |
|
||||
| `SearchError` | 在索引前进行搜索、无效的搜索类型 |
|
||||
| `VideodbError` | 服务器错误、网络问题、通用故障 |
|
||||
416
docs/zh-CN/skills/videodb/reference/capture-reference.md
Normal file
416
docs/zh-CN/skills/videodb/reference/capture-reference.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# 捕获参考
|
||||
|
||||
VideoDB 捕获会话的代码级详情。工作流程指南请参阅 [capture.md](capture.md)。
|
||||
|
||||
***
|
||||
|
||||
## WebSocket 事件
|
||||
|
||||
来自捕获会话和 AI 流水线的实时事件。无需 webhook 或轮询。
|
||||
|
||||
使用 [scripts/ws\_listener.py](../../../../../skills/videodb/scripts/ws_listener.py) 连接并将事件转储到 `${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}/videodb_events.jsonl`。
|
||||
|
||||
### 事件通道
|
||||
|
||||
| 通道 | 来源 | 内容 |
|
||||
|---------|--------|---------|
|
||||
| `capture_session` | 会话生命周期 | 状态变更 |
|
||||
| `transcript` | `start_transcript()` | 语音转文字 |
|
||||
| `visual_index` / `scene_index` | `index_visuals()` | 视觉分析 |
|
||||
| `audio_index` | `index_audio()` | 音频分析 |
|
||||
| `alert` | `create_alert()` | 警报通知 |
|
||||
|
||||
### 会话生命周期事件
|
||||
|
||||
| 事件 | 状态 | 关键数据 |
|
||||
|-------|--------|----------|
|
||||
| `capture_session.created` | `created` | — |
|
||||
| `capture_session.starting` | `starting` | — |
|
||||
| `capture_session.active` | `active` | `rtstreams[]` |
|
||||
| `capture_session.stopping` | `stopping` | — |
|
||||
| `capture_session.stopped` | `stopped` | — |
|
||||
| `capture_session.exported` | `exported` | `exported_video_id`, `stream_url`, `player_url` |
|
||||
| `capture_session.failed` | `failed` | `error` |
|
||||
|
||||
### 事件结构
|
||||
|
||||
**转录事件:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "transcript",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"rtstream_name": "mic:default",
|
||||
"data": {
|
||||
"text": "Let's schedule the meeting for Thursday",
|
||||
"is_final": true,
|
||||
"start": 1710000001234,
|
||||
"end": 1710000002345
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**视觉索引事件:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "visual_index",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"rtstream_name": "display:1",
|
||||
"data": {
|
||||
"text": "User is viewing a Slack conversation with 3 unread messages",
|
||||
"start": 1710000012340,
|
||||
"end": 1710000018900
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**音频索引事件:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "audio_index",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"rtstream_name": "mic:default",
|
||||
"data": {
|
||||
"text": "Discussion about scheduling a team meeting",
|
||||
"start": 1710000021500,
|
||||
"end": 1710000029200
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**会话激活事件:**
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "capture_session.active",
|
||||
"capture_session_id": "cap-xxx",
|
||||
"status": "active",
|
||||
"data": {
|
||||
"rtstreams": [
|
||||
{ "rtstream_id": "rts-1", "name": "mic:default", "media_types": ["audio"] },
|
||||
{ "rtstream_id": "rts-2", "name": "system_audio:default", "media_types": ["audio"] },
|
||||
{ "rtstream_id": "rts-3", "name": "display:1", "media_types": ["video"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**会话导出事件:**
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "capture_session.exported",
|
||||
"capture_session_id": "cap-xxx",
|
||||
"status": "exported",
|
||||
"data": {
|
||||
"exported_video_id": "v_xyz789",
|
||||
"stream_url": "https://stream.videodb.io/...",
|
||||
"player_url": "https://console.videodb.io/player?url=..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 有关最新详情,请参阅 [VideoDB 实时上下文文档](https://docs.videodb.io/pages/ingest/capture-sdks/realtime-context.md)。
|
||||
|
||||
***
|
||||
|
||||
## 事件持久化
|
||||
|
||||
使用 `ws_listener.py` 将所有 WebSocket 事件转储到 JSONL 文件以供后续分析。
|
||||
|
||||
### 启动监听器并获取 WebSocket ID
|
||||
|
||||
```bash
|
||||
# Start with --clear to clear old events (recommended for new sessions)
|
||||
python scripts/ws_listener.py --clear &
|
||||
|
||||
# Append to existing events (for reconnects)
|
||||
python scripts/ws_listener.py &
|
||||
```
|
||||
|
||||
或者指定自定义输出目录:
|
||||
|
||||
```bash
|
||||
python scripts/ws_listener.py --clear /path/to/output &
|
||||
# Or via environment variable:
|
||||
VIDEODB_EVENTS_DIR=/path/to/output python scripts/ws_listener.py --clear &
|
||||
```
|
||||
|
||||
脚本在第一行输出 `WS_ID=<connection_id>`,然后无限期监听。
|
||||
|
||||
**获取 ws\_id:**
|
||||
|
||||
```bash
|
||||
cat "${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}/videodb_ws_id"
|
||||
```
|
||||
|
||||
**停止监听器:**
|
||||
|
||||
```bash
|
||||
kill "$(cat "${VIDEODB_EVENTS_DIR:-$HOME/.local/state/videodb}/videodb_ws_pid")"
|
||||
```
|
||||
|
||||
**接受 `ws_connection_id` 的函数:**
|
||||
|
||||
| 函数 | 用途 |
|
||||
|----------|---------|
|
||||
| `conn.create_capture_session()` | 会话生命周期事件 |
|
||||
| RTStream 方法 | 参见 [rtstream-reference.md](rtstream-reference.md) |
|
||||
|
||||
**输出文件**(位于输出目录中,默认为 `${XDG_STATE_HOME:-$HOME/.local/state}/videodb`):
|
||||
|
||||
* `videodb_ws_id` - WebSocket 连接 ID
|
||||
* `videodb_events.jsonl` - 所有事件
|
||||
* `videodb_ws_pid` - 进程 ID,便于终止
|
||||
|
||||
**特性:**
|
||||
|
||||
* `--clear` 标志,用于在启动时清除事件文件(用于新会话)
|
||||
* 连接断开时,使用指数退避自动重连
|
||||
* 在 SIGINT/SIGTERM 时优雅关闭
|
||||
* 连接状态日志记录
|
||||
|
||||
### JSONL 格式
|
||||
|
||||
每行是一个添加了时间戳的 JSON 对象:
|
||||
|
||||
```json
|
||||
{"ts": "2026-03-02T10:15:30.123Z", "unix_ts": 1772446530.123, "channel": "visual_index", "data": {"text": "..."}}
|
||||
{"ts": "2026-03-02T10:15:31.456Z", "unix_ts": 1772446531.456, "event": "capture_session.active", "capture_session_id": "cap-xxx"}
|
||||
```
|
||||
|
||||
### 读取事件
|
||||
|
||||
```python
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
events_path = Path.home() / ".local" / "state" / "videodb" / "videodb_events.jsonl"
|
||||
transcripts = []
|
||||
recent = []
|
||||
visual = []
|
||||
|
||||
cutoff = time.time() - 600
|
||||
with events_path.open(encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
event = json.loads(line)
|
||||
if event.get("channel") == "transcript":
|
||||
transcripts.append(event)
|
||||
if event.get("unix_ts", 0) > cutoff:
|
||||
recent.append(event)
|
||||
if (
|
||||
event.get("channel") == "visual_index"
|
||||
and "code" in event.get("data", {}).get("text", "").lower()
|
||||
):
|
||||
visual.append(event)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## WebSocket 连接
|
||||
|
||||
连接以接收来自转录和索引流水线的实时 AI 结果。
|
||||
|
||||
```python
|
||||
ws_wrapper = conn.connect_websocket()
|
||||
ws = await ws_wrapper.connect()
|
||||
ws_id = ws.connection_id
|
||||
```
|
||||
|
||||
| 属性 / 方法 | 类型 | 描述 |
|
||||
|-------------------|------|-------------|
|
||||
| `ws.connection_id` | `str` | 唯一连接 ID(传递给 AI 流水线方法) |
|
||||
| `ws.receive()` | `AsyncIterator[dict]` | 异步迭代器,产生实时消息 |
|
||||
|
||||
***
|
||||
|
||||
## CaptureSession
|
||||
|
||||
### 连接方法
|
||||
|
||||
| 方法 | 返回值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `conn.create_capture_session(end_user_id, collection_id, ws_connection_id, metadata)` | `CaptureSession` | 创建新的捕获会话 |
|
||||
| `conn.get_capture_session(capture_session_id)` | `CaptureSession` | 检索现有的捕获会话 |
|
||||
| `conn.generate_client_token()` | `str` | 生成客户端身份验证令牌 |
|
||||
|
||||
### 创建捕获会话
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
ws_id = (Path.home() / ".local" / "state" / "videodb" / "videodb_ws_id").read_text().strip()
|
||||
|
||||
session = conn.create_capture_session(
|
||||
end_user_id="user-123", # required
|
||||
collection_id="default",
|
||||
ws_connection_id=ws_id,
|
||||
metadata={"app": "my-app"},
|
||||
)
|
||||
print(f"Session ID: {session.id}")
|
||||
```
|
||||
|
||||
> **注意:** `end_user_id` 是必需的,用于标识发起捕获的用户。用于测试或演示目的时,任何唯一的字符串标识符都有效(例如 `"demo-user"`、`"test-123"`)。
|
||||
|
||||
### CaptureSession 属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `session.id` | `str` | 唯一的捕获会话 ID |
|
||||
|
||||
### CaptureSession 方法
|
||||
|
||||
| 方法 | 返回值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `session.get_rtstream(type)` | `list[RTStream]` | 按类型获取 RTStream:`"mic"`、`"screen"` 或 `"system_audio"` |
|
||||
|
||||
### 生成客户端令牌
|
||||
|
||||
```python
|
||||
token = conn.generate_client_token()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## CaptureClient
|
||||
|
||||
客户端在用户机器上运行,处理权限、通道发现和流传输。
|
||||
|
||||
```python
|
||||
from videodb.capture import CaptureClient
|
||||
|
||||
client = CaptureClient(client_token=token)
|
||||
```
|
||||
|
||||
### CaptureClient 方法
|
||||
|
||||
| 方法 | 返回值 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `await client.request_permission(type)` | `None` | 请求设备权限(`"microphone"`、`"screen_capture"`) |
|
||||
| `await client.list_channels()` | `Channels` | 发现可用的音频/视频通道 |
|
||||
| `await client.start_capture_session(capture_session_id, channels, primary_video_channel_id)` | `None` | 开始流式传输选定的通道 |
|
||||
| `await client.stop_capture()` | `None` | 优雅地停止捕获会话 |
|
||||
| `await client.shutdown()` | `None` | 清理客户端资源 |
|
||||
|
||||
### 请求权限
|
||||
|
||||
```python
|
||||
await client.request_permission("microphone")
|
||||
await client.request_permission("screen_capture")
|
||||
```
|
||||
|
||||
### 启动会话
|
||||
|
||||
```python
|
||||
selected_channels = [c for c in [mic, display, system_audio] if c]
|
||||
await client.start_capture_session(
|
||||
capture_session_id=session.id,
|
||||
channels=selected_channels,
|
||||
primary_video_channel_id=display.id if display else None,
|
||||
)
|
||||
```
|
||||
|
||||
### 停止会话
|
||||
|
||||
```python
|
||||
await client.stop_capture()
|
||||
await client.shutdown()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 通道
|
||||
|
||||
由 `client.list_channels()` 返回。按类型分组可用设备。
|
||||
|
||||
```python
|
||||
channels = await client.list_channels()
|
||||
for ch in channels.all():
|
||||
print(f" {ch.id} ({ch.type}): {ch.name}")
|
||||
|
||||
mic = channels.mics.default
|
||||
display = channels.displays.default
|
||||
system_audio = channels.system_audio.default
|
||||
```
|
||||
|
||||
### 通道组
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `channels.mics` | `ChannelGroup` | 可用的麦克风 |
|
||||
| `channels.displays` | `ChannelGroup` | 可用的屏幕显示器 |
|
||||
| `channels.system_audio` | `ChannelGroup` | 可用的系统音频源 |
|
||||
|
||||
### ChannelGroup 方法与属性
|
||||
|
||||
| 成员 | 类型 | 描述 |
|
||||
|--------|------|-------------|
|
||||
| `group.default` | `Channel` | 组中的默认通道(或 `None`) |
|
||||
| `group.all()` | `list[Channel]` | 组中的所有通道 |
|
||||
|
||||
### 通道属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `ch.id` | `str` | 唯一的通道 ID |
|
||||
| `ch.type` | `str` | 通道类型(`"mic"`、`"display"`、`"system_audio"`) |
|
||||
| `ch.name` | `str` | 人类可读的通道名称 |
|
||||
| `ch.store` | `bool` | 是否持久化录制(设置为 `True` 以保存) |
|
||||
|
||||
没有 `store = True`,流会实时处理但不保存。
|
||||
|
||||
***
|
||||
|
||||
## RTStream 和 AI 流水线
|
||||
|
||||
会话激活后,使用 `session.get_rtstream()` 检索 RTStream 对象。
|
||||
|
||||
关于 RTStream 方法(索引、转录、警报、批处理配置),请参阅 [rtstream-reference.md](rtstream-reference.md)。
|
||||
|
||||
***
|
||||
|
||||
## 会话生命周期
|
||||
|
||||
```
|
||||
create_capture_session()
|
||||
│
|
||||
v
|
||||
┌───────────────┐
|
||||
│ created │
|
||||
└───────┬───────┘
|
||||
│ client.start_capture_session()
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.starting
|
||||
│ starting │ ──> Capture channels connect
|
||||
└───────┬───────┘
|
||||
│
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.active
|
||||
│ active │ ──> Start AI pipelines
|
||||
└───────┬──────────────┐
|
||||
│ │
|
||||
│ v
|
||||
│ ┌───────────────┐ WebSocket: capture_session.failed
|
||||
│ │ failed │ ──> Inspect error payload and retry setup
|
||||
│ └───────────────┘
|
||||
│ unrecoverable capture error
|
||||
│
|
||||
│ client.stop_capture()
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.stopping
|
||||
│ stopping │ ──> Finalize streams
|
||||
└───────┬───────┘
|
||||
│
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.stopped
|
||||
│ stopped │ ──> All streams finalized
|
||||
└───────┬───────┘
|
||||
│ (if store=True)
|
||||
v
|
||||
┌───────────────┐ WebSocket: capture_session.exported
|
||||
│ exported │ ──> Access video_id, stream_url, player_url
|
||||
└───────────────┘
|
||||
```
|
||||
104
docs/zh-CN/skills/videodb/reference/capture.md
Normal file
104
docs/zh-CN/skills/videodb/reference/capture.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Capture 指南
|
||||
|
||||
## 概述
|
||||
|
||||
VideoDB Capture 支持实时屏幕和音频录制,并具备 AI 处理能力。桌面捕获目前仅支持 **macOS**。
|
||||
|
||||
关于代码层面的详细信息(SDK 方法、事件结构、AI 管道),请参阅 [capture-reference.md](capture-reference.md)。
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. **启动 WebSocket 监听器**:`python scripts/ws_listener.py --clear &`
|
||||
2. **运行捕获代码**(见下方完整捕获工作流)
|
||||
3. **事件写入到**:`/tmp/videodb_events.jsonl`
|
||||
|
||||
***
|
||||
|
||||
## 完整捕获工作流
|
||||
|
||||
无需 webhook 或轮询。WebSocket 会传递所有事件,包括会话生命周期事件。
|
||||
|
||||
> **关键提示:** `CaptureClient` 必须在整个捕获期间持续运行。它运行本地录制器二进制文件,将屏幕/音频数据流式传输到 VideoDB。如果创建 `CaptureClient` 的 Python 进程退出,录制器二进制文件将被终止,捕获会静默停止。请始终将捕获代码作为**长期运行的后台进程**运行(例如 `nohup python capture_script.py &`),并使用信号处理(`asyncio.Event` + `SIGINT`/`SIGTERM`)来保持其存活,直到您明确停止它。
|
||||
|
||||
1. 在后台**启动 WebSocket 监听器**,使用 `--clear` 标志来清除旧事件。等待其创建 WebSocket ID 文件。
|
||||
|
||||
2. **读取 WebSocket ID**。此 ID 是捕获会话和 AI 管道所必需的。
|
||||
|
||||
3. **创建捕获会话**,并为桌面客户端生成客户端令牌。
|
||||
|
||||
4. 使用令牌**初始化 CaptureClient**。请求麦克风和屏幕捕获权限。
|
||||
|
||||
5. **列出并选择通道**(麦克风、显示器、系统音频)。在您希望持久化为视频的通道上设置 `store = True`。
|
||||
|
||||
6. 使用选定的通道**启动会话**。
|
||||
|
||||
7. 通过读取事件直到看到 `capture_session.active` 来**等待会话激活**。此事件包含 `rtstreams` 数组。将会话信息(会话 ID、RTStream ID)保存到文件(例如 `/tmp/videodb_capture_info.json`),以便其他脚本可以读取。
|
||||
|
||||
8. **保持进程存活**。使用 `asyncio.Event` 配合 `SIGINT`/`SIGTERM` 的信号处理器来阻塞进程,直到显式停止。写入一个 PID 文件(例如 `/tmp/videodb_capture_pid`),以便稍后可以使用 `kill $(cat /tmp/videodb_capture_pid)` 停止该进程。PID 文件应在每次运行时被覆盖,以便重新运行时始终具有正确的 PID。
|
||||
|
||||
9. **启动 AI 管道**(在单独的命令/脚本中)对每个 RTStream 进行音频索引和视觉索引。从保存的会话信息文件中读取 RTStream ID。
|
||||
|
||||
10. **编写自定义事件处理逻辑**(在单独的命令/脚本中),根据您的用例读取实时事件。示例:
|
||||
* 当 `visual_index` 提到 "Slack" 时记录 Slack 活动
|
||||
* 当 `audio_index` 事件到达时总结讨论
|
||||
* 当 `transcript` 中出现特定关键词时触发警报
|
||||
* 从屏幕描述中跟踪应用程序使用情况
|
||||
|
||||
11. **停止捕获** - 完成后,向捕获进程发送 SIGTERM。它应在信号处理器中调用 `client.stop_capture()` 和 `client.shutdown()`。
|
||||
|
||||
12. **等待导出** - 通过读取事件直到看到 `capture_session.exported`。此事件包含 `exported_video_id`、`stream_url` 和 `player_url`。这可能在停止捕获后需要几秒钟。
|
||||
|
||||
13. **停止 WebSocket 监听器** - 收到导出事件后,使用 `kill $(cat /tmp/videodb_ws_pid)` 来干净地终止它。
|
||||
|
||||
***
|
||||
|
||||
## 关机顺序
|
||||
|
||||
正确的关机顺序对于确保捕获所有事件非常重要:
|
||||
|
||||
1. **停止捕获会话** — `client.stop_capture()` 然后 `client.shutdown()`
|
||||
2. **等待导出事件** — 轮询 `/tmp/videodb_events.jsonl` 以查找 `capture_session.exported`
|
||||
3. **停止 WebSocket 监听器** — `kill $(cat /tmp/videodb_ws_pid)`
|
||||
|
||||
在收到导出事件之前,请**不要**杀死 WebSocket 监听器,否则您将错过最终的视频 URL。
|
||||
|
||||
***
|
||||
|
||||
## 脚本
|
||||
|
||||
| 脚本 | 描述 |
|
||||
|--------|-------------|
|
||||
| `scripts/ws_listener.py` | WebSocket 事件监听器(转储为 JSONL) |
|
||||
|
||||
### ws\_listener.py 用法
|
||||
|
||||
```bash
|
||||
# Start listener in background (append to existing events)
|
||||
python scripts/ws_listener.py &
|
||||
|
||||
# Start listener with clear (new session, clears old events)
|
||||
python scripts/ws_listener.py --clear &
|
||||
|
||||
# Custom output directory
|
||||
python scripts/ws_listener.py --clear /path/to/events &
|
||||
|
||||
# Stop the listener
|
||||
kill $(cat /tmp/videodb_ws_pid)
|
||||
```
|
||||
|
||||
**选项:**
|
||||
|
||||
* `--clear`:在启动前清除事件文件。启动新捕获会话时使用。
|
||||
|
||||
**输出文件:**
|
||||
|
||||
* `videodb_events.jsonl` - 所有 WebSocket 事件
|
||||
* `videodb_ws_id` - WebSocket 连接 ID(用于 `ws_connection_id` 参数)
|
||||
* `videodb_ws_pid` - 进程 ID(用于停止监听器)
|
||||
|
||||
**功能:**
|
||||
|
||||
* 连接断开时自动重连,并采用指数退避
|
||||
* 收到 SIGINT/SIGTERM 时优雅关机
|
||||
* PID 文件,便于进程管理
|
||||
* 连接状态日志记录
|
||||
443
docs/zh-CN/skills/videodb/reference/editor.md
Normal file
443
docs/zh-CN/skills/videodb/reference/editor.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# 时间线编辑指南
|
||||
|
||||
VideoDB 提供了一个非破坏性的时间线编辑器,用于从多个素材合成视频、添加文本和图像叠加、混合音轨以及修剪片段——所有这些都在服务器端完成,无需重新编码或本地工具。可用于修剪、合并片段、在视频上叠加音频/音乐、添加字幕以及叠加文本或图像。
|
||||
|
||||
## 前提条件
|
||||
|
||||
视频、音频和图像**必须上传**到集合中,才能用作时间线素材。对于字幕叠加,视频还必须**为口语单词建立索引**。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 时间线
|
||||
|
||||
`Timeline` 是一个虚拟合成层。素材可以**内联**(在主轨道上顺序放置)或作为**叠加层**(在特定时间戳分层放置)放置在时间线上。不会修改原始媒体;最终流是按需编译的。
|
||||
|
||||
```python
|
||||
from videodb.timeline import Timeline
|
||||
|
||||
timeline = Timeline(conn)
|
||||
```
|
||||
|
||||
### 素材
|
||||
|
||||
时间线上的每个元素都是一个**素材**。VideoDB 提供五种素材类型:
|
||||
|
||||
| 素材 | 导入 | 主要用途 |
|
||||
|-------|--------|-------------|
|
||||
| `VideoAsset` | `from videodb.asset import VideoAsset` | 视频片段(修剪、排序) |
|
||||
| `AudioAsset` | `from videodb.asset import AudioAsset` | 音乐、音效、旁白 |
|
||||
| `ImageAsset` | `from videodb.asset import ImageAsset` | 徽标、缩略图、叠加层 |
|
||||
| `TextAsset` | `from videodb.asset import TextAsset, TextStyle` | 标题、字幕、下三分之一字幕 |
|
||||
| `CaptionAsset` | `from videodb.editor import CaptionAsset` | 自动渲染的字幕(编辑器 API) |
|
||||
|
||||
## 构建时间线
|
||||
|
||||
### 内联添加视频片段
|
||||
|
||||
内联素材在主视频轨道上一个接一个播放。`add_inline` 方法只接受 `VideoAsset`:
|
||||
|
||||
```python
|
||||
from videodb.asset import VideoAsset
|
||||
|
||||
video_a = coll.get_video(video_id_a)
|
||||
video_b = coll.get_video(video_id_b)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
timeline.add_inline(VideoAsset(asset_id=video_a.id))
|
||||
timeline.add_inline(VideoAsset(asset_id=video_b.id))
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
```
|
||||
|
||||
### 修剪 / 子片段
|
||||
|
||||
在 `VideoAsset` 上使用 `start` 和 `end` 来提取一部分:
|
||||
|
||||
```python
|
||||
# Take only seconds 10–30 from the source video
|
||||
clip = VideoAsset(asset_id=video.id, start=10, end=30)
|
||||
timeline.add_inline(clip)
|
||||
```
|
||||
|
||||
### VideoAsset 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `asset_id` | `str` | 必填 | 视频媒体 ID |
|
||||
| `start` | `float` | `0` | 修剪开始时间(秒) |
|
||||
| `end` | `float\|None` | `None` | 修剪结束时间(`None` = 完整视频) |
|
||||
|
||||
> **警告:** SDK 不会验证负时间戳。传递 `start=-5` 会被静默接受,但会产生损坏或意外的输出。在创建 `VideoAsset` 之前,请始终确保 `start >= 0`、`start < end` 和 `end <= video.length`。
|
||||
|
||||
## 文本叠加
|
||||
|
||||
在时间线的任意点添加标题、下三分之一字幕或说明文字:
|
||||
|
||||
```python
|
||||
from videodb.asset import TextAsset, TextStyle
|
||||
|
||||
title = TextAsset(
|
||||
text="Welcome to the Demo",
|
||||
duration=5,
|
||||
style=TextStyle(
|
||||
fontsize=36,
|
||||
fontcolor="white",
|
||||
boxcolor="black",
|
||||
alpha=0.8,
|
||||
font="Sans",
|
||||
),
|
||||
)
|
||||
|
||||
# Overlay the title at the very start (t=0)
|
||||
timeline.add_overlay(0, title)
|
||||
```
|
||||
|
||||
### TextStyle 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `fontsize` | `int` | `24` | 字体大小(像素) |
|
||||
| `fontcolor` | `str` | `"black"` | CSS 颜色名称或十六进制值 |
|
||||
| `fontcolor_expr` | `str` | `""` | 动态字体颜色表达式 |
|
||||
| `alpha` | `float` | `1.0` | 文本不透明度(0.0–1.0) |
|
||||
| `font` | `str` | `"Sans"` | 字体系列 |
|
||||
| `box` | `bool` | `True` | 启用背景框 |
|
||||
| `boxcolor` | `str` | `"white"` | 背景框颜色 |
|
||||
| `boxborderw` | `str` | `"10"` | 框边框宽度 |
|
||||
| `boxw` | `int` | `0` | 框宽度覆盖 |
|
||||
| `boxh` | `int` | `0` | 框高度覆盖 |
|
||||
| `line_spacing` | `int` | `0` | 行间距 |
|
||||
| `text_align` | `str` | `"T"` | 框内文本对齐方式 |
|
||||
| `y_align` | `str` | `"text"` | 垂直对齐参考 |
|
||||
| `borderw` | `int` | `0` | 文本边框宽度 |
|
||||
| `bordercolor` | `str` | `"black"` | 文本边框颜色 |
|
||||
| `expansion` | `str` | `"normal"` | 文本扩展模式 |
|
||||
| `basetime` | `int` | `0` | 基于时间的表达式的基础时间 |
|
||||
| `fix_bounds` | `bool` | `False` | 固定文本边界 |
|
||||
| `text_shaping` | `bool` | `True` | 启用文本整形 |
|
||||
| `shadowcolor` | `str` | `"black"` | 阴影颜色 |
|
||||
| `shadowx` | `int` | `0` | 阴影 X 偏移 |
|
||||
| `shadowy` | `int` | `0` | 阴影 Y 偏移 |
|
||||
| `tabsize` | `int` | `4` | 制表符大小(空格数) |
|
||||
| `x` | `str` | `"(main_w-text_w)/2"` | 水平位置表达式 |
|
||||
| `y` | `str` | `"(main_h-text_h)/2"` | 垂直位置表达式 |
|
||||
|
||||
## 音频叠加
|
||||
|
||||
在主视频轨道上叠加背景音乐、音效或旁白:
|
||||
|
||||
```python
|
||||
from videodb.asset import AudioAsset
|
||||
|
||||
music = coll.get_audio(music_id)
|
||||
|
||||
audio_layer = AudioAsset(
|
||||
asset_id=music.id,
|
||||
disable_other_tracks=False,
|
||||
fade_in_duration=2,
|
||||
fade_out_duration=2,
|
||||
)
|
||||
|
||||
# Start the music at t=0, overlaid on the video track
|
||||
timeline.add_overlay(0, audio_layer)
|
||||
```
|
||||
|
||||
### AudioAsset 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `asset_id` | `str` | 必填 | 音频媒体 ID |
|
||||
| `start` | `float` | `0` | 修剪开始时间(秒) |
|
||||
| `end` | `float\|None` | `None` | 修剪结束时间(`None` = 完整音频) |
|
||||
| `disable_other_tracks` | `bool` | `True` | 为 True 时,静音其他音轨 |
|
||||
| `fade_in_duration` | `float` | `0` | 淡入秒数(最大 5) |
|
||||
| `fade_out_duration` | `float` | `0` | 淡出秒数(最大 5) |
|
||||
|
||||
## 图像叠加
|
||||
|
||||
添加徽标、水印或生成的图像作为叠加层:
|
||||
|
||||
```python
|
||||
from videodb.asset import ImageAsset
|
||||
|
||||
logo = coll.get_image(logo_id)
|
||||
|
||||
logo_overlay = ImageAsset(
|
||||
asset_id=logo.id,
|
||||
duration=10,
|
||||
width=120,
|
||||
height=60,
|
||||
x=20,
|
||||
y=20,
|
||||
)
|
||||
|
||||
timeline.add_overlay(0, logo_overlay)
|
||||
```
|
||||
|
||||
### ImageAsset 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `asset_id` | `str` | 必填 | 图像媒体 ID |
|
||||
| `width` | `int\|str` | `100` | 显示宽度 |
|
||||
| `height` | `int\|str` | `100` | 显示高度 |
|
||||
| `x` | `int` | `80` | 水平位置(距离左侧的像素) |
|
||||
| `y` | `int` | `20` | 垂直位置(距离顶部的像素) |
|
||||
| `duration` | `float\|None` | `None` | 显示时长(秒) |
|
||||
|
||||
## 字幕叠加
|
||||
|
||||
有两种方式可以为视频添加字幕。
|
||||
|
||||
### 方法 1:字幕工作流(最简单)
|
||||
|
||||
使用 `video.add_subtitle()` 将字幕直接烧录到视频流中。这在内部使用 `videodb.timeline.Timeline`:
|
||||
|
||||
```python
|
||||
from videodb import SubtitleStyle
|
||||
|
||||
# Video must have spoken words indexed first (force=True skips if already done)
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# Add subtitles with default styling
|
||||
stream_url = video.add_subtitle()
|
||||
|
||||
# Or customise the subtitle style
|
||||
stream_url = video.add_subtitle(style=SubtitleStyle(
|
||||
font_name="Arial",
|
||||
font_size=22,
|
||||
primary_colour="&H00FFFFFF",
|
||||
bold=True,
|
||||
))
|
||||
```
|
||||
|
||||
### 方法 2:编辑器 API(高级)
|
||||
|
||||
编辑器 API(`videodb.editor`)提供了一个基于轨道的合成系统,包含 `CaptionAsset`、`Clip`、`Track` 及其自身的 `Timeline`。这是一个与上述使用的 `videodb.timeline.Timeline` 独立的 API。
|
||||
|
||||
```python
|
||||
from videodb.editor import (
|
||||
CaptionAsset,
|
||||
Clip,
|
||||
Track,
|
||||
Timeline as EditorTimeline,
|
||||
FontStyling,
|
||||
BorderAndShadow,
|
||||
Positioning,
|
||||
CaptionAnimation,
|
||||
)
|
||||
|
||||
# Video must have spoken words indexed first (force=True skips if already done)
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# Create a caption asset
|
||||
caption = CaptionAsset(
|
||||
src="auto",
|
||||
font=FontStyling(name="Clear Sans", size=30),
|
||||
primary_color="&H00FFFFFF",
|
||||
back_color="&H00000000",
|
||||
border=BorderAndShadow(outline=1),
|
||||
position=Positioning(margin_v=30),
|
||||
animation=CaptionAnimation.box_highlight,
|
||||
)
|
||||
|
||||
# Build an editor timeline with tracks and clips
|
||||
editor_tl = EditorTimeline(conn)
|
||||
track = Track()
|
||||
track.add_clip(start=0, clip=Clip(asset=caption, duration=video.length))
|
||||
editor_tl.add_track(track)
|
||||
stream_url = editor_tl.generate_stream()
|
||||
```
|
||||
|
||||
### CaptionAsset 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `src` | `str` | `"auto"` | 字幕来源(`"auto"` 或 base64 ASS 字符串) |
|
||||
| `font` | `FontStyling\|None` | `FontStyling()` | 字体样式(名称、大小、粗体、斜体等) |
|
||||
| `primary_color` | `str` | `"&H00FFFFFF"` | 主文本颜色(ASS 格式) |
|
||||
| `secondary_color` | `str` | `"&H000000FF"` | 次文本颜色(ASS 格式) |
|
||||
| `back_color` | `str` | `"&H00000000"` | 背景颜色(ASS 格式) |
|
||||
| `border` | `BorderAndShadow\|None` | `BorderAndShadow()` | 边框和阴影样式 |
|
||||
| `position` | `Positioning\|None` | `Positioning()` | 字幕对齐方式和边距 |
|
||||
| `animation` | `CaptionAnimation\|None` | `None` | 动画效果(例如,`box_highlight`、`reveal`、`karaoke`) |
|
||||
|
||||
## 编译与流式传输
|
||||
|
||||
组装好时间线后,将其编译成可流式传输的 URL。流是即时生成的——无需渲染等待时间。
|
||||
|
||||
```python
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Stream: {stream_url}")
|
||||
```
|
||||
|
||||
有关更多流式传输选项(分段流、搜索到流、音频播放),请参阅 [streaming.md](streaming.md)。
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
### 带标题卡的高光集锦
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# 1. Search for key moments
|
||||
video.index_spoken_words(force=True)
|
||||
try:
|
||||
results = video.search("product announcement", search_type=SearchType.semantic)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
|
||||
# 2. Build timeline
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Title card
|
||||
title = TextAsset(
|
||||
text="Product Launch Highlights",
|
||||
duration=4,
|
||||
style=TextStyle(fontsize=48, fontcolor="white", boxcolor="#1a1a2e", alpha=0.95),
|
||||
)
|
||||
timeline.add_overlay(0, title)
|
||||
|
||||
# Append each matching clip
|
||||
for shot in shots:
|
||||
asset = VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
timeline.add_inline(asset)
|
||||
|
||||
# 3. Generate stream
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Highlight reel: {stream_url}")
|
||||
```
|
||||
|
||||
### 带背景音乐的徽标叠加
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset, ImageAsset
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
main_video = coll.get_video(main_video_id)
|
||||
music = coll.get_audio(music_id)
|
||||
logo = coll.get_image(logo_id)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Main video track
|
||||
timeline.add_inline(VideoAsset(asset_id=main_video.id))
|
||||
|
||||
# Background music — disable_other_tracks=False to mix with video audio
|
||||
timeline.add_overlay(
|
||||
0,
|
||||
AudioAsset(asset_id=music.id, disable_other_tracks=False, fade_in_duration=3),
|
||||
)
|
||||
|
||||
# Logo in top-right corner for first 10 seconds
|
||||
timeline.add_overlay(
|
||||
0,
|
||||
ImageAsset(asset_id=logo.id, duration=10, x=1140, y=20, width=120, height=60),
|
||||
)
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Final video: {stream_url}")
|
||||
```
|
||||
|
||||
### 来自多个视频的多片段蒙太奇
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
clips = [
|
||||
{"video_id": "vid_001", "start": 5, "end": 15, "label": "Scene 1"},
|
||||
{"video_id": "vid_002", "start": 0, "end": 20, "label": "Scene 2"},
|
||||
{"video_id": "vid_003", "start": 30, "end": 45, "label": "Scene 3"},
|
||||
]
|
||||
|
||||
timeline = Timeline(conn)
|
||||
timeline_offset = 0.0
|
||||
|
||||
for clip in clips:
|
||||
# Add a label as an overlay on each clip
|
||||
label = TextAsset(
|
||||
text=clip["label"],
|
||||
duration=2,
|
||||
style=TextStyle(fontsize=32, fontcolor="white", boxcolor="#333333"),
|
||||
)
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=clip["video_id"], start=clip["start"], end=clip["end"])
|
||||
)
|
||||
timeline.add_overlay(timeline_offset, label)
|
||||
timeline_offset += clip["end"] - clip["start"]
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Montage: {stream_url}")
|
||||
```
|
||||
|
||||
## 两个时间线 API
|
||||
|
||||
VideoDB 有两个独立的时间线系统。它们**不可互换**:
|
||||
|
||||
| | `videodb.timeline.Timeline` | `videodb.editor.Timeline`(编辑器 API) |
|
||||
|---|---|---|
|
||||
| **导入** | `from videodb.timeline import Timeline` | `from videodb.editor import Timeline as EditorTimeline` |
|
||||
| **素材** | `VideoAsset`、`AudioAsset`、`ImageAsset`、`TextAsset` | `CaptionAsset`、`Clip`、`Track` |
|
||||
| **方法** | `add_inline()`、`add_overlay()` | `add_track()` 配合 `Track` / `Clip` |
|
||||
| **最适合** | 视频合成、叠加、多片段编辑 | 带动画的字幕/字幕样式设计 |
|
||||
|
||||
不要将一个 API 的素材混入另一个 API。`CaptionAsset` 仅适用于编辑器 API。`VideoAsset` / `AudioAsset` / `ImageAsset` / `TextAsset` 仅适用于 `videodb.timeline.Timeline`。
|
||||
|
||||
## 限制与约束
|
||||
|
||||
时间线编辑器专为**非破坏性线性合成**而设计。**不支持**以下操作:
|
||||
|
||||
### 不支持的操作
|
||||
|
||||
| 限制 | 详情 |
|
||||
|---|---|
|
||||
| **无过渡或效果** | 片段之间没有交叉淡入淡出、划像、溶解或过渡。所有剪辑都是硬切。 |
|
||||
| **无视频叠加视频(画中画)** | `add_inline()` 只接受 `VideoAsset`。无法将一个视频流叠加在另一个之上。图像叠加可以近似静态画中画,但不能是实时视频。 |
|
||||
| **无速度或播放控制** | 没有慢动作、快进、倒放或时间重映射。`VideoAsset` 没有 `speed` 参数。 |
|
||||
| **无裁剪、缩放或平移** | 无法裁剪视频帧的区域、应用缩放效果或在帧上平移。`video.reframe()` 仅用于宽高比转换。 |
|
||||
| **无视频滤镜或色彩分级** | 没有亮度、对比度、饱和度、色调或色彩校正调整。 |
|
||||
| **无动画文本** | `TextAsset` 在其整个持续时间内是静态的。没有淡入/淡出、移动或动画。对于动画字幕,请使用带有编辑器 API 的 `CaptionAsset`。 |
|
||||
| **无混合文本样式** | 单个 `TextAsset` 只有一个 `TextStyle`。无法在单个文本块内混合粗体、斜体或颜色。 |
|
||||
| **无空白或纯色片段** | 无法创建纯色帧、黑屏或独立的标题卡。文本和图像叠加需要在内联轨道上有 `VideoAsset` 作为底层。 |
|
||||
| **无音频音量控制** | `AudioAsset` 没有 `volume` 参数。音频要么是全音量,要么通过 `disable_other_tracks` 静音。无法以降低的音量混合。 |
|
||||
| **无关键帧动画** | 无法随时间改变叠加属性(例如,将图像从位置 A 移动到 B)。 |
|
||||
|
||||
### 约束
|
||||
|
||||
| 约束 | 详情 |
|
||||
|---|---|
|
||||
| **音频淡入淡出最长 5 秒** | `fade_in_duration` 和 `fade_out_duration` 各自上限为 5 秒。 |
|
||||
| **叠加层定位为绝对定位** | 叠加层使用时间轴起始点的绝对时间戳。重新排列内联片段不会移动其叠加层。 |
|
||||
| **内联轨道仅支持视频** | `add_inline()` 仅接受 `VideoAsset`。音频、图像和文本必须使用 `add_overlay()`。 |
|
||||
| **叠加层与片段无绑定关系** | 叠加层被放置在固定的时间轴时间戳上。无法将叠加层附加到特定的内联片段以使其随之移动。 |
|
||||
|
||||
## 提示
|
||||
|
||||
* **非破坏性**:时间轴从不修改源媒体。您可以使用相同的素材创建多个时间轴。
|
||||
* **叠加层堆叠**:多个叠加层可以在同一时间戳开始。音频叠加层会混合在一起;图像/文本叠加层按添加顺序分层叠加。
|
||||
* **内联轨道仅支持 VideoAsset**:`add_inline()` 仅接受 `VideoAsset`。对于 `AudioAsset`、`ImageAsset` 和 `TextAsset`,请使用 `add_overlay()`。
|
||||
* **裁剪精度**:`start`/`end` 在 `VideoAsset` 和 `AudioAsset` 上以秒为单位。
|
||||
* **静音视频音频**:在 `AudioAsset` 上设置 `disable_other_tracks=True`,以便在叠加音乐或旁白时静音原始视频音频。
|
||||
* **淡入淡出限制**:`fade_in_duration` 和 `fade_out_duration` 在 `AudioAsset` 上最长不超过 5 秒。
|
||||
* **生成媒体**:使用 `coll.generate_music()`、`coll.generate_sound_effect()`、`coll.generate_voice()` 和 `coll.generate_image()` 创建可立即用作时间轴素材的媒体。
|
||||
331
docs/zh-CN/skills/videodb/reference/generative.md
Normal file
331
docs/zh-CN/skills/videodb/reference/generative.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# 生成式媒体指南
|
||||
|
||||
VideoDB 提供 AI 驱动的图像、视频、音乐、音效、语音和文本内容生成。所有生成方法均在 **Collection** 对象上。
|
||||
|
||||
## 先决条件
|
||||
|
||||
在调用任何生成方法之前,您需要一个连接和一个集合引用:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
```
|
||||
|
||||
## 图像生成
|
||||
|
||||
根据文本提示生成图像:
|
||||
|
||||
```python
|
||||
image = coll.generate_image(
|
||||
prompt="a futuristic cityscape at sunset with flying cars",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
|
||||
# Access the generated image
|
||||
print(image.id)
|
||||
print(image.generate_url()) # returns a signed download URL
|
||||
```
|
||||
|
||||
### generate\_image 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必需 | 要生成的图像的文本描述 |
|
||||
| `aspect_ratio` | `str` | `"1:1"` | 宽高比:`"1:1"`, `"9:16"`, `"16:9"`, `"4:3"`, 或 `"3:4"` |
|
||||
| `callback_url` | `str\|None` | `None` | 接收异步回调的 URL |
|
||||
|
||||
返回一个 `Image` 对象,包含 `.id`、`.name` 和 `.collection_id`。`.url` 属性对于生成的图像可能为 `None` —— 始终使用 `image.generate_url()` 来获取可靠的签名下载 URL。
|
||||
|
||||
> **注意:** 与 `Video` 对象(使用 `.generate_stream()`)不同,`Image` 对象使用 `.generate_url()` 来检索图像 URL。`.url` 属性仅针对某些图像类型(例如缩略图)填充。
|
||||
|
||||
## 视频生成
|
||||
|
||||
根据文本提示生成短视频片段:
|
||||
|
||||
```python
|
||||
video = coll.generate_video(
|
||||
prompt="a timelapse of a flower blooming in a garden",
|
||||
duration=5,
|
||||
)
|
||||
|
||||
stream_url = video.generate_stream()
|
||||
video.play()
|
||||
```
|
||||
|
||||
### generate\_video 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必需 | 要生成的视频的文本描述 |
|
||||
| `duration` | `int` | `5` | 持续时间(秒)(必须是整数值,5-8) |
|
||||
| `callback_url` | `str\|None` | `None` | 接收异步回调的 URL |
|
||||
|
||||
返回一个 `Video` 对象。生成的视频会自动添加到集合中,并且可以像任何上传的视频一样在时间线、搜索和编译中使用。
|
||||
|
||||
## 音频生成
|
||||
|
||||
VideoDB 为不同的音频类型提供了三种独立的方法。
|
||||
|
||||
### 音乐
|
||||
|
||||
根据文本描述生成背景音乐:
|
||||
|
||||
```python
|
||||
music = coll.generate_music(
|
||||
prompt="upbeat electronic music with a driving beat, suitable for a tech demo",
|
||||
duration=30,
|
||||
)
|
||||
|
||||
print(music.id)
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必需 | 音乐的文本描述 |
|
||||
| `duration` | `int` | `5` | 持续时间(秒) |
|
||||
| `callback_url` | `str\|None` | `None` | 接收异步回调的 URL |
|
||||
|
||||
### 音效
|
||||
|
||||
生成特定的音效:
|
||||
|
||||
```python
|
||||
sfx = coll.generate_sound_effect(
|
||||
prompt="thunderstorm with heavy rain and distant thunder",
|
||||
duration=10,
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必需 | 音效的文本描述 |
|
||||
| `duration` | `int` | `2` | 持续时间(秒) |
|
||||
| `config` | `dict` | `{}` | 附加配置 |
|
||||
| `callback_url` | `str\|None` | `None` | 接收异步回调的 URL |
|
||||
|
||||
### 语音(文本转语音)
|
||||
|
||||
从文本生成语音:
|
||||
|
||||
```python
|
||||
voice = coll.generate_voice(
|
||||
text="Welcome to our product demo. Today we'll walk through the key features.",
|
||||
voice_name="Default",
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `text` | `str` | 必需 | 要转换为语音的文本 |
|
||||
| `voice_name` | `str` | `"Default"` | 要使用的声音 |
|
||||
| `config` | `dict` | `{}` | 附加配置 |
|
||||
| `callback_url` | `str\|None` | `None` | 接收异步回调的 URL |
|
||||
|
||||
所有三种音频方法都返回一个 `Audio` 对象,包含 `.id`、`.name`、`.length` 和 `.collection_id`。
|
||||
|
||||
## 文本生成(LLM 集成)
|
||||
|
||||
使用 `coll.generate_text()` 来运行 LLM 分析。这是一个 **集合级** 方法 —— 直接在提示字符串中传递任何上下文(转录、描述)。
|
||||
|
||||
```python
|
||||
# Get transcript from a video first
|
||||
transcript_text = video.get_transcript_text()
|
||||
|
||||
# Generate analysis using collection LLM
|
||||
result = coll.generate_text(
|
||||
prompt=f"Summarize the key points discussed in this video:\n{transcript_text}",
|
||||
model_name="pro",
|
||||
)
|
||||
|
||||
print(result["output"])
|
||||
```
|
||||
|
||||
### generate\_text 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `prompt` | `str` | 必需 | 包含 LLM 上下文的提示 |
|
||||
| `model_name` | `str` | `"basic"` | 模型层级:`"basic"`、`"pro"` 或 `"ultra"` |
|
||||
| `response_type` | `str` | `"text"` | 响应格式:`"text"` 或 `"json"` |
|
||||
|
||||
返回一个 `dict`,带有一个 `output` 键。当 `response_type="text"` 时,`output` 是一个 `str`。当 `response_type="json"` 时,`output` 是一个 `dict`。
|
||||
|
||||
```python
|
||||
result = coll.generate_text(prompt="Summarize this", model_name="pro")
|
||||
print(result["output"]) # access the actual text/dict
|
||||
```
|
||||
|
||||
### 使用 LLM 分析场景
|
||||
|
||||
将场景提取与文本生成相结合:
|
||||
|
||||
```python
|
||||
from videodb import SceneExtractionType
|
||||
|
||||
# First index scenes
|
||||
scenes = video.index_scenes(
|
||||
extraction_type=SceneExtractionType.time_based,
|
||||
extraction_config={"time": 10},
|
||||
prompt="Describe the visual content in this scene.",
|
||||
)
|
||||
|
||||
# Get transcript for spoken context
|
||||
transcript_text = video.get_transcript_text()
|
||||
scene_descriptions = []
|
||||
for scene in scenes:
|
||||
if isinstance(scene, dict):
|
||||
description = scene.get("description") or scene.get("summary")
|
||||
else:
|
||||
description = getattr(scene, "description", None) or getattr(scene, "summary", None)
|
||||
scene_descriptions.append(description or str(scene))
|
||||
|
||||
scenes_text = "\n".join(scene_descriptions)
|
||||
|
||||
# Analyze with collection LLM
|
||||
result = coll.generate_text(
|
||||
prompt=(
|
||||
f"Given this video transcript:\n{transcript_text}\n\n"
|
||||
f"And these visual scene descriptions:\n{scenes_text}\n\n"
|
||||
"Based on the spoken and visual content, describe the main topics covered."
|
||||
),
|
||||
model_name="pro",
|
||||
)
|
||||
print(result["output"])
|
||||
```
|
||||
|
||||
## 配音和翻译
|
||||
|
||||
### 为视频配音
|
||||
|
||||
使用集合方法将视频配音为另一种语言:
|
||||
|
||||
```python
|
||||
dubbed_video = coll.dub_video(
|
||||
video_id=video.id,
|
||||
language_code="es", # Spanish
|
||||
)
|
||||
|
||||
dubbed_video.play()
|
||||
```
|
||||
|
||||
### dub\_video 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `video_id` | `str` | 必需 | 要配音的视频 ID |
|
||||
| `language_code` | `str` | 必需 | 目标语言代码(例如,`"es"`、`"fr"`、`"de"`) |
|
||||
| `callback_url` | `str\|None` | `None` | 接收异步回调的 URL |
|
||||
|
||||
返回一个 `Video` 对象,其中包含配音内容。
|
||||
|
||||
### 翻译转录
|
||||
|
||||
翻译视频的转录文本,无需配音:
|
||||
|
||||
```python
|
||||
translated = video.translate_transcript(
|
||||
language="Spanish",
|
||||
additional_notes="Use formal tone",
|
||||
)
|
||||
|
||||
for entry in translated:
|
||||
print(entry)
|
||||
```
|
||||
|
||||
**支持的语言** 包括:`en`、`es`、`fr`、`de`、`it`、`pt`、`ja`、`ko`、`zh`、`hi`、`ar` 等。
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
### 为视频生成旁白
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# Get transcript
|
||||
transcript_text = video.get_transcript_text()
|
||||
|
||||
# Generate narration script using collection LLM
|
||||
result = coll.generate_text(
|
||||
prompt=(
|
||||
f"Write a professional narration script for this video content:\n"
|
||||
f"{transcript_text[:2000]}"
|
||||
),
|
||||
model_name="pro",
|
||||
)
|
||||
script = result["output"]
|
||||
|
||||
# Convert script to speech
|
||||
narration = coll.generate_voice(text=script)
|
||||
print(f"Narration audio: {narration.id}")
|
||||
```
|
||||
|
||||
### 根据提示生成缩略图
|
||||
|
||||
```python
|
||||
thumbnail = coll.generate_image(
|
||||
prompt="professional video thumbnail showing data analytics dashboard, modern design",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
print(f"Thumbnail URL: {thumbnail.generate_url()}")
|
||||
```
|
||||
|
||||
### 为视频添加生成的音乐
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# Generate background music
|
||||
music = coll.generate_music(
|
||||
prompt="calm ambient background music for a tutorial video",
|
||||
duration=60,
|
||||
)
|
||||
|
||||
# Build timeline with video + music overlay
|
||||
timeline = Timeline(conn)
|
||||
timeline.add_inline(VideoAsset(asset_id=video.id))
|
||||
timeline.add_overlay(0, AudioAsset(asset_id=music.id, disable_other_tracks=False))
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Video with music: {stream_url}")
|
||||
```
|
||||
|
||||
### 结构化 JSON 输出
|
||||
|
||||
```python
|
||||
transcript_text = video.get_transcript_text()
|
||||
|
||||
result = coll.generate_text(
|
||||
prompt=(
|
||||
f"Given this transcript:\n{transcript_text}\n\n"
|
||||
"Return a JSON object with keys: summary, topics (array), action_items (array)."
|
||||
),
|
||||
model_name="pro",
|
||||
response_type="json",
|
||||
)
|
||||
|
||||
# result["output"] is a dict when response_type="json"
|
||||
print(result["output"]["summary"])
|
||||
print(result["output"]["topics"])
|
||||
```
|
||||
|
||||
## 提示
|
||||
|
||||
* **生成的媒体是持久性的**:所有生成的内容都存储在您的集合中,并且可以重复使用。
|
||||
* **三种音频方法**:使用 `generate_music()` 生成背景音乐,`generate_sound_effect()` 生成音效,`generate_voice()` 进行文本转语音。没有统一的 `generate_audio()` 方法。
|
||||
* **文本生成是集合级的**:`coll.generate_text()` 不会自动访问视频内容。使用 `video.get_transcript_text()` 获取转录文本,并将其传递到提示中。
|
||||
* **模型层级**:`"basic"` 速度最快,`"pro"` 是平衡选项,`"ultra"` 质量最高。对于大多数分析任务,使用 `"pro"`。
|
||||
* **组合生成类型**:生成图像用于叠加、生成音乐用于背景、生成语音用于旁白,然后使用时间线进行组合(参见 [editor.md](editor.md))。
|
||||
* **提示质量很重要**:描述性、具体的提示在所有生成类型中都能产生更好的结果。
|
||||
* **图像的宽高比**:从 `"1:1"`、`"9:16"`、`"16:9"`、`"4:3"` 或 `"3:4"` 中选择。
|
||||
567
docs/zh-CN/skills/videodb/reference/rtstream-reference.md
Normal file
567
docs/zh-CN/skills/videodb/reference/rtstream-reference.md
Normal file
@@ -0,0 +1,567 @@
|
||||
# RTStream 参考
|
||||
|
||||
RTStream 操作的代码级详情。工作流程指南请参阅 [rtstream.md](rtstream.md)。
|
||||
有关使用指导和流程选择,请从 [../SKILL.md](../SKILL.md) 开始。
|
||||
|
||||
基于 [docs.videodb.io](https://docs.videodb.io/pages/ingest/live-streams/realtime-apis.md)。
|
||||
|
||||
***
|
||||
|
||||
## Collection RTStream 方法
|
||||
|
||||
`Collection` 上用于管理 RTStream 的方法:
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `coll.connect_rtstream(url, name, ...)` | `RTStream` | 从 RTSP/RTMP URL 创建新的 RTStream |
|
||||
| `coll.get_rtstream(id)` | `RTStream` | 通过 ID 获取现有的 RTStream |
|
||||
| `coll.list_rtstreams(limit, offset, status, name, ordering)` | `List[RTStream]` | 列出集合中的所有 RTStream |
|
||||
| `coll.search(query, namespace="rtstream")` | `RTStreamSearchResult` | 在所有 RTStream 中搜索 |
|
||||
|
||||
### 连接 RTStream
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
rtstream = coll.connect_rtstream(
|
||||
url="rtmp://your-stream-server/live/stream-key",
|
||||
name="My Live Stream",
|
||||
media_types=["video"], # or ["audio", "video"]
|
||||
sample_rate=30, # optional
|
||||
store=True, # enable recording storage for export
|
||||
enable_transcript=True, # optional
|
||||
ws_connection_id=ws_id, # optional, for real-time events
|
||||
)
|
||||
```
|
||||
|
||||
### 获取现有 RTStream
|
||||
|
||||
```python
|
||||
rtstream = coll.get_rtstream("rts-xxx")
|
||||
```
|
||||
|
||||
### 列出 RTStream
|
||||
|
||||
```python
|
||||
rtstreams = coll.list_rtstreams(
|
||||
limit=10,
|
||||
offset=0,
|
||||
status="connected", # optional filter
|
||||
name="meeting", # optional filter
|
||||
ordering="-created_at",
|
||||
)
|
||||
|
||||
for rts in rtstreams:
|
||||
print(f"{rts.id}: {rts.name} - {rts.status}")
|
||||
```
|
||||
|
||||
### 从捕获会话获取
|
||||
|
||||
捕获会话激活后,检索 RTStream 对象:
|
||||
|
||||
```python
|
||||
session = conn.get_capture_session(session_id)
|
||||
|
||||
mics = session.get_rtstream("mic")
|
||||
displays = session.get_rtstream("screen")
|
||||
system_audios = session.get_rtstream("system_audio")
|
||||
```
|
||||
|
||||
或使用 `capture_session.active` WebSocket 事件中的 `rtstreams` 数据:
|
||||
|
||||
```python
|
||||
for rts in rtstreams:
|
||||
rtstream = coll.get_rtstream(rts["rtstream_id"])
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## RTStream 方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `rtstream.start()` | `None` | 开始摄取 |
|
||||
| `rtstream.stop()` | `None` | 停止摄取 |
|
||||
| `rtstream.generate_stream(start, end)` | `str` | 流式传输录制的片段(Unix 时间戳) |
|
||||
| `rtstream.export(name=None)` | `RTStreamExportResult` | 导出为永久视频 |
|
||||
| `rtstream.index_visuals(prompt, ...)` | `RTStreamSceneIndex` | 创建带 AI 分析的视觉索引 |
|
||||
| `rtstream.index_audio(prompt, ...)` | `RTStreamSceneIndex` | 创建带 LLM 摘要的音频索引 |
|
||||
| `rtstream.list_scene_indexes()` | `List[RTStreamSceneIndex]` | 列出流上的所有场景索引 |
|
||||
| `rtstream.get_scene_index(index_id)` | `RTStreamSceneIndex` | 获取特定场景索引 |
|
||||
| `rtstream.search(query, ...)` | `RTStreamSearchResult` | 搜索索引内容 |
|
||||
| `rtstream.start_transcript(ws_connection_id, engine)` | `dict` | 开始实时转录 |
|
||||
| `rtstream.get_transcript(page, page_size, start, end, since)` | `dict` | 获取转录页面 |
|
||||
| `rtstream.stop_transcript(engine)` | `dict` | 停止转录 |
|
||||
|
||||
***
|
||||
|
||||
## 启动和停止
|
||||
|
||||
```python
|
||||
# Begin ingestion
|
||||
rtstream.start()
|
||||
|
||||
# ... stream is being recorded ...
|
||||
|
||||
# Stop ingestion
|
||||
rtstream.stop()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 生成流
|
||||
|
||||
使用 Unix 时间戳(而非秒数偏移)从录制内容生成播放流:
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
start_ts = time.time()
|
||||
rtstream.start()
|
||||
|
||||
# Let it record for a while...
|
||||
time.sleep(60)
|
||||
|
||||
end_ts = time.time()
|
||||
rtstream.stop()
|
||||
|
||||
# Generate a stream URL for the recorded segment
|
||||
stream_url = rtstream.generate_stream(start=start_ts, end=end_ts)
|
||||
print(f"Recorded stream: {stream_url}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 导出为视频
|
||||
|
||||
将录制的流导出为集合中的永久视频:
|
||||
|
||||
```python
|
||||
export_result = rtstream.export(name="Meeting Recording 2024-01-15")
|
||||
|
||||
print(f"Video ID: {export_result.video_id}")
|
||||
print(f"Stream URL: {export_result.stream_url}")
|
||||
print(f"Player URL: {export_result.player_url}")
|
||||
print(f"Duration: {export_result.duration}s")
|
||||
```
|
||||
|
||||
### RTStreamExportResult 属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `video_id` | `str` | 导出视频的 ID |
|
||||
| `stream_url` | `str` | HLS 流 URL |
|
||||
| `player_url` | `str` | Web 播放器 URL |
|
||||
| `name` | `str` | 视频名称 |
|
||||
| `duration` | `float` | 时长(秒) |
|
||||
|
||||
***
|
||||
|
||||
## AI 管道
|
||||
|
||||
AI 管道处理实时流并通过 WebSocket 发送结果。
|
||||
|
||||
### RTStream AI 管道方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `rtstream.index_audio(prompt, batch_config, ...)` | `RTStreamSceneIndex` | 开始带 LLM 摘要的音频索引 |
|
||||
| `rtstream.index_visuals(prompt, batch_config, ...)` | `RTStreamSceneIndex` | 开始屏幕内容的视觉索引 |
|
||||
|
||||
### 音频索引
|
||||
|
||||
以一定间隔生成音频内容的 LLM 摘要:
|
||||
|
||||
```python
|
||||
audio_index = rtstream.index_audio(
|
||||
prompt="Summarize what is being discussed",
|
||||
batch_config={"type": "word", "value": 50},
|
||||
model_name=None, # optional
|
||||
name="meeting_audio", # optional
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
**音频 batch\_config 选项:**
|
||||
|
||||
| 类型 | 值 | 描述 |
|
||||
|------|-------|-------------|
|
||||
| `"word"` | count | 每 N 个词分段 |
|
||||
| `"sentence"` | count | 每 N 个句子分段 |
|
||||
| `"time"` | seconds | 每 N 秒分段 |
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
{"type": "word", "value": 50} # every 50 words
|
||||
{"type": "sentence", "value": 5} # every 5 sentences
|
||||
{"type": "time", "value": 30} # every 30 seconds
|
||||
```
|
||||
|
||||
结果通过 `audio_index` WebSocket 通道送达。
|
||||
|
||||
### 视觉索引
|
||||
|
||||
生成视觉内容的 AI 描述:
|
||||
|
||||
```python
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="Describe what is happening on screen",
|
||||
batch_config={"type": "time", "value": 2, "frame_count": 5},
|
||||
model_name="basic",
|
||||
name="screen_monitor", # optional
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
**参数:**
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
|-----------|------|-------------|
|
||||
| `prompt` | `str` | AI 模型的指令(支持结构化 JSON 输出) |
|
||||
| `batch_config` | `dict` | 控制帧采样(见下文) |
|
||||
| `model_name` | `str` | 模型层级:`"mini"`、`"basic"`、`"pro"`、`"ultra"` |
|
||||
| `name` | `str` | 索引名称(可选) |
|
||||
| `ws_connection_id` | `str` | 用于接收结果的 WebSocket 连接 ID |
|
||||
|
||||
**视觉 batch\_config:**
|
||||
|
||||
| 键 | 类型 | 描述 |
|
||||
|-----|------|-------------|
|
||||
| `type` | `str` | 仅 `"time"` 支持视觉索引 |
|
||||
| `value` | `int` | 窗口大小(秒) |
|
||||
| `frame_count` | `int` | 每个窗口提取的帧数 |
|
||||
|
||||
示例:`{"type": "time", "value": 2, "frame_count": 5}` 每 2 秒采样 5 帧并将其发送到模型。
|
||||
|
||||
**结构化 JSON 输出:**
|
||||
|
||||
使用请求 JSON 格式的提示语以获得结构化响应:
|
||||
|
||||
```python
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="""Analyze the screen and return a JSON object with:
|
||||
{
|
||||
"app_name": "name of the active application",
|
||||
"activity": "what the user is doing",
|
||||
"ui_elements": ["list of visible UI elements"],
|
||||
"contains_text": true/false,
|
||||
"dominant_colors": ["list of main colors"]
|
||||
}
|
||||
Return only valid JSON.""",
|
||||
batch_config={"type": "time", "value": 3, "frame_count": 3},
|
||||
model_name="pro",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
结果通过 `scene_index` WebSocket 通道送达。
|
||||
|
||||
***
|
||||
|
||||
## 批处理配置摘要
|
||||
|
||||
| 索引类型 | `type` 选项 | `value` | 额外键 |
|
||||
|---------------|----------------|---------|------------|
|
||||
| **音频** | `"word"`、`"sentence"`、`"time"` | words/sentences/seconds | - |
|
||||
| **视觉** | 仅 `"time"` | seconds | `frame_count` |
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
# Audio: every 50 words
|
||||
{"type": "word", "value": 50}
|
||||
|
||||
# Audio: every 30 seconds
|
||||
{"type": "time", "value": 30}
|
||||
|
||||
# Visual: 5 frames every 2 seconds
|
||||
{"type": "time", "value": 2, "frame_count": 5}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 转录
|
||||
|
||||
通过 WebSocket 进行实时转录:
|
||||
|
||||
```python
|
||||
# Start live transcription
|
||||
rtstream.start_transcript(
|
||||
ws_connection_id=ws_id,
|
||||
engine=None, # optional, defaults to "assemblyai"
|
||||
)
|
||||
|
||||
# Get transcript pages (with optional filters)
|
||||
transcript = rtstream.get_transcript(
|
||||
page=1,
|
||||
page_size=100,
|
||||
start=None, # optional: start timestamp filter
|
||||
end=None, # optional: end timestamp filter
|
||||
since=None, # optional: for polling, get transcripts after this timestamp
|
||||
engine=None,
|
||||
)
|
||||
|
||||
# Stop transcription
|
||||
rtstream.stop_transcript(engine=None)
|
||||
```
|
||||
|
||||
转录结果通过 `transcript` WebSocket 通道送达。
|
||||
|
||||
***
|
||||
|
||||
## RTStreamSceneIndex
|
||||
|
||||
当您调用 `index_audio()` 或 `index_visuals()` 时,该方法返回一个 `RTStreamSceneIndex` 对象。此对象表示正在运行的索引,并提供用于管理场景和警报的方法。
|
||||
|
||||
```python
|
||||
# index_visuals returns an RTStreamSceneIndex
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="Describe what is on screen",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
|
||||
# index_audio also returns an RTStreamSceneIndex
|
||||
audio_index = rtstream.index_audio(
|
||||
prompt="Summarize the discussion",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
```
|
||||
|
||||
### RTStreamSceneIndex 属性
|
||||
|
||||
| 属性 | 类型 | 描述 |
|
||||
|----------|------|-------------|
|
||||
| `rtstream_index_id` | `str` | 索引的唯一 ID |
|
||||
| `rtstream_id` | `str` | 父 RTStream 的 ID |
|
||||
| `extraction_type` | `str` | 提取类型(`time` 或 `transcript`) |
|
||||
| `extraction_config` | `dict` | 提取配置 |
|
||||
| `prompt` | `str` | 用于分析的提示语 |
|
||||
| `name` | `str` | 索引名称 |
|
||||
| `status` | `str` | 状态(`connected`、`stopped`) |
|
||||
|
||||
### RTStreamSceneIndex 方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `index.get_scenes(start, end, page, page_size)` | `dict` | 获取已索引的场景 |
|
||||
| `index.start()` | `None` | 启动/恢复索引 |
|
||||
| `index.stop()` | `None` | 停止索引 |
|
||||
| `index.create_alert(event_id, callback_url, ws_connection_id)` | `str` | 创建事件检测警报 |
|
||||
| `index.list_alerts()` | `list` | 列出此索引上的所有警报 |
|
||||
| `index.enable_alert(alert_id)` | `None` | 启用警报 |
|
||||
| `index.disable_alert(alert_id)` | `None` | 禁用警报 |
|
||||
|
||||
### 获取场景
|
||||
|
||||
从索引轮询已索引的场景:
|
||||
|
||||
```python
|
||||
result = scene_index.get_scenes(
|
||||
start=None, # optional: start timestamp
|
||||
end=None, # optional: end timestamp
|
||||
page=1,
|
||||
page_size=100,
|
||||
)
|
||||
|
||||
for scene in result["scenes"]:
|
||||
print(f"[{scene['start']}-{scene['end']}] {scene['text']}")
|
||||
|
||||
if result["next_page"]:
|
||||
# fetch next page
|
||||
pass
|
||||
```
|
||||
|
||||
### 管理场景索引
|
||||
|
||||
```python
|
||||
# List all indexes on the stream
|
||||
indexes = rtstream.list_scene_indexes()
|
||||
|
||||
# Get a specific index by ID
|
||||
scene_index = rtstream.get_scene_index(index_id)
|
||||
|
||||
# Stop an index
|
||||
scene_index.stop()
|
||||
|
||||
# Restart an index
|
||||
scene_index.start()
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 事件
|
||||
|
||||
事件是可重用的检测规则。创建一次,即可通过警报附加到任何索引。
|
||||
|
||||
### 连接事件方法
|
||||
|
||||
| 方法 | 返回 | 描述 |
|
||||
|--------|---------|-------------|
|
||||
| `conn.create_event(event_prompt, label)` | `str` (event\_id) | 创建检测事件 |
|
||||
| `conn.list_events()` | `list` | 列出所有事件 |
|
||||
|
||||
### 创建事件
|
||||
|
||||
```python
|
||||
event_id = conn.create_event(
|
||||
event_prompt="User opened Slack application",
|
||||
label="slack_opened",
|
||||
)
|
||||
```
|
||||
|
||||
### 列出事件
|
||||
|
||||
```python
|
||||
events = conn.list_events()
|
||||
for event in events:
|
||||
print(f"{event['event_id']}: {event['label']}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 警报
|
||||
|
||||
警报将事件连接到索引以实现实时通知。当 AI 检测到与事件描述匹配的内容时,会发送警报。
|
||||
|
||||
### 创建警报
|
||||
|
||||
```python
|
||||
# Get the RTStreamSceneIndex from index_visuals
|
||||
scene_index = rtstream.index_visuals(
|
||||
prompt="Describe what application is open on screen",
|
||||
ws_connection_id=ws_id,
|
||||
)
|
||||
|
||||
# Create an alert on the index
|
||||
alert_id = scene_index.create_alert(
|
||||
event_id=event_id,
|
||||
callback_url="https://your-backend.com/alerts", # for webhook delivery
|
||||
ws_connection_id=ws_id, # for WebSocket delivery (optional)
|
||||
)
|
||||
```
|
||||
|
||||
**注意:** `callback_url` 是必需的。如果仅使用 WebSocket 交付,请传递空字符串 `""`。
|
||||
|
||||
### 管理警报
|
||||
|
||||
```python
|
||||
# List all alerts on an index
|
||||
alerts = scene_index.list_alerts()
|
||||
|
||||
# Enable/disable alerts
|
||||
scene_index.disable_alert(alert_id)
|
||||
scene_index.enable_alert(alert_id)
|
||||
```
|
||||
|
||||
### 警报交付
|
||||
|
||||
| 方法 | 延迟 | 使用场景 |
|
||||
|--------|---------|----------|
|
||||
| WebSocket | 实时 | 仪表板、实时 UI |
|
||||
| Webhook | < 1 秒 | 服务器到服务器、自动化 |
|
||||
|
||||
### WebSocket 警报事件
|
||||
|
||||
```json
|
||||
{
|
||||
"channel": "alert",
|
||||
"rtstream_id": "rts-xxx",
|
||||
"data": {
|
||||
"event_label": "slack_opened",
|
||||
"timestamp": 1710000012340,
|
||||
"text": "User opened Slack application"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook 负载
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "event-xxx",
|
||||
"label": "slack_opened",
|
||||
"confidence": 0.95,
|
||||
"explanation": "User opened the Slack application",
|
||||
"timestamp": "2024-01-15T10:30:45Z",
|
||||
"start_time": 1234.5,
|
||||
"end_time": 1238.0,
|
||||
"stream_url": "https://stream.videodb.io/v3/...",
|
||||
"player_url": "https://console.videodb.io/player?url=..."
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## WebSocket 集成
|
||||
|
||||
所有实时 AI 结果均通过 WebSocket 交付。将 `ws_connection_id` 传递给:
|
||||
|
||||
* `rtstream.start_transcript()`
|
||||
* `rtstream.index_audio()`
|
||||
* `rtstream.index_visuals()`
|
||||
* `scene_index.create_alert()`
|
||||
|
||||
### WebSocket 通道
|
||||
|
||||
| 通道 | 来源 | 内容 |
|
||||
|---------|--------|---------|
|
||||
| `transcript` | `start_transcript()` | 实时语音转文本 |
|
||||
| `scene_index` | `index_visuals()` | 视觉分析结果 |
|
||||
| `audio_index` | `index_audio()` | 音频分析结果 |
|
||||
| `alert` | `create_alert()` | 警报通知 |
|
||||
|
||||
有关 WebSocket 事件结构和 ws\_listener 用法,请参阅 [capture-reference.md](capture-reference.md)。
|
||||
|
||||
***
|
||||
|
||||
## 完整工作流程
|
||||
|
||||
```python
|
||||
import time
|
||||
import videodb
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
# 1. Connect and start recording
|
||||
rtstream = coll.connect_rtstream(
|
||||
url="rtmp://your-stream-server/live/stream-key",
|
||||
name="Weekly Standup",
|
||||
store=True,
|
||||
)
|
||||
rtstream.start()
|
||||
|
||||
# 2. Record for the duration of the meeting
|
||||
start_ts = time.time()
|
||||
time.sleep(1800) # 30 minutes
|
||||
end_ts = time.time()
|
||||
rtstream.stop()
|
||||
|
||||
# Generate an immediate playback URL for the captured window
|
||||
stream_url = rtstream.generate_stream(start=start_ts, end=end_ts)
|
||||
print(f"Recorded stream: {stream_url}")
|
||||
|
||||
# 3. Export to a permanent video
|
||||
export_result = rtstream.export(name="Weekly Standup Recording")
|
||||
print(f"Exported video: {export_result.video_id}")
|
||||
|
||||
# 4. Index the exported video for search
|
||||
video = coll.get_video(export_result.video_id)
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# 5. Search for action items
|
||||
try:
|
||||
results = video.search("action items and next steps")
|
||||
stream_url = results.compile()
|
||||
print(f"Action items clip: {stream_url}")
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
print("No action items were detected in the recording.")
|
||||
else:
|
||||
raise
|
||||
```
|
||||
59
docs/zh-CN/skills/videodb/reference/rtstream.md
Normal file
59
docs/zh-CN/skills/videodb/reference/rtstream.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# RTStream 指南
|
||||
|
||||
## 概述
|
||||
|
||||
RTStream 支持实时摄取直播视频流(RTSP/RTMP)和桌面捕获会话。连接后,您可以录制、索引、搜索和导出实时源的内容。
|
||||
|
||||
有关代码级别的详细信息(SDK 方法、参数、示例),请参阅 [rtstream-reference.md](rtstream-reference.md)。
|
||||
|
||||
## 使用场景
|
||||
|
||||
* **安防与监控**:连接 RTSP 摄像头,检测事件,触发警报
|
||||
* **直播广播**:摄取 RTMP 流,实时索引,实现即时搜索
|
||||
* **会议录制**:捕获桌面屏幕和音频,实时转录,导出录制内容
|
||||
* **事件处理**:监控实时视频流,运行 AI 分析,响应检测到的内容
|
||||
|
||||
## 快速入门
|
||||
|
||||
1. **连接到实时流**(RTSP/RTMP URL)或从捕获会话获取 RTStream
|
||||
2. **开始摄取**以开始录制实时内容
|
||||
3. **启动 AI 流水线**以进行实时索引(音频、视觉、转录)
|
||||
4. **通过 WebSocket 监控事件**以获取实时 AI 结果和警报
|
||||
5. **完成时停止摄取**
|
||||
6. **导出为视频**以便永久存储和进一步处理
|
||||
7. **搜索录制内容**以查找特定时刻
|
||||
|
||||
## RTStream 来源
|
||||
|
||||
### 来自 RTSP/RTMP 流
|
||||
|
||||
直接连接到实时视频源:
|
||||
|
||||
```python
|
||||
rtstream = coll.connect_rtstream(
|
||||
url="rtmp://your-stream-server/live/stream-key",
|
||||
name="My Live Stream",
|
||||
)
|
||||
```
|
||||
|
||||
### 来自捕获会话
|
||||
|
||||
从桌面捕获(麦克风、屏幕、系统音频)获取 RTStream:
|
||||
|
||||
```python
|
||||
session = conn.get_capture_session(session_id)
|
||||
|
||||
mics = session.get_rtstream("mic")
|
||||
displays = session.get_rtstream("screen")
|
||||
system_audios = session.get_rtstream("system_audio")
|
||||
```
|
||||
|
||||
有关捕获会话的工作流程,请参阅 [capture.md](capture.md)。
|
||||
|
||||
***
|
||||
|
||||
## 脚本
|
||||
|
||||
| 脚本 | 描述 |
|
||||
|--------|-------------|
|
||||
| `scripts/ws_listener.py` | 用于实时 AI 结果的 WebSocket 事件监听器 |
|
||||
230
docs/zh-CN/skills/videodb/reference/search.md
Normal file
230
docs/zh-CN/skills/videodb/reference/search.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 搜索与索引指南
|
||||
|
||||
搜索功能允许您使用自然语言查询、精确关键词或视觉场景描述来查找视频中的特定时刻。
|
||||
|
||||
## 前提条件
|
||||
|
||||
视频**必须被索引**后才能进行搜索。每种索引类型对每个视频只需执行一次索引操作。
|
||||
|
||||
## 索引
|
||||
|
||||
### 口语词索引
|
||||
|
||||
为视频的转录语音内容建立索引,以支持语义搜索和关键词搜索:
|
||||
|
||||
```python
|
||||
video = coll.get_video(video_id)
|
||||
|
||||
# force=True makes indexing idempotent — skips if already indexed
|
||||
video.index_spoken_words(force=True)
|
||||
```
|
||||
|
||||
此操作会转录音轨,并在口语内容上构建可搜索的索引。这是进行语义搜索和关键词搜索所必需的。
|
||||
|
||||
**参数:**
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|-----------|------|---------|-------------|
|
||||
| `language_code` | `str\|None` | `None` | 视频的语言代码 |
|
||||
| `segmentation_type` | `SegmentationType` | `SegmentationType.sentence` | 分割类型 (`sentence` 或 `llm`) |
|
||||
| `force` | `bool` | `False` | 设置为 `True` 以跳过已索引的情况(避免“已存在”错误) |
|
||||
| `callback_url` | `str\|None` | `None` | 用于异步通知的 Webhook URL |
|
||||
|
||||
### 场景索引
|
||||
|
||||
通过生成场景的 AI 描述来索引视觉内容。与口语词索引类似,如果场景索引已存在,此操作会引发错误。从错误消息中提取现有的 `scene_index_id`。
|
||||
|
||||
```python
|
||||
import re
|
||||
from videodb import SceneExtractionType
|
||||
|
||||
try:
|
||||
scene_index_id = video.index_scenes(
|
||||
extraction_type=SceneExtractionType.shot_based,
|
||||
prompt="Describe the visual content, objects, actions, and setting in this scene.",
|
||||
)
|
||||
except Exception as e:
|
||||
match = re.search(r"id\s+([a-f0-9]+)", str(e))
|
||||
if match:
|
||||
scene_index_id = match.group(1)
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
**提取类型:**
|
||||
|
||||
| 类型 | 描述 | 最佳适用场景 |
|
||||
|------|-------------|----------|
|
||||
| `SceneExtractionType.shot_based` | 基于视觉镜头边界进行分割 | 通用目的,动作内容 |
|
||||
| `SceneExtractionType.time_based` | 按固定间隔进行分割 | 均匀采样,长时间静态内容 |
|
||||
| `SceneExtractionType.transcript` | 基于转录片段进行分割 | 语音驱动的场景边界 |
|
||||
|
||||
**`time_based` 的参数:**
|
||||
|
||||
```python
|
||||
video.index_scenes(
|
||||
extraction_type=SceneExtractionType.time_based,
|
||||
extraction_config={"time": 5, "select_frames": ["first", "last"]},
|
||||
prompt="Describe what is happening in this scene.",
|
||||
)
|
||||
```
|
||||
|
||||
## 搜索类型
|
||||
|
||||
### 语义搜索
|
||||
|
||||
使用自然语言查询匹配口语内容:
|
||||
|
||||
```python
|
||||
from videodb import SearchType
|
||||
|
||||
results = video.search(
|
||||
query="explaining the benefits of machine learning",
|
||||
search_type=SearchType.semantic,
|
||||
)
|
||||
```
|
||||
|
||||
返回口语内容在语义上与查询匹配的排序片段。
|
||||
|
||||
### 关键词搜索
|
||||
|
||||
在转录语音中进行精确术语匹配:
|
||||
|
||||
```python
|
||||
results = video.search(
|
||||
query="artificial intelligence",
|
||||
search_type=SearchType.keyword,
|
||||
)
|
||||
```
|
||||
|
||||
返回包含精确关键词或短语的片段。
|
||||
|
||||
### 场景搜索
|
||||
|
||||
视觉内容查询与已索引的场景描述进行匹配。需要事先调用 `index_scenes()`。
|
||||
|
||||
`index_scenes()` 返回一个 `scene_index_id`。将其传递给 `video.search()` 以定位特定的场景索引(当视频有多个场景索引时尤其重要):
|
||||
|
||||
```python
|
||||
from videodb import SearchType, IndexType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
# Search using semantic search against the scene index.
|
||||
# Use score_threshold to filter low-relevance noise (recommended: 0.3+).
|
||||
try:
|
||||
results = video.search(
|
||||
query="person writing on a whiteboard",
|
||||
search_type=SearchType.semantic,
|
||||
index_type=IndexType.scene,
|
||||
scene_index_id=scene_index_id,
|
||||
score_threshold=0.3,
|
||||
)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as e:
|
||||
if "No results found" in str(e):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
**重要说明:**
|
||||
|
||||
* 将 `SearchType.semantic` 与 `index_type=IndexType.scene` 结合使用——这是最可靠的组合,适用于所有套餐。
|
||||
* `SearchType.scene` 存在,但可能并非在所有套餐中都可用(例如免费套餐)。建议优先使用 `SearchType.semantic` 与 `IndexType.scene`。
|
||||
* `scene_index_id` 参数是可选的。如果省略,搜索将针对视频上的所有场景索引运行。传递此参数以定位特定索引。
|
||||
* 您可以为每个视频创建多个场景索引(使用不同的提示或提取类型),并使用 `scene_index_id` 独立搜索它们。
|
||||
|
||||
### 带元数据筛选的场景搜索
|
||||
|
||||
使用自定义元数据索引场景时,可以将语义搜索与元数据筛选器结合使用:
|
||||
|
||||
```python
|
||||
from videodb import SearchType, IndexType
|
||||
|
||||
results = video.search(
|
||||
query="a skillful chasing scene",
|
||||
search_type=SearchType.semantic,
|
||||
index_type=IndexType.scene,
|
||||
scene_index_id=scene_index_id,
|
||||
filter=[{"camera_view": "road_ahead"}, {"action_type": "chasing"}],
|
||||
)
|
||||
```
|
||||
|
||||
有关自定义元数据索引和筛选搜索的完整示例,请参阅 [scene\_level\_metadata\_indexing 示例](https://github.com/video-db/videodb-cookbook/blob/main/quickstart/scene_level_metadata_indexing.ipynb)。
|
||||
|
||||
## 处理结果
|
||||
|
||||
### 获取片段
|
||||
|
||||
访问单个结果片段:
|
||||
|
||||
```python
|
||||
results = video.search("your query")
|
||||
|
||||
for shot in results.get_shots():
|
||||
print(f"Video: {shot.video_id}")
|
||||
print(f"Start: {shot.start:.2f}s")
|
||||
print(f"End: {shot.end:.2f}s")
|
||||
print(f"Text: {shot.text}")
|
||||
print("---")
|
||||
```
|
||||
|
||||
### 播放编译结果
|
||||
|
||||
将所有匹配片段作为单个编译视频进行流式播放:
|
||||
|
||||
```python
|
||||
results = video.search("your query")
|
||||
stream_url = results.compile()
|
||||
results.play() # opens compiled stream in browser
|
||||
```
|
||||
|
||||
### 提取剪辑
|
||||
|
||||
下载或流式播放特定的结果片段:
|
||||
|
||||
```python
|
||||
for shot in results.get_shots():
|
||||
stream_url = shot.generate_stream()
|
||||
print(f"Clip: {stream_url}")
|
||||
```
|
||||
|
||||
## 跨集合搜索
|
||||
|
||||
跨集合中的所有视频进行搜索:
|
||||
|
||||
```python
|
||||
coll = conn.get_collection()
|
||||
|
||||
# Search across all videos in the collection
|
||||
results = coll.search(
|
||||
query="product demo",
|
||||
search_type=SearchType.semantic,
|
||||
)
|
||||
|
||||
for shot in results.get_shots():
|
||||
print(f"Video: {shot.video_id} [{shot.start:.1f}s - {shot.end:.1f}s]")
|
||||
```
|
||||
|
||||
> **注意:** 集合级搜索仅支持 `SearchType.semantic`。将 `SearchType.keyword` 或 `SearchType.scene` 与 `coll.search()` 结合使用将引发 `NotImplementedError`。要进行关键词或场景搜索,请改为对单个视频使用 `video.search()`。
|
||||
|
||||
## 搜索 + 编译
|
||||
|
||||
对匹配片段进行索引、搜索并编译成单个可播放的流:
|
||||
|
||||
```python
|
||||
video.index_spoken_words(force=True)
|
||||
results = video.search(query="your query", search_type=SearchType.semantic)
|
||||
stream_url = results.compile()
|
||||
print(stream_url)
|
||||
```
|
||||
|
||||
## 提示
|
||||
|
||||
* **一次索引,多次搜索**:索引是昂贵的操作。一旦索引完成,搜索会很快。
|
||||
* **组合索引类型**:同时索引口语词和场景,以便在同一视频上启用所有搜索类型。
|
||||
* **优化查询**:语义搜索最适合描述性的自然语言短语,而不是单个关键词。
|
||||
* **使用关键词搜索提高精度**:当您需要精确的术语匹配时,关键词搜索可以避免语义漂移。
|
||||
* **处理“未找到结果”**:当没有结果匹配时,`video.search()` 会引发 `InvalidRequestError`。始终将搜索调用包装在 try/except 中,并将 `"No results found"` 视为空结果集。
|
||||
* **过滤场景搜索噪声**:对于模糊查询,语义场景搜索可能会返回低相关性的结果。使用 `score_threshold=0.3`(或更高值)来过滤噪声。
|
||||
* **幂等索引**:使用 `index_spoken_words(force=True)` 可以安全地重新索引。`index_scenes()` 没有 `force` 参数——将其包装在 try/except 中,并使用 `re.search(r"id\s+([a-f0-9]+)", str(e))` 从错误消息中提取现有的 `scene_index_id`。
|
||||
406
docs/zh-CN/skills/videodb/reference/streaming.md
Normal file
406
docs/zh-CN/skills/videodb/reference/streaming.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# 流媒体与播放
|
||||
|
||||
VideoDB 按需生成流媒体,返回 HLS 兼容的 URL,可在任何标准视频播放器中即时播放。无需渲染时间或导出等待——编辑、搜索和组合内容可立即流式传输。
|
||||
|
||||
## 前提条件
|
||||
|
||||
视频**必须上传**到某个集合后,才能生成流媒体。对于基于搜索的流媒体,视频还必须被**索引**(口语单词和/或场景)。有关索引的详细信息,请参阅 [search.md](search.md)。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 流媒体生成
|
||||
|
||||
VideoDB 中的每个视频、搜索结果和时间线都可以生成一个**流媒体 URL**。该 URL 指向一个按需编译的 HLS(HTTP 实时流媒体)清单。
|
||||
|
||||
```python
|
||||
# From a video
|
||||
stream_url = video.generate_stream()
|
||||
|
||||
# From a timeline
|
||||
stream_url = timeline.generate_stream()
|
||||
|
||||
# From search results
|
||||
stream_url = results.compile()
|
||||
```
|
||||
|
||||
## 流式传输单个视频
|
||||
|
||||
### 基本播放
|
||||
|
||||
```python
|
||||
import videodb
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
# Generate stream URL
|
||||
stream_url = video.generate_stream()
|
||||
print(f"Stream: {stream_url}")
|
||||
|
||||
# Open in default browser
|
||||
video.play()
|
||||
```
|
||||
|
||||
### 带字幕
|
||||
|
||||
```python
|
||||
# Index and add subtitles first
|
||||
video.index_spoken_words(force=True)
|
||||
stream_url = video.add_subtitle()
|
||||
|
||||
# Returned URL already includes subtitles
|
||||
print(f"Subtitled stream: {stream_url}")
|
||||
```
|
||||
|
||||
### 特定片段
|
||||
|
||||
通过传递时间戳范围的时间线,仅流式传输视频的一部分:
|
||||
|
||||
```python
|
||||
# Stream seconds 10-30 and 60-90
|
||||
stream_url = video.generate_stream(timeline=[(10, 30), (60, 90)])
|
||||
print(f"Segment stream: {stream_url}")
|
||||
```
|
||||
|
||||
## 流式传输时间线组合
|
||||
|
||||
构建多资产组合并实时流式传输:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset, ImageAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
video = coll.get_video(video_id)
|
||||
music = coll.get_audio(music_id)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Main video content
|
||||
timeline.add_inline(VideoAsset(asset_id=video.id))
|
||||
|
||||
# Background music overlay (starts at second 0)
|
||||
timeline.add_overlay(0, AudioAsset(asset_id=music.id))
|
||||
|
||||
# Text overlay at the beginning
|
||||
timeline.add_overlay(0, TextAsset(
|
||||
text="Live Demo",
|
||||
duration=3,
|
||||
style=TextStyle(fontsize=48, fontcolor="white", boxcolor="#000000"),
|
||||
))
|
||||
|
||||
# Generate the composed stream
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Composed stream: {stream_url}")
|
||||
```
|
||||
|
||||
**重要说明:**`add_inline()` 仅接受 `VideoAsset`。对于 `AudioAsset`、`ImageAsset` 和 `TextAsset`,请使用 `add_overlay()`。
|
||||
|
||||
有关详细的时间线编辑,请参阅 [editor.md](editor.md)。
|
||||
|
||||
## 流式传输搜索结果
|
||||
|
||||
将搜索结果编译为包含所有匹配片段的单一流:
|
||||
|
||||
```python
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
try:
|
||||
results = video.search("key announcement", search_type=SearchType.semantic)
|
||||
|
||||
# Compile all matching shots into one stream
|
||||
stream_url = results.compile()
|
||||
print(f"Search results stream: {stream_url}")
|
||||
|
||||
# Or play directly
|
||||
results.play()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
print("No matching announcement segments were found.")
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
### 流式传输单个搜索结果
|
||||
|
||||
```python
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
|
||||
try:
|
||||
results = video.search("product demo", search_type=SearchType.semantic)
|
||||
for i, shot in enumerate(results.get_shots()):
|
||||
stream_url = shot.generate_stream()
|
||||
print(f"Hit {i+1} [{shot.start:.1f}s-{shot.end:.1f}s]: {stream_url}")
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
print("No product demo segments matched the query.")
|
||||
else:
|
||||
raise
|
||||
```
|
||||
|
||||
## 音频播放
|
||||
|
||||
获取音频内容的签名播放 URL:
|
||||
|
||||
```python
|
||||
audio = coll.get_audio(audio_id)
|
||||
playback_url = audio.generate_url()
|
||||
print(f"Audio URL: {playback_url}")
|
||||
```
|
||||
|
||||
## 完整工作流程示例
|
||||
|
||||
### 搜索到流媒体管道
|
||||
|
||||
在一个工作流程中结合搜索、时间线组合和流式传输:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
# Search for key moments
|
||||
queries = ["introduction", "main demo", "Q&A"]
|
||||
timeline = Timeline(conn)
|
||||
timeline_offset = 0.0
|
||||
|
||||
for query in queries:
|
||||
try:
|
||||
results = video.search(query, search_type=SearchType.semantic)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
|
||||
if not shots:
|
||||
continue
|
||||
|
||||
# Add the section label where this batch starts in the compiled timeline
|
||||
timeline.add_overlay(timeline_offset, TextAsset(
|
||||
text=query.title(),
|
||||
duration=2,
|
||||
style=TextStyle(fontsize=36, fontcolor="white", boxcolor="#222222"),
|
||||
))
|
||||
|
||||
for shot in shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Dynamic compilation: {stream_url}")
|
||||
```
|
||||
|
||||
### 多视频流
|
||||
|
||||
将来自不同视频的片段组合成单一流:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
video_clips = [
|
||||
{"id": "vid_001", "start": 0, "end": 15},
|
||||
{"id": "vid_002", "start": 10, "end": 30},
|
||||
{"id": "vid_003", "start": 5, "end": 25},
|
||||
]
|
||||
|
||||
timeline = Timeline(conn)
|
||||
for clip in video_clips:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=clip["id"], start=clip["start"], end=clip["end"])
|
||||
)
|
||||
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Multi-video stream: {stream_url}")
|
||||
```
|
||||
|
||||
### 条件流媒体组装
|
||||
|
||||
根据搜索结果的可用性动态构建流媒体:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
video = coll.get_video("your-video-id")
|
||||
|
||||
video.index_spoken_words(force=True)
|
||||
|
||||
timeline = Timeline(conn)
|
||||
|
||||
# Try to find specific content; fall back to full video
|
||||
topics = ["opening remarks", "technical deep dive", "closing"]
|
||||
|
||||
found_any = False
|
||||
timeline_offset = 0.0
|
||||
for topic in topics:
|
||||
try:
|
||||
results = video.search(topic, search_type=SearchType.semantic)
|
||||
shots = results.get_shots()
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
shots = []
|
||||
else:
|
||||
raise
|
||||
|
||||
if shots:
|
||||
found_any = True
|
||||
timeline.add_overlay(timeline_offset, TextAsset(
|
||||
text=topic.title(),
|
||||
duration=2,
|
||||
style=TextStyle(fontsize=32, fontcolor="white", boxcolor="#1a1a2e"),
|
||||
))
|
||||
for shot in shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
|
||||
if found_any:
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Curated stream: {stream_url}")
|
||||
else:
|
||||
# Fall back to full video stream
|
||||
stream_url = video.generate_stream()
|
||||
print(f"Full video stream: {stream_url}")
|
||||
```
|
||||
|
||||
### 直播事件回顾
|
||||
|
||||
将事件录音处理成包含多个部分的可流式传输回顾:
|
||||
|
||||
```python
|
||||
import videodb
|
||||
from videodb import SearchType
|
||||
from videodb.exceptions import InvalidRequestError
|
||||
from videodb.timeline import Timeline
|
||||
from videodb.asset import VideoAsset, AudioAsset, ImageAsset, TextAsset, TextStyle
|
||||
|
||||
conn = videodb.connect()
|
||||
coll = conn.get_collection()
|
||||
|
||||
# Upload event recording
|
||||
event = coll.upload(url="https://example.com/event-recording.mp4")
|
||||
event.index_spoken_words(force=True)
|
||||
|
||||
# Generate background music
|
||||
music = coll.generate_music(
|
||||
prompt="upbeat corporate background music",
|
||||
duration=120,
|
||||
)
|
||||
|
||||
# Generate title image
|
||||
title_img = coll.generate_image(
|
||||
prompt="modern event recap title card, dark background, professional",
|
||||
aspect_ratio="16:9",
|
||||
)
|
||||
|
||||
# Build the recap timeline
|
||||
timeline = Timeline(conn)
|
||||
timeline_offset = 0.0
|
||||
|
||||
# Main video segments from search
|
||||
try:
|
||||
keynote = event.search("keynote announcement", search_type=SearchType.semantic)
|
||||
keynote_shots = keynote.get_shots()[:5]
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
keynote_shots = []
|
||||
else:
|
||||
raise
|
||||
if keynote_shots:
|
||||
keynote_start = timeline_offset
|
||||
for shot in keynote_shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
else:
|
||||
keynote_start = None
|
||||
|
||||
try:
|
||||
demo = event.search("product demo", search_type=SearchType.semantic)
|
||||
demo_shots = demo.get_shots()[:5]
|
||||
except InvalidRequestError as exc:
|
||||
if "No results found" in str(exc):
|
||||
demo_shots = []
|
||||
else:
|
||||
raise
|
||||
if demo_shots:
|
||||
demo_start = timeline_offset
|
||||
for shot in demo_shots:
|
||||
timeline.add_inline(
|
||||
VideoAsset(asset_id=shot.video_id, start=shot.start, end=shot.end)
|
||||
)
|
||||
timeline_offset += shot.end - shot.start
|
||||
else:
|
||||
demo_start = None
|
||||
|
||||
# Overlay title card image
|
||||
timeline.add_overlay(0, ImageAsset(
|
||||
asset_id=title_img.id, width=100, height=100, x=80, y=20, duration=5
|
||||
))
|
||||
|
||||
# Overlay section labels at the correct timeline offsets
|
||||
if keynote_start is not None:
|
||||
timeline.add_overlay(max(5, keynote_start), TextAsset(
|
||||
text="Keynote Highlights",
|
||||
duration=3,
|
||||
style=TextStyle(fontsize=40, fontcolor="white", boxcolor="#0d1117"),
|
||||
))
|
||||
if demo_start is not None:
|
||||
timeline.add_overlay(max(5, demo_start), TextAsset(
|
||||
text="Demo Highlights",
|
||||
duration=3,
|
||||
style=TextStyle(fontsize=36, fontcolor="white", boxcolor="#0d1117"),
|
||||
))
|
||||
|
||||
# Overlay background music
|
||||
timeline.add_overlay(0, AudioAsset(
|
||||
asset_id=music.id, fade_in_duration=3
|
||||
))
|
||||
|
||||
# Stream the final recap
|
||||
stream_url = timeline.generate_stream()
|
||||
print(f"Event recap: {stream_url}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 提示
|
||||
|
||||
* **HLS 兼容性**:流媒体 URL 返回 HLS 清单(`.m3u8`)。它们在 Safari 中原生工作,在其他浏览器中通过 hls.js 或类似库工作。
|
||||
* **按需编译**:流媒体在请求时在服务器端编译。首次播放可能会有短暂的编译延迟;同一组合的后续播放会被缓存。
|
||||
* **缓存**:第二次调用 `video.generate_stream()`(不带参数)将返回缓存的流媒体 URL,而不是重新编译。
|
||||
* **片段流**:`video.generate_stream(timeline=[(start, end)])` 是流式传输特定剪辑的最快方式,无需构建完整的 `Timeline` 对象。
|
||||
* **内联与叠加**:`add_inline()` 仅接受 `VideoAsset` 并将资产按顺序放置在主轨道上。`add_overlay()` 接受 `AudioAsset`、`ImageAsset` 和 `TextAsset`,并在给定开始时间将它们叠加在顶部。
|
||||
* **TextStyle 默认值**:`TextStyle` 默认为 `font='Sans'`、`fontcolor='black'`。对于文本背景色,请使用 `boxcolor`(而非 `bgcolor`)。
|
||||
* **与生成结合**:使用 `coll.generate_music(prompt, duration)` 和 `coll.generate_image(prompt, aspect_ratio)` 为时间线组合创建资产。
|
||||
* **播放**:`.play()` 在默认系统浏览器中打开流媒体 URL。对于编程使用,请直接处理 URL 字符串。
|
||||
142
docs/zh-CN/skills/videodb/reference/use-cases.md
Normal file
142
docs/zh-CN/skills/videodb/reference/use-cases.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 使用场景
|
||||
|
||||
常见工作流及 VideoDB 所实现的功能。代码详情请参阅 [api-reference.md](api-reference.md)、[capture.md](capture.md)、[editor.md](editor.md) 和 [search.md](search.md)。
|
||||
|
||||
***
|
||||
|
||||
## 视频搜索与精彩片段
|
||||
|
||||
### 创建精彩集锦
|
||||
|
||||
上传长视频(会议演讲、讲座、会议录音),按主题("产品发布"、"问答环节"、"演示")搜索关键片段,并自动将匹配的片段汇编成可分享的精彩集锦。
|
||||
|
||||
### 构建可搜索视频库
|
||||
|
||||
批量上传视频到集合中,为语音内容建立索引以便搜索,然后在整个库中进行查询。即时在数百小时的内容中找到特定主题。
|
||||
|
||||
### 提取特定片段
|
||||
|
||||
搜索与查询匹配的片段("预算讨论"、"行动项"),并将每个匹配的片段提取为独立的剪辑,拥有自己的流媒体 URL。
|
||||
|
||||
***
|
||||
|
||||
## 视频增强
|
||||
|
||||
### 增添专业质感
|
||||
|
||||
获取原始素材并进行增强:
|
||||
|
||||
* 根据语音自动生成字幕
|
||||
* 在特定时间戳添加自定义缩略图
|
||||
* 背景音乐叠加
|
||||
* 带有生成图像的开场/结尾序列
|
||||
|
||||
### AI 增强内容
|
||||
|
||||
将现有视频与生成式 AI 结合:
|
||||
|
||||
* 根据转录内容生成文本摘要
|
||||
* 创建与视频时长匹配的背景音乐
|
||||
* 生成标题卡和叠加图像
|
||||
* 将所有元素混合成精美的最终输出
|
||||
|
||||
***
|
||||
|
||||
## 实时录制(桌面/会议)
|
||||
|
||||
### 带 AI 的屏幕 + 音频录制
|
||||
|
||||
同时捕获屏幕、麦克风和系统音频。实时获取:
|
||||
|
||||
* **实时转录** - 语音即时转文本
|
||||
* **音频摘要** - 定期生成的 AI 讨论摘要
|
||||
* **视觉索引** - AI 对屏幕活动的描述
|
||||
|
||||
### 带摘要功能的会议录制
|
||||
|
||||
录制会议并实时转录所有参与者的发言。获取包含关键讨论点、决策和行动项的定期摘要,实时交付。
|
||||
|
||||
### 屏幕活动追踪
|
||||
|
||||
通过 AI 生成的描述追踪屏幕活动:
|
||||
|
||||
* "用户正在 Google Sheets 中浏览电子表格"
|
||||
* "用户切换到了包含 Python 文件的代码编辑器"
|
||||
* "正在进行屏幕共享的视频通话"
|
||||
|
||||
### 会话后处理
|
||||
|
||||
录制结束后,录音将导出为永久视频。然后:
|
||||
|
||||
* 生成可搜索的转录稿
|
||||
* 在录制内容中搜索特定主题
|
||||
* 提取重要时刻的片段
|
||||
* 通过流媒体 URL 或播放器链接分享
|
||||
|
||||
***
|
||||
|
||||
## 直播流智能处理(RTSP/RTMP)
|
||||
|
||||
### 连接外部流
|
||||
|
||||
从 RTSP/RTMP 源(安全摄像头、编码器、广播)摄取实时视频。实时处理和索引内容。
|
||||
|
||||
### 实时事件检测
|
||||
|
||||
定义要在直播流中检测的事件:
|
||||
|
||||
* "人员进入限制区域"
|
||||
* "十字路口交通违规"
|
||||
* "货架上可见产品"
|
||||
|
||||
当事件发生时,通过 WebSocket 或 webhook 获取警报。
|
||||
|
||||
### 直播流搜索
|
||||
|
||||
在已录制的直播流内容中搜索。从数小时的连续素材中找到特定时刻并生成剪辑。
|
||||
|
||||
***
|
||||
|
||||
## 内容审核与安全
|
||||
|
||||
### 自动化内容审查
|
||||
|
||||
使用 AI 索引视频场景并搜索有问题内容。标记包含暴力、不当内容或违反政策的视频。
|
||||
|
||||
### 脏话检测
|
||||
|
||||
检测并定位音频中的脏话。可选择在检测到的时间戳叠加哔声。
|
||||
|
||||
***
|
||||
|
||||
## 平台集成
|
||||
|
||||
### 社交媒体格式调整
|
||||
|
||||
为不同平台调整视频格式:
|
||||
|
||||
* 垂直(9:16)用于 TikTok、Reels、Shorts
|
||||
* 方形(1:1)用于 Instagram 动态
|
||||
* 横屏(16:9)用于 YouTube
|
||||
|
||||
### 为分发转码
|
||||
|
||||
针对不同的分发目标更改分辨率、比特率或质量。为网页、移动端或广播输出优化的流。
|
||||
|
||||
### 生成可分享链接
|
||||
|
||||
每次操作都会生成可播放的流媒体 URL。可嵌入网页播放器、直接分享或与现有平台集成。
|
||||
|
||||
***
|
||||
|
||||
## 工作流摘要
|
||||
|
||||
| 目标 | VideoDB 方法 |
|
||||
|------|------------------|
|
||||
| 在视频中查找片段 | 索引语音/场景 → 搜索 → 汇编剪辑 |
|
||||
| 创建精彩集锦 | 搜索多个主题 → 构建时间线 → 生成流 |
|
||||
| 添加字幕 | 索引语音 → 添加字幕叠加层 |
|
||||
| 录制屏幕 + AI | 开始录制 → 运行 AI 流水线 → 导出视频 |
|
||||
| 监控直播流 | 连接 RTSP → 索引场景 → 创建警报 |
|
||||
| 为社交媒体调整格式 | 调整为目标宽高比 |
|
||||
| 合并剪辑 | 使用多个素材构建时间线 → 生成流 |
|
||||
211
docs/zh-CN/skills/x-api/SKILL.md
Normal file
211
docs/zh-CN/skills/x-api/SKILL.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
name: x-api
|
||||
description: X/Twitter API集成,用于发布推文、线程、读取时间线、搜索和分析。涵盖OAuth认证模式、速率限制和平台原生内容发布。当用户希望以编程方式与X交互时使用。
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# X API
|
||||
|
||||
以编程方式与 X(Twitter)交互,用于发布、读取、搜索和分析。
|
||||
|
||||
## 何时激活
|
||||
|
||||
* 用户希望以编程方式发布推文或帖子串
|
||||
* 从 X 读取时间线、提及或用户数据
|
||||
* 在 X 上搜索内容、趋势或对话
|
||||
* 构建 X 集成或机器人
|
||||
* 分析和参与度跟踪
|
||||
* 用户提及"发布到 X"、"发推"、"X API"或"Twitter API"
|
||||
|
||||
## 认证
|
||||
|
||||
### OAuth 2.0 Bearer 令牌(仅应用)
|
||||
|
||||
最佳适用场景:读取密集型操作、搜索、公开数据。
|
||||
|
||||
```bash
|
||||
# Environment setup
|
||||
export X_BEARER_TOKEN="your-bearer-token"
|
||||
```
|
||||
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
|
||||
bearer = os.environ["X_BEARER_TOKEN"]
|
||||
headers = {"Authorization": f"Bearer {bearer}"}
|
||||
|
||||
# Search recent tweets
|
||||
resp = requests.get(
|
||||
"https://api.x.com/2/tweets/search/recent",
|
||||
headers=headers,
|
||||
params={"query": "claude code", "max_results": 10}
|
||||
)
|
||||
tweets = resp.json()
|
||||
```
|
||||
|
||||
### OAuth 1.0a(用户上下文)
|
||||
|
||||
必需用于:发布推文、管理账户、私信。
|
||||
|
||||
```bash
|
||||
# Environment setup — source before use
|
||||
export X_API_KEY="your-api-key"
|
||||
export X_API_SECRET="your-api-secret"
|
||||
export X_ACCESS_TOKEN="your-access-token"
|
||||
export X_ACCESS_SECRET="your-access-secret"
|
||||
```
|
||||
|
||||
```python
|
||||
import os
|
||||
from requests_oauthlib import OAuth1Session
|
||||
|
||||
oauth = OAuth1Session(
|
||||
os.environ["X_API_KEY"],
|
||||
client_secret=os.environ["X_API_SECRET"],
|
||||
resource_owner_key=os.environ["X_ACCESS_TOKEN"],
|
||||
resource_owner_secret=os.environ["X_ACCESS_SECRET"],
|
||||
)
|
||||
```
|
||||
|
||||
## 核心操作
|
||||
|
||||
### 发布一条推文
|
||||
|
||||
```python
|
||||
resp = oauth.post(
|
||||
"https://api.x.com/2/tweets",
|
||||
json={"text": "Hello from Claude Code"}
|
||||
)
|
||||
resp.raise_for_status()
|
||||
tweet_id = resp.json()["data"]["id"]
|
||||
```
|
||||
|
||||
### 发布一个帖子串
|
||||
|
||||
```python
|
||||
def post_thread(oauth, tweets: list[str]) -> list[str]:
|
||||
ids = []
|
||||
reply_to = None
|
||||
for text in tweets:
|
||||
payload = {"text": text}
|
||||
if reply_to:
|
||||
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
|
||||
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
|
||||
resp.raise_for_status()
|
||||
tweet_id = resp.json()["data"]["id"]
|
||||
ids.append(tweet_id)
|
||||
reply_to = tweet_id
|
||||
return ids
|
||||
```
|
||||
|
||||
### 读取用户时间线
|
||||
|
||||
```python
|
||||
resp = requests.get(
|
||||
f"https://api.x.com/2/users/{user_id}/tweets",
|
||||
headers=headers,
|
||||
params={
|
||||
"max_results": 10,
|
||||
"tweet.fields": "created_at,public_metrics",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 搜索推文
|
||||
|
||||
```python
|
||||
resp = requests.get(
|
||||
"https://api.x.com/2/tweets/search/recent",
|
||||
headers=headers,
|
||||
params={
|
||||
"query": "from:affaanmustafa -is:retweet",
|
||||
"max_results": 10,
|
||||
"tweet.fields": "public_metrics,created_at",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 通过用户名获取用户
|
||||
|
||||
```python
|
||||
resp = requests.get(
|
||||
"https://api.x.com/2/users/by/username/affaanmustafa",
|
||||
headers=headers,
|
||||
params={"user.fields": "public_metrics,description,created_at"}
|
||||
)
|
||||
```
|
||||
|
||||
### 上传媒体并发布
|
||||
|
||||
```python
|
||||
# Media upload uses v1.1 endpoint
|
||||
|
||||
# Step 1: Upload media
|
||||
media_resp = oauth.post(
|
||||
"https://upload.twitter.com/1.1/media/upload.json",
|
||||
files={"media": open("image.png", "rb")}
|
||||
)
|
||||
media_id = media_resp.json()["media_id_string"]
|
||||
|
||||
# Step 2: Post with media
|
||||
resp = oauth.post(
|
||||
"https://api.x.com/2/tweets",
|
||||
json={"text": "Check this out", "media": {"media_ids": [media_id]}}
|
||||
)
|
||||
```
|
||||
|
||||
## 速率限制
|
||||
|
||||
X API 的速率限制因端点、认证方法和账户等级而异,并且会随时间变化。请始终:
|
||||
|
||||
* 在硬编码假设之前,查看当前的 X 开发者文档
|
||||
* 在运行时读取 `x-rate-limit-remaining` 和 `x-rate-limit-reset` 头部信息
|
||||
* 自动退避,而不是依赖代码中的静态表格
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
|
||||
if remaining < 5:
|
||||
reset = int(resp.headers.get("x-rate-limit-reset", 0))
|
||||
wait = max(0, reset - int(time.time()))
|
||||
print(f"Rate limit approaching. Resets in {wait}s")
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```python
|
||||
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
|
||||
if resp.status_code == 201:
|
||||
return resp.json()["data"]["id"]
|
||||
elif resp.status_code == 429:
|
||||
reset = int(resp.headers["x-rate-limit-reset"])
|
||||
raise Exception(f"Rate limited. Resets at {reset}")
|
||||
elif resp.status_code == 403:
|
||||
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
|
||||
else:
|
||||
raise Exception(f"X API error {resp.status_code}: {resp.text}")
|
||||
```
|
||||
|
||||
## 安全性
|
||||
|
||||
* **切勿硬编码令牌。** 使用环境变量或 `.env` 文件。
|
||||
* **切勿提交 `.env` 文件。** 将其添加到 `.gitignore`。
|
||||
* **如果令牌暴露,请轮换令牌。** 在 developer.x.com 重新生成。
|
||||
* **当不需要写权限时,使用只读令牌。**
|
||||
* **安全存储 OAuth 密钥** — 不要存储在源代码或日志中。
|
||||
|
||||
## 与内容引擎集成
|
||||
|
||||
使用 `content-engine` 技能生成平台原生内容,然后通过 X API 发布:
|
||||
|
||||
1. 使用内容引擎生成内容(X 平台格式)
|
||||
2. 验证长度(单条推文 280 字符)
|
||||
3. 使用上述模式通过 X API 发布
|
||||
4. 通过 public\_metrics 跟踪参与度
|
||||
|
||||
## 相关技能
|
||||
|
||||
* `content-engine` — 为 X 生成平台原生内容
|
||||
* `crosspost` — 在 X、LinkedIn 和其他平台分发内容
|
||||
@@ -130,7 +130,7 @@ alias claude-research='claude --system-prompt "$(cat ~/.claude/contexts/research
|
||||
**定价参考:**
|
||||
|
||||

|
||||
*来源:<https://platform.claude.com/docs/en/about-claude/pricing>*
|
||||
*来源: <https://platform.claude.com/docs/en/about-claude/pricing>*
|
||||
|
||||
**工具特定优化:**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user