1// Web Push Service Workerï¼æ¥æ¶æ¨éãå±ç¤ºéç¥ã䏿¥çå½å¨æ · vanceai-aio 2// 3// è¡ä¸ºçä»·äºæ§ç« web-next/public/webpush/sw.jsï¼åæä»¶æ¯åè¡å缩代ç ï¼æ¤å¤éå为å¯è¯»çæ¬ï¼ã 4// 5// ð´ æ¬æä»¶ä¸ public/sw.js å¿ é¡»éåèç¸å ââ tests/unit/config/webpush.test.ts æä¸åéå®çã 6// 两个路å¾é½è¦åå¨ï¼/webpush/sw.js æ¯å½å注åè·¯å¾ï¼scope /webpush/ï¼ï¼ 7// /sw.js æ¯æ´æ©æçæ¬çä¸çåéæ³¨åï¼scope /ï¼ãä»»ä¸è·¯å¾è¿åé 2xxï¼ 8// æµè§å¨ update check 失败åä¼**ä¿çæ§ SW 䏿 æ³æ´æ°**ï¼è¿æ£æ¯ 2026-07~08 飿¬¡å¤±æ§çæå ï¼ã 9// 10// ð´ API_ORIGIN å¿ é¡»ä¸ src/config/webpush.ts ç WEB_PUSH_API_ORIGIN ä¸è´ï¼å䏿µè¯å®çï¼ã 11// éææä»¶è¯»ä¸å°æå»ºæ envï¼åªè½ç¡¬ç¼ç ã 12// 13// ð´ æ¬æä»¶ç fetch åç«ç¹ CSP connect-src 约æ ââ next.config.ts ç headers() ç¨ 14// source '/(.*)' è¦çææè·¯å¾ï¼å 嫿¬èæ¬èªèº«ï¼æ¼æ¾è¡ä¼è®©ä¸æ¥è¢«é黿¦æã 15 16const DEFAULT_API_ORIGIN = 'https://aiportrait.online' 17 18/** 19 * 䏿¥éç¥çå½å¨æäºä»¶ï¼received / clicked / closedï¼ã 20 * 失败åªåæä¸éè¯ï¼é¥æµä¸å¼å¾å½±åéç¥æ¬èº«çå±ç¤ºã 21 * 22 * ð´ **å¿ é¡»æè¿å Promise**ï¼è°ç¨æ¹è¦æå®äº¤ç» `event.waitUntil()`ï¼ 23 * è `waitUntil(undefined)` 伿 TypeErrorï¼åèææ´ä¸ªäºä»¶å¤çææã 24 */ 25function report(apiOrigin, payload) { 26 try { 27 return fetch(apiOrigin + '/index.php/sub/listen', { 28 method: 'POST', 29 body: JSON.stringify(payload), 30 headers: new Headers({ 'Content-Type': 'application/json' }), 31 }).catch(() => undefined) 32 } catch { 33 return Promise.resolve() 34 } 35} 36 37/** ä» å 许 http(s) è·³è½¬ï¼æ¡ä½ javascript: / data: è¿ç±»åè®®ã */ 38function isSafeUrl(url) { 39 return typeof url === 'string' && /^https?:\/\//.test(url) 40} 41 42self.addEventListener('install', () => { 43 // è·³è¿ waitingï¼è®©æ°ç SW ç«å³æ¥ç®¡ï¼å¦åæ´æ°è¦çæææ§æ ç¾é¡µå ³é 44 self.skipWaiting() 45}) 46 47self.addEventListener('activate', (event) => { 48 // ç«å³æ¥ç®¡å·²æå¼ç页é¢ï¼å¦å馿¬¡å®è£ åè¦ç䏿¬¡å¯¼èªæçæï¼ 49 event.waitUntil(self.clients.claim()) 50}) 51 52self.addEventListener('push', (event) => { 53 let data 54 try { 55 data = JSON.parse(event.data.text()) 56 } catch { 57 return 58 } 59 if (!data || !data.message_id || !data.title || !data.alert) return 60 61 // æ¨éè½½è·å¯è¦ç䏿¥ç«¯ç¹ï¼æ¨éå¹³å°çå¤ç§æ·/ç°åº¦æºå¶ï¼1:1 æ§ç«è¡ä¸ºï¼ 62 const apiOrigin = isSafeUrl(data.sw_api) ? data.sw_api : DEFAULT_API_ORIGIN 63 64 const reported = report(apiOrigin, { 65 type: 'received', 66 user_id: data.user_id, 67 message_id: data.message_id, 68 app_id: data.app_id, 69 }) 70 71 const options = { 72 body: data.alert, 73 data: { 74 uid: data.user_id, 75 mid: data.message_id, 76 aid: data.app_id, 77 url: data.custom && data.custom.u, 78 apiOrigin, 79 actions: data.o, 80 }, 81 } 82 if (data.icon) options.icon = data.icon 83 if (data.image) options.image = data.image 84 if (data.tag) options.tag = data.tag 85 if (data.badge) options.badge = data.badge 86 if (data.require_interaction) options.requireInteraction = true 87 if (Array.isArray(data.o)) { 88 const actions = data.o.filter((a) => a && a.i && a.n).map((a) => ({ action: a.i, title: a.n, icon: a.p })) 89 if (actions.length) options.actions = actions 90 } 91 92 // waitUntil å¿ é¡»åæ¶æ¬ä½ä¸æ¥ï¼äºä»¶å¤ç彿°ä¸è¿åæµè§å¨å°±å¯è½ç»æ¢ SWï¼ 93 // 裸 fetch ä¼è¢«è¿å¸¦ä¸¢å¼ ââ 饿µéé»ä¸¢æ°ï¼ä¸åªå¨çæºä½çµé/åå°æ¶æå¤ç°ã 94 event.waitUntil(Promise.all([reported, self.registration.showNotification(data.title, options)])) 95}) 96 97self.addEventListener('notificationclick', (event) => { 98 event.notification.close() 99 const info = event.notification.data || {} 100 const apiOrigin = isSafeUrl(info.apiOrigin) ? info.apiOrigin : DEFAULT_API_ORIGIN 101 102 const reported = report(apiOrigin, { 103 type: 'clicked', 104 user_id: info.uid, 105 message_id: info.mid, 106 app_id: info.aid, 107 }) 108 109 let target = info.url 110 // ç¹äºæä¸ª action æé®æ¶ï¼ç¨è¯¥ action èªå¸¦ç URL è¦çé»è®¤è·³è½¬ 111 if (event.action && Array.isArray(info.actions)) { 112 const hit = info.actions.find((a) => a && a.i === event.action) 113 if (hit && hit.u === 'close') { 114 event.waitUntil(reported) 115 return 116 } 117 if (hit && isSafeUrl(hit.u)) target = hit.u 118 } 119 event.waitUntil(isSafeUrl(target) ? Promise.all([reported, self.clients.openWindow(target)]) : reported) 120}) 121 122self.addEventListener('notificationclose', (event) => { 123 const info = event.notification.data || {} 124 const apiOrigin = isSafeUrl(info.apiOrigin) ? info.apiOrigin : DEFAULT_API_ORIGIN 125 event.waitUntil(report(apiOrigin, { type: 'closed', user_id: info.uid, message_id: info.mid, app_id: info.aid })) 126})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.