first commit

This commit is contained in:
张可成
2026-09-05 00:27:41 +08:00
commit a9175e42de
9 changed files with 1406 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
/**
* Browser half of the EPSE regeneration guard — a settings card on the
* 插件配置 page. Plain-JS bundle (no build step): it registers through the
* client module loader and draws its own card, mirroring the stock Terminal /
* Agent loop / Web search cards so the layout and controls look identical.
*
* Registered into `settings.plugin.item` keyed by the guard's settings
* namespace; the configurable-plugins tab only dispatches a card when the Host
* also serves that namespace, which the settings registration in index.js
* provides (via the settings service's `installSection`, when mounted). The
* two inputs write straight to the durable settings document.
*/
window.__ModuleLoader__.load({
id: "@kevin/dsh-enhance",
factory: (require) => {
const React = require("react");
const { createElement: h, useState } = React;
const { IconChevronDownOutline14 } = require("@deepseek-ai/dsh-client-ui-primitives");
const NS = "kevin-dsh-enhance-guard";
const LOCALE_NS = "kevin-dsh-enhance-guard";
const cardCss = `
.epg_card{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}
.epg_card:hover{border-color:var(--dsw-alias-label-dimmed)}
.epg_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
.epg_header{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}
.epg_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
.epg_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
.epg_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
.epg_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
.epg_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
.epg_chevronOpen{transform:rotate(180deg)}
.epg_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
.epg_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
.epg_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
.epg_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}
.epg_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}
.epg_discard,.epg_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
.epg_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
.epg_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
.epg_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
.epg_discard:disabled,.epg_save:disabled{opacity:.4;cursor:default}
.epg_discard:focus-visible,.epg_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
`;
const fieldCss = `
.epg_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
.epg_field+.epg_field{border-top:1px solid var(--dsw-alias-border-l2)}
.epg_head{align-items:center;gap:8px;display:flex}
.epg_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
.epg_badges{align-items:center;gap:8px;display:inline-flex}
.epg_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}
.epg_badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}
.epg_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}
.epg_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
.epg_reset:disabled{cursor:default}
.epg_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}
.epg_input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
.epg_input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
.epg_inputInvalid{border-color:var(--dsw-alias-label-error)}
.epg_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
.epg_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
`;
const TAG = "@kevin/dsh-enhance/card.module.css";
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(TAG) + "]") === null) {
const tag = document.createElement("style");
tag.dataset.plugin = "@kevin/dsh-enhance";
tag.dataset.pluginCss = TAG;
tag.textContent = cardCss + fieldCss;
document.head.appendChild(tag);
}
const c = {
card: "epg_card", cardOpen: "epg_cardOpen", header: "epg_header",
headText: "epg_headText", name: "epg_name", description: "epg_description",
chevron: "epg_chevron", chevronOpen: "epg_chevronOpen", body: "epg_body",
readOnly: "epg_readOnly", pending: "epg_pending", footer: "epg_footer",
failed: "epg_failed", discard: "epg_discard", save: "epg_save",
field: "epg_field", head: "epg_head", label: "epg_label",
badges: "epg_badges", badge: "epg_badge", badgeMuted: "epg_badgeMuted",
reset: "epg_reset", input: "epg_input", inputInvalid: "epg_inputInvalid",
invalid: "epg_invalid", hint: "epg_hint",
};
const en = {
title: "EPSE leakage guard",
description: "Companion Plugin Settings.",
maxRegenerations: "Regenerations per step",
maxRegenerationsHint: "How many times one step may be forced to regenerate after a framed reply (min 1).",
targetProviders: "Target providers / models",
targetProvidersHint: "Comma-separated provider or model names. Leave blank to apply to every agent.",
unsaved: "Unsaved",
readOnly: "This deployment stores settings read-only.",
expand: "Show settings",
collapse: "Hide settings",
save: "Save",
saving: "Saving…",
discard: "Discard",
saveFailed: "The deployment did not accept these values; they were left for you to correct.",
overridden: "Overridden",
reset: "Reset to default",
invalidNumber: "Enter a whole number, or leave blank to use the default.",
};
const zh = {
title: "EPSE 泄露守护",
description: "DS2api 辅助插件设置。",
maxRegenerations: "每步重生成次数",
maxRegenerationsHint: "模型把工具调用框架写进文本时,同一 step 最多强制重生成多少次(最小 1)。",
targetProviders: "限定的 provider / model",
targetProvidersHint: "逗号分隔的 provider 或 model 名。留空表示对所有 agent 生效。",
unsaved: "未保存",
readOnly: "本部署的设置为只读。",
expand: "展开设置",
collapse: "收起设置",
save: "保存",
saving: "保存中…",
discard: "放弃修改",
saveFailed: "本部署没有接受这些值,已保留供你修改。",
overridden: "已覆盖",
reset: "恢复默认",
invalidNumber: "请填整数;留空表示使用默认值。",
};
const cls = (...parts) => parts.filter(Boolean).join(" ");
const formatMax = (value) => (value == null ? "" : String(value));
const formatProviders = (value) => (Array.isArray(value) ? value.join(", ") : "");
const parseMax = (text) => {
if (text.trim() === "") return { kind: "clear" };
const n = Number(text);
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) return undefined;
return { kind: "set", value: n };
};
const parseProviders = (text) => {
const parts = text.split(/[,,\s]+/).map((s) => s.trim()).filter(Boolean);
return { kind: "set", value: parts };
};
class CardController {
constructor(scope) {
this.scope = scope;
this.staged = {};
this.saving = false;
this.failed = false;
this.listeners = new Set();
this.parsers = { maxRegenerationsPerTurn: parseMax, targetProviders: parseProviders };
this.state = this.compute();
this.unsubscribe = scope.subscribe(() => {
this.state = this.compute();
this.publish();
});
}
compute() {
const snap = this.scope.getSnapshot();
const available = snap.status === "ready";
const writable = snap.writable === true;
const user = snap.user && typeof snap.user === "object" ? snap.user : {};
const value = snap.value && typeof snap.value === "object" ? snap.value : {};
const staged = this.staged;
const maxText = Object.hasOwn(staged, "maxRegenerationsPerTurn") ? staged.maxRegenerationsPerTurn : formatMax(value.maxRegenerationsPerTurn);
const maxOverridden = Object.hasOwn(staged, "maxRegenerationsPerTurn") || Object.hasOwn(user, "maxRegenerationsPerTurn");
const maxInvalid = Object.hasOwn(staged, "maxRegenerationsPerTurn") ? parseMax(staged.maxRegenerationsPerTurn) === undefined : false;
const provText = Object.hasOwn(staged, "targetProviders") ? staged.targetProviders : formatProviders(value.targetProviders);
const provOverridden = Object.hasOwn(staged, "targetProviders") || Object.hasOwn(user, "targetProviders");
const provInvalid = false;
const dirty = Object.keys(staged).length > 0;
return {
available,
writable,
dirty,
invalid: maxInvalid || provInvalid,
saving: this.saving,
failed: this.failed,
maxRegenerationsPerTurn: { text: maxText, overridden: maxOverridden, invalid: maxInvalid },
targetProviders: { text: provText, overridden: provOverridden, invalid: provInvalid },
};
}
getSnapshot() { return this.state; }
subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
publish() { for (const listener of this.listeners) listener(); }
dispose() { this.unsubscribe(); this.listeners.clear(); }
edit(field, text) { this.staged[field] = text; this.state = this.compute(); this.publish(); }
resetField(field) { this.staged[field] = ""; this.state = this.compute(); this.publish(); }
discard() { this.staged = {}; this.state = this.compute(); this.publish(); }
async save() {
if (this.saving) return;
this.saving = true;
this.failed = false;
this.state = this.compute();
this.publish();
try {
for (const field of Object.keys(this.staged)) {
const result = this.parsers[field](this.staged[field]);
if (result === undefined) continue;
if (result.kind === "set") await this.scope.set(field, result.value);
else if (result.kind === "clear") await this.scope.unset(field);
}
this.staged = {};
this.saving = false;
this.failed = false;
this.state = this.compute();
this.publish();
} catch (error) {
this.saving = false;
this.failed = true;
this.state = this.compute();
this.publish();
}
}
inject() {
return {
hooks: { epseCard: this },
edit: (field, text) => this.edit(field, text),
resetField: (field) => this.resetField(field),
save: () => this.save(),
discard: () => this.discard(),
};
}
}
function ValueField(props) {
return h("div", { className: c.field }, [
h("div", { className: c.head }, [
h("label", { className: c.label, htmlFor: props.id }, props.label),
props.overridden ? h("span", { className: c.badges }, [
h("span", { className: c.badge }, props.overriddenLabel),
h("button", { type: "button", className: c.reset, disabled: props.disabled, onClick: props.onReset }, props.resetLabel),
]) : null,
]),
h("input", {
id: props.id,
className: props.invalid ? c.inputInvalid : c.input,
type: "text",
...(props.numeric === true ? { inputMode: "numeric" } : {}),
...(props.invalid ? { "aria-invalid": true } : {}),
value: props.text,
placeholder: props.placeholder ?? "",
disabled: props.disabled,
onChange: (event) => props.onEdit(event.target.value),
}),
h("p", { className: props.invalid ? c.invalid : c.hint }, props.invalid ? props.invalidLabel : props.hint),
]);
}
function EpseGuardCard(props) {
const [open, setOpen] = useState(false);
const state = props.useEpseCard((snapshot) => snapshot);
if (!state.available) return null;
const title = props.t("title");
const blocked = !state.dirty || state.invalid || state.saving;
return h("li", { className: cls(c.card, open && c.cardOpen) }, [
h("button", {
type: "button",
className: c.header,
"aria-expanded": open,
"aria-label": `${props.t(open ? "collapse" : "expand")}: ${title}`,
onClick: () => setOpen(!open),
}, [
h("span", { className: c.headText }, [
h("span", { className: c.name }, title),
h("span", { className: c.description }, props.t("description")),
]),
state.dirty ? h("span", { className: c.pending }, props.t("unsaved")) : null,
h(IconChevronDownOutline14, { className: cls(c.chevron, open && c.chevronOpen) }),
]),
open ? h("div", { className: c.body }, [
!state.writable ? h("p", { className: c.readOnly, role: "status" }, props.t("readOnly")) : null,
h(ValueField, {
id: "plugin-config-epse-max",
label: props.t("maxRegenerations"),
hint: props.t("maxRegenerationsHint"),
overriddenLabel: props.t("overridden"),
resetLabel: props.t("reset"),
invalidLabel: props.t("invalidNumber"),
numeric: true,
disabled: !state.writable,
...state.maxRegenerationsPerTurn,
onEdit: (text) => props.edit("maxRegenerationsPerTurn", text),
onReset: () => props.resetField("maxRegenerationsPerTurn"),
}),
h(ValueField, {
id: "plugin-config-epse-targets",
label: props.t("targetProviders"),
hint: props.t("targetProvidersHint"),
overriddenLabel: props.t("overridden"),
resetLabel: props.t("reset"),
invalidLabel: "",
numeric: false,
disabled: !state.writable,
...state.targetProviders,
onEdit: (text) => props.edit("targetProviders", text),
onReset: () => props.resetField("targetProviders"),
}),
h("div", { className: c.footer }, [
state.failed ? h("p", { className: c.failed, role: "status" }, props.t("saveFailed")) : null,
h("button", { type: "button", className: c.discard, disabled: !state.dirty || state.saving, onClick: props.discard }, props.t("discard")),
h("button", { type: "button", className: c.save, disabled: blocked, onClick: props.save }, props.t(state.saving ? "saving" : "save")),
]),
]) : null,
]);
}
const inject = ["slots", "settingsScope", "locale"];
function apply(ctx) {
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "kevin-dsh-enhance-guard: locale");
const scope = ctx.settingsScope.bind({ namespace: NS });
const controller = new CardController(scope);
ctx.effect(() => () => controller.dispose(), "kevin-dsh-enhance-guard: card controller");
ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
name: "settings.plugin.item",
key: NS,
locale: LOCALE_NS,
inject: () => controller.inject(),
}, EpseGuardCard)), "kevin-dsh-enhance-guard: settings card");
}
return { apply, inject };
},
});