@nathan33/nao-skill 0.13.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/checklists/architecture-designer.md +3 -1
- package/.agents/checklists/comm-templates.md +4 -1
- package/.agents/checklists/deliverable-docs.md +2 -1
- package/.agents/checklists/pm.md +3 -1
- package/.agents/checklists/qa.md +8 -3
- package/.agents/checklists/rd-be.md +2 -1
- package/.agents/checklists/rd-fe.md +3 -1
- package/.agents/common/intercom-protocol.md +3 -5
- package/.agents/prompts/architecture-designer.md +4 -4
- package/.agents/prompts/backend-developer.md +4 -4
- package/.agents/prompts/frontend-developer.md +4 -4
- package/.agents/prompts/infra-engineer.md +3 -3
- package/.agents/prompts/product-manager.md +9 -12
- package/.agents/prompts/test-engineer.md +10 -7
- package/.agents/scripts/nao-fleet.sh +511 -83
- package/.agents/skills/github-flow.md +1 -0
- package/.agents/skills/nao-fleet/SKILL.md +3 -0
- package/.agents/skills/pm-operations.md +1 -0
- package/.agents/skills/pm-routing.md +1 -1
- package/.agents/skills/test-design.md +18 -0
- package/.agents/templates/user-acceptance.md.example +68 -0
- package/README.md +30 -30
- package/bin/nao-skill.js +2 -2
- package/package.json +1 -1
- package/.agents/scripts/qq-notify +0 -199
- package/.agents/skills/qq-notify.md +0 -87
|
@@ -34,6 +34,7 @@ PM 在**立项 / 终签 / 发布**时读取;RD 在**起分支 / 开 PR / 合
|
|
|
34
34
|
- **禁止** `git push` 到 main、**禁止绕过 PR 合并**;feature 分支允许 `--force-with-lease`(改基 / 压缩历史时)。
|
|
35
35
|
- 提交规范、`wip()` 前缀、路径级暂存、可读性:见 @.agents/skills/commit.md。
|
|
36
36
|
- **squash 后 main 上的提交信息 = PR 标题 + PR 正文** → **可读性硬性落在 PR 标题**(用户可见行为,禁纯编号/类名/路径)。
|
|
37
|
+
- ⚠️ **单提交 PR 的 squash 陷阱(T511 实测 3/3 仓中招)**:`gh pr merge --squash` 在 PR **只有 1 条提交**时,取的是**该提交的 subject**(**不是** PR 标题)⇒ 分支纪律的 `wip(<编号>):` 前缀会直接进主干。**合并命令必须显式指定**:`gh pr merge <n> --squash --delete-branch --subject "<用户可读主题>" --body "<摘要>"`;或合并前把分支压成一条用户可读提交。主干受保护(禁 force-push / `enforce_admins=true`)时该偏差**不可回溯**,只能记录。
|
|
37
38
|
|
|
38
39
|
## Issue 与状态(避免双源)
|
|
39
40
|
|
|
@@ -31,6 +31,9 @@ nao 是一套「多角色 AI 开发舰队」:6 个分工明确的常驻会话
|
|
|
31
31
|
2. **拉起舰队**:`bash .agents/scripts/nao-fleet.sh ensure pm arch-designer rd-fe rd-be qa rd-infra`
|
|
32
32
|
- 任务派生(并行隔离):`ensure --task <编号> <别名>`
|
|
33
33
|
- 状态 / 回收:`status` / `close --task <编号> <别名>`
|
|
34
|
+
- 实验场清理:`cleanup -S <隔离socket>`(强制显式 socket;无 `-S`/指向默认 socket ⇒ exit 2 拒绝)
|
|
35
|
+
⚠ 实验纪律:实验/夹具一律 `env -u TMUX` + `-S <隔离socket>`——tmux 客户端在 `$TMUX` 已设时
|
|
36
|
+
无视 `TMUX_TMPDIR`,裸 `tmux`/`kill-server` 会连坐真实 server(T515 两次自灭根因)
|
|
34
37
|
3. **迁移旧版安装**:`npx @nathan33/nao-skill migrate`(旧文件备份到 `.agents/.nao-obsolete/`)。是否写 shim 由**判定式**决定(旧脚本 / 已有 shim / 全仓 `nao-fleet|nao-skill|NAO_SKILLS` 引用 ⇒ 写);纯文档仓判定为跳过并显式告知,可用 `--shim` / `--no-shim` 覆盖。
|
|
35
38
|
|
|
36
39
|
## 硬约束(勿违反)
|
|
@@ -34,6 +34,7 @@ description: 按需技能——PM 运行手册(调度细则 / 开工确认卡
|
|
|
34
34
|
| T1 | rd-be | 后端 | ... | AC1/AC3 |
|
|
35
35
|
|
|
36
36
|
分支:`feat/<issue-id>-<slug>`(降级 `nao/<批次-slug>`)· PR owner:<角色> · Reviewer:<arch-designer / 无> · 预览环境:<URL / 无>
|
|
37
|
+
QA 测试范围(派发无人值守时按此执行;用户在场时由 QA 直接询问用户):<列出范围 / 无 QA>
|
|
37
38
|
|
|
38
39
|
建议顺序:**QA 先行用例 → 前后端并行**。
|
|
39
40
|
|
|
@@ -19,7 +19,7 @@ description: 按需技能——PM 路由与派单纪律(角色路由口诀 /
|
|
|
19
19
|
|
|
20
20
|
> 依据:TASK-23 越界调研真实案例——PM 亲自读仓储实现 / sweep 机制 / 服务端 Go 源码 / 限流桶与排序语义,据此形成**错误前提**(误判服务端字段不可用),用户在错误前提下多做一轮选择,最终由 arch 以更硬理由拉回。**越界调研的实际成本 = 无效决策轮次。**
|
|
21
21
|
|
|
22
|
-
- **PM 可自做(简略调研)**:① 粗略分析任务设计的**技术深度**(供范围划分与派发,不下结论);② UI/文案/样式/交互的**落点确认**(组件在哪、有无可复用件、i18n
|
|
22
|
+
- **PM 可自做(简略调研)**:① 粗略分析任务设计的**技术深度**(供范围划分与派发,不下结论);② UI/文案/样式/交互的**落点确认**(组件在哪、有无可复用件、i18n 键、插槽/属性支持)——**仅限 Presentation/UI 层定位**;Domain / Infra / 服务端 / 构建配置一律转角色(见下「必须转角色」);③ 读 `docs/prds` / `docs/adr` / `docs/tasks-state.md` 等**决策留痕**;④ 判断任务编号与范围划分;⑤ **产品形态业界调研**(同类功能有哪些功能点 / 属性 / 组织结构 / 视图 / 协作 / 生命周期;清单与检索纪律见 @.agents/skills/research.md)——只作**参考**,不得据此下技术结论。
|
|
23
23
|
- **必须转角色(任一命中即转,PM 不自行下结论)**:
|
|
24
24
|
1. 出现「哪个方案更好」的**取舍**;
|
|
25
25
|
2. 需读 `packages/domain-*` / `packages/infrastructure` / 服务端 / 构建配置 / 同步链;
|
|
@@ -31,8 +31,26 @@ description: 按需技能——用例设计、缺陷管理、性能安全、DDD
|
|
|
31
31
|
| presentation | VTU / Testing Library | 组件渲染与交互 |
|
|
32
32
|
| E2E | Playwright / Cypress | 关键路径、跨模块流程 |
|
|
33
33
|
|
|
34
|
+
## 视觉/UI 呈现验证(用户验收清单,不做浏览器自动化)
|
|
35
|
+
|
|
36
|
+
> 视觉呈现(风格/气质/体感)本质是**人眼判断**:不做无头浏览器/截图对比等视觉自动化——由 QA 产出**用户验收清单**移交用户人工执行(模板 `templates/user-acceptance.md.example`)。读者 = 最终用户,禁内部术语/自动化断言。
|
|
37
|
+
|
|
38
|
+
- **最低覆盖集**(按需求范围取用,不适用项写「不适用 + 理由」):
|
|
39
|
+
1. **视觉一致性**:新页面/组件与既有风格一致(tokens/组件库/既有风格模式,见 frontend-ddd-details「UI/UX 落地」)
|
|
40
|
+
2. **四态**:加载 / 空 / 错误 / 成功
|
|
41
|
+
3. **响应式**:关键断点下不破版
|
|
42
|
+
4. **键盘可达**:focus 可见、可 Tab 遍历
|
|
43
|
+
5. **对比度**:文本/交互元素达标
|
|
44
|
+
6. **`prefers-reduced-motion`**:动效尊重系统偏好
|
|
45
|
+
7. **AI 默认样式抽查**:frontend-design calibration 7 类(奶油底衬线大标题 / 全圆角灰阴影卡片 / 居中渐变 hero 等)
|
|
46
|
+
- **每项四要素**:编号 · 对应 AC · 测什么(一句话)· 怎么操作(可执行步骤)· 预期结果(用户可判断的具体现象)。
|
|
47
|
+
- **落与移交**:落 `docs/reports/<编号>-user-acceptance.md`;QA 回执时附路径;PM 在验收环节安排用户执行(用户在场直接交用户;无人值守留盘待取)。
|
|
48
|
+
- **回填闭环**:用户逐项回填 通过 / 不通过(附理由或截图,截图可选)→ QA 据回填**关闭缺陷**(通过)或 **Reopen/新增**(不通过)→ 仍走既有缺陷生命周期。
|
|
49
|
+
- **边界**:功能/交互 E2E(Playwright / Cypress)自动化**保留**,不受本节影响;`ui-tokens-check.sh` 为 rd-fe 代码级令牌静态扫描(交付自查),不在本节。
|
|
50
|
+
|
|
34
51
|
## 缺陷管理
|
|
35
52
|
|
|
53
|
+
- **落点(入库)**:缺陷全量落 `docs/reports/<编号>-defects.md`(编号 = 任务编号,与测试报告同批次);项目已有缺陷平台(如 GitHub Issues 标签)时用平台,报告内只留指针。
|
|
36
54
|
- 生命周期:New → Open → Fixed → Verify → Closed / Reopen
|
|
37
55
|
- 严重度:致命 / 严重 / 一般 / 轻微
|
|
38
56
|
- 优先级:P0 / P1 / P2 / P3
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 用户验收清单 <编号>(视觉 / UI 呈现类)
|
|
2
|
+
|
|
3
|
+
> 说人话版本:这份清单是给你(最终用户)看的——每个测试项都写了 **测什么 / 怎么操作 / 预期看到什么**,你按步骤走一遍,勾结果就行。
|
|
4
|
+
> 流程:QA 产出 → PM 审 → 你执行回填 → QA 据回填关闭 / 重开缺陷。
|
|
5
|
+
> 回填:每项勾 [x] 通过 或 [x] 不通过(不通过请附一句「你看到什么」;截图可选)。
|
|
6
|
+
|
|
7
|
+
## 执行说明
|
|
8
|
+
|
|
9
|
+
- 环境:<预览地址 / 本地启动命令 / 分支或版本>
|
|
10
|
+
- 版本 / commit:<hash 或版本号>
|
|
11
|
+
- 预计耗时:约 <N> 分钟
|
|
12
|
+
- 检查项按编号顺序做即可;还没准备好的可不做并写「跳过」。
|
|
13
|
+
|
|
14
|
+
## 检查项
|
|
15
|
+
|
|
16
|
+
1. **视觉一致性**(对应 AC<n>)
|
|
17
|
+
- 测什么:新页面 / 组件与项目既有风格一致(颜色、间距、圆角、阴影、字体)。
|
|
18
|
+
- 怎么操作:打开 <页面 / 组件位置>,把它和同屏既有 <参照组件> 并排看。
|
|
19
|
+
- 预期结果:颜色用同一套令牌,间距节奏、圆角阴影与既有组件一致;不出现一屏两种风格。
|
|
20
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
21
|
+
|
|
22
|
+
2. **四态覆盖**(对应 AC<n>)
|
|
23
|
+
- 测什么:加载 / 空 / 错误 / 成功 四种状态都有且不突兀。
|
|
24
|
+
- 怎么操作:
|
|
25
|
+
- 加载:<如何触发慢请求 / 断网看 loading>
|
|
26
|
+
- 空:<如何构造无数据场景>
|
|
27
|
+
- 错误:<如何触发失败与错误提示>
|
|
28
|
+
- 成功:<完成一次主操作看成功反馈>
|
|
29
|
+
- 预期结果:四态都有明确视觉反馈,无白屏 / 无裸报错;空态有引导文案。
|
|
30
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
31
|
+
|
|
32
|
+
3. **响应式**(对应 AC<n>)
|
|
33
|
+
- 测什么:窄屏 / 常规 / 宽屏下不破版。
|
|
34
|
+
- 怎么操作:把窗口拉到 <约 360px / 1280px / 1920px> 各看一次 <页面>。
|
|
35
|
+
- 预期结果:内容不重叠、不横向溢出、按钮不被挤压到不可点。
|
|
36
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
37
|
+
|
|
38
|
+
4. **键盘可达**(对应 AC<n>)
|
|
39
|
+
- 测什么:不用鼠标也能走完主流程。
|
|
40
|
+
- 怎么操作:只按 Tab / Shift+Tab 在 <页面> 上移动焦点,按 Enter / Space 触发。
|
|
41
|
+
- 预期结果:焦点有可见轮廓,能按顺序到达所有可交互元素。
|
|
42
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
43
|
+
|
|
44
|
+
5. **对比度**(对应 AC<n>)
|
|
45
|
+
- 测什么:文字与背景对比够,能看清。
|
|
46
|
+
- 怎么操作:目视 <正文 / 浅色文字区域>;拿不准可放大观看。
|
|
47
|
+
- 预期结果:正文/按钮文字清晰可读,无「灰字贴灰底」。
|
|
48
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
49
|
+
|
|
50
|
+
6. **动效偏好**(对应 AC<n>)
|
|
51
|
+
- 测什么:开启了「减弱动态效果」后动效收敛。
|
|
52
|
+
- 怎么操作:系统设置开启 <减少动态效果 / prefers-reduced-motion>,再看 <页面>。
|
|
53
|
+
- 预期结果:没有大范围位移 / 闪烁动画,页面可正常使用。
|
|
54
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
55
|
+
|
|
56
|
+
7. **AI 默认样式抽查**(对应 AC<n>、按范围可选)
|
|
57
|
+
- 测什么:页面不像「模板生成」的(奶油底+衬线大标题、全套圆角灰阴影卡片、居中渐变 hero 等)。
|
|
58
|
+
- 怎么操作:通看一遍 <页面>,对照 frontend-design「calibration」清单。
|
|
59
|
+
- 预期结果:视觉是有针对本产品做的选择,不是通用默认模板。
|
|
60
|
+
- 回填:[ ] 通过 [ ] 不通过(备注:____)
|
|
61
|
+
|
|
62
|
+
> 不适用项:<列出并写一句理由;或写「无」>
|
|
63
|
+
|
|
64
|
+
## 回填汇总
|
|
65
|
+
|
|
66
|
+
- 通过:<N> 项 · 不通过:<M> 项 · 跳过/不适用:<K> 项
|
|
67
|
+
- 不通过项给 QA/RD 的一句话结论:<你最在意的问题 1–2 条>
|
|
68
|
+
- 执行人 / 日期:<你> / <YYYY-MM-DD>
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ nao-skills 是一套 **角色卡 + 协作规矩 + 小工具**。装上以后,
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
cd 你的项目
|
|
27
|
-
pi install --local npm:@nathan33/nao-skill@0.
|
|
27
|
+
pi install --local npm:@nathan33/nao-skill@0.16.1 # 声明 pin + 把机制包物化到项目内 .pi/npm
|
|
28
28
|
npx @nathan33/nao-skill init # 写入转发 shim + 生成 AGENTS.md
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -36,13 +36,7 @@ nao-skills 是一套 **角色卡 + 协作规矩 + 小工具**。装上以后,
|
|
|
36
36
|
|
|
37
37
|
升级:改 `.pi/settings.json` 里的 pin 版本,再 `pi update --extensions`(机制以包为准,项目里只有 shim)。
|
|
38
38
|
|
|
39
|
-
**老项目迁移**(曾在项目里铺开整套 `.agents/`
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npx @nathan33/nao-skill migrate # 旧文件备份到 .agents/.nao-obsolete/,再按需写入转发入口
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
迁移默认按**判定式**决定是否写转发入口(shim):仓里还有旧的 `nao-fleet.sh`、或已存在 shim、或全仓有 `nao-fleet` / `nao-skill` / `NAO_SKILLS` 引用,就写;**无脚本、无引用的纯文档仓会被判定跳过**,并打印一行告知。要改变判定结果,显式加 `--shim`(强制写)或 `--no-shim`(强制不写),两者不能同时给(`exit 2`)。旧版的 `.agents/.nao-migrated` 标记**已废弃**:迁移不再读写它,可安全删除。`.agents/.nao-version` 的语义是「本机已安装机制包版本」(供升级检测),**不表达迁移状态**——迁移状态由「是否还有旧资产」推断。
|
|
39
|
+
**老项目迁移**(曾在项目里铺开整套 `.agents/` 的):执行 `npx @nathan33/nao-skill migrate` 即可,细则见文末「老用户迁移与维护者注记」。
|
|
46
40
|
|
|
47
41
|
## 用起来(3 步)
|
|
48
42
|
|
|
@@ -53,6 +47,8 @@ npx @nathan33/nao-skill migrate # 旧文件备份到 .agents/.nao-obsolete/,
|
|
|
53
47
|
|
|
54
48
|
角色名可以写**别名**或**规范名**(两者等价):`arch` = `arch-designer`、`infra` = `rd-infra`;上例里两者混用也照常拉起。完整别名表见 `.agents/roles.yaml` 或 `nao-fleet.sh --help`。
|
|
55
49
|
|
|
50
|
+
**不在 tmux 里也能用**:默认每个角色开一个**看得见的终端窗口**(走 `xdg-terminal-exec`,窗口标识 `org.nao.<角色>`),不是后台黑箱;无图形会话(如 SSH)默认拒绝,需显式 `--background` 才建后台会话,并打印可复制的 `tmux attach` 命令。宿主的完整选择顺序与降级边界见 `nao-fleet.sh --help`。不过我们**推荐装 tmux**——多数能力在 tmux 宿主下体验最完整,理由见下方常见问题。
|
|
51
|
+
|
|
56
52
|
然后**在 PM 会话里说需求**就行,剩下的它自己流转。大概长这样:
|
|
57
53
|
|
|
58
54
|
```text
|
|
@@ -78,17 +74,6 @@ PM:在 PR 核 AC 并评论 → 验收通过(授权合并)→ rd-be squash
|
|
|
78
74
|
|
|
79
75
|
不想要哪个角色,删掉对应文件即可;只留 PM + 一个研发角色也能跑。
|
|
80
76
|
|
|
81
|
-
## 可选:关键节点 QQ 主动推送
|
|
82
|
-
|
|
83
|
-
装上 [`pi-agent-qqbot`](https://github.com/Nathan3303) 并配好 `~/.pi/agent/pi-agent-qqbot.json`(`appId` / `clientSecret` / `ownerOpenId` / `sandbox`)后,PM 可把**关键节点**主动推到你 QQ——批次进度汇总、验收结论、发版或合并完成、异常阻塞:
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
$NAO_SKILLS/.agents/scripts/qq-notify "[T301] 验收通过 · PR #4 已合并 · v0.8.0"
|
|
87
|
-
$NAO_SKILLS/.agents/scripts/qq-notify --dry-run "连通性自检" # 只取 token、不发送
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**为什么默认不主动打扰**:这是**可选能力**——没配 `pi-agent-qqbot` 就自动视为未启用;四类节点之外不发,一个节点最多一条(禁刷屏);推送失败只记回执行风险项,**不阻断交付**。默认走 sandbox(测试环境),正式发布通知才考虑 prod。能力边界、退出码与纪律见 `$NAO_SKILLS/.agents/skills/qq-notify.md`。
|
|
91
|
-
|
|
92
77
|
## 为什么它省 token(这是本项目的核心追求)
|
|
93
78
|
|
|
94
79
|
和 AI 聊天,钱花在「它每轮要读多少字」。所以:
|
|
@@ -108,7 +93,16 @@ $NAO_SKILLS/.agents/scripts/qq-notify --dry-run "连通性自检" # 只取
|
|
|
108
93
|
|
|
109
94
|
**它会在 GitHub 上开 PR 吗?** 会——每个需求一条分支、一个 PR(Draft 早开、CI 早跑),你确认验收后由研发角色 squash 合并,main 上只留 1 条可读提交。没有远端或 `gh` 时降级为本地分支,流程不变。
|
|
110
95
|
|
|
111
|
-
**跑完的会话要留着吗?** 后台派生会话验收完可以回收:`nao-fleet.sh close --task T1 rd-be`(等价写法 `nao-fleet.sh close rd-be-T1`,直接用派生会话名即可);常驻角色留着复用。`nao-fleet.sh status` 会提示哪些是残留。目标在别的项目里也能回收——`close` 会按名册/窗口标题跨项目定位;定位不到时会**非 0
|
|
96
|
+
**跑完的会话要留着吗?** 后台派生会话验收完可以回收:`nao-fleet.sh close --task T1 rd-be`(等价写法 `nao-fleet.sh close rd-be-T1`,直接用派生会话名即可);常驻角色留着复用。`nao-fleet.sh status` 会提示哪些是残留。目标在别的项目里也能回收——`close` 会按名册/窗口标题跨项目定位;定位不到时会**非 0 退出并打印诊断**,不会假装已回收。实验/夹具用的隔离 tmux server 用 `nao-fleet.sh cleanup -S <socket>` 清理:**强制显式 socket**,不带 `-S` 或指向生产默认 socket 一律 exit 2 拒绝,绝不会连坐你正在用的真实 server。
|
|
97
|
+
|
|
98
|
+
**推荐用 tmux 吗?** **推荐**——装上 tmux 后,舰队机制的使用体验最完整:
|
|
99
|
+
|
|
100
|
+
- **布局可控**:6 个角色会话在 tmux 里按三种布局自动排布(`main-row2` / `main-col` / `grid`),间距与列宽可用 `NAO_TMUX_*` 变量调,不会把桌面铺满一圈窗口。
|
|
101
|
+
- **后台会话可回看**:后台派生的会话随时 `tmux attach` 进去看,不挑图形环境(SSH 无图形会话也能用)。
|
|
102
|
+
- **定位回收更可靠**:`status` 查残留、`close` 跨项目回收,都是按会话名册与窗口标题定位的——tmux 宿主下窗口标题稳定可寻,误判和找不到的概率最低。
|
|
103
|
+
- **多种宿主探测路径**:机制里 tmux 属首选阶梯;非 tmux 宿主走外部终端窗口,探测与降级链路更长。
|
|
104
|
+
|
|
105
|
+
没装 tmux 也能正常用(见「不在 tmux 里也能用」),只是上述便利项会依次降级。
|
|
112
106
|
|
|
113
107
|
**终端窗口太挤怎么办?** tmux 宿主支持三种布局:默认 `main-row2`(主会话占左,其余往右分列)、`main-col`(其余在右列竖排)、`grid`(等大网格)。窗口太窄时 `main-row2` 会**自动回退** `main-col`、仍不够再退 `grid`,并打印带数字的提示(不中断拉起);用 `NAO_TMUX_LAYOUT` / `NAO_TMUX_MAIN_WIDTH` / `NAO_TMUX_MIN_PANE_WIDTH` 调整,详见 `bash .agents/scripts/nao-fleet.sh --help`。
|
|
114
108
|
|
|
@@ -130,29 +124,35 @@ $NAO_SKILLS/.agents/scripts/qq-notify --dry-run "连通性自检" # 只取
|
|
|
130
124
|
|
|
131
125
|
机制本体(6 张角色卡、协作规矩、清单、按需技能、小工具)都在 npm 包 `@nathan33/nao-skill` 的 `.agents/` 里——**包里是什么就是什么**,项目里不留副本。
|
|
132
126
|
|
|
133
|
-
##
|
|
127
|
+
## 老用户迁移与维护者注记(新用户可跳过)
|
|
128
|
+
|
|
129
|
+
本节面向**已有旧版安装**或需要维护使用机制的仓库 CI 的读者,新用户可以直接跳到「想深入」。
|
|
130
|
+
|
|
131
|
+
### 老项目迁移(migrate)
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx @nathan33/nao-skill migrate # 旧文件备份到 .agents/.nao-obsolete/,再按需写入转发入口
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
迁移默认按**判定式**决定是否写转发入口(shim):仓里还有旧的 `nao-fleet.sh`、或已存在 shim、或全仓有 `nao-fleet` / `nao-skill` / `NAO_SKILLS` 引用,就写;**无脚本、无引用的纯文档仓会被判定跳过**,并打印一行告知。要改变判定结果,显式加 `--shim`(强制写)或 `--no-shim`(强制不写),两者不能同时给(`exit 2`)。旧版的 `.agents/.nao-migrated` 标记**已废弃**:迁移不再读写它,可安全删除。`.agents/.nao-version` 的语义是「本机已安装机制包版本」(供升级检测),**不表达迁移状态**——迁移状态由「是否还有旧资产」推断。
|
|
138
|
+
|
|
139
|
+
### 发布形态与完整性判据(0.12 起有变化)
|
|
134
140
|
|
|
135
141
|
**从 0.12 起,机制不再铺进项目**:项目里只留一个转发入口,机制的**单一事实来源**是 npm 包 `@nathan33/nao-skill` 内的 `.agents/`。
|
|
136
142
|
|
|
137
|
-
-
|
|
143
|
+
- **旧的完整性判据已失效**:以前靠「项目内 `.agents/**` 与包内逐字节 sha 一致」证明机制没漏没改;现在项目内不再有机制副本,这条判据不再适用。
|
|
138
144
|
- **新的完整性判据:以 pin 的包版本为准**。`.pi/settings.json` 记录版本,`pi install/update` 从包物化;包里是什么就是什么,不存在「项目内被就地改动」的问题。转发入口是生成的固定文本,可用 `npx @nathan33/nao-skill init` 幂等重写核对。
|
|
139
145
|
- **版本权威口径**:`.pi/settings.json` 里的 pin 是**唯一权威**(`pi install/update` 据此物化到 `.pi/npm`);`.agents/.nao-version` 的语义是**本机已安装机制包版本**(供升级检测),**不表达迁移状态**、**不作为版本依据**;两者不一致时**以 pin 为准**。
|
|
140
|
-
- **fmt 边界**:包内 `.agents/**`
|
|
146
|
+
- **fmt 边界**:包内 `.agents/**` 按原始存储形态发布(部分 YAML / Markdown / MTS 未按 oxfmt / prettier 归一)。迁移期仍持有旧版全套 `.agents/**` 的仓库,继续把 `.agents/**` 排除出 fmt 校验即可,⛔ **不要为过 fmt 就地格式化**;新形态项目内只有转发入口,无需再处理。
|
|
141
147
|
|
|
142
148
|
### 物化与离线(CI / 无 pi 环境必读)
|
|
143
149
|
|
|
144
150
|
- `.pi/npm/` 由 pi 管理并 **gitignore**。因此 **fresh clone 上没有 `.pi/npm/package.json` 与 lockfile**,`npm ci --prefix .pi/npm` 当前**不可用**。
|
|
145
151
|
- 物化只有一条路:**显式执行** `pi install -l --approve npm:@nathan33/nao-skill@<pin>`(需要 pi 与网络,是有意为之的一步)。
|
|
146
|
-
- **联动条款**:若某个仓库想把机制体检 `check` 放进 CI
|
|
152
|
+
- **联动条款**:若某个仓库想把机制体检 `check` 放进 CI,**必须同时**用 `git add -f` 把 `.pi/npm/package.json`、`.pi/npm/package-lock.json` 纳入版本控制,`npm ci` 才有输入。
|
|
147
153
|
- **pi 启动的隐式安装**:当 `.pi/npm/node_modules` 缺失时,pi 启动会隐式执行 `npm install --prefix .pi/npm --legacy-peer-deps`;离线且缓存为空会得到可读的 npm 报错,并让 pi 以非 0 退出(不会静默假装成功)。
|
|
148
154
|
- **运行纪律**:**先显式物化,再 `PI_OFFLINE=1` 运行**。nao 机制自身(转发入口 / fleet / CLI / check)**零网络调用**;入口找不到机制包时不静默成功,而是 `exit 2` 并打印一行 `DEGRADED:` + 恢复命令。
|
|
149
155
|
|
|
150
|
-
## 下游仓库迁移(nao-todo / nao-todo-server / nue-ui)
|
|
151
|
-
|
|
152
|
-
- **迁移期零改动**:旧的全套 `.agents/scripts/nao-fleet.sh` 照常可用,这些仓库的 CI 目前不调用 `check`,不受影响。
|
|
153
|
-
- 想切到新形态时,在各仓执行 `npx @nathan33/nao-skill migrate`:只删**已知 nao 资产**(共享的 `.agents/skills/` 里项目自有的技能会保留),并把 `skills-lock.json` 里重复的 `frontend-design` 来源收敛为「机制包唯一来源」。
|
|
154
|
-
- **纯文档仓(如 `nao-todo-minimal`)特例**:无脚本、无 `nao-fleet`/`nao-skill` 引用 ⇒ 迁移判定为**纯文档迁移,不建转发入口**(可显式 `--shim` 强制;也可 `--no-shim` 强制不建);它的 `.agents/commands/`、`.agents/prompts/` 不在 pi 的 skill 扫描面(pi 只扫 `.agents/skills/**`)。
|
|
155
|
-
|
|
156
156
|
## 想深入
|
|
157
157
|
|
|
158
158
|
README 只讲怎么用。真正的机制都在依赖包 `@nathan33/nao-skill` 的 `.agents/` 里(会话里以 `$NAO_SKILLS/.agents/` 指向),想改就从这几处入手:
|
package/bin/nao-skill.js
CHANGED
|
@@ -46,12 +46,12 @@ const VERSION_FILE = '.agents/.nao-version';
|
|
|
46
46
|
// 旧版全套安装的已知 nao 资产(迁移时**精准**备份+移除;共享目录不得整目录删)
|
|
47
47
|
const LEGACY_TOP_DIRS = ['prompts', 'common', 'checklists', 'templates'];
|
|
48
48
|
const LEGACY_TOP_FILES = ['roles.yaml'];
|
|
49
|
-
const LEGACY_SCRIPT_FILES = ['nao-fleet.sh', 'intercom-probe.mts', '
|
|
49
|
+
const LEGACY_SCRIPT_FILES = ['nao-fleet.sh', 'intercom-probe.mts', 'ui-tokens-check.sh'];
|
|
50
50
|
const LEGACY_SKILL_ENTRIES = [
|
|
51
51
|
'nao-fleet', 'frontend-design',
|
|
52
52
|
'arch-patterns.md', 'backend-ddd-details.md', 'codegraph.md', 'commit.md',
|
|
53
53
|
'frontend-ddd-details.md', 'github-flow.md', 'pm-grill.md', 'pm-operations.md',
|
|
54
|
-
'pm-rice.md', 'pm-routing.md', '
|
|
54
|
+
'pm-rice.md', 'pm-routing.md', 'research.md', 'test-design.md',
|
|
55
55
|
];
|
|
56
56
|
|
|
57
57
|
const log = (...a) => console.log('[nao-skill]', ...a);
|
package/package.json
CHANGED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
//bin/true; exec node "$0" "$@"
|
|
3
|
-
//
|
|
4
|
-
// qq-notify —— QQ 官方机器人「主动消息」推送器(舰队可选能力)
|
|
5
|
-
//
|
|
6
|
-
// 用法(三种形式等价:./qq-notify · node qq-notify · sh qq-notify)
|
|
7
|
-
// ./qq-notify "文本" 发给配置中的 ownerOpenId(默认收件人)
|
|
8
|
-
// ./qq-notify --to OPENID "文本" 发给指定 openid
|
|
9
|
-
// ./qq-notify --dry-run "文本" 只取 token、不发送(连通性自检)
|
|
10
|
-
// ./qq-notify --config PATH "文本" 指定配置文件
|
|
11
|
-
// ./qq-notify --help 显示帮助
|
|
12
|
-
// stdin 管道 无参数时从标准输入读取文本
|
|
13
|
-
//
|
|
14
|
-
// 配置优先级:--config PATH > 环境变量 QQ_NOTIFY_CONFIG > ~/.pi/agent/pi-agent-qqbot.json
|
|
15
|
-
// 退出码:0 成功 · 1 参数/配置错 · 2 取 token 失败 · 3 发送失败
|
|
16
|
-
// 安全:绝不打印/落盘 clientSecret 与 access_token;错误输出截断。
|
|
17
|
-
//
|
|
18
|
-
// ⛔ 禁用 `bash <本脚本>` 这类「用 shell 逐行解释 Node 脚本」的调用方式:
|
|
19
|
-
// shell 会把注释里的示例当命令执行(管道、重定向、glob),可能触发意外发送
|
|
20
|
-
// (本仓历史上正因此误发过一条真实消息)。正确形式:./qq-notify 或 node qq-notify。
|
|
21
|
-
// 第 2 行的 exec 守卫已使 sh/bash 调用自动转交 node,但仍不建议依赖该行为。
|
|
22
|
-
//
|
|
23
|
-
// 实现说明:本文件无扩展名,需同时兼容宿主项目 package.json 的
|
|
24
|
-
// "type": "module" 与 CommonJS 两种解析;因此只用动态 import()(两种格式都合法)
|
|
25
|
-
// 并包在 async IIFE 中,避免静态 import / 顶层 await / require。
|
|
26
|
-
;(async () => {
|
|
27
|
-
const fs = await import('node:fs')
|
|
28
|
-
const os = await import('node:os')
|
|
29
|
-
const path = await import('node:path')
|
|
30
|
-
|
|
31
|
-
const EXIT = { OK: 0, ARG: 1, TOKEN: 2, SEND: 3 }
|
|
32
|
-
const DEFAULT_CFG = path.join(os.homedir(), '.pi/agent/pi-agent-qqbot.json')
|
|
33
|
-
const TOKEN_URL = 'https://bots.qq.com/app/getAppAccessToken'
|
|
34
|
-
const BODY_LIMIT = 300
|
|
35
|
-
|
|
36
|
-
const USAGE = [
|
|
37
|
-
'qq-notify —— QQ 官方机器人「主动消息」推送器',
|
|
38
|
-
'',
|
|
39
|
-
'用法',
|
|
40
|
-
' qq-notify "文本" 发给配置中的 ownerOpenId',
|
|
41
|
-
' qq-notify --to <openid> "文本" 发给指定 openid',
|
|
42
|
-
' echo "文本" | qq-notify 从 stdin 读取文本',
|
|
43
|
-
' qq-notify --dry-run "文本" 只取 token、不发送(连通性自检)',
|
|
44
|
-
' qq-notify --config <path> ... 指定配置文件',
|
|
45
|
-
' qq-notify -h | --help 显示本帮助',
|
|
46
|
-
'',
|
|
47
|
-
'配置(优先级从高到低)',
|
|
48
|
-
' 1) --config <path>',
|
|
49
|
-
' 2) 环境变量 QQ_NOTIFY_CONFIG',
|
|
50
|
-
' 3) ' + DEFAULT_CFG,
|
|
51
|
-
' 字段:{ "appId": "...", "clientSecret": "...", "ownerOpenId": "...", "sandbox": true }',
|
|
52
|
-
'',
|
|
53
|
-
'退出码',
|
|
54
|
-
' 0 成功 1 参数/配置错 2 取 token 失败 3 发送失败',
|
|
55
|
-
'',
|
|
56
|
-
'说明',
|
|
57
|
-
' · 主动消息受 QQ 开放平台权限与月度配额限制;sandbox 与 prod 权限不通用',
|
|
58
|
-
' · 绝不打印或落盘 clientSecret / access_token',
|
|
59
|
-
].join('\n')
|
|
60
|
-
|
|
61
|
-
const trunc = (v, n = BODY_LIMIT) => {
|
|
62
|
-
const s = String(v ?? '')
|
|
63
|
-
return s.length > n ? s.slice(0, n) + '…(截断)' : s
|
|
64
|
-
}
|
|
65
|
-
// 从任何即将打印的文本中抹去已知凭证值(客户端密钥 / access_token)
|
|
66
|
-
const redact = (v, secrets = [], n = BODY_LIMIT) => {
|
|
67
|
-
let s = String(v ?? '')
|
|
68
|
-
for (const sec of secrets) {
|
|
69
|
-
if (sec && String(sec).length >= 6) s = s.split(String(sec)).join('***')
|
|
70
|
-
}
|
|
71
|
-
return trunc(s, n)
|
|
72
|
-
}
|
|
73
|
-
const maskId = (v) => (v ? String(v).slice(0, 6) + '…' : '<无>')
|
|
74
|
-
const fail = (code, msg, hint) => {
|
|
75
|
-
process.stderr.write('FAIL: ' + msg + '\n')
|
|
76
|
-
if (hint) process.stderr.write(' → ' + hint + '\n')
|
|
77
|
-
process.exit(code)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ---- 参数解析 -----------------------------------------------------------
|
|
81
|
-
const argv = process.argv.slice(2)
|
|
82
|
-
let to = null
|
|
83
|
-
let cfgPath = null
|
|
84
|
-
let dry = false
|
|
85
|
-
let text = null
|
|
86
|
-
let endOpts = false
|
|
87
|
-
|
|
88
|
-
for (let i = 0; i < argv.length; i++) {
|
|
89
|
-
const a = argv[i]
|
|
90
|
-
if (endOpts) {
|
|
91
|
-
if (text === null) { text = a; continue }
|
|
92
|
-
fail(EXIT.ARG, '多余的位置参数: ' + trunc(a, 60), '文本请用引号括起为一个参数')
|
|
93
|
-
}
|
|
94
|
-
if (a === '-h' || a === '--help') { process.stdout.write(USAGE + '\n'); process.exit(EXIT.OK) }
|
|
95
|
-
else if (a === '--dry-run') dry = true
|
|
96
|
-
else if (a === '--') endOpts = true
|
|
97
|
-
else if (a === '--to') {
|
|
98
|
-
const v = argv[++i]
|
|
99
|
-
if (!v || v.startsWith('--')) fail(EXIT.ARG, '--to 缺少 openid', '用法:qq-notify --to <openid> "文本"')
|
|
100
|
-
to = v
|
|
101
|
-
} else if (a.startsWith('--to=')) {
|
|
102
|
-
to = a.slice(5)
|
|
103
|
-
if (!to) fail(EXIT.ARG, '--to 缺少 openid', '用法:qq-notify --to <openid> "文本"')
|
|
104
|
-
} else if (a === '--config') {
|
|
105
|
-
const v = argv[++i]
|
|
106
|
-
if (!v || v.startsWith('--')) fail(EXIT.ARG, '--config 缺少路径', '用法:qq-notify --config <path> "文本"')
|
|
107
|
-
cfgPath = v
|
|
108
|
-
} else if (a.startsWith('--config=')) {
|
|
109
|
-
cfgPath = a.slice(9)
|
|
110
|
-
if (!cfgPath) fail(EXIT.ARG, '--config 缺少路径', '用法:qq-notify --config <path> "文本"')
|
|
111
|
-
} else if (a.startsWith('-') && a !== '-') {
|
|
112
|
-
fail(EXIT.ARG, '未知参数: ' + trunc(a, 60), '查看帮助:qq-notify --help')
|
|
113
|
-
} else {
|
|
114
|
-
if (text === null) text = a
|
|
115
|
-
else fail(EXIT.ARG, '多余的位置参数: ' + trunc(a, 60), '文本请用引号括起为一个参数')
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!text && !process.stdin.isTTY) {
|
|
120
|
-
try {
|
|
121
|
-
const piped = fs.readFileSync(0, 'utf8').trim()
|
|
122
|
-
if (piped) text = piped
|
|
123
|
-
} catch { /* 无 stdin:忽略 */ }
|
|
124
|
-
}
|
|
125
|
-
if (!text) fail(EXIT.ARG, '缺少推送文本', '用法:qq-notify "文本" 或 echo "文本" | qq-notify')
|
|
126
|
-
|
|
127
|
-
// ---- 配置 ---------------------------------------------------------------
|
|
128
|
-
const cfgFile = cfgPath || process.env.QQ_NOTIFY_CONFIG || DEFAULT_CFG
|
|
129
|
-
let cfg
|
|
130
|
-
try {
|
|
131
|
-
cfg = JSON.parse(fs.readFileSync(cfgFile, 'utf8'))
|
|
132
|
-
} catch (e) {
|
|
133
|
-
if (e && e.code === 'ENOENT') {
|
|
134
|
-
fail(EXIT.ARG, '读不到配置:' + cfgFile,
|
|
135
|
-
'创建该 JSON(appId / clientSecret / ownerOpenId / sandbox),或用 --config <path> / QQ_NOTIFY_CONFIG=<path> 指定')
|
|
136
|
-
}
|
|
137
|
-
fail(EXIT.ARG, '配置解析失败:' + cfgFile + ':' + trunc(e && e.message, 160), '确认该文件是合法 JSON')
|
|
138
|
-
}
|
|
139
|
-
if (!cfg || typeof cfg !== 'object') fail(EXIT.ARG, '配置内容非法:' + cfgFile, '应为 JSON 对象')
|
|
140
|
-
if (!cfg.appId || !cfg.clientSecret) fail(EXIT.ARG, '配置缺少 appId 或 clientSecret', '检查 ' + cfgFile)
|
|
141
|
-
|
|
142
|
-
const target = to || cfg.ownerOpenId
|
|
143
|
-
if (!target) {
|
|
144
|
-
fail(EXIT.ARG, '没有收件人:配置缺 ownerOpenId,且未传 --to',
|
|
145
|
-
'在配置中填 ownerOpenId,或 qq-notify --to <openid> "文本"')
|
|
146
|
-
}
|
|
147
|
-
const env = cfg.sandbox ? 'sandbox' : 'prod'
|
|
148
|
-
const base = cfg.sandbox ? 'https://sandbox.api.sgroup.qq.com' : 'https://api.sgroup.qq.com'
|
|
149
|
-
|
|
150
|
-
// ---- 取 access token(绝不打日志) --------------------------------------
|
|
151
|
-
let tokenRes
|
|
152
|
-
try {
|
|
153
|
-
tokenRes = await fetch(TOKEN_URL, {
|
|
154
|
-
method: 'POST',
|
|
155
|
-
headers: { 'content-type': 'application/json' },
|
|
156
|
-
body: JSON.stringify({ appId: cfg.appId, clientSecret: cfg.clientSecret }),
|
|
157
|
-
})
|
|
158
|
-
} catch (e) {
|
|
159
|
-
fail(EXIT.TOKEN, '取 token 网络错误:' + trunc(e && e.message, 160), '检查网络/代理后重试')
|
|
160
|
-
}
|
|
161
|
-
let tokenJson = {}
|
|
162
|
-
try { tokenJson = await tokenRes.json() } catch { /* 非 JSON 响应 */ }
|
|
163
|
-
if (!tokenRes.ok || !tokenJson.access_token) {
|
|
164
|
-
fail(EXIT.TOKEN, '取 token 失败:status=' + tokenRes.status + ' body=' + redact(JSON.stringify(tokenJson), [cfg.clientSecret]),
|
|
165
|
-
'核对 appId/clientSecret,并确认 QQ 开放平台应用已开通机器人能力')
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (dry) {
|
|
169
|
-
process.stdout.write('OK (dry-run) 取 token 成功 · env=' + env + ' · to=' + maskId(target) + '\n')
|
|
170
|
-
process.exit(EXIT.OK)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ---- 发送主动消息(不带 msg_id) ----------------------------------------
|
|
174
|
-
let res
|
|
175
|
-
try {
|
|
176
|
-
res = await fetch(base + '/v2/users/' + encodeURIComponent(target) + '/messages', {
|
|
177
|
-
method: 'POST',
|
|
178
|
-
headers: {
|
|
179
|
-
'content-type': 'application/json',
|
|
180
|
-
authorization: 'QQBot ' + tokenJson.access_token,
|
|
181
|
-
},
|
|
182
|
-
body: JSON.stringify({ content: text, msg_type: 0 }),
|
|
183
|
-
})
|
|
184
|
-
} catch (e) {
|
|
185
|
-
fail(EXIT.SEND, '发送网络错误:' + trunc(e && e.message, 160), '检查网络后重试')
|
|
186
|
-
}
|
|
187
|
-
const body = await res.text().catch(() => '')
|
|
188
|
-
if (res.ok) {
|
|
189
|
-
process.stdout.write('OK env=' + env + ' to=' + maskId(target) + ' ' + redact(body, [cfg.clientSecret, tokenJson.access_token], 120) + '\n')
|
|
190
|
-
process.exit(EXIT.OK)
|
|
191
|
-
}
|
|
192
|
-
fail(EXIT.SEND, '发送失败:status=' + res.status + ' body=' + redact(body, [cfg.clientSecret, tokenJson.access_token]),
|
|
193
|
-
'核对收件人 openid、主动消息权限与月度配额;sandbox 与 prod 权限不通用')
|
|
194
|
-
})().catch((e) => {
|
|
195
|
-
// 兜底:任何未预期错误都不抛栈(此处不依赖 IIFE 内作用域)
|
|
196
|
-
const msg = String((e && e.message) ? e.message : e)
|
|
197
|
-
process.stderr.write('FAIL: 未预期错误:' + (msg.length > 200 ? msg.slice(0, 200) + '…(截断)' : msg) + '\n')
|
|
198
|
-
process.exit(1)
|
|
199
|
-
})
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: 按需技能——QQ 主动推送(关键节点通知;PM 在里程碑/阻塞时读取)
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# QQ 主动推送(qq-notify)
|
|
6
|
-
|
|
7
|
-
## 触发时机
|
|
8
|
-
|
|
9
|
-
**仅关键节点**由 PM 使用,读本文件确认纪律后调用;日常轮次不加载、不发送。
|
|
10
|
-
本能力**可选**:未配置 `pi-agent-qqbot` 时不要使用,按普通回执继续交付。
|
|
11
|
-
|
|
12
|
-
## 定位与能力边界
|
|
13
|
-
|
|
14
|
-
- 把「关键节点」用 QQ 官方机器人的**主动消息**推给 owner,让用户离开工位也能收到交付信号。
|
|
15
|
-
- **主动消息**(不带 `msg_id`)受 **QQ 开放平台账号权限与月度配额**限制:配额用尽或未开通对应权限时发送会失败(exit 3),**不得重试轰炸**。
|
|
16
|
-
- 只做**单向推送**,不做接收/被动回复;不做定时调度、不做退避重发(这些是平台与宿主的事)。
|
|
17
|
-
- **默认 sandbox**:`pi-agent-qqbot.json` 中 `sandbox: true` 走测试环境,`false` 才走 `prod`。**两者权限与收件人不通用**,切换 prod 前先确认机器人已在正式环境配置好。
|
|
18
|
-
|
|
19
|
-
## 何时用(四类节点,仅此四类)
|
|
20
|
-
|
|
21
|
-
| 节点 | 典型内容 |
|
|
22
|
-
| :--- | :--- |
|
|
23
|
-
| 批次进度汇总 | 一个批次开始/收尾时的整体进度(不是每个子任务) |
|
|
24
|
-
| 验收结论 | PM 验收通过 / 驳回,含 PR 编号与一句话结论 |
|
|
25
|
-
| 发版或合并完成 | tag / Release / squash 合并完成,含版本号 |
|
|
26
|
-
| 异常阻塞 | 需要用户决策、外部依赖卡住、门禁长期红 |
|
|
27
|
-
|
|
28
|
-
## 纪律(禁刷屏)
|
|
29
|
-
|
|
30
|
-
- **一个节点最多一条**;同一批次不逐任务推送。
|
|
31
|
-
- 推文用**一句话**:`[编号] 结论 + 关键数字/链接`,不贴长正文(正文留 `docs/`)。
|
|
32
|
-
- 用户未回应不重复推;阻塞未解除才允许在**跨天**时补一条。
|
|
33
|
-
- **默认 sandbox**,仅在正式发布通知用户时考虑 prod(须用户明确知情)。
|
|
34
|
-
|
|
35
|
-
## 调用禁令(⛔ 重要:曾因此误发过真实消息)
|
|
36
|
-
|
|
37
|
-
本脚本是 **Node 程序**,文件头部的注释里写着用法示例。⛔ **禁止用 shell 把它当 shell 脚本逐行解释**:
|
|
38
|
-
|
|
39
|
-
```text
|
|
40
|
-
⛔ bash .agents/scripts/qq-notify "文本" ← 错:shell 会执行注释里的示例
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
原因(实测):`bash <本脚本>` 会把注释当命令执行——注释里的 `*` 触发 **glob 展开**(把 `AGENTS.md apps docs …` 当命令跑)、`--to <openid>` 变成 **重定向**、`echo "文本" | qq-notify` 变成 **真实管道**(PATH 命中同名脚本即**真发**)——本仓历史上正因此误发过一条真实 QQ 消息。
|
|
44
|
-
|
|
45
|
-
**正确形式(三者等价,均 rc=0 且输出一致)**:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
.agents/scripts/qq-notify "文本" # ./ 直接执行(推荐)
|
|
49
|
-
node .agents/scripts/qq-notify "文本" # 显式用 node
|
|
50
|
-
sh .agents/scripts/qq-notify "文本" # 已加固:头部 exec 守卫自动转交 node
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
> 加固方式(v0.9.2):第 1 行 `#!/bin/sh` + 第 2 行 `//bin/true; exec node "$0" "$@"`。
|
|
54
|
-
> `sh/bash` 执行时第 2 行即 `exec node`(**自动转交**,注释不会再被 shell 解析);`node` 执行时第 2 行是 JS 注释。
|
|
55
|
-
> `nao-fleet.sh check` 已增加该守卫的存在性校验(缺失 ⇒ rc=1)。
|
|
56
|
-
|
|
57
|
-
## 用法
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
# 安装后位于当前项目:.agents/scripts/qq-notify
|
|
61
|
-
.agents/scripts/qq-notify "[T301] 验收通过 · PR #4 已合并 · v0.8.0"
|
|
62
|
-
.agents/scripts/qq-notify --dry-run "连通性自检" # 只取 token、不发送
|
|
63
|
-
echo "文本" | .agents/scripts/qq-notify # 从 stdin 读
|
|
64
|
-
.agents/scripts/qq-notify --to <openid> "文本" # 指定收件人
|
|
65
|
-
.agents/scripts/qq-notify -h # 帮助
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
- 配置优先级:`--config <path>` > 环境变量 `QQ_NOTIFY_CONFIG` > `~/.pi/agent/pi-agent-qqbot.json`
|
|
69
|
-
- 配置文件字段:`{ "appId", "clientSecret", "ownerOpenId", "sandbox" }`;**无此文件即视为未启用本能力**(exit 1)。
|
|
70
|
-
|
|
71
|
-
## 退出码与失败回报
|
|
72
|
-
|
|
73
|
-
| 退出码 | 含义 | 处理 |
|
|
74
|
-
| :--- | :--- | :--- |
|
|
75
|
-
| `0` | 发送成功(或 `--dry-run` 取 token 成功) | 正常,可在回执记一行 `notify=ok` |
|
|
76
|
-
| `1` | 参数或配置错(缺文本 / 读不到配置 / 缺 `ownerOpenId`) | 按 stderr 的 `→` 提示修复;**不要重试**,先补配置 |
|
|
77
|
-
| `2` | 取 access token 失败(网络 / 凭证错误) | 报出 `status` 与截断错误体;不重试轰炸 |
|
|
78
|
-
| `3` | 发送失败(权限 / 配额 / openid 非法) | 报出 `status` 与截断错误体;主动消息受权限与配额限制 |
|
|
79
|
-
|
|
80
|
-
- 失败时**不抛栈**:脚本输出 `FAIL: …` 与一行可执行修复提示,**非 0 退出码 + 截断错误体**即为回报内容。
|
|
81
|
-
- 推送失败**不阻断交付**:把 `notify=fail(exit=<n>)` 写进回执的风险项,继续原流程。
|
|
82
|
-
- **安全**:脚本绝不打印/落盘 `clientSecret` 与 `access_token`;回执中也不要粘贴这两者。
|
|
83
|
-
|
|
84
|
-
## 跨项目用法
|
|
85
|
-
|
|
86
|
-
安装后脚本固定位于 **`<项目>/.agents/scripts/qq-notify`**(随 `nao-skill install/update` 整目录分发)。
|
|
87
|
-
在项目里直接用相对路径调用;从别的仓库操作时用绝对路径 `.agents/scripts/qq-notify`(可配 `NAO_SKILLS`)。
|