完善 MCP/会话上下文治理并补齐搜索与诊断能力。

补充历史压缩与会话诊断链路,强化 tool pairing 与空响应兜底观测;完善 Admin MCP 导入/绑定与相关前端展示;新增 web_search_fast/web_fetch_clean 及多项测试与文档更新,并同步技能安装与角色绑定策略改进。

Made-with: Cursor
This commit is contained in:
oliver 2026-05-01 17:46:50 +08:00
parent df91434936
commit ae44cbcad5
43 changed files with 3549 additions and 57 deletions

View file

@ -4796,7 +4796,7 @@ async function renderPlugins() {
const jsonInstallInput = el("textarea", {
class: "input",
placeholder:
"Paste install JSON: one object, array of objects, or { servers: [...] } / { payload: {...} } (seed file shape)",
"Paste install JSON: one object, array of objects, { servers:[...] }, { payload:{...} }, or Cursor-style { mcpServers:{...} }",
rows: "6",
});
const cliInstallInput = el("input", {
@ -4962,10 +4962,89 @@ async function renderPlugins() {
return;
}
let list;
let detectedMcpServersShape = false;
if (Array.isArray(parsed)) {
list = parsed;
} else if (parsed && typeof parsed === "object" && Array.isArray(parsed.servers)) {
list = parsed.servers;
} else if (parsed && typeof parsed === "object" && parsed.mcpServers && typeof parsed.mcpServers === "object") {
detectedMcpServersShape = true;
installStatus.textContent =
"[json] detected mcpServers shape; streamableHttp/sse entries will be installed via mcp-remote bridge";
const envVarRe = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
list = Object.entries(parsed.mcpServers).map(([rawKey, rawServer]) => {
const key = String(rawKey || "").trim();
const s = rawServer && typeof rawServer === "object" ? rawServer : {};
const transport = String(s.type || "").trim().toLowerCase();
const baseUrl = String(s.baseUrl || s.url || "").trim();
const headers = s.headers && typeof s.headers === "object" ? s.headers : {};
// Build env schema from ${ENV_VAR} placeholders in headers.
const envSchema = {};
Object.entries(headers).forEach(([hk, hv]) => {
const v = String(hv || "");
let m;
while ((m = envVarRe.exec(v)) !== null) {
const envName = String(m[1] || "").trim();
if (!envName) continue;
envSchema[envName] = {
required: true,
description: `Auto-detected from header ${String(hk || "").trim()}`,
};
}
});
// Convert remote MCP (streamable HTTP / SSE) to stdio bridge via mcp-remote.
// Equivalent runtime command: npx -y mcp-remote <baseUrl> --header "K: V" ...
//
// Note: mcp-remote uses transport strategy "http-first" and may fall back to "sse-only"
// automatically, so both shapes can share the same bridge command.
if (
transport === "streamablehttp" ||
transport === "streamable_http" ||
transport === "streamable-http" ||
transport === "sse"
) {
const entryArgs = ["-y", "mcp-remote"];
if (baseUrl) entryArgs.push(baseUrl);
Object.entries(headers).forEach(([hk, hv]) => {
const hn = String(hk || "").trim();
const hvs = String(hv || "").trim();
if (!hn || !hvs) return;
entryArgs.push("--header", `${hn}: ${hvs}`);
});
return {
source_type: "npm",
source_ref: "mcp-remote",
server_id: key || String(s.name || "mcp-remote").trim() || "mcp-remote",
entry_command: "npx",
entry_args: entryArgs,
env_schema: envSchema,
required_permissions: Array.isArray(s.required_permissions) ? s.required_permissions.map((x) => String(x)) : [],
risk_level: String(s.risk_level || "high"),
enabled: Object.prototype.hasOwnProperty.call(s, "isActive") ? !!s.isActive : true,
timeout_s: Number(s.timeout_s || 30),
dry_run: false,
};
}
// Best-effort passthrough for stdio-like definitions.
const cmd = String(s.command || s.entry_command || "").trim();
const cmdArgs = Array.isArray(s.args) ? s.args.map((x) => String(x)) : [];
return {
source_type: String(s.source_type || "npm").trim() || "npm",
source_ref: String(s.source_ref || "mcp-remote").trim() || "mcp-remote",
server_id: key || String(s.name || s.server_id || "mcp-server").trim() || "mcp-server",
entry_command: cmd,
entry_args: cmdArgs,
env_schema: Object.keys(envSchema).length ? envSchema : (s.env_schema && typeof s.env_schema === "object" ? s.env_schema : {}),
required_permissions: Array.isArray(s.required_permissions) ? s.required_permissions.map((x) => String(x)) : [],
risk_level: String(s.risk_level || "high"),
enabled: Object.prototype.hasOwnProperty.call(s, "isActive") ? !!s.isActive : true,
timeout_s: Number(s.timeout_s || 30),
dry_run: false,
};
});
} else if (parsed && typeof parsed === "object" && parsed.payload && typeof parsed.payload === "object") {
list = [parsed.payload];
} else {
@ -5014,6 +5093,10 @@ async function renderPlugins() {
});
}
installStatus.textContent = JSON.stringify(results);
if (detectedMcpServersShape) {
installStatus.textContent =
"[json] mcpServers import finished (using mcp-remote bridge where needed)\n" + installStatus.textContent;
}
markPrewarmReminder("mcp_batch_installed");
router();
},

View file

@ -36,6 +36,10 @@ const I18N = {
"chat.tools": "推理",
"chat.tools.hidden": "推理已隐藏",
"chat.tools.visible": "推理已显示",
"chat.compressHistory": "压缩历史",
"chat.compressHistoryPrompt": "将本会话历史工具输出按回放策略写回压缩(不可逆)?建议仅在发现超大 tool_result/导出卡顿时使用。",
"chat.compressHistoryOk": "压缩完成:扫描 {scanned} 条 tool 消息,重写 {rewritten} 条,超限压缩 {compacted} 条(已跳过 {skipped} 条已压缩)。",
"chat.compressHistoryFail": "压缩失败:{error}",
"chat.tools.off": "关",
"chat.tools.on": "开",
"chat.status.oclaw": "主控",
@ -187,6 +191,10 @@ const I18N = {
"chat.tools": "Reasoning",
"chat.tools.hidden": "Reasoning hidden",
"chat.tools.visible": "Reasoning visible",
"chat.compressHistory": "Compress history",
"chat.compressHistoryPrompt": "Rewrite this session's historical tool outputs using replay-guard compaction (irreversible). Use only when a session is polluted by huge tool_result.",
"chat.compressHistoryOk": "Compaction done: scanned {scanned} tool messages, rewritten {rewritten}, oversized compacted {compacted} (skipped {skipped} already compacted).",
"chat.compressHistoryFail": "Compaction failed: {error}",
"chat.tools.off": "Off",
"chat.tools.on": "On",
"chat.status.oclaw": "Controller",
@ -2556,6 +2564,39 @@ async function renderChatUi() {
el("span", { class: "muted", text: t("chat.modeLabel") }),
modeSelect,
toolToggleWrap,
el("button", {
type: "button",
class: "btn",
style: "padding:4px 8px;min-height:auto;font-size:12px;",
text: t("chat.compressHistory"),
onclick: async () => {
const sid = String(activeId || "");
if (!sid) return;
if (!(await confirmChatAction(t("chat.compressHistoryPrompt")))) return;
try {
const resp = await apiPost(`/admin/api/chat/sessions/${encodeURIComponent(sid)}/compress-history`, {});
const r = resp && resp.result ? resp.result : {};
if (!resp || !resp.ok) {
const err = String((resp && (resp.error || resp.detail)) || "failed");
showToast(t("chat.compressHistoryFail", { error: err.slice(0, 180) }), { kind: "error", ttlMs: 6500 });
return;
}
showToast(
t("chat.compressHistoryOk", {
scanned: String(Number(r.scanned_tool_messages || 0)),
rewritten: String(Number(r.rewritten_all_tool_messages || 0)),
compacted: String(Number(r.compacted_tool_messages || 0)),
skipped: String(Number(r.skipped_already_guarded || 0)),
}),
{ kind: "info", ttlMs: 6500 },
);
// Reload messages so UI reflects compacted history.
loadMessagesForActive().catch(() => {});
} catch (e) {
showToast(t("chat.compressHistoryFail", { error: String(e || "failed").slice(0, 180) }), { kind: "error", ttlMs: 6500 });
}
},
}),
]);
const fitComposerTextarea = () => {