Files
agents-skills/ticket-management/bin/ticket.mjs
T
xuxiang 47f82dfde8 feat(ticket-management)!: 结构评估先行 —— 四段任务全貌 + 只记要点闸门 + 有效性与整洁性自检
用户 2026-09-24 交办:工单 skill 在**新建 / 更新**时须**优先评估正文 + 评论该有的结构**,
以合适的安排把关键内容记进工单;**不可事无巨细**,只记能组成「任务全貌」的要点
(**问题背景 / 处置提案 / 处置结果 / 效果对比**);skill 须**始终维护所编辑工单的结构有效性与整洁性**。

【本轮整改】
- SKILL.md
  · 新增**闸门 A「先评结构」**(§8.0 五步 + 落点表)与**闸门 B「只记要点」**(§8.2 四问),
    置于 §1 硬规则之首 —— 动工单前的第一动作,不能事后补。
  · 重写 §8.1 正文骨架为**四段任务全貌**;③处置结果 / ④效果对比**未发生不写空壳**。
  · 新增 §8.3 **有效性 + 整洁性自检清单**,并升为 §1 规则 4(可交付条件)。
  · 修 §1 规则 2 与 §2 的自相矛盾(「可选【闸门】」vs「并进同一括号」)⇒ 统一为【域-闸门】。
  · §3.2 补四段段序;§7 写明 verify=有效性 / assess=结构 的分工。
- bin/ticket.mjs
  · 新增 assess(只读结构体检:四段 / 缺段 / 顺序 / 空壳 + 评论清单)
    · update(**标题与正文同一次 PATCH**,过自检才放行)
    · comments(清单 / --dump **删前全量备份**)· comment-add · comment-del(**默认只预览**,--yes 才删)。
  · create 也先打印结构评估;令牌改**延迟读取**(assess --body-file 无需令牌)。
  · 结构判定取「最长词优先」+「同级标题界定空壳」+「更正/补充小节不计顺序」。
- templates/issue.md:改为四段骨架;修 `域=<L1>/<L2>/<L3>` 与 §3.1 / 代码不一致(应为 `-` 分隔)。
- README.md:技能清单说明同步。

【收录此前在途未提交改动】(原属他方;依 86 §13.26 三「待技能属主 / 委托人提交」,本轮一并收束)
- 建单 `--label` 必传 + 断言 7「已带标签」;§1 规则 1b/1c;§8 时间线纪律;§9 实测补充。

【验证 · E1 · 本机】
- `node --check bin/ticket.mjs` 通过。
- `verify 157 158 159 --repo=xbc/xbc` ⇒ **21/21 通过**(七断言未改 ⇒ 向后兼容)。
- `assess --body-file=_body157/158/159.md` ⇒ 段位与顺序判定与三单实际结构相符;
  合成反例(缺必备段 + 乱序 + 空壳)三项全部命中。
- `update 159 --dry-run` 与 `comment-del 534`(仅预览)均未落盘。

【遗留】
- 本仓只改文件不推送时,工作树在 `分析/_tmp/`(可删)⇒ 需推送 `origin/main` 才算持久。
- 接入本工作区(`.dsh/skills/agents-skills` junction)依 86 §12.43 属「待产研负责人决定」。
2026-09-24 10:21:28 +08:00

