@stofone/dsh-agent-roster 0.12.0 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +18 -10
  3. package/README.zh.md +16 -9
  4. package/client.js +334 -165
  5. package/examples/dist/core-review-1.0.0.json +9 -0
  6. package/examples/dist/research-0.1.0.json +7 -0
  7. package/examples-orchestration/dist/core-review-1.0.0.json +9 -0
  8. package/index.js +104 -66
  9. package/lib/compile2.js +13 -4
  10. package/locale/en.json +11 -8
  11. package/locale/zh.json +11 -8
  12. package/orgs/agent-roster-collective/dist/api-design-1.0.0.json +10 -0
  13. package/orgs/agent-roster-collective/dist/content-ops-1.0.0.json +9 -0
  14. package/orgs/agent-roster-collective/dist/contract-review-1.0.0.json +9 -0
  15. package/orgs/agent-roster-collective/dist/data-quality-1.0.0.json +10 -0
  16. package/orgs/agent-roster-collective/dist/db-migration-1.0.0.json +10 -0
  17. package/orgs/agent-roster-collective/dist/docs-writer-1.0.0.json +9 -0
  18. package/orgs/agent-roster-collective/dist/finance-recon-1.0.0.json +9 -0
  19. package/orgs/agent-roster-collective/dist/frontend-ui-1.0.0.json +10 -0
  20. package/orgs/agent-roster-collective/dist/i18n-l10n-1.0.0.json +10 -0
  21. package/orgs/agent-roster-collective/dist/incident-response-1.0.0.json +10 -0
  22. package/orgs/agent-roster-collective/dist/literature-review-1.0.0.json +9 -0
  23. package/orgs/agent-roster-collective/dist/market-research-1.0.0.json +9 -0
  24. package/orgs/agent-roster-collective/dist/ml-eval-1.0.0.json +9 -0
  25. package/orgs/agent-roster-collective/dist/performance-1.0.0.json +9 -0
  26. package/orgs/agent-roster-collective/dist/product-discovery-1.0.0.json +9 -0
  27. package/orgs/agent-roster-collective/dist/qa-regression-1.0.0.json +10 -0
  28. package/orgs/agent-roster-collective/dist/recruiting-1.0.0.json +9 -0
  29. package/orgs/agent-roster-collective/dist/release-manager-1.0.0.json +9 -0
  30. package/orgs/agent-roster-collective/dist/security-audit-1.0.0.json +10 -0
  31. package/orgs/agent-roster-collective/dist/sre-oncall-1.0.0.json +10 -0
  32. package/orgs/agent-roster-collective/dist/support-triage-1.0.0.json +9 -0
  33. package/package.json +1 -1
  34. package/schema/compiled.v1.schema.json +30 -0
