@yottameta/yotta-skills 0.22.2 → 0.23.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v0.23.1 (2026-10-01)
2
+
3
+ - 文档:环境变量表补齐 `YOTTA_SKILLS_REGISTRY_FILE` / `YOTTA_SKILLS_USAGE_FILE` / `YOTTA_PROVIDER_HOME` 三项,并新增「隔离环境:三个状态文件成组导出」小节(bash / PowerShell 示例)——测试、CI 与多 agent 场景需三件成组导出,避免读写宿主真实状态。
4
+ - 行为零变更:安装 / 更新 / 路由 / hook / MCP 契约不变。
5
+
6
+ ## v0.23.0 (2026-10-01)
7
+
8
+ - O1 动态路由 MVP:`--route` / MCP `route_request` 在静态 playbook 之上,可选调用本地 provider(capability `o1.route`)做确定性组合排序;新增 `confidence` / `reasons` / `summary` / `alternatives` 输出。
9
+ - 开源侧新增 `lib/route-features.js`(请求特征 + 已装技能元数据 + 聚合使用信号 + 公开 playbook 元数据)与 `lib/route-dynamic.js`(白名单 / 枚举 / 限长校验);静态技能始终保留,provider 不能删除静态技能或改变 `missing_skills` 语义。
10
+ - `usage` 默认关闭;仅显式启用后向本地 provider 发送聚合计数(used / named / accepted / route_hits / distinct_pairs / last_signal_at),不发送需求原文、记忆正文、路径或身份信息;provider 审计仍只记元数据。
11
+ - 非元阁家族技能仍只作候选并标注 `scan_required`;不自动安装、不自动调用。
12
+ - 评分算法位于本地私有目录 `license/o1/`,不进任何发布件;未配置 / 未授权 / 超时 / 非法输出时回落静态路由,退出码保持 0。
13
+ - 元忆 0.20.0 不变;安装 / 更新 / hook / scanPolicy 契约零变更。
14
+
1
15
  ## v0.22.2 (2026-09-30)
2
16
 
3
17
  - 修复 `doctor --dir` 指向单个技能包目录时的 UX:现在会按 `SKILL.md` / `skill-manifest.json` / 目录名自动识别技能身份,不再误报「目标目录下没有可检查的元阁家族技能」。
package/README.md CHANGED
@@ -26,7 +26,7 @@ Installing the whole YottaMeta family used to mean running <code>npx</code> for
26
26
  元阁 is also the **family orchestration planner**: given a task, check the orchestration table first — which skills to combine, in what order, and why — then install exactly those. The decision table ships in the package as <code>references/orchestration.md</code> and is summarized in <code>SKILL.md</code>.
27
27
 
28
28
  - **List** - see the whole family: slug, Chinese name, package, version and a one-line description.
29
- - **Orchestration routing** - <code>--route</code> turns a task summary into a candidate combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install command; it only suggests installation and never installs automatically. An optional local provider may add or re-order installed skills within the allow-list (no behavior change when unconfigured; see <code>references/provider-protocol.md</code>).
29
+ - **Orchestration routing** - <code>--route</code> turns a task summary into a candidate combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install command; it only suggests installation and never installs automatically. An optional local provider may add or re-order installed skills within the allow-list and return <code>confidence</code> / <code>reasons</code> / <code>summary</code> / <code>alternatives</code> (no behavior change when unconfigured; see <code>references/provider-protocol.md</code>).
30
30
  - **M1 memory adjudication** - <code>decide-memory</code> returns read-only <code>promote / hold / demote</code> suggestions, scores and signal details. Scoring is performed by a user-configured local provider (capability <code>m1.adjudicate</code>). It is read-only by default; <code>--promote</code> writes only a local recommendation file and never writes memory or deletes anything. <code>usage</code> is off by default and records only structured signals after the user enables it.
31
31
  - **Install** — install everything (or a subset) into an agent's default user-level directory or any directory.
32
32
  - **Update** — incremental update: add missing skills, upgrade version-skewed ones.
