Files
ChildHood/scripts/gitea-api.js
T
DESKTOP-2K5TJI4\admin eee3a4925c feat(tools): 新增 Gitea CLI 工具并迁移进度管理到 Issue/Milestone
- 新增 scripts/gitea.js CLI(20+ 子命令,--role Token 管理)
- 新增 scripts/gitea-api.js API 客户端模块
- 新增 scripts/.gitea-tokens.json(gitignored,按角色存 Token)
- 更新 .gitignore 排除 Token 文件
- 更新 gitea-api-guide.md 新增 CLI 用法和标签体系文档
- Gitea 上初始化 23 个 Labels + 8 个 Issues + 依赖链
- 16/16 API 接口验证全部通过(含 UTF-8 中文编码)
2026-06-05 19:07:37 +08:00

255 lines
7.1 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.
/**
* 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<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) {
// 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 };