@numa-tech/numa 1.12.7 → 1.12.8

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.
Files changed (65) hide show
  1. package/README.md +139 -43
  2. package/dist/ai-registry/bridge.d.ts +8 -0
  3. package/dist/ai-registry/bridge.js +63 -0
  4. package/dist/ai-registry/bridge.js.map +1 -0
  5. package/dist/ai-registry/client.d.ts +29 -3
  6. package/dist/ai-registry/client.js +112 -8
  7. package/dist/ai-registry/client.js.map +1 -1
  8. package/dist/ai-registry/commands.js +367 -112
  9. package/dist/ai-registry/commands.js.map +1 -1
  10. package/dist/ai-registry/errors.d.ts +2 -1
  11. package/dist/ai-registry/errors.js +12 -1
  12. package/dist/ai-registry/errors.js.map +1 -1
  13. package/dist/ai-registry/mcp-installer.d.ts +1 -0
  14. package/dist/ai-registry/mcp-installer.js +4 -0
  15. package/dist/ai-registry/mcp-installer.js.map +1 -1
  16. package/dist/ai-registry/namespace.d.ts +17 -0
  17. package/dist/ai-registry/namespace.js +53 -0
  18. package/dist/ai-registry/namespace.js.map +1 -0
  19. package/dist/ai-registry/schemas.d.ts +127 -2
  20. package/dist/ai-registry/schemas.js +42 -2
  21. package/dist/ai-registry/schemas.js.map +1 -1
  22. package/dist/ai-registry/sync.d.ts +23 -0
  23. package/dist/ai-registry/sync.js +276 -0
  24. package/dist/ai-registry/sync.js.map +1 -0
  25. package/dist/ai-registry/tui.d.ts +2 -0
  26. package/dist/ai-registry/tui.js +140 -0
  27. package/dist/ai-registry/tui.js.map +1 -0
  28. package/dist/ai-registry/workspace.d.ts +191 -0
  29. package/dist/ai-registry/workspace.js +167 -0
  30. package/dist/ai-registry/workspace.js.map +1 -0
  31. package/dist/app-config.d.ts +3 -2
  32. package/dist/app-config.js +5 -3
  33. package/dist/app-config.js.map +1 -1
  34. package/dist/cli.js +25 -3
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +1 -0
  37. package/dist/command-catalog.js +97 -10
  38. package/dist/command-catalog.js.map +1 -1
  39. package/dist/config-page.js +2 -0
  40. package/dist/config-page.js.map +1 -1
  41. package/dist/config.d.ts +1 -0
  42. package/dist/config.js +1 -0
  43. package/dist/config.js.map +1 -1
  44. package/dist/pipeline/client.d.ts +10 -6
  45. package/dist/pipeline/client.js +7 -4
  46. package/dist/pipeline/client.js.map +1 -1
  47. package/dist/pipeline/commands.d.ts +5 -1
  48. package/dist/pipeline/commands.js +89 -18
  49. package/dist/pipeline/commands.js.map +1 -1
  50. package/dist/pipeline/schemas.d.ts +7 -7
  51. package/dist/pipeline/schemas.js +2 -2
  52. package/dist/pipeline/schemas.js.map +1 -1
  53. package/dist/platform-profile.d.ts +8 -2
  54. package/dist/platform-profile.js +5 -2
  55. package/dist/platform-profile.js.map +1 -1
  56. package/dist/web-console-page.d.ts +3 -3
  57. package/dist/web-console-page.js +75 -20
  58. package/dist/web-console-page.js.map +1 -1
  59. package/dist/web-console.d.ts +1 -1
  60. package/dist/web-console.js +46 -0
  61. package/dist/web-console.js.map +1 -1
  62. package/package.json +8 -5
  63. package/skills/numa-ai-registry/SKILL.md +25 -19
  64. package/skills/numa-ai-registry/evals/evals.json +10 -10
  65. package/skills/numa-ai-registry/references/command-contract.md +41 -6
package/README.md CHANGED
@@ -187,9 +187,11 @@ numa config \
187
187
  | `numa admin context` | 需角色 | 要求 `platform-admin` 或 `numa-admin` |
188
188
  | `numa registry instances list/create/update/delete` | 需登录 | 管理数据库中的 Nacos 实例和 OIDC 配置;写操作需要 Registry admin |
189
189
  | `numa registry namespaces list/resolve/check/create/update/delete` | 需登录 | 管理多实例 namespace 路由与跨集群默认项;写操作需要 Registry admin |
190
- | `numa registry skill list/inspect/install/upload/...` | 需登录 | 发现、安装和治理 Nacos Skills;发布类操作需要 Registry admin |
191
- | `numa registry mcp list/inspect/install/create/update/delete` | 需登录 | 发现、安装和维护 Nacos MCP Servers;写操作需要 Registry admin |
192
- | `numa registry tui` | 需登录 | 在终端交互浏览、安装和治理 Skill/MCP |
190
+ | `numa registry capabilities/init/lock/sync/upgrade` | 需登录 | 读取授权边界并以可提交的 manifest/lock 确定性安装 |
191
+ | `numa registry skill list/inspect/install/upload/...` | 需登录 | 发现、安装和治理 Nacos Skills;服务端按团队、角色和生命周期授权 |
192
+ | `numa registry mcp list/inspect/install/bridge/...` | 需登录 | 通过企业 Router 安装/桥接 MCP,并维护版本、标签和工具状态 |
193
+ | `numa registry approvals/router/audit` | 需登录 | 两人制发布、Router 健康/对账和追加式审计 |
194
+ | `numa registry tui` | 需登录 | 打开 namespace、Skills、MCP、审批、Router、审计全屏工作台 |
193
195
  | `numa media capabilities` | 需登录 | 查询媒体 API、Provider 和版本能力 |
