From de8c6541e55c1019f61e38b190ad23c73016f85d Mon Sep 17 00:00:00 2001 From: mid Date: Sat, 8 Aug 2026 19:41:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AE=8C=E6=95=B4=E7=9A=84?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=9B=BE=E6=A0=87=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + CHANGELOG.md | 5 +- Dockerfile | 1 + README.md | 3 + README.zh-CN.md | 4 + index.html | 4 + public/apple-touch-icon.png | Bin 0 -> 3460 bytes public/favicon.ico | Bin 0 -> 2505 bytes public/favicon.svg | 12 +++ public/icon-192.png | Bin 0 -> 4783 bytes public/icon-512.png | Bin 0 -> 17860 bytes public/icon-maskable-512.png | Bin 0 -> 13277 bytes public/site.webmanifest | 30 +++++++ server/index.mjs | 1 + tests/appIcons.test.mjs | 157 +++++++++++++++++++++++++++++++++++ tests/server.test.mjs | 9 ++ 16 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon.ico create mode 100644 public/favicon.svg create mode 100644 public/icon-192.png create mode 100644 public/icon-512.png create mode 100644 public/icon-maskable-512.png create mode 100644 public/site.webmanifest create mode 100644 tests/appIcons.test.mjs diff --git a/AGENTS.md b/AGENTS.md index e365f74..3e7f1ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,8 @@ This file is mandatory for AI and coding agents working in this repository. Read - Prevent horizontal page overflow, clipped controls, nested scrolling accidents, text overlap, layout shifts, and unbounded result columns. Long Hex, hashes, filenames, and table cells require explicit wrapping or contained scrolling. - Version information remains at the bottom of the sidebar and is read from `package.json`. Every page footer displays exactly `Copyright © mid All Rights Reserved.` - Do not add a visible link to `/setting`, explanatory marketing copy, login UI, favorites, or sponsorship UI. +- Preserve the application icon contract under `public/`: `favicon.svg`, a 16/32/48-frame `favicon.ico`, `apple-touch-icon.png` at 180x180, `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`, and `site.webmanifest`. Keep the icon visually aligned with the sidebar brand and keep the maskable artwork inside its safe zone. +- The manifest is metadata/install-ready only. Do not add a Service Worker, offline cache, install prompt, splash-screen bundle, Safari mask icon, legacy `browserconfig.xml`, redundant Apple sizes, or separate 16/32 PNG favicons unless requirements explicitly change. - For UI work, verify a wide desktop, an intermediate width around 801-1040 px, and mobile portrait around 390 x 844 in a real browser. Check direct routes, refresh, navigation, search, `/setting`, both themes, console errors, and network requests. ## Adding a tool diff --git a/CHANGELOG.md b/CHANGELOG.md index fd347c8..0f9f88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Added +- 增加与侧边栏品牌一致的浏览器图标体系:SVG、内含 16/32/48 像素帧的 ICO、180 像素 Apple 图标、192/512 像素 Web App 图标、512 像素 maskable 图标及 Web App Manifest。 - 增加统一的 Windows PowerShell 启动与更新脚本,提供固定的生产配置、Git 快进更新、锁定依赖重装和构建后启动流程。 - 增加 Windows Server 非 Docker 部署、启动和更新说明。 - 增加独立的固件 Hash 工具,计算 Intel HEX 总有效数据和各 block 的 SHA-256 / SHA-512。 @@ -13,6 +14,7 @@ ### Fixed +- 修复 Docker 构建未复制 `public/` 资源的问题,并为 `.webmanifest` 增加标准响应类型。 - Intel HEX 解析器增加全部非空记录上限、显式 uint32 地址空间检查和无符号扩展地址运算,拒绝越过 `0xFFFFFFFF` 的数据。 - 固件 Hash 在普通 HTTP 局域网访问时增加本地 SHA-256/SHA-512 回退,不再依赖 `SubtleCrypto` 必须可用。 - 生产服务在监听端口前检查 `dist/index.html`,缺少构建产物时立即给出构建命令,不再启动后按请求重复返回 500。 @@ -21,6 +23,7 @@ ### Documentation +- 明确应用图标资源契约,并记录不启用 Service Worker、离线缓存和冗余旧平台图标的边界。 - 补充 `dist/index.html` 缺失、Node 进程未重启、浏览器页面未刷新和前台 PowerShell 退出等启动故障排查说明。 - 将 Node、Docker Compose、容器健康检查、Windows 脚本和示例配置的生产默认端口统一为 `9000`。 - 增加通用 `README.md` 入口并强化 `AGENTS.md` 新会话接手流程,便于其他 AI 在不依赖历史对话的情况下继续维护。 @@ -29,7 +32,7 @@ ### Verification -- 74 条 Vitest 测试通过,新增标准 SHA 向量、Block 头大小端、地址绑定、最高 uint32 地址、地址溢出、控制记录边界、无 Web Crypto 回退、生产构建预检和部署配置一致性覆盖。 +- 85 条 Vitest 测试通过,新增标准 SHA 向量、Block 头大小端、地址绑定、最高 uint32 地址、地址溢出、控制记录边界、无 Web Crypto 回退、生产构建预检、部署配置一致性和应用图标完整性覆盖。 - Vue/TypeScript 类型检查和生产构建通过。 ## 0.0.5 - 2026-08-07 diff --git a/Dockerfile b/Dockerfile index 9e32fb7..ce2f686 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ COPY package.json package-lock.json ./ RUN npm ci COPY index.html tsconfig*.json vite.config.ts ./ +COPY public ./public COPY src ./src COPY shared ./shared RUN npm run build diff --git a/README.md b/README.md index ff9ff87..8b4a616 100644 --- a/README.md +++ b/README.md @@ -112,10 +112,13 @@ server/ static server and navigation configuration API shared/tool-catalog.json authoritative tool metadata shared/default-navigation.json first-run navigation order tests/ unit and server integration tests +public/ favicon, Apple touch icon, and Web App Manifest assets ``` New tools must keep their computation independent, register the same stable ID in the catalog, default navigation, and lazy-load registry, reuse the shared UI, define resource limits where appropriate, and include normal, invalid, boundary, and known-vector tests. Routes are generated from the tool registry; ordinary tools do not add routes manually. +The browser icon set follows the sidebar brand and includes SVG, multi-frame ICO, Apple touch, 192/512, and maskable assets. The manifest supplies metadata only: the project deliberately has no Service Worker, offline cache, install prompt, or splash-screen bundle, so a deployment update cannot be hidden by application-managed stale caches. + For the complete extension procedure and AI handoff rules, read [AGENTS.md](AGENTS.md) and [README.zh-CN.md](README.zh-CN.md) before editing. Inspect the Git worktree first and preserve existing changes. ## Documentation diff --git a/README.zh-CN.md b/README.zh-CN.md index 7ef57db..34a7a4e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -35,6 +35,7 @@ shared/tool-catalog.json 工具与分类的权威元数据目录 shared/default-navigation.json 新部署的默认分类和工具顺序 tests/ Vitest 单元测试 dist/ npm run build 生成的生产前端 +public/ 浏览器标签页、Apple 主屏幕和 Web App Manifest 图标资源 Dockerfile 多阶段容器构建 docker-compose.yml 默认部署配置 THIRD_PARTY_NOTICES.md 第三方依赖许可说明 @@ -54,6 +55,9 @@ CHANGELOG.md 版本变更、修复和验证记录 - 服务端只持久化导航顺序,文件为 `/data/navigation.json`,写入采用临时文件和原子替换。 - `/setting` 是固定但不公开显示的管理入口;项目没有登录、收藏、赞助或用户系统。 - 页面必须保持统一 UI,并兼容桌面端和移动端竖屏。 +- 浏览器图标与左侧品牌标记保持一致。`favicon.svg` 是现代浏览器主图标,`favicon.ico` 内含 16/32/48 像素回退图,另提供 180 像素 Apple 图标、192/512 像素 Web App 图标和 512 像素 maskable 图标。 + +`site.webmanifest` 只提供应用名称、颜色和图标元数据,便于浏览器添加到主屏幕。项目不注册 Service Worker,不启用离线缓存、安装提示或启动画面,避免部署更新后继续加载过期构建。 ## 核心功能契约 diff --git a/index.html b/index.html index f20257c..0a67709 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,10 @@ + + + + Web Tools diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e5648e25d4005cf2f091724694cdf9d019822b0e GIT binary patch literal 3460 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 literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..41f6c056038d1914e145f6bd4e048230f5a41cb2 GIT binary patch literal 2505 zcmaJ>c{J2}AO4MP>}Kpy846v*q|k7cvCR<8D2$0B8d0=R_RLtKZpe})*(sV)L~(P; z3^H1T7REZ>kbO7V#{Ay7_n!Bh_mBI&e|)~rc|M=d`JC@L&v_mIfB=F33M8{2mhdH>U~#fSND>+{K^7NQtY6^H-(pu-2#e$iD!HcsqTie>1+ML zyehXQk&hy~1cL0&IZcC2+cL5tjB)?N^iRU5$W5vdsC4k`Px5R^zE^h6py{ZImtndu z%sj9+4}3~Qf&0G8@^t-YN*d&cDI;PT_#<(Wt&b{FrSSMZ9>{Bjem82BOg-I;jT5Up z+YArDvfJLZY5J3r<&(c zM+5+@_3i9*srX913V@x>N$cm9Ua|im8G=7X{3Tg%M8q1O{F7t?Q~!}nIl#ih&d$Ns zrqsrjxYJl!v!z5-NCxH4lQ;&3?`I}2WE$n7eaBvToU}1_b|sFr^zET`U$1H~m8;-SFNPfxw7pTvm$;$JGZ01!#Z660rP zRONq?Kh({G>?Sg(CKS!xSO%wFjDa_kIz{7Sw@~|`6=W^u%Fl%n5ajI$x=RMp`&X5C z;N~jy<9(c+b`AnAjc8pNX!otQxyo+ZE=+S@@;v7_3nujkGrSGXz2R1o{Or3WYxa0~ zlh(o2Yy+@4diy84XIND)A>ef!P20`kY~K?3lB@KmDa0TI$zFHNll)PBaguScK9mF8 zi}8c030e5ZQl(info3c(88(0tGNy_iHfyxp^cb!JUPQ z-DJRfuHCapp%A5i%aTwsi0M~}u;nk*WYQQ3w%Yq!-`eX?87UM<>V){r0LUh9EP!+-G+ZW8?2b5?7}`BZht3LZi-(3GNp>} z%WE_qpvm=>wxbc;`tcNSjdsRfz+zHF+oL9K%~$tn|JrHaB#I@*pBa ze*r9P4udEb8igLS6U*5Tbto>L5Zcq-{W-yeh3>JuI}x@AQBK_i$CxXLZmDz17S-145DRMNmsBWStL_O%<)US1>WRPJsnhIo2 zaykQ@Yj>Z>a46lCZK)7=i+V}4KA+`?QDUyio5+0lF5dGqD(JV_%Od>;$S54taRR^lHQ!~ZnG|8osL z3IXI_t|16)CjJlC2zmVNnu&*OoIF9E8Gd<3)wEXkWB$k=dXDZeXIPAKtjOo?tUAiG z9wlAdZW}R~_BNuBo`qWls%OHG2A*YkRA zF~6S_U1t977HivQanfu(FL?1|XUE3zoYc2LLYC|IWG{!ndM{moYxZ)z^|Q^){m?9p zj+Zv6tO~5Mo0HJpWN}&Z=^Lh=--H)a{few4dW=?W>ruL)O+qnHGih8H0D0t+8|J>f@Py5W~P1Dks0{#TFzfB3?j!9SSlC zfDLJ4G~%+m3TU;M*n08B?tR6_D?p)!hzQ1F;cfhC=STYE4}q!gf^LB2aThLL6{RP) z0!!QyS!HFeL^^SH`ta5;*7v6Rf)y^%I4XR?47t5=UHJfyS6ZbOz0e>KV?pVwe-&D$ z1TC@dx}wFcYZ*qG{Q)_C><}dJzHy7|j7Z{KY}K3j@jw~_;q2@(TZk#SC?(fTzL_d` zt{K%PXsj4zm7)DAJV_RRdi@YIFDEa4u6`B8?aOCt##dEP6Aa!25WF?~y;L=eX6+3U zt?Qj;yT~Dj)VXQxa`sJ^a`N@y$g%ZRicKM*kk`_3x^v0zTwzxfWML=@|Lp3hPE}Xg z6}K~IPR&gIm@KX=G1QMj>^IXKnoELJ`-JW) z#89$olhbtwgv=00{IA*^?oYEdDajRzixgYw=N#>`F}XqDNGoQpdU>vk2k96jR_3ek zfjp(l+#ia?d>KLdRdtm&sx}h-@NC|GqJvNfGwP!G--x{KpS#$mX9P)GVtB!FbKP6E zq9W$Pl)3B2xgGy9d4)9 + + + diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..3f3d614e4d8a76f039e2a643b443b75083a12450 GIT binary patch literal 4783 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 literal 0 HcmV?d00001 diff --git a/public/icon-maskable-512.png b/public/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..847f74910ff61d2cb0d497c840e610bc714289bf GIT binary patch literal 13277 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 4 ? pixels[rowOffset + column - 4] : 0 + const above = row > 0 ? pixels[previousRowOffset + column] : 0 + const upperLeft = row > 0 && column >= 4 ? pixels[previousRowOffset + column - 4] : 0 + const predictor = filter === 1 ? left + : filter === 2 ? above + : filter === 3 ? Math.floor((left + above) / 2) + : filter === 4 ? paethPredictor(left, above, upperLeft) + : 0 + pixels[rowOffset + column] = (encoded[encodedOffset] + predictor) & 0xff + encodedOffset += 1 + } + } + expect(encodedOffset).toBe(encoded.length) + return { width, height, pixels } +} + +describe('application icon assets', () => { + it('keeps the vector favicon self-contained', async () => { + const svg = await readFile(path.join(publicRoot, 'favicon.svg'), 'utf8') + + expect(svg).toContain('viewBox="0 0 64 64"') + expect(svg).toContain('#1d2428') + expect(svg).toContain('stroke="#ffffff"') + expect(svg).not.toMatch(/ { + const { width, height, pixels } = decodeRgbaPng(await readFile(path.join(publicRoot, filename))) + let minimumAlpha = 255 + let maximumAlpha = 0 + let hasLightArtwork = false + for (let offset = 0; offset < pixels.length; offset += 4) { + minimumAlpha = Math.min(minimumAlpha, pixels[offset + 3]) + maximumAlpha = Math.max(maximumAlpha, pixels[offset + 3]) + if (pixels[offset] > 240 && pixels[offset + 1] > 240 && pixels[offset + 2] > 240) hasLightArtwork = true + } + + expect({ width, height }).toEqual({ width: size, height: size }) + expect([minimumAlpha, maximumAlpha]).toEqual(opaque ? [255, 255] : [0, 255]) + expect(hasLightArtwork).toBe(true) + }) + + it('packs 16, 32 and 48 pixel PNG frames into the ICO fallback', async () => { + const ico = await readFile(path.join(publicRoot, 'favicon.ico')) + + expect(ico.readUInt16LE(0)).toBe(0) + expect(ico.readUInt16LE(2)).toBe(1) + expect(ico.readUInt16LE(4)).toBe(3) + + const sizes = [] + for (let index = 0; index < 3; index += 1) { + const entryOffset = 6 + index * 16 + const width = ico[entryOffset] || 256 + const height = ico[entryOffset + 1] || 256 + const length = ico.readUInt32LE(entryOffset + 8) + const imageOffset = ico.readUInt32LE(entryOffset + 12) + const frame = ico.subarray(imageOffset, imageOffset + length) + + expect(height).toBe(width) + expect(imageOffset + length).toBeLessThanOrEqual(ico.length) + expect(pngDimensions(frame)).toEqual({ width, height }) + sizes.push(width) + } + expect(sizes).toEqual([16, 32, 48]) + }) + + it('references complete install metadata and existing icon files', async () => { + const manifest = JSON.parse(await readFile(path.join(publicRoot, 'site.webmanifest'), 'utf8')) + + expect(manifest).toMatchObject({ + name: 'Web Tools', + short_name: 'Web Tools', + start_url: '/', + scope: '/', + display: 'standalone', + background_color: '#ffffff', + theme_color: '#f5f6f7', + }) + expect(manifest.icons).toEqual([ + { src: '/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, + { src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, + { src: '/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, + ]) + await Promise.all(manifest.icons.map((icon) => access(path.join(publicRoot, icon.src.slice(1))))) + }) + + it('declares modern, legacy and Apple browser icon entry points', async () => { + const html = (await readProjectFile('index.html')).toString('utf8') + + expect(html).toContain('') + expect(html).toContain('') + expect(html).toContain('') + expect(html).toContain('') + }) + + it('includes public assets in container builds', async () => { + expect((await readProjectFile('Dockerfile')).toString('utf8')).toContain('COPY public ./public') + }) +}) diff --git a/tests/server.test.mjs b/tests/server.test.mjs index dc72173..d73ca30 100644 --- a/tests/server.test.mjs +++ b/tests/server.test.mjs @@ -36,6 +36,7 @@ async function createFixtureServer() { await mkdir(staticRoot, { recursive: true }) await writeFile(path.join(staticRoot, 'index.html'), 'Web Tools') await writeFile(path.join(staticRoot, 'app.js'), 'console.log("ok")') + await writeFile(path.join(staticRoot, 'site.webmanifest'), '{}') const server = await createWebToolsServer({ staticRoot, dataDir, catalog, defaults, logger: { error: () => undefined } }) servers.push(server) await new Promise((resolve, reject) => { @@ -78,6 +79,14 @@ describe('HTTP server', () => { expect(missingApi.headers.get('cache-control')).toBe('no-store') }) + it('serves the Web App Manifest with its registered media type', async () => { + const { baseUrl } = await createFixtureServer() + const response = await fetch(`${baseUrl}/site.webmanifest`) + + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe('application/manifest+json; charset=utf-8') + }) + it('creates, reads and updates navigation through the API', async () => { const { baseUrl, dataDir } = await createFixtureServer() const initial = await fetch(`${baseUrl}/api/navigation`)