/** * 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 }; }, });