/** * TalkingQ Platform - Privacy (PII) Page */ registerPage('privacy', function(container) { const DEFAULT_PRIVACY_STATE = { activeTab: 'pii', piiGlobalEnabled: true, piiRules: [ { id: 'name', name: '姓名', type: 'person_name', mask: '***', example: '张三 → ***', enabled: true, system: true }, { id: 'phone', name: '手机号', type: 'phone', mask: '138****8888', example: '13812348888 → 138****8888', enabled: true, system: true }, { id: 'ip', name: 'IP 地址', type: 'ip', mask: '192.168.x.x', example: '192.168.1.10 → 192.168.x.x', enabled: true, system: true }, { id: 'idcard', name: '身份证号', type: 'id_card', mask: '330102****1234', example: '330102199001011234 → 330102****1234', enabled: true, system: true }, { id: 'address', name: '家庭住址', type: 'address', mask: '[地址已脱敏]', example: '北京市朝阳区某小区 → [地址已脱敏]', enabled: true, system: true }, { id: 'bankcard', name: '银行卡号', type: 'bank_card', mask: '6222****1234', example: '6222020202021234 → 6222****1234', enabled: false, system: true }, ], coppaItems: [ { id: 'age', label: '用户年龄验证', enabled: true }, { id: 'parent-consent', label: '家长知情同意收集', enabled: true }, { id: 'location', label: '禁止收集13岁以下精确位置', enabled: true }, { id: 'tracking', label: '行为追踪限制', enabled: true }, { id: 'minimize', label: '数据最小化原则', enabled: true }, ], gdprItems: [ { id: 'notice', label: '知情权(提供隐私政策)', enabled: true }, { id: 'access', label: '访问权(数据导出)', enabled: true }, { id: 'rectify', label: '更正权', enabled: true }, { id: 'erase', label: '删除权(被遗忘权)', enabled: true }, { id: 'portable', label: '数据可携带权', enabled: false }, ], dataRequests: [ { id: 'DSR-2026-001', subject: 'parent-a@example.com', company: '智慧星科技', type: '访问数据', status: 'pending', createdAt: '2026-05-12 13:20', result: '' }, { id: 'DSR-2026-002', subject: 'child-user-003', company: '未来玩具厂', type: '删除数据', status: 'processed', createdAt: '2026-05-11 10:05', result: '已删除关联会话与设备行为数据' }, { id: 'DSR-2026-003', subject: 'privacy@partner.com', company: '童趣电子', type: '更正数据', status: 'rejected', createdAt: '2026-05-10 16:45', result: '未能验证请求人身份' }, ], retentionPolicies: [ { id: 'sessions', type: '对话记录 (Sessions)', period: 180, unit: '天' }, { id: 'device-logs', type: '设备日志 (Device Logs)', period: 90, unit: '天' }, { id: 'badcase', type: 'Bad Case 数据', period: 365, unit: '天' }, { id: 'ota', type: 'OTA 升级记录', period: 365, unit: '天' }, { id: 'audit', type: '审计日志 (Audit Logs)', period: 730, unit: '天' }, { id: 'pii-logs', type: 'PII 脱敏日志', period: 30, unit: '天' }, ], lastCleanup: '', }; const state = window.__privacyState || clonePrivacyState(DEFAULT_PRIVACY_STATE); window.__privacyState = state; function clonePrivacyState(value) { return JSON.parse(JSON.stringify(value)); } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function getNowText() { const now = new Date(); const pad = value => String(value).padStart(2, '0'); return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; } async function callPrivacyApi(method, ...args) { try { if (typeof API !== 'undefined' && API.privacy?.[method]) { await API.privacy[method](...args); } } catch (error) { console.warn(`Mock privacy API ${method} failed:`, error); } } function getRequestStatusMeta(status) { if (status === 'processed') return { label: '已处理', badge: 'badge-success' }; if (status === 'rejected') return { label: '已拒绝', badge: 'badge-danger' }; return { label: '待处理', badge: 'badge-warning' }; } function getComplianceStatus(items) { return items.every(item => item.enabled) ? { label: '已合规', badge: 'badge-success', text: '所有必要控制项均已开启' } : { label: '需关注', badge: 'badge-warning', text: '仍有控制项处于关闭状态' }; } function maskText(text) { let result = String(text || ''); const enabledRules = state.piiGlobalEnabled ? state.piiRules.filter(rule => rule.enabled) : []; enabledRules.forEach(rule => { if (rule.type === 'phone') result = result.replace(/1[3-9]\d{9}/g, '1**********'); if (rule.type === 'ip') result = result.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '***.***.x.x'); if (rule.type === 'id_card') result = result.replace(/\b\d{6}(?:19|20)\d{2}\d{2}\d{2}\d{3}[\dXx]\b/g, '****** ******** ****'); if (rule.type === 'bank_card') result = result.replace(/\b\d{12,19}\b/g, '**** **** **** ****'); if (rule.type === 'email') result = result.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[邮箱已脱敏]'); if (rule.type === 'address') result = result.replace(/[\u4e00-\u9fa5]{2,}(省|市|区|县|镇|街道|小区)[\u4e00-\u9fa5\d号栋单元室-]*/g, '[地址已脱敏]'); if (rule.type === 'person_name') result = result.replace(/([\u4e00-\u9fa5]{2,4})(?=说|的|,|,|。|\s|$)/g, '***'); if (rule.type === 'custom' && rule.pattern) { try { result = result.replace(new RegExp(rule.pattern, 'g'), rule.mask || '[已脱敏]'); } catch (error) { console.warn('Invalid custom PII pattern', error); } } }); return result; } function renderPage() { container.innerHTML = `
PII 脱敏规则
GDPR/COPPA
数据保留
${renderPiiTab()}
${renderComplianceTab()}
${renderRetentionTab()}
`; bindEvents(); lucide.createIcons({ elements: [container] }); } function renderPiiTab() { return `
PII 自动脱敏配置
自动识别并脱敏对话中的个人敏感信息
全局启用
${state.piiRules.map(rule => `
${escapeHtml(rule.name)} ${rule.system ? '系统' : '自定义'}
${escapeHtml(rule.example)}
`).join('')}
PII 规则接口预留:GET /api/v1/privacy/pii-rules · POST /api/v1/privacy/pii-rules · PUT /api/v1/privacy/pii-rules/{id} · DELETE /api/v1/privacy/pii-rules/{id}
`; } function renderComplianceToggleList(items, group) { return items.map(item => `
${escapeHtml(item.label)}
`).join(''); } function renderComplianceTab() { const coppaStatus = getComplianceStatus(state.coppaItems); const gdprStatus = getComplianceStatus(state.gdprItems); return `
COPPA 合规(儿童隐私保护)
${renderComplianceToggleList(state.coppaItems, 'coppa')}
COPPA 合规状态:${coppaStatus.label} · ${coppaStatus.text}
GDPR 数据权利
${renderComplianceToggleList(state.gdprItems, 'gdpr')}
GDPR 合规状态:${gdprStatus.label} · ${gdprStatus.text}
数据主体请求
处理访问、更正、删除等 GDPR 数据权利请求
${state.dataRequests.map(request => { const meta = getRequestStatusMeta(request.status); return ` `; }).join('')}
请求 ID请求人公司类型状态创建时间操作
${escapeHtml(request.id)} ${escapeHtml(request.subject)} ${escapeHtml(request.company)} ${escapeHtml(request.type)} ${meta.label} ${escapeHtml(request.createdAt)} ${request.status === 'pending' ? `` : ``}
`; } function renderRetentionTab() { return `
数据保留策略
设置不同数据类型的自动保留周期
${state.lastCleanup ? `最近清理:${escapeHtml(state.lastCleanup)}` : ''}
${state.retentionPolicies.map(policy => `
${escapeHtml(policy.type)} ${escapeHtml(policy.unit)}
`).join('')}
后端接口预留:GET /api/v1/privacy/retention-policies · PUT /api/v1/privacy/retention-policies/{id} · POST /api/v1/privacy/cleanup-expired
`; } function bindEvents() { container.querySelectorAll('#privacyTabs .tab-item').forEach(tab => { tab.addEventListener('click', () => { state.activeTab = tab.dataset.tab; renderPage(); }); }); container.querySelector('#piiGlobalToggle')?.addEventListener('change', event => { state.piiGlobalEnabled = event.target.checked; Toast.info(state.piiGlobalEnabled ? 'PII 全局脱敏已开启' : 'PII 全局脱敏已关闭', 1000); }); container.querySelectorAll('.pii-rule-toggle').forEach(input => { input.addEventListener('change', event => { const rule = state.piiRules.find(item => item.id === event.target.dataset.ruleId); if (rule) rule.enabled = event.target.checked; }); }); container.querySelector('#addPiiRuleBtn')?.addEventListener('click', openPiiRuleModal); container.querySelector('#testPiiBtn')?.addEventListener('click', openPiiTestModal); container.querySelector('#savePiiBtn')?.addEventListener('click', savePiiConfig); container.querySelectorAll('.compliance-toggle').forEach(input => { input.addEventListener('change', event => { const list = event.target.dataset.group === 'coppa' ? state.coppaItems : state.gdprItems; const target = list.find(item => item.id === event.target.dataset.itemId); if (target) target.enabled = event.target.checked; }); }); container.querySelector('#saveComplianceBtn')?.addEventListener('click', saveComplianceConfig); container.querySelector('#runComplianceCheckBtn')?.addEventListener('click', runComplianceCheck); container.querySelector('#addDataRequestBtn')?.addEventListener('click', openDataRequestModal); container.querySelector('#saveRetentionBtn')?.addEventListener('click', saveRetentionPolicies); container.querySelector('#resetRetentionBtn')?.addEventListener('click', resetRetentionPolicies); container.querySelector('#cleanupExpiredBtn')?.addEventListener('click', cleanupExpiredData); } function openPiiRuleModal(ruleId = '') { const editingRule = state.piiRules.find(rule => rule.id === ruleId); const modal = createModal({ title: editingRule ? '编辑脱敏规则' : '新增脱敏规则', content: `
`, onConfirm: async () => { const name = modal.overlay.querySelector('#piiRuleName')?.value.trim(); const type = modal.overlay.querySelector('#piiRuleType')?.value || 'custom'; const pattern = modal.overlay.querySelector('#piiRulePattern')?.value.trim(); const mask = modal.overlay.querySelector('#piiRuleMask')?.value.trim(); const example = modal.overlay.querySelector('#piiRuleExample')?.value.trim(); const enabled = modal.overlay.querySelector('#piiRuleEnabled')?.checked; if (!name || !mask) { Toast.warning('请填写规则名称和脱敏展示'); return false; } if (type === 'custom' && !pattern) { Toast.warning('自定义规则需要填写正则表达式'); return false; } if (type === 'custom') { try { new RegExp(pattern); } catch (error) { Toast.warning('自定义正则格式不正确'); return false; } } if (editingRule) { Object.assign(editingRule, { name, type, pattern, mask, example: example || `${name} → ${mask}`, enabled }); await callPrivacyApi('updateRule', editingRule.id, editingRule); Toast.success('脱敏规则已更新'); } else { const newRule = { id: `custom-${Date.now()}`, name, type, pattern, mask, example: example || `${name} → ${mask}`, enabled, system: false, }; state.piiRules.push(newRule); await callPrivacyApi('createRule', newRule); Toast.success('脱敏规则已创建'); } renderPage(); }, confirmText: editingRule ? '保存' : '创建', }); modal.overlay.querySelector('#piiRuleType')?.addEventListener('change', event => { const group = modal.overlay.querySelector('#customPatternGroup'); if (group) group.style.display = event.target.value === 'custom' ? '' : 'none'; }); } window.editPiiRule = function(ruleId) { openPiiRuleModal(ruleId); }; window.deletePiiRule = function(ruleId) { const rule = state.piiRules.find(item => item.id === ruleId); if (!rule) return; createModal({ title: '删除脱敏规则', content: `

确认删除规则「${escapeHtml(rule.name)}」?

删除后该类型信息将不再被此规则脱敏。

`, onConfirm: async () => { state.piiRules = state.piiRules.filter(item => item.id !== rule.id); await callPrivacyApi('deleteRule', rule.id); Toast.success('脱敏规则已删除'); renderPage(); }, confirmText: '确认删除', }); lucide.createIcons(); }; function openPiiTestModal() { const sample = '张三说他的手机号是13812348888,家庭住址是北京市朝阳区星河小区,登录 IP 是 192.168.1.10。'; const modal = createModal({ title: '测试 PII 脱敏', size: 'modal-lg', content: `
`, onConfirm: () => true, confirmText: '关闭', showCancel: false, }); const run = () => { const input = modal.overlay.querySelector('#piiTestInput')?.value || ''; const output = modal.overlay.querySelector('#piiTestOutput'); if (output) output.textContent = maskText(input); }; modal.overlay.querySelector('#runPiiMaskBtn')?.addEventListener('click', run); run(); lucide.createIcons({ elements: [modal.overlay] }); } async function savePiiConfig() { await callPrivacyApi('updateConfig', { piiGlobalEnabled: state.piiGlobalEnabled, piiRules: state.piiRules, }); Toast.success('PII 脱敏配置已保存'); } async function saveComplianceConfig() { await callPrivacyApi('updateConfig', { coppaItems: state.coppaItems, gdprItems: state.gdprItems, }); Toast.success('合规配置已保存'); renderPage(); } function runComplianceCheck() { const disabled = [...state.coppaItems, ...state.gdprItems].filter(item => !item.enabled); createModal({ title: '合规检查结果', content: disabled.length ? `
发现 ${disabled.length} 个待关注项:
${disabled.map(item => `
${escapeHtml(item.label)}
`).join('')}
` : `
检查通过
COPPA / GDPR 控制项均已开启。
`, onConfirm: () => true, confirmText: '关闭', showCancel: false, }); lucide.createIcons(); } function openDataRequestModal() { const modal = createModal({ title: '新增数据主体请求', content: `
`, onConfirm: async () => { const subject = modal.overlay.querySelector('#dsrSubject')?.value.trim(); if (!subject) { Toast.warning('请填写请求人'); return false; } const request = { id: `DSR-${new Date().getFullYear()}-${String(state.dataRequests.length + 1).padStart(3, '0')}`, subject, company: modal.overlay.querySelector('#dsrCompany')?.value || '默认公司', type: modal.overlay.querySelector('#dsrType')?.value || '访问数据', status: 'pending', createdAt: getNowText(), result: '', }; state.dataRequests.unshift(request); await callPrivacyApi('getDataRequests', { mockCreated: request.id }); Toast.success('数据主体请求已创建'); renderPage(); }, confirmText: '创建请求', }); } window.processDataRequest = function(requestId) { const request = state.dataRequests.find(item => item.id === requestId); if (!request) return; const modal = createModal({ title: `处理数据请求 - ${escapeHtml(request.id)}`, content: `
请求人:${escapeHtml(request.subject)}
请求类型:${escapeHtml(request.type)}
所属公司:${escapeHtml(request.company)}
`, onConfirm: async () => { const action = modal.overlay.querySelector('#dsrAction')?.value || 'processed'; const result = modal.overlay.querySelector('#dsrResult')?.value.trim(); if (!result) { Toast.warning('请填写处理说明'); return false; } request.status = action; request.result = result; request.processedAt = getNowText(); await callPrivacyApi('processDataRequest', request.id, action); Toast.success(action === 'processed' ? '数据请求已处理' : '数据请求已拒绝'); renderPage(); }, confirmText: '确认处理', }); }; window.viewDataRequestResult = function(requestId) { const request = state.dataRequests.find(item => item.id === requestId); if (!request) return; const meta = getRequestStatusMeta(request.status); createModal({ title: `请求结果 - ${escapeHtml(request.id)}`, content: `
请求人:${escapeHtml(request.subject)}
请求类型:${escapeHtml(request.type)}
状态:${meta.label}
处理说明:${escapeHtml(request.result || '暂无')}
${request.processedAt ? `
处理时间:${escapeHtml(request.processedAt)}
` : ''}
`, onConfirm: () => true, confirmText: '关闭', showCancel: false, }); }; async function saveRetentionPolicies() { const nextPolicies = state.retentionPolicies.map(policy => { const value = Number(container.querySelector(`.retention-input[data-policy-id="${policy.id}"]`)?.value); return { ...policy, period: value }; }); const invalid = nextPolicies.find(policy => !Number.isFinite(policy.period) || policy.period < 1 || policy.period > 3650); if (invalid) { Toast.warning(`${invalid.type} 的保留周期需在 1 到 3650 天之间`); return; } state.retentionPolicies = nextPolicies; await Promise.all(nextPolicies.map(policy => callPrivacyApi('updateRetentionPolicy', policy.id, policy))); Toast.success('数据保留策略已保存'); renderPage(); } function resetRetentionPolicies() { createModal({ title: '恢复默认保留策略', content: '

确认将所有数据保留周期恢复为默认值?

', onConfirm: () => { state.retentionPolicies = clonePrivacyState(DEFAULT_PRIVACY_STATE.retentionPolicies); Toast.success('数据保留策略已恢复默认'); renderPage(); }, confirmText: '确认恢复', }); } function cleanupExpiredData() { createModal({ title: '清理过期数据', content: `

确认执行过期数据清理?

前端将模拟提交清理任务,真实删除由后端异步任务完成。

`, onConfirm: async () => { state.lastCleanup = getNowText(); await callPrivacyApi('updateConfig', { cleanupExpired: true, at: state.lastCleanup }); Toast.success('过期数据清理任务已提交'); renderPage(); }, confirmText: '提交清理', }); lucide.createIcons(); } renderPage(); });