dsh-armory 1.9.3 → 1.10.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.
@@ -27,9 +27,9 @@
27
27
  // - 技能落盘目标与 SkillHub 技能同一发现范围 ~/.agents/skills/,目录名带 wbexpert__ 前缀
28
28
  // 防与其它来源撞名;本地缓存复制整树,CDN 兜底只写 SKILL.md 并如实标注。
29
29
  // - patch 写入用 yaml 文档 API(与 mcp-config.js 同款 !!js 标量约定),保留其余条目与注释。
30
- import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
30
+ import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
31
31
  import { homedir } from "node:os";
32
- import { basename, join, resolve, sep } from "node:path";
32
+ import { basename, join } from "node:path";
33
33
  import { parseDocument, Scalar, YAMLMap, YAMLSeq, isMap, isSeq } from "yaml";
34
34
 
35
35
  const EXPERT_ORIGIN = "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/expert-marketplace";
@@ -60,6 +60,14 @@ function skillsRoot() {
60
60
  function workbuddyExpertCache() {
61
61
  return join(homedir(), ".workbuddy", "plugins", "cache", "experts");
62
62
  }
63
+ // 团队专家:主理人 + N 个成员,每人一份人设。成员人设不塞进 preset(会白白撑大每轮
64
+ // 系统提示),而是落盘成独立 md,由主理人调度时按需 read 进 subagent 的 prompt。
65
+ function teamMemberDir(plugin) {
66
+ return join(cacheDir(), "teams", safeSegment(plugin));
67
+ }
68
+ function teamMemberPersonaPath(plugin, memberId) {
69
+ return join(teamMemberDir(plugin), `${safeSegment(memberId)}.md`);
70
+ }
63
71
  function safeSegment(value) {
64
72
  // 目录名防穿越:只留安全字符,与 wb-market.js 的 skillTargetName 同一纪律。
65
73
  const safe = String(value).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
@@ -143,15 +151,33 @@ async function loadManifest() {
143
151
  function bi(value) {
144
152
  return value && typeof value === "object" ? String(value.zh || value.en || "") : String(value || "");
145
153
  }
154
+ function biMember(value) {
155
+ return value && typeof value === "object" ? String(value.zh || value.en || "") : String(value || "");
156
+ }
157
+ /** 团队编制里的一行:成员 ID / 花名 / 职业 / 角色 / 人设文件名。 */
158
+ function projectMember(member) {
159
+ const id = String(member?.id || "").trim();
160
+ if (!id) return null;
161
+ return {
162
+ id,
163
+ name: biMember(member.displayName || member.name),
164
+ profession: biMember(member.profession),
165
+ role: String(member.role || "member"),
166
+ promptFile: String(member.promptFile || ""),
167
+ avatar: String(member.avatar || ""),
168
+ };
169
+ }
146
170
  function projectExpert(entry, categoryById) {
147
171
  const plugin = String(entry.plugin || "").trim();
148
172
  if (!plugin) return null;
149
173
  const category = categoryById.get(entry.categoryId);
174
+ const members = Array.isArray(entry.members) ? entry.members.map(projectMember).filter(Boolean) : [];
150
175
  return {
151
176
  id: String(entry.id || plugin),
152
177
  plugin,
153
178
  agentName: String(entry.agentName || ""),
154
179
  expertType: entry.expertType === "team" ? "team" : "agent",
180
+ members,
155
181
  name: bi(entry.displayName) || plugin,
156
182
  profession: bi(entry.profession),
157
183
  description: bi(entry.description),
@@ -298,6 +324,120 @@ export async function installExpertSkills(plugin, skillNames, { onLog } = {}) {
298
324
  }
299
325
  return { installed, notes };
300
326
  }
327
+ // ── 团队专家:成员人设落盘(召唤时执行,送还时整体清除)─────────────────
328
+ async function localMemberPersona(plugin, memberId) {
329
+ for (const version of await localExpertVersions(plugin)) {
330
+ const candidate = join(version.dir, "agents", `${memberId}.md`);
331
+ try {
332
+ const text = await readFile(candidate, "utf8");
333
+ if (text.trim()) return text;
334
+ } catch { /* 该版本没有这个成员 */ }
335
+ }
336
+ return null;
337
+ }
338
+ async function readMemberPersona(plugin, member) {
339
+ const local = await localMemberPersona(plugin, member.id);
340
+ if (local) return { text: local, source: "workbuddy-cache" };
341
+ const promptFile = String(member.promptFile || `/plugins/${plugin}/agents/${member.id}.md`).trim();
342
+ const text = await fetchText(EXPERT_ORIGIN + promptFile);
343
+ if (!text.trim()) throw new Error("人设文件为空");
344
+ return { text, source: "cdn" };
345
+ }
346
+ /**
347
+ * 把团队成员的独立人设写成本机 md(不进 preset:5 份人设塞进系统提示会按轮计费)。
348
+ * 主理人调度时先 read 出来再拼进 subagent 的 prompt,语义等同 WorkBuddy 的独立协作方。
349
+ * @returns {Promise<{installed: Array, notes: string[], dir: string}>}
350
+ */
351
+ export async function installTeamMemberPersonas(plugin, members, { onLog } = {}) {
352
+ const installed = [];
353
+ const notes = [];
354
+ const dir = teamMemberDir(plugin);
355
+ for (const member of Array.isArray(members) ? members : []) {
356
+ // 主理人人设本身就是召唤预设的 persona,不需要再落一份。
357
+ if (member.role === "lead") continue;
358
+ try {
359
+ const { text, source } = await readMemberPersona(plugin, member);
360
+ await mkdir(dir, { recursive: true });
361
+ const path = teamMemberPersonaPath(plugin, member.id);
362
+ await writeFile(path, text);
363
+ installed.push({ id: member.id, name: member.name, profession: member.profession, path, source });
364
+ onLog?.(`团队成员 ${member.name || member.id}:人设已落盘(${source})`);
365
+ } catch (error) {
366
+ notes.push(`团队成员 ${member.name || member.id} 人设获取失败:${error.message}`);
367
+ }
368
+ }
369
+ return { installed, notes, dir };
370
+ }
371
+ export async function uninstallTeamMemberPersonas(plugin) {
372
+ const dir = teamMemberDir(plugin);
373
+ try {
374
+ await rm(dir, { recursive: true, force: true });
375
+ return dir;
376
+ } catch {
377
+ return null;
378
+ }
379
+ }
380
+ /**
381
+ * DSH 运行时桥接段:WorkBuddy 团队人设里的 TeamCreate / Agent(name, subagent_type)
382
+ * 在 DSH 里不存在,这里给出等价映射。追加在主理人人设末尾,只在召唤时拼一次。
383
+ * @param {object} detail wbExpertDetail 的返回值
384
+ * @param {Map<string,string>} personaFiles 成员 ID → 已落盘的人设路径(未落盘则不传)
385
+ */
386
+ // 桥接段的唯一识别标记:readExpertPresets 靠它在召唤预设的人设正文里判断
387
+ // 「这条团队预设带没带桥接段」。旧版本召唤写入的团队预设没有这段——那种预设下的
388
+ // 会话主理人会去调不存在的 TeamCreate / Agent,成员永远开不出来(用户实测症状)。
389
+ export const TEAM_BRIDGE_MARKER = "## DSH 运行时适配(团队协作桥接)";
390
+ /** 团队人设正文是否已带桥接段(用于现存召唤预设行的 stale 体检)。 */
391
+ export function hasTeamBridge(personaText) {
392
+ return typeof personaText === "string" && personaText.includes(TEAM_BRIDGE_MARKER);
393
+ }
394
+ export function buildTeamBridge(detail, personaFiles) {
395
+ const members = Array.isArray(detail?.members) ? detail.members : [];
396
+ const others = members.filter((member) => member.role !== "lead");
397
+ if (!others.length) return "";
398
+ const rows = others.map((member) => {
399
+ const file = personaFiles?.get(member.id);
400
+ return `| \`${member.id}\` | ${member.name || "-"} | ${member.profession || "-"} | ${file ? `\`${file}\`` : "(人设未落盘,按职责执行)"} |`;
401
+ });
402
+ return [
403
+ "",
404
+ "---",
405
+ "",
406
+ "## DSH 运行时适配(团队协作桥接)",
407
+ "",
408
+ "本会话运行在 DSH 上,**没有 WorkBuddy 的 `TeamCreate` 与 `Agent` 工具**。团队编制已在召唤时固定,因此上文里「建立团队」「Agent 工具 name/subagent_type」两类指令按下面的等价方式执行;其余铁律(不得代写成员产出、成员之间不直连、一切经你中转)保持不变。",
409
+ "",
410
+ "1. **建团队 → 无需调用**:本会话即本团队,成员固定如下表,不需要创建、也不需要命名。",
411
+ "2. **调度成员 → `subagent` 工具**:",
412
+ " - `description`:`<成员ID>:<阶段产出名>`,例如 `software-architect:系统设计与任务分解`。",
413
+ " - `prompt`:先用 `read` 读出该成员的人设文件(下表最后一列),把**人设正文原文**放在 prompt 最前面,接一行 `---`,再往下写本次任务、上游产出、期望交付物与验收标准。",
414
+ " - 顺序 SOP(PRD → 架构 → 实现 → QA)**必须**传 `run_in_background: false`,当场等它返回再往下走;只有互不依赖的并行任务才留默认后台模式。",
415
+ "3. **成员产出**:子代理的最终回复就是该成员的专业产出,原文转交下一阶段,不要改写、不要替它编。",
416
+ "4. **续聊追问**:需要同一成员接着干活时,用 `send_message` 继续它的会话 id,不要重新 spawn 一个同名成员。",
417
+ "5. **上下文继承**:`subagent` 是干净上下文(更接近 WorkBuddy 的独立协作方);只有确实需要它继承你当前会话上下文时才用 `subagent_fork`。",
418
+ "",
419
+ "### 团队编制",
420
+ "",
421
+ "| 成员 ID | 花名 | 职责 | 人设文件 |",
422
+ "| --- | --- | --- | --- |",
423
+ ...rows,
424
+ "",
425
+ ].join("\n");
426
+ }
427
+ /**
428
+ * 把「详情」变成真正可用的会话人设:单人专家原样返回,团队专家先落成员人设、
429
+ * 再在末尾追加 DSH 桥接段。召唤预设与会话级挂载走同一条路径,保证两个入口一致。
430
+ * @returns {Promise<{personaText: string, memberResult: object|null}>}
431
+ */
432
+ export async function prepareExpertPersona(detail, { onLog, withMembers = true } = {}) {
433
+ if (detail?.expertType !== "team") return { personaText: detail?.persona || "", memberResult: null };
434
+ let memberResult = { installed: [], notes: [], dir: teamMemberDir(detail.plugin) };
435
+ if (withMembers) {
436
+ memberResult = await installTeamMemberPersonas(detail.plugin, detail.members, { onLog });
437
+ }
438
+ const files = new Map((memberResult.installed || []).map((row) => [row.id, row.path]));
439
+ return { personaText: `${detail.persona}${buildTeamBridge(detail, files)}`, memberResult };
440
+ }
301
441
  export async function uninstallExpertSkills(plugin) {
302
442
  const root = skillsRoot();
303
443
  let names = [];
@@ -390,7 +530,20 @@ export function readExpertPresets(content) {
390
530
  const config = entry.get("config", true);
391
531
  const presetId = String(mapValue(config, "id") || "");
392
532
  if (!presetId.startsWith(EXPERT_PRESET_PREFIX)) continue;
393
- rows.push({ presetId: String(mapValue(entry, "id")), plugin: presetId.slice(EXPERT_PRESET_PREFIX.length) });
533
+ // 带出人设正文(plugins[0] 的人设插件的 config.prefix),供 stale 体检:
534
+ // 旧版本召唤写入的团队预设没有桥接段,读出来后前端能标「需更新」。
535
+ let personaText = null;
536
+ const plugins = config.get("plugins", true);
537
+ if (isSeq(plugins) && plugins.items.length > 0 && isMap(plugins.items[0])) {
538
+ const personaConfig = plugins.items[0].get("config", true);
539
+ const prefix = personaConfig ? personaConfig.get("prefix", true) : null;
540
+ if (prefix && typeof prefix.value === "string") personaText = prefix.value;
541
+ }
542
+ rows.push({
543
+ presetId: String(mapValue(entry, "id")),
544
+ plugin: presetId.slice(EXPERT_PRESET_PREFIX.length),
545
+ personaText,
546
+ });
394
547
  }
395
548
  return rows;
396
549
  }
@@ -597,7 +750,22 @@ async function readRecentRaw() {
597
750
  }
598
751
  async function writeRecentRaw(data) {
599
752
  await mkdir(cacheDir(), { recursive: true });
600
- await writeFile(recentPath(), JSON.stringify(data, null, 1));
753
+ // OPT-08:原子写。直接覆盖写若中途断电/被杀,会留下截断的 JSON——readRecentRaw
754
+ // 虽然会静默重置(只丢「最近使用」排序、不至于崩),但没必要留着这个窗口。
755
+ //
756
+ // 这里刻意用「临时文件 + rename」而不引 @deepseek-ai/dsh-atomic-write:那是宿主提供的
757
+ // peer 依赖,本包 node_modules 里没有它,而 expert-market.js 会被 test/expert-manual.mjs
758
+ // 直接 import —— 加静态导入会让本地测试当场 ERR_MODULE_NOT_FOUND。
759
+ // 同目录 rename 在同一文件系统内是原子操作,足够。
760
+ const target = recentPath();
761
+ const tmp = `${target}.${process.pid}.${Date.now().toString(36)}.tmp`;
762
+ try {
763
+ await writeFile(tmp, JSON.stringify(data, null, 1));
764
+ await rename(tmp, target);
765
+ } catch (e) {
766
+ await rm(tmp, { force: true }).catch(() => {});
767
+ throw e;
768
+ }
601
769
  }
602
770
  /** 记录一次使用。row 携带菜单展示所需的最小字段;kind ∈ skill|mcp|expert。 */
603
771
  export async function recordExpertRecent(kind, row) {
package/lib/index.js CHANGED
@@ -13,10 +13,11 @@ import { collectImportLocations, missingEnvNames, resolveImportLocation } from "
13
13
  import { deriveMcpPhase, formatMcpLog, sanitizeMcpLog } from "./mcp-observability.js";
14
14
  import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInventory } from "./mcp-registry.js";
15
15
  import { WHATS_NEW } from "./whats-new.js";
16
- import { wbConnectorCatalog, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillFiles, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
17
- import { installExpertSkills, listExpertRecent, listExpertSkills, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
16
+ import { wbConnectorCatalog, wbConnectorCachedCount, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
17
+ import { hasTeamBridge, installExpertSkills, listExpertRecent, listExpertSkills, prepareExpertPersona, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, uninstallTeamMemberPersonas, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
18
18
  import { attachPersona, detachPersona, listPersonas, reconcilePersonas } from "./session-persona.js";
19
- import { scanSessionLedger } from "./tool-ledger.js";
19
+ // OPT-19:clearTextCache 一并引入——toolTailor 的可选动作要回收解压文本缓存。
20
+ import { clearTextCache, scanSessionLedger } from "./tool-ledger.js";
20
21
  import { evaluateEnvExpression, jsExpressionToTemplate } from "./workspace-config.js";
21
22
  import { canonicalWorkspacePath, installAgentRuntime, listWorkspaceRecords, liveWorkspaceAgentCount, projectConnectionsView, readWorkspaceConfigCached, reconcileWorkspaceConnections, reconcileWorkspaceRestricts, reconnectWorkspaceServer, withWorkspaceWrite, workspaceConnectionStatus, workspaceMountErrorsView, workspaceRestrictErrorView, workspaceScopeErrorsView, workspaceToolSchemas, writeWorkspaceConfigFile } from "./workspace-runtime.js";
22
23
 
@@ -603,7 +604,19 @@ function selectSourceServers(row, names) {
603
604
  if (!Array.isArray(names) || !names.every((name) => typeof name === "string")) throw new Error("导入条目名必须是字符串数组");
604
605
  wanted = new Set(names);
605
606
  const unknown = [...wanted].filter((name) => !row.entries.some((entry) => entry.name === name && !entry.error));
606
- if (unknown.length) throw new Error(`来源中没有可导入的 MCP:${unknown.join(", ")}`);
607
+ if (unknown.length) {
608
+ // OPT-17:这个名字可能真在这份配置里,只是 `${input:}` 落在 url/command 上、
609
+ // 位置剔不掉而整条降级。此时再说「来源中没有」会把用户送去改一个没写错的地方,
610
+ // 所以按 `unsupportedEntries` 把两边分开报(两个集合互斥:一个条目只会进其中一边)。
611
+ const blocked = new Set((row.unsupportedEntries ?? []).map((entry) => entry.name));
612
+ const interactive = unknown.filter((name) => blocked.has(name));
613
+ if (interactive.length) {
614
+ const absent = unknown.filter((name) => !blocked.has(name));
615
+ throw new Error(`以下 MCP 需要交互取值,DSH 无法导入:${interactive.join(", ")}${
616
+ absent.length ? `;另有不存在的条目:${absent.join(", ")}` : ""}`);
617
+ }
618
+ throw new Error(`来源中没有可导入的 MCP:${unknown.join(", ")}`);
619
+ }
607
620
  }
608
621
  const served = row.entries.filter((entry) => !entry.error && (wanted === null || wanted.has(entry.name)));
609
622
  if (!served.length) throw new Error("没有选中任何可导入的 MCP");
@@ -640,6 +653,18 @@ function projectSourceRow(row) {
640
653
  maskedFields: entry.spec ? maskedSourceFields(entry.spec) : [],
641
654
  missingEnv: entry.spec ? missingEnvNames(entry.spec, process.env) : [],
642
655
  })),
656
+ // OPT-17:`${input:}` 的位置不可剔时,那条会整条降级、不进 `entries`,只在
657
+ // `unsupportedEntries` 里留一份可展示信息。不透出来的话,界面只能笼统说「这里
658
+ // 没有可导入的 MCP」,用户看不出是「有 N 条需要交互取值」。仍只发最小的四个键:
659
+ // name/transport 是标签,reason 是已经写好、可直接渲染的说明,names 是占位符名
660
+ // (名字不是密钥);条目本体 `spec` 不出去,泄漏面不扩大。
661
+ // `?? []` 是给没挂这个字段的 row 兜底,避免投影本身抛错。
662
+ unsupportedEntries: (row.unsupportedEntries ?? []).map((entry) => ({
663
+ name: entry.name,
664
+ transport: entry.transport,
665
+ reason: entry.reason,
666
+ names: entry.names ?? [],
667
+ })),
643
668
  };
644
669
  }
645
670
  // 来源层与导入目标必须同一层,否则报错。「看到的来源 == 会写进去的那一层」这条不变量
@@ -835,7 +860,7 @@ async function listMcpServers(ctx) {
835
860
  let McpManagerGateway = (() => {
836
861
  let _classSuper = TypertRemoteService;
837
862
  let _instanceExtraInitializers = [];
838
- let _list_decorators, _status_decorators, _enable_decorators, _disable_decorators, _reconnect_decorators, _tools_decorators, _builtins_decorators, _installBuiltins_decorators, _add_decorators, _update_decorators, _remove_decorators, _reveal_decorators, _previewImport_decorators, _importJson_decorators, _listWorkspaces_decorators, _getWorkspaceView_decorators, _addWorkspaceServer_decorators, _updateWorkspaceServer_decorators, _removeWorkspaceServer_decorators, _setWorkspaceExclude_decorators, _installWorkspaceBuiltins_decorators, _previewWorkspaceImport_decorators, _importWorkspaceJson_decorators, _revealWorkspaceServer_decorators, _projectConnections_decorators, _reconnectWorkspaceServer_decorators, _updateHint_decorators, _whatsNew_decorators, _scanImportSources_decorators, _previewImportSource_decorators, _importSource_decorators, _toolTailor_decorators, _wbConnectorCatalog_decorators, _wbConnectorPreview_decorators, _wbConnectorInstall_decorators, _wbSkillCatalog_decorators, _wbSkillDetail_decorators, _wbSkillInstall_decorators, _wbSkillUninstall_decorators, _wbSkillInstalled_decorators;
863
+ let _list_decorators, _status_decorators, _enable_decorators, _disable_decorators, _reconnect_decorators, _tools_decorators, _builtins_decorators, _installBuiltins_decorators, _add_decorators, _update_decorators, _remove_decorators, _reveal_decorators, _previewImport_decorators, _importJson_decorators, _listWorkspaces_decorators, _getWorkspaceView_decorators, _addWorkspaceServer_decorators, _updateWorkspaceServer_decorators, _removeWorkspaceServer_decorators, _setWorkspaceExclude_decorators, _installWorkspaceBuiltins_decorators, _previewWorkspaceImport_decorators, _importWorkspaceJson_decorators, _revealWorkspaceServer_decorators, _projectConnections_decorators, _reconnectWorkspaceServer_decorators, _updateHint_decorators, _whatsNew_decorators, _scanImportSources_decorators, _previewImportSource_decorators, _importSource_decorators, _toolTailor_decorators, _wbConnectorCatalog_decorators, _wbConnectorCachedCount_decorators, _wbConnectorPreview_decorators, _wbConnectorInstall_decorators, _wbSkillCatalog_decorators, _wbSkillDetail_decorators, _wbSkillInstall_decorators, _wbSkillUninstall_decorators, _wbSkillInstalled_decorators;
839
864
  return class McpManagerGateway extends _classSuper {
840
865
  static {
841
866
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -905,6 +930,8 @@ let McpManagerGateway = (() => {
905
930
  __esDecorate(this, null, _toolTailor_decorators, { kind: "method", name: "toolTailor", static: false, private: false, access: { has: (obj) => "toolTailor" in obj, get: (obj) => obj.toolTailor }, metadata: _metadata }, null, _instanceExtraInitializers);
906
931
  _wbConnectorCatalog_decorators = [Remote("wbConnectorCatalog")];
907
932
  __esDecorate(this, null, _wbConnectorCatalog_decorators, { kind: "method", name: "wbConnectorCatalog", static: false, private: false, access: { has: (obj) => "wbConnectorCatalog" in obj, get: (obj) => obj.wbConnectorCatalog }, metadata: _metadata }, null, _instanceExtraInitializers);
933
+ _wbConnectorCachedCount_decorators = [Remote("wbConnectorCachedCount")];
934
+ __esDecorate(this, null, _wbConnectorCachedCount_decorators, { kind: "method", name: "wbConnectorCachedCount", static: false, private: false, access: { has: (obj) => "wbConnectorCachedCount" in obj, get: (obj) => obj.wbConnectorCachedCount }, metadata: _metadata }, null, _instanceExtraInitializers);
908
935
  _wbConnectorPreview_decorators = [Remote("wbConnectorPreview")];
909
936
  __esDecorate(this, null, _wbConnectorPreview_decorators, { kind: "method", name: "wbConnectorPreview", static: false, private: false, access: { has: (obj) => "wbConnectorPreview" in obj, get: (obj) => obj.wbConnectorPreview }, metadata: _metadata }, null, _instanceExtraInitializers);
910
937
  _wbConnectorInstall_decorators = [Remote("wbConnectorInstall")];
@@ -1114,6 +1141,12 @@ let McpManagerGateway = (() => {
1114
1141
  async toolTailor(payload) {
1115
1142
  // 只读账本:扫描本机会话日志 + 当前注册表 Schema,见 tool-ledger.js。
1116
1143
  // days 由调用方传(7/30/90),缺省 30,服务端钳到 [1,365]。
1144
+ // OPT-19:可选清空动作——payload.action === 'clearTextCache' 时先回收解压文本缓存
1145
+ // 再出报告。透传性已核实(typert.js:35 payloadParam 的 schema 是 $any =
1146
+ // z.unknown(),strict 只约束「必须有值」不做键剥离),额外键 action 能从客户端
1147
+ // 原样到达这里,无需新增 RPC 契约。先清后扫:报告里的 textCache 快照直接呈现
1148
+ // 清空后的状态,面板能立即看到「占用已归零」,不必再刷一次。
1149
+ if (payload?.action === "clearTextCache") clearTextCache();
1117
1150
  return buildToolTailorReport(this.ctx, payload || {});
1118
1151
  }
1119
1152
  async listWorkspaces() {
@@ -1423,6 +1456,10 @@ let McpManagerGateway = (() => {
1423
1456
  async wbConnectorCatalog() {
1424
1457
  return wbConnectorCatalog();
1425
1458
  }
1459
+ // 顶栏「市场」chip 的常驻计数:只读磁盘缓存,绝不联网(无缓存返回 null)。
1460
+ async wbConnectorCachedCount() {
1461
+ return wbConnectorCachedCount();
1462
+ }
1426
1463
  async wbConnectorPreview(payload) {
1427
1464
  return wbConnectorPreview(payload?.connectorId);
1428
1465
  }
@@ -1470,13 +1507,23 @@ let McpManagerGateway = (() => {
1470
1507
  if (!sessionId) throw new Error("缺少 sessionId");
1471
1508
  if (!plugin) throw new Error("缺少 plugin");
1472
1509
  const detail = await wbExpertDetail({ plugin });
1510
+ // 团队型:成员人设先落盘,主理人人设末尾追加 DSH 桥接段(与召唤预设同一套)。
1511
+ const prepared = await prepareExpertPersona(detail, {
1512
+ onLog: (message) => this.ctx.logger?.debug?.(`wb-expert: ${message}`),
1513
+ });
1473
1514
  const result = attachPersona(this.ctx, sessionId, {
1474
1515
  plugin,
1475
1516
  name: detail.name,
1476
- personaText: detail.persona,
1517
+ personaText: prepared.personaText,
1477
1518
  });
1478
1519
  await recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: `session-${plugin}` }).catch(() => {});
1479
- return { ...result, profession: detail.profession };
1520
+ return {
1521
+ ...result,
1522
+ profession: detail.profession,
1523
+ expertType: detail.expertType,
1524
+ members: detail.members || [],
1525
+ memberNotes: prepared.memberResult?.notes || [],
1526
+ };
1480
1527
  }
1481
1528
  async expertDetach(payload) {
1482
1529
  const sessionId = String(payload?.sessionId || "").trim();
@@ -1499,6 +1546,10 @@ let McpManagerGateway = (() => {
1499
1546
  // 详情即预览:召唤写盘前拉一次真实人设与技能清单,失败则整体不动。
1500
1547
  const detail = await wbExpertDetail({ plugin });
1501
1548
  const withSkills = payload?.withSkills !== false && detail.skills.length > 0;
1549
+ // 团队型:成员人设先落盘(人设不进 preset,避免每轮白付),主理人人设追加桥接段。
1550
+ const prepared = await prepareExpertPersona(detail, {
1551
+ onLog: (message) => this.ctx.logger?.debug?.(`wb-expert: ${message}`),
1552
+ });
1502
1553
  // 技能先落盘(失败只影响技能,不阻断召唤),预设行再走唯一写链(文件锁 + 版本校验 + 原子写)。
1503
1554
  let skillResult = { installed: [], notes: [] };
1504
1555
  if (withSkills) {
@@ -1513,7 +1564,7 @@ let McpManagerGateway = (() => {
1513
1564
  displayName: detail.name,
1514
1565
  profession: detail.profession,
1515
1566
  description: detail.description,
1516
- personaText: detail.persona,
1567
+ personaText: prepared.personaText,
1517
1568
  });
1518
1569
  if (written.content === state.content) return { mode: "unchanged", presetId: written.presetId };
1519
1570
  await writePatch(state, written.content);
@@ -1521,14 +1572,26 @@ let McpManagerGateway = (() => {
1521
1572
  });
1522
1573
  // 召唤成功即记入最近使用(送还由 listExpertRecent 对账剔除)。记录失败不阻断召唤。
1523
1574
  await recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: result.presetId }).catch(() => {});
1575
+ const teamMembers = prepared.memberResult?.installed || [];
1576
+ // upsert 后核对人设正文是否真带桥接段(团队型);不带则提示更新结果不完整。
1577
+ // 正常路径 prepared.personaText 已拼桥接段,这个分支只在异常数据时触发。
1578
+ const staleBridge = detail.expertType === "team" && !hasTeamBridge(prepared.personaText);
1524
1579
  return {
1525
1580
  presetId: result.presetId,
1526
1581
  presetMode: result.mode,
1582
+ expertType: detail.expertType,
1583
+ members: detail.members || [],
1527
1584
  summonedSkills: skillResult.installed,
1528
1585
  skillNotes: skillResult.notes,
1586
+ summonedMembers: teamMembers,
1587
+ memberNotes: prepared.memberResult?.notes || [],
1588
+ staleBridge,
1529
1589
  note: `已召唤「${detail.name}」:新会话选择预设「WorkBuddy 专家 · ${detail.name}」即专家人设接管`
1590
+ + (result.mode === "unchanged" ? "(预设已是最新,未改动)" : "")
1591
+ + (result.mode === "updated" ? "(旧预设行已刷新为人设最新版)" : "")
1592
+ + (teamMembers.length ? `;团队成员人设 ${teamMembers.length} 份已落盘,主理人按编制用 subagent 调度` : "")
1530
1593
  + (skillResult.installed.length ? `;捆绑技能 ${skillResult.installed.length} 个已装进 ~/.agents/skills/` : "")
1531
- + (skillResult.notes.length ? `;注意:${skillResult.notes.join(";")}` : ""),
1594
+ + ([...(prepared.memberResult?.notes || []), ...skillResult.notes].length ? `;注意:${[...(prepared.memberResult?.notes || []), ...skillResult.notes].join(";")}` : ""),
1532
1595
  };
1533
1596
  }
1534
1597
  async wbExpertDismiss(payload) {
@@ -1548,17 +1611,29 @@ let McpManagerGateway = (() => {
1548
1611
  } catch (error) {
1549
1612
  this.ctx.logger?.warn?.(`wb-expert: 预设已摘除,但技能清理失败:${error?.message ?? error}`);
1550
1613
  }
1551
- if (result.mode === "absent" && !removedSkills.length) throw new Error("本机没有召唤过这个专家(profile patch 里没有对应预设行)");
1614
+ // 团队型:成员人设目录同样按 plugin 隔离,一并清除。
1615
+ let removedTeam = null;
1616
+ try {
1617
+ removedTeam = await uninstallTeamMemberPersonas(plugin);
1618
+ } catch (error) {
1619
+ this.ctx.logger?.warn?.(`wb-expert: 预设已摘除,但成员人设清理失败:${error?.message ?? error}`);
1620
+ }
1621
+ if (result.mode === "absent" && !removedSkills.length && !removedTeam) throw new Error("本机没有召唤过这个专家(profile patch 里没有对应预设行)");
1552
1622
  return {
1553
1623
  removedSkills,
1624
+ removedTeam,
1554
1625
  note: `已送还「${plugin}」`
1555
1626
  + (result.mode === "removed" ? ":召唤预设已从 profile patch 摘除(热加载,无需重启)" : ":本没有召唤预设")
1556
- + (removedSkills.length ? `;捆绑技能已清理 ${removedSkills.length} 个目录` : ""),
1627
+ + (removedSkills.length ? `;捆绑技能已清理 ${removedSkills.length} 个目录` : "")
1628
+ + (removedTeam ? ";团队成员人设已清理" : ""),
1557
1629
  };
1558
1630
  }
1559
1631
  async wbExpertInstalled() {
1560
1632
  // 已召唤 = profile patch 里的召唤预设行;已装技能 = ~/.agents/skills 的 wbexpert__ 目录。
1561
1633
  // patch 读走与 managedState 同一条解析(readPatch),但不持写锁——只读快照。
1634
+ // 团队行做 stale 体检:人设正文缺桥接段 = 旧版本召唤写入的行,新会话选它会调
1635
+ // 不存在的 TeamCreate/Agent(成员开不出来)。标记桥接判定放前端(需要市场目录的
1636
+ // expertType),这里只把 personaText 原样带出去。
1562
1637
  const patch = await readPatch(this.ctx);
1563
1638
  return { presets: readExpertPresets(patch.content), skills: await listExpertSkills() };
1564
1639
  }