window.__ModuleLoader__.load({ id: "dsh-purge", factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); let react = require("react"); const h = react.createElement; const { useState, useEffect, useCallback, useRef, Component } = react; const name = "dsh-purge"; const inject = ["slots", "locale"]; const NS = "settings.dsh-purge"; let translate = (key) => key; function useT() { return translate; } function clientGuessSurface() { try { if (typeof navigator !== "undefined" && /electron/i.test(navigator.userAgent || "")) return "desktop"; if (typeof location !== "undefined") { const port = String(location.port || ""); if (port === "43120") return "desktop"; const host = String(location.hostname || ""); if (host && host !== "127.0.0.1" && host !== "localhost" && /dsh desktop/i.test(String(location.href || ""))) return "desktop"; } } catch { /* ignore */ } return ""; } function hostSurfaceOf(s) { return (s && s.surface) || clientGuessSurface() || "web"; } function hostText(t, key, surface) { if (surface && surface !== "web") { const specific = key + "." + surface; const hit = t(specific); if (hit && hit !== specific) return hit; } return t(key); } function apiUrl(p) { try { return new URL(p, window.location.origin).toString(); } catch { return p; } } function apiTimeoutMs(p, init) { const path = String(p || ""); const method = String((init && init.method) || "GET").toUpperCase(); if (path.indexOf("/dsh-purge/update") !== -1) return method === "POST" ? 180000 : 45000; if (path.indexOf("/dsh-purge/uninstall") !== -1) return 90000; if (path.indexOf("/dsh-purge/skill") !== -1) return method === "POST" ? 120000 : 20000; // status 会在"应用"期间被后端的同步落盘阻塞,默认 20s 必然被 abort,界面只会显示"读取失败: timeout"。 if (path.indexOf("/dsh-purge/status") !== -1) return 60000; if (path.indexOf("/dsh-purge/apply") !== -1) return 240000; if (path.indexOf("/dsh-purge/revert") !== -1) return 120000; if (path.indexOf("/dsh-purge/restart") !== -1) return 60000; return 20000; } function isAbortError(e) { const msg = String((e && e.message) || e || ""); return (e && e.name === "AbortError") || /aborted|abort/i.test(msg); } async function apiJson(p, init) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), apiTimeoutMs(p, init)); try { const r = await fetch(apiUrl(p), Object.assign({ cache: "no-store", credentials: "same-origin", signal: ctrl.signal }, init || {})); const text = await r.text(); let data; try { data = text ? JSON.parse(text) : null; } catch { throw new Error(r.status + " non-json: " + text.slice(0, 120)); } if (!r.ok) throw new Error((data && (data.error || data.message)) || (r.status + " " + r.statusText)); return data; } catch (e) { if (isAbortError(e)) throw new Error("timeout"); const msg = String((e && e.message) || e || ""); if (/failed to fetch|networkerror|load failed/i.test(msg)) { throw new Error("连不上接口,请确认 dsh 还在跑后 Ctrl+F5"); } throw e; } finally { clearTimeout(timer); } } function promptBoxEmpty(text) { return !String(text || "").trim(); } function responseNeedsPrompt(d) { return Boolean(d && (d.needPrompt || d.injectSource === "none")); } function warnNeedPrompt(d, tr) { if (!responseNeedsPrompt(d)) return false; window.alert(tr("need.prompt")); return true; } async function bothInjectEmpty(overrideText, defaultText) { if (!promptBoxEmpty(overrideText)) return false; if (!promptBoxEmpty(defaultText)) return false; return true; } function rulesApi(op, extra) { return apiJson("/dsh-purge/rules", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(Object.assign({ op: op }, extra || {})), }); } function skillsRouteMissing(e) { return /\b404\b|\b405\b|non-json/i.test(String((e && e.message) || e || "")); } function skillsApi(op, extra) { const init = { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(Object.assign({ op: op }, extra || {})), }; return apiJson("/dsh-purge/skills", init).catch((e) => { if (!skillsRouteMissing(e)) throw e; return apiJson("/dsh-purge/skill", init); }); } const TARGETS = ["AGENTS.md", "CLAUDE.md"]; const zh = { nav: "规则设定", "page.title": "DeepSeek Harness Purge", "page.nav": "Purge", "page.lead": "本机提示词与引擎层清洗。改完点「应用」,重启宿主后生效。", "page.seg.armor": "破甲", "page.seg.rules": "规则", "page.seg.skills": "技能", "page.seg.drill": "演练台", "drill.title": "演练台", "drill.lead": "资产、技能与本机环境。首次进入需要读完声明、等完倒计时并勾选三项。", "drill.authorize": "开始授权", "drill.unavailable": "演练台资源未随插件加载,重装或重启后可恢复。", "dock.clean": "清洗", "dock.drill": "演练台", "dock.toggle": "dsh-purge", "dock.newSession": "dsh-purge", "dock.inSession": "dsh-purge", "dock.redteam": "红队模式", "dock.full": "全面浏览", "dock.close": "收起", "dock.openSettings": "设置页", "dock.unauthorized": "未授权", "welcome.title": "用户须知", "welcome.lead": "首次安装本插件时请阅读以下说明。确认后本窗不再弹出。", "welcome.warn": "本插件免费开源。任何背着作者私自售卖、付费倒卖或借本项目盈利的行为,作者将依法追究法律责任。", "welcome.body": "dsh-purge 是 DeepSeek Harness 的本机网络安全红队评测与清洗插件:在受控本机环境中调整提示词与补丁、做授权范围内的演练台操作。项目免费、非营利,源码仓库:", "welcome.repo": "https://github.com/YuJunZhiXue/dsh-purge", "welcome.ban1": "禁止将本项目用于未授权的网络攻击、入侵、窃取数据、破坏系统或任何违法犯罪。", "welcome.ban2": "禁止商业售卖、付费倒卖、黑灰产牟利,以及隐瞒作者的营利性再分发。", "welcome.ban3": "使用者须自行遵守所在地法律法规与平台规范;违法后果由行为人自行承担,与作者无关。", "welcome.bans": "禁止事项", "welcome.check": "我已阅读并理解上述用户须知,同意合法合规使用。", "welcome.ok": "我已知晓并继续", "auth.title": "演练台授权确认", "auth.lead": "清洗不需要这一步。第一次进入演练台,或打开全面浏览,都要先完成本窗。", "auth.warn": "安全警告:演练台只用于你有权管理的本机、离线靶标,或已书面授权的演练环境。未经授权的渗透、攻击、窃取和破坏一律禁止。", "auth.hint.wait": "请阅读声明。{n} 秒后才能勾选。", "auth.hint.scroll": "倒计时已结束。请把声明滚到文末,再勾选。", "auth.hint.check": "倒计时已结束。请勾选全部三项。", "auth.check.read": "我已读完《安全与禁止违法声明》,并认可其中的全部条款。", "auth.check.scope": "我确认只用在本机已授权环境、离线靶标或合规演练中。", "auth.check.ban": "我确认禁止任何违法用途。未完成本授权,不得使用演练台。", "auth.no": "暂不进入", "auth.ok": "确认授权", "auth.ok.tab": "确认授权并进入演练台", "auth.ok.full": "确认授权并全面浏览", "auth.ok.env": "确认授权并打开环境适配", "envGate.title": "环境还没配好", "envGate.lead": "红队模式发送前需要本机工具。未检测到可用环境时,先去环境适配,或确认暂不配置。", "envGate.kaliHint": "检测到 Kali 时会直接用本机工具,不必填路径。没有 Kali 时,在环境适配里填写各工具路径,或选一个整包文件夹自动分配。", "envGate.skip": "暂不配置", "envGate.skipConfirm": "确认后这次可以继续对话。扫描和利用类技能仍会因为缺少工具而不可用,之后可以在 dsh-purge → 环境适配里补路径。", "envGate.skipNo": "返回", "envGate.skipYes": "确认暂不配置", "envGate.go": "去环境适配", "theme.aria": "外观", "theme.white": "白", "theme.ink": "墨", "purge.title": "补丁", "purge.lead": "本机 0.2.1 的 82 个锚点:已应用 / 总数、命令 shim、备份状态。检测到待处理项时点应用。", "override.title": "提示词", "override.lead": "注入到 system 段的操作员指令。改成自己的稿子会按原样落盘,不再随插件刷新。", "own.title": "自己的服务器", "own.hint": "每行一个 IP 或完整主机名。可以写成 账号@主机,账号只用来认出这种写法。主机名会连同它当时解析出的地址一起放行。不要写密钥、密码、网段。", "own.save": "保存名单", "saved.own": "已保存自己的服务器", "saved.own.dropped": "已保存。有 {n} 行不是单个 IP 或主机名,已丢掉", "metric.purged": "已写入", "metric.purged.skip": "已写入。跳过 {n} 项,本机没有这些文件", "metric.pendingShort": "待处理 {n}", "metric.unmatchedShort": "没对上 {n}", "metric.skipShort": "跳过 {n}", "metric.unlocated.count": "—", "metric.shim": "shim", "metric.shim.na": "本端不需要", "metric.bak.yes": "有备份", "metric.bak.no": "无备份", "metric.bak.hint": "备份", "table.patch": "项目", "table.status": "状态", "status.applied": "已应用", "status.off": "已关闭", "status.pending": "待应用", "status.unmatched": "没对上", "status.skipped": "跳过", "apply.hint": "点应用会打补丁并把当前默认提示词挂上,框里仍是插件旧默认时自动刷到加密槽,不必再点保存。只有你另写的稿才会按框落盘。待应用=原文还在。跳过=当前版本不需要或组件未安装。", "warn.noRoot": "未定位到当前宿主的 @deepseek-ai,清洗不会生效。请完全退出后再打开本宿主,在本页点「应用」。桌面端安装目录可以是任意盘符;目录名带 DeepseekHarness / DeepSeek Harness 均可。", "warn.noRoot.desktop": "官方客户端的代码在 app.asar 里。点「应用」会解开并自动重启,不用另跑脚本。若一直显示还没定位,先完全退出客户端再开,再点「应用」。第三方 DSH Desktop 直接点「应用」。", "unpack.restarting": "补丁已写入。点「重启」后客户端会自己重新打开。", "restart.fullQuit": "正在重启,客户端会自己重新打开。", "warn.noInject": "官方始终用默认提示词。红队开了规则集就只用规则集;没开时用去掉 CTF 的默认提示词,再接红队操作。两边都空会提示必须添加。", "need.prompt": "提示词和规则集都是空的,必须先添加提示词,或启用一条有内容的规则集。", "btn.restoreInject": "恢复默认", "saved.restoreInject": "已恢复默认提示词", "skip": "跳过", "unknown": "未知", "delete": "删除", "btn.apply": "应用", "btn.apply.busy": "处理中…", "btn.revert": "还原", "btn.uninstall": "卸载", "btn.uninstall.busy": "卸载中…", "uninstall.title": "卸载 dsh-purge", "uninstall.body": "是否卸载?卸载将还原回原版,并清除本插件的全部文件与补丁。", "uninstall.confirm": "确认卸载", "uninstall.cancel": "取消", "uninstall.applying": "正在卸载并还原…", "uninstall.done": "已卸载并还原,正在重启…", "uninstall.done.desktop": "已卸载并还原,正在重启桌面应用…", "uninstall.fail": "卸载失败: {error}", "btn.checkUpdate": "检测更新", "btn.checkUpdate.busy": "检测中…", "btn.doUpdate": "更新", "btn.doUpdate.busy": "更新中…", "channel.title": "通道", "channel.stable": "正式版", "channel.beta": "测试版", "channel.useStable": "切换到正式版", "channel.useBeta": "切换到测试版", "channel.local": "本机", "update.checking": "正在检测更新…", "update.applying": "正在更新…", "update.switching": "正在切换版本…", "update.latest": "已是最新({version})", "update.available": "有新版本 {remote},当前 {local}", "update.pinned": "已固定在 {version},通道最新是 {remote}", "update.done": "已更新到 {version}", "update.restarting": "已还原并写入新补丁,正在重启…", "update.fullQuit": "补丁已写入。点「重启」后客户端会自己重新打开。", "restart.official.title": "需要重启", "restart.official.body": "补丁已写入。点重启后客户端会自己重新打开。", "update.switched": "已切换到 {version}", "update.autoDone": "已自动更新到 {version}", "update.reloading": "正在刷新设置页…", "update.dirty": "有新版本,本地有改动未自动覆盖", "update.fail": "更新失败: {error}", "update.timeout": "检测超时,请再点一次检测更新", "update.needRestart": "检测接口未加载,请先重启 dsh 再点检测更新", "update.noBeta": "还没有测试版", "update.switch": "切换到此版本", "update.pick": "选择版本", "update.tip": "最新", "update.current": "当前", "update.confirmSwitch": "切换到 {version}?之后可再切回另一通道或其它版本。本地未提交的插件改动不会保留。", "update.confirmChannel": "切换到{version}?会按该通道重新安装插件,本地未提交的插件改动不会保留。", "metric.version": "版本", "action.apply": "应用", "action.revert": "还原", "action.uninstall": "卸载", "ok.done": "已完成", "err.action": "失败: {error}", "err.status": "读取失败: {error}", "err.stale": "另一个窗口刚改过状态(现在是 rev {rev},最后一次动作 {last})。已重新读取,请确认后再点。", "err.override": "读取失败: {error}", "err.rules": "读取失败: {error}", "err.read": "读取失败: {error}", "err.save": "保存失败: {error}", "btn.saveInject": "保存", "saved.override": "已保存", "rules.title": "规则集", "rules.none": "无", "rules.reset": "还原", "rules.reset.confirm": "还原全局指令文件?规则库会保留。", "rules.empty": "暂无规则", "rules.pick": "选择规则", "policy.title": "工作区策略", "policy.strict": "严格", "policy.fallback": "空位兜底", "policy.always": "总是允许", "policy.hint": "严格:永远剥 AGENTS.md / CLAUDE.md;空位兜底:提示词框为空时才用;总是允许:保留 DSH 原生行为", "cwd.title": "本地规则(当前目录)", "cwd.empty": "当前目录没有 AGENTS.md / CLAUDE.md", "cwd.rescan": "重新扫描", "cwd.hint": "只有提示词框为空时,被勾选的本地文件才作为兜底注入;提示词框一旦非空就不会用这些文件。", "cwd.scope.cwd": "当前目录", "cwd.scope.ancestor": "上级", "cwd.scope.root": "项目根", "cwd.no.workspace": "DSH 尚未选过工作目录,请先在 DSH 里开始一次任务再回来。", "pill.current": "当前", "btn.use": "启用", "btn.inUse": "当前", "confirm.delete": "删除 {id}?", "ph.id": "id", "ph.alias": "名称", "ph.alias.short": "名称", "btn.create": "新建", "need.id": "需要 id", "ph.content": "", "btn.saveRule": "保存", "skills.title": "Skill", "skills.hint": "导入到官方目录后,由 DSH 调用:聊天输入 /名称 立即加载;任务对上 description 或 whenToUse 时模型会调 skill 工具。正文写「激活」不会触发。", "skills.hint.desktop": "当前桌面宿主的官方 Skill 目录。聊天输入 /名称 立即加载;任务对上 description 时模型会调 skill 工具。", "skills.empty": "暂无用户 Skill", "btn.importZip": "导入压缩包", "btn.importFolder": "导入文件夹", "skills.importing": "正在导入…", "saved.import": "已导入 {count} 个 Skill", "need.import": "请选择压缩包或文件夹", "err.import.folder": "没读到文件夹里的文件,请直接选 Skill 目录(里面要有 SKILL.md)", "skills.pick": "选择 Skill", "skills.invalid": "官方会忽略:格式不对", "skills.call.slash": "聊天输入 {cmd}", "skills.call.model": "模型可调", "skills.call.modelOff": "模型不可调", "skills.call.userOff": "不可 /名称", "ph.skill.desc": "description(何时用)", "btn.saveSkill": "保存", "need.skill.id": "id 必须是 kebab-case,例如 code-review", "confirm.delete.skill": "从官方 $DSH_HOME/skills 删除 {id}?宿主会自动从目录拿掉。", "err.skills": "读取失败: {error}", "err.skills.needRestart": "Skill 接口未加载,请先重启当前宿主", "btn.restart": "重启", "btn.restart.busy": "重启中…", "restarting": "重启中…", "restarting.desktop": "正在重启桌面应用…", "restart.timeout": "超时,请刷新", "restart.fail": "失败: {error}", "restart.notStarted": "重启没有开始,客户端还在。不会自动再试。", "restart.already": "这一次重启已经开始,不会再重启一次。", "restart.confirm": "清洗已完成,重启后生效。", "restart.confirm.desktop": "清洗已完成,重启桌面应用后生效。", "restart.ask.title": "要现在重启吗", "restart.ask.body": "已经写入。重启后才会在当前客户端里生效。", "restart.now": "现在重启", "restart.notNow": "稍后", "restart.asarSwap": "补丁已写入磁盘。正在结束桌面进程并挪开 app.asar,完成后会自动重新打开;若仍无效,请先完全退出 Harness(托盘也关),再点一次应用。", "surface.web": "Web", "surface.desktop": "桌面端", "surface.gui": "GUI", "surface.tui": "TUI", "restart.incomplete": "提示词已写入。{detail}", "restart.incomplete.patches": "还有 {failed} 项代码补丁没对上当前版本(#{ids}),不影响这段提示词", "restart.incomplete.flash": "CMD 无感未钉入({entry})", "restart.later": "以后", "group.prompt": "提示词", "group.code": "代码", "group.engine": "引擎", "group.tools": "工具", "group.compat": "0.1.5 兼容", "group.dsh20": "0.2.0", "patch.1": "全局免责 → 强指令", "patch.2": "替换式免责 → 强指令", "patch.3": "作用域免责 → 强指令", "patch.4": "persona 会话强指令", "patch.5": "审批提示句 → 全权限", "patch.6": "沙箱 deny → allow 写", "patch.7": "沙箱默认全权限", "patch.8": "审批默认 never", "patch.9": "审批门自动放行", "patch.10": "审批配置默认 never", "patch.11": "never 提示句 → 自动放行", "patch.12": "ask 提示句 → 自动放行", "patch.13": "豁免升级阶梯", "patch.14": "升级无条件授信", "patch.15": "沙箱 confine 直通", "patch.16": "文件系统围栏取消", "patch.17": "观察策略读写放行", "patch.18": "重复调用守卫禁用", "patch.19": "工具结果修剪禁用", "patch.21": "恢复插件延长的 bash 等待", "patch.22": "read 上限放宽", "patch.23": "子代理深度 3 → 10", "patch.24": "preset fetch 启用", "patch.25": "去掉官方默认身份(功能提示词不动)", "patch.26": "minimal:去掉默认身份并打开 inject", "patch.28": "dsh-liangshen(梁神):phase-1 保留 dsh-purge / dshpg 段", "patch.70": "极简模式终端不再禁止网络", "patch.71": "梁神去掉官方身份句", "patch.29": "dsh-tool-web:外部 untrusted 拦截→可执行", "patch.30": "dsh-hooks-claude-code:deny→allow", "patch.31": "dsh-hooks-codex:deny→allow", "patch.32": "Web 表面身份中性化", "patch.33": "Shell 拒绝提示 → 可执行", "patch.34": "沙箱提示中性化", "patch.35": "升级 never 不拒", "patch.36": "文件系统升级 schema 放行", "patch.37": "子代理 scope lock 中性化", "patch.38": "persona text→prefix(0.1.2 预设)", "patch.39": "会话 v0 plugin summary(mnemon)", "patch.40": "complete 预设仍保留注入", "patch.42": "设置页旧接口", "patch.43": "网页预设去掉默认身份", "patch.45": "官方桌面更新交接", "patch.46": "官方更新脚本只用英文", "patch.47": "OAuth 不再插入 Claude 身份", "patch.48": "shell 去掉删除前的额外确认", "patch.49": "OAuth 不再伪装 claude-cli", "patch.50": "跨会话引用不再标不可信", "patch.51": "新会话保持完全权限", "patch.52": "会话日志默认不上传", "patch.53": "网页提示去掉禁止另开服务器", "patch.54": "交付物提示去掉多余禁止", "patch.55": "每一步替换第一条系统提示", "patch.56": "回退只藏被撤掉的那一轮", "patch.57": "developer role 仅限 openai/azure,第三方代理回退 system", "patch.58": "回退空标记不进模型请求", "patch.59": "官方 Auto Review 不再拦工具", "patch.60": "官方 run_code 去掉审批教学", "patch.61": "官方沙箱策略不再教拒绝", "patch.62": "无审批通道时放行(用户拒绝仍有效)", "patch.63": "计划模式不再软拦执行", "patch.64": "Stop hook 不再注入假用户话", "patch.65": "UserPromptSubmit deny 不再整轮 reject", "patch.66": "空的系统段不再把这一轮打成内部错误", "patch.67": "缺 messages 时这一轮不中断", "patch.68": "回退后缺 content 不中断", "patch.69": "deepseek-flash 保持官方 in-history", "patch.72": "Messages 的 system 用当前系统提示", "patch.73": "极简 bash 去掉误留的镜像说明", "patch.74": "首发前先挂默认提示词", "patch.75": "回退把已完成任务收回成未开始", "patch.76": "unrestricted 预设去掉身份句", "patch.77": "回退标记到达时丢弃已发送消息", "patch.78": "补丁文件缺失时从 .bak 恢复", "patch.80": "恢复官方极简工具表", "patch.82": "主进程兜底 DSH_PROFILE=desktop", "patch.83": "host 信号杀时不报错不弹对话框", "patch.84": "host code=0 正常退出不报错 (消除 stopped 对话框)", "status.unlocated": "未定位", "status.na": "本端不需要", "metric.unlocated": "还没定位到本机 Harness", "metric.sealed": "已找到官方客户端,点应用解开", "metric.sealed.count": "待解开", "metric.lost": "补丁已丢失,请重新应用", "metric.lost.count": "已丢失", "rewind.label": "回退", "rewind.aria": "回退", "rewind.busy": "回退中…", "rewind.empty": "没有可回退的上一句", "rewind.fail": "回退失败: {error}", "rewind.once": "回退一次", "rewind.once.hint": "把这一轮发出去的那句放回输入框,并清掉这一轮", "rewind.round": "回退上一轮", "rewind.round.hint": "把这一轮发出去的那句放回输入框;子代理也只撤这一轮", "gate.title": "审批与审计", "gate.hint": "审计永远记流水。审批档位只影响模型发起的 purge_apply / purge_revert / purge_uninstall;你自己点按钮不受影响。", "gate.mode.audit": "只记流水(默认,行为同旧版)", "gate.mode.enforce": "要求一次批准(allowed-once)", "gate.gated": "受管工具:{list}", "gate.saved": "已保存,重启 dsh 后对工具调用生效。", "gate.audit.show": "查看审计流水", "gate.audit.hide": "收起审计流水", "gate.audit.refresh": "刷新", "gate.audit.busy": "读取中…", "gate.audit.empty": "还没有记录。", "hooksDeny.title": "Hooks 拦截", "hooksDeny.bypass": "放行 hooks 的 deny / ask(补丁 30 / 31 / 64 / 65)", "hooksDeny.hint": "关掉后,用户自己的 PreToolUse / UserPromptSubmit / Stop hook 的 deny 会按官方逻辑生效。官方 ask 分支本身是空操作,关开关不会弹出审批框。改完后点「应用」并重启才写进宿主。", "saved.hooksDeny": "已保存", "continue.title": "失败重试 / 继续", "continue.hint": "请求失败会自动重试;异常停止或中断可点「继续」或自动续跑。自己点停止不会自动继续。次数用完后需新开一轮。", "continue.autoRetry": "失败自动重试", "continue.retryMax": "重试次数", "continue.autoContinue": "中断后自动继续", "continue.continueMax": "继续次数", "continue.text": "继续用语", "saved.continue": "已保存", "continue.label": "继续", "continue.aria": "异常停止后继续", "continue.busy": "继续中…", "continue.ready": "继续(还可 {left} 次)", "continue.limit": "已达继续上限", "continue.fail": "继续失败: {error}", }; const en = { nav: "Rules", "page.title": "DeepSeek Harness Purge", "page.nav": "Purge", "page.lead": "Prompt and engine-level cleaning for this install. Press Apply, then restart the host.", "page.seg.armor": "Armor", "page.seg.rules": "Rules", "page.seg.skills": "Skills", "page.seg.drill": "Drill", "drill.title": "Drill", "drill.lead": "Assets, skills, and the local environment. The first visit needs the notice, the countdown, and three boxes.", "drill.authorize": "Start authorization", "drill.unavailable": "Drill resources are not loaded; reinstall or restart to restore them.", "dock.clean": "Clean", "dock.drill": "Drill", "dock.toggle": "dsh-purge", "dock.newSession": "dsh-purge", "dock.inSession": "dsh-purge", "dock.redteam": "Red team", "dock.full": "Full view", "dock.close": "Collapse", "dock.openSettings": "Settings page", "dock.unauthorized": "Unauthorized", "welcome.title": "User notice", "welcome.lead": "Please read this on first install. After you confirm, it will not appear again.", "welcome.warn": "This plugin is free and open source. Anyone who sells, resells, or profits from it behind the author's back may face legal action.", "welcome.body": "dsh-purge is a local DeepSeek Harness plugin for security evaluation and cleaning: adjust prompts and patches on a machine you control, and use the drill console only within authorized scope. It is free and non-profit. Repository:", "welcome.repo": "https://github.com/YuJunZhiXue/dsh-purge", "welcome.ban1": "Do not use this project for unauthorized attacks, intrusion, data theft, sabotage, or any illegal activity.", "welcome.ban2": "Do not commercially sell, resell, or profit from gray-market use, or redistribute for profit without the author.", "welcome.ban3": "You must follow local law and platform rules; illegal use is solely your responsibility.", "welcome.bans": "Prohibitions", "welcome.check": "I have read and understand this notice and will use the plugin lawfully.", "welcome.ok": "I understand — continue", "auth.title": "Drill console authorization", "auth.lead": "Cleaning does not need this step. Opening Drill or Full view the first time requires this dialog.", "auth.warn": "Warning: the drill console is only for hosts you manage, offline targets, or written authorized exercises. Unauthorized intrusion, attack, theft, or sabotage is forbidden.", "auth.hint.wait": "Read the notice. You can check the boxes in {n}s.", "auth.hint.scroll": "Countdown finished. Scroll to the end, then check the boxes.", "auth.hint.check": "Countdown finished. Check all three boxes.", "auth.check.read": "I have read the Safety and Anti-Abuse Notice and accept every clause.", "auth.check.scope": "I will only use authorized local hosts, offline targets, or compliant exercises.", "auth.check.ban": "I will not use this for any illegal purpose. Without this authorization the drill console stays closed.", "auth.no": "Not now", "auth.ok": "Authorize", "auth.ok.tab": "Authorize and open Drill", "auth.ok.full": "Authorize and open Full view", "auth.ok.env": "Authorize and open Env adapt", "envGate.title": "Environment is not ready", "envGate.lead": "Red team mode needs local tools before you send. If none are available, open Env adapt or confirm you will continue without them.", "envGate.kaliHint": "When Kali is detected, local tools are used directly and paths are optional. Otherwise set each tool path in Env adapt, or pick one toolkit folder to assign them.", "envGate.skip": "Skip for now", "envGate.skipConfirm": "You can keep chatting after this. Scan and exploit skills stay unavailable until the tools are installed. You can add paths later under dsh-purge → Env adapt.", "envGate.skipNo": "Back", "envGate.skipYes": "Continue without tools", "envGate.go": "Open Env adapt", "theme.aria": "Appearance", "theme.white": "Light", "theme.ink": "Ink", "purge.title": "Patches", "purge.lead": "82 anchors on this 0.2.1 install: applied / total, command shims, backup state. Press Apply when anything is pending.", "override.title": "Prompt", "override.lead": "Operator instructions injected into the system section. Your own text is written verbatim and stops following plugin updates.", "own.title": "Own servers", "own.hint": "One IP or exact hostname per line. account@host is accepted; the account only identifies that host. A hostname also allows the addresses it resolves to. Do not enter a key, a password, or a range.", "own.save": "Save list", "saved.own": "Own servers saved", "saved.own.dropped": "Saved. {n} lines were not a single IP or hostname and were dropped", "metric.purged": "Written", "metric.purged.skip": "Written. Skipped {n}; those files are not on this install", "metric.pendingShort": "Pending {n}", "metric.unmatchedShort": "No match {n}", "metric.skipShort": "Skipped {n}", "metric.unlocated.count": "—", "metric.shim": "shim", "metric.shim.na": "Not needed here", "metric.bak.yes": "Backup", "metric.bak.no": "No backup", "metric.bak.hint": "Backup", "table.patch": "Item", "table.status": "Status", "status.applied": "Applied", "status.off": "Off", "status.pending": "Pending", "status.unmatched": "No match", "status.skipped": "Skipped", "apply.hint": "Apply writes patches and hangs the current default prompt. A stale plugin default in the box is refreshed from the sealed slot; a separate save is unnecessary. Only a prompt you wrote yourself is kept from the box. Pending = original text still present. Skipped = not needed for this version or the component is not installed.", "warn.noRoot": "Could not find this host’s @deepseek-ai tree, so Apply will not patch anything. Fully quit and reopen this host, then Apply here. Desktop may live on any drive; folder names like DeepseekHarnessDesktop or DeepSeek Harness are fine.", "warn.noRoot.desktop": "Official Harness keeps its code in app.asar. Apply unpacks it and restarts the app. If this page stays “not found”, fully quit the client, reopen it, then Apply again. Third-party DSH Desktop: just Apply.", "unpack.restarting": "Patches are written. Click Restart and the client opens again by itself.", "restart.fullQuit": "Restarting. The client will open again by itself.", "warn.noInject": "Official mode always uses the default prompt. Red team uses only the active rule set; otherwise it uses the default prompt without the CTF section, then the red team steps. If both are empty you will be asked to add a prompt.", "need.prompt": "Both the prompt and the rule set are empty. Add a prompt, or enable a rule that has content.", "btn.restoreInject": "Reset default", "saved.restoreInject": "Default prompt restored", "skip": "Skipped", "unknown": "Unknown", "delete": "Delete", "btn.apply": "Apply", "btn.apply.busy": "Working…", "btn.revert": "Restore", "btn.uninstall": "Uninstall", "btn.uninstall.busy": "Uninstalling…", "uninstall.title": "Uninstall dsh-purge", "uninstall.body": "Uninstall? This restores the original Harness and removes this plugin completely.", "uninstall.confirm": "Uninstall", "uninstall.cancel": "Cancel", "uninstall.applying": "Uninstalling and restoring…", "uninstall.done": "Uninstalled and restored. Restarting…", "uninstall.done.desktop": "Uninstalled and restored. Restarting the desktop app…", "uninstall.fail": "Uninstall failed: {error}", "btn.checkUpdate": "Check update", "btn.checkUpdate.busy": "Checking…", "btn.doUpdate": "Update", "btn.doUpdate.busy": "Updating…", "channel.title": "Channel", "channel.stable": "Stable", "channel.beta": "Beta", "channel.useStable": "Switch to stable", "channel.useBeta": "Switch to beta", "channel.local": "This copy", "update.checking": "Checking for updates…", "update.applying": "Updating…", "update.switching": "Switching version…", "update.latest": "Up to date ({version})", "update.available": "Update {remote} available (now {local})", "update.pinned": "Pinned at {version}; channel latest is {remote}", "update.done": "Updated to {version}.", "update.restarting": "Restored the old patches, wrote the new ones, and restarting…", "update.fullQuit": "Patches are written. Click Restart and the client opens again by itself.", "restart.official.title": "Restart required", "restart.official.body": "Patches are written. Click Restart and the client opens again by itself.", "update.switched": "Switched to {version}.", "update.autoDone": "Auto-updated to {version}.", "update.reloading": "Refreshing settings…", "update.dirty": "Update available; local edits were not overwritten.", "update.fail": "Update failed: {error}", "update.timeout": "Check timed out. Click Check update again.", "update.needRestart": "Update API is not loaded. Restart dsh, then check again.", "update.noBeta": "No beta build yet", "update.switch": "Switch to this version", "update.pick": "Select version", "update.tip": "latest", "update.current": "current", "update.confirmSwitch": "Switch to {version}? You can switch back later. Uncommitted plugin edits will not be kept.", "update.confirmChannel": "Switch to {version}? The plugin will be reinstalled from that channel. Uncommitted plugin edits will not be kept.", "metric.version": "Version", "action.apply": "Apply", "action.revert": "Restore", "action.uninstall": "Uninstall", "ok.done": "Done", "err.action": "Failed: {error}", "err.status": "Read failed: {error}", "err.stale": "Another window changed the state (now rev {rev}, last action {last}). Re-read; check before clicking again.", "err.override": "Read failed: {error}", "err.rules": "Read failed: {error}", "err.read": "Read failed: {error}", "err.save": "Save failed: {error}", "btn.saveInject": "Save", "saved.override": "Saved", "rules.title": "Rule sets", "rules.none": "None", "rules.reset": "Restore", "rules.reset.confirm": "Restore global instruction files? The rule library is kept.", "rules.empty": "No rules", "rules.pick": "Select a rule", "policy.title": "Workspace policy", "policy.strict": "Strict", "policy.fallback": "Fallback", "policy.always": "Always on", "policy.hint": "Strict: always strip AGENTS.md / CLAUDE.md. Fallback: use workspace files only when the prompt box is empty. Always on: keep DSH default behavior.", "cwd.title": "Local rules (current folder)", "cwd.empty": "No AGENTS.md / CLAUDE.md in this folder", "cwd.rescan": "Rescan", "cwd.hint": "Checked files are injected only when the prompt box is empty. A non-empty prompt box always wins.", "cwd.scope.cwd": "cwd", "cwd.scope.ancestor": "ancestor", "cwd.scope.root": "project root", "cwd.no.workspace": "DSH has not opened a workspace yet. Start a task in DSH first, then come back.", "pill.current": "On", "btn.use": "Enable", "btn.inUse": "On", "confirm.delete": "Delete {id}?", "ph.id": "id", "ph.alias": "Name", "ph.alias.short": "Name", "btn.create": "New", "need.id": "id required", "ph.content": "", "btn.saveRule": "Save", "skills.title": "Skills", "skills.hint": "After import, DSH calls it: type /name in chat to load now; the model calls the skill tool when the task matches description or whenToUse. Writing「激活」in the body does nothing.", "skills.hint.desktop": "This desktop host’s official skill catalog. Type /name in chat to load; the model calls the skill tool when the task matches the description.", "skills.empty": "No user skills", "btn.importZip": "Import zip", "btn.importFolder": "Import folder", "skills.importing": "Importing…", "saved.import": "Imported {count} skill(s)", "need.import": "Choose a zip or folder", "err.import.folder": "No files were read. Select the skill folder itself (it must contain SKILL.md).", "skills.pick": "Select a skill", "skills.invalid": "Official catalog will ignore this: invalid format", "skills.call.slash": "Type {cmd} in chat", "skills.call.model": "Model can load", "skills.call.modelOff": "Hidden from model", "skills.call.userOff": "/name disabled", "ph.skill.desc": "description (when to use)", "btn.saveSkill": "Save", "need.skill.id": "id must be kebab-case, e.g. code-review", "confirm.delete.skill": "Delete {id} from official $DSH_HOME/skills? The host will drop it from the catalog.", "err.skills": "Read failed: {error}", "err.skills.needRestart": "Skill API is not loaded. Restart this host first.", "btn.restart": "Restart", "btn.restart.busy": "Restarting…", "restarting": "Restarting…", "restarting.desktop": "Restarting the desktop app…", "restart.timeout": "Timed out; refresh", "restart.fail": "Failed: {error}", "restart.notStarted": "Restart did not start. This client is still open, and it will not try again by itself.", "restart.already": "This restart already started. It will not restart again.", "restart.confirm": "Apply finished. Restart to take effect.", "restart.confirm.desktop": "Apply finished. Restart the desktop app to take effect.", "restart.ask.title": "Restart now?", "restart.ask.body": "It is written. It takes effect in this client after a restart.", "restart.now": "Restart now", "restart.notNow": "Later", "restart.asarSwap": "Patches are on disk. Quitting the desktop app and moving app.asar aside; it will reopen when done. If behavior is unchanged, fully quit Harness (including tray) and Apply again.", "surface.web": "Web", "surface.desktop": "Desktop", "surface.gui": "GUI", "surface.tui": "TUI", "restart.incomplete": "The prompt is in. {detail}", "restart.incomplete.patches": "{failed} code patch(es) did not match this host (#{ids}). The prompt is unaffected", "restart.incomplete.flash": "CMD silence not pinned ({entry})", "restart.later": "Later", "group.prompt": "Prompt", "group.code": "Code", "group.engine": "Engine", "group.tools": "Tools", "group.compat": "0.1.5 compat", "group.dsh20": "0.2.0", "patch.1": "Global disclaimer → mandate", "patch.2": "Replacement disclaimer → mandate", "patch.3": "Scope disclaimer → mandate", "patch.4": "Persona session mandate", "patch.5": "Approval copy → full access", "patch.6": "Sandbox deny → allow write", "patch.7": "Sandbox default full access", "patch.8": "Approval default never", "patch.9": "Approval gate auto-grant", "patch.10": "Approval config default never", "patch.11": "never copy → auto-grant", "patch.12": "ask copy → auto-grant", "patch.13": "Escalation ladder exempt", "patch.14": "Escalation unconditional grant", "patch.15": "Sandbox confine passthrough", "patch.16": "Filesystem fence off", "patch.17": "Observation policy allow R/W", "patch.18": "Repeat-call guard off", "patch.19": "Tool-result pruner off", "patch.21": "Restore plugin-raised bash wait", "patch.22": "read cap raised", "patch.23": "subagent depth 3 → 10", "patch.24": "preset fetch on", "patch.25": "Strip official default identity (keep feature prompts)", "patch.26": "minimal: strip identity, open inject", "patch.28": "dsh-liangshen: keep dsh-purge / dshpg in phase-1", "patch.70": "Minimal shell drops the no-network line", "patch.71": "Liangshen drops the stock identity line", "patch.29": "dsh-tool-web: untrusted framing → usable data", "patch.30": "dsh-hooks-claude-code: deny→allow", "patch.31": "dsh-hooks-codex: deny→allow", "patch.32": "Web surface identity neutral", "patch.33": "Shell denial prompt → executable", "patch.34": "Sandbox hint neutralized", "patch.35": "Escalation never-reject", "patch.36": "FS escalation schema opened", "patch.37": "Subagent scope lock neutralized", "patch.38": "persona text→prefix (0.1.2 presets)", "patch.39": "session v0 plugin summary (mnemon)", "patch.40": "Keep inject when a complete prompt is set", "patch.42": "Settings legacy API", "patch.43": "Strip identity from web presets", "patch.45": "Official desktop update handoff", "patch.46": "Official update script stays ASCII", "patch.47": "OAuth no longer inserts Claude identity", "patch.48": "Shell drops extra delete caution", "patch.49": "OAuth no longer mimics claude-cli", "patch.50": "Session references are not marked untrusted", "patch.51": "Fresh session stays on full access", "patch.52": "Session log upload off by default", "patch.53": "Web prompt drops do-not-start", "patch.54": "Deliverable prompt drops extra prohibition", "patch.55": "Replace the first system prompt every step", "patch.56": "Rewind hides only the cut range", "patch.57": "developer role for openai/azure only, third-party gateways use system", "patch.58": "Rewind markers stay off the model request", "patch.59": "Official Auto Review no longer denies tools", "patch.60": "Official run_code drops approval teaching", "patch.61": "Official sandbox policy no longer teaches refusal", "patch.62": "Fail-open without approval channel (user reject kept)", "patch.63": "Plan mode no longer soft-blocks execution", "patch.64": "Stop hook no longer steers fake user text", "patch.65": "UserPromptSubmit deny no longer rejects turn", "patch.66": "Empty system section no longer ends the turn as an internal error", "patch.67": "A missing messages list no longer ends the turn", "patch.68": "Missing content after rewind no longer ends the turn", "patch.69": "deepseek-flash keeps the official in-history flag", "patch.72": "Messages system field uses the current system prompt", "patch.73": "Minimal preset drops stray mirror sentence", "patch.74": "Hang default prompt before first assemble", "patch.75": "Rewind resets completed todos to pending", "patch.76": "Unrestricted preset: strip identity sentence", "patch.77": "Rewind marker drops already-sent messages", "patch.78": "Restore missing overlay from .bak backup", "patch.80": "Restore official minimal tool plugins", "patch.82": "Fallback DSH_PROFILE=desktop in main process", "patch.83": "Don't fail on signal kill (no crash dialog)", "patch.84": "Don't fail on code=0 clean exit (no stopped dialog)", "status.na": "Not for this host", "metric.sealed": "Official client found. Apply to unpack", "metric.sealed.count": "Sealed", "metric.lost": "Patches lost — Apply again", "metric.lost.count": "Lost", "status.unlocated": "Not located", "metric.unlocated": "This install was not found yet", "rewind.label": "Undo", "rewind.aria": "Undo", "rewind.busy": "Undoing…", "rewind.empty": "Nothing to undo", "rewind.fail": "Undo failed: {error}", "rewind.once": "Undo once", "rewind.once.hint": "Put this round's message back in the box and drop only this round", "rewind.round": "Undo last round", "rewind.round.hint": "Put this round's message back in the box; subagents drop only this round", "gate.title": "Approval and audit", "gate.hint": "Audit always records. The approval tier only affects model-issued purge_apply / purge_revert / purge_uninstall; your own clicks are unaffected.", "gate.mode.audit": "Record only (default, same as before)", "gate.mode.enforce": "Require one-time approval (allowed-once)", "gate.gated": "Gated tools: {list}", "gate.saved": "Saved; applies to tool calls after dsh restarts.", "gate.audit.show": "Show audit trail", "gate.audit.hide": "Hide audit trail", "gate.audit.refresh": "Refresh", "gate.audit.busy": "Reading…", "gate.audit.empty": "No records yet.", "hooksDeny.title": "Hook interception", "hooksDeny.bypass": "Bypass hook deny / ask (patches 30 / 31 / 64 / 65)", "hooksDeny.hint": "When off, your own PreToolUse / UserPromptSubmit / Stop hook deny runs as the official host wrote it. The official ask branch is a no-op; turning this off will not show an approval dialog. Click Apply and restart to write the host files.", "saved.hooksDeny": "Saved", "continue.title": "Retry / Continue", "continue.hint": "Failed requests auto-retry. After an abnormal stop or interrupt, use Continue or auto-resume. A manual stop never auto-continues. Counts reset after a completed turn.", "continue.autoRetry": "Auto-retry on failure", "continue.retryMax": "Retry count", "continue.autoContinue": "Auto-continue after interrupt", "continue.continueMax": "Continue count", "continue.text": "Continue text", "saved.continue": "Saved", "continue.label": "Continue", "continue.aria": "Continue after an abnormal stop", "continue.busy": "Continuing…", "continue.ready": "Continue ({left} left)", "continue.limit": "Continue limit reached", "continue.fail": "Continue failed: {error}", }; /* ──────────────────────────────────────────────────────────────────────── * 视觉层 v2(对齐 DeepSeek Harness 自身的设计语言) * * 旧版是自己发明的一套皮:衬线标题 + 橄榄绿玻璃 + 手写 --dshp-* 调色板。 * 结果是「设置里嵌了个别的产品」。现在整个面板只吃官方 token: * · 排版 --dsw-font-*(s-14 / xs-13 / xxs-12 / xxxs-11 与官方设置页同阶) * · 圆角 --dsw-radius-{xs,sm,md,lg,xl} * · 底色 --dsw-alias-bg-layer-{1,2,3} / bg-base * · 描边 --dsw-alias-border-l{1..4} * · 文字 --dsw-alias-label-{primary,secondary,tertiary,caption} * · 状态 --dsw-alias-state-{success,warn,error}-primary * · 交互 --dsw-alias-interactive-bg-{hover,active} * · 卡片 --dsw-alias-settings-card-{fill,stroke} * 保留 --dshp-* 作为语义别名(ok/warn/bad/accent…),让老组件不用改。 * 强调色只出现在三处:主按钮、当前项、状态点。其余一律中性色。 * ──────────────────────────────────────────────────────────────────────── */ const PURGE_CSS = ` .dshp-root{ --dshp-display:var(--dsw-font-family,inherit); --dshp-sans:var(--dsw-font-family,inherit); --dshp-mono:var(--dsh-family-code,"Cascadia Mono",ui-monospace,SFMono-Regular,Menlo,monospace); --dshp-ease:var(--ds-ease-in-out,cubic-bezier(.4,0,.2,1)); --dshp-bg:transparent; --dshp-paper:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-2,color-mix(in srgb,currentColor 4%,transparent))); --dshp-ink:var(--dsw-alias-label-primary,currentColor); --dshp-mute:var(--dsw-alias-label-secondary,color-mix(in srgb,currentColor 62%,transparent)); --dshp-line:var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l2,color-mix(in srgb,currentColor 12%,transparent))); --dshp-fill:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 6%,transparent)); --dshp-accent:var(--dsw-alias-state-business-primary,var(--dsw-alias-button-info-fill,#4176e6)); --dshp-accent-soft:color-mix(in srgb,var(--dshp-accent) 14%,transparent); --dshp-ok:var(--dsw-alias-state-success-primary,#22c55e); --dshp-warn:var(--dsw-alias-state-warn-primary,#f59e0b); --dshp-bad:var(--dsw-alias-state-error-primary,#ef4444); --dshp-danger:var(--dsw-alias-state-error-primary,#ef4444); max-width:720px;display:flex;flex-direction:column;gap:var(--dsw-gap-16,16px);padding:0;border-radius:0;background:transparent;color:var(--dshp-ink); font-family:var(--dshp-sans);font-size:14px;line-height:22px;-webkit-font-smoothing:antialiased;text-shadow:none} .dshp-root[data-theme="white"]{color-scheme:light} .dshp-root[data-theme="dusk"]{color-scheme:dark} .dshp-root *,.dshp-root *::before,.dshp-root *::after{box-sizing:border-box} /* 分区标题:官方设置页的 h2/h3 阶(16/24 500),不用衬线 */ .dshp-title,.dshp-sub h4,.dshp-block-head h4,.dshp-group-h strong,.dshp-metric b,.dshp-rule-name,.dshp-now-ver{ font-family:inherit;font-weight:500;letter-spacing:0;color:var(--dshp-ink);margin:0} .dshp-title{font-size:16px;line-height:24px} .dshp-kicker{font-family:var(--dshp-mono);font-size:11px;line-height:16px;letter-spacing:.02em;color:var(--dsw-alias-label-caption,var(--dshp-mute));margin:0 0 2px;text-transform:none} .dshp-toolbar{display:flex;align-items:center;justify-content:flex-end;gap:8px} .dshp-switch{display:inline-flex;border:.5px solid var(--dshp-line);border-radius:var(--dsw-radius-md,12px);overflow:hidden;background:var(--dsw-alias-button-ghost-active-fill,var(--dshp-fill))} .dshp-switch button{appearance:none;border:0;background:transparent;color:var(--dshp-mute);font:inherit;font-size:12px;line-height:16px;padding:5px 12px;cursor:pointer;border-radius:calc(var(--dsw-radius-md,12px) - 1px)} .dshp-switch button.is-on{background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dshp-ink);box-shadow:0 1px 2px color-mix(in srgb,#000 10%,transparent)} .dshp-switch button:focus-visible{outline:2px solid var(--dshp-accent);outline-offset:-2px} /* 面板 = 官方 settings rowCard:0.5px 描边 + 卡片底色 + xl 圆角 */ .dshp-panel{border:.5px solid var(--dshp-line);background:var(--dshp-paper);border-radius:var(--dsw-radius-xl,20px);padding:14px 16px;display:flex;flex-direction:column;gap:12px} .dshp-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin:0;flex-wrap:wrap} .dshp-panel-lead{margin:2px 0 0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary,var(--dshp-mute));max-width:60ch} .dshp-sub{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:4px 0 0} .dshp-sub h4{font-size:14px;line-height:22px} /* 指标条:不套卡片。面板本身已经是卡,里面再摆三张卡就是卡中卡。 这里用一条统计带 + 竖分隔线,数字自己承担层级。 */ .dshp-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;padding:12px 0 2px;border-top:.5px solid var(--dsw-alias-border-l1,var(--dshp-line))} .dshp-metric{display:flex;flex-direction:column;justify-content:center;gap:2px;padding:0 16px;min-height:44px} .dshp-metric:first-child{padding-left:0} .dshp-metric+.dshp-metric{border-left:.5px solid var(--dsw-alias-border-l1,var(--dshp-line))} .dshp-metric b{font-size:16px;line-height:24px;font-variant-numeric:tabular-nums;font-weight:500} .dshp-metric span{font-size:12px;line-height:18px;color:var(--dshp-mute)} .dshp-bar{height:4px;border-radius:999px;background:var(--dsw-alias-bg-skeleton,var(--dshp-fill));overflow:hidden;margin:0} .dshp-bar>i{display:block;height:100%;background:var(--dshp-accent);border-radius:999px;width:0;transition:width .4s var(--dshp-ease)} /* 补丁分组:行,不是卡。面板里每一组只占一行(展开才出现表格), 用发丝线分隔,避免「卡片里套卡片」。 */ .dshp-group{border:0;border-top:.5px solid var(--dsw-alias-border-l1,var(--dshp-line));margin:0;background:transparent;border-radius:0;overflow:hidden} .dshp-group-h{width:100%;display:flex;align-items:center;gap:10px;padding:9px 0;border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;text-align:left} .dshp-group-h:hover{background:transparent;color:var(--dshp-ink)} .dshp-group-h:hover strong{text-decoration:underline;text-underline-offset:2px} .dshp-group-h:focus-visible{outline:2px solid var(--dshp-accent);outline-offset:-2px} .dshp-group-h strong{font-size:14px;line-height:22px} .dshp-group-h em{font-style:normal;font-size:12px;line-height:18px;color:var(--dshp-mute);flex:1} .dshp-count{font-family:var(--dshp-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption,var(--dshp-mute));font-variant-numeric:tabular-nums} .dshp-caret{display:inline-block;width:12px;flex:none;font-size:11px;line-height:1;color:var(--dsw-alias-label-tertiary,var(--dshp-mute));transition:transform .15s var(--dshp-ease)} .dshp-group.is-open .dshp-caret{transform:rotate(90deg)} .dshp-table{width:100%;border-collapse:collapse;font-size:13px;line-height:20px} .dshp-table th{text-align:left;font-weight:500;font-size:12px;line-height:18px;color:var(--dshp-mute);padding:7px 12px;border-bottom:.5px solid var(--dshp-line)} .dshp-table td{padding:7px 12px;border-bottom:.5px solid var(--dsw-alias-border-l1,var(--dshp-line));vertical-align:middle;color:var(--dshp-ink)} .dshp-table tr:last-child td{border-bottom:0} .dshp-table tr.is-on td{background:var(--dshp-accent-soft)} .dshp-id{font-family:var(--dshp-mono);font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption,var(--dshp-mute));width:44px;font-variant-numeric:tabular-nums} .dshp-actions{text-align:right;white-space:nowrap} .dshp-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .dshp-row+.dshp-row{margin-top:10px} /* 审批档位:与上方 hooks 开关同区块,靠发丝线分隔,不再套一层卡片 */ .dshp-gate{margin-top:12px;padding-top:12px;border-top:.5px solid var(--dsw-alias-border-l2,var(--dshp-line))} .dshp-gate .dshp-hint{margin:0 0 6px;color:var(--dsw-alias-label-tertiary,var(--dshp-mute));font-size:12px;line-height:18px} .dshp-gate label{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--dsw-alias-label-primary,inherit)} .dshp-audit{max-height:220px;overflow:auto;margin:8px 0 0;padding:10px 12px;border-radius:var(--dsw-radius-md,12px); background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.03));font-family:var(--dsw-font-family-mono,ui-monospace,monospace); font-size:11.5px;line-height:17px;white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-secondary,var(--dshp-mute))} /* 状态胶囊:中性底 + 语义点,不再用彩色描边 */ .dshp-pill{display:inline-flex;align-items:center;gap:6px;font-family:inherit;font-size:12px;line-height:18px;padding:1px 8px;border:.5px solid var(--dsw-alias-border-l3,var(--dshp-line));border-radius:999px;background:transparent;color:var(--dshp-mute);white-space:nowrap} .dshp-pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;flex:none} .dshp-pill.is-ok{color:var(--dshp-ok);border-color:color-mix(in srgb,var(--dshp-ok) 38%,var(--dsw-alias-border-l3,transparent))} .dshp-pill.is-wait{color:var(--dshp-warn);border-color:color-mix(in srgb,var(--dshp-warn) 38%,var(--dsw-alias-border-l3,transparent))} .dshp-pill.is-miss{color:var(--dsw-alias-label-dimmed,var(--dshp-mute))} .dshp-pill.is-bad{color:var(--dshp-bad);border-color:color-mix(in srgb,var(--dshp-bad) 38%,var(--dsw-alias-border-l3,transparent))} /* 按钮:官方 primaryButton / secondaryButton 尺寸(h36 / md 圆角 / 14px) */ .dshp-btn{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:4px;font-family:inherit;font-size:14px;line-height:22px;font-weight:400;min-height:36px;padding:0 14px;border-radius:var(--dsw-radius-md,12px);border:.5px solid transparent;cursor:pointer;transition:background .15s var(--dshp-ease),border-color .15s var(--dshp-ease),opacity .15s var(--dshp-ease);background:transparent;color:var(--dshp-ink)} .dshp-btn:disabled{opacity:.45;cursor:not-allowed} .dshp-btn:focus-visible{outline:2px solid var(--dshp-accent);outline-offset:2px} .dshp-btn-primary{background:var(--dsw-alias-button-primary-fill,var(--dshp-ink));color:var(--dsw-alias-label-primary-foreground,#fff);border-color:transparent} .dshp-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,color-mix(in srgb,var(--dshp-ink) 82%,transparent))} .dshp-btn-ghost{background:transparent;color:var(--dshp-ink);border-color:var(--dsw-alias-border-l3,var(--dshp-line))} .dshp-btn-ghost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid,var(--dshp-fill))} .dshp-btn-danger{background:transparent;color:var(--dshp-bad);border-color:color-mix(in srgb,var(--dshp-bad) 44%,var(--dsw-alias-border-l3,transparent))} .dshp-btn-danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger,color-mix(in srgb,var(--dshp-bad) 10%,transparent))} .dshp-btn-solid-danger{background:color-mix(in srgb,var(--dshp-bad) 14%,transparent);color:var(--dshp-bad);border-color:color-mix(in srgb,var(--dshp-bad) 40%,transparent)} .dshp-btn-solid-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dshp-bad) 22%,transparent)} .dshp-btn-tiny{min-height:28px;padding:0 10px;font-size:13px;line-height:20px} /* 输入:官方 input —— 0.5px l4 描边 / md 圆角 / layer-1 底 */ .dshp-field,.dshp-area{width:100%;font-family:inherit;font-size:13px;line-height:20px;padding:6px 10px;background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dshp-ink);border:.5px solid var(--dsw-alias-border-l4,var(--dshp-line));border-radius:var(--dsw-radius-md,12px);resize:vertical} .dshp-field{width:auto;min-width:140px;height:32px;padding:0 10px} .dshp-area{font-family:var(--dshp-mono);font-size:12.5px;line-height:1.55;min-height:220px} .dshp-field:focus,.dshp-area:focus{outline:none;border-color:var(--dshp-accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--dshp-accent) 18%,transparent)} .dshp-field:disabled,.dshp-area:disabled{opacity:.5} .dshp-cr-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0;font-size:13px;line-height:20px} .dshp-cr-row label{display:inline-flex;align-items:center;gap:6px;color:var(--dshp-ink);cursor:pointer} .dshp-cr-num{width:64px;min-width:64px;height:28px;padding:0 8px} .dshp-cr-text{width:120px;min-width:88px;height:28px;padding:0 8px} .dshp-ver{width:100%;min-width:0;max-width:none;height:32px;padding:0 10px;font-size:12px} .dshp-rel{margin:0;display:flex;flex-direction:column;gap:8px} .dshp-now{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin:0} .dshp-now-label{font-size:12px;line-height:18px;color:var(--dshp-mute)} .dshp-now-ver{font-size:16px;line-height:24px;font-variant-numeric:tabular-nums} .dshp-now-mark{display:inline-flex;align-items:center;padding:0 6px;border-radius:var(--dsw-radius-xs,4px);font-family:inherit;font-size:11px;line-height:16px;font-style:normal;font-weight:500;color:var(--dshp-ok);border:.5px solid color-mix(in srgb,var(--dshp-ok) 40%,transparent);background:color-mix(in srgb,var(--dshp-ok) 12%,transparent)} .dshp-editions{display:flex;flex-direction:column;margin:0;border-top:.5px solid var(--dsw-alias-border-l1,var(--dshp-line))} .dshp-edition{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;padding:8px 0;border-bottom:.5px solid var(--dsw-alias-border-l1,var(--dshp-line))} .dshp-edition-name{display:inline-flex;align-items:center;gap:0;font-size:14px;line-height:22px;font-weight:500;white-space:nowrap} .dshp-edition.is-on .dshp-edition-name{color:var(--dshp-ink)} .dshp-edition:not(.is-on) .dshp-edition-name{color:var(--dshp-mute)} .dshp-edition .dshp-btn{min-width:118px} .dshp-rel .dshp-notice{display:block;min-height:16px} @media (max-width:560px){.dshp-edition{grid-template-columns:1fr auto;grid-template-areas:"name btn" "sel sel"}.dshp-edition-name{grid-area:name}.dshp-edition .dshp-ver{grid-area:sel}.dshp-edition .dshp-btn{grid-area:btn}} .dshp-ask{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0;padding:10px 12px;border:.5px solid var(--dshp-line);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,transparent);font-size:13px;line-height:20px} /* 卸载确认:设置面板本身 z-index:1000,这层遮罩必须盖在它上面, 否则在设置页里点「卸载」看不到确认框。 */ .dshp-modal-bg{position:fixed;inset:0;z-index:1300;display:flex;align-items:center;justify-content:center;padding:20px;background:var(--dsw-alias-bg-mask-3,rgba(0,0,0,.48));backdrop-filter:blur(2px)} .dshp-modal{width:min(420px,100%);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dshp-ink);border:.5px solid var(--dshp-line);border-radius:var(--dsw-radius-xl,20px);padding:20px;box-shadow:var(--dsw-elevation-prominent,0 16px 40px rgba(0,0,0,.22))} .dshp-modal h4{margin:0 0 8px;font-size:16px;line-height:24px;font-weight:500} .dshp-modal p{margin:0 0 16px;font-size:14px;line-height:22px;color:var(--dshp-mute)} .dshp-modal-ops{display:flex;justify-content:flex-end;gap:8px} .dshp-restart-mask{position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-3,rgba(0,0,0,.55));font-family:var(--dsw-font-family,inherit)} .dshp-restart-card{width:min(420px,100%);box-sizing:border-box;padding:20px;border-radius:var(--dsw-radius-xl,20px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);border:.5px solid var(--dsw-alias-border-l3,#ddd);box-shadow:var(--dsw-elevation-prominent,0 18px 50px rgba(0,0,0,.28))} .dshp-restart-card h4{margin:0 0 8px;font-family:inherit;font-size:16px;line-height:24px;font-weight:500;color:inherit} .dshp-restart-card p{margin:0 0 16px;font-size:14px;line-height:22px;color:var(--dsw-alias-label-secondary,inherit)} .dshp-restart-card .dshp-modal-ops{display:flex;justify-content:flex-end;gap:8px} .dshp-restart-card .dshp-btn{min-height:36px;padding:0 14px;font-size:14px;font-weight:400;color:var(--dsw-alias-label-primary,#111);background:transparent;border:.5px solid var(--dsw-alias-border-l3,#ccc)} .dshp-restart-card .dshp-btn-primary,.dshp-restart-card .dshp-btn-primary:hover:not(:disabled){color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-button-primary-fill,#111);border-color:transparent} .dshp-notice{font-size:12px;line-height:18px} .dshp-notice.is-ok{color:var(--dshp-ok)} .dshp-notice.is-bad{color:var(--dshp-bad)} .dshp-skel{height:12px;border-radius:6px;background:linear-gradient(90deg,var(--dshp-fill),var(--dshp-line),var(--dshp-fill));background-size:200% 100%;animation:dshp-pulse 1.2s ease-in-out infinite} .dshp-skel-rows{display:flex;flex-direction:column;gap:10px;padding:14px 0 4px} .dshp-skel-rows .dshp-skel{height:10px;border-radius:5px} .dshp-skel-rows .dshp-skel:nth-child(1){width:38%} .dshp-skel-rows .dshp-skel:nth-child(2){width:52%} .dshp-skel-rows .dshp-skel:nth-child(3){width:29%} /* ── 设置页(1.3.0 主入口)─────────────────────────────────────────────── 与官方设置页同构:标题 16/24、说明 14/22 tertiary、段间 16。 分段控件用官方 toolbar 的写法(ghost 底 + 选中层一),不要 capsule 药丸。 */ /* 官方 .options 只有左右/下 padding,顶部留白由各 section 自己给(模型页靠首行说明)。 这里补 8px,避免标题贴住设置外壳的 header 发丝线。 */ .dshp-page{max-width:720px;gap:16px;padding-top:8px} .dshp-page-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap} .dshp-page-text{display:flex;flex-direction:column;gap:4px;min-width:0} .dshp-page-lead{margin:0;font-size:14px;line-height:22px;color:var(--dsw-alias-label-tertiary,var(--dshp-mute));max-width:52ch} .dshp-segbar{display:inline-flex;align-items:center;gap:2px;padding:2px;border:.5px solid var(--dsw-alias-border-l3,var(--dshp-line));border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-button-ghost-active-fill,var(--dshp-fill));flex:none} .dshp-seg{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,var(--dshp-mute));font:inherit;font-size:13px;line-height:20px;padding:4px 12px;border-radius:calc(var(--dsw-radius-md,12px) - 2px);cursor:pointer;white-space:nowrap;transition:background .15s var(--dshp-ease),color .15s var(--dshp-ease)} .dshp-seg:hover{color:var(--dsw-alias-label-primary,var(--dshp-ink))} .dshp-seg.on{background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base,#fff));color:var(--dsw-alias-label-primary,var(--dshp-ink));box-shadow:0 1px 2px color-mix(in srgb,#000 12%,transparent)} .dshp-seg:focus-visible{outline:2px solid var(--dshp-accent);outline-offset:1px} @media (max-width:620px){.dshp-page-head{flex-direction:column;align-items:stretch}.dshp-segbar{align-self:flex-start}} .dshp-active{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;margin:0;border:.5px solid var(--dshp-line);background:var(--dsw-alias-bg-layer-1,transparent);border-radius:var(--dsw-radius-md,12px)} .dshp-active b{font-family:inherit;font-weight:500} .dshp-empty{padding:28px 12px;font-size:13px;line-height:20px;color:var(--dshp-mute);text-align:center} .dshp-split{display:flex;flex-direction:column;gap:12px} .dshp-rulelist{display:flex;flex-direction:column;border:.5px solid var(--dshp-line);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,transparent);overflow:hidden} .dshp-rulebody{max-height:240px;overflow:auto} .dshp-ruleitem{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:0;border-bottom:.5px solid var(--dsw-alias-border-l1,var(--dshp-line));background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer} .dshp-ruleitem:hover{background:var(--dshp-fill)} .dshp-ruleitem.is-edit{background:var(--dshp-accent-soft)} .dshp-rule-main{flex:1;min-width:0} .dshp-rulename,.dshp-rule-name{display:flex;align-items:center;gap:8px;font-family:inherit;font-size:14px;line-height:22px} .dshp-rule-meta{display:block;margin-top:2px;font-family:var(--dshp-mono);font-size:11px;line-height:16px;color:var(--dshp-mute)} .dshp-rule-ops{display:flex;flex-direction:row;align-items:center;gap:6px;flex-shrink:0} .dshp-create{display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-top:.5px solid var(--dshp-line);background:transparent} .dshp-create-row{display:flex;gap:6px;align-items:center} .dshp-create .dshp-field{flex:1;min-width:0;width:auto} .dshp-editor{display:flex;flex-direction:column;gap:10px;min-width:0;border:.5px solid var(--dshp-line);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,transparent);padding:12px;min-height:320px} .dshp-editor .dshp-area{flex:1;min-height:240px} .dshp-editor-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--dshp-mute);font-size:13px;line-height:20px;min-height:160px} .dshp-block{padding:14px 0;border-top:.5px solid var(--dshp-line)} .dshp-block-head{display:flex;align-items:center;gap:10px;margin:0 0 8px} .dshp-block-head h4{margin:0;font-family:inherit;font-size:14px;line-height:22px;font-weight:500;flex:1;color:var(--dshp-ink)} .dshp-block-hint{margin:6px 0 0;font-size:12px;line-height:18px;color:var(--dshp-mute)} .dshp-policy-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap} .dshp-policy-row>label{font-size:12px;line-height:18px;color:var(--dshp-mute);flex:0 0 auto} .dshp-cwd-path{margin:0 0 8px;font-family:var(--dshp-mono);font-size:11px;line-height:16px;color:var(--dshp-mute);word-break:break-all} .dshp-cwd-list{display:flex;flex-direction:column;border:.5px solid var(--dshp-line);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,transparent);overflow:hidden} .dshp-cwd-item{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-bottom:.5px solid var(--dsw-alias-border-l1,var(--dshp-line));cursor:pointer;margin:0} .dshp-cwd-item:last-child{border-bottom:0} .dshp-cwd-item:hover{background:var(--dshp-fill)} .dshp-cwd-item.is-on{background:var(--dshp-accent-soft)} .dshp-cwd-item input{margin:3px 0 0;flex:0 0 auto;accent-color:var(--dshp-accent)} .dshp-cwd-main{flex:1;min-width:0} .dshp-cwd-name{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-family:inherit;font-size:13px;line-height:20px;color:var(--dshp-ink)} .dshp-cwd-badge{font-family:inherit;font-size:11px;line-height:16px;padding:0 6px;border-radius:var(--dsw-radius-xs,4px);border:.5px solid var(--dsw-alias-border-l3,var(--dshp-line));color:var(--dshp-mute)} .dshp-cwd-size{font-family:var(--dshp-mono);font-size:11px;line-height:16px;color:var(--dshp-mute)} .dshp-cwd-sub{margin-top:2px;font-family:var(--dshp-mono);font-size:11px;line-height:16px;color:var(--dshp-mute);word-break:break-all} .dshp-cwd-prev{margin-top:4px;font-size:11px;line-height:16px;color:var(--dshp-mute);max-height:34px;overflow:hidden} @keyframes dshp-pulse{0%{background-position:200% 0}100%{background-position:-200% 0}} @media (max-width:640px){.dshp-metrics{grid-template-columns:1fr}.dshp-ruleitem{flex-wrap:wrap}.dshp-rule-ops{width:100%;justify-content:flex-end}} @media (prefers-reduced-motion:reduce){.dshp-btn,.dshp-bar>i{transition:none}.dshp-skel{animation:none}} /* ── 快捷面板(对话内浮窗)──────────────────────────────────────────────── 跟设置页同一套 token:不透明层一底 + 0.5px 描边 + xl 圆角 + 官方抬升阴影。 不再做绿玻璃渐变和衬线标题。 */ .dshp-dock{position:fixed;top:12px;right:12px;z-index:10050;display:flex;flex-direction:column;width:var(--dshp-dock-w,620px);height:var(--dshp-dock-h,72vh);min-width:380px;min-height:320px;max-width:calc(100vw - 16px);max-height:calc(100vh - 16px);box-sizing:border-box;background:var(--dsw-alias-bg-layer-1,#fff);border:.5px solid var(--dsw-alias-border-l3,#d5d5d5);border-radius:var(--dsw-radius-xl,20px);box-shadow:var(--dsw-elevation-prominent,0 18px 48px rgba(0,0,0,.24));pointer-events:auto;color:var(--dsw-alias-label-primary,#111);overflow:visible;font-family:var(--dsw-font-family,inherit);font-size:14px;line-height:22px} .dshp-dock[data-open="0"]{display:none} .dshp-launch{position:fixed;right:16px;bottom:16px;z-index:10040;appearance:none;display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 14px;border:.5px solid var(--dsw-alias-border-l3,#d0d0d0);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);cursor:pointer;font:inherit;font-size:13px;line-height:20px;box-shadow:0 6px 18px color-mix(in srgb,#000 14%,transparent);pointer-events:auto} .dshp-launch:hover{background:var(--dsw-alias-interactive-bg-hover-solid,color-mix(in srgb,currentColor 8%,transparent))} .dshp-launch[data-open="1"]{display:none} .dshp-dock-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:.5px solid var(--dsw-alias-border-l1,#eee);background:transparent;cursor:grab;user-select:none;touch-action:none;color:var(--dsw-alias-label-primary,#111);border-radius:var(--dsw-radius-xl,20px) var(--dsw-radius-xl,20px) 0 0} .dshp-dock-head:active{cursor:grabbing} .dshp-dock-head b{font-size:14px;line-height:22px;font-weight:500;color:inherit} .dshp-dock-head button{cursor:pointer;color:inherit;background:transparent;border:.5px solid var(--dsw-alias-border-l3,#d5d5d5);border-radius:var(--dsw-radius-sm,8px);padding:3px 10px;font:inherit;font-size:12px;line-height:18px} .dshp-dock-head button:hover{background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 8%,transparent))} .dshp-dock-tabs{display:flex;gap:2px;padding:6px 10px 0;border-bottom:.5px solid var(--dsw-alias-border-l1,#eee);background:transparent} .dshp-dock-tab{appearance:none;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary,#666);border-radius:0;padding:6px 10px;margin-bottom:-1px;cursor:pointer;font:inherit;font-size:13px;line-height:20px} .dshp-dock-tab:hover{color:var(--dsw-alias-label-primary,#111)} .dshp-dock-tab.on{color:var(--dsw-alias-label-primary,#111);font-weight:500;border-bottom-color:var(--dsw-alias-label-primary,#111);background:transparent} .dshp-dock-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;background:transparent;border-radius:0 0 var(--dsw-radius-xl,20px) var(--dsw-radius-xl,20px)} .dshp-dock-body .dshp-root{max-width:none;height:100%;flex:1;min-height:0;overflow:auto;background:transparent!important;padding:12px 14px;--dshp-paper:var(--dsw-alias-bg-layer-2,color-mix(in srgb,currentColor 4%,transparent));--dshp-fill:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 6%,transparent));--dshp-line:var(--dsw-alias-border-l2,color-mix(in srgb,currentColor 14%,transparent));--dshp-accent-soft:color-mix(in srgb,var(--dshp-accent) 14%,transparent)} .dshp-dock-body .dshp-panel,.dshp-dock-body .dshp-metric,.dshp-dock-body .dshp-group,.dshp-dock-body .dshp-editor,.dshp-dock-body .dshp-active,.dshp-dock-body .dshp-ask,.dshp-dock-body .dshp-create,.dshp-dock-body .dshp-rulelist,.dshp-dock-body .dshp-table,.dshp-dock-body .dshp-ruleitem,.dshp-dock-body .dshp-cwd-list,.dshp-dock-body .dshp-cwd-item{background:transparent!important;border-color:var(--dsw-alias-border-l1,#ececec)!important;color:var(--dshp-ink)!important;box-shadow:none;backdrop-filter:none;-webkit-backdrop-filter:none} .dshp-dock-body .dshp-panel,.dshp-dock-body .dshp-editor,.dshp-dock-body .dshp-rulelist,.dshp-dock-body .dshp-cwd-list{background:var(--dsw-alias-bg-layer-2,#fafafa)!important} .dshp-dock-body .dshp-field,.dshp-dock-body .dshp-area{background:var(--dsw-alias-bg-layer-1,#fff)!important;color:var(--dshp-ink)!important;border-color:var(--dsw-alias-border-l4,#d0d0d0)!important} .dshp-dock-body select.dshp-field,.dshp-dock-body .dshp-field.dshp-ver{background:var(--dsw-alias-bg-layer-1,#fff)!important;color:var(--dshp-ink)!important;color-scheme:inherit} .dshp-dock-body .dshp-title,.dshp-dock-body .dshp-sub h4,.dshp-dock-body .dshp-group-h strong,.dshp-dock-body .dshp-metric b,.dshp-dock-body .dshp-rule-name{font-family:inherit;font-weight:500;letter-spacing:0;color:var(--dshp-ink)!important;text-shadow:none} .dshp-dock-body .dshp-mute,.dshp-dock-body .dshp-kicker,.dshp-dock-body .dshp-metric span,.dshp-dock-body .dshp-group-h em,.dshp-dock-body .dshp-count,.dshp-dock-body .dshp-rule-meta,.dshp-dock-body .dshp-hint{color:var(--dshp-mute)!important;font-weight:400;text-shadow:none} .dshp-dock-body .rt-dock{position:relative;inset:auto;width:100%!important;height:100%;max-width:none!important;flex:1;min-height:0;min-width:0;display:flex!important;flex-direction:column;box-shadow:none;border:0;transform:none!important;opacity:1!important;pointer-events:auto!important;background:transparent!important} .dshp-dock-body .rt-grip{display:none!important} .dshp-dock-body .rt-embedded>.rt-head,.dshp-dock-body .rt-tabs,.dshp-dock-body .rt-foot{flex:none} .dshp-dock-body .rt-body{flex:1;min-height:0;overflow:hidden!important;background:transparent!important} .dshp-dock-body .rt-main{min-height:0;flex:1;overflow:hidden} .dshp-dock-body .rt-pane{min-height:0;overflow:auto!important} .dshp-dock-body .rt-embedded>.rt-head{padding:8px 12px} .dshp-dock-body .rt-embedded>.rt-head .rt-title{font-size:14px;letter-spacing:0} .dshp-dock-body .rt-tabs{gap:2px;padding:6px 10px 0;flex-wrap:wrap} .dshp-dock-body .rt-tab{border-radius:0;border-bottom:2px solid transparent;margin-bottom:-1px;padding:6px 9px;background:transparent;font-size:13px} .dshp-dock-body .rt-tab.on{border-bottom-color:var(--dsw-alias-label-primary,#111);background:transparent;font-weight:500} .dshp-dock-body .rt-row:hover,.dshp-dock-body .rt-item:hover,.dshp-dock-body .rt-item.on,.dshp-dock-body .rt-seg:hover,.dshp-dock-body .rt-seg.on,.dshp-dock-body .rt-vrow:hover,.dshp-dock-body .rt-score-row:hover{background:var(--rt-surf-2)!important} body:not([data-ds-dark-theme]) .dshp-auth-modal{background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);border-color:var(--dsw-alias-border-l3,#ddd)} body:not([data-ds-dark-theme]) .dshp-auth-legal{background:var(--dsw-alias-bg-layer-2,#fafafa);color:var(--dsw-alias-label-primary,#111);border-color:var(--dsw-alias-border-l3,#ddd)} .dshp-dock-resize,.dshp-dock-resize-l,.dshp-dock-resize-r,.dshp-dock-resize-b{position:absolute;z-index:60;pointer-events:auto;touch-action:none} .dshp-dock-resize{right:0;bottom:0;width:20px;height:20px;cursor:nwse-resize;border-radius:0 0 var(--dsw-radius-xl,20px) 0;background:linear-gradient(135deg,transparent 50%,var(--dsw-alias-border-l4,#c8c8c8) 50%);opacity:.9} .dshp-dock-resize-l{left:-2px;top:0;bottom:0;width:10px;cursor:ew-resize;background:transparent} .dshp-dock-resize-l:hover,.dshp-dock-resize-r:hover{background:color-mix(in srgb,currentColor 10%,transparent)} .dshp-dock-resize-r{right:-2px;top:0;bottom:20px;width:10px;cursor:ew-resize;background:transparent} .dshp-dock-resize-b{left:10px;right:20px;bottom:-2px;height:10px;cursor:ns-resize;background:transparent} .dshp-dock-resize-b:hover{background:color-mix(in srgb,currentColor 10%,transparent)} .dshp-auth-mask{position:fixed;inset:0;z-index:10100;background:var(--dsw-alias-bg-mask-3,rgba(0,0,0,.5));display:flex;align-items:center;justify-content:center;padding:20px;pointer-events:auto;backdrop-filter:blur(2px)} .dshp-env-gate{z-index:10200} .dshp-auth-modal{width:min(640px,100%);max-height:min(88vh,820px);display:flex;flex-direction:column;gap:10px;padding:20px;border:.5px solid var(--dsw-alias-border-l3,#ddd);border-radius:var(--dsw-radius-xl,20px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);box-shadow:var(--dsw-elevation-prominent,0 18px 48px rgba(0,0,0,.24));font-family:var(--dsw-font-family,inherit)} .dshp-auth-modal h2{margin:0;font-size:16px;line-height:24px;font-weight:500} .dshp-auth-warn{padding:8px 10px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-state-warn-tertiary,#fef5e7);color:var(--dsw-alias-state-warn-label,#8a5a12);font-size:13px;line-height:20px} .dshp-auth-legal{overflow:auto;height:280px;margin:0;padding:10px 12px;border:.5px solid var(--dsw-alias-border-l3,#ddd);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-2,#fafafa);font-size:13px;line-height:20px} .dshp-auth-legal h3{margin:12px 0 4px;font-size:13px;line-height:20px;font-weight:500} .dshp-auth-legal h3:first-child{margin-top:0} .dshp-auth-legal p{margin:0 0 8px} .dshp-auth-checks{display:flex;flex-direction:column;gap:8px;font-size:13px;line-height:20px} .dshp-auth-checks label{display:flex;gap:8px;align-items:flex-start;cursor:pointer} .dshp-auth-checks input{accent-color:var(--dsw-alias-state-business-primary,#4176e6);margin-top:3px} .dshp-auth-checks label.locked{opacity:.45;cursor:default} .dshp-auth-ops{display:flex;justify-content:flex-end;gap:8px} .dshp-auth-ops button{appearance:none;border:.5px solid var(--dsw-alias-border-l3,#d0d0d0);background:transparent;color:inherit;border-radius:var(--dsw-radius-md,12px);padding:0 14px;height:36px;cursor:pointer;font:inherit;font-size:14px;line-height:22px} .dshp-auth-ops button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid,color-mix(in srgb,currentColor 8%,transparent))} .dshp-auth-ops button.primary{background:var(--dsw-alias-button-primary-fill,#111);border-color:transparent;color:var(--dsw-alias-label-primary-foreground,#fff)} .dshp-auth-ops button.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#333)} .dshp-auth-ops button:disabled{opacity:.45;cursor:default} .dshp-icon-btn{appearance:none;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3,#d0d0d0);border-radius:var(--dsw-radius-md,12px);background:transparent;color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;font:inherit;font-size:13px;line-height:20px;white-space:nowrap;flex:0 0 auto} .dshp-icon-btn:hover{border-color:var(--dsw-alias-label-tertiary,#999);background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 6%,transparent));color:var(--dsw-alias-label-primary,currentColor)} .dshp-icon-btn.on{border-color:var(--dsw-alias-label-primary,currentColor);color:var(--dsw-alias-label-primary,currentColor);background:var(--dsw-alias-interactive-bg-active,color-mix(in srgb,currentColor 10%,transparent))} .dshp-hbtn{appearance:none;display:inline-flex;flex:0 0 auto;align-items:center;gap:6px;border:.5px solid var(--dsw-alias-border-l3,#d0d0d0);background:transparent;color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;font:inherit;font-size:12px;line-height:18px;padding:3px 10px;border-radius:var(--dsw-radius-sm,8px);white-space:nowrap} .dshp-hbtn:hover{border-color:var(--dsw-alias-label-tertiary,#999);background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 6%,transparent));color:var(--dsw-alias-label-primary,currentColor)} .dshp-hbtn.on{border-color:var(--dsw-alias-label-primary,currentColor);color:var(--dsw-alias-label-primary,currentColor);background:var(--dsw-alias-interactive-bg-active,color-mix(in srgb,currentColor 10%,transparent))} .dshp-live-dot{width:6px;height:6px;border-radius:50%;flex:none;background:var(--dsw-alias-state-success-primary,#22c55e);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 26%,transparent)} .dshp-hero-chip{display:inline-flex;align-items:center;margin-left:6px;flex:0 0 auto} .dshp-hero-chip-btn{appearance:none;display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l3,#d0d0d0);border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-secondary,currentColor);cursor:pointer;font:inherit;font-size:12px;line-height:18px;white-space:nowrap} .dshp-hero-chip-btn:hover{border-color:var(--dsw-alias-label-tertiary,#999);background:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 6%,transparent));color:var(--dsw-alias-label-primary,currentColor)} .dshp-hero-chip-btn.on{border-color:var(--dsw-alias-label-primary,currentColor);color:var(--dsw-alias-label-primary,currentColor);background:var(--dsw-alias-interactive-bg-active,color-mix(in srgb,currentColor 10%,transparent))} .dshp-tab-chip{display:inline-flex;align-items:flex-end;flex:0 0 auto} .dshp-tab-btn{appearance:none;position:relative;border:none;background:transparent;cursor:pointer;padding:0 0 9px;font:inherit;font-size:13px;font-weight:500;line-height:16px;color:var(--dsw-alias-label-tertiary,currentColor);white-space:nowrap} .dshp-tab-btn:hover{color:var(--dsw-alias-label-primary,currentColor)} .dshp-tab-btn.on{color:var(--dsw-alias-label-primary,currentColor)} .dshp-tab-btn.on:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;border-radius:2px;background:var(--dsw-alias-label-primary,currentColor)} .dshp-dock[data-theme="dusk"]{--dshp-ink:var(--dsw-alias-label-primary,#f9fafb);--dshp-mute:var(--dsw-alias-label-secondary,#cfd3d6)} .dshp-dock[data-theme="white"]{--dshp-ink:var(--dsw-alias-label-primary,#0f1115);--dshp-mute:var(--dsw-alias-label-secondary,#61666b)} .dshp-dock .dshp-dock-head,.dshp-dock .dshp-dock-tabs{background:transparent!important;color:var(--dshp-ink)!important} .dshp-dock .dshp-dock-head,.dshp-dock .dshp-dock-head b,.dshp-dock .dshp-dock-head button,.dshp-dock .dshp-dock-tab,.dshp-dock .dshp-dock-tab.on{color:var(--dshp-ink)!important;text-shadow:none!important;font-weight:500} .dshp-dock .dshp-dock-tab{font-size:13px} .dshp-dock .dshp-dock-tab.on{border-bottom-color:var(--dshp-ink)!important} .dshp-dock-body .rt-dock{font-size:13px!important;line-height:20px!important;font-weight:400!important;color:var(--dshp-ink)!important;--rt-ink:var(--dshp-ink);--rt-mute:var(--dshp-mute);--rt-surf:var(--dsw-alias-bg-layer-2,color-mix(in srgb,currentColor 4%,transparent));--rt-surf-2:var(--dsw-alias-interactive-bg-hover,color-mix(in srgb,currentColor 6%,transparent));--rt-line:var(--dsw-alias-border-l2,color-mix(in srgb,currentColor 14%,transparent))} .dshp-dock-body .rt-title,.dshp-dock-body .rt-tab,.dshp-dock-body .rt-tab.on,.dshp-dock-body .rt-card,.dshp-dock-body .rt-card h4,.dshp-dock-body .rt-row,.dshp-dock-body .rt-vrow,.dshp-dock-body .rt-kv,.dshp-dock-body .rt-kv b,.dshp-dock-body .rt-kv span,.dshp-dock-body .rt-mono,.dshp-dock-body .rt-empty,.dshp-dock-body .rt-foot,.dshp-dock-body .rt-tag,.dshp-dock-body .rt-item,.dshp-dock-body .rt-item-name,.dshp-dock-body .rt-item-desc,.dshp-dock-body .rt-sec-sub,.dshp-dock-body .rt-kb-sub,.dshp-dock-body .rt-seg-cidr,.dshp-dock-body .rt-seg-meta,.dshp-dock-body .rt-section,.dshp-dock-body .rt-note,.dshp-dock-body .rt-hint{color:var(--dshp-ink)!important;text-shadow:none!important;opacity:1!important} .dshp-dock-body .rt-tab,.dshp-dock-body .rt-foot,.dshp-dock-body .rt-empty,.dshp-dock-body .rt-item-desc,.dshp-dock-body .rt-kb-sub,.dshp-dock-body .rt-kv span,.dshp-dock-body .rt-tag,.dshp-dock-body .rt-sec-sub{color:var(--dshp-mute)!important} .dshp-dock-body .rt-card,.dshp-dock-body .rt-pane,.dshp-dock-body .rt-side,.dshp-dock-body .rt-table,.dshp-dock-body .rt-toolbar,.dshp-dock-body .rt-embedded>.rt-head,.dshp-dock-body .rt-tabs{background:transparent!important;border-color:var(--dsw-alias-border-l1,#ececec)!important;color:var(--dshp-ink)!important;box-shadow:none!important} .dshp-dock-body .rt-btn,.dshp-dock-body .rt-input,.dshp-dock-body .rt-eng-select,.dshp-dock-body textarea.rt-input{background:var(--dsw-alias-bg-layer-1,#fff)!important;color:var(--dshp-ink)!important;border-color:var(--dsw-alias-border-l4,#d0d0d0)!important} .dshp-dock-body .rt-btn-primary{background:var(--dsw-alias-button-primary-fill,#111)!important;border-color:transparent!important;color:var(--dsw-alias-label-primary-foreground,#fff)!important} .dshp-dock-body .rt-err{font-weight:500} .dshp-dock[data-theme="white"] .dshp-dock-body .rt-err{color:var(--dsw-alias-state-error-primary,#ef4444)!important} .dshp-dock[data-theme="dusk"] .dshp-dock-body .rt-err{color:var(--dsw-alias-state-error-primary,#f25a5a)!important} .dshp-dock-body .rt-rep-http,.dshp-dock-body pre{background:var(--dsw-alias-markdown-code-block,#f5f5f5)!important;color:var(--dshp-ink)!important;border:.5px solid var(--dsw-alias-border-l2,#e5e5e5)!important} `; function formatSize(bytes) { if (typeof bytes !== "number" || !isFinite(bytes) || bytes < 0) return "0 B"; if (bytes < 1024) return bytes + " B"; const kb = bytes / 1024; return (kb >= 100 ? Math.round(kb) : kb.toFixed(1)) + " KB"; } function statusKind(st) { if (st === "applied" || st === "already") return "ok"; if (st === "pending") return "wait"; if (st === "unmatched") return "bad"; if (st === "na") return "ok"; if (st === "missing_file" || st === "skipped" || st === "unlocated" || st === "off") return "miss"; return "bad"; } function statusLabel(st, t) { if (st === "applied" || st === "already") return t("status.applied"); if (st === "off") return t("status.skipped"); if (st === "pending") return t("status.pending"); if (st === "unmatched") return t("status.unmatched"); if (st === "skipped") return t("status.skipped"); if (st === "missing_file") return t("skip"); if (st === "unlocated") return t("status.unlocated"); if (st === "na") return t("status.na"); return st || t("unknown"); } function shimKind(v) { if (v === "patched" || v === "n/a") return "ok"; if (v === "original") return "wait"; if (v === "missing" || !v) return "miss"; return "bad"; } function noticeNode(notice) { if (!notice || notice.kind === "idle" || !notice.text) return null; return h("span", { className: "dshp-notice " + (notice.kind === "error" ? "is-bad" : "is-ok") }, notice.text); } function Btn(props) { const { kind, tiny, children, className, type, ...rest } = props; const cls = ["dshp-btn", kind ? "dshp-btn-" + kind : "dshp-btn-ghost", tiny ? "dshp-btn-tiny" : "", className || ""].filter(Boolean).join(" "); return h("button", Object.assign({ type: type || "button", className: cls }, rest), children); } function Pill({ value, label }) { const t = useT(); return h("span", { className: "dshp-pill is-" + statusKind(value) }, label || statusLabel(value, t)); } const LAYER_GROUP = { "提示词": "prompt", "代码": "code", "工具": "tools", "0.1.5 兼容": "compat", "0.2.0 兼容": "dsh20", }; const GROUP_ORDER = ["prompt", "code", "tools", "compat", "dsh20"]; function patchRowsOf(state) { if (!state || !state.ai_base) return []; const fromList = Array.isArray(state.patches) ? state.patches : null; const rows = fromList ? fromList.map((p) => ({ id: Number(p.id), st: p.status || "missing_file", layer: p.layer || "", })) : Object.keys(state.patch_status || {}).map((id) => ({ id: Number(id), st: state.patch_status[id], layer: "", })); return rows.filter((r) => r.st && r.st !== "na" && Number.isFinite(r.id)); } function tallyPatches(rows) { const out = { applied: 0, pending: 0, unmatched: 0, skipped: 0, total: 0 }; for (const row of rows || []) { out.total += 1; if (row.st === "applied" || row.st === "already") out.applied += 1; else if (row.st === "pending") out.pending += 1; else if (row.st === "unmatched") out.unmatched += 1; else out.skipped += 1; } return out; } function PatchGroups({ state }) { const t = useT(); const [open, setOpen] = useState({}); if (!state || !state.patch_status) return h("div", { className: "dshp-skel-rows" }, h("div", { className: "dshp-skel" }), h("div", { className: "dshp-skel" }), h("div", { className: "dshp-skel" }), ); const located = !!state.ai_base; if (!located) return null; const grouped = new Map(); for (const row of patchRowsOf(state)) { const key = LAYER_GROUP[row.layer] || "code"; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(row); } return GROUP_ORDER.filter((key) => grouped.has(key)).map((key) => { const rows = grouped.get(key).slice().sort((a, b) => a.id - b.id); if (!rows.length) return null; const tally = tallyPatches(rows); const notes = []; if (tally.pending) notes.push(t("metric.pendingShort", { n: tally.pending })); if (tally.unmatched) notes.push(t("metric.unmatchedShort", { n: tally.unmatched })); if (tally.skipped) notes.push(t("metric.skipShort", { n: tally.skipped })); const expanded = !!open[key]; const allDone = tally.applied === tally.total; return h("div", { key: key, className: "dshp-group" + (expanded ? " is-open" : "") }, h("button", { type: "button", className: "dshp-group-h", "aria-expanded": expanded ? "true" : "false", onClick: () => setOpen((prev) => Object.assign({}, prev, { [key]: !prev[key] })), }, h("span", { className: "dshp-caret", "aria-hidden": "true" }, "›"), h("strong", null, t("group." + key)), h("em", null, notes.length ? notes.join(" · ") : ""), h("span", { className: "dshp-count" }, tally.applied + "/" + tally.total), h(Pill, { value: allDone ? "applied" : (tally.pending ? "pending" : "unmatched") }), ), expanded ? h("table", { className: "dshp-table" }, h("thead", null, h("tr", null, h("th", null, "#"), h("th", null, t("table.patch")), h("th", null, t("table.status")), )), h("tbody", null, rows.map((r) => h("tr", { key: r.id }, h("td", { className: "dshp-id" }, "#" + r.id), h("td", null, t("patch." + r.id)), h("td", null, h(Pill, { value: r.st })), ))), ) : null, ); }); } /* 审批档位 + 审计流水。 * 两个诉求分开:审计永远记(谁调的、批没批、结果如何),审批只是可选门槛。 * 默认 audit 表示行为与旧版一致,仅多一份流水;enforce 才要求 allowed-once。 */ function GateSection({ t }) { const tr = typeof t === "function" ? t : ((key) => key); const [mode, setMode] = useState(null); const [gated, setGated] = useState([]); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState({ kind: "idle", text: "" }); const [auditOpen, setAuditOpen] = useState(false); const [auditText, setAuditText] = useState(""); const [auditBusy, setAuditBusy] = useState(false); useEffect(() => { apiJson("/dsh-purge/approval-mode") .then((d) => { if (d && d.ok) { setMode(d.mode === "enforce" ? "enforce" : "audit"); setGated(Array.isArray(d.gated) ? d.gated : []); } }) .catch(() => {}); }, []); const save = (next) => { if (busy || !next) return; const previous = mode; setMode(next); setBusy(true); apiJson("/dsh-purge/approval-mode", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ mode: next }), }) .then((d) => { if (d && d.ok) { setNotice({ kind: "ok", text: tr("gate.saved") }); } else { setMode(previous); setNotice({ kind: "error", text: tr("err.save", { error: (d && d.error) || "" }) }); } }) .catch((e) => { setMode(previous); setNotice({ kind: "error", text: tr("err.save", { error: e.message }) }); }) .finally(() => setBusy(false)); }; const loadAudit = () => { setAuditBusy(true); apiJson("/dsh-purge/audit?limit=40") .then((d) => setAuditText(d && d.ok ? String(d.text || "") : tr("err.status", { error: (d && d.error) || "" }))) .catch((e) => setAuditText(tr("err.status", { error: e.message }))) .finally(() => setAuditBusy(false)); }; const toggleAudit = () => { const next = !auditOpen; setAuditOpen(next); if (next) loadAudit(); }; if (!mode) return null; return h("div", { className: "dshp-gate" }, h("div", { className: "dshp-sub" }, h("h4", null, tr("gate.title")), noticeNode(notice), ), h("p", { className: "dshp-hint" }, tr("gate.hint")), h("div", { className: "dshp-row" }, h("label", null, h("input", { type: "radio", name: "dshp-approval-mode", checked: mode === "audit", disabled: busy, onChange: () => save("audit"), }), tr("gate.mode.audit"), ), h("label", null, h("input", { type: "radio", name: "dshp-approval-mode", checked: mode === "enforce", disabled: busy, onChange: () => save("enforce"), }), tr("gate.mode.enforce"), ), ), gated.length ? h("p", { className: "dshp-hint" }, tr("gate.gated", { list: gated.join(", ") })) : null, h("div", { className: "dshp-row" }, h(Btn, { tiny: true, onClick: toggleAudit }, auditOpen ? tr("gate.audit.hide") : tr("gate.audit.show")), auditOpen ? h(Btn, { tiny: true, disabled: auditBusy, onClick: loadAudit }, auditBusy ? tr("gate.audit.busy") : tr("gate.audit.refresh")) : null, ), auditOpen ? h("pre", { className: "dshp-audit" }, auditBusy ? tr("gate.audit.busy") : (auditText || tr("gate.audit.empty"))) : null, ); } function HooksDenySection({ onSaved }) { const t = useT(); const [cfg, setCfg] = useState(null); const [notice, setNotice] = useState({ kind: "idle", text: "" }); const [busy, setBusy] = useState(false); useEffect(() => { apiJson("/dsh-purge/hooks-deny") .then((d) => { if (d && d.ok) setCfg({ hooksDenyBypass: d.hooksDenyBypass !== false }); }) .catch(() => {}); }, []); const save = (hooksDenyBypass) => { if (!cfg || busy) return; setCfg({ hooksDenyBypass }); setBusy(true); apiJson("/dsh-purge/hooks-deny", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ hooksDenyBypass }), }) .then((d) => { if (d && d.ok) { setCfg({ hooksDenyBypass: d.hooksDenyBypass === true }); setNotice({ kind: "ok", text: t("saved.hooksDeny") }); if (typeof onSaved === "function") onSaved(); } else { setNotice({ kind: "error", text: t("err.save", { error: (d && d.error) || "" }) }); } }) .catch((e) => setNotice({ kind: "error", text: t("err.save", { error: e.message }) })) .finally(() => setBusy(false)); }; if (!cfg) return null; return h("div", { className: "dshp-cr", style: { marginTop: 12 } }, h("div", { className: "dshp-sub" }, h("h4", null, t("hooksDeny.title")), noticeNode(notice), ), h("p", { className: "dshp-hint", style: { margin: "0 0 4px", color: "var(--dshp-mute)", fontSize: 12 } }, t("hooksDeny.hint")), h("div", { className: "dshp-cr-row" }, h("label", null, h("input", { type: "checkbox", checked: cfg.hooksDenyBypass === true, disabled: busy, onChange: (e) => save(e.target.checked), }), t("hooksDeny.bypass"), ), ), ); } function ContinueRetrySection() { const t = useT(); const [cfg, setCfg] = useState(null); const [notice, setNotice] = useState({ kind: "idle", text: "" }); const [busy, setBusy] = useState(false); useEffect(() => { apiJson("/dsh-purge/continue/settings") .then((d) => { if (d && d.ok) setCfg(d); }) .catch(() => {}); }, []); const save = (patch) => { if (!cfg || busy) return; const next = { ...cfg, ...patch }; if (next.autoRetry && !(Number(next.retryMax) > 0)) next.retryMax = 3; if (next.autoContinue && !(Number(next.continueMax) > 0)) next.continueMax = 3; setCfg(next); setBusy(true); apiJson("/dsh-purge/continue/settings", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(next), }) .then((d) => { if (d && d.ok) { setCfg(d); setNotice({ kind: "ok", text: t("saved.continue") }); } else { setNotice({ kind: "error", text: t("err.save", { error: (d && d.error) || "" }) }); } }) .catch((e) => setNotice({ kind: "error", text: t("err.save", { error: e.message }) })) .finally(() => setBusy(false)); }; if (!cfg) return null; return h("div", { className: "dshp-cr" }, h("div", { className: "dshp-sub" }, h("h4", null, t("continue.title")), noticeNode(notice), ), h("p", { className: "dshp-hint", style: { margin: "0 0 4px", color: "var(--dshp-mute)", fontSize: 12 } }, t("continue.hint")), h("div", { className: "dshp-cr-row" }, h("label", null, h("input", { type: "checkbox", checked: Boolean(cfg.autoRetry), disabled: busy, onChange: (e) => save({ autoRetry: e.target.checked }), }), t("continue.autoRetry"), ), h("label", null, t("continue.retryMax"), h("input", { className: "dshp-field dshp-cr-num", type: "number", min: 0, max: 20, value: cfg.retryMax, disabled: busy, onChange: (e) => save({ retryMax: Number(e.target.value) }), }), ), ), h("div", { className: "dshp-cr-row" }, h("label", null, h("input", { type: "checkbox", checked: Boolean(cfg.autoContinue), disabled: busy, onChange: (e) => save({ autoContinue: e.target.checked }), }), t("continue.autoContinue"), ), h("label", null, t("continue.continueMax"), h("input", { className: "dshp-field dshp-cr-num", type: "number", min: 0, max: 20, value: cfg.continueMax, disabled: busy, onChange: (e) => save({ continueMax: Number(e.target.value) }), }), ), h("label", null, t("continue.text"), h("input", { className: "dshp-field dshp-cr-text", type: "text", value: cfg.continueText || "", disabled: busy, onChange: (e) => save({ continueText: e.target.value }), }), ), ), ); } function RestartAsk(props) { const node = h("div", { className: "dshp-restart-mask", "data-theme": props.theme === "dusk" ? "dusk" : "white", role: "dialog", "aria-modal": "true", "aria-labelledby": "dshp-restart-title", onClick: (e) => { if (e.target === e.currentTarget && props.onLater) props.onLater(); }, }, h("div", { className: "dshp-restart-card" }, h("h4", { id: "dshp-restart-title" }, props.t("restart.ask.title")), h("p", null, props.t("restart.ask.body")), h("div", { className: "dshp-modal-ops" }, h(Btn, { onClick: props.onLater }, props.t("restart.notNow")), h(Btn, { kind: "primary", onClick: props.onRestart }, props.t("restart.now")), ), ), ); try { const rd = require("react-dom"); if (rd && typeof rd.createPortal === "function" && typeof document !== "undefined" && document.body) { return rd.createPortal(node, document.body); } } catch { /* host 可能没暴露 react-dom */ } return node; } function PurgifySection() { const t = useT(); const tRef = useRef(t); tRef.current = t; const [state, setState] = useState(null); /* doAction 是 useCallback、依赖数组很短,直接闭包读 state 会永远拿到第一次的值。 * 用 ref 镜像当前状态,动作时才读真实 rev。 */ const stateRef = useRef(state); stateRef.current = state; const [override, setOverride] = useState(""); const [defaultOverride, setDefaultOverride] = useState(""); const [overrideLoaded, setOverrideLoaded] = useState(false); const [ownServers, setOwnServers] = useState(""); const [ownLoaded, setOwnLoaded] = useState(false); const [patchBusy, setPatchBusy] = useState(false); const [updateBusy, setUpdateBusy] = useState(false); const [canApplyUpdate, setCanApplyUpdate] = useState(false); const [updateJob, setUpdateJob] = useState(null); const [askRestart, setAskRestart] = useState(false); const suppressRestartAsk = useRef(false); const [askUninstall, setAskUninstall] = useState(false); const [uninstallBusy, setUninstallBusy] = useState(false); const [notice, setNotice] = useState({ kind: "idle", text: "" }); const [updateNotice, setUpdateNotice] = useState({ kind: "idle", text: "" }); const [updateInfo, setUpdateInfo] = useState(null); const [channel, setChannel] = useState("stable"); const [pick, setPick] = useState({ stable: "", beta: "" }); const actionTicket = useRef(0); const overrideRef = useRef(""); const defaultOverrideRef = useRef(""); const promptSeededRef = useRef(false); const promptAreaRef = useRef(null); overrideRef.current = override; defaultOverrideRef.current = defaultOverride; const readPromptBox = () => { const node = promptAreaRef.current; const live = node && typeof node.value === "string" ? node.value : overrideRef.current; overrideRef.current = live || ""; return overrideRef.current; }; const syncPicks = (d) => { const versions = (d && d.versions) || []; const ceiling = listedCeiling(versions, d && d.localVersion); const next = { stable: "", beta: "" }; for (const id of ["stable", "beta"]) { const list = versions.filter((item) => item.channel === id && keepListedVersion(item, ceiling)); const hit = preferListed(list, d && d.localVersion, d && d.pin); next[id] = (hit && hit.ref) || ""; } setPick(next); }; const updateErrorText = (tr, e) => { const msg = String((e && e.message) || e || ""); if (msg === "timeout" || /aborted|abort|超时/i.test(msg)) return tr("update.timeout"); if (/\b404\b/.test(msg) || /non-json/.test(msg)) return tr("update.needRestart"); return tr("update.fail", { error: msg }); }; const applyUpdateInfo = useCallback((d, tr, kind) => { if (d.channel) setChannel(d.channel); setUpdateInfo((prev) => { const versions = (d.versions && d.versions.length) ? d.versions : ((prev && prev.versions) || []); return Object.assign({}, prev || {}, d, { versions }); }); if (d.versions && d.versions.length) syncPicks(d); if (kind === "check") setCanApplyUpdate(Boolean(d.hasUpdate) && !d.error); else if (kind === "done" || kind === "switched" || kind === "channel") setCanApplyUpdate(false); const version = d.localVersion || d.localSha || "—"; const remote = d.remoteVersion || d.remoteSha || "—"; let text = tr("update.latest", { version }); if (d.hasUpdate) text = tr("update.available", { remote, local: version }); if (d.pinned && d.hasUpdate) text = tr("update.pinned", { version, remote }); if (kind === "switched" || kind === "channel") text = tr("update.switched", { version: d.localVersion || d.remoteVersion || "—" }); if (kind === "done") text = tr("update.done", { version: d.localVersion || d.remoteVersion || "—" }); if (d.error) text = d.error; setUpdateNotice({ kind: d.ok === false || d.error ? "error" : "ok", text }); }, []); const checkUpdate = useCallback((id) => { setUpdateBusy(true); setUpdateJob({ id: id || "", kind: "check" }); const tr = tRef.current; setUpdateNotice({ kind: "ok", text: tr("update.checking") }); apiJson("/dsh-purge/update") .then((d) => { if (!d || (!d.ok && !d.channel)) throw new Error((d && d.error) || "check failed"); applyUpdateInfo(d, tr, "check"); }) .catch((e) => setUpdateNotice({ kind: "error", text: updateErrorText(tr, e) })) .finally(() => { setUpdateBusy(false); setUpdateJob(null); }); }, [applyUpdateInfo]); const postUpdate = useCallback((body, kind, id) => { setUpdateBusy(true); setUpdateJob({ id: id || "", kind }); const tr = tRef.current; setUpdateNotice({ kind: "ok", text: kind === "switched" || kind === "channel" ? tr("update.switching") : tr("update.applying") }); return apiJson("/dsh-purge/update", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body || {}), }) .then((d) => { if (!d || (d.ok === false && d.error)) throw new Error((d && d.error) || "update failed"); applyUpdateInfo(d, tr, kind); if (d.needsFullQuit) { suppressRestartAsk.current = false; setAskRestart(true); setUpdateNotice({ kind: "ok", text: tr("restart.official.body") }); return d; } if (d.restartAfter) { setUpdateNotice({ kind: "ok", text: tr("update.restarting") }); return d; } if (d.applied && d.reloadClient !== false) { setUpdateNotice({ kind: "ok", text: tr("update.reloading") }); setTimeout(() => { try { window.location.reload(); } catch { /* ignore */ } }, 280); } else if (d.needRestart) { setAskRestart(true); } return d; }) .catch((e) => { setUpdateNotice({ kind: "error", text: updateErrorText(tr, e) }); throw e; }) .finally(() => { setUpdateBusy(false); setUpdateJob(null); }); }, [applyUpdateInfo]); const doUpdate = useCallback((id) => { const lane = id === "beta" ? "beta" : "stable"; postUpdate({ op: "apply", ref: lane === "beta" ? "beta" : "master", channel: lane }, "done", id).catch(() => {}); }, [postUpdate]); const changeChannel = useCallback((next) => { if (!next) return; const tr = tRef.current; if (!window.confirm(tr("update.confirmChannel", { version: tr("channel." + next) }))) return; const prevChannel = channel; const prevInfo = updateInfo; setChannel(next); setUpdateInfo((cur) => Object.assign({}, cur || {}, { channel: next })); postUpdate({ op: "channel", channel: next }, "channel", next).catch(() => { setChannel(prevChannel); setUpdateInfo(prevInfo); }); }, [channel, postUpdate, updateInfo]); const switchSelected = useCallback((id) => { const tr = tRef.current; const ref = pick[id]; const versions = ((updateInfo && updateInfo.versions) || []).filter((item) => item.channel === id); const hit = versions.find((item) => item.ref === ref); const label = (hit && (hit.label || hit.version)) || ref; if (!ref) return; if (!window.confirm(tr("update.confirmSwitch", { version: label }))) return; const prevChannel = channel; const prevInfo = updateInfo; if (hit && hit.channel) { setChannel(hit.channel); setUpdateInfo((cur) => Object.assign({}, cur || {}, { channel: hit.channel })); } postUpdate({ op: "switch", ref }, "switched", id).catch(() => { setChannel(prevChannel); setUpdateInfo(prevInfo); }); }, [channel, pick, postUpdate, updateInfo]); const loadAll = useCallback(() => { const tr = tRef.current; apiJson("/dsh-purge/status") .then((d) => { if (d && d.ok) { setState(d); if (d.channel) setChannel(d.channel); if (d.update && d.update.ok && !d.update.error) setUpdateInfo(d.update); if (d.boot_full_quit && !suppressRestartAsk.current) setAskRestart(true); } else { setState({ ok: false, patches_total: 0, patches_applied: 0, patch_status: {}, shim_cmd: "n/a", shim_ps1: "n/a", shim_bin: "n/a", has_backup: false }); setNotice({ kind: "error", text: tr("err.status", { error: (d && d.error) || "bad response" }) }); } }) .catch((e) => { setState({ ok: false, patches_total: 0, patches_applied: 0, patch_status: {}, shim_cmd: "n/a", shim_ps1: "n/a", shim_bin: "n/a", has_backup: false }); setNotice({ kind: "error", text: tr("err.status", { error: e.message }) }); }); apiJson("/dsh-purge/update") .then((d) => { if (!d) return; if (d.channel) setChannel(d.channel); setUpdateInfo(d); syncPicks(d); }) .catch(() => {}); apiJson("/dsh-purge/override") .then((d) => { if (d && d.ok) { const packed = typeof d.defaultContent === "string" ? d.defaultContent : ""; const shown = typeof d.content === "string" ? d.content : packed; if (packed) setDefaultOverride(packed); if (!promptSeededRef.current) { promptSeededRef.current = true; setOverride(shown || packed); } setOverrideLoaded(true); } else setNotice({ kind: "error", text: tr("err.override", { error: (d && d.error) || "" }) }); }) .catch((e) => setNotice({ kind: "error", text: tr("err.override", { error: e.message }) })); apiJson("/dsh-purge/own-servers") .then((d) => { if (d && d.ok) { setOwnServers(typeof d.content === "string" ? d.content : ""); setOwnLoaded(true); } }) .catch(() => {}); }, []); useEffect(() => { loadAll(); const timer = setTimeout(() => { loadAll(); }, 5000); return () => clearTimeout(timer); }, [loadAll]); const rejectNeedPrompt = useCallback((tr) => { setAskRestart(false); setNotice({ kind: "error", text: tr("need.prompt") }); setTimeout(() => window.alert(tr("need.prompt")), 0); }, []); const doAction = useCallback((action, actionKey) => { const tr = tRef.current; const label = tr(actionKey); const ticket = ++actionTicket.current; const run = () => { setPatchBusy(true); setAskRestart(false); setNotice({ kind: "idle", text: "" }); const applyContent = action === "apply" ? readPromptBox() : ""; /* 带上当前修订号:两个窗口同时开着,后点的那个会被服务端 409 挡住, * 而不是静默覆盖先点的结果。rev 来自 /status 与上一次动作的响应。 */ const body = action === "apply" ? { content: applyContent, rev: stateRef.current?.rev } : { rev: stateRef.current?.rev }; fetch("/dsh-purge/" + action, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }) .then((r) => r.json().then((d) => ({ status: r.status, d }))) .then(({ status, d }) => { if (ticket !== actionTicket.current) return; if (status === 409 && d && d.stale) { /* 过期写入:说明别的窗口已经动过。提示后自动重新拉一次状态, * 让界面回到真实值,而不是留着旧数字让用户重复点击。 */ setNotice({ kind: "error", text: tr("err.stale", { rev: String(d.rev ?? "?"), last: String(d.last?.action || "?") }) }); if (typeof d.rev === "number") setState((prev) => ({ ...(prev || {}), rev: d.rev })); loadAll(); return; } if (responseNeedsPrompt(d)) { rejectNeedPrompt(tr); return; } if (!d.ok) { setNotice({ kind: "error", text: tr("err.action", { action: label, error: d.error || "" }) }); return; } if (typeof d.rev === "number") setState((prev) => ({ ...(prev || {}), rev: d.rev })); if (action === "apply") { if (typeof d.defaultContent === "string") setDefaultOverride(d.defaultContent); if (typeof d.override_content === "string") { setOverride(d.override_content); setOverrideLoaded(true); } } if (action === "apply" && d.complete === false) { const parts = []; if (d.failed > 0) { const ids = (d.failed_items || []) .map((x) => x.id) .filter((id) => id != null) .join(","); parts.push( tr("restart.incomplete.patches", { failed: String(d.failed || 0), ids: ids || "?", }), ); } if (d.cmd_flash && d.cmd_flash.ok === false) { parts.push( tr("restart.incomplete.flash", { entry: String(d.cmd_flash.entry || "fail"), }), ); } if (parts.length === 0) parts.push(tr("restart.incomplete.flash", { entry: "unknown" })); setNotice({ kind: "error", text: tr("restart.incomplete", { detail: parts.join(";") }), }); loadAll(); if (d.restarting || d.needs_full_quit) { suppressRestartAsk.current = false; setAskRestart(true); } return; } if (action === "apply") { setNotice({ kind: "ok", text: tr("ok.done") }); loadAll(); if (d.restarting || d.needs_full_quit) { suppressRestartAsk.current = false; setAskRestart(true); } else { setAskRestart(false); } return; } setNotice({ kind: "ok", text: tr("ok.done") }); loadAll(); }) .catch((e) => { if (ticket !== actionTicket.current) return; setNotice({ kind: "error", text: tr("err.action", { action: label, error: e.message }) }); }) .finally(() => { if (ticket === actionTicket.current) setPatchBusy(false); }); }; run(); }, [loadAll, rejectNeedPrompt, state]); const saveOverride = useCallback(() => { const tr = t; const ticket = ++actionTicket.current; const go = () => { setPatchBusy(true); setAskRestart(false); setNotice({ kind: "idle", text: "" }); const content = readPromptBox(); fetch("/dsh-purge/override", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: content }), }) .then((r) => r.json()) .then((d) => { if (ticket !== actionTicket.current) return; if (!d.ok || responseNeedsPrompt(d)) { rejectNeedPrompt(tr); return; } if (typeof d.content === "string") setOverride(d.content); setNotice({ kind: "ok", text: t("saved.override") }); }) .catch((e) => { if (ticket !== actionTicket.current) return; setNotice({ kind: "error", text: t("err.save", { error: e.message }) }); }) .finally(() => { if (ticket === actionTicket.current) setPatchBusy(false); }); }; bothInjectEmpty(readPromptBox(), defaultOverrideRef.current).then((empty) => { if (ticket !== actionTicket.current) return; if (empty) { rejectNeedPrompt(tr); return; } go(); }); }, [t, rejectNeedPrompt]); const restoreOverride = useCallback(() => { if (!defaultOverride) return; const tr = t; const ticket = ++actionTicket.current; overrideRef.current = defaultOverride; setOverride(defaultOverride); setPatchBusy(true); setNotice({ kind: "idle", text: "" }); apiJson("/dsh-purge/override", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: defaultOverride }), }) .then((d) => { if (ticket !== actionTicket.current) return; if (!d || !d.ok) { setNotice({ kind: "error", text: tr("err.save", { error: (d && d.error) || "" }) }); return; } if (typeof d.content === "string") { overrideRef.current = d.content; setOverride(d.content); } setNotice({ kind: "ok", text: tr("saved.restoreInject") }); }) .catch((e) => { if (ticket !== actionTicket.current) return; setNotice({ kind: "error", text: tr("err.save", { error: e.message }) }); }) .finally(() => { if (ticket === actionTicket.current) setPatchBusy(false); }); }, [defaultOverride, t]); const waitHostAfterUninstall = useCallback((tr) => { const started = Date.now(); const ping = () => { fetch("/", { cache: "no-store", credentials: "same-origin" }) .then((r) => { if (r.ok) { try { window.localStorage.removeItem("dshp-theme"); window.localStorage.removeItem("dshp-theme-mode"); } catch { /* ignore */ } window.location.reload(); return; } retry(); }) .catch(retry); }; const retry = () => { if (Date.now() - started > 90000) { setNotice({ kind: "error", text: tr("restart.timeout") }); setUninstallBusy(false); return; } setTimeout(ping, 500); }; setTimeout(ping, 800); }, []); const doUninstall = useCallback(() => { setUninstallBusy(true); setAskUninstall(false); setAskRestart(false); const tr = tRef.current; setNotice({ kind: "ok", text: tr("uninstall.applying") }); apiJson("/dsh-purge/uninstall", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }) .then((d) => { if (!d || !d.ok) throw new Error((d && d.error) || "uninstall failed"); setNotice({ kind: "ok", text: (d && d.note) || hostText(tr, "uninstall.done", d.surface || hostSurfaceOf(state)) }); if ((d && d.surface) === "desktop" || (d && d.fullApp) || hostSurfaceOf(state) === "desktop") return; waitHostAfterUninstall(tr); }) .catch((e) => { const msg = String((e && e.message) || e || ""); if (/failed to fetch|networkerror|load failed/i.test(msg)) { setNotice({ kind: "ok", text: hostText(tr, "uninstall.done", hostSurfaceOf(state)) }); if (hostSurfaceOf(state) === "desktop" || clientGuessSurface() === "desktop") return; waitHostAfterUninstall(tr); return; } setNotice({ kind: "error", text: tr("uninstall.fail", { error: msg }) }); setUninstallBusy(false); }); }, [state, waitHostAfterUninstall]); const s = state; const located = !!(s && s.ai_base); const hostSurface = hostSurfaceOf(s); const desktopLikely = hostSurface === "desktop" || clientGuessSurface() === "desktop"; const sealedReady = !located && desktopLikely && !!(s && (s.asar_still_sealed || s.desktop_exe || s.desktop_install)); const patchesLost = !located && !!(s && s.patches_lost); const tally = located ? tallyPatches(patchRowsOf(s)) : { applied: 0, pending: 0, unmatched: 0, skipped: 0, total: 0 }; const total = tally.total; const applied = tally.applied; const pct = located && total ? Math.round((applied / total) * 100) : 0; const metricNotes = []; if (tally.pending) metricNotes.push(t("metric.pendingShort", { n: tally.pending })); if (tally.unmatched) metricNotes.push(t("metric.unmatchedShort", { n: tally.unmatched })); if (tally.skipped) metricNotes.push(t("metric.skipShort", { n: tally.skipped })); const versions = (updateInfo && updateInfo.versions) || []; const channelNow = (updateInfo && updateInfo.channel) || channel || "stable"; const localVer = (s && s.plugin_version) || (updateInfo && updateInfo.localVersion) || ""; const renderEdition = (id) => { const lane = (updateInfo && updateInfo.lanes && updateInfo.lanes[id]) || {}; const ceiling = listedCeiling(versions, localVer || (updateInfo && updateInfo.localVersion)); const list = versions.filter((item) => item.channel === id && keepListedVersion(item, ceiling)); const selectedRef = pick[id] || ""; const hit = list.find((item) => item.ref === selectedRef) || preferListed(list, localVer, updateInfo && updateInfo.pin); const onLane = channelNow === id; const noBeta = id === "beta" && Array.isArray(updateInfo && updateInfo.versions) && !list.length; const pickedOther = Boolean(hit && selectedRef && !hit.current); const thisJob = updateJob && updateJob.id === id ? updateJob : null; let actionLabel = t("btn.checkUpdate"); let actionKind; let actionClick = () => checkUpdate(id); let actionDisabled = updateBusy || Boolean(noBeta); if (noBeta) { actionLabel = t("update.noBeta"); actionClick = () => {}; } else if (pickedOther) { actionLabel = t("update.switch"); actionClick = () => switchSelected(id); } else if (!onLane) { actionLabel = id === "beta" ? t("channel.useBeta") : t("channel.useStable"); actionClick = () => changeChannel(id); } else if (canApplyUpdate && lane.hasUpdate) { actionLabel = t("btn.doUpdate"); actionClick = () => doUpdate(id); } if (thisJob) { if (thisJob.kind === "check") actionLabel = t("btn.checkUpdate.busy"); else if (thisJob.kind === "done") actionLabel = t("btn.doUpdate.busy"); else actionLabel = t("update.switching"); } return h("div", { className: "dshp-edition" + (onLane ? " is-on" : ""), "aria-label": t("channel." + id), }, h("span", { className: "dshp-edition-name" }, t("channel." + id), onLane ? h("em", { className: "dshp-now-mark" }, t("update.current")) : null, ), h("select", { className: "dshp-field dshp-ver", value: list.length ? ((hit && hit.ref) || "") : "", disabled: updateBusy || !list.length, onChange: (e) => setPick((prev) => Object.assign({}, prev, { [id]: e.target.value })), "aria-label": t("update.pick"), }, list.length ? list.map((item) => h("option", { key: item.ref + (item.sha || ""), value: item.ref, }, versionChoiceLabel(item, t))) : h("option", { value: "" }, noBeta ? t("update.noBeta") : t("update.pick")), ), h(Btn, { tiny: true, kind: actionKind, disabled: actionDisabled, onClick: actionClick }, actionLabel), ); }; return h(react.Fragment, null, h("section", { className: "dshp-panel", "aria-label": t("purge.title") }, h("div", { className: "dshp-head" }, h("div", null, h("h3", { className: "dshp-title" }, t("purge.title")), h("p", { className: "dshp-panel-lead" }, t("purge.lead")), ), h("span", { className: "dshp-pill" }, t("surface." + hostSurface)), ), h("div", { className: "dshp-rel" }, h("div", { className: "dshp-now" }, h("span", { className: "dshp-now-label" }, t("channel.local")), h("b", { className: "dshp-now-ver" }, localVer ? "v" + localVer : "—"), h("em", { className: "dshp-now-mark" }, t("update.current")), ), h("div", { className: "dshp-editions" }, renderEdition("stable"), renderEdition("beta"), ), noticeNode(updateNotice), ), s ? h("div", { className: "dshp-metrics" }, h("div", { className: "dshp-metric" }, h("b", null, located ? (applied + " / " + total) : (patchesLost ? t("metric.lost.count") : (sealedReady ? t("metric.sealed.count") : t("metric.unlocated.count")))), h("span", null, !located ? (patchesLost ? t("metric.lost") : (sealedReady ? t("metric.sealed") : t("metric.unlocated"))) : (metricNotes.length ? metricNotes.join(" · ") : t("metric.purged"))), ), h("div", { className: "dshp-metric" }, h("b", { style: { fontSize: 13, fontFamily: "var(--dshp-mono)", fontWeight: 500 } }, h("span", { className: "dshp-pill is-" + shimKind(s.shim_cmd) }, "cmd"), " ", h("span", { className: "dshp-pill is-" + shimKind(s.shim_ps1) }, "ps1"), " ", h("span", { className: "dshp-pill is-" + shimKind(s.shim_bin || "missing") }, "unix"), ), h("span", null, (s.shim_cmd === "n/a" && s.shim_ps1 === "n/a" && (s.shim_bin === "n/a" || !s.shim_bin)) ? t("metric.shim.na") : t("metric.shim")), ), h("div", { className: "dshp-metric" }, h("b", null, s.has_backup ? t("metric.bak.yes") : t("metric.bak.no")), h("span", null, t("metric.bak.hint")), ), ) : h("div", { className: "dshp-metrics" }, h("div", { className: "dshp-metric" }, h("div", { className: "dshp-skel" }), h("div", { className: "dshp-skel", style: { width: "40%" } })), h("div", { className: "dshp-metric" }, h("div", { className: "dshp-skel" }), h("div", { className: "dshp-skel", style: { width: "40%" } })), h("div", { className: "dshp-metric" }, h("div", { className: "dshp-skel" }), h("div", { className: "dshp-skel", style: { width: "40%" } })), ), h("div", { className: "dshp-bar", "aria-hidden": "true" }, h("i", { style: { width: pct + "%" } })), h(PatchGroups, { state: s }), h(HooksDenySection, { onSaved: loadAll }), h(GateSection, { t }), h("div", { className: "dshp-row", style: { marginTop: 2 } }, h(Btn, { kind: "primary", disabled: patchBusy || uninstallBusy || !overrideLoaded, onClick: () => doAction("apply", "action.apply") }, patchBusy ? t("btn.apply.busy") : t("btn.apply")), h(Btn, { kind: "ghost", disabled: patchBusy || uninstallBusy, onClick: () => doAction("revert", "action.revert") }, t("btn.revert")), h(Btn, { kind: "danger", disabled: patchBusy || uninstallBusy, onClick: () => setAskUninstall(true) }, uninstallBusy ? t("btn.uninstall.busy") : t("btn.uninstall")), noticeNode(notice), ), h("p", { className: "dshp-block-hint" }, t("apply.hint")), s && !s.ai_base ? h("p", { className: "dshp-block-hint", style: { color: "var(--dshp-bad)" } }, hostText(t, "warn.noRoot", hostSurface)) : null, s && s.ai_base && s.override_status === "missing" ? h("p", { className: "dshp-block-hint" }, t("warn.noInject")) : null, askRestart ? h(RestartAsk, { t: t, theme: detectHostTheme(), onLater: () => { suppressRestartAsk.current = true; setAskRestart(false); }, onRestart: () => { suppressRestartAsk.current = true; setAskRestart(false); restartDsh(setNotice, function () {}, t, hostSurface); }, }) : null, ), h("section", { className: "dshp-panel", "aria-label": t("override.title") }, h("div", { className: "dshp-head" }, h("div", null, h("h3", { className: "dshp-title" }, t("override.title")), h("p", { className: "dshp-panel-lead" }, t("override.lead")), ), h("div", { className: "dshp-row", style: { margin: 0 } }, h(Btn, { kind: "primary", tiny: true, disabled: patchBusy || !overrideLoaded, onClick: saveOverride }, t("btn.saveInject")), h(Btn, { tiny: true, disabled: patchBusy || !overrideLoaded || !defaultOverride, onClick: restoreOverride }, t("btn.restoreInject")), ), ), h("textarea", { className: "dshp-area", ref: promptAreaRef, value: override, onChange: (e) => { overrideRef.current = e.target.value; setOverride(e.target.value); }, spellCheck: false, placeholder: "", }), promptBoxEmpty(override) ? h("p", { className: "dshp-block-hint", style: { color: "var(--dshp-bad)" }, }, t("need.prompt")) : null, ), h("section", { className: "dshp-panel", "aria-label": t("own.title") }, h("div", { className: "dshp-head" }, h("div", null, h("h3", { className: "dshp-title" }, t("own.title")), h("p", { className: "dshp-panel-lead" }, t("own.hint")), ), h(Btn, { kind: "primary", tiny: true, disabled: !ownLoaded, onClick: () => { fetch("/dsh-purge/own-servers", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: ownServers }), }).then((r) => r.json()).then((d) => { if (!d || !d.ok) { setNotice({ kind: "error", text: (d && d.error) || t("err.override", { error: "" }) }); return; } if (typeof d.content === "string") setOwnServers(d.content); setNotice({ kind: "ok", text: d.dropped > 0 ? t("saved.own.dropped", { n: d.dropped }) : t("saved.own"), }); }).catch((e) => setNotice({ kind: "error", text: e.message })); }, }, t("own.save")), ), h("textarea", { className: "dshp-area", value: ownServers, onChange: (e) => setOwnServers(e.target.value), spellCheck: false, rows: 4, style: { minHeight: 96 }, }), ), askUninstall ? h("div", { className: "dshp-modal-bg", role: "dialog", "aria-modal": "true", "aria-labelledby": "dshp-uninstall-title", onClick: (e) => { if (e.target === e.currentTarget && !uninstallBusy) setAskUninstall(false); }, }, h("div", { className: "dshp-modal" }, h("h4", { id: "dshp-uninstall-title" }, t("uninstall.title")), h("p", null, t("uninstall.body")), h("div", { className: "dshp-modal-ops" }, h(Btn, { disabled: uninstallBusy, onClick: () => setAskUninstall(false) }, t("uninstall.cancel")), h(Btn, { kind: "solid-danger", disabled: uninstallBusy, onClick: doUninstall }, t("uninstall.confirm")), ), ), ) : null, ); } function ruleLabel(r) { if (!r) return ""; if (r.name !== r.id) return r.name + " (" + r.id + ")"; return r.id; } function RulesSection() { const t = useT(); const tRef = useRef(t); tRef.current = t; const [st, setSt] = useState(null); const [editId, setEditId] = useState(null); const [editName, setEditName] = useState(""); const [editTarget, setEditTarget] = useState("AGENTS.md"); const [content, setContent] = useState(""); const [newId, setNewId] = useState(""); const [newName, setNewName] = useState(""); const [newTarget, setNewTarget] = useState("AGENTS.md"); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState({ kind: "idle", text: "" }); const loadStatus = useCallback(() => { const tr = tRef.current; let cwd = ""; try { cwd = clientCurrentCwd(); } catch { /* ignore */ } rulesApi("status", cwd ? { cwd: cwd } : undefined) .then((d) => { if (d && d.ok) setSt(d); else { setSt({ ok: false, rules: [], active: null }); setNotice({ kind: "error", text: tr("err.rules", { error: (d && d.error) || "bad response" }) }); } }) .catch((e) => { setSt({ ok: false, rules: [], active: null }); setNotice({ kind: "error", text: tr("err.rules", { error: e.message }) }); }); }, []); useEffect(() => { loadStatus(); }, [loadStatus]); const doPost = useCallback((action, payload, after) => { setBusy(true); setNotice({ kind: "idle", text: "" }); rulesApi(action, payload) .then((d) => { if (d && d.ok) { setNotice({ kind: "ok", text: t("ok.done") }); if (action === "save" || action === "activate") warnNeedPrompt(d, t); loadStatus(); if (after) after(); } else { warnNeedPrompt(d, t); setNotice({ kind: "error", text: t("err.action", { error: (d && d.error) || "" }) }); } }) .catch((e) => setNotice({ kind: "error", text: t("err.action", { error: e.message }) })) .finally(() => setBusy(false)); }, [loadStatus, t]); const openRule = useCallback((id) => { setBusy(true); rulesApi("read", { id: id }) .then((d) => { if (d.ok) { setEditId(id); setEditName(d.name || id); setEditTarget(d.target || "AGENTS.md"); setContent(typeof d.content === "string" ? d.content : ""); } else setNotice({ kind: "error", text: t("err.read", { error: d.error || "" }) }); }) .catch((e) => setNotice({ kind: "error", text: t("err.read", { error: e.message }) })) .finally(() => setBusy(false)); }, [t]); const clearEditor = () => { setEditId(null); setEditName(""); setEditTarget("AGENTS.md"); setContent(""); }; let list; if (!st) { list = h("div", { className: "dshp-skel", style: { height: 80, margin: 12 } }); } else if (!st.rules || st.rules.length === 0) { list = h("p", { className: "dshp-empty" }, t("rules.empty")); } else { list = st.rules.map((r) => { const isActive = r.id === st.active; const isEdit = r.id === editId; return h("div", { key: r.id, className: "dshp-ruleitem" + (isEdit ? " is-edit" : ""), role: "button", tabIndex: 0, onClick: () => openRule(r.id), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openRule(r.id); } }, }, h("div", { className: "dshp-rule-main" }, h("span", { className: "dshp-rule-name" }, ruleLabel(r), isActive ? h("span", { className: "dshp-pill is-ok" }, t("pill.current")) : null, ), h("span", { className: "dshp-rule-meta" }, r.target + " · " + formatSize(r.size)), ), h("div", { className: "dshp-rule-ops" }, h(Btn, { tiny: true, kind: isActive ? undefined : "primary", disabled: busy || isActive, onClick: (e) => { e.stopPropagation(); if (!isActive) doPost("activate", { id: r.id }); }, }, isActive ? t("btn.inUse") : t("btn.use")), h(Btn, { tiny: true, kind: "danger", disabled: busy, onClick: (e) => { e.stopPropagation(); if (!window.confirm(t("confirm.delete", { id: r.id }))) return; if (editId === r.id) clearEditor(); doPost("delete", { id: r.id }); }, }, t("delete")), ), ); }); } const policyValue = (st && typeof st.workspace_policy === "string") ? st.workspace_policy : "fallback"; const policyOptions = (st && Array.isArray(st.workspace_policies) && st.workspace_policies.length) ? st.workspace_policies : ["fallback", "strict", "always"]; const cwdCandidates = (st && Array.isArray(st.cwd_candidates)) ? st.cwd_candidates : []; const cwdValue = (st && typeof st.cwd === "string") ? st.cwd : ""; return h("section", { className: "dshp-panel", "aria-label": t("rules.title") }, h("div", { className: "dshp-head" }, h("h3", { className: "dshp-title" }, t("rules.title")), h("div", { className: "dshp-row", style: { margin: 0 } }, noticeNode(notice), h(Btn, { tiny: true, kind: "danger", disabled: busy, onClick: () => { if (!window.confirm(t("rules.reset.confirm"))) return; clearEditor(); doPost("reset", {}); }, }, t("rules.reset")), ), ), h("div", { className: "dshp-block" }, h("div", { className: "dshp-policy-row" }, h("label", { htmlFor: "dshp-policy-select" }, t("policy.title")), h("select", { id: "dshp-policy-select", className: "dshp-field", value: policyValue, disabled: busy || !st, onChange: (e) => doPost("set-policy", { policy: e.target.value }), }, policyOptions.map((p) => h("option", { key: p, value: p }, t("policy." + p)))), ), h("p", { className: "dshp-block-hint" }, t("policy.hint")), ), h("div", { className: "dshp-block" }, h("div", { className: "dshp-block-head" }, h("h4", null, t("cwd.title")), h(Btn, { tiny: true, disabled: busy, onClick: () => { let cwd = cwdValue; if (!cwd) { try { cwd = clientCurrentCwd(); } catch { /* ignore */ } } doPost("scan-cwd", cwd ? { cwd: cwd } : {}); }, }, t("cwd.rescan")), ), cwdValue ? h("p", { className: "dshp-cwd-path" }, cwdValue) : null, (!st || !cwdValue) ? h("p", { className: "dshp-empty" }, t("cwd.no.workspace")) : (cwdCandidates.length === 0 ? h("p", { className: "dshp-empty" }, t("cwd.empty")) : h("div", { className: "dshp-cwd-list" }, cwdCandidates.map((item) => h("label", { key: item.path, className: "dshp-cwd-item" + (item.selected ? " is-on" : ""), }, h("input", { type: "checkbox", checked: Boolean(item.selected), disabled: busy, onChange: (e) => doPost("toggle-cwd", { path: item.path, enabled: e.target.checked }), }), h("div", { className: "dshp-cwd-main" }, h("div", { className: "dshp-cwd-name" }, h("span", null, item.name), h("span", { className: "dshp-cwd-badge" }, t("cwd.scope." + (item.scope || "cwd"))), h("span", { className: "dshp-cwd-size" }, formatSize(item.size || 0)), ), h("div", { className: "dshp-cwd-sub" }, item.path), item.preview ? h("div", { className: "dshp-cwd-prev" }, item.preview) : null, ), )))), h("p", { className: "dshp-block-hint" }, t("cwd.hint")), ), h("div", { className: "dshp-block" }, h("div", { className: "dshp-split" }, h("div", { className: "dshp-rulelist" }, h("div", { className: "dshp-rulebody" }, list), h("div", { className: "dshp-create" }, h("div", { className: "dshp-create-row" }, h("input", { className: "dshp-field", placeholder: t("ph.id"), value: newId, onChange: (e) => setNewId(e.target.value) }), h("input", { className: "dshp-field", placeholder: t("ph.alias"), value: newName, onChange: (e) => setNewName(e.target.value) }), h("select", { className: "dshp-field", style: { minWidth: 120, flex: "0 0 auto" }, value: newTarget, onChange: (e) => setNewTarget(e.target.value) }, TARGETS.map((x) => h("option", { key: x, value: x }, x))), h(Btn, { kind: "primary", tiny: true, disabled: busy, onClick: () => { const id = newId.trim(); if (!id) { setNotice({ kind: "error", text: t("need.id") }); return; } const name = newName.trim() || id; const target = newTarget; doPost("save", { id, content: "", name, target }, () => { setEditId(id); setEditName(name); setEditTarget(target); setContent(""); }); setNewId(""); setNewName(""); setNewTarget("AGENTS.md"); }, }, t("btn.create")), ), ), ), h("div", { className: "dshp-editor" }, editId ? [ h("div", { key: "meta", className: "dshp-row" }, h("input", { className: "dshp-field", placeholder: t("ph.alias.short"), value: editName, onChange: (e) => setEditName(e.target.value), }), h("select", { className: "dshp-field", style: { minWidth: 120 }, value: editTarget, onChange: (e) => setEditTarget(e.target.value), }, TARGETS.map((x) => h("option", { key: x, value: x }, x))), ), h("textarea", { key: "body", className: "dshp-area", value: content, onChange: (e) => setContent(e.target.value), placeholder: "", }), h("div", { key: "save", className: "dshp-row" }, h(Btn, { kind: "primary", disabled: busy, onClick: () => doPost("save", { id: editId, content, name: editName, target: editTarget }), }, t("btn.saveRule")), noticeNode(notice), ), ] : h("div", { className: "dshp-editor-empty" }, t("rules.pick")), ), ), ), ); } function skillUiAllowed(d) { if (!d) return false; const ver = String(d.plugin_version || d.localVersion || "").replace(/^v/i, ""); if (/^1\.1\.11(?:-|$)/.test(ver)) return false; const parts = ver.match(/^(\d+)\.(\d+)\.(\d+)/); if (!parts) return false; if (+parts[1] !== 1) return +parts[1] > 1; if (+parts[2] !== 1) return +parts[2] > 1; return +parts[3] >= 12; } function splitListedVersion(raw) { const text = String(raw || "").trim().replace(/^v/i, ""); const cut = text.split("-"); const core = String(cut[0] || "").split(".").map((n) => parseInt(n, 10) || 0); return { core: [core[0] || 0, core[1] || 0, core[2] || 0], pre: cut.slice(1).join("-") }; } function listedVersionNewer(remote, local) { if (!remote || !local || String(remote) === String(local)) return false; const a = splitListedVersion(remote); const b = splitListedVersion(local); for (let i = 0; i < 3; i += 1) { if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i]; } if (a.pre && !b.pre) return false; if (!a.pre && b.pre) return true; return a.pre > b.pre; } function listedCeiling(versions, localVer) { let best = ""; const rows = (versions || []).slice(); const local = String(localVer || "").replace(/^v/i, ""); if (local && !/(?:^|[-._])(beta|rc|pre|preview|test)(?:\d|$|[-._])/i.test(local)) { rows.push({ channel: "stable", version: local }); } for (const item of rows) { if (!item || item.channel === "beta") continue; const ver = String(item.version || "").replace(/^v/i, ""); if (!/^\d+\.\d+\.\d+/.test(ver) || /(?:^|[-._])(beta|rc|pre|preview|test)(?:\d|$|[-._])/i.test(ver)) continue; if (!best || listedVersionNewer(ver, best)) best = ver; } return best; } function keepListedVersion(item, _ceiling) { const ver = String((item && (item.version || item.ref || item.id)) || ""); if (!item) return false; if (item.ref === "beta" || item.id === "beta") return true; if (item.channel === "beta") return false; if (/(?:^|[-._])(beta|rc|pre|preview|test)(?:\d|$|[-._])/i.test(ver)) return false; return true; } function preferListed(list, localVer, pin) { const pinned = pin ? (list || []).find((item) => item.ref === pin) : null; if (pinned) return pinned; const ver = String(localVer || "").replace(/^v/i, ""); const same = (list || []).filter((item) => ver && String(item.version || "").replace(/^v/i, "") === ver); if (same.length) { return same.find((item) => item.current) || same.find((item) => item.latest) || same[0]; } return (list || []).find((item) => item.current) || (list || []).find((item) => item.latest) || (list || [])[0] || null; } function versionChoiceLabel(item, tr) { const ver = String((item && item.version) || "").replace(/^v/i, ""); const base = ver ? ("v" + ver) : ((item && (item.label || item.ref)) || "—"); if (item && item.current) return base + " · " + tr("update.current"); if (item && item.latest) return base + " · " + tr("update.tip"); return base; } function SkillsSection() { const t = useT(); const tRef = useRef(t); tRef.current = t; const [onBeta, setOnBeta] = useState(false); const [st, setSt] = useState(null); const [editId, setEditId] = useState(null); const [editDesc, setEditDesc] = useState(""); const [content, setContent] = useState(""); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState({ kind: "idle", text: "" }); const zipRef = useRef(null); const folderRef = useRef(null); const loadStatus = useCallback(() => { const tr = tRef.current; const fail = (e) => { setSt({ ok: false, skills: [] }); setNotice({ kind: "error", text: skillsRouteMissing(e) ? tr("err.skills.needRestart") : tr("err.skills", { error: (e && e.message) || e || "bad response" }), }); }; const apply = (d) => { if (d && d.ok) setSt({ ok: true, skills: d.skills || [] }); else fail({ message: (d && d.error) || "bad response" }); }; apiJson("/dsh-purge/status") .then((d) => { const allowed = skillUiAllowed(d); setOnBeta(allowed); if (!allowed) { setSt(null); return; } if (d.skills) { apply({ ok: true, skills: d.skills }); return; } skillsApi("status").then(apply).catch(fail); }) .catch(() => { setOnBeta(false); setSt(null); }); }, []); useEffect(() => { loadStatus(); }, [loadStatus]); const doPost = useCallback((action, payload, after) => { setBusy(true); setNotice({ kind: "idle", text: "" }); skillsApi(action, payload) .then((d) => { if (d && d.ok) { setNotice({ kind: "ok", text: t("ok.done") }); loadStatus(); if (after) after(d); } else { setNotice({ kind: "error", text: t("err.action", { error: (d && d.error) || "" }) }); } }) .catch((e) => setNotice({ kind: "error", text: skillsRouteMissing(e) ? t("err.skills.needRestart") : t("err.action", { error: e.message }), })) .finally(() => setBusy(false)); }, [loadStatus, t]); const openSkill = useCallback((id) => { setBusy(true); skillsApi("read", { id: id }) .then((d) => { if (d.ok) { setEditId(id); setEditDesc(d.description || ""); setContent(typeof d.content === "string" ? d.content : ""); } else setNotice({ kind: "error", text: t("err.read", { error: d.error || "" }) }); }) .catch((e) => setNotice({ kind: "error", text: t("err.read", { error: e.message }) })) .finally(() => setBusy(false)); }, [t]); const clearEditor = () => { setEditId(null); setEditDesc(""); setContent(""); }; const readAsBase64 = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const text = String(reader.result || ""); const at = text.indexOf(","); resolve(at >= 0 ? text.slice(at + 1) : text); }; reader.onerror = () => reject(new Error(t("need.import"))); reader.readAsDataURL(file); }); const importZip = (file) => { if (!file) { setNotice({ kind: "error", text: t("need.import") }); return; } setBusy(true); setNotice({ kind: "ok", text: t("skills.importing") }); readAsBase64(file) .then((data) => skillsApi("import", { data, name: file.name })) .then((d) => { if (!d || !d.ok) throw new Error((d && d.error) || ""); setNotice({ kind: "ok", text: t("saved.import", { count: (d.imported || []).length || 1 }) }); loadStatus(); }) .catch((e) => setNotice({ kind: "error", text: skillsRouteMissing(e) ? t("err.skills.needRestart") : t("err.action", { error: e.message }), })) .finally(() => setBusy(false)); }; const keepFolderFile = (file) => { const rel = String(file.webkitRelativePath || file.name || "").replace(/\\/g, "/"); if (!rel || rel.includes("..")) return false; if (/(^|\/)(node_modules|\.git|__MACOSX|\.system)(\/|$)/i.test(rel)) return false; if (/(^|\/)\._/.test(rel) || /\/\.DS_Store$/i.test(rel)) return false; return true; }; const importFolder = (list) => { const files = Array.from(list || []).filter(keepFolderFile); if (!files.length) { setNotice({ kind: "error", text: t("err.import.folder") }); return; } setBusy(true); setNotice({ kind: "ok", text: t("skills.importing") }); Promise.all(files.map((file) => readAsBase64(file).then((content) => ({ path: file.webkitRelativePath || file.name, content, encoding: "base64", })))) .then((payload) => skillsApi("import", { files: payload, name: String(payload[0] && payload[0].path || "").split("/")[0], })) .then((d) => { if (!d || !d.ok) throw new Error((d && d.error) || ""); setNotice({ kind: "ok", text: t("saved.import", { count: (d.imported || []).length || 1 }) }); loadStatus(); }) .catch((e) => setNotice({ kind: "error", text: skillsRouteMissing(e) ? t("err.skills.needRestart") : t("err.action", { error: e.message }), })) .finally(() => setBusy(false)); }; if (!onBeta) return null; let list; if (!st) { list = h("div", { className: "dshp-skel", style: { height: 80, margin: 12 } }); } else if (!st.skills || st.skills.length === 0) { list = h("p", { className: "dshp-empty" }, t("skills.empty")); } else { list = st.skills.map((item) => { const isEdit = item.id === editId; return h("div", { key: item.id, className: "dshp-ruleitem" + (isEdit ? " is-edit" : ""), role: "button", tabIndex: 0, onClick: () => openSkill(item.id), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openSkill(item.id); } }, }, h("div", { className: "dshp-rule-main" }, h("span", { className: "dshp-rule-name" }, item.id), h("span", { className: "dshp-rule-meta" }, (item.valid ? item.description : t("skills.invalid")) + " · " + formatSize(item.size), ), item.valid ? h("span", { className: "dshp-rule-meta" }, [ item.userInvocable ? t("skills.call.slash", { cmd: "/" + item.id }) : t("skills.call.userOff"), item.modelInvocable ? t("skills.call.model") : t("skills.call.modelOff"), ].join(" · "), ) : null, ), h("div", { className: "dshp-rule-ops" }, h(Btn, { tiny: true, kind: "danger", disabled: busy, onClick: (e) => { e.stopPropagation(); if (!window.confirm(t("confirm.delete.skill", { id: item.id }))) return; if (editId === item.id) clearEditor(); doPost("delete", { id: item.id }); }, }, t("delete")), ), ); }); } return h("section", { className: "dshp-panel", "aria-label": t("skills.title") }, h("div", { className: "dshp-head" }, h("h3", { className: "dshp-title" }, t("skills.title")), h("div", { className: "dshp-row", style: { margin: 0 } }, noticeNode(notice)), ), h("p", { className: "dshp-hint", style: { margin: "0 0 10px", color: "var(--dshp-mute)", fontSize: 12 } }, hostText(t, "skills.hint", hostSurfaceOf())), h("div", { className: "dshp-split" }, h("div", { className: "dshp-rulelist" }, h("div", { className: "dshp-rulebody" }, list), h("div", { className: "dshp-create" }, h("div", { className: "dshp-create-row" }, h("input", { ref: zipRef, type: "file", accept: ".zip,.tgz,.tar.gz,.tar,.skill", style: { display: "none" }, onChange: (e) => { const file = e.target.files && e.target.files[0]; e.target.value = ""; importZip(file); }, }), h("input", { ref: (el) => { folderRef.current = el; if (!el) return; el.setAttribute("webkitdirectory", ""); el.setAttribute("directory", ""); el.multiple = true; }, type: "file", multiple: true, style: { display: "none" }, onChange: (e) => { const files = Array.from((e.target && e.target.files) || []); e.target.value = ""; importFolder(files); }, }), h(Btn, { kind: "primary", tiny: true, disabled: busy, onClick: () => zipRef.current && zipRef.current.click(), }, t("btn.importZip")), h(Btn, { tiny: true, disabled: busy, onClick: () => folderRef.current && folderRef.current.click(), }, t("btn.importFolder")), ), ), ), h("div", { className: "dshp-editor" }, editId ? [ h("div", { key: "meta", className: "dshp-row" }, h("input", { className: "dshp-field", placeholder: t("ph.skill.desc"), value: editDesc, onChange: (e) => setEditDesc(e.target.value), }), ), h("textarea", { key: "body", className: "dshp-area", value: content, onChange: (e) => setContent(e.target.value), spellCheck: false, placeholder: "", }), h("div", { key: "save", className: "dshp-row" }, h(Btn, { kind: "primary", disabled: busy, onClick: () => doPost("save", { id: editId, content, description: editDesc }), }, t("btn.saveSkill")), noticeNode(notice), ), ] : h("div", { className: "dshp-editor-empty" }, t("skills.pick")), ), ), ); } function reopenAfterRestart() { window.location.reload(); } function waitForRestart(setNotice, setBusy, t) { const started = Date.now(); // 必须先看到旧进程掉线,再等新进程就绪;否则同进程立刻 200 会被当成「重启成功」又弹重启。 let sawDown = false; const ping = () => { fetch("/dsh-purge/status", { cache: "no-store", credentials: "same-origin" }) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((d) => { if (!sawDown) { retry(); return; } if (d && d.ok && d.ready !== false) reopenAfterRestart(); else retry(); }) .catch(() => { sawDown = true; retry(); }); }; const retry = () => { if (Date.now() - started > 90000) { setNotice({ kind: "error", text: t("restart.timeout") }); setBusy(false); return; } setTimeout(ping, 500); }; setTimeout(ping, 800); } let restartOnce = false; function restartDsh(setNotice, setBusy, t, surface) { if (restartOnce) return; const surf = surface || clientGuessSurface() || "desktop"; const desktop = surf === "desktop" || clientGuessSurface() === "desktop"; restartOnce = true; setBusy(true); setNotice({ kind: "ok", text: hostText(t, "restarting", surf) }); const stop = (error) => { restartOnce = false; setNotice({ kind: "error", text: error ? t("restart.fail", { error }) : t("restart.notStarted") }); setBusy(false); }; let sawDrop = false; const watch = setTimeout(() => { if (sawDrop) return; stop(""); }, 25000); fetch("/dsh-purge/restart", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }) .then((r) => r.json()) .then((d) => { if (!d.ok) throw new Error(d.error || "restart failed"); const next = (d && d.surface) || surf; const full = next === "desktop" || (d && d.fullApp) || desktop; if (full && d.already) { setNotice({ kind: "ok", text: hostText(t, "restarting", next) }); return; } if (full && d.restarting === false) { clearTimeout(watch); stop(d.error || "not started"); return; } setNotice({ kind: "ok", text: hostText(t, "restarting", next) }); if (full) return; clearTimeout(watch); restartOnce = false; waitForRestart(setNotice, setBusy, t); }) .catch((e) => { const msg = String((e && e.message) || e || ""); const dropped = /failed to fetch|networkerror|load failed/i.test(msg) || e.name === "TypeError"; if (dropped && desktop) { sawDrop = true; clearTimeout(watch); return; } clearTimeout(watch); stop(msg || "restart failed"); }); } function detectHostTheme() { try { if (typeof document !== "undefined") { const root = document.documentElement; if (document.body?.hasAttribute("data-ds-dark-theme") || root?.hasAttribute("data-ds-dark-theme")) return "dusk"; const scheme = (root.style.colorScheme || getComputedStyle(root).colorScheme || getComputedStyle(root).getPropertyValue("color-scheme") || "").toLowerCase(); if (scheme.includes("dark")) return "dusk"; if (scheme.includes("light")) return "white"; } if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches) { return "dusk"; } } catch { /* ignore */ } return "white"; } function readTheme() { return detectHostTheme(); } /* ── 设置页外壳 ─────────────────────────────────────────────────────────── * 1.3.0 起主入口搬到「设置 → DeepSeek Harness 破甲」: * · 页面用官方 settings.section 契约注册,描述符只有 id/order/label; * 官方设置页会按 section id 画图标,未知 id 落到齿轮,所以这里不传 icon; * · 页面内部用分段控件(破甲 / 规则 / 技能),不再横向摊三块; * · 顶部只留一行说明 + 分段,避免和设置外壳的双层标题打架。 * 右侧浮动 dock 仍然保留,作为对话里随手打开的快捷面板。 * ──────────────────────────────────────────────────────────────────────── */ const PAGE_SEGMENTS = [ { id: "armor", key: "page.seg.armor" }, { id: "rules", key: "page.seg.rules" }, { id: "skills", key: "page.seg.skills" }, { id: "drill", key: "page.seg.drill" }, ]; /* 设置导航行的图标。 * `settings.section` 契约只投影 id / order / label,没有 icon 字段;外壳按 section id * 决定字形,未知 id 一律画齿轮(和「通用设置」撞脸)。所以只能按 label 认出自己那一行、 * 打上标记,再用 CSS 把外壳的 svg 藏掉、把下面这张 mask 图盖上去。 * 图形是「开了口的锁」:一是不和任何官方图标重复,二是 16px 下轮廓仍然清楚, * 三是不用颜色也能读懂(mask 只吃 alpha,本来就没法做双色)。 */ const NAV_ICON_MARKER = "data-dsh-purge-nav-icon"; const NAV_ICON_SVG = '' + '' + '' + ""; const NAV_ICON_MASK_URL = "url(\"data:image/svg+xml;utf8," + encodeURIComponent(NAV_ICON_SVG) + "\")"; const NAV_ROW_SELECTOR = "[role=\"dialog\"] nav button"; function installSettingsNavIcon(ctx, resolveLabel) { if (typeof document === "undefined" || !ctx || typeof ctx.effect !== "function") return; ctx.effect(() => { const tag = document.createElement("style"); tag.dataset.plugin = "dsh-purge"; tag.dataset.pluginCss = "dsh-purge/settings-nav-icon"; tag.textContent = [ "[" + NAV_ICON_MARKER + "] > svg { display: none; }", "[" + NAV_ICON_MARKER + "]::before {", " content: '';", " flex: none;", " width: 16px;", " height: 16px;", " background-color: currentColor;", " -webkit-mask-image: " + NAV_ICON_MASK_URL + ";", " mask-image: " + NAV_ICON_MASK_URL + ";", " -webkit-mask-repeat: no-repeat;", " mask-repeat: no-repeat;", " -webkit-mask-position: center;", " mask-position: center;", " -webkit-mask-size: 16px 16px;", " mask-size: 16px 16px;", "}", ].join("\n"); document.head.appendChild(tag); let disposed = false; let scheduled = false; let missStreak = 0; let warned = false; const sync = () => { scheduled = false; if (disposed) return; const wanted = String(resolveLabel() || "").trim(); if (!wanted) return; const rows = document.querySelectorAll(NAV_ROW_SELECTOR); let hit = null; for (const row of rows) { if (String(row.textContent || "").trim() === wanted) { hit = row; break; } } if (!hit) { /* 面板没开的时候整条 nav 都不存在,这是常态,不该报警。 只有「nav 已经在、我们的行却不在」才是真的注册失败。 */ if (rows.length === 0) { missStreak = 0; return; } missStreak += 1; if (missStreak === 2 && !warned) { warned = true; try { console.warn("[dsh-purge] settings nav has no 破甲 row; section registration may have failed"); } catch { /* ignore */ } } return; } missStreak = 0; if (!hit.hasAttribute(NAV_ICON_MARKER)) hit.setAttribute(NAV_ICON_MARKER, "1"); }; const schedule = () => { if (scheduled || disposed) return; scheduled = true; window.requestAnimationFrame(sync); }; const observer = new MutationObserver(schedule); observer.observe(document.body || document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "data-ds-dark-theme"], }); schedule(); return () => { disposed = true; observer.disconnect(); tag.remove(); for (const row of document.querySelectorAll("[" + NAV_ICON_MARKER + "]")) { row.removeAttribute(NAV_ICON_MARKER); } }; }, "dsh-purge: settings nav icon"); } function SettingsPage(props) { const t = typeof props.t === "function" ? props.t : translate; const [seg, setSeg] = useState("armor"); const [theme, setTheme] = useState(readTheme); useEffect(() => { let cancelled = false; let timer = 0; const syncFromHost = () => { if (!cancelled) setTheme(detectHostTheme()); }; const syncSoon = () => { if (timer) window.clearTimeout(timer); timer = window.setTimeout(syncFromHost, 50); }; syncFromHost(); let mq; try { mq = window.matchMedia?.("(prefers-color-scheme: dark)"); mq?.addEventListener?.("change", syncFromHost); } catch { /* ignore */ } let obs; try { obs = new MutationObserver(syncSoon); if (document.body) obs.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme", "class", "style"] }); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-ds-dark-theme", "class", "style"] }); } catch { /* ignore */ } return () => { cancelled = true; if (timer) window.clearTimeout(timer); try { mq?.removeEventListener?.("change", syncFromHost); } catch { /* ignore */ } try { obs?.disconnect(); } catch { /* ignore */ } }; }, []); translate = t; return h("div", { className: "dshp-root dshp-page", "data-theme": theme, "data-seg": seg }, h("style", null, PURGE_CSS), h("div", { className: "dshp-page-head" }, h("div", { className: "dshp-page-text" }, h("h2", { className: "dshp-title" }, t("page.title")), h("p", { className: "dshp-page-lead" }, t("page.lead")), ), h("div", { className: "dshp-segbar", role: "tablist", "aria-label": t("page.title") }, PAGE_SEGMENTS.map((item) => h("button", { key: item.id, type: "button", role: "tab", "aria-selected": seg === item.id ? "true" : "false", className: "dshp-seg" + (seg === item.id ? " on" : ""), onClick: () => setSeg(item.id), }, t(item.key))), ), ), seg === "armor" ? h(PurgifySection, { embedded: true, t: t }) : null, seg === "rules" ? h(RulesSection, { embedded: true, t: t }) : null, seg === "skills" ? h(SkillsSection, { embedded: true, t: t }) : null, seg === "drill" ? h(DrillSection, { t: t }) : null, ); } /* 演练台。1.3.0 之后会话里不再有浮窗入口,演练台就挂在设置页的第四段。 * 授权门没走过时先渲染门槛说明 + 打开授权窗的按钮,走完就地展开;不再自己拉一块浮层。 */ function DrillSection(props) { const t = typeof props.t === "function" ? props.t : translate; const [granted, setGranted] = useState(() => readDrillAuth()); useEffect(() => { if (granted) return undefined; const timer = window.setInterval(() => { if (readDrillAuth()) setGranted(true); }, 800); return () => window.clearInterval(timer); }, [granted]); if (!granted) { return h("section", { className: "dshp-panel", "aria-label": t("drill.title") }, h("div", { className: "dshp-head" }, h("div", null, h("h3", { className: "dshp-title" }, t("drill.title")), h("p", { className: "dshp-panel-lead" }, t("drill.lead")), ), ), h("div", { className: "dshp-row" }, h(Btn, { kind: "primary", onClick: () => requestDrill("tab") }, t("drill.authorize")), ), ); } const Panel = __dshPurgeDrill && __dshPurgeDrill.Panel; if (!Panel) { return h("section", { className: "dshp-panel" }, h("p", { className: "dshp-panel-lead" }, t("drill.unavailable")), ); } return h("div", { className: "dshp-drill-host" }, h(Panel, { embedded: true })); } function SettingsRoot(props) { const t = typeof props.t === "function" ? props.t : ((key) => key); const [theme, setTheme] = useState(readTheme); useEffect(() => { let cancelled = false; let timer = 0; const syncFromHost = () => { if (!cancelled) setTheme(detectHostTheme()); }; const syncSoon = () => { if (timer) window.clearTimeout(timer); timer = window.setTimeout(syncFromHost, 50); }; syncFromHost(); let mq; try { mq = window.matchMedia?.("(prefers-color-scheme: dark)"); mq?.addEventListener?.("change", syncFromHost); } catch { /* ignore */ } let obs; try { obs = new MutationObserver(syncSoon); if (document.body) obs.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme", "class", "style"] }); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-ds-dark-theme", "class", "style"] }); } catch { /* ignore */ } return () => { cancelled = true; if (timer) window.clearTimeout(timer); try { mq?.removeEventListener?.("change", syncFromHost); } catch { /* ignore */ } try { obs?.disconnect(); } catch { /* ignore */ } }; }, []); translate = t; return h("div", { className: "dshp-root", "data-theme": theme }, h("style", null, PURGE_CSS), h(PurgifySection, null), h(RulesSection, null), h(SkillsSection, null), ); } const REWIND_CSS = ` .dshp-rewind-wrap{position:relative;display:inline-flex} .dshp-rewind{appearance:none;display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,currentColor);font:12px/1 var(--ds-font-sans,system-ui,sans-serif);cursor:pointer} .dshp-rewind:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,currentColor)} .dshp-rewind:disabled{opacity:.4;cursor:default} .dshp-rewind svg{display:block} .dshp-rewind-menu{position:fixed;z-index:2147483000;box-sizing:border-box;min-width:220px;padding:4px;border:0;border-radius:12px;background:var(--dsw-specific-menu,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent,0 8px 24px rgba(0,0,0,.16));transform:translateY(-100%)} body[data-ds-dark-theme] .dshp-rewind-menu{background:var(--dsw-specific-menu,#32312d);color:var(--dsw-alias-label-primary,#e6e2db);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1,#3f3d38);box-shadow:var(--dsw-elevation-prominent,0 10px 28px rgba(0,0,0,.45))} .dshp-rewind-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:8px 10px;border:0;border-radius:8px;background:transparent;color:inherit;text-align:left;cursor:pointer;font:12px/1.3 var(--ds-font-sans,system-ui,sans-serif)} .dshp-rewind-item:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))} .dshp-rewind-item b{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,inherit)} .dshp-rewind-item span{color:var(--dsw-alias-label-tertiary,#9a958c);font-size:11px} body[data-ds-dark-theme] .dshp-rewind-item span{color:var(--dsw-alias-label-tertiary,#a8a39a)} .dshp-continue{appearance:none;display:inline-flex;align-items:center;gap:4px;height:28px;padding:0 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,currentColor);font:12px/1 var(--ds-font-sans,system-ui,sans-serif);cursor:pointer} .dshp-continue:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,currentColor)} .dshp-continue:disabled{opacity:.4;cursor:default} .dshp-continue.is-ready{color:var(--dsw-alias-label-primary,currentColor)} .dshp-continue svg{display:block} `; const DRAFT_KEY = "dshp-rewind-draft:"; const ARM_KEY = "dshp-rewind-arm:"; const FILL_DELAYS = [0, 50, 180, 400, 800]; const ARM_TTL_MS = 45 * 1000; let rewindSeenAt = 0; let rewindSessions = null; let rewindHost = null; /* 客户端根 ctx:apply() 时写入,供「跳到设置页」这类跨槽位调用取 shortcuts。 */ let purgeHost = null; let rewindWorkspace = null; let rewindConversation = null; let pendingComposer = { sessionId: "", text: "", at: 0 }; let armedFill = { sessionId: "", text: "", at: 0 }; function rewindText(t, key, fallback) { try { if (typeof t === "function") { const value = t(key); if (value && value !== key) return value; } } catch { /* ignore */ } return fallback; } function currentSessionId(sessions) { try { const selected = rewindWorkspace?.selection?.getSnapshot?.()?.sessionId; if (typeof selected === "string" && selected) return selected; } catch { /* ignore */ } try { const snap = sessions?.list?.getSnapshot?.(); if (typeof snap?.current === "string" && snap.current) return snap.current; } catch { /* ignore */ } return ""; } function sessionKnown(sessions, sessionId) { try { const snap = sessions?.list?.getSnapshot?.(); if (!snap || !sessionId) return false; if (snap.byId && Object.prototype.hasOwnProperty.call(snap.byId, sessionId)) return true; if (Array.isArray(snap.ids) && snap.ids.includes(sessionId)) return true; } catch { /* ignore */ } return false; } function waitMs(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function isPluginDraft(text) { const value = String(text || "").trim(); if (!value) return false; return /^\[MNEMON\]/i.test(value) || /MNEMON RUNTIME MEMORY SNAPSHOT/i.test(value) || /^MNEMON VIEW TOOLS/im.test(value); } function writeDraft(sessionId, text) { if (!sessionId) return; const value = isPluginDraft(text) ? "" : (text || ""); pendingComposer = { sessionId, text: value, at: Date.now() }; try { sessionStorage.setItem(DRAFT_KEY + sessionId, value); } catch { /* ignore */ } } function peekDraft(sessionId) { if (!sessionId) return ""; if (pendingComposer.sessionId === sessionId && pendingComposer.text) return pendingComposer.text; try { return sessionStorage.getItem(DRAFT_KEY + sessionId) || ""; } catch { return ""; } } function clearStoredDraft(sessionId) { if (!sessionId) return; if (pendingComposer.sessionId === sessionId) pendingComposer = { sessionId: "", text: "", at: 0 }; try { sessionStorage.removeItem(DRAFT_KEY + sessionId); } catch { /* ignore */ } try { sessionStorage.removeItem(ARM_KEY + sessionId); } catch { /* ignore */ } } function armComposerFill(sessionId, text) { if (!sessionId || !text || isPluginDraft(text)) return; const at = Date.now(); armedFill = { sessionId, text, at }; writeDraft(sessionId, text); try { sessionStorage.setItem(ARM_KEY + sessionId, String(at)); } catch { /* ignore */ } } function takeArmedFill(sessionId) { if (!sessionId) return ""; let text = ""; let at = 0; if (armedFill.sessionId === sessionId && armedFill.text) { text = armedFill.text; at = armedFill.at; } else { text = peekDraft(sessionId); try { at = Number(sessionStorage.getItem(ARM_KEY + sessionId) || 0); } catch { at = 0; } } if (!text || !at || Date.now() - at > ARM_TTL_MS) { if (text && (!at || Date.now() - at > ARM_TTL_MS)) clearStoredDraft(sessionId); return ""; } clearStoredDraft(sessionId); armedFill = { sessionId: "", text: "", at: 0 }; return text; } function conversationFace() { if (rewindConversation) return rewindConversation; const host = rewindHost; if (host?.conversation) return host.conversation; if (typeof host?.get === "function") { try { return host.get("conversation"); } catch { return undefined; } } return undefined; } function conversationInput(sessionId) { try { const input = conversationFace()?.input; if (!input || !sessionId) return null; if (typeof input.shell === "function") { try { return input.shell(sessionId); } catch { /* session not retained yet */ } } const sessions = rewindHost?.sessions || rewindSessions; const actx = typeof sessions?.scope === "function" ? sessions.scope(sessionId) : undefined; if (typeof input.for === "function" && actx) return input.for(actx); return null; } catch { return null; } } function liveDraft(sessionId, inputActions) { try { const fromActions = inputActions?.state?.getSnapshot?.()?.draft; if (typeof fromActions === "string") return fromActions; } catch { /* ignore */ } try { const draft = conversationInput(sessionId)?.state?.getSnapshot?.()?.draft; if (typeof draft === "string") return draft; } catch { /* ignore */ } return ""; } function hostSetDraft(sessionId, text) { if (!sessionId) return false; try { const input = conversationInput(sessionId); if (input && typeof input.setDraft === "function") { input.setDraft(text || ""); return true; } } catch { /* ignore */ } return false; } function fillComposer(inputActions, text) { if (inputActions && typeof inputActions.setDraft === "function") { try { inputActions.setDraft(text || ""); } catch { /* ignore */ } } } function scheduleComposerFill(sessionId, text, inputActions) { if (!sessionId || !text || isPluginDraft(text)) return () => {}; let stopped = false; let filled = false; const timers = []; const tryFill = () => { if (stopped) return; const current = liveDraft(sessionId, inputActions); if (filled && current !== text) { stopped = true; return; } if (current && current !== text) { stopped = true; return; } fillComposer(inputActions, text); hostSetDraft(sessionId, text); filled = true; }; for (const ms of FILL_DELAYS) timers.push(window.setTimeout(tryFill, ms)); return () => { stopped = true; for (const t of timers) { try { window.clearTimeout(t); } catch { /* ignore */ } } }; } function scheduleEchoClear(sessionId, inputActions) { if (!sessionId) return () => {}; let last = ""; let stopped = false; const timers = []; const tryClear = () => { if (stopped || !last) return; const current = String(liveDraft(sessionId, inputActions) || "").trim(); if (!current) { stopped = true; return; } if (current !== last) return; fillComposer(inputActions, ""); hostSetDraft(sessionId, ""); clearStoredDraft(sessionId); stopped = true; }; apiJson("/dsh-purge/last-user?sessionId=" + encodeURIComponent(sessionId)).then((data) => { if (stopped) return; last = String(data?.text || "").trim(); if (!last || isPluginDraft(last)) return; tryClear(); for (const ms of [0, 50, 180, 400]) timers.push(window.setTimeout(tryClear, ms)); }).catch(() => {}); return () => { stopped = true; for (const t of timers) { try { window.clearTimeout(t); } catch { /* ignore */ } } }; } async function dropInheritedQueue(sessions, sessionId) { if (!sessions || !sessionId) return; const drop = async () => { try { const actx = typeof sessions.scope === "function" ? sessions.scope(sessionId) : undefined; const face = typeof sessions.sessionOf === "function" ? sessions.sessionOf(actx) : undefined; const queue = face?.getSnapshot?.()?.queue || []; for (const item of queue) { const id = item?.id || item?.itemId; if (!id) continue; try { await face.updateQueue?.(id, { kind: "remove" }); } catch { /* ignore */ } } } catch { /* ignore */ } }; await drop(); window.setTimeout(() => { drop(); }, 80); } async function openRewoundSession(sessions, sessionId) { if (!sessionId) return; const workspace = rewindWorkspace; let lastError = null; for (let attempt = 0; attempt < 25; attempt += 1) { if (sessions && typeof sessions.refresh === "function" && !sessionKnown(sessions, sessionId)) { try { await sessions.refresh(); } catch (error) { lastError = error; } } const known = sessionKnown(sessions, sessionId); if (known || typeof sessions?.open === "function") { try { if (workspace && typeof workspace.openSession === "function") { workspace.openSession(sessionId); } else if (typeof sessions?.open === "function") { sessions.open(sessionId); } else { throw new Error("无法打开回退后的会话"); } const selected = workspace?.selection?.getSnapshot?.()?.sessionId; if (!workspace || selected === sessionId) { await dropInheritedQueue(sessions, sessionId); window.setTimeout(() => { dropInheritedQueue(sessions, sessionId); }, 250); return; } } catch (error) { lastError = error; } } await waitMs(120); } throw new Error((lastError && lastError.message) || "回退后的会话没有打开"); } class RewindSafe extends Component { constructor(p) { super(p); this.state = { failed: false }; } static getDerivedStateFromError() { return { failed: true }; } componentDidCatch(error) { try { console.error("[dsh-purge] rewind slot:", error); } catch { /* ignore */ } } render() { if (this.state.failed) return h(RewindFallback, this.props); return h(RewindButton, this.props); } } class ContinueSafe extends Component { constructor(p) { super(p); this.state = { failed: false }; } static getDerivedStateFromError() { return { failed: true }; } componentDidCatch(error) { try { console.error("[dsh-purge] continue slot:", error); } catch { /* ignore */ } } render() { if (this.state.failed) { return h("button", { type: "button", className: "dshp-continue", title: rewindText(translate, "continue.label", "继续"), onMouseDown: (e) => e.preventDefault(), onClick: async () => { const sessionId = currentSessionId(rewindSessions); if (!sessionId) return; try { const data = await apiJson("/dsh-purge/continue", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ sessionId, force: true }), }); if (!data || !data.ok) throw new Error((data && data.error) || "continue"); } catch (e) { window.alert(rewindText(translate, "continue.fail", "继续失败: {error}").replace("{error}", String((e && e.message) || e))); } }, }, rewindText(translate, "continue.label", "继续")); } return h(ContinueButton, this.props); } } function ContinueButton(props) { const t = props.t || translate; const sessions = rewindSessions; const sessionId = props.sessionId || currentSessionId(sessions); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(""); const [info, setInfo] = useState(null); useEffect(() => { if (!sessionId) { setInfo(null); return; } let cancelled = false; const tick = () => { apiJson("/dsh-purge/continue?sessionId=" + encodeURIComponent(sessionId)) .then((data) => { if (!cancelled && data && data.ok) setInfo(data); }) .catch(() => {}); }; tick(); const timer = window.setInterval(tick, 1200); return () => { cancelled = true; window.clearInterval(timer); }; }, [sessionId]); const canContinue = Boolean(info && info.canContinue); const left = info ? Math.max(0, (info.continueMax || 0) - (info.continueUsed || 0)) : 0; const runContinue = async () => { if (!sessionId || busy || !canContinue) return; setBusy(true); setNotice(""); try { const data = await apiJson("/dsh-purge/continue", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ sessionId }), }); if (!data || !data.ok) throw new Error((data && data.error) || "continue"); setInfo((prev) => prev ? { ...prev, ...data, canContinue: false, continueUsed: data.used } : data); } catch (e) { setNotice(rewindText(t, "continue.fail", "继续失败: {error}").replace("{error}", String((e && e.message) || e))); } finally { setBusy(false); } }; const label = busy ? rewindText(t, "continue.busy", "继续中…") : rewindText(t, "continue.label", "继续"); const title = notice || (canContinue ? rewindText(t, "continue.ready", "继续(还可 {left} 次)").replace("{left}", String(left)) : (info && info.reason && left <= 0 ? rewindText(t, "continue.limit", "已达继续上限") : rewindText(t, "continue.aria", "异常停止后继续"))); return h("button", { type: "button", className: "dshp-continue" + (canContinue ? " is-ready" : ""), title, "aria-label": rewindText(t, "continue.aria", "异常停止后继续"), disabled: busy || !sessionId || !canContinue, onMouseDown: (e) => e.preventDefault(), onClick: (e) => { e.preventDefault(); e.stopPropagation(); runContinue(); }, }, h("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": true }, h("path", { fill: "currentColor", d: "M4.2 2.8v10.4L13 8 4.2 2.8z" }), ), label, ); } function rewindMenuNode(t, menuPos, menuRef, runRewind) { const menu = h("div", { className: "dshp-rewind-menu", role: "menu", ref: menuRef, style: menuPos ? { left: menuPos.left + "px", top: menuPos.top + "px" } : { left: "8px", bottom: "48px", transform: "none" }, }, h("button", { type: "button", className: "dshp-rewind-item", onMouseDown: (e) => { e.preventDefault(); e.stopPropagation(); }, onClick: (e) => { e.preventDefault(); e.stopPropagation(); runRewind("once"); }, }, h("b", null, rewindText(t, "rewind.once", "回退一次")), h("span", null, rewindText(t, "rewind.once.hint", "把这一轮发出去的那句放回输入框,并清掉这一轮")), ), h("button", { type: "button", className: "dshp-rewind-item", onMouseDown: (e) => { e.preventDefault(); e.stopPropagation(); }, onClick: (e) => { e.preventDefault(); e.stopPropagation(); runRewind("round"); }, }, h("b", null, rewindText(t, "rewind.round", "回退上一轮")), h("span", null, rewindText(t, "rewind.round.hint", "把这一轮发出去的那句放回输入框;子代理也只撤这一轮")), ), ); try { const rd = require("react-dom"); if (rd && typeof rd.createPortal === "function" && typeof document !== "undefined") { return rd.createPortal(menu, document.body); } } catch { /* 没有 portal 时菜单仍挂在按钮旁 */ } return menu; } class RewindFallback extends Component { constructor(p) { super(p); this.state = { menu: false, busy: false, notice: "", menuPos: null }; this.wrap = null; this.menuEl = null; this.onDoc = (event) => { if (this.wrap && this.wrap.contains(event.target)) return; if (this.menuEl && this.menuEl.contains(event.target)) return; this.setState({ menu: false }); }; } componentDidMount() { document.addEventListener("mousedown", this.onDoc); } componentWillUnmount() { document.removeEventListener("mousedown", this.onDoc); } run(mode) { const sessionId = this.props.sessionId || currentSessionId(rewindSessions); if (!sessionId || this.state.busy) return; this.setState({ busy: true, menu: false, notice: "" }); apiJson("/dsh-purge/rewind", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ sessionId, mode }), }).then(async (data) => { if (!data || !data.ok || !data.sessionId || data.cleared === false) { throw new Error((data && data.error) || "没有清掉之前发送的内容"); } if (data.at) rewindSeenAt = data.at; const text = isPluginDraft(data.text) ? "" : (data.text || ""); armComposerFill(data.sessionId, text); await openRewoundSession(rewindSessions, data.sessionId); scheduleComposerFill(data.sessionId, text); this.setState({ busy: false }); }).catch((e) => { this.setState({ busy: false, notice: "回退失败: " + String((e && e.message) || e), }); }); } openMenu() { const rect = this.wrap && this.wrap.getBoundingClientRect ? this.wrap.getBoundingClientRect() : null; this.setState({ menu: !this.state.menu, notice: "", menuPos: rect ? { left: Math.max(8, rect.right - 228), top: Math.max(8, rect.top - 8) } : null, }); } render() { const sessionId = this.props.sessionId || currentSessionId(rewindSessions); const t = this.props.t || translate; const label = this.state.busy ? "回退中…" : (this.state.notice ? this.state.notice.slice(0, 18) : "回退"); return h("div", { className: "dshp-rewind-wrap", ref: (node) => { this.wrap = node; } }, h("style", null, REWIND_CSS), h("button", { type: "button", className: "dshp-rewind", disabled: this.state.busy || !sessionId, onMouseDown: (e) => e.preventDefault(), onClick: (e) => { e.preventDefault(); this.openMenu(); }, }, label), this.state.menu ? rewindMenuNode(t, this.state.menuPos, (node) => { this.menuEl = node; }, (mode) => this.run(mode)) : null, ); } } function RewindButton(props) { const t = props.t || translate; const sessions = rewindSessions; const inputActions = props.inputActions; const sessionId = props.sessionId || currentSessionId(sessions); const wrapRef = useRef(null); const menuRef = useRef(null); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(""); const [menu, setMenu] = useState(false); const [menuPos, setMenuPos] = useState(null); useEffect(() => { if (!sessionId) return; const armed = takeArmedFill(sessionId); if (armed) { scheduleComposerFill(sessionId, armed, inputActions); return; } scheduleEchoClear(sessionId, inputActions); }, [sessionId, inputActions]); useEffect(() => { if (!menu) return; const onDoc = (event) => { if (wrapRef.current && wrapRef.current.contains(event.target)) return; if (menuRef.current && menuRef.current.contains(event.target)) return; setMenu(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [menu]); const runRewind = async (mode) => { if (!sessionId || busy) return; setBusy(true); setMenu(false); setNotice(""); try { const data = await apiJson("/dsh-purge/rewind", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ sessionId, mode }), }); if (!data || !data.ok || !data.sessionId || data.cleared === false) { throw new Error((data && data.error) || "没有清掉之前发送的内容"); } if (data.at) rewindSeenAt = data.at; const text = isPluginDraft(data.text) ? "" : (data.text || ""); armComposerFill(data.sessionId, text); await openRewoundSession(sessions, data.sessionId); scheduleComposerFill(data.sessionId, text); } catch (e) { const message = rewindText(t, "rewind.fail", "回退失败: {error}").replace("{error}", String((e && e.message) || e)); setNotice(message); } finally { setBusy(false); } }; const onClick = (event) => { event?.preventDefault?.(); event?.stopPropagation?.(); if (!sessionId || busy) return; if (menu) { setMenu(false); return; } const rect = wrapRef.current?.getBoundingClientRect?.(); if (rect) { setMenuPos({ left: Math.max(8, rect.right - 228), top: Math.max(8, rect.top - 8), }); } setNotice(""); setMenu(true); }; const failLabel = notice ? (notice.length > 18 ? notice.slice(0, 18) + "…" : notice) : ""; const label = busy ? rewindText(t, "rewind.busy", "回退中…") : (failLabel || rewindText(t, "rewind.label", "回退")); const title = notice || rewindText(t, "rewind.aria", "回退"); return h("div", { className: "dshp-rewind-wrap", ref: wrapRef }, h("style", null, REWIND_CSS), h("button", { type: "button", className: "dshp-rewind", title, "aria-label": rewindText(t, "rewind.aria", "回退"), "aria-expanded": menu ? "true" : "false", disabled: busy || !sessionId, onMouseDown: (e) => e.preventDefault(), onClick, }, h("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": true }, h("path", { fill: "currentColor", d: "M7.2 3.2 3.4 7l3.8 3.8V8.6c2.8 0 4.7.7 5.9 2.3-.2-2.8-1.9-5.2-5.9-5.6V3.2z", }), ), label, ), menu ? rewindMenuNode(t, menuPos, menuRef, runRewind) : null, ); } function installRewindWatch(ctx) { const sessions = ctx.sessions; if (!sessions) return () => {}; const tick = async () => { try { const data = await apiJson("/dsh-purge/rewind"); if (!data || !data.sessionId || !data.at || data.at <= rewindSeenAt) return; rewindSeenAt = data.at; const current = currentSessionId(sessions); if (current && current !== data.parentId) return; const text = isPluginDraft(data.text) ? "" : (data.text || ""); armComposerFill(data.sessionId, text); scheduleComposerFill(data.sessionId, text); await openRewoundSession(sessions, data.sessionId); scheduleComposerFill(data.sessionId, text); } catch { /* ignore */ } }; const timer = window.setInterval(tick, 1200); return () => window.clearInterval(timer); } /* __DSH_PURGE_DRILL_BEGIN__ */ // 内嵌演练台(源码在 lib/redteam,已并进本插件) const __dshPurgeDrill = (() => { var module = { exports: {} }; var exports = module.exports; var module = { exports: {} } var exports = module.exports Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }) const React = require('react') const h = React.createElement /* * 演练台自己是一块浮动窗:position:fixed,用 left/top/width/height 放。 * 不给官方 frame 加 padding,不改官方输入框的宽度。嵌在清洗面板里时走 .rt-embedded。 * 外观按宿主 token:不透明层一底 + 0.5px 描边 + xl 圆角 + 官方抬升阴影,不再用玻璃模糊。 */ const CSS = ` .rt-dock{position:fixed;top:12px;right:12px;z-index:20;display:flex;flex-direction:column;box-sizing:border-box; background:var(--dsw-alias-bg-layer-1,#fff); border:.5px solid var(--dsw-alias-border-l3,#d5d5d5); border-radius:var(--dsw-radius-xl,20px); box-shadow:var(--dsw-elevation-prominent,0 18px 48px rgba(0,0,0,.24));pointer-events:auto;color:var(--dsw-alias-label-primary); font-size:13px;line-height:20px} .rt-dock.rt-embedded{position:relative;inset:auto;width:100%;height:100%;max-width:none;border:0;border-radius:0;box-shadow:none;background:transparent!important;backdrop-filter:none;-webkit-backdrop-filter:none} .rt-grip,.rt-resize-r,.rt-resize-b,.rt-resize-c{position:absolute;z-index:3;touch-action:none} .rt-grip{left:-3px;top:0;bottom:0;width:8px;cursor:ew-resize;background:transparent} .rt-resize-r{right:-3px;top:0;bottom:14px;width:8px;cursor:ew-resize} .rt-resize-b{left:8px;right:14px;bottom:-3px;height:8px;cursor:ns-resize} .rt-resize-c{right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize} .rt-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1)} .rt-title{font-weight:600;font-size:14px;display:flex;align-items:center;gap:6px;white-space:nowrap} .rt-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-brand-primary)} .rt-spacer{flex:1} .rt-btn{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:inherit; border-radius:6px;padding:3px 9px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap} .rt-btn:hover{border-color:var(--dsw-alias-border-l2)} .rt-btn-primary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:#fff} .rt-btn-primary:hover{opacity:.9} .rt-btn:disabled{opacity:.5;cursor:default} .rt-tabs{display:flex;flex-wrap:wrap;gap:2px;padding:6px 12px 0;border-bottom:1px solid var(--dsw-alias-border-l1)} .rt-tab{padding:8px 10px 7px;border-radius:0;cursor:pointer;font-size:12.5px;color:var(--dsw-alias-label-secondary); display:inline-flex;align-items:center;gap:5px;border-bottom:2px solid transparent;margin-bottom:-1px;background:transparent} .rt-tab:hover{color:var(--dsw-alias-label-primary)} .rt-tab.on{color:var(--dsw-alias-label-primary);background:transparent;font-weight:600;border-bottom-color:var(--dsw-alias-brand-primary,#6dbf8c)} /* 未读红点:该页签有新内容(新资产/新漏洞/新得分/新步骤…),点开看过就消失 */ .rt-tab-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-error-primary,#ef4444);flex:none; box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-error-primary,#ef4444) 22%, transparent)} .rt-body{flex:1;min-height:0;display:flex;flex-direction:column} .rt-split{flex:1;min-height:0;display:flex} .rt-side{width:200px;flex:none;border-right:1px solid var(--dsw-alias-border-l1);overflow:auto;padding:8px} .rt-main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden} .rt-seg{padding:7px 8px;border-radius:6px;cursor:pointer;margin-bottom:4px;border:1px solid transparent} .rt-seg:hover{background:var(--dsw-alias-bg-layer-2)} .rt-seg.on{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-brand-primary)} .rt-seg-cidr{font-family:ui-monospace,Menlo,monospace;font-size:12.5px} .rt-seg-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:2px} .rt-toolbar{display:flex;gap:6px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);flex-wrap:wrap;align-items:center} .rt-input{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:inherit; border-radius:6px;padding:4px 8px;font-size:12px;font-family:inherit;outline:none;min-width:0} .rt-input:focus{border-color:var(--dsw-alias-brand-primary)} .rt-input option,.rt-eng-select option{background:var(--dsw-alias-bg-layer-1,#1c1c1c);color:var(--dsw-alias-label-primary,#f2f2f2)} .rt-eng-select{color:var(--dsw-alias-label-primary,#f2f2f2);background:var(--dsw-alias-bg-layer-2,#2a2a2a)} .rt-table{flex:1;overflow:auto} .rt-row{display:grid;grid-template-columns:150px 74px 104px 1.15fr 1fr;gap:8px;padding:6px 10px; border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;cursor:pointer;font-size:12.5px} .rt-row:hover{background:var(--dsw-alias-bg-layer-2)} .rt-row.head{cursor:default;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-weight:600;position:sticky;top:0; background:var(--dsw-alias-bg-layer-1);z-index:1} .rt-row.head:hover{background:var(--dsw-alias-bg-layer-1)} .rt-mono{font-family:ui-monospace,Menlo,monospace} .rt-tag{display:inline-block;padding:0 5px;border-radius:4px;font-size:11px;margin-right:4px; border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);white-space:nowrap} .rt-tag-passive{color:#8b5cf6;border-color:#8b5cf655;background:#8b5cf61a} .rt-tag-active{color:var(--dsw-alias-state-warn-primary,#f59e0b);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b)) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b)) 53%,transparent)} .rt-tag-live{color:var(--dsw-alias-state-success-primary,#22c55e);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)} .rt-tag-warn{color:var(--dsw-alias-state-error-primary,#ef4444);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent)} /* 纪律提示条:不满足交付要求(非冰蝎/哥斯拉马、没有 suo5 隧道)时顶在区块最上方 */ .rt-hint{border-radius:6px;padding:7px 9px;margin-bottom:8px;font-size:11.5px;line-height:1.6; border:1px dashed color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent);color:var(--dsw-alias-label-primary)} .rt-hint b{color:var(--dsw-alias-state-error-primary,#ef4444)} .rt-tag-dead{color:var(--dsw-alias-label-secondary)} .rt-expand{grid-column:1/-1;padding:8px 4px 10px;font-size:12px;color:var(--dsw-alias-label-secondary)} .rt-kv{display:flex;gap:8px;margin-bottom:3px;align-items:baseline} .rt-kv b{color:var(--dsw-alias-label-primary);font-weight:600;min-width:64px;flex:none} /* 图谱视图已移除(见 AssetsTab:资产关系由「域名维度」与 redteam_attack_path 工具承担) */ .rt-pane{flex:1;min-height:0;overflow:auto;padding:12px} .rt-card{border:.5px solid var(--dsw-alias-border-l2,#e8e8e8);border-radius:var(--dsw-radius-md,12px);padding:10px 12px;margin-bottom:10px; background:transparent} .rt-card h4{margin:0 0 6px;font-size:13px;font-weight:500} .rt-textarea{width:100%;min-height:260px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1); color:inherit;border-radius:6px;padding:8px;font-size:12.5px;font-family:ui-monospace,Menlo,monospace; line-height:1.6;resize:vertical;outline:none;box-sizing:border-box} .rt-textarea:focus{border-color:var(--dsw-alias-brand-primary)} .rt-list{width:210px;flex:none;border-right:1px solid var(--dsw-alias-border-l1);overflow:auto;padding:8px} .rt-item{padding:7px 8px;border-radius:6px;cursor:pointer;margin-bottom:4px;border:1px solid transparent} .rt-item:hover{background:var(--dsw-alias-bg-layer-2)} .rt-item.on{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-brand-primary)} .rt-item-name{font-weight:600;font-size:12.5px} .rt-item-desc{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:2px; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .rt-empty{padding:24px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12.5px} .rt-err{color:var(--dsw-alias-state-error-primary);font-size:12px;padding:6px 10px} .rt-foot{padding:8px 12px;border-top:1px solid var(--dsw-alias-border-l1);font-size:11px; color:var(--dsw-alias-label-secondary);display:flex;gap:12px;flex-wrap:wrap;align-items:center} .rt-icon-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;border:1px solid var(--dsw-alias-border-l1); background:transparent;color:inherit;border-radius:6px;padding:6px 8px;cursor:pointer;font-family:inherit;font-size:12.5px} .rt-icon-btn:hover{background:var(--dsw-alias-bg-layer-2)} .rt-icon-btn.on{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)} .rt-hbtn{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:inherit;border-radius:6px; padding:2px 8px;font-size:12px;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:5px} .rt-hbtn:hover{background:var(--dsw-alias-bg-layer-2)} .rt-hbtn.on{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)} .rt-test{display:inline-block;padding:0 5px;border-radius:4px;font-size:11px;white-space:nowrap; border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)} .rt-test-testing{color:var(--dsw-alias-state-warn-primary,#f59e0b);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b)) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b)) 53%,transparent)} .rt-test-tested{color:var(--dsw-alias-state-success-primary,#22c55e);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)} .rt-test-blocked{color:#fff;background:var(--dsw-alias-state-error-primary,#ef4444);border-color:var(--dsw-alias-state-error-primary,#ef4444)} .rt-test-abandoned{color:#94a3b8;border-color:#94a3b855;background:#94a3b81a} .rt-test-no_surface{color:#6366f1;border-color:#6366f155;background:#6366f11a} .rt-scope-testing{color:#5b21b6;background:#ddd6fe} .rt-pri{display:inline-block;padding:0 6px;border-radius:4px;font-size:11px;font-weight:600;white-space:nowrap} .rt-pri-high{color:#fff;background:var(--dsw-alias-state-error-primary,#ef4444)} .rt-pri-medium{color:#fff;background:var(--dsw-alias-state-warn-primary,#f59e0b)} .rt-pri-low{color:#fff;background:#94a3b8} .rt-score-row{display:grid;grid-template-columns:16px 62px minmax(0,1fr) 116px 66px;gap:8px;padding:7px 10px; border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;font-size:12.5px;cursor:pointer} .rt-score-row:hover{background:var(--dsw-alias-bg-layer-2)} .rt-score-row.head{cursor:default;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-weight:600; position:sticky;top:0;background:var(--dsw-alias-bg-layer-1);z-index:1} .rt-score-detail{grid-column:1/-1;padding:8px 4px 10px;font-size:12px;color:var(--dsw-alias-label-secondary)} /* 得分目标按合并版的 8 个类别分组:类别头 + 组内按分值升序 */ .rt-score-group{display:flex;align-items:center;gap:8px;padding:7px 10px 5px;margin-top:2px; font-size:12px;font-weight:600;border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)} .rt-score-group:first-child{border-top:none} .rt-score-group .rt-sg-sub{font-weight:400;font-size:11px;color:var(--dsw-alias-label-secondary)} /* ── 会话与入口:每张卡片分「标题行 / 关键事实 / 可用命令 / 备注」四段,避免一行糊在一起 ── */ .rt-sess-facts{display:flex;flex-direction:column;gap:2px;margin-top:5px} .rt-sess-fact{display:flex;gap:6px;font-size:11.5px;line-height:1.5} .rt-sess-fact>b{flex:none;min-width:62px;font-weight:600;color:var(--dsw-alias-label-secondary)} .rt-sess-fact>span{min-width:0;overflow-wrap:anywhere} .rt-sess-cmd{margin-top:6px} .rt-sess-cmd>b{display:block;font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-weight:600} .rt-sess-fold{margin-top:6px;font-size:11.5px} .rt-sess-fold>summary{cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none} .rt-sess-fold>summary:hover{color:var(--dsw-alias-label-primary)} .rt-score-row .rt-scope{font-size:10.5px;color:var(--dsw-alias-label-secondary)} .rt-score-form{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px} .rt-score-form input,.rt-score-form select{width:100%;box-sizing:border-box} .rt-vrow{display:grid;grid-template-columns:62px minmax(0,1fr) 132px 108px 74px 52px;gap:8px;padding:6px 10px; border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;font-size:12.5px;cursor:pointer} .rt-vrow:hover{background:var(--dsw-alias-bg-layer-2)} .rt-vrow.head{cursor:default;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-weight:600; position:sticky;top:0;background:var(--dsw-alias-bg-layer-1);z-index:1} .rt-vdetail{grid-column:1/-1;padding:8px 4px 10px;font-size:12px;color:var(--dsw-alias-label-secondary)} .rt-vdetail .rt-kv{margin-bottom:4px} .rt-actions{display:flex;gap:6px;margin-top:6px} .rt-section{padding:8px 10px 2px;font-size:11.5px;font-weight:600;color:var(--dsw-alias-label-secondary)} .rt-link{color:var(--dsw-alias-brand-primary);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-link:hover{text-decoration:underline} .rt-full{position:fixed;inset:0;z-index:60;display:flex;flex-direction:column; background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5; border-top:1px solid var(--dsw-alias-brand-primary)} .rt-full .rt-head{padding:12px 18px} .rt-full .rt-tabs{padding:10px 18px 0;gap:6px;flex-wrap:wrap} .rt-full .rt-side{width:260px} .rt-full .rt-list{width:280px} .rt-full .rt-row{grid-template-columns:190px 90px 130px 1.4fr 1.2fr} .rt-full .rt-vrow{grid-template-columns:80px minmax(0,1.6fr) 200px 150px 90px 64px} .rt-full .rt-pane{padding:18px} .rt-full .rt-textarea{min-height:60vh} .rt-full .rt-foot{padding:10px 18px;font-size:12px} .rt-full .rt-body{max-width:1400px;width:100%;margin:0 auto;flex:1;min-height:0;display:flex;flex-direction:column} .rt-chain{flex:1;overflow:auto;padding:10px 12px} .rt-step{display:flex;gap:10px;padding:8px 6px;border-left:1px solid var(--dsw-alias-border-l1);margin-left:6px} .rt-step:last-child{border-left-color:transparent} .rt-step-dot{width:22px;height:22px;flex:none;border-radius:50%;display:flex;align-items:center;justify-content:center; font-size:11px;font-weight:700;color:#fff;background:var(--dsw-alias-border-l3,#64748b);margin-left:-13px} .rt-step-body{min-width:0} .rt-step-title{font-weight:600;font-size:13px} .rt-step-meta{font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-top:2px;word-break:break-word} .rt-stage-recon{background:#6366f1} .rt-stage-vuln{background:var(--dsw-alias-state-warn-primary,#f59e0b)} .rt-stage-exploit{background:var(--dsw-alias-state-error-primary,#ef4444)} .rt-stage-access{background:var(--dsw-alias-state-success-primary,#22c55e)} .rt-stage-pivot{background:#8b5cf6} .rt-stage-data{background:#0ea5e9} .rt-stage-other{background:var(--dsw-alias-border-l3,#64748b)} .rt-step-head{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap} .rt-step-time{font-size:11px;color:var(--dsw-alias-label-secondary);margin-left:auto;white-space:nowrap} .rt-step-detail{font-size:12px;margin-top:5px;white-space:pre-wrap;word-break:break-word; border-left:1px solid var(--dsw-alias-border-l1);padding:2px 0 2px 9px;line-height:1.6} .rt-chip{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;padding:1px 7px;border-radius:5px; border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);margin:4px 5px 0 0;max-width:100%} .rt-chip>i{font-style:normal;color:var(--dsw-alias-label-secondary);font-size:10.5px} .rt-chip>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:420px} .rt-stage-tag{display:inline-block;padding:0 6px;border-radius:4px;font-size:11px;color:#fff;font-weight:600} .rt-st-recon{background:#6366f1}.rt-st-vuln{background:var(--dsw-alias-state-warn-primary,#f59e0b)}.rt-st-exploit{background:var(--dsw-alias-state-error-primary,#ef4444)} .rt-st-access{background:var(--dsw-alias-state-success-primary,#22c55e)}.rt-st-pivot{background:#8b5cf6}.rt-st-data{background:#0ea5e9}.rt-st-other{background:var(--dsw-alias-border-l3,#64748b)} .rt-sev{display:inline-block;padding:0 6px;border-radius:4px;font-size:11px;font-weight:600;border:1px solid transparent} .rt-sev-critical{color:#fff;background:#b91c1c}.rt-sev-high{color:#fff;background:var(--dsw-alias-state-error-primary,#ef4444)} .rt-sev-medium{color:#7c2d12;background:#fdba74}.rt-sev-low{color:#1e3a8a;background:#bfdbfe} .rt-sev-info{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l1)} .rt-dot-on{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)} .rt-dot-off{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent)} .rt-dot-unk{display:inline-block;width:7px;height:7px;border-radius:50%;background:#94a3b8;box-shadow:0 0 0 3px #94a3b833} .rt-sess-grid{display:grid;grid-template-columns:1fr;gap:8px;padding:10px 12px} .rt-sess{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:9px 10px;background:var(--dsw-alias-bg-layer-2)} .rt-sess-head{display:flex;align-items:center;gap:7px;flex-wrap:wrap} .rt-sess-title{font-weight:600;font-size:12.5px;font-family:ui-monospace,Menlo,monospace;word-break:break-all} .rt-sess-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-top:3px;word-break:break-word} .rt-code{font-family:ui-monospace,Menlo,monospace;font-size:11px;background:var(--dsw-alias-bg-base); border:1px solid var(--dsw-alias-border-l1);border-radius:4px;padding:1px 5px;cursor:pointer;word-break:break-all} .rt-code:hover{border-color:var(--dsw-alias-brand-primary)} .rt-evi{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;margin-top:8px} .rt-evi-head{display:flex;align-items:center;gap:8px;padding:5px 9px;background:var(--dsw-alias-bg-layer-2); font-size:11.5px;font-weight:600;border-bottom:1px solid var(--dsw-alias-border-l1)} .rt-evi-body{margin:0;padding:9px 11px;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.6; white-space:pre-wrap;word-break:break-word;max-height:340px;overflow:auto;background:var(--dsw-alias-bg-base)} .rt-evi-body.req{max-height:220px} .rt-hl-req{color:var(--dsw-alias-state-success-primary,#22c55e);font-weight:600} .rt-hl-res{color:#0ea5e9;font-weight:600} .rt-gain{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;padding:2px 8px;border-radius:12px; color:#065f46;background:#a7f3d0;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)} .rt-total{font-size:20px;font-weight:700;font-family:ui-monospace,Menlo,monospace} .rt-sidehead{padding:7px 8px 2px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary); display:flex;align-items:center;gap:5px} .rt-sidehead-btn{cursor:pointer;outline:none;padding:6px 6px 5px;border-radius:5px;user-select:none} .rt-sidehead-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)} .rt-sidehead-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-scope{display:inline-block;padding:0 4px;border-radius:3px;font-size:10px;font-weight:700;line-height:15px;flex:none} .rt-scope-internal{color:#0e7490;background:#a5f3fc} .rt-scope-external{color:#9a3412;background:#fed7aa} .rt-hits{display:flex;flex-direction:column;gap:6px;margin-top:7px} .rt-hit{border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-success-primary,#22c55e);border-radius:6px; padding:7px 9px;background:var(--dsw-alias-bg-layer-2)} .rt-hit-head{display:flex;align-items:center;gap:7px;flex-wrap:wrap} .rt-hit-idx{width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);color:#fff;font-size:10.5px;font-weight:700; display:inline-flex;align-items:center;justify-content:center;flex:none} .rt-hit-target{font-family:ui-monospace,Menlo,monospace;font-weight:600;font-size:12px;word-break:break-all} .rt-hit-time{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap} .rt-hit-evi{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.65;white-space:pre-wrap;word-break:break-word; background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:5px;padding:6px 8px;margin-top:5px} .rt-hit-note{font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-top:4px} .rt-cred{border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b));border-radius:6px; padding:8px 10px;background:var(--dsw-alias-bg-layer-2);margin-bottom:7px} .rt-cred-head{display:flex;align-items:center;gap:7px;flex-wrap:wrap} .rt-cred-host{font-family:ui-monospace,Menlo,monospace;font-weight:600;font-size:12.5px;word-break:break-all} .rt-secret{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;line-height:1.6;background:#fef3c7;color:#78350f; border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b)) 53%,transparent);border-radius:5px;padding:6px 9px;margin-top:6px;white-space:pre-wrap;word-break:break-all; user-select:all;cursor:text} .rt-secret-none{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;color:var(--dsw-alias-state-error-primary); border:1px dashed var(--dsw-alias-state-error-primary);border-radius:5px;padding:5px 9px;margin-top:6px} .rt-cred-meta{font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-top:5px;word-break:break-word} .rt-stage-score{background:var(--dsw-alias-state-success-primary,#22c55e)} .rt-counted{font-family:ui-monospace,Menlo,monospace;font-size:12px;font-weight:700;color:#065f46;background:#a7f3d0;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent);border-radius:9px;padding:0 7px} .rt-scorepts{font-size:11.5px;font-weight:700;color:#065f46;background:#a7f3d0;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent); border-radius:10px;padding:0 7px;white-space:nowrap} .rt-livebar{display:flex;align-items:center;gap:7px;padding:7px 12px;border-bottom:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-2);flex-wrap:wrap} .rt-live-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);flex:none;animation:rt-pulse 1.6s ease-in-out infinite} .rt-live-dot.idle{background:#94a3b8;animation:none} @keyframes rt-pulse{0%,100%{opacity:1;box-shadow:0 0 0 0 color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)}50%{opacity:.5;box-shadow:0 0 0 5px color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)}} .rt-live-body{padding:8px 12px 2px;max-height:44vh;overflow:auto} .rt-atest{border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-success-primary,#22c55e);border-radius:6px; padding:7px 10px;background:var(--dsw-alias-bg-layer-2);margin-bottom:6px} .rt-atest.past{border-left-color:#94a3b8;opacity:.85} .rt-atest-head{display:flex;align-items:center;gap:7px;flex-wrap:wrap} .rt-atest-ip{font-family:ui-monospace,Menlo,monospace;font-weight:600;font-size:12.5px;word-break:break-all} .rt-atest-meta{font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-top:3px;word-break:break-word} .rt-atest-notes{font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.6;white-space:pre-wrap; word-break:break-word;background:var(--dsw-alias-bg-base);border-radius:4px;padding:5px 7px;margin-top:4px;max-height:76px;overflow:auto} .rt-concl{display:flex;align-items:center;gap:4px;padding:6px 12px;border-bottom:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-2);flex-wrap:wrap} .rt-concl-i{display:inline-flex;align-items:baseline;gap:4px;padding:2px 8px;border-radius:6px;cursor:pointer; border:1px solid transparent} .rt-concl-i:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)} .rt-concl-i.on{border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-1)} .rt-concl-i b{font-family:ui-monospace,Menlo,monospace;font-size:14px;font-weight:700} .rt-concl-i>span{color:var(--dsw-alias-label-secondary);font-size:11.5px} .rt-more{color:var(--dsw-alias-brand-primary);font-size:11.5px;cursor:pointer;user-select:none;margin-top:4px;display:inline-block} .rt-more:hover{text-decoration:underline} .rt-subtabs{display:flex;gap:4px;padding:6px 10px 0;border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center} .rt-subtab{padding:4px 10px;border-radius:6px 6px 0 0;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)} .rt-subtab.on{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font-weight:600} .rt-sevbar{width:3px;border-radius:2px;align-self:stretch;flex:none;margin-right:2px} /* ── 折叠层次体系 ───────────────────────────────────────────────────────────── L1 折叠头 .rt-sec :通栏、无圆角无边框、左色条、深底 —— 永远是"扁"的 L2 内容卡 .rt-atest/.rt-hit/.rt-cred/.rt-evi:内缩、有边框、圆角 —— 立起来 L3 详情/长文本 .rt-clip-body / .rt-evi-body :无边框、最浅、等宽 L4 子项容器 .rt-sec-body:左缩进 + 竖引导线,表明"属于上面那个头" ──────────────────────────────────────────────────────────────────────────── */ .rt-sec-wrap{margin:0} .rt-sec{display:flex;align-items:center;gap:8px;padding:7px 12px 7px 9px;cursor:pointer; background:var(--dsw-alias-bg-layer-2);border-left:1px solid var(--dsw-alias-border-l2); border-top:1px solid var(--dsw-alias-border-l1);user-select:none;outline:none} .rt-sec:hover{background:var(--dsw-alias-bg-layer-1)} .rt-sec:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-sec.flat{cursor:default} .rt-sec.flat:hover{background:var(--dsw-alias-bg-layer-2)} .rt-sec-caret{flex:none;width:11px;font-size:10px;color:var(--dsw-alias-label-secondary);text-align:center} .rt-sec-title{font-weight:600;font-size:13px;white-space:nowrap} .rt-sec-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap} .rt-sec-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-sec-right{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none} .rt-sec.t-stage{border-left-color:#8b5cf6} .rt-sec.t-target{border-left-color:#0ea5e9} .rt-sec.t-folder{border-left-color:var(--dsw-alias-border-l3,#64748b)} .rt-sec.t-test{border-left-color:var(--dsw-alias-state-success-primary,#22c55e)} .rt-sec.t-queue{border-left-color:var(--dsw-alias-state-warn-primary,#f59e0b)} .rt-sec.t-past{border-left-color:#94a3b8} .rt-sec-body{margin-left:12px;border-left:1px solid var(--dsw-alias-border-l1);padding:7px 0 3px 10px} .rt-sec-body>.rt-atest:last-child,.rt-sec-body>.rt-hit:last-child{margin-bottom:2px} /* 长文本折叠(L3):默认预览 2 行并渐隐,展开后限高滚动 */ .rt-clip{margin-top:6px} .rt-clip-head{display:flex;align-items:center;gap:6px} .rt-clip-label{font-size:11px;color:var(--dsw-alias-label-secondary);font-weight:600} .rt-clip-body{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.65;white-space:pre-wrap; word-break:break-word;background:var(--dsw-alias-bg-base);border-radius:5px;padding:6px 8px;margin-top:3px} .rt-clip:not(.open) .rt-clip-body{max-height:46px;overflow:hidden; -webkit-mask-image:linear-gradient(180deg,#000 55%,transparent);mask-image:linear-gradient(180deg,#000 55%,transparent)} .rt-clip.open .rt-clip-body{max-height:340px;overflow:auto} .rt-flow{padding:10px 12px 16px} .rt-flow-start,.rt-flow-end{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:4px 0} .rt-flow-end{font-weight:600;color:var(--dsw-alias-label-primary)} .rt-flow-link{display:flex;align-items:center;gap:8px;padding:3px 0 3px 10px} .rt-flow-arrow{color:var(--dsw-alias-border-l2);font-size:11px;flex:none} .rt-flow-action{font-size:11.5px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-flow-action.inferred{opacity:.7;font-style:italic} .rt-flow-node{border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-success-primary,#22c55e);border-radius:8px; padding:8px 11px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;outline:none} .rt-flow-node:hover{border-color:var(--dsw-alias-border-l2)} .rt-flow-node:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-flow-node.overflow{border-left-color:#94a3b8;opacity:.75} .rt-flow-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .rt-flow-idx{width:18px;height:18px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);color:#fff;font-size:11px;font-weight:700; display:inline-flex;align-items:center;justify-content:center;flex:none} .rt-flow-node.overflow .rt-flow-idx{background:#94a3b8} .rt-flow-name{font-weight:600;font-size:13px} .rt-flow-pts{margin-left:auto;font-family:ui-monospace,Menlo,monospace;font-size:12.5px;font-weight:700;color:#065f46; background:#a7f3d0;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent);border-radius:10px;padding:0 8px;white-space:nowrap} .rt-flow-pts.off{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-border-l1)} .rt-flow-times{font-size:11px;color:var(--dsw-alias-label-secondary)} .rt-flow-target{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;margin-top:4px;word-break:break-all; color:var(--dsw-alias-label-secondary)} .rt-flow-gain{font-size:12px;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-flow-detail{margin-top:7px;padding-top:7px;border-top:1px dashed var(--dsw-alias-border-l1)} .rt-hflow-wrap{flex:1;min-height:0;display:flex;flex-direction:column;overflow:auto} .rt-hflow{display:flex;align-items:center;gap:0;padding:16px 12px;overflow-x:auto;flex-wrap:nowrap} .rt-hflow-item{display:flex;align-items:center;flex:none} .rt-hflow-start,.rt-hflow-end{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;padding:0 4px} .rt-hflow-end{font-weight:600;color:var(--dsw-alias-label-primary)} .rt-hflow-arrow{color:var(--dsw-alias-border-l2);padding:0 5px;font-size:13px;flex:none} .rt-hflow-node{display:flex;align-items:center;gap:5px;border:1px solid var(--dsw-alias-border-l1); border-left:1px solid var(--dsw-alias-state-success-primary,#22c55e);border-radius:7px;padding:5px 8px;background:var(--dsw-alias-bg-layer-2); cursor:pointer;white-space:nowrap;outline:none} .rt-hflow-node:hover{border-color:var(--dsw-alias-border-l2)} .rt-hflow-node:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-hflow-node.overflow{border-left-color:#94a3b8;opacity:.72} .rt-hflow-node.open{border-color:var(--dsw-alias-brand-primary)} .rt-hflow-pts{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;font-weight:700;color:#065f46;background:#a7f3d0; border-radius:8px;padding:0 6px} .rt-hflow-node.overflow .rt-hflow-pts{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1)} .rt-hflow-name{font-size:11.5px} .rt-hflow-n{font-size:10.5px;color:var(--dsw-alias-label-secondary)} .rt-rep-list{padding:10px 12px 14px} .rt-rep-tools{display:flex;gap:6px;justify-content:flex-end;margin-bottom:8px} .rt-rep{border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-success-primary,#22c55e);border-radius:8px; padding:9px 11px;margin-bottom:10px;background:var(--dsw-alias-bg-layer-2)} .rt-rep-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .rt-rep-idx{font-size:15px;color:var(--dsw-alias-state-success-primary,#22c55e);font-weight:700;flex:none} .rt-rep-name{font-weight:600;font-size:13px} .rt-rep-meta{font-size:12px;margin-top:4px;color:var(--dsw-alias-label-secondary);word-break:break-word} .rt-rep-meta b{color:var(--dsw-alias-label-primary);font-weight:600;margin-right:2px} .rt-rep-missing{font-size:11.5px;color:var(--dsw-alias-state-error-primary);margin-top:6px; border:1px dashed var(--dsw-alias-state-error-primary);border-radius:5px;padding:5px 8px} .rt-rep-req{margin-top:7px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;overflow:hidden} .rt-rep-req-head{display:flex;align-items:center;gap:7px;padding:4px 8px;font-size:11.5px;font-weight:600; background:var(--dsw-alias-bg-layer-1);border-bottom:1px solid var(--dsw-alias-border-l1)} .rt-rep-http{margin:0;padding:8px 10px;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto;background:var(--dsw-alias-bg-base)} /* ── 报告里的「这一步怎么来的」:动作步骤 / 命令 / 凭据 / 隧道 ───────────── */ .rt-rep-trace{margin-top:8px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;padding:8px 10px; background:var(--dsw-alias-bg-base)} .rt-rep-trace-head{display:flex;align-items:center;gap:7px;margin-bottom:5px} .rt-rep-trace-title{font-weight:600;font-size:12.5px} .rt-rep-trace-how{font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:6px} .rt-rep-step{border-left:1px solid var(--dsw-alias-border-l1);padding:2px 0 6px 9px;margin-bottom:6px} .rt-rep-step-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap} .rt-rep-step-no{width:17px;height:17px;border-radius:50%;background:var(--dsw-alias-brand-primary);color:#fff; font-size:10.5px;display:flex;align-items:center;justify-content:center;flex:none} .rt-rep-step-title{font-weight:600;font-size:12.5px} .rt-rep-step-detail{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:2px} .rt-rep-step-cmd{font-size:11.5px;margin-top:4px;display:flex;gap:6px;align-items:flex-start;flex-wrap:wrap} .rt-rep-step-cmd .rt-mono{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1); border-radius:5px;padding:2px 6px;word-break:break-all} .rt-rep-step-result{font-size:11.5px;margin-top:3px;word-break:break-word} .rt-rep-src{margin-top:6px;padding-top:6px;border-top:1px dashed var(--dsw-alias-border-l1)} .rt-rep-src-title{font-weight:600;font-size:12px;margin-bottom:3px} /* ── 全链路攻击路径图 ─────────────────────────────────────────────── */ .rt-ap{padding:10px 12px 18px;overflow:auto} .rt-ap-stage{margin-bottom:2px} .rt-ap-head{display:flex;align-items:center;gap:8px;padding:7px 10px;background:var(--dsw-alias-bg-layer-2); border-left:1px solid var(--dsw-alias-border-l3,var(--dsw-alias-border-l3,#64748b));border-top:1px solid var(--dsw-alias-border-l1);border-radius:6px 6px 0 0} .rt-ap-no{width:20px;height:20px;border-radius:5px;color:#fff;font-size:11px;font-weight:700;flex:none; display:inline-flex;align-items:center;justify-content:center} .rt-ap-name{font-weight:700;font-size:13.5px} .rt-ap-en{font-size:10px;color:var(--dsw-alias-label-secondary);letter-spacing:.3px} .rt-ap-phase{font-size:10.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap} .rt-ap-goal{display:flex;align-items:baseline;gap:8px;padding:6px 10px 6px 9px;font-size:12px; border-left:1px solid var(--dsw-alias-border-l3,var(--dsw-alias-border-l3,#64748b));background:var(--dsw-alias-bg-layer-1)} .rt-ap-goal-tag{font-size:10.5px;font-weight:700;border:1px solid;border-radius:4px;padding:0 5px;white-space:nowrap;flex:none} .rt-ap-result{padding:7px 10px 3px;border-left:4px solid transparent} .rt-ap-result-head{display:flex;align-items:center;gap:8px;margin-bottom:5px;flex-wrap:wrap} .rt-ap-pts{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;font-weight:700;color:#065f46;background:#a7f3d0; border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent);border-radius:10px;padding:0 7px;white-space:nowrap} .rt-ap-pts.off{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-border-l1)} /* 按服务封顶/自建而不计分的命中:分值标灰(+0),避免看着像又加了分 */ .rt-ap-pts.uncounted{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-border-l1)} .rt-ap-nth{font-size:10.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap} .rt-ap-hit{border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-success-primary,#22c55e);border-radius:6px; padding:6px 9px;margin-bottom:5px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;outline:none} .rt-ap-hit:hover{border-color:var(--dsw-alias-border-l2)} .rt-ap-hit:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-ap-hit.overflow{border-left-color:#94a3b8;opacity:.75} .rt-ap-hit-head{display:flex;align-items:center;gap:7px;flex-wrap:wrap} .rt-ap-hit-name{font-weight:600;font-size:12.5px} .rt-ap-hit-target{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-secondary); margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:46%} .rt-ap-act{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:3px} .rt-ap-act.inferred{font-style:italic;opacity:.75} .rt-ap-none{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 0 4px} .rt-ap-trans{display:flex;align-items:center;gap:7px;padding:2px 0 2px 14px} .rt-ap-trans-t{font-size:11px;color:var(--dsw-alias-label-secondary)} /* 横向路径图 */ .rt-ap-h{display:flex;align-items:stretch;padding:10px 12px 14px;overflow-x:auto;flex:1;min-height:0} .rt-ap-col{display:flex;align-items:stretch;flex:none} .rt-ap-col-arrow{align-self:center;color:var(--dsw-alias-border-l2);padding:0 6px;font-size:12px;flex:none} .rt-hcol{width:228px;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1); border-top:1px solid var(--dsw-alias-border-l3,var(--dsw-alias-border-l3,#64748b));border-radius:7px;background:var(--dsw-alias-bg-layer-1);overflow:hidden} .rt-hcol-goal{font-size:11px;color:var(--dsw-alias-label-secondary);padding:5px 8px;line-height:1.45; border-bottom:1px solid var(--dsw-alias-border-l1);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} .rt-hcol-body{padding:6px 8px;flex:1;min-height:0;overflow:auto} .rt-hcol-hit{display:flex;align-items:center;gap:5px;font-size:11px;margin-bottom:3px} .rt-hcol-hit.off{opacity:.6} .rt-hcol-pts{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;font-weight:700;color:#065f46;background:#a7f3d0; border-radius:7px;padding:0 5px;flex:none} .rt-hcol-hit.off .rt-hcol-pts{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)} .rt-hcol-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-hcol-none{font-size:11px;color:var(--dsw-alias-label-secondary)} .rt-hcol-secs{display:flex;flex-wrap:wrap;gap:3px;margin-top:6px} .rt-hcol-sec{font-size:10px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1); border-radius:4px;padding:0 4px} .rt-hcol-attck{font-family:ui-monospace,Menlo,monospace;font-size:9.5px;color:var(--dsw-alias-label-secondary); padding:4px 8px;border-top:1px solid var(--dsw-alias-border-l1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-ap-cum{font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap} .rt-ap-sub{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);margin:6px 0 3px} .rt-ap-assets,.rt-ap-tunnels{display:flex;flex-direction:column;gap:3px} .rt-ap-asset{display:flex;align-items:center;gap:6px;font-size:11.5px;padding:3px 6px;border-radius:5px; background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)} .rt-ap-tunnel{display:flex;align-items:center;gap:6px;font-size:11.5px;padding:4px 7px;border-radius:5px; background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-left:1px solid var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b))} .rt-hcol-sub{font-size:10.5px;color:var(--dsw-alias-label-secondary);margin-bottom:2px; overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:flex;gap:5px;align-items:center} .rt-hcol-sub.off{opacity:.6} .rt-hit-row{display:flex;flex-wrap:wrap;align-items:center;gap:2px 7px;font-size:11.5px;padding:4px 7px;border-radius:5px; background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);margin-bottom:3px} /* 资产与内容都自适应换行:长域名/长口令/长结果一律换行显示,不截断成省略号 */ .rt-hit-asset{font-family:ui-monospace,Menlo,monospace;font-weight:600;flex:0 1 auto;max-width:100%; overflow-wrap:anywhere;word-break:break-word} .rt-hit-txt{flex:1 1 100%;color:var(--dsw-alias-label-primary);line-height:1.55; white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word} .rt-hit-txt.none{color:var(--dsw-alias-state-error-primary)} .rt-hit-row .rt-hit-time{flex:none;margin-left:auto} /* 自己注册/自建的账号:留痕但不计分,整行压暗 */ .rt-hit-row.self-created{opacity:.72;border-left:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent)} /* 同一资产同一端口的重复账号/库权限:服务已拿满,不计分(只作留痕) */ .rt-hit-row.service-capped{opacity:.72;border-left:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warn-primary,#f59e0b)) 53%,transparent)} .rt-hit-row.service-capped .rt-hit-idx{background:var(--dsw-alias-state-warn-primary,#f59e0b)} .rt-rep-group{margin-bottom:14px} .rt-rep-stage{display:flex;align-items:center;gap:8px;padding:7px 10px;margin-bottom:7px; background:var(--dsw-alias-bg-layer-2);border-left:1px solid var(--dsw-alias-border-l3,var(--dsw-alias-border-l3,#64748b));border-radius:6px; cursor:pointer;outline:none;user-select:none} .rt-rep-stage:hover{background:var(--dsw-alias-bg-layer-1)} .rt-rep-stage:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-rep-no{width:20px;height:20px;border-radius:5px;color:#fff;font-size:11px;font-weight:700;flex:none; display:inline-flex;align-items:center;justify-content:center} .rt-rep-stage-name{font-weight:700;font-size:13px} .rt-rep-stage-n{font-size:11px;color:var(--dsw-alias-label-secondary)} /* 折叠后仍要能一眼看到"这一阶段拿了多少分",所以分数留在头上 */ .rt-rep-pts{font-size:11px;font-weight:600;padding:1px 6px;border-radius:5px;border:1px solid transparent;flex:none} .rt-rep-body{padding-left:6px} /* ── 知识库(POC/EXP) ──────────────────────────────────────────── */ .rt-kb-filter{display:flex;align-items:center;gap:7px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l1)} /* 知识库归类总览:一行标签,点一下按该类筛选 */ .rt-kb-cats{display:flex;flex-wrap:wrap;gap:6px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l1)} /* 知识库分组标题(按归类分组时每组一条) */ .rt-kb-cat{display:flex;align-items:center;gap:7px;padding:7px 10px;background:var(--dsw-alias-bg-layer-2); border-bottom:1px solid var(--dsw-alias-border-l1);position:sticky;top:0;z-index:1} .rt-kb-cat-name{font-weight:600;font-size:12.5px} /* 技能可用性徽章(技能库页签) */ .rt-avail{display:inline-block;padding:0 5px;border-radius:4px;font-size:10.5px;white-space:nowrap; border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)} .rt-avail-available{color:var(--dsw-alias-state-success-primary,#22c55e);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 53%,transparent)} .rt-avail-broken{color:var(--dsw-alias-state-error-primary,#ef4444);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,var(--dsw-alias-state-error-primary,#ef4444)) 53%,transparent)} .rt-avail-unknown{color:#94a3b8;border-color:#94a3b855;background:#94a3b81a} /* 版本 / 更新弹窗 */ .rt-modal{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center} .rt-modal-box{width:min(560px,92vw);max-height:80vh;overflow:auto;background:var(--dsw-alias-bg-layer-1); border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:14px 16px;font-size:12.5px;line-height:1.7; box-shadow:0 18px 48px rgba(0,0,0,.35);color:var(--dsw-alias-label-primary)} .rt-modal-box .rt-kv b{min-width:76px} .rt-kb-check{display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer} .rt-kb{border:1px solid var(--dsw-alias-border-l1);border-radius:7px;margin:0 0 8px;overflow:hidden; background:var(--dsw-alias-bg-layer-2)} .rt-kb.open{border-color:var(--dsw-alias-brand-primary)} .rt-kb-head{display:flex;align-items:center;gap:7px;padding:7px 9px;cursor:pointer;outline:none;flex-wrap:wrap} .rt-kb-head:hover{background:var(--dsw-alias-bg-layer-1)} .rt-kb-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px} .rt-kb-title{font-weight:600;font-size:12.5px;overflow-wrap:anywhere} .rt-kb-kind{font-size:10px;font-weight:700;letter-spacing:.4px;padding:1px 5px;border-radius:4px; color:#fff;background:var(--dsw-alias-border-l3,#64748b);flex:none} .rt-kb-kind.k-exp{background:var(--dsw-alias-state-error-primary,#ef4444)} .rt-kb-kind.k-poc{background:var(--dsw-alias-state-warn-primary,#f59e0b)} .rt-kb-kind.k-template{background:#8b5cf6} .rt-kb-kind.k-script{background:#0ea5e9} .rt-kb-kind.k-payload{background:var(--dsw-alias-state-success-primary,#22c55e)} .rt-kb-sub{font-size:11px;color:var(--dsw-alias-label-secondary);padding:0 9px 7px;overflow-wrap:anywhere} .rt-kb-body{padding:0 9px 9px} .rt-kb-actions{display:flex;gap:6px;margin:7px 0} .rt-kb-body .rt-kv span{overflow-wrap:anywhere;word-break:break-word} /* 本机 nuclei 模板命中:路径要能完整看到(复制成命令直接跑) */ .rt-kb-tpl{margin-top:12px;border-top:1px dashed var(--dsw-alias-border-l1);padding-top:8px} .rt-kb-tpl-row{display:flex;align-items:center;gap:7px;font-size:11.5px;padding:3px 7px;border-radius:5px; background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);margin-bottom:3px} .rt-kb-tpl-path{flex:0 1 auto;font-weight:600;overflow-wrap:anywhere} .rt-kb-tpl-name{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere} .rt-md{flex:1;overflow:auto;margin:0;padding:14px 16px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px; line-height:1.65;white-space:pre-wrap;word-break:break-word;background:var(--dsw-alias-bg-base)} .rt-weblink{display:block;font-size:11.5px;margin-top:1px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .rt-shots{margin:8px 0 4px;padding:7px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px; background:var(--dsw-alias-bg-layer-1)} .rt-shots-head{display:flex;align-items:baseline;gap:8px;font-size:11.5px;margin-bottom:6px} .rt-shots-n{color:var(--dsw-alias-label-secondary)} .rt-shot-row{display:flex;gap:8px;flex-wrap:wrap} .rt-shot{display:flex;flex-direction:column;gap:3px;max-width:220px} .rt-shot-thumb{border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:#0b1020; object-fit:contain;display:block;width:auto;max-width:216px} .rt-shot-loading{display:flex;align-items:center;justify-content:center;width:150px;font-size:11px; color:var(--dsw-alias-label-secondary)} .rt-shot-cap{display:flex;align-items:center;gap:5px;font-size:10.5px;overflow-wrap:anywhere} .rt-shot-label{color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:170px} .rt-shot-zoom{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.82);display:flex;flex-direction:column; align-items:center;justify-content:center;gap:10px;cursor:zoom-out;padding:18px} .rt-shot-zoom img{max-width:96vw;max-height:86vh;border-radius:6px;box-shadow:0 18px 48px rgba(0,0,0,.5)} .rt-shot-zoom-cap{color:#e5e7eb;font-size:12px;text-align:center;max-width:90vw;overflow-wrap:anywhere}` /* ---------------------------------------------------------- 桥接与状态 */ /** 是否在「全面浏览」独立窗口里(URL hash 标记,复用同一套界面代码)。 */ const isFullWindow = () => { try { return String(window.location.hash || '') === '#redteam-full' } catch { return false } } const api = (req) => fetch('/redteam/api', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(req), }).then((res) => res.json()) let ui = { open: true, tab: 'assets' } const subs = new Set() const setUI = (patch) => { ui = Object.assign({}, ui, patch) const snap = Object.assign({}, ui) for (const f of Array.from(subs)) { try { f(snap) } catch { /* ignore subscriber errors */ } } } /* 保留导出。面板尺寸只写在自己的元素上,不再改官方 frame 的宽度。 */ let dockWidthTag = null const setDockWidth = () => {} const useUI = () => { const [st, setSt] = React.useState(() => Object.assign({}, ui)) React.useEffect(() => { const f = (snap) => setSt(Object.assign({}, snap || ui)) subs.add(f) setSt(Object.assign({}, ui)) return () => { subs.delete(f) } }, []) return st } /* 知识库归类(与 core.js 的 POC_CATEGORIES 一致;面板按它分组) */ const POC_CAT_NAME = { rce: '远程命令执行', deserialization: '反序列化', 'file-upload': '文件上传 getshell', sqli: 'SQL 注入', unauthorized: '未授权访问', 'auth-bypass': '认证绕过 / 越权', 'weak-password': '弱口令 / 爆破', ssrf: 'SSRF', xxe: 'XXE', 'path-traversal': '目录穿越 / 任意文件读', 'info-leak': '信息泄露', privesc: '提权 / 横向', tunnel: '隧道 / 代理', other: '其它', } const POC_CAT_ORDER = Object.keys(POC_CAT_NAME) /* 角色 code → 中文(报告/知识库都要标"谁发现的") */ const ROLE_LABEL = { plan: '主会话(指挥)', recon: '信息收集', assess: '资产梳理', 'vuln-scan': '漏洞发现', exploit: '漏洞利用', internal: '内网渗透', } const fmt = (s) => (s ? String(s).replace('T', ' ').slice(0, 16) : '—') const shotCache = new Map() const shotLoading = new Map() const shotSrc = (eng, id) => shotCache.get(eng + ':' + id) || null const loadShot = (eng, id) => { const key = eng + ':' + id if (shotCache.has(key) || shotLoading.has(key)) return const p = api({ op: 'shotData', engagement: eng, id: id }).then((r) => { shotCache.set(key, r && r.base64 ? 'data:' + (r.mime || 'image/png') + ';base64,' + r.base64 : '') shotLoading.delete(key) const snap = Object.assign({}, ui) for (const f of Array.from(subs)) { try { f(snap) } catch { /* ignore subscriber errors */ } } }).catch(() => { shotLoading.delete(key) }) shotLoading.set(key, p) } const SHOT_KIND = { screenshot: '现场截图', card: '记录生成的证据摘录' } const ShotImage = ({ eng, shot, height }) => { const [zoom, setZoom] = React.useState(false) const src = shotSrc(eng, shot.id) if (src === null) { loadShot(eng, shot.id); } const card = shot.kind === 'card' return h('div', { className: 'rt-shot' }, src === null ? h('div', { className: 'rt-shot-thumb rt-shot-loading', style: { height: (height || 96) + 'px' } }, '载入中…') : h('img', { className: 'rt-shot-thumb', src: src, alt: shot.label || ('证据 ' + shot.id), style: { height: (height || 96) + 'px', cursor: 'zoom-in' }, onClick: () => setZoom(true), }), h('div', { className: 'rt-shot-cap' }, h('span', { className: 'rt-tag ' + (card ? 'rt-tag-warn' : 'rt-tag-live'), title: card ? '由当日记录生成的证据摘录,不是现场截图 —— 报告里同样会标注' : '现场截图' }, SHOT_KIND[shot.kind] || shot.kind || '证据'), h('span', { className: 'rt-shot-label', title: shot.label || shot.note || '' }, shot.label || shot.target || ('#' + shot.id))), zoom ? h('div', { className: 'rt-shot-zoom', onClick: () => setZoom(false) }, h('img', { src: src, alt: shot.label || '' }), h('div', { className: 'rt-shot-zoom-cap' }, (shot.label || shot.target || '') + (card ? ' · 由当日记录生成(非现场截图)' : '') + (shot.captured_at ? ' · ' + fmt(shot.captured_at) : '') + ' · 点击任意处关闭')) : null) } const ShotStrip = ({ eng, shots, total, title }) => { const list = shots || [] if (list.length === 0) return null return h('div', { className: 'rt-shots' }, h('div', { className: 'rt-shots-head' }, h('b', null, title || '证据截图'), h('span', { className: 'rt-shots-n' }, '显示 ' + list.length + ' 张' + (total && total > list.length ? '(该得分点共 ' + total + ' 张,取代表图)' : ''))), h('div', { className: 'rt-shot-row' }, list.map((sh) => h(ShotImage, { key: 'sh' + sh.id, eng: eng, shot: sh })))) } /** 发现时间在列表里只留「月-日 时:分」:整行宽度紧张,完整时间进悬浮提示 */ const fmtShort = (s) => { const t = String(s || '').replace('T', ' ') return t.length >= 16 ? t.slice(5, 16) : (t || '—') } const provLabel = (p) => (p === 'passive' ? '被动' : p === 'active' ? '主动' : '未知') /** * 给「可点击但不是