From 1daf08a9649d646482c016f7ea10a22d52b1e283 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Tue, 3 Nov 2026 13:36:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=81=9C=E7=94=A8=E6=97=A7=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=B9=B6=E6=9B=B4=E6=96=B0=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=20Logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit s3.1080p.hk 已永久停用且文件不可恢复。首页查询排除旧源并统一分页统计,历史图册、聊天和画布保留记录并显示失效说明;下载和代理返回 410,停止旧源上传与归档。使用本地黑白 Logo 适配主题。 提供保存原配置后停用旧存储、取消失效精选、替换 Logo 的整理脚本。七牛 10 月 14 日上传核查记录见 docs/retired-storage.md。 验证:7 项旧存储检查、41 项下载和归档回归测试、TypeScript 检查通过。 --- app/(chat)/gallery/page.tsx | 10 ++- app/api/canvas/3d/model-proxy/route.ts | 6 +- app/api/canvas/download-image/route.ts | 7 +- app/api/featured/route.ts | 10 ++- app/api/image-proxy/route.ts | 4 + app/api/site-settings/route.ts | 13 +-- components/canvas/nodes/ImageNode.tsx | 7 +- components/home/lovart/home-image.tsx | 3 +- .../home/lovart/lovart-featured-gallery.tsx | 13 +-- components/image-preview.tsx | 5 ++ docs/retired-storage.md | 33 ++++++++ lib/canvas/photoshop-safe-download.ts | 4 + lib/db/queries.ts | 5 +- lib/site-branding.ts | 13 +++ lib/storage/image-archive-db.ts | 3 +- lib/storage/image-archive.ts | 2 + lib/storage/image-byte-cache.ts | 2 + lib/storage/migration-db.ts | 5 +- lib/storage/retired-storage.ts | 16 ++++ lib/storage/service.ts | 8 +- public/brand/logo-black.png | Bin 0 -> 8345 bytes public/brand/logo-white.svg | 1 + scripts/storage/retire-1080p-storage.cjs | 29 +++++++ tests/retired-storage.config.ts | 2 + tests/routes/retired-storage.test.ts | 76 ++++++++++++++++++ 25 files changed, 251 insertions(+), 26 deletions(-) create mode 100644 docs/retired-storage.md create mode 100644 lib/site-branding.ts create mode 100644 lib/storage/retired-storage.ts create mode 100644 public/brand/logo-black.png create mode 100644 public/brand/logo-white.svg create mode 100644 scripts/storage/retire-1080p-storage.cjs create mode 100644 tests/retired-storage.config.ts create mode 100644 tests/routes/retired-storage.test.ts diff --git a/app/(chat)/gallery/page.tsx b/app/(chat)/gallery/page.tsx index fbba95141..2816cd436 100644 --- a/app/(chat)/gallery/page.tsx +++ b/app/(chat)/gallery/page.tsx @@ -1,4 +1,5 @@ "use client"; +import { isRetiredStorageUrl, RETIRED_STORAGE_MESSAGE } from "@/lib/storage/retired-storage"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; @@ -316,7 +317,12 @@ export default function GalleryPage() { <>
{RETIRED_STORAGE_MESSAGE}
+{img.alt}
+*@v78rH zqk1jD8M!vnhS68pA=cUwr`%oEz{?p-I!rPo8qf#?B5V#$CT7MKB$Bzgxjt-6FkWgz zFf%eXH85UkX=-jsG=ZnSw1GutK%nKi6%JGOz;D*tL2Pz}rIFFLZQBgDEj45?HyaU2 zB$5%q*vQz}0K8$qiV0`?MH_^(bfz_|ptESq;0SgwBOFFF`uQ^=+1A>?TS*vU5i@GT zSyNB}(2Sz}B8-TJ1SG8K7hPQb@vC8BGjFrl>*#;_`&Gp(*O&;p(K xmk>gxVTt4hqKsz;WWDQ z3Tthk#4tEGz>;8L>_8-$lZ;L6EQmx0f}Op&g@dVuEz#6;DZ#|fbXs-=gBBS^4`)xy z2K*^&`?s ha1=ejfyoGi(Woth|K1n#ztuM-8}RqOSp2Q55pc!`$?bp0 z_4E}W5OSPhFZgAK|MYM`dM04^u1UF75TqgFyu#Ks`dR<;9Rdw{T ;k()0M(y~)GUmRcxP=VZhdB6a}=*`y;vPwV- zdGy5r^%S}J^6(9OpNZxR-_iK~>if*ixA46V>Aio6bmr#ENM~aI1|fev(r+H}0luqV ze%cM+cWzXOQ*$6Sb(7z$*`LHjO_-X$niI`|;Oo#2tx~s`(zCuss()Ex{^z0ovLI$i zHFNkD?2`WHs~T+qGjRX)@O8fYO%UI}_y2_n;OL(5#0b{Wh<_Ae_8J=mW}5=gd?yCd zkA}$;4VEl`{FM>Nh~!nn~hc58nWq|4Q}$%hLLPr-jWhapv&<34mq>#XmUw_nU+7 zJnujpVqs!jVRY7=e{NR(oBbIo2H57D;a`+O2dLEal%4&C?uP6XwbutZ*Yk_O=mm|g zw&aWlM&P!d2L=8915v~)co?1(b5k|5^?@89F@d q~mcKz=wdwp+dHo9Ui#xRQx-zr-0bwG(=;i05TWL9>__{ z0>~+x;k=c-2==NX=2Vp7xZ4(qIP|SMa+zDDXCdlPpN|MI7%58Bgo2ahfW$|O#Dx?D zo79+_5`~EOr^|TCNCfSdv6*S+FwhP!yN1(ye*F!ViUnRYrRul`M~`C=6Fo-+IK+{B z;;+=55C)hssh^{dkpiy;@ejFyA3(dy(DL2Eqw)xdFeQlJ?1`YR1e?&8p)QlQx65U= zb|87Agts(U(dC_M(5zCz)vy23I7c6q1FAtqV; O2!0;hA5F)mNVT1NJpkb6{H#C*_r)={dEp2u|`TLC-o2%7ch0 z%u*re2Gn{+t}(fx=*J|Va`c_!la~^8y)~EKQbk?XES)wbwY%XmM*+pfQTAa32F>Ja zvYyjTiJIT=z|cmNbV&FW2+R;B5yG_A`iAlc?bmi=RA*aJYFXKhp#X)V{YQi4O%{@E z6he3U#bBjs1F9P3GNXSRML?C>eMVwSS~m4)7!nhOBY|6oU8xLw04S`F55P%D1Na2H zH~SA ) zh)1be-UP05B{b7di)jai{8&PO3zbq5H}6EcWvqx1Pi#belvO&m=Cjn;d^F+Ai{~RS z)>%)`4$^ejWseU<1)&FLuNPo{KiZ7|7v^ jgom=t-biO!iylJfnvV4U;Ya2(}{cp *QY2jx)3@W$bNpjyCpE+m%2bcXJ zm)X{aqOLG-pbF&R87UBtsvucBl*` W;KYSWrR`ei!1jMX4;^6hZu& zS_$8E$U^M+U2?G~B#!&klST zK}Yx#aVamcBB?nYOZ9aJjkBcS5HDZ(<2)^-rwm@upLpj>y*!Dun(c$#ZUWVDH08Zz zgY{m1^1Kon-%DaODo44zVHwKV;int@=X)# !H#HLAJ9Cs+)!>D4?lw=P_<9fN(a2%)JlD^o`HbsQK5?Q)9_>AB--QJTvLC-D;;GBe#@>_T ktrP<${~ehl1<>T4eSRO>_4phAc!H` ztlMV}LtVS>kEe|y4YF(3nb-z7Xi1vy+CdE1I_mJpS#f-Z2R5!tq!S6IwZMB{Qo6Ur zeL3jFv +!W?HJE}9 zY>&L`s)}xZa h!DOP#!d?f4(vhs8rE8(ZNvmW<*Qp0N1@gxn`pUgPsI##nDE1$ z$pyppTpiv?^Y8;Pr=Wu3*!a@}Hrt`oOZ9{`Tx`+Fjh{VZ!RX{vJT(*^C+_gHFZP-v z(rN+o8G06QmOEJ2@2`ASMAU;?O?-xA1vN3mCm@WmXIE^fkl78iZy6TcTA?wIwVJ=G zus3(Jzp$>It~{JOg(fR4ReWW%m$$SIUn`8k+JQ8wDJ@7}Ow3Ca>lF6#8fj79Bi98x z5I|S dd@T-X99`?n#x@4%K5hTDGB%pTXQ4QAGwa;heUmw_iR83U0A!8PTl!g-L ze(N%!%+SF-w0bSrNLVLSq=L4!amNcyw zzjb>Vmo9cYm^+@*SD1B#iX(}{2Q9%~$K}3npTrTDdD>@_VKUSQ)->OVwB)K^VgugH zBC1fa6zeHUrC6;>Cm9HryB*~1phX#+@BGZ^N8AC!wx&GK=)BHG#>M0!4FfW?XrGQd zbll7)@Ze~Zx0tu_hzGW|Cqg=9yJCip`|$AnG_gDXHlW?1>NbC2eNnkw^a%tA+4;3& z-g87Rxmcd|jza$q9Wv-;wdW4|lu|!mYYZ)fV|9g5ArRLqZ9e3BhjICeXKcy6>y0^n z^3b7X4J1(m96=nlKF7n=F)V-Bm*389ERr(2_sX#kEK*HRbGV|GPezAZ>eLO%kONrf z;EgZU($(tlY8DsHrFCHl+S8E^Jk=IHx> =ki)*M(?X+hqTd5;ek z`c%r9l~@Sni8iHjO7MYf9e0Z19o8rF6h$+Cd%n~VvKKD8b%1MBm$S*WGa9F6X(@CI zfr9ojZb*&s59*n{I?QG9F!`hbr0Fp6yFV^T_sZQ6{pQA*P3kPl+PDvf1PsDRpt#e5 zSsy(?5E6zk0xQQ74tZ-&^3dp3{!Zer?l_7Fd_oxO+JCdst2Cy$PJJ#beFIWWe*6RC zv)lv&^(uc+T2j>=Rp SMIIj&kgzT5TM!e#ft ziz3q+z`j^{qq} L)QtCc-kf@C)O zDp8ojed=~Y+Df(TSN_~WC`bzgXtu;iB~Dkk1t_tuG9(XNl;cFbmyr%S{x-R GD z2{)fk9ItN=REFNZl$RBlfnK*f`uw?pST6>$C-`dL(K=9FtIFl%U6{2IpoWRZ?*`QC z#Q2$1Bvp|De=77<9~!DA9`VGPGy-yf(HnH?z4lN6v5+Up-LZF{xYx^N%nE8E;`2wL zUQNf6b0ZKbvA#FgXLOF8x4M>L&T=~m6vIcy;TpmB#PJkGC768#WUQ@9@g_?%DJBIi z%u_ _|gR)jOSCQ0}X#iVVsghhwgsI+wP|pj0jhFCiE61=%S+{O9w<&o*sQhAF*x z0EXdKEnTC|l$dH%*WxP0%zb-^gZpEiAqY~ooNe^-mB0GoHkjonO6548x1y0QgHLyp zohFM?#S#2hJgH}QUpY$4O6~S~tI2ZVvkQA`UJ~~;dIcdi%n=(bt WPx{FL$^wn4yx)t<2p@~$SEuxi0`aEZAY84{xQqdD8yDAw1)B&v zYaD)FVl3Rx?N08a7DG)3bld@*)Nh=OYsUu{zdzA V=A0;xBwE&g#i-Fj_4aCN}*fmO0@6@L 8kJtPDDcXL zM{XTM7RJJAH;i})v{X<~%r0_M&t;Rq3!@MKB(&o-QN{h%nmd2IY;5dT6J p?=07a{_jRN%@EgFYyl1Z>oX$V8fo8D}q@0EbC)5OQQ50gKB zeqdR \ No newline at end of file diff --git a/scripts/storage/retire-1080p-storage.cjs b/scripts/storage/retire-1080p-storage.cjs new file mode 100644 index 000000000..e50dd7640 --- /dev/null +++ b/scripts/storage/retire-1080p-storage.cjs @@ -0,0 +1,29 @@ +const fs = require('node:fs'); +const path = require('node:path'); +require('node:module').createRequire(require.resolve('next/package.json'))('@next/env').loadEnvConfig(process.cwd()); +const sql = require('postgres')(process.env.POSTGRES_URL, { max: 1, prepare: false }); +const pattern = '^https?://s3[.]1080p[.]hk([/:?#]|$)'; + +async function main() { + const apply = process.argv.includes('--apply'); + const result = await sql.begin(async tx => { + const storage = await tx`select id,"isEnabled","isDefault" from "StorageConfig" where endpoint ~* ${pattern} or "publicUrl" ~* ${pattern}`; + const featured = await tx`select id,"isFeatured" from "GeneratedImage" where "isFeatured" and "originalUrl" ~* ${pattern}`; + const branding = await tx`select id,"siteLogo","siteLogoDark",favicon from "SiteSettings" where "siteLogo" ~* ${pattern} or "siteLogoDark" ~* ${pattern} or favicon ~* ${pattern}`; + if (!apply) return { apply, storage: storage.length, featured: featured.length, branding: branding.length }; + const directory = process.env.STORAGE_RETIREMENT_BACKUP_DIR || '/var/lib/ai-chatbot/retired-storage-20261103'; + fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); + const backup = path.join(directory, `before-${Date.now()}.json`); + fs.writeFileSync(backup, JSON.stringify({ storage, featured, branding }, null, 2), { flag: 'wx', mode: 0o600 }); + const disabled = await tx`update "StorageConfig" set "isEnabled"=false,"isDefault"=false,"updatedAt"=now() where (endpoint ~* ${pattern} or "publicUrl" ~* ${pattern}) and ("isEnabled" or "isDefault") returning id`; + const unfeatured = await tx`update "GeneratedImage" set "isFeatured"=false where "isFeatured" and "originalUrl" ~* ${pattern} returning id`; + const branded = await tx`update "SiteSettings" set + "siteLogo"=case when "siteLogo" ~* ${pattern} then '/brand/logo-black.png' else "siteLogo" end, + "siteLogoDark"=case when "siteLogoDark" ~* ${pattern} then '/brand/logo-white.svg' else "siteLogoDark" end, + favicon=case when favicon ~* ${pattern} then '/brand/logo-black.png' else favicon end + where "siteLogo" ~* ${pattern} or "siteLogoDark" ~* ${pattern} or favicon ~* ${pattern} returning id`; + return { apply, backup, disabled: disabled.length, unfeatured: unfeatured.length, branded: branded.length }; + }); + console.log(JSON.stringify(result)); +} +main().catch(error => { console.error(error.message); process.exitCode = 1; }).finally(() => sql.end()); diff --git a/tests/retired-storage.config.ts b/tests/retired-storage.config.ts new file mode 100644 index 000000000..fa5bfd217 --- /dev/null +++ b/tests/retired-storage.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from '@playwright/test'; +export default defineConfig({ testDir: './routes', testMatch: /retired-storage\.test\.ts/, workers: 1, reporter: 'list' }); diff --git a/tests/routes/retired-storage.test.ts b/tests/routes/retired-storage.test.ts new file mode 100644 index 000000000..2aab014d2 --- /dev/null +++ b/tests/routes/retired-storage.test.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@playwright/test'; +import { NextRequest, NextResponse } from 'next/server'; +import { serverLoader } from '../helpers/canvas-download-server-loader'; +import { isRetiredStorageUrl, RETIRED_STORAGE_MESSAGE } from '@/lib/storage/retired-storage'; +import { resolveSiteBranding, DEFAULT_SITE_LOGO, DEFAULT_SITE_LOGO_DARK } from '@/lib/site-branding'; + +const retired = 'https://s3.1080p.hk/jsxiangche/2025/01/image.png'; +const current = 'https://scqnycdn.jiansun.vip/uploads/current.png'; + +test('retired URLs include stored proxy URLs but do not include live storage', () => { + for (const url of [retired, retired.replace('https:', 'http:'), '/api/image-proxy?url=' + encodeURIComponent(retired)]) { + expect(isRetiredStorageUrl(url)).toBe(true); + } + for (const url of [current, '/brand/logo-black.png', 'https://s3.jiansun.vip/gongxiangpan/a.png', 'https://example.com/s3.1080p.hk/a.png']) { + expect(isRetiredStorageUrl(url)).toBe(false); + } +}); + +test('original image proxy returns permanent failure without redirecting or fetching', async () => { + let fetches = 0; + const route = serverLoader({ 'next/server': { NextResponse }, fetch: () => { fetches++; throw Error('Unexpected network'); } })('app/api/image-proxy/route.ts'); + const response = await route.GET(new NextRequest('https://ai.yydd.ai/api/image-proxy?original=1&url=' + encodeURIComponent(retired), {headers:{'sec-fetch-dest':'image'}})); + expect(response.status).toBe(410); + expect(await response.json()).toMatchObject({code:'storage_retired',error:RETIRED_STORAGE_MESSAGE}); + expect(fetches).toBe(0); +}); + +test('byte cache never fetches retired resources', async () => { + let fetches = 0; + const module = serverLoader({ fetch: () => { fetches++; throw Error('Unexpected network'); } })('lib/storage/image-byte-cache.ts'); + await expect(module.fetchImageBytes(retired)).rejects.toThrow('HTTP 410'); + expect(fetches).toBe(0); +}); + +test('retired sources cannot enter the archive queue', async () => { + let inserts = 0; + const module = serverLoader({ + '@/lib/db': {db:{insert:()=>{inserts++;throw Error('Unexpected insert');}}}, + '@/lib/db/schema': {}, + })('lib/storage/image-archive-db.ts'); + expect(await module.enqueueImageArchiveTask({sourceUrl:retired})).toBeNull(); + expect(inserts).toBe(0); +}); + +test('single downloads reject retired sources before async preparation, while live candidates remain usable', async () => { + const fetched: string[] = []; + const route = serverLoader({ + '@/app/(auth)/auth':{auth:async()=>({user:{id:'owner'}})}, + 'next/server':{NextResponse,after:()=>{}}, + '@/lib/storage/canvas-cdn-download':{}, + '@/lib/storage/image-download-preparation':{}, + '@/lib/canvas/photoshop-safe-download':{ + fetchCanvasDownloadAsset:async({file}:any)=>{fetched.push(file.url);return {ok:true,bytes:Buffer.from('original'),filename:'original.png',contentType:'image/png'};}, + sanitizeCanvasDownloadFileName:()=> 'original', buildAttachmentContentDisposition:()=> 'attachment', + }, + })('app/api/canvas/download-image/route.ts'); + const request = (body:unknown) => new NextRequest('https://ai.yydd.ai/api/canvas/download-image',{method:'POST',body:JSON.stringify(body)}); + const failure = await route.POST(request({url:retired,preparation:'async',preferDirect:true})); + expect(failure.status).toBe(410); + expect(fetched).toEqual([]); + const success = await route.POST(request({urls:[retired,current]})); + expect(success.status).toBe(200); + expect(fetched).toEqual([current]); + expect(await success.text()).toBe('original'); +}); + +test('ZIP asset preparation reports permanent failure without retrying the source', async () => { + const module = serverLoader({})( 'lib/canvas/photoshop-safe-download.ts'); + const result = await module.fetchCanvasDownloadAsset({file:{url:retired},index:0,requestOrigin:'https://ai.yydd.ai',cookie:''}); + expect(result).toMatchObject({ok:false,error:RETIRED_STORAGE_MESSAGE}); +}); + +test('branding replaces unavailable storage with theme variants and preserves custom live branding', () => { + expect(resolveSiteBranding({siteLogo:retired,siteLogoDark:retired,favicon:retired})).toEqual({siteLogo:DEFAULT_SITE_LOGO,siteLogoDark:DEFAULT_SITE_LOGO_DARK,favicon:DEFAULT_SITE_LOGO}); + expect(resolveSiteBranding({siteLogo:current,siteLogoDark:current}).siteLogo).toBe(current); +});