dsh-paoding 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -32
- package/README_en.md +27 -32
- package/bin/dsh-paoding.mjs +73 -11
- package/docs/architecture.md +41 -34
- package/docs/architecture_en.md +65 -42
- package/docs/configuration.md +159 -65
- package/docs/configuration_en.md +161 -66
- package/docs/images/1.png +0 -0
- package/docs/installation.md +100 -237
- package/docs/installation_en.md +112 -252
- package/docs/orchestration.md +34 -38
- package/docs/orchestration_en.md +34 -38
- package/package.json +8 -5
- package/plugins/paoding-config-ui/api-core.mjs +215 -18
- package/plugins/paoding-config-ui/index.mjs +414 -154
- package/plugins/paoding-config-ui/lib/base.css +1083 -42
- package/plugins/paoding-config-ui/lib/client.js +2266 -252
- package/plugins/paoding-config-ui/package.json +2 -2
- package/plugins/paoding-config-ui/runtime-inventory.mjs +27 -26
- package/presets/orchestrator/agent.cordis.yml +19 -9
- package/presets/orchestrator/preset.yml +3 -2
- package/presets/orchestrator/restrict.mjs +2 -1
- package/tools/install.mjs +9 -4
- package/tools/lib/alloc.mjs +60 -29
- package/tools/lib/cli.mjs +127 -278
- package/tools/lib/compose.mjs +261 -95
- package/tools/lib/config.mjs +266 -86
- package/tools/lib/host.mjs +55 -24
- package/tools/lib/skills.mjs +72 -45
- package/tools/lib/state.mjs +172 -40
- package/tools/lib/upgrade.mjs +170 -0
- package/tools/lib/util.mjs +7 -0
- package/tools/lib/version.mjs +233 -0
- package/tools/lib/workspaces.mjs +74 -0
- package/tools/lib/yaml.mjs +92 -15
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6.png +0 -0
- package/install.sh +0 -31
- package/tools/lib/wizard.mjs +0 -516
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* - factory 内 require() 只解析平台 seed 模块(react / cordis /
|
|
11
11
|
* ui-slots / ui-primitives 与已物化模块);本 bundle 依赖 react 与
|
|
12
12
|
* @deepseek-ai/dsh-client-ui-primitives。
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* - 挂载点:左侧栏底部动作位入口(sidebar.footer.action 键控槽,与插件
|
|
14
|
+
* 广场 cordis 徽章同槽,设置行上方)+ 会话中栏全页 overlay
|
|
15
|
+
* (data-dsh-paoding-view / html[data-dsh-paoding-active]);
|
|
16
|
+
* 与 mnemon / taskboard / ssh 全页面板互斥(dsh-panel-activate 事件 +
|
|
17
|
+
* html active 属性观察),点侧栏会话行等上下文自动关闭。
|
|
16
18
|
* - UI:按 DSH 设置壳原生观感重绘(dsh-better-sidebar 的 SideCardSection
|
|
17
19
|
* 配方)——分组容器卡 + 设计令牌(--dsw-alias-*,运行时注入,浅/深色
|
|
18
20
|
* 主题经 body[data-ds-dark-theme] 自动跟随)。样式正文独立在 lib/base.css,
|
|
@@ -71,7 +73,9 @@ window.__ModuleLoader__.load({
|
|
|
71
73
|
h("span", { className: "omd-statusText" }, text));
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
// projBadge(可选):项目级技能角标文案(如「项目」),仅技能项在
|
|
77
|
+
// 工作区视角下、该技能来自工作区项目根时传入;其余网格不传不渲染。
|
|
78
|
+
function toolItem(tool, tag, checked, disabled, onChange, key, projBadge) {
|
|
75
79
|
return h("label", { key: key, className: "omd-toolItem", title: tool },
|
|
76
80
|
h("input", {
|
|
77
81
|
type: "checkbox",
|
|
@@ -82,7 +86,8 @@ window.__ModuleLoader__.load({
|
|
|
82
86
|
onChange: onChange,
|
|
83
87
|
}),
|
|
84
88
|
h("span", { className: "omd-toolName" }, tool),
|
|
85
|
-
tag ? h("span", { className: tag === "
|
|
89
|
+
tag ? h("span", { className: "omd-toolTag" + (tag === "mcp" || tag === "plugin" ? " omd-toolTagHost" : "") }, tag) : null,
|
|
90
|
+
projBadge ? h("span", { className: "omd-skillProj" }, projBadge) : null);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
var DEFAULT_ROLES = ["search_external", "design", "implement"];
|
|
@@ -107,77 +112,235 @@ window.__ModuleLoader__.load({
|
|
|
107
112
|
return roleStringRef(baseRoles, n, "name");
|
|
108
113
|
}
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
// ── 会话模式(roles.<toolName>.background_mode)─────────────────────────
|
|
116
|
+
// 定稿键名 'one-shot' | 'continuable',缺省 one-shot。与后端序列化形状
|
|
117
|
+
// 一致:config 层 normalizeBackgroundMode 恒收成这两个字面量(缺省/非法
|
|
118
|
+
// 回落 'one-shot',落盘恒写出),assignments 与回填照搬同款取值。
|
|
119
|
+
var ROLE_BG_CONTINUABLE = "continuable";
|
|
120
|
+
var ROLE_BG_ONESHOT = "one-shot";
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
122
|
+
// 角色条目 background_mode 透传归一:恰好等于 'continuable' 才认,其余
|
|
123
|
+
//(缺省 / null / 手编误值)一律 'one-shot'(与 normalizeBackgroundMode 同口径)。
|
|
124
|
+
function roleBgModeOf(baseRoles, n) {
|
|
125
|
+
var r = (baseRoles || {})[n];
|
|
126
|
+
return r && r.background_mode === ROLE_BG_CONTINUABLE ? ROLE_BG_CONTINUABLE : ROLE_BG_ONESHOT;
|
|
117
127
|
}
|
|
118
128
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// 未知名默认归 agent;新装插件一律能识别并在主/次行出现,不会漏。
|
|
127
|
-
function pluginKindOf(p) {
|
|
128
|
-
var label = ((p && (p.label || p.moduleName)) || "");
|
|
129
|
-
if (label.charAt(0) === "/" || label.indexOf("./") === 0) return "internal";
|
|
130
|
-
if (label === "paoding-config-ui") return "self";
|
|
131
|
-
if (label === "dshmarket" || label === "dsh-better-sidebar") return "other";
|
|
132
|
-
return "agent";
|
|
129
|
+
// 一组角色里是否有人配成可续(供面板顶警告横幅判定,与 roleBgModeOf 同口径)。
|
|
130
|
+
function hasContinuableRole(roles) {
|
|
131
|
+
var keys = Object.keys(roles || {});
|
|
132
|
+
for (var i = 0; i < keys.length; i++) {
|
|
133
|
+
if (roles[keys[i]] && roles[keys[i]].background_mode === ROLE_BG_CONTINUABLE) return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// UI 双保险:只展示启用且运行中的条目(与服务端过滤口径一致)
|
|
153
|
-
if (e.enabled === false) return;
|
|
154
|
-
if (e.fiberPhase != null && e.fiberPhase !== "active") return;
|
|
155
|
-
if (label.indexOf("@deepseek-ai/") === 0) return;
|
|
156
|
-
if (label.indexOf("dsh-client") !== -1) return;
|
|
157
|
-
if (label.indexOf("cordis") !== -1) return;
|
|
158
|
-
push({ label: label, tools: [], runtime: true, moduleName: label });
|
|
159
|
-
});
|
|
160
|
-
return { primary: primary, secondary: secondary };
|
|
138
|
+
// 「可续」选中时的行内成本提示(定稿文案)。
|
|
139
|
+
var ROLE_BG_CONTINUABLE_HINT =
|
|
140
|
+
"可续会话跨轮保留子代理上下文:产出不满可用 send_message 就地续修;" +
|
|
141
|
+
"无自动清理,落盘与 token 成本随轮次累积,建议仅多轮打磨类角色开启。";
|
|
142
|
+
|
|
143
|
+
// persistence 后端缺失时的警告(定稿文案;只警示不拦截,预览 / 应用照常)。
|
|
144
|
+
var ROLE_BG_PERSISTENCE_WARN =
|
|
145
|
+
"未检测到会话持久化后端(sessionPersistence)。可续模式在委派时会直接报错 " +
|
|
146
|
+
"PERSISTENCE_UNAVAILABLE:需在 ~/.dsh/cordis.patch.yml 挂 " +
|
|
147
|
+
"@deepseek-ai/dsh-session-persistence-jsonl(host 层、机器级改动,详见 docs/orchestration.md)。";
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// mcp__ 前缀判定(归「MCP 工具」组的依据,供角色工具网格分组与
|
|
151
|
+
// customCoreToolsOf 使用;只做展示分组,不影响识别/分配)。非 mcp__ 名字
|
|
152
|
+
// 再按是否在检测清单 inventory 里分进「基础工具」/「插件工具」两组。
|
|
153
|
+
function hostTool(name) {
|
|
154
|
+
return name.indexOf("mcp__") === 0;
|
|
161
155
|
}
|
|
162
156
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
157
|
+
// 自定义角色「基础工具」核心候选:自定义角色没有 SRC 基础面(blocks 里没
|
|
158
|
+
// 有它的块),从主 agent 核心 allow(state.restrictBase)派生——去掉四个
|
|
159
|
+
// 委派名(DEFAULT_ROLES 三内置角色 + 静态的 search_internal_deep,委派
|
|
160
|
+
// 工具不走工具网格)与 mcp__ 工具(归「MCP 工具」组),剩下的就是可通过
|
|
161
|
+
// UI 勾给自定义角色的核心工具面;检测清单里的插件工具(mnemon_* 等)不
|
|
162
|
+
// 在此列,由「插件工具」组单独提供。
|
|
163
|
+
function customCoreToolsOf(data) {
|
|
164
|
+
var delegations = DEFAULT_ROLES.concat(DELEGATE_STATIC_TOOLS);
|
|
165
|
+
return ((data && data.restrictBase) || []).filter(function (t) {
|
|
166
|
+
return delegations.indexOf(t) === -1 && !hostTool(t);
|
|
167
|
+
});
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
//
|
|
170
|
+
// 运行时识别不可用时的回退提示:持续状态(不是一次性事件),只在
|
|
171
|
+
// runtimeOk === false 且带原因时渲染一行小字,贴在标签行上方;字段缺失或
|
|
172
|
+
// 正常时返回 null 零占位。错误原文可能很长,只留前 200 字符(排障够用,
|
|
173
|
+
// 不撑爆版面),仅超长才补省略号。
|
|
171
174
|
function runtimeNoteOf(d) {
|
|
172
175
|
if (!d || d.runtimeOk !== false || !d.runtimeError) return null;
|
|
173
176
|
var err = String(d.runtimeError);
|
|
174
177
|
if (err.length > 200) err = err.slice(0, 200) + "…";
|
|
175
|
-
return h("div", { className: "omd-
|
|
178
|
+
return h("div", { className: "omd-runtimeNote" },
|
|
176
179
|
"运行时识别不可用,已回退文件扫描(" + err + ")");
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
// 版本号展示归一:tag 缺前缀 v 时补上(服务端原样透传 GitHub tag_name,
|
|
183
|
+
// 通常自带 v,本地版本号则来自 package.json 不带 v,显示口径统一为 vX.Y.Z)。
|
|
184
|
+
function versionTagOf(tag) {
|
|
185
|
+
var s = String(tag || "");
|
|
186
|
+
return /^v/i.test(s) ? s : "v" + s;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 版本提示(渲染在页头标题旁,由 PageShell 持有):常态版本号不进正文;
|
|
190
|
+
// updateAvailable 时这里渲染醒目提示卡:releaseUrl 新标签页打开 release 页、
|
|
191
|
+
// 「升级」按钮走 onUpgrade(一键 dsh plugin update 自升级),并保留当前版本号。
|
|
192
|
+
// latest 为 null(含检测失败 error 的情况)一律不显示新版部分——检测失败
|
|
193
|
+
// 时服务端也只回 200 + error 字段,这里整行静默。
|
|
194
|
+
// opts(可省):busy/busyOp 复用面板级互斥(任何操作进行中都禁用升级,
|
|
195
|
+
// 升级进行中也反过来禁用其他按钮),onUpgrade 为点击回调。
|
|
196
|
+
// ── 服务端 API 助手(PageShell 与 ConfigPanel 共用)──────────────────────
|
|
197
|
+
// GET(无 body)/ POST(有 body)对 /api/paoding/*,非 2xx 抛 Error。
|
|
198
|
+
// 服务端失败响应可能带排障附件(如 upgrade 的子进程输出尾部),Error 只有
|
|
199
|
+
// message 装不下它——挂到 error.output 上,调用方 catch 侧自行取舍(见
|
|
200
|
+
// PageShell doUpgrade 的尾部并入)。
|
|
201
|
+
// 总超时(models 等慢路由同限):请求挂死时按超时报错,不让按钮永久转圈。
|
|
202
|
+
var PAODING_API_TIMEOUT_MS = 60000;
|
|
203
|
+
|
|
204
|
+
function paodingApi(path, body) {
|
|
205
|
+
var opts = body === undefined ? {} : {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: { "content-type": "application/json" },
|
|
208
|
+
body: JSON.stringify(body),
|
|
209
|
+
};
|
|
210
|
+
// AbortController 超时;老环境没有该构造器就降级为无超时(功能不变)。
|
|
211
|
+
var ctrl = typeof AbortController === "function" ? new AbortController() : null;
|
|
212
|
+
var timer = null;
|
|
213
|
+
if (ctrl) {
|
|
214
|
+
opts.signal = ctrl.signal;
|
|
215
|
+
timer = setTimeout(function () { ctrl.abort(); }, PAODING_API_TIMEOUT_MS);
|
|
216
|
+
}
|
|
217
|
+
return fetch("/api/paoding/" + path, opts).then(function (r) {
|
|
218
|
+
// 先取全文再 JSON.parse:非 JSON 响应(代理错误页 / 空体)不再抛
|
|
219
|
+
// SyntaxError,回落 HTTP <status> 的可读错误。
|
|
220
|
+
return r.text().then(function (raw) {
|
|
221
|
+
var d = null;
|
|
222
|
+
try { d = raw ? JSON.parse(raw) : null; } catch (err) { d = null; }
|
|
223
|
+
if (!r.ok) {
|
|
224
|
+
var err = new Error((d && d.error) || ("HTTP " + r.status));
|
|
225
|
+
if (d && typeof d.output === "string" && d.output) err.output = d.output;
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
if (!d) throw new Error("HTTP " + r.status + "(响应不是 JSON)");
|
|
229
|
+
return d;
|
|
230
|
+
});
|
|
231
|
+
}).catch(function (err) {
|
|
232
|
+
if (ctrl && err && err.name === "AbortError") throw new Error("请求超时(" + (PAODING_API_TIMEOUT_MS / 1000) + "s 无响应)");
|
|
233
|
+
throw err;
|
|
234
|
+
}).finally(function () {
|
|
235
|
+
if (timer !== null) clearTimeout(timer);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Toast 反馈控制器(模块级单例,PageShell 的 ToastHost 订阅渲染)──────
|
|
240
|
+
// 三档:ok(成功,4s 自动消失)/ warn(校验提示,6s)/ err(错误,粘性——
|
|
241
|
+
// 不自动消失,手动 × 关;错误文本可能很长(子进程输出尾部等),限高滚动。
|
|
242
|
+
// 栈上限 4 条,超出丢最旧。id 自增;定时器只挂自动消失档。
|
|
243
|
+
function createToastCtrl() {
|
|
244
|
+
var seq = 0;
|
|
245
|
+
var items = [];
|
|
246
|
+
var listeners = [];
|
|
247
|
+
function notify() { listeners.slice().forEach(function (fn) { fn(); }); }
|
|
248
|
+
function dismiss(id) {
|
|
249
|
+
items = items.filter(function (it) { return it.id !== id; });
|
|
250
|
+
notify();
|
|
251
|
+
}
|
|
252
|
+
function push(kind, text) {
|
|
253
|
+
var it = { id: ++seq, kind: kind, text: String(text || "") };
|
|
254
|
+
items = items.concat([it]);
|
|
255
|
+
if (items.length > 4) items = items.slice(items.length - 4);
|
|
256
|
+
if (kind !== "err") {
|
|
257
|
+
it.timer = setTimeout(function () { dismiss(it.id); }, kind === "warn" ? 6000 : 4000);
|
|
258
|
+
}
|
|
259
|
+
notify();
|
|
260
|
+
return it.id;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
ok: function (t) { return push("ok", t); },
|
|
264
|
+
warn: function (t) { return push("warn", t); },
|
|
265
|
+
err: function (t) { return push("err", t); },
|
|
266
|
+
dismiss: dismiss,
|
|
267
|
+
subscribe: function (fn) {
|
|
268
|
+
listeners.push(fn);
|
|
269
|
+
return function () { listeners = listeners.filter(function (f) { return f !== fn; }); };
|
|
270
|
+
},
|
|
271
|
+
snapshot: function () { return items.slice(); },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var toast = createToastCtrl();
|
|
275
|
+
|
|
276
|
+
// 页头版本更新卡:常态版本号常驻标题旁(pd-versionTag),检测到新版本时
|
|
277
|
+
// 在标题右侧并排一张紧凑警示卡(🆙 新版本链接 + 升级按钮)。由 PageShell
|
|
278
|
+
// 渲染并持有升级动作,面板正文不再重复版本信息;当前版本号就在旁边的
|
|
279
|
+
// pd-versionTag 上,卡内不再赘述「(当前 vX)」。note / error 为升级
|
|
280
|
+
// 反馈行(成功提示 / 失败原因,含子进程输出尾部),也收在卡内。
|
|
281
|
+
function versionRowOf(v, opts) {
|
|
282
|
+
if (!v || typeof v.current !== "string" || !v.current) return null;
|
|
283
|
+
if (!(v.updateAvailable && v.latest)) return null;
|
|
284
|
+
var busy = !!opts && !!opts.busy;
|
|
285
|
+
var note = (opts && opts.note) || "";
|
|
286
|
+
var error = (opts && opts.error) || "";
|
|
287
|
+
return h("div", { className: "omd-versionRow omd-versionNew" },
|
|
288
|
+
h("span", { className: "omd-versionIcon" }, "🆙"),
|
|
289
|
+
h("span", { className: "omd-versionText" },
|
|
290
|
+
"新版本 " + versionTagOf(v.latest) + " 可用 · ",
|
|
291
|
+
// 只信 https://github.com/ 的 release 链接;其余取值一律不渲染 <a>。
|
|
292
|
+
v.releaseUrl && /^https:\/\/github\.com\//.test(v.releaseUrl)
|
|
293
|
+
? h("a", {
|
|
294
|
+
className: "omd-versionLink",
|
|
295
|
+
href: v.releaseUrl,
|
|
296
|
+
target: "_blank",
|
|
297
|
+
rel: "noreferrer",
|
|
298
|
+
}, "查看更新")
|
|
299
|
+
: null),
|
|
300
|
+
typeof (opts && opts.onUpgrade) === "function"
|
|
301
|
+
? h("button", {
|
|
302
|
+
type: "button",
|
|
303
|
+
className: "omd-upgradeBtn",
|
|
304
|
+
disabled: busy,
|
|
305
|
+
onClick: opts.onUpgrade,
|
|
306
|
+
}, busy ? "升级中…" : "升级")
|
|
307
|
+
: null,
|
|
308
|
+
busy ? h("span", { className: "omd-upgradeHint" }, "下载安装中,最长约 2 分钟") : null,
|
|
309
|
+
error
|
|
310
|
+
? h("span", { className: "omd-versionNote omd-versionNoteErr" }, error)
|
|
311
|
+
: (note ? h("span", { className: "omd-versionNote" }, note) : null));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 反馈弹窗宿主:fixed 悬浮在操作坞上方,水平居中;host 本身 pointer-events:
|
|
315
|
+
// none,只有 toast 本体接事件(不挡下方按钮)。ok/warn 自动消失,err 带 ×
|
|
316
|
+
// 手动关。aria-live 播报;err 单条 role=alert 更醒目。
|
|
317
|
+
function ToastHost() {
|
|
318
|
+
var _t = useState(function () { return toast.snapshot(); }), items = _t[0], setItems = _t[1];
|
|
319
|
+
useEffect(function () {
|
|
320
|
+
return toast.subscribe(function () { setItems(toast.snapshot()); });
|
|
321
|
+
}, []);
|
|
322
|
+
if (!items.length) return null;
|
|
323
|
+
return h("div", { className: "pd-toastHost", "aria-live": "polite" },
|
|
324
|
+
items.map(function (it) {
|
|
325
|
+
return h("div", {
|
|
326
|
+
key: it.id,
|
|
327
|
+
className: "pd-toast pd-toast" + it.kind.charAt(0).toUpperCase() + it.kind.slice(1),
|
|
328
|
+
role: it.kind === "err" ? "alert" : "status",
|
|
329
|
+
},
|
|
330
|
+
h("span", { className: "pd-toastIcon", "aria-hidden": "true" },
|
|
331
|
+
it.kind === "err" ? "⛔" : it.kind === "warn" ? "⚠️" : "✅"),
|
|
332
|
+
h("div", { className: "pd-toastText" }, it.text),
|
|
333
|
+
h("button", {
|
|
334
|
+
type: "button",
|
|
335
|
+
className: "pd-toastClose",
|
|
336
|
+
"aria-label": "关闭提示",
|
|
337
|
+
onClick: function () { toast.dismiss(it.id); },
|
|
338
|
+
}, h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×")));
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
|
|
179
342
|
// ── 角色专用模型下拉(目录来自 GET /api/paoding/models,Node 半经
|
|
180
|
-
// ctx.
|
|
343
|
+
// ctx.llm 服务(listProviders / listModels)取已注册的模型路由)──────
|
|
181
344
|
|
|
182
345
|
// 选项 value 编码:JSON.stringify([provider, model])。provider 路由与模型
|
|
183
346
|
// id 都可能含任意字符(/ : . 等),裸拼接有歧义;JSON 数组编码可逆且无
|
|
@@ -247,6 +410,216 @@ window.__ModuleLoader__.load({
|
|
|
247
410
|
};
|
|
248
411
|
}
|
|
249
412
|
|
|
413
|
+
// ── 工作区选择(配置对象)助手 ───────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
// 剥掉两个非目标字段容器键后的全局配置副本:existing 里现在还带 workspaces /
|
|
416
|
+
// profile,工作区视角回落全局时不能把它们当目标字段读。返回 null 表示无。
|
|
417
|
+
function globalConfigOf(d) {
|
|
418
|
+
var existing = d && d.existing;
|
|
419
|
+
if (!existing || typeof existing !== "object") return null;
|
|
420
|
+
var out = Object.assign({}, existing);
|
|
421
|
+
delete out.workspaces;
|
|
422
|
+
delete out.profile;
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 工作区标签条收纳个数的初值与兜底:测量未就绪(首帧)或环境不支持
|
|
427
|
+
// ResizeObserver 时按这个数拆可见区 / 隐藏区;正常路径的收纳个数由装填
|
|
428
|
+
// 算法按标签行实宽决定(见 ConfigPanel 的 tabsRowWidth / wsFitCount)。
|
|
429
|
+
var WS_TABS_FALLBACK_COUNT = 5;
|
|
430
|
+
|
|
431
|
+
// 工作区 pill 数据行:宿主注册表列表(GET /api/paoding/workspaces)为主,
|
|
432
|
+
// state.existing.workspaces 里多出的键(宿主列表不可用 / 尚未在 GUI 注册)
|
|
433
|
+
// 补在后面——「配置文件里出现过的工作区」必须永远可见、可切换。
|
|
434
|
+
function workspaceRowsOf(d, list) {
|
|
435
|
+
var rows = [];
|
|
436
|
+
var seen = {};
|
|
437
|
+
(list || []).forEach(function (it) {
|
|
438
|
+
if (!it || typeof it.path !== "string" || it.path === "" || seen[it.path]) return;
|
|
439
|
+
seen[it.path] = true;
|
|
440
|
+
rows.push({
|
|
441
|
+
path: it.path,
|
|
442
|
+
title: typeof it.title === "string" && it.title !== "" ? it.title : null,
|
|
443
|
+
sessionCount: typeof it.sessionCount === "number" ? it.sessionCount : null,
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
var wsCfg = (d && d.existing && d.existing.workspaces) || {};
|
|
447
|
+
Object.keys(wsCfg).forEach(function (p) {
|
|
448
|
+
if (seen[p]) return;
|
|
449
|
+
seen[p] = true;
|
|
450
|
+
rows.push({ path: p, title: null, sessionCount: null });
|
|
451
|
+
});
|
|
452
|
+
return rows;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// pill 文案:宿主 title 优先,缺省回落路径 basename(宿主列表不可用的降级行)。
|
|
456
|
+
function wsLabelOf(row) {
|
|
457
|
+
if (row.title) return row.title;
|
|
458
|
+
var parts = String(row.path).replace(/[\/\\]+$/, "").split(/[\/\\]/); // Windows 路径按 \ 也能取到 basename
|
|
459
|
+
return parts[parts.length - 1] || row.path;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 工作区配置状态三态(tab / 菜单项共用的小标签):
|
|
463
|
+
// project —— 专属预设已生成(orchestrator-<slug> 已落盘,项目配置生效)
|
|
464
|
+
// pending —— 配置里存了工作区条目、但预设还没生成(编辑过没应用)
|
|
465
|
+
// global —— 没有任何条目(新会话跟随全局 orchestrator 预设)
|
|
466
|
+
// 判定优先级 installed > existing 条目 > 无。existing.workspaces 是磁盘
|
|
467
|
+
// 配置原文透传,meta 的 installed 对账 installedPresets。
|
|
468
|
+
function wsTagOf(existingWs, meta, wsPath) {
|
|
469
|
+
var m = meta && meta[wsPath];
|
|
470
|
+
if (m && m.installed) return "project";
|
|
471
|
+
if (existingWs && Object.prototype.hasOwnProperty.call(existingWs, wsPath)) return "pending";
|
|
472
|
+
return "global";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 三态 → 标签文案(中文,与 tooltip 口径一致)
|
|
476
|
+
function wsTagTextOf(kind) {
|
|
477
|
+
return kind === "project" ? "项目" : kind === "pending" ? "待应用" : "全局";
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 角色卡「默认展开」判定单一来源(toggleRole / roleCard 共用):主 agent
|
|
481
|
+
// 与外部检索卡默认展开,其余默认收起;openMap 显式开关优先。
|
|
482
|
+
function defaultOpenOf(name) {
|
|
483
|
+
return name === "__main__" || name === "search_external";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 工作区 tab / 「更多」菜单项共用的 title 提示串(两处渲染同款拼法,抽
|
|
487
|
+
// 一处防漂移):路径 + 会话数 + 配置状态(项目 / 待应用 / 全局)。
|
|
488
|
+
function wsTabTipOf(row, meta, existingWs) {
|
|
489
|
+
var m = meta && meta[row.path];
|
|
490
|
+
var kind = wsTagOf(existingWs, meta, row.path);
|
|
491
|
+
return row.path +
|
|
492
|
+
(row.sessionCount != null ? " · " + row.sessionCount + " 会话" : "") +
|
|
493
|
+
(kind === "project"
|
|
494
|
+
? " · 项目配置(预设 " + (m && m.presetId) + ")"
|
|
495
|
+
: kind === "pending"
|
|
496
|
+
? " · 已保存项目配置、预设未生成(点「保存并应用」生效)"
|
|
497
|
+
: " · 跟随全局配置");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 工作区 tab 的显示顺序:按会话数降序(缺失沉底、同分保持注册表顺序),
|
|
501
|
+
// 激活项再恒置顶(紧跟「全局默认」之后)。收纳装填按这个顺序进行,激活
|
|
502
|
+
// tab 自然永远留在可见区;「更多」按钮上的 omd-tabMoreActive 兜底分支
|
|
503
|
+
// 保留(万一首帧测量未就绪)。
|
|
504
|
+
function wsDisplayOrderOf(rows, activeWs) {
|
|
505
|
+
// 会话数从多到少:常用工作区靠前,冷门的自然沉进「更多」菜单。缺失
|
|
506
|
+
//(null)按 -1 处理沉底;同分保持注册表顺序(V8 的 sort 稳定,不抖)。
|
|
507
|
+
var order = rows.slice().sort(function (a, b) {
|
|
508
|
+
var ca = a && a.sessionCount != null ? a.sessionCount : -1;
|
|
509
|
+
var cb = b && b.sessionCount != null ? b.sessionCount : -1;
|
|
510
|
+
return cb - ca;
|
|
511
|
+
});
|
|
512
|
+
if (activeWs === null) return order;
|
|
513
|
+
// 激活项恒置顶(紧跟「全局默认」):装填按此顺序进行,正在操作的 tab
|
|
514
|
+
// 永远不会被收进「更多」。
|
|
515
|
+
for (var i = 0; i < order.length; i++) {
|
|
516
|
+
if (order[i].path === activeWs && i > 0) {
|
|
517
|
+
var active = order.splice(i, 1)[0];
|
|
518
|
+
order.unshift(active);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return order;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 「添加工作区」组合框建议行:服务端候选(GET workspaces?discover=1 的
|
|
526
|
+
// items)按当前输入做子串过滤——完整路径或 basename 命中即可,不分大小写;
|
|
527
|
+
// 空输入 = 全部候选。没拉到候选(null)或无匹配时返回空数组,调用方据此
|
|
528
|
+
// 整块不渲染下拉。
|
|
529
|
+
function wsSuggestionsOf(cands, query) {
|
|
530
|
+
if (!Array.isArray(cands) || !cands.length) return [];
|
|
531
|
+
var q = String(query || "").trim().toLowerCase();
|
|
532
|
+
if (q === "") return cands.slice();
|
|
533
|
+
return cands.filter(function (c) {
|
|
534
|
+
return (typeof c.path === "string" && c.path.toLowerCase().indexOf(q) !== -1) ||
|
|
535
|
+
(typeof c.basename === "string" && c.basename.toLowerCase().indexOf(q) !== -1);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── 通用确认弹窗(confirm 型模态框)────────────────────────────────────
|
|
540
|
+
// 照 80-mount 的 createWorkspaceController 做模块级小状态器 + 单例:发起方
|
|
541
|
+
// (ConfigPanel 的删除 / 保存并应用、86 页头的升级)与渲染方(86 的
|
|
542
|
+
// ConfirmLayer)分属两棵 React 子树,中间隔着懒挂载的 createRoot,props
|
|
543
|
+
// 传不通,靠快照 + subscribe 同步。req 纯 JS 对象直存(含 onConfirm 回调),
|
|
544
|
+
// 不经 JSON 序列化。本片段拼接序在 86 之前,confirmCtrl / confirmModalOf
|
|
545
|
+
// 由 86 直接共用,不得重复声明。
|
|
546
|
+
function createConfirmController() {
|
|
547
|
+
var snapshot = { req: null };
|
|
548
|
+
var listeners = [];
|
|
549
|
+
function notify() {
|
|
550
|
+
listeners.slice().forEach(function (listener) { listener(); });
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
getSnapshot: function () { return snapshot; },
|
|
554
|
+
subscribe: function (listener) {
|
|
555
|
+
listeners.push(listener);
|
|
556
|
+
return function () {
|
|
557
|
+
var i = listeners.indexOf(listener);
|
|
558
|
+
if (i !== -1) listeners.splice(i, 1);
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
// 弹出确认框:opts = { title, body, confirmText, cancelText, danger, onConfirm }。
|
|
562
|
+
// 不在这里执行 onConfirm——确认键点击时(confirmModalOf)先 close 再调,
|
|
563
|
+
// req 先行清空,异步 onConfirm 期间弹窗已卸载,双击 / Enter 不会二触发。
|
|
564
|
+
ask: function (opts) {
|
|
565
|
+
snapshot = {
|
|
566
|
+
req: {
|
|
567
|
+
title: opts.title,
|
|
568
|
+
body: opts.body,
|
|
569
|
+
confirmText: opts.confirmText || "确认",
|
|
570
|
+
cancelText: opts.cancelText || "取消",
|
|
571
|
+
danger: !!opts.danger,
|
|
572
|
+
onConfirm: opts.onConfirm,
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
notify();
|
|
576
|
+
},
|
|
577
|
+
close: function () {
|
|
578
|
+
if (snapshot.req === null) return;
|
|
579
|
+
snapshot = { req: null };
|
|
580
|
+
notify();
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
var confirmCtrl = createConfirmController();
|
|
585
|
+
|
|
586
|
+
// 确认弹窗纯渲染函数:只依赖模块级 h / P,与控制器同址声明。结构对齐
|
|
587
|
+
// wsModalOf / roleAddModalOf(pd-modalMask/Card/Header/Title/Close/Body/
|
|
588
|
+
// Footer),正文用 pd-modalText(多行文案 pre-line),danger 时确认钮加
|
|
589
|
+
// pd-dangerBtn(红底,删除类操作专用)。close 由 ConfirmLayer 传入。
|
|
590
|
+
function confirmModalOf(req, close) {
|
|
591
|
+
if (!req) return null;
|
|
592
|
+
return h("div", { className: "pd-modalMask",
|
|
593
|
+
// 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
|
|
594
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) close(); } },
|
|
595
|
+
h("div", { className: "pd-modalCard pd-confirmCard", role: "alertdialog", "aria-modal": "true",
|
|
596
|
+
"aria-label": req.title, "aria-describedby": "pd-confirmBody" },
|
|
597
|
+
h("div", { className: "pd-modalHeader" },
|
|
598
|
+
h("h2", { className: "pd-modalTitle" }, req.title),
|
|
599
|
+
h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: close },
|
|
600
|
+
h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
601
|
+
h("div", { className: "pd-modalBody" },
|
|
602
|
+
h("div", { className: "pd-modalText", id: "pd-confirmBody" }, req.body)),
|
|
603
|
+
h("div", { className: "pd-modalFooter" },
|
|
604
|
+
// 焦点默认落在确认钮(危险操作落在取消钮):开窗即可 Enter 确认 /
|
|
605
|
+
// 取消,与原生 confirm 的键盘习惯对齐;danger 反向落焦是让破坏性
|
|
606
|
+
// 操作多一道显式点击的门槛。弹窗关闭即整体卸载,重开重新自动聚焦。
|
|
607
|
+
h(P.Button, { variant: "outline", size: "md", autoFocus: !!req.danger, onClick: close },
|
|
608
|
+
req.cancelText || "取消"),
|
|
609
|
+
h(P.Button, {
|
|
610
|
+
variant: "primary",
|
|
611
|
+
size: "md",
|
|
612
|
+
className: req.danger ? "pd-dangerBtn" : undefined,
|
|
613
|
+
autoFocus: !req.danger,
|
|
614
|
+
onClick: function () {
|
|
615
|
+
// 先 close 再执行:req 立即清空、按钮随弹窗卸载,onConfirm
|
|
616
|
+
// 异步期间的重复点击 / Enter 都不会再命中确认路径。
|
|
617
|
+
close();
|
|
618
|
+
if (typeof req.onConfirm === "function") req.onConfirm();
|
|
619
|
+
},
|
|
620
|
+
}, req.confirmText || "确认"))));
|
|
621
|
+
}
|
|
622
|
+
|
|
250
623
|
// ── 主面板 ─────────────────────────────────────────────────────────────
|
|
251
624
|
|
|
252
625
|
function ConfigPanel(props) {
|
|
@@ -256,37 +629,102 @@ window.__ModuleLoader__.load({
|
|
|
256
629
|
var _b = useState(false), busy = _b[0], setBusy = _b[1];
|
|
257
630
|
var _op = useState(null), busyOp = _op[0], setBusyOp = _op[1];
|
|
258
631
|
var _p = useState(null), preview = _p[0], setPreview = _p[1];
|
|
259
|
-
var _n = useState(""), note = _n[0], setNote = _n[1];
|
|
260
632
|
var _o = useState({}), openMap = _o[0], setOpenMap = _o[1];
|
|
261
633
|
var _m = useState({}), personaMap = _m[0], setPersonaMap = _m[1];
|
|
262
634
|
var _x = useState(false), extraOpen = _x[0], setExtraOpen = _x[1];
|
|
263
635
|
var _r = useState(0), extraRev = _r[0], setExtraRev = _r[1];
|
|
264
|
-
var
|
|
636
|
+
var _dnr = useState(0), displayNameRev = _dnr[0], setDisplayNameRev = _dnr[1]; // 主 agent 显示名 input 重挂(key 技巧)
|
|
265
637
|
var _rn = useState({}), roleNameRev = _rn[0], setRoleNameRev = _rn[1]; // 内置角色显示名 input 重挂(每角色独立 key 计数)
|
|
266
638
|
var _rm = useState({}), roleModelRev = _rm[0], setRoleModelRev = _rm[1]; // 角色专用模型/provider input 重挂(仅降级手输模式用;每角色独立 key 计数)
|
|
267
639
|
var _mg = useState(null), modelGroups = _mg[0], setModelGroups = _mg[1]; // 模型目录 groups(/api/paoding/models)
|
|
268
640
|
var _mgok = useState(false), modelGroupsOk = _mgok[0], setModelGroupsOk = _mgok[1]; // 目录可用 = 下拉模式;false = 降级双 input
|
|
269
641
|
var _mgerr = useState(""), modelGroupsErr = _mgerr[0], setModelGroupsErr = _mgerr[1]; // 目录拉取失败原因(降级 hint 一句话展示)
|
|
642
|
+
// 版本信息(/api/paoding/version;null = 未取到/检测失败,静默不显示)。
|
|
643
|
+
// 状态挂在页头 PageShell(版本号展示在标题旁),经 props 下发:
|
|
644
|
+
// setVerInfo 供 loadVersion 回写页头(版本号与更新卡都在 PageShell)。
|
|
645
|
+
var setVerInfo = props && props.setVerInfo;
|
|
646
|
+
var _ws = useState(null), activeWs = _ws[0], setActiveWs = _ws[1]; // 当前配置对象:null = 全局默认;否则工作区绝对路径
|
|
647
|
+
// 视角切换计数:switchWorkspace 每次自增,拼进全部未受控(defaultValue)
|
|
648
|
+
// 编辑器的 key——否则切 tab 不重挂,旧工作区输入框里未提交的文本会在
|
|
649
|
+
// blur 时写进新工作区的 assign(串台)。现有各 rev 只覆盖「校验失败 /
|
|
650
|
+
// 恢复默认」重挂,不覆盖切视角,故统一再加这一维。
|
|
651
|
+
var _swr = useState(0), switchRev = _swr[0], setSwitchRev = _swr[1];
|
|
652
|
+
var _wsl = useState([]), wsList = _wsl[0], setWsList = _wsl[1]; // 宿主工作区列表(GET /api/paoding/workspaces;失败降级为空数组,pill 由配置文件键兜底)
|
|
653
|
+
var _wslt = useState(false), wsListTried = _wslt[0], setWsListTried = _wslt[1]; // 列表请求尘埃落定标记(成败均置位):pendingOpenWs 消费条件用
|
|
654
|
+
var _wsao = useState(false), wsAddOpen = _wsao[0], setWsAddOpen = _wsao[1]; // 「添加工作区」弹窗开合态(旧版是 tab 条下方的内联展开行)
|
|
655
|
+
var _wsap = useState(""), wsAddPath = _wsap[0], setWsAddPath = _wsap[1];
|
|
656
|
+
var _wsae = useState(""), wsAddErr = _wsae[0], setWsAddErr = _wsae[1];
|
|
657
|
+
var _rao = useState(false), roleAddOpen = _rao[0], setRoleAddOpen = _rao[1]; // 「新建自定义 agent 角色」弹窗开合态(原原生 prompt 改模态框)
|
|
658
|
+
var _ran = useState(""), roleAddName = _ran[0], setRoleAddName = _ran[1];
|
|
659
|
+
var _radn = useState(""), roleAddDispName = _radn[0], setRoleAddDispName = _radn[1]; // 弹窗第二字段「显示名」(可选;残值语义同 roleAddName,成功提交才清)
|
|
660
|
+
var _rabm = useState("one-shot"), roleAddBgMode = _rabm[0], setRoleAddBgMode = _rabm[1]; // 弹窗第三字段「会话模式」:'one-shot' | 'continuable'(残值语义同上,成功提交才复位)
|
|
661
|
+
var _rae = useState(""), roleAddErr = _rae[0], setRoleAddErr = _rae[1];
|
|
662
|
+
// 「重命名自定义角色」弹窗三件套(原原生 prompt 改模态框):For 为
|
|
663
|
+
// null = 关闭,非空 = 待改名的旧 toolName(身份键,提交时迁移 skills 引用)。
|
|
664
|
+
var _rrf = useState(null), roleRenameFor = _rrf[0], setRoleRenameFor = _rrf[1];
|
|
665
|
+
var _rrv = useState(""), roleRenameVal = _rrv[0], setRoleRenameVal = _rrv[1]; // 弹窗输入值(开窗预填旧名)
|
|
666
|
+
var _rre = useState(""), roleRenameErr = _rre[0], setRoleRenameErr = _rre[1];
|
|
667
|
+
var roleAddDispRef = useRef(null); // 显示名 input 引用:toolName 框 Enter 的移焦落点(两字段分工见 roleAddModalOf 注释)
|
|
668
|
+
var _wsc = useState(null), wsCands = _wsc[0], setWsCands = _wsc[1]; // 服务端候选目录(?discover=1);null = 未拉到(含失败),纯手输兜底
|
|
669
|
+
var _wsso = useState(false), wsSuggestOpen = _wsso[0], setWsSuggestOpen = _wsso[1]; // 建议下拉展开态(跟随输入框焦点 / Esc)
|
|
670
|
+
var _wsmo = useState(false), wsMenuOpen = _wsmo[0], setWsMenuOpen = _wsmo[1]; // 「更多」下拉展开态(放不下收进来的那批工作区)
|
|
671
|
+
var _wssk = useState(null), wsSkills = _wssk[0], setWsSkills = _wssk[1]; // 当前工作区技能合并列表(用户级 + 该工作区项目级;null = 全局视角 / 未拉到)
|
|
672
|
+
var _wspk = useState(null), wsProjSkills = _wspk[0], setWsProjSkills = _wspk[1]; // 项目级技能子集(Set,与 wsSkills 同源同步;项目角标据此渲染)
|
|
673
|
+
var _wsse = useState(""), wsSkillsErr = _wsse[0], setWsSkillsErr = _wsse[1]; // 工作区技能拉取失败的降级一句话提示
|
|
674
|
+
var wsSkillsCacheRef = useRef({}); // 工作区技能缓存:每路径只拉一次(面板生命周期;失败也记 null 不重试)
|
|
675
|
+
var wsCandsTriedRef = useRef(false); // 候选只拉一次的面板级闸门(失败的尝试也算数,防聚焦一次请求一次)
|
|
676
|
+
var activeWsRef = useRef(activeWs); // load / rescan / preview / apply 等异步回调里读最新值,不依赖闭包里的旧 activeWs
|
|
677
|
+
activeWsRef.current = activeWs; // 渲染期同步(回调只在这些 setter 之后触发,安全)
|
|
270
678
|
var rootRef = useRef(null); // 面板根节点(重新检测后回顶用)
|
|
271
679
|
var previewRef = useRef(null); // 预览生成组根节点(条件渲染,预览成功后定位用)
|
|
680
|
+
var assignRef = useRef(assign); // doPreview 完成回调里比对请求时快照:生成期间 assign 变过则结果作废
|
|
681
|
+
assignRef.current = assign; // 渲染期同步(同 activeWsRef 的做法)
|
|
272
682
|
var _sr = useState(null), scrollReq = _sr[0], setScrollReq = _sr[1]; // 滚动请求:"preview" | "top" | null
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
var
|
|
289
|
-
|
|
683
|
+
// 工作区标签行自动收纳:量出标签行实宽后由装填算法决定直接露出几个
|
|
684
|
+
// tab(wsFitCount),放不下的收进「更多」。tabWidthsRef 缓存测量层量到
|
|
685
|
+
// 的每个 tab 自然宽度(data-ws-key → offsetWidth),跨渲染复用。
|
|
686
|
+
var tabsRowRef = useRef(null); // .omd-tabs 标签行(宽度来源)
|
|
687
|
+
var tabsMeasureRef = useRef(null); // 隐藏测量层容器
|
|
688
|
+
var tabWidthsRef = useRef({});
|
|
689
|
+
var _rw = useState(0), tabsRowWidth = _rw[0], setTabsRowWidth = _rw[1];
|
|
690
|
+
var _fc = useState(WS_TABS_FALLBACK_COUNT), wsFitCount = _fc[0], setWsFitCount = _fc[1]; // 初值与兜底 = 测量未就绪时的固定 5 个
|
|
691
|
+
|
|
692
|
+
var ready = !!(data && assign); // 面板数据就绪:宽度观察 / 装填计算只在就绪后跑
|
|
693
|
+
// 装填计算的依赖指纹:工作区集合、显示顺序、文案或安装标记任一变化都会
|
|
694
|
+
// 改变这个串,effect 据此重算收纳个数(行宽变化经 tabsRowWidth 触达)。
|
|
695
|
+
// 必须先于下方用到它的 useEffect 求值,依赖数组才能拿到当次渲染的值。
|
|
696
|
+
var wsMetaNow = (data && data.workspaceMeta) || {};
|
|
697
|
+
var existingWsKey = (data && data.existing && data.existing.workspaces) || null;
|
|
698
|
+
var wsWidthKey = "";
|
|
699
|
+
(function buildKey() {
|
|
700
|
+
var rowsFor = workspaceRowsOf(data, wsList);
|
|
701
|
+
var ord = wsDisplayOrderOf(rowsFor, activeWs);
|
|
702
|
+
for (var i = 0; i < ord.length; i++) {
|
|
703
|
+
var m = wsMetaNow[ord[i].path];
|
|
704
|
+
wsWidthKey += (i ? "|" : "") + ord[i].path + ":" + wsLabelOf(ord[i]) + ":"
|
|
705
|
+
+ (ord[i].sessionCount != null ? ord[i].sessionCount : "-") + ":"
|
|
706
|
+
+ (m && m.installed ? "2" : existingWsKey && Object.prototype.hasOwnProperty.call(existingWsKey, ord[i].path) ? "1" : "0");
|
|
707
|
+
}
|
|
708
|
+
})();
|
|
709
|
+
|
|
710
|
+
// 实现在模块级 paodingApi(PageShell 的页头升级卡也用它),这里薄包一层
|
|
711
|
+
// 保持既有调用点与 useCallback 依赖口径不变。
|
|
712
|
+
var api = useCallback(function (path, body) { return paodingApi(path, body); }, []);
|
|
713
|
+
|
|
714
|
+
// 目标配置基座选取:工作区视角(wsPath 非空)优先取该工作区已保存条目
|
|
715
|
+
// (existing.workspaces[wsPath],没应用过则回落剥掉 workspaces/profile 的
|
|
716
|
+
// 全局 existing),全局视角直接用 existing(同样剥污染键)、再回落建议值。
|
|
717
|
+
// 返回的 assignments 只由下方显式字段拼出,workspaces / profile 两个容器
|
|
718
|
+
// 键天然混不进 POST 体。
|
|
719
|
+
var buildTargetAssignments = useCallback(function (d, wsPath) {
|
|
720
|
+
var base = null;
|
|
721
|
+
if (wsPath) {
|
|
722
|
+
var wsCfg = d.existing && d.existing.workspaces && d.existing.workspaces[wsPath];
|
|
723
|
+
if (wsCfg) base = wsCfg;
|
|
724
|
+
}
|
|
725
|
+
if (!base) {
|
|
726
|
+
base = globalConfigOf(d) || d.suggested || { roles: {}, roles_remove: [], main_agent_display_name: null, main_agent_extra: [], main_agent_remove: [], main_agent_skills: [], main_agent_skills_inline: [], main_agent_persona_extra: null, skills: {} };
|
|
727
|
+
}
|
|
290
728
|
// roles_remove:被删除的内置角色名单(只含 DEFAULT_ROLES 名)。名单内的内置角色
|
|
291
729
|
// 不重建进 roles —— 删除状态跨刷新 / 重新检测保留,卡片消失,仅留在恢复区。
|
|
292
730
|
var removed = (base.roles_remove || []).filter(function (n) { return DEFAULT_ROLES.indexOf(n) !== -1; });
|
|
@@ -301,6 +739,9 @@ window.__ModuleLoader__.load({
|
|
|
301
739
|
//(null = 跟随主 agent 当前模型;provider 只配不配 model 时后端 warn 并忽略)
|
|
302
740
|
model: roleStringRef(base.roles, name, "model"),
|
|
303
741
|
provider: roleStringRef(base.roles, name, "provider"),
|
|
742
|
+
// 会话模式透传:仅 'continuable' 认可,其余归 'one-shot'(字面量,
|
|
743
|
+
// 与 config 层 normalizeBackgroundMode 的序列化形状一致)
|
|
744
|
+
background_mode: roleBgModeOf(base.roles, name),
|
|
304
745
|
tools: (base.roles && base.roles[name] && base.roles[name].tools) ||
|
|
305
746
|
(d.blocks && d.blocks[name]) || [],
|
|
306
747
|
};
|
|
@@ -313,14 +754,17 @@ window.__ModuleLoader__.load({
|
|
|
313
754
|
// 自定义角色同样携带 model / provider(null 或 string),形状与内置一致
|
|
314
755
|
model: roleStringRef(base.roles, name, "model"),
|
|
315
756
|
provider: roleStringRef(base.roles, name, "provider"),
|
|
757
|
+
// 会话模式与内置同款收法:'one-shot' = 缺省一次性,'continuable' = 可续
|
|
758
|
+
background_mode: roleBgModeOf(base.roles, name),
|
|
316
759
|
tools: base.roles[name].tools || [],
|
|
317
760
|
};
|
|
318
761
|
});
|
|
319
762
|
return {
|
|
320
763
|
roles: roles,
|
|
321
764
|
roles_remove: removed,
|
|
322
|
-
//
|
|
323
|
-
|
|
765
|
+
// 显示名键(仅 preset 显示名,persona 身份行不可改名):null / '' / 缺省
|
|
766
|
+
// 一律归一为 null(= 默认「编排模式 (Orchestrator)」,工作区预设用派生名)
|
|
767
|
+
main_agent_display_name: base.main_agent_display_name == null || base.main_agent_display_name === "" ? null : base.main_agent_display_name,
|
|
324
768
|
main_agent_extra: base.main_agent_extra || [],
|
|
325
769
|
main_agent_remove: base.main_agent_remove || [],
|
|
326
770
|
main_agent_skills: (base.main_agent_skills || []).slice(),
|
|
@@ -332,16 +776,252 @@ window.__ModuleLoader__.load({
|
|
|
332
776
|
}, []);
|
|
333
777
|
|
|
334
778
|
var load = useCallback(function () {
|
|
335
|
-
setError(null);
|
|
336
779
|
api("state").then(function (d) {
|
|
337
780
|
setData(d);
|
|
338
|
-
setAssign(
|
|
339
|
-
setNote("");
|
|
781
|
+
setAssign(buildTargetAssignments(d, activeWsRef.current));
|
|
340
782
|
}).catch(function (err) { setError(err.message); });
|
|
341
|
-
}, [api,
|
|
783
|
+
}, [api, buildTargetAssignments]);
|
|
342
784
|
|
|
343
785
|
useEffect(load, [load]);
|
|
344
786
|
|
|
787
|
+
// 工作区列表:面板加载时拉一次宿主注册表(与 state 并行)。失败降级为
|
|
788
|
+
// 空列表——workspaceRowsOf 会用 state.existing.workspaces 的键兜底出 pill,
|
|
789
|
+
// 绝不阻塞面板。
|
|
790
|
+
var loadWorkspaces = useCallback(function () {
|
|
791
|
+
api("workspaces").then(function (d) {
|
|
792
|
+
if (!d || d.ok !== true || !Array.isArray(d.items)) throw new Error("工作区列表响应格式异常");
|
|
793
|
+
setWsList(d.items);
|
|
794
|
+
setWsListTried(true);
|
|
795
|
+
}).catch(function () {
|
|
796
|
+
setWsList([]); // 静默降级:只用配置文件里已有的工作区键
|
|
797
|
+
setWsListTried(true);
|
|
798
|
+
});
|
|
799
|
+
}, [api]);
|
|
800
|
+
|
|
801
|
+
useEffect(loadWorkspaces, [loadWorkspaces]);
|
|
802
|
+
|
|
803
|
+
// 宿主菜单「庖丁配置」入口的落地:state 与工作区列表都就绪(列表成败
|
|
804
|
+
// 均算就绪,wsListTried 见 loadWorkspaces)后,把 pendingOpenWs 指着的
|
|
805
|
+
// 工作区切为激活 tab。找不到(未注册 / 列表拉失败)给 warn、留在当前
|
|
806
|
+
// tab,一次性消费不再重试。
|
|
807
|
+
useEffect(function () {
|
|
808
|
+
if (pendingOpenWs === null || !data || !wsListTried) return;
|
|
809
|
+
var target = pendingOpenWs;
|
|
810
|
+
pendingOpenWs = null;
|
|
811
|
+
var rows = workspaceRowsOf(data, wsList);
|
|
812
|
+
var hit = null;
|
|
813
|
+
for (var i = 0; i < rows.length; i++) {
|
|
814
|
+
if (rows[i].path.replace(/[\/\\]+$/, "") === target) { hit = rows[i].path; break; }
|
|
815
|
+
}
|
|
816
|
+
if (hit) switchWorkspace(hit);
|
|
817
|
+
else toast.warn("工作区 " + target + " 不在列表里,未切换 tab");
|
|
818
|
+
}, [data, wsListTried]);
|
|
819
|
+
|
|
820
|
+
// 「添加工作区」候选:输入框首次聚焦时拉一次服务端扫描
|
|
821
|
+
//(GET workspaces?discover=1),面板生命周期内缓存不重拉(含失败的
|
|
822
|
+
// 尝试——不该每次聚焦都打一遍接口)。请求错误 / 响应异常一律静默:
|
|
823
|
+
// 候选保持 null,输入框退化成现状的纯手输,不弹错不打扰。
|
|
824
|
+
function loadWsCands() {
|
|
825
|
+
if (wsCandsTriedRef.current) return;
|
|
826
|
+
wsCandsTriedRef.current = true;
|
|
827
|
+
api("workspaces?discover=1").then(function (d) {
|
|
828
|
+
if (d && d.ok === true && Array.isArray(d.items)) setWsCands(d.items);
|
|
829
|
+
}).catch(function () { /* 静默降级:纯手输 */ });
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 按工作区拉技能(GET workspaces?skills=1&path=…):切到某工作区时拉取
|
|
833
|
+
// 该目录的技能全集(用户级 + 该工作区项目级)与项目级子集。面板生命周期
|
|
834
|
+
// 内每路径只拉一次(结果存 wsSkillsCacheRef,失败也记 null 不重试)。
|
|
835
|
+
// 失败降级:wsSkills 置 null(技能网格回落 data.skills 用户级)并留一句
|
|
836
|
+
// 提示;切回全局清空两态。响应回来时若已切走(activeWsRef 对不上)只写
|
|
837
|
+
// 缓存、不再动当前视图,防串台。
|
|
838
|
+
function applyWsSkills(wsPath) {
|
|
839
|
+
if (!wsPath) {
|
|
840
|
+
setWsSkills(null);
|
|
841
|
+
setWsProjSkills(null);
|
|
842
|
+
setWsSkillsErr("");
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
var cached = wsSkillsCacheRef.current[wsPath];
|
|
846
|
+
if (cached !== undefined) {
|
|
847
|
+
setWsSkills(cached ? cached.skills : null);
|
|
848
|
+
setWsProjSkills(cached ? new Set(cached.projectSkills) : null);
|
|
849
|
+
setWsSkillsErr(cached ? "" : "该工作区技能列表拉取失败,技能网格已回退用户级技能");
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
api("workspaces?skills=1&path=" + encodeURIComponent(wsPath)).then(function (d) {
|
|
853
|
+
if (!d || d.ok !== true || !Array.isArray(d.skills)) throw new Error("技能列表响应格式异常");
|
|
854
|
+
var proj = Array.isArray(d.projectSkills) ? d.projectSkills : [];
|
|
855
|
+
wsSkillsCacheRef.current[wsPath] = { skills: d.skills, projectSkills: proj };
|
|
856
|
+
if (activeWsRef.current !== wsPath) return; // 期间已切走:只留缓存
|
|
857
|
+
setWsSkills(d.skills);
|
|
858
|
+
setWsProjSkills(new Set(proj));
|
|
859
|
+
setWsSkillsErr("");
|
|
860
|
+
}).catch(function () {
|
|
861
|
+
wsSkillsCacheRef.current[wsPath] = null;
|
|
862
|
+
if (activeWsRef.current !== wsPath) return;
|
|
863
|
+
setWsSkills(null);
|
|
864
|
+
setWsProjSkills(null);
|
|
865
|
+
setWsSkillsErr("该工作区技能列表拉取失败,技能网格已回退用户级技能");
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// 切换配置对象(全局 ↔ 工作区):按新视角重建 assign,并清掉上一视角的
|
|
870
|
+
// 预览(预览文本对应的是旧目标,留着只会误导);切到工作区时顺带
|
|
871
|
+
// 拉取该工作区的技能列表(带缓存,见 applyWsSkills)。
|
|
872
|
+
function switchWorkspace(wsPath) {
|
|
873
|
+
setActiveWs(wsPath);
|
|
874
|
+
activeWsRef.current = wsPath;
|
|
875
|
+
if (data) setAssign(buildTargetAssignments(data, wsPath));
|
|
876
|
+
setPreview(null);
|
|
877
|
+
setSwitchRev(switchRev + 1); // 未受控编辑器随 key 变化重挂:显示新目标值,blur 不再把旧视角文本写进新视角
|
|
878
|
+
applyWsSkills(wsPath);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// 添加工作区:POST /api/paoding/workspaces 注册到宿主(幂等),成功后
|
|
882
|
+
// 刷新工作区列表并切到该工作区(未应用过配置的工作区先回落全局现状,
|
|
883
|
+
// 首次「保存并应用」才生成工作区专属 preset 与 workspaces 条目)。
|
|
884
|
+
// rawPath 仅组合框建议行点击时传选中的候选路径(只认 string,按钮
|
|
885
|
+
// onClick 透传进来的事件对象等一律回落当前输入值,Enter 语义不变)。
|
|
886
|
+
function submitWorkspace(rawPath) {
|
|
887
|
+
var p = String(typeof rawPath === "string" ? rawPath : wsAddPath || "").trim();
|
|
888
|
+
if (p === "") return;
|
|
889
|
+
setBusy(true);
|
|
890
|
+
setBusyOp("wsadd");
|
|
891
|
+
api("workspaces", { path: p }).then(function (r) {
|
|
892
|
+
var ws = (r && r.workspace) || {};
|
|
893
|
+
var wsPath = typeof ws.path === "string" && ws.path !== "" ? ws.path : p;
|
|
894
|
+
setWsAddPath("");
|
|
895
|
+
setWsAddOpen(false);
|
|
896
|
+
setWsSuggestOpen(false);
|
|
897
|
+
setWsAddErr("");
|
|
898
|
+
return api("workspaces").then(function (d) {
|
|
899
|
+
if (d && d.ok === true && Array.isArray(d.items)) setWsList(d.items);
|
|
900
|
+
switchWorkspace(wsPath);
|
|
901
|
+
toast.ok((r.created === false ? "工作区已在列表里:" : "已注册工作区:") + wsPath + ";配置后点「保存并应用」生成专属预设");
|
|
902
|
+
});
|
|
903
|
+
}).catch(function (err) {
|
|
904
|
+
setWsAddErr(err.message);
|
|
905
|
+
}).finally(function () {
|
|
906
|
+
setBusy(false);
|
|
907
|
+
setBusyOp(null);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// 收起「添加工作区」弹窗(右上 × / 取消按钮 / 点遮罩空白处共用)。不清
|
|
912
|
+
// wsAddPath / wsAddErr:成功路径由 submitWorkspace 自己清理,取消后重开
|
|
913
|
+
// 保留残值——改两个字就能续填,报错文案也留着,重开即见上次失败原因。
|
|
914
|
+
function closeWsModal() {
|
|
915
|
+
setWsAddOpen(false);
|
|
916
|
+
setWsSuggestOpen(false);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 收起「新建自定义 agent 角色」弹窗(右上 × / 取消按钮 / 点遮罩空白处
|
|
920
|
+
// 共用)。同样不清 roleAddName / roleAddDispName / roleAddErr:取消重开
|
|
921
|
+
// 保留残值续填。
|
|
922
|
+
function closeRoleAddModal() {
|
|
923
|
+
setRoleAddOpen(false);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 收起「重命名自定义角色」弹窗(右上 × / 取消按钮 / 点遮罩空白处 /
|
|
927
|
+
// Esc 共用)。同样不清 roleRenameVal / roleRenameErr:取消重开保留
|
|
928
|
+
// 残值续填,重开时由 renameRole 重置(预填旧名 + 清错误行)。
|
|
929
|
+
function closeRoleRenameModal() {
|
|
930
|
+
setRoleRenameFor(null);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// 弹窗 / 「更多」菜单的 Esc 分层拦截:捕获阶段先于 PageShell 的冒泡监听
|
|
934
|
+
// 触发,stopPropagation 之后页面级关页逻辑不再收到该事件。优先级:更多
|
|
935
|
+
// 菜单 > 弹窗;弹窗里建议下拉还开着时先只收下拉,再按一次才收弹窗。
|
|
936
|
+
// 确认弹窗(confirmCtrl)不在本链里——它的 Esc 由 86 的 ConfirmLayer
|
|
937
|
+
// 自带捕获级监听处理(注册在本监听之后,本监听只在自己弹窗开着时才
|
|
938
|
+
// stopPropagation,互不踩踏)。
|
|
939
|
+
useEffect(function () {
|
|
940
|
+
var onKey = function (e) {
|
|
941
|
+
if (!e || e.key !== "Escape") return;
|
|
942
|
+
if (wsMenuOpen) { e.stopPropagation(); setWsMenuOpen(false); return; }
|
|
943
|
+
if (wsAddOpen) {
|
|
944
|
+
e.stopPropagation();
|
|
945
|
+
if (wsSuggestOpen) setWsSuggestOpen(false);
|
|
946
|
+
else setWsAddOpen(false);
|
|
947
|
+
}
|
|
948
|
+
if (roleAddOpen) { e.stopPropagation(); closeRoleAddModal(); }
|
|
949
|
+
if (roleRenameFor) { e.stopPropagation(); closeRoleRenameModal(); }
|
|
950
|
+
};
|
|
951
|
+
document.addEventListener("keydown", onKey, true);
|
|
952
|
+
return function () { document.removeEventListener("keydown", onKey, true); };
|
|
953
|
+
}, [wsAddOpen, wsSuggestOpen, wsMenuOpen, roleAddOpen, roleRenameFor]);
|
|
954
|
+
|
|
955
|
+
// 「更多」菜单的外点关闭:目标不在菜单本体、也不在「更多」按钮内就收起。
|
|
956
|
+
// 不 stopPropagation——正常点击照常生效;菜单项与「更多」按钮自身的
|
|
957
|
+
// onClick 已各自收菜单,这里只兜「点面板其它地方」这一种情况。
|
|
958
|
+
useEffect(function () {
|
|
959
|
+
if (!wsMenuOpen) return;
|
|
960
|
+
var onDocClick = function (e) {
|
|
961
|
+
var t = e && e.target;
|
|
962
|
+
if (t && typeof t.closest === "function" && t.closest(".omd-tabMenu, .omd-tabMore")) return;
|
|
963
|
+
setWsMenuOpen(false);
|
|
964
|
+
};
|
|
965
|
+
document.addEventListener("click", onDocClick, true);
|
|
966
|
+
return function () { document.removeEventListener("click", onDocClick, true); };
|
|
967
|
+
}, [wsMenuOpen]);
|
|
968
|
+
|
|
969
|
+
// 标签行宽度观察:就绪后先量一次行宽,再挂 ResizeObserver 跟随窗口缩放、
|
|
970
|
+
// 侧栏开合等布局变化。老环境没有 ResizeObserver 就止步于这一次测量,
|
|
971
|
+
// 收纳个数停在兜底值(WS_TABS_FALLBACK_COUNT),交互不缺失只是不自适应。
|
|
972
|
+
useEffect(function () {
|
|
973
|
+
if (!ready) return;
|
|
974
|
+
var row = tabsRowRef.current;
|
|
975
|
+
if (!row) return;
|
|
976
|
+
var measure = function () { setTabsRowWidth(row.clientWidth); };
|
|
977
|
+
measure();
|
|
978
|
+
if (typeof ResizeObserver !== "function") return;
|
|
979
|
+
var ro = new ResizeObserver(measure);
|
|
980
|
+
ro.observe(row);
|
|
981
|
+
return function () { ro.disconnect(); };
|
|
982
|
+
}, [ready]);
|
|
983
|
+
|
|
984
|
+
// 装填计算:先从测量层收一轮实测宽度(data-ws-key → offsetWidth),再按
|
|
985
|
+
// 「全局默认必放、工作区按显示顺序逐个尝试」做前缀和装填,算出直接露出
|
|
986
|
+
// 的 tab 个数。行宽为 0 或某行宽度还没量到(首帧)就维持现值等下一轮——
|
|
987
|
+
// 首帧 wsFitCount 仍是兜底 5,宽度到位后这里修正,可能轻微跳变,可接受。
|
|
988
|
+
useEffect(function () {
|
|
989
|
+
if (!ready) return;
|
|
990
|
+
var layer = tabsMeasureRef.current;
|
|
991
|
+
if (layer) {
|
|
992
|
+
var nodes = layer.querySelectorAll("[data-ws-key]");
|
|
993
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
994
|
+
tabWidthsRef.current[nodes[i].getAttribute("data-ws-key")] = nodes[i].offsetWidth;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
var widths = tabWidthsRef.current;
|
|
998
|
+
var globalW = widths.__global__ || 0;
|
|
999
|
+
var addW = widths.__add__ || 0;
|
|
1000
|
+
var moreW = widths.__more__ || 0;
|
|
1001
|
+
if (!globalW || !addW || !tabsRowWidth) return; // 测量未就绪:维持现值
|
|
1002
|
+
var order = wsDisplayOrderOf(workspaceRowsOf(data, wsList), activeWs);
|
|
1003
|
+
// gap 4px、行内边距 2px×2,再留 8px 安全余量抵消「更多」计数徽标的
|
|
1004
|
+
// 位数差与亚像素取整。
|
|
1005
|
+
var GAP = 4, SAFETY = 8;
|
|
1006
|
+
var avail = tabsRowWidth - 4 - SAFETY;
|
|
1007
|
+
var prefix = [0];
|
|
1008
|
+
for (var j = 0; j < order.length; j++) {
|
|
1009
|
+
var w = widths[order[j].path] || 0;
|
|
1010
|
+
if (!w) return; // 某行还没量到(首帧):等下一轮,别算出缩水值
|
|
1011
|
+
prefix.push(prefix[j] + w);
|
|
1012
|
+
}
|
|
1013
|
+
var N = order.length, k = 0;
|
|
1014
|
+
while (k < N) {
|
|
1015
|
+
var next = k + 1;
|
|
1016
|
+
// 放进 next 个工作区时整行要摆:全局默认 + 这 next 个 + 「+」,
|
|
1017
|
+
// 还有装不下的(next < N)就再为「更多」预留一位;间隙数 = 项数 - 1。
|
|
1018
|
+
var need = globalW + prefix[next] + addW + (next < N ? moreW : 0) + GAP * (1 + next + (next < N ? 1 : 0));
|
|
1019
|
+
if (need > avail) break;
|
|
1020
|
+
k = next;
|
|
1021
|
+
}
|
|
1022
|
+
setWsFitCount(k);
|
|
1023
|
+
}, [ready, tabsRowWidth, wsWidthKey]);
|
|
1024
|
+
|
|
345
1025
|
// 模型目录:与 loadState 并行拉一次(目录发现可能稍慢,属正常;失败不
|
|
346
1026
|
// 阻塞面板,只把角色专用模型降级为双 input 手输)。请求错误 / ok:false /
|
|
347
1027
|
// 响应格式异常都归入降级,原因一句话留在 modelGroupsErr。
|
|
@@ -360,6 +1040,18 @@ window.__ModuleLoader__.load({
|
|
|
360
1040
|
|
|
361
1041
|
useEffect(loadModels, [loadModels]);
|
|
362
1042
|
|
|
1043
|
+
// 版本提示:面板挂载后异步拉一次 /api/paoding/version(api 助手无 body
|
|
1044
|
+
// 即 GET,与首屏 state/models 并行、不阻塞渲染;服务端带 1 小时缓存且
|
|
1045
|
+
// 绝不 5xx)。失败静默:不设 error 状态——检测只是锦上添花,网络不通、
|
|
1046
|
+
// 接口异常都不该打扰面板;error/latest 皆空时 versionRowOf 也不渲染。
|
|
1047
|
+
var loadVersion = useCallback(function () {
|
|
1048
|
+
api("version").then(function (d) {
|
|
1049
|
+
if (d && typeof d.current === "string" && d.current && typeof setVerInfo === "function") setVerInfo(d);
|
|
1050
|
+
}).catch(function () { /* 静默:版本检测失败不影响面板 */ });
|
|
1051
|
+
}, [api]);
|
|
1052
|
+
|
|
1053
|
+
useEffect(loadVersion, [loadVersion]);
|
|
1054
|
+
|
|
363
1055
|
// 滚动请求:preview 需等条件渲染挂载后生效(effect 在 commit 后运行),top 立即生效。
|
|
364
1056
|
useEffect(function () {
|
|
365
1057
|
if (!scrollReq) return;
|
|
@@ -452,51 +1144,162 @@ window.__ModuleLoader__.load({
|
|
|
452
1144
|
setExtraRev(extraRev + 1); // 重挂 textarea,defaultValue 重新取默认文本
|
|
453
1145
|
}
|
|
454
1146
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1147
|
+
// 「新建自定义 agent 角色」三件套(原原生 prompt 实现改为模态框,
|
|
1148
|
+
// 弹窗本体见下方 roleAddModalOf):open 开弹窗,close 收起,submit
|
|
1149
|
+
// 做校验 + 创建。校验失败走弹窗内错误行,不再用原生弹窗 / toast。
|
|
1150
|
+
function openRoleAddModal() {
|
|
1151
|
+
setRoleAddErr(""); // 上次失败原因开弹窗即清;roleAddName / roleAddDispName
|
|
1152
|
+
// 故意不清——与 closeWsModal「取消保留残值」同语义,重开接着上次输入续填。
|
|
1153
|
+
setRoleAddOpen(true);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function submitRoleAdd() {
|
|
1157
|
+
var name = String(roleAddName || "").trim();
|
|
1158
|
+
// 空名静默吞掉:确认按钮在空值时本来就 disabled,这里只兜输入框直接
|
|
1159
|
+
// 按 Enter 的路径(与旧 prompt 实现的 if (!name) return 同语义)。
|
|
1160
|
+
if (name === "") return;
|
|
1161
|
+
if (!/^[a-z][a-z0-9_-]{1,31}$/.test(name)) { setRoleAddErr("角色名须为 2-32 位小写字母/数字/下划线/连字符"); return; }
|
|
1162
|
+
// 内置名不可经「新建」再造:已删除(不在 roles、在 roles_remove)的
|
|
1163
|
+
// 内置名在此拦截并引导走恢复区,避免同一内置名同时出现在卡片流与
|
|
1164
|
+
// 「已删除的内置角色」恢复区;仍正常存在的内置名由下方「已存在」命中。
|
|
1165
|
+
if (DEFAULT_ROLES.indexOf(name) !== -1) { setRoleAddErr('内置角色不可新建("' + name + '"如已删除,请在下方「已删除的内置角色」区恢复)'); return; }
|
|
1166
|
+
// 已存在检查从 setAssign updater 挪到外层:报错要走弹窗内错误行,
|
|
1167
|
+
// setState 副作用不能塞进 updater;闭包 assign 与点击时刻一致,双击
|
|
1168
|
+
// 极端场景下重复创建也只是同形状覆盖,幂等无害。
|
|
1169
|
+
if (assign.roles[name]) { setRoleAddErr('角色 "' + name + '" 已存在'); return; }
|
|
1170
|
+
// 显示名校验与 roleTitleBlock 的 commitRoleName 同源(不含 \r \n、
|
|
1171
|
+
// trim 后 ≤60、空 = null):新建时就把显示名写进出厂形状,省得建完
|
|
1172
|
+
// 再去卡片里补一枪。报错走弹窗内错误行(不 toast)——此刻弹窗还开着,
|
|
1173
|
+
// 错误行就贴在输入框下面,比 toast 更贴输入现场。
|
|
1174
|
+
var dispRaw = String(roleAddDispName == null ? "" : roleAddDispName);
|
|
1175
|
+
if (/[\r\n]/.test(dispRaw)) { setRoleAddErr("显示名不能包含换行(\\r / \\n)"); return; }
|
|
1176
|
+
var dispTrim = dispRaw.trim();
|
|
1177
|
+
if (dispTrim.length > 60) { setRoleAddErr("显示名过长:trim 后最多 60 字符"); return; }
|
|
1178
|
+
var disp = dispTrim === "" ? null : dispTrim; // 空 / 纯空白 = null:事件流标题回落 toolName
|
|
464
1179
|
setAssign(function (prev) {
|
|
465
|
-
if (prev.roles[name]) { setNote('角色 "' + name + '" 已存在'); return prev; }
|
|
466
1180
|
var next = JSON.parse(JSON.stringify(prev));
|
|
467
1181
|
// 新角色全字段出厂形状:与 buildAssignments / restoreBuiltinRole 条目一致
|
|
468
|
-
//(model/provider 置 null = 跟随主 agent
|
|
469
|
-
|
|
1182
|
+
//(model/provider 置 null = 跟随主 agent 当前模型;background_mode
|
|
1183
|
+
// 用弹窗单选值,字面量 'one-shot' | 'continuable')。name 是可选
|
|
1184
|
+
// 显示名(null = 用 toolName 当标题),不再恒 null。
|
|
1185
|
+
next.roles[name] = {
|
|
1186
|
+
persona: null,
|
|
1187
|
+
name: disp,
|
|
1188
|
+
model: null,
|
|
1189
|
+
provider: null,
|
|
1190
|
+
background_mode: roleAddBgMode,
|
|
1191
|
+
// 出厂 tools 默认全选 = 核心工具面(customCoreToolsOf:restrictBase
|
|
1192
|
+
// 去四个委派名与 mcp__ 工具)+ host 检测库存。两集合不相交,无需去重
|
|
1193
|
+
//(render 侧 roleCandidateTools 拼候选时自带去重兜底)。
|
|
1194
|
+
tools: customCoreToolsOf(data).concat((data && data.inventory) || []),
|
|
1195
|
+
};
|
|
470
1196
|
return next;
|
|
471
1197
|
});
|
|
1198
|
+
setRoleAddOpen(false);
|
|
1199
|
+
setRoleAddName("");
|
|
1200
|
+
setRoleAddDispName("");
|
|
1201
|
+
setRoleAddBgMode("one-shot"); // 成功提交即复位出厂默认(取消重开才保留残值)
|
|
1202
|
+
setRoleAddErr("");
|
|
1203
|
+
toast.ok('已新建角色 "' + name + '",记得「保存并应用」');
|
|
472
1204
|
}
|
|
473
1205
|
|
|
474
|
-
|
|
475
|
-
|
|
1206
|
+
// 自定义角色改名(toolName = 委派调用名):名称是身份键,改它要联动
|
|
1207
|
+
// 迁移 skills 里的角色引用(skills.<技能> = [角色名…])。roles 键按原
|
|
1208
|
+
// 顺序重建(旧名位置原位替换);校验与新建同源(2-32 位小写字母/数字/
|
|
1209
|
+
// 下划线/连字符),内置名 / 已存在的名字一律拦。只改当前视角的
|
|
1210
|
+
// assignments——工作区视角改的是该工作区条目,别的视角不动(与编辑
|
|
1211
|
+
// 语义一致)。应用后生成侧换 delegation-<新名> 委派块,旧调用名消失,
|
|
1212
|
+
// 需重启 / 新会话生效。
|
|
1213
|
+
// 原原生 prompt 实现改为模态框三件套(弹窗本体见 72 片段的
|
|
1214
|
+
// roleRenameModalOf):open 开窗预填旧名,close 收起,submit 走校验链。
|
|
1215
|
+
// 校验失败走弹窗内错误行,不再 toast。
|
|
1216
|
+
function renameRole(name) {
|
|
1217
|
+
setRoleRenameVal(name); // 预填当前名:多数场景只改一两个字
|
|
1218
|
+
setRoleRenameErr("");
|
|
1219
|
+
setRoleRenameFor(name);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function submitRoleRename() {
|
|
1223
|
+
var name = roleRenameFor; // 旧名(身份键)
|
|
1224
|
+
var v = String(roleRenameVal || "").trim();
|
|
1225
|
+
// 未改动:原 prompt 下按 Enter 即关窗(只是没改名),这里对齐——直接收起。
|
|
1226
|
+
if (v === name) { closeRoleRenameModal(); return; }
|
|
1227
|
+
if (!/^[a-z][a-z0-9_-]{1,31}$/.test(v)) { setRoleRenameErr("角色名须为 2-32 位小写字母/数字/下划线/连字符"); return; }
|
|
1228
|
+
if (DEFAULT_ROLES.indexOf(v) !== -1) { setRoleRenameErr('不可改成内置角色名 "' + v + '"'); return; }
|
|
1229
|
+
// 已存在检查从 setAssign updater 挪到外层(与 submitRoleAdd 同款):
|
|
1230
|
+
// 报错要走弹窗内错误行,setState 副作用不能塞进 updater;闭包 assign
|
|
1231
|
+
// 与点击时刻一致。
|
|
1232
|
+
if (assign.roles[v]) { setRoleRenameErr('角色 "' + v + '" 已存在'); return; }
|
|
476
1233
|
setAssign(function (prev) {
|
|
477
1234
|
var next = JSON.parse(JSON.stringify(prev));
|
|
478
|
-
|
|
479
|
-
Object.keys(next.
|
|
480
|
-
|
|
1235
|
+
var roles = {};
|
|
1236
|
+
Object.keys(next.roles).forEach(function (k) { roles[k === name ? v : k] = next.roles[k]; });
|
|
1237
|
+
next.roles = roles;
|
|
1238
|
+
Object.keys(next.skills || {}).forEach(function (sk) {
|
|
1239
|
+
var arr = next.skills[sk];
|
|
1240
|
+
if (Array.isArray(arr) && arr.indexOf(name) !== -1) {
|
|
1241
|
+
next.skills[sk] = arr.map(function (r) { return r === name ? v : r; });
|
|
1242
|
+
}
|
|
481
1243
|
});
|
|
482
1244
|
return next;
|
|
483
1245
|
});
|
|
1246
|
+
// 卡片开合状态跟着换键(旧键遗留无碍,顺手清干净)
|
|
1247
|
+
setOpenMap(function (m) {
|
|
1248
|
+
if (!(name in m)) return m;
|
|
1249
|
+
var c = Object.assign({}, m);
|
|
1250
|
+
c[v] = c[name];
|
|
1251
|
+
delete c[name];
|
|
1252
|
+
return c;
|
|
1253
|
+
});
|
|
1254
|
+
closeRoleRenameModal();
|
|
1255
|
+
setRoleRenameVal("");
|
|
1256
|
+
setRoleRenameErr("");
|
|
1257
|
+
toast.ok('已重命名 "' + name + '" → "' + v + '",记得「保存并应用」');
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function removeRole(name) {
|
|
1261
|
+
// 原原生 confirm 改通用确认弹窗:删除属破坏性操作,danger 红底确认钮。
|
|
1262
|
+
confirmCtrl.ask({
|
|
1263
|
+
title: "删除自定义角色",
|
|
1264
|
+
body: "删除自定义角色 \"" + name + "\"?",
|
|
1265
|
+
danger: true,
|
|
1266
|
+
confirmText: "确认删除",
|
|
1267
|
+
onConfirm: function () {
|
|
1268
|
+
setAssign(function (prev) {
|
|
1269
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
1270
|
+
delete next.roles[name];
|
|
1271
|
+
Object.keys(next.skills).forEach(function (s) {
|
|
1272
|
+
next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
|
|
1273
|
+
});
|
|
1274
|
+
return next;
|
|
1275
|
+
});
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
484
1278
|
}
|
|
485
1279
|
|
|
486
1280
|
// 内置角色删除:roles 删 key + skills 引用剔除 + roles_remove 记名(去重)。
|
|
487
1281
|
// 后端语义:roles_remove 里的内置名 = 已删除(卡片消失、生成不再委派),
|
|
488
1282
|
// 可经 restoreBuiltinRole 恢复;自定义角色删除语义不变(只删 roles key,不进 roles_remove)。
|
|
489
1283
|
function removeBuiltinRole(name) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1284
|
+
// 原原生 confirm 改通用确认弹窗;文案含恢复引导(多行由
|
|
1285
|
+
// pd-modalText 的 pre-line 保留换行),danger 红底确认钮。
|
|
1286
|
+
confirmCtrl.ask({
|
|
1287
|
+
title: "删除内置角色",
|
|
1288
|
+
body: "删除内置角色 \"" + name + "\"?\n该角色的委派工具、工具面、persona 与主 agent 的委派指引将一并移除;\n可在下方「已删除的内置角色」区一键恢复。",
|
|
1289
|
+
danger: true,
|
|
1290
|
+
confirmText: "确认删除",
|
|
1291
|
+
onConfirm: function () {
|
|
1292
|
+
setAssign(function (prev) {
|
|
1293
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
1294
|
+
delete next.roles[name];
|
|
1295
|
+
Object.keys(next.skills).forEach(function (s) {
|
|
1296
|
+
next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
|
|
1297
|
+
});
|
|
1298
|
+
next.roles_remove = next.roles_remove || [];
|
|
1299
|
+
if (next.roles_remove.indexOf(name) === -1) next.roles_remove.push(name);
|
|
1300
|
+
return next;
|
|
1301
|
+
});
|
|
1302
|
+
},
|
|
500
1303
|
});
|
|
501
1304
|
}
|
|
502
1305
|
|
|
@@ -504,57 +1307,98 @@ window.__ModuleLoader__.load({
|
|
|
504
1307
|
// (data.blocks[name]),不沿用删前自定义配置——恢复即出厂默认,简化处理。
|
|
505
1308
|
// 显示名同样置 null(出厂默认),重建条目与 buildAssignments 形状一致。
|
|
506
1309
|
// 专用模型 / provider 同理置 null:恢复 = 回到「跟随主 agent 当前模型」,
|
|
507
|
-
// 不保留删前的模型固定配置。
|
|
1310
|
+
// 不保留删前的模型固定配置。background_mode 置 'one-shot'(出厂缺省)。
|
|
508
1311
|
function restoreBuiltinRole(name) {
|
|
509
1312
|
setAssign(function (prev) {
|
|
510
1313
|
var next = JSON.parse(JSON.stringify(prev));
|
|
511
1314
|
next.roles_remove = (next.roles_remove || []).filter(function (r) { return r !== name; });
|
|
512
1315
|
var defTools = (data && data.blocks && data.blocks[name]) || [];
|
|
513
|
-
next.roles[name] = { persona: null, name: null, model: null, provider: null, tools: defTools.slice() };
|
|
1316
|
+
next.roles[name] = { persona: null, name: null, model: null, provider: null, background_mode: "one-shot", tools: defTools.slice() };
|
|
514
1317
|
return next;
|
|
515
1318
|
});
|
|
516
1319
|
}
|
|
517
1320
|
|
|
518
1321
|
function doPreview() {
|
|
519
1322
|
if (!assign) return;
|
|
1323
|
+
var reqAssign = assign;
|
|
520
1324
|
setBusy(true);
|
|
521
1325
|
setBusyOp("preview");
|
|
522
1326
|
setPreview(null);
|
|
523
|
-
|
|
1327
|
+
// workspace 透传当前配置对象(null = 全局默认),服务端据此选生成落点。
|
|
1328
|
+
api("preview", { assignments: reqAssign, workspace: activeWsRef.current }).then(function (r) {
|
|
1329
|
+
if (assignRef.current !== reqAssign) return; // 生成期间配置已改:结果过期,不挂旧预览
|
|
524
1330
|
setPreview(r);
|
|
525
1331
|
setScrollReq("preview"); // 预览成功后自动滚动定位到预览生成区
|
|
526
|
-
|
|
527
|
-
}).catch(function (err) { setError(err.message); }).finally(function () {
|
|
1332
|
+
}).catch(function (err) { toast.err(err.message); }).finally(function () {
|
|
528
1333
|
setBusy(false);
|
|
529
1334
|
setBusyOp(null);
|
|
530
1335
|
});
|
|
531
1336
|
}
|
|
532
1337
|
|
|
1338
|
+
// 预览时效:assign 一变即作废已挂出的预览——预览文本对应生成时刻的目标
|
|
1339
|
+
// 配置,继续挂着「✅ 生成成功」只会误导(switchWorkspace 自带
|
|
1340
|
+
// setPreview(null) 只覆盖切视角,这里兜住其余一切编辑路径;生成中的
|
|
1341
|
+
// 请求由 doPreview 里的 assignRef 快照比对作废)。
|
|
1342
|
+
useEffect(function () {
|
|
1343
|
+
setPreview(null);
|
|
1344
|
+
}, [assign]);
|
|
1345
|
+
|
|
533
1346
|
function doApply() {
|
|
534
1347
|
if (!assign) return;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
1348
|
+
var wsPath = activeWsRef.current;
|
|
1349
|
+
// 确认文案按配置对象分流(语义原样保留):全局覆盖共享的 orchestrator
|
|
1350
|
+
// 预设(现状文案);工作区落独立预设、只并进 workspaces 段,全局预设
|
|
1351
|
+
// 不受影响。写预设非破坏性操作,danger 不置位(主色确认钮)。
|
|
1352
|
+
var applyMsg = wsPath
|
|
1353
|
+
? "确认应用当前配置?将为该工作区生成专属预设(orchestrator-<slug>)并保存到 dsh-paoding.config.yml 的 workspaces 段,全局 orchestrator 预设不受影响。"
|
|
1354
|
+
: "确认应用当前配置?将写入 preset 并覆盖 ~/.dsh/.agent-presets/orchestrator,且保存到 dsh-paoding.config.yml。";
|
|
1355
|
+
confirmCtrl.ask({
|
|
1356
|
+
title: "保存并应用",
|
|
1357
|
+
body: applyMsg,
|
|
1358
|
+
danger: false,
|
|
1359
|
+
confirmText: "确认应用",
|
|
1360
|
+
onConfirm: function () {
|
|
1361
|
+
setBusy(true);
|
|
1362
|
+
setBusyOp("apply");
|
|
1363
|
+
api("apply", { assignments: assign, workspace: wsPath }).then(function (r) {
|
|
1364
|
+
var notes = (r.roleResults || []).map(function (x) {
|
|
1365
|
+
return x.role + ": " + x.intent.length + " → " + x.kept.length;
|
|
1366
|
+
});
|
|
1367
|
+
var summary = notes.join(";") + "。";
|
|
1368
|
+
if (wsPath) {
|
|
1369
|
+
// 工作区应用:点明生成的 preset id,提示去预设选择器选用(工作区
|
|
1370
|
+
// 预设不会自动成为默认,须在新建会话时显式选择)。
|
|
1371
|
+
toast.ok("已生成预设 " + (r.presetId || "orchestrator-<slug>") + ":" + summary + "新建会话时在预设选择器选它。" + (r.staleNote ? " ⚠️ patch 较新,重新应用后生效。" : ""));
|
|
1372
|
+
// 服务端 apply 后已重刷状态缓存:静默拉一次,让 workspaceMeta /
|
|
1373
|
+
// existing.workspaces 跟上(全局路径保持旧交互,不额外拉取)。
|
|
1374
|
+
api("state").then(function (d) {
|
|
1375
|
+
setData(d);
|
|
1376
|
+
setAssign(buildTargetAssignments(d, activeWsRef.current));
|
|
1377
|
+
}).catch(function () { /* 静默:落盘已成功,仅展示层暂旧 */ });
|
|
1378
|
+
} else {
|
|
1379
|
+
toast.ok("已安装:" + summary + (r.staleNote ? " ⚠️ patch 较新,重启后生效。" : " 重启 DSH 或新建会话后生效。"));
|
|
1380
|
+
}
|
|
1381
|
+
// 显示名 / 角色名单可能变了:事件流委派行键控注册跟着刷新(行组件
|
|
1382
|
+
// 标题读 delegateMeta,刷新即可见,无需重开会话)。
|
|
1383
|
+
refreshDelegateRows();
|
|
1384
|
+
}).catch(function (err) { toast.err(err.message); }).finally(function () {
|
|
1385
|
+
setBusy(false);
|
|
1386
|
+
setBusyOp(null);
|
|
1387
|
+
});
|
|
1388
|
+
},
|
|
546
1389
|
});
|
|
547
1390
|
}
|
|
548
1391
|
|
|
549
1392
|
function doRescan() {
|
|
550
1393
|
setBusy(true);
|
|
551
1394
|
setBusyOp("rescan");
|
|
552
|
-
setScrollReq("top"); //
|
|
1395
|
+
setScrollReq("top"); // 立即滚动回面板顶部(不等接口返回,结果经 toast 播报)
|
|
553
1396
|
api("rescan", {}).then(function (d) {
|
|
554
1397
|
setData(d);
|
|
555
|
-
setAssign(
|
|
556
|
-
|
|
557
|
-
|
|
1398
|
+
setAssign(buildTargetAssignments(d, activeWsRef.current));
|
|
1399
|
+
// 回调参数 d 即重扫后的新 state,直接报数字,补回原徽标条的信息量
|
|
1400
|
+
toast.ok("已重新检测:" + (d.inventory || []).length + " 个 MCP / 插件工具 · " + (d.skills || []).length + " 个技能");
|
|
1401
|
+
}).catch(function (err) { toast.err(err.message); }).finally(function () {
|
|
558
1402
|
setBusy(false);
|
|
559
1403
|
setBusyOp(null);
|
|
560
1404
|
});
|
|
@@ -563,7 +1407,7 @@ window.__ModuleLoader__.load({
|
|
|
563
1407
|
function toggleRole(name) {
|
|
564
1408
|
setOpenMap(function (prev) {
|
|
565
1409
|
var next = Object.assign({}, prev);
|
|
566
|
-
next[name] = !(name in prev ? prev[name] : name
|
|
1410
|
+
next[name] = !(name in prev ? prev[name] : defaultOpenOf(name));
|
|
567
1411
|
return next;
|
|
568
1412
|
});
|
|
569
1413
|
}
|
|
@@ -578,29 +1422,387 @@ window.__ModuleLoader__.load({
|
|
|
578
1422
|
|
|
579
1423
|
// ── 渲染 ───────────────────────────────────────────────────────────────
|
|
580
1424
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1425
|
+
// 配置对象标签页(tablist):「全局默认」+ 各工作区(preset 已生成的
|
|
1426
|
+
// 前缀小圆点标记,元数据来自 state.workspaceMeta)+「更多」下拉 + 末尾
|
|
1427
|
+
// 「+ 添加工作区」按钮(点击打开输入弹窗,弹窗由 wsModalOf 渲染)。露出
|
|
1428
|
+
// 几个 tab 不再是固定数:ConfigPanel 的装填算法按标签行实宽算出
|
|
1429
|
+
// wsFitCount,本函数照它拆可见 / 隐藏两段;行内另挂一个常驻隐藏测量层
|
|
1430
|
+
// (omd-tabsMeasure)供算法读每个 tab 的自然宽度,测量未就绪时
|
|
1431
|
+
// wsFitCount 停在兜底值 5(WS_TABS_FALLBACK_COUNT)。tab 点击语义与旧
|
|
1432
|
+
// pill 行一致 = switchWorkspace(逻辑不动,只换了皮)。
|
|
1433
|
+
function workspaceBarOf() {
|
|
1434
|
+
var rows = workspaceRowsOf(data, wsList);
|
|
1435
|
+
var meta = (data && data.workspaceMeta) || {};
|
|
1436
|
+
var existingWs = (data && data.existing && data.existing.workspaces) || {};
|
|
1437
|
+
// 可见 / 隐藏拆分:显示顺序里激活工作区恒排最前,它天然留在可见区;
|
|
1438
|
+
// 下标落在 wsFitCount 之后的收进「更多」菜单。
|
|
1439
|
+
var order = wsDisplayOrderOf(rows, activeWs);
|
|
1440
|
+
var visible = order.slice(0, Math.min(order.length, wsFitCount));
|
|
1441
|
+
var hidden = order.slice(visible.length);
|
|
1442
|
+
var tabs = [h("button", {
|
|
1443
|
+
key: "__global__",
|
|
1444
|
+
type: "button",
|
|
1445
|
+
role: "tab",
|
|
1446
|
+
"aria-selected": activeWs === null ? "true" : "false",
|
|
1447
|
+
className: "omd-tab" + (activeWs === null ? " omd-tabActive" : ""),
|
|
1448
|
+
title: "全局默认:写入共享的 orchestrator 预设",
|
|
1449
|
+
onClick: function () { switchWorkspace(null); },
|
|
1450
|
+
}, h("span", { className: "omd-tabText" }, "全局默认"))];
|
|
1451
|
+
tabs = tabs.concat(visible.map(function (row) {
|
|
1452
|
+
var kind = wsTagOf(existingWs, meta, row.path);
|
|
1453
|
+
var tip = wsTabTipOf(row, meta, existingWs);
|
|
1454
|
+
return h("button", {
|
|
1455
|
+
key: row.path,
|
|
1456
|
+
type: "button",
|
|
1457
|
+
role: "tab",
|
|
1458
|
+
"aria-selected": activeWs === row.path ? "true" : "false",
|
|
1459
|
+
className: "omd-tab" + (activeWs === row.path ? " omd-tabActive" : ""),
|
|
1460
|
+
title: tip,
|
|
1461
|
+
onClick: function () { switchWorkspace(row.path); },
|
|
1462
|
+
},
|
|
1463
|
+
h("span", { className: "omd-tabText" }, wsLabelOf(row)),
|
|
1464
|
+
h("span", { className: "omd-tabTag omd-tabTag" + kind.charAt(0).toUpperCase() + kind.slice(1) },
|
|
1465
|
+
wsTagTextOf(kind)),
|
|
1466
|
+
row.sessionCount != null ? h("span", { className: "omd-tabCount" }, row.sessionCount + " 会话") : null);
|
|
1467
|
+
}));
|
|
1468
|
+
// 「更多」入口:仅在确有收进来的工作区时出现。正常路径激活项按显示
|
|
1469
|
+
// 顺序排最前、必然可见,这里仍兜底判一次——首帧 wsFitCount 还是兜底
|
|
1470
|
+
// 值时激活项可能暂居 hidden,就双挂 omd-tabMoreActive + omd-tabActive
|
|
1471
|
+
// 借用激活视觉(主色 + 激活条,CSS 不另画一份)。
|
|
1472
|
+
var hiddenActive = false;
|
|
1473
|
+
for (var k = 0; k < hidden.length; k++) {
|
|
1474
|
+
if (hidden[k].path === activeWs) { hiddenActive = true; break; }
|
|
1475
|
+
}
|
|
1476
|
+
if (hidden.length > 0) {
|
|
1477
|
+
tabs.push(h("button", {
|
|
1478
|
+
key: "__more__",
|
|
1479
|
+
type: "button",
|
|
1480
|
+
className: "omd-tab omd-tabMore" + (hiddenActive ? " omd-tabMoreActive omd-tabActive" : ""),
|
|
1481
|
+
title: "其余 " + hidden.length + " 个工作区",
|
|
1482
|
+
"aria-haspopup": "listbox",
|
|
1483
|
+
"aria-expanded": wsMenuOpen ? "true" : "false",
|
|
1484
|
+
onClick: function () { setWsMenuOpen(!wsMenuOpen); },
|
|
1485
|
+
},
|
|
1486
|
+
h("span", { className: "omd-tabText" }, "更多"),
|
|
1487
|
+
h("span", { className: "omd-tabMoreCount" }, String(hidden.length))));
|
|
1488
|
+
}
|
|
1489
|
+
// 末尾「+ 添加工作区」:与 tab 同尺寸的弱化按钮(ghost),点击打开输入弹窗。
|
|
1490
|
+
tabs.push(h("button", {
|
|
1491
|
+
key: "__add__",
|
|
1492
|
+
type: "button",
|
|
1493
|
+
className: "omd-tab omd-tabAdd",
|
|
1494
|
+
title: "注册新的工作区目录",
|
|
1495
|
+
disabled: busy,
|
|
1496
|
+
onClick: function () { setWsAddOpen(true); setWsAddErr(""); },
|
|
1497
|
+
},
|
|
1498
|
+
h("span", { className: "omd-groupIcon" }, ic(P.IconPlusOutline16, 14)),
|
|
1499
|
+
h("span", { className: "omd-tabText" }, "添加工作区")));
|
|
1500
|
+
// 测量层单件:与真实 tab 同款 .omd-tab 类,自然宽度因此一致;不进
|
|
1501
|
+
// a11y 树、不可聚焦,只负责被装填算法 offsetWidth 量。
|
|
1502
|
+
function measureTabOf(key, label) {
|
|
1503
|
+
return h("button", { type: "button", "data-ws-key": key, className: "omd-tab", tabIndex: -1 },
|
|
1504
|
+
h("span", { className: "omd-tabText" }, label));
|
|
1505
|
+
}
|
|
1506
|
+
// 行包裹层 omd-tabsRow:给「更多」下拉做锚点——挂在整列容器上的话
|
|
1507
|
+
// top:100% 会落到提示行下面,视觉上和标签行脱节;锚在行包裹层上,
|
|
1508
|
+
// 下拉恰好贴着标签行的下边框展开。
|
|
1509
|
+
return h("div", { className: "omd-wsTabs" },
|
|
1510
|
+
h("div", { className: "omd-tabsRow" },
|
|
1511
|
+
h("div", { className: "omd-tabs", role: "tablist", "aria-label": "配置对象", ref: tabsRowRef }, tabs),
|
|
1512
|
+
// 宽度测量层:常驻渲染(不随 wsFitCount 变),挪出视口且不可见,只把
|
|
1513
|
+
// 各 tab 的自然宽度喂给装填算法。「更多」原型计数固定写 99 按两位数
|
|
1514
|
+
// 算宽,与装填里的 SAFETY 余量搭配。
|
|
1515
|
+
h("div", { className: "omd-tabsMeasure", ref: tabsMeasureRef, "aria-hidden": "true" },
|
|
1516
|
+
measureTabOf("__global__", "全局默认"),
|
|
1517
|
+
order.map(function (row) {
|
|
1518
|
+
var m = meta[row.path];
|
|
1519
|
+
var mk = wsTagOf(existingWs, meta, row.path);
|
|
1520
|
+
return h("button", { key: row.path, type: "button", "data-ws-key": row.path, className: "omd-tab", tabIndex: -1 },
|
|
1521
|
+
h("span", { className: "omd-tabText" }, wsLabelOf(row)),
|
|
1522
|
+
h("span", { className: "omd-tabTag omd-tabTag" + mk.charAt(0).toUpperCase() + mk.slice(1) },
|
|
1523
|
+
wsTagTextOf(mk)),
|
|
1524
|
+
row.sessionCount != null ? h("span", { className: "omd-tabCount" }, row.sessionCount + " 会话") : null);
|
|
1525
|
+
}),
|
|
1526
|
+
h("button", { type: "button", "data-ws-key": "__more__", className: "omd-tab omd-tabMore", tabIndex: -1 },
|
|
1527
|
+
h("span", { className: "omd-tabText" }, "更多"),
|
|
1528
|
+
h("span", { className: "omd-tabMoreCount" }, "99")),
|
|
1529
|
+
h("button", { type: "button", "data-ws-key": "__add__", className: "omd-tab omd-tabAdd", tabIndex: -1 },
|
|
1530
|
+
h("span", { className: "omd-groupIcon" }, ic(P.IconPlusOutline16, 14)),
|
|
1531
|
+
h("span", { className: "omd-tabText" }, "添加工作区"))),
|
|
1532
|
+
// 「更多」下拉:菜单项信息结构与 tab 同款(小圆点 / 名称 / 会话数,
|
|
1533
|
+
// title 拼法也一致),点击直接切过去。role=listbox/option 表达
|
|
1534
|
+
// 「从清单里选一个配置对象」的语义,aria-selected 标当前项。
|
|
1535
|
+
wsMenuOpen && hidden.length > 0
|
|
1536
|
+
? h("div", { className: "omd-tabMenu", role: "listbox", "aria-label": "更多工作区" },
|
|
1537
|
+
hidden.map(function (row) {
|
|
1538
|
+
var kind = wsTagOf(existingWs, meta, row.path);
|
|
1539
|
+
var tip = wsTabTipOf(row, meta, existingWs);
|
|
1540
|
+
return h("button", {
|
|
1541
|
+
key: row.path,
|
|
1542
|
+
type: "button",
|
|
1543
|
+
role: "option",
|
|
1544
|
+
"aria-selected": activeWs === row.path ? "true" : "false",
|
|
1545
|
+
className: "omd-tabMenuItem" + (activeWs === row.path ? " omd-tabMenuItemActive" : ""),
|
|
1546
|
+
title: tip,
|
|
1547
|
+
onClick: function () { setWsMenuOpen(false); switchWorkspace(row.path); },
|
|
1548
|
+
},
|
|
1549
|
+
h("span", { className: "omd-tabText" }, wsLabelOf(row)),
|
|
1550
|
+
h("span", { className: "omd-tabTag omd-tabTag" + kind.charAt(0).toUpperCase() + kind.slice(1) },
|
|
1551
|
+
wsTagTextOf(kind)),
|
|
1552
|
+
row.sessionCount != null ? h("span", { className: "omd-tabCount" }, row.sessionCount + " 会话") : null);
|
|
1553
|
+
}))
|
|
1554
|
+
: null),
|
|
1555
|
+
h("div", { className: "omd-hint" },
|
|
1556
|
+
"「全局默认」写入共享的 orchestrator 预设;选中某个工作区后,预览 / 应用会生成该工作区专属预设(orchestrator-<目录名>),两者互不影响。"));
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// 「添加工作区」弹窗:旧版是 tab 条下方的内联展开行,改为模态框输入。
|
|
1560
|
+
// 右上 × / 取消 / 点遮罩空白处 / Esc 都只收弹窗(Esc 分层见上方捕获级
|
|
1561
|
+
// useEffect);确认按钮与输入框 Enter 走 submitWorkspace——成功路径自己
|
|
1562
|
+
// 关弹窗、刷新列表并切到新工作区,这里不重复收尾。
|
|
1563
|
+
function wsModalOf() {
|
|
1564
|
+
if (!wsAddOpen) return null;
|
|
1565
|
+
// 建议行:候选按当前输入子串过滤(下拉展开才算);没拉到候选或无匹配
|
|
1566
|
+
// 时是空数组,下拉整块不渲染。(随旧内联行整体迁入弹窗)
|
|
1567
|
+
var wsSuggestRows = wsSuggestOpen ? wsSuggestionsOf(wsCands, wsAddPath) : [];
|
|
1568
|
+
return h("div", { className: "pd-modalMask",
|
|
1569
|
+
// 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
|
|
1570
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) closeWsModal(); } },
|
|
1571
|
+
h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "添加工作区" },
|
|
1572
|
+
h("div", { className: "pd-modalHeader" },
|
|
1573
|
+
h("h2", { className: "pd-modalTitle" }, "添加工作区"),
|
|
1574
|
+
h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: closeWsModal },
|
|
1575
|
+
h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
1576
|
+
h("div", { className: "pd-modalBody" },
|
|
1577
|
+
h("label", { className: "pd-modalLabel" }, "工作区目录绝对路径"),
|
|
1578
|
+
h("div", { className: "omd-wsSuggestWrap" },
|
|
1579
|
+
h("input", {
|
|
1580
|
+
type: "text",
|
|
1581
|
+
className: "omd-nameInput omd-wsAddInput",
|
|
1582
|
+
value: wsAddPath,
|
|
1583
|
+
placeholder: "输入工作区目录绝对路径,如 /Users/me/code/project-a",
|
|
1584
|
+
spellCheck: false,
|
|
1585
|
+
autoFocus: true,
|
|
1586
|
+
onFocus: function () { loadWsCands(); setWsSuggestOpen(true); },
|
|
1587
|
+
onBlur: function () { setWsSuggestOpen(false); },
|
|
1588
|
+
onChange: function (e) { setWsAddPath(e.target.value); setWsSuggestOpen(true); },
|
|
1589
|
+
// 只认 Enter;Esc 不在这里拦——捕获级监听(见上方 useEffect)
|
|
1590
|
+
// 在 document 上先截停并 stopPropagation,事件到不了 React
|
|
1591
|
+
// 合成层,这里写 Escape 分支永远不会触发。
|
|
1592
|
+
onKeyDown: function (e) {
|
|
1593
|
+
if (e.key === "Enter") { setWsSuggestOpen(false); submitWorkspace(); }
|
|
1594
|
+
},
|
|
1595
|
+
}),
|
|
1596
|
+
wsSuggestRows.length
|
|
1597
|
+
? h("div", { className: "omd-wsSuggest", role: "listbox" },
|
|
1598
|
+
wsSuggestRows.map(function (c) {
|
|
1599
|
+
return h("button", {
|
|
1600
|
+
key: c.path,
|
|
1601
|
+
type: "button",
|
|
1602
|
+
className: "omd-wsSuggestItem",
|
|
1603
|
+
title: c.path,
|
|
1604
|
+
disabled: busy,
|
|
1605
|
+
// mousedown 的默认行为会把焦点从输入框夺走,blur 先于
|
|
1606
|
+
// click 把下拉卸载、点击就落空——preventDefault 掐掉它,
|
|
1607
|
+
// input 保持聚焦,click 才能安稳到达建议行。
|
|
1608
|
+
onMouseDown: function (e) { e.preventDefault(); },
|
|
1609
|
+
onClick: function () {
|
|
1610
|
+
setWsSuggestOpen(false);
|
|
1611
|
+
setWsAddPath(c.path);
|
|
1612
|
+
submitWorkspace(c.path);
|
|
1613
|
+
},
|
|
1614
|
+
},
|
|
1615
|
+
h("span", { className: "omd-wsSuggestName" }, c.basename || c.path),
|
|
1616
|
+
h("span", { className: "omd-wsSuggestPath" }, c.path));
|
|
1617
|
+
}))
|
|
1618
|
+
: null),
|
|
1619
|
+
wsAddErr ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), wsAddErr) : null,
|
|
1620
|
+
h("div", { className: "omd-hint" }, "目录需已存在;已注册的目录重复提交幂等返回。")),
|
|
1621
|
+
h("div", { className: "pd-modalFooter" },
|
|
1622
|
+
h(P.Button, { variant: "outline", size: "md", disabled: busy, onClick: closeWsModal }, "取消"),
|
|
1623
|
+
h(P.Button, {
|
|
1624
|
+
variant: "primary",
|
|
1625
|
+
size: "md",
|
|
1626
|
+
disabled: busy || String(wsAddPath).trim() === "",
|
|
1627
|
+
onClick: submitWorkspace,
|
|
1628
|
+
}, busyOp === "wsadd" ? "添加中…" : "确认添加"))));
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// 「新建自定义 agent 角色」弹窗:结构 / 关闭路径与「添加工作区」弹窗
|
|
1632
|
+
// (wsModalOf)完全一致——右上 × / 取消 / 点遮罩空白处 / Esc 都只收弹窗
|
|
1633
|
+
//(Esc 分层见上方捕获级 useEffect)。两字段表单的 Enter 分工:toolName
|
|
1634
|
+
// 框 Enter 不提交(用户多半还想填显示名),只把焦点挪到显示名框;显示
|
|
1635
|
+
// 名框 Enter 才走 submitRoleAdd——成功路径自己关弹窗并清残值,这里不
|
|
1636
|
+
// 重复收尾。新建是纯本地 setAssign 操作,无请求,按钮不挂 busy 态。
|
|
1637
|
+
function roleAddModalOf() {
|
|
1638
|
+
if (!roleAddOpen) return null;
|
|
1639
|
+
return h("div", { className: "pd-modalMask",
|
|
1640
|
+
// 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
|
|
1641
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) closeRoleAddModal(); } },
|
|
1642
|
+
h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "新建自定义 agent 角色" },
|
|
1643
|
+
h("div", { className: "pd-modalHeader" },
|
|
1644
|
+
h("h2", { className: "pd-modalTitle" }, "新建自定义 agent 角色"),
|
|
1645
|
+
h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: closeRoleAddModal },
|
|
1646
|
+
h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
1647
|
+
h("div", { className: "pd-modalBody" },
|
|
1648
|
+
h("label", { className: "pd-modalLabel" }, "角色 toolName(主 agent 的委派调用名)"),
|
|
1649
|
+
h("input", {
|
|
1650
|
+
type: "text",
|
|
1651
|
+
className: "omd-nameInput",
|
|
1652
|
+
value: roleAddName,
|
|
1653
|
+
placeholder: "输入英文小写角色名,如 reviewer",
|
|
1654
|
+
spellCheck: false,
|
|
1655
|
+
autoFocus: true,
|
|
1656
|
+
onChange: function (e) { setRoleAddName(e.target.value); },
|
|
1657
|
+
// Enter 只移焦不提交:这是两字段表单的第一段,直接提交会跳过
|
|
1658
|
+
// 还没填的显示名。Esc 仍不在这里拦——捕获级监听(见上方
|
|
1659
|
+
// useEffect)在 document 上先截停并 stopPropagation,事件到不了
|
|
1660
|
+
// React 合成层,这里写 Escape 分支永远不会触发。
|
|
1661
|
+
onKeyDown: function (e) {
|
|
1662
|
+
if (e.key === "Enter") {
|
|
1663
|
+
var inp = roleAddDispRef.current; // 弹窗条件渲染,此刻必已挂载;仍判空兜极端时序
|
|
1664
|
+
if (inp && typeof inp.focus === "function") inp.focus();
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1667
|
+
}),
|
|
1668
|
+
h("label", { className: "pd-modalLabel" }, "显示名(可选)"),
|
|
1669
|
+
h("input", {
|
|
1670
|
+
ref: roleAddDispRef,
|
|
1671
|
+
type: "text",
|
|
1672
|
+
className: "omd-nameInput",
|
|
1673
|
+
value: roleAddDispName,
|
|
1674
|
+
// 占位语义与 roleTitleBlock 的 subject=toolName 一致:留空 = 事件流标题回落 toolName
|
|
1675
|
+
placeholder: "如 代码评审,留空 = 用 toolName",
|
|
1676
|
+
spellCheck: false,
|
|
1677
|
+
onChange: function (e) { setRoleAddDispName(e.target.value); },
|
|
1678
|
+
// 第二段的 Enter 才是提交:第一段已把焦点送到这里,用户在此按
|
|
1679
|
+
// Enter 即视为整个表单填完。
|
|
1680
|
+
onKeyDown: function (e) {
|
|
1681
|
+
if (e.key === "Enter") submitRoleAdd();
|
|
1682
|
+
},
|
|
1683
|
+
}),
|
|
1684
|
+
// 「会话模式」单选(与角色卡 roleBgModeBlock 同款语义):缺省一次性,
|
|
1685
|
+
// 选「可续」才写 background_mode: 'continuable';提示 / 警告行内联。
|
|
1686
|
+
h("label", { className: "pd-modalLabel" }, "会话模式"),
|
|
1687
|
+
h("div", { className: "omd-nameRow" },
|
|
1688
|
+
h("label", { className: "omd-toolItem" },
|
|
1689
|
+
h("input", {
|
|
1690
|
+
type: "radio",
|
|
1691
|
+
className: "omd-check",
|
|
1692
|
+
name: "pd-bgm-roleadd",
|
|
1693
|
+
checked: roleAddBgMode !== "continuable",
|
|
1694
|
+
onChange: function () { setRoleAddBgMode("one-shot"); },
|
|
1695
|
+
}),
|
|
1696
|
+
h("span", { className: "omd-toolName" }, "一次性(one-shot)")),
|
|
1697
|
+
h("label", { className: "omd-toolItem" },
|
|
1698
|
+
h("input", {
|
|
1699
|
+
type: "radio",
|
|
1700
|
+
className: "omd-check",
|
|
1701
|
+
name: "pd-bgm-roleadd",
|
|
1702
|
+
checked: roleAddBgMode === "continuable",
|
|
1703
|
+
onChange: function () { setRoleAddBgMode("continuable"); },
|
|
1704
|
+
}),
|
|
1705
|
+
h("span", { className: "omd-toolName" }, "可续(continuable)"))),
|
|
1706
|
+
roleAddBgMode === "continuable"
|
|
1707
|
+
? h("div", { className: "omd-hint" }, ROLE_BG_CONTINUABLE_HINT)
|
|
1708
|
+
: null,
|
|
1709
|
+
roleAddBgMode === "continuable" && data && data.persistenceAvailable === false
|
|
1710
|
+
? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), ROLE_BG_PERSISTENCE_WARN)
|
|
1711
|
+
: null,
|
|
1712
|
+
roleAddErr ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), roleAddErr) : null,
|
|
1713
|
+
h("div", { className: "omd-hint" },
|
|
1714
|
+
"角色名须为 2-32 位小写字母/数字/下划线/连字符,小写字母开头;内置名不可新建。显示名可选,不能换行,trim 后最多 60 字符,留空用 toolName 当标题。新角色工具面默认放开全部检测到的工具,可在卡片里再收窄。会话模式缺省一次性(one-shot),选「可续」后仍可在角色卡里改。")),
|
|
1715
|
+
h("div", { className: "pd-modalFooter" },
|
|
1716
|
+
h(P.Button, { variant: "outline", size: "md", onClick: closeRoleAddModal }, "取消"),
|
|
1717
|
+
h(P.Button, {
|
|
1718
|
+
variant: "primary",
|
|
1719
|
+
size: "md",
|
|
1720
|
+
disabled: String(roleAddName).trim() === "",
|
|
1721
|
+
onClick: submitRoleAdd,
|
|
1722
|
+
}, "确认新建"))));
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// 「重命名自定义角色」弹窗:结构 / 关闭路径与 roleAddModalOf 一致——
|
|
1726
|
+
// 右上 × / 取消 / 点遮罩空白处 / Esc 都只收弹窗(Esc 分层见 70 片段的
|
|
1727
|
+
// 捕获级 useEffect)。单字段表单:开窗预填旧名,Enter 直接提交(重命名
|
|
1728
|
+
// 没有第二段);校验失败走弹窗内错误行,成功路径由 submitRoleRename 关
|
|
1729
|
+
// 弹窗并清残值。改的是 toolName(身份键),显示名不在此处。
|
|
1730
|
+
function roleRenameModalOf() {
|
|
1731
|
+
if (!roleRenameFor) return null;
|
|
1732
|
+
return h("div", { className: "pd-modalMask",
|
|
1733
|
+
// 只点遮罩空白处才收(mousedown 真正落在遮罩上),在卡片内按下拖选不误关
|
|
1734
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) closeRoleRenameModal(); } },
|
|
1735
|
+
h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": "重命名自定义角色" },
|
|
1736
|
+
h("div", { className: "pd-modalHeader" },
|
|
1737
|
+
h("h2", { className: "pd-modalTitle" }, "重命名自定义角色"),
|
|
1738
|
+
h("button", { type: "button", className: "pd-modalClose", "aria-label": "关闭", onClick: closeRoleRenameModal },
|
|
1739
|
+
h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
1740
|
+
h("div", { className: "pd-modalBody" },
|
|
1741
|
+
h("label", { className: "pd-modalLabel" }, "新 toolName(主 agent 的委派调用名)"),
|
|
1742
|
+
h("input", {
|
|
1743
|
+
type: "text",
|
|
1744
|
+
className: "omd-nameInput",
|
|
1745
|
+
value: roleRenameVal,
|
|
1746
|
+
placeholder: roleRenameFor || "", // 占位即旧名(输入框已预填,占位只在清空后可见)
|
|
1747
|
+
spellCheck: false,
|
|
1748
|
+
autoFocus: true,
|
|
1749
|
+
onChange: function (e) { setRoleRenameVal(e.target.value); },
|
|
1750
|
+
// Esc 不在这里拦——捕获级监听(见 70 片段 useEffect)在 document
|
|
1751
|
+
// 上先截停并 stopPropagation,事件到不了 React 合成层,这里写
|
|
1752
|
+
// Escape 分支永远不会触发。
|
|
1753
|
+
onKeyDown: function (e) {
|
|
1754
|
+
if (e.key === "Enter") submitRoleRename();
|
|
1755
|
+
},
|
|
1756
|
+
}),
|
|
1757
|
+
roleRenameErr ? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), roleRenameErr) : null,
|
|
1758
|
+
h("div", { className: "omd-hint" },
|
|
1759
|
+
"2-32 位小写字母/数字/下划线/连字符,小写字母开头;不可改成内置名或已存在的名字。skills 里的引用自动迁移;改名后需重启 / 新会话生效。")),
|
|
1760
|
+
h("div", { className: "pd-modalFooter" },
|
|
1761
|
+
h(P.Button, { variant: "outline", size: "md", onClick: closeRoleRenameModal }, "取消"),
|
|
1762
|
+
h(P.Button, {
|
|
1763
|
+
variant: "primary",
|
|
1764
|
+
size: "md",
|
|
1765
|
+
// 空值 / 与旧名相同都禁用确认:后者点确认只会空转收窗,禁用
|
|
1766
|
+
// 让按钮状态与「确实改了名」对齐。
|
|
1767
|
+
disabled: String(roleRenameVal).trim() === "" || String(roleRenameVal).trim() === roleRenameFor,
|
|
1768
|
+
onClick: submitRoleRename,
|
|
1769
|
+
}, "确认重命名"))));
|
|
589
1770
|
}
|
|
590
1771
|
|
|
591
1772
|
if (!data || !assign) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
h("div", { className: "omd-loading" },
|
|
598
|
-
|
|
599
|
-
|
|
1773
|
+
// 面板没起来(含 state 加载失败)才走这条整页分支:失败渲染常驻错误行,
|
|
1774
|
+
// 不进 toast——此刻页面还没有内容可依托,错误必须一直可见。
|
|
1775
|
+
return h("div", { className: "pd-page" },
|
|
1776
|
+
error
|
|
1777
|
+
? statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), error)
|
|
1778
|
+
: h("div", { className: "omd-loading" },
|
|
1779
|
+
h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16)),
|
|
1780
|
+
h("span", null, "加载检测状态…")));
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// 技能网格数据源(主 agent 软引导/内联 + 下方角色 skills 分组共用):
|
|
1784
|
+
// 工作区视角(activeWs 非空)且已拉到 wsSkills 时用「用户级 + 该工作区
|
|
1785
|
+
// 项目级」合并列表;全局视角 / 拉取中 / 拉取失败降级一律回落 state 的
|
|
1786
|
+
// 用户级技能 data.skills。gridProj 仅在合并生效时非空(项目角标据此渲染)。
|
|
1787
|
+
var gridSkills = activeWs !== null && Array.isArray(wsSkills) ? wsSkills : (data.skills || []);
|
|
1788
|
+
var gridProj = activeWs !== null && wsProjSkills ? wsProjSkills : null;
|
|
1789
|
+
|
|
1790
|
+
// 技能网格作用域小字(分组标题旁):说明当前网格覆盖的技能来源——
|
|
1791
|
+
// 全局 = 仅用户级技能;工作区 = 用户级 + 该工作区项目级技能。
|
|
1792
|
+
function skillScopeNote() {
|
|
1793
|
+
return h("span", { className: "omd-skillScope" },
|
|
1794
|
+
activeWs !== null ? "用户级 + 该工作区项目级技能" : "仅用户级技能");
|
|
600
1795
|
}
|
|
601
1796
|
|
|
1797
|
+
// 角色工具候选(三组网格共用数据源):所有角色(内置 + 自定义)= SRC
|
|
1798
|
+
// allow 面 ∪ 核心工具清单 ∪ host 检测库存。内置角色在 SRC 面上再并核心
|
|
1799
|
+
// 工具候选——勾选才生效,默认勾选仍来自 suggested/staticBase,默认面不
|
|
1800
|
+
// 变;自定义角色无 SRC 基础面,自然回落核心清单(customCoreToolsOf:去
|
|
1801
|
+
// 四个委派名与 mcp__ 工具)。分组口径:mcp__ 前缀 → 「MCP 工具」;其余
|
|
1802
|
+
// 名字里在 inventory 中的 → 「插件工具」(mnemon_* 等直注册工具);两者
|
|
1803
|
+
// 皆非 → 「基础工具」。全部拼接后按首次出现去重。
|
|
602
1804
|
function roleCandidateTools(name) {
|
|
603
|
-
var base = (data.blocks && data.blocks[name]) || [];
|
|
1805
|
+
var base = ((data.blocks && data.blocks[name]) || []).concat(customCoreToolsOf(data));
|
|
604
1806
|
return base.concat(data.inventory || []).filter(function (t, i, arr) { return arr.indexOf(t) === i; });
|
|
605
1807
|
}
|
|
606
1808
|
|
|
@@ -618,10 +1820,12 @@ window.__ModuleLoader__.load({
|
|
|
618
1820
|
h("span", { className: "omd-personaEdit" }, ic(P.IconEditOutline16, 14))),
|
|
619
1821
|
open ? h("div", { className: "omd-personaBody" },
|
|
620
1822
|
h("textarea", {
|
|
1823
|
+
key: "omd-persona-" + name + "-" + switchRev, // 切视角重挂:defaultValue 显示新目标的 persona
|
|
621
1824
|
className: "omd-textarea",
|
|
622
1825
|
defaultValue: assign.roles[name].persona || "",
|
|
623
1826
|
placeholder: "输入该角色的 persona 说明…",
|
|
624
1827
|
spellCheck: false,
|
|
1828
|
+
disabled: busy,
|
|
625
1829
|
onBlur: function (e) {
|
|
626
1830
|
setAssign(function (prev) {
|
|
627
1831
|
var next = JSON.parse(JSON.stringify(prev));
|
|
@@ -652,17 +1856,19 @@ window.__ModuleLoader__.load({
|
|
|
652
1856
|
h("span", { className: "omd-personaEdit" }, ic(P.IconEditOutline16, 14))),
|
|
653
1857
|
extraOpen ? h("div", { className: "omd-personaBody" },
|
|
654
1858
|
h("textarea", {
|
|
655
|
-
key: "omd-extra-" + extraRev,
|
|
1859
|
+
key: "omd-extra-" + extraRev + "-" + switchRev,
|
|
656
1860
|
className: "omd-textarea omd-extraArea",
|
|
657
1861
|
defaultValue: val,
|
|
658
1862
|
placeholder: "留空并保存 = 不追加任何内容(默认 codegraph 规则将被移除)…",
|
|
659
1863
|
spellCheck: false,
|
|
1864
|
+
disabled: busy,
|
|
660
1865
|
onBlur: function (e) { setMainPersonaExtra(e.target.value); },
|
|
661
1866
|
}),
|
|
662
1867
|
h("div", { className: "omd-extraActions" },
|
|
663
1868
|
h("button", {
|
|
664
1869
|
type: "button",
|
|
665
1870
|
className: "omd-extraRestore",
|
|
1871
|
+
disabled: busy,
|
|
666
1872
|
onClick: restoreMainPersonaExtra,
|
|
667
1873
|
}, "恢复默认"),
|
|
668
1874
|
h("span", { className: "omd-extraHint" },
|
|
@@ -671,61 +1877,65 @@ window.__ModuleLoader__.load({
|
|
|
671
1877
|
: null);
|
|
672
1878
|
}
|
|
673
1879
|
|
|
674
|
-
// 主 agent
|
|
1880
|
+
// 主 agent「显示名」行(main_agent_display_name):仅替换 preset 显示名
|
|
1881
|
+
//(GUI 预设选择器里的名字),不动 persona 身份行——编排主 agent 的身份
|
|
1882
|
+
// 语义不属于可配置项,UI 不提供改名入口。默认文案
|
|
675
1883
|
// 「编排模式 (Orchestrator)」为后端默认常量,UI 侧硬编码做占位(后端
|
|
676
1884
|
// state 无该默认字段就不引,与 mainPersonaExtraDefault 处理思路一致)。
|
|
677
|
-
// 写回校验与后端一致:trim 后 1-60 字符、不含 \r \n
|
|
678
|
-
//
|
|
1885
|
+
// 写回校验与后端一致:trim 后 1-60 字符、不含 \r \n;空 = null(默认显示
|
|
1886
|
+
// 名,工作区预设用派生名);非空纯空白视为非法(防误触把已有显示名静默
|
|
1887
|
+
// 清回默认)。
|
|
679
1888
|
function mainNameBlock() {
|
|
680
1889
|
var DEFAULT_NAME = "编排模式 (Orchestrator)";
|
|
681
|
-
function
|
|
1890
|
+
function commitDisplayName(raw) {
|
|
682
1891
|
var s = String(raw == null ? "" : raw);
|
|
683
1892
|
if (/[\r\n]/.test(s)) {
|
|
684
|
-
|
|
685
|
-
|
|
1893
|
+
toast.warn("主 agent 显示名(仅 preset 显示名)不能包含换行(\\r / \\n),未保存");
|
|
1894
|
+
setDisplayNameRev(displayNameRev + 1); // 重挂 input,回到上次生效值
|
|
686
1895
|
return;
|
|
687
1896
|
}
|
|
688
1897
|
var v = s.trim();
|
|
689
1898
|
if (s !== "" && v === "") {
|
|
690
|
-
|
|
691
|
-
|
|
1899
|
+
toast.warn("主 agent 显示名(仅 preset 显示名)不能为纯空白(清空字段或点「恢复默认」= 默认显示名),未保存");
|
|
1900
|
+
setDisplayNameRev(displayNameRev + 1);
|
|
692
1901
|
return;
|
|
693
1902
|
}
|
|
694
1903
|
if (v.length > 60) {
|
|
695
|
-
|
|
696
|
-
|
|
1904
|
+
toast.warn("主 agent 显示名(仅 preset 显示名)过长:trim 后最多 60 字符,未保存");
|
|
1905
|
+
setDisplayNameRev(displayNameRev + 1);
|
|
697
1906
|
return;
|
|
698
1907
|
}
|
|
699
1908
|
setAssign(function (prev) {
|
|
700
1909
|
var next = JSON.parse(JSON.stringify(prev));
|
|
701
|
-
next.
|
|
1910
|
+
next.main_agent_display_name = v === "" ? null : v; // null = 默认显示名
|
|
702
1911
|
return next;
|
|
703
1912
|
});
|
|
704
|
-
if (v === "")
|
|
1913
|
+
if (v === "") setDisplayNameRev(displayNameRev + 1); // 清空 = 默认:重挂显示占位
|
|
705
1914
|
}
|
|
706
|
-
function
|
|
1915
|
+
function restoreMainDisplayName() {
|
|
707
1916
|
setAssign(function (prev) {
|
|
708
1917
|
var next = JSON.parse(JSON.stringify(prev));
|
|
709
|
-
next.
|
|
1918
|
+
next.main_agent_display_name = null; // null = 默认显示名
|
|
710
1919
|
return next;
|
|
711
1920
|
});
|
|
712
|
-
|
|
1921
|
+
setDisplayNameRev(displayNameRev + 1);
|
|
713
1922
|
}
|
|
714
1923
|
return h("div", { className: "omd-mainName" },
|
|
715
|
-
h("div", { className: "omd-glabel" }, "主 agent
|
|
1924
|
+
h("div", { className: "omd-glabel" }, "主 agent 显示名(仅 preset 显示名)"),
|
|
716
1925
|
h("div", { className: "omd-nameRow" },
|
|
717
1926
|
h("input", {
|
|
718
|
-
key: "omd-
|
|
1927
|
+
key: "omd-displayName-" + displayNameRev + "-" + switchRev,
|
|
719
1928
|
type: "text",
|
|
720
1929
|
className: "omd-nameInput",
|
|
721
|
-
defaultValue: assign.
|
|
1930
|
+
defaultValue: assign.main_agent_display_name || "",
|
|
722
1931
|
placeholder: DEFAULT_NAME,
|
|
723
1932
|
spellCheck: false,
|
|
724
|
-
|
|
1933
|
+
disabled: busy,
|
|
1934
|
+
onBlur: function (e) { commitDisplayName(e.target.value); },
|
|
725
1935
|
}),
|
|
726
|
-
h("button", { type: "button", className: "omd-extraRestore", onClick:
|
|
1936
|
+
h("button", { type: "button", className: "omd-extraRestore", disabled: busy, onClick: restoreMainDisplayName }, "恢复默认")),
|
|
727
1937
|
h("div", { className: "omd-hint" },
|
|
728
|
-
"
|
|
1938
|
+
"仅替换预设显示名,persona 身份行(You are the …)不动;留空 = 默认「编排模式 (Orchestrator)」(工作区预设用派生名)。改动需重启 GUI / 新会话生效。"));
|
|
729
1939
|
}
|
|
730
1940
|
|
|
731
1941
|
// 内置角色「显示名」input 重挂(key 技巧):写回校验失败 / 清空回默认时
|
|
@@ -751,23 +1961,25 @@ window.__ModuleLoader__.load({
|
|
|
751
1961
|
});
|
|
752
1962
|
}
|
|
753
1963
|
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
1964
|
+
// 角色「显示名」行(内置与自定义通用):
|
|
1965
|
+
// - 内置:替换默认 persona 首行身份句主语(persona 已自定义时后端
|
|
1966
|
+
// 忽略,仅提示),同时是事件流委派行的标题;
|
|
1967
|
+
// - 自定义:后端不消费此字段(其 persona 全文自写),纯粹用作事件流
|
|
1968
|
+
// 委派行标题——不动 toolName / 调用名 / persona。
|
|
1969
|
+
// 校验与主 agent 名同源约束:trim 后 1-60 字符、不含 \r \n;
|
|
1970
|
+
// 空 / 纯空白 = 默认(写入 null = 事件流回落缺省标题)。
|
|
1971
|
+
function roleTitleBlock(name, isCustom) {
|
|
1972
|
+
var subject = isCustom ? name : (ROLE_DEFAULT_SUBJECT[name] || name);
|
|
761
1973
|
function commitRoleName(raw) {
|
|
762
1974
|
var s = String(raw == null ? "" : raw);
|
|
763
1975
|
if (/[\r\n]/.test(s)) {
|
|
764
|
-
|
|
1976
|
+
toast.warn('角色 "' + name + '" 显示名不能包含换行(\\r / \\n),未保存');
|
|
765
1977
|
bumpRoleNameRev(name); // 重挂 input,回到上次生效值
|
|
766
1978
|
return;
|
|
767
1979
|
}
|
|
768
1980
|
var v = s.trim();
|
|
769
1981
|
if (v.length > 60) {
|
|
770
|
-
|
|
1982
|
+
toast.warn('角色 "' + name + '" 显示名过长:trim 后最多 60 字符,未保存');
|
|
771
1983
|
bumpRoleNameRev(name);
|
|
772
1984
|
return;
|
|
773
1985
|
}
|
|
@@ -779,19 +1991,23 @@ window.__ModuleLoader__.load({
|
|
|
779
1991
|
if (v === "") bumpRoleNameRev(name); // 空 / 纯空白 = 默认:重挂显示占位
|
|
780
1992
|
}
|
|
781
1993
|
return h("div", { className: "omd-mainName" },
|
|
782
|
-
h("div", { className: "omd-glabel" },
|
|
1994
|
+
h("div", { className: "omd-glabel" },
|
|
1995
|
+
isCustom ? "角色显示名(事件流委派行标题;不动 toolName / persona)" : "角色显示名(persona 身份句主语 + 事件流委派行标题;不动工具名/调用名)"),
|
|
783
1996
|
h("div", { className: "omd-nameRow" },
|
|
784
1997
|
h("input", {
|
|
785
|
-
key: "omd-roleName-" + name + "-" + ((roleNameRev && roleNameRev[name]) || 0),
|
|
1998
|
+
key: "omd-roleName-" + name + "-" + ((roleNameRev && roleNameRev[name]) || 0) + "-" + switchRev,
|
|
786
1999
|
type: "text",
|
|
787
2000
|
className: "omd-nameInput",
|
|
788
2001
|
defaultValue: (assign.roles[name] && assign.roles[name].name) || "",
|
|
789
2002
|
placeholder: subject,
|
|
790
2003
|
spellCheck: false,
|
|
2004
|
+
disabled: busy,
|
|
791
2005
|
onBlur: function (e) { commitRoleName(e.target.value); },
|
|
792
2006
|
})),
|
|
793
2007
|
h("div", { className: "omd-hint" },
|
|
794
|
-
|
|
2008
|
+
isCustom
|
|
2009
|
+
? '留空 = 事件流显示 toolName(' + name + ');保存并应用后生效(仅 GUI 展示,不影响生成产物与调用名)。'
|
|
2010
|
+
: '留空 = 默认身份(如 "You are the ' + subject + ' agent.");已为该角色自定义 persona 时仅作事件流标题。'));
|
|
795
2011
|
}
|
|
796
2012
|
|
|
797
2013
|
// 角色「专用模型」行(内置与自定义角色通用同一组件,签名不变):
|
|
@@ -814,13 +2030,13 @@ window.__ModuleLoader__.load({
|
|
|
814
2030
|
function commitRoleModel(key, raw) {
|
|
815
2031
|
var s = String(raw == null ? "" : raw);
|
|
816
2032
|
if (/[\r\n]/.test(s)) {
|
|
817
|
-
|
|
2033
|
+
toast.warn('角色 "' + name + '" ' + labelOf(key) + "不能包含换行(\\r / \\n),未保存");
|
|
818
2034
|
bumpRoleModelRev(name); // 重挂 input,回到上次生效值
|
|
819
2035
|
return;
|
|
820
2036
|
}
|
|
821
2037
|
var v = s.trim();
|
|
822
2038
|
if (v.length > 120) {
|
|
823
|
-
|
|
2039
|
+
toast.warn('角色 "' + name + '" ' + labelOf(key) + "过长:trim 后最多 120 字符,未保存");
|
|
824
2040
|
bumpRoleModelRev(name);
|
|
825
2041
|
return;
|
|
826
2042
|
}
|
|
@@ -836,7 +2052,7 @@ window.__ModuleLoader__.load({
|
|
|
836
2052
|
});
|
|
837
2053
|
// provider 非空而 model 为空 → 允许保存(后端原样落盘),但明确提示会被忽略
|
|
838
2054
|
if (provider && !model) {
|
|
839
|
-
|
|
2055
|
+
toast.warn('角色 "' + name + '":provider 仅在同时配置模型时生效(后端将忽略),已保存');
|
|
840
2056
|
}
|
|
841
2057
|
if (v === "") bumpRoleModelRev(name); // 清空 = 跟随主 agent:重挂显示占位
|
|
842
2058
|
}
|
|
@@ -844,21 +2060,23 @@ window.__ModuleLoader__.load({
|
|
|
844
2060
|
h("div", { className: "omd-glabel" }, "专用模型(可选;留空 = 跟随主 agent 当前模型)"),
|
|
845
2061
|
h("div", { className: "omd-nameRow" },
|
|
846
2062
|
h("input", {
|
|
847
|
-
key: "omd-roleModel-" + name + "-" + rev,
|
|
2063
|
+
key: "omd-roleModel-" + name + "-" + rev + "-" + switchRev,
|
|
848
2064
|
type: "text",
|
|
849
2065
|
className: "omd-nameInput",
|
|
850
2066
|
defaultValue: role.model || "",
|
|
851
2067
|
placeholder: "deepseek-v4-pro",
|
|
852
2068
|
spellCheck: false,
|
|
2069
|
+
disabled: busy,
|
|
853
2070
|
onBlur: function (e) { commitRoleModel("model", e.target.value); },
|
|
854
2071
|
}),
|
|
855
2072
|
h("input", {
|
|
856
|
-
key: "omd-roleProvider-" + name + "-" + rev,
|
|
2073
|
+
key: "omd-roleProvider-" + name + "-" + rev + "-" + switchRev,
|
|
857
2074
|
type: "text",
|
|
858
2075
|
className: "omd-nameInput",
|
|
859
2076
|
defaultValue: role.provider || "",
|
|
860
2077
|
placeholder: "provider(留空=跟随主 agent)",
|
|
861
2078
|
spellCheck: false,
|
|
2079
|
+
disabled: busy,
|
|
862
2080
|
onBlur: function (e) { commitRoleModel("provider", e.target.value); },
|
|
863
2081
|
})),
|
|
864
2082
|
h("div", { className: "omd-hint" },
|
|
@@ -900,6 +2118,7 @@ window.__ModuleLoader__.load({
|
|
|
900
2118
|
h("select", {
|
|
901
2119
|
key: "omd-roleModel-" + name, // 每角色独立实例;模式 / 选项集切换可整体重挂
|
|
902
2120
|
className: "omd-nameInput",
|
|
2121
|
+
disabled: busy,
|
|
903
2122
|
value: sel.value,
|
|
904
2123
|
onChange: function (e) { commitRoleModelOption(e.target.value); },
|
|
905
2124
|
}, children)),
|
|
@@ -907,11 +2126,50 @@ window.__ModuleLoader__.load({
|
|
|
907
2126
|
"选项来自 DSH 已注册的模型路由;留空 = 跟随主 agent,主 agent 换模型时未固定的角色跟着变。"));
|
|
908
2127
|
}
|
|
909
2128
|
|
|
2129
|
+
// 角色「会话模式」行(内置与自定义角色通用,同 roleModelBlock 的定位):
|
|
2130
|
+
// 单选 roles.<toolName>.background_mode ——「一次性(one-shot)」为缺省
|
|
2131
|
+
//(字面量 'one-shot',与 config 层 normalizeBackgroundMode 恒收两个合法
|
|
2132
|
+
// 值的序列化形状一致),「可续(continuable)」写 'continuable'。选中
|
|
2133
|
+
// 可续时行内展示成本提示;host 层 persistence 后端缺失
|
|
2134
|
+
//(state.persistenceAvailable === false,字段缺失的老服务端视为可用不
|
|
2135
|
+
// 误报)时再叠加委派将报错的警告——只警示,不拦截预览 / 应用。
|
|
2136
|
+
function roleBgModeBlock(name) {
|
|
2137
|
+
var continuable = !!(assign.roles[name] && assign.roles[name].background_mode === "continuable");
|
|
2138
|
+
function commitBgMode(on) {
|
|
2139
|
+
setAssign(function (prev) {
|
|
2140
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
2141
|
+
next.roles[name].background_mode = on ? "continuable" : "one-shot";
|
|
2142
|
+
return next;
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
function bgOption(key, label, checked, on) {
|
|
2146
|
+
return h("label", { key: key, className: "omd-toolItem" },
|
|
2147
|
+
h("input", {
|
|
2148
|
+
type: "radio",
|
|
2149
|
+
className: "omd-check",
|
|
2150
|
+
name: "pd-bgm-" + name, // 每角色独立单选组;name 含 toolName 不串台
|
|
2151
|
+
checked: checked,
|
|
2152
|
+
disabled: busy,
|
|
2153
|
+
onChange: function () { commitBgMode(on); },
|
|
2154
|
+
}),
|
|
2155
|
+
h("span", { className: "omd-toolName" }, label));
|
|
2156
|
+
}
|
|
2157
|
+
return h("div", { className: "omd-mainName" },
|
|
2158
|
+
h("div", { className: "omd-glabel" }, "会话模式"),
|
|
2159
|
+
h("div", { className: "omd-nameRow" },
|
|
2160
|
+
bgOption("one-shot", "一次性(one-shot)", !continuable, false),
|
|
2161
|
+
bgOption("continuable", "可续(continuable)", continuable, true)),
|
|
2162
|
+
continuable ? h("div", { className: "omd-hint" }, ROLE_BG_CONTINUABLE_HINT) : null,
|
|
2163
|
+
continuable && data.persistenceAvailable === false
|
|
2164
|
+
? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), ROLE_BG_PERSISTENCE_WARN)
|
|
2165
|
+
: null);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
910
2168
|
function roleCard(name) {
|
|
911
2169
|
var isMain = name === "__main__";
|
|
912
2170
|
var display = isMain ? "主 agent" : name;
|
|
913
2171
|
var isCustom = !isMain && DEFAULT_ROLES.indexOf(name) === -1;
|
|
914
|
-
var open = name in openMap ? openMap[name] : (
|
|
2172
|
+
var open = name in openMap ? openMap[name] : defaultOpenOf(name);
|
|
915
2173
|
|
|
916
2174
|
var d = data;
|
|
917
2175
|
var a = assign;
|
|
@@ -928,6 +2186,19 @@ window.__ModuleLoader__.load({
|
|
|
928
2186
|
h("span", { className: "omd-roleBadge" }, isMain ? "固定" : (isCustom ? "自定义" : "内置")),
|
|
929
2187
|
h("span", { className: "omd-roleCount" },
|
|
930
2188
|
(isMain ? ((data.restrictBase || []).length + a.main_agent_extra.length - (a.main_agent_remove || []).length) : a.roles[name].tools.length) + " 个工具")),
|
|
2189
|
+
// 自定义角色改名按钮:toolName 是身份键(委派调用名 / skills 引用),
|
|
2190
|
+
// 与内置角色的「显示名」不是一回事——那个只改 persona 主语。
|
|
2191
|
+
isCustom
|
|
2192
|
+
? h("button", {
|
|
2193
|
+
type: "button",
|
|
2194
|
+
className: "omd-roleEdit",
|
|
2195
|
+
title: '重命名角色 "' + name + '"(toolName = 委派调用名)',
|
|
2196
|
+
onClick: function (e) {
|
|
2197
|
+
e.stopPropagation();
|
|
2198
|
+
renameRole(name);
|
|
2199
|
+
},
|
|
2200
|
+
}, ic(P.IconEditOutline16, 16))
|
|
2201
|
+
: null,
|
|
931
2202
|
// 删除按钮:自定义角色走 removeRole(仅删 roles key);内置角色走
|
|
932
2203
|
// removeBuiltinRole(删 key + roles_remove 记名 → 卡片消失、可恢复)。
|
|
933
2204
|
// 主 agent(__main__)卡不可删。内置卡只渲染存在于 assign.roles 的,删后即隐。
|
|
@@ -950,28 +2221,44 @@ window.__ModuleLoader__.load({
|
|
|
950
2221
|
body.push(h("div", { className: "omd-glabel" }, "基础工具(取消勾选即从主 agent 移除——写入 main_agent_remove)"));
|
|
951
2222
|
body.push(h("div", { className: "omd-toolGrid" },
|
|
952
2223
|
(data.restrictBase || []).map(function (t) {
|
|
953
|
-
return toolItem(t, "base", (a.main_agent_remove || []).indexOf(t) === -1,
|
|
2224
|
+
return toolItem(t, "base", (a.main_agent_remove || []).indexOf(t) === -1, busy,
|
|
954
2225
|
function (e) { setMainRemove(t, e.target.checked); }, t);
|
|
955
2226
|
})));
|
|
956
|
-
|
|
2227
|
+
// host 检测库存按角色卡同口径拆两组:mcp__ 前缀 → 「MCP 工具」,
|
|
2228
|
+
// 其余(mnemon_* 等插件直注册工具)→ 「插件工具」。勾选逻辑不变,
|
|
2229
|
+
// 都写入 main_agent_extra。
|
|
2230
|
+
var mainInv = data.inventory || [];
|
|
2231
|
+
var mainMcpTools = mainInv.filter(function (t) { return hostTool(t); });
|
|
2232
|
+
var mainPluginTools = mainInv.filter(function (t) { return !hostTool(t); });
|
|
2233
|
+
body.push(h("div", { className: "omd-glabel" }, "MCP 工具(可加入主 agent)"));
|
|
2234
|
+
body.push(h("div", { className: "omd-toolGrid" },
|
|
2235
|
+
mainMcpTools.length
|
|
2236
|
+
? mainMcpTools.map(function (t) {
|
|
2237
|
+
return toolItem(t, "mcp", a.main_agent_extra.indexOf(t) !== -1, busy,
|
|
2238
|
+
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
2239
|
+
})
|
|
2240
|
+
: h("div", { className: "omd-empty" }, "未检测到 MCP 工具")));
|
|
2241
|
+
body.push(h("div", { className: "omd-glabel" }, "插件工具(可加入主 agent)"));
|
|
957
2242
|
body.push(h("div", { className: "omd-toolGrid" },
|
|
958
|
-
|
|
959
|
-
?
|
|
960
|
-
return toolItem(t, "
|
|
2243
|
+
mainPluginTools.length
|
|
2244
|
+
? mainPluginTools.map(function (t) {
|
|
2245
|
+
return toolItem(t, "plugin", a.main_agent_extra.indexOf(t) !== -1, busy,
|
|
961
2246
|
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
962
2247
|
})
|
|
963
|
-
: h("div", { className: "omd-empty" }, "
|
|
964
|
-
body.push(h("div", { className: "omd-glabel" }, "技能分配(read 按需:勾选 = persona 写入技能行,主 agent 用 read 加载正文)"));
|
|
965
|
-
body.push(
|
|
2248
|
+
: h("div", { className: "omd-empty" }, "未检测到插件工具")));
|
|
2249
|
+
body.push(h("div", { className: "omd-glabel" }, "技能分配(read 按需:勾选 = persona 写入技能行,主 agent 用 read 加载正文)", skillScopeNote()));
|
|
2250
|
+
body.push(gridSkills.length
|
|
966
2251
|
? h("div", null,
|
|
967
|
-
h("div", { className: "omd-toolGrid" },
|
|
968
|
-
return toolItem(s, "read按需", a.main_agent_skills.indexOf(s) !== -1,
|
|
969
|
-
function (e) { setMainSkill(s, e.target.checked); }, s + "s"
|
|
2252
|
+
h("div", { className: "omd-toolGrid" }, gridSkills.map(function (s) {
|
|
2253
|
+
return toolItem(s, "read按需", a.main_agent_skills.indexOf(s) !== -1, busy,
|
|
2254
|
+
function (e) { setMainSkill(s, e.target.checked); }, s + "s",
|
|
2255
|
+
gridProj && gridProj.has(s) ? "项目" : null);
|
|
970
2256
|
})),
|
|
971
2257
|
h("div", { className: "omd-glabel" }, "内联全文(硬生效,每轮固定 persona 开销,适合 caveman 等风格技能)"),
|
|
972
|
-
h("div", { className: "omd-toolGrid" },
|
|
973
|
-
return toolItem(s, "内联全文", a.main_agent_skills_inline.indexOf(s) !== -1,
|
|
974
|
-
function (e) { setMainSkillInline(s, e.target.checked); }, s + "i"
|
|
2258
|
+
h("div", { className: "omd-toolGrid" }, gridSkills.map(function (s) {
|
|
2259
|
+
return toolItem(s, "内联全文", a.main_agent_skills_inline.indexOf(s) !== -1, busy,
|
|
2260
|
+
function (e) { setMainSkillInline(s, e.target.checked); }, s + "i",
|
|
2261
|
+
gridProj && gridProj.has(s) ? "项目" : null);
|
|
975
2262
|
})))
|
|
976
2263
|
: h("div", { className: "omd-empty" }, "未检测到技能目录"));
|
|
977
2264
|
body.push(mainPersonaExtraBlock());
|
|
@@ -981,27 +2268,46 @@ window.__ModuleLoader__.load({
|
|
|
981
2268
|
// 通用同一 roleModelBlock——下拉模式成对写 provider + model(不存在
|
|
982
2269
|
// 「只配 provider」);目录不可用降级双 input 时才可能出现该状态(后端
|
|
983
2270
|
// warn 并忽略)。留空 = 跟随主 agent 当前模型。
|
|
984
|
-
|
|
2271
|
+
// 「会话模式」单选紧随模型行(内置 / 自定义通用):缺省一次性,
|
|
2272
|
+
// 选可续时行内给成本提示 + persistence 缺失警告(如有)。
|
|
2273
|
+
body.push(roleTitleBlock(name, isCustom));
|
|
985
2274
|
body.push(roleModelBlock(name));
|
|
986
|
-
|
|
987
|
-
|
|
2275
|
+
body.push(roleBgModeBlock(name));
|
|
2276
|
+
// 三组网格(同 roleCandidateTools 注释的分组口径):「基础工具」=
|
|
2277
|
+
// 非 mcp__ 且不在检测库存(blocks ∪ customCoreToolsOf 减 inventory);
|
|
2278
|
+
// 「MCP 工具」= mcp__ 前缀(含 SRC 自带行,如 tavily);「插件工具」
|
|
2279
|
+
// = 检测库存中非 mcp__ 的直注册工具(mnemon_* 等)。三组勾选逻辑
|
|
2280
|
+
// 一致(setRoleTools),分组只影响展示。
|
|
2281
|
+
var candidates = roleCandidateTools(name);
|
|
2282
|
+
var inv = data.inventory || [];
|
|
2283
|
+
var mcpTools = candidates.filter(function (t) { return hostTool(t); });
|
|
2284
|
+
var pluginTools = candidates.filter(function (t) { return !hostTool(t) && inv.indexOf(t) !== -1; });
|
|
2285
|
+
var baseTools = candidates.filter(function (t) { return !hostTool(t) && inv.indexOf(t) === -1; });
|
|
988
2286
|
var checked = a.roles[name].tools;
|
|
989
2287
|
body.push(h("div", { className: "omd-glabel" }, "基础工具"));
|
|
990
2288
|
body.push(h("div", { className: "omd-toolGrid" },
|
|
991
2289
|
baseTools.length
|
|
992
2290
|
? baseTools.map(function (t) {
|
|
993
|
-
return toolItem(t, "base", checked.indexOf(t) !== -1,
|
|
2291
|
+
return toolItem(t, "base", checked.indexOf(t) !== -1, busy,
|
|
994
2292
|
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
995
2293
|
})
|
|
996
2294
|
: h("div", { className: "omd-empty" }, "无")));
|
|
997
|
-
body.push(h("div", { className: "omd-glabel" }, "
|
|
2295
|
+
body.push(h("div", { className: "omd-glabel" }, "MCP 工具"));
|
|
998
2296
|
body.push(h("div", { className: "omd-toolGrid" },
|
|
999
|
-
|
|
1000
|
-
?
|
|
1001
|
-
return toolItem(t, "
|
|
2297
|
+
mcpTools.length
|
|
2298
|
+
? mcpTools.map(function (t) {
|
|
2299
|
+
return toolItem(t, "mcp", checked.indexOf(t) !== -1, busy,
|
|
1002
2300
|
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
1003
2301
|
})
|
|
1004
|
-
: h("div", { className: "omd-empty" }, "未检测到
|
|
2302
|
+
: h("div", { className: "omd-empty" }, "未检测到 MCP 工具")));
|
|
2303
|
+
body.push(h("div", { className: "omd-glabel" }, "插件工具"));
|
|
2304
|
+
body.push(h("div", { className: "omd-toolGrid" },
|
|
2305
|
+
pluginTools.length
|
|
2306
|
+
? pluginTools.map(function (t) {
|
|
2307
|
+
return toolItem(t, "plugin", checked.indexOf(t) !== -1, busy,
|
|
2308
|
+
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
2309
|
+
})
|
|
2310
|
+
: h("div", { className: "omd-empty" }, "未检测到插件工具")));
|
|
1005
2311
|
body.push(personaBlock(name));
|
|
1006
2312
|
}
|
|
1007
2313
|
|
|
@@ -1011,17 +2317,21 @@ window.__ModuleLoader__.load({
|
|
|
1011
2317
|
}
|
|
1012
2318
|
|
|
1013
2319
|
function skillsBlock() {
|
|
1014
|
-
|
|
2320
|
+
// 数据源与主 agent 技能网格一致(工作区视角 = 合并列表,见 gridSkills);
|
|
2321
|
+
// 项目级技能在名字旁加「项目」角标。
|
|
2322
|
+
var rows = gridSkills.map(function (s) {
|
|
1015
2323
|
var roleNames = Object.keys(assign.roles);
|
|
1016
2324
|
var picked = assign.skills[s] || [];
|
|
1017
2325
|
return h("div", { className: "omd-skillRow", key: s },
|
|
1018
2326
|
h("span", { className: "omd-skillName", title: s }, s),
|
|
2327
|
+
gridProj && gridProj.has(s) ? h("span", { className: "omd-skillProj" }, "项目") : null,
|
|
1019
2328
|
h("div", { className: "omd-skillRoles" }, roleNames.map(function (r) {
|
|
1020
2329
|
return h("label", { className: "omd-skillRole", key: r },
|
|
1021
2330
|
h("input", {
|
|
1022
2331
|
type: "checkbox",
|
|
1023
2332
|
className: "omd-check",
|
|
1024
2333
|
checked: picked.indexOf(r) !== -1,
|
|
2334
|
+
disabled: busy,
|
|
1025
2335
|
onChange: function (e) { setSkill(s, r, e.target.checked); },
|
|
1026
2336
|
}),
|
|
1027
2337
|
h("span", null, r));
|
|
@@ -1032,6 +2342,11 @@ window.__ModuleLoader__.load({
|
|
|
1032
2342
|
|
|
1033
2343
|
var customRoles = Object.keys(assign.roles).filter(function (n) { return DEFAULT_ROLES.indexOf(n) === -1; });
|
|
1034
2344
|
|
|
2345
|
+
// persistence 警告横幅:host 层会话持久化后端缺失(state.persistenceAvailable
|
|
2346
|
+
// === false;字段缺失的老版服务端视为可用,不误报)且当前视角的任意角色
|
|
2347
|
+
// 配了可续(continuable)时,面板顶部警示——只提示不拦截,预览 / 应用照常。
|
|
2348
|
+
var persistenceWarn = data.persistenceAvailable === false && hasContinuableRole(assign.roles);
|
|
2349
|
+
|
|
1035
2350
|
var applyBtn = h(P.Button, {
|
|
1036
2351
|
variant: "primary",
|
|
1037
2352
|
size: "md",
|
|
@@ -1062,32 +2377,30 @@ window.__ModuleLoader__.load({
|
|
|
1062
2377
|
: ic(P.IconRefreshOutline16, 16),
|
|
1063
2378
|
}, busyOp === "rescan" ? "检测中…" : "重新检测");
|
|
1064
2379
|
|
|
1065
|
-
|
|
2380
|
+
return h("div", { className: "pd-page", ref: rootRef },
|
|
2381
|
+
// 运行时识别降级:持续状态(不是事件),内联一行小字贴标签行上方,
|
|
2382
|
+
// 平时(runtimeOk 正常)零占位
|
|
2383
|
+
runtimeNoteOf(data),
|
|
1066
2384
|
|
|
1067
|
-
|
|
1068
|
-
|
|
2385
|
+
// persistence 警告横幅:仅当前视角确有可续角色且 host 层持久化后端
|
|
2386
|
+
// 缺失时出现(判定见上方 persistenceWarn),其余场合零占位
|
|
2387
|
+
persistenceWarn
|
|
2388
|
+
? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), ROLE_BG_PERSISTENCE_WARN)
|
|
2389
|
+
: null,
|
|
1069
2390
|
|
|
1070
|
-
//
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}),
|
|
1084
|
-
pluginChips.primary.map(function (p) {
|
|
1085
|
-
return h(P.Pill, { key: p.label }, chipTextOf(p));
|
|
1086
|
-
}),
|
|
1087
|
-
pluginChips.secondary.length
|
|
1088
|
-
? h("span", { className: "omd-chipSecondary" },
|
|
1089
|
-
"其他运行时插件:" + pluginChips.secondary.map(function (p) { return p.label; }).join(" · "))
|
|
1090
|
-
: null)),
|
|
2391
|
+
// 配置对象(全局默认 / 各工作区,标签页)
|
|
2392
|
+
workspaceBarOf(),
|
|
2393
|
+
|
|
2394
|
+
// 「添加工作区」弹窗:fixed 全屏遮罩不占布局流,挂在根数组末尾即可;
|
|
2395
|
+
// 未打开时返回 null,零开销
|
|
2396
|
+
wsModalOf(),
|
|
2397
|
+
|
|
2398
|
+
// 「新建自定义 agent 角色」弹窗:同为 fixed 全屏遮罩不占布局流,
|
|
2399
|
+
// 未打开时返回 null,零开销
|
|
2400
|
+
roleAddModalOf(),
|
|
2401
|
+
|
|
2402
|
+
// 「重命名自定义角色」弹窗:同上(原原生 prompt 改模态框)
|
|
2403
|
+
roleRenameModalOf(),
|
|
1091
2404
|
|
|
1092
2405
|
// 角色与工具
|
|
1093
2406
|
h("div", { className: "omd-group" },
|
|
@@ -1115,7 +2428,7 @@ window.__ModuleLoader__.load({
|
|
|
1115
2428
|
h("div", { className: "omd-hint" },
|
|
1116
2429
|
"恢复后卡片回到上方内置列表;工具面取内置默认(不沿用删前自定义配置)。"))
|
|
1117
2430
|
: null,
|
|
1118
|
-
h("button", { type: "button", className: "omd-addRole", onClick:
|
|
2431
|
+
h("button", { type: "button", className: "omd-addRole", onClick: openRoleAddModal },
|
|
1119
2432
|
h("span", { className: "omd-addRoleIcon" }, ic(P.IconPlusOutline16, 16)),
|
|
1120
2433
|
h("span", null, "新建自定义 agent 角色"))),
|
|
1121
2434
|
|
|
@@ -1124,7 +2437,12 @@ window.__ModuleLoader__.load({
|
|
|
1124
2437
|
h("div", { className: "omd-groupHeading" },
|
|
1125
2438
|
h("span", { className: "omd-groupIcon" }, ic(P.IconSkillOutline16, 14)),
|
|
1126
2439
|
h("span", null, "技能分配(软约束:写入对应 agent 的 persona 引导)"),
|
|
1127
|
-
|
|
2440
|
+
skillScopeNote(),
|
|
2441
|
+
h("span", { className: "omd-count" }, gridSkills.length)),
|
|
2442
|
+
// 工作区技能拉取失败的降级提示(一句话):此刻网格数据源已回落用户级。
|
|
2443
|
+
activeWs !== null && wsSkillsErr
|
|
2444
|
+
? statusRow("omd-statusWarn", ic(P.IconWarningOutline16, 16), wsSkillsErr)
|
|
2445
|
+
: null,
|
|
1128
2446
|
skillsBlock()),
|
|
1129
2447
|
|
|
1130
2448
|
// 预览生成(预览成功后才出现)
|
|
@@ -1133,7 +2451,9 @@ window.__ModuleLoader__.load({
|
|
|
1133
2451
|
h("span", { className: "omd-groupIcon" }, ic(P.IconDataOutline16, 14)),
|
|
1134
2452
|
h("span", null, "预览生成")),
|
|
1135
2453
|
h("div", { className: "omd-previewSummary" },
|
|
1136
|
-
"✅ 生成成功(未写盘)" +
|
|
2454
|
+
"✅ 生成成功(未写盘)" +
|
|
2455
|
+
(preview.presetId ? " · 预设 " + preview.presetId : "") +
|
|
2456
|
+
(preview.roleResults || []).map(function (r) {
|
|
1137
2457
|
return " · " + r.role + ": " + r.intent.length + " → " + r.kept.length;
|
|
1138
2458
|
}).join("")),
|
|
1139
2459
|
h("pre", { className: "omd-pre" }, preview.text)) : null,
|
|
@@ -1144,23 +2464,717 @@ window.__ModuleLoader__.load({
|
|
|
1144
2464
|
h("span", { className: "omd-applyDock" }, applyBtn)));
|
|
1145
2465
|
}
|
|
1146
2466
|
|
|
1147
|
-
// ──
|
|
2467
|
+
// ── 挂载:底部入口 + 会话中栏全页 overlay ──────────────────────────────
|
|
1148
2468
|
//
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
2469
|
+
// 入口走宿主 slots 键控槽(sidebar.footer.action,见下方 mountFooterEntry
|
|
2470
|
+
// 与 exports.inject 的 slots 声明);全页 overlay 为纯 DOM + React root:
|
|
2471
|
+
// 1) 底部入口:左下角动作条(与插件广场 cordis 徽章同槽位,设置行上方);
|
|
2472
|
+
// 宽栏 42px 圆角行 / 窄栏 36px 圆钮,点击开关 overlay;
|
|
2473
|
+
// 由观察器自动放回。
|
|
2474
|
+
// 2) 全页 overlay:容器 div(data-dsh-paoding-view)append 到会话中栏,
|
|
2475
|
+
// createRoot 渲染整页配置页;打开态在 <html> 上设
|
|
2476
|
+
// data-dsh-paoding-active,CSS 据此隐藏会话内容、显示本页。与
|
|
2477
|
+
// mnemon / taskboard / ssh 互斥(dsh-panel-activate 事件 + active
|
|
2478
|
+
// 属性双通道),点侧栏会话行等上下文自动关闭。
|
|
2479
|
+
|
|
2480
|
+
// react-dom/client:宿主 ModuleLoader seed 提供(mnemon 线上 bundle 同款
|
|
2481
|
+
// 用法)。异常宿主下取不到时 overlay 不挂,侧栏入口仍可用(保底不炸)。
|
|
2482
|
+
var ReactDOMClient = null;
|
|
2483
|
+
try { ReactDOMClient = require("react-dom/client"); } catch (err) { ReactDOMClient = null; }
|
|
2484
|
+
|
|
2485
|
+
// 框架无关的小状态器(照 mnemon 的 MnemonWorkspaceController):侧栏入口
|
|
2486
|
+
// 与全页 overlay 共享同一份 { open } 快照,subscribe 驱动两侧同步。
|
|
2487
|
+
// 「带目标工作区打开」的待办路径:宿主工作区菜单的「庖丁配置」入口点击时
|
|
2488
|
+
// 写入(controller.open(path)),ConfigPanel 在 state + 工作区列表都就绪后
|
|
2489
|
+
// 消费并切到该 tab。放模块级而非 prop 钻透——controller 与 ConfigPanel 分属
|
|
2490
|
+
// DOM 注入层与 React 层,中间隔着懒挂载的 createRoot,props 传不进去。
|
|
2491
|
+
var pendingOpenWs = null;
|
|
2492
|
+
|
|
2493
|
+
function setPendingOpenWs(wsPath) {
|
|
2494
|
+
pendingOpenWs = typeof wsPath === "string" && wsPath.trim() !== ""
|
|
2495
|
+
? wsPath.trim().replace(/[\/\\]+$/, "")
|
|
2496
|
+
: null;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function createWorkspaceController() {
|
|
2500
|
+
var snapshot = { open: false };
|
|
2501
|
+
var listeners = [];
|
|
2502
|
+
function notify() {
|
|
2503
|
+
listeners.slice().forEach(function (listener) { listener(); });
|
|
2504
|
+
}
|
|
2505
|
+
function setOpen(open, reassert) {
|
|
2506
|
+
if (snapshot.open === open && !reassert) return;
|
|
2507
|
+
snapshot = { open: open };
|
|
2508
|
+
notify();
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
getSnapshot: function () { return snapshot; },
|
|
2512
|
+
subscribe: function (listener) {
|
|
2513
|
+
listeners.push(listener);
|
|
2514
|
+
return function () {
|
|
2515
|
+
var i = listeners.indexOf(listener);
|
|
2516
|
+
if (i !== -1) listeners.splice(i, 1);
|
|
2517
|
+
};
|
|
2518
|
+
},
|
|
2519
|
+
// open 带 reassert:重复打开(如属性观察兜底触发)时也把 active 属性
|
|
2520
|
+
// 与互斥广播重新刷一遍,而不是因「已是 true」短路。可选 wsPath = 打开
|
|
2521
|
+
// 后要切到的工作区路径(宿主菜单「庖丁配置」入口用,见 pendingOpenWs)。
|
|
2522
|
+
open: function (wsPath) { setPendingOpenWs(wsPath); setOpen(true, true); },
|
|
2523
|
+
close: function () { setOpen(false); },
|
|
2524
|
+
toggle: function () { setOpen(!snapshot.open); },
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// ── 侧栏底部入口(sidebar.footer.action 键控槽)────────────────────────
|
|
2529
|
+
// 入口挂在左下角底部动作条:注册 sidebar.footer.action
|
|
2530
|
+
// 列表槽(与插件广场 cordis 徽章同一槽位,渲染在设置行上方的动作条里)。
|
|
2531
|
+
// 观感复刻 cordis 徽章 / 设置触发钮:宽栏 = 42px 圆角行(图标 + 文案),
|
|
2532
|
+
// 窄栏(56px rail)= 36px 圆形图标钮。点击开 / 关全页 overlay(mountPanel
|
|
2533
|
+
// 一套不动:互斥广播、工作区菜单深链、Esc 层级照旧)。
|
|
2534
|
+
function makeFooterEntry(controller) {
|
|
2535
|
+
return function FooterEntry(props) {
|
|
2536
|
+
var wide = !!(props && props.wide);
|
|
2537
|
+
var _st = useState(controller.getSnapshot().open), open = _st[0], setOpenState = _st[1];
|
|
2538
|
+
useEffect(function () {
|
|
2539
|
+
return controller.subscribe(function () { setOpenState(controller.getSnapshot().open); });
|
|
2540
|
+
}, []);
|
|
2541
|
+
return h("button", {
|
|
2542
|
+
type: "button",
|
|
2543
|
+
className: "pd-footerBtn" + (wide ? "" : " pd-footerRail"),
|
|
2544
|
+
"data-active": open ? "true" : undefined,
|
|
2545
|
+
"aria-label": "庖丁配置",
|
|
2546
|
+
"aria-haspopup": "dialog",
|
|
2547
|
+
"aria-expanded": open ? "true" : "false",
|
|
2548
|
+
title: wide ? undefined : "庖丁配置",
|
|
2549
|
+
onClick: function () { controller.toggle(); },
|
|
2550
|
+
},
|
|
2551
|
+
h(P.IconSkillOutline16, { size: wide ? 16 : 18 }),
|
|
2552
|
+
wide ? h("span", { className: "pd-footerLabel" }, "庖丁配置") : null);
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function mountFooterEntry(ctx, controller) {
|
|
2557
|
+
if (typeof document === "undefined") return function () {};
|
|
2558
|
+
if (!ctx || !ctx.slots || typeof ctx.slots.inject !== "function" || typeof ctx.slots.register !== "function") {
|
|
2559
|
+
return function () {}; // 槽口不可用(老宿主):静默不挂,面板仅失去底部入口
|
|
2560
|
+
}
|
|
2561
|
+
var Entry = makeFooterEntry(controller);
|
|
2562
|
+
var dispose = ctx.slots.inject("sidebar.footer.action", function () {
|
|
2563
|
+
return ctx.slots.register({ name: "sidebar.footer.action", id: "paoding-config", order: 10, label: "庖丁配置" }, Entry);
|
|
2564
|
+
});
|
|
2565
|
+
return typeof dispose === "function" ? dispose : function () {};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
|
|
2569
|
+
// ── 委派事件流行(tool.call.toolview 键控行替换)────────────────────────
|
|
2570
|
+
// 事件流里每个工具调用行由 ToolCallTree 按 toolName 查 tool.call.toolview
|
|
2571
|
+
// 键控插槽,键命中则整行替换成注册组件(bash / read / grep / cordis 等都
|
|
2572
|
+
// 走这条路)。这里为庖丁的委派工具(内置四角色 + 自定义角色名)注册行:
|
|
2573
|
+
// 标题 = 显示名(roles.<toolName>.name,未配走中文缺省映射 / toolName),
|
|
2574
|
+
// 副题 = 调用参数 description(模型写的任务一句话),尾部状态点,点击展开
|
|
2575
|
+
// 子 agent 返回文本——通用行的结果查看能力不能丢。
|
|
2576
|
+
// 边界:只管工具调用行;子 agent 会话树 / 面包屑标题来自服务端会话摘要,
|
|
2577
|
+
// 不在客户端控制面。标题取全局配置的显示名(行渲染时不知道会话属于哪个
|
|
2578
|
+
// 工作区,工作区级显示名够不着)。
|
|
2579
|
+
var DELEGATE_TITLE_FALLBACK = {
|
|
2580
|
+
search_external: "外部检索",
|
|
2581
|
+
design: "设计",
|
|
2582
|
+
implement: "实现",
|
|
2583
|
+
search_internal_deep: "深搜",
|
|
2584
|
+
};
|
|
2585
|
+
// 静态委派工具:不在 roles 键里(无条目、不可删不可改名),恒注册。
|
|
2586
|
+
var DELEGATE_STATIC_TOOLS = ["search_internal_deep"];
|
|
2587
|
+
|
|
2588
|
+
function buildDelegateMeta(d) {
|
|
2589
|
+
var cfg = globalConfigOf(d) || (d && d.suggested) || {};
|
|
2590
|
+
var roles = (cfg && typeof cfg.roles === "object" && cfg.roles) || {};
|
|
2591
|
+
var removed = Array.isArray(cfg.roles_remove) ? cfg.roles_remove : [];
|
|
2592
|
+
var titles = {};
|
|
2593
|
+
var tools = DELEGATE_STATIC_TOOLS.slice();
|
|
2594
|
+
DELEGATE_STATIC_TOOLS.forEach(function (n) { titles[n] = DELEGATE_TITLE_FALLBACK[n] || n; });
|
|
2595
|
+
Object.keys(roles).forEach(function (n) {
|
|
2596
|
+
if (removed.indexOf(n) !== -1) return; // 已删内置角色:委派工具不存在,键命中不了,不注册
|
|
2597
|
+
if (tools.indexOf(n) === -1) tools.push(n);
|
|
2598
|
+
var nm = roles[n] && typeof roles[n].name === "string" ? roles[n].name.trim() : "";
|
|
2599
|
+
titles[n] = nm !== "" ? nm : (DELEGATE_TITLE_FALLBACK[n] || n);
|
|
2600
|
+
});
|
|
2601
|
+
return { titles: titles, tools: tools };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
var delegateMeta = { titles: {}, tools: [] };
|
|
2605
|
+
var delegateRowsDispose = null;
|
|
2606
|
+
var delegateRowsCtx = null;
|
|
2607
|
+
|
|
2608
|
+
// 调用块是双形态(与宿主 toolRowModel 同源判据):进行中块无 kind、
|
|
2609
|
+
// 参数在 block.argsRaw;结算块有 kind、参数挪进 block.call.argsRaw,
|
|
2610
|
+
// 结果在 block.content,error.code === "interrupted" 表示被停。
|
|
2611
|
+
function delegateArgsRawOf(block) {
|
|
2612
|
+
if (!block || typeof block !== "object") return "";
|
|
2613
|
+
var raw = "kind" in block
|
|
2614
|
+
? (block.call && typeof block.call.argsRaw === "string" ? block.call.argsRaw : "")
|
|
2615
|
+
: (typeof block.argsRaw === "string" ? block.argsRaw : "");
|
|
2616
|
+
return raw || "";
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
function delegateStatusOf(block) {
|
|
2620
|
+
if (!block || typeof block !== "object" || !("kind" in block)) return "running";
|
|
2621
|
+
if (block.error && block.error.code === "interrupted") return "stopped";
|
|
2622
|
+
if (block.isError || block.error) return "error";
|
|
2623
|
+
return "done";
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// 展开用的结果文本:文本块原文 + 其余块 pretty JSON;空内容回落错误行。
|
|
2627
|
+
function delegateResultText(block) {
|
|
2628
|
+
var parts = [];
|
|
2629
|
+
var content = block && Array.isArray(block.content) ? block.content : [];
|
|
2630
|
+
for (var i = 0; i < content.length; i++) {
|
|
2631
|
+
var c = content[i];
|
|
2632
|
+
if (c && c.type === "text" && typeof c.text === "string") parts.push(c.text);
|
|
2633
|
+
else if (c) parts.push(JSON.stringify(c, null, 2));
|
|
2634
|
+
}
|
|
2635
|
+
if (parts.length === 0 && block && block.error) {
|
|
2636
|
+
parts.push((block.error.name || "error") + ": " + (block.error.code || ""));
|
|
2637
|
+
}
|
|
2638
|
+
return parts.join("\n");
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
function DelegateRow(props) {
|
|
2642
|
+
var block = props && props.block ? props.block : {};
|
|
2643
|
+
var toolName = (props && props.toolName) || "";
|
|
2644
|
+
var _o = useState(false), open = _o[0], setOpen = _o[1];
|
|
2645
|
+
var args = {};
|
|
2646
|
+
var argsRaw = delegateArgsRawOf(block);
|
|
2647
|
+
if (argsRaw !== "") {
|
|
2648
|
+
try {
|
|
2649
|
+
var parsed = JSON.parse(argsRaw);
|
|
2650
|
+
if (parsed && typeof parsed === "object") args = parsed;
|
|
2651
|
+
} catch (err) { /* 原始参数非 JSON:忽略,副题留空 */ }
|
|
2652
|
+
}
|
|
2653
|
+
var desc = typeof args.description === "string" ? args.description.trim() : "";
|
|
2654
|
+
var prompt = typeof args.prompt === "string" ? args.prompt : "";
|
|
2655
|
+
var status = delegateStatusOf(block);
|
|
2656
|
+
var stateCls = status === "running" ? "pd-delegateRunning" : (status === "error" ? "pd-delegateFailed" : (status === "stopped" ? "pd-delegateStopped" : "pd-delegateDone"));
|
|
2657
|
+
var stateGlyph = status === "running" ? "◌" : (status === "error" ? "✕" : (status === "stopped" ? "⊘" : "✓"));
|
|
2658
|
+
var title = delegateMeta.titles[toolName] || DELEGATE_TITLE_FALLBACK[toolName] || toolName;
|
|
2659
|
+
return h("div", { className: "pd-delegateRow" + (status === "error" ? " pd-delegateErr" : "") },
|
|
2660
|
+
h("button", {
|
|
2661
|
+
type: "button",
|
|
2662
|
+
className: "pd-delegateHead",
|
|
2663
|
+
"aria-expanded": open ? "true" : "false",
|
|
2664
|
+
title: prompt ? prompt.slice(0, 400) : "",
|
|
2665
|
+
onClick: function () { setOpen(!open); },
|
|
2666
|
+
},
|
|
2667
|
+
h("span", { className: "pd-delegateState " + stateCls, "aria-hidden": "true" }, stateGlyph),
|
|
2668
|
+
h("span", { className: "pd-delegateTitle" }, title),
|
|
2669
|
+
desc ? h("span", { className: "pd-delegateDesc" }, desc) : null,
|
|
2670
|
+
h("span", { className: open ? "pd-delegateChev pd-delegateChevOpen" : "pd-delegateChev", "aria-hidden": "true" }, "▸")),
|
|
2671
|
+
open ? h("pre", { className: "pd-delegateBody" }, delegateResultText(block)) : null);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// 重注册:先撤旧键(inject 返回处置器,dsh-mnemon 同款用法),再按当前
|
|
2675
|
+
// 名单重建——自定义角色增删 / 显示名改动后由 refreshDelegateRows 触发。
|
|
2676
|
+
function syncDelegateRows() {
|
|
2677
|
+
if (delegateRowsDispose) {
|
|
2678
|
+
try { delegateRowsDispose(); } catch (err) { /* 撤旧失败不阻断重建 */ }
|
|
2679
|
+
delegateRowsDispose = null;
|
|
2680
|
+
}
|
|
2681
|
+
var ctx = delegateRowsCtx;
|
|
2682
|
+
if (!ctx || !ctx.slots || typeof ctx.slots.inject !== "function" || typeof ctx.slots.register !== "function") return;
|
|
2683
|
+
if (delegateMeta.tools.length === 0) return;
|
|
2684
|
+
delegateRowsDispose = ctx.slots.inject("tool.call.toolview", function () {
|
|
2685
|
+
var disposers = [];
|
|
2686
|
+
try {
|
|
2687
|
+
delegateMeta.tools.forEach(function (n) {
|
|
2688
|
+
disposers.push(ctx.slots.register({ name: "tool.call.toolview", key: n }, DelegateRow));
|
|
2689
|
+
});
|
|
2690
|
+
} catch (err) {
|
|
2691
|
+
disposers.forEach(function (d) { try { d(); } catch (e2) { /* 连锁失败静默 */ } });
|
|
2692
|
+
throw err;
|
|
2693
|
+
}
|
|
2694
|
+
return function () { disposers.forEach(function (d) { try { d(); } catch (e2) { /* 静默 */ } }); };
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function refreshDelegateRows() {
|
|
2699
|
+
paodingApi("state").then(function (d) {
|
|
2700
|
+
var next = buildDelegateMeta(d);
|
|
2701
|
+
// 比对串必须并进 titles 的值:显示名单独变更时键集合不变,只比键会
|
|
2702
|
+
// 漏判,事件流委派行标题刷不出来。
|
|
2703
|
+
var oldKeys = Object.keys(delegateMeta.titles).sort().map(function (k) { return k + ":" + delegateMeta.titles[k]; }).join("|")
|
|
2704
|
+
+ "//" + delegateMeta.tools.slice().sort().join("|");
|
|
2705
|
+
var newKeys = Object.keys(next.titles).sort().map(function (k) { return k + ":" + next.titles[k]; }).join("|")
|
|
2706
|
+
+ "//" + next.tools.slice().sort().join("|");
|
|
2707
|
+
if (newKeys !== oldKeys) {
|
|
2708
|
+
delegateMeta = next;
|
|
2709
|
+
syncDelegateRows();
|
|
2710
|
+
}
|
|
2711
|
+
}).catch(function () { /* 静默:行回落通用卡片 */ });
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
function mountDelegateRows(ctx) {
|
|
2715
|
+
if (typeof document === "undefined") return function () {};
|
|
2716
|
+
delegateRowsCtx = ctx;
|
|
2717
|
+
refreshDelegateRows();
|
|
2718
|
+
var offReset = null;
|
|
2719
|
+
if (ctx && typeof ctx.on === "function") {
|
|
2720
|
+
try {
|
|
2721
|
+
var r = ctx.on("connection/reset", function () { refreshDelegateRows(); });
|
|
2722
|
+
if (typeof r === "function") offReset = r;
|
|
2723
|
+
} catch (err) { /* 事件口不可用:仅失去断线重连后的刷新 */ }
|
|
2724
|
+
}
|
|
2725
|
+
return function () {
|
|
2726
|
+
if (delegateRowsDispose) { try { delegateRowsDispose(); } catch (err) { /* 静默 */ } }
|
|
2727
|
+
delegateRowsDispose = null;
|
|
2728
|
+
if (offReset) { try { offReset(); } catch (err) { /* 静默 */ } }
|
|
2729
|
+
delegateRowsCtx = null;
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
// ── 宿主工作区「...」菜单注入桥 ──────────────────────────────────────────
|
|
2734
|
+
// 宿主侧栏工作区行的「...」菜单只有「重命名 / 删除工作区」两项,onSelect
|
|
2735
|
+
// 硬编码只认这两个 id,没有插件扩展点。这里从 DOM 层补一个「庖丁配置」项:
|
|
2736
|
+
// 1. 捕获级监听 document 点击,按触发钮 aria-label(工作区“<名>”的操作)
|
|
2737
|
+
// 记下最近一次操作的宿主工作区名;
|
|
2738
|
+
// 2. MutationObserver 盯菜单 portal(每次打开都是新建 DOM):认出「重命名
|
|
2739
|
+
// + 删除工作区」两行 = 工作区菜单,克隆重命名行改文案为「庖丁配置」——
|
|
2740
|
+
// 克隆而非自绘,类名跟宿主构建走,观感天然一致,hash 类名升级也不怕;
|
|
2741
|
+
// 3. 点击:先合成 Escape 关掉宿主菜单,再按记下的名字到 /api/paoding/
|
|
2742
|
+
// workspaces 反查路径(title 精确 → basename 兜底),controller.open
|
|
2743
|
+
// (path) 打开本页,ConfigPanel 就绪后自动切到该工作区 tab。
|
|
2744
|
+
// 反查失败(列表拉不到 / 名字对不上)仍打开页面,落在全局默认并给 warn。
|
|
2745
|
+
// 宿主 i18n 的 aria-label 模板是「工作区“{name}”的操作」(zh)/「Workspace
|
|
2746
|
+
// “{name}” actions」类变体(en),这里宽松匹配:引号样式 + 尾缀都容错。
|
|
2747
|
+
function wsTitleFromAriaLabel(label) {
|
|
2748
|
+
if (typeof label !== "string" || label === "") return null;
|
|
2749
|
+
var m = label.match(/^(?:工作区|Workspace)\s*[“"']([^”"']{1,120})[”"']/);
|
|
2750
|
+
return m ? m[1] : null;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
function mountWorkspaceMenuBridge(controller) {
|
|
2754
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") {
|
|
2755
|
+
return function () {};
|
|
2756
|
+
}
|
|
2757
|
+
var lastWsTitle = null;
|
|
2758
|
+
|
|
2759
|
+
function onClickCapture(e) {
|
|
2760
|
+
var btn = e.target && e.target.closest ? e.target.closest("button[aria-label]") : null;
|
|
2761
|
+
if (!btn) return;
|
|
2762
|
+
var title = wsTitleFromAriaLabel(btn.getAttribute("aria-label"));
|
|
2763
|
+
if (title !== null) lastWsTitle = title;
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// 在菜单 portal 里找「工作区菜单」:所有文本叶子按已知文案对筛,命中后
|
|
2767
|
+
// 爬回菜单行(列表容器的直接子级),并要求同容器存在「删除」行作证。
|
|
2768
|
+
function findHostWsMenu(rootNode) {
|
|
2769
|
+
var scopes = [rootNode, document.body];
|
|
2770
|
+
for (var si = 0; si < scopes.length; si++) {
|
|
2771
|
+
var scope = scopes[si];
|
|
2772
|
+
if (!scope || !scope.querySelectorAll) continue;
|
|
2773
|
+
var leafs = scope.querySelectorAll("button, [role='menuitem'], li, div, span");
|
|
2774
|
+
for (var i = 0; i < leafs.length; i++) {
|
|
2775
|
+
var leaf = leafs[i];
|
|
2776
|
+
if (leaf.childElementCount !== 0) continue;
|
|
2777
|
+
var txt = (leaf.textContent || "").trim();
|
|
2778
|
+
var isRename = txt === "重命名" || txt === "Rename";
|
|
2779
|
+
if (!isRename) continue;
|
|
2780
|
+
var row = leaf;
|
|
2781
|
+
while (row.parentElement && row.parentElement.childElementCount < 2) row = row.parentElement;
|
|
2782
|
+
var list = row.parentElement;
|
|
2783
|
+
if (!list) continue;
|
|
2784
|
+
var hasDelete = false;
|
|
2785
|
+
for (var c = 0; c < list.children.length; c++) {
|
|
2786
|
+
var t = (list.children[c].textContent || "");
|
|
2787
|
+
if (t.indexOf("删除工作区") !== -1 || t.indexOf("Delete workspace") !== -1) { hasDelete = true; break; }
|
|
2788
|
+
}
|
|
2789
|
+
if (!hasDelete) continue; // 缺「删除工作区」佐证:非工作区菜单(其他重命名入口),继续扫下一候选
|
|
2790
|
+
// 已注入过(自家行在菜单里)就不再动
|
|
2791
|
+
if (list.querySelector("[data-paoding-ws-menu]")) return null;
|
|
2792
|
+
return row;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return null;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// 克隆宿主菜单行:换文案、清 id / aria 引用(防重复),打自家标记。
|
|
2799
|
+
function buildPaodingRow(modelRow, onPick) {
|
|
2800
|
+
var mine = modelRow.cloneNode(true);
|
|
2801
|
+
mine.removeAttribute("id");
|
|
2802
|
+
var inner = mine.querySelectorAll ? mine.querySelectorAll("[id]") : [];
|
|
2803
|
+
for (var i = 0; i < inner.length; i++) inner[i].removeAttribute("id");
|
|
2804
|
+
mine.setAttribute("data-paoding-ws-menu", "1");
|
|
2805
|
+
mine.setAttribute("aria-label", "庖丁配置");
|
|
2806
|
+
mine.setAttribute("title", "打开庖丁配置并切到该工作区");
|
|
2807
|
+
var walker = document.createTreeWalker(mine, NodeFilter.SHOW_TEXT, null);
|
|
2808
|
+
var node;
|
|
2809
|
+
while ((node = walker.nextNode())) {
|
|
2810
|
+
var t = node.nodeValue;
|
|
2811
|
+
if (t.indexOf("重命名") !== -1 || t.toLowerCase().indexOf("rename") !== -1) {
|
|
2812
|
+
node.nodeValue = t.replace(/重命名|Rename/gi, "庖丁配置");
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
mine.addEventListener("click", function (e) {
|
|
2816
|
+
e.preventDefault();
|
|
2817
|
+
e.stopPropagation();
|
|
2818
|
+
onPick();
|
|
2819
|
+
});
|
|
2820
|
+
return mine;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// 合成 Escape 关宿主菜单(portal 挂 body,宿主 Menu 自行监听关闭)。
|
|
2824
|
+
function closeHostMenu() {
|
|
2825
|
+
try { document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true })); } catch (err) { /* 老内核无构造器:跳过 */ }
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function pickAndOpen() {
|
|
2829
|
+
var title = lastWsTitle;
|
|
2830
|
+
closeHostMenu();
|
|
2831
|
+
// 等 Escape 落地再开本页(宿主菜单关闭与 overlay 激活互不抢事件)
|
|
2832
|
+
setTimeout(function () {
|
|
2833
|
+
if (!title) { controller.open(); return; }
|
|
2834
|
+
paodingApi("workspaces").then(function (d) {
|
|
2835
|
+
var items = (d && Array.isArray(d.items)) ? d.items : [];
|
|
2836
|
+
var hit = null;
|
|
2837
|
+
for (var i = 0; i < items.length; i++) {
|
|
2838
|
+
var it = items[i];
|
|
2839
|
+
if (!it || typeof it.path !== "string" || it.path === "") continue;
|
|
2840
|
+
if (typeof it.title === "string" && it.title === title) { hit = it.path; break; }
|
|
2841
|
+
}
|
|
2842
|
+
if (!hit) {
|
|
2843
|
+
// 标题对不上:按 basename 兜底(宿主缺省标题常取目录名)
|
|
2844
|
+
var norm = title.replace(/[\/\\]+$/, "");
|
|
2845
|
+
var base = norm.split("/").pop();
|
|
2846
|
+
for (var j = 0; j < items.length; j++) {
|
|
2847
|
+
var p = items[j] && items[j].path;
|
|
2848
|
+
if (typeof p !== "string" || p === "") continue;
|
|
2849
|
+
var pb = p.replace(/[\/\\]+$/, "").split("/").pop();
|
|
2850
|
+
if (pb === base) { hit = p; break; }
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
if (hit) controller.open(hit);
|
|
2854
|
+
else {
|
|
2855
|
+
controller.open();
|
|
2856
|
+
toast.warn("宿主工作区「" + title + "」还没在庖丁注册,已打开全局默认");
|
|
2857
|
+
}
|
|
2858
|
+
}).catch(function () {
|
|
2859
|
+
controller.open();
|
|
2860
|
+
toast.warn("工作区列表拉取失败,已打开全局默认");
|
|
2861
|
+
});
|
|
2862
|
+
}, 80);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// 闸门:mutation 记录里新增的元素含「重命名 / Rename」才进全量扫描——
|
|
2866
|
+
// 聊天流 DOM 变更频繁,菜单 portal 是 body 直挂的小 subtree,先粗筛再细认。
|
|
2867
|
+
function recordsLookLikeMenu(records) {
|
|
2868
|
+
for (var i = 0; i < records.length; i++) {
|
|
2869
|
+
var added = records[i] && records[i].addedNodes;
|
|
2870
|
+
if (!added) continue;
|
|
2871
|
+
for (var j = 0; j < added.length; j++) {
|
|
2872
|
+
var n = added[j];
|
|
2873
|
+
if (!n || n.nodeType !== 1) continue;
|
|
2874
|
+
var txt = n.textContent || "";
|
|
2875
|
+
if (txt.indexOf("重命名") !== -1 || txt.toLowerCase().indexOf("rename") !== -1) return true;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return false;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
var mo = new MutationObserver(function (records) {
|
|
2882
|
+
if (!recordsLookLikeMenu(records)) return;
|
|
2883
|
+
var model = findHostWsMenu(document.body);
|
|
2884
|
+
if (!model || !model.parentElement) return;
|
|
2885
|
+
model.parentElement.appendChild(buildPaodingRow(model, pickAndOpen));
|
|
2886
|
+
});
|
|
2887
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
2888
|
+
document.addEventListener("click", onClickCapture, true);
|
|
2889
|
+
return function () {
|
|
2890
|
+
mo.disconnect();
|
|
2891
|
+
document.removeEventListener("click", onClickCapture, true);
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
1152
2894
|
|
|
2895
|
+
// ── 全页 overlay ────────────────────────────────────────────────────────
|
|
2896
|
+
|
|
2897
|
+
var CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"], .dshDesktopConversationSurface';
|
|
2898
|
+
var ACTIVE_ATTR = "data-dsh-paoding-active";
|
|
2899
|
+
var MNEMON_ACTIVE_ATTR = "data-dsh-mnemon-active";
|
|
2900
|
+
var TASKBOARD_ACTIVE_ATTR = "data-dsh-taskboard-active";
|
|
2901
|
+
var SSH_ACTIVE_ATTR = "data-dsh-ssh-active";
|
|
2902
|
+
var ACTIVATE_EVENT = "dsh-panel-activate";
|
|
2903
|
+
// 点侧栏上下文(会话行 / 项目行 / 搜索结果 / 新会话 / 各插件入口)时自动
|
|
2904
|
+
// 关闭本页——用户已离开配置语境。
|
|
2905
|
+
var SIDEBAR_CONTEXT_SELECTOR = "[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-mnemon-entry], [data-dsh-paoding-entry], [class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
|
|
2906
|
+
|
|
2907
|
+
// 整页骨架:页头(标题 + 副标题 + 右上关闭)+ 内容区(ConfigPanel 整页
|
|
2908
|
+
// 内容,.pd-page 自身即滚动容器,悬浮操作坞的 sticky 语义在其中成立)。
|
|
2909
|
+
// Esc 关闭挂 document keydown(冒泡阶段):「添加工作区」弹窗、「新建 /
|
|
2910
|
+
// 重命名角色」弹窗与「更多」菜单开着时,ConfigPanel 的捕获级监听先截停
|
|
2911
|
+
// Esc(只收菜单 / 建议下拉 / 弹窗,不关页);确认弹窗开着时由 ConfirmLayer
|
|
2912
|
+
// 的捕获级监听截停(只关确认框);事件都到不了这里。其余位置 Esc 一律关页。
|
|
2913
|
+
// 一键升级(页头更新卡「升级」按钮,统一入口):
|
|
2914
|
+
// POST /api/paoding/upgrade,服务端 spawn `dsh plugin --profile <profile>
|
|
2915
|
+
// update dsh-paoding` 原地换版本,分钟级操作。确认弹窗(原原生 confirm
|
|
2916
|
+
// 改通用确认弹窗,confirmCtrl 单例见 70 片段)文案按挂载形态分流:
|
|
2917
|
+
// dev link 直连形态(layout 由服务端 version 路由按包根 .git 判定)无法
|
|
2918
|
+
// 就地升级,把手动切换命令说在前头;installed(registry 版)原地换版本,
|
|
2919
|
+
// 从简。反馈(成功 note / 失败 err,含子进程输出尾部)都收在卡内,不与
|
|
2920
|
+
// 面板的 toast 反馈混流。升级非破坏性操作,danger 不置位;弹窗本体由
|
|
2921
|
+
// PageShell 末尾的 ConfirmLayer 渲染(页头与面板发起的确认共用一层)。
|
|
2922
|
+
function runUpgradeFlow(verInfo, setVerInfo, setUpgBusy, setUpgNote, setUpgErr) {
|
|
2923
|
+
if (!verInfo || !verInfo.latest) return;
|
|
2924
|
+
var isDev = verInfo.layout === "dev";
|
|
2925
|
+
var msg = isDev
|
|
2926
|
+
? "当前为开发 link 直连形态:无法就地升级,请手动执行 dsh plugin add dsh-paoding@latest 切换到 registry 版本。仍要继续尝试吗?"
|
|
2927
|
+
: "确定升级到 " + versionTagOf(verInfo.latest) + "?升级将通过 dsh plugin update 原地换版本,完成后需重启 DSH 生效。";
|
|
2928
|
+
confirmCtrl.ask({
|
|
2929
|
+
title: "升级 dsh-paoding",
|
|
2930
|
+
body: msg,
|
|
2931
|
+
danger: false,
|
|
2932
|
+
confirmText: "升级",
|
|
2933
|
+
onConfirm: function () {
|
|
2934
|
+
setUpgBusy(true);
|
|
2935
|
+
setUpgNote("");
|
|
2936
|
+
setUpgErr("");
|
|
2937
|
+
paodingApi("upgrade", {}).then(function (r) {
|
|
2938
|
+
if (r && r.upToDate) { setUpgNote("已是最新版本"); return; }
|
|
2939
|
+
var done = versionTagOf((r && r.version) || verInfo.latest);
|
|
2940
|
+
setUpgNote((r && r.message) || ("✅ 已升级到 " + done + ",重启 DSH 后新版本生效(preset 会自动按新版重生成)"));
|
|
2941
|
+
// 重拉版本信息:服务端有 1 小时缓存且本进程挂的还是旧代码,重启前
|
|
2942
|
+
// 更新卡可能仍在(重启后自然消失),以此处的生效提示为准。
|
|
2943
|
+
paodingApi("version").then(function (d) {
|
|
2944
|
+
if (d && typeof d.current === "string" && d.current) setVerInfo(d);
|
|
2945
|
+
}).catch(function () { /* 静默:升级已完成,重拉失败不打扰 */ });
|
|
2946
|
+
}).catch(function (err) {
|
|
2947
|
+
// 失败要醒目:截服务端透传的子进程输出尾部 ~300 字符并入 err,方便
|
|
2948
|
+
// 定位 pnpm 缓存权限(EPERM)之类环境问题,不用翻服务端日志。
|
|
2949
|
+
var tail = typeof err.output === "string" && err.output ? ":" + err.output.slice(-300) : "";
|
|
2950
|
+
setUpgErr(err.message + tail);
|
|
2951
|
+
}).finally(function () {
|
|
2952
|
+
setUpgBusy(false);
|
|
2953
|
+
});
|
|
2954
|
+
},
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// 确认弹窗层:订阅 70 片段的 confirmCtrl 单例(照 makeFooterEntry 的
|
|
2959
|
+
// useState + subscribe 写法),渲染 confirmModalOf。挂在 PageShell 根数组
|
|
2960
|
+
// 末尾——fixed 全屏遮罩覆盖整页,面板(删除角色 / 保存并应用)与页头
|
|
2961
|
+
// (升级)发起的确认都罩得住。
|
|
2962
|
+
// Esc 分层:自带 document 捕获级 keydown 监听。ConfigPanel 的捕获级监听
|
|
2963
|
+
// 先注册(ConfigPanel 是 PageShell 的子组件,子 effect 先于本组件 effect
|
|
2964
|
+
// 运行),但它只在自己弹窗 / 菜单开着时才 stopPropagation——确认框开着
|
|
2965
|
+
// 时该监听放行,本监听(注册在后)截停并只关确认框;stopPropagation 之后
|
|
2966
|
+
// PageShell 冒泡层的关页监听收不到事件,确认框开着时 Esc 不会顺带关页。
|
|
2967
|
+
// req 为空时不截停,Esc 照常落到关页监听。确认路径见 confirmModalOf:
|
|
2968
|
+
// 先 close(req 清空、弹窗卸载)再执行 onConfirm,异步期间重复 Enter /
|
|
2969
|
+
// 双击不会再触发。
|
|
2970
|
+
function ConfirmLayer() {
|
|
2971
|
+
var _cq = useState(confirmCtrl.getSnapshot()), confirmSnap = _cq[0], setConfirmSnap = _cq[1];
|
|
2972
|
+
useEffect(function () {
|
|
2973
|
+
return confirmCtrl.subscribe(function () { setConfirmSnap(confirmCtrl.getSnapshot()); });
|
|
2974
|
+
}, []);
|
|
2975
|
+
useEffect(function () {
|
|
2976
|
+
var onKey = function (e) {
|
|
2977
|
+
if (!e || e.key !== "Escape") return;
|
|
2978
|
+
if (!confirmCtrl.getSnapshot().req) return; // 无确认框不截停:放行给关页监听
|
|
2979
|
+
e.stopPropagation();
|
|
2980
|
+
confirmCtrl.close();
|
|
2981
|
+
};
|
|
2982
|
+
document.addEventListener("keydown", onKey, true);
|
|
2983
|
+
return function () { document.removeEventListener("keydown", onKey, true); };
|
|
2984
|
+
}, []);
|
|
2985
|
+
return confirmModalOf(confirmSnap.req, confirmCtrl.close);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
function PageShell(props) {
|
|
2989
|
+
var onClose = props.onClose;
|
|
2990
|
+
// 版本信息挂页头(拉取逻辑仍在 ConfigPanel 的 loadVersion,经 setVerInfo 回写)
|
|
2991
|
+
var _v = useState(null), verInfo = _v[0], setVerInfo = _v[1];
|
|
2992
|
+
// 升级动作随更新卡留在页头:独立 busy/note/err,不复用 ConfigPanel 的
|
|
2993
|
+
// 面板级互斥——页头与面板各自成区,写盘目标不同(升级换插件载荷、面板
|
|
2994
|
+
// 写预设与配置),升级期间面板照常预览编辑互不干扰。
|
|
2995
|
+
var _ub = useState(false), upgBusy = _ub[0], setUpgBusy = _ub[1];
|
|
2996
|
+
var _un = useState(""), upgNote = _un[0], setUpgNote = _un[1];
|
|
2997
|
+
var _ue = useState(""), upgErr = _ue[0], setUpgErr = _ue[1];
|
|
2998
|
+
|
|
2999
|
+
// 一键升级逻辑在共享的 runUpgradeFlow(页头更新卡统一走这一份)。
|
|
3000
|
+
function doUpgrade() {
|
|
3001
|
+
runUpgradeFlow(verInfo, setVerInfo, setUpgBusy, setUpgNote, setUpgErr);
|
|
3002
|
+
}
|
|
3003
|
+
useEffect(function () {
|
|
3004
|
+
var onKey = function (e) {
|
|
3005
|
+
if (e && e.key === "Escape" && typeof onClose === "function") onClose();
|
|
3006
|
+
};
|
|
3007
|
+
document.addEventListener("keydown", onKey);
|
|
3008
|
+
return function () { document.removeEventListener("keydown", onKey); };
|
|
3009
|
+
}, []);
|
|
3010
|
+
return h("div", { className: "pd-shell" },
|
|
3011
|
+
h("header", { className: "pd-pageHeader" },
|
|
3012
|
+
h("div", { className: "pd-pageTitles" },
|
|
3013
|
+
// 标题行:标题 + 版本号 + (有新版时的)更新卡并排;窄屏放不下时
|
|
3014
|
+
// 换行收纳,副标题独占下一行
|
|
3015
|
+
h("div", { className: "pd-titleRow" },
|
|
3016
|
+
h("h1", { className: "pd-pageTitle" }, "庖丁配置",
|
|
3017
|
+
verInfo && typeof verInfo.current === "string" && verInfo.current
|
|
3018
|
+
? h("span", { className: "pd-versionTag" }, versionTagOf(verInfo.current))
|
|
3019
|
+
: null),
|
|
3020
|
+
versionRowOf(verInfo, { busy: upgBusy, onUpgrade: doUpgrade, note: upgNote, error: upgErr })),
|
|
3021
|
+
h("p", { className: "pd-pageSubtitle" },
|
|
3022
|
+
"为主 agent 与子 agent 编排工具、技能与角色,预览并应用生成的 agent.cordis.yml。")),
|
|
3023
|
+
h("button", {
|
|
3024
|
+
type: "button",
|
|
3025
|
+
className: "pd-closeBtn",
|
|
3026
|
+
"aria-label": "关闭庖丁配置",
|
|
3027
|
+
title: "关闭(Esc)",
|
|
3028
|
+
onClick: function () { if (typeof onClose === "function") onClose(); },
|
|
3029
|
+
}, h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
3030
|
+
h(ConfigPanel, { setVerInfo: setVerInfo }),
|
|
3031
|
+
// 反馈 toast 宿主:订阅模块级 toast 控制器,悬浮于操作坞上方
|
|
3032
|
+
h(ToastHost),
|
|
3033
|
+
// 通用确认弹窗层:订阅 confirmCtrl(发起方见各 confirmCtrl.ask 调用点),
|
|
3034
|
+
// 放根数组末尾——fixed 遮罩不占布局流,页头 / 面板发起的确认都盖得住;
|
|
3035
|
+
// 无确认时渲染 null,仅保留 Esc 监听。
|
|
3036
|
+
h(ConfirmLayer));
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// 全页 overlay 挂载:容器 div append 到会话中栏,createRoot 渲染 PageShell。
|
|
3040
|
+
// 懒挂载——首次打开才建容器 / 发起面板首屏请求;此后容器常驻(CSS 按
|
|
3041
|
+
// active 属性显隐),被 React 挤掉时由观察器重挂。
|
|
3042
|
+
function mountPanel(controller) {
|
|
3043
|
+
if (typeof document === "undefined" || typeof window === "undefined" || !ReactDOMClient) {
|
|
3044
|
+
return function () {};
|
|
3045
|
+
}
|
|
3046
|
+
var root;
|
|
3047
|
+
var container;
|
|
3048
|
+
var ensure = function () {
|
|
3049
|
+
if (!controller.getSnapshot().open) return; // 懒挂载:只在打开态维护容器
|
|
3050
|
+
if (container && container.isConnected) return;
|
|
3051
|
+
if (container) {
|
|
3052
|
+
if (root) root.unmount();
|
|
3053
|
+
root = undefined;
|
|
3054
|
+
container = undefined;
|
|
3055
|
+
}
|
|
3056
|
+
var column = document.querySelector(CONVERSATION_COLUMN_SELECTOR);
|
|
3057
|
+
if (!column) return;
|
|
3058
|
+
container = document.createElement("div");
|
|
3059
|
+
// dataset 驼峰转数据属性:dshPaodingView → data-dsh-paoding-view
|
|
3060
|
+
container.dataset.dshPaodingView = "";
|
|
3061
|
+
container.className = "pd-panelView";
|
|
3062
|
+
column.appendChild(container);
|
|
3063
|
+
root = ReactDOMClient.createRoot(container);
|
|
3064
|
+
root.render(h(PageShell, {
|
|
3065
|
+
onClose: function () { controller.close(); },
|
|
3066
|
+
}));
|
|
3067
|
+
};
|
|
3068
|
+
// 会话中栏晚于插件挂载 / 容器被挤掉时重挂
|
|
3069
|
+
var waitObserver = new MutationObserver(ensure);
|
|
3070
|
+
waitObserver.observe(document.body, { childList: true, subtree: true });
|
|
3071
|
+
|
|
3072
|
+
var suppressCompatibilityClose = false;
|
|
3073
|
+
var applyActive = function () {
|
|
3074
|
+
if (!controller.getSnapshot().open) {
|
|
3075
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
// 互斥:先广播请走 taskboard / ssh(兼容旧面板的事件协议),清掉三家
|
|
3079
|
+
// 的 active 属性(它们的属性观察者会自行关闭),再点亮自己、以
|
|
3080
|
+
// detail:"paoding" 对外宣告。
|
|
3081
|
+
suppressCompatibilityClose = true;
|
|
3082
|
+
try {
|
|
3083
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "ssh" }));
|
|
3084
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "taskboard" }));
|
|
3085
|
+
} finally {
|
|
3086
|
+
suppressCompatibilityClose = false;
|
|
3087
|
+
}
|
|
3088
|
+
document.documentElement.removeAttribute(MNEMON_ACTIVE_ATTR);
|
|
3089
|
+
document.documentElement.removeAttribute(TASKBOARD_ACTIVE_ATTR);
|
|
3090
|
+
document.documentElement.removeAttribute(SSH_ACTIVE_ATTR);
|
|
3091
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
3092
|
+
ensure();
|
|
3093
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "paoding" }));
|
|
3094
|
+
};
|
|
3095
|
+
// 其它全页面板激活(taskboard / ssh / mnemon)→ 关闭自己
|
|
3096
|
+
var onOtherPanelActivate = function (event) {
|
|
3097
|
+
if (suppressCompatibilityClose || !controller.getSnapshot().open) return;
|
|
3098
|
+
var detail = event && event.detail;
|
|
3099
|
+
if (detail === "taskboard" || detail === "ssh" || detail === "mnemon") controller.close();
|
|
3100
|
+
};
|
|
3101
|
+
var onSidebarContextClick = function (event) {
|
|
3102
|
+
if (!controller.getSnapshot().open) return;
|
|
3103
|
+
var target = event.target;
|
|
3104
|
+
if (target && typeof target.closest === "function" && target.closest(SIDEBAR_CONTEXT_SELECTOR)) {
|
|
3105
|
+
controller.close();
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
// 属性兜底:发现自己 active 丢了、或别的面板 active 了 → 关闭
|
|
3109
|
+
var activeObserver = new MutationObserver(function () {
|
|
3110
|
+
if (!controller.getSnapshot().open) return;
|
|
3111
|
+
var html = document.documentElement;
|
|
3112
|
+
if (!html.hasAttribute(ACTIVE_ATTR) || html.hasAttribute(MNEMON_ACTIVE_ATTR) ||
|
|
3113
|
+
html.hasAttribute(TASKBOARD_ACTIVE_ATTR) || html.hasAttribute(SSH_ACTIVE_ATTR)) {
|
|
3114
|
+
controller.close();
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
activeObserver.observe(document.documentElement, {
|
|
3118
|
+
attributes: true,
|
|
3119
|
+
attributeFilter: [ACTIVE_ATTR, MNEMON_ACTIVE_ATTR, TASKBOARD_ACTIVE_ATTR, SSH_ACTIVE_ATTR],
|
|
3120
|
+
});
|
|
3121
|
+
|
|
3122
|
+
document.addEventListener("click", onSidebarContextClick, true);
|
|
3123
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherPanelActivate);
|
|
3124
|
+
var unsubscribe = controller.subscribe(applyActive);
|
|
3125
|
+
applyActive();
|
|
3126
|
+
return function () {
|
|
3127
|
+
document.removeEventListener("click", onSidebarContextClick, true);
|
|
3128
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherPanelActivate);
|
|
3129
|
+
activeObserver.disconnect();
|
|
3130
|
+
waitObserver.disconnect();
|
|
3131
|
+
unsubscribe();
|
|
3132
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
3133
|
+
if (root) root.unmount();
|
|
3134
|
+
root = undefined;
|
|
3135
|
+
if (container) container.remove();
|
|
3136
|
+
container = undefined;
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// ── cordis client 插件入口 ──────────────────────────────────────────────
|
|
3141
|
+
|
|
3142
|
+
// 依赖声明:底部入口(sidebar.footer.action)与事件流委派行
|
|
3143
|
+
// (tool.call.toolview)都走宿主 slots 键控槽,须声明 slots 服务——
|
|
3144
|
+
// cordis 护栏下未声明服务的 ctx.slots 取不到(旧代码此处曾是空表,
|
|
3145
|
+
// 槽口挂载全部静默折返,入口不显示)。
|
|
1153
3146
|
var inject = ["slots"];
|
|
1154
3147
|
|
|
1155
3148
|
function apply(ctx) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
3149
|
+
var controller = createWorkspaceController();
|
|
3150
|
+
var manual = []; // ctx.effect 不可用时的保底清理函数累计
|
|
3151
|
+
// 优先交给宿主 effect 管生命周期(重载自动清理);ctx.effect 不可用或
|
|
3152
|
+
// 调用失败则直接执行并自行累计清理,保底不炸。
|
|
3153
|
+
function track(mount, label) {
|
|
3154
|
+
if (ctx && typeof ctx.effect === "function") {
|
|
3155
|
+
try {
|
|
3156
|
+
ctx.effect(mount, label);
|
|
3157
|
+
return;
|
|
3158
|
+
} catch (err) { /* 落入手动路径 */ }
|
|
3159
|
+
}
|
|
3160
|
+
try {
|
|
3161
|
+
var dispose = mount();
|
|
3162
|
+
if (typeof dispose === "function") manual.push(dispose);
|
|
3163
|
+
} catch (err) {
|
|
3164
|
+
if (ctx && ctx.logger && typeof ctx.logger.warn === "function") {
|
|
3165
|
+
ctx.logger.warn("paoding-config-ui: " + label + " mount failed: " + String((err && err.message) || err));
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
track(function () { return mountFooterEntry(ctx, controller); }, "paoding-config-ui: footer entry");
|
|
3170
|
+
track(function () { return mountPanel(controller); }, "paoding-config-ui: page overlay");
|
|
3171
|
+
track(function () { return mountWorkspaceMenuBridge(controller); }, "paoding-config-ui: workspace menu bridge");
|
|
3172
|
+
track(function () { return mountDelegateRows(ctx); }, "paoding-config-ui: delegate rows");
|
|
3173
|
+
return function () {
|
|
3174
|
+
manual.forEach(function (dispose) {
|
|
3175
|
+
try { dispose(); } catch (err) { /* 单个清理失败不阻断其余清理 */ }
|
|
3176
|
+
});
|
|
3177
|
+
};
|
|
1164
3178
|
}
|
|
1165
3179
|
|
|
1166
3180
|
exports.inject = inject;
|