@stofone/dsh-agent-roster 0.10.0 → 0.10.2
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 +31 -1
- package/README.md +6 -1
- package/README.zh.md +33 -6
- package/bin/cli.mjs +12 -0
- package/docs/roster-v2.md +6 -1
- package/index/registry.json +30 -3
- package/lib/compile2.js +108 -0
- package/lib/format.js +1 -0
- package/lib/remote.js +55 -7
- package/package.json +1 -1
- package/schema/registry.v1.schema.json +68 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.2] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Every v2 collection was unfetchable (HTTP 404).** The index assumed the v1 flat layout
|
|
12
|
+
(`<path>/<id>/roster.yaml`), which stopped existing the moment rosters moved to `rosters/<id>/`.
|
|
13
|
+
A collection may now pin one relative path per roster in an optional `files` map;
|
|
14
|
+
`registry sync` fills it in and points v2 rosters at the self-contained `dist/<id>-<ver>.json`
|
|
15
|
+
(an assembly alone cannot be pulled — its members are role references).
|
|
16
|
+
- Pulling a compiled artifact flattens it into an inline v1 document (personas are the assembled
|
|
17
|
+
prompt, which is what would actually be sent); run `migrate` afterwards if you want the role split
|
|
18
|
+
on your machine. A bare `roster@2` is reported as `collection-roster-needs-roles` instead of being
|
|
19
|
+
fetched as if it were one file.
|
|
20
|
+
- `flattenCompiledToRoster` no longer emits empty `title`/`description` strings — v1 rejects those,
|
|
21
|
+
so a flattened artifact used to fail its own validation.
|
|
22
|
+
|
|
23
|
+
## [0.10.1] - 2026-11-03
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
|
|
27
|
+
- The Chinese README said `migrate` was "not implemented", listed one example library where there are
|
|
28
|
+
two, and had a file tree from before `roles/`, `roster.v2` and `compiled.v1` existed — docs that
|
|
29
|
+
describe last week's format read as current, which is worse than no docs.
|
|
30
|
+
- Both READMEs now carry the same two sections: one-command v1→v2 migration (including why role names
|
|
31
|
+
are prefixed and why a colliding captain gets `-captain`), and feeding a failed round back into the
|
|
32
|
+
plan with `compile --run`.
|
|
33
|
+
|
|
7
34
|
## [0.10.0] - 2026-11-03
|
|
8
35
|
|
|
9
36
|
### Roles, rosters and compiled artifacts (v2)
|
|
@@ -445,7 +472,10 @@
|
|
|
445
472
|
- 工具返回值含 `undefined` 键,运行时按 `value is not lossless JSON` 整次拒绝;缺失值统一写 `null`,
|
|
446
473
|
且所有返回值先过 `lib/json.js#lossless`。
|
|
447
474
|
|
|
448
|
-
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.
|
|
475
|
+
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.2...main
|
|
476
|
+
[0.10.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.1...v0.10.2
|
|
477
|
+
[0.10.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.0...v0.10.1
|
|
478
|
+
[0.10.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.10.0
|
|
449
479
|
[0.9.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.2
|
|
450
480
|
[0.9.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.1
|
|
451
481
|
[0.9.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.0
|
package/README.md
CHANGED
|
@@ -383,6 +383,10 @@ repository's `index/registry.json`, and anyone who subscribes can see them in th
|
|
|
383
383
|
- **Pulling**: writes are idempotent and report one of `created`, `updated` (explicit overwrite), `unchanged`
|
|
384
384
|
(same content, nothing written) or `conflict` (the local copy differs and is **not** overwritten by default).
|
|
385
385
|
Conflicts are yours to resolve: merge in the editor, or pull again with `overwrite` in the request body.
|
|
386
|
+
- **Where each roster lives**: a collection's `rosters[]` is just the list of ids; the optional
|
|
387
|
+
`files` map says where each one is (`{"core-review": "dist/core-review-1.0.0.json"}`). It is optional —
|
|
388
|
+
without it the fetcher assumes the v1 layout `<id>/roster.yaml` — but a v2 library needs it, because an
|
|
389
|
+
assembly only references roles and cannot be pulled on its own. `registry sync` writes this map for you.
|
|
386
390
|
- **The index has a schema**: [`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) mirrors the
|
|
387
391
|
runtime validator, so an editor flags a typo before CI does.
|
|
388
392
|
- **Publishing your own**: add an entry to `collections` in `index/registry.json` (`id`, `repo: github:owner/repo`,
|
|
@@ -450,7 +454,8 @@ lib/ format, parse, validate, registry, bridge, verify, render, lib
|
|
|
450
454
|
config-view, json, http, write, service, teams-state, tools
|
|
451
455
|
client.js client plugin: sidebar entry, main panel (rosters/teams/health), settings section
|
|
452
456
|
locale/ panel + listing dictionaries (en/zh)
|
|
453
|
-
schema/ roster /
|
|
457
|
+
schema/ roster v1 / role v1 / roster v2 / compiled v1 / capabilities / organization / registry
|
|
458
|
+
schemas (editor completion; kept aligned by tests)
|
|
454
459
|
bin/cli.mjs validate/list/compile/migrate/registry for CI
|
|
455
460
|
scripts/ host-smoke.mjs, client-smoke.mjs, check-locale.mjs (dev only, not shipped)
|
|
456
461
|
examples/ a v2 library migrated from the v1 archives
|
package/README.zh.md
CHANGED
|
@@ -82,7 +82,7 @@ captain: # 可选:记录队长(=创建团队的那个
|
|
|
82
82
|
|
|
83
83
|
JSON 也可以(`roster.json`)。同一目录下按 `roster.yaml` → `roster.yml` → `roster.json` 顺序探测,**只认一份**;出现第二份会报诊断,绝不静默挑一份。
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
本包自带两份示例档案库:[`examples/`](./examples)(v2,由本仓历史档案迁移而来)与 [`examples-orchestration/`](./examples-orchestration)(v2,手写,用来演示门与施工图)。`rosterDirs` 默认留空,也就是上面那座全局库;
|
|
86
86
|
新装时它是空的,所以面板一开始显示「0 份档案」——在面板里新建一份即可(目录会自动创建)。想先看示例库:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
@@ -227,7 +227,7 @@ rosters/<id>/roster.yaml 组装:成员是引用,队长独立成段
|
|
|
227
227
|
dist/<id>-<ver>.json 编译产物:自包含、可哈希,消费者只看它
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
判别键决定版本——`dsh-agent-roster/role@1`、`dsh-agent-roster/roster@2`、`dsh-agent-roster/compiled@1`。同一目录不允许两种格式共存:v1 冻结,`
|
|
230
|
+
判别键决定版本——`dsh-agent-roster/role@1`、`dsh-agent-roster/roster@2`、`dsh-agent-roster/compiled@1`。同一目录不允许两种格式共存:v1 冻结,`migrate` 负责原地重写。
|
|
231
231
|
|
|
232
232
|
拼装写进格式,而不是藏在代码里:
|
|
233
233
|
|
|
@@ -260,6 +260,28 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review --j
|
|
|
260
260
|
|
|
261
261
|
`orchestration` 是格式的一部分,但编译时标 `status: "declaration"`:阶段与门会被校验(门的 `after` 必须指向真实阶段,`by` 必须指向真实成员),但没有任何东西执行它。能声明不等于已被证明会运行,所以产物在 `declarations[]` 里逐条说明谁属于哪一类。完整契约——字段归宿、拼装语义、身份与唯一性、迁移规则——见 [`docs/roster-v2.md`](./docs/roster-v2.md);schema 见 [`schema/role.v1.schema.json`](./schema/role.v1.schema.json)、[`schema/roster.v2.schema.json`](./schema/roster.v2.schema.json)、[`schema/compiled.v1.schema.json`](./schema/compiled.v1.schema.json)。
|
|
262
262
|
|
|
263
|
+
### 从 v1 到 v2,一条命令
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
node bin/cli.mjs migrate --dir examples # 只出计划:要写哪些文件、能不能编译
|
|
267
|
+
node bin/cli.mjs migrate --dir examples --write # 原地重写:roles/ + rosters/ + dist/,删掉旧目录
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
迁移逐字保留人设:成员的 `systemPrompt` 原文成为 `roles/<档案>-<成员>/AGENT.md` 的正文,成员本身变成引用。队长不再是一个散字段,而是自己的角色文件——与成员同名时得到 `-captain` 后缀,因为那是两份人设,悄悄合并会丢掉一份。
|
|
271
|
+
|
|
272
|
+
角色名带档案 id 前缀,是因为 v2 要求同库角色名唯一:一座库里 21 套组装都叫自己的队长 `lead`,不可能有 21 个同名文件。
|
|
273
|
+
|
|
274
|
+
### 把失败的一轮喂回施工图
|
|
275
|
+
|
|
276
|
+
`compile --run <文件>` 读执行回执(`run@1`),把上一轮的失败并进重试卡的 description:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
280
|
+
--run examples-orchestration/dist/core-review-1.0.0.run.json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
回执由执行施工图的队长会话写,不由插件写:任务 id 属于团队,插件从不写团队状态。插件负责的是合并——只活在红卡上的失败,下一次编译就消失了,门也就变成了装饰。
|
|
284
|
+
|
|
263
285
|
## 工具
|
|
264
286
|
|
|
265
287
|
| 工具 | 作用 |
|
|
@@ -342,6 +364,9 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review --j
|
|
|
342
364
|
冲突要你自己决定:要么在编辑器里合并,要么再点一次「拉取到本机」并在请求里带 `overwrite`。
|
|
343
365
|
- **索引本身也有 schema**:[`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) 与运行时校验器对齐,
|
|
344
366
|
拼错字段在编辑器里就会提示,不必等 CI。
|
|
367
|
+
- **每份档案住在哪**:collection 的 `rosters[]` 只是 id 名单,实际位置由可选的 `files` 映射给出
|
|
368
|
+
(如 `{"core-review": "dist/core-review-1.0.0.json"}`)。不写就按 v1 布局 `<id>/roster.yaml` 猜——v2 的库必须写,
|
|
369
|
+
因为组装本身只引用角色,单独拉一个文件拉不到角色目录。`registry sync` 会替你填。
|
|
345
370
|
- **登记自己的档案**:在 `index/registry.json` 的 `collections` 里加一条(`id`、`repo: github:owner/repo`、
|
|
346
371
|
`ref`、`path`、`rosters[]`),提交 PR 即可;`.github/workflows/registry.yml` 会校验索引与仓库真实文件是否对得上。
|
|
347
372
|
|
|
@@ -405,11 +430,13 @@ lib/ format / parse / validate / registry / bridge / verify / rende
|
|
|
405
430
|
config-view / json / http / write / service / teams-state / tools
|
|
406
431
|
client.js 客户端半边:左侧栏图标 + 主面板(档案/团队/体检)+ 设置页小节
|
|
407
432
|
locale/ 面板与列表词典(en/zh)
|
|
408
|
-
schema/ roster / capabilities / organization / registry
|
|
409
|
-
|
|
433
|
+
schema/ roster v1 / role v1 / roster v2 / compiled v1 / capabilities / organization / registry
|
|
434
|
+
schema(编辑器补全用,与校验器由测试对齐)
|
|
435
|
+
bin/cli.mjs validate / list / compile / migrate / registry,给 CI 用
|
|
410
436
|
scripts/ host-smoke.mjs、client-smoke.mjs、check-locale.mjs(开发用,不随包发布)
|
|
411
|
-
examples/
|
|
412
|
-
|
|
437
|
+
examples/ 由历史 v1 档案迁移来的 v2 库(roles/ + rosters/ + dist/)
|
|
438
|
+
examples-orchestration/ 手写的 v2 库:编排、门与施工图
|
|
439
|
+
orgs/ 带 organization.yaml 的示例库(21 套 v2 档案)
|
|
413
440
|
test/ 单测
|
|
414
441
|
```
|
|
415
442
|
|
package/bin/cli.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { compileRosterAt } from "../lib/compile2.js";
|
|
|
26
26
|
import { migrateLibrary } from "../lib/migrate.js";
|
|
27
27
|
import { compilePlan } from "../lib/plan.js";
|
|
28
28
|
import { mergeRunIntoPlan, validateRun } from "../lib/run.js";
|
|
29
|
+
import { DIST_DIR } from "../lib/migrate.js";
|
|
29
30
|
|
|
30
31
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
31
32
|
|
|
@@ -442,6 +443,17 @@ async function runRegistry(options) {
|
|
|
442
443
|
entry.rosters = ids;
|
|
443
444
|
changed = true;
|
|
444
445
|
}
|
|
446
|
+
const files = {};
|
|
447
|
+
for (const roster of scanned.rosters.slice().sort((a, b) => a.id.localeCompare(b.id))) {
|
|
448
|
+
files[roster.id] =
|
|
449
|
+
roster.format === "v2"
|
|
450
|
+
? `${DIST_DIR}/${roster.id}-${roster.version}.json`
|
|
451
|
+
: `${roster.id}/${roster.file ?? "roster.yaml"}`;
|
|
452
|
+
}
|
|
453
|
+
if (JSON.stringify(files) !== JSON.stringify(entry.files ?? {})) {
|
|
454
|
+
entry.files = files;
|
|
455
|
+
changed = true;
|
|
456
|
+
}
|
|
445
457
|
}
|
|
446
458
|
if (options.write) {
|
|
447
459
|
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
|
package/docs/roster-v2.md
CHANGED
|
@@ -212,9 +212,14 @@ gates:
|
|
|
212
212
|
|
|
213
213
|
三者正交:`requires` 说"必须有",`capabilities.yaml` 说"这份组装必须有",`organization.yaml` 说"这座库有哪些"。
|
|
214
214
|
|
|
215
|
+
远端的 registry 索引也是同理:登记的 `rosters[]` 只是 id 名单,实际位置由 `files` 映射给出。
|
|
216
|
+
v1 的库可以省略(回退成 `<id>/roster.yaml`);v2 的库必须写,**指向 `dist/` 的编译产物**——
|
|
217
|
+
组装文件本身只引用角色,单独拉一个 `roster.yaml` 拉不到角色目录,那不是"没实现",是"拉下来也不完整"。
|
|
218
|
+
拉到编译产物后,客户端把它摊成一份内联 v1 文档(人设就是拼装结果),要角色分层就在本地跑 `migrate`。
|
|
219
|
+
|
|
215
220
|
## 10. 未决与下一步
|
|
216
221
|
|
|
217
222
|
1. **施工图 ↔ 团队真实卡 id 的映射**:`run@1` 已经能读、能合并(`compile --run` 会把上一轮失败写进重试卡),但「施工图内部 id ↔ 团队真实 task id」的映射表还没落成文件——目前靠执行者按 id 命名建卡。
|
|
218
|
-
2. `dist`
|
|
223
|
+
2. `dist` 的发布流程(索引已能逐份指向 `dist/<id>-<ver>.json`,但"从 dist 反推角色版本、做跨库一致性检查"还没做)。
|
|
219
224
|
3. `profile/agent-preset` 目标:把队长与成员的 preset 片段生成出来,仍然只产参不写入。
|
|
220
225
|
4. 编辑器支持:三个 schema 已就位,LSP 级校验待做。
|
package/index/registry.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../schema/registry.v1.schema.json",
|
|
3
3
|
"kind": "dsh-agent-roster-registry/v1",
|
|
4
4
|
"version": 1,
|
|
5
|
-
"generatedAt": "2026-11-
|
|
5
|
+
"generatedAt": "2026-11-03T05:50:01.540Z",
|
|
6
6
|
"generator": "dsh-agent-roster/dsh-agent-roster (hand-maintained; verified by .github/workflows/registry.yml)",
|
|
7
7
|
"source": "https://github.com/dsh-agent-roster/dsh-agent-roster",
|
|
8
8
|
"collections": [
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
"maintainers": [
|
|
27
27
|
"STofone"
|
|
28
28
|
],
|
|
29
|
-
"homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster#examples"
|
|
29
|
+
"homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster#examples",
|
|
30
|
+
"files": {
|
|
31
|
+
"core-review": "dist/core-review-1.0.0.json",
|
|
32
|
+
"research": "dist/research-0.1.0.json"
|
|
33
|
+
}
|
|
30
34
|
},
|
|
31
35
|
{
|
|
32
36
|
"id": "agent-roster-collective",
|
|
@@ -70,7 +74,30 @@
|
|
|
70
74
|
"maintainers": [
|
|
71
75
|
"STofone"
|
|
72
76
|
],
|
|
73
|
-
"homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster/tree/main/orgs/agent-roster-collective"
|
|
77
|
+
"homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster/tree/main/orgs/agent-roster-collective",
|
|
78
|
+
"files": {
|
|
79
|
+
"api-design": "dist/api-design-1.0.0.json",
|
|
80
|
+
"content-ops": "dist/content-ops-1.0.0.json",
|
|
81
|
+
"contract-review": "dist/contract-review-1.0.0.json",
|
|
82
|
+
"data-quality": "dist/data-quality-1.0.0.json",
|
|
83
|
+
"db-migration": "dist/db-migration-1.0.0.json",
|
|
84
|
+
"docs-writer": "dist/docs-writer-1.0.0.json",
|
|
85
|
+
"finance-recon": "dist/finance-recon-1.0.0.json",
|
|
86
|
+
"frontend-ui": "dist/frontend-ui-1.0.0.json",
|
|
87
|
+
"i18n-l10n": "dist/i18n-l10n-1.0.0.json",
|
|
88
|
+
"incident-response": "dist/incident-response-1.0.0.json",
|
|
89
|
+
"literature-review": "dist/literature-review-1.0.0.json",
|
|
90
|
+
"market-research": "dist/market-research-1.0.0.json",
|
|
91
|
+
"ml-eval": "dist/ml-eval-1.0.0.json",
|
|
92
|
+
"performance": "dist/performance-1.0.0.json",
|
|
93
|
+
"product-discovery": "dist/product-discovery-1.0.0.json",
|
|
94
|
+
"qa-regression": "dist/qa-regression-1.0.0.json",
|
|
95
|
+
"recruiting": "dist/recruiting-1.0.0.json",
|
|
96
|
+
"release-manager": "dist/release-manager-1.0.0.json",
|
|
97
|
+
"security-audit": "dist/security-audit-1.0.0.json",
|
|
98
|
+
"sre-oncall": "dist/sre-oncall-1.0.0.json",
|
|
99
|
+
"support-triage": "dist/support-triage-1.0.0.json"
|
|
100
|
+
}
|
|
74
101
|
},
|
|
75
102
|
{
|
|
76
103
|
"id": "agent-teams",
|
package/lib/compile2.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ROLE_PARTS_DIR,
|
|
22
22
|
ROSTER2_FILE_NAMES,
|
|
23
23
|
} from "./format-v2.js";
|
|
24
|
+
import { ROSTER_KIND } from "./format.js";
|
|
24
25
|
import { diagnostic, parseDocumentText } from "./parse.js";
|
|
25
26
|
import { loadRoleAt, safeResolve } from "./role.js";
|
|
26
27
|
import { validateRoster2 } from "./roster2.js";
|
|
@@ -520,3 +521,110 @@ export function rosterViewOf(compiled, roster2, options = {}) {
|
|
|
520
521
|
orchestration: compiled.orchestration ?? null,
|
|
521
522
|
};
|
|
522
523
|
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 校验一份编译产物。
|
|
527
|
+
*
|
|
528
|
+
* 这是「消费者只看产物」的入口:远端订阅拿到的就是它,所以这里只认产物该有的形状,
|
|
529
|
+
* 不回头读角色文件——拉一份档案不该变成拉一个仓库。
|
|
530
|
+
*
|
|
531
|
+
* @param {unknown} value
|
|
532
|
+
* @param {{ file?: string | null }} [options]
|
|
533
|
+
* @returns {{ ok: boolean, compiled: object | null, diagnostics: object[] }}
|
|
534
|
+
*/
|
|
535
|
+
export function validateCompiled(value, { file = null } = {}) {
|
|
536
|
+
const diagnostics = [];
|
|
537
|
+
const at = (relative) => (file === null ? relative : `${file} · ${relative}`);
|
|
538
|
+
const fail = () => ({ ok: false, compiled: null, diagnostics });
|
|
539
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
540
|
+
diagnostics.push({ severity: "error", code: "compiled-not-object", reason: "编译产物必须是一个对象", file });
|
|
541
|
+
return fail();
|
|
542
|
+
}
|
|
543
|
+
if (value.kind !== COMPILED_KIND) {
|
|
544
|
+
diagnostics.push({
|
|
545
|
+
severity: "error",
|
|
546
|
+
code: "compiled-kind",
|
|
547
|
+
reason: `判别键必须是 ${COMPILED_KIND},实际是 ${JSON.stringify(value.kind ?? null)}`,
|
|
548
|
+
file,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
const source = value.source;
|
|
552
|
+
if (source === null || typeof source !== "object") {
|
|
553
|
+
diagnostics.push({ severity: "error", code: "compiled-source", reason: "缺少 source", file });
|
|
554
|
+
} else {
|
|
555
|
+
for (const key of ["roster", "version"]) {
|
|
556
|
+
if (typeof source[key] !== "string" || source[key] === "") {
|
|
557
|
+
diagnostics.push({
|
|
558
|
+
severity: "error",
|
|
559
|
+
code: "compiled-source",
|
|
560
|
+
reason: `source.${key} 必须是非空字符串`,
|
|
561
|
+
file: at("source"),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const members = value.members;
|
|
567
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
568
|
+
diagnostics.push({ severity: "error", code: "compiled-members", reason: "members 必须是非空数组", file });
|
|
569
|
+
} else {
|
|
570
|
+
for (const [index, member] of members.entries()) {
|
|
571
|
+
const where = at(`members[${String(index)}]`);
|
|
572
|
+
if (member === null || typeof member !== "object") {
|
|
573
|
+
diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员必须是对象", file: where });
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (typeof member.name !== "string" || member.name === "") {
|
|
577
|
+
diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员缺少 name", file: where });
|
|
578
|
+
}
|
|
579
|
+
if (typeof member.executionPrompt !== "string" || member.executionPrompt.trim() === "") {
|
|
580
|
+
diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员缺少 executionPrompt", file: where });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const ok = diagnostics.every((item) => item.severity !== "error");
|
|
585
|
+
return { ok, compiled: ok ? value : null, diagnostics };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* 把编译产物摊回一份 v1 档案文档。
|
|
590
|
+
*
|
|
591
|
+
* 远端订阅只拿得到产物:产物里的人设已经是最终拼装结果,所以导入本地时写成 v1 内联档案,
|
|
592
|
+
* 不假装自己带着角色目录。想要角色分层,导入后跑一次 `migrate` 重写即可。
|
|
593
|
+
*
|
|
594
|
+
* @param {object} compiled
|
|
595
|
+
* @returns {object}
|
|
596
|
+
*/
|
|
597
|
+
export function flattenCompiledToRoster(compiled) {
|
|
598
|
+
const source = compiled.source ?? {};
|
|
599
|
+
const members = (compiled.members ?? []).map((member) => {
|
|
600
|
+
const flat = {
|
|
601
|
+
name: member.name,
|
|
602
|
+
role: member.role ?? "",
|
|
603
|
+
systemPrompt: member.executionPrompt,
|
|
604
|
+
};
|
|
605
|
+
if (member.title) flat.title = member.title;
|
|
606
|
+
if (member.description) flat.description = member.description;
|
|
607
|
+
if (member.provider) flat.provider = member.provider;
|
|
608
|
+
if (member.model) flat.model = member.model;
|
|
609
|
+
if (member.reasoningEffort) flat.reasoningEffort = member.reasoningEffort;
|
|
610
|
+
if (member.toolPolicy) flat.toolPolicy = member.toolPolicy;
|
|
611
|
+
return flat;
|
|
612
|
+
});
|
|
613
|
+
const roster = {
|
|
614
|
+
kind: ROSTER_KIND,
|
|
615
|
+
roster: source.roster,
|
|
616
|
+
version: source.version,
|
|
617
|
+
members,
|
|
618
|
+
};
|
|
619
|
+
if (source.title) roster.title = source.title;
|
|
620
|
+
if (source.description) roster.description = source.description;
|
|
621
|
+
if (compiled.captain) {
|
|
622
|
+
const captain = { name: String(compiled.captain.role ?? "").replace(/@[^@]*$/, "") };
|
|
623
|
+
if (compiled.captain.provider) captain.provider = compiled.captain.provider;
|
|
624
|
+
if (compiled.captain.model) captain.model = compiled.captain.model;
|
|
625
|
+
if (compiled.captain.reasoningEffort) captain.reasoningEffort = compiled.captain.reasoningEffort;
|
|
626
|
+
if (compiled.captain.persona) captain.personaPrefix = compiled.captain.persona;
|
|
627
|
+
roster.captain = captain;
|
|
628
|
+
}
|
|
629
|
+
return roster;
|
|
630
|
+
}
|
package/lib/format.js
CHANGED
package/lib/remote.js
CHANGED
|
@@ -23,6 +23,8 @@ import fs from "node:fs";
|
|
|
23
23
|
import nodePath from "node:path";
|
|
24
24
|
import { gunzipSync } from "node:zlib";
|
|
25
25
|
|
|
26
|
+
import { flattenCompiledToRoster, validateCompiled } from "./compile2.js";
|
|
27
|
+
import { COMPILED_KIND, ROSTER2_KIND } from "./format-v2.js";
|
|
26
28
|
import {
|
|
27
29
|
COLLECTION_FIELDS,
|
|
28
30
|
LIMITS,
|
|
@@ -486,6 +488,25 @@ export function validateRegistry(value, meta = {}) {
|
|
|
486
488
|
}
|
|
487
489
|
if (bad) return;
|
|
488
490
|
|
|
491
|
+
const files = {};
|
|
492
|
+
if (entry.files !== undefined) {
|
|
493
|
+
if (entry.files === null || typeof entry.files !== "object" || Array.isArray(entry.files)) {
|
|
494
|
+
diagnostics.push(diagnostic("warn", "collection-files", `${where}.files 必须是「档案 id → 仓库内相对路径」的对象`, file));
|
|
495
|
+
} else {
|
|
496
|
+
for (const [key, value] of Object.entries(entry.files)) {
|
|
497
|
+
if (!rosters.includes(key)) {
|
|
498
|
+
diagnostics.push(diagnostic("warn", "collection-files-unknown", `${where}.files 里的 ${key} 不在 rosters 名单里,已忽略`, file));
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (typeof value !== "string" || value.trim() === "" || !PATTERNS.relPath.test(value) || value.split("/").includes("..")) {
|
|
502
|
+
diagnostics.push(diagnostic("warn", "collection-files-path", `${where}.files.${key} 必须是仓库内相对路径`, file));
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
files[key] = value.trim();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
489
510
|
seen.add(id);
|
|
490
511
|
collections.push({
|
|
491
512
|
id,
|
|
@@ -495,6 +516,7 @@ export function validateRegistry(value, meta = {}) {
|
|
|
495
516
|
ref,
|
|
496
517
|
path: path.replace(/^\/+|\/+$/g, ""),
|
|
497
518
|
rosters,
|
|
519
|
+
files,
|
|
498
520
|
tags: Array.isArray(entry.tags) ? entry.tags.filter((tag) => typeof tag === "string") : [],
|
|
499
521
|
license: typeof entry.license === "string" ? entry.license : "",
|
|
500
522
|
maintainers: Array.isArray(entry.maintainers) ? entry.maintainers.filter((item) => typeof item === "string") : [],
|
|
@@ -526,8 +548,10 @@ export async function fetchCollection(collection, options = {}) {
|
|
|
526
548
|
for (const rosterId of wanted) {
|
|
527
549
|
let loaded = null;
|
|
528
550
|
let lastError = "";
|
|
529
|
-
|
|
530
|
-
|
|
551
|
+
const listedPath = collection.files?.[rosterId] ?? null;
|
|
552
|
+
const candidateNames = listedPath === null ? ["roster.yaml", "roster.yml", "roster.json"] : [null];
|
|
553
|
+
for (const fileName of candidateNames) {
|
|
554
|
+
const url = listedPath === null ? collectionFileUrl(collection, rosterId, fileName) : collectionFileUrl(collection, rosterId, "");
|
|
531
555
|
try {
|
|
532
556
|
const response = await fetchImpl(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
533
557
|
if (!response.ok) {
|
|
@@ -545,15 +569,37 @@ export async function fetchCollection(collection, options = {}) {
|
|
|
545
569
|
continue;
|
|
546
570
|
}
|
|
547
571
|
const parsed = parseDocumentText(loaded.text, { file: loaded.url });
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
572
|
+
if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === ROSTER2_KIND) {
|
|
573
|
+
diagnostics.push(
|
|
574
|
+
diagnostic(
|
|
575
|
+
"warn",
|
|
576
|
+
"collection-roster-needs-roles",
|
|
577
|
+
`${collection.id}/${rosterId} 是 v2 组装,成员是角色引用,单个文件拉不到角色目录;索引应指向 dist/ 里的编译产物`,
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
let checked;
|
|
583
|
+
if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === COMPILED_KIND) {
|
|
584
|
+
const compiled = validateCompiled(parsed.value, { file: loaded.url });
|
|
585
|
+
if (!compiled.ok) {
|
|
586
|
+
diagnostics.push(...compiled.diagnostics);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
checked = validateRoster(flattenCompiledToRoster(compiled.compiled), { file: loaded.url, expectedId: rosterId });
|
|
590
|
+
checked = { ...checked, format: "compiled" };
|
|
591
|
+
} else {
|
|
592
|
+
checked = parsed.ok
|
|
593
|
+
? validateRoster(parsed.value, { file: loaded.url, expectedId: rosterId })
|
|
594
|
+
: { ok: false, diagnostics: parsed.diagnostics };
|
|
595
|
+
}
|
|
551
596
|
if (!checked.ok) {
|
|
552
597
|
diagnostics.push(...checked.diagnostics);
|
|
553
598
|
continue;
|
|
554
599
|
}
|
|
555
600
|
rosters.push({
|
|
556
601
|
...checked.roster,
|
|
602
|
+
...(checked.format === undefined ? {} : { format: checked.format }),
|
|
557
603
|
origin: {
|
|
558
604
|
kind: "remote",
|
|
559
605
|
collection: collection.id,
|
|
@@ -569,10 +615,12 @@ export async function fetchCollection(collection, options = {}) {
|
|
|
569
615
|
return { collection, rosters, diagnostics };
|
|
570
616
|
}
|
|
571
617
|
|
|
572
|
-
/** 拼一份档案的 raw
|
|
618
|
+
/** 拼一份档案的 raw 地址(relativePath 由索引给出,或回退到 v1 的 `<id>/roster.<ext>`)。 */
|
|
573
619
|
export function collectionFileUrl(collection, rosterId, fileName) {
|
|
574
620
|
const repo = collection.repo.replace(/^github:/, "");
|
|
575
|
-
|
|
621
|
+
const relative = collection.files?.[rosterId] ?? `${rosterId}/${fileName}`;
|
|
622
|
+
const base = collection.path === "" ? "" : `${collection.path}/`;
|
|
623
|
+
return `${GITHUB_RAW}/${repo}/${encodeURIComponent(collection.ref)}/${base}${relative}`;
|
|
576
624
|
}
|
|
577
625
|
|
|
578
626
|
/** 一次读多个源(串行:GitHub 未认证配额经不起并发烧)。 */
|
package/package.json
CHANGED
|
@@ -5,32 +5,57 @@
|
|
|
5
5
|
"description": "公共档案索引:登记若干 collection(仓库 + 目录 + 确有哪几份档案),供订阅与一键安装。权威校验实现是 lib/remote.js#validateRegistry;本文件供编辑器与外部工具提示,两者由 test/registry-schema.test.js 对齐。注意:运行时对未知字段只告警不判错,这里写成 additionalProperties:false 是为了让编辑器早点指出拼写错误。",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
-
"required": [
|
|
8
|
+
"required": [
|
|
9
|
+
"kind",
|
|
10
|
+
"collections"
|
|
11
|
+
],
|
|
9
12
|
"properties": {
|
|
10
13
|
"$schema": {
|
|
11
14
|
"type": "string",
|
|
12
15
|
"description": "编辑器用的 schema 地址,插件不读它。"
|
|
13
16
|
},
|
|
14
|
-
"kind": {
|
|
15
|
-
|
|
17
|
+
"kind": {
|
|
18
|
+
"const": "dsh-agent-roster-registry/v1"
|
|
19
|
+
},
|
|
20
|
+
"version": {
|
|
21
|
+
"type": "integer",
|
|
22
|
+
"minimum": 1
|
|
23
|
+
},
|
|
16
24
|
"generatedAt": {
|
|
17
25
|
"type": "string",
|
|
18
26
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$",
|
|
19
27
|
"description": "上一次校验通过的时间,由 CI 或维护者盖章。"
|
|
20
28
|
},
|
|
21
|
-
"generator": {
|
|
22
|
-
|
|
29
|
+
"generator": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"maxLength": 200
|
|
32
|
+
},
|
|
33
|
+
"source": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"maxLength": 200
|
|
36
|
+
},
|
|
23
37
|
"collections": {
|
|
24
38
|
"type": "array",
|
|
25
39
|
"maxItems": 500,
|
|
26
40
|
"items": {
|
|
27
41
|
"type": "object",
|
|
28
42
|
"additionalProperties": false,
|
|
29
|
-
"required": [
|
|
43
|
+
"required": [
|
|
44
|
+
"id",
|
|
45
|
+
"repo"
|
|
46
|
+
],
|
|
30
47
|
"properties": {
|
|
31
|
-
"id": {
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
"id": {
|
|
49
|
+
"$ref": "#/$defs/id"
|
|
50
|
+
},
|
|
51
|
+
"title": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"maxLength": 120
|
|
54
|
+
},
|
|
55
|
+
"description": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"maxLength": 500
|
|
58
|
+
},
|
|
34
59
|
"repo": {
|
|
35
60
|
"type": "string",
|
|
36
61
|
"pattern": "^(?:github:)?[A-Za-z0-9][A-Za-z0-9_.-]{0,38}/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$",
|
|
@@ -52,7 +77,9 @@
|
|
|
52
77
|
"type": "array",
|
|
53
78
|
"maxItems": 200,
|
|
54
79
|
"description": "目录里确实存在的档案 id。索引不撒谎:不存在的 id 会被 registry sync 剔除。",
|
|
55
|
-
"items": {
|
|
80
|
+
"items": {
|
|
81
|
+
"$ref": "#/$defs/id"
|
|
82
|
+
}
|
|
56
83
|
},
|
|
57
84
|
"tags": {
|
|
58
85
|
"type": "array",
|
|
@@ -62,13 +89,34 @@
|
|
|
62
89
|
"pattern": "^[a-z0-9][a-z0-9-]{0,31}$"
|
|
63
90
|
}
|
|
64
91
|
},
|
|
65
|
-
"license": {
|
|
92
|
+
"license": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"maxLength": 120
|
|
95
|
+
},
|
|
66
96
|
"maintainers": {
|
|
67
97
|
"type": "array",
|
|
68
98
|
"maxItems": 16,
|
|
69
|
-
"items": {
|
|
99
|
+
"items": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"minLength": 1,
|
|
102
|
+
"maxLength": 120
|
|
103
|
+
}
|
|
70
104
|
},
|
|
71
|
-
"homepage": {
|
|
105
|
+
"homepage": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"maxLength": 200,
|
|
108
|
+
"pattern": "^https?://\\S+$"
|
|
109
|
+
},
|
|
110
|
+
"files": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"description": "档案 id → 仓库内相对路径。v2 档案指向 dist/ 的编译产物(自包含),v1 可省略。",
|
|
113
|
+
"propertyNames": {
|
|
114
|
+
"$ref": "#/$defs/id"
|
|
115
|
+
},
|
|
116
|
+
"additionalProperties": {
|
|
117
|
+
"$ref": "#/$defs/relPath"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
72
120
|
}
|
|
73
121
|
}
|
|
74
122
|
}
|
|
@@ -78,6 +126,13 @@
|
|
|
78
126
|
"type": "string",
|
|
79
127
|
"pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
|
|
80
128
|
"description": "kebab-case 标识,与档案目录名同规则。"
|
|
129
|
+
},
|
|
130
|
+
"relPath": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"minLength": 1,
|
|
133
|
+
"maxLength": 200,
|
|
134
|
+
"pattern": "^(?:[A-Za-z0-9._-]+/)*[A-Za-z0-9._-]+$",
|
|
135
|
+
"description": "仓库内相对路径,不含 ..、不以 / 开头。"
|
|
81
136
|
}
|
|
82
137
|
}
|
|
83
138
|
}
|