diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 81721eb..9a5017e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -124,6 +124,19 @@ ], "strict": false }, + { + "name": "dev-commit-plugin", + "source": "./skills-dev/dev-commit-plugin", + "description": "智能 /commit 命令:分支保护 + 自动建功能分支 + Conventional Commits 生成", + "version": "1.0.0", + "category": "development", + "keywords": [ + "development", + "coding", + "workflow" + ], + "strict": false + }, { "name": "dev-deploy-plugin", "source": "./skills-dev/dev-deploy-plugin", diff --git a/docs/design/release-draft-gate.md b/docs/design/release-draft-gate.md new file mode 100644 index 0000000..5eabcca --- /dev/null +++ b/docs/design/release-draft-gate.md @@ -0,0 +1,253 @@ +# Release Draft 闸门机制 — 设计文档 + +**REQ**: REQ-20260416-0017 P0-3 +**状态**: 设计阶段 +**创建时间**: 2026-04-16 + +--- + +## 1. 背景 + +### 当前问题 + +ai-proj 当前 CI/CD 流程(`.gitea/workflows/build.yaml`): + +``` +push 到 main → 自动 build 镜像 → 自动部署生产 +``` + +**这个链路没有"最后一道人工闸门"**。merge develop→main 的 PR 一合并,生产就开始部署。一旦合并,只能事后回滚。 + +**典型事故场景**:PR 评审时漏了某个改动,merge 后立即推到线上,5 分钟后发现问题,但已经影响用户 5 分钟。 + +### 借鉴方案 + +**devflow-claude `/req:release` 命令的核心做法:** + +1. merge PR 不直接部署 +2. 创建一个 **draft release**(草稿) +3. **需要人工在 Gitea/GitHub 平台点 "Publish" 按钮** 才触发部署 +4. Draft 可以审查 SQL migration、回滚脚本、changelog 等"产物清单" +5. Publish 后才打 tag + 触发 build.yaml + +--- + +## 2. 目标 + +- 在 merge main 和"真正部署"之间插入 **人工 publish 闸门** +- 提供 **release 产物清单预览**(SQL migration / 回滚脚本 / changelog) +- 支持 **一键回滚**(从 draft release 追溯回滚脚本) +- 与现有 ai-proj MCP 工具链集成 + +--- + +## 3. 架构设计 + +### 3.1 流程对比 + +**当前流程:** +``` +[feat/xxx] → PR → merge develop → 测试 +[develop] → PR → merge main → ✗ 立即触发 build.yaml → 生产部署 +``` + +**新流程:** +``` +[feat/xxx] → PR → merge develop → 测试 +[develop] → PR → merge main → Gitea draft release(待审查) + ↓ + 人工 publish + ↓ + tag 推送 + 触发 build.yaml → 生产部署 +``` + +### 3.2 组件职责 + +| 组件 | 新增/改造 | 职责 | +|------|---------|------| +| **ai-proj backend** | 新增 | `/api/v1/releases` REST API(创建 draft、publish、查询、回滚) | +| **mcp-task-bridge** | 新增工具 | `create_release_draft` / `publish_release` / `list_releases` / `get_release` / `rollback_release` | +| **Gitea Actions** | 改造 | `build.yaml` 触发条件改为 `release.published` 事件 | +| **`release` skill** | 新增 | AI 侧命令封装(`/release new`, `/release publish`, `/release rollback`) | +| **release-draft.sh** | 新增脚本 | 本地命令行工具,用于在 CI 外手动创建 draft | + +### 3.3 数据模型 + +**新增 `releases` 表:** + +```sql +CREATE TABLE releases ( + id SERIAL PRIMARY KEY, + display_id VARCHAR(64) UNIQUE NOT NULL, -- e.g. "RELEASE-20260416-001" + version VARCHAR(64) NOT NULL, -- e.g. "v1.2.0" + title VARCHAR(255) NOT NULL, + description TEXT, + status VARCHAR(32) NOT NULL, -- draft / published / rolled_back + git_base_ref VARCHAR(255) NOT NULL, -- e.g. previous tag "v1.1.9" + git_head_ref VARCHAR(255) NOT NULL, -- e.g. commit sha after merge + git_tag VARCHAR(64), -- 生成的 tag,publish 后才有 + changelog_md TEXT NOT NULL, -- 自动生成的 changelog + sql_migration_paths TEXT[], -- 合并的 SQL 文件路径列表 + sql_rollback_md TEXT, -- 回滚脚本 + gitea_release_id BIGINT, -- Gitea 上的 release ID + gitea_release_url VARCHAR(512), + created_by INT REFERENCES users(id), + published_by INT REFERENCES users(id), + published_at TIMESTAMP, + rolled_back_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +### 3.4 API 设计 + +| Endpoint | 作用 | 权限 | +|----------|------|------| +| `POST /api/v1/releases/drafts` | 创建 draft release(自动推导版本、生成 changelog) | 开发者 | +| `POST /api/v1/releases/:id/publish` | 发布(推 tag、触发部署) | 需确认 | +| `GET /api/v1/releases` | 列表 | 所有人 | +| `GET /api/v1/releases/:id` | 详情 | 所有人 | +| `POST /api/v1/releases/:id/rollback` | 回滚到上一版本 | 管理员 | + +### 3.5 Gitea 集成 + +**创建 draft release(调 Gitea API):** +```http +POST /api/v1/repos/{owner}/{repo}/releases +{ + "tag_name": "v1.2.0", + "target_commitish": "main", + "name": "v1.2.0", + "body": "......", + "draft": true +} +``` + +**build.yaml 改造:** + +```yaml +# Before +on: + push: + branches: [main] + +# After +on: + release: + types: [published] + workflow_dispatch: # 保留手动触发 +``` + +**关键变化**:`release.published` 事件只在草稿被 publish 时触发,merge main 不再自动触发。 + +--- + +## 4. 用户流程(典型场景) + +### 4.1 开发者发版 + +``` +1. 合并 PR 到 main(照旧) +2. 在 Claude Code 里:/release new + - AI 读 git log,自动推导版本号 + - 汇总 SQL migration + - 生成 changelog + - 生成回滚脚本 + - 展示产物清单,等用户确认 +3. 确认后:MCP create_release_draft → 后端 API → Gitea draft +4. 收到 Slack/企微通知:"draft release v1.2.0 已创建,请审查" +5. 打开 Gitea 查看产物清单 +6. 点 "Publish release" 按钮 +7. CI/CD 自动触发 → 生产部署 +``` + +### 4.2 回滚 + +``` +1. /release rollback v1.2.0 +2. MCP rollback_release → 后端执行回滚 SQL → 重新部署上一版本镜像 +3. 记录到 releases 表:status=rolled_back +``` + +--- + +## 5. 渐进式落地路径(P0-3 拆分子任务) + +由于 P0-3 完整实现涉及后端 + MCP + CI/CD + skill 四个层面,**不建议单会话完成**。拆分为: + +| 子任务 | 范围 | 风险 | +|-------|------|------| +| **P0-3.1** | 后端 release model + migration | 低(只是建表) | +| **P0-3.2** | 后端 /api/v1/releases API(draft + publish) | 中 | +| **P0-3.3** | MCP 工具 create_release_draft / publish_release | 中 | +| **P0-3.4** | `release` skill 创建 | 低 | +| **P0-3.5** | Gitea draft release 集成 | 中 | +| **P0-3.6** | build.yaml 改为 release.published 触发 | **高**(影响所有人的发版流程) | +| **P0-3.7** | 回滚能力 | 中 | + +**推荐落地节奏:** +- 先做 P0-3.1 ~ P0-3.5(构建基础能力) +- **保留旧 push-to-main 流程作为 fallback** +- 跑 2 周观察 +- 再做 P0-3.6(切换 CI/CD 触发源) +- 最后 P0-3.7(回滚能力) + +--- + +## 6. 风险与应对 + +### R1: build.yaml 触发源切换影响所有开发者 + +**影响**:目前大家习惯 "merge main 即部署",切换后需等 publish + +**应对**: +- 上线前 1 周发邮件 + 企微通知 +- 提供手动触发(workflow_dispatch)作为应急入口 +- 文档化新流程 + +### R2: 回滚脚本自动生成不够可靠 + +**影响**:复杂 migration(如数据迁移)的回滚无法自动推导 + +**应对**: +- AI 只生成简单反向 DDL(CREATE→DROP 等) +- 复杂情况标记 `⚠️ 需手工补全` +- 回滚 SQL 由人工审查后纳入 draft release + +### R3: draft release 被忘记 publish + +**影响**:功能开发完但生产没部署 + +**应对**: +- 企微机器人每天检查超 24h 未 publish 的 draft,发提醒 +- 管理员 dashboard 显示 draft 列表 + +--- + +## 7. 验收标准 + +- [ ] `releases` 表创建,migration 落地 +- [ ] `/api/v1/releases/drafts` 创建 draft(至少支持最小字段) +- [ ] MCP `create_release_draft` 工具可用 +- [ ] 能从 Claude Code 一键创建 Gitea draft release +- [ ] build.yaml 支持 `release.published` 触发 +- [ ] 至少跑通 1 次完整流程(draft → 人工 publish → 自动部署) +- [ ] 回滚脚本自动生成覆盖 >80% 的 DDL 场景 + +--- + +## 8. 参考 + +- devflow-claude: `plugins/req/commands/release.md` +- REQ-20260416-0017(母需求) +- 当前 build.yaml: `/Users/donglinlai/coding/qiudl/new-ai-proj/.gitea/workflows/build.yaml` +- Gitea Release API: https://docs.gitea.com/api/next/#tag/repository/operation/repoCreateRelease + +--- + +## 9. 下一步行动 + +- [ ] 本文档提交评审 +- [ ] 评审通过后拆 7 个子任务(P0-3.1 ~ P0-3.7)并分别关联到 REQ-20260416-0017 +- [ ] 从 P0-3.1(数据模型)开始实施 diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..61a0e21 --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,100 @@ +# Claude Code Hooks + +本目录包含 ai-proj-helper 体系的 Claude Code 钩子脚本。 + +**REQ-20260416-0017 P0 批 — 源自 devflow-claude 借鉴** + +## 脚本清单 + +| 脚本 | 事件 | 作用 | +|------|------|------| +| `session-context.sh` | SessionStart | 从分支名解析 REQ-ID,注入需求上下文到会话 | +| `pre-tool-confirm.sh` | PreToolUse (Bash) | 拦截生产发布、force push、docker 生产容器、reset --hard 等危险操作 | + +## 安装(用户级,一次即可) + +### 方式 1:编辑 `~/.claude/settings.json` + +```jsonc +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "/Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/session-context.sh", + "timeout": 10 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "/Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/pre-tool-confirm.sh", + "timeout": 30 + } + ] + } + ] + } +} +``` + +### 方式 2:一键安装脚本 + +```bash +bash /Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/install.sh +``` + +## 验证 + +### SessionStart hook + +1. 切到一个带 REQ-ID 的分支:`git checkout feat/REQ-20260416-0017-xxx` +2. 打开新的 Claude Code 会话 +3. 会话开头应看到 `# 需求上下文(SessionStart Hook)` 块 + +### PreToolUse hook + +让 AI 尝试执行这些命令中的任一条,应弹出原生确认对话框: + +- `git push origin main` +- `git push --force` +- `tea pr merge --base main` +- `docker rm ai_postgres_prod` +- `git reset --hard` + +## 依赖 + +- `bash` (macOS / Linux 默认) +- `jq` (PreToolUse hook 需要,`brew install jq`) +- `python3` (SessionStart hook 解析 JSON) +- `curl` (SessionStart hook 调 MCP API) + +## 自定义 + +### 修改 MCP API 地址 + +在项目根目录创建 `.ai-proj-env`: + +```bash +export AI_PROJ_API_BASE="https://api.ai-proj.example.com" +export AI_PROJ_MCP_KEY="your-mcp-api-key" +``` + +或设置全局环境变量。 + +### 拦截更多命令 + +编辑 `pre-tool-confirm.sh`,在 `REASON` 赋值的 elif 链中增加规则。 + +## 设计原则 + +1. **快速失败退出**:不处理的命令立即 `exit 0`,不影响性能 +2. **非侵入性**:网络/依赖缺失时静默退出,不阻塞正常工作流 +3. **可复现**:hook 脚本跟随仓库分发,方便团队一致部署 diff --git a/hooks/install.sh b/hooks/install.sh new file mode 100755 index 0000000..32a4020 --- /dev/null +++ b/hooks/install.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# install.sh +# 一键把 ai-proj-helper hooks 注册到 ~/.claude/settings.json +# +# 用法: bash hooks/install.sh + +set -e + +HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SETTINGS_FILE="$HOME/.claude/settings.json" + +if ! command -v python3 >/dev/null 2>&1; then + echo "❌ 需要 python3(用于合并 JSON)" + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "⚠️ 未安装 jq,PreToolUse hook 将无法正常工作" + echo " 请执行: brew install jq" +fi + +mkdir -p "$HOME/.claude" + +python3 << EOF +import json +import os + +settings_file = "$SETTINGS_FILE" +hooks_dir = "$HOOKS_DIR" + +# 读已有配置 +if os.path.exists(settings_file): + with open(settings_file) as f: + data = json.load(f) +else: + data = {} + +# 合并 hooks +hooks = data.setdefault("hooks", {}) + +# SessionStart +session_start = hooks.setdefault("SessionStart", []) +session_cmd = f"{hooks_dir}/session-context.sh" +already_registered = any( + any(h.get("command") == session_cmd for h in entry.get("hooks", [])) + for entry in session_start +) +if not already_registered: + session_start.append({ + "hooks": [{"type": "command", "command": session_cmd, "timeout": 10}] + }) + print("✅ 注册 SessionStart hook") +else: + print("⏭️ SessionStart hook 已存在") + +# PreToolUse (Bash) +pre_tool = hooks.setdefault("PreToolUse", []) +pre_cmd = f"{hooks_dir}/pre-tool-confirm.sh" +already_registered = any( + entry.get("matcher") == "Bash" and any(h.get("command") == pre_cmd for h in entry.get("hooks", [])) + for entry in pre_tool +) +if not already_registered: + pre_tool.append({ + "matcher": "Bash", + "hooks": [{"type": "command", "command": pre_cmd, "timeout": 30}] + }) + print("✅ 注册 PreToolUse (Bash) hook") +else: + print("⏭️ PreToolUse hook 已存在") + +# 写回 +with open(settings_file, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + +print(f"\n📝 已更新: {settings_file}") +print("\n下一步:") +print(" 1. 重启 Claude Code 会话") +print(" 2. 切到含 REQ-ID 的分支测试 SessionStart hook") +print(" 3. 让 AI 尝试 'git push origin main' 测试 PreToolUse hook") +EOF diff --git a/hooks/pre-tool-confirm.sh b/hooks/pre-tool-confirm.sh new file mode 100755 index 0000000..5130a70 --- /dev/null +++ b/hooks/pre-tool-confirm.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# pre-tool-confirm.sh +# PreToolUse Hook: 拦截危险操作,强制原生确认对话框 +# +# 输入格式(stdin JSON): +# { "tool_name": "Bash", "tool_input": { "command": "..." } } +# +# 输出格式(stdout JSON): +# { "hookSpecificOutput": { "hookEventName": "PreToolUse", +# "permissionDecision": "ask", +# "permissionDecisionReason": "..." } } +# +# 安装方式:在 ~/.claude/settings.json 配置 +# hooks.PreToolUse: +# - matcher: "Bash" +# hooks: +# - type: command +# command: "/hooks/pre-tool-confirm.sh" +# timeout: 30 +# +# 参考:devflow-claude confirm-before-commit.sh + ai-proj memory 规则 +# REQ-20260416-0017 P0-2 + +set -e + +INPUT=$(cat) + +if ! command -v jq >/dev/null 2>&1; then + # 没有 jq 就不处理 + exit 0 +fi + +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +REASON="" + +# ============ 1. 生产分支推送 ============ +if echo "$COMMAND" | grep -qE '\bgit\s+push\b.*\b(origin\s+)?(main|master)\b'; then + REASON="⚠️ 即将推送到 main/master 生产分支。确认已过 PR 评审?" + +# ============ 2. 强制推送 ============ +elif echo "$COMMAND" | grep -qE '\bgit\s+push\b.*(--force|--force-with-lease|-f\b)'; then + REASON="⛔ 危险:force push 会覆盖远程历史,可能丢失他人提交。确认继续?" + +# ============ 3. tea pr merge --base main ============ +elif echo "$COMMAND" | grep -qE '\btea\s+pr\s+merge\b.*--base\s+main\b'; then + REASON="⚠️ 即将合并 PR 到 main 分支,合并后将触发生产部署。确认 PR 已测试?" + +# ============ 4. docker rm/stop 生产容器 ============ +elif echo "$COMMAND" | grep -qE '\bdocker\s+(rm|stop|kill)\b.*\b(ai_postgres_prod|ai_backend_prod|ai_frontend_prod|ai_redis_prod)\b'; then + REASON="⛔ 危险:即将停止/删除生产容器!2026-04-05 曾因此宕机 15 分钟。确认是灾难恢复?" + +# ============ 5. docker rm/stop 其他 prod 相关 ============ +elif echo "$COMMAND" | grep -qE '\bdocker\s+(rm|stop|kill)\b.*_prod\b'; then + REASON="⚠️ 即将停止/删除含 _prod 的容器。确认是生产环境?" + +# ============ 6. git reset --hard / clean -fd ============ +elif echo "$COMMAND" | grep -qE '\bgit\s+reset\s+--hard\b'; then + REASON="⚠️ git reset --hard 会丢弃所有未提交改动。确认继续?" + +elif echo "$COMMAND" | grep -qE '\bgit\s+clean\s+.*-f'; then + REASON="⚠️ git clean -f 会删除未跟踪文件。确认继续?" + +# ============ 7. rm -rf / 系统路径 ============ +elif echo "$COMMAND" | grep -qE '\brm\s+.*-[rf]+.*\s+(/|/\*|~|\$HOME)'; then + REASON="⛔ 危险:rm -rf 指向系统根或家目录。确认继续?" + +# ============ 8. ssh 生产服务器 + 破坏性命令 ============ +elif echo "$COMMAND" | grep -qE 'ssh\s+\S*prod\S*.*\b(rm|drop|truncate|delete)\b'; then + REASON="⛔ 危险:在生产服务器执行破坏性命令。确认继续?" + +# ============ 9. psql/mysql 生产数据库 + DROP/TRUNCATE/DELETE ============ +elif echo "$COMMAND" | grep -qiE '(psql|mysql).*prod.*\b(DROP|TRUNCATE|DELETE FROM)\b'; then + REASON="⛔ 危险:生产数据库 DROP/TRUNCATE/DELETE。确认已备份?" + +# ============ 10. MCP advance_delivery_stage force=true ============ +# 这种会走 MCP 而不是 Bash,本 hook 不好拦,留给另一个 matcher 处理 +fi + +if [ -n "$REASON" ]; then + jq -n --arg reason "$REASON" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "ask", + permissionDecisionReason: $reason + } + }' +else + exit 0 +fi diff --git a/hooks/release-draft.sh b/hooks/release-draft.sh new file mode 100755 index 0000000..29d27bb --- /dev/null +++ b/hooks/release-draft.sh @@ -0,0 +1,161 @@ +#!/bin/bash +# release-draft.sh +# 最小可用版:从本地 git 仓库创建 Gitea draft release +# +# 用法: +# export GITEA_TOKEN=$(bw get password "Gitea - qiudl Token") +# bash release-draft.sh v1.2.0 [--from v1.1.9] +# +# 输出: 创建的 draft release URL(待人工 publish) +# REQ-20260416-0017 P0-3 最小可用脚本 + +set -e + +VERSION="$1" +if [ -z "$VERSION" ]; then + echo "❌ 用法: $0 [--from ]" + exit 1 +fi + +FROM_TAG="" +if [ "$2" = "--from" ]; then + FROM_TAG="$3" +fi + +if ! git rev-parse --git-dir >/dev/null 2>&1; then + echo "❌ 不在 git 仓库内" + exit 1 +fi + +REPO_SLUG=$(git remote get-url origin 2>/dev/null | \ + sed -E 's|.*[:/]([^/]+/[^/]+)\.git$|\1|' | \ + sed -E 's|.*[:/]([^/]+/[^/]+)$|\1|') + +if [ -z "$REPO_SLUG" ]; then + echo "❌ 无法从 git remote 推断 OWNER/REPO" + exit 1 +fi + +if [ -z "$GITEA_TOKEN" ]; then + echo "❌ 需要 GITEA_TOKEN 环境变量" + echo " export GITEA_TOKEN=\$(bw get password 'Gitea - qiudl Token')" + exit 1 +fi + +GITEA_URL="${GITEA_URL:-https://gitea.pipexerp.com}" + +# 推断 from +if [ -z "$FROM_TAG" ]; then + FROM_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") +fi + +TO_REF="HEAD" + +# 生成 changelog 内容 +echo "📋 生成 changelog..." + +CHANGELOG="" +if [ -n "$FROM_TAG" ]; then + COMMITS=$(git log --pretty=format:'- %s (%h)' "${FROM_TAG}..${TO_REF}" 2>/dev/null || echo "") +else + COMMITS=$(git log --pretty=format:'- %s (%h)' "${TO_REF}" 2>/dev/null || echo "") +fi + +if [ -z "$COMMITS" ]; then + echo "⚠️ 无 commit,放弃" + exit 1 +fi + +# 按类型分组 +FEATS=$(echo "$COMMITS" | grep -iE 'feat(\(|:)|新功能' || true) +FIXES=$(echo "$COMMITS" | grep -iE 'fix(\(|:)|修复' || true) +CHORES=$(echo "$COMMITS" | grep -iE 'chore(\(|:)' || true) +OTHERS=$(echo "$COMMITS" | grep -vE 'feat(\(|:)|fix(\(|:)|chore(\(|:)|新功能|修复' || true) + +CHANGELOG="## 发布内容 + +**版本**: \`${VERSION}\` +**区间**: \`${FROM_TAG:-init}..${TO_REF}\` + +" + +if [ -n "$FEATS" ]; then + CHANGELOG="${CHANGELOG}### 新功能 + +${FEATS} + +" +fi + +if [ -n "$FIXES" ]; then + CHANGELOG="${CHANGELOG}### Bug 修复 + +${FIXES} + +" +fi + +if [ -n "$CHORES" ]; then + CHANGELOG="${CHANGELOG}### 杂项 + +${CHORES} + +" +fi + +if [ -n "$OTHERS" ]; then + CHANGELOG="${CHANGELOG}### 其他 + +${OTHERS} + +" +fi + +CHANGELOG="${CHANGELOG} + +--- + +⚠️ **这是 draft release**,审查无误后点击 'Publish release' 按钮才会触发生产部署。 + +📋 审查要点: +- [ ] 所有改动已过 PR 评审 +- [ ] SQL migration 已验证(如有) +- [ ] 回滚方案已确认(如有) +- [ ] 生产环境准备就绪" + +# 创建 draft release +echo "🚀 创建 Gitea draft release..." + +BODY_JSON=$(python3 -c " +import json +print(json.dumps({ + 'tag_name': '$VERSION', + 'target_commitish': 'main', + 'name': '$VERSION', + 'body': '''$CHANGELOG''', + 'draft': True, + 'prerelease': False, +})) +") + +RESP=$(curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$BODY_JSON" \ + "${GITEA_URL}/api/v1/repos/${REPO_SLUG}/releases") + +HTML_URL=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('html_url',''))" 2>/dev/null) + +if [ -n "$HTML_URL" ]; then + echo "✅ Draft release 已创建" + echo "🔗 $HTML_URL" + echo "" + echo "⏭️ 下一步:" + echo " 1. 打开链接审查产物清单" + echo " 2. 确认无误后点 'Publish release' 按钮" + echo " 3. CI/CD 将自动触发生产部署" +else + echo "❌ 创建失败" + echo "$RESP" | head -20 + exit 1 +fi diff --git a/hooks/session-context.sh b/hooks/session-context.sh new file mode 100755 index 0000000..f953924 --- /dev/null +++ b/hooks/session-context.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# session-context.sh +# SessionStart Hook: 会话启动时自动注入需求上下文 +# +# 从当前 Git 分支名解析 REQ-ID,调用 ai-proj MCP API 查询需求详情, +# 把标题 / 状态 / delivery_stage / reviewer / 进行中需求数注入 system-reminder。 +# +# 安装方式: +# 在 ~/.claude/settings.json 的 hooks.SessionStart 配置: +# { +# "command": "/Users/donglinlai/coding/qiudl/ai-proj-helper/hooks/session-context.sh", +# "timeout": 10 +# } +# +# 参考:devflow-claude 同名脚本 + ai-proj MCP 适配 +# REQ-20260416-0017 P0-1 + +set -e + +# 仅在 git 仓库内执行 +if ! git rev-parse --git-dir >/dev/null 2>&1; then + exit 0 +fi + +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +if [ -z "$REPO_ROOT" ]; then + exit 0 +fi + +cd "$REPO_ROOT" + +# ============ 1. 当前分支 → REQ ID ============ +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") +REQ_ID="" + +if [ -n "$BRANCH" ]; then + # 匹配 feat/REQ-20260416-0017-xxx / fix/REQ-20260416-0017 / feature/req-20260416-0017 + REQ_ID=$(echo "$BRANCH" | grep -oiE 'REQ-[0-9]{8}-[0-9]{4}' | head -1 | tr '[:lower:]' '[:upper:]') +fi + +# ============ 2. 无 REQ 时仅输出分支信息(静默退出条件) ============ +if [ -z "$REQ_ID" ]; then + # 只要不在 main/develop 上就提示一下 + case "$BRANCH" in + main|master|develop|"") exit 0 ;; + esac + echo "# 会话上下文" + echo "" + echo "- 当前分支: \`${BRANCH}\`(未检测到 REQ ID)" + exit 0 +fi + +# ============ 3. 查询 MCP API ============ +# MCP API 通过 localhost:8080 直连(ai-proj 本地后端)或 ai-proj-prod +# 这里优先读项目根的 .ai-proj-env 决定环境 +API_BASE="${AI_PROJ_API_BASE:-}" +API_TOKEN="${AI_PROJ_MCP_KEY:-}" + +if [ -f "$REPO_ROOT/.ai-proj-env" ]; then + # shellcheck disable=SC1091 + source "$REPO_ROOT/.ai-proj-env" +fi + +if [ -z "$API_BASE" ]; then + # 默认走本地 dev + API_BASE="http://localhost:8080" +fi + +# 查询需求 +RESP="" +if command -v curl >/dev/null 2>&1; then + if [ -n "$API_TOKEN" ]; then + RESP=$(curl -s --max-time 3 -H "X-MCP-API-Key: $API_TOKEN" \ + "${API_BASE}/api/v1/mcp/requirements/by-display-id/${REQ_ID}" 2>/dev/null || echo "") + else + RESP=$(curl -s --max-time 3 \ + "${API_BASE}/api/v1/mcp/requirements/by-display-id/${REQ_ID}" 2>/dev/null || echo "") + fi +fi + +# ============ 4. 解析并输出 ============ +echo "# 需求上下文(SessionStart Hook)" +echo "" +echo "- 分支: \`${BRANCH}\`" +echo "- 需求: **${REQ_ID}**" + +if [ -n "$RESP" ] && command -v python3 >/dev/null 2>&1; then + # 尝试用 python 解析 + PARSED=$(python3 -c " +import sys, json +try: + d = json.loads('''$RESP''') + data = d.get('data', {}) + if not data: + sys.exit(0) + title = data.get('title', '?') + status = data.get('status', '?') + stage = data.get('delivery_stage', '?') + priority = data.get('priority', '?') + project = data.get('project_name', '?') + print(f'title={title}') + print(f'status={status}') + print(f'stage={stage}') + print(f'priority={priority}') + print(f'project={project}') +except Exception: + pass +" 2>/dev/null) + + if [ -n "$PARSED" ]; then + TITLE=$(echo "$PARSED" | grep '^title=' | sed 's/^title=//') + STATUS=$(echo "$PARSED" | grep '^status=' | sed 's/^status=//') + STAGE=$(echo "$PARSED" | grep '^stage=' | sed 's/^stage=//') + PRIORITY=$(echo "$PARSED" | grep '^priority=' | sed 's/^priority=//') + PROJECT=$(echo "$PARSED" | grep '^project=' | sed 's/^project=//') + + [ -n "$TITLE" ] && echo "- 标题: ${TITLE}" + [ -n "$PROJECT" ] && echo "- 项目: ${PROJECT}" + [ -n "$STATUS" ] && echo "- 状态: ${STATUS}" + [ -n "$STAGE" ] && echo "- 交付阶段: ${STAGE}" + [ -n "$PRIORITY" ] && echo "- 优先级: ${PRIORITY}" + else + echo "- 📡 MCP API 响应为空或未授权(API_BASE=${API_BASE})" + fi +else + echo "- ⚠️ 无法连接 MCP API(${API_BASE}),仅显示分支信息" +fi + +echo "" +echo "💡 相关命令:\`/req get ${REQ_ID}\` 查看详情 · \`/commit\` 智能提交" diff --git a/skills-dev/dev-coding-plugin/skills/SKILL.md b/skills-dev/dev-coding-plugin/skills/SKILL.md index 8160fce..198b432 100644 --- a/skills-dev/dev-coding-plugin/skills/SKILL.md +++ b/skills-dev/dev-coding-plugin/skills/SKILL.md @@ -490,3 +490,55 @@ fi | `dev-ios` | iOS 开发(插件,按需加载)| | `dev-android` | Android 开发(插件,按需加载)| | `dev-mcp` | MCP bridge 开发(插件,按需加载)| + +--- + +## CLAUDE.md 架构检查机制(REQ-20260416-0017 P0-5) + +**原则:本 skill 不硬编码任何项目的架构细节,从项目 CLAUDE.md 读取**。 + +### 为什么 + +同一套 skill 要支持多个技术栈(Go+Gin / React+AntD / Vue+Element / Python+FastAPI)。如果把分层、命名、目录结构写死在 SKILL.md 里,跨项目就会冲突。 + +devflow-claude 的做法(借鉴):skill 只管**流程和模板**,项目架构由 CLAUDE.md 的 "Architecture" / "项目架构" 章节定义。 + +### 执行前检查 + +开始编码任务前,skill 先检查项目根 `CLAUDE.md`: + +```bash +# 检查 CLAUDE.md 是否含架构关键词 +if [ -f "CLAUDE.md" ]; then + if grep -qiE "(架构|分层|目录结构|tech stack|architecture|project structure)" CLAUDE.md; then + echo "✅ 检测到项目架构信息" + else + echo "⚠️ CLAUDE.md 缺少架构描述" + echo " dev-coding 需要架构信息来生成准确的文件路径和分层顺序" + echo "" + echo " 📋 建议操作:" + echo " - 查看预置架构片段: ai-proj-helper/skills-dev/dev-coding-plugin/templates/claude-md-snippets/" + echo " - 选择匹配技术栈的片段,补充到 CLAUDE.md 的 '## Architecture' 章节" + echo "" + echo " ⚠️ 继续执行,但生成的文件路径可能不够准确" + fi +else + echo "⚠️ 未找到项目 CLAUDE.md,建议创建" +fi +``` + +### 架构片段模板库 + +位于 `skills-dev/dev-coding-plugin/templates/claude-md-snippets/`: + +| 文件 | 适用场景 | +|------|---------| +| `go-gin-gorm.md` | Go + Gin + GORM 后端(ai-proj backend 风格) | +| `react-antd.md` | React + TypeScript + Ant Design(ai-proj frontend 风格) | +| `vue-element.md` | Vue 3 + Element Plus(coolbuy-paas 风格) | +| `mcp-typescript.md` | MCP bridge TypeScript(mcp-task-bridge 风格) | +| `generic.md` | 通用空白骨架 | + +### 非阻断原则 + +架构信息缺失时**仅警告不阻止**。用户仍可继续,但会被告知"生成的建议可能不够准确"。 diff --git a/skills-dev/dev-coding-plugin/templates/claude-md-snippets/generic.md b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/generic.md new file mode 100644 index 0000000..4ddafca --- /dev/null +++ b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/generic.md @@ -0,0 +1,47 @@ + + +## Architecture + +### 技术栈 + +- **语言**: _TODO_ +- **框架**: _TODO_ +- **数据库**: _TODO_ +- **缓存**: _TODO_ +- **部署**: _TODO_ + +### 目录结构 + +``` +project-root/ +├── ???/ # _TODO: 说明_ +├── ???/ # _TODO_ +└── ???/ +``` + +### 分层 / 模块规则 + +1. _TODO: 依赖方向_ +2. _TODO: 允许/禁止的跨层调用_ + +### 命名规范 + +| 类型 | 约定 | 示例 | +|------|------|------| +| _TODO_ | _TODO_ | _TODO_ | + +### 错误处理 + +_TODO_ + +### 日志 + +_TODO_ + +### 测试 + +_TODO_ + +### 其他关键约定 + +- _TODO_ diff --git a/skills-dev/dev-coding-plugin/templates/claude-md-snippets/go-gin-gorm.md b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/go-gin-gorm.md new file mode 100644 index 0000000..6954135 --- /dev/null +++ b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/go-gin-gorm.md @@ -0,0 +1,56 @@ + + +## Architecture + +### 分层结构(Go + Gin + GORM) + +``` +backend/ +├── routes/ # HTTP 路由定义(按模块拆分) +├── handlers/ # 请求解析 + 响应组装(薄层,不含业务) +├── services/ # 业务逻辑(事务、组合、校验) +├── models/ # GORM 数据模型 +├── database/ # Repository 层(SQL、查询) +├── middleware/ # 认证、CORS、日志、限流 +├── migrations/ # SQL 迁移文件 +└── utils/ # 通用工具(密码、签名等) +``` + +### 分层规则(强制) + +1. **请求流向**:Route → Handler → Service → Database → Models +2. **Handler 禁止直接访问 database**:必须走 Service 层 +3. **Service 禁止调用 Handler 或 Route**:单向依赖 +4. **Model 仅定义结构 + GORM tag**:不含业务方法 + +### 命名规范 + +| 类型 | 约定 | 示例 | +|------|------|------| +| 文件名 | snake_case | `user_service.go` | +| 包名 | lowercase | `services`, `handlers` | +| 导出函数/类型 | PascalCase | `CreateUser`, `UserRepository` | +| 内部函数 | camelCase | `validatePassword` | +| 常量 | SCREAMING_SNAKE_CASE | `MAX_RETRY_COUNT` | + +### 错误处理 + +- 使用 `errors.New()` 或自定义 error type +- Handler 层统一返回 `{"code": X, "msg": "...", "data": ...}` +- Service 层返回原始 error,由 Handler 转换 + +### 日志 + +- 使用结构化 log:`log.WithField("user_id", uid).Info("...")` +- 禁用 `fmt.Println` / `print` + +### 测试 + +- 单元测试文件名:`xxx_test.go` +- 使用 `testify/assert` +- Mock 用 `testify/mock` 或 `gomock` + +### 依赖检查 + +- **新 handler 禁止直接 `import database/`**:需走 Service 层 +- `./scripts/check-architecture.sh check` 作为 CI 门禁 diff --git a/skills-dev/dev-coding-plugin/templates/claude-md-snippets/mcp-typescript.md b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/mcp-typescript.md new file mode 100644 index 0000000..2f54214 --- /dev/null +++ b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/mcp-typescript.md @@ -0,0 +1,75 @@ + + +## Architecture + +### 目录结构(MCP Bridge - TypeScript) + +``` +mcp-task-bridge/ +├── src/ +│ ├── tools/ # MCP tool 定义(每个工具一个文件) +│ ├── resources/ # MCP resources(若有) +│ ├── prompts/ # MCP prompts(若有) +│ ├── client/ # 后端 REST API 客户端 +│ ├── utils/ # 工具函数 +│ └── index.ts # 入口 +├── tests/ +└── dist/ # 编译产物(不提交) +``` + +### 工具定义规范 + +每个 MCP tool 一个文件: + +```typescript +// tools/create-task.ts +export const createTaskTool: Tool = { + name: 'create_task', + description: '...', + inputSchema: { + type: 'object', + properties: { ... }, + required: [...] + } +}; + +export async function handleCreateTask(args) { ... } +``` + +### 后端 API 调用 + +- 所有 REST 请求通过 `src/client/api.ts` 统一封装 +- 认证头由 client 自动附加(不在 tool 里处理) +- 错误统一转成 MCP error response + +### 命名规范 + +| 类型 | 约定 | 示例 | +|------|------|------| +| MCP tool name | snake_case | `create_task`, `list_requirements` | +| 文件名 | kebab-case | `create-task.ts` | +| 函数名 | camelCase | `handleCreateTask` | +| Tool 变量 | camelCase + `Tool` | `createTaskTool` | + +### 构建与部署 + +- `npm run build` → `dist/` +- **修改代码后必须重新 build**:`pkill -f mcp-task-bridge/dist/index.js` 重启 MCP server +- 不能直接运行 TypeScript 源码 + +### 环境配置 + +- `dev` 环境:`ai-proj-dev` MCP server +- `prod` 环境:`ai-proj-prod` MCP server +- 禁止跨环境传数据(dev 需求不能关联 prod 任务) + +### 测试 + +- Jest + ts-jest +- 集成测试模拟真实 MCP 协议 + +### 常见错误 + +- **Rule 1**: MCP 端点必须 `/api/v1/mcp/` 前缀 +- **Rule 2**: 修改后必须 rebuild + 重启 +- **Rule 3**: 环境隔离(dev / prod) diff --git a/skills-dev/dev-coding-plugin/templates/claude-md-snippets/react-antd.md b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/react-antd.md new file mode 100644 index 0000000..4d991ff --- /dev/null +++ b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/react-antd.md @@ -0,0 +1,78 @@ + + +## Architecture + +### 目录结构(React + TypeScript + Ant Design) + +``` +frontend/src/ +├── pages/ # 页面级组件(路由对应) +├── components/ # 可复用 UI 组件 +├── services/ # API 客户端(Axios 封装) +├── hooks/ # 自定义 React Hooks +├── contexts/ # Context Providers(auth, timer 等) +├── utils/ # 工具函数(auth, validation, date 等) +├── types/ # TypeScript 类型定义 +└── config/ # Feature flags, 性能配置 +``` + +### 状态管理 + +| 状态类型 | 方案 | +|---------|------| +| 服务器状态 | React Query (TanStack Query) | +| 全局状态 | Context API | +| 本地状态 | useState / useReducer | +| 表单状态 | Ant Design Form | + +**禁止**:Redux / MobX(本项目不使用) + +### 路由 + +- React Router v6 +- 路由定义集中在 `src/routes/` +- 懒加载:`const Page = lazy(() => import(...))` + +### API 调用 + +- 使用 `services/` 下的封装函数,不要在组件里直接 `axios.get` +- 响应类型必须有 TypeScript interface +- 错误统一由 axios 拦截器处理 + +### 样式 + +- Ant Design 组件 + CSS Module +- 禁止内联 `style={{ ... }}` 用于复杂样式 +- 全局变量走 CSS Variables + +### Modal 安全规则(重要) + +`Modal.success/info/warning/error` 是非阻塞调用,后续 UI 操作必须放在 `onOk` 回调中: + +```tsx +// WRONG +Modal.success({ title: '成功' }); +setNextModalOpen(true); // 立即执行,两个 modal 冲突 + +// CORRECT +Modal.success({ + title: '成功', + onOk: () => setNextModalOpen(true), +}); +``` + +### 命名规范 + +| 类型 | 约定 | 示例 | +|------|------|------| +| 组件文件 | PascalCase | `UserProfile.tsx` | +| Hook 文件 | camelCase | `useAuth.ts` | +| 工具文件 | kebab-case | `date-utils.ts` | +| 组件名 | PascalCase | `UserProfile` | +| Hook 名 | `use` 前缀 | `useAuth` | + +### 测试 + +- 单测:Jest + React Testing Library +- E2E:Playwright +- 测试文件:`xxx.test.tsx` 与源文件同目录 diff --git a/skills-dev/dev-coding-plugin/templates/claude-md-snippets/vue-element.md b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/vue-element.md new file mode 100644 index 0000000..f8f8a16 --- /dev/null +++ b/skills-dev/dev-coding-plugin/templates/claude-md-snippets/vue-element.md @@ -0,0 +1,67 @@ + + +## Architecture + +### 目录结构(Vue 3 + TypeScript + Element Plus) + +``` +src/ +├── views/ # 页面级组件(路由对应) +├── components/ # 可复用组件 +├── api/ # API 封装 +├── stores/ # Pinia stores +├── composables/ # 组合式函数(use* hooks) +├── utils/ # 工具函数 +├── types/ # TypeScript 类型定义 +└── router/ # Vue Router 配置 +``` + +### 状态管理 + +- **Pinia**(官方推荐) +- 每个业务模块一个 store:`stores/user.ts`、`stores/order.ts` +- 禁止直接在组件里写持久状态 + +### 路由 + +- Vue Router 4 +- 路由守卫统一在 `router/guards.ts` +- 懒加载:`component: () => import('@/views/...')` + +### Composition API + +- **强制使用 `