@agile-team/wl-skills-ui 1.15.0 → 1.16.0

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/CHANGELOG.md CHANGED
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.16.0] - 2026-10-08
10
+
11
+ ### Added
12
+
13
+ - 新增公开集成协议 describe/request 统一信封:能力清单与路由语料经真实打包产物验证,供外部宿主与适配器按公开契约接入。
14
+
15
+ ### Fixed
16
+
17
+ - 输入校验补齐:context 为 null、targets 为空等边界输入返回可解释拒绝,不再产生歧义结果。
18
+ - MCP 目录对齐真实 `tools/list` 的 18 项,修复目录声明与实际暴露不一致。
19
+
9
20
  ## [1.15.0] - 2026-10-07
10
21
 
11
22
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agile-team/wl-skills-ui
2
2
 
3
- **企业级 UI 一致性与 AI 精确治理工具包 v1.15.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
3
+ **企业级 UI 一致性与 AI 精确治理工具包 v1.16.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
4
4
 
5
5
  它把“视觉统一、问题识别、机械修复、项目适配和样例沉淀”拆成可验证、可组合的工程能力。脚本、规则和 MCP 先完成确定性工作,AI 只处理业务语义与人工决策,从而减少重复读源码、上下文 token 和误修改。
6
6
 
@@ -94,6 +94,23 @@ CLI、安装器和 MCP 无需安装其他 wl-skills 包或统一客户端即可
94
94
 
95
95
  `init` 新插入的 HTML tokens link 使用独立边界标记并登记贡献,`clean` 只移除未修改的本包链接,保留项目其他 HTML。历史未标记的链接缺少归属证据,升级时仍作为项目配置保留;SCSS 与 runtime 接入只给出建议,不自动修改或认领项目已有 import。
96
96
 
97
+ ## 公开集成协议 v1
98
+
99
+ 供 harness、客户端或其他编排层以统一 JSON 入口调用本包判定与状态能力(业务执行仍走本包 CLI/MCP,不因协议新增兄弟包依赖)。
100
+
101
+ ```bash
102
+ wl-ui protocol describe --json # 能力目录、五操作映射、请求/响应 Schema、错误码
103
+ wl-ui protocol request --input-file request.json --json
104
+ ```
105
+
106
+ - 操作:`route` / `explain`(只读判定)、`task`(判定并持久化计划,尚未执行)、`status`(回查本包执行回执)、`doctor-host`(宿主入口静态诊断)。
107
+ - 请求字段:`protocolVersion`(1)、`operation`、`requestId`、`runId`、`projectRoot`、`task`、`targets`、`skill`、`host`+ `profile`。
108
+ - 统一信封:`{ ok, result | error, diagnostics }`;机器结果与诊断分离;非法输入在触达执行器前判 `invalid-input`,不写入任何记录。
109
+ - 错误码:`unsupported-protocol` / `unknown-operation` / `missing-input` / `invalid-input` / `internal-error`;请求失败(exit 2)时 stdout 仍为可解析 JSON 信封。
110
+ - **`ok=true` 只表示协议调用成功,不代表业务验证通过**;业务状态以 `result` 内 `validationStatus` / `executionStatus` 为准(仅计划时为 `unverified` / `not-executed`)。
111
+ - 副作用:仅 `task` 写入本包自己的 runs 目录(见 describe 中各操作 `sideEffects`);其余操作只读。
112
+ - 协议实现为本包内置的同源快照 `integration-protocol.cjs`,五包一致;跨包一致性由 conformance `integration-protocol.check.cjs` 验收。
113
+
97
114
  ## 能力地图
98
115
 
99
116
  | 层级 | 能力 | 典型内容 | 是否需要业务源码 |
@@ -483,7 +500,7 @@ npx wl-ui doctor --project . --print-overrides
483
500
 
484
501
  ## 版本与进一步阅读
485
502
 
486
- 当前 v1.15.0:任务判定、真实执行/验证回执、实际检查范围和过期检测;Codex 原生薄入口按需加载 canonical Skill。确定性路由覆盖已发布目录与显式缺口,不保证识别所有自然语言。v1.14.0:明确独立能力边界,安装、更新、清理按本包贡献保护用户及其他包内容;共享文件采用单文件原子替换,支持历史路由与 `.clinerules` 目录迁移。v1.13.2:R044 审查多列表单超宽标签,R040 识别自定义 `stepper`,均供人工复核。v1.13.0:新增 `native-jh-ag` Profile、显式 Profile 兼容承诺与联邦 AG 依赖识别。
503
+ 当前 v1.16.0:公开集成协议 describe/request 统一信封(能力清单与路由语料经真实打包产物验证)、输入校验收口与 MCP 目录对齐真实 tools/list。v1.15.0:任务判定、真实执行/验证回执、实际检查范围和过期检测;Codex 原生薄入口按需加载 canonical Skill。确定性路由覆盖已发布目录与显式缺口,不保证识别所有自然语言。v1.14.0:明确独立能力边界,安装、更新、清理按本包贡献保护用户及其他包内容;共享文件采用单文件原子替换,支持历史路由与 `.clinerules` 目录迁移。v1.13.2:R044 审查多列表单超宽标签,R040 识别自定义 `stepper`,均供人工复核。v1.13.0:新增 `native-jh-ag` Profile、显式 Profile 兼容承诺与联邦 AG 依赖识别。
487
504
 
488
505
  - [版本摘要](docs/version-history.md)
489
506
  - [完整 Changelog](CHANGELOG.md)
