Files
ChildHood/.claude/skills/gitea-pm/gitea.js
T

758 lines
27 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
/**
* 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);
});