feat(skills): merge unavailable tools into footnote, bind cache to token, prune stale caches, warn on remote plain HTTP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 23:37:16 +08:00
parent 056f2d4ea3
commit 05bfd97c6e
19 changed files with 1100 additions and 496 deletions

View file

@ -1070,6 +1070,40 @@ function serializeDisabledTools(names) {
return JSON.stringify([...new Set(names.map((n) => n.trim()).filter(Boolean))].sort());
}
// src/netx/http.ts
function normalizeApiUrl(raw) {
const trimmed = typeof raw === "string" ? raw.trim() : "";
if (!trimmed)
return { ok: false, error: "empty" };
let parsed;
try {
parsed = new URL(trimmed);
} catch {
return { ok: false, error: "invalid_url" };
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { ok: false, error: "unsupported_scheme" };
}
if (parsed.username || parsed.password) {
return { ok: false, error: "credentials_in_url" };
}
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
return { ok: true, url: `${parsed.origin}${path}${search}` };
}
function isInsecureRemoteApiUrl(raw) {
let url;
try {
url = new URL(raw.trim());
} catch {
return false;
}
if (url.protocol !== "http:")
return false;
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
return !(host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127\./.test(host));
}
// src/client/NetxopsCard.tsx
var jsx_runtime = require("react/jsx-runtime");
function phaseLocaleKey(phase) {
@ -1641,6 +1675,10 @@ function NetxopsCard(props) {
props.resetField("apiUrl");
}
}),
isInsecureRemoteApiUrl(state.apiUrl.text) ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-invalid",
children: t("apiUrlInsecure")
}) : null,
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-fieldGrid",
children: [
@ -2161,28 +2199,6 @@ function NetxopsCard(props) {
});
}
// src/netx/http.ts
function normalizeApiUrl(raw) {
const trimmed = typeof raw === "string" ? raw.trim() : "";
if (!trimmed)
return { ok: false, error: "empty" };
let parsed;
try {
parsed = new URL(trimmed);
} catch {
return { ok: false, error: "invalid_url" };
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { ok: false, error: "unsupported_scheme" };
}
if (parsed.username || parsed.password) {
return { ok: false, error: "credentials_in_url" };
}
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
return { ok: true, url: `${parsed.origin}${path}${search}` };
}
// src/client/snapshot-store.ts
function createSnapshotStore(init) {
let state = init;
@ -3228,6 +3244,7 @@ var en = {
sectionExport: "Session export",
apiUrl: "API URL",
apiUrlHint: "http://127.0.0.1:8890",
apiUrlInsecure: "Plain HTTP to a remote host: the token and the skills sent to the model are not encrypted. Use https://.",
apiUrlInvalid: "Enter an HTTP or HTTPS URL without credentials.",
lang: "API / alarm language",
thinkingLanguage: "Thinking language",
@ -3324,6 +3341,7 @@ var zh = {
sectionExport: "会话导出",
apiUrl: "API 地址",
apiUrlHint: "http://127.0.0.1:8890",
apiUrlInsecure: "连接的是远程主机且用的是明文 HTTP:token 和发给模型的 skill 都不加密,请改用 https://。",
apiUrlInvalid: "请输入不含账号密码的 HTTP 或 HTTPS 地址。",
lang: "API / 告警语言",
thinkingLanguage: "思考语言",