package/client.js CHANGED
@@ -35,6 +35,7 @@ window.__ModuleLoader__.load({
35
35
  const COLLECTION_ROUTE = "/plugins/dsh-agent-roster/collection";
36
36
  const APPLY_ROUTE = "/plugins/dsh-agent-roster/apply";
37
37
  const SEARCH_ROUTE = "/plugins/dsh-agent-roster/search";
38
+ const CATALOG_ROUTE = "/plugins/dsh-agent-roster/catalog";
38
39
 
39
40
  const EFFORTS = ["low", "medium", "high", "default"];
40
41
  const LANGUAGES = ["auto", "zh", "en"];
@@ -1273,50 +1274,296 @@ window.__ModuleLoader__.load({
1273
1274
  // 面板本身拿不到 ctx:这里由 apply() 把宿主服务放进来,供设置弹窗使用。
1274
1275
  const HOST = { form: null, openPanel: null };
1275
1276
 
1276
- function Panel() {
1277
+
1278
+ /**
1279
+ * 档案库:像插件市场那样,把「能用的档案」全部摊在一张表里。
1280
+ *
1281
+ * 本机的与订阅来源的放在一起,因为人找的是能力,不是"这东西归谁管"。两种看法:
1282
+ * 按来源聚合(知道自己订了什么)、平铺(只想横向比较)。两种都只是**视图**,
1283
+ * 不改变数据,也不写回任何文件。
1284
+ *
1285
+ * 查询走后端排序(`/search`):前端再写一套打分,两处迟早给出不同顺序。
1286
+ * 标签筛选是「且」,与档案页签的标签池同一语义。
1287
+ */
1288
+ function CatalogTab({ onOpen, onPull }) {
1277
1289
  useTranslation();
1278
- const library = useLibrary();
1279
- const [tab, setTab] = useState("rosters");
1290
+ const [mode, setMode] = useState("source");
1280
1291
  const [query, setQuery] = useState("");
1281
- const [selectedTags, setSelectedTags] = useState(() => new Set());
1282
- const [settingsOpen, setSettingsOpen] = useState(false);
1283
- const [editor, setEditor] = useState(null);
1284
- const [importing, setImporting] = useState(false);
1285
- const [removing, setRemoving] = useState(null);
1286
- const [toast, setToast] = useState(null);
1287
- // 「找一份档案」是首屏动作,不是某个页签里的筛选器:它同时问本机与所有订阅来源。
1288
- const [lookup, setLookup] = useState("");
1289
- const [lookupState, setLookupState] = useState({ status: "idle", body: null });
1290
- const [pullingCollection, setPullingCollection] = 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);
1291
1297
 
1292
1298
  useEffect(() => {
1293
- const needle = lookup.trim();
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();
1294
1320
  if (needle.length < 2) {
1295
- setLookupState({ status: "idle", body: null });
1321
+ setHits(null);
1296
1322
  return;
1297
1323
  }
1298
1324
  const controller = new AbortController();
1299
- // 停止输入之后才发请求:每敲一个字都打一次,等于自己给自己造并发。
1325
+ // 停止输入之后才问:每敲一个字打一次,等于自己给自己造并发。
1300
1326
  const timer = setTimeout(() => {
1301
- setLookupState((current) => ({ status: "loading", body: current.body }));
1327
+ setHits((current) => ({ status: "loading", matches: current?.matches ?? [] }));
1302
1328
  readJson(`${SEARCH_ROUTE}?q=${encodeURIComponent(needle)}`, { signal: controller.signal })
1303
1329
  .then((result) => {
1304
1330
  if (controller.signal.aborted) return;
1305
- setLookupState(
1306
- result.ok && result.body !== null
1307
- ? { status: "ready", body: result.body }
1308
- : { status: "error", body: null },
1309
- );
1331
+ setHits({ status: result.ok ? "ready" : "error", matches: result.body?.matches ?? [] });
1310
1332
  })
1311
1333
  .catch(() => {
1312
- if (!controller.signal.aborted) setLookupState({ status: "error", body: null });
1334
+ if (!controller.signal.aborted) setHits({ status: "error", matches: [] });
1313
1335
  });
1314
- }, 220);
1336
+ }, 240);
1315
1337
  return () => {
1316
1338
  clearTimeout(timer);
1317
1339
  controller.abort();
1318
1340
  };
1319
- }, [lookup]);
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
+
1556
+ function Panel() {
1557
+ useTranslation();
1558
+ const library = useLibrary();
1559
+ const [tab, setTab] = useState("catalog");
1560
+ const [query, setQuery] = useState("");
1561
+ const [selectedTags, setSelectedTags] = useState(() => new Set());
1562
+ const [settingsOpen, setSettingsOpen] = useState(false);
1563
+ const [editor, setEditor] = useState(null);
1564
+ const [importing, setImporting] = useState(false);
1565
+ const [removing, setRemoving] = useState(null);
1566
+ const [toast, setToast] = useState(null);
1320
1567
 
1321
1568
  useEffect(() => {
1322
1569
  if (toast === null) return;
@@ -1355,38 +1602,6 @@ window.__ModuleLoader__.load({
1355
1602
  });
1356
1603
  }, [rosters, query, selectedTags]);
1357
1604
 
1358
- async function openMatch(match) {
1359
- if (match.scope === "local") {
1360
- setLookup("");
1361
- setTab("rosters");
1362
- setQuery(match.rosterId);
1363
- return;
1364
- }
1365
- setPullingCollection(match.collectionId ?? "");
1366
- try {
1367
- const result = await readJson(INSTALL_ROUTE, {
1368
- method: "POST",
1369
- headers: { "content-type": "application/json" },
1370
- body: JSON.stringify({ collection: match.collectionId }),
1371
- });
1372
- if (!result.ok || result.body === null) {
1373
- setToast(`${t("misc.failed")} (${String(result.status)})`);
1374
- return;
1375
- }
1376
- const summary = t("source.pulled", {
1377
- written: result.body.written ?? 0,
1378
- unchanged: result.body.unchanged ?? 0,
1379
- conflicts: result.body.conflicts ?? 0,
1380
- });
1381
- setToast(summary);
1382
- library.reload();
1383
- } catch {
1384
- setToast(t("misc.failed"));
1385
- } finally {
1386
- setPullingCollection("");
1387
- }
1388
- }
1389
-
1390
1605
  function toggleTag(tag) {
1391
1606
  setSelectedTags((current) => {
1392
1607
  const next = new Set(current);
@@ -1498,6 +1713,7 @@ window.__ModuleLoader__.load({
1498
1713
  "div",
1499
1714
  { className: "rstr-tabs", role: "tablist" },
1500
1715
  [
1716
+ ["catalog", t("panel.tabsCatalog"), null],
1501
1717
  ["rosters", t("panel.tabsRosters"), rosters.length],
1502
1718
  ["teams", t("panel.tabsTeams"), teams.length],
1503
1719
  ["sources", t("panel.tabsSources"), snapshot?.remote?.collections?.length ?? 0],
@@ -1515,89 +1731,44 @@ window.__ModuleLoader__.load({
1515
1731
  onClick: () => setTab(value),
1516
1732
  },
1517
1733
  label,
1518
- h("span", { className: "rstr-tab-count" }, String(count)),
1734
+ count === null ? null : h("span", { className: "rstr-tab-count" }, String(count)),
1519
1735
  ),
1520
1736
  ),
1521
1737
  );
1522
1738
 
1523
- // 搜索态压过页签:输入了问题就只看答案,不再让人先去挑一个合集。
1524
- let lookupView = null;
1525
- const needle = lookup.trim();
1526
- if (needle !== "") {
1527
- const body = lookupState.body;
1528
- const matches = body?.matches ?? [];
1529
- lookupView = h(
1530
- "div",
1531
- { className: "rstr-results" },
1532
- h(
1533
- "p",
1534
- { className: "rstr-results-head" },
1535
- lookupState.status === "loading" && body === null
1536
- ? t("lookup.searching")
1537
- : t("lookup.counts", {
1538
- total: String(matches.length),
1539
- local: String(body?.counts?.local ?? 0),
1540
- remote: String(body?.counts?.remote ?? 0),
1541
- }),
1542
- ),
1543
- lookupState.status === "error"
1544
- ? h("p", { className: "rstr-muted" }, t("lookup.failed"))
1545
- : null,
1546
- lookupState.status === "ready" && matches.length === 0
1547
- ? h("p", { className: "rstr-muted" }, t("lookup.empty", { q: needle }))
1548
- : null,
1549
- matches.map((match) =>
1550
- h(
1551
- "article",
1552
- { key: `${match.scope}:${match.collectionId ?? ""}:${match.rosterId}`, className: "rstr-row" },
1553
- h(
1554
- "div",
1555
- { className: "rstr-row-body" },
1556
- h("code", { className: "rstr-row-id", title: match.rosterId }, match.rosterId),
1557
- h(
1558
- "div",
1559
- { className: "rstr-row-text" },
1560
- h("h3", { className: "rstr-row-title" }, match.title === "" ? match.rosterId : match.title),
1561
- h(
1562
- "p",
1563
- { className: "rstr-row-desc" },
1564
- match.hits.map((hit) => `${hit.label}:${hit.text}`).join(" · "),
1565
- ),
1566
- ),
1567
- h(
1568
- "div",
1569
- { className: "rstr-row-stats" },
1570
- h("span", { className: "rstr-stat" }, t("lookup.members", { count: String(match.members) })),
1571
- h(
1572
- "span",
1573
- { className: "rstr-stat" },
1574
- match.scope === "local"
1575
- ? t("lookup.local")
1576
- : t("lookup.remoteFrom", { collection: match.collectionTitle ?? match.collectionId ?? "" }),
1577
- ),
1578
- ),
1579
- h(
1580
- "div",
1581
- { className: "rstr-row-actions" },
1582
- h(
1583
- "button",
1584
- {
1585
- type: "button",
1586
- className: "rstr-btn rstr-btn-sm",
1587
- disabled: match.scope === "remote" && pullingCollection !== "",
1588
- onClick: () => openMatch(match),
1589
- },
1590
- match.scope === "local" ? t("lookup.open") : t("lookup.pull"),
1591
- ),
1592
- ),
1593
- ),
1594
- ),
1595
- ),
1596
- );
1597
- }
1598
-
1599
1739
  let content = null;
1600
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
+
1601
1772
  if (tab === "rosters") {
1602
1773
  if (rosters.length === 0) {
1603
1774
  // 空状态与列表共享同一条左沿:页面本身 padding 为 0,留白由各页签自己给。
@@ -1741,31 +1912,8 @@ window.__ModuleLoader__.load({
1741
1912
  { className: "rstr-page" },
1742
1913
  header,
1743
1914
  banner,
1744
- h(
1745
- "div",
1746
- { className: "rstr-lookup" },
1747
- h("input", {
1748
- className: "rstr-input rstr-lookup-input",
1749
- value: lookup,
1750
- placeholder: t("lookup.placeholder"),
1751
- "aria-label": t("lookup.placeholder"),
1752
- onChange: (event) => setLookup(event.target.value),
1753
- onKeyDown: (event) => {
1754
- if (event.key !== "Escape") return;
1755
- setLookup("");
1756
- event.stopPropagation();
1757
- },
1758
- }),
1759
- lookup === ""
1760
- ? h("span", { className: "rstr-lookup-hint" }, t("lookup.hint"))
1761
- : h(
1762
- "button",
1763
- { type: "button", className: "rstr-btn rstr-btn-sm rstr-btn-ghost", onClick: () => setLookup("") },
1764
- t("lookup.clear"),
1765
- ),
1766
- ),
1767
- lookupView === null ? tabs : null,
1768
- lookupView === null ? content : lookupView,
1915
+ tabs,
1916
+ content,
1769
1917
  h(
1770
1918
  "footer",
1771
1919
  { className: "rstr-foot" },
@@ -2512,14 +2660,35 @@ window.__ModuleLoader__.load({
2512
2660
 
2513
2661
  /* ---- 账本(行) ------------------------------------------------------- */
2514
2662
 
2515
- .rstr-lookup { display: flex; align-items: center; gap: 10px; padding: 0 28px 12px; min-width: 0; }
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; }
2516
2686
 
2517
- .rstr-lookup-input { flex: 1 1 auto; max-width: 560px; }
2687
+ .rstr-group-head { display: flex; align-items: baseline; gap: 8px; padding: 10px 0 4px; border-bottom: 1px solid var(--dsw-alias-border-l2); }
2518
2688
 
2519
- .rstr-lookup-hint { font-size: 12px; color: var(--dsw-alias-label-secondary); }
2689
+ .rstr-group-title { font-size: 12px; font-weight: 600; letter-spacing: 0.02em; color: var(--dsw-alias-label-secondary); }
2520
2690
 
2521
- /* 清除按钮不参与压缩:窄面板里被挤成竖排的两个字最难认。 */
2522
- .rstr-lookup > .rstr-btn { flex: 0 0 auto; white-space: nowrap; }
2691
+ .rstr-group-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
2523
2692
 
2524
2693
  .rstr-results { display: flex; flex-direction: column; min-width: 0; padding: 0 28px 28px; border-top: 1px solid var(--dsw-alias-border-l2); }
2525
2694
 
@@ -2840,7 +3009,7 @@ window.__ModuleLoader__.load({
2840
3009
  /* ---- 窄面板 ----------------------------------------------------------- */
2841
3010
 
2842
3011
  @media (max-width: 860px) {
2843
- .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; }
2844
3013
  .rstr-banner { margin-left: 18px; margin-right: 18px; }
2845
3014
  .rstr-rosters { grid-template-columns: minmax(0, 1fr); }
2846
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,