diff --git a/.claude/agent-memory/game-project-pm/MEMORY.md b/.claude/agent-memory/game-project-pm/MEMORY.md index 1677fdd..dbfe2d5 100644 --- a/.claude/agent-memory/game-project-pm/MEMORY.md +++ b/.claude/agent-memory/game-project-pm/MEMORY.md @@ -1,7 +1,8 @@ # PM Agent Memory Index -- [流水线阶段 2026-06-05 P002](pipeline-status-20260605-p002.md) — P-002 质量门不通过,三个系统均需修正执行案 -- [流水线阶段 2026-06-05](pipeline-status-20260605.md) — P-001 完成,v0.1 处于③执行案待确认(历史状态) -- [流水线阶段判断 2026-06-03](pipeline-status-20260603.md) — v0.1 处于阶段④执行案完善中,质量门未通过(历史状态) +- [进度管理迁移 Gitea](gitea-migration.md) — 2026-06-05 进度管理从本地文件迁移到 Gitea Issue + Milestone,废弃 current.md 和 reports/ +- [流水线阶段 2026-06-05 P002](pipeline-status-20260605-p002.md) — P-002 质量门不通过,三个系统均需修正执行案(当前阻塞点) +- ~~[流水线阶段 2026-06-05](pipeline-status-20260605.md)~~ — 已过时,进度已迁移到 Gitea +- ~~[流水线阶段判断 2026-06-03](pipeline-status-20260603.md)~~ — 已过时,进度已迁移到 Gitea - [Roadmap 对齐问题](roadmap-alignment.md) — roadmap.md 缺少 v0.5-v0.7,需与 game-vision.md 对齐 - [设计文档质量审查](design-doc-review.md) — 交互系统技术越界、收集系统缺v0.1执行细节、边界情况不足 diff --git a/.claude/agent-memory/game-project-pm/gitea-migration.md b/.claude/agent-memory/game-project-pm/gitea-migration.md new file mode 100644 index 0000000..1e441ee --- /dev/null +++ b/.claude/agent-memory/game-project-pm/gitea-migration.md @@ -0,0 +1,29 @@ +--- +name: gitea-migration +description: 进度管理已迁移到 Gitea Issue + Milestone,废弃 current.md 和 reports/ +metadata: + type: project +--- + +进度管理迁移到 Gitea(2026-06-05) + +**事实**: 进度管理唯一真实来源从本地 Markdown 文件迁移到 Gitea Issue + Milestone。 + +**废弃的文件**: +- `docs/plans/current.md` — 不再维护 +- `docs/plans/reports/*.md` — 不再维护 +- `docs/plans/archive/` — 不再新增(closed Issues = 归档) + +**保留的本地文件**: +- `docs/plans/reviews/` — 审查记录(结构化文档) +- `docs/plans/prototype-roadmap.md` — 阶段路线图 +- `docs/plans/roadmap.md` — 全局路线图 +- `docs/plans/work-log.md` — 工作日志 + +**核心工具**: `scripts/gitea.js` CLI(Node.js),通过 `GITEA_TOKEN` 环境变量认证 + +**映射**: 任务→Issue, 版本→Milestone, 状态→Label, 汇报→Issue 评论, 归档→close Issue, 流水线→Pipeline Tracker Meta Issue + +**Why**: 用户已在 Gitea 搭建 Issue/Milestone/看板基础设施,希望获得可视化看板和 Web 面板查看体验。 + +**How to apply**: 所有进度追踪操作必须通过 `scripts/gitea.js` CLI。不读 current.md,不写 reports/。 diff --git a/.gitignore b/.gitignore index 1138287..9bcaa56 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,11 @@ Desktop.ini .claude/worktrees/ .worktrees/ +# ============================== +# Gitea API Tokens(每台机器独立配置) +# ============================== +scripts/.gitea-tokens.json + # ============================== # 其他 # ============================== diff --git a/docs/technical/gitea-api-guide.md b/docs/technical/gitea-api-guide.md new file mode 100644 index 0000000..b5361c7 --- /dev/null +++ b/docs/technical/gitea-api-guide.md @@ -0,0 +1,291 @@ +# Gitea API 访问指南 + +> 最后更新:2026-06-05 +> 适用项目:ChildHood (starmen1265/ChildHood) + +--- + +## 基本信息 + +| 项目 | 值 | +|------|-----| +| Gitea 版本 | 1.23.4 | +| Web 面板 | `http://micecola.top:8418` | +| SSH 端口 | 2223 | +| SSH 用户 | `sc-gitea` | +| API 基地址 | `http://micecola.top:8418/api/v1` | +| 仓库 API 路径 | `/repos/starmen1265/ChildHood` | + +## Agent 账号 + +| 账号 | 用途 | 认证方式 | +|------|------|----------| +| `starmen1265` | 主开发者(用户) | 密码 + SSH | +| `ai-pm` | PM Agent | SSH Key + API Token | +| `ai-designer` | 策划 Agent | 待配置 | +| `ai-artist` | 美术 Agent | 待配置 | + +--- + +## SSH 配置 + +PM Agent 的 SSH 密钥已配置在 `~/.ssh/id_ed25519_childhood_pm`。 + +现有 SSH Config(`~/.ssh/config`): + +``` +Host gitea micecola.top + HostName micecola.top + Port 2223 + User sc-gitea + IdentityFile ~/.ssh/id_ed25519_gitea + IdentitiesOnly yes +``` + +> **注意**:当前 SSH Config 指向 `sc-gitea` 用户的密钥。如需用 `ai-pm` 身份操作,Git 命令需指定密钥: +> ``` +> GIT_SSH_COMMAND="ssh -i ~/.ssh/id_ed25519_childhood_pm -p 2223" git push +> ``` + +--- + +## API 访问方式 + +### ⚠️ 重要:不要用 curl 发送中文 + +Windows 环境下 curl 发送中文 JSON 会出现编码错误(GBK/UTF-8 冲突),导致内容乱码。 + +**正确做法:使用 Python urllib** + +```python +import urllib.request +import json + +API_BASE = "http://micecola.top:8418/api/v1" +REPO_PATH = "/repos/starmen1265/ChildHood" +TOKEN = "" + +def gitea_api(method, path, data=None): + """发送 Gitea API 请求 + + Args: + method: GET / POST / PATCH / DELETE + path: API 路径(如 /issues/1) + data: 请求体(dict),None 则无 body + Returns: + API 响应(dict) + """ + url = f"{API_BASE}{REPO_PATH}{path}" + body = json.dumps(data).encode('utf-8') if data else None + req = urllib.request.Request(url, data=body, headers={ + 'Authorization': f'token {TOKEN}', + 'Content-Type': 'application/json; charset=utf-8' + }, method=method) + resp = urllib.request.urlopen(req, timeout=10) + return json.loads(resp.read().decode('utf-8')) +``` + +### API Token + +各 Agent 账号的 Token 存储位置: + +| Agent | Token 获取位置 | +|-------|---------------| +| `ai-pm` | Gitea → ai-pm 设置 → 应用 → Access Token | + +> **安全提醒**:Token 不应硬编码在代码中。当前会话中由用户提供,每次新会话需重新获取或从安全存储读取。 + +--- + +## 常用 API 操作 + +### Issues + +```python +# 列出所有 Issue(含已关闭) +issues = gitea_api('GET', '/issues?state=all') + +# 列出某个里程碑下的 Issue +issues = gitea_api('GET', '/issues?milestones=2') + +# 创建 Issue(关联里程碑) +gitea_api('POST', '/issues', { + 'title': '任务标题', + 'body': '任务描述(支持 Markdown)', + 'milestone': 2, # 里程碑 ID + 'assignees': ['ai-pm'] # 指派人(可选) +}) + +# 修改 Issue +gitea_api('PATCH', '/issues/1', { + 'title': '新标题', + 'body': '新内容', + 'state': 'closed' # 关闭 Issue +}) + +# Issue 添加评论 +gitea_api('POST', '/issues/1/comments', { + 'body': '评论内容' +}) +``` + +### Milestones(里程碑) + +```python +# 列出所有里程碑 +milestones = gitea_api('GET', '/milestones') + +# 创建里程碑 +gitea_api('POST', '/milestones', { + 'title': 'v0.2-小游戏', + 'description': '验证小游戏核心循环', + 'state': 'open' +}) + +# 修改里程碑 +gitea_api('PATCH', '/milestones/2', { + 'title': '新标题', + 'state': 'closed' +}) +``` + +### Labels(标签) + +```python +# 列出标签 +labels = gitea_api('GET', '/labels') + +# 创建标签 +gitea_api('POST', '/labels', { + 'name': '执行案', + 'color': '#0052cc' # 十六进制颜色(不带 # 也行) +}) + +# 给 Issue 添加标签 +gitea_api('POST', '/issues/1/labels', { + 'labels': [1, 2] # 标签 ID 列表 +}) +``` + +--- + +## 已知限制 + +### Projects(项目看板)无 REST API + +Gitea 1.23.4 **不支持** 项目看板的 REST API,端点返回 404。 + +- 功能请求追踪:[go-gitea/gitea#36824](https://github.com/go-gitea/gitea/issues/36824) +- 升级 Gitea 版本**不会**解决此问题(截至 2026-06,该功能尚未实现) +- 看板列的操作(如将 Issue 拖入 Backlog/To Do/In Progress/Done 列)只能通过 Web 页面手动完成 + +**替代方案**:使用 Issue 的 **状态**(open/closed)+ **标签** 追踪进度,看板仅作为可视化展示。 + +### 当前看板 + +- 看板地址:`http://micecola.top:8418/starmen1265/ChildHood/projects/2` +- 看板名称:**原型制作** +- 列结构:`Backlog → To Do → In Progress → Done` + +--- + +## CLI 工具(推荐) + +> 自 2026-06-05 起,所有 Agent 统一使用 CLI 工具操作 Gitea,不再直接使用 curl。 + +### 工具位置 + +`scripts/gitea.js` — Node.js CLI,封装所有 API 操作,自动处理 UTF-8 编码。 + +### Token 管理 + +Token 存储在 `scripts/.gitea-tokens.json`(已加入 `.gitignore`),每台机器配置一次。 + +```json +{ + "pm": "", + "designer": "", + "dev": "" +} +``` + +**Token 优先级**: `GITEA_TOKEN` 环境变量 > `--role ` 文件读取 > 报错 + +两台机器使用同一套 Token,只需将此文件拷贝到对应机器的 `scripts/` 目录下。 + +### 用法 + +```bash +# Agent 调用(通过 --role 自动选 Token,无需手动传 Token) +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 + +# 查看所有命令 +node scripts/gitea.js --help + +# 常用操作(以 --role pm 为例) +node scripts/gitea.js --role pm issue list --milestone v0.1-基础交互 +node scripts/gitea.js --role pm issue get +node scripts/gitea.js --role pm issue create --title "P-XXX 标题" --body "..." --milestone 2 +node scripts/gitea.js --role pm issue close +node scripts/gitea.js --role pm issue comment --body "汇报内容" +node scripts/gitea.js --role pm issue labels --set +node scripts/gitea.js --role pm issue block + +node scripts/gitea.js --role pm milestone list +node scripts/gitea.js --role pm label list +node scripts/gitea.js label init # 初始化标准标签体系 + +node scripts/gitea.js pipeline status +node scripts/gitea.js pipeline update --step <1-7> --status "描述" + +node scripts/gitea.js test # 验证所有 API 接口 + +# --json 标志输出纯 JSON(供脚本解析) +node scripts/gitea.js issue list --json +``` + +### 标准标签体系(23 个) + +| 类别 | 标签 | 用途 | +|------|------|------| +| status | `status:backlog` `status:ready` `status:in-progress` `status:blocked` `status:in-review` `status:done` | 任务生命周期 | +| pipeline | `pipeline:3-execution` `pipeline:4-tech-design` `pipeline:5-art` `pipeline:6-impl` `pipeline:7-retro` | 流水线步骤 | +| role | `role:design` `role:dev` `role:art` `role:pm` | 角色归属 | +| gate | `gate:execution-pass/fail` `gate:tech-pass/fail` `gate:art-pass/fail` `gate:impl-pass/fail` | 质量门判定 | + +### API 客户端模块 + +`scripts/gitea-api.js` — Node.js 模块,提供 `GiteaAPI` 类。可供自定义脚本引用: + +```javascript +const { GiteaAPI } = require('./gitea-api'); +const api = new GiteaAPI('your-token'); +const issues = await api.listIssues({ state: 'open' }); +``` + +--- + +## 当前里程碑与 Issue 状态 + +> 最后更新:2026-06-05 + +### 里程碑 + +| ID | 标题 | 状态 | +|----|------|------| +| 2 | v0.1-基础交互 | open | + +### Issues(v0.1-基础交互) + +| # | 标题 | 标签 | 状态 | 依赖 | +|---|------|------|------|------| +| 9 | P-001 原型阶段启动准备 | role:pm, status:done | ✅ 已完成 | — | +| 2 | P-002a 交互系统执行案 | status:blocked, pipeline:3, role:design, gate:execution-fail | 🔴 质量门未通过 | — | +| 1 | P-002b 收集系统执行案 | status:blocked, pipeline:3, role:design, gate:execution-fail | 🔴 质量门未通过 | — | +| 3 | P-002c 角色控制执行案 | status:blocked, pipeline:3, role:design, gate:execution-fail | 🔴 质量门未通过 | — | +| 10 | P-003 v0.1 ④程序设计 | status:backlog, pipeline:4, role:dev | ⬜ Backlog | #1, #2, #3 | +| 11 | P-004 v0.1 ⑤美术 + ⑥程序实现 | status:backlog, role:dev, role:art | ⬜ Backlog | #10 | +| 12 | P-005 v0.1 ⑦版本复盘 | status:backlog, pipeline:7, role:pm | ⬜ Backlog | #11 | +| 8 | Pipeline Tracker — v0.1 | role:pm, status:in-progress | 🔄 流水线追踪 | — | diff --git a/scripts/gitea-api.js b/scripts/gitea-api.js new file mode 100644 index 0000000..6909b7c --- /dev/null +++ b/scripts/gitea-api.js @@ -0,0 +1,254 @@ +/** + * Gitea API Client — ChildHood 项目进度管理 + * + * 封装所有 Gitea REST API 调用,处理 UTF-8 编码和错误重试。 + * 供 gitea.js CLI 使用。 + */ + +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] - 覆盖默认配置 + */ + constructor(token, config = {}) { + if (!token) throw new Error('GITEA_TOKEN 未设置。请设置环境变量 GITEA_TOKEN。'); + this.token = token; + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + // ====== 底层 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} 解析后的 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) { + // 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], + }); + } + + /** 移除依赖 */ + removeBlock(number, blockedNumber) { + return this.request('DELETE', `/issues/${number}/blocks`, { + index: blockedNumber, + owner: this.config.repoPath.split('/')[2], + repo: this.config.repoPath.split('/')[3], + }); + } + + // ====== 辅助方法 ====== + + /** 通过名称查找标签,返回 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, DEFAULT_CONFIG }; diff --git a/scripts/gitea.js b/scripts/gitea.js new file mode 100644 index 0000000..05d42f9 --- /dev/null +++ b/scripts/gitea.js @@ -0,0 +1,787 @@ +#!/usr/bin/env node +/** + * ChildHood Gitea CLI — 项目进度管理工具 + * + * 用法: node scripts/gitea.js [subcommand] [options] + * 环境变量: GITEA_TOKEN(必须) + */ + +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: '已完成' }, + // Pipeline (5) + { name: 'pipeline:3-execution', color: '#c5def5', description: '③ 执行案' }, + { name: 'pipeline:4-tech-design', color: '#bfdadc', description: '④ 程序设计' }, + { name: 'pipeline:5-art', color: '#d4c5f9', description: '⑤ 美术产出' }, + { name: 'pipeline:6-impl', color: '#fce6c0', description: '⑥ 程序实现' }, + { name: 'pipeline:7-retro', color: '#f0e5b0', description: '⑦ 版本复盘' }, + // Role (4) + { name: 'role:design', color: '#bfd4f2', description: '策划任务' }, + { name: 'role:dev', color: '#c1f0c1', description: '程序任务' }, + { name: 'role:art', color: '#ffccdd', description: '美术任务' }, + { name: 'role:pm', color: '#e0e0e0', description: 'PM 任务' }, + // 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: '程序实现验收门 ❌' }, +]; + +// ═══════════════════════════════════════════════════ +// 命令行参数解析 +// ═══════════════════════════════════════════════════ + +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 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); +} + +// ═══════════════════════════════════════════════════ +// 命令实现 +// ═══════════════════════════════════════════════════ + +const cmd = {}; + +// ──────── issue ──────── + +cmd.issue = { + async list(api, opts) { + 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) }; + if (ms) params.milestones = ms.id; + else { fail(`未找到里程碑: ${opts.options.milestone}`); return; } + } + if (opts.options.labels) params.labels = opts.options.labels; + + 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 '); 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 ] [--labels ]'); 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); + } + if (opts.options.assignees) { + data.assignees = opts.options.assignees.split(','); + } + + 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 [--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(','); + + 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 '); 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 '); 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 --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 '); 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 [--add ] [--set ]'); 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 '); 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 '); 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 '); 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 [--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'); + 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) { + 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 — v0.1'; + const milestone = opts.options.milestone; + + const body = `# 流水线进度追踪 + +## v0.1 流水线 + +| 步骤 | 状态 | 说明 | +|------|------|------| +| ① 设计案 | ✅ 已完成 | \`docs/design/prototype/\` 下 10 份设计文档 | +| ② 阶段路线图 | ✅ 已完成 | \`docs/plans/prototype-roadmap.md\` | +| ③ 执行案 | 🔴 质量门未通过 | 需创建独立执行案并修正 | +| ④ 程序设计 | ⬜ 未开始 | 依赖 ③ | +| ⑤ 美术产出 | ⬜ 未开始 | 白盒场景 + 占位资产 | +| ⑥ 程序实现 | ⬜ 未开始 | 原型允许与 ⑤ 交替 | +| ⑦ 版本复盘 | ⬜ 未开始 | — | + +## 当前阻塞 + +③ 执行案质量门未通过,需修正后重新提交 + +## 相关任务 + +> PM 在此维护 Issue 索引 +`; + + 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) { + // 查找 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) { + outText(`\n📊 Pipeline Tracker #${tracker.number}\n`); + outText(tracker.body); + } + }, + + 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 --step <1-7> --status <状态描述>'); + 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}`); + } else { + fail(`未在 Issue #${num} 的 body 中找到步骤: ${stepLabel}`); + } + }, +}; + +// ──────── 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()); + 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)); + + 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}`; + }); + + await test('POST 创建含中文评论', async () => { + const c = await api.createComment(1, '🧪 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 newTitle = orig.title + ' [测试更新]'; + await api.updateIssue(2, { title: newTitle }); + await api.updateIssue(2, { title: orig.title }); // 还原 + return 'Issue #2 标题更新并还原'; + }); + + 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); // 最终关闭 + 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}) 已创建并清理`; + }); + + 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`; + }); + + // 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`; + }); + + await test('POST/DELETE Issue 依赖', async () => { + // Issue #1 block #2,然后移除 + await api.addBlock(1, 2); + await api.removeBlock(1, 2); + return '依赖关系添加→移除'; + }); + + 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() { + console.log(` +ChildHood Gitea CLI — 项目进度管理工具 + +用法: node scripts/gitea.js [subcommand] [options] + +全局选项: + --help 显示帮助信息 + --json 以 JSON 格式输出(供 Agent 脚本调用) + --role 使用对应角色的 Token(从 scripts/.gitea-tokens.json 读取) + +命令列表: + + issue list 列出 Issues + [--state open|closed|all] [--milestone ] [--labels ] + issue get <#> 查看 Issue 详情 + issue create 创建 Issue + --title "标题" [--body "内容"] [--milestone ] [--labels ] + issue update <#> 更新 Issue + [--title "…"] [--body "…"] [--state open|closed] [--milestone ] + issue close <#> 关闭 Issue [--body "完成评论"] + issue reopen <#> 重新打开 Issue + issue comment <#> 添加评论 --body "内容" + issue comments <#> 查看评论列表 + issue labels <#> 查看/管理标签 [--add ] [--set ] + issue deps <#> 查看依赖关系 + issue block <#> <#> 设置依赖关系(前者阻塞后者) + issue unblock <#> <#> 移除依赖关系 + + milestone list 列出里程碑 [--state open|closed|all] + milestone create 创建里程碑 --title "标题" [--description "…"] [--due YYYY-MM-DD] + milestone update 更新里程碑 [--title "…"] [--state open|closed] + + label list 列出标签 + label create 创建标签 --name "名称" [--color "#RRGGBB"] + label init 初始化标准标签体系(status/pipeline/role/gate 共 23 个) + + pipeline init 创建 Pipeline Tracker Meta Issue [--title "…"] [--milestone ] + pipeline status 查看当前流水线进度 + pipeline update <#> 更新流水线步骤 --step <1-7> --status "状态描述" + + test 验证所有 API 接口(P0 阻塞性 + P1 核心功能) + +Token 管理: + --role 从 scripts/.gitea-tokens.json 自动读取对应角色的 Token + GITEA_TOKEN 环境变量 手动覆盖(优先级最高) + 两者都不提供时报错 + +示例: + 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 +`); +} + +// ═══════════════════════════════════════════════════ +// 路由与入口 +// ═══════════════════════════════════════════════════ + +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']; + 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; + + if (args.options.help || (!args.command && !args.sub)) { + showHelp(); + return; + } + + // Token 解析 + const token = resolveToken(args.options); + + const api = new GiteaAPI(token); + + // 路由 + const commandGroup = cmd[args.command]; + if (!commandGroup) { + console.error(`❌ 未知命令: ${args.command}`); + console.error(' 运行 --help 查看可用命令'); + process.exit(1); + } + + // 特殊: test 是单级命令 + 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); +});