Skip to content

Commit 8a22fcc

Browse files
committed
fix: 保护隐私并改进 AI 诊断的可访问性
1 parent 6383488 commit 8a22fcc

10 files changed

Lines changed: 178 additions & 34 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ The `detect.html` page now includes an AI diagnosis panel. It sends the current
138138

139139
Current implementation notes:
140140

141+
* Querying an external AI provider sends a structured detection snapshot to that provider. This can include fingerprint-related signals such as language settings, locale and timezone data, partial browser fingerprint fields, and redacted WebRTC or header-derived context. Treat AI diagnosis as third-party data sharing and only use providers you trust.
141142
* AI configuration is stored locally in `chrome.storage.local`, grouped by provider
142143
* Replies follow the current detect page language by default
143-
* Exported Markdown only contains visible conversation, excluding hidden system prompt and initial detection snapshot injection messages
144+
* Exported Markdown only contains visible conversation, excluding the hidden system prompt and hidden initial detection snapshot injection messages
144145

145146
***
146147

detect-ai.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,47 @@
6565
const getVisibleChatMessages = () =>
6666
aiSessionState.messages.filter((message) => message.visible !== false);
6767

68+
const sanitizeSnapshotForAI = (snapshot) => {
69+
if (!snapshot) {
70+
return snapshot;
71+
}
72+
73+
const sanitized = JSON.parse(JSON.stringify(snapshot));
74+
75+
if (sanitized.http) {
76+
sanitized.http.headers = {
77+
redacted: true,
78+
headerNames: Object.keys(snapshot.http?.headers || {}),
79+
};
80+
}
81+
82+
if (sanitized.webrtc) {
83+
sanitized.webrtc.ips = Array.isArray(snapshot.webrtc?.ips)
84+
? snapshot.webrtc.ips.map(() => "[redacted]")
85+
: [];
86+
}
87+
88+
if (sanitized.browserFingerprint) {
89+
sanitized.browserFingerprint.userAgent = "[redacted]";
90+
}
91+
92+
if (sanitized.hardwareFingerprint?.canvas) {
93+
sanitized.hardwareFingerprint.canvas.hash = "[redacted]";
94+
}
95+
96+
if (sanitized.hardwareFingerprint?.webgl) {
97+
sanitized.hardwareFingerprint.webgl.hash = "[redacted]";
98+
sanitized.hardwareFingerprint.webgl.vendor = "[redacted]";
99+
sanitized.hardwareFingerprint.webgl.renderer = "[redacted]";
100+
}
101+
102+
if (sanitized.hardwareFingerprint?.audio) {
103+
sanitized.hardwareFingerprint.audio.hash = "[redacted]";
104+
}
105+
106+
return sanitized;
107+
};
108+
68109
const setStatusToneClass = (element, tone) => {
69110
if (!element) {
70111
return;
@@ -602,6 +643,8 @@
602643
};
603644

604645
const buildInitialPrompt = (snapshot) => {
646+
const sanitizedSnapshot = sanitizeSnapshotForAI(snapshot);
647+
605648
if (getUiLanguage() === "zh") {
606649
return [
607650
"请基于下面这份浏览器环境检测快照做一次中性的结果解读。",
@@ -610,7 +653,7 @@
610653
"如果某些信号在检测页里属于常见现象,请明确写出“这是常见现象,不代表真实网站一定存在问题”。",
611654
"不要重复整段 JSON,只提炼真正重要的点。",
612655
"",
613-
JSON.stringify(snapshot, null, 2),
656+
JSON.stringify(sanitizedSnapshot, null, 2),
614657
].join("\n");
615658
}
616659

@@ -621,7 +664,7 @@
621664
"If a signal is common on a detect page, explicitly say it is common and does not automatically mean a real website problem.",
622665
"Do not repeat the full JSON; extract only the important points.",
623666
"",
624-
JSON.stringify(snapshot, null, 2),
667+
JSON.stringify(sanitizedSnapshot, null, 2),
625668
].join("\n");
626669
};
627670

detect.html

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,12 @@
146146
flex-wrap: wrap;
147147
}
148148

