netxops/lib/client.js
oliver fd1a86ec2f Add Netx Ops Plugins settings card for URL and token.
Ship the browser half so Settings → Plugins can configure apiUrl/lang/python and NETX_API_TOKEN without OS env vars.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 22:16:14 +08:00

655 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

window.__ModuleLoader__.load({
id: "dsh-netxops",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) {
return this[key];
}
var __toCommonJS = (from) => {
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
if (entry)
return entry;
entry = __defProp({}, "__esModule", { value: true });
if (from && typeof from === "object" || typeof from === "function") {
for (var key of __getOwnPropNames(from))
if (!__hasOwnProp.call(entry, key))
__defProp(entry, key, {
get: __accessProp.bind(from, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
__moduleCache.set(from, entry);
return entry;
};
var __moduleCache;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: __exportSetter.bind(all, name)
});
};
// src/client/index.ts
var exports_client = {};
__export(exports_client, {
inject: () => inject,
apply: () => apply
});
module.exports = __toCommonJS(exports_client);
// src/client/NetxopsCard.tsx
var import_react = require("react");
// src/client/styles.ts
var STYLE_ID = "dsh-netxops-card-css";
var CSS = `
.dsh-nx-card{list-style:none;border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s}
.dsh-nx-card:hover{border-color:var(--dsw-alias-label-dimmed)}
.dsh-nx-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
.dsh-nx-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}
.dsh-nx-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
.dsh-nx-headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.dsh-nx-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
.dsh-nx-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
.dsh-nx-pending{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
.dsh-nx-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
.dsh-nx-chevronOpen{transform:rotate(180deg)}
.dsh-nx-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
.dsh-nx-readOnly{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
.dsh-nx-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}
.dsh-nx-field+.dsh-nx-field{border-top:.5px solid var(--dsw-alias-border-l2)}
.dsh-nx-fieldHead{display:flex;align-items:center;gap:8px}
.dsh-nx-label{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
.dsh-nx-badges{display:inline-flex;align-items:center;gap:8px}
.dsh-nx-badge{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
.dsh-nx-badgeMuted{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;color:var(--dsw-alias-label-tertiary)}
.dsh-nx-reset{border:none;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}
.dsh-nx-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
.dsh-nx-input{height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary)}
.dsh-nx-input:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}
.dsh-nx-inputInvalid{border-color:var(--dsw-alias-label-error)}
.dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
.dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
.dsh-nx-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 4px;border-top:.5px solid var(--dsw-alias-border-l2)}
.dsh-nx-failed{flex:1;min-width:0;margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
.dsh-nx-btn{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer}
.dsh-nx-discard{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary)}
.dsh-nx-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
.dsh-nx-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
.dsh-nx-btn:disabled{opacity:.4;cursor:default}
`;
function ensureStyles() {
if (typeof document === "undefined")
return;
if (document.getElementById(STYLE_ID))
return;
const tag = document.createElement("style");
tag.id = STYLE_ID;
tag.textContent = CSS;
document.head.appendChild(tag);
}
// src/client/NetxopsCard.tsx
var jsx_dev_runtime = require("react/jsx-dev-runtime");
function ValueField(props) {
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
className: "dsh-nx-field",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
className: "dsh-nx-fieldHead",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("label", {
className: "dsh-nx-label",
htmlFor: props.id,
children: props.label
}, undefined, false, undefined, this),
props.field.overridden ? /* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-badges",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-badge",
children: props.overriddenLabel
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("button", {
type: "button",
className: "dsh-nx-reset",
disabled: props.disabled,
onClick: props.onReset,
children: props.resetLabel
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this) : null
]
}, undefined, true, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("input", {
id: props.id,
className: props.field.invalid ? "dsh-nx-input dsh-nx-inputInvalid" : "dsh-nx-input",
type: "text",
value: props.field.text,
disabled: props.disabled,
"aria-invalid": props.field.invalid || undefined,
onChange: (event) => {
props.onEdit(event.target.value);
}
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("p", {
className: props.field.invalid ? "dsh-nx-invalid" : "dsh-nx-hint",
children: props.field.invalid ? props.invalidLabel : props.hint
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this);
}
function NetxopsCard(props) {
ensureStyles();
const { t } = props;
const state = props.useNetxopsCard((snapshot) => snapshot);
const [open, setOpen] = import_react.useState(false);
const saveStarted = import_react.useRef(false);
import_react.useEffect(() => {
if (state.saving) {
saveStarted.current = true;
return;
}
if (!saveStarted.current)
return;
saveStarted.current = false;
if (!state.dirty && !state.failed)
setOpen(false);
}, [state.dirty, state.failed, state.saving]);
if (!state.available)
return null;
const title = t("title");
const disabled = !state.writable;
const blocked = !state.dirty || state.invalid || state.saving;
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV("li", {
className: open ? "dsh-nx-card dsh-nx-cardOpen" : "dsh-nx-card",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("button", {
type: "button",
className: "dsh-nx-header",
"aria-expanded": open,
"aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
onClick: () => {
setOpen(!open);
},
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-headText",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-name",
children: title
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-desc",
children: t("description")
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this),
state.dirty ? /* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-pending",
children: t("unsaved")
}, undefined, false, undefined, this) : null,
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: open ? "dsh-nx-chevron dsh-nx-chevronOpen" : "dsh-nx-chevron",
"aria-hidden": true,
children: "▾"
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this),
open ? /* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
className: "dsh-nx-body",
children: [
!state.writable ? /* @__PURE__ */ jsx_dev_runtime.jsxDEV("p", {
className: "dsh-nx-readOnly",
role: "status",
children: t("readOnly")
}, undefined, false, undefined, this) : null,
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
className: "dsh-nx-field",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
className: "dsh-nx-fieldHead",
children: [
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("label", {
className: "dsh-nx-label",
htmlFor: "netxops-api-token",
children: t("apiToken")
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: "dsh-nx-badges",
children: /* @__PURE__ */ jsx_dev_runtime.jsxDEV("span", {
className: state.apiTokenConfigured ? "dsh-nx-badge" : "dsh-nx-badgeMuted",
children: state.apiTokenConfigured ? t("apiTokenSet") : t("apiTokenUnset")
}, undefined, false, undefined, this)
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("input", {
id: "netxops-api-token",
className: "dsh-nx-input",
type: "password",
autoComplete: "off",
value: state.apiToken.text,
disabled: !state.apiTokenWritable,
onChange: (event) => {
props.edit("apiToken", event.target.value);
}
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("p", {
className: "dsh-nx-hint",
children: t("apiTokenHint")
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(ValueField, {
id: "netxops-api-url",
label: t("apiUrl"),
hint: t("apiUrlHint"),
field: state.apiUrl,
overriddenLabel: t("overridden"),
resetLabel: t("reset"),
invalidLabel: t("invalid"),
disabled,
onEdit: (text) => {
props.edit("apiUrl", text);
},
onReset: () => {
props.resetField("apiUrl");
}
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(ValueField, {
id: "netxops-lang",
label: t("lang"),
hint: t("langHint"),
field: state.lang,
overriddenLabel: t("overridden"),
resetLabel: t("reset"),
invalidLabel: t("invalid"),
disabled,
onEdit: (text) => {
props.edit("lang", text);
},
onReset: () => {
props.resetField("lang");
}
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV(ValueField, {
id: "netxops-python",
label: t("pythonCommand"),
hint: t("pythonCommandHint"),
field: state.pythonCommand,
overriddenLabel: t("overridden"),
resetLabel: t("reset"),
invalidLabel: t("invalid"),
disabled,
onEdit: (text) => {
props.edit("pythonCommand", text);
},
onReset: () => {
props.resetField("pythonCommand");
}
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("div", {
className: "dsh-nx-footer",
children: [
state.failed ? /* @__PURE__ */ jsx_dev_runtime.jsxDEV("p", {
className: "dsh-nx-failed",
role: "status",
children: t("saveFailed")
}, undefined, false, undefined, this) : null,
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("button", {
type: "button",
className: "dsh-nx-btn dsh-nx-discard",
disabled: !state.dirty || state.saving,
onClick: props.discard,
children: t("discard")
}, undefined, false, undefined, this),
/* @__PURE__ */ jsx_dev_runtime.jsxDEV("button", {
type: "button",
className: "dsh-nx-btn dsh-nx-save",
disabled: blocked,
onClick: props.save,
children: t(state.saving ? "saving" : "save")
}, undefined, false, undefined, this)
]
}, undefined, true, undefined, this)
]
}, undefined, true, undefined, this) : null
]
}, undefined, true, undefined, this);
}
// src/client/card-form.ts
var import_dsh_client_store = require("@deepseek-ai/dsh-client-store");
function textField(field) {
return {
field,
format: (value) => typeof value === "string" ? value : "",
parse: (text) => {
const trimmed = text.trim();
return trimmed === "" ? { kind: "clear" } : { kind: "set", value: trimmed };
}
};
}
class CardForm {
scope;
specs;
secretSpecs;
staged = new Map;
listeners = new Set;
saving = false;
failed = false;
constructor(scope, specs, secrets = []) {
this.scope = scope;
this.specs = new Map(specs.map((spec) => [spec.field, spec]));
this.secretSpecs = new Map(secrets.map((spec) => [spec.field, spec]));
scope.subscribe(() => {
this.publish();
});
}
bind(project) {
const store = import_dsh_client_store.createSnapshotStore(project());
this.listeners.add(() => {
store.set(project());
});
return store;
}
shell() {
const snapshot = this.scope.getSnapshot();
const plan = this.plan();
return {
available: snapshot.status === "ready",
writable: snapshot.writable,
dirty: plan.length > 0,
invalid: plan.some((item) => item.run === undefined),
saving: this.saving,
failed: this.failed
};
}
field(field) {
const staged = this.staged.get(field);
if (this.secretSpecs.has(field)) {
return { text: staged?.text ?? "", overridden: false, invalid: false };
}
const spec = this.spec(field);
if (staged === undefined) {
return { text: spec.format(this.sectionValue(field)), overridden: this.stored(field), invalid: false };
}
const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
return {
text: staged.text,
overridden: write?.kind === "set",
invalid: write === undefined
};
}
actions() {
return {
edit: (field, text) => {
this.stage(field, { text, clear: false });
},
resetField: (field) => {
this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true });
},
save: () => {
this.save();
},
discard: () => {
if (this.staged.size === 0 && !this.failed)
return;
this.staged.clear();
this.failed = false;
this.publish();
}
};
}
async save() {
const plan = this.plan();
const writes = plan.flatMap((item) => item.run === undefined ? [] : [item.run]);
if (plan.length === 0 || this.saving || writes.length !== plan.length)
return;
this.saving = true;
this.failed = false;
this.publish();
let landed = true;
for (const write of writes) {
landed = await write() && landed;
}
if (landed)
this.staged.clear();
this.saving = false;
this.failed = !landed;
this.publish();
}
plan() {
const plan = [];
for (const [field, staged] of this.staged) {
const secret = this.secretSpecs.get(field);
if (secret !== undefined) {
const value = staged.text.trim();
if (value !== "")
plan.push({ field, run: () => secret.write(value) });
continue;
}
const spec = this.spec(field);
if (staged.clear) {
if (this.stored(field))
plan.push({ field, run: () => this.clear(field) });
continue;
}
if (staged.text === spec.format(this.sectionValue(field)))
continue;
const write = spec.parse(staged.text);
if (write === undefined)
plan.push({ field, run: undefined });
else if (write.kind === "clear")
plan.push({ field, run: () => this.clear(field) });
else
plan.push({ field, run: () => this.store(field, write.value) });
}
return plan;
}
async clear(field) {
await this.scope.unset(field);
return !this.stored(field);
}
async store(field, value) {
await this.scope.set(field, value);
return this.userLayer()?.[field] === value;
}
stage(field, edit) {
this.staged.set(field, edit);
this.failed = false;
this.publish();
}
spec(field) {
const spec = this.specs.get(field);
if (spec === undefined)
throw new Error(`netxops card has no field ${field}`);
return spec;
}
snapshotOf() {
return this.scope.getSnapshot();
}
sectionValue(field) {
return this.snapshotOf().value?.[field];
}
baseValue(field) {
return this.snapshotOf().base?.[field];
}
userLayer() {
return this.snapshotOf().user;
}
stored(field) {
const user = this.userLayer();
return user !== undefined && Object.hasOwn(user, field);
}
publish() {
for (const listener of this.listeners)
listener();
}
}
// src/client/controller.ts
var NETXOPS_NS = "netxops";
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
var API_TOKEN_FIELD = "apiToken";
class NetxopsCardController {
scope;
ctx;
form;
store;
credential = { ref: "", configured: false, writable: true };
constructor(scope, ctx) {
this.scope = scope;
this.ctx = ctx;
this.form = new CardForm(scope, [textField("apiUrl"), textField("lang"), textField("pythonCommand")], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]);
this.store = this.form.bind(() => this.projection());
scope.subscribe(() => {
this.readCredential();
});
this.readCredential();
}
projection() {
return {
...this.form.shell(),
apiUrl: this.form.field("apiUrl"),
lang: this.form.field("lang"),
pythonCommand: this.form.field("pythonCommand"),
apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured,
apiTokenWritable: this.credential.writable
};
}
async readCredential() {
const ref = refOf(this.scope.getSnapshot());
if (ref !== this.credential.ref) {
this.credential = { ref, configured: false, writable: true };
this.store.set(this.projection());
}
const response = await this.ctx.remote.credentials.describe([ref]);
if (!response.ok || ref !== refOf(this.scope.getSnapshot()))
return;
const view = response.value[ref];
const next = {
ref,
configured: view?.configured ?? false,
writable: view?.writable ?? true
};
if (next.configured === this.credential.configured && next.writable === this.credential.writable)
return;
this.credential = next;
this.store.set(this.projection());
}
refreshCredential(ref) {
if (ref !== this.credential.ref)
return;
this.readCredential();
}
inject() {
return { hooks: { netxopsCard: this.store }, ...this.form.actions() };
}
async writeToken(value) {
await this.ctx.remote.credentials.set(refOf(this.scope.getSnapshot()), value);
await this.readCredential();
return this.credential.configured;
}
}
function refOf(snapshot) {
const declared = snapshot.value?.tokenCredentialRef;
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF;
}
// src/client/locales.ts
var en = {
title: "Netx Ops",
description: "UME API endpoint and bearer token for mcp__netx__* tools.",
apiUrl: "API URL",
apiUrlHint: "netx REST root, e.g. http://127.0.0.1:8890",
lang: "Language",
langHint: "Passed as NETX_LANG (zh / en).",
pythonCommand: "Python command",
pythonCommandHint: "Executable that can run `python -m netx_mcp`.",
apiToken: "API token",
apiTokenHint: "Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.",
apiTokenSet: "Configured",
apiTokenUnset: "Not set",
overridden: "Overridden",
reset: "Reset",
invalid: "Invalid value",
expand: "Expand",
collapse: "Collapse",
unsaved: "Unsaved",
readOnly: "This document is read-only.",
saveFailed: "Save failed — drafts kept for correction.",
discard: "Discard",
save: "Save",
saving: "Saving…"
};
var zh = {
title: "Netx Ops",
description: "UME API 地址与 Bearer Token,供 mcp__netx__* 工具使用。",
apiUrl: "API 地址",
apiUrlHint: "netx REST 根地址,例如 http://127.0.0.1:8890",
lang: "语言",
langHint: "传给 MCP 的 NETX_LANG(zh / en)。",
pythonCommand: "Python 命令",
pythonCommandHint: "能执行 `python -m netx_mcp` 的解释器。",
apiToken: "API Token",
apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。",
apiTokenSet: "已配置",
apiTokenUnset: "未设置",
overridden: "已覆盖",
reset: "重置",
invalid: "无效值",
expand: "展开",
collapse: "收起",
unsaved: "未保存",
readOnly: "当前文档只读。",
saveFailed: "保存失败,草稿已保留以便修改。",
discard: "丢弃",
save: "保存",
saving: "保存中…"
};
// src/client/index.ts
var LOCALE_NS = "settings.netxops";
var inject = [
"slots",
"locale",
"remote",
"remote.credentials",
"settingsScope"
];
function apply(ctx) {
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "netxops: locales");
const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx);
ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => {
card.refreshCredential(ref);
}), "netxops: credential invalidations");
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
name: "settings.plugin.item",
key: NETXOPS_NS,
locale: LOCALE_NS,
inject: () => card.inject()
}, NetxopsCard));
}
return module.exports;
}
});