refactor: 通用技能按类别拆分为独立目录
skills/ → skills-dev(9), skills-req(10), skills-ops(4), skills-integration(8), skills-biz(4), skills-workflow(7) generate-marketplace.py 改为自动扫描所有 skills-* 目录。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
8
skills-dev/agent-swarm-plugin/.claude-plugin/plugin.json
Normal file
8
skills-dev/agent-swarm-plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "agent-swarm-plugin",
|
||||
"description": "Multi-agent orchestration using OpenAI Swarm patterns. Coordinate specialized agents for complex development workflows with handoffs and context sharing.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
406
skills-dev/agent-swarm-plugin/skills/SKILL.md
Normal file
406
skills-dev/agent-swarm-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,406 @@
|
||||
---
|
||||
name: agent-swarm
|
||||
description: Multi-agent orchestration using OpenAI Swarm patterns. Coordinate specialized agents for complex development workflows with handoffs and context sharing.
|
||||
---
|
||||
|
||||
# Agent Swarm - Multi-Agent Orchestration
|
||||
|
||||
基于 OpenAI Swarm 设计模式的多智能体协作系统,用于复杂开发任务的智能分解与协调。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 1. Agent(智能体)
|
||||
每个 Agent 是具有特定职责的专家:
|
||||
- **Instructions**: Agent 的角色定义和行为准则
|
||||
- **Functions**: Agent 可以调用的工具函数
|
||||
- **Handoffs**: 何时移交给其他 Agent
|
||||
|
||||
### 2. Handoff(任务移交)
|
||||
Agent 之间的控制权转移机制:
|
||||
- 当前 Agent 完成自己的职责
|
||||
- 识别需要其他专长
|
||||
- 移交给最合适的 Agent
|
||||
|
||||
### 3. Context Variables(上下文变量)
|
||||
跨 Agent 共享的状态:
|
||||
- 项目目录
|
||||
- 技术栈信息
|
||||
- 当前进度
|
||||
- 发现的问题
|
||||
|
||||
---
|
||||
|
||||
## 预定义 Agent
|
||||
|
||||
### 1. Architect Agent(架构师)
|
||||
**职责**: 理解需求、技术选型、设计系统架构
|
||||
|
||||
**何时使用**:
|
||||
- 用户描述新功能或系统
|
||||
- 需要技术方案设计
|
||||
- 需要架构评审
|
||||
|
||||
**工具**:
|
||||
- Read codebase
|
||||
- Grep patterns
|
||||
- 设计文档生成
|
||||
|
||||
**Handoff to**:
|
||||
- Coder Agent(开始编码)
|
||||
- Reviewer Agent(评审设计)
|
||||
|
||||
---
|
||||
|
||||
### 2. Coder Agent(编码者)
|
||||
**职责**: 实现功能、编写代码、修复 bug
|
||||
|
||||
**何时使用**:
|
||||
- 架构师完成设计
|
||||
- 用户提出 bug 修复
|
||||
- 需要代码重构
|
||||
|
||||
**工具**:
|
||||
- Edit files
|
||||
- Write files
|
||||
- Git operations
|
||||
|
||||
**Handoff to**:
|
||||
- Tester Agent(代码完成后)
|
||||
- Architect Agent(遇到设计问题)
|
||||
|
||||
---
|
||||
|
||||
### 3. Tester Agent(测试员)
|
||||
**职责**: 编写测试、运行测试、验证功能
|
||||
|
||||
**何时使用**:
|
||||
- 代码编写完成
|
||||
- 需要测试覆盖
|
||||
- 验证 bug 修复
|
||||
|
||||
**工具**:
|
||||
- Run tests
|
||||
- Write test cases
|
||||
- Coverage reports
|
||||
|
||||
**Handoff to**:
|
||||
- Deployer Agent(测试通过)
|
||||
- Coder Agent(发现问题)
|
||||
|
||||
---
|
||||
|
||||
### 4. Deployer Agent(部署员)
|
||||
**职责**: 构建镜像、部署服务、监控上线
|
||||
|
||||
**何时使用**:
|
||||
- 测试全部通过
|
||||
- 需要发布到环境
|
||||
- 需要回滚版本
|
||||
|
||||
**工具**:
|
||||
- Docker build
|
||||
- SSH deployment
|
||||
- Health checks
|
||||
|
||||
**Handoff to**:
|
||||
- Monitor Agent(部署完成)
|
||||
- Coder Agent(部署失败)
|
||||
|
||||
---
|
||||
|
||||
### 5. Reviewer Agent(评审员)
|
||||
**职责**: 代码审查、文档审查、安全检查
|
||||
|
||||
**何时使用**:
|
||||
- PR 创建后
|
||||
- 重要功能完成
|
||||
- 需要质量把关
|
||||
|
||||
**工具**:
|
||||
- Diff analysis
|
||||
- Security scan
|
||||
- Best practices check
|
||||
|
||||
**Handoff to**:
|
||||
- Coder Agent(需要修改)
|
||||
- Deployer Agent(审查通过)
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本调用
|
||||
|
||||
```bash
|
||||
/swarm start "在 new-ai-proj 中实现任务批量删除功能"
|
||||
```
|
||||
|
||||
**执行流程**:
|
||||
1. **Architect** 分析需求 → 设计 API 和前端交互
|
||||
2. **Coder** 实现后端 API → 实现前端 UI
|
||||
3. **Tester** 编写单元测试 → 运行测试
|
||||
4. **Reviewer** 代码审查 → 安全检查
|
||||
5. **Deployer** 部署到 staging → 验证功能
|
||||
|
||||
---
|
||||
|
||||
### 指定起始 Agent
|
||||
|
||||
```bash
|
||||
/swarm coder "修复 backend/handlers/task_handler.go 的空指针 bug"
|
||||
```
|
||||
|
||||
直接从 Coder Agent 开始,跳过架构设计阶段。
|
||||
|
||||
---
|
||||
|
||||
### 传递上下文
|
||||
|
||||
```bash
|
||||
/swarm start "优化数据库查询性能" \
|
||||
--context project=/Users/coolbuy-dev/coding/new-ai-proj \
|
||||
--context stack=Go,PostgreSQL,Redis \
|
||||
--context module=backend/services
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 查看执行轨迹
|
||||
|
||||
```bash
|
||||
/swarm trace
|
||||
```
|
||||
|
||||
显示 Agent 调用链:
|
||||
```
|
||||
Architect → analyzed requirements (3 min)
|
||||
↓ handoff: "Design complete, ready for implementation"
|
||||
Coder → implemented 5 files (12 min)
|
||||
↓ handoff: "Code complete, needs testing"
|
||||
Tester → wrote 8 test cases, all passed (5 min)
|
||||
↓ handoff: "Tests passed, ready for review"
|
||||
Reviewer → approved with 2 suggestions (2 min)
|
||||
↓ handoff: "Approved, ready for deployment"
|
||||
Deployer → deployed to staging, health check OK (3 min)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件
|
||||
|
||||
### swarm.yaml
|
||||
|
||||
在项目根目录创建 `swarm.yaml` 自定义 Agent 行为:
|
||||
|
||||
```yaml
|
||||
agents:
|
||||
architect:
|
||||
instructions: |
|
||||
你是系统架构师,专注于 Go + Vue.js 技术栈。
|
||||
遵循 RESTful API 设计原则。
|
||||
考虑性能、安全性、可维护性。
|
||||
max_turns: 5
|
||||
|
||||
coder:
|
||||
instructions: |
|
||||
你是 Go 后端工程师和 Vue.js 前端工程师。
|
||||
编写清晰、简洁、高性能的代码。
|
||||
遵循项目现有代码风格。
|
||||
tools:
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
max_turns: 10
|
||||
|
||||
tester:
|
||||
instructions: |
|
||||
你是测试工程师,编写全面的测试用例。
|
||||
确保边界条件、错误处理、并发安全。
|
||||
tools:
|
||||
- Bash
|
||||
- Write
|
||||
test_command: "go test ./... -v"
|
||||
max_turns: 5
|
||||
|
||||
context_variables:
|
||||
project_root: /Users/coolbuy-dev/coding/new-ai-proj
|
||||
backend_lang: Go 1.21
|
||||
frontend_framework: Vue 3
|
||||
database: PostgreSQL 15
|
||||
deployment_target: staging.ai.pipexerp.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 1. 自定义 Agent
|
||||
|
||||
```yaml
|
||||
agents:
|
||||
database-optimizer:
|
||||
instructions: |
|
||||
你是数据库性能优化专家。
|
||||
分析慢查询、优化索引、设计缓存策略。
|
||||
functions:
|
||||
- explain_analyze
|
||||
- create_index
|
||||
- cache_design
|
||||
handoff_to:
|
||||
- coder # 实现优化方案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 条件 Handoff
|
||||
|
||||
```yaml
|
||||
handoff_rules:
|
||||
- from: tester
|
||||
to: coder
|
||||
condition: "test_pass_rate < 90%"
|
||||
message: "测试失败率超过 10%,需要修复"
|
||||
|
||||
- from: tester
|
||||
to: deployer
|
||||
condition: "test_pass_rate == 100%"
|
||||
message: "所有测试通过,可以部署"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 并行 Agent
|
||||
|
||||
对于独立任务,多个 Agent 可以并行工作:
|
||||
|
||||
```bash
|
||||
/swarm parallel \
|
||||
"coder: 实现后端 API" \
|
||||
"coder: 实现前端 UI" \
|
||||
"tester: 编写 API 测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与 Remote Coding 集成
|
||||
|
||||
在 OpenClaw 中调用本地 Claude Code 执行 Swarm 工作流:
|
||||
|
||||
```bash
|
||||
# OpenClaw 调用 Melbourne Claude Code
|
||||
ssh melbourne "cd /Users/coolbuy-dev/coding/new-ai-proj && \
|
||||
/opt/homebrew/bin/claude --dangerously-skip-permissions \
|
||||
-p '/swarm start 实现任务批量删除功能'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实际案例
|
||||
|
||||
### 案例 1: 新功能开发
|
||||
|
||||
**任务**: "为 AI-Proj 实现需求批量导出功能"
|
||||
|
||||
**执行过程**:
|
||||
1. **Architect**:
|
||||
- 分析需求:导出格式(Excel/PDF)、筛选条件、数据脱敏
|
||||
- 设计 API: `POST /api/v1/requirements/export`
|
||||
- 设计前端:导出按钮、进度条、下载链接
|
||||
|
||||
2. **Coder**:
|
||||
- 后端实现 export service
|
||||
- 前端实现导出 UI 组件
|
||||
- 集成 file download 功能
|
||||
|
||||
3. **Tester**:
|
||||
- 测试大量数据导出(1000+ 需求)
|
||||
- 测试并发导出
|
||||
- 测试下载失败重试
|
||||
|
||||
4. **Reviewer**:
|
||||
- 检查文件大小限制
|
||||
- 检查内存泄漏风险
|
||||
- 检查数据权限控制
|
||||
|
||||
5. **Deployer**:
|
||||
- 部署到 staging
|
||||
- 验证导出功能
|
||||
- 监控资源使用
|
||||
|
||||
---
|
||||
|
||||
### 案例 2: Bug 修复
|
||||
|
||||
**任务**: "修复任务详情页加载缓慢问题"
|
||||
|
||||
**执行过程**:
|
||||
1. **Architect**:
|
||||
- 分析性能瓶颈:N+1 查询问题
|
||||
- 设计优化方案:使用 JOIN 和预加载
|
||||
|
||||
2. **Coder**:
|
||||
- 优化数据库查询
|
||||
- 添加 Redis 缓存
|
||||
- 更新前端数据获取逻辑
|
||||
|
||||
3. **Tester**:
|
||||
- 性能测试:加载时间从 3s → 300ms
|
||||
- 并发测试:100 用户同时访问
|
||||
- 缓存一致性测试
|
||||
|
||||
4. **Deployer**:
|
||||
- 灰度发布到 10% 用户
|
||||
- 监控性能指标
|
||||
- 全量发布
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **明确任务范围**: 复杂任务交给 Swarm,简单任务直接执行
|
||||
2. **合理设置 max_turns**: 避免 Agent 陷入死循环
|
||||
3. **记录 Handoff 原因**: 便于追溯和调试
|
||||
4. **定期审查轨迹**: 优化 Agent 协作流程
|
||||
5. **利用 Context Variables**: 避免重复传递信息
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| Agent 一直循环 | max_turns 设置过大 | 降低 max_turns,添加明确的 handoff 条件 |
|
||||
| Handoff 失败 | 目标 Agent 未定义 | 检查 swarm.yaml 配置 |
|
||||
| 上下文丢失 | Context Variables 未传递 | 在 handoff 时显式传递 context |
|
||||
| 执行太慢 | 串行执行可并行任务 | 使用 `/swarm parallel` |
|
||||
|
||||
---
|
||||
|
||||
## 与其他 Skills 集成
|
||||
|
||||
- **dev-coding**: Coder Agent 使用 dev-coding 的编码规范
|
||||
- **dev-test**: Tester Agent 使用 dev-test 的测试策略
|
||||
- **ops-tools**: Deployer Agent 使用 ops-tools 进行部署
|
||||
- **ai-proj**: 所有 Agent 使用 ai-proj MCP 进行任务同步
|
||||
|
||||
---
|
||||
|
||||
## 命令速查
|
||||
|
||||
| 命令 | 功能 |
|
||||
|------|------|
|
||||
| `/swarm start <task>` | 启动 Swarm 工作流(从 Architect 开始) |
|
||||
| `/swarm <agent> <task>` | 从指定 Agent 开始 |
|
||||
| `/swarm parallel <tasks>` | 并行执行多个任务 |
|
||||
| `/swarm trace` | 查看执行轨迹 |
|
||||
| `/swarm config` | 显示当前配置 |
|
||||
| `/swarm agents` | 列出所有可用 Agent |
|
||||
| `/swarm stop` | 终止当前 Swarm 执行 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [OpenAI Swarm 文档](https://github.com/openai/swarm)
|
||||
- [Multi-Agent Systems 设计模式](https://arxiv.org/abs/2308.00352)
|
||||
- [Claude Code Skills 文档](https://docs.anthropic.com/claude-code/skills)
|
||||
8
skills-dev/dev-arch-plugin/.claude-plugin/plugin.json
Normal file
8
skills-dev/dev-arch-plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "dev-arch-plugin",
|
||||
"description": "Plugin for dev-arch",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
390
skills-dev/dev-arch-plugin/skills/SKILL.md
Normal file
390
skills-dev/dev-arch-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,390 @@
|
||||
---
|
||||
name: dev-arch
|
||||
description: 软件架构设计技能。用于系统设计、技术选型、架构评审、设计文档编写。当用户提到架构设计、系统设计、技术方案、API 设计相关任务时自动激活。
|
||||
---
|
||||
|
||||
# 软件架构设计 Skill (dev-arch)
|
||||
|
||||
## 概述
|
||||
|
||||
本技能用于软件架构设计工作,包括:
|
||||
- 系统架构设计与评审
|
||||
- 技术选型与方案对比
|
||||
- API 设计与文档
|
||||
- 数据库设计
|
||||
- 设计模式应用
|
||||
|
||||
---
|
||||
|
||||
## 架构设计流程
|
||||
|
||||
### 1. 需求分析
|
||||
|
||||
```
|
||||
输入:
|
||||
- 业务需求文档
|
||||
- 非功能性需求(性能、安全、可用性)
|
||||
- 技术约束条件
|
||||
|
||||
输出:
|
||||
- 需求清单
|
||||
- 约束条件列表
|
||||
```
|
||||
|
||||
### 2. 概要设计
|
||||
|
||||
```
|
||||
内容:
|
||||
- 系统边界定义
|
||||
- 模块划分
|
||||
- 技术栈选择
|
||||
- 部署架构
|
||||
|
||||
输出:
|
||||
- 架构图
|
||||
- 技术方案文档
|
||||
```
|
||||
|
||||
### 3. 详细设计
|
||||
|
||||
```
|
||||
内容:
|
||||
- API 接口设计
|
||||
- 数据模型设计
|
||||
- 核心流程设计
|
||||
- 异常处理设计
|
||||
|
||||
输出:
|
||||
- API 文档
|
||||
- ER 图
|
||||
- 时序图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构设计模板
|
||||
|
||||
### 系统设计文档模板
|
||||
|
||||
```markdown
|
||||
# [系统名称] 技术设计文档
|
||||
|
||||
## 1. 概述
|
||||
### 1.1 背景
|
||||
[项目背景和目标]
|
||||
|
||||
### 1.2 范围
|
||||
[系统边界和功能范围]
|
||||
|
||||
### 1.3 术语定义
|
||||
| 术语 | 定义 |
|
||||
|------|------|
|
||||
| ... | ... |
|
||||
|
||||
## 2. 系统架构
|
||||
### 2.1 整体架构图
|
||||
[架构图]
|
||||
|
||||
### 2.2 模块说明
|
||||
| 模块 | 职责 | 技术栈 |
|
||||
|------|------|--------|
|
||||
| ... | ... | ... |
|
||||
|
||||
### 2.3 部署架构
|
||||
[部署图]
|
||||
|
||||
## 3. 技术选型
|
||||
### 3.1 技术栈
|
||||
| 类别 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 后端框架 | ... | ... |
|
||||
| 数据库 | ... | ... |
|
||||
| 缓存 | ... | ... |
|
||||
| 消息队列 | ... | ... |
|
||||
|
||||
### 3.2 技术对比
|
||||
[备选方案对比分析]
|
||||
|
||||
## 4. 数据设计
|
||||
### 4.1 ER 图
|
||||
[ER 图]
|
||||
|
||||
### 4.2 核心表设计
|
||||
[表结构说明]
|
||||
|
||||
### 4.3 索引策略
|
||||
[索引设计]
|
||||
|
||||
## 5. API 设计
|
||||
### 5.1 API 规范
|
||||
[RESTful 规范说明]
|
||||
|
||||
### 5.2 核心接口
|
||||
[接口定义]
|
||||
|
||||
## 6. 非功能性设计
|
||||
### 6.1 性能设计
|
||||
- 响应时间目标
|
||||
- 吞吐量目标
|
||||
- 优化策略
|
||||
|
||||
### 6.2 安全设计
|
||||
- 认证授权
|
||||
- 数据安全
|
||||
- 日志审计
|
||||
|
||||
### 6.3 可用性设计
|
||||
- 容错机制
|
||||
- 监控告警
|
||||
- 备份恢复
|
||||
|
||||
## 7. 风险评估
|
||||
| 风险 | 影响 | 概率 | 应对措施 |
|
||||
|------|------|------|----------|
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
## 8. 附录
|
||||
- 参考资料
|
||||
- 变更历史
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用架构模式
|
||||
|
||||
### 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ 表现层 (Presentation) │
|
||||
├─────────────────────────────────┤
|
||||
│ 业务层 (Business) │
|
||||
├─────────────────────────────────┤
|
||||
│ 数据层 (Data Access) │
|
||||
├─────────────────────────────────┤
|
||||
│ 基础设施 (Infrastructure) │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 微服务架构
|
||||
|
||||
```
|
||||
┌─────┐ ┌─────┐ ┌─────┐
|
||||
│ API │ │ API │ │ API │
|
||||
│ GW │──│ SVC │──│ SVC │
|
||||
└──┬──┘ └──┬──┘ └──┬──┘
|
||||
│ │ │
|
||||
└────────┴────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ Message │
|
||||
│ Queue │
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
### 六边形架构 (端口-适配器)
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
┌──────│ Adapters │──────┐
|
||||
│ │ (Inbound) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
┌───┴───┐ ┌──────┴──────┐ ┌────┴────┐
|
||||
│ REST │ │ Core │ │ Repo │
|
||||
│ API │────│ Business │───│ Impl │
|
||||
└───────┘ │ Domain │ └─────────┘
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 设计规范
|
||||
|
||||
### RESTful 规范
|
||||
|
||||
| 方法 | 用途 | 示例 |
|
||||
|------|------|------|
|
||||
| GET | 查询资源 | `GET /users/{id}` |
|
||||
| POST | 创建资源 | `POST /users` |
|
||||
| PUT | 全量更新 | `PUT /users/{id}` |
|
||||
| PATCH | 部分更新 | `PATCH /users/{id}` |
|
||||
| DELETE | 删除资源 | `DELETE /users/{id}` |
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {},
|
||||
"meta": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误码设计
|
||||
|
||||
| 范围 | 类型 | 示例 |
|
||||
|------|------|------|
|
||||
| 10000-19999 | 系统错误 | 10001 内部错误 |
|
||||
| 20000-29999 | 参数错误 | 20001 参数缺失 |
|
||||
| 30000-39999 | 业务错误 | 30001 用户不存在 |
|
||||
| 40000-49999 | 权限错误 | 40001 未授权 |
|
||||
|
||||
---
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 表名 | 小写下划线,复数 | `user_accounts` |
|
||||
| 字段 | 小写下划线 | `created_at` |
|
||||
| 主键 | id | `id` |
|
||||
| 外键 | 表名_id | `user_id` |
|
||||
| 索引 | idx_表名_字段 | `idx_users_email` |
|
||||
|
||||
### 通用字段
|
||||
|
||||
```sql
|
||||
-- 每个表必须包含的字段
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME DEFAULT NULL -- 软删除
|
||||
```
|
||||
|
||||
### 索引策略
|
||||
|
||||
- 主键索引:每表必须
|
||||
- 唯一索引:业务唯一字段
|
||||
- 普通索引:高频查询字段
|
||||
- 联合索引:遵循最左前缀原则
|
||||
|
||||
---
|
||||
|
||||
## 技术选型参考
|
||||
|
||||
### 后端框架
|
||||
|
||||
| 语言 | 框架 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| Go | Gin, Echo | 高并发 API |
|
||||
| Java | Spring Boot | 企业应用 |
|
||||
| Python | FastAPI, Django | 快速开发 |
|
||||
| Node.js | NestJS, Express | 全栈/实时 |
|
||||
|
||||
### 数据库选型
|
||||
|
||||
| 类型 | 选项 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| 关系型 | MySQL, PostgreSQL | 事务型业务 |
|
||||
| 文档型 | MongoDB | 灵活结构 |
|
||||
| 键值型 | Redis | 缓存/会话 |
|
||||
| 时序 | InfluxDB | 监控/IoT |
|
||||
| 搜索 | Elasticsearch | 全文检索 |
|
||||
|
||||
### 消息队列
|
||||
|
||||
| 选项 | 特点 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| Redis | 简单快速 | 轻量级队列 |
|
||||
| RabbitMQ | 可靠传递 | 企业集成 |
|
||||
| Kafka | 高吞吐 | 日志/流处理 |
|
||||
| NATS | 轻量级 | 微服务通信 |
|
||||
|
||||
---
|
||||
|
||||
## 架构评审检查清单
|
||||
|
||||
### 功能性检查
|
||||
- [ ] 需求覆盖完整
|
||||
- [ ] 边界条件处理
|
||||
- [ ] 异常情况处理
|
||||
- [ ] 数据一致性保证
|
||||
|
||||
### 非功能性检查
|
||||
- [ ] 性能目标明确
|
||||
- [ ] 安全措施到位
|
||||
- [ ] 可扩展性设计
|
||||
- [ ] 可维护性考虑
|
||||
|
||||
### 运维检查
|
||||
- [ ] 监控指标定义
|
||||
- [ ] 日志规范
|
||||
- [ ] 部署方案
|
||||
- [ ] 回滚机制
|
||||
|
||||
---
|
||||
|
||||
## 与 ai-proj 集成
|
||||
|
||||
### 创建架构设计任务
|
||||
|
||||
```bash
|
||||
# 创建架构设计任务
|
||||
ai-proj task create --title "[系统名称] 架构设计"
|
||||
|
||||
# 创建子任务
|
||||
ai-proj task create --title "需求分析" --parent-id <parentId>
|
||||
ai-proj task create --title "概要设计" --parent-id <parentId>
|
||||
ai-proj task create --title "详细设计" --parent-id <parentId>
|
||||
ai-proj task create --title "架构评审" --parent-id <parentId>
|
||||
```
|
||||
|
||||
### 关联设计文档
|
||||
|
||||
```bash
|
||||
# 附加设计文档到任务
|
||||
ai-proj task append-doc --id <taskId> --content "设计文档内容 (Markdown)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **KISS 原则** - 保持简单,避免过度设计
|
||||
2. **YAGNI 原则** - 不要预测未来需求
|
||||
3. **DRY 原则** - 避免重复代码
|
||||
4. **SOLID 原则** - 面向对象设计原则
|
||||
5. **关注点分离** - 模块职责单一
|
||||
6. **高内聚低耦合** - 模块独立性
|
||||
|
||||
---
|
||||
|
||||
## 文档工具
|
||||
|
||||
### 架构图工具
|
||||
|
||||
- **draw.io** - 在线免费绘图
|
||||
- **PlantUML** - 代码生成图
|
||||
- **Mermaid** - Markdown 内嵌图
|
||||
- **Excalidraw** - 手绘风格
|
||||
|
||||
### Mermaid 示例
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
Client->>API Gateway: Request
|
||||
API Gateway->>Auth Service: Validate Token
|
||||
Auth Service-->>API Gateway: Valid
|
||||
API Gateway->>Business Service: Forward Request
|
||||
Business Service->>Database: Query
|
||||
Database-->>Business Service: Result
|
||||
Business Service-->>API Gateway: Response
|
||||
API Gateway-->>Client: Response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [系统设计入门](https://github.com/donnemartin/system-design-primer)
|
||||
- [微服务架构](https://microservices.io/)
|
||||
- [RESTful API 设计指南](https://restfulapi.net/)
|
||||
- [数据库设计范式](https://www.guru99.com/database-normalization.html)
|
||||
8
skills-dev/dev-coding-plugin/.claude-plugin/plugin.json
Normal file
8
skills-dev/dev-coding-plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "dev-coding-plugin",
|
||||
"description": "Plugin for dev-coding",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
810
skills-dev/dev-coding-plugin/skills/SKILL.md
Normal file
810
skills-dev/dev-coding-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,810 @@
|
||||
---
|
||||
name: dev-coding
|
||||
description: 软件编码开发技能。用于代码编写、功能实现、代码审查、重构优化。集成 ai-proj CLI 进行任务管理和进度跟踪。支持 Go、Vue、React、iOS、Android、小程序等全栈开发。
|
||||
---
|
||||
|
||||
# 软件编码开发 Skill (dev-coding)
|
||||
|
||||
## ⚠️ REQ 任务自动工作流
|
||||
|
||||
**当收到 REQ 任务(包含 REQ-YYYYMMDD-XXXX)需要开发时,必须严格按以下顺序执行:**
|
||||
|
||||
1. **读取 ticket** — 从 ai-proj 获取需求详情和关联文档
|
||||
```
|
||||
mcp__ai-proj-dev__get_detailed_task_info (通过 REQ 号查找)
|
||||
mcp__ai-proj-dev__get_task_document (如果有 PRD 文档)
|
||||
```
|
||||
|
||||
2. **进入 Plan Mode** — 调用 `EnterPlanMode` 工具
|
||||
- 分析需求,探索代码库,设计实现方案
|
||||
- 输出实现计划(涉及的文件、改动范围、测试策略)
|
||||
- 等待用户审批后再开始编码
|
||||
|
||||
3. **执行计划** — 用户批准后按计划编码 + 写测试
|
||||
|
||||
**禁止跳过 plan mode 直接编码。**
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本技能用于软件编码开发工作,支持多种项目类型:
|
||||
- Go 后端 (Gin + GORM)
|
||||
- Vue 3 / React 前端
|
||||
- iOS (Swift/SwiftUI)
|
||||
- Android (Kotlin/Jetpack Compose)
|
||||
- PDA 应用
|
||||
- MCP 桥接服务
|
||||
- 微服务架构
|
||||
|
||||
核心集成 **ai-proj CLI** 进行任务管理。
|
||||
|
||||
---
|
||||
|
||||
## ai-proj 任务管理
|
||||
|
||||
### 开发任务工作流
|
||||
|
||||
```
|
||||
1. 查看/创建任务 → 2. 启动任务 → 3. 编码实现 → 4. 完成任务 → 5. 记录文档
|
||||
```
|
||||
|
||||
### 任务操作速查
|
||||
|
||||
| 操作 | CLI 命令 | 说明 |
|
||||
|------|----------|------|
|
||||
| 查看任务列表 | `ai-proj task list` | 查看项目所有任务 |
|
||||
| 创建任务 | `ai-proj task create` | 创建新任务 |
|
||||
| 创建子任务 | `ai-proj task create --parent-id` | 分解任务 |
|
||||
| 启动任务 | `ai-proj task start --id` | 开始执行 |
|
||||
| 完成任务 | `ai-proj task complete --id` | 标记完成 |
|
||||
| 更新任务 | `ai-proj task update --id` | 更新状态/描述 |
|
||||
| 查看详情 | `ai-proj task get --id` | 完整任务信息 |
|
||||
| 记录文档 | `ai-proj task append-doc --id` | 附加文档 |
|
||||
|
||||
### 开始新任务
|
||||
|
||||
```bash
|
||||
# 1. 查看任务列表
|
||||
ai-proj task list --status todo,in_progress
|
||||
|
||||
# 2. 启动任务
|
||||
ai-proj task start --id <taskId>
|
||||
|
||||
# 3. 完成后
|
||||
ai-proj task complete --id <taskId>
|
||||
|
||||
# 4. 记录文档
|
||||
ai-proj task append-doc --id <taskId> --content "实现说明"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目类型速查
|
||||
|
||||
### 当前项目生态
|
||||
|
||||
| 项目 | 类型 | 后端 | 前端 | 移动端 |
|
||||
|------|------|------|------|--------|
|
||||
| TWMS | 仓储物流 | Go+Gin+MySQL | Vue 3 | - |
|
||||
| AI-Proj | 项目管理 | Go+Gin+PostgreSQL | React 18 | iOS+Android |
|
||||
| DICIAI | 进销存SaaS | Go+Gin+MySQL | Vue 3 | Android PDA |
|
||||
|
||||
---
|
||||
|
||||
## Go 后端开发
|
||||
|
||||
### 分层架构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/main.go # 入口点
|
||||
├── internal/
|
||||
│ ├── controller/handlers/ # HTTP 处理层
|
||||
│ ├── biz/services/ # 业务逻辑层
|
||||
│ ├── store/database/ # 数据访问层
|
||||
│ └── middleware/ # 中间件
|
||||
├── pkg/
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── errno/ # 错误定义
|
||||
│ ├── api/ # API 类型
|
||||
│ └── util/ # 工具函数
|
||||
└── configs/migrations/ # 配置和迁移
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
```go
|
||||
// 包声明和导入组织
|
||||
package main
|
||||
|
||||
import (
|
||||
// 标准库
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
// 第三方
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
// 项目内部
|
||||
"project/internal/pkg/errno"
|
||||
)
|
||||
|
||||
// 错误处理 (Errno 模式)
|
||||
if err != nil {
|
||||
core.WriteResponse(c, errno.ErrBind, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 接口定义
|
||||
type IStore interface {
|
||||
Users() UserStore
|
||||
}
|
||||
|
||||
// 服务注入
|
||||
type userBiz struct {
|
||||
ds store.IStore
|
||||
}
|
||||
|
||||
func NewUserBiz(ds store.IStore) *userBiz {
|
||||
return &userBiz{ds: ds}
|
||||
}
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
make build
|
||||
go build -o ./_output/main ./cmd/main.go
|
||||
|
||||
# 运行
|
||||
./_output/main --config ./configs/config.yaml
|
||||
|
||||
# 测试
|
||||
make test
|
||||
go test -v ./...
|
||||
|
||||
# 代码检查
|
||||
make lint
|
||||
golangci-lint run
|
||||
|
||||
# Swagger 文档
|
||||
make swagger
|
||||
swag init -g cmd/main.go
|
||||
```
|
||||
|
||||
### 数据库模型
|
||||
|
||||
```go
|
||||
type UserM struct {
|
||||
Id int64 `gorm:"column:id;primary_key"`
|
||||
Username string `gorm:"column:username;not null"`
|
||||
CreateTime int64 `gorm:"column:create_time"`
|
||||
UpdateTime int64 `gorm:"column:update_time"`
|
||||
DeletedAt soft_delete.DeletedAt `gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *UserM) TableName() string { return "users" }
|
||||
|
||||
func (m *UserM) BeforeCreate(tx *gorm.DB) error {
|
||||
m.CreateTime = time.Now().Unix()
|
||||
m.UpdateTime = m.CreateTime
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端 data-testid 规范
|
||||
|
||||
编写或修改前端组件时,**所有可交互元素必须加 `data-testid`**。
|
||||
|
||||
**命名格式:** `<模块>-<元素类型>[-<标识>]`
|
||||
|
||||
```vue
|
||||
<!-- ✅ 正确 -->
|
||||
<a-input data-testid="product-input-name" v-model:value="form.name" placeholder="商品名称" />
|
||||
<a-button data-testid="product-btn-submit" type="primary">创建商品</a-button>
|
||||
<a-select data-testid="product-select-brand" v-model:value="form.brandId" />
|
||||
<a-table data-testid="product-table" :dataSource="list" />
|
||||
|
||||
<!-- ❌ 错误 — 交互元素无 data-testid -->
|
||||
<a-input v-model:value="form.name" placeholder="商品名称" />
|
||||
<a-button type="primary">创建商品</a-button>
|
||||
```
|
||||
|
||||
**必须加:** 输入框、选择器、开关、按钮(提交/取消/删除)、表格、模态框确认按钮、导航菜单项
|
||||
**不需要加:** 纯展示文本、图标、布局容器(Row/Col/Space)
|
||||
|
||||
---
|
||||
|
||||
## Vue 3 前端开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── api/ # API 服务 (按模块分组)
|
||||
│ ├── wms/ # 仓储管理
|
||||
│ ├── oms/ # 订单管理
|
||||
│ └── system/ # 系统管理
|
||||
├── views/ # 页面组件
|
||||
├── components/ # 可复用组件
|
||||
├── store/modules/ # Pinia 状态
|
||||
├── router/ # 路由配置
|
||||
├── utils/
|
||||
│ ├── request.ts # Axios 拦截器
|
||||
│ └── permission.ts # 权限检查
|
||||
└── i18n/ # 国际化
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
```typescript
|
||||
// API 服务层
|
||||
// api/user/model/index.ts
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
// api/user/index.ts
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { User } from './model';
|
||||
|
||||
export async function getUsers(params: UserParams) {
|
||||
const res = await request.get<ApiResult<PageResult<User>>>(
|
||||
'/v1/users',
|
||||
{ params }
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
pnpm install # DICIAI 使用 pnpm
|
||||
|
||||
# 开发
|
||||
npm run dev
|
||||
|
||||
# 构建
|
||||
npm run build:prod
|
||||
npm run build:test
|
||||
|
||||
# 代码检查
|
||||
npm run lint:eslint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React 前端开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── pages/ # 页面组件
|
||||
├── components/ # 可复用组件
|
||||
├── services/ # API 服务
|
||||
├── hooks/ # 自定义 Hooks
|
||||
├── contexts/ # Context Providers
|
||||
├── types/ # TypeScript 类型
|
||||
├── utils/ # 工具函数
|
||||
└── config/ # 配置
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
```typescript
|
||||
// Context 集成
|
||||
<QueryProvider>
|
||||
<AuthProvider>
|
||||
<TimerProvider>
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<Router>
|
||||
<Routes />
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
</TimerProvider>
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
|
||||
// API 服务
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = TokenManager.getToken();
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
npm start
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 测试
|
||||
npm test
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## iOS 开发 (Swift/SwiftUI)
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
AI-Proj-iOS/
|
||||
├── Core/ # 核心服务
|
||||
│ ├── Network/ # 网络层
|
||||
│ ├── Storage/ # 本地存储
|
||||
│ └── Auth/ # 认证
|
||||
├── Features/ # 功能模块
|
||||
│ ├── Dashboard/
|
||||
│ ├── Tasks/
|
||||
│ └── Settings/
|
||||
├── Models/ # 数据模型
|
||||
└── UI/ # UI 组件
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
```swift
|
||||
// MVVM 架构
|
||||
class TaskViewModel: ObservableObject {
|
||||
@Published var tasks: [Task] = []
|
||||
@Published var isLoading = false
|
||||
|
||||
private let taskService: TaskServiceProtocol
|
||||
|
||||
init(taskService: TaskServiceProtocol = TaskService()) {
|
||||
self.taskService = taskService
|
||||
}
|
||||
|
||||
func fetchTasks() async {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
tasks = try await taskService.getTasks()
|
||||
} catch {
|
||||
// 错误处理
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SwiftUI 视图
|
||||
struct TaskListView: View {
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
|
||||
var body: some View {
|
||||
List(viewModel.tasks) { task in
|
||||
TaskRow(task: task)
|
||||
}
|
||||
.task {
|
||||
await viewModel.fetchTasks()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 构建命令
|
||||
|
||||
```bash
|
||||
# Xcode 构建
|
||||
xcodebuild -scheme AI-Proj-iOS -configuration Debug
|
||||
|
||||
# 测试
|
||||
xcodebuild test -scheme AI-Proj-iOS
|
||||
```
|
||||
|
||||
### 常见问题排查
|
||||
|
||||
#### SwiftLint 沙盒错误
|
||||
|
||||
**问题描述**:
|
||||
构建时出现错误:
|
||||
```
|
||||
Sandbox: swiftlint(xxxx) deny(1) file-read-data /path/to/.swiftlint.yml
|
||||
```
|
||||
|
||||
**原因**:
|
||||
Xcode 15+ 默认启用 User Script Sandboxing,限制脚本访问文件系统。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
方案 1 - 修改项目配置(推荐):
|
||||
1. 打开 Xcode → 选择项目 → Build Settings
|
||||
2. 搜索 "User Script Sandboxing"
|
||||
3. 将 `ENABLE_USER_SCRIPT_SANDBOXING` 设置为 `NO`
|
||||
|
||||
方案 2 - 命令行构建时禁用:
|
||||
```bash
|
||||
xcodebuild -scheme AI-Proj-iOS -configuration Debug \
|
||||
ENABLE_USER_SCRIPT_SANDBOXING=NO
|
||||
```
|
||||
|
||||
方案 3 - 直接修改 project.pbxproj:
|
||||
```bash
|
||||
sed -i '' 's/ENABLE_USER_SCRIPT_SANDBOXING = YES/ENABLE_USER_SCRIPT_SANDBOXING = NO/g' \
|
||||
AI-Proj-iOS.xcodeproj/project.pbxproj
|
||||
```
|
||||
|
||||
#### Personal Development Team 功能限制
|
||||
|
||||
**问题描述**:
|
||||
使用免费 Personal Team 签名时报错:
|
||||
```
|
||||
Cannot create iOS App Development provisioning profile...
|
||||
Personal development teams do not support the Associated Domains,
|
||||
Push Notifications and App Groups capabilities.
|
||||
```
|
||||
|
||||
**原因**:
|
||||
Personal Team(免费账户)不支持以下 Entitlements:
|
||||
- Associated Domains (`com.apple.developer.associated-domains`)
|
||||
- Push Notifications (`aps-environment`)
|
||||
- App Groups (`com.apple.security.application-groups`)
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. 从 Entitlements 文件中移除不支持的功能:
|
||||
|
||||
```xml
|
||||
<!-- AI-Proj-iOS.entitlements -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- 仅保留 Personal Team 支持的功能 -->
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.yourcompany.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
2. Personal Team 支持的功能:
|
||||
- Keychain Access Groups ✓
|
||||
- In-App Purchase ✓
|
||||
- Game Center ✓
|
||||
|
||||
3. 需要付费 Apple Developer Program 的功能:
|
||||
- Push Notifications ✗
|
||||
- Associated Domains ✗
|
||||
- App Groups ✗
|
||||
- CloudKit ✗
|
||||
- Sign in with Apple ✗
|
||||
|
||||
---
|
||||
|
||||
## Android 开发 (Kotlin)
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
android-app/app/src/main/
|
||||
├── java/com/project/
|
||||
│ ├── ui/ # UI 层
|
||||
│ │ ├── screens/ # Compose 屏幕
|
||||
│ │ └── components/ # 可复用组件
|
||||
│ ├── data/ # 数据层
|
||||
│ │ ├── api/ # 网络接口
|
||||
│ │ ├── repository/ # 仓库模式
|
||||
│ │ └── local/ # 本地存储
|
||||
│ ├── domain/ # 业务逻辑
|
||||
│ └── di/ # 依赖注入
|
||||
└── res/ # 资源文件
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
```kotlin
|
||||
// Hilt 依赖注入
|
||||
@HiltViewModel
|
||||
class TaskViewModel @Inject constructor(
|
||||
private val taskRepository: TaskRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
|
||||
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
|
||||
|
||||
fun fetchTasks() {
|
||||
viewModelScope.launch {
|
||||
taskRepository.getTasks()
|
||||
.collect { _tasks.value = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Jetpack Compose
|
||||
@Composable
|
||||
fun TaskListScreen(
|
||||
viewModel: TaskViewModel = hiltViewModel()
|
||||
) {
|
||||
val tasks by viewModel.tasks.collectAsState()
|
||||
|
||||
LazyColumn {
|
||||
items(tasks) { task ->
|
||||
TaskItem(task = task)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 构建命令
|
||||
|
||||
```bash
|
||||
# 构建 Debug
|
||||
./gradlew assembleDebug
|
||||
|
||||
# 构建 Release
|
||||
./gradlew assembleRelease
|
||||
|
||||
# 测试
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PDA 应用开发
|
||||
|
||||
### 特点
|
||||
|
||||
- Android 原生开发
|
||||
- 扫码枪集成
|
||||
- 离线优先
|
||||
- 简洁 UI
|
||||
|
||||
### 常见功能
|
||||
|
||||
```kotlin
|
||||
// 扫码处理
|
||||
class ScanReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val barcode = intent.getStringExtra("SCAN_BARCODE")
|
||||
// 处理扫码结果
|
||||
}
|
||||
}
|
||||
|
||||
// 离线存储
|
||||
@Entity(tableName = "inventory")
|
||||
data class Inventory(
|
||||
@PrimaryKey val id: Long,
|
||||
val barcode: String,
|
||||
val quantity: Int,
|
||||
@ColumnInfo(name = "sync_status")
|
||||
val syncStatus: SyncStatus = SyncStatus.PENDING
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP 桥接开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
mcp-task-bridge/
|
||||
├── index.ts # 入口
|
||||
├── task-service.ts # 任务服务
|
||||
├── document-service.ts # 文档服务
|
||||
├── base-client.ts # HTTP 基类
|
||||
├── types.ts # 类型定义
|
||||
└── token-storage.ts # Token 管理
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
|
||||
```typescript
|
||||
// 服务类模式
|
||||
export class TaskService extends BaseClient {
|
||||
async createTask(
|
||||
title: string,
|
||||
projectId: number = 1,
|
||||
options: CreateTaskOptions = {}
|
||||
): Promise<ApiResponse<Task>> {
|
||||
try {
|
||||
const response = await this.makeRequest<Task>(
|
||||
'POST',
|
||||
`/projects/${projectId}/tasks`,
|
||||
{ title, project_id: projectId, ...options }
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
message: `✅ 任务 "${title}" 创建成功`
|
||||
};
|
||||
}
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: `创建任务失败: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通用开发规范
|
||||
|
||||
### API 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页参数
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"sort": "created_at",
|
||||
"order": "desc"
|
||||
}
|
||||
```
|
||||
|
||||
### 认证方式
|
||||
|
||||
- JWT Token
|
||||
- Header: `Authorization: Bearer <token>`
|
||||
|
||||
### 错误处理
|
||||
|
||||
```go
|
||||
// Go
|
||||
if err != nil {
|
||||
core.WriteResponse(c, errno.ErrXxx, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// TypeScript
|
||||
try {
|
||||
const result = await api.call();
|
||||
} catch (error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
|
||||
// Swift
|
||||
do {
|
||||
let result = try await service.fetch()
|
||||
} catch {
|
||||
// 处理错误
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git 工作流
|
||||
|
||||
### 提交规范
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| feat | 新功能 |
|
||||
| fix | Bug 修复 |
|
||||
| docs | 文档 |
|
||||
| refactor | 重构 |
|
||||
| test | 测试 |
|
||||
| chore | 杂项 |
|
||||
|
||||
### 双电脑同步 (au-dev / cn-dev)
|
||||
|
||||
```bash
|
||||
# 离开时
|
||||
git add -A
|
||||
git commit -m "WIP: sync from $(hostname)"
|
||||
git push origin $(git branch --show-current)
|
||||
|
||||
# 到达时
|
||||
git fetch origin
|
||||
git pull origin $(git branch --show-current)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 标准配置
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
# 或 postgres:15
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
```
|
||||
|
||||
### 常用端口
|
||||
|
||||
| 服务 | 端口 |
|
||||
|------|------|
|
||||
| Backend | 8080 / 9099 |
|
||||
| Frontend | 80 / 3000 |
|
||||
| MySQL | 3306 |
|
||||
| PostgreSQL | 5432 |
|
||||
| Redis | 6379 |
|
||||
|
||||
---
|
||||
|
||||
## Push 前必须通过:变更包单元测试
|
||||
|
||||
**在 `git push` 或 `/pr create` 之前,必须跑所有变更文件对应包的单元测试。**
|
||||
|
||||
```bash
|
||||
# 找出变更的 Go 文件所在包,跑对应测试
|
||||
PKGS=$(git diff --name-only origin/main...HEAD | grep '\.go$' | grep -v '_test\.go' | sed 's|/[^/]*$||' | sort -u | sed 's|^|./|' | tr '\n' ' ')
|
||||
|
||||
if [ -n "$PKGS" ]; then
|
||||
echo "Running tests for changed packages: $PKGS"
|
||||
go test -v -count=1 $PKGS
|
||||
else
|
||||
echo "No Go files changed, skipping tests"
|
||||
fi
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- 测试通过 → 继续 push + `/pr create`
|
||||
- 测试失败 → 尝试自动修复,修复后重跑
|
||||
- 修复成功 → 继续 push
|
||||
- **修复失败 → 禁止 push,向用户报告失败原因,等待指示**
|
||||
- 仅改了 `_test.go` → 同样需要跑(验证测试本身通过)
|
||||
- 无 Go 文件变更(纯前端/文档) → 跳过
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **任务驱动** - 使用 ai-proj 管理所有开发任务
|
||||
2. **分层清晰** - Controller → Service → Repository
|
||||
3. **接口先行** - 先定义接口再实现
|
||||
4. **小步提交** - 频繁提交,每次做一件事
|
||||
5. **测试覆盖** - 核心逻辑必须有测试
|
||||
6. **文档同步** - 代码变更同步更新文档
|
||||
8
skills-dev/dev-plugin/.claude-plugin/plugin.json
Normal file
8
skills-dev/dev-plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "dev-plugin",
|
||||
"description": "Plugin for dev",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
314
skills-dev/dev-plugin/skills/SKILL.md
Normal file
314
skills-dev/dev-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,314 @@
|
||||
---
|
||||
name: dev
|
||||
description: 软件开发技能组入口。整合架构设计(dev-arch)、编码实现(dev-coding)、测试(dev-test)三个子技能,提供完整的软件开发工作流。支持全栈开发:Go、Vue、React、iOS、Android、小程序等。
|
||||
---
|
||||
|
||||
# 软件开发 Skill (dev)
|
||||
|
||||
## 概述
|
||||
|
||||
dev 是一个技能组入口,整合了软件开发的三个核心阶段:
|
||||
|
||||
| 子技能 | 用途 | 触发词 |
|
||||
|--------|------|--------|
|
||||
| **dev-arch** | 架构设计、技术选型、系统设计 | 架构、设计、技术方案 |
|
||||
| **dev-coding** | 编码实现、功能开发、代码审查 | 编码、开发、实现 |
|
||||
| **dev-test** | 单元测试、集成测试、E2E测试 | 测试、test、覆盖率 |
|
||||
|
||||
---
|
||||
|
||||
## 开发工作流
|
||||
|
||||
```
|
||||
需求分析 → 架构设计 → 编码实现 → 测试验证 → 部署上线
|
||||
↓ ↓ ↓
|
||||
dev-arch dev-coding dev-test
|
||||
```
|
||||
|
||||
### 典型流程
|
||||
|
||||
1. **架构设计** (dev-arch)
|
||||
- 需求分析
|
||||
- 技术选型
|
||||
- 系统设计文档
|
||||
- 架构评审
|
||||
|
||||
2. **编码实现** (dev-coding)
|
||||
- 任务分解
|
||||
- 功能开发
|
||||
- 代码审查
|
||||
- 文档记录
|
||||
|
||||
3. **测试验证** (dev-test)
|
||||
- 单元测试
|
||||
- 集成测试
|
||||
- E2E 测试
|
||||
- 覆盖率分析
|
||||
|
||||
---
|
||||
|
||||
## 支持的项目类型
|
||||
|
||||
### 当前项目生态
|
||||
|
||||
| 项目 | 类型 | 技术栈 |
|
||||
|------|------|--------|
|
||||
| **TWMS** | 仓储物流 | Go + Vue 3 + MySQL |
|
||||
| **AI-Proj** | 项目管理 | Go + React + PostgreSQL + iOS + Android |
|
||||
| **DICIAI** | 进销存SaaS | Go + Vue 3 + MySQL + Android PDA |
|
||||
|
||||
### 技术栈矩阵
|
||||
|
||||
| 端 | 语言/框架 | 工具 |
|
||||
|-----|----------|------|
|
||||
| **后端** | Go (Gin + GORM) | MySQL/PostgreSQL, Redis, Docker |
|
||||
| **Web前端** | Vue 3 / React 18 | TypeScript, Vite/CRA, Ant Design |
|
||||
| **iOS** | Swift + SwiftUI | Xcode, XCTest |
|
||||
| **Android** | Kotlin + Compose | Gradle, Hilt, Room |
|
||||
| **PDA** | Android 原生 | 扫码枪集成, 离线存储 |
|
||||
| **MCP** | TypeScript | Node.js, MCP SDK |
|
||||
|
||||
---
|
||||
|
||||
## ai-proj 任务管理集成
|
||||
|
||||
所有开发工作都通过 ai-proj CLI 进行任务管理:
|
||||
|
||||
### 快速开始
|
||||
|
||||
```bash
|
||||
# 1. 查看待办任务
|
||||
ai-proj task list --status in_progress,todo
|
||||
|
||||
# 2. 启动任务
|
||||
ai-proj task start --id <taskId>
|
||||
|
||||
# 3. 完成任务
|
||||
ai-proj task complete --id <taskId>
|
||||
|
||||
# 4. 记录文档
|
||||
ai-proj task append-doc --id <taskId> --content "实现说明"
|
||||
```
|
||||
|
||||
### 任务分解
|
||||
|
||||
```bash
|
||||
# 创建主任务
|
||||
ai-proj task create --title "功能名称"
|
||||
|
||||
# 创建子任务
|
||||
ai-proj task create --title "架构设计" --parent-id <parentId>
|
||||
ai-proj task create --title "功能开发" --parent-id <parentId>
|
||||
ai-proj task create --title "测试验证" --parent-id <parentId>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用命令速查
|
||||
|
||||
### Go 后端
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
make build
|
||||
|
||||
# 运行
|
||||
./_output/main --config ./configs/config.yaml
|
||||
|
||||
# 测试
|
||||
make test
|
||||
make cover
|
||||
```
|
||||
|
||||
### Vue 前端
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
npm run dev
|
||||
|
||||
# 构建
|
||||
npm run build:prod
|
||||
|
||||
# 检查
|
||||
npm run lint:eslint
|
||||
```
|
||||
|
||||
### React 前端
|
||||
|
||||
```bash
|
||||
# 开发
|
||||
npm start
|
||||
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 测试
|
||||
npm test
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
xcodebuild -scheme ProjectName -configuration Debug
|
||||
|
||||
# 测试
|
||||
xcodebuild test -scheme ProjectName
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
./gradlew assembleDebug
|
||||
./gradlew assembleRelease
|
||||
|
||||
# 测试
|
||||
./gradlew test
|
||||
./gradlew connectedAndroidTest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git 工作流
|
||||
|
||||
### 提交规范
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| feat | 新功能 |
|
||||
| fix | Bug 修复 |
|
||||
| docs | 文档 |
|
||||
| refactor | 重构 |
|
||||
| test | 测试 |
|
||||
| chore | 杂项 |
|
||||
|
||||
### 分支策略
|
||||
|
||||
```bash
|
||||
# 功能开发
|
||||
git checkout -b feature/功能名称
|
||||
|
||||
# 提交
|
||||
git commit -m "feat: 功能描述"
|
||||
|
||||
# 推送
|
||||
git push origin feature/功能名称
|
||||
|
||||
# 合并
|
||||
git checkout main
|
||||
git merge feature/功能名称
|
||||
```
|
||||
|
||||
### 双电脑同步 (au-dev / cn-dev)
|
||||
|
||||
```bash
|
||||
# 离开时
|
||||
git add -A
|
||||
git commit -m "WIP: sync from $(hostname)"
|
||||
git push origin $(git branch --show-current)
|
||||
|
||||
# 到达时
|
||||
git fetch origin
|
||||
git pull origin $(git branch --show-current)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 标准配置
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
# 或 postgres:15
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
```
|
||||
|
||||
### 常用端口
|
||||
|
||||
| 服务 | 端口 |
|
||||
|------|------|
|
||||
| Backend | 8080 / 9099 |
|
||||
| Frontend | 80 / 3000 |
|
||||
| MySQL | 3306 |
|
||||
| PostgreSQL | 5432 |
|
||||
| Redis | 6379 |
|
||||
|
||||
---
|
||||
|
||||
## 子技能详情
|
||||
|
||||
### dev-arch (架构设计)
|
||||
|
||||
用于系统设计阶段:
|
||||
- 需求分析
|
||||
- 技术选型
|
||||
- 架构设计文档
|
||||
- API 设计
|
||||
- 数据库设计
|
||||
- 架构评审
|
||||
|
||||
### dev-coding (编码实现)
|
||||
|
||||
用于开发实现阶段:
|
||||
- Go 后端开发
|
||||
- Vue/React 前端开发
|
||||
- iOS/Android 移动开发
|
||||
- PDA 应用开发
|
||||
- MCP 桥接开发
|
||||
- 代码审查
|
||||
|
||||
### dev-test (测试)
|
||||
|
||||
用于测试验证阶段:
|
||||
- 单元测试
|
||||
- 集成测试
|
||||
- E2E 测试
|
||||
- UI 测试
|
||||
- 覆盖率分析
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **任务驱动** - 使用 ai-proj 管理所有开发任务
|
||||
2. **设计先行** - 复杂功能先设计后编码
|
||||
3. **分层清晰** - Controller → Service → Repository
|
||||
4. **小步提交** - 频繁提交,每次做一件事
|
||||
5. **测试覆盖** - 核心逻辑必须有测试
|
||||
6. **文档同步** - 代码变更同步更新文档
|
||||
7. **代码审查** - 重要变更必须审查
|
||||
|
||||
---
|
||||
|
||||
## 何时使用哪个子技能
|
||||
|
||||
| 场景 | 推荐技能 |
|
||||
|------|----------|
|
||||
| 新功能设计 | dev-arch |
|
||||
| 技术方案评审 | dev-arch |
|
||||
| 功能开发实现 | dev-coding |
|
||||
| Bug 修复 | dev-coding |
|
||||
| 编写测试 | dev-test |
|
||||
| 测试覆盖率提升 | dev-test |
|
||||
| 代码审查 | dev-coding |
|
||||
| 性能优化 | dev-arch + dev-coding |
|
||||
8
skills-dev/dev-test-plugin/.claude-plugin/plugin.json
Normal file
8
skills-dev/dev-test-plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "dev-test-plugin",
|
||||
"description": "软件测试技能。用于单元测试、集成测试、E2E测试、测试用例设计。支持 Go、Vue、React、iOS、Android 等多平台测试。",
|
||||
"version": "2.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
141
skills-dev/dev-test-plugin/skills/dev-test/SKILL.md
Normal file
141
skills-dev/dev-test-plugin/skills/dev-test/SKILL.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: dev-test
|
||||
description: 软件测试技能。用于单元测试、集成测试、E2E测试、测试用例设计。支持 Go、Vue、React、iOS、Android 等多平台测试。
|
||||
---
|
||||
|
||||
# 软件测试 Skill (dev-test)
|
||||
|
||||
## 子文件索引
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `go-testing.md` | Go 后端测试 (testify + test DB + httptest)。**biz 层禁止 mock,必须用真实 PostgreSQL test DB** |
|
||||
| `frontend-testing.md` | Vue (Vitest) + React (Jest) 前端测试 |
|
||||
| `ios-testing.md` | iOS 测试 (XCTest + Swift Concurrency) |
|
||||
| `android-testing.md` | Android 测试 (JUnit + Espresso + Compose) |
|
||||
| `e2e-testing.md` | E2E Playwright + Coolbuy PaaS 集成测试 |
|
||||
|
||||
---
|
||||
|
||||
## 测试金字塔
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E (少量)
|
||||
/----\
|
||||
/ \ 集成测试 (适量)
|
||||
/--------\
|
||||
/ \ 单元测试 (大量)
|
||||
/------------\
|
||||
```
|
||||
|
||||
| 类型 | 范围 | 速度 | 数量 |
|
||||
|------|------|------|------|
|
||||
| 单元测试 | 函数/方法 | 快 | 多 |
|
||||
| 集成测试 | 模块交互 | 中 | 适量 |
|
||||
| E2E 测试 | 完整流程 | 慢 | 少 |
|
||||
|
||||
---
|
||||
|
||||
## 测试命令速查
|
||||
|
||||
| 平台 | 命令 | 详见 |
|
||||
|------|------|------|
|
||||
| Go | `make test` / `go test ./...` | `go-testing.md` |
|
||||
| Vue | `npm run test` | `frontend-testing.md` |
|
||||
| React | `npm test` | `frontend-testing.md` |
|
||||
| iOS | `xcodebuild test` | `ios-testing.md` |
|
||||
| Android | `./gradlew test` | `android-testing.md` |
|
||||
| E2E (通用) | `npm run test:e2e` | `e2e-testing.md` |
|
||||
| E2E (Coolbuy PaaS) | `make e2e` | `e2e-testing.md` |
|
||||
|
||||
---
|
||||
|
||||
## Chrome DevTools MCP (AI 浏览器调试)
|
||||
|
||||
> Google 官方 MCP 服务器,让 AI 助手直接控制和检查 Chrome 浏览器。
|
||||
|
||||
```bash
|
||||
claude mcp add chrome-devtools npx chrome-devtools-mcp@latest
|
||||
```
|
||||
|
||||
| 分类 | 工具 | 说明 |
|
||||
|------|------|------|
|
||||
| **输入** | `click` / `fill` / `fill_form` / `hover` / `upload_file` | 页面交互 |
|
||||
| **导航** | `navigate_page` / `new_page` / `list_pages` / `wait_for` | 页面导航 |
|
||||
| **调试** | `evaluate_script` / `list_console_messages` / `take_screenshot` | 调试工具 |
|
||||
| **网络** | `list_network_requests` / `get_network_request` | 网络分析 |
|
||||
| **性能** | `performance_start_trace` / `performance_stop_trace` | 性能追踪 |
|
||||
| **模拟** | `emulate_device` / `throttle_network` / `throttle_cpu` | 环境模拟 |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例设计
|
||||
|
||||
### 等价类划分
|
||||
|
||||
| 输入 | 有效类 | 无效类 |
|
||||
|------|--------|--------|
|
||||
| 用户名 | 3-64字符 | <3, >64 |
|
||||
| 年龄 | 0-150 | <0, >150 |
|
||||
| 邮箱 | 有效格式 | 无效格式 |
|
||||
|
||||
### 边界值
|
||||
|
||||
```
|
||||
范围 [1, 100]:
|
||||
测试点: 0, 1, 2, 99, 100, 101
|
||||
```
|
||||
|
||||
### 测试用例模板
|
||||
|
||||
```markdown
|
||||
## TC-001: 用户登录成功
|
||||
|
||||
**前置条件**: 用户已注册
|
||||
**步骤**:
|
||||
1. 输入有效用户名
|
||||
2. 输入有效密码
|
||||
3. 点击登录
|
||||
|
||||
**预期**: 跳转到首页
|
||||
**优先级**: P0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 覆盖率目标
|
||||
|
||||
| 类型 | 目标 |
|
||||
|------|------|
|
||||
| 行覆盖 | >80% |
|
||||
| 分支覆盖 | >70% |
|
||||
| 函数覆盖 | >90% |
|
||||
|
||||
---
|
||||
|
||||
## 与 ai-proj 集成
|
||||
|
||||
```bash
|
||||
# 创建测试任务
|
||||
ai-proj task create --title "[模块] 单元测试"
|
||||
|
||||
# 记录测试结果
|
||||
ai-proj task append-doc --id <taskId> --content "# 测试报告
|
||||
- 覆盖率: 85%
|
||||
- 通过: 42
|
||||
- 失败: 0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **测试金字塔** - 多单元测试,少 E2E
|
||||
2. **测试隔离** - 每个测试独立
|
||||
3. **命名清晰** - 描述预期行为
|
||||
4. **快速反馈** - 测试要快
|
||||
5. **持续集成** - 每次提交运行
|
||||
6. **Biz 层禁止 Mock** - biz/service 层必须使用真实 PostgreSQL test DB + 真实 store,mock 等于没测
|
||||
7. **Mock 仅限 Handler 层** - handler 层可以 mock biz 接口 + httptest
|
||||
7. **李宁测试用例** - Excel 导出见 `coolbuy-legacy` 技能的 `test-cases-excel.md`
|
||||
145
skills-dev/dev-test-plugin/skills/dev-test/android-testing.md
Normal file
145
skills-dev/dev-test-plugin/skills/dev-test/android-testing.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Android 测试 (JUnit + Espresso)
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 单元测试
|
||||
./gradlew test
|
||||
|
||||
# UI 测试
|
||||
./gradlew connectedAndroidTest
|
||||
```
|
||||
|
||||
## 单元测试 (JUnit)
|
||||
|
||||
```kotlin
|
||||
class TaskViewModelTest {
|
||||
@get:Rule
|
||||
val instantTaskRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val coroutineRule = MainCoroutineRule()
|
||||
|
||||
private lateinit var viewModel: TaskViewModel
|
||||
private lateinit var repository: FakeTaskRepository
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
repository = FakeTaskRepository()
|
||||
viewModel = TaskViewModel(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetchTasks updates state`() = runTest {
|
||||
// Arrange
|
||||
repository.addTasks(listOf(
|
||||
Task(1, "Task 1"),
|
||||
Task(2, "Task 2")
|
||||
))
|
||||
|
||||
// Act
|
||||
viewModel.fetchTasks()
|
||||
|
||||
// Assert
|
||||
val tasks = viewModel.tasks.first()
|
||||
assertEquals(2, tasks.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createTask adds task`() = runTest {
|
||||
// Act
|
||||
viewModel.createTask("New Task")
|
||||
|
||||
// Assert
|
||||
val tasks = viewModel.tasks.first()
|
||||
assertTrue(tasks.any { it.title == "New Task" })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UI 测试 (Espresso)
|
||||
|
||||
```kotlin
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class TaskListActivityTest {
|
||||
|
||||
@get:Rule
|
||||
val activityRule = ActivityScenarioRule(TaskListActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun displayTaskList() {
|
||||
onView(withId(R.id.taskList))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clickTask_opensDetail() {
|
||||
onView(withId(R.id.taskList))
|
||||
.perform(RecyclerViewActions.actionOnItemAtPosition<TaskViewHolder>(0, click()))
|
||||
|
||||
onView(withId(R.id.taskDetail))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addTask_showsInList() {
|
||||
// Click add button
|
||||
onView(withId(R.id.addButton)).perform(click())
|
||||
|
||||
// Enter title
|
||||
onView(withId(R.id.titleInput))
|
||||
.perform(typeText("New Task"), closeSoftKeyboard())
|
||||
|
||||
// Save
|
||||
onView(withId(R.id.saveButton)).perform(click())
|
||||
|
||||
// Verify in list
|
||||
onView(withText("New Task"))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Compose UI 测试
|
||||
|
||||
```kotlin
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TaskListScreenTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun taskList_displays() {
|
||||
val tasks = listOf(
|
||||
Task(1, "Task 1"),
|
||||
Task(2, "Task 2")
|
||||
)
|
||||
|
||||
composeRule.setContent {
|
||||
TaskListScreen(tasks = tasks)
|
||||
}
|
||||
|
||||
composeRule.onNodeWithText("Task 1").assertExists()
|
||||
composeRule.onNodeWithText("Task 2").assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskClick_callsOnClick() {
|
||||
var clickedId: Int? = null
|
||||
val tasks = listOf(Task(1, "Task 1"))
|
||||
|
||||
composeRule.setContent {
|
||||
TaskListScreen(
|
||||
tasks = tasks,
|
||||
onTaskClick = { clickedId = it.id }
|
||||
)
|
||||
}
|
||||
|
||||
composeRule.onNodeWithText("Task 1").performClick()
|
||||
|
||||
assertEquals(1, clickedId)
|
||||
}
|
||||
}
|
||||
```
|
||||
169
skills-dev/dev-test-plugin/skills/dev-test/e2e-testing.md
Normal file
169
skills-dev/dev-test-plugin/skills/dev-test/e2e-testing.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# E2E 测试 (Playwright)
|
||||
|
||||
## 通用 Playwright 配置
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
timeout: 30000,
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { browserName: 'chromium' } },
|
||||
{ name: 'firefox', use: { browserName: 'firefox' } },
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## 通用 E2E 测试示例
|
||||
|
||||
```typescript
|
||||
// login.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Login', () => {
|
||||
test('successful login', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
|
||||
await page.fill('[data-testid="username"]', 'testuser')
|
||||
await page.fill('[data-testid="password"]', 'password')
|
||||
await page.click('[data-testid="submit"]')
|
||||
|
||||
await expect(page).toHaveURL('/dashboard')
|
||||
await expect(page.locator('.welcome')).toContainText('testuser')
|
||||
})
|
||||
|
||||
test('invalid credentials', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
|
||||
await page.fill('[data-testid="username"]', 'wrong')
|
||||
await page.fill('[data-testid="password"]', 'wrong')
|
||||
await page.click('[data-testid="submit"]')
|
||||
|
||||
await expect(page.locator('.error')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Task Management', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await page.fill('[data-testid="username"]', 'testuser')
|
||||
await page.fill('[data-testid="password"]', 'password')
|
||||
await page.click('[data-testid="submit"]')
|
||||
})
|
||||
|
||||
test('create task', async ({ page }) => {
|
||||
await page.click('[data-testid="new-task"]')
|
||||
await page.fill('[data-testid="task-title"]', 'E2E Test Task')
|
||||
await page.click('[data-testid="save"]')
|
||||
|
||||
await expect(page.locator('text=E2E Test Task')).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coolbuy PaaS E2E 集成测试
|
||||
|
||||
> Playwright 全链路 E2E 测试,独立环境(DB + 端口),可与 dev 服务并行运行。
|
||||
|
||||
### 环境架构
|
||||
|
||||
| 服务 | Dev 端口 | E2E 端口 | DB |
|
||||
|------|---------|---------|-----|
|
||||
| Auth Service | 7089 | 7189 | coolbuy_paas_e2e |
|
||||
| Foundation Service | 7090 | 7190 | coolbuy_paas_e2e |
|
||||
| ERP Service | 7091 | 7191 | coolbuy_paas_e2e |
|
||||
| Web Frontend | 4000 | 4010 | - |
|
||||
|
||||
**E2E DB 初始化**(首次/重置):
|
||||
```bash
|
||||
psql -U coolbuy-dev -d postgres -c "DROP DATABASE IF EXISTS coolbuy_paas_e2e;"
|
||||
psql -U coolbuy-dev -d postgres -c "CREATE DATABASE coolbuy_paas_e2e OWNER \"coolbuy-dev\";"
|
||||
pg_dump -U coolbuy-dev coolbuy_paas_local | psql -U coolbuy-dev coolbuy_paas_e2e
|
||||
```
|
||||
|
||||
### 启动 / 停止 E2E 服务
|
||||
|
||||
```bash
|
||||
make e2e-start # 启动全部 E2E 服务(auth/foundation/erp/web)
|
||||
make e2e-stop # 停止全部 E2E 服务
|
||||
make e2e-reset # 重置 DB 后启动
|
||||
make e2e # 启动服务 + 运行全部测试
|
||||
```
|
||||
|
||||
脚本位置:`scripts/start-e2e-services.sh` / `scripts/stop-e2e-services.sh`
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
cd web
|
||||
|
||||
# 全部测试(无头模式)
|
||||
npx playwright test
|
||||
|
||||
# 带 UI 调试
|
||||
npx playwright test --headed
|
||||
|
||||
# 单个文件
|
||||
npx playwright test tests/product-crud.spec.ts
|
||||
|
||||
# 查看 HTML 报告(注意:会启动 HTTP server,需 Ctrl+C 退出)
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
### Auth 自动登录
|
||||
|
||||
`tests/auth.setup.ts` 优先点击快速登录按钮(`VITE_ENABLE_QUICK_LOGIN=true`),降级为表单登录:
|
||||
|
||||
```typescript
|
||||
// 快速登录(E2E 环境默认开启)
|
||||
const quickLoginBtn = page.locator('button, a').filter({ hasText: /李宁|lining|ID:2/i }).first();
|
||||
if (await quickLoginBtn.isVisible({ timeout: 3000 })) {
|
||||
await quickLoginBtn.click();
|
||||
} else {
|
||||
// 降级:填写 lining_admin / admin123,验证码任意 4 位(SkipVerify=true)
|
||||
}
|
||||
await page.waitForURL(/\/tenant/, { timeout: 15000 });
|
||||
await page.context().storageState({ path: authFile });
|
||||
```
|
||||
|
||||
Session 保存至 `.auth/user.json`,后续测试自动复用,无需重复登录。
|
||||
|
||||
### 配置文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `web/.env.e2e` | E2E 环境变量(端口 / 快速登录开关) |
|
||||
| `web/playwright.config.ts` | baseURL=localhost:4010,reporter=[html, list] |
|
||||
| `auth-service/api/etc/auth-api-e2e.yaml` | E2E auth 配置(SkipVerify=true) |
|
||||
| `foundation-service/api/etc/foundation-api-e2e.yaml` | E2E foundation 配置 |
|
||||
| `erp-service/configs/config.e2e.yaml` | E2E ERP 配置 |
|
||||
|
||||
### 测试结果解读
|
||||
|
||||
当前 **113 tests — 103 ✅ / 10 ❌**,已知失败项:
|
||||
|
||||
| 失败原因 | 涉及测试 |
|
||||
|---------|---------|
|
||||
| `/tenant/order/business` 路由 404(页面未实现) | 业务订单列表 × 3、订单模块导航 |
|
||||
| 预警管理无搜索表单组件 | 预警管理搜索/筛选 |
|
||||
| 库存管理页无 table/empty 状态 | 仓库管理-库存管理 |
|
||||
| 待审批订单 networkidle 超时(>60s) | 待审批订单列表 |
|
||||
| 数据权限/字段权限页渲染异常 | 系统管理 × 2 |
|
||||
| 业务 CRM 页渲染异常 | 业务 CRM |
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 解决 |
|
||||
|------|------|
|
||||
| `Executable doesn't exist` | `npx playwright install chromium` |
|
||||
| 端口 4010 被占用 | `make e2e-stop` 后重试 |
|
||||
| GORM migration 失败 | 检查 DB 是否有旧约束名,手动 DROP CONSTRAINT 后重启服务 |
|
||||
| HTML 报告进程不退出 | Playwright 在 report 模式会启动 HTTP server,用 `Ctrl+C` 停止 |
|
||||
174
skills-dev/dev-test-plugin/skills/dev-test/frontend-testing.md
Normal file
174
skills-dev/dev-test-plugin/skills/dev-test/frontend-testing.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 前端测试 (Vue + React)
|
||||
|
||||
## Vue 前端测试
|
||||
|
||||
### 测试框架
|
||||
|
||||
- **Vitest**: 测试运行器
|
||||
- **Vue Test Utils**: 组件测试
|
||||
- **MSW**: API Mock
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
npm run test:watch
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### 组件测试
|
||||
|
||||
```typescript
|
||||
// UserList.test.ts
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserList from './UserList.vue'
|
||||
|
||||
describe('UserList', () => {
|
||||
it('renders user list', () => {
|
||||
const wrapper = mount(UserList, {
|
||||
props: {
|
||||
users: [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.user-item')).toHaveLength(2)
|
||||
expect(wrapper.text()).toContain('Alice')
|
||||
})
|
||||
|
||||
it('emits select event', async () => {
|
||||
const wrapper = mount(UserList, {
|
||||
props: { users: [{ id: 1, name: 'Alice' }] }
|
||||
})
|
||||
|
||||
await wrapper.find('.user-item').trigger('click')
|
||||
|
||||
expect(wrapper.emitted('select')).toBeTruthy()
|
||||
expect(wrapper.emitted('select')[0]).toEqual([1])
|
||||
})
|
||||
|
||||
it('shows empty state', () => {
|
||||
const wrapper = mount(UserList, {
|
||||
props: { users: [] }
|
||||
})
|
||||
|
||||
expect(wrapper.find('.empty-state').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### API Mock (MSW)
|
||||
|
||||
```typescript
|
||||
// mocks/handlers.ts
|
||||
import { rest } from 'msw'
|
||||
|
||||
export const handlers = [
|
||||
rest.get('/api/v1/users', (req, res, ctx) => {
|
||||
return res(ctx.json({
|
||||
code: 0,
|
||||
data: {
|
||||
total: 2,
|
||||
list: [{ id: 1, name: 'Alice' }]
|
||||
}
|
||||
}))
|
||||
}),
|
||||
|
||||
rest.post('/api/v1/users', async (req, res, ctx) => {
|
||||
const body = await req.json()
|
||||
return res(ctx.json({
|
||||
code: 0,
|
||||
data: { id: 3, ...body }
|
||||
}))
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React 前端测试
|
||||
|
||||
### 测试框架
|
||||
|
||||
- **Jest**: 测试运行器
|
||||
- **React Testing Library**: 组件测试
|
||||
- **Playwright**: E2E 测试
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run test:e2e
|
||||
npm run test:e2e:headed
|
||||
```
|
||||
|
||||
### 组件测试
|
||||
|
||||
```typescript
|
||||
// TaskCard.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import TaskCard from './TaskCard'
|
||||
|
||||
describe('TaskCard', () => {
|
||||
const mockTask = {
|
||||
id: 1,
|
||||
title: 'Test Task',
|
||||
status: 'todo',
|
||||
priority: 'high'
|
||||
}
|
||||
|
||||
it('renders task title', () => {
|
||||
render(<TaskCard task={mockTask} />)
|
||||
expect(screen.getByText('Test Task')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays priority', () => {
|
||||
render(<TaskCard task={mockTask} />)
|
||||
expect(screen.getByText('high')).toHaveClass('priority-high')
|
||||
})
|
||||
|
||||
it('calls onClick', () => {
|
||||
const handleClick = jest.fn()
|
||||
render(<TaskCard task={mockTask} onClick={handleClick} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('article'))
|
||||
|
||||
expect(handleClick).toHaveBeenCalledWith(mockTask)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Hook 测试
|
||||
|
||||
```typescript
|
||||
// useTimer.test.ts
|
||||
import { renderHook, act } from '@testing-library/react-hooks'
|
||||
import { useTimer } from './useTimer'
|
||||
|
||||
describe('useTimer', () => {
|
||||
beforeEach(() => jest.useFakeTimers())
|
||||
afterEach(() => jest.useRealTimers())
|
||||
|
||||
it('starts timer', () => {
|
||||
const { result } = renderHook(() => useTimer())
|
||||
|
||||
act(() => result.current.start())
|
||||
|
||||
expect(result.current.isRunning).toBe(true)
|
||||
})
|
||||
|
||||
it('increments time', () => {
|
||||
const { result } = renderHook(() => useTimer())
|
||||
|
||||
act(() => {
|
||||
result.current.start()
|
||||
jest.advanceTimersByTime(3000)
|
||||
})
|
||||
|
||||
expect(result.current.seconds).toBe(3)
|
||||
})
|
||||
})
|
||||
```
|
||||
208
skills-dev/dev-test-plugin/skills/dev-test/go-testing.md
Normal file
208
skills-dev/dev-test-plugin/skills/dev-test/go-testing.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Go 后端测试
|
||||
|
||||
## 测试框架
|
||||
|
||||
- **testify**: 断言和套件
|
||||
- **httptest**: HTTP 测试
|
||||
- **gomock**: Mock 生成(仅用于 handler 层)
|
||||
|
||||
## ⚠️ Biz 层测试规则:禁止使用 Mock
|
||||
|
||||
**Biz/Service 层测试必须使用真实 PostgreSQL test DB,不允许使用 mock store。**
|
||||
|
||||
Mock store 只是在测试你的 mock 实现,无法验证真实的 SQL 行为、事务、FK 约束等。
|
||||
|
||||
| 层 | 测试方式 | 原因 |
|
||||
|----|---------|------|
|
||||
| model/store | **test DB** (PostgreSQL) | 验证真实 SQL/ORM 行为 |
|
||||
| biz/service | **test DB** (PostgreSQL) + 真实 store | 验证业务逻辑 + 真实数据交互 |
|
||||
| handler | **mock biz + httptest** | 只测 HTTP 路由和参数绑定 |
|
||||
|
||||
```go
|
||||
// ✅ 正确 — biz 层使用真实 test DB + 真实 store
|
||||
func setupBiz(t *testing.T) (*SomeBiz, *gorm.DB) {
|
||||
db := newTestDB(t)
|
||||
s := store.NewSomeStore(db)
|
||||
biz := NewSomeBiz(s)
|
||||
t.Cleanup(func() {
|
||||
db.Exec("DELETE FROM some_table WHERE tenant_id = ?", testTenantID)
|
||||
})
|
||||
return biz, db
|
||||
}
|
||||
|
||||
// ❌ 错误 — biz 层使用 mock store(等于没测)
|
||||
mockStore := store.NewMockIStore(ctrl)
|
||||
mockStore.EXPECT().Get(gomock.Any(), id).Return(fakeData, nil)
|
||||
biz := NewSomeBiz(mockStore)
|
||||
```
|
||||
|
||||
### testdb_test.go 模板
|
||||
|
||||
```go
|
||||
package biz
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const testTenantID int64 = 99
|
||||
|
||||
func newTestDB(t *testing.T, models ...interface{}) *gorm.DB {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_DATABASE_DSN")
|
||||
if dsn == "" {
|
||||
dsn = "host=localhost user=coolbuy-dev dbname=coolbuy_paas_test sslmode=disable"
|
||||
}
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(models...))
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 所有测试
|
||||
go test ./...
|
||||
make test
|
||||
|
||||
# 带覆盖率
|
||||
make cover
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# 特定包
|
||||
go test -v ./internal/twms/biz/...
|
||||
|
||||
# 特定函数
|
||||
go test -v -run TestFunctionName ./...
|
||||
```
|
||||
|
||||
## Biz 层单元测试模板(真实 DB)
|
||||
|
||||
```go
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"project/internal/user/model"
|
||||
"project/internal/user/store"
|
||||
)
|
||||
|
||||
func setupUserBiz(t *testing.T) (*UserBiz, *gorm.DB) {
|
||||
db := newTestDB(t, &model.User{})
|
||||
s := store.NewUserStore(db)
|
||||
biz := NewUserBiz(s)
|
||||
t.Cleanup(func() {
|
||||
db.Exec("DELETE FROM users WHERE tenant_id = ?", testTenantID)
|
||||
})
|
||||
return biz, db
|
||||
}
|
||||
|
||||
func createTestUser(t *testing.T, db *gorm.DB, username string) *model.User {
|
||||
t.Helper()
|
||||
user := &model.User{TenantID: testTenantID, Username: username, Email: username + "@test.com"}
|
||||
require.NoError(t, db.Create(user).Error)
|
||||
return user
|
||||
}
|
||||
|
||||
func TestUserBiz_Get(t *testing.T) {
|
||||
biz, db := setupUserBiz(t)
|
||||
user := createTestUser(t, db, "john")
|
||||
|
||||
result, err := biz.Get(context.Background(), user.ID)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "john", result.Username)
|
||||
}
|
||||
|
||||
func TestUserBiz_Get_NotFound(t *testing.T) {
|
||||
biz, _ := setupUserBiz(t)
|
||||
|
||||
_, err := biz.Get(context.Background(), 99999)
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
## 表驱动测试
|
||||
|
||||
```go
|
||||
func TestValidateUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", "john_doe", false},
|
||||
{"too_short", "ab", true},
|
||||
{"too_long", strings.Repeat("a", 65), true},
|
||||
{"special_chars", "user@name", true},
|
||||
{"empty", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateUsername(tt.input)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP Handler 测试
|
||||
|
||||
```go
|
||||
func TestUserController_List(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockBiz := biz.NewMockIBiz(ctrl)
|
||||
mockUserBiz := biz.NewMockUserBiz(ctrl)
|
||||
|
||||
mockBiz.EXPECT().Users().Return(mockUserBiz).AnyTimes()
|
||||
mockUserBiz.EXPECT().List(gomock.Any(), gomock.Any()).Return(&v1.ListUsersResponse{
|
||||
Total: 1,
|
||||
Users: []*v1.User{{Id: 1, Username: "test"}},
|
||||
}, nil)
|
||||
|
||||
controller := NewUserController(mockBiz)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/v1/users?page=1&limit=10", nil)
|
||||
|
||||
controller.List(c)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
```
|
||||
|
||||
## Mock 生成
|
||||
|
||||
```bash
|
||||
# 生成 Mock
|
||||
mockgen -source=internal/twms/store/store.go \
|
||||
-destination=internal/twms/store/mock_store.go \
|
||||
-package=store
|
||||
|
||||
# go:generate 方式
|
||||
//go:generate mockgen -source=store.go -destination=mock_store.go -package=store
|
||||
```
|
||||
157
skills-dev/dev-test-plugin/skills/dev-test/ios-testing.md
Normal file
157
skills-dev/dev-test-plugin/skills/dev-test/ios-testing.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# iOS 测试 (XCTest + Swift Concurrency)
|
||||
|
||||
## 测试框架
|
||||
|
||||
- **XCTest**: Apple 官方测试框架
|
||||
- **Swift Testing**: Swift 6 新测试框架 (可选)
|
||||
- **ViewInspector**: SwiftUI 视图测试 (第三方)
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 全部测试
|
||||
xcodebuild test \
|
||||
-scheme AI-Proj-iOS \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||
-quiet
|
||||
|
||||
# 特定测试类
|
||||
xcodebuild test \
|
||||
-scheme AI-Proj-iOS \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||
-only-testing:AI-Proj-iOSTests/DashboardViewModelTests
|
||||
|
||||
# 覆盖率
|
||||
xcodebuild test \
|
||||
-scheme AI-Proj-iOS \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||
-enableCodeCoverage YES
|
||||
```
|
||||
|
||||
## 项目测试结构 (AI-Proj-iOS)
|
||||
|
||||
```
|
||||
AI-Proj-iOSTests/
|
||||
├── Mocks/
|
||||
│ ├── MockServices.swift # Mock 服务协议实现
|
||||
│ └── MockNetworkService.swift
|
||||
├── ViewModels/
|
||||
│ ├── DashboardViewModelTests.swift
|
||||
│ ├── TaskViewModelTests.swift
|
||||
│ └── RequirementViewModelTests.swift
|
||||
├── Services/
|
||||
│ ├── TaskServiceTests.swift
|
||||
│ └── DashboardAggregationServiceTests.swift
|
||||
├── Models/
|
||||
│ └── ModelDecodingTests.swift
|
||||
└── Utilities/
|
||||
└── DateFormatterTests.swift
|
||||
```
|
||||
|
||||
## 关键模式
|
||||
|
||||
### 1. Mock 服务 — Result 注入
|
||||
|
||||
```swift
|
||||
class MockTaskService: TaskServiceProtocol {
|
||||
var fetchTasksResult: Result<TaskListResponse, Error> = .success(.mock)
|
||||
|
||||
func fetchTasks(...) async throws -> TaskListResponse {
|
||||
switch fetchTasksResult {
|
||||
case .success(let response): return response
|
||||
case .failure(let error): throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有 Mock 服务统一用 `Result` 属性控制成功/失败返回。
|
||||
|
||||
### 2. ViewModel 测试 — @MainActor + async
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
final class DashboardViewModelTests: XCTestCase {
|
||||
var sut: DashboardViewModel!
|
||||
var mockService: MockDashboardAggregationService!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
mockService = MockDashboardAggregationService()
|
||||
sut = DashboardViewModel(dashboardService: mockService)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
sut = nil; mockService = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testLoadDashboardData_Success() async {
|
||||
mockService.fetchDashboardDataResult = .success(expectedData)
|
||||
await sut.loadDashboardData()
|
||||
XCTAssertFalse(sut.isLoading)
|
||||
XCTAssertEqual(sut.todayStats.completedTasks, 5)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要点:`@MainActor` + `async` 测试方法 + setUp/tearDown 重置。
|
||||
|
||||
### 3. Mock 数据工厂 — 静态 `.mock()` 方法
|
||||
|
||||
```swift
|
||||
extension TaskModel {
|
||||
static func mock(id: Int = 1, status: TaskStatus = .todo) -> TaskModel {
|
||||
TaskModel(id: id, title: "Mock Task", status: status, ...)
|
||||
}
|
||||
}
|
||||
|
||||
extension TaskListResponse {
|
||||
static var mock: TaskListResponse {
|
||||
TaskListResponse(tasks: [.mock(id: 1), .mock(id: 2)], total: 2, page: 1, pageSize: 20)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 模型解码测试 — JSON → Model
|
||||
|
||||
```swift
|
||||
func testTaskModel_DecodesFromJSON() throws {
|
||||
let json = """
|
||||
{ "id": 123, "status": "in_progress", "priority": "high", ... }
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let task = try decoder.decode(TaskModel.self, from: json)
|
||||
XCTAssertEqual(task.status, .inProgress)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. SwiftUI 视图测试 — ViewInspector
|
||||
|
||||
```swift
|
||||
extension EnhancedStatsSection: Inspectable {}
|
||||
|
||||
func testStatsSection_DisplaysCorrectValues() throws {
|
||||
let view = EnhancedStatsSection(stats: .mock)
|
||||
let text = try view.inspect().find(text: "5")
|
||||
XCTAssertNotNil(text)
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **@MainActor** — ViewModel 测试必须在主线程
|
||||
2. **Mock 所有依赖** — 协议抽象 + Result 注入
|
||||
3. **async/await** — 避免 XCTestExpectation 回调
|
||||
4. **数据工厂** — `.mock()` 静态方法,参数带默认值
|
||||
5. **隔离测试** — setUp/tearDown 重置所有状态
|
||||
6. **命名** — `test<Method>_<Scenario>` 格式
|
||||
|
||||
## Xcode 快捷键
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|--------|------|
|
||||
| `Cmd + U` | 运行所有测试 |
|
||||
| `Ctrl + Opt + Cmd + U` | 运行当前测试方法 |
|
||||
| `Ctrl + Opt + Cmd + G` | 重新运行上次测试 |
|
||||
| `Cmd + 6` | Test Navigator |
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "finishing-a-development-branch-plugin",
|
||||
"description": "Plugin for finishing-a-development-branch",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
104
skills-dev/finishing-a-development-branch-plugin/skills/SKILL.md
Normal file
104
skills-dev/finishing-a-development-branch-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: finishing-a-development-branch
|
||||
description: Use when implementation is complete and all tests pass - verifies and creates PR
|
||||
---
|
||||
|
||||
# Finishing a Development Branch
|
||||
|
||||
## Overview
|
||||
|
||||
Verify tests pass, then push and create PR.
|
||||
|
||||
**Core principle:** Verify tests → Create PR → Done.
|
||||
|
||||
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Verify Tests
|
||||
|
||||
**Before creating PR, verify tests pass:**
|
||||
|
||||
```bash
|
||||
# Run project's test suite
|
||||
npm test / cargo test / pytest / go test ./... / mvn test
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
```
|
||||
Tests failing (<N> failures). Must fix before completing:
|
||||
|
||||
[Show failures]
|
||||
|
||||
Cannot proceed with PR until tests pass.
|
||||
```
|
||||
|
||||
Stop. Fix tests first.
|
||||
|
||||
**If tests pass:** Continue to Step 2.
|
||||
|
||||
### Step 2: Push and Create PR
|
||||
|
||||
Use the `/pr create` command which will:
|
||||
1. **Check for existing PR first** - avoids duplicates
|
||||
2. If PR exists: Report existing PR URL and skip
|
||||
3. If no PR: Analyze commits, generate title/description, push, create PR
|
||||
|
||||
```bash
|
||||
/pr create
|
||||
```
|
||||
|
||||
**Duplicate prevention:** The `/pr create` command checks for existing open PRs on the current branch before creating a new one.
|
||||
|
||||
Report the PR URL when complete (whether existing or newly created).
|
||||
|
||||
### Step 3: Cleanup Worktree (if applicable)
|
||||
|
||||
Check if working in a worktree:
|
||||
```bash
|
||||
git worktree list | grep $(git branch --show-current)
|
||||
```
|
||||
|
||||
If yes, ask user:
|
||||
```
|
||||
Worktree at <path>. Remove it now? (y/n)
|
||||
```
|
||||
|
||||
If confirmed:
|
||||
```bash
|
||||
git worktree remove <worktree-path>
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
Tests Pass?
|
||||
↓ yes
|
||||
/pr create
|
||||
↓
|
||||
PR URL returned
|
||||
↓
|
||||
Cleanup worktree (optional)
|
||||
↓
|
||||
Done
|
||||
```
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Create PR with failing tests
|
||||
- Skip test verification
|
||||
- Force-push without explicit request
|
||||
|
||||
**Always:**
|
||||
- Verify tests before creating PR
|
||||
- Use `/pr create` for consistent PR format
|
||||
- Report the PR URL
|
||||
|
||||
## Integration
|
||||
|
||||
**Called by:**
|
||||
- **executing-plans** (Step 6) - After all batches complete
|
||||
|
||||
**Uses:**
|
||||
- **/pr create** - For pushing and PR creation
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "frontend-design-plugin",
|
||||
"description": "Plugin for frontend-design",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
695
skills-dev/frontend-design-plugin/skills/SKILL.md
Normal file
695
skills-dev/frontend-design-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,695 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
arguments: [component|page|storybook] <description>
|
||||
---
|
||||
|
||||
# Frontend Design 前端设计技能
|
||||
|
||||
创建高质量、有设计感的前端界面和组件,支持 Storybook 组件开发。
|
||||
|
||||
---
|
||||
|
||||
## 命令格式
|
||||
|
||||
| 命令 | 功能 | 示例 |
|
||||
|------|------|------|
|
||||
| `/frontend-design component <描述>` | 创建 React/Vue 组件 | `/frontend-design component 产品卡片` |
|
||||
| `/frontend-design page <描述>` | 创建完整页面 | `/frontend-design page 登录页` |
|
||||
| `/frontend-design storybook <描述>` | 创建带 Storybook 的组件 | `/frontend-design storybook 按钮组件` |
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 设计思维先行
|
||||
|
||||
在编码前,明确以下问题:
|
||||
|
||||
- **目的**:这个界面解决什么问题?谁在使用?
|
||||
- **调性**:选择一个明确的美学方向
|
||||
- **差异化**:什么让这个设计令人难忘?
|
||||
|
||||
### 2. 美学方向选择
|
||||
|
||||
| 风格 | 特点 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| 极简主义 | 大量留白、精炼元素 | 工具类、专业平台 |
|
||||
| 现代商务 | 清晰层次、专业配色 | 企业官网、B2B |
|
||||
| 活力年轻 | 鲜艳色彩、动感动画 | 消费品、社交 |
|
||||
| 奢华精致 | 深色调、金属质感 | 高端品牌、金融 |
|
||||
| 自然有机 | 柔和曲线、自然色系 | 健康、环保 |
|
||||
| 复古怀旧 | 经典字体、做旧质感 | 文化、艺术 |
|
||||
| 未来科技 | 渐变、玻璃拟态 | 科技、创新 |
|
||||
|
||||
### 3. 避免的设计陷阱
|
||||
|
||||
**禁止使用**:
|
||||
- 过度使用的字体:Inter、Roboto、Arial
|
||||
- 陈词滥调的配色:紫色渐变白底
|
||||
- 千篇一律的布局
|
||||
- 缺乏个性的通用组件
|
||||
|
||||
**应该追求**:
|
||||
- 独特的字体组合
|
||||
- 有意图的配色方案
|
||||
- 打破常规的布局
|
||||
- 有记忆点的细节
|
||||
|
||||
---
|
||||
|
||||
## Storybook 组件开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Button/
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.stories.tsx
|
||||
│ │ ├── Button.module.css
|
||||
│ │ └── index.ts
|
||||
│ ├── Card/
|
||||
│ │ ├── Card.tsx
|
||||
│ │ ├── Card.stories.tsx
|
||||
│ │ ├── Card.module.css
|
||||
│ │ └── index.ts
|
||||
│ └── index.ts
|
||||
├── styles/
|
||||
│ ├── variables.css
|
||||
│ ├── typography.css
|
||||
│ └── animations.css
|
||||
└── .storybook/
|
||||
├── main.ts
|
||||
└── preview.ts
|
||||
```
|
||||
|
||||
### 组件模板
|
||||
|
||||
#### 1. 组件文件 (Component.tsx)
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import styles from './Component.module.css';
|
||||
|
||||
export interface ComponentProps {
|
||||
/** 组件变体 */
|
||||
variant?: 'primary' | 'secondary' | 'outline';
|
||||
/** 尺寸 */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 子元素 */
|
||||
children: React.ReactNode;
|
||||
/** 点击事件 */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Component: React.FC<ComponentProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.component} ${styles[variant]} ${styles[size]}`}
|
||||
data-disabled={disabled}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 2. Storybook Stories (Component.stories.tsx)
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Component } from './Component';
|
||||
|
||||
const meta: Meta<typeof Component> = {
|
||||
title: 'Components/Component',
|
||||
component: Component,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: '组件描述文档',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'outline'],
|
||||
description: '组件变体样式',
|
||||
},
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['sm', 'md', 'lg'],
|
||||
description: '组件尺寸',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: '是否禁用',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Component>;
|
||||
|
||||
/** 默认状态 */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: '默认组件',
|
||||
},
|
||||
};
|
||||
|
||||
/** 主要变体 */
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: '主要按钮',
|
||||
},
|
||||
};
|
||||
|
||||
/** 次要变体 */
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
children: '次要按钮',
|
||||
},
|
||||
};
|
||||
|
||||
/** 不同尺寸 */
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Component size="sm">小号</Component>
|
||||
<Component size="md">中号</Component>
|
||||
<Component size="lg">大号</Component>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** 禁用状态 */
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: '禁用状态',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. 样式文件 (Component.module.css)
|
||||
|
||||
```css
|
||||
.component {
|
||||
/* 基础样式 */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 变体 */
|
||||
.primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--color-primary-shadow);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 尺寸 */
|
||||
.sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.md {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* 状态 */
|
||||
[data-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计系统变量
|
||||
|
||||
### CSS 变量模板
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 颜色 */
|
||||
--color-primary: #0066ff;
|
||||
--color-primary-dark: #0052cc;
|
||||
--color-primary-light: #4d94ff;
|
||||
--color-primary-shadow: rgba(0, 102, 255, 0.25);
|
||||
|
||||
--color-secondary: #f0f4f8;
|
||||
--color-accent: #ff6b35;
|
||||
|
||||
--color-text: #1a1a2e;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-inverse: #ffffff;
|
||||
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f8fafc;
|
||||
--color-border: #e2e8f0;
|
||||
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
|
||||
/* 字体 */
|
||||
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-display: 'Clash Display', var(--font-sans);
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
/* 字号 */
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 2rem;
|
||||
--text-4xl: 2.5rem;
|
||||
|
||||
/* 间距 */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-12: 3rem;
|
||||
--space-16: 4rem;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* 动画 */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 300ms;
|
||||
--duration-slow: 500ms;
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* 暗色主题 */
|
||||
[data-theme="dark"] {
|
||||
--color-text: #f1f5f9;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-background: #0f172a;
|
||||
--color-surface: #1e293b;
|
||||
--color-border: #334155;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用组件示例
|
||||
|
||||
### 1. 产品卡片 (ProductCard)
|
||||
|
||||
```tsx
|
||||
// ProductCard.tsx
|
||||
import React from 'react';
|
||||
import styles from './ProductCard.module.css';
|
||||
|
||||
export interface ProductCardProps {
|
||||
image: string;
|
||||
title: string;
|
||||
location: string;
|
||||
rating: number;
|
||||
reviewCount: number;
|
||||
price: number;
|
||||
originalPrice?: number;
|
||||
tags?: string[];
|
||||
onAddToCart?: () => void;
|
||||
}
|
||||
|
||||
export const ProductCard: React.FC<ProductCardProps> = ({
|
||||
image,
|
||||
title,
|
||||
location,
|
||||
rating,
|
||||
reviewCount,
|
||||
price,
|
||||
originalPrice,
|
||||
tags = [],
|
||||
onAddToCart,
|
||||
}) => {
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<img src={image} alt={title} className={styles.image} />
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className={styles.tag} data-tag={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<p className={styles.location}>📍 {location}</p>
|
||||
|
||||
<div className={styles.rating}>
|
||||
<span className={styles.stars}>⭐ {rating.toFixed(1)}</span>
|
||||
<span className={styles.reviewCount}>({reviewCount}条评价)</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.priceRow}>
|
||||
<div className={styles.price}>
|
||||
<span className={styles.currency}>¥</span>
|
||||
<span className={styles.amount}>{price}</span>
|
||||
<span className={styles.suffix}>起</span>
|
||||
</div>
|
||||
{originalPrice && (
|
||||
<span className={styles.originalPrice}>¥{originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className={styles.addButton} onClick={onAddToCart}>
|
||||
加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ProductCard.stories.tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProductCard } from './ProductCard';
|
||||
|
||||
const meta: Meta<typeof ProductCard> = {
|
||||
title: 'Components/ProductCard',
|
||||
component: ProductCard,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ProductCard>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
image: 'https://images.unsplash.com/photo-1494947665470-20322015e3a8',
|
||||
title: '袋鼠岛一日游',
|
||||
location: '阿德莱德出发',
|
||||
rating: 4.8,
|
||||
reviewCount: 126,
|
||||
price: 389,
|
||||
tags: ['热卖', '含午餐'],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDiscount: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
originalPrice: 499,
|
||||
tags: ['特惠', '限时'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Grid: Story = {
|
||||
render: () => (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 300px)',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
<ProductCard
|
||||
image="https://images.unsplash.com/photo-1494947665470-20322015e3a8"
|
||||
title="袋鼠岛一日游"
|
||||
location="阿德莱德出发"
|
||||
rating={4.8}
|
||||
reviewCount={126}
|
||||
price={389}
|
||||
tags={['热卖']}
|
||||
/>
|
||||
<ProductCard
|
||||
image="https://images.unsplash.com/photo-1506905925346-21bda4d32df4"
|
||||
title="巴罗莎谷酒庄之旅"
|
||||
location="阿德莱德出发"
|
||||
rating={4.9}
|
||||
reviewCount={89}
|
||||
price={299}
|
||||
originalPrice={399}
|
||||
tags={['特惠', '含品酒']}
|
||||
/>
|
||||
<ProductCard
|
||||
image="https://images.unsplash.com/photo-1540202403-b7abd6747a18"
|
||||
title="海豚巡航体验"
|
||||
location="格雷尔海滩"
|
||||
rating={4.7}
|
||||
reviewCount={234}
|
||||
price={159}
|
||||
tags={['亲子']}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 按钮组件 (Button)
|
||||
|
||||
```tsx
|
||||
// Button.tsx
|
||||
import React from 'react';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
${styles.button}
|
||||
${styles[variant]}
|
||||
${styles[size]}
|
||||
${fullWidth ? styles.fullWidth : ''}
|
||||
`}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
>
|
||||
{loading ? (
|
||||
<span className={styles.spinner} />
|
||||
) : (
|
||||
<>
|
||||
{leftIcon && <span className={styles.icon}>{leftIcon}</span>}
|
||||
<span>{children}</span>
|
||||
{rightIcon && <span className={styles.icon}>{rightIcon}</span>}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Storybook 配置
|
||||
|
||||
### .storybook/main.ts
|
||||
|
||||
```ts
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-a11y',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### .storybook/preview.ts
|
||||
|
||||
```ts
|
||||
import type { Preview } from '@storybook/react';
|
||||
import '../src/styles/variables.css';
|
||||
import '../src/styles/typography.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{ name: 'light', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#f8fafc' },
|
||||
{ name: 'dark', value: '#0f172a' },
|
||||
],
|
||||
},
|
||||
},
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'light',
|
||||
toolbar: {
|
||||
title: 'Theme',
|
||||
icon: 'circlehollow',
|
||||
items: ['light', 'dark'],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速启动命令
|
||||
|
||||
### 创建新组件
|
||||
|
||||
```bash
|
||||
# 创建组件目录
|
||||
mkdir -p src/components/ComponentName
|
||||
|
||||
# 创建文件
|
||||
touch src/components/ComponentName/{ComponentName.tsx,ComponentName.stories.tsx,ComponentName.module.css,index.ts}
|
||||
```
|
||||
|
||||
### 安装 Storybook
|
||||
|
||||
```bash
|
||||
# 初始化 Storybook
|
||||
npx storybook@latest init
|
||||
|
||||
# 安装额外插件
|
||||
npm install -D @storybook/addon-a11y @storybook/addon-interactions
|
||||
|
||||
# 启动 Storybook
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计检查清单
|
||||
|
||||
### 组件质量检查
|
||||
|
||||
- [ ] Props 接口定义完整,带 JSDoc 注释
|
||||
- [ ] 支持必要的变体(variant)和尺寸(size)
|
||||
- [ ] 处理禁用和加载状态
|
||||
- [ ] 支持自定义 className
|
||||
- [ ] 键盘可访问性
|
||||
- [ ] 屏幕阅读器友好
|
||||
|
||||
### Storybook 质量检查
|
||||
|
||||
- [ ] 所有变体都有对应 Story
|
||||
- [ ] argTypes 配置完整
|
||||
- [ ] 包含组件文档描述
|
||||
- [ ] 交互状态可测试
|
||||
- [ ] 响应式展示
|
||||
|
||||
### 视觉质量检查
|
||||
|
||||
- [ ] 字体选择有特色
|
||||
- [ ] 配色方案协调
|
||||
- [ ] 动画流畅自然
|
||||
- [ ] 间距一致
|
||||
- [ ] 暗色主题支持
|
||||
8
skills-dev/gitea-plugin/.claude-plugin/plugin.json
Normal file
8
skills-dev/gitea-plugin/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "gitea-plugin",
|
||||
"description": "Gitea 代码托管与 CI/CD 管理。用于 Gitea Actions workflow 管理、Runner 管理、PR 操作、仓库配置。",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
211
skills-dev/gitea-plugin/scripts/gitea-runs
Executable file
211
skills-dev/gitea-plugin/scripts/gitea-runs
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/bin/bash
|
||||
# gitea-runs — Gitea Actions CLI helper
|
||||
# Usage:
|
||||
# gitea-runs List recent runs
|
||||
# gitea-runs list [limit] List recent runs (default 10)
|
||||
# gitea-runs view <run_number> View run details & jobs
|
||||
# gitea-runs open [run_number] Open run in browser
|
||||
# gitea-runs workflows List workflows
|
||||
# gitea-runs dispatch <wf> [ref] Trigger a workflow dispatch
|
||||
# gitea-runs help Show this help
|
||||
|
||||
set -e
|
||||
|
||||
# Config from tea CLI
|
||||
TEA_CONFIG="${XDG_CONFIG_HOME:-$HOME/Library/Application Support}/tea/config.yml"
|
||||
if [ ! -f "$TEA_CONFIG" ]; then
|
||||
TEA_CONFIG="$HOME/.config/tea/config.yml"
|
||||
fi
|
||||
|
||||
# Parse tea config (nested under logins)
|
||||
GITEA_URL=$(grep 'url:' "$TEA_CONFIG" | head -1 | awk '{print $NF}')
|
||||
GITEA_TOKEN=$(grep 'token:' "$TEA_CONFIG" | head -1 | awk '{print $NF}')
|
||||
|
||||
# Detect repo from git remote
|
||||
REPO=$(git remote get-url origin 2>/dev/null | sed 's|.*gitea.pipexerp.com[:/]*||;s|\.git$||;s|^10022/||')
|
||||
if [ -z "$REPO" ]; then
|
||||
echo "Error: not in a git repo or remote not configured"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API="$GITEA_URL/api/v1"
|
||||
AUTH="Authorization: token $GITEA_TOKEN"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
cmd_dispatch() {
|
||||
local workflow="${1:-}"
|
||||
local ref="${2:-main}"
|
||||
if [ -z "$workflow" ]; then
|
||||
echo "Usage: gitea-runs dispatch <workflow> [ref]"
|
||||
echo ""
|
||||
echo "Available workflows:"
|
||||
curl -s -H "$AUTH" "$API/repos/$REPO/actions/workflows" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for w in data.get('workflows', []):
|
||||
print(f\" {w['id']:30s} {w['name']}\")
|
||||
" 2>/dev/null
|
||||
return
|
||||
fi
|
||||
|
||||
local http_code
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST "$API/repos/$REPO/actions/workflows/$workflow/dispatches" \
|
||||
-d "{\"ref\":\"$ref\"}" 2>/dev/null)
|
||||
|
||||
if [ "$http_code" = "204" ]; then
|
||||
echo -e "${GREEN}✓${NC} Dispatched workflow: $workflow (ref: $ref)"
|
||||
echo " View: $GITEA_URL/$REPO/actions"
|
||||
else
|
||||
echo -e "${RED}✗${NC} Failed to dispatch (HTTP $http_code)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_workflows() {
|
||||
echo -e "${CYAN}Workflows for $REPO${NC}"
|
||||
echo ""
|
||||
curl -s -H "$AUTH" "$API/repos/$REPO/actions/workflows" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for w in data.get('workflows', []):
|
||||
state = '✓' if w['state'] == 'active' else '✗'
|
||||
print(f\" {state} {w['id']:30s} {w['name']}\")
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local limit="${1:-10}"
|
||||
echo -e "${CYAN}Recent runs for $REPO${NC}"
|
||||
echo ""
|
||||
curl -s -H "$AUTH" "$API/repos/$REPO/actions/runs?limit=$limit" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
limit = $limit
|
||||
data = json.load(sys.stdin)
|
||||
for r in data.get('workflow_runs', [])[:limit]:
|
||||
status = r.get('status', '?')
|
||||
num = r.get('run_number', 0)
|
||||
title = r.get('display_title', '')[:60]
|
||||
wf = r.get('path', '')
|
||||
wf = wf.split('@')[0] if '@' in wf else wf
|
||||
icon = {'success':'\u2713','completed':'\u2713','failure':'\u2717','cancelled':'\u2717','in_progress':'\u27f3','running':'\u27f3','queued':'\u25cc','waiting':'\u25cc'}.get(status, '?')
|
||||
color = {'success':'\033[0;32m','completed':'\033[0;32m','failure':'\033[0;31m','cancelled':'\033[0;31m','in_progress':'\033[0;33m','running':'\033[0;33m'}.get(status, '\033[0;37m')
|
||||
print(f\"{color}{icon}\033[0m #{num:<4} {status:<12} {wf:<20} {title}\")
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
cmd_view() {
|
||||
local run_number="${1:-}"
|
||||
if [ -z "$run_number" ]; then
|
||||
echo "Usage: gitea-runs view <run_number>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find run by run_number (API uses internal id, html uses run_number)
|
||||
local run_data
|
||||
run_data=$(curl -s -H "$AUTH" "$API/repos/$REPO/actions/runs?limit=50" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for r in data.get('workflow_runs', []):
|
||||
if r['run_number'] == $run_number:
|
||||
print(json.dumps(r))
|
||||
break
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -z "$run_data" ]; then
|
||||
echo -e "${RED}✗${NC} Run #$run_number not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local run_id
|
||||
run_id=$(echo "$run_data" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
||||
|
||||
# Print run info
|
||||
echo "$run_data" | python3 -c "
|
||||
import json, sys
|
||||
r = json.load(sys.stdin)
|
||||
status = r.get('status', '?')
|
||||
icon = {'success':'\u2713','failure':'\u2717','in_progress':'\u27f3','queued':'\u25cc'}.get(status, '?')
|
||||
color = {'success':'\033[0;32m','failure':'\033[0;31m','in_progress':'\033[0;33m'}.get(status, '\033[0;37m')
|
||||
print(f\"{color}{icon} Run #{r.get('run_number',0)} \u2014 {status}\033[0m\")
|
||||
print(f\" Title: {r.get('display_title','')}\")
|
||||
print(f\" Event: {r.get('event','')}\")
|
||||
print(f\" Branch: {r.get('head_branch','')}\")
|
||||
print(f\" Commit: {r.get('head_sha','')[:8]}\")
|
||||
print(f\" Actor: {r.get('actor',{}).get('login','')}\")
|
||||
wf = r.get('path', '')
|
||||
wf = wf.split('@')[0] if '@' in wf else wf
|
||||
print(f\" Workflow: {wf}\")
|
||||
" 2>/dev/null
|
||||
|
||||
# Print jobs
|
||||
echo ""
|
||||
echo -e "${CYAN}Jobs:${NC}"
|
||||
curl -s -H "$AUTH" "$API/repos/$REPO/actions/runs/$run_id/jobs" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
from datetime import datetime
|
||||
data = json.load(sys.stdin)
|
||||
for j in data.get('jobs', []):
|
||||
status = j.get('status', '?')
|
||||
icon = {'success':'\u2713','failure':'\u2717','in_progress':'\u27f3','queued':'\u25cc','waiting':'\u25cc'}.get(status, '?')
|
||||
color = {'success':'\033[0;32m','failure':'\033[0;31m','in_progress':'\033[0;33m'}.get(status, '\033[0;37m')
|
||||
runner = j.get('runner_name', '-')
|
||||
started = j.get('started_at', '')[:19].replace('T', ' ')
|
||||
completed = j.get('completed_at', '')[:19].replace('T', ' ')
|
||||
duration = ''
|
||||
if completed and not completed.startswith('1970'):
|
||||
try:
|
||||
d = datetime.fromisoformat(completed) - datetime.fromisoformat(started)
|
||||
duration = f' ({int(d.total_seconds())}s)'
|
||||
except: pass
|
||||
print(f\" {color}{icon}\033[0m {j.get('name',''):<30} {status:<12} runner: {runner}{duration}\")
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
cmd_open() {
|
||||
local run_id="${1:-}"
|
||||
local url="$GITEA_URL/$REPO/actions"
|
||||
if [ -n "$run_id" ]; then
|
||||
url="$url/runs/$run_id"
|
||||
fi
|
||||
echo "Opening: $url"
|
||||
open "$url" 2>/dev/null || xdg-open "$url" 2>/dev/null || echo "$url"
|
||||
}
|
||||
|
||||
cmd_help() {
|
||||
echo "gitea-runs — Gitea Actions CLI helper"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " gitea-runs List recent runs"
|
||||
echo " gitea-runs list [limit] List recent runs (default 10)"
|
||||
echo " gitea-runs view <run_number> View run details & jobs"
|
||||
echo " gitea-runs open [run_number] Open run in browser"
|
||||
echo " gitea-runs workflows List workflows"
|
||||
echo " gitea-runs dispatch <wf> [ref] Trigger a workflow dispatch"
|
||||
echo " gitea-runs help Show this help"
|
||||
echo ""
|
||||
echo "Repo: $REPO"
|
||||
echo "Gitea: $GITEA_URL"
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-}" in
|
||||
list|ls) shift; cmd_list "$@" ;;
|
||||
view|v) shift; cmd_view "$@" ;;
|
||||
dispatch) shift; cmd_dispatch "$@" ;;
|
||||
workflows|wf) cmd_workflows ;;
|
||||
open|o) shift; cmd_open "$@" ;;
|
||||
help|--help|-h) cmd_help ;;
|
||||
"") cmd_list ;;
|
||||
*) cmd_view "$1" ;;
|
||||
esac
|
||||
281
skills-dev/gitea-plugin/skills/SKILL.md
Normal file
281
skills-dev/gitea-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,281 @@
|
||||
---
|
||||
name: gitea
|
||||
description: Gitea 代码托管与 CI/CD 管理。用于 Gitea Actions workflow 管理、Runner 管理、PR 操作、仓库配置。当用户提到 Gitea、Actions、Runner、CI/CD workflow、PR 检查相关任务时自动激活。
|
||||
---
|
||||
|
||||
# Gitea Skill
|
||||
|
||||
Gitea 代码托管平台管理,覆盖 Actions CI/CD、Runner、PR、仓库配置。
|
||||
|
||||
## 服务器信息
|
||||
|
||||
| 服务 | 地址 | SSH |
|
||||
|------|------|-----|
|
||||
| Gitea Web | https://gitea.pipexerp.com | — |
|
||||
| Gitea SSH | gitea.pipexerp.com:10022 | `ssh -i ~/.ssh/id_ed25519 git@gitea.pipexerp.com -p 10022` |
|
||||
| Gitea 服务器 | 123.56.89.187 | `ssh -i ~/.ssh/tools.pem root@123.56.89.187` |
|
||||
| Runner 服务器 | 101.200.136.200 (Jenkins 服务器) | `ssh -i ~/.ssh/tools.pem root@101.200.136.200` |
|
||||
|
||||
## API 访问
|
||||
|
||||
```bash
|
||||
# Gitea API Token (仓库级)
|
||||
GITEA_TOKEN="483a2b65219625ee382eb6d023cda39238c32e24"
|
||||
|
||||
# 通用请求格式
|
||||
curl -s "https://gitea.pipexerp.com/api/v1/repos/pipexerp/<repo>/..." \
|
||||
-H "Authorization: token $GITEA_TOKEN"
|
||||
```
|
||||
|
||||
### 常用 API
|
||||
|
||||
| 操作 | 方法 | 端点 |
|
||||
|------|------|------|
|
||||
| 创建 PR | POST | `/repos/{owner}/{repo}/pulls` |
|
||||
| 更新 PR | PATCH | `/repos/{owner}/{repo}/pulls/{id}` |
|
||||
| 列出 Runs | GET | `/repos/{owner}/{repo}/actions/runs` |
|
||||
| Run 详情 | GET | `/repos/{owner}/{repo}/actions/runs/{id}` |
|
||||
| Job 详情 | GET | `/repos/{owner}/{repo}/actions/runs/{id}/jobs` |
|
||||
| 手动触发 Workflow | POST | `/repos/{owner}/{repo}/actions/workflows/{file}/dispatches` body: `{"ref":"main"}` |
|
||||
| 获取 Runner Token | POST | `/repos/{owner}/{repo}/actions/runners/registration-token` |
|
||||
| 添加 Secret | PUT | `/repos/{owner}/{repo}/actions/secrets/{name}` body: `{"data":"value"}` |
|
||||
| 删除 Run(仅已完成)| DELETE | `/repos/{owner}/{repo}/actions/runs/{id}` |
|
||||
|
||||
**注意**: Gitea 1.25 **不支持**通过 API cancel 正在排队/运行的 run。
|
||||
|
||||
## 仓库
|
||||
|
||||
| 仓库 | 地址 | 主分支 |
|
||||
|------|------|--------|
|
||||
| coolbuy-paas | pipexerp/coolbuy-paas | main |
|
||||
| dotfiles | huangjun/dotfiles | main |
|
||||
| claude-marketplace | huangjun/claude-marketplace | main |
|
||||
|
||||
## Actions Runners
|
||||
|
||||
### 主 Runner (lint/test/e2e)
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 名称 | jenkins-runner |
|
||||
| 配置 | `/opt/act_runner/config.yaml` |
|
||||
| Capacity | 3 |
|
||||
| Labels | `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-20.04` |
|
||||
| 进程 | `/usr/local/bin/act_runner daemon --config /opt/act_runner/config.yaml` |
|
||||
|
||||
### Deploy Runner (staging 部署专用)
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 名称 | deploy-runner |
|
||||
| 配置 | `/opt/act_runner_deploy/config.yaml` |
|
||||
| Capacity | 1 |
|
||||
| Labels | `deploy:host` |
|
||||
| PID | `/opt/act_runner_deploy/runner.pid` |
|
||||
| 日志 | `/opt/act_runner_deploy/runner.log` |
|
||||
| 启动 | `cd /opt/act_runner_deploy && nohup act_runner daemon --config config.yaml > runner.log 2>&1 &` |
|
||||
|
||||
### 注册新 Runner
|
||||
|
||||
```bash
|
||||
# 1. 获取 registration token
|
||||
curl -s -X POST "https://gitea.pipexerp.com/api/v1/repos/pipexerp/coolbuy-paas/actions/runners/registration-token" \
|
||||
-H "Authorization: token $GITEA_TOKEN"
|
||||
|
||||
# 2. SSH 到 runner 服务器
|
||||
ssh -i ~/.ssh/tools.pem root@101.200.136.200
|
||||
|
||||
# 3. 创建目录和配置
|
||||
mkdir -p /opt/act_runner_<name>
|
||||
cat > /opt/act_runner_<name>/config.yaml << 'EOF'
|
||||
log:
|
||||
level: info
|
||||
runner:
|
||||
file: .runner
|
||||
capacity: 1
|
||||
timeout: 30m
|
||||
labels:
|
||||
- "<label>:host" # host 模式用系统 shell
|
||||
# 或 "<label>:docker://image" # docker 模式
|
||||
cache:
|
||||
enabled: false
|
||||
EOF
|
||||
|
||||
# 4. 注册
|
||||
cd /opt/act_runner_<name>
|
||||
act_runner register --instance https://gitea.pipexerp.com \
|
||||
--token <TOKEN> --name <NAME> --labels '<LABEL>:host' \
|
||||
--config config.yaml --no-interactive
|
||||
|
||||
# 5. 启动
|
||||
nohup act_runner daemon --config config.yaml > runner.log 2>&1 &
|
||||
echo $! > runner.pid
|
||||
```
|
||||
|
||||
### Runner 运维
|
||||
|
||||
```bash
|
||||
# 检查 runner 状态
|
||||
ssh -i ~/.ssh/tools.pem root@101.200.136.200 "ps aux | grep act_runner | grep -v grep"
|
||||
|
||||
# 查看 deploy runner 日志
|
||||
ssh -i ~/.ssh/tools.pem root@101.200.136.200 "tail -20 /opt/act_runner_deploy/runner.log"
|
||||
|
||||
# 重启 deploy runner
|
||||
ssh -i ~/.ssh/tools.pem root@101.200.136.200 "
|
||||
kill \$(cat /opt/act_runner_deploy/runner.pid) 2>/dev/null
|
||||
cd /opt/act_runner_deploy
|
||||
nohup act_runner daemon --config config.yaml > runner.log 2>&1 &
|
||||
echo \$! > runner.pid
|
||||
"
|
||||
```
|
||||
|
||||
## Workflows (coolbuy-paas)
|
||||
|
||||
| Workflow | 触发 | Runner | paths-ignore | 用途 |
|
||||
|----------|------|--------|-------------|------|
|
||||
| 🚀 deploy-staging.yml | push → main | `deploy` | md, docs, .gitea, scripts, *_test.go | 触发 Jenkins 部署到 staging |
|
||||
| 🔍 lint.yml | PR → main | `ubuntu-latest` | md, docs, .gitea, scripts | Go lint + ESLint auto-fix |
|
||||
| 🧪 unit-test.yml | PR → main | `ubuntu-latest` | md, docs, .gitea, scripts | 4 个 Go 服务单元测试 |
|
||||
| 🎭 e2e-tests.yml | schedule 12h | `ubuntu-latest` | — | Playwright E2E(仅定时) |
|
||||
| 📋 notify-aiproj.yml | PR merged | `ubuntu-latest` | — | 同步需求状态到 ai-proj |
|
||||
| 📦 build.yaml | 手动 | `ubuntu-latest` | — | Docker 构建推 Hub |
|
||||
|
||||
### Workflow 编写规范
|
||||
|
||||
```yaml
|
||||
# 1. 名称加 emoji 前缀
|
||||
name: "🚀 Deploy Staging"
|
||||
|
||||
# 2. 非代码变更加 paths-ignore
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- 'docs/**'
|
||||
- '.gitea/**'
|
||||
- 'scripts/**'
|
||||
|
||||
# 3. 加 concurrency 防重复
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: true
|
||||
|
||||
# 4. 仅定时的 workflow 加 event 守卫
|
||||
jobs:
|
||||
e2e:
|
||||
if: github.event_name == 'schedule'
|
||||
|
||||
# 5. auto-fix 提交加 [skip ci]
|
||||
git commit -m "style: auto-fix [skip ci]"
|
||||
```
|
||||
|
||||
### Checkout 模式(容器内)
|
||||
|
||||
Gitea Actions 不支持 `actions/checkout`,用原生 git:
|
||||
|
||||
```yaml
|
||||
- name: Checkout
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
git init
|
||||
git remote add origin "https://oauth2:${TOKEN}@gitea.pipexerp.com/${{ github.repository }}.git"
|
||||
git fetch origin "${{ github.event.pull_request.head.ref }}"
|
||||
git checkout -b pr-branch "origin/${{ github.event.pull_request.head.ref }}"
|
||||
git config user.name "CI Bot"
|
||||
git config user.email "ci@pipexerp.com"
|
||||
```
|
||||
|
||||
## Secrets 管理
|
||||
|
||||
### 当前 Secrets (coolbuy-paas 仓库级)
|
||||
|
||||
| Secret | 用途 |
|
||||
|--------|------|
|
||||
| `JENKINS_USER` | Jenkins API 用户名 |
|
||||
| `JENKINS_TOKEN` | Jenkins API Token |
|
||||
| `DOCKER_HUB_TOKEN` | Docker Hub 推送 |
|
||||
| `AI_PROJ_TOKEN` | ai-proj API 认证 |
|
||||
|
||||
### 添加/更新 Secret
|
||||
|
||||
```bash
|
||||
curl -s -X PUT \
|
||||
"https://gitea.pipexerp.com/api/v1/repos/pipexerp/coolbuy-paas/actions/secrets/<NAME>" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"data": "<VALUE>"}'
|
||||
```
|
||||
|
||||
## CI/CD 完整流程
|
||||
|
||||
```
|
||||
PR → main
|
||||
├── 🧪 unit-test.yml (Go 服务测试)
|
||||
├── 🔍 lint.yml (auto-fix 格式)
|
||||
└── merge 后:
|
||||
├── 📋 notify-aiproj.yml (需求状态 → testing)
|
||||
└── 🚀 deploy-staging.yml (Jenkins → staging)
|
||||
├── ≥5 commits/2h → 立即部署
|
||||
└── <5 commits → 等3分钟 debounce
|
||||
|
||||
定时:
|
||||
└── 🎭 e2e-tests.yml (每12h Playwright)
|
||||
|
||||
手动:
|
||||
└── 📦 build.yaml (Docker 构建推 Hub)
|
||||
|
||||
生产部署:
|
||||
└── ./scripts/build-and-push.sh prod --deploy (触发 Jenkins)
|
||||
```
|
||||
|
||||
## 本地 CLI 工具
|
||||
|
||||
### tea CLI (Gitea 官方命令行)
|
||||
|
||||
tea 是 Gitea 官方 CLI 客户端,已配置好认证信息。
|
||||
|
||||
```bash
|
||||
# 配置文件位置
|
||||
~/Library/Application Support/tea/config.yml
|
||||
# 或 ~/.config/tea/config.yml
|
||||
|
||||
# gitea-runs 脚本从 tea config 读取 url 和 token
|
||||
```
|
||||
|
||||
### gitea-runs (Actions 快捷命令)
|
||||
|
||||
位置: `~/.local/bin/gitea-runs`
|
||||
|
||||
自动从 git remote 检测仓库,从 tea CLI 配置读取认证信息。
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `gitea-runs` | 列出最近 10 条 run |
|
||||
| `gitea-runs list [N]` | 列出最近 N 条 run |
|
||||
| `gitea-runs view <run_number>` | 查看 run 详情和 jobs |
|
||||
| `gitea-runs open [run_number]` | 在浏览器打开 run 页面 |
|
||||
| `gitea-runs workflows` | 列出所有 workflow |
|
||||
| `gitea-runs dispatch <wf> [ref]` | 手动触发 workflow |
|
||||
|
||||
```bash
|
||||
# 示例
|
||||
gitea-runs # 查看最近 runs
|
||||
gitea-runs view 303 # 查看 run #303 详情
|
||||
gitea-runs dispatch deploy-staging.yml main # 手动触发部署
|
||||
gitea-runs open # 打开 Actions 页面
|
||||
```
|
||||
|
||||
**优先使用 `gitea-runs` 而非 curl API**,更简洁且自动处理认证。
|
||||
|
||||
## 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| Run 一直 queued | Runner 被占满 | 等其他 job 完成,或加 runner |
|
||||
| deploy 被 test 阻塞 | 共用 runner | 用 `runs-on: deploy` 专属 runner |
|
||||
| Workflow 被误触发 | push 新 workflow 文件到 main | 加 `if: github.event_name == 'schedule'` 守卫 |
|
||||
| auto-fix 无限循环 | 提交触发新 run | 提交信息加 `[skip ci]` |
|
||||
| API 无法 cancel run | Gitea 1.25 限制 | 网页手动取消,或等完成后 DELETE |
|
||||
| `date -d` 报错 | 容器 date 不兼容 | 用 host 模式 runner,或兼容写法 |
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "pr-plugin",
|
||||
"description": "Plugin for pr",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
201
skills-dev/pull-request-plugin/skills/SKILL.md
Normal file
201
skills-dev/pull-request-plugin/skills/SKILL.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
name: pull-request
|
||||
description: Use when starting new development tasks, creating pull requests, reviewing code, or managing PR feedback cycles on Gitea. Triggers on /pr commands or when user mentions PR, pull request, code review, or branch creation.
|
||||
---
|
||||
|
||||
# PR Workflow
|
||||
|
||||
Standardized PR lifecycle for Gitea: start task → create PR → review → update → merge.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# One-time setup
|
||||
brew install tea
|
||||
tea login --url https://gitea.pipexerp.com
|
||||
```
|
||||
|
||||
## Push 前必须检查 PR 状态 ⭐
|
||||
|
||||
**每次 `git push` 之前,必须检查当前分支关联的 PR 状态:**
|
||||
|
||||
```bash
|
||||
BRANCH=$(git branch --show-current)
|
||||
# 用 Gitea API 检查该分支的 PR(包括 open 和 closed)
|
||||
# - PR 还 open → 正常 push
|
||||
# - PR 已 merge → 禁止 push,切回 main 拉最新,新建分支重新提交
|
||||
# - 无 PR → 正常 push(后续 /pr create 会创建)
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- PR 已 merge 后**绝对不能**再往该分支 push
|
||||
- 发现 PR 已 merge → 自动:`git checkout main && git pull` → 新建分支 → cherry-pick 或重新提交变更
|
||||
- 向用户报告情况,不要静默处理
|
||||
|
||||
## Commands
|
||||
|
||||
### /pr start `<type>` `<REQ-id>` `<name>`
|
||||
|
||||
Start fresh branch from origin/main.
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout -b <type>/REQ-<id>-<name> origin/main
|
||||
```
|
||||
|
||||
**Types:** `feature`, `fix`, `refactor`
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
/pr start feature REQ-123 user-login
|
||||
# Creates: feature/REQ-123-user-login from origin/main
|
||||
```
|
||||
|
||||
### /pr create
|
||||
|
||||
Create PR on Gitea from current branch.
|
||||
|
||||
**Steps:**
|
||||
1. **Check for existing PR first:**
|
||||
```bash
|
||||
tea pr list --state open --head $(git branch --show-current)
|
||||
```
|
||||
- If PR exists: Report existing PR URL and skip creation
|
||||
- If no PR: Continue to step 2
|
||||
|
||||
2. Get task ID from branch name or ai-proj session, ask if missing
|
||||
3. Analyze commits with `git log origin/main..HEAD`
|
||||
4. Generate title: `[REQ-xxx] Brief description`
|
||||
5. Generate description (What + Why)
|
||||
6. Push branch and create PR
|
||||
|
||||
```bash
|
||||
# Check existing PR
|
||||
BRANCH=$(git branch --show-current)
|
||||
EXISTING_PR=$(tea pr list --state open --head "$BRANCH" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR already exists: $EXISTING_PR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create new PR
|
||||
git push -u origin HEAD
|
||||
tea pr create --title "[REQ-123] Add user login" --body "$(cat <<'EOF'
|
||||
## What
|
||||
Added user authentication with session management.
|
||||
|
||||
## Why
|
||||
Users need to log in to access protected features.
|
||||
EOF
|
||||
)" --head feature/REQ-123-user-login --base main
|
||||
```
|
||||
|
||||
### /pr review `[url|number]`
|
||||
|
||||
Review a PR for code quality and tests.
|
||||
|
||||
**Checklist:**
|
||||
|
||||
| Check | Verify |
|
||||
|-------|--------|
|
||||
| Logic | Code does what it claims, edge cases handled |
|
||||
| Readability | Clear naming, reasonable complexity |
|
||||
| Patterns | Consistent with codebase conventions |
|
||||
| Tests exist | New/changed code has test coverage |
|
||||
| Tests pass | All tests green |
|
||||
|
||||
**Steps:**
|
||||
1. Fetch PR details: `tea pr view <number>`
|
||||
2. Get diff: `tea pr diff <number>`
|
||||
3. Review against checklist
|
||||
4. Summarize findings with file:line references
|
||||
5. Recommend: approve or request changes
|
||||
|
||||
### /pr update
|
||||
|
||||
Address review feedback.
|
||||
|
||||
**Steps:**
|
||||
1. Fetch PR comments: `tea pr view <number>`
|
||||
2. Make requested changes
|
||||
3. Commit with descriptive message
|
||||
4. Push: `git push`
|
||||
5. Comment on PR that changes are ready
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "Address review feedback: fix edge case handling"
|
||||
git push
|
||||
tea pr comment <number> --body "Feedback addressed, ready for re-review."
|
||||
```
|
||||
|
||||
### /pr list
|
||||
|
||||
Show open PRs for current repo.
|
||||
|
||||
```bash
|
||||
tea pr list --state open
|
||||
```
|
||||
|
||||
## Branch Naming
|
||||
|
||||
```
|
||||
feature/REQ-123-user-login
|
||||
fix/REQ-456-order-calculation
|
||||
refactor/REQ-789-api-cleanup
|
||||
```
|
||||
|
||||
Format: `<type>/REQ-<id>-<brief-description>`
|
||||
|
||||
## PR Format
|
||||
|
||||
**Title:**
|
||||
```
|
||||
[REQ-123] Brief description of change
|
||||
```
|
||||
|
||||
**Description:**
|
||||
```markdown
|
||||
## What
|
||||
One paragraph describing the change.
|
||||
|
||||
## Why
|
||||
One paragraph explaining the motivation.
|
||||
```
|
||||
|
||||
## ai-proj Integration
|
||||
|
||||
- Check session context for current task ID
|
||||
- Extract from branch name if available: `feature/REQ-123-...` → `REQ-123`
|
||||
- Ask user if not found: "What's the task ticket? (e.g., REQ-123)"
|
||||
- Read-only: no status updates to ai-proj
|
||||
|
||||
## tea CLI Reference
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tea pr list` | List PRs |
|
||||
| `tea pr create --title T --body B --head H --base main` | Create PR |
|
||||
| `tea pr view N` | View PR details |
|
||||
| `tea pr diff N` | View PR diff |
|
||||
| `tea pr merge N` | Merge PR |
|
||||
| `tea pr close N` | Close PR without merging |
|
||||
| `tea pr comment N --body "..."` | Add comment |
|
||||
| `tea pr review N --approve` | Approve PR |
|
||||
| `tea pr review N --reject --body "..."` | Request changes |
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
/pr start feature REQ-123 user-auth
|
||||
↓
|
||||
(implement feature, commit changes)
|
||||
↓
|
||||
/pr create
|
||||
↓
|
||||
(reviewer: /pr review 42)
|
||||
↓
|
||||
(if changes needed: /pr update)
|
||||
↓
|
||||
tea pr merge 42
|
||||
```
|
||||
Reference in New Issue
Block a user