dsh-paoding 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/README_en.md +95 -0
- package/bin/dsh-paoding.mjs +12 -0
- package/cordis.patch.yml +6 -0
- package/docs/architecture.md +292 -0
- package/docs/architecture_en.md +363 -0
- package/docs/configuration.md +551 -0
- package/docs/configuration_en.md +560 -0
- package/docs/images/1.png +0 -0
- 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/docs/installation.md +364 -0
- package/docs/installation_en.md +367 -0
- package/docs/orchestration.md +254 -0
- package/docs/orchestration_en.md +255 -0
- package/install.sh +31 -0
- package/package.json +29 -0
- package/plugins/paoding-config-ui/api-core.mjs +100 -0
- package/plugins/paoding-config-ui/index.mjs +327 -0
- package/plugins/paoding-config-ui/lib/base.css +615 -0
- package/plugins/paoding-config-ui/lib/client.js +1154 -0
- package/plugins/paoding-config-ui/package.json +23 -0
- package/plugins/paoding-config-ui/runtime-inventory.mjs +212 -0
- package/presets/orchestrator/agent.cordis.yml +323 -0
- package/presets/orchestrator/preset.yml +6 -0
- package/presets/orchestrator/restrict.mjs +191 -0
- package/tools/install.mjs +24 -0
- package/tools/lib/alloc.mjs +138 -0
- package/tools/lib/cli.mjs +522 -0
- package/tools/lib/compose.mjs +647 -0
- package/tools/lib/config.mjs +297 -0
- package/tools/lib/host.mjs +288 -0
- package/tools/lib/skills.mjs +146 -0
- package/tools/lib/spans.mjs +188 -0
- package/tools/lib/state.mjs +328 -0
- package/tools/lib/util.mjs +26 -0
- package/tools/lib/wizard.mjs +516 -0
- package/tools/lib/yaml.mjs +266 -0
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paoding-config-ui — DSH Web client bundle (手写,零构建)
|
|
3
|
+
*
|
|
4
|
+
* 加载机制(借鉴 dsh-better-sidebar,DSH 0.1.1-rc.1 实测通过):
|
|
5
|
+
* - dsh-client-modules 节点半扫描本包 package.json 的 dsh.client 声明 +
|
|
6
|
+
* exports["./client"],把本文件编入 window.__DSH_BOOT__ 并以
|
|
7
|
+
* /plugins/paoding-config-ui/client.js 服务(图行 id = 包名)。
|
|
8
|
+
* - 浏览器内核为 manifest 中每个插件创建 loader 条目并激活,故本文件
|
|
9
|
+
* 必然被拉取执行;__ModuleLoader__.load 的 id 必须等于包名(图行 id)。
|
|
10
|
+
* - factory 内 require() 只解析平台 seed 模块(react / cordis /
|
|
11
|
+
* ui-slots / ui-primitives 与已物化模块);本 bundle 依赖 react 与
|
|
12
|
+
* @deepseek-ai/dsh-client-ui-primitives。
|
|
13
|
+
* - 挂载点:设置页 settings.section 槽位(list slot,id: "paoding"),
|
|
14
|
+
* ctx.slots.inject() 等待设置壳声明后 register;id/order/label 驱动导航,
|
|
15
|
+
* 与 ui-settings-general 的 general 分区同款模式(参见该包 client.js)。
|
|
16
|
+
* - UI:按 DSH 设置壳原生观感重绘(dsh-better-sidebar 的 SideCardSection
|
|
17
|
+
* 配方)——分组容器卡 + 设计令牌(--dsw-alias-*,运行时注入,浅/深色
|
|
18
|
+
* 主题经 body[data-ds-dark-theme] 自动跟随)。样式正文独立在 lib/base.css,
|
|
19
|
+
* 由 Node 半经 GET /api/paoding/client.css 下发,本文件只在首次物化时
|
|
20
|
+
* 幂等注入 <link data-plugin-css="paoding-config-ui/base">(热重载不重复;
|
|
21
|
+
* bundle 内不含 CSS 正文,改样式无需重编 bundle);组件用 ui-primitives
|
|
22
|
+
* (Button / Pill / 图标)+ 原生 checkbox / textarea(原生语义与焦点)。
|
|
23
|
+
* 数据走同源 /api/paoding/*(Node 半注册,带浏览器信任围栏)。
|
|
24
|
+
*/
|
|
25
|
+
window.__ModuleLoader__.load({
|
|
26
|
+
id: "paoding-config-ui",
|
|
27
|
+
factory: (require) => {
|
|
28
|
+
var module = { exports: {} };
|
|
29
|
+
var exports = module.exports;
|
|
30
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
31
|
+
|
|
32
|
+
var React = require("react");
|
|
33
|
+
var h = React.createElement;
|
|
34
|
+
var useState = React.useState, useEffect = React.useEffect, useCallback = React.useCallback;
|
|
35
|
+
var P = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
36
|
+
|
|
37
|
+
// ── 样式注入(幂等)─────────────────────────────────────────────────
|
|
38
|
+
//
|
|
39
|
+
// 样式正文在 lib/base.css,由 Node 半经 GET /api/paoding/client.css 下发
|
|
40
|
+
//(同源请求,落在 /api/paoding 前缀路由的浏览器信任围栏内);此处只在
|
|
41
|
+
// 首次物化时注入一个 <link>。幂等:同名 data 属性的 link 已存在即跳过;
|
|
42
|
+
// API 不可达时面板降级为无样式(不阻塞功能)。
|
|
43
|
+
|
|
44
|
+
function mountCss() {
|
|
45
|
+
if (document.querySelector('link[data-plugin-css="paoding-config-ui/base"]')) return;
|
|
46
|
+
var link = document.createElement("link");
|
|
47
|
+
link.setAttribute("rel", "stylesheet");
|
|
48
|
+
link.setAttribute("data-plugin", "paoding-config-ui");
|
|
49
|
+
link.setAttribute("data-plugin-css", "paoding-config-ui/base");
|
|
50
|
+
link.setAttribute("href", "/api/paoding/client.css");
|
|
51
|
+
if (document.head) {
|
|
52
|
+
document.head.appendChild(link);
|
|
53
|
+
} else if (document.readyState === "loading") {
|
|
54
|
+
document.addEventListener("DOMContentLoaded", mountCss);
|
|
55
|
+
} else {
|
|
56
|
+
document.documentElement.appendChild(link);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (typeof document !== "undefined") mountCss();
|
|
60
|
+
|
|
61
|
+
// ── 渲染小助手(零构建,无 JSX)──────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
// 图标兜底:任一图标缺失时降级为 null,绝不崩溃面板。
|
|
64
|
+
function ic(Icon, size) {
|
|
65
|
+
return Icon ? h(Icon, { size: size }) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function statusRow(cls, iconNode, text) {
|
|
69
|
+
return h("div", { className: cls, role: cls === "omd-statusError" ? "alert" : undefined },
|
|
70
|
+
iconNode ? h("span", { className: "omd-statusIcon" }, iconNode) : null,
|
|
71
|
+
h("span", { className: "omd-statusText" }, text));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toolItem(tool, tag, checked, disabled, onChange, key) {
|
|
75
|
+
return h("label", { key: key, className: "omd-toolItem", title: tool },
|
|
76
|
+
h("input", {
|
|
77
|
+
type: "checkbox",
|
|
78
|
+
className: "omd-check",
|
|
79
|
+
checked: !!checked,
|
|
80
|
+
disabled: !!disabled,
|
|
81
|
+
readOnly: !!disabled,
|
|
82
|
+
onChange: onChange,
|
|
83
|
+
}),
|
|
84
|
+
h("span", { className: "omd-toolName" }, tool),
|
|
85
|
+
tag ? h("span", { className: tag === "host" ? "omd-toolTag omd-toolTagHost" : "omd-toolTag" }, tag) : null);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var DEFAULT_ROLES = ["search_external", "design", "implement"];
|
|
89
|
+
// 内置角色默认 persona 首行「身份主语」(仅用于 UI 占位与提示文案;实际生效值
|
|
90
|
+
// 由后端 SRC 模板 persona 首行决定)。「显示名」= 替换该主语(其余不动)。
|
|
91
|
+
var ROLE_DEFAULT_SUBJECT = {
|
|
92
|
+
search_external: "external-research",
|
|
93
|
+
design: "design",
|
|
94
|
+
implement: "implementation",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// 角色可选 string 字段透传归一(name / model / provider 三处共用,供
|
|
98
|
+
// buildAssignments 使用):string 且非 '' 才保留,null / 缺省 / '' 一律
|
|
99
|
+
// null(= 未配置)。字段拼写与后端契约一致。
|
|
100
|
+
function roleStringRef(baseRoles, n, key) {
|
|
101
|
+
var r = (baseRoles || {})[n];
|
|
102
|
+
return r && typeof r[key] === "string" && r[key] !== "" ? r[key] : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 内置角色显示名透传归一(= roleStringRef 之于 name):null = 默认身份。
|
|
106
|
+
function roleNameOf(baseRoles, n) {
|
|
107
|
+
return roleStringRef(baseRoles, n, "name");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var INTRO = "可视化编排 dsh-paoding(庖丁解牛)预设:为主 agent 与子 agent 分配 Host 工具与技能,预览并应用生成的 agent.cordis.yml(修改后先预览,确认无误再应用)。";
|
|
111
|
+
|
|
112
|
+
function hostTool(name) {
|
|
113
|
+
// host 工具 = MCP 暴露的工具(mcp__<server>__<tool>)。memory_search
|
|
114
|
+
// 旧归类已删:magic-memory 停用、mnemon 现在注册 mnemon_*(本函数只做
|
|
115
|
+
// 展示分组,不影响识别/分配)。
|
|
116
|
+
return name.indexOf("mcp__") === 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 插件 chips:识别保持通用(pluginReports + 未覆盖的 runtimePlugins 全量并入),
|
|
120
|
+
// 这里只做【纯展示】分组,绝不反向过滤识别结果:
|
|
121
|
+
// agent → 主行(工具型/agent 插件:dsh-mnemon、@hyzyn/dsh-codegraph,以及
|
|
122
|
+
// 文件白名单命中的工具插件)
|
|
123
|
+
// other → 次要行(市场/纯界面插件:dshmarket、dsh-better-sidebar)
|
|
124
|
+
// internal → 不展示(预设内部行,如 ./restrict.mjs)
|
|
125
|
+
// self → 不展示(本插件自己)
|
|
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";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pluginChipsOf(d) {
|
|
136
|
+
var primary = [];
|
|
137
|
+
var secondary = [];
|
|
138
|
+
var seen = {};
|
|
139
|
+
function push(p) {
|
|
140
|
+
var label = p && (p.label || p.moduleName);
|
|
141
|
+
if (!label || seen[label]) return;
|
|
142
|
+
seen[label] = true;
|
|
143
|
+
var kind = pluginKindOf(p);
|
|
144
|
+
if (kind === "agent") primary.push(p);
|
|
145
|
+
else if (kind === "other") secondary.push(p);
|
|
146
|
+
// internal / self:不展示
|
|
147
|
+
}
|
|
148
|
+
((d && d.pluginReports) || []).forEach(push);
|
|
149
|
+
((d && d.runtimePlugins) || []).forEach(function (e) {
|
|
150
|
+
var label = e && e.moduleName;
|
|
151
|
+
if (!label || seen[label]) return;
|
|
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 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 运行时来源的插件条目没有工具归属信息,不显示计数(避免误读为 “0 工具”);
|
|
164
|
+
// 其工具实际已并入 inventory / MCP chips,可在角色卡片里勾选。
|
|
165
|
+
function chipTextOf(p) {
|
|
166
|
+
if (p && p.runtime) return "插件 " + p.label + "(运行时)";
|
|
167
|
+
return "插件 " + (p && p.label) + " · " + ((p && p.tools) || []).length + " 工具";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 运行时识别不可用时的回退提示(小字);字段缺失或正常时不渲染任何内容。
|
|
171
|
+
function runtimeNoteOf(d) {
|
|
172
|
+
if (!d || d.runtimeOk !== false || !d.runtimeError) return null;
|
|
173
|
+
var err = String(d.runtimeError);
|
|
174
|
+
if (err.length > 200) err = err.slice(0, 200) + "…";
|
|
175
|
+
return h("div", { className: "omd-hint" },
|
|
176
|
+
"运行时识别不可用,已回退文件扫描(" + err + ")");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 角色专用模型下拉(目录来自 GET /api/paoding/models,Node 半经
|
|
180
|
+
// ctx.apiProxy.llm.models({}) 取 DSH 已注册的模型路由)────────────────
|
|
181
|
+
|
|
182
|
+
// 选项 value 编码:JSON.stringify([provider, model])。provider 路由与模型
|
|
183
|
+
// id 都可能含任意字符(/ : . 等),裸拼接有歧义;JSON 数组编码可逆且无
|
|
184
|
+
// 冲突,null(= 未配一侧)也能原样往返。
|
|
185
|
+
function encodeModelOption(provider, model) {
|
|
186
|
+
return JSON.stringify([provider, model]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 逆解码:非 JSON / 形状不对(含空串 = 「跟随主 agent」项)一律 null。
|
|
190
|
+
function decodeModelOption(value) {
|
|
191
|
+
try {
|
|
192
|
+
var arr = JSON.parse(value);
|
|
193
|
+
if (Array.isArray(arr) && arr.length === 2
|
|
194
|
+
&& (arr[0] === null || typeof arr[0] === "string")
|
|
195
|
+
&& (arr[1] === null || typeof arr[1] === "string")) {
|
|
196
|
+
return { provider: arr[0], model: arr[1] };
|
|
197
|
+
}
|
|
198
|
+
} catch (err) { /* 空串或非编码值:按未识别处理 */ }
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 目录 → 扁平选项表:group.id = provider 路由、model.id = 模型 id,显示名
|
|
203
|
+
// 缺省回退 id;重复的 (provider, model) 对去重(value 兼作 React key)。
|
|
204
|
+
function modelOptionsOf(groups) {
|
|
205
|
+
var out = [];
|
|
206
|
+
var seen = {};
|
|
207
|
+
(groups || []).forEach(function (g) {
|
|
208
|
+
if (!g || !Array.isArray(g.models)) return;
|
|
209
|
+
var provider = typeof g.id === "string" ? g.id : "";
|
|
210
|
+
g.models.forEach(function (m) {
|
|
211
|
+
if (!m || typeof m.id !== "string" || m.id === "") return;
|
|
212
|
+
var value = encodeModelOption(provider, m.id);
|
|
213
|
+
if (seen[value]) return;
|
|
214
|
+
seen[value] = true;
|
|
215
|
+
out.push({
|
|
216
|
+
value: value,
|
|
217
|
+
provider: provider,
|
|
218
|
+
providerName: g.name || provider,
|
|
219
|
+
model: m.id,
|
|
220
|
+
modelName: m.name || m.id,
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 选中值求解 + round-trip 保护:当前 (provider, model) 命中目录 → 返回该
|
|
228
|
+
// 选项 value;未命中且至少一侧非空(手编配置、目录未刷新等)→ 追加一个
|
|
229
|
+
// 「当前配置」选项并保持选中,绝不让已有配置值在下拉里凭空消失;两者皆
|
|
230
|
+
// 空 → ""(「跟随主 agent 当前模型」)。extra 为需追加的 <option> 描述
|
|
231
|
+
// (无需追加时 null),value/label 由调用方喂给 h("option", …)。
|
|
232
|
+
function roleModelSelectionOf(options, provider, model) {
|
|
233
|
+
if (!provider && !model) return { value: "", extra: null };
|
|
234
|
+
for (var i = 0; i < options.length; i++) {
|
|
235
|
+
if (options[i].model === model && options[i].provider === provider) {
|
|
236
|
+
return { value: options[i].value, extra: null };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
var rtValue = encodeModelOption(provider || null, model || null);
|
|
240
|
+
// 文案照实:模型缺失(只配 provider,后端会忽略)时按约定不带空格接全角括号
|
|
241
|
+
var label = model
|
|
242
|
+
? "当前配置:" + (provider || "(跟随主 agent 路由)") + " · " + model
|
|
243
|
+
: "当前配置:" + (provider || "(跟随主 agent 路由)") + " ·(无模型,后端忽略)";
|
|
244
|
+
return {
|
|
245
|
+
value: rtValue,
|
|
246
|
+
extra: { value: rtValue, label: label },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── 主面板 ─────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function ConfigPanel(props) {
|
|
253
|
+
var _s = useState(null), data = _s[0], setData = _s[1];
|
|
254
|
+
var _e = useState(null), error = _e[0], setError = _e[1];
|
|
255
|
+
var _a = useState(null), assign = _a[0], setAssign = _a[1];
|
|
256
|
+
var _b = useState(false), busy = _b[0], setBusy = _b[1];
|
|
257
|
+
var _op = useState(null), busyOp = _op[0], setBusyOp = _op[1];
|
|
258
|
+
var _p = useState(null), preview = _p[0], setPreview = _p[1];
|
|
259
|
+
var _n = useState(""), note = _n[0], setNote = _n[1];
|
|
260
|
+
var _o = useState({}), openMap = _o[0], setOpenMap = _o[1];
|
|
261
|
+
var _m = useState({}), personaMap = _m[0], setPersonaMap = _m[1];
|
|
262
|
+
var _x = useState(false), extraOpen = _x[0], setExtraOpen = _x[1];
|
|
263
|
+
var _r = useState(0), extraRev = _r[0], setExtraRev = _r[1];
|
|
264
|
+
var _nr = useState(0), nameRev = _nr[0], setNameRev = _nr[1]; // 主 agent 名称 input 重挂(key 技巧)
|
|
265
|
+
var _rn = useState({}), roleNameRev = _rn[0], setRoleNameRev = _rn[1]; // 内置角色显示名 input 重挂(每角色独立 key 计数)
|
|
266
|
+
var _rm = useState({}), roleModelRev = _rm[0], setRoleModelRev = _rm[1]; // 角色专用模型/provider input 重挂(仅降级手输模式用;每角色独立 key 计数)
|
|
267
|
+
var _mg = useState(null), modelGroups = _mg[0], setModelGroups = _mg[1]; // 模型目录 groups(/api/paoding/models)
|
|
268
|
+
var _mgok = useState(false), modelGroupsOk = _mgok[0], setModelGroupsOk = _mgok[1]; // 目录可用 = 下拉模式;false = 降级双 input
|
|
269
|
+
var _mgerr = useState(""), modelGroupsErr = _mgerr[0], setModelGroupsErr = _mgerr[1]; // 目录拉取失败原因(降级 hint 一句话展示)
|
|
270
|
+
|
|
271
|
+
var api = useCallback(function (path, body) {
|
|
272
|
+
var opts = body === undefined ? {} : {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "content-type": "application/json" },
|
|
275
|
+
body: JSON.stringify(body),
|
|
276
|
+
};
|
|
277
|
+
return fetch("/api/paoding/" + path, opts).then(function (r) {
|
|
278
|
+
return r.json().then(function (d) {
|
|
279
|
+
if (!r.ok) throw new Error(d.error || ("HTTP " + r.status));
|
|
280
|
+
return d;
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
var buildAssignments = useCallback(function (d) {
|
|
286
|
+
var base = d.existing || d.suggested || { roles: {}, roles_remove: [], main_agent_name: null, main_agent_extra: [], main_agent_remove: [], main_agent_skills: [], main_agent_skills_inline: [], skills: {} };
|
|
287
|
+
// roles_remove:被删除的内置角色名单(只含 DEFAULT_ROLES 名)。名单内的内置角色
|
|
288
|
+
// 不重建进 roles —— 删除状态跨刷新 / 重新检测保留,卡片消失,仅留在恢复区。
|
|
289
|
+
var removed = (base.roles_remove || []).filter(function (n) { return DEFAULT_ROLES.indexOf(n) !== -1; });
|
|
290
|
+
var roles = {};
|
|
291
|
+
DEFAULT_ROLES.forEach(function (name) {
|
|
292
|
+
if (removed.indexOf(name) !== -1) return;
|
|
293
|
+
roles[name] = {
|
|
294
|
+
persona: (base.roles && base.roles[name] && base.roles[name].persona) || null,
|
|
295
|
+
// 显示名透传:string 非 '' 保留,缺省 / '' / null 一律 null(内置默认条目 name:null)
|
|
296
|
+
name: roleNameOf(base.roles, name),
|
|
297
|
+
// 专用模型 / provider 透传:string 非 '' 保留,缺省 / '' / null 一律 null
|
|
298
|
+
//(null = 跟随主 agent 当前模型;provider 只配不配 model 时后端 warn 并忽略)
|
|
299
|
+
model: roleStringRef(base.roles, name, "model"),
|
|
300
|
+
provider: roleStringRef(base.roles, name, "provider"),
|
|
301
|
+
tools: (base.roles && base.roles[name] && base.roles[name].tools) ||
|
|
302
|
+
(d.blocks && d.blocks[name]) || [],
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
Object.keys(base.roles || {}).forEach(function (name) {
|
|
306
|
+
if (DEFAULT_ROLES.indexOf(name) !== -1) return;
|
|
307
|
+
roles[name] = {
|
|
308
|
+
persona: base.roles[name].persona || null,
|
|
309
|
+
name: roleNameOf(base.roles, name),
|
|
310
|
+
// 自定义角色同样携带 model / provider(null 或 string),形状与内置一致
|
|
311
|
+
model: roleStringRef(base.roles, name, "model"),
|
|
312
|
+
provider: roleStringRef(base.roles, name, "provider"),
|
|
313
|
+
tools: base.roles[name].tools || [],
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
roles: roles,
|
|
318
|
+
roles_remove: removed,
|
|
319
|
+
// 语义:null / '' / 缺省一律归一为 null('' 等于没配 = 默认显示名)
|
|
320
|
+
main_agent_name: base.main_agent_name == null || base.main_agent_name === "" ? null : base.main_agent_name,
|
|
321
|
+
main_agent_extra: base.main_agent_extra || [],
|
|
322
|
+
main_agent_remove: base.main_agent_remove || [],
|
|
323
|
+
main_agent_skills: (base.main_agent_skills || []).slice(),
|
|
324
|
+
main_agent_skills_inline: (base.main_agent_skills_inline || []).slice(),
|
|
325
|
+
// 语义:null/缺省 = 用默认常量(compose 时回落);'' = 明确清空;其他 = 用户覆盖
|
|
326
|
+
main_agent_persona_extra: base.main_agent_persona_extra == null ? null : base.main_agent_persona_extra,
|
|
327
|
+
skills: Object.assign({}, base.skills || {}),
|
|
328
|
+
};
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
var load = useCallback(function () {
|
|
332
|
+
setError(null);
|
|
333
|
+
api("state").then(function (d) {
|
|
334
|
+
setData(d);
|
|
335
|
+
setAssign(buildAssignments(d));
|
|
336
|
+
setNote("");
|
|
337
|
+
}).catch(function (err) { setError(err.message); });
|
|
338
|
+
}, [api, buildAssignments]);
|
|
339
|
+
|
|
340
|
+
useEffect(load, [load]);
|
|
341
|
+
|
|
342
|
+
// 模型目录:与 loadState 并行拉一次(目录发现可能稍慢,属正常;失败不
|
|
343
|
+
// 阻塞面板,只把角色专用模型降级为双 input 手输)。请求错误 / ok:false /
|
|
344
|
+
// 响应格式异常都归入降级,原因一句话留在 modelGroupsErr。
|
|
345
|
+
var loadModels = useCallback(function () {
|
|
346
|
+
api("models").then(function (d) {
|
|
347
|
+
var groups = d && Array.isArray(d.groups) ? d.groups : null;
|
|
348
|
+
if (!d || d.ok !== true || !groups) throw new Error("模型目录响应格式异常");
|
|
349
|
+
setModelGroups(groups);
|
|
350
|
+
setModelGroupsOk(true);
|
|
351
|
+
setModelGroupsErr("");
|
|
352
|
+
}).catch(function (err) {
|
|
353
|
+
setModelGroupsOk(false);
|
|
354
|
+
setModelGroupsErr(err.message);
|
|
355
|
+
});
|
|
356
|
+
}, [api]);
|
|
357
|
+
|
|
358
|
+
useEffect(loadModels, [loadModels]);
|
|
359
|
+
|
|
360
|
+
function setRoleTools(name, tool, on) {
|
|
361
|
+
setAssign(function (prev) {
|
|
362
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
363
|
+
var list = next.roles[name].tools;
|
|
364
|
+
if (on && list.indexOf(tool) === -1) list.push(tool);
|
|
365
|
+
if (!on) next.roles[name].tools = list.filter(function (t) { return t !== tool; });
|
|
366
|
+
return next;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setMainExtra(tool, on) {
|
|
371
|
+
setAssign(function (prev) {
|
|
372
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
373
|
+
if (on && next.main_agent_extra.indexOf(tool) === -1) next.main_agent_extra.push(tool);
|
|
374
|
+
if (!on) next.main_agent_extra = next.main_agent_extra.filter(function (t) { return t !== tool; });
|
|
375
|
+
return next;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function setMainRemove(tool, on) {
|
|
380
|
+
setAssign(function (prev) {
|
|
381
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
382
|
+
next.main_agent_remove = next.main_agent_remove || [];
|
|
383
|
+
if (on && next.main_agent_remove.indexOf(tool) !== -1) next.main_agent_remove = next.main_agent_remove.filter(function (t) { return t !== tool; });
|
|
384
|
+
if (!on && next.main_agent_remove.indexOf(tool) === -1) next.main_agent_remove.push(tool);
|
|
385
|
+
return next;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function setMainSkill(skill, on) {
|
|
390
|
+
setAssign(function (prev) {
|
|
391
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
392
|
+
next.main_agent_skills = next.main_agent_skills || [];
|
|
393
|
+
if (on && next.main_agent_skills.indexOf(skill) === -1) next.main_agent_skills.push(skill);
|
|
394
|
+
if (!on) next.main_agent_skills = next.main_agent_skills.filter(function (s) { return s !== skill; });
|
|
395
|
+
return next;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function setMainSkillInline(skill, on) {
|
|
400
|
+
setAssign(function (prev) {
|
|
401
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
402
|
+
next.main_agent_skills_inline = next.main_agent_skills_inline || [];
|
|
403
|
+
if (on && next.main_agent_skills_inline.indexOf(skill) === -1) next.main_agent_skills_inline.push(skill);
|
|
404
|
+
if (!on) next.main_agent_skills_inline = next.main_agent_skills_inline.filter(function (s) { return s !== skill; });
|
|
405
|
+
return next;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function setSkill(skill, role, on) {
|
|
410
|
+
setAssign(function (prev) {
|
|
411
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
412
|
+
var picked = next.skills[skill] || [];
|
|
413
|
+
if (on && picked.indexOf(role) === -1) picked.push(role);
|
|
414
|
+
if (!on) picked = picked.filter(function (r) { return r !== role; });
|
|
415
|
+
if (picked.length) next.skills[skill] = picked;
|
|
416
|
+
else delete next.skills[skill];
|
|
417
|
+
return next;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 主 persona 尾部追加(main_agent_persona_extra)写入:
|
|
422
|
+
// 全空 → ''(明确清空,不追加任何内容);与默认常量一致 → null(= 用默认常量,
|
|
423
|
+
// 代码里 DEFAULT_MAIN_AGENT_PERSONA_EXTRA 演进时自动跟随);其余 → 用户覆盖文本。
|
|
424
|
+
function setMainPersonaExtra(raw) {
|
|
425
|
+
setAssign(function (prev) {
|
|
426
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
427
|
+
var v = String(raw || "").replace(/[ \t\r\n]+$/, "");
|
|
428
|
+
var def = (data && data.mainPersonaExtraDefault) || "";
|
|
429
|
+
if (v.trim() === "") next.main_agent_persona_extra = "";
|
|
430
|
+
else next.main_agent_persona_extra = v === def ? null : v;
|
|
431
|
+
return next;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function restoreMainPersonaExtra() {
|
|
436
|
+
setAssign(function (prev) {
|
|
437
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
438
|
+
next.main_agent_persona_extra = null; // null = 用默认常量
|
|
439
|
+
return next;
|
|
440
|
+
});
|
|
441
|
+
setExtraRev(extraRev + 1); // 重挂 textarea,defaultValue 重新取默认文本
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function addRole() {
|
|
445
|
+
var name = window.prompt('新角色 toolName(英文小写,如 "reviewer"):');
|
|
446
|
+
if (!name) return;
|
|
447
|
+
name = name.trim();
|
|
448
|
+
if (!/^[a-z][a-z0-9_-]{1,31}$/.test(name)) { setNote("角色名须为 2-32 位小写字母/数字/下划线/连字符"); return; }
|
|
449
|
+
// 内置名不可经「新建」再造:正常存在时由下方 prev.roles 命中提示;已删除
|
|
450
|
+
// (不在 roles、在 roles_remove)时在此拦截并引导走恢复区,避免同一内置名
|
|
451
|
+
// 同时出现在卡片流与「已删除的内置角色」恢复区。
|
|
452
|
+
if (DEFAULT_ROLES.indexOf(name) !== -1) { setNote('内置角色不可新建("' + name + '"如已删除,请在下方「已删除的内置角色」区恢复)'); return; }
|
|
453
|
+
setAssign(function (prev) {
|
|
454
|
+
if (prev.roles[name]) { setNote('角色 "' + name + '" 已存在'); return prev; }
|
|
455
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
456
|
+
// 新角色全字段出厂形状:与 buildAssignments / restoreBuiltinRole 条目一致
|
|
457
|
+
//(model/provider 置 null = 跟随主 agent 当前模型)。
|
|
458
|
+
next.roles[name] = { persona: null, name: null, model: null, provider: null, tools: (data && data.inventory) || [] };
|
|
459
|
+
return next;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function removeRole(name) {
|
|
464
|
+
if (!window.confirm('删除自定义角色 "' + name + '"?')) return;
|
|
465
|
+
setAssign(function (prev) {
|
|
466
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
467
|
+
delete next.roles[name];
|
|
468
|
+
Object.keys(next.skills).forEach(function (s) {
|
|
469
|
+
next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
|
|
470
|
+
});
|
|
471
|
+
return next;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 内置角色删除:roles 删 key + skills 引用剔除 + roles_remove 记名(去重)。
|
|
476
|
+
// 后端语义:roles_remove 里的内置名 = 已删除(卡片消失、生成不再委派),
|
|
477
|
+
// 可经 restoreBuiltinRole 恢复;自定义角色删除语义不变(只删 roles key,不进 roles_remove)。
|
|
478
|
+
function removeBuiltinRole(name) {
|
|
479
|
+
if (!window.confirm('删除内置角色 "' + name + '"?该角色的委派工具、工具面、persona 与主 agent 的委派指引将一并移除;可在下方「已删除的内置角色」区一键恢复。')) return;
|
|
480
|
+
setAssign(function (prev) {
|
|
481
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
482
|
+
delete next.roles[name];
|
|
483
|
+
Object.keys(next.skills).forEach(function (s) {
|
|
484
|
+
next.skills[s] = next.skills[s].filter(function (r) { return r !== name; });
|
|
485
|
+
});
|
|
486
|
+
next.roles_remove = next.roles_remove || [];
|
|
487
|
+
if (next.roles_remove.indexOf(name) === -1) next.roles_remove.push(name);
|
|
488
|
+
return next;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 内置角色恢复:roles_remove 除名 + 重建角色。工具面取内置默认 allow
|
|
493
|
+
// (data.blocks[name]),不沿用删前自定义配置——恢复即出厂默认,简化处理。
|
|
494
|
+
// 显示名同样置 null(出厂默认),重建条目与 buildAssignments 形状一致。
|
|
495
|
+
// 专用模型 / provider 同理置 null:恢复 = 回到「跟随主 agent 当前模型」,
|
|
496
|
+
// 不保留删前的模型固定配置。
|
|
497
|
+
function restoreBuiltinRole(name) {
|
|
498
|
+
setAssign(function (prev) {
|
|
499
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
500
|
+
next.roles_remove = (next.roles_remove || []).filter(function (r) { return r !== name; });
|
|
501
|
+
var defTools = (data && data.blocks && data.blocks[name]) || [];
|
|
502
|
+
next.roles[name] = { persona: null, name: null, model: null, provider: null, tools: defTools.slice() };
|
|
503
|
+
return next;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function doPreview() {
|
|
508
|
+
if (!assign) return;
|
|
509
|
+
setBusy(true);
|
|
510
|
+
setBusyOp("preview");
|
|
511
|
+
setPreview(null);
|
|
512
|
+
api("preview", { assignments: assign }).then(function (r) {
|
|
513
|
+
setPreview(r);
|
|
514
|
+
setNote("");
|
|
515
|
+
}).catch(function (err) { setError(err.message); }).finally(function () {
|
|
516
|
+
setBusy(false);
|
|
517
|
+
setBusyOp(null);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function doApply() {
|
|
522
|
+
if (!assign) return;
|
|
523
|
+
if (!window.confirm("确认应用当前配置?将写入 preset 并覆盖 ~/.dsh/.agent-presets/orchestrator,且保存到 dsh-paoding.config.yml。")) return;
|
|
524
|
+
setBusy(true);
|
|
525
|
+
setBusyOp("apply");
|
|
526
|
+
api("apply", { assignments: assign }).then(function (r) {
|
|
527
|
+
var notes = (r.roleResults || []).map(function (x) {
|
|
528
|
+
return x.role + ": " + x.intent.length + " → " + x.kept.length;
|
|
529
|
+
});
|
|
530
|
+
setNote("✅ 已安装:" + notes.join(";") + "。" + (r.staleNote ? " ⚠️ patch 较新,重启后生效。" : " 重启 DSH 或新建会话后生效。"));
|
|
531
|
+
}).catch(function (err) { setError(err.message); }).finally(function () {
|
|
532
|
+
setBusy(false);
|
|
533
|
+
setBusyOp(null);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function doRescan() {
|
|
538
|
+
setBusy(true);
|
|
539
|
+
setBusyOp("rescan");
|
|
540
|
+
api("rescan", {}).then(function (d) {
|
|
541
|
+
setData(d);
|
|
542
|
+
setAssign(buildAssignments(d));
|
|
543
|
+
setNote("↻ 已重新检测");
|
|
544
|
+
}).catch(function (err) { setError(err.message); }).finally(function () {
|
|
545
|
+
setBusy(false);
|
|
546
|
+
setBusyOp(null);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function toggleRole(name) {
|
|
551
|
+
setOpenMap(function (prev) {
|
|
552
|
+
var next = Object.assign({}, prev);
|
|
553
|
+
next[name] = !(name in prev ? prev[name] : name === "__main__" || name === "search_external");
|
|
554
|
+
return next;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function togglePersona(name) {
|
|
559
|
+
setPersonaMap(function (prev) {
|
|
560
|
+
var next = Object.assign({}, prev);
|
|
561
|
+
next[name] = !prev[name];
|
|
562
|
+
return next;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── 渲染 ───────────────────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
function statusBarOf() {
|
|
569
|
+
if (error) return statusRow("omd-statusError", ic(P.IconWarningOutline16, 16), error);
|
|
570
|
+
if (note.indexOf("✅") === 0) return statusRow("omd-statusOk", null, note);
|
|
571
|
+
if (note) return statusRow("omd-statusWarn", null, note);
|
|
572
|
+
if (data) return statusRow("omd-status", ic(P.IconDataOutline16, 14),
|
|
573
|
+
"检测到 " + (data.inventory || []).length + " 个 host 工具 · " +
|
|
574
|
+
(data.skills || []).length + " 个技能 · config: " + (data.configFile || "默认"));
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!data || !assign) {
|
|
579
|
+
return h("div", { className: "omd-section" },
|
|
580
|
+
h("p", { className: "omd-intro" }, INTRO),
|
|
581
|
+
h("div", { className: "omd-group" },
|
|
582
|
+
h("div", { className: "omd-groupHeading" }, h("span", null, "状态与操作")),
|
|
583
|
+
statusBarOf(),
|
|
584
|
+
h("div", { className: "omd-loading" },
|
|
585
|
+
h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16)),
|
|
586
|
+
h("span", null, error ? "加载失败" : "加载检测状态…"))));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function roleCandidateTools(name) {
|
|
590
|
+
var base = (data.blocks && data.blocks[name]) || [];
|
|
591
|
+
return base.concat(data.inventory || []).filter(function (t, i, arr) { return arr.indexOf(t) === i; });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function personaBlock(name) {
|
|
595
|
+
var open = !!personaMap[name];
|
|
596
|
+
return h("div", { className: "omd-persona" },
|
|
597
|
+
h("button", {
|
|
598
|
+
type: "button",
|
|
599
|
+
className: "omd-personaToggle",
|
|
600
|
+
"aria-expanded": open ? "true" : "false",
|
|
601
|
+
onClick: function () { togglePersona(name); },
|
|
602
|
+
},
|
|
603
|
+
h("span", { className: open ? "omd-chevron omd-chevronOpen" : "omd-chevron" }, ic(P.IconChevronDownOutline14, 14)),
|
|
604
|
+
h("span", null, "persona(可选,覆盖默认角色说明)"),
|
|
605
|
+
h("span", { className: "omd-personaEdit" }, ic(P.IconEditOutline16, 14))),
|
|
606
|
+
open ? h("div", { className: "omd-personaBody" },
|
|
607
|
+
h("textarea", {
|
|
608
|
+
className: "omd-textarea",
|
|
609
|
+
defaultValue: assign.roles[name].persona || "",
|
|
610
|
+
placeholder: "输入该角色的 persona 说明…",
|
|
611
|
+
spellCheck: false,
|
|
612
|
+
onBlur: function (e) {
|
|
613
|
+
setAssign(function (prev) {
|
|
614
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
615
|
+
next.roles[name].persona = e.target.value.trim() === "" ? null : e.target.value;
|
|
616
|
+
return next;
|
|
617
|
+
});
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
h("div", { className: "omd-hint" }, "技能引导会自动追加到 persona 末尾;自定义角色必填此说明。"))
|
|
621
|
+
: null);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 主 agent「人设追加」:折叠编辑器,结构与角色 personaBlock 一致。
|
|
625
|
+
// textarea 显示生效文本 = 配置值 ?? 后端默认常量(mainPersonaExtraDefault);
|
|
626
|
+
// 清空保存 = ''(移除默认规则);文本 == 默认常量时回落 null(跟随默认演进)。
|
|
627
|
+
function mainPersonaExtraBlock() {
|
|
628
|
+
var def = (data && data.mainPersonaExtraDefault) || "";
|
|
629
|
+
var val = assign.main_agent_persona_extra == null ? def : assign.main_agent_persona_extra;
|
|
630
|
+
return h("div", { className: "omd-persona" },
|
|
631
|
+
h("button", {
|
|
632
|
+
type: "button",
|
|
633
|
+
className: "omd-personaToggle",
|
|
634
|
+
"aria-expanded": extraOpen ? "true" : "false",
|
|
635
|
+
onClick: function () { setExtraOpen(!extraOpen); },
|
|
636
|
+
},
|
|
637
|
+
h("span", { className: extraOpen ? "omd-chevron omd-chevronOpen" : "omd-chevron" }, ic(P.IconChevronDownOutline14, 14)),
|
|
638
|
+
h("span", null, "人设追加(主 agent 提示词尾部,可编辑)"),
|
|
639
|
+
h("span", { className: "omd-personaEdit" }, ic(P.IconEditOutline16, 14))),
|
|
640
|
+
extraOpen ? h("div", { className: "omd-personaBody" },
|
|
641
|
+
h("textarea", {
|
|
642
|
+
key: "omd-extra-" + extraRev,
|
|
643
|
+
className: "omd-textarea omd-extraArea",
|
|
644
|
+
defaultValue: val,
|
|
645
|
+
placeholder: "留空并保存 = 不追加任何内容(默认 codegraph 规则将被移除)…",
|
|
646
|
+
spellCheck: false,
|
|
647
|
+
onBlur: function (e) { setMainPersonaExtra(e.target.value); },
|
|
648
|
+
}),
|
|
649
|
+
h("div", { className: "omd-extraActions" },
|
|
650
|
+
h("button", {
|
|
651
|
+
type: "button",
|
|
652
|
+
className: "omd-extraRestore",
|
|
653
|
+
onClick: restoreMainPersonaExtra,
|
|
654
|
+
}, "恢复默认"),
|
|
655
|
+
h("span", { className: "omd-extraHint" },
|
|
656
|
+
"保存并应用时按此追加到主 agent persona 末尾(顺序:SRC 本体 → 技能行 → 此处)。改动需重启 GUI / 新会话生效。")),
|
|
657
|
+
h("div", { className: "omd-hint" }, "清空保存 = 移除默认 codegraph 规则(配置键 main_agent_persona_extra 记为 '')。"))
|
|
658
|
+
: null);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 主 agent「名称」行:preset 显示名 + persona 身份行 label。默认文案
|
|
662
|
+
// 「编排模式 (Orchestrator)」为后端默认常量,UI 侧硬编码做占位(后端
|
|
663
|
+
// state 无该默认字段就不引,与 mainPersonaExtraDefault 处理思路一致)。
|
|
664
|
+
// 写回校验与后端一致:trim 后 1-60 字符、不含 \r \n;真空字段 = 默认;
|
|
665
|
+
// 非空纯空白视为非法(防误触把已有名称静默清回默认)。
|
|
666
|
+
function mainNameBlock() {
|
|
667
|
+
var DEFAULT_NAME = "编排模式 (Orchestrator)";
|
|
668
|
+
function commitName(raw) {
|
|
669
|
+
var s = String(raw == null ? "" : raw);
|
|
670
|
+
if (/[\r\n]/.test(s)) {
|
|
671
|
+
setNote("主 agent 名称不能包含换行(\\r / \\n),未保存");
|
|
672
|
+
setNameRev(nameRev + 1); // 重挂 input,回到上次生效值
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
var v = s.trim();
|
|
676
|
+
if (s !== "" && v === "") {
|
|
677
|
+
setNote("主 agent 名称不能为纯空白(清空字段或点「恢复默认」可回到默认名称),未保存");
|
|
678
|
+
setNameRev(nameRev + 1);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (v.length > 60) {
|
|
682
|
+
setNote("主 agent 名称过长:trim 后最多 60 字符,未保存");
|
|
683
|
+
setNameRev(nameRev + 1);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
setAssign(function (prev) {
|
|
687
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
688
|
+
next.main_agent_name = v === "" ? null : v;
|
|
689
|
+
return next;
|
|
690
|
+
});
|
|
691
|
+
if (v === "") setNameRev(nameRev + 1); // 清空 = 默认:重挂显示占位
|
|
692
|
+
}
|
|
693
|
+
function restoreMainName() {
|
|
694
|
+
setAssign(function (prev) {
|
|
695
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
696
|
+
next.main_agent_name = null; // null = 默认显示名
|
|
697
|
+
return next;
|
|
698
|
+
});
|
|
699
|
+
setNameRev(nameRev + 1);
|
|
700
|
+
}
|
|
701
|
+
return h("div", { className: "omd-mainName" },
|
|
702
|
+
h("div", { className: "omd-glabel" }, "主 agent 名称(preset 显示名 + persona 身份行)"),
|
|
703
|
+
h("div", { className: "omd-nameRow" },
|
|
704
|
+
h("input", {
|
|
705
|
+
key: "omd-name-" + nameRev,
|
|
706
|
+
type: "text",
|
|
707
|
+
className: "omd-nameInput",
|
|
708
|
+
defaultValue: assign.main_agent_name || "",
|
|
709
|
+
placeholder: DEFAULT_NAME,
|
|
710
|
+
spellCheck: false,
|
|
711
|
+
onBlur: function (e) { commitName(e.target.value); },
|
|
712
|
+
}),
|
|
713
|
+
h("button", { type: "button", className: "omd-extraRestore", onClick: restoreMainName }, "恢复默认")),
|
|
714
|
+
h("div", { className: "omd-hint" },
|
|
715
|
+
"保存并应用后 persona 首行与预设显示名一并替换;留空 = 默认「编排模式 (Orchestrator)」。改动需重启 GUI / 新会话生效。"));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 内置角色「显示名」input 重挂(key 技巧):写回校验失败 / 清空回默认时
|
|
719
|
+
// 让对应 input 以生效值重新挂载(defaultValue 重读);每角色独立计数,
|
|
720
|
+
// 互不牵连(其余内置角色 input 已提交的值不受影响)。
|
|
721
|
+
function bumpRoleNameRev(name) {
|
|
722
|
+
setRoleNameRev(function (prev) {
|
|
723
|
+
var next = Object.assign({}, prev);
|
|
724
|
+
next[name] = (next[name] || 0) + 1;
|
|
725
|
+
return next;
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 角色「专用模型 / provider」input 重挂(key 技巧):与 roleNameRev 同款。
|
|
730
|
+
// 仅降级手输模式(模型目录不可用)仍在用:校验失败 / 清空回默认时让对应
|
|
731
|
+
// input 以生效值重新挂载;每角色独立计数,model 与 provider 两个 input
|
|
732
|
+
// 共用同一 rev(成对重挂,不残留旧输入)。下拉模式为受控 select,无此需要。
|
|
733
|
+
function bumpRoleModelRev(name) {
|
|
734
|
+
setRoleModelRev(function (prev) {
|
|
735
|
+
var next = Object.assign({}, prev);
|
|
736
|
+
next[name] = (next[name] || 0) + 1;
|
|
737
|
+
return next;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 内置角色「显示名」行:仅替换该角色默认 persona 首行身份句的主语
|
|
742
|
+
// (toolName / 调用名 / 工具面 / restrict 全不变);已自定义该角色
|
|
743
|
+
// persona 时此项后端忽略(此处仅提示)。校验与主 agent 名同源约束:
|
|
744
|
+
// trim 后 1-60 字符、不含 \r \n;空 / 纯空白 = 默认(写入 null)。
|
|
745
|
+
// 自定义角色卡不加此输入(其 persona 首行由用户自写),不入此函数。
|
|
746
|
+
function builtinNameBlock(name) {
|
|
747
|
+
var subject = (ROLE_DEFAULT_SUBJECT[name] || name);
|
|
748
|
+
function commitRoleName(raw) {
|
|
749
|
+
var s = String(raw == null ? "" : raw);
|
|
750
|
+
if (/[\r\n]/.test(s)) {
|
|
751
|
+
setNote('内置角色 "' + name + '" 显示名不能包含换行(\\r / \\n),未保存');
|
|
752
|
+
bumpRoleNameRev(name); // 重挂 input,回到上次生效值
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
var v = s.trim();
|
|
756
|
+
if (v.length > 60) {
|
|
757
|
+
setNote('内置角色 "' + name + '" 显示名过长:trim 后最多 60 字符,未保存');
|
|
758
|
+
bumpRoleNameRev(name);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
setAssign(function (prev) {
|
|
762
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
763
|
+
next.roles[name].name = v === "" ? null : v;
|
|
764
|
+
return next;
|
|
765
|
+
});
|
|
766
|
+
if (v === "") bumpRoleNameRev(name); // 空 / 纯空白 = 默认:重挂显示占位
|
|
767
|
+
}
|
|
768
|
+
return h("div", { className: "omd-mainName" },
|
|
769
|
+
h("div", { className: "omd-glabel" }, "角色显示名(默认 persona 身份句主语;不动工具名/调用名)"),
|
|
770
|
+
h("div", { className: "omd-nameRow" },
|
|
771
|
+
h("input", {
|
|
772
|
+
key: "omd-roleName-" + name + "-" + ((roleNameRev && roleNameRev[name]) || 0),
|
|
773
|
+
type: "text",
|
|
774
|
+
className: "omd-nameInput",
|
|
775
|
+
defaultValue: (assign.roles[name] && assign.roles[name].name) || "",
|
|
776
|
+
placeholder: subject,
|
|
777
|
+
spellCheck: false,
|
|
778
|
+
onBlur: function (e) { commitRoleName(e.target.value); },
|
|
779
|
+
})),
|
|
780
|
+
h("div", { className: "omd-hint" },
|
|
781
|
+
'留空 = 默认身份(如 "You are the ' + subject + ' agent.");已为该角色自定义 persona 时此项不生效。'));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 角色「专用模型」行(内置与自定义角色通用同一组件,签名不变):
|
|
785
|
+
// - 下拉模式(模型目录可用):单选 select,选项 = 「跟随主 agent 当前
|
|
786
|
+
// 模型」(value "")+ 目录里每个 (provider 路由, 模型) 一项;选中目录
|
|
787
|
+
// 项即成对写入 roles[name].provider + model(生成层写入委派块
|
|
788
|
+
// agentOptions,含冷恢复在内固定)。目录外的已有配置值(手编配置、
|
|
789
|
+
// 目录未刷新等)以「当前配置」选项保真显示并保持选中(round-trip
|
|
790
|
+
// 保护,绝不凭空消失)。
|
|
791
|
+
// - 降级模式(目录拉取失败 / ok:false / 格式异常):回退现状的 model +
|
|
792
|
+
// provider 双 input 手输,全部原有校验保留(禁换行、≤120、空 = null、
|
|
793
|
+
// provider 单配 hint),失败原因一句话进 omd-hint,不阻塞面板其余功能。
|
|
794
|
+
function roleModelBlock(name) {
|
|
795
|
+
var role = assign.roles[name] || {};
|
|
796
|
+
|
|
797
|
+
// ── 降级模式:目录不可用 → 现状双 input ───────────────────────────
|
|
798
|
+
if (!modelGroupsOk) {
|
|
799
|
+
var rev = (roleModelRev && roleModelRev[name]) || 0;
|
|
800
|
+
function labelOf(key) { return key === "provider" ? "provider" : "专用模型"; }
|
|
801
|
+
function commitRoleModel(key, raw) {
|
|
802
|
+
var s = String(raw == null ? "" : raw);
|
|
803
|
+
if (/[\r\n]/.test(s)) {
|
|
804
|
+
setNote('角色 "' + name + '" ' + labelOf(key) + "不能包含换行(\\r / \\n),未保存");
|
|
805
|
+
bumpRoleModelRev(name); // 重挂 input,回到上次生效值
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
var v = s.trim();
|
|
809
|
+
if (v.length > 120) {
|
|
810
|
+
setNote('角色 "' + name + '" ' + labelOf(key) + "过长:trim 后最多 120 字符,未保存");
|
|
811
|
+
bumpRoleModelRev(name);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// 成对性检查按「本次写入后的最终值」判定(assign 为 blur 前的生效值,
|
|
815
|
+
// 另一字段的已提交值已在其中),不放进 setAssign updater(保持其纯函数)。
|
|
816
|
+
var cur = assign.roles[name] || {};
|
|
817
|
+
var model = key === "model" ? (v === "" ? null : v) : (cur.model || null);
|
|
818
|
+
var provider = key === "provider" ? (v === "" ? null : v) : (cur.provider || null);
|
|
819
|
+
setAssign(function (prev) {
|
|
820
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
821
|
+
next.roles[name][key] = v === "" ? null : v;
|
|
822
|
+
return next;
|
|
823
|
+
});
|
|
824
|
+
// provider 非空而 model 为空 → 允许保存(后端原样落盘),但明确提示会被忽略
|
|
825
|
+
if (provider && !model) {
|
|
826
|
+
setNote('角色 "' + name + '":provider 仅在同时配置模型时生效(后端将忽略),已保存');
|
|
827
|
+
}
|
|
828
|
+
if (v === "") bumpRoleModelRev(name); // 清空 = 跟随主 agent:重挂显示占位
|
|
829
|
+
}
|
|
830
|
+
return h("div", { className: "omd-mainName" },
|
|
831
|
+
h("div", { className: "omd-glabel" }, "专用模型(可选;留空 = 跟随主 agent 当前模型)"),
|
|
832
|
+
h("div", { className: "omd-nameRow" },
|
|
833
|
+
h("input", {
|
|
834
|
+
key: "omd-roleModel-" + name + "-" + rev,
|
|
835
|
+
type: "text",
|
|
836
|
+
className: "omd-nameInput",
|
|
837
|
+
defaultValue: role.model || "",
|
|
838
|
+
placeholder: "deepseek-v4-pro",
|
|
839
|
+
spellCheck: false,
|
|
840
|
+
onBlur: function (e) { commitRoleModel("model", e.target.value); },
|
|
841
|
+
}),
|
|
842
|
+
h("input", {
|
|
843
|
+
key: "omd-roleProvider-" + name + "-" + rev,
|
|
844
|
+
type: "text",
|
|
845
|
+
className: "omd-nameInput",
|
|
846
|
+
defaultValue: role.provider || "",
|
|
847
|
+
placeholder: "provider(留空=跟随主 agent)",
|
|
848
|
+
spellCheck: false,
|
|
849
|
+
onBlur: function (e) { commitRoleModel("provider", e.target.value); },
|
|
850
|
+
})),
|
|
851
|
+
h("div", { className: "omd-hint" },
|
|
852
|
+
"模型目录不可用(" + (modelGroupsErr || "未知错误") + "),已回退手输。写入该角色委派块的 agentOptions;含冷恢复在内,子 agent 固定用该模型。provider 需与模型成对(如 deepseek-official)。"));
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── 下拉模式:选项来自 /api/paoding/models ────────────────────────
|
|
856
|
+
var options = modelOptionsOf(modelGroups);
|
|
857
|
+
var sel = roleModelSelectionOf(options, role.provider || null, role.model || null);
|
|
858
|
+
var children = [h("option", { key: "follow", value: "" }, "跟随主 agent 当前模型")];
|
|
859
|
+
options.forEach(function (opt) {
|
|
860
|
+
children.push(h("option", { key: opt.value, value: opt.value },
|
|
861
|
+
opt.modelName + "(" + opt.providerName + ")"));
|
|
862
|
+
});
|
|
863
|
+
if (sel.extra) children.push(h("option", { key: "current", value: sel.extra.value }, sel.extra.label));
|
|
864
|
+
function commitRoleModelOption(value) {
|
|
865
|
+
if (value === "") {
|
|
866
|
+
// 「跟随主 agent 当前模型」:model / provider 一并归 null
|
|
867
|
+
setAssign(function (prev) {
|
|
868
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
869
|
+
next.roles[name].model = null;
|
|
870
|
+
next.roles[name].provider = null;
|
|
871
|
+
return next;
|
|
872
|
+
});
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
var dec = decodeModelOption(value);
|
|
876
|
+
if (!dec) return; // 理论不可达:选项 value 均由本组件编码
|
|
877
|
+
setAssign(function (prev) {
|
|
878
|
+
var next = JSON.parse(JSON.stringify(prev));
|
|
879
|
+
next.roles[name].provider = dec.provider || null; // 成对写入;下拉模式不存在「只配 provider」状态
|
|
880
|
+
next.roles[name].model = dec.model || null;
|
|
881
|
+
return next;
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return h("div", { className: "omd-mainName" },
|
|
885
|
+
h("div", { className: "omd-glabel" }, "专用模型(可选;留空 = 跟随主 agent 当前模型)"),
|
|
886
|
+
h("div", { className: "omd-nameRow" },
|
|
887
|
+
h("select", {
|
|
888
|
+
key: "omd-roleModel-" + name, // 每角色独立实例;模式 / 选项集切换可整体重挂
|
|
889
|
+
className: "omd-nameInput",
|
|
890
|
+
value: sel.value,
|
|
891
|
+
onChange: function (e) { commitRoleModelOption(e.target.value); },
|
|
892
|
+
}, children)),
|
|
893
|
+
h("div", { className: "omd-hint" },
|
|
894
|
+
"选项来自 DSH 已注册的模型路由;留空 = 跟随主 agent,主 agent 换模型时未固定的角色跟着变。"));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function roleCard(name) {
|
|
898
|
+
var isMain = name === "__main__";
|
|
899
|
+
var display = isMain ? "主 agent" : name;
|
|
900
|
+
var isCustom = !isMain && DEFAULT_ROLES.indexOf(name) === -1;
|
|
901
|
+
var open = name in openMap ? openMap[name] : (isMain || name === "search_external");
|
|
902
|
+
|
|
903
|
+
var d = data;
|
|
904
|
+
var a = assign;
|
|
905
|
+
|
|
906
|
+
var head = h("div", { className: "omd-roleHeadWrap" },
|
|
907
|
+
h("button", {
|
|
908
|
+
type: "button",
|
|
909
|
+
className: "omd-roleHead",
|
|
910
|
+
"aria-expanded": open ? "true" : "false",
|
|
911
|
+
onClick: function () { toggleRole(name); },
|
|
912
|
+
},
|
|
913
|
+
h("span", { className: open ? "omd-chevron omd-chevronOpen" : "omd-chevron" }, ic(P.IconChevronDownOutline14, 14)),
|
|
914
|
+
h("span", { className: "omd-roleName" }, display),
|
|
915
|
+
h("span", { className: "omd-roleBadge" }, isMain ? "固定" : (isCustom ? "自定义" : "内置")),
|
|
916
|
+
h("span", { className: "omd-roleCount" },
|
|
917
|
+
(isMain ? ((data.restrictBase || []).length + a.main_agent_extra.length - (a.main_agent_remove || []).length) : a.roles[name].tools.length) + " 个工具")),
|
|
918
|
+
// 删除按钮:自定义角色走 removeRole(仅删 roles key);内置角色走
|
|
919
|
+
// removeBuiltinRole(删 key + roles_remove 记名 → 卡片消失、可恢复)。
|
|
920
|
+
// 主 agent(__main__)卡不可删。内置卡只渲染存在于 assign.roles 的,删后即隐。
|
|
921
|
+
!isMain
|
|
922
|
+
? h("button", {
|
|
923
|
+
type: "button",
|
|
924
|
+
className: "omd-roleDel",
|
|
925
|
+
title: isCustom ? '删除角色 "' + name + '"' : '删除内置角色 "' + name + '"(可在下方恢复)',
|
|
926
|
+
onClick: function (e) {
|
|
927
|
+
e.stopPropagation();
|
|
928
|
+
if (isCustom) removeRole(name);
|
|
929
|
+
else removeBuiltinRole(name);
|
|
930
|
+
},
|
|
931
|
+
}, ic(P.IconTrashOutline16, 16))
|
|
932
|
+
: null);
|
|
933
|
+
|
|
934
|
+
var body = [];
|
|
935
|
+
if (isMain) {
|
|
936
|
+
body.push(mainNameBlock());
|
|
937
|
+
body.push(h("div", { className: "omd-glabel" }, "基础工具(取消勾选即从主 agent 移除——写入 main_agent_remove)"));
|
|
938
|
+
body.push(h("div", { className: "omd-toolGrid" },
|
|
939
|
+
(data.restrictBase || []).map(function (t) {
|
|
940
|
+
return toolItem(t, "base", (a.main_agent_remove || []).indexOf(t) === -1, false,
|
|
941
|
+
function (e) { setMainRemove(t, e.target.checked); }, t);
|
|
942
|
+
})));
|
|
943
|
+
body.push(h("div", { className: "omd-glabel" }, "Host 工具(可加入主 agent)"));
|
|
944
|
+
body.push(h("div", { className: "omd-toolGrid" },
|
|
945
|
+
(data.inventory || []).length
|
|
946
|
+
? (data.inventory || []).map(function (t) {
|
|
947
|
+
return toolItem(t, "host", a.main_agent_extra.indexOf(t) !== -1, false,
|
|
948
|
+
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
949
|
+
})
|
|
950
|
+
: h("div", { className: "omd-empty" }, "未检测到 host 工具")));
|
|
951
|
+
body.push(h("div", { className: "omd-glabel" }, "技能分配(read 按需:勾选 = persona 写入技能行,主 agent 用 read 加载正文)"));
|
|
952
|
+
body.push((data.skills || []).length
|
|
953
|
+
? h("div", null,
|
|
954
|
+
h("div", { className: "omd-toolGrid" }, (data.skills || []).map(function (s) {
|
|
955
|
+
return toolItem(s, "read按需", a.main_agent_skills.indexOf(s) !== -1, false,
|
|
956
|
+
function (e) { setMainSkill(s, e.target.checked); }, s + "s");
|
|
957
|
+
})),
|
|
958
|
+
h("div", { className: "omd-glabel" }, "内联全文(硬生效,每轮固定 persona 开销,适合 caveman 等风格技能)"),
|
|
959
|
+
h("div", { className: "omd-toolGrid" }, (data.skills || []).map(function (s) {
|
|
960
|
+
return toolItem(s, "内联全文", a.main_agent_skills_inline.indexOf(s) !== -1, false,
|
|
961
|
+
function (e) { setMainSkillInline(s, e.target.checked); }, s + "i");
|
|
962
|
+
})))
|
|
963
|
+
: h("div", { className: "omd-empty" }, "未检测到技能目录"));
|
|
964
|
+
body.push(mainPersonaExtraBlock());
|
|
965
|
+
} else {
|
|
966
|
+
// 模型行位置:内置角色卡在「显示名」之后、工具面区之前;自定义角色卡
|
|
967
|
+
// 在 body 顶部(其无默认显示名可换,不加 builtinNameBlock)。两个场景
|
|
968
|
+
// 通用同一 roleModelBlock——下拉模式成对写 provider + model(不存在
|
|
969
|
+
// 「只配 provider」);目录不可用降级双 input 时才可能出现该状态(后端
|
|
970
|
+
// warn 并忽略)。留空 = 跟随主 agent 当前模型。
|
|
971
|
+
if (!isCustom) body.push(builtinNameBlock(name));
|
|
972
|
+
body.push(roleModelBlock(name));
|
|
973
|
+
var baseTools = roleCandidateTools(name).filter(function (t) { return !hostTool(t); });
|
|
974
|
+
var hostTools = roleCandidateTools(name).filter(function (t) { return hostTool(t); });
|
|
975
|
+
var checked = a.roles[name].tools;
|
|
976
|
+
body.push(h("div", { className: "omd-glabel" }, "基础工具"));
|
|
977
|
+
body.push(h("div", { className: "omd-toolGrid" },
|
|
978
|
+
baseTools.length
|
|
979
|
+
? baseTools.map(function (t) {
|
|
980
|
+
return toolItem(t, "base", checked.indexOf(t) !== -1, false,
|
|
981
|
+
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
982
|
+
})
|
|
983
|
+
: h("div", { className: "omd-empty" }, "无")));
|
|
984
|
+
body.push(h("div", { className: "omd-glabel" }, "Host 工具"));
|
|
985
|
+
body.push(h("div", { className: "omd-toolGrid" },
|
|
986
|
+
hostTools.length
|
|
987
|
+
? hostTools.map(function (t) {
|
|
988
|
+
return toolItem(t, "host", checked.indexOf(t) !== -1, false,
|
|
989
|
+
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
990
|
+
})
|
|
991
|
+
: h("div", { className: "omd-empty" }, "未检测到 host 工具")));
|
|
992
|
+
body.push(personaBlock(name));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return h("div", { className: "omd-roleCard", key: name },
|
|
996
|
+
head,
|
|
997
|
+
open ? h("div", { className: "omd-roleBody" }, body) : null);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function skillsBlock() {
|
|
1001
|
+
var rows = (data.skills || []).map(function (s) {
|
|
1002
|
+
var roleNames = Object.keys(assign.roles);
|
|
1003
|
+
var picked = assign.skills[s] || [];
|
|
1004
|
+
return h("div", { className: "omd-skillRow", key: s },
|
|
1005
|
+
h("span", { className: "omd-skillName", title: s }, s),
|
|
1006
|
+
h("div", { className: "omd-skillRoles" }, roleNames.map(function (r) {
|
|
1007
|
+
return h("label", { className: "omd-skillRole", key: r },
|
|
1008
|
+
h("input", {
|
|
1009
|
+
type: "checkbox",
|
|
1010
|
+
className: "omd-check",
|
|
1011
|
+
checked: picked.indexOf(r) !== -1,
|
|
1012
|
+
onChange: function (e) { setSkill(s, r, e.target.checked); },
|
|
1013
|
+
}),
|
|
1014
|
+
h("span", null, r));
|
|
1015
|
+
})));
|
|
1016
|
+
});
|
|
1017
|
+
return rows.length ? rows : h("div", { className: "omd-empty" }, "未检测到技能目录");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
var customRoles = Object.keys(assign.roles).filter(function (n) { return DEFAULT_ROLES.indexOf(n) === -1; });
|
|
1021
|
+
|
|
1022
|
+
var applyBtn = h(P.Button, {
|
|
1023
|
+
variant: "primary",
|
|
1024
|
+
size: "md",
|
|
1025
|
+
disabled: busy,
|
|
1026
|
+
onClick: doApply,
|
|
1027
|
+
icon: busyOp === "apply"
|
|
1028
|
+
? h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16))
|
|
1029
|
+
: ic(P.IconCheckOutline16, 16),
|
|
1030
|
+
}, busyOp === "apply" ? "处理中…" : "保存并应用");
|
|
1031
|
+
|
|
1032
|
+
var previewBtn = h(P.Button, {
|
|
1033
|
+
variant: "outline",
|
|
1034
|
+
size: "md",
|
|
1035
|
+
disabled: busy,
|
|
1036
|
+
onClick: doPreview,
|
|
1037
|
+
icon: busyOp === "preview"
|
|
1038
|
+
? h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16))
|
|
1039
|
+
: ic(P.IconCordisPluginOutline14, 14),
|
|
1040
|
+
}, busyOp === "preview" ? "生成中…" : "预览生成");
|
|
1041
|
+
|
|
1042
|
+
var rescanBtn = h(P.Button, {
|
|
1043
|
+
variant: "ghost",
|
|
1044
|
+
size: "md",
|
|
1045
|
+
disabled: busy,
|
|
1046
|
+
onClick: doRescan,
|
|
1047
|
+
icon: busyOp === "rescan"
|
|
1048
|
+
? h("span", { className: "omd-spin" }, ic(P.IconLoadingOutline16, 16))
|
|
1049
|
+
: ic(P.IconRefreshOutline16, 16),
|
|
1050
|
+
}, busyOp === "rescan" ? "检测中…" : "重新检测");
|
|
1051
|
+
|
|
1052
|
+
var pluginChips = data ? pluginChipsOf(data) : { primary: [], secondary: [] };
|
|
1053
|
+
|
|
1054
|
+
return h("div", { className: "omd-section" },
|
|
1055
|
+
h("p", { className: "omd-intro" }, INTRO),
|
|
1056
|
+
|
|
1057
|
+
// 状态与操作
|
|
1058
|
+
h("div", { className: "omd-group" },
|
|
1059
|
+
h("div", { className: "omd-groupHeading" },
|
|
1060
|
+
h("span", { className: "omd-groupIcon" }, ic(P.IconSettingsOutline16, 14)),
|
|
1061
|
+
h("span", null, "状态与操作")),
|
|
1062
|
+
statusBarOf(),
|
|
1063
|
+
runtimeNoteOf(data),
|
|
1064
|
+
h("div", { className: "omd-actions" }, applyBtn, previewBtn, rescanBtn),
|
|
1065
|
+
h("div", { className: "omd-chips" },
|
|
1066
|
+
h(P.Pill, null, "profile " + (data.effectiveProfile || "web")),
|
|
1067
|
+
(data.mcpReports || []).map(function (m) {
|
|
1068
|
+
return h(P.Pill, { key: m.serverName },
|
|
1069
|
+
"MCP " + m.serverName + " · " + m.count + " 工具",
|
|
1070
|
+
m.error ? h("span", { className: "omd-chipErrText" }, "(" + m.error + ")") : null);
|
|
1071
|
+
}),
|
|
1072
|
+
pluginChips.primary.map(function (p) {
|
|
1073
|
+
return h(P.Pill, { key: p.label }, chipTextOf(p));
|
|
1074
|
+
}),
|
|
1075
|
+
pluginChips.secondary.length
|
|
1076
|
+
? h("span", { className: "omd-chipSecondary" },
|
|
1077
|
+
"其他运行时插件:" + pluginChips.secondary.map(function (p) { return p.label; }).join(" · "))
|
|
1078
|
+
: null)),
|
|
1079
|
+
|
|
1080
|
+
// 角色与工具
|
|
1081
|
+
h("div", { className: "omd-group" },
|
|
1082
|
+
h("div", { className: "omd-groupHeading" },
|
|
1083
|
+
h("span", { className: "omd-groupIcon" }, ic(P.IconUserOutline16, 14)),
|
|
1084
|
+
h("span", null, "角色与工具"),
|
|
1085
|
+
h("span", { className: "omd-count" }, Object.keys(assign.roles).length)),
|
|
1086
|
+
roleCard("__main__"),
|
|
1087
|
+
// 内置角色卡只渲染 assign.roles 里存在的(roles_remove 中的已删名不重建、不渲染)
|
|
1088
|
+
DEFAULT_ROLES.filter(function (n) { return !!assign.roles[n]; }).map(function (n) { return roleCard(n); }),
|
|
1089
|
+
customRoles.map(function (n) { return roleCard(n); }),
|
|
1090
|
+
// 已删除的内置角色恢复区(仅删除过内置角色时出现)
|
|
1091
|
+
(assign.roles_remove || []).length
|
|
1092
|
+
? h("div", { className: "omd-restoreRow" },
|
|
1093
|
+
h("div", { className: "omd-restoreTitle" }, "已删除的内置角色"),
|
|
1094
|
+
h("div", { className: "omd-chips" }, (assign.roles_remove || []).map(function (n) {
|
|
1095
|
+
return h("button", {
|
|
1096
|
+
type: "button",
|
|
1097
|
+
key: n,
|
|
1098
|
+
className: "omd-restorePill",
|
|
1099
|
+
title: '恢复内置角色 "' + n + '"(工具面重建为内置默认 allow)',
|
|
1100
|
+
onClick: function () { restoreBuiltinRole(n); },
|
|
1101
|
+
}, "恢复 " + n);
|
|
1102
|
+
})),
|
|
1103
|
+
h("div", { className: "omd-hint" },
|
|
1104
|
+
"恢复后卡片回到上方内置列表;工具面取内置默认(不沿用删前自定义配置)。"))
|
|
1105
|
+
: null,
|
|
1106
|
+
h("button", { type: "button", className: "omd-addRole", onClick: addRole },
|
|
1107
|
+
h("span", { className: "omd-addRoleIcon" }, ic(P.IconPlusOutline16, 16)),
|
|
1108
|
+
h("span", null, "新建自定义 agent 角色"))),
|
|
1109
|
+
|
|
1110
|
+
// 技能分配
|
|
1111
|
+
h("div", { className: "omd-group" },
|
|
1112
|
+
h("div", { className: "omd-groupHeading" },
|
|
1113
|
+
h("span", { className: "omd-groupIcon" }, ic(P.IconSkillOutline16, 14)),
|
|
1114
|
+
h("span", null, "技能分配(软约束:写入对应 agent 的 persona 引导)"),
|
|
1115
|
+
h("span", { className: "omd-count" }, (data.skills || []).length)),
|
|
1116
|
+
skillsBlock()),
|
|
1117
|
+
|
|
1118
|
+
// 预览生成(预览成功后才出现)
|
|
1119
|
+
preview ? h("div", { className: "omd-group" },
|
|
1120
|
+
h("div", { className: "omd-groupHeading" },
|
|
1121
|
+
h("span", { className: "omd-groupIcon" }, ic(P.IconDataOutline16, 14)),
|
|
1122
|
+
h("span", null, "预览生成")),
|
|
1123
|
+
h("div", { className: "omd-previewSummary" },
|
|
1124
|
+
"✅ 生成成功(未写盘)" + (preview.roleResults || []).map(function (r) {
|
|
1125
|
+
return " · " + r.role + ": " + r.intent.length + " → " + r.kept.length;
|
|
1126
|
+
}).join("")),
|
|
1127
|
+
h("pre", { className: "omd-pre" }, preview.text)) : null);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// ── cordis client 插件:注册到设置页 settings.section 槽位 ──────────────
|
|
1131
|
+
//
|
|
1132
|
+
// register 是 "slots" service(SlotRegistry)的方法,不是 slots 包的静态导出;
|
|
1133
|
+
// 必须经 fiber inject 依赖 slots,并在 apply(ctx) 里通过 ctx.slots.inject()
|
|
1134
|
+
// 等待 settings.section 被声明后再注册(与 ui-settings-general 同款模式)。
|
|
1135
|
+
|
|
1136
|
+
var inject = ["slots"];
|
|
1137
|
+
|
|
1138
|
+
function apply(ctx) {
|
|
1139
|
+
ctx.slots.inject("settings.section", function () {
|
|
1140
|
+
return ctx.slots.register({
|
|
1141
|
+
name: "settings.section",
|
|
1142
|
+
id: "paoding",
|
|
1143
|
+
order: 50,
|
|
1144
|
+
label: function () { return "庖丁配置"; },
|
|
1145
|
+
}, ConfigPanel);
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
exports.inject = inject;
|
|
1150
|
+
exports.apply = apply;
|
|
1151
|
+
|
|
1152
|
+
return module.exports;
|
|
1153
|
+
},
|
|
1154
|
+
});
|