@@ -121,7 +121,7 @@ Requirements: Node.js 18+, npm, and system <code>tar</code> (built into Windows
121
121
  | `--registry <url>` | npm registry to check against (default https://registry.npmjs.org/; `YOTTA_SKILLS_REGISTRY` overrides) |
122
122
  | `--inventory` | Inventory installed skills: scan skill directories and update the local registry (self-contained); `--json` for JSON, `--project` adds project-level dirs |
123
123
  | `--reindex` | Re-index the registry: re-scan skill directories and merge changes incrementally (on demand; `install` / `update` re-index automatically; `--rescan` is a synonym); `--json` for JSON |
124
- | `--route <task-summary>` | Static orchestration routing: return a combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install suggestion; also lists other installed (non-YottaMeta) skills as candidates matched mechanically against their frontmatter description, tagged "not scanned", read-only and never auto-invoked; an optional local provider may add or re-order installed skills within the allow-list (see <code>references/provider-protocol.md</code>); `--json` for JSON plus a `dynamic` status block, `--project` adds project-level dirs |
124
+ | `--route <task-summary>` | Orchestration routing: return a combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install suggestion; also lists other installed (non-YottaMeta) skills as candidates matched mechanically against their frontmatter description, tagged "not scanned", read-only and never auto-invoked; an optional local provider may add or re-order installed skills within the allow-list and return <code>confidence</code> / <code>reasons</code> / <code>summary</code> / <code>alternatives</code> (see <code>references/provider-protocol.md</code>); `--json` for JSON plus a `dynamic` status block, `--project` adds project-level dirs |
125
125
  | `usage status` / `usage enable` / `usage disable` | Inspect / enable / disable local structured usage signals; off by default and limited to slug, time, signal type, playbook / confidence and skill pairs |
126
126
  | `usage mark --skill <slug> --signal used\|named\|accepted` | Record one explicit usage signal; no prior enable required |
127
127
  | `usage reset --yes` | Clear the local usage record |
package/README.zh-CN.md CHANGED
@@ -29,7 +29,7 @@
29
29
  元阁也是全家的**编排策划层**:接到需求先查「编排策划」组合表——该组合哪几个技能、按什么顺序、各自强在哪——然后给出安装与调用**建议**,由用户确认后执行。决策表随包提供(<code>references/orchestration.md</code>),<code>SKILL.md</code> 中有摘要。
30
30
 
31
31
  - **看清单**--全家技能一览:slug / 中文名 / 包名 / 版本 / 一句话说明。
32
- - **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。可选本地扩展提供方可在已装白名单内增补 / 重排(未配置时行为不变),协议见 `references/provider-protocol.md`。
32
+ - **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。可选本地扩展提供方可在已装白名单内增补 / 重排,并返回 `confidence` / `reasons` / `summary` / `alternatives`(未配置时行为不变),协议见 `references/provider-protocol.md`。
33
33
  - **M1 记忆裁决**--`decide-memory` 输出技能 `promote / hold / demote` 只读建议、分数与信号明细;评分由用户显式配置的本地 provider(capability `m1.adjudicate`)完成。默认只读,`--promote` 只写本地建议文件,不写元忆、不删除内容。`usage` 默认关闭,显式开启后才记录结构化使用信号(不含需求原文)。
34
34
  - **安装**——装全家(或指定技能)到智能体默认用户级目录或任意目录。
35
35
  - **更新**——增量更新:补齐缺失技能、升级版本不一致的技能。
@@ -124,7 +124,7 @@ npx -y @yottameta/yotta-skills --reindex
124
124
  | `--registry <url>` | 检查的 npm registry 地址(默认 https://registry.npmjs.org/;`YOTTA_SKILLS_REGISTRY` 覆盖) |
125
125
  | `--inventory` | 盘点已装技能:扫描技能目录并更新本地注册表(自包含);`--json` 输出 JSON、`--project` 附扫项目级目录 |
126
126
  | `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后自动重扫,也可随时手动运行;`--rescan` 同义);`--json` 输出 JSON |
127
- | `--route <需求摘要>` | 静态编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地扩展提供方在已装白名单内增补 / 重排(未配置时行为不变,协议见 `references/provider-protocol.md`);`--json` 输出 JSON 与 `dynamic` 状态块、`--project` 附扫项目级目录 |
127
+ | `--route <需求摘要>` | 编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地扩展提供方在已装白名单内增补 / 重排并返回 `confidence` / `reasons` / `summary` / `alternatives`(未配置时行为不变,协议见 `references/provider-protocol.md`);`--json` 输出 JSON 与 `dynamic` 状态块、`--project` 附扫项目级目录 |
128
128
  | `usage status` / `usage enable` / `usage disable` | 查看 / 开启 / 关闭本地结构化使用记录;默认关闭,只记录 slug、时间、信号类型、playbook / confidence 与组合对 |
129
129
  | `usage mark --skill <slug> --signal used\|named\|accepted` | 记录一次显式使用信号;不要求先 enable |
130
130
  | `usage reset --yes` | 清空本地使用记录 |
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-skills
3
- version: 0.22.2
3
+ version: 0.23.1
4
4
  description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 运行时 hook 适配。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);M1 记忆裁决层:usage enable/mark 本地结构化记录 + decide-memory / MCP decide_memory 输出 promote / hold / demote 只读建议(需授权 provider;只建议不删除、不自动写元忆);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录(默认 --pin 锁死清单精确版本);盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工只建议跑本地 --reindex;更新检查走手动 --check 或后台 --check --scheduled);运行时适配层:hook capabilities / evaluate / bind / unbind 按宿主能力矩阵执行六个统一事件并留证降级,元信 before_install 已接入安装管线;自包含零依赖,不依赖任何元技能;MCP 按需加载且需用户确认后写入配置(可选:list_installed_skills/describe_skill/reindex/route_request/decide_memory,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / usage 使用记录 / decide-memory 记忆裁决 / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、判断哪些技能值得长期记忆、查看或记录技能使用信号、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/记忆裁决/技能该不该记住/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + M1 记忆裁决只读建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆、不自动删除技能或记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
5
5
  license: MIT
6
6
  metadata:
@@ -43,7 +43,7 @@ metadata:
43
43
  **元阁全家的总编排策划 + 编排路由 + 一键安装 + 技能盘点**,一句话四层:
44
44
 
45
45
  - **路由层(该用哪几个)**:`--route "<需求摘要>"` / MCP `route_request` 查本地注册表与静态编排 playbook,输出候选组合、调用顺序、每个技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。
46
- - **动态扩展口(v0.20.0,capability `o1.route`)**:可选调用用户显式配置的本地扩展提供方(provider),在已装注册表白名单内对路由结果增补 / 重排;静态 playbook 结果先算必算、永不缺席,未配置或调用失败时文本输出与历史一致,`--json` 仅多一个 `dynamic` 状态块。协议与配置见 `references/provider-protocol.md`。
46
+ - **O1 动态路由层(v0.23.0,capability `o1.route`)**:可选调用用户显式配置的本地扩展提供方(provider),在已装注册表白名单内对路由结果增补 / 重排,并返回 `confidence` / `reasons` / `summary` / `alternatives`;静态 playbook 结果先算必算、永不缺席,未配置或调用失败时文本输出与历史一致,`--json` 仅多一个 `dynamic` 状态块。协议与配置见 `references/provider-protocol.md`。
47
47
  - **M1 记忆裁决层(哪些值得长期记住)**:`decide-memory` / MCP `decide_memory` 调用可选本地 provider(capability `m1.adjudicate`),输出 `promote / hold / demote` 只读建议、分数与信号明细;默认不写元忆、不删除技能。`usage` 默认关闭,用户显式开启后才记录结构化使用信号。协议与边界见 `references/provider-protocol.md`。
48
48
  - **策划层(怎么用)**:接到需求,先按「编排策划」定位命中哪个组合——哪些元技能搭配起来最强、适合什么场景、AI 该给出什么安装与调用建议(**安装与调用均由用户确认后执行**)。
49
49
  - **安装层(怎么装)**:一条 `npx -y @yottameta/yotta-skills` 把组合/全家装进指定智能体或目录——`--list` 看清单、`install` 装、`update` 增量更新、`doctor` 只读自检、`rollback` 恢复快照、`--dry-run` 预览、`--pin` 锁版本。
@@ -173,7 +173,7 @@ openclaw skills update @yottameta/yotta-skills
173
173
  | `hook bind --host <name> --manifest <file>` | 幂等注册 hook 声明;`hook unbind <id>` 反注册 |
174
174
  | `--inventory` | 盘点本机已装技能:扫描技能目录生成/更新注册表(自包含,不依赖元技能);`--json` 输出 JSON、`--project` 附扫项目级目录 |
175
175
  | `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后 CLI 自动重扫;也可在会话开工等时机手动运行);`--json` 输出 JSON、`--project` 附扫项目级目录;`--rescan` 同义 |
176
- | `--route <需求摘要>` | 静态编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;`--json` 输出 JSON、`--project` 附扫项目级目录 |
176
+ | `--route <需求摘要>` | 编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地 provider 增补 / 重排并返回 `confidence` / `reasons` / `summary` / `alternatives`;`--json` 输出 JSON、`--project` 附扫项目级目录 |
177
177
  | `usage status` | 查看本地使用记录开关与计数;默认关闭,不创建文件 |
178
178
  | `usage enable` / `usage disable` | 开启 / 关闭 `--route` 的结构化使用记录;不记录需求原文 |
179
179
  | `usage mark --skill <slug> --signal used\|named\|accepted` | 记录一次显式使用信号;不要求先 enable |
@@ -340,7 +340,7 @@ npx -y @yottameta/yotta-skills usage reset --yes
340
340
 
341
341
  本技能自带一个 MCP server:`yotta-skills`(`scripts/yotta-skills-mcp.py`,零依赖、数据不出本机),
342
342
  基于 MCP 最新协议 2026-07-28(无状态时代;向后兼容 2025-11-25 及更早握手客户端)。
343
- 提供 `list_installed_skills`(盘点)/ `describe_skill`(单技能详情)/ `reindex`(强制重扫)/ `route_request`(静态编排路由)/ `decide_memory`(M1 记忆裁决只读建议)五个工具。
343
+ 提供 `list_installed_skills`(盘点)/ `describe_skill`(单技能详情)/ `reindex`(强制重扫)/ `route_request`(编排路由,静态基线 + 可选本地动态扩展)/ `decide_memory`(M1 记忆裁决只读建议)五个工具。
344
344
 
345
345
  **按需加载,不走常驻**:本技能与 MCP 均为按需触发。默认以 CLI 为主
346
346
  (`npx -y @yottameta/yotta-skills --inventory`);需要让 AI 通过工具直接调用时,再按下面配置启用。
@@ -417,6 +417,29 @@ npx -y @yottameta/yotta-skills usage reset --yes
417
417
  | `YOTTA_SKILLS_PYTHON` | 指定 python 可执行文件(元信 scan 用,同 `--python`) |
418
418
  | `YOTTA_SKILLS_VERIFY` | 指定 yotta_verify.py 路径(同 `--verify`) |
419
419
  | `YOTTA_SKILLS_MANIFEST` | 指定技能清单 JSON 路径(默认随包 skills.json) |
420
+ | `YOTTA_SKILLS_REGISTRY_FILE` | 技能注册表文件路径(多 agent / 隔离环境用;默认 `~/.yottaskills/registry.json`) |
421
+ | `YOTTA_SKILLS_USAGE_FILE` | 使用记录文件路径(`usage` 记录落点;默认 `~/.yottaskills/usage.json`) |
422
+ | `YOTTA_PROVIDER_HOME` | provider 配置根目录(M1 裁决 / O1 动态路由;默认 `~/.yottameta/`,协议见 `references/provider-protocol.md`) |
423
+
424
+ ### 隔离环境:三个状态文件成组导出
425
+
426
+ 测试、CI 与多 agent 共存场景建议把下面三个状态文件变量**成组**导出,避免读写宿主真实状态;
427
+ 只导出其中一个时,其余状态仍会落到用户目录(例如漏掉 `YOTTA_SKILLS_USAGE_FILE` 时,
428
+ `usage` 记录仍会写入 `~/.yottaskills/usage.json`)。
429
+
430
+ ```bash
431
+ # bash(Linux / macOS / Git Bash)
432
+ export YOTTA_SKILLS_REGISTRY_FILE="${TMPDIR:-/tmp}/registry.json"
433
+ export YOTTA_SKILLS_USAGE_FILE="${TMPDIR:-/tmp}/usage.json"
434
+ export YOTTA_PROVIDER_HOME="${TMPDIR:-/tmp}/provider"
435
+ ```
436
+
437
+ ```powershell
438
+ # PowerShell(Windows)
439
+ $env:YOTTA_SKILLS_REGISTRY_FILE = "$env:TEMP\registry.json"
440
+ $env:YOTTA_SKILLS_USAGE_FILE = "$env:TEMP\usage.json"
441
+ $env:YOTTA_PROVIDER_HOME = "$env:TEMP\provider"
442
+ ```
420
443
 
421
444
  ## 常见问题
422
445
 
@@ -39,10 +39,12 @@ const scanPolicyLib = require('../lib/scan-policy');
39
39
  const npmPackLib = require('../lib/npm-pack');
40
40
  const usageJournalLib = require('../lib/usage-journal');
41
41
  const m1FeaturesLib = require('../lib/m1-features');
42
+ const routeFeaturesLib = require('../lib/route-features');
43
+ const routeDynamicLib = require('../lib/route-dynamic');
42
44
  const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
43
45
 
44
46
  const PKG_ROOT = path.join(__dirname, '..');
45
- let VERSION = '0.22.2';
47
+ let VERSION = '0.23.1';
46
48
  try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
47
49
 
48
50
  function loadManifest() {
@@ -1250,95 +1252,40 @@ function runReindex(opts) {
1250
1252
  out('注册表: ' + scan.registryPath());
1251
1253
  }
1252
1254
 
1253
- function dynamicBlock(status, providerId, note) {
1254
- return {
1255
- status: status || 'not_installed',
1256
- provider_id: providerId || '',
1257
- applied: false,
1258
- added: [],
1259
- dropped: [],
1260
- note: note || '',
1261
- };
1262
- }
1263
-
1264
1255
  /**
1265
1256
  * o1.route 动态扩展口:静态结果先算必算;provider 只允许在已装注册表白名单内增补 / 重排,
1266
1257
  * 任何失败都回静态结果(fail-open)。协议见 references/provider-protocol.md。
1267
1258
  */
1268
1259
  function applyDynamicRoute(result, registry, opts) {
1269
1260
  const provider = require('../lib/provider');
1261
+ const { PLAYBOOKS } = require('../lib/route');
1262
+ const usage = usageJournalLib.readUsage();
1263
+ const payload = routeFeaturesLib.buildRouteFeatures({
1264
+ request: String(opts.route || ''),
1265
+ registry,
1266
+ staticResult: result,
1267
+ usage,
1268
+ playbooks: PLAYBOOKS,
1269
+ });
1270
1270
  let run;
1271
1271
  try {
1272
- run = provider.runCapability('o1.route', {
1273
- request: String(opts.route || ''),
1274
- static_result: {
1275
- playbook: result.playbook.id,
1276
- confidence: result.confidence,
1277
- skills: result.skills.map((skill) => skill.slug),
1278
- },
1279
- installed_skills: Object.values(registry.skills || {})
1280
- .filter((record) => record && record.status !== 'gone')
1281
- .map((record) => ({ slug: record.slug, version: record.version || '', sources: record.sources || [] })),
1282
- });
1272
+ run = provider.runCapability('o1.route', payload);
1283
1273
  } catch (e) {
1284
- return dynamicBlock('error', '', '动态路由装载失败:' + e.message);
1274
+ return routeDynamicLib.dynamicBlock('error', '', '动态路由装载失败:' + e.message);
1285
1275
  }
1286
- const block = dynamicBlock(run.status, run.provider_id, run.note || run.message || '');
1276
+ const block = routeDynamicLib.dynamicBlock(run.status, run.provider_id, run.note || run.message || '');
1287
1277
  if (run.status !== 'active' || !run.data || typeof run.data !== 'object') return block;
1288
-
1289
- const installed = new Set(Object.values(registry.skills || {})
1290
- .filter((record) => record && record.status !== 'gone')
1291
- .map((record) => record.slug));
1292
- const requested = Array.isArray(run.data.skills) ? run.data.skills : [];
1293
- const seen = new Set();
1294
- const ordered = [];
1295
- for (const item of requested) {
1296
- const slug = item && typeof item === 'object' ? String(item.slug || '') : '';
1297
- if (!slug) continue;
1298
- if (seen.has(slug) || !installed.has(slug)) {
1299
- block.dropped.push(slug);
1300
- continue;
1301
- }
1302
- seen.add(slug);
1303
- ordered.push({ slug, role: typeof item.role === 'string' ? item.role : '' });
1304
- }
1305
- if (!ordered.length) return block;
1306
-
1307
- const staticBySlug = new Map(result.skills.map((skill) => [skill.slug, skill]));
1308
- const next = [];
1309
- for (const item of ordered) {
1310
- const existing = staticBySlug.get(item.slug);
1311
- if (existing) {
1312
- next.push(existing);
1313
- continue;
1314
- }
1315
- const record = registry.skills[item.slug] || {};
1316
- next.push({
1317
- slug: item.slug,
1318
- order: 0,
1319
- role: item.role || '由扩展提供方补充',
1320
- installed: true,
1321
- version: record.version || '',
1322
- sources: record.sources || [],
1323
- variants: Array.isArray(record.variants) ? record.variants : [],
1324
- conflicts: Array.isArray(record.conflicts) ? record.conflicts : [],
1325
- });
1326
- }
1327
- for (const skill of result.skills) {
1328
- if (!next.some((item) => item.slug === skill.slug)) next.push(skill);
1329
- }
1330
- next.forEach((skill, index) => { skill.order = index + 1; });
1331
- block.added = next.filter((skill) => !staticBySlug.has(skill.slug)).map((skill) => skill.slug);
1332
- block.applied = true;
1333
- result.skills = next;
1334
- return block;
1278
+ return routeDynamicLib.applyDynamicData(result, run.data, registry, {
1279
+ providerId: run.provider_id,
1280
+ });
1335
1281
  }
1336
1282
 
1337
1283
  function dynamicRouteText(block) {
1338
1284
  if (!block) return '';
1339
1285
  if (block.status === 'active') {
1340
1286
  const who = block.provider_id ? '提供方 ' + block.provider_id : '提供方';
1341
- return '已应用(' + who + ';新增 ' + block.added.length + ' / 丢弃 ' + block.dropped.length + ')';
1287
+ const confidence = block.confidence ? ';置信度 ' + block.confidence : '';
1288
+ return '已应用(' + who + confidence + ';新增 ' + block.added.length + ' / 丢弃 ' + block.dropped.length + ')';
1342
1289
  }
1343
1290
  if (block.status === 'license_required') return '需授权(该能力需要授权后使用;静态路由不受影响)';
1344
1291
  if (block.status === 'timeout') return '未生效(提供方超时,已回落静态路由)';
@@ -1402,6 +1349,10 @@ function runRoute(opts) {
1402
1349
  if (result.dynamic.status !== 'not_installed') {
1403
1350
  out('');
1404
1351
  out('动态路由: ' + dynamicRouteText(result.dynamic));
1352
+ if (result.dynamic.status === 'active' && opts.explain) {
1353
+ if (result.dynamic.summary) out('动态摘要: ' + result.dynamic.summary);
1354
+ for (const reason of result.dynamic.reasons || []) out(' - ' + reason);
1355
+ }
1405
1356
  }
1406
1357
  out('');
1407
1358
  out('应用模式: 显式调用(可经用户确认后切换为按场景自动调用)');
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 元阁 O1 动态路由:provider 响应白名单校验与应用。
5
+ *
6
+ * provider 输出一律当数据:未知 slug 丢弃、非法字段忽略、展示文本限长去控制字符。
7
+ * 静态 playbook 是锚点:provider 只允许在已装白名单内增补 / 重排,不能删除静态技能。
8
+ */
9
+
10
+ const CONFIDENCE = new Set(['high', 'medium', 'low']);
11
+ const MAX_SKILLS = 8;
12
+ const MAX_ADDED = 3;
13
+ const MAX_REASONS = 20;
14
+ const MAX_ALTERNATIVES = 3;
15
+ const MAX_ROLE = 160;
16
+ const MAX_REASON = 240;
17
+ const MAX_SUMMARY = 320;
18
+
19
+ function sanitizeText(value, maxLen) {
20
+ return String(value || '')
21
+ .replace(/[\u0000-\u001f\u007f]/g, ' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim()
24
+ .slice(0, maxLen);
25
+ }
26
+
27
+ function sanitizeList(value, maxItems, maxLen) {
28
+ if (!Array.isArray(value)) return [];
29
+ const out = [];
30
+ const seen = new Set();
31
+ for (const item of value) {
32
+ const text = sanitizeText(item, maxLen);
33
+ if (!text || seen.has(text)) continue;
34
+ seen.add(text);
35
+ out.push(text);
36
+ if (out.length >= maxItems) break;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function dynamicBlock(status, providerId, note) {
42
+ return {
43
+ status: status || 'not_installed',
44
+ provider_id: providerId || '',
45
+ applied: false,
46
+ confidence: '',
47
+ added: [],
48
+ dropped: [],
49
+ summary: '',
50
+ reasons: [],
51
+ alternatives: [],
52
+ note: note || '',
53
+ };
54
+ }
55
+
56
+ function installedSet(registry) {
57
+ return new Set(Object.values((registry && registry.skills) || {})
58
+ .filter((record) => record && record.slug && record.status !== 'gone')
59
+ .map((record) => String(record.slug)));
60
+ }
61
+
62
+ function normalizeRequestedSkills(data, installed) {
63
+ const requested = Array.isArray(data && data.skills) ? data.skills : [];
64
+ const seen = new Set();
65
+ const ordered = [];
66
+ const dropped = [];
67
+ for (const raw of requested) {
68
+ const slug = raw && typeof raw === 'object' ? String(raw.slug || '') : '';
69
+ if (!slug) continue;
70
+ if (seen.has(slug) || !installed.has(slug)) {
71
+ dropped.push(slug);
72
+ continue;
73
+ }
74
+ seen.add(slug);
75
+ const score = Number(raw.score);
76
+ ordered.push({
77
+ slug,
78
+ role: sanitizeText(raw.role, MAX_ROLE),
79
+ reason: sanitizeText(raw.reason, MAX_REASON),
80
+ score: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score))) : null,
81
+ });
82
+ if (ordered.length >= MAX_SKILLS) break;
83
+ }
84
+ return { ordered, dropped };
85
+ }
86
+
87
+ function normalizeAlternatives(value, installed, selected) {
88
+ if (!Array.isArray(value)) return [];
89
+ const out = [];
90
+ const seen = new Set();
91
+ for (const raw of value) {
92
+ const slug = raw && typeof raw === 'object' ? String(raw.slug || '') : '';
93
+ if (!slug || seen.has(slug) || !installed.has(slug) || selected.has(slug)) continue;
94
+ seen.add(slug);
95
+ const score = Number(raw.score);
96
+ out.push({
97
+ slug,
98
+ score: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score))) : null,
99
+ reason: sanitizeText(raw.reason, MAX_REASON),
100
+ });
101
+ if (out.length >= MAX_ALTERNATIVES) break;
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * 把 provider 的 data 应用到静态结果。
108
+ * 返回 dynamic 块;若 provider 未给出任何有效技能,则不修改 result。
109
+ */
110
+ function applyDynamicData(result, data, registry, options) {
111
+ const opts = options || {};
112
+ const block = dynamicBlock('active', opts.providerId || '', '');
113
+ const installed = installedSet(registry);
114
+ const staticBySlug = new Map((result.skills || []).map((skill) => [skill.slug, skill]));
115
+ const { ordered, dropped } = normalizeRequestedSkills(data, installed);
116
+ block.dropped = dropped;
117
+ if (!ordered.length) {
118
+ block.note = '提供方未返回白名单内技能';
119
+ return block;
120
+ }
121
+
122
+ const next = [];
123
+ for (const item of ordered) {
124
+ const existing = staticBySlug.get(item.slug);
125
+ if (existing) {
126
+ next.push(Object.assign({}, existing, {
127
+ role: item.role || existing.role,
128
+ dynamic_reason: item.reason || '',
129
+ dynamic_score: item.score,
130
+ }));
131
+ continue;
132
+ }
133
+ const record = (registry.skills && registry.skills[item.slug]) || {};
134
+ next.push({
135
+ slug: item.slug,
136
+ order: 0,
137
+ role: item.role || '由动态路由补充',
138
+ installed: true,
139
+ version: record.version || '',
140
+ sources: record.sources || [],
141
+ variants: Array.isArray(record.variants) ? record.variants : [],
142
+ conflicts: Array.isArray(record.conflicts) ? record.conflicts : [],
143
+ dynamic_reason: item.reason || '',
144
+ dynamic_score: item.score,
145
+ });
146
+ }
147
+ for (const skill of result.skills || []) {
148
+ if (!next.some((item) => item.slug === skill.slug)) next.push(skill);
149
+ }
150
+ next.forEach((skill, index) => { skill.order = index + 1; });
151
+
152
+ const added = next.filter((skill) => !staticBySlug.has(skill.slug)).map((skill) => skill.slug);
153
+ if (added.length > MAX_ADDED) {
154
+ const allowed = new Set(added.slice(0, MAX_ADDED));
155
+ for (const skill of next.slice()) {
156
+ if (!staticBySlug.has(skill.slug) && !allowed.has(skill.slug)) {
157
+ block.dropped.push(skill.slug);
158
+ }
159
+ }
160
+ for (let i = next.length - 1; i >= 0; i--) {
161
+ if (!staticBySlug.has(next[i].slug) && !allowed.has(next[i].slug)) next.splice(i, 1);
162
+ }
163
+ next.forEach((skill, index) => { skill.order = index + 1; });
164
+ }
165
+
166
+ const selected = new Set(next.map((skill) => skill.slug));
167
+ block.applied = true;
168
+ block.confidence = CONFIDENCE.has(data && data.confidence) ? data.confidence : (result.confidence || '');
169
+ block.summary = sanitizeText(data && data.summary, MAX_SUMMARY);
170
+ block.reasons = sanitizeList(data && data.reasons, MAX_REASONS, MAX_REASON);
171
+ block.alternatives = normalizeAlternatives(data && data.alternatives, installed, selected);
172
+ block.added = next.filter((skill) => !staticBySlug.has(skill.slug)).map((skill) => skill.slug);
173
+ result.skills = next;
174
+ return block;
175
+ }
176
+
177
+ module.exports = {
178
+ CONFIDENCE,
179
+ MAX_SKILLS,
180
+ MAX_ADDED,
181
+ MAX_REASONS,
182
+ MAX_ALTERNATIVES,
183
+ sanitizeText,
184
+ sanitizeList,
185
+ dynamicBlock,
186
+ installedSet,
187
+ applyDynamicData,
188
+ };
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 元阁 O1 动态路由:开源侧确定性特征快照。
5
+ *
6
+ * 边界:
7
+ * - 只读注册表元数据与本地使用记录的聚合计数;不读 SKILL.md 全文。
8
+ * - 不评分、不排序;评分在私有 provider 中完成。
9
+ * - 不写文件、不联网、不调用模型。
10
+ */
11
+
12
+ const crypto = require('crypto');
13
+
14
+ const MAX_TOKENS = 64;
15
+ const MAX_INSTALLED = 500;
16
+ const MAX_PLAYBOOK_MATCHES = 5;
17
+ const MAX_DESCRIPTION = 500;
18
+
19
+ const EN_STOPWORDS = new Set([
20
+ 'a', 'an', 'the', 'and', 'or', 'of', 'to', 'in', 'on', 'for', 'with', 'from',
21
+ 'this', 'that', 'is', 'are', 'was', 'were', 'be', 'been', 'your', 'you', 'our',
22
+ 'we', 'it', 'at', 'by', 'as', 'not', 'no', 'so', 'do', 'does',
23
+ ]);
24
+
25
+ function normalizeText(text) {
26
+ return String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
27
+ }
28
+
29
+ function normalizedKeyword(text) {
30
+ return normalizeText(text).replace(/\s+/g, '');
31
+ }
32
+
33
+ function englishTokens(text) {
34
+ const out = [];
35
+ const seen = new Set();
36
+ for (const match of normalizeText(text).match(/[a-z][a-z0-9]{2,}/g) || []) {
37
+ if (EN_STOPWORDS.has(match) || seen.has(match)) continue;
38
+ seen.add(match);
39
+ out.push(match);
40
+ if (out.length >= MAX_TOKENS) break;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function cjkBigrams(text) {
46
+ const out = [];
47
+ const seen = new Set();
48
+ for (const match of String(text || '').match(/[\u4e00-\u9fff]+/g) || []) {
49
+ for (let i = 0; i < match.length - 1; i++) {
50
+ const bigram = match.slice(i, i + 2);
51
+ if (seen.has(bigram)) continue;
52
+ seen.add(bigram);
53
+ out.push(bigram);
54
+ if (out.length >= MAX_TOKENS) return out;
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function requestHash(request) {
61
+ return crypto.createHash('sha256').update(normalizeText(request), 'utf8').digest('hex');
62
+ }
63
+
64
+ function playbookMatches(request, playbooks) {
65
+ const text = normalizedKeyword(request);
66
+ const matches = [];
67
+ for (const playbook of Array.isArray(playbooks) ? playbooks : []) {
68
+ const matched = [];
69
+ let score = 0;
70
+ for (const keyword of Array.isArray(playbook.keywords) ? playbook.keywords : []) {
71
+ const normalized = normalizedKeyword(keyword);
72
+ if (!normalized || !text.includes(normalized)) continue;
73
+ matched.push(String(keyword));
74
+ score += normalized.length >= 3 ? 2 : 1;
75
+ }
76
+ if (score > 0) {
77
+ matches.push({
78
+ id: String(playbook.id || ''),
79
+ score,
80
+ matched_keywords: matched,
81
+ });
82
+ }
83
+ }
84
+ return matches
85
+ .sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
86
+ .slice(0, MAX_PLAYBOOK_MATCHES);
87
+ }
88
+
89
+ function trustOf(record) {
90
+ const slug = String(record && record.slug || '');
91
+ const sources = Array.isArray(record && record.sources) ? record.sources.join(' ').toLowerCase() : '';
92
+ if (slug.startsWith('yotta-') || sources.includes('yottameta')) return 'yottameta';
93
+ return 'third-party';
94
+ }
95
+
96
+ function skillUsage(usage, slug) {
97
+ const source = usage && usage.skills && usage.skills[slug] ? usage.skills[slug] : {};
98
+ const pairs = source.pairs && typeof source.pairs === 'object' && !Array.isArray(source.pairs)
99
+ ? source.pairs
100
+ : {};
101
+ return {
102
+ used: Number(source.used) || 0,
103
+ named: Number(source.named) || 0,
104
+ accepted: Number(source.accepted) || 0,
105
+ route_hits: Number(source.route_hits) || 0,
106
+ distinct_pairs: Object.keys(pairs).length,
107
+ last_signal_at: String(source.last_signal_at || ''),
108
+ };
109
+ }
110
+
111
+ function buildRouteFeatures(options) {
112
+ const opts = options || {};
113
+ const request = String(opts.request || '');
114
+ const registry = opts.registry || { skills: {} };
115
+ const staticResult = opts.staticResult || {};
116
+ const usage = opts.usage || {};
117
+ const playbooks = Array.isArray(opts.playbooks) ? opts.playbooks : [];
118
+ const usageEnabled = usage.enabled === true;
119
+
120
+ const installedSkills = Object.values(registry.skills || {})
121
+ .filter((record) => record && record.slug && record.status !== 'gone')
122
+ .slice(0, MAX_INSTALLED)
123
+ .map((record) => ({
124
+ slug: String(record.slug),
125
+ version: String(record.version || ''),
126
+ description: String(record.description || '').slice(0, MAX_DESCRIPTION),
127
+ status: String(record.status || 'known'),
128
+ sources: Array.isArray(record.sources) ? record.sources.slice(0, 8).map(String) : [],
129
+ first_seen: String(record.first_seen || ''),
130
+ last_seen: String(record.last_seen || ''),
131
+ trust: trustOf(record),
132
+ }));
133
+
134
+ const usageSkills = {};
135
+ if (usageEnabled) {
136
+ for (const slug of Object.keys(usage.skills || {}).sort()) {
137
+ usageSkills[slug] = skillUsage(usage, slug);
138
+ }
139
+ }
140
+
141
+ return {
142
+ schema: 2,
143
+ request,
144
+ request_features: {
145
+ request_hash: requestHash(request),
146
+ english_tokens: englishTokens(request),
147
+ cjk_bigrams: cjkBigrams(request),
148
+ playbook_matches: playbookMatches(request, playbooks),
149
+ },
150
+ static_result: {
151
+ playbook: String(staticResult.playbook && staticResult.playbook.id || staticResult.playbook || ''),
152
+ confidence: String(staticResult.confidence || ''),
153
+ skills: Array.isArray(staticResult.skills)
154
+ ? staticResult.skills.map((skill) => String(skill && skill.slug || skill || '')).filter(Boolean)
155
+ : [],
156
+ },
157
+ installed_skills: installedSkills,
158
+ usage: {
159
+ enabled: usageEnabled,
160
+ skills: usageSkills,
161
+ last_route: usage.last_route && typeof usage.last_route === 'object'
162
+ ? {
163
+ at: String(usage.last_route.at || ''),
164
+ playbook: String(usage.last_route.playbook || ''),
165
+ confidence: String(usage.last_route.confidence || ''),
166
+ skills: Array.isArray(usage.last_route.skills) ? usage.last_route.skills.map(String).slice(0, 12) : [],
167
+ }
168
+ : null,
169
+ },
170
+ playbooks: playbooks.map((playbook) => ({
171
+ id: String(playbook.id || ''),
172
+ name: String(playbook.name || ''),
173
+ intent: String(playbook.intent || ''),
174
+ keywords: Array.isArray(playbook.keywords) ? playbook.keywords.slice(0, 32).map(String) : [],
175
+ skills: Array.isArray(playbook.skills)
176
+ ? playbook.skills.map((skill) => String(skill && skill.slug || skill || '')).filter(Boolean)
177
+ : [],
178
+ })),
179
+ };
180
+ }
181
+
182
+ module.exports = {
183
+ MAX_TOKENS,
184
+ MAX_INSTALLED,
185
+ MAX_PLAYBOOK_MATCHES,
186
+ normalizeText,
187
+ englishTokens,
188
+ cjkBigrams,
189
+ requestHash,
190
+ playbookMatches,
191
+ trustOf,
192
+ skillUsage,
193
+ buildRouteFeatures,
194
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-skills",
3
- "version": "0.22.2",
3
+ "version": "0.23.1",
4
4
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,5 +1,8 @@
1
1
  # 扩展提供方协议 v1(元阁 · capability `o1.route` / `m1.adjudicate`)
2
2
 
3
+ > O1 动态路由的请求 payload 已加法扩展到 schema 2(新增 `request_features` / `usage` / `playbooks`);
4
+ > 旧 provider 仍可按原 `skills[]` 响应工作,新 provider 可返回 `confidence` / `reasons` / `summary` / `alternatives`。
5
+
3
6
  元阁可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**:
4
7
 
5
8
  - `o1.route`:在已装技能白名单内对静态路由结果做增补 / 重排;未配置时 `--route` 行为与输出与之前完全一致。
@@ -54,6 +57,63 @@
54
57
  }
55
58
  ```
56
59
 
60
+ ### 2.1 O1 动态路由 payload(schema 2)
61
+
62
+ ```json
63
+ {
64
+ "schema": 1,
65
+ "capability": "o1.route",
66
+ "request_id": "<uuid>",
67
+ "payload": {
68
+ "schema": 2,
69
+ "request": "帮我做发布前质量检查",
70
+ "request_features": {
71
+ "request_hash": "<sha256>",
72
+ "english_tokens": ["release", "quality"],
73
+ "cjk_bigrams": ["发布", "质量", "检查"],
74
+ "playbook_matches": [
75
+ { "id": "delivery-quality-gate", "score": 6, "matched_keywords": ["检查", "质量", "发布"] }
76
+ ]
77
+ },
78
+ "static_result": {
79
+ "playbook": "delivery-quality-gate",
80
+ "confidence": "high",
81
+ "skills": ["yotta-anti-shallow", "yotta-code-quality", "yotta-publish-guard"]
82
+ },
83
+ "installed_skills": [
84
+ {
85
+ "slug": "yotta-code-quality",
86
+ "version": "0.3.0",
87
+ "description": "代码质量评审...",
88
+ "status": "known",
89
+ "sources": ["Codex"],
90
+ "first_seen": "2026-08-23T00:00:00Z",
91
+ "last_seen": "2026-10-01T00:00:00Z",
92
+ "trust": "yottameta"
93
+ }
94
+ ],
95
+ "usage": {
96
+ "enabled": false,
97
+ "skills": {},
98
+ "last_route": null
99
+ },
100
+ "playbooks": [
101
+ {
102
+ "id": "delivery-quality-gate",
103
+ "name": "交付质量门",
104
+ "keywords": ["检查", "代码", "质量", "发布"],
105
+ "skills": ["yotta-anti-shallow", "yotta-code-quality", "yotta-publish-guard"]
106
+ }
107
+ ]
108
+ }
109
+ }
110
+ ```
111
+
112
+ - `request_features` 由元阁本地确定性提取;`request_hash` 只用于审计关联,不用于还原原文。
113
+ - `usage.enabled=false` 时 `usage.skills` 为空;只有用户显式启用使用记录后,才发送聚合计数。
114
+ - `installed_skills` 只含 slug / 版本 / frontmatter description / 状态 / 来源标签 / 时间戳 / 信任标注;不含安装路径。
115
+ - `playbooks` 只含公开静态 playbook 元数据。
116
+
57
117
  ## 3. 响应
58
118
 
59
119
  ```json
@@ -61,9 +121,20 @@
61
121
  "ok": true,
62
122
  "capability": "o1.route",
63
123
  "data": {
124
+ "policy": { "version": "o1-mvp-1" },
125
+ "confidence": "high",
126
+ "summary": "建议先跑防敷衍,再做代码质量评审,最后过发布守门。",
64
127
  "skills": [
65
- { "slug": "yotta-code-quality", "role": "可选:补充代码质量评审" },
66
- { "slug": "yotta-anti-shallow" }
128
+ { "slug": "yotta-anti-shallow", "role": "先做防敷衍检查", "score": 82, "reason": "意图匹配 + 静态场景" },
129
+ { "slug": "yotta-code-quality", "role": "补充代码质量评审", "score": 76, "reason": "历史使用 x2" },
130
+ { "slug": "yotta-publish-guard", "role": "最后做发布守门", "score": 70, "reason": "静态场景 + 组合出现" }
131
+ ],
132
+ "reasons": [
133
+ "意图匹配:检查 / 代码 / 质量 / 发布",
134
+ "静态场景:交付质量门"
135
+ ],
136
+ "alternatives": [
137
+ { "slug": "yotta-verify", "score": 54, "reason": "可补充装前安全扫描" }
67
138
  ]
68
139
  }
69
140
  }
@@ -71,6 +142,9 @@
71
142
 
72
143
  - `skills[].slug` 必须 ∈ `installed_skills` 白名单;未安装或未知的 slug 会被丢弃并记入 `dropped`,不会写入结果。
73
144
  - `role` 可选;缺省时增补条目使用「由扩展提供方补充」。
145
+ - `confidence` 只接受 `high / medium / low`;非法值回落静态置信度。
146
+ - `reasons` / `summary` 只作为展示数据,限长并去除控制字符;不作为指令执行。
147
+ - `alternatives` 只接受白名单内、且未进入主组合的技能。
74
148
  - 元阁先算静态结果:provider 给出的顺序用于重排;静态结果中未被提及的技能会**追加保留**,不会丢失。
75
149
  - 返回空列表 / 全是不合法 slug:视为未应用(`applied = false`),静态结果原样返回。
76
150
 
@@ -98,6 +172,8 @@ CLI 文本输出只在状态非 `not_installed` 时多一行「动态路由: ...
98
172
 
99
173
  - 每次实际调用写一行 `<YOTTA_PROVIDER_HOME>/provider-audit.jsonl`:`ts` / `capability` / `provider_id` / `status` / `duration_ms` / `bytes_out`。
100
174
  - 审计**不记录**需求原文或任何 payload 内容。
175
+ - `usage` 只含聚合计数与技能 slug;不记录需求原文、记忆正文、路径或身份信息。
176
+ - P3 仅面向用户显式配置的本地 provider;未来云端 provider 的「只发特征、不传原文」模式留 P5。
101
177
  - 元阁不替 provider 联网;provider 自身行为由它自己的包声明。
102
178
  - 删除 `provider.json` 即回到纯静态路由,无残留依赖。
103
179
  - provider 输出只当数据使用:白名单外的 slug、非法结构一律丢弃,不作为指令执行。
@@ -27,7 +27,7 @@ import subprocess
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
- VERSION = "0.22.2"
30
+ VERSION = "0.23.1"
31
31
  TOOL_NAME = "yotta-skills"
32
32
  CN_NAME = "元阁"
33
33
  MCP_PROTOCOL_MODERN = "2026-07-28"
@@ -209,9 +209,9 @@ def mcp_tools():
209
209
  ),
210
210
  _tool_spec(
211
211
  "route_request",
212
- "按需求摘要给出静态编排路由建议:候选组合、调用顺序、每个技能角色、置信度、依据、已装/缺失状态与安装命令。"
212
+ "按需求摘要给出编排路由建议:静态 playbook 基线 + 可选本地扩展提供方动态增补 / 重排;返回候选组合、调用顺序、每个技能角色、置信度、依据、已装 / 缺失状态与安装命令。"
213
213
  "并给出其他已装技能候选(仅按 frontmatter description 本地机械匹配、标注未扫描状态,不读取全文指令、不自动调用)。"
214
- "只建议安装,不自动安装;数据不出本机。",
214
+ "动态扩展仅在配置了本地 provider 且授权可用时生效;未配置 / 未授权 / 异常时回落静态路由。只建议安装,不自动安装;数据不出本机。",
215
215
  {"request": {"type": "string", "description": "用户需求摘要"}},
216
216
  ["request"],
217
217
  ),
@@ -285,7 +285,7 @@ def handle_message(msg):
285
285
  "result": _modern_ok({
286
286
  "supportedVersions": [MCP_PROTOCOL_MODERN],
287
287
  "capabilities": {"tools": {}},
288
- "instructions": "元阁 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):本机技能盘点、静态编排路由与 M1 记忆裁决只读建议 list_installed_skills/describe_skill/reindex/route_request/decide_memory;数据不出本机。",
288
+ "instructions": "元阁 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):本机技能盘点、编排路由(静态基线 + 可选本地动态扩展)与 M1 记忆裁决只读建议 list_installed_skills/describe_skill/reindex/route_request/decide_memory;数据不出本机。",
289
289
  }, (3600000, "public")),
290
290
  }
291
291
  if method == "tools/list":
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-skills",
4
4
  "name": "元阁",
5
5
  "package": "@yottameta/yotta-skills",
6
- "version": "0.22.2",
6
+ "version": "0.23.1",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true