diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2e77f05..d5195c6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "workflow", - "version": "0.6.0", + "version": "0.6.1", "description": "让 AI Agent 直连 Workflow(workflow.games):规划需求与需求室、建需求、记 bug、查任务、拿单执行与证据回写、流转状态、线上 QA 验收、查文档、经确认后向平台方匿名反馈问题与建议", "author": { "name": "Workflow", diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a72680..d2bfd4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [0.6.1] + +修正 `workflow-update` 的渠道误判:插件有两条分发渠道,版本真值不是同一个——宿主托管安装(Claude Code marketplace)认公开仓的 `plugin.json`,手动安装(Codex / 官网脚本)认官网 `version.json`。旧正文让所有形态先读 `version.json` 再分流,官网清单一旦滞后于 marketplace 发布,宿主托管用户就会拿到「已是最新」的反向结论、永远升不上去(2026-08-29 实测:官网停在 0.3.0,marketplace 已发 0.6.0)。 + +### 修复 + +- **`workflow-update` 改为「先判安装形态,再查版本」。** 分流升为第 1 节,且三条判定按「源码态 → 宿主托管 → 手动安装」定序、先命中先算——源码态必须最先判(否则插件仓库自己的工作副本会被「向上两级有 plugin.json」误判成宿主托管),手动安装的路径特征优先于宿主托管的清单特征;宿主托管(第 2 节)明确**不读 `version.json`**,改走宿主自己的机制(`claude plugin marketplace update` + `claude plugin update`,并提醒更新后需重启会话),版本真值指向 marketplace 公开仓的 `plugin.json`;手动安装(第 3 节)保留原有的 `cb` 绕缓存、语义化逐段比较与「线上 < 本地 绝不更新」降级红线。安全边界补一条:宿主托管形态不下载任何文件。 +- **`/workflow:update` 命令描述同步新次序**,避免命令入口与技能正文两个口径。 +- **README 中英更新表补上 Claude Code 的两条 CLI 命令与重启提醒**——原表只给了 `/plugin` 界面与 autoUpdate,非交互终端里两者都用不了。 + +### 测试 + +- **新增 `tests/workflow-update-contract.test.mjs`(16 项)**:分流段落必须先于抓取 `version.json?cb=` 的**次序断言**(本次回归的根因守卫)、开篇必须声明两条渠道版本真值不同、两个分支的路径判定特征、宿主托管分支的「不读 version.json + 说明滞后原因 + 宿主命令 + 重启提醒 + 不自改插件目录」、手动安装分支的 `cb` 与三分支比较与降级红线、安全边界四条、命令入口与技能同次序。 + ## [0.6.0] 新增面向平台方的匿名反馈通道,并与「记 bug 进自己项目」彻底分流。全部口径按线上合同(`createSupportTicket` / `getSupportConfig`)与官方指南逐条核实:只收集用户主动提供的信息、发送前逐字确认、公开匿名端点不碰凭证、202 回执如实说明不是正式单。反馈范围不限于报错——体验不佳、加载或操作卡慢、缺失功能、产品建议同样可报。 diff --git a/README.en.md b/README.en.md index 922c054..39616f2 100644 --- a/README.en.md +++ b/README.en.md @@ -192,7 +192,7 @@ surfaces = ["web"] | Install method | How to update | | :-- | :-- | -| Claude Code (marketplace) | Supports autoUpdate, or update manually from the `/plugin` UI | +| Claude Code (marketplace) | `claude plugin marketplace update workflow-plugin` + `claude plugin update workflow@workflow-plugin --scope user` (restart the session afterwards); autoUpdate and the `/plugin` UI also work | | Agent Plugins client | Re-run `npx plugins add Go1c/workflow-plugin` | | Codex / manual | Tell the agent "update the workflow plugin" — checks the published version, verifies sha256 per file, backs up the old version, installs | diff --git a/README.md b/README.md index cbcd88d..abca0bc 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ surfaces = ["web"] | 安装方式 | 怎么更新 | | :-- | :-- | -| Claude Code(marketplace) | 支持 autoUpdate 自动升级,也可在 `/plugin` 界面手动更新 | +| Claude Code(marketplace) | `claude plugin marketplace update workflow-plugin` + `claude plugin update workflow@workflow-plugin --scope user`(更新后需重启会话);也支持 autoUpdate 自动升级或在 `/plugin` 界面手动更新 | | Agent Plugins 客户端 | 重跑一次 `npx plugins add Go1c/workflow-plugin` | | Codex / 手动安装 | 对 Agent 说「更新 workflow 插件」—— 查线上版本 → 逐文件校验 sha256 → 备份旧版 → 就位 | diff --git a/commands/update.md b/commands/update.md index c220f7e..326584c 100644 --- a/commands/update.md +++ b/commands/update.md @@ -2,4 +2,4 @@ description: 检查并更新 Workflow Agent 插件到最新版本 --- -强制调起 **workflow-update** 技能,严格按其正文流程执行:读本地 VERSION → 带 cb 参数查线上 version.json → 按宿主分流决定提示更新还是自更新。 +强制调起 **workflow-update** 技能,严格按其正文流程执行:**先判安装形态**(宿主托管 / 手动安装)→ 宿主托管交给宿主自己的更新机制、不读官网 version.json;手动安装才读本地 VERSION 并带 cb 参数比对线上 version.json,必要时走 sha256 校验的自更新。 diff --git a/package.json b/package.json index bb292b6..0d77289 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "workflow-plugin", - "version": "0.6.0", + "version": "0.6.1", "private": true, "type": "module", "description": "Agent plugin for Workflow (workflow.games) — 技能包与契约测试", diff --git a/plugin.json b/plugin.json index bc1af12..6989c25 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "workflow", - "version": "0.6.0", + "version": "0.6.1", "description": "让 AI Agent 直连 Workflow(workflow.games):规划需求与需求室、建需求、记 bug、查任务、拿单执行与证据回写、流转状态、线上 QA 验收、查文档、经确认后向平台方匿名反馈问题与建议", "author": { "name": "Workflow", diff --git a/skills/workflow-update/SKILL.md b/skills/workflow-update/SKILL.md index 0b79807..c8dc692 100644 --- a/skills/workflow-update/SKILL.md +++ b/skills/workflow-update/SKILL.md @@ -5,14 +5,44 @@ description: 更新或检查 Workflow(workflow.games)Agent 插件版本时 # workflow-update — 检查与更新插件 -## 1. 读本地版本 +**先判安装形态,再查版本。** 插件有两条分发渠道,**版本真值不是同一个**:宿主托管安装(Claude Code marketplace)认公开仓的 `plugin.json`,手动安装(Codex / 官网脚本)认官网 `version.json`。两条渠道的发布节奏可以脱节,拿另一条渠道的版本号判自己,会得出「已是最新」甚至反向降级的错误结论。 + +## 1. 判断安装形态 + +看**本技能所在路径**,按下面的顺序判,**先命中先算**: + +**0. 源码态** —— 向上两级既是插件根、又带 `.git` 或 `tests/`(你在开发这个插件本身,不是在用它)→ **提示无需更新,结束**。源码态归 git 管,不归本技能;这一条必须先判,否则插件仓库自己的工作副本会被下面的清单特征误判成宿主托管。 + +**A. 宿主托管安装** —— 技能路径在 `~/.claude/plugins/cache` 或其他客户端的插件缓存下 → 走**第 2 节**。 +没有缓存路径特征、但向上两级是插件根(存在 `plugin.json` 或 `.claude-plugin/plugin.json`),且**不在 B 列出的手动安装目录里**,同样按 A 处理。 + +**B. 手动安装** —— 技能目录直接落在 `~/.codex/skills`、`.agents/skills` 或 `~/.claude/skills` 下 → 走**第 3 节**。**B 的路径特征优先于 A 的清单特征**:项目里恰好放着一份 `plugin.json`,不改变这是手动安装的事实。 + +## 2. 宿主托管:交给宿主更新 + +**这条路不读 `version.json`。** 那份清单只服务手动安装渠道,可能**滞后**于 marketplace 发布;宿主托管的版本真值是 marketplace 公开仓里的 `plugin.json`,由宿主自己拉取比对。用官网清单判宿主托管安装,最常见的结果是误报「已是最新」,用户永远升不上去。 + +**本技能不自改插件目录**——宿主管理的目录由宿主维护,绕过它手改会造成状态不一致。改为提示用户走宿主自己的机制: + +- **Claude Code**:先刷新 marketplace,再更新插件。 + +``` +claude plugin marketplace update workflow-plugin +claude plugin update workflow@workflow-plugin --scope user +``` + + 也可以用 `/plugin` 界面,或等 marketplace autoUpdate 自己生效。**更新后必须重启会话**才加载新版本——不提醒的话用户会以为没升成功。 + +- **其他 Agent Plugins 客户端**:用各自的安装器重装(例如 `npx plugins add` 那条路径)。 + +收尾读 `claude plugin list`(或宿主对应的列表命令)确认版本已变,向用户报告新旧版本号。 + +## 3. 手动安装:比对官网版本 读**本技能目录下的 `VERSION` 文件**(安装包内由构建器生成,纯文本一行版本号)。 - 没有 `VERSION` 文件 = 你运行的是源码态(开发仓里直接用),**提示无需更新**,结束。 -## 2. 查线上版本 - 抓取: ``` @@ -25,17 +55,7 @@ https://workflow.games/plugin/version.json?cb=<当前 epoch 秒> - 线上 **==** 本地 → 报告「已是最新(<版本>)」,结束。 - 线上 **<** 本地 → 本地更新(多半是源码态或线上发布滞后)。报告两个版本号并**结束,绝不"更新"**——照旧逻辑跑会把新版覆盖成旧版,是降级不是升级。 -- 线上 **>** 本地 → 才进入第 3 节。 - -## 3. 宿主分流 - -看本技能所在路径判断安装方式: - -**A. 宿主托管安装**(路径在 `~/.claude/plugins/cache` 或其他客户端的插件缓存下,或从本技能目录向上两级即为插件根、其中存在 `plugin.json` 或 `.claude-plugin/plugin.json`): - -提示用户按宿主自己的方式更新——Claude Code 用 `/plugin` 界面或等 marketplace autoUpdate;其他 Agent Plugins 客户端用各自的安装器(如 `npx plugins add` 重装)。**本技能不自改插件目录**——宿主管理的目录由宿主维护,绕过它手改会造成状态不一致。 - -**B. 手动安装**(技能目录直接落在 `~/.codex/skills`、`.agents/skills` 或 `~/.claude/skills` 下,同级没有插件清单)→ 自更新,按第 4 节。 +- 线上 **>** 本地 → 才进入第 4 节。 ## 4. 自更新流程(仅手动安装) @@ -49,4 +69,5 @@ https://workflow.games/plugin/version.json?cb=<当前 epoch 秒> - **只从 `workflow.games` 域下载**。清单里出现任何其他域的地址 → 中止并告警。 - 技能包只应包含 **`.md` 与 `VERSION` 纯文本**。清单或下载内容里发现可执行文件(`.sh`、二进制等)→ **立即中止并告警**,不安装。 +- 宿主托管形态(第 2 节)**不下载任何文件**——它只调用宿主自己的命令,下载与落盘都由宿主负责。 - 更新**绝不触碰** `~/.config/workflow/config.toml`——凭证与插件更新无关。 diff --git a/skills/workflow-update/VERSION b/skills/workflow-update/VERSION index a918a2a..ee6cdce 100644 --- a/skills/workflow-update/VERSION +++ b/skills/workflow-update/VERSION @@ -1 +1 @@ -0.6.0 +0.6.1 diff --git a/tests/workflow-update-contract.test.mjs b/tests/workflow-update-contract.test.mjs new file mode 100644 index 0000000..17ea74f --- /dev/null +++ b/tests/workflow-update-contract.test.mjs @@ -0,0 +1,160 @@ +// workflow-update 的提示词契约。 +// +// 存在的理由:这个技能的失败模式不是「答得不好」,而是**拿错渠道的版本号判自己**。 +// 插件有两条分发渠道,版本真值不是同一个:宿主托管(Claude Code marketplace)看公开仓的 +// plugin.json,手动安装(Codex / 官网脚本)看官网 version.json。两条渠道的发布节奏可以脱节, +// 2026-08-29 实测官网 version.json 停在 0.3.0 而 marketplace 已发 0.6.0——此时若让宿主托管 +// 安装去读 version.json,会得出「线上 0.3.0 < 本地 0.5.0,无需更新」的反向结论,用户永远升不上去。 +// +// 因此本文件锁死一条结构性纪律:**先判安装形态,再查版本**;且宿主托管分支明确不读 version.json。 +// 次序断言(分流段落必须出现在 version.json 之前)是这里的核心,不是风格检查。 + +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); + +function read(relativePath) { + const absolute = join(repoRoot, relativePath); + assert.ok(existsSync(absolute), `缺少 ${relativePath}`); + return readFileSync(absolute, "utf8"); +} + +const skill = read("skills/workflow-update/SKILL.md"); +const command = read("commands/update.md"); + +describe("分流先于查版本", () => { + test("安装形态分流段落出现在抓取 version.json 之前", () => { + // 量的是**抓取动作**(version.json?cb=)而不是任何一次提及:开篇点名两条渠道的版本真值 + // 恰恰是这次修复的重点,不该被自己的测试判违规。真正的红线是「先分流,后抓」。 + const routing = skill.search(/##\s*1\.\s*判断安装形态/); + const probe = skill.indexOf("version.json?cb="); + assert.ok(routing !== -1, "SKILL.md 缺少「## 1. 判断安装形态」——分流必须是第一节"); + assert.ok(probe !== -1, "SKILL.md 不再抓 version.json?cb=?手动安装渠道的探针不能删"); + assert.ok( + routing < probe, + `分流段落(${routing})必须出现在抓取 version.json(${probe})之前,否则宿主托管安装会先被官网渠道的版本号误导`, + ); + }); + + test("开篇即声明两条渠道版本真值不同", () => { + const opening = skill.slice(0, skill.search(/##\s*1\./)); + assert.match(opening, /先判安装形态/, "开篇必须先立「先判形态」的规矩"); + assert.match(opening, /版本真值/, "开篇必须点明两条渠道的版本真值不同"); + }); + + test("源码态最先判,且排在 A / B 之前", () => { + // 插件仓库自己的工作副本:技能目录向上两级就是仓库根,那里有 plugin.json 与 + // .claude-plugin/plugin.json。不先判源码态的话,开发者会被指去跑宿主的 update 命令。 + const routing = skill.slice(skill.search(/##\s*1\./), skill.search(/##\s*2\./)); + assert.match(routing, /先命中先算/, "三条判定必须声明先后,不能并列"); + const source = routing.search(/\*\*0\.\s*源码态\*\*/); + const hosted = routing.search(/\*\*A\.\s*宿主托管安装\*\*/); + const manual = routing.search(/\*\*B\.\s*手动安装\*\*/); + assert.ok(source !== -1, "缺少源码态判定"); + assert.ok(source < hosted && hosted < manual, "顺序必须是 源码态 → 宿主托管 → 手动安装"); + assert.match(routing, /\.git|tests\//, "源码态要给出可判定的特征"); + }); + + test("手动安装的路径特征优先于宿主托管的清单特征", () => { + // 项目里恰好放一份 plugin.json,不该把 ~/.codex/skills 下的手动安装改判成宿主托管。 + const routing = skill.slice(skill.search(/##\s*1\./), skill.search(/##\s*2\./)); + assert.match(routing, /B 的路径特征优先于 A 的清单特征/); + assert.match(routing, /不在 B 列出的手动安装目录里/, "A 的清单特征必须显式让位给 B"); + }); + + test("两个分支都给出可判定的路径特征", () => { + assert.match(skill, /~\/\.claude\/plugins\/cache/, "宿主托管的判定特征丢了"); + assert.match(skill, /\.claude-plugin\/plugin\.json/, "插件清单判定特征丢了"); + assert.match(skill, /~\/\.codex\/skills/, "手动安装的判定特征丢了"); + }); +}); + +describe("宿主托管分支", () => { + const hosted = skill.slice(skill.search(/##\s*2\./), skill.search(/##\s*3\./)); + + test("明确不读 version.json,并说明为什么", () => { + assert.match(hosted, /不读\s*`?version\.json`?/, "必须显式写「不读 version.json」"); + assert.match(hosted, /滞后/, "必须说明官网清单可能滞后于 marketplace 发布,否则下次还会有人接回去"); + }); + + test("版本真值指向 marketplace 清单而非官网探针", () => { + assert.match(hosted, /marketplace/i); + assert.match(hosted, /plugin\.json/); + }); + + test("给出宿主自己的更新机制与重启提醒", () => { + assert.match(hosted, /claude plugin marketplace update/, "缺少刷新 marketplace 的命令"); + assert.match(hosted, /claude plugin update/, "缺少更新插件的命令"); + assert.match(hosted, /重启/, "宿主更新后需重启才生效,不写用户会以为没升级成功"); + }); + + test("不自改宿主管理的插件目录", () => { + assert.match(hosted, /不自改插件目录/); + }); +}); + +describe("手动安装分支", () => { + const manual = skill.slice(skill.search(/##\s*3\./), skill.search(/##\s*4\./)); + + test("读本地 VERSION,源码态直接结束", () => { + assert.match(manual, /`VERSION`/); + assert.match(manual, /源码态/); + }); + + test("查线上版本必须带 cb 参数绕缓存", () => { + assert.match(manual, /version\.json\?cb=/); + assert.match(manual, /必须带\s*`?cb`?\s*参数/); + }); + + test("语义化逐段比较,三个分支齐全", () => { + assert.match(manual, /语义化版本逐段比大小/); + assert.match(manual, /线上\s*\*\*==\*\*\s*本地/); + assert.match(manual, /线上\s*\*\*<\*\*\s*本地/); + assert.match(manual, /线上\s*\*\*>\*\*\s*本地/); + }); + + test("线上更旧时绝不更新——降级红线仍在", () => { + assert.match(manual, /绝不("|「|")?更新/, "「线上 < 本地 绝不更新」这条降级红线不得删除"); + assert.match(manual, /降级不是升级/); + }); +}); + +describe("安全边界不得放宽", () => { + const safety = skill.slice(skill.search(/##\s*安全边界/)); + + test("只从 workflow.games 域下载", () => { + assert.match(safety, /只从\s*`?workflow\.games`?\s*域下载/); + }); + + test("技能包只允许 Markdown 与 VERSION,遇可执行文件中止", () => { + assert.match(safety, /`\.md`\s*与\s*`VERSION`/); + assert.match(safety, /立即中止并告警/); + }); + + test("不触碰凭证文件", () => { + assert.match(safety, /config\.toml/); + assert.match(safety, /绝不触碰/); + }); + + test("宿主托管形态不下载任何文件", () => { + assert.match(safety, /不下载任何文件/, "宿主托管只调宿主命令,这条边界要写明"); + }); +}); + +describe("命令入口与技能同口径", () => { + test("update 命令描述的次序是「先分流、后查版本」", () => { + const routing = command.search(/分流|安装形态/); + const probe = command.indexOf("version.json"); + assert.ok(routing !== -1, "commands/update.md 必须提到分流"); + if (probe !== -1) { + assert.ok( + routing < probe, + "commands/update.md 里分流必须写在 version.json 之前——命令与技能次序不一致会让 Agent 按旧序执行", + ); + } + }); +});