{"owner":"yansongda","repo":"pay","hasSkills":true,"totalSkillsCount":5,"totalTokensCount":11231,"categories":["anthropic-skill","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/releasing-php-package/SKILL.md",".agents/skills/dev-guide/SKILL.md",".agents/skills/pr-review-provider/SKILL.md",".agents/skills/container-dev/SKILL.md","AGENTS.md"],"skills":{".agents/skills/releasing-php-package/SKILL.md":"---\nname: releasing-php-package\ndescription: Use when preparing to publish a new version of a PHP Composer package and need to write or update CHANGELOG, upgrade guides, and documentation before tagging and releasing\n---\n\n# 发布 PHP Composer 包\n\n## 概述\n\n用于发布 PHP Composer 包的结构化流程，避免 CHANGELOG 格式混乱、升级指南缺失、文档示例过期等常见问题，并强制要求在打 tag 前通过独立分支 + PR 完成文档改动。\n\n**硬性约束（违反即流程无效）：**\n- Agent 只负责创建 PR，**严禁自行合并 PR**（包括 `gh pr merge`、平台合并 API 等任何形式的自动合并）\n- PR 必须由人工审核后合并；在获得人工「PR 已合并」确认前，Agent 不得打 tag、不得创建 Release\n\n## 使用场景\n\n- 准备发布新版本（patch/minor/major/beta/rc）\n- CHANGELOG 使用了非标准分类或前缀\n- 目标版本还没有对应的升级指南\n- 文档中包含已移除或已弃用功能的示例\n- 上次发布曾因文档问题被反馈\n\n**不要使用：**\n- 项目已有自动化发布工具（semantic-release、release-please）\n- 仅单文件改动且对用户无可见影响\n\n## 版本分类\n\n| 变更类型 | 版本号变化 | 是否需要升级指南 |\n|---|---|---|\n| 仅 Bug 修复 | Patch（1.0.X） | 否 |\n| 新功能，向后兼容 | Minor（1.X.0） | 是 |\n| 破坏性变更 | Major（X.0.0） | 是 |\n| 预发布（beta/rc/alpha） | 预发布版本（1.0.0-beta.1） | 是 |\n\n## 核心模式\n\n### 错误示例\n\n```markdown\n## v1.2.0 - 2024-01-01\n\n### BREAKING CHANGES\n- change: PHP version bumped to 8.2\n\n### Added\n- feat: add new provider support (#42)\n- feat: add new config system (#43)\n\n### Changed\n- fix: correct error code (#45)\n- chore: update dependencies (#46)\n```\n\n### 正确示例（符合 Keep a Changelog）\n\n```markdown\n## [v1.2.0] - 2024-01-01\n\n### Added\n- New payment provider support (#42)\n- Typed configuration objects replacing arrays (#43)\n\n### Changed\n- Minimum PHP version from 8.0 to 8.2 (#41)\n- All internal plugins migrated to trait-based architecture (#44)\n\n### Removed\n- Legacy helper functions file, migrated to static methods (#44)\n- Deprecated `getOldConfig()` functions, use `getProviderConfig()` (#41)\n```\n\n## 速查表\n\n| 任务 | 命令 / 文件 | 关键规则 |\n|---|---|---|\n| 查看当前分支 | `git branch --show-current` | 禁止在 `master`/`main` 上直接修改 CHANGELOG |\n| 创建发布分支 | `git checkout -b docs/changelog-vX.Y.Z` | 所有文档改动必须走 PR |\n| 列出自上一个 tag 以来的提交 | `git log --oneline --no-merges $(git describe --tags --abbrev=0)..HEAD` | 排除 merge commit |\n| 标准分类 | `Added` → `Changed` → `Deprecated` → `Removed` → `Fixed` → `Security` | 严格顺序，不新增分类 |\n| 去除前缀 | 移除 `feat:`、`fix:`、`refactor:`、`chore:` | 仅当存在时才移除；已干净的中文描述保持原样 |\n| 升级指南 | `docs/v{major}/upgrade/v{version}.md` 或实际等价路径 | 先探测真实 docs 路径 |\n| 更新导航 | 侧边栏 / 菜单配置文件 | 若 `.gitignore` 忽略 docs 目录，使用 `git add -f` |\n| 合并 PR | 由人工在平台审核后执行 | Agent 严禁自动合并（`gh pr merge` 等一律禁止） |\n| 创建 annotated tag | `git tag -a vX.Y.Z -m \"release: vX.Y.Z\"` | 必须在 PR 由人工合并后执行 |\n| 创建 GitHub Release | `gh release create vX.Y.Z --notes-file release-notes.md --prerelease` | beta/rc/alpha 需标记为 pre-release |\n\n## 执行步骤\n\n### 0. 分支隔离（关键）\n\n**在修改任何发布相关文件前，必须先创建独立分支。**\n\n```bash\n# 先确认当前分支\ngit branch --show-current\n\n# 如果在 master/main 上，立即切出发布分支\ngit checkout -b docs/changelog-vX.Y.Z\n```\n\n**禁止**在 `master`/`main` 上直接提交 CHANGELOG 或升级指南改动。\n\n### 1. 分析提交\n\n```bash\nlatest=$(git describe --tags --abbrev=0)\ngit log --oneline --no-merges ${latest}..HEAD\n```\n\n重点关注：PR 编号、破坏性变更、新功能、已弃用 / 已移除项。\n\n**仅文档发布判断：** 如果自上一个 tag 以来的代码变更已经在目标分支上，而你只是整理文档，则按「仅文档发布」处理。流程相同，但提交范围只包含 CHANGELOG / 升级指南 / 导航。\n\n**关键验证步骤：**\n分析完提交后，务必验证实际代码状态，避免把「新增后又移除」的功能写进发布说明：\n\n```bash\n# 验证文件增删\ngit diff ${latest}..HEAD --stat\n\n# 验证 src/ 下实际代码变更\ngit diff ${latest}..HEAD -- src/\n```\n\n**验证规则：**\n1. 对于 commit 消息中的「新增」项：\n   - 确认文件 / 类 / 方法在 HEAD 中仍然存在\n   - 如果后续 commit 已移除，则从发布说明中排除\n   - 示例：#1157 新增 NetworkException，但 #1161 又移除了 → 不要写进 Added\n\n2. 对于 commit 消息中的「移除」项：\n   - 确认文件 / 类 / 方法在 HEAD 中已不存在\n   - 如果仍然存在，则排除或改为「已弃用」\n\n3. 对于 commit 消息中的「变更」项：\n   - 确认变更在 HEAD 中真实存在\n   - 如果已被 revert，则从发布说明中排除\n\n**验证命令：**\n```bash\n# 确认文件存在于 HEAD\ngit show HEAD:path/to/file\n\n# 确认文件变更\ngit diff ${latest}..HEAD -- path/to/file\n```\n\n### 2. 规范化 CHANGELOG\n\n**分类顺序（严格）**：Added、Changed、Deprecated、Removed、Fixed、Security。\n\n**规则：**\n- 仅当存在 `feat:` / `fix:` / `refactor:` / `chore:` 等前缀时才移除\n- 不要重写已经是干净中文描述的条目\n- 将 `BREAKING CHANGES` 内容移入 `Changed` 或 `Removed`\n- 合并重复条目（如同一文件被删除两次）\n- 保留 PR 引用 `(#1234)` 以便追溯\n- 子列表缩进 2 个空格\n\n**版本标题格式：**\n```markdown\n## [v1.2.0] - 2024-01-01\n```\n\n### 3. 编写升级指南\n\n**先探测真实 docs 路径。** 常见位置：\n- `docs/v{major}/upgrade/v{version}.md`\n- `web/docs/v{major}/upgrade/v{version}.md`\n- `src/docs/v{major}/upgrade/v{version}.md`\n\n如果目标升级指南已存在（例如后续 beta 版本），则在原有基础上追加新内容，不要整篇替换。\n\n**章节结构：**\n1. `## 重点检查`\n   - `### 运行环境` — PHP 版本、扩展、其他运行时要求\n   - `### 简单使用者` — **仅 end-user 可见变更**（命名空间变化、移除的方法、配置调整）。不要罗列内部重构（内部辅助函数重组、不影响公共 API 的基类变化、代码组织调整）。\n   - `### 自有插件开发者` — 影响自定义插件或扩展的内部架构变更（新基类、方法签名变化、移除的内部工具）。\n2. `## 更改版本号` — `composer require vendor/package:~{version}`\n3. `### BREAKING CHANGES`（如有）— 完整破坏性变更列表\n\n### 4. 清理过期示例\n\n搜索文档中已移除或已弃用功能的使用：\n```bash\ngrep -rn \"deprecatedMethod\\|removedClass\\|oldNamespace\" docs/\n```\n\n**规则：** 删除已移除功能的示例。不要保留「兼容两种写法」的双示例。文档只反映当前版本。\n\n### 5. 更新导航\n\n将新升级指南链接加入文档导航配置（如侧边栏、菜单、索引文件）。\n\n**注意：** 如果 `.gitignore` 包含 docs 目录匹配规则，新文件需用 `git add -f` 强制添加。\n\n### 6. PR → Tag → Release\n\n**创建 PR：**\n```bash\ngit add -f CHANGELOG.md docs/  # 或 web/docs/，按实际路径调整\n\ngit commit -m \"docs: update CHANGELOG and upgrade guide for vX.Y.Z\"\n\ngit push -u origin docs/changelog-vX.Y.Z\n\n# 通过 GitHub / GitLab / Gitea 等平台创建 PR\n```\n\n**⛔ 流程暂停点：创建 PR 后必须停止，等待人工审核合并**\n\n- 告知用户 PR 链接，明确说明等待人工审核合并，然后结束当前步骤\n- **严禁**执行 `gh pr merge`（含 --squash/--merge/--rebase）、平台合并 API 等任何自动合并操作\n- 后续步骤（push tag、创建 Release）即使已获授权，也不包含 PR 合并权；合并永远由人工执行\n\n**人工合并后（继续前必须确认 PR 已由人工合并）：**\n```bash\n# 核实 PR 状态（state 应为 MERGED，mergedBy 应为人工账号）\ngh pr view <PR编号> --json state,mergedAt,mergedBy\ngit fetch origin\ngit log --oneline origin/master | head -5\n\n# 打 tag 并推送\ngit checkout master && git pull origin master\ngit tag -a vX.Y.Z -m \"release: vX.Y.Z\"\ngit push origin vX.Y.Z\n```\n\n**创建 Release（以 GitHub 为例）：**\n```bash\n# 从 CHANGELOG 提取当前版本段落\nsed -n '/^## vX.Y.Z/,/^## v/p' CHANGELOG.md | sed '$d' > release-notes.md\n\n# beta/rc/alpha 版本需添加 --prerelease\ngh release create vX.Y.Z --title \"vX.Y.Z\" --notes-file release-notes.md --prerelease\n```\n\n## 常见错误\n\n| 错误 | 原因 | 修正 |\n|---|---|---|\n| 直接在 master 提交 CHANGELOG | 跳过分支隔离 | 必须先创建 `docs/changelog-vX.Y.Z` 分支 |\n| CHANGELOG 保留 `feat:`/`fix:` 前缀 | 从 commit 消息复制粘贴 | 仅在有前缀时剥离；保持已有干净描述 |\n| 内部重构写入「简单使用者」 | 未区分用户可见变更与内部变更 | 移到「自有插件开发者」或直接删除 |\n| 文档保留新旧双示例 | 试图在文档中保持向后兼容 | 删除旧示例；文档只反映当前版本 |\n| 忘记更新导航 | 创建升级指南后未注册 | 立即加入侧边栏 / 菜单 |\n| PR 未合并就创建 tag | 急于完成发布 | 先确认 `origin/master` 包含合并提交 |\n| 使用 lightweight tag | `git tag` 缺少 `-a` | 始终使用 `git tag -a` 并附带 message |\n| Release notes 与 CHANGELOG 不一致 | 单独编写 release notes | 从 CHANGELOG 提取或复制 |\n| 预发布版本未标记 pre-release | 忘记 beta/rc/alpha 后缀 | 按版本后缀在发布平台勾选 pre-release |\n| 写入「新增后又移除」的功能 | 仅依赖 commit 消息未验证代码 | 用 `git diff` 确认功能在 HEAD 中真实存在 |\n| Agent 自行合并 PR | 误将「发布流程授权」当作「合并授权」 | 如需撤销：`git revert <merge-commit>`；此后一律仅创建 PR 并等待人工合并 |\n",".agents/skills/dev-guide/SKILL.md":"---\nname: dev-guide\ndescription: Use when developing, testing, or adding new providers to yansongda/pay project. Covers commands, code standards, and provider implementation guide.\n---\n\n# yansongda/pay 开发指南\n\n## 开发命令\n\n**对 `src/` PHP 代码的任何修改，需运行以下三项检查：**\n\n```bash\ncomposer cs-fix && composer analyse && composer test\n```\n\n### 测试\n```bash\ncomposer test       # PHPUnit 11.x + Mockery 1.6\n```\n- 测试前需安装 `hyperf/pimple`\n- Mock HTTP 客户端避免真实请求\n\n### 代码风格\n```bash\ncomposer cs-fix     # 代码格式化检查（dry-run）\n```\n\n### 静态分析\n```bash\ncomposer analyse    # PHPStan\n```\n\n### 文档\n```bash\ncd web && pnpm web:dev    # 文档开发\ncd web && pnpm web:build  # 文档构建\ncd web && pnpm web:serve  # 本地预览\n```\n\n### 本地开发环境\n优先本地 PHP 环境；若无 PHP，使用 Container 作为备选，详见 `container-dev` Skill。\n\n### CI 矩阵\nPHP 8.2-8.5 + Laravel/Hyperf/Default\n\n---\n\n## 代码规范\n\n- `declare(strict_types=1);` 必须\n- `use` 导入，禁止 `\\Yansongda\\Pay\\...`\n- 日志/异常消息用中文\n- 命名：`{Action}Plugin.php`、`{Action}Shortcut.php`、`{Provider}Trait.php`\n\n---\n\n## 新增 Provider 流程\n\n### 6 阶段\n1. **配置**：`src/Config/{Provider}Config.php`\n2. **核心类**：`src/Provider/{Provider}.php` + `src/Service/{Provider}ServiceProvider.php`（继承 AbstractServiceProvider）\n3. **插件**：`src/Plugin/{Provider}/V{n}/`（含 CallbackPlugin）\n4. **快捷方式**：`src/Shortcut/{Provider}/{Action}Shortcut.php`\n5. **Trait**：`src/Traits/{Provider}Trait.php`（`get{Provider}Url`、`verify{Provider}Sign`）\n6. **注册**：`Pay.php` + `Config.php` 映射 + `Exception.php` 常量 + 测试 + 文档\n\n---\n\n## 常见错误\n\n- 忽略 `declare(strict_types=1);`\n- 直接写完整命名空间\n- 回调未验签\n- 测试未 Mock HTTP 客户端\n",".agents/skills/pr-review-provider/SKILL.md":"---\nname: pr-review-provider\ndescription: \"Use when reviewing PRs that add or modify a payment Provider in yansongda/pay - covers plugin pipeline, multi-tenant safety, signature verification, docs, and naming conventions.\"\n---\n\n# PR Review Checklist — Payment Provider\n\nyansongda/pay 新增/修改 Provider 时的 Code Review 专用检查清单。基于 Airwallex PR #1140 review 经验沉淀。\n\n## Review 规范（强制）\n\n### 1. 提交评论前必须征求用户许可\n\n**绝对禁止**：未经用户明确允许，不得使用 `gh pr comment` 或其他方式向 PR 提交任何评论。\n\n**正确流程**：\n1. 完成 review 分析\n2. 向用户展示 review 报告内容\n3. 询问用户：\"是否需要我将此报告提交为 PR 评论？\"\n4. 仅在用户明确确认后才执行提交\n\n### 2. 报告末尾必须注明使用的模型及审核状态\n\n在 review 报告的结论部分之后，必须添加以下信息：\n\n```markdown\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n```\n\n示例：\n```markdown\n---\n*Review by deepseek-v4-pro | 2026-05-07 | 经人工审核确认*\n```\n\n---\n\n## Review 流程\n\n按以下阶段顺序审查，确保覆盖完整：\n\n1. **Phase 1**: Provider 结构完整性\n2. **Phase 2**: 代码规范检查\n3. **Phase 3**: 安全性检查（签名验证、加密解密）\n4. **Phase 4**: 架构一致性（管道、Trait、Event）\n5. **Phase 5**: 官方文档对照\n6. **Phase 6**: 测试覆盖\n7. **Phase 7**: 文档完整性\n\n---\n\n## Phase 1: Provider 结构完整性\n\n对照以下清单逐项检查：\n\n| # | 检查项 | 位置 | 说明 |\n|---|--------|------|------|\n| 1 | 插件 | `src/Plugin/{Provider}/V{n}/` | 按版本组织 |\n| 2 | Provider 类 | `src/Provider/{Provider}.php` | 实现 `ProviderInterface` |\n| 3 | 服务提供者 | `src/Service/{Provider}ServiceProvider.php` | 服务注册 |\n| 4 | 快捷方式 | `src/Shortcut/{Provider}/` | `{Method}Shortcut.php` |\n| 5 | Trait 方法 | `src/Traits/{Provider}Trait.php` | `get{Provider}Url`、`verify{Provider}WebhookSign` 等 |\n| 6 | Provider 注册 | `src/Pay.php` | 添加 `{Provider}::class` 和入口方法 |\n| 7 | 异常常量 | `src/Exception/Exception.php` | `PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*` |\n| 8 | 测试 | `tests/` | 与源码结构对应 |\n| 9 | 文档 | `web/docs/v3/{provider}/` | VitePress 文档 |\n| 10 | 侧边栏/CHANGELOG | `web/.vitepress/sidebar/v3.js`、`CHANGELOG.md` | 更新配置 |\n\n**注意**：`src/Functions.php` 已不存在，URL/签名等方法已下沉到 `src/Traits/*Trait.php`。\n\n---\n\n## Phase 2: 代码规范检查\n\n### 基本规范\n\n| 检查项 | 要求 |\n|--------|------|\n| `declare(strict_types=1)` | 每个文件必须有 |\n| `use` 导入 | 除动态类名字符串/反射场景外，禁止直接写完整命名空间（如 `\\Yansongda\\Pay\\...`） |\n| 多行条件 | `&&` / `||` 放在续行开头 |\n\n### 命名规范\n\n| 类型 | 格式 | 示例 |\n|------|------|------|\n| 插件 | `{Action}Plugin.php` | `PayPlugin`、`RefundPlugin` |\n| 快捷方式 | `{Method}Shortcut.php` | `WebShortcut`、`QueryShortcut` |\n| Provider | `{ProviderName}.php` | `Paypal.php`、`Stripe.php` |\n| ServiceProvider | `{ProviderName}ServiceProvider.php` | `PaypalServiceProvider.php` |\n| Trait | `{Provider}Trait.php` | `WechatTrait`、`StripeTrait` |\n| 命名空间 | `Yansongda\\Pay\\Plugin\\{Provider}\\V{n}\\Pay\\{Plugin}` | 版本号与 API 版本一致 |\n\n### 日志与异常\n\n- 日志格式：`[Provider][V{n}][Category][Plugin]`，使用中文消息\n- 异常常量：`PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*`\n- 异常消息：中文，附带上下文参数\n\n---\n\n## Phase 3: 安全性检查\n\n### 1. params vs payload 混淆\n\n**高危**：`Artful::artful()` 第二参数必须是 `params`（含 `_config`），不能是 `payload`。\n\n```php\n// ❌ 错误 — payload 不含 _config，多租户必崩\n$result = Artful::artful([...], $confirmPayload);\n\n// ✅ 正确 — params 含 _config 租户标识\n$result = Artful::artful([...], $confirmParams);\n```\n\n**检查方法**：搜索 `Artful::artful(` 调用，追踪第二参数来源。\n\n### 2. 空值处理\n\n检查最终发送的 body/query 是否包含不应出现的 `null`/空字段。\n\n**推荐方式**：\n- 优先使用 `filter_params()` 函数（artful 库提供）\n- 嵌套数组场景补充 `array_filter()`\n\n```php\n// ✅ 优先方式 — filter_params\n$body = http_build_query(filter_params($payload)->toArray());\n\n// ✅ 嵌套场景 — array_filter\n$rocket->mergePayload(array_filter([\n    'application_context' => $payload->get('application_context'),\n], static fn ($value) => !is_null($value)));\n```\n\n**检查位置**：`AddRadarPlugin::getBody()`、`getQueryString()`、业务 Plugin 的 `mergePayload()`。\n\n### 3. Webhook 签名验证\n\n**安全强制**：所有 CallbackPlugin 必须验签。\n\n| Provider | Trait 方法 | 必需配置字段 |\n|----------|------------|--------------|\n| Stripe | `StripeTrait::verifyStripeWebhookSign()` | `webhook_secret` |\n| PayPal | `PaypalTrait::verifyPaypalWebhookSign()` | `webhook_id` + OAuth |\n| 微信 | `WechatTrait::verifyWechatSign()` | `mch_secret_cert`、`wechat_public_cert_path`（可预置或运行时拉取） |\n| 抖音 | —（在 `CallbackPlugin::verifySign()` 中实现） | `mch_secret_token`、`mch_secret_salt` |\n| 银联 | `UnipayTrait::verifyUnipaySign()` | `unipay_public_cert_path` |\n| 支付宝 | `AlipayTrait::verifyAlipaySign()` | `alipay_public_cert_path` |\n\n**检查点**：\n- CallbackPlugin 是否调用 Trait 提供或 Plugin 自身实现的签名验证方法\n- 签名算法是否与官方文档一致（对照 `@see` 链接）\n- 配置缺失时抛异常\n- 签名为空时抛异常\n- 使用 `hash_equals` 防时序攻击\n\n### 4. 数组回调的处理\n\n**仅适用于 Stripe/Wechat/Paypal**（构造 `ServerRequest` 并验签）：\n\n`getCallbackParams()` 处理逻辑：\n\n| 输入类型 | 行为 |\n|----------|------|\n| `['body' => ..., 'headers' => ...]` | 构造带 headers 的 `ServerRequest`，**会验签** |\n| 纯数组（无 headers） | 构造无 headers 的 `ServerRequest`，验签时会抛 `SIGN_EMPTY` |\n| `ServerRequestInterface` | 直接使用，验签 |\n| `null` | 从 `ServerRequest::fromGlobals()` 获取 |\n\n**结论**：数组回调只有提供完整 `headers` + `body` 才能通过验签；否则验签阶段抛异常。\n\n**Alipay/Douyin/Unipay 不同**：\n- `getCallbackParams()` 返回 `Collection`（从 query/parsedBody 取值）\n- 直接 merge 到 params，不构造 `ServerRequest`\n- CallbackPlugin 从 params 中取值验签\n\n### 5. 加密资源解密（微信）\n\n微信回调的 `resource` 字段需解密：\n\n```php\n$body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config);\n```\n\n检查 `CallbackPlugin` 是否调用此方法。\n\n---\n\n## Phase 4: 架构一致性\n\n### 1. 插件管道骨架\n\n**通用骨架**：\n```\nStartPlugin → [前置插件] → 业务插件 → [后置插件] → ParserPlugin\n```\n\n**各 Provider 差异**：\n\n| Provider | 前置插件 | 后置插件 |\n|----------|----------|----------|\n| Stripe | 无 | `AddRadarPlugin` → `ResponsePlugin` |\n| PayPal | `ObtainAccessTokenPlugin` | `AddPayloadBodyPlugin` → `AddRadarPlugin` → `ResponsePlugin` |\n| 微信 | 无 | `AddPayloadBodyPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n| 支付宝 | 无 | `FormatPayloadBizContentPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n\n**注意**：不同 Provider 管道差异较大，不要按固定模板审查。\n\n### 2. mergeCommonPlugins 实现\n\nProvider 类必须实现此方法，返回完整管道：\n\n```php\npublic function mergeCommonPlugins(array $plugins): array\n{\n    return array_merge(\n        [StartPlugin::class, /* 前置插件 */],\n        $plugins,\n        [/* 后置插件 */, ParserPlugin::class],\n    );\n}\n```\n\n### 3. Trait 方法复用\n\n新增 Provider 应复用 Trait 方法而非重新实现：\n\n| 功能 | Trait 方法 |\n|------|------------|\n| URL 构建 | `get{Provider}Url()` |\n| 签名验证 | `verify{Provider}WebhookSign()` / `verify{Provider}Sign()` |\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()` |\n| 加密解密 | `WechatTrait::decryptWechatResource()` |\n\n### 4. Provider 常量定义\n\n必须定义 `URL` 常量：\n\n```php\npublic const URL = [\n    PayMode::NORMAL->value => 'https://api.xxx.com/',\n    PayMode::SANDBOX->value => 'https://sandbox.api.xxx.com/',\n    PayMode::SERVICE->value => 'https://api.xxx.com/',\n];\n```\n\n特殊常量（如微信）：\n- `AUTH_TAG_LENGTH_BYTE`\n- `MCH_SECRET_KEY_LENGTH_BYTE`\n\n### 5. Event 调用\n\ncallback 方法必须触发事件：\n\n```php\n// Stripe/Wechat/Paypal（ServerRequestInterface）\nEvent::dispatch(new CallbackReceived('provider', clone $request, $params, null));\n\n// Alipay/Douyin/Unipay/Jsb（Collection）\nEvent::dispatch(new CallbackReceived('provider', $request->all(), $params, null));\n```\n\n其他方法触发：\n\n```php\nEvent::dispatch(new MethodCalled('provider', __METHOD__, $order, null));\n```\n\n### 6. PHPStan ignore 注释\n\nTrait 静态方法调用需添加注释：\n\n```php\n/* @phpstan-ignore-next-line */\nself::verifyWechatSign(...);\n```\n\n这是 PHPStan 对 Trait 静态调用的已知限制，非代码质量问题。\n\n---\n\n## Phase 5: 官方文档对照\n\n结合代码中的 `@see` 链接验证：\n\n| # | 检查点 |\n|---|--------|\n| 1 | API 端点 URL 是否与官方一致 |\n| 2 | HTTP 方法（GET/POST）是否正确 |\n| 3 | 认证 Header 名称、格式是否正确 |\n| 4 | 请求/响应字段（必填/可选）是否正确 |\n| 5 | 签名算法、拼接顺序是否与官方完全一致 |\n| 6 | Base URL（production/sandbox）是否正确 |\n\n---\n\n## Phase 6: 测试覆盖\n\n| # | 检查项 |\n|---|--------|\n| 1 | 每个 Plugin 有对应测试 |\n| 2 | 必填参数缺失的异常测试 |\n| 3 | 可选参数缺失的边界测试 |\n| 4 | 多租户场景（`_config` 参数） |\n| 5 | HTTP client mock，禁止真实 API 调用 |\n| 6 | Callback 签名验证的正向/反向测试 |\n\n---\n\n## Phase 7: 文档完整性\n\n| # | 检查项 |\n|---|--------|\n| 1 | 官方链接可访问且指向正确端点 |\n| 2 | 示例代码使用原生 PHP，框架无关 |\n| 3 | 侧边栏已更新 |\n| 4 | CHANGELOG 已更新 |\n\n---\n\n## 常见误报\n\n### null-safe 操作符\n\n```php\n$payload?->get('a', $payload->get('b'))\n```\n\nPHP 8.0 的 null-safe 实现了 **full short-circuiting**：当 `$payload` 为 `null` 时，右侧参数不会被求值。这不是 bug。\n\n参考：[PHP RFC nullsafe_operator](https://wiki.php.net/rfc/nullsafe_operator)\n\n---\n\n## 避免重复造轮子\n\n检查新增功能是否在 `yansongda/supports` 或 `yansongda/artful` 中已有实现：\n\n| 功能 | 已有实现 |\n|------|----------|\n| UUID 生成 | `Str::uuidV4()` |\n| 字符串处理 | `Str::*` |\n| 集合操作 | `Collection::*` |\n| 空值过滤 | `filter_params()` (artful) |\n| HTTP 方法获取 | `get_radar_method()` (artful) |\n| Body 获取 | `get_radar_body()` (artful) |\n\n\n\n## Review 报告模板\n\n以下为提交 review 时的标准报告格式：\n\n````markdown\n# PR #{number} Code Review — {Provider} Provider\n\n## 一、总览\n\n| 维度 | 内容 |\n|------|------|\n| PR | #{number} — {title} |\n| 涉及文件 | {n} 个 |\n| Review 范围 | 全量代码 + 文档 + 测试 |\n| 方法 | 逐文件审查 + 官方文档对照 + 跨 Provider 一致性比对 |\n\n## 二、已确认 Bug（含证据链）\n\n### BUG-{n}: {简要描述}\n\n**严重级别**：{高/中高/中/低}\n\n**位置**：`{file_path}` L{line}\n\n**问题**：\n{详细描述问题本质}\n\n**证据链**：\n1. 代码现状：`{相关代码片段}`\n2. 预期行为：{应该怎样}\n3. 实际行为：{实际会怎样}\n4. 影响范围：{哪些场景会触发}\n\n**修复建议**：\n```php\n// 修复方案\n```\n\n---\n\n（重复以上格式，每个 Bug 单独一节）\n\n## 三、已排除的误报\n\n### {误报描述}\n\n**结论**：非 Bug\n\n**原因**：{详细解释，附 RFC/文档链接}\n\n## 四、风险项\n\n| # | 风险 | 严重级别 | 文件 | 说明 |\n|---|------|----------|------|------|\n| RISK-{n} | {风险名} | {级别} | `{file}` | {说明} |\n\n## 五、改进建议\n\n| # | 建议 | 优先级 | 文件 | 说明 |\n|---|------|--------|------|------|\n| SUG-{n} | {建议名} | {级别} | `{file}` | {说明} |\n\n## 六、官方文档对照验证\n\n| 功能 | 代码中的 URL/算法 | 官方文档 | 是否一致 |\n|------|-------------------|----------|----------|\n| {功能名} | `{代码实现}` | [官方文档]({url}) | ✅/❌ |\n\n## 七、代码规范检查\n\n| 检查项 | 状态 | 备注 |\n|--------|------|------|\n| `declare(strict_types=1)` | ✅/❌ | |\n| `use` 导入（无内联命名空间） | ✅/❌ | |\n| 日志格式 `[Provider][V{n}][Category][Plugin]` | ✅/❌ | |\n| 异常常量命名 | ✅/❌ | |\n| Plugin/Shortcut 命名规范 | ✅/❌ | |\n| 测试覆盖 | ✅/❌ | |\n\n## 八、汇总\n\n| 类别 | 数量 | 详情 |\n|------|------|------|\n| 已确认 Bug | {n} | {简要列举} |\n| 已排除误报 | {n} | {简要列举} |\n| 风险项 | {n} | {简要列举} |\n| 改进建议 | {n} | {简要列举} |\n\n## 九、结论\n\n{总体评价：代码质量、架构一致性、安全性等。明确给出 Approve / Request Changes 建议及原因。}\n\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n````\n",".agents/skills/container-dev/SKILL.md":"---\nname: container-dev\ndescription: \"Use when local PHP environment is unavailable. Fallback container-based development environment for yansongda/pay project.\"\n---\n\n# Container-based Local Development (Fallback)\n\n当本地没有 PHP 环境时，使用 Apple Container 或 Docker 作为备选方案。\n\n## 使用原则\n\n**优先本地环境**：先检查本地是否有 PHP/composer，有则直接使用。\n\n**容器作为备选**：仅在本地环境不可用时使用容器。\n\n## 快速参考\n\n### 镜像\n\n```\nregistry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine\n```\n\n### 常用命令\n\n| 任务 | 命令 |\n|------|------|\n| 运行测试 | `composer test` |\n| PHPStan 分析 | `composer analyse` |\n| 代码风格检查 | `composer cs-fix`（**仅检查，不修复**） |\n| 代码风格修复 | `php-cs-fixer fix ./src` |\n| Composer 更新 | `composer update --with-all-dependencies` |\n| 安装依赖 | `composer install` |\n| Web 文档开发 | `pnpm web:dev`（需本地 Node 或 Node 容器） |\n| Web 文档构建 | `pnpm web:build`（需本地 Node 或 Node 容器） |\n\n---\n\n## Apple Container\n\n### 基本模板\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  COMMAND\n```\n\n### DNS 问题处理\n\n若出现 DNS 解析失败（`198.18.0.x` 地址、连接超时），添加 DNS fix：\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMMAND\"\n```\n\n### PHP 命令示例\n\n```bash\n# 测试（需 DNS fix + COMPOSER_ALLOW_SUPERUSER）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer test\"\n\n# PHPStan 分析\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer analyse\"\n\n# 代码风格检查（仅查看差异）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer cs-fix\"\n\n# 代码风格修复\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 php-cs-fixer fix ./src\"\n\n# Composer update\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && composer update --with-all-dependencies\"\n```\n\n### Web 文档命令\n\nWeb 文档使用 pnpm，镜像中无 Node.js，需使用 Node 镜像或本地运行。\n\n**注意**：`web/package.json` 要求 Node `>=20.12.0`，请确保使用的镜像版本满足此约束。\n\n```bash\n# 方案1: 本地运行（推荐）\ncd web && pnpm web:dev\n\n# 方案2: 使用 Node 容器（版本 >=20.12.0）\ndocker run --rm -v \"$(pwd)/web\":/app -w /app \\\n  node:20-alpine \\\n  sh -c \"npm install -g pnpm && pnpm install && pnpm web:dev\"\n```\n\n---\n\n## Docker\n\nDocker 命令类似，通常不需要 DNS fix：\n\n```bash\n# 测试\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer test\n\n# PHPStan 分析\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer analyse\n\n# 代码风格检查\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer cs-fix\n\n# 代码风格修复\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  php-cs-fixer fix ./src\n```\n\n---\n\n## DNS 问题诊断（Apple Container）\n\n**症状**：\n- `curl error 28 Connection timed out`\n- DNS 解析返回 `198.18.0.x`（基准测试保留地址）\n\n**原因**：Container 系统 DNS 解析器异常。\n\n**解决**：手动写入 `nameserver 8.8.8.8` 到 `/etc/resolv.conf`。\n\n---\n\n## 注意事项\n\n- `COMPOSER_ALLOW_SUPERUSER=1`：容器内运行 composer 脚本必须\n- `composer cs-fix`：**仅检查并显示差异**，不自动修复\n- `php-cs-fixer fix ./src`：实际修复代码风格\n- Apple Container 需先启动：`container system start`\n- Docker 需确保 Docker Desktop 或 daemon 已启动\n- 容器内网络可能无法访问某些 CDN，测试中可能出现 PHP warning（不影响核心功能）","AGENTS.md":"# yansongda/pay AGENTS.md\n\n## OVERVIEW\nPHP 支付 SDK，支持支付宝、微信、银联、抖音、江苏银行、PayPal、Stripe。基于插件管道架构，关注支付流程、回调验签和 Provider 扩展一致性。\n\n## STRUCTURE\n```\nsrc/\n├── Config/           # Typed Config（WechatConfig、AlipayConfig 等）\n├── Event/            # PSR-14 事件\n├── Plugin/{Provider}/V{n}/\n├── Provider/         # Alipay、Wechat 等\n├── Shortcut/{Provider}/\n├── Traits/           # {Provider}Trait、ProviderConfigTrait\n├── CertManager.php   # 证书缓存\n└── Pay.php\n```\n\n## 核心组件\n| 符号 | 位置 | 用途 |\n|---|---|---|\n| `Pay::alipay()` | `Pay.php` | 支付入口 |\n| `Pay::wechat()` | `Pay.php` | 微信入口 |\n| `ProviderInterface` | `Contract/` | 核心接口（pay/query/cancel/close/refund/callback/success） |\n| `Rocket` | `Yansongda\\Artful\\Rocket` | 承载 params/payload/radar/destination |\n| `CertManager` | `CertManager.php` | 证书读取与缓存 |\n| `Event` | `Event.php` | PSR-14 事件分发 |\n| `{Provider}Config` | `Config/` | 类型化配置对象 |\n\n## 核心原则\n- **安全第一**：回调/Webhook 必须验签，禁止直接信任传入数据\n- **代码一致性**：新增 Provider 遵循现有结构\n\n## COMMANDS & 开发规范\n详见 `.agents/skills/dev-guide/SKILL.md`，使用时加载。\n\n## 架构要点\n- **插件管道**：`StartPlugin → [前置] → 业务插件 → [后置] → ParserPlugin`\n- **动态调用**：Provider 通过 `__call` 调用 Shortcut，再进入管道\n- **配置系统**：`Config` 类自动将数组配置转为 `{Provider}Config` 对象\n- **事件系统**：`PayListener` 桥接 Artful 事件 → Pay 事件\n\n## 签名验证（必须）\n| Provider | 方式 | Trait 方法 |\n|---|---|---|\n| 支付宝 | RSA 本地验证 | `AlipayTrait::verifyAlipaySign()` |\n| 微信 | 证书签名本地验证 | `WechatTrait::verifyWechatSign()` |\n| PayPal | API 验证 | `PaypalTrait::verifyPaypalWebhookSign()` |\n| Stripe | HMAC-SHA256 本地 | `StripeTrait::verifyStripeWebhookSign()` |\n| 银联 | 证书签名本地验证 | `UnipayTrait::verifyUnipaySign()` |\n| 江苏银行 | RSA 本地验证 | `JsbTrait::verifyJsbSign()` |\n| 抖音 | SHA1 本地验证 | `CallbackPlugin::verifySign()` |\n| 通联支付 | RSA-SHA1 本地验证 | `AllinpayTrait::verifyAllinpaySign()` |\n\n回调处理差异：\n- **Wechat/Stripe/Paypal**：传 `_request`（ServerRequestInterface）到 CallbackPlugin\n- **Alipay/Douyin/Unipay/Jsb/Allinpay**：通过 `getCallbackParams()` 获取 Collection\n\n## Trait 参考\n| 用途 | 方法（示例） |\n|---|---|\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()`、`getRadarUrl()` |\n| URL 构建 | `get{Provider}Url()`（各 Trait） |\n| 签名验证 | `verify{Provider}Sign()`、`verify{Provider}WebhookSign()` |\n| 证书管理 | `CertManager::getPublicCert()`、`getPrivateCert()` |\n| 微信专用 | `decryptWechatResource()`、`reloadWechatPublicCerts()` |\n\n> 完整方法列表见 `src/Traits/{Provider}Trait.php`\n\n"},"files":{".agents/skills/releasing-php-package/SKILL.md":"---\nname: releasing-php-package\ndescription: Use when preparing to publish a new version of a PHP Composer package and need to write or update CHANGELOG, upgrade guides, and documentation before tagging and releasing\n---\n\n# 发布 PHP Composer 包\n\n## 概述\n\n用于发布 PHP Composer 包的结构化流程，避免 CHANGELOG 格式混乱、升级指南缺失、文档示例过期等常见问题，并强制要求在打 tag 前通过独立分支 + PR 完成文档改动。\n\n**硬性约束（违反即流程无效）：**\n- Agent 只负责创建 PR，**严禁自行合并 PR**（包括 `gh pr merge`、平台合并 API 等任何形式的自动合并）\n- PR 必须由人工审核后合并；在获得人工「PR 已合并」确认前，Agent 不得打 tag、不得创建 Release\n\n## 使用场景\n\n- 准备发布新版本（patch/minor/major/beta/rc）\n- CHANGELOG 使用了非标准分类或前缀\n- 目标版本还没有对应的升级指南\n- 文档中包含已移除或已弃用功能的示例\n- 上次发布曾因文档问题被反馈\n\n**不要使用：**\n- 项目已有自动化发布工具（semantic-release、release-please）\n- 仅单文件改动且对用户无可见影响\n\n## 版本分类\n\n| 变更类型 | 版本号变化 | 是否需要升级指南 |\n|---|---|---|\n| 仅 Bug 修复 | Patch（1.0.X） | 否 |\n| 新功能，向后兼容 | Minor（1.X.0） | 是 |\n| 破坏性变更 | Major（X.0.0） | 是 |\n| 预发布（beta/rc/alpha） | 预发布版本（1.0.0-beta.1） | 是 |\n\n## 核心模式\n\n### 错误示例\n\n```markdown\n## v1.2.0 - 2024-01-01\n\n### BREAKING CHANGES\n- change: PHP version bumped to 8.2\n\n### Added\n- feat: add new provider support (#42)\n- feat: add new config system (#43)\n\n### Changed\n- fix: correct error code (#45)\n- chore: update dependencies (#46)\n```\n\n### 正确示例（符合 Keep a Changelog）\n\n```markdown\n## [v1.2.0] - 2024-01-01\n\n### Added\n- New payment provider support (#42)\n- Typed configuration objects replacing arrays (#43)\n\n### Changed\n- Minimum PHP version from 8.0 to 8.2 (#41)\n- All internal plugins migrated to trait-based architecture (#44)\n\n### Removed\n- Legacy helper functions file, migrated to static methods (#44)\n- Deprecated `getOldConfig()` functions, use `getProviderConfig()` (#41)\n```\n\n## 速查表\n\n| 任务 | 命令 / 文件 | 关键规则 |\n|---|---|---|\n| 查看当前分支 | `git branch --show-current` | 禁止在 `master`/`main` 上直接修改 CHANGELOG |\n| 创建发布分支 | `git checkout -b docs/changelog-vX.Y.Z` | 所有文档改动必须走 PR |\n| 列出自上一个 tag 以来的提交 | `git log --oneline --no-merges $(git describe --tags --abbrev=0)..HEAD` | 排除 merge commit |\n| 标准分类 | `Added` → `Changed` → `Deprecated` → `Removed` → `Fixed` → `Security` | 严格顺序，不新增分类 |\n| 去除前缀 | 移除 `feat:`、`fix:`、`refactor:`、`chore:` | 仅当存在时才移除；已干净的中文描述保持原样 |\n| 升级指南 | `docs/v{major}/upgrade/v{version}.md` 或实际等价路径 | 先探测真实 docs 路径 |\n| 更新导航 | 侧边栏 / 菜单配置文件 | 若 `.gitignore` 忽略 docs 目录，使用 `git add -f` |\n| 合并 PR | 由人工在平台审核后执行 | Agent 严禁自动合并（`gh pr merge` 等一律禁止） |\n| 创建 annotated tag | `git tag -a vX.Y.Z -m \"release: vX.Y.Z\"` | 必须在 PR 由人工合并后执行 |\n| 创建 GitHub Release | `gh release create vX.Y.Z --notes-file release-notes.md --prerelease` | beta/rc/alpha 需标记为 pre-release |\n\n## 执行步骤\n\n### 0. 分支隔离（关键）\n\n**在修改任何发布相关文件前，必须先创建独立分支。**\n\n```bash\n# 先确认当前分支\ngit branch --show-current\n\n# 如果在 master/main 上，立即切出发布分支\ngit checkout -b docs/changelog-vX.Y.Z\n```\n\n**禁止**在 `master`/`main` 上直接提交 CHANGELOG 或升级指南改动。\n\n### 1. 分析提交\n\n```bash\nlatest=$(git describe --tags --abbrev=0)\ngit log --oneline --no-merges ${latest}..HEAD\n```\n\n重点关注：PR 编号、破坏性变更、新功能、已弃用 / 已移除项。\n\n**仅文档发布判断：** 如果自上一个 tag 以来的代码变更已经在目标分支上，而你只是整理文档，则按「仅文档发布」处理。流程相同，但提交范围只包含 CHANGELOG / 升级指南 / 导航。\n\n**关键验证步骤：**\n分析完提交后，务必验证实际代码状态，避免把「新增后又移除」的功能写进发布说明：\n\n```bash\n# 验证文件增删\ngit diff ${latest}..HEAD --stat\n\n# 验证 src/ 下实际代码变更\ngit diff ${latest}..HEAD -- src/\n```\n\n**验证规则：**\n1. 对于 commit 消息中的「新增」项：\n   - 确认文件 / 类 / 方法在 HEAD 中仍然存在\n   - 如果后续 commit 已移除，则从发布说明中排除\n   - 示例：#1157 新增 NetworkException，但 #1161 又移除了 → 不要写进 Added\n\n2. 对于 commit 消息中的「移除」项：\n   - 确认文件 / 类 / 方法在 HEAD 中已不存在\n   - 如果仍然存在，则排除或改为「已弃用」\n\n3. 对于 commit 消息中的「变更」项：\n   - 确认变更在 HEAD 中真实存在\n   - 如果已被 revert，则从发布说明中排除\n\n**验证命令：**\n```bash\n# 确认文件存在于 HEAD\ngit show HEAD:path/to/file\n\n# 确认文件变更\ngit diff ${latest}..HEAD -- path/to/file\n```\n\n### 2. 规范化 CHANGELOG\n\n**分类顺序（严格）**：Added、Changed、Deprecated、Removed、Fixed、Security。\n\n**规则：**\n- 仅当存在 `feat:` / `fix:` / `refactor:` / `chore:` 等前缀时才移除\n- 不要重写已经是干净中文描述的条目\n- 将 `BREAKING CHANGES` 内容移入 `Changed` 或 `Removed`\n- 合并重复条目（如同一文件被删除两次）\n- 保留 PR 引用 `(#1234)` 以便追溯\n- 子列表缩进 2 个空格\n\n**版本标题格式：**\n```markdown\n## [v1.2.0] - 2024-01-01\n```\n\n### 3. 编写升级指南\n\n**先探测真实 docs 路径。** 常见位置：\n- `docs/v{major}/upgrade/v{version}.md`\n- `web/docs/v{major}/upgrade/v{version}.md`\n- `src/docs/v{major}/upgrade/v{version}.md`\n\n如果目标升级指南已存在（例如后续 beta 版本），则在原有基础上追加新内容，不要整篇替换。\n\n**章节结构：**\n1. `## 重点检查`\n   - `### 运行环境` — PHP 版本、扩展、其他运行时要求\n   - `### 简单使用者` — **仅 end-user 可见变更**（命名空间变化、移除的方法、配置调整）。不要罗列内部重构（内部辅助函数重组、不影响公共 API 的基类变化、代码组织调整）。\n   - `### 自有插件开发者` — 影响自定义插件或扩展的内部架构变更（新基类、方法签名变化、移除的内部工具）。\n2. `## 更改版本号` — `composer require vendor/package:~{version}`\n3. `### BREAKING CHANGES`（如有）— 完整破坏性变更列表\n\n### 4. 清理过期示例\n\n搜索文档中已移除或已弃用功能的使用：\n```bash\ngrep -rn \"deprecatedMethod\\|removedClass\\|oldNamespace\" docs/\n```\n\n**规则：** 删除已移除功能的示例。不要保留「兼容两种写法」的双示例。文档只反映当前版本。\n\n### 5. 更新导航\n\n将新升级指南链接加入文档导航配置（如侧边栏、菜单、索引文件）。\n\n**注意：** 如果 `.gitignore` 包含 docs 目录匹配规则，新文件需用 `git add -f` 强制添加。\n\n### 6. PR → Tag → Release\n\n**创建 PR：**\n```bash\ngit add -f CHANGELOG.md docs/  # 或 web/docs/，按实际路径调整\n\ngit commit -m \"docs: update CHANGELOG and upgrade guide for vX.Y.Z\"\n\ngit push -u origin docs/changelog-vX.Y.Z\n\n# 通过 GitHub / GitLab / Gitea 等平台创建 PR\n```\n\n**⛔ 流程暂停点：创建 PR 后必须停止，等待人工审核合并**\n\n- 告知用户 PR 链接，明确说明等待人工审核合并，然后结束当前步骤\n- **严禁**执行 `gh pr merge`（含 --squash/--merge/--rebase）、平台合并 API 等任何自动合并操作\n- 后续步骤（push tag、创建 Release）即使已获授权，也不包含 PR 合并权；合并永远由人工执行\n\n**人工合并后（继续前必须确认 PR 已由人工合并）：**\n```bash\n# 核实 PR 状态（state 应为 MERGED，mergedBy 应为人工账号）\ngh pr view <PR编号> --json state,mergedAt,mergedBy\ngit fetch origin\ngit log --oneline origin/master | head -5\n\n# 打 tag 并推送\ngit checkout master && git pull origin master\ngit tag -a vX.Y.Z -m \"release: vX.Y.Z\"\ngit push origin vX.Y.Z\n```\n\n**创建 Release（以 GitHub 为例）：**\n```bash\n# 从 CHANGELOG 提取当前版本段落\nsed -n '/^## vX.Y.Z/,/^## v/p' CHANGELOG.md | sed '$d' > release-notes.md\n\n# beta/rc/alpha 版本需添加 --prerelease\ngh release create vX.Y.Z --title \"vX.Y.Z\" --notes-file release-notes.md --prerelease\n```\n\n## 常见错误\n\n| 错误 | 原因 | 修正 |\n|---|---|---|\n| 直接在 master 提交 CHANGELOG | 跳过分支隔离 | 必须先创建 `docs/changelog-vX.Y.Z` 分支 |\n| CHANGELOG 保留 `feat:`/`fix:` 前缀 | 从 commit 消息复制粘贴 | 仅在有前缀时剥离；保持已有干净描述 |\n| 内部重构写入「简单使用者」 | 未区分用户可见变更与内部变更 | 移到「自有插件开发者」或直接删除 |\n| 文档保留新旧双示例 | 试图在文档中保持向后兼容 | 删除旧示例；文档只反映当前版本 |\n| 忘记更新导航 | 创建升级指南后未注册 | 立即加入侧边栏 / 菜单 |\n| PR 未合并就创建 tag | 急于完成发布 | 先确认 `origin/master` 包含合并提交 |\n| 使用 lightweight tag | `git tag` 缺少 `-a` | 始终使用 `git tag -a` 并附带 message |\n| Release notes 与 CHANGELOG 不一致 | 单独编写 release notes | 从 CHANGELOG 提取或复制 |\n| 预发布版本未标记 pre-release | 忘记 beta/rc/alpha 后缀 | 按版本后缀在发布平台勾选 pre-release |\n| 写入「新增后又移除」的功能 | 仅依赖 commit 消息未验证代码 | 用 `git diff` 确认功能在 HEAD 中真实存在 |\n| Agent 自行合并 PR | 误将「发布流程授权」当作「合并授权」 | 如需撤销：`git revert <merge-commit>`；此后一律仅创建 PR 并等待人工合并 |\n",".agents/skills/dev-guide/SKILL.md":"---\nname: dev-guide\ndescription: Use when developing, testing, or adding new providers to yansongda/pay project. Covers commands, code standards, and provider implementation guide.\n---\n\n# yansongda/pay 开发指南\n\n## 开发命令\n\n**对 `src/` PHP 代码的任何修改，需运行以下三项检查：**\n\n```bash\ncomposer cs-fix && composer analyse && composer test\n```\n\n### 测试\n```bash\ncomposer test       # PHPUnit 11.x + Mockery 1.6\n```\n- 测试前需安装 `hyperf/pimple`\n- Mock HTTP 客户端避免真实请求\n\n### 代码风格\n```bash\ncomposer cs-fix     # 代码格式化检查（dry-run）\n```\n\n### 静态分析\n```bash\ncomposer analyse    # PHPStan\n```\n\n### 文档\n```bash\ncd web && pnpm web:dev    # 文档开发\ncd web && pnpm web:build  # 文档构建\ncd web && pnpm web:serve  # 本地预览\n```\n\n### 本地开发环境\n优先本地 PHP 环境；若无 PHP，使用 Container 作为备选，详见 `container-dev` Skill。\n\n### CI 矩阵\nPHP 8.2-8.5 + Laravel/Hyperf/Default\n\n---\n\n## 代码规范\n\n- `declare(strict_types=1);` 必须\n- `use` 导入，禁止 `\\Yansongda\\Pay\\...`\n- 日志/异常消息用中文\n- 命名：`{Action}Plugin.php`、`{Action}Shortcut.php`、`{Provider}Trait.php`\n\n---\n\n## 新增 Provider 流程\n\n### 6 阶段\n1. **配置**：`src/Config/{Provider}Config.php`\n2. **核心类**：`src/Provider/{Provider}.php` + `src/Service/{Provider}ServiceProvider.php`（继承 AbstractServiceProvider）\n3. **插件**：`src/Plugin/{Provider}/V{n}/`（含 CallbackPlugin）\n4. **快捷方式**：`src/Shortcut/{Provider}/{Action}Shortcut.php`\n5. **Trait**：`src/Traits/{Provider}Trait.php`（`get{Provider}Url`、`verify{Provider}Sign`）\n6. **注册**：`Pay.php` + `Config.php` 映射 + `Exception.php` 常量 + 测试 + 文档\n\n---\n\n## 常见错误\n\n- 忽略 `declare(strict_types=1);`\n- 直接写完整命名空间\n- 回调未验签\n- 测试未 Mock HTTP 客户端\n",".agents/skills/pr-review-provider/SKILL.md":"---\nname: pr-review-provider\ndescription: \"Use when reviewing PRs that add or modify a payment Provider in yansongda/pay - covers plugin pipeline, multi-tenant safety, signature verification, docs, and naming conventions.\"\n---\n\n# PR Review Checklist — Payment Provider\n\nyansongda/pay 新增/修改 Provider 时的 Code Review 专用检查清单。基于 Airwallex PR #1140 review 经验沉淀。\n\n## Review 规范（强制）\n\n### 1. 提交评论前必须征求用户许可\n\n**绝对禁止**：未经用户明确允许，不得使用 `gh pr comment` 或其他方式向 PR 提交任何评论。\n\n**正确流程**：\n1. 完成 review 分析\n2. 向用户展示 review 报告内容\n3. 询问用户：\"是否需要我将此报告提交为 PR 评论？\"\n4. 仅在用户明确确认后才执行提交\n\n### 2. 报告末尾必须注明使用的模型及审核状态\n\n在 review 报告的结论部分之后，必须添加以下信息：\n\n```markdown\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n```\n\n示例：\n```markdown\n---\n*Review by deepseek-v4-pro | 2026-05-07 | 经人工审核确认*\n```\n\n---\n\n## Review 流程\n\n按以下阶段顺序审查，确保覆盖完整：\n\n1. **Phase 1**: Provider 结构完整性\n2. **Phase 2**: 代码规范检查\n3. **Phase 3**: 安全性检查（签名验证、加密解密）\n4. **Phase 4**: 架构一致性（管道、Trait、Event）\n5. **Phase 5**: 官方文档对照\n6. **Phase 6**: 测试覆盖\n7. **Phase 7**: 文档完整性\n\n---\n\n## Phase 1: Provider 结构完整性\n\n对照以下清单逐项检查：\n\n| # | 检查项 | 位置 | 说明 |\n|---|--------|------|------|\n| 1 | 插件 | `src/Plugin/{Provider}/V{n}/` | 按版本组织 |\n| 2 | Provider 类 | `src/Provider/{Provider}.php` | 实现 `ProviderInterface` |\n| 3 | 服务提供者 | `src/Service/{Provider}ServiceProvider.php` | 服务注册 |\n| 4 | 快捷方式 | `src/Shortcut/{Provider}/` | `{Method}Shortcut.php` |\n| 5 | Trait 方法 | `src/Traits/{Provider}Trait.php` | `get{Provider}Url`、`verify{Provider}WebhookSign` 等 |\n| 6 | Provider 注册 | `src/Pay.php` | 添加 `{Provider}::class` 和入口方法 |\n| 7 | 异常常量 | `src/Exception/Exception.php` | `PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*` |\n| 8 | 测试 | `tests/` | 与源码结构对应 |\n| 9 | 文档 | `web/docs/v3/{provider}/` | VitePress 文档 |\n| 10 | 侧边栏/CHANGELOG | `web/.vitepress/sidebar/v3.js`、`CHANGELOG.md` | 更新配置 |\n\n**注意**：`src/Functions.php` 已不存在，URL/签名等方法已下沉到 `src/Traits/*Trait.php`。\n\n---\n\n## Phase 2: 代码规范检查\n\n### 基本规范\n\n| 检查项 | 要求 |\n|--------|------|\n| `declare(strict_types=1)` | 每个文件必须有 |\n| `use` 导入 | 除动态类名字符串/反射场景外，禁止直接写完整命名空间（如 `\\Yansongda\\Pay\\...`） |\n| 多行条件 | `&&` / `||` 放在续行开头 |\n\n### 命名规范\n\n| 类型 | 格式 | 示例 |\n|------|------|------|\n| 插件 | `{Action}Plugin.php` | `PayPlugin`、`RefundPlugin` |\n| 快捷方式 | `{Method}Shortcut.php` | `WebShortcut`、`QueryShortcut` |\n| Provider | `{ProviderName}.php` | `Paypal.php`、`Stripe.php` |\n| ServiceProvider | `{ProviderName}ServiceProvider.php` | `PaypalServiceProvider.php` |\n| Trait | `{Provider}Trait.php` | `WechatTrait`、`StripeTrait` |\n| 命名空间 | `Yansongda\\Pay\\Plugin\\{Provider}\\V{n}\\Pay\\{Plugin}` | 版本号与 API 版本一致 |\n\n### 日志与异常\n\n- 日志格式：`[Provider][V{n}][Category][Plugin]`，使用中文消息\n- 异常常量：`PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*`\n- 异常消息：中文，附带上下文参数\n\n---\n\n## Phase 3: 安全性检查\n\n### 1. params vs payload 混淆\n\n**高危**：`Artful::artful()` 第二参数必须是 `params`（含 `_config`），不能是 `payload`。\n\n```php\n// ❌ 错误 — payload 不含 _config，多租户必崩\n$result = Artful::artful([...], $confirmPayload);\n\n// ✅ 正确 — params 含 _config 租户标识\n$result = Artful::artful([...], $confirmParams);\n```\n\n**检查方法**：搜索 `Artful::artful(` 调用，追踪第二参数来源。\n\n### 2. 空值处理\n\n检查最终发送的 body/query 是否包含不应出现的 `null`/空字段。\n\n**推荐方式**：\n- 优先使用 `filter_params()` 函数（artful 库提供）\n- 嵌套数组场景补充 `array_filter()`\n\n```php\n// ✅ 优先方式 — filter_params\n$body = http_build_query(filter_params($payload)->toArray());\n\n// ✅ 嵌套场景 — array_filter\n$rocket->mergePayload(array_filter([\n    'application_context' => $payload->get('application_context'),\n], static fn ($value) => !is_null($value)));\n```\n\n**检查位置**：`AddRadarPlugin::getBody()`、`getQueryString()`、业务 Plugin 的 `mergePayload()`。\n\n### 3. Webhook 签名验证\n\n**安全强制**：所有 CallbackPlugin 必须验签。\n\n| Provider | Trait 方法 | 必需配置字段 |\n|----------|------------|--------------|\n| Stripe | `StripeTrait::verifyStripeWebhookSign()` | `webhook_secret` |\n| PayPal | `PaypalTrait::verifyPaypalWebhookSign()` | `webhook_id` + OAuth |\n| 微信 | `WechatTrait::verifyWechatSign()` | `mch_secret_cert`、`wechat_public_cert_path`（可预置或运行时拉取） |\n| 抖音 | —（在 `CallbackPlugin::verifySign()` 中实现） | `mch_secret_token`、`mch_secret_salt` |\n| 银联 | `UnipayTrait::verifyUnipaySign()` | `unipay_public_cert_path` |\n| 支付宝 | `AlipayTrait::verifyAlipaySign()` | `alipay_public_cert_path` |\n\n**检查点**：\n- CallbackPlugin 是否调用 Trait 提供或 Plugin 自身实现的签名验证方法\n- 签名算法是否与官方文档一致（对照 `@see` 链接）\n- 配置缺失时抛异常\n- 签名为空时抛异常\n- 使用 `hash_equals` 防时序攻击\n\n### 4. 数组回调的处理\n\n**仅适用于 Stripe/Wechat/Paypal**（构造 `ServerRequest` 并验签）：\n\n`getCallbackParams()` 处理逻辑：\n\n| 输入类型 | 行为 |\n|----------|------|\n| `['body' => ..., 'headers' => ...]` | 构造带 headers 的 `ServerRequest`，**会验签** |\n| 纯数组（无 headers） | 构造无 headers 的 `ServerRequest`，验签时会抛 `SIGN_EMPTY` |\n| `ServerRequestInterface` | 直接使用，验签 |\n| `null` | 从 `ServerRequest::fromGlobals()` 获取 |\n\n**结论**：数组回调只有提供完整 `headers` + `body` 才能通过验签；否则验签阶段抛异常。\n\n**Alipay/Douyin/Unipay 不同**：\n- `getCallbackParams()` 返回 `Collection`（从 query/parsedBody 取值）\n- 直接 merge 到 params，不构造 `ServerRequest`\n- CallbackPlugin 从 params 中取值验签\n\n### 5. 加密资源解密（微信）\n\n微信回调的 `resource` 字段需解密：\n\n```php\n$body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config);\n```\n\n检查 `CallbackPlugin` 是否调用此方法。\n\n---\n\n## Phase 4: 架构一致性\n\n### 1. 插件管道骨架\n\n**通用骨架**：\n```\nStartPlugin → [前置插件] → 业务插件 → [后置插件] → ParserPlugin\n```\n\n**各 Provider 差异**：\n\n| Provider | 前置插件 | 后置插件 |\n|----------|----------|----------|\n| Stripe | 无 | `AddRadarPlugin` → `ResponsePlugin` |\n| PayPal | `ObtainAccessTokenPlugin` | `AddPayloadBodyPlugin` → `AddRadarPlugin` → `ResponsePlugin` |\n| 微信 | 无 | `AddPayloadBodyPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n| 支付宝 | 无 | `FormatPayloadBizContentPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n\n**注意**：不同 Provider 管道差异较大，不要按固定模板审查。\n\n### 2. mergeCommonPlugins 实现\n\nProvider 类必须实现此方法，返回完整管道：\n\n```php\npublic function mergeCommonPlugins(array $plugins): array\n{\n    return array_merge(\n        [StartPlugin::class, /* 前置插件 */],\n        $plugins,\n        [/* 后置插件 */, ParserPlugin::class],\n    );\n}\n```\n\n### 3. Trait 方法复用\n\n新增 Provider 应复用 Trait 方法而非重新实现：\n\n| 功能 | Trait 方法 |\n|------|------------|\n| URL 构建 | `get{Provider}Url()` |\n| 签名验证 | `verify{Provider}WebhookSign()` / `verify{Provider}Sign()` |\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()` |\n| 加密解密 | `WechatTrait::decryptWechatResource()` |\n\n### 4. Provider 常量定义\n\n必须定义 `URL` 常量：\n\n```php\npublic const URL = [\n    PayMode::NORMAL->value => 'https://api.xxx.com/',\n    PayMode::SANDBOX->value => 'https://sandbox.api.xxx.com/',\n    PayMode::SERVICE->value => 'https://api.xxx.com/',\n];\n```\n\n特殊常量（如微信）：\n- `AUTH_TAG_LENGTH_BYTE`\n- `MCH_SECRET_KEY_LENGTH_BYTE`\n\n### 5. Event 调用\n\ncallback 方法必须触发事件：\n\n```php\n// Stripe/Wechat/Paypal（ServerRequestInterface）\nEvent::dispatch(new CallbackReceived('provider', clone $request, $params, null));\n\n// Alipay/Douyin/Unipay/Jsb（Collection）\nEvent::dispatch(new CallbackReceived('provider', $request->all(), $params, null));\n```\n\n其他方法触发：\n\n```php\nEvent::dispatch(new MethodCalled('provider', __METHOD__, $order, null));\n```\n\n### 6. PHPStan ignore 注释\n\nTrait 静态方法调用需添加注释：\n\n```php\n/* @phpstan-ignore-next-line */\nself::verifyWechatSign(...);\n```\n\n这是 PHPStan 对 Trait 静态调用的已知限制，非代码质量问题。\n\n---\n\n## Phase 5: 官方文档对照\n\n结合代码中的 `@see` 链接验证：\n\n| # | 检查点 |\n|---|--------|\n| 1 | API 端点 URL 是否与官方一致 |\n| 2 | HTTP 方法（GET/POST）是否正确 |\n| 3 | 认证 Header 名称、格式是否正确 |\n| 4 | 请求/响应字段（必填/可选）是否正确 |\n| 5 | 签名算法、拼接顺序是否与官方完全一致 |\n| 6 | Base URL（production/sandbox）是否正确 |\n\n---\n\n## Phase 6: 测试覆盖\n\n| # | 检查项 |\n|---|--------|\n| 1 | 每个 Plugin 有对应测试 |\n| 2 | 必填参数缺失的异常测试 |\n| 3 | 可选参数缺失的边界测试 |\n| 4 | 多租户场景（`_config` 参数） |\n| 5 | HTTP client mock，禁止真实 API 调用 |\n| 6 | Callback 签名验证的正向/反向测试 |\n\n---\n\n## Phase 7: 文档完整性\n\n| # | 检查项 |\n|---|--------|\n| 1 | 官方链接可访问且指向正确端点 |\n| 2 | 示例代码使用原生 PHP，框架无关 |\n| 3 | 侧边栏已更新 |\n| 4 | CHANGELOG 已更新 |\n\n---\n\n## 常见误报\n\n### null-safe 操作符\n\n```php\n$payload?->get('a', $payload->get('b'))\n```\n\nPHP 8.0 的 null-safe 实现了 **full short-circuiting**：当 `$payload` 为 `null` 时，右侧参数不会被求值。这不是 bug。\n\n参考：[PHP RFC nullsafe_operator](https://wiki.php.net/rfc/nullsafe_operator)\n\n---\n\n## 避免重复造轮子\n\n检查新增功能是否在 `yansongda/supports` 或 `yansongda/artful` 中已有实现：\n\n| 功能 | 已有实现 |\n|------|----------|\n| UUID 生成 | `Str::uuidV4()` |\n| 字符串处理 | `Str::*` |\n| 集合操作 | `Collection::*` |\n| 空值过滤 | `filter_params()` (artful) |\n| HTTP 方法获取 | `get_radar_method()` (artful) |\n| Body 获取 | `get_radar_body()` (artful) |\n\n\n\n## Review 报告模板\n\n以下为提交 review 时的标准报告格式：\n\n````markdown\n# PR #{number} Code Review — {Provider} Provider\n\n## 一、总览\n\n| 维度 | 内容 |\n|------|------|\n| PR | #{number} — {title} |\n| 涉及文件 | {n} 个 |\n| Review 范围 | 全量代码 + 文档 + 测试 |\n| 方法 | 逐文件审查 + 官方文档对照 + 跨 Provider 一致性比对 |\n\n## 二、已确认 Bug（含证据链）\n\n### BUG-{n}: {简要描述}\n\n**严重级别**：{高/中高/中/低}\n\n**位置**：`{file_path}` L{line}\n\n**问题**：\n{详细描述问题本质}\n\n**证据链**：\n1. 代码现状：`{相关代码片段}`\n2. 预期行为：{应该怎样}\n3. 实际行为：{实际会怎样}\n4. 影响范围：{哪些场景会触发}\n\n**修复建议**：\n```php\n// 修复方案\n```\n\n---\n\n（重复以上格式，每个 Bug 单独一节）\n\n## 三、已排除的误报\n\n### {误报描述}\n\n**结论**：非 Bug\n\n**原因**：{详细解释，附 RFC/文档链接}\n\n## 四、风险项\n\n| # | 风险 | 严重级别 | 文件 | 说明 |\n|---|------|----------|------|------|\n| RISK-{n} | {风险名} | {级别} | `{file}` | {说明} |\n\n## 五、改进建议\n\n| # | 建议 | 优先级 | 文件 | 说明 |\n|---|------|--------|------|------|\n| SUG-{n} | {建议名} | {级别} | `{file}` | {说明} |\n\n## 六、官方文档对照验证\n\n| 功能 | 代码中的 URL/算法 | 官方文档 | 是否一致 |\n|------|-------------------|----------|----------|\n| {功能名} | `{代码实现}` | [官方文档]({url}) | ✅/❌ |\n\n## 七、代码规范检查\n\n| 检查项 | 状态 | 备注 |\n|--------|------|------|\n| `declare(strict_types=1)` | ✅/❌ | |\n| `use` 导入（无内联命名空间） | ✅/❌ | |\n| 日志格式 `[Provider][V{n}][Category][Plugin]` | ✅/❌ | |\n| 异常常量命名 | ✅/❌ | |\n| Plugin/Shortcut 命名规范 | ✅/❌ | |\n| 测试覆盖 | ✅/❌ | |\n\n## 八、汇总\n\n| 类别 | 数量 | 详情 |\n|------|------|------|\n| 已确认 Bug | {n} | {简要列举} |\n| 已排除误报 | {n} | {简要列举} |\n| 风险项 | {n} | {简要列举} |\n| 改进建议 | {n} | {简要列举} |\n\n## 九、结论\n\n{总体评价：代码质量、架构一致性、安全性等。明确给出 Approve / Request Changes 建议及原因。}\n\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n````\n",".agents/skills/container-dev/SKILL.md":"---\nname: container-dev\ndescription: \"Use when local PHP environment is unavailable. Fallback container-based development environment for yansongda/pay project.\"\n---\n\n# Container-based Local Development (Fallback)\n\n当本地没有 PHP 环境时，使用 Apple Container 或 Docker 作为备选方案。\n\n## 使用原则\n\n**优先本地环境**：先检查本地是否有 PHP/composer，有则直接使用。\n\n**容器作为备选**：仅在本地环境不可用时使用容器。\n\n## 快速参考\n\n### 镜像\n\n```\nregistry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine\n```\n\n### 常用命令\n\n| 任务 | 命令 |\n|------|------|\n| 运行测试 | `composer test` |\n| PHPStan 分析 | `composer analyse` |\n| 代码风格检查 | `composer cs-fix`（**仅检查，不修复**） |\n| 代码风格修复 | `php-cs-fixer fix ./src` |\n| Composer 更新 | `composer update --with-all-dependencies` |\n| 安装依赖 | `composer install` |\n| Web 文档开发 | `pnpm web:dev`（需本地 Node 或 Node 容器） |\n| Web 文档构建 | `pnpm web:build`（需本地 Node 或 Node 容器） |\n\n---\n\n## Apple Container\n\n### 基本模板\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  COMMAND\n```\n\n### DNS 问题处理\n\n若出现 DNS 解析失败（`198.18.0.x` 地址、连接超时），添加 DNS fix：\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMMAND\"\n```\n\n### PHP 命令示例\n\n```bash\n# 测试（需 DNS fix + COMPOSER_ALLOW_SUPERUSER）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer test\"\n\n# PHPStan 分析\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer analyse\"\n\n# 代码风格检查（仅查看差异）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer cs-fix\"\n\n# 代码风格修复\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 php-cs-fixer fix ./src\"\n\n# Composer update\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && composer update --with-all-dependencies\"\n```\n\n### Web 文档命令\n\nWeb 文档使用 pnpm，镜像中无 Node.js，需使用 Node 镜像或本地运行。\n\n**注意**：`web/package.json` 要求 Node `>=20.12.0`，请确保使用的镜像版本满足此约束。\n\n```bash\n# 方案1: 本地运行（推荐）\ncd web && pnpm web:dev\n\n# 方案2: 使用 Node 容器（版本 >=20.12.0）\ndocker run --rm -v \"$(pwd)/web\":/app -w /app \\\n  node:20-alpine \\\n  sh -c \"npm install -g pnpm && pnpm install && pnpm web:dev\"\n```\n\n---\n\n## Docker\n\nDocker 命令类似，通常不需要 DNS fix：\n\n```bash\n# 测试\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer test\n\n# PHPStan 分析\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer analyse\n\n# 代码风格检查\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer cs-fix\n\n# 代码风格修复\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  php-cs-fixer fix ./src\n```\n\n---\n\n## DNS 问题诊断（Apple Container）\n\n**症状**：\n- `curl error 28 Connection timed out`\n- DNS 解析返回 `198.18.0.x`（基准测试保留地址）\n\n**原因**：Container 系统 DNS 解析器异常。\n\n**解决**：手动写入 `nameserver 8.8.8.8` 到 `/etc/resolv.conf`。\n\n---\n\n## 注意事项\n\n- `COMPOSER_ALLOW_SUPERUSER=1`：容器内运行 composer 脚本必须\n- `composer cs-fix`：**仅检查并显示差异**，不自动修复\n- `php-cs-fixer fix ./src`：实际修复代码风格\n- Apple Container 需先启动：`container system start`\n- Docker 需确保 Docker Desktop 或 daemon 已启动\n- 容器内网络可能无法访问某些 CDN，测试中可能出现 PHP warning（不影响核心功能）","AGENTS.md":"# yansongda/pay AGENTS.md\n\n## OVERVIEW\nPHP 支付 SDK，支持支付宝、微信、银联、抖音、江苏银行、PayPal、Stripe。基于插件管道架构，关注支付流程、回调验签和 Provider 扩展一致性。\n\n## STRUCTURE\n```\nsrc/\n├── Config/           # Typed Config（WechatConfig、AlipayConfig 等）\n├── Event/            # PSR-14 事件\n├── Plugin/{Provider}/V{n}/\n├── Provider/         # Alipay、Wechat 等\n├── Shortcut/{Provider}/\n├── Traits/           # {Provider}Trait、ProviderConfigTrait\n├── CertManager.php   # 证书缓存\n└── Pay.php\n```\n\n## 核心组件\n| 符号 | 位置 | 用途 |\n|---|---|---|\n| `Pay::alipay()` | `Pay.php` | 支付入口 |\n| `Pay::wechat()` | `Pay.php` | 微信入口 |\n| `ProviderInterface` | `Contract/` | 核心接口（pay/query/cancel/close/refund/callback/success） |\n| `Rocket` | `Yansongda\\Artful\\Rocket` | 承载 params/payload/radar/destination |\n| `CertManager` | `CertManager.php` | 证书读取与缓存 |\n| `Event` | `Event.php` | PSR-14 事件分发 |\n| `{Provider}Config` | `Config/` | 类型化配置对象 |\n\n## 核心原则\n- **安全第一**：回调/Webhook 必须验签，禁止直接信任传入数据\n- **代码一致性**：新增 Provider 遵循现有结构\n\n## COMMANDS & 开发规范\n详见 `.agents/skills/dev-guide/SKILL.md`，使用时加载。\n\n## 架构要点\n- **插件管道**：`StartPlugin → [前置] → 业务插件 → [后置] → ParserPlugin`\n- **动态调用**：Provider 通过 `__call` 调用 Shortcut，再进入管道\n- **配置系统**：`Config` 类自动将数组配置转为 `{Provider}Config` 对象\n- **事件系统**：`PayListener` 桥接 Artful 事件 → Pay 事件\n\n## 签名验证（必须）\n| Provider | 方式 | Trait 方法 |\n|---|---|---|\n| 支付宝 | RSA 本地验证 | `AlipayTrait::verifyAlipaySign()` |\n| 微信 | 证书签名本地验证 | `WechatTrait::verifyWechatSign()` |\n| PayPal | API 验证 | `PaypalTrait::verifyPaypalWebhookSign()` |\n| Stripe | HMAC-SHA256 本地 | `StripeTrait::verifyStripeWebhookSign()` |\n| 银联 | 证书签名本地验证 | `UnipayTrait::verifyUnipaySign()` |\n| 江苏银行 | RSA 本地验证 | `JsbTrait::verifyJsbSign()` |\n| 抖音 | SHA1 本地验证 | `CallbackPlugin::verifySign()` |\n| 通联支付 | RSA-SHA1 本地验证 | `AllinpayTrait::verifyAllinpaySign()` |\n\n回调处理差异：\n- **Wechat/Stripe/Paypal**：传 `_request`（ServerRequestInterface）到 CallbackPlugin\n- **Alipay/Douyin/Unipay/Jsb/Allinpay**：通过 `getCallbackParams()` 获取 Collection\n\n## Trait 参考\n| 用途 | 方法（示例） |\n|---|---|\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()`、`getRadarUrl()` |\n| URL 构建 | `get{Provider}Url()`（各 Trait） |\n| 签名验证 | `verify{Provider}Sign()`、`verify{Provider}WebhookSign()` |\n| 证书管理 | `CertManager::getPublicCert()`、`getPrivateCert()` |\n| 微信专用 | `decryptWechatResource()`、`reloadWechatPublicCerts()` |\n\n> 完整方法列表见 `src/Traits/{Provider}Trait.php`\n\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/yansongda/pay/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# yansongda/pay AGENTS.md\n\n## OVERVIEW\nPHP 支付 SDK，支持支付宝、微信、银联、抖音、江苏银行、PayPal、Stripe。基于插件管道架构，关注支付流程、回调验签和 Provider 扩展一致性。\n\n## STRUCTURE\n```\nsrc/\n├── Config/           # Typed Config（WechatConfig、AlipayConfig 等）\n├── Event/            # PSR-14 事件\n├── Plugin/{Provider}/V{n}/\n├── Provider/         # Alipay、Wechat 等\n├── Shortcut/{Provider}/\n├── Traits/           # {Provider}Trait、ProviderConfigTrait\n├── CertManager.php   # 证书缓存\n└── Pay.php\n```\n\n## 核心组件\n| 符号 | 位置 | 用途 |\n|---|---|---|\n| `Pay::alipay()` | `Pay.php` | 支付入口 |\n| `Pay::wechat()` | `Pay.php` | 微信入口 |\n| `ProviderInterface` | `Contract/` | 核心接口（pay/query/cancel/close/refund/callback/success） |\n| `Rocket` | `Yansongda\\Artful\\Rocket` | 承载 params/payload/radar/destination |\n| `CertManager` | `CertManager.php` | 证书读取与缓存 |\n| `Event` | `Event.php` | PSR-14 事件分发 |\n| `{Provider}Config` | `Config/` | 类型化配置对象 |\n\n## 核心原则\n- **安全第一**：回调/Webhook 必须验签，禁止直接信任传入数据\n- **代码一致性**：新增 Provider 遵循现有结构\n\n## COMMANDS & 开发规范\n详见 `.agents/skills/dev-guide/SKILL.md`，使用时加载。\n\n## 架构要点\n- **插件管道**：`StartPlugin → [前置] → 业务插件 → [后置] → ParserPlugin`\n- **动态调用**：Provider 通过 `__call` 调用 Shortcut，再进入管道\n- **配置系统**：`Config` 类自动将数组配置转为 `{Provider}Config` 对象\n- **事件系统**：`PayListener` 桥接 Artful 事件 → Pay 事件\n\n## 签名验证（必须）\n| Provider | 方式 | Trait 方法 |\n|---|---|---|\n| 支付宝 | RSA 本地验证 | `AlipayTrait::verifyAlipaySign()` |\n| 微信 | 证书签名本地验证 | `WechatTrait::verifyWechatSign()` |\n| PayPal | API 验证 | `PaypalTrait::verifyPaypalWebhookSign()` |\n| Stripe | HMAC-SHA256 本地 | `StripeTrait::verifyStripeWebhookSign()` |\n| 银联 | 证书签名本地验证 | `UnipayTrait::verifyUnipaySign()` |\n| 江苏银行 | RSA 本地验证 | `JsbTrait::verifyJsbSign()` |\n| 抖音 | SHA1 本地验证 | `CallbackPlugin::verifySign()` |\n| 通联支付 | RSA-SHA1 本地验证 | `AllinpayTrait::verifyAllinpaySign()` |\n\n回调处理差异：\n- **Wechat/Stripe/Paypal**：传 `_request`（ServerRequestInterface）到 CallbackPlugin\n- **Alipay/Douyin/Unipay/Jsb/Allinpay**：通过 `getCallbackParams()` 获取 Collection\n\n## Trait 参考\n| 用途 | 方法（示例） |\n|---|---|\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()`、`getRadarUrl()` |\n| URL 构建 | `get{Provider}Url()`（各 Trait） |\n| 签名验证 | `verify{Provider}Sign()`、`verify{Provider}WebhookSign()` |\n| 证书管理 | `CertManager::getPublicCert()`、`getPrivateCert()` |\n| 微信专用 | `decryptWechatResource()`、`reloadWechatPublicCerts()` |\n\n> 完整方法列表见 `src/Traits/{Provider}Trait.php`\n\n","isInternal":false,"tokens":1013,"sizeBytes":3075},{"name":"SKILL.md","path":".agents/skills/container-dev/SKILL.md","rawUrl":"https://raw.githubusercontent.com/yansongda/pay/HEAD/.agents/skills/container-dev/SKILL.md","title":"container-dev","category":"anthropic-skill","format":"markdown","content":"---\nname: container-dev\ndescription: \"Use when local PHP environment is unavailable. Fallback container-based development environment for yansongda/pay project.\"\n---\n\n# Container-based Local Development (Fallback)\n\n当本地没有 PHP 环境时，使用 Apple Container 或 Docker 作为备选方案。\n\n## 使用原则\n\n**优先本地环境**：先检查本地是否有 PHP/composer，有则直接使用。\n\n**容器作为备选**：仅在本地环境不可用时使用容器。\n\n## 快速参考\n\n### 镜像\n\n```\nregistry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine\n```\n\n### 常用命令\n\n| 任务 | 命令 |\n|------|------|\n| 运行测试 | `composer test` |\n| PHPStan 分析 | `composer analyse` |\n| 代码风格检查 | `composer cs-fix`（**仅检查，不修复**） |\n| 代码风格修复 | `php-cs-fixer fix ./src` |\n| Composer 更新 | `composer update --with-all-dependencies` |\n| 安装依赖 | `composer install` |\n| Web 文档开发 | `pnpm web:dev`（需本地 Node 或 Node 容器） |\n| Web 文档构建 | `pnpm web:build`（需本地 Node 或 Node 容器） |\n\n---\n\n## Apple Container\n\n### 基本模板\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  COMMAND\n```\n\n### DNS 问题处理\n\n若出现 DNS 解析失败（`198.18.0.x` 地址、连接超时），添加 DNS fix：\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMMAND\"\n```\n\n### PHP 命令示例\n\n```bash\n# 测试（需 DNS fix + COMPOSER_ALLOW_SUPERUSER）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer test\"\n\n# PHPStan 分析\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer analyse\"\n\n# 代码风格检查（仅查看差异）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer cs-fix\"\n\n# 代码风格修复\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 php-cs-fixer fix ./src\"\n\n# Composer update\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && composer update --with-all-dependencies\"\n```\n\n### Web 文档命令\n\nWeb 文档使用 pnpm，镜像中无 Node.js，需使用 Node 镜像或本地运行。\n\n**注意**：`web/package.json` 要求 Node `>=20.12.0`，请确保使用的镜像版本满足此约束。\n\n```bash\n# 方案1: 本地运行（推荐）\ncd web && pnpm web:dev\n\n# 方案2: 使用 Node 容器（版本 >=20.12.0）\ndocker run --rm -v \"$(pwd)/web\":/app -w /app \\\n  node:20-alpine \\\n  sh -c \"npm install -g pnpm && pnpm install && pnpm web:dev\"\n```\n\n---\n\n## Docker\n\nDocker 命令类似，通常不需要 DNS fix：\n\n```bash\n# 测试\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer test\n\n# PHPStan 分析\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer analyse\n\n# 代码风格检查\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer cs-fix\n\n# 代码风格修复\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  php-cs-fixer fix ./src\n```\n\n---\n\n## DNS 问题诊断（Apple Container）\n\n**症状**：\n- `curl error 28 Connection timed out`\n- DNS 解析返回 `198.18.0.x`（基准测试保留地址）\n\n**原因**：Container 系统 DNS 解析器异常。\n\n**解决**：手动写入 `nameserver 8.8.8.8` 到 `/etc/resolv.conf`。\n\n---\n\n## 注意事项\n\n- `COMPOSER_ALLOW_SUPERUSER=1`：容器内运行 composer 脚本必须\n- `composer cs-fix`：**仅检查并显示差异**，不自动修复\n- `php-cs-fixer fix ./src`：实际修复代码风格\n- Apple Container 需先启动：`container system start`\n- Docker 需确保 Docker Desktop 或 daemon 已启动\n- 容器内网络可能无法访问某些 CDN，测试中可能出现 PHP warning（不影响核心功能）","frontmatter":{"name":"container-dev","description":"Use when local PHP environment is unavailable. Fallback container-based development environment for yansongda/pay project."},"isInternal":false,"tokens":1608,"sizeBytes":4699},{"name":"SKILL.md","path":".agents/skills/dev-guide/SKILL.md","rawUrl":"https://raw.githubusercontent.com/yansongda/pay/HEAD/.agents/skills/dev-guide/SKILL.md","title":"dev-guide","category":"anthropic-skill","format":"markdown","content":"---\nname: dev-guide\ndescription: Use when developing, testing, or adding new providers to yansongda/pay project. Covers commands, code standards, and provider implementation guide.\n---\n\n# yansongda/pay 开发指南\n\n## 开发命令\n\n**对 `src/` PHP 代码的任何修改，需运行以下三项检查：**\n\n```bash\ncomposer cs-fix && composer analyse && composer test\n```\n\n### 测试\n```bash\ncomposer test       # PHPUnit 11.x + Mockery 1.6\n```\n- 测试前需安装 `hyperf/pimple`\n- Mock HTTP 客户端避免真实请求\n\n### 代码风格\n```bash\ncomposer cs-fix     # 代码格式化检查（dry-run）\n```\n\n### 静态分析\n```bash\ncomposer analyse    # PHPStan\n```\n\n### 文档\n```bash\ncd web && pnpm web:dev    # 文档开发\ncd web && pnpm web:build  # 文档构建\ncd web && pnpm web:serve  # 本地预览\n```\n\n### 本地开发环境\n优先本地 PHP 环境；若无 PHP，使用 Container 作为备选，详见 `container-dev` Skill。\n\n### CI 矩阵\nPHP 8.2-8.5 + Laravel/Hyperf/Default\n\n---\n\n## 代码规范\n\n- `declare(strict_types=1);` 必须\n- `use` 导入，禁止 `\\Yansongda\\Pay\\...`\n- 日志/异常消息用中文\n- 命名：`{Action}Plugin.php`、`{Action}Shortcut.php`、`{Provider}Trait.php`\n\n---\n\n## 新增 Provider 流程\n\n### 6 阶段\n1. **配置**：`src/Config/{Provider}Config.php`\n2. **核心类**：`src/Provider/{Provider}.php` + `src/Service/{Provider}ServiceProvider.php`（继承 AbstractServiceProvider）\n3. **插件**：`src/Plugin/{Provider}/V{n}/`（含 CallbackPlugin）\n4. **快捷方式**：`src/Shortcut/{Provider}/{Action}Shortcut.php`\n5. **Trait**：`src/Traits/{Provider}Trait.php`（`get{Provider}Url`、`verify{Provider}Sign`）\n6. **注册**：`Pay.php` + `Config.php` 映射 + `Exception.php` 常量 + 测试 + 文档\n\n---\n\n## 常见错误\n\n- 忽略 `declare(strict_types=1);`\n- 直接写完整命名空间\n- 回调未验签\n- 测试未 Mock HTTP 客户端\n","frontmatter":{"name":"dev-guide","description":"Use when developing, testing, or adding new providers to yansongda/pay project. Covers commands, code standards, and provider implementation guide."},"isInternal":false,"tokens":639,"sizeBytes":1905},{"name":"SKILL.md","path":".agents/skills/pr-review-provider/SKILL.md","rawUrl":"https://raw.githubusercontent.com/yansongda/pay/HEAD/.agents/skills/pr-review-provider/SKILL.md","title":"pr-review-provider","category":"anthropic-skill","format":"markdown","content":"---\nname: pr-review-provider\ndescription: \"Use when reviewing PRs that add or modify a payment Provider in yansongda/pay - covers plugin pipeline, multi-tenant safety, signature verification, docs, and naming conventions.\"\n---\n\n# PR Review Checklist — Payment Provider\n\nyansongda/pay 新增/修改 Provider 时的 Code Review 专用检查清单。基于 Airwallex PR #1140 review 经验沉淀。\n\n## Review 规范（强制）\n\n### 1. 提交评论前必须征求用户许可\n\n**绝对禁止**：未经用户明确允许，不得使用 `gh pr comment` 或其他方式向 PR 提交任何评论。\n\n**正确流程**：\n1. 完成 review 分析\n2. 向用户展示 review 报告内容\n3. 询问用户：\"是否需要我将此报告提交为 PR 评论？\"\n4. 仅在用户明确确认后才执行提交\n\n### 2. 报告末尾必须注明使用的模型及审核状态\n\n在 review 报告的结论部分之后，必须添加以下信息：\n\n```markdown\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n```\n\n示例：\n```markdown\n---\n*Review by deepseek-v4-pro | 2026-05-07 | 经人工审核确认*\n```\n\n---\n\n## Review 流程\n\n按以下阶段顺序审查，确保覆盖完整：\n\n1. **Phase 1**: Provider 结构完整性\n2. **Phase 2**: 代码规范检查\n3. **Phase 3**: 安全性检查（签名验证、加密解密）\n4. **Phase 4**: 架构一致性（管道、Trait、Event）\n5. **Phase 5**: 官方文档对照\n6. **Phase 6**: 测试覆盖\n7. **Phase 7**: 文档完整性\n\n---\n\n## Phase 1: Provider 结构完整性\n\n对照以下清单逐项检查：\n\n| # | 检查项 | 位置 | 说明 |\n|---|--------|------|------|\n| 1 | 插件 | `src/Plugin/{Provider}/V{n}/` | 按版本组织 |\n| 2 | Provider 类 | `src/Provider/{Provider}.php` | 实现 `ProviderInterface` |\n| 3 | 服务提供者 | `src/Service/{Provider}ServiceProvider.php` | 服务注册 |\n| 4 | 快捷方式 | `src/Shortcut/{Provider}/` | `{Method}Shortcut.php` |\n| 5 | Trait 方法 | `src/Traits/{Provider}Trait.php` | `get{Provider}Url`、`verify{Provider}WebhookSign` 等 |\n| 6 | Provider 注册 | `src/Pay.php` | 添加 `{Provider}::class` 和入口方法 |\n| 7 | 异常常量 | `src/Exception/Exception.php` | `PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*` |\n| 8 | 测试 | `tests/` | 与源码结构对应 |\n| 9 | 文档 | `web/docs/v3/{provider}/` | VitePress 文档 |\n| 10 | 侧边栏/CHANGELOG | `web/.vitepress/sidebar/v3.js`、`CHANGELOG.md` | 更新配置 |\n\n**注意**：`src/Functions.php` 已不存在，URL/签名等方法已下沉到 `src/Traits/*Trait.php`。\n\n---\n\n## Phase 2: 代码规范检查\n\n### 基本规范\n\n| 检查项 | 要求 |\n|--------|------|\n| `declare(strict_types=1)` | 每个文件必须有 |\n| `use` 导入 | 除动态类名字符串/反射场景外，禁止直接写完整命名空间（如 `\\Yansongda\\Pay\\...`） |\n| 多行条件 | `&&` / `||` 放在续行开头 |\n\n### 命名规范\n\n| 类型 | 格式 | 示例 |\n|------|------|------|\n| 插件 | `{Action}Plugin.php` | `PayPlugin`、`RefundPlugin` |\n| 快捷方式 | `{Method}Shortcut.php` | `WebShortcut`、`QueryShortcut` |\n| Provider | `{ProviderName}.php` | `Paypal.php`、`Stripe.php` |\n| ServiceProvider | `{ProviderName}ServiceProvider.php` | `PaypalServiceProvider.php` |\n| Trait | `{Provider}Trait.php` | `WechatTrait`、`StripeTrait` |\n| 命名空间 | `Yansongda\\Pay\\Plugin\\{Provider}\\V{n}\\Pay\\{Plugin}` | 版本号与 API 版本一致 |\n\n### 日志与异常\n\n- 日志格式：`[Provider][V{n}][Category][Plugin]`，使用中文消息\n- 异常常量：`PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*`\n- 异常消息：中文，附带上下文参数\n\n---\n\n## Phase 3: 安全性检查\n\n### 1. params vs payload 混淆\n\n**高危**：`Artful::artful()` 第二参数必须是 `params`（含 `_config`），不能是 `payload`。\n\n```php\n// ❌ 错误 — payload 不含 _config，多租户必崩\n$result = Artful::artful([...], $confirmPayload);\n\n// ✅ 正确 — params 含 _config 租户标识\n$result = Artful::artful([...], $confirmParams);\n```\n\n**检查方法**：搜索 `Artful::artful(` 调用，追踪第二参数来源。\n\n### 2. 空值处理\n\n检查最终发送的 body/query 是否包含不应出现的 `null`/空字段。\n\n**推荐方式**：\n- 优先使用 `filter_params()` 函数（artful 库提供）\n- 嵌套数组场景补充 `array_filter()`\n\n```php\n// ✅ 优先方式 — filter_params\n$body = http_build_query(filter_params($payload)->toArray());\n\n// ✅ 嵌套场景 — array_filter\n$rocket->mergePayload(array_filter([\n    'application_context' => $payload->get('application_context'),\n], static fn ($value) => !is_null($value)));\n```\n\n**检查位置**：`AddRadarPlugin::getBody()`、`getQueryString()`、业务 Plugin 的 `mergePayload()`。\n\n### 3. Webhook 签名验证\n\n**安全强制**：所有 CallbackPlugin 必须验签。\n\n| Provider | Trait 方法 | 必需配置字段 |\n|----------|------------|--------------|\n| Stripe | `StripeTrait::verifyStripeWebhookSign()` | `webhook_secret` |\n| PayPal | `PaypalTrait::verifyPaypalWebhookSign()` | `webhook_id` + OAuth |\n| 微信 | `WechatTrait::verifyWechatSign()` | `mch_secret_cert`、`wechat_public_cert_path`（可预置或运行时拉取） |\n| 抖音 | —（在 `CallbackPlugin::verifySign()` 中实现） | `mch_secret_token`、`mch_secret_salt` |\n| 银联 | `UnipayTrait::verifyUnipaySign()` | `unipay_public_cert_path` |\n| 支付宝 | `AlipayTrait::verifyAlipaySign()` | `alipay_public_cert_path` |\n\n**检查点**：\n- CallbackPlugin 是否调用 Trait 提供或 Plugin 自身实现的签名验证方法\n- 签名算法是否与官方文档一致（对照 `@see` 链接）\n- 配置缺失时抛异常\n- 签名为空时抛异常\n- 使用 `hash_equals` 防时序攻击\n\n### 4. 数组回调的处理\n\n**仅适用于 Stripe/Wechat/Paypal**（构造 `ServerRequest` 并验签）：\n\n`getCallbackParams()` 处理逻辑：\n\n| 输入类型 | 行为 |\n|----------|------|\n| `['body' => ..., 'headers' => ...]` | 构造带 headers 的 `ServerRequest`，**会验签** |\n| 纯数组（无 headers） | 构造无 headers 的 `ServerRequest`，验签时会抛 `SIGN_EMPTY` |\n| `ServerRequestInterface` | 直接使用，验签 |\n| `null` | 从 `ServerRequest::fromGlobals()` 获取 |\n\n**结论**：数组回调只有提供完整 `headers` + `body` 才能通过验签；否则验签阶段抛异常。\n\n**Alipay/Douyin/Unipay 不同**：\n- `getCallbackParams()` 返回 `Collection`（从 query/parsedBody 取值）\n- 直接 merge 到 params，不构造 `ServerRequest`\n- CallbackPlugin 从 params 中取值验签\n\n### 5. 加密资源解密（微信）\n\n微信回调的 `resource` 字段需解密：\n\n```php\n$body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config);\n```\n\n检查 `CallbackPlugin` 是否调用此方法。\n\n---\n\n## Phase 4: 架构一致性\n\n### 1. 插件管道骨架\n\n**通用骨架**：\n```\nStartPlugin → [前置插件] → 业务插件 → [后置插件] → ParserPlugin\n```\n\n**各 Provider 差异**：\n\n| Provider | 前置插件 | 后置插件 |\n|----------|----------|----------|\n| Stripe | 无 | `AddRadarPlugin` → `ResponsePlugin` |\n| PayPal | `ObtainAccessTokenPlugin` | `AddPayloadBodyPlugin` → `AddRadarPlugin` → `ResponsePlugin` |\n| 微信 | 无 | `AddPayloadBodyPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n| 支付宝 | 无 | `FormatPayloadBizContentPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n\n**注意**：不同 Provider 管道差异较大，不要按固定模板审查。\n\n### 2. mergeCommonPlugins 实现\n\nProvider 类必须实现此方法，返回完整管道：\n\n```php\npublic function mergeCommonPlugins(array $plugins): array\n{\n    return array_merge(\n        [StartPlugin::class, /* 前置插件 */],\n        $plugins,\n        [/* 后置插件 */, ParserPlugin::class],\n    );\n}\n```\n\n### 3. Trait 方法复用\n\n新增 Provider 应复用 Trait 方法而非重新实现：\n\n| 功能 | Trait 方法 |\n|------|------------|\n| URL 构建 | `get{Provider}Url()` |\n| 签名验证 | `verify{Provider}WebhookSign()` / `verify{Provider}Sign()` |\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()` |\n| 加密解密 | `WechatTrait::decryptWechatResource()` |\n\n### 4. Provider 常量定义\n\n必须定义 `URL` 常量：\n\n```php\npublic const URL = [\n    PayMode::NORMAL->value => 'https://api.xxx.com/',\n    PayMode::SANDBOX->value => 'https://sandbox.api.xxx.com/',\n    PayMode::SERVICE->value => 'https://api.xxx.com/',\n];\n```\n\n特殊常量（如微信）：\n- `AUTH_TAG_LENGTH_BYTE`\n- `MCH_SECRET_KEY_LENGTH_BYTE`\n\n### 5. Event 调用\n\ncallback 方法必须触发事件：\n\n```php\n// Stripe/Wechat/Paypal（ServerRequestInterface）\nEvent::dispatch(new CallbackReceived('provider', clone $request, $params, null));\n\n// Alipay/Douyin/Unipay/Jsb（Collection）\nEvent::dispatch(new CallbackReceived('provider', $request->all(), $params, null));\n```\n\n其他方法触发：\n\n```php\nEvent::dispatch(new MethodCalled('provider', __METHOD__, $order, null));\n```\n\n### 6. PHPStan ignore 注释\n\nTrait 静态方法调用需添加注释：\n\n```php\n/* @phpstan-ignore-next-line */\nself::verifyWechatSign(...);\n```\n\n这是 PHPStan 对 Trait 静态调用的已知限制，非代码质量问题。\n\n---\n\n## Phase 5: 官方文档对照\n\n结合代码中的 `@see` 链接验证：\n\n| # | 检查点 |\n|---|--------|\n| 1 | API 端点 URL 是否与官方一致 |\n| 2 | HTTP 方法（GET/POST）是否正确 |\n| 3 | 认证 Header 名称、格式是否正确 |\n| 4 | 请求/响应字段（必填/可选）是否正确 |\n| 5 | 签名算法、拼接顺序是否与官方完全一致 |\n| 6 | Base URL（production/sandbox）是否正确 |\n\n---\n\n## Phase 6: 测试覆盖\n\n| # | 检查项 |\n|---|--------|\n| 1 | 每个 Plugin 有对应测试 |\n| 2 | 必填参数缺失的异常测试 |\n| 3 | 可选参数缺失的边界测试 |\n| 4 | 多租户场景（`_config` 参数） |\n| 5 | HTTP client mock，禁止真实 API 调用 |\n| 6 | Callback 签名验证的正向/反向测试 |\n\n---\n\n## Phase 7: 文档完整性\n\n| # | 检查项 |\n|---|--------|\n| 1 | 官方链接可访问且指向正确端点 |\n| 2 | 示例代码使用原生 PHP，框架无关 |\n| 3 | 侧边栏已更新 |\n| 4 | CHANGELOG 已更新 |\n\n---\n\n## 常见误报\n\n### null-safe 操作符\n\n```php\n$payload?->get('a', $payload->get('b'))\n```\n\nPHP 8.0 的 null-safe 实现了 **full short-circuiting**：当 `$payload` 为 `null` 时，右侧参数不会被求值。这不是 bug。\n\n参考：[PHP RFC nullsafe_operator](https://wiki.php.net/rfc/nullsafe_operator)\n\n---\n\n## 避免重复造轮子\n\n检查新增功能是否在 `yansongda/supports` 或 `yansongda/artful` 中已有实现：\n\n| 功能 | 已有实现 |\n|------|----------|\n| UUID 生成 | `Str::uuidV4()` |\n| 字符串处理 | `Str::*` |\n| 集合操作 | `Collection::*` |\n| 空值过滤 | `filter_params()` (artful) |\n| HTTP 方法获取 | `get_radar_method()` (artful) |\n| Body 获取 | `get_radar_body()` (artful) |\n\n\n\n## Review 报告模板\n\n以下为提交 review 时的标准报告格式：\n\n````markdown\n# PR #{number} Code Review — {Provider} Provider\n\n## 一、总览\n\n| 维度 | 内容 |\n|------|------|\n| PR | #{number} — {title} |\n| 涉及文件 | {n} 个 |\n| Review 范围 | 全量代码 + 文档 + 测试 |\n| 方法 | 逐文件审查 + 官方文档对照 + 跨 Provider 一致性比对 |\n\n## 二、已确认 Bug（含证据链）\n\n### BUG-{n}: {简要描述}\n\n**严重级别**：{高/中高/中/低}\n\n**位置**：`{file_path}` L{line}\n\n**问题**：\n{详细描述问题本质}\n\n**证据链**：\n1. 代码现状：`{相关代码片段}`\n2. 预期行为：{应该怎样}\n3. 实际行为：{实际会怎样}\n4. 影响范围：{哪些场景会触发}\n\n**修复建议**：\n```php\n// 修复方案\n```\n\n---\n\n（重复以上格式，每个 Bug 单独一节）\n\n## 三、已排除的误报\n\n### {误报描述}\n\n**结论**：非 Bug\n\n**原因**：{详细解释，附 RFC/文档链接}\n\n## 四、风险项\n\n| # | 风险 | 严重级别 | 文件 | 说明 |\n|---|------|----------|------|------|\n| RISK-{n} | {风险名} | {级别} | `{file}` | {说明} |\n\n## 五、改进建议\n\n| # | 建议 | 优先级 | 文件 | 说明 |\n|---|------|--------|------|------|\n| SUG-{n} | {建议名} | {级别} | `{file}` | {说明} |\n\n## 六、官方文档对照验证\n\n| 功能 | 代码中的 URL/算法 | 官方文档 | 是否一致 |\n|------|-------------------|----------|----------|\n| {功能名} | `{代码实现}` | [官方文档]({url}) | ✅/❌ |\n\n## 七、代码规范检查\n\n| 检查项 | 状态 | 备注 |\n|--------|------|------|\n| `declare(strict_types=1)` | ✅/❌ | |\n| `use` 导入（无内联命名空间） | ✅/❌ | |\n| 日志格式 `[Provider][V{n}][Category][Plugin]` | ✅/❌ | |\n| 异常常量命名 | ✅/❌ | |\n| Plugin/Shortcut 命名规范 | ✅/❌ | |\n| 测试覆盖 | ✅/❌ | |\n\n## 八、汇总\n\n| 类别 | 数量 | 详情 |\n|------|------|------|\n| 已确认 Bug | {n} | {简要列举} |\n| 已排除误报 | {n} | {简要列举} |\n| 风险项 | {n} | {简要列举} |\n| 改进建议 | {n} | {简要列举} |\n\n## 九、结论\n\n{总体评价：代码质量、架构一致性、安全性等。明确给出 Approve / Request Changes 建议及原因。}\n\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n````\n","frontmatter":{"name":"pr-review-provider","description":"Use when reviewing PRs that add or modify a payment Provider in yansongda/pay - covers plugin pipeline, multi-tenant safety, signature verification, docs, and naming conventions."},"isInternal":false,"tokens":4505,"sizeBytes":13402},{"name":"SKILL.md","path":".agents/skills/releasing-php-package/SKILL.md","rawUrl":"https://raw.githubusercontent.com/yansongda/pay/HEAD/.agents/skills/releasing-php-package/SKILL.md","title":"releasing-php-package","category":"anthropic-skill","format":"markdown","content":"---\nname: releasing-php-package\ndescription: Use when preparing to publish a new version of a PHP Composer package and need to write or update CHANGELOG, upgrade guides, and documentation before tagging and releasing\n---\n\n# 发布 PHP Composer 包\n\n## 概述\n\n用于发布 PHP Composer 包的结构化流程，避免 CHANGELOG 格式混乱、升级指南缺失、文档示例过期等常见问题，并强制要求在打 tag 前通过独立分支 + PR 完成文档改动。\n\n**硬性约束（违反即流程无效）：**\n- Agent 只负责创建 PR，**严禁自行合并 PR**（包括 `gh pr merge`、平台合并 API 等任何形式的自动合并）\n- PR 必须由人工审核后合并；在获得人工「PR 已合并」确认前，Agent 不得打 tag、不得创建 Release\n\n## 使用场景\n\n- 准备发布新版本（patch/minor/major/beta/rc）\n- CHANGELOG 使用了非标准分类或前缀\n- 目标版本还没有对应的升级指南\n- 文档中包含已移除或已弃用功能的示例\n- 上次发布曾因文档问题被反馈\n\n**不要使用：**\n- 项目已有自动化发布工具（semantic-release、release-please）\n- 仅单文件改动且对用户无可见影响\n\n## 版本分类\n\n| 变更类型 | 版本号变化 | 是否需要升级指南 |\n|---|---|---|\n| 仅 Bug 修复 | Patch（1.0.X） | 否 |\n| 新功能，向后兼容 | Minor（1.X.0） | 是 |\n| 破坏性变更 | Major（X.0.0） | 是 |\n| 预发布（beta/rc/alpha） | 预发布版本（1.0.0-beta.1） | 是 |\n\n## 核心模式\n\n### 错误示例\n\n```markdown\n## v1.2.0 - 2024-01-01\n\n### BREAKING CHANGES\n- change: PHP version bumped to 8.2\n\n### Added\n- feat: add new provider support (#42)\n- feat: add new config system (#43)\n\n### Changed\n- fix: correct error code (#45)\n- chore: update dependencies (#46)\n```\n\n### 正确示例（符合 Keep a Changelog）\n\n```markdown\n## [v1.2.0] - 2024-01-01\n\n### Added\n- New payment provider support (#42)\n- Typed configuration objects replacing arrays (#43)\n\n### Changed\n- Minimum PHP version from 8.0 to 8.2 (#41)\n- All internal plugins migrated to trait-based architecture (#44)\n\n### Removed\n- Legacy helper functions file, migrated to static methods (#44)\n- Deprecated `getOldConfig()` functions, use `getProviderConfig()` (#41)\n```\n\n## 速查表\n\n| 任务 | 命令 / 文件 | 关键规则 |\n|---|---|---|\n| 查看当前分支 | `git branch --show-current` | 禁止在 `master`/`main` 上直接修改 CHANGELOG |\n| 创建发布分支 | `git checkout -b docs/changelog-vX.Y.Z` | 所有文档改动必须走 PR |\n| 列出自上一个 tag 以来的提交 | `git log --oneline --no-merges $(git describe --tags --abbrev=0)..HEAD` | 排除 merge commit |\n| 标准分类 | `Added` → `Changed` → `Deprecated` → `Removed` → `Fixed` → `Security` | 严格顺序，不新增分类 |\n| 去除前缀 | 移除 `feat:`、`fix:`、`refactor:`、`chore:` | 仅当存在时才移除；已干净的中文描述保持原样 |\n| 升级指南 | `docs/v{major}/upgrade/v{version}.md` 或实际等价路径 | 先探测真实 docs 路径 |\n| 更新导航 | 侧边栏 / 菜单配置文件 | 若 `.gitignore` 忽略 docs 目录，使用 `git add -f` |\n| 合并 PR | 由人工在平台审核后执行 | Agent 严禁自动合并（`gh pr merge` 等一律禁止） |\n| 创建 annotated tag | `git tag -a vX.Y.Z -m \"release: vX.Y.Z\"` | 必须在 PR 由人工合并后执行 |\n| 创建 GitHub Release | `gh release create vX.Y.Z --notes-file release-notes.md --prerelease` | beta/rc/alpha 需标记为 pre-release |\n\n## 执行步骤\n\n### 0. 分支隔离（关键）\n\n**在修改任何发布相关文件前，必须先创建独立分支。**\n\n```bash\n# 先确认当前分支\ngit branch --show-current\n\n# 如果在 master/main 上，立即切出发布分支\ngit checkout -b docs/changelog-vX.Y.Z\n```\n\n**禁止**在 `master`/`main` 上直接提交 CHANGELOG 或升级指南改动。\n\n### 1. 分析提交\n\n```bash\nlatest=$(git describe --tags --abbrev=0)\ngit log --oneline --no-merges ${latest}..HEAD\n```\n\n重点关注：PR 编号、破坏性变更、新功能、已弃用 / 已移除项。\n\n**仅文档发布判断：** 如果自上一个 tag 以来的代码变更已经在目标分支上，而你只是整理文档，则按「仅文档发布」处理。流程相同，但提交范围只包含 CHANGELOG / 升级指南 / 导航。\n\n**关键验证步骤：**\n分析完提交后，务必验证实际代码状态，避免把「新增后又移除」的功能写进发布说明：\n\n```bash\n# 验证文件增删\ngit diff ${latest}..HEAD --stat\n\n# 验证 src/ 下实际代码变更\ngit diff ${latest}..HEAD -- src/\n```\n\n**验证规则：**\n1. 对于 commit 消息中的「新增」项：\n   - 确认文件 / 类 / 方法在 HEAD 中仍然存在\n   - 如果后续 commit 已移除，则从发布说明中排除\n   - 示例：#1157 新增 NetworkException，但 #1161 又移除了 → 不要写进 Added\n\n2. 对于 commit 消息中的「移除」项：\n   - 确认文件 / 类 / 方法在 HEAD 中已不存在\n   - 如果仍然存在，则排除或改为「已弃用」\n\n3. 对于 commit 消息中的「变更」项：\n   - 确认变更在 HEAD 中真实存在\n   - 如果已被 revert，则从发布说明中排除\n\n**验证命令：**\n```bash\n# 确认文件存在于 HEAD\ngit show HEAD:path/to/file\n\n# 确认文件变更\ngit diff ${latest}..HEAD -- path/to/file\n```\n\n### 2. 规范化 CHANGELOG\n\n**分类顺序（严格）**：Added、Changed、Deprecated、Removed、Fixed、Security。\n\n**规则：**\n- 仅当存在 `feat:` / `fix:` / `refactor:` / `chore:` 等前缀时才移除\n- 不要重写已经是干净中文描述的条目\n- 将 `BREAKING CHANGES` 内容移入 `Changed` 或 `Removed`\n- 合并重复条目（如同一文件被删除两次）\n- 保留 PR 引用 `(#1234)` 以便追溯\n- 子列表缩进 2 个空格\n\n**版本标题格式：**\n```markdown\n## [v1.2.0] - 2024-01-01\n```\n\n### 3. 编写升级指南\n\n**先探测真实 docs 路径。** 常见位置：\n- `docs/v{major}/upgrade/v{version}.md`\n- `web/docs/v{major}/upgrade/v{version}.md`\n- `src/docs/v{major}/upgrade/v{version}.md`\n\n如果目标升级指南已存在（例如后续 beta 版本），则在原有基础上追加新内容，不要整篇替换。\n\n**章节结构：**\n1. `## 重点检查`\n   - `### 运行环境` — PHP 版本、扩展、其他运行时要求\n   - `### 简单使用者` — **仅 end-user 可见变更**（命名空间变化、移除的方法、配置调整）。不要罗列内部重构（内部辅助函数重组、不影响公共 API 的基类变化、代码组织调整）。\n   - `### 自有插件开发者` — 影响自定义插件或扩展的内部架构变更（新基类、方法签名变化、移除的内部工具）。\n2. `## 更改版本号` — `composer require vendor/package:~{version}`\n3. `### BREAKING CHANGES`（如有）— 完整破坏性变更列表\n\n### 4. 清理过期示例\n\n搜索文档中已移除或已弃用功能的使用：\n```bash\ngrep -rn \"deprecatedMethod\\|removedClass\\|oldNamespace\" docs/\n```\n\n**规则：** 删除已移除功能的示例。不要保留「兼容两种写法」的双示例。文档只反映当前版本。\n\n### 5. 更新导航\n\n将新升级指南链接加入文档导航配置（如侧边栏、菜单、索引文件）。\n\n**注意：** 如果 `.gitignore` 包含 docs 目录匹配规则，新文件需用 `git add -f` 强制添加。\n\n### 6. PR → Tag → Release\n\n**创建 PR：**\n```bash\ngit add -f CHANGELOG.md docs/  # 或 web/docs/，按实际路径调整\n\ngit commit -m \"docs: update CHANGELOG and upgrade guide for vX.Y.Z\"\n\ngit push -u origin docs/changelog-vX.Y.Z\n\n# 通过 GitHub / GitLab / Gitea 等平台创建 PR\n```\n\n**⛔ 流程暂停点：创建 PR 后必须停止，等待人工审核合并**\n\n- 告知用户 PR 链接，明确说明等待人工审核合并，然后结束当前步骤\n- **严禁**执行 `gh pr merge`（含 --squash/--merge/--rebase）、平台合并 API 等任何自动合并操作\n- 后续步骤（push tag、创建 Release）即使已获授权，也不包含 PR 合并权；合并永远由人工执行\n\n**人工合并后（继续前必须确认 PR 已由人工合并）：**\n```bash\n# 核实 PR 状态（state 应为 MERGED，mergedBy 应为人工账号）\ngh pr view <PR编号> --json state,mergedAt,mergedBy\ngit fetch origin\ngit log --oneline origin/master | head -5\n\n# 打 tag 并推送\ngit checkout master && git pull origin master\ngit tag -a vX.Y.Z -m \"release: vX.Y.Z\"\ngit push origin vX.Y.Z\n```\n\n**创建 Release（以 GitHub 为例）：**\n```bash\n# 从 CHANGELOG 提取当前版本段落\nsed -n '/^## vX.Y.Z/,/^## v/p' CHANGELOG.md | sed '$d' > release-notes.md\n\n# beta/rc/alpha 版本需添加 --prerelease\ngh release create vX.Y.Z --title \"vX.Y.Z\" --notes-file release-notes.md --prerelease\n```\n\n## 常见错误\n\n| 错误 | 原因 | 修正 |\n|---|---|---|\n| 直接在 master 提交 CHANGELOG | 跳过分支隔离 | 必须先创建 `docs/changelog-vX.Y.Z` 分支 |\n| CHANGELOG 保留 `feat:`/`fix:` 前缀 | 从 commit 消息复制粘贴 | 仅在有前缀时剥离；保持已有干净描述 |\n| 内部重构写入「简单使用者」 | 未区分用户可见变更与内部变更 | 移到「自有插件开发者」或直接删除 |\n| 文档保留新旧双示例 | 试图在文档中保持向后兼容 | 删除旧示例；文档只反映当前版本 |\n| 忘记更新导航 | 创建升级指南后未注册 | 立即加入侧边栏 / 菜单 |\n| PR 未合并就创建 tag | 急于完成发布 | 先确认 `origin/master` 包含合并提交 |\n| 使用 lightweight tag | `git tag` 缺少 `-a` | 始终使用 `git tag -a` 并附带 message |\n| Release notes 与 CHANGELOG 不一致 | 单独编写 release notes | 从 CHANGELOG 提取或复制 |\n| 预发布版本未标记 pre-release | 忘记 beta/rc/alpha 后缀 | 按版本后缀在发布平台勾选 pre-release |\n| 写入「新增后又移除」的功能 | 仅依赖 commit 消息未验证代码 | 用 `git diff` 确认功能在 HEAD 中真实存在 |\n| Agent 自行合并 PR | 误将「发布流程授权」当作「合并授权」 | 如需撤销：`git revert <merge-commit>`；此后一律仅创建 PR 并等待人工合并 |\n","frontmatter":{"name":"releasing-php-package","description":"Use when preparing to publish a new version of a PHP Composer package and need to write or update CHANGELOG, upgrade guides, and documentation before tagging and releasing"},"isInternal":false,"tokens":3466,"sizeBytes":10171}],"systemPromptSnippet":"<yakaai_skills repo=\"yansongda/pay\">\n<!-- File: AGENTS.md (Tokens: ~1013 | Category: root-instruction) -->\n# yansongda/pay AGENTS.md\n\n## OVERVIEW\nPHP 支付 SDK，支持支付宝、微信、银联、抖音、江苏银行、PayPal、Stripe。基于插件管道架构，关注支付流程、回调验签和 Provider 扩展一致性。\n\n## STRUCTURE\n```\nsrc/\n├── Config/           # Typed Config（WechatConfig、AlipayConfig 等）\n├── Event/            # PSR-14 事件\n├── Plugin/{Provider}/V{n}/\n├── Provider/         # Alipay、Wechat 等\n├── Shortcut/{Provider}/\n├── Traits/           # {Provider}Trait、ProviderConfigTrait\n├── CertManager.php   # 证书缓存\n└── Pay.php\n```\n\n## 核心组件\n| 符号 | 位置 | 用途 |\n|---|---|---|\n| `Pay::alipay()` | `Pay.php` | 支付入口 |\n| `Pay::wechat()` | `Pay.php` | 微信入口 |\n| `ProviderInterface` | `Contract/` | 核心接口（pay/query/cancel/close/refund/callback/success） |\n| `Rocket` | `Yansongda\\Artful\\Rocket` | 承载 params/payload/radar/destination |\n| `CertManager` | `CertManager.php` | 证书读取与缓存 |\n| `Event` | `Event.php` | PSR-14 事件分发 |\n| `{Provider}Config` | `Config/` | 类型化配置对象 |\n\n## 核心原则\n- **安全第一**：回调/Webhook 必须验签，禁止直接信任传入数据\n- **代码一致性**：新增 Provider 遵循现有结构\n\n## COMMANDS & 开发规范\n详见 `.agents/skills/dev-guide/SKILL.md`，使用时加载。\n\n## 架构要点\n- **插件管道**：`StartPlugin → [前置] → 业务插件 → [后置] → ParserPlugin`\n- **动态调用**：Provider 通过 `__call` 调用 Shortcut，再进入管道\n- **配置系统**：`Config` 类自动将数组配置转为 `{Provider}Config` 对象\n- **事件系统**：`PayListener` 桥接 Artful 事件 → Pay 事件\n\n## 签名验证（必须）\n| Provider | 方式 | Trait 方法 |\n|---|---|---|\n| 支付宝 | RSA 本地验证 | `AlipayTrait::verifyAlipaySign()` |\n| 微信 | 证书签名本地验证 | `WechatTrait::verifyWechatSign()` |\n| PayPal | API 验证 | `PaypalTrait::verifyPaypalWebhookSign()` |\n| Stripe | HMAC-SHA256 本地 | `StripeTrait::verifyStripeWebhookSign()` |\n| 银联 | 证书签名本地验证 | `UnipayTrait::verifyUnipaySign()` |\n| 江苏银行 | RSA 本地验证 | `JsbTrait::verifyJsbSign()` |\n| 抖音 | SHA1 本地验证 | `CallbackPlugin::verifySign()` |\n| 通联支付 | RSA-SHA1 本地验证 | `AllinpayTrait::verifyAllinpaySign()` |\n\n回调处理差异：\n- **Wechat/Stripe/Paypal**：传 `_request`（ServerRequestInterface）到 CallbackPlugin\n- **Alipay/Douyin/Unipay/Jsb/Allinpay**：通过 `getCallbackParams()` 获取 Collection\n\n## Trait 参考\n| 用途 | 方法（示例） |\n|---|---|\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()`、`getRadarUrl()` |\n| URL 构建 | `get{Provider}Url()`（各 Trait） |\n| 签名验证 | `verify{Provider}Sign()`、`verify{Provider}WebhookSign()` |\n| 证书管理 | `CertManager::getPublicCert()`、`getPrivateCert()` |\n| 微信专用 | `decryptWechatResource()`、`reloadWechatPublicCerts()` |\n\n> 完整方法列表见 `src/Traits/{Provider}Trait.php`\n\n<!-- File: .agents/skills/container-dev/SKILL.md (Tokens: ~1608 | Category: anthropic-skill) -->\n---\nname: container-dev\ndescription: \"Use when local PHP environment is unavailable. Fallback container-based development environment for yansongda/pay project.\"\n---\n\n# Container-based Local Development (Fallback)\n\n当本地没有 PHP 环境时，使用 Apple Container 或 Docker 作为备选方案。\n\n## 使用原则\n\n**优先本地环境**：先检查本地是否有 PHP/composer，有则直接使用。\n\n**容器作为备选**：仅在本地环境不可用时使用容器。\n\n## 快速参考\n\n### 镜像\n\n```\nregistry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine\n```\n\n### 常用命令\n\n| 任务 | 命令 |\n|------|------|\n| 运行测试 | `composer test` |\n| PHPStan 分析 | `composer analyse` |\n| 代码风格检查 | `composer cs-fix`（**仅检查，不修复**） |\n| 代码风格修复 | `php-cs-fixer fix ./src` |\n| Composer 更新 | `composer update --with-all-dependencies` |\n| 安装依赖 | `composer install` |\n| Web 文档开发 | `pnpm web:dev`（需本地 Node 或 Node 容器） |\n| Web 文档构建 | `pnpm web:build`（需本地 Node 或 Node 容器） |\n\n---\n\n## Apple Container\n\n### 基本模板\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  COMMAND\n```\n\n### DNS 问题处理\n\n若出现 DNS 解析失败（`198.18.0.x` 地址、连接超时），添加 DNS fix：\n\n```bash\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMMAND\"\n```\n\n### PHP 命令示例\n\n```bash\n# 测试（需 DNS fix + COMPOSER_ALLOW_SUPERUSER）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer test\"\n\n# PHPStan 分析\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer analyse\"\n\n# 代码风格检查（仅查看差异）\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 composer cs-fix\"\n\n# 代码风格修复\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && COMPOSER_ALLOW_SUPERUSER=1 php-cs-fixer fix ./src\"\n\n# Composer update\ncontainer run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  sh -c \"echo 'nameserver 8.8.8.8' > /etc/resolv.conf && composer update --with-all-dependencies\"\n```\n\n### Web 文档命令\n\nWeb 文档使用 pnpm，镜像中无 Node.js，需使用 Node 镜像或本地运行。\n\n**注意**：`web/package.json` 要求 Node `>=20.12.0`，请确保使用的镜像版本满足此约束。\n\n```bash\n# 方案1: 本地运行（推荐）\ncd web && pnpm web:dev\n\n# 方案2: 使用 Node 容器（版本 >=20.12.0）\ndocker run --rm -v \"$(pwd)/web\":/app -w /app \\\n  node:20-alpine \\\n  sh -c \"npm install -g pnpm && pnpm install && pnpm web:dev\"\n```\n\n---\n\n## Docker\n\nDocker 命令类似，通常不需要 DNS fix：\n\n```bash\n# 测试\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer test\n\n# PHPStan 分析\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer analyse\n\n# 代码风格检查\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  composer cs-fix\n\n# 代码风格修复\ndocker run --rm -v \"$(pwd)\":/app -w /app \\\n  registry.cn-shenzhen.aliyuncs.com/yansongda/php:cli-8.5-alpine \\\n  php-cs-fixer fix ./src\n```\n\n---\n\n## DNS 问题诊断（Apple Container）\n\n**症状**：\n- `curl error 28 Connection timed out`\n- DNS 解析返回 `198.18.0.x`（基准测试保留地址）\n\n**原因**：Container 系统 DNS 解析器异常。\n\n**解决**：手动写入 `nameserver 8.8.8.8` 到 `/etc/resolv.conf`。\n\n---\n\n## 注意事项\n\n- `COMPOSER_ALLOW_SUPERUSER=1`：容器内运行 composer 脚本必须\n- `composer cs-fix`：**仅检查并显示差异**，不自动修复\n- `php-cs-fixer fix ./src`：实际修复代码风格\n- Apple Container 需先启动：`container system start`\n- Docker 需确保 Docker Desktop 或 daemon 已启动\n- 容器内网络可能无法访问某些 CDN，测试中可能出现 PHP warning（不影响核心功能）\n\n<!-- File: .agents/skills/dev-guide/SKILL.md (Tokens: ~639 | Category: anthropic-skill) -->\n---\nname: dev-guide\ndescription: Use when developing, testing, or adding new providers to yansongda/pay project. Covers commands, code standards, and provider implementation guide.\n---\n\n# yansongda/pay 开发指南\n\n## 开发命令\n\n**对 `src/` PHP 代码的任何修改，需运行以下三项检查：**\n\n```bash\ncomposer cs-fix && composer analyse && composer test\n```\n\n### 测试\n```bash\ncomposer test       # PHPUnit 11.x + Mockery 1.6\n```\n- 测试前需安装 `hyperf/pimple`\n- Mock HTTP 客户端避免真实请求\n\n### 代码风格\n```bash\ncomposer cs-fix     # 代码格式化检查（dry-run）\n```\n\n### 静态分析\n```bash\ncomposer analyse    # PHPStan\n```\n\n### 文档\n```bash\ncd web && pnpm web:dev    # 文档开发\ncd web && pnpm web:build  # 文档构建\ncd web && pnpm web:serve  # 本地预览\n```\n\n### 本地开发环境\n优先本地 PHP 环境；若无 PHP，使用 Container 作为备选，详见 `container-dev` Skill。\n\n### CI 矩阵\nPHP 8.2-8.5 + Laravel/Hyperf/Default\n\n---\n\n## 代码规范\n\n- `declare(strict_types=1);` 必须\n- `use` 导入，禁止 `\\Yansongda\\Pay\\...`\n- 日志/异常消息用中文\n- 命名：`{Action}Plugin.php`、`{Action}Shortcut.php`、`{Provider}Trait.php`\n\n---\n\n## 新增 Provider 流程\n\n### 6 阶段\n1. **配置**：`src/Config/{Provider}Config.php`\n2. **核心类**：`src/Provider/{Provider}.php` + `src/Service/{Provider}ServiceProvider.php`（继承 AbstractServiceProvider）\n3. **插件**：`src/Plugin/{Provider}/V{n}/`（含 CallbackPlugin）\n4. **快捷方式**：`src/Shortcut/{Provider}/{Action}Shortcut.php`\n5. **Trait**：`src/Traits/{Provider}Trait.php`（`get{Provider}Url`、`verify{Provider}Sign`）\n6. **注册**：`Pay.php` + `Config.php` 映射 + `Exception.php` 常量 + 测试 + 文档\n\n---\n\n## 常见错误\n\n- 忽略 `declare(strict_types=1);`\n- 直接写完整命名空间\n- 回调未验签\n- 测试未 Mock HTTP 客户端\n\n<!-- File: .agents/skills/pr-review-provider/SKILL.md (Tokens: ~4505 | Category: anthropic-skill) -->\n---\nname: pr-review-provider\ndescription: \"Use when reviewing PRs that add or modify a payment Provider in yansongda/pay - covers plugin pipeline, multi-tenant safety, signature verification, docs, and naming conventions.\"\n---\n\n# PR Review Checklist — Payment Provider\n\nyansongda/pay 新增/修改 Provider 时的 Code Review 专用检查清单。基于 Airwallex PR #1140 review 经验沉淀。\n\n## Review 规范（强制）\n\n### 1. 提交评论前必须征求用户许可\n\n**绝对禁止**：未经用户明确允许，不得使用 `gh pr comment` 或其他方式向 PR 提交任何评论。\n\n**正确流程**：\n1. 完成 review 分析\n2. 向用户展示 review 报告内容\n3. 询问用户：\"是否需要我将此报告提交为 PR 评论？\"\n4. 仅在用户明确确认后才执行提交\n\n### 2. 报告末尾必须注明使用的模型及审核状态\n\n在 review 报告的结论部分之后，必须添加以下信息：\n\n```markdown\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n```\n\n示例：\n```markdown\n---\n*Review by deepseek-v4-pro | 2026-05-07 | 经人工审核确认*\n```\n\n---\n\n## Review 流程\n\n按以下阶段顺序审查，确保覆盖完整：\n\n1. **Phase 1**: Provider 结构完整性\n2. **Phase 2**: 代码规范检查\n3. **Phase 3**: 安全性检查（签名验证、加密解密）\n4. **Phase 4**: 架构一致性（管道、Trait、Event）\n5. **Phase 5**: 官方文档对照\n6. **Phase 6**: 测试覆盖\n7. **Phase 7**: 文档完整性\n\n---\n\n## Phase 1: Provider 结构完整性\n\n对照以下清单逐项检查：\n\n| # | 检查项 | 位置 | 说明 |\n|---|--------|------|------|\n| 1 | 插件 | `src/Plugin/{Provider}/V{n}/` | 按版本组织 |\n| 2 | Provider 类 | `src/Provider/{Provider}.php` | 实现 `ProviderInterface` |\n| 3 | 服务提供者 | `src/Service/{Provider}ServiceProvider.php` | 服务注册 |\n| 4 | 快捷方式 | `src/Shortcut/{Provider}/` | `{Method}Shortcut.php` |\n| 5 | Trait 方法 | `src/Traits/{Provider}Trait.php` | `get{Provider}Url`、`verify{Provider}WebhookSign` 等 |\n| 6 | Provider 注册 | `src/Pay.php` | 添加 `{Provider}::class` 和入口方法 |\n| 7 | 异常常量 | `src/Exception/Exception.php` | `PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*` |\n| 8 | 测试 | `tests/` | 与源码结构对应 |\n| 9 | 文档 | `web/docs/v3/{provider}/` | VitePress 文档 |\n| 10 | 侧边栏/CHANGELOG | `web/.vitepress/sidebar/v3.js`、`CHANGELOG.md` | 更新配置 |\n\n**注意**：`src/Functions.php` 已不存在，URL/签名等方法已下沉到 `src/Traits/*Trait.php`。\n\n---\n\n## Phase 2: 代码规范检查\n\n### 基本规范\n\n| 检查项 | 要求 |\n|--------|------|\n| `declare(strict_types=1)` | 每个文件必须有 |\n| `use` 导入 | 除动态类名字符串/反射场景外，禁止直接写完整命名空间（如 `\\Yansongda\\Pay\\...`） |\n| 多行条件 | `&&` / `||` 放在续行开头 |\n\n### 命名规范\n\n| 类型 | 格式 | 示例 |\n|------|------|------|\n| 插件 | `{Action}Plugin.php` | `PayPlugin`、`RefundPlugin` |\n| 快捷方式 | `{Method}Shortcut.php` | `WebShortcut`、`QueryShortcut` |\n| Provider | `{ProviderName}.php` | `Paypal.php`、`Stripe.php` |\n| ServiceProvider | `{ProviderName}ServiceProvider.php` | `PaypalServiceProvider.php` |\n| Trait | `{Provider}Trait.php` | `WechatTrait`、`StripeTrait` |\n| 命名空间 | `Yansongda\\Pay\\Plugin\\{Provider}\\V{n}\\Pay\\{Plugin}` | 版本号与 API 版本一致 |\n\n### 日志与异常\n\n- 日志格式：`[Provider][V{n}][Category][Plugin]`，使用中文消息\n- 异常常量：`PARAMS_{PROVIDER}_*`、`CONFIG_{PROVIDER}_*`\n- 异常消息：中文，附带上下文参数\n\n---\n\n## Phase 3: 安全性检查\n\n### 1. params vs payload 混淆\n\n**高危**：`Artful::artful()` 第二参数必须是 `params`（含 `_config`），不能是 `payload`。\n\n```php\n// ❌ 错误 — payload 不含 _config，多租户必崩\n$result = Artful::artful([...], $confirmPayload);\n\n// ✅ 正确 — params 含 _config 租户标识\n$result = Artful::artful([...], $confirmParams);\n```\n\n**检查方法**：搜索 `Artful::artful(` 调用，追踪第二参数来源。\n\n### 2. 空值处理\n\n检查最终发送的 body/query 是否包含不应出现的 `null`/空字段。\n\n**推荐方式**：\n- 优先使用 `filter_params()` 函数（artful 库提供）\n- 嵌套数组场景补充 `array_filter()`\n\n```php\n// ✅ 优先方式 — filter_params\n$body = http_build_query(filter_params($payload)->toArray());\n\n// ✅ 嵌套场景 — array_filter\n$rocket->mergePayload(array_filter([\n    'application_context' => $payload->get('application_context'),\n], static fn ($value) => !is_null($value)));\n```\n\n**检查位置**：`AddRadarPlugin::getBody()`、`getQueryString()`、业务 Plugin 的 `mergePayload()`。\n\n### 3. Webhook 签名验证\n\n**安全强制**：所有 CallbackPlugin 必须验签。\n\n| Provider | Trait 方法 | 必需配置字段 |\n|----------|------------|--------------|\n| Stripe | `StripeTrait::verifyStripeWebhookSign()` | `webhook_secret` |\n| PayPal | `PaypalTrait::verifyPaypalWebhookSign()` | `webhook_id` + OAuth |\n| 微信 | `WechatTrait::verifyWechatSign()` | `mch_secret_cert`、`wechat_public_cert_path`（可预置或运行时拉取） |\n| 抖音 | —（在 `CallbackPlugin::verifySign()` 中实现） | `mch_secret_token`、`mch_secret_salt` |\n| 银联 | `UnipayTrait::verifyUnipaySign()` | `unipay_public_cert_path` |\n| 支付宝 | `AlipayTrait::verifyAlipaySign()` | `alipay_public_cert_path` |\n\n**检查点**：\n- CallbackPlugin 是否调用 Trait 提供或 Plugin 自身实现的签名验证方法\n- 签名算法是否与官方文档一致（对照 `@see` 链接）\n- 配置缺失时抛异常\n- 签名为空时抛异常\n- 使用 `hash_equals` 防时序攻击\n\n### 4. 数组回调的处理\n\n**仅适用于 Stripe/Wechat/Paypal**（构造 `ServerRequest` 并验签）：\n\n`getCallbackParams()` 处理逻辑：\n\n| 输入类型 | 行为 |\n|----------|------|\n| `['body' => ..., 'headers' => ...]` | 构造带 headers 的 `ServerRequest`，**会验签** |\n| 纯数组（无 headers） | 构造无 headers 的 `ServerRequest`，验签时会抛 `SIGN_EMPTY` |\n| `ServerRequestInterface` | 直接使用，验签 |\n| `null` | 从 `ServerRequest::fromGlobals()` 获取 |\n\n**结论**：数组回调只有提供完整 `headers` + `body` 才能通过验签；否则验签阶段抛异常。\n\n**Alipay/Douyin/Unipay 不同**：\n- `getCallbackParams()` 返回 `Collection`（从 query/parsedBody 取值）\n- 直接 merge 到 params，不构造 `ServerRequest`\n- CallbackPlugin 从 params 中取值验签\n\n### 5. 加密资源解密（微信）\n\n微信回调的 `resource` 字段需解密：\n\n```php\n$body['resource'] = self::decryptWechatResource($body['resource'] ?? [], $config);\n```\n\n检查 `CallbackPlugin` 是否调用此方法。\n\n---\n\n## Phase 4: 架构一致性\n\n### 1. 插件管道骨架\n\n**通用骨架**：\n```\nStartPlugin → [前置插件] → 业务插件 → [后置插件] → ParserPlugin\n```\n\n**各 Provider 差异**：\n\n| Provider | 前置插件 | 后置插件 |\n|----------|----------|----------|\n| Stripe | 无 | `AddRadarPlugin` → `ResponsePlugin` |\n| PayPal | `ObtainAccessTokenPlugin` | `AddPayloadBodyPlugin` → `AddRadarPlugin` → `ResponsePlugin` |\n| 微信 | 无 | `AddPayloadBodyPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n| 支付宝 | 无 | `FormatPayloadBizContentPlugin` → `AddPayloadSignaturePlugin` → `AddRadarPlugin` → `VerifySignaturePlugin` → `ResponsePlugin` |\n\n**注意**：不同 Provider 管道差异较大，不要按固定模板审查。\n\n### 2. mergeCommonPlugins 实现\n\nProvider 类必须实现此方法，返回完整管道：\n\n```php\npublic function mergeCommonPlugins(array $plugins): array\n{\n    return array_merge(\n        [StartPlugin::class, /* 前置插件 */],\n        $plugins,\n        [/* 后置插件 */, ParserPlugin::class],\n    );\n}\n```\n\n### 3. Trait 方法复用\n\n新增 Provider 应复用 Trait 方法而非重新实现：\n\n| 功能 | Trait 方法 |\n|------|------------|\n| URL 构建 | `get{Provider}Url()` |\n| 签名验证 | `verify{Provider}WebhookSign()` / `verify{Provider}Sign()` |\n| 配置获取 | `ProviderConfigTrait::getProviderConfig()`、`getTenant()` |\n| 加密解密 | `WechatTrait::decryptWechatResource()` |\n\n### 4. Provider 常量定义\n\n必须定义 `URL` 常量：\n\n```php\npublic const URL = [\n    PayMode::NORMAL->value => 'https://api.xxx.com/',\n    PayMode::SANDBOX->value => 'https://sandbox.api.xxx.com/',\n    PayMode::SERVICE->value => 'https://api.xxx.com/',\n];\n```\n\n特殊常量（如微信）：\n- `AUTH_TAG_LENGTH_BYTE`\n- `MCH_SECRET_KEY_LENGTH_BYTE`\n\n### 5. Event 调用\n\ncallback 方法必须触发事件：\n\n```php\n// Stripe/Wechat/Paypal（ServerRequestInterface）\nEvent::dispatch(new CallbackReceived('provider', clone $request, $params, null));\n\n// Alipay/Douyin/Unipay/Jsb（Collection）\nEvent::dispatch(new CallbackReceived('provider', $request->all(), $params, null));\n```\n\n其他方法触发：\n\n```php\nEvent::dispatch(new MethodCalled('provider', __METHOD__, $order, null));\n```\n\n### 6. PHPStan ignore 注释\n\nTrait 静态方法调用需添加注释：\n\n```php\n/* @phpstan-ignore-next-line */\nself::verifyWechatSign(...);\n```\n\n这是 PHPStan 对 Trait 静态调用的已知限制，非代码质量问题。\n\n---\n\n## Phase 5: 官方文档对照\n\n结合代码中的 `@see` 链接验证：\n\n| # | 检查点 |\n|---|--------|\n| 1 | API 端点 URL 是否与官方一致 |\n| 2 | HTTP 方法（GET/POST）是否正确 |\n| 3 | 认证 Header 名称、格式是否正确 |\n| 4 | 请求/响应字段（必填/可选）是否正确 |\n| 5 | 签名算法、拼接顺序是否与官方完全一致 |\n| 6 | Base URL（production/sandbox）是否正确 |\n\n---\n\n## Phase 6: 测试覆盖\n\n| # | 检查项 |\n|---|--------|\n| 1 | 每个 Plugin 有对应测试 |\n| 2 | 必填参数缺失的异常测试 |\n| 3 | 可选参数缺失的边界测试 |\n| 4 | 多租户场景（`_config` 参数） |\n| 5 | HTTP client mock，禁止真实 API 调用 |\n| 6 | Callback 签名验证的正向/反向测试 |\n\n---\n\n## Phase 7: 文档完整性\n\n| # | 检查项 |\n|---|--------|\n| 1 | 官方链接可访问且指向正确端点 |\n| 2 | 示例代码使用原生 PHP，框架无关 |\n| 3 | 侧边栏已更新 |\n| 4 | CHANGELOG 已更新 |\n\n---\n\n## 常见误报\n\n### null-safe 操作符\n\n```php\n$payload?->get('a', $payload->get('b'))\n```\n\nPHP 8.0 的 null-safe 实现了 **full short-circuiting**：当 `$payload` 为 `null` 时，右侧参数不会被求值。这不是 bug。\n\n参考：[PHP RFC nullsafe_operator](https://wiki.php.net/rfc/nullsafe_operator)\n\n---\n\n## 避免重复造轮子\n\n检查新增功能是否在 `yansongda/supports` 或 `yansongda/artful` 中已有实现：\n\n| 功能 | 已有实现 |\n|------|----------|\n| UUID 生成 | `Str::uuidV4()` |\n| 字符串处理 | `Str::*` |\n| 集合操作 | `Collection::*` |\n| 空值过滤 | `filter_params()` (artful) |\n| HTTP 方法获取 | `get_radar_method()` (artful) |\n| Body 获取 | `get_radar_body()` (artful) |\n\n\n\n## Review 报告模板\n\n以下为提交 review 时的标准报告格式：\n\n````markdown\n# PR #{number} Code Review — {Provider} Provider\n\n## 一、总览\n\n| 维度 | 内容 |\n|------|------|\n| PR | #{number} — {title} |\n| 涉及文件 | {n} 个 |\n| Review 范围 | 全量代码 + 文档 + 测试 |\n| 方法 | 逐文件审查 + 官方文档对照 + 跨 Provider 一致性比对 |\n\n## 二、已确认 Bug（含证据链）\n\n### BUG-{n}: {简要描述}\n\n**严重级别**：{高/中高/中/低}\n\n**位置**：`{file_path}` L{line}\n\n**问题**：\n{详细描述问题本质}\n\n**证据链**：\n1. 代码现状：`{相关代码片段}`\n2. 预期行为：{应该怎样}\n3. 实际行为：{实际会怎样}\n4. 影响范围：{哪些场景会触发}\n\n**修复建议**：\n```php\n// 修复方案\n```\n\n---\n\n（重复以上格式，每个 Bug 单独一节）\n\n## 三、已排除的误报\n\n### {误报描述}\n\n**结论**：非 Bug\n\n**原因**：{详细解释，附 RFC/文档链接}\n\n## 四、风险项\n\n| # | 风险 | 严重级别 | 文件 | 说明 |\n|---|------|----------|------|------|\n| RISK-{n} | {风险名} | {级别} | `{file}` | {说明} |\n\n## 五、改进建议\n\n| # | 建议 | 优先级 | 文件 | 说明 |\n|---|------|--------|------|------|\n| SUG-{n} | {建议名} | {级别} | `{file}` | {说明} |\n\n## 六、官方文档对照验证\n\n| 功能 | 代码中的 URL/算法 | 官方文档 | 是否一致 |\n|------|-------------------|----------|----------|\n| {功能名} | `{代码实现}` | [官方文档]({url}) | ✅/❌ |\n\n## 七、代码规范检查\n\n| 检查项 | 状态 | 备注 |\n|--------|------|------|\n| `declare(strict_types=1)` | ✅/❌ | |\n| `use` 导入（无内联命名空间） | ✅/❌ | |\n| 日志格式 `[Provider][V{n}][Category][Plugin]` | ✅/❌ | |\n| 异常常量命名 | ✅/❌ | |\n| Plugin/Shortcut 命名规范 | ✅/❌ | |\n| 测试覆盖 | ✅/❌ | |\n\n## 八、汇总\n\n| 类别 | 数量 | 详情 |\n|------|------|------|\n| 已确认 Bug | {n} | {简要列举} |\n| 已排除误报 | {n} | {简要列举} |\n| 风险项 | {n} | {简要列举} |\n| 改进建议 | {n} | {简要列举} |\n\n## 九、结论\n\n{总体评价：代码质量、架构一致性、安全性等。明确给出 Approve / Request Changes 建议及原因。}\n\n---\n*Review by {模型名称} | {YYYY-MM-DD} | 经人工审核确认*\n````\n\n<!-- File: .agents/skills/releasing-php-package/SKILL.md (Tokens: ~3466 | Category: anthropic-skill) -->\n---\nname: releasing-php-package\ndescription: Use when preparing to publish a new version of a PHP Composer package and need to write or update CHANGELOG, upgrade guides, and documentation before tagging and releasing\n---\n\n# 发布 PHP Composer 包\n\n## 概述\n\n用于发布 PHP Composer 包的结构化流程，避免 CHANGELOG 格式混乱、升级指南缺失、文档示例过期等常见问题，并强制要求在打 tag 前通过独立分支 + PR 完成文档改动。\n\n**硬性约束（违反即流程无效）：**\n- Agent 只负责创建 PR，**严禁自行合并 PR**（包括 `gh pr merge`、平台合并 API 等任何形式的自动合并）\n- PR 必须由人工审核后合并；在获得人工「PR 已合并」确认前，Agent 不得打 tag、不得创建 Release\n\n## 使用场景\n\n- 准备发布新版本（patch/minor/major/beta/rc）\n- CHANGELOG 使用了非标准分类或前缀\n- 目标版本还没有对应的升级指南\n- 文档中包含已移除或已弃用功能的示例\n- 上次发布曾因文档问题被反馈\n\n**不要使用：**\n- 项目已有自动化发布工具（semantic-release、release-please）\n- 仅单文件改动且对用户无可见影响\n\n## 版本分类\n\n| 变更类型 | 版本号变化 | 是否需要升级指南 |\n|---|---|---|\n| 仅 Bug 修复 | Patch（1.0.X） | 否 |\n| 新功能，向后兼容 | Minor（1.X.0） | 是 |\n| 破坏性变更 | Major（X.0.0） | 是 |\n| 预发布（beta/rc/alpha） | 预发布版本（1.0.0-beta.1） | 是 |\n\n## 核心模式\n\n### 错误示例\n\n```markdown\n## v1.2.0 - 2024-01-01\n\n### BREAKING CHANGES\n- change: PHP version bumped to 8.2\n\n### Added\n- feat: add new provider support (#42)\n- feat: add new config system (#43)\n\n### Changed\n- fix: correct error code (#45)\n- chore: update dependencies (#46)\n```\n\n### 正确示例（符合 Keep a Changelog）\n\n```markdown\n## [v1.2.0] - 2024-01-01\n\n### Added\n- New payment provider support (#42)\n- Typed configuration objects replacing arrays (#43)\n\n### Changed\n- Minimum PHP version from 8.0 to 8.2 (#41)\n- All internal plugins migrated to trait-based architecture (#44)\n\n### Removed\n- Legacy helper functions file, migrated to static methods (#44)\n- Deprecated `getOldConfig()` functions, use `getProviderConfig()` (#41)\n```\n\n## 速查表\n\n| 任务 | 命令 / 文件 | 关键规则 |\n|---|---|---|\n| 查看当前分支 | `git branch --show-current` | 禁止在 `master`/`main` 上直接修改 CHANGELOG |\n| 创建发布分支 | `git checkout -b docs/changelog-vX.Y.Z` | 所有文档改动必须走 PR |\n| 列出自上一个 tag 以来的提交 | `git log --oneline --no-merges $(git describe --tags --abbrev=0)..HEAD` | 排除 merge commit |\n| 标准分类 | `Added` → `Changed` → `Deprecated` → `Removed` → `Fixed` → `Security` | 严格顺序，不新增分类 |\n| 去除前缀 | 移除 `feat:`、`fix:`、`refactor:`、`chore:` | 仅当存在时才移除；已干净的中文描述保持原样 |\n| 升级指南 | `docs/v{major}/upgrade/v{version}.md` 或实际等价路径 | 先探测真实 docs 路径 |\n| 更新导航 | 侧边栏 / 菜单配置文件 | 若 `.gitignore` 忽略 docs 目录，使用 `git add -f` |\n| 合并 PR | 由人工在平台审核后执行 | Agent 严禁自动合并（`gh pr merge` 等一律禁止） |\n| 创建 annotated tag | `git tag -a vX.Y.Z -m \"release: vX.Y.Z\"` | 必须在 PR 由人工合并后执行 |\n| 创建 GitHub Release | `gh release create vX.Y.Z --notes-file release-notes.md --prerelease` | beta/rc/alpha 需标记为 pre-release |\n\n## 执行步骤\n\n### 0. 分支隔离（关键）\n\n**在修改任何发布相关文件前，必须先创建独立分支。**\n\n```bash\n# 先确认当前分支\ngit branch --show-current\n\n# 如果在 master/main 上，立即切出发布分支\ngit checkout -b docs/changelog-vX.Y.Z\n```\n\n**禁止**在 `master`/`main` 上直接提交 CHANGELOG 或升级指南改动。\n\n### 1. 分析提交\n\n```bash\nlatest=$(git describe --tags --abbrev=0)\ngit log --oneline --no-merges ${latest}..HEAD\n```\n\n重点关注：PR 编号、破坏性变更、新功能、已弃用 / 已移除项。\n\n**仅文档发布判断：** 如果自上一个 tag 以来的代码变更已经在目标分支上，而你只是整理文档，则按「仅文档发布」处理。流程相同，但提交范围只包含 CHANGELOG / 升级指南 / 导航。\n\n**关键验证步骤：**\n分析完提交后，务必验证实际代码状态，避免把「新增后又移除」的功能写进发布说明：\n\n```bash\n# 验证文件增删\ngit diff ${latest}..HEAD --stat\n\n# 验证 src/ 下实际代码变更\ngit diff ${latest}..HEAD -- src/\n```\n\n**验证规则：**\n1. 对于 commit 消息中的「新增」项：\n   - 确认文件 / 类 / 方法在 HEAD 中仍然存在\n   - 如果后续 commit 已移除，则从发布说明中排除\n   - 示例：#1157 新增 NetworkException，但 #1161 又移除了 → 不要写进 Added\n\n2. 对于 commit 消息中的「移除」项：\n   - 确认文件 / 类 / 方法在 HEAD 中已不存在\n   - 如果仍然存在，则排除或改为「已弃用」\n\n3. 对于 commit 消息中的「变更」项：\n   - 确认变更在 HEAD 中真实存在\n   - 如果已被 revert，则从发布说明中排除\n\n**验证命令：**\n```bash\n# 确认文件存在于 HEAD\ngit show HEAD:path/to/file\n\n# 确认文件变更\ngit diff ${latest}..HEAD -- path/to/file\n```\n\n### 2. 规范化 CHANGELOG\n\n**分类顺序（严格）**：Added、Changed、Deprecated、Removed、Fixed、Security。\n\n**规则：**\n- 仅当存在 `feat:` / `fix:` / `refactor:` / `chore:` 等前缀时才移除\n- 不要重写已经是干净中文描述的条目\n- 将 `BREAKING CHANGES` 内容移入 `Changed` 或 `Removed`\n- 合并重复条目（如同一文件被删除两次）\n- 保留 PR 引用 `(#1234)` 以便追溯\n- 子列表缩进 2 个空格\n\n**版本标题格式：**\n```markdown\n## [v1.2.0] - 2024-01-01\n```\n\n### 3. 编写升级指南\n\n**先探测真实 docs 路径。** 常见位置：\n- `docs/v{major}/upgrade/v{version}.md`\n- `web/docs/v{major}/upgrade/v{version}.md`\n- `src/docs/v{major}/upgrade/v{version}.md`\n\n如果目标升级指南已存在（例如后续 beta 版本），则在原有基础上追加新内容，不要整篇替换。\n\n**章节结构：**\n1. `## 重点检查`\n   - `### 运行环境` — PHP 版本、扩展、其他运行时要求\n   - `### 简单使用者` — **仅 end-user 可见变更**（命名空间变化、移除的方法、配置调整）。不要罗列内部重构（内部辅助函数重组、不影响公共 API 的基类变化、代码组织调整）。\n   - `### 自有插件开发者` — 影响自定义插件或扩展的内部架构变更（新基类、方法签名变化、移除的内部工具）。\n2. `## 更改版本号` — `composer require vendor/package:~{version}`\n3. `### BREAKING CHANGES`（如有）— 完整破坏性变更列表\n\n### 4. 清理过期示例\n\n搜索文档中已移除或已弃用功能的使用：\n```bash\ngrep -rn \"deprecatedMethod\\|removedClass\\|oldNamespace\" docs/\n```\n\n**规则：** 删除已移除功能的示例。不要保留「兼容两种写法」的双示例。文档只反映当前版本。\n\n### 5. 更新导航\n\n将新升级指南链接加入文档导航配置（如侧边栏、菜单、索引文件）。\n\n**注意：** 如果 `.gitignore` 包含 docs 目录匹配规则，新文件需用 `git add -f` 强制添加。\n\n### 6. PR → Tag → Release\n\n**创建 PR：**\n```bash\ngit add -f CHANGELOG.md docs/  # 或 web/docs/，按实际路径调整\n\ngit commit -m \"docs: update CHANGELOG and upgrade guide for vX.Y.Z\"\n\ngit push -u origin docs/changelog-vX.Y.Z\n\n# 通过 GitHub / GitLab / Gitea 等平台创建 PR\n```\n\n**⛔ 流程暂停点：创建 PR 后必须停止，等待人工审核合并**\n\n- 告知用户 PR 链接，明确说明等待人工审核合并，然后结束当前步骤\n- **严禁**执行 `gh pr merge`（含 --squash/--merge/--rebase）、平台合并 API 等任何自动合并操作\n- 后续步骤（push tag、创建 Release）即使已获授权，也不包含 PR 合并权；合并永远由人工执行\n\n**人工合并后（继续前必须确认 PR 已由人工合并）：**\n```bash\n# 核实 PR 状态（state 应为 MERGED，mergedBy 应为人工账号）\ngh pr view <PR编号> --json state,mergedAt,mergedBy\ngit fetch origin\ngit log --oneline origin/master | head -5\n\n# 打 tag 并推送\ngit checkout master && git pull origin master\ngit tag -a vX.Y.Z -m \"release: vX.Y.Z\"\ngit push origin vX.Y.Z\n```\n\n**创建 Release（以 GitHub 为例）：**\n```bash\n# 从 CHANGELOG 提取当前版本段落\nsed -n '/^## vX.Y.Z/,/^## v/p' CHANGELOG.md | sed '$d' > release-notes.md\n\n# beta/rc/alpha 版本需添加 --prerelease\ngh release create vX.Y.Z --title \"vX.Y.Z\" --notes-file release-notes.md --prerelease\n```\n\n## 常见错误\n\n| 错误 | 原因 | 修正 |\n|---|---|---|\n| 直接在 master 提交 CHANGELOG | 跳过分支隔离 | 必须先创建 `docs/changelog-vX.Y.Z` 分支 |\n| CHANGELOG 保留 `feat:`/`fix:` 前缀 | 从 commit 消息复制粘贴 | 仅在有前缀时剥离；保持已有干净描述 |\n| 内部重构写入「简单使用者」 | 未区分用户可见变更与内部变更 | 移到「自有插件开发者」或直接删除 |\n| 文档保留新旧双示例 | 试图在文档中保持向后兼容 | 删除旧示例；文档只反映当前版本 |\n| 忘记更新导航 | 创建升级指南后未注册 | 立即加入侧边栏 / 菜单 |\n| PR 未合并就创建 tag | 急于完成发布 | 先确认 `origin/master` 包含合并提交 |\n| 使用 lightweight tag | `git tag` 缺少 `-a` | 始终使用 `git tag -a` 并附带 message |\n| Release notes 与 CHANGELOG 不一致 | 单独编写 release notes | 从 CHANGELOG 提取或复制 |\n| 预发布版本未标记 pre-release | 忘记 beta/rc/alpha 后缀 | 按版本后缀在发布平台勾选 pre-release |\n| 写入「新增后又移除」的功能 | 仅依赖 commit 消息未验证代码 | 用 `git diff` 确认功能在 HEAD 中真实存在 |\n| Agent 自行合并 PR | 误将「发布流程授权」当作「合并授权」 | 如需撤销：`git revert <merge-commit>`；此后一律仅创建 PR 并等待人工合并 |\n</yakaai_skills>"}