mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 04:40:45 +08:00
完善 MCP/会话上下文治理并补齐搜索与诊断能力。
补充历史压缩与会话诊断链路,强化 tool pairing 与空响应兜底观测;完善 Admin MCP 导入/绑定与相关前端展示;新增 web_search_fast/web_fetch_clean 及多项测试与文档更新,并同步技能安装与角色绑定策略改进。 Made-with: Cursor
This commit is contained in:
parent
df91434936
commit
ae44cbcad5
43 changed files with 3549 additions and 57 deletions
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue