MiniAgent

极简 userScript 智能体(原生 DOM + GM 桥接 LLM;核心经 @require 引入 basement 全局 MiniAgent)

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Вам потребуется установить расширение, например Tampermonkey или Violentmonkey, чтобы установить этот скрипт.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         MiniAgent
// @namespace    https://github.com/chensiyi/MiniAgent
// @version      0.2.1.20260727023843
// @author       chensiyi
// @description  极简 userScript 智能体(原生 DOM + GM 桥接 LLM;核心经 @require 引入 basement 全局 MiniAgent)
// @license      https://www.apache.org/licenses/LICENSE-2.0
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/gh/chensiyi/MiniAgent@6859708db9fa4973a5ebfcedcd814a78243aa5aa/dist/miniagent-basement.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js
// @connect      *
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_setValue
// @noframes
// ==/UserScript==

(function() {
	"use strict";
	var _GM_addStyle = (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)();
	var _GM_deleteValue = (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)();
	var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
	var _GM_listValues = (() => typeof GM_listValues != "undefined" ? GM_listValues : void 0)();
	var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
	var _tt = globalThis.trustedTypes;
	var _hp = null;
	if (_tt) {
		const g = globalThis;
		if (!g.__maHp) try {
			g.__maHp = _tt.createPolicy("miniagent", { createHTML: (s) => s });
		} catch {
			g.__maHp = null;
		}
		_hp = g.__maHp ?? null;
	}
	function setHTML(el, html) {
		el.innerHTML = _hp ? _hp.createHTML(html) : html;
	}
	function renderMarkdownLocal(src) {
		const parsed = marked.parse(src);
		const html = typeof parsed === "string" ? parsed : "";
		return DOMPurify.sanitize(html);
	}
	var STYLE = `
:root{--brand:#378DDD;--brand-soft:rgba(55,141,221,.22);--glass:rgba(18,26,44,.52);--glass-strong:rgba(22,31,52,.66);--glass-border:rgba(255,255,255,.16);--glass-border-strong:rgba(255,255,255,.26);--text:#eef2ff;--text-dim:rgba(238,242,255,.62);--risk-high:#fb923c}
#miniagent-root{position:fixed;right:16px;bottom:16px;z-index:2147483647;width:320px;max-height:calc(100vh - 32px);display:flex;flex-direction:column;gap:8px;font:14px system-ui;color:var(--text)}
.ma-bubbles{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:4px 2px;-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 10%,#000 100%);mask-image:linear-gradient(to bottom,transparent 0,#000 10%,#000 100%)}
.ma-bubble{padding:6px 9px;max-width:88%;white-space:pre-wrap;word-break:break-word;border:1px solid var(--glass-border);background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);box-shadow:0 2px 10px rgba(0,0,0,.18);align-self:flex-start}
.ma-bubble.user{align-self:flex-end;background:var(--brand-soft);border-color:rgba(55,141,221,.5)}
.ma-bubble.tool{align-self:flex-start;font-size:12px;background:rgba(18,26,44,.62)}
.ma-input-row{display:flex;gap:6px;align-items:center;position:relative}
.ma-ac{position:absolute;left:0;right:0;bottom:100%;margin-bottom:4px;background:var(--glass-strong);border:1px solid var(--glass-border);overflow:auto;max-height:210px;box-shadow:0 4px 16px rgba(0,0,0,.3);color:var(--text)}
.ma-ac-item{padding:6px 10px;cursor:pointer;display:flex;flex-direction:column;gap:1px}
.ma-ac-item.active,.ma-ac-item:hover{background:var(--brand-soft)}
.ma-ac-name{font-weight:600;color:#6fb0f0;font-size:12px}
.ma-ac-hint{color:var(--text-dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ma-input{flex:1;min-width:0;padding:8px 10px;border:1px solid var(--glass-border);outline:0;background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);color:var(--text)}
.ma-input::placeholder{color:var(--text-dim)}
.ma-input-row button{padding:8px 14px;border:1px solid rgba(55,141,221,.5);background:rgba(55,141,221,.85);color:#fff;cursor:pointer;white-space:nowrap}
.ma-input-row button.stop{background:rgba(248,113,113,.85);border-color:rgba(248,113,113,.5)}
.ma-code{max-height:160px;overflow:auto;margin:6px 0;padding:6px 8px;background:rgba(0,0,0,.35);color:#cfe0f5;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;white-space:pre-wrap;word-break:break-all}
.ma-approve{display:flex;gap:8px;margin-top:4px}
.ma-approve button{flex:1;border:0;padding:5px 0;font-size:12px;cursor:pointer;color:#fff}
.ma-approve .ok{background:rgba(55,141,221,.9)}
.ma-approve .no{background:rgba(255,255,255,.12);color:var(--text)}
.ma-risk{color:var(--risk-high);font-weight:600}
.ma-tools{padding:8px 10px;border:1px solid var(--glass-border);background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);cursor:pointer}
.ma-tools-panel{padding:8px;border:1px solid var(--glass-border-strong);background:var(--glass-strong);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;gap:4px;min-height:120px;max-height:40vh;overflow:auto}
.ma-tool-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:13px}
.ma-tool-name{word-break:break-all;flex-shrink:0}
.ma-tool-desc{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;color:var(--text-dim);font-size:11px}
.ma-think{margin:0 0 6px;border-left:3px solid var(--brand);overflow:hidden}
.ma-think summary{cursor:pointer;padding:4px 8px;font-size:12px;color:var(--text-dim);background:var(--brand-soft);user-select:none}
.ma-think summary:hover{background:rgba(55,141,221,.14)}
.ma-think-body{padding:6px 8px;font-size:12px;color:var(--text-dim);max-height:300px;overflow:auto;white-space:pre-wrap}
.ma-md-content{color:var(--text);white-space:normal}
.ma-md-content p{margin:4px 0}
.ma-md-content h1,.ma-md-content h2,.ma-md-content h3{margin:8px 0 4px;line-height:1.3}
.ma-md-content ul,.ma-md-content ol{margin:4px 0;padding-left:20px}
.ma-md-content blockquote{margin:4px 0;padding:2px 8px;border-left:3px solid var(--glass-border);color:var(--text-dim)}
.ma-md-content pre{max-height:200px;overflow:auto;margin:6px 0;padding:6px 8px;background:rgba(0,0,0,.3);font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-all}
.ma-md-content :not(pre) > code{padding:1px 4px;background:rgba(255,255,255,.12);font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
.ma-md-content img{max-width:100%}
.ma-md-content table{border-collapse:collapse;margin:6px 0;font-size:12px}
.ma-md-content th,.ma-md-content td{border:1px solid var(--glass-border);padding:2px 6px}
.ma-md-content hr{border:0;border-top:1px solid var(--glass-border);margin:8px 0}
.ma-md-content a{color:#6fb0f0}
.ma-toggle{display:block;width:100%;padding:2px 0;margin:0;text-align:center;border:1px solid var(--glass-border);background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);color:var(--text);cursor:pointer;font-size:11px;line-height:1.3}
.ma-toggle:hover{background:rgba(255,255,255,.16)}
.ma-collapsed .ma-bubbles,.ma-collapsed .ma-input-row,.ma-collapsed .ma-tools-panel{display:none}
`;
	function renderToolsPanel(panel) {
		panel.replaceChildren();
		for (const s of MiniAgent.toolManager.getStates()) {
			const row = document.createElement("label");
			row.className = "ma-tool-row";
			const name = document.createElement("span");
			name.className = "ma-tool-name";
			name.textContent = s.author && s.author !== "sys" ? `${s.name} @${s.author}` : s.name;
			const desc = document.createElement("span");
			desc.className = "ma-tool-desc";
			desc.textContent = s.description ?? "";
			desc.title = s.description ?? "";
			const cb = document.createElement("input");
			cb.type = "checkbox";
			cb.checked = s.enabled;
			cb.onchange = async () => {
				const next = cb.checked;
				await MiniAgent.toolManager.setEnabled(s.name, next);
				const live = MiniAgent.executor.list(true).some((t) => t.name === s.name);
				if (cb.checked !== live) cb.checked = live;
			};
			row.append(name, desc, cb);
			panel.append(row);
		}
	}
	var root;
	var bubbles;
	var input;
	var sendBtn;
	var stopBtn;
	var bubblesById = new Map();
	var bubbleSeq = 0;
	var acEl = null;
	var acItems = [];
	var acIndex = -1;
	var onSendRef = null;
	var toolsPanelEl = null;
	var toggleBtnEl = null;
	function computeAc(text) {
		if (!text.startsWith("/")) return [];
		const lastSpace = text.lastIndexOf(" ");
		const after = text.slice(lastSpace + 1);
		const hasPrefix = lastSpace > 0;
		const propsOf = (name) => MiniAgent.executor.list(true).find((t) => t.name === name)?.parameters?.properties ?? {};
		const usedParams = (toolName, activeQ) => {
			const used = new Set();
			const re = /\/(\S+)/g;
			let m;
			while (m = re.exec(text)) {
				const w = m[1];
				if (w === toolName || w === activeQ) continue;
				used.add(w);
			}
			return used;
		};
		const paramItems = (toolName, q) => {
			const props = propsOf(toolName);
			return Object.entries(props).filter(([k]) => !usedParams(toolName, q).has(k) && k.toLowerCase().includes(q)).slice(0, 8).map(([k, v]) => ({
				text: "/" + k + " ",
				hint: (v.description ?? "") + (v.type ? ` (${v.type})` : "")
			}));
		};
		if (after.startsWith("/")) {
			const q = after.slice(1).toLowerCase();
			if (!hasPrefix) return MiniAgent.executor.list(true).filter((t) => t.name.toLowerCase().includes(q)).slice(0, 8).map((t) => ({
				text: "/" + t.name + " ",
				hint: t.description ?? ""
			}));
			const toolName = text.slice(1, lastSpace).split(/\s+/)[0];
			return paramItems(toolName, q);
		}
		if (after === "" && hasPrefix) {
			const toolName = text.slice(1, lastSpace).split(/\s+/)[0];
			return paramItems(toolName, "");
		}
		return [];
	}
	function renderAc() {
		const el = acEl;
		if (!el) return;
		if (!acItems.length) {
			el.style.display = "none";
			el.replaceChildren();
			return;
		}
		el.replaceChildren();
		acItems.forEach((it, i) => {
			const item = document.createElement("div");
			item.className = "ma-ac-item" + (i === acIndex ? " active" : "");
			const name = document.createElement("div");
			name.className = "ma-ac-name";
			name.textContent = it.text.trim();
			const hint = document.createElement("div");
			hint.className = "ma-ac-hint";
			hint.textContent = it.hint;
			item.append(name, hint);
			item.onmousedown = (e) => {
				e.preventDefault();
				acceptAc(it.text);
			};
			item.onmouseenter = () => {
				acIndex = i;
				renderAc();
			};
			el.append(item);
		});
		el.style.display = "";
	}
	function updateAc() {
		if (!acEl || !input) return;
		acItems = computeAc(input.value);
		acIndex = acItems.length ? 0 : -1;
		renderAc();
	}
	function acceptAc(insert) {
		if (!input) return;
		const text = input.value;
		const lastSpace = text.lastIndexOf(" ");
		const before = lastSpace === -1 ? "" : text.slice(0, lastSpace + 1);
		input.value = before + insert;
		input.focus();
		updateAc();
	}
	var ui = {
		chat: {
			mount(onSend) {
				if (document.getElementById("miniagent-root")) return;
				_GM_addStyle(STYLE);
				root = document.createElement("div");
				root.id = "miniagent-root";
				setHTML(root, `
        <button class="ma-toggle" type="button" title="折叠/展开面板">▾</button>
        <div class="ma-bubbles"></div>
        <div class="ma-input-row">
          <button class="ma-tools" type="button" title="工具开关">⚙</button>
          <input class="ma-input" type="text" placeholder="问点什么…(Enter 发送)" />
          <button class="ma-send" type="button">发送</button>
          <button class="ma-stop" type="button" style="display:none">停止</button>
        </div>
        <div class="ma-tools-panel" style="display:none"></div>`);
				document.body.append(root);
				bubbles = root.querySelector(".ma-bubbles");
				input = root.querySelector(".ma-input");
				sendBtn = root.querySelector(".ma-send");
				stopBtn = root.querySelector(".ma-stop");
				const toolsBtn = root.querySelector(".ma-tools");
				toolsPanelEl = root.querySelector(".ma-tools-panel");
				acEl = document.createElement("div");
				acEl.className = "ma-ac";
				acEl.style.display = "none";
				root.querySelector(".ma-input-row").append(acEl);
				onSendRef = onSend;
				toolsBtn.onclick = () => ui.tools.toggle();
				toggleBtnEl = root.querySelector(".ma-toggle");
				toggleBtnEl.onclick = () => ui.panel.toggle();
				const doSend = () => {
					const text = input.value.trim();
					if (!text) return;
					input.value = "";
					if (acEl) acEl.style.display = "none";
					onSendRef?.(text);
				};
				sendBtn.onclick = doSend;
				input.oninput = () => ui.chat.refreshAutocomplete();
				input.onblur = () => {
					if (acEl) acEl.style.display = "none";
				};
				input.onkeydown = (e) => {
					if (acEl && acEl.style.display !== "none" && acItems.length) {
						if (e.key === "ArrowDown") {
							e.preventDefault();
							acIndex = (acIndex + 1) % acItems.length;
							renderAc();
							return;
						}
						if (e.key === "ArrowUp") {
							e.preventDefault();
							acIndex = (acIndex - 1 + acItems.length) % acItems.length;
							renderAc();
							return;
						}
						if (e.key === "Enter" || e.key === "Tab") {
							e.preventDefault();
							if (acIndex >= 0) ui.chat.acceptAutocomplete(acItems[acIndex].text);
							return;
						}
						if (e.key === "Escape") {
							acEl.style.display = "none";
							return;
						}
					}
					if (e.key === "Enter") {
						e.preventDefault();
						doSend();
					}
				};
			},
			unmount() {
				if (root && root.parentNode) root.parentNode.removeChild(root);
				root = null;
				bubbles = null;
				input = null;
				sendBtn = null;
				stopBtn = null;
				bubblesById.clear();
				acEl = null;
				onSendRef = null;
				toolsPanelEl = null;
				toggleBtnEl = null;
			},
			append(role, text, id) {
				const el = document.createElement("div");
				el.className = `ma-bubble ${role}`;
				const mid = id ?? `b${(++bubbleSeq).toString(36)}`;
				el.dataset.mid = mid;
				if (role === "assistant") {
					setHTML(el, "<details class=\"ma-think\" style=\"display:none\"><summary>💭 思考过程</summary><div class=\"ma-think-body\"></div></details><div class=\"ma-md-content\"></div>");
					const c = el.querySelector(".ma-md-content");
					if (c) c.textContent = text;
				} else el.textContent = text;
				bubbles.append(el);
				bubbles.scrollTop = bubbles.scrollHeight;
				bubblesById.set(mid, el);
				return mid;
			},
			update(mid, role, text, reasoning) {
				const el = bubblesById.get(mid) ?? null;
				if (!el) {
					console.warn("[MiniAgent.UI] update 跳过:找不到气泡", {
						mid,
						role,
						textLen: text?.length
					});
					return;
				}
				if (!el.isConnected) {
					console.warn("[MiniAgent.UI] update 跳过:气泡已脱离 DOM", {
						mid,
						role
					});
					return;
				}
				if (role === "assistant") {
					const c = el.querySelector(".ma-md-content");
					if (c) c.textContent = text;
					if (reasoning != null) {
						const think = el.querySelector(".ma-think");
						if (think) {
							think.style.display = "";
							const tb = el.querySelector(".ma-think-body");
							if (tb) tb.textContent = reasoning;
						}
					}
				} else el.textContent = text;
				bubbles.scrollTop = bubbles.scrollHeight;
			},
			setToolHTML(mid, html) {
				const el = bubblesById.get(mid) ?? null;
				if (!el) return;
				if (typeof DOMPurify !== "undefined" && typeof DOMPurify.sanitize === "function") setHTML(el, DOMPurify.sanitize(html));
				else el.textContent = html;
				bubbles.scrollTop = bubbles.scrollHeight;
			},
			finalize(mid, role, text, reasoning) {
				const el = bubblesById.get(mid) ?? null;
				if (!el) {
					console.warn("[MiniAgent.UI] finalize 跳过:找不到气泡", {
						mid,
						role,
						textLen: text?.length,
						reasoningLen: reasoning?.length
					});
					return;
				}
				if (!el.isConnected) {
					console.warn("[MiniAgent.UI] finalize 跳过:气泡已脱离 DOM", { mid });
					return;
				}
				const c = el.querySelector(".ma-md-content");
				if (c) if (!text || !text.trim()) c.textContent = reasoning ? "(模型已思考,本轮未返回正文)" : "(空响应)";
				else try {
					setHTML(c, renderMarkdownLocal(text));
				} catch (e) {
					console.error("[MiniAgent.UI] renderMarkdown 异常:", e);
					c.textContent = text;
				}
				const think = el.querySelector(".ma-think");
				if (think) if (reasoning) {
					const tb = el.querySelector(".ma-think-body");
					if (tb) setHTML(tb, renderMarkdownLocal(reasoning));
				} else think.remove();
			},
			setRunning(running, onStop) {
				sendBtn.style.display = running ? "none" : "";
				stopBtn.style.display = running ? "" : "none";
				input.disabled = running;
				if (running && onStop) stopBtn.onclick = onStop;
			},
			send() {
				const text = input?.value.trim();
				if (!text || !onSendRef) return;
				if (input) input.value = "";
				if (acEl) acEl.style.display = "none";
				onSendRef(text);
			},
			setInput(text) {
				if (!input) return;
				input.value = text;
				updateAc();
			},
			refreshAutocomplete() {
				updateAc();
			},
			acceptAutocomplete(insert) {
				acceptAc(insert);
			}
		},
		panel: {
			toggle() {
				if (!root || !toggleBtnEl) return;
				const collapsed = root.classList.toggle("ma-collapsed");
				toggleBtnEl.textContent = collapsed ? "▴" : "▾";
				if (!collapsed && toolsPanelEl && toolsPanelEl.style.display === "") renderToolsPanel(toolsPanelEl);
			},
			setCollapsed(c) {
				if (!root || !toggleBtnEl) return;
				if (root.classList.contains("ma-collapsed") === c) return;
				root.classList.toggle("ma-collapsed", c);
				toggleBtnEl.textContent = c ? "▴" : "▾";
				if (!c && toolsPanelEl && toolsPanelEl.style.display === "") renderToolsPanel(toolsPanelEl);
			},
			isCollapsed() {
				return !!root && root.classList.contains("ma-collapsed");
			}
		},
		tools: {
			toggle() {
				if (!toolsPanelEl) return;
				if (toolsPanelEl.style.display === "none") {
					renderToolsPanel(toolsPanelEl);
					toolsPanelEl.style.display = "";
				} else toolsPanelEl.style.display = "none";
			},
			open() {
				if (!toolsPanelEl) return;
				renderToolsPanel(toolsPanelEl);
				toolsPanelEl.style.display = "";
			},
			close() {
				if (toolsPanelEl) toolsPanelEl.style.display = "none";
			},
			refresh() {
				if (toolsPanelEl && toolsPanelEl.style.display !== "none") renderToolsPanel(toolsPanelEl);
			}
		},
		requestApproval: async (call) => new Promise((resolve) => {
			const el = document.createElement("div");
			el.className = "ma-bubble tool";
			const risk = call.riskLevel ? ` <span class="ma-risk">[${call.riskLevel}]</span>` : "";
			setHTML(el, `
        <div>工具请求执行(${call.name})${risk},是否允许?</div>
        ${call.code ? "<pre class=\"ma-code\"></pre>" : ""}
        <div class="ma-approve"><button class="ok" type="button">允许</button><button class="no" type="button">拒绝</button></div>`);
			if (call.code) el.querySelector(".ma-code").textContent = call.code;
			const finish = (ok) => {
				el.remove();
				resolve(ok);
			};
			el.querySelector(".ok").onclick = () => finish(true);
			el.querySelector(".no").onclick = () => finish(false);
			bubbles.append(el);
			bubbles.scrollTop = bubbles.scrollHeight;
		})
	};
	var headlessSink = {
		append: () => "",
		update() {},
		finalize() {},
		setToolHTML() {},
		setRunning() {}
	};
	var CONFIG_HINT = "⚠️ 未配置 API Key。两种设置方式:\n① 打开 Tampermonkey 仪表盘 → 本脚本 → 数值,直接编辑 `config` 键(JSON:{\"apiKey\":\"你的Key\",\"baseURL\":\"https://openrouter.ai/api/v1\",\"model\":\"openrouter/free\"});\n② 或运行命令:/gm_storage /action set /ns \"\" /key config /update true /value {\"apiKey\":\"你的Key\",\"baseURL\":\"https://openrouter.ai/api/v1\",\"model\":\"openrouter/free\"}";
	function whenDomReady() {
		return new Promise((resolve) => {
			if (document.readyState !== "loading") return resolve();
			document.addEventListener("DOMContentLoaded", () => resolve(), { once: true });
		});
	}
	var launcherEl = null;
	function ensureLauncher() {
		if (launcherEl) return launcherEl;
		const css = "#miniagent-launcher{position:fixed;right:14px;bottom:14px;z-index:2147483646}#miniagent-launcher button{padding:6px 12px;border:1px solid rgba(55,141,221,.6);border-radius:8px;background:rgba(55,141,221,.92);color:#fff;cursor:pointer;font-size:13px;box-shadow:0 4px 16px rgba(0,0,0,.3)}";
		const style = document.createElement("style");
		style.textContent = css;
		(document.head ?? document.documentElement).append(style);
		const el = document.createElement("div");
		el.id = "miniagent-launcher";
		el.innerHTML = "<button type=\"button\" title=\"启用 MiniAgent 界面\">💬 启用界面</button>";
		el.querySelector("button").onclick = () => {
			MiniAgent.toolManager.setEnabled("ui", true);
		};
		if (document.body) document.body.append(el);
		else document.addEventListener("DOMContentLoaded", () => document.body.append(el), { once: true });
		launcherEl = el;
		return el;
	}
	function showLauncher() {
		ensureLauncher().style.display = "";
	}
	function hideLauncher() {
		ensureLauncher().style.display = "none";
	}
	function createLauncher() {
		const el = ensureLauncher();
		const uiUp = agent$2.tools.has("ui");
		el.style.display = uiUp ? "none" : "";
	}
	var { agent: agent$2, handleToolCommand } = MiniAgent;
	var uiTool = {
		name: "ui",
		author: "sys",
		deps: [{
			name: "gm_storage",
			author: "sys"
		}],
		description: "界面工具:注册后挂载聊天界面并接管输出/渲染/确认闸;在工具清单禁用即\"关闭界面\"(经确认闸、可逆),核心仍 headless 运行。启用即重新挂载。",
		parameters: {},
		register: async (_ctx) => {
			agent$2.output = ui.chat;
			agent$2.extensions.set("ui", ui.chat);
			agent$2.extensions.set("approval", ui.requestApproval);
			await whenDomReady();
			ui.chat.mount((text) => {
				const run = (fn) => {
					ui.chat.setRunning(true, () => agent$2.chatStop());
					fn().finally(() => ui.chat.setRunning(false));
				};
				if (text.startsWith("/")) {
					agent$2.output.append("user", text);
					run(() => handleToolCommand(text));
					return;
				}
				if (!agent$2.config.apiKey) {
					agent$2.output.append("user", text);
					agent$2.output.append("tool", CONFIG_HINT);
					return;
				}
				run(() => agent$2.sendMessage(text));
			});
			hideLauncher();
		},
		unregister: (_ctx) => {
			ui.chat.unmount();
			agent$2.output = headlessSink;
			agent$2.extensions.delete("ui");
			agent$2.extensions.delete("approval");
			showLauncher();
		}
	};
	createLauncher();
	var { agent: agent$1 } = MiniAgent;
	var hooks;
	var NS_MEM = "memory";
	var OVERVIEW_NS = [
		"session",
		"tools",
		"code",
		"memory"
	];
	function resolveSet(nsOrKey, keyOrValue, value) {
		if (value === void 0) return {
			key: nsOrKey,
			val: keyOrValue
		};
		const ns = String(nsOrKey);
		const key = String(keyOrValue);
		return {
			key: ns ? `${ns}:${key}` : key,
			val: value
		};
	}
	function resolveDel(nsOrKey, key) {
		return key === void 0 ? nsOrKey : `${nsOrKey}:${key}`;
	}
	var mirrored = false;
	var gmStorageTool = {
		name: "gm_storage",
		author: "sys",
		deps: [{
			name: "hooks",
			author: "sys"
		}],
		description: "统一的持久存储管理(默认 memory 命名空间,可指定其它 ns)。action 取值:get=读取键;set=写入键(update=true 时合并已有对象);list=列出键(给定 ns 列该分区子键,不给 ns 按 session/tools/code/memory 分区概览);del=删除键(不可恢复,删除前会请求确认)。用于记忆、配置、状态管理。底层 storage 为内存 Map,本工具经 before 钩子透明落盘到 GM_*,其它工具无需关心环境。",
		parameters: {
			type: "object",
			properties: {
				action: {
					type: "string",
					enum: [
						"get",
						"set",
						"list",
						"del"
					],
					description: "操作类型:get=读取某个键的值;set=写入/更新某个键;list=列出命名空间下的键(不给 ns 则按 session/tools/code/memory 分区概览);del=删除某个键(不可恢复,删除前会请求用户确认)。"
				},
				key: {
					type: "string",
					description: "键名。get/set/del 必需;list 不需要。"
				},
				value: {
					type: "string",
					description: "要保存的值(set 必需)。会原样写入存储;get 时以 JSON 字符串形式返回,因此对象/数组等复杂值建议先 JSON 序列化后传入。"
				},
				ns: {
					type: "string",
					description: "可选命名空间(分区)。默认 memory;也可用 session/tools/code 等已有分区,或自定义新分区。"
				},
				update: {
					type: "boolean",
					description: "仅 set 生效。为 true 时进入合并模式:先读取已有值,再把传入的值(对象)浅合并进去,而非整条覆盖。"
				}
			},
			required: [
				"action",
				"key",
				"value",
				"ns",
				"update"
			]
		},
		register(ctx) {
			if (!mirrored) {
				for (const k of _GM_listValues()) {
					const raw = _GM_getValue(k, void 0);
					if (raw === void 0 || raw === null) continue;
					try {
						agent$1.storage.set("", k, JSON.parse(raw));
					} catch {
						agent$1.storage.set("", k, raw);
					}
				}
				mirrored = true;
			}
			hooks = agent$1.tools.get("hooks");
			hooks.installHook("storageSet", "before", (opts) => {
				const { key, val } = resolveSet(opts.args[0], opts.args[1], opts.args[2]);
				_GM_setValue(key, typeof val === "string" ? val : JSON.stringify(val));
			}, {
				id: "sys-gm-persist-set",
				name: "GM 落盘(set)",
				toolName: "gm_storage",
				core: true,
				agentRef: ctx.agent,
				execRef: ctx.executor
			});
			hooks.installHook("storageDelete", "before", (opts) => {
				_GM_deleteValue(resolveDel(opts.args[0], opts.args[1]));
			}, {
				id: "sys-gm-persist-del",
				name: "GM 落盘(del)",
				toolName: "gm_storage",
				core: true,
				agentRef: ctx.agent,
				execRef: ctx.executor
			});
			console.log("[MiniAgent] gm_storage 已挂载落盘钩子(storageSet/storageDelete → GM_*)");
		},
		unregister(_ctx) {
			hooks?.uninstallToolHooks("gm_storage");
			console.log("[MiniAgent] gm_storage 已卸载,落盘钩子已摘除");
		},
		call: async (args, ctx) => {
			const action = String(args.action ?? "");
			const ns = String(args.ns ?? NS_MEM);
			switch (action) {
				case "get": {
					const key = String(args.key ?? "");
					if (!key) return "参数 key 缺失";
					const v = ctx.storage.get(ns, key);
					return v === void 0 ? "(无此键)" : JSON.stringify(v);
				}
				case "set": {
					const key = String(args.key ?? "");
					if (!key) return "参数 key 缺失";
					if (args.value === void 0) return "参数 value 缺失";
					if (args.update) {
						const existing = ctx.storage.get(ns, key) ?? {};
						const incoming = args.value;
						const merged = typeof existing === "object" && existing && typeof incoming === "object" && incoming ? {
							...existing,
							...incoming
						} : incoming;
						ctx.storage.set(ns, key, merged);
						return `已合并保存 ${ns ? ns + ":" : ""}${key}`;
					}
					ctx.storage.set(ns, key, args.value);
					return `已保存 ${ns ? ns + ":" : ""}${key}`;
				}
				case "list": {
					if (args.ns) {
						const keys = agent$1.storage.keys(ns).sort((a, b) => a.localeCompare(b));
						return JSON.stringify({
							ns,
							count: keys.length,
							keys
						});
					}
					const overview = {};
					for (const n of OVERVIEW_NS) overview[n] = agent$1.storage.keys(n).sort((a, b) => a.localeCompare(b));
					overview.flat = agent$1.storage.keys().filter((k) => !k.includes(":")).sort((a, b) => a.localeCompare(b));
					return JSON.stringify(overview);
				}
				case "del": {
					const key = String(args.key ?? "");
					if (!key) return "参数 key 缺失";
					if (!await ctx.executor.requestApproval({
						name: `gm_storage:del ${ns}:${key}`,
						riskLevel: "high"
					}, ctx.agent)) return "用户拒绝了执行";
					ctx.storage.del(ns, key);
					return `已删除 ${ns}:${key}`;
				}
				default: return `未知 action: ${action}(支持 get/set/list/del)`;
			}
		}
	};
	var { agent, toolManager, defaultTools } = MiniAgent;
	var hooksTool = defaultTools.find((t) => t.name === "hooks");
	toolManager.definePreset([gmStorageTool, hooksTool], [
		gmStorageTool,
		...defaultTools,
		uiTool
	]);
	toolManager.bootstrap();
	globalThis.agent = agent;
})();