@stofone/dsh-agent-roster 0.5.0 → 0.6.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 +42 -0
- package/README.md +31 -2
- package/README.zh.md +26 -2
- package/client.js +459 -61
- package/index/registry.json +77 -3
- package/index.js +41 -1
- package/lib/apply.js +186 -0
- package/lib/captain.js +259 -0
- package/lib/config-view.js +20 -3
- package/lib/render.js +25 -0
- package/lib/service.js +50 -0
- package/lib/tools.js +20 -1
- package/locale/en.json +48 -3
- package/locale/zh.json +48 -3
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -33,9 +33,11 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
const ROSTERS_ROUTE = "/plugins/dsh-agent-roster/rosters";
|
|
34
34
|
const INSTALL_ROUTE = "/plugins/dsh-agent-roster/install";
|
|
35
35
|
const COLLECTION_ROUTE = "/plugins/dsh-agent-roster/collection";
|
|
36
|
+
const APPLY_ROUTE = "/plugins/dsh-agent-roster/apply";
|
|
36
37
|
|
|
37
38
|
const EFFORTS = ["low", "medium", "high", "default"];
|
|
38
39
|
const LANGUAGES = ["auto", "zh", "en"];
|
|
40
|
+
const REGIONS = ["auto", "global", "china"];
|
|
39
41
|
const FORMATS = ["yaml", "json"];
|
|
40
42
|
const STYLE_ID = "dsh-agent-roster-style";
|
|
41
43
|
|
|
@@ -156,10 +158,12 @@ window.__ModuleLoader__.load({
|
|
|
156
158
|
|
|
157
159
|
function useLibrary() {
|
|
158
160
|
const [state, setState] = useState({ status: "loading", data: null, error: false });
|
|
159
|
-
const load = useCallback(async () => {
|
|
161
|
+
const load = useCallback(async (options) => {
|
|
162
|
+
// 只有档案源那处会传 { refresh: true }:绕过 registry 的 TTL,强制回源。
|
|
163
|
+
const forced = options !== null && typeof options === "object" && options.refresh === true;
|
|
160
164
|
setState((current) => ({ ...current, status: current.data === null ? "loading" : "refreshing" }));
|
|
161
165
|
try {
|
|
162
|
-
const result = await readJson(LIBRARY_ROUTE);
|
|
166
|
+
const result = await readJson(forced ? `${LIBRARY_ROUTE}?refresh=1` : LIBRARY_ROUTE);
|
|
163
167
|
if (!result.ok || result.body === null) throw new Error(String(result.status));
|
|
164
168
|
setState({ status: "ready", data: result.body, error: false });
|
|
165
169
|
} catch {
|
|
@@ -169,7 +173,7 @@ window.__ModuleLoader__.load({
|
|
|
169
173
|
useEffect(() => {
|
|
170
174
|
load();
|
|
171
175
|
}, [load]);
|
|
172
|
-
return { ...state, reload: load };
|
|
176
|
+
return { ...state, reload: () => load(), refresh: () => load({ refresh: true }) };
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
/* ------------------------------------------------------------------ */
|
|
@@ -503,7 +507,152 @@ window.__ModuleLoader__.load({
|
|
|
503
507
|
);
|
|
504
508
|
}
|
|
505
509
|
|
|
506
|
-
|
|
510
|
+
/**
|
|
511
|
+
* 队长卡:显示档案里声明的队长 + 与部署实际值的核对结果 + “应用”入口。
|
|
512
|
+
*
|
|
513
|
+
* 应用只做两件事:切会话模型、往会话里排一段建团队提示词。它不会、也不能
|
|
514
|
+
* 直接改部署配置——provider/model 不一致时,界面如实说“要改部署”,而不是假装成功。
|
|
515
|
+
*/
|
|
516
|
+
function CaptainPanel({ roster, report, sessions, onToast }) {
|
|
517
|
+
useTranslation();
|
|
518
|
+
const captain = roster.captain ?? {};
|
|
519
|
+
const [mode, setMode] = useState("existing");
|
|
520
|
+
const [sessionId, setSessionId] = useState("");
|
|
521
|
+
const [cwd, setCwd] = useState("");
|
|
522
|
+
const [busy, setBusy] = useState(false);
|
|
523
|
+
const [result, setResult] = useState(null);
|
|
524
|
+
|
|
525
|
+
useEffect(() => {
|
|
526
|
+
if (sessionId === "" && sessions.length > 0) setSessionId(sessions[0].sessionId);
|
|
527
|
+
}, [sessions, sessionId]);
|
|
528
|
+
|
|
529
|
+
const checks = report?.checks ?? [];
|
|
530
|
+
const canApply = mode === "new" || sessionId !== "";
|
|
531
|
+
|
|
532
|
+
async function apply() {
|
|
533
|
+
if (!canApply || busy) return;
|
|
534
|
+
setBusy(true);
|
|
535
|
+
setResult(null);
|
|
536
|
+
try {
|
|
537
|
+
const payload = { rosterId: roster.id, mode };
|
|
538
|
+
if (mode === "existing") payload.sessionId = sessionId;
|
|
539
|
+
else if (cwd.trim() !== "") payload.cwd = cwd.trim();
|
|
540
|
+
const response = await readJson(APPLY_ROUTE, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: { "content-type": "application/json" },
|
|
543
|
+
body: JSON.stringify(payload),
|
|
544
|
+
});
|
|
545
|
+
if (!response.ok || response.body?.ok !== true) {
|
|
546
|
+
setResult({ ok: false, code: response.body?.code ?? response.body?.error?.code ?? String(response.status) });
|
|
547
|
+
onToast(t("captain.applyFailed"));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
setResult({ ok: true, sessionId: response.body.sessionId });
|
|
551
|
+
onToast(t("captain.applied"));
|
|
552
|
+
} catch {
|
|
553
|
+
setResult({ ok: false, code: "network" });
|
|
554
|
+
onToast(t("misc.failed"));
|
|
555
|
+
} finally {
|
|
556
|
+
setBusy(false);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return h(
|
|
561
|
+
"section",
|
|
562
|
+
{ className: "rstr-captain" },
|
|
563
|
+
h("div", { className: "rstr-section-title rstr-mt" }, t("captain.title")),
|
|
564
|
+
h("p", { className: "rstr-hint" }, t("captain.hint")),
|
|
565
|
+
h(
|
|
566
|
+
"div",
|
|
567
|
+
{ className: "rstr-meta-grid" },
|
|
568
|
+
h("span", { className: "rstr-label" }, t("captain.declared")),
|
|
569
|
+
h("span", { className: "rstr-mono" }, captain.provider || "?") ,
|
|
570
|
+
h("span", { className: "rstr-mono" }, captain.model || "?"),
|
|
571
|
+
h("span", { className: "rstr-mono" }, captain.reasoningEffort || t("captain.effortDefault")),
|
|
572
|
+
h(
|
|
573
|
+
"span",
|
|
574
|
+
{ className: "rstr-muted" },
|
|
575
|
+
String(captain.personaPrefix ?? "").trim() === "" ? t("captain.noPersona") : t("captain.hasPersona"),
|
|
576
|
+
),
|
|
577
|
+
),
|
|
578
|
+
checks.length === 0
|
|
579
|
+
? h("p", { className: "rstr-hint" }, t("captain.noChecks"))
|
|
580
|
+
: h(
|
|
581
|
+
"ul",
|
|
582
|
+
{ className: "rstr-checks" },
|
|
583
|
+
checks.map((check) =>
|
|
584
|
+
h(
|
|
585
|
+
"li",
|
|
586
|
+
{ key: check.field, className: "rstr-check" },
|
|
587
|
+
h("span", null, t(`captain.check.${check.field}`)),
|
|
588
|
+
h(
|
|
589
|
+
"span",
|
|
590
|
+
{ className: check.status === "match" ? "rstr-ok" : check.status === "differ" ? "rstr-bad" : "rstr-muted" },
|
|
591
|
+
t(`captain.status.${check.status}`, { declared: check.declared || "?", actual: check.actual || "?" }),
|
|
592
|
+
),
|
|
593
|
+
),
|
|
594
|
+
),
|
|
595
|
+
),
|
|
596
|
+
h(
|
|
597
|
+
"div",
|
|
598
|
+
{ className: "rstr-apply" },
|
|
599
|
+
h("div", { className: "rstr-label" }, t("captain.applyTo")),
|
|
600
|
+
h(
|
|
601
|
+
"div",
|
|
602
|
+
{ className: "rstr-segmented" },
|
|
603
|
+
h(
|
|
604
|
+
"button",
|
|
605
|
+
{ type: "button", className: mode === "existing" ? "rstr-seg rstr-seg-on" : "rstr-seg", onClick: () => setMode("existing") },
|
|
606
|
+
t("captain.modeExisting"),
|
|
607
|
+
),
|
|
608
|
+
h(
|
|
609
|
+
"button",
|
|
610
|
+
{ type: "button", className: mode === "new" ? "rstr-seg rstr-seg-on" : "rstr-seg", onClick: () => setMode("new") },
|
|
611
|
+
t("captain.modeNew"),
|
|
612
|
+
),
|
|
613
|
+
),
|
|
614
|
+
mode === "existing"
|
|
615
|
+
? h(
|
|
616
|
+
"select",
|
|
617
|
+
{
|
|
618
|
+
className: "rstr-select",
|
|
619
|
+
value: sessionId,
|
|
620
|
+
onChange: (event) => setSessionId(event.target.value),
|
|
621
|
+
},
|
|
622
|
+
sessions.length === 0
|
|
623
|
+
? h("option", { value: "" }, t("captain.noSessions"))
|
|
624
|
+
: sessions.map((session) =>
|
|
625
|
+
h(
|
|
626
|
+
"option",
|
|
627
|
+
{ key: session.sessionId, value: session.sessionId },
|
|
628
|
+
`${session.sessionId.slice(0, 12)} · ${session.cwd || "?"}${
|
|
629
|
+
session.model === null ? "" : ` · ${session.model.model}`
|
|
630
|
+
}`,
|
|
631
|
+
),
|
|
632
|
+
),
|
|
633
|
+
)
|
|
634
|
+
: h("input", {
|
|
635
|
+
className: "rstr-input",
|
|
636
|
+
placeholder: t("captain.cwdPlaceholder"),
|
|
637
|
+
value: cwd,
|
|
638
|
+
onChange: (event) => setCwd(event.target.value),
|
|
639
|
+
}),
|
|
640
|
+
h(
|
|
641
|
+
"button",
|
|
642
|
+
{ type: "button", className: "rstr-btn rstr-btn-primary", disabled: !canApply || busy, onClick: apply },
|
|
643
|
+
busy ? t("captain.applying") : t("captain.apply"),
|
|
644
|
+
),
|
|
645
|
+
),
|
|
646
|
+
result !== null && result.ok === false
|
|
647
|
+
? h("p", { className: "rstr-bad rstr-small" }, t("captain.applyFailedCode", { code: result.code }))
|
|
648
|
+
: null,
|
|
649
|
+
result !== null && result.ok === true
|
|
650
|
+
? h("p", { className: "rstr-ok rstr-small" }, t("captain.appliedTo", { session: result.sessionId }))
|
|
651
|
+
: null,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function RosterCard({ roster, deployment, captainReport, sessions, onToast, onEdit, onDuplicate, onDelete, onCopy }) {
|
|
507
656
|
const [open, setOpen] = useState(false);
|
|
508
657
|
const errors = (roster.diagnostics ?? []).filter((item) => item?.severity === "error").length;
|
|
509
658
|
const deployed = deployment !== undefined && deployment.deployed > 0;
|
|
@@ -573,6 +722,9 @@ window.__ModuleLoader__.load({
|
|
|
573
722
|
roster.members.length === 0
|
|
574
723
|
? h("p", { className: "rstr-muted" }, t("roster.noMembers"))
|
|
575
724
|
: roster.members.map((member) => h(MemberBlock, { key: member.name, member })),
|
|
725
|
+
roster.captain === null || roster.captain === undefined
|
|
726
|
+
? null
|
|
727
|
+
: h(CaptainPanel, { roster, report: captainReport, sessions: sessions ?? [], onToast }),
|
|
576
728
|
h(DiagnosticList, { items: roster.diagnostics }),
|
|
577
729
|
)
|
|
578
730
|
: null,
|
|
@@ -1064,11 +1216,16 @@ window.__ModuleLoader__.load({
|
|
|
1064
1216
|
/* 主面板 */
|
|
1065
1217
|
/* ------------------------------------------------------------------ */
|
|
1066
1218
|
|
|
1219
|
+
// 面板本身拿不到 ctx:这里由 apply() 把宿主服务放进来,供设置弹窗使用。
|
|
1220
|
+
const HOST = { form: null, openPanel: null };
|
|
1221
|
+
|
|
1067
1222
|
function Panel() {
|
|
1068
1223
|
useTranslation();
|
|
1069
1224
|
const library = useLibrary();
|
|
1070
1225
|
const [tab, setTab] = useState("rosters");
|
|
1071
1226
|
const [query, setQuery] = useState("");
|
|
1227
|
+
const [selectedTags, setSelectedTags] = useState(() => new Set());
|
|
1228
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
1072
1229
|
const [editor, setEditor] = useState(null);
|
|
1073
1230
|
const [importing, setImporting] = useState(false);
|
|
1074
1231
|
const [removing, setRemoving] = useState(null);
|
|
@@ -1082,17 +1239,47 @@ window.__ModuleLoader__.load({
|
|
|
1082
1239
|
|
|
1083
1240
|
const snapshot = library.data;
|
|
1084
1241
|
const rosters = useMemo(() => {
|
|
1085
|
-
|
|
1242
|
+
return snapshot?.workspaces?.flatMap((workspace) =>
|
|
1086
1243
|
(workspace.rosters ?? []).map((roster) => ({ ...roster, rootDir: workspace.rootDir })),
|
|
1087
1244
|
) ?? [];
|
|
1245
|
+
}, [snapshot]);
|
|
1246
|
+
|
|
1247
|
+
// 标签池:整个库出现过的标签 + 各标签档案数,按出现次数降序(同数按字母)。
|
|
1248
|
+
const tagPool = useMemo(() => {
|
|
1249
|
+
const counts = new Map();
|
|
1250
|
+
for (const roster of rosters) {
|
|
1251
|
+
for (const tag of roster.tags ?? []) counts.set(tag, (counts.get(tag) ?? 0) + 1);
|
|
1252
|
+
}
|
|
1253
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
1254
|
+
}, [rosters]);
|
|
1255
|
+
|
|
1256
|
+
const filtered = useMemo(() => {
|
|
1088
1257
|
const needle = query.trim().toLowerCase();
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1258
|
+
return rosters.filter((roster) => {
|
|
1259
|
+
if (selectedTags.size > 0) {
|
|
1260
|
+
const tags = roster.tags ?? [];
|
|
1261
|
+
// 多选是“且”:勾了 revision 和 testing,就只看同时带这两个标签的档案。
|
|
1262
|
+
for (const tag of selectedTags) if (!tags.includes(tag)) return false;
|
|
1263
|
+
}
|
|
1264
|
+
if (needle === "") return true;
|
|
1265
|
+
return [roster.id, roster.title, roster.description, ...(roster.tags ?? []), ...(roster.members ?? []).map((member) => member.name)]
|
|
1092
1266
|
.filter((value) => typeof value === "string")
|
|
1093
|
-
.some((value) => value.toLowerCase().includes(needle))
|
|
1094
|
-
);
|
|
1095
|
-
}, [
|
|
1267
|
+
.some((value) => value.toLowerCase().includes(needle));
|
|
1268
|
+
});
|
|
1269
|
+
}, [rosters, query, selectedTags]);
|
|
1270
|
+
|
|
1271
|
+
function toggleTag(tag) {
|
|
1272
|
+
setSelectedTags((current) => {
|
|
1273
|
+
const next = new Set(current);
|
|
1274
|
+
if (next.has(tag)) next.delete(tag);
|
|
1275
|
+
else next.add(tag);
|
|
1276
|
+
return next;
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function clearTags() {
|
|
1281
|
+
setSelectedTags(() => new Set());
|
|
1282
|
+
}
|
|
1096
1283
|
|
|
1097
1284
|
const teams = snapshot?.teams?.teams ?? [];
|
|
1098
1285
|
const hints = snapshot?.hints ?? [];
|
|
@@ -1153,9 +1340,18 @@ window.__ModuleLoader__.load({
|
|
|
1153
1340
|
h(
|
|
1154
1341
|
"div",
|
|
1155
1342
|
{ className: "rstr-head-actions" },
|
|
1156
|
-
|
|
1157
|
-
h(
|
|
1158
|
-
|
|
1343
|
+
// 头部只留一个入口:导入/新建属于档案页,刷新索引属于档案源,各自回到自己的页签。
|
|
1344
|
+
h(
|
|
1345
|
+
"button",
|
|
1346
|
+
{
|
|
1347
|
+
type: "button",
|
|
1348
|
+
className: "rstr-btn rstr-settings-btn",
|
|
1349
|
+
onClick: () => setSettingsOpen(true),
|
|
1350
|
+
title: t("panel.settings"),
|
|
1351
|
+
},
|
|
1352
|
+
h("span", { className: "rstr-settings-glyph", "aria-hidden": "true" }, "\u2699"),
|
|
1353
|
+
t("panel.settings"),
|
|
1354
|
+
),
|
|
1159
1355
|
),
|
|
1160
1356
|
);
|
|
1161
1357
|
|
|
@@ -1216,31 +1412,93 @@ window.__ModuleLoader__.load({
|
|
|
1216
1412
|
} else {
|
|
1217
1413
|
content = h(
|
|
1218
1414
|
"div",
|
|
1219
|
-
|
|
1220
|
-
h("
|
|
1221
|
-
className: "rstr-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1415
|
+
{ className: "rstr-rosters" },
|
|
1416
|
+
h("aside", { className: "rstr-tagpool", "aria-label": t("panel.tagPool") }, [
|
|
1417
|
+
h("span", { className: "rstr-tagpool-title" }, t("panel.tagPool")),
|
|
1418
|
+
tagPool.length === 0
|
|
1419
|
+
? h("p", { className: "rstr-muted rstr-small" }, t("panel.tagPoolEmpty"))
|
|
1420
|
+
: h(
|
|
1421
|
+
"div",
|
|
1422
|
+
{ className: "rstr-tagpool-list" },
|
|
1423
|
+
tagPool.map(([tag, count]) =>
|
|
1424
|
+
h(
|
|
1425
|
+
"button",
|
|
1426
|
+
{
|
|
1427
|
+
key: tag,
|
|
1428
|
+
type: "button",
|
|
1429
|
+
className: `rstr-tag${selectedTags.has(tag) ? " rstr-tag-on" : ""}`,
|
|
1430
|
+
"aria-pressed": selectedTags.has(tag),
|
|
1431
|
+
onClick: () => toggleTag(tag),
|
|
1432
|
+
},
|
|
1433
|
+
h("span", { className: "rstr-tag-name" }, tag),
|
|
1434
|
+
h("span", { className: "rstr-tag-count" }, String(count)),
|
|
1435
|
+
),
|
|
1436
|
+
),
|
|
1437
|
+
),
|
|
1438
|
+
selectedTags.size === 0
|
|
1439
|
+
? null
|
|
1440
|
+
: h(
|
|
1441
|
+
"button",
|
|
1442
|
+
{ type: "button", className: "rstr-btn rstr-btn-sm rstr-tag-clear", onClick: clearTags },
|
|
1443
|
+
t("panel.tagClear"),
|
|
1444
|
+
),
|
|
1445
|
+
]),
|
|
1446
|
+
h(
|
|
1447
|
+
"div",
|
|
1448
|
+
{ className: "rstr-rosters-main" },
|
|
1449
|
+
h(
|
|
1450
|
+
"div",
|
|
1451
|
+
{ className: "rstr-toolbar" },
|
|
1452
|
+
h("input", {
|
|
1453
|
+
className: "rstr-input rstr-search",
|
|
1454
|
+
value: query,
|
|
1455
|
+
placeholder: t("panel.searchPlaceholder"),
|
|
1456
|
+
"aria-label": t("panel.search"),
|
|
1457
|
+
onChange: (event) => setQuery(event.target.value),
|
|
1458
|
+
}),
|
|
1459
|
+
h(
|
|
1230
1460
|
"div",
|
|
1231
|
-
{ className: "rstr-
|
|
1232
|
-
|
|
1461
|
+
{ className: "rstr-toolbar-actions" },
|
|
1462
|
+
h(
|
|
1463
|
+
"button",
|
|
1464
|
+
{ type: "button", className: "rstr-btn", onClick: () => setImporting(true), disabled: targets.length === 0 },
|
|
1465
|
+
t("panel.import"),
|
|
1466
|
+
),
|
|
1467
|
+
h(
|
|
1468
|
+
"button",
|
|
1469
|
+
{
|
|
1470
|
+
type: "button",
|
|
1471
|
+
className: "rstr-btn rstr-btn-primary",
|
|
1472
|
+
onClick: () => setEditor({ roster: null, rootDir: defaultRoot }),
|
|
1473
|
+
disabled: targets.length === 0,
|
|
1474
|
+
},
|
|
1475
|
+
t("panel.create"),
|
|
1476
|
+
),
|
|
1477
|
+
),
|
|
1478
|
+
),
|
|
1479
|
+
filtered.length === 0
|
|
1480
|
+
? h("p", { className: "rstr-muted" }, t("panel.noMatch"))
|
|
1481
|
+
: h(
|
|
1482
|
+
"div",
|
|
1483
|
+
{ className: "rstr-cards" },
|
|
1484
|
+
filtered.map((roster) =>
|
|
1233
1485
|
h(RosterCard, {
|
|
1234
1486
|
key: `${roster.rootDir}::${roster.id}`,
|
|
1235
1487
|
roster,
|
|
1236
1488
|
deployment: snapshot?.deployments?.[roster.id],
|
|
1489
|
+
captainReport: (snapshot?.captain?.reports ?? []).find(
|
|
1490
|
+
(item) => item.rosterId === roster.id && item.rootDir === roster.rootDir,
|
|
1491
|
+
),
|
|
1492
|
+
sessions: snapshot?.sessions?.sessions ?? [],
|
|
1493
|
+
onToast: setToast,
|
|
1237
1494
|
onEdit: () => setEditor({ roster, rootDir: roster.rootDir }),
|
|
1238
1495
|
onDuplicate: () => setEditor({ roster, rootDir: roster.rootDir, duplicate: true }),
|
|
1239
1496
|
onDelete: () => setRemoving(roster),
|
|
1240
1497
|
onCopy: copyPayload,
|
|
1241
1498
|
}),
|
|
1242
1499
|
),
|
|
1243
|
-
|
|
1500
|
+
),
|
|
1501
|
+
),
|
|
1244
1502
|
);
|
|
1245
1503
|
}
|
|
1246
1504
|
}
|
|
@@ -1257,6 +1515,8 @@ window.__ModuleLoader__.load({
|
|
|
1257
1515
|
remote: snapshot?.remote ?? null,
|
|
1258
1516
|
onToast: setToast,
|
|
1259
1517
|
onPulled: () => library.reload(),
|
|
1518
|
+
onRefresh: () => library.refresh(),
|
|
1519
|
+
refreshing: library.status === "refreshing",
|
|
1260
1520
|
});
|
|
1261
1521
|
}
|
|
1262
1522
|
|
|
@@ -1290,6 +1550,22 @@ window.__ModuleLoader__.load({
|
|
|
1290
1550
|
: null,
|
|
1291
1551
|
),
|
|
1292
1552
|
toast !== null ? h("div", { className: "rstr-toast", role: "status" }, toast) : null,
|
|
1553
|
+
settingsOpen
|
|
1554
|
+
? h(
|
|
1555
|
+
Modal,
|
|
1556
|
+
{ title: t("panel.settings"), onClose: () => setSettingsOpen(false), width: "560px" },
|
|
1557
|
+
h(SettingsSection, {
|
|
1558
|
+
form: HOST.form,
|
|
1559
|
+
library,
|
|
1560
|
+
onSaved: () => library.reload(),
|
|
1561
|
+
inModal: true,
|
|
1562
|
+
onOpenPanel: () => {
|
|
1563
|
+
setSettingsOpen(false);
|
|
1564
|
+
HOST.openPanel?.();
|
|
1565
|
+
},
|
|
1566
|
+
}),
|
|
1567
|
+
)
|
|
1568
|
+
: null,
|
|
1293
1569
|
editor !== null
|
|
1294
1570
|
? h(Editor, {
|
|
1295
1571
|
target: { roster: editor.roster, rootDir: editor.rootDir, duplicate: editor.duplicate === true },
|
|
@@ -1332,7 +1608,7 @@ window.__ModuleLoader__.load({
|
|
|
1332
1608
|
* 拉取是**显式动作**:后端按内容比对,相同的跳过、不同的默认不覆盖(会把冲突数报回来)。
|
|
1333
1609
|
* 前端不做这份判断——它只把结果说清楚,避免"点一下按钮,本地改动就没了"。
|
|
1334
1610
|
*/
|
|
1335
|
-
function SourcesTab({ remote, onToast, onPulled }) {
|
|
1611
|
+
function SourcesTab({ remote, onToast, onPulled, onRefresh, refreshing }) {
|
|
1336
1612
|
useTranslation();
|
|
1337
1613
|
const [pulling, setPulling] = useState("");
|
|
1338
1614
|
const [openId, setOpenId] = useState("");
|
|
@@ -1387,10 +1663,33 @@ window.__ModuleLoader__.load({
|
|
|
1387
1663
|
}
|
|
1388
1664
|
}
|
|
1389
1665
|
|
|
1666
|
+
// 刷新是档案源自己的动作:绕过 TTL 重新回源,而不是重读本地快照。
|
|
1667
|
+
const head = h(
|
|
1668
|
+
"div",
|
|
1669
|
+
{ className: "rstr-sources-head" },
|
|
1670
|
+
h(
|
|
1671
|
+
"p",
|
|
1672
|
+
{ className: "rstr-muted rstr-small" },
|
|
1673
|
+
indexes.length > 0
|
|
1674
|
+
? indexes
|
|
1675
|
+
.map((index) =>
|
|
1676
|
+
`${index.label} · ${index.stale ? t("source.stale") : t("source.fetchedAt", { when: index.fetchedAt ?? "-" })}`,
|
|
1677
|
+
)
|
|
1678
|
+
.join(" ")
|
|
1679
|
+
: t("source.refreshHint"),
|
|
1680
|
+
),
|
|
1681
|
+
h(
|
|
1682
|
+
"button",
|
|
1683
|
+
{ type: "button", className: "rstr-btn", onClick: onRefresh, disabled: refreshing === true },
|
|
1684
|
+
refreshing === true ? t("panel.refreshing") : t("source.refresh"),
|
|
1685
|
+
),
|
|
1686
|
+
);
|
|
1687
|
+
|
|
1390
1688
|
if (collections.length === 0) {
|
|
1391
1689
|
return h(
|
|
1392
1690
|
"div",
|
|
1393
|
-
|
|
1691
|
+
{ className: "rstr-sources" },
|
|
1692
|
+
head,
|
|
1394
1693
|
h(EmptyState, { title: t("source.empty"), body: t("source.emptyHint") }),
|
|
1395
1694
|
problems.length > 0 ? h(DiagnosticList, { items: problems }) : null,
|
|
1396
1695
|
);
|
|
@@ -1399,17 +1698,7 @@ window.__ModuleLoader__.load({
|
|
|
1399
1698
|
return h(
|
|
1400
1699
|
"div",
|
|
1401
1700
|
{ className: "rstr-sources" },
|
|
1402
|
-
|
|
1403
|
-
? h(
|
|
1404
|
-
"p",
|
|
1405
|
-
{ className: "rstr-muted rstr-small" },
|
|
1406
|
-
indexes
|
|
1407
|
-
.map((index) =>
|
|
1408
|
-
`${index.label} · ${index.stale ? t("source.stale") : t("source.fetchedAt", { when: index.fetchedAt ?? "-" })}`,
|
|
1409
|
-
)
|
|
1410
|
-
.join(" "),
|
|
1411
|
-
)
|
|
1412
|
-
: null,
|
|
1701
|
+
head,
|
|
1413
1702
|
h(
|
|
1414
1703
|
"div",
|
|
1415
1704
|
{ className: "rstr-cards" },
|
|
@@ -1553,14 +1842,20 @@ window.__ModuleLoader__.load({
|
|
|
1553
1842
|
* 语言之外的配置项只读展示——档案目录与状态目录属于"部署事实",
|
|
1554
1843
|
* 让它们出现在两个地方反而会让人分不清哪个生效。
|
|
1555
1844
|
*/
|
|
1556
|
-
function SettingsSection({ form, library, onOpenPanel }) {
|
|
1845
|
+
function SettingsSection({ form, library, onOpenPanel, onSaved, inModal }) {
|
|
1557
1846
|
useTranslation();
|
|
1558
|
-
const [language, setLanguage] = useState(() =>
|
|
1847
|
+
const [language, setLanguage] = useState(() => readLanguage(form));
|
|
1848
|
+
const [region, setRegion] = useState(() => readFormValue(form, "region", "auto"));
|
|
1849
|
+
const [dirs, setDirs] = useState(() => (library?.data?.config?.rosterDirs ?? []).join(", "));
|
|
1850
|
+
const [saved, setSaved] = useState("");
|
|
1559
1851
|
const [version, setVersion] = useState("");
|
|
1560
1852
|
|
|
1561
1853
|
useEffect(() => {
|
|
1562
1854
|
if (form?.subscribe === undefined) return undefined;
|
|
1563
|
-
const off = form.subscribe(() =>
|
|
1855
|
+
const off = form.subscribe(() => {
|
|
1856
|
+
setLanguage(readLanguage(form));
|
|
1857
|
+
setRegion(readFormValue(form, "region", "auto"));
|
|
1858
|
+
});
|
|
1564
1859
|
return () => {
|
|
1565
1860
|
if (typeof off === "function") off();
|
|
1566
1861
|
};
|
|
@@ -1568,12 +1863,42 @@ window.__ModuleLoader__.load({
|
|
|
1568
1863
|
|
|
1569
1864
|
useEffect(() => {
|
|
1570
1865
|
setVersion(String(library?.data?.version ?? ""));
|
|
1571
|
-
|
|
1866
|
+
setDirs((library?.data?.config?.rosterDirs ?? []).join(", "));
|
|
1867
|
+
}, [library?.data?.version, library?.data?.config?.rosterDirs]);
|
|
1868
|
+
|
|
1869
|
+
useEffect(() => {
|
|
1870
|
+
if (saved === "") return undefined;
|
|
1871
|
+
const timer = setTimeout(() => setSaved(""), 2000);
|
|
1872
|
+
return () => clearTimeout(timer);
|
|
1873
|
+
}, [saved]);
|
|
1572
1874
|
|
|
1573
|
-
function
|
|
1875
|
+
function write(key, value) {
|
|
1876
|
+
try {
|
|
1877
|
+
form?.set?.(key, value);
|
|
1878
|
+
setSaved(t("settings.saved"));
|
|
1879
|
+
} catch {
|
|
1880
|
+
setSaved(t("settings.saveFailed"));
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function changeLanguage(next) {
|
|
1574
1885
|
setLanguage(next);
|
|
1575
|
-
|
|
1576
|
-
|
|
1886
|
+
write("language", next);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function changeRegion(next) {
|
|
1890
|
+
setRegion(next);
|
|
1891
|
+
write("region", next);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function saveDirs() {
|
|
1895
|
+
// 逗号/分号/空白都当分隔符:用户从别处粘路径时不该因为分隔符被卡住。
|
|
1896
|
+
const list = dirs
|
|
1897
|
+
.split(/[,;\n]/)
|
|
1898
|
+
.map((item) => item.trim())
|
|
1899
|
+
.filter((item) => item !== "");
|
|
1900
|
+
write("rosterDirs", list);
|
|
1901
|
+
onSaved?.();
|
|
1577
1902
|
}
|
|
1578
1903
|
|
|
1579
1904
|
return h(
|
|
@@ -1596,7 +1921,7 @@ window.__ModuleLoader__.load({
|
|
|
1596
1921
|
type: "button",
|
|
1597
1922
|
className: `rstr-seg${language === value ? " rstr-seg-on" : ""}`,
|
|
1598
1923
|
"aria-pressed": language === value,
|
|
1599
|
-
onClick: () =>
|
|
1924
|
+
onClick: () => changeLanguage(value),
|
|
1600
1925
|
},
|
|
1601
1926
|
value === "auto" ? t("settings.languageAuto") : value === "zh" ? t("settings.languageZh") : t("settings.languageEn"),
|
|
1602
1927
|
),
|
|
@@ -1606,14 +1931,42 @@ window.__ModuleLoader__.load({
|
|
|
1606
1931
|
h(
|
|
1607
1932
|
"div",
|
|
1608
1933
|
{ className: "rstr-settings-row" },
|
|
1934
|
+
h("span", { className: "rstr-label" }, t("settings.region")),
|
|
1935
|
+
h(
|
|
1936
|
+
"div",
|
|
1937
|
+
{ className: "rstr-segmented" },
|
|
1938
|
+
REGIONS.map((value) =>
|
|
1939
|
+
h(
|
|
1940
|
+
"button",
|
|
1941
|
+
{
|
|
1942
|
+
key: value,
|
|
1943
|
+
type: "button",
|
|
1944
|
+
className: `rstr-seg${region === value ? " rstr-seg-on" : ""}`,
|
|
1945
|
+
"aria-pressed": region === value,
|
|
1946
|
+
onClick: () => changeRegion(value),
|
|
1947
|
+
},
|
|
1948
|
+
value === "auto" ? t("settings.regionAuto") : value === "global" ? t("settings.regionGlobal") : t("settings.regionChina"),
|
|
1949
|
+
),
|
|
1950
|
+
),
|
|
1951
|
+
),
|
|
1952
|
+
),
|
|
1953
|
+
h(
|
|
1954
|
+
"div",
|
|
1955
|
+
{ className: "rstr-settings-field" },
|
|
1609
1956
|
h("span", { className: "rstr-label" }, t("settings.rosterDirs")),
|
|
1610
1957
|
h(
|
|
1611
|
-
"
|
|
1612
|
-
{ className: "rstr-
|
|
1613
|
-
(
|
|
1614
|
-
|
|
1615
|
-
:
|
|
1958
|
+
"div",
|
|
1959
|
+
{ className: "rstr-settings-inline" },
|
|
1960
|
+
h("input", {
|
|
1961
|
+
className: "rstr-input",
|
|
1962
|
+
value: dirs,
|
|
1963
|
+
placeholder: library?.data?.config?.defaultRoot ?? "~/.dsh/agent-roster",
|
|
1964
|
+
"aria-label": t("settings.rosterDirs"),
|
|
1965
|
+
onChange: (event) => setDirs(event.target.value),
|
|
1966
|
+
}),
|
|
1967
|
+
h("button", { type: "button", className: "rstr-btn rstr-btn-sm", onClick: saveDirs }, t("settings.save")),
|
|
1616
1968
|
),
|
|
1969
|
+
h("span", { className: "rstr-muted rstr-small" }, t("settings.rosterDirsHint")),
|
|
1617
1970
|
),
|
|
1618
1971
|
h(
|
|
1619
1972
|
"div",
|
|
@@ -1636,11 +1989,28 @@ window.__ModuleLoader__.load({
|
|
|
1636
1989
|
{ className: "rstr-settings-row" },
|
|
1637
1990
|
h("span", { className: "rstr-label" }, t("settings.version")),
|
|
1638
1991
|
h("code", { className: "rstr-code" }, version),
|
|
1639
|
-
h("button", { type: "button", className: "rstr-btn rstr-btn-sm", onClick: onOpenPanel }, t("settings.openPanel")),
|
|
1992
|
+
inModal === true ? null : h("button", { type: "button", className: "rstr-btn rstr-btn-sm", onClick: onOpenPanel }, t("settings.openPanel")),
|
|
1640
1993
|
),
|
|
1994
|
+
saved === "" ? null : h("p", { className: "rstr-muted rstr-small" }, saved),
|
|
1641
1995
|
);
|
|
1642
1996
|
}
|
|
1643
1997
|
|
|
1998
|
+
/* 通用读表单值:volatile 字段在 getSnapshot().value 里,取不到就用兜底。 */
|
|
1999
|
+
function readFormValue(form, key, fallback) {
|
|
2000
|
+
try {
|
|
2001
|
+
const value = form?.getSnapshot?.()?.value;
|
|
2002
|
+
if (value !== null && typeof value === "object" && typeof value[key] === "string") return value[key];
|
|
2003
|
+
} catch {
|
|
2004
|
+
/* 表单不可用时按兜底值。 */
|
|
2005
|
+
}
|
|
2006
|
+
return fallback;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function readLanguage(form) {
|
|
2010
|
+
const value = readFormValue(form, "language", "auto");
|
|
2011
|
+
return LANGUAGES.includes(value) ? value : "auto";
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1644
2014
|
function readFormLanguage(form) {
|
|
1645
2015
|
let value = "auto";
|
|
1646
2016
|
try {
|
|
@@ -1668,7 +2038,7 @@ window.__ModuleLoader__.load({
|
|
|
1668
2038
|
* 文本色一律显式给出(不用 inherit),避免继承到与背景同色的文本。
|
|
1669
2039
|
*/
|
|
1670
2040
|
const CSS = `
|
|
1671
|
-
.rstr-page { display: flex; flex-direction: column; gap: 16px; padding: 20px 24px 32px;
|
|
2041
|
+
.rstr-page { display: flex; flex-direction: column; gap: 16px; padding: 20px 24px 32px; width: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 1.55; }
|
|
1672
2042
|
.rstr-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
|
|
1673
2043
|
.rstr-h1 { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.01em; color: var(--dsw-alias-label-primary); }
|
|
1674
2044
|
.rstr-muted { color: var(--dsw-alias-label-secondary); margin: 0; }
|
|
@@ -1678,6 +2048,13 @@ window.__ModuleLoader__.load({
|
|
|
1678
2048
|
.rstr-code-strong { font-size: 13px; color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
1679
2049
|
.rstr-truncate { display: inline-block; max-width: 420px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
|
|
1680
2050
|
.rstr-head-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
2051
|
+
.rstr-settings-btn { display: inline-flex; align-items: center; gap: 6px; }
|
|
2052
|
+
.rstr-settings-glyph { font-size: 14px; line-height: 1; }
|
|
2053
|
+
.rstr-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
2054
|
+
.rstr-toolbar-actions { display: flex; gap: 8px; margin-left: auto; }
|
|
2055
|
+
.rstr-sources-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
2056
|
+
.rstr-settings { display: flex; flex-direction: column; gap: 10px; }
|
|
2057
|
+
.rstr-settings-title { margin: 0; font-size: 14px; font-weight: 600; }
|
|
1681
2058
|
|
|
1682
2059
|
.rstr-btn { border: 1px solid var(--dsw-alias-border-l3); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); border-radius: 8px; padding: 6px 12px; font-size: 13px; font-family: inherit; cursor: pointer; transition: background .12s ease, border-color .12s ease; }
|
|
1683
2060
|
.rstr-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
@@ -1708,7 +2085,19 @@ window.__ModuleLoader__.load({
|
|
|
1708
2085
|
.rstr-tab-on { color: var(--dsw-alias-label-primary); border-bottom-color: var(--dsw-alias-state-business-primary); font-weight: 600; }
|
|
1709
2086
|
.rstr-tab-count { font-size: 11px; padding: 0 6px; border-radius: 999px; background: var(--dsw-alias-button-ghost-active-fill); color: var(--dsw-alias-label-secondary); }
|
|
1710
2087
|
|
|
1711
|
-
.rstr-
|
|
2088
|
+
.rstr-rosters { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
2089
|
+
@media (max-width: 860px) { .rstr-rosters { grid-template-columns: minmax(0, 1fr); } }
|
|
2090
|
+
.rstr-tagpool { display: flex; flex-direction: column; gap: 8px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; background: var(--dsw-alias-bg-layer-2); position: sticky; top: 12px; }
|
|
2091
|
+
.rstr-tagpool-title { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
2092
|
+
.rstr-tagpool-list { display: flex; flex-direction: column; gap: 4px; }
|
|
2093
|
+
.rstr-tag { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; border: 1px solid transparent; background: transparent; color: var(--dsw-alias-label-secondary); border-radius: 6px; padding: 4px 8px; font-size: 12px; font-family: inherit; cursor: pointer; text-align: left; }
|
|
2094
|
+
.rstr-tag:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
2095
|
+
.rstr-tag:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 1px; }
|
|
2096
|
+
.rstr-tag-on { background: var(--dsw-alias-button-ghost-active-fill); border-color: var(--dsw-alias-border-l3); color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
2097
|
+
.rstr-tag-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2098
|
+
.rstr-tag-count { font-size: 11px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
|
|
2099
|
+
.rstr-tag-clear { align-self: flex-start; }
|
|
2100
|
+
.rstr-cards { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); align-items: start; }
|
|
1712
2101
|
.rstr-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; padding: 14px 16px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); display: flex; flex-direction: column; gap: 10px; }
|
|
1713
2102
|
.rstr-card-bad { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent); }
|
|
1714
2103
|
.rstr-card-id { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
@@ -1768,6 +2157,13 @@ window.__ModuleLoader__.load({
|
|
|
1768
2157
|
|
|
1769
2158
|
.rstr-editor-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
1770
2159
|
.rstr-editor-toolbar .rstr-input { width: 110px; }
|
|
2160
|
+
.rstr-captain { display: flex; flex-direction: column; gap: 8px; border-top: 1px dashed var(--dsw-alias-border-l3); margin-top: 12px; padding-top: 10px; }
|
|
2161
|
+
.rstr-meta-grid { display: grid; grid-template-columns: auto repeat(4, minmax(0, auto)); gap: 6px 12px; align-items: baseline; font-size: 12px; }
|
|
2162
|
+
.rstr-checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
2163
|
+
.rstr-check { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); padding-bottom: 3px; }
|
|
2164
|
+
.rstr-apply { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
2165
|
+
.rstr-select { background: var(--dsw-alias-bg-l2); color: var(--dsw-alias-label-primary); border: 1px solid var(--dsw-alias-border-l3); border-radius: 8px; padding: 5px 8px; font-size: 12px; max-width: 320px; }
|
|
2166
|
+
.rstr-bad { color: var(--dsw-alias-state-error-primary); }
|
|
1771
2167
|
.rstr-segmented { display: inline-flex; border: 1px solid var(--dsw-alias-border-l3); border-radius: 8px; overflow: hidden; }
|
|
1772
2168
|
.rstr-seg { border: none; background: transparent; padding: 5px 12px; font-size: 12px; cursor: pointer; color: var(--dsw-alias-label-secondary); }
|
|
1773
2169
|
.rstr-seg:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
@@ -1786,10 +2182,11 @@ window.__ModuleLoader__.load({
|
|
|
1786
2182
|
|
|
1787
2183
|
.rstr-foot { display: flex; justify-content: space-between; gap: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 10px; margin-top: 4px; color: var(--dsw-alias-label-secondary); }
|
|
1788
2184
|
|
|
1789
|
-
.rstr-settings { display: flex; flex-direction: column; gap: 10px; }
|
|
1790
|
-
.rstr-settings-title { margin: 0; font-size: 14px; font-weight: 600; }
|
|
1791
2185
|
.rstr-settings-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
1792
2186
|
.rstr-settings-row .rstr-label { min-width: 120px; }
|
|
2187
|
+
.rstr-settings-field { display: flex; flex-direction: column; gap: 6px; }
|
|
2188
|
+
.rstr-settings-inline { display: flex; align-items: center; gap: 8px; }
|
|
2189
|
+
.rstr-settings-inline .rstr-input { flex: 1 1 auto; min-width: 0; }
|
|
1793
2190
|
`;
|
|
1794
2191
|
|
|
1795
2192
|
function ensureStyle() {
|
|
@@ -1889,7 +2286,8 @@ window.__ModuleLoader__.load({
|
|
|
1889
2286
|
"agent-roster: language sync",
|
|
1890
2287
|
);
|
|
1891
2288
|
|
|
1892
|
-
|
|
2289
|
+
HOST.form = form;
|
|
2290
|
+
HOST.openPanel = () => {
|
|
1893
2291
|
try {
|
|
1894
2292
|
layout?.selectPanel?.(PANEL_ID);
|
|
1895
2293
|
} catch {
|
|
@@ -1938,7 +2336,7 @@ window.__ModuleLoader__.load({
|
|
|
1938
2336
|
{ name: "settings.section", id: PANEL_ID, order: 55, locale: NS, label: () => t("settings.nav") },
|
|
1939
2337
|
function AgentRosterSettings() {
|
|
1940
2338
|
const library = useLibrary();
|
|
1941
|
-
return h(SettingsSection, { form, library, onOpenPanel: openPanel });
|
|
2339
|
+
return h(SettingsSection, { form, library, onOpenPanel: HOST.openPanel, inModal: false });
|
|
1942
2340
|
},
|
|
1943
2341
|
),
|
|
1944
2342
|
);
|