#!/usr/bin/env node // ticket.mjs — Gitea 工单工具(人读 + 机读规范) // 子命令:labels | milestones | list | verify | create | rename // 令牌:GITEA_TOKEN 环境变量,或 --token-file=<含 key=value 的文件>(不落盘、不回显) import fs from "node:fs"; const DOMAINS = ["订单", "库存", "结算", "商品", "报表", "平台", "其他"]; const GATES = ["等口径", "等取证", "等部署"]; const NL = String.fromCharCode(10); const BACKSLASH = String.fromCharCode(92); const BAD = ["codes/", "分析/数据/", "分析/D-", "分析/E-", "分析/A-", "分析/B-", "分析/C-", "分析/F-", "分析/_tmp", "分析/_archive", "分析/README", "86 §", "80 · V", "/home/", "C:" + BACKSLASH]; function parseArgs(argv) { const pos = [], opt = {}; for (const a of argv) { if (a.startsWith("--")) { const i = a.indexOf("="); if (i < 0) opt[a.slice(2)] = true; else opt[a.slice(2, i)] = a.slice(i + 1); } else pos.push(a); } return { pos: pos, opt: opt }; } function readToken(opt) { if (process.env.GITEA_TOKEN) return process.env.GITEA_TOKEN.trim(); const f = opt["token-file"]; if (!f) throw new Error("缺令牌:设 GITEA_TOKEN 或 --token-file=<文件>"); for (const line of fs.readFileSync(f, "utf8").split(NL)) { if (/token/i.test(line)) { const i = line.indexOf("="); if (i > 0) return line.slice(i + 1).split("#")[0].trim(); } } throw new Error("令牌文件里没找到 token = ..."); } function mkApi(opt) { const host = opt.host || "code.hoteas.com"; const H = { Authorization: "token " + readToken(opt), "Content-Type": "application/json", Accept: "application/json", "User-Agent": "agents-skills/ticket-management" }; return async function api(path, method, body) { const r = await fetch("https://" + host + "/api/v1" + path, { method: method || "GET", headers: H, body: body ? JSON.stringify(body) : undefined }); const t = await r.text(); let j = null; try { j = JSON.parse(t); } catch (e) {} return { s: r.status, total: r.headers.get("x-total-count"), j: j, t: t }; }; } function splitTitle(title) { const brackets = []; let s = String(title || "").trim(); while (s.startsWith("【")) { const e = s.indexOf("】"); if (e < 0) break; brackets.push(s.slice(1, e)); s = s.slice(e + 1).trim(); } const parts = brackets.length ? brackets[0].split("/").map(function (x) { return x.trim(); }) : []; return { brackets: brackets, parts: parts, rest: s }; } function firstLine(body) { return String(body || "").split(NL)[0] || ""; } function fieldOf(line, key) { for (const p of String(line).split(" | ")) { const i = p.indexOf("="); if (i > 0 && p.slice(0, i).trim() === key) return p.slice(i + 1).trim(); } return ""; } function humanPart(body) { const b = String(body || ""); const i = b.indexOf("