dsh-armory 2.3.0 → 2.3.1
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 +21 -15
- package/README.md +24 -14
- package/docs/installation.md +0 -2
- package/lib/client.js +2323 -629
- package/lib/community-meta.js +222 -0
- package/lib/expert-market.js +119 -23
- package/lib/index.js +218 -91
- package/lib/loadout.js +178 -0
- package/lib/mcp-config.js +0 -171
- package/lib/mcp-oauth.js +2 -4
- package/lib/npm-downloads.js +262 -0
- package/lib/suites-registry.js +10 -4
- package/lib/typert.js +13 -10
- package/lib/wb-awesome-market.js +706 -214
- package/lib/wb-install-state.js +10 -4
- package/lib/wb-market.js +99 -27
- package/lib/wb-registry.js +32 -31
- package/lib/whats-new-en.js +21 -0
- package/lib/whats-new.js +24 -0
- package/package.json +2 -2
- package/docs/mcp-market-design.md +0 -617
- package/lib/mcp-catalog-en.js +0 -244
- package/lib/mcp-catalog.js +0 -806
- package/lib/wb-plugin-market.js +0 -291
package/lib/client.js
CHANGED
|
@@ -33,7 +33,7 @@ var __injectCss = function(text) {
|
|
|
33
33
|
return () => tag.remove();
|
|
34
34
|
};
|
|
35
35
|
const React2 = React;
|
|
36
|
-
const { useState, useEffect, useCallback, useMemo, useRef } = React2;
|
|
36
|
+
const { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React2;
|
|
37
37
|
|
|
38
38
|
// ── OPT-11:i18n 骨架(gettext 模型,键 = 中文原文) ────────────────────────
|
|
39
39
|
// zh-CN 是源语言:字典为空时 t() 恒等返回原文,缺键永远不会让界面出现 key 名。
|
|
@@ -137,7 +137,6 @@ const EN_US_MESSAGES = {
|
|
|
137
137
|
"MCP 工具按 server 生效:选择后点名 mcp__服务__* 工具": "MCP tools apply per server: picking this names mcp__server__* tools",
|
|
138
138
|
"MCP 市场服务未连接(面板仍可用)": "MCP market service not connected (panel still usable)",
|
|
139
139
|
"MCP 市场面板": "MCP Market panel",
|
|
140
|
-
"MCP 目录暂时是空的": "The MCP catalog is empty for now",
|
|
141
140
|
"Memory · 知识图谱记忆": "Memory · knowledge-graph memory",
|
|
142
141
|
"Schema 税": "Schema tax",
|
|
143
142
|
"Sequential Thinking · 分步推理": "Sequential Thinking · step-by-step reasoning",
|
|
@@ -239,7 +238,6 @@ const EN_US_MESSAGES = {
|
|
|
239
238
|
"保存": "Save",
|
|
240
239
|
"保存中…": "Saving…",
|
|
241
240
|
"保存失败": "Save failed",
|
|
242
|
-
"免密钥": "Keyless",
|
|
243
241
|
"入口": "Entries",
|
|
244
242
|
"入口显示:": "Entry display:",
|
|
245
243
|
"全局": "Global",
|
|
@@ -250,14 +248,12 @@ const EN_US_MESSAGES = {
|
|
|
250
248
|
"全局面板 + 悬浮按钮(推荐)": "Global panel + floating button (recommended)",
|
|
251
249
|
"全部": "All",
|
|
252
250
|
"全部传输": "All transports",
|
|
253
|
-
"全部分类": "All categories",
|
|
254
251
|
"全部状态": "All states",
|
|
255
252
|
"共 {0} 个": "Total {0}",
|
|
256
253
|
"关闭": "Close",
|
|
257
254
|
"关闭更新提示": "Dismiss update notice",
|
|
258
255
|
"其他 bundle / Agent(只读)": "Other bundles / agents (read-only)",
|
|
259
256
|
"其余分类仍可正常使用;完整堆栈已打印到控制台。": "Other categories keep working; the full stack trace is printed to the console.",
|
|
260
|
-
"内置 MCP": "Built-in MCPs",
|
|
261
257
|
"再看看这些(目录前 {0} 位未召唤)": "Also consider these (first {0} in catalog, not yet summoned)",
|
|
262
258
|
"出现过的工具(含已卸载 MCP 的历史调用)": "Tools ever seen (including historical calls of uninstalled MCPs)",
|
|
263
259
|
"列表(最多显示 6 行,滚动查看更多)": "List (shows up to 6 rows; scroll for more)",
|
|
@@ -296,7 +292,6 @@ const EN_US_MESSAGES = {
|
|
|
296
292
|
"只看窗口内零调用的工具(默认关闭:默认显示全部工具与真实调用次数)": "Show only zero-call tools in the window (off by default: all tools with real call counts are shown)",
|
|
297
293
|
"只看零调用": "Zero-call only",
|
|
298
294
|
"只看零调用({0})": "Zero-call only ({0})",
|
|
299
|
-
"只要免密钥": "Keyless only",
|
|
300
295
|
"召唤": "Summon",
|
|
301
296
|
"召唤 = 往 profile 补丁写入一条独立 Agent 预设(人设全程接管该预设的会话,标准工具原样保留);": "Summon = write an independent agent preset into the profile patch (the persona takes over that preset's sessions; standard tools are kept); ",
|
|
302
297
|
"召唤专家团": "Summon expert team",
|
|
@@ -346,22 +341,10 @@ const EN_US_MESSAGES = {
|
|
|
346
341
|
"正在安装插件 {0}…": "Installing plugin {0}…",
|
|
347
342
|
"正在安装 {0} 个插件…": "Installing {0} plugin(s)…",
|
|
348
343
|
"宿主未提供插件管理器(pluginManager),无法安装插件": "The host does not expose a plugin manager (pluginManager); cannot install plugins",
|
|
349
|
-
"该插件未提供可推导的安装来源(repository),暂不能安装": "This plugin declares no derivable install source (repository); display only for now",
|
|
350
344
|
"插件 {0} 安装成功,需重启 DSH 宿主后生效。": "Plugin {0} installed. Restart the DSH host for it to take effect.",
|
|
351
|
-
"正在读取插件目录…": "Loading the plugin catalog…",
|
|
352
|
-
"插件目录暂时是空的": "The plugin catalog is empty for now",
|
|
353
|
-
"没有匹配的插件": "No plugins match",
|
|
354
|
-
"插件详情": "Plugin details",
|
|
355
|
-
"依赖 {0}": "{0} deps",
|
|
356
|
-
"依赖": "Dependencies",
|
|
357
|
-
"本插件声明了 {0} 个 LSP 服务,安装后由宿主按声明启动(此处仅展示)。": "This plugin declares {0} LSP services; the host starts them per declaration after install (display only here).",
|
|
358
|
-
"来源:": "Source: ",
|
|
359
|
-
"主页:": "Homepage: ",
|
|
360
|
-
"安装 = 通过宿主 pluginManager 拉取并落盘;安装成功后需重启 DSH 宿主才会生效。": "Install fetches and lands the plugin through the host's pluginManager; restart the DSH host afterwards for it to take effect.",
|
|
361
345
|
"知道了": "Got it",
|
|
362
346
|
"调用失败": "Call failed",
|
|
363
347
|
"({0})": " ({0})",
|
|
364
|
-
"目录来自 codebuddy 官方市场 ZIP 分发,可能还没同步下来;重读一次通常就好。": "The catalog comes from the official codebuddy marketplace ZIP; it may not have synced yet. Re-reading usually fixes it.",
|
|
365
348
|
"含技能": "With skills",
|
|
366
349
|
"含需掩码字段:": "Contains fields needing masking: ",
|
|
367
350
|
"启动失败策略": "Start-failure policy",
|
|
@@ -371,6 +354,7 @@ const EN_US_MESSAGES = {
|
|
|
371
354
|
"命令面板": "Command palette",
|
|
372
355
|
"团队型走 DSH 桥接:主理人人设接管会话,": "Team-type experts bridge through DSH: the lead's persona takes over the session, ",
|
|
373
356
|
"团队编制({0} 人 · 召唤后成员人设落盘,由主理人调度)": "Team roster ({0} members · personas land on disk after summoning; dispatched by the lead)",
|
|
357
|
+
"在 GitHub 打开 dsh-armory 仓库,点右上角的 Star": "Open the dsh-armory repo on GitHub and click Star in its top-right corner",
|
|
374
358
|
"在 Skill 页查看 {0}": "View {0} on the Skill page",
|
|
375
359
|
"在 profile 里找不到这个插件,未升级。": "Plugin not found in the profile; not upgraded.",
|
|
376
360
|
"在不离开会话的情况下查看 MCP": "Inspect MCPs without leaving the session",
|
|
@@ -394,7 +378,6 @@ const EN_US_MESSAGES = {
|
|
|
394
378
|
"大肥鱼军械库": "Big Fish Armory",
|
|
395
379
|
"宿主正在停止,未升级;请重启后再试。": "Host is shutting down; not upgraded. Restart and try again.",
|
|
396
380
|
"宿主没有挂载 skill 注册表(@deepseek-ai/dsh-skill),本看板不可用": "Host has no skill registry mounted (@deepseek-ai/dsh-skill); this board is unavailable",
|
|
397
|
-
"密钥:": "Key: ",
|
|
398
381
|
"导入 MCP": "Import MCPs",
|
|
399
382
|
"导入 MCP(": "Import MCPs (",
|
|
400
383
|
"导入中…": "Importing…",
|
|
@@ -441,11 +424,7 @@ const EN_US_MESSAGES = {
|
|
|
441
424
|
"目录默认顺序": "Catalog order",
|
|
442
425
|
"技能数由多到少": "Most skills first",
|
|
443
426
|
"名称 A→Z": "Name A→Z",
|
|
444
|
-
"插件市场有新版本可更新(本地缓存 {0})。点「重新加载」可拉取最新目录。": "A newer plugin market catalog is available (local cache {0}). Click Reload to fetch the latest catalog.",
|
|
445
|
-
"本地还没有插件市场缓存,点「重新加载」可首次拉取目录。": "No local plugin market cache yet. Click Reload to fetch the catalog for the first time.",
|
|
446
427
|
"重新加载": "Reload",
|
|
447
|
-
"插件市场更新探测失败(不影响已安装插件)。": "Plugin market update check failed (installed plugins are unaffected).",
|
|
448
|
-
"重新探测": "Recheck",
|
|
449
428
|
"已导入 MCP": "MCPs imported",
|
|
450
429
|
"已屏蔽": "Blocked",
|
|
451
430
|
"已屏蔽 ": "Blocked ",
|
|
@@ -462,12 +441,11 @@ const EN_US_MESSAGES = {
|
|
|
462
441
|
"已连接": "Connected",
|
|
463
442
|
"已连接 · ": "Connected · ",
|
|
464
443
|
"已连接 · 当前无会话使用(连接保留,下次直接用)": "Connected · no session using it right now (connection kept for next time)",
|
|
465
|
-
"已配置": "Configured",
|
|
466
444
|
"已重连 ": "Reconnected ",
|
|
467
445
|
"已隐藏 {0} 个不兼容条目": "Hidden {0} incompatible entries",
|
|
468
446
|
"市场": "Market",
|
|
469
447
|
"市场数据加载失败;其余分类仍可正常使用。": "Failed to load market data; other categories keep working.",
|
|
470
|
-
"
|
|
448
|
+
"市场:连接器、SkillHub 技能与专家召唤。": "Market: connectors, SkillHub skills and expert summoning.",
|
|
471
449
|
"归属存疑": "Ownership uncertain",
|
|
472
450
|
"当前": "Current",
|
|
473
451
|
"当前 Web profile": "Current web profile",
|
|
@@ -479,7 +457,6 @@ const EN_US_MESSAGES = {
|
|
|
479
457
|
"当前有 {0} 个 skill,但都被筛选条挡住了。": "{0} skills exist, but all are filtered out.",
|
|
480
458
|
"当前生效专家(人设接管本会话;可在输入框工具行的专家菜单随时换/摘)": "Active expert (persona runs this session; swap/detach anytime from the expert menu on the input toolbar)",
|
|
481
459
|
"当前筛选下没有可安装的连接器": "No installable connectors under the current filter",
|
|
482
|
-
"当前筛选下没有服务": "No servers under the current filter",
|
|
483
460
|
"当前运行在 Node {0},请升级到 22.15 以上后重启宿主。": "Running Node {0}; upgrade to 22.15+ and restart the host.",
|
|
484
461
|
"必填": "Required",
|
|
485
462
|
"快速调用": "Quick invoke",
|
|
@@ -489,7 +466,6 @@ const EN_US_MESSAGES = {
|
|
|
489
466
|
"或粘贴 JSON:支持 Claude/Cursor/Cline/Roo 的 mcpServers、VS Code 的 servers,以及单个 MCP 对象": "or paste JSON: supports mcpServers from Claude/Cursor/Cline/Roo, servers from VS Code, and a single MCP object",
|
|
490
467
|
"手动添加": "Add manually",
|
|
491
468
|
"打开": "Open",
|
|
492
|
-
"打开官方文档": "Open official docs",
|
|
493
469
|
"打开悬浮管理面板": "Open the floating panel",
|
|
494
470
|
"打开技能 · MCP · 专家看板": "Open the Skills · MCPs · Experts board",
|
|
495
471
|
"打开技能、MCP、专家快速调用菜单": "Open the skills/MCPs/experts quick-invoke menu",
|
|
@@ -537,7 +513,6 @@ const EN_US_MESSAGES = {
|
|
|
537
513
|
"搜索全局 MCP…": "Search global MCPs…",
|
|
538
514
|
"搜索工具名或描述…": "Search tool name or description…",
|
|
539
515
|
"搜索时自动在全库匹配(忽略分类)": "Search matches across all libraries (ignores category)",
|
|
540
|
-
"搜索服务、用途或提供方…": "Search services, purpose or provider…",
|
|
541
516
|
"搜索连接器(名称 / 说明 / ID)": "Search connectors (name / description / ID)",
|
|
542
517
|
"摘掉专家": "Detach expert",
|
|
543
518
|
"摘掉专家(切回默认预设)": "Detach expert (back to default preset)",
|
|
@@ -545,7 +520,6 @@ const EN_US_MESSAGES = {
|
|
|
545
520
|
"收起原始 JSON": "Collapse raw JSON",
|
|
546
521
|
"收起参数": "Collapse parameters",
|
|
547
522
|
"收起工具清单": "Collapse tool list",
|
|
548
|
-
"文档": "Docs",
|
|
549
523
|
"断开后自动重连": "Auto-reconnect after disconnect",
|
|
550
524
|
"新增:": "New: ",
|
|
551
525
|
"新建会话时选择「WorkBuddy 专家 · ": "When creating a session, pick \"WorkBuddy Expert · ",
|
|
@@ -608,7 +582,6 @@ const EN_US_MESSAGES = {
|
|
|
608
582
|
"正在安装 {0}…": "Installing {0}…",
|
|
609
583
|
"正在扫描本机客户端配置…": "Scanning local client configs…",
|
|
610
584
|
"正在挂载专家…": "Attaching expert…",
|
|
611
|
-
"正在读取 MCP 目录…": "Reading MCP catalog…",
|
|
612
585
|
"正在读取…": "Reading…",
|
|
613
586
|
"正在读取专家目录…": "Reading expert catalog…",
|
|
614
587
|
"正在读取已召唤专家…": "Reading summoned experts…",
|
|
@@ -645,6 +618,7 @@ const EN_US_MESSAGES = {
|
|
|
645
618
|
"清空搜索": "Clear search",
|
|
646
619
|
"清除全部筛选": "Clear all filters",
|
|
647
620
|
"清除来源筛选": "Clear source filter",
|
|
621
|
+
"点 Star": "Star it",
|
|
648
622
|
"点击切换到此分类": "Click to switch to this category",
|
|
649
623
|
"版本号不合法,未升级。": "Invalid version; not upgraded.",
|
|
650
624
|
"状态未知": "State unknown",
|
|
@@ -657,8 +631,6 @@ const EN_US_MESSAGES = {
|
|
|
657
631
|
"用户可调用": "User-invocable",
|
|
658
632
|
"用法示例": "Usage example",
|
|
659
633
|
"目录来自 WorkBuddy 官方分发,可能还没同步下来;重读一次通常就好。": "The catalog comes from official WorkBuddy distribution and may not have synced; re-reading usually fixes it.",
|
|
660
|
-
"目录由宿主侧维护,可能还没同步下来;重读一次通常就好。": "The catalog is maintained host-side and may not have synced; re-reading usually fixes it.",
|
|
661
|
-
"目录里有 {0} 个服务,都被关键词、分类或「只要免密钥」筛掉了。": "The catalog has {0} servers, all filtered out by keyword, category or 'Keyless only'.",
|
|
662
634
|
"目录里有 {0} 个条目,「只看可直接安装」把不具备传输类型的挡掉了。": "The catalog has {0} entries; 'Directly installable only' hides the ones without a transport type.",
|
|
663
635
|
"看板分类": "Board categories",
|
|
664
636
|
"看这次改了什么": "See what changed",
|
|
@@ -675,7 +647,8 @@ const EN_US_MESSAGES = {
|
|
|
675
647
|
"第 {0} / {1} 页 · 共 {2} 个技能": "Page {0} / {1} · {2} skills",
|
|
676
648
|
"第三方接口的字段可能随上游改版变化,安装前请在详情里核对传输类型与凭据要求。": "Third-party API fields may change upstream; verify transport type and credential requirements in details before installing.",
|
|
677
649
|
"筛选": "Filters",
|
|
678
|
-
"管理": "Manage",
|
|
650
|
+
"管理{0}…": "Manage {0}…",
|
|
651
|
+
"给 dsh-armory 点 Star(新标签页打开 GitHub 仓库)": "Star dsh-armory on GitHub (opens the repo in a new tab)",
|
|
679
652
|
"综合评分": "Overall score",
|
|
680
653
|
"编辑": "Edit",
|
|
681
654
|
"编辑 MCP": "Edit MCP",
|
|
@@ -696,13 +669,11 @@ const EN_US_MESSAGES = {
|
|
|
696
669
|
"详情": "Details",
|
|
697
670
|
"请使用 {0} 的 MCP 工具(mcp__{1}__*)处理接下来的任务": "Use {0}'s MCP tools (mcp__{1}__*) for the tasks ahead",
|
|
698
671
|
"请填写 MCP 名称": "Please fill in the MCP name",
|
|
699
|
-
"读取 MCP 目录失败": "Failed to read the MCP catalog",
|
|
700
672
|
"读取 skill 内容…": "Reading skill content…",
|
|
701
673
|
"读取 skill 内容失败": "Failed to read skill content",
|
|
702
674
|
"读取 skill 列表失败": "Failed to read the skill list",
|
|
703
675
|
"读取专家目录失败": "Failed to read the expert catalog",
|
|
704
676
|
"读取中…": "Reading…",
|
|
705
|
-
"读取内置 MCP 目录": "Read the built-in MCP catalog",
|
|
706
677
|
"读取已召唤专家失败": "Failed to read summoned experts",
|
|
707
678
|
"读取技能目录失败": "Failed to read the skill catalog",
|
|
708
679
|
"读取本机客户端配置失败": "Failed to read local client configs",
|
|
@@ -744,7 +715,6 @@ const EN_US_MESSAGES = {
|
|
|
744
715
|
"连接器详情": "Connector details",
|
|
745
716
|
"连接失败": "Connection failed",
|
|
746
717
|
"连接状态": "Connection status",
|
|
747
|
-
"连接:": "Connection: ",
|
|
748
718
|
"送还": "Return",
|
|
749
719
|
"送还后此人设从所有会话移除,确认送还?": "Returning removes this persona from all sessions. Confirm return?",
|
|
750
720
|
"送还失败": "Return failed",
|
|
@@ -773,10 +743,7 @@ const EN_US_MESSAGES = {
|
|
|
773
743
|
"零调用的 MCP 工具 · 点击下钻": "Zero-call MCP tools · click to drill down",
|
|
774
744
|
"零调用的 MCP 工具(已筛选)": "Zero-call MCP tools (filtered)",
|
|
775
745
|
"需上游提供传输类型": "Needs upstream transport type",
|
|
776
|
-
"需填参数": "Needs arguments",
|
|
777
|
-
"需密钥": "Needs a key",
|
|
778
746
|
"需更新 · 点击更新": "Update available · click to update",
|
|
779
|
-
"需连接信息": "Needs connection info",
|
|
780
747
|
"项目 .agents/skills": "Project .agents/skills",
|
|
781
748
|
"项目 .dsh/skills": "Project .dsh/skills",
|
|
782
749
|
"项目级": "Project-level",
|
|
@@ -875,7 +842,6 @@ const EN_US_MESSAGES = {
|
|
|
875
842
|
"页": "page",
|
|
876
843
|
"跳转到指定页": "Jump to a specific page",
|
|
877
844
|
"共 {0} 条": "{0} total",
|
|
878
|
-
"免认证": "No auth",
|
|
879
845
|
"宿主只提供 skill 的读取接口;这里可以卸载用户级/项目级目录里的 skill(永久删除,不可撤销)。随 DSH 分发、运行时注册、自定义目录的 skill 不可卸载——要改内容请直接编辑 SKILL.md。": "The host only exposes read endpoints for skills. Skills in user-level/project-level directories can be uninstalled here (permanent deletion, no undo). Skills shipped with DSH, registered at runtime, or living in custom directories cannot be uninstalled — edit SKILL.md directly to change them.",
|
|
880
846
|
"将从磁盘永久删除 {0}(不可撤销),确认卸载?": "This permanently deletes {0} from disk (no undo). Uninstall?",
|
|
881
847
|
"确认卸载": "Uninstall",
|
|
@@ -1006,10 +972,120 @@ const EN_US_MESSAGES = {
|
|
|
1006
972
|
"{0} README 预览": "{0} · README preview",
|
|
1007
973
|
"正在读取 README…": "Loading README…",
|
|
1008
974
|
"README 过长,已截断显示。": "README is long; showing a truncated version.",
|
|
975
|
+
"跳到末尾": "Jump to end",
|
|
976
|
+
"滑动到 README 末尾": "Scroll to the end of README",
|
|
1009
977
|
"重新读取": "Reload",
|
|
1010
978
|
"读取 README 失败:未知错误": "Failed to read README: unknown error",
|
|
1011
979
|
"预览服务尚未加载,请完全退出 DSH 后重新打开再试": "The preview service is not loaded yet. Fully quit DSH, reopen it, then try again.",
|
|
1012
980
|
|
|
981
|
+
"目录维护方:{0}": "Catalog maintained by: {0}",
|
|
982
|
+
"目录更新于 {0}": "Catalog updated {0}",
|
|
983
|
+
"查看来源": "View source",
|
|
984
|
+
"更新于 {0}": "Updated {0}",
|
|
985
|
+
"专家人设": "Expert persona",
|
|
986
|
+
"加入时间 {0}": "Added {0}",
|
|
987
|
+
"人设更新于 {0}": "Persona updated {0}",
|
|
988
|
+
"已归档": "Archived",
|
|
989
|
+
"近 30 天有提交": "Committed in the last 30 days",
|
|
990
|
+
"最近更新 {0}": "Last updated {0}",
|
|
991
|
+
"超过半年未更新": "No updates for over 6 months",
|
|
992
|
+
"打开维护者主页": "Open maintainer profile",
|
|
993
|
+
"来源未标注": "Source not listed",
|
|
994
|
+
"读取维护动态…": "Loading maintenance activity…",
|
|
995
|
+
"最近提交 {0}": "Last commit {0}",
|
|
996
|
+
"{0} 个开放 issue": "{0} open issues",
|
|
997
|
+
"维护动态读取失败:{0}": "Couldn't load maintenance activity: {0}",
|
|
998
|
+
"维护动态读取中…": "Loading maintenance activity…",
|
|
999
|
+
"这个条目没有公开仓库": "This entry has no public repository",
|
|
1000
|
+
"条目": "Item",
|
|
1001
|
+
"未命名条目": "Unnamed item",
|
|
1002
|
+
"条目:{0}": "Item: {0}",
|
|
1003
|
+
"类型:{0}": "Type: {0}",
|
|
1004
|
+
"来源:{0}": "Source: {0}",
|
|
1005
|
+
"版本:{0}": "Version: {0}",
|
|
1006
|
+
"发生了什么": "What happened",
|
|
1007
|
+
"环境": "Environment",
|
|
1008
|
+
"系统:{0}": "System: {0}",
|
|
1009
|
+
"{0} · {1} 的反馈": "{0} · feedback on {1}",
|
|
1010
|
+
"找到真实的人": "Reach the real people",
|
|
1011
|
+
"去仓库": "Repository",
|
|
1012
|
+
"看讨论 / 提 Issue": "Discussions / Issues",
|
|
1013
|
+
"来源主页": "Source page",
|
|
1014
|
+
"向军械库反馈": "Report to Armory",
|
|
1015
|
+
"已复制模板": "Template copied",
|
|
1016
|
+
"复制反馈模板": "Copy feedback template",
|
|
1017
|
+
"反馈会打开 GitHub 新标签页;内容先由你过目,确认后才提交。": "Feedback opens a new GitHub tab; you review the text before submitting it.",
|
|
1018
|
+
"专家中心": "Expert Center",
|
|
1019
|
+
"连接器市场": "Connector market",
|
|
1020
|
+
"最近动静": "Recent activity",
|
|
1021
|
+
"暂无新变化 · 上次对比 {0} · 只在本机对比快照": "No new changes · last compared {0} · local snapshot comparison only",
|
|
1022
|
+
"已开始记录市场动态;下次打开时这里会列出真实变化": "Started tracking market activity; real changes will appear here next time",
|
|
1023
|
+
"自上次打开市场以来,目录里真实发生的变化(本机快照对比)": "Real catalog changes since you last opened the market (local snapshot comparison)",
|
|
1024
|
+
"清除": "Clear",
|
|
1025
|
+
"{0} 新增 {1} 条": "{0}: {1} added",
|
|
1026
|
+
"{0} 移除 {1} 条": "{0}: {1} removed",
|
|
1027
|
+
"{0} 更新 {1} 条": "{0}: {1} updated",
|
|
1028
|
+
" 等": " and more",
|
|
1029
|
+
"跳到对应市场": "Jump to the matching market",
|
|
1030
|
+
"核验 {0}": "Verified {0}",
|
|
1031
|
+
"已验证 · dsh {0}": "Verified · dsh {0}",
|
|
1032
|
+
// 底栏两枚 tooltip:口径是要命的信息,不能省。「↓」是近 30 天(技能卡那枚是累计),
|
|
1033
|
+
// 「核验」是目录批次核验时间(不是插件自己的更新时间),英文必须把这两点讲全。
|
|
1034
|
+
"npm 近 30 天下载量": "npm downloads in the last 30 days",
|
|
1035
|
+
"核验日期来自目录批次核验,不是插件自身的更新时间": "The verification date is when the catalog batch was checked, not when this plugin was updated",
|
|
1036
|
+
"查看 {0} 的 README": "View the README for {0}",
|
|
1037
|
+
"来源:WorkBuddy 官方分发": "Source: official WorkBuddy distribution",
|
|
1038
|
+
|
|
1039
|
+
"装备清单": "Loadout",
|
|
1040
|
+
"导出": "Export",
|
|
1041
|
+
"导入对比": "Compare import",
|
|
1042
|
+
"把本机的 MCP、技能、专家与插件导出成一份可分享的清单;导入只做对比,不会自动安装。": "Export your local MCP servers, skills, experts, and plugins as a shareable list. Import only compares — nothing is installed automatically.",
|
|
1043
|
+
"清单名称": "Loadout name",
|
|
1044
|
+
"我的 DSH 装备清单": "My DSH loadout",
|
|
1045
|
+
"读取本机配置…": "Reading local setup…",
|
|
1046
|
+
"重新生成": "Regenerate",
|
|
1047
|
+
"超过 200 条,只导出前 200 条。": "More than 200 items; only the first 200 are exported.",
|
|
1048
|
+
"复制 JSON": "Copy JSON",
|
|
1049
|
+
"已复制 JSON": "JSON copied",
|
|
1050
|
+
"下载 .json": "Download .json",
|
|
1051
|
+
"已开始下载": "Download started",
|
|
1052
|
+
"浏览器不允许下载,请改用复制 JSON": "The browser blocked the download; use Copy JSON instead",
|
|
1053
|
+
"还没有生成清单。": "No loadout generated yet.",
|
|
1054
|
+
"这份清单只含名称、类型、来源与版本;不会写入密钥、header 或环境变量的值。": "This loadout only includes names, types, sources, and versions; it never contains keys, headers, or environment variable values.",
|
|
1055
|
+
"把朋友发来的清单贴在这里,或选择文件。导入只做对比,不会自动安装。": "Paste a loadout a friend sent you, or choose a file. Import only compares — nothing is installed automatically.",
|
|
1056
|
+
"粘贴清单 JSON…": "Paste loadout JSON…",
|
|
1057
|
+
"选择 .json 文件": "Choose a .json file",
|
|
1058
|
+
"文件过大(上限 256KB)": "File too large (256KB max)",
|
|
1059
|
+
"文件读取失败:{0}": "Couldn't read the file: {0}",
|
|
1060
|
+
"先粘贴一份清单,或选择 .json 文件。": "Paste a loadout or choose a .json file first.",
|
|
1061
|
+
"不是有效的 JSON:{0}": "Not valid JSON: {0}",
|
|
1062
|
+
"对比失败": "Comparison failed",
|
|
1063
|
+
"对比中…": "Comparing…",
|
|
1064
|
+
"对比本机": "Compare with this device",
|
|
1065
|
+
"清单为空。": "The loadout is empty.",
|
|
1066
|
+
"共 {0} 项 · 已安装 {1} · 可更新 {2} · 本机没有 {3}": "{0} items · {1} installed · {2} updates · {3} missing",
|
|
1067
|
+
"清单来自 {0}": "Loadout from {0}",
|
|
1068
|
+
"本机:{0}": "Local: {0}",
|
|
1069
|
+
"可更新": "Update available",
|
|
1070
|
+
"本机没有": "Not installed",
|
|
1071
|
+
"去市场": "Find in market",
|
|
1072
|
+
"复制名称": "Copy name",
|
|
1073
|
+
"已复制名称": "Name copied",
|
|
1074
|
+
"导入只做对比;点「去市场」逐项安装,不会自动写入任何配置。": "Import only compares; use Find in market to install each item. Nothing is written automatically.",
|
|
1075
|
+
"内置 · {0}": "Built-in · {0}",
|
|
1076
|
+
"连接器 · {0}": "Connector · {0}",
|
|
1077
|
+
"手动配置": "Manual config",
|
|
1078
|
+
"WorkBuddy 专家中心": "WorkBuddy Expert Center",
|
|
1079
|
+
"本机 / 自带": "Local / bundled",
|
|
1080
|
+
"导出失败": "Export failed",
|
|
1081
|
+
"生成失败:{0}": "Couldn't generate: {0}",
|
|
1082
|
+
"已定位到连接器市场:{0}": "Opened the connector market: {0}",
|
|
1083
|
+
"这是手动配置的 MCP,名称已复制;可到 MCP 页自行添加。": "This MCP was configured manually; its name is copied. Add it on the MCP page.",
|
|
1084
|
+
"已定位到技能市场:{0}": "Opened the skill market: {0}",
|
|
1085
|
+
"已定位到专家市场:{0}": "Opened the expert market: {0}",
|
|
1086
|
+
"已定位到插件市场:{0}": "Opened the plugin market: {0}",
|
|
1087
|
+
"导出本机配置,或导入朋友的清单做差异对比": "Export your local setup, or import a friend's loadout to see the differences",
|
|
1088
|
+
|
|
1013
1089
|
":": ": ",
|
|
1014
1090
|
};
|
|
1015
1091
|
// 模块加载时先用 ①/③ 判定的 locale 装载一次;apply() 拿到宿主 locale 后会再按
|
|
@@ -1042,13 +1118,13 @@ const useI18nRevision = () => {
|
|
|
1042
1118
|
useEffect(() => onI18nChange(() => setRevision((value) => value + 1)), []);
|
|
1043
1119
|
};
|
|
1044
1120
|
|
|
1045
|
-
// ── P1
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1121
|
+
// ── P1:数据型英文覆盖(更新日志)───────────────────────────────────────────
|
|
1122
|
+
// 这类文案不是 UI 字面量,而是宿主随 RPC 送来的数据:更新日志在 whats-new.js。
|
|
1123
|
+
// 它不能走 t() 字典(键集合由那个文件决定,硬抄进字典会让 i18n-coverage 的「死键」
|
|
1124
|
+
// 断言永久变红),所以由宿主同时送中文与英文字段,客户端按当前 locale 选择。
|
|
1125
|
+
// 英文缺失时逐条回落中文,绝不渲染 undefined。
|
|
1050
1126
|
//
|
|
1051
|
-
//
|
|
1127
|
+
// helper 的 `english` 参数只用于测试时显式指定;运行时用当前 locale 判定。
|
|
1052
1128
|
const localizeWhatsNewEntries = (entries, english = usesEnglish()) => {
|
|
1053
1129
|
if (!english || !Array.isArray(entries)) return entries;
|
|
1054
1130
|
return entries.map((entry) => ({
|
|
@@ -1062,20 +1138,6 @@ const localizeWhatsNewEntries = (entries, english = usesEnglish()) => {
|
|
|
1062
1138
|
: [],
|
|
1063
1139
|
}));
|
|
1064
1140
|
};
|
|
1065
|
-
const localizeBuiltinItem = (item, english = usesEnglish()) => {
|
|
1066
|
-
if (!english || !item) return item;
|
|
1067
|
-
const authNote = item.authNoteEn || item.authNote;
|
|
1068
|
-
// 只在英文字段真的存在时才覆盖:缺译文时保留原键(连 undefined 键都不新增),
|
|
1069
|
-
// 面板据此区分「没有说明」与「说明是空的」。
|
|
1070
|
-
return {
|
|
1071
|
-
...item,
|
|
1072
|
-
...(item.labelEn ? { label: item.labelEn } : {}),
|
|
1073
|
-
...(item.summaryEn ? { summary: item.summaryEn } : {}),
|
|
1074
|
-
...(item.accessEn ? { access: item.accessEn } : {}),
|
|
1075
|
-
...(authNote ? { authNote } : {}),
|
|
1076
|
-
...(item.categoryLabelEn ? { categoryLabel: item.categoryLabelEn } : {}),
|
|
1077
|
-
};
|
|
1078
|
-
};
|
|
1079
1141
|
|
|
1080
1142
|
// ── 旧插件名 → 新插件名的 localStorage 键迁移 ──────────────────────────────
|
|
1081
1143
|
// 1.9.0 起本插件由 dsh-skill-mcp-board 更名为 dsh-armory(package.json 与
|
|
@@ -1144,7 +1206,6 @@ function writeAwesomeSnapshot(payload) {
|
|
|
1144
1206
|
localStorage.setItem(AWESOME_SNAPSHOT_KEY, text);
|
|
1145
1207
|
} catch { /* 配额溢出 / 隐私模式:快照只是加速,失败不致命 */ }
|
|
1146
1208
|
}
|
|
1147
|
-
|
|
1148
1209
|
// ── 更新探测节流:把「每次进面板都 HEAD 一发」压到 6 小时一次 ────────────────
|
|
1149
1210
|
// 目录本身已有服务端 stale-while-revalidate 在后台自动换新,这个 ETag 探测只是
|
|
1150
1211
|
// 锦上添花;没必要每开一次面板就多打一次网络、多一次误报机会。
|
|
@@ -1294,6 +1355,12 @@ function ensureReadmeStyle() {
|
|
|
1294
1355
|
'.dsh-mcp-readme-modal{width:min(880px,94vw);max-width:94vw;max-height:86vh}',
|
|
1295
1356
|
'.dsh-mcp-readme-meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:12px;opacity:.75}',
|
|
1296
1357
|
'.dsh-mcp-readme-body{margin-top:12px;overflow-wrap:anywhere}',
|
|
1358
|
+
// 长 README 的右下角「跳到末尾」:sticky 让它不随正文滚走,又留在弹层内部,
|
|
1359
|
+
// 焦点陷阱、Esc 关闭与「点遮罩关闭」都不受影响。外层不拦鼠标,只有按钮可点。
|
|
1360
|
+
'.dsh-mcp-readme-jump-wrap{position:sticky;bottom:12px;z-index:2;display:flex;justify-content:flex-end;margin-top:12px;pointer-events:none}',
|
|
1361
|
+
'.dsh-mcp-readme-jump{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:999px;border:1px solid var(--mcp-line);background:var(--mcp-bg-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;font-weight:600;line-height:1.2;cursor:pointer;pointer-events:auto;box-shadow:var(--dsw-elevation-prominent);transition:border-color .12s,background .12s}',
|
|
1362
|
+
'.dsh-mcp-readme-jump:hover{border-color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
1363
|
+
'.dsh-mcp-readme-jump:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:2px}',
|
|
1297
1364
|
'.dsh-mcp-md-h{margin:20px 0 8px;font-weight:600;line-height:1.35;letter-spacing:0}',
|
|
1298
1365
|
'.dsh-mcp-md-h:first-child{margin-top:0}',
|
|
1299
1366
|
'.dsh-mcp-md-p{margin:8px 0;line-height:1.72}',
|
|
@@ -1870,6 +1937,55 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
1870
1937
|
.dsh-mcp-card.wb.off{opacity:.82}
|
|
1871
1938
|
.dsh-mcp-card.wb .dsh-mcp-card-summary{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:11.5px;color:var(--mcp-mut);line-height:1.5;min-height:2.9em}
|
|
1872
1939
|
.dsh-mcp-wb-card-actions{display:flex;align-items:center;gap:8px;margin-top:auto;flex-wrap:wrap}
|
|
1940
|
+
/* ── DSH 社区插件卡片:五条固定水平线(头部 34 / 摘要 38 / 标签 18 / 页脚 38)──
|
|
1941
|
+
只作用于 .dsh-mcp-pcard,连接器 / 技能 / 专家卡片不受影响。
|
|
1942
|
+
卡片固定 176px:标题永远一行,标签最多三个,按钮落在同一水平线。 */
|
|
1943
|
+
.dsh-mcp-pcard{display:flex;flex-direction:column;gap:7px;min-width:0;min-height:176px;padding:12px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1);cursor:pointer;transition:border-color .15s,box-shadow .15s,background .15s}
|
|
1944
|
+
.dsh-mcp-pcard:hover{border-color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover);box-shadow:0 10px 28px color-mix(in srgb,var(--mcp-accent) 12%,transparent)}
|
|
1945
|
+
.dsh-mcp-pcard:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:2px}
|
|
1946
|
+
.dsh-mcp-pcard.off{opacity:.82}
|
|
1947
|
+
.dsh-mcp-phead{display:grid;grid-template-columns:32px minmax(0,1fr) auto;align-items:center;gap:10px;min-height:34px}
|
|
1948
|
+
.dsh-mcp-pavatar{position:relative;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;overflow:hidden;border:1px solid var(--mcp-line);border-radius:50%;corner-shape:round;background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--mcp-accent);font-size:12px;font-weight:650}
|
|
1949
|
+
.dsh-mcp-pavatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
1950
|
+
.dsh-mcp-piden{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
1951
|
+
.dsh-mcp-pname{margin:0;font-size:14px;line-height:1.3;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1952
|
+
.dsh-mcp-pmeta{display:flex;align-items:center;gap:5px;min-width:0;overflow:hidden;font-size:10.5px;line-height:1.4;color:var(--mcp-mut);white-space:nowrap}
|
|
1953
|
+
.dsh-mcp-pmeta .official{display:inline-flex;align-items:center;gap:4px;flex:none;color:var(--dsw-alias-label-primary);font-weight:600}
|
|
1954
|
+
.dsh-mcp-pmeta .odot{width:5px;height:5px;flex:none;border-radius:50%;background:var(--mcp-accent)}
|
|
1955
|
+
.dsh-mcp-pmeta .pdot{flex:none;opacity:.55}
|
|
1956
|
+
.dsh-mcp-prepo{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
1957
|
+
.dsh-mcp-pverified{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex:none;justify-self:end;color:var(--dsw-alias-state-success-primary)}
|
|
1958
|
+
.dsh-mcp-pdesc{margin:0;height:38px;overflow:hidden;font-size:12px;line-height:1.55;color:var(--mcp-mut);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
|
1959
|
+
.dsh-mcp-pchips{display:flex;align-items:center;gap:5px;height:18px;min-width:0;overflow:hidden;white-space:nowrap}
|
|
1960
|
+
.dsh-mcp-pchip{display:inline-flex;align-items:center;flex:none;padding:0 7px;border:1px solid var(--mcp-line);border-radius:999px;background:var(--mcp-bg-2);font-size:10.5px;line-height:16px;color:var(--mcp-mut)}
|
|
1961
|
+
.dsh-mcp-pchip.more{border-style:dashed}
|
|
1962
|
+
/* 包名芯片是标签行里唯一允许收缩的一枚(其余 flex:none):放不下时它自己省略号截断,
|
|
1963
|
+
分类/标签不会被挤成半截。行高仍由 .dsh-mcp-pchip 的 line-height:16px 决定 → 18px 不漂。*/
|
|
1964
|
+
.dsh-mcp-pchip.mono{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;font-family:var(--mcp-mono);font-size:10px;letter-spacing:.2px;background:var(--mcp-bg-3)}
|
|
1965
|
+
.dsh-mcp-pfoot{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0;min-height:38px;margin-top:auto;padding-top:8px;border-top:1px solid var(--mcp-line)}
|
|
1966
|
+
/* 底栏事实行(2.3.x 起 3 枚:↓近30天下载 / ★星数 / 核验日期):
|
|
1967
|
+
· 字号 10.5→11.5px。实测卡高由 .dsh-mcp-pfoot 的 min-height:38 决定(footH 恒 47),
|
|
1968
|
+
这行字变大不动卡片一 px——用户明确要求「信息更丰富但卡不再变大」。
|
|
1969
|
+
· 去掉两枚「·」分隔点(每枚连 gap ≈12.7px),改成纯 gap:7px 间距,把省下的宽度
|
|
1970
|
+
付给新出现的「↓」,而不是让它去挤日期。
|
|
1971
|
+
· flex:1 1 0 + position:relative:这一行的盒子宽度**恒等于可用宽**(不随内容伸缩),
|
|
1972
|
+
JS 量到的 clientWidth 就是真可用宽;同时它独吞全部收缩,右侧按钮(.dsh-mcp-pbtn
|
|
1973
|
+
flex:none)永不被挤到换行——换行会把卡撑高。
|
|
1974
|
+
· .probe 是离流探针(width:max-content 才不会被可用宽夹住,否则永远量不出溢出),
|
|
1975
|
+
与真行同构,用来判断「三枚全画要多宽」;放不下时整枚收起日期,不裁半截字。 */
|
|
1976
|
+
.dsh-mcp-pfacts{position:relative;flex:1 1 0;display:flex;align-items:center;gap:7px;min-width:0;overflow:hidden;font-size:11.5px;line-height:1.4;color:var(--mcp-mut);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
1977
|
+
.dsh-mcp-pfacts .dl{display:inline-flex;align-items:center;flex:none;color:var(--dsw-alias-label-primary);opacity:.72}
|
|
1978
|
+
.dsh-mcp-pfacts .star{display:inline-flex;align-items:center;gap:3px;flex:none;color:var(--dsw-alias-label-primary);opacity:.72}
|
|
1979
|
+
.dsh-mcp-pfacts .when{flex:none;opacity:.85}
|
|
1980
|
+
.dsh-mcp-pfacts-probe{position:absolute;left:0;top:0;width:max-content;display:flex;align-items:center;gap:7px;white-space:nowrap;visibility:hidden;pointer-events:none}
|
|
1981
|
+
.dsh-mcp-pbtn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 12px;flex:none;border:1px solid var(--mcp-accent);border-radius:8px;background:var(--mcp-accent);color:#fff;font:inherit;font-size:12px;font-weight:600;line-height:1;cursor:pointer;transition:background .12s,border-color .12s,opacity .12s}
|
|
1982
|
+
.dsh-mcp-pbtn:hover:not(:disabled){border-color:var(--mcp-accent);background:color-mix(in srgb,var(--mcp-accent) 86%,#000)}
|
|
1983
|
+
.dsh-mcp-pbtn:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:2px}
|
|
1984
|
+
.dsh-mcp-pbtn:disabled{opacity:.55;cursor:not-allowed}
|
|
1985
|
+
.dsh-mcp-pbtn.installed{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-label-primary)}
|
|
1986
|
+
.dsh-mcp-pbtn.installed:hover:not(:disabled){border-color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 14%,transparent)}
|
|
1987
|
+
.dsh-mcp-phost{display:inline-flex;align-items:center;gap:4px;flex:none;min-width:0;font-size:10.5px;line-height:1.4;color:var(--mcp-mut);white-space:nowrap}
|
|
1988
|
+
.dsh-mcp-phost .ico{flex:none;color:var(--mcp-warn)}
|
|
1873
1989
|
.dsh-mcp-wb-blocked{font-size:10.5px;color:var(--mcp-warn);line-height:1.4;min-width:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
|
1874
1990
|
.dsh-mcp-wb-pager{display:flex;align-items:center;justify-content:center;gap:12px;padding:6px 0 2px}
|
|
1875
1991
|
.dsh-mcp-page-jump{display:flex;align-items:center;gap:4px;font-size:11.5px;color:var(--mcp-mut)}
|
|
@@ -1878,6 +1994,8 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
1878
1994
|
.dsh-mcp-readonly-note{display:flex;align-items:flex-start;gap:5px;font-size:10.5px;line-height:1.5;color:var(--mcp-mut);padding:5px 7px;border-radius:var(--mcp-r-sm);background:var(--mcp-bg-3)}
|
|
1879
1995
|
.dsh-mcp-readonly-note>svg{flex:none;margin-top:1px;opacity:.75}
|
|
1880
1996
|
.dsh-mcp-wb-modal{width:640px;max-width:92vw}
|
|
1997
|
+
.dsh-mcp-wb-modal-head{display:flex;align-items:center;gap:10px;min-width:0}
|
|
1998
|
+
.dsh-mcp-wb-modal-head h3{margin:0;min-width:0;overflow-wrap:anywhere}
|
|
1881
1999
|
.dsh-mcp-wb-modal-desc{font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
1882
2000
|
.dsh-mcp-wb-modal-note{font-size:11.5px;line-height:1.6;color:var(--mcp-mut)}
|
|
1883
2001
|
.dsh-mcp-wb-examples{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary);background:var(--mcp-bg-2);border-radius:var(--mcp-r-sm);padding:8px 12px;margin:10px 0}
|
|
@@ -1895,7 +2013,7 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
1895
2013
|
.dsh-mcp-wb-roster-prof{font-size:11px;color:var(--mcp-mut);flex:none}
|
|
1896
2014
|
.dsh-mcp-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px;flex-wrap:wrap}
|
|
1897
2015
|
.dsh-mcp-advanced{margin-top:14px}.dsh-mcp-check{display:flex!important;align-items:center;gap:8px;margin:9px 0!important;color:var(--dsw-alias-label-primary)!important}.dsh-mcp-check input{width:auto!important}.dsh-mcp-number-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dsh-mcp-number-grid .dsh-mcp-field{margin-bottom:0}.dsh-mcp-import-preview{font-size:12px;line-height:1.6;color:var(--mcp-mut);white-space:pre-wrap}.dsh-mcp-import-json{min-height:220px;resize:vertical;font-family:var(--mcp-mono)!important}
|
|
1898
|
-
.dsh-mcp-builtin-
|
|
2016
|
+
.dsh-mcp-builtin-count{font-size:11.5px;color:var(--mcp-mut)}
|
|
1899
2017
|
.dsh-mcp-source-box{margin:0 0 14px;flex:none}.dsh-mcp-source-list{display:flex;flex-direction:column;gap:6px;max-height:186px;overflow-y:auto;margin-top:7px}.dsh-mcp-source-row{box-sizing:border-box;display:flex;flex-direction:column;gap:3px;width:100%;padding:8px 10px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;line-height:1.45;text-align:left;cursor:pointer;transition:border-color .12s,background .12s}.dsh-mcp-source-row:hover:not(:disabled){border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dsh-mcp-source-row.sel{border-color:var(--mcp-accent);background:var(--mcp-bg-2)}.dsh-mcp-source-row:disabled{cursor:default;opacity:.7}.dsh-mcp-source-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.dsh-mcp-source-name{font-weight:600}.dsh-mcp-source-path{font-family:var(--mcp-mono);font-size:10.5px;color:var(--dsw-alias-brand-text);overflow-wrap:anywhere}.dsh-mcp-source-detail{font-size:11px;color:var(--mcp-mut);overflow-wrap:anywhere}.dsh-mcp-source-note{font-size:10.5px;color:var(--mcp-mut)}.dsh-mcp-source-warn{font-size:10.5px;color:var(--mcp-warn)}
|
|
1900
2018
|
/* OPT-17 · 2.5:不支持分组的呈现(复用 source-row 的排版,只补两处色语与分组间距) */
|
|
1901
2019
|
.dsh-mcp-source-unsupported{font-size:11px;line-height:1.5;color:var(--mcp-warn);overflow-wrap:anywhere}
|
|
@@ -2006,7 +2124,8 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2006
2124
|
「整块突现→重绘」残影。定位 position:fixed(由 JS 实测 trigger 视口坐标后
|
|
2007
2125
|
内联写入):absolute 锚定会让菜单变高时伸展祖先滚动容器,整个会话页跟着
|
|
2008
2126
|
跳动;fixed 脱离滚动容器,菜单再高页面也零位移。left/top/bottom 全部内联。 */
|
|
2009
|
-
|
|
2127
|
+
/* max-width:窄窗下菜单本身也不许顶出视口(width 是内容盒,+10 padding +2 border 仍留 8px 余量)。 */
|
|
2128
|
+
.dsh-mcp-quick-menu{position:fixed;z-index:950;min-width:128px;max-width:calc(100vw - 20px);padding:5px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-lg);background:var(--mcp-bg-1);box-shadow:var(--dsw-elevation-overlay,0 8px 28px rgba(0,0,0,.14));transform-origin:left bottom;animation:dshMcpMenuIn .12s ease-out}
|
|
2010
2129
|
@keyframes dshMcpMenuIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
2011
2130
|
@media (prefers-reduced-motion: reduce){.dsh-mcp-quick-menu{animation:none}.dsh-mcp-quick-sub{animation:none}}
|
|
2012
2131
|
.dsh-mcp-quick-row{position:relative}
|
|
@@ -2019,7 +2138,8 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2019
2138
|
出场动画只动 opacity/transform(动效克制),替代上一版「整块突现」的残影来源。
|
|
2020
2139
|
面板本体 max-height 由「6 行列表 + 底部固定管理按钮」精确算出(6×28 + 5×2 padding
|
|
2021
2140
|
+ 30 按钮 + 4 间距 + 边框),列表自身滚动,管理按钮永远贴底可见。 */
|
|
2022
|
-
|
|
2141
|
+
/* 232px 是常用的展开宽度;max-width 保证极窄窗下也不横出视口。 */
|
|
2142
|
+
.dsh-mcp-quick-sub{position:absolute;top:50%;left:calc(100% + 4px);z-index:951;display:flex;flex-direction:column;width:232px;max-width:calc(100vw - 28px);max-height:min(216px,40vh);padding:5px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-lg);background:var(--mcp-bg-1);box-shadow:var(--dsw-elevation-overlay,0 8px 28px rgba(0,0,0,.14));transform:translateY(-50%);transform-origin:left center;animation:dshMcpQuickIn .13s ease-out}
|
|
2023
2143
|
/* 可滚动列表:最多 6 行(6 × 28px),溢出即滚动;隐藏水平溢出防止拖宽面板 */
|
|
2024
2144
|
.dsh-mcp-quick-list{min-height:0;max-height:168px;overflow-y:auto;overflow-x:hidden;border-radius:var(--mcp-r-sm)}
|
|
2025
2145
|
/* 细滚动条(跟随主题 token,悬停加深;hover 之外 6px 宽半透明,几乎不占视觉) */
|
|
@@ -2030,10 +2150,18 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2030
2150
|
.dsh-mcp-quick-list:hover::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--dsw-alias-label-tertiary) 45%,transparent)}
|
|
2031
2151
|
.dsh-mcp-quick-list:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:-1px}
|
|
2032
2152
|
/* 视口下缘不够时 JS 加 flip:子菜单改为向锚点行上方展开 */
|
|
2033
|
-
|
|
2153
|
+
/* 向上展开 = 子菜单底边贴锚点行底边、向上生长。旧写法 bottom:50% + translateY(50%)
|
|
2154
|
+
与默认的 top:50% + translateY(-50%) 数学上等价,等于没翻,子菜单照样越过视口下缘。 */
|
|
2155
|
+
.dsh-mcp-quick-row.flip .dsh-mcp-quick-sub{top:auto;bottom:0;transform:none}
|
|
2034
2156
|
.dsh-mcp-quick-row.flip .dsh-mcp-quick-sub{animation-name:dshMcpQuickInFlip}
|
|
2157
|
+
/* 视口右缘不够时 JS 加 hflip:子菜单改到锚点行左侧展开,出场动画镜像。
|
|
2158
|
+
菜单本体贴屏幕右边时,这一侧才是「向内开」,否则 232px 的第二级会整块伸出视口。 */
|
|
2159
|
+
.dsh-mcp-quick-row.hflip .dsh-mcp-quick-sub{left:auto;right:calc(100% + 4px);transform-origin:right center;animation-name:dshMcpQuickInLeft}
|
|
2160
|
+
.dsh-mcp-quick-row.hflip.flip .dsh-mcp-quick-sub{animation-name:dshMcpQuickInFlipLeft}
|
|
2035
2161
|
@keyframes dshMcpQuickIn{from{opacity:0;transform:translateY(-50%) translateX(-4px)}to{opacity:1;transform:translateY(-50%)}}
|
|
2036
|
-
@keyframes dshMcpQuickInFlip{from{opacity:0;transform:translateY(
|
|
2162
|
+
@keyframes dshMcpQuickInFlip{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
2163
|
+
@keyframes dshMcpQuickInLeft{from{opacity:0;transform:translateY(-50%) translateX(4px)}to{opacity:1;transform:translateY(-50%)}}
|
|
2164
|
+
@keyframes dshMcpQuickInFlipLeft{from{opacity:0;transform:translateX(4px)}to{opacity:1;transform:none}}
|
|
2037
2165
|
@media (prefers-reduced-motion: reduce){.dsh-mcp-quick-sub{animation:none}}
|
|
2038
2166
|
.dsh-mcp-quick-sub.empty{display:block;padding:9px 11px;color:var(--mcp-mut);font-size:12px}
|
|
2039
2167
|
.dsh-mcp-quick-sub-note{padding:6px 9px;color:var(--mcp-mut);font-size:11px;white-space:normal}
|
|
@@ -2092,6 +2220,39 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2092
2220
|
.dsh-mcp-cat-count{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
|
|
2093
2221
|
.dsh-mcp-cat[aria-pressed="true"] .dsh-mcp-cat-count{color:var(--dsw-alias-label-secondary)}
|
|
2094
2222
|
|
|
2223
|
+
/* ── 顶栏右端「点 Star」──────────────────────────────────────────────
|
|
2224
|
+
分类栏本身是 flex-wrap 的(窄屏换行),所以外面套一行:chip 们占剩余宽度自由换行,
|
|
2225
|
+
star 贴右上不参与换行。它挂在 role=tablist 容器**之外**——tablist 里只该有 tab。 */
|
|
2226
|
+
.dsh-mcp-cats-row{display:flex;align-items:flex-start;gap:10px}
|
|
2227
|
+
.dsh-mcp-cats-row>.dsh-mcp-cats{flex:1 1 auto;min-width:0}
|
|
2228
|
+
.dsh-mcp-star{position:relative;display:inline-flex;align-items:center;gap:6px;flex:none;box-sizing:border-box;padding:5px 11px 5px 9px;border:1px solid var(--mcp-line);border-radius:999px;corner-shape:round;background:var(--mcp-bg-1);color:var(--mcp-mut);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;text-decoration:none;cursor:pointer;transition:border-color .14s,color .14s,background .14s}
|
|
2229
|
+
.dsh-mcp-star:hover{border-color:var(--mcp-cat-auto);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
2230
|
+
.dsh-mcp-star:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--mcp-accent));outline-offset:1px}
|
|
2231
|
+
.dsh-mcp-star-label{font-weight:600}
|
|
2232
|
+
.dsh-mcp-star-count{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary)}
|
|
2233
|
+
.dsh-mcp-star:hover .dsh-mcp-star-count{color:var(--mcp-cat-auto)}
|
|
2234
|
+
/* 星形:平时淡灰描边感(低不透明度实心),hover 转实心琥珀并轻转——「这里可以点」的线索 */
|
|
2235
|
+
.dsh-mcp-star-svg{width:15px;height:15px;flex:none;color:var(--dsw-alias-label-tertiary);transition:color .14s,transform .2s}
|
|
2236
|
+
.dsh-mcp-star:hover .dsh-mcp-star-svg{color:var(--mcp-cat-auto);transform:rotate(18deg) scale(1.12)}
|
|
2237
|
+
.dsh-mcp-star-svg path{fill:currentColor;stroke:none;opacity:.3;transition:opacity .14s}
|
|
2238
|
+
.dsh-mcp-star:hover .dsh-mcp-star-svg path{opacity:1}
|
|
2239
|
+
/* 小交互图案:两颗四角星屑,平时不显示,hover / 点下去才闪出来(不占布局) */
|
|
2240
|
+
.dsh-mcp-star-spark{position:absolute;width:5px;height:5px;border-radius:1px;background:var(--mcp-cat-auto);opacity:0;pointer-events:none;transform:scale(.3)}
|
|
2241
|
+
.dsh-mcp-star-spark.a{left:3px;top:3px}
|
|
2242
|
+
.dsh-mcp-star-spark.b{right:4px;bottom:4px;width:4px;height:4px}
|
|
2243
|
+
.dsh-mcp-star:hover .dsh-mcp-star-spark{animation:dshMcpStarTwinkle .9s ease-out}
|
|
2244
|
+
.dsh-mcp-star:hover .dsh-mcp-star-spark.b{animation-delay:.16s}
|
|
2245
|
+
/* 点下去:星形放大回弹 + 星屑迸一下,明确「已经跳去 GitHub 了」 */
|
|
2246
|
+
.dsh-mcp-star.burst{border-color:var(--mcp-cat-auto)}
|
|
2247
|
+
.dsh-mcp-star.burst .dsh-mcp-star-svg{animation:dshMcpStarPop .55s cubic-bezier(.22,1.4,.36,1)}
|
|
2248
|
+
.dsh-mcp-star.burst .dsh-mcp-star-spark{animation:dshMcpStarTwinkle .55s ease-out}
|
|
2249
|
+
@keyframes dshMcpStarTwinkle{0%{opacity:0;transform:scale(.3) rotate(0)}45%{opacity:1;transform:scale(1) rotate(35deg)}100%{opacity:0;transform:scale(.5) rotate(70deg)}}
|
|
2250
|
+
@keyframes dshMcpStarPop{0%{transform:scale(1) rotate(0)}35%{transform:scale(1.5) rotate(24deg)}70%{transform:scale(.92) rotate(-6deg)}100%{transform:scale(1.12) rotate(18deg)}}
|
|
2251
|
+
/* 窄屏只留图案与数字,文字标签让位给分类 chip 的换行 */
|
|
2252
|
+
@media (max-width:760px){.dsh-mcp-star{padding:5px 9px}.dsh-mcp-star-label{display:none}}
|
|
2253
|
+
/* 减弱动效:星形不再转、不再迸,但 hover 的配色反馈与静态星屑都留着——信息量不打折 */
|
|
2254
|
+
@media (prefers-reduced-motion: reduce){.dsh-mcp-star,.dsh-mcp-star-svg,.dsh-mcp-star-svg path{transition:none}.dsh-mcp-star:hover .dsh-mcp-star-svg{transform:none}.dsh-mcp-star.burst .dsh-mcp-star-svg{animation:none;transform:none}.dsh-mcp-star:hover .dsh-mcp-star-spark,.dsh-mcp-star.burst .dsh-mcp-star-spark{animation:none;opacity:.9}}
|
|
2255
|
+
|
|
2095
2256
|
/* ── Skill 看板 ─────────────────────────────────────────────────── */
|
|
2096
2257
|
.dsh-mcp-skill-view{display:flex;flex-direction:column;gap:12px;min-height:0}
|
|
2097
2258
|
.dsh-mcp-skill-body{display:flex;gap:14px;min-height:0;align-items:stretch}
|
|
@@ -2124,7 +2285,21 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2124
2285
|
/* Skill 看板升级:分类轨道 + 网格/列表双视图 + 详情操作区。
|
|
2125
2286
|
全部走宿主主题令牌(决策 2/10),分类色只出现在色点与徽标,且永远伴随文字标签。 */
|
|
2126
2287
|
.dsh-mcp-skill-toolbar{display:flex;align-items:center;gap:10px;flex:none}
|
|
2127
|
-
|
|
2288
|
+
/* 看板搜索框自带完整样式(此前只有 flex 尺寸,放大镜裸排在输入框外、输入框吃浏览器默认态)。
|
|
2289
|
+
观感对齐 .suite-search:图标内嵌居中、统一 30px 高。聚焦圈只能用 inset 内描边:
|
|
2290
|
+
搜索框贴着 .dsh-mcp-skill-content(overflow 滚动容器)的边缘,向外的 shadow 光环
|
|
2291
|
+
会被裁出残缺口(.dsh-mcp-search 聚焦因此直接 box-shadow:none,同理)。
|
|
2292
|
+
输入非空时容器挂 querying 类,input 的右 padding 给「清空」按钮让位。 */
|
|
2293
|
+
.dsh-mcp-skill-search{position:relative;display:flex;align-items:center;flex:1 1 auto;min-width:0;height:30px}
|
|
2294
|
+
.dsh-mcp-skill-search>svg{position:absolute;top:50%;left:9px;transform:translateY(-50%);pointer-events:none;color:var(--mcp-mut)}
|
|
2295
|
+
.dsh-mcp-skill-search input{box-sizing:border-box;width:100%;height:30px;padding:0 10px 0 28px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);corner-shape:round;background:var(--mcp-bg-1);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;outline:0;transition:border-color .15s,box-shadow .15s}
|
|
2296
|
+
.dsh-mcp-skill-search input:hover{border-color:var(--dsw-alias-border-l3)}
|
|
2297
|
+
.dsh-mcp-skill-search input::placeholder{color:var(--mcp-mut)}
|
|
2298
|
+
.dsh-mcp-skill-search input:focus{border-color:var(--mcp-accent);box-shadow:inset 0 0 0 1px var(--mcp-accent)}
|
|
2299
|
+
.dsh-mcp-skill-search.querying input{padding-right:28px}
|
|
2300
|
+
.dsh-mcp-skill-search .clear{position:absolute;top:50%;right:5px;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:0;border-radius:var(--mcp-r-xs);corner-shape:round;background:0 0;color:var(--mcp-mut);cursor:pointer;transition:background .12s,color .12s}
|
|
2301
|
+
.dsh-mcp-skill-search .clear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
2302
|
+
.dsh-mcp-skill-search .clear:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:1px}
|
|
2128
2303
|
.dsh-mcp-skill-seg{display:inline-flex;flex:none;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);corner-shape:round;background:var(--mcp-bg-2);padding:2px;gap:2px}
|
|
2129
2304
|
.dsh-mcp-skill-seg-btn{display:inline-flex;align-items:center;gap:5px;border:0;border-radius:var(--mcp-r-sm);corner-shape:round;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11.5px;line-height:20px;padding:2px 9px;cursor:pointer;transition:background .12s,color .12s}
|
|
2130
2305
|
.dsh-mcp-skill-seg-btn:hover{color:var(--dsw-alias-label-primary)}
|
|
@@ -2149,6 +2324,20 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2149
2324
|
.dsh-mcp-skill-card:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:2px}
|
|
2150
2325
|
.dsh-mcp-skill-card-top{display:flex;align-items:center;gap:8px;min-width:0}
|
|
2151
2326
|
.dsh-mcp-skill-cat-tile{display:inline-block;width:10px;height:10px;border-radius:3px;flex:none}
|
|
2327
|
+
/* Skill 头像:真图与生成头像同一几何,直径对齐市场(网格 30 / 列表 22 / 详情 42)。
|
|
2328
|
+
生成版渐变底色由组件按名字哈希内联,颜色这里只管字形与描边——白字在深浅两主题的
|
|
2329
|
+
饱和底上都可读;真图版与 .dsh-mcp-card-avatar 同规格,探活失败同样回落字母。 */
|
|
2330
|
+
.dsh-mcp-skill-avatar{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%;corner-shape:round;flex:none;font-size:13px;font-weight:650;line-height:1;color:#fff;text-shadow:0 1px 2px rgb(0 0 0 / 22%);border:1px solid var(--mcp-line);box-shadow:inset 0 0 0 1px rgb(255 255 255 / 18%)}
|
|
2331
|
+
.dsh-mcp-skill-avatar.sm{width:22px;height:22px;font-size:11px}
|
|
2332
|
+
.dsh-mcp-skill-avatar.lg{width:42px;height:42px;font-size:17px}
|
|
2333
|
+
.dsh-mcp-skill-avatar-img{width:30px;height:30px;border-radius:50%;corner-shape:round;object-fit:cover;flex:none;border:1px solid var(--mcp-line);background:var(--mcp-bg-1)}
|
|
2334
|
+
.dsh-mcp-skill-avatar-img.sm{width:22px;height:22px}
|
|
2335
|
+
.dsh-mcp-skill-avatar-img.lg{width:42px;height:42px}
|
|
2336
|
+
.dsh-mcp-skill-avatar-img.fallback{display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:650;color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
2337
|
+
.dsh-mcp-skill-d-id{display:flex;align-items:flex-start;gap:10px;min-width:0}
|
|
2338
|
+
.dsh-mcp-skill-d-id .dsh-mcp-skill-avatar.lg,.dsh-mcp-skill-d-id .dsh-mcp-skill-avatar-img.lg{margin-top:2px}
|
|
2339
|
+
.dsh-mcp-onsite-avatar{width:24px;height:24px;border-radius:50%;corner-shape:round;object-fit:cover;flex:none;border:1px solid var(--mcp-line);font-size:11px}
|
|
2340
|
+
.dsh-mcp-onsite-avatar.fallback{display:inline-flex;align-items:center;justify-content:center;font-weight:650;color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
2152
2341
|
.dsh-mcp-skill-card-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary)}
|
|
2153
2342
|
.dsh-mcp-skill-desc-none{color:var(--dsw-alias-label-tertiary)}
|
|
2154
2343
|
.dsh-mcp-skill-cat-tag{color:var(--dsw-alias-label-secondary);font-size:10.5px;line-height:16px;white-space:nowrap}
|
|
@@ -2238,8 +2427,8 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2238
2427
|
.suite-body{flex:1;min-height:0;display:grid;grid-template-columns:minmax(320px,44%) 1fr}
|
|
2239
2428
|
/* ── 左:技能库选择器 ──────────────────────────────────────────────── */
|
|
2240
2429
|
.suite-left{display:flex;flex-direction:column;gap:9px;min-height:0;padding:13px 14px;border-right:1px solid var(--mcp-line);background:var(--mcp-bg-2)}
|
|
2241
|
-
/* 搜索框不复用 .dsh-mcp-skill-search
|
|
2242
|
-
|
|
2430
|
+
/* 搜索框不复用 .dsh-mcp-skill-search:看板那条已补齐自己的完整样式(图标内嵌 + 清空按钮),
|
|
2431
|
+
但它是工具条场景的尺寸与交互;弹窗这边保留独立一份,两处后续调整互不牵连。 */
|
|
2243
2432
|
.suite-search{position:relative;display:flex;align-items:center;flex:none;height:30px}
|
|
2244
2433
|
.suite-search svg{position:absolute;top:50%;left:9px;transform:translateY(-50%);pointer-events:none;color:var(--mcp-mut)}
|
|
2245
2434
|
.suite-search input{box-sizing:border-box;width:100%;height:30px;padding:0 10px 0 28px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12px;outline:0;transition:border-color .15s,box-shadow .15s}
|
|
@@ -2587,7 +2776,8 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2587
2776
|
.dsh-mcp-rowmenu-sep{height:1px;background:var(--mcp-line);margin:3px 2px}
|
|
2588
2777
|
|
|
2589
2778
|
/* ── §4.1 专家「当前在场」区:绿顶条 + 更大的卡,与普通罗列拉开权重 ───────────── */
|
|
2590
|
-
|
|
2779
|
+
/* margin-bottom:与下方专家卡网格留一口气(两者原本紧贴,卡片像从提示框里溢出来)。 */
|
|
2780
|
+
.dsh-mcp-onsite{display:flex;flex-direction:column;gap:9px;flex:none;margin-bottom:12px;border:1px solid color-mix(in srgb,var(--mcp-ok) 34%,transparent);border-radius:var(--mcp-r-lg);background:var(--mcp-bg-2);padding:12px 14px}
|
|
2591
2781
|
.dsh-mcp-onsite-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2592
2782
|
.dsh-mcp-onsite-head>b{font-size:11px;font-weight:650;letter-spacing:.5px;text-transform:uppercase;color:var(--dsw-alias-state-success-primary);display:inline-flex;align-items:center;gap:6px}
|
|
2593
2783
|
.dsh-mcp-onsite-head>b::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--mcp-ok);box-shadow:0 0 0 3px color-mix(in srgb,var(--mcp-ok) 22%,transparent);corner-shape:round}
|
|
@@ -2604,6 +2794,10 @@ body[data-ds-dark-theme]{--mcp-cat-dev:var(--dsw-static-deepseek-400);--mcp-cat-
|
|
|
2604
2794
|
.dsh-mcp-onsite-chips>button.dsh-mcp-chip-s:hover{border-color:var(--mcp-accent);color:var(--dsw-alias-brand-text)}
|
|
2605
2795
|
/* 推荐位与在场区之间留一口气,否则两块卡片网格会连成一片。 */
|
|
2606
2796
|
.dsh-mcp-wb-grid+.dsh-mcp-item-sub{margin-top:14px}
|
|
2797
|
+
/* 推荐位整块(提示行 + 推荐卡网格):上方与已召唤网格隔 18px,提示行与推荐卡之间
|
|
2798
|
+
隔 10px——此前提示行与推荐卡之间是 0 间距,小字提示直接贴在卡片圆角上。 */
|
|
2799
|
+
.dsh-mcp-wb-reco{margin-top:18px}
|
|
2800
|
+
.dsh-mcp-wb-reco>.dsh-mcp-item-sub{margin-bottom:10px}
|
|
2607
2801
|
/* 送还内联二次确认:把「点一下就没了」改成「点一下先问一句」 */
|
|
2608
2802
|
.dsh-mcp-confirm{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;padding:4px 8px;border:1px solid color-mix(in srgb,var(--mcp-err) 40%,transparent);border-radius:var(--mcp-r-sm);background:var(--dsw-alias-interactive-bg-hover-danger);font-size:11.5px;color:var(--dsw-alias-label-primary)}
|
|
2609
2803
|
.dsh-mcp-confirm>span{color:var(--mcp-err);font-weight:600}
|
|
@@ -2629,10 +2823,43 @@ button.dsh-mcp-chip-s.on .dsh-mcp-chip-s-count{color:var(--mcp-accent)}
|
|
|
2629
2823
|
.dsh-mcp-skill-src-badge{display:inline-flex;align-items:center;padding:0 6px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-xs);background:var(--mcp-bg-2);color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px;font-family:var(--mcp-mono);white-space:nowrap}
|
|
2630
2824
|
|
|
2631
2825
|
/* ── §6 市场:面包屑 + 子页签(下划线式)+ 兼容性前置 + 密度 + 分类条 + 免责折叠 ── */
|
|
2632
|
-
|
|
2826
|
+
/* 面包屑不再独占一行:作为子页签行的最后一个元素靠右贴齐(margin-left:auto),
|
|
2827
|
+
与页签同一基线、正好坐在分割线上方。 */
|
|
2828
|
+
.dsh-mcp-mk-crumb{display:flex;align-items:center;gap:6px;flex:none;margin-left:auto;padding-left:12px;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary);min-height:17px;white-space:nowrap}
|
|
2633
2829
|
.dsh-mcp-mk-crumb-root{color:var(--dsw-alias-label-secondary)}
|
|
2634
2830
|
.dsh-mcp-mk-crumb-sep{opacity:.55}
|
|
2635
2831
|
.dsh-mcp-mk-crumb>b{font-weight:600;color:var(--dsw-alias-label-primary);font-size:11.5px}
|
|
2832
|
+
.dsh-mcp-mk-topright{display:flex;align-items:center;gap:10px;flex:none;margin-left:auto}
|
|
2833
|
+
.dsh-mcp-mk-topright .dsh-mcp-mk-crumb{margin-left:0;padding-left:0}
|
|
2834
|
+
.dsh-mcp-loadout-open{border:1px solid var(--mcp-line);background:transparent;color:var(--mcp-mut);border-radius:999px;padding:4px 10px;font-size:11px;line-height:17px;cursor:pointer;white-space:nowrap}
|
|
2835
|
+
.dsh-mcp-loadout-open:hover{border-color:var(--mcp-accent);color:var(--dsw-alias-label-primary)}
|
|
2836
|
+
.dsh-mcp-loadout-modal{width:min(780px,94vw);height:min(720px,88vh);max-height:88vh;display:flex;flex-direction:column;padding:18px 20px;overflow:hidden}
|
|
2837
|
+
.dsh-mcp-loadout-tabs{display:flex;gap:2px;flex:none;border-bottom:1px solid var(--mcp-line);margin:-4px 0 12px}
|
|
2838
|
+
.dsh-mcp-loadout-tab{border:0;background:transparent;color:var(--mcp-mut);font:inherit;font-size:12px;padding:7px 11px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
|
|
2839
|
+
.dsh-mcp-loadout-tab[aria-selected="true"]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--mcp-accent);font-weight:600}
|
|
2840
|
+
.dsh-mcp-loadout-pane{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;overflow-y:auto;padding-right:2px}
|
|
2841
|
+
.dsh-mcp-loadout-hint,.dsh-mcp-loadout-note{margin:0;font-size:11.5px;line-height:1.55;color:var(--mcp-mut)}
|
|
2842
|
+
.dsh-mcp-loadout-note{flex:none;border-top:1px dashed var(--mcp-line);padding-top:8px}
|
|
2843
|
+
.dsh-mcp-loadout-name{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--mcp-mut);flex:none}
|
|
2844
|
+
.dsh-mcp-loadout-name input{flex:1;min-width:180px;box-sizing:border-box;padding:7px 9px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1);color:var(--dsw-alias-label-primary);font:inherit}
|
|
2845
|
+
.dsh-mcp-loadout-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:none}
|
|
2846
|
+
.dsh-mcp-loadout-result{display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
2847
|
+
.dsh-mcp-loadout-summary{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:11.5px;color:var(--mcp-mut);flex:none}
|
|
2848
|
+
.dsh-mcp-loadout-warn{color:var(--dsw-alias-state-warning-primary)}
|
|
2849
|
+
.dsh-mcp-loadout-from{color:var(--dsw-alias-label-tertiary)}
|
|
2850
|
+
.dsh-mcp-loadout-json{margin:0;min-height:160px;max-height:320px;overflow:auto;padding:10px 12px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1);font-family:var(--mcp-mono);font-size:11px;line-height:1.5;white-space:pre;color:var(--dsw-alias-label-secondary)}
|
|
2851
|
+
.dsh-mcp-loadout-input{width:100%;box-sizing:border-box;resize:vertical;min-height:150px;padding:10px 12px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1);color:var(--dsw-alias-label-primary);font-family:var(--mcp-mono);font-size:11.5px;line-height:1.5}
|
|
2852
|
+
.dsh-mcp-loadout-file{cursor:pointer}
|
|
2853
|
+
.dsh-mcp-loadout-rows{display:flex;flex-direction:column;gap:6px;overflow-y:auto;min-height:0;padding-right:2px}
|
|
2854
|
+
.dsh-mcp-loadout-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:8px 10px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-1)}
|
|
2855
|
+
.dsh-mcp-loadout-row-main{min-width:0;display:flex;flex-direction:column;gap:3px}
|
|
2856
|
+
.dsh-mcp-loadout-row-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px;overflow-wrap:anywhere}
|
|
2857
|
+
.dsh-mcp-loadout-kind{font-size:10px;color:var(--mcp-mut);border:1px solid var(--mcp-line);border-radius:999px;padding:0 6px}
|
|
2858
|
+
.dsh-mcp-loadout-row-meta{display:flex;gap:4px 10px;flex-wrap:wrap;font-size:10.5px;color:var(--mcp-mut)}
|
|
2859
|
+
.dsh-mcp-loadout-row-actions{display:flex;gap:6px;flex:none;flex-wrap:wrap;justify-content:flex-end}
|
|
2860
|
+
@media (max-width:480px){.dsh-mcp-loadout-modal{width:100%;height:calc(100vh - 24px);max-height:none;padding:14px}.dsh-mcp-loadout-row{flex-direction:column}.dsh-mcp-loadout-row-actions{width:100%;justify-content:flex-start}}
|
|
2861
|
+
/* 窄屏页签行放不下 5 个页签 + 面包屑:页签优先,面包屑让位(页签本身已标明当前位置)。 */
|
|
2862
|
+
@media (max-width:480px){.dsh-mcp-mk-crumb{display:none}}
|
|
2636
2863
|
.dsh-mcp-mk-subtabs-idle{flex:none;padding:7px 10px 8px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:19px;white-space:nowrap;cursor:pointer;opacity:.8;transition:color .13s,opacity .13s}
|
|
2637
2864
|
.dsh-mcp-mk-subtabs-idle:hover{opacity:1;color:var(--dsw-alias-label-secondary);text-decoration:underline;text-underline-offset:2px}
|
|
2638
2865
|
.dsh-mcp-mk-subtabs-idle:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:-2px}
|
|
@@ -2678,9 +2905,70 @@ button.dsh-mcp-chip-s.on .dsh-mcp-chip-s-count{color:var(--mcp-accent)}
|
|
|
2678
2905
|
@keyframes mcpSkeletonShimmer{100%{transform:translateX(100%)}}
|
|
2679
2906
|
@media (prefers-reduced-motion: reduce){.dsh-mcp-skeleton-bar::after{animation:none;opacity:0}}
|
|
2680
2907
|
|
|
2908
|
+
/* ── 社区「人味」:作者头像 / 维护状态 / 最近动静 / 真实反馈出口 ────────
|
|
2909
|
+
全部使用既有主题令牌;头像只是 <img>,加载失败就隐藏,不留破图占位。 */
|
|
2910
|
+
.dsh-mcp-card-person{display:flex;align-items:flex-start;gap:8px;min-width:0}
|
|
2911
|
+
.dsh-mcp-card-person>div{min-width:0}
|
|
2912
|
+
.dsh-mcp-card-avatar{width:30px;height:30px;border-radius:50%;corner-shape:round;object-fit:cover;flex:none;border:1px solid var(--mcp-line);background:var(--mcp-bg-1)}
|
|
2913
|
+
.dsh-mcp-card-avatar.fallback{display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:650;color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
2914
|
+
.dsh-mcp-detail-person{display:flex;align-items:flex-start;gap:10px;margin-bottom:8px}
|
|
2915
|
+
.dsh-mcp-detail-person>div{min-width:0}
|
|
2916
|
+
.dsh-mcp-detail-person h3{margin:0}
|
|
2917
|
+
.dsh-mcp-detail-avatar{width:42px;height:42px;border-radius:50%;corner-shape:round;object-fit:cover;flex:none;border:1px solid var(--mcp-line);background:var(--mcp-bg-1)}
|
|
2918
|
+
.dsh-mcp-detail-avatar.fallback{display:inline-flex;align-items:center;justify-content:center;font-size:18px;font-weight:650;color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
2919
|
+
.dsh-mcp-presence{display:flex;align-items:flex-start;gap:9px;padding:8px 10px;margin:7px 0 10px;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-2)}
|
|
2920
|
+
.dsh-mcp-presence.compact{padding:6px 8px;margin:6px 0 8px}
|
|
2921
|
+
.dsh-mcp-presence-avatar{width:32px;height:32px;border-radius:50%;corner-shape:round;object-fit:cover;flex:none;border:1px solid var(--mcp-line)}
|
|
2922
|
+
.dsh-mcp-presence-avatar.fallback{display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:650;color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
2923
|
+
.dsh-mcp-presence-body{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:3px}
|
|
2924
|
+
.dsh-mcp-presence-line{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px;min-width:0}
|
|
2925
|
+
.dsh-mcp-presence-owner{font-weight:650;color:var(--dsw-alias-label-primary);text-decoration:none;overflow-wrap:anywhere}
|
|
2926
|
+
a.dsh-mcp-presence-owner:hover{color:var(--mcp-accent);text-decoration:underline}
|
|
2927
|
+
.dsh-mcp-presence-loading{font-size:10.5px;color:var(--mcp-mut)}
|
|
2928
|
+
.dsh-mcp-presence-meta{display:flex;align-items:center;gap:5px 10px;flex-wrap:wrap;font-size:10.5px;line-height:1.45;color:var(--mcp-mut)}
|
|
2929
|
+
.dsh-mcp-presence-inline{display:flex;align-items:center;gap:6px;font-size:10.5px;line-height:1.4;color:var(--mcp-mut);min-width:0;flex-wrap:wrap}
|
|
2930
|
+
.dsh-mcp-presence-inline-avatar{width:18px;height:18px;border-radius:50%;corner-shape:round;object-fit:cover;flex:none;border:1px solid var(--mcp-line)}
|
|
2931
|
+
.dsh-mcp-presence-inline-avatar.fallback{display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:650;color:var(--mcp-accent);background:var(--dsw-alias-interactive-bg-hover-accent)}
|
|
2932
|
+
.dsh-mcp-presence-inline-owner{color:var(--dsw-alias-label-secondary);font-weight:600;overflow-wrap:anywhere}
|
|
2933
|
+
.dsh-mcp-presence-inline-owner a{color:inherit;text-decoration:none}
|
|
2934
|
+
.dsh-mcp-presence-inline-owner a:hover{color:var(--mcp-accent);text-decoration:underline}
|
|
2935
|
+
.dsh-mcp-presence-inline-note{color:var(--mcp-mut);font-variant-numeric:tabular-nums}
|
|
2936
|
+
.dsh-mcp-presence-card{margin-top:-3px}
|
|
2937
|
+
.dsh-mcp-presence-detail{background:transparent;border:0;padding:0;margin:0 0 8px}
|
|
2938
|
+
.dsh-mcp-catalog-meta{display:flex;align-items:center;gap:6px 12px;flex-wrap:wrap;font-size:11px;color:var(--mcp-mut);padding:0 1px;margin:-4px 0 9px}
|
|
2939
|
+
.dsh-mcp-catalog-meta a{color:var(--mcp-accent);text-decoration:none}
|
|
2940
|
+
.dsh-mcp-catalog-meta a:hover{text-decoration:underline}
|
|
2941
|
+
.dsh-mcp-feedback{margin:10px 0 4px;padding:9px 10px;border:1px dashed var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-2)}
|
|
2942
|
+
.dsh-mcp-feedback-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin-bottom:7px;font-size:11.5px;color:var(--dsw-alias-label-primary)}
|
|
2943
|
+
.dsh-mcp-feedback-source{font-size:10.5px;color:var(--mcp-mut);font-weight:400}
|
|
2944
|
+
.dsh-mcp-feedback-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
2945
|
+
.dsh-mcp-feedback-actions .dsh-mcp-btn{font-size:11px;padding:5px 9px;text-decoration:none}
|
|
2946
|
+
.dsh-mcp-feedback-note{margin:7px 0 0;font-size:10.5px;line-height:1.5;color:var(--mcp-mut)}
|
|
2947
|
+
/* 与下方工具的间距交给 .dsh-mcp-wb 的 gap:再加 margin 会叠成 20px,显得空 */
|
|
2948
|
+
.dsh-mcp-pulse{flex:none;margin:0;border:1px solid var(--mcp-line);border-radius:var(--mcp-r-md);background:var(--mcp-bg-2);padding:8px 10px}
|
|
2949
|
+
/* 首个内容块再上提 4px:上侧留白才与下侧(工具条贴分类条)接近 */
|
|
2950
|
+
.dsh-mcp-pulse + *{margin-top:-4px}
|
|
2951
|
+
.dsh-mcp-pulse.quiet{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 10px;border-style:dashed;background:transparent}
|
|
2952
|
+
.dsh-mcp-pulse-title{font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-primary);letter-spacing:.01em}
|
|
2953
|
+
.dsh-mcp-pulse-quiet{font-size:10.5px;color:var(--mcp-mut)}
|
|
2954
|
+
.dsh-mcp-pulse-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin-bottom:5px}
|
|
2955
|
+
.dsh-mcp-pulse-note{font-size:10.5px;color:var(--mcp-mut);flex:1 1 auto}
|
|
2956
|
+
.dsh-mcp-pulse-clear{border:0;background:transparent;color:var(--mcp-mut);font:inherit;font-size:10.5px;cursor:pointer;padding:2px 4px;border-radius:var(--mcp-r-sm)}
|
|
2957
|
+
.dsh-mcp-pulse-clear:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
2958
|
+
.dsh-mcp-pulse-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
|
|
2959
|
+
.dsh-mcp-pulse-list>li{display:flex;align-items:center;gap:8px;min-width:0}
|
|
2960
|
+
.dsh-mcp-pulse-item{display:flex;align-items:center;gap:6px;min-width:0;flex:1 1 auto;border:0;background:transparent;font:inherit;font-size:11px;color:var(--dsw-alias-label-secondary);text-align:left;padding:3px 4px;border-radius:var(--mcp-r-sm);cursor:pointer}
|
|
2961
|
+
.dsh-mcp-pulse-item:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
2962
|
+
.dsh-mcp-pulse-kind{font-weight:600;white-space:nowrap}
|
|
2963
|
+
.dsh-mcp-pulse-kind.added{color:var(--dsw-alias-state-success-primary)}
|
|
2964
|
+
.dsh-mcp-pulse-kind.removed{color:var(--dsw-alias-state-warn-primary)}
|
|
2965
|
+
.dsh-mcp-pulse-kind.updated{color:var(--mcp-accent)}
|
|
2966
|
+
.dsh-mcp-pulse-sample{color:var(--mcp-mut);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
2967
|
+
.dsh-mcp-pulse-time{flex:none;font-size:10px;color:var(--mcp-mut);font-variant-numeric:tabular-nums}
|
|
2968
|
+
|
|
2681
2969
|
/* 响应式覆盖必须位于全部基础规则之后:同特异性下后出现者胜出。 */
|
|
2682
2970
|
@media (max-width:760px){.dsh-mcp-panel-overlay{padding:10px}.dsh-mcp-panel{max-width:none;width:100%;height:calc(100dvh - 20px);border-radius:var(--mcp-r-panel)}.dsh-mcp-panel .dsh-mcp-wrap{padding:12px}.dsh-mcp-head{align-items:flex-start}.dsh-mcp-body{flex-direction:column;overflow-y:auto;overflow-x:hidden}.dsh-mcp-side,.dsh-mcp-ws-body{width:auto;min-width:0;max-height:none;flex:none}.dsh-mcp-side:not(.embedded) .dsh-mcp-list{max-height:38vh}.dsh-mcp-section.local,.dsh-mcp-section.global{flex:none;min-height:0;max-height:none}.dsh-mcp-section.local .dsh-mcp-list{max-height:30vh}.dsh-mcp-global-list{max-height:30vh}.dsh-mcp-detail{padding:12px;flex:none}.dsh-mcp-d-head{flex-direction:column}.dsh-mcp-kv-edit{grid-template-columns:minmax(90px,35%) minmax(0,1fr) auto}.dsh-mcp-page{padding:10px 12px 20px}[data-platform=darwin] .dsh-mcp-page{padding-top:calc(var(--dsh-frame-top-clearance,0px) + 10px)}.dsh-mcp-page>.dsh-mcp-wrap{flex:0 0 auto;width:100%;min-height:0;height:auto}.dsh-mcp-skill-body{flex-direction:column;overflow:visible}.dsh-mcp-skill-side{width:auto;flex:none}.dsh-mcp-skill-list{max-height:34vh}.dsh-mcp-skill-detail{flex:none;max-height:none}.dsh-mcp-skill-d-md{max-height:38vh}}
|
|
2683
|
-
@media (max-width:480px){.dsh-mcp-head{flex-direction:column}.dsh-mcp-head-actions{width:100%;justify-content:flex-start}.dsh-mcp-btn{padding:7px 9px}.dsh-mcp-radio-row,.dsh-mcp-number-grid{grid-template-columns:1fr}.dsh-mcp-modal{max-width:none;width:100%;padding:14px}.dsh-mcp-kv{grid-template-columns:1fr;gap:4px}.dsh-mcp-kv>b{margin-top:4px}.dsh-mcp-secret-row{grid-template-columns:1fr;gap:4px}.dsh-mcp-secret{width:100%}.dsh-mcp-kv-edit{grid-template-columns:1fr auto}.dsh-mcp-kv-edit input:first-child{grid-column:1 / -1}.dsh-mcp-tool{flex-direction:column}.dsh-mcp-tool-name{max-width:100%}.dsh-mcp-tool-desc{white-space:normal}
|
|
2971
|
+
@media (max-width:480px){.dsh-mcp-head{flex-direction:column}.dsh-mcp-head-actions{width:100%;justify-content:flex-start}.dsh-mcp-btn{padding:7px 9px}.dsh-mcp-radio-row,.dsh-mcp-number-grid{grid-template-columns:1fr}.dsh-mcp-modal{max-width:none;width:100%;padding:14px}.dsh-mcp-kv{grid-template-columns:1fr;gap:4px}.dsh-mcp-kv>b{margin-top:4px}.dsh-mcp-secret-row{grid-template-columns:1fr;gap:4px}.dsh-mcp-secret{width:100%}.dsh-mcp-kv-edit{grid-template-columns:1fr auto}.dsh-mcp-kv-edit input:first-child{grid-column:1 / -1}.dsh-mcp-tool{flex-direction:column}.dsh-mcp-tool-name{max-width:100%}.dsh-mcp-tool-desc{white-space:normal}}
|
|
2684
2972
|
`;
|
|
2685
2973
|
|
|
2686
2974
|
// ── DOM 基础件:createElement 绑定、值遮蔽、通用小控件与图标表 ──────────────────────
|
|
@@ -2801,6 +3089,14 @@ const Badge = ({ cls, children, ...rest }) => h('span', { ...rest, className: 'd
|
|
|
2801
3089
|
const Icon = ({ d, size, className }) =>
|
|
2802
3090
|
h('svg', { className, width: size || 13, height: size || 13, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }, h('path', { d }));
|
|
2803
3091
|
|
|
3092
|
+
// 星标用实心小星;验证用「印章 + 白色对勾」。两者都不吃主题文字色,只在卡片里当状态图标。
|
|
3093
|
+
const StarIcon = ({ size }) =>
|
|
3094
|
+
h('svg', { width: size || 10, height: size || 10, viewBox: '0 0 16 16', 'aria-hidden': 'true', focusable: 'false' }, h('path', { fill: 'currentColor', d: 'M8 1.7l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.5l-3.8 2 .7-4.3-3.1-3 4.3-.6z' }));
|
|
3095
|
+
const VerifiedSeal = ({ size }) =>
|
|
3096
|
+
h('svg', { width: size || 15, height: size || 15, viewBox: '0 0 16 16', 'aria-hidden': 'true', focusable: 'false' },
|
|
3097
|
+
h('path', { fill: 'currentColor', d: 'M8 .9l1.8 1.4 2.3-.3 1 2.1 2 1.2-.5 2.3.5 2.3-2 1.2-1 2.1-2.3-.3L8 15.1l-1.8-1.4-2.3.3-1-2.1-2-1.2.5-2.3-.5-2.3 2-1.2 1-2.1 2.3.3z' }),
|
|
3098
|
+
h('path', { fill: 'none', stroke: '#fff', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', d: 'M5.1 8.2l1.9 1.9 3.9-4' }));
|
|
3099
|
+
|
|
2804
3100
|
const ICONS = {
|
|
2805
3101
|
search: 'M7 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM13.5 13.5L11 11',
|
|
2806
3102
|
plus: 'M8 3v10M3 8h10',
|
|
@@ -2831,6 +3127,10 @@ const ICONS = {
|
|
|
2831
3127
|
// 用 .2 长的短线段而不是零长路径——零长路径在部分渲染器里画不出圆点)。
|
|
2832
3128
|
layers: 'M8 2l6 3.3-6 3.4-6-3.4zM2.4 8.4L8 11.5l5.6-3.1M2.4 11.1L8 14.2l5.6-3.1',
|
|
2833
3129
|
grip: 'M6 3.4v.2M10 3.4v.2M6 7.9v.2M10 7.9v.2M6 12.4v.2M10 12.4v.2',
|
|
3130
|
+
// down:向下箭头,README 弹层「跳到末尾」用
|
|
3131
|
+
down: 'M8 3v9.4M4 8.6l4 4 4-4',
|
|
3132
|
+
// info:信息圆圈,宿主组件 / 不可安装的说明图标
|
|
3133
|
+
info: 'M8 7.2v4.3M8 4.7v.2M8 14.2A6.2 6.2 0 1 0 8 1.8a6.2 6.2 0 0 0 0 12.4z',
|
|
2834
3134
|
};
|
|
2835
3135
|
|
|
2836
3136
|
// ── 常用 MCP 预设模板(主流管理器常见的“浏览即添加”能力)────────────
|
|
@@ -3342,18 +3642,6 @@ const Pager = ({ page, pageCount, total, onPage, disabled }) => {
|
|
|
3342
3642
|
);
|
|
3343
3643
|
};
|
|
3344
3644
|
|
|
3345
|
-
// ── 内置目录的 auth 枚举(OPT-24 · 2.3)──────────────────────────────────────────
|
|
3346
|
-
// 宿主 lib/mcp-config.js 的 builtinMcpCatalog 透出 auth(枚举 none/apiKey/oauth/
|
|
3347
|
-
// oauth-or-pat)+ 有则挂 authNote。**这与市场连接器 tab 的 authMode 不是一回事**:
|
|
3348
|
-
// authMode 是上游 connectors 清单里的自由文本,只能原样显示;auth 是枚举,可以映射
|
|
3349
|
-
// 成人能读的标签。未知枚举值原样显示,绝不猜。
|
|
3350
|
-
const AUTH_KIND_LABELS = { apiKey: 'API Key', oauth: 'OAuth', 'oauth-or-pat': 'OAuth / PAT' };
|
|
3351
|
-
// 惰性求值:模块加载时宿主 locale 还没接进来,只有 none 需要翻译。
|
|
3352
|
-
const authKindLabel = (kind) => (kind === 'none' ? t('免认证') : AUTH_KIND_LABELS[kind] || kind);
|
|
3353
|
-
// 色语沿用 §1.3:oauth 系装完还得去浏览器授权,属「还需注意」→ 橙;apiKey 有既有
|
|
3354
|
-
// 「需密钥」徽章兜住,这里只作信息性标注,用默认灰。
|
|
3355
|
-
const authBadgeCls = (kind) => (kind === 'oauth' || kind === 'oauth-or-pat' ? 's-warn' : '');
|
|
3356
|
-
|
|
3357
3645
|
// ── 服务器列表:按传输 / 状态筛选,选中与启停 ──────────────────────────────────
|
|
3358
3646
|
const ServerList = ({ servers, total, selected, query, onQuery, onSelect, onToggle, busy, transportFilter, statusFilter, onTransportFilter, onStatusFilter, embedded, workspace, emptyHint, loading }) => {
|
|
3359
3647
|
const count = total === undefined ? servers.length : total;
|
|
@@ -5553,7 +5841,7 @@ const CATEGORY_BLURB = {
|
|
|
5553
5841
|
get skill() { return t('本地 Skill 清单:来源、是否启用、是否可被模型调用。'); },
|
|
5554
5842
|
get expert() { return t('已召唤的专家:人设、捆绑技能、当前在场状态,可送还。'); },
|
|
5555
5843
|
get tailor() { return t('工具使用账本:谁在被调用、谁在白缴上下文税。'); },
|
|
5556
|
-
get wb() { return t('
|
|
5844
|
+
get wb() { return t('市场:连接器、SkillHub 技能与专家召唤。'); },
|
|
5557
5845
|
};
|
|
5558
5846
|
// §1.1 分类 hover 摘要。定位由父层算好(left),这里只负责内容与外观。
|
|
5559
5847
|
// 只用 pointer-events:none 的只读浮层:不承载交互,因此不需要焦点陷阱,
|
|
@@ -6101,6 +6389,265 @@ const onSkillChange = (listener) => {
|
|
|
6101
6389
|
return () => { skillChangeListeners.delete(listener); };
|
|
6102
6390
|
};
|
|
6103
6391
|
|
|
6392
|
+
// ── Skill 头像:确定性生成 ───────────────────────────────────────────
|
|
6393
|
+
// 本地 skill 的数据里没有任何图标字段(宿主注册表只给能力位与来源目录),而市场页
|
|
6394
|
+
// 每张卡都有一个圆形头像位。为了同一观感,这里从 skill 名哈希出固定色相,画
|
|
6395
|
+
// 「渐变圆 + 首字母」的生成头像:零网络、同一名永远同色、不同 skill 之间色相可辨。
|
|
6396
|
+
// 只做渲染期纯计算,不发请求也不登记 avatarRegistry —— 那条链是给真图片探活用的。
|
|
6397
|
+
const skillAvatarHue = (name) => {
|
|
6398
|
+
const text = String(name || '');
|
|
6399
|
+
let hash = 5381;
|
|
6400
|
+
for (let i = 0; i < text.length; i += 1) hash = ((hash << 5) + hash + text.charCodeAt(i)) >>> 0;
|
|
6401
|
+
return hash % 360;
|
|
6402
|
+
};
|
|
6403
|
+
const SkillAvatar = ({ name, size, title }) => {
|
|
6404
|
+
const hue = skillAvatarHue(name);
|
|
6405
|
+
const letter = String(name || '?').slice(0, 1).toUpperCase() || '?';
|
|
6406
|
+
return h('span', {
|
|
6407
|
+
className: `dsh-mcp-skill-avatar${size ? ` ${size}` : ''}`,
|
|
6408
|
+
style: {
|
|
6409
|
+
background: `linear-gradient(135deg, hsl(${hue} 64% 56%), hsl(${(hue + 38) % 360} 64% 42%))`,
|
|
6410
|
+
},
|
|
6411
|
+
title,
|
|
6412
|
+
'aria-hidden': true,
|
|
6413
|
+
}, letter);
|
|
6414
|
+
};
|
|
6415
|
+
|
|
6416
|
+
// ── Skill 真图标:目录名反推 + SkillHub 精确补全 ────────────────────────────
|
|
6417
|
+
// 市场卡有真图标,本地 skill 的行数据里没有。真图标的来源有两层,按可信度取用:
|
|
6418
|
+
// ① wbexpert 捆绑技能:安装目录名 `wbexpert__<plugin>__<skill>` 里的 plugin 能在
|
|
6419
|
+
// 专家目录里查到头像(与已召唤页同源)。listSkills 行带 path,这里从 path 尾段解析。
|
|
6420
|
+
// ② 其余技能(SkillHub / clawhub 装的):真图标只在 SkillHub 目录里有——按名字做一次
|
|
6421
|
+
// keyword 查询,**slug 精确相等**才采纳(绝不按模糊命中张冠李戴)。纪律与头像探活
|
|
6422
|
+
// 同一套:模块级缓存(同一名本次会话只查一次)、失败粘住('')、同时在途 ≤3、
|
|
6423
|
+
// 按登记顺序排队。查询是宿主侧 keyword GET(自带 15s 超时),47 个技能 ≈ 市场翻两页。
|
|
6424
|
+
// 两层都没有 → SkillAvatar 生成头像兜底,永不空圈。
|
|
6425
|
+
const SKILL_DIR_EXPERT_RE = /^wbexpert__([a-z0-9_-]+?)__/i;
|
|
6426
|
+
/** path 尾段目录名解析出真图标;解析不出或目录里查不到该 plugin → ''(交给下一层)。 */
|
|
6427
|
+
const skillIconFromDir = (path, expertByPlugin) => {
|
|
6428
|
+
const base = String(path || '').trim().replace(/[\\/]+$/, '');
|
|
6429
|
+
const dir = base.split(/[\\/]/).pop() || '';
|
|
6430
|
+
const match = dir.match(SKILL_DIR_EXPERT_RE);
|
|
6431
|
+
return (match && expertByPlugin.get(match[1])) || '';
|
|
6432
|
+
};
|
|
6433
|
+
|
|
6434
|
+
const skillIconCache = new Map(); // name → url('' = 查过、没有)
|
|
6435
|
+
const skillIconQueued = new Set(); // 已入队(含在途)
|
|
6436
|
+
const skillIconQueue = []; // FIFO,与头像闸门同一口径
|
|
6437
|
+
let skillIconInflight = 0;
|
|
6438
|
+
const skillIconWaiters = new Set(); // (name) => void:结果落地后通知组件重绘
|
|
6439
|
+
const SKILL_ICON_INFLIGHT_MAX = 3;
|
|
6440
|
+
|
|
6441
|
+
const skillIconFinish = (name, url) => {
|
|
6442
|
+
skillIconCache.set(name, String(url || ''));
|
|
6443
|
+
skillIconQueued.delete(name);
|
|
6444
|
+
for (const notify of [...skillIconWaiters]) {
|
|
6445
|
+
try { notify(name); } catch { /* 单个订阅者失败不影响其它 */ }
|
|
6446
|
+
}
|
|
6447
|
+
};
|
|
6448
|
+
|
|
6449
|
+
const skillIconDrain = (call) => {
|
|
6450
|
+
while (skillIconInflight < SKILL_ICON_INFLIGHT_MAX && skillIconQueue.length) {
|
|
6451
|
+
const name = skillIconQueue.shift();
|
|
6452
|
+
skillIconInflight += 1;
|
|
6453
|
+
call('wbSkillCatalog', { keyword: name, pageSize: 5 })
|
|
6454
|
+
.then((res) => {
|
|
6455
|
+
const rows = res && res.ok && Array.isArray(res.skills) ? res.skills : [];
|
|
6456
|
+
const hit = rows.find((row) => row && (row.slug === name || row.canonical === name) && row.iconUrl);
|
|
6457
|
+
skillIconFinish(name, hit ? hit.iconUrl : '');
|
|
6458
|
+
})
|
|
6459
|
+
.catch(() => skillIconFinish(name, ''))
|
|
6460
|
+
.finally(() => {
|
|
6461
|
+
skillIconInflight -= 1;
|
|
6462
|
+
skillIconDrain(call);
|
|
6463
|
+
});
|
|
6464
|
+
}
|
|
6465
|
+
};
|
|
6466
|
+
|
|
6467
|
+
const skillIconEnqueue = (call, name) => {
|
|
6468
|
+
const key = String(name || '').trim();
|
|
6469
|
+
if (!key || skillIconCache.has(key) || skillIconQueued.has(key)) return;
|
|
6470
|
+
skillIconQueued.add(key);
|
|
6471
|
+
skillIconQueue.push(key);
|
|
6472
|
+
skillIconDrain(call);
|
|
6473
|
+
};
|
|
6474
|
+
|
|
6475
|
+
/** 渲染期取真图标:目录反推优先(分组可辨),回落 SkillHub 补全缓存,都没有 → ''。 */
|
|
6476
|
+
const skillIconFor = (s, expertByPlugin) =>
|
|
6477
|
+
skillIconFromDir(s && s.path, expertByPlugin) || skillIconCache.get(String((s && s.name) || '').trim()) || '';
|
|
6478
|
+
|
|
6479
|
+
// 组件侧接线:订阅结果 + 把还没有真图的技能名入队。version 只是重绘信号,
|
|
6480
|
+
// 渲染一律以 skillIconCache 的现值为准(迟到的成功也会被下一帧采纳)。
|
|
6481
|
+
// items = [{ name, path }]:path 在这里就地判定,不经模块级中转(同名技能可能来自
|
|
6482
|
+
// 不同专家,按名字反查 path 会张冠李戴)。
|
|
6483
|
+
const useSkillIcons = (call, items, expertByPlugin) => {
|
|
6484
|
+
const [, setVersion] = useState(0);
|
|
6485
|
+
useEffect(() => {
|
|
6486
|
+
const notify = () => setVersion((v) => v + 1);
|
|
6487
|
+
skillIconWaiters.add(notify);
|
|
6488
|
+
return () => { skillIconWaiters.delete(notify); };
|
|
6489
|
+
}, []);
|
|
6490
|
+
const list = (Array.isArray(items) ? items : [])
|
|
6491
|
+
.map((it) => `${(it && it.name) || ''}\u0001${(it && it.path) || ''}`)
|
|
6492
|
+
.join('\u0000');
|
|
6493
|
+
useEffect(() => {
|
|
6494
|
+
for (const entry of list.split('\u0000')) {
|
|
6495
|
+
if (!entry) continue;
|
|
6496
|
+
const sep = entry.indexOf('\u0001');
|
|
6497
|
+
const name = entry.slice(0, sep);
|
|
6498
|
+
const path = entry.slice(sep + 1);
|
|
6499
|
+
// 目录反推已经拿到的不再查询(wbexpert 捆绑技能不该为它多花一次请求)。
|
|
6500
|
+
if (skillIconFromDir(path, expertByPlugin)) continue;
|
|
6501
|
+
skillIconEnqueue(call, name);
|
|
6502
|
+
}
|
|
6503
|
+
// list 由 items 逐项拼接而来,items 在渲染期重建,这里只关心集合内容变化。
|
|
6504
|
+
}, [call, list, expertByPlugin]);
|
|
6505
|
+
};
|
|
6506
|
+
|
|
6507
|
+
// ── 插件卡片下载量:npm 近 30 天统计,按当前页批量懒取 ──────────────────────
|
|
6508
|
+
// 技能卡的 ↓ 是 SkillHub 目录自带的;社区插件目录(slim.json 的 11 个字段)里没有下载量,
|
|
6509
|
+
// 只能在卡片渲染时按 npm 包名去公开统计接口补。取数在宿主侧(lib/npm-downloads.js:7 天
|
|
6510
|
+
// 落盘缓存、单请求 ≤12 包、429/5xx 冷却 10 分钟),这一层只管「渲染期不联网 + 结果落地后
|
|
6511
|
+
// 重绘」,纪律与 skillIcon* 同一套:
|
|
6512
|
+
// · 模块级缓存,同一个包整个会话只问一次;值为 null = 问过了但没有(下架 / 未被统计 /
|
|
6513
|
+
// 接口挂了)→ 卡片上那枚「↓」干脆不出现。宁缺不假:绝不显示 0 冒充「没人下载」。
|
|
6514
|
+
// · 当前页 50 张卡按卡片顺序入队,同一提交内的入队合并成批(≤12 包/请求)、**在途只 1 批**,
|
|
6515
|
+
// 所以翻一页最多 5 个请求,首屏靠前的先出数;翻页或换筛选只补新出现的包。
|
|
6516
|
+
// · 口径与技能卡不同(那是累计、这是近 30 天),所以 title 里写明窗口。
|
|
6517
|
+
const npmDownloadsCache = new Map(); // pkg → number | null(null = 问过、没有)
|
|
6518
|
+
const npmDownloadsQueued = new Set(); // 已入队(含在途与已定值)
|
|
6519
|
+
const npmDownloadsWaiters = new Set(); // () => void:结果落地后通知组件重绘
|
|
6520
|
+
let npmDownloadsBuffer = []; // 待发送包名,FIFO(= 卡片顺序)
|
|
6521
|
+
let npmDownloadsTimer = null;
|
|
6522
|
+
let npmDownloadsInflight = 0;
|
|
6523
|
+
const NPM_DOWNLOADS_BATCH = 12; // 与宿主侧 MAX_PACKAGES 同口径
|
|
6524
|
+
// 在途必须 ≤1:宿主侧那道节流(并发 2 + 起手间隔 220ms,实测出来的数)是**全局**的,
|
|
6525
|
+
// 面板同时压两个批次就等于把速率翻倍 —— 实测 12 个请求并发打过去 429 占一半以上。
|
|
6526
|
+
const NPM_DOWNLOADS_INFLIGHT_MAX = 1;
|
|
6527
|
+
|
|
6528
|
+
const npmDownloadsSettle = (values, retryable = []) => {
|
|
6529
|
+
for (const pkg of Object.keys(values || {})) {
|
|
6530
|
+
npmDownloadsCache.set(pkg, values[pkg]);
|
|
6531
|
+
npmDownloadsQueued.add(pkg);
|
|
6532
|
+
}
|
|
6533
|
+
// 限流 / 网络抖动 ≠「这个包没有下载量」:把它从已入队集合放出来,下次翻页、换筛选或
|
|
6534
|
+
// 重载面板会再试一次(真确认没有的 missing 才粘住,不反复白打公开接口)。
|
|
6535
|
+
for (const pkg of retryable) npmDownloadsQueued.delete(pkg);
|
|
6536
|
+
for (const notify of [...npmDownloadsWaiters]) {
|
|
6537
|
+
try { notify(); } catch { /* 单个订阅者失败不影响其它 */ }
|
|
6538
|
+
}
|
|
6539
|
+
};
|
|
6540
|
+
|
|
6541
|
+
const npmDownloadsFlush = (call) => {
|
|
6542
|
+
while (npmDownloadsInflight < NPM_DOWNLOADS_INFLIGHT_MAX && npmDownloadsBuffer.length) {
|
|
6543
|
+
const batch = npmDownloadsBuffer.splice(0, NPM_DOWNLOADS_BATCH);
|
|
6544
|
+
npmDownloadsInflight += 1;
|
|
6545
|
+
call('wbNpmDownloads', { packages: batch })
|
|
6546
|
+
.then((res) => {
|
|
6547
|
+
const rows = res && res.packages ? res.packages : {};
|
|
6548
|
+
const values = {};
|
|
6549
|
+
const retryable = [];
|
|
6550
|
+
for (const pkg of batch) {
|
|
6551
|
+
const row = rows[pkg];
|
|
6552
|
+
const n = row && row.ok ? Number(row.downloads) : Number.NaN;
|
|
6553
|
+
if (Number.isFinite(n)) { values[pkg] = n; continue; }
|
|
6554
|
+
// 分两种「没有」:missing = 上游确认这个包没有统计(下架/未收录),粘住不再问;
|
|
6555
|
+
// 其它(限流、超时、接口挂了)本轮画不出来,但允许下次翻页/重载再试一次。
|
|
6556
|
+
values[pkg] = null;
|
|
6557
|
+
if (!(row && row.missing)) retryable.push(pkg);
|
|
6558
|
+
}
|
|
6559
|
+
npmDownloadsSettle(values, retryable);
|
|
6560
|
+
})
|
|
6561
|
+
.catch(() => npmDownloadsSettle(Object.fromEntries(batch.map((pkg) => [pkg, null])), batch))
|
|
6562
|
+
.finally(() => {
|
|
6563
|
+
npmDownloadsInflight -= 1;
|
|
6564
|
+
npmDownloadsFlush(call);
|
|
6565
|
+
});
|
|
6566
|
+
}
|
|
6567
|
+
};
|
|
6568
|
+
|
|
6569
|
+
const npmDownloadsEnqueue = (call, packages) => {
|
|
6570
|
+
let added = 0;
|
|
6571
|
+
for (const raw of Array.isArray(packages) ? packages : []) {
|
|
6572
|
+
const pkg = String(raw || '').trim();
|
|
6573
|
+
if (!pkg || npmDownloadsQueued.has(pkg)) continue;
|
|
6574
|
+
npmDownloadsQueued.add(pkg);
|
|
6575
|
+
npmDownloadsBuffer.push(pkg);
|
|
6576
|
+
added += 1;
|
|
6577
|
+
}
|
|
6578
|
+
if (!added || npmDownloadsTimer !== null) return;
|
|
6579
|
+
// 宏任务合并:一页 50 个 effect 各自入队,到这儿并成批再发,50 个请求就成 5 个。
|
|
6580
|
+
npmDownloadsTimer = setTimeout(() => { npmDownloadsTimer = null; npmDownloadsFlush(call); }, 0);
|
|
6581
|
+
};
|
|
6582
|
+
|
|
6583
|
+
/** 渲染期取下载量:还没问到 / 问过但没有 → null(这一枚不画)。 */
|
|
6584
|
+
const npmDownloadsFor = (pkg) => {
|
|
6585
|
+
const value = npmDownloadsCache.get(String(pkg || '').trim());
|
|
6586
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
6587
|
+
};
|
|
6588
|
+
|
|
6589
|
+
// 组件侧接线:订阅结果 + 把当前页还没有值的包入队。与 useSkillIcons 一样,返回值只是重绘
|
|
6590
|
+
// 信号,渲染一律以 npmDownloadsCache 现值为准(迟到的成功会被下一帧采纳)。
|
|
6591
|
+
const useNpmDownloads = (call, items) => {
|
|
6592
|
+
const [, setVersion] = useState(0);
|
|
6593
|
+
useEffect(() => {
|
|
6594
|
+
const notify = () => setVersion((v) => v + 1);
|
|
6595
|
+
npmDownloadsWaiters.add(notify);
|
|
6596
|
+
return () => { npmDownloadsWaiters.delete(notify); };
|
|
6597
|
+
}, []);
|
|
6598
|
+
const list = (Array.isArray(items) ? items : [])
|
|
6599
|
+
.map((it) => String((it && it.npm) || '').trim())
|
|
6600
|
+
.join('\u0000');
|
|
6601
|
+
useEffect(() => {
|
|
6602
|
+
npmDownloadsEnqueue(call, list.split('\u0000'));
|
|
6603
|
+
// list 是当前页包名拼出来的字符串:只有集合内容变了才重新入队,
|
|
6604
|
+
// 同一页反复重绘(滚动/装完一个)不会多打一次请求。
|
|
6605
|
+
}, [call, list]);
|
|
6606
|
+
};
|
|
6607
|
+
|
|
6608
|
+
// ── 插件卡片底栏那一行事实 ────────────────────────────────────────────────
|
|
6609
|
+
// 三枚(↓近 30 天下载 / ★星数 / 核验日期)在 250px 列下可用宽只有 153–174px,英文又比
|
|
6610
|
+
// 中文长,装不下时以前靠 overflow:hidden 裁掉尾字,读起来像界面坏了。这里改成「整枚收起」:
|
|
6611
|
+
// 离流探针 .dsh-mcp-pfacts-probe 与真行同构、量的是「三枚全画要多宽」,真行(flex:1 1 0)
|
|
6612
|
+
// 的宽度恒等于可用宽,两者一比就知道放不放得下——放不下收起日期那枚(信息量最低、字最长),
|
|
6613
|
+
// 列变宽了再放回来。结论只由「探针宽 vs 行宽」两个几何量决定,不依赖上一次的结论,所以
|
|
6614
|
+
// 不会「收起→行变窄→又放下→又溢出」地抖;面板切走(宽度 0)时直接不下结论。
|
|
6615
|
+
const PluginCardFacts = ({ downloadsText, downloadsTitle, starText, starTitle, dateText, dateTitle }) => {
|
|
6616
|
+
const rowRef = useRef(null);
|
|
6617
|
+
const probeRef = useRef(null);
|
|
6618
|
+
const [dropDate, setDropDate] = useState(false);
|
|
6619
|
+
const chips = (withDate) => {
|
|
6620
|
+
const out = [];
|
|
6621
|
+
if (downloadsText) out.push(h('span', { key: 'dl', className: 'dl', title: downloadsTitle }, '↓' + downloadsText));
|
|
6622
|
+
if (starText) out.push(h('span', { key: 'star', className: 'star', title: starTitle }, h(StarIcon, { size: 10 }), starText));
|
|
6623
|
+
if (withDate && dateText) out.push(h('span', { key: 'when', className: 'when', title: dateTitle }, dateText));
|
|
6624
|
+
return out;
|
|
6625
|
+
};
|
|
6626
|
+
useEffect(() => {
|
|
6627
|
+
const measure = () => {
|
|
6628
|
+
const row = rowRef.current;
|
|
6629
|
+
const probe = probeRef.current;
|
|
6630
|
+
if (!row || !probe) return;
|
|
6631
|
+
const have = row.getBoundingClientRect().width;
|
|
6632
|
+
if (have < 8) return;
|
|
6633
|
+
setDropDate(probe.getBoundingClientRect().width > have + 1);
|
|
6634
|
+
};
|
|
6635
|
+
measure();
|
|
6636
|
+
if (typeof ResizeObserver !== 'function') return undefined;
|
|
6637
|
+
let observer = null;
|
|
6638
|
+
try {
|
|
6639
|
+
observer = new ResizeObserver(measure);
|
|
6640
|
+
if (rowRef.current) observer.observe(rowRef.current);
|
|
6641
|
+
} catch {
|
|
6642
|
+
return undefined;
|
|
6643
|
+
}
|
|
6644
|
+
return () => { if (observer) observer.disconnect(); };
|
|
6645
|
+
}, [downloadsText, starText, dateText]);
|
|
6646
|
+
return h('div', { ref: rowRef, className: 'dsh-mcp-pfacts' },
|
|
6647
|
+
chips(!dropDate),
|
|
6648
|
+
dateText ? h('div', { ref: probeRef, className: 'dsh-mcp-pfacts-probe', 'aria-hidden': true }, chips(true)) : null);
|
|
6649
|
+
};
|
|
6650
|
+
|
|
6104
6651
|
// ── Skill 看板:列表 + 详情 ──────────────────────────────────────────
|
|
6105
6652
|
// 数据来自宿主新增的 listSkills / skillDetail(走同一个 mcpManager Remote 命名空间,
|
|
6106
6653
|
// 复用既有的 call 信封 {ok, ...}),不引入第二条 Remote 通道。
|
|
@@ -6139,6 +6686,8 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6139
6686
|
const detailSeq = useRef(0);
|
|
6140
6687
|
const toastCancel = useRef(null);
|
|
6141
6688
|
const noticeCancel = useRef(null);
|
|
6689
|
+
// 工具条搜索框:清空按钮点下去自己随即卸载,焦点会掉到 body——clear 后把焦点送回 input。
|
|
6690
|
+
const searchRef = useRef(null);
|
|
6142
6691
|
// 作用域切换检测(skillpick 只在真换作用域时清)+ skillpick 恢复只做一次的哨兵。
|
|
6143
6692
|
const mountedScope = useRef(null);
|
|
6144
6693
|
const seededPick = useRef(false);
|
|
@@ -6200,6 +6749,22 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6200
6749
|
// 不挂着时不用补——上面的挂载 effect 每次都会 load(false),切回本页天然是新数据。
|
|
6201
6750
|
useEffect(() => onSkillChange(() => { load(true); }), [load]);
|
|
6202
6751
|
|
|
6752
|
+
// 真图标数据源①:专家目录(模块级缓存,与市场/已召唤页同一条)——wbexpert 捆绑技能
|
|
6753
|
+
// 用安装目录名里的 plugin 反查专家头像。拉不到静默降级:这部分技能走生成头像。
|
|
6754
|
+
const [expertByPlugin, setExpertByPlugin] = useState(() => new Map());
|
|
6755
|
+
useEffect(() => {
|
|
6756
|
+
let alive = true;
|
|
6757
|
+
loadExpertCatalogShared(call).then((data) => {
|
|
6758
|
+
if (!alive) return;
|
|
6759
|
+
const map = new Map();
|
|
6760
|
+
for (const item of (data.experts || [])) {
|
|
6761
|
+
if (item && item.plugin) map.set(item.plugin, item.avatarUrl || '');
|
|
6762
|
+
}
|
|
6763
|
+
setExpertByPlugin(map);
|
|
6764
|
+
}).catch(() => {});
|
|
6765
|
+
return () => { alive = false; };
|
|
6766
|
+
}, [call]);
|
|
6767
|
+
|
|
6203
6768
|
const queryKey = (query || '').toLowerCase();
|
|
6204
6769
|
// 分类判定只依赖 name+description、与筛选无关 → 一次 memo 全量映射;
|
|
6205
6770
|
// 分类轨道计数固定全量(决策 9),不随来源/权限/搜索联动。
|
|
@@ -6255,6 +6820,10 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6255
6820
|
}
|
|
6256
6821
|
return map;
|
|
6257
6822
|
}, [skills]);
|
|
6823
|
+
|
|
6824
|
+
// 真图标补全(数据源②):把还没有①目录反推的技能名交给 SkillHub 精确匹配。
|
|
6825
|
+
// 必须无条件调用(hook 规矩);入队侧自行跳过已有真图与已缓存/在途的名字。
|
|
6826
|
+
useSkillIcons(call, filtered, expertByPlugin);
|
|
6258
6827
|
|
|
6259
6828
|
|
|
6260
6829
|
const openDetail = useCallback(async (name) => {
|
|
@@ -6443,9 +7012,20 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6443
7012
|
h('div', { className: 'dsh-mcp-skill-content' },
|
|
6444
7013
|
// 顶部工具条:搜索 + 网格/列表分段控件(决策 5:不加 Hero,直奔工作区)。
|
|
6445
7014
|
h('div', { className: 'dsh-mcp-skill-toolbar' },
|
|
6446
|
-
h('div', { className: 'dsh-mcp-skill-search' },
|
|
7015
|
+
h('div', { className: 'dsh-mcp-skill-search' + (query ? ' querying' : '') },
|
|
6447
7016
|
h(Icon, { d: ICONS.search }),
|
|
6448
|
-
h('input', {
|
|
7017
|
+
h('input', {
|
|
7018
|
+
ref: searchRef,
|
|
7019
|
+
placeholder: t('搜索 skill…'), value: query,
|
|
7020
|
+
onChange: (e) => setQuery(e.target.value), 'aria-label': t('搜索 skill'),
|
|
7021
|
+
// Escape 清词时吃掉事件(别让上层把弹窗/浮层一起关了);没有词时放行。
|
|
7022
|
+
onKeyDown: (e) => { if (e.key === 'Escape' && query) { e.stopPropagation(); setQuery(''); } },
|
|
7023
|
+
}),
|
|
7024
|
+
query ? h('button', {
|
|
7025
|
+
type: 'button', className: 'clear',
|
|
7026
|
+
title: t('清空搜索'), 'aria-label': t('清空搜索'),
|
|
7027
|
+
onClick: () => { setQuery(''); searchRef.current?.focus(); },
|
|
7028
|
+
}, h(Icon, { d: ICONS.x, size: 10 })) : null,
|
|
6449
7029
|
),
|
|
6450
7030
|
h('div', { className: 'dsh-mcp-skill-seg', role: 'group', 'aria-label': t('视图切换') },
|
|
6451
7031
|
h('button', {
|
|
@@ -6548,6 +7128,11 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6548
7128
|
className: 'dsh-mcp-skill' + (selected === s.name ? ' on' : ''),
|
|
6549
7129
|
onClick: () => openDetail(s.name),
|
|
6550
7130
|
},
|
|
7131
|
+
// 列表行行首也给头像:有真图用真图(与市场同源),没有用生成头像,
|
|
7132
|
+
// 两种视图观感一致。
|
|
7133
|
+
skillIconFor(s, expertByPlugin)
|
|
7134
|
+
? h(Avatar, { url: skillIconFor(s, expertByPlugin), name: s.name, className: 'dsh-mcp-skill-avatar-img sm' })
|
|
7135
|
+
: h(SkillAvatar, { name: s.name, size: 'sm' }),
|
|
6551
7136
|
h('div', { className: 'dsh-mcp-skill-main' },
|
|
6552
7137
|
h('div', { className: 'dsh-mcp-skill-name' }, s.name),
|
|
6553
7138
|
h('div', { className: 'dsh-mcp-skill-sub' },
|
|
@@ -6580,7 +7165,12 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6580
7165
|
},
|
|
6581
7166
|
h('div', { className: 'dsh-mcp-skill-card-inner' },
|
|
6582
7167
|
h('div', { className: 'dsh-mcp-skill-card-top' },
|
|
6583
|
-
|
|
7168
|
+
// 市场同款头像位:wbexpert 捆绑技能反推专家真头像、其余按 SkillHub
|
|
7169
|
+
// slug 精确匹配补真图标(skillIconFor),取不到真图才用生成头像。
|
|
7170
|
+
// 分类语义保留:title 带分类名,foot 里有分类文字。
|
|
7171
|
+
skillIconFor(s, expertByPlugin)
|
|
7172
|
+
? h(Avatar, { url: skillIconFor(s, expertByPlugin), name: s.name, className: 'dsh-mcp-skill-avatar-img', title: sCat.name })
|
|
7173
|
+
: h(SkillAvatar, { name: s.name, title: sCat.name }),
|
|
6584
7174
|
h('span', { className: 'dsh-mcp-skill-card-name' }, s.name),
|
|
6585
7175
|
),
|
|
6586
7176
|
s.description
|
|
@@ -6635,15 +7225,21 @@ const SkillBoard = ({ call, scope, timer }) => {
|
|
|
6635
7225
|
? h('div', { className: 'dsh-mcp-skill-empty' }, t('从左侧选择一个 skill 查看详情'))
|
|
6636
7226
|
: null,
|
|
6637
7227
|
summary ? h('div', { className: 'dsh-mcp-skill-d-head' },
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
7228
|
+
// 详情头与市场详情弹层同一构图:头像 + 标题块,打开目录按钮保持在最右。
|
|
7229
|
+
h('div', { className: 'dsh-mcp-skill-d-id' },
|
|
7230
|
+
skillIconFor(summary, expertByPlugin)
|
|
7231
|
+
? h(Avatar, { url: skillIconFor(summary, expertByPlugin), name: summary.name, className: 'dsh-mcp-skill-avatar-img lg' })
|
|
7232
|
+
: h(SkillAvatar, { name: summary.name, size: 'lg' }),
|
|
7233
|
+
h('div', null,
|
|
7234
|
+
h('h3', { className: 'dsh-mcp-skill-d-title' }, summary.name),
|
|
7235
|
+
h('div', { className: 'dsh-mcp-skill-d-badges' },
|
|
7236
|
+
summaryCat ? h(Badge, { cls: 't-stdio' }, summaryCat.name) : null,
|
|
7237
|
+
summary.modelInvocable ? h(Badge, { cls: 't-http' }, t('模型可调用')) : null,
|
|
7238
|
+
summary.userInvocable ? h(Badge, { cls: 't-stdio' }, t('用户可调用')) : null,
|
|
7239
|
+
),
|
|
7240
|
+
h('p', { className: 'dsh-mcp-skill-d-src' },
|
|
7241
|
+
skillSourceLabel(summary.source) + (summary.provider ? ' · ' + summary.provider : '')),
|
|
6644
7242
|
),
|
|
6645
|
-
h('p', { className: 'dsh-mcp-skill-d-src' },
|
|
6646
|
-
skillSourceLabel(summary.source) + (summary.provider ? ' · ' + summary.provider : '')),
|
|
6647
7243
|
),
|
|
6648
7244
|
summary.path ? h('button', {
|
|
6649
7245
|
type: 'button',
|
|
@@ -6762,7 +7358,9 @@ const WB_CONNECTOR_SORTS = [
|
|
|
6762
7358
|
{ key: 'name', get label() { return t('名称 A→Z'); } },
|
|
6763
7359
|
];
|
|
6764
7360
|
// T06/Awesome:DSH 社区目录排序(全量本地排序,不受分页影响)。
|
|
6765
|
-
// upstream 自带 stars 与 lastVerified
|
|
7361
|
+
// upstream 自带 stars 与 lastVerified,无评分字段,不臆造。
|
|
7362
|
+
// 下载量从 2.3.x 起有了(npm 公开统计),但它**只给当前页按可见卡片懒取**、且是
|
|
7363
|
+
// 近 30 天窗口:拿一份不完整、随页变化的数据做全量排序会排出个假名次,所以不加这档。
|
|
6766
7364
|
const AWESOME_SORTS = [
|
|
6767
7365
|
{ key: 'stars', get label() { return t('最多星标'); } },
|
|
6768
7366
|
{ key: 'recent', get label() { return t('最新收录'); } },
|
|
@@ -6872,15 +7470,18 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
6872
7470
|
return () => { alive = false; off(); stopPoll(); };
|
|
6873
7471
|
}, [call]);
|
|
6874
7472
|
|
|
6875
|
-
// §4.1
|
|
7473
|
+
// §4.1 召唤位推荐 + 头像补全:无条件取共享目录(模块级 10 分钟缓存,与市场页同一条,
|
|
7474
|
+
// 市场开过就是零网络)。为什么不能只在架上为空时取:预设行的 avatarUrl 依赖**宿主侧**
|
|
7475
|
+
// expert-market.js 的新旧——宿主进程没重启时 wbExpertInstalled 不带 avatarUrl,卡面就
|
|
7476
|
+
// 永远是字母。目录是稳定的旧 RPC,卡片头像一律先查目录(plugin → avatarUrl)再回落行字段。
|
|
6876
7477
|
useEffect(() => {
|
|
6877
|
-
if (loading
|
|
7478
|
+
if (loading) return undefined;
|
|
6878
7479
|
let alive = true;
|
|
6879
7480
|
loadExpertCatalogShared(call).then((data) => {
|
|
6880
7481
|
if (alive) setCatalog(Array.isArray(data.experts) ? data.experts : []);
|
|
6881
7482
|
}).catch(() => {});
|
|
6882
7483
|
return () => { alive = false; };
|
|
6883
|
-
}, [call, loading
|
|
7484
|
+
}, [call, loading]);
|
|
6884
7485
|
|
|
6885
7486
|
// 召唤(推荐位点击):与市场页同一条写链,成功后刷新本页 + 广播给会话头徽章。
|
|
6886
7487
|
const summon = async (expert) => {
|
|
@@ -6942,6 +7543,14 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
6942
7543
|
// 推荐位:目录里还没召唤的,按目录序取前 6 —— 不排序、不评分,避免自造推荐逻辑。
|
|
6943
7544
|
const summonedPlugins = new Set(presets.map((row) => row.plugin));
|
|
6944
7545
|
const recommend = catalog.filter((item) => item && item.plugin && !summonedPlugins.has(item.plugin)).slice(0, 6);
|
|
7546
|
+
// plugin → 头像:预设行字段优先?不——目录优先。预设行的 avatarUrl 依赖宿主侧模块
|
|
7547
|
+
// 新旧(宿主没重启就不带),目录走的是稳定旧 RPC,两处都有时值也相同,取目录只为
|
|
7548
|
+
// 在行字段缺席时仍能出图。查不到(目录没落盘)回落行字段,再回落空 → Avatar 出字母。
|
|
7549
|
+
const catalogAvatar = new Map();
|
|
7550
|
+
for (const item of catalog) {
|
|
7551
|
+
if (item && item.plugin && item.avatarUrl) catalogAvatar.set(item.plugin, item.avatarUrl);
|
|
7552
|
+
}
|
|
7553
|
+
const presetAvatar = (plugin) => catalogAvatar.get(plugin) || '';
|
|
6945
7554
|
return h('div', null,
|
|
6946
7555
|
note ? h('div', { className: 'dsh-mcp-wb-note', role: 'status' }, note) : null,
|
|
6947
7556
|
busyLabel ? h('div', { className: 'dsh-mcp-wb-note busy', 'aria-live': 'polite' }, busyLabel) : null,
|
|
@@ -6965,6 +7574,12 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
6965
7574
|
const preset = presets.find((p) => p.plugin === plugin);
|
|
6966
7575
|
const bound = skillsByPlugin[plugin] || [];
|
|
6967
7576
|
return h('div', { key: plugin, className: 'dsh-mcp-onsite-body' },
|
|
7577
|
+
// 在场行也给头像:目录优先(稳定旧 RPC),回落预设行字段,与下方卡片一一对应。
|
|
7578
|
+
h(Avatar, {
|
|
7579
|
+
url: preset ? (presetAvatar(plugin) || preset.avatarUrl) : '',
|
|
7580
|
+
name: (rows[0] && rows[0].name) || (preset && preset.nameZh) || plugin,
|
|
7581
|
+
className: 'dsh-mcp-onsite-avatar',
|
|
7582
|
+
}),
|
|
6968
7583
|
h('div', { className: 'dsh-mcp-onsite-name' }, (rows[0] && rows[0].name) || (preset && preset.presetId) || plugin),
|
|
6969
7584
|
preset && preset.presetId ? h('div', { className: 'dsh-mcp-onsite-role' }, preset.presetId) : null,
|
|
6970
7585
|
bound.length
|
|
@@ -7002,11 +7617,20 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
7002
7617
|
const boundSkills = skillsByPlugin[row.plugin] || [];
|
|
7003
7618
|
const activeList = activeByPlugin.get(row.plugin) || [];
|
|
7004
7619
|
const confirming = confirmDismiss === row.plugin;
|
|
7620
|
+
// 显示名与召唤菜单同一投影(跟宿主语言):目录缓存名 → 预设行自带名 → plugin 标识名。
|
|
7621
|
+
const presetLabel = usesEnglish()
|
|
7622
|
+
? (row.nameEn || row.nameZh || row.plugin)
|
|
7623
|
+
: (row.nameZh || row.nameEn || row.plugin);
|
|
7005
7624
|
return h('div', { key: row.presetId, className: 'dsh-mcp-card wb installed' },
|
|
7006
7625
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7626
|
+
// 市场专家卡同款头像位:目录优先(稳定旧 RPC,宿主没重启也能出图),
|
|
7627
|
+
// 回落 wbExpertInstalled 带出的行字段;取不到图时 Avatar 自动回落首字母。
|
|
7628
|
+
h('div', { className: 'dsh-mcp-card-person' },
|
|
7629
|
+
h(Avatar, { url: presetAvatar(row.plugin) || row.avatarUrl, name: presetLabel, className: 'dsh-mcp-card-avatar' }),
|
|
7630
|
+
h('div', null,
|
|
7631
|
+
h('div', { className: 'dsh-mcp-card-name' }, presetLabel),
|
|
7632
|
+
h('div', { className: 'dsh-mcp-card-vendor' }, row.presetId),
|
|
7633
|
+
),
|
|
7010
7634
|
),
|
|
7011
7635
|
activeList.length
|
|
7012
7636
|
? h(Badge, { cls: 's-on' }, t('在场 · {0} 会话', [activeList.length]))
|
|
@@ -7044,18 +7668,24 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
7044
7668
|
)
|
|
7045
7669
|
: null,
|
|
7046
7670
|
// §4.1 空白区:推荐位(原页面 80% 是空白,且没有任何「下一步」)。
|
|
7671
|
+
// 排版走 .dsh-mcp-wb-reco:提示行与上方已召唤网格、下方推荐卡网格之间都要留白,
|
|
7672
|
+
// 10.5px 的小字提示紧贴卡片会糊成一片(间距见 CSS 侧同名规则)。
|
|
7047
7673
|
!loading && recommend.length
|
|
7048
|
-
? h('div',
|
|
7049
|
-
h('div', { className: 'dsh-mcp-item-sub'
|
|
7674
|
+
? h('div', { className: 'dsh-mcp-wb-reco' },
|
|
7675
|
+
h('div', { className: 'dsh-mcp-item-sub' },
|
|
7050
7676
|
h('span', null, presets.length ? t('再看看这些(目录前 {0} 位未召唤)', [recommend.length]) : t('可以从这几位开始(目录前 {0} 位)', [recommend.length])),
|
|
7051
7677
|
h('a', { href: '#', onClick: (e) => { e.preventDefault(); openBoardCategory('wb'); } }, t('去市场看全部')),
|
|
7052
7678
|
),
|
|
7053
7679
|
h('div', { className: 'dsh-mcp-wb-grid' },
|
|
7054
7680
|
recommend.map((item) => h('div', { key: item.plugin, className: 'dsh-mcp-card wb' },
|
|
7055
7681
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
h(
|
|
7682
|
+
// 推荐位与市场卡同构(card-person + Avatar),目录行自带 avatarUrl。
|
|
7683
|
+
h('div', { className: 'dsh-mcp-card-person' },
|
|
7684
|
+
h(Avatar, { url: item.avatarUrl, name: item.name || item.plugin, className: 'dsh-mcp-card-avatar' }),
|
|
7685
|
+
h('div', null,
|
|
7686
|
+
h('div', { className: 'dsh-mcp-card-name' }, item.name || item.plugin),
|
|
7687
|
+
h('div', { className: 'dsh-mcp-card-vendor' }, item.profession || item.category || item.plugin),
|
|
7688
|
+
),
|
|
7059
7689
|
),
|
|
7060
7690
|
),
|
|
7061
7691
|
item.description ? h('div', { className: 'dsh-mcp-card-summary' }, item.description) : null,
|
|
@@ -7070,7 +7700,11 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
7070
7700
|
detail
|
|
7071
7701
|
? h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && setDetail(null) },
|
|
7072
7702
|
h('div', { ref: summonedRef, className: 'dsh-mcp-modal dsh-mcp-wb-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('已召唤专家详情'), tabIndex: -1, onKeyDown: (e) => { if (e.key === 'Escape' && !busy) { e.stopPropagation(); setDetail(null); } } },
|
|
7073
|
-
|
|
7703
|
+
// 详情头与市场专家详情同一构图:头像 + 标题行(wbExpertDetail 展开 catalog 行自带 avatarUrl)。
|
|
7704
|
+
h('div', { className: 'dsh-mcp-wb-modal-head' },
|
|
7705
|
+
h(Avatar, { url: detail.avatarUrl, name: detail.name || detail.plugin, className: 'dsh-mcp-detail-avatar' }),
|
|
7706
|
+
h('h3', null, detail.name ? t('{0}({1})', [detail.name, detail.profession || '']) : detail.plugin),
|
|
7707
|
+
),
|
|
7074
7708
|
h('div', null,
|
|
7075
7709
|
detail.detailError ? h('p', { className: 'dsh-mcp-wb-error' }, detail.detailError) : null,
|
|
7076
7710
|
detail.description ? h('p', { className: 'dsh-mcp-wb-modal-desc' }, detail.description) : null,
|
|
@@ -7101,6 +7735,15 @@ const ExpertInstalledBoard = ({ call }) => {
|
|
|
7101
7735
|
: null,
|
|
7102
7736
|
);
|
|
7103
7737
|
};
|
|
7738
|
+
// 装备清单「去市场」需要把专家搜索词带进专家页;ExpertMarket 的签名被回归测试钉死,
|
|
7739
|
+
// 所以用一个一次性模块级种子,不新增 prop。
|
|
7740
|
+
let expertMarketSeedQuery = '';
|
|
7741
|
+
const seedExpertMarketQuery = (value) => { expertMarketSeedQuery = String(value || '').trim(); };
|
|
7742
|
+
const takeExpertMarketSeedQuery = () => {
|
|
7743
|
+
const value = expertMarketSeedQuery;
|
|
7744
|
+
expertMarketSeedQuery = '';
|
|
7745
|
+
return value;
|
|
7746
|
+
};
|
|
7104
7747
|
// 专家市场(市场页子标签):浏览全量目录 + 召唤/送还。
|
|
7105
7748
|
const ExpertMarket = ({ call }) => {
|
|
7106
7749
|
const [loading, setLoading] = useState(true);
|
|
@@ -7110,8 +7753,9 @@ const ExpertMarket = ({ call }) => {
|
|
|
7110
7753
|
const [note, setNote] = useState('');
|
|
7111
7754
|
const [experts, setExperts] = useState([]);
|
|
7112
7755
|
const [categories, setCategories] = useState([]);
|
|
7756
|
+
const [meta, setMeta] = useState({});
|
|
7113
7757
|
const [source, setSource] = useState('');
|
|
7114
|
-
const [query, setQuery] = useState(
|
|
7758
|
+
const [query, setQuery] = useState(() => takeExpertMarketSeedQuery());
|
|
7115
7759
|
const [category, setCategory] = useState('all');
|
|
7116
7760
|
const [page, setPage] = useState(1);
|
|
7117
7761
|
const [detail, setDetail] = useState(null);
|
|
@@ -7152,7 +7796,15 @@ const ExpertMarket = ({ call }) => {
|
|
|
7152
7796
|
const data = await loadExpertCatalogShared(call, force);
|
|
7153
7797
|
setExperts(data.experts);
|
|
7154
7798
|
setCategories(data.categories);
|
|
7799
|
+
setMeta(data.meta || {});
|
|
7155
7800
|
setSource(data.loadedFrom);
|
|
7801
|
+
if (recordMarketSnapshot('expert', data.experts.map((expert) => ({
|
|
7802
|
+
key: expert.id || expert.plugin,
|
|
7803
|
+
name: expert.name,
|
|
7804
|
+
sig: String(expert.updatedAt || ''),
|
|
7805
|
+
})))) {
|
|
7806
|
+
emitMarketPulse();
|
|
7807
|
+
}
|
|
7156
7808
|
} catch (e) {
|
|
7157
7809
|
setError(String(e?.message || e));
|
|
7158
7810
|
} finally {
|
|
@@ -7256,6 +7908,16 @@ const ExpertMarket = ({ call }) => {
|
|
|
7256
7908
|
h('span', { className: 'dsh-mcp-builtin-count' },
|
|
7257
7909
|
t('显示 {0} / {1}', [filtered.length, experts.length]) + (searching ? t(' · 全库搜索(忽略分类)') : '') + (source === 'network' ? t(' · 已更新') : source === 'disk' ? t(' · 缓存') : '')),
|
|
7258
7910
|
),
|
|
7911
|
+
(meta.authorName || meta.lastUpdated)
|
|
7912
|
+
? h('div', { className: 'dsh-mcp-catalog-meta' },
|
|
7913
|
+
meta.authorName ? h('span', null, t('目录维护方:{0}', [meta.authorName])) : null,
|
|
7914
|
+
meta.lastUpdated ? h('span', null, t('目录更新于 {0}', [relativeLabel(meta.lastUpdated)])) : null,
|
|
7915
|
+
/^https?:\/\//i.test(String(meta.authorSource || ''))
|
|
7916
|
+
? h('a', { href: meta.authorSource, target: '_blank', rel: 'noreferrer' }, t('查看来源'))
|
|
7917
|
+
: null,
|
|
7918
|
+
meta.license ? h('span', null, meta.license) : null,
|
|
7919
|
+
)
|
|
7920
|
+
: null,
|
|
7259
7921
|
// 搜索态置灰不隐藏:位置稳定,用户知道「分类还在,只是这次搜索没按它筛」。
|
|
7260
7922
|
h('div', { className: 'dsh-mcp-market-chips' + (searching ? ' muted' : ''), title: searching ? t('搜索时自动在全库匹配(忽略分类)') : undefined },
|
|
7261
7923
|
categoryChips.map((row) => h('button', {
|
|
@@ -7284,9 +7946,12 @@ const ExpertMarket = ({ call }) => {
|
|
|
7284
7946
|
const staleTeam = actions.update;
|
|
7285
7947
|
return h('div', { key: item.plugin, className: 'dsh-mcp-card wb' + (summoned ? ' installed' : '') },
|
|
7286
7948
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
7287
|
-
h('div',
|
|
7288
|
-
h(
|
|
7289
|
-
h('div',
|
|
7949
|
+
h('div', { className: 'dsh-mcp-card-person' },
|
|
7950
|
+
h(Avatar, { url: item.avatarUrl, name: item.name, className: 'dsh-mcp-card-avatar' }),
|
|
7951
|
+
h('div', null,
|
|
7952
|
+
h('div', { className: 'dsh-mcp-card-name' }, t('{0}({1})', [item.name, item.profession])),
|
|
7953
|
+
h('div', { className: 'dsh-mcp-card-vendor' }, item.categoryName || item.categoryId),
|
|
7954
|
+
),
|
|
7290
7955
|
),
|
|
7291
7956
|
staleTeam
|
|
7292
7957
|
? h(Badge, {
|
|
@@ -7305,6 +7970,7 @@ const ExpertMarket = ({ call }) => {
|
|
|
7305
7970
|
h(Badge, null, item.expertType === 'team' ? t('专家团') : t('专家')),
|
|
7306
7971
|
item.tags.map((tag) => h(Badge, { key: tag }, tag)),
|
|
7307
7972
|
installedSkillPlugins.has(item.plugin) ? h(Badge, null, t('含技能')) : null,
|
|
7973
|
+
item.updatedAt ? h(Badge, { title: wbFormatDate(timestampOf(item.updatedAt)) }, t('更新于 {0}', [relativeLabel(item.updatedAt)])) : null,
|
|
7308
7974
|
),
|
|
7309
7975
|
h('div', { className: 'dsh-mcp-wb-card-actions' },
|
|
7310
7976
|
h('button', { type: 'button', className: 'dsh-mcp-btn', disabled: openingPlugin === item.plugin, 'aria-busy': openingPlugin === item.plugin || undefined, onClick: () => openDetail(item) }, openingPlugin === item.plugin ? t('读取中…') : t('详情')),
|
|
@@ -7341,11 +8007,35 @@ const ExpertMarket = ({ call }) => {
|
|
|
7341
8007
|
});
|
|
7342
8008
|
return h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && setDetail(null) },
|
|
7343
8009
|
h('div', { ref: expertDetailRef, className: 'dsh-mcp-modal dsh-mcp-wb-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('专家详情'), tabIndex: -1, onKeyDown: (e) => { if (e.key === 'Escape' && !busy) { e.stopPropagation(); setDetail(null); } } },
|
|
7344
|
-
h('
|
|
7345
|
-
|
|
8010
|
+
h('div', { className: 'dsh-mcp-detail-person' },
|
|
8011
|
+
h(Avatar, { url: detail.avatarUrl, name: detail.name, className: 'dsh-mcp-detail-avatar' }),
|
|
8012
|
+
h('div', null,
|
|
8013
|
+
h('h3', null, t('{0}({1})', [detail.name, detail.profession])),
|
|
8014
|
+
h('div', { className: 'dsh-mcp-card-vendor' }, detail.categoryName || detail.categoryId || ''),
|
|
8015
|
+
),
|
|
8016
|
+
),
|
|
8017
|
+
h(PresenceLine, {
|
|
8018
|
+
owner: meta.authorName,
|
|
8019
|
+
updatedAt: detail.updatedAt,
|
|
8020
|
+
note: detail.expertType === 'team' ? t('专家团') : t('专家人设'),
|
|
8021
|
+
className: 'dsh-mcp-presence-detail',
|
|
8022
|
+
}),
|
|
8023
|
+
// 真实出口前置:详情再长也不必先滑到底才能去仓库 / 提 Issue。
|
|
8024
|
+
h(FeedbackLinks, {
|
|
8025
|
+
item: detail, kind: 'expert',
|
|
8026
|
+
homepage: meta.authorSource,
|
|
8027
|
+
sourceLabel: meta.authorName ? t('目录维护方:{0}', [meta.authorName]) : '',
|
|
8028
|
+
}),
|
|
7346
8029
|
h('div', null,
|
|
7347
8030
|
detail.detailError ? h('p', { className: 'dsh-mcp-wb-error' }, detail.detailError) : null,
|
|
7348
8031
|
detail.description ? h('p', { className: 'dsh-mcp-wb-modal-desc' }, detail.description) : null,
|
|
8032
|
+
(meta.version || detail.createdAt || detail.updatedAt)
|
|
8033
|
+
? h('div', { className: 'dsh-mcp-card-tags' },
|
|
8034
|
+
meta.version ? h(Badge, null, 'v' + meta.version) : null,
|
|
8035
|
+
detail.createdAt ? h(Badge, null, t('加入时间 {0}', [wbFormatDate(timestampOf(detail.createdAt))])) : null,
|
|
8036
|
+
detail.updatedAt ? h(Badge, null, t('人设更新于 {0}', [relativeLabel(detail.updatedAt)])) : null,
|
|
8037
|
+
)
|
|
8038
|
+
: null,
|
|
7349
8039
|
detail.quickPrompts && detail.quickPrompts.length
|
|
7350
8040
|
? (() => {
|
|
7351
8041
|
// OPT-26 · 3.3:quickPrompts 超过 3 条时默认折叠,按钮切换展开/收起。
|
|
@@ -7496,6 +8186,20 @@ const quickMentionText = (kind, item) => {
|
|
|
7496
8186
|
const token = String(item.name || '');
|
|
7497
8187
|
return /^[\w-]+$/.test(token) ? `/${token} ` : t('{0}:', [token]);
|
|
7498
8188
|
};
|
|
8189
|
+
// 二级菜单横向展开方向:输入框动作区贴右侧,主菜单常被视口夹到右缘;若第二级仍向右开,
|
|
8190
|
+
// 232px 会整块伸出屏幕。以锚点行为基准判断(不用子菜单当前所在侧,避免翻转后来回抖动):
|
|
8191
|
+
// 右侧放不下且左侧更宽裕时向左开;两侧都放不下时也选更宽的一侧,把溢出减到最小。
|
|
8192
|
+
// width 传子菜单实测外宽(含 padding/border);未挂载时按 232 + 12 估算。
|
|
8193
|
+
const quickSubmenuOpensLeft = (row, width = 244) => {
|
|
8194
|
+
if (!row) return false;
|
|
8195
|
+
const rect = row.getBoundingClientRect();
|
|
8196
|
+
const viewportW = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
8197
|
+
const gap = 4 + 8; // 与主菜单的 4px 间距 + 视口 8px 留白
|
|
8198
|
+
const roomRight = viewportW - rect.right - gap;
|
|
8199
|
+
const roomLeft = rect.left - gap;
|
|
8200
|
+
const outerWidth = Math.min(width, Math.max(0, viewportW - 16));
|
|
8201
|
+
return roomRight < outerWidth && roomLeft >= roomRight;
|
|
8202
|
+
};
|
|
7499
8203
|
const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
7500
8204
|
useI18nRevision(); // 宿主切语言后重渲染,模块级 getter 文案即时更新
|
|
7501
8205
|
const [open, setOpen] = useState(false);
|
|
@@ -7503,13 +8207,25 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7503
8207
|
const [recent, setRecent] = useState(null); // { skill: [], mcp: [], expert: [] }
|
|
7504
8208
|
const [listError, setListError] = useState('');
|
|
7505
8209
|
const [copied, setCopied] = useState('');
|
|
7506
|
-
//
|
|
8210
|
+
// 子菜单翻转:展开后实测几何,纵向越界加 flip(向上展开),横向越界加 hflip(向左展开)。
|
|
7507
8211
|
// 只在展开/hoverKind 变化时测一次(临时浮层,不监听滚动缩放)。
|
|
7508
8212
|
const [flipKinds, setFlipKinds] = useState(() => new Set());
|
|
8213
|
+
const [hflipKinds, setHflipKinds] = useState(() => new Set());
|
|
7509
8214
|
const rowRefs = useRef(new Map());
|
|
7510
8215
|
const rootRef = useRef(null);
|
|
7511
8216
|
const closeTimer = useRef(null);
|
|
7512
8217
|
const cancelClose = () => { if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; } };
|
|
8218
|
+
// 展开子菜单前先定横向方向:等挂载后再测会先按默认方向画一帧,而菜单贴右缘时
|
|
8219
|
+
// 那一帧整块在屏幕外。实测宽度的精修仍由下面的 effect 负责。
|
|
8220
|
+
const markSubmenuSide = (kindKey, row) => {
|
|
8221
|
+
const opensLeft = quickSubmenuOpensLeft(row);
|
|
8222
|
+
setHflipKinds((prev) => {
|
|
8223
|
+
if (prev.has(kindKey) === opensLeft) return prev;
|
|
8224
|
+
const next = new Set(prev);
|
|
8225
|
+
if (opensLeft) next.add(kindKey); else next.delete(kindKey);
|
|
8226
|
+
return next;
|
|
8227
|
+
});
|
|
8228
|
+
};
|
|
7513
8229
|
// scheduleClose 在 JSX 前定义(rootHoverProps 用它);此处不重复声明。
|
|
7514
8230
|
|
|
7515
8231
|
// 菜单打开时拉一次各类清单。清单取全量(不做 6 条截断),交给子菜单自己
|
|
@@ -7534,7 +8250,14 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7534
8250
|
setRecent({
|
|
7535
8251
|
skill: skillRows.map((row) => ({ name: row.name, description: row.description || row.whenToUse || '' })),
|
|
7536
8252
|
mcp: mcpRows.map((row) => ({ name: row.serverName, description: row.url || row.command || row.transport || '' })),
|
|
7537
|
-
|
|
8253
|
+
// 专家显示名由宿主从目录缓存补上(nameZh/nameEn),两套都留在行里:
|
|
8254
|
+
// 菜单开着时宿主切语言,itemLabel 直接按当前 locale 重选,不必重新拉取。
|
|
8255
|
+
expert: expertRows.map((row) => ({
|
|
8256
|
+
plugin: row.plugin,
|
|
8257
|
+
name: row.nameZh || row.nameEn || row.plugin,
|
|
8258
|
+
nameEn: row.nameEn || row.nameZh || row.plugin,
|
|
8259
|
+
presetId: row.presetId,
|
|
8260
|
+
})),
|
|
7538
8261
|
// 数据层读侧已经滤掉「无名/无成员」的残缺条目,这里再兜一次手改文件的形状:
|
|
7539
8262
|
// 空套件展开后什么都插不进草稿,留在菜单里只是个点了没反应的死条目。
|
|
7540
8263
|
suite: suiteRows
|
|
@@ -7550,10 +8273,13 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7550
8273
|
recentExpert: Array.isArray(kinds.expert) ? kinds.expert : [],
|
|
7551
8274
|
});
|
|
7552
8275
|
}, [call]);
|
|
8276
|
+
// 关闭时保留上次清单:再次打开先用缓存内容按最终高度出菜单,后台再刷新,
|
|
8277
|
+
// 避免每次点击都先闪一版 loading 骨架再收缩。首次打开仍会短暂显示骨架,
|
|
8278
|
+
// 但位置已在布局阶段定好,不会出现「高菜单栏闪一下」。
|
|
7553
8279
|
useEffect(() => {
|
|
7554
|
-
if (open
|
|
7555
|
-
|
|
7556
|
-
}, [open,
|
|
8280
|
+
if (!open) return;
|
|
8281
|
+
loadRecent().catch((e) => setListError(String(e?.message || e)));
|
|
8282
|
+
}, [open, loadRecent]);
|
|
7557
8283
|
|
|
7558
8284
|
// ── 主菜单 fixed 定位(页面不动的根修)────────────────────────────────
|
|
7559
8285
|
// 菜单原来 position:absolute 锚在图标上:attachNote/新建按钮让菜单变高时,
|
|
@@ -7562,15 +8288,20 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7562
8288
|
// 菜单再高也只是视口内的浮层,页面滚动零位移。翻转/关闭判定不受影响。
|
|
7563
8289
|
const menuRef = useRef(null);
|
|
7564
8290
|
const [menuPos, setMenuPos] = useState(null);
|
|
7565
|
-
|
|
8291
|
+
// 必须用 useLayoutEffect:菜单首帧还没有定位(style 为空)时,position:fixed 会按
|
|
8292
|
+
// 静态位置落在输入框行里,一帧后再跳到实测位置——用户看到的就是「很高的菜单栏闪现」。
|
|
8293
|
+
// 布局阶段测量 + 定位,浏览器一次绘制就已经在正确位置;未定位前菜单保持 visibility:hidden。
|
|
8294
|
+
useLayoutEffect(() => {
|
|
7566
8295
|
if (!open) { setMenuPos(null); return; }
|
|
7567
8296
|
const measure = () => {
|
|
7568
8297
|
const trigger = rootRef.current?.querySelector('.dsh-mcp-quick-trigger');
|
|
7569
8298
|
if (!trigger) return;
|
|
7570
8299
|
const rect = trigger.getBoundingClientRect();
|
|
7571
|
-
const width = menuRef.current?.offsetWidth ||
|
|
7572
|
-
//
|
|
7573
|
-
|
|
8300
|
+
const width = menuRef.current?.offsetWidth || 128;
|
|
8301
|
+
// 一级菜单水平居中于引用图标:left = 图标中心 − 菜单半宽。
|
|
8302
|
+
// 贴到视口右缘时夹回视口内,左侧不足 4px 时向右兜底。
|
|
8303
|
+
const centered = rect.left + rect.width / 2 - width / 2;
|
|
8304
|
+
const left = Math.max(4, Math.min(centered, window.innerWidth - width - 4));
|
|
7574
8305
|
const style = { position: 'fixed', left: Math.round(left) + 'px' };
|
|
7575
8306
|
const needsUp = rect.top > window.innerHeight / 2;
|
|
7576
8307
|
if (needsUp) style.bottom = Math.round(window.innerHeight - rect.top + 6) + 'px';
|
|
@@ -7586,7 +8317,14 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7586
8317
|
// 子菜单翻转测量:hoverKind 变化且子菜单已挂载后,实测几何再决定 flip。
|
|
7587
8318
|
// 布局在一帧内即可稳定(子菜单定位纯 CSS),rAF 双保险避免读到中间态。
|
|
7588
8319
|
useEffect(() => {
|
|
7589
|
-
if (!open
|
|
8320
|
+
if (!open) {
|
|
8321
|
+
// 关闭后清空翻转态:下次在别处打开时先按默认方向渲染,再由实测修正,
|
|
8322
|
+
// 避免沿用上一次位置的 flip/hflip 闪一下。
|
|
8323
|
+
setFlipKinds((prev) => (prev.size ? new Set() : prev));
|
|
8324
|
+
setHflipKinds((prev) => (prev.size ? new Set() : prev));
|
|
8325
|
+
return;
|
|
8326
|
+
}
|
|
8327
|
+
if (!hoverKind) return;
|
|
7590
8328
|
let alive = true;
|
|
7591
8329
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
7592
8330
|
if (!alive) return;
|
|
@@ -7594,7 +8332,8 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7594
8332
|
const sub = row ? row.querySelector('.dsh-mcp-quick-sub') : null;
|
|
7595
8333
|
if (!row || !sub) return;
|
|
7596
8334
|
const rect = sub.getBoundingClientRect();
|
|
7597
|
-
const
|
|
8335
|
+
const viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
8336
|
+
const overflowBottom = rect.bottom - viewportH;
|
|
7598
8337
|
setFlipKinds((prev) => {
|
|
7599
8338
|
const shouldFlip = overflowBottom > 8;
|
|
7600
8339
|
if (prev.has(hoverKind) === shouldFlip) return prev;
|
|
@@ -7602,6 +8341,15 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7602
8341
|
if (shouldFlip) next.add(hoverKind); else next.delete(hoverKind);
|
|
7603
8342
|
return next;
|
|
7604
8343
|
});
|
|
8344
|
+
// 横向用子菜单实测外宽精修一次(CSS max-width 在窄窗下会收窄它);
|
|
8345
|
+
// 方向判据与悬停前那次一致,不会因当前所在侧而抖动。
|
|
8346
|
+
const shouldHflip = quickSubmenuOpensLeft(row, sub.offsetWidth || 232);
|
|
8347
|
+
setHflipKinds((prev) => {
|
|
8348
|
+
if (prev.has(hoverKind) === shouldHflip) return prev;
|
|
8349
|
+
const next = new Set(prev);
|
|
8350
|
+
if (shouldHflip) next.add(hoverKind); else next.delete(hoverKind);
|
|
8351
|
+
return next;
|
|
8352
|
+
});
|
|
7605
8353
|
}));
|
|
7606
8354
|
return () => { alive = false; };
|
|
7607
8355
|
}, [open, hoverKind, recent]);
|
|
@@ -7690,7 +8438,14 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7690
8438
|
if (kind === 'mcp') return orderSubItems(kind, recent.mcp);
|
|
7691
8439
|
return orderSubItems(kind, recent.expert);
|
|
7692
8440
|
};
|
|
7693
|
-
|
|
8441
|
+
// 专家名跟随宿主语言:两套名都已随行送到(见 loadRecent),这里只做投影;
|
|
8442
|
+
// 由 useI18nRevision 驱动重渲染,切换语言当帧生效。缺译文时回落另一语言,最后才是 plugin。
|
|
8443
|
+
const itemLabel = (kind, item) => {
|
|
8444
|
+
if (kind !== 'expert') return item.name;
|
|
8445
|
+
return usesEnglish()
|
|
8446
|
+
? (item.nameEn || item.name || item.plugin)
|
|
8447
|
+
: (item.name || item.nameEn || item.plugin);
|
|
8448
|
+
};
|
|
7694
8449
|
const itemDesc = (kind, item) => (kind === 'mcp' ? (item.transport ? item.transport : '') : (item.description || '').slice(0, 48));
|
|
7695
8450
|
|
|
7696
8451
|
// ── 挂专家(WorkBuddy 式:随时挂 / 换 / 摘)──────────────────────────────
|
|
@@ -7724,7 +8479,7 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7724
8479
|
if (res && res.ok) {
|
|
7725
8480
|
setAttached({ plugin: item.plugin, name: res.name || item.name || item.plugin, mode: 'persona' });
|
|
7726
8481
|
// 团队型说明只在 trigger 的 title(悬停)里出现,不进菜单气泡:
|
|
7727
|
-
// 这句 60+ 字,菜单 min-width 只有
|
|
8482
|
+
// 这句 60+ 字,菜单 min-width 只有 128px,塞进气泡会折成多行把菜单撑高。
|
|
7728
8483
|
// 成功态本来就有两处可见反馈——专家 chip(名字 + ×)与菜单项的「已挂」徽章。
|
|
7729
8484
|
setAttachNote('');
|
|
7730
8485
|
setTeamHint(res.expertType === 'team'
|
|
@@ -7820,7 +8575,7 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7820
8575
|
onClick: (e) => { e.stopPropagation(); setOpen((value) => !value); setHoverKind(null); },
|
|
7821
8576
|
}, h(Icon, { d: ICONS.board, size: 15 })),
|
|
7822
8577
|
open
|
|
7823
|
-
? h('div', { className: 'dsh-mcp-quick-menu', role: 'menu', 'aria-label': t('快速调用'), ref: menuRef, style: menuPos },
|
|
8578
|
+
? h('div', { className: 'dsh-mcp-quick-menu', role: 'menu', 'aria-label': t('快速调用'), ref: menuRef, style: menuPos || { position: 'fixed', visibility: 'hidden' } },
|
|
7824
8579
|
// 只报「已复制」,不复述内容:MCP 那条 mention 有 60+ 字,拼进来会折行撑高菜单
|
|
7825
8580
|
// (OPT-32)。内容已在剪贴板,用户直接粘贴即可;完整文本留作 title 悬停可查。
|
|
7826
8581
|
copied ? h('div', { className: 'dsh-mcp-quick-note', title: copied }, t('已复制,粘贴到输入框')) : null,
|
|
@@ -7842,10 +8597,10 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7842
8597
|
const hasAny = skillRows.length > 0 || suites.length > 0;
|
|
7843
8598
|
return h('div', {
|
|
7844
8599
|
key: kind.key,
|
|
7845
|
-
className: 'dsh-mcp-quick-row' + (expanded && flipKinds.has(kind.key) ? ' flip' : ''),
|
|
8600
|
+
className: 'dsh-mcp-quick-row' + (expanded && flipKinds.has(kind.key) ? ' flip' : '') + (expanded && hflipKinds.has(kind.key) ? ' hflip' : ''),
|
|
7846
8601
|
ref: (el) => { if (el && expanded) rowRefs.current.set(kind.key, el); else rowRefs.current.delete(kind.key); },
|
|
7847
8602
|
// 悬停开合由根节点统一管理(见 rootHoverProps):行内只负责展开子菜单。
|
|
7848
|
-
onMouseEnter: () => { cancelClose(); setHoverKind(kind.key); },
|
|
8603
|
+
onMouseEnter: (e) => { cancelClose(); markSubmenuSide(kind.key, e.currentTarget); setHoverKind(kind.key); },
|
|
7849
8604
|
},
|
|
7850
8605
|
h('button', {
|
|
7851
8606
|
type: 'button',
|
|
@@ -7856,7 +8611,13 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7856
8611
|
// 分类行点击 = 只切换子菜单(绝不跳板)。跳页是上一版缺陷的根因:
|
|
7857
8612
|
// WorkBuddy 式交互里悬停已展开子菜单,点击再跳管理页属意外导航。
|
|
7858
8613
|
// 键盘/触屏路径:Enter/点按切换子菜单;管理入口在子菜单底部明确按钮。
|
|
7859
|
-
onClick: (e) => {
|
|
8614
|
+
onClick: (e) => {
|
|
8615
|
+
e.stopPropagation(); cancelClose();
|
|
8616
|
+
const willOpen = hoverKind !== kind.key;
|
|
8617
|
+
// 点按 / 键盘路径也要先定方向:贴右缘时先向右画一帧会整块跑到屏幕外。
|
|
8618
|
+
if (willOpen) markSubmenuSide(kind.key, e.currentTarget.closest('.dsh-mcp-quick-row'));
|
|
8619
|
+
setHoverKind(willOpen ? kind.key : null);
|
|
8620
|
+
},
|
|
7860
8621
|
title: kind.hint,
|
|
7861
8622
|
},
|
|
7862
8623
|
h(Icon, { d: ICONS[kind.icon], size: 13 }),
|
|
@@ -7914,7 +8675,7 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
7914
8675
|
title: t('打开看板的「') + kind.label + t('」分类管理'),
|
|
7915
8676
|
onClick: (e) => { e.stopPropagation(); openBoardCategory(kind.key === 'expert' ? 'expert' : kind.key); },
|
|
7916
8677
|
},
|
|
7917
|
-
h('span', { className: 'dsh-mcp-quick-item-name' }, t('管理'
|
|
8678
|
+
h('span', { className: 'dsh-mcp-quick-item-name' }, t('管理{0}…', [kind.label])),
|
|
7918
8679
|
),
|
|
7919
8680
|
// 套件管理入口只在技能子菜单里给(套件就是技能的组合,放在别处会找不到语境)。
|
|
7920
8681
|
// 走 openSuiteManager():它广播 + 切到 skill 看板,弹窗由 SkillBoard 那边挂出来——
|
|
@@ -8063,9 +8824,1031 @@ const wbFormatDate = (ms) => {
|
|
|
8063
8824
|
if (!ms) return '';
|
|
8064
8825
|
try { return new Date(Number(ms)).toISOString().slice(0, 10); } catch { return ''; }
|
|
8065
8826
|
};
|
|
8066
|
-
|
|
8827
|
+
|
|
8828
|
+
// ── 社区「人味」信号:真实的人、真实的维护节奏、真实的反馈出口 ────────────────
|
|
8829
|
+
// 市场目录里的条目不该只是「一个可安装的对象」:谁做的、最后一次动是什么时候、
|
|
8830
|
+
// 项目还活着吗、遇到问题该找谁。这些信号全部来自上游既有字段或 GitHub 公开 API,
|
|
8831
|
+
// 不生成任何虚构的作者 / 评价 / 在线人数。
|
|
8832
|
+
const ARMORY_REPO_URL = 'https://github.com/2026heshao/dsh-armory';
|
|
8833
|
+
const parseGithubRepoKey = (value) => {
|
|
8834
|
+
const text = String(value || '').trim();
|
|
8835
|
+
if (!text) return '';
|
|
8836
|
+
const ssh = /^git@github\.com:([^/\s]+)\/([^/\s#]+?)(?:\.git)?$/i.exec(text);
|
|
8837
|
+
if (ssh) return `${ssh[1]}/${ssh[2]}`;
|
|
8838
|
+
const http = /^(?:git\+)?https?:\/\/(?:www\.)?github\.com\/([^/\s#?]+)\/([^/\s#?]+)/i.exec(text);
|
|
8839
|
+
if (http) return `${http[1]}/${http[2].replace(/\.git$/i, '')}`;
|
|
8840
|
+
const bare = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(text);
|
|
8841
|
+
if (bare) return `${bare[1]}/${bare[2].replace(/\.git$/i, '')}`;
|
|
8842
|
+
return '';
|
|
8843
|
+
};
|
|
8844
|
+
// ── 头像取图通道:GitHub 系一律走镜像代理 ───────────────────────────────────
|
|
8845
|
+
// 三条候选通道都在本机实测过(2026-10-24,真实 owner 名单取自 awesome 目录 4203 个):
|
|
8846
|
+
// · 直连 `avatars.githubusercontent.com/<login>`:单发 0.35s 很快,但 6 并发就有
|
|
8847
|
+
// 一半挂到 12 秒;更不能接受的是它对不存在的账号回 **200 + 一张占位图**,而且
|
|
8848
|
+
// 大小写敏感(`perrylink` 失败、`PERRYLINK` 成功)。目录里的 owner 写法五花八门,
|
|
8849
|
+
// 这条会把「头像取不到」变成「显示成别人的脸」,比字母更糟。
|
|
8850
|
+
// · `api.github.com/users/<login>` 换 canonical `/u/<id>`:0.67s 能成,但未认证配额
|
|
8851
|
+
// 只有 60 次/小时(实测 X-RateLimit-Remaining 剩 38),而首页 50 张卡就有 41 个
|
|
8852
|
+
// 不同 owner —— 看完一页配额见底,后面几页全退回字母。
|
|
8853
|
+
// · 镜像代理 `wsrv.nl` 去取 `https://github.com/<login>.png`(重定向由代理在服务端跟,
|
|
8854
|
+
// 本机的挂起路由根本不参与):12 并发全部 200,0.56–2.83s、中位约 1.1s,
|
|
8855
|
+
// 单张 webp 0.3–2.7KB,响应头 `Cache-Control: public, max-age=31536000`
|
|
8856
|
+
// (浏览器自己缓存一年),账号不存在干净回 404 → 落回字母,且**一次都不花配额**。
|
|
8857
|
+
// README 插图早就走同一条代理(`README_IMG_PROXY` / `readmeProxiedSrc`),头像沿用
|
|
8858
|
+
// 同一个取舍。代价说清楚:代理是第三方服务,它会知道面板这一屏渲染了哪些 GitHub
|
|
8859
|
+
// 用户名——面板已经把这批名字发给同一个代理处理 README 图,所以不是新增的暴露面。
|
|
8860
|
+
// 也**不做**「代理失败再回退直连 github.com」:直连那条源要 10 秒级(见下面 avatarRecord
|
|
8861
|
+
// 的实测注释),为一张头像把请求发回那条挂起的路上不值得。
|
|
8862
|
+
//
|
|
8863
|
+
// 但「首选代理挂了怎么办」要有答案。这里照 dshmarket 的 `githubRouteCandidates` /
|
|
8864
|
+
// `OwnerAvatar` 做成**有序路由**:第一条 onError/超时就直接换下一条,全部用完才落字母。
|
|
8865
|
+
// · 首选 `wsrv.nl`,备胎 `images.weserv.nl` —— 实测两者是**同一个后端的不同域名**
|
|
8866
|
+
// (同一份图、同样 200、同样一年强缓存),差别只在浏览器把它们当两个 host、
|
|
8867
|
+
// 各有约 6 条 HTTP/1.1 连接。所以备胎不只是「挂了再试」,也躲得开主域名被挤爆
|
|
8868
|
+
// 的那段时间(本机测过:12 并发走 wsrv 全部 200,但 images.weserv 单张更慢,
|
|
8869
|
+
// 所以它只配当第二条,不配当首选)。
|
|
8870
|
+
// · 另两条常见加速域名对头像地址是坏的:`gh-proxy.com` 回 404、`ghfast.top` 回 403
|
|
8871
|
+
// (都实测过),别再往表里加。
|
|
8872
|
+
const AVATAR_PROXY_BASES = ['https://wsrv.nl/?output=webp&q=80', 'https://images.weserv.nl/?output=webp&q=80'];
|
|
8873
|
+
const AVATAR_PROXY_HOSTS = ['github.com', 'avatars.githubusercontent.com', 'camo.githubusercontent.com'];
|
|
8874
|
+
|
|
8875
|
+
/** 该不该交给代理取:只有 GitHub 系域名需要(本机跟不动它的重定向),别的域名原样。 */
|
|
8876
|
+
const avatarProxyHost = (text) => {
|
|
8877
|
+
try {
|
|
8878
|
+
return AVATAR_PROXY_HOSTS.indexOf(new URL(text).hostname.toLowerCase()) >= 0;
|
|
8879
|
+
} catch {
|
|
8880
|
+
return false;
|
|
8881
|
+
}
|
|
8882
|
+
};
|
|
8883
|
+
|
|
8884
|
+
/**
|
|
8885
|
+
* 头像地址 → **有序候选路由**(数组长度就是允许的尝试次数)。
|
|
8886
|
+
* · 已经是本代理族某条路由的地址:保留它自带的 `w`/`h`,只把域名依次换成族里每一条。
|
|
8887
|
+
* 这样「卡片算出来的地址」和「这里再算一遍」永远是同一个字符串(合并的前提),
|
|
8888
|
+
* 也不会套出第二层代理 —— 套两层就把图取坏了。
|
|
8889
|
+
* · GitHub 系源地址:按 `size`(CSS 像素,夹在 32~192,按 2 倍图取)拼出每一条路由。
|
|
8890
|
+
* · 其他域名 / 脏串:原样一条,交给浏览器自己取,取不到就是字母。
|
|
8891
|
+
* · 空输入返回**空数组**,调用方靠它判「这一位压根没图」。
|
|
8892
|
+
*/
|
|
8893
|
+
const avatarRouteCandidates = (src, size = 64) => {
|
|
8894
|
+
const text = String(src || '').trim();
|
|
8895
|
+
if (!text) return [];
|
|
8896
|
+
const wrapped = AVATAR_PROXY_BASES.findIndex((base) => text.startsWith(base));
|
|
8897
|
+
if (wrapped >= 0) {
|
|
8898
|
+
const tail = text.slice(AVATAR_PROXY_BASES[wrapped].length);
|
|
8899
|
+
return AVATAR_PROXY_BASES.map((base) => `${base}${tail}`);
|
|
8900
|
+
}
|
|
8901
|
+
if (!avatarProxyHost(text)) return [text];
|
|
8902
|
+
const side = Math.max(32, Math.min(192, Math.round(Number(size) || 64)));
|
|
8903
|
+
return AVATAR_PROXY_BASES.map((base) => `${base}&w=${side}&h=${side}&url=${encodeURIComponent(text)}`);
|
|
8904
|
+
};
|
|
8905
|
+
/** 首选路由 = 登记表里的键 = 界面上最常见的那个 src;再过一道也不变(幂等)。 */
|
|
8906
|
+
const avatarProxyUrl = (src, size = 64) => avatarRouteCandidates(src, size)[0] || '';
|
|
8907
|
+
const githubAvatarUrl = (repo, size = 64) => {
|
|
8908
|
+
const key = parseGithubRepoKey(repo);
|
|
8909
|
+
return key ? avatarProxyUrl(`https://github.com/${key.split('/')[0]}.png`, size) : '';
|
|
8910
|
+
};
|
|
8911
|
+
// ── 头像:首帧字母,图片探到了再换 ───────────────────────────────────────────
|
|
8912
|
+
//
|
|
8913
|
+
// 为什么不能再把 src 直接交给 <img>(实测,2026-10-24):
|
|
8914
|
+
// · 全部卡片头像的**源**都是 `https://github.com/<login>.png`(awesome 的 slim.json 里
|
|
8915
|
+
// 根本没有 icon/avatar 字段,插件目录也一样);现在这个源只作为代理的 `url=` 参数出现,
|
|
8916
|
+
// 面板自己不再直连 github.com(通道选型见上面 avatarProxyUrl);
|
|
8917
|
+
// · 首页 50 张卡 = 100 个 <img>(卡片一个、它下面那行 PresenceLine 又一个),
|
|
8918
|
+
// 而这 50 条只有 41 个不同 owner —— 一半请求是重复的;
|
|
8919
|
+
// · 直连那条源时,github.com 的 302 回得很快(0.76s),慢在重定向落点:实测跟完整重定向链
|
|
8920
|
+
// 要 14.7 秒才拿到字节,单发 10.7 秒抛 "fetch failed"。浏览器在**报错之前**一直算
|
|
8921
|
+
// 「加载中」,Chromium 对单个 host 又只有约 6 条 HTTP/1.1 连接,100 个请求排队,于是
|
|
8922
|
+
// 整页都在等 10 秒级的挂起;这段时间 CSS 给 <img> 画的 background + border 就是用户
|
|
8923
|
+
// 看到的空灰圈,`onError` 里的 visibility:hidden 根本没机会触发。
|
|
8924
|
+
// 所以这里把「发请求」和「显示」拆开:
|
|
8925
|
+
// 1. 首帧永远渲染字母占位(.fallback),不留空圈 —— 慢的时候也得有内容;
|
|
8926
|
+
// 2. 后台用 new Image() 探一次,AVATAR_TIMEOUT_MS 内成功才换成 <img>
|
|
8927
|
+
// (此时字节已在浏览器缓存里,绘制是即时的);首选代理不成换下一条路由,
|
|
8928
|
+
// 全部用完才判死(见上面的 AVATAR_PROXY_BASES);
|
|
8929
|
+
// 3. 超时或报错就**粘住**记进模块级登记表:同一个 URL 本次会话不再发第二次,
|
|
8930
|
+
// 再进市场这些头像一次请求都不发(粘住的是请求,不是显示——迟到的成功仍然采纳,
|
|
8931
|
+
// 因为「慢」不等于「坏」,而且不额外花一次网络);
|
|
8932
|
+
// 4. 卡片行与 PresenceLine 传的是同一个 URL 字符串,共用同一条登记 →
|
|
8933
|
+
// 一次挂载发两遍同样的请求,自动合并成一遍;
|
|
8934
|
+
// 5. 探测只在「这个头像离视口 200px 以内」时才发起(useNearViewport,抄 dshmarket
|
|
8935
|
+
// 的卡片图口径)—— 视口外的那几十个 owner 根本进不了登记表。
|
|
8936
|
+
// 代价说清楚:每个 owner 至多 AVATAR_PROXY_BASES.length 次请求(一条代理 + 一条备胎),
|
|
8937
|
+
// 全挂之后归零,本次会话不再探;面板列表滚动时视口外的卡片根本进不了登记(见下面
|
|
8938
|
+
// useNearViewport),所以总量还能从 41 再往下压。
|
|
8939
|
+
//
|
|
8940
|
+
// 还要再加一层**并发闸门**(同样是本机实测,不是猜的):wsrv.nl 在这台机上只给
|
|
8941
|
+
// HTTP/1.1(curl 实测 ver=1.1),Chromium 每域约 6 条连接,而代理单张串行
|
|
8942
|
+
// 0.58–2.46s、中位 1.7s;一屏就有 41~57 个不同 owner,浏览器里 57 并发实测
|
|
8943
|
+
// 「1.5 秒内只上屏 3 张、中位 4825ms、9 秒后仍有 30 张在途」—— 全并发打出去等于谁都要
|
|
8944
|
+
// 等 9 秒。更要紧的是:浏览器的 socket 队列排在**上网之前**,而 `new Image()` 的超时
|
|
8945
|
+
// 是从调用起算的,全并发时第 30 张其实还没发出就被我们的 4 秒判了死,而且是**粘住**的死
|
|
8946
|
+
// (本次会话不再重试)—— 把「慢」误杀成「坏」。所以:
|
|
8947
|
+
// · 同时在途的头像请求不超过 AVATAR_INFLIGHT_MAX 条,其余排队(FIFO,入队顺序 =
|
|
8948
|
+
// 挂载顺序 = 文档顺序,也就是从上往下:用户先看到的那几张先拿到带宽);
|
|
8949
|
+
// · 超时从**真正发出请求**那一刻起算,不是从挂载起算 —— 排在队里等名额不算它慢;
|
|
8950
|
+
// · 只取 3 条而不是把 6 条连接占满:README 插图走的是**同一个 wsrv.nl 域名**,
|
|
8951
|
+
// 打开 README 弹层时不能把它的额度挤死。
|
|
8952
|
+
const AVATAR_TIMEOUT_MS = 4000;
|
|
8953
|
+
const AVATAR_REGISTRY_MAX = 300;
|
|
8954
|
+
const AVATAR_INFLIGHT_MAX = 3;
|
|
8955
|
+
/** 首选路由地址 → { status, waiters, timer, dispatched, released, routes, route, resolved } */
|
|
8956
|
+
const avatarRegistry = new Map();
|
|
8957
|
+
/** 已登记、还没轮到发出的 URL(按挂载顺序) */
|
|
8958
|
+
const avatarQueue = [];
|
|
8959
|
+
let avatarInflight = 0;
|
|
8960
|
+
let avatarDraining = false;
|
|
8961
|
+
|
|
8962
|
+
const avatarSettle = (rec, next) => {
|
|
8963
|
+
if (rec.status === 'ok') return;
|
|
8964
|
+
if (rec.status === 'fail' && next !== 'ok') return;
|
|
8965
|
+
rec.status = next;
|
|
8966
|
+
for (const notify of rec.waiters) notify();
|
|
8967
|
+
};
|
|
8968
|
+
|
|
8969
|
+
const avatarClearTimer = (rec) => {
|
|
8970
|
+
if (rec.timer) {
|
|
8971
|
+
clearTimeout(rec.timer);
|
|
8972
|
+
rec.timer = 0;
|
|
8973
|
+
}
|
|
8974
|
+
};
|
|
8975
|
+
|
|
8976
|
+
const avatarSucceeded = (rec, url) => {
|
|
8977
|
+
avatarClearTimer(rec);
|
|
8978
|
+
// resolved 记住**成功那条路由的地址**:备胎出图时界面上要画备胎那条,不能画回主那条。
|
|
8979
|
+
rec.resolved = url;
|
|
8980
|
+
avatarFinish(rec, 'ok');
|
|
8981
|
+
};
|
|
8982
|
+
|
|
8983
|
+
/**
|
|
8984
|
+
* 一条路由失败(onerror 或超时):还有备胎就换下一条,**继续占着同一个在途名额**
|
|
8985
|
+
* —— 名额代表「这个 owner 还没结算」,不是一次 HTTP;备胎用尽才判死并释放名额。
|
|
8986
|
+
* 换路由后重新起表:主域名挂满 4 秒不该把备域名那 4 秒也吃掉。
|
|
8987
|
+
*/
|
|
8988
|
+
const avatarAttemptFailed = (rec) => {
|
|
8989
|
+
avatarClearTimer(rec);
|
|
8990
|
+
if (rec.route + 1 < rec.routes.length) {
|
|
8991
|
+
rec.route += 1;
|
|
8992
|
+
avatarDispatch(rec);
|
|
8993
|
+
return;
|
|
8994
|
+
}
|
|
8995
|
+
avatarFinish(rec, 'fail');
|
|
8996
|
+
};
|
|
8997
|
+
|
|
8998
|
+
const avatarDispatch = (rec) => {
|
|
8999
|
+
if (!rec.dispatched) {
|
|
9000
|
+
rec.dispatched = true;
|
|
9001
|
+
avatarInflight += 1;
|
|
9002
|
+
}
|
|
9003
|
+
const url = rec.routes[rec.route];
|
|
9004
|
+
// 计时起点在这里,不在 avatarRecord:排队时间不该计入超时(见上面的并发闸门说明)。
|
|
9005
|
+
rec.timer = setTimeout(() => {
|
|
9006
|
+
rec.timer = 0;
|
|
9007
|
+
avatarAttemptFailed(rec);
|
|
9008
|
+
}, AVATAR_TIMEOUT_MS);
|
|
9009
|
+
try {
|
|
9010
|
+
const image = new Image();
|
|
9011
|
+
image.referrerPolicy = 'no-referrer';
|
|
9012
|
+
image.onload = () => avatarSucceeded(rec, url);
|
|
9013
|
+
image.onerror = () => avatarAttemptFailed(rec);
|
|
9014
|
+
image.src = url;
|
|
9015
|
+
} catch {
|
|
9016
|
+
// 拿不到 Image 构造器(非浏览器宿主 / 内存受限):走完尝试后判失败,界面停在字母上。
|
|
9017
|
+
avatarAttemptFailed(rec);
|
|
9018
|
+
}
|
|
9019
|
+
};
|
|
9020
|
+
|
|
9021
|
+
/** 腾出一个在途名额并立刻补队。每条只允许释放一次,否则计数会跑负、闸门形同虚设。 */
|
|
9022
|
+
const avatarRelease = (rec) => {
|
|
9023
|
+
if (!rec.dispatched || rec.released) return;
|
|
9024
|
+
rec.released = true;
|
|
9025
|
+
avatarInflight -= 1;
|
|
9026
|
+
avatarDrain();
|
|
9027
|
+
};
|
|
9028
|
+
|
|
9029
|
+
const avatarFinish = (rec, next) => {
|
|
9030
|
+
avatarClearTimer(rec);
|
|
9031
|
+
avatarSettle(rec, next);
|
|
9032
|
+
avatarRelease(rec);
|
|
9033
|
+
};
|
|
9034
|
+
|
|
9035
|
+
const avatarDrain = () => {
|
|
9036
|
+
// 重入保护:同步判死那条路径(Image 构造抛异常)会在 dispatch 里再调一次 drain,
|
|
9037
|
+
// 嵌套调用会绕开 while 的计数检查、把在途数打出上限。外层循环每轮都重读计数,
|
|
9038
|
+
// 所以让嵌套那次直接返回、由外层收摊,不会漏发。
|
|
9039
|
+
if (avatarDraining) return;
|
|
9040
|
+
avatarDraining = true;
|
|
9041
|
+
try {
|
|
9042
|
+
while (avatarInflight < AVATAR_INFLIGHT_MAX && avatarQueue.length) {
|
|
9043
|
+
const url = avatarQueue.shift();
|
|
9044
|
+
const rec = avatarRegistry.get(url);
|
|
9045
|
+
// 队头跳过:这条已被别的入口结算,或登记表已因溢出把它换掉了(绝不重复发)。
|
|
9046
|
+
if (!rec || rec.status !== 'loading' || rec.dispatched) continue;
|
|
9047
|
+
avatarDispatch(rec);
|
|
9048
|
+
}
|
|
9049
|
+
} finally {
|
|
9050
|
+
avatarDraining = false;
|
|
9051
|
+
}
|
|
9052
|
+
};
|
|
9053
|
+
|
|
9054
|
+
/**
|
|
9055
|
+
* 取某头像的加载登记,键是**首选路由地址**(卡片与 PresenceLine 算出同一个字符串,
|
|
9056
|
+
* 所以自动共用同一条登记)。只有第一次建立时才入队发请求。
|
|
9057
|
+
* 组件只在 useEffect 里、且只在「离视口够近」时调用,所以首帧一定先出字母。
|
|
9058
|
+
*/
|
|
9059
|
+
const avatarRecord = (routes) => {
|
|
9060
|
+
const key = Array.isArray(routes) ? routes[0] : '';
|
|
9061
|
+
if (!key) return { status: 'fail', waiters: new Set(), timer: 0, dispatched: false, released: true, route: 0, routes: [], resolved: '' };
|
|
9062
|
+
const existing = avatarRegistry.get(key);
|
|
9063
|
+
if (existing) return existing;
|
|
9064
|
+
if (avatarRegistry.size >= AVATAR_REGISTRY_MAX) {
|
|
9065
|
+
// 先按插入序腾出一条**已结算**的。绝不优先杀在途或还在排队的:它们的回调 / 队位还认着
|
|
9066
|
+
// 这条记录,杀了之后同 URL 会被重建、再发一遍 —— 正好违反本层唯一的硬约束
|
|
9067
|
+
// (每个 URL 至多一次)。若一条都腾不出来,宁可不再探,也不让登记表随刷过的页数无限长。
|
|
9068
|
+
let freed = false;
|
|
9069
|
+
for (const [registered, rec] of avatarRegistry) {
|
|
9070
|
+
if (rec.status !== 'loading') {
|
|
9071
|
+
avatarRegistry.delete(registered);
|
|
9072
|
+
freed = true;
|
|
9073
|
+
break;
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
// 溢出条:dispatched=false + released=true,让它永远不碰在途计数、也不入队。
|
|
9077
|
+
if (!freed) return { status: 'fail', waiters: new Set(), timer: 0, dispatched: false, released: true, route: 0, routes, resolved: '', overflow: true };
|
|
9078
|
+
}
|
|
9079
|
+
const rec = { status: 'loading', waiters: new Set(), timer: 0, dispatched: false, released: false, routes, route: 0, resolved: '' };
|
|
9080
|
+
avatarRegistry.set(key, rec);
|
|
9081
|
+
avatarQueue.push(key);
|
|
9082
|
+
avatarDrain();
|
|
9083
|
+
return rec;
|
|
9084
|
+
};
|
|
9085
|
+
|
|
9086
|
+
/**
|
|
9087
|
+
* 「这一个头像离视口近不近」—— 照 dshmarket 的 `useNearViewport` / `CardShot` 的口径:
|
|
9088
|
+
* **没靠近视口就不发请求**(`rootMargin` 提前 200px,滚到之前就备好)。这条比并发闸门
|
|
9089
|
+
* 更根本:闸门只是排队,视口门是**根本不排队**——一屏十几张卡之外的那几十个 owner
|
|
9090
|
+
* 一次都不请求;面板切走、容器隐藏时同样一次都不请求。
|
|
9091
|
+
* 宿主没有 IntersectionObserver(老 Webview、非浏览器环境)时初值就是 `true`:
|
|
9092
|
+
* 宁可退回「全都探」,也绝不能因为观察不到而让头像永远停在字母上。
|
|
9093
|
+
* 节点走 `useRef` 而不是 `useState`:effect 在提交之后运行,那时 ref 已经挂上真实 DOM,
|
|
9094
|
+
* 不必为了「拿到节点」再多渲染一次(dshmarket 用 state,代价是每个头像挂载都多一趟重绘)。
|
|
9095
|
+
*/
|
|
9096
|
+
const AVATAR_VIEWPORT_MARGIN = '200px';
|
|
9097
|
+
const useNearViewport = (rootMargin = AVATAR_VIEWPORT_MARGIN) => {
|
|
9098
|
+
const [near, setNear] = useState(typeof IntersectionObserver === 'undefined');
|
|
9099
|
+
const nodeRef = useRef(null);
|
|
9100
|
+
useEffect(() => {
|
|
9101
|
+
if (near || nodeRef.current === null) return undefined;
|
|
9102
|
+
let observer = null;
|
|
9103
|
+
try {
|
|
9104
|
+
observer = new IntersectionObserver((entries) => {
|
|
9105
|
+
if (entries.some((entry) => entry && entry.isIntersecting)) setNear(true);
|
|
9106
|
+
}, { rootMargin });
|
|
9107
|
+
observer.observe(nodeRef.current);
|
|
9108
|
+
} catch {
|
|
9109
|
+
// 构造 / 挂载失败(老内核不认 rootMargin 这类参数):当作已在视口,别把头像锁死。
|
|
9110
|
+
setNear(true);
|
|
9111
|
+
}
|
|
9112
|
+
return () => {
|
|
9113
|
+
if (observer) observer.disconnect();
|
|
9114
|
+
};
|
|
9115
|
+
}, [near, rootMargin]);
|
|
9116
|
+
return [nodeRef, near];
|
|
9117
|
+
};
|
|
9118
|
+
|
|
9119
|
+
/**
|
|
9120
|
+
* 统一的头像位。`url` 空、还没进视口范围、或还没探到 → 字母;探到了 → <img>。
|
|
9121
|
+
* 地址在这一层先过一道 `avatarRouteCandidates`:GitHub 系换成代理小图(包括 wbRepoMeta
|
|
9122
|
+
* 给的 canonical `/u/<id>?v=4`,那是原图尺寸、直连又慢),别的域名原样透传。归一化收在
|
|
9123
|
+
* 这层而不是散到各调用点,十个头像位才不会有人漏掉。默认尺寸:卡片和它下面那行
|
|
9124
|
+
* PresenceLine 传的是同一个 owner,只有两边算出**同一个字符串**才合并成一次请求。
|
|
9125
|
+
* 状态用 `ready = { key, src }` 而不是布尔:切换详情时 key 变了但布尔还留着 true,
|
|
9126
|
+
* 会有一帧拿旧状态去绘新 src,这里靠「ready.key !== 当前 key 即回落字母」挡掉;
|
|
9127
|
+
* src 单独记是因为出图的可能不是首选路由(备胎),要画就画真正成功的那条地址。
|
|
9128
|
+
* hook 一律声明在组件顶层,条件只控制渲染、不控制声明(本仓库的 React #310 规矩)。
|
|
9129
|
+
*/
|
|
9130
|
+
const Avatar = ({ url, name, className }) => {
|
|
9131
|
+
const [nodeRef, near] = useNearViewport();
|
|
9132
|
+
const routes = avatarRouteCandidates(url);
|
|
9133
|
+
const key = routes[0] || '';
|
|
9134
|
+
const letter = String(name || '?').slice(0, 1).toUpperCase() || '?';
|
|
9135
|
+
const [ready, setReady] = useState(null);
|
|
9136
|
+
useEffect(() => {
|
|
9137
|
+
if (!key || !near) return undefined;
|
|
9138
|
+
const rec = avatarRecord(routes);
|
|
9139
|
+
if (rec.status === 'ok') {
|
|
9140
|
+
setReady({ key, src: rec.resolved || key });
|
|
9141
|
+
return undefined;
|
|
9142
|
+
}
|
|
9143
|
+
const done = () => {
|
|
9144
|
+
if (rec.status === 'ok') setReady({ key, src: rec.resolved || key });
|
|
9145
|
+
else if (rec.status === 'fail') setReady(null);
|
|
9146
|
+
};
|
|
9147
|
+
rec.waiters.add(done);
|
|
9148
|
+
return () => {
|
|
9149
|
+
rec.waiters.delete(done);
|
|
9150
|
+
};
|
|
9151
|
+
}, [key, near]);
|
|
9152
|
+
if (!key || !near || !ready || ready.key !== key) {
|
|
9153
|
+
return h('span', { ref: nodeRef, className: `${className} fallback`, 'aria-hidden': true }, letter);
|
|
9154
|
+
}
|
|
9155
|
+
return h('img', {
|
|
9156
|
+
ref: nodeRef,
|
|
9157
|
+
className,
|
|
9158
|
+
src: ready.src,
|
|
9159
|
+
alt: '',
|
|
9160
|
+
// 字节已经在浏览器缓存里(探到了才画这张图),所以**不写** `loading="lazy"`:
|
|
9161
|
+
// 那等于对一张必定要画的图说「晚点再取」,是自相矛盾的写法。
|
|
9162
|
+
// decoding / fetchPriority 是给少见情况兜底:缓存被挤掉、浏览器真要回源时,
|
|
9163
|
+
// 别抢别的请求的优先级、也别同步解码卡住主线程(一次可能有好几张同时换成图)。
|
|
9164
|
+
decoding: 'async',
|
|
9165
|
+
fetchPriority: 'low',
|
|
9166
|
+
referrerPolicy: 'no-referrer',
|
|
9167
|
+
// 缓存被挤掉、图片中途失效这类少见情况:退回字母,不留破图。
|
|
9168
|
+
onError: () => setReady(null),
|
|
9169
|
+
});
|
|
9170
|
+
};
|
|
9171
|
+
const timestampOf = (value) => {
|
|
9172
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) return value;
|
|
9173
|
+
const parsed = Date.parse(String(value || ''));
|
|
9174
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
9175
|
+
};
|
|
9176
|
+
const relativeLabel = (value) => {
|
|
9177
|
+
const at = timestampOf(value);
|
|
9178
|
+
return at ? fmtLast(at) : '';
|
|
9179
|
+
};
|
|
9180
|
+
const communityStatus = (meta) => {
|
|
9181
|
+
if (!meta) return null;
|
|
9182
|
+
if (meta.archived === true) return { cls: 's-warn dot', label: t('已归档') };
|
|
9183
|
+
const at = timestampOf(meta.pushedAt || meta.updatedAt);
|
|
9184
|
+
if (!at) return null;
|
|
9185
|
+
const days = Math.floor((Date.now() - at) / 86_400_000);
|
|
9186
|
+
if (days <= 30) return { cls: 's-on dot', label: t('近 30 天有提交') };
|
|
9187
|
+
if (days <= 180) return { cls: '', label: t('最近更新 {0}', [relativeLabel(at)]) };
|
|
9188
|
+
return { cls: 's-warn dot', label: t('超过半年未更新') };
|
|
9189
|
+
};
|
|
9190
|
+
// 卡片级的轻量「人」行:不发网络请求,只用目录里已经带回来的发布方 / 头像 / 时间。
|
|
9191
|
+
const PresenceLine = ({ owner, avatar, updatedAt, href, note, className }) => {
|
|
9192
|
+
const at = timestampOf(updatedAt);
|
|
9193
|
+
const meta = at ? t('更新于 {0}', [relativeLabel(at)]) : (note || '');
|
|
9194
|
+
const name = String(owner || '').trim();
|
|
9195
|
+
if (!name && !meta) return null;
|
|
9196
|
+
return h('div', { className: 'dsh-mcp-presence-inline' + (className ? ' ' + className : '') },
|
|
9197
|
+
// 头像与发布方都没有时仍然什么都不画(这条分支是 PresenceLine 的既有契约);
|
|
9198
|
+
// 只要有其一,就交给 Avatar:url 为空它会直接出字母,和原来「没图就字母」一致,
|
|
9199
|
+
// 而 url 非空时首帧也是字母,直到后台探到图片才换 —— 这正是合并请求后不重复发的那条路。
|
|
9200
|
+
avatar || name
|
|
9201
|
+
? h(Avatar, { url: avatar, name, className: 'dsh-mcp-presence-inline-avatar' })
|
|
9202
|
+
: null,
|
|
9203
|
+
name
|
|
9204
|
+
? h('span', { className: 'dsh-mcp-presence-inline-owner' },
|
|
9205
|
+
href ? h('a', { href, target: '_blank', rel: 'noreferrer' }, name) : name)
|
|
9206
|
+
: null,
|
|
9207
|
+
meta ? h('span', { className: 'dsh-mcp-presence-inline-note' }, meta) : null,
|
|
9208
|
+
);
|
|
9209
|
+
};
|
|
9210
|
+
// ── 市场连接器卡片头像 ──────────────────────────────────────────────────
|
|
9211
|
+
// 连接器没有 GitHub 仓库可考,图标本体在官方分发包 ZIP 的 icons/<id>.<ext> 里,
|
|
9212
|
+
// 宿主按需出 data URL(wbConnectorIcon)。与 Avatar(远程 URL 探活)分开:
|
|
9213
|
+
// data URL 不走网络,占探活闸门的并发名额没有意义;但「进视口才取、取到就缓存」
|
|
9214
|
+
// 的纪律一致 —— 客户端按 id 缓存,卡片、详情弹层、翻页重挂载都只发一次 RPC;
|
|
9215
|
+
// 探过且没有的记 null,同样不再发。RPC 失败不记缓存,下次进视口还能重试。
|
|
9216
|
+
const connectorIconCache = new Map();
|
|
9217
|
+
const connectorIconPending = new Map();
|
|
9218
|
+
const ConnectorAvatar = ({ call, connectorId, name, className }) => {
|
|
9219
|
+
const [nodeRef, near] = useNearViewport();
|
|
9220
|
+
const key = String(connectorId || '').trim();
|
|
9221
|
+
const letter = String(name || key || '?').slice(0, 1).toUpperCase() || '?';
|
|
9222
|
+
const [src, setSrc] = useState(() => connectorIconCache.get(key) || '');
|
|
9223
|
+
useEffect(() => {
|
|
9224
|
+
setSrc(connectorIconCache.get(key) || '');
|
|
9225
|
+
if (!key || !near || connectorIconCache.has(key)) return undefined;
|
|
9226
|
+
let alive = true;
|
|
9227
|
+
let pending = connectorIconPending.get(key);
|
|
9228
|
+
if (!pending) {
|
|
9229
|
+
pending = call('wbConnectorIcon', { connectorId: key })
|
|
9230
|
+
.then((res) => {
|
|
9231
|
+
// 两层 ok:false 语义不同,缓存策略也不同:
|
|
9232
|
+
// · 信封层的 ok:false 带 error(方法 404 / 宿主抛错 / 服务未连接)——是
|
|
9233
|
+
// **暂时**的(宿主还没重载等),不记缓存,下次进视口重试;
|
|
9234
|
+
// · 宿主业务层的 ok:false 不带 error(目录里就是没图标)——是**最终**的,
|
|
9235
|
+
// 记 null,同一会话不再发。
|
|
9236
|
+
if (res?.ok) {
|
|
9237
|
+
const dataUrl = String(res.dataUrl || '');
|
|
9238
|
+
connectorIconCache.set(key, dataUrl || null);
|
|
9239
|
+
return dataUrl;
|
|
9240
|
+
}
|
|
9241
|
+
if (!res?.error) connectorIconCache.set(key, null);
|
|
9242
|
+
return '';
|
|
9243
|
+
})
|
|
9244
|
+
.catch(() => '')
|
|
9245
|
+
.finally(() => { connectorIconPending.delete(key); });
|
|
9246
|
+
connectorIconPending.set(key, pending);
|
|
9247
|
+
}
|
|
9248
|
+
pending.then((dataUrl) => { if (alive && dataUrl) setSrc(dataUrl); });
|
|
9249
|
+
return () => { alive = false; };
|
|
9250
|
+
}, [call, key, near]);
|
|
9251
|
+
if (!src) {
|
|
9252
|
+
return h('span', { ref: nodeRef, className: `${className} fallback`, 'aria-hidden': true }, letter);
|
|
9253
|
+
}
|
|
9254
|
+
return h('img', {
|
|
9255
|
+
ref: nodeRef,
|
|
9256
|
+
className,
|
|
9257
|
+
src,
|
|
9258
|
+
alt: '',
|
|
9259
|
+
decoding: 'async',
|
|
9260
|
+
referrerPolicy: 'no-referrer',
|
|
9261
|
+
// data URL 一般不会中途失效;真坏了退回字母,不留破图。
|
|
9262
|
+
onError: () => setSrc(''),
|
|
9263
|
+
});
|
|
9264
|
+
};
|
|
9265
|
+
// 详情级的维护信号:打开详情时按需查一次 GitHub(宿主侧 6 小时缓存)。
|
|
9266
|
+
// 单条仓库查不到不阻塞详情——只把这一行收掉,正文照常可看。
|
|
9267
|
+
const CommunityPresence = ({ call, repo, owner, avatar, updatedAt, compact }) => {
|
|
9268
|
+
const key = parseGithubRepoKey(repo);
|
|
9269
|
+
const [state, setState] = useState({ loading: Boolean(key), meta: null, error: '' });
|
|
9270
|
+
useEffect(() => {
|
|
9271
|
+
if (!key || typeof call !== 'function') {
|
|
9272
|
+
setState({ loading: false, meta: null, error: '' });
|
|
9273
|
+
return undefined;
|
|
9274
|
+
}
|
|
9275
|
+
let alive = true;
|
|
9276
|
+
setState({ loading: true, meta: null, error: '' });
|
|
9277
|
+
call('wbRepoMeta', { repos: [key] }).then((res) => {
|
|
9278
|
+
if (!alive) return;
|
|
9279
|
+
const row = res && res.repos ? res.repos[key] : null;
|
|
9280
|
+
setState({
|
|
9281
|
+
loading: false,
|
|
9282
|
+
meta: row && row.ok === true ? row : null,
|
|
9283
|
+
error: row && row.ok !== true ? String(row.error || '') : '',
|
|
9284
|
+
});
|
|
9285
|
+
}).catch((error) => {
|
|
9286
|
+
if (alive) setState({ loading: false, meta: null, error: String(error?.message || error || '') });
|
|
9287
|
+
});
|
|
9288
|
+
return () => { alive = false; };
|
|
9289
|
+
}, [call, key]);
|
|
9290
|
+
const meta = state.meta;
|
|
9291
|
+
const ownerName = String(meta?.owner || owner || (key ? key.split('/')[0] : '')).trim();
|
|
9292
|
+
const ownerUrl = meta?.ownerUrl || (key ? `https://github.com/${key.split('/')[0]}` : '');
|
|
9293
|
+
const avatarUrl = meta?.ownerAvatar || avatar || (key ? githubAvatarUrl(key) : '');
|
|
9294
|
+
const status = communityStatus(meta);
|
|
9295
|
+
const pushedAt = timestampOf(meta?.pushedAt);
|
|
9296
|
+
const fromUpstream = (compact ? ' dsh-mcp-presence-compact' : '');
|
|
9297
|
+
return h('div', { className: 'dsh-mcp-presence' + fromUpstream },
|
|
9298
|
+
h(Avatar, { url: avatarUrl, name: ownerName, className: 'dsh-mcp-presence-avatar' }),
|
|
9299
|
+
h('div', { className: 'dsh-mcp-presence-body' },
|
|
9300
|
+
h('div', { className: 'dsh-mcp-presence-line' },
|
|
9301
|
+
ownerName
|
|
9302
|
+
? (ownerUrl
|
|
9303
|
+
? h('a', { className: 'dsh-mcp-presence-owner', href: ownerUrl, target: '_blank', rel: 'noreferrer', title: t('打开维护者主页') }, ownerName)
|
|
9304
|
+
: h('span', { className: 'dsh-mcp-presence-owner' }, ownerName))
|
|
9305
|
+
: h('span', { className: 'dsh-mcp-presence-owner' }, t('来源未标注')),
|
|
9306
|
+
status ? h(Badge, { cls: status.cls }, status.label) : null,
|
|
9307
|
+
state.loading ? h('span', { className: 'dsh-mcp-presence-loading' }, t('读取维护动态…')) : null,
|
|
9308
|
+
),
|
|
9309
|
+
h('div', { className: 'dsh-mcp-presence-meta' },
|
|
9310
|
+
meta
|
|
9311
|
+
? [
|
|
9312
|
+
pushedAt ? h('span', { key: 'pushed' }, t('最近提交 {0}', [relativeLabel(pushedAt)])) : null,
|
|
9313
|
+
h('span', { key: 'issues' }, t('{0} 个开放 issue', [fmtInt(meta.openIssues)])),
|
|
9314
|
+
h('span', { key: 'stars' }, '★ ' + wbFormatCount(meta.stars)),
|
|
9315
|
+
meta.license ? h('span', { key: 'license' }, meta.license) : null,
|
|
9316
|
+
meta.language ? h('span', { key: 'lang' }, meta.language) : null,
|
|
9317
|
+
]
|
|
9318
|
+
: h('span', null,
|
|
9319
|
+
updatedAt
|
|
9320
|
+
? t('更新于 {0}', [relativeLabel(updatedAt)])
|
|
9321
|
+
: state.error
|
|
9322
|
+
? t('维护动态读取失败:{0}', [state.error])
|
|
9323
|
+
: key
|
|
9324
|
+
? t('维护动态读取中…')
|
|
9325
|
+
: t('这个条目没有公开仓库')),
|
|
9326
|
+
),
|
|
9327
|
+
),
|
|
9328
|
+
);
|
|
9329
|
+
};
|
|
9330
|
+
const feedbackKindLabel = (kind) =>
|
|
9331
|
+
kind === 'skill' ? t('技能') : kind === 'expert' ? t('专家') : kind === 'plugin' ? t('插件') : kind === 'connector' ? t('连接器') : t('条目');
|
|
9332
|
+
const feedbackTemplate = (item, kind, repo, homepage) => {
|
|
9333
|
+
const name = String(item?.name || item?.id || item?.plugin || item?.slug || '').trim() || t('未命名条目');
|
|
9334
|
+
const version = String(item?.version || '').trim();
|
|
9335
|
+
const source = String(repo || item?.repo || item?.repository || homepage || item?.homepage || item?.upstreamUrl || '').trim();
|
|
9336
|
+
const env = typeof navigator !== 'undefined' ? String(navigator.userAgent || '') : '';
|
|
9337
|
+
return [
|
|
9338
|
+
'### ' + t('条目:{0}', [name]),
|
|
9339
|
+
'- ' + t('类型:{0}', [feedbackKindLabel(kind)]),
|
|
9340
|
+
source ? '- ' + t('来源:{0}', [source]) : '',
|
|
9341
|
+
version ? '- ' + t('版本:{0}', [version]) : '',
|
|
9342
|
+
'',
|
|
9343
|
+
'### ' + t('发生了什么'),
|
|
9344
|
+
'',
|
|
9345
|
+
'',
|
|
9346
|
+
'### ' + t('环境'),
|
|
9347
|
+
'- ' + t('系统:{0}', [env]),
|
|
9348
|
+
].filter(Boolean).join('\n');
|
|
9349
|
+
};
|
|
9350
|
+
// 真实社区出口:GitHub 仓库 / Issue / 复制模板 / 给军械库提建议。
|
|
9351
|
+
// 刻意不做站内论坛、不做点赞收藏——那些只会把「有没有人」变成一种布景。
|
|
9352
|
+
// 所有链接都在新标签页打开,正文先给用户看,点了才提交。
|
|
9353
|
+
const FeedbackLinks = ({ item, kind, repo, homepage, sourceLabel }) => {
|
|
9354
|
+
const [copied, setCopied] = useState(false);
|
|
9355
|
+
const name = String(item?.name || item?.id || item?.plugin || item?.slug || '').trim() || t('未命名条目');
|
|
9356
|
+
const repoKey = parseGithubRepoKey(repo || item?.repo || item?.repository);
|
|
9357
|
+
const repoUrl = repoKey ? `https://github.com/${repoKey}` : '';
|
|
9358
|
+
const home = String(homepage || item?.homepage || item?.upstreamUrl || '').trim();
|
|
9359
|
+
const homeUrl = /^https?:\/\//i.test(home) && home !== repoUrl ? home : '';
|
|
9360
|
+
const template = feedbackTemplate(item, kind, repoUrl || homeUrl, homeUrl);
|
|
9361
|
+
const title = t('{0} · {1} 的反馈', [feedbackKindLabel(kind), name]);
|
|
9362
|
+
const issueUrl = ARMORY_REPO_URL + '/issues/new?title=' + encodeURIComponent(title) + '&body=' + encodeURIComponent(template);
|
|
9363
|
+
return h('div', { className: 'dsh-mcp-feedback' },
|
|
9364
|
+
h('div', { className: 'dsh-mcp-feedback-head' },
|
|
9365
|
+
h('b', null, t('找到真实的人')),
|
|
9366
|
+
sourceLabel ? h('span', { className: 'dsh-mcp-feedback-source' }, sourceLabel) : null,
|
|
9367
|
+
),
|
|
9368
|
+
h('div', { className: 'dsh-mcp-feedback-actions' },
|
|
9369
|
+
repoUrl ? h('a', { className: 'dsh-mcp-btn', href: repoUrl, target: '_blank', rel: 'noreferrer' }, t('去仓库')) : null,
|
|
9370
|
+
repoUrl ? h('a', { className: 'dsh-mcp-btn', href: repoUrl + '/issues', target: '_blank', rel: 'noreferrer' }, t('看讨论 / 提 Issue')) : null,
|
|
9371
|
+
homeUrl ? h('a', { className: 'dsh-mcp-btn', href: homeUrl, target: '_blank', rel: 'noreferrer' }, t('来源主页')) : null,
|
|
9372
|
+
h('a', { className: 'dsh-mcp-btn', href: issueUrl, target: '_blank', rel: 'noreferrer' }, t('向军械库反馈')),
|
|
9373
|
+
h('button', {
|
|
9374
|
+
type: 'button', className: 'dsh-mcp-btn',
|
|
9375
|
+
onClick: async () => { setCopied(await copyText(template)); },
|
|
9376
|
+
}, copied ? t('已复制模板') : t('复制反馈模板')),
|
|
9377
|
+
),
|
|
9378
|
+
h('p', { className: 'dsh-mcp-feedback-note' },
|
|
9379
|
+
t('反馈会打开 GitHub 新标签页;内容先由你过目,确认后才提交。')),
|
|
9380
|
+
);
|
|
9381
|
+
};
|
|
9382
|
+
|
|
9383
|
+
// ── 顶栏「点 Star」:把用户送到 GitHub 那个真正的 Star 按钮前面 ─────────────
|
|
9384
|
+
// 为什么放在分类栏右端而不是市场页里:这是整个插件的门面位,五个页签都看得到。
|
|
9385
|
+
// 「给项目点个 Star」本来就不该藏在某一个子页的详情里。
|
|
9386
|
+
//
|
|
9387
|
+
// 边界(刻意的,别改成"真一键"):GitHub 不支持匿名 star——PUT /user/starred 必须带
|
|
9388
|
+
// 用户自己的凭据。插件不做站内账号、不存 token(见 docs/design.md 的边界一节),
|
|
9389
|
+
// 所以这里只诚实做两件事:显示上游**真实**的 star 数,和一键打开仓库页让用户按
|
|
9390
|
+
// GitHub 自己的 Star。不写本地「已 star」状态:那是一条没人能验证的布景,
|
|
9391
|
+
// 而且会误导用户以为已经点过了。
|
|
9392
|
+
//
|
|
9393
|
+
// 数字走宿主侧 wbRepoMeta(6 小时缓存 + ETag + 限流回落),客户端不自己打 api.github.com。
|
|
9394
|
+
// 读不到就不显示数字,按钮本身照常可用——它不该因为一个计数失败而消失。
|
|
9395
|
+
const ARMORY_REPO_KEY = parseGithubRepoKey(ARMORY_REPO_URL);
|
|
9396
|
+
const RepoStarChip = ({ call }) => {
|
|
9397
|
+
const [stars, setStars] = useState(null);
|
|
9398
|
+
// 点击爆裂动画靠 class 开关驱动;burstKey 让「连点」也能重放(同 class 不会重启动画)。
|
|
9399
|
+
const [burstKey, setBurstKey] = useState(0);
|
|
9400
|
+
useEffect(() => {
|
|
9401
|
+
if (typeof call !== 'function' || !ARMORY_REPO_KEY) return undefined;
|
|
9402
|
+
let alive = true;
|
|
9403
|
+
call('wbRepoMeta', { repos: [ARMORY_REPO_KEY] }).then((res) => {
|
|
9404
|
+
const row = alive && res && res.repos ? res.repos[ARMORY_REPO_KEY] : null;
|
|
9405
|
+
if (row && row.ok === true && typeof row.stars === 'number') setStars(row.stars);
|
|
9406
|
+
}).catch(() => { /* 保留 null:宁可不显示数字,也不显示一个假的 */ });
|
|
9407
|
+
return () => { alive = false; };
|
|
9408
|
+
}, [call]);
|
|
9409
|
+
useEffect(() => {
|
|
9410
|
+
if (!burstKey) return undefined;
|
|
9411
|
+
const timer = setTimeout(() => setBurstKey(0), 620);
|
|
9412
|
+
return () => clearTimeout(timer);
|
|
9413
|
+
}, [burstKey]);
|
|
9414
|
+
return h('a', {
|
|
9415
|
+
className: 'dsh-mcp-star' + (burstKey ? ' burst' : ''),
|
|
9416
|
+
href: ARMORY_REPO_URL,
|
|
9417
|
+
target: '_blank',
|
|
9418
|
+
rel: 'noreferrer',
|
|
9419
|
+
title: t('在 GitHub 打开 dsh-armory 仓库,点右上角的 Star'),
|
|
9420
|
+
'aria-label': t('给 dsh-armory 点 Star(新标签页打开 GitHub 仓库)'),
|
|
9421
|
+
onClick: () => setBurstKey((n) => n + 1),
|
|
9422
|
+
},
|
|
9423
|
+
h('span', { className: 'dsh-mcp-star-spark a', 'aria-hidden': true }),
|
|
9424
|
+
h('span', { className: 'dsh-mcp-star-spark b', 'aria-hidden': true }),
|
|
9425
|
+
h('svg', { className: 'dsh-mcp-star-svg', viewBox: '0 0 16 16', 'aria-hidden': true },
|
|
9426
|
+
h('path', { d: 'M8 1l1.7 4.65 4.96.19-3.9 3.14 1.37 4.8L8 10.6l-4.13 3.18 1.37-4.8-3.9-3.14 4.96-.19z' })),
|
|
9427
|
+
h('span', { className: 'dsh-mcp-star-label' }, t('点 Star')),
|
|
9428
|
+
// 数字没读到就整段不出现:空位比「★ —」诚实。
|
|
9429
|
+
stars === null ? null : h('span', { className: 'dsh-mcp-star-count' }, wbFormatCount(stars)),
|
|
9430
|
+
);
|
|
9431
|
+
};
|
|
9432
|
+
|
|
9433
|
+
// ── 装备清单:把本机配置导出成可分享的 JSON,导入只做差异对比 ──────────────
|
|
9434
|
+
// 与「最近动静」同一原则:不搭后端、不做账号、不自动装。清单只含身份字段,
|
|
9435
|
+
// 敏感值在宿主侧就被投影掉(lib/loadout.js 的白名单),客户端拿不到也不该存。
|
|
9436
|
+
const LOADOUT_KIND_ORDER = ['mcp', 'skill', 'expert', 'plugin'];
|
|
9437
|
+
const loadoutKindLabel = (kind) =>
|
|
9438
|
+
kind === 'mcp' ? t('MCP') : kind === 'skill' ? t('技能') : kind === 'expert' ? t('专家') : kind === 'plugin' ? t('插件') : t('条目');
|
|
9439
|
+
const loadoutSourceLabel = (source) => {
|
|
9440
|
+
const value = String(source || '').trim();
|
|
9441
|
+
if (!value) return '';
|
|
9442
|
+
if (value.startsWith('builtin:')) return t('内置 · {0}', [value.slice(8)]);
|
|
9443
|
+
if (value.startsWith('connector:')) return t('连接器 · {0}', [value.slice(10)]);
|
|
9444
|
+
if (value === 'manual') return t('手动配置');
|
|
9445
|
+
if (value === 'workbuddy-expert') return t('WorkBuddy 专家中心');
|
|
9446
|
+
if (value === 'skillhub' || value === 'workbuddy-skills') return t('SkillHub');
|
|
9447
|
+
if (value === 'codebuddy-plugins-official') return t('CodeBuddy 官方');
|
|
9448
|
+
if (value === 'awesome') return t('DSH 社区');
|
|
9449
|
+
if (value === 'local') return t('本机 / 自带');
|
|
9450
|
+
return value;
|
|
9451
|
+
};
|
|
9452
|
+
const loadoutStatusMeta = (status) =>
|
|
9453
|
+
status === 'update'
|
|
9454
|
+
? { cls: 's-warn', label: t('可更新') }
|
|
9455
|
+
: status === 'missing'
|
|
9456
|
+
? { cls: 's-warn', label: t('本机没有') }
|
|
9457
|
+
: { cls: 's-on', label: t('已安装') };
|
|
9458
|
+
const loadoutCountText = (loadout) => {
|
|
9459
|
+
const counts = loadout && loadout.counts ? loadout.counts : {};
|
|
9460
|
+
const parts = LOADOUT_KIND_ORDER
|
|
9461
|
+
.filter((kind) => Number(counts[kind]) > 0)
|
|
9462
|
+
.map((kind) => `${loadoutKindLabel(kind)} ${counts[kind]}`);
|
|
9463
|
+
return parts.length ? parts.join(' · ') : t('清单为空。');
|
|
9464
|
+
};
|
|
9465
|
+
const loadoutFileNameOf = (name) => {
|
|
9466
|
+
const base = String(name || '').replace(/[\\/:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '');
|
|
9467
|
+
return `${base || 'dsh-loadout'}.json`;
|
|
9468
|
+
};
|
|
9469
|
+
const downloadLoadoutFile = (filename, text) => {
|
|
9470
|
+
try {
|
|
9471
|
+
const blob = new Blob([text], { type: 'application/json;charset=utf-8' });
|
|
9472
|
+
const url = URL.createObjectURL(blob);
|
|
9473
|
+
const anchor = document.createElement('a');
|
|
9474
|
+
anchor.href = url;
|
|
9475
|
+
anchor.download = filename;
|
|
9476
|
+
document.body.appendChild(anchor);
|
|
9477
|
+
anchor.click();
|
|
9478
|
+
anchor.remove();
|
|
9479
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
9480
|
+
return true;
|
|
9481
|
+
} catch {
|
|
9482
|
+
return false;
|
|
9483
|
+
}
|
|
9484
|
+
};
|
|
9485
|
+
|
|
9486
|
+
const LoadoutModal = ({ call, onClose, onLocate }) => {
|
|
9487
|
+
const dialogRef = useRef(null);
|
|
9488
|
+
useFocusTrap(dialogRef, true);
|
|
9489
|
+
const [mode, setMode] = useState('export');
|
|
9490
|
+
const [name, setName] = useState('');
|
|
9491
|
+
const [loadout, setLoadout] = useState(null);
|
|
9492
|
+
const [text, setText] = useState('');
|
|
9493
|
+
const [busy, setBusy] = useState(false);
|
|
9494
|
+
const [error, setError] = useState('');
|
|
9495
|
+
const [note, setNote] = useState('');
|
|
9496
|
+
const [copied, setCopied] = useState('');
|
|
9497
|
+
const [importText, setImportText] = useState('');
|
|
9498
|
+
const [diff, setDiff] = useState(null);
|
|
9499
|
+
|
|
9500
|
+
const generate = useCallback(async (customName) => {
|
|
9501
|
+
setBusy(true); setError(''); setNote('');
|
|
9502
|
+
try {
|
|
9503
|
+
const res = await call('wbLoadoutExport', { name: String(customName ?? '').trim() });
|
|
9504
|
+
if (!res?.ok || !res.loadout) throw new Error(res?.error || t('导出失败'));
|
|
9505
|
+
setLoadout(res.loadout);
|
|
9506
|
+
setText(JSON.stringify(res.loadout, null, 2));
|
|
9507
|
+
if (!String(customName ?? name ?? '').trim()) setName(res.loadout.name || '');
|
|
9508
|
+
return res.loadout;
|
|
9509
|
+
} catch (e) {
|
|
9510
|
+
setError(t('生成失败:{0}', [String(e?.message || e)]));
|
|
9511
|
+
return null;
|
|
9512
|
+
} finally {
|
|
9513
|
+
setBusy(false);
|
|
9514
|
+
}
|
|
9515
|
+
}, [call, name]);
|
|
9516
|
+
|
|
9517
|
+
useEffect(() => { generate(''); }, []);
|
|
9518
|
+
|
|
9519
|
+
const markCopied = (tag) => {
|
|
9520
|
+
setCopied(tag);
|
|
9521
|
+
setTimeout(() => setCopied(''), 1600);
|
|
9522
|
+
};
|
|
9523
|
+
const copyJson = async () => {
|
|
9524
|
+
if (!text) return;
|
|
9525
|
+
if (await copyText(text)) {
|
|
9526
|
+
setNote(t('已复制 JSON'));
|
|
9527
|
+
markCopied('json');
|
|
9528
|
+
}
|
|
9529
|
+
};
|
|
9530
|
+
const download = () => {
|
|
9531
|
+
if (!text) return;
|
|
9532
|
+
if (downloadLoadoutFile(loadoutFileNameOf(loadout?.name || name), text)) setNote(t('已开始下载'));
|
|
9533
|
+
else setNote(t('浏览器不允许下载,请改用复制 JSON'));
|
|
9534
|
+
};
|
|
9535
|
+
const copyName = async (value) => {
|
|
9536
|
+
if (await copyText(String(value || ''))) markCopied('name:' + String(value || ''));
|
|
9537
|
+
};
|
|
9538
|
+
const readFile = async (event) => {
|
|
9539
|
+
const file = event.target.files && event.target.files[0];
|
|
9540
|
+
event.target.value = '';
|
|
9541
|
+
if (!file) return;
|
|
9542
|
+
if (file.size > 256 * 1024) {
|
|
9543
|
+
setError(t('文件过大(上限 256KB)'));
|
|
9544
|
+
return;
|
|
9545
|
+
}
|
|
9546
|
+
try {
|
|
9547
|
+
setImportText(await file.text());
|
|
9548
|
+
setDiff(null);
|
|
9549
|
+
setError('');
|
|
9550
|
+
} catch (e) {
|
|
9551
|
+
setError(t('文件读取失败:{0}', [String(e?.message || e)]));
|
|
9552
|
+
}
|
|
9553
|
+
};
|
|
9554
|
+
const compare = async () => {
|
|
9555
|
+
const raw = importText.trim();
|
|
9556
|
+
if (!raw) {
|
|
9557
|
+
setError(t('先粘贴一份清单,或选择 .json 文件。'));
|
|
9558
|
+
return;
|
|
9559
|
+
}
|
|
9560
|
+
setBusy(true); setError(''); setNote(''); setDiff(null);
|
|
9561
|
+
try {
|
|
9562
|
+
let parsed;
|
|
9563
|
+
try {
|
|
9564
|
+
parsed = JSON.parse(raw);
|
|
9565
|
+
} catch (e) {
|
|
9566
|
+
throw new Error(t('不是有效的 JSON:{0}', [String(e?.message || e)]));
|
|
9567
|
+
}
|
|
9568
|
+
const res = await call('wbLoadoutDiff', { loadout: parsed });
|
|
9569
|
+
if (!res?.ok) throw new Error(res?.error || t('对比失败'));
|
|
9570
|
+
setDiff(res);
|
|
9571
|
+
} catch (e) {
|
|
9572
|
+
setError(String(e?.message || e));
|
|
9573
|
+
} finally {
|
|
9574
|
+
setBusy(false);
|
|
9575
|
+
}
|
|
9576
|
+
};
|
|
9577
|
+
|
|
9578
|
+
return h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && onClose() },
|
|
9579
|
+
h('div', {
|
|
9580
|
+
ref: dialogRef, className: 'dsh-mcp-modal dsh-mcp-loadout-modal', role: 'dialog', 'aria-modal': true,
|
|
9581
|
+
'aria-label': t('装备清单'), tabIndex: -1,
|
|
9582
|
+
onKeyDown: (e) => { if (e.key === 'Escape' && !busy) { e.stopPropagation(); onClose(); } },
|
|
9583
|
+
},
|
|
9584
|
+
h('h3', null, t('装备清单')),
|
|
9585
|
+
h('div', { className: 'dsh-mcp-loadout-tabs', role: 'tablist' },
|
|
9586
|
+
h('button', {
|
|
9587
|
+
type: 'button', role: 'tab', 'aria-selected': mode === 'export',
|
|
9588
|
+
className: 'dsh-mcp-loadout-tab', onClick: () => setMode('export'),
|
|
9589
|
+
}, t('导出')),
|
|
9590
|
+
h('button', {
|
|
9591
|
+
type: 'button', role: 'tab', 'aria-selected': mode === 'import',
|
|
9592
|
+
className: 'dsh-mcp-loadout-tab', onClick: () => setMode('import'),
|
|
9593
|
+
}, t('导入对比')),
|
|
9594
|
+
),
|
|
9595
|
+
mode === 'export'
|
|
9596
|
+
? h('div', { className: 'dsh-mcp-loadout-pane' },
|
|
9597
|
+
h('p', { className: 'dsh-mcp-loadout-hint' },
|
|
9598
|
+
t('把本机的 MCP、技能、专家与插件导出成一份可分享的清单;导入只做对比,不会自动安装。')),
|
|
9599
|
+
h('label', { className: 'dsh-mcp-loadout-name' },
|
|
9600
|
+
h('span', null, t('清单名称')),
|
|
9601
|
+
h('input', {
|
|
9602
|
+
value: name, maxLength: 120, disabled: busy,
|
|
9603
|
+
placeholder: t('我的 DSH 装备清单'),
|
|
9604
|
+
onChange: (e) => setName(e.target.value),
|
|
9605
|
+
}),
|
|
9606
|
+
h(Btn, { onClick: () => generate(name), disabled: busy || !name.trim(), children: busy ? t('读取本机配置…') : t('重新生成') }),
|
|
9607
|
+
),
|
|
9608
|
+
loadout
|
|
9609
|
+
? h('div', { className: 'dsh-mcp-loadout-result' },
|
|
9610
|
+
h('div', { className: 'dsh-mcp-loadout-summary' },
|
|
9611
|
+
h('span', null, loadoutCountText(loadout)),
|
|
9612
|
+
loadout.truncated ? h('span', { className: 'dsh-mcp-loadout-warn' }, t('超过 200 条,只导出前 200 条。')) : null,
|
|
9613
|
+
),
|
|
9614
|
+
h('pre', { className: 'dsh-mcp-loadout-json' }, text),
|
|
9615
|
+
h('div', { className: 'dsh-mcp-loadout-actions' },
|
|
9616
|
+
h(Btn, { onClick: copyJson, disabled: !text, children: copied === 'json' ? t('已复制') : t('复制 JSON') }),
|
|
9617
|
+
h(Btn, { onClick: download, disabled: !text, children: t('下载 .json') }),
|
|
9618
|
+
h(Btn, { onClick: () => setMode('import'), children: t('导入对比') }),
|
|
9619
|
+
),
|
|
9620
|
+
)
|
|
9621
|
+
: h('div', { className: 'dsh-mcp-wb-empty' }, busy ? t('读取本机配置…') : t('还没有生成清单。')),
|
|
9622
|
+
h('p', { className: 'dsh-mcp-loadout-note' },
|
|
9623
|
+
t('这份清单只含名称、类型、来源与版本;不会写入密钥、header 或环境变量的值。')),
|
|
9624
|
+
)
|
|
9625
|
+
: h('div', { className: 'dsh-mcp-loadout-pane' },
|
|
9626
|
+
h('p', { className: 'dsh-mcp-loadout-hint' },
|
|
9627
|
+
t('把朋友发来的清单贴在这里,或选择文件。导入只做对比,不会自动安装。')),
|
|
9628
|
+
h('textarea', {
|
|
9629
|
+
className: 'dsh-mcp-loadout-input', value: importText, rows: 8, spellCheck: false,
|
|
9630
|
+
placeholder: t('粘贴清单 JSON…'),
|
|
9631
|
+
onChange: (e) => { setImportText(e.target.value); setDiff(null); setError(''); },
|
|
9632
|
+
}),
|
|
9633
|
+
h('div', { className: 'dsh-mcp-loadout-actions' },
|
|
9634
|
+
h('label', { className: 'dsh-mcp-btn dsh-mcp-loadout-file' },
|
|
9635
|
+
t('选择 .json 文件'),
|
|
9636
|
+
h('input', { type: 'file', accept: '.json,application/json', hidden: true, onChange: readFile }),
|
|
9637
|
+
),
|
|
9638
|
+
h(Btn, { onClick: compare, disabled: busy || !importText.trim(), children: busy ? t('对比中…') : t('对比本机') }),
|
|
9639
|
+
importText ? h('button', {
|
|
9640
|
+
type: 'button', className: 'dsh-mcp-btn',
|
|
9641
|
+
onClick: () => { setImportText(''); setDiff(null); setError(''); },
|
|
9642
|
+
}, t('清空')) : null,
|
|
9643
|
+
),
|
|
9644
|
+
diff
|
|
9645
|
+
? h('div', { className: 'dsh-mcp-loadout-result' },
|
|
9646
|
+
h('div', { className: 'dsh-mcp-loadout-summary' },
|
|
9647
|
+
h('span', null, t('共 {0} 项 · 已安装 {1} · 可更新 {2} · 本机没有 {3}', [
|
|
9648
|
+
diff.counts?.total || 0, diff.counts?.installed || 0, diff.counts?.update || 0, diff.counts?.missing || 0,
|
|
9649
|
+
])),
|
|
9650
|
+
diff.loadout?.name ? h('span', { className: 'dsh-mcp-loadout-from' }, t('清单来自 {0}', [diff.loadout.name])) : null,
|
|
9651
|
+
),
|
|
9652
|
+
h('div', { className: 'dsh-mcp-loadout-rows' },
|
|
9653
|
+
(diff.rows || []).map((row) => {
|
|
9654
|
+
const meta = loadoutStatusMeta(row.status);
|
|
9655
|
+
const nameKey = 'name:' + String(row.name || '');
|
|
9656
|
+
return h('div', { key: String(row.kind) + ':' + String(row.name), className: 'dsh-mcp-loadout-row' },
|
|
9657
|
+
h('div', { className: 'dsh-mcp-loadout-row-main' },
|
|
9658
|
+
h('div', { className: 'dsh-mcp-loadout-row-title' },
|
|
9659
|
+
h('span', { className: 'dsh-mcp-loadout-kind' }, loadoutKindLabel(row.kind)),
|
|
9660
|
+
h('b', null, row.name),
|
|
9661
|
+
h(Badge, { cls: meta.cls }, meta.label),
|
|
9662
|
+
),
|
|
9663
|
+
h('div', { className: 'dsh-mcp-loadout-row-meta' },
|
|
9664
|
+
row.source ? h('span', null, t('来源:{0}', [loadoutSourceLabel(row.source)])) : null,
|
|
9665
|
+
row.version ? h('span', null, t('版本:{0}', [row.version])) : null,
|
|
9666
|
+
row.localVersion && row.localVersion !== row.version ? h('span', null, t('本机:{0}', [row.localVersion])) : null,
|
|
9667
|
+
),
|
|
9668
|
+
),
|
|
9669
|
+
h('div', { className: 'dsh-mcp-loadout-row-actions' },
|
|
9670
|
+
row.status !== 'installed' ? h('button', {
|
|
9671
|
+
type: 'button', className: 'dsh-mcp-btn',
|
|
9672
|
+
onClick: () => onLocate(row),
|
|
9673
|
+
}, t('去市场')) : null,
|
|
9674
|
+
h('button', {
|
|
9675
|
+
type: 'button', className: 'dsh-mcp-btn',
|
|
9676
|
+
onClick: () => copyName(row.name),
|
|
9677
|
+
}, copied === nameKey ? t('已复制名称') : t('复制名称')),
|
|
9678
|
+
),
|
|
9679
|
+
);
|
|
9680
|
+
}),
|
|
9681
|
+
),
|
|
9682
|
+
)
|
|
9683
|
+
: null,
|
|
9684
|
+
h('p', { className: 'dsh-mcp-loadout-note' },
|
|
9685
|
+
t('导入只做对比;点「去市场」逐项安装,不会自动写入任何配置。')),
|
|
9686
|
+
),
|
|
9687
|
+
error ? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' }, error) : null,
|
|
9688
|
+
note ? h('div', { className: 'dsh-mcp-wb-note', role: 'status' }, note) : null,
|
|
9689
|
+
h('div', { className: 'dsh-mcp-modal-actions' },
|
|
9690
|
+
h(Btn, { onClick: onClose, disabled: busy, children: t('关闭') }),
|
|
9691
|
+
),
|
|
9692
|
+
),
|
|
9693
|
+
);
|
|
9694
|
+
};
|
|
9695
|
+
|
|
9696
|
+
// ── 最近动静:本机快照对比,不联网、不统计、不伪造「在线人数」───────────────
|
|
9697
|
+
// 语义边界(必须诚实):
|
|
9698
|
+
// - 只对比「这台机器上次成功读取目录」与「这一次读取」的差异;
|
|
9699
|
+
// - 只有**同一份清单**的两次读取才互为基线(basis,见 recordMarketSnapshot):翻页切片、
|
|
9700
|
+
// 带搜索词的结果、不同排序的首页是三个不同的集合,拿来互比只会造出假动静;
|
|
9701
|
+
// - 只记录新增 / 移除 / 版本或核验状态变化,不做消息流,也不假装实时;
|
|
9702
|
+
// - 没有变化就直说没有;事件 7 天过期,用户可一键清除。
|
|
9703
|
+
const MARKET_PULSE_KEY = KEY_PREFIX + '.marketPulse.v2';
|
|
9704
|
+
// v2 的由来(2026-10-24 用户报「切换页面重新进入时重复读取」):v1 的技能基线取的是
|
|
9705
|
+
// SkillHub「下载量(本页排序)」首页,而这一档根本不给上游发 sortBy(见 wb-market.js 的
|
|
9706
|
+
// SKILLHUB_REMOTE_SORTS)——无 ORDER BY 的 LIMIT 20 每次返回的是 19 万库里随机 20 条
|
|
9707
|
+
// (实测:同一请求隔 2.5 秒重合 0/20,而 sortBy=installs 重合 20/20)。于是每进一次市场
|
|
9708
|
+
// 就记一对「新增 20 / 移除 20」。旧键里攒下来的全是这种假动静,没有一条可信到值得挑出来
|
|
9709
|
+
// 保留,所以读到即整份丢弃、不搬值。
|
|
9710
|
+
const LEGACY_MARKET_PULSE_KEY = KEY_PREFIX + '.marketPulse.v1';
|
|
9711
|
+
// 两次读取的键重合度低于此值 ⇒ 读到的不是同一份清单(上游换了默认排序、目录整批改写、
|
|
9712
|
+
// 或一次被截断的部分响应)。这种差异报出来就是「全增全删」的噪音,闭嘴重新起底更诚实。
|
|
9713
|
+
const MARKET_PULSE_MIN_OVERLAP = 0.1;
|
|
9714
|
+
const MARKET_PULSE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9715
|
+
const MARKET_PULSE_MAX_EVENTS = 8;
|
|
9716
|
+
const MARKET_PULSE_SOURCES = {
|
|
9717
|
+
awesome: { get label() { return t('DSH 社区'); }, tab: 'plugin' },
|
|
9718
|
+
expert: { get label() { return t('专家中心'); }, tab: 'expert' },
|
|
9719
|
+
skill: { get label() { return t('SkillHub'); }, tab: 'skill' },
|
|
9720
|
+
connector: { get label() { return t('连接器市场'); }, tab: 'connector' },
|
|
9721
|
+
};
|
|
9722
|
+
const emptyMarketPulse = () => ({ version: 2, snapshots: {}, events: [] });
|
|
9723
|
+
const readMarketPulse = () => {
|
|
9724
|
+
try {
|
|
9725
|
+
// 旧键一次性丢弃(removeItem 幂等;隐私模式下和下面的 getItem 一起被同一个 catch 兜住)。
|
|
9726
|
+
localStorage.removeItem(LEGACY_MARKET_PULSE_KEY);
|
|
9727
|
+
const raw = JSON.parse(localStorage.getItem(MARKET_PULSE_KEY) || 'null');
|
|
9728
|
+
if (!raw || raw.version !== 2 || typeof raw.snapshots !== 'object' || !Array.isArray(raw.events)) return emptyMarketPulse();
|
|
9729
|
+
const now = Date.now();
|
|
9730
|
+
// CodeBuddy 插件源已移除:旧快照与「最近动静」里 source='plugin' 的记录一并丢弃,
|
|
9731
|
+
// 否则会留下一条点不进去、也没有市场对应的历史事件。
|
|
9732
|
+
const snapshots = { ...raw.snapshots };
|
|
9733
|
+
delete snapshots.plugin;
|
|
9734
|
+
return {
|
|
9735
|
+
version: 2,
|
|
9736
|
+
snapshots,
|
|
9737
|
+
events: raw.events.filter((event) => event && event.source !== 'plugin' && now - Number(event.at || 0) <= MARKET_PULSE_TTL_MS).slice(0, MARKET_PULSE_MAX_EVENTS),
|
|
9738
|
+
};
|
|
9739
|
+
} catch { return emptyMarketPulse(); }
|
|
9740
|
+
};
|
|
9741
|
+
const writeMarketPulse = (state) => {
|
|
9742
|
+
try { localStorage.setItem(MARKET_PULSE_KEY, JSON.stringify(state)); } catch { /* 配额 / 隐私模式:静默降级 */ }
|
|
9743
|
+
};
|
|
9744
|
+
const pushPulseEvent = (state, source, kind, keys, current, previous) => {
|
|
9745
|
+
const names = keys.map((key) => current[key]?.n || previous[key]?.n || key).slice(0, 3);
|
|
9746
|
+
state.events.unshift({
|
|
9747
|
+
id: source + ':' + kind + ':' + Date.now() + ':' + Math.random().toString(36).slice(2, 6),
|
|
9748
|
+
source, kind, count: keys.length, names, at: Date.now(),
|
|
9749
|
+
});
|
|
9750
|
+
state.events = state.events.slice(0, MARKET_PULSE_MAX_EVENTS);
|
|
9751
|
+
};
|
|
9752
|
+
/**
|
|
9753
|
+
* 记录一次目录快照并返回是否有变化。
|
|
9754
|
+
* rows: [{ key, name, sig }];sig 是「版本 + 核验状态」这类稳定签名,不用来比较热门度
|
|
9755
|
+
* (星数每天波动,属于噪音,不当作「动静」)。
|
|
9756
|
+
* options.basis: 这份清单的「可比身份」,默认 'full'(整份目录)。只有 basis 相同的两次
|
|
9757
|
+
* 读取才互为基线;不同就静默换基线——不报错、也不造事件。局部视图必须把视图身份写进
|
|
9758
|
+
* basis(技能侧传 'skill:installs' 这类,见 loadSkills)。
|
|
9759
|
+
*/
|
|
9760
|
+
const recordMarketSnapshot = (source, rows, options = {}) => {
|
|
9761
|
+
if (!source || !Array.isArray(rows) || !rows.length) return false;
|
|
9762
|
+
const basis = String(options.basis || 'full');
|
|
9763
|
+
const state = readMarketPulse();
|
|
9764
|
+
const current = {};
|
|
9765
|
+
for (const row of rows) {
|
|
9766
|
+
const key = String(row && row.key || '').trim();
|
|
9767
|
+
if (!key) continue;
|
|
9768
|
+
current[key] = { s: String(row.sig || ''), n: String(row.name || key) };
|
|
9769
|
+
}
|
|
9770
|
+
if (!Object.keys(current).length) return false;
|
|
9771
|
+
const last = state.snapshots[source];
|
|
9772
|
+
const previous = last && last.items ? last.items : null;
|
|
9773
|
+
state.snapshots[source] = { at: Date.now(), basis, items: current };
|
|
9774
|
+
// 三条「不比」的路径:没有基线、基线不是同一份清单、两份清单重合度过低。
|
|
9775
|
+
// 都只写新基线、不产事件——宁可少报,也不报假的。
|
|
9776
|
+
if (!previous || last.basis !== basis) {
|
|
9777
|
+
writeMarketPulse(state);
|
|
9778
|
+
return false;
|
|
9779
|
+
}
|
|
9780
|
+
const currentKeys = Object.keys(current);
|
|
9781
|
+
const previousKeys = Object.keys(previous);
|
|
9782
|
+
const shared = currentKeys.reduce((n, key) => n + (key in previous ? 1 : 0), 0);
|
|
9783
|
+
if (shared / Math.max(currentKeys.length, previousKeys.length) < MARKET_PULSE_MIN_OVERLAP) {
|
|
9784
|
+
writeMarketPulse(state);
|
|
9785
|
+
return false;
|
|
9786
|
+
}
|
|
9787
|
+
const added = currentKeys.filter((key) => !(key in previous));
|
|
9788
|
+
const removed = previousKeys.filter((key) => !(key in current));
|
|
9789
|
+
const updated = currentKeys.filter((key) => key in previous && current[key].s !== previous[key].s);
|
|
9790
|
+
if (!added.length && !removed.length && !updated.length) {
|
|
9791
|
+
writeMarketPulse(state);
|
|
9792
|
+
return false;
|
|
9793
|
+
}
|
|
9794
|
+
if (added.length) pushPulseEvent(state, source, 'added', added, current, previous);
|
|
9795
|
+
if (removed.length) pushPulseEvent(state, source, 'removed', removed, current, previous);
|
|
9796
|
+
if (updated.length) pushPulseEvent(state, source, 'updated', updated, current, previous);
|
|
9797
|
+
writeMarketPulse(state);
|
|
9798
|
+
return true;
|
|
9799
|
+
};
|
|
9800
|
+
const clearMarketPulse = () => {
|
|
9801
|
+
try { localStorage.removeItem(MARKET_PULSE_KEY); } catch { /* 隐私模式 */ }
|
|
9802
|
+
};
|
|
9803
|
+
// 子组件(专家市场)写完备忘录后,通知面板重读一次;模块级单监听,组件卸载即摘除。
|
|
9804
|
+
let marketPulseListener = null;
|
|
9805
|
+
const setMarketPulseListener = (listener) => { marketPulseListener = typeof listener === 'function' ? listener : null; };
|
|
9806
|
+
const emitMarketPulse = () => { try { marketPulseListener?.(); } catch { /* 单个监听失败不影响业务 */ } };
|
|
9807
|
+
const MarketPulse = ({ state, onJump, onClear }) => {
|
|
9808
|
+
const events = Array.isArray(state?.events) ? state.events : [];
|
|
9809
|
+
const snapshotAts = Object.values(state?.snapshots || {}).map((row) => Number(row?.at || 0)).filter(Boolean);
|
|
9810
|
+
const lastAt = snapshotAts.length ? Math.max(...snapshotAts) : 0;
|
|
9811
|
+
if (!events.length) {
|
|
9812
|
+
return h('div', { className: 'dsh-mcp-pulse quiet' },
|
|
9813
|
+
h('span', { className: 'dsh-mcp-pulse-title' }, t('最近动静')),
|
|
9814
|
+
h('span', { className: 'dsh-mcp-pulse-quiet' },
|
|
9815
|
+
lastAt
|
|
9816
|
+
? t('暂无新变化 · 上次对比 {0} · 只在本机对比快照', [relativeLabel(lastAt)])
|
|
9817
|
+
: t('已开始记录市场动态;下次打开时这里会列出真实变化')),
|
|
9818
|
+
);
|
|
9819
|
+
}
|
|
9820
|
+
return h('div', { className: 'dsh-mcp-pulse' },
|
|
9821
|
+
h('div', { className: 'dsh-mcp-pulse-head' },
|
|
9822
|
+
h('b', { className: 'dsh-mcp-pulse-title' }, t('最近动静')),
|
|
9823
|
+
h('span', { className: 'dsh-mcp-pulse-note' }, t('自上次打开市场以来,目录里真实发生的变化(本机快照对比)')),
|
|
9824
|
+
h('button', { type: 'button', className: 'dsh-mcp-pulse-clear', onClick: onClear }, t('清除')),
|
|
9825
|
+
),
|
|
9826
|
+
h('ul', { className: 'dsh-mcp-pulse-list' }, events.map((event) => {
|
|
9827
|
+
const meta = MARKET_PULSE_SOURCES[event.source] || null;
|
|
9828
|
+
const sourceLabel = meta ? meta.label : event.source;
|
|
9829
|
+
const text = event.kind === 'added'
|
|
9830
|
+
? t('{0} 新增 {1} 条', [sourceLabel, event.count])
|
|
9831
|
+
: event.kind === 'removed'
|
|
9832
|
+
? t('{0} 移除 {1} 条', [sourceLabel, event.count])
|
|
9833
|
+
: t('{0} 更新 {1} 条', [sourceLabel, event.count]);
|
|
9834
|
+
const sample = event.names && event.names.length ? ' · ' + event.names.join(', ') + (event.count > event.names.length ? t(' 等') : '') : '';
|
|
9835
|
+
return h('li', { key: event.id },
|
|
9836
|
+
h('button', {
|
|
9837
|
+
type: 'button', className: 'dsh-mcp-pulse-item', title: t('跳到对应市场'),
|
|
9838
|
+
onClick: () => { if (meta && typeof onJump === 'function') onJump(meta); },
|
|
9839
|
+
},
|
|
9840
|
+
h('span', { className: 'dsh-mcp-pulse-kind ' + event.kind }, text),
|
|
9841
|
+
h('span', { className: 'dsh-mcp-pulse-sample' }, sample),
|
|
9842
|
+
),
|
|
9843
|
+
h('span', { className: 'dsh-mcp-pulse-time' }, relativeLabel(event.at)),
|
|
9844
|
+
);
|
|
9845
|
+
})),
|
|
9846
|
+
);
|
|
9847
|
+
};
|
|
9848
|
+
|
|
9849
|
+
// ── 市场面板:WorkBuddy 连接器 / SkillHub 技能 / 插件 / 专家子标签 ──────────────
|
|
8067
9850
|
// 市场搜索框:左内置放大镜、圆角胶囊、聚焦高亮、右一键清空。
|
|
8068
|
-
//
|
|
9851
|
+
// 两处复用(连接器 / 插件):两者都是纯本地过滤,输入即过滤;
|
|
8069
9852
|
// 技能走服务端搜索,onCommit 由防抖或回车触发(见 WorkBuddyMarket 内注释)。
|
|
8070
9853
|
const WbSearchBox = ({ value, onChange, placeholder, disabled, onCommit }) =>
|
|
8071
9854
|
h('div', { className: 'dsh-mcp-wb-search' },
|
|
@@ -8094,7 +9877,20 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8094
9877
|
const [busy, setBusy] = useState(false);
|
|
8095
9878
|
const [busyLabel, setBusyLabel] = useState('');
|
|
8096
9879
|
const [note, setNote] = useState('');
|
|
8097
|
-
//
|
|
9880
|
+
// 「最近动静」只读本机快照:读写失败静默降级,绝不阻塞市场主流程。
|
|
9881
|
+
const [pulse, setPulse] = useState(() => readMarketPulse());
|
|
9882
|
+
// 装备清单:打开时现读本机,不在面板挂载时就做汇总(省一次 IPC 与目录读取)。
|
|
9883
|
+
const [loadoutOpen, setLoadoutOpen] = useState(false);
|
|
9884
|
+
// 专家页搜索种子的一次性 epoch:即使当前已停在专家页,也能用 key 重挂并带上搜索词。
|
|
9885
|
+
const [expertEpoch, setExpertEpoch] = useState(0);
|
|
9886
|
+
// 专家子页签徽标:市场挂载后延迟一拍预取专家目录,只取条数。目录读失败静默——
|
|
9887
|
+
// 徽标先不显示,绝不拖垮市场主流程;用户进过一次专家页后磁盘缓存常驻,之后零网络。
|
|
9888
|
+
const [expertTotal, setExpertTotal] = useState(null);
|
|
9889
|
+
const refreshPulse = useCallback(() => setPulse(readMarketPulse()), []);
|
|
9890
|
+
useEffect(() => {
|
|
9891
|
+
setMarketPulseListener(refreshPulse);
|
|
9892
|
+
return () => setMarketPulseListener(null);
|
|
9893
|
+
}, [refreshPulse]);
|
|
8098
9894
|
const [connectorSource, setConnectorSource] = useState('');
|
|
8099
9895
|
const [connectors, setConnectors] = useState([]);
|
|
8100
9896
|
const [connectorQuery, setConnectorQuery] = useState('');
|
|
@@ -8112,6 +9908,9 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8112
9908
|
const [skillKeywordInput, setSkillKeywordInput] = useState('');
|
|
8113
9909
|
const [skills, setSkills] = useState([]);
|
|
8114
9910
|
const [skillTotal, setSkillTotal] = useState(0);
|
|
9911
|
+
// 子页签徽标用的「全库」总量:只在无关键词的响应里更新。搜索命中数不进来,
|
|
9912
|
+
// 徽标与连接器 / 插件徽标同语义——市场规模,不随当前筛选变。
|
|
9913
|
+
const [skillMarketTotal, setSkillMarketTotal] = useState(0);
|
|
8115
9914
|
// 技能侧自己的加载/错误态:此前与连接器共用 `loading`/`error`,两种请求谁先回来谁就
|
|
8116
9915
|
// 把对方的态改掉——连接器 1 秒读完就把 loading 置 false,技能还在飞,于是技能页先画
|
|
8117
9916
|
// 「技能目录暂时是空的 / 可能还没同步下来」;等技能真的超时,又把整页 banner 改成报错。
|
|
@@ -8169,14 +9968,19 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8169
9968
|
try {
|
|
8170
9969
|
const res = await call('wbConnectorCatalog');
|
|
8171
9970
|
if (!res?.ok) throw new Error(res?.error || t('读取连接器目录失败'));
|
|
8172
|
-
|
|
9971
|
+
const rows = res.connectors || [];
|
|
9972
|
+
setConnectors(rows);
|
|
8173
9973
|
setConnectorSource(res.loadedFrom || '');
|
|
9974
|
+
// 目录快照只记「版本 + 可装性」这种稳定签名:星数 / 下载量波动不是「动静」。
|
|
9975
|
+
if (recordMarketSnapshot('connector', rows.map((row) => ({
|
|
9976
|
+
key: row.id, name: row.name, sig: String(row.version || '') + '|' + (row.installable ? '1' : '0'),
|
|
9977
|
+
})))) refreshPulse();
|
|
8174
9978
|
} catch (e) {
|
|
8175
9979
|
setError(String(e?.message || e));
|
|
8176
9980
|
} finally {
|
|
8177
9981
|
setLoading(false);
|
|
8178
9982
|
}
|
|
8179
|
-
}, [call]);
|
|
9983
|
+
}, [call, refreshPulse]);
|
|
8180
9984
|
|
|
8181
9985
|
// 技能列表请求序号:搜索防抖与翻页会连发多个请求,慢的那个回来时不能盖掉新的结果。
|
|
8182
9986
|
const skillSeq = useRef(0);
|
|
@@ -8187,8 +9991,23 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8187
9991
|
const res = await call('wbSkillCatalog', { page, sort, keyword, pageSize: WB_PAGE_SIZE });
|
|
8188
9992
|
if (seq !== skillSeq.current) return; // 过期响应,直接丢弃
|
|
8189
9993
|
if (!res?.ok) throw new Error(res?.error || t('读取技能目录失败'));
|
|
8190
|
-
|
|
9994
|
+
const rows = res.skills || [];
|
|
9995
|
+
setSkills(rows);
|
|
8191
9996
|
setSkillTotal(res.total || 0);
|
|
9997
|
+
if (!String(keyword || '').trim()) setSkillMarketTotal(res.total || 0);
|
|
9998
|
+
// 「最近动静」:技能是远程分页 + 服务端搜索,任何一次响应都只是 19 万库里的一个切片,
|
|
9999
|
+
// 切片之间互比等于自欺。只有**服务端全局排序**(最多安装 / 最多星标 / 综合评分)的
|
|
10000
|
+
// 无关键词首页才是稳定集合,才配当基线,并且基线按排序分档存(installs 的 Top20 和
|
|
10001
|
+
// stars 的 Top20 是两个集合,互比又会造出「全增全删」)。
|
|
10002
|
+
// 默认那档「下载量(本页排序)」不给上游发 sortBy,上游无 ORDER BY 的 LIMIT 20
|
|
10003
|
+
// 每次返回随机 20 条(2026-10-24 实测重合 0/20)——v1 就是把它当基线,才每次进市场
|
|
10004
|
+
// 刷出一对「新增 20 / 移除 20」,即用户报的「切换页面重新进入时重复读取」。
|
|
10005
|
+
const skillPulseBasis = page === 1 && !String(keyword || '').trim() && sort !== 'downloads'
|
|
10006
|
+
? 'skill:' + sort
|
|
10007
|
+
: '';
|
|
10008
|
+
if (skillPulseBasis && recordMarketSnapshot('skill', rows.map((row) => ({
|
|
10009
|
+
key: row.namespace + '/' + row.slug, name: row.name, sig: String(row.version || ''),
|
|
10010
|
+
})), { basis: skillPulseBasis })) refreshPulse();
|
|
8192
10011
|
} catch (e) {
|
|
8193
10012
|
if (seq === skillSeq.current) setSkillError(String(e?.message || e));
|
|
8194
10013
|
} finally {
|
|
@@ -8465,105 +10284,12 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8465
10284
|
}
|
|
8466
10285
|
};
|
|
8467
10286
|
|
|
8468
|
-
//
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
const [builtinCategory, setBuiltinCategory] = useState('all');
|
|
8472
|
-
const [builtinOnlyReady, setBuiltinOnlyReady] = useState(false);
|
|
8473
|
-
const [builtinsLoading, setBuiltinsLoading] = useState(false);
|
|
8474
|
-
const [builtinError, setBuiltinError] = useState('');
|
|
8475
|
-
const loadBuiltins = useCallback(async () => {
|
|
8476
|
-
setBuiltinsLoading(true); setBuiltinError('');
|
|
8477
|
-
try {
|
|
8478
|
-
const res = await call('builtins');
|
|
8479
|
-
if (!res?.ok) throw new Error(res?.error || t('读取 MCP 目录失败'));
|
|
8480
|
-
setBuiltins(res.builtins || []);
|
|
8481
|
-
} catch (e) {
|
|
8482
|
-
setBuiltinError(String(e?.message || e));
|
|
8483
|
-
} finally {
|
|
8484
|
-
setBuiltinsLoading(false);
|
|
8485
|
-
}
|
|
8486
|
-
}, [call]);
|
|
8487
|
-
useEffect(() => {
|
|
8488
|
-
if (tab === 'builtin' && !builtins.length && !builtinsLoading) loadBuiltins();
|
|
8489
|
-
}, [tab, builtins.length, builtinsLoading, loadBuiltins]);
|
|
8490
|
-
const builtinNeedsKey = (item) => Array.isArray(item.envKeys) && item.envKeys.length > 0;
|
|
8491
|
-
const builtinNeedsConfig = (item) => Array.isArray(item.configKeys) && item.configKeys.length > 0;
|
|
8492
|
-
const builtinNeedsArgs = (item) => Array.isArray(item.placeholders) && item.placeholders.length > 0;
|
|
8493
|
-
const builtinReady = (item) => !builtinNeedsKey(item) && !builtinNeedsConfig(item) && !builtinNeedsArgs(item);
|
|
8494
|
-
// P1:目录随 RPC 同时送中英字段,这里按当前 locale 投影一次。只在 47 条上跑,
|
|
8495
|
-
// 不做 useMemo——locale 变化时 useI18nRevision 已强制重渲染,缓存反而会留下旧语言。
|
|
8496
|
-
const localizedBuiltins = builtins.map((item) => localizeBuiltinItem(item));
|
|
8497
|
-
const builtinCategories = (() => {
|
|
8498
|
-
const counts = new Map();
|
|
8499
|
-
for (const item of localizedBuiltins) {
|
|
8500
|
-
const key = item.category || 'other';
|
|
8501
|
-
const entry = counts.get(key) || { label: item.categoryLabel || key, count: 0 };
|
|
8502
|
-
entry.count += 1;
|
|
8503
|
-
counts.set(key, entry);
|
|
8504
|
-
}
|
|
8505
|
-
return [...counts.entries()].map(([key, entry]) => ({ key, ...entry }));
|
|
8506
|
-
})();
|
|
8507
|
-
const builtinQueryKey = builtinQuery.trim().toLowerCase();
|
|
8508
|
-
// ── T03/R5:codebuddy 插件市场 ──
|
|
8509
|
-
// 目录一次全量(官方 zip 里的 external_plugins 清单,无分页概念),本地过滤。
|
|
8510
|
-
// 安装走官方 pluginManager.installBundle(spec, {}) 严格双参(少传第二个会被 wire 拒绝,
|
|
8511
|
-
// 见本文件 runUpgrade 的踩坑注释);装完宿主进程不热载新包,必须重启 DSH 才生效——
|
|
8512
|
-
// 提示条 state 常驻(不写 localStorage,本次会话内不消失)。
|
|
8513
|
-
const [plugins, setPlugins] = useState([]);
|
|
8514
|
-
const [pluginsLoading, setPluginsLoading] = useState(false);
|
|
8515
|
-
const [pluginsError, setPluginsError] = useState('');
|
|
8516
|
-
const [pluginQuery, setPluginQuery] = useState('');
|
|
8517
|
-
// T04/R8:插件「只看已安装」(全量本地筛选)。
|
|
8518
|
-
const [pluginOnlyInstalled, setPluginOnlyInstalled] = useState(false);
|
|
8519
|
-
const [pluginDetail, setPluginDetail] = useState(null);
|
|
10287
|
+
// 插件安装走宿主的 pluginManager.installBundle(spec, {}) 严格双参(少传第二个会被
|
|
10288
|
+
// wire 拒绝,见本文件 runUpgrade 的踩坑注释);装完宿主进程不热载新包,必须重启 DSH
|
|
10289
|
+
// 才生效——提示条 state 常驻(不写 localStorage,本次会话内不消失)。
|
|
8520
10290
|
const [restartNotice, setRestartNotice] = useState('');
|
|
8521
|
-
// T05/R9:插件市场更新提示。**状态名不能叫 updateHint**——那个名字已被本文件
|
|
8522
|
-
// L3288 的 dsh-armory 自身版本更新提示占用。只按 reason 分支,不读 error 正文。
|
|
8523
|
-
const [pluginUpdateHint, setPluginUpdateHint] = useState(null);
|
|
8524
|
-
const pluginDetailRef = useRef(null);
|
|
8525
|
-
useFocusTrap(pluginDetailRef, !!pluginDetail);
|
|
8526
|
-
const loadPlugins = useCallback(async () => {
|
|
8527
|
-
setPluginsLoading(true); setPluginsError('');
|
|
8528
|
-
try {
|
|
8529
|
-
const res = await call('wbPluginCatalog');
|
|
8530
|
-
if (!res?.ok) throw new Error(res?.error || t('读取插件目录失败'));
|
|
8531
|
-
setPlugins(res.plugins || []);
|
|
8532
|
-
} catch (e) {
|
|
8533
|
-
setPluginsError(String(e?.message || e));
|
|
8534
|
-
} finally {
|
|
8535
|
-
setPluginsLoading(false);
|
|
8536
|
-
}
|
|
8537
|
-
}, [call]);
|
|
8538
|
-
// T05/R9:插件市场更新探测——**懒触发、只跑一次**(X4 边界:绝不常驻定时器)。
|
|
8539
|
-
// 只在「已进过 plugin tab 且目录已加载」时探一次;reason 分支见下方提示条。
|
|
8540
|
-
const checkPluginUpdate = useCallback(async () => {
|
|
8541
|
-
try {
|
|
8542
|
-
const res = await call('wbPluginUpdateCheck');
|
|
8543
|
-
setPluginUpdateHint(res || null);
|
|
8544
|
-
} catch (e) {
|
|
8545
|
-
// 探测失败不打扰用户(辅助信息),保留 null 即不渲染提示条。
|
|
8546
|
-
setPluginUpdateHint(null);
|
|
8547
|
-
}
|
|
8548
|
-
}, [call]);
|
|
8549
10291
|
// T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)状态机与数据接入。
|
|
8550
|
-
//
|
|
8551
|
-
// 来源切换控件在插件工具行(select 两态),写入 localStorage 持久化。
|
|
8552
|
-
// 注意:必须在下方两个依赖数组含 pluginSource 的 effect 之前声明——依赖数组在渲染期求值,
|
|
8553
|
-
// const 声明若在其后,渲染即抛 ReferenceError: Cannot access 'pluginSource' before initialization(TDZ)。
|
|
8554
|
-
const [pluginSource, setPluginSource] = useState(() => {
|
|
8555
|
-
try { return localStorage.getItem('dsh-armory.pluginSource') === 'codebuddy' ? 'codebuddy' : 'awesome'; } catch { return 'awesome'; }
|
|
8556
|
-
});
|
|
8557
|
-
const pluginUpdateChecked = useRef(false);
|
|
8558
|
-
useEffect(() => {
|
|
8559
|
-
if (tab === 'plugin' && pluginSource === 'codebuddy' && !plugins.length && !pluginsLoading) loadPlugins();
|
|
8560
|
-
}, [tab, pluginSource, plugins.length, pluginsLoading, loadPlugins]);
|
|
8561
|
-
useEffect(() => {
|
|
8562
|
-
// 只在目录加载完成后探一次;pluginUpdateChecked 保证组件存续期内不重复。
|
|
8563
|
-
if (tab !== 'plugin' || pluginSource !== 'codebuddy' || pluginsLoading || !plugins.length || pluginUpdateChecked.current) return;
|
|
8564
|
-
pluginUpdateChecked.current = true;
|
|
8565
|
-
checkPluginUpdate();
|
|
8566
|
-
}, [tab, pluginSource, pluginsLoading, plugins.length, checkPluginUpdate]);
|
|
10292
|
+
// 插件页只有这一个来源;首屏快照见下方 awesomeSnapshotRef。
|
|
8567
10293
|
// 首屏秒开:首次挂载就把上次的快照铺上,用户进插件页立刻看到目录而不是一句「正在加载」。
|
|
8568
10294
|
// 快照只在挂载时读一次(JSON.parse 一两 MB 不能每次 render 都干);真实目录随后覆盖。
|
|
8569
10295
|
const awesomeSnapshotRef = useRef(undefined);
|
|
@@ -8582,7 +10308,10 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8582
10308
|
const [awesomeSort, setAwesomeSort] = useState('stars');
|
|
8583
10309
|
const [awesomePage, setAwesomePage] = useState(1);
|
|
8584
10310
|
const [awesomeOnlyInstalled, setAwesomeOnlyInstalled] = useState(false);
|
|
8585
|
-
|
|
10311
|
+
// 静默预取失败时用来判断「有没有快照可留」:有快照就不弹错误条。
|
|
10312
|
+
const awesomeLenRef = useRef(awesomePlugins.length);
|
|
10313
|
+
useEffect(() => { awesomeLenRef.current = awesomePlugins.length; }, [awesomePlugins.length]);
|
|
10314
|
+
const loadAwesomeCatalog = useCallback(async (refresh = false, silent = false) => {
|
|
8586
10315
|
setAwesomeLoading(true); setAwesomeError('');
|
|
8587
10316
|
try {
|
|
8588
10317
|
const res = await call('wbAwesomeCatalog', refresh ? { refresh: true } : {});
|
|
@@ -8592,10 +10321,17 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8592
10321
|
const marketplace = res.marketplace || 'awesome-dsh-plugins';
|
|
8593
10322
|
const catalog = (res.plugins || []).map((item) => ({ ...item, marketplace }));
|
|
8594
10323
|
setAwesomePlugins(catalog);
|
|
10324
|
+
// 「最近动静」:只对比新增 / 移除 / 版本或核验状态变化。星数不进签名(每天波动属噪音)。
|
|
10325
|
+
if (recordMarketSnapshot('awesome', catalog.map((item) => ({
|
|
10326
|
+
key: item.name,
|
|
10327
|
+
name: item.name,
|
|
10328
|
+
sig: String(item.version || '') + '|' + String(item.verifiedAgainst || '') + '|' + String(item.status || ''),
|
|
10329
|
+
})))) refreshPulse();
|
|
8595
10330
|
// 顺手把这次的目录留成下次的首屏快照(写失败静默,不影响本次渲染)。
|
|
8596
10331
|
writeAwesomeSnapshot({ plugins: catalog, marketplace, fetchedAt: res.fetchedAt });
|
|
8597
10332
|
} catch (e) {
|
|
8598
|
-
|
|
10333
|
+
// 静默预取失败且已有快照 → 保留旧目录;没有快照才让错误条接管。
|
|
10334
|
+
if (!silent || awesomeLenRef.current === 0) setAwesomeError(String(e?.message || e));
|
|
8599
10335
|
} finally {
|
|
8600
10336
|
setAwesomeLoading(false);
|
|
8601
10337
|
}
|
|
@@ -8653,39 +10389,51 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8653
10389
|
}
|
|
8654
10390
|
setReadmeView({ item, loading: false, ok: true, markdown: res.markdown || '', base: res.base || '', error: '', source: res.source || '', reason: res.reason || '', truncated: res.truncated === true });
|
|
8655
10391
|
}, [call]);
|
|
8656
|
-
//
|
|
8657
|
-
//
|
|
10392
|
+
// 目录预取:市场面板一挂载就在后台把 DSH 社区目录拉好。以前必须切到插件页才发请求,
|
|
10393
|
+
// 用户看到的是「空白 → loading → 列表」;现在切过去通常已经就绪(另有 localStorage
|
|
10394
|
+
// 快照垫底)。延迟一拍,避免和 MCP 首屏争带宽;只跑一次,失败由列表里的错误条 / 空态
|
|
10395
|
+
// 提供手动重试。
|
|
8658
10396
|
const awesomeAutoLoaded = useRef(false);
|
|
8659
10397
|
useEffect(() => {
|
|
8660
|
-
if (
|
|
10398
|
+
if (awesomeAutoLoaded.current) return;
|
|
10399
|
+
const timer = setTimeout(() => {
|
|
10400
|
+
if (awesomeAutoLoaded.current) return;
|
|
10401
|
+
awesomeAutoLoaded.current = true;
|
|
10402
|
+
loadAwesomeCatalog(false, true);
|
|
10403
|
+
}, 400);
|
|
10404
|
+
return () => clearTimeout(timer);
|
|
10405
|
+
}, [loadAwesomeCatalog]);
|
|
10406
|
+
// 用户已经在插件页时不等预取延迟:立即发(旧行为保留,预取只负责「还没切过来」)。
|
|
10407
|
+
useEffect(() => {
|
|
10408
|
+
if (tab !== 'plugin' || awesomeAutoLoaded.current) return;
|
|
8661
10409
|
awesomeAutoLoaded.current = true;
|
|
8662
|
-
loadAwesomeCatalog();
|
|
8663
|
-
}, [tab,
|
|
10410
|
+
loadAwesomeCatalog(false, true);
|
|
10411
|
+
}, [tab, loadAwesomeCatalog]);
|
|
10412
|
+
// 专家目录预取(只为子页签徽标取条数):与插件目录预取同一节奏,延迟一拍避免和
|
|
10413
|
+
// MCP 首屏争带宽。loadExpertCatalogShared 带模块级 TTL 缓存,专家页挂载时复用
|
|
10414
|
+
// 同一份,不会二连网络;组件卸载后回包只作废,不写状态。
|
|
10415
|
+
useEffect(() => {
|
|
10416
|
+
let alive = true;
|
|
10417
|
+
const timer = setTimeout(() => {
|
|
10418
|
+
loadExpertCatalogShared(call).then((data) => {
|
|
10419
|
+
if (alive) setExpertTotal((data.experts || []).length || null);
|
|
10420
|
+
}).catch(() => { /* 徽标读不到就先不显示 */ });
|
|
10421
|
+
}, 400);
|
|
10422
|
+
return () => { alive = false; clearTimeout(timer); };
|
|
10423
|
+
}, [call]);
|
|
8664
10424
|
useEffect(() => {
|
|
8665
10425
|
// 目录加载完成后探一次;awesomeUpdateChecked 保证组件存续期内不重复;已有提示不再探。
|
|
8666
|
-
if (tab !== 'plugin' ||
|
|
10426
|
+
if (tab !== 'plugin' || awesomeLoading || !awesomePlugins.length || awesomeError || awesomeUpdateHint) return;
|
|
8667
10427
|
if (awesomeUpdateChecked.current) return;
|
|
8668
10428
|
awesomeUpdateChecked.current = true;
|
|
8669
10429
|
checkAwesomeUpdate();
|
|
8670
|
-
}, [tab,
|
|
10430
|
+
}, [tab, awesomeLoading, awesomePlugins.length, awesomeError, awesomeUpdateHint, checkAwesomeUpdate]);
|
|
8671
10431
|
useEffect(() => { setAwesomePage(1); }, [awesomeQuery, awesomeCategory, awesomeTags, awesomeSort]);
|
|
8672
|
-
//
|
|
8673
|
-
//
|
|
8674
|
-
// 的 pnpm add 语义支持该 spec);
|
|
8675
|
-
// 2. 其余字符串(裸 npm 名、含版本号等)原样当作 spec 交给宿主;
|
|
8676
|
-
// 3. 没有 repository → null = 不可推导 → UI 只展示、不渲染安装按钮(绝不猜名字)。
|
|
10432
|
+
// DSH 社区条目的主安装路径是 npm 包名:installBundle 直接接受 `npm:<name>` 形态的
|
|
10433
|
+
// spec。没有 npm 的条目在 UI 层已灰置(「暂不支持安装」),走不到这里。
|
|
8677
10434
|
const pluginInstallSpec = (plugin) => {
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
if (plugin?.npm) return `npm:${String(plugin.npm).trim()}`;
|
|
8681
|
-
const repo = String(plugin?.repository || '').trim();
|
|
8682
|
-
if (!repo) return null;
|
|
8683
|
-
const gitHub = repo.match(/^https?:\/\/github\.com\/([^/]+)\/([^/#?]+)(?:[/#?]|\b)/i);
|
|
8684
|
-
if (gitHub) {
|
|
8685
|
-
const ref = (repo.split('#')[1] || '').trim();
|
|
8686
|
-
return `github:${gitHub[1]}/${gitHub[2].replace(/\.git$/, '')}${ref ? '#' + ref : ''}`;
|
|
8687
|
-
}
|
|
8688
|
-
return repo;
|
|
10435
|
+
const npm = String(plugin?.npm || '').trim();
|
|
10436
|
+
return npm ? `npm:${npm}` : null;
|
|
8689
10437
|
};
|
|
8690
10438
|
const installPlugin = async (plugin) => {
|
|
8691
10439
|
if (!pluginManager || typeof pluginManager[UPGRADE_METHOD] !== 'function') {
|
|
@@ -8693,7 +10441,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8693
10441
|
return;
|
|
8694
10442
|
}
|
|
8695
10443
|
const spec = pluginInstallSpec(plugin);
|
|
8696
|
-
if (!spec) { setError(t('
|
|
10444
|
+
if (!spec) { setError(t('该条目未提供 npm 包名,暂不支持安装')); return; }
|
|
8697
10445
|
setBusy(true); setBusyLabel(t('正在安装插件 {0}…', [plugin.name])); setError('');
|
|
8698
10446
|
// 顶部浮层与组件 state 并行记账:面板被卸载(切标签 / 切窗口)后浮层仍在,用户不会误以为被取消。
|
|
8699
10447
|
installToastStart(plugin.name);
|
|
@@ -8713,7 +10461,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8713
10461
|
key: plugin.name,
|
|
8714
10462
|
name: plugin.name,
|
|
8715
10463
|
version: plugin.version || '',
|
|
8716
|
-
source: { kind: 'marketplace', marketplace: plugin.marketplace || '
|
|
10464
|
+
source: { kind: 'marketplace', marketplace: plugin.marketplace || 'awesome-dsh-plugins' },
|
|
8717
10465
|
});
|
|
8718
10466
|
if (!rec?.ok) setNote(t('插件已安装,但安装状态登记失败:{0}', [rec?.error || t('未知错误')]));
|
|
8719
10467
|
} catch (e) {
|
|
@@ -8722,7 +10470,6 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8722
10470
|
await loadInstallState();
|
|
8723
10471
|
// 快照里若还有这条旧目录数据,标记已安装由 installedPluginKeys 负责,无需改动。
|
|
8724
10472
|
setRestartNotice(t('插件 {0} 安装成功,需重启 DSH 宿主后生效。', [plugin.name]));
|
|
8725
|
-
setPluginDetail(null);
|
|
8726
10473
|
installToastFinish(plugin.name, 'ok');
|
|
8727
10474
|
} catch (e) {
|
|
8728
10475
|
setError(String(e?.message || e));
|
|
@@ -8731,13 +10478,6 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8731
10478
|
setBusy(false); setBusyLabel('');
|
|
8732
10479
|
}
|
|
8733
10480
|
};
|
|
8734
|
-
const pluginQueryKey = pluginQuery.trim().toLowerCase();
|
|
8735
|
-
const filteredPlugins = plugins.filter((item) => {
|
|
8736
|
-
// T04/R8:只看已安装——插件账本 key 是 plugin.name。
|
|
8737
|
-
if (pluginOnlyInstalled && !installedPluginKeys.has(String(item.name || ''))) return false;
|
|
8738
|
-
if (!pluginQueryKey) return true;
|
|
8739
|
-
return [item.name, item.description, item.author].filter(Boolean).join(' ').toLowerCase().includes(pluginQueryKey);
|
|
8740
|
-
});
|
|
8741
10481
|
// T06/Awesome:社区目录筛选 / 排序 / 分页(全量本地,不受分页影响;tags 多选为「或」语义)。
|
|
8742
10482
|
const awesomeQueryKey = awesomeQuery.trim().toLowerCase();
|
|
8743
10483
|
const filteredAwesome = awesomePlugins.filter((item) => {
|
|
@@ -8757,24 +10497,49 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8757
10497
|
const awesomePageCount = Math.max(1, Math.ceil(sortedAwesome.length / AWESOME_PAGE_SIZE));
|
|
8758
10498
|
const safeAwesomePage = Math.min(awesomePage, awesomePageCount);
|
|
8759
10499
|
const visibleAwesome = sortedAwesome.slice((safeAwesomePage - 1) * AWESOME_PAGE_SIZE, safeAwesomePage * AWESOME_PAGE_SIZE);
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
const
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
10500
|
+
// 下载量只给「当前这一页」排队:一页 50 张 → 最多 5 个批量请求(宿主侧再缓存 7 天)。
|
|
10501
|
+
// 放在这里(不是每张卡片里)是因为 hook 不能在 .map 里调,而页级批量已经足够省。
|
|
10502
|
+
useNpmDownloads(call, visibleAwesome);
|
|
10503
|
+
|
|
10504
|
+
// 装备清单的「去市场」:只做定位,不替用户点安装。各市场搜索词互不相通,
|
|
10505
|
+
// 所以按 kind / source 落到对应子页并写入该页的搜索框。
|
|
10506
|
+
const locateLoadoutRow = async (row) => {
|
|
10507
|
+
const name = String(row?.name || '').trim();
|
|
10508
|
+
const source = String(row?.source || '');
|
|
10509
|
+
setLoadoutOpen(false);
|
|
10510
|
+
if (row?.kind === 'mcp') {
|
|
10511
|
+
if (source.startsWith('connector:')) {
|
|
10512
|
+
setTab('connector');
|
|
10513
|
+
setConnectorQuery(name);
|
|
10514
|
+
setConnectorOnlyReady(false);
|
|
10515
|
+
setConnectorPage(1);
|
|
10516
|
+
setNote(t('已定位到连接器市场:{0}', [name]));
|
|
10517
|
+
} else {
|
|
10518
|
+
// 非 connector 来源(手动配置或历史 builtin 导入)不在任何市场里:
|
|
10519
|
+
// 复制名称让用户去 MCP 页自己加,绝不替用户写配置。
|
|
10520
|
+
await copyText(name);
|
|
10521
|
+
setNote(t('这是手动配置的 MCP,名称已复制;可到 MCP 页自行添加。'));
|
|
10522
|
+
}
|
|
10523
|
+
return;
|
|
10524
|
+
}
|
|
10525
|
+
if (row?.kind === 'skill') {
|
|
10526
|
+
setTab('skill');
|
|
10527
|
+
setSkillPage(1);
|
|
10528
|
+
setSkillKeywordInput(name);
|
|
10529
|
+
setSkillKeyword(name);
|
|
10530
|
+
setNote(t('已定位到技能市场:{0}', [name]));
|
|
10531
|
+
return;
|
|
10532
|
+
}
|
|
10533
|
+
if (row?.kind === 'expert') {
|
|
10534
|
+
seedExpertMarketQuery(name);
|
|
10535
|
+
setExpertEpoch((value) => value + 1);
|
|
10536
|
+
setTab('expert');
|
|
10537
|
+
setNote(t('已定位到专家市场:{0}', [name]));
|
|
10538
|
+
return;
|
|
8777
10539
|
}
|
|
10540
|
+
setAwesomeQuery(name);
|
|
10541
|
+
setTab('plugin');
|
|
10542
|
+
setNote(t('已定位到插件市场:{0}', [name]));
|
|
8778
10543
|
};
|
|
8779
10544
|
|
|
8780
10545
|
const connectorSearch = h(WbSearchBox, {
|
|
@@ -8802,33 +10567,37 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8802
10567
|
|
|
8803
10568
|
// §6.1 子页签:原来用与顶栏 `.dsh-mcp-cat` 同款的胶囊按钮,两排长得一样,
|
|
8804
10569
|
// 用户分不清「换整页」还是「换市场内部子页」。改成下划线式页签 + 面包屑,
|
|
8805
|
-
//
|
|
10570
|
+
// 视觉上与顶栏彻底分开。面包屑并入页签行最右侧(不再独占一行),随当前子页更新。
|
|
8806
10571
|
const MARKET_TABS = [
|
|
8807
10572
|
{ key: 'connector', label: t('连接器'), count: connectors.length || null },
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
{ key: '
|
|
10573
|
+
// 技能库 19 万+,徽标走 wbFormatCount 缩写(193k);数字只在无关键词响应里更新,
|
|
10574
|
+
// 搜索命中数不进来——与连接器 / 插件徽标同语义:市场规模,不随筛选变。
|
|
10575
|
+
{ key: 'skill', label: t('技能'), count: skillMarketTotal ? wbFormatCount(skillMarketTotal) : null },
|
|
10576
|
+
// 插件市场子页签。count 在目录加载后才有(未载时灰字入口,同技能)。
|
|
10577
|
+
{ key: 'plugin', label: t('插件'), count: awesomePlugins.length || null },
|
|
10578
|
+
// 专家徽标同款:挂载后延迟预取目录条数,读到才显示。
|
|
10579
|
+
{ key: 'expert', label: t('专家'), count: expertTotal },
|
|
8813
10580
|
];
|
|
8814
10581
|
const marketTabLabel = (MARKET_TABS.find((row) => row.key === tab) || MARKET_TABS[0]).label;
|
|
8815
10582
|
return h('div', { className: 'dsh-mcp-wb' },
|
|
8816
|
-
h('div', { className: 'dsh-mcp-mk-crumb' },
|
|
8817
|
-
h('span', { className: 'dsh-mcp-mk-crumb-root' }, t('市场')),
|
|
8818
|
-
h('span', { className: 'dsh-mcp-mk-crumb-sep', 'aria-hidden': 'true' }, '/'),
|
|
8819
|
-
h('b', null, marketTabLabel),
|
|
8820
|
-
),
|
|
8821
10583
|
h('div', { className: 'dsh-mcp-mk-subtabs', role: 'tablist', 'aria-label': t('WorkBuddy 市场') },
|
|
8822
|
-
MARKET_TABS.map((row) => (
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
10584
|
+
MARKET_TABS.map((row) => h('button', {
|
|
10585
|
+
key: row.key, type: 'button', role: 'tab', 'aria-selected': tab === row.key,
|
|
10586
|
+
className: 'dsh-mcp-mk-subtab', onClick: () => setTab(row.key),
|
|
10587
|
+
}, row.label, row.count ? h('i', null, row.count) : null)),
|
|
10588
|
+
// 右侧:装备清单入口 + 面包屑(面包屑 aria-hidden:当前位置已由选中页签表达)。
|
|
10589
|
+
h('div', { className: 'dsh-mcp-mk-topright' },
|
|
10590
|
+
h('button', {
|
|
10591
|
+
type: 'button', className: 'dsh-mcp-loadout-open',
|
|
10592
|
+
title: t('导出本机配置,或导入朋友的清单做差异对比'),
|
|
10593
|
+
onClick: () => setLoadoutOpen(true),
|
|
10594
|
+
}, t('装备清单')),
|
|
10595
|
+
h('div', { className: 'dsh-mcp-mk-crumb', 'aria-hidden': 'true' },
|
|
10596
|
+
h('span', { className: 'dsh-mcp-mk-crumb-root' }, t('市场')),
|
|
10597
|
+
h('span', { className: 'dsh-mcp-mk-crumb-sep' }, '/'),
|
|
10598
|
+
h('b', null, marketTabLabel),
|
|
10599
|
+
),
|
|
10600
|
+
),
|
|
8832
10601
|
),
|
|
8833
10602
|
// §6.5 免责声明:原来整段灰字常驻在页签下方,每次进市场都吃掉两行高度。
|
|
8834
10603
|
// 收敛为一行摘要 + details 展开全文,默认折叠。
|
|
@@ -8838,6 +10607,14 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8838
10607
|
t('连接器来自 WorkBuddy 官方分发(static.workbuddy.cn);技能来自 SkillHub 公开 API;专家来自 WorkBuddy 专家中心分发。') +
|
|
8839
10608
|
t('第三方接口的字段可能随上游改版变化,安装前请在详情里核对传输类型与凭据要求。')),
|
|
8840
10609
|
),
|
|
10610
|
+
// 「最近动静」:本机快照对比,只报真实的目录变化;没有变化就说没有。
|
|
10611
|
+
h(MarketPulse, {
|
|
10612
|
+
state: pulse,
|
|
10613
|
+
onClear: () => { clearMarketPulse(); setPulse(emptyMarketPulse()); },
|
|
10614
|
+
onJump: (meta) => {
|
|
10615
|
+
if (meta.tab) setTab(meta.tab);
|
|
10616
|
+
},
|
|
10617
|
+
}),
|
|
8841
10618
|
tab === 'connector'
|
|
8842
10619
|
? h('div', { className: 'dsh-mcp-wb-toolbar' },
|
|
8843
10620
|
connectorSearch,
|
|
@@ -8889,51 +10666,15 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8889
10666
|
: null,
|
|
8890
10667
|
)
|
|
8891
10668
|
: null,
|
|
8892
|
-
tab === '
|
|
10669
|
+
tab === 'plugin'
|
|
8893
10670
|
? h('div', { className: 'dsh-mcp-wb-toolbar' },
|
|
8894
|
-
h(WbSearchBox, {
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
}),
|
|
8898
|
-
h('label', { className: 'dsh-mcp-check' },
|
|
8899
|
-
|
|
8900
|
-
t('只要免密钥')),
|
|
8901
|
-
h('select', {
|
|
8902
|
-
className: 'dsh-mcp-input dsh-mcp-wb-sort', value: builtinCategory,
|
|
8903
|
-
onChange: (e) => setBuiltinCategory(e.target.value),
|
|
8904
|
-
}, h('option', { key: 'all', value: 'all' }, t('全部分类')),
|
|
8905
|
-
builtinCategories.map((cat) => h('option', { key: cat.key, value: cat.key }, cat.label + t('(') + cat.count + t(')')))),
|
|
8906
|
-
h('span', { className: 'dsh-mcp-builtin-count' }, t('显示 {0} / {1}', [filteredBuiltins.length, builtins.length])),
|
|
10671
|
+
h(WbSearchBox, { value: awesomeQuery, placeholder: t('搜索插件(名称 / 描述 / 作者)'), onChange: (value) => setAwesomeQuery(value) }),
|
|
10672
|
+
...AWESOME_CATEGORIES.map((cat) => h('button', { key: cat.key, type: 'button', className: 'dsh-mcp-tag-chip' + (awesomeCategory === cat.key ? ' on' : ''), 'aria-pressed': awesomeCategory === cat.key, onClick: () => setAwesomeCategory(cat.key) }, cat.label)),
|
|
10673
|
+
...AWESOME_TAGS.map((tag) => h('button', { key: tag, type: 'button', className: 'dsh-mcp-tag-chip' + (awesomeTags.includes(tag) ? ' on' : ''), 'aria-pressed': awesomeTags.includes(tag), onClick: () => setAwesomeTags(awesomeTags.includes(tag) ? awesomeTags.filter((row) => row !== tag) : [...awesomeTags, tag]) }, tag)),
|
|
10674
|
+
h('select', { className: 'dsh-mcp-wb-sort', value: awesomeSort, onChange: (e) => setAwesomeSort(e.target.value) }, AWESOME_SORTS.map((option) => h('option', { key: option.key, value: option.key }, option.label))),
|
|
10675
|
+
h('label', { className: 'dsh-mcp-check', title: t('在全量 {0} 条插件里筛出已安装的(跨页生效)', [awesomePlugins.length]) }, h('input', { type: 'checkbox', checked: awesomeOnlyInstalled, onChange: (e) => setAwesomeOnlyInstalled(e.target.checked) }), t('只看已安装')),
|
|
10676
|
+
h('span', { className: 'dsh-mcp-builtin-count' }, t('显示 {0} / {1}', [visibleAwesome.length, filteredAwesome.length])),
|
|
8907
10677
|
)
|
|
8908
|
-
: tab === 'plugin'
|
|
8909
|
-
? h('div', { className: 'dsh-mcp-wb-toolbar' },
|
|
8910
|
-
h('select', { className: 'dsh-mcp-wb-sort', value: pluginSource, onChange: (e) => { const next = e.target.value === 'awesome' ? 'awesome' : 'codebuddy'; setPluginSource(next); try { localStorage.setItem('dsh-armory.pluginSource', next); } catch { /* localStorage 不可用时仅本会话生效 */ } } },
|
|
8911
|
-
h('option', { value: 'codebuddy' }, t('CodeBuddy 官方')),
|
|
8912
|
-
h('option', { value: 'awesome' }, t('DSH 社区')),
|
|
8913
|
-
),
|
|
8914
|
-
...(pluginSource === 'codebuddy' ? [
|
|
8915
|
-
h(WbSearchBox, {
|
|
8916
|
-
value: pluginQuery, placeholder: t('搜索插件(名称 / 描述 / 作者)'),
|
|
8917
|
-
onChange: (value) => setPluginQuery(value),
|
|
8918
|
-
}),
|
|
8919
|
-
// T04/R8:插件的已安装筛选同样是全量本地筛选。
|
|
8920
|
-
h('label', { className: 'dsh-mcp-check', title: t('在全量 {0} 条插件里筛出已安装的(跨页生效)', [plugins.length]) },
|
|
8921
|
-
h('input', { type: 'checkbox', checked: pluginOnlyInstalled, disabled: pluginsLoading, onChange: (e) => setPluginOnlyInstalled(e.target.checked) }),
|
|
8922
|
-
t('只看已安装')),
|
|
8923
|
-
pluginOnlyInstalled
|
|
8924
|
-
? h('span', { className: 'dsh-mcp-builtin-count' }, t('已安装 {0} / {1}', [installedPluginKeys.size, plugins.length]))
|
|
8925
|
-
: null,
|
|
8926
|
-
h('span', { className: 'dsh-mcp-builtin-count' }, t('显示 {0} / {1}', [filteredPlugins.length, plugins.length])),
|
|
8927
|
-
] : []),
|
|
8928
|
-
...(pluginSource === 'awesome' ? [
|
|
8929
|
-
h(WbSearchBox, { value: awesomeQuery, placeholder: t('搜索插件(名称 / 描述 / 作者)'), onChange: (value) => setAwesomeQuery(value) }),
|
|
8930
|
-
...AWESOME_CATEGORIES.map((cat) => h('button', { key: cat.key, type: 'button', className: 'dsh-mcp-tag-chip' + (awesomeCategory === cat.key ? ' on' : ''), 'aria-pressed': awesomeCategory === cat.key, onClick: () => setAwesomeCategory(cat.key) }, cat.label)),
|
|
8931
|
-
...AWESOME_TAGS.map((tag) => h('button', { key: tag, type: 'button', className: 'dsh-mcp-tag-chip' + (awesomeTags.includes(tag) ? ' on' : ''), 'aria-pressed': awesomeTags.includes(tag), onClick: () => setAwesomeTags(awesomeTags.includes(tag) ? awesomeTags.filter((row) => row !== tag) : [...awesomeTags, tag]) }, tag)),
|
|
8932
|
-
h('select', { className: 'dsh-mcp-wb-sort', value: awesomeSort, onChange: (e) => setAwesomeSort(e.target.value) }, AWESOME_SORTS.map((option) => h('option', { key: option.key, value: option.key }, option.label))),
|
|
8933
|
-
h('label', { className: 'dsh-mcp-check', title: t('在全量 {0} 条插件里筛出已安装的(跨页生效)', [awesomePlugins.length]) }, h('input', { type: 'checkbox', checked: awesomeOnlyInstalled, onChange: (e) => setAwesomeOnlyInstalled(e.target.checked) }), t('只看已安装')),
|
|
8934
|
-
h('span', { className: 'dsh-mcp-builtin-count' }, t('显示 {0} / {1}', [visibleAwesome.length, filteredAwesome.length])),
|
|
8935
|
-
] : []),
|
|
8936
|
-
)
|
|
8937
10678
|
: tab === 'expert'
|
|
8938
10679
|
? null
|
|
8939
10680
|
: tab === 'skill'
|
|
@@ -8957,7 +10698,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
8957
10698
|
busyLabel ? h('div', { className: 'dsh-mcp-wb-note busy', 'aria-live': 'polite' }, busyLabel) : null,
|
|
8958
10699
|
error ? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' }, error) : null,
|
|
8959
10700
|
// 技能加载失败只落在这一行(带重试),不再借用整页 banner:一个上游接口挂了
|
|
8960
|
-
// 不该让「连接器 /
|
|
10701
|
+
// 不该让「连接器 / 技能 / 插件 / 专家」页签看起来都坏了。
|
|
8961
10702
|
tab === 'skill' && skillError
|
|
8962
10703
|
? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' }, skillError, ' ',
|
|
8963
10704
|
h('button', {
|
|
@@ -9021,7 +10762,12 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9021
10762
|
// 完整原因仍留在 blockedReason,hover 卡片或角标都能看到。
|
|
9022
10763
|
item.installable ? null : h('span', { className: 'dsh-mcp-mk-corner', title: item.blockedReason || t('不兼容') }, t('不兼容')),
|
|
9023
10764
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
9024
|
-
|
|
10765
|
+
// 头像来自官方分发包 ZIP 的 icons/<id>(ConnectorAvatar 进视口才取),
|
|
10766
|
+
// 布局与技能卡同款 person 行:头像 + 名字并排,徽标仍靠右。
|
|
10767
|
+
h('div', { className: 'dsh-mcp-card-person' },
|
|
10768
|
+
h(ConnectorAvatar, { call, connectorId: item.id, name: item.name, className: 'dsh-mcp-card-avatar' }),
|
|
10769
|
+
h('div', null, h('div', { className: 'dsh-mcp-card-name' }, item.name)),
|
|
10770
|
+
),
|
|
9025
10771
|
// T04/R8:已安装的条目优先显示「已安装」,不再提示可安装。
|
|
9026
10772
|
installedConnectorKeys.has(String(item.id || ''))
|
|
9027
10773
|
? h(Badge, { cls: 's-on' }, t('已安装'))
|
|
@@ -9059,94 +10805,33 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9059
10805
|
tab === 'connector' && connectorPageCount > 1
|
|
9060
10806
|
? h(Pager, { page: safeConnectorPage, pageCount: connectorPageCount, total: sortedConnectors.length, onPage: setConnectorPage })
|
|
9061
10807
|
: null,
|
|
9062
|
-
tab === 'builtin' && builtinError
|
|
9063
|
-
? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' }, builtinError, ' ',
|
|
9064
|
-
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: loadBuiltins, disabled: builtinsLoading }, t('重试')))
|
|
9065
|
-
: null,
|
|
9066
|
-
tab === 'builtin' && !builtinError && !filteredBuiltins.length
|
|
9067
|
-
? (builtinsLoading
|
|
9068
|
-
? h('div', { className: 'dsh-mcp-wb-empty' }, t('正在读取 MCP 目录…'))
|
|
9069
|
-
// §1.4 空状态四件套。这里是最典型的「裸空白」:目录 0 条时原来只有一句
|
|
9070
|
-
// 「没有匹配的服务」,用户既不知道是筛选造成的还是目录真的空,也不知道点哪。
|
|
9071
|
-
// 主操作按成因分流——有筛选就清筛选(一次点击立刻有结果),没筛选才重读目录。
|
|
9072
|
-
: h(EmptyState, {
|
|
9073
|
-
icon: 'store',
|
|
9074
|
-
title: builtins.length ? t('当前筛选下没有服务') : t('MCP 目录暂时是空的'),
|
|
9075
|
-
hint: builtins.length
|
|
9076
|
-
? t('目录里有 {0} 个服务,都被关键词、分类或「只要免密钥」筛掉了。', [builtins.length])
|
|
9077
|
-
: t('目录由宿主侧维护,可能还没同步下来;重读一次通常就好。'),
|
|
9078
|
-
actions: builtins.length
|
|
9079
|
-
? [
|
|
9080
|
-
{ label: t('清除全部筛选'), icon: 'x', onClick: () => { setBuiltinQuery(''); setBuiltinCategory('all'); setBuiltinOnlyReady(false); } },
|
|
9081
|
-
{ label: t('重新读取目录'), icon: 'refresh', onClick: () => { loadBuiltins(); } },
|
|
9082
|
-
]
|
|
9083
|
-
: [{ label: t('重新读取目录'), icon: 'refresh', onClick: () => { loadBuiltins(); } }],
|
|
9084
|
-
}))
|
|
9085
|
-
: null,
|
|
9086
|
-
tab === 'builtin' && filteredBuiltins.length
|
|
9087
|
-
? h('div', { className: 'dsh-mcp-wb-grid' },
|
|
9088
|
-
filteredBuiltins.map((item) => {
|
|
9089
|
-
const http = item.transport === 'streamable-http';
|
|
9090
|
-
return h('div', { key: item.id, className: 'dsh-mcp-card wb' + (item.installed ? ' installed' : '') },
|
|
9091
|
-
h('div', { className: 'dsh-mcp-card-top' },
|
|
9092
|
-
h('div', null,
|
|
9093
|
-
h('div', { className: 'dsh-mcp-card-name' }, item.label),
|
|
9094
|
-
item.vendor ? h('div', { className: 'dsh-mcp-card-vendor' }, item.vendor) : null,
|
|
9095
|
-
),
|
|
9096
|
-
item.installed ? h(Badge, { cls: 's-on' }, t('已配置')) : null,
|
|
9097
|
-
),
|
|
9098
|
-
h('div', { className: 'dsh-mcp-card-summary' }, item.summary),
|
|
9099
|
-
h('div', { className: 'dsh-mcp-card-tags' },
|
|
9100
|
-
h(Badge, { cls: http ? 't-http' : 't-stdio' }, http ? 'HTTP' : 'stdio'),
|
|
9101
|
-
item.categoryLabel ? h(Badge, null, item.categoryLabel) : null,
|
|
9102
|
-
item.installed
|
|
9103
|
-
? null
|
|
9104
|
-
: builtinNeedsKey(item)
|
|
9105
|
-
? h(Badge, null, t('需密钥'))
|
|
9106
|
-
: builtinNeedsConfig(item)
|
|
9107
|
-
? h(Badge, null, t('需连接信息'))
|
|
9108
|
-
: h(Badge, { cls: 's-off' }, t('免密钥')),
|
|
9109
|
-
// §1.3 色语:缺参数是「还需注意」不是「已故障」,用橙不用红——
|
|
9110
|
-
// 红色的名额留给真正的失败与破坏性操作,否则满屏红之后就没人在意红了。
|
|
9111
|
-
builtinNeedsArgs(item) ? h(Badge, { cls: 's-warn' }, t('需填参数')) : null,
|
|
9112
|
-
// OPT-24 · 2.3:auth 是「装完之后还要做什么」的关键信息——oauth 条目装完
|
|
9113
|
-
// 并不等于能用,得去浏览器授权。none 不出徽章(「免密钥」已覆盖这层语义)。
|
|
9114
|
-
item.auth && item.auth !== 'none' ? h(Badge, { cls: authBadgeCls(item.auth) }, authKindLabel(item.auth)) : null,
|
|
9115
|
-
),
|
|
9116
|
-
h('div', { className: 'dsh-mcp-card-access' }, item.access),
|
|
9117
|
-
// authNote 只在宿主给了才显示(oauth 类条目才有),说明授权流程怎么走。
|
|
9118
|
-
item.auth && item.auth !== 'none' && item.authNote ? h('div', { className: 'dsh-mcp-card-note' }, item.authNote) : null,
|
|
9119
|
-
builtinNeedsKey(item) || builtinNeedsConfig(item)
|
|
9120
|
-
? h('div', { className: 'dsh-mcp-card-vars' },
|
|
9121
|
-
builtinNeedsKey(item) ? h('span', null, t('密钥:') + item.envKeys.join(t('、'))) : null,
|
|
9122
|
-
builtinNeedsConfig(item) ? h('span', null, t('连接:') + item.configKeys.join(t('、'))) : null,
|
|
9123
|
-
)
|
|
9124
|
-
: null,
|
|
9125
|
-
h('div', { className: 'dsh-mcp-wb-card-actions' },
|
|
9126
|
-
item.installed
|
|
9127
|
-
? null
|
|
9128
|
-
: h('button', { type: 'button', className: 'dsh-mcp-btn primary', disabled: busy, onClick: () => installBuiltin(item) }, t('安装')),
|
|
9129
|
-
item.homepage
|
|
9130
|
-
? h('a', { className: 'dsh-mcp-btn', href: item.homepage, target: '_blank', rel: 'noreferrer', title: t('打开官方文档') }, t('文档'))
|
|
9131
|
-
: null,
|
|
9132
|
-
),
|
|
9133
|
-
);
|
|
9134
|
-
}),
|
|
9135
|
-
)
|
|
9136
|
-
: null,
|
|
9137
10808
|
tab === 'skill' && shownSkills.length
|
|
9138
10809
|
? h('div', { className: 'dsh-mcp-wb-grid' },
|
|
9139
10810
|
shownSkills.map((skill) => {
|
|
9140
10811
|
const installed = installedSkills.has(skill.slug);
|
|
9141
10812
|
return h('div', { key: skill.namespace + '/' + skill.slug, className: 'dsh-mcp-card wb' },
|
|
9142
10813
|
h('div', { className: 'dsh-mcp-card-top' },
|
|
9143
|
-
h('div',
|
|
9144
|
-
h(
|
|
9145
|
-
|
|
10814
|
+
h('div', { className: 'dsh-mcp-card-person' },
|
|
10815
|
+
h(Avatar, {
|
|
10816
|
+
url: skill.iconUrl || githubAvatarUrl(skill.homepage),
|
|
10817
|
+
name: skill.ownerName || skill.namespace || skill.name,
|
|
10818
|
+
className: 'dsh-mcp-card-avatar',
|
|
10819
|
+
}),
|
|
10820
|
+
h('div', null,
|
|
10821
|
+
h('div', { className: 'dsh-mcp-card-name' }, skill.name),
|
|
10822
|
+
h('div', { className: 'dsh-mcp-card-vendor' }, skill.canonical || skill.namespace),
|
|
10823
|
+
),
|
|
9146
10824
|
),
|
|
9147
10825
|
installed ? h(Badge, { cls: 's-on' }, t('已安装')) : null,
|
|
9148
10826
|
),
|
|
9149
10827
|
h('div', { className: 'dsh-mcp-card-summary' }, skill.description || skill.name),
|
|
10828
|
+
h(PresenceLine, {
|
|
10829
|
+
owner: skill.ownerName || skill.namespace,
|
|
10830
|
+
avatar: skill.iconUrl || githubAvatarUrl(skill.homepage),
|
|
10831
|
+
updatedAt: skill.updatedAt,
|
|
10832
|
+
href: /^https?:\/\//i.test(String(skill.homepage || '')) ? skill.homepage : '',
|
|
10833
|
+
className: 'dsh-mcp-presence-card',
|
|
10834
|
+
}),
|
|
9150
10835
|
h('div', { className: 'dsh-mcp-card-tags' },
|
|
9151
10836
|
h(Badge, null, `↓${wbFormatCount(skill.downloads)}`),
|
|
9152
10837
|
skill.installs ? h(Badge, null, t('装 {0}', [wbFormatCount(skill.installs)])) : null,
|
|
@@ -9168,135 +10853,118 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9168
10853
|
tab === 'skill' && skillPageCount > 1
|
|
9169
10854
|
? h(Pager, { page: skillPage, pageCount: skillPageCount, total: skillTotal, onPage: setSkillPage, disabled: skillLoading })
|
|
9170
10855
|
: null,
|
|
9171
|
-
// ──
|
|
9172
|
-
//
|
|
9173
|
-
//
|
|
10856
|
+
// ── 插件安装后的重启提示 ──
|
|
10857
|
+
// 常驻本 tab 顶部(组件卸载即消失,不写 localStorage)——installBundle 装完包,
|
|
10858
|
+
// 宿主进程不热载,不重启就以为装失败了。
|
|
9174
10859
|
tab === 'plugin' && restartNotice
|
|
9175
10860
|
? h('div', { className: 'dsh-mcp-wb-note restart', role: 'status' },
|
|
9176
10861
|
restartNotice,
|
|
9177
10862
|
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: () => setRestartNotice('') }, t('知道了')),
|
|
9178
10863
|
)
|
|
9179
10864
|
: null,
|
|
9180
|
-
// T05/R9:插件市场更新提示条。只按 reason 分支(设计文档:UI 只看 reason,不看 error)。
|
|
9181
|
-
// no-source / probe-failed / up-to-date → 不提示;no-cache / update-available → 提示。
|
|
9182
|
-
// probe-failed 不自动重试(X4),用户可手动点「重新探测」。
|
|
9183
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && pluginUpdateHint && (pluginUpdateHint.reason === 'update-available' || pluginUpdateHint.reason === 'no-cache')
|
|
9184
|
-
? h('div', { className: 'dsh-mcp-wb-note', role: 'status' },
|
|
9185
|
-
pluginUpdateHint.reason === 'update-available'
|
|
9186
|
-
? t('插件市场有新版本可更新(本地缓存 {0})。点「重新加载」可拉取最新目录。', [pluginUpdateHint.localEtag || t('未知错误')])
|
|
9187
|
-
: t('本地还没有插件市场缓存,点「重新加载」可首次拉取目录。'),
|
|
9188
|
-
h('button', {
|
|
9189
|
-
type: 'button', className: 'dsh-mcp-btn', disabled: pluginsLoading,
|
|
9190
|
-
onClick: async () => { setPluginUpdateHint(null); await loadPlugins(); },
|
|
9191
|
-
}, t('重新加载')),
|
|
9192
|
-
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: () => setPluginUpdateHint(null) }, t('知道了')),
|
|
9193
|
-
)
|
|
9194
|
-
: null,
|
|
9195
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && pluginUpdateHint && pluginUpdateHint.reason === 'probe-failed'
|
|
9196
|
-
? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' },
|
|
9197
|
-
t('插件市场更新探测失败(不影响已安装插件)。'),
|
|
9198
|
-
h('button', { type: 'button', className: 'dsh-mcp-btn', disabled: pluginsLoading, onClick: () => { pluginUpdateChecked.current = false; checkPluginUpdate(); } }, t('重新探测')),
|
|
9199
|
-
)
|
|
9200
|
-
: null,
|
|
9201
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && pluginsError
|
|
9202
|
-
? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' }, pluginsError, ' ',
|
|
9203
|
-
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: loadPlugins, disabled: pluginsLoading }, t('重试')))
|
|
9204
|
-
: null,
|
|
9205
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && pluginsLoading && !plugins.length
|
|
9206
|
-
? h('div', { className: 'dsh-mcp-wb-empty' }, t('正在读取插件目录…'))
|
|
9207
|
-
: null,
|
|
9208
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && !pluginsError && !pluginsLoading && !plugins.length
|
|
9209
|
-
? h(EmptyState, {
|
|
9210
|
-
icon: 'store',
|
|
9211
|
-
title: t('插件目录暂时是空的'),
|
|
9212
|
-
hint: t('目录来自 codebuddy 官方市场 ZIP 分发,可能还没同步下来;重读一次通常就好。'),
|
|
9213
|
-
actions: [{ label: t('重新读取目录'), icon: 'refresh', onClick: loadPlugins }],
|
|
9214
|
-
})
|
|
9215
|
-
: null,
|
|
9216
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && plugins.length && !filteredPlugins.length
|
|
9217
|
-
? h(EmptyState, {
|
|
9218
|
-
icon: 'store',
|
|
9219
|
-
tight: true,
|
|
9220
|
-
title: t('没有匹配的插件'),
|
|
9221
|
-
hint: t('换个关键词试试,或清空搜索回到完整目录。'),
|
|
9222
|
-
actions: [{ label: t('清空搜索'), icon: 'x', onClick: () => setPluginQuery('') }],
|
|
9223
|
-
})
|
|
9224
|
-
: null,
|
|
9225
|
-
tab === 'plugin' && pluginSource === 'codebuddy' && filteredPlugins.length
|
|
9226
|
-
? h('div', { className: 'dsh-mcp-wb-grid' },
|
|
9227
|
-
filteredPlugins.map((item) => {
|
|
9228
|
-
const spec = pluginInstallSpec(item);
|
|
9229
|
-
return h('div', { key: item.name, className: 'dsh-mcp-card wb' },
|
|
9230
|
-
h('div', { className: 'dsh-mcp-card-top' },
|
|
9231
|
-
h('div', null,
|
|
9232
|
-
h('div', { className: 'dsh-mcp-card-name' }, item.name),
|
|
9233
|
-
item.author ? h('div', { className: 'dsh-mcp-card-vendor' }, item.author) : null,
|
|
9234
|
-
),
|
|
9235
|
-
// T04/R8:已安装徽章(账本 key 是 plugin.name)。
|
|
9236
|
-
installedPluginKeys.has(String(item.name || ''))
|
|
9237
|
-
? h(Badge, { cls: 's-on' }, t('已安装'))
|
|
9238
|
-
: null,
|
|
9239
|
-
item.version ? h(Badge, null, 'v' + item.version) : null,
|
|
9240
|
-
),
|
|
9241
|
-
h('div', { className: 'dsh-mcp-card-summary' }, item.description || item.name),
|
|
9242
|
-
h('div', { className: 'dsh-mcp-card-tags' },
|
|
9243
|
-
item.license ? h(Badge, null, item.license) : null,
|
|
9244
|
-
// R6:dependencies 只展示(缺 = 无依赖不出徽章),绝不自动安装。
|
|
9245
|
-
(item.dependencies || []).length ? h(Badge, { title: item.dependencies.map((d) => d.name).join(', ') }, t('依赖 {0}', [item.dependencies.length])) : null,
|
|
9246
|
-
),
|
|
9247
|
-
h('div', { className: 'dsh-mcp-wb-card-actions' },
|
|
9248
|
-
h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: async () => {
|
|
9249
|
-
setPluginDetail({ ...item, loading: true });
|
|
9250
|
-
try {
|
|
9251
|
-
const res = await call('wbPluginDetail', { name: item.name });
|
|
9252
|
-
setPluginDetail(res?.ok ? { ...res.plugin, loading: false } : { ...item, loading: false, detailError: res?.error || t('读取详情失败') });
|
|
9253
|
-
} catch (e) {
|
|
9254
|
-
setPluginDetail({ ...item, loading: false, detailError: String(e?.message || e) });
|
|
9255
|
-
}
|
|
9256
|
-
} }, t('详情')),
|
|
9257
|
-
// UNCLEAR#1:repository 可推导才渲染安装按钮;不可推导 → 只展示。
|
|
9258
|
-
spec
|
|
9259
|
-
? h('button', { type: 'button', className: 'dsh-mcp-btn primary', disabled: busy || isInstallRunning(item.name), onClick: () => installPlugin(item) }, t('安装'))
|
|
9260
|
-
: h('span', { className: 'dsh-mcp-wb-blocked', title: t('该插件未提供可推导的安装来源(repository),暂不能安装') }, t('仅展示')),
|
|
9261
|
-
),
|
|
9262
|
-
);
|
|
9263
|
-
}),
|
|
9264
|
-
)
|
|
9265
|
-
: null,
|
|
9266
10865
|
// ── T06/Awesome:DSH 社区(awesome-dsh-plugins)目录 ──
|
|
9267
|
-
tab === 'plugin' &&
|
|
9268
|
-
tab === 'plugin' &&
|
|
10866
|
+
tab === 'plugin' && awesomeStale ? h('div', { className: 'dsh-mcp-wb-note', role: 'status' }, t('目录缓存已过期(更新源暂时不可达,显示的是本地缓存)。'), h('button', { type: 'button', className: 'dsh-mcp-btn', disabled: awesomeLoading, onClick: () => { writeAwesomeProbeAt(0); awesomeUpdateChecked.current = false; loadAwesomeCatalog(true); } }, t('重新加载'))) : null,
|
|
10867
|
+
tab === 'plugin' && awesomeUpdateHint && (awesomeUpdateHint.reason === 'update-available' || awesomeUpdateHint.reason === 'no-cache') ? h('div', { className: 'dsh-mcp-wb-note', role: 'status' }, awesomeUpdateHint.reason === 'update-available' ? t('DSH 社区目录有新版本(本地缓存 {0})。点「重新加载」可拉取最新目录。', [awesomeUpdateHint.localEtag || t('未知错误')]) : t('本地还没有 DSH 社区目录缓存,点「重新加载」可首次拉取目录。'), h('button', { type: 'button', className: 'dsh-mcp-btn', disabled: awesomeLoading, onClick: async () => { setAwesomeUpdateHint(null); writeAwesomeProbeAt(0); awesomeUpdateChecked.current = false; await loadAwesomeCatalog(true); } }, t('重新加载')), h('button', { type: 'button', className: 'dsh-mcp-btn', onClick: () => setAwesomeUpdateHint(null) }, t('知道了'))) : null,
|
|
9269
10868
|
|
|
9270
|
-
tab === 'plugin' &&
|
|
9271
|
-
tab === 'plugin' &&
|
|
9272
|
-
tab === 'plugin' &&
|
|
9273
|
-
tab === 'plugin' &&
|
|
9274
|
-
tab === 'plugin' &&
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
10869
|
+
tab === 'plugin' && awesomeError ? h('div', { className: 'dsh-mcp-wb-error', role: 'alert' }, awesomeError, h('button', { type: 'button', className: 'dsh-mcp-btn', disabled: awesomeLoading, onClick: () => loadAwesomeCatalog() }, t('重试'))) : null,
|
|
10870
|
+
tab === 'plugin' && awesomeLoading && !awesomePlugins.length ? h('div', { className: 'dsh-mcp-wb-empty' }, t('正在加载 DSH 社区目录…')) : null,
|
|
10871
|
+
tab === 'plugin' && !awesomeError && !awesomeLoading && !awesomePlugins.length ? h(EmptyState, { icon: 'store', title: t('DSH 社区目录暂时是空的'), hint: t('目录来自 awesome-dsh-plugins 社区清单(jsdelivr CDN 分发),拉取失败可重试。'), actions: [{ label: t('重新读取目录'), icon: 'refresh', onClick: () => loadAwesomeCatalog() }] }) : null,
|
|
10872
|
+
tab === 'plugin' && awesomePlugins.length && !visibleAwesome.length ? h(EmptyState, { tight: true, title: t('没有匹配的社区插件'), hint: t('换个关键词,或清空分类与标签筛选。'), actions: [{ label: t('清空筛选'), icon: 'x', onClick: () => { setAwesomeQuery(''); setAwesomeCategory('all'); setAwesomeTags([]); } }] }) : null,
|
|
10873
|
+
tab === 'plugin' && visibleAwesome.length ? h('div', { className: 'dsh-mcp-wb-grid' }, visibleAwesome.map((item) => {
|
|
10874
|
+
const repoKey = parseGithubRepoKey(item.repo);
|
|
10875
|
+
const repoLabel = item.repo ? String(item.repo).replace(/^https?:\/\/github\.com\//i, '') : (item.npm ? String(item.npm) : '');
|
|
10876
|
+
const metaTitle = item.repo ? String(item.repo) : (item.npm ? String(item.npm) : '');
|
|
10877
|
+
const verifiedLabel = item.verifiedAgainst ? t('已验证 · dsh {0}', [item.verifiedAgainst]) : '';
|
|
10878
|
+
// 42.3% 的社区条目 npm 包名 ≠ 显示名,装的时候只能用包名,以前卡面上完全不显示。
|
|
10879
|
+
// 它排第一,且是标签行里唯一可收缩的一枚(CSS .dsh-mcp-pchip.mono 是 flex:0 1 auto):
|
|
10880
|
+
// 放不下时它自己省略号截断,后面的分类/标签不会被挤成半截;完整包名进 title 兜底。
|
|
10881
|
+
const npmPackage = item.npm && String(item.npm) !== String(item.name || '') ? String(item.npm) : '';
|
|
10882
|
+
const chips = [];
|
|
10883
|
+
if (npmPackage) chips.push(npmPackage);
|
|
10884
|
+
if (item.category && item.category !== 'plugin' && !chips.includes(item.category)) chips.push(item.category);
|
|
10885
|
+
for (const tag of item.tags || []) if (tag && !chips.includes(tag)) chips.push(tag);
|
|
10886
|
+
const shownChips = chips.slice(0, 3);
|
|
10887
|
+
const extraChips = chips.length - shownChips.length;
|
|
10888
|
+
const stars = Number(item.stars) || 0;
|
|
10889
|
+
const verifiedDate = item.lastVerified ? wbFormatDate(timestampOf(item.lastVerified)).slice(5) : '';
|
|
10890
|
+
// 底栏三枚事实,对齐技能卡的「↓N ★N 日期」,但卡高一 px 都不长:
|
|
10891
|
+
// ↓ 近 30 天 npm 下载量——目录里没这字段,按包名去 npm 公开统计接口补;问不到
|
|
10892
|
+
// (下架 / 未被统计)就整枚不画,绝不显示 0 冒充「没人下载」。
|
|
10893
|
+
// ★ 仓库星数,目录自带。
|
|
10894
|
+
// 核验 MM-DD 是**目录批次核验日期**,不是插件自身的更新时间(6430 条里 37.5%
|
|
10895
|
+
// 同一批核验);所以带「核验」二字与说明 title,宁可不写也不伪造「更新于」。
|
|
10896
|
+
// 「dsh 版本」不再占这一行,退回右上角已验证徽标的 title(装不装看的是已验证)。
|
|
10897
|
+
const downloads = npmDownloadsFor(item.npm);
|
|
10898
|
+
const dateFact = verifiedDate ? t('核验 {0}', [verifiedDate]) : '';
|
|
10899
|
+
const sealTip = verifiedLabel ? verifiedLabel + (verifiedDate ? ' · ' + t('核验 {0}', [verifiedDate]) : '') : '';
|
|
10900
|
+
const installed = installedPluginKeys.has(String(item.name || ''));
|
|
10901
|
+
const hostComponent = item.npm && String(item.npm).startsWith('@deepseek-ai/');
|
|
10902
|
+
return h('div', { key: item.name, className: 'dsh-mcp-pcard', role: 'button', tabIndex: 0, title: t('查看 README 介绍与插图'), 'aria-label': t('查看 {0} 的 README', [item.name]), onClick: () => loadAwesomeReadme(item), onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); loadAwesomeReadme(item); } } },
|
|
10903
|
+
h('div', { className: 'dsh-mcp-phead' },
|
|
10904
|
+
h('span', { className: 'dsh-mcp-pavatar', 'aria-hidden': true },
|
|
10905
|
+
String(item.name || '?').slice(0, 1).toUpperCase() || '?',
|
|
10906
|
+
repoKey ? h('img', {
|
|
10907
|
+
src: githubAvatarUrl(repoKey), alt: '', loading: 'lazy', referrerPolicy: 'no-referrer',
|
|
10908
|
+
// 头像加载失败就藏起图片(不删 React 管理的节点),露出底下的首字母,不留空白圆圈。
|
|
10909
|
+
onError: (e) => { e.currentTarget.style.display = 'none'; },
|
|
10910
|
+
}) : null),
|
|
10911
|
+
h('div', { className: 'dsh-mcp-piden' },
|
|
10912
|
+
h('h3', { className: 'dsh-mcp-pname', title: item.name }, item.name),
|
|
10913
|
+
h('div', { className: 'dsh-mcp-pmeta' },
|
|
10914
|
+
item.official ? h('span', { className: 'official' }, h('span', { className: 'odot', 'aria-hidden': true }), t('官方')) : null,
|
|
10915
|
+
item.official && repoLabel ? h('span', { className: 'pdot', 'aria-hidden': true }, '·') : null,
|
|
10916
|
+
repoLabel ? h('span', { className: 'dsh-mcp-prepo', title: metaTitle }, repoLabel) : null)),
|
|
10917
|
+
verifiedLabel ? h('span', { className: 'dsh-mcp-pverified', role: 'img', 'aria-label': verifiedLabel, title: sealTip || verifiedLabel }, h(VerifiedSeal, { size: 15 })) : null),
|
|
10918
|
+
h('div', { className: 'dsh-mcp-pdesc' }, item.description || item.name),
|
|
10919
|
+
h('div', { className: 'dsh-mcp-pchips' },
|
|
10920
|
+
shownChips.map((tag) => h('span', { key: tag, className: 'dsh-mcp-pchip' + (tag === npmPackage ? ' mono' : ''), title: tag === npmPackage ? npmPackage : undefined }, tag)),
|
|
10921
|
+
extraChips > 0 ? h('span', { className: 'dsh-mcp-pchip more', title: chips.slice(3).join(', ') }, '+' + extraChips) : null),
|
|
10922
|
+
h('div', { className: 'dsh-mcp-pfoot' },
|
|
10923
|
+
h(PluginCardFacts, {
|
|
10924
|
+
downloadsText: downloads && downloads > 0 ? wbFormatCount(downloads) : '',
|
|
10925
|
+
downloadsTitle: t('npm 近 30 天下载量'),
|
|
10926
|
+
starText: stars ? wbFormatCount(stars) : '',
|
|
10927
|
+
starTitle: '★ ' + stars,
|
|
10928
|
+
dateText: dateFact,
|
|
10929
|
+
dateTitle: t('核验日期来自目录批次核验,不是插件自身的更新时间'),
|
|
10930
|
+
}),
|
|
10931
|
+
h('div', { className: 'dsh-mcp-wb-card-actions', onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation() },
|
|
10932
|
+
item.npm
|
|
10933
|
+
? (hostComponent
|
|
10934
|
+
? h('span', { className: 'dsh-mcp-phost', title: t('DSH 宿主官方组件随宿主本体分发,无法也不需要从市场安装') }, h(Icon, { className: 'ico', d: ICONS.info, size: 11 }), t('宿主组件'))
|
|
10935
|
+
: h('button', { type: 'button', className: 'dsh-mcp-pbtn' + (installed ? ' installed' : ''), disabled: busy || isInstallRunning(item.name), onClick: () => installPlugin(item) }, installed ? t('已安装') : t('安装')))
|
|
10936
|
+
: h('span', { className: 'dsh-mcp-phost', title: t('该条目未提供 npm 包名,暂不支持安装') }, t('暂不支持安装')))));
|
|
10937
|
+
})) : null,
|
|
10938
|
+
tab === 'plugin' && awesomePageCount > 1 ? h(Pager, { page: safeAwesomePage, pageCount: awesomePageCount, total: sortedAwesome.length, onPage: setAwesomePage }) : null,
|
|
9291
10939
|
// ── DSH 社区插件 README 预览(点卡片打开;插图来自 README 自身的相对/绝对地址)──
|
|
9292
10940
|
readmeView
|
|
9293
10941
|
? h('div', { className: 'dsh-mcp-overlay', onClick: (e) => { if (e.target === e.currentTarget) closeReadme(); } },
|
|
9294
10942
|
h('div', { ref: readmeNodeRef, className: 'dsh-mcp-modal dsh-mcp-wb-modal dsh-mcp-readme-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('{0} README 预览', [readmeView.item && readmeView.item.name ? readmeView.item.name : '']), tabIndex: -1, onKeyDown: (e) => { if (e.key === 'Escape') { e.stopPropagation(); closeReadme(); } } },
|
|
9295
|
-
h('
|
|
10943
|
+
h('div', { className: 'dsh-mcp-detail-person' },
|
|
10944
|
+
h(Avatar, { url: githubAvatarUrl(readmeView.item?.repo, 88), name: readmeView.item?.name, className: 'dsh-mcp-detail-avatar' }),
|
|
10945
|
+
h('div', null,
|
|
10946
|
+
h('h3', null, readmeView.item && readmeView.item.name ? readmeView.item.name : ''),
|
|
10947
|
+
readmeView.item && readmeView.item.repo
|
|
10948
|
+
? h('div', { className: 'dsh-mcp-card-vendor', title: readmeView.item.repo }, String(readmeView.item.repo).replace(/^https?:\/\/github\.com\//i, ''))
|
|
10949
|
+
: null,
|
|
10950
|
+
),
|
|
10951
|
+
),
|
|
9296
10952
|
h('div', { className: 'dsh-mcp-readme-meta' },
|
|
9297
10953
|
readmeView.item && readmeView.item.repo ? h(Badge, { title: readmeView.item.repo }, String(readmeView.item.repo).replace(/^https?:\/\/github\.com\//i, '')) : null,
|
|
9298
10954
|
readmeView.item && readmeView.item.npm ? h(Badge, { title: readmeView.item.npm }, 'npm') : null,
|
|
9299
10955
|
readmeView.source ? h(Badge, null, readmeView.source === 'npm' ? 'npm registry' : 'GitHub') : null),
|
|
10956
|
+
h(CommunityPresence, {
|
|
10957
|
+
call,
|
|
10958
|
+
repo: readmeView.item && readmeView.item.repo,
|
|
10959
|
+
owner: readmeView.item ? parseGithubRepoKey(readmeView.item.repo).split('/')[0] : '',
|
|
10960
|
+
compact: true,
|
|
10961
|
+
}),
|
|
10962
|
+
// README 可能很长:出口放在正文之前,打开即点得到。
|
|
10963
|
+
readmeView.item ? h(FeedbackLinks, {
|
|
10964
|
+
item: readmeView.item, kind: 'plugin',
|
|
10965
|
+
repo: readmeView.item.repo, homepage: readmeView.item.homepage,
|
|
10966
|
+
sourceLabel: readmeView.item.repo ? String(readmeView.item.repo).replace(/^https?:\/\/github\.com\//i, '') : '',
|
|
10967
|
+
}) : null,
|
|
9300
10968
|
readmeView.loading
|
|
9301
10969
|
? h('p', { className: 'dsh-mcp-wb-modal-note', role: 'status' }, t('正在读取 README…'))
|
|
9302
10970
|
: readmeView.ok
|
|
@@ -9306,7 +10974,23 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9306
10974
|
h('div', { className: 'dsh-mcp-modal-actions' },
|
|
9307
10975
|
h(Btn, { onClick: closeReadme, children: t('关闭') }),
|
|
9308
10976
|
readmeView.item && readmeView.item.npm ? h(Btn, { primary: true, icon: 'plus', disabled: busy || isInstallRunning(readmeView.item.name), onClick: () => { const target = readmeView.item; closeReadme(); installPlugin(target); } }, t('安装')) : null,
|
|
9309
|
-
h(Btn, { onClick: () => loadAwesomeReadme(readmeView.item, true), disabled: readmeView.loading, children: t('重新读取') }))
|
|
10977
|
+
h(Btn, { onClick: () => loadAwesomeReadme(readmeView.item, true), disabled: readmeView.loading, children: t('重新读取') })),
|
|
10978
|
+
// README 可能很长:右下角常驻「跳到末尾」。sticky 让它不随正文滚走,又留在
|
|
10979
|
+
// 弹层内部(焦点陷阱、Esc 与点遮罩关闭都不受影响);外层不拦鼠标,只有按钮可点。
|
|
10980
|
+
readmeView.ok
|
|
10981
|
+
? h('div', { className: 'dsh-mcp-readme-jump-wrap' },
|
|
10982
|
+
h('button', {
|
|
10983
|
+
type: 'button',
|
|
10984
|
+
className: 'dsh-mcp-readme-jump',
|
|
10985
|
+
title: t('滑动到 README 末尾'),
|
|
10986
|
+
'aria-label': t('滑动到 README 末尾'),
|
|
10987
|
+
onClick: () => {
|
|
10988
|
+
const node = readmeNodeRef.current;
|
|
10989
|
+
if (!node) return;
|
|
10990
|
+
node.scrollTo({ top: node.scrollHeight, behavior: 'smooth' });
|
|
10991
|
+
},
|
|
10992
|
+
}, h(Icon, { d: ICONS.down, size: 12 }), h('span', null, t('跳到末尾'))))
|
|
10993
|
+
: null))
|
|
9310
10994
|
: null,
|
|
9311
10995
|
// ── T04/R7:令牌弹窗(连接器 mcp.json 里的 ${...} 占位符)──
|
|
9312
10996
|
// 设计约束:只收集用户输入并原样透传;槽位来自目录条目的 tokenSlots,
|
|
@@ -9349,7 +11033,7 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9349
11033
|
);
|
|
9350
11034
|
})()
|
|
9351
11035
|
: null,
|
|
9352
|
-
tab === 'expert' ? h(ExpertMarket, { call }) : null,
|
|
11036
|
+
tab === 'expert' ? h(ExpertMarket, { call, key: 'expert-' + expertEpoch }) : null,
|
|
9353
11037
|
// ── R8:OAuth 授权弹层(401 → 浏览器授权 → 自动重装)──
|
|
9354
11038
|
authDialog
|
|
9355
11039
|
? h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && closeAuthDialog() },
|
|
@@ -9422,7 +11106,18 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9422
11106
|
connectorDetail
|
|
9423
11107
|
? h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && setConnectorDetail(null) },
|
|
9424
11108
|
h('div', { ref: connectorDetailRef, className: 'dsh-mcp-modal dsh-mcp-wb-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('连接器详情'), tabIndex: -1, onKeyDown: (e) => { if (e.key === 'Escape' && !busy) { e.stopPropagation(); setConnectorDetail(null); } } },
|
|
9425
|
-
|
|
11109
|
+
// 头像行与技能详情同款(detail-person):图标 + 名称并排。
|
|
11110
|
+
h('div', { className: 'dsh-mcp-detail-person' },
|
|
11111
|
+
h(ConnectorAvatar, {
|
|
11112
|
+
call, connectorId: connectorDetail.id, name: connectorDetail.name,
|
|
11113
|
+
className: 'dsh-mcp-detail-avatar',
|
|
11114
|
+
}),
|
|
11115
|
+
h('div', null, h('h3', null, connectorDetail.name)),
|
|
11116
|
+
),
|
|
11117
|
+
h(FeedbackLinks, {
|
|
11118
|
+
item: connectorDetail, kind: 'connector',
|
|
11119
|
+
sourceLabel: t('来源:WorkBuddy 官方分发'),
|
|
11120
|
+
}),
|
|
9426
11121
|
// T02/R3:详情正文同样 examples 优先,description 兜底。
|
|
9427
11122
|
connectorDetail.displayBody || connectorDetail.description
|
|
9428
11123
|
? h('p', { className: 'dsh-mcp-wb-modal-desc' }, connectorDetail.displayBody || connectorDetail.description) : null,
|
|
@@ -9456,50 +11151,33 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9456
11151
|
),
|
|
9457
11152
|
)
|
|
9458
11153
|
: null,
|
|
9459
|
-
// ── T03/R5:插件详情弹层(repository/homepage/lspServers 全量透出;X7 lspServers 只展示)──
|
|
9460
|
-
pluginDetail
|
|
9461
|
-
? h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && setPluginDetail(null) },
|
|
9462
|
-
h('div', { ref: pluginDetailRef, className: 'dsh-mcp-modal dsh-mcp-wb-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('插件详情'), tabIndex: -1, onKeyDown: (e) => { if (e.key === 'Escape' && !busy) { e.stopPropagation(); setPluginDetail(null); } } },
|
|
9463
|
-
h('h3', null, pluginDetail.name),
|
|
9464
|
-
pluginDetail.description ? h('p', { className: 'dsh-mcp-wb-modal-desc' }, pluginDetail.description) : null,
|
|
9465
|
-
pluginDetail.detailError ? h('p', { className: 'dsh-mcp-wb-error' }, pluginDetail.detailError) : null,
|
|
9466
|
-
h('div', { className: 'dsh-mcp-card-tags' },
|
|
9467
|
-
pluginDetail.version ? h(Badge, null, 'v' + pluginDetail.version) : null,
|
|
9468
|
-
pluginDetail.author ? h(Badge, null, pluginDetail.author) : null,
|
|
9469
|
-
pluginDetail.license ? h(Badge, null, pluginDetail.license) : null,
|
|
9470
|
-
),
|
|
9471
|
-
(pluginDetail.dependencies || []).length
|
|
9472
|
-
? h('div', { className: 'dsh-mcp-wb-examples' },
|
|
9473
|
-
h('b', null, t('依赖')),
|
|
9474
|
-
h('ul', null, pluginDetail.dependencies.map((dep) => h('li', { key: dep.name }, dep.name + (dep.version ? t('({0})', [dep.version]) : '')))),
|
|
9475
|
-
)
|
|
9476
|
-
: null,
|
|
9477
|
-
pluginDetail.lspServers
|
|
9478
|
-
? h('p', { className: 'dsh-mcp-wb-modal-note' },
|
|
9479
|
-
t('本插件声明了 {0} 个 LSP 服务,安装后由宿主按声明启动(此处仅展示)。', [Object.keys(pluginDetail.lspServers).length]))
|
|
9480
|
-
: null,
|
|
9481
|
-
pluginDetail.repository || pluginDetail.homepage
|
|
9482
|
-
? h('p', { className: 'dsh-mcp-wb-modal-note' },
|
|
9483
|
-
pluginDetail.repository ? t('来源:') + pluginDetail.repository + ' ' : '',
|
|
9484
|
-
pluginDetail.homepage ? t('主页:') + pluginDetail.homepage : '')
|
|
9485
|
-
: null,
|
|
9486
|
-
h('p', { className: 'dsh-mcp-wb-modal-note' },
|
|
9487
|
-
t('安装 = 通过宿主 pluginManager 拉取并落盘;安装成功后需重启 DSH 宿主才会生效。')),
|
|
9488
|
-
h('div', { className: 'dsh-mcp-modal-actions' },
|
|
9489
|
-
h(Btn, { onClick: () => setPluginDetail(null), disabled: busy, children: t('关闭') }),
|
|
9490
|
-
pluginInstallSpec(pluginDetail)
|
|
9491
|
-
? h(Btn, { primary: true, icon: 'plus', disabled: busy || pluginDetail.loading || isInstallRunning(pluginDetail.name), onClick: () => installPlugin(pluginDetail), children: busy || isInstallRunning(pluginDetail.name) ? t('安装中…') : t('安装') })
|
|
9492
|
-
: h('span', { className: 'dsh-mcp-wb-blocked' }, t('该插件未提供可推导的安装来源(repository),暂不能安装')),
|
|
9493
|
-
),
|
|
9494
|
-
),
|
|
9495
|
-
)
|
|
9496
|
-
: null,
|
|
9497
11154
|
// ── 技能详情弹层:安全报告 + 发布方 + 安装/卸载 ──
|
|
9498
11155
|
skillDetail
|
|
9499
11156
|
? h('div', { className: 'dsh-mcp-overlay', onClick: (e) => !busy && e.target === e.currentTarget && setSkillDetail(null) },
|
|
9500
11157
|
h('div', { ref: skillDetailRef, className: 'dsh-mcp-modal dsh-mcp-wb-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('技能详情'), tabIndex: -1, onKeyDown: (e) => { if (e.key === 'Escape' && !busy) { e.stopPropagation(); setSkillDetail(null); } } },
|
|
9501
|
-
h('
|
|
9502
|
-
|
|
11158
|
+
h('div', { className: 'dsh-mcp-detail-person' },
|
|
11159
|
+
h(Avatar, {
|
|
11160
|
+
url: skillDetail.iconUrl || githubAvatarUrl(skillDetail.homepage, 88),
|
|
11161
|
+
name: skillDetail.ownerName || skillDetail.publisher || skillDetail.namespace,
|
|
11162
|
+
className: 'dsh-mcp-detail-avatar',
|
|
11163
|
+
}),
|
|
11164
|
+
h('div', null,
|
|
11165
|
+
h('h3', null, skillDetail.name),
|
|
11166
|
+
h('div', { className: 'dsh-mcp-card-vendor' }, skillDetail.canonical || skillDetail.namespace || ''),
|
|
11167
|
+
),
|
|
11168
|
+
),
|
|
11169
|
+
h(CommunityPresence, {
|
|
11170
|
+
call, repo: skillDetail.homepage || skillDetail.upstreamUrl,
|
|
11171
|
+
owner: skillDetail.ownerName || skillDetail.publisher,
|
|
11172
|
+
avatar: skillDetail.iconUrl,
|
|
11173
|
+
updatedAt: skillDetail.updatedAt,
|
|
11174
|
+
}),
|
|
11175
|
+
h(FeedbackLinks, {
|
|
11176
|
+
item: skillDetail, kind: 'skill',
|
|
11177
|
+
repo: parseGithubRepoKey(skillDetail.homepage || skillDetail.upstreamUrl) ? (skillDetail.homepage || skillDetail.upstreamUrl) : '',
|
|
11178
|
+
homepage: skillDetail.homepage || skillDetail.upstreamUrl,
|
|
11179
|
+
sourceLabel: skillDetail.ownerName || skillDetail.publisher || t('SkillHub'),
|
|
11180
|
+
}),
|
|
9503
11181
|
skillDetail.loading
|
|
9504
11182
|
? h('p', { className: 'dsh-mcp-wb-modal-note' }, t('正在读取详情…'))
|
|
9505
11183
|
: h('div', null,
|
|
@@ -9509,6 +11187,8 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9509
11187
|
skillDetail.version ? h(Badge, null, 'v' + skillDetail.version) : null,
|
|
9510
11188
|
skillDetail.publisher ? h(Badge, null, skillDetail.publisher + (skillDetail.verifiedPublisher ? t('(已认证)') : '')) : null,
|
|
9511
11189
|
skillDetail.certifiedName ? h(Badge, { cls: 's-on' }, skillDetail.certifiedName) : null,
|
|
11190
|
+
skillDetail.createdAt ? h(Badge, null, t('加入时间 {0}', [wbFormatDate(skillDetail.createdAt)])) : null,
|
|
11191
|
+
skillDetail.updatedAt ? h(Badge, null, t('更新于 {0}', [relativeLabel(skillDetail.updatedAt)])) : null,
|
|
9512
11192
|
Array.isArray(skillDetail.security) ? skillDetail.security.map((status, index) => h(Badge, { key: index, cls: status === 'benign' ? 's-on' : '' }, t('安全: ') + status)) : null,
|
|
9513
11193
|
),
|
|
9514
11194
|
skillDetail.changelog ? h('p', { className: 'dsh-mcp-wb-modal-note' }, t('最新版本说明:') + skillDetail.changelog) : null,
|
|
@@ -9524,6 +11204,14 @@ const WorkBuddyMarket = ({ call, pluginManager }) => {
|
|
|
9524
11204
|
),
|
|
9525
11205
|
)
|
|
9526
11206
|
: null,
|
|
11207
|
+
// 装备清单:导出 / 导入对比。只读,不自动安装。
|
|
11208
|
+
loadoutOpen
|
|
11209
|
+
? h(LoadoutModal, {
|
|
11210
|
+
call,
|
|
11211
|
+
onClose: () => setLoadoutOpen(false),
|
|
11212
|
+
onLocate: locateLoadoutRow,
|
|
11213
|
+
})
|
|
11214
|
+
: null,
|
|
9527
11215
|
);
|
|
9528
11216
|
};
|
|
9529
11217
|
|
|
@@ -10123,7 +11811,7 @@ const McpBoardPage = (props) => {
|
|
|
10123
11811
|
// '/' 聚焦搜索框:宿主会话页也把 '/' 当命令入口,所以这里必须避开输入态,
|
|
10124
11812
|
// 并且只在事件目标不在可编辑元素内时才接管。
|
|
10125
11813
|
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey && !isTyping(e.target)) {
|
|
10126
|
-
const box = document.querySelector('.dsh-mcp-page .dsh-mcp-search input, .dsh-mcp-page .dsh-mcp-tools-search input, .dsh-mcp-page .dsh-mcp-wb-search input');
|
|
11814
|
+
const box = document.querySelector('.dsh-mcp-page .dsh-mcp-search input, .dsh-mcp-page .dsh-mcp-tools-search input, .dsh-mcp-page .dsh-mcp-wb-search input, .dsh-mcp-page .dsh-mcp-skill-search input');
|
|
10127
11815
|
if (box) { e.preventDefault(); box.focus(); box.select?.(); }
|
|
10128
11816
|
}
|
|
10129
11817
|
};
|
|
@@ -10133,38 +11821,43 @@ const McpBoardPage = (props) => {
|
|
|
10133
11821
|
|
|
10134
11822
|
return h('div', { className: 'dsh-mcp-page' },
|
|
10135
11823
|
h('div', { className: 'dsh-mcp-wrap' },
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
11824
|
+
// 分类栏与右端「点 Star」共行。star 刻意挂在 role=tablist **之外**:
|
|
11825
|
+
// tablist 里只该有 tab,把非 tab 控件塞进去会让读屏器的 tab 计数出错。
|
|
11826
|
+
h('div', { className: 'dsh-mcp-cats-row' },
|
|
11827
|
+
h('div', { className: 'dsh-mcp-cats', role: 'tablist', 'aria-label': t('看板分类'), ref: catsRef },
|
|
11828
|
+
BOARD_CATEGORIES.map((cat) =>
|
|
11829
|
+
h('button', {
|
|
11830
|
+
key: cat.key,
|
|
11831
|
+
type: 'button',
|
|
11832
|
+
role: 'tab',
|
|
11833
|
+
'aria-pressed': category === cat.key,
|
|
11834
|
+
'aria-selected': category === cat.key,
|
|
11835
|
+
className: 'dsh-mcp-cat',
|
|
11836
|
+
onClick: () => setCategory(cat.key),
|
|
11837
|
+
// §1.1 hover 摘要替代原来的 title 长文案:title 要等一秒、不可扫读、
|
|
11838
|
+
// 移动端根本没有。摘要用同一份数据渲染成两三行事实,鼠标停住就能读。
|
|
11839
|
+
onMouseEnter: (e) => showCatHint(cat.key, e),
|
|
11840
|
+
onMouseLeave: hideCatHint,
|
|
11841
|
+
onFocus: (e) => showCatHint(cat.key, e),
|
|
11842
|
+
onBlur: hideCatHint,
|
|
11843
|
+
},
|
|
11844
|
+
h(Icon, { d: ICONS[cat.icon], size: 14 }),
|
|
11845
|
+
cat.label,
|
|
11846
|
+
cat.key === 'mcp' && mcpCount !== null ? h('span', { className: 'dsh-mcp-cat-count' }, mcpCount) : null,
|
|
11847
|
+
cat.key === 'skill' && skillCount !== null ? h('span', { className: 'dsh-mcp-cat-count' }, skillCount) : null,
|
|
11848
|
+
cat.key === 'tailor' && tailorReport && typeof tailorReport.deadMcpCount === 'number' ? h('span', { className: 'dsh-mcp-cat-count' }, tailorReport.deadMcpCount) : null,
|
|
11849
|
+
cat.key === 'expert' && expertCount !== null ? h('span', { className: 'dsh-mcp-cat-count' }, expertCount) : null,
|
|
11850
|
+
cat.key === 'wb' && mkCount !== null ? h('span', { className: 'dsh-mcp-cat-count' }, mkCount) : null,
|
|
11851
|
+
// 异常红点:只在「这一页里有需要处理的东西」时出现,不参与布局也不改宽度。
|
|
11852
|
+
// 裁缝页的死 MCP 用 warn(橙)——它只是白交税,不是错误;
|
|
11853
|
+
// 若无死工具则完全不点,避免"每个标签都带点"导致红点失效。
|
|
11854
|
+
cat.key === 'tailor' && tailorReport && typeof tailorReport.deadMcpCount === 'number' && tailorReport.deadMcpCount > 0
|
|
11855
|
+
? h('span', { className: 'dsh-mcp-cat-alert warn', 'aria-hidden': true })
|
|
11856
|
+
: null,
|
|
11857
|
+
)),
|
|
11858
|
+
catHint ? h(CatHintPopover, { hint: catHint, category, counts: { mcp: mcpCount, skill: skillCount, expert: expertCount, tailor: tailorReport, wb: mkCount } }) : null,
|
|
11859
|
+
),
|
|
11860
|
+
h(RepoStarChip, { call }),
|
|
10168
11861
|
),
|
|
10169
11862
|
// OPT-07:每个分类各自包一层错误边界。key 用分类名,切走再切回会重建边界,
|
|
10170
11863
|
// 顺带让「重试」之外还有一条自然恢复路径。
|
|
@@ -10575,8 +12268,6 @@ const MCP_CONTRIBUTION = {
|
|
|
10575
12268
|
// tools 收 payload 而不是裸 name:项目 MCP 必须带 wsPath 才能精确定位(两个项目可能同名)。
|
|
10576
12269
|
// 声明成标量 name 时,网关会把传入对象当成 args 映射展开并报 unexpected "wsPath"。
|
|
10577
12270
|
{ id: 'dsh-armory#mcpManager/tools', service: 'mcpManager', namespace: 'mcpManager', method: 'tools', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('ToolsResult') },
|
|
10578
|
-
{ id: 'dsh-armory#mcpManager/builtins', service: 'mcpManager', namespace: 'mcpManager', method: 'builtins', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('BuiltinCatalogResult') },
|
|
10579
|
-
{ id: 'dsh-armory#mcpManager/installBuiltins', service: 'mcpManager', namespace: 'mcpManager', method: 'installBuiltins', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'BuiltinInstallPayload')], result: mcpCodec('BuiltinInstallResult') },
|
|
10580
12271
|
{ id: 'dsh-armory#mcpManager/add', service: 'mcpManager', namespace: 'mcpManager', method: 'add', invocation: { kind: 'direct' }, parameters: [mcpParam('spec', 'Spec')], result: mcpCodec('NoteResult') },
|
|
10581
12272
|
{ id: 'dsh-armory#mcpManager/update', service: 'mcpManager', namespace: 'mcpManager', method: 'update', invocation: { kind: 'direct' }, parameters: [mcpParam('spec', 'Spec')], result: mcpCodec('NoteResult') },
|
|
10582
12273
|
{ id: 'dsh-armory#mcpManager/removeServer', service: 'mcpManager', namespace: 'mcpManager', method: 'removeServer', invocation: { kind: 'direct' }, parameters: [mcpParam('name', 'String')], result: mcpCodec('NoteResult') },
|
|
@@ -10592,7 +12283,6 @@ const MCP_CONTRIBUTION = {
|
|
|
10592
12283
|
{ id: 'dsh-armory#mcpManager/updateWorkspaceServer', service: 'mcpManager', namespace: 'mcpManager', method: 'updateWorkspaceServer', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10593
12284
|
{ id: 'dsh-armory#mcpManager/removeWorkspaceServer', service: 'mcpManager', namespace: 'mcpManager', method: 'removeWorkspaceServer', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10594
12285
|
{ id: 'dsh-armory#mcpManager/setWorkspaceExclude', service: 'mcpManager', namespace: 'mcpManager', method: 'setWorkspaceExclude', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10595
|
-
{ id: 'dsh-armory#mcpManager/installWorkspaceBuiltins', service: 'mcpManager', namespace: 'mcpManager', method: 'installWorkspaceBuiltins', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'BuiltinInstallPayload')], result: mcpCodec('BuiltinInstallResult') },
|
|
10596
12286
|
{ id: 'dsh-armory#mcpManager/previewWorkspaceImport', service: 'mcpManager', namespace: 'mcpManager', method: 'previewWorkspaceImport', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('ImportPreview') },
|
|
10597
12287
|
{ id: 'dsh-armory#mcpManager/importWorkspaceJson', service: 'mcpManager', namespace: 'mcpManager', method: 'importWorkspaceJson', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('ImportResult') },
|
|
10598
12288
|
{ id: 'dsh-armory#mcpManager/revealWorkspaceServer', service: 'mcpManager', namespace: 'mcpManager', method: 'revealWorkspaceServer', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('RevealResult') },
|
|
@@ -10621,6 +12311,8 @@ const MCP_CONTRIBUTION = {
|
|
|
10621
12311
|
// 顶栏「市场」chip 常驻计数:只读磁盘缓存(零网络),无缓存返回 { total: null }。
|
|
10622
12312
|
{ id: 'dsh-armory#mcpManager/wbConnectorCachedCount', service: 'mcpManager', namespace: 'mcpManager', method: 'wbConnectorCachedCount', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbCachedCountResult') },
|
|
10623
12313
|
{ id: 'dsh-armory#mcpManager/wbConnectorPreview', service: 'mcpManager', namespace: 'mcpManager', method: 'wbConnectorPreview', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('WbPreviewResult') },
|
|
12314
|
+
// 市场连接器卡片头像:payload {connectorId},出 {ok, dataUrl}。与宿主 method、typert.js 三处同步。
|
|
12315
|
+
{ id: 'dsh-armory#mcpManager/wbConnectorIcon', service: 'mcpManager', namespace: 'mcpManager', method: 'wbConnectorIcon', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('WbIconResult') },
|
|
10624
12316
|
{ id: 'dsh-armory#mcpManager/wbConnectorInstall', service: 'mcpManager', namespace: 'mcpManager', method: 'wbConnectorInstall', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10625
12317
|
{ id: 'dsh-armory#mcpManager/wbOAuthBegin', service: 'mcpManager', namespace: 'mcpManager', method: 'wbOAuthBegin', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10626
12318
|
{ id: 'dsh-armory#mcpManager/wbOAuthPoll', service: 'mcpManager', namespace: 'mcpManager', method: 'wbOAuthPoll', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
@@ -10636,20 +12328,22 @@ const MCP_CONTRIBUTION = {
|
|
|
10636
12328
|
// T03/R4:多市场注册表(读 / 增改)。与宿主 method、typert.js 三处必须同步。
|
|
10637
12329
|
{ id: 'dsh-armory#mcpManager/wbMarketRegistry', service: 'mcpManager', namespace: 'mcpManager', method: 'wbMarketRegistry', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbMarketRegistryResult') },
|
|
10638
12330
|
{ id: 'dsh-armory#mcpManager/wbMarketRegistryUpsert', service: 'mcpManager', namespace: 'mcpManager', method: 'wbMarketRegistryUpsert', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('WbMarketRegistryResult') },
|
|
10639
|
-
// T03/R5:codebuddy 插件市场(列表 / 详情)。与宿主 method、typert.js 三处必须同步。
|
|
10640
|
-
{ id: 'dsh-armory#mcpManager/wbPluginCatalog', service: 'mcpManager', namespace: 'mcpManager', method: 'wbPluginCatalog', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbPluginCatalogResult') },
|
|
10641
12331
|
// T04/R8:安装状态账本(读全量 / 登记 / 忘记)。与宿主 method、typert.js 三处必须同步。
|
|
10642
12332
|
{ id: 'dsh-armory#mcpManager/wbInstallState', service: 'mcpManager', namespace: 'mcpManager', method: 'wbInstallState', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('InstallStateResult') },
|
|
10643
12333
|
{ id: 'dsh-armory#mcpManager/wbInstallStateRecord', service: 'mcpManager', namespace: 'mcpManager', method: 'wbInstallStateRecord', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10644
12334
|
{ id: 'dsh-armory#mcpManager/wbInstallStateForget', service: 'mcpManager', namespace: 'mcpManager', method: 'wbInstallStateForget', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
10645
|
-
// T05/R9:插件市场更新探测(只读 ETag,绝不落新包)。与宿主 method、typert.js 三处必须同步。
|
|
10646
|
-
{ id: 'dsh-armory#mcpManager/wbPluginUpdateCheck', service: 'mcpManager', namespace: 'mcpManager', method: 'wbPluginUpdateCheck', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbUpdateHintResult') },
|
|
10647
12335
|
// T06/Awesome:DSH 社区插件市场(awesome-dsh-plugins)。与宿主 method、typert.js 三处必须同步。
|
|
10648
12336
|
{ id: 'dsh-armory#mcpManager/wbAwesomeCatalog', service: 'mcpManager', namespace: 'mcpManager', method: 'wbAwesomeCatalog', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('AwesomeCatalogResult') },
|
|
10649
12337
|
{ id: 'dsh-armory#mcpManager/wbAwesomeDetail', service: 'mcpManager', namespace: 'mcpManager', method: 'wbAwesomeDetail', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('AwesomeDetailResult') },
|
|
10650
12338
|
{ id: 'dsh-armory#mcpManager/wbAwesomeReadme', service: 'mcpManager', namespace: 'mcpManager', method: 'wbAwesomeReadme', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('AwesomeReadmeResult') },
|
|
10651
12339
|
{ id: 'dsh-armory#mcpManager/wbAwesomeUpdateCheck', service: 'mcpManager', namespace: 'mcpManager', method: 'wbAwesomeUpdateCheck', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbUpdateHintResult') },
|
|
10652
|
-
|
|
12340
|
+
// 社区元信息(仓库维护信号):payload {repos, refresh},返回 {repos:{repo:{...}}}。
|
|
12341
|
+
{ id: 'dsh-armory#mcpManager/wbRepoMeta', service: 'mcpManager', namespace: 'mcpManager', method: 'wbRepoMeta', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('WbRepoMetaResult') },
|
|
12342
|
+
// npm 近 30 天下载量:payload {packages, refresh},返回 {packages:{pkg:{ok,downloads,...}}}。
|
|
12343
|
+
// 缺这一行 = 面板 call('wbNpmDownloads') 恒 404,卡片上那枚「↓」永远不出现。
|
|
12344
|
+
{ id: 'dsh-armory#mcpManager/wbNpmDownloads', service: 'mcpManager', namespace: 'mcpManager', method: 'wbNpmDownloads', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('WbNpmDownloadsResult') },
|
|
12345
|
+
{ id: 'dsh-armory#mcpManager/wbLoadoutExport', service: 'mcpManager', namespace: 'mcpManager', method: 'wbLoadoutExport', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('LoadoutResult') },
|
|
12346
|
+
{ id: 'dsh-armory#mcpManager/wbLoadoutDiff', service: 'mcpManager', namespace: 'mcpManager', method: 'wbLoadoutDiff', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('LoadoutDiffResult') },
|
|
10653
12347
|
// ── WorkBuddy 专家直连:召唤预设 + 捆绑技能(同一命名空间同一信封)──
|
|
10654
12348
|
{ id: 'dsh-armory#mcpManager/wbExpertCatalog', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertCatalog', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbExpertCatalogResult') },
|
|
10655
12349
|
{ id: 'dsh-armory#mcpManager/wbExpertDetail', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertDetail', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('WbExpertDetailResult') },
|