@yottameta/yotta-skills 0.22.1 → 0.23.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 +15 -0
- package/README.md +5 -3
- package/README.zh-CN.md +5 -3
- package/SKILL.md +8 -6
- package/bin/yotta-skills.js +64 -78
- package/lib/route-dynamic.js +188 -0
- package/lib/route-features.js +194 -0
- package/package.json +1 -1
- package/references/install-flow.md +3 -1
- package/references/provider-protocol.md +78 -2
- package/scripts/yotta-skills-mcp.py +4 -4
- package/skill-manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## v0.23.0 (2026-10-01)
|
|
2
|
+
|
|
3
|
+
- O1 动态路由 MVP:`--route` / MCP `route_request` 在静态 playbook 之上,可选调用本地 provider(capability `o1.route`)做确定性组合排序;新增 `confidence` / `reasons` / `summary` / `alternatives` 输出。
|
|
4
|
+
- 开源侧新增 `lib/route-features.js`(请求特征 + 已装技能元数据 + 聚合使用信号 + 公开 playbook 元数据)与 `lib/route-dynamic.js`(白名单 / 枚举 / 限长校验);静态技能始终保留,provider 不能删除静态技能或改变 `missing_skills` 语义。
|
|
5
|
+
- `usage` 默认关闭;仅显式启用后向本地 provider 发送聚合计数(used / named / accepted / route_hits / distinct_pairs / last_signal_at),不发送需求原文、记忆正文、路径或身份信息;provider 审计仍只记元数据。
|
|
6
|
+
- 非元阁家族技能仍只作候选并标注 `scan_required`;不自动安装、不自动调用。
|
|
7
|
+
- 评分算法位于本地私有目录 `license/o1/`,不进任何发布件;未配置 / 未授权 / 超时 / 非法输出时回落静态路由,退出码保持 0。
|
|
8
|
+
- 元忆 0.20.0 不变;安装 / 更新 / hook / scanPolicy 契约零变更。
|
|
9
|
+
|
|
10
|
+
## v0.22.2 (2026-09-30)
|
|
11
|
+
|
|
12
|
+
- 修复 `doctor --dir` 指向单个技能包目录时的 UX:现在会按 `SKILL.md` / `skill-manifest.json` / 目录名自动识别技能身份,不再误报「目标目录下没有可检查的元阁家族技能」。
|
|
13
|
+
- 修复 doctor 退出码语义:没有可检查对象返回 `4`,检查失败仍返回 `1`,manifest / 身份校验失败仍返回 `6`,便于只读调用方区分「没目标」与「检查失败」。
|
|
14
|
+
- 新增单技能目录、显式 `--slug` 单技能目录、空目录退出码三组 CLI 回归。
|
|
15
|
+
|
|
1
16
|
## v0.22.1 (2026-09-30)
|
|
2
17
|
|
|
3
18
|
- 清单同步:元忆 pin 0.19.0 → 0.20.0,承接上下文分页 (`context.paging`) 与 `memory.hook` PREF 驱逐修复。
|
package/README.md
CHANGED
|
@@ -26,12 +26,12 @@ 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.
|
|
33
33
|
- **Update check / auto-update** — <code>update --check</code> compares installed skill versions against the npm registry and reports; <code>update --check --scheduled</code> is the weekly background entry with a local cache and jitter, so it stays offline until due; <code>update --auto</code> upgrades the installed YottaMeta family to the latest (non-family skills are never auto-updated).
|
|
34
|
-
- **Doctor / rollback** — <code>doctor</code> checks the installed skill directory, version, manifest, registry and custom doctor script without modifying it; <code>rollback</code> validates a snapshot and restores the latest install or update.
|
|
34
|
+
- **Doctor / rollback** — <code>doctor</code> checks the installed skill directory, version, manifest, registry and custom doctor script without modifying it; <code>--dir</code> may point to a skills container or directly to a single skill package. <code>rollback</code> validates a snapshot and restores the latest install or update.
|
|
35
35
|
- **Runtime hook adapter** — manifests declare six lifecycle requirements; <code>hook capabilities / evaluate / bind / unbind</code> probe host support, apply deterministic decisions, record evidence, and never overstate audit-only hosts as enforced.
|
|
36
36
|
- **Idempotent** — a skill already at the manifest version is skipped; re-running is safe.
|
|
37
37
|
- **Pre-install gate** — family installs read the package manifest, bootstrap yotta-verify (元信) when it is missing, then scan each package. <code>DO NOT INSTALL</code> blocks; caution/review continue with visible risk. Old versions are snapshotted before replacement.
|
|
@@ -113,13 +113,15 @@ Requirements: Node.js 18+, npm, and system <code>tar</code> (built into Windows
|
|
|
113
113
|
| `update --check` | Read-only update check: compare installed versions against the npm registry and report (exit 0 = up to date, 3 = updates available, 1 = could not check; run with `--registry <url>` to target a mirror); no changes |
|
|
114
114
|
| `update --check --scheduled` | Weekly background entry: no network until due, then one check and a local cache write; text mode stays quiet on failure, `--json` keeps diagnostics; always exits 0 |
|
|
115
115
|
| `update --auto` | Check and automatically update the installed YottaMeta family to the latest (runs the pre-install gate; never auto-updates non-family skills) |
|
|
116
|
+
| `doctor [--agent <name> \| --dir <path>] [--slug <slug>]` | Read-only self-check: SKILL / version / manifest / registry / custom doctor; `--dir` may point to a skills container or a single skill package; exit 0 = pass, 4 = no checkable target, 1 = check failed, 6 = manifest identity failure |
|
|
117
|
+
| `rollback [--agent <name> \| --dir <path>] [--slug <slug>]` | Validate and restore the latest snapshot; `--list` lists snapshots; `--json` returns machine-readable output |
|
|
116
118
|
| `hook capabilities --host <name>` | Show the six-event host capability matrix; unknown hosts stay <code>unsupported</code> |
|
|
117
119
|
| `hook evaluate --host <name> --event <event> --manifest <file> --context <json>` | Evaluate a manifest hook event, return allow / block / warn / unverified, and append structured evidence |
|
|
118
120
|
| `hook bind --host <name> --manifest <file>` / `hook unbind <id>` | Idempotently register or remove hook declarations |
|
|
119
121
|
| `--registry <url>` | npm registry to check against (default https://registry.npmjs.org/; `YOTTA_SKILLS_REGISTRY` overrides) |
|
|
120
122
|
| `--inventory` | Inventory installed skills: scan skill directories and update the local registry (self-contained); `--json` for JSON, `--project` adds project-level dirs |
|
|
121
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 |
|
|
122
|
-
| `--route <task-summary>` |
|
|
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 |
|
|
123
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 |
|
|
124
126
|
| `usage mark --skill <slug> --signal used\|named\|accepted` | Record one explicit usage signal; no prior enable required |
|
|
125
127
|
| `usage reset --yes` | Clear the local usage record |
|
package/README.zh-CN.md
CHANGED
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
元阁也是全家的**编排策划层**:接到需求先查「编排策划」组合表——该组合哪几个技能、按什么顺序、各自强在哪——然后给出安装与调用**建议**,由用户确认后执行。决策表随包提供(<code>references/orchestration.md</code>),<code>SKILL.md</code> 中有摘要。
|
|
30
30
|
|
|
31
31
|
- **看清单**--全家技能一览:slug / 中文名 / 包名 / 版本 / 一句话说明。
|
|
32
|
-
- **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。可选本地扩展提供方可在已装白名单内增补 /
|
|
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
|
- **更新**——增量更新:补齐缺失技能、升级版本不一致的技能。
|
|
36
36
|
- **更新检查 / 自动更新**——<code>update --check</code> 只读比对已装技能版本与 npm 注册表并报告;<code>update --check --scheduled</code> 是后台周检入口,用本地缓存和随机抖动做到未到期不联网;<code>update --auto</code> 检查后自动把已装元阁家族升到最新(非元阁家族技能绝不自动更新)。
|
|
37
|
-
- **自检 / 回滚**——<code>doctor</code> 只读检查技能目录、版本、manifest、注册表和自定义 doctor
|
|
37
|
+
- **自检 / 回滚**——<code>doctor</code> 只读检查技能目录、版本、manifest、注册表和自定义 doctor,`--dir` 可直接指向单个技能包目录;<code>rollback</code> 校验快照后恢复最近一次安装或更新,恢复失败不会覆盖当前目录。
|
|
38
38
|
- **运行时 hook 适配层**——技能 manifest 只声明六个生命周期要求;<code>hook capabilities / evaluate / bind / unbind</code> 负责宿主能力探测、确定性决策、结构化证据和降级标注,不把 audit 能力夸大为强制。
|
|
39
39
|
- **幂等**——已在清单版本的技能跳过;重复运行安全。
|
|
40
40
|
- **装前门禁**——家族安装先读取包内 manifest,元信(yotta-verify)缺失时自动自举,再逐个扫描。`DO NOT INSTALL` 阻断;`CAUTION` / `REVIEW` 继续但显示风险。旧版本会先做快照再替换。
|
|
@@ -116,13 +116,15 @@ npx -y @yottameta/yotta-skills --reindex
|
|
|
116
116
|
| `update --check` | 只读更新检查:比对已装版本与 npm 注册表并报告(退出码 0=最新 / 3=有更新 / 1=无法检查;可用 `--registry <url>` 指定镜像);不改动 |
|
|
117
117
|
| `update --check --scheduled` | 后台周检入口:未到期不联网;到期只检查一次并写本地缓存;文本失败静默,`--json` 保留诊断;始终退出 0 |
|
|
118
118
|
| `update --auto` | 检查后自动更新已装元阁家族到最新(含装前门禁;非元阁家族技能绝不自动更新) |
|
|
119
|
+
| `doctor [--agent <name> \| --dir <path>] [--slug <slug>]` | 只读自检:SKILL / 版本 / manifest / 注册表 / 自定义 doctor;`--dir` 可指向技能集合目录或单个技能包目录;退出码 0=通过 / 4=没有可检查对象 / 1=检查失败 / 6=manifest 身份失败 |
|
|
120
|
+
| `rollback [--agent <name> \| --dir <path>] [--slug <slug>]` | 校验并恢复最近一次快照;`--list` 只列快照;`--json` 输出机器可读结果 |
|
|
119
121
|
| `hook capabilities --host <name>` | 查看六个统一事件的宿主能力矩阵;未知宿主默认 `unsupported` |
|
|
120
122
|
| `hook evaluate --host <name> --event <event> --manifest <file> --context <json>` | 评估 manifest hook 事件,返回 allow / block / warn / unverified,并写结构化证据 |
|
|
121
123
|
| `hook bind --host <name> --manifest <file>` / `hook unbind <id>` | 幂等注册或反注册 hook 声明 |
|
|
122
124
|
| `--registry <url>` | 检查的 npm registry 地址(默认 https://registry.npmjs.org/;`YOTTA_SKILLS_REGISTRY` 覆盖) |
|
|
123
125
|
| `--inventory` | 盘点已装技能:扫描技能目录并更新本地注册表(自包含);`--json` 输出 JSON、`--project` 附扫项目级目录 |
|
|
124
126
|
| `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后自动重扫,也可随时手动运行;`--rescan` 同义);`--json` 输出 JSON |
|
|
125
|
-
| `--route <需求摘要>` |
|
|
127
|
+
| `--route <需求摘要>` | 编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地扩展提供方在已装白名单内增补 / 重排并返回 `confidence` / `reasons` / `summary` / `alternatives`(未配置时行为不变,协议见 `references/provider-protocol.md`);`--json` 输出 JSON 与 `dynamic` 状态块、`--project` 附扫项目级目录 |
|
|
126
128
|
| `usage status` / `usage enable` / `usage disable` | 查看 / 开启 / 关闭本地结构化使用记录;默认关闭,只记录 slug、时间、信号类型、playbook / confidence 与组合对 |
|
|
127
129
|
| `usage mark --skill <slug> --signal used\|named\|accepted` | 记录一次显式使用信号;不要求先 enable |
|
|
128
130
|
| `usage reset --yes` | 清空本地使用记录 |
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-skills
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.23.0
|
|
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
|
-
-
|
|
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` 锁版本。
|
|
@@ -166,14 +166,14 @@ openclaw skills update @yottameta/yotta-skills
|
|
|
166
166
|
| `update --check [--agent <name> \| --dir <path>]` | 只读检查更新:联网对 npm 最新,不改动;退出码 0=全部最新 / 3=有更新 / 1=查失败 |
|
|
167
167
|
| `update --check --scheduled [--agent <name> \| --dir <path>]` | 后台周检入口:未到期不联网;到期只检查一次并写本地缓存;文本失败静默,`--json` 保留诊断;始终退出 0 |
|
|
168
168
|
| `update --auto [--agent <name> \| --dir <path>]` | 检查到家族更新后自动更新(仅 yotta-* 自家家族,含装前安全扫描) |
|
|
169
|
-
| `doctor [--agent <name> \| --dir <path>] [--slug <slug>]` | 只读自检:SKILL / 版本 / manifest / 注册表 / 自定义 doctor;`--
|
|
169
|
+
| `doctor [--agent <name> \| --dir <path>] [--slug <slug>]` | 只读自检:SKILL / 版本 / manifest / 注册表 / 自定义 doctor;`--dir` 可指向技能集合目录或单个技能包目录;退出码 0=通过 / 4=没有可检查对象 / 1=检查失败 / 6=manifest 身份失败 |
|
|
170
170
|
| `rollback [--agent <name> \| --dir <path>] [--slug <slug>]` | 校验并恢复最近一次快照;`--list` 只列快照;`--json` 输出机器可读结果 |
|
|
171
171
|
| `hook capabilities --host <name>` | 查看宿主六个统一事件的能力等级;未知宿主默认 `unsupported` |
|
|
172
172
|
| `hook evaluate --host <name> --event <event> --manifest <file> --context <json>` | 评估 manifest hook 声明,输出 allow / block / warn / unverified,并写结构化证据 |
|
|
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 <需求摘要>` |
|
|
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 |
|
|
@@ -262,7 +262,9 @@ npx -y @yottameta/yotta-skills update --auto --agent codex
|
|
|
262
262
|
|
|
263
263
|
`doctor` 是只读自检,不会修改技能目录或注册表。它检查目标技能目录、`SKILL.md`、
|
|
264
264
|
frontmatter 版本、manifest 身份、本地注册表记录,以及包内声明的自定义 doctor 脚本。
|
|
265
|
-
`--
|
|
265
|
+
`--dir` 既可指向包含多个 `yotta-*` 子目录的技能集合目录,也可直接指向单个技能包目录;
|
|
266
|
+
后者会按 `SKILL.md` / `skill-manifest.json` 自动识别技能身份。`--json` 返回稳定字段,
|
|
267
|
+
适合脚本和智能体消费。退出码:`0` 通过,`4` 没有可检查对象,`1` 检查失败,`6` manifest 身份失败。
|
|
266
268
|
|
|
267
269
|
```bash
|
|
268
270
|
npx -y @yottameta/yotta-skills doctor --agent codex --slug yotta-memory
|
|
@@ -338,7 +340,7 @@ npx -y @yottameta/yotta-skills usage reset --yes
|
|
|
338
340
|
|
|
339
341
|
本技能自带一个 MCP server:`yotta-skills`(`scripts/yotta-skills-mcp.py`,零依赖、数据不出本机),
|
|
340
342
|
基于 MCP 最新协议 2026-07-28(无状态时代;向后兼容 2025-11-25 及更早握手客户端)。
|
|
341
|
-
提供 `list_installed_skills`(盘点)/ `describe_skill`(单技能详情)/ `reindex`(强制重扫)/ `route_request
|
|
343
|
+
提供 `list_installed_skills`(盘点)/ `describe_skill`(单技能详情)/ `reindex`(强制重扫)/ `route_request`(编排路由,静态基线 + 可选本地动态扩展)/ `decide_memory`(M1 记忆裁决只读建议)五个工具。
|
|
342
344
|
|
|
343
345
|
**按需加载,不走常驻**:本技能与 MCP 均为按需触发。默认以 CLI 为主
|
|
344
346
|
(`npx -y @yottameta/yotta-skills --inventory`);需要让 AI 通过工具直接调用时,再按下面配置启用。
|
package/bin/yotta-skills.js
CHANGED
|
@@ -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.
|
|
47
|
+
let VERSION = '0.23.0';
|
|
46
48
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
47
49
|
|
|
48
50
|
function loadManifest() {
|
|
@@ -820,27 +822,62 @@ function runCustomDoctor(skillDir, dest) {
|
|
|
820
822
|
});
|
|
821
823
|
}
|
|
822
824
|
|
|
825
|
+
function withDoctorTarget(skill, target) {
|
|
826
|
+
return Object.assign({}, skill, { target });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function selfDoctorTarget(dest) {
|
|
830
|
+
const meta = readInstalledMeta(dest);
|
|
831
|
+
const manifest = readInstalledManifest(dest);
|
|
832
|
+
const candidates = [
|
|
833
|
+
manifest && manifest.slug,
|
|
834
|
+
meta.name,
|
|
835
|
+
path.basename(dest),
|
|
836
|
+
]
|
|
837
|
+
.filter(Boolean)
|
|
838
|
+
.map((value) => String(value).trim().toLowerCase());
|
|
839
|
+
|
|
840
|
+
for (const slug of candidates) {
|
|
841
|
+
const skill = familySkillFor(slug);
|
|
842
|
+
if (skill) return withDoctorTarget(skill, dest);
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
|
|
823
847
|
function doctorTargets(opts, dest) {
|
|
848
|
+
const self = selfDoctorTarget(dest);
|
|
824
849
|
if (opts.slug) {
|
|
850
|
+
if (self && self.slug === opts.slug) return [self];
|
|
825
851
|
const skill = familySkillFor(opts.slug);
|
|
826
|
-
return skill ? [skill] : [];
|
|
852
|
+
return skill ? [withDoctorTarget(skill, path.join(dest, skill.slug))] : [];
|
|
827
853
|
}
|
|
828
854
|
if (opts.skills.length) {
|
|
829
|
-
|
|
855
|
+
const targets = [];
|
|
856
|
+
for (const slug of opts.skills) {
|
|
857
|
+
if (self && self.slug === slug) {
|
|
858
|
+
targets.push(self);
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const skill = familySkillFor(slug);
|
|
862
|
+
if (skill) targets.push(withDoctorTarget(skill, path.join(dest, skill.slug)));
|
|
863
|
+
}
|
|
864
|
+
return targets;
|
|
830
865
|
}
|
|
831
866
|
const found = [];
|
|
832
867
|
let entries;
|
|
833
|
-
try { entries = fs.readdirSync(dest, { withFileTypes: true }); } catch (_) { return found; }
|
|
868
|
+
try { entries = fs.readdirSync(dest, { withFileTypes: true }); } catch (_) { return self ? [self] : found; }
|
|
834
869
|
for (const entry of entries) {
|
|
835
870
|
if (!entry.isDirectory() || !/^yotta-/.test(entry.name)) continue;
|
|
836
871
|
const skill = familySkillFor(entry.name);
|
|
837
|
-
if (skill) found.push(skill);
|
|
872
|
+
if (skill) found.push(withDoctorTarget(skill, path.join(dest, skill.slug)));
|
|
838
873
|
}
|
|
874
|
+
if (found.length === 0 && self) return [self];
|
|
839
875
|
return found;
|
|
840
876
|
}
|
|
841
877
|
|
|
842
878
|
function doctorExitCode(payload) {
|
|
843
879
|
if (payload.ok) return 0;
|
|
880
|
+
if (payload.checked === 0) return 4;
|
|
844
881
|
const manifestFailure = payload.results.some((result) =>
|
|
845
882
|
(result.checks || []).some((check) =>
|
|
846
883
|
(check.id === 'manifest' || check.id === 'manifest_trust') && !check.ok));
|
|
@@ -861,7 +898,7 @@ function runDoctor(opts, dest) {
|
|
|
861
898
|
}
|
|
862
899
|
|
|
863
900
|
for (const skill of targets) {
|
|
864
|
-
const target = path.join(dest, skill.slug);
|
|
901
|
+
const target = skill.target || path.join(dest, skill.slug);
|
|
865
902
|
const result = healthLib.checkInstalledSkill({
|
|
866
903
|
slug: skill.slug,
|
|
867
904
|
target,
|
|
@@ -1215,95 +1252,40 @@ function runReindex(opts) {
|
|
|
1215
1252
|
out('注册表: ' + scan.registryPath());
|
|
1216
1253
|
}
|
|
1217
1254
|
|
|
1218
|
-
function dynamicBlock(status, providerId, note) {
|
|
1219
|
-
return {
|
|
1220
|
-
status: status || 'not_installed',
|
|
1221
|
-
provider_id: providerId || '',
|
|
1222
|
-
applied: false,
|
|
1223
|
-
added: [],
|
|
1224
|
-
dropped: [],
|
|
1225
|
-
note: note || '',
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
1255
|
/**
|
|
1230
1256
|
* o1.route 动态扩展口:静态结果先算必算;provider 只允许在已装注册表白名单内增补 / 重排,
|
|
1231
1257
|
* 任何失败都回静态结果(fail-open)。协议见 references/provider-protocol.md。
|
|
1232
1258
|
*/
|
|
1233
1259
|
function applyDynamicRoute(result, registry, opts) {
|
|
1234
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
|
+
});
|
|
1235
1270
|
let run;
|
|
1236
1271
|
try {
|
|
1237
|
-
run = provider.runCapability('o1.route',
|
|
1238
|
-
request: String(opts.route || ''),
|
|
1239
|
-
static_result: {
|
|
1240
|
-
playbook: result.playbook.id,
|
|
1241
|
-
confidence: result.confidence,
|
|
1242
|
-
skills: result.skills.map((skill) => skill.slug),
|
|
1243
|
-
},
|
|
1244
|
-
installed_skills: Object.values(registry.skills || {})
|
|
1245
|
-
.filter((record) => record && record.status !== 'gone')
|
|
1246
|
-
.map((record) => ({ slug: record.slug, version: record.version || '', sources: record.sources || [] })),
|
|
1247
|
-
});
|
|
1272
|
+
run = provider.runCapability('o1.route', payload);
|
|
1248
1273
|
} catch (e) {
|
|
1249
|
-
return dynamicBlock('error', '', '动态路由装载失败:' + e.message);
|
|
1274
|
+
return routeDynamicLib.dynamicBlock('error', '', '动态路由装载失败:' + e.message);
|
|
1250
1275
|
}
|
|
1251
|
-
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 || '');
|
|
1252
1277
|
if (run.status !== 'active' || !run.data || typeof run.data !== 'object') return block;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
.map((record) => record.slug));
|
|
1257
|
-
const requested = Array.isArray(run.data.skills) ? run.data.skills : [];
|
|
1258
|
-
const seen = new Set();
|
|
1259
|
-
const ordered = [];
|
|
1260
|
-
for (const item of requested) {
|
|
1261
|
-
const slug = item && typeof item === 'object' ? String(item.slug || '') : '';
|
|
1262
|
-
if (!slug) continue;
|
|
1263
|
-
if (seen.has(slug) || !installed.has(slug)) {
|
|
1264
|
-
block.dropped.push(slug);
|
|
1265
|
-
continue;
|
|
1266
|
-
}
|
|
1267
|
-
seen.add(slug);
|
|
1268
|
-
ordered.push({ slug, role: typeof item.role === 'string' ? item.role : '' });
|
|
1269
|
-
}
|
|
1270
|
-
if (!ordered.length) return block;
|
|
1271
|
-
|
|
1272
|
-
const staticBySlug = new Map(result.skills.map((skill) => [skill.slug, skill]));
|
|
1273
|
-
const next = [];
|
|
1274
|
-
for (const item of ordered) {
|
|
1275
|
-
const existing = staticBySlug.get(item.slug);
|
|
1276
|
-
if (existing) {
|
|
1277
|
-
next.push(existing);
|
|
1278
|
-
continue;
|
|
1279
|
-
}
|
|
1280
|
-
const record = registry.skills[item.slug] || {};
|
|
1281
|
-
next.push({
|
|
1282
|
-
slug: item.slug,
|
|
1283
|
-
order: 0,
|
|
1284
|
-
role: item.role || '由扩展提供方补充',
|
|
1285
|
-
installed: true,
|
|
1286
|
-
version: record.version || '',
|
|
1287
|
-
sources: record.sources || [],
|
|
1288
|
-
variants: Array.isArray(record.variants) ? record.variants : [],
|
|
1289
|
-
conflicts: Array.isArray(record.conflicts) ? record.conflicts : [],
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
for (const skill of result.skills) {
|
|
1293
|
-
if (!next.some((item) => item.slug === skill.slug)) next.push(skill);
|
|
1294
|
-
}
|
|
1295
|
-
next.forEach((skill, index) => { skill.order = index + 1; });
|
|
1296
|
-
block.added = next.filter((skill) => !staticBySlug.has(skill.slug)).map((skill) => skill.slug);
|
|
1297
|
-
block.applied = true;
|
|
1298
|
-
result.skills = next;
|
|
1299
|
-
return block;
|
|
1278
|
+
return routeDynamicLib.applyDynamicData(result, run.data, registry, {
|
|
1279
|
+
providerId: run.provider_id,
|
|
1280
|
+
});
|
|
1300
1281
|
}
|
|
1301
1282
|
|
|
1302
1283
|
function dynamicRouteText(block) {
|
|
1303
1284
|
if (!block) return '';
|
|
1304
1285
|
if (block.status === 'active') {
|
|
1305
1286
|
const who = block.provider_id ? '提供方 ' + block.provider_id : '提供方';
|
|
1306
|
-
|
|
1287
|
+
const confidence = block.confidence ? ';置信度 ' + block.confidence : '';
|
|
1288
|
+
return '已应用(' + who + confidence + ';新增 ' + block.added.length + ' / 丢弃 ' + block.dropped.length + ')';
|
|
1307
1289
|
}
|
|
1308
1290
|
if (block.status === 'license_required') return '需授权(该能力需要授权后使用;静态路由不受影响)';
|
|
1309
1291
|
if (block.status === 'timeout') return '未生效(提供方超时,已回落静态路由)';
|
|
@@ -1367,6 +1349,10 @@ function runRoute(opts) {
|
|
|
1367
1349
|
if (result.dynamic.status !== 'not_installed') {
|
|
1368
1350
|
out('');
|
|
1369
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
|
+
}
|
|
1370
1356
|
}
|
|
1371
1357
|
out('');
|
|
1372
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.
|
|
3
|
+
"version": "0.23.0",
|
|
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": [
|
|
@@ -145,6 +145,8 @@ yotta-skills doctor --dir <skills-dir> --json
|
|
|
145
145
|
|
|
146
146
|
doctor 只读检查目录、`SKILL.md`、版本、manifest 身份、注册表记录和自定义 doctor,
|
|
147
147
|
不会修改目标目录或注册表。注册表版本不一致属于 warning,可用 `--reindex` 修复。
|
|
148
|
+
`--dir` 也可以直接指向单个技能包目录,此时会按 `SKILL.md` / `skill-manifest.json` / 目录名自动识别技能身份。
|
|
149
|
+
退出码:`0` 通过,`4` 没有可检查对象,`1` 检查失败,`6` manifest / 身份校验失败。
|
|
148
150
|
|
|
149
151
|
## 安装证据
|
|
150
152
|
|
|
@@ -164,7 +166,7 @@ doctor 只读检查目录、`SKILL.md`、版本、manifest 身份、注册表记
|
|
|
164
166
|
| 0 | 成功(含全部跳过) |
|
|
165
167
|
| 1 | 安装 / 更新存在失败项 |
|
|
166
168
|
| 2 | 用法错误(未知参数 / 未知技能 / 未收录智能体) |
|
|
167
|
-
| 4 |
|
|
169
|
+
| 4 | 未指定目标且当前目录未检测到项目级技能目录,或 doctor 没有可检查的元阁家族技能 |
|
|
168
170
|
| 5 | 装前 gate 阻断或元信不可用 |
|
|
169
171
|
| 6 | manifest / 身份校验失败 |
|
|
170
172
|
|
|
@@ -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-
|
|
66
|
-
{ "slug": "yotta-
|
|
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.
|
|
30
|
+
VERSION = "0.23.0"
|
|
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
|
|
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":
|