From 765a7de7b5e62dc2a2f9c2b0037a371e4a28b670 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 25 Sep 2026 19:51:21 +0900 Subject: [PATCH] fix(wallpaper): proxy Bing images through Worker --- .gitignore | 1 + .openai/hosting.json | 3 --- README.md | 18 +++++++++++------ package.json | 9 +++++++++ {dist => public}/addons.js | 0 {dist => public}/index.html | 2 +- scripts/build.mjs | 14 +++++++++++++ scripts/validate.mjs | 8 ++++++++ worker/index.js | 40 +++++++++++++++++++++++++++++++++++++ 9 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 package.json rename {dist => public}/addons.js (100%) rename {dist => public}/index.html (97%) create mode 100644 scripts/build.mjs create mode 100644 scripts/validate.mjs create mode 100644 worker/index.js diff --git a/.gitignore b/.gitignore index 335ec95..f43c4cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.tar.gz +dist/ diff --git a/.openai/hosting.json b/.openai/hosting.json index cf4e28b..8d1ff98 100644 --- a/.openai/hosting.json +++ b/.openai/hosting.json @@ -1,6 +1,3 @@ { - "static": { - "directory": "dist" - }, "project_id": "appgprj_6ab265f0f7dc81919b4e281000e6f7a9" } diff --git a/README.md b/README.md index 538066f..5b607f4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - **Museum**:将重要记忆长期珍藏为馆藏。 - **Insights / Profile**:查看情绪、能量、连续记录与个人系统状态。 - **System**:深浅主题、自动识别浏览器语言(也可固定中文/English)、JSON 导入导出与本地存储说明。 -- **Bing 每日一图**:桌面壁纸每日自动更新;接口不可用时保留内置背景。 +- **Bing 每日一图**:桌面壁纸每日自动更新;由 Life OS 同源代理读取,避免浏览器跨域限制。 - **Windows 式操作**:桌面端单击选择、双击打开;右键桌面或 App 图标可使用快捷菜单。 - **快捷键与草稿恢复**:`Ctrl / ⌘ + K` 打开启动器,`Ctrl / ⌘ + F` 搜索记忆,`Ctrl / ⌘ + S` 归档 Today;当天未归档文字会自动恢复。 - **移动端**:App 以全屏方式打开,保留任务栏恢复入口。 @@ -33,19 +33,25 @@ ## 运行 -这是一个无构建步骤的静态站点。用任意静态文件服务器指向 `dist/` 即可,例如: +这是一个轻量的同源 Worker 网站。Worker 会代理 Bing 每日图片,避免浏览器跨域限制导致壁纸回退为纯色。 + +安装依赖后构建并运行验证: ```bash -npx serve dist +npm run build +npm run validate ``` -直接打开 `dist/index.html` 也能查看基础界面;联网时 Bing 每日壁纸会自动加载。 +构建产物位于 `dist/`,其中包含 Cloudflare Worker 入口与界面资源。 ## 项目结构 ```text -dist/index.html # 整个应用:界面、样式与本地数据逻辑 -.openai/hosting.json # Sites 静态托管配置 +public/index.html # 界面、样式与本地数据逻辑 +public/addons.js # 窗口与辅助交互 +worker/index.js # 同源 Bing 每日壁纸代理 +scripts/build.mjs # 生成 Worker 部署产物 +.openai/hosting.json # Sites 托管配置 ``` ## 隐私说明 diff --git a/package.json b/package.json new file mode 100644 index 0000000..5b864e5 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "life-os", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "validate": "node scripts/validate.mjs" + } +} diff --git a/dist/addons.js b/public/addons.js similarity index 100% rename from dist/addons.js rename to public/addons.js diff --git a/dist/index.html b/public/index.html similarity index 97% rename from dist/index.html rename to public/index.html index 0111c8f..dd3bb7a 100644 --- a/dist/index.html +++ b/public/index.html @@ -12,7 +12,7 @@ function load(k,d){try{return JSON.parse(localStorage.getItem(k))||d}catch{retur const ZH={'LIFE OS / PERSONAL DESKTOP':'LIFE OS / 人生操作系统','TODAY':'今日','TIMELINE':'时间线','MUSEUM':'人生博物馆','INSIGHTS':'洞察','PROFILE':'个人资料','SYSTEM':'系统','SYSTEM ONLINE · LIFE OS v0.1':'系统在线 · LIFE OS v0.1','SYSTEM DATE':'系统日期','MEMORY ARCHIVE':'记忆档案','MEMORIES ARCHIVED':'条记忆已归档','MEMORY ARCHIVED':'记忆已归档','How was your day?':'今天过得怎么样?','Write something about today...':'写下今天的片段…',"TODAY'S WEATHER":'今日天气','ENERGY LEVEL':'能量等级',"TODAY'S FILM":'今日电影','ARCHIVE TODAY':'归档今日','UPDATE MEMORY':'更新记忆','ARCHIVING MEMORY...':'正在归档…','MUSEUM':'人生博物馆','Preserved moments':'被珍藏的瞬间','Worth keeping for the long view.':'值得留给漫长回望的片段。','A museum begins with one preserved moment.':'从一份被珍藏的记忆开始。','STATUS: PRESERVED':'状态:已珍藏','LAST 30 DAYS':'最近 30 天','Insights, quietly.':'安静地看见时间。','WEATHER':'天气','AVERAGE ENERGY':'平均能量','MOST ACTIVE DAY':'最活跃日','CURRENT STREAK':'当前连续记录','LONGEST STREAK':'最长连续记录','SYSTEM PROFILE':'系统档案','LIFE OS ONLINE':'LIFE OS 在线','DAYS ONLINE':'系统运行天数','MEMORIES':'记忆','MUSEUM ITEMS':'博物馆藏品','COMMON WEATHER':'常见天气','PERSONAL SYSTEM':'个人系统','Display Name':'显示名称','Birthday (optional)':'生日(选填)','Life OS Start Date':'Life OS 启动日期','Personal Motto':'个人格言','SAVE PROFILE':'保存资料','PROFILE SAVED':'资料已保存','SYSTEM SETTINGS':'系统设置','System, local and yours.':'你的本地个人系统。','APPEARANCE':'外观','DATA':'数据','STORAGE':'存储','DATA STORAGE':'数据存储','Your data is stored locally in this browser.':'你的数据仅存储在此浏览器中。','Create regular backups to prevent data loss.':'定期备份,让记忆一直在。','LAST BACKUP · NEVER':'上次备份 · 从未','BACKUP RECOMMENDED':'建议备份','VERSION':'版本','EXPORT DATA':'导出数据','IMPORT DATA':'导入数据','RESET LIFE OS':'重置 Life OS','LANGUAGE':'语言','ENGLISH':'English','CHINESE':'中文','Dark':'深色','Light':'浅色','Auto':'自动','No memories yet.':'还没有记忆。','Open Today when you are ready to keep one.':'准备好记录时,打开“今日”。','MEMORY DETAIL':'记忆详情','EDIT':'编辑','ADD TO MUSEUM':'加入博物馆','PRESERVED':'已珍藏','DELETE':'删除','CONFIRM DELETE':'确认删除','Remove this memory?':'删除这条记忆?','This cannot be undone.':'此操作无法撤销。','DELETE MEMORY':'删除记忆','CANCEL':'取消','BACKUP CREATED':'备份已创建','INVALID BACKUP FILE':'无效备份文件','SYSTEM RESTORED':'系统已恢复','RESTORING SYSTEM...':'正在恢复系统…','MINIMIZED — TAP THE APP ICON TO RESTORE':'已最小化 — 点击任务栏图标即可恢复'}; function browserLanguage(){let ls=navigator.languages?.length?navigator.languages:[navigator.language||'en'];return ls.some(x=>String(x).toLowerCase().startsWith('zh'))?'zh':'en'}function lang(){let s=settings();return s.languageMode==='manual'&&['en','zh'].includes(s.language)?s.language:browserLanguage()}function applyLanguage(){let zh=lang()==='zh';document.title=zh?'Life OS / 人生操作系统':'Life OS';let map=zh?ZH:Object.fromEntries(Object.entries(ZH).map(([a,b])=>[b,a]));let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let n=[];while(walker.nextNode())n.push(walker.currentNode);n.forEach(x=>{let v=x.nodeValue.trim();if(map[v])x.nodeValue=x.nodeValue.replace(v,map[v])});document.querySelectorAll('[placeholder]').forEach(x=>{if(zh&&ZH[x.placeholder])x.placeholder=ZH[x.placeholder];if(!zh){let k=Object.entries(ZH).find(([,v])=>v===x.placeholder)?.[0];if(k)x.placeholder=k}})}function setLanguage(v){let s=settings();s.language=v;s.languageMode=v==='auto'?'auto':'manual';save('lifeos_settings',s);renderAll();applyLanguage();toast(v==='auto'?(lang()==='zh'?'已跟随浏览器语言:中文':'Following browser language: English'):v==='zh'?'语言已切换为中文':'Language set to English')} function day(){return new Date().toISOString().slice(0,10)}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&','<':'<','>':'>'}[x]))} -function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=`Bing daily image: ${w.title||'Daily image'}`}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()){applyWallpaper(s.wallpaper);return}try{let r=await fetch('https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US'),d=(await r.json()).images?.[0];if(!d)throw Error('No Bing image');s.wallpaper={date:day(),url:'https://www.bing.com'+d.url,title:d.copyright||'Bing daily image'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(applyLanguage,0);setTimeout(()=>boot.remove(),650)} +function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=`Bing daily image: ${w.title||'Daily image'}`}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()&&s.wallpaper?.source==='lifeos-proxy'){applyWallpaper(s.wallpaper);return}try{let r=await fetch('/api/bing-wallpaper'),d=await r.json();if(!r.ok||!d.url)throw Error(d.error||'No Bing image');s.wallpaper={date:day(),url:'/api/bing-wallpaper-image?date='+day(),title:d.title||'Bing daily image',source:'lifeos-proxy'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(applyLanguage,0);setTimeout(()=>boot.remove(),650)} function desktopIconClick(id,e){if(innerWidth<650){openApp(id);return}selectedIcon=id;document.querySelectorAll('.app-icon').forEach(x=>x.querySelector('.glyph').style.outline='none');e.currentTarget.querySelector('.glyph').style.outline='2px solid var(--accent)'}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>``).join('')}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights'];tasks.innerHTML=ids.map(id=>``).join('')} function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString('en-US',{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OSGOOD ${new Date().getHours()<12?'MORNING':new Date().getHours()<18?'AFTERNOON':'EVENING'}
${Object.entries(APPS).map(([id,a])=>``).join('')}
SYSTEM ONLINE · LIFE OS v0.1
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
SYSTEM DATE

