@stofone/dsh-agent-roster 0.10.1 → 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 +20 -1
- package/README.md +4 -0
- package/README.zh.md +3 -0
- 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,22 @@
|
|
|
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
|
+
|
|
7
23
|
## [0.10.1] - 2026-11-03
|
|
8
24
|
|
|
9
25
|
### Docs
|
|
@@ -456,7 +472,10 @@
|
|
|
456
472
|
- 工具返回值含 `undefined` 键,运行时按 `value is not lossless JSON` 整次拒绝;缺失值统一写 `null`,
|
|
457
473
|
且所有返回值先过 `lib/json.js#lossless`。
|
|
458
474
|
|
|
459
|
-
[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
|
|
460
479
|
[0.9.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.2
|
|
461
480
|
[0.9.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.1
|
|
462
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`,
|
package/README.zh.md
CHANGED
|
@@ -364,6 +364,9 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
364
364
|
冲突要你自己决定:要么在编辑器里合并,要么再点一次「拉取到本机」并在请求里带 `overwrite`。
|
|
365
365
|
- **索引本身也有 schema**:[`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) 与运行时校验器对齐,
|
|
366
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` 会替你填。
|
|
367
370
|
- **登记自己的档案**:在 `index/registry.json` 的 `collections` 里加一条(`id`、`repo: github:owner/repo`、
|
|
368
371
|
`ref`、`path`、`rosters[]`),提交 PR 即可;`.github/workflows/registry.yml` 会校验索引与仓库真实文件是否对得上。
|
|
369
372
|
|
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
|
}
|