149-
.ai-chat-composer textarea {
149+
.ai-chat-composer-main {
150150
flex: 1 1 420px;
151+
}
152+
153+
.ai-chat-composer textarea {
154+
width: 100%;
151155
min-height: 96px;
152156
resize: vertical;
153157
}
@@ -227,11 +231,14 @@ <h4 class="mb-1" id="aiDiagnosisTitle"></h4>
227231
<button class="btn btn-outline-secondary" type="button" id="aiClearButton"></button>
228232
</div>
229233

230-
<div class="ai-chat-messages mb-3" id="aiChatMessages" aria-live="polite"></div>
231-
<div class="small text-muted mb-3" id="aiChatStatus"></div>
234+
<div class="ai-chat-messages mb-3" id="aiChatMessages"></div>
235+
<div class="small text-muted mb-3" id="aiChatStatus" aria-live="polite" role="status"></div>
232236

233237
<div class="ai-chat-composer">
234-
<textarea class="form-control" id="aiUserInput"></textarea>
238+
<div class="ai-chat-composer-main">
239+
<label class="visually-hidden" for="aiUserInput" id="aiUserInputLabel"></label>
240+
<textarea class="form-control" id="aiUserInput"></textarea>
241+
</div>
235242
<div class="ai-chat-composer-actions">
236243
<button class="btn btn-success" type="button" id="aiSendButton"></button>
237244
<button class="btn btn-outline-secondary" type="button" id="aiExportButton"></button>

detect.js

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ const renderJsLanguageInfo = (jsLanguageInfo) => {
310310
const langTitleP = document.createElement("p");
311311
langTitleP.className = "mb-1";
312312
const strongLang = document.createElement("strong");
313-
strongLang.textContent = "navigator.language:";
313+
strongLang.textContent = translateDetect("navigator_language_label");
314314
langTitleP.appendChild(strongLang);
315315
fragment.appendChild(langTitleP);
316316

@@ -322,7 +322,7 @@ const renderJsLanguageInfo = (jsLanguageInfo) => {
322322
const langsTitleP = document.createElement("p");
323323
langsTitleP.className = "mb-1 mt-2";
324324
const strongLangs = document.createElement("strong");
325-
strongLangs.textContent = "navigator.languages:";
325+
strongLangs.textContent = translateDetect("navigator_languages_label");
326326
langsTitleP.appendChild(strongLangs);
327327
fragment.appendChild(langsTitleP);
328328

@@ -393,7 +393,7 @@ const renderCanvasFingerprintInfo = (canvasFingerprintInfo) => {
393393
const hashTitleP = document.createElement("p");
394394
hashTitleP.className = "mb-1";
395395
const strongHash = document.createElement("strong");
396-
strongHash.textContent = "Canvas hash:";
396+
strongHash.textContent = translateDetect("canvas_hash_label");
397397
hashTitleP.appendChild(strongHash);
398398
fragment.appendChild(hashTitleP);
399399

@@ -501,12 +501,18 @@ const renderWebglFingerprintInfo = (webglFingerprintInfo) => {
501501
fragment.appendChild(valP);
502502
};
503503

504-
addDetail("WebGL hash:", webglFingerprintInfo.hash, true, "");
505-
addDetail("WebGL unmasked vendor:", webglFingerprintInfo.vendor);
506-
addDetail("WebGL unmasked renderer:", webglFingerprintInfo.renderer);
507-
addDetail("WebGL version:", webglFingerprintInfo.version);
504+
addDetail(translateDetect("webgl_hash_label"), webglFingerprintInfo.hash, true, "");
508505
addDetail(
509-
"Shading Language Version:",
506+
translateDetect("webgl_unmasked_vendor_label"),
507+
webglFingerprintInfo.vendor,
508+
);
509+
addDetail(
510+
translateDetect("webgl_unmasked_renderer_label"),
511+
webglFingerprintInfo.renderer,
512+
);
513+
addDetail(translateDetect("webgl_version_label"), webglFingerprintInfo.version);
514+
addDetail(
515+
translateDetect("webgl_shading_language_version_label"),
510516
webglFingerprintInfo.shadingLanguageVersion,
511517
);
512518

@@ -599,7 +605,7 @@ const renderAudioFingerprintInfo = (audioFingerprintInfo) => {
599605
const hashTitleP = document.createElement("p");
600606
hashTitleP.className = "mb-1";
601607
const strongHash = document.createElement("strong");
602-
strongHash.textContent = "AudioContext hash:";
608+
strongHash.textContent = translateDetect("audio_context_hash_label");
603609
hashTitleP.appendChild(strongHash);
604610
fragment.appendChild(hashTitleP);
605611

@@ -656,7 +662,7 @@ const renderIntlInfo = (intlInfo) => {
656662
const dtTitleP = document.createElement("p");
657663
dtTitleP.className = "mb-1";
658664
const strongDt = document.createElement("strong");
659-
strongDt.textContent = "DateTimeFormat Locale:";
665+
strongDt.textContent = translateDetect("datetime_format_locale_label");
660666
dtTitleP.appendChild(strongDt);
661667
fragment.appendChild(dtTitleP);
662668

@@ -668,7 +674,7 @@ const renderIntlInfo = (intlInfo) => {
668674
const nfTitleP = document.createElement("p");
669675
nfTitleP.className = "mb-1 mt-2";
670676
const strongNf = document.createElement("strong");
671-
strongNf.textContent = "NumberFormat Locale:";
677+
strongNf.textContent = translateDetect("number_format_locale_label");
672678
nfTitleP.appendChild(strongNf);
673679
fragment.appendChild(nfTitleP);
674680

@@ -693,7 +699,23 @@ const collectWebRtcIps = async () =>
693699
const ips = [];
694700

695701
try {
702+
if (typeof ResourceManager.createRTCPeerConnection !== "function") {
703+
resolve({
704+
unsupported: true,
705+
error: translateDetect("webrtc_not_supported"),
706+
});
707+
return;
708+
}
709+
696710
const pc = ResourceManager.createRTCPeerConnection({ iceServers: [] });
711+
if (!pc) {
712+
resolve({
713+
unsupported: true,
714+
error: translateDetect("webrtc_not_supported"),
715+
});
716+
return;
717+
}
718+
697719
pc.createDataChannel("");
698720

699721
pc.onicecandidate = (event) => {
@@ -719,17 +741,30 @@ const collectWebRtcIps = async () =>
719741

720742
ResourceManager.setTimeout(() => {
721743
ResourceManager.closeRTCPeerConnection(pc);
722-
resolve(ips);
744+
resolve({ ips, unsupported: false, error: "" });
723745
}, 1000);
724746
} catch (error) {
725747
console.error("WebRTC collection error:", error);
726-
resolve([]);
748+
resolve({
749+
unsupported: true,
750+
error: error?.message || String(error),
751+
});
727752
}
728753
});
729754

730755
const collectWebRtcInfo = async () => {
731756
try {
732-
const ips = await collectWebRtcIps();
757+
const result = await collectWebRtcIps();
758+
if (result?.unsupported) {
759+
return {
760+
status: "unsupported",
761+
ips: [],
762+
ipLeakDetected: false,
763+
error: result.error || translateDetect("webrtc_not_supported"),
764+
};
765+
}
766+
767+
const ips = result?.ips || [];
733768
return {
734769
status: ips.length > 0 ? "ok" : "none",
735770
ips,
@@ -754,7 +789,7 @@ const renderWebRtcInfo = (webRtcInfo) => {
754789
webRtcInfoElement.innerHTML = "";
755790
const fragment = document.createDocumentFragment();
756791

757-
if (webRtcInfo.status === "error") {
792+
if (webRtcInfo.status === "error" || webRtcInfo.status === "unsupported") {
758793
const errorP = document.createElement("p");
759794
errorP.className = "text-danger";
760795
errorP.textContent = `${translateDetect("webrtc_not_supported")}: ${webRtcInfo.error}`;
@@ -865,15 +900,15 @@ const renderFingerprintInfo = (fingerprintInfo) => {
865900
fragment.appendChild(valP);
866901
};
867902

868-
addDetail("User Agent:", fingerprintInfo.userAgent, false, "", true);
903+
addDetail(translateDetect("user_agent_label"), fingerprintInfo.userAgent, false, "", true);
869904
addDetail(
870-
"Screen information:",
905+
translateDetect("screen_information_label"),
871906
`${fingerprintInfo.screen.width}x${fingerprintInfo.screen.height}x${fingerprintInfo.screen.colorDepth}`,
872907
true,
873908
);
874909
addDetail(
875-
"Timezone:",
876-
`${fingerprintInfo.timezone} (Offset: ${fingerprintInfo.timezoneOffset})`,
910+
translateDetect("timezone_label"),
911+
`${fingerprintInfo.timezone} (${translateDetect("offset_label")} ${fingerprintInfo.timezoneOffset})`,
877912
true,
878913
);
879914

@@ -896,7 +931,7 @@ const renderCompatibilityInfo = (compatibilityInfo) => {
896931
const apiListEl = document.getElementById("apiCompatibilityList");
897932
if (!browserInfoEl || !apiListEl) return;
898933

899-
browserInfoEl.textContent = `${compatibilityInfo.browser.name} ${compatibilityInfo.browser.fullVersion} on ${compatibilityInfo.browser.os}`;
934+
browserInfoEl.textContent = `${compatibilityInfo.browser.name} ${compatibilityInfo.browser.fullVersion} ${translateDetect("on_connector")} ${compatibilityInfo.browser.os}`;
900935
apiListEl.innerHTML = "";
901936

902937
compatibilityInfo.apiSupport.forEach((api) => {

docs/Project_Structure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ MultiLangSwitcher/
2121
│ ├── shared-language-options.js - 共享语言选项列表
2222
│ ├── header-check-utils.js - 请求头检查工具函数
2323
│ ├── shared-resource-manager.js - 共享资源管理器
24-
│ ├── md5.js - MD5加密算法
24+
│ ├── md5.js - MD5 哈希函数
2525
│ ├── theme-init.js - 主题初始化脚本
2626
│ └── vendor/ - 第三方前端库
2727
│ ├── marked.umd.min.js - Markdown 渲染库

docs/README/README.zh-CN.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,10 @@ MultiLangSwitcher 是一个 Chromium 内核浏览器扩展,帮助用户快速
141141

142142
当前实现要点:
143143

144+
* 调用外部 AI 服务商时,会把结构化检测快照发送给该服务商。这可能包含与指纹相关的信号,例如语言设置、Locale 与时区信息、部分浏览器指纹字段,以及已脱敏的 WebRTC 或请求头上下文。请将 AI 诊断视为一次第三方数据共享,只在信任该服务商时使用。
144145
* AI 配置保存在 `chrome.storage.local`,并按 provider 分桶保存
145146
* 回复默认跟随 detect 页面当前语言
146-
* 导出的 Markdown 只包含可见对话,不包含隐藏的 system prompt 和初始检测快照注入消息
147+
* 导出的 Markdown 只包含可见对话,不包含隐藏的 system prompt 和隐藏的初始检测快照注入消息
147148

148149
***
149150

i18n/detect-en.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ if (typeof detectEn === "undefined") {
2929
partial_fingerprint: "Partial Browser Fingerprint Information",
3030
supported: "Supported",
3131
not_supported: "Not Supported",
32+
navigator_language_label: "navigator.language:",
33+
navigator_languages_label: "navigator.languages:",
34+
canvas_hash_label: "Canvas hash:",
35+
webgl_hash_label: "WebGL hash:",
36+
webgl_unmasked_vendor_label: "WebGL unmasked vendor:",
37+
webgl_unmasked_renderer_label: "WebGL unmasked renderer:",
38+
webgl_version_label: "WebGL version:",
39+
webgl_shading_language_version_label: "Shading Language Version:",
40+
audio_context_hash_label: "AudioContext hash:",
41+
datetime_format_locale_label: "DateTimeFormat Locale:",
42+
number_format_locale_label: "NumberFormat Locale:",
43+
user_agent_label: "User Agent:",
44+
screen_information_label: "Screen information:",
45+
timezone_label: "Timezone:",
46+
offset_label: "Offset:",
47+
on_connector: "on",
3248
"Refresh detection": "Refresh Detection Info",
3349
request_header_method: "Request Header",
3450
javascript_method: "JavaScript",
@@ -132,6 +148,7 @@ if (typeof detectEn === "undefined") {
132148
ai_clear: "Clear Session",
133149
ai_send: "Send",
134150
ai_export: "Export Markdown",
151+
ai_user_input_label: "Follow-up question",
135152
ai_export_empty: "There is no visible chat content to export yet.",
136153
ai_export_success: "Chat exported as Markdown.",
137154
ai_copy_failed: "Copy failed. Please try again.",

i18n/detect-i18n.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,10 @@ class DetectI18n extends BaseI18n {
170170
);
171171
if (aiProviderDescription && !aiProviderDescription.dataset.initialized) {
172172
const providerKey = aiProviderSelect?.value || "openrouter";
173-
aiProviderDescription.textContent = this.t(
174-
`ai_provider_${providerKey}_desc`,
175-
);
173+
const preset = window.AIProviderPresets?.[providerKey];
174+
aiProviderDescription.textContent = preset?.descriptionKey
175+
? this.t(preset.descriptionKey)
176+
: "";
176177
}
177178

178179
const aiBaseUrlLabel = document.querySelector("#aiBaseUrlLabel");
@@ -226,8 +227,13 @@ class DetectI18n extends BaseI18n {
226227
}
227228

228229
const aiUserInput = document.querySelector("#aiUserInput");
230+
const aiUserInputLabel = document.querySelector("#aiUserInputLabel");
231+
if (aiUserInputLabel) {
232+
aiUserInputLabel.textContent = this.t("ai_user_input_label");
233+
}
229234
if (aiUserInput) {
230235
aiUserInput.placeholder = this.t("ai_user_input_placeholder");
236+
aiUserInput.setAttribute("aria-label", this.t("ai_user_input_label"));
231237
}
232238

233239
// 处理所有检测中文本
@@ -269,12 +275,12 @@ class DetectI18n extends BaseI18n {
269275

270276
const aiChatStatus = document.querySelector("#aiChatStatus");
271277
if (aiChatStatus && !aiChatStatus.dataset.initialized) {
272-
aiChatStatus.textContent = this.t("ai_config_incomplete");
278+
aiChatStatus.textContent = "";
273279
}
274280

275281
const aiConfigHint = document.querySelector("#aiConfigHint");
276282
if (aiConfigHint && !aiConfigHint.dataset.initialized) {
277-
aiConfigHint.textContent = this.t("ai_config_incomplete");
283+
aiConfigHint.textContent = "";
278284
}
279285
}
280286
}

0 commit comments

Comments
 (0)