agent-bridge-abweb 1.0.12 → 1.0.16
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/dist/src-ts/agent.js +35 -0
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +110 -5
- package/dist/src-ts/plugins/abweb/public/app.ts +86 -5
- package/dist/src-ts/plugins/abweb/public/index.html +24 -1
- package/dist/src-ts/plugins/abweb/public/style.css +14 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
package/dist/src-ts/agent.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AgentOperationError = exports.AgentError = void 0;
|
|
4
|
+
exports.abortable = abortable;
|
|
4
5
|
exports.errorDetails = errorDetails;
|
|
6
|
+
async function abortable(promise, signal) {
|
|
7
|
+
if (!signal)
|
|
8
|
+
return promise;
|
|
9
|
+
if (signal.aborted) {
|
|
10
|
+
// promise 已在调用方启动,先观察其拒绝,避免预取消时产生未处理拒绝。
|
|
11
|
+
void promise.catch(() => undefined);
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let settled = false;
|
|
16
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
17
|
+
const abort = () => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
cleanup();
|
|
22
|
+
reject(signal.reason);
|
|
23
|
+
};
|
|
24
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
25
|
+
promise.then((value) => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
|
+
cleanup();
|
|
30
|
+
resolve(value);
|
|
31
|
+
}, (error) => {
|
|
32
|
+
if (settled)
|
|
33
|
+
return;
|
|
34
|
+
settled = true;
|
|
35
|
+
cleanup();
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
5
40
|
class AgentError extends Error {
|
|
6
41
|
constructor(message, options) {
|
|
7
42
|
super(message, options);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
const logsNode = node("#logs");
|
|
7
7
|
const logsEmpty = node("#logs-empty");
|
|
8
8
|
const logsPageNode = node("#logs-page");
|
|
9
|
+
const logsLevelNode = node("#logs-level");
|
|
10
|
+
const logsQueryNode = node("#logs-query");
|
|
9
11
|
const errorNode = node("#error");
|
|
10
12
|
const pairCodeNode = node("#pair-code");
|
|
11
13
|
const packageSearchNode = node("#package-search");
|
|
@@ -34,6 +36,7 @@
|
|
|
34
36
|
let logHasNext = false;
|
|
35
37
|
let loading = false;
|
|
36
38
|
const installingPackages = new Set();
|
|
39
|
+
let assistantOriginal = {};
|
|
37
40
|
function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
38
41
|
function showResult(title, message, failed) {
|
|
39
42
|
node("#operation-result-title").textContent = title;
|
|
@@ -76,7 +79,7 @@
|
|
|
76
79
|
head.append(headRow);
|
|
77
80
|
const body = document.createElement("tbody");
|
|
78
81
|
for (const plugin of items) {
|
|
79
|
-
const installed = plugins.find((item) => item.id === plugin.name.replace(/^agent-bridge-/, ""));
|
|
82
|
+
const installed = plugins.find((item) => item.id === (plugin.id ?? plugin.name.replace(/^agent-bridge-/, "")));
|
|
80
83
|
const versionStatus = installed ? pluginVersions[installed.id] : undefined;
|
|
81
84
|
const isUpgrade = Boolean(installed && versionStatus?.packageName === plugin.name && newerVersion(plugin.version, installed.version));
|
|
82
85
|
const row = document.createElement("tr");
|
|
@@ -218,7 +221,8 @@
|
|
|
218
221
|
versionsNode.replaceChildren(...entries.map((entry) => {
|
|
219
222
|
const row = document.createElement("div");
|
|
220
223
|
row.className = "version-row";
|
|
221
|
-
|
|
224
|
+
const state = entry.state === "unpublished" ? "未发布" : entry.state;
|
|
225
|
+
row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${state}${entry.error ? ` · ${entry.error}` : ""}`;
|
|
222
226
|
return row;
|
|
223
227
|
}));
|
|
224
228
|
if (!entries.length)
|
|
@@ -344,6 +348,8 @@
|
|
|
344
348
|
controls.className = "controls";
|
|
345
349
|
if (plugin.manifest?.executionMode === "cli")
|
|
346
350
|
controls.append(button("配置", `配置 ${plugin.id}`, () => configureCli(plugin)));
|
|
351
|
+
if (plugin.id === "personal-assistant")
|
|
352
|
+
controls.append(button("配置模型", "配置个人助理模型来源", () => configureAssistant(plugin)));
|
|
347
353
|
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
|
|
348
354
|
controlsUpgrade(plugin.id);
|
|
349
355
|
controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); }));
|
|
@@ -401,6 +407,96 @@
|
|
|
401
407
|
detail.hidden = false;
|
|
402
408
|
}).finally(() => { save.disabled = false; });
|
|
403
409
|
};
|
|
410
|
+
node("#assistant-config-cancel").onclick = () => node("#assistant-config-dialog").close();
|
|
411
|
+
async function configureAssistant(plugin) {
|
|
412
|
+
const [config, sourceResult] = await Promise.all([
|
|
413
|
+
request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`),
|
|
414
|
+
request("/api/model-providers"),
|
|
415
|
+
]);
|
|
416
|
+
assistantOriginal = config;
|
|
417
|
+
const select = node("#assistant-model-provider");
|
|
418
|
+
select.replaceChildren();
|
|
419
|
+
const add = (value, label, disabled = false) => { const option = document.createElement("option"); option.value = value; option.textContent = label; option.disabled = disabled; select.append(option); };
|
|
420
|
+
add("openai", "OpenAI 兼容接口");
|
|
421
|
+
if (config.modelProvider === "claude-cli")
|
|
422
|
+
add("claude-cli", "Claude CLI(原有配置,用户默认模型)");
|
|
423
|
+
for (const provider of sourceResult.providers ?? [])
|
|
424
|
+
add(`plugin:${provider.id}`, `${provider.displayName || provider.id}${provider.available ? "" : `(不可用:${provider.reason || provider.status})`}`, !provider.available);
|
|
425
|
+
if (config.modelProvider === "plugin" && config.modelPlugin && !(sourceResult.providers ?? []).some((item) => item.id === config.modelPlugin))
|
|
426
|
+
add(`plugin:${config.modelPlugin}`, `${config.modelPlugin}(未安装或未声明模型来源)`, true);
|
|
427
|
+
select.value = config.modelProvider === "plugin" && config.modelPlugin ? `plugin:${config.modelPlugin}` : config.modelProvider ?? "openai";
|
|
428
|
+
node("#assistant-model").value = config.model ?? "";
|
|
429
|
+
node("#assistant-endpoint").value = config.endpoint ?? "";
|
|
430
|
+
node("#assistant-api-key").value = "";
|
|
431
|
+
node("#assistant-api-key").placeholder = config.apiKey ? "已配置,留空保留" : "可选,本地免认证服务可留空";
|
|
432
|
+
node("#assistant-timeout").value = String(config.timeoutMs ?? 120000);
|
|
433
|
+
node("#assistant-allowed-peers").value = config.allowedPeers?.includes("*") ? "" : (config.allowedPeers ?? []).join("\n");
|
|
434
|
+
node("#assistant-allowed-agents").value = config.allowedAgents?.includes("*") ? "" : (config.allowedAgents ?? []).join("\n");
|
|
435
|
+
node("#assistant-skills-enabled").checked = config.skills?.enabled ?? true;
|
|
436
|
+
node("#assistant-skills-learn-mode").value = config.skills?.learnMode ?? "draft";
|
|
437
|
+
node("#assistant-skills-read-dirs").value = (config.skills?.readDirs ?? []).join("\n");
|
|
438
|
+
node("#assistant-skills-share-dir").value = config.skills?.shareDir ?? "";
|
|
439
|
+
node("#assistant-memory-mode").value = config.memory?.mode ?? "stated";
|
|
440
|
+
node("#assistant-config-error").hidden = true;
|
|
441
|
+
node("#assistant-config-dialog").showModal();
|
|
442
|
+
}
|
|
443
|
+
node("#assistant-config-form").onsubmit = event => {
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
const save = node("#assistant-config-save");
|
|
446
|
+
save.disabled = true;
|
|
447
|
+
const source = node("#assistant-model-provider").value;
|
|
448
|
+
const provider = source.startsWith("plugin:") ? "plugin" : source;
|
|
449
|
+
const modelPlugin = source.startsWith("plugin:") ? source.slice("plugin:".length) : undefined;
|
|
450
|
+
const body = {};
|
|
451
|
+
if (provider !== (assistantOriginal.modelProvider ?? "openai"))
|
|
452
|
+
body.modelProvider = provider;
|
|
453
|
+
if (provider === "plugin" && modelPlugin !== assistantOriginal.modelPlugin)
|
|
454
|
+
body.modelPlugin = modelPlugin;
|
|
455
|
+
const model = node("#assistant-model").value.trim();
|
|
456
|
+
const endpoint = node("#assistant-endpoint").value.trim();
|
|
457
|
+
const apiKey = node("#assistant-api-key").value;
|
|
458
|
+
if (model && model !== String(assistantOriginal.model ?? ""))
|
|
459
|
+
body.model = model;
|
|
460
|
+
if (endpoint && endpoint !== String(assistantOriginal.endpoint ?? ""))
|
|
461
|
+
body.endpoint = endpoint;
|
|
462
|
+
if (apiKey.trim())
|
|
463
|
+
body.apiKey = apiKey.trim();
|
|
464
|
+
const timeoutMs = Number(node("#assistant-timeout").value);
|
|
465
|
+
if (timeoutMs !== Number(assistantOriginal.timeoutMs ?? 120000))
|
|
466
|
+
body.timeoutMs = timeoutMs;
|
|
467
|
+
for (const [field, selector] of [["allowedPeers", "#assistant-allowed-peers"], ["allowedAgents", "#assistant-allowed-agents"]]) {
|
|
468
|
+
const entries = [...new Set(node(selector).value.split(/[\s,,]+/).filter(Boolean))];
|
|
469
|
+
const values = entries.includes("*") ? [] : entries;
|
|
470
|
+
if (JSON.stringify(values) !== JSON.stringify(assistantOriginal[field]))
|
|
471
|
+
body[field] = values;
|
|
472
|
+
}
|
|
473
|
+
const previousSkills = assistantOriginal.skills;
|
|
474
|
+
const shareDir = node("#assistant-skills-share-dir").value.trim();
|
|
475
|
+
const shareDirChanged = shareDir !== (previousSkills?.shareDir ?? "");
|
|
476
|
+
const skills = {
|
|
477
|
+
...previousSkills,
|
|
478
|
+
enabled: node("#assistant-skills-enabled").checked,
|
|
479
|
+
learnMode: node("#assistant-skills-learn-mode").value,
|
|
480
|
+
readDirs: node("#assistant-skills-read-dirs").value.split(/\r?\n/).map(value => value.trim()).filter(Boolean),
|
|
481
|
+
...(shareDirChanged ? { shareDir } : {}),
|
|
482
|
+
};
|
|
483
|
+
if (skills.enabled !== (previousSkills?.enabled ?? true) || skills.learnMode !== (previousSkills?.learnMode ?? "draft") || JSON.stringify(skills.readDirs) !== JSON.stringify(previousSkills?.readDirs ?? []) || shareDirChanged)
|
|
484
|
+
body.skills = skills;
|
|
485
|
+
const previousMemory = assistantOriginal.memory;
|
|
486
|
+
const mode = node("#assistant-memory-mode").value;
|
|
487
|
+
if (mode !== (previousMemory?.mode ?? "stated"))
|
|
488
|
+
body.memory = { ...previousMemory, mode };
|
|
489
|
+
if (!Object.keys(body).length) {
|
|
490
|
+
node("#assistant-config-dialog").close();
|
|
491
|
+
save.disabled = false;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
void request("/api/plugins/personal-assistant/config", "PATCH", body).then(result => {
|
|
495
|
+
node("#assistant-config-dialog").close();
|
|
496
|
+
showResult("个人助理配置已保存", result.restartRequired || result.action === "restart" ? "配置已保存,需要重启后台后生效。" : "个人助理配置已更新。", false);
|
|
497
|
+
return load(false);
|
|
498
|
+
}).catch(cause => { const detail = node("#assistant-config-error"); detail.textContent = String(cause); detail.hidden = false; }).finally(() => { save.disabled = false; });
|
|
499
|
+
};
|
|
404
500
|
function renderMessages(messages) {
|
|
405
501
|
messagesNode.replaceChildren();
|
|
406
502
|
messagesEmpty.hidden = messages.length > 0;
|
|
@@ -432,7 +528,12 @@
|
|
|
432
528
|
logsNode.append(row);
|
|
433
529
|
}
|
|
434
530
|
}
|
|
435
|
-
async function loadLogs(page = logPage) {
|
|
531
|
+
async function loadLogs(page = logPage) {
|
|
532
|
+
const params = new URLSearchParams({ page: String(page), pageSize: "100", level: logsLevelNode.value });
|
|
533
|
+
if (logsQueryNode.value.trim())
|
|
534
|
+
params.set("q", logsQueryNode.value.trim());
|
|
535
|
+
renderLogs(await request(`/api/logs?${params}`));
|
|
536
|
+
}
|
|
436
537
|
for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
437
538
|
node(`#${tabId}`).onclick = () => {
|
|
438
539
|
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
@@ -445,6 +546,10 @@
|
|
|
445
546
|
};
|
|
446
547
|
}
|
|
447
548
|
node("#logs-refresh").onclick = () => void loadLogs().catch((cause) => error(String(cause)));
|
|
549
|
+
node("#logs-search").onclick = () => void loadLogs(1).catch((cause) => error(String(cause)));
|
|
550
|
+
logsLevelNode.onchange = () => void loadLogs(1).catch((cause) => error(String(cause)));
|
|
551
|
+
logsQueryNode.onkeydown = event => { if (event.key === "Enter")
|
|
552
|
+
void loadLogs(1).catch((cause) => error(String(cause))); };
|
|
448
553
|
node("#logs-prev").onclick = () => { if (logPage > 1)
|
|
449
554
|
void loadLogs(logPage - 1).catch((cause) => error(String(cause))); };
|
|
450
555
|
node("#logs-next").onclick = () => { if (logHasNext)
|
|
@@ -453,8 +558,8 @@
|
|
|
453
558
|
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
454
559
|
node("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
455
560
|
node("#save-plugin-trust").onclick = () => { const button = node("#save-plugin-trust"); button.disabled = true; trustStatusNode.textContent = "正在保存…"; void request("/api/plugin-trust", "PATCH", pluginTrustConfig).then(() => { pluginTrustDirty = false; trustStatusNode.textContent = "信任设置已保存"; return load(false); }).catch((cause) => { trustStatusNode.textContent = String(cause); }).finally(() => { button.disabled = false; }); };
|
|
456
|
-
node("#update-core").onclick = () => { if (confirm("
|
|
457
|
-
void request("/api/update", "POST", {}).then(() => { error("
|
|
561
|
+
node("#update-core").onclick = () => { if (confirm("确认检查后台更新?有更高版本时将升级并自动重启"))
|
|
562
|
+
void request("/api/update", "POST", {}).then(() => { error("后台正在检查更新,有更高版本时升级并重启;结果请查看运行日志"); }).catch((cause) => error(String(cause))); };
|
|
458
563
|
node("#pair-button").onclick = () => {
|
|
459
564
|
const code = pairCodeNode.value.trim();
|
|
460
565
|
if (!/^\d{6}$/.test(code))
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
const logsNode = node<HTMLTableSectionElement>("#logs");
|
|
8
8
|
const logsEmpty = node<HTMLParagraphElement>("#logs-empty");
|
|
9
9
|
const logsPageNode = node<HTMLSpanElement>("#logs-page");
|
|
10
|
+
const logsLevelNode = node<HTMLSelectElement>("#logs-level");
|
|
11
|
+
const logsQueryNode = node<HTMLInputElement>("#logs-query");
|
|
10
12
|
const errorNode = node<HTMLParagraphElement>("#error");
|
|
11
13
|
const pairCodeNode = node<HTMLInputElement>("#pair-code");
|
|
12
14
|
const packageSearchNode = node<HTMLInputElement>("#package-search");
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
let logHasNext = false;
|
|
33
35
|
let loading = false;
|
|
34
36
|
const installingPackages = new Set<string>();
|
|
37
|
+
let assistantOriginal: Record<string, unknown> = {};
|
|
35
38
|
|
|
36
39
|
function error(message: string): void { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
37
40
|
function showResult(title: string, message: string, failed: boolean): void {
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
});
|
|
52
55
|
async function loadCatalog(): Promise<void> {
|
|
53
56
|
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
54
|
-
const items: Array<{ name: string; version: string; description: string; type: string }> = result.plugins ?? [];
|
|
57
|
+
const items: Array<{ id?: string; name: string; version: string; description: string; type: string }> = result.plugins ?? [];
|
|
55
58
|
catalogNode.replaceChildren();
|
|
56
59
|
if (!items.length) { catalogNode.textContent = "未找到已声明类型的 npm 插件"; return; }
|
|
57
60
|
const typeNames: Record<string, string> = { agent: "Agent", channel: "通道", management: "管理" };
|
|
@@ -62,7 +65,7 @@
|
|
|
62
65
|
head.append(headRow);
|
|
63
66
|
const body = document.createElement("tbody");
|
|
64
67
|
for (const plugin of items) {
|
|
65
|
-
const installed = plugins.find((item) => item.id === plugin.name.replace(/^agent-bridge-/, ""));
|
|
68
|
+
const installed = plugins.find((item) => item.id === (plugin.id ?? plugin.name.replace(/^agent-bridge-/, "")));
|
|
66
69
|
const versionStatus = installed ? pluginVersions[installed.id] : undefined;
|
|
67
70
|
const isUpgrade = Boolean(installed && versionStatus?.packageName === plugin.name && newerVersion(plugin.version, installed.version));
|
|
68
71
|
const row = document.createElement("tr");
|
|
@@ -149,7 +152,8 @@
|
|
|
149
152
|
const entries = [data.core, ...(data.plugins ?? []).map((plugin: any) => plugin.versionStatus)].filter(Boolean);
|
|
150
153
|
versionsNode.replaceChildren(...entries.map((entry: any) => {
|
|
151
154
|
const row = document.createElement("div"); row.className = "version-row";
|
|
152
|
-
|
|
155
|
+
const state = entry.state === "unpublished" ? "未发布" : entry.state;
|
|
156
|
+
row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${state}${entry.error ? ` · ${entry.error}` : ""}`;
|
|
153
157
|
return row;
|
|
154
158
|
}));
|
|
155
159
|
if (!entries.length) versionsNode.textContent = "尚未检查版本";
|
|
@@ -207,6 +211,7 @@
|
|
|
207
211
|
}
|
|
208
212
|
const controls = document.createElement("td"); controls.className = "controls";
|
|
209
213
|
if (plugin.manifest?.executionMode === "cli") controls.append(button("配置", `配置 ${plugin.id}`, () => configureCli(plugin)));
|
|
214
|
+
if (plugin.id === "personal-assistant") controls.append(button("配置模型", "配置个人助理模型来源", () => configureAssistant(plugin)));
|
|
210
215
|
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) { controlsUpgrade(plugin.id); controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); })); }
|
|
211
216
|
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
212
217
|
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
@@ -246,6 +251,75 @@
|
|
|
246
251
|
const detail = node<HTMLElement>("#cli-config-error"); detail.textContent = String(cause); detail.hidden = false;
|
|
247
252
|
}).finally(() => { save.disabled = false; });
|
|
248
253
|
};
|
|
254
|
+
node<HTMLButtonElement>("#assistant-config-cancel").onclick = () => node<HTMLDialogElement>("#assistant-config-dialog").close();
|
|
255
|
+
async function configureAssistant(plugin: Plugin): Promise<void> {
|
|
256
|
+
const [config, sourceResult] = await Promise.all([
|
|
257
|
+
request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`),
|
|
258
|
+
request("/api/model-providers"),
|
|
259
|
+
]);
|
|
260
|
+
assistantOriginal = config;
|
|
261
|
+
const select = node<HTMLSelectElement>("#assistant-model-provider");
|
|
262
|
+
select.replaceChildren();
|
|
263
|
+
const add = (value: string, label: string, disabled = false) => { const option = document.createElement("option"); option.value = value; option.textContent = label; option.disabled = disabled; select.append(option); };
|
|
264
|
+
add("openai", "OpenAI 兼容接口");
|
|
265
|
+
if (config.modelProvider === "claude-cli") add("claude-cli", "Claude CLI(原有配置,用户默认模型)");
|
|
266
|
+
for (const provider of sourceResult.providers ?? []) add(`plugin:${provider.id}`, `${provider.displayName || provider.id}${provider.available ? "" : `(不可用:${provider.reason || provider.status})`}`, !provider.available);
|
|
267
|
+
if (config.modelProvider === "plugin" && config.modelPlugin && !(sourceResult.providers ?? []).some((item: any) => item.id === config.modelPlugin)) add(`plugin:${config.modelPlugin}`, `${config.modelPlugin}(未安装或未声明模型来源)`, true);
|
|
268
|
+
select.value = config.modelProvider === "plugin" && config.modelPlugin ? `plugin:${config.modelPlugin}` : config.modelProvider ?? "openai";
|
|
269
|
+
node<HTMLInputElement>("#assistant-model").value = config.model ?? "";
|
|
270
|
+
node<HTMLInputElement>("#assistant-endpoint").value = config.endpoint ?? "";
|
|
271
|
+
node<HTMLInputElement>("#assistant-api-key").value = "";
|
|
272
|
+
node<HTMLInputElement>("#assistant-api-key").placeholder = config.apiKey ? "已配置,留空保留" : "可选,本地免认证服务可留空";
|
|
273
|
+
node<HTMLInputElement>("#assistant-timeout").value = String(config.timeoutMs ?? 120000);
|
|
274
|
+
node<HTMLTextAreaElement>("#assistant-allowed-peers").value = config.allowedPeers?.includes("*") ? "" : (config.allowedPeers ?? []).join("\n");
|
|
275
|
+
node<HTMLTextAreaElement>("#assistant-allowed-agents").value = config.allowedAgents?.includes("*") ? "" : (config.allowedAgents ?? []).join("\n");
|
|
276
|
+
node<HTMLInputElement>("#assistant-skills-enabled").checked = config.skills?.enabled ?? true;
|
|
277
|
+
node<HTMLSelectElement>("#assistant-skills-learn-mode").value = config.skills?.learnMode ?? "draft";
|
|
278
|
+
node<HTMLTextAreaElement>("#assistant-skills-read-dirs").value = (config.skills?.readDirs ?? []).join("\n");
|
|
279
|
+
node<HTMLInputElement>("#assistant-skills-share-dir").value = config.skills?.shareDir ?? "";
|
|
280
|
+
node<HTMLSelectElement>("#assistant-memory-mode").value = config.memory?.mode ?? "stated";
|
|
281
|
+
node<HTMLElement>("#assistant-config-error").hidden = true;
|
|
282
|
+
node<HTMLDialogElement>("#assistant-config-dialog").showModal();
|
|
283
|
+
}
|
|
284
|
+
node<HTMLFormElement>("#assistant-config-form").onsubmit = event => {
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
const save = node<HTMLButtonElement>("#assistant-config-save"); save.disabled = true;
|
|
287
|
+
const source = node<HTMLSelectElement>("#assistant-model-provider").value;
|
|
288
|
+
const provider = source.startsWith("plugin:") ? "plugin" : source;
|
|
289
|
+
const modelPlugin = source.startsWith("plugin:") ? source.slice("plugin:".length) : undefined;
|
|
290
|
+
const body: Record<string, unknown> = {};
|
|
291
|
+
if (provider !== (assistantOriginal.modelProvider ?? "openai")) body.modelProvider = provider;
|
|
292
|
+
if (provider === "plugin" && modelPlugin !== assistantOriginal.modelPlugin) body.modelPlugin = modelPlugin;
|
|
293
|
+
const model = node<HTMLInputElement>("#assistant-model").value.trim(); const endpoint = node<HTMLInputElement>("#assistant-endpoint").value.trim(); const apiKey = node<HTMLInputElement>("#assistant-api-key").value;
|
|
294
|
+
if (model && model !== String(assistantOriginal.model ?? "")) body.model = model;
|
|
295
|
+
if (endpoint && endpoint !== String(assistantOriginal.endpoint ?? "")) body.endpoint = endpoint;
|
|
296
|
+
if (apiKey.trim()) body.apiKey = apiKey.trim();
|
|
297
|
+
const timeoutMs = Number(node<HTMLInputElement>("#assistant-timeout").value);
|
|
298
|
+
if (timeoutMs !== Number(assistantOriginal.timeoutMs ?? 120000)) body.timeoutMs = timeoutMs;
|
|
299
|
+
for (const [field, selector] of [["allowedPeers", "#assistant-allowed-peers"], ["allowedAgents", "#assistant-allowed-agents"]]) {
|
|
300
|
+
const entries = [...new Set(node<HTMLTextAreaElement>(selector).value.split(/[\s,,]+/).filter(Boolean))];
|
|
301
|
+
const values = entries.includes("*") ? [] : entries;
|
|
302
|
+
if (JSON.stringify(values) !== JSON.stringify(assistantOriginal[field])) body[field] = values;
|
|
303
|
+
}
|
|
304
|
+
const previousSkills = assistantOriginal.skills as { enabled?: boolean; learnMode?: string; readDirs?: string[]; shareDir?: string } | undefined;
|
|
305
|
+
const shareDir = node<HTMLInputElement>("#assistant-skills-share-dir").value.trim();
|
|
306
|
+
const shareDirChanged = shareDir !== (previousSkills?.shareDir ?? "");
|
|
307
|
+
const skills = {
|
|
308
|
+
...previousSkills,
|
|
309
|
+
enabled: node<HTMLInputElement>("#assistant-skills-enabled").checked,
|
|
310
|
+
learnMode: node<HTMLSelectElement>("#assistant-skills-learn-mode").value,
|
|
311
|
+
readDirs: node<HTMLTextAreaElement>("#assistant-skills-read-dirs").value.split(/\r?\n/).map(value => value.trim()).filter(Boolean),
|
|
312
|
+
...(shareDirChanged ? { shareDir } : {}),
|
|
313
|
+
};
|
|
314
|
+
if (skills.enabled !== (previousSkills?.enabled ?? true) || skills.learnMode !== (previousSkills?.learnMode ?? "draft") || JSON.stringify(skills.readDirs) !== JSON.stringify(previousSkills?.readDirs ?? []) || shareDirChanged) body.skills = skills;
|
|
315
|
+
const previousMemory = assistantOriginal.memory as { mode?: string } | undefined;
|
|
316
|
+
const mode = node<HTMLSelectElement>("#assistant-memory-mode").value;
|
|
317
|
+
if (mode !== (previousMemory?.mode ?? "stated")) body.memory = { ...previousMemory, mode };
|
|
318
|
+
if (!Object.keys(body).length) { node<HTMLDialogElement>("#assistant-config-dialog").close(); save.disabled = false; return; }
|
|
319
|
+
void request("/api/plugins/personal-assistant/config", "PATCH", body).then(result => {
|
|
320
|
+
node<HTMLDialogElement>("#assistant-config-dialog").close(); showResult("个人助理配置已保存", result.restartRequired || result.action === "restart" ? "配置已保存,需要重启后台后生效。" : "个人助理配置已更新。", false); return load(false);
|
|
321
|
+
}).catch(cause => { const detail = node<HTMLElement>("#assistant-config-error"); detail.textContent = String(cause); detail.hidden = false; }).finally(() => { save.disabled = false; });
|
|
322
|
+
};
|
|
249
323
|
function renderMessages(messages: Array<{ time: string; pluginId: string; direction: "request" | "reply" | "error"; text: string; peer?: string; agentAid?: string }>): void {
|
|
250
324
|
messagesNode.replaceChildren();
|
|
251
325
|
messagesEmpty.hidden = messages.length > 0;
|
|
@@ -262,7 +336,11 @@
|
|
|
262
336
|
logsNode.replaceChildren(); logsEmpty.hidden = data.logs.length > 0; logPage = data.page; logHasNext = data.hasNext; logsPageNode.textContent = `第 ${data.page} 页`;
|
|
263
337
|
for (const log of data.logs) { const row = document.createElement("tr"); for (const value of [new Date(log.time).toLocaleString(), log.level, log.source, log.message, log.fields ? JSON.stringify(log.fields) : ""]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); } logsNode.append(row); }
|
|
264
338
|
}
|
|
265
|
-
async function loadLogs(page = logPage): Promise<void> {
|
|
339
|
+
async function loadLogs(page = logPage): Promise<void> {
|
|
340
|
+
const params = new URLSearchParams({ page: String(page), pageSize: "100", level: logsLevelNode.value });
|
|
341
|
+
if (logsQueryNode.value.trim()) params.set("q", logsQueryNode.value.trim());
|
|
342
|
+
renderLogs(await request(`/api/logs?${params}`));
|
|
343
|
+
}
|
|
266
344
|
for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
267
345
|
node<HTMLButtonElement>(`#${tabId}`).onclick = () => {
|
|
268
346
|
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
@@ -274,13 +352,16 @@
|
|
|
274
352
|
};
|
|
275
353
|
}
|
|
276
354
|
node<HTMLButtonElement>("#logs-refresh").onclick = () => void loadLogs().catch((cause) => error(String(cause)));
|
|
355
|
+
node<HTMLButtonElement>("#logs-search").onclick = () => void loadLogs(1).catch((cause) => error(String(cause)));
|
|
356
|
+
logsLevelNode.onchange = () => void loadLogs(1).catch((cause) => error(String(cause)));
|
|
357
|
+
logsQueryNode.onkeydown = event => { if (event.key === "Enter") void loadLogs(1).catch((cause) => error(String(cause))); };
|
|
277
358
|
node<HTMLButtonElement>("#logs-prev").onclick = () => { if (logPage > 1) void loadLogs(logPage - 1).catch((cause) => error(String(cause))); };
|
|
278
359
|
node<HTMLButtonElement>("#logs-next").onclick = () => { if (logHasNext) void loadLogs(logPage + 1).catch((cause) => error(String(cause))); };
|
|
279
360
|
node<HTMLButtonElement>("#refresh").onclick = () => void load();
|
|
280
361
|
node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
281
362
|
node<HTMLButtonElement>("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
282
363
|
node<HTMLButtonElement>("#save-plugin-trust").onclick = () => { const button = node<HTMLButtonElement>("#save-plugin-trust"); button.disabled = true; trustStatusNode.textContent = "正在保存…"; void request("/api/plugin-trust", "PATCH", pluginTrustConfig).then(() => { pluginTrustDirty = false; trustStatusNode.textContent = "信任设置已保存"; return load(false); }).catch((cause) => { trustStatusNode.textContent = String(cause); }).finally(() => { button.disabled = false; }); };
|
|
283
|
-
node<HTMLButtonElement>("#update-core").onclick = () => { if (confirm("
|
|
364
|
+
node<HTMLButtonElement>("#update-core").onclick = () => { if (confirm("确认检查后台更新?有更高版本时将升级并自动重启")) void request("/api/update", "POST", {}).then(() => { error("后台正在检查更新,有更高版本时升级并重启;结果请查看运行日志"); }).catch((cause) => error(String(cause))); };
|
|
284
365
|
node<HTMLButtonElement>("#pair-button").onclick = () => {
|
|
285
366
|
const code = pairCodeNode.value.trim();
|
|
286
367
|
if (!/^\d{6}$/.test(code)) return error("请输入 6 位配置码");
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<section><div class="section-heading"><h2>Channel 设置</h2></div><nav id="channel-tabs" class="tabs channel-tabs" role="tablist" aria-label="Channel 设置"></nav><div id="channel-panel" role="tabpanel" aria-live="polite"></div></section>
|
|
19
19
|
</div>
|
|
20
20
|
<div id="messages-panel" role="tabpanel" aria-labelledby="messages-tab" hidden><section><div class="section-heading"><h2>最近通信</h2></div><div class="table-scroll"><table><thead><tr><th scope="col">时间</th><th scope="col">插件</th><th scope="col">接收 AID</th><th scope="col">来源 Peer</th><th scope="col">方向</th><th scope="col">消息</th><th scope="col">错误</th></tr></thead><tbody id="messages"></tbody></table></div><p id="messages-empty" class="muted" hidden>暂无通信记录</p><h2>通道会话</h2><div class="table-scroll"><table><thead><tr><th>来源</th><th>Agent</th><th>会话 ID</th><th>最近活跃时间</th></tr></thead><tbody id="sessions"></tbody></table></div></section></div>
|
|
21
|
-
<div id="logs-panel" role="tabpanel" aria-labelledby="logs-tab" hidden><section><div class="section-heading"><h2>运行日志</h2><button id="logs-refresh" type="button">刷新日志</button></div><div class="table-scroll"><table><thead><tr><th>时间</th><th>级别</th><th>来源</th><th>消息</th><th>详细字段</th></tr></thead><tbody id="logs"></tbody></table></div><p id="logs-empty" class="muted" hidden>暂无运行日志</p><div class="section-heading"><button id="logs-prev" type="button">上一页</button><span id="logs-page" class="muted">第 1 页</span><button id="logs-next" type="button">下一页</button></div></section></div>
|
|
21
|
+
<div id="logs-panel" role="tabpanel" aria-labelledby="logs-tab" hidden><section><div class="section-heading"><h2>运行日志</h2><button id="logs-refresh" type="button">刷新日志</button></div><div class="log-filters"><label for="logs-level">最低级别</label><select id="logs-level"><option value="info">Info 及以上</option><option value="warn">Warn 及以上</option><option value="error">仅 Error</option></select><label for="logs-query">内容</label><input id="logs-query" type="search" placeholder="搜索来源、消息或详细字段"><button id="logs-search" type="button">筛选</button></div><div class="table-scroll"><table class="logs-table"><thead><tr><th>时间</th><th>级别</th><th>来源</th><th>消息</th><th>详细字段</th></tr></thead><tbody id="logs"></tbody></table></div><p id="logs-empty" class="muted" hidden>暂无运行日志</p><div class="section-heading"><button id="logs-prev" type="button">上一页</button><span id="logs-page" class="muted">第 1 页</span><button id="logs-next" type="button">下一页</button></div></section></div>
|
|
22
22
|
<p id="error" class="error" role="alert" hidden></p>
|
|
23
23
|
</main>
|
|
24
24
|
<dialog id="cli-config-dialog" aria-labelledby="cli-config-title">
|
|
@@ -31,6 +31,29 @@
|
|
|
31
31
|
<div><button id="cli-config-save" type="submit">保存配置</button><button id="cli-config-cancel" type="button">取消</button></div>
|
|
32
32
|
</form>
|
|
33
33
|
</dialog>
|
|
34
|
+
<dialog id="assistant-config-dialog" aria-labelledby="assistant-config-title">
|
|
35
|
+
<h2 id="assistant-config-title">个人助理配置</h2>
|
|
36
|
+
<form id="assistant-config-form">
|
|
37
|
+
<label for="assistant-model-provider">模型来源</label><select id="assistant-model-provider" required></select>
|
|
38
|
+
<label for="assistant-model">OpenAI 兼容模型名称</label><input id="assistant-model" placeholder="仅 HTTP 来源需要">
|
|
39
|
+
<label for="assistant-endpoint">OpenAI 兼容地址</label><input id="assistant-endpoint" type="url" placeholder="http://127.0.0.1:11434/v1/chat/completions">
|
|
40
|
+
<label for="assistant-api-key">API 密钥(留空保留已有密钥)</label><input id="assistant-api-key" type="password" autocomplete="new-password">
|
|
41
|
+
<label for="assistant-timeout">模型超时(毫秒)</label><input id="assistant-timeout" type="number" min="1000" max="120000" required>
|
|
42
|
+
<label for="assistant-allowed-peers">允许使用助理的 Peer AID(每行一个,留空接受所有)</label><textarea id="assistant-allowed-peers" rows="3" placeholder="留空接受所有 Peer"></textarea>
|
|
43
|
+
<label for="assistant-allowed-agents">允许调用的 Agent 插件 ID(每行一个,留空允许所有)</label><textarea id="assistant-allowed-agents" rows="3" placeholder="留空允许所有已就绪 Agent"></textarea>
|
|
44
|
+
<p class="muted">默认留空,接受所有 Peer 并允许调用所有已就绪的普通 Agent;填写具体名单后才限制。插件来源沿用客户端登录及当前模型,AID 关联请在 Channel 设置中完成。</p>
|
|
45
|
+
<h3>技能与习惯</h3>
|
|
46
|
+
<label for="assistant-skills-enabled"><input id="assistant-skills-enabled" type="checkbox" checked style="width: auto; min-height: auto"> 启用技能</label>
|
|
47
|
+
<label for="assistant-skills-learn-mode">技能学习方式</label><select id="assistant-skills-learn-mode"><option value="draft">生成草稿,确认后采用</option><option value="auto">自动学习并采用</option><option value="off">关闭学习</option></select>
|
|
48
|
+
<label for="assistant-skills-read-dirs">只读技能目录(每行一个,最多 8 个)</label><textarea id="assistant-skills-read-dirs" rows="3" placeholder="C:\Users\用户名\skills"></textarea>
|
|
49
|
+
<p class="muted">填写后台主机上的规范绝对路径,由运行时校验;留空仅使用该用户已学习的个人技能。</p>
|
|
50
|
+
<label for="assistant-skills-share-dir">技能分享目录</label><input id="assistant-skills-share-dir" type="text" placeholder="/opt/shared-skills">
|
|
51
|
+
<p class="muted">仅导出用户明确分享的技能正文和引用;填写后台主机上已存在的规范绝对路径,留空关闭分享。</p>
|
|
52
|
+
<label for="assistant-memory-mode">习惯记录方式</label><select id="assistant-memory-mode"><option value="stated">仅记录明确表达的习惯</option><option value="off">关闭习惯记录</option></select>
|
|
53
|
+
<p id="assistant-config-error" class="error" role="alert" hidden></p>
|
|
54
|
+
<div><button id="assistant-config-save" type="submit">保存配置</button><button id="assistant-config-cancel" type="button">取消</button></div>
|
|
55
|
+
</form>
|
|
56
|
+
</dialog>
|
|
34
57
|
<dialog id="operation-result" aria-labelledby="operation-result-title" aria-describedby="operation-result-message">
|
|
35
58
|
<h2 id="operation-result-title">操作结果</h2><p id="operation-result-message"></p><button id="operation-result-close" type="button" autofocus>确定</button>
|
|
36
59
|
</dialog>
|
|
@@ -50,6 +50,16 @@ form { margin-top: 14px; flex-wrap: wrap; }
|
|
|
50
50
|
.status-failed { color: #a83427; background: #fce9e5; }
|
|
51
51
|
.error, .error-text { color: #a83427; }
|
|
52
52
|
.table-scroll { overflow-x: auto; margin-top: 13px; }
|
|
53
|
+
.log-filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
|
54
|
+
.log-filters input { min-width: min(360px, 100%); flex: 1; }
|
|
55
|
+
.shell:has(#logs-panel:not([hidden])) { max-width: none; }
|
|
56
|
+
.logs-table { min-width: 900px; font-size: 12px; }
|
|
57
|
+
.logs-table th, .logs-table td { padding: 5px 7px; }
|
|
58
|
+
.logs-table th:nth-child(1) { width: 130px; }
|
|
59
|
+
.logs-table th:nth-child(2) { width: 65px; }
|
|
60
|
+
.logs-table th:nth-child(3) { width: 150px; }
|
|
61
|
+
.logs-table th:nth-child(4) { width: auto; }
|
|
62
|
+
.logs-table th:nth-child(5) { width: 260px; }
|
|
53
63
|
table { width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px; font-size: 13px; }
|
|
54
64
|
th, td { padding: 10px 9px; text-align: left; vertical-align: top; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
55
65
|
th { color: #566761; font-weight: 600; background: #edf1ef; }
|
|
@@ -63,3 +73,7 @@ th:nth-child(1) { width: 145px; } th:nth-child(2) { width: 100px; } th:nth-child
|
|
|
63
73
|
|
|
64
74
|
#cli-config-form { display: grid; min-width: min(70vw, 480px); }
|
|
65
75
|
#cli-config-form input { width: 100%; }
|
|
76
|
+
#assistant-config-form { display: grid; min-width: min(70vw, 480px); }
|
|
77
|
+
#assistant-config-form input, #assistant-config-form select, #assistant-config-form textarea { width: 100%; }
|
|
78
|
+
#assistant-config-dialog { max-height: 90vh; overflow: auto; }
|
|
79
|
+
#assistant-config-form textarea { font: inherit; padding: 6px 9px; border: 1px solid #bfc9c5; border-radius: 5px; }
|
package/package.json
CHANGED