194
196
  | `numa media tts submit` | 需登录 | 提交 TTS 异步任务 |
195
197
  | `numa media video submit` | 需登录 | 提交视频生成异步任务 |
@@ -232,6 +234,8 @@ CLI 的本地角色检查用于快速反馈和隐藏无权限入口,不能替
232
234
  - `profileUrl`:完整的公开 client-configuration URL;设置后各模块只使用该
233
235
  profile 声明的 service 与 authority;
234
236
  - `profileId`:可选的 profile 标识锁定;远端返回其他标识时拒绝启用;
237
+ - `aiRegistryDefaultNamespaceKey`:用户级 Registry 默认 namespace;优先级低于命令行和
238
+ 项目 `.numa/registry.json`,高于 profile 默认值;
235
239
  - `registryUrl`:npm registry,默认公共 npm,也可配置公司私有 registry;
236
240
  - `checkUpdates`:是否检查 CLI 更新。
237
241
 
@@ -268,9 +272,9 @@ numa login --no-console --no-config --json
268
272
  ```
269
273
 
270
274
  控制台展示当前身份、Realm/Client 角色、AI Registry、ChatGPT 账号、基础配置和机器可读命令目录。
271
- AI Registry 页按“namespace → Skill/MCP → 详情/动作”组织,支持搜索、详情、安装以及 Skill
272
- 提交/发布/上下线。发布按钮是否可用只是界面提示,实际权限始终由平台 Keycloak Policy
273
- Enforcer 决定。
275
+ AI Registry 页包含目录、发布收件箱、实例/双凭据、Namespace/Profile、Router、权限/审计六个
276
+ 工作区,并展示 capability 返回的环境、动作和 Router 状态。按钮是否可用只是界面提示,实际
277
+ 权限、两人制和生命周期始终由平台服务端决定。
274
278
  ChatGPT 账号页先读取 `codex-multi-auth` 的本地账号与额度缓存并立即展示,再使用当前
275
279
  Keycloak 会话连接平台补充当前用户可用账号、剩余 limit 和同步状态。平台不可用时页面
276
280
  保留本地结果并明确显示“远程同步失败”,不会把 access token 或 refresh token 返回给页面。
@@ -287,20 +291,27 @@ CSP。OIDC access/refresh token 始终保留在 CLI 进程及私有缓存中,
287
291
 
288
292
  ## 私有 Nacos AI Registry
289
293
 
290
- `numa registry` 对接 `mci-devops-platform` 的 `/api/v1/ai-registry` 类型化 API。客户端只携带
291
- 当前用户 Keycloak Bearer Token;平台从自己的数据库解析 Nacos 实例、namespace 和该实例的
292
- OIDC client-credentials,再访问目标 Nacos。Nacos token、OIDC client secret endpoint
293
- credential 不写入 Numa 配置,也不由读取 API 返回。
294
+ `numa registry` 对接 `mci-devops-platform` 的 `/api/v1/ai-registry` 类型化 API。Nacos 是
295
+ Skill/MCP 内容、版本、标签和上下线状态的主仓库;平台只维护组织、团队、权限、审批、profile、
296
+ Router 绑定、对账和审计。Numa 只携带当前用户 Keycloak Bearer Token,且 capability 响应若
297
+ 包含 Nacos 地址、token、secret、password credential 字段会被客户端拒绝。
298
+
299
+ `GET /api/v1/ai-registry/capabilities` 返回当前用户可见 namespace、环境、动作、Router
300
+ Streamable HTTP/OAuth resource 和客户端适配器。profile v1 可追加
301
+ `services.aiRegistry.capabilityUrl` 与
302
+ `services.aiRegistry.configuration.defaultNamespaceKey`,旧客户端会忽略这些字段。
294
303
 
295
304
  权限契约:
296
305
 
297
- - `mci-devops-platform-ai-registry-reader`:`view`,可列出、检查、下载和安装;
298
- - `mci-devops-platform-ai-registry-admin`:`view` + `edit`,作为操作管理员管理实例、namespace、Skill 和 MCP;
299
- - 平台 operations admin 同时拥有 `view` + `edit`。
306
+ - 团队 `MEMBER+` 可使用已授权 namespace;
307
+ - 团队 `MAINTAINER/ADMIN/OWNER` 可维护本团队草稿;
308
+ - `ai-registry-reviewer` 可审核发布申请,且不得审核或发布自己的申请;
309
+ - `ai-registry-admin` 管理 Registry,`ops-admin` 才能执行 `*` allowlist、Admin API 和紧急操作。
300
310
 
301
- Nacos `baseUrl`、token URI、client ID、client secret scope 全部按实例保存在
302
- `mci-devops-platform` 数据库。client secret 使用 AES-256-GCM 密文存储,读取接口只返回
303
- `credentialConfigured`,不会返回明文或密文。平台部署只需从 Secret/KMS 注入数据库密文的
311
+ 每个 Nacos 实例分别配置 `READER` `MAINTAINER` 凭据,可使用 OIDC client credentials 或
312
+ Nacos username/password,并为每套凭据设置精确 namespace allowlist。凭据使用 AES-256-GCM
313
+ 密文存储,读取接口只返回 configured 状态,不返回明文或密文。旧单凭据作为 `LEGACY_BOTH`
314
+ 继续工作,管理员验证两套新凭据后再显式停用。平台部署从 Secret/KMS 注入数据库密文的
304
315
  主密钥环 `AI_REGISTRY_SECRET_ENCRYPTION_KEYS` 与当前 key ID
305
316
  `AI_REGISTRY_SECRET_CURRENT_KEY_ID`;这两项是密钥管理材料,不是某个 Nacos 实例的配置。
306
317
  密钥环格式为逗号分隔的 `keyId=Base64(32-byte-key)`;轮换时先加入新 key 并切换当前 key ID,
@@ -309,25 +320,35 @@ SSRF 安全策略仍由 `AI_REGISTRY_ALLOWED_UPSTREAM_HOSTS`、
309
320
  `AI_REGISTRY_ALLOWED_TOKEN_HOSTS` 控制,allow-list 为空时 fail closed;HTTP 只可通过
310
321
  `AI_REGISTRY_ALLOW_HTTP=true` 用于开发环境。
311
322
 
312
- Nacos 实例文件示例(`clientSecret` 仅写入):
323
+ Nacos 实例文件示例(凭据字段仅写入,实际字段以平台 capability/API revision 为准):
313
324
 
314
325
  ```json
