@stofone/dsh-agent-roster 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -1
- package/README.md +27 -2
- package/README.zh.md +25 -2
- package/client.js +350 -3
- package/examples/dist/core-review-1.0.0.json +9 -0
- package/examples/dist/research-0.1.0.json +7 -0
- package/examples-orchestration/dist/core-review-1.0.0.json +9 -0
- package/index.js +120 -0
- package/lib/compile2.js +13 -4
- package/lib/search.js +155 -0
- package/locale/en.json +19 -2
- package/locale/zh.json +19 -2
- package/orgs/agent-roster-collective/dist/api-design-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/content-ops-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/contract-review-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/data-quality-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/db-migration-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/docs-writer-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/finance-recon-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/frontend-ui-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/i18n-l10n-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/incident-response-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/literature-review-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/market-research-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/ml-eval-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/performance-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/product-discovery-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/qa-regression-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/recruiting-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/release-manager-1.0.0.json +9 -0
- package/orgs/agent-roster-collective/dist/security-audit-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/sre-oncall-1.0.0.json +10 -0
- package/orgs/agent-roster-collective/dist/support-triage-1.0.0.json +9 -0
- package/package.json +1 -1
- package/schema/compiled.v1.schema.json +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,63 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.13.0] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### A tab that behaves like a marketplace, not a filter on a list
|
|
10
|
+
|
|
11
|
+
0.12.0 put a search field above the tabs. That was still the wrong shape: search belonged to a page about
|
|
12
|
+
collections, and browsing still meant "pick a source, then read". The panel now opens on its own **Library**
|
|
13
|
+
tab — search, tags, and one list that spans this machine and every subscribed source.
|
|
14
|
+
|
|
15
|
+
- `GET /plugins/dsh-agent-roster/catalog` returns every roster in view in one response: this machine plus
|
|
16
|
+
each subscribed collection, with tags, member counts, member roles and where each one lives.
|
|
17
|
+
- The tag strip is a real filter: counts are computed from everything in view, multiple tags are AND, and
|
|
18
|
+
the selection never writes back to a file. The segmented control switches between **by source** (this
|
|
19
|
+
machine, then one group per collection) and **flat** (one list, source named per row).
|
|
20
|
+
- Search keeps its own route and its own ranking (`lib/search.js`): two characters, 240 ms debounce, and the
|
|
21
|
+
in-flight request is aborted when you keep typing, so typing never queues requests.
|
|
22
|
+
- Compiled artifacts now carry what it takes to be *found*: `source.title`, `description`, `tags`, `license`
|
|
23
|
+
and `maintainers`. A subscribed roster is one file (`dist/<id>-<ver>.json`) resolved through `files` in the
|
|
24
|
+
index — previously the artifact knew its id and version and nothing else, so remote results could only be
|
|
25
|
+
searched by id.
|
|
26
|
+
- The panel's tag chips were inheriting the sidebar's full-width row form; they are now chips (own width,
|
|
27
|
+
pill border, count attached), and the narrow layout gets the same 18px gutter as the rest of the page.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- The header lookup field is gone; its behaviour lives inside the Library tab. `lookup.*` strings were
|
|
32
|
+
replaced by `catalog.*` (both locales).
|
|
33
|
+
- `flattenCompiledToRoster` and `rosterViewOf` now fall back to artifact metadata, so a remote roster keeps
|
|
34
|
+
its title and tags after being flattened into a v1 document.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- `GET /catalog` and search share one `gatherEntries()` path; before, each route assembled local + remote
|
|
39
|
+
entries itself, which is how a cache or concurrency fix lands in one and not the other.
|
|
40
|
+
|
|
41
|
+
## [0.12.0] - 2026-11-03
|
|
42
|
+
|
|
43
|
+
### The panel answers "which one", not "show me everything"
|
|
44
|
+
|
|
45
|
+
Until now the panel was organised by **collection**: expand a source, then read a list. But the real
|
|
46
|
+
question is almost always the opposite — "I need someone who watches data quality" — so the entry
|
|
47
|
+
point is now one search field above the tabs.
|
|
48
|
+
|
|
49
|
+
- `GET /plugins/dsh-agent-roster/search?q=` searches this machine and every subscribed source in one
|
|
50
|
+
request, reusing the collection cache (60s) so repeated typing does not hit the network again.
|
|
51
|
+
- Ranking is explainable: every match reports `hits[{field, label, text}]` ("标签:数据质量"), and id
|
|
52
|
+
matches outrank title matches outrank tag matches outrank member roles. Multiple words are **AND**:
|
|
53
|
+
"data kubernetes" matches nothing here, instead of matching everything that has "data".
|
|
54
|
+
- A result that lives in another library says so ("来自 agent-roster-collective") and can be pulled
|
|
55
|
+
from the row itself; a local one opens straight into the roster list filtered to that id.
|
|
56
|
+
- `lib/search.js` is a pure function — no filesystem, no network — so the ranking is unit-tested.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- `lib/search.js` with `searchRosters`, `searchableFields`, `tokenize`; 6 tests.
|
|
61
|
+
- `/plugins/dsh-agent-roster/search` route (local + remote, bounded source concurrency).
|
|
62
|
+
- Panel lookup field, result rows with match provenance, and `lookup.*` strings in both locales.
|
|
63
|
+
|
|
7
64
|
## [0.11.1] - 2026-11-03
|
|
8
65
|
|
|
9
66
|
### Fixed
|
|
@@ -557,7 +614,9 @@
|
|
|
557
614
|
- 工具返回值含 `undefined` 键,运行时按 `value is not lossless JSON` 整次拒绝;缺失值统一写 `null`,
|
|
558
615
|
且所有返回值先过 `lib/json.js#lossless`。
|
|
559
616
|
|
|
560
|
-
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.
|
|
617
|
+
[Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.12.0...main
|
|
618
|
+
[0.13.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.12.0...v0.13.0
|
|
619
|
+
[0.12.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.11.1...v0.12.0
|
|
561
620
|
[0.11.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.11.0...v0.11.1
|
|
562
621
|
[0.11.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.7...v0.11.0
|
|
563
622
|
[0.10.7]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.6...v0.10.7
|
package/README.md
CHANGED
|
@@ -306,6 +306,31 @@ id belongs to the team, and the plugin never writes team state. What it does own
|
|
|
306
306
|
failure that only exists on a red card never reaches the next compile, which is exactly how a gate
|
|
307
307
|
turns into decoration.
|
|
308
308
|
|
|
309
|
+
## The library tab: find by capability, not by source
|
|
310
|
+
|
|
311
|
+
The panel opens on **Library**, which is meant to read like a plugin marketplace: one search field, one row
|
|
312
|
+
of tags, and one list that spans this machine and every subscribed source.
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
要找什么能力?例:数据质量、发版、代码评审
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Two characters in, after a 240 ms pause, the panel asks `/search` a question. Every row says **why** it
|
|
319
|
+
matched (`标签:数据质量 · 成员角色:数据质量工程师`) and where it lives (`来自 agent-roster-collective`),
|
|
320
|
+
and carries the next action — open it locally, or pull that collection — inside the row.
|
|
321
|
+
|
|
322
|
+
With an empty field it **browses**: `/catalog` flattens this machine and the subscribed sources in one
|
|
323
|
+
response, the tag strip summarises every tag in view with its count (multiple tags are AND), and the
|
|
324
|
+
segmented control switches between **by source** (this machine, then one group per collection) and **flat**
|
|
325
|
+
(one list, source named per row). Tag filtering is view state only; it never writes back to a file.
|
|
326
|
+
|
|
327
|
+
Multiple words are AND, not OR: `data kubernetes` returns nothing rather than every roster that merely
|
|
328
|
+
mentions "data". Ranking is a pure function in `lib/search.js`, so it is unit-tested rather than eyeballed.
|
|
329
|
+
|
|
330
|
+
Subscribed rosters resolve through `files` in the index, which points at a concrete compiled artifact
|
|
331
|
+
(`dist/<id>-<ver>.json`). The artifact carries its own title, description, tags, licence and maintainers,
|
|
332
|
+
so finding a roster from a distance costs one file — not a clone.
|
|
333
|
+
|
|
309
334
|
## Tools
|
|
310
335
|
|
|
311
336
|
| Tool | Purpose |
|
|
@@ -368,7 +393,7 @@ data directory, the team state directory and the version.
|
|
|
368
393
|
|
|
369
394
|
A badge at the top summarises how many items need attention
|
|
370
395
|
|
|
371
|
-
The client plugin adds an **Agent Roster** entry to the left sidebar: one row in the `sidebar.panellist` slot plus the matching key in the `main` slot. Clicking the row opens a full-width page with
|
|
396
|
+
The client plugin adds an **Agent Roster** entry to the left sidebar: one row in the `sidebar.panellist` slot plus the matching key in the `main` slot. Clicking the row opens a full-width page with five tabs — the panel is not only a viewer:
|
|
372
397
|
|
|
373
398
|
| Tab | What you can do |
|
|
374
399
|
|---|---|
|
|
@@ -492,7 +517,7 @@ test/ unit tests
|
|
|
492
517
|
|
|
493
518
|
Verified on this machine:
|
|
494
519
|
|
|
495
|
-
- `npm test` —
|
|
520
|
+
- `npm test` — 308/308 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).
|
|
496
521
|
- `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.
|
|
497
522
|
- CLI: `validate --dir examples` returns exit 0 over both example libraries.
|
|
498
523
|
- 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
|
@@ -292,6 +292,28 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
292
292
|
|
|
293
293
|
回执由执行施工图的队长会话写,不由插件写:任务 id 属于团队,插件从不写团队状态。插件负责的是合并——只活在红卡上的失败,下一次编译就消失了,门也就变成了装饰。
|
|
294
294
|
|
|
295
|
+
## 档案库:按能力找,而不是按来源翻
|
|
296
|
+
|
|
297
|
+
面板的第一个页签是**档案库**,定位接近一个插件市场:一个搜索框、一排标签、一份横跨本机与所有订阅来源的清单。
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
要找什么能力?例:数据质量、发版、代码评审
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
输入两个字以上,停顿 240 毫秒后向 `/search` 提一个问题。每一行都写清**为什么命中**
|
|
304
|
+
(`标签:数据质量 · 成员角色:数据质量工程师`)与**它住在哪**(`来自 agent-roster-collective`),
|
|
305
|
+
并把下一步放在行内:本机的「打开」,远端的「拉取」。
|
|
306
|
+
|
|
307
|
+
不输入时是**浏览**:`/catalog` 一次把本机与订阅来源摊平返回;上方标签池汇总全库出现过的标签与计数,
|
|
308
|
+
多选是「且」;右侧可以在**按来源**(本机 + 每个 collection 各一组)与**平铺**(一份清单,来源写在行内)
|
|
309
|
+
之间切换。标签筛选只影响视图,不写回文件。
|
|
310
|
+
|
|
311
|
+
多个词是「且」不是「或」:`data kubernetes` 一条都不返,而不是把所有提到 data 的档案都堆上来。
|
|
312
|
+
排序是 `lib/search.js` 里的纯函数,所以它被单测钉住,而不是靠肉眼看着顺。
|
|
313
|
+
|
|
314
|
+
订阅来源的档案在索引里由 `files` 指向具体的编译产物(`dist/<id>-<ver>.json`)。产物自带标题、描述、
|
|
315
|
+
标签、许可与维护者——「从远处找一份档案」因此只需要拉一个文件,不必先克隆一个仓库。
|
|
316
|
+
|
|
295
317
|
## 工具
|
|
296
318
|
|
|
297
319
|
| 工具 | 作用 |
|
|
@@ -347,10 +369,11 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
|
|
|
347
369
|
团队状态目录与版本号也列在同一处。导入与新建档案挪到档案页签的搜索框右侧,刷新只属于档案源页签
|
|
348
370
|
(绕过一小时的缓存,直接回源读公共索引)。
|
|
349
371
|
|
|
350
|
-
客户端插件在左侧全局栏新增一行 **Agent Roster** 图标(`sidebar.panellist` 列表槽),并在 `main` 槽用同一个面板 id
|
|
372
|
+
客户端插件在左侧全局栏新增一行 **Agent Roster** 图标(`sidebar.panellist` 列表槽),并在 `main` 槽用同一个面板 id 注册整页内容;点图标即在主区域打开,宽度与对话页一致。面板分五个页签,不只是看:
|
|
351
373
|
|
|
352
374
|
| 页签 | 能做什么 |
|
|
353
375
|
|---|---|
|
|
376
|
+
| 档案库 | 按能力找档案:搜索(横跨本机与订阅来源,命中给出处)、标签筛选、按来源聚合或平铺,行内直接打开或拉取 |
|
|
354
377
|
| 档案 | 浏览每个工作区 × 每座档案库,展开成员;**新建 / 编辑 / 复制 / 删除 / 导入 / 导出**档案。编辑器有表单与源码两种模式(源码就是文件原文);写之前由宿主校验,覆盖已有文件会留一份 `.bak` |
|
|
355
378
|
| 团队 | 读一个配置好的状态目录(默认 `.agent-teams`),列出运行时真正建出来的每个团队:档案、成员档案与模型路由、每个持久化成员是否真的与其档案一致,以及该团队的任务 |
|
|
356
379
|
| 档案源 | 列出订阅的公共 registry 目录,每个 collection 一个「拉取到本机」按钮;可展开先看里面有哪些档案 |
|
|
@@ -463,7 +486,7 @@ test/ 单测
|
|
|
463
486
|
|
|
464
487
|
本机已验证:
|
|
465
488
|
|
|
466
|
-
- `npm test` ——
|
|
489
|
+
- `npm test` —— 308/308 通过(格式、解析、校验、扫描、路径、桥接、核对四桶、摘要与溯源、档案差异、能力要求与能力闸门、档案库组织、索引 schema、无损 JSON、写入计划与执行、六条路由、快照与提示、两本词典一致、工具契约、客户端加载器契约、包清单、配置视图、样式 token 约束、声明层字段与队长段、schema 与常量对齐、v2 角色/组装/编译三层及其 schema 对齐、v1→v2 迁移、执行回执并入施工图)。
|
|
467
490
|
- `npm run smoke` —— 用真实 DSH bundle 的 `defineTool` 注册六个工具,逐个调用:输出全部通过无损校验、`path` 覆盖生效、释放干净;同一条命令随后用桩模块表加载 `client.js`,断言三处槽位注册、样式只注入一次、effect 可回收。
|
|
468
491
|
- CLI:`validate --dir examples` 对两座示例库退出码 0。
|
|
469
492
|
- 用插件管理器真的装进了本机 profile(`plugin_manager install_bundle` 指向本仓库 → `link:` 依赖 + bundle 条目),重启后按链接读取本仓库文件。
|
package/client.js
CHANGED
|
@@ -34,6 +34,8 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
const INSTALL_ROUTE = "/plugins/dsh-agent-roster/install";
|
|
35
35
|
const COLLECTION_ROUTE = "/plugins/dsh-agent-roster/collection";
|
|
36
36
|
const APPLY_ROUTE = "/plugins/dsh-agent-roster/apply";
|
|
37
|
+
const SEARCH_ROUTE = "/plugins/dsh-agent-roster/search";
|
|
38
|
+
const CATALOG_ROUTE = "/plugins/dsh-agent-roster/catalog";
|
|
37
39
|
|
|
38
40
|
const EFFORTS = ["low", "medium", "high", "default"];
|
|
39
41
|
const LANGUAGES = ["auto", "zh", "en"];
|
|
@@ -1272,10 +1274,289 @@ window.__ModuleLoader__.load({
|
|
|
1272
1274
|
// 面板本身拿不到 ctx:这里由 apply() 把宿主服务放进来,供设置弹窗使用。
|
|
1273
1275
|
const HOST = { form: null, openPanel: null };
|
|
1274
1276
|
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* 档案库:像插件市场那样,把「能用的档案」全部摊在一张表里。
|
|
1280
|
+
*
|
|
1281
|
+
* 本机的与订阅来源的放在一起,因为人找的是能力,不是"这东西归谁管"。两种看法:
|
|
1282
|
+
* 按来源聚合(知道自己订了什么)、平铺(只想横向比较)。两种都只是**视图**,
|
|
1283
|
+
* 不改变数据,也不写回任何文件。
|
|
1284
|
+
*
|
|
1285
|
+
* 查询走后端排序(`/search`):前端再写一套打分,两处迟早给出不同顺序。
|
|
1286
|
+
* 标签筛选是「且」,与档案页签的标签池同一语义。
|
|
1287
|
+
*/
|
|
1288
|
+
function CatalogTab({ onOpen, onPull }) {
|
|
1289
|
+
useTranslation();
|
|
1290
|
+
const [mode, setMode] = useState("source");
|
|
1291
|
+
const [query, setQuery] = useState("");
|
|
1292
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
1293
|
+
const [catalog, setCatalog] = useState({ status: "loading", entries: [], counts: null });
|
|
1294
|
+
const [hits, setHits] = useState(null);
|
|
1295
|
+
const [pulling, setPulling] = useState("");
|
|
1296
|
+
const [nonce, setNonce] = useState(0);
|
|
1297
|
+
|
|
1298
|
+
useEffect(() => {
|
|
1299
|
+
let alive = true;
|
|
1300
|
+
setCatalog((current) => ({ ...current, status: "loading" }));
|
|
1301
|
+
readJson(CATALOG_ROUTE)
|
|
1302
|
+
.then((result) => {
|
|
1303
|
+
if (!alive) return;
|
|
1304
|
+
setCatalog({
|
|
1305
|
+
status: result.ok ? "ready" : "error",
|
|
1306
|
+
entries: result.body?.entries ?? [],
|
|
1307
|
+
counts: result.body?.counts ?? null,
|
|
1308
|
+
});
|
|
1309
|
+
})
|
|
1310
|
+
.catch(() => {
|
|
1311
|
+
if (alive) setCatalog({ status: "error", entries: [], counts: null });
|
|
1312
|
+
});
|
|
1313
|
+
return () => {
|
|
1314
|
+
alive = false;
|
|
1315
|
+
};
|
|
1316
|
+
}, [nonce]);
|
|
1317
|
+
|
|
1318
|
+
useEffect(() => {
|
|
1319
|
+
const needle = query.trim();
|
|
1320
|
+
if (needle.length < 2) {
|
|
1321
|
+
setHits(null);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const controller = new AbortController();
|
|
1325
|
+
// 停止输入之后才问:每敲一个字打一次,等于自己给自己造并发。
|
|
1326
|
+
const timer = setTimeout(() => {
|
|
1327
|
+
setHits((current) => ({ status: "loading", matches: current?.matches ?? [] }));
|
|
1328
|
+
readJson(`${SEARCH_ROUTE}?q=${encodeURIComponent(needle)}`, { signal: controller.signal })
|
|
1329
|
+
.then((result) => {
|
|
1330
|
+
if (controller.signal.aborted) return;
|
|
1331
|
+
setHits({ status: result.ok ? "ready" : "error", matches: result.body?.matches ?? [] });
|
|
1332
|
+
})
|
|
1333
|
+
.catch(() => {
|
|
1334
|
+
if (!controller.signal.aborted) setHits({ status: "error", matches: [] });
|
|
1335
|
+
});
|
|
1336
|
+
}, 240);
|
|
1337
|
+
return () => {
|
|
1338
|
+
clearTimeout(timer);
|
|
1339
|
+
controller.abort();
|
|
1340
|
+
};
|
|
1341
|
+
}, [query]);
|
|
1342
|
+
|
|
1343
|
+
const pool = useMemo(() => {
|
|
1344
|
+
const counts = new Map();
|
|
1345
|
+
for (const entry of catalog.entries) {
|
|
1346
|
+
for (const tag of entry.tags ?? []) counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
1347
|
+
}
|
|
1348
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
1349
|
+
}, [catalog.entries]);
|
|
1350
|
+
|
|
1351
|
+
const entries = hits !== null && hits.status === "ready" ? hits.matches : catalog.entries;
|
|
1352
|
+
const filtered = useMemo(() => {
|
|
1353
|
+
return entries.filter((entry) => {
|
|
1354
|
+
for (const tag of selected) if (!(entry.tags ?? []).includes(tag)) return false;
|
|
1355
|
+
return true;
|
|
1356
|
+
});
|
|
1357
|
+
}, [entries, selected]);
|
|
1358
|
+
|
|
1359
|
+
const groups = useMemo(() => {
|
|
1360
|
+
if (mode === "flat") return [{ key: "flat", title: "", scope: "mixed", entries: filtered }];
|
|
1361
|
+
const map = new Map();
|
|
1362
|
+
for (const entry of filtered) {
|
|
1363
|
+
const key = entry.scope === "local" ? "__local" : `c:${entry.collectionId ?? ""}`;
|
|
1364
|
+
if (!map.has(key)) {
|
|
1365
|
+
map.set(key, {
|
|
1366
|
+
key,
|
|
1367
|
+
scope: entry.scope,
|
|
1368
|
+
title: entry.scope === "local" ? t("catalog.local") : entry.collectionTitle ?? entry.collectionId ?? "",
|
|
1369
|
+
entries: [],
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
map.get(key).entries.push(entry);
|
|
1373
|
+
}
|
|
1374
|
+
return [...map.values()];
|
|
1375
|
+
}, [filtered, mode]);
|
|
1376
|
+
|
|
1377
|
+
function toggleTag(tag) {
|
|
1378
|
+
setSelected((current) => {
|
|
1379
|
+
const next = new Set(current);
|
|
1380
|
+
if (next.has(tag)) next.delete(tag);
|
|
1381
|
+
else next.add(tag);
|
|
1382
|
+
return next;
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function why(entry) {
|
|
1387
|
+
if (Array.isArray(entry.hits) && entry.hits.length > 0) {
|
|
1388
|
+
return entry.hits.map((hit) => `${hit.label}:${hit.text}`).join(" · ");
|
|
1389
|
+
}
|
|
1390
|
+
if (Array.isArray(entry.memberRoles) && entry.memberRoles.length > 0) return entry.memberRoles.join(" · ");
|
|
1391
|
+
return entry.description ?? "";
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function row(entry) {
|
|
1395
|
+
const busy = entry.scope === "remote" && pulling !== "";
|
|
1396
|
+
return h(
|
|
1397
|
+
"article",
|
|
1398
|
+
{ key: `${entry.scope}:${entry.collectionId ?? ""}:${entry.rosterId}`, className: "rstr-row" },
|
|
1399
|
+
h(
|
|
1400
|
+
"div",
|
|
1401
|
+
{ className: "rstr-row-body" },
|
|
1402
|
+
h("code", { className: "rstr-row-id", title: entry.rosterId }, entry.rosterId),
|
|
1403
|
+
h(
|
|
1404
|
+
"div",
|
|
1405
|
+
{ className: "rstr-row-text" },
|
|
1406
|
+
h("h3", { className: "rstr-row-title" }, entry.title === "" ? entry.rosterId : entry.title),
|
|
1407
|
+
h("p", { className: "rstr-row-desc" }, why(entry)),
|
|
1408
|
+
),
|
|
1409
|
+
h(
|
|
1410
|
+
"div",
|
|
1411
|
+
{ className: "rstr-row-stats" },
|
|
1412
|
+
h("span", { className: "rstr-stat" }, t("catalog.members", { count: String(entry.members ?? 0) })),
|
|
1413
|
+
h(
|
|
1414
|
+
"span",
|
|
1415
|
+
{ className: "rstr-stat" },
|
|
1416
|
+
entry.scope === "local"
|
|
1417
|
+
? t("catalog.local")
|
|
1418
|
+
: t("catalog.remoteFrom", { collection: entry.collectionTitle ?? entry.collectionId ?? "" }),
|
|
1419
|
+
),
|
|
1420
|
+
),
|
|
1421
|
+
h(
|
|
1422
|
+
"div",
|
|
1423
|
+
{ className: "rstr-row-actions" },
|
|
1424
|
+
h(
|
|
1425
|
+
"button",
|
|
1426
|
+
{
|
|
1427
|
+
type: "button",
|
|
1428
|
+
className: "rstr-btn rstr-btn-sm",
|
|
1429
|
+
disabled: busy,
|
|
1430
|
+
onClick: async () => {
|
|
1431
|
+
if (entry.scope === "local") {
|
|
1432
|
+
onOpen(entry.rosterId);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
setPulling(entry.collectionId ?? "");
|
|
1436
|
+
try {
|
|
1437
|
+
await onPull(entry.collectionId);
|
|
1438
|
+
setNonce((value) => value + 1);
|
|
1439
|
+
} finally {
|
|
1440
|
+
setPulling("");
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
},
|
|
1444
|
+
entry.scope === "local" ? t("catalog.open") : t("catalog.pull"),
|
|
1445
|
+
),
|
|
1446
|
+
),
|
|
1447
|
+
),
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
return h(
|
|
1452
|
+
"div",
|
|
1453
|
+
{ className: "rstr-catalog" },
|
|
1454
|
+
h(
|
|
1455
|
+
"div",
|
|
1456
|
+
{ className: "rstr-toolbar" },
|
|
1457
|
+
h("input", {
|
|
1458
|
+
className: "rstr-input rstr-search",
|
|
1459
|
+
value: query,
|
|
1460
|
+
placeholder: t("catalog.placeholder"),
|
|
1461
|
+
"aria-label": t("catalog.placeholder"),
|
|
1462
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1463
|
+
onKeyDown: (event) => {
|
|
1464
|
+
if (event.key !== "Escape") return;
|
|
1465
|
+
setQuery("");
|
|
1466
|
+
event.stopPropagation();
|
|
1467
|
+
},
|
|
1468
|
+
}),
|
|
1469
|
+
h(
|
|
1470
|
+
"div",
|
|
1471
|
+
{ className: "rstr-toolbar-actions" },
|
|
1472
|
+
h(
|
|
1473
|
+
"div",
|
|
1474
|
+
{ className: "rstr-segmented", role: "group", "aria-label": t("catalog.mode") },
|
|
1475
|
+
h(
|
|
1476
|
+
"button",
|
|
1477
|
+
{
|
|
1478
|
+
type: "button",
|
|
1479
|
+
className: `rstr-seg${mode === "source" ? " rstr-seg-on" : ""}`,
|
|
1480
|
+
"aria-pressed": mode === "source",
|
|
1481
|
+
onClick: () => setMode("source"),
|
|
1482
|
+
},
|
|
1483
|
+
t("catalog.modeSource"),
|
|
1484
|
+
),
|
|
1485
|
+
h(
|
|
1486
|
+
"button",
|
|
1487
|
+
{
|
|
1488
|
+
type: "button",
|
|
1489
|
+
className: `rstr-seg${mode === "flat" ? " rstr-seg-on" : ""}`,
|
|
1490
|
+
"aria-pressed": mode === "flat",
|
|
1491
|
+
onClick: () => setMode("flat"),
|
|
1492
|
+
},
|
|
1493
|
+
t("catalog.modeFlat"),
|
|
1494
|
+
),
|
|
1495
|
+
),
|
|
1496
|
+
h(
|
|
1497
|
+
"button",
|
|
1498
|
+
{ type: "button", className: "rstr-btn rstr-btn-sm", onClick: () => setNonce((value) => value + 1) },
|
|
1499
|
+
t("catalog.refresh"),
|
|
1500
|
+
),
|
|
1501
|
+
),
|
|
1502
|
+
),
|
|
1503
|
+
pool.length === 0
|
|
1504
|
+
? null
|
|
1505
|
+
: h(
|
|
1506
|
+
"div",
|
|
1507
|
+
{ className: "rstr-catalog-tags" },
|
|
1508
|
+
pool.map(([tag, count]) =>
|
|
1509
|
+
h(
|
|
1510
|
+
"button",
|
|
1511
|
+
{
|
|
1512
|
+
key: tag,
|
|
1513
|
+
type: "button",
|
|
1514
|
+
className: `rstr-tag${selected.has(tag) ? " rstr-tag-on" : ""}`,
|
|
1515
|
+
"aria-pressed": selected.has(tag),
|
|
1516
|
+
onClick: () => toggleTag(tag),
|
|
1517
|
+
},
|
|
1518
|
+
h("span", { className: "rstr-tag-name" }, tag),
|
|
1519
|
+
h("span", { className: "rstr-tag-count" }, String(count)),
|
|
1520
|
+
),
|
|
1521
|
+
),
|
|
1522
|
+
),
|
|
1523
|
+
h(
|
|
1524
|
+
"p",
|
|
1525
|
+
{ className: "rstr-results-head" },
|
|
1526
|
+
hits !== null && hits.status === "loading" && query.trim() !== ""
|
|
1527
|
+
? t("catalog.searching")
|
|
1528
|
+
: t("catalog.counts", {
|
|
1529
|
+
shown: String(filtered.length),
|
|
1530
|
+
total: String(entries.length),
|
|
1531
|
+
local: String(catalog.counts?.local ?? 0),
|
|
1532
|
+
remote: String(catalog.counts?.remote ?? 0),
|
|
1533
|
+
}),
|
|
1534
|
+
),
|
|
1535
|
+
catalog.status === "error" ? h("p", { className: "rstr-muted" }, t("catalog.failed")) : null,
|
|
1536
|
+
filtered.length === 0
|
|
1537
|
+
? h("p", { className: "rstr-muted" }, t("catalog.empty"))
|
|
1538
|
+
: groups.map((group) =>
|
|
1539
|
+
h(
|
|
1540
|
+
"div",
|
|
1541
|
+
{ key: group.key, className: "rstr-group" },
|
|
1542
|
+
mode === "flat" || group.title === ""
|
|
1543
|
+
? null
|
|
1544
|
+
: h(
|
|
1545
|
+
"div",
|
|
1546
|
+
{ className: "rstr-group-head" },
|
|
1547
|
+
h("span", { className: "rstr-group-title" }, group.title),
|
|
1548
|
+
h("span", { className: "rstr-group-count" }, String(group.entries.length)),
|
|
1549
|
+
),
|
|
1550
|
+
group.entries.map(row),
|
|
1551
|
+
),
|
|
1552
|
+
),
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1275
1556
|
function Panel() {
|
|
1276
1557
|
useTranslation();
|
|
1277
1558
|
const library = useLibrary();
|
|
1278
|
-
const [tab, setTab] = useState("
|
|
1559
|
+
const [tab, setTab] = useState("catalog");
|
|
1279
1560
|
const [query, setQuery] = useState("");
|
|
1280
1561
|
const [selectedTags, setSelectedTags] = useState(() => new Set());
|
|
1281
1562
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
@@ -1432,6 +1713,7 @@ window.__ModuleLoader__.load({
|
|
|
1432
1713
|
"div",
|
|
1433
1714
|
{ className: "rstr-tabs", role: "tablist" },
|
|
1434
1715
|
[
|
|
1716
|
+
["catalog", t("panel.tabsCatalog"), null],
|
|
1435
1717
|
["rosters", t("panel.tabsRosters"), rosters.length],
|
|
1436
1718
|
["teams", t("panel.tabsTeams"), teams.length],
|
|
1437
1719
|
["sources", t("panel.tabsSources"), snapshot?.remote?.collections?.length ?? 0],
|
|
@@ -1449,13 +1731,44 @@ window.__ModuleLoader__.load({
|
|
|
1449
1731
|
onClick: () => setTab(value),
|
|
1450
1732
|
},
|
|
1451
1733
|
label,
|
|
1452
|
-
h("span", { className: "rstr-tab-count" }, String(count)),
|
|
1734
|
+
count === null ? null : h("span", { className: "rstr-tab-count" }, String(count)),
|
|
1453
1735
|
),
|
|
1454
1736
|
),
|
|
1455
1737
|
);
|
|
1456
1738
|
|
|
1457
1739
|
let content = null;
|
|
1458
1740
|
|
|
1741
|
+
if (tab === "catalog") {
|
|
1742
|
+
content = h(CatalogTab, {
|
|
1743
|
+
onOpen: (rosterId) => {
|
|
1744
|
+
setTab("rosters");
|
|
1745
|
+
setQuery(rosterId);
|
|
1746
|
+
},
|
|
1747
|
+
onPull: async (collectionId) => {
|
|
1748
|
+
try {
|
|
1749
|
+
const result = await readJson(INSTALL_ROUTE, {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
headers: { "content-type": "application/json" },
|
|
1752
|
+
body: JSON.stringify({ collection: collectionId }),
|
|
1753
|
+
});
|
|
1754
|
+
if (!result.ok || result.body === null) {
|
|
1755
|
+
setToast(`${t("misc.failed")} (${String(result.status)})`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const summary = t("source.pulled", {
|
|
1759
|
+
written: result.body.written ?? 0,
|
|
1760
|
+
unchanged: result.body.unchanged ?? 0,
|
|
1761
|
+
conflicts: result.body.conflicts ?? 0,
|
|
1762
|
+
});
|
|
1763
|
+
setToast(summary);
|
|
1764
|
+
library.reload();
|
|
1765
|
+
} catch {
|
|
1766
|
+
setToast(t("misc.failed"));
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1459
1772
|
if (tab === "rosters") {
|
|
1460
1773
|
if (rosters.length === 0) {
|
|
1461
1774
|
// 空状态与列表共享同一条左沿:页面本身 padding 为 0,留白由各页签自己给。
|
|
@@ -2347,6 +2660,40 @@ window.__ModuleLoader__.load({
|
|
|
2347
2660
|
|
|
2348
2661
|
/* ---- 账本(行) ------------------------------------------------------- */
|
|
2349
2662
|
|
|
2663
|
+
.rstr-catalog { display: flex; flex-direction: column; gap: 10px; padding: 16px 28px 28px; min-width: 0; }
|
|
2664
|
+
|
|
2665
|
+
/* 标签池横着排:档案库是"逛"的地方,不该为了几个标签先把屏幕切成两栏。 */
|
|
2666
|
+
.rstr-catalog-tags { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
|
|
2667
|
+
/* 标签池在侧栏里是"一列可选的筛选项",但放进档案库的顶部它就是一枚筛选标签:
|
|
2668
|
+
宽度、边框、圆角都得按标签给,不能沿用侧栏那套整行铺满的形态。 */
|
|
2669
|
+
.rstr-catalog-tags .rstr-tag {
|
|
2670
|
+
width: auto;
|
|
2671
|
+
flex: 0 0 auto;
|
|
2672
|
+
gap: 6px;
|
|
2673
|
+
padding: 3px 10px;
|
|
2674
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2675
|
+
border-radius: 999px;
|
|
2676
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2677
|
+
}
|
|
2678
|
+
.rstr-catalog-tags .rstr-tag-on {
|
|
2679
|
+
border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary) 55%, var(--dsw-alias-border-l2));
|
|
2680
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);
|
|
2681
|
+
color: var(--dsw-alias-label-primary);
|
|
2682
|
+
}
|
|
2683
|
+
.rstr-catalog .rstr-search { max-width: 560px; }
|
|
2684
|
+
|
|
2685
|
+
.rstr-group { display: flex; flex-direction: column; min-width: 0; }
|
|
2686
|
+
|
|
2687
|
+
.rstr-group-head { display: flex; align-items: baseline; gap: 8px; padding: 10px 0 4px; border-bottom: 1px solid var(--dsw-alias-border-l2); }
|
|
2688
|
+
|
|
2689
|
+
.rstr-group-title { font-size: 12px; font-weight: 600; letter-spacing: 0.02em; color: var(--dsw-alias-label-secondary); }
|
|
2690
|
+
|
|
2691
|
+
.rstr-group-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
|
|
2692
|
+
|
|
2693
|
+
.rstr-results { display: flex; flex-direction: column; min-width: 0; padding: 0 28px 28px; border-top: 1px solid var(--dsw-alias-border-l2); }
|
|
2694
|
+
|
|
2695
|
+
.rstr-results-head { margin: 12px 0 4px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
|
|
2696
|
+
|
|
2350
2697
|
.rstr-cards { display: flex; flex-direction: column; min-width: 0; border-top: 1px solid var(--dsw-alias-border-l2); }
|
|
2351
2698
|
|
|
2352
2699
|
.rstr-row {
|
|
@@ -2662,7 +3009,7 @@ window.__ModuleLoader__.load({
|
|
|
2662
3009
|
/* ---- 窄面板 ----------------------------------------------------------- */
|
|
2663
3010
|
|
|
2664
3011
|
@media (max-width: 860px) {
|
|
2665
|
-
.rstr-head, .rstr-tabs, .rstr-rosters-main, .rstr-health, .rstr-foot { padding-left: 18px; padding-right: 18px; }
|
|
3012
|
+
.rstr-head, .rstr-tabs, .rstr-rosters-main, .rstr-health, .rstr-catalog, .rstr-foot { padding-left: 18px; padding-right: 18px; }
|
|
2666
3013
|
.rstr-banner { margin-left: 18px; margin-right: 18px; }
|
|
2667
3014
|
.rstr-rosters { grid-template-columns: minmax(0, 1fr); }
|
|
2668
3015
|
.rstr-tagpool {
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
"roster": "core-review",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"title": "代码审查小组",
|
|
7
|
+
"description": "一名队长加两名审查者:一人盯正确性,一人盯可维护性与边界条件。",
|
|
8
|
+
"tags": [
|
|
9
|
+
"review",
|
|
10
|
+
"example"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"maintainers": [
|
|
14
|
+
"STofone"
|
|
15
|
+
],
|
|
7
16
|
"repo": "dsh-agent-roster/dsh-agent-roster",
|
|
8
17
|
"ref": "main",
|
|
9
18
|
"lock": null,
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
"roster": "research",
|
|
5
5
|
"version": "0.1.0",
|
|
6
6
|
"title": "外部调研小组",
|
|
7
|
+
"description": "一手资料收集 + 交叉复核;复核者的结论必须能被另一条独立来源支撑。",
|
|
8
|
+
"tags": [
|
|
9
|
+
"research",
|
|
10
|
+
"example"
|
|
11
|
+
],
|
|
12
|
+
"license": null,
|
|
13
|
+
"maintainers": [],
|
|
7
14
|
"repo": "dsh-agent-roster/dsh-agent-roster",
|
|
8
15
|
"ref": "main",
|
|
9
16
|
"lock": null,
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
"roster": "core-review",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"title": "核心评审组",
|
|
7
|
+
"description": "一个小团队:队长拆任务,评审把关,修复收尾。用来演示 v2 的引用式成员与声明式编排。",
|
|
8
|
+
"tags": [
|
|
9
|
+
"review",
|
|
10
|
+
"example"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"maintainers": [
|
|
14
|
+
"stofone"
|
|
15
|
+
],
|
|
7
16
|
"repo": "dsh-agent-roster/dsh-agent-roster",
|
|
8
17
|
"ref": "main",
|
|
9
18
|
"lock": null,
|