dsh-armory 2.4.2 → 2.4.4
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/README.en.md +13 -4
- package/README.md +21 -7
- package/docs/perf-market-load.md +469 -0
- package/docs/project-mcp-lifecycle.md +2 -0
- package/lib/client.js +1407 -114
- package/lib/expert-market.js +137 -6
- package/lib/index.js +314 -62
- package/lib/mcp-community.js +353 -0
- package/lib/session-persona.js +302 -48
- package/lib/skill-disable.js +178 -0
- package/lib/system-open.js +113 -0
- package/lib/typert.js +10 -0
- package/lib/wb-auth-rules.js +47 -16
- package/lib/wb-market.js +85 -1
- package/lib/whats-new-en.js +44 -0
- package/lib/whats-new.js +50 -0
- package/lib/workspace-runtime.js +37 -3
- package/package.json +1 -1
package/lib/expert-market.js
CHANGED
|
@@ -66,6 +66,9 @@ function skillsRoot() {
|
|
|
66
66
|
return join(homedir(), ".agents", "skills");
|
|
67
67
|
}
|
|
68
68
|
function workbuddyExpertCache() {
|
|
69
|
+
// 测试隔离口(同 DSH_ARMORY_MARKET_HOME 的纪律):设置时本机 WorkBuddy 缓存根指到临时目录。
|
|
70
|
+
const override = process.env.DSH_ARMORY_WORKBUDDY_CACHE;
|
|
71
|
+
if (override) return override;
|
|
69
72
|
return join(homedir(), ".workbuddy", "plugins", "cache", "experts");
|
|
70
73
|
}
|
|
71
74
|
// 团队专家:主理人 + N 个成员,每人一份人设。成员人设不塞进 preset(会白白撑大每轮
|
|
@@ -335,6 +338,105 @@ async function localExpertPluginJson(plugin) {
|
|
|
335
338
|
return null;
|
|
336
339
|
}
|
|
337
340
|
|
|
341
|
+
// 主理人人设的本机来源:WorkBuddy 自己下载过的专家包里就有 agents/<name>.md。
|
|
342
|
+
// 上游只把一部分专家放在公开分发地址上(2026-10-27 实测 428 个里 185 个 404),
|
|
343
|
+
// 所以本机缓存不是「优化」,而是这些专家唯一能拿到正文的途径。
|
|
344
|
+
// 人设文件名只做「取最后一段 + 拒绝穿越」,不做 ASCII 化:WorkBuddy 里真实存在
|
|
345
|
+
// 中文 agentName(如「腾讯健康-觅影-眼底彩照疾病分析专家」),safeSegment 会把它压成
|
|
346
|
+
// 一串连字符,永远找不到本机缓存里的 agents/<agentName>.md。
|
|
347
|
+
function agentFileStem(value) {
|
|
348
|
+
const raw = String(value || "").trim();
|
|
349
|
+
if (!raw) return "";
|
|
350
|
+
const base = basename(raw.replace(/\\/g, "/")).replace(/\.md$/i, "").trim();
|
|
351
|
+
if (!base || base === "." || base === ".." || base.includes("\0")) return "";
|
|
352
|
+
return base;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function localExpertPersona(plugin, agentName) {
|
|
356
|
+
const names = [agentName, plugin].map(agentFileStem).filter(Boolean);
|
|
357
|
+
for (const version of await localExpertVersions(plugin)) {
|
|
358
|
+
const dir = join(version.dir, "agents");
|
|
359
|
+
for (const name of new Set(names)) {
|
|
360
|
+
try {
|
|
361
|
+
const text = await readFile(join(dir, `${name}.md`), "utf8");
|
|
362
|
+
if (text.trim()) return text;
|
|
363
|
+
} catch { /* 换个名字或换下一个版本 */ }
|
|
364
|
+
}
|
|
365
|
+
// 人设文件名与 plugin / agentName 都不一致时,接受 agents/ 下唯一那份 md。
|
|
366
|
+
try {
|
|
367
|
+
const files = (await readdir(dir)).filter((file) => /\.md$/i.test(file));
|
|
368
|
+
if (files.length === 1) {
|
|
369
|
+
const text = await readFile(join(dir, files[0]), "utf8");
|
|
370
|
+
if (text.trim()) return text;
|
|
371
|
+
}
|
|
372
|
+
} catch { /* 该版本没有 agents/ */ }
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 本机覆盖:用户自备的人设正文,优先级高于缓存与 CDN。上游 185/429 没有公开分发、
|
|
378
|
+
// 本机也没有缓存时,这是唯一能拿到「原文」的途径。文件名走 safeSegment,不能借 plugin 穿越目录。
|
|
379
|
+
function overridePersonaPath(plugin) {
|
|
380
|
+
return join(cacheDir(), "overrides", `${safeSegment(plugin)}.md`);
|
|
381
|
+
}
|
|
382
|
+
async function overridePersona(plugin) {
|
|
383
|
+
try {
|
|
384
|
+
const text = await readFile(overridePersonaPath(plugin), "utf8");
|
|
385
|
+
if (text.trim()) return text;
|
|
386
|
+
} catch { /* 绝大多数专家没有覆盖文件 */ }
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 原文取不到的原因:上游缺包与网络故障说法不同,用户据此决定换专家还是重试。 */
|
|
391
|
+
function describePersonaFailure(error) {
|
|
392
|
+
const detail = String(error?.message || error);
|
|
393
|
+
if (/HTTP 404/.test(detail)) return "上游 404:专家中心目录里挂着它,但公开分发地址上没有这个包";
|
|
394
|
+
return `拉取原文失败:${detail}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 兜底人设:三条正路都拿不到原文时,用专家中心公开的介绍 / 专业 / 标签 / 示例问题拼一份替身。
|
|
399
|
+
* 必须显式标注非原文——召唤出来的会话会照这份文字行事,用户有权知道它不是原作者设定;
|
|
400
|
+
* 拿到原文后放进本机覆盖目录即自动改用原文(覆盖优先级最高)。
|
|
401
|
+
*/
|
|
402
|
+
function generatedPersona(row, reason) {
|
|
403
|
+
const name = row.name || row.plugin;
|
|
404
|
+
const profession = row.profession || "该领域的专家";
|
|
405
|
+
const description = row.description || "(专家中心没有为它提供介绍文字)";
|
|
406
|
+
const category = row.categoryName || row.categoryId || "";
|
|
407
|
+
const tags = Array.isArray(row.tags) ? row.tags.filter(Boolean) : [];
|
|
408
|
+
const prompts = Array.isArray(row.quickPrompts) ? row.quickPrompts.filter(Boolean) : [];
|
|
409
|
+
const overridePath = overridePersonaPath(row.plugin).replace(/\\/g, "/");
|
|
410
|
+
const lines = [
|
|
411
|
+
`# ${name}(${profession})`,
|
|
412
|
+
"",
|
|
413
|
+
`> **这份人设不是原文。**${reason};本机 WorkBuddy 缓存里也没有它。`,
|
|
414
|
+
"> 下面的内容由 WorkBuddy 专家中心公开的介绍、标签与示例问题整理而成,",
|
|
415
|
+
"> 只能还原「大致是谁、负责什么」,不等同于原作者写的工作方法与铁律。",
|
|
416
|
+
`> 拿到原始人设后,放到 \`${overridePath}\` 即可自动改用原文。`,
|
|
417
|
+
"",
|
|
418
|
+
"## 身份",
|
|
419
|
+
`你是「${name}」,身份是「${profession}」${category ? `,属于「${category}」` : ""}。`,
|
|
420
|
+
"",
|
|
421
|
+
"## 你负责什么",
|
|
422
|
+
description,
|
|
423
|
+
"",
|
|
424
|
+
"## 工作原则",
|
|
425
|
+
"- 只做上面这段职责范围内的事;被问到范围外的内容时说明边界并给出替代方向,不冒充全才。",
|
|
426
|
+
"- 这不是原作者人设:不要声称掌握原作者未公开的资料、模板或内部流程,拿不准就直说。",
|
|
427
|
+
"- 先确认目标、已有输入与验收标准,再动手;信息不足时先问最关键的那一个。",
|
|
428
|
+
"- 给结论时附上依据与可执行的下一步,不写空泛的鼓励语。",
|
|
429
|
+
];
|
|
430
|
+
if (tags.length) lines.push(`- 额外关注:${tags.join("、")}。`);
|
|
431
|
+
if (prompts.length) lines.push("", "## 用户常这样开始", ...prompts.map((prompt) => `- ${prompt}`));
|
|
432
|
+
lines.push(
|
|
433
|
+
"",
|
|
434
|
+
"## 开场",
|
|
435
|
+
"第一次对话先用一句话说明自己是谁、能做什么,然后按上面的原则推进;不要主动展开与任务无关的话题。",
|
|
436
|
+
);
|
|
437
|
+
return `${lines.join("\n")}\n`;
|
|
438
|
+
}
|
|
439
|
+
|
|
338
440
|
// ── 专家详情:人设正文 + 捆绑技能清单 ──────────────────────────────────
|
|
339
441
|
// 召唤前给用户看的全部真实内容:人设 MD 原文、plugin.json 的技能声明、quickPrompts。
|
|
340
442
|
export async function wbExpertDetail(payload) {
|
|
@@ -356,13 +458,40 @@ export async function wbExpertDetail(payload) {
|
|
|
356
458
|
if (!row.agentName && pluginJson?.agentName) row.agentName = String(pluginJson.agentName);
|
|
357
459
|
const promptFile = String(row.promptFile || (pluginJson ? `/plugins/${plugin}/agents/${pluginJson.agentName || plugin}.md` : "")).trim();
|
|
358
460
|
if (!promptFile) throw new Error(`专家 ${plugin} 没有人设文件路径(promptFile 缺失)`);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
461
|
+
// 取数顺序:本机覆盖 → 本机 WorkBuddy 缓存 → 公开分发地址 → 兜底生成。
|
|
462
|
+
// 上游 185/429 没有公开分发人设(issue #5),所以「三条路都空」是常态而非异常:
|
|
463
|
+
// 这时给一份明确标注非原文的替身,保证专家至少能看详情、能召唤,不再抛 404。
|
|
464
|
+
let persona = (await overridePersona(plugin)) || "";
|
|
465
|
+
let personaSource = persona ? "override" : "";
|
|
466
|
+
let personaNotice = "";
|
|
467
|
+
if (!persona) {
|
|
468
|
+
persona = (await localExpertPersona(plugin, row.agentName || pluginJson?.agentName)) || "";
|
|
469
|
+
if (persona) personaSource = "workbuddy-cache";
|
|
470
|
+
}
|
|
471
|
+
if (!persona) {
|
|
472
|
+
try {
|
|
473
|
+
const fetched = await fetchText(EXPERT_ORIGIN + promptFile);
|
|
474
|
+
if (fetched.trim()) {
|
|
475
|
+
persona = fetched;
|
|
476
|
+
personaSource = "cdn";
|
|
477
|
+
} else {
|
|
478
|
+
personaNotice = "公开分发地址上的人设文件是空的";
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
personaNotice = describePersonaFailure(error);
|
|
482
|
+
// 只有「上游确实没有这份文件」(404 / 410)才生成替身。断网 / 超时 / 5xx 是暂时故障,
|
|
483
|
+
// 静默换成替身会让用户以为专家原文就长这样——如实报错,让用户重试或放本机覆盖。
|
|
484
|
+
if (!/HTTP (404|410)\b/.test(String(error?.message || error))) {
|
|
485
|
+
throw new Error(`拉取专家人设失败:${String(error?.message || error)}。请检查网络后重试,或把原文放到本机覆盖目录后再打开。`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (!persona) {
|
|
490
|
+
persona = generatedPersona(row, personaNotice);
|
|
491
|
+
personaSource = "generated";
|
|
492
|
+
personaNotice = personaNotice || "原文不可用";
|
|
364
493
|
}
|
|
365
|
-
if (!persona.trim()) throw new Error("
|
|
494
|
+
if (!persona.trim()) throw new Error("人设正文为空");
|
|
366
495
|
const skillNames = Array.isArray(pluginJson?.skills)
|
|
367
496
|
? pluginJson.skills.map((entry) => basename(String(entry).replace(/\/+$/, ""))).filter(Boolean)
|
|
368
497
|
: [];
|
|
@@ -374,6 +503,8 @@ export async function wbExpertDetail(payload) {
|
|
|
374
503
|
promptFile,
|
|
375
504
|
persona,
|
|
376
505
|
personaChars: persona.length,
|
|
506
|
+
personaSource,
|
|
507
|
+
personaNotice,
|
|
377
508
|
version: String(pluginJson?.version || ""),
|
|
378
509
|
skills,
|
|
379
510
|
avatarPluginUrl: pluginJson?.avatar ? `${EXPERT_ORIGIN}/plugins/${encodeURIComponent(plugin)}/${String(pluginJson.avatar).replace(/^\//, "")}` : row.avatarUrl,
|