@yottameta/yotta-skills-plugin 0.19.28 → 0.20.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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-skills/SKILL.md +2 -1
- package/skills/yotta-skills/bin/yotta-skills.js +103 -1
- package/skills/yotta-skills/lib/provider.js +248 -0
- package/skills/yotta-skills/references/provider-protocol.md +101 -0
- package/skills/yotta-skills/references/skill-list.md +1 -1
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
- package/skills/yotta-skills/skills.json +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.20.0",
|
|
16
16
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元阁 yotta-skills",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.20.0",
|
|
16
16
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元阁 yotta-skills",
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "yotta-skills",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.0",
|
|
5
5
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "YottaMeta",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-skills
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.20.0
|
|
4
4
|
description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 运行时 hook 适配。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 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,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / 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):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
@@ -43,6 +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
47
|
- **策划层(怎么用)**:接到需求,先按「编排策划」定位命中哪个组合——哪些元技能搭配起来最强、适合什么场景、AI 该给出什么安装与调用建议(**安装与调用均由用户确认后执行**)。
|
|
47
48
|
- **安装层(怎么装)**:一条 `npx -y @yottameta/yotta-skills` 把组合/全家装进指定智能体或目录——`--list` 看清单、`install` 装、`update` 增量更新、`doctor` 只读自检、`rollback` 恢复快照、`--dry-run` 预览、`--pin` 锁版本。
|
|
48
49
|
|
|
@@ -38,7 +38,7 @@ const hookAdapterLib = require('../lib/hook-adapter');
|
|
|
38
38
|
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
39
39
|
|
|
40
40
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
41
|
-
let VERSION = '0.
|
|
41
|
+
let VERSION = '0.20.0';
|
|
42
42
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
43
43
|
|
|
44
44
|
function loadManifest() {
|
|
@@ -1202,12 +1202,110 @@ function runReindex(opts) {
|
|
|
1202
1202
|
out('注册表: ' + scan.registryPath());
|
|
1203
1203
|
}
|
|
1204
1204
|
|
|
1205
|
+
function dynamicBlock(status, providerId, note) {
|
|
1206
|
+
return {
|
|
1207
|
+
status: status || 'not_installed',
|
|
1208
|
+
provider_id: providerId || '',
|
|
1209
|
+
applied: false,
|
|
1210
|
+
added: [],
|
|
1211
|
+
dropped: [],
|
|
1212
|
+
note: note || '',
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* o1.route 动态扩展口:静态结果先算必算;provider 只允许在已装注册表白名单内增补 / 重排,
|
|
1218
|
+
* 任何失败都回静态结果(fail-open)。协议见 references/provider-protocol.md。
|
|
1219
|
+
*/
|
|
1220
|
+
function applyDynamicRoute(result, registry, opts) {
|
|
1221
|
+
const provider = require('../lib/provider');
|
|
1222
|
+
let run;
|
|
1223
|
+
try {
|
|
1224
|
+
run = provider.runCapability('o1.route', {
|
|
1225
|
+
request: String(opts.route || ''),
|
|
1226
|
+
static_result: {
|
|
1227
|
+
playbook: result.playbook.id,
|
|
1228
|
+
confidence: result.confidence,
|
|
1229
|
+
skills: result.skills.map((skill) => skill.slug),
|
|
1230
|
+
},
|
|
1231
|
+
installed_skills: Object.values(registry.skills || {})
|
|
1232
|
+
.filter((record) => record && record.status !== 'gone')
|
|
1233
|
+
.map((record) => ({ slug: record.slug, version: record.version || '', sources: record.sources || [] })),
|
|
1234
|
+
});
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
return dynamicBlock('error', '', '动态路由装载失败:' + e.message);
|
|
1237
|
+
}
|
|
1238
|
+
const block = dynamicBlock(run.status, run.provider_id, run.note || run.message || '');
|
|
1239
|
+
if (run.status !== 'active' || !run.data || typeof run.data !== 'object') return block;
|
|
1240
|
+
|
|
1241
|
+
const installed = new Set(Object.values(registry.skills || {})
|
|
1242
|
+
.filter((record) => record && record.status !== 'gone')
|
|
1243
|
+
.map((record) => record.slug));
|
|
1244
|
+
const requested = Array.isArray(run.data.skills) ? run.data.skills : [];
|
|
1245
|
+
const seen = new Set();
|
|
1246
|
+
const ordered = [];
|
|
1247
|
+
for (const item of requested) {
|
|
1248
|
+
const slug = item && typeof item === 'object' ? String(item.slug || '') : '';
|
|
1249
|
+
if (!slug) continue;
|
|
1250
|
+
if (seen.has(slug) || !installed.has(slug)) {
|
|
1251
|
+
block.dropped.push(slug);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
seen.add(slug);
|
|
1255
|
+
ordered.push({ slug, role: typeof item.role === 'string' ? item.role : '' });
|
|
1256
|
+
}
|
|
1257
|
+
if (!ordered.length) return block;
|
|
1258
|
+
|
|
1259
|
+
const staticBySlug = new Map(result.skills.map((skill) => [skill.slug, skill]));
|
|
1260
|
+
const next = [];
|
|
1261
|
+
for (const item of ordered) {
|
|
1262
|
+
const existing = staticBySlug.get(item.slug);
|
|
1263
|
+
if (existing) {
|
|
1264
|
+
next.push(existing);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
const record = registry.skills[item.slug] || {};
|
|
1268
|
+
next.push({
|
|
1269
|
+
slug: item.slug,
|
|
1270
|
+
order: 0,
|
|
1271
|
+
role: item.role || '由扩展提供方补充',
|
|
1272
|
+
installed: true,
|
|
1273
|
+
version: record.version || '',
|
|
1274
|
+
sources: record.sources || [],
|
|
1275
|
+
variants: Array.isArray(record.variants) ? record.variants : [],
|
|
1276
|
+
conflicts: Array.isArray(record.conflicts) ? record.conflicts : [],
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
for (const skill of result.skills) {
|
|
1280
|
+
if (!next.some((item) => item.slug === skill.slug)) next.push(skill);
|
|
1281
|
+
}
|
|
1282
|
+
next.forEach((skill, index) => { skill.order = index + 1; });
|
|
1283
|
+
block.added = next.filter((skill) => !staticBySlug.has(skill.slug)).map((skill) => skill.slug);
|
|
1284
|
+
block.applied = true;
|
|
1285
|
+
result.skills = next;
|
|
1286
|
+
return block;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function dynamicRouteText(block) {
|
|
1290
|
+
if (!block) return '';
|
|
1291
|
+
if (block.status === 'active') {
|
|
1292
|
+
const who = block.provider_id ? '提供方 ' + block.provider_id : '提供方';
|
|
1293
|
+
return '已应用(' + who + ';新增 ' + block.added.length + ' / 丢弃 ' + block.dropped.length + ')';
|
|
1294
|
+
}
|
|
1295
|
+
if (block.status === 'license_required') return '需授权(该能力需要授权后使用;静态路由不受影响)';
|
|
1296
|
+
if (block.status === 'timeout') return '未生效(提供方超时,已回落静态路由)';
|
|
1297
|
+
if (block.status === 'invalid_output') return '未生效(提供方输出无效,已回落静态路由)';
|
|
1298
|
+
if (block.status === 'error') return '未生效(提供方异常,已回落静态路由)';
|
|
1299
|
+
return block.status;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1205
1302
|
/** --route:静态编排路由,输出组合、顺序、角色、缺失技能建议与其他已装技能候选。 */
|
|
1206
1303
|
function runRoute(opts) {
|
|
1207
1304
|
const { routeRequest, defaultYottaSlugs } = require('../lib/route');
|
|
1208
1305
|
const { registry } = reindexRegistry(opts);
|
|
1209
1306
|
const yottaSlugs = new Set([...defaultYottaSlugs(), ...MANIFEST.map((s) => s.slug)]);
|
|
1210
1307
|
const result = routeRequest(opts.route, { registry, yottaSlugs });
|
|
1308
|
+
result.dynamic = applyDynamicRoute(result, registry, opts);
|
|
1211
1309
|
if (opts.json) {
|
|
1212
1310
|
out(JSON.stringify(result, null, 2));
|
|
1213
1311
|
return;
|
|
@@ -1244,6 +1342,10 @@ function runRoute(opts) {
|
|
|
1244
1342
|
}
|
|
1245
1343
|
out('安全提示: 其他已装技能只读 frontmatter description 做机械匹配,不读取全文指令、不自动调用;使用/安装前请先执行装前安全扫描,决定权在用户。');
|
|
1246
1344
|
}
|
|
1345
|
+
if (result.dynamic.status !== 'not_installed') {
|
|
1346
|
+
out('');
|
|
1347
|
+
out('动态路由: ' + dynamicRouteText(result.dynamic));
|
|
1348
|
+
}
|
|
1247
1349
|
out('');
|
|
1248
1350
|
out('应用模式: 显式调用(可经用户确认后切换为按场景自动调用)');
|
|
1249
1351
|
out('说明: ' + result.disclaimer);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 扩展提供方(provider)装载器 v1:外部子进程 + stdin/stdout JSON。
|
|
4
|
+
// 设计:docs/路线B-P1-接口预留设计-2026-09-28.md(根仓库);对外协议:references/provider-protocol.md。
|
|
5
|
+
// 零依赖;未配置 / 任何失败都 fail-open 回开源基线,不阻断调用方。
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const SCHEMA = 1;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 600;
|
|
15
|
+
const MAX_TIMEOUT_MS = 5000;
|
|
16
|
+
const MIN_TIMEOUT_MS = 50;
|
|
17
|
+
const MAX_STDOUT_BYTES = 256 * 1024;
|
|
18
|
+
const KNOWN_CAPABILITIES = ['o1.route', 'm1.adjudicate', 'memory.hook', 'context.paging'];
|
|
19
|
+
|
|
20
|
+
function providerHome() {
|
|
21
|
+
const fromEnv = process.env.YOTTA_PROVIDER_HOME;
|
|
22
|
+
if (fromEnv && String(fromEnv).trim()) return path.resolve(String(fromEnv).trim());
|
|
23
|
+
return path.join(os.homedir(), '.yottameta');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function configPath() {
|
|
27
|
+
return path.join(providerHome(), 'provider.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateProvider(item) {
|
|
31
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return 'provider 条目必须是对象';
|
|
32
|
+
if (!item.id || typeof item.id !== 'string') return 'provider 缺 id';
|
|
33
|
+
if (!Array.isArray(item.capabilities) || !item.capabilities.length) return 'provider ' + item.id + ' 缺 capabilities';
|
|
34
|
+
for (const capability of item.capabilities) {
|
|
35
|
+
if (KNOWN_CAPABILITIES.indexOf(capability) === -1) return 'provider ' + item.id + ' 含未知 capability:' + capability;
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(item.command) || !item.command.length) return 'provider ' + item.id + ' 的 command 必须是数组';
|
|
38
|
+
for (const part of item.command) {
|
|
39
|
+
if (typeof part !== 'string' || !part) return 'provider ' + item.id + ' 的 command 含非法参数';
|
|
40
|
+
if (part.indexOf('\0') !== -1) return 'provider ' + item.id + ' 的 command 含非法字符';
|
|
41
|
+
}
|
|
42
|
+
if (item.timeout_ms !== undefined && (!Number.isFinite(item.timeout_ms) || item.timeout_ms <= 0)) {
|
|
43
|
+
return 'provider ' + item.id + ' 的 timeout_ms 非法';
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeProvider(item) {
|
|
49
|
+
return {
|
|
50
|
+
id: item.id,
|
|
51
|
+
version: typeof item.version === 'string' ? item.version : '',
|
|
52
|
+
capabilities: item.capabilities.slice(),
|
|
53
|
+
command: item.command.slice(),
|
|
54
|
+
timeout_ms: Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.round(item.timeout_ms || DEFAULT_TIMEOUT_MS))),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readConfig() {
|
|
59
|
+
const file = configPath();
|
|
60
|
+
if (!fs.existsSync(file)) return { status: 'not_installed', providers: [], error: '' };
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { status: 'error', providers: [], error: 'provider.json 解析失败:' + e.message };
|
|
66
|
+
}
|
|
67
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
68
|
+
return { status: 'error', providers: [], error: 'provider.json 必须是对象' };
|
|
69
|
+
}
|
|
70
|
+
if (raw.schema !== SCHEMA) {
|
|
71
|
+
return { status: 'error', providers: [], error: 'provider.json schema 不支持:' + String(raw.schema) };
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(raw.providers)) {
|
|
74
|
+
return { status: 'error', providers: [], error: 'provider.json providers 必须是数组' };
|
|
75
|
+
}
|
|
76
|
+
const providers = [];
|
|
77
|
+
for (const item of raw.providers) {
|
|
78
|
+
const invalid = validateProvider(item);
|
|
79
|
+
if (invalid) return { status: 'error', providers: [], error: invalid };
|
|
80
|
+
providers.push(normalizeProvider(item));
|
|
81
|
+
}
|
|
82
|
+
return { status: 'ok', providers, error: '' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pickProvider(config, capability) {
|
|
86
|
+
if (!config || config.status !== 'ok') return null;
|
|
87
|
+
for (const item of config.providers) {
|
|
88
|
+
if (item.capabilities.indexOf(capability) !== -1) return item;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function minimalEnv() {
|
|
94
|
+
const keep = [
|
|
95
|
+
'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'COMSPEC', 'ComSpec',
|
|
96
|
+
'TEMP', 'TMP', 'HOME', 'USERPROFILE', 'LANG', 'LC_ALL',
|
|
97
|
+
];
|
|
98
|
+
const env = {};
|
|
99
|
+
for (const key of keep) {
|
|
100
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
101
|
+
}
|
|
102
|
+
env.YOTTA_PROVIDER_HOME = providerHome();
|
|
103
|
+
return env;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requestId() {
|
|
107
|
+
try {
|
|
108
|
+
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// 回退到随机 hex
|
|
111
|
+
}
|
|
112
|
+
return crypto.randomBytes(16).toString('hex');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function invokeProvider(provider, capability, payload) {
|
|
116
|
+
const started = Date.now();
|
|
117
|
+
const request = {
|
|
118
|
+
schema: SCHEMA,
|
|
119
|
+
capability,
|
|
120
|
+
request_id: requestId(),
|
|
121
|
+
payload: payload || {},
|
|
122
|
+
};
|
|
123
|
+
let proc;
|
|
124
|
+
try {
|
|
125
|
+
proc = spawnSync(provider.command[0], provider.command.slice(1), {
|
|
126
|
+
input: JSON.stringify(request),
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
timeout: provider.timeout_ms,
|
|
129
|
+
maxBuffer: MAX_STDOUT_BYTES,
|
|
130
|
+
shell: false,
|
|
131
|
+
cwd: providerHome(),
|
|
132
|
+
env: minimalEnv(),
|
|
133
|
+
windowsHide: true,
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return {
|
|
137
|
+
status: 'error',
|
|
138
|
+
provider_id: provider.id,
|
|
139
|
+
duration_ms: Date.now() - started,
|
|
140
|
+
note: 'provider 启动失败:' + e.message,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const duration = Date.now() - started;
|
|
144
|
+
if (proc.error) {
|
|
145
|
+
const code = proc.error.code || '';
|
|
146
|
+
const timedOut = code === 'ETIMEDOUT';
|
|
147
|
+
const oversize = code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
148
|
+
return {
|
|
149
|
+
status: timedOut ? 'timeout' : 'error',
|
|
150
|
+
provider_id: provider.id,
|
|
151
|
+
duration_ms: duration,
|
|
152
|
+
note: timedOut
|
|
153
|
+
? 'provider 超时'
|
|
154
|
+
: (oversize
|
|
155
|
+
? 'provider 输出超过 ' + MAX_STDOUT_BYTES + ' 字节'
|
|
156
|
+
: 'provider 执行失败:' + (code || proc.error.message || 'unknown')),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (proc.status !== 0) {
|
|
160
|
+
return {
|
|
161
|
+
status: 'error',
|
|
162
|
+
provider_id: provider.id,
|
|
163
|
+
duration_ms: duration,
|
|
164
|
+
note: 'provider 退出码非 0:' + proc.status,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
let response;
|
|
168
|
+
try {
|
|
169
|
+
response = JSON.parse(String(proc.stdout || '').trim() || '{}');
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是 JSON' };
|
|
172
|
+
}
|
|
173
|
+
if (!response || typeof response !== 'object' || Array.isArray(response)) {
|
|
174
|
+
return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是对象' };
|
|
175
|
+
}
|
|
176
|
+
if (response.ok === false) {
|
|
177
|
+
const code = typeof response.code === 'string' && response.code ? response.code : 'provider_error';
|
|
178
|
+
return {
|
|
179
|
+
status: code === 'license_required' ? 'license_required' : 'error',
|
|
180
|
+
provider_id: provider.id,
|
|
181
|
+
code,
|
|
182
|
+
message: typeof response.message === 'string' ? response.message : '',
|
|
183
|
+
duration_ms: duration,
|
|
184
|
+
note: typeof response.message === 'string' ? response.message : code,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (response.ok !== true) {
|
|
188
|
+
return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 响应缺 ok 字段' };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
status: 'active',
|
|
192
|
+
provider_id: provider.id,
|
|
193
|
+
data: response.data,
|
|
194
|
+
duration_ms: duration,
|
|
195
|
+
bytes_out: Buffer.byteLength(String(proc.stdout || ''), 'utf8'),
|
|
196
|
+
note: '',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function appendAudit(entry) {
|
|
201
|
+
try {
|
|
202
|
+
const home = providerHome();
|
|
203
|
+
fs.mkdirSync(home, { recursive: true });
|
|
204
|
+
fs.appendFileSync(
|
|
205
|
+
path.join(home, 'provider-audit.jsonl'),
|
|
206
|
+
JSON.stringify(Object.assign({ schema: SCHEMA, ts: new Date().toISOString() }, entry)) + '\n',
|
|
207
|
+
'utf8',
|
|
208
|
+
);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// 审计失败不影响调用
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runCapability(capability, payload) {
|
|
215
|
+
const config = readConfig();
|
|
216
|
+
if (config.status === 'not_installed') {
|
|
217
|
+
return { status: 'not_installed', provider_id: '', note: '未配置扩展提供方' };
|
|
218
|
+
}
|
|
219
|
+
if (config.status === 'error') {
|
|
220
|
+
return { status: 'error', provider_id: '', note: config.error };
|
|
221
|
+
}
|
|
222
|
+
const provider = pickProvider(config, capability);
|
|
223
|
+
if (!provider) {
|
|
224
|
+
return { status: 'not_installed', provider_id: '', note: '未配置声明 ' + capability + ' 的提供方' };
|
|
225
|
+
}
|
|
226
|
+
const result = invokeProvider(provider, capability, payload);
|
|
227
|
+
appendAudit({
|
|
228
|
+
capability,
|
|
229
|
+
provider_id: result.provider_id,
|
|
230
|
+
status: result.status,
|
|
231
|
+
duration_ms: result.duration_ms,
|
|
232
|
+
bytes_out: result.bytes_out || 0,
|
|
233
|
+
});
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
SCHEMA,
|
|
239
|
+
DEFAULT_TIMEOUT_MS,
|
|
240
|
+
MAX_TIMEOUT_MS,
|
|
241
|
+
MAX_STDOUT_BYTES,
|
|
242
|
+
KNOWN_CAPABILITIES,
|
|
243
|
+
providerHome,
|
|
244
|
+
configPath,
|
|
245
|
+
readConfig,
|
|
246
|
+
pickProvider,
|
|
247
|
+
runCapability,
|
|
248
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 扩展提供方协议 v1(元阁 · capability `o1.route`)
|
|
2
|
+
|
|
3
|
+
元阁可以可选地调用一个由用户显式配置的**本地扩展提供方(provider)**,由它在已装技能白名单内
|
|
4
|
+
对静态路由结果做增补 / 重排。未配置提供方时,`--route` 的行为与输出与之前完全一致。
|
|
5
|
+
|
|
6
|
+
## 1. 配置
|
|
7
|
+
|
|
8
|
+
配置文件:`<YOTTA_PROVIDER_HOME>/provider.json`,默认 `~/.yottameta/provider.json`。
|
|
9
|
+
环境变量 `YOTTA_PROVIDER_HOME` 可覆盖根目录(测试与隔离环境用)。
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"schema": 1,
|
|
14
|
+
"providers": [
|
|
15
|
+
{
|
|
16
|
+
"id": "local-provider",
|
|
17
|
+
"version": "0.1.0",
|
|
18
|
+
"capabilities": ["o1.route"],
|
|
19
|
+
"command": ["node", "C:/path/to/provider.js"],
|
|
20
|
+
"timeout_ms": 600
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `command` 必须是**数组**(argv 语义),以 `shell: false` 执行;不接受字符串命令。
|
|
27
|
+
- `timeout_ms` 默认 600,最小 50,最大 5000;超时即回落。
|
|
28
|
+
- 配置文件缺失、解析失败、`command` 非数组、capability 未知:**只记录状态,不阻断命令**;缺失等同「未安装」。
|
|
29
|
+
|
|
30
|
+
## 2. 调用
|
|
31
|
+
|
|
32
|
+
- 只在用户显式执行 `--route`(CLI)或 `route_request`(MCP,内部走同一 CLI)时触发;安装 / 更新 / 盘点等路径不触发。
|
|
33
|
+
- 元阁把**一个 JSON 请求**写入 provider 的 stdin(随后关闭),从 stdout 读**一个 JSON 响应**;stderr 只作诊断。
|
|
34
|
+
- stdout 上限 256 KB;超过按错误处理并回落。
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"schema": 1,
|
|
39
|
+
"capability": "o1.route",
|
|
40
|
+
"request_id": "<uuid>",
|
|
41
|
+
"payload": {
|
|
42
|
+
"request": "帮我做发布前质量检查",
|
|
43
|
+
"static_result": {
|
|
44
|
+
"playbook": "delivery-quality-gate",
|
|
45
|
+
"confidence": "high",
|
|
46
|
+
"skills": ["yotta-anti-shallow", "yotta-code-quality", "yotta-publish-guard"]
|
|
47
|
+
},
|
|
48
|
+
"installed_skills": [
|
|
49
|
+
{ "slug": "yotta-code-quality", "version": "0.3.0", "sources": ["Codex"] }
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 3. 响应
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"ok": true,
|
|
60
|
+
"capability": "o1.route",
|
|
61
|
+
"data": {
|
|
62
|
+
"skills": [
|
|
63
|
+
{ "slug": "yotta-code-quality", "role": "可选:补充代码质量评审" },
|
|
64
|
+
{ "slug": "yotta-anti-shallow" }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `skills[].slug` 必须 ∈ `installed_skills` 白名单;未安装或未知的 slug 会被丢弃并记入 `dropped`,不会写入结果。
|
|
71
|
+
- `role` 可选;缺省时增补条目使用「由扩展提供方补充」。
|
|
72
|
+
- 元阁先算静态结果:provider 给出的顺序用于重排;静态结果中未被提及的技能会**追加保留**,不会丢失。
|
|
73
|
+
- 返回空列表 / 全是不合法 slug:视为未应用(`applied = false`),静态结果原样返回。
|
|
74
|
+
|
|
75
|
+
需要授权或不可用时:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{ "ok": false, "code": "license_required", "message": "该能力需要授权后使用" }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 4. 状态与回落
|
|
82
|
+
|
|
83
|
+
| 状态 | 触发 | 元阁行为 |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `not_installed` | 无配置 / 无匹配 capability | 纯静态路由,文本输出与历史一致 |
|
|
86
|
+
| `active` | 调用成功且响应合法 | 白名单内增补 / 重排,静态结果保留 |
|
|
87
|
+
| `license_required` | provider 明确返回该 code | 纯静态路由 +「需授权」提示 |
|
|
88
|
+
| `timeout` | 超过 `timeout_ms` | 纯静态路由 + 一行状态提示 |
|
|
89
|
+
| `invalid_output` | 非 JSON / 缺 `ok` | 纯静态路由 + 一行状态提示 |
|
|
90
|
+
| `error` | 启动失败 / 退出码非 0 / 输出超限 / 配置非法 | 纯静态路由 + 一行状态提示 |
|
|
91
|
+
|
|
92
|
+
`--json` 输出始终包含 `dynamic` 块:`status` / `provider_id` / `applied` / `added` / `dropped` / `note`;
|
|
93
|
+
CLI 文本输出只在状态非 `not_installed` 时多一行「动态路由: ...」,退出码不受影响(始终 0)。
|
|
94
|
+
|
|
95
|
+
## 5. 审计与边界
|
|
96
|
+
|
|
97
|
+
- 每次实际调用写一行 `<YOTTA_PROVIDER_HOME>/provider-audit.jsonl`:`ts` / `capability` / `provider_id` / `status` / `duration_ms` / `bytes_out`。
|
|
98
|
+
- 审计**不记录**需求原文或任何 payload 内容。
|
|
99
|
+
- 元阁不替 provider 联网;provider 自身行为由它自己的包声明。
|
|
100
|
+
- 删除 `provider.json` 即回到纯静态路由,无残留依赖。
|
|
101
|
+
- provider 输出只当数据使用:白名单外的 slug、非法结构一律丢弃,不作为指令执行。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| `yotta-anti-shallow` | 元谨 | `@yottameta/yotta-anti-shallow` | 1.4.1 | 防 AI 敷衍规则引擎(深入分析/根因追溯时激活) |
|
|
9
9
|
| `yotta-code-quality` | 元质 | `@yottameta/yotta-code-quality` | 0.4.1 | 结对式代码质量评审(十二类腐化风险 + 0-100 健康分) |
|
|
10
10
|
| `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.2 | 跨会话/跨项目通用工作流协议(状态就近存) |
|
|
11
|
-
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.
|
|
11
|
+
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.19.0 | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
|
|
12
12
|
| `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.2.1 | 学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目 |
|
|
13
13
|
| `yotta-security-audit` | 元安 | `@yottameta/yotta-security-audit` | 0.3.0 | 安全扫描引擎:13 类检测器 + 系统安全基线 + 学生数据隐私(教育版) |
|
|
14
14
|
| `yotta-vetter` | 元审 | `@yottameta/yotta-vetter` | 0.2.6 | 安全审查协议:四阶段 review + SAFE TO INSTALL 判定 |
|