feat: 增加脱敏诊断导出便于复制到 Issue

业务页初始化后可在控制台执行 copyEsimDiagnostics(),复制非敏感环境与状态摘要。
This commit is contained in:
Abner
2026-07-17 20:53:24 +08:00
parent 700f7dbaf8
commit fef5fc9e63
3 changed files with 156 additions and 0 deletions

View File

@@ -23,6 +23,7 @@ import { t, tl } from '../../js/modules/i18n.js';
import { isMobileBrowser, showMobileWarning } from '../../js/modules/browser-utils.js';
import captchaManager from '../../js/modules/captcha-manager.js';
import Logger from '../../js/modules/logger.js';
import { installDiagnosticsGlobal } from '../../js/modules/diagnostics.js';
class GiffgaffApp {
constructor() {
@@ -225,6 +226,12 @@ class GiffgaffApp {
cookieHandler.startValidityMonitor();
}
// 控制台copyEsimDiagnostics() 复制脱敏诊断信息到剪贴板(便于贴 Issue
installDiagnosticsGlobal({
app: 'giffgaff',
getState: () => stateManager.getState()
});
this.initialized = true;
Logger.log(t('giffgaff.app.console.initDone'));
}

View File

@@ -0,0 +1,142 @@
'use strict';
/**
* 诊断信息导出
* 供用户在控制台执行 copyEsimDiagnostics(),将非敏感环境信息复制到 Issue。
* 不包含 token / 密码 / cookie / LPA 等敏感字段。
*/
import { copyToClipboard } from './clipboard.js';
const SENSITIVE_KEY_RE = /token|password|cookie|secret|signature|verifier|lpa|activation|ssn|authorization/i;
/**
* 脱敏对象:剔除敏感键,截断过长字符串
* @param {Object} source
* @param {number} maxDepth
* @returns {Object}
*/
function sanitizeObject(source, maxDepth = 2) {
if (!source || typeof source !== 'object' || maxDepth < 0) {
return {};
}
const out = {};
Object.keys(source).forEach((key) => {
if (SENSITIVE_KEY_RE.test(key)) {
out[key] = '[redacted]';
return;
}
const value = source[key];
if (value == null) {
out[key] = value;
} else if (typeof value === 'string') {
out[key] = value.length > 120 ? `${value.slice(0, 120)}…(len=${value.length})` : value;
} else if (typeof value === 'number' || typeof value === 'boolean') {
out[key] = value;
} else if (typeof value === 'object' && maxDepth > 0) {
out[key] = sanitizeObject(value, maxDepth - 1);
} else {
out[key] = `[${typeof value}]`;
}
});
return out;
}
/**
* 收集诊断快照
* @param {Object} [options]
* @param {string} [options.app] - 应用标识 giffgaff|simyo|home
* @param {Object} [options.state] - 可选业务状态(会脱敏)
* @returns {Object}
*/
export function collectDiagnostics(options = {}) {
const { app = 'unknown', state = null } = options;
const loc = typeof window !== 'undefined' ? window.location : null;
const nav = typeof navigator !== 'undefined' ? navigator : null;
return {
generatedAt: new Date().toISOString(),
app,
page: {
href: loc ? loc.href : '',
hostname: loc ? loc.hostname : '',
pathname: loc ? loc.pathname : '',
protocol: loc ? loc.protocol : ''
},
runtime: {
userAgent: nav ? nav.userAgent : '',
language: nav ? nav.language : '',
online: nav ? nav.onLine : null,
cookieEnabled: nav ? nav.cookieEnabled : null,
viewport: typeof window !== 'undefined'
? { w: window.innerWidth, h: window.innerHeight }
: null
},
state: state ? sanitizeObject(state) : null
};
}
/**
* 格式化为适合粘贴到 Issue 的文本
* @param {Object} [options]
* @returns {string}
*/
export function formatDiagnostics(options = {}) {
const payload = collectDiagnostics(options);
return [
'```json',
'// eSIM Tools 诊断信息(已脱敏,可直接贴到 Issue',
JSON.stringify(payload, null, 2),
'```'
].join('\n');
}
/**
* 复制诊断信息到剪贴板
* @param {Object} [options]
* @returns {Promise<string>} 复制的文本
*/
export async function copyDiagnostics(options = {}) {
const text = formatDiagnostics(options);
await copyToClipboard(text);
return text;
}
/**
* 挂到 window便于控制台copyEsimDiagnostics()
* @param {Object} [options]
*/
export function installDiagnosticsGlobal(options = {}) {
if (typeof window === 'undefined') return;
const getter = typeof options.getState === 'function' ? options.getState : null;
const app = options.app || 'unknown';
window.copyEsimDiagnostics = async function copyEsimDiagnostics() {
const state = getter ? getter() : null;
const text = await copyDiagnostics({ app, state });
// 保留 console方便用户确认已复制并在剪贴板失败时仍能手工拷贝
console.log('[eSIM Diagnostics] 已复制到剪贴板(敏感字段已脱敏)\n', text);
return text;
};
window.__esimDiagnostics = {
collect: () => collectDiagnostics({
app,
state: getter ? getter() : null
}),
format: () => formatDiagnostics({
app,
state: getter ? getter() : null
}),
copy: window.copyEsimDiagnostics
};
}
export default {
collectDiagnostics,
formatDiagnostics,
copyDiagnostics,
installDiagnosticsGlobal
};

View File

@@ -17,6 +17,7 @@ import {
import { t, tl } from '../../js/modules/i18n.js';
import { isMobileBrowser, showMobileWarning } from '../../js/modules/browser-utils.js';
import Logger from '../../js/modules/logger.js';
import { installDiagnosticsGlobal } from '../../js/modules/diagnostics.js';
class SimyoApp {
constructor() {
@@ -66,6 +67,12 @@ class SimyoApp {
// 更新状态显示
uiController.updateStatusPanel();
// 控制台copyEsimDiagnostics() 复制脱敏诊断信息到剪贴板(便于贴 Issue
installDiagnosticsGlobal({
app: 'simyo',
getState: () => stateManager.getState()
});
this.initialized = true;
Logger.log(t('simyo.app.console.initDone'));
}