window.__ModuleLoader__.load({ id: "dsh-trusted-page", factory: (require) => { var module = { exports: {} }; var exports = module.exports; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); /** * dsh-trusted-page —— 浏览器端(设置面板 → 通用 → 「远程受信域名」)。 * * 提供受信域名列表的可视化编辑:增删域名即写入 Host 设置命名空间 * trusted-page.hosts;宿主侧(lib/index.js)随即把它并入 /api 的 * Host/Origin fence(即时生效),并在下次渲染页面时更新受信判定脚本。 * * 只在本机或已受信域名打开的页面里可编辑(settings 持久化前提), * 其它远程页面显示只读提示 —— 与 DSH 的信任模型一致。 */ exports.name = 'dsh-trusted-page-client' // 客户端 cordis 服务依赖: // slots —— 注册设置面板行(settings.general.item 槽位) // settingsScope —— Host 设置命名空间的读写(ui-settings 提供) exports.inject = ['slots', 'settingsScope'] const NAMESPACE = 'trusted-page' // --------------------------------------------------------------------------- // 工具 // --------------------------------------------------------------------------- /** UI 输入的宽松规范化(完整校验在宿主侧 authority.normalizeAuthority)。 */ function normalizeDraft(input) { let value = String(input || '').trim().toLowerCase() value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//, '') value = value.split(/[/?#]/)[0] if (value === '' || /[\s@<>"'`\\]/.test(value)) return '' return value } function isLoopbackHostname(hostname) { if (hostname === 'localhost' || hostname === '[::1]') return true const parts = hostname.split('.') return parts.length === 4 && parts[0] === '127' && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255) } /** 当前页面是否受信(与宿主注入的判定脚本同口径,另加回环直通)。 */ function pageTrusted(hosts) { const hostname = location.hostname if (isLoopbackHostname(hostname)) return true const port = location.port return hosts.some((entry) => { const bracketEnd = entry.lastIndexOf(']') const colon = entry.lastIndexOf(':') if (colon <= bracketEnd) return entry === hostname return entry.slice(0, colon) === hostname && entry.slice(colon + 1) === port }) } // --------------------------------------------------------------------------- // 样式(settings.general.item 行内布局 + DSW 设计令牌) // --------------------------------------------------------------------------- const CSS = [ '.dshtp-row{border-bottom:.5px solid var(--dsw-alias-border-l2);padding:16px 0;display:flex;flex-direction:column;gap:10px}', '.dshtp-head{display:flex;align-items:center;gap:8px}', '.dshtp-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px;flex:1;min-width:0}', '.dshtp-badge{flex:none;font-size:11px;line-height:18px;padding:0 8px;border-radius:9px;border:1px solid var(--dsw-alias-border-l2)}', '.dshtp-badge.is-on{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-success,#3fb950));border-color:currentColor}', '.dshtp-badge.is-off{color:var(--dsw-alias-label-tertiary)}', '.dshtp-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}', '.dshtp-list{display:flex;flex-wrap:wrap;gap:6px}', '.dshtp-list.is-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:22px}', '.dshtp-chip{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary)}', '.dshtp-chipHost{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', '.dshtp-chipX{flex:none;width:18px;height:18px;border:none;background:none;cursor:pointer;border-radius:4px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:18px;padding:0;display:inline-flex;align-items:center;justify-content:center}', '.dshtp-chipX:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-module-platform))}', '.dshtp-add{display:flex;gap:6px}', '.dshtp-input{flex:1;min-width:0;height:28px;padding:0 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);font-size:12px}', '.dshtp-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}', '.dshtp-input::placeholder{color:var(--dsw-alias-label-caption,var(--dsw-alias-label-tertiary))}', '.dshtp-btn{flex:none;height:28px;padding:0 12px;border-radius:6px;border:none;cursor:pointer;background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#0f1115));color:var(--dsw-alias-label-primary-foreground,#fff);font-size:12px;font-weight:500;transition:opacity .15s}', '.dshtp-btn:hover:not([disabled]){opacity:.88}', '.dshtp-btn[disabled]{opacity:.4;cursor:not-allowed}', 'body[data-ds-dark-theme] .dshtp-btn{color:var(--dsw-alias-label-primary-foreground,#0f1115)}', '.dshtp-note{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}', '.dshtp-note.is-ok{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-success,#3fb950))}', '.dshtp-note.is-err{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-danger,#f85149))}', ].join('') function installCss(doc) { const tagId = 'dsh-trusted-page/settings-row.css' if (doc.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return const tag = doc.createElement('style') tag.dataset.plugin = 'dsh-trusted-page' tag.dataset.pluginCss = tagId tag.textContent = CSS doc.head.appendChild(tag) } // --------------------------------------------------------------------------- // 设置面板行组件 // --------------------------------------------------------------------------- /** * 「远程受信域名」设置行。props.scope 由 apply() 经槽位 inject 提供 * (ctx.settingsScope.bind({ namespace: 'trusted-page' }))。 * 纯 React.createElement,无 JSX / 构建步骤。 */ function TrustedHostsRow({ scope }) { const React = require('react') const h = React.createElement // SettingsScopeController 的 subscribe/getSnapshot 是实例方法(依赖 this), // 不能作裸函数引用传给 useSyncExternalStore,需包一层保持 this 绑定。 const snap = React.useSyncExternalStore( (listener) => scope.subscribe(listener), () => scope.getSnapshot(), ) const [draft, setDraft] = React.useState('') const [busy, setBusy] = React.useState(false) const [note, setNote] = React.useState(null) // { kind: 'ok' | 'err' | 'info', text } const hosts = Array.isArray(snap.value?.hosts) ? snap.value.hosts : [] const trusted = pageTrusted(hosts) const write = async (next) => { setBusy(true) setNote(null) try { await scope.set('hosts', next) setNote({ kind: 'ok', text: '已保存,刷新远程页面后生效' }) } catch (error) { setNote({ kind: 'err', text: String(error?.message || error) }) } finally { setBusy(false) } } const addDraft = () => { const entry = normalizeDraft(draft) if (entry === '') { setNote({ kind: 'err', text: '请输入有效的域名(如 dsh.example.com)' }) return } if (hosts.includes(entry)) { setDraft('') setNote({ kind: 'info', text: '该域名已在列表中' }) return } setDraft('') void write([...hosts, entry]) } const badge = h('span', { className: 'dshtp-badge ' + (trusted ? 'is-on' : 'is-off'), }, trusted ? '当前页面:已受信' : '当前页面:未受信') if (snap.status !== 'ready') { return h('div', { className: 'dshtp-row' }, h('div', { className: 'dshtp-head' }, h('div', { className: 'dshtp-title' }, '远程受信域名'), badge), h('div', { className: 'dshtp-desc' }, snap.status === 'loading' ? '正在读取设置…' : '请从本机或已受信域名访问后再进行配置。')) } const disabled = busy || snap.writable !== true return h('div', { className: 'dshtp-row' }, h('div', { className: 'dshtp-head' }, h('div', { className: 'dshtp-title' }, '远程受信域名'), badge), h('div', { className: 'dshtp-desc' }, '允许通过自定义域名远程访问 DSH。添加后可正常使用设置与模型等全部功能。'), hosts.length === 0 ? h('div', { className: 'dshtp-list is-empty' }, '暂无受信域名(默认仅允许本机访问)') : h('div', { className: 'dshtp-list' }, hosts.map((entry) => h('span', { key: entry, className: 'dshtp-chip', title: entry }, h('span', { className: 'dshtp-chipHost' }, entry), h('button', { type: 'button', className: 'dshtp-chipX', 'aria-label': '移除 ' + entry, disabled, onClick: () => void write(hosts.filter((item) => item !== entry)), }, '×')))), h('div', { className: 'dshtp-add' }, h('input', { className: 'dshtp-input', type: 'text', value: draft, placeholder: '输入域名,如 dsh.example.com', disabled, onChange: (event) => setDraft(event.target.value), onKeyDown: (event) => { if (event.key === 'Enter') { event.preventDefault() addDraft() } }, }), h('button', { type: 'button', className: 'dshtp-btn', disabled: disabled || draft.trim() === '', onClick: addDraft }, '添加')), h('div', { className: 'dshtp-note' + (note ? ' is-' + (note.kind === 'ok' ? 'ok' : note.kind === 'err' ? 'err' : 'info') : '') }, note?.text ?? '支持输入域名或带端口的地址,添加后刷新远程页面生效。')) } // --------------------------------------------------------------------------- // 插件装配 // --------------------------------------------------------------------------- exports.apply = function (ctx) { ctx.effect(() => { installCss(document) const scope = ctx.settingsScope.bind({ namespace: NAMESPACE }) const disposeSlot = ctx.slots.inject('settings.general.item', () => ctx.slots.register({ name: 'settings.general.item', id: 'trusted-page-hosts', order: 22, // 紧随 dsh-mobile-keys 的行(order 21) inject: () => ({ scope }), }, TrustedHostsRow)) return () => { try { disposeSlot?.() } catch { /* 插件卸载时槽位可能已先行释放 */ } } }, 'dsh-trusted-page: settings row') } return module.exports; } });