dsh-work-components 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/NOTICE +49 -0
- package/README.md +159 -2
- package/cordis.patch.yml +5 -0
- package/lib/client.js +2979 -0
- package/lib/index.mjs +6682 -0
- package/office/launch.py +41 -0
- package/package.json +46 -4
- package/vendor/dsh-tools/LICENSE +21 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2979 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-workbench",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
/**
|
|
7
|
+
* dsh-workbench — 客户端半边(手写,无需构建)。
|
|
8
|
+
* 1. settings.section「工作组件」设置页:首页是功能列表(按「多模态 / 工作组件 / 通用 / 基础工具」分组,每行显示实时状态),
|
|
9
|
+
* 点击一行进入该功能的管理页(运行状态、下载安装 + 启用旋钮、安装日志、配置),「‹ 返回」或 Esc 回到列表。
|
|
10
|
+
* 「启用」旋钮和安装按钮在同一行靠右,拨动立即保存(只改这一项)。列表行和管理页标题前带通用线条图标(内置 SVG,不用第三方标志)。
|
|
11
|
+
* 组件状态:未安装 / 未启动 / 已启动(MCP 服务器已挂载)/ 已连接(对应程序在运行且 MCP 够得着它,绿色实心徽标)/ 出错;
|
|
12
|
+
* 管理页状态行下面的连接行给出探测细节(程序未运行 / 未连接的原因)。有组件在探测连接时每 5 秒刷新一次状态。
|
|
13
|
+
* 工作组件:Office / Blender / Unity / Godot(Python,uv 安装)、Figma / Photoshop / Chrome(npm,Node.js 安装)。
|
|
14
|
+
* 组件可带安装行下面的说明(/components 里的 note,如 Godot 要装哪个版本的插件)和字段旁的动作按钮
|
|
15
|
+
* (Godot「安装插件到项目」:POST /dsh-workbench/api/components/godot/addon { project },成功后记住项目路径)。
|
|
16
|
+
* 「通用」:会话控制、uv、Node.js、下载代理(共享工具,与各工作组件并列成行,不埋在基础工具脚注里)。
|
|
17
|
+
* 「基础工具」:添加工作组件——填写程序名称,复制给 AI 的接入提示词(只加可下载列表,不装进 tools/)。
|
|
18
|
+
* 读写 /dsh-workbench/api/settings,状态来自 /dsh-workbench/api/components,
|
|
19
|
+
* 「下载安装」调用 POST /dsh-workbench/api/components/<id>/install,安装期间轮询状态(不论当前在哪一页)。
|
|
20
|
+
* 2. 多模态 MmCard:运行中 `tool.call.toolview` 只显示 pending/sending;settled 后完整卡片挂在
|
|
21
|
+
* `conversation.chat.turnTail`(与 deliverables 同属 Host 产物表面),过程折叠后仍可见。
|
|
22
|
+
* 数据经 ConversationNodeDefinition 写入 turn data `mmCards`(presentationMeta.mm / image 合成);
|
|
23
|
+
* model 历史仍只含 text+image,不得含 type:mm。loadImage 经 inject uiConversation.imageUrl。
|
|
24
|
+
* 所有网络请求都是同源 fetch 到宿主,插件前端不直连外部资源(CSP)。
|
|
25
|
+
*/
|
|
26
|
+
var React = require("react");
|
|
27
|
+
var h = React.createElement;
|
|
28
|
+
var createPortal = React.createPortal;
|
|
29
|
+
if (typeof createPortal !== "function") {
|
|
30
|
+
try { createPortal = require("react-dom").createPortal; } catch (_) { createPortal = null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var API = "/dsh-workbench/api";
|
|
34
|
+
|
|
35
|
+
var STYLES = [
|
|
36
|
+
".mm-page{font-size:13px;line-height:1.6;padding:14px 16px;max-width:640px}",
|
|
37
|
+
".mm-page [hidden]{display:none!important}",
|
|
38
|
+
".mm-page h3{margin:0 0 4px;font-size:14px}",
|
|
39
|
+
".mm-page .mm-sub{color:var(--theme-text-secondary,#888);font-size:12px;margin:0 0 14px}",
|
|
40
|
+
".mm-field{display:flex;flex-direction:column;gap:4px;margin-bottom:12px}",
|
|
41
|
+
".mm-field label{font-weight:600;font-size:12px}",
|
|
42
|
+
".mm-field .mm-desc{color:var(--theme-text-secondary,#888);font-size:11px}",
|
|
43
|
+
".mm-input{background:var(--theme-input-bg,#111);color:var(--theme-text,#ddd);border:1px solid var(--theme-border,#333);border-radius:6px;padding:6px 8px;font-size:12px}",
|
|
44
|
+
".mm-row{display:flex;gap:8px;align-items:center;margin-top:6px}",
|
|
45
|
+
".mm-btn{background:var(--theme-accent,#4a9eff);color:#fff;border:none;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:12px}",
|
|
46
|
+
".mm-btn:disabled{opacity:.45;cursor:not-allowed}",
|
|
47
|
+
".mm-msg{font-size:12px;color:var(--theme-text-secondary,#888)}",
|
|
48
|
+
".mm-msg.err{color:#e55}",
|
|
49
|
+
".mm-section{margin:18px 0 6px;padding-top:12px;border-top:1px solid var(--theme-border,#333);font-weight:600;font-size:13px}",
|
|
50
|
+
".mm-status{font-size:12px;margin:-4px 0 12px;word-break:break-all}",
|
|
51
|
+
".mm-status.on{color:#3a3}",
|
|
52
|
+
".mm-status.error{color:#e55}",
|
|
53
|
+
".mm-status.off{color:var(--theme-text-secondary,#888)}",
|
|
54
|
+
".mm-status.missing{color:#d93}",
|
|
55
|
+
".mm-status.ready{color:#3a3}",
|
|
56
|
+
".mm-status.connected{color:#2da44e;font-weight:600}",
|
|
57
|
+
// 连接行(管理页状态行下面):已连接 / 程序未运行 / 未连接 / MCP 未就绪 / 正在检查
|
|
58
|
+
".mm-conn{position:relative;font-size:12px;margin:-8px 0 12px;padding-left:14px;word-break:break-all;color:var(--theme-text-secondary,#888)}",
|
|
59
|
+
".mm-conn::before{content:\"\";position:absolute;left:1px;top:.55em;width:7px;height:7px;border-radius:50%;background:currentColor}",
|
|
60
|
+
".mm-conn.connected{color:#2da44e}",
|
|
61
|
+
".mm-conn.no-app,.mm-conn.unreachable{color:#d93}",
|
|
62
|
+
".mm-conn.mcp-down{color:#e55}",
|
|
63
|
+
".mm-install{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:-6px 0 8px}",
|
|
64
|
+
".mm-install .mm-istate{flex-basis:100%}", // 安装步骤单独一行,按钮行不挤
|
|
65
|
+
".mm-btn.mm-ghost{background:transparent;color:var(--theme-text,#ddd);border:1px solid var(--theme-border,#333)}",
|
|
66
|
+
".mm-istate{font-size:12px;color:var(--theme-text-secondary,#888);word-break:break-all}",
|
|
67
|
+
".mm-istate.installing,.mm-istate.queued{color:var(--theme-accent,#4a9eff)}",
|
|
68
|
+
".mm-istate.done{color:#3a3}",
|
|
69
|
+
".mm-istate.error{color:#e55}",
|
|
70
|
+
".mm-log{margin:0 0 12px;font-size:12px}",
|
|
71
|
+
".mm-log summary{cursor:pointer;color:var(--theme-text-secondary,#888)}",
|
|
72
|
+
".mm-log pre{max-height:180px;overflow:auto;margin:4px 0 0;padding:6px 8px;border-radius:6px;background:var(--theme-input-bg,#111);border:1px solid var(--theme-border,#333);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-all}",
|
|
73
|
+
// 功能列表(首页)
|
|
74
|
+
".mm-group{margin:16px 0 6px;font-size:12px;font-weight:600;color:var(--theme-text-secondary,#888)}",
|
|
75
|
+
".mm-list{border:1px solid var(--theme-border,#333);border-radius:8px;overflow:hidden}",
|
|
76
|
+
".mm-item{display:flex;align-items:center;gap:10px;padding:9px 12px;cursor:pointer;outline:none;user-select:none}",
|
|
77
|
+
".mm-item+.mm-item{border-top:1px solid var(--theme-border,#333)}",
|
|
78
|
+
".mm-item:hover{background:rgba(127,127,127,.12)}",
|
|
79
|
+
".mm-item:focus-visible{box-shadow:inset 0 0 0 2px var(--theme-accent,#4a9eff)}",
|
|
80
|
+
".mm-item-main{flex:1;min-width:0}",
|
|
81
|
+
".mm-item-name{font-weight:600;font-size:13px;display:inline-flex;align-items:center;gap:6px;min-width:0}",
|
|
82
|
+
".mm-item-desc{color:var(--theme-text-secondary,#888);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
83
|
+
".mm-chev{flex:none;color:var(--theme-text-secondary,#888);font-size:18px;line-height:1}",
|
|
84
|
+
".mm-badge{flex:none;font-size:11px;line-height:18px;padding:0 8px;border-radius:9px;border:1px solid currentColor;white-space:nowrap;color:var(--theme-text-secondary,#888)}",
|
|
85
|
+
".mm-badge.on,.mm-badge.ready{color:#3a3}",
|
|
86
|
+
".mm-badge.connected{color:#fff;background:#2da44e;border-color:#2da44e;font-weight:600}",
|
|
87
|
+
".mm-badge.error{color:#e55}",
|
|
88
|
+
".mm-badge.missing{color:#d93}",
|
|
89
|
+
".mm-badge.busy{color:var(--theme-accent,#4a9eff)}",
|
|
90
|
+
// 功能管理页
|
|
91
|
+
".mm-dhead{display:flex;align-items:center;gap:10px;margin:0 0 12px}",
|
|
92
|
+
".mm-dhead h3{margin:0;flex:1;min-width:0;display:flex;align-items:center;gap:8px}",
|
|
93
|
+
".mm-dtitle{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
94
|
+
// 上游仓库外链(标题旁蓝色网址文字,新标签打开)
|
|
95
|
+
".mm-ext{flex:none;display:inline;max-width:min(42vw,280px);margin:0;padding:0;border:none;border-radius:0;color:#3b82f6;text-decoration:none;font-weight:400;font-size:12px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:baseline}",
|
|
96
|
+
".mm-ext:hover,.mm-ext:focus-visible{color:#2563eb;text-decoration:underline}",
|
|
97
|
+
".mm-ext:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}",
|
|
98
|
+
".mm-item-name .mm-ext{font-size:11px;max-width:min(36vw,220px)}",
|
|
99
|
+
".mm-dhead .mm-ext{font-size:12px;max-width:min(40vw,260px)}",
|
|
100
|
+
".mm-btn.mm-back{padding:3px 10px}",
|
|
101
|
+
// 应用图标
|
|
102
|
+
".mm-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px}",
|
|
103
|
+
".mm-ico svg{display:block;width:100%;height:100%;overflow:visible}",
|
|
104
|
+
".mm-dhead .mm-ico{width:22px;height:22px}",
|
|
105
|
+
// 「启用」旋钮(button role=switch,和安装按钮同一行靠右)
|
|
106
|
+
".mm-toggle{margin-left:auto;display:flex;align-items:center;gap:8px;min-width:0}",
|
|
107
|
+
".mm-toggle label{font-size:12px;cursor:pointer;user-select:none}",
|
|
108
|
+
".mm-tmsg{font-size:12px;color:var(--theme-text-secondary,#888);word-break:break-all}",
|
|
109
|
+
".mm-tmsg.err{color:#e55}",
|
|
110
|
+
".mm-tmsg:empty{display:none}",
|
|
111
|
+
".mm-switch{position:relative;flex:none;box-sizing:border-box;width:36px;height:20px;margin:0;padding:0;border:1px solid var(--theme-border,#555);border-radius:10px;background:rgba(127,127,127,.32);cursor:pointer;transition:background-color .15s,border-color .15s}",
|
|
112
|
+
".mm-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.4);transition:transform .15s}",
|
|
113
|
+
".mm-switch[aria-checked=true]{background:var(--theme-accent,#4a9eff);border-color:var(--theme-accent,#4a9eff)}",
|
|
114
|
+
".mm-switch[aria-checked=true]::after{transform:translateX(16px)}",
|
|
115
|
+
".mm-switch:focus-visible{outline:2px solid var(--theme-accent,#4a9eff);outline-offset:2px}",
|
|
116
|
+
".mm-switch:disabled{opacity:.45;cursor:not-allowed}",
|
|
117
|
+
".mm-switch[aria-busy=true]{cursor:progress}",
|
|
118
|
+
".mm-note{color:var(--theme-text-secondary,#888);font-size:11px;margin:-2px 0 10px}",
|
|
119
|
+
// 组件说明(安装行下面,如 Godot 插件版本)与字段旁的动作按钮(安装插件到项目)
|
|
120
|
+
".mm-cnote{font-size:11px;line-height:1.55;margin:-2px 0 12px;padding:6px 9px;border:1px solid var(--theme-border,#333);border-radius:6px;color:var(--theme-text-secondary,#888);word-break:break-all}",
|
|
121
|
+
".mm-action{flex-wrap:wrap;margin:2px 0 0}",
|
|
122
|
+
".mm-amsg{flex:1;min-width:0;font-size:12px;color:var(--theme-text-secondary,#888);word-break:break-all}",
|
|
123
|
+
".mm-amsg.ok{color:#3a3}",
|
|
124
|
+
".mm-amsg.err{color:#e55}",
|
|
125
|
+
".mm-amsg:empty{display:none}",
|
|
126
|
+
".mm-warn{font-size:12px;line-height:1.55;margin:0 0 14px;padding:8px 10px;border-radius:6px;border:1px solid #c9842a;background:rgba(201,132,42,.12);color:#e0a84a;word-break:break-word;white-space:pre-line}",
|
|
127
|
+
".mm-msg.ok{color:#3a3}",
|
|
128
|
+
".mm-src{flex:none;font-size:10px;line-height:16px;padding:0 6px;border-radius:8px;border:1px solid var(--theme-border,#555);color:var(--theme-text-secondary,#888);white-space:nowrap}",
|
|
129
|
+
".mm-src.bundled{color:#6a9;border-color:#6a9}",
|
|
130
|
+
".mm-src.local{color:#c9842a;border-color:#c9842a}",
|
|
131
|
+
".mm-src.experimental{color:#e0a84a;border-color:#c9842a;background:rgba(201,132,42,.12)}",
|
|
132
|
+
".mm-item-name .mm-src{margin-left:2px}",
|
|
133
|
+
".mm-dhead .mm-src{margin-left:4px}",
|
|
134
|
+
".mm-pr{margin:12px 0;padding:10px;border:1px solid var(--theme-border,#333);border-radius:8px;background:rgba(127,127,127,.06)}",
|
|
135
|
+
".mm-pr h4{margin:0 0 6px;font-size:12px}",
|
|
136
|
+
".mm-pr p{margin:0 0 8px;font-size:12px;color:var(--theme-text-secondary,#888);line-height:1.55}",
|
|
137
|
+
".mm-pr-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
|
|
138
|
+
".mm-pr pre{margin:8px 0 0;max-height:160px;overflow:auto;padding:6px 8px;border-radius:6px;background:var(--theme-input-bg,#111);border:1px solid var(--theme-border,#333);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-all}",
|
|
139
|
+
".mm-copy-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px}"
|
|
140
|
+
].join("\n");
|
|
141
|
+
|
|
142
|
+
// 设置项(按 key 引用)。type "switch" 是管理页里安装按钮行右侧的「启用」旋钮,拨动即保存,不进下方字段区。
|
|
143
|
+
var INPUT_FIELDS = [
|
|
144
|
+
{ key: "officeEnabled", label: "启用", type: "switch", desc: "仅 Windows。注意:其中 RunPython 工具会执行模型给出的任意 Python 代码。" },
|
|
145
|
+
{ key: "officeRepo", label: "OfficeMCP 仓库目录", type: "text", desc: "已「下载安装」时优先用插件 tools\\officemcp,这里可留空;否则用这里的目录,留空时找插件目录旁边的 officemcp。" },
|
|
146
|
+
{ key: "officeFolder", label: "工作根目录", type: "text", desc: "OfficeMCP 读写文件的根目录;留空时使用它的默认值 D:\\@OfficeMCP(没有 D 盘时用 文档\\OfficeMCP)。" },
|
|
147
|
+
{ key: "blenderEnabled", label: "启用", type: "switch", desc: "Blender 没开时会在后台反复重连,不影响其他功能。" },
|
|
148
|
+
{ key: "blenderPackage", label: "pip 包名", type: "text", desc: "默认 mcp-for-blender(入口命令同名)。改了包名后需要重新「下载安装」;未安装时用 uvx 临时运行。" },
|
|
149
|
+
{ key: "unityEnabled", label: "启用", type: "switch", desc: "Unity 没开时会在后台反复重连,不影响其他功能。" },
|
|
150
|
+
{ key: "unityPackage", label: "pip 包名", type: "text", desc: "默认 mcpforunityserver(入口命令 mcp-for-unity)。改了包名后需要重新「下载安装」;未安装时用 uvx 临时运行。" },
|
|
151
|
+
{ key: "figmaEnabled", label: "启用", type: "switch", desc: "需要 Figma 桌面版。figma-console 模式下要在 Figma 里导入并运行 Desktop Bridge 插件(见上方说明);Figma 没开时工具会报未连接,不影响其他功能。" },
|
|
152
|
+
{ key: "figmaMode", label: "连接方式", type: "select", options: [
|
|
153
|
+
{ value: "console", label: "figma-console-mcp(推荐:读写画布,免费计划可用)" },
|
|
154
|
+
{ value: "official", label: "官方 Figma 桌面版 MCP(需付费计划的 Dev / Full 席位)" }
|
|
155
|
+
], desc: "官方桌面版 MCP 要在 Figma 桌面版的 Dev Mode 里打开「启用桌面版 MCP 服务器」,只读、不能改画布;官方远程服务器需要 OAuth 登录,这里不支持。" },
|
|
156
|
+
{ key: "figmaToken", label: "个人访问令牌", type: "password", placeholder: "figd_…", desc: "可选。Figma → 设置 → 安全 → 个人访问令牌(figd_ 开头)。读写画布走插件,不需要令牌;读取文件 / 变量 / 评论 / 版本历史等 REST 类工具需要。只在 figma-console 模式下使用。" },
|
|
157
|
+
{ key: "figmaOfficialUrl", label: "官方 MCP 地址", type: "text", desc: "官方 Figma 桌面版 MCP 的地址,默认 http://127.0.0.1:3845/mcp;只在「官方」模式下使用。" },
|
|
158
|
+
{ key: "figmaPackage", label: "npm 包名", type: "text", desc: "默认 figma-console-mcp(可带版本,如 figma-console-mcp@1.40.7)。改了包名后需要重新「下载安装」;未安装时用 npx 临时运行。" },
|
|
159
|
+
{ key: "photoshopEnabled", label: "启用", type: "switch", desc: "Photoshop 没开时工具会报找不到 Photoshop,不影响其他功能。注意:其中 photoshop_execute_script 工具会执行模型给出的任意 ExtendScript。" },
|
|
160
|
+
{ key: "photoshopPath", label: "Photoshop 路径", type: "text", placeholder: "C:\\Program Files\\Adobe\\Adobe Photoshop 2026\\Photoshop.exe", desc: "可选。留空时从注册表自动检测已安装的 Photoshop。" },
|
|
161
|
+
{ key: "photoshopPackage", label: "npm 包名", type: "text", desc: "默认 @alisaitteke/photoshop-mcp(可带版本)。改了包名后需要重新「下载安装」;未安装时用 npx 临时运行。" },
|
|
162
|
+
{ key: "chromeEnabled", label: "启用", type: "switch", desc: "AI第一次使用此工具时才会启动 Chrome。注意:evaluate_script 会在网页里执行模型给出的 JS,模型能看到页面里的所有内容。" },
|
|
163
|
+
{ key: "chromeConnect", label: "连接方式", type: "select", options: [
|
|
164
|
+
{ value: "launch", label: "启动独立的 Chrome(默认)" },
|
|
165
|
+
{ value: "autoConnect", label: "连接正在运行的 Chrome(autoConnect,Chrome 144+)" },
|
|
166
|
+
{ value: "browserUrl", label: "连接调试端口(browserUrl)" }
|
|
167
|
+
], desc: "「启动独立的 Chrome」:用专用配置目录(默认 %USERPROFILE%\\.cache\\chrome-devtools-mcp),不动你平时的配置,AI第一次使用此工具时才会启动。.cache 目录不存在是正常的,用到时会自动创建。「连接正在运行的 Chrome(autoConnect)」:Chrome 144+,在 chrome://inspect/#remote-debugging 打开「允许为此浏览器实例进行远程调试」;打开开关本身不会弹窗——第一次用浏览器工具连接时,Chrome 才会弹出「允许调试」确认框,点允许即可。此模式不需要也不使用 .cache。「连接调试端口」:必须用 chrome.exe --remote-debugging-port=… --user-data-dir=<非默认目录> 启动;inspect 页开关不会提供 /json/version(会 404),那种情况请改用 autoConnect。" },
|
|
168
|
+
{ key: "chromeBrowserUrl", label: "调试地址", type: "text", desc: "只在「连接调试端口(browserUrl)」时使用。Chrome 必须用 --remote-debugging-port 且带非默认 --user-data-dir 启动,例如 chrome.exe --remote-debugging-port=9222 --user-data-dir=%TEMP%\\chrome-profile-stable 后填 http://127.0.0.1:9222。chrome://inspect 开关开出的 9222 端口没有 /json/version(会 404),请改用 autoConnect。" },
|
|
169
|
+
{ key: "chromeChannel", label: "Chrome 渠道", type: "select", options: ["stable", "beta", "dev", "canary"], desc: "启动或连接哪个渠道的 Chrome,默认正式版(stable)。" },
|
|
170
|
+
{ key: "chromeHeadless", label: "窗口", type: "select", bool: true, options: [
|
|
171
|
+
{ value: "false", label: "显示窗口" },
|
|
172
|
+
{ value: "true", label: "无头(不显示窗口)" }
|
|
173
|
+
], desc: "只对「启动独立的 Chrome」有效。" },
|
|
174
|
+
{ key: "chromeUserDataDir", label: "配置目录", type: "text", desc: "可选,只对「启动独立的 Chrome」有效。留空时用 chrome-devtools-mcp 自己的专用目录(%USERPROFILE%\\.cache\\chrome-devtools-mcp\\chrome-profile);目录不存在会在第一次启动时自动创建。autoConnect / browserUrl 不用这个目录。" },
|
|
175
|
+
{ key: "chromeToolset", label: "工具集", type: "select", options: [
|
|
176
|
+
{ value: "standard", label: "标准(约 30 个工具)" },
|
|
177
|
+
{ value: "full", label: "完整(再加内存分析、坐标点击、扩展与 PWA 等)" },
|
|
178
|
+
{ value: "slim", label: "精简(只有导航 / 执行脚本 / 截图)" }
|
|
179
|
+
], desc: "工具越多,每次对话占用的上下文越多。扩展与 PWA 工具只在「启动独立的 Chrome」时可用。" },
|
|
180
|
+
{ key: "chromePackage", label: "npm 包名", type: "text", desc: "默认 chrome-devtools-mcp(可带版本,如 chrome-devtools-mcp@1.10.1)。改了包名后需要重新「下载安装」;未安装时用 npx 临时运行。" },
|
|
181
|
+
{ key: "godotEnabled", label: "启用", type: "switch", desc: "Godot 没开时服务器照常运行、等编辑器连上,不影响其他功能;启用期间占用 HTTP / WebSocket 端口(默认 8000 / 9500)。注意:模型可以改项目里的场景、脚本和资源,也可以运行项目。" },
|
|
182
|
+
{ key: "godotProject", label: "Godot 项目路径", type: "text", placeholder: "D:\\Games\\MyGame", desc: "含 project.godot 的文件夹。「安装插件到项目」把与服务器同版本、签名校验过的插件装进 <项目>\\addons\\godot_ai:已是同版本时不改动;已有其他版本时先把旧目录整个挪到 addons\\.godot_ai_backup(需先关闭 Godot),不覆盖。装完在 Godot「项目 → 项目设置 → 插件」里启用 Godot AI。",
|
|
183
|
+
action: { component: "godot", path: "/components/godot/addon", label: "安装插件到项目" } },
|
|
184
|
+
{ key: "godotPackage", label: "pip 包名", type: "text", desc: "默认 godot-ai(装最新版并固定下来;可写 godot-ai==4.2.3 这样的版本,对上项目里已有的插件)。改了包名后需要重新「下载安装」;不用 uvx 临时运行。" },
|
|
185
|
+
{ key: "godotHttpPort", label: "HTTP 端口", type: "number", min: 1, max: 65535, desc: "godot-ai 共享后端的端口(attach --port),默认 8000;要和 Godot「编辑器设置 → godot_ai/http_port」一致。" },
|
|
186
|
+
{ key: "godotWsPort", label: "WebSocket 端口", type: "number", min: 1, max: 65535, desc: "Godot 编辑器插件连接的端口(attach --ws-port),默认 9500;要和「编辑器设置 → godot_ai/ws_port」一致。" },
|
|
187
|
+
|
|
188
|
+
{ key: "windowsEnabled", label: "启用", type: "switch", desc: "仅 Windows。注意:模型可以操作桌面、读写文件并执行 PowerShell。" },
|
|
189
|
+
{ key: "windowsMode", label: "连接方式", type: "select", options: [
|
|
190
|
+
{ value: "stdio", label: "插件拉起(stdio,推荐)" },
|
|
191
|
+
{ value: "http", label: "连接已有 HTTP 服务" }
|
|
192
|
+
], desc: "stdio:由本插件启动 windows-mcp;http:连接计划任务等方式已启动的服务(默认 127.0.0.1:18765)。" },
|
|
193
|
+
{ key: "windowsUrl", label: "HTTP 地址", type: "text", desc: "只在「连接已有 HTTP 服务」时使用,默认 http://127.0.0.1:18765/mcp。" },
|
|
194
|
+
{ key: "windowsPackage", label: "pip 包名", type: "text", desc: "默认 windows-mcp。改了包名后需要重新「下载安装」;未安装时用系统 uv tool 或 uvx。" },
|
|
195
|
+
{ key: "notionEnabled", label: "启用", type: "switch", desc: "首次连接会弹出 Notion 授权页;token 缓存在 %USERPROFILE%\\.mcp-auth。" },
|
|
196
|
+
{ key: "notionUrl", label: "远程地址", type: "text", desc: "默认 https://mcp.notion.com/mcp。" },
|
|
197
|
+
{ key: "notionPackage", label: "npm 包名(桥)", type: "text", desc: "默认 mcp-remote。已有 %USERPROFILE%\\.dsh\\mcp-remote 时可直接用,不必再装。" },
|
|
198
|
+
{ key: "cloudflareEnabled", label: "启用", type: "switch", desc: "首次连接会弹出 Cloudflare 授权页;插件只预申请 offline_access,请在同意页勾选权限。" },
|
|
199
|
+
{ key: "cloudflareUrl", label: "远程地址", type: "text", desc: "默认 https://mcp.cloudflare.com/mcp。" },
|
|
200
|
+
{ key: "cloudflarePackage", label: "npm 包名(桥)", type: "text", desc: "默认 mcp-remote。" },
|
|
201
|
+
{ key: "cloudflare-docsEnabled", label: "启用", type: "switch", desc: "公开文档 MCP,无需登录。" },
|
|
202
|
+
{ key: "cloudflare-docsUrl", label: "远程地址", type: "text", desc: "默认 https://docs.mcp.cloudflare.com/mcp。" },
|
|
203
|
+
{ key: "githubEnabled", label: "启用", type: "switch", desc: "需要 GitHub PAT(设置或环境变量 GITHUB_MCP_PAT)。" },
|
|
204
|
+
{ key: "githubUrl", label: "托管端点", type: "text", desc: "默认 https://api.githubcopilot.com/mcp/。" },
|
|
205
|
+
{ key: "githubToken", label: "GitHub PAT", type: "password", placeholder: "ghp_… / github_pat_…", desc: "优先用这里的 token;留空时读用户环境变量 GITHUB_MCP_PAT。不要提交到 git。" },
|
|
206
|
+
{ key: "comfyuiEnabled", label: "启用", type: "switch", desc: "ComfyUI 没开时仍可挂载 MCP(可用工具 launch_comfyui);本机 8188 开着时状态会注明。" },
|
|
207
|
+
{ key: "comfyuiPackage", label: "pip 包名", type: "text", desc: "默认 comfy-mcp。改了包名后需要重新「下载安装」;未安装时用系统 uv tool 或 uvx。" },
|
|
208
|
+
{ key: "comfyuiBin", label: "comfy.exe 路径", type: "text", placeholder: "C:\\Users\\…\\uv\\tools\\comfy-cli\\Scripts\\comfy.exe", desc: "可选。作为 COMFY_BIN 传给服务器;留空时用环境变量 COMFY_BIN。" },
|
|
209
|
+
{ key: "ffmpegEnabled", label: "启用", type: "switch", desc: "需要本机已安装 ffmpeg / ffprobe(PATH 或下方路径)。未装 FFmpeg 时服务器无法可靠工作;关掉不影响其他功能。" },
|
|
210
|
+
{ key: "ffmpegPath", label: "FFmpeg 路径", type: "text", placeholder: "C:\\ffmpeg\\bin\\ffmpeg.exe", desc: "可选。本机 ffmpeg 可执行文件完整路径;留空时从 PATH 查找。填写后启动 MCP 时会把该目录前置到 PATH(便于找到同目录的 ffprobe)。" },
|
|
211
|
+
{ key: "ffmpegPackage", label: "pip 包名", type: "text", desc: "默认 kinocut(入口命令 kino,原 mcp-video)。改了包名后需要重新「下载安装」;未安装时用 uvx 临时运行。" },
|
|
212
|
+
{ key: "obsidianEnabled", label: "启用", type: "switch", desc: "需要 Obsidian 开着,并启用社区插件 Local REST API。没开时工具会报连不上,不影响其他功能。" },
|
|
213
|
+
{ key: "obsidianApiKey", label: "API 密钥", type: "password", placeholder: "在 Local REST API 插件设置里复制", desc: "必填。Obsidian → 设置 → 社区插件 → Local REST API 中的 API key(Bearer Token)。" },
|
|
214
|
+
{ key: "obsidianBaseUrl", label: "API 地址", type: "text", desc: "默认 http://127.0.0.1:27123(需在插件里开启 Non-encrypted HTTP)。HTTPS 默认端口可用 https://127.0.0.1:27124(自签证书由服务器侧跳过校验)。" },
|
|
215
|
+
{ key: "obsidianEnableCommands", label: "允许执行命令面板", type: "switch", desc: "对应 OBSIDIAN_ENABLE_COMMANDS。打开后 MCP 可列出并执行 Obsidian 命令(可能含破坏性操作),默认关闭。" },
|
|
216
|
+
{ key: "obsidianPackage", label: "npm 包名", type: "text", desc: "默认 obsidian-mcp-server(可带版本)。改了包名后需要重新「下载安装」;未安装时用 npx 临时运行。" },
|
|
217
|
+
{ key: "nodePath", label: "node 路径", type: "text", placeholder: "C:\\Program Files\\nodejs\\node.exe", desc: "插件 tools\\node 里有 Node.js 时优先用它;否则用这里的路径(npm 取同目录),留空时用 PATH 里的 node(需 20.19+ / 22.12+),再退回 DSH 自带的运行时(只能运行,不能安装)。" },
|
|
218
|
+
{ key: "npmRegistry", label: "npm 镜像", type: "text", placeholder: "https://registry.npmmirror.com", desc: "可选。安装 npm 组件时使用的镜像地址;留空时用 npm 自己的设置(默认 registry.npmjs.org)。改完先点保存再安装。" },
|
|
219
|
+
{ key: "uvPath", label: "uv 路径", type: "text", desc: "插件 tools\\uv 里有 uv 时优先用它;否则用这里的路径(uvx 取同目录),留空时查找 WinGet 安装的 uv,再用 PATH。" },
|
|
220
|
+
{ key: "sessionControlsEnabled", label: "启用会话控制", type: "switch", desc: "实验性。默认关闭。关闭后仍保留本页配置;助手消息撤回/重试与输入框暂停熔断会隐藏。启用前会再次确认。" },
|
|
221
|
+
{ key: "sessionCircuitEnabled", label: "自动熔断", type: "switch", desc: "同一工具连打 / 步数过多 / 思考过长时自动取消当前回合。" },
|
|
222
|
+
{ key: "sessionCircuitMaxSameTool", label: "同工具连续上限", type: "number", min: 2, max: 50, desc: "同一工具(含相同参数)连续调用达到该次数时熔断。" },
|
|
223
|
+
{ key: "sessionCircuitMaxSteps", label: "单回合步数上限", type: "number", min: 5, max: 500, desc: "单回合 step/start 次数上限。" },
|
|
224
|
+
{ key: "sessionCircuitMaxReasoningChars", label: "思考字符上限", type: "number", min: 1000, max: 5000000, desc: "单回合累计思考字符上限。" },
|
|
225
|
+
|
|
226
|
+
{ key: "proxy", label: "下载代理", type: "text", desc: "下载安装时使用的 HTTP 代理(uv、Python、Node.js、npm 都走它),例如 http://127.0.0.1:7890;留空时使用环境变量 HTTPS_PROXY / HTTP_PROXY。改完先点保存再安装。" }
|
|
227
|
+
];
|
|
228
|
+
var FIELD_BY_KEY = {};
|
|
229
|
+
INPUT_FIELDS.forEach(function (f) { FIELD_BY_KEY[f.key] = f; });
|
|
230
|
+
|
|
231
|
+
/** Local component enable switch: same <id>Enabled key as bundled; register dynamically when not in INPUT_FIELDS. Default OFF (fresh install: no MCP until enabled). */
|
|
232
|
+
function ensureLocalEnabledField(id) {
|
|
233
|
+
var key = id + "Enabled";
|
|
234
|
+
if (!FIELD_BY_KEY[key]) {
|
|
235
|
+
FIELD_BY_KEY[key] = {
|
|
236
|
+
key: key,
|
|
237
|
+
label: "启用",
|
|
238
|
+
type: "switch",
|
|
239
|
+
desc: "关闭后此组件不会在后台启动,也不影响其它功能。",
|
|
240
|
+
localDynamic: true
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return FIELD_BY_KEY[key];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 列表图标:通用线条,跟随文字颜色。只表达功能类别,不使用第三方标志的图形或品牌色。
|
|
247
|
+
function svgIcon(body) {
|
|
248
|
+
return '<svg viewBox="0 0 24 24" width="100%" height="100%" focusable="false" aria-hidden="true">' + body + "</svg>";
|
|
249
|
+
}
|
|
250
|
+
function strokeIcon(body) {
|
|
251
|
+
return svgIcon('<g fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">' + body + "</g>");
|
|
252
|
+
}
|
|
253
|
+
var ICONS = {
|
|
254
|
+
session: strokeIcon('<path d="M5 4.5h14v15H5z"/><path d="M8 8h8M8 12h8M8 16h5"/>'),
|
|
255
|
+
image: strokeIcon('<rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="9.8" r="1.7"/><path d="M20.5 15.6l-4.8-4.8L6.2 19.5"/>'),
|
|
256
|
+
office: strokeIcon('<rect x="4" y="3.5" width="16" height="17" rx="1.5"/><path d="M4 8h16M9 8v12.5M4 13h16"/>'),
|
|
257
|
+
blender: strokeIcon('<path d="M12 3.2 20 7.6v8.8L12 20.8 4 16.4V7.6Z"/><path d="M12 3.2v17.6M4 7.6l8 4.4 8-4.4"/>'),
|
|
258
|
+
unity: strokeIcon('<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M10 9.2v6.2l5.2-3.1Z"/>'),
|
|
259
|
+
uv: strokeIcon('<path d="M4 8.2 12 4.2l8 4v7.6l-8 4-8-4Z"/><path d="M12 12.2V20M4 8.2l8 4 8-4"/>'),
|
|
260
|
+
figma: strokeIcon('<rect x="3.5" y="4" width="10" height="13" rx="1.4"/><rect x="10.5" y="7" width="10" height="13" rx="1.4"/>'),
|
|
261
|
+
photoshop: strokeIcon('<rect x="3.5" y="5" width="17" height="12" rx="1.6"/><path d="M3.5 15.2 8.2 11l3.2 2.6 2.4-2 6.7 4.6"/><circle cx="8.2" cy="9" r="1.2"/>'),
|
|
262
|
+
chrome: strokeIcon('<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9h18"/><circle cx="6.2" cy="7" r=".7" fill="currentColor" stroke="none"/>'),
|
|
263
|
+
godot: strokeIcon('<circle cx="7" cy="6.5" r="1.6"/><circle cx="7" cy="12" r="1.6"/><circle cx="7" cy="17.5" r="1.6"/><path d="M8.6 6.5H16M8.6 12H18M8.6 17.5H14"/>'),
|
|
264
|
+
node: strokeIcon('<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M7.2 10.2 9.6 12 7.2 13.8M11.4 14.6h5"/>'),
|
|
265
|
+
windows: strokeIcon('<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M3.5 8.5h17"/>'),
|
|
266
|
+
notion: strokeIcon('<path d="M7 3.5h8.2L19 7.2V20.5H7z"/><path d="M15 3.8V7.4H19"/><path d="M10 11h6M10 14h6M10 17h4"/>'),
|
|
267
|
+
cloudflare: strokeIcon('<circle cx="12" cy="12" r="3"/><path d="M12 3.5v2.2M12 18.3v2.2M3.5 12h2.2M18.3 12h2.2M6 6l1.6 1.6M16.4 16.4 18 18M18 6l-1.6 1.6M7.6 16.4 6 18"/>'),
|
|
268
|
+
cloudflareDocs: strokeIcon('<path d="M6.5 3.5h8L18.5 8v12.5h-12z"/><path d="M14.2 3.8V8H18"/><path d="M9 12.2h6.5M9 15.2h6.5M9 18.2h4"/>'),
|
|
269
|
+
github: strokeIcon('<circle cx="7" cy="7" r="2.2"/><circle cx="17" cy="7" r="2.2"/><circle cx="12" cy="17" r="2.2"/><path d="M8.8 8.4 10.6 15.2M15.2 8.4 13.4 15.2"/>'),
|
|
270
|
+
comfyui: strokeIcon('<rect x="3.2" y="8" width="5.2" height="4.2" rx="1"/><rect x="15.6" y="4.5" width="5.2" height="4.2" rx="1"/><rect x="15.6" y="15.3" width="5.2" height="4.2" rx="1"/><path d="M8.4 10.1h7.2M16.4 8.7 10.2 10.1M16.4 15.3 10.2 12.2"/>'),
|
|
271
|
+
proxy: strokeIcon('<circle cx="12" cy="12" r="8.5"/><ellipse cx="12" cy="12" rx="3.7" ry="8.5"/><path d="M3.9 9.2h16.2M3.9 14.8h16.2"/>'),
|
|
272
|
+
ffmpeg: strokeIcon('<rect x="3.5" y="6" width="17" height="12" rx="2"/><path d="M8 6v12M12 6v12M16 6v12M3.5 10h17M3.5 14h17"/>'),
|
|
273
|
+
obsidian: strokeIcon('<path d="M8 6.5h11v13H8z"/><path d="M5 4.5h11"/><path d="M5 4.5v11"/><path d="M11 10.5h5M11 13.5h5M11 16.5h3"/>'),
|
|
274
|
+
addComponent: strokeIcon('<rect x="3.5" y="3.5" width="7" height="7" rx="1.2"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.2"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.2"/><path d="M17 13.5v7M13.5 17h7"/>')
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// 功能列表:每项一行、一个管理页。component:状态来自 /components 里同 id 的一项;
|
|
278
|
+
// 没有 component 的是纯设置项,列表里的状态由 summary(已保存的设置) 给出。icon:ICONS 里的静态 SVG。
|
|
279
|
+
var FEATURES = [
|
|
280
|
+
{
|
|
281
|
+
id: "image", group: "多模态", title: "媒体卡片", icon: ICONS.image,
|
|
282
|
+
desc: "mm_send_image 把本地图片发成 MmCard(Host attachmentId)。",
|
|
283
|
+
intro: "调用 mm_send_image,传入本地 png/jpg/webp/gif/bmp 路径,经 attachments.saveImage 得到 attachmentId,工具卡片渲染 type:mm kind:image。",
|
|
284
|
+
keys: [],
|
|
285
|
+
summary: function () {
|
|
286
|
+
return { text: "mm_send_image · 可用", cls: "ready" };
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "office", group: "工作组件", title: "Office", component: "office",
|
|
291
|
+
moduleSource: "bundled", icon: ICONS.office,
|
|
292
|
+
url: "https://github.com/officemcp/officemcp",
|
|
293
|
+
desc: "经 COM 控制本机的 Word / Excel / PowerPoint(OfficeMCP)。",
|
|
294
|
+
intro: "经 COM 控制本机的 Word / Excel / PowerPoint(OfficeMCP),工具名前缀 mcp__officemcp__。",
|
|
295
|
+
keys: ["officeEnabled", "officeRepo", "officeFolder"]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "blender", group: "工作组件", title: "Blender", component: "blender",
|
|
299
|
+
moduleSource: "bundled", icon: ICONS.blender,
|
|
300
|
+
url: "https://github.com/ahujasid/blender-mcp",
|
|
301
|
+
desc: "控制正在打开的 Blender(mcp-for-blender)。",
|
|
302
|
+
intro: "控制正在打开的 Blender(mcp-for-blender),工具名前缀 mcp__blender__。Blender 里需要启用对应插件。",
|
|
303
|
+
keys: ["blenderEnabled", "blenderPackage"]
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "unity", group: "工作组件", title: "Unity", component: "unity",
|
|
307
|
+
moduleSource: "bundled", icon: ICONS.unity,
|
|
308
|
+
url: "https://github.com/CoplayDev/unity-mcp",
|
|
309
|
+
desc: "控制 Unity 编辑器(Coplay mcp-for-unity)。",
|
|
310
|
+
intro: "控制 Unity 编辑器(Coplay mcp-for-unity),工具名前缀 mcp__unity__。编辑器需装 MCP for Unity 包并开着工程。",
|
|
311
|
+
keys: ["unityEnabled", "unityPackage"]
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: "figma", group: "工作组件", title: "Figma", component: "figma",
|
|
315
|
+
moduleSource: "bundled", icon: ICONS.figma,
|
|
316
|
+
url: "https://github.com/southleft/figma-console-mcp",
|
|
317
|
+
desc: "读写 Figma 设计稿(figma-console-mcp)。",
|
|
318
|
+
intro: "经 Figma 桌面版里的 Desktop Bridge 插件读写设计稿(figma-console-mcp,121 个工具:创建 / 修改节点和组件、变量与设计令牌、截图、FigJam、Slides 等),工具名前缀 mcp__figma__。准备:装 Figma 桌面版;启用本组件后,在 Figma 菜单选择「插件 → 开发 → 从清单导入插件…」(英文界面:Plugins → Development → Import plugin from manifest…),选 %USERPROFILE%\\.figma-console-mcp\\plugin\\manifest.json,再在设计文件里运行「Figma Desktop Bridge」。",
|
|
319
|
+
keys: ["figmaEnabled", "figmaMode", "figmaToken", "figmaOfficialUrl", "figmaPackage"]
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: "photoshop", group: "工作组件", title: "Photoshop", component: "photoshop",
|
|
323
|
+
moduleSource: "bundled", icon: ICONS.photoshop,
|
|
324
|
+
url: "https://github.com/alisaitteke/photoshop-mcp",
|
|
325
|
+
desc: "经 COM 控制本机的 Photoshop(photoshop-mcp)。",
|
|
326
|
+
intro: "经 COM 控制本机的 Adobe Photoshop(photoshop-mcp,125 个工具:文档、图层、选区与蒙版、滤镜、调色、文字、导出、动作、创成式填充等),工具名前缀 mcp__photoshop__。需要本机装有 Photoshop(CS6 / 2012 及以上)并开着;不需要装 Photoshop 插件(只有神经滤镜要另装它的 UXP 插件)。",
|
|
327
|
+
keys: ["photoshopEnabled", "photoshopPath", "photoshopPackage"]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "chrome", group: "工作组件", title: "Chrome", component: "chrome",
|
|
331
|
+
moduleSource: "bundled", icon: ICONS.chrome,
|
|
332
|
+
url: "https://github.com/ChromeDevTools/chrome-devtools-mcp",
|
|
333
|
+
desc: "控制本机的 Google Chrome(Chrome DevTools MCP)。",
|
|
334
|
+
intro: "用 Google 官方的 Chrome DevTools MCP 控制本机 Chrome:打开 / 切换标签页、点击输入和填表、上传文件、处理对话框、截图和页面快照、执行 JS、网络请求与控制台、性能追踪与 Lighthouse、设备 / 网络 / 地理位置模拟等,工具名前缀 mcp__chrome__。默认启动一个独立配置的 Chrome,也可以连接你正在用的 Chrome。",
|
|
335
|
+
keys: ["chromeEnabled", "chromeConnect", "chromeBrowserUrl", "chromeChannel", "chromeHeadless", "chromeUserDataDir", "chromeToolset", "chromePackage"]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "godot", group: "工作组件", title: "Godot", component: "godot",
|
|
339
|
+
moduleSource: "bundled", icon: ICONS.godot,
|
|
340
|
+
url: "https://github.com/hi-godot/godot-ai",
|
|
341
|
+
desc: "控制 Godot 编辑器(hi-godot/godot-ai)。",
|
|
342
|
+
intro: "用 godot-ai 控制 Godot 编辑器:场景与节点、脚本(GDScript 校验并热重载)、信号、UI、材质、动画、粒子、相机与环境,运行项目、跑测试、读日志、编辑器截图等(47 个工具),工具名前缀 mcp__godot__。需要 Godot 4.7+。准备:① 点「下载安装」(服务器版本会固定下来,并下载同版本、签名校验过的 Godot 插件);② 在下面填 Godot 项目目录,点「安装插件到项目」(或按下方说明手动解压同版本的 godot-ai-v4-plugin.zip);③ 在 Godot 里「项目 → 项目设置 → 插件」启用 Godot AI;④ 打开「启用」。DSH 已经通过本组件连接,不要再在 Godot AI 面板里给 DeepSeek Harness 点 Configure(会多出一份重复的工具)。",
|
|
343
|
+
keys: ["godotEnabled", "godotProject", "godotPackage", "godotHttpPort", "godotWsPort"]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "windows", group: "工作组件", title: "Windows", component: "windows",
|
|
347
|
+
moduleSource: "bundled", icon: ICONS.windows,
|
|
348
|
+
url: "https://github.com/CursorTouch/Windows-MCP",
|
|
349
|
+
desc: "控制本机 Windows 桌面(Windows-MCP)。",
|
|
350
|
+
intro: "控制本机 Windows 桌面(Windows-MCP):窗口、键鼠、截图、文件系统、注册表、PowerShell 等,工具名前缀 mcp__windows__。默认由本插件以 stdio 拉起;也可改连已在跑的 HTTP 服务(如计划任务 127.0.0.1:18765)。注意:模型可以操作桌面与执行 PowerShell。",
|
|
351
|
+
keys: ["windowsEnabled", "windowsMode", "windowsUrl", "windowsPackage"]
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "notion", group: "工作组件", title: "Notion", component: "notion",
|
|
355
|
+
moduleSource: "bundled", icon: ICONS.notion,
|
|
356
|
+
url: "https://developers.notion.com/docs/mcp",
|
|
357
|
+
desc: "官方 Notion MCP(OAuth,经 mcp-remote)。",
|
|
358
|
+
intro: "官方 Notion MCP(OAuth)。DSH HTTP 传输没有浏览器授权,经 mcp-remote stdio 桥;首次连接会弹出 Notion 授权页,token 缓存在 %USERPROFILE%\\.mcp-auth。工具名前缀 mcp__notion__。",
|
|
359
|
+
keys: ["notionEnabled", "notionUrl", "notionPackage"]
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "cloudflare", group: "工作组件", title: "Cloudflare", component: "cloudflare",
|
|
363
|
+
moduleSource: "bundled", icon: ICONS.cloudflare,
|
|
364
|
+
url: "https://github.com/cloudflare/mcp-server-cloudflare",
|
|
365
|
+
desc: "官方 Cloudflare API MCP(OAuth,经 mcp-remote)。",
|
|
366
|
+
intro: "官方 Cloudflare API MCP(OAuth / Code Mode)。经 mcp-remote 桥;预申请 scope=offline_access,授权页请自行勾选权限。工具名前缀 mcp__cloudflare__(docs / search / execute)。",
|
|
367
|
+
keys: ["cloudflareEnabled", "cloudflareUrl", "cloudflarePackage"]
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: "cloudflare-docs", group: "工作组件", title: "Cloudflare Docs", component: "cloudflare-docs",
|
|
371
|
+
moduleSource: "bundled", icon: ICONS.cloudflareDocs,
|
|
372
|
+
url: "https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-catalog/",
|
|
373
|
+
desc: "Cloudflare 文档 MCP(公开,无需登录)。",
|
|
374
|
+
intro: "Cloudflare 文档 MCP(公开 streamable-http,无需登录)。查文档不必等 API 授权。工具名前缀 mcp__cloudflare-docs__。",
|
|
375
|
+
keys: ["cloudflare-docsEnabled", "cloudflare-docsUrl"]
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
id: "github", group: "工作组件", title: "GitHub", component: "github",
|
|
379
|
+
moduleSource: "bundled", icon: ICONS.github,
|
|
380
|
+
url: "https://github.com/github/github-mcp-server",
|
|
381
|
+
desc: "官方 GitHub MCP(托管 HTTP + PAT)。",
|
|
382
|
+
intro: "官方 GitHub MCP 托管端点。Token 优先读设置 githubToken,否则读环境变量 GITHUB_MCP_PAT;不要把 token 写进仓库。工具名前缀 mcp__github__。",
|
|
383
|
+
keys: ["githubEnabled", "githubUrl", "githubToken"]
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "comfyui", group: "工作组件", title: "ComfyUI", component: "comfyui",
|
|
387
|
+
moduleSource: "bundled", icon: ICONS.comfyui,
|
|
388
|
+
url: "https://github.com/Comfy-Org/comfy-mcp",
|
|
389
|
+
desc: "官方 Comfy MCP(驱动本机 ComfyUI)。",
|
|
390
|
+
intro: "官方 Comfy MCP(comfy-mcp,经 comfy-cli 驱动本机 ComfyUI)。工具名前缀 mcp__comfyui__。ComfyUI 需先开着,或用工具 launch_comfyui。可选填写 COMFY_BIN(comfy.exe)。",
|
|
391
|
+
keys: ["comfyuiEnabled", "comfyuiPackage", "comfyuiBin"]
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "ffmpeg", group: "工作组件", title: "FFmpeg", component: "ffmpeg",
|
|
395
|
+
moduleSource: "bundled", icon: ICONS.ffmpeg,
|
|
396
|
+
url: "https://github.com/KyaniteLabs/kinocut",
|
|
397
|
+
desc: "本机媒体转换 / 探针 / 剪辑(Kinocut MCP,封装 FFmpeg)。",
|
|
398
|
+
intro: "用 Kinocut(原 mcp-video)经类型化工具调用本机 FFmpeg:转码、剪辑、合并、字幕、探针、缩略图、质量检查与 Shorts/Reels 再包装等(百余工具,可用 search_tools 发现),工具名前缀 mcp__ffmpeg__。准备:① 本机安装 ffmpeg/ffprobe(winget install ffmpeg,或填「FFmpeg 路径」);② 点「下载安装」装 Kinocut 到 tools\\ffmpeg;③ 打开「启用」。不会把庞大的 FFmpeg 二进制打进插件仓库。",
|
|
399
|
+
keys: ["ffmpegEnabled", "ffmpegPath", "ffmpegPackage"]
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "obsidian", group: "工作组件", title: "Obsidian", component: "obsidian",
|
|
403
|
+
moduleSource: "bundled", icon: ICONS.obsidian,
|
|
404
|
+
url: "https://github.com/cyanheads/obsidian-mcp-server",
|
|
405
|
+
desc: "读写本机 Obsidian 库(经 Local REST API 插件)。",
|
|
406
|
+
intro: "用 obsidian-mcp-server 读写 Obsidian 库:读笔记、列表、搜索、追加/补丁、frontmatter 与标签等(14 个工具),工具名前缀 mcp__obsidian__。准备:① 安装并打开 Obsidian;② 设置 → 社区插件,关闭安全模式后浏览,搜索并安装「Local REST API」,再启用它;③ 在该插件设置里复制 API 密钥,填到下方,并建议开启 Non-encrypted HTTP(默认 http://127.0.0.1:27123);④ 点「下载安装」;⑤ 打开「启用」。",
|
|
407
|
+
keys: ["obsidianEnabled", "obsidianApiKey", "obsidianBaseUrl", "obsidianEnableCommands", "obsidianPackage"]
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: "session-controls", group: "通用", title: "会话控制", icon: ICONS.session,
|
|
411
|
+
maturity: "experimental",
|
|
412
|
+
desc: "用户消息下「撤回」、助手操作行「重试」、输入框「暂停」=熔断。",
|
|
413
|
+
intro: "通用会话能力(非 MCP)。撤回在用户消息下方的操作行:确认后在当前页收起该回合及之后的内容。重试在助手回复的复制与分支之间:确认后收起原回复并立刻重新生成。暂停只取消当前回合。只打开着的会话会先恢复到内存,再在当前页完成,不用退出重进。",
|
|
414
|
+
warn: "【实验性】此功能尚未开发完毕。启用后使用撤回 / 重试 / 熔断,可能会对对话造成不可逆的破坏。默认关闭;请仅在明确需要时启用。",
|
|
415
|
+
keys: ["sessionControlsEnabled", "sessionCircuitEnabled", "sessionCircuitMaxSameTool", "sessionCircuitMaxSteps", "sessionCircuitMaxReasoningChars"],
|
|
416
|
+
summary: function (v) {
|
|
417
|
+
if (v.sessionControlsEnabled !== true) return { text: "已关闭 · 实验性", cls: "off" };
|
|
418
|
+
var bits = ["实验性"];
|
|
419
|
+
bits.push(v.sessionCircuitEnabled === false ? "自动熔断关" : "自动熔断开");
|
|
420
|
+
bits.push("同工具×" + (v.sessionCircuitMaxSameTool ?? 6));
|
|
421
|
+
bits.push("步数≤" + (v.sessionCircuitMaxSteps ?? 80));
|
|
422
|
+
return { text: bits.join(" · "), cls: "ready" };
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
{
|
|
427
|
+
id: "uv", group: "通用", title: "uv", component: "uv", icon: ICONS.uv,
|
|
428
|
+
url: "https://github.com/astral-sh/uv",
|
|
429
|
+
desc: "下载 Python、安装和运行 Python 组件的工具(Office / Blender / Unity / Godot / Windows / ComfyUI / FFmpeg)。",
|
|
430
|
+
intro: "uv 负责下载 Python 和安装 Office / Blender / Unity / Godot / Windows / ComfyUI / FFmpeg;安装这些组件时会自动下载到插件的 tools\\uv。所有组件、Python 和缓存都放在插件的 tools 目录里。",
|
|
431
|
+
keys: ["uvPath"]
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: "node", group: "通用", title: "Node.js", component: "node", icon: ICONS.node,
|
|
435
|
+
url: "https://nodejs.org/",
|
|
436
|
+
desc: "运行和安装 Chrome / Figma / Photoshop / Notion / Cloudflare / Obsidian 等依赖 npm 的组件。",
|
|
437
|
+
intro: "Chrome / Figma / Photoshop / Notion / Cloudflare(mcp-remote 桥)/ Obsidian 依赖 npm。把 npm 装进插件 tools 目录或本机 Node.js(20.19+ / 22.12+)都行。查找顺序:插件 tools\\node → 设置路径 → 系统 PATH。",
|
|
438
|
+
keys: ["nodePath", "npmRegistry"]
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: "proxy", group: "通用", title: "下载代理", icon: ICONS.proxy,
|
|
442
|
+
desc: "下载安装 uv、Python、Node.js 和各组件时使用的 HTTP 代理。",
|
|
443
|
+
intro: "只影响「下载安装」,不影响组件运行。改完先点保存再安装。",
|
|
444
|
+
keys: ["proxy"],
|
|
445
|
+
summary: function (v) {
|
|
446
|
+
return v.proxy
|
|
447
|
+
? { text: "已设置", cls: "on", title: v.proxy }
|
|
448
|
+
: { text: "未设置", cls: "off", title: "使用环境变量 HTTPS_PROXY / HTTP_PROXY" };
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: "add-component", group: "基础工具", title: "添加工作组件", icon: ICONS.addComponent,
|
|
453
|
+
kind: "add-component",
|
|
454
|
+
desc: "生成提示词,让 AI 在 local-components/ 写本地兼容模块;装好后可在管理页提交 PR。",
|
|
455
|
+
intro: "填写要接入的程序名称,复制下方命令粘贴给 AI。AI 按 docs/component-module.md 在 local-components/<id>/ 写本地兼容模块(与仓库自带同接口);不会下载进 tools/。写好后出现在「本地」分组,可在管理页「提交 PR」贡献到仓库。",
|
|
456
|
+
keys: [],
|
|
457
|
+
summary: function () {
|
|
458
|
+
return { text: "提示词工具", cls: "ready" };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
// 上游仓库外链:标题旁蓝色网址文字,新标签打开(rel=noopener)。无 url 的功能(媒体卡片 / 下载代理)不渲染。
|
|
465
|
+
function extLinkLabel(url) {
|
|
466
|
+
try {
|
|
467
|
+
var u = new URL(url);
|
|
468
|
+
var host = (u.hostname || "").replace(/^www\./, "");
|
|
469
|
+
var path = (u.pathname || "/").replace(/\/+$/, "") || "";
|
|
470
|
+
if (!host) return url;
|
|
471
|
+
if (!path || path === "/") return host;
|
|
472
|
+
var label = host + path;
|
|
473
|
+
if (u.search) label += u.search;
|
|
474
|
+
if (label.length > 42) label = label.slice(0, 39) + "\u2026";
|
|
475
|
+
return label;
|
|
476
|
+
} catch (e) {
|
|
477
|
+
return url.length > 42 ? url.slice(0, 39) + "\u2026" : url;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function extLink(f) {
|
|
481
|
+
if (!f || !f.url) return null;
|
|
482
|
+
var a = document.createElement("a");
|
|
483
|
+
a.className = "mm-ext";
|
|
484
|
+
a.href = f.url;
|
|
485
|
+
a.target = "_blank";
|
|
486
|
+
a.rel = "noopener noreferrer";
|
|
487
|
+
a.title = "打开上游:" + f.url;
|
|
488
|
+
a.setAttribute("aria-label", "打开上游仓库(新标签):" + extLinkLabel(f.url));
|
|
489
|
+
a.textContent = extLinkLabel(f.url);
|
|
490
|
+
a.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
491
|
+
a.addEventListener("keydown", function (e) { e.stopPropagation(); });
|
|
492
|
+
return a;
|
|
493
|
+
}
|
|
494
|
+
function titleName(f, cls) {
|
|
495
|
+
var wrap = el("div", cls || "mm-item-name");
|
|
496
|
+
wrap.append(document.createTextNode(f.title));
|
|
497
|
+
var link = extLink(f);
|
|
498
|
+
if (link) wrap.append(link);
|
|
499
|
+
return wrap;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var MODULE_SOURCE_TEXT = { bundled: "已验证", local: "本地" };
|
|
503
|
+
var CONTRIBUTE_COMPARE_DEFAULT = "https://github.com/meya-ashuripehya/dsh-multimodal/compare";
|
|
504
|
+
|
|
505
|
+
function sourceChip(src) {
|
|
506
|
+
var kind = src === "local" ? "local" : "bundled";
|
|
507
|
+
var span = el("span", "mm-src " + kind, MODULE_SOURCE_TEXT[kind] || kind);
|
|
508
|
+
span.title = kind === "local"
|
|
509
|
+
? "用户/AI 写在 local-components/ 的本地兼容组件,可提交 PR"
|
|
510
|
+
: "随 git 仓库分发的已兼容组件";
|
|
511
|
+
return span;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function maturityChip(kind) {
|
|
515
|
+
if (kind !== "experimental") return null;
|
|
516
|
+
var span = el("span", "mm-src experimental", "实验性");
|
|
517
|
+
span.title = "尚未开发完毕;启用后可能对对话造成不可逆破坏";
|
|
518
|
+
return span;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function titleNameWithSource(f, cls) {
|
|
522
|
+
var wrap = titleName(f, cls);
|
|
523
|
+
if (f.moduleSource === "bundled" || f.moduleSource === "local") wrap.append(sourceChip(f.moduleSource));
|
|
524
|
+
var m = maturityChip(f.maturity);
|
|
525
|
+
if (m) wrap.append(m);
|
|
526
|
+
return wrap;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
// 应用图标节点:只装 FEATURES 里的静态 SVG 常量;装饰性,读屏跳过。
|
|
531
|
+
function iconEl(f) {
|
|
532
|
+
var span = el("span", "mm-ico");
|
|
533
|
+
span.setAttribute("aria-hidden", "true");
|
|
534
|
+
if (f.icon) span.innerHTML = f.icon;
|
|
535
|
+
return span;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function el(tag, cls, text) {
|
|
539
|
+
var e = document.createElement(tag);
|
|
540
|
+
if (cls) e.className = cls;
|
|
541
|
+
if (text !== undefined) e.textContent = text;
|
|
542
|
+
return e;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// init 原样透传给 fetch(含可选的 signal,用于中止请求)。
|
|
546
|
+
function fetchJson(path, init) {
|
|
547
|
+
return fetch(API + path, Object.assign({ headers: { "content-type": "application/json" } }, init || {}))
|
|
548
|
+
.then(function (r) { return r.json(); });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// 基础工具「添加工作组件」:Token 声明 + 作者实测参考(非通用上限)。
|
|
552
|
+
var ADD_COMPONENT_TOKEN_WARN =
|
|
553
|
+
"注意:不同的 Coding Agent、不同模型在处理时所用的 Token 量与成本都不同。请确认后再复制。\n\n" +
|
|
554
|
+
"以作者使用 Deepseek Harness 作为 Coding Agent,模型使用 DeepSeek-V41-Flash、思考强度 Low,添加 Docker 组件时,Token 用量信息如下:\n" +
|
|
555
|
+
"· 本轮用量:1,572,371 tok\n" +
|
|
556
|
+
"· 提供方 / 模型:deepseek-official/deepseek-flash\n" +
|
|
557
|
+
"· 缓存命中:98.9%\n" +
|
|
558
|
+
"· 未缓存输入:17,855 tok\n" +
|
|
559
|
+
"· 缓存读取:1,542,144 tok\n" +
|
|
560
|
+
"· 输出:12,372 tok(其中推理 6,349 tok)";
|
|
561
|
+
|
|
562
|
+
function buildAddComponentPrompt(programName) {
|
|
563
|
+
var name = String(programName || "").trim();
|
|
564
|
+
return [
|
|
565
|
+
"请按下列步骤,为程序「" + name + "」设计并接入 dsh-workbench(本插件仓库:meya-ashuripehya/dsh-multimodal,设置页「工作组件」):",
|
|
566
|
+
"",
|
|
567
|
+
"1. 先确认 / 核实「" + name + "」:它是什么软件、主要运行平台(Windows / macOS / Linux)、是否暴露自动化能力(COM、HTTP API、CLI、官方/社区插件、MCP server 等)。用简短结论说明是否适合做成工作组件。",
|
|
568
|
+
"",
|
|
569
|
+
"2. 搜索可用于「" + name + "」的 MCP server 或其他自动化工具(GitHub / npm / PyPI / 官方文档),列出可选方案并简要对比(能力范围、协议、维护状态、许可证)。**选型优先:功能最全优先**,其次再看维护状态与许可证。",
|
|
570
|
+
"",
|
|
571
|
+
"3. 选定上游后,写成**本地兼容组件**(不要直接改仓库自带清单)。下列约定已内嵌,按此实现即可,**不要**再打开 docs/ 或其他源码文件(省 token):",
|
|
572
|
+
"",
|
|
573
|
+
"### 落盘位置",
|
|
574
|
+
"- 只创建:`local-components/<id>/index.mjs`(插件根下;该目录 gitignore)。",
|
|
575
|
+
"- `<id>`:小写 kebab,须匹配 `^[a-z][a-z0-9-]*$`;文件夹名、`export const id`、`component.id` 三者必须相同。现有风格如 `office` / `chrome` / `blender`,本地也可用 `demo-local` 这类带连字符的 id。",
|
|
576
|
+
"- **不要**占用已有 bundled id:`office` / `blender` / `unity` / `figma` / `photoshop` / `chrome` / `godot`。",
|
|
577
|
+
"- **不要**把文件写进 `src/components/`(那是仓库自带;合入仓库时才拷过去)。",
|
|
578
|
+
"- 可从本地模块相对导入宿主原语(勿整份复制):",
|
|
579
|
+
" - `../../src/components/shared.mjs`(`stdio` / `http` / `npmLaunch` / `installNpmTool` / `OFF` / `NOT_INSTALLED` / `resolveUv` / `resolveUvx` 等)",
|
|
580
|
+
" - `../../src/connect-lib.mjs`(`appRunning` / `result` / `noAppFor` / `mcpNotReady` / `callMcpTool` / `toolFailed` 等)",
|
|
581
|
+
" - `../../src/tools.mjs`(如 `installVenvTool` / `installOffice`,按需)",
|
|
582
|
+
"",
|
|
583
|
+
"### 必须导出(与 chrome / office 同约定;示例已内嵌)",
|
|
584
|
+
"",
|
|
585
|
+
"**(a) `export const id`** — 字符串,与文件夹名相同。例:`export const id = 'chrome'`。",
|
|
586
|
+
"",
|
|
587
|
+
"**(b) `export const meta`** — `{ id, title, group, url?, serverName, summary }`。",
|
|
588
|
+
"- chrome 例:`{ id: 'chrome', title: 'Chrome', group: '工作组件', url: 'https://github.com/ChromeDevTools/chrome-devtools-mcp', serverName: 'chrome', summary: '控制本机的 Google Chrome(Chrome DevTools MCP):网页操作、截图快照…' }`。",
|
|
589
|
+
"- office 例:`{ id: 'office', title: 'Office', group: '工作组件', url: 'https://github.com/officemcp/officemcp', serverName: 'officemcp', summary: '经 COM 控制本机的 Word / Excel / PowerPoint(OfficeMCP)。' }`。",
|
|
590
|
+
"- `serverName` 决定工具前缀 `mcp__<serverName>__`,选定后勿改。",
|
|
591
|
+
"- 启用键约定为 `<id>Enabled`(如 `chromeEnabled` / `officeEnabled`):manager 读取配置键 = id 拼接 Enabled;**新鲜安装 / 缺键时默认关闭**(不自动挂 MCP)。",
|
|
592
|
+
"- **launch 关闭判断**:写 `if (!cfg.<id>Enabled) return OFF`(或 `cfg.<id>Enabled !== true` / `{ ok:false, reason:'已在设置里关闭' }`)。仅显式 `true` 才挂载;缺键 / false 都关闭。宿主 `RuntimeSettingsSchema` 会为本地组件补上 `<id>Enabled` 默认 `false`。",
|
|
593
|
+
"",
|
|
594
|
+
"**(c) `export const app`** — `{ name, exe, match }`,供进程探测「已连接」。",
|
|
595
|
+
"- chrome:`{ name: 'Chrome', exe: 'chrome.exe', match: /^(chrome|google chrome( beta| dev| canary)?)$/ }`。",
|
|
596
|
+
"- office:`{ name: 'Office(Word / Excel / PowerPoint 等)', exe: 'WINWORD / EXCEL / POWERPNT', match: /^(winword|excel|powerpnt|visio|msaccess|winproj|outlook|mspub|onenote|wps|et|wpp|microsoft (word|excel|powerpoint))$/ }`。",
|
|
597
|
+
"- `match` 对着 `listProcesses()` 返回的**小写、已去掉 .exe** 的进程名。",
|
|
598
|
+
"",
|
|
599
|
+
"**(d) `export async function probe(ctx)`** — 只读探测,**不要**在探测里拉起目标程序。",
|
|
600
|
+
"- `ctx`:`{ cfg, tools, env, procs }`;函数内 `this` 绑定为 component。",
|
|
601
|
+
"- 高层流程(常见桌面应用):`const procs = await ctx.procs()` → 若 `!appRunning(procs, app)` 则 `return noAppFor(app)` → `const nr = mcpNotReady(ctx, this); if (nr) return nr` → 再用 TCP / HTTP / `callMcpTool` 确认可达。",
|
|
602
|
+
"- **例外(守护进程 / 远程 / WSL / 无头)**:进程名可能对不上 Desktop,但仍可达时——先 `mcpNotReady` + 工具/端口探测;工具成功即可 `connected`(文案可写「未见 Desktop 进程」);仅当工具失败且也没有匹配进程时才 `noAppFor`。",
|
|
603
|
+
"- 返回形状:`{ state, detail, via }`;`state` ∈ `connected` | `no-app` | `unreachable` | `mcp-down` | `checking`。",
|
|
604
|
+
"- 助手:`result(state, detail, via)`、`noAppFor(app)`、`toolFailed(r, detail, via)`(均来自 connect-lib)。",
|
|
605
|
+
"- blender 简例:进程在跑后检查插件端口 `tcpOpen('127.0.0.1', 9876)`,通则 `result('connected', '…', 'socket')`,不通则 `result('unreachable', '…', 'socket')`。",
|
|
606
|
+
"- office 简例:进程在跑且 MCP 就绪后 `callMcpTool(ctx.tools, this.serverName, 'RunningApps')`,解析结果后 `result('connected', 'COM 可连接:…', 'tool:RunningApps')`。",
|
|
607
|
+
"",
|
|
608
|
+
"**(e) `export const component` + `export default component`** — 注册给宿主的对象,至少包含:",
|
|
609
|
+
"- `id` / `label` / `url?` / `serverName` / `summary`(与 meta 对齐)。",
|
|
610
|
+
"- `keys: string[]`:变更后需重挂的设置键,通常含 `<id>Enabled`、包名键等。chrome 例:`['chromeEnabled','chromePackage','chromeConnect','chromeBrowserUrl','chromeChannel','chromeHeadless','chromeUserDataDir','chromeToolset','nodePath']`;office 例:`['officeEnabled','officeRepo','officeFolder','uvPath']`。",
|
|
611
|
+
"- `runtime?: 'node'`:npm 包组件设为 `'node'`;缺省按 Python/uv。",
|
|
612
|
+
"- `spec?(cfg)=>string`:包名,如 `(cfg) => cfg.chromePackage || 'chrome-devtools-mcp'` 或 `(cfg) => cfg.blenderPackage || 'mcp-for-blender'`。",
|
|
613
|
+
"- `bin?` / `installArgs?`:按上游需要。",
|
|
614
|
+
"- `installed(cfg)=>boolean`:是否已落到运行时目录 `tools/<id>/`(可用 shared 的 `managedNpmEntry` / `managedEntry` 等)。",
|
|
615
|
+
"- `install?(cfg, task, hooks)`:设置页「下载安装」会调它;可委托 `installNpmTool(this, cfg, task, hooks)` 或 `installVenvTool(...)` / 专用安装器。**写好即可,本任务不要执行下载。**",
|
|
616
|
+
"- `launch(cfg)` → LaunchPlan:成功 `{ ok:true, source, config, runtime?, via?, viaUvx? }`;失败 `{ ok:false, reason, missing? }`。关闭时返回 `OFF` 或 `{ ok:false, reason:'已在设置里关闭' }`(`!cfg.<id>Enabled` / 缺键 / false 均关闭;仅 `true` 启用)。`config` 一般用 `stdio(serverName, command, args, opts)` 或 `npmLaunch` / `http`。",
|
|
617
|
+
"- 可选:`note(cfg)`、`beforeRemove(log?)`、`installAddon(cfg, project, opts?)`(如 Godot 装插件到项目)。",
|
|
618
|
+
"",
|
|
619
|
+
"### MCP 包落地位置(重要)",
|
|
620
|
+
"- 下载安装的产物只应落在插件根下 **`tools/<id>/`**(运行时目录,gitignore)。",
|
|
621
|
+
"- **只有**用户在设置页点击「下载安装」时,宿主才会调用 `component.install` 写入该目录。",
|
|
622
|
+
"- **本 AI 任务禁止**自动下载 / 解压 / `npm install` / `uv` / 联网装包进 `tools/`;只添加模块 + 安装元数据(`spec` / `installed` / `install` / `launch`),以便设置页稍后下载。",
|
|
623
|
+
"",
|
|
624
|
+
"### 注册表(本地 = drop-in 发现)",
|
|
625
|
+
"- **本地**:启动时 `registry.mjs` 扫描 `local-components/*/index.mjs` 并动态 `import()`,与 bundled 合并;**不必**编辑 `registry.mjs`。",
|
|
626
|
+
"- 与 bundled **同 id** 时 bundled 优先,本地副本被忽略并 warn。",
|
|
627
|
+
"- **bundled**(本次不要做):才需要在 `registry.mjs` 加静态 import,并改 `SettingsSchema` / `lib/client.js` 的 FEATURES / ICONS / INPUT_FIELDS。",
|
|
628
|
+
"",
|
|
629
|
+
"### 设置页 UI(本地还要不要手同步)",
|
|
630
|
+
"- 详情页在 `/components` 拉取后,对 `moduleSource === 'local'` **自动**补齐「本地组件」条目(`ensureLocalFeature`):标题用 `label`、简介用 `summary`、图标用通用「添加」图标。",
|
|
631
|
+
"- 后端暴露后**不必**再改 `FEATURES` / `ICONS` / `INPUT_FIELDS` 也能在列表和详情页露脸(含状态、连接、本地下载安装、卸载)。",
|
|
632
|
+
"- **详情页已有「启用」开关**(与 bundled 相同,安装行右侧 `role=switch`):键名 `<id>Enabled`,经 settings 持久化;**缺省关闭**(仅显式 `true` 启用,与 launch / manager 一致)。",
|
|
633
|
+
"- AI **可以/应该**为组件增加可配置或必填参数(如 host、package、path、token 占位等),并给出清晰的中文 label / 帮助文案;**优先补上有用参数,而不是省略**。本地阶段:在 `keys` 中声明,并在 `launch()` / `spec()` 里硬编码默认值;每个参数在模块注释与 `meta.summary` / `note` 中说明。若这些参数也需要 UI 字段,在注释中写出拟议的 `SettingsSchema` / `INPUT_FIELDS` 形状(含中文 label / desc),供后续 PR;在此之前详情页**不会**渲染这些 `keys` 输入,只有状态、启用开关、本地下载安装、贡献。",
|
|
634
|
+
"- **未进 schema 的自定义键**(除已由 API 合并保留的本地 `*Enabled` 外)不会进持久化 `cfg`;本地仍按上条在 `spec()` / `launch()` 硬编码默认(如 `cfg.xxxPackage || 'upstream-pkg'`,可选路径 `String(cfg.xxx || '').trim()`)——此时无法在 UI 修改。`keys` + 模块注释 + 拟议 schema 形状保留,进仓库时再补 schema / INPUT_FIELDS。",
|
|
635
|
+
"- 本提示词**不要** commit / push / 开 PR。",
|
|
636
|
+
"",
|
|
637
|
+
"4. 模块就位后,用户可在设置页「本地」分组看到它;用户可重启 DSH 后下载。",
|
|
638
|
+
"",
|
|
639
|
+
"请从第 1 步开始执行,并在关键结论后再改代码。程序名称:" + name + "。"
|
|
640
|
+
].join("\n");
|
|
641
|
+
}
|
|
642
|
+
function copyTextToClipboard(text) {
|
|
643
|
+
function fallback() {
|
|
644
|
+
var ta = document.createElement("textarea");
|
|
645
|
+
ta.value = text;
|
|
646
|
+
ta.setAttribute("readonly", "");
|
|
647
|
+
ta.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:none;opacity:0;";
|
|
648
|
+
document.body.appendChild(ta);
|
|
649
|
+
ta.focus();
|
|
650
|
+
ta.select();
|
|
651
|
+
ta.setSelectionRange(0, text.length);
|
|
652
|
+
var ok = false;
|
|
653
|
+
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
|
|
654
|
+
document.body.removeChild(ta);
|
|
655
|
+
return ok;
|
|
656
|
+
}
|
|
657
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
658
|
+
return navigator.clipboard.writeText(text).then(function () { return true; }, function () { return fallback(); });
|
|
659
|
+
}
|
|
660
|
+
return Promise.resolve(fallback());
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function renderSettingsPage() {
|
|
664
|
+
var page = el("div", "mm-page");
|
|
665
|
+
var style = document.createElement("style");
|
|
666
|
+
style.textContent = STYLES;
|
|
667
|
+
page.append(style);
|
|
668
|
+
|
|
669
|
+
var inputs = {};
|
|
670
|
+
var statusLines = {}; // 组件 id -> 管理页里的状态行
|
|
671
|
+
var connLines = {}; // 组件 id -> 管理页里的连接行(只有工作组件有)
|
|
672
|
+
var installUis = {}; // 组件 id -> 管理页里的安装按钮 / 日志
|
|
673
|
+
var badges = {}; // 功能 id -> [列表里的状态徽标, 管理页标题旁的状态徽标]
|
|
674
|
+
var rows = {}; // 功能 id -> 列表行
|
|
675
|
+
var details = {}; // 功能 id -> { view, back, save, msg }
|
|
676
|
+
var toggles = {}; // 设置 key -> 「启用」旋钮 { key, btn, msg, saving }
|
|
677
|
+
var actions = {}; // 组件 id -> 字段旁的动作按钮 { key, btn, msg, path, busy, ready }(如 Godot「安装插件到项目」)
|
|
678
|
+
var currentView = null; // 当前打开的管理页(功能 id);null 为列表。只记在本页实例里。
|
|
679
|
+
var lastValue = null; // 最近一次从宿主读到 / 保存后的完整设置
|
|
680
|
+
var byComponent = {};
|
|
681
|
+
FEATURES.forEach(function (f) { if (f.component) byComponent[f.component] = f; });
|
|
682
|
+
var localGroupEl = null; // 「本地兼容」分组标题(动态)
|
|
683
|
+
var localListEl = null; // 本地组件列表容器
|
|
684
|
+
var compareUrlCached = CONTRIBUTE_COMPARE_DEFAULT;
|
|
685
|
+
|
|
686
|
+
// ── 列表(首页)──
|
|
687
|
+
var listView = el("div", "mm-view mm-home");
|
|
688
|
+
listView.append(el("h3", undefined, "工作组件(dsh-workbench)"));
|
|
689
|
+
listView.append(el("p", "mm-sub", "点击一项进入它的管理页:开关、配置、运行状态和下载安装。保存后只重新挂载改动过的组件。"));
|
|
690
|
+
var sub = el("p", "mm-sub", "正在读取设置…");
|
|
691
|
+
listView.append(sub);
|
|
692
|
+
page.append(listView);
|
|
693
|
+
|
|
694
|
+
var lastGroup = null;
|
|
695
|
+
var list = null;
|
|
696
|
+
FEATURES.forEach(function (f) {
|
|
697
|
+
if (f.group !== lastGroup) {
|
|
698
|
+
lastGroup = f.group;
|
|
699
|
+
listView.append(el("div", "mm-group", f.group));
|
|
700
|
+
list = el("div", "mm-list");
|
|
701
|
+
list.setAttribute("role", "list");
|
|
702
|
+
listView.append(list);
|
|
703
|
+
}
|
|
704
|
+
var item = el("div", "mm-item");
|
|
705
|
+
item.setAttribute("role", "button");
|
|
706
|
+
item.tabIndex = 0;
|
|
707
|
+
item.setAttribute("data-feature", f.id);
|
|
708
|
+
var main = el("div", "mm-item-main");
|
|
709
|
+
main.append(titleNameWithSource(f, "mm-item-name"), el("div", "mm-item-desc", f.desc));
|
|
710
|
+
var badge = el("span", "mm-badge off", "读取中…");
|
|
711
|
+
var chev = el("span", "mm-chev", "›");
|
|
712
|
+
chev.setAttribute("aria-hidden", "true");
|
|
713
|
+
item.append(iconEl(f), main, badge, chev);
|
|
714
|
+
item.addEventListener("click", function () { openDetail(f.id); });
|
|
715
|
+
item.addEventListener("keydown", function (e) {
|
|
716
|
+
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
|
|
717
|
+
e.preventDefault();
|
|
718
|
+
openDetail(f.id);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
rows[f.id] = item;
|
|
722
|
+
badges[f.id] = [badge];
|
|
723
|
+
list.append(item);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
function openCompareUrl(url) {
|
|
728
|
+
var u = url || compareUrlCached || CONTRIBUTE_COMPARE_DEFAULT;
|
|
729
|
+
try { window.open(u, "_blank", "noopener,noreferrer"); } catch (e) { /* ignore */ }
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function buildContributePanel(f) {
|
|
733
|
+
var box = el("div", "mm-pr");
|
|
734
|
+
box.setAttribute("data-contribute", f.component || f.id);
|
|
735
|
+
box.append(el("h4", undefined, "贡献到仓库"));
|
|
736
|
+
box.append(el("p", undefined, "安全默认:复制 PR 清单与命令到剪贴板,并打开 GitHub compare。不会自动 commit / push / force-push;只有你在本机确认后才应手动跑 git/gh。"));
|
|
737
|
+
var row = el("div", "mm-pr-row");
|
|
738
|
+
var copyBtn = el("button", "mm-btn", "提交 PR(复制清单)");
|
|
739
|
+
copyBtn.type = "button";
|
|
740
|
+
var openBtn = el("button", "mm-btn mm-ghost", "贡献到仓库(Compare)");
|
|
741
|
+
openBtn.type = "button";
|
|
742
|
+
var msg = el("span", "mm-msg");
|
|
743
|
+
row.append(copyBtn, openBtn, msg);
|
|
744
|
+
var pre = el("pre");
|
|
745
|
+
pre.hidden = true;
|
|
746
|
+
box.append(row, pre);
|
|
747
|
+
var lastInfo = null;
|
|
748
|
+
function fillPre(text) {
|
|
749
|
+
pre.textContent = text || "";
|
|
750
|
+
pre.hidden = !text;
|
|
751
|
+
}
|
|
752
|
+
copyBtn.addEventListener("click", function () {
|
|
753
|
+
if (disposed) return;
|
|
754
|
+
copyBtn.disabled = true;
|
|
755
|
+
msg.textContent = "生成清单…";
|
|
756
|
+
msg.className = "mm-msg";
|
|
757
|
+
pageFetch("/components/" + encodeURIComponent(f.component || f.id) + "/contribute").then(function (d) {
|
|
758
|
+
if (disposed) return;
|
|
759
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "无法生成贡献说明");
|
|
760
|
+
lastInfo = d;
|
|
761
|
+
if (d.compareUrl) compareUrlCached = d.compareUrl;
|
|
762
|
+
fillPre(d.checklist || "");
|
|
763
|
+
return copyTextToClipboard(d.checklist || "");
|
|
764
|
+
}).then(function (ok) {
|
|
765
|
+
if (disposed) return;
|
|
766
|
+
msg.textContent = ok ? "已复制清单(含命令模板)" : "复制失败,请从下方全文手动复制";
|
|
767
|
+
msg.className = ok ? "mm-msg ok" : "mm-msg err";
|
|
768
|
+
}).catch(function (e) {
|
|
769
|
+
if (disposed || isAbort(e)) return;
|
|
770
|
+
msg.textContent = e.message || String(e);
|
|
771
|
+
msg.className = "mm-msg err";
|
|
772
|
+
}).finally(function () {
|
|
773
|
+
if (disposed) return;
|
|
774
|
+
copyBtn.disabled = false;
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
openBtn.addEventListener("click", function () {
|
|
778
|
+
if (disposed) return;
|
|
779
|
+
var url = (lastInfo && lastInfo.compareUrl) || compareUrlCached || CONTRIBUTE_COMPARE_DEFAULT;
|
|
780
|
+
// 二次确认:仅打开浏览器,不执行 git
|
|
781
|
+
if (!window.confirm("打开 GitHub compare 页面?\\n\\n不会在本机执行 git push / gh pr create。\\n" + url)) return;
|
|
782
|
+
openCompareUrl(url);
|
|
783
|
+
msg.textContent = "已尝试打开浏览器";
|
|
784
|
+
msg.className = "mm-msg ok";
|
|
785
|
+
});
|
|
786
|
+
return box;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function ensureLocalGroup() {
|
|
790
|
+
if (localListEl) return localListEl;
|
|
791
|
+
localGroupEl = el("div", "mm-group", "本地兼容");
|
|
792
|
+
localGroupEl.title = "来自 local-components/ 的用户本地模块(可提交 PR)";
|
|
793
|
+
localListEl = el("div", "mm-list");
|
|
794
|
+
localListEl.setAttribute("role", "list");
|
|
795
|
+
// 插在「工作组件」之后、「通用」之前:找到第一个 group===通用 的行,插到其 group 标题前
|
|
796
|
+
var general = null;
|
|
797
|
+
FEATURES.forEach(function (f) {
|
|
798
|
+
if (!general && f.group === "通用" && rows[f.id]) general = rows[f.id].parentNode && rows[f.id].parentNode.previousSibling;
|
|
799
|
+
});
|
|
800
|
+
// fallback:追加在列表视图末尾(返回按钮区域之前)
|
|
801
|
+
var anchorNode = null;
|
|
802
|
+
FEATURES.some(function (f) {
|
|
803
|
+
if (f.group === "通用" && rows[f.id]) {
|
|
804
|
+
// group title is previous sibling of the list containing the row
|
|
805
|
+
var list = rows[f.id].parentNode;
|
|
806
|
+
anchorNode = list && list.previousSibling; // mm-group 通用
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
});
|
|
811
|
+
if (anchorNode && anchorNode.parentNode === listView) {
|
|
812
|
+
listView.insertBefore(localGroupEl, anchorNode);
|
|
813
|
+
listView.insertBefore(localListEl, anchorNode);
|
|
814
|
+
} else {
|
|
815
|
+
listView.append(localGroupEl, localListEl);
|
|
816
|
+
}
|
|
817
|
+
return localListEl;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function buildLocalDetail(f) {
|
|
821
|
+
var view = el("div", "mm-view mm-detail");
|
|
822
|
+
view.hidden = true;
|
|
823
|
+
view.setAttribute("data-feature", f.id);
|
|
824
|
+
view.setAttribute("role", "region");
|
|
825
|
+
view.setAttribute("aria-label", f.title);
|
|
826
|
+
var head = el("div", "mm-dhead");
|
|
827
|
+
var back = el("button", "mm-btn mm-ghost mm-back", "‹ 返回");
|
|
828
|
+
back.type = "button";
|
|
829
|
+
back.title = "返回功能列表(Esc)";
|
|
830
|
+
back.addEventListener("click", showList);
|
|
831
|
+
var hbadge = el("span", "mm-badge off", "读取中…");
|
|
832
|
+
var title = el("h3");
|
|
833
|
+
title.append(iconEl(f), el("span", "mm-dtitle", f.title));
|
|
834
|
+
var _ext = extLink(f);
|
|
835
|
+
if (_ext) title.append(_ext);
|
|
836
|
+
title.append(sourceChip("local"));
|
|
837
|
+
head.append(back, title, hbadge);
|
|
838
|
+
badges[f.id] = badges[f.id] || [];
|
|
839
|
+
badges[f.id].push(hbadge);
|
|
840
|
+
view.append(head, buildIntro(f));
|
|
841
|
+
statusLines[f.component] = el("div", "mm-status off", "状态读取中…");
|
|
842
|
+
view.append(statusLines[f.component]);
|
|
843
|
+
var cl = connLines[f.component] = el("div", "mm-conn");
|
|
844
|
+
cl.hidden = true;
|
|
845
|
+
cl.setAttribute("aria-live", "polite");
|
|
846
|
+
view.append(cl);
|
|
847
|
+
var ui = buildInstallUi(f.component);
|
|
848
|
+
installUis[f.component] = ui;
|
|
849
|
+
// Enable switch: same as bundled work components — right side of install row; persist via settings <id>Enabled.
|
|
850
|
+
var enabledField = ensureLocalEnabledField(f.component);
|
|
851
|
+
if (!f.keys || !f.keys.length) f.keys = [enabledField.key];
|
|
852
|
+
var tg = buildToggle(enabledField);
|
|
853
|
+
ui.row.insertBefore(tg.wrap, ui.state);
|
|
854
|
+
view.append(ui.row, tg.note, ui.cnote, ui.details);
|
|
855
|
+
// /components often arrives after /settings: sync switch from lastValue (default OFF) and unlock.
|
|
856
|
+
if (lastValue) {
|
|
857
|
+
var _hasEn = Object.prototype.hasOwnProperty.call(lastValue, enabledField.key);
|
|
858
|
+
setSwitch(enabledField.key, _hasEn ? !!lastValue[enabledField.key] : true);
|
|
859
|
+
if (toggles[enabledField.key]) toggles[enabledField.key].btn.disabled = false;
|
|
860
|
+
}
|
|
861
|
+
view.append(buildContributePanel(f));
|
|
862
|
+
var foot = el("div", "mm-row");
|
|
863
|
+
var msg = el("span", "mm-msg");
|
|
864
|
+
foot.append(msg);
|
|
865
|
+
view.append(foot);
|
|
866
|
+
details[f.id] = { view: view, back: back, save: null, msg: msg };
|
|
867
|
+
page.append(view);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function ensureLocalFeature(c) {
|
|
871
|
+
if (!c || c.kind === "prerequisite") return;
|
|
872
|
+
if (c.moduleSource !== "local") return;
|
|
873
|
+
if (byComponent[c.id]) return;
|
|
874
|
+
var f = {
|
|
875
|
+
id: c.id,
|
|
876
|
+
group: "本地兼容",
|
|
877
|
+
title: c.label || c.id,
|
|
878
|
+
component: c.id,
|
|
879
|
+
moduleSource: "local",
|
|
880
|
+
icon: ICONS.addComponent,
|
|
881
|
+
url: c.url || null,
|
|
882
|
+
desc: c.summary || "本地兼容组件(local-components/" + c.id + ")",
|
|
883
|
+
intro: (c.summary || "") + " 来源:本地目录 local-components/" + c.id + "/。可用下方「贡献到仓库」生成 PR 清单。",
|
|
884
|
+
keys: [c.id + "Enabled"]
|
|
885
|
+
};
|
|
886
|
+
ensureLocalEnabledField(c.id);
|
|
887
|
+
FEATURES.push(f);
|
|
888
|
+
byComponent[c.id] = f;
|
|
889
|
+
var list = ensureLocalGroup();
|
|
890
|
+
var item = el("div", "mm-item");
|
|
891
|
+
item.setAttribute("role", "button");
|
|
892
|
+
item.tabIndex = 0;
|
|
893
|
+
item.setAttribute("data-feature", f.id);
|
|
894
|
+
var main = el("div", "mm-item-main");
|
|
895
|
+
main.append(titleNameWithSource(f, "mm-item-name"), el("div", "mm-item-desc", f.desc));
|
|
896
|
+
var badge = el("span", "mm-badge off", "读取中…");
|
|
897
|
+
var chev = el("span", "mm-chev", "›");
|
|
898
|
+
chev.setAttribute("aria-hidden", "true");
|
|
899
|
+
item.append(iconEl(f), main, badge, chev);
|
|
900
|
+
item.addEventListener("click", function () { openDetail(f.id); });
|
|
901
|
+
item.addEventListener("keydown", function (e) {
|
|
902
|
+
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
|
|
903
|
+
e.preventDefault();
|
|
904
|
+
openDetail(f.id);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
rows[f.id] = item;
|
|
908
|
+
badges[f.id] = [badge];
|
|
909
|
+
list.append(item);
|
|
910
|
+
buildLocalDetail(f);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function syncLocalFromComponents(components, meta) {
|
|
914
|
+
if (meta && meta.contributeCompareUrl) compareUrlCached = meta.contributeCompareUrl;
|
|
915
|
+
(components || []).forEach(ensureLocalFeature);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
// ── 各功能的管理页(一次建好,切换时只改 hidden;轮询不管在哪一页都更新这里)──
|
|
920
|
+
FEATURES.forEach(function (f) {
|
|
921
|
+
var view = el("div", "mm-view mm-detail");
|
|
922
|
+
view.hidden = true;
|
|
923
|
+
view.setAttribute("data-feature", f.id);
|
|
924
|
+
view.setAttribute("role", "region");
|
|
925
|
+
view.setAttribute("aria-label", f.title);
|
|
926
|
+
var head = el("div", "mm-dhead");
|
|
927
|
+
var back = el("button", "mm-btn mm-ghost mm-back", "‹ 返回");
|
|
928
|
+
back.type = "button";
|
|
929
|
+
back.title = "返回功能列表(Esc)";
|
|
930
|
+
back.addEventListener("click", showList);
|
|
931
|
+
var hbadge = el("span", "mm-badge off", "读取中…");
|
|
932
|
+
var title = el("h3");
|
|
933
|
+
title.append(iconEl(f), el("span", "mm-dtitle", f.title));
|
|
934
|
+
var _ext = extLink(f);
|
|
935
|
+
if (_ext) title.append(_ext);
|
|
936
|
+
if (f.moduleSource === "bundled" || f.moduleSource === "local") title.append(sourceChip(f.moduleSource));
|
|
937
|
+
var _mat = maturityChip(f.maturity);
|
|
938
|
+
if (_mat) title.append(_mat);
|
|
939
|
+
head.append(back, title, hbadge);
|
|
940
|
+
badges[f.id].push(hbadge);
|
|
941
|
+
view.append(head, buildIntro(f));
|
|
942
|
+
if (f.warn) {
|
|
943
|
+
var fwarn = el("div", "mm-warn", f.warn);
|
|
944
|
+
fwarn.setAttribute("role", "note");
|
|
945
|
+
view.append(fwarn);
|
|
946
|
+
}
|
|
947
|
+
// 基础工具「添加工作组件」:警告 + 程序名称 + 复制命令(无设置字段 / 保存)
|
|
948
|
+
if (f.kind === "add-component") {
|
|
949
|
+
var warn = el("div", "mm-warn", ADD_COMPONENT_TOKEN_WARN);
|
|
950
|
+
warn.setAttribute("role", "note");
|
|
951
|
+
var nameField = el("div", "mm-field");
|
|
952
|
+
var nameLab = el("label", undefined, "程序名称");
|
|
953
|
+
nameLab.setAttribute("for", "mm-add-component-name");
|
|
954
|
+
var nameDesc = el("div", "mm-desc", "要接入的软件名,例如 Blender、Notion、After Effects。");
|
|
955
|
+
var nameInput = el("input", "mm-input");
|
|
956
|
+
nameInput.type = "text";
|
|
957
|
+
nameInput.id = "mm-add-component-name";
|
|
958
|
+
nameInput.placeholder = "例如 Notion";
|
|
959
|
+
nameInput.autocomplete = "off";
|
|
960
|
+
nameField.append(nameLab, nameDesc, nameInput);
|
|
961
|
+
var crow = el("div", "mm-copy-row");
|
|
962
|
+
var copyBtn = el("button", "mm-btn", "复制命令");
|
|
963
|
+
copyBtn.type = "button";
|
|
964
|
+
copyBtn.disabled = true;
|
|
965
|
+
var cmsg = el("span", "mm-msg");
|
|
966
|
+
crow.append(copyBtn, cmsg);
|
|
967
|
+
view.append(warn, nameField, crow);
|
|
968
|
+
function syncCopyEnabled() {
|
|
969
|
+
if (disposed) return;
|
|
970
|
+
copyBtn.disabled = !String(nameInput.value || "").trim();
|
|
971
|
+
}
|
|
972
|
+
nameInput.addEventListener("input", syncCopyEnabled);
|
|
973
|
+
nameInput.addEventListener("change", syncCopyEnabled);
|
|
974
|
+
copyBtn.addEventListener("click", function () {
|
|
975
|
+
if (disposed) return;
|
|
976
|
+
var name = String(nameInput.value || "").trim();
|
|
977
|
+
if (!name) {
|
|
978
|
+
cmsg.textContent = "请先填写程序名称";
|
|
979
|
+
cmsg.className = "mm-msg err";
|
|
980
|
+
copyBtn.disabled = true;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
copyBtn.disabled = true;
|
|
984
|
+
cmsg.textContent = "复制中…";
|
|
985
|
+
cmsg.className = "mm-msg";
|
|
986
|
+
copyTextToClipboard(buildAddComponentPrompt(name)).then(function (ok) {
|
|
987
|
+
if (disposed) return;
|
|
988
|
+
if (ok) {
|
|
989
|
+
cmsg.textContent = "已复制到剪贴板";
|
|
990
|
+
cmsg.className = "mm-msg ok";
|
|
991
|
+
} else {
|
|
992
|
+
cmsg.textContent = "复制失败,请手动全选提示词";
|
|
993
|
+
cmsg.className = "mm-msg err";
|
|
994
|
+
}
|
|
995
|
+
}).finally(function () {
|
|
996
|
+
if (disposed) return;
|
|
997
|
+
syncCopyEnabled();
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
details[f.id] = { view: view, back: back, save: null, msg: cmsg };
|
|
1001
|
+
page.append(view);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
var ui = null;
|
|
1005
|
+
if (f.component) {
|
|
1006
|
+
statusLines[f.component] = el("div", "mm-status off", "状态读取中…");
|
|
1007
|
+
view.append(statusLines[f.component]);
|
|
1008
|
+
if (f.group === "工作组件") {
|
|
1009
|
+
var cl = connLines[f.component] = el("div", "mm-conn");
|
|
1010
|
+
cl.hidden = true;
|
|
1011
|
+
cl.setAttribute("aria-live", "polite");
|
|
1012
|
+
view.append(cl);
|
|
1013
|
+
}
|
|
1014
|
+
ui = installUis[f.component] = buildInstallUi(f.component);
|
|
1015
|
+
view.append(ui.row);
|
|
1016
|
+
}
|
|
1017
|
+
// 「启用」旋钮:放进安装按钮那一行(安装步骤之前,靠右);说明文字放在这一行下面。
|
|
1018
|
+
f.keys.forEach(function (k) {
|
|
1019
|
+
if (FIELD_BY_KEY[k].type !== "switch") return;
|
|
1020
|
+
var tg = buildToggle(FIELD_BY_KEY[k]);
|
|
1021
|
+
if (ui) ui.row.insertBefore(tg.wrap, ui.state);
|
|
1022
|
+
else {
|
|
1023
|
+
var trow = el("div", "mm-install");
|
|
1024
|
+
trow.append(tg.wrap);
|
|
1025
|
+
view.append(trow);
|
|
1026
|
+
}
|
|
1027
|
+
view.append(tg.note);
|
|
1028
|
+
});
|
|
1029
|
+
if (ui) view.append(ui.cnote, ui.details);
|
|
1030
|
+
f.keys.forEach(function (k) { if (FIELD_BY_KEY[k].type !== "switch") view.append(buildField(FIELD_BY_KEY[k])); });
|
|
1031
|
+
var srow = el("div", "mm-row");
|
|
1032
|
+
var save = el("button", "mm-btn", "保存");
|
|
1033
|
+
save.type = "button";
|
|
1034
|
+
save.disabled = true; // 设置读到之前不能保存,免得用空值覆盖
|
|
1035
|
+
var msg = el("span", "mm-msg");
|
|
1036
|
+
srow.append(save, msg);
|
|
1037
|
+
view.append(srow);
|
|
1038
|
+
details[f.id] = { view: view, back: back, save: save, msg: msg };
|
|
1039
|
+
save.addEventListener("click", function () { saveFeature(f); });
|
|
1040
|
+
page.append(view);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
function buildIntro(f) {
|
|
1044
|
+
return el("p", "mm-sub", f.intro || "");
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// 「启用」旋钮:设置读到之前禁用;拨动后立即保存(见 toggleSetting)。
|
|
1048
|
+
function buildToggle(f) {
|
|
1049
|
+
var wrap = el("div", "mm-toggle");
|
|
1050
|
+
var msg = el("span", "mm-tmsg");
|
|
1051
|
+
msg.setAttribute("aria-live", "polite");
|
|
1052
|
+
var btn = el("button", "mm-switch");
|
|
1053
|
+
btn.type = "button";
|
|
1054
|
+
btn.id = "mm-sw-" + f.key + "-" + Math.random().toString(36).slice(2, 8);
|
|
1055
|
+
btn.setAttribute("role", "switch");
|
|
1056
|
+
btn.setAttribute("aria-checked", "false");
|
|
1057
|
+
btn.disabled = true;
|
|
1058
|
+
btn.title = f.desc;
|
|
1059
|
+
var label = el("label", undefined, f.label);
|
|
1060
|
+
label.htmlFor = btn.id;
|
|
1061
|
+
var note = el("div", "mm-note", f.desc);
|
|
1062
|
+
note.id = btn.id + "-note";
|
|
1063
|
+
btn.setAttribute("aria-describedby", note.id);
|
|
1064
|
+
wrap.append(msg, label, btn);
|
|
1065
|
+
var t = { key: f.key, btn: btn, msg: msg, saving: false };
|
|
1066
|
+
toggles[f.key] = t;
|
|
1067
|
+
btn.addEventListener("click", function () { toggleSetting(t); });
|
|
1068
|
+
return { wrap: wrap, note: note };
|
|
1069
|
+
}
|
|
1070
|
+
function setSwitch(k, on) {
|
|
1071
|
+
var t = toggles[k];
|
|
1072
|
+
if (!t || t.saving) return; // 正在保存的旋钮由保存结果决定
|
|
1073
|
+
t.btn.setAttribute("aria-checked", on ? "true" : "false");
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function buildField(f) {
|
|
1077
|
+
var wrap = el("div", "mm-field");
|
|
1078
|
+
var label = el("label", undefined, f.label);
|
|
1079
|
+
var input;
|
|
1080
|
+
if (f.type === "select") {
|
|
1081
|
+
input = el("select", "mm-input");
|
|
1082
|
+
// 选项可以是字符串(值即文字),也可以是 { value, label }。
|
|
1083
|
+
f.options.forEach(function (o) {
|
|
1084
|
+
var value = typeof o === "string" ? o : o.value;
|
|
1085
|
+
var opt = el("option", undefined, typeof o === "string" ? o : o.label);
|
|
1086
|
+
opt.value = value;
|
|
1087
|
+
input.append(opt);
|
|
1088
|
+
});
|
|
1089
|
+
} else {
|
|
1090
|
+
input = el("input", "mm-input");
|
|
1091
|
+
input.type = f.type;
|
|
1092
|
+
if (f.min !== undefined) input.min = String(f.min);
|
|
1093
|
+
if (f.max !== undefined) input.max = String(f.max);
|
|
1094
|
+
if (f.type === "password") {
|
|
1095
|
+
// 令牌类:不让浏览器记住 / 自动填充,也不做拼写检查。
|
|
1096
|
+
input.autocomplete = "new-password";
|
|
1097
|
+
input.spellcheck = false;
|
|
1098
|
+
input.setAttribute("data-secret", "true");
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (f.placeholder) input.placeholder = f.placeholder;
|
|
1102
|
+
input.id = "mm-f-" + f.key + "-" + Math.random().toString(36).slice(2, 8);
|
|
1103
|
+
label.htmlFor = input.id;
|
|
1104
|
+
inputs[f.key] = input;
|
|
1105
|
+
wrap.append(label, input);
|
|
1106
|
+
if (f.action) wrap.append(buildAction(f));
|
|
1107
|
+
wrap.append(el("span", "mm-desc", f.desc));
|
|
1108
|
+
return wrap;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// 字段旁的动作按钮:组件装好之前禁用(renderInstall 里更新);点了用输入框当前的值调用接口,成功后顺手保存这个字段。
|
|
1112
|
+
function buildAction(f) {
|
|
1113
|
+
var row = el("div", "mm-row mm-action");
|
|
1114
|
+
var btn = el("button", "mm-btn mm-ghost", f.action.label);
|
|
1115
|
+
btn.type = "button";
|
|
1116
|
+
btn.disabled = true;
|
|
1117
|
+
btn.title = "先「下载安装」";
|
|
1118
|
+
var msg = el("span", "mm-amsg");
|
|
1119
|
+
msg.setAttribute("aria-live", "polite");
|
|
1120
|
+
row.append(btn, msg);
|
|
1121
|
+
var a = actions[f.action.component] = { key: f.key, btn: btn, msg: msg, path: f.action.path, busy: false, ready: false };
|
|
1122
|
+
btn.addEventListener("click", function () { runAction(a); });
|
|
1123
|
+
return row;
|
|
1124
|
+
}
|
|
1125
|
+
function runAction(a) {
|
|
1126
|
+
if (disposed || a.busy || a.btn.disabled) return;
|
|
1127
|
+
var value = inputs[a.key].value.trim();
|
|
1128
|
+
function say(text, cls) {
|
|
1129
|
+
a.msg.textContent = text;
|
|
1130
|
+
a.msg.className = "mm-amsg" + (cls ? " " + cls : "");
|
|
1131
|
+
}
|
|
1132
|
+
if (!value) {
|
|
1133
|
+
say("先填 " + FIELD_BY_KEY[a.key].label, "err");
|
|
1134
|
+
inputs[a.key].focus();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
a.busy = true;
|
|
1138
|
+
a.btn.disabled = true;
|
|
1139
|
+
say("正在安装…(第一次要先下载插件包)");
|
|
1140
|
+
var body = {};
|
|
1141
|
+
body.project = value;
|
|
1142
|
+
pageFetch(a.path, { method: "POST", body: JSON.stringify(body) }).then(function (d) {
|
|
1143
|
+
if (disposed) return;
|
|
1144
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "安装失败");
|
|
1145
|
+
say(d.message || "已完成", "ok");
|
|
1146
|
+
// 记住这次用的路径(只改这一个字段)。
|
|
1147
|
+
if (lastValue && lastValue[a.key] !== value) {
|
|
1148
|
+
return postSettings(function (patch) { patch[a.key] = value; return patch; }).then(function () {}, function () {});
|
|
1149
|
+
}
|
|
1150
|
+
}).catch(function (e) {
|
|
1151
|
+
if (disposed || isAbort(e)) return;
|
|
1152
|
+
say(e.message, "err");
|
|
1153
|
+
}).finally(function () {
|
|
1154
|
+
if (disposed) return;
|
|
1155
|
+
a.busy = false;
|
|
1156
|
+
a.btn.disabled = !a.ready;
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// ── 页内导航(不改 location,避免干扰宿主路由)──
|
|
1161
|
+
function openDetail(id) {
|
|
1162
|
+
if (disposed || !details[id]) return;
|
|
1163
|
+
if (currentView && currentView !== id) details[currentView].view.hidden = true;
|
|
1164
|
+
listView.hidden = true;
|
|
1165
|
+
details[id].view.hidden = false;
|
|
1166
|
+
currentView = id;
|
|
1167
|
+
details[id].back.focus();
|
|
1168
|
+
}
|
|
1169
|
+
function showList() {
|
|
1170
|
+
if (disposed || !currentView) return;
|
|
1171
|
+
var id = currentView;
|
|
1172
|
+
details[id].view.hidden = true;
|
|
1173
|
+
listView.hidden = false;
|
|
1174
|
+
currentView = null;
|
|
1175
|
+
rows[id].focus();
|
|
1176
|
+
}
|
|
1177
|
+
// Esc 只在焦点位于本页内时生效;管理页里处理掉,免得宿主把整个设置窗口关掉。
|
|
1178
|
+
function onKeydown(e) {
|
|
1179
|
+
if (e.key !== "Escape" && e.key !== "Esc") return;
|
|
1180
|
+
if (!currentView) return;
|
|
1181
|
+
e.preventDefault();
|
|
1182
|
+
e.stopPropagation();
|
|
1183
|
+
showList();
|
|
1184
|
+
}
|
|
1185
|
+
page.addEventListener("keydown", onKeydown);
|
|
1186
|
+
|
|
1187
|
+
function setBadge(id, b) {
|
|
1188
|
+
(badges[id] || []).forEach(function (node) {
|
|
1189
|
+
node.className = "mm-badge " + (b.cls || "off");
|
|
1190
|
+
node.textContent = b.text;
|
|
1191
|
+
node.title = b.title || "";
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
// 列表按运行态排序:已连接 > 已启动 > 安装中/可用 > 未启动类 > 未安装 > 出错
|
|
1195
|
+
var FEATURE_ORDER = {};
|
|
1196
|
+
FEATURES.forEach(function (f, i) { FEATURE_ORDER[f.id] = i; });
|
|
1197
|
+
function statusRank(cls) {
|
|
1198
|
+
switch (cls) {
|
|
1199
|
+
case "connected": return 0;
|
|
1200
|
+
case "on": return 1;
|
|
1201
|
+
case "busy": return 2;
|
|
1202
|
+
case "ready": return 3;
|
|
1203
|
+
case "checking": return 4;
|
|
1204
|
+
case "off": return 5;
|
|
1205
|
+
case "missing": return 6;
|
|
1206
|
+
case "error": return 7;
|
|
1207
|
+
default: return 5;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function badgeClsOfItem(item) {
|
|
1211
|
+
var b = item && item.querySelector && item.querySelector(".mm-badge");
|
|
1212
|
+
if (!b || !b.className) return "off";
|
|
1213
|
+
var m = String(b.className).match(/\bmm-badge\s+(\S+)/);
|
|
1214
|
+
return (m && m[1]) || "off";
|
|
1215
|
+
}
|
|
1216
|
+
function sortFeatureLists() {
|
|
1217
|
+
if (!listView) return;
|
|
1218
|
+
var lists = listView.querySelectorAll(".mm-list");
|
|
1219
|
+
for (var li = 0; li < lists.length; li++) {
|
|
1220
|
+
var list = lists[li];
|
|
1221
|
+
var items = Array.prototype.filter.call(list.children, function (n) { return n.classList && n.classList.contains("mm-item"); });
|
|
1222
|
+
if (items.length < 2) continue;
|
|
1223
|
+
items.sort(function (a, b) {
|
|
1224
|
+
var ra = statusRank(badgeClsOfItem(a));
|
|
1225
|
+
var rb = statusRank(badgeClsOfItem(b));
|
|
1226
|
+
if (ra !== rb) return ra - rb;
|
|
1227
|
+
var ia = FEATURE_ORDER[a.getAttribute("data-feature")] ;
|
|
1228
|
+
var ib = FEATURE_ORDER[b.getAttribute("data-feature")] ;
|
|
1229
|
+
if (ia == null) ia = 9999;
|
|
1230
|
+
if (ib == null) ib = 9999;
|
|
1231
|
+
return ia - ib;
|
|
1232
|
+
});
|
|
1233
|
+
for (var i = 0; i < items.length; i++) list.appendChild(items[i]);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function updateSummaries() {
|
|
1237
|
+
if (!lastValue) return;
|
|
1238
|
+
FEATURES.forEach(function (f) { if (f.summary) setBadge(f.id, f.summary(lastValue)); });
|
|
1239
|
+
sortFeatureLists();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function fill(value, keys) {
|
|
1243
|
+
var list = keys || INPUT_FIELDS.map(function (f) { return f.key; });
|
|
1244
|
+
// Full fill also refreshes dynamic local *Enabled toggles (not in INPUT_FIELDS).
|
|
1245
|
+
if (!keys) {
|
|
1246
|
+
Object.keys(toggles).forEach(function (k) {
|
|
1247
|
+
if (list.indexOf(k) < 0) list = list.concat([k]);
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
list.forEach(function (k) {
|
|
1251
|
+
var f = FIELD_BY_KEY[k];
|
|
1252
|
+
if (!f) return;
|
|
1253
|
+
var has = value && Object.prototype.hasOwnProperty.call(value, k);
|
|
1254
|
+
if (f.type === "switch") {
|
|
1255
|
+
// Fresh install: all capability *Enabled switches default OFF.
|
|
1256
|
+
// sessionCircuitEnabled stays ON as a sub-option (only active when sessionControlsEnabled).
|
|
1257
|
+
var defOn = k === "sessionCircuitEnabled";
|
|
1258
|
+
setSwitch(k, has ? !!value[k] : defOn);
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
var v = value && value[k];
|
|
1262
|
+
inputs[k].value = v === undefined || v === null ? "" : String(v);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
var STATUS_TEXT = { connected: "已连接", on: "已启动", off: "未启动", error: "出错", missing: "未安装", ready: "可用" };
|
|
1267
|
+
var CONN_TEXT = { connected: "已连接", "no-app": "程序未运行", unreachable: "未连接", "mcp-down": "MCP 未就绪", checking: "正在检查连接" };
|
|
1268
|
+
var WATCH_MS = 5000; // 有组件在探测连接时的状态刷新间隔(安装期间仍是 1.5 秒)
|
|
1269
|
+
var disposed = false;
|
|
1270
|
+
var pollTimer = null;
|
|
1271
|
+
var pollMs = 0; // 当前轮询间隔:1500(安装中)/ WATCH_MS(探测连接)
|
|
1272
|
+
var settleTimer = null; // 安装结束后延迟 2 秒补刷一次状态的一次性定时器
|
|
1273
|
+
var wasActive = false;
|
|
1274
|
+
var statusInFlight = false; // /components 请求进行中,定时器不再叠加新请求
|
|
1275
|
+
var refreshAgain = false; // 请求进行中时有手动刷新,回来后再补刷一次
|
|
1276
|
+
var failCount = 0; // 连续读取状态失败次数
|
|
1277
|
+
var pollPaused = false; // 因连续失败暂停了自动刷新
|
|
1278
|
+
var MAX_FAILS = 3;
|
|
1279
|
+
var controllers = new Set(); // 本页进行中的请求,卸载时统一中止
|
|
1280
|
+
|
|
1281
|
+
function abortError() {
|
|
1282
|
+
var e = new Error("已中止");
|
|
1283
|
+
e.name = "AbortError";
|
|
1284
|
+
return e;
|
|
1285
|
+
}
|
|
1286
|
+
function isAbort(e) {
|
|
1287
|
+
return !!e && e.name === "AbortError";
|
|
1288
|
+
}
|
|
1289
|
+
// 本页发出的请求都走这里:登记 AbortController,卸载时统一中止;卸载后不再发请求。
|
|
1290
|
+
function pageFetch(path, init) {
|
|
1291
|
+
if (disposed) return Promise.reject(abortError());
|
|
1292
|
+
var ctrl = typeof AbortController === "function" ? new AbortController() : null;
|
|
1293
|
+
var opts = Object.assign({}, init || {});
|
|
1294
|
+
if (ctrl) {
|
|
1295
|
+
controllers.add(ctrl);
|
|
1296
|
+
opts.signal = ctrl.signal;
|
|
1297
|
+
}
|
|
1298
|
+
function done() { if (ctrl) controllers.delete(ctrl); }
|
|
1299
|
+
return fetchJson(path, opts).then(function (d) { done(); return d; }, function (e) { done(); throw e; });
|
|
1300
|
+
}
|
|
1301
|
+
function stopPolling() {
|
|
1302
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1303
|
+
pollTimer = null;
|
|
1304
|
+
pollMs = 0;
|
|
1305
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
1306
|
+
settleTimer = null;
|
|
1307
|
+
wasActive = false;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function buildInstallUi(id) {
|
|
1311
|
+
var row = el("div", "mm-install");
|
|
1312
|
+
var install = el("button", "mm-btn", "下载安装");
|
|
1313
|
+
var remove = el("button", "mm-btn mm-ghost", "卸载");
|
|
1314
|
+
var state = el("span", "mm-istate");
|
|
1315
|
+
install.type = "button";
|
|
1316
|
+
remove.type = "button";
|
|
1317
|
+
remove.style.display = "none";
|
|
1318
|
+
row.append(install, remove, state);
|
|
1319
|
+
var details = el("details", "mm-log");
|
|
1320
|
+
var summary = el("summary", undefined, "安装日志");
|
|
1321
|
+
var pre = el("pre");
|
|
1322
|
+
details.append(summary, pre);
|
|
1323
|
+
details.style.display = "none";
|
|
1324
|
+
var cnote = el("div", "mm-cnote");
|
|
1325
|
+
cnote.hidden = true;
|
|
1326
|
+
install.addEventListener("click", function () {
|
|
1327
|
+
install.disabled = true;
|
|
1328
|
+
state.className = "mm-istate queued";
|
|
1329
|
+
state.textContent = "正在提交…";
|
|
1330
|
+
pageFetch("/components/" + id + "/install", { method: "POST", body: "{}" }).then(function (d) {
|
|
1331
|
+
if (disposed) return;
|
|
1332
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "无法开始安装");
|
|
1333
|
+
details.style.display = "";
|
|
1334
|
+
details.open = true;
|
|
1335
|
+
}).catch(function (e) {
|
|
1336
|
+
if (disposed || isAbort(e)) return;
|
|
1337
|
+
state.className = "mm-istate error";
|
|
1338
|
+
state.textContent = e.message;
|
|
1339
|
+
}).finally(function () {
|
|
1340
|
+
refreshStatus();
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
remove.addEventListener("click", function () {
|
|
1344
|
+
var keep = id === "uv" || id === "node" ? "" : "(共用的 Python、Node.js 和缓存会保留)";
|
|
1345
|
+
if (!window.confirm("删除插件 tools 目录里的「" + id + "」安装?" + keep)) return;
|
|
1346
|
+
remove.disabled = true;
|
|
1347
|
+
install.disabled = true;
|
|
1348
|
+
state.className = "mm-istate queued";
|
|
1349
|
+
state.textContent = "正在卸载…";
|
|
1350
|
+
pageFetch("/components/" + id + "/uninstall", { method: "POST", body: "{}" }).then(function (d) {
|
|
1351
|
+
if (disposed) return;
|
|
1352
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "卸载失败");
|
|
1353
|
+
}).catch(function (e) {
|
|
1354
|
+
if (disposed || isAbort(e)) return;
|
|
1355
|
+
state.className = "mm-istate error";
|
|
1356
|
+
state.textContent = e.message;
|
|
1357
|
+
}).finally(function () {
|
|
1358
|
+
if (disposed) return;
|
|
1359
|
+
remove.disabled = false;
|
|
1360
|
+
refreshStatus();
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
return { row: row, install: install, remove: remove, state: state, details: details, pre: pre, cnote: cnote };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function elapsed(job) {
|
|
1367
|
+
if (!job.startedAt) return "";
|
|
1368
|
+
var s = Math.max(0, Math.round(((job.finishedAt || Date.now()) - job.startedAt) / 1000));
|
|
1369
|
+
return s >= 60 ? Math.floor(s / 60) + " 分 " + (s % 60) + " 秒" : s + " 秒";
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// 列表行 / 管理页标题旁的状态徽标:安装进行中优先,其次是运行状态。
|
|
1373
|
+
function componentBadge(c) {
|
|
1374
|
+
var job = c.install || {};
|
|
1375
|
+
if (job.state === "installing") return { text: "安装中" + (job.startedAt ? " " + elapsed(job) : ""), cls: "busy", title: job.step || "" };
|
|
1376
|
+
if (job.state === "queued") return { text: "排队中", cls: "busy", title: "等前一个安装完成" };
|
|
1377
|
+
var cn = c.connection;
|
|
1378
|
+
var title = cn && cn.detail ? (CONN_TEXT[cn.state] || cn.state) + ":" + cn.detail : c.detail || "";
|
|
1379
|
+
return { text: STATUS_TEXT[c.status] || c.status, cls: c.status, title: title };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// 管理页的连接行:只在组件已启动(带 connection)时显示。
|
|
1383
|
+
function renderConn(c) {
|
|
1384
|
+
var line = connLines[c.id];
|
|
1385
|
+
if (!line) return;
|
|
1386
|
+
var cn = c.connection;
|
|
1387
|
+
if (!cn) {
|
|
1388
|
+
line.hidden = true;
|
|
1389
|
+
line.textContent = "";
|
|
1390
|
+
line.className = "mm-conn";
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
line.hidden = false;
|
|
1394
|
+
line.className = "mm-conn " + cn.state;
|
|
1395
|
+
var label = CONN_TEXT[cn.state] || cn.state;
|
|
1396
|
+
line.textContent = cn.state === "checking" || !cn.detail || cn.detail === label ? label + "…" : label + ":" + cn.detail;
|
|
1397
|
+
line.title = cn.checkedAt ? "检查于 " + new Date(cn.checkedAt).toLocaleTimeString() + (cn.via ? "(" + cn.via + ")" : "") : "";
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function renderInstall(c) {
|
|
1401
|
+
var ui = installUis[c.id];
|
|
1402
|
+
if (!ui) return;
|
|
1403
|
+
var job = c.install || { state: "idle", log: [] };
|
|
1404
|
+
var busy = job.state === "installing" || job.state === "queued";
|
|
1405
|
+
ui.install.textContent = c.installed ? "重新安装" : "下载安装";
|
|
1406
|
+
ui.install.title = c.installed ? "重新下载最新版本并覆盖插件 tools 目录里的安装" : "下载并安装到插件的 tools 目录";
|
|
1407
|
+
ui.install.disabled = busy;
|
|
1408
|
+
ui.remove.style.display = c.installed && !busy ? "" : "none";
|
|
1409
|
+
var text;
|
|
1410
|
+
if (job.state === "installing") text = "安装中(" + elapsed(job) + "):" + (job.step || "");
|
|
1411
|
+
else if (job.state === "queued") text = "排队中,等前一个安装完成";
|
|
1412
|
+
else if (job.state === "error") text = "安装失败:" + (job.error || "");
|
|
1413
|
+
else if (job.state === "done") text = "安装完成,用时 " + elapsed(job);
|
|
1414
|
+
else text = c.installed ? "已装进插件 tools 目录" : (job.step || "未装进插件 tools 目录");
|
|
1415
|
+
ui.state.className = "mm-istate " + job.state;
|
|
1416
|
+
ui.state.textContent = text;
|
|
1417
|
+
// 组件说明(如 Godot:装的是哪个版本、项目里的插件要装哪个版本)
|
|
1418
|
+
ui.cnote.textContent = c.note || "";
|
|
1419
|
+
ui.cnote.hidden = !c.note;
|
|
1420
|
+
var a = actions[c.id];
|
|
1421
|
+
if (a) {
|
|
1422
|
+
a.ready = !!c.installed && !busy;
|
|
1423
|
+
if (!a.busy) a.btn.disabled = !a.ready;
|
|
1424
|
+
a.btn.title = a.ready ? "把与服务器同版本的插件装进上面填的项目" : busy ? "正在安装,完成后再试" : "先「下载安装」";
|
|
1425
|
+
}
|
|
1426
|
+
var log = (job.log || []).join("\n");
|
|
1427
|
+
if (log) {
|
|
1428
|
+
ui.details.style.display = "";
|
|
1429
|
+
if (ui.pre.textContent !== log) {
|
|
1430
|
+
var stick = ui.pre.scrollTop + ui.pre.clientHeight >= ui.pre.scrollHeight - 8;
|
|
1431
|
+
ui.pre.textContent = log;
|
|
1432
|
+
if (stick) ui.pre.scrollTop = ui.pre.scrollHeight;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// active:有安装在进行(1.5 秒一刷);watch:有已启动的组件在探测连接(WATCH_MS 一刷,看「已连接」的变化)。
|
|
1438
|
+
// 只用一个定时器,间隔变了就换一个。
|
|
1439
|
+
function setPolling(active, watch) {
|
|
1440
|
+
if (disposed) return;
|
|
1441
|
+
var ms = active ? 1500 : watch ? WATCH_MS : 0;
|
|
1442
|
+
if (pollTimer && pollMs !== ms) { clearInterval(pollTimer); pollTimer = null; pollMs = 0; }
|
|
1443
|
+
if (ms && !pollTimer) { pollTimer = setInterval(function () { refreshStatus(true); }, ms); pollMs = ms; }
|
|
1444
|
+
// 安装刚结束时组件还在重新挂载,稍后再刷一次状态。
|
|
1445
|
+
if (wasActive && !active) {
|
|
1446
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
1447
|
+
settleTimer = setTimeout(function () {
|
|
1448
|
+
settleTimer = null;
|
|
1449
|
+
refreshStatus();
|
|
1450
|
+
}, 2000);
|
|
1451
|
+
}
|
|
1452
|
+
wasActive = active;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// 唯一的状态刷新管线:同时更新列表徽标和各管理页。
|
|
1456
|
+
// fromTimer:由轮询定时器触发;手动操作(保存、安装、卸载)不传。
|
|
1457
|
+
function refreshStatus(fromTimer) {
|
|
1458
|
+
if (disposed) return;
|
|
1459
|
+
if (statusInFlight) {
|
|
1460
|
+
// 上一次请求还没回来:定时器直接跳过,手动刷新等它回来后补刷一次。
|
|
1461
|
+
if (fromTimer !== true) refreshAgain = true;
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
statusInFlight = true;
|
|
1465
|
+
function settle() {
|
|
1466
|
+
statusInFlight = false;
|
|
1467
|
+
if (refreshAgain && !disposed) {
|
|
1468
|
+
refreshAgain = false;
|
|
1469
|
+
refreshStatus();
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
pageFetch("/components").then(function (d) {
|
|
1473
|
+
if (disposed) return;
|
|
1474
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "读取失败");
|
|
1475
|
+
failCount = 0;
|
|
1476
|
+
pollPaused = false;
|
|
1477
|
+
var active = false;
|
|
1478
|
+
var watch = false;
|
|
1479
|
+
syncLocalFromComponents(d.components, d);
|
|
1480
|
+
d.components.forEach(function (c) {
|
|
1481
|
+
if (c.install && (c.install.state === "installing" || c.install.state === "queued")) active = true;
|
|
1482
|
+
if (c.connection) watch = true;
|
|
1483
|
+
renderInstall(c);
|
|
1484
|
+
renderConn(c);
|
|
1485
|
+
if (byComponent[c.id]) setBadge(byComponent[c.id].id, componentBadge(c));
|
|
1486
|
+
var line = statusLines[c.id];
|
|
1487
|
+
if (!line) return;
|
|
1488
|
+
line.className = "mm-status " + c.status;
|
|
1489
|
+
line.textContent = (STATUS_TEXT[c.status] || c.status) + ":" + c.detail;
|
|
1490
|
+
line.title = c.command || "";
|
|
1491
|
+
});
|
|
1492
|
+
sortFeatureLists();
|
|
1493
|
+
setPolling(active, watch);
|
|
1494
|
+
settle();
|
|
1495
|
+
}).catch(function (e) {
|
|
1496
|
+
if (disposed) return;
|
|
1497
|
+
if (isAbort(e)) { settle(); return; }
|
|
1498
|
+
failCount++;
|
|
1499
|
+
// 轮询中连续失败 MAX_FAILS 次就暂停自动刷新;手动操作成功后会重新开始。
|
|
1500
|
+
if (failCount >= MAX_FAILS && pollTimer) {
|
|
1501
|
+
stopPolling();
|
|
1502
|
+
pollPaused = true;
|
|
1503
|
+
}
|
|
1504
|
+
var text = "无法读取组件状态:" + e.message + (pollPaused ? "(已暂停自动刷新,稍后再试或点保存刷新)" : "");
|
|
1505
|
+
Object.keys(statusLines).forEach(function (id) {
|
|
1506
|
+
statusLines[id].className = "mm-status error";
|
|
1507
|
+
statusLines[id].textContent = text;
|
|
1508
|
+
if (byComponent[id]) setBadge(byComponent[id].id, { text: "读取失败", cls: "error", title: text });
|
|
1509
|
+
});
|
|
1510
|
+
settle();
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
pageFetch("/settings").then(function (d) {
|
|
1515
|
+
if (disposed) return;
|
|
1516
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "读取失败");
|
|
1517
|
+
lastValue = d.value || {};
|
|
1518
|
+
fill(lastValue);
|
|
1519
|
+
if (typeof lastValue.sessionControlsEnabled === "boolean") {
|
|
1520
|
+
setScEnabled(lastValue.sessionControlsEnabled === true);
|
|
1521
|
+
}
|
|
1522
|
+
updateSummaries();
|
|
1523
|
+
FEATURES.forEach(function (f) { if (details[f.id].save) details[f.id].save.disabled = false; });
|
|
1524
|
+
Object.keys(toggles).forEach(function (k) { toggles[k].btn.disabled = false; });
|
|
1525
|
+
refreshStatus();
|
|
1526
|
+
sub.textContent = d.persisted
|
|
1527
|
+
? "设置已持久化(DSH 设置服务或插件目录 .dsh-workbench-settings.json),修改后立即生效。"
|
|
1528
|
+
: "当前无法持久化设置,修改只在本次运行内有效。";
|
|
1529
|
+
}).catch(function (e) {
|
|
1530
|
+
if (disposed || isAbort(e)) return;
|
|
1531
|
+
sub.textContent = "无法连接插件宿主接口:" + e.message;
|
|
1532
|
+
sub.className = "mm-msg err";
|
|
1533
|
+
FEATURES.forEach(function (f) {
|
|
1534
|
+
if (f.summary) setBadge(f.id, { text: "读取失败", cls: "error", title: e.message });
|
|
1535
|
+
if (details[f.id].save && details[f.id].msg) {
|
|
1536
|
+
details[f.id].msg.textContent = "设置读取失败,暂时不能保存";
|
|
1537
|
+
details[f.id].msg.className = "mm-msg err";
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
// 唯一的保存管线:请求排队依次发出,每次都以「轮到它时」最近读到的完整设置为底、只换上调用方的字段,
|
|
1543
|
+
// 所以旋钮和「保存」前后脚点也不会互相覆盖。成功后更新 lastValue、列表徽标并刷新一次状态。
|
|
1544
|
+
var saveQueue = Promise.resolve();
|
|
1545
|
+
function postSettings(change) {
|
|
1546
|
+
var run = saveQueue.then(function () {
|
|
1547
|
+
if (disposed) throw abortError();
|
|
1548
|
+
return pageFetch("/settings", { method: "POST", body: JSON.stringify(change(Object.assign({}, lastValue || {}))) });
|
|
1549
|
+
}).then(function (d) {
|
|
1550
|
+
if (disposed) throw abortError();
|
|
1551
|
+
if (!d || !d.ok) throw new Error((d && d.error) || "保存失败");
|
|
1552
|
+
lastValue = d.value || lastValue;
|
|
1553
|
+
// Immediately show/hide retract·retry·pause without waiting for the 15s poll.
|
|
1554
|
+
if (lastValue && typeof lastValue.sessionControlsEnabled === "boolean") {
|
|
1555
|
+
setScEnabled(lastValue.sessionControlsEnabled === true);
|
|
1556
|
+
}
|
|
1557
|
+
updateSummaries();
|
|
1558
|
+
refreshStatus();
|
|
1559
|
+
return lastValue;
|
|
1560
|
+
});
|
|
1561
|
+
saveQueue = run.catch(function () {});
|
|
1562
|
+
return run;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// 某个管理页的「保存」:以最近读到的完整设置为底,只换上本页的字段,
|
|
1566
|
+
// 其他功能的设置原样带回(不会被清空,也不会带上别的页里没保存的改动)。
|
|
1567
|
+
// 「启用」不在这里:它由旋钮单独保存,这里沿用已保存的值(即旋钮当前状态)。
|
|
1568
|
+
function saveFeature(f) {
|
|
1569
|
+
var ui = details[f.id];
|
|
1570
|
+
function say(text, isErr) {
|
|
1571
|
+
ui.msg.textContent = text;
|
|
1572
|
+
ui.msg.className = isErr ? "mm-msg err" : "mm-msg";
|
|
1573
|
+
}
|
|
1574
|
+
var values = {};
|
|
1575
|
+
f.keys.forEach(function (k) {
|
|
1576
|
+
var fd = FIELD_BY_KEY[k];
|
|
1577
|
+
if (fd.type === "switch") return;
|
|
1578
|
+
var raw = inputs[k].value;
|
|
1579
|
+
values[k] = fd.type === "number" ? Number(raw) : fd.bool ? raw === "true" : raw;
|
|
1580
|
+
});
|
|
1581
|
+
ui.save.disabled = true;
|
|
1582
|
+
say("保存中…");
|
|
1583
|
+
postSettings(function (patch) { return Object.assign(patch, values); }).then(function () {
|
|
1584
|
+
fill(lastValue, f.keys);
|
|
1585
|
+
say("已保存");
|
|
1586
|
+
}).catch(function (e) {
|
|
1587
|
+
if (disposed || isAbort(e)) return;
|
|
1588
|
+
say(e.message, true);
|
|
1589
|
+
}).finally(function () {
|
|
1590
|
+
if (disposed) return;
|
|
1591
|
+
ui.save.disabled = false;
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// 拨动「启用」旋钮:先显示新状态并禁用旋钮,只把这一项改掉后保存;失败时回到已保存的状态并提示。
|
|
1596
|
+
function toggleSetting(t) {
|
|
1597
|
+
if (disposed || t.saving || t.btn.disabled || !lastValue) return;
|
|
1598
|
+
var next = t.btn.getAttribute("aria-checked") !== "true";
|
|
1599
|
+
if (next && t.key === "sessionControlsEnabled") {
|
|
1600
|
+
var ok = window.confirm(
|
|
1601
|
+
"警告:会话控制为实验性功能,尚未开发完毕。\n\n使用此功能可能会对对话造成不可逆的破坏。\n\n仍要启用吗?"
|
|
1602
|
+
);
|
|
1603
|
+
if (!ok) return;
|
|
1604
|
+
}
|
|
1605
|
+
function say(text, isErr) {
|
|
1606
|
+
t.msg.textContent = text;
|
|
1607
|
+
t.msg.className = isErr ? "mm-tmsg err" : "mm-tmsg";
|
|
1608
|
+
}
|
|
1609
|
+
t.btn.setAttribute("aria-checked", next ? "true" : "false");
|
|
1610
|
+
t.btn.setAttribute("aria-busy", "true");
|
|
1611
|
+
t.btn.disabled = true;
|
|
1612
|
+
t.saving = true;
|
|
1613
|
+
say("保存中…");
|
|
1614
|
+
postSettings(function (patch) { patch[t.key] = next; return patch; }).then(function (v) {
|
|
1615
|
+
t.saving = false;
|
|
1616
|
+
setSwitch(t.key, !!v[t.key]);
|
|
1617
|
+
say(v[t.key] ? "已启用" : "已停用");
|
|
1618
|
+
}).catch(function (e) {
|
|
1619
|
+
if (disposed || isAbort(e)) return;
|
|
1620
|
+
t.saving = false;
|
|
1621
|
+
setSwitch(t.key, !!(lastValue || {})[t.key]);
|
|
1622
|
+
say("未保存:" + e.message, true);
|
|
1623
|
+
}).finally(function () {
|
|
1624
|
+
if (disposed) return;
|
|
1625
|
+
t.saving = false;
|
|
1626
|
+
t.btn.disabled = false;
|
|
1627
|
+
t.btn.removeAttribute("aria-busy");
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
page.__mmDispose = function () {
|
|
1632
|
+
disposed = true;
|
|
1633
|
+
stopPolling();
|
|
1634
|
+
page.removeEventListener("keydown", onKeydown);
|
|
1635
|
+
// 中止所有进行中的请求;回调里见到 disposed / AbortError 会直接返回,不再写 DOM。
|
|
1636
|
+
controllers.forEach(function (c) { c.abort(); });
|
|
1637
|
+
controllers.clear();
|
|
1638
|
+
};
|
|
1639
|
+
return page;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// settings.section 的第二个参数是 React 组件;把手写 DOM 页面挂进去。
|
|
1643
|
+
function SettingsPage() {
|
|
1644
|
+
var ref = React.useRef(null);
|
|
1645
|
+
React.useEffect(function () {
|
|
1646
|
+
var node = renderSettingsPage();
|
|
1647
|
+
if (ref.current) ref.current.appendChild(node);
|
|
1648
|
+
return function () {
|
|
1649
|
+
if (node.__mmDispose) node.__mmDispose();
|
|
1650
|
+
node.remove();
|
|
1651
|
+
};
|
|
1652
|
+
}, []);
|
|
1653
|
+
return h("div", { ref: ref });
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* MmBlock UI shape (toolview; from presentationMeta.mm — not model content):
|
|
1658
|
+
* { type:'mm', kind:'image'|'video'|'webpage'|'document',
|
|
1659
|
+
* status:'pending'|'ready'|'error'|'canceled',
|
|
1660
|
+
* progress?, title?, caption?, error?,
|
|
1661
|
+
* asset?: { attachment }, source? }
|
|
1662
|
+
* Prefer meta.mm; tolerate content type:'mm' from older sessions; synthesize from type:'image'.
|
|
1663
|
+
*/
|
|
1664
|
+
|
|
1665
|
+
// ── multimodal (MmBlock) tool cards ──
|
|
1666
|
+
function blockText(block) {
|
|
1667
|
+
var content = block && Array.isArray(block.content) ? block.content : [];
|
|
1668
|
+
return content.filter(function (b) { return b && b.type === "text"; })
|
|
1669
|
+
.map(function (b) { return b.text; }).join("\n");
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function mmBlocksFromMeta(meta) {
|
|
1673
|
+
if (!meta || typeof meta !== "object") return [];
|
|
1674
|
+
if (meta.mm && meta.mm.type === "mm") return [meta.mm];
|
|
1675
|
+
if (Array.isArray(meta.mms)) {
|
|
1676
|
+
return meta.mms.filter(function (b) { return b && b.type === "mm"; });
|
|
1677
|
+
}
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function mmBlocksFromContent(content) {
|
|
1682
|
+
var list = Array.isArray(content) ? content : [];
|
|
1683
|
+
// Older sessions may still have type:'mm' in content (broke DeepSeek Messages); show in UI only.
|
|
1684
|
+
var mms = list.filter(function (b) { return b && b.type === "mm"; });
|
|
1685
|
+
if (mms.length) return mms;
|
|
1686
|
+
// Official attachment path: type:'image' → synthetic MmBlock (kind image / ready)
|
|
1687
|
+
return list.filter(function (b) { return b && b.type === "image" && b.attachment; })
|
|
1688
|
+
.map(function (b) {
|
|
1689
|
+
return {
|
|
1690
|
+
type: "mm",
|
|
1691
|
+
kind: "image",
|
|
1692
|
+
status: "ready",
|
|
1693
|
+
title: (b.attachment && b.attachment.name) || undefined,
|
|
1694
|
+
asset: { attachment: b.attachment }
|
|
1695
|
+
};
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/** Enrich mm title/caption from call args / meta when the image-only path omitted them. */
|
|
1700
|
+
function enrichMmBlocks(mms, block, args) {
|
|
1701
|
+
var meta = (block && block.meta) || {};
|
|
1702
|
+
var a = args || {};
|
|
1703
|
+
return (mms || []).map(function (mm) {
|
|
1704
|
+
var title = mm.title || meta.title || a.title || a.path || undefined;
|
|
1705
|
+
var caption = mm.caption || meta.caption || a.caption || undefined;
|
|
1706
|
+
if (title === mm.title && caption === mm.caption) return mm;
|
|
1707
|
+
var next = {};
|
|
1708
|
+
for (var k in mm) if (Object.prototype.hasOwnProperty.call(mm, k)) next[k] = mm[k];
|
|
1709
|
+
if (title !== undefined) next.title = title;
|
|
1710
|
+
if (caption !== undefined) next.caption = caption;
|
|
1711
|
+
return next;
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function mmKindIcon(kind) {
|
|
1716
|
+
if (kind === "video") return "🎬";
|
|
1717
|
+
if (kind === "webpage") return "🌐";
|
|
1718
|
+
if (kind === "document") return "📄";
|
|
1719
|
+
return "🖼";
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function mmKindLabel(kind) {
|
|
1723
|
+
if (kind === "video") return "视频";
|
|
1724
|
+
if (kind === "webpage") return "网页";
|
|
1725
|
+
if (kind === "document") return "文档";
|
|
1726
|
+
return "图片";
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function mmStatusLabel(status) {
|
|
1730
|
+
if (status === "pending") return "生成中";
|
|
1731
|
+
if (status === "error") return "失败";
|
|
1732
|
+
if (status === "canceled") return "已取消";
|
|
1733
|
+
return "";
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
var MM_SHELL_STYLE = {
|
|
1737
|
+
border: "1px solid var(--theme-border,#333)",
|
|
1738
|
+
borderRadius: 10,
|
|
1739
|
+
padding: "10px 12px",
|
|
1740
|
+
margin: "4px 0",
|
|
1741
|
+
fontSize: 12,
|
|
1742
|
+
background: "var(--theme-bg-secondary,transparent)"
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
var MM_SKELETON_STYLE = {
|
|
1746
|
+
width: 160,
|
|
1747
|
+
height: 120,
|
|
1748
|
+
borderRadius: 8,
|
|
1749
|
+
background: "linear-gradient(90deg, rgba(127,127,127,.12) 25%, rgba(127,127,127,.22) 50%, rgba(127,127,127,.12) 75%)",
|
|
1750
|
+
backgroundSize: "200% 100%"
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
function MmAttachmentImage(props) {
|
|
1754
|
+
var st = React.useState(null);
|
|
1755
|
+
var url = st[0];
|
|
1756
|
+
var setUrl = st[1];
|
|
1757
|
+
var es = React.useState(false);
|
|
1758
|
+
var failed = es[0];
|
|
1759
|
+
var setFailed = es[1];
|
|
1760
|
+
React.useEffect(function () {
|
|
1761
|
+
var alive = true;
|
|
1762
|
+
if (!props.loadImage || !props.attachment) return undefined;
|
|
1763
|
+
props.loadImage(props.attachment).then(function (u) { if (alive) setUrl(u); }, function () { if (alive) setFailed(true); });
|
|
1764
|
+
return function () { alive = false; };
|
|
1765
|
+
}, [props.attachment, props.loadImage]);
|
|
1766
|
+
if (failed) return h("div", { style: { fontSize: 12, color: "#e55" } }, "图片加载失败");
|
|
1767
|
+
if (!url) return h("div", { style: MM_SKELETON_STYLE, title: "加载中…" });
|
|
1768
|
+
return h("img", {
|
|
1769
|
+
src: url,
|
|
1770
|
+
alt: props.alt || (props.attachment && props.attachment.name) || "image",
|
|
1771
|
+
style: { maxWidth: 320, maxHeight: 320, borderRadius: 8, display: "block" }
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function MmProgressBar(props) {
|
|
1776
|
+
var p = props.progress;
|
|
1777
|
+
if (p == null || !(typeof p === "number") || isNaN(p)) return null;
|
|
1778
|
+
var pct = p <= 1 ? Math.round(p * 100) : Math.round(Math.max(0, Math.min(100, p)));
|
|
1779
|
+
return h("div", { style: { marginTop: 8 } },
|
|
1780
|
+
h("div", {
|
|
1781
|
+
style: {
|
|
1782
|
+
height: 4, borderRadius: 2, overflow: "hidden",
|
|
1783
|
+
background: "rgba(127,127,127,.2)"
|
|
1784
|
+
}
|
|
1785
|
+
}, h("div", {
|
|
1786
|
+
style: {
|
|
1787
|
+
height: "100%", width: pct + "%",
|
|
1788
|
+
background: "var(--theme-accent,#4a9eff)",
|
|
1789
|
+
transition: "width .2s ease"
|
|
1790
|
+
}
|
|
1791
|
+
})),
|
|
1792
|
+
h("div", { style: { marginTop: 4, fontSize: 11, color: "var(--theme-text-secondary,#888)" } }, pct + "%")
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function MmImageBody(props) {
|
|
1797
|
+
var mm = props.mm;
|
|
1798
|
+
var status = mm.status || "ready";
|
|
1799
|
+
if (status === "pending") {
|
|
1800
|
+
return h("div", null,
|
|
1801
|
+
h("div", { style: MM_SKELETON_STYLE }),
|
|
1802
|
+
h(MmProgressBar, { progress: mm.progress }),
|
|
1803
|
+
h("div", { style: { marginTop: 6, color: "var(--theme-text-secondary,#888)" } }, "生成中…")
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
if (status === "error") {
|
|
1807
|
+
return h("div", { style: { color: "#e55" } }, mm.error || "生成失败");
|
|
1808
|
+
}
|
|
1809
|
+
if (status === "canceled") {
|
|
1810
|
+
return h("div", { style: { color: "var(--theme-text-secondary,#888)" } }, "已取消");
|
|
1811
|
+
}
|
|
1812
|
+
var att = mm.asset && mm.asset.attachment;
|
|
1813
|
+
if (!att) {
|
|
1814
|
+
return h("div", { style: { color: "var(--theme-text-secondary,#888)" } }, "无图片资源");
|
|
1815
|
+
}
|
|
1816
|
+
return h(MmAttachmentImage, {
|
|
1817
|
+
attachment: att,
|
|
1818
|
+
loadImage: props.loadImage,
|
|
1819
|
+
alt: mm.title || (att && att.name) || "image"
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/** Placeholder bodies for kinds not yet wired (video / webpage / document). */
|
|
1824
|
+
function MmStubBody(props) {
|
|
1825
|
+
var mm = props.mm;
|
|
1826
|
+
var kind = mm.kind || "document";
|
|
1827
|
+
var status = mm.status || "ready";
|
|
1828
|
+
var label = mmKindLabel(kind);
|
|
1829
|
+
if (status === "pending") {
|
|
1830
|
+
return h("div", null,
|
|
1831
|
+
h("div", { style: Object.assign({}, MM_SKELETON_STYLE, { width: "100%", maxWidth: 280, height: 72 }) }),
|
|
1832
|
+
h(MmProgressBar, { progress: mm.progress }),
|
|
1833
|
+
h("div", { style: { marginTop: 6, color: "var(--theme-text-secondary,#888)" } }, label + "生成中…(占位)")
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
if (status === "error") {
|
|
1837
|
+
return h("div", { style: { color: "#e55" } }, mm.error || (label + "失败"));
|
|
1838
|
+
}
|
|
1839
|
+
if (status === "canceled") {
|
|
1840
|
+
return h("div", { style: { color: "var(--theme-text-secondary,#888)" } }, "已取消");
|
|
1841
|
+
}
|
|
1842
|
+
var src = (mm.source && (mm.source.url || mm.source.href)) || (mm.asset && mm.asset.url) || "";
|
|
1843
|
+
return h("div", {
|
|
1844
|
+
style: {
|
|
1845
|
+
padding: "14px 12px", borderRadius: 8,
|
|
1846
|
+
background: "rgba(127,127,127,.1)",
|
|
1847
|
+
color: "var(--theme-text-secondary,#888)"
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
h("div", { style: { fontWeight: 600, marginBottom: 4, color: "var(--theme-text,#ddd)" } }, label + "卡片(占位)"),
|
|
1851
|
+
src ? h("div", { style: { wordBreak: "break-all", fontSize: 11 } }, String(src)) : null,
|
|
1852
|
+
h("div", { style: { marginTop: 4, fontSize: 11 } }, "后续接入真实 " + kind + " 预览")
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function MmCard(props) {
|
|
1857
|
+
var mm = props.mm || {};
|
|
1858
|
+
var kind = mm.kind || "image";
|
|
1859
|
+
var status = mm.status || "ready";
|
|
1860
|
+
var statusLbl = mmStatusLabel(status);
|
|
1861
|
+
var title = mm.title || "";
|
|
1862
|
+
var body = kind === "image"
|
|
1863
|
+
? h(MmImageBody, { mm: mm, loadImage: props.loadImage })
|
|
1864
|
+
: h(MmStubBody, { mm: mm });
|
|
1865
|
+
return h("div", { style: MM_SHELL_STYLE },
|
|
1866
|
+
h("div", {
|
|
1867
|
+
style: {
|
|
1868
|
+
display: "flex", alignItems: "center", gap: 8, marginBottom: 8,
|
|
1869
|
+
fontWeight: 600
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
h("span", null, mmKindIcon(kind)),
|
|
1873
|
+
h("span", { style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } },
|
|
1874
|
+
title || mmKindLabel(kind)),
|
|
1875
|
+
statusLbl
|
|
1876
|
+
? h("span", {
|
|
1877
|
+
style: {
|
|
1878
|
+
fontSize: 11, fontWeight: 500, padding: "1px 6px", borderRadius: 999,
|
|
1879
|
+
color: status === "error" ? "#e55" : "var(--theme-text-secondary,#888)",
|
|
1880
|
+
background: "rgba(127,127,127,.12)"
|
|
1881
|
+
}
|
|
1882
|
+
}, statusLbl)
|
|
1883
|
+
: null
|
|
1884
|
+
),
|
|
1885
|
+
body,
|
|
1886
|
+
mm.caption
|
|
1887
|
+
? h("div", { style: { marginTop: 8, color: "var(--theme-text-secondary,#888)", fontSize: 11 } }, mm.caption)
|
|
1888
|
+
: null
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Parse tool-call argsRaw / block.args for title/path enrichment.
|
|
1894
|
+
*/
|
|
1895
|
+
function mmArgsFromBlockOrRaw(block, argsRaw) {
|
|
1896
|
+
var args = (block && (block.args || block.input)) || null;
|
|
1897
|
+
if (args && typeof args === "object") return args;
|
|
1898
|
+
if (typeof argsRaw === "string" && argsRaw) {
|
|
1899
|
+
try {
|
|
1900
|
+
var parsed = JSON.parse(argsRaw);
|
|
1901
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
1902
|
+
} catch (_) {}
|
|
1903
|
+
}
|
|
1904
|
+
return {};
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* Multimodal toolview row for mm_send_image.
|
|
1909
|
+
* Running: pending/sending card only. Settled: compact one-liner (full MmCard lives on turnTail).
|
|
1910
|
+
*/
|
|
1911
|
+
function MmToolRow(props) {
|
|
1912
|
+
var block = props.block;
|
|
1913
|
+
var settled = !!(block && block.kind === "tool-result");
|
|
1914
|
+
var call = settled ? (block.call || null) : null;
|
|
1915
|
+
var toolName = (call && call.name)
|
|
1916
|
+
|| (block && (block.toolName || block.name || block.tool))
|
|
1917
|
+
|| "mm_send_image";
|
|
1918
|
+
var args = mmArgsFromBlockOrRaw(block, call && call.argsRaw);
|
|
1919
|
+
var label = args.title || args.path || "";
|
|
1920
|
+
if (label) label = String(label);
|
|
1921
|
+
if (settled) {
|
|
1922
|
+
var err = block.isError === true;
|
|
1923
|
+
var status = err ? "失败" : "已发送";
|
|
1924
|
+
return h("div", {
|
|
1925
|
+
style: {
|
|
1926
|
+
margin: "2px 0",
|
|
1927
|
+
fontSize: 12,
|
|
1928
|
+
color: err ? "#e55" : "var(--theme-text-secondary,#888)"
|
|
1929
|
+
},
|
|
1930
|
+
"data-tool": "mm_send_image",
|
|
1931
|
+
"data-state": err ? "error" : "ok"
|
|
1932
|
+
}, toolName + " · " + status + (label ? " · " + label : ""));
|
|
1933
|
+
}
|
|
1934
|
+
// Preparing / running: pending card only (no settled gallery here).
|
|
1935
|
+
var pendingTitle = label || undefined;
|
|
1936
|
+
return h("div", {
|
|
1937
|
+
style: { margin: "4px 0" },
|
|
1938
|
+
"data-tool": "mm_send_image",
|
|
1939
|
+
"data-state": "running"
|
|
1940
|
+
},
|
|
1941
|
+
h("div", {
|
|
1942
|
+
style: { fontWeight: 600, marginBottom: 4, fontSize: 12, color: "var(--theme-text-secondary,#888)" }
|
|
1943
|
+
}, toolName + (label ? " · " + label : "") + " · 发送中"),
|
|
1944
|
+
h(MmCard, {
|
|
1945
|
+
mm: {
|
|
1946
|
+
type: "mm",
|
|
1947
|
+
kind: "image",
|
|
1948
|
+
status: "pending",
|
|
1949
|
+
title: pendingTitle
|
|
1950
|
+
},
|
|
1951
|
+
loadImage: props.loadImage
|
|
1952
|
+
})
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** Append-origin surface events only (same filter as Host deliverables / schedule). */
|
|
1957
|
+
function mmIsAppendSurfaceEvent(event) {
|
|
1958
|
+
if (!event || typeof event !== "object") return false;
|
|
1959
|
+
if (event.type !== "tool/call" && event.type !== "tool/result") return false;
|
|
1960
|
+
return event.surfaceOp === undefined || event.surfaceOp === "append";
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Collect settled mm_send_image cards for one Turn into turn data `mmCards`.
|
|
1965
|
+
* Prefers presentationMeta.mm, then content type:mm / type:image (legacy).
|
|
1966
|
+
* Publishes no Chat view Node — turnTail reads owner.turn.data.get("mmCards").
|
|
1967
|
+
*/
|
|
1968
|
+
function mmEntryFromResult(match, call) {
|
|
1969
|
+
if (!match || !match.event || match.event.type !== "tool/result") return null;
|
|
1970
|
+
var message = match.event.data && match.event.data.message;
|
|
1971
|
+
if (!message || message.isError === true) return null;
|
|
1972
|
+
var meta = match.event.data.meta;
|
|
1973
|
+
var content = message.content;
|
|
1974
|
+
var mms = mmBlocksFromMeta(meta);
|
|
1975
|
+
if (!mms.length) mms = mmBlocksFromContent(content);
|
|
1976
|
+
var args = mmArgsFromBlockOrRaw(null, call && call.argsRaw);
|
|
1977
|
+
mms = enrichMmBlocks(mms, { meta: meta }, args);
|
|
1978
|
+
if (!mms.length) return null;
|
|
1979
|
+
return {
|
|
1980
|
+
seq: match.event.seq,
|
|
1981
|
+
callId: String(message.source && message.source.callId || ""),
|
|
1982
|
+
mms: mms
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function mmCardsForClosing(data, seq) {
|
|
1987
|
+
var bound = seq == null ? Number.POSITIVE_INFINITY : seq;
|
|
1988
|
+
var list = (data && data.cards) || [];
|
|
1989
|
+
return list.filter(function (c) { return c && typeof c.seq === "number" && c.seq <= bound; });
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function selectMmCards(owner) {
|
|
1993
|
+
if (!owner || !owner.turn || !owner.turn.data) return null;
|
|
1994
|
+
var endSeq = owner.turn.end && typeof owner.turn.end.seq === "number"
|
|
1995
|
+
? owner.turn.end.seq
|
|
1996
|
+
: owner.seq;
|
|
1997
|
+
var cards = mmCardsForClosing(owner.turn.data.get("mmCards"), endSeq);
|
|
1998
|
+
return cards.length === 0 ? null : cards;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
var mmCardsDefinition = {
|
|
2002
|
+
kind: "mmCards",
|
|
2003
|
+
match: function (event) {
|
|
2004
|
+
if (event.type === "turn/start") {
|
|
2005
|
+
return { id: String(event.data.turn), role: "start" };
|
|
2006
|
+
}
|
|
2007
|
+
if (event.type === "tool/call" && mmIsAppendSurfaceEvent(event)) {
|
|
2008
|
+
return { id: String(event.data.turn), role: "update" };
|
|
2009
|
+
}
|
|
2010
|
+
if (event.type === "tool/result" && mmIsAppendSurfaceEvent(event)) {
|
|
2011
|
+
return { id: String(event.data.turn), role: "update" };
|
|
2012
|
+
}
|
|
2013
|
+
return null;
|
|
2014
|
+
},
|
|
2015
|
+
start: function (_context, match) {
|
|
2016
|
+
if (match.event.type !== "turn/start") throw new Error("mmCards start requires turn/start");
|
|
2017
|
+
return {
|
|
2018
|
+
turn: match.event.data.turn,
|
|
2019
|
+
calls: new Map(),
|
|
2020
|
+
cards: []
|
|
2021
|
+
};
|
|
2022
|
+
},
|
|
2023
|
+
update: function (context, match) {
|
|
2024
|
+
if (match.event.type === "tool/call") {
|
|
2025
|
+
if (match.event.data.name !== "mm_send_image") return context.state;
|
|
2026
|
+
var calls = new Map(context.state.calls);
|
|
2027
|
+
calls.set(String(match.event.data.callId), {
|
|
2028
|
+
name: match.event.data.name,
|
|
2029
|
+
argsRaw: match.event.data.arguments,
|
|
2030
|
+
time: match.event.time
|
|
2031
|
+
});
|
|
2032
|
+
return { turn: context.state.turn, calls: calls, cards: context.state.cards };
|
|
2033
|
+
}
|
|
2034
|
+
if (match.event.type !== "tool/result") return context.state;
|
|
2035
|
+
var call = context.state.calls.get(String(match.event.data.message.source.callId));
|
|
2036
|
+
if (!call || call.name !== "mm_send_image") return context.state;
|
|
2037
|
+
var entry = mmEntryFromResult(match, call);
|
|
2038
|
+
if (!entry) return context.state;
|
|
2039
|
+
return {
|
|
2040
|
+
turn: context.state.turn,
|
|
2041
|
+
calls: context.state.calls,
|
|
2042
|
+
cards: context.state.cards.concat([entry])
|
|
2043
|
+
};
|
|
2044
|
+
},
|
|
2045
|
+
buildLocationData: function (context, scope, previous) {
|
|
2046
|
+
if (scope !== "turn" || context.state === undefined) return null;
|
|
2047
|
+
if (
|
|
2048
|
+
previous
|
|
2049
|
+
&& previous.kind === "turn"
|
|
2050
|
+
&& previous.turn === context.state.turn
|
|
2051
|
+
&& previous.key === "mmCards"
|
|
2052
|
+
&& previous.value.cards === context.state.cards
|
|
2053
|
+
) return previous;
|
|
2054
|
+
return {
|
|
2055
|
+
kind: "turn",
|
|
2056
|
+
turn: context.state.turn,
|
|
2057
|
+
key: "mmCards",
|
|
2058
|
+
value: { cards: context.state.cards }
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* Completed-turn artifact: full MmCards after tools/thinking fold.
|
|
2065
|
+
* loadImage is injected from uiConversation.imageUrl(sessionId, attachment).
|
|
2066
|
+
*/
|
|
2067
|
+
function MmTurnTail(props) {
|
|
2068
|
+
var matched = selectMmCards(props);
|
|
2069
|
+
if (!matched) return null;
|
|
2070
|
+
var loadImage = props.loadImage;
|
|
2071
|
+
var nodes = [];
|
|
2072
|
+
matched.forEach(function (entry) {
|
|
2073
|
+
(entry.mms || []).forEach(function (mm, i) {
|
|
2074
|
+
var key = (mm.asset && mm.asset.attachment && mm.asset.attachment.attachmentId)
|
|
2075
|
+
|| (entry.callId + "-" + i);
|
|
2076
|
+
nodes.push(h(MmCard, {
|
|
2077
|
+
key: key,
|
|
2078
|
+
mm: mm,
|
|
2079
|
+
loadImage: loadImage
|
|
2080
|
+
}));
|
|
2081
|
+
});
|
|
2082
|
+
});
|
|
2083
|
+
if (!nodes.length) return null;
|
|
2084
|
+
return h("div", {
|
|
2085
|
+
style: { display: "flex", flexDirection: "column", gap: 8, margin: "4px 0" },
|
|
2086
|
+
"data-mm-turn-tail": true
|
|
2087
|
+
}, nodes);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
var inject = ["slots", "sessions", "uiConversation"];
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* 会话控制(Harness 0.2.0-rc.2):
|
|
2094
|
+
* 官方用户气泡没有 extraActions。撤回用 portal 放进用户消息自己的操作行。
|
|
2095
|
+
* 重试只挂 conversation.chat.assistant-actions(复制与分支之间),确认后就地替换原回复。
|
|
2096
|
+
* 暂停=熔断挂在 conversation.input.right(随时可点,只取消,不截断)。
|
|
2097
|
+
* sessionId 由 session-scope slot 自动注入。
|
|
2098
|
+
*/
|
|
2099
|
+
|
|
2100
|
+
var SC_CSS_TAG = "dsh-workbench/session-controls.css";
|
|
2101
|
+
var SC_CSS = [
|
|
2102
|
+
".dsw-sc-action{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));",
|
|
2103
|
+
"color:var(--dsw-alias-label-tertiary,#888);cursor:pointer;background:transparent;border:none;border-radius:var(--dsw-radius-sm,6px);",
|
|
2104
|
+
"justify-content:center;align-items:center;padding:6px;display:inline-flex;flex:none}",
|
|
2105
|
+
".dsw-sc-action svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}",
|
|
2106
|
+
".dsw-sc-action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));color:var(--dsw-alias-label-secondary,#bbb)}",
|
|
2107
|
+
".dsw-sc-action:disabled{cursor:default;opacity:.4}",
|
|
2108
|
+
".dsw-sc-action[data-busy]{opacity:.55}",
|
|
2109
|
+
".dsw-sc-pause{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));",
|
|
2110
|
+
"color:var(--dsw-alias-label-secondary,#bbb);cursor:pointer;background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));",
|
|
2111
|
+
"border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex;flex:none}",
|
|
2112
|
+
".dsw-sc-pause svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}",
|
|
2113
|
+
".dsw-sc-pause:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.2));color:var(--dsw-alias-label-primary,#ddd)}",
|
|
2114
|
+
".dsw-sc-pause:disabled{cursor:default;opacity:.4}",
|
|
2115
|
+
".dsw-sc-modal{pointer-events:auto;position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;",
|
|
2116
|
+
"padding:max(24px,var(--dsh-frame-overlay-top,24px)) 24px;font-family:inherit}",
|
|
2117
|
+
".dsw-sc-modal-mask{position:absolute;inset:var(--dsh-frame-chrome-top,0px) 0 0;backdrop-filter:var(--dsw-mask-blur)}",
|
|
2118
|
+
".dsw-sc-modal-mask::after{content:'';position:absolute;inset:0;background:var(--dsw-alias-bg-mask-1);",
|
|
2119
|
+
"animation:dsw-sc-modal-enter var(--ds-transition-duration,.16s) var(--ds-ease-in-out,ease)}",
|
|
2120
|
+
".dsw-sc-modal-dialog{box-sizing:border-box;position:relative;z-index:1;display:flex;flex-direction:column;gap:20px;",
|
|
2121
|
+
"width:min(380px,100%);padding:0 0 24px;overflow:hidden;border:0;border-radius:var(--dsw-radius-panel,12px);",
|
|
2122
|
+
"background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-elevation-prominent);",
|
|
2123
|
+
"animation:dsw-sc-modal-enter var(--ds-transition-duration,.16s) var(--ds-ease-in-out,ease)}",
|
|
2124
|
+
".dsw-sc-modal-dialog:focus{outline:none}",
|
|
2125
|
+
"@keyframes dsw-sc-modal-enter{from{opacity:0}to{opacity:1}}",
|
|
2126
|
+
"@media (prefers-reduced-motion:reduce){.dsw-sc-modal-mask::after,.dsw-sc-modal-dialog{animation:none}}",
|
|
2127
|
+
".dsw-sc-modal-header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:22px 14px 12px 24px}",
|
|
2128
|
+
".dsw-sc-modal-title{margin:0;font-size:16px;line-height:24px;font-weight:500;color:var(--dsw-alias-label-primary)}",
|
|
2129
|
+
".dsw-sc-modal-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;",
|
|
2130
|
+
"border:none;border-radius:var(--dsw-radius-sm);background:transparent;cursor:pointer;color:var(--dsw-alias-label-secondary);padding:0}",
|
|
2131
|
+
".dsw-sc-modal-close:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2132
|
+
".dsw-sc-modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:0 24px}",
|
|
2133
|
+
".dsw-sc-modal-btn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;height:36px;",
|
|
2134
|
+
"padding:0 14px;border:none;border-radius:var(--dsw-radius-md);cursor:pointer;font:inherit;font-size:14px;line-height:22px}",
|
|
2135
|
+
".dsw-sc-modal-btn-outline{border:.5px solid var(--dsw-alias-border-l3);background:transparent;color:var(--dsw-alias-label-primary)}",
|
|
2136
|
+
".dsw-sc-modal-btn-outline:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2137
|
+
".dsw-sc-modal-btn-primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}",
|
|
2138
|
+
".dsw-sc-modal-btn-primary:hover{background:var(--dsw-alias-button-primary-hover)}",
|
|
2139
|
+
".dsw-sc-replaced{display:none !important}"
|
|
2140
|
+
].join("");
|
|
2141
|
+
|
|
2142
|
+
var scEnabled = false;
|
|
2143
|
+
var scEnabledListeners = new Set();
|
|
2144
|
+
function setScEnabled(next) {
|
|
2145
|
+
var v = next === true;
|
|
2146
|
+
if (scEnabled === v) return;
|
|
2147
|
+
scEnabled = v;
|
|
2148
|
+
scEnabledListeners.forEach(function (fn) { try { fn(); } catch (_) {} });
|
|
2149
|
+
}
|
|
2150
|
+
function useScEnabled() {
|
|
2151
|
+
return React.useSyncExternalStore(
|
|
2152
|
+
function (fn) { scEnabledListeners.add(fn); return function () { scEnabledListeners.delete(fn); }; },
|
|
2153
|
+
function () { return scEnabled; },
|
|
2154
|
+
function () { return scEnabled; }
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
function refreshScEnabled() {
|
|
2158
|
+
return fetch(API + "/settings").then(function (r) { return r.json(); }).then(function (j) {
|
|
2159
|
+
// API shape is { ok, persisted, value: { sessionControlsEnabled, ... } }
|
|
2160
|
+
var v = j && j.value && typeof j.value === "object" ? j.value : j;
|
|
2161
|
+
if (v && typeof v === "object") setScEnabled(v.sessionControlsEnabled === true);
|
|
2162
|
+
}).catch(function () {});
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function iconUndo() {
|
|
2166
|
+
// 撤回:左向 U 形撤回箭头(非圆圈),与重试区分
|
|
2167
|
+
return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true },
|
|
2168
|
+
h("path", {
|
|
2169
|
+
d: "M5.2 4.2H9.5a3.8 3.8 0 0 1 0 7.6H4.8",
|
|
2170
|
+
fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round"
|
|
2171
|
+
}),
|
|
2172
|
+
h("path", {
|
|
2173
|
+
d: "M7.4 1.8 4.2 4.2 7.4 6.6",
|
|
2174
|
+
fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round"
|
|
2175
|
+
})
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
function iconRetry() {
|
|
2179
|
+
// 重试:闭环刷新圆箭头(顺时针),与撤回的开口 U 形对比
|
|
2180
|
+
return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true },
|
|
2181
|
+
h("path", {
|
|
2182
|
+
d: "M13.25 8a5.25 5.25 0 1 1-1.55-3.7",
|
|
2183
|
+
fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round"
|
|
2184
|
+
}),
|
|
2185
|
+
h("path", {
|
|
2186
|
+
d: "M13.4 2.2v3.6H9.8",
|
|
2187
|
+
fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round"
|
|
2188
|
+
})
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
function iconStop() {
|
|
2192
|
+
return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true },
|
|
2193
|
+
h("rect", { x: 3, y: 3, width: 10, height: 10, rx: 3, fill: "currentColor" })
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function chatOf(props) {
|
|
2198
|
+
if (typeof props.useChat === "function") return props.useChat(function (c) { return c; });
|
|
2199
|
+
if (typeof props.useSession === "function") {
|
|
2200
|
+
return props.useSession(function (s) { return s && s.chat; });
|
|
2201
|
+
}
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/** Resolve assistant finalNode.seq (any seq in the turn works for retract/regenerate APIs). */
|
|
2206
|
+
function assistantSeqForMessage(chat, messageId) {
|
|
2207
|
+
if (!messageId || !chat) return null;
|
|
2208
|
+
var found = null;
|
|
2209
|
+
function consider(finalNode) {
|
|
2210
|
+
if (found != null || !finalNode || finalNode.messageId !== messageId) return;
|
|
2211
|
+
if (Number.isSafeInteger(finalNode.seq)) found = finalNode.seq;
|
|
2212
|
+
}
|
|
2213
|
+
function visit(node) {
|
|
2214
|
+
if (!node || found != null) return;
|
|
2215
|
+
var data = node.data || {};
|
|
2216
|
+
consider(data.closing && data.closing.finalNode);
|
|
2217
|
+
consider(data.finalNode);
|
|
2218
|
+
}
|
|
2219
|
+
if (chat.timeline && chat.timeline.turns && typeof chat.timeline.turns.forEach === "function") {
|
|
2220
|
+
chat.timeline.turns.forEach(function (turn) {
|
|
2221
|
+
if (found != null) return;
|
|
2222
|
+
var tail = turn && turn.data && typeof turn.data.get === "function" ? turn.data.get("turn-tail") : null;
|
|
2223
|
+
consider(tail && tail.closing && tail.closing.finalNode);
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
if (found == null && chat.nodes && typeof chat.nodes.forEach === "function") chat.nodes.forEach(visit);
|
|
2227
|
+
if (found == null && chat.order && chat.nodes) {
|
|
2228
|
+
for (var i = 0; i < chat.order.length; i++) {
|
|
2229
|
+
try { visit(chat.nodes.get(chat.order[i])); } catch (_) {}
|
|
2230
|
+
if (found != null) break;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return found;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
async function scPost(path, body) {
|
|
2237
|
+
var r = await fetch(API + path, {
|
|
2238
|
+
method: "POST",
|
|
2239
|
+
headers: { "content-type": "application/json" },
|
|
2240
|
+
body: JSON.stringify(body)
|
|
2241
|
+
});
|
|
2242
|
+
var j = null;
|
|
2243
|
+
try { j = await r.json(); } catch (_) { j = null; }
|
|
2244
|
+
if (!j) throw new Error("empty response");
|
|
2245
|
+
if (j.ok === false && j.error) throw new Error(j.error);
|
|
2246
|
+
return j;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
var REGENERATE_CARRIER = "已丢弃此前的回答,准备重新生成。";
|
|
2250
|
+
var REGENERATE_KIND = "dsw-regenerate";
|
|
2251
|
+
var scHideNotes = [];
|
|
2252
|
+
var scHideEpoch = 0;
|
|
2253
|
+
var scHideListeners = new Set();
|
|
2254
|
+
function bumpScHide() {
|
|
2255
|
+
scHideEpoch += 1;
|
|
2256
|
+
scHideListeners.forEach(function (fn) { try { fn(); } catch (_) {} });
|
|
2257
|
+
}
|
|
2258
|
+
function useScHideEpoch() {
|
|
2259
|
+
return React.useSyncExternalStore(
|
|
2260
|
+
function (fn) { scHideListeners.add(fn); return function () { scHideListeners.delete(fn); }; },
|
|
2261
|
+
function () { return scHideEpoch; },
|
|
2262
|
+
function () { return 0; }
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
function noteLiveReplace(sessionId, j) {
|
|
2266
|
+
if (!sessionId || !j || j.applied !== "live") return;
|
|
2267
|
+
var messageId = j.prompt && j.prompt.messageId ? String(j.prompt.messageId) : "";
|
|
2268
|
+
var hasRange = Number.isSafeInteger(j.hideFrom) && Number.isSafeInteger(j.hideUntil);
|
|
2269
|
+
if (!hasRange && !messageId) return;
|
|
2270
|
+
scHideNotes.push({
|
|
2271
|
+
sessionId: String(sessionId),
|
|
2272
|
+
from: hasRange ? j.hideFrom : null,
|
|
2273
|
+
until: hasRange ? j.hideUntil : null,
|
|
2274
|
+
exclusive: j.hideFromExclusive !== false,
|
|
2275
|
+
messageId: messageId
|
|
2276
|
+
});
|
|
2277
|
+
if (scHideNotes.length > 80) scHideNotes.splice(0, scHideNotes.length - 80);
|
|
2278
|
+
bumpScHide();
|
|
2279
|
+
}
|
|
2280
|
+
function eventOfEntry(entry) {
|
|
2281
|
+
if (!entry || typeof entry !== "object") return null;
|
|
2282
|
+
if (entry.event && typeof entry.event === "object" && entry.event.type) return entry.event;
|
|
2283
|
+
if (entry.type && Number.isSafeInteger(entry.seq)) return entry;
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
function carrierText(ev) {
|
|
2287
|
+
var parts = ev && ev.data && ev.data.content;
|
|
2288
|
+
if (!Array.isArray(parts)) return "";
|
|
2289
|
+
var out = [];
|
|
2290
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2291
|
+
if (parts[i] && parts[i].type === "text") out.push(String(parts[i].text || ""));
|
|
2292
|
+
}
|
|
2293
|
+
return out.join("\n").trim();
|
|
2294
|
+
}
|
|
2295
|
+
function replaceBounds(ev) {
|
|
2296
|
+
var op = ev && ev.surfaceOp;
|
|
2297
|
+
if (!op || typeof op !== "object" || op.op !== "replace") return null;
|
|
2298
|
+
if (!Number.isSafeInteger(op.startSeq) || !Number.isSafeInteger(op.endSeq) || !Number.isSafeInteger(ev.seq)) return null;
|
|
2299
|
+
return op;
|
|
2300
|
+
}
|
|
2301
|
+
function rangesFromEvents(entries) {
|
|
2302
|
+
var events = [];
|
|
2303
|
+
var list = entries || [];
|
|
2304
|
+
for (var i = 0; i < list.length; i++) {
|
|
2305
|
+
var item = eventOfEntry(list[i]);
|
|
2306
|
+
if (item) events.push(item);
|
|
2307
|
+
}
|
|
2308
|
+
var ranges = [];
|
|
2309
|
+
var ids = [];
|
|
2310
|
+
var seqs = [];
|
|
2311
|
+
for (var n = 0; n < events.length; n++) {
|
|
2312
|
+
var ev = events[n];
|
|
2313
|
+
var source = ev.data && ev.data.source;
|
|
2314
|
+
if (ev.type === "user/message" && source && source.kind === REGENERATE_KIND) {
|
|
2315
|
+
if (ev.data.id) ids.push(String(ev.data.id));
|
|
2316
|
+
if (Number.isSafeInteger(ev.seq)) seqs.push(ev.seq);
|
|
2317
|
+
}
|
|
2318
|
+
// A system/message replace only shadows the system prompt, but its own seq
|
|
2319
|
+
// is much later. Hiding through that seq would cover every earlier turn.
|
|
2320
|
+
if (ev.type !== "user/message") continue;
|
|
2321
|
+
var op = replaceBounds(ev);
|
|
2322
|
+
if (!op) continue;
|
|
2323
|
+
if (carrierText(ev) === REGENERATE_CARRIER) {
|
|
2324
|
+
var boundary = -1;
|
|
2325
|
+
for (var j = 0; j < n; j++) {
|
|
2326
|
+
var prev = events[j];
|
|
2327
|
+
if (!prev || prev.surfaceOp !== "append" || !Number.isSafeInteger(prev.seq)) continue;
|
|
2328
|
+
if (prev.seq < op.startSeq && prev.seq > boundary) boundary = prev.seq;
|
|
2329
|
+
}
|
|
2330
|
+
ranges.push({
|
|
2331
|
+
after: boundary >= 0 ? boundary : op.startSeq - 0.5,
|
|
2332
|
+
until: ev.seq,
|
|
2333
|
+
keepBoundary: boundary >= 0
|
|
2334
|
+
});
|
|
2335
|
+
} else {
|
|
2336
|
+
ranges.push({ after: op.startSeq - 0.5, until: ev.seq, keepBoundary: false });
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return { ranges: ranges, ids: ids, seqs: seqs };
|
|
2340
|
+
}
|
|
2341
|
+
function rangesForSession(sessionId, entries) {
|
|
2342
|
+
var plan = rangesFromEvents(entries);
|
|
2343
|
+
var id = String(sessionId);
|
|
2344
|
+
for (var i = 0; i < scHideNotes.length; i++) {
|
|
2345
|
+
var note = scHideNotes[i];
|
|
2346
|
+
if (note.sessionId !== id) continue;
|
|
2347
|
+
if (note.messageId) plan.ids.push(note.messageId);
|
|
2348
|
+
if (note.from == null || note.until == null) continue;
|
|
2349
|
+
plan.ranges.push({
|
|
2350
|
+
after: note.exclusive ? note.from : note.from - 0.5,
|
|
2351
|
+
until: note.until,
|
|
2352
|
+
keepBoundary: !!note.exclusive
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
return plan;
|
|
2356
|
+
}
|
|
2357
|
+
function eachChatNode(chat, visit) {
|
|
2358
|
+
if (chat && chat.order && chat.nodes && typeof chat.nodes.get === "function") {
|
|
2359
|
+
for (var i = 0; i < chat.order.length; i++) visit(chat.nodes.get(chat.order[i]));
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
if (chat && chat.nodes && typeof chat.nodes.values === "function") {
|
|
2363
|
+
var values = chat.nodes.values();
|
|
2364
|
+
if (values && typeof values.forEach === "function") values.forEach(visit);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
if (chat && chat.nodes && typeof chat.nodes.forEach === "function") chat.nodes.forEach(visit);
|
|
2368
|
+
}
|
|
2369
|
+
function nodeAnchor(node) {
|
|
2370
|
+
if (!node) return null;
|
|
2371
|
+
if (typeof node.anchorSeq === "number" && Number.isFinite(node.anchorSeq)) return node.anchorSeq;
|
|
2372
|
+
var data = node.data || {};
|
|
2373
|
+
if (Number.isSafeInteger(data.seq)) return data.seq;
|
|
2374
|
+
return null;
|
|
2375
|
+
}
|
|
2376
|
+
function nodeMessageId(node) {
|
|
2377
|
+
if (!node) return "";
|
|
2378
|
+
var data = node.data || {};
|
|
2379
|
+
if (data.id != null && data.id !== "") return String(data.id);
|
|
2380
|
+
if (data.messageId != null && data.messageId !== "") return String(data.messageId);
|
|
2381
|
+
if (node.id != null && node.id !== "") return String(node.id);
|
|
2382
|
+
return "";
|
|
2383
|
+
}
|
|
2384
|
+
function nodeHidden(node, plan) {
|
|
2385
|
+
if (!node) return false;
|
|
2386
|
+
var data = node.data || {};
|
|
2387
|
+
var source = data.source || (data.message && data.message.source);
|
|
2388
|
+
if (source && source.kind === REGENERATE_KIND) return true;
|
|
2389
|
+
var id = nodeMessageId(node);
|
|
2390
|
+
if (id && plan.ids && plan.ids.indexOf(id) >= 0) return true;
|
|
2391
|
+
var seq = nodeAnchor(node);
|
|
2392
|
+
if (seq != null && plan.seqs && plan.seqs.indexOf(seq) >= 0) return true;
|
|
2393
|
+
if (seq == null) return false;
|
|
2394
|
+
for (var i = 0; i < plan.ranges.length; i++) {
|
|
2395
|
+
var range = plan.ranges[i];
|
|
2396
|
+
if (!(seq > range.after && seq < range.until)) continue;
|
|
2397
|
+
if (range.keepBoundary && node.kind === "user" && seq <= range.after + 0.001) return false;
|
|
2398
|
+
return true;
|
|
2399
|
+
}
|
|
2400
|
+
return false;
|
|
2401
|
+
}
|
|
2402
|
+
function nodeTurnId(node) {
|
|
2403
|
+
if (!node) return "";
|
|
2404
|
+
var data = node.data || {};
|
|
2405
|
+
if (data.turn != null && data.turn !== "") return String(data.turn);
|
|
2406
|
+
var location = node.location;
|
|
2407
|
+
if (location && (location.kind === "turn" || location.kind === "step")) {
|
|
2408
|
+
var turn = location.turn;
|
|
2409
|
+
if (turn && turn.turn != null && turn.turn !== "") return String(turn.turn);
|
|
2410
|
+
}
|
|
2411
|
+
return "";
|
|
2412
|
+
}
|
|
2413
|
+
function applyReplacedHide(chat, plan) {
|
|
2414
|
+
if (typeof document === "undefined") return;
|
|
2415
|
+
if (!chat) {
|
|
2416
|
+
var stuck = document.querySelectorAll(".dsw-sc-replaced");
|
|
2417
|
+
for (var n = 0; n < stuck.length; n++) stuck[n].classList.remove("dsw-sc-replaced");
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
var known = new Set();
|
|
2421
|
+
var hidden = new Set();
|
|
2422
|
+
// Turns whose process chrome (「已搜索代码」「执行了命令」「已调用工具」) must leave with the retract.
|
|
2423
|
+
var hiddenTurns = new Set();
|
|
2424
|
+
eachChatNode(chat, function (node) {
|
|
2425
|
+
if (!node || node.key == null) return;
|
|
2426
|
+
var key = String(node.key);
|
|
2427
|
+
known.add(key);
|
|
2428
|
+
if (!nodeHidden(node, plan)) return;
|
|
2429
|
+
hidden.add(key);
|
|
2430
|
+
if (node.kind === "turn-process" || node.kind === "tool-call" || node.kind === "tool-result" || node.kind === "assistant-step") {
|
|
2431
|
+
var turnId = nodeTurnId(node);
|
|
2432
|
+
if (turnId) hiddenTurns.add(turnId);
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
var flows = document.querySelectorAll("[data-chat-node-key]");
|
|
2436
|
+
for (var i = 0; i < flows.length; i++) {
|
|
2437
|
+
var el = flows[i];
|
|
2438
|
+
var key = el.getAttribute("data-chat-node-key");
|
|
2439
|
+
if (!known.has(key)) continue;
|
|
2440
|
+
if (hidden.has(key)) el.classList.add("dsw-sc-replaced");
|
|
2441
|
+
else el.classList.remove("dsw-sc-replaced");
|
|
2442
|
+
}
|
|
2443
|
+
// Collapsed step-process groups render under data-chat-group-key / data-step-process,
|
|
2444
|
+
// not data-chat-node-key. Surface replace only shadows message nodes, so these stubs
|
|
2445
|
+
// stayed after retract. Hide when the turn was retracted or every known member is gone.
|
|
2446
|
+
var groups = document.querySelectorAll("[data-step-process], [data-chat-group-key]");
|
|
2447
|
+
for (var g = 0; g < groups.length; g++) {
|
|
2448
|
+
var group = groups[g];
|
|
2449
|
+
var turnAttr = group.getAttribute("data-chat-turn");
|
|
2450
|
+
var hideGroup = turnAttr != null && hiddenTurns.has(String(turnAttr));
|
|
2451
|
+
if (!hideGroup) {
|
|
2452
|
+
var members = group.querySelectorAll("[data-chat-node-key]");
|
|
2453
|
+
var anyKnown = false;
|
|
2454
|
+
var anyShown = false;
|
|
2455
|
+
for (var m = 0; m < members.length; m++) {
|
|
2456
|
+
var mk = members[m].getAttribute("data-chat-node-key");
|
|
2457
|
+
if (!known.has(mk)) continue;
|
|
2458
|
+
anyKnown = true;
|
|
2459
|
+
if (!hidden.has(mk)) { anyShown = true; break; }
|
|
2460
|
+
}
|
|
2461
|
+
hideGroup = anyKnown && !anyShown;
|
|
2462
|
+
}
|
|
2463
|
+
if (hideGroup) group.classList.add("dsw-sc-replaced");
|
|
2464
|
+
else group.classList.remove("dsw-sc-replaced");
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function sessionEventEntries(sessions, sessionId) {
|
|
2468
|
+
try {
|
|
2469
|
+
var binding = sessions && typeof sessions.binding === "function" ? sessions.binding(sessionId) : null;
|
|
2470
|
+
var source = binding && binding.session && binding.session.eventSource;
|
|
2471
|
+
var snap = source && typeof source.getSnapshot === "function" ? source.getSnapshot() : null;
|
|
2472
|
+
return {
|
|
2473
|
+
entries: snap && snap.entries ? snap.entries : [],
|
|
2474
|
+
source: source || null
|
|
2475
|
+
};
|
|
2476
|
+
} catch (_) {
|
|
2477
|
+
return { entries: [], source: null };
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function chatMaxSeq(chat) {
|
|
2482
|
+
var max = -1;
|
|
2483
|
+
eachChatNode(chat, function (node) {
|
|
2484
|
+
var seq = nodeAnchor(node);
|
|
2485
|
+
if (seq != null && seq > max) max = seq;
|
|
2486
|
+
});
|
|
2487
|
+
return max;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
/** The open window still lists seqs past the marker, so it is showing a longer log than the one just written. */
|
|
2491
|
+
function pageAheadOfCut(chat, j) {
|
|
2492
|
+
var until = j && Number(j.hideUntil);
|
|
2493
|
+
return Number.isFinite(until) && chatMaxSeq(chat) > until;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
async function settleSession(sessions, sessionId) {
|
|
2497
|
+
try {
|
|
2498
|
+
var binding = sessions && typeof sessions.binding === "function" ? sessions.binding(sessionId) : null;
|
|
2499
|
+
var session = binding && binding.session;
|
|
2500
|
+
if (session && typeof session.resync === "function") {
|
|
2501
|
+
await session.resync();
|
|
2502
|
+
return true;
|
|
2503
|
+
}
|
|
2504
|
+
} catch (_) {}
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function listUserAnchors(chat) {
|
|
2509
|
+
var out = [];
|
|
2510
|
+
function visit(node) {
|
|
2511
|
+
if (!node || node.kind !== "user" || node.key == null) return;
|
|
2512
|
+
var seq = node.data && Number.isSafeInteger(node.data.seq) ? node.data.seq : node.anchorSeq;
|
|
2513
|
+
if (!Number.isSafeInteger(seq)) return;
|
|
2514
|
+
out.push({ key: String(node.key), seq: seq });
|
|
2515
|
+
}
|
|
2516
|
+
if (chat && chat.order && chat.nodes && typeof chat.nodes.get === "function") {
|
|
2517
|
+
for (var i = 0; i < chat.order.length; i++) visit(chat.nodes.get(chat.order[i]));
|
|
2518
|
+
} else if (chat && chat.nodes && typeof chat.nodes.values === "function") {
|
|
2519
|
+
var values = chat.nodes.values();
|
|
2520
|
+
if (values && typeof values.forEach === "function") values.forEach(visit);
|
|
2521
|
+
} else if (chat && chat.nodes && typeof chat.nodes.forEach === "function") {
|
|
2522
|
+
chat.nodes.forEach(visit);
|
|
2523
|
+
}
|
|
2524
|
+
out.sort(function (a, b) { return a.seq - b.seq; });
|
|
2525
|
+
return out;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function cssAttr(value) {
|
|
2529
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
|
|
2530
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
var RETRACT_ICON_HTML = '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M5.2 4.2H9.5a3.8 3.8 0 0 1 0 7.6H4.8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7.4 1.8 4.2 4.2 7.4 6.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
|
|
2534
|
+
|
|
2535
|
+
function userActionHost(key) {
|
|
2536
|
+
var flow = document.querySelector('[data-chat-flow-kind="user"][data-chat-node-key="' + cssAttr(key) + '"]')
|
|
2537
|
+
|| document.querySelector('[data-chat-node-key="' + cssAttr(key) + '"]');
|
|
2538
|
+
if (!flow) return null;
|
|
2539
|
+
return flow.querySelector('[data-clock="start"]');
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
var SC_CLOSE_ICON = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M3.2 3.2l7.6 7.6M10.8 3.2L3.2 10.8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
|
|
2543
|
+
|
|
2544
|
+
/** 页面内确认,版式对齐官方 Modal(遮罩、卡片、取消/确定)。 */
|
|
2545
|
+
function scConfirm(message) {
|
|
2546
|
+
return new Promise(function (resolve) {
|
|
2547
|
+
var restore = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
2548
|
+
var root = document.createElement("div");
|
|
2549
|
+
root.className = "dsw-sc-modal";
|
|
2550
|
+
root.setAttribute("role", "presentation");
|
|
2551
|
+
var mask = document.createElement("div");
|
|
2552
|
+
mask.className = "dsw-sc-modal-mask";
|
|
2553
|
+
mask.setAttribute("aria-hidden", "true");
|
|
2554
|
+
var dialog = document.createElement("div");
|
|
2555
|
+
dialog.className = "dsw-sc-modal-dialog";
|
|
2556
|
+
dialog.tabIndex = -1;
|
|
2557
|
+
dialog.setAttribute("role", "dialog");
|
|
2558
|
+
dialog.setAttribute("aria-modal", "true");
|
|
2559
|
+
dialog.setAttribute("aria-labelledby", "dsw-sc-confirm-title");
|
|
2560
|
+
var header = document.createElement("div");
|
|
2561
|
+
header.className = "dsw-sc-modal-header";
|
|
2562
|
+
var title = document.createElement("h2");
|
|
2563
|
+
title.className = "dsw-sc-modal-title";
|
|
2564
|
+
title.id = "dsw-sc-confirm-title";
|
|
2565
|
+
title.textContent = message;
|
|
2566
|
+
var closeBtn = document.createElement("button");
|
|
2567
|
+
closeBtn.type = "button";
|
|
2568
|
+
closeBtn.className = "dsw-sc-modal-close";
|
|
2569
|
+
closeBtn.setAttribute("aria-label", "关闭");
|
|
2570
|
+
closeBtn.innerHTML = SC_CLOSE_ICON;
|
|
2571
|
+
header.appendChild(title);
|
|
2572
|
+
header.appendChild(closeBtn);
|
|
2573
|
+
var footer = document.createElement("div");
|
|
2574
|
+
footer.className = "dsw-sc-modal-footer";
|
|
2575
|
+
var cancelBtn = document.createElement("button");
|
|
2576
|
+
cancelBtn.type = "button";
|
|
2577
|
+
cancelBtn.className = "dsw-sc-modal-btn dsw-sc-modal-btn-outline";
|
|
2578
|
+
cancelBtn.textContent = "取消";
|
|
2579
|
+
var okBtn = document.createElement("button");
|
|
2580
|
+
okBtn.type = "button";
|
|
2581
|
+
okBtn.className = "dsw-sc-modal-btn dsw-sc-modal-btn-primary";
|
|
2582
|
+
okBtn.textContent = "确定";
|
|
2583
|
+
footer.appendChild(cancelBtn);
|
|
2584
|
+
footer.appendChild(okBtn);
|
|
2585
|
+
dialog.appendChild(header);
|
|
2586
|
+
dialog.appendChild(footer);
|
|
2587
|
+
root.appendChild(mask);
|
|
2588
|
+
root.appendChild(dialog);
|
|
2589
|
+
var settled = false;
|
|
2590
|
+
function finish(ok) {
|
|
2591
|
+
if (settled) return;
|
|
2592
|
+
settled = true;
|
|
2593
|
+
document.removeEventListener("keydown", onKey, true);
|
|
2594
|
+
if (root.parentNode) root.parentNode.removeChild(root);
|
|
2595
|
+
resolve(ok);
|
|
2596
|
+
if (restore && restore.isConnected) restore.focus();
|
|
2597
|
+
}
|
|
2598
|
+
function onKey(ev) {
|
|
2599
|
+
if (ev.key === "Escape") {
|
|
2600
|
+
ev.preventDefault();
|
|
2601
|
+
ev.stopPropagation();
|
|
2602
|
+
finish(false);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (ev.key !== "Tab") return;
|
|
2606
|
+
var items = [closeBtn, cancelBtn, okBtn];
|
|
2607
|
+
var i = items.indexOf(document.activeElement);
|
|
2608
|
+
ev.preventDefault();
|
|
2609
|
+
if (i < 0) {
|
|
2610
|
+
(ev.shiftKey ? okBtn : closeBtn).focus();
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
items[(i + (ev.shiftKey ? items.length - 1 : 1)) % items.length].focus();
|
|
2614
|
+
}
|
|
2615
|
+
mask.addEventListener("click", function () { finish(false); });
|
|
2616
|
+
closeBtn.addEventListener("click", function () { finish(false); });
|
|
2617
|
+
cancelBtn.addEventListener("click", function () { finish(false); });
|
|
2618
|
+
okBtn.addEventListener("click", function () { finish(true); });
|
|
2619
|
+
document.addEventListener("keydown", onKey, true);
|
|
2620
|
+
document.body.appendChild(root);
|
|
2621
|
+
okBtn.focus();
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/** 撤回:直接挂进用户消息下方已有的操作行。官方 nodes 没有 forEach,且 React 会清掉 portal。 */
|
|
2626
|
+
function UserRetractHost(props) {
|
|
2627
|
+
var enabled = useScEnabled();
|
|
2628
|
+
var chat = chatOf(props);
|
|
2629
|
+
var sessionId = props.sessionId;
|
|
2630
|
+
var sessions = props._sessions;
|
|
2631
|
+
var signature = enabled && sessionId ? listUserAnchors(chat).map(function (row) { return row.key + "\0" + row.seq; }).join("\n") : "";
|
|
2632
|
+
|
|
2633
|
+
React.useLayoutEffect(function () {
|
|
2634
|
+
var owned = [];
|
|
2635
|
+
function removeOwned() {
|
|
2636
|
+
for (var i = 0; i < owned.length; i++) {
|
|
2637
|
+
if (owned[i].parentNode) owned[i].parentNode.removeChild(owned[i]);
|
|
2638
|
+
}
|
|
2639
|
+
owned = [];
|
|
2640
|
+
}
|
|
2641
|
+
if (!signature || !sessionId) {
|
|
2642
|
+
removeOwned();
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
var rows = signature.split("\n").map(function (line) {
|
|
2646
|
+
var cut = line.indexOf("\0");
|
|
2647
|
+
return { key: line.slice(0, cut), seq: Number(line.slice(cut + 1)) };
|
|
2648
|
+
});
|
|
2649
|
+
function ensure(host, seq) {
|
|
2650
|
+
var btn = host.querySelector(':scope > [data-dsw-retract="' + seq + '"]');
|
|
2651
|
+
if (btn) return;
|
|
2652
|
+
btn = document.createElement("button");
|
|
2653
|
+
btn.type = "button";
|
|
2654
|
+
btn.className = "dsw-sc-action";
|
|
2655
|
+
btn.dataset.dswRetract = String(seq);
|
|
2656
|
+
btn.title = "撤回:收起本回合及之后的内容";
|
|
2657
|
+
btn.setAttribute("aria-label", "撤回");
|
|
2658
|
+
btn.innerHTML = RETRACT_ICON_HTML;
|
|
2659
|
+
btn.addEventListener("mousedown", function (ev) { ev.stopPropagation(); });
|
|
2660
|
+
btn.addEventListener("click", function (ev) {
|
|
2661
|
+
ev.stopPropagation();
|
|
2662
|
+
if (btn.disabled || btn.dataset.confirming) return;
|
|
2663
|
+
btn.dataset.confirming = "1";
|
|
2664
|
+
scConfirm("确定撤回该回合及之后的内容?").then(function (ok) {
|
|
2665
|
+
delete btn.dataset.confirming;
|
|
2666
|
+
if (!ok) return;
|
|
2667
|
+
if (btn.isConnected) {
|
|
2668
|
+
btn.disabled = true;
|
|
2669
|
+
btn.setAttribute("data-busy", "");
|
|
2670
|
+
}
|
|
2671
|
+
return scPost("/session/retract", { sessionId: sessionId, userMessageSeq: seq });
|
|
2672
|
+
}).then(function (j) {
|
|
2673
|
+
if (!j || !btn.isConnected) return;
|
|
2674
|
+
btn.disabled = false;
|
|
2675
|
+
btn.removeAttribute("data-busy");
|
|
2676
|
+
noteLiveReplace(sessionId, j);
|
|
2677
|
+
if (j.applied === "live" && !pageAheadOfCut(chat, j)) return;
|
|
2678
|
+
return settleSession(sessions, sessionId).then(function (refreshed) {
|
|
2679
|
+
if (!refreshed && j.hint) {
|
|
2680
|
+
try { window.alert(j.hint); } catch (_) {}
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
}).catch(function (e) {
|
|
2684
|
+
if (btn.isConnected) {
|
|
2685
|
+
btn.disabled = false;
|
|
2686
|
+
btn.removeAttribute("data-busy");
|
|
2687
|
+
}
|
|
2688
|
+
try { window.alert("操作失败:" + String(e && e.message || e)); } catch (_) {}
|
|
2689
|
+
});
|
|
2690
|
+
});
|
|
2691
|
+
host.appendChild(btn);
|
|
2692
|
+
owned.push(btn);
|
|
2693
|
+
}
|
|
2694
|
+
var alive = true;
|
|
2695
|
+
function scan() {
|
|
2696
|
+
if (!alive) return;
|
|
2697
|
+
for (var i = 0; i < rows.length; i++) {
|
|
2698
|
+
var host = userActionHost(rows[i].key);
|
|
2699
|
+
if (host) ensure(host, rows[i].seq);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
scan();
|
|
2703
|
+
var root = document.querySelector("[data-conversation-scroll]") || document.body;
|
|
2704
|
+
var obs = new MutationObserver(scan);
|
|
2705
|
+
obs.observe(root, { childList: true, subtree: true });
|
|
2706
|
+
return function () {
|
|
2707
|
+
alive = false;
|
|
2708
|
+
obs.disconnect();
|
|
2709
|
+
removeOwned();
|
|
2710
|
+
};
|
|
2711
|
+
}, [signature, sessionId, sessions]);
|
|
2712
|
+
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
/** 确认重试/撤回后,按替换标记收起原输出,新回复留在页面上。 */
|
|
2717
|
+
function ReplacedOutputHost(props) {
|
|
2718
|
+
var enabled = useScEnabled();
|
|
2719
|
+
var epoch = useScHideEpoch();
|
|
2720
|
+
var chat = chatOf(props);
|
|
2721
|
+
var sessionId = props.sessionId;
|
|
2722
|
+
var sessions = props._sessions;
|
|
2723
|
+
var signature = "";
|
|
2724
|
+
if (enabled && sessionId) {
|
|
2725
|
+
var parts = [];
|
|
2726
|
+
eachChatNode(chat, function (node) {
|
|
2727
|
+
if (!node || node.key == null) return;
|
|
2728
|
+
parts.push(String(node.key) + ":" + String(node.anchorSeq == null ? "" : node.anchorSeq) + ":" + String(node.kind || ""));
|
|
2729
|
+
});
|
|
2730
|
+
signature = parts.join("\n") + "\0" + epoch;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
React.useLayoutEffect(function () {
|
|
2734
|
+
if (!signature || !sessionId) {
|
|
2735
|
+
applyReplacedHide(null, { ranges: [], ids: [] });
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
var alive = true;
|
|
2739
|
+
var scheduled = false;
|
|
2740
|
+
function apply() {
|
|
2741
|
+
if (!alive) return;
|
|
2742
|
+
var read = sessionEventEntries(sessions, sessionId);
|
|
2743
|
+
applyReplacedHide(chat, rangesForSession(sessionId, read.entries));
|
|
2744
|
+
}
|
|
2745
|
+
function schedule() {
|
|
2746
|
+
if (scheduled || !alive) return;
|
|
2747
|
+
scheduled = true;
|
|
2748
|
+
window.requestAnimationFrame(function () {
|
|
2749
|
+
scheduled = false;
|
|
2750
|
+
apply();
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
apply();
|
|
2754
|
+
var unsub = function () {};
|
|
2755
|
+
var read = sessionEventEntries(sessions, sessionId);
|
|
2756
|
+
if (read.source && typeof read.source.subscribe === "function") {
|
|
2757
|
+
try {
|
|
2758
|
+
var off = read.source.subscribe(schedule);
|
|
2759
|
+
if (typeof off === "function") unsub = off;
|
|
2760
|
+
} catch (_) {}
|
|
2761
|
+
}
|
|
2762
|
+
var root = document.querySelector("[data-conversation-scroll]") || document.body;
|
|
2763
|
+
var obs = new MutationObserver(schedule);
|
|
2764
|
+
obs.observe(root, { childList: true, subtree: true });
|
|
2765
|
+
return function () {
|
|
2766
|
+
alive = false;
|
|
2767
|
+
try { unsub(); } catch (_) {}
|
|
2768
|
+
obs.disconnect();
|
|
2769
|
+
};
|
|
2770
|
+
}, [signature, sessionId, sessions]);
|
|
2771
|
+
|
|
2772
|
+
return null;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
/** 重试,只出现在复制与分支之间。 */
|
|
2776
|
+
function AssistantSessionActions(props) {
|
|
2777
|
+
var enabled = useScEnabled();
|
|
2778
|
+
var busyState = React.useState(false);
|
|
2779
|
+
var busy = busyState[0], setBusy = busyState[1];
|
|
2780
|
+
var pendingRef = React.useRef(false);
|
|
2781
|
+
var sessions = props._sessions;
|
|
2782
|
+
var chat = chatOf(props);
|
|
2783
|
+
if (!enabled || props.messageId == null) return null;
|
|
2784
|
+
var seq = assistantSeqForMessage(chat, props.messageId);
|
|
2785
|
+
var sessionId = props.sessionId;
|
|
2786
|
+
if (!sessionId) return null;
|
|
2787
|
+
|
|
2788
|
+
function run(path, confirmMsg) {
|
|
2789
|
+
if (busy || pendingRef.current) return;
|
|
2790
|
+
pendingRef.current = true;
|
|
2791
|
+
scConfirm(confirmMsg).then(function (ok) {
|
|
2792
|
+
pendingRef.current = false;
|
|
2793
|
+
if (!ok) return;
|
|
2794
|
+
setBusy(true);
|
|
2795
|
+
var body = { sessionId: sessionId };
|
|
2796
|
+
if (Number.isSafeInteger(seq)) body.userMessageSeq = seq;
|
|
2797
|
+
if (props.messageId != null) body.messageId = props.messageId;
|
|
2798
|
+
scPost(path, body).then(function (j) {
|
|
2799
|
+
noteLiveReplace(sessionId, j);
|
|
2800
|
+
if (pageAheadOfCut(chat, j)) {
|
|
2801
|
+
return settleSession(sessions, sessionId).then(function () { setBusy(false); });
|
|
2802
|
+
}
|
|
2803
|
+
if (j && j.applied === "live") {
|
|
2804
|
+
if (j.prompt && j.prompt.ok === false && j.prompt.error) throw new Error(j.prompt.error);
|
|
2805
|
+
setBusy(false);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
return settleSession(sessions, sessionId).then(function (refreshed) {
|
|
2809
|
+
setBusy(false);
|
|
2810
|
+
if (!refreshed && j && j.needReload && j.hint) {
|
|
2811
|
+
try { window.alert(j.hint); } catch (_) {}
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
}).catch(function (e) {
|
|
2815
|
+
setBusy(false);
|
|
2816
|
+
try { window.alert("操作失败:" + String(e && e.message || e)); } catch (_) {}
|
|
2817
|
+
});
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
return h("button", {
|
|
2822
|
+
type: "button",
|
|
2823
|
+
className: "dsw-sc-action",
|
|
2824
|
+
title: "重试:收起这条回复并立刻重新生成",
|
|
2825
|
+
"aria-label": "重试",
|
|
2826
|
+
disabled: busy,
|
|
2827
|
+
"data-busy": busy || undefined,
|
|
2828
|
+
onClick: function () {
|
|
2829
|
+
run("/session/regenerate", "确定重试?");
|
|
2830
|
+
}
|
|
2831
|
+
}, iconRetry());
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
function ComposerPauseFuse(props) {
|
|
2835
|
+
var enabled = useScEnabled();
|
|
2836
|
+
var busyState = React.useState(false);
|
|
2837
|
+
var busy = busyState[0], setBusy = busyState[1];
|
|
2838
|
+
var sessionId = props.sessionId;
|
|
2839
|
+
if (!enabled || !sessionId) return null;
|
|
2840
|
+
|
|
2841
|
+
return h("button", {
|
|
2842
|
+
type: "button",
|
|
2843
|
+
className: "dsw-sc-pause",
|
|
2844
|
+
title: "暂停 / 熔断:随时取消当前回合(含思考中,不依赖生成态)",
|
|
2845
|
+
"aria-label": "暂停",
|
|
2846
|
+
disabled: busy,
|
|
2847
|
+
onClick: function () {
|
|
2848
|
+
if (busy) return;
|
|
2849
|
+
setBusy(true);
|
|
2850
|
+
scPost("/session/cancel", { sessionId: sessionId, reason: "手动熔断(暂停)" }).then(function (j) {
|
|
2851
|
+
setBusy(false);
|
|
2852
|
+
if (j && j.ok === false) {
|
|
2853
|
+
try { window.alert(j.error || j.message || "熔断失败"); } catch (_) {}
|
|
2854
|
+
}
|
|
2855
|
+
}).catch(function (e) {
|
|
2856
|
+
setBusy(false);
|
|
2857
|
+
try { window.alert("熔断失败:" + String(e && e.message || e)); } catch (_) {}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
}, iconStop());
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function apply(ctx) {
|
|
2864
|
+
ctx.effect(function () {
|
|
2865
|
+
return ctx.slots.inject("settings.section", function () {
|
|
2866
|
+
return ctx.slots.register({
|
|
2867
|
+
name: "settings.section",
|
|
2868
|
+
id: "dsh-workbench",
|
|
2869
|
+
order: 60,
|
|
2870
|
+
label: function () { return "工作组件"; }
|
|
2871
|
+
}, SettingsPage);
|
|
2872
|
+
});
|
|
2873
|
+
}, "dsh-workbench: settings page");
|
|
2874
|
+
|
|
2875
|
+
// Running toolview: pending/sending only. Settled gallery → conversation.chat.turnTail.
|
|
2876
|
+
ctx.slots.inject("tool.call.toolview", function () {
|
|
2877
|
+
var offSend = ctx.slots.register({ name: "tool.call.toolview", key: "mm_send_image" }, MmToolRow);
|
|
2878
|
+
return function () { if (offSend) offSend(); };
|
|
2879
|
+
});
|
|
2880
|
+
|
|
2881
|
+
try {
|
|
2882
|
+
ctx.uiConversation.events.register(mmCardsDefinition);
|
|
2883
|
+
} catch (_) { /* uiConversation unavailable */ }
|
|
2884
|
+
|
|
2885
|
+
ctx.effect(function () {
|
|
2886
|
+
return ctx.slots.inject("conversation.chat.turnTail", function () {
|
|
2887
|
+
return ctx.slots.register({
|
|
2888
|
+
name: "conversation.chat.turnTail",
|
|
2889
|
+
id: "dsh-workbench-mm-cards",
|
|
2890
|
+
order: 25,
|
|
2891
|
+
inject: function (sessionId) {
|
|
2892
|
+
var ui = null;
|
|
2893
|
+
try { ui = ctx.uiConversation; } catch (_) { ui = null; }
|
|
2894
|
+
if (!ui || typeof ui.imageUrl !== "function") return {};
|
|
2895
|
+
return {
|
|
2896
|
+
loadImage: Object.assign(
|
|
2897
|
+
function (attachment) { return ui.imageUrl(sessionId, attachment); },
|
|
2898
|
+
{
|
|
2899
|
+
peek: typeof ui.peekImageUrl === "function"
|
|
2900
|
+
? function (attachment) { return ui.peekImageUrl(sessionId, attachment); }
|
|
2901
|
+
: function () { return undefined; }
|
|
2902
|
+
}
|
|
2903
|
+
)
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
}, MmTurnTail);
|
|
2907
|
+
});
|
|
2908
|
+
}, "dsh-workbench: mm cards turnTail");
|
|
2909
|
+
|
|
2910
|
+
ctx.effect(function () {
|
|
2911
|
+
var styleEl = null;
|
|
2912
|
+
if (typeof document !== "undefined" && !document.querySelector('style[data-plugin-css="' + SC_CSS_TAG + '"]')) {
|
|
2913
|
+
styleEl = document.createElement("style");
|
|
2914
|
+
styleEl.dataset.plugin = "dsh-workbench";
|
|
2915
|
+
styleEl.dataset.pluginCss = SC_CSS_TAG;
|
|
2916
|
+
styleEl.textContent = SC_CSS;
|
|
2917
|
+
document.head.appendChild(styleEl);
|
|
2918
|
+
}
|
|
2919
|
+
refreshScEnabled();
|
|
2920
|
+
var poll = window.setInterval(refreshScEnabled, 15000);
|
|
2921
|
+
return function () {
|
|
2922
|
+
window.clearInterval(poll);
|
|
2923
|
+
if (styleEl) styleEl.remove();
|
|
2924
|
+
};
|
|
2925
|
+
}, "dsh-workbench: session-controls styles");
|
|
2926
|
+
|
|
2927
|
+
var sessions = null;
|
|
2928
|
+
try { sessions = ctx.get("sessions"); } catch (_) { sessions = null; }
|
|
2929
|
+
|
|
2930
|
+
ctx.effect(function () {
|
|
2931
|
+
return ctx.slots.inject("conversation.chat.assistant-actions", function () {
|
|
2932
|
+
return ctx.slots.register({
|
|
2933
|
+
name: "conversation.chat.assistant-actions",
|
|
2934
|
+
id: "dsh-workbench-session-actions",
|
|
2935
|
+
order: 20,
|
|
2936
|
+
inject: function () { return { _sessions: sessions }; }
|
|
2937
|
+
}, AssistantSessionActions);
|
|
2938
|
+
});
|
|
2939
|
+
}, "dsh-workbench: assistant retract+retry");
|
|
2940
|
+
|
|
2941
|
+
ctx.effect(function () {
|
|
2942
|
+
return ctx.slots.inject("conversation.input.right", function () {
|
|
2943
|
+
return ctx.slots.register({
|
|
2944
|
+
name: "conversation.input.right",
|
|
2945
|
+
id: "dsh-workbench-user-retract",
|
|
2946
|
+
order: 6,
|
|
2947
|
+
inject: function () { return { _sessions: sessions }; }
|
|
2948
|
+
}, UserRetractHost);
|
|
2949
|
+
});
|
|
2950
|
+
}, "dsh-workbench: user-message retract");
|
|
2951
|
+
|
|
2952
|
+
ctx.effect(function () {
|
|
2953
|
+
return ctx.slots.inject("conversation.input.right", function () {
|
|
2954
|
+
return ctx.slots.register({
|
|
2955
|
+
name: "conversation.input.right",
|
|
2956
|
+
id: "dsh-workbench-replaced-output",
|
|
2957
|
+
order: 7,
|
|
2958
|
+
inject: function () { return { _sessions: sessions }; }
|
|
2959
|
+
}, ReplacedOutputHost);
|
|
2960
|
+
});
|
|
2961
|
+
}, "dsh-workbench: replaced output");
|
|
2962
|
+
|
|
2963
|
+
ctx.effect(function () {
|
|
2964
|
+
return ctx.slots.inject("conversation.input.right", function () {
|
|
2965
|
+
return ctx.slots.register({
|
|
2966
|
+
name: "conversation.input.right",
|
|
2967
|
+
id: "dsh-workbench-pause-fuse",
|
|
2968
|
+
order: 5
|
|
2969
|
+
}, ComposerPauseFuse);
|
|
2970
|
+
});
|
|
2971
|
+
}, "dsh-workbench: composer pause=fuse");
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
exports.inject = inject;
|
|
2975
|
+
exports.apply = apply;
|
|
2976
|
+
exports.name = "dsh-workbench-client";
|
|
2977
|
+
return module.exports;
|
|
2978
|
+
}
|
|
2979
|
+
});
|