315
326
  {
316
327
  "code": "cluster-a",
317
328
  "displayName": "Nacos Cluster A",
318
329
  "baseUrl": "https://nacos.internal.example:8848",
319
- "authenticationType": "OIDC_CLIENT_CREDENTIALS",
320
- "tokenUri": "https://sso.internal.example/realms/mci/protocol/openid-connect/token",
321
- "clientId": "nacos-registry-broker",
322
- "clientSecret": "write-only-secret",
323
- "scope": "openid",
330
+ "credentials": {
331
+ "reader": {
332
+ "authenticationType": "OIDC_CLIENT_CREDENTIALS",
333
+ "tokenUri": "https://sso.internal.example/realms/mci/protocol/openid-connect/token",
334
+ "clientId": "nacos-registry-reader",
335
+ "clientSecret": "write-only-secret",
336
+ "namespaceAllowlist": ["team-ai"]
337
+ },
338
+ "maintainer": {
339
+ "authenticationType": "NACOS_USERNAME_PASSWORD",
340
+ "username": "registry-maintainer",
341
+ "password": "write-only-password",
342
+ "namespaceAllowlist": ["team-ai"]
343
+ }
344
+ },
324
345
  "description": "A 集群",
325
346
  "status": "ACTIVE"
326
347
  }
327
348
  ```
328
349
 
329
- `instances update` 必须带列表结果中的当前 `version`,用于乐观锁保护;省略 `clientSecret`
330
- 表示保留已有密文。建议让包含明文 secret 的输入文件仅对当前用户可读,并在写入后安全删除。
350
+ `instances update` 必须带列表结果中的当前 `version`,用于乐观锁保护;省略某套凭据表示保留
351
+ 已有密文。建议让包含明文 secret 的输入文件仅对当前用户可读,并在写入后安全删除。
331
352
 
332
353
  namespace 文件示例:
333
354
 
@@ -343,13 +364,13 @@ namespace 文件示例:
343
364
  ```
344
365
 
345
366
  `namespaceKey` 是客户端使用的稳定名称,`namespaceId` 是该 Nacos 实例中的真实 ID。同一个
346
- `namespaceKey` 可以存在于多个实例;其中最多一个 ACTIVE 路由可设为 `isDefault=true`。
347
- 客户端只传 `--namespace team-ai` 时,平台会选择唯一匹配项或默认项;同名且没有默认项时返回
348
- `AI_REGISTRY_NAMESPACE_AMBIGUOUS`,此时必须加 `--instance cluster-b` 消歧。
367
+ `namespaceKey` 可以存在于多个实例。Numa 的选择顺序固定为 `--namespace` → 最近的
368
+ `.numa/registry.json` 用户 `aiRegistryDefaultNamespaceKey` → profile 默认值;选择结果必须
369
+ 存在于 capability 授权列表。同名绑定必须由 manifest/`--instance` 消歧,默认值无效时明确失败,
370
+ 不会切换到其他可用 namespace。旧 `isDefault` 仅供迁移期兼容客户端使用。
349
371
 
350
- V34 会把旧服务行拆成实例和 namespace,但旧 `credentialRef` 无法自动转换为数据库密文,
351
- 所以迁移后的实例会先置为 `DISABLED`。管理员写入完整 OIDC 配置和 `clientSecret` 后再启用;
352
- 跨集群重复的旧 namespace 不会自动猜测默认项。
372
+ profile 管理员发布 `defaultNamespaceKey` 且满足最低客户端版本后,才可逐步移除旧 `isDefault`
373
+ fallback。服务器端数据库迁移必须从当前版本新增 Flyway migration,不修改历史迁移。
353
374
 
354
375
  先发现实例和 namespace,再通过 namespace 操作资源:
355
376
 
@@ -359,6 +380,13 @@ numa registry namespaces list --namespace team-ai --json
359
380
  numa registry namespaces resolve team-ai --json
360
381
  numa registry namespaces check team-ai --json
361
382
 
383
+ numa registry capabilities --json
384
+ numa registry init --namespace team-ai --instance cluster-b --json
385
+ # 编辑 .numa/registry.json 后:
386
+ numa registry lock --json
387
+ numa registry sync --json
388
+ numa registry upgrade release-helper --json
389
+
362
390
  numa registry skill list --namespace team-ai --search deploy --json
363
391
  numa registry skill inspect release-helper --namespace team-ai --json
364
392
  numa registry skill install release-helper \
@@ -367,50 +395,118 @@ numa registry skill install release-helper \
367
395
  numa registry mcp list --namespace team-ai --json
368
396
  numa registry mcp inspect internal-docs --namespace team-ai --json
