refactor(skills): Gitea技能拆分——gitea-pm/gitea-collab双技能+配置集中化+角色自动映射

This commit is contained in:
DESKTOP-2K5TJI4\admin
2026-06-08 14:02:10 +08:00
parent d36991299c
commit 37b8996fae
16 changed files with 1685 additions and 688 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ memory: project
### 步骤 1:获取项目状态
通过项目管理工具(`gitea-collab` 技能)执行:
通过项目管理工具(`gitea-pm` 技能)执行:
1. **读取 Pipeline Tracker** — 仅获取「当前阶段」(草图/总路线/迭代)
2. **列出激活里程碑** — `milestone list --state open`
+3 -3
View File
@@ -3,7 +3,7 @@
> 本文档定义游戏项目的开发阶段、任务管理、角色职责、产出标准和质量检查规则。
> 项目采用 3 阶段开发模型:草图 → 总路线 → 迭代。每个阶段有独立的技能文件管理任务序列。
>
> 项目管理工具当前使用 Gitea。所有操作命令和标签定义详见 `.claude/skills/gitea-collab.md`。
> 项目管理工具当前使用 Gitea。PM 操作详见 `gitea-pm` 技能,通用协作详见 `gitea-collab` 技能。
> 阶段技能文件:
> - `.claude/skills/project-initializer.md` — 项目初始化
> - `.claude/skills/stage-sketch.md` — 草图阶段
@@ -26,7 +26,7 @@
- **策划先行**:设计文档是其他角色的需求源
- **程序读设计**:实现前必须先读取对应设计文档和配置表
- **文档即交接**:Agent 之间不直接通信,通过共享文档目录 + 任务 Issue 评论交接(参见 `.claude/skills/gitea-collab.md`)
- **文档即交接**:Agent 之间不直接通信,通过共享文档目录 + 任务 Issue 评论交接(参见 `gitea-collab` 技能)
- **冲突时用户说了算**:玩法/数值/叙事的最终决定权在用户
- **PM 是进度调度器**:PM 管理进度和任务分配,不做审查
- **审查是子代理**:角色完成工作后启动 game-reviewer 子代理审查,审查结果返回给角色,角色向用户汇报
@@ -36,7 +36,7 @@
### 进度汇报规则
- 进度管理唯一真实来源是 **项目管理工具**(当前为 Gitea Issues + Milestones)。具体操作参见 `.claude/skills/gitea-collab.md`
- 进度管理唯一真实来源是 **项目管理工具**(当前为 Gitea Issues + Milestones)。具体操作参见 `gitea-pm` / `gitea-collab` 技能
- 各角色完成工作后在对应 Issue 下评论汇报,不更新本地 report 文件
- PM 通过项目管理工具读取任务和评论,给出行动建议
- 角色完成工作后自行启动 game-reviewer 子代理审查,审查通过后 PM 请示用户关闭 Issue
-379
View File
@@ -1,379 +0,0 @@
---
name: "gitea-collab"
description: "Gitea 协作工具 — CLI 命令速查、认证机制、Assignee 映射、标签体系、Issue/Milestone/Pipeline 操作规范、角色汇报模板"
---
# Gitea 协作工具
> 本 skill 封装 ChildHood 项目基于 Gitea 的进度管理全部实现细节。
> Agent 文件只描述项目管理的逻辑(做什么),本文件描述实现细节(怎么做)。
> 未来若更换项目管理后端,只需替换本 skill,Agent 逻辑无需修改。
---
## 1. CLI 工具
**工具路径**: `node scripts/gitea.js <command> [subcommand] [options]`
**API 封装**: `scripts/gitea-api.js`(GiteaAPI 类,HTTP REST 调用)
**服务端**: `micecola.top:8418`,仓库 `starmen1265/ChildHood`
**`--json` 标志**: 所有命令支持 `--json` 输出纯 JSON,供脚本解析。
---
## 2. 认证机制
通过 `--role <角色>` 参数自动从 `scripts/.gitea-tokens.json` 读取对应 Token,无需手动传 `GITEA_TOKEN`。
| 角色 | `--role` 值 | 适用 Agent |
|------|------------|-----------|
| 策划 | `designer` | game-designer |
| 程序 | `dev` | game-dev |
| 美术 | `artist` | (暂无独立 Agent) |
| PM | `pm` | game-pm |
| 审查 | `reviewer` | game-reviewer |
---
## 3. Assignee 映射
创建任务时通过 `--assignees` 指派给对应角色的 Gitea 用户。
| 角色 | `--assignees` 值 | 任务 ID 前缀 |
|------|-----------------|-------------|
| 策划 | `ai-designer` | `D-XXX` |
| 程序 | `ai-programmer` | `P-XXX` |
| 美术 | `ai-artist` | `A-XXX` |
| PM | `ai-pm` | (管理任务) |
| 审查 | `ai-reviewer` | (审查操作,不拥有任务) |
---
## 4. 标签体系
19 个标准标签,通过 `node scripts/gitea.js label init` 初始化。
### status(任务状态)
| 标签 | 含义 |
|------|------|
| `status:backlog` | 待处理 |
| `status:ready` | 已就绪,可开始 |
| `status:in-progress` | 进行中 |
| `status:blocked` | 被阻塞 |
| `status:in-review` | 待审查(角色完成工作后设置) |
| `status:done` | 已完成 |
### gate(质量门判定 — 旧标签,保留用于历史 Issue)
| 标签 | 含义 |
|------|------|
| `gate:execution-pass/fail` | 执行案质量门 |
| `gate:tech-pass/fail` | 程序设计质量门 |
| `gate:art-pass/fail` | 美术资产验收门 |
| `gate:impl-pass/fail` | 程序实现验收门 |
> 新工作流使用 `review:*` 标签替代 `gate:*`。旧 Issue 上的 gate 标签保留不动。
### review(审查结果 — 新标签)
| 标签 | 含义 |
|------|------|
| `review:pass` | 审查已通过 ✅ |
| `review:fail` | 审查未通过 ❌ |
### stage(开发阶段 — 新标签)
| 标签 | 含义 |
|------|------|
| `stage:sketch` | 草图阶段任务 |
| `stage:roadmap` | 总路线阶段任务 |
| `stage:iteration` | 迭代阶段任务 |
> **注**:`pipeline:*` 和 `role:*` 标签已废弃并从 Gitea 上删除。如旧 Issue 上仍残留这些标签,忽略即可。
---
## 5. 命令速查
### issue
```bash
# 列出 Issue(支持按 assignee 过滤)
node scripts/gitea.js --role <role> issue list --milestone <标题> [--state open|closed|all] [--labels <id,...>] [--assigned <用户名>] [--json]
# 查看 Issue 详情
node scripts/gitea.js --role <role> issue get <number> [--json]
# 创建 Issue(必须指定 assignees)
node scripts/gitea.js --role <role> issue create --title "..." --body "..." --milestone <id> --assignees <用户名,...>
# 更新 Issue
node scripts/gitea.js --role <role> issue update <number> --title "..." --body "..."
# 关闭 Issue
node scripts/gitea.js --role <role> issue close <number> [--body "关闭说明"]
# 重新打开 Issue
node scripts/gitea.js --role <role> issue reopen <number>
# 评论
node scripts/gitea.js --role <role> issue comment <number> --body "评论内容"
# 查看评论
node scripts/gitea.js --role <role> issue comments <number> [--json]
# 设置标签
node scripts/gitea.js --role <role> issue labels <number> --set <id,id,...>
# 依赖关系
node scripts/gitea.js --role <role> issue deps <number>
# 阻塞关系
node scripts/gitea.js --role <role> issue block <blocker_number> <blocked_number>
node scripts/gitea.js --role <role> issue unblock <blocker_number> <blocked_number>
```
### milestone
```bash
# 列出里程碑
node scripts/gitea.js --role <role> milestone list [--state open|closed|all] [--json]
# 创建里程碑
node scripts/gitea.js --role <role> milestone create --title "..." --description "..."
# 更新里程碑
node scripts/gitea.js --role <role> milestone update <id> --title "..." --description "..." --due "YYYY-MM-DD"
```
### label
```bash
# 列出标签
node scripts/gitea.js --role <role> label list [--json]
# 创建标签
node scripts/gitea.js --role <role> label create --name "..." --color "#hex"
# 初始化标准标签体系
node scripts/gitea.js --role <role> label init
```
### pipeline
```bash
# 初始化 Pipeline Tracker Meta Issue
node scripts/gitea.js --role <role> pipeline init
# 查看 Pipeline 状态
node scripts/gitea.js --role <role> pipeline status
# 更新 Pipeline 步骤状态
node scripts/gitea.js --role <role> pipeline update <tracker_number> --step <1-7> --status "状态描述"
```
### test
```bash
# 运行 API 连通性测试(P0 + P1)
node scripts/gitea.js test
```
---
## 6. 数据映射
| 概念 | Gitea 对应 | 说明 |
|------|-----------|------|
| 任务 (P-002) | Issue | 标题含任务 ID 前缀 |
| 阶段/版本 | Milestone | 阶段名或版本号作为里程碑标题 |
| 任务状态 | Label (`status:*`) | 6 种状态标签 |
| 审查结果 | Label (`review:*`) | 通过/未通过 |
| 开发阶段 | Label (`stage:*`) | 草图/总路线/迭代 |
| 质量门结果 | Label (`gate:*`) | 旧标签,保留用于历史 Issue |
| 角色归属 | Assignee | 通过 `--assignees` 指派 |
| 当前进度 | Pipeline Tracker | Meta Issue 记录当前阶段和进度 |
| 角色汇报 | Issue 评论 | 各角色在对应 Issue 下评论 |
| 审查记录 | Issue 评论(摘要+链接)+ 本地 `docs/plans/reviews/` | 本地文件名格式 `<任务ID>-审查记录.md` |
| 任务归档 | close Issue | 用户确认后由 PM 关闭 |
---
## 7. 任务 ID 规范
格式:`[角色前缀]-[三位数字]`
| 前缀 | 角色 | 示例 |
|------|------|------|
| `D` | 策划 (Design) | `D-001` |
| `P` | 程序 (Programmer) | `P-002` |
| `A` | 美术 (Artist) | `A-003` |
每个前缀从 001 独立编号。Issue 标题需包含任务 ID 前缀。
---
## 8. Pipeline Tracker
Pipeline Tracker 是一个 **Meta Issue**,仅记录**无法从 Issue/Milestone 系统直接推导**的信息。
### Tracker 正文只保留
```markdown
# 流水线进度追踪
## 当前阶段
草图阶段 / 总路线阶段 / 迭代阶段
## 当前迭代
v1.4(迭代阶段时填写,其他阶段留空)
## 阶段历史
| 阶段 | 里程碑 | 进入日期 | 完成日期 |
|------|--------|----------|----------|
| 草图阶段 | 草图阶段 | YYYY-MM-DD | YYYY-MM-DD |
| 总路线阶段 | 总路线阶段 | YYYY-MM-DD | — |
```
### 不记录的信息(从 Issue/Milestone 直接查询)
- ~~当前里程碑详情~~ → `milestone list --state open`
- ~~Open/Closed Issues 计数~~ → `milestone list` 自带
- ~~7 步流水线步骤状态~~ → 从任务的 status/review 标签聚合
- ~~相关任务索引~~ → `issue list --state open`
- ~~当前阻塞~~ → `issue list --labels status:blocked`
### 相关命令
- 初始化:`pipeline init`
- 查看状态:`pipeline status`
- 阶段转换时直接编辑 Issue 正文更新「当前阶段」和「阶段历史」
---
## 9. 角色汇报模板
各角色完成工作后,在对应 Issue 下评论汇报。
### 策划汇报
```bash
node scripts/gitea.js --role designer issue comment <number> --body "## 策划汇报 — YYYY-MM-DD
**任务ID**: D-XXX
**状态**: 进行中 / 已完成
**产出**: path/to/output
### 完成内容
- ...
### 待确认
- ..."
```
### 程序汇报
```bash
node scripts/gitea.js --role dev issue comment <number> --body "## 程序汇报 — YYYY-MM-DD
**任务ID**: P-XXX
**状态**: 进行中 / 已完成
**产出**: path/to/output
### 完成内容
- ...
### 偏差/问题
- ..."
```
### 审查修改后汇报(审查退回修改后使用)
```bash
node scripts/gitea.js --role <role> issue comment <number> --body "## 修改汇报 — YYYY-MM-DD
**任务ID**: <ID>
**修改原因**: 质量门审查退回
### 修改内容
- [对应审查意见第X条]:[具体修改内容]
### 修改后产出
- path/to/output"
```
### PM 确认关闭任务(审查通过后)
```bash
# 用户确认后关闭 Issue
node scripts/gitea.js --role pm issue close <number> --body "## 任务完成 ✔
**完成日期**:YYYY-MM-DD
**产出**:path/to/output
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md"
```
---
## 10. PM 会话启动
PM 每次启动时:
1. **读取当前阶段**:`pipeline status`(仅获取当前阶段)
2. **列出激活里程碑**:`milestone list --state open`
3. **列出所有活跃任务**:`issue list --state open`,展示摘要(任务 ID、标题、状态标签、assignee)
4. **展开需要关注任务的详情**:仅对 `status:in-progress`、`status:blocked`、`review:pass` 的任务执行 `issue get` 获取详情
5. **按阶段调用技能**:无进度 → project-initializer / 草图 → stage-sketch / 总路线 → stage-roadmap / 迭代 → stage-iteration
---
## 11. PM 任务管理命令序列
### 创建任务
```bash
# 创建 Issue(必须指定 assignees 和 stage 标签)
node scripts/gitea.js --role pm issue create \
--title "D-001 概念设计" \
--body "## 任务信息\n- **任务ID**: D-001\n- **所属阶段**: 草图阶段\n- **角色**: 策划\n- **预期产出**: docs/design/概念草案.md\n..." \
--milestone <id> \
--assignees ai-designer
# 设置标签(status + stage + role)
node scripts/gitea.js --role pm issue labels <number> --set <status-label-id>,<stage-label-id>,<role-label-id>
# 设置依赖
node scripts/gitea.js --role pm issue block <blocker_number> <new_number>
```
### 关闭任务(用户确认后)
```bash
# 审查通过 + 用户确认关闭后
node scripts/gitea.js --role pm issue close <number> --body "## 任务完成 ✔\n**完成日期**: YYYY-MM-DD\n**产出**: path/to/output\n**审查记录**: docs/plans/reviews/<task-ID>-审查记录.md"
```
---
## 12. 角色(非 PM)任务查询
非 PM 角色启动时只需三步,**不需要**读取 Pipeline Tracker:
```bash
# 1. 查看当前激活里程碑
node scripts/gitea.js --role <role> milestone list --state open
# 2. 查看分配给自己的活跃任务
node scripts/gitea.js --role <role> issue list --assigned <自己的用户名> --state open
# 3. 查看具体任务详情(仅在需要时)
node scripts/gitea.js --role <role> issue get <number>
```
### 角色参数速查
| 角色 | `--role` | `--assigned` |
|------|----------|-------------|
| 策划 | `designer` | `ai-designer` |
| 程序 | `dev` | `ai-programmer` |
| 美术 | `artist` | `ai-artist` |
| 审查 | `reviewer` | (不拥有任务) |
+201
View File
@@ -0,0 +1,201 @@
---
name: "gitea-collab"
description: "通用协作技能 — 任务查询、进度汇报、审查操作。非 PM 角色使用。"
---
# Gitea 协作技能
> 通用协作技能,供策划、程序、美术、审查角色使用。
> 所有配置从 `gitea-config.json` 读取。
> 本文档不含 PM 专属命令。
---
## 1. CLI 工具
```bash
node .claude/skills/gitea-collab/gitea.js --role <角色名> <command> [subcommand] [options]
```
**`--json` 标志**:所有命令支持 `--json` 输出纯 JSON。
---
## 2. 角色参数
使用 `--role <角色名>` 参数,自动解析为 Token 和身份:
| 角色 | `--role` 值 |
|------|------------|
| 策划 | `designer` |
| 程序 | `dev` |
| 美术 | `artist` |
| 审查 | `reviewer` |
**不需要记住 Gitea 用户名** — `--role` 自动处理认证和身份。
---
## 3. 任务查询(启动时使用)
### 查看激活里程碑
```bash
node .claude/skills/gitea-collab/gitea.js --role <角色名> milestone list --state open
```
### 查看分配给自己的活跃任务
```bash
# --assigned 用角色名,自动映射
node .claude/skills/gitea-collab/gitea.js --role <角色名> issue list --assigned <角色名> --state open
```
### 查看任务详情
```bash
node .claude/skills/gitea-collab/gitea.js --role <角色名> issue get <number>
```
---
## 4. 进度汇报
### 添加进度评论
```bash
node .claude/skills/gitea-collab/gitea.js --role <角色名> issue comment <number> --body "汇报内容"
```
### 查看已有评论
```bash
node .claude/skills/gitea-collab/gitea.js --role <角色名> issue comments <number>
```
### 查看任务依赖
```bash
node .claude/skills/gitea-collab/gitea.js --role <角色名> issue deps <number>
```
---
## 5. 审查操作(reviewer 使用)
### 评论审查结果
```bash
node .claude/skills/gitea-collab/gitea.js --role reviewer issue comment <number> --body "## 审查通过 ✅
**审查类型**:[执行案/程序设计/美术资产/程序实现]
**审查日期**:YYYY-MM-DD
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
**审查者**:审查 Agent (ai-reviewer)
### 结论
[一句话总结]
### 检查通过项 / 问题清单
- ..."
```
### 更新 Issue 标签
```bash
# 审查通过:移除 status:in-review,添加 review:pass
node .claude/skills/gitea-collab/gitea.js --role reviewer issue labels <number> --set <保留的非status标签ID>,<review:pass的ID>
# 审查未通过:移除 status:in-review,改为 status:blocked,添加 review:fail
node .claude/skills/gitea-collab/gitea.js --role reviewer issue labels <number> --set <status:blocked的ID>,<保留标签ID>,<review:fail的ID>
```
---
## 6. 审查评论格式
### 通过
```markdown
## 审查通过 ✅
**审查类型**:[执行案/程序设计/美术资产/程序实现]
**审查日期**:YYYY-MM-DD
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
**审查者**:审查 Agent (ai-reviewer)
### 结论
[一句话总结]
### 检查通过项
- ✅ [检查项1]
- ✅ [检查项2]
```
### 未通过
```markdown
## 审查未通过 ❌
**审查类型**:[执行案/程序设计/美术资产/程序实现]
**审查日期**:YYYY-MM-DD
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
**审查者**:审查 Agent (ai-reviewer)
### 结论
[一句话总结]
### 问题清单
1. **[检查维度]**:[具体问题描述,附文档位置] — [修改建议]
```
---
## 7. 汇报模板
### 策划汇报
```markdown
## 策划汇报 — YYYY-MM-DD
**任务ID**: D-XXX
**状态**: 进行中 / 已完成
**产出**: path/to/output
### 完成内容
- ...
### 待确认
- ...
```
### 程序汇报
```markdown
## 程序汇报 — YYYY-MM-DD
**任务ID**: P-XXX
**状态**: 进行中 / 已完成
**产出**: path/to/output
### 完成内容
- ...
### 偏差/问题
- ...
```
### 审查退回后修改汇报
```markdown
## 修改汇报 — YYYY-MM-DD
**任务ID**: <ID>
**修改原因**: 质量门审查退回
### 修改内容
- [对应审查意见第X条]:[具体修改内容]
### 修改后产出
- path/to/output
```
@@ -1,29 +1,29 @@
/**
* Gitea API Client — ChildHood 项目进度管理
* Gitea API Client — 通用项目管理工具
*
* 封装所有 Gitea REST API 调用,处理 UTF-8 编码和错误重试。
* 供 gitea.js CLI 使用。
* 供 gitea.js CLI 使用。连接信息从 gitea-config.json 读取。
*/
const http = require('http');
const DEFAULT_CONFIG = {
host: 'micecola.top',
port: 8418,
apiBase: '/api/v1',
repoPath: '/repos/starmen1265/ChildHood',
timeout: 15000,
};
class GiteaAPI {
/**
* @param {string} token - Gitea API Token
* @param {object} [config] - 覆盖默认配置
* @param {object} config - 必须包含 host, port, repoOwner, repoName
*/
constructor(token, config = {}) {
if (!token) throw new Error('GITEA_TOKEN 未设置。请设置环境变量 GITEA_TOKEN。');
this.token = token;
this.config = { ...DEFAULT_CONFIG, ...config };
this.config = {
apiBase: '/api/v1',
timeout: 15000,
...config,
};
// 从 config 构建 repoPath
if (!this.config.repoPath && this.config.repoOwner && this.config.repoName) {
this.config.repoPath = `/repos/${this.config.repoOwner}/${this.config.repoName}`;
}
}
// ====== 底层 HTTP 请求 ======
@@ -206,11 +206,10 @@ class GiteaAPI {
/** 设置依赖:number 阻塞 blockedNumber */
addBlock(number, blockedNumber) {
// Gitea 要求 body 为 IssueMeta 格式(含 owner + repo)
return this.request('POST', `/issues/${number}/blocks`, {
index: blockedNumber,
owner: this.config.repoPath.split('/')[2], // /repos/{owner}/{repo}
repo: this.config.repoPath.split('/')[3],
owner: this.config.repoOwner,
repo: this.config.repoName,
});
}
@@ -218,8 +217,8 @@ class GiteaAPI {
removeBlock(number, blockedNumber) {
return this.request('DELETE', `/issues/${number}/blocks`, {
index: blockedNumber,
owner: this.config.repoPath.split('/')[2],
repo: this.config.repoPath.split('/')[3],
owner: this.config.repoOwner,
repo: this.config.repoName,
});
}
@@ -251,4 +250,4 @@ class GiteaAPI {
}
}
module.exports = { GiteaAPI, DEFAULT_CONFIG };
module.exports = { GiteaAPI };
@@ -1,42 +1,28 @@
#!/usr/bin/env node
/**
* ChildHood Gitea CLI — 项目进度管理工具
* Gitea CLI — 项目管理工具
*
* 用法: node scripts/gitea.js <command> [subcommand] [options]
* 环境变量: GITEA_TOKEN(必须)
* 用法: node gitea.js --role <角色> <command> [subcommand] [options]
* 所有配置(连接信息、Token、角色映射、标签定义)从 gitea-config.json 读取。
*/
const fs = require('fs');
const path = require('path');
const { GiteaAPI } = require('./gitea-api');
// ═══════════════════════════════════════════════════
// 标准标签定义
// 加载配置
// ═══════════════════════════════════════════════════
const STANDARD_LABELS = [
// Status (6)
{ name: 'status:backlog', color: '#666666', description: '已定义但未排入当前工作' },
{ name: 'status:ready', color: '#007fff', description: '依赖已满足,可以开始' },
{ name: 'status:in-progress', color: '#e8b50a', description: '正在进行中' },
{ name: 'status:blocked', color: '#ee0701', description: '被阻塞(质量门未通过/依赖未完成)' },
{ name: 'status:in-review', color: '#8957e5', description: '等待质量门审查' },
{ name: 'status:done', color: '#0e8a16', description: '已完成' },
// Gate (8)
{ name: 'gate:execution-pass', color: '#28a745', description: '执行案质量门 ✅' },
{ name: 'gate:execution-fail', color: '#d73a49', description: '执行案质量门 ❌' },
{ name: 'gate:tech-pass', color: '#28a745', description: '程序设计质量门 ✅' },
{ name: 'gate:tech-fail', color: '#d73a49', description: '程序设计质量门 ❌' },
{ name: 'gate:art-pass', color: '#28a745', description: '美术资产验收门 ✅' },
{ name: 'gate:art-fail', color: '#d73a49', description: '美术资产验收门 ❌' },
{ name: 'gate:impl-pass', color: '#28a745', description: '程序实现验收门 ✅' },
{ name: 'gate:impl-fail', color: '#d73a49', description: '程序实现验收门 ❌' },
// Review (2)
{ name: 'review:pass', color: '#28a745', description: '审查已通过 ✅' },
{ name: 'review:fail', color: '#d73a49', description: '审查未通过 ❌' },
// Stage (3)
{ name: 'stage:sketch', color: '#bfd4f2', description: '草图阶段' },
{ name: 'stage:roadmap', color: '#d4c5f9', description: '总路线阶段' },
{ name: 'stage:iteration', color: '#fce6c0', description: '迭代阶段' },
];
const CONFIG_PATH = path.join(__dirname, 'gitea-config.json');
let config;
try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
} catch (e) {
console.error(`❌ 无法读取配置文件: ${CONFIG_PATH}`);
console.error(` ${e.message}`);
process.exit(1);
}
// ═══════════════════════════════════════════════════
// 命令行参数解析
@@ -79,38 +65,80 @@ function outText(text) {
if (!globalArgs.options.json) console.log(text);
}
function outJson(data) {
console.log(JSON.stringify(data, null, 2));
}
/** 简易表格输出 */
function table(headers, rows) {
if (!rows.length) { outText(' (空)'); return; }
const widths = headers.map((h, i) =>
Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length))
);
const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' ');
outText(' ' + fmt(headers));
outText(' ' + fmt(headers.map(() => '─'.repeat(2))));
for (const r of rows) outText(' ' + fmt(r));
}
function ok(msg) { outText(`✅ ${msg}`); }
function fail(msg) { outText(`❌ ${msg}`); }
function info(msg) { outText(`ℹ️ ${msg}`); }
/** 截断长文本 */
function truncate(s, max = 60) {
if (!s) return '';
const oneLine = s.replace(/\n/g, ' ').trim();
return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
}
/** 从 Issue 提取 label 名称列表 */
function labelNames(issue) {
return (issue.labels || []).map(l => l.name);
}
/** 简易表格输出 */
function table(headers, rows) {
if (!rows.length) { outText(' (空)'); return; }
const widths = headers.map((h, i) =>
Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length)))
;
const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' ');
outText(' ' + fmt(headers));
outText(' ' + fmt(headers.map(() => '─'.repeat(2))));
for (const r of rows) outText(' ' + fmt(r));
}
// ═══════════════════════════════════════════════════
// 角色解析
// ═══════════════════════════════════════════════════
/**
* 获取 Token(优先级:环境变量 > config.roles[role].token)
*/
function resolveToken(options) {
if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN;
const role = options.role;
if (role) {
const validRoles = Object.keys(config.roles);
if (!validRoles.includes(role)) {
console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`);
process.exit(1);
}
const token = config.roles[role]?.token;
if (!token) {
console.error(`❌ 角色 "${role}" 未配置 token。请在 gitea-config.json 中设置。`);
process.exit(1);
}
return token;
}
console.error('❌ 缺少认证信息。请使用以下方式之一:');
console.error(' --role <角色名> 从 gitea-config.json 读取 Token');
console.error(' GITEA_TOKEN=xxx 通过环境变量传入');
process.exit(1);
}
/**
* 解析 assignee:支持角色名(自动映射)或直接用户名
*/
function resolveAssignee(value) {
if (!value) return null;
return config.roles[value]?.assignee || value;
}
/**
* 自动从 --role 解析 assignee(用于 issue create)
*/
function autoAssignee(role) {
if (!role) return [];
const assignee = config.roles[role]?.assignee;
return assignee ? [assignee] : [];
}
// ═══════════════════════════════════════════════════
// 命令实现
// ═══════════════════════════════════════════════════
@@ -124,7 +152,6 @@ cmd.issue = {
const params = {};
if (opts.options.state) params.state = opts.options.state;
if (opts.options.milestone) {
// 支持数字 ID 或标题
const ms = isNaN(opts.options.milestone)
? await api.findMilestoneByTitle(opts.options.milestone)
: { id: Number(opts.options.milestone) };
@@ -132,7 +159,10 @@ cmd.issue = {
else { fail(`未找到里程碑: ${opts.options.milestone}`); return; }
}
if (opts.options.labels) params.labels = opts.options.labels;
if (opts.options.assigned) params.assigned_by = opts.options.assigned;
if (opts.options.assigned) {
// 支持角色名或直接用户名
params.assigned_by = resolveAssignee(opts.options.assigned);
}
const issues = await api.listIssues(params);
out(issues);
@@ -186,8 +216,11 @@ cmd.issue = {
if (opts.options.labels) {
data.labels = opts.options.labels.split(',').map(Number);
}
// assignee: 显式指定 > 自动从 --role 映射
if (opts.options.assignees) {
data.assignees = opts.options.assignees.split(',');
data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a));
} else {
data.assignees = autoAssignee(opts.options.role);
}
const issue = await api.createIssue(data);
@@ -212,7 +245,9 @@ cmd.issue = {
: { id: Number(opts.options.milestone) };
if (ms) data.milestone = ms.id;
}
if (opts.options.assignees) data.assignees = opts.options.assignees.split(',');
if (opts.options.assignees) {
data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a));
}
const issue = await api.updateIssue(num, data);
out(issue);
@@ -273,7 +308,6 @@ cmd.issue = {
await api.replaceIssueLabels(num, ids);
if (!opts.options.json) ok(`Issue #${num} 的标签已替换为: ${ids.join(', ')}`);
} else {
// 查询当前标签
const labels = await api.getIssueLabels(num);
out(labels);
if (!opts.options.json) {
@@ -392,11 +426,13 @@ cmd.label = {
async init(api, opts) {
outText('\n🏷️ 初始化标准标签体系…\n');
// 从 config.labels 合并所有标签定义
const allLabels = Object.values(config.labels).flat();
const existing = await api.listLabels();
const existingNames = new Set(existing.map(l => l.name));
let created = 0, skipped = 0;
for (const def of STANDARD_LABELS) {
for (const def of allLabels) {
if (existingNames.has(def.name)) {
info(`跳过 (已存在): ${def.name}`);
skipped++;
@@ -414,31 +450,21 @@ cmd.label = {
cmd.pipeline = {
async init(api, opts) {
const title = opts.options.title || 'Pipeline Tracker — v0.1';
const title = opts.options.title || 'Pipeline Tracker';
const milestone = opts.options.milestone;
const body = `# 流水线进度追踪
## v0.1 流水线
## 当前阶段
草图阶段
| 步骤 | 状态 | 说明 |
|------|------|------|
| ① 设计案 | ✅ 已完成 | \`docs/design/prototype/\` 下 10 份设计文档 |
| ② 阶段路线图 | ✅ 已完成 | \`docs/plans/prototype-roadmap.md\` |
| ③ 执行案 | 🔴 质量门未通过 | 需创建独立执行案并修正 |
| ④ 程序设计 | ⬜ 未开始 | 依赖 ③ |
| ⑤ 美术产出 | ⬜ 未开始 | 白盒场景 + 占位资产 |
| ⑥ 程序实现 | ⬜ 未开始 | 原型允许与 ⑤ 交替 |
| ⑦ 版本复盘 | ⬜ 未开始 | — |
## 当前迭代
(非迭代阶段,留空)
## 当前阻塞
③ 执行案质量门未通过,需修正后重新提交
## 相关任务
> PM 在此维护 Issue 索引
`;
## 阶段历史
| 阶段 | 里程碑 | 进入日期 | 完成日期 |
|------|--------|----------|----------|
| 草图阶段 | 草图阶段 | ${new Date().toISOString().slice(0, 10)} | — |`;
const data = { title, body };
if (milestone) {
@@ -457,10 +483,8 @@ cmd.pipeline = {
},
async status(api, opts) {
// 查找 Pipeline Tracker Issue
let tracker = await api.findIssueByTitle('Pipeline Tracker');
if (!tracker) { fail('未找到 Pipeline Tracker Issue。请先运行: pipeline init'); return; }
// 获取完整内容
tracker = await api.getIssue(tracker.number);
out({ number: tracker.number, title: tracker.title, body: tracker.body });
if (!opts.options.json) {
@@ -471,30 +495,28 @@ cmd.pipeline = {
async update(api, opts) {
const num = opts.rest[0];
const step = opts.options.step;
const status = opts.options.status;
if (!num || !step || !status) {
fail('用法: pipeline update <issue-number> --step <1-7> --status <状态描述>');
const field = opts.options.field;
const value = opts.options.value;
if (!num || !field || !value) {
fail('用法: pipeline update <issue-number> --field <阶段|当前迭代> --value <值>');
return;
}
const issue = await api.getIssue(num);
let body = issue.body || '';
const stepLabels = ['', '① 设计案', '② 阶段路线图', '③ 执行案', '④ 程序设计', '⑤ 美术产出', '⑥ 程序实现', '⑦ 版本复盘'];
const stepLabel = stepLabels[Number(step)];
if (!stepLabel) { fail(`无效步骤: ${step},应为 1-7`); return; }
// 替换对应步骤的状态
const regex = new RegExp(`(\\| ${stepLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} \\| )(.+?)( \\|)`);
if (regex.test(body)) {
body = body.replace(regex, `$1${status}$3`);
await api.updateIssue(num, { body });
out({ number: Number(num), step, status, updated: true });
if (!opts.options.json) ok(`步骤 ${stepLabel} 已更新为: ${status}`);
if (field === '阶段') {
body = body.replace(/## 当前阶段\n.*/, `## 当前阶段\n${value}`);
} else if (field === '当前迭代') {
body = body.replace(/## 当前迭代\n.*/, `## 当前迭代\n${value}`);
} else {
fail(`未在 Issue #${num} 的 body 中找到步骤: ${stepLabel}`);
fail(`无效字段: ${field},可选: 阶段, 当前迭代`);
return;
}
await api.updateIssue(num, { body });
out({ number: Number(num), field, value, updated: true });
if (!opts.options.json) ok(`Pipeline Tracker 字段 "${field}" 已更新为: ${value}`);
},
};
@@ -525,75 +547,77 @@ cmd.test = {
await test('GET /milestones', () => api.listMilestones());
await test('GET /issues', () => api.listIssues({ limit: 5 }));
await test('GET /labels', () => api.listLabels());
await test('GET /issues/1', () => api.getIssue(1));
await test('GET /issues/1/comments', () => api.listComments(1));
await test('GET /issues/1/dependencies', () => api.listDependencies(1));
await test('GET /issues/1/blocks', () => api.listBlocks(1));
// 查找第一个存在的 Issue 用于后续测试
const issues = await api.listIssues({ limit: 5, state: 'all' });
const testIssue = issues.find(i => i.state === 'open') || issues[0];
if (testIssue) {
await test(`GET /issues/${testIssue.number}`, () => api.getIssue(testIssue.number));
await test(`GET /issues/${testIssue.number}/comments`, () => api.listComments(testIssue.number));
await test(`GET /issues/${testIssue.number}/dependencies`, () => api.listDependencies(testIssue.number));
await test(`GET /issues/${testIssue.number}/blocks`, () => api.listBlocks(testIssue.number));
}
await test('POST 创建含中文 Issue', async () => {
const i = await api.createIssue({
title: '测试 Issue — UTF-8 验证 🎮',
body: '这是中文内容测试。\n\n- 列表项\n- 特殊字符:✅❌🔴⬜\n\n创建时间: ' + new Date().toISOString(),
});
// 立即关闭测试 Issue
await api.closeIssue(i.number);
return `#${i.number}: ${i.title}`;
});
if (testIssue) {
await test('POST 创建含中文评论', async () => {
const c = await api.createComment(1, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19));
const c = await api.createComment(testIssue.number, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19));
return `comment_id: ${c.id}`;
});
await test('PATCH 更新 Issue', async () => {
// 读取 Issue #2 的当前标题,更新后再还原
const orig = await api.getIssue(2);
const orig = await api.getIssue(testIssue.number);
const newTitle = orig.title + ' [测试更新]';
await api.updateIssue(2, { title: newTitle });
await api.updateIssue(2, { title: orig.title }); // 还原
return 'Issue #2 标题更新并还原';
await api.updateIssue(testIssue.number, { title: newTitle });
await api.updateIssue(testIssue.number, { title: orig.title });
return `Issue #${testIssue.number} 标题更新并还原`;
});
}
await test('PATCH 关闭/重开 Issue', async () => {
// 创建临时 Issue 测试
const tmp = await api.createIssue({ title: '临时测试 — 开关', body: '将被关闭再打开' });
await api.closeIssue(tmp.number);
await api.reopenIssue(tmp.number);
await api.closeIssue(tmp.number); // 最终关闭
await api.closeIssue(tmp.number);
return `#${tmp.number} 关闭→重开→关闭`;
});
await test('POST 创建 Label', async () => {
const l = await api.createLabel({ name: '_test-label', color: 'ededed' });
// 清理
try { await api.deleteLabel(l.id); } catch { /* ignore */ }
return `label: ${l.name} (ID: ${l.id}) 已创建并清理`;
});
if (testIssue) {
await test('POST Issue 添加标签', async () => {
// 获取已有 labels,添加到 Issue #1 再移除
const labels = await api.listLabels();
if (!labels.length) return '无可用标签,跳过';
await api.addIssueLabels(1, [labels[0].id]);
return `添加标签 ${labels[0].name} 到 Issue #1`;
await api.addIssueLabels(testIssue.number, [labels[0].id]);
return `添加标签 ${labels[0].name} 到 Issue #${testIssue.number}`;
});
}
// P1: 核心功能
outText('\n── P1: 核心功能验证 ──\n');
await test('GET /issues?milestones=X 过滤', async () => {
const ms = await api.findMilestoneByTitle('v0.1-基础交互');
if (!ms) return '未找到 v0.1 里程碑';
const issues = await api.listIssues({ milestones: ms.id });
return `里程碑 ${ms.title} 下有 ${issues.length} 个 Issue`;
});
if (testIssue && issues.length >= 2) {
const other = issues.find(i => i.number !== testIssue.number);
if (other) {
await test('POST/DELETE Issue 依赖', async () => {
// Issue #1 block #2,然后移除
await api.addBlock(1, 2);
await api.removeBlock(1, 2);
return '依赖关系添加→移除';
await api.addBlock(testIssue.number, other.number);
await api.removeBlock(testIssue.number, other.number);
return `依赖关系 ${testIssue.number}→${other.number} 添加→移除`;
});
}
}
await test('POST/PATCH Milestone', async () => {
const ms = await api.createMilestone({ title: '_test-milestone', state: 'open' });
@@ -616,32 +640,34 @@ cmd.test = {
// ═══════════════════════════════════════════════════
function showHelp() {
const roles = Object.keys(config.roles).join('|');
console.log(`
ChildHood Gitea CLI — 项目进度管理工具
Gitea CLI — 项目管理工具
用法: node scripts/gitea.js <command> [subcommand] [options]
用法: node gitea.js --role <${roles}> <command> [subcommand] [options]
全局选项:
--help 显示帮助信息
--json 以 JSON 格式输出(供 Agent 脚本调用)
--role <pm|designer|dev> 使用对应角色的 Token(从 scripts/.gitea-tokens.json 读取)
--json 以 JSON 格式输出
--role <角色> 使用对应角色的身份(自动解析 Token 和 Assignee)
命令列表:
issue list 列出 Issues
[--state open|closed|all] [--milestone <id|标题>] [--labels <id,...>] [--assigned <用户名>]
[--state open|closed|all] [--milestone <id|标题>]
[--labels <id,...>] [--assigned <角色名|用户名>]
issue get <#> 查看 Issue 详情
issue create 创建 Issue
--title "标题" [--body "内容"] [--milestone <id>] [--labels <id,...>] [--assignees <用户名,...>]
issue update <#> 更新 Issue
[--title "…"] [--body "…"] [--state open|closed] [--milestone <id>]
issue create 创建 Issue(assignee 自动从 --role 映射)
--title "标题" [--body "内容"] [--milestone <id>]
[--labels <id,...>] [--assignees <角色名,...>]
issue update <#> 更新 Issue [--title "…"] [--body "…"] [--state open|closed]
issue close <#> 关闭 Issue [--body "完成评论"]
issue reopen <#> 重新打开 Issue
issue comment <#> 添加评论 --body "内容"
issue comments <#> 查看评论列表
issue labels <#> 查看/管理标签 [--add <id,...>] [--set <id,...>]
issue deps <#> 查看依赖关系
issue block <#> <#> 设置依赖关系(前者阻塞后者)
issue block <#> <#> 设置依赖关系
issue unblock <#> <#> 移除依赖关系
milestone list 列出里程碑 [--state open|closed|all]
@@ -650,24 +676,20 @@ ChildHood Gitea CLI — 项目进度管理工具
label list 列出标签
label create 创建标签 --name "名称" [--color "#RRGGBB"]
label init 初始化标准标签体系(status/gate 共 14 个)
label init 初始化标准标签体系(从 gitea-config.json 读取定义)
pipeline init 创建 Pipeline Tracker Meta Issue [--title "…"] [--milestone <id>]
pipeline init 创建 Pipeline Tracker Meta Issue
pipeline status 查看当前流水线进度
pipeline update <#> 更新流水线步骤 --step <1-7> --status "状态描述"
pipeline update <#> 更新 Pipeline 字段 --field <阶段|当前迭代> --value <值>
test 验证所有 API 接口(P0 阻塞性 + P1 核心功能)
test 验证所有 API 接口
Token 管理:
--role <pm|designer|dev> 从 scripts/.gitea-tokens.json 自动读取对应角色的 Token
GITEA_TOKEN 环境变量 手动覆盖(优先级最高)
两者都不提供时报错
角色自动映射:
--role <角色名> 自动解析为 Token + Assignee
--assigned <角色名> 自动解析为 Gitea 用户名
--assignees <角色名,...> 同上
示例:
node scripts/gitea.js --role pm issue list --milestone v0.1-基础交互
node scripts/gitea.js --role designer issue comment 2 --body "汇报内容"
node scripts/gitea.js --role dev pipeline status
GITEA_TOKEN=xxx node scripts/gitea.js test
配置文件: gitea-config.json(与 gitea.js 同目录)
`);
}
@@ -677,60 +699,6 @@ Token 管理:
let globalArgs;
// ====== Token 文件读取 ======
const path = require('path');
/**
* 从 scripts/.gitea-tokens.json 读取指定角色的 Token
* @param {string} role - 角色名(pm / designer / dev)
* @returns {string|null} Token 或 null
*/
function loadTokenFromFile(role) {
const tokenFile = path.join(__dirname, '.gitea-tokens.json');
try {
const fs = require('fs');
const data = JSON.parse(fs.readFileSync(tokenFile, 'utf-8'));
return data[role] || null;
} catch {
return null;
}
}
/**
* 获取 Token(优先级:环境变量 > --role 文件 > 报错)
* @param {object} options - 解析后的 CLI options
* @returns {string} Token
*/
function resolveToken(options) {
// 优先级 1:环境变量
if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN;
// 优先级 2:--role 参数
const role = options.role;
if (role) {
const validRoles = ['pm', 'designer', 'dev', 'artist', 'reviewer'];
if (!validRoles.includes(role)) {
console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`);
process.exit(1);
}
const token = loadTokenFromFile(role);
if (!token) {
console.error(`❌ 未在 scripts/.gitea-tokens.json 中找到角色 "${role}" 的 Token`);
console.error(' 请创建 scripts/.gitea-tokens.json,格式:');
console.error(' { "pm": "token1", "designer": "token2", "dev": "token3" }');
process.exit(1);
}
return token;
}
// 都没有
console.error('❌ 缺少认证信息。请使用以下方式之一:');
console.error(' --role pm 从 scripts/.gitea-tokens.json 读取 Token');
console.error(' GITEA_TOKEN=xxx 通过环境变量传入');
process.exit(1);
}
async function main() {
const args = parseArgs(process.argv.slice(2));
globalArgs = args;
@@ -743,7 +711,13 @@ async function main() {
// Token 解析
const token = resolveToken(args.options);
const api = new GiteaAPI(token);
// 构建 API 实例(从 config 读取连接信息)
const api = new GiteaAPI(token, {
host: config.host,
port: config.port,
repoOwner: config.repoOwner,
repoName: config.repoName,
});
// 路由
const commandGroup = cmd[args.command];
@@ -753,7 +727,6 @@ async function main() {
process.exit(1);
}
// 特殊: test 是单级命令
if (args.command === 'test') {
await cmd.test.run(api, args);
return;
+233
View File
@@ -0,0 +1,233 @@
---
name: "gitea-pm"
description: "PM 专属项目管理技能 — 任务创建/关闭/里程碑管理/Pipeline Tracker、会话启动、角色汇报模板。"
---
# Gitea PM 技能
> PM Agent 的专属技能,封装所有项目管理操作。
> 所有配置(连接信息、Token、角色映射、标签定义)从 `gitea-config.json` 读取。
> 本文档不含项目特定信息。
---
## 1. CLI 工具
```bash
node .claude/skills/gitea-pm/gitea.js --role pm <command> [subcommand] [options]
```
**`--json` 标志**:所有命令支持 `--json` 输出纯 JSON,供脚本解析。
---
## 2. 认证与角色映射
- `--role <角色名>` 自动解析 Token 和 Assignee(从 `gitea-config.json` 的 `roles` 配置)
- `--assigned <角色名>` 自动解析为 Gitea 用户名
- `--assignees <角色名,...>` 同上
- 环境变量 `GITEA_TOKEN` 优先级最高
---
## 3. PM 会话启动
PM 每次启动时:
1. **读取当前阶段**:`pipeline status`
2. **列出激活里程碑**:`milestone list --state open`
3. **列出所有活跃任务**:`issue list --state open`,展示摘要
4. **展开需要关注的任务**:仅对 `status:in-progress`、`status:blocked`、`review:pass` 的任务执行 `issue get`
5. **按阶段调用技能**:无进度 → project-initializer / 草图 → stage-sketch / 总路线 → stage-roadmap / 迭代 → stage-iteration
---
## 4. 命令速查
### 🔒 PM-only 命令
#### 任务创建
```bash
# 创建任务(assignee 自动从 --role 映射;如需指定其他角色,用 --assignees)
node .claude/skills/gitea-pm/gitea.js --role pm issue create \
--title "D-001 概念设计" \
--body "任务正文" \
--milestone <id|标题> \
--labels <id,...>
# 创建任务并指派给策划(显式指定)
node .claude/skills/gitea-pm/gitea.js --role pm issue create \
--title "D-001 概念设计" \
--assignees designer \
--milestone <id|标题>
# 设置标签
node .claude/skills/gitea-pm/gitea.js --role pm issue labels <number> --set <id,id,...>
# 设置依赖
node .claude/skills/gitea-pm/gitea.js --role pm issue block <blocker_number> <blocked_number>
node .claude/skills/gitea-pm/gitea.js --role pm issue unblock <blocker_number> <blocked_number>
```
#### 任务关闭(用户确认后)
```bash
node .claude/skills/gitea-pm/gitea.js --role pm issue close <number> --body "关闭说明"
```
#### 任务重开
```bash
node .claude/skills/gitea-pm/gitea.js --role pm issue reopen <number>
```
#### 里程碑管理
```bash
node .claude/skills/gitea-pm/gitea.js --role pm milestone create \
--title "标题" --description "描述"
node .claude/skills/gitea-pm/gitea.js --role pm milestone update <id> \
--title "..." --state closed
```
#### Pipeline Tracker
```bash
# 初始化
node .claude/skills/gitea-pm/gitea.js --role pm pipeline init
# 更新阶段
node .claude/skills/gitea-pm/gitea.js --role pm pipeline update <number> --field 阶段 --value "迭代阶段"
# 更新当前迭代
node .claude/skills/gitea-pm/gitea.js --role pm pipeline update <number> --field 当前迭代 --value "v1.4"
```
#### 标签初始化
```bash
node .claude/skills/gitea-pm/gitea.js --role pm label init
node .claude/skills/gitea-pm/gitea.js --role pm label create --name "名称" --color "#RRGGBB"
```
### 通用命令(PM 也用)
```bash
# 查询
node .claude/skills/gitea-pm/gitea.js --role pm issue list --state open
node .claude/skills/gitea-pm/gitea.js --role pm issue get <number>
node .claude/skills/gitea-pm/gitea.js --role pm issue comments <number>
node .claude/skills/gitea-pm/gitea.js --role pm issue deps <number>
node .claude/skills/gitea-pm/gitea.js --role pm milestone list --state open
node .claude/skills/gitea-pm/gitea.js --role pm label list
node .claude/skills/gitea-pm/gitea.js --role pm pipeline status
# 测试连通
node .claude/skills/gitea-pm/gitea.js --role pm test
```
---
## 5. 任务 ID 规范
格式:`[角色前缀]-[三位数字]`
| 前缀 | 角色 | `--assignees` 值(角色名) |
|------|------|--------------------------|
| `D` | 策划 | `designer` |
| `P` | 程序 | `dev` |
| `A` | 美术 | `artist` |
每个前缀从 001 独立编号。
---
## 6. Pipeline Tracker
Pipeline Tracker 是一个 Meta Issue,仅记录**无法从 Issue/Milestone 推导**的信息:
- **当前阶段**:草图阶段 / 总路线阶段 / 迭代阶段
- **当前迭代**:版本号(迭代阶段时填写)
- **阶段历史**:阶段转换记录表
**不记录**:里程碑详情、Issues 计数、流水线步骤状态、任务索引(这些从查询获取)。
---
## 7. 角色汇报模板
各角色完成工作后在对应 Issue 下评论。
### 策划汇报
```
## 策划汇报 — YYYY-MM-DD
**任务ID**: D-XXX
**状态**: 进行中 / 已完成
**产出**: path/to/output
### 完成内容
- ...
### 待确认
- ...
```
### 程序汇报
```
## 程序汇报 — YYYY-MM-DD
**任务ID**: P-XXX
**状态**: 进行中 / 已完成
**产出**: path/to/output
### 完成内容
- ...
### 偏差/问题
- ...
```
### 审查修改后汇报
```
## 修改汇报 — YYYY-MM-DD
**任务ID**: <ID>
**修改原因**: 质量门审查退回
### 修改内容
- [对应审查意见第X条]:[具体修改内容]
### 修改后产出
- path/to/output
```
### PM 确认关闭任务
```
## 任务完成 ✔
**完成日期**:YYYY-MM-DD
**产出**:path/to/output
**审查记录**:docs/plans/reviews/<task-ID>-审查记录.md
```
---
## 8. 标签体系
从 `gitea-config.json` 的 `labels` 字段定义。`label init` 命令从此配置创建标签。
### 标签类别
| 类别 | 前缀 | 用途 |
|------|------|------|
| status | `status:` | 任务状态(backlog/ready/in-progress/blocked/in-review/done) |
| review | `review:` | 审查结果(pass/fail) |
| stage | `stage:` | 开发阶段(sketch/roadmap/iteration) |
| gate | `gate:` | 旧标签,保留用于历史 Issue |
+253
View File
@@ -0,0 +1,253 @@
/**
* Gitea API Client — 通用项目管理工具
*
* 封装所有 Gitea REST API 调用,处理 UTF-8 编码和错误重试。
* 供 gitea.js CLI 使用。连接信息从 gitea-config.json 读取。
*/
const http = require('http');
class GiteaAPI {
/**
* @param {string} token - Gitea API Token
* @param {object} config - 必须包含 host, port, repoOwner, repoName
*/
constructor(token, config = {}) {
if (!token) throw new Error('GITEA_TOKEN 未设置。请设置环境变量 GITEA_TOKEN。');
this.token = token;
this.config = {
apiBase: '/api/v1',
timeout: 15000,
...config,
};
// 从 config 构建 repoPath
if (!this.config.repoPath && this.config.repoOwner && this.config.repoName) {
this.config.repoPath = `/repos/${this.config.repoOwner}/${this.config.repoName}`;
}
}
// ====== 底层 HTTP 请求 ======
/**
* 发送 API 请求
* @param {string} method - GET / POST / PATCH / PUT / DELETE
* @param {string} path - API 路径(相对于 repo root),如 /issues、/milestones/2
* @param {object|null} data - 请求体(自动 JSON 序列化)
* @param {object} [params] - URL 查询参数
* @returns {Promise<any>} 解析后的 JSON 响应
*/
async request(method, path, data = null, params = {}) {
const basePath = this.config.apiBase + this.config.repoPath + path;
const url = new URL(`http://${this.config.host}:${this.config.port}${basePath}`);
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== null && v !== '') {
url.searchParams.set(k, String(v));
}
}
const body = data ? JSON.stringify(data) : null;
const headers = {
'Authorization': `token ${this.token}`,
'Accept': 'application/json',
};
if (body) {
headers['Content-Type'] = 'application/json; charset=utf-8';
headers['Content-Length'] = Buffer.byteLength(body, 'utf-8');
}
return new Promise((resolve, reject) => {
const options = {
hostname: url.hostname,
port: url.port || 80,
path: url.pathname + url.search,
method: method.toUpperCase(),
headers,
};
const req = http.request(options, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf-8');
if (res.statusCode >= 400) {
const err = new Error(`HTTP ${res.statusCode}: ${raw.substring(0, 300)}`);
err.statusCode = res.statusCode;
err.body = raw;
reject(err);
return;
}
if (!raw.trim()) { resolve(null); return; }
try { resolve(JSON.parse(raw)); }
catch { resolve(raw); }
});
});
req.on('error', reject);
req.setTimeout(this.config.timeout, () => {
req.destroy();
reject(new Error(`请求超时 (${this.config.timeout}ms): ${method} ${path}`));
});
if (body) req.write(body, 'utf-8');
req.end();
});
}
// ====== Issues ======
listIssues(params = {}) {
return this.request('GET', '/issues', null, { state: 'open', limit: 50, ...params });
}
getIssue(number) {
return this.request('GET', `/issues/${number}`);
}
createIssue(data) {
return this.request('POST', '/issues', data);
}
updateIssue(number, data) {
return this.request('PATCH', `/issues/${number}`, data);
}
closeIssue(number, comment) {
const p = this.updateIssue(number, { state: 'closed' });
if (comment) return p.then(() => this.createComment(number, comment));
return p;
}
reopenIssue(number) {
return this.updateIssue(number, { state: 'open' });
}
// ====== Comments ======
listComments(number) {
return this.request('GET', `/issues/${number}/comments`);
}
createComment(number, body) {
return this.request('POST', `/issues/${number}/comments`, { body });
}
// ====== Labels ======
listLabels(params = {}) {
return this.request('GET', '/labels', null, { page: 1, limit: 100, ...params });
}
createLabel(data) {
return this.request('POST', '/labels', data);
}
getLabel(id) {
return this.request('GET', `/labels/${id}`);
}
deleteLabel(id) {
return this.request('DELETE', `/labels/${id}`);
}
getIssueLabels(number) {
return this.request('GET', `/issues/${number}/labels`);
}
/** 添加标签到 Issue(不清除已有标签) */
addIssueLabels(number, labelIds) {
return this.request('POST', `/issues/${number}/labels`, { labels: labelIds });
}
/** 替换 Issue 的全部标签 */
replaceIssueLabels(number, labelIds) {
return this.request('PUT', `/issues/${number}/labels`, { labels: labelIds });
}
/** 移除 Issue 的单个标签 */
removeIssueLabel(number, labelId) {
return this.request('DELETE', `/issues/${number}/labels/${labelId}`);
}
// ====== Milestones ======
listMilestones(params = {}) {
return this.request('GET', '/milestones', null, { limit: 50, ...params });
}
getMilestone(id) {
return this.request('GET', `/milestones/${id}`);
}
createMilestone(data) {
return this.request('POST', '/milestones', data);
}
updateMilestone(id, data) {
return this.request('PATCH', `/milestones/${id}`, data);
}
closeMilestone(id) {
return this.updateMilestone(id, { state: 'closed' });
}
// ====== Dependencies ======
/** 获取阻塞当前 Issue 的前置任务列表 */
listDependencies(number) {
return this.request('GET', `/issues/${number}/dependencies`);
}
/** 获取当前 Issue 阻塞的任务列表 */
listBlocks(number) {
return this.request('GET', `/issues/${number}/blocks`);
}
/** 设置依赖:number 阻塞 blockedNumber */
addBlock(number, blockedNumber) {
return this.request('POST', `/issues/${number}/blocks`, {
index: blockedNumber,
owner: this.config.repoOwner,
repo: this.config.repoName,
});
}
/** 移除依赖 */
removeBlock(number, blockedNumber) {
return this.request('DELETE', `/issues/${number}/blocks`, {
index: blockedNumber,
owner: this.config.repoOwner,
repo: this.config.repoName,
});
}
// ====== 辅助方法 ======
/** 通过名称查找标签,返回 ID(不存在则返回 null) */
async findLabelByName(name) {
const labels = await this.listLabels();
return labels.find(l => l.name === name) || null;
}
/** 通过名称查找标签,不存在则创建,返回标签对象 */
async ensureLabel(name, color) {
const existing = await this.findLabelByName(name);
if (existing) return existing;
return this.createLabel({ name, color });
}
/** 通过标题查找 Milestone,返回 ID */
async findMilestoneByTitle(title) {
const milestones = await this.listMilestones();
return milestones.find(m => m.title === title) || null;
}
/** 通过标题查找 Issue(模糊匹配) */
async findIssueByTitle(title) {
const issues = await this.listIssues({ state: 'all' });
return issues.find(i => i.title.includes(title)) || null;
}
}
module.exports = { GiteaAPI };
+757
View File
@@ -0,0 +1,757 @@
#!/usr/bin/env node
/**
* Gitea CLI — 项目管理工具
*
* 用法: node gitea.js --role <角色> <command> [subcommand] [options]
* 所有配置(连接信息、Token、角色映射、标签定义)从 gitea-config.json 读取。
*/
const fs = require('fs');
const path = require('path');
const { GiteaAPI } = require('./gitea-api');
// ═══════════════════════════════════════════════════
// 加载配置
// ═══════════════════════════════════════════════════
const CONFIG_PATH = path.join(__dirname, 'gitea-config.json');
let config;
try {
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
} catch (e) {
console.error(`❌ 无法读取配置文件: ${CONFIG_PATH}`);
console.error(` ${e.message}`);
process.exit(1);
}
// ═══════════════════════════════════════════════════
// 命令行参数解析
// ═══════════════════════════════════════════════════
function parseArgs(argv) {
const positional = [];
const options = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--') { positional.push(...argv.slice(i + 1)); break; }
if (arg.startsWith('--')) {
const key = arg.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) {
options[key] = next;
i++;
} else {
options[key] = true;
}
} else {
positional.push(arg);
}
}
return { command: positional[0] || '', sub: positional[1] || '', rest: positional.slice(2), options };
}
// ═══════════════════════════════════════════════════
// 输出格式化
// ═══════════════════════════════════════════════════
function out(data) {
if (globalArgs.options.json) {
console.log(JSON.stringify(data, null, 2));
}
}
function outText(text) {
if (!globalArgs.options.json) console.log(text);
}
function ok(msg) { outText(`✅ ${msg}`); }
function fail(msg) { outText(`❌ ${msg}`); }
function info(msg) { outText(`ℹ️ ${msg}`); }
function truncate(s, max = 60) {
if (!s) return '';
const oneLine = s.replace(/\n/g, ' ').trim();
return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
}
function labelNames(issue) {
return (issue.labels || []).map(l => l.name);
}
/** 简易表格输出 */
function table(headers, rows) {
if (!rows.length) { outText(' (空)'); return; }
const widths = headers.map((h, i) =>
Math.max(h.length, ...rows.map(r => String(r[i] ?? '').length)))
;
const fmt = (cells) => cells.map((c, i) => String(c ?? '').padEnd(widths[i])).join(' ');
outText(' ' + fmt(headers));
outText(' ' + fmt(headers.map(() => '─'.repeat(2))));
for (const r of rows) outText(' ' + fmt(r));
}
// ═══════════════════════════════════════════════════
// 角色解析
// ═══════════════════════════════════════════════════
/**
* 获取 Token(优先级:环境变量 > config.roles[role].token)
*/
function resolveToken(options) {
if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN;
const role = options.role;
if (role) {
const validRoles = Object.keys(config.roles);
if (!validRoles.includes(role)) {
console.error(`❌ 无效角色: ${role},可选: ${validRoles.join(', ')}`);
process.exit(1);
}
const token = config.roles[role]?.token;
if (!token) {
console.error(`❌ 角色 "${role}" 未配置 token。请在 gitea-config.json 中设置。`);
process.exit(1);
}
return token;
}
console.error('❌ 缺少认证信息。请使用以下方式之一:');
console.error(' --role <角色名> 从 gitea-config.json 读取 Token');
console.error(' GITEA_TOKEN=xxx 通过环境变量传入');
process.exit(1);
}
/**
* 解析 assignee:支持角色名(自动映射)或直接用户名
*/
function resolveAssignee(value) {
if (!value) return null;
return config.roles[value]?.assignee || value;
}
/**
* 自动从 --role 解析 assignee(用于 issue create)
*/
function autoAssignee(role) {
if (!role) return [];
const assignee = config.roles[role]?.assignee;
return assignee ? [assignee] : [];
}
// ═══════════════════════════════════════════════════
// 命令实现
// ═══════════════════════════════════════════════════
const cmd = {};
// ──────── issue ────────
cmd.issue = {
async list(api, opts) {
const params = {};
if (opts.options.state) params.state = opts.options.state;
if (opts.options.milestone) {
const ms = isNaN(opts.options.milestone)
? await api.findMilestoneByTitle(opts.options.milestone)
: { id: Number(opts.options.milestone) };
if (ms) params.milestones = ms.id;
else { fail(`未找到里程碑: ${opts.options.milestone}`); return; }
}
if (opts.options.labels) params.labels = opts.options.labels;
if (opts.options.assigned) {
// 支持角色名或直接用户名
params.assigned_by = resolveAssignee(opts.options.assigned);
}
const issues = await api.listIssues(params);
out(issues);
if (!opts.options.json) {
outText(`\n📋 Issues (${issues.length || 0})\n`);
if (!issues || !issues.length) { outText(' (无)'); return; }
table(
['#', '标题', '状态', '里程碑', '标签'],
issues.map(i => [
i.number,
truncate(i.title, 40),
i.state,
i.milestone?.title || '—',
labelNames(i).join(', ') || '—',
])
);
}
},
async get(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue get <number>'); return; }
const issue = await api.getIssue(num);
out(issue);
if (!opts.options.json) {
outText(`\n📌 Issue #${issue.number}: ${issue.title}`);
outText(` 状态: ${issue.state}`);
outText(` 里程碑: ${issue.milestone?.title || '—'}`);
outText(` 指派: ${(issue.assignees || []).map(a => a.login).join(', ') || '—'}`);
outText(` 标签: ${labelNames(issue).join(', ') || '—'}`);
outText(` 创建: ${issue.created_at?.slice(0, 10)}`);
outText(` 更新: ${issue.updated_at?.slice(0, 10)}`);
if (issue.body) {
outText(`\n--- Body ---\n${issue.body.slice(0, 500)}${issue.body.length > 500 ? '…' : ''}`);
}
}
},
async create(api, opts) {
const title = opts.options.title;
const body = opts.options.body || '';
if (!title) { fail('用法: issue create --title "标题" [--body "内容"] [--milestone <id>] [--labels <id,...>]'); return; }
const data = { title, body };
if (opts.options.milestone) {
const ms = isNaN(opts.options.milestone)
? await api.findMilestoneByTitle(opts.options.milestone)
: { id: Number(opts.options.milestone) };
if (ms) data.milestone = ms.id;
}
if (opts.options.labels) {
data.labels = opts.options.labels.split(',').map(Number);
}
// assignee: 显式指定 > 自动从 --role 映射
if (opts.options.assignees) {
data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a));
} else {
data.assignees = autoAssignee(opts.options.role);
}
const issue = await api.createIssue(data);
out(issue);
if (!opts.options.json) {
ok(`Issue #${issue.number} 已创建: ${issue.title}`);
outText(` ${issue.html_url}`);
}
},
async update(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue update <number> [--title "…"] [--body "…"] [--state open|closed]'); return; }
const data = {};
if (opts.options.title !== undefined) data.title = opts.options.title;
if (opts.options.body !== undefined) data.body = opts.options.body;
if (opts.options.state) data.state = opts.options.state;
if (opts.options.milestone) {
const ms = isNaN(opts.options.milestone)
? await api.findMilestoneByTitle(opts.options.milestone)
: { id: Number(opts.options.milestone) };
if (ms) data.milestone = ms.id;
}
if (opts.options.assignees) {
data.assignees = opts.options.assignees.split(',').map(a => resolveAssignee(a));
}
const issue = await api.updateIssue(num, data);
out(issue);
if (!opts.options.json) ok(`Issue #${num} 已更新`);
},
async close(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue close <number>'); return; }
const comment = opts.options.body || opts.options.comment;
await api.closeIssue(num, comment);
out({ closed: true, number: Number(num) });
if (!opts.options.json) ok(`Issue #${num} 已关闭`);
},
async reopen(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue reopen <number>'); return; }
await api.reopenIssue(num);
out({ reopened: true, number: Number(num) });
if (!opts.options.json) ok(`Issue #${num} 已重新打开`);
},
async comment(api, opts) {
const num = opts.rest[0];
const body = opts.options.body;
if (!num || !body) { fail('用法: issue comment <number> --body "内容"'); return; }
const c = await api.createComment(num, body);
out(c);
if (!opts.options.json) ok(`评论已添加到 Issue #${num}`);
},
async comments(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue comments <number>'); return; }
const comments = await api.listComments(num);
out(comments);
if (!opts.options.json) {
outText(`\n💬 Issue #${num} 的评论 (${comments.length})\n`);
if (!comments.length) { outText(' (无评论)'); return; }
for (const c of comments) {
outText(` @${c.user?.login || '?'} (${c.created_at?.slice(0, 10)}):`);
outText(` ${truncate(c.body, 80)}`);
}
}
},
async labels(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue labels <number> [--add <id,...>] [--set <id,...>]'); return; }
if (opts.options.add) {
const ids = opts.options.add.split(',').map(Number);
await api.addIssueLabels(num, ids);
if (!opts.options.json) ok(`标签已添加到 Issue #${num}: ${ids.join(', ')}`);
} else if (opts.options.set) {
const ids = opts.options.set.split(',').map(Number);
await api.replaceIssueLabels(num, ids);
if (!opts.options.json) ok(`Issue #${num} 的标签已替换为: ${ids.join(', ')}`);
} else {
const labels = await api.getIssueLabels(num);
out(labels);
if (!opts.options.json) {
outText(`\n🏷️ Issue #${num} 的标签 (${labels.length})`);
for (const l of labels) {
outText(` ${l.name} (${l.id}) #${l.color}`);
}
}
}
},
async deps(api, opts) {
const num = opts.rest[0];
if (!num) { fail('用法: issue deps <number>'); return; }
const [blockedBy, blocking] = await Promise.all([
api.listDependencies(num).catch(() => []),
api.listBlocks(num).catch(() => []),
]);
out({ issue: Number(num), blockedBy, blocking });
if (!opts.options.json) {
outText(`\n🔗 Issue #${num} 的依赖关系`);
outText(` 被阻塞于 (${blockedBy.length}): ${blockedBy.map(i => '#' + i.number).join(', ') || '无'}`);
outText(` 阻塞了 (${blocking.length}): ${blocking.map(i => '#' + i.number).join(', ') || '无'}`);
}
},
async block(api, opts) {
const [num, blocked] = opts.rest;
if (!num || !blocked) { fail('用法: issue block <number> <blocked_number>'); return; }
await api.addBlock(Number(num), Number(blocked));
out({ blocker: Number(num), blocked: Number(blocked) });
if (!opts.options.json) ok(`Issue #${blocked} 现在依赖于 Issue #${num}`);
},
async unblock(api, opts) {
const [num, blocked] = opts.rest;
if (!num || !blocked) { fail('用法: issue unblock <number> <blocked_number>'); return; }
await api.removeBlock(Number(num), Number(blocked));
out({ blocker: Number(num), unblocked: Number(blocked) });
if (!opts.options.json) ok(`Issue #${blocked} 不再依赖于 Issue #${num}`);
},
};
// ──────── milestone ────────
cmd.milestone = {
async list(api, opts) {
const milestones = await api.listMilestones({ state: opts.options.state || 'all' });
out(milestones);
if (!opts.options.json) {
outText(`\n🏁 里程碑 (${milestones.length})\n`);
if (!milestones.length) { outText(' (无)'); return; }
table(
['ID', '标题', '状态', 'Open', 'Closed', '截止日期'],
milestones.map(m => [
m.id,
m.title,
m.state,
m.open_issues,
m.closed_issues,
m.due_on?.slice(0, 10) || '—',
])
);
}
},
async create(api, opts) {
const title = opts.options.title;
if (!title) { fail('用法: milestone create --title "标题" [--description "描述"] [--due YYYY-MM-DD]'); return; }
const data = { title, state: 'open' };
if (opts.options.description) data.description = opts.options.description;
if (opts.options.due) data.due_on = opts.options.due + 'T00:00:00Z';
const ms = await api.createMilestone(data);
out(ms);
if (!opts.options.json) ok(`里程碑已创建: ${ms.title} (ID: ${ms.id})`);
},
async update(api, opts) {
const id = opts.rest[0];
if (!id) { fail('用法: milestone update <id> [--title "…"] [--description "…"] [--state open|closed]'); return; }
const data = {};
if (opts.options.title) data.title = opts.options.title;
if (opts.options.description !== undefined) data.description = opts.options.description;
if (opts.options.state) data.state = opts.options.state;
if (opts.options.due) data.due_on = opts.options.due + 'T00:00:00Z';
const ms = await api.updateMilestone(id, data);
out(ms);
if (!opts.options.json) ok(`里程碑 ${id} 已更新`);
},
};
// ──────── label ────────
cmd.label = {
async list(api, opts) {
const labels = await api.listLabels();
out(labels);
if (!opts.options.json) {
outText(`\n🏷️ 标签 (${labels.length})\n`);
if (!labels.length) { outText(' (无)'); return; }
table(
['ID', '名称', '颜色'],
labels.map(l => [l.id, l.name, '#' + l.color])
);
}
},
async create(api, opts) {
const name = opts.options.name;
const color = (opts.options.color || '#ededed').replace('#', '');
if (!name) { fail('用法: label create --name "名称" [--color "#RRGGBB"]'); return; }
const label = await api.createLabel({ name, color });
out(label);
if (!opts.options.json) ok(`标签已创建: ${name} (ID: ${label.id})`);
},
async init(api, opts) {
outText('\n🏷️ 初始化标准标签体系…\n');
// 从 config.labels 合并所有标签定义
const allLabels = Object.values(config.labels).flat();
const existing = await api.listLabels();
const existingNames = new Set(existing.map(l => l.name));
let created = 0, skipped = 0;
for (const def of allLabels) {
if (existingNames.has(def.name)) {
info(`跳过 (已存在): ${def.name}`);
skipped++;
} else {
await api.createLabel({ name: def.name, color: def.color.replace('#', ''), description: def.description });
ok(`已创建: ${def.name}`);
created++;
}
}
outText(`\n完成: 新建 ${created} 个, 跳过 ${skipped} 个`);
},
};
// ──────── pipeline ────────
cmd.pipeline = {
async init(api, opts) {
const title = opts.options.title || 'Pipeline Tracker';
const milestone = opts.options.milestone;
const body = `# 流水线进度追踪
## 当前阶段
草图阶段
## 当前迭代
(非迭代阶段,留空)
## 阶段历史
| 阶段 | 里程碑 | 进入日期 | 完成日期 |
|------|--------|----------|----------|
| 草图阶段 | 草图阶段 | ${new Date().toISOString().slice(0, 10)} | — |`;
const data = { title, body };
if (milestone) {
const ms = isNaN(milestone)
? await api.findMilestoneByTitle(milestone)
: { id: Number(milestone) };
if (ms) data.milestone = ms.id;
}
const issue = await api.createIssue(data);
out(issue);
if (!opts.options.json) {
ok(`Pipeline Tracker 已创建: Issue #${issue.number}`);
outText(` ${issue.html_url}`);
}
},
async status(api, opts) {
let tracker = await api.findIssueByTitle('Pipeline Tracker');
if (!tracker) { fail('未找到 Pipeline Tracker Issue。请先运行: pipeline init'); return; }
tracker = await api.getIssue(tracker.number);
out({ number: tracker.number, title: tracker.title, body: tracker.body });
if (!opts.options.json) {
outText(`\n📊 Pipeline Tracker #${tracker.number}\n`);
outText(tracker.body);
}
},
async update(api, opts) {
const num = opts.rest[0];
const field = opts.options.field;
const value = opts.options.value;
if (!num || !field || !value) {
fail('用法: pipeline update <issue-number> --field <阶段|当前迭代> --value <值>');
return;
}
const issue = await api.getIssue(num);
let body = issue.body || '';
if (field === '阶段') {
body = body.replace(/## 当前阶段\n.*/, `## 当前阶段\n${value}`);
} else if (field === '当前迭代') {
body = body.replace(/## 当前迭代\n.*/, `## 当前迭代\n${value}`);
} else {
fail(`无效字段: ${field},可选: 阶段, 当前迭代`);
return;
}
await api.updateIssue(num, { body });
out({ number: Number(num), field, value, updated: true });
if (!opts.options.json) ok(`Pipeline Tracker 字段 "${field}" 已更新为: ${value}`);
},
};
// ──────── test ────────
cmd.test = {
async run(api, opts) {
outText('\n🧪 Gitea API 接口验证\n');
const results = [];
let passed = 0, failed = 0;
async function test(name, fn) {
try {
const result = await fn();
ok(name);
results.push({ name, status: 'pass', detail: result });
passed++;
} catch (e) {
fail(`${name} — ${e.message}`);
results.push({ name, status: 'fail', error: e.message });
failed++;
}
}
// P0: 阻塞性验证
outText('── P0: 阻塞性验证 ──\n');
await test('GET /milestones', () => api.listMilestones());
await test('GET /issues', () => api.listIssues({ limit: 5 }));
await test('GET /labels', () => api.listLabels());
// 查找第一个存在的 Issue 用于后续测试
const issues = await api.listIssues({ limit: 5, state: 'all' });
const testIssue = issues.find(i => i.state === 'open') || issues[0];
if (testIssue) {
await test(`GET /issues/${testIssue.number}`, () => api.getIssue(testIssue.number));
await test(`GET /issues/${testIssue.number}/comments`, () => api.listComments(testIssue.number));
await test(`GET /issues/${testIssue.number}/dependencies`, () => api.listDependencies(testIssue.number));
await test(`GET /issues/${testIssue.number}/blocks`, () => api.listBlocks(testIssue.number));
}
await test('POST 创建含中文 Issue', async () => {
const i = await api.createIssue({
title: '测试 Issue — UTF-8 验证 🎮',
body: '这是中文内容测试。\n\n- 列表项\n- 特殊字符:✅❌🔴⬜\n\n创建时间: ' + new Date().toISOString(),
});
await api.closeIssue(i.number);
return `#${i.number}: ${i.title}`;
});
if (testIssue) {
await test('POST 创建含中文评论', async () => {
const c = await api.createComment(testIssue.number, '🧪 API 评论测试 — 中文编码验证 ' + new Date().toISOString().slice(0, 19));
return `comment_id: ${c.id}`;
});
await test('PATCH 更新 Issue', async () => {
const orig = await api.getIssue(testIssue.number);
const newTitle = orig.title + ' [测试更新]';
await api.updateIssue(testIssue.number, { title: newTitle });
await api.updateIssue(testIssue.number, { title: orig.title });
return `Issue #${testIssue.number} 标题更新并还原`;
});
}
await test('PATCH 关闭/重开 Issue', async () => {
const tmp = await api.createIssue({ title: '临时测试 — 开关', body: '将被关闭再打开' });
await api.closeIssue(tmp.number);
await api.reopenIssue(tmp.number);
await api.closeIssue(tmp.number);
return `#${tmp.number} 关闭→重开→关闭`;
});
await test('POST 创建 Label', async () => {
const l = await api.createLabel({ name: '_test-label', color: 'ededed' });
try { await api.deleteLabel(l.id); } catch { /* ignore */ }
return `label: ${l.name} (ID: ${l.id}) 已创建并清理`;
});
if (testIssue) {
await test('POST Issue 添加标签', async () => {
const labels = await api.listLabels();
if (!labels.length) return '无可用标签,跳过';
await api.addIssueLabels(testIssue.number, [labels[0].id]);
return `添加标签 ${labels[0].name} 到 Issue #${testIssue.number}`;
});
}
// P1: 核心功能
outText('\n── P1: 核心功能验证 ──\n');
if (testIssue && issues.length >= 2) {
const other = issues.find(i => i.number !== testIssue.number);
if (other) {
await test('POST/DELETE Issue 依赖', async () => {
await api.addBlock(testIssue.number, other.number);
await api.removeBlock(testIssue.number, other.number);
return `依赖关系 ${testIssue.number}→${other.number} 添加→移除`;
});
}
}
await test('POST/PATCH Milestone', async () => {
const ms = await api.createMilestone({ title: '_test-milestone', state: 'open' });
await api.updateMilestone(ms.id, { description: '测试用' });
await api.updateMilestone(ms.id, { state: 'closed' });
return `milestone ${ms.id} 创建→更新→关闭`;
});
// 汇总
outText(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━`);
outText(`总计: ${passed + failed} 项, ✅ ${passed} 通过, ❌ ${failed} 失败`);
out(results);
if (failed > 0) process.exit(1);
},
};
// ═══════════════════════════════════════════════════
// 帮助文本
// ═══════════════════════════════════════════════════
function showHelp() {
const roles = Object.keys(config.roles).join('|');
console.log(`
Gitea CLI — 项目管理工具
用法: node gitea.js --role <${roles}> <command> [subcommand] [options]
全局选项:
--help 显示帮助信息
--json 以 JSON 格式输出
--role <角色> 使用对应角色的身份(自动解析 Token 和 Assignee)
命令列表:
issue list 列出 Issues
[--state open|closed|all] [--milestone <id|标题>]
[--labels <id,...>] [--assigned <角色名|用户名>]
issue get <#> 查看 Issue 详情
issue create 创建 Issue(assignee 自动从 --role 映射)
--title "标题" [--body "内容"] [--milestone <id>]
[--labels <id,...>] [--assignees <角色名,...>]
issue update <#> 更新 Issue [--title "…"] [--body "…"] [--state open|closed]
issue close <#> 关闭 Issue [--body "完成评论"]
issue reopen <#> 重新打开 Issue
issue comment <#> 添加评论 --body "内容"
issue comments <#> 查看评论列表
issue labels <#> 查看/管理标签 [--add <id,...>] [--set <id,...>]
issue deps <#> 查看依赖关系
issue block <#> <#> 设置依赖关系
issue unblock <#> <#> 移除依赖关系
milestone list 列出里程碑 [--state open|closed|all]
milestone create 创建里程碑 --title "标题" [--description "…"] [--due YYYY-MM-DD]
milestone update <id> 更新里程碑 [--title "…"] [--state open|closed]
label list 列出标签
label create 创建标签 --name "名称" [--color "#RRGGBB"]
label init 初始化标准标签体系(从 gitea-config.json 读取定义)
pipeline init 创建 Pipeline Tracker Meta Issue
pipeline status 查看当前流水线进度
pipeline update <#> 更新 Pipeline 字段 --field <阶段|当前迭代> --value <值>
test 验证所有 API 接口
角色自动映射:
--role <角色名> 自动解析为 Token + Assignee
--assigned <角色名> 自动解析为 Gitea 用户名
--assignees <角色名,...> 同上
配置文件: gitea-config.json(与 gitea.js 同目录)
`);
}
// ═══════════════════════════════════════════════════
// 路由与入口
// ═══════════════════════════════════════════════════
let globalArgs;
async function main() {
const args = parseArgs(process.argv.slice(2));
globalArgs = args;
if (args.options.help || (!args.command && !args.sub)) {
showHelp();
return;
}
// Token 解析
const token = resolveToken(args.options);
// 构建 API 实例(从 config 读取连接信息)
const api = new GiteaAPI(token, {
host: config.host,
port: config.port,
repoOwner: config.repoOwner,
repoName: config.repoName,
});
// 路由
const commandGroup = cmd[args.command];
if (!commandGroup) {
console.error(`❌ 未知命令: ${args.command}`);
console.error(' 运行 --help 查看可用命令');
process.exit(1);
}
if (args.command === 'test') {
await cmd.test.run(api, args);
return;
}
const handler = commandGroup[args.sub];
if (!handler) {
console.error(`❌ 未知子命令: ${args.command} ${args.sub}`);
console.error(` 运行 --help 查看 ${args.command} 的可用子命令`);
process.exit(1);
}
try {
await handler(api, args);
} catch (e) {
if (args.options.json) {
console.error(JSON.stringify({ error: e.message, statusCode: e.statusCode }));
} else {
console.error(`\n❌ 错误: ${e.message}`);
}
process.exit(1);
}
}
main().catch(e => {
console.error('致命错误:', e);
process.exit(1);
});
+23 -66
View File
@@ -1,6 +1,6 @@
---
name: "project-initializer"
description: "项目初始化技能 — 检查版本管理/项目管理配置、初始化 Gitea 标签/里程碑/任务。仅在项目无进度时由 PM 调用。"
description: "项目初始化技能 — 检查版本管理/项目管理配置、初始化 Gitea 标签/里程碑。仅在项目无进度时由 PM 调用。"
---
# 项目初始化
@@ -17,24 +17,27 @@ description: "项目初始化技能 — 检查版本管理/项目管理配置、
逐项确认以下内容:
1. **SSH 仓库地址**:确认 `git remote -v` 中的仓库地址正确
2. **角色 Gitea 账号**:确认以下账号存在且可访问
- `starmen1265`(用户)
2. **角色 Gitea 账号**:确认以下 AI 角色账号存在且可访问
- `ai-pm`
- `ai-designer`
- `ai-programmer`
- `ai-artist`
- `ai-reviewer`
3. **SSH key 配置**:运行 `ssh -T git@micecola.top -p 2223` 测试连接
如不确定账号是否存在,通过 Token 文件或 API 验证。
3. **SSH key 配置**:测试 SSH 连接(命令参数中的主机和端口需向用户确认)
- 失败 → 引导用户生成并配置 SSH key
### 步骤 2:项目管理配置
确认以下配置就绪:
1. **Token 文件**:`scripts/.gitea-tokens.json` 存在且包含所有角色 token
- `pm`、`designer`、`dev`、`artist`、`reviewer`
2. **Token 文件已忽略**:`.gitignore` 中包含 `scripts/.gitea-tokens.json`
3. **Gitea CLI 可用**:`node scripts/gitea.js --role pm test` 运行通过
1. **配置文件**:`.claude/skills/gitea-pm/gitea-config.json` 存在且包含:
- `host`、`port`、`repoOwner`、`repoName`
- `roles` 中所有角色的 `assignee` 和 `token`
2. **配置文件已忽略**:`.gitignore` 中包含 `gitea-config.json`
3. **Gitea CLI 可用**:`node .claude/skills/gitea-pm/gitea.js --role pm test` 运行通过
缺失时引导用户配置。
@@ -50,74 +53,27 @@ description: "项目初始化技能 — 检查版本管理/项目管理配置、
- 将迁移方案提交用户确认后执行
**情况 B:无现有代码(全新项目)**
- 建议用户手动准备开发环境:
1. 安装 UE 5.7.4
2. 创建第三人称/第一人称/顶视角模板项目
3. 添加插件:BlockoutStarterPack、LowpolyCharacter
4. 启用 PythonEditorPlugins 插件
- 等待用户确认环境就绪
- 建议用户准备开发环境:
1. 安装对应版本的 UE
2. 创建模板项目
3. 添加所需插件
- **不等待就绪**,继续往下执行。只要在迭代阶段开始前环境准备就绪即可。
### 步骤 4:Gitea 初始化
依次执行(所有操作需用户确认后再执行):
```bash
# 1. 创建标准标签
node scripts/gitea.js --role pm label init
# 1. 创建标准标签(从 gitea-config.json 读取定义)
node .claude/skills/gitea-pm/gitea.js --role pm label init
# 2. 创建 Pipeline Tracker Issue
node scripts/gitea.js --role pm issue create \
--title "Pipeline Tracker" \
--body "# 流水线进度追踪
## 当前阶段
草图阶段
## 当前里程碑
- 名称: 草图阶段
- 状态: open
- Open Issues: 1
- Closed Issues: 0
## 阶段历史
| 阶段 | 里程碑 | 进入日期 | 完成日期 |
|------|--------|----------|----------|
| 草图阶段 | 草图阶段 | $(date +%Y-%m-%d) | — |" \
--labels "stage:sketch,role:pm,status:in-progress"
node .claude/skills/gitea-pm/gitea.js --role pm pipeline init
# 3. 创建草图阶段里程碑
node scripts/gitea.js --role pm milestone create \
node .claude/skills/gitea-pm/gitea.js --role pm milestone create \
--title "草图阶段" \
--description "概念设计与技术可行性验证"
# 4. 创建第一个任务:概念设计
node scripts/gitea.js --role pm issue create \
--title "D-001 概念设计" \
--body "## 任务信息
- **任务ID**: D-001
- **所属阶段**: 草图阶段
- **角色**: 策划
## 预期产出
产出概念草案 \`docs/design/概念草案.md\`,包含以下 7 个必填字段:
1. 一句话概述(这是什么游戏)
2. 核心玩法(玩家做什么)
3. 美术风格
4. 目标平台
5. 灵感来源
6. 核心假设(为什么好玩)
7. 范围约束(MVP 包含什么)
## 质量标准
- 每个字段有明确的、不含糊的描述
- 美术风格有参考图或参考作品
- 核心假设可验证(能通过原型验证)" \
--milestone "草图阶段" \
--labels "stage:sketch,role:design,status:ready" \
--assignees "ai-designer"
```
### 步骤 5:确认完成
@@ -125,8 +81,8 @@ node scripts/gitea.js --role pm issue create \
初始化完成后向用户报告:
- ✅ 版本管理配置状态
- ✅ 项目管理配置状态
- ✅ Gitea 初始化结果(标签数、Pipeline Tracker Issue 编号、里程碑、首个任务编号)
- 📋 建议下一步:启动策划角色开始概念设计
- ✅ Gitea 初始化结果(标签数、Pipeline Tracker Issue 编号、里程碑)
- 📋 建议下一步:进入草图阶段,由 stage-sketch 技能接管
---
@@ -135,3 +91,4 @@ node scripts/gitea.js --role pm issue create \
- 每一步都需要用户确认后才执行,不自动推进
- 如果中途失败(如 Gitea 连接失败),报告具体错误并建议修复方案
- 初始化完成后,PM 进入草图阶段,后续由 stage-sketch 技能接管
- **不创建首个任务** — 任务创建由 PM 根据阶段技能的逻辑来执行
+1 -1
View File
@@ -7,7 +7,7 @@ description: "审查标准技能 — 4 道质量门标准、阶段对应标准
> 本技能定义 4 道质量门的审查标准、阶段对应标准和审查后强制操作。
> 供 **game-reviewer 子代理**使用——角色完成工作后启动 reviewer 子代理,reviewer 按此标准审查。
> 项目管理工具操作细节参见 `.claude/skills/gitea-collab.md`。
> 项目管理工具操作细节参见 `gitea-collab` 技能。
---
+2 -2
View File
@@ -285,10 +285,10 @@ docs/technical/<系统名>-tech.md"
```bash
# 1. 关闭当前迭代里程碑
node scripts/gitea.js --role pm milestone update <当前里程碑ID> --state closed
node .claude/skills/gitea-pm/gitea.js --role pm milestone update <当前里程碑ID> --state closed
# 2. 创建下一个迭代里程碑
node scripts/gitea.js --role pm milestone create \
node .claude/skills/gitea-pm/gitea.js --role pm milestone create \
--title "迭代-vX.X" \
--description "第 N 个迭代版本:<简要描述>"
+2 -2
View File
@@ -164,10 +164,10 @@ description: "总路线阶段技能 — 全面设计文档产出与版本路径
```bash
# 1. 关闭总路线阶段里程碑
node scripts/gitea.js --role pm milestone update <总路线里程碑ID> --state closed
node .claude/skills/gitea-pm/gitea.js --role pm milestone update <总路线里程碑ID> --state closed
# 2. 创建第一个迭代版本里程碑
node scripts/gitea.js --role pm milestone create \
node .claude/skills/gitea-pm/gitea.js --role pm milestone create \
--title "迭代-v0.1" \
--description "第一个迭代版本"
+4 -4
View File
@@ -32,7 +32,7 @@ description: "草图阶段技能 — 概念设计与技术可行性验证。管
**任务 1:概念设计** (D-XXX)
```bash
node scripts/gitea.js --role pm issue create \
node .claude/skills/gitea-pm/gitea.js --role pm issue create \
--title "D-XXX 概念设计" \
--body "## 任务信息
- **任务ID**: D-XXX
@@ -59,7 +59,7 @@ node scripts/gitea.js --role pm issue create \
**任务 2:技术可行性报告** (P-XXX)
```bash
node scripts/gitea.js --role pm issue create \
node .claude/skills/gitea-pm/gitea.js --role pm issue create \
--title "P-XXX 技术可行性评估" \
--body "## 任务信息
- **任务ID**: P-XXX
@@ -105,10 +105,10 @@ node scripts/gitea.js --role pm issue create \
```bash
# 1. 关闭草图阶段里程碑
node scripts/gitea.js --role pm milestone update <草图里程碑ID> --state closed
node .claude/skills/gitea-pm/gitea.js --role pm milestone update <草图里程碑ID> --state closed
# 2. 创建总路线阶段里程碑
node scripts/gitea.js --role pm milestone create \
node .claude/skills/gitea-pm/gitea.js --role pm milestone create \
--title "总路线阶段" \
--description "全面设计文档产出:世界观、剧情、角色、系统、关卡、UI、音频、版本路径"
+3 -1
View File
@@ -98,9 +98,11 @@ Desktop.ini
.worktrees/
# ==============================
# Gitea API Tokens(每台机器独立配置)
# Gitea 配置(含 Token,每台机器独立配置)
# ==============================
scripts/.gitea-tokens.json
.claude/skills/gitea-pm/gitea-config.json
.claude/skills/gitea-collab/gitea-config.json
# ==============================
# Python
+3 -2
View File
@@ -16,9 +16,10 @@
## 项目管理
- **工具**: Gitea(Issues + Milestones)
- **协作技能**: `gitea-collab`(封装所有项目管理操作的实现细节)
- **PM 技能**: `gitea-pm`(任务创建/关闭、里程碑管理、Pipeline Tracker)
- **通用技能**: `gitea-collab`(任务查询、进度汇报、审查操作)
- **进度查询**: Pipeline Tracker(Meta Issue,仅记录当前阶段和阶段历史)
- **角色操作**: 各角色通过 `gitea-collab` 技能与 Gitea 交互,不在角色文档中硬编码具体命令
- **配置文件**: `.claude/skills/gitea-pm/gitea-config.json`(含连接信息、Token、角色映射、标签定义)
---