@@ -0,0 +1,122 @@
1
+ /**
2
+ * protocol-cli.test.mjs — ui 公开集成协议统一信封与接线测试
3
+ */
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdtempSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import { fileURLToPath } from "node:url";
11
+ import { createRequire } from "node:module";
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
+ const { protocol, runOperation } = await import("../protocol-cli.mjs");
16
+ const pkg = require("../../package.json");
17
+ const BIN = path.join(here, "..", "wl-ui.js");
18
+
19
+ function tempRoot() {
20
+ return mkdtempSync(path.join(tmpdir(), "wl-ui-protocol-"));
21
+ }
22
+
23
+ test("describe 返回协议版本、能力目录与五个统一操作", () => {
24
+ const described = protocol.describe();
25
+ assert.equal(described.protocolVersion, 1);
26
+ assert.equal(described.package, pkg.name);
27
+ assert.ok(Array.isArray(described.capabilities) && described.capabilities.length > 0);
28
+ assert.deepEqual(described.operations.map((operation) => operation.id), ["route", "explain", "task", "status", "doctor-host"]);
29
+ assert.ok(described.errorCodes.includes("unsupported-protocol"));
30
+ });
31
+
32
+ test("route 只读判定返回六类状态之一", () => {
33
+ const envelope = protocol.request({ operation: "route", projectRoot: tempRoot(), task: "统一这个页面的表格视觉" }, runOperation);
34
+ assert.equal(envelope.ok, true);
35
+ const status = envelope.result.decision ? envelope.result.decision.status : envelope.result.status;
36
+ assert.ok(["matched", "baseline", "ambiguous", "gap", "not-applicable", "needs-context"].includes(status));
37
+ });
38
+
39
+ test("task 持久化返回 runId 且 status 可回查", () => {
40
+ const root = tempRoot();
41
+ const planned = protocol.request({ operation: "task", projectRoot: root, task: "扫描 src/views 视觉问题" }, runOperation);
42
+ assert.equal(planned.ok, true);
43
+ assert.ok(planned.result.runId);
44
+ const status = protocol.request({ operation: "status", projectRoot: root, runId: planned.result.runId }, runOperation);
45
+ assert.equal(status.ok, true);
46
+ assert.equal(status.result.runId, planned.result.runId);
47
+ assert.equal(status.result.executionStatus, "not-executed");
48
+ });
49
+
50
+ test("doctor-host 按指定 host 诊断", () => {
51
+ const envelope = protocol.request({ operation: "doctor-host", projectRoot: tempRoot(), host: "claude" }, runOperation);
52
+ assert.equal(envelope.ok, true);
53
+ assert.equal(envelope.result.host, "claude");
54
+ });
55
+
56
+ test("协议错误码:missing-input / unknown-operation / invalid-input / unsupported-protocol", () => {
57
+ assert.equal(protocol.request({ operation: "route" }, runOperation).error.code, "missing-input");
58
+ assert.equal(protocol.request({ operation: "skin" }, runOperation).error.code, "unknown-operation");
59
+ assert.equal(protocol.request(42, runOperation).error.code, "invalid-input");
60
+ assert.equal(protocol.request({ protocolVersion: 3, operation: "route", task: "x" }, runOperation).error.code, "unsupported-protocol");
61
+ });
62
+
63
+ test("边界输入校验:非法类型在触达执行器前判 invalid-input(独立复验缺陷回归)", () => {
64
+ const invalidPayloads = [
65
+ { operation: "task", task: true },
66
+ { operation: "task", task: { text: "bad" } },
67
+ { operation: "task", task: "检查目标", targets: [null, 42, {}] },
68
+ { operation: "route", task: "检查目标", projectRoot: 42 },
69
+ { operation: "task", task: "检查目标", runId: {} },
70
+ ];
71
+ for (const payload of invalidPayloads) {
72
+ const envelope = protocol.request(payload, () => { throw new Error("不应触达执行器"); });
73
+ assert.equal(envelope.ok, false);
74
+ assert.equal(envelope.error.code, "invalid-input");
75
+ assert.ok(envelope.error.field);
76
+ }
77
+ });
78
+
79
+ test("CLI 缺 --input-file 时 stdout 输出 missing-input JSON 信封", () => {
80
+ const run = spawnSync(process.execPath, [BIN, "protocol", "request"], { encoding: "utf8" });
81
+ assert.equal(run.status, 2);
82
+ const envelope = JSON.parse(run.stdout);
83
+ assert.equal(envelope.ok, false);
84
+ assert.equal(envelope.error.code, "missing-input");
85
+ assert.equal(envelope.error.field, "input-file");
86
+ });
87
+
88
+ test("CLI protocol describe / request 全链路", () => {
89
+ const describe = spawnSync(process.execPath, [BIN, "protocol", "describe"], { encoding: "utf8" });
90
+ assert.equal(describe.status, 0);
91
+ assert.equal(JSON.parse(describe.stdout).package, "@agile-team/wl-skills-ui");
92
+ const root = tempRoot();
93
+ const file = path.join(root, "req.json");
94
+ const { writeFileSync } = require("node:fs");
95
+ writeFileSync(file, JSON.stringify({ operation: "doctor-host", projectRoot: root, host: "copilot" }));
96
+ const request = spawnSync(process.execPath, [BIN, "protocol", "request", "--input-file", file], { encoding: "utf8" });
97
+ assert.equal(request.status, 0);
98
+ assert.equal(JSON.parse(request.stdout).result.host, "copilot");
99
+ });
100
+
101
+
102
+ test("context null 判 invalid-input 且零写入(CLI 回归)", () => {
103
+ const root = tempRoot();
104
+ const file = path.join(root, "req.json");
105
+ const { writeFileSync, readdirSync } = require("node:fs");
106
+ writeFileSync(file, JSON.stringify({ operation: "task", projectRoot: root, task: "扫描 src/views 视觉问题", runId: "ctx-null", context: null }));
107
+ const run = spawnSync(process.execPath, [BIN, "protocol", "request", "--input-file", file], { encoding: "utf8" });
108
+ assert.equal(run.status, 2);
109
+ const envelope = JSON.parse(run.stdout);
110
+ assert.equal(envelope.ok, false);
111
+ assert.equal(envelope.error.code, "invalid-input");
112
+ assert.equal(envelope.error.field, "context");
113
+ assert.equal(readdirSync(root).filter((name) => name.startsWith(".")).length, 0, "不得写入任何任务记录");
114
+ });
115
+
116
+ test("targets 空数组与 Schema 一致(运行时接受空范围)", () => {
117
+ const root = tempRoot();
118
+ const envelope = protocol.request({ operation: "route", projectRoot: root, task: "扫描 src/views 视觉问题", targets: [] }, runOperation);
119
+ assert.equal(envelope.ok, true);
120
+ const described = protocol.describe();
121
+ assert.equal(described.schemas.request.properties.targets.minItems, undefined, "Schema 不得要求非空 targets");
122
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * protocol-evidence.test.mjs — ui 证据闭环严格断言(不得兜底通过)
3
+ */
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdtempSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+
10
+ const { protocol, runOperation } = await import("../protocol-cli.mjs");
11
+
12
+ function tempRoot() {
13
+ return mkdtempSync(path.join(tmpdir(), "wl-ui-evidence-"));
14
+ }
15
+
16
+ test("status 严格回查同一 runId:字段精确、不兜底", () => {
17
+ const root = tempRoot();
18
+ const planned = protocol.request({ operation: "task", projectRoot: root, task: "统一表格视觉", runId: "evidence-run-a" }, runOperation);
19
+ assert.equal(planned.ok, true);
20
+ assert.equal(planned.result.runId, "evidence-run-a");
21
+ assert.equal(planned.result.executionStatus, "not-executed");
22
+ assert.equal(planned.result.validationStatus, "unverified");
23
+ const status = protocol.request({ operation: "status", projectRoot: root, runId: "evidence-run-a" }, runOperation);
24
+ assert.equal(status.result.runId, "evidence-run-a");
25
+ assert.equal(status.result.executionStatus, "not-executed");
26
+ });
27
+
28
+ test("混用 runId 不得串记录", () => {
29
+ const root = tempRoot();
30
+ protocol.request({ operation: "task", projectRoot: root, task: "任务A统一表格视觉", runId: "evidence-run-a" }, runOperation);
31
+ protocol.request({ operation: "task", projectRoot: root, task: "任务B统一表格视觉", runId: "evidence-run-b" }, runOperation);
32
+ const statusB = protocol.request({ operation: "status", projectRoot: root, runId: "evidence-run-b" }, runOperation);
33
+ assert.equal(statusB.result.runId, "evidence-run-b");
34
+ assert.equal(JSON.stringify(statusB.result).includes("任务A"), false);
35
+ });
36
+
37
+ test("不存在的 runId 不得伪造成功记录", () => {
38
+ const root = tempRoot();
39
+ const status = protocol.request({ operation: "status", projectRoot: root, runId: "no-such-run" }, runOperation);
40
+ const serialized = JSON.stringify(status.result);
41
+ assert.equal(serialized.includes('"validationStatus":"passed"'), false);
42
+ assert.equal(serialized.includes('"executionStatus":"completed"'), false);
43
+ });
44
+
45
+ test("空检查集不得判定为通过", () => {
46
+ const root = tempRoot();
47
+ const planned = protocol.request({ operation: "task", projectRoot: root, task: "统一表格视觉", runId: "empty-run" }, runOperation);
48
+ assert.notEqual(planned.result.validationStatus, "passed");
49
+ assert.equal(planned.result.executionStatus, "not-executed");
50
+ });
@@ -0,0 +1,105 @@
1
+ [
2
+ {
3
+ "task": "统一表格视觉",
4
+ "skill": "element/el-table"
5
+ },
6
+ {
7
+ "task": "查询表单密集审计",
8
+ "skill": "element/el-form"
9
+ },
10
+ {
11
+ "task": "弹窗样式对齐",
12
+ "skill": "element/el-dialog"
13
+ },
14
+ {
15
+ "task": "状态标签颜色统一",
16
+ "skill": "element/el-tag"
17
+ },
18
+ {
19
+ "task": "列表布局骨架",
20
+ "skill": "layouts/list-page"
21
+ },
22
+ {
23
+ "task": "AG Grid 适配",
24
+ "skill": "vendors/ag-grid"
25
+ },
26
+ {
27
+ "task": "UI扫描",
28
+ "skill": "ops/scan"
29
+ },
30
+ {
31
+ "task": "UI修复统一视觉",
32
+ "skill": "ops/fix"
33
+ },
34
+ {
35
+ "task": "UI迁移迁移tokens",
36
+ "skill": "ops/migrate"
37
+ },
38
+ {
39
+ "task": "设计令牌接入消除颜色硬编码",
40
+ "skill": "runtime/design-tokens"
41
+ },
42
+ {
43
+ "task": "写诗",
44
+ "skill": null,
45
+ "status": "not-applicable"
46
+ },
47
+ {
48
+ "task": "生成测试用例",
49
+ "skill": null,
50
+ "status": "needs-context"
51
+ },
52
+ {
53
+ "task": "检查",
54
+ "skill": null,
55
+ "status": "needs-context"
56
+ },
57
+ {
58
+ "task": "整理页签和卡片组件",
59
+ "skill": "element/component-family"
60
+ },
61
+ {
62
+ "task": "详情布局整改",
63
+ "skill": "layouts/detail-page"
64
+ },
65
+ {
66
+ "task": "调整表单弹窗布局",
67
+ "skill": "layouts/form-dialog"
68
+ },
69
+ {
70
+ "task": "树列表布局",
71
+ "skill": "layouts/tree-list"
72
+ },
73
+ {
74
+ "task": "做一次UI审计",
75
+ "skill": "ops/audit"
76
+ },
77
+ {
78
+ "task": "runtime迁移",
79
+ "skill": "runtime/migration"
80
+ },
81
+ {
82
+ "task": "样式统一风格对齐",
83
+ "skill": "runtime/style-align"
84
+ },
85
+ {
86
+ "task": "BaseTable 适配",
87
+ "skill": "vendors/base-table"
88
+ },
89
+ {
90
+ "task": "C组件用法审计",
91
+ "skill": "vendors/c-components"
92
+ },
93
+ {
94
+ "task": "自定义封装组件治理",
95
+ "skill": "vendors/custom-wrappers"
96
+ },
97
+ {
98
+ "task": "jh组件接入",
99
+ "skill": "vendors/jh-components"
100
+ },
101
+ {
102
+ "task": "未知封装组件排查",
103
+ "skill": "vendors/unknown-wrapper"
104
+ }
105
+ ]
@@ -0,0 +1,63 @@
1
+ /**
2
+ * protocol-routing.test.mjs — ui 路由准确性逐技能语料(预期经人工复核冻结)
3
+ * ui 是依赖型包:安装态通过真实打包产物 npm 安装到隔离项目验证。
4
+ */
5
+ import { test, before } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import path from "node:path";
10
+ import { spawnSync } from "node:child_process";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+ const { protocol, runOperation } = await import("../protocol-cli.mjs");
15
+ const cases = JSON.parse((await import("node:fs")).readFileSync(path.join(here, "protocol-routing-cases.json"), "utf8"));
16
+ const PKG_ROOT = path.join(here, "..", "..");
17
+
18
+ const state = { installedRoot: null, tarball: null };
19
+
20
+ function tempRoot() {
21
+ return mkdtempSync(path.join(tmpdir(), "wl-ui-routing-"));
22
+ }
23
+
24
+ function routeAt(projectRoot, task) {
25
+ return protocol.request({ operation: "route", projectRoot, task }, runOperation);
26
+ }
27
+
28
+ before(() => {
29
+ const packDir = tempRoot();
30
+ const pack = spawnSync("npm", ["pack", "--pack-destination", packDir, "--ignore-scripts"], { cwd: PKG_ROOT, encoding: "utf8", timeout: 180000 });
31
+ assert.equal(pack.status, 0, pack.stderr);
32
+ state.tarball = path.join(packDir, pack.stdout.trim().split("\n").pop());
33
+ state.installedRoot = tempRoot();
34
+ assert.equal(spawnSync("npm", ["init", "-y"], { cwd: state.installedRoot, encoding: "utf8" }).status, 0);
35
+ const install = spawnSync("npm", ["i", state.tarball], { cwd: state.installedRoot, encoding: "utf8", timeout: 180000 });
36
+ assert.equal(install.status, 0, install.stderr);
37
+ }, 600000);
38
+
39
+ for (const item of cases) {
40
+ const expectedBare = item.skill ? "gap" : item.status;
41
+ const expectedInstalled = item.installedStatus || (item.skill ? "matched" : item.status);
42
+
43
+ test(`未安装:「${item.task}」→ ${expectedBare}${item.skill ? ` + ${item.skill}` : ""}`, () => {
44
+ const envelope = routeAt(tempRoot(), item.task);
45
+ assert.equal(envelope.ok, true);
46
+ const decision = envelope.result.decision || envelope.result;
47
+ assert.equal(decision.status, expectedBare);
48
+ if (item.skill) assert.ok(decision.selectedSkills.includes(item.skill), `应选中 ${item.skill},实际 ${decision.selectedSkills}`);
49
+ else assert.equal((decision.selectedSkills || []).length, 0);
50
+ });
51
+
52
+ test(`已安装(真实打包产物):「${item.task}」→ ${expectedInstalled}${item.skill ? ` + ${item.skill}` : ""}`, () => {
53
+ const envelope = routeAt(state.installedRoot, item.task);
54
+ assert.equal(envelope.ok, true);
55
+ const decision = envelope.result.decision || envelope.result;
56
+ assert.equal(decision.status, expectedInstalled);
57
+ if (item.skill) assert.ok(decision.selectedSkills.includes(item.skill));
58
+ });
59
+ }
60
+
61
+ test.after(() => {
62
+ if (state.tarball) rmSync(path.dirname(state.tarball), { recursive: true, force: true });
63
+ });
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * integration-protocol.cjs — 五包统一公开集成协议 v1(单源快照)
5
+ *
6
+ * 由 kit-internal/conformance/scripts/sync-support.cjs 分发到各包,各包不得修改本文件;
7
+ * 包特定能力目录、操作映射与执行器在各包的 protocol 接线文件中提供。
8
+ *
9
+ * 协议约定:
10
+ * - describe:返回能力目录、协议版本、操作清单、输入输出约束与错误码;
11
+ * request/envelope Schema 按各包操作定义动态生成,与运行时校验一致。
12
+ * - request:所有字段在执行前完成类型校验;非法输入返回 invalid-input,不触发执行器、
13
+ * 不写入任何记录。非法的 operation/requestId 不回显到信封(保持 string|null)。
14
+ * - 信封结构互斥:ok=true 必有 result 且无 error;ok=false 必有 error 且无 result。
15
+ * - 本协议只统一判定、解释、任务记录、状态查询与宿主诊断;业务执行仍走各包原有 CLI/MCP。
16
+ */
17
+
18
+ const PROTOCOL_VERSION = 1;
19
+ const SUPPORTED_VERSIONS = [1];
20
+ const ERROR_CODES = ["unsupported-protocol", "unknown-operation", "missing-input", "invalid-input", "internal-error"];
21
+ const OPTIONAL_STRING_FIELDS = ["requestId", "runId", "projectRoot", "skill", "host", "type", "domain", "profile"];
22
+
23
+ const STRING_FIELD_SCHEMA = { type: "string", minLength: 1, pattern: "\\S" };
24
+ const ERROR_SCHEMA = {
25
+ type: "object",
26
+ required: ["code", "message"],
27
+ properties: {
28
+ code: { type: "string", enum: ERROR_CODES },
29
+ message: { type: "string", minLength: 1 },
30
+ field: { oneOf: [STRING_FIELD_SCHEMA, { type: "array", items: STRING_FIELD_SCHEMA, minItems: 1 }] },
31
+ supportedProtocolVersions: { type: "array", items: { type: "integer" }, minItems: 1 },
32
+ availableOperations: { type: "array", items: STRING_FIELD_SCHEMA, minItems: 1 },
33
+ },
34
+ additionalProperties: true,
35
+ };
36
+ const CONTEXT_FIELD_SCHEMA = {
37
+ type: "object",
38
+ properties: {
39
+ signals: { type: "array", items: STRING_FIELD_SCHEMA },
40
+ domainRelevant: { type: "boolean" },
41
+ },
42
+ additionalProperties: true,
43
+ };
44
+
45
+ function envelopeSchema(packageName) {
46
+ const stringOrNull = { type: ["string", "null"] };
47
+ return {
48
+ $schema: "https://json-schema.org/draft/2020-12/schema",
49
+ title: `wl-skills protocol envelope v1 (${packageName})`,
50
+ type: "object",
51
+ required: ["protocolVersion", "package", "packageVersion", "operation", "requestId", "ok", "diagnostics"],
52
+ properties: {
53
+ protocolVersion: { type: "integer", const: 1 },
54
+ package: { type: "string" },
55
+ packageVersion: stringOrNull,
56
+ operation: stringOrNull,
57
+ requestId: stringOrNull,
58
+ ok: { type: "boolean" },
59
+ result: { type: "object", description: "各包原执行器结果;业务状态以其中 executionStatus/validationStatus 等字段为准" },
60
+ error: ERROR_SCHEMA,
61
+ diagnostics: { type: "array", items: { type: "string" } },
62
+ },
63
+ oneOf: [
64
+ { properties: { ok: { const: true }, result: { type: "object" } }, required: ["result"], not: { required: ["error"] } },
65
+ { properties: { ok: { const: false }, error: ERROR_SCHEMA }, required: ["error"], not: { required: ["result"] } },
66
+ ],
67
+ };
68
+ }
69
+
70
+ function operationCondition(operation) {
71
+ const clauses = [];
72
+ if (operation.required.length > 0) clauses.push({ required: [...operation.required] });
73
+ if (operation.requireAny) clauses.push(...operation.requireAny.map((field) => ({ required: [field] })));
74
+ if (clauses.length === 0) return null;
75
+ return {
76
+ if: { properties: { operation: { const: operation.id } }, required: ["operation"] },
77
+ then: clauses.length === 1 ? clauses[0] : { anyOf: clauses },
78
+ };
79
+ }
80
+
81
+ function requestSchema(config) {
82
+ const {operations} = config;
83
+ const conditional = operations.map(operationCondition).filter(Boolean);
84
+ return {
85
+ $schema: "https://json-schema.org/draft/2020-12/schema",
86
+ title: `wl-skills protocol request v1 (${config.packageName})`,
87
+ type: "object",
88
+ required: ["operation"],
89
+ properties: {
90
+ protocolVersion: { type: "integer", const: 1 },
91
+ operation: { type: "string", enum: operations.map((operation) => operation.id) },
92
+ requestId: STRING_FIELD_SCHEMA,
93
+ runId: STRING_FIELD_SCHEMA,
94
+ projectRoot: STRING_FIELD_SCHEMA,
95
+ task: STRING_FIELD_SCHEMA,
96
+ targets: { type: "array", items: STRING_FIELD_SCHEMA, description: "空数组表示空范围(与运行时既有语义一致)" },
97
+ skill: STRING_FIELD_SCHEMA,
98
+ host: STRING_FIELD_SCHEMA,
99
+ type: STRING_FIELD_SCHEMA,
100
+ domain: STRING_FIELD_SCHEMA,
101
+ profile: STRING_FIELD_SCHEMA,
102
+ context: CONTEXT_FIELD_SCHEMA,
103
+ },
104
+ ...(conditional.length > 0 ? { allOf: conditional } : {}),
105
+ };
106
+ }
107
+
108
+ function isEmptyValue(value) {
109
+ return value === undefined || value === null || (typeof value === "string" && value.trim() === "");
110
+ }
111
+
112
+ function isNonEmptyString(value) {
113
+ return typeof value === "string" && value.trim() !== "";
114
+ }
115
+
116
+ function assertValidOperation(operation) {
117
+ if (!operation.id || typeof operation.summary !== "string") throw new Error(`操作定义不完整:${JSON.stringify(operation)}`);
118
+ if (typeof operation.readOnly !== "boolean") throw new Error(`操作 ${operation.id} 缺少 readOnly 声明`);
119
+ if (!Array.isArray(operation.required) || !Array.isArray(operation.optional)) throw new Error(`操作 ${operation.id} 缺少 required/optional 输入声明`);
120
+ if (operation.requireAny !== undefined && !Array.isArray(operation.requireAny)) throw new Error(`操作 ${operation.id} 的 requireAny 必须是字符串数组`);
121
+ if (typeof operation.mapping !== "string") throw new Error(`操作 ${operation.id} 缺少原 CLI 映射说明`);
122
+ }
123
+
124
+ function assertValidConfig(config) {
125
+ if (!config || typeof config !== "object") throw new Error("integration protocol 需要配置对象");
126
+ if (!config.packageName) throw new Error("integration protocol 配置缺少 packageName");
127
+ if (!Array.isArray(config.operations) || config.operations.length === 0) throw new Error("integration protocol 配置缺少 operations");
128
+ config.operations.forEach(assertValidOperation);
129
+ }
130
+
131
+ function createProtocol(config) {
132
+ assertValidConfig(config);
133
+
134
+ function base(operation, requestId) {
135
+ return {
136
+ protocolVersion: PROTOCOL_VERSION,
137
+ package: config.packageName,
138
+ packageVersion: config.packageVersion || null,
139
+ operation: isNonEmptyString(operation) ? operation : null,
140
+ requestId: isNonEmptyString(requestId) ? requestId : null,
141
+ };
142
+ }
143
+
144
+ function fail(operation, requestId, code, message, extra) {
145
+ return { ...base(operation, requestId), ok: false, error: { code, message, ...(extra || {}) }, diagnostics: [] };
146
+ }
147
+
148
+ function invalidField(input, field, expected) {
149
+ return fail(input.operation, input.requestId, "invalid-input", `字段 ${field} 非法:应为${expected}`, { field });
150
+ }
151
+
152
+ function validateContextField(input) {
153
+ const {context} = input;
154
+ if (context === null || typeof context !== "object" || Array.isArray(context)) return invalidField(input, "context", "非 null 对象(signals: string[] / domainRelevant: boolean)");
155
+ if (context.signals !== undefined && (!Array.isArray(context.signals) || context.signals.some((item) => !isNonEmptyString(item)))) {
156
+ return invalidField(input, "context.signals", "非空字符串数组");
157
+ }
158
+ if (context.domainRelevant !== undefined && typeof context.domainRelevant !== "boolean") return invalidField(input, "context.domainRelevant", "布尔值");
159
+ return null;
160
+ }
161
+
162
+ function validateStringFields(input) {
163
+ for (const field of OPTIONAL_STRING_FIELDS) {
164
+ if (input[field] !== undefined && !isNonEmptyString(input[field])) return invalidField(input, field, "非空字符串");
165
+ }
166
+ if (input.task !== undefined && !isNonEmptyString(input.task)) return invalidField(input, "task", "非空字符串");
167
+ return null;
168
+ }
169
+
170
+ function validateTargetsField(input) {
171
+ if (input.targets === undefined) return null;
172
+ if (!Array.isArray(input.targets)) return invalidField(input, "targets", "字符串数组");
173
+ if (input.targets.some((item) => !isNonEmptyString(item))) return invalidField(input, "targets", "非空字符串元素数组");
174
+ return null;
175
+ }
176
+
177
+ function validateFieldTypes(input) {
178
+ if (!isNonEmptyString(input.operation)) return invalidField(input, "operation", "非空字符串");
179
+ const stringError = validateStringFields(input);
180
+ if (stringError) return stringError;
181
+ const targetsError = validateTargetsField(input);
182
+ if (targetsError) return targetsError;
183
+ if (input.context !== undefined) {
184
+ const contextError = validateContextField(input);
185
+ if (contextError) return contextError;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function unsupportedVersionError(input) {
191
+ if (input.protocolVersion === undefined || SUPPORTED_VERSIONS.includes(input.protocolVersion)) return null;
192
+ return fail(input.operation, input.requestId, "unsupported-protocol", `不支持的协议版本:${input.protocolVersion}`, { supportedProtocolVersions: [...SUPPORTED_VERSIONS] });
193
+ }
194
+
195
+ function findOperation(input) {
196
+ const operation = config.operations.find((item) => item.id === input.operation);
197
+ if (operation) return { operation };
198
+ return { error: fail(input.operation, input.requestId, "unknown-operation", `未知操作:${isNonEmptyString(input.operation) ? input.operation : "(缺失或非法)"}`, { availableOperations: config.operations.map((item) => item.id) }) };
199
+ }
200
+
201
+ function missingInputError(input, operation) {
202
+ const missingField = operation.required.find((field) => isEmptyValue(input[field]));
203
+ if (missingField !== undefined) {
204
+ return fail(operation.id, input.requestId, "missing-input", `操作 ${operation.id} 缺少必要输入:${missingField}`, { field: missingField });
205
+ }
206
+ if (operation.requireAny && !operation.requireAny.some((field) => !isEmptyValue(input[field]))) {
207
+ return fail(operation.id, input.requestId, "missing-input", `操作 ${operation.id} 需要以下输入至少之一:${operation.requireAny.join(" / ")}`, { field: [...operation.requireAny] });
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function validateRequest(input) {
213
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
214
+ return { error: fail(null, null, "invalid-input", "request 必须是 JSON 对象") };
215
+ }
216
+ const versionError = unsupportedVersionError(input);
217
+ if (versionError) return { error: versionError };
218
+ const typeError = validateFieldTypes(input);
219
+ if (typeError) return { error: typeError };
220
+ const found = findOperation(input);
221
+ if (found.error) return found;
222
+ const { operation } = found;
223
+ const inputError = missingInputError(input, operation);
224
+ if (inputError) return { error: inputError };
225
+ return { operation };
226
+ }
227
+
228
+ function buildInventory(config) {
229
+ const inventory = config.inventory || {};
230
+ return {
231
+ skills: inventory.skills || [],
232
+ taskTypes: inventory.taskTypes || [],
233
+ commands: inventory.commands || [],
234
+ mcpTools: inventory.mcpTools || [],
235
+ loadErrors: inventory.loadErrors || [],
236
+ note: "业务执行仍走各包原 CLI/MCP;inventory 只声明公开调用映射与入口,不改变执行归属;loadErrors 非空表示目录派生存在异常,不得忽略",
237
+ };
238
+ }
239
+
240
+ function buildConstraints(config) {
241
+ return {
242
+ ...(config.constraints || {}),
243
+ instructionOnly: "未提供 programmatic 执行入口的能力均为指令指导型,由宿主/AI 按各包技能流程执行并提供证据",
244
+ evidence: "request 返回的判定与状态来自各包自身记录;宿主是否加载、读取、调用以宿主证据为准,本协议不代为宣称",
245
+ okSemantics: "ok=true 仅表示协议调用成功(判定/记录/查询完成);业务验证状态以 result 内的 validationStatus/executionStatus 等字段为准",
246
+ };
247
+ }
248
+
249
+ return {
250
+ PROTOCOL_VERSION,
251
+ SUPPORTED_VERSIONS,
252
+ ERROR_CODES,
253
+
254
+ describe() {
255
+ return {
256
+ ...base(null, null),
257
+ capabilities: config.capabilities || [],
258
+ operations: config.operations.map((operation) => ({ ...operation })),
259
+ inventory: buildInventory(config),
260
+ schemas: { request: requestSchema(config), envelope: envelopeSchema(config.packageName) },
261
+ constraints: buildConstraints(config),
262
+ errorCodes: [...ERROR_CODES],
263
+ };
264
+ },
265
+
266
+ request(input, runOperation) {
267
+ const { operation, error } = validateRequest(input);
268
+ if (error) return error;
269
+ const diagnostics = [];
270
+ try {
271
+ const result = runOperation(operation.id, input, diagnostics) ?? {};
272
+ return { ...base(operation.id, input.requestId), ok: true, result, diagnostics };
273
+ } catch (thrown) {
274
+ diagnostics.push(String((thrown && thrown.message) || thrown));
275
+ const envelope = fail(operation.id, input.requestId, "internal-error", "操作执行异常,详见 diagnostics");
276
+ envelope.diagnostics = diagnostics;
277
+ return envelope;
278
+ }
279
+ },
280
+ };
281
+ }
282
+
283
+ module.exports = { createProtocol, PROTOCOL_VERSION, SUPPORTED_VERSIONS, ERROR_CODES };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * protocol-cli.mjs — ui 的公开集成协议接线(薄层)
3
+ *
4
+ * 协议实现来自快照 bin/integration-protocol.cjs(单源 conformance/support,勿改);
5
+ * 本文件只提供 ui 的能力目录、操作映射,并复用 task-integration 的原执行器。
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { createRequire } from "node:module";
10
+ import { runTaskAction, taskCatalog } from "./task-integration.mjs";
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require("../package.json");
14
+ const capabilitiesDocument = require("./capabilities.json");
15
+ const { createProtocol } = require("./integration-protocol.cjs");
16
+
17
+ const BIN = "wl-ui";
18
+ const OPERATIONS = [
19
+ { id: "route", summary: "只读任务判定:视觉/样式域技能、约束、歧义与缺口", readOnly: true, required: ["task"], optional: ["targets", "profile", "skill", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} route --text "<task>"` },
20
+ { id: "explain", summary: "解释本次任务判定(只读,不记录)", readOnly: true, required: ["task"], optional: ["targets", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} explain --text "<task>"` },
21
+ { id: "task", summary: "判定并持久化任务计划(尚未执行扫描或修复)", readOnly: false, required: ["task"], optional: ["runId", "targets", "profile", "projectRoot"], sideEffects: "写入 .wl-skills-ui/runs/ 下本包任务记录", mapping: `${BIN} task --text "<task>" [--run-id <id>]` },
22
+ { id: "status", summary: "读取本包执行/校验记录与新鲜度", readOnly: true, required: [], optional: ["runId", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} status [--run-id <id>]` },
23
+ { id: "doctor-host", summary: "宿主入口静态诊断(不证明宿主已加载)", readOnly: true, required: [], optional: ["host", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} doctor-host [--host <host>]` },
24
+ ];
25
+
26
+ function buildInventory() {
27
+ const commands = [
28
+ { name: "init", args: "[--project <path>] [--editor <id>] [--profile <id>] [--skills-only] [--dry-run]", summary: "写入编辑器规则与技能", execution: "programmatic", sideEffects: "写入目标项目编辑器规则目录" },
29
+ { name: "update", args: "[--project <path>] [--editor <id|all>] [--force] [--dry-run]", summary: "更新已安装 rules/MCP/提示", execution: "programmatic", sideEffects: "按 manifest 更新" },
30
+ { name: "diff", args: "[--project <path>]", summary: "对比安装与 manifest", execution: "programmatic", sideEffects: "无" },
31
+ { name: "clean", args: "[--project <path>] [--dry-run]", summary: "清理本包安装文件", execution: "programmatic", sideEffects: "删除本包登记文件" },
32
+ { name: "scan", args: "[--project <path>] [--profile <id>]", summary: "R 规则扫描", execution: "programmatic", sideEffects: "无(报告输出)" },
33
+ { name: "audit", args: "[--project <path>]", summary: "审计汇总", execution: "programmatic", sideEffects: "无" },
34
+ { name: "check", args: "[--project <path>]", summary: "扫描+回执记录", execution: "programmatic", sideEffects: "写 .wl-skills-ui/runs/" },
35
+ { name: "fix", args: "--run-id <id> --rule <id> [--confirm]", summary: "确定性修复(快照/回滚)", execution: "programmatic", sideEffects: "写源文件(确认制+快照回滚)" },
36
+ { name: "snapshot", args: "/ drift / exempt", summary: "漂移基线管理", execution: "programmatic", sideEffects: "写基线文件" },
37
+ { name: "contract", args: "<extract|validate|match>", summary: "ui-contract 提取/校验/匹配", execution: "programmatic", sideEffects: "extract 写契约文件" },
38
+ { name: "doctor", args: "[--project <path>]", summary: "项目接入诊断", execution: "programmatic", sideEffects: "无" },
39
+ { name: "task", args: "\"任务\" [--run-id <id>]", summary: "判定并持久化任务计划", execution: "programmatic", sideEffects: "写 .wl-skills-ui/runs/" },
40
+ { name: "route", args: "\"任务\"", summary: "只读判定", execution: "programmatic", sideEffects: "无" },
41
+ { name: "explain", args: "\"任务\"", summary: "只读判定解释", execution: "programmatic", sideEffects: "无" },
42
+ { name: "status", args: "[--run-id <id>]", summary: "读取本包回执", execution: "programmatic", sideEffects: "无" },
43
+ { name: "doctor-host", args: "[--host <name>]", summary: "宿主入口静态诊断", execution: "programmatic", sideEffects: "无" },
44
+ { name: "protocol", args: "describe | request --input-file <file>", summary: "本公开集成协议", execution: "programmatic", sideEffects: "见操作声明" },
45
+ ];
46
+ const skills = [];
47
+ const loadErrors = [];
48
+ try {
49
+ for (const skill of taskCatalog()) {
50
+ skills.push({ id: skill.id, description: skill.description, triggers: skill.triggers, status: skill.status, entry: skill.path, execution: "instructional" });
51
+ }
52
+ } catch (error) {
53
+ loadErrors.push(`skills 加载失败:${error.message}`);
54
+ }
55
+ // 与 mcp/server.js tools/list 对齐的 18 项(由 readiness 探针持续校验数量与名称一致)
56
+ const mcpTools = [
57
+ { name: "wl_ui_task", summary: "判定并持久化 UI 任务计划", write: "persist" },
58
+ { name: "wl_ui_route", summary: "只读任务判定", write: "readonly" },
59
+ { name: "wl_ui_explain", summary: "只读判定解释", write: "readonly" },
60
+ { name: "wl_ui_status", summary: "读取执行回执", write: "readonly" },
61
+ { name: "wl_ui_doctor_host", summary: "宿主入口静态诊断", write: "readonly" },
62
+ { name: "wl_ui_check", summary: "执行 UI 检查并记录回执", write: "guarded" },
63
+ { name: "wl_ui_scan", summary: "R 规则扫描", write: "readonly" },
64
+ { name: "wl_ui_fix_dry_run", summary: "修复预演(不写盘)", write: "readonly" },
65
+ { name: "wl_ui_skill_prompt", summary: "技能提示词", write: "readonly" },
66
+ { name: "wl_ui_route_intent", summary: "意图路由", write: "readonly" },
67
+ { name: "wl_ui_detect_skin", summary: "皮肤检测", write: "readonly" },
68
+ { name: "wl_ui_list_rules", summary: "规则清单", write: "readonly" },
69
+ { name: "wl_ui_describe_rule", summary: "规则详情", write: "readonly" },
70
+ { name: "wl_ui_drift", summary: "漂移对比", write: "readonly" },
71
+ { name: "wl_ui_recommend_flow", summary: "推荐流程", write: "readonly" },
72
+ { name: "wl_ui_contract_extract", summary: "契约提取", write: "guarded" },
73
+ { name: "wl_ui_contract_validate", summary: "契约校验", write: "readonly" },
74
+ { name: "wl_ui_contract_match", summary: "契约匹配", write: "readonly" },
75
+ ];
76
+ return { skills, commands, mcpTools, loadErrors };
77
+ }
78
+
79
+ export const protocol = createProtocol({
80
+ packageName: pkg.name,
81
+ packageVersion: pkg.version,
82
+ capabilities: capabilitiesDocument.capabilities || [],
83
+ constraints: { node: (pkg.engines && pkg.engines.node) || null, boundaryVersion: capabilitiesDocument.boundaryVersion || null },
84
+ operations: OPERATIONS,
85
+ inventory: buildInventory(),
86
+ });
87
+
88
+ export function runOperation(operation, input) {
89
+ return runTaskAction(operation, {
90
+ projectRoot: input.projectRoot || ".",
91
+ task: input.task,
92
+ targets: Array.isArray(input.targets) ? input.targets : [],
93
+ runId: input.runId,
94
+ skill: input.skill,
95
+ host: input.host,
96
+ profile: input.profile,
97
+ context: input.context,
98
+ });
99
+ }
100
+
101
+ function envelopeError(code, message, field) {
102
+ return { protocolVersion: 1, package: pkg.name, packageVersion: pkg.version, operation: null, requestId: null, ok: false, error: { code, message, ...(field ? { field } : {}) }, diagnostics: [] };
103
+ }
104
+
105
+ export function runCli(argv) {
106
+ const sub = argv[0];
107
+ if (sub === "describe") {
108
+ console.log(JSON.stringify(protocol.describe(), null, 2));
109
+ return 0;
110
+ }
111
+ if (sub === "request") {
112
+ const fileIndex = argv.indexOf("--input-file");
113
+ const file = fileIndex >= 0 ? argv[fileIndex + 1] : null;
114
+ if (!file) {
115
+ console.log(JSON.stringify(envelopeError("missing-input", "缺少必要输入:--input-file <request.json>", "input-file"), null, 2));
116
+ return 2;
117
+ }
118
+ let input;
119
+ try {
120
+ input = JSON.parse(readFileSync(path.resolve(file), "utf8"));
121
+ } catch (error) {
122
+ const message = error.code === "ENOENT" ? `请求文件不存在:${file}` : `请求文件无法解析:${error.message}`;
123
+ console.log(JSON.stringify(envelopeError("invalid-input", message, "input-file"), null, 2));
124
+ return 2;
125
+ }
126
+ const envelope = protocol.request(input, runOperation);
127
+ console.log(JSON.stringify(envelope, null, 2));
128
+ return envelope.ok ? 0 : 2;
129
+ }
130
+ console.error("用法:");
131
+ console.error(` ${BIN} protocol describe --json`);
132
+ console.error(` ${BIN} protocol request --input-file <request.json> --json`);
133
+ return 2;
134
+ }
@@ -69,7 +69,10 @@ function scoreSkill(skill, text, contextText) {
69
69
  const hits = matches(text, skill.triggers);
70
70
  const negative = matches(text, skill.negative);
71
71
  const contextHits = matches(contextText, skill.contexts);
72
- const score = negative.length ? -100 : hits.length * 5 + contextHits.length * 2;
72
+ // 最长命中加权:更长的触发短语代表更具体的意图(如「表单弹窗布局」优先于「表单」「弹窗」),
73
+ // 只在同一命中数级别内打破平局,不改变命中数量的主导地位。
74
+ const longestHit = hits.reduce((max, phrase) => Math.max(max, String(phrase).length), 0);
75
+ const score = negative.length ? -100 : hits.length * 5 + contextHits.length * 2 + longestHit;
73
76
  return { id: skill.id, path: skill.path || null, score, hits, contextHits, negative, status: skill.status || "released" };
74
77
  }
75
78
 
package/bin/wl-ui.js CHANGED
@@ -78,7 +78,7 @@ const SUBCOMMANDS = new Set([
78
78
  "contract",
79
79
  "rules",
80
80
  "profiles",
81
- "task", "route", "explain", "status", "doctor-host",
81
+ "task", "route", "explain", "status", "doctor-host", "protocol",
82
82
  ]);
83
83
  let subcommand = "help";
84
84
 
@@ -95,7 +95,10 @@ if (rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0])) {
95
95
  }
96
96
 
97
97
  // 非 init 子命令:直接委托给 scanner/index.mjs
98
- if (["task", "route", "explain", "status", "doctor-host"].includes(subcommand)) {
98
+ if (subcommand === "protocol") {
99
+ const { runCli } = await import("./protocol-cli.mjs");
100
+ process.exitCode = runCli(rawArgs);
101
+ } else if (["task", "route", "explain", "status", "doctor-host"].includes(subcommand)) {
99
102
  const { taskCli } = await import("./task-integration.mjs");
100
103
  taskCli(subcommand, rawArgs);
101
104
  process.exit(0);
@@ -1370,6 +1373,8 @@ wl-ui — @agile-team/wl-skills-ui 统一 CLI v${PKG.version}
1370
1373
  查看本包实际执行、验证、检查范围和过期状态
1371
1374
  wl-ui doctor-host [--host codex|claude|copilot|cursor] [--json]
1372
1375
  静态入口诊断;宿主发现与读取仍须宿主证据
1376
+ wl-ui protocol describe --json | wl-ui protocol request --input-file <request.json> --json
1377
+ 公开集成协议:能力目录与统一判定/状态 JSON 信封
1373
1378
  wl-ui init [--project <path>] [--editor <editor>] [--profile <id>]
1374
1379
  [--dry-run] [--skills-only]
1375
1380
  把 skills/ 写入目标项目的 AI 编辑器规则目录
@@ -2,6 +2,16 @@
2
2
 
3
3
  本页保留面向使用者的版本能力摘要;逐提交级变更、修复细节和旧版本记录见根目录 [`CHANGELOG.md`](../CHANGELOG.md)。
4
4
 
5
+ ## v1.16.0 — 公开集成协议与输入校验收口(2026-10-08)
6
+
7
+ ### Added
8
+
9
+ - 新增公开集成协议 describe/request 统一信封:能力清单与路由语料以真实打包产物验证;外部适配器仅依赖公开入口,移除适配器后本包独立能力不受影响。
10
+
11
+ ### Fixed
12
+
13
+ - 输入校验收口(context null / 空 targets 返回可解释拒绝);MCP 目录与真实 `tools/list` 18 项对齐。
14
+
5
15
  ## v1.15.0 — 任务判定、真实执行回执与原生技能入口
6
16
 
7
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-ui",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "企业级 UI 风格对齐框架 — Vue + Element Plus 双模式、任务路由与真实检查回执(tokens / element / vendors / layouts / runtime / scanner / fixer / skills)",
5
5
  "type": "module",
6
6
  "main": "./es/index.js",
@@ -59,7 +59,7 @@
59
59
  "scripts": {
60
60
  "build": "tsup && npm run sync:tokens",
61
61
  "sync:tokens": "node -e \"import('fs').then(fs=>fs.copyFileSync('design/tokens/base.css','dist/tokens.css'))\"",
62
- "test": "node --test scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs",
62
+ "test": "node --test scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs bin/__tests__/*.test.mjs",
63
63
  "test:visual": "playwright test --config tests/visual/playwright.config.ts",
64
64
  "test:visual:contracts": "playwright test --config tests/visual/playwright.config.ts tests/visual/specs/browser-compat.spec.ts",
65
65
  "test:visual:ui": "playwright test --config tests/visual/playwright.config.ts --ui",
@@ -80,7 +80,7 @@
80
80
  "pack:dry": "npm pack --dry-run --ignore-scripts",
81
81
  "release:check": "npm run verify && npm run test:visual:contracts && npm run pack:dry",
82
82
  "prepublishOnly": "npm run release:check",
83
- "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=bin/vendor/** --test-coverage-lines=70 --test-coverage-branches=65 --test-coverage-functions=70 scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs",
83
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=bin/vendor/** --test-coverage-lines=70 --test-coverage-branches=65 --test-coverage-functions=70 scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs bin/__tests__/*.test.mjs",
84
84
  "benchmark:runtime": "node scripts/benchmark-observer-hub.mjs"
85
85
  },
86
86
  "files": [