${fmtDate(day())}

${mems().length} memories · ${museum().length} preserved

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let zh=lang()==='zh',labels=zh?{open:'打开',min:'最小化',close:'关闭',refresh:'刷新桌面',new:'新建今日记录',system:'系统设置'}:{open:'Open',min:'Minimize',close:'Close',refresh:'Refresh desktop',new:'New today entry',system:'System settings'},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) function openApp(id){launcher.classList.add('hidden');let w=windows[id];if(w){w.classList.remove('min');focus(w);renderTaskbar();return}w=document.createElement('section');w.className='window';w.dataset.id=id;w.style.left=(innerWidth<650?0:90+Object.keys(windows).length*36)+'px';w.style.top=(innerWidth<650?0:72+Object.keys(windows).length*28)+'px';w.innerHTML=`
${APPS[id][0]}${APPS[id][1]}
`;windows[id]=w;document.querySelector('#windows').append(w);let controls=w.querySelector('.controls');controls.addEventListener('pointerdown',e=>{e.preventDefault();e.stopPropagation()});controls.addEventListener('click',e=>{let action=e.target.closest('button')?.dataset.windowAction;if(!action)return;e.preventDefault();e.stopPropagation();if(action==='min')minWin(id);if(action==='max')maxWin(id);if(action==='close')closeWin(id)});w.addEventListener('mousedown',()=>focus(w));drag(w);renderApp(id);focus(w);renderTaskbar()}function focus(w){w.style.zIndex=++z}function closeWin(id){windows[id]?.remove();delete windows[id];renderTaskbar()}function minWin(id){let w=windows[id];if(!w)return;w.classList.add('min');renderTaskbar();toast('MINIMIZED — TAP THE APP ICON TO RESTORE')}function maxWin(id){windows[id].classList.toggle('max');focus(windows[id])}function drag(w){let bar=w.querySelector('.titlebar'),x,y,l,t;bar.onpointerdown=e=>{if(e.target.closest('.controls')||innerWidth<650)return;x=e.clientX;y=e.clientY;l=w.offsetLeft;t=w.offsetTop;bar.setPointerCapture(e.pointerId);bar.onpointermove=e=>{w.style.left=Math.max(0,l+e.clientX-x)+'px';w.style.top=Math.max(0,t+e.clientY-y)+'px'};bar.onpointerup=()=>bar.onpointermove=null}} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..6eab86b --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,14 @@ +import { mkdir, readFile, rm, writeFile, copyFile } from "node:fs/promises"; + +const text = (file) => readFile(new URL(`../public/${file}`, import.meta.url), "utf8"); +const dist = new URL("../dist/", import.meta.url); +const server = new URL("../dist/server/", import.meta.url); +const meta = new URL("../dist/.openai/", import.meta.url); + +await rm(dist, { recursive: true, force: true }); +await Promise.all([mkdir(server, { recursive: true }), mkdir(meta, { recursive: true })]); +const [indexHtml, addonsJs] = await Promise.all([text("index.html"), text("addons.js")]); +await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\n`); +await copyFile(new URL("../worker/index.js", import.meta.url), new URL("index.js", server)); +await copyFile(new URL("../.openai/hosting.json", import.meta.url), new URL("hosting.json", meta)); +console.log("Built Life OS Worker artifact"); diff --git a/scripts/validate.mjs b/scripts/validate.mjs new file mode 100644 index 0000000..afb6a87 --- /dev/null +++ b/scripts/validate.mjs @@ -0,0 +1,8 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +const worker = await import(new URL("../dist/server/index.js", import.meta.url)); +assert.equal(typeof worker.default?.fetch, "function"); +const html = await readFile(new URL("../dist/server/assets.js", import.meta.url), "utf8"); +assert.match(html, /indexHtml/); +console.log("Life OS Worker artifact is valid"); diff --git a/worker/index.js b/worker/index.js new file mode 100644 index 0000000..9215749 --- /dev/null +++ b/worker/index.js @@ -0,0 +1,40 @@ +import { addonsJs, indexHtml } from "./assets.js"; + +const BING_ARCHIVE = "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US"; + +async function bingMetadata() { + const response = await fetch(BING_ARCHIVE, { headers: { accept: "application/json" } }); + if (!response.ok) throw new Error(`Bing metadata request failed: ${response.status}`); + const image = (await response.json()).images?.[0]; + if (!image?.url) throw new Error("Bing did not return today's image"); + return { + title: image.copyright || "Bing Daily Image", + url: new URL(image.url, "https://www.bing.com").toString(), + }; +} + +function headers(type, cache = "public, max-age=3600") { + return { "content-type": type, "cache-control": cache }; +} + +export default { + async fetch(request) { + const url = new URL(request.url); + try { + if (url.pathname === "/api/bing-wallpaper") { + return Response.json(await bingMetadata(), { headers: { "cache-control": "public, max-age=3600" } }); + } + if (url.pathname === "/api/bing-wallpaper-image") { + const image = await bingMetadata(); + const response = await fetch(image.url); + if (!response.ok) throw new Error(`Bing image request failed: ${response.status}`); + return new Response(response.body, { headers: headers(response.headers.get("content-type") || "image/jpeg", "public, max-age=21600") }); + } + } catch (error) { + return Response.json({ error: "Bing wallpaper is temporarily unavailable", detail: String(error) }, { status: 502 }); + } + if (url.pathname === "/addons.js") return new Response(addonsJs, { headers: headers("application/javascript; charset=utf-8") }); + if (url.pathname === "/" || url.pathname === "/index.html") return new Response(indexHtml, { headers: headers("text/html; charset=utf-8", "no-cache") }); + return new Response("Not found", { status: 404 }); + }, +};