Files
ChildHood/.claude/skills/gitea-collab/gitea-api.js
T

254 lines
7.1 KiB
JavaScript

/**
* 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 };