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:
2026-03-14 11:31:58 +10:30
parent ea266e9cce
commit 712063071c
170 changed files with 341 additions and 346 deletions

View 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"
}
}

View 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)

View File

@@ -0,0 +1,8 @@
{
"name": "dev-arch-plugin",
"description": "Plugin for dev-arch",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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)

View File

@@ -0,0 +1,8 @@
{
"name": "dev-coding-plugin",
"description": "Plugin for dev-coding",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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. **文档同步** - 代码变更同步更新文档

View File

@@ -0,0 +1,8 @@
{
"name": "dev-plugin",
"description": "Plugin for dev",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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 |

View File

@@ -0,0 +1,8 @@
{
"name": "dev-test-plugin",
"description": "软件测试技能。用于单元测试、集成测试、E2E测试、测试用例设计。支持 Go、Vue、React、iOS、Android 等多平台测试。",
"version": "2.0.0",
"author": {
"name": "qiudl"
}
}

View 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 + 真实 storemock 等于没测
7. **Mock 仅限 Handler 层** - handler 层可以 mock biz 接口 + httptest
7. **李宁测试用例** - Excel 导出见 `coolbuy-legacy` 技能的 `test-cases-excel.md`

View 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)
}
}
```

View 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:4010reporter=[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` 停止 |

View 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)
})
})
```

View 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
```

View 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 |

View File

@@ -0,0 +1,8 @@
{
"name": "finishing-a-development-branch-plugin",
"description": "Plugin for finishing-a-development-branch",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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

View File

@@ -0,0 +1,8 @@
{
"name": "frontend-design-plugin",
"description": "Plugin for frontend-design",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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 配置完整
- [ ] 包含组件文档描述
- [ ] 交互状态可测试
- [ ] 响应式展示
### 视觉质量检查
- [ ] 字体选择有特色
- [ ] 配色方案协调
- [ ] 动画流畅自然
- [ ] 间距一致
- [ ] 暗色主题支持

View File

@@ -0,0 +1,8 @@
{
"name": "gitea-plugin",
"description": "Gitea 代码托管与 CI/CD 管理。用于 Gitea Actions workflow 管理、Runner 管理、PR 操作、仓库配置。",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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

View 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或兼容写法 |

View File

@@ -0,0 +1,8 @@
{
"name": "pr-plugin",
"description": "Plugin for pr",
"version": "1.0.0",
"author": {
"name": "qiudl"
}
}

View 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
```