dsh-skill-tag 0.1.12 → 0.1.13
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/lib/client.js +121 -0
- package/lib/index.js +8 -0
- package/package.json +1 -1
- package/src/api.js +66 -3
- package/src/service.js +48 -0
package/lib/client.js
CHANGED
|
@@ -1309,6 +1309,98 @@ window.__ModuleLoader__.load({
|
|
|
1309
1309
|
const listRow = { display: "flex", alignItems: "center", gap: "8px", padding: "5px 2px", borderBottom: "1px solid var(--theme-border,#222)", fontSize: "12px" };
|
|
1310
1310
|
const monoStyle = { fontFamily: "ui-monospace,monospace" };
|
|
1311
1311
|
|
|
1312
|
+
// ── 「一键下发」确认框(工作区版专用) ───────────────────────────
|
|
1313
|
+
//
|
|
1314
|
+
// 两步式(preview → push),因为这是**批量破坏性**操作:
|
|
1315
|
+
// · 打开时先 preview,看清"未配置 N 个 / 已配置 M 个"再决定;
|
|
1316
|
+
// · 默认只下发未配置的(overwrite=false);
|
|
1317
|
+
// · 要覆盖已配置的必须勾选复选框(overwrite=true);
|
|
1318
|
+
// · 工作区为空时明确警告「下发后这些对话将掌握全部技能」。
|
|
1319
|
+
function PushToConversationsDialog({ workspaceId, workspaceEmpty, busy, onClose, onDone }) {
|
|
1320
|
+
const [loading, setLoading] = react.useState(true);
|
|
1321
|
+
const [info, setInfo] = react.useState(null);
|
|
1322
|
+
const [err, setErr] = react.useState(null);
|
|
1323
|
+
const [overwrite, setOverwrite] = react.useState(false);
|
|
1324
|
+
const [running, setRunning] = react.useState(false);
|
|
1325
|
+
|
|
1326
|
+
react.useEffect(() => {
|
|
1327
|
+
let alive = true;
|
|
1328
|
+
(async () => {
|
|
1329
|
+
try {
|
|
1330
|
+
const d = await apiPost("/workspaces/push-preview", { workspaceId });
|
|
1331
|
+
if (!d?.ok) throw new Error(d?.error ?? "预览失败");
|
|
1332
|
+
if (alive) setInfo(d);
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
if (alive) setErr(String(e?.message ?? e));
|
|
1335
|
+
} finally {
|
|
1336
|
+
if (alive) setLoading(false);
|
|
1337
|
+
}
|
|
1338
|
+
})();
|
|
1339
|
+
return () => { alive = false; };
|
|
1340
|
+
}, [workspaceId]);
|
|
1341
|
+
|
|
1342
|
+
const fresh = info?.fresh ?? [];
|
|
1343
|
+
const configured = info?.configured ?? [];
|
|
1344
|
+
const targetCount = fresh.length + (overwrite ? configured.length : 0);
|
|
1345
|
+
const isEmpty = info?.workspaceEmpty ?? workspaceEmpty;
|
|
1346
|
+
|
|
1347
|
+
const run = async () => {
|
|
1348
|
+
setRunning(true);
|
|
1349
|
+
try {
|
|
1350
|
+
const d = await apiPost("/workspaces/push", { workspaceId, overwrite });
|
|
1351
|
+
if (!d?.ok) throw new Error(d?.error ?? "下发失败");
|
|
1352
|
+
onDone(d);
|
|
1353
|
+
} catch (e) {
|
|
1354
|
+
setErr(String(e?.message ?? e));
|
|
1355
|
+
} finally {
|
|
1356
|
+
setRunning(false);
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
const rowStyle = { display: "flex", gap: "8px", alignItems: "center", padding: "4px 0", fontSize: "12px" };
|
|
1361
|
+
return react.createElement("div", { style: overlayStyle, onMouseDown: (e) => { if (e.target === e.currentTarget) onClose(); } },
|
|
1362
|
+
react.createElement("div", { style: { ...modalBox, width: "600px" }, onClick: (e) => e.stopPropagation() }, [
|
|
1363
|
+
react.createElement("div", { key: "h", style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "10px" } }, [
|
|
1364
|
+
react.createElement("strong", { key: "t" }, "下发到该工作区下的所有对话"),
|
|
1365
|
+
react.createElement("button", { key: "x", style: btn, onClick: onClose }, "✕"),
|
|
1366
|
+
]),
|
|
1367
|
+
loading && react.createElement("div", { key: "l", style: { color: "#888", padding: "14px 0" } }, "正在统计受影响的对话…"),
|
|
1368
|
+
err && react.createElement("div", { key: "e", style: { color: "#e74c3c", padding: "8px 0" } }, err),
|
|
1369
|
+
!loading && !err && info && react.createElement(react.Fragment, { key: "body" }, [
|
|
1370
|
+
// 工作区为空时:必须让用户看见"这等于全部技能"
|
|
1371
|
+
isEmpty && react.createElement("div", { key: "warn", style: { background: "rgba(230,126,34,0.14)", border: "1px solid rgba(230,126,34,0.45)", borderRadius: "8px", padding: "8px 10px", marginBottom: "10px", color: "#e67e22", fontSize: "12px" } },
|
|
1372
|
+
"⚠ 当前工作区的标签条件为空、且没有手动添加/禁用的技能。下发后这些对话会变成「空条件 = 掌握全部技能」。"),
|
|
1373
|
+
react.createElement("div", { key: "sum", style: { fontSize: "12px", marginBottom: "8px", lineHeight: 1.7 } }, [
|
|
1374
|
+
react.createElement("div", { key: "a" }, `该工作区下共 ${info.total ?? 0} 个对话`),
|
|
1375
|
+
react.createElement("div", { key: "b", style: { color: "#2ecc71" } }, `· 从未配置过:${fresh.length} 个(默认下发)`),
|
|
1376
|
+
react.createElement("div", { key: "c", style: { color: "#888" } }, `· 已有自己的配置:${configured.length} 个(默认跳过)`),
|
|
1377
|
+
]),
|
|
1378
|
+
configured.length > 0 && react.createElement("label", { key: "ow", style: { ...rowStyle, cursor: "pointer", padding: "6px 0", borderTop: "1px solid var(--theme-border,#222)" } }, [
|
|
1379
|
+
react.createElement("input", {
|
|
1380
|
+
key: "c", type: "checkbox", checked: overwrite,
|
|
1381
|
+
"data-skill-tag-push-overwrite": "1",
|
|
1382
|
+
onChange: (e) => setOverwrite(e.target.checked),
|
|
1383
|
+
}),
|
|
1384
|
+
react.createElement("span", { key: "t", style: { color: "#e67e22" } },
|
|
1385
|
+
`同时覆盖那 ${configured.length} 个已有配置的对话(会抹掉它们各自的调整)`),
|
|
1386
|
+
]),
|
|
1387
|
+
react.createElement("div", { key: "cnt", style: { fontSize: "12px", color: "#888", marginTop: "8px" } },
|
|
1388
|
+
targetCount === 0
|
|
1389
|
+
? "没有需要下发的对话。勾选上面的复选框可以覆盖已配置的对话。"
|
|
1390
|
+
: `将实际写入 ${targetCount} 个对话`),
|
|
1391
|
+
]),
|
|
1392
|
+
react.createElement("div", { key: "f", style: { display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "14px" } }, [
|
|
1393
|
+
react.createElement("button", { key: "c", style: btn, disabled: running, onClick: onClose }, "取消"),
|
|
1394
|
+
react.createElement("button", {
|
|
1395
|
+
key: "go", style: { ...btn, background: "#e67e22", color: "#fff", borderColor: "#e67e22" },
|
|
1396
|
+
disabled: running || busy || loading || !!err || targetCount === 0,
|
|
1397
|
+
"data-skill-tag-push-confirm": "1",
|
|
1398
|
+
onClick: () => void run(),
|
|
1399
|
+
}, running ? "下发中…" : `确认下发(${targetCount})`),
|
|
1400
|
+
]),
|
|
1401
|
+
]),
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1312
1404
|
// ── 双列主界面(工作区版 / 对话版共用) ─────────────────────────
|
|
1313
1405
|
//
|
|
1314
1406
|
// 两个作用域的唯一差别是四组端点 + 一个「继承工作区配置」按钮:
|
|
@@ -1355,6 +1447,8 @@ window.__ModuleLoader__.load({
|
|
|
1355
1447
|
const [pickOpen, setPickOpen] = react.useState(false);
|
|
1356
1448
|
const [disabledOpen, setDisabledOpen] = react.useState(false);
|
|
1357
1449
|
const [tagOfSkill, setTagOfSkill] = react.useState(null);
|
|
1450
|
+
/** 「一键下发」确认框(只在工作区版用) */
|
|
1451
|
+
const [pushOpen, setPushOpen] = react.useState(false);
|
|
1358
1452
|
/** 右列技能搜索词(对话版 / 工作区版共用;只影响列表显示,不影响下发结果) */
|
|
1359
1453
|
const [skillSearch, setSkillSearch] = react.useState("");
|
|
1360
1454
|
|
|
@@ -1472,6 +1566,14 @@ window.__ModuleLoader__.load({
|
|
|
1472
1566
|
key: "set", style: btn, disabled: busy,
|
|
1473
1567
|
onClick: () => setSettingsOpen(true),
|
|
1474
1568
|
}, "tag 设置"),
|
|
1569
|
+
// 「一键下发」只在工作区版出现:把当前工作区配置推给该工作区下所有对话。
|
|
1570
|
+
// 与对话版的「继承」是同一个数据动作,区别是批量 + 带选择性覆盖。
|
|
1571
|
+
!scope.isConv && react.createElement("button", {
|
|
1572
|
+
key: "push", style: { ...btn, color: "#e67e22" }, disabled: busy,
|
|
1573
|
+
title: "把本工作区的标签条件 / 手动添加 / 禁用一次性下发到该工作区下的所有对话",
|
|
1574
|
+
"data-skill-tag-push": "1",
|
|
1575
|
+
onClick: () => setPushOpen(true),
|
|
1576
|
+
}, "下发到所有对话"),
|
|
1475
1577
|
// 「继承工作区配置」只在对话版出现:把当前工作区的三件套整体搬过来
|
|
1476
1578
|
scope.isConv && react.createElement("button", {
|
|
1477
1579
|
key: "inh", style: { ...btn, color: "#3498db" }, disabled: busy,
|
|
@@ -1573,6 +1675,25 @@ window.__ModuleLoader__.load({
|
|
|
1573
1675
|
},
|
|
1574
1676
|
}),
|
|
1575
1677
|
|
|
1678
|
+
// ── 弹窗:一键下发到所有对话(工作区版) ──
|
|
1679
|
+
!scope.isConv && pushOpen && react.createElement(PushToConversationsDialog, {
|
|
1680
|
+
key: "push",
|
|
1681
|
+
workspaceId: scope.id,
|
|
1682
|
+
workspaceEmpty: !view.hasFilter && (ws?.manualSkills ?? []).length === 0 && (ws?.disabledSkills ?? []).length === 0,
|
|
1683
|
+
busy,
|
|
1684
|
+
onClose: () => setPushOpen(false),
|
|
1685
|
+
onDone: (d) => {
|
|
1686
|
+
setPushOpen(false);
|
|
1687
|
+
// 服务端返回的是 id 数组(updated / overwritten),界面只关心条数
|
|
1688
|
+
const n = (v) => (Array.isArray(v) ? v.length : (typeof v === "number" ? v : 0));
|
|
1689
|
+
const parts = ["已下发 " + n(d.updated) + " 个对话"];
|
|
1690
|
+
if (n(d.overwritten)) parts.push("其中覆盖 " + n(d.overwritten) + " 个");
|
|
1691
|
+
if (n(d.skipped)) parts.push("跳过 " + n(d.skipped) + " 个已有配置的");
|
|
1692
|
+
setNotice(parts.join(","));
|
|
1693
|
+
void load();
|
|
1694
|
+
},
|
|
1695
|
+
}),
|
|
1696
|
+
|
|
1576
1697
|
// ── 弹窗:被禁用的技能 ──
|
|
1577
1698
|
disabledOpen && react.createElement(DisabledSkillsDialog, {
|
|
1578
1699
|
key: "dis",
|
package/lib/index.js
CHANGED
|
@@ -952,6 +952,14 @@ export async function apply(ctx, config) {
|
|
|
952
952
|
const entity = registry?.get?.(workspaceId);
|
|
953
953
|
return entity?.path ?? null;
|
|
954
954
|
},
|
|
955
|
+
// 工作区 id → 该工作区下的会话 id 列表(「一键下发」要知道发给谁)。
|
|
956
|
+
// 宿主把归属记在 workspace entity 的 sessionIds 上(dsh-workspace)。
|
|
957
|
+
(workspaceId) => {
|
|
958
|
+
const registry = getWorkspaceRegistry(ctx);
|
|
959
|
+
const entity = registry?.get?.(workspaceId);
|
|
960
|
+
const ids = entity?.sessionIds;
|
|
961
|
+
return Array.isArray(ids) ? [...ids] : [];
|
|
962
|
+
},
|
|
955
963
|
);
|
|
956
964
|
} else {
|
|
957
965
|
ctx.logger?.warn?.("[skill-tag] webServer missing — settings API not mounted");
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -33,8 +33,11 @@ function wsShape(ws) {
|
|
|
33
33
|
* @param {((workspaceId: string) => string|null) | null} [resolveWorkspacePath]
|
|
34
34
|
* 工作区 id → 目录路径(宿主 workspaceRegistry entity.path)。用于把
|
|
35
35
|
* 项目级技能根目录扫进来;为 null 时只能用插件自己记录过的 path。
|
|
36
|
+
* @param {((workspaceId: string) => string[]) | null} [resolveWorkspaceSessions]
|
|
37
|
+
* 工作区 id → 该工作区下的会话 id 列表(宿主 workspaceRegistry entity.sessionIds)。
|
|
38
|
+
* 「一键下发」要知道发给谁;为 null 时退回对话自己记录的 sourceWorkspaceId。
|
|
36
39
|
*/
|
|
37
|
-
export function createApiHandler(services, log, listSkills = null, resolveWorkspaceId = null, resolveWorkspacePath = null) {
|
|
40
|
+
export function createApiHandler(services, log, listSkills = null, resolveWorkspaceId = null, resolveWorkspacePath = null, resolveWorkspaceSessions = null) {
|
|
38
41
|
const json = (res, code, obj) => {
|
|
39
42
|
res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
|
|
40
43
|
res.end(JSON.stringify(obj));
|
|
@@ -205,6 +208,20 @@ export function createApiHandler(services, log, listSkills = null, resolveWorksp
|
|
|
205
208
|
};
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
/**
|
|
212
|
+
* 工作区配置是否"全空"(无条件 + 无手动 + 无禁用)。
|
|
213
|
+
* 下发全空配置 = 让对话掌握全部技能,必须在界面上明确警告。
|
|
214
|
+
*/
|
|
215
|
+
function isEmptyWorkspaceConfig(ws) {
|
|
216
|
+
if (!ws) return true;
|
|
217
|
+
const ors = (ws.config?.orTags ?? []).length;
|
|
218
|
+
const groups = (ws.config?.andGroups ?? []).length;
|
|
219
|
+
const expr = String(ws.expression ?? "").trim();
|
|
220
|
+
return ors === 0 && groups === 0 && expr === ""
|
|
221
|
+
&& (ws.manualSkills ?? []).length === 0
|
|
222
|
+
&& (ws.disabledSkills ?? []).length === 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
208
225
|
/** 对话绑定到哪个工作区(优先宿主 registry,其次对话自己记录的)。 */
|
|
209
226
|
function workspaceIdForConversation(conversationId) {
|
|
210
227
|
const conv = services.conversations.get(conversationId);
|
|
@@ -503,6 +520,52 @@ export function createApiHandler(services, log, listSkills = null, resolveWorksp
|
|
|
503
520
|
});
|
|
504
521
|
}
|
|
505
522
|
|
|
523
|
+
// ── 「一键下发工作区配置到该工作区下所有对话」 ────────────────
|
|
524
|
+
//
|
|
525
|
+
// 动机:工作区配置改完,散落的对话各自为政。逐个点「继承」太麻烦,
|
|
526
|
+
// 于是提供一个批量下发。但**批量破坏性操作必须有护栏**:
|
|
527
|
+
// · preview(GET/POST /workspaces/push-preview)只试算,不写;
|
|
528
|
+
// · push 默认 overwrite=false,只填「从未配置过」的对话;
|
|
529
|
+
// · 已配置的对话必须显式 overwrite=true 才覆盖(界面二次勾选确认)。
|
|
530
|
+
//
|
|
531
|
+
// 这是 0.1.9 那轮修复的直接延续:用户改的东西不能被无差别覆盖。
|
|
532
|
+
if ((path === "/workspaces/push-preview" || path === "/workspaces/push") && (method === "GET" || method === "POST")) {
|
|
533
|
+
const wid = q("workspaceId");
|
|
534
|
+
if (!wid) return json(res, 400, { ok: false, error: "缺少 workspaceId" });
|
|
535
|
+
// 该工作区下的对话:宿主 sessionIds 是权威来源。
|
|
536
|
+
// resolveWorkspaceSessions 没注入时退回对话自己记录的 sourceWorkspaceId。
|
|
537
|
+
let ids = [];
|
|
538
|
+
if (typeof resolveWorkspaceSessions === "function") {
|
|
539
|
+
try { ids = resolveWorkspaceSessions(wid) ?? []; } catch { ids = []; }
|
|
540
|
+
}
|
|
541
|
+
if (!Array.isArray(ids)) ids = [];
|
|
542
|
+
// 并入已存在的 own.sourceWorkspaceId 指向本工作区的对话(宿主列表可能不含历史会话)
|
|
543
|
+
for (const c of services._caches.conversations.values()) {
|
|
544
|
+
if (c?.own?.sourceWorkspaceId === wid) ids.push(c.conversationId);
|
|
545
|
+
}
|
|
546
|
+
ids = [...new Set(ids.filter(Boolean))];
|
|
547
|
+
|
|
548
|
+
if (path === "/workspaces/push-preview") {
|
|
549
|
+
const preview = services.conversations.previewPushWorkspace(ids);
|
|
550
|
+
const ws = services.workspaces.get(wid);
|
|
551
|
+
return json(res, 200, {
|
|
552
|
+
ok: true,
|
|
553
|
+
workspaceId: wid,
|
|
554
|
+
...preview,
|
|
555
|
+
// 工作区当前是否为空(空 = 下发等于"掌握全部技能",界面要明确警告)
|
|
556
|
+
workspaceEmpty: isEmptyWorkspaceConfig(ws),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const overwrite = body?.overwrite === true;
|
|
561
|
+
try {
|
|
562
|
+
const out = await services.conversations.pushWorkspaceToConversations(wid, ids, { overwrite });
|
|
563
|
+
return json(res, 200, { ok: true, workspaceId: wid, ...out });
|
|
564
|
+
} catch (error) {
|
|
565
|
+
return json(res, 400, { ok: false, error: String(error?.message ?? error) });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
506
569
|
// ── 「对话 → 管理标签」双列界面 ────────────────────────────────
|
|
507
570
|
// 对话拥有**自己的**配置快照(own):标签条件 + 手动添加 + 禁用。
|
|
508
571
|
// 工作区配置不会自动流入对话,除非:新建对话(自动继承一次)或
|
|
@@ -564,13 +627,13 @@ export function createApiHandler(services, log, listSkills = null, resolveWorksp
|
|
|
564
627
|
}
|
|
565
628
|
|
|
566
629
|
/** Mount the handler on a cordis ctx (inject webServer). */
|
|
567
|
-
export function mountApi(ctx, services, listSkills = null, resolveWorkspaceId = null, resolveWorkspacePath = null) {
|
|
630
|
+
export function mountApi(ctx, services, listSkills = null, resolveWorkspaceId = null, resolveWorkspacePath = null, resolveWorkspaceSessions = null) {
|
|
568
631
|
ctx.effect(
|
|
569
632
|
() =>
|
|
570
633
|
ctx.webServer.register({
|
|
571
634
|
kind: "prefix",
|
|
572
635
|
path: "/skill-tag/api",
|
|
573
|
-
handler: createApiHandler(services, ctx.logger, listSkills, resolveWorkspaceId, resolveWorkspacePath),
|
|
636
|
+
handler: createApiHandler(services, ctx.logger, listSkills, resolveWorkspaceId, resolveWorkspacePath, resolveWorkspaceSessions),
|
|
574
637
|
}),
|
|
575
638
|
"skill-tag: api",
|
|
576
639
|
);
|
package/src/service.js
CHANGED
|
@@ -701,6 +701,54 @@ export function createServices(repo, opts = {}) {
|
|
|
701
701
|
return Boolean(conversations.get(conversationId)?.own);
|
|
702
702
|
},
|
|
703
703
|
|
|
704
|
+
/**
|
|
705
|
+
* 「一键下发到工作区下所有对话」:把工作区当前三件套批量写进一批对话的 own。
|
|
706
|
+
*
|
|
707
|
+
* 与 inheritFromWorkspace 的区别是**批量**且带**选择性覆盖**:
|
|
708
|
+
* overwrite=false(默认)→ 只填 own 为 null 的对话(从未配置过);
|
|
709
|
+
* overwrite=true → 连已配置的也一起覆盖(界面要求二次确认)。
|
|
710
|
+
*
|
|
711
|
+
* 为什么默认不覆盖:已配置的对话要么是出生时继承的快照,要么是用户手工
|
|
712
|
+
* 调过的。无差别覆盖会抹掉用户意志 —— 0.1.9 那次修复的核心教训。
|
|
713
|
+
*
|
|
714
|
+
* @param {string} workspaceId
|
|
715
|
+
* @param {string[]} conversationIds 该工作区下的对话(由宿主 sessionIds 解析)
|
|
716
|
+
* @param {{overwrite?: boolean}} [opts]
|
|
717
|
+
* @returns {Promise<{updated: string[], skipped: string[], overwritten: string[]}>}
|
|
718
|
+
*/
|
|
719
|
+
async pushWorkspaceToConversations(workspaceId, conversationIds, opts = {}) {
|
|
720
|
+
const overwrite = opts.overwrite === true;
|
|
721
|
+
const ws = workspaces.get(workspaceId);
|
|
722
|
+
if (!ws) throw new Error(`workspace '${workspaceId}' not found`);
|
|
723
|
+
const updated = [];
|
|
724
|
+
const skipped = [];
|
|
725
|
+
const overwritten = [];
|
|
726
|
+
for (const cid of [...new Set(conversationIds ?? [])].filter(Boolean)) {
|
|
727
|
+
const conv = await this.ensureOwn(cid);
|
|
728
|
+
const hadOwn = Boolean(conv.own);
|
|
729
|
+
if (hadOwn && !overwrite) { skipped.push(cid); continue; }
|
|
730
|
+
conv.own = ownConfigFromWorkspace(ws);
|
|
731
|
+
if (!conv.inherited.workspaceId) conv.inherited.workspaceId = ws.workspaceId;
|
|
732
|
+
await repo.putConversation(serializeConversation(conv));
|
|
733
|
+
updated.push(cid);
|
|
734
|
+
if (hadOwn) overwritten.push(cid);
|
|
735
|
+
}
|
|
736
|
+
return { updated, skipped, overwritten };
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 试算「一键下发」会影响哪些对话(不写任何数据)。
|
|
741
|
+
* 界面在确认框里展示这个结果,用户看清范围再决定。
|
|
742
|
+
*/
|
|
743
|
+
previewPushWorkspace(conversationIds) {
|
|
744
|
+
const fresh = [];
|
|
745
|
+
const configured = [];
|
|
746
|
+
for (const cid of [...new Set(conversationIds ?? [])].filter(Boolean)) {
|
|
747
|
+
(this.isConfigured(cid) ? configured : fresh).push(cid);
|
|
748
|
+
}
|
|
749
|
+
return { fresh, configured, total: fresh.length + configured.length };
|
|
750
|
+
},
|
|
751
|
+
|
|
704
752
|
/** 对话自己的配置快照(只读视图;保证字段完整) */
|
|
705
753
|
own(conversationId) {
|
|
706
754
|
const conv = conversations.get(conversationId);
|