// API 客户端:Web Crypto 实现 HMAC-SHA256 签名,与后端 httpx.Sign 一致。 // // ⚠️ 安全提示:四角色密钥放前端仅用于 MVP/演示。 // 生产必须改为「控制台 BFF + 会话令牌」,密钥不下发浏览器。 const enc = new TextEncoder() async function hmacSha256Base64(secret, message) { const key = await crypto.subtle.importKey( 'raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ) const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message)) let bin = '' for (const b of new Uint8Array(sig)) bin += String.fromCharCode(b) return btoa(bin) } // 四角色演示密钥(与 api-svc 预置一致) export const ROLE_KEYS = { regulator: { apiKey: 'ak-regulator', apiSecret: 'sk-regulator', label: '监管主体' }, reviewer: { apiKey: 'ak-reviewer', apiSecret: 'sk-reviewer', label: '审核/媒资' }, cp: { apiKey: 'ak-cp', apiSecret: 'sk-cp', label: '内容提供商' }, operator: { apiKey: 'ak-operator', apiSecret: 'sk-operator', label: '运营商' }, } async function request(role, method, path, body) { const cred = ROLE_KEYS[role] const signPath = '/api/v1' + path.split('?')[0] const sig = await hmacSha256Base64(cred.apiSecret, method + '\n' + signPath) const headers = { Authorization: `TCS ${cred.apiKey}:${sig}` } const opts = { method, headers } if (body !== undefined) { headers['Content-Type'] = 'application/json' opts.body = JSON.stringify(body) } const resp = await fetch('/api/v1' + path, opts) const data = await resp.json().catch(() => ({})) return { status: resp.status, ok: resp.status >= 200 && resp.status < 300, data } } // 通用:按角色发起请求(供多角色工作台使用) export function call(role, method, path, body) { return request(role, method, path, body) } export const api = { // 全流程各步骤(标注发起角色) register: (body) => request('cp', 'POST', '/content/register', body), issue: (body) => request('regulator', 'POST', '/content/issue', body), csps: (body) => request('reviewer', 'POST', '/content/csps-result', body), ingest: (body) => request('reviewer', 'POST', '/content/ingest', body), publish: (body) => request('reviewer', 'POST', '/content/publish', body), inject: (body) => request('operator', 'POST', '/content/inject', body), // 监管功能 verify: (maCode, fileHash) => request('regulator', 'POST', '/content/verify', { ma_code: maCode, file_sha256: fileHash }), mappings: (maCode) => request('regulator', 'GET', '/content/mappings?ma_code=' + encodeURIComponent(maCode)), takedown: (maCode, reason) => request('regulator', 'POST', '/content/takedown', { ma_code: maCode, reason }), takedownEpisode: (maCode, episode, reason) => request('regulator', 'POST', '/content/takedown-episode', { ma_code: maCode, episode, reason }), restore: (maCode) => request('regulator', 'POST', '/content/restore', { ma_code: maCode }), restoreEpisode: (maCode, episode) => request('regulator', 'POST', '/content/restore-episode', { ma_code: maCode, episode }), // 集级粒度(一剧一码 + 集级哈希) episodes: (maCode) => request('regulator', 'GET', '/content/episodes?ma_code=' + encodeURIComponent(maCode)), verifyEpisode: (maCode, episode, fileHash) => request('regulator', 'POST', '/content/verify-episode', { ma_code: maCode, episode, file_sha256: fileHash }), // 工作队列(多角色工作台) reviews: (role, status) => request(role, 'GET', '/content/reviews?status=' + status), list: (role, status) => request(role, 'GET', '/content/list?status=' + status), // 二期:分账/追责/确权/授权/跨省/追更/回传 playback: (platformId, batch) => request('operator', 'POST', '/data/playback', { platform_id: platformId, batch }), playbackSummary: (maCode) => request('regulator', 'GET', '/data/playback-summary?ma_code=' + encodeURIComponent(maCode)), settlement: (maCode, period) => request('regulator', 'POST', '/settlement/compute', { ma_code: maCode, period }), accountability: (maCode) => request('regulator', 'GET', '/content/accountability?ma_code=' + encodeURIComponent(maCode)), evidence: (maCode) => request('regulator', 'GET', '/content/evidence?ma_code=' + encodeURIComponent(maCode)), authorize: (maCode, regions, platforms, expiryAt) => request('regulator', 'POST', '/content/authorize', { ma_code: maCode, regions, platforms, expiry_at: expiryAt }), authCheck: (maCode, region, platform) => request('regulator', 'POST', '/content/auth-check', { ma_code: maCode, region, platform }), crossProvince: (maCode, fileHash, province) => request('regulator', 'POST', '/content/cross-province', { ma_code: maCode, file_sha256: fileHash, province }), }