@stofone/dsh-agent-roster 0.10.1 → 0.10.3
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 +39 -1
- package/README.md +17 -1
- package/README.zh.md +14 -1
- package/bin/cli.mjs +36 -2
- package/docs/roster-v2.md +8 -1
- package/index/registry.json +45 -9
- package/lib/compile2.js +108 -0
- package/lib/format.js +1 -0
- package/lib/migrate.js +14 -5
- package/lib/remote.js +58 -9
- package/package.json +1 -1
- package/schema/registry.v1.schema.json +69 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.3] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `migrate --from <dir>` and `--repo <owner/repo>`: a repository that publishes a collection keeps its
|
|
12
|
+
v1 rosters in `rosters/<id>/`, so the library root and the old files are not the same directory. The
|
|
13
|
+
migration now rewrites that file in place, adds `roles/` and `dist/` at the root, removes the stale
|
|
14
|
+
`roster.json`/`roster.yml` beside it (two spellings of one roster are two sources of truth), and does
|
|
15
|
+
**not** delete `rosters/<id>/` — that is where the new assembly lives.
|
|
16
|
+
- A collection's `path` may be empty, meaning the repository root *is* the library. The three example
|
|
17
|
+
repositories (`agent-teams`, `metagpt-company`, `openmanus`) now use that shape: `roles/`, `rosters/`,
|
|
18
|
+
`dist/` at the root, `files` pointing at the compiled artifact.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- The `agent-teams`, `metagpt-company` and `openmanus` collections were still published as v1, so their
|
|
23
|
+
`roster.yaml` was fetched while a v2 world had already moved on; every entry now names its artifact.
|
|
24
|
+
|
|
25
|
+
## [0.10.2] - 2026-11-03
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Every v2 collection was unfetchable (HTTP 404).** The index assumed the v1 flat layout
|
|
30
|
+
(`<path>/<id>/roster.yaml`), which stopped existing the moment rosters moved to `rosters/<id>/`.
|
|
31
|
+
A collection may now pin one relative path per roster in an optional `files` map;
|
|
32
|
+
`registry sync` fills it in and points v2 rosters at the self-contained `dist/<id>-<ver>.json`
|
|
33
|
+
(an assembly alone cannot be pulled — its members are role references).
|
|
34
|
+
- Pulling a compiled artifact flattens it into an inline v1 document (personas are the assembled
|
|
35
|
+
prompt, which is what would actually be sent); run `migrate` afterwards if you want the role split
|
|
36
|
+
on your machine. A bare `roster@2` is reported as `collection-roster-needs-roles` instead of being
|
|
37
|
+
fetched as if it were one file.
|
|
38
|
+
- `flattenCompiledToRoster` no longer emits empty `title`/`description` strings — v1 rejects those,
|
|
39
|
+
so a flattened artifact used to fail its own validation.
|
|
40
|
+
|
|
7
41
|
## [0.10.1] - 2026-11-03
|
|
8
42
|
|
|
9
43
|
### Docs
|
|
@@ -456,7 +490,11 @@
|
|
|
456
490
|
- 工具返回值含 `undefined` 键,运行时按 `value is not lossless JSON` 整次拒绝;缺失值统一写 `null`,
|
|
457
491
|
且所有返回值先过 `lib/json.js#lossless`。
|
|
458
492
|
|
|
459
|
-
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.
|
|
493
|
+
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.3...main
|
|
494
|
+
[0.10.3]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.2...v0.10.3
|
|
495
|
+
[0.10.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.1...v0.10.2
|
|
496
|
+
[0.10.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.0...v0.10.1
|
|
497
|
+
[0.10.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.10.0
|
|
460
498
|
[0.9.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.2
|
|
461
499
|
[0.9.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.1
|
|
462
500
|
[0.9.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.0
|
package/README.md
CHANGED
|
@@ -276,6 +276,18 @@ Migration keeps personas verbatim: a member's `systemPrompt` becomes the body of
|
|
|
276
276
|
loose field and becomes a role file of its own — `<roster>-<captain>-captain` when its name collides
|
|
277
277
|
with a member, because those are two different personas and silently merging them would lose one.
|
|
278
278
|
|
|
279
|
+
If the v1 rosters live in a subdirectory (the usual case for a repository that publishes a
|
|
280
|
+
collection), point the migration at the library root and tell it where the old files are:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
node bin/cli.mjs migrate --dir . --from rosters --repo owner/repo --write
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
That layout rewrites `rosters/<id>/roster.yaml` in place, adds `roles/` and `dist/` at the repository
|
|
287
|
+
root, and drops the stale `roster.json`/`roster.yml` next to it — two spellings of one roster are two
|
|
288
|
+
sources of truth. The collection then uses `path: ""` (the repository root *is* the library) and a
|
|
289
|
+
`files` map pointing at `dist/`, because an assembly only references roles and cannot be pulled alone.
|
|
290
|
+
|
|
279
291
|
Role names carry the roster id prefix because v2 requires role names to be **unique per library**,
|
|
280
292
|
and a library whose 21 rosters all call their captain `lead` cannot have 21 files with the same name.
|
|
281
293
|
|
|
@@ -383,6 +395,10 @@ repository's `index/registry.json`, and anyone who subscribes can see them in th
|
|
|
383
395
|
- **Pulling**: writes are idempotent and report one of `created`, `updated` (explicit overwrite), `unchanged`
|
|
384
396
|
(same content, nothing written) or `conflict` (the local copy differs and is **not** overwritten by default).
|
|
385
397
|
Conflicts are yours to resolve: merge in the editor, or pull again with `overwrite` in the request body.
|
|
398
|
+
- **Where each roster lives**: a collection's `rosters[]` is just the list of ids; the optional
|
|
399
|
+
`files` map says where each one is (`{"core-review": "dist/core-review-1.0.0.json"}`). It is optional —
|
|
400
|
+
without it the fetcher assumes the v1 layout `<id>/roster.yaml` — but a v2 library needs it, because an
|
|
401
|
+
assembly only references roles and cannot be pulled on its own. `registry sync` writes this map for you.
|
|
386
402
|
- **The index has a schema**: [`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) mirrors the
|
|
387
403
|
runtime validator, so an editor flags a typo before CI does.
|
|
388
404
|
- **Publishing your own**: add an entry to `collections` in `index/registry.json` (`id`, `repo: github:owner/repo`,
|
|
@@ -464,7 +480,7 @@ test/ unit tests
|
|
|
464
480
|
|
|
465
481
|
Verified on this machine:
|
|
466
482
|
|
|
467
|
-
- `npm test` —
|
|
483
|
+
- `npm test` — 293/293 passing (format, parse, validate, registry, paths, bridge, verify buckets, digests and provenance, roster diff, capability requirements and the capability gate, library organization, registry schema, lossless JSON, write plan/apply, all routes, panel snapshot and hints, dictionary consistency, tool contracts, client loader contract, library snapshot, config view, package manifest, declared fields and captain block, schema/constant alignment, the v2 role/roster/compiled layers and their schema alignment, v1→v2 migration, run records feeding the plan, remote registry and one-click install).
|
|
468
484
|
- `npm run smoke` — `apply()` registers all six tools through a real `defineTool` from a DSH bundle; `roster_list` / `roster_show` / `roster_create` / `roster_verify` / `roster_diff` execute, every output survives `lossless` checks, `path` overrides are honored, effect disposal runs cleanly. The same command then loads `client.js` against a stubbed module table and asserts the three slot registrations, style injection and effect disposal.
|
|
469
485
|
- CLI: `validate --dir examples` returns exit 0 over both example libraries.
|
|
470
486
|
- Installed into a real profile with the plugin manager (`plugin_manager install_bundle` against this directory → `link:` dependency + bundle entry); the profile re-reads the linked files on restart.
|
package/README.zh.md
CHANGED
|
@@ -269,6 +269,16 @@ node bin/cli.mjs migrate --dir examples --write # 原地重写:roles/ + ros
|
|
|
269
269
|
|
|
270
270
|
迁移逐字保留人设:成员的 `systemPrompt` 原文成为 `roles/<档案>-<成员>/AGENT.md` 的正文,成员本身变成引用。队长不再是一个散字段,而是自己的角色文件——与成员同名时得到 `-captain` 后缀,因为那是两份人设,悄悄合并会丢掉一份。
|
|
271
271
|
|
|
272
|
+
如果 v1 的档案在子目录里(把仓库当 collection 发布的常见形态),把 `--dir` 指到库根、用 `--from` 指旧目录:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
node bin/cli.mjs migrate --dir . --from rosters --repo owner/repo --write
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
这种布局会就地重写 `rosters/<id>/roster.yaml`,在仓库根上补 `roles/` 与 `dist/`,并删掉并排的旧
|
|
279
|
+
`roster.json`/`roster.yml`——同一份档案两种写法就是两个真源。之后 collection 用 `path: ""`(仓库根就是库),
|
|
280
|
+
再用 `files` 指向 `dist/`:组装只引用角色,单独拉一个文件拉不到角色目录。
|
|
281
|
+
|
|
272
282
|
角色名带档案 id 前缀,是因为 v2 要求同库角色名唯一:一座库里 21 套组装都叫自己的队长 `lead`,不可能有 21 个同名文件。
|
|
273
283
|
|
|
274
284
|
### 把失败的一轮喂回施工图
|
|
@@ -364,6 +374,9 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
364
374
|
冲突要你自己决定:要么在编辑器里合并,要么再点一次「拉取到本机」并在请求里带 `overwrite`。
|
|
365
375
|
- **索引本身也有 schema**:[`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) 与运行时校验器对齐,
|
|
366
376
|
拼错字段在编辑器里就会提示,不必等 CI。
|
|
377
|
+
- **每份档案住在哪**:collection 的 `rosters[]` 只是 id 名单,实际位置由可选的 `files` 映射给出
|
|
378
|
+
(如 `{"core-review": "dist/core-review-1.0.0.json"}`)。不写就按 v1 布局 `<id>/roster.yaml` 猜——v2 的库必须写,
|
|
379
|
+
因为组装本身只引用角色,单独拉一个文件拉不到角色目录。`registry sync` 会替你填。
|
|
367
380
|
- **登记自己的档案**:在 `index/registry.json` 的 `collections` 里加一条(`id`、`repo: github:owner/repo`、
|
|
368
381
|
`ref`、`path`、`rosters[]`),提交 PR 即可;`.github/workflows/registry.yml` 会校验索引与仓库真实文件是否对得上。
|
|
369
382
|
|
|
@@ -441,7 +454,7 @@ test/ 单测
|
|
|
441
454
|
|
|
442
455
|
本机已验证:
|
|
443
456
|
|
|
444
|
-
- `npm test` ——
|
|
457
|
+
- `npm test` —— 293/293 通过(格式、解析、校验、扫描、路径、桥接、核对四桶、摘要与溯源、档案差异、能力要求与能力闸门、档案库组织、索引 schema、无损 JSON、写入计划与执行、六条路由、快照与提示、两本词典一致、工具契约、客户端加载器契约、包清单、配置视图、样式 token 约束、声明层字段与队长段、schema 与常量对齐、v2 角色/组装/编译三层及其 schema 对齐、v1→v2 迁移、执行回执并入施工图)。
|
|
445
458
|
- `npm run smoke` —— 用真实 DSH bundle 的 `defineTool` 注册六个工具,逐个调用:输出全部通过无损校验、`path` 覆盖生效、释放干净;同一条命令随后用桩模块表加载 `client.js`,断言三处槽位注册、样式只注入一次、effect 可回收。
|
|
446
459
|
- CLI:`validate --dir examples` 对两座示例库退出码 0。
|
|
447
460
|
- 用插件管理器真的装进了本机 profile(`plugin_manager install_bundle` 指向本仓库 → `link:` 依赖 + bundle 条目),重启后按链接读取本仓库文件。
|
package/bin/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* dsh-agent-roster registry sync --file index/registry.json [--write]
|
|
14
14
|
* dsh-agent-roster compile --roster rosters/money-lab [--roles roles] [--run run.json] [--write] [--json]
|
|
15
15
|
* dsh-agent-roster migrate --dir examples [--write] [--json]
|
|
16
|
+
* dsh-agent-roster migrate --dir . --from rosters --repo owner/repo --write
|
|
16
17
|
*/
|
|
17
18
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
19
|
import nodePath from "node:path";
|
|
@@ -26,6 +27,7 @@ import { compileRosterAt } from "../lib/compile2.js";
|
|
|
26
27
|
import { migrateLibrary } from "../lib/migrate.js";
|
|
27
28
|
import { compilePlan } from "../lib/plan.js";
|
|
28
29
|
import { mergeRunIntoPlan, validateRun } from "../lib/run.js";
|
|
30
|
+
import { DIST_DIR } from "../lib/migrate.js";
|
|
29
31
|
|
|
30
32
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
31
33
|
|
|
@@ -36,7 +38,8 @@ const USAGE = `用法:
|
|
|
36
38
|
dsh-agent-roster registry fetch --file <路径> 逐条确认 collection 真的能取到
|
|
37
39
|
dsh-agent-roster registry sync --file <路径> 按仓库实际内容重算 roster 列表
|
|
38
40
|
dsh-agent-roster compile --roster <目录> 把 v2 组装编译成自包含产物(不写盘)
|
|
39
|
-
dsh-agent-roster migrate --dir <目录>
|
|
41
|
+
dsh-agent-roster migrate --dir <目录> [--from <子目录>] [--repo <owner/repo>] [--write]
|
|
42
|
+
把 v1 档案原地迁成 v2(roles/ + rosters/ + dist/)
|
|
40
43
|
dsh-agent-roster --version 输出版本号
|
|
41
44
|
|
|
42
45
|
选项:
|
|
@@ -46,6 +49,8 @@ const USAGE = `用法:
|
|
|
46
49
|
--local-only fetch 只验本仓库登记的 collection(CI 里用,不依赖外网)
|
|
47
50
|
--write sync 把重算结果写回文件
|
|
48
51
|
--roster <path> v2 组装目录(compile 用)
|
|
52
|
+
--from <path> v1 档案所在目录(migrate 用;缺省与 --dir 相同,用于仓库根 + rosters/ 布局)
|
|
53
|
+
--repo <owner/repo> 写进 dist 产物的仓库坐标(migrate 用;缺省取本仓 remote)
|
|
49
54
|
--run <path> 执行回执(compile 用;把上一轮失败并入重试卡)
|
|
50
55
|
--roles <path> 角色库目录(compile 用;缺省按 rosters/<id> 推同级的 roles/)
|
|
51
56
|
--json 以 JSON 输出(validate / list / compile 都支持)
|
|
@@ -67,6 +72,8 @@ function parseArgs(argv) {
|
|
|
67
72
|
roster: null,
|
|
68
73
|
roles: null,
|
|
69
74
|
run: null,
|
|
75
|
+
from: null,
|
|
76
|
+
repo: null,
|
|
70
77
|
};
|
|
71
78
|
for (let index = 0; index < argv.length; index += 1) {
|
|
72
79
|
const arg = argv[index];
|
|
@@ -90,6 +97,16 @@ function parseArgs(argv) {
|
|
|
90
97
|
if (value === undefined) throw new UsageError("--roster 需要一个路径");
|
|
91
98
|
options.roster = value;
|
|
92
99
|
index += 1;
|
|
100
|
+
} else if (arg === "--from") {
|
|
101
|
+
const value = argv[index + 1];
|
|
102
|
+
if (value === undefined) throw new UsageError("--from 需要一个路径");
|
|
103
|
+
options.from = value;
|
|
104
|
+
index += 1;
|
|
105
|
+
} else if (arg === "--repo") {
|
|
106
|
+
const value = argv[index + 1];
|
|
107
|
+
if (value === undefined) throw new UsageError("--repo 需要 owner/name");
|
|
108
|
+
options.repo = value;
|
|
109
|
+
index += 1;
|
|
93
110
|
} else if (arg === "--run") {
|
|
94
111
|
const value = argv[index + 1];
|
|
95
112
|
if (value === undefined) throw new UsageError("--run 需要一个路径");
|
|
@@ -442,6 +459,17 @@ async function runRegistry(options) {
|
|
|
442
459
|
entry.rosters = ids;
|
|
443
460
|
changed = true;
|
|
444
461
|
}
|
|
462
|
+
const files = {};
|
|
463
|
+
for (const roster of scanned.rosters.slice().sort((a, b) => a.id.localeCompare(b.id))) {
|
|
464
|
+
files[roster.id] =
|
|
465
|
+
roster.format === "v2"
|
|
466
|
+
? `${DIST_DIR}/${roster.id}-${roster.version}.json`
|
|
467
|
+
: `${roster.id}/${roster.file ?? "roster.yaml"}`;
|
|
468
|
+
}
|
|
469
|
+
if (JSON.stringify(files) !== JSON.stringify(entry.files ?? {})) {
|
|
470
|
+
entry.files = files;
|
|
471
|
+
changed = true;
|
|
472
|
+
}
|
|
445
473
|
}
|
|
446
474
|
if (options.write) {
|
|
447
475
|
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
|
|
@@ -508,7 +536,13 @@ function runMigrate(options) {
|
|
|
508
536
|
const reports = [];
|
|
509
537
|
for (const dir of dirs) {
|
|
510
538
|
const rootDir = resolveDirSpec(dir, process.cwd());
|
|
511
|
-
const
|
|
539
|
+
const fromDir = options.from === null ? rootDir : resolveDirSpec(options.from, process.cwd());
|
|
540
|
+
const report = migrateLibrary(rootDir, {
|
|
541
|
+
write: options.write,
|
|
542
|
+
knownToolPolicies,
|
|
543
|
+
source: { repo: options.repo ?? ownRepo(), ref: "main" },
|
|
544
|
+
fromDir,
|
|
545
|
+
});
|
|
512
546
|
reports.push({ dir, rootDir, report });
|
|
513
547
|
if (report.diagnostics.some((item) => item.severity === "error")) rc = 1;
|
|
514
548
|
if (options.json) continue;
|
package/docs/roster-v2.md
CHANGED
|
@@ -197,6 +197,8 @@ gates:
|
|
|
197
197
|
## 8. 迁移
|
|
198
198
|
|
|
199
199
|
- `roster migrate --dir <库> [--write]` 读 v1 档案,把 `systemPrompt` 拆成角色(`roles/<member>/AGENT.md`)+ 组装(成员改为引用),原地重写并生成 `dist/`。默认只出计划,`--write` 才落盘——这是唯一会删旧目录的命令。
|
|
200
|
+
- `roster migrate --dir <库根> --from <旧目录> [--repo <owner/repo>] [--write]`:v1 档案放在子目录(`rosters/<id>/roster.yaml`,即"仓库本身就是库、只是档案隔了一层")时用这条。它就地重写 `rosters/<id>/roster.yaml`、在库根补 `roles/` 与 `dist/`、删掉并排的 `roster.json`/`roster.yml`(同义文件=第二真源),**不删** `rosters/<id>/` —— 那个目录就是新档案的落脚处。`--repo` 写进产物的 `source.repo`,避免发布仓的产物署错名。
|
|
201
|
+
- **collection 的 `path` 可以是空字符串**,表示仓库根就是库:`roles/`、`rosters/`、`dist/` 直接躺在根上。配合 `files` 映射指向 `dist/<id>-<ver>.json`。
|
|
200
202
|
- **角色名带档案 id 前缀**(`core-review-reviewer`):v2 要求同库角色名唯一,而真实档案库里几十套组装都可能叫 `lead`。队长与成员同名时,队长得到 `-captain` 后缀,不与人设合并。
|
|
201
203
|
- `maintainers` 用 `handle` 模式(允许大写与点),不复用档案 id 的小写短横线规则——维护者是账号,不是标识符。
|
|
202
204
|
- v1 冻结:新字段只进 v2,不再回填 v1。
|
|
@@ -212,9 +214,14 @@ gates:
|
|
|
212
214
|
|
|
213
215
|
三者正交:`requires` 说"必须有",`capabilities.yaml` 说"这份组装必须有",`organization.yaml` 说"这座库有哪些"。
|
|
214
216
|
|
|
217
|
+
远端的 registry 索引也是同理:登记的 `rosters[]` 只是 id 名单,实际位置由 `files` 映射给出。
|
|
218
|
+
v1 的库可以省略(回退成 `<id>/roster.yaml`);v2 的库必须写,**指向 `dist/` 的编译产物**——
|
|
219
|
+
组装文件本身只引用角色,单独拉一个 `roster.yaml` 拉不到角色目录,那不是"没实现",是"拉下来也不完整"。
|
|
220
|
+
拉到编译产物后,客户端把它摊成一份内联 v1 文档(人设就是拼装结果),要角色分层就在本地跑 `migrate`。
|
|
221
|
+
|
|
215
222
|
## 10. 未决与下一步
|
|
216
223
|
|
|
217
224
|
1. **施工图 ↔ 团队真实卡 id 的映射**:`run@1` 已经能读、能合并(`compile --run` 会把上一轮失败写进重试卡),但「施工图内部 id ↔ 团队真实 task id」的映射表还没落成文件——目前靠执行者按 id 命名建卡。
|
|
218
|
-
2. `dist`
|
|
225
|
+
2. `dist` 的发布流程(索引已能逐份指向 `dist/<id>-<ver>.json`,但"从 dist 反推角色版本、做跨库一致性检查"还没做)。
|
|
219
226
|
3. `profile/agent-preset` 目标:把队长与成员的 preset 片段生成出来,仍然只产参不写入。
|
|
220
227
|
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-03T06:14:00.000Z",
|
|
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",
|
|
@@ -78,7 +105,7 @@
|
|
|
78
105
|
"description": "把 wshobson/agents 的 agent-teams 插件(4.2 万星,MIT)迁成档案:队长、实现者、审查者、调试者,保留上游工具边界。",
|
|
79
106
|
"repo": "github:dsh-agent-roster/agent-teams",
|
|
80
107
|
"ref": "main",
|
|
81
|
-
"path": "
|
|
108
|
+
"path": "",
|
|
82
109
|
"rosters": [
|
|
83
110
|
"agent-teams"
|
|
84
111
|
],
|
|
@@ -92,7 +119,10 @@
|
|
|
92
119
|
"maintainers": [
|
|
93
120
|
"STofone"
|
|
94
121
|
],
|
|
95
|
-
"homepage": "https://github.com/dsh-agent-roster/agent-teams"
|
|
122
|
+
"homepage": "https://github.com/dsh-agent-roster/agent-teams",
|
|
123
|
+
"files": {
|
|
124
|
+
"agent-teams": "dist/agent-teams-1.0.0.json"
|
|
125
|
+
}
|
|
96
126
|
},
|
|
97
127
|
{
|
|
98
128
|
"id": "metagpt-company",
|
|
@@ -100,7 +130,7 @@
|
|
|
100
130
|
"description": "MetaGPT(5 万星+,MIT)的软件公司角色:产品经理、架构师、项目经理、工程师,字段取自上游 Role 的 profile/goal/constraints。",
|
|
101
131
|
"repo": "github:dsh-agent-roster/metagpt-company",
|
|
102
132
|
"ref": "main",
|
|
103
|
-
"path": "
|
|
133
|
+
"path": "",
|
|
104
134
|
"rosters": [
|
|
105
135
|
"metagpt-company"
|
|
106
136
|
],
|
|
@@ -113,7 +143,10 @@
|
|
|
113
143
|
"maintainers": [
|
|
114
144
|
"STofone"
|
|
115
145
|
],
|
|
116
|
-
"homepage": "https://github.com/dsh-agent-roster/metagpt-company"
|
|
146
|
+
"homepage": "https://github.com/dsh-agent-roster/metagpt-company",
|
|
147
|
+
"files": {
|
|
148
|
+
"metagpt-company": "dist/metagpt-company-1.0.0.json"
|
|
149
|
+
}
|
|
117
150
|
},
|
|
118
151
|
{
|
|
119
152
|
"id": "openmanus",
|
|
@@ -121,7 +154,7 @@
|
|
|
121
154
|
"description": "OpenManus(5.8 万星,MIT)的三个代理提示词:规划、编码、浏览器,systemPrompt 保留上游原文。",
|
|
122
155
|
"repo": "github:dsh-agent-roster/openmanus",
|
|
123
156
|
"ref": "main",
|
|
124
|
-
"path": "
|
|
157
|
+
"path": "",
|
|
125
158
|
"rosters": [
|
|
126
159
|
"openmanus"
|
|
127
160
|
],
|
|
@@ -135,7 +168,10 @@
|
|
|
135
168
|
"maintainers": [
|
|
136
169
|
"STofone"
|
|
137
170
|
],
|
|
138
|
-
"homepage": "https://github.com/dsh-agent-roster/openmanus"
|
|
171
|
+
"homepage": "https://github.com/dsh-agent-roster/openmanus",
|
|
172
|
+
"files": {
|
|
173
|
+
"openmanus": "dist/openmanus-1.0.0.json"
|
|
174
|
+
}
|
|
139
175
|
}
|
|
140
176
|
]
|
|
141
177
|
}
|
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/migrate.js
CHANGED
|
@@ -206,11 +206,11 @@ function loadCapabilitiesOf(dir) {
|
|
|
206
206
|
*
|
|
207
207
|
* 不写盘时只返回计划;`write: true` 才落盘、编译、写出 `dist/`,并在最后删掉旧的 `<id>/` 目录。
|
|
208
208
|
*/
|
|
209
|
-
export function migrateLibrary(rootDir, { write = false, knownToolPolicies = null, source = {} } = {}) {
|
|
209
|
+
export function migrateLibrary(rootDir, { write = false, knownToolPolicies = null, source = {}, fromDir = rootDir } = {}) {
|
|
210
210
|
const report = { rootDir, write, entries: [], diagnostics: [], written: [], removed: [], compiled: [] };
|
|
211
211
|
let children = [];
|
|
212
212
|
try {
|
|
213
|
-
children = readdirSync(
|
|
213
|
+
children = readdirSync(fromDir, { withFileTypes: true });
|
|
214
214
|
} catch (error) {
|
|
215
215
|
report.diagnostics.push({ severity: "error", code: "unreadable-dir", path: "", file: null, reason: `档案库不可读:${error?.message ?? error}` });
|
|
216
216
|
return report;
|
|
@@ -220,7 +220,7 @@ export function migrateLibrary(rootDir, { write = false, knownToolPolicies = nul
|
|
|
220
220
|
for (const child of children) {
|
|
221
221
|
if (!child.isDirectory() || child.isSymbolicLink()) continue;
|
|
222
222
|
if (child.name.startsWith(".") || child.name === ROLES_DIR || child.name === ROSTERS_DIR || child.name === DIST_DIR) continue;
|
|
223
|
-
const dir = nodePath.join(
|
|
223
|
+
const dir = nodePath.join(fromDir, child.name);
|
|
224
224
|
// v1 的档案文件名可以是 yaml / yml / json——迁移只认内容,不认扩展名。
|
|
225
225
|
if (!MEMBER_FILE_NAMES.some((name) => existsSync(nodePath.join(dir, name)))) continue;
|
|
226
226
|
const loaded = loadRosterAt(dir, { knownToolPolicies });
|
|
@@ -269,7 +269,7 @@ export function migrateLibrary(rootDir, { write = false, knownToolPolicies = nul
|
|
|
269
269
|
report.written.push(file.path);
|
|
270
270
|
}
|
|
271
271
|
for (const name of CAPABILITIES_FILE_NAMES) {
|
|
272
|
-
const source = nodePath.join(
|
|
272
|
+
const source = nodePath.join(fromDir, entry.id, name);
|
|
273
273
|
if (!existsSync(source)) continue;
|
|
274
274
|
const relative = nodePath.join(ROSTERS_DIR, entry.id, name);
|
|
275
275
|
const target = nodePath.join(rootDir, relative);
|
|
@@ -278,6 +278,13 @@ export function migrateLibrary(rootDir, { write = false, knownToolPolicies = nul
|
|
|
278
278
|
report.written.push(relative);
|
|
279
279
|
break;
|
|
280
280
|
}
|
|
281
|
+
// 就地迁移时旧的 roster.json / roster.yml 会和新的 roster.yaml 并排躺着:
|
|
282
|
+
// 两份同义文件就是两个真源,删掉非 .yaml 的旧件。
|
|
283
|
+
for (const name of MEMBER_FILE_NAMES) {
|
|
284
|
+
if (name === "roster.yaml") continue;
|
|
285
|
+
const stale = nodePath.join(rootDir, ROSTERS_DIR, entry.id, name);
|
|
286
|
+
if (existsSync(stale)) rmSync(stale, { force: true });
|
|
287
|
+
}
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
const scanned = scanRoles(nodePath.join(rootDir, ROLES_DIR), { knownToolPolicies });
|
|
@@ -316,7 +323,9 @@ export function migrateLibrary(rootDir, { write = false, knownToolPolicies = nul
|
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
for (const entry of report.entries) {
|
|
319
|
-
const legacy = nodePath.join(
|
|
326
|
+
const legacy = nodePath.join(fromDir, entry.id);
|
|
327
|
+
// 仓库根 + rosters/ 的布局里,v1 的 rosters/<id>/ 正是 v2 新档案落脚处 —— 不能删。
|
|
328
|
+
if (nodePath.resolve(legacy) === nodePath.resolve(nodePath.join(rootDir, ROSTERS_DIR, entry.id))) continue;
|
|
320
329
|
if (!existsSync(legacy) || !statSync(legacy).isDirectory()) continue;
|
|
321
330
|
// 只删「迁移前的那份目录」:v2 布局里档案在 rosters/ 下,root 上不该再有同名目录。
|
|
322
331
|
rmSync(legacy, { recursive: true, force: true });
|
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,
|
|
@@ -466,8 +468,9 @@ export function validateRegistry(value, meta = {}) {
|
|
|
466
468
|
bad = true;
|
|
467
469
|
}
|
|
468
470
|
const path = entry.path === undefined ? "rosters" : String(entry.path);
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
// 空 path 表示「仓库根就是这座库」:v2 的 roles/ + rosters/ + dist/ 直接躺在仓库根上。
|
|
472
|
+
if (path !== "" && !PATTERNS.relPath.test(path)) {
|
|
473
|
+
diagnostics.push(diagnostic("error", "collection-path", `${where}.path 必须是仓库内相对目录(空字符串表示仓库根)`, file));
|
|
471
474
|
bad = true;
|
|
472
475
|
}
|
|
473
476
|
|
|
@@ -486,6 +489,25 @@ export function validateRegistry(value, meta = {}) {
|
|
|
486
489
|
}
|
|
487
490
|
if (bad) return;
|
|
488
491
|
|
|
492
|
+
const files = {};
|
|
493
|
+
if (entry.files !== undefined) {
|
|
494
|
+
if (entry.files === null || typeof entry.files !== "object" || Array.isArray(entry.files)) {
|
|
495
|
+
diagnostics.push(diagnostic("warn", "collection-files", `${where}.files 必须是「档案 id → 仓库内相对路径」的对象`, file));
|
|
496
|
+
} else {
|
|
497
|
+
for (const [key, value] of Object.entries(entry.files)) {
|
|
498
|
+
if (!rosters.includes(key)) {
|
|
499
|
+
diagnostics.push(diagnostic("warn", "collection-files-unknown", `${where}.files 里的 ${key} 不在 rosters 名单里,已忽略`, file));
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (typeof value !== "string" || value.trim() === "" || !PATTERNS.relPath.test(value) || value.split("/").includes("..")) {
|
|
503
|
+
diagnostics.push(diagnostic("warn", "collection-files-path", `${where}.files.${key} 必须是仓库内相对路径`, file));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
files[key] = value.trim();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
489
511
|
seen.add(id);
|
|
490
512
|
collections.push({
|
|
491
513
|
id,
|
|
@@ -495,6 +517,7 @@ export function validateRegistry(value, meta = {}) {
|
|
|
495
517
|
ref,
|
|
496
518
|
path: path.replace(/^\/+|\/+$/g, ""),
|
|
497
519
|
rosters,
|
|
520
|
+
files,
|
|
498
521
|
tags: Array.isArray(entry.tags) ? entry.tags.filter((tag) => typeof tag === "string") : [],
|
|
499
522
|
license: typeof entry.license === "string" ? entry.license : "",
|
|
500
523
|
maintainers: Array.isArray(entry.maintainers) ? entry.maintainers.filter((item) => typeof item === "string") : [],
|
|
@@ -526,8 +549,10 @@ export async function fetchCollection(collection, options = {}) {
|
|
|
526
549
|
for (const rosterId of wanted) {
|
|
527
550
|
let loaded = null;
|
|
528
551
|
let lastError = "";
|
|
529
|
-
|
|
530
|
-
|
|
552
|
+
const listedPath = collection.files?.[rosterId] ?? null;
|
|
553
|
+
const candidateNames = listedPath === null ? ["roster.yaml", "roster.yml", "roster.json"] : [null];
|
|
554
|
+
for (const fileName of candidateNames) {
|
|
555
|
+
const url = listedPath === null ? collectionFileUrl(collection, rosterId, fileName) : collectionFileUrl(collection, rosterId, "");
|
|
531
556
|
try {
|
|
532
557
|
const response = await fetchImpl(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
533
558
|
if (!response.ok) {
|
|
@@ -545,15 +570,37 @@ export async function fetchCollection(collection, options = {}) {
|
|
|
545
570
|
continue;
|
|
546
571
|
}
|
|
547
572
|
const parsed = parseDocumentText(loaded.text, { file: loaded.url });
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
573
|
+
if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === ROSTER2_KIND) {
|
|
574
|
+
diagnostics.push(
|
|
575
|
+
diagnostic(
|
|
576
|
+
"warn",
|
|
577
|
+
"collection-roster-needs-roles",
|
|
578
|
+
`${collection.id}/${rosterId} 是 v2 组装,成员是角色引用,单个文件拉不到角色目录;索引应指向 dist/ 里的编译产物`,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
let checked;
|
|
584
|
+
if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === COMPILED_KIND) {
|
|
585
|
+
const compiled = validateCompiled(parsed.value, { file: loaded.url });
|
|
586
|
+
if (!compiled.ok) {
|
|
587
|
+
diagnostics.push(...compiled.diagnostics);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
checked = validateRoster(flattenCompiledToRoster(compiled.compiled), { file: loaded.url, expectedId: rosterId });
|
|
591
|
+
checked = { ...checked, format: "compiled" };
|
|
592
|
+
} else {
|
|
593
|
+
checked = parsed.ok
|
|
594
|
+
? validateRoster(parsed.value, { file: loaded.url, expectedId: rosterId })
|
|
595
|
+
: { ok: false, diagnostics: parsed.diagnostics };
|
|
596
|
+
}
|
|
551
597
|
if (!checked.ok) {
|
|
552
598
|
diagnostics.push(...checked.diagnostics);
|
|
553
599
|
continue;
|
|
554
600
|
}
|
|
555
601
|
rosters.push({
|
|
556
602
|
...checked.roster,
|
|
603
|
+
...(checked.format === undefined ? {} : { format: checked.format }),
|
|
557
604
|
origin: {
|
|
558
605
|
kind: "remote",
|
|
559
606
|
collection: collection.id,
|
|
@@ -569,10 +616,12 @@ export async function fetchCollection(collection, options = {}) {
|
|
|
569
616
|
return { collection, rosters, diagnostics };
|
|
570
617
|
}
|
|
571
618
|
|
|
572
|
-
/** 拼一份档案的 raw
|
|
619
|
+
/** 拼一份档案的 raw 地址(relativePath 由索引给出,或回退到 v1 的 `<id>/roster.<ext>`)。 */
|
|
573
620
|
export function collectionFileUrl(collection, rosterId, fileName) {
|
|
574
621
|
const repo = collection.repo.replace(/^github:/, "");
|
|
575
|
-
|
|
622
|
+
const relative = collection.files?.[rosterId] ?? `${rosterId}/${fileName}`;
|
|
623
|
+
const base = collection.path === "" ? "" : `${collection.path}/`;
|
|
624
|
+
return `${GITHUB_RAW}/${repo}/${encodeURIComponent(collection.ref)}/${base}${relative}`;
|
|
576
625
|
}
|
|
577
626
|
|
|
578
627
|
/** 一次读多个源(串行: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}$",
|
|
@@ -46,13 +71,15 @@
|
|
|
46
71
|
"type": "string",
|
|
47
72
|
"pattern": "^(?:/)?(?:[A-Za-z0-9._-]+/)*[A-Za-z0-9._-]*$",
|
|
48
73
|
"default": "rosters",
|
|
49
|
-
"description": "
|
|
74
|
+
"description": "仓库内存放档案的目录;空字符串表示仓库根(v2 布局的 roles/ + rosters/ + dist/ 就在根上)。"
|
|
50
75
|
},
|
|
51
76
|
"rosters": {
|
|
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
|
}
|