const TRACE_TARGETS = [ { id: 'claude-com', label: 'claude.com', url: 'https://claude.com/cdn-cgi/trace' }, { id: 'claude-ai', label: 'claude.ai', url: 'https://claude.ai/cdn-cgi/trace' } ] /** 中国大陆常用时区,命中任一即视为国内系统时区 */ const MAINLAND_TIMEZONES = new Set(['Asia/Shanghai', 'Asia/Urumqi', 'Asia/Chongqing', 'Asia/Harbin', 'Asia/Kashgar', 'Asia/Chungking', 'Asia/PRC']) /** 国内高频、非中文环境较少预装的字体 */ const CN_FONTS = [ { name: 'DengXian', label: '等线 DengXian' }, { name: 'FangSong', label: '仿宋 FangSong' }, { name: 'KaiTi', label: '楷体 KaiTi' }, { name: 'SimSun', label: '宋体 SimSun' }, { name: 'SimHei', label: '黑体 SimHei' }, { name: 'Microsoft YaHei', label: '微软雅黑 Microsoft YaHei' }, { name: 'HarmonyOS Sans SC', label: '鸿蒙 HarmonyOS Sans' }, { name: 'Alibaba PuHuiTi', label: '阿里普惠体 Alibaba PuHuiTi' }, { name: 'MiSans', label: 'MiSans' }, { name: 'WenQuanYi Micro Hei', label: '文泉驿微米黑' } ] const STATUS_META = { risk: { color: 'red', text: '风险' }, pass: { color: 'green', text: '通过' }, unknown: { color: 'default', text: '未知' } } const FLAG_FONT = '32px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif' const FONT_PROBE_TEXT = '中文测试 mmm' const FALLBACK_FONTS = ['monospace', 'sans-serif', 'serif'] const CANVAS_SIZE = 72 const COLOR_THRESHOLD = 24 const LETTER_MISMATCH_THRESHOLD = 0.5 /** @type {EsmPlugin} */ export default (Plugin) => { let closePanel = undefined /* 触发器 手动触发 */ const onRun = () => { openPanel() } /* 菜单 打开面板 */ const openPanel = () => { const { computed, defineComponent, onMounted, ref } = Vue const panelTemplate = /* html */ `
{{ verdict.text }} {{ verdict.hint }}
{{ item.label }}
{{ item.detail }} {{ item.tag }}
{{ item.label }}
{{ item.detail }} {{ statusMeta[item.status].text }}
建议:将系统时区改为 Asia/Singapore 等 UTC+8 服务区内时区,并改用 TUN/软路由的透明代理,避免本机代理与中继被识别。
` const panelSetup = (_props, context) => { const loading = ref(false) const network = ref( TRACE_TARGETS.map((target) => ({ id: target.id, label: target.label, detail: '等待检测', status: 'unknown' })) ) const features = ref([]) const verdict = computed(() => { if (loading.value) { return { color: 'default', text: '检测中', hint: '正在检测当前环境,请稍候…' } } const risks = [...network.value, ...features.value].filter((item) => item.status === 'risk') if (risks.length === 0) { return { color: 'green', text: '安全', hint: '未发现明显的中国用户特征' } } const hint = `命中 ${risks.length} 项中国用户特征` if (risks.length <= 2) { return { color: 'orange', text: '可疑', hint } } return { color: 'red', text: '高风险', hint } }) const formatError = (error) => { if (typeof error === 'string') { return error } if (error instanceof Error) { return error.message } return '未知错误' } const parseTrace = (text) => { const ip = /(?:^|\n)ip=([^\n]+)/.exec(text)?.[1]?.trim() ?? '' const loc = /(?:^|\n)loc=([^\n]+)/.exec(text)?.[1]?.trim().toUpperCase() ?? '' return { ip, loc } } const checkNetwork = async () => { await Promise.all( TRACE_TARGETS.map(async (target) => { const item = network.value.find((entry) => entry.id === target.id) if (!item) { return } try { const { body } = await Plugins.HttpGet(target.url, {}, { Timeout: 8 }) const text = typeof body === 'string' ? body : JSON.stringify(body) const { ip, loc } = parseTrace(text) if (!ip) { item.detail = '未能解析出口地址' item.status = 'unknown' return } item.detail = `${ip}(${loc || '未知地区'})` item.tag = loc || '未知' item.status = loc === 'CN' ? 'risk' : 'pass' } catch (error) { item.detail = `检测失败:${formatError(error)}` item.status = 'unknown' } }) ) } const checkLanguage = () => { const languages = navigator.languages.length > 0 ? [...navigator.languages] : [navigator.language] const zh = languages.filter((lang) => /^zh/i.test(lang)) const item = { id: 'language', label: '浏览器语言', detail: languages.join(', '), status: zh.length > 0 ? 'risk' : 'pass' } if (zh.length > 0) { item.detail = `${languages.join(', ')}(含 ${zh.join('、')})` } return item } const checkTimezone = () => { const { timeZone } = Intl.DateTimeFormat().resolvedOptions() return { id: 'timezone', label: '系统时区', detail: timeZone || '未知时区', status: MAINLAND_TIMEZONES.has(timeZone) ? 'risk' : 'pass' } } const hasColor = (data) => { for (let i = 0; i < data.length; i += 4) { const red = data[i] ?? 0 const green = data[i + 1] ?? 0 const blue = data[i + 2] ?? 0 const alpha = data[i + 3] ?? 0 const saturation = Math.max(red, green, blue) - Math.min(red, green, blue) if (alpha > 0 && saturation > COLOR_THRESHOLD) { return true } } return false } const mismatchRatio = (a, b) => { let counted = 0 let diff = 0 for (let i = 0; i < a.length; i += 4) { const alphaA = a[i + 3] ?? 0 const alphaB = b[i + 3] ?? 0 if (alphaA === 0 && alphaB === 0) { continue } counted += 1 if (a[i] !== b[i] || a[i + 1] !== b[i + 1] || a[i + 2] !== b[i + 2] || alphaA !== alphaB) { diff += 1 } } return counted === 0 ? 1 : diff / counted } const checkFlagEmoji = () => { const item = { id: 'flag', label: '国旗 Emoji', detail: '未检测', status: 'unknown' } const canvas = document.createElement('canvas') canvas.width = CANVAS_SIZE canvas.height = CANVAS_SIZE const ctx = canvas.getContext('2d', { willReadFrequently: true }) if (!ctx) { item.detail = '当前环境不支持 Canvas 检测' return item } const draw = (text) => { ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE) ctx.textBaseline = 'top' ctx.font = FLAG_FONT ctx.fillText(text, 0, 0) return ctx.getImageData(0, 0, CANVAS_SIZE, CANVAS_SIZE).data } const flag = draw('\u{1F1E8}\u{1F1F3}') const letters = draw('CN') if (hasColor(flag)) { item.detail = '国旗以彩色渲染' item.status = 'pass' return item } if (mismatchRatio(flag, letters) < LETTER_MISMATCH_THRESHOLD) { item.detail = '国旗被降级渲染为字母 CN' item.status = 'risk' return item } item.detail = '国旗为单色渲染,未能确认' return item } const checkFonts = () => { const item = { id: 'font', label: '中文字体', detail: '未检测', status: 'unknown' } const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') if (!ctx) { item.detail = '当前环境不支持 Canvas 检测' return item } const measure = (font) => { ctx.font = font return ctx.measureText(FONT_PROBE_TEXT).width } const baseWidths = FALLBACK_FONTS.map((font) => measure(`72px ${font}`)) const found = CN_FONTS.filter((font) => { const width = measure(`72px "${font.name}", monospace`) return baseWidths.every((baseWidth) => Math.abs(width - baseWidth) > 0.1) }) if (found.length === 0) { item.detail = '未检测到国内特有字体' item.status = 'pass' return item } item.detail = `检测到:${found.map((font) => font.label).join('、')}` item.status = 'risk' return item } const buildFeatures = () => [checkLanguage(), checkTimezone(), checkFlagEmoji(), checkFonts()] const runChecks = async () => { loading.value = true network.value = TRACE_TARGETS.map((target) => ({ id: target.id, label: target.label, detail: '检测中…', status: 'unknown' })) features.value = buildFeatures() await checkNetwork() loading.value = false } onMounted(runChecks) /* 声明显示在模态框底栏左侧 */ context?.expose({ modalSlots: { action: () => Vue.h('div', { class: 'mr-auto text-12' }, '测试结果仅供参考,请以实际使用情况为准') } }) return { loading, network, features, verdict, runChecks, statusMeta: STATUS_META } } const component = defineComponent({ template: panelTemplate, setup: panelSetup }) const modal = Plugins.modal({ title: Plugin.name, submit: false, cancelText: '关闭', width: '90', height: '90' }) modal.setContent(component) modal.open() closePanel = () => modal.destroy() } /* 触发器 插件销毁时 */ const onDispose = () => { closePanel?.() closePanel = undefined } return { onRun, onDispose } }