369
397
  numa registry mcp install internal-docs \
370
- --namespace team-ai --instance cluster-b --agent claude --scope project --json
398
+ --namespace team-ai --instance cluster-b --agent claude --scope project --transport remote --json
399
+
400
+ # 仅供 stdio Agent;复用 Numa 登录/刷新连接 Router
401
+ numa registry mcp bridge --namespace team-ai
371
402
 
372
403
  # 也可使用终端交互界面
373
404
  numa registry tui
374
405
  ```
375
406
 
376
- Skill 发布生命周期:
407
+ 两人制发布生命周期:
377
408
 
378
409
  ```bash
379
410
  numa registry skill upload ./release-helper.zip \
380
411
  --namespace team-ai --target-version 1.0.0 --json
381
- numa registry skill submit release-helper --namespace team-ai --json
382
- numa registry skill publish release-helper \
383
- --namespace team-ai --version 1.0.0 --json
384
- numa registry skill online release-helper --namespace team-ai --json
385
- numa registry skill scope release-helper PRIVATE --namespace team-ai --json
386
- numa registry skill tags release-helper deploy java --namespace team-ai --json
412
+ numa registry approvals submit skill release-helper \
413
+ --namespace team-ai --resource-version 1.0.0 --reason "scanner passed" --json
414
+ # 必须由另一位 reviewer 执行
415
+ numa registry approvals review rr_123 \
416
+ --decision APPROVED --reason "reviewed" --version 1 --json
417
+ numa registry approvals publish rr_123 --version 2 --json
418
+ numa registry audit --namespace team-ai --json
387
419
  ```
388
420
 
421
+ 生命周期由服务端统一为
422
+ `DRAFT → SUBMITTED → APPROVED/REJECTED → PUBLISHING → PUBLISHED/FAILED → OFFLINE`。
423
+ 生产及企业默认 namespace 的 Skill 不允许绕过 scanner;非生产豁免也必须带原因并由 Reviewer
424
+ 审批。MCP 发布由服务端执行协议、工具 schema、禁用工具和 Endpoint 健康预检。
425
+
389
426
  远端删除必须带 `--yes`。本地同名 Skill/MCP 默认拒绝覆盖;用户明确同意后才使用
390
427
  `--force`,支持原子替换的适配器会先保留时间戳备份。Skill 下载必须通过平台返回的
391
428
  SHA-256 校验,ZIP 在解压前会检查根 `SKILL.md`、路径穿越、符号链接、重复路径、文件数和
392
- 膨胀大小。MCP 定义中的敏感 env/header 只能使用 `${ENV_VAR}` 占位符,不能携带明文密钥。
429
+ 膨胀大小。`.numa/registry.lock.json` 保存解析后的精确版本、Nacos 来源、SHA-256 和安装状态;
430
+ 普通 `sync` 不解析标签,只有首次 `lock` 或显式 `upgrade` 会变更版本。MCP 客户端只安装
431
+ capability 发布且健康的企业 Router,不安装 Nacos 中的下游 endpoint。
393
432
 
394
433
  ### 客户端安装适配
395
434
 
396
435
  | 客户端 | Skill | MCP Server |
397
436
  | --- | --- | --- |
398
- | ChatGPT / Codex coding workspace | `.codex/skills` | ChatGPT 仅接受远程 HTTPS MCP App;Numa 返回 Developer mode/App 创建指引,不会冒充修改工作区设置 |
437
+ | ChatGPT / Codex coding workspace | `.codex/skills` | ChatGPT 连接企业 Router 的远程 HTTPS OAuth resource;Numa 返回 App 创建指引 |
399
438
  | Codex | `.codex/skills` | 调用官方 `codex mcp add`,仅 user scope |
400
439
  | Claude | `.claude/skills` | 项目 `.mcp.json`;user scope 使用 `~/.claude.json` |
401
440
  | Cursor | `.cursor/skills` | `.cursor/mcp.json` |
402
441
  | WorkBuddy | `.workbuddy/skills` | `.workbuddy/mcp.json`;目录可通过 custom adapter 调整 |
403
442
  | OpenClaw | `.openclaw/skills` | 调用官方 `openclaw mcp set`,仅 user scope |
404
443
 
405
- ChatGPT 本身不能直接连接本地 stdio MCP。Numa `--agent chatgpt` 只为远程 HTTPS endpoint
406
- 生成受控安装指引;私网或本机 MCP 需要先发布为远程服务,或使用 ChatGPT 支持的 Secure
407
- MCP Tunnel。ChatGPT workspace 还需管理员按其计划和 RBAC 开启 Developer mode。
444
+ ChatGPT 不能启动本地 stdio bridge;它直接连接企业 Router 的远程 HTTPS OAuth resource。
445
+ 其他只支持 stdio Agent 可运行 `numa registry mcp bridge`,由 Numa 获取/刷新用户 Token
446
+ 并只发送给 Router,不向 Nacos 或下游 MCP 透传。ChatGPT workspace 仍需管理员按计划和
447
+ RBAC 开启 Developer mode。
408
448
 
409
449
  仓库内置的自用 Skill 位于 `skills/numa-ai-registry/SKILL.md`,并随 npm 包发布。它指导 AI
410
450
  按“认证 → 服务解析 → inspect → 安装/治理 → 复查”的顺序使用 Registry,同时明确禁止索取
411
451
  Nacos token、绕过 403 或未经确认覆盖本地配置。对应场景评测在
412
452
  `skills/numa-ai-registry/evals/evals.json`。
413
453
 
454
+ ## Jenkins 流水线与生产操作
455
+
456
+ `numa pipeline` 通过平台授权代理查询和操作 Jenkins。CLI 不接收 Jenkins 用户名、Token 或
457
+ URL;生产 build、retry 和 stop 由服务端再次校验 `ops-admin` 与审批依据。
458
+
459
+ 三个容易混淆的标识含义不同:
460
+
461
+ | 标识 | 生成方 | 用途 | 重试规则 |
462
+ | --- | --- | --- | --- |
463
+ | `requestId` | 平台 | 标识已创建的构建任务,用于 status、log、stop、retry | 新任务产生新值 |
464
+ | `idempotency-key` | CLI 或调用者 | 判断重复提交是否属于同一次部署 | 同一次提交重试必须复用;新部署使用新值 |
465
+ | `approve-id` | 审批流程或管理员 | 记录生产操作依据 | 按审批范围使用,不是任务标识 |
466
+
467
+ `idempotency-key` 是可选参数。不传时 CLI 会自动生成 UUID,并在成功结果中以
468
+ `idempotency_key` 返回。生产部署和自动化脚本建议显式指定:如果网络中断导致结果不明,
469
+ 使用原值重新提交,平台会返回原任务或拒绝内容不一致的冲突,不会无意创建第二个构建。
470
+
471
+ ```bash
472
+ numa pipeline inspect mci-devops-platform --json
473
+
474
+ numa pipeline build mci-devops-platform \
475
+ --tier production \
476
+ --branch main \
477
+ --approve-id OPS-APPROVAL-20260818-001 \
478
+ --idempotency-key mci-devops-platform-prd-20260818-001 \
479
+ --wait \
480
+ --json
481
+ ```
482
+
483
+ 生产操作必须提供 `--approve-id` 或至少 10 个字符的 `--production-reason`。CLI 将二者作为
484
+ 独立治理字段提交;不能使用 `--param APPROVE_ID=...` 绕过治理。平台保存两者及审计记录,
485
+ 仅在生产 trigger/retry 时把经过验证的 `approveId` 映射为 Jenkins `APPROVE_ID`;
486
+ `productionReason` 不会转发给 Jenkins。
487
+
488
+ `--wait` 和 `status --watch` 约每 2 秒轮询一次,当前没有默认超时,会持续到 `SUCCESS`、
489
+ `FAILED`、`ABORTED`、`CANCELLED` 或 `NOT_BUILT`。按 `Ctrl+C` 或遇到网络错误只会结束本地
490
+ 等待,不会停止 Jenkins 构建;真正停止任务需要显式执行 `pipeline stop ... --yes`。
491
+ `build --wait` 仅在 `SUCCESS` 时返回退出码 0,其他终态返回非零。
492
+
493
+ 保存 build 返回的 `requestId`,可以随时恢复查询:
494
+
495
+ ```bash
496
+ numa pipeline status req_01ABC --watch
497
+ numa pipeline log req_01ABC --follow
498
+ ```
499
+
500
+ 如果没有保存 `requestId`,先按应用和环境检索构建记录,再使用返回的 `requestId`:
501
+
502
+ ```bash
503
+ numa pipeline builds --app mci-devops-platform --tier production --limit 20 --json
504
+ ```
505
+
506
+ `pipeline builds` 使用游标分页:`--limit` 是每页条数,不是页码;下一页应把上一页响应中的
507
+ `next_cursor` 传给 `--cursor`。面向 AI 和自动化的同一套操作语义也包含在 `numa commands --json` 的
508
+ `operationalNotes` 字段中,因此基本流水线操作不依赖额外 Skill。
509
+
414
510
  ## 平台媒体服务
415
511
 
416
512
  自 1.8.0 起,`numa media` 通过当前 Keycloak 会话调用 `/api/v1/media`。第三方
@@ -0,0 +1,8 @@
1
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
2
+ import type { AiRegistryClient } from "./client.js";
3
+ export declare function runMcpBridge(client: AiRegistryClient, options: {
4
+ namespace?: string;
5
+ instance?: string;
6
+ workspace?: string;
7
+ }): Promise<void>;
8
+ export declare function bridgeMcpTransports(local: Transport, remote: Transport): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { loadConfig } from "../config.js";
4
+ import { fetchWithKeycloakIdentity } from "../oauth.js";
5
+ import { AiRegistryError, redactRegistryText } from "./errors.js";
6
+ import { resolveRegistryNamespace } from "./namespace.js";
7
+ export async function runMcpBridge(client, options) {
8
+ const context = await resolveRegistryNamespace(client, options);
9
+ const router = context.capability.router;
10
+ if (!router?.endpoint || router.status !== "READY") {
11
+ throw new AiRegistryError(`Namespace ${context.namespace.namespaceKey} has no ready enterprise Router.`, "ai_registry_router_unavailable");
12
+ }
13
+ const endpoint = new URL(router.endpoint);
14
+ const local = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(endpoint.hostname);
15
+ if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && local)) {
16
+ throw new AiRegistryError("Enterprise Router endpoint must use HTTPS.", "ai_registry_router_invalid");
17
+ }
18
+ await client.checkRouter(context.namespace.id);
19
+ const authConfig = loadConfig("aiRegistry");
20
+ const remote = new StreamableHTTPClientTransport(endpoint, {
21
+ fetch: async (input, init) => fetchWithKeycloakIdentity(authConfig, String(input), () => ({ ...init, headers: new Headers(init?.headers) }))
22
+ });
23
+ await bridgeMcpTransports(new StdioServerTransport(), remote);
24
+ }
25
+ export async function bridgeMcpTransports(local, remote) {
26
+ let closed = false;
27
+ let resolveClosed;
28
+ let rejectClosed;
29
+ const completed = new Promise((resolvePromise, rejectPromise) => {
30
+ resolveClosed = resolvePromise;
31
+ rejectClosed = rejectPromise;
32
+ });
33
+ const fail = (error) => {
34
+ if (closed)
35
+ return;
36
+ closed = true;
37
+ rejectClosed?.(new AiRegistryError(`MCP bridge failed: ${redactRegistryText(error.message)}`, "ai_registry_bridge_failed"));
38
+ void Promise.allSettled([local.close(), remote.close()]);
39
+ };
40
+ const finish = () => {
41
+ if (closed)
42
+ return;
43
+ closed = true;
44
+ resolveClosed?.();
45
+ void Promise.allSettled([local.close(), remote.close()]);
46
+ };
47
+ local.onmessage = (message) => { void remote.send(message).catch(fail); };
48
+ remote.onmessage = (message) => { void local.send(message).catch(fail); };
49
+ local.onerror = fail;
50
+ remote.onerror = fail;
51
+ local.onclose = finish;
52
+ remote.onclose = finish;
53
+ try {
54
+ await remote.start();
55
+ await local.start();
56
+ await completed;
57
+ }
58
+ catch (error) {
59
+ fail(error instanceof Error ? error : new Error(String(error)));
60
+ await completed;
61
+ }
62
+ }
63
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../../src/ai-registry/bridge.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import type { AuthConfig } from "../config.js";
2
2
  import { type KeycloakTokenProvider } from "../oauth.js";
3
- import { type JsonObject, type RegistryInstance, type RegistryNamespace } from "./schemas.js";
3
+ import { type JsonObject, type RegistryCapabilities, type RegistryInstance, type RegistryNamespace, type RegistryRouter } from "./schemas.js";
4
4
  export interface AiRegistryClientDependencies {
5
5
  fetch?: typeof fetch;
6
6
  tokenProvider?: KeycloakTokenProvider;
@@ -9,6 +9,7 @@ export interface SkillDownload {
9
9
  bytes: Buffer;
10
10
  fileName: string;
11
11
  sha256: string;
12
+ version?: string;
12
13
  }
13
14
  export declare class AiRegistryClient {
14
15
  private readonly config;
@@ -18,6 +19,7 @@ export declare class AiRegistryClient {
18
19
  private authenticatedFetch;
19
20
  private json;
20
21
  listInstances(): Promise<RegistryInstance[]>;
22
+ getCapabilities(): Promise<RegistryCapabilities>;
21
23
  createInstance(input: JsonObject): Promise<RegistryInstance>;
22
24
  updateInstance(instanceId: number, input: JsonObject): Promise<RegistryInstance>;
23
25
  deleteInstance(instanceId: number): Promise<void>;
@@ -48,17 +50,41 @@ export declare class AiRegistryClient {
48
50
  commitMsg?: string;
49
51
  uploadAction?: string;
50
52
  }): Promise<unknown>;
51
- skillAction(namespaceId: number, skillName: string, action: "submit" | "publish" | "online" | "offline" | "scope" | "biz-tags" | "delete", body?: Record<string, unknown>): Promise<unknown>;
53
+ skillAction(namespaceId: number, skillName: string, action: "submit" | "publish" | "online" | "offline" | "scope" | "biz-tags" | "labels" | "delete", body?: Record<string, unknown>): Promise<unknown>;
52
54
  listMcpServers(namespaceId: number, options?: {
53
55
  search?: string;
54
56
  mcpName?: string;
55
57
  pageNo?: number;
56
58
  pageSize?: number;
57
59
  }): Promise<JsonObject>;
58
- getMcpServer(namespaceId: number, mcpName: string, version?: string): Promise<JsonObject>;
60
+ getMcpServer(namespaceId: number, mcpName: string, versionOrOptions?: string | {
61
+ version?: string;
62
+ label?: string;
63
+ }): Promise<JsonObject>;
59
64
  createMcpServer(namespaceId: number, specification: JsonObject): Promise<unknown>;
60
65
  updateMcpServer(namespaceId: number, mcpName: string, specification: JsonObject): Promise<unknown>;
61
66
  deleteMcpServer(namespaceId: number, mcpName: string, version?: string): Promise<unknown>;
67
+ mcpAction(namespaceId: number, mcpName: string, action: "online" | "offline" | "biz-tags", body?: Record<string, unknown>): Promise<unknown>;
68
+ setMcpToolEnabled(namespaceId: number, mcpName: string, toolName: string, enabled: boolean, version?: number): Promise<unknown>;
69
+ createSkillDraft(namespaceId: number, skillName: string, body?: Record<string, unknown>): Promise<unknown>;
70
+ listReleaseRequests(options?: {
71
+ namespaceId?: number;
72
+ state?: string;
73
+ cursor?: string;
74
+ limit?: number;
75
+ }): Promise<JsonObject>;
76
+ createReleaseRequest(body: JsonObject): Promise<unknown>;
77
+ reviewReleaseRequest(requestId: string, body: JsonObject): Promise<unknown>;
78
+ publishReleaseRequest(requestId: string, body?: JsonObject): Promise<unknown>;
79
+ getRouter(namespaceId: number): Promise<RegistryRouter>;
80
+ connectRouter(namespaceId: number, body?: JsonObject): Promise<unknown>;
81
+ checkRouter(namespaceId: number): Promise<JsonObject>;
82
+ reconcileNamespace(namespaceId: number): Promise<JsonObject>;
83
+ listAuditEvents(options?: {
84
+ namespaceId?: number;
85
+ cursor?: string;
86
+ limit?: number;
87
+ }): Promise<JsonObject>;
62
88
  private object;
63
89
  private instanceResponse;
64
90
  private namespaceResponse;
@@ -4,7 +4,7 @@ import { readFile, stat } from "node:fs/promises";
4
4
  import { resolveBackendUrl } from "../backend.js";
5
5
  import { ensureAccessToken, fetchWithKeycloakIdentity } from "../oauth.js";
6
6
  import { AiRegistryError, redactRegistryText } from "./errors.js";
7
- import { ApiErrorSchema, JsonObjectSchema, RegistryInstancesSchema, RegistryNamespacesSchema } from "./schemas.js";
7
+ import { ApiErrorSchema, JsonObjectSchema, RegistryCapabilitiesSchema, RegistryRouterSchema, RegistryInstancesSchema, RegistryNamespacesSchema } from "./schemas.js";
8
8
  const API_ROOT = "/api/v1/ai-registry";
9
9
  const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
10
10
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
@@ -50,7 +50,9 @@ function apiError(response, body) {
50
50
  : typeof body === "string"
51
51
  ? body
52
52
  : response.statusText;
53
- return new AiRegistryError(`AI Registry HTTP ${response.status}: ${redactRegistryText(message || "request failed")}`, parsed.success ? parsed.data.code ?? "ai_registry_api_error" : "ai_registry_api_error", response.status, parsed.success ? parsed.data.requestId : response.headers.get("x-request-id") ?? undefined);
53
+ return new AiRegistryError(`AI Registry HTTP ${response.status}: ${redactRegistryText(message || "request failed")}`, parsed.success ? parsed.data.code ?? "ai_registry_api_error" : "ai_registry_api_error", response.status, parsed.success
54
+ ? parsed.data.requestId ?? response.headers.get("x-request-id") ?? undefined
55
+ : response.headers.get("x-request-id") ?? undefined, parsed.success ? parsed.data.details : undefined);
54
56
  }
55
57
  function safeIdentifier(value, label) {
56
58
  if (!value || value.length > 160 || /[\\/?#%\u0000-\u001f\u007f]/u.test(value)) {
@@ -68,7 +70,9 @@ export class AiRegistryClient {
68
70
  this.tokenProvider = dependencies.tokenProvider ?? ensureAccessToken;
69
71
  }
70
72
  async authenticatedFetch(path, init = {}) {
71
- const url = resolveBackendUrl(this.config, { method: "GET", path });
73
+ const url = /^https?:\/\//iu.test(path)
74
+ ? resolveBackendUrl({ ...this.config, allowAbsoluteBackendUrls: true }, { method: "GET", url: path })
75
+ : resolveBackendUrl(this.config, { method: "GET", path });
72
76
  const headers = new Headers(init.headers);
73
77
  if (!headers.has("Accept"))
74
78
  headers.set("Accept", "application/json");
@@ -91,6 +95,15 @@ export class AiRegistryClient {
91
95
  }
92
96
  return parsed.data;
93
97
  }
98
+ async getCapabilities() {
99
+ const raw = await this.json(this.config.capabilityUrl ?? `${API_ROOT}/capabilities`);
100
+ assertCapabilitySafe(raw);
101
+ const parsed = RegistryCapabilitiesSchema.safeParse(raw);
102
+ if (!parsed.success) {
103
+ throw new AiRegistryError(`Capability response is invalid: ${parsed.error.message}`, "ai_registry_invalid_response");
104
+ }
105
+ return parsed.data;
106
+ }
94
107
  async createInstance(input) {
95
108
  return this.instanceResponse(await this.json(`${API_ROOT}/instances`, { method: "POST", body: JSON.stringify(input) }));
96
109
  }
@@ -179,7 +192,8 @@ export class AiRegistryClient {
179
192
  const disposition = response.headers.get("content-disposition") ?? "";
180
193
  const matched = /filename="?([^";]+)"?/iu.exec(disposition)?.[1];
181
194
  const fileName = matched && matched === basename(matched) ? matched : `${safeIdentifier(skillName, "skillName")}.zip`;
182
- return { bytes, fileName, sha256 };
195
+ const version = response.headers.get("x-resource-version")?.trim() || undefined;
196
+ return { bytes, fileName, sha256, ...(version ? { version } : {}) };
183
197
  }
184
198
  async uploadSkill(namespaceId, filePath, options = {}) {
185
199
  const info = await stat(filePath);
@@ -202,7 +216,7 @@ export class AiRegistryClient {
202
216
  const path = `${API_ROOT}/namespaces/${namespaceId}/skills/${encodeURIComponent(safeIdentifier(skillName, "skillName"))}`;
203
217
  if (action === "delete")
204
218
  return this.json(path, { method: "DELETE" });
205
- const method = action === "scope" || action === "biz-tags" ? "PUT" : "POST";
219
+ const method = action === "scope" || action === "biz-tags" || action === "labels" ? "PUT" : "POST";
206
220
  return this.json(`${path}/${action}`, { method, body: JSON.stringify(body) });
207
221
  }
208
222
  async listMcpServers(namespaceId, options = {}) {
@@ -215,10 +229,15 @@ export class AiRegistryClient {
215
229
  query.set("pageSize", String(options.pageSize ?? 20));
216
230
  return this.object(`${API_ROOT}/namespaces/${namespaceId}/mcp-servers?${query}`);
217
231
  }
218
- async getMcpServer(namespaceId, mcpName, version) {
232
+ async getMcpServer(namespaceId, mcpName, versionOrOptions) {
233
+ const options = typeof versionOrOptions === "string" ? { version: versionOrOptions } : versionOrOptions ?? {};
219
234
  const query = new URLSearchParams();
220
- if (version)
221
- query.set("version", version);
235
+ if (options.version && options.label)
236
+ throw new AiRegistryError("Specify version or label, not both.", "ai_registry_input_invalid");
237
+ if (options.version)
238
+ query.set("version", options.version);
239
+ if (options.label)
240
+ query.set("label", options.label);
222
241
  const suffix = query.size ? `?${query}` : "";
223
242
  return this.object(`${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}${suffix}`);
224
243
  }
@@ -235,6 +254,78 @@ export class AiRegistryClient {
235
254
  const suffix = query.size ? `?${query}` : "";
236
255
  return this.json(`${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}${suffix}`, { method: "DELETE" });
237
256
  }
257
+ async mcpAction(namespaceId, mcpName, action, body = {}) {
258
+ const path = `${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}`;
259
+ return this.json(`${path}/${action}`, {
260
+ method: action === "biz-tags" ? "PUT" : "POST",
261
+ body: JSON.stringify(body)
262
+ });
263
+ }
264
+ async setMcpToolEnabled(namespaceId, mcpName, toolName, enabled, version) {
265
+ const path = `${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}/tools/${encodeURIComponent(safeIdentifier(toolName, "toolName"))}`;
266
+ return this.json(path, {
267
+ method: "PUT",
268
+ body: JSON.stringify({ enabled, ...(version == null ? {} : { version }) })
269
+ });
270
+ }
271
+ async createSkillDraft(namespaceId, skillName, body = {}) {
272
+ return this.json(`${API_ROOT}/namespaces/${namespaceId}/skills/${encodeURIComponent(safeIdentifier(skillName, "skillName"))}/drafts`, { method: "POST", body: JSON.stringify(body) });
273
+ }
274
+ async listReleaseRequests(options = {}) {
275
+ const query = new URLSearchParams();
276
+ if (options.namespaceId)
277
+ query.set("namespaceId", String(options.namespaceId));
278
+ if (options.state)
279
+ query.set("state", safeIdentifier(options.state, "state"));
280
+ if (options.cursor)
281
+ query.set("cursor", options.cursor);
282
+ if (options.limit)
283
+ query.set("limit", String(options.limit));
284
+ return this.object(`${API_ROOT}/release-requests${query.size ? `?${query}` : ""}`);
285
+ }
286
+ async createReleaseRequest(body) {
287
+ return this.json(`${API_ROOT}/release-requests`, { method: "POST", body: JSON.stringify(body) });
288
+ }
289
+ async reviewReleaseRequest(requestId, body) {
290
+ return this.json(`${API_ROOT}/release-requests/${encodeURIComponent(safeIdentifier(requestId, "requestId"))}/review`, {
291
+ method: "POST",
292
+ body: JSON.stringify(body)
293
+ });
294
+ }
295
+ async publishReleaseRequest(requestId, body = {}) {
296
+ return this.json(`${API_ROOT}/release-requests/${encodeURIComponent(safeIdentifier(requestId, "requestId"))}/publish`, {
297
+ method: "POST",
298
+ body: JSON.stringify(body)
299
+ });
300
+ }
301
+ async getRouter(namespaceId) {
302
+ const parsed = RegistryRouterSchema.safeParse(await this.json(`${API_ROOT}/namespaces/${namespaceId}/router`));
303
+ if (!parsed.success)
304
+ throw new AiRegistryError(`Router response is invalid: ${parsed.error.message}`, "ai_registry_invalid_response");
305
+ return parsed.data;
306
+ }
307
+ async connectRouter(namespaceId, body = {}) {
308
+ return this.json(`${API_ROOT}/namespaces/${namespaceId}/router/connection`, {
309
+ method: "POST",
310
+ body: JSON.stringify(body)
311
+ });
312
+ }
313
+ async checkRouter(namespaceId) {
314
+ return this.object(`${API_ROOT}/namespaces/${namespaceId}/router/health`);
315
+ }
316
+ async reconcileNamespace(namespaceId) {
317
+ return this.object(`${API_ROOT}/namespaces/${namespaceId}/reconciliation`);
318
+ }
319
+ async listAuditEvents(options = {}) {
320
+ const query = new URLSearchParams();
321
+ if (options.namespaceId)
322
+ query.set("namespaceId", String(options.namespaceId));
323
+ if (options.cursor)
324
+ query.set("cursor", options.cursor);
325
+ if (options.limit)
326
+ query.set("limit", String(options.limit));
327
+ return this.object(`${API_ROOT}/audit-events${query.size ? `?${query}` : ""}`);
328
+ }
238
329
  async object(path) {
239
330
  const parsed = JsonObjectSchema.safeParse(await this.json(path));
240
331
  if (!parsed.success) {
@@ -257,4 +348,17 @@ export class AiRegistryClient {
257
348
  return parsed.data;
258
349
  }
259
350
  }
351
+ function assertCapabilitySafe(value, key = "") {
352
+ if (/(?:credential|password|secret|token|authorization|nacos(?:Base)?Url|baseUrl)/iu.test(key)) {
353
+ throw new AiRegistryError("Capability response contains a forbidden address or credential field.", "ai_registry_capability_unsafe");
354
+ }
355
+ if (Array.isArray(value)) {
356
+ for (const item of value)
357
+ assertCapabilitySafe(item, key);
358
+ }
359
+ else if (value && typeof value === "object") {
360
+ for (const [name, item] of Object.entries(value))
361
+ assertCapabilitySafe(item, name);
362
+ }
363
+ }
260
364
  //# sourceMappingURL=client.js.map