Files
ChildHood/scripts/gitea.js
T
starmen1265 8011cb6472 docs(review): P-008交互系统程序设计质量门审查通过+美术调研+工作流完善
- P-008 交互系统技术设计审查通过,审查记录 docs/plans/reviews/P-008-review.md
- D-001 关卡执行案审查通过,审查记录 docs/plans/reviews/D-001-review.md
- 新增 AI 美术调研参考图(我的暑假/放学后少年/真实场景/美术风格)
- 新增关卡执行案、白模搭建指南和搭建报告
- 完善工作流技能:质量门审查、Agent记忆、需求分析
- 更新交互系统和角色控制系统技术设计文档
- 清理过时参考文档
2026-06-07 01:54:02 +08:00

778 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* ChildHood Gitea CLI — 项目进度管理工具
*
* 用法: node scripts/gitea.js <command> [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: '已完成' },
// 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;
if (opts.options.assigned) params.assigned_by = 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);
}
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 <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(',');
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');
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 <issue-number> --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 <command> [subcommand] [options]
全局选项:
--help 显示帮助信息
--json 以 JSON 格式输出(供 Agent 脚本调用)
--role <pm|designer|dev> 使用对应角色的 Token(从 scripts/.gitea-tokens.json 读取)
命令列表:
issue list 列出 Issues
[--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 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 初始化标准标签体系(status/gate 共 14 个)
pipeline init 创建 Pipeline Tracker Meta Issue [--title "…"] [--milestone <id>]
pipeline status 查看当前流水线进度
pipeline update <#> 更新流水线步骤 --step <1-7> --status "状态描述"
test 验证所有 API 接口(P0 阻塞性 + P1 核心功能)
Token 管理:
--role <pm|designer|dev> 从 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);
});