/** OpenCode Web 内嵌代理的纯函数。 */ export const OPENCODE_EMBED_ROOT = '/__opencode' export const OPENCODE_EMBED_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/ export function openCodeEmbedPrefix(token) { const normalized = String(token || '').trim() if (!OPENCODE_EMBED_TOKEN_PATTERN.test(normalized)) throw new Error('无效的 OpenCode 内嵌令牌') return `${OPENCODE_EMBED_ROOT}/${normalized}` } export function parseOpenCodeEmbedUrl(rawUrl) { let parsed try { parsed = new URL(String(rawUrl || ''), 'http://clawpanel.local') } catch { return null } let decodedPath try { decodedPath = decodeURIComponent(parsed.pathname) } catch { return null } if (decodedPath.includes('\\') || decodedPath.split('/').includes('..')) return null const match = parsed.pathname.match(/^\/__opencode\/([A-Za-z0-9_-]{32,128})(\/.*)?$/) if (!match) return null const upstreamPathname = match[2] || '/' if (upstreamPathname.startsWith('/__api') || upstreamPathname.startsWith('/__opencode')) return null return { token: match[1], prefix: `${OPENCODE_EMBED_ROOT}/${match[1]}`, upstreamPathname, upstreamPath: `${upstreamPathname}${parsed.search}`, } } function bridgeScript(prefix) { const escaped = JSON.stringify(prefix) return `` } function prefixRootReferences(text, prefix) { return text .replace(/((?:src|href|action)=['"])\/(?!\/|__opencode\/)/gi, `$1${prefix}/`) .replace(/url\((['"]?)\/(?!\/|__opencode\/)/gi, `url($1${prefix}/`) } export function rewriteOpenCodeProxyText(input, prefixValue, { contentType = 'text/html' } = {}) { const prefix = openCodeEmbedPrefix(String(prefixValue || '').replace(`${OPENCODE_EMBED_ROOT}/`, '')) let text = String(input ?? '') if (String(contentType).toLowerCase().includes('text/html')) { text = prefixRootReferences(text, prefix) if (!text.includes('__CLAWPANEL_OPENCODE_PREFIX__')) { text = text.replace(/]*)>/i, `${bridgeScript(prefix)}`) } return text } if (String(contentType).toLowerCase().includes('text/css')) return prefixRootReferences(text, prefix) if (String(contentType).toLowerCase().includes('manifest')) return text.replace(/"\/(?!\/|__opencode\/)/g, `"${prefix}/`) return text } export function shouldRewriteOpenCodeResponse(contentType) { const type = String(contentType || '').toLowerCase() return type.includes('text/html') || type.includes('text/css') || type.includes('manifest') } export function openCodeUpstreamHeaders(headers, port, { password = '', websocket = false } = {}) { const result = {} const blocked = new Set([ 'connection', 'content-length', 'cookie', 'host', 'origin', 'referer', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site', 'upgrade', ]) for (const [rawName, value] of Object.entries(headers || {})) { const name = rawName.toLowerCase() if (blocked.has(name) || value === undefined) continue result[name] = value } result.host = `127.0.0.1:${Number(port)}` result.origin = `http://127.0.0.1:${Number(port)}` if (password) result.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` if (websocket) { result.connection = 'Upgrade' result.upgrade = headers?.upgrade || 'websocket' } return result }