const PATH = 'data/third/node-configurator' const ProxyUtilsFile = PATH + '/proxy-utils.esm.mjs' let produce /** @type {EsmPlugin} */ export default async (Plugin) => { await loadModule().catch(() => {}) const Update = async () => { const { body } = await Plugins.HttpGet('https://api.github.com/repos/sub-store-org/Sub-Store/releases/latest') const url = body.assets.find((v) => v.uploader.login === 'github-actions[bot]' && v.name === 'proxy-utils.esm.mjs')?.browser_download_url if (!url) { Plugins.message.error('未找到依赖: proxy-utils.esm.mjs') return } await Plugins.Download(url, ProxyUtilsFile) await loadModule() Plugins.message.success('更新成功') } const onInstall = async () => { await Update() } const onUninstall = async () => { await Plugins.RemoveFile(PATH) } const onRun = async () => { openProxyConfigurator(Plugin) return 0 } return { onInstall, onUninstall, onRun, Update } } const loadModule = async () => { const source = await Plugins.ReadFile(ProxyUtilsFile) const blob = new Blob([source], { type: 'text/javascript' }) const url = URL.createObjectURL(blob) try { ;({ produce } = await import(url)) } finally { URL.revokeObjectURL(url) } } const PROTOCOL_OPTIONS = [ { label: 'Shadowsocks', value: 'ss' }, { label: 'VMess', value: 'vmess' }, { label: 'VLESS', value: 'vless' }, { label: 'Trojan', value: 'trojan' }, { label: 'Hysteria2', value: 'hysteria2' }, { label: 'TUIC', value: 'tuic' }, { label: 'HTTP / HTTPS', value: 'http' }, { label: 'SOCKS5', value: 'socks5' }, { label: 'WireGuard', value: 'wireguard' } ] const NETWORK_OPTIONS = [ { label: 'TCP', value: 'tcp' }, { label: 'WebSocket', value: 'ws' }, { label: 'HTTP', value: 'http' }, { label: 'HTTP/2', value: 'h2' }, { label: 'gRPC', value: 'grpc' }, { label: 'XHTTP', value: 'xhttp' } ] const SECURITY_OPTIONS = [ { label: '无', value: 'none' }, { label: 'TLS', value: 'tls' }, { label: 'Reality', value: 'reality' } ] const JSON_SCHEMA = { $schema: 'https://json-schema.org/draft/2020-12/schema', title: 'Proxy Parse Input Schema', type: 'object', required: ['type', 'name', 'server', 'port'], properties: { type: { title: '代理协议', description: '切换协议后,表单会自动显示该协议相关字段。', type: 'string', enum: PROTOCOL_OPTIONS.map((item) => item.value), default: 'vless', 'ui:component': 'Select', 'ui:options': PROTOCOL_OPTIONS }, name: { title: '节点名称', type: 'string', default: 'My_Proxy', 'ui:component': 'Input' }, server: { title: '服务器地址', type: 'string', default: 'example.com', 'ui:component': 'Input' }, port: { title: '端口', type: 'integer', default: 443, 'ui:component': 'Input' }, uuid: { title: 'UUID', type: 'string', default: '', 'ui:component': 'Input' }, password: { title: '密码', type: 'string', default: '', 'ui:component': 'Input' }, cipher: { title: '加密方式', type: 'string', default: 'auto', 'ui:component': 'Select', 'ui:options': ['auto', 'none', 'aes-128-gcm', 'aes-256-gcm', 'chacha20-ietf-poly1305'].map((v) => ({ label: v, value: v })) }, alterId: { title: 'Alter ID', type: 'integer', default: 0, 'ui:component': 'Input' }, flow: { title: 'VLESS Flow', type: 'string', default: '', 'ui:component': 'Select', 'ui:options': [ { label: '不启用', value: '' }, { label: 'xtls-rprx-vision', value: 'xtls-rprx-vision' } ] }, network: { title: '传输层', type: 'string', default: 'tcp', 'ui:component': 'Select', 'ui:options': NETWORK_OPTIONS }, security: { title: '安全层', type: 'string', default: 'tls', 'ui:component': 'Select', 'ui:options': SECURITY_OPTIONS }, sni: { title: 'SNI / Server Name', type: 'string', default: '', 'ui:component': 'Input' }, skipCertVerify: { title: '跳过证书验证', type: 'boolean', default: false, 'ui:component': 'Switch' }, clientFingerprint: { title: 'TLS 指纹', type: 'string', default: '', 'ui:component': 'Select', 'ui:options': ['', 'chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'].map((v) => ({ label: v || '不设置', value: v })) }, alpn: { title: 'ALPN', description: '多个值用英文逗号分隔,例如 h2,http/1.1。', type: 'string', default: '', 'ui:component': 'Input' }, realityPublicKey: { title: 'Reality Public Key', type: 'string', default: '', 'ui:component': 'Input' }, realityShortId: { title: 'Reality Short ID', type: 'string', default: '', 'ui:component': 'Input' }, transportPath: { title: '传输路径 / Service Name', type: 'string', default: '/', 'ui:component': 'Input' }, transportHost: { title: '传输 Host / Authority', type: 'string', default: '', 'ui:component': 'Input' }, xhttpMode: { title: 'XHTTP 模式', type: 'string', default: 'auto', 'ui:component': 'Select', 'ui:options': ['auto', 'packet-up', 'stream-up', 'stream-one'].map((v) => ({ label: v, value: v })) }, ssPlugin: { title: 'SS 插件', type: 'string', default: '', 'ui:component': 'Select', 'ui:options': [ { label: '不启用', value: '' }, { label: 'obfs', value: 'obfs' }, { label: 'v2ray-plugin', value: 'v2ray-plugin' } ] }, ssPluginMode: { title: '插件模式', type: 'string', default: 'websocket', 'ui:component': 'Select', 'ui:options': ['http', 'tls', 'websocket'].map((v) => ({ label: v, value: v })) }, obfs: { title: '混淆类型', type: 'string', default: '', 'ui:component': 'Select', 'ui:options': [ { label: '不启用', value: '' }, { label: 'salamander', value: 'salamander' } ] }, obfsPassword: { title: '混淆密码', type: 'string', default: '', 'ui:component': 'Input' }, ports: { title: '端口跳跃', description: '例如 20000-50000 或 1000,2000。', type: 'string', default: '', 'ui:component': 'Input' }, hopInterval: { title: '端口跳跃间隔', description: '支持数字或 15-30 区间。', type: 'string', default: '', 'ui:component': 'Input' }, congestionControl: { title: '拥塞控制', type: 'string', default: 'bbr', 'ui:component': 'Select', 'ui:options': ['bbr', 'cubic', 'new_reno'].map((v) => ({ label: v, value: v })) }, udpRelayMode: { title: 'UDP Relay Mode', type: 'string', default: 'native', 'ui:component': 'Select', 'ui:options': ['native', 'quic'].map((v) => ({ label: v, value: v })) }, username: { title: '用户名', type: 'string', default: '', 'ui:component': 'Input' }, tls: { title: '启用 TLS', type: 'boolean', default: false, 'ui:component': 'Switch' }, privateKey: { title: 'Private Key', type: 'string', default: '', 'ui:component': 'Input' }, publicKey: { title: 'Peer Public Key', type: 'string', default: '', 'ui:component': 'Input' }, presharedKey: { title: 'Preshared Key', type: 'string', default: '', 'ui:component': 'Input' }, ip: { title: '本机 IP/CIDR', description: '多个地址用逗号分隔。', type: 'string', default: '', 'ui:component': 'Input' }, dns: { title: 'DNS', description: '多个 DNS 用逗号分隔。', type: 'string', default: '', 'ui:component': 'Input' }, mtu: { title: 'MTU', type: 'integer', default: '', 'ui:component': 'Input' }, reserved: { title: 'Reserved', description: 'WireGuard reserved 数组,例如 1,2,3。', type: 'string', default: '', 'ui:component': 'Input' }, udp: { title: 'UDP', type: 'boolean', default: true, 'ui:component': 'Switch' }, tfo: { title: 'TCP Fast Open', type: 'boolean', default: false, 'ui:component': 'Switch' } }, allOf: [ { if: { properties: { type: { const: 'ss' } } }, then: { required: ['cipher', 'password'] } }, { if: { properties: { type: { const: 'vmess' } } }, then: { required: ['uuid'] } }, { if: { properties: { type: { const: 'vless' } } }, then: { required: ['uuid'] } }, { if: { properties: { type: { const: 'trojan' } } }, then: { required: ['password'] } }, { if: { properties: { type: { const: 'hysteria2' } } }, then: { required: ['password'] } }, { if: { properties: { type: { const: 'tuic' } } }, then: { required: ['uuid', 'password'] } }, { if: { properties: { type: { const: 'wireguard' } } }, then: { required: ['privateKey', 'publicKey', 'ip'] } } ] } const COMMON_FIELDS = ['type', 'name', 'server', 'port'] const TYPE_FIELDS = { ss: ['cipher', 'password', 'ssPlugin', 'udp', 'tfo'], vmess: ['uuid', 'cipher', 'alterId', 'network', 'security', 'udp', 'tfo'], vless: ['uuid', 'flow', 'network', 'security', 'udp', 'tfo'], trojan: ['password', 'network', 'security', 'udp', 'tfo'], hysteria2: ['password', 'obfs', 'ports', 'hopInterval', 'sni', 'skipCertVerify', 'alpn', 'udp'], tuic: ['uuid', 'password', 'congestionControl', 'udpRelayMode', 'sni', 'skipCertVerify', 'alpn'], http: ['username', 'password', 'tls', 'skipCertVerify'], socks5: ['username', 'password', 'tls', 'udp'], wireguard: ['privateKey', 'publicKey', 'presharedKey', 'ip', 'dns', 'mtu', 'reserved', 'udp'] } const TRANSPORT_FIELDS = ['transportPath', 'transportHost'] const SECURITY_FIELDS = ['sni', 'skipCertVerify', 'clientFingerprint', 'alpn'] const REALITY_FIELDS = ['sni', 'clientFingerprint', 'realityPublicKey', 'realityShortId'] const DEFAULT_FORM = Object.fromEntries(Object.entries(JSON_SCHEMA.properties).map(([key, schema]) => [key, cloneDefault(schema.default)])) const TYPE_DEFAULTS = { ss: { port: 8388, cipher: 'chacha20-ietf-poly1305', security: 'none', network: 'tcp' }, vmess: { port: 443, cipher: 'auto', alterId: 0, network: 'ws', security: 'tls', transportPath: '/', udp: true }, vless: { port: 443, network: 'ws', security: 'tls', transportPath: '/', udp: true }, trojan: { port: 443, network: 'tcp', security: 'tls', udp: true }, hysteria2: { port: 443, security: 'tls', udp: true }, tuic: { port: 443, security: 'tls', congestionControl: 'bbr', udpRelayMode: 'native' }, http: { port: 8080, tls: false, security: 'none', udp: false }, socks5: { port: 1080, tls: false, security: 'none', udp: true }, wireguard: { port: 51820, udp: true, security: 'none' } } const PRODUCE_TARGET_OPTIONS = [ { label: 'Mihomo', value: 'mihomo' }, { label: 'sing-box', value: 'sing-box' }, { label: 'V2Ray Base64', value: 'v2ray' }, { label: 'URI 分享链接', value: 'URI' }, { label: 'Shadowrocket', value: 'Shadowrocket' }, { label: 'Quantumult X', value: 'QX' }, { label: 'Loon', value: 'Loon' }, { label: 'Surge', value: 'Surge' }, { label: 'JSON', value: 'JSON' } ] function openProxyConfigurator(Plugin) { const { h, ref, reactive, computed, watch, defineComponent } = Vue const state = reactive({ ...DEFAULT_FORM }) applyTypeDefaults(state.type, state) const defaultTarget = getDefaultProduceTarget() const produceTarget = ref(defaultTarget) const outputFormat = ref(getDefaultOutputFormat(produceTarget.value)) const rawJson = ref(JSON.stringify(buildProxy(state), null, 2)) const syncRawWithForm = ref(true) const generatedJson = computed(() => JSON.stringify(buildProxy(state), null, 2)) const validation = computed(() => validateState(state)) const activeKeys = computed(() => getActiveKeys(state)) const activeFields = computed(() => activeKeys.value.map((key) => ({ key, schema: JSON_SCHEMA.properties[key] })).filter((item) => item.schema)) const producePreview = computed(() => buildProducePreview(rawJson.value, produceTarget.value, outputFormat.value)) watch( () => state.type, (type) => applyTypeDefaults(type, state) ) watch(generatedJson, (value) => { if (syncRawWithForm.value) rawJson.value = value }) const component = defineComponent({ template: /* html */ `
JSON Schema Driven
节点配置器
根据协议生成 ProxyUtils 可识别的 JSON 节点,并实时 produce 到目标客户端。
{{ field.schema.title }} 必填
{{ field.schema.description }}
需要补齐
{{ item }}
当前表单已满足 schema 必填规则,当前 json 可直接作为 Sub-Store 输入。
支持单个节点对象、节点数组,或 { "proxies": [...] }。手动编辑后会立刻驱动下方 produce 预览。
`, setup() { const produceTargetOptions = PRODUCE_TARGET_OPTIONS const outputFormatOptions = [ { label: 'YAML', value: 'yaml' }, { label: 'JSON', value: 'json' } ] const produceLang = computed(() => outputFormat.value) const isRequired = (key) => getRequiredKeys(state).includes(key) const markRawEdited = () => { syncRawWithForm.value = rawJson.value === generatedJson.value } const useFormJson = () => { syncRawWithForm.value = true rawJson.value = generatedJson.value Plugins.message.info('parse 输入已重新跟随表单') } const copyProduceOutput = async () => { await Plugins.ClipboardSetText(producePreview.value) Plugins.message.success('已复制 produce 输出') } return { state, activeFields, validation, rawJson, produceTarget, produceTargetOptions, outputFormat, outputFormatOptions, producePreview, produceLang, syncRawWithForm, isRequired, markRawEdited, useFormJson, copyProduceOutput } } }) const modal = Plugins.modal( { title: Plugin?.name || '节点配置器', width: '92', height: '88', submit: false, cancelText: '关闭', afterClose: () => modal.destroy() }, { default: () => h(component) } ) injectStyle() modal.open() } function getActiveKeys(state) { const keys = [...COMMON_FIELDS, ...(TYPE_FIELDS[state.type] || [])] if (['vmess', 'vless', 'trojan'].includes(state.type) && state.network && state.network !== 'tcp') { keys.push(...TRANSPORT_FIELDS) } if (state.network === 'xhttp') { keys.push('xhttpMode') } if (['vmess', 'vless', 'trojan'].includes(state.type)) { if (state.security === 'tls') keys.push(...SECURITY_FIELDS) if (state.security === 'reality') keys.push(...REALITY_FIELDS) } if (state.type === 'ss' && state.ssPlugin) { keys.push('ssPluginMode', 'transportHost', 'transportPath') } if (state.type === 'hysteria2' && state.obfs) { keys.push('obfsPassword') } return [...new Set(keys)] } function getRequiredKeys(state) { const required = [...JSON_SCHEMA.required] const matched = JSON_SCHEMA.allOf.find((rule) => rule.if?.properties?.type?.const === state.type) if (matched?.then?.required) required.push(...matched.then.required) if (state.type === 'ss' && state.ssPlugin) required.push('transportHost') if (state.type === 'hysteria2' && state.obfs) required.push('obfsPassword') if (state.security === 'reality') required.push('realityPublicKey') return [...new Set(required)].filter((key) => getActiveKeys(state).includes(key)) } function validateState(state) { const errors = [] for (const key of getRequiredKeys(state)) { const value = state[key] if (value == null || value === '' || (Array.isArray(value) && value.length === 0)) { errors.push(`${JSON_SCHEMA.properties[key]?.title || key} 不能为空`) } } const port = Number(state.port) if (!Number.isInteger(port) || port < 1 || port > 65535) errors.push('端口必须是 1-65535 的整数') return { valid: errors.length === 0, errors } } function buildProxy(state) { const proxy = pickBase(state) if (state.type === 'ss') { proxy.cipher = text(state.cipher) proxy.password = text(state.password) if (state.ssPlugin) { proxy.plugin = state.ssPlugin proxy['plugin-opts'] = compactObject({ mode: state.ssPluginMode, host: text(state.transportHost), path: normalizePath(state.transportPath), tls: state.ssPlugin === 'v2ray-plugin' && ['tls', 'websocket'].includes(state.ssPluginMode) ? true : undefined }) } } if (state.type === 'vmess') { proxy.uuid = text(state.uuid) proxy.cipher = text(state.cipher) || 'auto' proxy.alterId = toNumber(state.alterId, 0) applyTransport(proxy, state) applySecurity(proxy, state) } if (state.type === 'vless') { proxy.uuid = text(state.uuid) if (state.flow) proxy.flow = text(state.flow) applyTransport(proxy, state) applySecurity(proxy, state) } if (state.type === 'trojan') { proxy.password = text(state.password) applyTransport(proxy, state) applySecurity(proxy, state) } if (state.type === 'hysteria2') { proxy.password = text(state.password) proxy.sni = text(state.sni) || undefined proxy['skip-cert-verify'] = boolOrUndefined(state.skipCertVerify) proxy.alpn = splitCsv(state.alpn) proxy.obfs = text(state.obfs) || undefined proxy['obfs-password'] = state.obfs ? text(state.obfsPassword) : undefined proxy.ports = text(state.ports) || undefined proxy['hop-interval'] = text(state.hopInterval) || undefined } if (state.type === 'tuic') { proxy.uuid = text(state.uuid) proxy.password = text(state.password) proxy.sni = text(state.sni) || undefined proxy['skip-cert-verify'] = boolOrUndefined(state.skipCertVerify) proxy.alpn = splitCsv(state.alpn) proxy['congestion-controller'] = text(state.congestionControl) || undefined proxy['udp-relay-mode'] = text(state.udpRelayMode) || undefined } if (['http', 'socks5'].includes(state.type)) { proxy.username = text(state.username) || undefined proxy.password = text(state.password) || undefined proxy.tls = !!state.tls || undefined proxy['skip-cert-verify'] = state.tls ? boolOrUndefined(state.skipCertVerify) : undefined } if (state.type === 'wireguard') { proxy['private-key'] = text(state.privateKey) proxy['public-key'] = text(state.publicKey) proxy['preshared-key'] = text(state.presharedKey) || undefined proxy.ip = splitCsv(state.ip) proxy.dns = splitCsv(state.dns) proxy.mtu = toNumber(state.mtu) proxy.reserved = splitCsv(state.reserved) .map((v) => Number(v)) .filter((v) => Number.isInteger(v)) if (!proxy.reserved.length) delete proxy.reserved } proxy.udp = state.udp === false ? false : state.udp === true ? true : undefined proxy.tfo = boolOrUndefined(state.tfo) return compactObject(proxy) } function pickBase(state) { return compactObject({ name: text(state.name), type: state.type, server: text(state.server), port: toNumber(state.port) }) } function applyTransport(proxy, state) { proxy.network = state.network || 'tcp' if (!proxy.network || proxy.network === 'tcp') return const host = text(state.transportHost) const path = normalizePath(state.transportPath) if (proxy.network === 'ws') { proxy['ws-opts'] = compactObject({ path, headers: host ? { Host: host } : undefined }) } else if (proxy.network === 'grpc') { proxy['grpc-opts'] = compactObject({ 'grpc-service-name': text(state.transportPath), 'grpc-service-name-separator': undefined }) } else if (proxy.network === 'http') { proxy['http-opts'] = compactObject({ path: path ? [path] : undefined, headers: host ? { Host: [host] } : undefined }) } else if (proxy.network === 'h2') { proxy['h2-opts'] = compactObject({ path, host: host ? [host] : undefined }) } else if (proxy.network === 'xhttp') { proxy['xhttp-opts'] = compactObject({ path, host, mode: state.xhttpMode || undefined }) } } function applySecurity(proxy, state) { if (state.security === 'tls' || state.security === 'reality') { proxy.tls = true proxy.sni = text(state.sni) || undefined proxy['skip-cert-verify'] = boolOrUndefined(state.skipCertVerify) proxy['client-fingerprint'] = text(state.clientFingerprint) || undefined proxy.alpn = splitCsv(state.alpn) } if (state.security === 'reality') { proxy['reality-opts'] = compactObject({ 'public-key': text(state.realityPublicKey), 'short-id': text(state.realityShortId) || undefined }) } } function applyTypeDefaults(type, state) { const defaults = TYPE_DEFAULTS[type] || {} for (const [key, value] of Object.entries(defaults)) { if (state[key] == null || state[key] === '' || key === 'port' || key === 'security' || key === 'network') { state[key] = cloneDefault(value) } } } function compactObject(obj) { for (const key of Object.keys(obj)) { const value = obj[key] if (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) { delete obj[key] } else if (typeof value === 'object' && !Array.isArray(value)) { compactObject(value) if (Object.keys(value).length === 0) delete obj[key] } } return obj } function normalizePath(value) { const path = text(value) if (!path) return undefined if (path === '/') return '/' return path.startsWith('/') ? path : `/${path}` } function splitCsv(value) { return text(value) .split(',') .map((item) => item.trim()) .filter(Boolean) } function toNumber(value, fallback) { if (value === '' || value == null) return fallback const number = Number(value) return Number.isFinite(number) ? number : fallback } function text(value) { return `${value ?? ''}`.trim() } function boolOrUndefined(value) { return value ? true : undefined } function cloneDefault(value) { if (Array.isArray(value)) return [...value] if (value && typeof value === 'object') return { ...value } return value } function getDefaultProduceTarget() { return Plugins.APP_TITLE?.includes('SingBox') ? 'sing-box' : 'mihomo' } function getDefaultOutputFormat(target) { return ['mihomo', 'Clash', 'Surge', 'Loon'].includes(target) ? 'yaml' : 'json' } function normalizeInputProxies(rawJson) { const parsed = JSON.parse(rawJson) if (Array.isArray(parsed)) return parsed if (Array.isArray(parsed?.proxies)) return parsed.proxies if (Array.isArray(parsed?.outbounds)) return parsed.outbounds return [parsed] } function buildProducePreview(rawJson, target, format) { if (!produce) return '请右键更新依赖,下载 proxy-utils.esm.mjs 后即可实时 produce。' try { const proxies = normalizeInputProxies(rawJson) const result = produce(JSON.parse(JSON.stringify(proxies)), target, 'internal', { prettyYaml: true }) return formatProduceResult(result, target, format) } catch (e) { return `输入 JSON 或 produce 失败:${e?.message || e}` } } function formatProduceResult(result, target, format) { if (typeof result === 'string') return format === 'json' ? JSON.stringify(result, null, 2) : result if (format === 'yaml') return Plugins.YAML.stringify(wrapYamlOutput(result, target)) return JSON.stringify(result, null, 2) } function wrapYamlOutput(result, target) { if (!Array.isArray(result)) return result if (['mihomo', 'Clash', 'Surge', 'Loon'].includes(target)) return { proxies: result } return result } function injectStyle() { if (document.getElementById('proxy-protocol-configurator-style')) return const style = document.createElement('style') style.id = 'proxy-protocol-configurator-style' style.textContent = ` .proxy-configurator { color: var(--color-text-1, #1f2937); } .proxy-configurator .hero-card { display: flex; justify-content: space-between; gap: 16px; align-items: center; padding: 18px; border-radius: 18px; background: linear-gradient(135deg, #f7efe2 0%, #e7f4ee 54%, #e6eef8 100%); border: 1px solid rgba(70, 94, 82, 0.18); box-shadow: 0 12px 28px rgba(49, 68, 58, 0.08); } .proxy-configurator .eyebrow { font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; color: #5f7f6c; font-weight: 700; } .proxy-configurator .hero-title { font-size: 24px; line-height: 1.2; font-weight: 800; color: #23352b; margin-top: 4px; } .proxy-configurator .hero-desc { margin-top: 6px; color: #526158; line-height: 1.6; } .proxy-configurator .layout-grid { display: grid; grid-template-columns: minmax(360px, 0.95fr) minmax(420px, 1.05fr); gap: 12px; align-items: start; } .proxy-configurator .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } .proxy-configurator .field-card { padding: 10px; border-radius: 12px; background: rgba(255, 255, 255, 0.66); border: 1px solid rgba(100, 116, 139, 0.18); } .proxy-configurator .field-label { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-weight: 700; margin-bottom: 6px; } .proxy-configurator .field-desc { color: #6b7280; font-size: 12px; line-height: 1.5; margin-bottom: 6px; } .proxy-configurator .notice { border-radius: 12px; padding: 10px 12px; line-height: 1.6; } .proxy-configurator .notice.error { background: #fff1f0; color: #9f2a24; border: 1px solid #ffd2ce; } .proxy-configurator .notice.ok { background: #edf9f1; color: #236b3d; border: 1px solid #cdeed7; } .proxy-configurator .notice-title { font-weight: 800; margin-bottom: 2px; } .proxy-configurator .tip-box { padding: 10px 12px; border-radius: 12px; background: #f8fafc; border: 1px dashed #cbd5e1; color: #475569; line-height: 1.7; } .proxy-configurator code { padding: 1px 5px; border-radius: 5px; background: rgba(15, 23, 42, 0.08); } @media (max-width: 980px) { .proxy-configurator .layout-grid { grid-template-columns: 1fr; } .proxy-configurator .form-grid { grid-template-columns: 1fr; } .proxy-configurator .hero-card { align-items: flex-start; flex-direction: column; } } ` document.head.appendChild(style) }