366 lines
22 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
// ticket.mjs — Gitea 工单工具(人读 + 机读规范 · **结构评估先行**)
// 子命令:labels | milestones | list | verify <n...> | assess <n|--body-file=f> | create
// update <n> | comments <n> | comment-add <n> | comment-del <id> | rename <n>
// 令牌:GITEA_TOKEN 环境变量,或 --token-file=<含 key=value 的文件>(不落盘、不回显)
// 纪律真源:同目录 ../SKILL.md(§7 verify 七断言 · §8 结构纪律 / assess 四段体检)
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];
// §8.1「任务全貌」四段:正文骨架词表(数组顺序 = 规范顺序)
const SECTIONS = [
{ key: "问题背景", words: ["现象", "根因", "影响面", "问题背景", "背景"], need: "必备" },
{ key: "处置提案", words: ["处置提案", "方案", "机制", "提案", "路线"], need: "必备" },
{ key: "处置结果", words: ["处置结果", "执行回执", "当前状态", "回执", "结果"], need: "按事实" },
{ key: "效果对比", words: ["效果对比", "收益验证", "收益", "对照", "效果"], need: "按事实" }
];
const OPTIONAL_SECTIONS = ["验收判据", "反证条件", "遗留"];
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";
let H = null; // 延迟取令牌 ⇒ assess --body-file 无需令牌
return async function api(path, method, body) {
if (!H) H = { Authorization: "token " + readToken(opt), "Content-Type": "application/json", Accept: "application/json", "User-Agent": "agents-skills/ticket-management" };
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("<!--"); return i < 0 ? b : b.slice(0, i); }
// 七项断言(verify 与 create/update 共用)
function checksOf(title, body) {
const sp = splitTitle(title), L = firstLine(body);
const dom = fieldOf(L, "域"), gate = fieldOf(L, "闸门"), hp = humanPart(body);
return [
["1 标题主前缀 ∈ 词表", sp.parts.length > 0 && DOMAINS.indexOf(sp.parts[0]) >= 0],
["2 首行含 域=", L.indexOf("域=") >= 0],
["3 标题主域 = 首行 域 的 L1", sp.parts.length > 0 && dom.split("-")[0] === sp.parts[0]],
["4 标题闸门 = 首行 闸门=", sp.parts.length < 2 ? (gate === "可开工") : (GATES.indexOf(sp.parts[1]) >= 0 && sp.parts[1] === gate)],
["5 人读部分无本地 / 外部引用", !BAD.some(function (b) { return hp.indexOf(b) >= 0; })],
["6 有引用块 ⇒ 有 ref-owner", body.indexOf("<!--") < 0 ? true : body.indexOf("ref-owner:") >= 0]
];
}
// —— §8.0 结构评估(只读)——
function headingsOf(body) {
const out = [];
for (const line of humanPart(body).split(NL)) {
const m = /^(#{1,6})\s+(.*)$/.exec(line);
if (m) out.push({ level: m[1].length, text: m[2].trim() });
}
return out;
}
function sectionIndex(text) {
// 最长词优先(更具体者胜):如「变更后收益验证(…方案)」应归「效果对比」而非「处置提案」
let best = -1, bestLen = 0;
for (let i = 0; i < SECTIONS.length; i++) {
for (const w of SECTIONS[i].words) {
if (text.indexOf(w) >= 0 && w.length > bestLen) { best = i; bestLen = w.length; }
}
}
return best;
}
function isPostNote(text) { return /更正|订正|勘误|补充|追加/.test(text); }
function isOptional(text) { return OPTIONAL_SECTIONS.some(function (w) { return text.indexOf(w) >= 0; }); }
function structureOf(body) {
const lines = humanPart(body).split(NL);
const heads = [];
lines.forEach(function (l, i) {
const m = /^(#{1,6})\s+(.*)$/.exec(l);
if (m) heads.push({ pos: i, level: m[1].length, text: m[2].trim() });
});
// 空壳判定:某标题之后、下一个「同级或更高级」标题之前无任何内容(含子标题)⇒ 空壳
const empty = [];
for (let k = 0; k < heads.length; k++) {
let j = k + 1;
while (j < heads.length && heads[j].level > heads[k].level) j++;
const inner = lines.slice(heads[k].pos + 1, j < heads.length ? heads[j].pos : lines.length)
.join(NL).replace(/<!--[\s\S]*?-->/g, "").trim();
if (!inner) empty.push(lines[heads[k].pos].trim());
}
// 归类只看 ## 级;「更正 / 补充」小节允许后置,不计入顺序
const tops = heads.filter(function (h) { return h.level === 2; })
.map(function (h) { return { text: h.text, idx: sectionIndex(h.text), post: isPostNote(h.text) }; });
const main = tops.filter(function (t) { return !t.post && t.idx >= 0; });
const order = main.map(function (t) { return t.idx; });
const ordered = order.every(function (v, i) { return i === 0 || order[i - 1] <= v; });
const hits = tops.filter(function (t) { return t.idx >= 0; });
const postNotes = tops.filter(function (t) { return t.post; }).map(function (t) { return t.text; });
const unclassified = tops.filter(function (t) { return t.idx < 0 && !t.post && !isOptional(t.text); }).map(function (t) { return t.text; });
const optional = OPTIONAL_SECTIONS.filter(function (w) { return heads.some(function (h) { return h.text.indexOf(w) >= 0; }); });
return { hits: hits, ordered: ordered, empty: empty, optional: optional, postNotes: postNotes, unclassified: unclassified };
}
function printStructure(body) {
const st = structureOf(body);
console.log("四段(§8.1 · 顺序 " + SECTIONS.map(function (s) { return s.key; }).join(" → ") + "):");
for (let i = 0; i < SECTIONS.length; i++) {
const own = st.hits.filter(function (x) { return x.idx === i; }).map(function (x) { return "## " + x.text; });
const tail = own.length ? " " + own.join(" · ")
: (SECTIONS[i].need === "必备" ? " ← 未识别到(必备段:确认是否用了同义小标题,或确实缺)" : " ← 未识别到(未发生 / 未测就不写,不要空壳)");
console.log(" " + (own.length ? "[✓]" : "[ ]") + " " + SECTIONS[i].key + "(" + SECTIONS[i].need + ")" + tail);
}
console.log(" 顺序:" + (st.ordered ? "OK" : "⚠ 主叙事未按 背景→提案→结果→效果 排列 ⇒ 调整(更正 / 补充小节不计)"));
if (st.postNotes.length) console.log(" 后置小节(不计顺序):" + st.postNotes.map(function (t) { return "## " + t; }).join(" · "));
if (st.unclassified.length) console.log(" 未归类 ## 小节(人工判断归属):" + st.unclassified.map(function (t) { return "## " + t; }).join(" · "));
console.log(" 空壳段:" + (st.empty.length ? "✗ " + st.empty.join(" · ") + "(删掉或补齐)" : "无"));
console.log(" 可选段:" + (st.optional.length ? st.optional.join(" · ") : "无"));
}
async function findTitle(api, repo, title) {
const r = await api("/repos/" + repo + "/issues?state=all&type=issues&limit=100");
return (Array.isArray(r.j) ? r.j : []).filter(function (x) { return String(x.title || "") === title; });
}
async function cmdRead(api, opt, kind) {
const r = await api("/repos/" + opt.repo + "/" + kind + "?limit=100&state=all");
if (r.s !== 200) { console.log("读取失败 status=" + r.s + " " + r.t.slice(0, 120)); return 1; }
const arr = Array.isArray(r.j) ? r.j : [];
console.log(kind + " 共 " + arr.length + " 项");
for (const x of arr) {
if (kind === "labels") console.log(" #" + x.id + " " + x.name + " color=" + x.color);
else console.log(" #" + x.id + " " + x.title + " open=" + x.open_issues + " closed=" + x.closed_issues + " due=" + (x.due_on || "-"));
}
return 0;
}
async function cmdList(api, opt) {
const state = opt.state || "open";
const r = await api("/repos/" + opt.repo + "/issues?state=" + state + "&type=issues&limit=" + (opt.limit || 30));
if (r.s !== 200) { console.log("读取失败 status=" + r.s); return 1; }
const arr = Array.isArray(r.j) ? r.j : [];
console.log("issues(" + state + ") 返回 " + arr.length + " · 总数 " + (r.total || "-"));
for (const it of arr) console.log(" #" + it.number + " [" + (it.labels || []).map(function (l) { return l.name; }).join(",") + "] " + it.title);
return 0;
}
async function cmdVerify(api, opt, nums) {
let fail = 0, total = 0;
for (const n of nums) {
const r = await api("/repos/" + opt.repo + "/issues/" + n);
if (r.s !== 200) { console.log("#" + n + " 读取失败 status=" + r.s); fail++; continue; }
console.log("#" + n + " " + r.j.title);
for (const c of checksOf(String(r.j.title || ""), String(r.j.body || ""))) {
total++; if (!c[1]) fail++;
console.log(" " + (c[1] ? "PASS" : "FAIL") + " " + c[0]);
}
const labNames = (r.j.labels || []).map(function (l) { return l.name; });
total++; if (!labNames.length) fail++;
console.log(" " + (labNames.length ? "PASS" : "FAIL") + " 7 已带标签" + (labNames.length ? " [" + labNames.join(",") + "]" : " (空 ⇒ 建单时未传 --label)"));
}
console.log("断言 " + (total - fail) + "/" + total + " 通过" + (fail ? " · FAIL " + fail : ""));
return fail ? 1 : 0;
}
async function cmdAssess(api, opt, n) {
let title = "", body = "", comments = null;
if (opt["body-file"]) {
body = fs.readFileSync(opt["body-file"], "utf8");
title = opt.title ? String(opt.title) : "(未给 --title,只评正文)";
} else {
if (!n) { console.log("用法:assess <n> --repo=o/r | assess --body-file=<文件> [--title=…]"); return 1; }
const r = await api("/repos/" + opt.repo + "/issues/" + n);
if (r.s !== 200) { console.log("读取失败 status=" + r.s); return 1; }
title = String(r.j.title || ""); body = String(r.j.body || "");
const rc = await api("/repos/" + opt.repo + "/issues/" + n + "/comments?limit=100");
comments = Array.isArray(rc.j) ? rc.j : [];
}
console.log("== 结构评估(§8.0 落点表底稿 · 只报结构,判断仍由你下)==");
console.log("标题:" + title);
console.log("首行机读:" + (firstLine(body) || "(空)"));
printStructure(body);
if (comments) {
console.log("评论清单(" + comments.length + ")—— 只应剩「必须可执行物」(§8.1 第 2 条):");
for (const c of comments) {
const b = String(c.body || "");
console.log(" " + c.id + " " + b.length + " 字 首行: " + (b.split(NL)[0] || "").slice(0, 90));
}
if (comments.length > 1) console.log(" ⚠ 多条评论 ⇒ 逐条过 §8.2;删前先备份:comments " + n + " --dump=<文件>(§8.1 第 5 条)");
}
console.log("下一步:把本轮候选信息逐条填进 §8.0 的落点表 ⇒ 再 create / update / comment-add。");
return 0;
}
async function cmdCreate(api, opt) {
const title = opt.title;
if (!title) { console.log("用法:create --repo=o/r --title=... --body-file=... --label=<id>[,<id>] [--dry-run] [--force]"); return 1; }
const sp = splitTitle(title);
if (!sp.parts.length || DOMAINS.indexOf(sp.parts[0]) < 0) { console.log("拒绝:标题主前缀不合规 —— " + title); return 1; }
const dup = await findTitle(api, opt.repo, title);
if (dup.length) { console.log("拒绝:已存在同名单 #" + dup[0].number + " —— 先查重是硬规则"); return 1; }
const bodyFile = opt["body-file"];
if (!bodyFile) { console.log("用法:create ... --body-file=..."); return 1; }
// 标签必传(2026-09-23 · 用户交办):缺失即拒建,--force 不可跳过(实测 #156/#157 因未传而裸建无标签)
const labelIds = String(opt.label == null ? "" : opt.label).split(",").map(function (x) { return x.trim(); }).filter(Boolean);
if (!labelIds.length) { console.log("拒绝:建单必须带标签 —— 传 --label=<id>[,<id>](标签表:ticket.mjs labels --repo=" + opt.repo + ")"); return 1; }
const badId = labelIds.filter(function (x) { return !/^[0-9]+$/.test(x) || Number(x) <= 0; });
if (badId.length) { console.log("拒绝:--label 只接受正整数 id,收到 " + badId.join(",")); return 1; }
const rl = await api("/repos/" + opt.repo + "/labels?limit=100");
const knownLabels = Array.isArray(rl.j) ? rl.j : [];
const missId = labelIds.filter(function (x) { return !knownLabels.some(function (l) { return String(l.id) === x; }); });
if (knownLabels.length && missId.length) {
console.log("拒绝:标签 id 不存在 —— " + missId.join(",") + ";本仓可选:" + knownLabels.map(function (l) { return l.id + "=" + l.name; }).join(" · "));
return 1;
}
const body = fs.readFileSync(bodyFile, "utf8");
const checks = checksOf(title, body);
const bad = checks.filter(function (c) { return !c[1]; });
console.log("结构评估(§8.0 · 建单前必看):");
printStructure(body);
if (bad.length && !opt.force) {
console.log("拒绝:正文/标题未过自检(" + bad.length + " 项)");
for (const c of bad) console.log(" FAIL " + c[0]);
console.log("(确认要建就加 --force)");
return 1;
}
if (opt["dry-run"]) { console.log("[dry-run] 将建单:" + title + " · 正文 " + body.length + " 字 · 标签 " + labelIds.join(",") + " · 自检 " + (checks.length - bad.length) + "/" + checks.length); return 0; }
const payload = { title: title, body: body, labels: labelIds.map(Number) };
const r = await api("/repos/" + opt.repo + "/issues", "POST", payload);
console.log("status=" + r.s + (r.s === 201 ? " → #" + r.j.number + " " + r.j.html_url : " " + r.t.slice(0, 200)));
return r.s === 201 ? 0 : 1;
}
async function cmdUpdate(api, opt, n) {
if (!n) { console.log("用法:update <n> --repo=o/r --body-file=<文件> [--title=…] [--dry-run] [--force]"); return 1; }
const bodyFile = opt["body-file"];
if (!bodyFile) { console.log("用法:update <n> --body-file=<文件>(改正文;标题与正文同一次提交,§8.1 第 4 条)"); return 1; }
const cur = await api("/repos/" + opt.repo + "/issues/" + n);
if (cur.s !== 200) { console.log("读取失败 status=" + cur.s); return 1; }
const title = opt.title != null ? String(opt.title) : String(cur.j.title || "");
const body = fs.readFileSync(bodyFile, "utf8");
const checks = checksOf(title, body);
const bad = checks.filter(function (c) { return !c[1]; });
console.log("结构评估(§8.0 · 改单前必看):");
printStructure(body);
if (bad.length && !opt.force) {
console.log("拒绝:正文/标题未过自检(" + bad.length + " 项)");
for (const c of bad) console.log(" FAIL " + c[0]);
console.log("(确认要改就加 --force)");
return 1;
}
const payload = { body: body };
if (opt.title != null) payload.title = title;
if (opt["dry-run"]) { console.log("[dry-run] PATCH #" + n + " · 标题" + (opt.title != null ? "(同批改)" : "(不动)") + " · 正文 " + body.length + " 字 · 自检 " + (checks.length - bad.length) + "/" + checks.length); return 0; }
const r = await api("/repos/" + opt.repo + "/issues/" + n, "PATCH", payload);
console.log("status=" + r.s + (r.j && r.j.html_url ? " → " + r.j.html_url : " " + r.t.slice(0, 200)));
return (r.s === 200 || r.s === 201) ? 0 : 1;
}
async function cmdComments(api, opt, n) {
if (!n) { console.log("用法:comments <n> --repo=o/r [--dump=<文件>]"); return 1; }
const r = await api("/repos/" + opt.repo + "/issues/" + n + "/comments?limit=100");
if (r.s !== 200) { console.log("读取失败 status=" + r.s); return 1; }
const arr = Array.isArray(r.j) ? r.j : [];
if (opt.dump) {
const head = "# 工单 #" + n + " 评论备份(" + new Date().toISOString() + ")" + NL + NL +
"> 删除前全量备份(SKILL §8.1 第 5 条:删除前必须备份 · 删除 ≠ 丢失)。共 " + arr.length + " 条。" + NL;
fs.writeFileSync(opt.dump, head + arr.map(function (c) {
return NL + NL + "## comment " + c.id + "(" + String(c.body || "").length + " 字符)" + NL + NL + String(c.body || "");
}).join(NL), "utf8");
console.log("已备份 " + arr.length + " 条 → " + opt.dump);
}
console.log("#" + n + " 评论 " + arr.length + " 条(只应剩「必须可执行物」):");
for (const c of arr) {
const b = String(c.body || "");
console.log(" " + c.id + " " + b.length + " 字 首行: " + (b.split(NL)[0] || "").slice(0, 90));
}
return 0;
}
async function cmdCommentAdd(api, opt, n) {
if (!n || !opt["body-file"]) { console.log("用法:comment-add <n> --repo=o/r --body-file=<文件> [--dry-run]"); return 1; }
const body = fs.readFileSync(opt["body-file"], "utf8");
if (opt["dry-run"]) { console.log("[dry-run] 将给 #" + n + " 加评论 · " + body.length + " 字"); return 0; }
const r = await api("/repos/" + opt.repo + "/issues/" + n + "/comments", "POST", { body: body });
console.log("status=" + r.s + (r.s === 201 && r.j ? " → comment " + r.j.id : " " + r.t.slice(0, 200)));
return r.s === 201 ? 0 : 1;
}
async function cmdCommentDel(api, opt, id) {
if (!id) { console.log("用法:comment-del <commentId> --repo=o/r [--yes](不带 --yes 只预览)"); return 1; }
const g = await api("/repos/" + opt.repo + "/issues/comments/" + id);
const text = g.s === 200 ? String(g.j.body || "") : "";
console.log("comment " + id + (g.s === 200 ? "(" + text.length + " 字)" : "(读取失败 status=" + g.s + ",继续前请自行确认 id)"));
if (text) console.log(text.split(NL).slice(0, 6).map(function (l) { return " | " + l; }).join(NL));
if (!opt.yes) {
console.log("预览模式(未删除)。确认要删加 --yes;**删前先备份**:comments <n> --dump=<文件>(§8.1 第 5 条)。");
return 0;
}
const r = await api("/repos/" + opt.repo + "/issues/comments/" + id, "DELETE");
console.log("DELETE status=" + r.s);
return (r.s === 204 || r.s === 200) ? 0 : 1;
}
async function cmdRename(api, opt, n) {
const r = await api("/repos/" + opt.repo + "/issues/" + n);
if (r.s !== 200) { console.log("读取失败 status=" + r.s); return 1; }
const sp = splitTitle(r.j.title);
const dom = opt.domain;
if (!dom || DOMAINS.indexOf(dom) < 0) { console.log("用法:rename <n> --domain=<词表值> [--gate=等口径|等取证|等部署] [--dry-run]"); return 1; }
let inner = dom;
if (opt.gate && opt.gate !== "可开工") {
if (GATES.indexOf(opt.gate) < 0) { console.log("闸门不在词表:" + opt.gate); return 1; }
inner = dom + "-" + opt.gate;
}
const title = "【" + inner + "】" + sp.rest;
if (opt["dry-run"]) { console.log("[dry-run] " + r.j.title + " → " + title); return 0; }
const p = await api("/repos/" + opt.repo + "/issues/" + n, "PATCH", { title: title });
console.log("status=" + p.s + " → " + (p.j && p.j.title ? p.j.title : p.t.slice(0, 160)));
return p.s === 201 ? 0 : 1;
}
const USAGE = [
"用法:ticket.mjs <子命令> --repo=owner/name [...]",
" labels | milestones 读标签 / 里程碑",
" list [--state=open|closed|all] 列工单",
" verify <n...> 七项断言(有效性:标题/首行/闸门/自包含/引用块/标签)",
" assess <n> | assess --body-file=<文件> 结构评估(§8.0 落点表底稿:四段现状/缺段/空壳/评论清单)",
" create --title=… --body-file=… --label=<id>[,<id>] [--dry-run] [--force]",
" update <n> --body-file=… [--title=…] [--dry-run] [--force]",
" comments <n> [--dump=<文件>] 评论清单 / 删前全量备份",
" comment-add <n> --body-file=… [--dry-run]",
" comment-del <id> [--yes] 默认只预览",
" rename <n> --domain=<词表值> [--gate=…] [--dry-run]",
"(公共:--token-file=<文件> | GITEA_TOKEN · --host=<默认 code.hoteas.com>)"
].join(NL);
const A = parseArgs(process.argv.slice(2));
const cmd = A.pos[0];
if (!cmd || cmd === "-h" || cmd === "--help") { console.log(USAGE); process.exit(0); }
if (!A.opt.repo && !(cmd === "assess" && A.opt["body-file"])) { console.log("缺 --repo=owner/name" + NL + USAGE); process.exit(1); }
const api = mkApi(A.opt);
let code = 0;
if (cmd === "labels" || cmd === "milestones") code = await cmdRead(api, A.opt, cmd);
else if (cmd === "list") code = await cmdList(api, A.opt);
else if (cmd === "verify") code = await cmdVerify(api, A.opt, A.pos.slice(1));
else if (cmd === "assess") code = await cmdAssess(api, A.opt, A.pos[1]);
else if (cmd === "create") code = await cmdCreate(api, A.opt);
else if (cmd === "update") code = await cmdUpdate(api, A.opt, A.pos[1]);
else if (cmd === "comments") code = await cmdComments(api, A.opt, A.pos[1]);
else if (cmd === "comment-add") code = await cmdCommentAdd(api, A.opt, A.pos[1]);
else if (cmd === "comment-del") code = await cmdCommentDel(api, A.opt, A.pos[1]);
else if (cmd === "rename") code = await cmdRename(api, A.opt, A.pos[1]);
else { console.log("未知子命令:" + cmd + NL + USAGE); code = 1; }
process.exit(code);