fix(ticket-management): 参数两种写法都收 + 新增 selftest + 模板域分隔符改连字符
- bin/ticket.mjs: parseArgs 同时接受 --x=v 与 --x v(仅取值型选项,布尔开关不吃后一个参数); 新增 selftest 子命令(不联网、不需要令牌),覆盖解析器 3 条与断言正反 5 条 - SKILL.md: §5 命令块补 selftest 与「两种写法等价」;子命令表补 selftest 行; §6 留档 403/405 假故障的成因(示例写空格、实现只认等号);frontmatter description 补 selftest - templates/issue.md: 域层级分隔符 / → -(断言 3 按 - 取 L1,斜杠版照抄必挂) 验证:node bin/ticket.mjs selftest → 8/8,退出码 0
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# agents-skills
|
# agents-skills
|
||||||
|
|
||||||
> 可复用的 **agent 技能库**(执行方法,不含项目结论)。每个技能一个**顶层目录** + `SKILL.md`,因此本仓**可以直接作为技能根**(如 junction 到 `<项目>/.dsh/skills` 后由 DSH 发现)。
|
> 可复用的 **agent 技能库**(执行方法,不含项目结论)。每个技能一个**顶层目录** + `SKILL.md`,按 DSH 的发现规则逐个接到技能根下即可(见「使用」)。
|
||||||
|
|
||||||
## 技能清单
|
## 技能清单
|
||||||
|
|
||||||
@@ -16,11 +16,30 @@
|
|||||||
4. **工具要能自检**:脚本必须能在自己的仓里跑通一条最小用例。
|
4. **工具要能自检**:脚本必须能在自己的仓里跑通一条最小用例。
|
||||||
5. **令牌不落盘**:脚本从 `GITEA_TOKEN` 或 `--token-file` 读,**绝不把令牌写进仓库、URL 或提交**。
|
5. **令牌不落盘**:脚本从 `GITEA_TOKEN` 或 `--token-file` 读,**绝不把令牌写进仓库、URL 或提交**。
|
||||||
|
|
||||||
## 使用(把本仓接到项目)
|
## 使用(把本仓接到 DSH)
|
||||||
|
|
||||||
|
DSH 的技能发现**只有一层**:技能根下的 `<name>/SKILL.md` 或平铺 `<name>.md`,**不认**嵌套的 `**/SKILL.md`。所以要把**每个技能目录**接到技能根下,不能把本仓整体 junction 成 `<技能根>/agents-skills`(那层里没有 `SKILL.md`,会发现不到)。
|
||||||
|
|
||||||
|
技能根与优先级(同名时 rank 小者胜):
|
||||||
|
|
||||||
|
| rank | 来源 | 路径 |
|
||||||
|
|---|---|---|
|
||||||
|
| 100 | 项目 | `<项目根>/.dsh/skills` |
|
||||||
|
| 200 | 项目 | `<项目根>/.agents/skills` |
|
||||||
|
| 400 | 用户 | `~/.dsh/skills`(即 `$DSH_HOME/skills`) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 方式一:作为技能根之一(Windows 目录 junction)
|
# 方式一:用户级 —— 所有项目都可见(推荐;本机已按此接好 ticket-management)
|
||||||
New-Item -ItemType Junction -Path '<项目>/.dsh/skills/agents-skills' -Target '<本仓路径>'
|
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.dsh\skills"
|
||||||
# 方式二:只取一个技能
|
New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\skills\ticket-management" -Target '<本仓>\ticket-management'
|
||||||
Copy-Item -Recurse '<本仓>/ticket-management' '<项目>/.dsh/skills/'
|
|
||||||
|
# 方式二:项目级 —— 只在该项目可见
|
||||||
|
New-Item -ItemType Junction -Path '<项目>/.dsh/skills/ticket-management' -Target '<本仓>\ticket-management'
|
||||||
|
|
||||||
|
# 方式三:拷一份 —— 与本仓脱钩,之后改动不再同步
|
||||||
|
Copy-Item -Recurse '<本仓>\ticket-management' '<技能根>\'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
junction 指向本仓 ⇒ 在这里改 `SKILL.md`,DSH 的下一次目录扫描即生效:**正文编辑不用重启,也不用重新链接**。技能名取自 frontmatter 的 `name`;技能根下的新增 / 改名 / 删除由 DSH 的目录监视自动发现。
|
||||||
|
|
||||||
|
> 进阶:也可让本仓**整体**充当一个技能根(`@deepseek-ai/dsh-skill-filesystem` 的 `customSkillDirs`,rank 300),但要在部署侧给该插件配目录;默认预设不带这行配置。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: ticket-management
|
name: ticket-management
|
||||||
description: 用 Gitea 工单库做「人读 + 机读」双面管理 —— 标签零新增;标题用【问题域】前缀(闸门仅「不能动」时作第二段);正文**首行一行机读字段**(域/闸门/优先级/类型/模式/端/环境/证据);正文**自包含**(禁本地路径与内部分析编号,源码用仓库相对路径 + 该 ref 的 permalink);来源只能进**末尾机读引用块**(须写明 ref-owner)。含 bin/ticket.mjs 的 labels/milestones/list/verify/create/rename。适用于:建单、批量规范化既有工单标题、校验工单是否合规、用 API 读写 Gitea issue。
|
description: 用 Gitea 工单库做「人读 + 机读」双面管理 —— 标签零新增;标题用【问题域】前缀(闸门仅「不能动」时作第二段);正文**首行一行机读字段**(域/闸门/优先级/类型/模式/端/环境/证据);正文**自包含**(禁本地路径与内部分析编号,源码用仓库相对路径 + 该 ref 的 permalink);来源只能进**末尾机读引用块**(须写明 ref-owner)。含 bin/ticket.mjs 的 labels/milestones/list/verify/create/rename/selftest(自检不联网、不需要令牌)。适用于:建单、批量规范化既有工单标题、校验工单是否合规、用 API 读写 Gitea issue。
|
||||||
---
|
---
|
||||||
|
|
||||||
# ticket-management(Gitea 工单管理)
|
# ticket-management(Gitea 工单管理)
|
||||||
@@ -68,7 +68,11 @@ refs:
|
|||||||
## 5 工具 `bin/ticket.mjs`
|
## 5 工具 `bin/ticket.mjs`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 自检:不联网、不需要令牌,改完脚本先跑这条
|
||||||
|
node bin/ticket.mjs selftest
|
||||||
|
|
||||||
# 令牌:GITEA_TOKEN 环境变量,或 --token-file=<含 key=value 的文件>(不落盘、不回显)
|
# 令牌:GITEA_TOKEN 环境变量,或 --token-file=<含 key=value 的文件>(不落盘、不回显)
|
||||||
|
# 取值型选项两种写法等价:--repo=xbc/xbc 与 --repo xbc/xbc 都能用
|
||||||
node bin/ticket.mjs labels --repo xbc/xbc
|
node bin/ticket.mjs labels --repo xbc/xbc
|
||||||
node bin/ticket.mjs milestones --repo xbc/xbc
|
node bin/ticket.mjs milestones --repo xbc/xbc
|
||||||
node bin/ticket.mjs list --repo xbc/xbc --state=open
|
node bin/ticket.mjs list --repo xbc/xbc --state=open
|
||||||
@@ -83,12 +87,14 @@ node bin/ticket.mjs rename 142 --repo xbc/xbc --domain=平台 [--gate=等口径]
|
|||||||
| `verify <n>` | **校验**(只读):见 §7 六项断言 | `read:issue` |
|
| `verify <n>` | **校验**(只读):见 §7 六项断言 | `read:issue` |
|
||||||
| `create` | 建单(**先按标题查重**) | `write:issue` |
|
| `create` | 建单(**先按标题查重**) | `write:issue` |
|
||||||
| `rename <n>` | 规范化标题(重建前缀,描述不动) | `write:issue` |
|
| `rename <n>` | 规范化标题(重建前缀,描述不动) | `write:issue` |
|
||||||
|
| `selftest` | **自检**:解析器两种写法 + §7 七项断言的正反样例;不联网、不需要令牌 | 无 |
|
||||||
|
|
||||||
## 6 常见坑(实测)
|
## 6 常见坑(实测)
|
||||||
|
|
||||||
- **HTTP 分块解码**:`let d = ''; res.on('data', c => d += c)` 会把**跨块的多字节字符**打成 `U+FFFD`(中文标签名就被打坏过)⇒ 必须 `res.setEncoding('utf8')`,或用 `fetch` 的 `text()`。
|
- **HTTP 分块解码**:`let d = ''; res.on('data', c => d += c)` 会把**跨块的多字节字符**打成 `U+FFFD`(中文标签名就被打坏过)⇒ 必须 `res.setEncoding('utf8')`,或用 `fetch` 的 `text()`。
|
||||||
- **建单前必须查重**:按标题查,否则会重复建单。
|
- **建单前必须查重**:按标题查,否则会重复建单。
|
||||||
- **标签传 id**:`POST /issues` 的 `labels` 是**标签 id 数组**,不是名字。
|
- **标签传 id**:`POST /issues` 的 `labels` 是**标签 id 数组**,不是名字。
|
||||||
|
- **参数写法的坑(已修,留档)**:早先 `parseArgs` 只认 `--repo=o/r`,而本文档示例写的是 `--repo o/r` ⇒ `xbc/xbc` 被当成位置参数、`repo` 变成 `true`,请求打到 `/repos/true/...`,报出来的是 403「缺 read 权限」与 405「方法不允许」——**全是假故障**,白排查了三轮。现在取值型选项两种写法都收;改动仍以 `selftest` 兜底。
|
||||||
- **批量改首行时必须保持 ` | ` 形态**(空格 + 竖线 + 空格):把 `域=…` 换成新值时若顺手吃掉它后面的空格,`闸门=` 会与 `域` 黏成一段 ⇒ **断言 4 全线 FAIL**(实测 14/14 中招,工具拦住了)。
|
- **批量改首行时必须保持 ` | ` 形态**(空格 + 竖线 + 空格):把 `域=…` 换成新值时若顺手吃掉它后面的空格,`闸门=` 会与 `域` 黏成一段 ⇒ **断言 4 全线 FAIL**(实测 14/14 中招,工具拦住了)。
|
||||||
- **改标题与正文**:都是 `PATCH /repos/{owner}/{repo}/issues/{n}`。
|
- **改标题与正文**:都是 `PATCH /repos/{owner}/{repo}/issues/{n}`。
|
||||||
- **令牌 scope**:只读 `read:issue,read:repository` 建不了单(403,报错会写 required scope);建 / 改需 `write:issue`;`GET /user` 另需 `read:user`。
|
- **令牌 scope**:只读 `read:issue,read:repository` 建不了单(403,报错会写 required scope);建 / 改需 `write:issue`;`GET /user` 另需 `read:user`。
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ const NL = String.fromCharCode(10);
|
|||||||
const BACKSLASH = String.fromCharCode(92);
|
const BACKSLASH = String.fromCharCode(92);
|
||||||
const BAD = ["codes/", "分析/数据/", "分析/D-", "分析/E-", "分析/A-", "分析/B-", "分析/C-", "分析/F-", "分析/_tmp", "分析/_archive", "分析/README", "86 §", "80 · V", "/home/", "C:" + BACKSLASH];
|
const BAD = ["codes/", "分析/数据/", "分析/D-", "分析/E-", "分析/A-", "分析/B-", "分析/C-", "分析/F-", "分析/_tmp", "分析/_archive", "分析/README", "86 §", "80 · V", "/home/", "C:" + BACKSLASH];
|
||||||
|
|
||||||
|
// 取值型选项:`--repo=o/r` 与 `--repo o/r` 两种写法都收(SKILL.md 的示例用的是后者)
|
||||||
|
const VALUE_OPTS = ["repo", "title", "body-file", "label", "milestone", "token-file", "host", "state", "limit", "domain", "gate"];
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const pos = [], opt = {};
|
const pos = [], opt = {};
|
||||||
for (const a of argv) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
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); }
|
const a = argv[i];
|
||||||
else pos.push(a);
|
if (!a.startsWith("--")) { pos.push(a); continue; }
|
||||||
|
const j = a.indexOf("=");
|
||||||
|
if (j >= 0) { opt[a.slice(2, j)] = a.slice(j + 1); continue; }
|
||||||
|
const k = a.slice(2), next = argv[i + 1];
|
||||||
|
if (VALUE_OPTS.indexOf(k) >= 0 && next !== undefined && !next.startsWith("--")) { opt[k] = next; i++; }
|
||||||
|
else opt[k] = true;
|
||||||
}
|
}
|
||||||
return { pos: pos, opt: opt };
|
return { pos: pos, opt: opt };
|
||||||
}
|
}
|
||||||
@@ -62,6 +69,29 @@ function checksOf(title, body) {
|
|||||||
["6 有引用块 ⇒ 有 ref-owner", body.indexOf("<!--") < 0 ? true : body.indexOf("ref-owner:") >= 0]
|
["6 有引用块 ⇒ 有 ref-owner", body.indexOf("<!--") < 0 ? true : body.indexOf("ref-owner:") >= 0]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
// 自检(不联网、不需要令牌):node bin/ticket.mjs selftest
|
||||||
|
function cmdSelftest() {
|
||||||
|
let pass = 0, fail = 0;
|
||||||
|
const t = function (name, ok) { if (ok) { pass++; console.log(" PASS " + name); } else { fail++; console.log(" FAIL " + name); } };
|
||||||
|
const p1 = parseArgs(["list", "--repo", "xbc/xbc", "--state=open"]);
|
||||||
|
t("空格写法 --repo xbc/xbc", p1.opt.repo === "xbc/xbc" && p1.pos[0] === "list" && p1.opt.state === "open");
|
||||||
|
const p2 = parseArgs(["verify", "142", "--repo=xbc/xbc"]);
|
||||||
|
t("等号写法 --repo=xbc/xbc", p2.opt.repo === "xbc/xbc" && p2.pos[1] === "142");
|
||||||
|
const p3 = parseArgs(["create", "--repo=xbc/xbc", "--dry-run"]);
|
||||||
|
t("布尔开关不吃后面的话", p3.opt["dry-run"] === true && p3.opt.repo === "xbc/xbc");
|
||||||
|
const ok = checksOf("【报表-等口径】报表未按经营模式分列", "域=报表-统计-模式分列 | 闸门=等口径 | 优先级=本迭代 | 类型=数据 | 模式=改造 | 端=后端 | 环境=测服 | 证据=E1" + NL + NL + "正文");
|
||||||
|
t("合规样例 6/6", ok.every(function (c) { return c[1]; }));
|
||||||
|
const b1 = checksOf("【报表】报表未按经营模式分列", "域=报表-统计-模式分列 | 闸门=可开工 | 优先级=本迭代" + NL + NL + "见 codes/foo.go");
|
||||||
|
t("标题无闸门 + 首行 闸门=可开工 ⇒ 断言 4 通过", b1[3][1] === true);
|
||||||
|
t("人读部分含禁用前缀 ⇒ 断言 5 失败", b1[4][1] === false);
|
||||||
|
const b2 = checksOf("【报表-等口径】x", "域=报表-统计-模式分列 | 闸门=可开工");
|
||||||
|
t("标题带闸门但首行 闸门=可开工 ⇒ 断言 4 失败", b2[3][1] === false);
|
||||||
|
const b3 = checksOf("【报表】x", "域=报表-统计-模式分列" + NL + NL + "<!-- refs:");
|
||||||
|
t("有引用块无 ref-owner ⇒ 断言 6 失败", b3[5][1] === false);
|
||||||
|
console.log("自检 " + pass + "/" + (pass + fail));
|
||||||
|
return fail === 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
async function findTitle(api, repo, title) {
|
async function findTitle(api, repo, title) {
|
||||||
const r = await api("/repos/" + repo + "/issues?state=all&type=issues&limit=100");
|
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; });
|
return (Array.isArray(r.j) ? r.j : []).filter(function (x) { return String(x.title || "") === title; });
|
||||||
@@ -102,7 +132,7 @@ async function cmdVerify(api, opt, nums) {
|
|||||||
}
|
}
|
||||||
async function cmdCreate(api, opt) {
|
async function cmdCreate(api, opt) {
|
||||||
const title = opt.title;
|
const title = opt.title;
|
||||||
if (!title) { console.log("用法:create --repo=o/r --title=... --body-file=... [--label=<id>] [--dry-run] [--force]"); return 1; }
|
if (!title) { console.log("用法:create --repo=o/r(或 --repo o/r)--title=... --body-file=... [--label=<id>] [--dry-run] [--force]"); return 1; }
|
||||||
const sp = splitTitle(title);
|
const sp = splitTitle(title);
|
||||||
if (!sp.parts.length || DOMAINS.indexOf(sp.parts[0]) < 0) { console.log("拒绝:标题主前缀不合规 —— " + title); return 1; }
|
if (!sp.parts.length || DOMAINS.indexOf(sp.parts[0]) < 0) { console.log("拒绝:标题主前缀不合规 —— " + title); return 1; }
|
||||||
const dup = await findTitle(api, opt.repo, title);
|
const dup = await findTitle(api, opt.repo, title);
|
||||||
@@ -144,7 +174,8 @@ async function cmdRename(api, opt, n) {
|
|||||||
|
|
||||||
const A = parseArgs(process.argv.slice(2));
|
const A = parseArgs(process.argv.slice(2));
|
||||||
const cmd = A.pos[0];
|
const cmd = A.pos[0];
|
||||||
if (!cmd || cmd === "-h" || cmd === "--help") { console.log("用法:ticket.mjs <labels|milestones|list|verify|create|rename> --repo=owner/name [...]"); process.exit(0); }
|
if (cmd === "selftest") process.exit(cmdSelftest());
|
||||||
|
if (!cmd || cmd === "-h" || cmd === "--help") { console.log("用法:ticket.mjs <labels|milestones|list|verify|create|rename|selftest> [--repo=owner/name | --repo owner/name] [...]"); process.exit(0); }
|
||||||
if (!A.opt.repo) { console.log("缺 --repo=owner/name"); process.exit(1); }
|
if (!A.opt.repo) { console.log("缺 --repo=owner/name"); process.exit(1); }
|
||||||
const api = mkApi(A.opt);
|
const api = mkApi(A.opt);
|
||||||
let code = 0;
|
let code = 0;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
域=<L1>/<L2>/<L3> | 闸门=<可开工|等口径|等取证|等部署> | 优先级=<立即|本迭代|排队> | 类型=<安全|数据|性能|可用|债务> | 模式=<…|全模式> | 端=<后端|前端|小程序|运维> | 环境=<正式服|测服> | 证据=<E1|E2|需生产取证>
|
域=<L1>-<L2>-<L3> | 闸门=<可开工|等口径|等取证|等部署> | 优先级=<立即|本迭代|排队> | 类型=<安全|数据|性能|可用|债务> | 模式=<…|全模式> | 端=<后端|前端|小程序|运维> | 环境=<正式服|测服> | 证据=<E1|E2|需生产取证>
|
||||||
|
(域内层级用**半角连字符**,不要用斜杠——断言 3 取 `域` 的 `-` 前第一段与标题主域比对,斜杠会让它整段不匹配)
|
||||||
|
|
||||||
**现象**:<一句话说清「谁在什么场景下会看到什么坏结果」>
|
**现象**:<一句话说清「谁在什么场景下会看到什么坏结果」>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user