mirror of
https://github.com/hansjone/dsh-search-mcp.git
synced 2026-10-08 22:20:47 +08:00
Based on gxpppp/dsh-search-mcp; includes DSH web settings fixes, Clash fake-IP URL policy, and Bailian default server patch. Co-authored-by: Cursor <cursoragent@cursor.com>
1077 lines
49 KiB
JavaScript
1077 lines
49 KiB
JavaScript
/*
|
||
* dsh-search-mcp — browser half.
|
||
*
|
||
* Registers one card into the Settings → Plugins → 插件配置 (configurable) tab,
|
||
* bound to the `search-mcp` settings namespace registered by the host half.
|
||
*
|
||
* The card is a structured form (no raw JSON editing), designed to feel like
|
||
* the neighbor cards:
|
||
* - 默认服务器: dropdown of the configured server ids (friendly labels)
|
||
* - 结果数上限 / 搜索超时: numeric inputs (timeout in seconds)
|
||
* - 常用提供商 quick-add: one-click preset rows (Tavily / Brave / Exa /
|
||
* Perplexity / DuckDuckGo) — only the API key still needs filling
|
||
* - 服务器列表: collapsible rows; known providers show only identity,
|
||
* provider, write-only key/reference status, and result limit; custom rows
|
||
* additionally expose transport, URL/command, authentication, and tool name.
|
||
*
|
||
* Loaded through window.__ModuleLoader__ like every shipped client bundle.
|
||
*/
|
||
window.__ModuleLoader__.load({
|
||
id: "dsh-search-mcp",
|
||
factory: (require) => {
|
||
var module = { exports: {} };
|
||
var exports = module.exports;
|
||
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||
let react_jsx_runtime = require("react/jsx-runtime");
|
||
let react = require("react");
|
||
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
||
// 0.1.2+: createSnapshotStore lives on the platform seed @deepseek-ai/dsh-client-store
|
||
// (old @deepseek-ai/dsh-client-runtime/client is gone from the module table).
|
||
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-store");
|
||
|
||
//#region styles
|
||
const css = [
|
||
".smcp_card{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}",
|
||
".smcp_card:hover{border-color:var(--dsw-alias-label-dimmed)}",
|
||
".smcp_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
|
||
".smcp_headBtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
|
||
".smcp_headBtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
|
||
".smcp_head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
|
||
".smcp_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
|
||
".smcp_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
|
||
".smcp_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
|
||
".smcp_chevronOpen{transform:rotate(180deg)}",
|
||
".smcp_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:0}",
|
||
".smcp_field{display:flex;flex-direction:column;gap:6px;padding:12px 0}",
|
||
".smcp_field + .smcp_field{border-top:1px solid var(--dsw-alias-border-l2)}",
|
||
".smcp_label{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
|
||
".smcp_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}",
|
||
".smcp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}",
|
||
".smcp_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
|
||
".smcp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px;line-height:1.5}",
|
||
".smcp_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
|
||
".smcp_invalid{border-color:var(--dsw-alias-label-error)}",
|
||
".smcp_error{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}",
|
||
".smcp_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
|
||
".smcp_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}",
|
||
".smcp_badgeDanger{white-space:nowrap;color:var(--dsw-alias-label-error);border:1px solid var(--dsw-alias-label-error);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}",
|
||
".smcp_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}",
|
||
".smcp_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
|
||
".smcp_reset:disabled{cursor:default}",
|
||
".smcp_footer{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}",
|
||
".smcp_btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 12px;height:auto;min-height:32px;font-size:13px;line-height:1.5}",
|
||
/* Match system plugin cards (e.g. dsh-ops-cron): primary text on contrast surface */
|
||
".smcp_btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}",
|
||
".smcp_btnPrimary:disabled{opacity:.5;cursor:default}",
|
||
".smcp_btnGhost{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary)}",
|
||
".smcp_btnGhost:disabled{opacity:.5;cursor:default}",
|
||
".smcp_failDetail{color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.45;text-align:left}",
|
||
".smcp_quickAdd{display:flex;flex-wrap:wrap;gap:8px;padding:2px 0 6px}",
|
||
".smcp_quickBtn{border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:4px 12px;font-size:12px;line-height:1.5;cursor:pointer}",
|
||
".smcp_quickBtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}",
|
||
".smcp_quickBtn:disabled{opacity:.5;cursor:default}",
|
||
".smcp_row{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;background:var(--dsw-alias-bg-layer-2)}",
|
||
".smcp_rowHead{display:flex;align-items:center;gap:8px;min-width:0}",
|
||
".smcp_rowToggle{appearance:none;border:0;background:0 0;padding:2px;cursor:pointer;color:var(--dsw-alias-label-tertiary);display:inline-flex;align-items:center;flex:none;transition:transform .16s}",
|
||
".smcp_rowToggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px;border-radius:6px}",
|
||
".smcp_rowToggleOpen{transform:rotate(180deg)}",
|
||
".smcp_kindBadge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;flex:none}",
|
||
".smcp_rowTitle{flex:0 0 auto;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}",
|
||
".smcp_rowSummary{flex:1;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
||
".smcp_rowBody{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:2px;padding-top:8px;display:flex;flex-direction:column;gap:8px}",
|
||
".smcp_rowId{flex:1;min-width:0}",
|
||
".smcp_rowGrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}",
|
||
".smcp_rowGrid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}",
|
||
".smcp_cell{display:flex;flex-direction:column;gap:4px;min-width:0}",
|
||
".smcp_cellLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.5}",
|
||
".smcp_cellHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}",
|
||
".smcp_add{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;border:1px dashed var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:8px;padding:6px 12px;font-size:12px;line-height:1.5;cursor:pointer}",
|
||
".smcp_add:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}",
|
||
".smcp_del{border:1px solid var(--dsw-alias-border-l2);background:0 0;color:var(--dsw-alias-label-secondary);border-radius:8px;padding:4px 10px;font-size:12px;line-height:1.5;cursor:pointer;flex:none}",
|
||
".smcp_del:hover{color:var(--dsw-alias-label-error);border-color:var(--dsw-alias-label-error)}"
|
||
].join("");
|
||
const cssTag = "dsh-search-mcp/card.css";
|
||
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${cssTag}"]`) === null) {
|
||
const tag = document.createElement("style");
|
||
tag.dataset.plugin = "dsh-search-mcp";
|
||
tag.dataset.pluginCss = cssTag;
|
||
tag.textContent = css;
|
||
document.head.appendChild(tag);
|
||
}
|
||
//#endregion
|
||
|
||
//#region provider catalog (browser-side metadata only)
|
||
const CATALOG = {
|
||
tavily: { needsKey: true },
|
||
brave: { needsKey: true },
|
||
exa: { needsKey: true },
|
||
perplexity: { needsKey: true },
|
||
duckduckgo: { needsKey: false },
|
||
custom: { needsKey: false }
|
||
};
|
||
const KIND_OPTIONS = Object.keys(CATALOG);
|
||
const TRANSPORT_OPTIONS = ["http", "stdio"];
|
||
const AUTH_STYLE_OPTIONS = ["", "query", "header"];
|
||
|
||
/** Brand labels shown on the kind badge / quick-add buttons. */
|
||
const KIND_LABELS = {
|
||
tavily: "Tavily",
|
||
brave: "Brave",
|
||
exa: "Exa",
|
||
perplexity: "Perplexity",
|
||
duckduckgo: "DuckDuckGo",
|
||
custom: "Custom"
|
||
};
|
||
/** Kinds offered as one-click presets (custom is added via the dashed button). */
|
||
const QUICK_KINDS = ["tavily", "brave", "exa", "perplexity", "duckduckgo"];
|
||
//#endregion
|
||
|
||
//#region locale
|
||
const en = {
|
||
title: "Search MCP",
|
||
description: "Search MCP servers behind the web_search tool; the built-in DeepSeek search stays disabled while this plugin is enabled.",
|
||
defaultServer: "Default server",
|
||
defaultServerHint: "Which server row serves searches (empty = first row).",
|
||
maxResults: "Max results",
|
||
maxResultsHint: "Sources returned per search (1–50; empty = default).",
|
||
searchTimeoutMs: "Search timeout (s)",
|
||
searchTimeoutMsHint: "Abort a search that takes longer than this.",
|
||
servers: "Search MCP servers",
|
||
serversHint: "Choose a provider and enter its CDKey/API key. Known providers keep connection details in the host catalog; custom rows expose advanced MCP settings.",
|
||
quickAdd: "Common providers",
|
||
quickAddHint: "Add a provider row, then enter its CDKey/API key before saving. No endpoint is required for known providers.",
|
||
addServer: "Add custom server",
|
||
removeServer: "Delete",
|
||
rowId: "ID",
|
||
rowIdHint: "Stable id used by “default server”.",
|
||
rowKind: "Provider",
|
||
rowTransport: "Transport",
|
||
rowUrl: "Endpoint (URL)",
|
||
rowCommand: "Command",
|
||
rowArgs: "Args (comma separated)",
|
||
rowApiKey: "API key",
|
||
cdKey: "CDKey / API key",
|
||
cdKeyHint: "enter CDKey / API key",
|
||
keyNotRequired: "no key required",
|
||
|
||
rowApiKeyEnv: "Key env/credential ref",
|
||
rowAuthStyle: "Key placement",
|
||
rowAuthParam: "Key param / env name",
|
||
rowToolName: "MCP tool name",
|
||
rowMaxResults: "Max results",
|
||
rowMaxResultsHint: "Empty = follow the global max results.",
|
||
missingId: "ID is required.",
|
||
dupId: "This ID is already used by another row.",
|
||
overridden: "Overridden",
|
||
overridesGlobal: "overrides global",
|
||
keySet: "key set",
|
||
keyRef: "credential ref",
|
||
keyConfigured: "credential configured",
|
||
keyRefMissing: "credential missing",
|
||
keyMissing: "key missing",
|
||
legacyKey: "legacy key",
|
||
legacyKeyBlocked: "This server still uses a hidden legacy literal key. Enter a replacement key or credential reference before changing the server list.",
|
||
|
||
customKind: "Custom",
|
||
reset: "Reset to default",
|
||
readOnly: "This deployment stores settings read-only.",
|
||
save: "Save",
|
||
saving: "Saving…",
|
||
discard: "Discard",
|
||
unsaved: "Unsaved",
|
||
saveFailed: "The deployment did not accept these values; they were left for you to correct.",
|
||
placeholderUrl: "https://mcp.example.com/mcp/",
|
||
placeholderCommand: "npx",
|
||
placeholderArgs: "-y, duckduckgo-mcp-server",
|
||
placeholderToolName: "tavily_search",
|
||
placeholderAuthParam: "tavilyApiKey"
|
||
};
|
||
const zh = {
|
||
title: "搜索 MCP",
|
||
description: "web_search 工具背后的搜索 MCP 服务器;插件启用期间内置 DeepSeek 搜索保持禁用。",
|
||
defaultServer: "默认服务器",
|
||
defaultServerHint: "使用哪一行服务器(留空 = 第一行)。",
|
||
maxResults: "结果数上限",
|
||
maxResultsHint: "每次搜索最多返回的条数(1–50;留空 = 默认)。",
|
||
searchTimeoutMs: "搜索超时(秒)",
|
||
searchTimeoutMsHint: "超过该时长即中止搜索。",
|
||
servers: "搜索 MCP 服务器",
|
||
serversHint: "选择提供商并填写 CDKey/API key。已知提供商的连接细节由 Host catalog 管理;自定义行才显示 MCP 高级设置。",
|
||
quickAdd: "常用提供商",
|
||
quickAddHint: "添加提供商行后填写 CDKey/API key 再保存。已知提供商不需要填写端点链接。",
|
||
addServer: "添加自定义服务器",
|
||
removeServer: "删除",
|
||
rowId: "ID",
|
||
rowIdHint: "“默认服务器”下拉框引用的稳定标识。",
|
||
rowKind: "提供商",
|
||
rowTransport: "传输方式",
|
||
rowUrl: "端点(URL)",
|
||
rowCommand: "命令",
|
||
rowArgs: "参数(逗号分隔)",
|
||
rowApiKey: "API 密钥",
|
||
cdKey: "CDKey / API key",
|
||
cdKeyHint: "请填写 CDKey / API key",
|
||
keyNotRequired: "无需密钥",
|
||
|
||
rowApiKeyEnv: "密钥环境变量/凭证引用",
|
||
rowAuthStyle: "密钥位置",
|
||
rowAuthParam: "密钥参数名/环境变量名",
|
||
rowToolName: "MCP 工具名",
|
||
rowMaxResults: "结果数上限",
|
||
rowMaxResultsHint: "留空 = 跟随全局结果数上限。",
|
||
missingId: "ID 不能为空。",
|
||
dupId: "该 ID 已被另一行占用。",
|
||
overridden: "已覆盖",
|
||
overridesGlobal: "覆盖全局",
|
||
keySet: "已填密钥",
|
||
keyRef: "凭证引用",
|
||
keyConfigured: "凭证已配置",
|
||
keyRefMissing: "凭证未配置",
|
||
keyMissing: "缺少密钥",
|
||
legacyKey: "旧版密钥",
|
||
legacyKeyBlocked: "此服务器仍使用客户端不可见的旧版字面密钥。修改服务器列表前,请输入替代密钥或凭证引用。",
|
||
|
||
customKind: "自定义",
|
||
reset: "恢复默认",
|
||
readOnly: "本部署的设置为只读。",
|
||
save: "保存",
|
||
saving: "保存中…",
|
||
discard: "放弃修改",
|
||
unsaved: "未保存",
|
||
saveFailed: "本部署没有接受这些值,已保留供你修改。",
|
||
placeholderUrl: "https://mcp.example.com/mcp/",
|
||
placeholderCommand: "npx",
|
||
placeholderArgs: "-y, duckduckgo-mcp-server",
|
||
placeholderToolName: "tavily_search",
|
||
placeholderAuthParam: "tavilyApiKey"
|
||
};
|
||
//#endregion
|
||
|
||
/** Namespace of this plugin's settings section (spelled, not imported). */
|
||
const NS = "search-mcp";
|
||
|
||
//#region row helpers
|
||
function emptyRow() {
|
||
return {
|
||
id: "",
|
||
kind: "custom",
|
||
transport: "http",
|
||
url: "",
|
||
command: "",
|
||
args: "",
|
||
apiKey: "",
|
||
apiKeyEnv: "",
|
||
authStyle: "",
|
||
authParam: "",
|
||
authPrefix: "",
|
||
toolName: "",
|
||
maxResults: ""
|
||
};
|
||
}
|
||
/** Convert a stored server entry (host shape) to a row draft. */
|
||
function rowFromEntry(entry) {
|
||
const kind = Object.hasOwn(CATALOG, entry.kind) ? entry.kind : "custom";
|
||
const known = kind !== "custom";
|
||
return {
|
||
id: entry.id ?? "",
|
||
kind,
|
||
transport: known ? "" : (entry.transport ?? "http"),
|
||
url: known ? "" : (entry.url ?? ""),
|
||
command: known ? "" : (entry.command ?? ""),
|
||
args: known ? "" : (Array.isArray(entry.args) ? entry.args.join(", ") : ""),
|
||
apiKey: entry.apiKey ?? "",
|
||
apiKeyEnv: entry.apiKeyEnv ?? "",
|
||
authStyle: known ? "" : (entry.authStyle ?? ""),
|
||
authParam: known ? "" : (entry.authParam ?? ""),
|
||
authPrefix: known ? "" : (entry.authPrefix ?? ""),
|
||
toolName: known ? "" : (entry.toolName ?? ""),
|
||
maxResults: entry.maxResults === undefined ? "" : String(entry.maxResults)
|
||
};
|
||
}
|
||
/** Convert a row draft to the minimal known entry or full custom entry. */
|
||
function entryFromRow(row) {
|
||
const entry = { id: row.id.trim(), kind: row.kind };
|
||
const known = Object.hasOwn(CATALOG, row.kind) && row.kind !== "custom";
|
||
if (!known) {
|
||
entry.transport = row.transport || "http";
|
||
if (entry.transport === "stdio") {
|
||
if (row.command.trim() !== "") entry.command = row.command.trim();
|
||
const args = row.args.split(",").map((a) => a.trim()).filter(Boolean);
|
||
if (args.length > 0) entry.args = args;
|
||
} else {
|
||
const url = normalizeServerUrl(row);
|
||
if (url !== "") entry.url = url;
|
||
}
|
||
if (row.authStyle !== "") entry.authStyle = row.authStyle;
|
||
if (row.authParam.trim() !== "") entry.authParam = row.authParam.trim();
|
||
const prefix = resolveAuthPrefix(row);
|
||
if (prefix !== "") entry.authPrefix = prefix;
|
||
if (row.toolName.trim() !== "") entry.toolName = row.toolName.trim();
|
||
}
|
||
if (row.apiKey.trim() !== "") entry.apiKey = row.apiKey.trim();
|
||
if (row.apiKeyEnv.trim() !== "") entry.apiKeyEnv = row.apiKeyEnv.trim();
|
||
if (row.maxResults.trim() !== "") {
|
||
const n = Number(row.maxResults);
|
||
if (Number.isFinite(n) && n > 0) entry.maxResults = Math.round(n);
|
||
}
|
||
return entry;
|
||
}
|
||
/** Changing provider starts a clean credential/connection draft. */
|
||
function applyKindDefaults(row, kind) {
|
||
const next = { ...row, kind, apiKey: "", apiKeyEnv: "" };
|
||
if (kind !== "custom") {
|
||
return { ...next, transport: "", url: "", command: "", args: "", authStyle: "", authParam: "", toolName: "" };
|
||
}
|
||
return { ...next, transport: row.transport || "http" };
|
||
}
|
||
|
||
/** Suggest a unique row id for a preset kind (tavily, tavily2, …). */
|
||
function suggestId(servers, kind) {
|
||
const used = new Set(servers.map((row) => row.id.trim()).filter(Boolean));
|
||
let id = kind;
|
||
let n = 2;
|
||
while (used.has(id)) id = `${kind}${n++}`;
|
||
return id;
|
||
}
|
||
/** Brand label for a kind, localized for `custom`. */
|
||
function kindLabel(kind, t) {
|
||
if (kind === "custom") return t("customKind");
|
||
return KIND_LABELS[kind] ?? kind;
|
||
}
|
||
function deepEqualJson(left, right) {
|
||
return JSON.stringify(left) === JSON.stringify(right);
|
||
}
|
||
/** Snapshot-safe copy — `createSnapshotStore.set` deep-freezes state in
|
||
* non-production, so the live editable draft must never be published by reference. */
|
||
function cloneDraft(draft) {
|
||
return {
|
||
defaultServer: draft.defaultServer,
|
||
maxResults: draft.maxResults,
|
||
searchTimeoutMs: draft.searchTimeoutMs,
|
||
servers: draft.servers.map((row) => ({ ...row })),
|
||
};
|
||
}
|
||
function credentialRefFor(row) {
|
||
if (row.apiKeyEnv.trim() !== "") return row.apiKeyEnv.trim();
|
||
const url = (row.url || "").toLowerCase();
|
||
if (url.includes("dashscope.aliyuncs.com")) return "DASHSCOPE_API_KEY";
|
||
const id = row.id.trim().replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
||
return `SEARCH_MCP_${id || "SERVER"}_API_KEY`;
|
||
}
|
||
/** Normalize Bailian WebSearch endpoint if the user truncated it. */
|
||
function normalizeServerUrl(row) {
|
||
let url = (row.url || "").trim();
|
||
if (!url) return url;
|
||
if (row.toolName === "bailian_web_search" || url.includes("dashscope.aliyuncs.com")) {
|
||
if (url === "https://dashscope.aliyuncs.com/api/v1"
|
||
|| url === "https://dashscope.aliyuncs.com/api/v1/"
|
||
|| /\/api\/v1\/?$/.test(url) && url.includes("dashscope")) {
|
||
url = "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp";
|
||
}
|
||
if (url.endsWith("/WebSearch/sse")) {
|
||
url = url.replace(/\/WebSearch\/sse$/, "/WebSearch/mcp");
|
||
}
|
||
}
|
||
return url;
|
||
}
|
||
function resolveAuthPrefix(row) {
|
||
const explicit = (row.authPrefix || "").trim();
|
||
if (explicit !== "") return explicit.endsWith(" ") ? explicit : `${explicit} `;
|
||
if (row.authStyle === "header" && row.authParam.trim() === "Authorization") return "Bearer ";
|
||
return "";
|
||
}
|
||
const CREDENTIAL_DESCRIBE_BATCH_SIZE = 64;
|
||
/** 0.1.2+ Remote face: `remote.credentials.describe(refs)` → `{ ok, value }`. */
|
||
async function describeCredentialRefs(remote, refs) {
|
||
const states = {};
|
||
for (let index = 0; index < refs.length; index += CREDENTIAL_DESCRIBE_BATCH_SIZE) {
|
||
const batch = refs.slice(index, index + CREDENTIAL_DESCRIBE_BATCH_SIZE);
|
||
const response = await remote.credentials.describe(batch);
|
||
if (!response.ok) throw new Error(response.error?.message || "credential state lookup failed");
|
||
Object.assign(states, response.value ?? {});
|
||
}
|
||
return states;
|
||
}
|
||
async function rollbackNewCredentialRefs(remote, refs) {
|
||
for (const ref of [...new Set(refs)].reverse()) {
|
||
try {
|
||
await remote.credentials.unset(ref);
|
||
} catch {
|
||
// The save already failed; preserve the original error state.
|
||
}
|
||
}
|
||
}
|
||
async function rollbackSettingsWrites(scope, writes) {
|
||
for (const [field, previous] of writes.reverse()) {
|
||
try {
|
||
if (previous === undefined) await scope.unset(field);
|
||
else await scope.set(field, previous);
|
||
} catch {
|
||
// The UI remains failed so the user can retry or discard the draft.
|
||
}
|
||
}
|
||
}
|
||
//#endregion
|
||
|
||
|
||
//#region controller (whole-section staged draft)
|
||
var SearchMcpCardController = class {
|
||
constructor(scope, remote, describeFace) {
|
||
this.scope = scope;
|
||
this.remote = remote;
|
||
this.describeFace = describeFace;
|
||
this.draft = null;
|
||
this.listeners = new Set();
|
||
this.saving = false;
|
||
this.failed = false;
|
||
this.failDetail = "";
|
||
this.legacyBlocked = false;
|
||
this.secretStateReady = false;
|
||
this.legacySecretIds = new Set();
|
||
this.credentialStates = {};
|
||
this.store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.project());
|
||
scope.subscribe(() => {
|
||
if (this.draft === null) this.publish();
|
||
this.readSecretState();
|
||
});
|
||
this.readSecretState();
|
||
}
|
||
section() {
|
||
return this.scope.getSnapshot();
|
||
}
|
||
value() {
|
||
return this.section().value ?? {};
|
||
}
|
||
userLayer() {
|
||
return this.section().user;
|
||
}
|
||
baseDraft() {
|
||
const value = this.value();
|
||
return {
|
||
defaultServer: typeof value.defaultServer === "string" ? value.defaultServer : "",
|
||
maxResults: typeof value.maxResults === "number" ? String(value.maxResults) : "",
|
||
searchTimeoutMs: typeof value.searchTimeoutMs === "number" ? String(value.searchTimeoutMs) : "",
|
||
servers: Array.isArray(value.servers) ? value.servers.map((entry) => ({
|
||
...rowFromEntry(entry),
|
||
legacySecret: this.legacySecretIds.has(entry.id)
|
||
})) : []
|
||
};
|
||
}
|
||
async readSecretState() {
|
||
let view;
|
||
try {
|
||
const response = await this.remote.settings.describe();
|
||
if (!response.ok) {
|
||
this.secretStateReady = true;
|
||
this.publish();
|
||
return;
|
||
}
|
||
view = (response.value?.namespaces ?? []).find((candidate) => candidate.ns === NS);
|
||
} catch {
|
||
this.secretStateReady = true;
|
||
this.publish();
|
||
return;
|
||
}
|
||
const servers = Array.isArray(view?.value?.servers) ? view.value.servers : [];
|
||
const ids = new Set();
|
||
for (const secret of view?.secrets ?? []) {
|
||
if (!secret.set || secret.path?.[0] !== "servers" || secret.path?.[2] !== "apiKey") continue;
|
||
const index = Number(secret.path[1]);
|
||
const id = Number.isInteger(index) ? servers[index]?.id : undefined;
|
||
if (typeof id === "string" && id.length > 0) ids.add(id);
|
||
}
|
||
const wasReady = this.secretStateReady;
|
||
const changed = !deepEqualJson([...ids].sort(), [...this.legacySecretIds].sort());
|
||
this.legacySecretIds = ids;
|
||
const previousCredentialStates = this.credentialStates;
|
||
const refs = [...new Set(servers.map((entry) => typeof entry.apiKeyEnv === "string" ? entry.apiKeyEnv.trim() : "").filter(Boolean))];
|
||
if (refs.length > 0) {
|
||
try {
|
||
this.credentialStates = await describeCredentialRefs(this.remote, refs);
|
||
} catch {
|
||
this.credentialStates = {};
|
||
}
|
||
} else {
|
||
this.credentialStates = {};
|
||
}
|
||
|
||
this.secretStateReady = true;
|
||
const credentialsChanged = !deepEqualJson(previousCredentialStates, this.credentialStates);
|
||
if (this.draft === null && (changed || credentialsChanged || !wasReady)) this.publish();
|
||
}
|
||
|
||
|
||
ensureDraft() {
|
||
if (this.draft === null) this.draft = this.baseDraft();
|
||
return this.draft;
|
||
}
|
||
project() {
|
||
const snapshot = this.section();
|
||
const draft = this.draft;
|
||
const sectionValue = snapshot.value ?? {};
|
||
const dirty = draft !== null && JSON.stringify(draft) !== JSON.stringify(this.baseDraft());
|
||
const ids = draft !== null ? draft.servers.map((row) => row.id.trim()) : [];
|
||
const invalid = draft !== null && (ids.some((id) => id === "") || new Set(ids.filter(Boolean)).size !== ids.filter(Boolean).length);
|
||
const serverKinds = draft !== null
|
||
? Object.fromEntries(draft.servers.map((row) => [row.id.trim(), row.kind]))
|
||
: Object.fromEntries((Array.isArray(sectionValue.servers) ? sectionValue.servers : []).map((s) => [s.id, s.kind]));
|
||
return {
|
||
available: snapshot.status === "ready",
|
||
// Do not gate edits on secretStateReady — a failed describe must not freeze the form.
|
||
writable: snapshot.writable === true,
|
||
|
||
dirty,
|
||
invalid,
|
||
saving: this.saving,
|
||
failed: this.failed,
|
||
failDetail: this.failDetail,
|
||
legacyBlocked: this.legacyBlocked,
|
||
|
||
overridden: this.userLayer() !== null && typeof this.userLayer() === "object" && !Array.isArray(this.userLayer())
|
||
? Object.keys(this.userLayer()).length > 0
|
||
: false,
|
||
value: draft !== null ? cloneDraft(draft) : this.baseDraft(),
|
||
serverIds: draft !== null
|
||
? ids.filter(Boolean)
|
||
: (Array.isArray(sectionValue.servers) ? sectionValue.servers.map((s) => s.id).filter(Boolean) : []),
|
||
serverKinds,
|
||
credentialStates: this.credentialStates
|
||
|
||
};
|
||
}
|
||
publish() {
|
||
this.store.set(this.project());
|
||
}
|
||
actions() {
|
||
return {
|
||
editScalar: (field, text) => {
|
||
this.ensureDraft()[field] = text;
|
||
this.failed = false;
|
||
this.legacyBlocked = false;
|
||
this.publish();
|
||
|
||
},
|
||
editRow: (index, field, value) => {
|
||
const draft = this.ensureDraft();
|
||
draft.servers[index] = { ...draft.servers[index], [field]: value };
|
||
this.failed = false;
|
||
this.legacyBlocked = false;
|
||
this.publish();
|
||
|
||
},
|
||
changeKind: (index, kind) => {
|
||
const draft = this.ensureDraft();
|
||
draft.servers[index] = applyKindDefaults(draft.servers[index], kind);
|
||
this.failed = false;
|
||
this.legacyBlocked = false;
|
||
|
||
this.publish();
|
||
},
|
||
addServer: () => {
|
||
const draft = this.ensureDraft();
|
||
draft.servers.push(emptyRow());
|
||
this.failed = false;
|
||
this.legacyBlocked = false;
|
||
|
||
this.publish();
|
||
},
|
||
quickAdd: (kind) => {
|
||
const draft = this.ensureDraft();
|
||
const id = suggestId(draft.servers, kind);
|
||
const row = applyKindDefaults({ ...emptyRow(), id, kind }, kind);
|
||
draft.servers.push(row);
|
||
this.failed = false;
|
||
this.legacyBlocked = false;
|
||
|
||
this.publish();
|
||
},
|
||
removeServer: (index) => {
|
||
const draft = this.ensureDraft();
|
||
draft.servers.splice(index, 1);
|
||
this.failed = false;
|
||
this.legacyBlocked = false;
|
||
|
||
this.publish();
|
||
},
|
||
discard: () => {
|
||
if (this.draft === null && !this.failed && !this.legacyBlocked) return;
|
||
this.draft = null;
|
||
this.failed = false;
|
||
this.failDetail = "";
|
||
this.legacyBlocked = false;
|
||
this.publish();
|
||
|
||
},
|
||
save: () => {
|
||
this.save();
|
||
}
|
||
};
|
||
}
|
||
async save() {
|
||
if (this.draft === null || this.saving) return;
|
||
|
||
const draft = this.draft;
|
||
const ids = draft.servers.map((row) => row.id.trim());
|
||
const invalid = ids.some((id) => id === "") || new Set(ids.filter(Boolean)).size !== ids.filter(Boolean).length;
|
||
if (invalid) return;
|
||
const base = this.baseDraft();
|
||
const serversChanged = !deepEqualJson(draft.servers, base.servers);
|
||
if (serversChanged) {
|
||
const blocked = draft.servers.some((row) => row.legacySecret && row.apiKey.trim() === "" && row.apiKeyEnv.trim() === "");
|
||
if (blocked) {
|
||
this.legacyBlocked = true;
|
||
this.publish();
|
||
return;
|
||
}
|
||
}
|
||
this.saving = true;
|
||
this.failed = false;
|
||
this.failDetail = "";
|
||
this.legacyBlocked = false;
|
||
this.publish();
|
||
|
||
let landed = true;
|
||
let failDetail = "";
|
||
let serverEntries;
|
||
const createdCredentialRefs = [];
|
||
if (serversChanged) {
|
||
serverEntries = [];
|
||
const credentialWrites = draft.servers
|
||
.filter((row) => row.apiKey.trim() !== "")
|
||
.map((row) => ({ ref: credentialRefFor(row), value: row.apiKey.trim() }));
|
||
let credentialBefore = {};
|
||
try {
|
||
if (credentialWrites.length > 0) {
|
||
credentialBefore = await describeCredentialRefs(this.remote, [...new Set(credentialWrites.map(({ ref }) => ref))]);
|
||
}
|
||
} catch (error) {
|
||
landed = false;
|
||
failDetail = error instanceof Error ? error.message : String(error);
|
||
}
|
||
if (landed && credentialWrites.some(({ ref }) => credentialBefore[ref]?.writable === false)) {
|
||
landed = false;
|
||
failDetail = "credential is not writable";
|
||
}
|
||
if (landed) {
|
||
for (const { ref, value } of credentialWrites) {
|
||
if (!credentialBefore[ref]?.configured) createdCredentialRefs.push(ref);
|
||
try {
|
||
const response = await this.remote.credentials.set(ref, value);
|
||
if (!response.ok) {
|
||
landed = false;
|
||
failDetail = response.error?.message || "credentials.set failed";
|
||
break;
|
||
}
|
||
} catch (error) {
|
||
landed = false;
|
||
failDetail = error instanceof Error ? error.message : String(error);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!landed) {
|
||
await rollbackNewCredentialRefs(this.remote, createdCredentialRefs);
|
||
this.saving = false;
|
||
this.failed = true;
|
||
this.failDetail = failDetail;
|
||
this.publish();
|
||
return;
|
||
}
|
||
for (const row of draft.servers) {
|
||
const entry = entryFromRow(row);
|
||
if (row.apiKey.trim() !== "") {
|
||
delete entry.apiKey;
|
||
entry.apiKeyEnv = credentialRefFor(row);
|
||
} else if (!entry.apiKeyEnv && (row.url || "").includes("dashscope")) {
|
||
entry.apiKeyEnv = "DASHSCOPE_API_KEY";
|
||
}
|
||
delete entry.legacySecret;
|
||
serverEntries.push(entry);
|
||
}
|
||
}
|
||
|
||
const ops = [];
|
||
const planned = [
|
||
["defaultServer", draft.defaultServer.trim(), base.defaultServer],
|
||
["maxResults", draft.maxResults.trim() === "" ? undefined : Number(draft.maxResults), base.maxResults === "" ? undefined : Number(base.maxResults)],
|
||
["searchTimeoutMs", draft.searchTimeoutMs.trim() === "" ? undefined : Number(draft.searchTimeoutMs), base.searchTimeoutMs === "" ? undefined : Number(base.searchTimeoutMs)],
|
||
...(serversChanged ? [["servers", serverEntries, this.value().servers]] : [])
|
||
];
|
||
for (const [field, value, previous] of planned) {
|
||
if (deepEqualJson(value, previous)) continue;
|
||
if (value === undefined) ops.push({ op: "unset", path: [field] });
|
||
else ops.push({ op: "set", path: [field], value });
|
||
}
|
||
|
||
if (ops.length > 0) {
|
||
try {
|
||
const revision = this.section().revision;
|
||
const response = await this.remote.settings.mutate(NS, ops, revision);
|
||
if (!response.ok) {
|
||
landed = false;
|
||
failDetail = response.error?.message || response.error?.code || "settings.mutate failed";
|
||
try {
|
||
if (typeof this.describeFace?.load === "function") await this.describeFace.load();
|
||
else if (typeof this.describeFace?.ensure === "function") await this.describeFace.ensure();
|
||
} catch { /* keep draft */ }
|
||
} else if (response.value && typeof this.describeFace?.acceptView === "function") {
|
||
this.describeFace.acceptView(response.value);
|
||
}
|
||
} catch (error) {
|
||
landed = false;
|
||
failDetail = error instanceof Error ? error.message : String(error);
|
||
}
|
||
}
|
||
|
||
if (!landed) {
|
||
await rollbackNewCredentialRefs(this.remote, createdCredentialRefs);
|
||
} else {
|
||
this.draft = null;
|
||
}
|
||
this.saving = false;
|
||
this.failed = !landed;
|
||
this.failDetail = landed ? "" : failDetail;
|
||
this.publish();
|
||
if (landed) this.readSecretState();
|
||
}
|
||
|
||
inject() {
|
||
return {
|
||
hooks: { searchMcpCard: this.store },
|
||
...this.actions()
|
||
};
|
||
}
|
||
};
|
||
//#endregion
|
||
|
||
//#region components
|
||
function SearchMcpCard(props) {
|
||
const { t } = props;
|
||
const [open, setOpen] = (0, react.useState)(false);
|
||
const state = props.useSearchMcpCard((snapshot) => snapshot);
|
||
if (!state.available) return null;
|
||
const disabled = !state.writable;
|
||
const value = state.value;
|
||
const title = t("title");
|
||
const serverOptions = [
|
||
{ value: "", label: "—" },
|
||
...state.serverIds.map((id) => ({ value: id, label: `${id} · ${kindLabel(state.serverKinds[id], t)}` }))
|
||
];
|
||
const rowIds = value.servers.map((row) => row.id.trim()).filter(Boolean);
|
||
const dupIds = new Set(rowIds.filter((id, index) => rowIds.indexOf(id) !== index));
|
||
return (0, react_jsx_runtime.jsxs)("li", {
|
||
className: "smcp_card" + (open ? " smcp_cardOpen" : ""),
|
||
children: [
|
||
(0, react_jsx_runtime.jsxs)("button", {
|
||
type: "button",
|
||
className: "smcp_headBtn",
|
||
"aria-expanded": open,
|
||
"aria-label": `${open ? "收起" : "展开"}: ${title}`,
|
||
onClick: () => {
|
||
setOpen(!open);
|
||
},
|
||
children: [
|
||
(0, react_jsx_runtime.jsxs)("span", {
|
||
className: "smcp_head",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_name", children: title }),
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_desc", children: t("description") }),
|
||
state.overridden ? (0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: t("overridden") }) : null,
|
||
state.dirty ? (0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("unsaved") }) : null
|
||
]
|
||
}),
|
||
(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {
|
||
className: "smcp_chevron" + (open ? " smcp_chevronOpen" : "")
|
||
})
|
||
]
|
||
}),
|
||
open ? (0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_body",
|
||
children: [
|
||
!state.writable ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("readOnly") }) : null,
|
||
(0, react_jsx_runtime.jsx)(SelectField, {
|
||
id: "smcp-default-server",
|
||
label: t("defaultServer"),
|
||
hint: t("defaultServerHint"),
|
||
disabled,
|
||
options: serverOptions,
|
||
value: value.defaultServer,
|
||
onEdit: (text) => props.editScalar("defaultServer", text)
|
||
}),
|
||
(0, react_jsx_runtime.jsx)(TextField, {
|
||
id: "smcp-max-results",
|
||
label: t("maxResults"),
|
||
hint: t("maxResultsHint"),
|
||
numeric: true,
|
||
disabled,
|
||
value: value.maxResults,
|
||
onEdit: (text) => props.editScalar("maxResults", text)
|
||
}),
|
||
(0, react_jsx_runtime.jsx)(TextField, {
|
||
id: "smcp-timeout",
|
||
label: t("searchTimeoutMs"),
|
||
hint: t("searchTimeoutMsHint"),
|
||
numeric: true,
|
||
disabled,
|
||
value: value.searchTimeoutMs === "" ? "" : String(Math.round(Number(value.searchTimeoutMs) / 1000)),
|
||
onEdit: (text) => props.editScalar("searchTimeoutMs", text === "" ? "" : String(Math.round((Number(text) || 0) * 1000)))
|
||
}),
|
||
(0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_field",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_label", children: t("servers") }),
|
||
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("serversHint") }),
|
||
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: t("quickAddHint") }),
|
||
(0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_quickAdd",
|
||
children: QUICK_KINDS.map((kind) => (0, react_jsx_runtime.jsx)(
|
||
"button",
|
||
{
|
||
type: "button",
|
||
className: "smcp_quickBtn",
|
||
disabled,
|
||
onClick: () => props.quickAdd(kind),
|
||
children: `+ ${KIND_LABELS[kind]}`
|
||
},
|
||
`smcp-quick-${kind}`
|
||
))
|
||
}),
|
||
value.servers.map((row, index) => (0, react_jsx_runtime.jsx)(
|
||
ServerRow,
|
||
{
|
||
t,
|
||
row,
|
||
index,
|
||
disabled,
|
||
dupIds,
|
||
credentialStates: state.credentialStates,
|
||
|
||
onEdit: (field, val) => props.editRow(index, field, val),
|
||
onKind: (kind) => props.changeKind(index, kind),
|
||
onRemove: () => props.removeServer(index)
|
||
},
|
||
`smcp-row-${index}`
|
||
)),
|
||
(0, react_jsx_runtime.jsx)("button", {
|
||
type: "button",
|
||
className: "smcp_add",
|
||
disabled,
|
||
onClick: props.addServer,
|
||
children: `+ ${t("addServer")}`
|
||
})
|
||
]
|
||
}),
|
||
state.legacyBlocked ? (0, react_jsx_runtime.jsx)("p", { role: "status", className: "smcp_error", children: t("legacyKeyBlocked") }) : null,
|
||
|
||
(0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_footer",
|
||
children: [
|
||
state.failed ? (0, react_jsx_runtime.jsx)("p", { role: "status", className: "smcp_failDetail", children: state.failDetail ? (t("saveFailed") + " — " + state.failDetail) : t("saveFailed") }) : null,
|
||
(0, react_jsx_runtime.jsx)("button", {
|
||
type: "button",
|
||
className: "smcp_btn smcp_btnGhost",
|
||
disabled: !state.dirty || state.saving,
|
||
onClick: props.discard,
|
||
children: t("discard")
|
||
}),
|
||
(0, react_jsx_runtime.jsx)("button", {
|
||
type: "button",
|
||
className: "smcp_btn smcp_btnPrimary",
|
||
disabled: !state.dirty || state.invalid || state.saving,
|
||
onClick: props.save,
|
||
children: t(state.saving ? "saving" : "save")
|
||
})
|
||
]
|
||
})
|
||
]
|
||
}) : null
|
||
]
|
||
});
|
||
}
|
||
function ServerRow(props) {
|
||
const { t, row, index, disabled, onEdit, onKind, onRemove } = props;
|
||
const [rowOpen, setRowOpen] = (0, react.useState)(false);
|
||
const rowInvalid = row.id.trim() === "";
|
||
const dup = !rowInvalid && row.id.trim() !== "" && props.dupIds && props.dupIds.has(row.id.trim());
|
||
const known = row.kind !== "custom";
|
||
const needsKey = (CATALOG[row.kind] ?? CATALOG.custom).needsKey;
|
||
const hasKey = row.apiKey.trim() !== "";
|
||
const hasKeyEnv = row.apiKeyEnv.trim() !== "";
|
||
const hasConfiguredRef = hasKeyEnv && props.credentialStates?.[row.apiKeyEnv.trim()]?.configured === true;
|
||
const hasLegacyKey = row.legacySecret === true;
|
||
const overridesGlobal = row.maxResults.trim() !== "";
|
||
const summary = known ? (needsKey ? (hasKeyEnv ? `${t("keyRef")}: ${row.apiKeyEnv.trim()}` : t("cdKeyHint")) : t("keyNotRequired")) : (row.transport === "stdio"
|
||
? ((row.command.trim() !== "" ? row.command.trim() : "") + (row.args.trim() !== "" ? " " + row.args.trim() : "")).trim() || row.id
|
||
: (row.url.trim() !== "" ? row.url.trim() : row.id));
|
||
const badges = [];
|
||
if (hasKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: t("keySet") }, "badge-key"));
|
||
else if (hasConfiguredRef) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badge", children: `${t("keyConfigured")}: ${row.apiKeyEnv.trim()}` }, "badge-env"));
|
||
else if (hasKeyEnv) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeDanger", children: `${t("keyRefMissing")}: ${row.apiKeyEnv.trim()}` }, "badge-env-missing"));
|
||
else if (hasLegacyKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("legacyKey") }, "badge-legacy-key"));
|
||
else if (needsKey) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeDanger", children: t("keyMissing") }, "badge-key-missing"));
|
||
if (overridesGlobal) badges.push((0, react_jsx_runtime.jsx)("span", { className: "smcp_badgeMuted", children: t("overridesGlobal") }, "badge-override"));
|
||
return (0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_row",
|
||
children: [
|
||
(0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_rowHead",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("button", {
|
||
type: "button",
|
||
className: "smcp_rowToggle" + (rowOpen ? " smcp_rowToggleOpen" : ""),
|
||
"aria-expanded": rowOpen,
|
||
"aria-label": `${rowOpen ? "收起" : "展开"} ${row.id || t("rowId")}`,
|
||
disabled,
|
||
onClick: () => setRowOpen(!rowOpen),
|
||
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
||
}),
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_kindBadge", children: kindLabel(row.kind, t) }),
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_rowTitle" + (rowInvalid ? " smcp_invalid" : ""), children: row.id.trim() !== "" ? row.id.trim() : t("rowId") }),
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_rowSummary", children: summary }),
|
||
...badges,
|
||
(0, react_jsx_runtime.jsx)("button", { type: "button", className: "smcp_del", disabled, onClick: onRemove, children: t("removeServer") })
|
||
]
|
||
}),
|
||
rowInvalid ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_error", children: t("missingId") }) : null,
|
||
dup ? (0, react_jsx_runtime.jsx)("p", { className: "smcp_error", children: t("dupId") }) : null,
|
||
rowOpen ? (0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_rowBody",
|
||
children: known ? [
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowId"), placeholder: t("rowIdHint"), value: row.id, disabled, onEdit: (v) => onEdit("id", v) }),
|
||
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid3", children: [
|
||
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowKind"), options: KIND_OPTIONS, value: row.kind, disabled, onEdit: onKind }),
|
||
needsKey ? (0, react_jsx_runtime.jsx)(CellInput, { label: t("cdKey"), type: "password", placeholder: t("cdKeyHint"), value: row.apiKey, disabled, onEdit: (v) => onEdit("apiKey", v) }) : null,
|
||
needsKey ? (0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKeyEnv"), placeholder: t("rowApiKeyEnv"), value: row.apiKeyEnv, disabled, onEdit: (v) => onEdit("apiKeyEnv", v) }) : null
|
||
] }),
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowMaxResults"), placeholder: t("rowMaxResultsHint"), value: row.maxResults, disabled, onEdit: (v) => onEdit("maxResults", v) })
|
||
] : [
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowId"), placeholder: t("rowIdHint"), value: row.id, disabled, onEdit: (v) => onEdit("id", v) }),
|
||
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid3", children: [
|
||
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowKind"), options: KIND_OPTIONS, value: row.kind, disabled, onEdit: onKind }),
|
||
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowTransport"), options: TRANSPORT_OPTIONS, value: row.transport, disabled, onEdit: (v) => onEdit("transport", v) }),
|
||
(0, react_jsx_runtime.jsx)(CellSelect, { label: t("rowAuthStyle"), options: AUTH_STYLE_OPTIONS, value: row.authStyle, disabled, onEdit: (v) => onEdit("authStyle", v) })
|
||
] }),
|
||
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowUrl"), placeholder: t("placeholderUrl"), value: row.url, disabled, onEdit: (v) => onEdit("url", v) }),
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKey"), type: "password", placeholder: t("cdKeyHint"), value: row.apiKey, disabled, onEdit: (v) => onEdit("apiKey", v) })
|
||
] }),
|
||
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowApiKeyEnv"), placeholder: t("rowApiKeyEnv"), value: row.apiKeyEnv, disabled, onEdit: (v) => onEdit("apiKeyEnv", v) }),
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowAuthParam"), placeholder: t("placeholderAuthParam"), value: row.authParam, disabled, onEdit: (v) => onEdit("authParam", v) })
|
||
] }),
|
||
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowCommand"), placeholder: t("placeholderCommand"), value: row.command, disabled, onEdit: (v) => onEdit("command", v) }),
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowArgs"), placeholder: t("placeholderArgs"), value: row.args, disabled, onEdit: (v) => onEdit("args", v) })
|
||
] }),
|
||
(0, react_jsx_runtime.jsxs)("div", { className: "smcp_rowGrid", children: [
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowToolName"), placeholder: t("placeholderToolName"), value: row.toolName, disabled, onEdit: (v) => onEdit("toolName", v) }),
|
||
(0, react_jsx_runtime.jsx)(CellInput, { label: t("rowMaxResults"), placeholder: t("rowMaxResultsHint"), value: row.maxResults, disabled, onEdit: (v) => onEdit("maxResults", v) })
|
||
] })
|
||
]
|
||
}) : null
|
||
]
|
||
});
|
||
}
|
||
|
||
function CellInput(props) {
|
||
return (0, react_jsx_runtime.jsxs)("label", {
|
||
className: "smcp_cell",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_cellLabel", children: props.label }),
|
||
(0, react_jsx_runtime.jsx)("input", {
|
||
className: "smcp_input",
|
||
type: props.type ?? "text",
|
||
placeholder: props.placeholder,
|
||
value: props.value,
|
||
disabled: props.disabled,
|
||
onChange: (e) => props.onEdit(e.target.value)
|
||
})
|
||
]
|
||
});
|
||
}
|
||
function CellSelect(props) {
|
||
return (0, react_jsx_runtime.jsxs)("label", {
|
||
className: "smcp_cell",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("span", { className: "smcp_cellLabel", children: props.label }),
|
||
(0, react_jsx_runtime.jsx)("select", {
|
||
className: "smcp_select",
|
||
value: props.value,
|
||
disabled: props.disabled,
|
||
onChange: (e) => props.onEdit(e.target.value),
|
||
children: props.options.map((option) => (0, react_jsx_runtime.jsx)("option", { value: option, children: option === "" ? "—" : option }, option))
|
||
})
|
||
]
|
||
});
|
||
}
|
||
function TextField(props) {
|
||
return (0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_field",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("label", { className: "smcp_label", htmlFor: props.id, children: props.label }),
|
||
(0, react_jsx_runtime.jsx)("input", {
|
||
id: props.id,
|
||
className: "smcp_input",
|
||
type: "text",
|
||
inputMode: props.numeric ? "numeric" : undefined,
|
||
value: props.value,
|
||
disabled: props.disabled,
|
||
onChange: (e) => props.onEdit(e.target.value)
|
||
}),
|
||
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: props.hint })
|
||
]
|
||
});
|
||
}
|
||
function SelectField(props) {
|
||
const options = props.options.map((option) => typeof option === "string" ? { value: option, label: option } : option);
|
||
return (0, react_jsx_runtime.jsxs)("div", {
|
||
className: "smcp_field",
|
||
children: [
|
||
(0, react_jsx_runtime.jsx)("label", { className: "smcp_label", htmlFor: props.id, children: props.label }),
|
||
(0, react_jsx_runtime.jsx)("select", {
|
||
id: props.id,
|
||
className: "smcp_select",
|
||
value: props.value,
|
||
disabled: props.disabled,
|
||
onChange: (e) => props.onEdit(e.target.value),
|
||
children: options.map((option) => (0, react_jsx_runtime.jsx)("option", { value: option.value, children: option.label }, option.value))
|
||
}),
|
||
(0, react_jsx_runtime.jsx)("p", { className: "smcp_hint", children: props.hint })
|
||
]
|
||
});
|
||
}
|
||
//#endregion
|
||
|
||
//#region apply
|
||
/** Required client services (resolved by the client kernel). */
|
||
const inject = ["slots", "locale", "settingsScope", "remote", "remote.credentials", "remote.settings"];
|
||
|
||
|
||
function apply(ctx) {
|
||
const t = ctx.locale.bind(NS);
|
||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-search-mcp: section dictionaries");
|
||
const describeFace = ctx.settingsScope.describe();
|
||
const controller = new SearchMcpCardController(ctx.settingsScope.bind({ namespace: NS }), ctx.remote, describeFace);
|
||
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
|
||
const used = controller.baseDraft().servers.some((row) => row.apiKeyEnv.trim() === ref);
|
||
if (used) controller.readSecretState();
|
||
}), "dsh-search-mcp: credential status invalidation");
|
||
|
||
ctx.slots.inject("settings.plugin.item", function* () {
|
||
yield ctx.slots.register({
|
||
name: "settings.plugin.item",
|
||
key: NS,
|
||
order: 30,
|
||
|
||
locale: NS,
|
||
inject: () => controller.inject()
|
||
}, SearchMcpCard);
|
||
});
|
||
}
|
||
//#endregion
|
||
|
||
exports.apply = apply;
|
||
exports.inject = inject;
|
||
return module.exports;
|
||
}
|
||
});
|