├── public ├── icons │ ├── ios │ │ ├── 16.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 48.png │ │ ├── 50.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 96.png │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 192.png │ │ ├── 256.png │ │ └── 512.png │ ├── windows11 │ │ ├── LargeTile.scale-100.png │ │ ├── LargeTile.scale-125.png │ │ ├── LargeTile.scale-150.png │ │ ├── LargeTile.scale-200.png │ │ ├── LargeTile.scale-400.png │ │ ├── SmallTile.scale-100.png │ │ ├── SmallTile.scale-125.png │ │ ├── SmallTile.scale-150.png │ │ ├── SmallTile.scale-200.png │ │ ├── SmallTile.scale-400.png │ │ ├── StoreLogo.scale-100.png │ │ ├── StoreLogo.scale-125.png │ │ ├── StoreLogo.scale-150.png │ │ ├── StoreLogo.scale-200.png │ │ ├── StoreLogo.scale-400.png │ │ ├── SplashScreen.scale-100.png │ │ ├── SplashScreen.scale-125.png │ │ ├── SplashScreen.scale-150.png │ │ ├── SplashScreen.scale-200.png │ │ ├── SplashScreen.scale-400.png │ │ ├── Square44x44Logo.scale-100.png │ │ ├── Square44x44Logo.scale-125.png │ │ ├── Square44x44Logo.scale-150.png │ │ ├── Square44x44Logo.scale-200.png │ │ ├── Square44x44Logo.scale-400.png │ │ ├── Wide310x150Logo.scale-100.png │ │ ├── Wide310x150Logo.scale-125.png │ │ ├── Wide310x150Logo.scale-150.png │ │ ├── Wide310x150Logo.scale-200.png │ │ ├── Wide310x150Logo.scale-400.png │ │ ├── Square150x150Logo.scale-100.png │ │ ├── Square150x150Logo.scale-125.png │ │ ├── Square150x150Logo.scale-150.png │ │ ├── Square150x150Logo.scale-200.png │ │ ├── Square150x150Logo.scale-400.png │ │ ├── Square44x44Logo.targetsize-16.png │ │ ├── Square44x44Logo.targetsize-20.png │ │ ├── Square44x44Logo.targetsize-24.png │ │ ├── Square44x44Logo.targetsize-30.png │ │ ├── Square44x44Logo.targetsize-32.png │ │ ├── Square44x44Logo.targetsize-36.png │ │ ├── Square44x44Logo.targetsize-40.png │ │ ├── Square44x44Logo.targetsize-44.png │ │ ├── Square44x44Logo.targetsize-48.png │ │ ├── Square44x44Logo.targetsize-60.png │ │ ├── Square44x44Logo.targetsize-64.png │ │ ├── Square44x44Logo.targetsize-72.png │ │ ├── Square44x44Logo.targetsize-80.png │ │ ├── Square44x44Logo.targetsize-96.png │ │ ├── Square44x44Logo.targetsize-256.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-20.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-24.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-30.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-36.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-40.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-44.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-48.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-60.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-64.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-72.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-80.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-96.png │ │ ├── Square44x44Logo.altform-unplated_targetsize-256.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-20.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-30.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-36.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-40.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-44.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-60.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-64.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-72.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-80.png │ │ ├── Square44x44Logo.altform-lightunplated_targetsize-96.png │ │ └── Square44x44Logo.altform-lightunplated_targetsize-256.png │ ├── android │ │ ├── android-launchericon-48-48.png │ │ ├── android-launchericon-72-72.png │ │ ├── android-launchericon-96-96.png │ │ ├── android-launchericon-144-144.png │ │ ├── android-launchericon-192-192.png │ │ └── android-launchericon-512-512.png │ └── icon.svg ├── js │ ├── search │ │ ├── searchHandler.js │ │ └── searchAPI.js │ ├── components │ │ └── functionButton.js │ ├── imageGen │ │ ├── imageGenAPI.js │ │ ├── imageGenHandler.js │ │ └── imageGenUI.js │ ├── ai │ │ └── aiAPI.js │ ├── auth.js │ ├── realtime.js │ ├── pwa.js │ └── app.js ├── css │ ├── reset.css │ ├── ios-fixes.css │ ├── main.css │ ├── daisyui-wechat.css │ ├── auth.css │ ├── imageGen.css │ ├── ai.css │ ├── functionComponents.css │ └── responsive.css ├── manifest.json ├── login.html ├── index.html └── sw.js ├── docker ├── .dockerignore ├── package.json ├── Dockerfile ├── docker-compose.yml ├── env.example ├── deploy.sh └── README.md ├── docker-compose.yml ├── wrangler.toml ├── package.json ├── env.docker ├── .gitignore ├── database └── schema.sql └── README.md /public/icons/ios/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/16.png -------------------------------------------------------------------------------- /public/icons/ios/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/20.png -------------------------------------------------------------------------------- /public/icons/ios/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/29.png -------------------------------------------------------------------------------- /public/icons/ios/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/32.png -------------------------------------------------------------------------------- /public/icons/ios/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/40.png -------------------------------------------------------------------------------- /public/icons/ios/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/48.png -------------------------------------------------------------------------------- /public/icons/ios/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/50.png -------------------------------------------------------------------------------- /public/icons/ios/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/57.png -------------------------------------------------------------------------------- /public/icons/ios/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/58.png -------------------------------------------------------------------------------- /public/icons/ios/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/60.png -------------------------------------------------------------------------------- /public/icons/ios/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/64.png -------------------------------------------------------------------------------- /public/icons/ios/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/72.png -------------------------------------------------------------------------------- /public/icons/ios/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/76.png -------------------------------------------------------------------------------- /public/icons/ios/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/80.png -------------------------------------------------------------------------------- /public/icons/ios/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/87.png -------------------------------------------------------------------------------- /public/icons/ios/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/96.png -------------------------------------------------------------------------------- /public/icons/ios/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/100.png -------------------------------------------------------------------------------- /public/icons/ios/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/1024.png -------------------------------------------------------------------------------- /public/icons/ios/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/114.png -------------------------------------------------------------------------------- /public/icons/ios/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/120.png -------------------------------------------------------------------------------- /public/icons/ios/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/128.png -------------------------------------------------------------------------------- /public/icons/ios/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/144.png -------------------------------------------------------------------------------- /public/icons/ios/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/152.png -------------------------------------------------------------------------------- /public/icons/ios/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/167.png -------------------------------------------------------------------------------- /public/icons/ios/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/180.png -------------------------------------------------------------------------------- /public/icons/ios/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/192.png -------------------------------------------------------------------------------- /public/icons/ios/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/256.png -------------------------------------------------------------------------------- /public/icons/ios/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/ios/512.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/LargeTile.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/LargeTile.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/LargeTile.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/LargeTile.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/LargeTile.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SmallTile.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SmallTile.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SmallTile.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SmallTile.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SmallTile.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-48-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/android/android-launchericon-48-48.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-72-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/android/android-launchericon-72-72.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-96-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/android/android-launchericon-96-96.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-144-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/android/android-launchericon-144-144.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-192-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/android/android-launchericon-192-192.png -------------------------------------------------------------------------------- /public/icons/android/android-launchericon-512-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/android/android-launchericon-512-512.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /public/icons/windows11/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-20.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-30.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-36.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-40.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-44.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-60.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-64.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-72.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-80.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-96.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-20.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-30.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-36.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-40.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-44.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-60.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-64.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-72.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-80.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-96.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png -------------------------------------------------------------------------------- /public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEKVIW/docker-wxchat/HEAD/public/icons/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | # 排除根目录的node_modules 2 | ../node_modules/ 3 | 4 | # 排除Cloudflare Workers相关文件 5 | ../worker/ 6 | ../.wrangler/ 7 | ../wrangler.toml 8 | ../build.js 9 | ../package.json 10 | ../package-lock.json 11 | 12 | # 排除开发文件 13 | ../.git/ 14 | ../.gitignore 15 | ../README.md 16 | ../LICENSE 17 | 18 | # 排除其他不需要的文件 19 | ../*.md 20 | ../.env* 21 | ../.DS_Store 22 | ../Thumbs.db 23 | 24 | # 排除构建产物 25 | ../dist/ 26 | ../build/ -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | wxchat: 5 | image: yilan666/wxchat:latest 6 | container_name: wxchat 7 | restart: unless-stopped 8 | ports: 9 | - "3000:3000" 10 | volumes: 11 | - ./data:/app/data 12 | - ./uploads:/app/uploads 13 | env_file: 14 | - .env.docker 15 | networks: 16 | - wxchat-network 17 | 18 | networks: 19 | wxchat-network: 20 | driver: bridge 21 | -------------------------------------------------------------------------------- /docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wxchat-docker", 3 | "version": "1.0.0", 4 | "description": "微信文件传输助手 - Docker版本", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js" 9 | }, 10 | "dependencies": { 11 | "express": "^4.18.2", 12 | "sqlite3": "^5.1.7", 13 | "multer": "^1.4.4", 14 | "cors": "^2.8.5", 15 | "jsonwebtoken": "^9.0.2", 16 | "bcryptjs": "^2.4.3" 17 | }, 18 | "keywords": [ 19 | "docker", 20 | "file-transfer", 21 | "wechat", 22 | "sqlite" 23 | ], 24 | "author": "xiyewuqiu ", 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | # 安装SQLite 4 | RUN apk add --no-cache sqlite 5 | 6 | # 设置工作目录 7 | WORKDIR /app 8 | 9 | # 复制package文件 10 | COPY docker/package.json ./ 11 | # 只安装生产依赖,清理缓存 12 | RUN npm install --omit=dev && npm cache clean --force 13 | 14 | # 复制源代码 15 | COPY docker/server.js ./ 16 | COPY public/ ./public/ 17 | COPY database/schema.sql ./database/ 18 | 19 | # 创建数据目录 20 | RUN mkdir -p /app/data /app/uploads 21 | 22 | # 暴露端口 23 | EXPOSE 3000 24 | 25 | # 启动命令(直接执行,避免脚本文件问题) 26 | CMD ["sh", "-c", "echo '🔧 初始化数据库...' && mkdir -p /app/data /app/uploads && sqlite3 /app/data/wxchat.db < /app/database/schema.sql && echo '✅ 数据库初始化完成!' && echo '🚀 启动微信文件传输助手 Docker版本...' && node server.js"] 27 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | wxchat: 4 | build: 5 | context: .. 6 | dockerfile: docker/Dockerfile 7 | ports: 8 | - "3000:3000" 9 | volumes: 10 | - ../data:/app/data # 数据库文件持久化 11 | - ../uploads:/app/uploads # 文件上传目录 12 | env_file: 13 | - .env # 从.env文件加载环境变量 14 | restart: unless-stopped 15 | healthcheck: 16 | test: 17 | [ 18 | "CMD", 19 | "wget", 20 | "--quiet", 21 | "--tries=1", 22 | "--spider", 23 | "http://localhost:3000/login.html", 24 | ] 25 | interval: 30s 26 | timeout: 10s 27 | retries: 3 28 | start_period: 40s 29 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "wxchat" 2 | main = "worker/index.js" 3 | compatibility_date = "2025-06-17" 4 | 5 | # 静态资源配置 - 使用正确的格式 6 | [assets] 7 | directory = "./public" 8 | binding = "ASSETS" 9 | 10 | # D1 数据库绑定 11 | [[d1_databases]] 12 | binding = "DB" 13 | database_name = "wxchat" 14 | database_id = "db2a9c2e-3758-42d6-a697-f99d1e290664" 15 | 16 | # R2 存储桶绑定 17 | [[r2_buckets]] 18 | binding = "R2" 19 | bucket_name = "wxchat" 20 | 21 | # 环境变量配置(用于鉴权) 22 | [vars] 23 | # 访问密码(生产环境请修改为强密码) 24 | ACCESS_PASSWORD = "3zHb0d44eW^mzLj" 25 | # JWT密钥(生产环境请使用强随机密钥) 26 | JWT_SECRET = "lazily-plunder-overboard-washer-rants-lingo" 27 | # 会话过期时间(小时) 28 | SESSION_EXPIRE_HOURS = "24" 29 | # 最大登录尝试次数 30 | MAX_LOGIN_ATTEMPTS = "5" 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wxchat", 3 | "version": "1.0.0", 4 | "description": "微信文件传输助手Web应用 - 模块化设计", 5 | "main": "worker/index.js", 6 | "scripts": { 7 | "dev": "wrangler dev", 8 | "deploy": "wrangler deploy", 9 | "build": "node build.js", 10 | "db:init": "wrangler d1 execute wxchat --file=./database/schema.sql" 11 | }, 12 | "dependencies": { 13 | "@cloudflare/kv-asset-handler": "^0.3.0", 14 | "hono": "^3.12.0", 15 | "swiper": "^11.2.10" 16 | }, 17 | "devDependencies": { 18 | "wrangler": "^3.0.0" 19 | }, 20 | "keywords": [ 21 | "cloudflare-workers", 22 | "hono", 23 | "file-transfer", 24 | "modular-design", 25 | "static-files" 26 | ], 27 | "author": "xiyewuqiu ", 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /docker/env.example: -------------------------------------------------------------------------------- 1 | # 微信文件传输助手 - Docker环境配置示例 2 | # 复制此文件为 .env 并修改相应配置 3 | 4 | # 应用基础配置 5 | # 运行环境:production(生产) | development(开发) 6 | NODE_ENV=production 7 | 8 | # 服务端口号,默认3000 9 | PORT=3000 10 | 11 | # 数据库配置 12 | # SQLite数据库文件路径(容器内路径) 13 | DATABASE_PATH=/app/data/wxchat.db 14 | 15 | # 文件存储配置 16 | # 文件上传存储路径(容器内路径) 17 | UPLOAD_PATH=/app/uploads 18 | 19 | # 安全配置(重要:生产环境必须修改) 20 | # 访问密码:用于登录系统,请修改为强密码 21 | ACCESS_PASSWORD=123456 22 | 23 | # JWT密钥:用于生成和验证访问令牌,请使用随机字符串 24 | JWT_SECRET=your_jwt_secret_key_here 25 | 26 | # 会话配置 27 | # 会话过期时间(小时),默认24小时 28 | SESSION_EXPIRE_HOURS=24 29 | 30 | # 最大登录尝试次数,超过后需要等待 31 | MAX_LOGIN_ATTEMPTS=5 32 | 33 | # 文件上传配置 34 | # 最大文件上传大小(MB),默认100MB 35 | MAX_FILE_SIZE_MB=100 36 | 37 | # AI功能配置 38 | # 聊天功能配置 39 | AI_CHAT_BASE_URL=https://api.example.com/v1/chat/completions 40 | AI_CHAT_API_KEY=your_ai_chat_api_key_here 41 | AI_CHAT_MODEL=gpt-4o-mini 42 | 43 | # 图片生成功能配置 44 | AI_IMAGE_BASE_URL=https://api.example.com/v1/images/generations 45 | AI_IMAGE_API_KEY=your_ai_image_api_key_here 46 | AI_IMAGE_MODEL=example-model 47 | 48 | # AI功能开关 49 | AI_ENABLED=true 50 | IMAGE_GEN_ENABLED=true 51 | 52 | # AI限流配置 53 | # AI聊天请求限制(每分钟最大请求数) 54 | AI_RATE_LIMIT=10 55 | # 图片生成请求限制(每分钟最大请求数) 56 | IMAGE_RATE_LIMIT=5 -------------------------------------------------------------------------------- /env.docker: -------------------------------------------------------------------------------- 1 | # 微信文件传输助手 - Docker环境配置 2 | # 使用Docker Hub镜像部署的配置文件 3 | 4 | # 应用基础配置 5 | # 运行环境:production(生产) | development(开发) 6 | NODE_ENV=production 7 | 8 | # 服务端口号,默认3000 9 | PORT=3000 10 | 11 | # 数据库配置 12 | # SQLite数据库文件路径(容器内路径) 13 | DATABASE_PATH=/app/data/wxchat.db 14 | 15 | # 文件存储配置 16 | # 文件上传存储路径(容器内路径) 17 | UPLOAD_PATH=/app/uploads 18 | 19 | # 安全配置(重要:生产环境必须修改) 20 | # 访问密码:用于登录系统,请修改为强密码 21 | ACCESS_PASSWORD=your_strong_password_here 22 | 23 | # JWT密钥:用于生成和验证访问令牌,请使用随机字符串 24 | JWT_SECRET=your_jwt_secret_key_here 25 | 26 | # 会话配置 27 | # 会话过期时间(小时),默认24小时 28 | SESSION_EXPIRE_HOURS=24 29 | 30 | # 最大登录尝试次数,超过后需要等待 31 | MAX_LOGIN_ATTEMPTS=5 32 | 33 | # 文件上传配置 34 | # 最大文件上传大小(MB),默认100MB 35 | MAX_FILE_SIZE_MB=100 36 | 37 | # AI功能配置 38 | # 聊天功能配置 39 | AI_CHAT_BASE_URL=https://api.example.com/v1/chat/completions 40 | AI_CHAT_API_KEY=your_ai_chat_api_key_here 41 | AI_CHAT_MODEL=gpt-4o-mini 42 | 43 | # 图片生成功能配置 44 | AI_IMAGE_BASE_URL=https://api.example.com/v1/images/generations 45 | AI_IMAGE_API_KEY=your_ai_image_api_key_here 46 | AI_IMAGE_MODEL=example-model 47 | 48 | # AI功能开关 49 | AI_ENABLED=true 50 | IMAGE_GEN_ENABLED=true 51 | 52 | # AI限流配置 53 | # AI聊天请求限制(每分钟最大请求数) 54 | AI_RATE_LIMIT=10 55 | # 图片生成请求限制(每分钟最大请求数) 56 | IMAGE_RATE_LIMIT=5 -------------------------------------------------------------------------------- /public/js/search/searchHandler.js: -------------------------------------------------------------------------------- 1 | // 搜索处理器模块 - 整合搜索API和UI的交互逻辑 2 | 3 | const SearchHandler = { 4 | // 初始化搜索功能 5 | init() { 6 | // 初始化API和UI模块 7 | if (typeof SearchAPI !== "undefined") { 8 | // SearchAPI已自动初始化 9 | } 10 | 11 | if (typeof SearchUI !== "undefined") { 12 | SearchUI.init(); 13 | } 14 | 15 | // console.log('🔍 搜索功能已初始化'); 16 | }, 17 | 18 | // 显示搜索模态框(供外部调用) 19 | showSearchModal() { 20 | if (typeof SearchUI !== "undefined" && SearchUI.showSearchModal) { 21 | SearchUI.showSearchModal(); 22 | } else { 23 | console.error("SearchUI未正确加载"); 24 | } 25 | }, 26 | 27 | // 执行搜索(供外部调用) 28 | async executeSearch(query, filters = {}) { 29 | if (typeof SearchAPI !== "undefined" && SearchAPI.search) { 30 | return await SearchAPI.search(query, filters); 31 | } else { 32 | throw new Error("SearchAPI未正确加载"); 33 | } 34 | }, 35 | 36 | // 清除搜索缓存 37 | clearCache() { 38 | if (typeof SearchAPI !== "undefined" && SearchAPI.clearCache) { 39 | SearchAPI.clearCache(); 40 | } 41 | }, 42 | }; 43 | 44 | // 导出到全局 45 | if (typeof window !== "undefined") { 46 | window.SearchHandler = SearchHandler; 47 | } 48 | 49 | // 模块导出 50 | if (typeof module !== "undefined" && module.exports) { 51 | module.exports = SearchHandler; 52 | } 53 | -------------------------------------------------------------------------------- /docker/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "🚀 微信文件传输助手 Docker部署脚本" 4 | echo "==================================" 5 | 6 | # 检查Docker是否安装 7 | if ! command -v docker &> /dev/null; then 8 | echo "❌ Docker未安装,请先安装Docker" 9 | exit 1 10 | fi 11 | 12 | if ! command -v docker-compose &> /dev/null; then 13 | echo "❌ Docker Compose未安装,请先安装Docker Compose" 14 | exit 1 15 | fi 16 | 17 | echo "✅ Docker环境检查通过" 18 | 19 | # 创建数据目录 20 | echo "📁 创建数据目录..." 21 | mkdir -p ../data ../uploads 22 | chmod 755 ../data ../uploads 23 | 24 | # 检查环境配置文件 25 | if [ ! -f .env ]; then 26 | echo "📝 创建环境配置文件..." 27 | cp env.example .env 28 | echo "⚠️ 请编辑 .env 文件修改密码等配置" 29 | fi 30 | 31 | # 构建镜像 32 | echo "🔨 构建Docker镜像..." 33 | docker-compose build 34 | 35 | if [ $? -ne 0 ]; then 36 | echo "❌ 镜像构建失败" 37 | exit 1 38 | fi 39 | 40 | echo "✅ 镜像构建成功" 41 | 42 | # 启动服务 43 | echo "🌐 启动服务..." 44 | docker-compose up -d 45 | 46 | if [ $? -ne 0 ]; then 47 | echo "❌ 服务启动失败" 48 | exit 1 49 | fi 50 | 51 | echo "✅ 服务启动成功" 52 | 53 | # 等待服务就绪 54 | echo "⏳ 等待服务就绪..." 55 | sleep 10 56 | 57 | # 检查服务状态 58 | if curl -s http://localhost:3000/login.html > /dev/null; then 59 | echo "✅ 服务运行正常" 60 | echo "" 61 | echo "🎉 部署完成!" 62 | echo "📱 访问地址: http://localhost:3000" 63 | echo "🔐 默认密码: 3zHb0d44eW^mzLj" 64 | echo "" 65 | echo "📋 常用命令:" 66 | echo " 查看日志: docker-compose logs -f" 67 | echo " 停止服务: docker-compose down" 68 | echo " 重启服务: docker-compose restart" 69 | else 70 | echo "⚠️ 服务可能未完全启动,请稍后访问 http://localhost:3000" 71 | fi 72 | -------------------------------------------------------------------------------- /public/icons/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Cloudflare Workers 8 | .wrangler/ 9 | dist/ 10 | .cloudflare/ 11 | 12 | # Environment variables and secrets 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | # IDE and editor files 20 | .vscode/ 21 | .idea/ 22 | *.swp 23 | *.swo 24 | *~ 25 | 26 | # OS generated files 27 | .DS_Store 28 | .DS_Store? 29 | ._* 30 | .Spotlight-V100 31 | .Trashes 32 | ehthumbs.db 33 | Thumbs.db 34 | 35 | # Logs 36 | logs 37 | *.log 38 | 39 | # Runtime data 40 | pids 41 | *.pid 42 | *.seed 43 | *.pid.lock 44 | 45 | # Coverage directory used by tools like istanbul 46 | coverage/ 47 | *.lcov 48 | 49 | # nyc test coverage 50 | .nyc_output 51 | 52 | # Dependency directories 53 | jspm_packages/ 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # parcel-bundler cache (https://parceljs.org/) 71 | .cache 72 | .parcel-cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | # TernJS port file 93 | .tern-port 94 | 95 | # Stores VSCode versions used for testing VSCode extensions 96 | .vscode-test 97 | 98 | # Temporary folders 99 | tmp/ 100 | temp/ 101 | 102 | # Build outputs 103 | build/ 104 | dist/ 105 | 106 | # Local development files 107 | *.local 108 | 109 | # Icon conversion scripts 110 | svg-to-icons-converter.js -------------------------------------------------------------------------------- /database/schema.sql: -------------------------------------------------------------------------------- 1 | -- 微信文件传输助手数据库结构 2 | 3 | -- 消息表 4 | CREATE TABLE IF NOT EXISTS messages ( 5 | id INTEGER PRIMARY KEY AUTOINCREMENT, 6 | type TEXT NOT NULL CHECK (type IN ('text', 'file', 'ai_response', 'ai_thinking')), -- 消息类型:文本、文件、AI响应、AI思考 7 | content TEXT, -- 文本消息内容 8 | file_id INTEGER, -- 关联的文件ID(如果是文件消息) 9 | device_id TEXT NOT NULL, -- 发送设备标识 10 | status TEXT DEFAULT 'sent' CHECK (status IN ('sending', 'sent', 'failed', 'read')), -- 消息状态 11 | read_by TEXT DEFAULT '[]', -- 已读设备列表(JSON格式) 12 | retry_count INTEGER DEFAULT 0, -- 重试次数 13 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 14 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 15 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 16 | FOREIGN KEY (file_id) REFERENCES files(id) 17 | ); 18 | 19 | -- 文件表 20 | CREATE TABLE IF NOT EXISTS files ( 21 | id INTEGER PRIMARY KEY AUTOINCREMENT, 22 | original_name TEXT NOT NULL, -- 原始文件名 23 | file_name TEXT NOT NULL, -- 存储在R2中的文件名 24 | file_size INTEGER NOT NULL, -- 文件大小(字节) 25 | mime_type TEXT NOT NULL, -- 文件MIME类型 26 | r2_key TEXT NOT NULL UNIQUE, -- R2存储的key 27 | upload_device_id TEXT NOT NULL, -- 上传设备标识 28 | download_count INTEGER DEFAULT 0, -- 下载次数 29 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 30 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 31 | ); 32 | 33 | -- 设备表 34 | CREATE TABLE IF NOT EXISTS devices ( 35 | id TEXT PRIMARY KEY, -- 设备唯一标识 36 | name TEXT, -- 设备名称 37 | last_active DATETIME DEFAULT CURRENT_TIMESTAMP, 38 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 39 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 40 | ); 41 | 42 | -- 创建索引以提高查询性能 43 | CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp DESC); 44 | CREATE INDEX IF NOT EXISTS idx_messages_device_id ON messages(device_id); 45 | CREATE INDEX IF NOT EXISTS idx_messages_type ON messages(type); 46 | CREATE INDEX IF NOT EXISTS idx_files_r2_key ON files(r2_key); 47 | CREATE INDEX IF NOT EXISTS idx_files_upload_device ON files(upload_device_id); 48 | CREATE INDEX IF NOT EXISTS idx_devices_last_active ON devices(last_active DESC); 49 | 50 | -- 插入默认设备(可选) 51 | INSERT OR IGNORE INTO devices (id, name) VALUES 52 | ('web-default', 'Web浏览器'), 53 | ('mobile-default', '移动设备'); 54 | -------------------------------------------------------------------------------- /public/css/reset.css: -------------------------------------------------------------------------------- 1 | /* CSS Reset - 重置样式 */ 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | scroll-behavior: smooth; 11 | } 12 | 13 | html, body { 14 | height: 100%; 15 | /* iOS Safari 视口修复 */ 16 | height: 100vh; 17 | height: -webkit-fill-available; 18 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 19 | 'Ubuntu', 'Cantarell', 'Open Sans', 'Helvetica Neue', sans-serif; 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | text-rendering: optimizeLegibility; 23 | background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); 24 | background-attachment: fixed; 25 | color: #333; 26 | font-size: 16px; /* 增大基础字体 */ 27 | line-height: 1.6; 28 | /* iOS Safari 特殊处理 */ 29 | -webkit-text-size-adjust: 100%; 30 | -webkit-touch-callout: none; 31 | -webkit-user-select: none; 32 | user-select: none; 33 | } 34 | 35 | /* iOS Safari 视口变量 */ 36 | :root { 37 | --vh: 1vh; 38 | --safe-area-inset-top: env(safe-area-inset-top); 39 | --safe-area-inset-right: env(safe-area-inset-right); 40 | --safe-area-inset-bottom: env(safe-area-inset-bottom); 41 | --safe-area-inset-left: env(safe-area-inset-left); 42 | } 43 | 44 | /* 移除默认样式 */ 45 | button { 46 | border: none; 47 | background: none; 48 | cursor: pointer; 49 | font-family: inherit; 50 | transition: all 0.3s ease; 51 | } 52 | 53 | input, textarea { 54 | border: none; 55 | outline: none; 56 | font-family: inherit; 57 | transition: all 0.3s ease; 58 | } 59 | 60 | a { 61 | text-decoration: none; 62 | color: inherit; 63 | transition: all 0.3s ease; 64 | } 65 | 66 | ul, ol { 67 | list-style: none; 68 | } 69 | 70 | img { 71 | max-width: 100%; 72 | height: auto; 73 | border-radius: 8px; 74 | } 75 | 76 | /* 选择文本样式 */ 77 | ::selection { 78 | background: rgba(7, 193, 96, 0.2); 79 | color: #333; 80 | } 81 | 82 | ::-moz-selection { 83 | background: rgba(7, 193, 96, 0.2); 84 | color: #333; 85 | } 86 | 87 | /* 焦点样式 */ 88 | button:focus-visible, 89 | input:focus-visible, 90 | textarea:focus-visible { 91 | outline: 2px solid rgba(7, 193, 96, 0.5); 92 | outline-offset: 2px; 93 | } 94 | 95 | /* 滚动条样式 */ 96 | ::-webkit-scrollbar { 97 | width: 6px; 98 | } 99 | 100 | ::-webkit-scrollbar-track { 101 | background: #f1f1f1; 102 | } 103 | 104 | ::-webkit-scrollbar-thumb { 105 | background: #c1c1c1; 106 | border-radius: 3px; 107 | } 108 | 109 | ::-webkit-scrollbar-thumb:hover { 110 | background: #a8a8a8; 111 | } 112 | -------------------------------------------------------------------------------- /public/js/components/functionButton.js: -------------------------------------------------------------------------------- 1 | // 功能按钮组件 - 微信风格动态输入功能 2 | // 实现输入框为空时显示的圆形加号按钮 3 | 4 | const FunctionButton = { 5 | // 组件状态 6 | isVisible: true, 7 | 8 | // DOM 元素引用 9 | elements: { 10 | functionButton: null, 11 | }, 12 | 13 | // 初始化功能按钮 14 | init() { 15 | // 确保DOM已加载 16 | if (document.readyState === "loading") { 17 | document.addEventListener("DOMContentLoaded", () => { 18 | this.doInit(); 19 | }); 20 | } else { 21 | this.doInit(); 22 | } 23 | }, 24 | 25 | // 执行实际初始化 26 | doInit() { 27 | this.cacheElements(); 28 | this.bindEvents(); 29 | this.updateVisibility(); 30 | }, 31 | 32 | // 缓存DOM元素 33 | cacheElements() { 34 | this.elements.functionButton = document.getElementById("functionButton"); 35 | 36 | // 检查关键元素是否存在 37 | if (!this.elements.functionButton) { 38 | console.error("FunctionButton: 找不到功能按钮元素 #functionButton"); 39 | } 40 | }, 41 | 42 | // 绑定事件 43 | bindEvents() { 44 | if (this.elements.functionButton) { 45 | // 点击功能按钮直接弹出菜单 46 | this.elements.functionButton.addEventListener("click", (e) => { 47 | e.preventDefault(); 48 | e.stopPropagation(); 49 | if ( 50 | window.FunctionMenu && 51 | typeof window.FunctionMenu.show === "function" 52 | ) { 53 | window.FunctionMenu.show(); 54 | } 55 | }); 56 | } else { 57 | console.error("FunctionButton: 无法绑定事件,按钮元素不存在"); 58 | } 59 | }, 60 | 61 | // 显示功能按钮 62 | show() { 63 | if (!this.elements.functionButton) return; 64 | 65 | this.isVisible = true; 66 | this.elements.functionButton.classList.remove("hide"); 67 | this.elements.functionButton.classList.add("show"); 68 | }, 69 | 70 | // 隐藏功能按钮 71 | hide() { 72 | if (!this.elements.functionButton) return; 73 | 74 | this.isVisible = false; 75 | this.elements.functionButton.classList.remove("show"); 76 | this.elements.functionButton.classList.add("hide"); 77 | }, 78 | 79 | // 更新可见性(根据输入框状态) 80 | updateVisibility() { 81 | const messageText = document.getElementById("messageText"); 82 | if (!messageText) { 83 | console.warn("FunctionButton: 找不到输入框元素"); 84 | return; 85 | } 86 | 87 | const hasContent = messageText.value.trim().length > 0; 88 | 89 | if (hasContent) { 90 | this.hide(); 91 | } else { 92 | this.show(); 93 | } 94 | }, 95 | 96 | // 获取当前状态 97 | getState() { 98 | return { 99 | isVisible: this.isVisible, 100 | }; 101 | }, 102 | }; 103 | 104 | // 导出组件(如果使用模块系统) 105 | if (typeof module !== "undefined" && module.exports) { 106 | module.exports = FunctionButton; 107 | } 108 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # 微信文件传输助手 - Docker 版本 2 | 3 | 这是微信文件传输助手的 Docker 部署版本,与 Cloudflare Workers 版本完全独立。 4 | 5 | ## 🚀 快速开始 6 | 7 | ### 1. 配置环境变量 8 | 9 | ```bash 10 | cd docker 11 | cp env.example .env 12 | # 编辑 .env 文件修改密码等配置 13 | ``` 14 | 15 | ### 2. 构建并启动 16 | 17 | ```bash 18 | docker-compose up -d 19 | ``` 20 | 21 | ### 3. 访问应用 22 | 23 | 打开浏览器访问:http://localhost:3000 24 | 25 | ### 4. 登录 26 | 27 | - 默认密码:`3zHb0d44eW^mzLj` 28 | - 可在 `.env` 文件中修改 `ACCESS_PASSWORD` 环境变量 29 | 30 | ## 📁 目录结构 31 | 32 | ``` 33 | docker/ 34 | ├── .dockerignore # Docker构建忽略文件 35 | ├── Dockerfile # Docker镜像构建文件 36 | ├── docker-compose.yml # Docker Compose配置 37 | ├── env.example # 环境变量配置示例 38 | ├── .env # 环境变量配置(需要创建) 39 | ├── server.js # Node.js服务器 40 | ├── package.json # 依赖配置 41 | ├── deploy.sh # 一键部署脚本 42 | └── README.md # 说明文档 43 | ``` 44 | 45 | ## 🔧 配置说明 46 | 47 | ### 环境变量 48 | 49 | 所有环境变量都在 `.env` 文件中配置: 50 | 51 | - `ACCESS_PASSWORD`: 访问密码(默认:3zHb0d44eW^mzLj) 52 | - `JWT_SECRET`: JWT 密钥 53 | - `SESSION_EXPIRE_HOURS`: 会话过期时间(小时) 54 | - `MAX_LOGIN_ATTEMPTS`: 最大登录尝试次数 55 | - `DATABASE_PATH`: 数据库文件路径 56 | - `UPLOAD_PATH`: 文件上传目录 57 | - `NODE_ENV`: 运行环境(production/development) 58 | - `PORT`: 服务端口 59 | 60 | ### 数据持久化 61 | 62 | - 数据库文件:`../data/wxchat.db` 63 | - 上传文件:`../uploads/` 64 | 65 | ## 📊 功能特性 66 | 67 | ### ✅ 已实现功能 68 | 69 | - 用户认证(JWT) 70 | - 消息发送和接收 71 | - 文件上传和下载 72 | - 实时通信(SSE) 73 | - 消息搜索 74 | - 数据清理 75 | - 设备同步 76 | 77 | ### 🔄 与 Cloudflare 版本的区别 78 | 79 | - 使用 SQLite 替代 D1 数据库 80 | - 使用本地文件系统替代 R2 存储 81 | - 使用 Express 替代 Hono 框架 82 | - 使用 Node.js 替代 Cloudflare Workers 83 | 84 | ## 🛠️ 开发模式 85 | 86 | ### 本地开发 87 | 88 | ```bash 89 | cd docker 90 | npm install 91 | npm run dev 92 | ``` 93 | 94 | ### 重新构建 95 | 96 | ```bash 97 | docker-compose down 98 | docker-compose build --no-cache 99 | docker-compose up -d 100 | ``` 101 | 102 | ## 📝 常用命令 103 | 104 | ```bash 105 | # 查看日志 106 | docker-compose logs -f 107 | 108 | # 停止服务 109 | docker-compose down 110 | 111 | # 重启服务 112 | docker-compose restart 113 | 114 | # 进入容器 115 | docker-compose exec wxchat sh 116 | 117 | # 查看容器状态 118 | docker-compose ps 119 | ``` 120 | 121 | ## 🔍 故障排除 122 | 123 | ### 1. 端口冲突 124 | 125 | 如果 3000 端口被占用,修改 `docker-compose.yml` 中的端口映射: 126 | 127 | ```yaml 128 | ports: 129 | - "3001:3000" # 改为3001端口 130 | ``` 131 | 132 | ### 2. 权限问题 133 | 134 | 确保数据目录有正确的权限: 135 | 136 | ```bash 137 | chmod 755 ../data ../uploads 138 | ``` 139 | 140 | ### 3. 数据库问题 141 | 142 | 重新初始化数据库: 143 | 144 | ```bash 145 | docker-compose exec wxchat ./init-db.sh 146 | ``` 147 | 148 | ## 📈 性能优化 149 | 150 | ### 1. 数据库优化 151 | 152 | - 已创建必要的索引 153 | - 支持分页查询 154 | - 自动清理过期数据 155 | 156 | ### 2. 文件存储 157 | 158 | - 支持大文件上传(最大 100MB) 159 | - 自动生成唯一文件名 160 | - 支持断点续传(可扩展) 161 | 162 | ### 3. 实时通信 163 | 164 | - SSE 长连接 165 | - 自动重连机制 166 | - 心跳检测 167 | 168 | ## 🔒 安全说明 169 | 170 | - 使用 JWT 进行身份认证 171 | - 文件上传类型检查 172 | - 访问频率限制 173 | - 数据清理确认机制 174 | 175 | ## 📞 支持 176 | 177 | 如有问题,请查看: 178 | 179 | 1. 容器日志:`docker-compose logs` 180 | 2. 数据库文件:`../data/wxchat.db` 181 | 3. 上传文件:`../uploads/` 182 | 183 | --- 184 | 185 | **注意**: 此 Docker 版本与 Cloudflare Workers 版本完全独立,数据不互通。 186 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "微信文件传输助手", 3 | "short_name": "wxchat", 4 | "description": "基于Cloudflare Workers的微信文件传输助手Web应用", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "orientation": "portrait-primary", 8 | "theme_color": "#07c160", 9 | "background_color": "#ffffff", 10 | "scope": "/", 11 | "lang": "zh-CN", 12 | "dir": "ltr", 13 | "categories": ["productivity", "utilities", "communication"], 14 | "icons": [ 15 | { 16 | "src": "./icons/ios/16.png", 17 | "sizes": "16x16", 18 | "type": "image/png", 19 | "purpose": "any" 20 | }, 21 | { 22 | "src": "./icons/ios/32.png", 23 | "sizes": "32x32", 24 | "type": "image/png", 25 | "purpose": "any" 26 | }, 27 | { 28 | "src": "./icons/android/android-launchericon-48-48.png", 29 | "sizes": "48x48", 30 | "type": "image/png", 31 | "purpose": "any" 32 | }, 33 | { 34 | "src": "./icons/android/android-launchericon-72-72.png", 35 | "sizes": "72x72", 36 | "type": "image/png", 37 | "purpose": "any" 38 | }, 39 | { 40 | "src": "./icons/android/android-launchericon-96-96.png", 41 | "sizes": "96x96", 42 | "type": "image/png", 43 | "purpose": "any" 44 | }, 45 | { 46 | "src": "./icons/ios/128.png", 47 | "sizes": "128x128", 48 | "type": "image/png", 49 | "purpose": "any" 50 | }, 51 | { 52 | "src": "./icons/android/android-launchericon-144-144.png", 53 | "sizes": "144x144", 54 | "type": "image/png", 55 | "purpose": "any" 56 | }, 57 | { 58 | "src": "./icons/ios/152.png", 59 | "sizes": "152x152", 60 | "type": "image/png", 61 | "purpose": "any" 62 | }, 63 | { 64 | "src": "./icons/ios/180.png", 65 | "sizes": "180x180", 66 | "type": "image/png", 67 | "purpose": "any" 68 | }, 69 | { 70 | "src": "./icons/android/android-launchericon-192-192.png", 71 | "sizes": "192x192", 72 | "type": "image/png", 73 | "purpose": "any" 74 | }, 75 | { 76 | "src": "./icons/ios/256.png", 77 | "sizes": "256x256", 78 | "type": "image/png", 79 | "purpose": "any" 80 | }, 81 | { 82 | "src": "./icons/android/android-launchericon-512-512.png", 83 | "sizes": "512x512", 84 | "type": "image/png", 85 | "purpose": "any maskable" 86 | }, 87 | { 88 | "src": "./icons/ios/1024.png", 89 | "sizes": "1024x1024", 90 | "type": "image/png", 91 | "purpose": "any" 92 | } 93 | ], 94 | "shortcuts": [ 95 | { 96 | "name": "发送文件", 97 | "short_name": "发送", 98 | "description": "快速发送文件", 99 | "url": "/?action=upload", 100 | "icons": [ 101 | { 102 | "src": "./icons/android/android-launchericon-96-96.png", 103 | "sizes": "96x96" 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "查看消息", 109 | "short_name": "消息", 110 | "description": "查看聊天消息", 111 | "url": "/?action=messages", 112 | "icons": [ 113 | { 114 | "src": "./icons/android/android-launchericon-96-96.png", 115 | "sizes": "96x96" 116 | } 117 | ] 118 | } 119 | ], 120 | 121 | "prefer_related_applications": false, 122 | "edge_side_panel": { 123 | "preferred_width": 400 124 | }, 125 | "launch_handler": { 126 | "client_mode": "focus-existing" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微信文件传输助手 Docker 版本 2 | 3 | ## 项目简介 4 | 5 | 基于 [xiyewuqiu/wxchat](https://github.com/xiyewuqiu/wxchat) 开发的微信文件传输助手,使用 Docker 容器化部署,支持 AI 聊天和图片生成功能。 6 | 7 | ## 主要功能 8 | 9 | - **跨设备文件传输** - 支持手机、电脑、平板等设备间文件传输 10 | - **AI 智能聊天** - 集成 AI 聊天功能,支持多种 AI 模型 11 | - **AI 图片生成** - 支持 AI 图片生成功能 12 | - **文件管理** - 支持各种文件类型的上传、下载、预览 13 | - **安全认证** - JWT 认证机制,保障数据安全 14 | - **PWA 支持** - 支持安装为桌面应用 15 | 16 | ### Docker 版本优化 17 | 18 | #### 用户体验优化 19 | 20 | - **长文本显示优化** - 长文本消息不会截断,完整显示内容 21 | - **滑动确认清空** - 清空数据使用滑动方法,无需再输入密码,操作更便捷 22 | - **一键复制功能** - 支持一键复制消息内容,方便快速分享 23 | - **单条消息删除** - 支持删除单条消息,灵活管理聊天记录 24 | - **时间显示修正** - 修正了前端时间显示,时间格式更准确 25 | 26 | #### 连接与状态优化 27 | 28 | - **智能连接状态** - 智能显示连接状态,实时反馈网络连接情况 29 | - **连接状态管理** - 优化连接状态逻辑,避免误显示"连接中"状态 30 | 31 | #### 配置与性能优化 32 | 33 | - **环境变量配置** - 添加 AI 模式、最大文件上传大小等环境变量,方便灵活配置 34 | - **文件传输优化** - 优化上传和下载进度显示,动态显示下载和上传速度(MB/s 或 KB/s) 35 | - **消息加载优化** - 支持一次性加载最多 100000 条消息,默认加载 5000 条 36 | - **历史消息保留** - 修复消息数超过限制时历史消息被删除的问题 37 | - **滚动位置保持** - 修复用户向上滚动查看历史消息时自动跳回底部的问题 38 | - **自动刷新优化** - 自动刷新频率从 1 秒优化为 5 秒,减少不必要的请求 39 | 40 | ## 快速开始 41 | 42 | ### 一键部署 43 | 44 | ```bash 45 | # 克隆项目 46 | git clone https://github.com/DEKVIW/docker-wxchat.git 47 | cd docker-wxchat 48 | 49 | # 进入 Docker 目录 50 | cd docker 51 | 52 | # 复制环境配置文件 53 | cp env.example .env 54 | 55 | # 编辑配置文件(重要!) 56 | nano .env 57 | 58 | # 启动服务 59 | docker-compose up -d 60 | 61 | # 查看服务状态 62 | docker-compose ps 63 | ``` 64 | 65 | ### 访问应用 66 | 67 | - **Web 界面**: http://localhost:3000 68 | - **默认密码**: 123456 69 | 70 | ### 自打包镜像 71 | 72 | 如果需要自己构建 Docker 镜像,只需要以下三个目录即可: 73 | 74 | #### 所需目录结构 75 | 76 | ``` 77 | 项目根目录/ 78 | ├── docker/ # Docker相关文件 79 | │ ├── Dockerfile 80 | │ ├── docker-compose.yml 81 | │ ├── server.js 82 | │ ├── package.json 83 | │ ├── env.example 84 | │ └── deploy.sh 85 | ├── database/ # 数据库schema 86 | │ └── schema.sql 87 | └── public/ # 前端文件 88 | ├── index.html 89 | ├── js/ 90 | ├── css/ 91 | └── ... 92 | ``` 93 | 94 | #### 构建步骤 95 | 96 | 1. **确保目录结构正确**(三个目录在同一父目录下) 97 | 98 | 2. **进入 docker 目录**: 99 | 100 | ```bash 101 | cd docker 102 | ``` 103 | 104 | 3. **准备环境变量**(如果还没有): 105 | 106 | ```bash 107 | cp env.example .env 108 | # 编辑 .env 文件修改配置 109 | ``` 110 | 111 | 4. **执行构建**: 112 | 113 | ```bash 114 | # 方式1:使用部署脚本(推荐) 115 | chmod +x deploy.sh 116 | ./deploy.sh 117 | 118 | # 方式2:手动构建 119 | docker-compose build 120 | docker-compose up -d 121 | ``` 122 | 123 | #### 重要说明 124 | 125 | - 构建上下文是项目根目录(`docker-compose.yml` 中 `context: ..`) 126 | - 这三个目录必须在同一父目录下 127 | - 需要在 `docker` 目录下执行构建命令 128 | - 确保 `docker` 目录下有 `.env` 文件(或从 `env.example` 复制) 129 | 130 | ## 配置说明 131 | 132 | ### 基础配置 133 | 134 | ```bash 135 | # 应用配置 136 | NODE_ENV=production 137 | PORT=3000 138 | 139 | # 安全配置(必须修改) 140 | ACCESS_PASSWORD=your_strong_password_here 141 | JWT_SECRET=your_jwt_secret_key_here 142 | 143 | # 文件上传配置 144 | MAX_FILE_SIZE_MB=100 145 | ``` 146 | 147 | ### AI 功能配置 148 | 149 | ```bash 150 | # AI 聊天配置 151 | AI_CHAT_BASE_URL=https://api.example.com/v1/chat/completions 152 | AI_CHAT_API_KEY=your_ai_chat_api_key_here 153 | AI_CHAT_MODEL=gpt-4o-mini 154 | 155 | # AI 图片生成配置 156 | AI_IMAGE_BASE_URL=https://api.example.com/v1/images/generations 157 | AI_IMAGE_API_KEY=your_ai_image_api_key_here 158 | AI_IMAGE_MODEL=example-model 159 | 160 | # 功能开关 161 | AI_ENABLED=true 162 | IMAGE_GEN_ENABLED=true 163 | ``` 164 | 165 | ## 开源协议 166 | 167 | 本项目遵循 [CC BY-NC-SA 4.0](https://github.com/xiyewuqiu/wxchat/blob/main/LICENSE) 开源协议。 168 | 169 | **⚠️ 重要声明**:本项目仅用于学习和研究目的,禁止用于商业用途。 170 | 171 | ## 致谢 172 | 173 | 感谢原项目作者 [xiyewuqiu](https://github.com/xiyewuqiu) 提供的优秀基础代码。 174 | -------------------------------------------------------------------------------- /public/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 登录 - 微信文件传输助手 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 | 微信文件传输助手 42 |
43 | 44 | 45 |

微信文件传输助手

46 |

请输入访问密码以继续使用

47 | 48 | 49 |
50 |
51 | 59 | 62 |
63 | 64 | 67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |

🔒 为了保护您的隐私,本应用需要密码验证

78 |

💡 如果忘记密码,请在Cloudflare控制台重新设置

79 |
80 | 81 | 82 |
83 | 🛡️ 安全提示: 84 |
    85 |
  • 请勿在公共设备上保存密码
  • 86 |
  • 建议定期更换访问密码
  • 87 |
  • 密码可在Cloudflare控制台随时修改
  • 88 |
89 |
90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /public/js/imageGen/imageGenAPI.js: -------------------------------------------------------------------------------- 1 | // SiliconFlow AI 图片生成 API 封装 2 | // 专门处理与SiliconFlow API的图片生成通信 3 | 4 | const ImageGenAPI = { 5 | // 当前请求的AbortController 6 | currentController: null, 7 | 8 | // 图片生成 - 核心方法 9 | async generateImage(prompt, options = {}) { 10 | // 取消之前的请求 11 | if (this.currentController) { 12 | this.currentController.abort(); 13 | } 14 | 15 | this.currentController = new AbortController(); 16 | 17 | try { 18 | // 构建请求参数 19 | const requestBody = { 20 | model: CONFIG.IMAGE_GEN.MODEL, 21 | prompt: prompt, 22 | image_size: options.imageSize || "1024x1024", 23 | batch_size: 1, 24 | num_inference_steps: options.numInferenceSteps || 20, 25 | guidance_scale: options.guidanceScale || 7.5, 26 | ...options, 27 | }; 28 | 29 | // 添加可选参数 30 | if (options.negativePrompt) { 31 | requestBody.negative_prompt = options.negativePrompt; 32 | } 33 | 34 | if (options.seed) { 35 | requestBody.seed = options.seed; 36 | } 37 | 38 | const response = await fetch("/api/ai/image", { 39 | method: "POST", 40 | headers: { 41 | "Content-Type": "application/json", 42 | Authorization: `Bearer ${localStorage.getItem("wxchat_auth_token")}`, 43 | }, 44 | body: JSON.stringify(requestBody), 45 | signal: this.currentController.signal, 46 | }); 47 | 48 | if (!response.ok) { 49 | const errorData = await response.json().catch(() => ({})); 50 | throw new Error( 51 | `API请求失败: ${response.status} ${response.statusText} - ${ 52 | errorData.error || "未知错误" 53 | }` 54 | ); 55 | } 56 | 57 | const result = await response.json(); 58 | 59 | if (result.success && result.data) { 60 | return { 61 | success: true, 62 | data: { 63 | imageUrl: result.data.images[0].url, 64 | seed: result.data.seed, 65 | timings: result.data.timings, 66 | prompt: prompt, 67 | options: options, 68 | }, 69 | }; 70 | } else { 71 | throw new Error(result.message || "图片生成失败"); 72 | } 73 | } catch (error) { 74 | if (error.name === "AbortError") { 75 | return { 76 | success: false, 77 | error: "请求被取消", 78 | cancelled: true, 79 | }; 80 | } 81 | 82 | return { 83 | success: false, 84 | error: error.message || "图片生成失败", 85 | }; 86 | } 87 | }, 88 | 89 | // 下载图片数据 90 | async downloadImageData(imageUrl) { 91 | try { 92 | const response = await fetch(imageUrl); 93 | 94 | if (!response.ok) { 95 | throw new Error( 96 | `图片下载失败: ${response.status} ${response.statusText}` 97 | ); 98 | } 99 | 100 | const blob = await response.blob(); 101 | 102 | return blob; 103 | } catch (error) { 104 | throw new Error(`图片下载失败: ${error.message}`); 105 | } 106 | }, 107 | 108 | // 取消当前请求 109 | cancelCurrentRequest() { 110 | if (this.currentController) { 111 | this.currentController.abort(); 112 | this.currentController = null; 113 | console.log("ImageGenAPI: 已取消当前请求"); 114 | } 115 | }, 116 | 117 | // 验证提示词 118 | validatePrompt(prompt) { 119 | if (!prompt || typeof prompt !== "string") { 120 | return { valid: false, error: "提示词不能为空" }; 121 | } 122 | 123 | if (prompt.trim().length === 0) { 124 | return { valid: false, error: "提示词不能为空" }; 125 | } 126 | 127 | if (prompt.length > 1000) { 128 | return { valid: false, error: "提示词长度不能超过1000个字符" }; 129 | } 130 | 131 | return { valid: true }; 132 | }, 133 | 134 | // 验证图片尺寸 135 | validateImageSize(size) { 136 | const validSizes = [ 137 | "512x512", 138 | "768x768", 139 | "1024x1024", 140 | "1024x1536", 141 | "1536x1024", 142 | ]; 143 | return validSizes.includes(size); 144 | }, 145 | 146 | // 获取支持的图片尺寸列表 147 | getSupportedImageSizes() { 148 | return [ 149 | { value: "512x512", label: "512×512 (正方形小图)" }, 150 | { value: "768x768", label: "768×768 (正方形中图)" }, 151 | { value: "1024x1024", label: "1024×1024 (正方形大图)" }, 152 | { value: "1024x1536", label: "1024×1536 (竖版)" }, 153 | { value: "1536x1024", label: "1536×1024 (横版)" }, 154 | ]; 155 | }, 156 | 157 | // 获取API状态 158 | getStatus() { 159 | return { 160 | hasActiveRequest: this.currentController !== null, 161 | }; 162 | }, 163 | }; 164 | 165 | // 导出到全局 166 | window.ImageGenAPI = ImageGenAPI; 167 | -------------------------------------------------------------------------------- /public/css/ios-fixes.css: -------------------------------------------------------------------------------- 1 | /* iOS Safari 专用修复样式 */ 2 | 3 | /* iOS Safari 视口和安全区域修复 */ 4 | @supports (-webkit-touch-callout: none) { 5 | /* 确保应用容器正确处理iOS视口 */ 6 | .app { 7 | height: -webkit-fill-available; 8 | min-height: -webkit-fill-available; 9 | /* 防止iOS Safari的橡皮筋效果 */ 10 | overscroll-behavior: none; 11 | -webkit-overflow-scrolling: touch; 12 | } 13 | 14 | /* 输入容器iOS优化 */ 15 | .input-container { 16 | /* 强制硬件加速,确保在iOS上正确渲染 */ 17 | -webkit-transform: translate3d(0, 0, 0); 18 | transform: translate3d(0, 0, 0); 19 | /* 确保在虚拟键盘弹出时保持可见 */ 20 | position: -webkit-sticky; 21 | position: sticky; 22 | bottom: 0; 23 | /* iOS 特殊的z-index处理 */ 24 | z-index: 9999; 25 | /* 防止iOS上的点击延迟 */ 26 | -webkit-tap-highlight-color: transparent; 27 | touch-action: manipulation; 28 | } 29 | 30 | /* 消息列表iOS优化 */ 31 | .message-list { 32 | /* iOS Safari 滚动优化 */ 33 | -webkit-overflow-scrolling: touch; 34 | /* 防止过度滚动 */ 35 | overscroll-behavior: contain; 36 | } 37 | 38 | /* 文本输入框iOS优化 */ 39 | .input-field-container textarea { 40 | /* 防止iOS上的缩放 */ 41 | -webkit-text-size-adjust: 100%; 42 | /* 优化iOS上的输入体验 */ 43 | -webkit-appearance: none; 44 | appearance: none; 45 | } 46 | } 47 | 48 | /* iOS 设备特定修复 - 竖屏模式 */ 49 | @media screen and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait) { 50 | .app { 51 | height: calc(var(--vh, 1vh) * 100); 52 | min-height: calc(var(--vh, 1vh) * 100); 53 | } 54 | 55 | /* 在小屏幕iOS设备上使用固定定位 */ 56 | @media (max-width: 768px) { 57 | .input-container { 58 | position: fixed; 59 | bottom: var(--safe-area-inset-bottom, 0); 60 | left: 0; 61 | right: 0; 62 | width: 100%; 63 | max-width: 800px; 64 | margin: 0 auto; 65 | /* 确保在iOS上的层级正确 */ 66 | z-index: 10000; 67 | } 68 | 69 | .message-list { 70 | /* 为固定的输入框留出空间 */ 71 | padding-bottom: calc(72px + var(--safe-area-inset-bottom, 0)); 72 | } 73 | } 74 | } 75 | 76 | /* iOS 设备特定修复 - 横屏模式 */ 77 | @media screen and (-webkit-min-device-pixel-ratio: 2) and (orientation: landscape) { 78 | .app { 79 | height: calc(var(--vh, 1vh) * 100); 80 | } 81 | 82 | .input-container { 83 | /* 横屏时减少padding */ 84 | padding: 6px 12px; 85 | min-height: 48px; 86 | } 87 | } 88 | 89 | /* iPhone X 系列及以上设备的安全区域适配 */ 90 | @media screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3), 91 | screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2), 92 | screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3), 93 | screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3), 94 | screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) { 95 | .input-container { 96 | /* iPhone X 系列的底部安全区域 */ 97 | padding-bottom: calc(8px + 34px); 98 | } 99 | 100 | .message-list { 101 | /* 为安全区域留出额外空间 */ 102 | padding-bottom: calc(72px + 34px); 103 | } 104 | } 105 | 106 | /* iOS 虚拟键盘弹出时的样式调整 */ 107 | body.keyboard-open { 108 | /* 防止页面滚动 */ 109 | overflow: hidden; 110 | } 111 | 112 | body.keyboard-open .app { 113 | height: 100vh; 114 | height: calc(var(--vh, 1vh) * 100); 115 | /* 防止键盘弹出时的布局跳动 */ 116 | transition: none; 117 | } 118 | 119 | body.keyboard-open .input-container { 120 | position: fixed; 121 | bottom: 0; 122 | left: 0; 123 | right: 0; 124 | width: 100%; 125 | max-width: 800px; 126 | margin: 0 auto; 127 | z-index: 10000; 128 | /* 确保在虚拟键盘上方 */ 129 | transform: translateY(0); 130 | /* 移除过渡效果,避免键盘弹出时的动画冲突 */ 131 | transition: none; 132 | /* 键盘弹出时,移除为安全区增加的padding,减小与键盘的距离 */ 133 | padding-bottom: 8px; 134 | } 135 | 136 | body.keyboard-open .message-list { 137 | /* 为固定输入框预留更多空间 */ 138 | padding-bottom: 100px; 139 | /* 防止滚动到键盘后面 */ 140 | max-height: calc(100vh - 80px); 141 | overflow-y: auto; 142 | } 143 | 144 | /* iOS Safari 特殊的滚动条样式 */ 145 | @supports (-webkit-touch-callout: none) { 146 | .message-list::-webkit-scrollbar { 147 | width: 0px; 148 | background: transparent; 149 | } 150 | 151 | .message-list::-webkit-scrollbar-thumb { 152 | background: transparent; 153 | } 154 | } 155 | 156 | /* iOS 点击高亮移除 */ 157 | @supports (-webkit-touch-callout: none) { 158 | * { 159 | -webkit-tap-highlight-color: transparent; 160 | -webkit-touch-callout: none; 161 | } 162 | 163 | /* 但保留输入框和消息文本的选择功能 */ 164 | input, 165 | textarea, 166 | .text-message, 167 | .message-content { 168 | -webkit-touch-callout: default; 169 | -webkit-user-select: text; 170 | user-select: text; 171 | } 172 | 173 | /* 确保消息文本在iOS上可以被选择 */ 174 | .text-message, 175 | .text-message.markdown-rendered { 176 | -webkit-touch-callout: default; 177 | -webkit-user-select: text; 178 | user-select: text; 179 | } 180 | } 181 | 182 | /* iOS 字体渲染优化 */ 183 | @supports (-webkit-touch-callout: none) { 184 | body { 185 | -webkit-font-smoothing: antialiased; 186 | -moz-osx-font-smoothing: grayscale; 187 | text-rendering: optimizeLegibility; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | /* 主要布局样式 */ 2 | 3 | .app { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100vh; 7 | /* iOS Safari 视口修复 */ 8 | height: calc(var(--vh, 1vh) * 100); 9 | min-height: -webkit-fill-available; 10 | max-width: 800px; 11 | margin: 0 auto; 12 | background-color: #f5f5f5; 13 | box-shadow: none; 14 | border-radius: 0; 15 | overflow: hidden; 16 | /* iOS 安全区域支持 */ 17 | padding-top: var(--safe-area-inset-top); 18 | padding-left: var(--safe-area-inset-left); 19 | padding-right: var(--safe-area-inset-right); 20 | } 21 | 22 | 23 | 24 | /* 主体区域 */ 25 | .app-main { 26 | flex: 1; 27 | display: flex; 28 | flex-direction: column; 29 | overflow: hidden; 30 | } 31 | 32 | .chat-container { 33 | flex: 1; 34 | display: flex; 35 | flex-direction: column; 36 | height: 100%; 37 | } 38 | 39 | /* 消息列表区域 - 微信标准样式 */ 40 | .message-list { 41 | flex: 1; 42 | overflow-y: auto; 43 | padding: 1rem 0.75rem; 44 | display: flex; 45 | flex-direction: column; 46 | gap: 0; 47 | background: #f5f5f5; 48 | scroll-behavior: smooth; 49 | } 50 | 51 | .message-list::-webkit-scrollbar { 52 | width: 6px; 53 | } 54 | 55 | .message-list::-webkit-scrollbar-track { 56 | background: transparent; 57 | } 58 | 59 | .message-list::-webkit-scrollbar-thumb { 60 | background: rgba(7, 193, 96, 0.3); 61 | border-radius: 3px; 62 | } 63 | 64 | .message-list::-webkit-scrollbar-thumb:hover { 65 | background: rgba(7, 193, 96, 0.5); 66 | } 67 | 68 | /* 输入区域 - 微信移动端风格 + iOS 修复 */ 69 | .input-container { 70 | border-top: 1px solid #d9d9d9; 71 | padding: 8px 12px; 72 | background-color: #f7f7f7; 73 | position: sticky; 74 | bottom: 0; 75 | z-index: 100; 76 | min-height: 56px; 77 | display: flex; 78 | align-items: center; 79 | /* iOS Safari 底部固定修复 */ 80 | padding-bottom: calc(8px + var(--safe-area-inset-bottom)); 81 | /* 确保在iOS上始终可见 */ 82 | -webkit-transform: translateZ(0); 83 | transform: translateZ(0); 84 | /* iOS虚拟键盘适配 */ 85 | transition: transform 0.3s ease; 86 | } 87 | 88 | 89 | 90 | /* 加载和空状态 */ 91 | .loading { 92 | text-align: center; 93 | color: #07c160; 94 | padding: 3rem 2rem; 95 | font-size: 1.1rem; 96 | font-weight: 500; 97 | display: flex; 98 | flex-direction: column; 99 | align-items: center; 100 | gap: 1rem; 101 | } 102 | 103 | .loading-spinner { 104 | font-size: 2rem; 105 | animation: spin 1s linear infinite; 106 | } 107 | 108 | .empty-state { 109 | text-align: center; 110 | color: #999; 111 | padding: 3rem 2rem; 112 | display: flex; 113 | flex-direction: column; 114 | align-items: center; 115 | gap: 1rem; 116 | } 117 | 118 | .empty-icon { 119 | font-size: 4rem; 120 | opacity: 0.6; 121 | background: linear-gradient(135deg, #07c160, #00d4aa); 122 | -webkit-background-clip: text; 123 | -webkit-text-fill-color: transparent; 124 | background-clip: text; 125 | filter: drop-shadow(0 2px 4px rgba(7, 193, 96, 0.2)); 126 | } 127 | 128 | .empty-state p { 129 | font-size: 1.1rem; 130 | font-weight: 500; 131 | margin: 0; 132 | } 133 | 134 | /* 动画效果 */ 135 | @keyframes fadeIn { 136 | from { 137 | opacity: 0; 138 | transform: translateY(8px); 139 | } 140 | to { 141 | opacity: 1; 142 | transform: translateY(0); 143 | } 144 | } 145 | 146 | @keyframes messageSlideIn { 147 | from { 148 | opacity: 0; 149 | transform: translateY(24px) scale(0.92); 150 | } 151 | to { 152 | opacity: 1; 153 | transform: translateY(0) scale(1); 154 | } 155 | } 156 | 157 | @keyframes shimmer { 158 | 0% { 159 | transform: translateX(-100%); 160 | } 161 | 100% { 162 | transform: translateX(100%); 163 | } 164 | } 165 | 166 | @keyframes pulse { 167 | 0%, 100% { 168 | opacity: 1; 169 | } 170 | 50% { 171 | opacity: 0.7; 172 | } 173 | } 174 | 175 | @keyframes float { 176 | from { 177 | transform: translateX(-100px); 178 | } 179 | to { 180 | transform: translateX(calc(100vw + 100px)); 181 | } 182 | } 183 | 184 | @keyframes pulse { 185 | 0%, 100% { 186 | transform: scale(1); 187 | opacity: 1; 188 | } 189 | 50% { 190 | transform: scale(1.05); 191 | opacity: 0.8; 192 | } 193 | } 194 | 195 | .fade-in { 196 | animation: fadeIn 0.1s ease-out; 197 | } 198 | 199 | /* 优化消息列表性能 */ 200 | .message-list { 201 | will-change: scroll-position; 202 | contain: layout style paint; 203 | } 204 | 205 | .message { 206 | will-change: transform, opacity; 207 | contain: layout style paint; 208 | } 209 | 210 | .pulse { 211 | animation: pulse 2s infinite; 212 | } 213 | 214 | @keyframes spin { 215 | from { transform: rotate(0deg); } 216 | to { transform: rotate(360deg); } 217 | } 218 | 219 | @keyframes shimmer { 220 | 0% { 221 | background-position: -200px 0; 222 | } 223 | 100% { 224 | background-position: calc(200px + 100%) 0; 225 | } 226 | } 227 | 228 | .shimmer { 229 | background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); 230 | background-size: 200px 100%; 231 | animation: shimmer 1.5s infinite; 232 | } 233 | -------------------------------------------------------------------------------- /public/css/daisyui-wechat.css: -------------------------------------------------------------------------------- 1 | /* DaisyUI微信风格覆盖 */ 2 | 3 | /* 覆盖DaisyUI的网格布局,使用flex布局 */ 4 | .chat { 5 | display: flex !important; 6 | flex-direction: column !important; 7 | } 8 | 9 | .chat.chat-end { 10 | align-items: flex-end; 11 | } 12 | 13 | .chat.chat-start { 14 | align-items: flex-start; 15 | } 16 | 17 | /* 消息滑入动画 */ 18 | @keyframes messageSlideIn { 19 | from { 20 | opacity: 0; 21 | transform: translateY(24px) scale(0.92); 22 | } 23 | to { 24 | opacity: 1; 25 | transform: translateY(0) scale(1); 26 | } 27 | } 28 | 29 | /* 发送消息 - 绿色气泡 */ 30 | .chat.chat-end .chat-bubble { 31 | background-color: #95ec69 !important; 32 | color: #000000 !important; 33 | border-radius: 0.5rem 0.5rem 0.5rem 0.5rem !important; 34 | position: relative; 35 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important; 36 | } 37 | 38 | /* 接收消息 - 白色气泡 */ 39 | .chat.chat-start .chat-bubble { 40 | background-color: #ffffff !important; 41 | color: #000000 !important; 42 | border: 1px solid #e5e5e5 !important; 43 | border-radius: 0.5rem 0.5rem 0.5rem 0.5rem !important; 44 | position: relative; 45 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important; 46 | } 47 | 48 | /* 发送消息箭头 */ 49 | .chat.chat-end .chat-bubble::before { 50 | content: ""; 51 | position: absolute; 52 | right: -5px; 53 | bottom: 10px; 54 | width: 0; 55 | height: 0; 56 | border-left: 5px solid #95ec69; 57 | border-top: 5px solid transparent; 58 | border-bottom: 5px solid transparent; 59 | z-index: 1; 60 | } 61 | 62 | /* 接收消息箭头 */ 63 | .chat.chat-start .chat-bubble::before { 64 | content: ""; 65 | position: absolute; 66 | left: -5px; 67 | bottom: 10px; 68 | width: 0; 69 | height: 0; 70 | border-right: 5px solid #ffffff; 71 | border-top: 5px solid transparent; 72 | border-bottom: 5px solid transparent; 73 | z-index: 1; 74 | } 75 | 76 | /* 消息时间样式 */ 77 | .chat-time { 78 | font-size: 0.75rem; 79 | color: #999999; 80 | margin-top: 0.25rem; 81 | display: flex; 82 | align-items: center; 83 | gap: 0.375rem; 84 | font-weight: 400; 85 | justify-content: flex-end; 86 | margin-bottom: 0.5rem; 87 | } 88 | 89 | .chat.chat-start .chat-time { 90 | justify-content: flex-start; 91 | } 92 | 93 | /* 文件消息样式 */ 94 | .chat-bubble .file-message { 95 | min-width: 220px; 96 | } 97 | 98 | /* 文件消息按钮样式 */ 99 | .chat-bubble .file-actions { 100 | display: flex; 101 | gap: 8px; 102 | margin-top: 8px; 103 | justify-content: flex-start; 104 | } 105 | 106 | .chat-bubble .file-actions .download-btn, 107 | .chat-bubble .file-actions .delete-btn { 108 | position: static; 109 | background: #07c160; 110 | color: white; 111 | border: none; 112 | padding: 0.5rem 0.875rem; 113 | border-radius: 4px; 114 | cursor: pointer; 115 | font-size: 0.875rem; 116 | font-weight: 400; 117 | transition: background 0.2s ease; 118 | flex-shrink: 0; 119 | width: auto; 120 | height: auto; 121 | display: inline-flex; 122 | align-items: center; 123 | justify-content: center; 124 | text-decoration: none; 125 | overflow: hidden; 126 | } 127 | 128 | .chat-bubble .file-actions .download-btn:hover { 129 | background: #06ad56; 130 | } 131 | 132 | .chat-bubble .file-actions .delete-btn { 133 | background: #07c160; 134 | } 135 | 136 | .chat-bubble .file-actions .delete-btn:hover { 137 | background: #06ad56; 138 | } 139 | 140 | .chat.chat-end .chat-bubble .file-actions .download-btn { 141 | background: rgba(0, 0, 0, 0.1); 142 | color: #000000; 143 | } 144 | 145 | .chat.chat-end .chat-bubble .file-actions .download-btn:hover { 146 | background: rgba(0, 0, 0, 0.15); 147 | } 148 | 149 | .chat.chat-end .chat-bubble .file-actions .delete-btn { 150 | background: rgba(0, 0, 0, 0.1); 151 | color: #000000; 152 | } 153 | 154 | .chat.chat-end .chat-bubble .file-actions .delete-btn:hover { 155 | background: rgba(0, 0, 0, 0.15); 156 | } 157 | 158 | /* 图片预览样式 */ 159 | .chat-bubble .image-preview { 160 | margin-top: 0.5rem; 161 | border-radius: 12px; 162 | overflow: hidden; 163 | background: #f8f9fa; 164 | border: 1px solid rgba(0, 0, 0, 0.08); 165 | max-width: 300px; 166 | position: relative; 167 | min-height: 120px; 168 | display: flex; 169 | align-items: center; 170 | justify-content: center; 171 | } 172 | 173 | .chat-bubble .image-preview img { 174 | width: 100%; 175 | height: auto; 176 | display: block; 177 | max-height: 200px; 178 | object-fit: cover; 179 | transition: transform 0.2s ease; 180 | border-radius: 8px; 181 | } 182 | 183 | /* 响应式调整 */ 184 | @media (max-width: 600px) { 185 | .chat { 186 | } 187 | } 188 | 189 | /* 文本消息的按钮样式 - 只针对文本消息 */ 190 | .chat-bubble .text-message + .copy-btn, 191 | .chat-bubble .text-message + .delete-btn { 192 | position: absolute; 193 | background: rgba(255, 255, 255, 0.8); 194 | border: none; 195 | border-radius: 50%; 196 | cursor: pointer; 197 | display: flex; 198 | align-items: center; 199 | justify-content: center; 200 | font-size: 10px; 201 | opacity: 0.6; 202 | transition: all 0.2s; 203 | pointer-events: auto; 204 | user-select: none; 205 | z-index: 2; 206 | padding: 0; 207 | width: 18px; 208 | height: 18px; 209 | } 210 | 211 | .chat-bubble .text-message + .copy-btn { 212 | top: -1px; 213 | right: -1px; 214 | } 215 | 216 | .chat-bubble .text-message + .delete-btn { 217 | bottom: 2px; 218 | left: 2px; 219 | } 220 | 221 | .chat.chat-end .chat-bubble .text-message + .copy-btn, 222 | .chat.chat-end .chat-bubble .text-message + .delete-btn { 223 | background: rgba(255, 255, 255, 0.3); 224 | } 225 | 226 | .chat-bubble .text-message + .copy-btn:hover, 227 | .chat-bubble .text-message + .delete-btn:hover { 228 | opacity: 1; 229 | transform: scale(1.1); 230 | } 231 | -------------------------------------------------------------------------------- /public/js/ai/aiAPI.js: -------------------------------------------------------------------------------- 1 | // SiliconFlow AI API 封装 2 | // 专门处理与SiliconFlow API的通信和DeepSeek-R1模型的流式响应 3 | 4 | const AIAPI = { 5 | // 当前请求的AbortController 6 | currentController: null, 7 | 8 | // 流式聊天 - 核心方法 9 | async streamChat(message, callbacks = {}) { 10 | // 取消之前的请求 11 | if (this.currentController) { 12 | this.currentController.abort(); 13 | } 14 | 15 | this.currentController = new AbortController(); 16 | 17 | try { 18 | const response = await fetch("/api/ai/chat", { 19 | method: "POST", 20 | headers: { 21 | "Content-Type": "application/json", 22 | Authorization: `Bearer ${localStorage.getItem("wxchat_auth_token")}`, 23 | }, 24 | body: JSON.stringify({ 25 | message: message, 26 | model: CONFIG.AI.MODEL, 27 | max_tokens: CONFIG.AI.MAX_TOKENS, 28 | temperature: CONFIG.AI.TEMPERATURE, 29 | stream: CONFIG.AI.STREAM, // 使用配置中的流式设置 30 | }), 31 | signal: this.currentController.signal, 32 | }); 33 | 34 | if (!response.ok) { 35 | throw new Error( 36 | `API请求失败: ${response.status} ${response.statusText}` 37 | ); 38 | } 39 | 40 | // 处理流式响应 41 | return await this.processStreamResponse(response, callbacks); 42 | } catch (error) { 43 | if (error.name === "AbortError") { 44 | return { thinking: "", response: "", cancelled: true }; 45 | } 46 | 47 | console.error("AIAPI: 请求失败", error); 48 | throw error; 49 | } finally { 50 | this.currentController = null; 51 | } 52 | }, 53 | 54 | // 处理流式响应 55 | async processStreamResponse(response, callbacks) { 56 | const reader = response.body.getReader(); 57 | const decoder = new TextDecoder(); 58 | 59 | let thinking = ""; 60 | let finalResponse = ""; 61 | let buffer = ""; 62 | let isInThinking = false; 63 | 64 | try { 65 | while (true) { 66 | const { done, value } = await reader.read(); 67 | if (done) break; 68 | 69 | // 解码数据块 70 | buffer += decoder.decode(value, { stream: true }); 71 | 72 | // 按行处理 73 | const lines = buffer.split("\n"); 74 | buffer = lines.pop() || ""; // 保留不完整的行 75 | 76 | for (const line of lines) { 77 | if (line.startsWith("data: ")) { 78 | const data = line.slice(6).trim(); 79 | if (data === "[DONE]") { 80 | continue; 81 | } 82 | 83 | try { 84 | const parsed = JSON.parse(data); 85 | const content = parsed.choices?.[0]?.delta?.content || ""; 86 | 87 | if (content) { 88 | // 检测思考过程标签 89 | if (content.includes("")) { 90 | isInThinking = true; 91 | thinking += content; 92 | callbacks.onThinking?.(thinking); 93 | } else if (content.includes("")) { 94 | isInThinking = false; 95 | thinking += content; 96 | callbacks.onThinking?.(thinking); 97 | callbacks.onThinkingComplete?.(thinking); 98 | } else if (isInThinking) { 99 | // 在思考过程中 100 | thinking += content; 101 | callbacks.onThinking?.(thinking); 102 | } else { 103 | // 最终回答 104 | finalResponse += content; 105 | callbacks.onResponse?.(content, finalResponse); 106 | } 107 | } 108 | } catch (parseError) { 109 | console.warn("AIAPI: 解析数据失败", parseError, data); 110 | } 111 | } 112 | } 113 | } 114 | 115 | return { 116 | thinking: this.cleanThinkingContent(thinking), 117 | response: finalResponse.trim(), 118 | }; 119 | } catch (error) { 120 | console.error("AIAPI: 流式处理错误", error); 121 | throw error; 122 | } finally { 123 | reader.releaseLock(); 124 | } 125 | }, 126 | 127 | // 清理思考内容,移除标签 128 | cleanThinkingContent(thinking) { 129 | if (!thinking) return ""; 130 | 131 | return thinking 132 | .replace(//g, "") 133 | .replace(/<\/think>/g, "") 134 | .trim(); 135 | }, 136 | 137 | // 取消当前请求 138 | cancelCurrentRequest() { 139 | if (this.currentController) { 140 | this.currentController.abort(); 141 | this.currentController = null; 142 | } 143 | }, 144 | 145 | // 检查API配置 146 | validateConfig() { 147 | if (!CONFIG.AI.ENABLED) { 148 | throw new Error("AI功能未启用"); 149 | } 150 | 151 | return true; 152 | }, 153 | 154 | // 测试API连接 155 | async testConnection() { 156 | try { 157 | this.validateConfig(); 158 | 159 | const response = await fetch("/api/ai/chat", { 160 | method: "POST", 161 | headers: { 162 | "Content-Type": "application/json", 163 | Authorization: `Bearer ${localStorage.getItem("wxchat_auth_token")}`, 164 | }, 165 | body: JSON.stringify({ 166 | message: "test", 167 | model: CONFIG.AI.MODEL, 168 | }), 169 | }); 170 | 171 | if (response.ok) { 172 | return true; 173 | } else { 174 | console.error("AIAPI: API连接测试失败", response.status); 175 | return false; 176 | } 177 | } catch (error) { 178 | console.error("AIAPI: API连接测试异常", error); 179 | return false; 180 | } 181 | }, 182 | 183 | // 获取API状态 184 | getStatus() { 185 | return { 186 | enabled: CONFIG.AI.ENABLED, 187 | model: CONFIG.AI.MODEL, 188 | isRequesting: !!this.currentController, 189 | }; 190 | }, 191 | }; 192 | 193 | // 导出到全局 194 | window.AIAPI = AIAPI; 195 | -------------------------------------------------------------------------------- /public/css/auth.css: -------------------------------------------------------------------------------- 1 | /* 登录页面样式 - WeChat绿色主题 */ 2 | 3 | /* 登录容器 */ 4 | .auth-container { 5 | min-height: 100vh; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | background: linear-gradient(135deg, #07c160 0%, #05a050 100%); 10 | padding: 20px; 11 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; 12 | } 13 | 14 | /* 登录卡片 */ 15 | .auth-card { 16 | background: white; 17 | border-radius: 16px; 18 | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); 19 | padding: 40px; 20 | width: 100%; 21 | max-width: 400px; 22 | text-align: center; 23 | animation: slideUp 0.6s ease-out; 24 | } 25 | 26 | @keyframes slideUp { 27 | from { 28 | opacity: 0; 29 | transform: translateY(30px); 30 | } 31 | to { 32 | opacity: 1; 33 | transform: translateY(0); 34 | } 35 | } 36 | 37 | /* 应用图标 */ 38 | .auth-icon { 39 | width: 80px; 40 | height: 80px; 41 | margin: 0 auto 24px; 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | font-size: 40px; 46 | color: white; 47 | filter: drop-shadow(0 8px 24px rgba(7, 193, 96, 0.3)); 48 | } 49 | 50 | .auth-icon-img { 51 | width: 100%; 52 | height: 100%; 53 | object-fit: contain; 54 | } 55 | 56 | /* 标题 */ 57 | .auth-title { 58 | font-size: 24px; 59 | font-weight: 600; 60 | color: #1a1a1a; 61 | margin-bottom: 8px; 62 | } 63 | 64 | /* 副标题 */ 65 | .auth-subtitle { 66 | font-size: 14px; 67 | color: #888; 68 | margin-bottom: 32px; 69 | line-height: 1.5; 70 | } 71 | 72 | /* 表单 */ 73 | .auth-form { 74 | display: flex; 75 | flex-direction: column; 76 | gap: 20px; 77 | } 78 | 79 | /* 输入框容器 */ 80 | .auth-input-group { 81 | position: relative; 82 | } 83 | 84 | /* 输入框 */ 85 | .auth-input { 86 | width: 100%; 87 | height: 50px; 88 | border: 2px solid #e5e5e5; 89 | border-radius: 12px; 90 | padding: 0 16px; 91 | font-size: 16px; 92 | background: #fafafa; 93 | transition: all 0.3s ease; 94 | box-sizing: border-box; 95 | } 96 | 97 | .auth-input:focus { 98 | outline: none; 99 | border-color: #07c160; 100 | background: white; 101 | box-shadow: 0 0 0 3px rgba(7, 193, 96, 0.1); 102 | } 103 | 104 | .auth-input::placeholder { 105 | color: #999; 106 | } 107 | 108 | /* 密码可见性切换按钮 */ 109 | .password-toggle { 110 | position: absolute; 111 | right: 16px; 112 | top: 50%; 113 | transform: translateY(-50%); 114 | background: none; 115 | border: none; 116 | color: #999; 117 | cursor: pointer; 118 | font-size: 18px; 119 | padding: 4px; 120 | transition: color 0.2s ease; 121 | } 122 | 123 | .password-toggle:hover { 124 | color: #07c160; 125 | } 126 | 127 | /* 登录按钮 */ 128 | .auth-button { 129 | height: 50px; 130 | background: linear-gradient(135deg, #07c160, #05a050); 131 | color: white; 132 | border: none; 133 | border-radius: 12px; 134 | font-size: 16px; 135 | font-weight: 600; 136 | cursor: pointer; 137 | transition: all 0.3s ease; 138 | position: relative; 139 | overflow: hidden; 140 | } 141 | 142 | .auth-button:hover:not(:disabled) { 143 | transform: translateY(-2px); 144 | box-shadow: 0 8px 24px rgba(7, 193, 96, 0.4); 145 | } 146 | 147 | .auth-button:active:not(:disabled) { 148 | transform: translateY(0); 149 | } 150 | 151 | .auth-button:disabled { 152 | opacity: 0.6; 153 | cursor: not-allowed; 154 | transform: none; 155 | } 156 | 157 | /* 加载状态 */ 158 | .auth-button.loading { 159 | color: transparent; 160 | } 161 | 162 | .auth-button.loading::after { 163 | content: ''; 164 | position: absolute; 165 | top: 50%; 166 | left: 50%; 167 | transform: translate(-50%, -50%); 168 | width: 20px; 169 | height: 20px; 170 | border: 2px solid rgba(255, 255, 255, 0.3); 171 | border-top: 2px solid white; 172 | border-radius: 50%; 173 | animation: spin 1s linear infinite; 174 | } 175 | 176 | @keyframes spin { 177 | to { 178 | transform: translate(-50%, -50%) rotate(360deg); 179 | } 180 | } 181 | 182 | /* 错误消息 */ 183 | .auth-error { 184 | background: #fff2f0; 185 | border: 1px solid #ffccc7; 186 | color: #ff4d4f; 187 | padding: 12px 16px; 188 | border-radius: 8px; 189 | font-size: 14px; 190 | margin-top: 16px; 191 | animation: shake 0.5s ease-in-out; 192 | } 193 | 194 | @keyframes shake { 195 | 0%, 100% { transform: translateX(0); } 196 | 25% { transform: translateX(-5px); } 197 | 75% { transform: translateX(5px); } 198 | } 199 | 200 | /* 尝试次数警告 */ 201 | .auth-warning { 202 | background: #fffbe6; 203 | border: 1px solid #ffe58f; 204 | color: #d48806; 205 | padding: 12px 16px; 206 | border-radius: 8px; 207 | font-size: 14px; 208 | margin-top: 16px; 209 | } 210 | 211 | /* 帮助信息 */ 212 | .auth-help { 213 | margin-top: 24px; 214 | padding-top: 24px; 215 | border-top: 1px solid #f0f0f0; 216 | font-size: 13px; 217 | color: #666; 218 | line-height: 1.5; 219 | } 220 | 221 | /* 响应式设计 */ 222 | @media (max-width: 480px) { 223 | .auth-container { 224 | padding: 16px; 225 | } 226 | 227 | .auth-card { 228 | padding: 32px 24px; 229 | } 230 | 231 | .auth-icon { 232 | width: 64px; 233 | height: 64px; 234 | font-size: 32px; 235 | } 236 | 237 | .auth-icon-img { 238 | width: 100%; 239 | height: 100%; 240 | } 241 | 242 | .auth-title { 243 | font-size: 20px; 244 | } 245 | 246 | .auth-input, 247 | .auth-button { 248 | height: 48px; 249 | } 250 | } 251 | 252 | /* 暗色模式支持 */ 253 | @media (prefers-color-scheme: dark) { 254 | .auth-card { 255 | background: #1a1a1a; 256 | color: white; 257 | } 258 | 259 | .auth-title { 260 | color: white; 261 | } 262 | 263 | .auth-subtitle { 264 | color: #999; 265 | } 266 | 267 | .auth-input { 268 | background: #2a2a2a; 269 | border-color: #404040; 270 | color: white; 271 | } 272 | 273 | .auth-input:focus { 274 | background: #333; 275 | border-color: #07c160; 276 | } 277 | 278 | .auth-help { 279 | border-top-color: #333; 280 | color: #999; 281 | } 282 | } 283 | 284 | /* 安全提示 */ 285 | .security-tips { 286 | margin-top: 20px; 287 | padding: 16px; 288 | background: #f6ffed; 289 | border: 1px solid #b7eb8f; 290 | border-radius: 8px; 291 | font-size: 12px; 292 | color: #52c41a; 293 | text-align: left; 294 | } 295 | 296 | .security-tips ul { 297 | margin: 8px 0 0 0; 298 | padding-left: 16px; 299 | } 300 | 301 | .security-tips li { 302 | margin-bottom: 4px; 303 | } 304 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 微信文件传输助手 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | 68 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 |
81 |
82 | 83 |
84 | 85 |
86 | 87 | 88 |
89 | 90 | 91 | 92 | 93 | 100 | 101 | 102 |
103 |
104 | 112 |
113 | 118 | 135 | 148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /public/js/imageGen/imageGenHandler.js: -------------------------------------------------------------------------------- 1 | // AI图片生成处理器 2 | // 负责协调图片生成的完整流程:API调用 → 图片下载 → R2上传 → 数据库存储 → UI更新 3 | 4 | const ImageGenHandler = { 5 | // 当前生成状态 6 | isGenerating: false, 7 | currentGenerationId: null, 8 | 9 | // 初始化 10 | init() { 11 | this.bindEvents(); 12 | }, 13 | 14 | // 绑定事件 15 | bindEvents() { 16 | // 监听图片生成请求 17 | document.addEventListener('imageGenRequest', (event) => { 18 | this.handleImageGeneration(event.detail); 19 | }); 20 | }, 21 | 22 | // 处理图片生成请求 - 主要流程 23 | async handleImageGeneration(options) { 24 | if (this.isGenerating) { 25 | UI.showError('正在生成图片中,请稍候...'); 26 | return; 27 | } 28 | 29 | try { 30 | this.isGenerating = true; 31 | const { prompt, negativePrompt, imageSize, guidanceScale, numInferenceSteps } = options; 32 | 33 | // 1. 验证提示词 34 | const validation = ImageGenAPI.validatePrompt(prompt); 35 | if (!validation.valid) { 36 | throw new Error(validation.error); 37 | } 38 | 39 | // 2. 显示生成状态 40 | const statusElement = this.showGeneratingStatus(prompt); 41 | 42 | // 3. 调用SiliconFlow API生成图片 43 | const generateResult = await ImageGenAPI.generateImage(prompt, { 44 | negativePrompt, 45 | imageSize: imageSize || CONFIG.IMAGE_GEN.DEFAULT_SIZE, 46 | guidanceScale: guidanceScale || CONFIG.IMAGE_GEN.DEFAULT_GUIDANCE, 47 | numInferenceSteps: numInferenceSteps || CONFIG.IMAGE_GEN.DEFAULT_STEPS 48 | }); 49 | 50 | if (!generateResult.success) { 51 | throw new Error(generateResult.error); 52 | } 53 | 54 | // 4. 更新状态为下载中 55 | this.updateGeneratingStatus(statusElement, '📥 正在下载图片...'); 56 | 57 | // 5. 下载图片数据 58 | const imageBlob = await ImageGenAPI.downloadImageData(generateResult.data.imageUrl); 59 | 60 | // 6. 更新状态为上传中 61 | this.updateGeneratingStatus(statusElement, CONFIG.IMAGE_GEN.UPLOADING_INDICATOR); 62 | 63 | // 7. 创建文件对象并上传到R2(使用现有的文件上传API) 64 | const timestamp = Date.now(); 65 | const fileName = `ai-generated-${timestamp}.png`; 66 | 67 | // 确保文件类型正确 68 | const file = new File([imageBlob], fileName, { 69 | type: 'image/png', 70 | lastModified: timestamp 71 | }); 72 | 73 | const deviceId = Utils.getDeviceId(); 74 | const uploadResult = await API.uploadFile(file, deviceId); 75 | 76 | // 8. 移除生成状态显示 77 | this.hideGeneratingStatus(statusElement); 78 | 79 | // 9. 显示成功消息 80 | UI.showSuccess(CONFIG.IMAGE_GEN.SUCCESS_INDICATOR); 81 | 82 | // 10. 刷新消息列表显示新图片(不重置已加载消息) 83 | setTimeout(async () => { 84 | await MessageHandler.loadMessages(true, false); // 强制滚动到底部 85 | }, 500); 86 | 87 | return { 88 | success: true, 89 | data: { 90 | fileId: uploadResult.fileId, 91 | fileName: uploadResult.fileName, 92 | prompt: prompt, 93 | generationData: generateResult.data 94 | } 95 | }; 96 | 97 | } catch (error) { 98 | // 根据错误类型显示不同的错误信息 99 | let errorMessage = CONFIG.ERRORS.IMAGE_GEN_FAILED; 100 | 101 | if (error.message.includes('网络')) { 102 | errorMessage = CONFIG.ERRORS.NETWORK; 103 | } else if (error.message.includes('API请求失败')) { 104 | errorMessage = CONFIG.ERRORS.IMAGE_GEN_API_ERROR; 105 | } else if (error.message.includes('下载失败')) { 106 | errorMessage = CONFIG.ERRORS.IMAGE_GEN_DOWNLOAD_FAILED; 107 | } else if (error.message.includes('上传失败')) { 108 | errorMessage = CONFIG.ERRORS.IMAGE_GEN_UPLOAD_FAILED; 109 | } else if (error.message.includes('提示词')) { 110 | errorMessage = error.message; // 使用原始提示词错误信息 111 | } else if (error.message.includes('quota') || error.message.includes('limit')) { 112 | errorMessage = CONFIG.ERRORS.IMAGE_GEN_QUOTA_EXCEEDED; 113 | } 114 | 115 | // 显示用户友好的错误信息 116 | UI.showError(errorMessage); 117 | 118 | return { 119 | success: false, 120 | error: error.message, 121 | userMessage: errorMessage 122 | }; 123 | 124 | } finally { 125 | this.isGenerating = false; 126 | this.currentGenerationId = null; 127 | } 128 | }, 129 | 130 | // 显示生成状态 131 | showGeneratingStatus(prompt) { 132 | const messageList = document.getElementById('messageList'); 133 | if (!messageList) return null; 134 | 135 | const statusId = `gen-status-${Date.now()}`; 136 | const statusElement = document.createElement('div'); 137 | statusElement.id = statusId; 138 | statusElement.className = 'message-item generating-status'; 139 | statusElement.innerHTML = ` 140 |
141 |
142 | ${CONFIG.IMAGE_GEN.GENERATING_INDICATOR} 143 |
144 |
145 | 提示词: ${this.escapeHtml(prompt)} 146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | AI图片生成 155 | ${Utils.formatTime(new Date())} 156 |
157 | `; 158 | 159 | messageList.appendChild(statusElement); 160 | messageList.scrollTop = messageList.scrollHeight; 161 | 162 | return statusElement; 163 | }, 164 | 165 | // 更新生成状态 166 | updateGeneratingStatus(statusElement, newStatus) { 167 | if (!statusElement) return; 168 | 169 | const indicator = statusElement.querySelector('.generating-indicator'); 170 | if (indicator) { 171 | indicator.textContent = newStatus; 172 | } 173 | }, 174 | 175 | // 隐藏生成状态 176 | hideGeneratingStatus(statusElement) { 177 | if (statusElement && statusElement.parentNode) { 178 | statusElement.parentNode.removeChild(statusElement); 179 | } 180 | }, 181 | 182 | // 取消当前生成 183 | cancelGeneration() { 184 | if (this.isGenerating) { 185 | ImageGenAPI.cancelCurrentRequest(); 186 | this.isGenerating = false; 187 | this.currentGenerationId = null; 188 | 189 | UI.showInfo('图片生成已取消'); 190 | } 191 | }, 192 | 193 | // 获取生成状态 194 | getStatus() { 195 | return { 196 | isGenerating: this.isGenerating, 197 | currentGenerationId: this.currentGenerationId 198 | }; 199 | }, 200 | 201 | // HTML转义 202 | escapeHtml(text) { 203 | const div = document.createElement('div'); 204 | div.textContent = text; 205 | return div.innerHTML; 206 | } 207 | }; 208 | 209 | // 导出到全局 210 | window.ImageGenHandler = ImageGenHandler; 211 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | // 微信文件传输助手 Service Worker 2 | // 提供离线缓存和后台同步功能 3 | 4 | const CACHE_NAME = "wxchat-v1.0.1"; 5 | const STATIC_CACHE_NAME = "wxchat-static-v1.0.1"; 6 | const DYNAMIC_CACHE_NAME = "wxchat-dynamic-v1.0.1"; 7 | 8 | // 需要缓存的静态资源 9 | const STATIC_ASSETS = [ 10 | "/", 11 | "/index.html", 12 | "/login.html", 13 | "/manifest.json", 14 | "/css/reset.css", 15 | "/css/main.css", 16 | "/css/components.css", 17 | "/css/responsive.css", 18 | "/css/auth.css", 19 | "/css/ios-fixes.css", 20 | "/js/config.js", 21 | "/js/utils.js", 22 | "/js/auth.js", 23 | "/js/api.js", 24 | "/js/ui.js", 25 | "/js/fileUpload.js", 26 | "/js/realtime.js", 27 | "/js/messageHandler.js", 28 | "/js/pwa.js", 29 | "/js/app.js", 30 | "/icons/android/android-launchericon-192-192.png", 31 | "/icons/android/android-launchericon-512-512.png", 32 | "/icons/ios/32.png", 33 | "/icons/ios/180.png", 34 | "https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js", 35 | ]; 36 | 37 | // 需要网络优先的资源(API请求等) 38 | const NETWORK_FIRST_PATTERNS = [/\/api\//, /\/auth\//]; 39 | 40 | // 需要完全跳过缓存的请求(实时数据) 41 | const NO_CACHE_PATTERNS = [ 42 | /\/api\/events/, // SSE连接 43 | /\/api\/files\/upload/, // 文件上传 44 | /\/api\/files\/download/, // 文件下载 45 | /\/api\/sync/, // 同步请求 46 | ]; 47 | 48 | // 需要缓存优先的资源 49 | const CACHE_FIRST_PATTERNS = [ 50 | /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/, 51 | /\.(?:css|js)$/, 52 | /\/icons\//, 53 | ]; 54 | 55 | // Service Worker 安装事件 56 | self.addEventListener("install", (event) => { 57 | console.log("🔧 Service Worker 安装中..."); 58 | 59 | event.waitUntil( 60 | Promise.all([ 61 | // 缓存静态资源 62 | caches.open(STATIC_CACHE_NAME).then(async (cache) => { 63 | console.log("📦 缓存静态资源..."); 64 | 65 | // 逐个添加资源,跳过失败的 66 | const cachePromises = STATIC_ASSETS.map(async (url) => { 67 | try { 68 | await cache.add(url); 69 | } catch (error) { 70 | console.warn(`⚠️ 缓存失败: ${url}`, error.message); 71 | } 72 | }); 73 | 74 | await Promise.all(cachePromises); 75 | // console.log("📦 静态资源缓存完成"); 76 | }), 77 | // 跳过等待,立即激活 78 | self.skipWaiting(), 79 | ]) 80 | ); 81 | }); 82 | 83 | // Service Worker 激活事件 84 | self.addEventListener("activate", (event) => { 85 | console.log("✅ Service Worker 激活中..."); 86 | 87 | event.waitUntil( 88 | Promise.all([ 89 | // 清理旧缓存 90 | caches.keys().then((cacheNames) => { 91 | return Promise.all( 92 | cacheNames.map((cacheName) => { 93 | if ( 94 | cacheName !== STATIC_CACHE_NAME && 95 | cacheName !== DYNAMIC_CACHE_NAME && 96 | cacheName.startsWith("wxchat-") 97 | ) { 98 | console.log("🗑️ 删除旧缓存:", cacheName); 99 | return caches.delete(cacheName); 100 | } 101 | }) 102 | ); 103 | }), 104 | // 立即控制所有客户端 105 | self.clients.claim(), 106 | ]) 107 | ); 108 | }); 109 | 110 | // 网络请求拦截 111 | self.addEventListener("fetch", (event) => { 112 | const { request } = event; 113 | const url = new URL(request.url); 114 | 115 | // 跳过非 HTTP(S) 请求 116 | if (!request.url.startsWith("http")) { 117 | return; 118 | } 119 | 120 | // 跳过 Chrome 扩展请求 121 | if (url.protocol === "chrome-extension:") { 122 | return; 123 | } 124 | 125 | event.respondWith(handleFetch(request)); 126 | }); 127 | 128 | // 处理网络请求的核心逻辑 129 | async function handleFetch(request) { 130 | const url = new URL(request.url); 131 | 132 | try { 133 | // 实时数据请求:完全跳过缓存,直接网络请求 134 | if (NO_CACHE_PATTERNS.some((pattern) => pattern.test(url.pathname))) { 135 | return await noCache(request); 136 | } 137 | 138 | // API 请求:网络优先策略 139 | if (NETWORK_FIRST_PATTERNS.some((pattern) => pattern.test(url.pathname))) { 140 | return await networkFirst(request); 141 | } 142 | 143 | // 静态资源:缓存优先策略 144 | if (CACHE_FIRST_PATTERNS.some((pattern) => pattern.test(url.pathname))) { 145 | return await cacheFirst(request); 146 | } 147 | 148 | // HTML 页面:网络优先,缓存备用 149 | if (request.destination === "document") { 150 | return await networkFirst(request); 151 | } 152 | 153 | // 其他请求:缓存优先 154 | return await cacheFirst(request); 155 | } catch (error) { 156 | console.error("请求处理失败:", error); 157 | 158 | // 如果是页面请求且离线,返回缓存的首页 159 | if (request.destination === "document") { 160 | const cachedResponse = await caches.match("/index.html"); 161 | if (cachedResponse) { 162 | return cachedResponse; 163 | } 164 | } 165 | 166 | // 返回离线页面或错误响应 167 | return new Response("离线状态,请检查网络连接", { 168 | status: 503, 169 | statusText: "Service Unavailable", 170 | headers: { "Content-Type": "text/plain; charset=utf-8" }, 171 | }); 172 | } 173 | } 174 | 175 | // 无缓存策略(实时数据) 176 | async function noCache(request) { 177 | // 直接网络请求,不进行任何缓存操作 178 | return fetch(request); 179 | } 180 | 181 | // 网络优先策略 182 | async function networkFirst(request) { 183 | try { 184 | const networkResponse = await fetch(request); 185 | 186 | // 只缓存GET请求的成功响应 187 | if (networkResponse.ok && request.method === "GET") { 188 | try { 189 | const cache = await caches.open(DYNAMIC_CACHE_NAME); 190 | await cache.put(request, networkResponse.clone()); 191 | } catch (cacheError) { 192 | // 缓存失败不影响主要功能,静默处理 193 | console.warn("缓存存储失败:", cacheError); 194 | } 195 | } 196 | 197 | return networkResponse; 198 | } catch (error) { 199 | // 网络失败,尝试从缓存获取(只对GET请求) 200 | if (request.method === "GET") { 201 | const cachedResponse = await caches.match(request); 202 | if (cachedResponse) { 203 | return cachedResponse; 204 | } 205 | } 206 | throw error; 207 | } 208 | } 209 | 210 | // 缓存优先策略 211 | async function cacheFirst(request) { 212 | // 只对GET请求使用缓存策略 213 | if (request.method !== "GET") { 214 | return fetch(request); 215 | } 216 | 217 | const cachedResponse = await caches.match(request); 218 | 219 | if (cachedResponse) { 220 | // 后台更新缓存 221 | updateCache(request); 222 | return cachedResponse; 223 | } 224 | 225 | // 缓存中没有,从网络获取 226 | const networkResponse = await fetch(request); 227 | 228 | if (networkResponse.ok) { 229 | try { 230 | const cache = await caches.open(DYNAMIC_CACHE_NAME); 231 | await cache.put(request, networkResponse.clone()); 232 | } catch (cacheError) { 233 | // 缓存失败不影响主要功能,静默处理 234 | console.warn("缓存存储失败:", cacheError); 235 | } 236 | } 237 | 238 | return networkResponse; 239 | } 240 | 241 | // 后台更新缓存 242 | async function updateCache(request) { 243 | // 只更新GET请求的缓存 244 | if (request.method !== "GET") { 245 | return; 246 | } 247 | 248 | try { 249 | const networkResponse = await fetch(request); 250 | if (networkResponse.ok) { 251 | try { 252 | const cache = await caches.open(DYNAMIC_CACHE_NAME); 253 | await cache.put(request, networkResponse); 254 | } catch (cacheError) { 255 | // 缓存失败不影响主要功能,静默处理 256 | console.warn("后台缓存更新失败:", cacheError); 257 | } 258 | } 259 | } catch (error) { 260 | // 静默失败,不影响用户体验 261 | console.log("后台缓存更新失败:", error); 262 | } 263 | } 264 | 265 | // 消息处理(用于与主线程通信) 266 | self.addEventListener("message", (event) => { 267 | const { type, data } = event.data; 268 | 269 | switch (type) { 270 | case "SKIP_WAITING": 271 | self.skipWaiting(); 272 | break; 273 | 274 | case "GET_VERSION": 275 | event.ports[0].postMessage({ version: CACHE_NAME }); 276 | break; 277 | 278 | case "CLEAR_CACHE": 279 | clearAllCaches().then(() => { 280 | event.ports[0].postMessage({ success: true }); 281 | }); 282 | break; 283 | 284 | default: 285 | console.log("未知消息类型:", type); 286 | } 287 | }); 288 | 289 | // 清理所有缓存 290 | async function clearAllCaches() { 291 | const cacheNames = await caches.keys(); 292 | await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName))); 293 | console.log("🗑️ 所有缓存已清理"); 294 | } 295 | 296 | // 后台同步(如果支持) 297 | if ("sync" in self.registration) { 298 | self.addEventListener("sync", (event) => { 299 | if (event.tag === "background-sync") { 300 | event.waitUntil(doBackgroundSync()); 301 | } 302 | }); 303 | } 304 | 305 | // 执行后台同步 306 | async function doBackgroundSync() { 307 | try { 308 | // 这里可以添加后台同步逻辑 309 | // 比如同步离线时的消息等 310 | console.log("🔄 执行后台同步..."); 311 | } catch (error) { 312 | console.error("后台同步失败:", error); 313 | } 314 | } 315 | 316 | console.log("🚀 Service Worker 已加载"); 317 | -------------------------------------------------------------------------------- /public/css/imageGen.css: -------------------------------------------------------------------------------- 1 | /* AI图片生成样式 - 微信风格 */ 2 | 3 | /* 图片生成模态框 */ 4 | .image-gen-modal-overlay { 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | background: rgba(0, 0, 0, 0.5); 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | z-index: 10000; 15 | padding: 20px; 16 | box-sizing: border-box; 17 | } 18 | 19 | .image-gen-modal { 20 | background: white; 21 | border-radius: 12px; 22 | width: 100%; 23 | max-width: 480px; 24 | max-height: 90vh; 25 | overflow-y: auto; 26 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 27 | animation: modalSlideIn 0.3s ease-out; 28 | } 29 | 30 | @keyframes modalSlideIn { 31 | from { 32 | opacity: 0; 33 | transform: translateY(-20px) scale(0.95); 34 | } 35 | to { 36 | opacity: 1; 37 | transform: translateY(0) scale(1); 38 | } 39 | } 40 | 41 | /* 模态框头部 */ 42 | .image-gen-header { 43 | display: flex; 44 | align-items: center; 45 | justify-content: space-between; 46 | padding: 20px 20px 0 20px; 47 | border-bottom: 1px solid #f0f0f0; 48 | margin-bottom: 20px; 49 | } 50 | 51 | .image-gen-header h3 { 52 | margin: 0; 53 | font-size: 18px; 54 | font-weight: 600; 55 | color: #333; 56 | } 57 | 58 | .close-btn { 59 | background: none; 60 | border: none; 61 | font-size: 24px; 62 | color: #999; 63 | cursor: pointer; 64 | padding: 0; 65 | width: 30px; 66 | height: 30px; 67 | display: flex; 68 | align-items: center; 69 | justify-content: center; 70 | border-radius: 50%; 71 | transition: all 0.2s ease; 72 | } 73 | 74 | .close-btn:hover { 75 | background: #f5f5f5; 76 | color: #666; 77 | } 78 | 79 | /* 模态框内容 */ 80 | .image-gen-content { 81 | padding: 0 20px; 82 | } 83 | 84 | .form-group { 85 | margin-bottom: 20px; 86 | } 87 | 88 | .form-group label { 89 | display: block; 90 | margin-bottom: 8px; 91 | font-weight: 500; 92 | color: #333; 93 | font-size: 14px; 94 | } 95 | 96 | .form-group input, 97 | .form-group textarea, 98 | .form-group select { 99 | width: 100%; 100 | padding: 12px; 101 | border: 1px solid #e0e0e0; 102 | border-radius: 8px; 103 | font-size: 14px; 104 | transition: border-color 0.2s ease; 105 | box-sizing: border-box; 106 | font-family: inherit; 107 | } 108 | 109 | .form-group input:focus, 110 | .form-group textarea:focus, 111 | .form-group select:focus { 112 | outline: none; 113 | border-color: #07c160; 114 | box-shadow: 0 0 0 2px rgba(7, 193, 96, 0.1); 115 | } 116 | 117 | .form-group textarea { 118 | resize: vertical; 119 | min-height: 80px; 120 | } 121 | 122 | /* 字符计数 */ 123 | .char-count { 124 | text-align: right; 125 | font-size: 12px; 126 | color: #666; 127 | margin-top: 4px; 128 | } 129 | 130 | /* 表单行布局 */ 131 | .form-row { 132 | display: flex; 133 | gap: 15px; 134 | } 135 | 136 | .form-row .form-group { 137 | flex: 1; 138 | } 139 | 140 | /* 滑块样式 */ 141 | input[type="range"] { 142 | width: 100%; 143 | height: 6px; 144 | border-radius: 3px; 145 | background: #e0e0e0; 146 | outline: none; 147 | -webkit-appearance: none; 148 | appearance: none; 149 | } 150 | 151 | input[type="range"]::-webkit-slider-thumb { 152 | -webkit-appearance: none; 153 | appearance: none; 154 | width: 20px; 155 | height: 20px; 156 | border-radius: 50%; 157 | background: #07c160; 158 | cursor: pointer; 159 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 160 | } 161 | 162 | input[type="range"]::-moz-range-thumb { 163 | width: 20px; 164 | height: 20px; 165 | border-radius: 50%; 166 | background: #07c160; 167 | cursor: pointer; 168 | border: none; 169 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 170 | } 171 | 172 | .range-labels { 173 | display: flex; 174 | justify-content: space-between; 175 | font-size: 12px; 176 | color: #666; 177 | margin-top: 4px; 178 | } 179 | 180 | /* 模态框底部 */ 181 | .image-gen-footer { 182 | display: flex; 183 | gap: 12px; 184 | padding: 20px; 185 | border-top: 1px solid #f0f0f0; 186 | margin-top: 20px; 187 | } 188 | 189 | .image-gen-footer button { 190 | flex: 1; 191 | padding: 12px 20px; 192 | border: none; 193 | border-radius: 8px; 194 | font-size: 14px; 195 | font-weight: 500; 196 | cursor: pointer; 197 | transition: all 0.2s ease; 198 | } 199 | 200 | .btn-cancel { 201 | background: #f5f5f5; 202 | color: #666; 203 | } 204 | 205 | .btn-cancel:hover { 206 | background: #e8e8e8; 207 | } 208 | 209 | .btn-generate { 210 | background: #07c160; 211 | color: white; 212 | } 213 | 214 | .btn-generate:hover { 215 | background: #06a552; 216 | } 217 | 218 | .btn-generate:disabled { 219 | background: #ccc; 220 | cursor: not-allowed; 221 | } 222 | 223 | /* 生成状态显示 */ 224 | .generating-status { 225 | margin: 10px 0; 226 | } 227 | 228 | .generating-message { 229 | background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%); 230 | border: 1px solid #e1e8ff; 231 | border-radius: 12px; 232 | padding: 15px; 233 | position: relative; 234 | } 235 | 236 | .generating-header { 237 | display: flex; 238 | align-items: center; 239 | margin-bottom: 10px; 240 | } 241 | 242 | .generating-indicator { 243 | font-size: 14px; 244 | font-weight: 500; 245 | color: #4a90e2; 246 | } 247 | 248 | .generating-prompt { 249 | font-size: 13px; 250 | color: #666; 251 | margin-bottom: 12px; 252 | line-height: 1.4; 253 | } 254 | 255 | .generating-prompt strong { 256 | color: #333; 257 | } 258 | 259 | /* 进度条 */ 260 | .generating-progress { 261 | margin-top: 10px; 262 | } 263 | 264 | .progress-bar { 265 | width: 100%; 266 | height: 4px; 267 | background: #e0e8ff; 268 | border-radius: 2px; 269 | overflow: hidden; 270 | } 271 | 272 | .progress-fill { 273 | height: 100%; 274 | background: linear-gradient(90deg, #4a90e2, #07c160); 275 | border-radius: 2px; 276 | animation: progressAnimation 2s ease-in-out infinite; 277 | } 278 | 279 | @keyframes progressAnimation { 280 | 0% { 281 | width: 0%; 282 | opacity: 0.8; 283 | } 284 | 50% { 285 | width: 70%; 286 | opacity: 1; 287 | } 288 | 100% { 289 | width: 100%; 290 | opacity: 0.8; 291 | } 292 | } 293 | 294 | /* 响应式设计 */ 295 | @media (max-width: 480px) { 296 | .image-gen-modal-overlay { 297 | padding: 10px; 298 | } 299 | 300 | .image-gen-modal { 301 | max-height: 95vh; 302 | } 303 | 304 | .image-gen-header, 305 | .image-gen-content, 306 | .image-gen-footer { 307 | padding-left: 15px; 308 | padding-right: 15px; 309 | } 310 | 311 | .form-row { 312 | flex-direction: column; 313 | gap: 10px; 314 | } 315 | 316 | .image-gen-footer { 317 | flex-direction: column; 318 | } 319 | 320 | .image-gen-footer button { 321 | width: 100%; 322 | } 323 | } 324 | 325 | /* 功能菜单中的图片生成按钮样式 */ 326 | .function-item[data-action="imageGen"] { 327 | background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%); 328 | } 329 | 330 | .function-item[data-action="imageGen"]:hover { 331 | background: linear-gradient(135deg, #ff5252 0%, #ff7979 100%); 332 | transform: translateY(-2px); 333 | } 334 | 335 | /* 深色模式支持 */ 336 | @media (prefers-color-scheme: dark) { 337 | .image-gen-modal { 338 | background: #2c2c2c; 339 | color: #fff; 340 | } 341 | 342 | .image-gen-header { 343 | border-bottom-color: #404040; 344 | } 345 | 346 | .image-gen-header h3 { 347 | color: #fff; 348 | } 349 | 350 | .close-btn { 351 | color: #ccc; 352 | } 353 | 354 | .close-btn:hover { 355 | background: #404040; 356 | color: #fff; 357 | } 358 | 359 | .form-group label { 360 | color: #fff; 361 | } 362 | 363 | .form-group input, 364 | .form-group textarea, 365 | .form-group select { 366 | background: #404040; 367 | border-color: #555; 368 | color: #fff; 369 | } 370 | 371 | .form-group input:focus, 372 | .form-group textarea:focus, 373 | .form-group select:focus { 374 | border-color: #07c160; 375 | box-shadow: 0 0 0 2px rgba(7, 193, 96, 0.2); 376 | } 377 | 378 | .image-gen-footer { 379 | border-top-color: #404040; 380 | } 381 | 382 | .btn-cancel { 383 | background: #404040; 384 | color: #ccc; 385 | } 386 | 387 | .btn-cancel:hover { 388 | background: #555; 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /public/js/search/searchAPI.js: -------------------------------------------------------------------------------- 1 | // 搜索API模块 - 封装搜索相关的API调用 2 | 3 | const SearchAPI = { 4 | // 搜索缓存 5 | cache: new Map(), 6 | cacheTimeout: 5 * 60 * 1000, // 5分钟缓存 7 | 8 | // 执行搜索(本地优先,后端兜底) 9 | async search(query, filters = {}) { 10 | try { 11 | // 1. 本地优先搜索 12 | if ( 13 | window.MessageHandler && 14 | typeof MessageHandler.getLocalCache === "function" 15 | ) { 16 | const localMessages = MessageHandler.getLocalCache(); 17 | const localResults = localMessages.filter((msg) => { 18 | // 支持文本和文件名模糊匹配 19 | if (msg.type === "text" && msg.content && msg.content.includes(query)) 20 | return true; 21 | if ( 22 | msg.type === "file" && 23 | msg.original_name && 24 | msg.original_name.includes(query) 25 | ) 26 | return true; 27 | return false; 28 | }); 29 | if (localResults.length > 0) { 30 | return { success: true, data: localResults, local: true }; 31 | } 32 | } 33 | // 2. 后端兜底 34 | // 构建缓存键 35 | const cacheKey = this.buildCacheKey(query, filters); 36 | 37 | // 检查缓存 38 | if (this.cache.has(cacheKey)) { 39 | const cached = this.cache.get(cacheKey); 40 | if (Date.now() - cached.timestamp < this.cacheTimeout) { 41 | return cached.data; 42 | } 43 | } 44 | 45 | // 构建搜索参数 46 | const params = new URLSearchParams(); 47 | params.append("q", query); 48 | 49 | if (filters.type && filters.type !== "all") { 50 | params.append("type", filters.type); 51 | } 52 | 53 | if (filters.timeRange && filters.timeRange !== "all") { 54 | params.append("timeRange", filters.timeRange); 55 | } 56 | 57 | if (filters.deviceId && filters.deviceId !== "all") { 58 | params.append("deviceId", filters.deviceId); 59 | } 60 | 61 | if (filters.fileType && filters.fileType !== "all") { 62 | params.append("fileType", filters.fileType); 63 | } 64 | 65 | params.append("limit", filters.limit || CONFIG.SEARCH.MAX_RESULTS); 66 | params.append("offset", filters.offset || 0); 67 | 68 | // 发送搜索请求 69 | const headers = { 70 | "Content-Type": "application/json", 71 | }; 72 | 73 | // 添加认证头 74 | const authHeaders = Auth ? Auth.addAuthHeader(headers) : headers; 75 | 76 | const response = await fetch( 77 | `${CONFIG.API.BASE_URL}${CONFIG.API.ENDPOINTS.SEARCH}?${params}`, 78 | { 79 | method: "GET", 80 | headers: authHeaders, 81 | } 82 | ); 83 | 84 | if (!response.ok) { 85 | throw new Error(`搜索请求失败: ${response.status}`); 86 | } 87 | 88 | const result = await response.json(); 89 | 90 | if (!result.success) { 91 | throw new Error(result.error || "搜索失败"); 92 | } 93 | 94 | // 缓存结果 95 | this.cache.set(cacheKey, { 96 | data: result, 97 | timestamp: Date.now(), 98 | }); 99 | 100 | return result; 101 | } catch (error) { 102 | console.error("搜索API错误:", error); 103 | throw error; 104 | } 105 | }, 106 | 107 | // 获取搜索建议 108 | async getSuggestions(query) { 109 | try { 110 | if (!query || query.trim().length < CONFIG.SEARCH.MIN_QUERY_LENGTH) { 111 | return { success: true, data: [] }; 112 | } 113 | 114 | const headers = { 115 | "Content-Type": "application/json", 116 | }; 117 | 118 | // 添加认证头 119 | const authHeaders = Auth ? Auth.addAuthHeader(headers) : headers; 120 | 121 | const response = await fetch( 122 | `${CONFIG.API.BASE_URL}${ 123 | CONFIG.API.ENDPOINTS.SEARCH_SUGGESTIONS 124 | }?q=${encodeURIComponent(query)}`, 125 | { 126 | method: "GET", 127 | headers: authHeaders, 128 | } 129 | ); 130 | 131 | if (!response.ok) { 132 | throw new Error(`建议请求失败: ${response.status}`); 133 | } 134 | 135 | const result = await response.json(); 136 | return result; 137 | } catch (error) { 138 | console.error("搜索建议API错误:", error); 139 | return { success: true, data: [] }; // 静默失败,不影响主要搜索功能 140 | } 141 | }, 142 | 143 | // 构建缓存键 144 | buildCacheKey(query, filters) { 145 | const parts = [query]; 146 | 147 | if (filters.type) parts.push(`type:${filters.type}`); 148 | if (filters.timeRange) parts.push(`time:${filters.timeRange}`); 149 | if (filters.deviceId) parts.push(`device:${filters.deviceId}`); 150 | if (filters.fileType) parts.push(`file:${filters.fileType}`); 151 | if (filters.limit) parts.push(`limit:${filters.limit}`); 152 | if (filters.offset) parts.push(`offset:${filters.offset}`); 153 | 154 | return parts.join("|"); 155 | }, 156 | 157 | // 清除缓存 158 | clearCache() { 159 | this.cache.clear(); 160 | }, 161 | 162 | // 清除过期缓存 163 | cleanExpiredCache() { 164 | const now = Date.now(); 165 | for (const [key, value] of this.cache.entries()) { 166 | if (now - value.timestamp >= this.cacheTimeout) { 167 | this.cache.delete(key); 168 | } 169 | } 170 | }, 171 | 172 | // 获取文件类型分类 173 | getFileTypeCategories() { 174 | return CONFIG.SEARCH.FILE_TYPE_CATEGORIES; 175 | }, 176 | 177 | // 格式化搜索结果 178 | formatSearchResults(results) { 179 | if (!results || !results.data || !Array.isArray(results.data)) { 180 | return []; 181 | } 182 | 183 | return results.data.map((item) => { 184 | // 格式化时间戳 185 | const timestamp = new Date(item.timestamp); 186 | const formattedTime = this.formatTimestamp(timestamp); 187 | 188 | // 获取文件图标 189 | let icon = "💬"; 190 | if (item.type === "file" && item.mime_type) { 191 | icon = Utils.getFileIcon(item.mime_type, item.original_name); 192 | } 193 | 194 | // 处理内容显示 195 | let displayContent = item.content || ""; 196 | let fileName = ""; 197 | 198 | if (item.type === "file") { 199 | fileName = item.original_name || "未知文件"; 200 | displayContent = fileName; 201 | } 202 | 203 | // 检测AI消息类型 204 | let messageType = item.type; 205 | let isAIMessage = false; 206 | 207 | if (item.content) { 208 | if (item.content.startsWith("[AI]")) { 209 | messageType = "ai_response"; 210 | isAIMessage = true; 211 | displayContent = item.content.replace("[AI]", "").trim(); 212 | } else if (item.content.startsWith("[AI-THINKING]")) { 213 | messageType = "ai_thinking"; 214 | isAIMessage = true; 215 | displayContent = item.content.replace("[AI-THINKING]", "").trim(); 216 | } 217 | } 218 | 219 | return { 220 | ...item, 221 | formattedTime, 222 | icon, 223 | displayContent, 224 | fileName, 225 | messageType, 226 | isAIMessage, 227 | fileSize: item.file_size ? Utils.formatFileSize(item.file_size) : null, 228 | }; 229 | }); 230 | }, 231 | 232 | // 格式化时间戳 233 | formatTimestamp(timestamp) { 234 | const now = new Date(); 235 | const date = new Date(timestamp); 236 | const diffMs = now.getTime() - date.getTime(); 237 | const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); 238 | 239 | if (diffDays === 0) { 240 | // 今天,显示时间 241 | return date.toLocaleTimeString("zh-CN", { 242 | hour: "2-digit", 243 | minute: "2-digit", 244 | }); 245 | } else if (diffDays === 1) { 246 | // 昨天 247 | return ( 248 | "昨天 " + 249 | date.toLocaleTimeString("zh-CN", { 250 | hour: "2-digit", 251 | minute: "2-digit", 252 | }) 253 | ); 254 | } else if (diffDays < 7) { 255 | // 一周内 256 | const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]; 257 | return ( 258 | weekdays[date.getDay()] + 259 | " " + 260 | date.toLocaleTimeString("zh-CN", { 261 | hour: "2-digit", 262 | minute: "2-digit", 263 | }) 264 | ); 265 | } else { 266 | // 超过一周,显示日期 267 | return ( 268 | date.toLocaleDateString("zh-CN") + 269 | " " + 270 | date.toLocaleTimeString("zh-CN", { 271 | hour: "2-digit", 272 | minute: "2-digit", 273 | }) 274 | ); 275 | } 276 | }, 277 | }; 278 | 279 | // 定期清理过期缓存 280 | setInterval(() => { 281 | SearchAPI.cleanExpiredCache(); 282 | }, 10 * 60 * 1000); // 每10分钟清理一次 283 | 284 | // 导出到全局 285 | if (typeof window !== "undefined") { 286 | window.SearchAPI = SearchAPI; 287 | } 288 | 289 | // 模块导出 290 | if (typeof module !== "undefined" && module.exports) { 291 | module.exports = SearchAPI; 292 | } 293 | -------------------------------------------------------------------------------- /public/css/ai.css: -------------------------------------------------------------------------------- 1 | /* AI聊天功能专用样式 - WeChat风格 */ 2 | 3 | /* AI消息容器 */ 4 | .message.ai { 5 | align-self: flex-start; 6 | align-items: flex-start; 7 | max-width: 80%; 8 | } 9 | 10 | /* AI消息内容基础样式 */ 11 | .message.ai .message-content { 12 | background: linear-gradient(135deg, #1e90ff, #4169e1); 13 | color: white; 14 | border: none; 15 | box-shadow: 0 2px 8px rgba(30, 144, 255, 0.2); 16 | position: relative; 17 | } 18 | 19 | /* AI思考过程消息 */ 20 | .ai-thinking-message { 21 | background: #f8f9fa !important; 22 | border: 1px solid #e9ecef !important; 23 | color: #6c757d !important; 24 | font-style: italic; 25 | position: relative; 26 | min-width: 200px; 27 | } 28 | 29 | .ai-thinking-header { 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: center; 33 | margin-bottom: 8px; 34 | padding-bottom: 6px; 35 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 36 | } 37 | 38 | .ai-thinking-indicator { 39 | font-size: 14px; 40 | font-weight: 500; 41 | color: #6c757d; 42 | } 43 | 44 | .ai-thinking-toggle { 45 | background: none; 46 | border: none; 47 | cursor: pointer; 48 | padding: 4px; 49 | border-radius: 4px; 50 | color: #6c757d; 51 | transition: all 0.2s ease; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | } 56 | 57 | .ai-thinking-toggle:hover { 58 | background: rgba(0, 0, 0, 0.1); 59 | color: #495057; 60 | } 61 | 62 | .ai-thinking-content { 63 | overflow: hidden; 64 | transition: max-height 0.3s ease, opacity 0.3s ease; 65 | } 66 | 67 | .ai-thinking-content.collapsed { 68 | max-height: 0; 69 | opacity: 0; 70 | } 71 | 72 | .ai-thinking-content.expanded { 73 | max-height: 300px; 74 | opacity: 1; 75 | } 76 | 77 | .thinking-text { 78 | padding: 8px 0; 79 | font-size: 13px; 80 | line-height: 1.4; 81 | white-space: pre-wrap; 82 | word-break: break-word; 83 | } 84 | 85 | /* AI响应消息 */ 86 | .ai-response-message { 87 | background: linear-gradient(135deg, #1e90ff, #4169e1) !important; 88 | color: white !important; 89 | border: none !important; 90 | box-shadow: 0 2px 8px rgba(30, 144, 255, 0.2) !important; 91 | } 92 | 93 | /* AI消息操作按钮 */ 94 | .ai-response-message .message-actions { 95 | display: flex; 96 | gap: 8px; 97 | margin-left: auto; 98 | } 99 | 100 | .ai-response-message .message-actions button { 101 | background: rgba(255, 255, 255, 0.2); 102 | border: 1px solid rgba(255, 255, 255, 0.3); 103 | color: white; 104 | padding: 4px 8px; 105 | border-radius: 4px; 106 | cursor: pointer; 107 | font-size: 12px; 108 | transition: all 0.2s ease; 109 | } 110 | 111 | .ai-response-message .message-actions button:hover { 112 | background: rgba(255, 255, 255, 0.3); 113 | border-color: rgba(255, 255, 255, 0.5); 114 | } 115 | 116 | .ai-response-message .message-actions .delete-btn:hover { 117 | background: rgba(255, 0, 0, 0.3); 118 | border-color: rgba(255, 0, 0, 0.5); 119 | } 120 | 121 | .ai-response-header { 122 | display: flex; 123 | align-items: center; 124 | justify-content: space-between; 125 | margin-bottom: 6px; 126 | padding-bottom: 4px; 127 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 128 | } 129 | 130 | .ai-response-indicator { 131 | font-size: 12px; 132 | font-weight: 500; 133 | opacity: 0.9; 134 | } 135 | 136 | /* AI响应内容样式 */ 137 | .ai-response-message .text-message { 138 | color: white !important; 139 | } 140 | 141 | .ai-response-message .text-message.markdown-rendered { 142 | padding: 8px 28px 8px 0 !important; 143 | } 144 | 145 | /* AI响应中的Markdown样式调整 */ 146 | .ai-response-message .text-message.markdown-rendered h1, 147 | .ai-response-message .text-message.markdown-rendered h2, 148 | .ai-response-message .text-message.markdown-rendered h3, 149 | .ai-response-message .text-message.markdown-rendered h4, 150 | .ai-response-message .text-message.markdown-rendered h5, 151 | .ai-response-message .text-message.markdown-rendered h6 { 152 | color: white; 153 | border-bottom: 1px solid rgba(255, 255, 255, 0.3); 154 | } 155 | 156 | .ai-response-message .text-message.markdown-rendered blockquote { 157 | border-left-color: rgba(255, 255, 255, 0.4); 158 | background: rgba(255, 255, 255, 0.1); 159 | } 160 | 161 | .ai-response-message .text-message.markdown-rendered code { 162 | background: rgba(255, 255, 255, 0.2); 163 | color: #f8f9fa; 164 | } 165 | 166 | .ai-response-message .text-message.markdown-rendered pre { 167 | background: rgba(255, 255, 255, 0.15); 168 | border: 1px solid rgba(255, 255, 255, 0.2); 169 | } 170 | 171 | .ai-response-message .text-message.markdown-rendered a { 172 | color: #87ceeb; 173 | text-decoration: underline; 174 | } 175 | 176 | .ai-response-message .text-message.markdown-rendered hr { 177 | border-top-color: rgba(255, 255, 255, 0.3); 178 | } 179 | 180 | /* AI打字指示器 */ 181 | .ai-typing-indicator { 182 | display: inline-block; 183 | color: rgba(255, 255, 255, 0.8); 184 | animation: aiTypingBlink 1s infinite; 185 | font-weight: bold; 186 | margin-left: 2px; 187 | } 188 | 189 | @keyframes aiTypingBlink { 190 | 0%, 191 | 50% { 192 | opacity: 1; 193 | } 194 | 51%, 195 | 100% { 196 | opacity: 0.3; 197 | } 198 | } 199 | 200 | /* AI模式指示器 - 切换开关形式 */ 201 | .ai-mode-indicator { 202 | position: absolute; 203 | top: -20px; 204 | right: 8px; 205 | background: #e0e0e0; 206 | color: #666; 207 | padding: 4px 8px; 208 | font-size: 10px; 209 | font-weight: 500; 210 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 211 | z-index: 10; 212 | cursor: pointer; 213 | transition: all 0.2s ease; 214 | animation: aiModeSlideIn 0.3s ease-out; 215 | user-select: none; 216 | display: flex; 217 | align-items: center; 218 | gap: 4px; 219 | } 220 | 221 | .ai-mode-indicator:hover { 222 | background: #d0d0d0; 223 | transform: scale(1.02); 224 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); 225 | } 226 | 227 | .ai-mode-indicator:active { 228 | transform: scale(0.98); 229 | } 230 | 231 | @keyframes aiModeSlideIn { 232 | from { 233 | opacity: 0; 234 | transform: translateY(-10px) scale(0.8); 235 | } 236 | to { 237 | opacity: 1; 238 | transform: translateY(0) scale(1); 239 | } 240 | } 241 | 242 | /* AI消息的Markdown切换按钮样式调整 */ 243 | .ai-response-message .markdown-toggle { 244 | background: rgba(255, 255, 255, 0.2); 245 | color: white; 246 | border: 1px solid rgba(255, 255, 255, 0.3); 247 | } 248 | 249 | .ai-response-message .markdown-toggle:hover { 250 | background: rgba(255, 255, 255, 0.3); 251 | color: white; 252 | } 253 | 254 | /* AI响应完成状态 */ 255 | .ai-response-complete { 256 | position: relative; 257 | } 258 | 259 | .ai-response-complete::after { 260 | content: "✓"; 261 | position: absolute; 262 | bottom: 4px; 263 | right: 4px; 264 | color: rgba(255, 255, 255, 0.6); 265 | font-size: 10px; 266 | font-weight: bold; 267 | } 268 | 269 | /* AI错误消息样式 */ 270 | .message.ai .message-content.ai-error { 271 | background: linear-gradient(135deg, #ff6b6b, #ee5a52) !important; 272 | color: white !important; 273 | border: 1px solid #ff5252 !important; 274 | } 275 | 276 | /* 响应式设计 */ 277 | @media (max-width: 480px) { 278 | .message.ai { 279 | max-width: 85%; 280 | } 281 | 282 | .ai-thinking-content.expanded { 283 | max-height: 200px; 284 | } 285 | 286 | .ai-mode-indicator { 287 | font-size: 9px; 288 | padding: 3px 6px; 289 | top: -15px; 290 | right: 8px; 291 | } 292 | 293 | .ai-thinking-toggle { 294 | padding: 2px; 295 | } 296 | 297 | .thinking-text { 298 | font-size: 12px; 299 | } 300 | 301 | .ai-response-indicator, 302 | .ai-thinking-indicator { 303 | font-size: 11px; 304 | } 305 | } 306 | 307 | /* 大屏幕优化 */ 308 | @media (min-width: 768px) { 309 | .message.ai { 310 | max-width: 70%; 311 | } 312 | 313 | .ai-thinking-content.expanded { 314 | max-height: 400px; 315 | } 316 | 317 | .thinking-text { 318 | font-size: 14px; 319 | } 320 | } 321 | 322 | /* AI消息动画效果 */ 323 | .message.ai.fade-in { 324 | animation: aiMessageSlideIn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); 325 | } 326 | 327 | @keyframes aiMessageSlideIn { 328 | from { 329 | opacity: 0; 330 | transform: translateX(-20px) scale(0.95); 331 | } 332 | to { 333 | opacity: 1; 334 | transform: translateX(0) scale(1); 335 | } 336 | } 337 | 338 | /* AI思考过程加载动画 */ 339 | .ai-thinking-message .ai-thinking-indicator::after { 340 | content: ""; 341 | display: inline-block; 342 | width: 4px; 343 | height: 4px; 344 | border-radius: 50%; 345 | background: currentColor; 346 | margin-left: 4px; 347 | animation: aiThinkingPulse 1.5s infinite; 348 | } 349 | 350 | @keyframes aiThinkingPulse { 351 | 0%, 352 | 100% { 353 | opacity: 0.3; 354 | transform: scale(1); 355 | } 356 | 50% { 357 | opacity: 1; 358 | transform: scale(1.2); 359 | } 360 | } 361 | 362 | /* 滚动条样式优化(AI消息区域) */ 363 | .ai-thinking-content::-webkit-scrollbar { 364 | width: 4px; 365 | } 366 | 367 | .ai-thinking-content::-webkit-scrollbar-track { 368 | background: rgba(0, 0, 0, 0.1); 369 | border-radius: 2px; 370 | } 371 | 372 | .ai-thinking-content::-webkit-scrollbar-thumb { 373 | background: rgba(0, 0, 0, 0.3); 374 | border-radius: 2px; 375 | } 376 | 377 | .ai-thinking-content::-webkit-scrollbar-thumb:hover { 378 | background: rgba(0, 0, 0, 0.5); 379 | } 380 | -------------------------------------------------------------------------------- /public/css/functionComponents.css: -------------------------------------------------------------------------------- 1 | /* 功能按钮和菜单组件样式 - 微信风格 */ 2 | 3 | /* 功能按钮样式增强 */ 4 | .function-button { 5 | background: #07c160; 6 | color: white; 7 | border: none; 8 | padding: 0; 9 | border-radius: 50%; 10 | cursor: pointer; 11 | transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); 12 | width: 32px; 13 | height: 32px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | flex-shrink: 0; 18 | margin-right: 4px; 19 | touch-action: manipulation; 20 | position: absolute; 21 | right: 4px; 22 | top: 50%; 23 | transform: translateY(-50%) scale(1); 24 | opacity: 1; 25 | box-shadow: 0 2px 8px rgba(7, 193, 96, 0.2); 26 | font-size: 16px; 27 | z-index: 10; 28 | } 29 | 30 | /* 功能按钮图标 */ 31 | .function-button .function-icon { 32 | width: 16px; 33 | height: 16px; 34 | transition: transform 0.2s ease; 35 | } 36 | 37 | /* 功能按钮悬停效果 */ 38 | .function-button:hover { 39 | background: #06ad56; 40 | transform: translateY(-50%) scale(1.05); 41 | box-shadow: 0 4px 12px rgba(7, 193, 96, 0.3); 42 | } 43 | 44 | .function-button:hover .function-icon { 45 | transform: rotate(90deg); 46 | } 47 | 48 | /* 功能按钮激活效果 */ 49 | .function-button:active { 50 | background: #059748; 51 | transform: translateY(-50%) scale(0.95); 52 | } 53 | 54 | /* 功能按钮显示状态 */ 55 | .function-button.show { 56 | transform: translateY(-50%) scale(1); 57 | opacity: 1; 58 | pointer-events: auto; 59 | } 60 | 61 | /* 功能按钮隐藏状态 */ 62 | .function-button.hide { 63 | transform: translateY(-50%) scale(0); 64 | opacity: 0; 65 | pointer-events: none; 66 | } 67 | 68 | /* 功能菜单容器 - 微信风格底部滑出 */ 69 | .function-menu { 70 | position: fixed; 71 | top: 0; 72 | left: 0; 73 | right: 0; 74 | bottom: 0; 75 | z-index: 1000; 76 | display: none; 77 | pointer-events: none; 78 | } 79 | 80 | .function-menu.show { 81 | display: block; 82 | pointer-events: auto; 83 | } 84 | 85 | /* 功能菜单遮罩层 */ 86 | .function-menu-overlay { 87 | position: absolute; 88 | top: 0; 89 | left: 0; 90 | right: 0; 91 | bottom: 0; 92 | background: rgba(0, 0, 0, 0.4); 93 | opacity: 0; 94 | transition: opacity 0.3s ease; 95 | cursor: pointer; 96 | } 97 | 98 | .function-menu.show .function-menu-overlay { 99 | opacity: 1; 100 | } 101 | 102 | /* 功能菜单内容 - 底部面板 */ 103 | .function-menu-content { 104 | position: absolute; 105 | left: 0; 106 | right: 0; 107 | top: 0; 108 | top: auto; 109 | bottom: 120px; 110 | background: white; 111 | border-radius: 16px 16px 0 0; 112 | box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1); 113 | overflow-y: auto; 114 | z-index: 1001; 115 | -webkit-overflow-scrolling: touch; 116 | touch-action: pan-y; 117 | } 118 | 119 | .function-menu.show .function-menu-content { 120 | transform: translateY(0); 121 | } 122 | 123 | /* 功能菜单头部 - 微信风格 */ 124 | .function-menu-header { 125 | display: flex; 126 | flex-direction: column; 127 | align-items: center; 128 | padding: 12px 24px 20px; 129 | border-bottom: 1px solid #f0f0f0; 130 | position: relative; 131 | } 132 | 133 | /* 拖拽指示器 */ 134 | .function-menu-header::before { 135 | content: ""; 136 | width: 36px; 137 | height: 4px; 138 | background: #d1d1d6; 139 | border-radius: 2px; 140 | margin-bottom: 16px; 141 | } 142 | 143 | .function-menu-header h3 { 144 | margin: 0; 145 | font-size: 16px; 146 | font-weight: 500; 147 | color: #666; 148 | } 149 | 150 | .function-menu-close { 151 | position: absolute; 152 | top: 12px; 153 | right: 16px; 154 | background: none; 155 | border: none; 156 | padding: 8px; 157 | cursor: pointer; 158 | border-radius: 50%; 159 | color: #666; 160 | transition: all 0.2s ease; 161 | display: flex; 162 | align-items: center; 163 | justify-content: center; 164 | width: 32px; 165 | height: 32px; 166 | } 167 | 168 | .function-menu-close:hover { 169 | background: #f5f5f5; 170 | color: #333; 171 | } 172 | 173 | /* 功能菜单网格 - 微信风格 */ 174 | .function-menu-grid { 175 | display: grid; 176 | grid-template-columns: repeat(4, 1fr); 177 | gap: 24px 16px; 178 | max-height: 50vh; 179 | overflow-y: auto; 180 | -webkit-overflow-scrolling: touch; 181 | touch-action: pan-y; 182 | } 183 | 184 | /* 功能菜单项 - 垂直布局 */ 185 | .function-menu-item { 186 | display: flex; 187 | flex-direction: column; 188 | align-items: center; 189 | padding: 12px 8px; 190 | cursor: pointer; 191 | transition: all 0.2s ease; 192 | border-radius: 8px; 193 | min-height: 80px; 194 | justify-content: center; 195 | } 196 | 197 | .function-menu-item:hover { 198 | background: rgba(0, 0, 0, 0.05); 199 | } 200 | 201 | .function-menu-item:active { 202 | background: rgba(0, 0, 0, 0.1); 203 | transform: scale(0.95); 204 | } 205 | 206 | /* 功能菜单项图标 - 微信风格 */ 207 | .function-menu-item-icon { 208 | font-size: 28px; 209 | margin-bottom: 8px; 210 | display: flex; 211 | align-items: center; 212 | justify-content: center; 213 | width: 48px; 214 | height: 48px; 215 | background: #f8f9fa; 216 | border-radius: 12px; 217 | flex-shrink: 0; 218 | transition: all 0.2s ease; 219 | } 220 | 221 | .function-menu-item:hover .function-menu-item-icon { 222 | transform: scale(1.05); 223 | } 224 | 225 | /* 功能菜单项内容 */ 226 | .function-menu-item-content { 227 | text-align: center; 228 | width: 100%; 229 | } 230 | 231 | .function-menu-item-title { 232 | font-size: 12px; 233 | font-weight: 400; 234 | color: #333; 235 | line-height: 1.2; 236 | margin: 0; 237 | } 238 | 239 | .function-menu-item-description { 240 | display: none; /* 微信风格不显示描述 */ 241 | } 242 | 243 | /* 响应式设计 - 微信风格 */ 244 | @media (max-width: 480px) { 245 | .function-menu-grid { 246 | gap: 12px 12px; 247 | grid-template-columns: repeat(5, 1fr); 248 | padding-bottom: 14px; 249 | } 250 | 251 | .function-menu-item { 252 | padding: 8px 4px; 253 | min-height: 72px; 254 | } 255 | 256 | .function-menu-item-icon { 257 | font-size: 24px; 258 | width: 44px; 259 | height: 44px; 260 | margin-bottom: 6px; 261 | } 262 | 263 | .function-menu-item-title { 264 | font-size: 11px; 265 | } 266 | 267 | .function-menu-header { 268 | padding: 10px 20px 16px; 269 | } 270 | 271 | .function-menu-header h3 { 272 | font-size: 15px; 273 | } 274 | } 275 | 276 | /* 大屏幕适配 */ 277 | @media (min-width: 768px) { 278 | .function-menu-grid { 279 | grid-template-columns: repeat(6, 1fr); 280 | padding: 32px 40px 40px; 281 | gap: 32px 20px; 282 | } 283 | 284 | .function-menu-item { 285 | min-height: 90px; 286 | padding: 16px 12px; 287 | } 288 | 289 | .function-menu-item-icon { 290 | width: 52px; 291 | height: 52px; 292 | font-size: 30px; 293 | margin-bottom: 10px; 294 | } 295 | 296 | .function-menu-item-title { 297 | font-size: 13px; 298 | } 299 | } 300 | 301 | /* 动画增强 - 底部滑出效果 */ 302 | .function-menu.animate-in .function-menu-content { 303 | animation: menuSlideUp 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 304 | } 305 | 306 | @keyframes menuSlideUp { 307 | 0% { 308 | transform: translateY(100%); 309 | } 310 | 100% { 311 | transform: translateY(0); 312 | } 313 | } 314 | 315 | /* 关闭动画 */ 316 | .function-menu.animate-out .function-menu-content { 317 | animation: menuSlideDown 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94); 318 | } 319 | 320 | @keyframes menuSlideDown { 321 | 0% { 322 | transform: translateY(0); 323 | } 324 | 100% { 325 | transform: translateY(100%); 326 | } 327 | } 328 | 329 | /* 滚动条样式 */ 330 | .function-menu-grid::-webkit-scrollbar { 331 | width: 6px; 332 | } 333 | 334 | .function-menu-grid::-webkit-scrollbar-track { 335 | background: #f1f1f1; 336 | border-radius: 3px; 337 | } 338 | 339 | .function-menu-grid::-webkit-scrollbar-thumb { 340 | background: #c1c1c1; 341 | border-radius: 3px; 342 | } 343 | 344 | .function-menu-grid::-webkit-scrollbar-thumb:hover { 345 | background: #a8a8a8; 346 | } 347 | 348 | /* 管理命令菜单项统一样式 - 与其他图标保持一致 */ 349 | .function-menu-item[data-action="clearChat"] .function-menu-item-icon, 350 | .function-menu-item[data-action="pwaManage"] .function-menu-item-icon, 351 | .function-menu-item[data-action="logout"] .function-menu-item-icon { 352 | background: #f8f9fa; 353 | color: inherit; 354 | } 355 | 356 | /* 管理命令菜单项悬停效果 - 统一样式 */ 357 | .function-menu-item[data-action="clearChat"]:hover .function-menu-item-icon, 358 | .function-menu-item[data-action="pwaManage"]:hover .function-menu-item-icon, 359 | .function-menu-item[data-action="logout"]:hover .function-menu-item-icon { 360 | background: #f8f9fa; 361 | transform: scale(1.05); 362 | } 363 | 364 | @media (max-width: 600px) { 365 | .function-menu-content { 366 | max-height: calc(100vh - 120px); 367 | } 368 | } 369 | 370 | .swiper-slide-grid { 371 | display: grid; 372 | grid-template-columns: repeat(4, 1fr); 373 | grid-template-rows: repeat(2, 1fr); 374 | gap: 20px 8px; 375 | padding: 20px 8px 28px 8px; 376 | justify-items: center; 377 | align-items: center; 378 | } 379 | @media (max-width: 600px) { 380 | .swiper-slide-grid { 381 | gap: 16px 4px; 382 | padding: 12px 2px 18px 2px; 383 | } 384 | } 385 | 386 | @media (min-width: 600px) { 387 | .function-menu-content { 388 | overflow-y: auto; 389 | } 390 | } 391 | @media (max-width: 599px) { 392 | .function-menu-content { 393 | overflow-y: visible; 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /public/js/imageGen/imageGenUI.js: -------------------------------------------------------------------------------- 1 | // AI图片生成UI组件 2 | // 负责图片生成的用户界面交互和显示 3 | 4 | const ImageGenUI = { 5 | // UI状态 6 | isModalOpen: false, 7 | currentModal: null, 8 | 9 | // 初始化 10 | init() { 11 | this.bindEvents(); 12 | }, 13 | 14 | // 绑定事件 15 | bindEvents() { 16 | // 监听模态框关闭事件 17 | document.addEventListener('click', (e) => { 18 | if (e.target.classList.contains('image-gen-modal-overlay')) { 19 | this.closeModal(); 20 | } 21 | }); 22 | 23 | // 监听ESC键关闭模态框 24 | document.addEventListener('keydown', (e) => { 25 | if (e.key === 'Escape' && this.isModalOpen) { 26 | this.closeModal(); 27 | } 28 | }); 29 | }, 30 | 31 | // 显示图片生成模态框 32 | showImageGenModal() { 33 | if (this.isModalOpen) { 34 | return; 35 | } 36 | 37 | const modal = this.createImageGenModal(); 38 | document.body.appendChild(modal); 39 | 40 | this.currentModal = modal; 41 | this.isModalOpen = true; 42 | 43 | // 聚焦到提示词输入框 44 | setTimeout(() => { 45 | const promptInput = modal.querySelector('#imageGenPrompt'); 46 | if (promptInput) { 47 | promptInput.focus(); 48 | } 49 | }, 100); 50 | }, 51 | 52 | // 创建图片生成模态框 53 | createImageGenModal() { 54 | const modal = document.createElement('div'); 55 | modal.className = 'image-gen-modal-overlay'; 56 | modal.innerHTML = ` 57 |
58 |
59 |

🎨 AI图片生成

60 | 61 |
62 | 63 |
64 |
65 | 66 | 72 |
73 | 0/${CONFIG.IMAGE_GEN.MAX_PROMPT_LENGTH} 74 |
75 |
76 | 77 |
78 | 79 | 84 |
85 | 86 |
87 |
88 | 89 | 96 |
97 | 98 |
99 | 100 | 106 |
107 |
108 | 109 |
110 | 111 | 119 |
120 | 创意 121 | 精确 122 |
123 |
124 |
125 | 126 | 132 |
133 | `; 134 | 135 | // 绑定事件 136 | this.bindModalEvents(modal); 137 | 138 | return modal; 139 | }, 140 | 141 | // 绑定模态框事件 142 | bindModalEvents(modal) { 143 | // 提示词字符计数 144 | const promptInput = modal.querySelector('#imageGenPrompt'); 145 | const charCount = modal.querySelector('#promptCharCount'); 146 | 147 | promptInput.addEventListener('input', () => { 148 | charCount.textContent = promptInput.value.length; 149 | 150 | // 字符数超限时的样式 151 | if (promptInput.value.length > CONFIG.IMAGE_GEN.MAX_PROMPT_LENGTH * 0.9) { 152 | charCount.style.color = '#ff4444'; 153 | } else { 154 | charCount.style.color = '#666'; 155 | } 156 | }); 157 | 158 | // 引导强度滑块 159 | const guidanceSlider = modal.querySelector('#imageGenGuidance'); 160 | const guidanceValue = modal.querySelector('#guidanceValue'); 161 | 162 | guidanceSlider.addEventListener('input', () => { 163 | guidanceValue.textContent = guidanceSlider.value; 164 | }); 165 | 166 | // 回车键提交 167 | promptInput.addEventListener('keydown', (e) => { 168 | if (e.key === 'Enter' && e.ctrlKey) { 169 | this.startGeneration(); 170 | } 171 | }); 172 | }, 173 | 174 | // 开始生成图片 175 | async startGeneration() { 176 | const modal = this.currentModal; 177 | if (!modal) return; 178 | 179 | // 获取表单数据 180 | const prompt = modal.querySelector('#imageGenPrompt').value.trim(); 181 | const negativePrompt = modal.querySelector('#imageGenNegativePrompt').value.trim(); 182 | const imageSize = modal.querySelector('#imageGenSize').value; 183 | const numInferenceSteps = parseInt(modal.querySelector('#imageGenSteps').value); 184 | const guidanceScale = parseFloat(modal.querySelector('#imageGenGuidance').value); 185 | 186 | // 验证提示词 187 | if (!prompt) { 188 | UI.showError('请输入图片描述'); 189 | return; 190 | } 191 | 192 | // 禁用生成按钮 193 | const generateBtn = modal.querySelector('.btn-generate'); 194 | const originalText = generateBtn.textContent; 195 | generateBtn.disabled = true; 196 | generateBtn.textContent = '🎨 生成中...'; 197 | 198 | try { 199 | // 关闭模态框 200 | this.closeModal(); 201 | 202 | // 触发图片生成事件 203 | const event = new CustomEvent('imageGenRequest', { 204 | detail: { 205 | prompt, 206 | negativePrompt: negativePrompt || undefined, 207 | imageSize, 208 | numInferenceSteps, 209 | guidanceScale 210 | } 211 | }); 212 | 213 | document.dispatchEvent(event); 214 | 215 | } catch (error) { 216 | UI.showError(`生成失败: ${error.message}`); 217 | 218 | // 恢复按钮状态 219 | generateBtn.disabled = false; 220 | generateBtn.textContent = originalText; 221 | } 222 | }, 223 | 224 | // 关闭模态框 225 | closeModal() { 226 | if (!this.isModalOpen || !this.currentModal) { 227 | return; 228 | } 229 | 230 | // 移除模态框 231 | if (this.currentModal.parentNode) { 232 | this.currentModal.parentNode.removeChild(this.currentModal); 233 | } 234 | 235 | this.currentModal = null; 236 | this.isModalOpen = false; 237 | }, 238 | 239 | // 显示快速生成输入框(简化版) 240 | showQuickGenInput() { 241 | const prompt = window.prompt('请输入图片描述:', ''); 242 | if (prompt && prompt.trim()) { 243 | // 使用默认参数快速生成 244 | const event = new CustomEvent('imageGenRequest', { 245 | detail: { 246 | prompt: prompt.trim(), 247 | imageSize: CONFIG.IMAGE_GEN.DEFAULT_SIZE, 248 | numInferenceSteps: CONFIG.IMAGE_GEN.DEFAULT_STEPS, 249 | guidanceScale: CONFIG.IMAGE_GEN.DEFAULT_GUIDANCE 250 | } 251 | }); 252 | 253 | document.dispatchEvent(event); 254 | } 255 | }, 256 | 257 | // 获取UI状态 258 | getStatus() { 259 | return { 260 | isModalOpen: this.isModalOpen, 261 | hasModal: this.currentModal !== null 262 | }; 263 | } 264 | }; 265 | 266 | // 导出到全局 267 | window.ImageGenUI = ImageGenUI; 268 | -------------------------------------------------------------------------------- /public/css/responsive.css: -------------------------------------------------------------------------------- 1 | /* 响应式设计 */ 2 | 3 | /* 平板设备 */ 4 | @media (max-width: 768px) { 5 | .app { 6 | height: 100vh; 7 | /* iOS Safari 视口修复 */ 8 | height: calc(var(--vh, 1vh) * 100); 9 | min-height: -webkit-fill-available; 10 | max-width: 100%; 11 | border-radius: 0; 12 | box-shadow: none; 13 | } 14 | 15 | .app-header { 16 | padding: 1.2rem 1rem; 17 | } 18 | 19 | .app-header h1 { 20 | font-size: 1.4rem; 21 | } 22 | 23 | .device-info { 24 | font-size: 0.8rem; 25 | flex-direction: row; 26 | gap: 1rem; 27 | } 28 | 29 | /* iOS 12 Safari Flexbox Gap 兼容性修复 */ 30 | @supports not (gap: 1rem) { 31 | .device-info > *:not(:last-child) { 32 | margin-right: 1rem; 33 | } 34 | } 35 | 36 | .connection-status { 37 | font-size: 0.75rem; 38 | } 39 | 40 | .message { 41 | max-width: 85%; 42 | margin-bottom: 0.5rem; 43 | padding: 0 0.25rem; 44 | } 45 | 46 | .message-content { 47 | padding: 0.5rem 0.75rem; 48 | font-size: 0.95rem; 49 | border-radius: 0.5rem; 50 | } 51 | 52 | .message-list { 53 | padding: 0.75rem 0.5rem; 54 | gap: 0; 55 | /* 为固定的输入框留出空间,防止最后一条消息被遮挡 */ 56 | padding-bottom: calc(72px + var(--safe-area-inset-bottom, 0)); 57 | } 58 | 59 | .input-container { 60 | /* 在移动端强制使用fixed定位,彻底解决iOS键盘和滚动问题 */ 61 | position: fixed; 62 | bottom: 0; 63 | left: 0; 64 | right: 0; 65 | width: 100%; 66 | /* 继承最大宽度和居中 */ 67 | max-width: 800px; 68 | margin: 0 auto; 69 | /* 基础样式 */ 70 | padding: 8px 12px; 71 | min-height: 56px; 72 | padding-bottom: calc(8px + var(--safe-area-inset-bottom)); 73 | z-index: 9999; 74 | } 75 | 76 | .upload-area { 77 | padding: 1.5rem 1rem; 78 | } 79 | 80 | .input-wrapper { 81 | gap: 8px; 82 | } 83 | 84 | /* iOS 12 Safari Flexbox Gap 兼容性修复 */ 85 | @supports not (gap: 8px) { 86 | .input-wrapper > *:not(:last-child) { 87 | margin-right: 8px; 88 | } 89 | } 90 | 91 | .file-button { 92 | width: 42px; 93 | height: 42px; 94 | font-size: 18px; 95 | } 96 | 97 | .input-field-container { 98 | min-height: 42px; 99 | } 100 | 101 | .input-field-container textarea { 102 | font-size: 16px; 103 | padding: 11px 13px; 104 | } 105 | } 106 | 107 | /* 手机设备 */ 108 | @media (max-width: 480px) { 109 | .app-header { 110 | padding: 1rem 0.75rem; 111 | } 112 | 113 | .app-header h1 { 114 | font-size: 1.3rem; 115 | margin-bottom: 0.3rem; 116 | } 117 | 118 | .device-info { 119 | font-size: 0.75rem; 120 | flex-direction: column; 121 | gap: 0.4rem; 122 | } 123 | 124 | /* iOS 12 Safari Flexbox Gap 兼容性修复 */ 125 | @supports not (gap: 0.4rem) { 126 | .device-info > *:not(:last-child) { 127 | margin-bottom: 0.4rem; 128 | } 129 | } 130 | 131 | .connection-status { 132 | font-size: 0.7rem; 133 | padding: 0.15rem 0.4rem; 134 | } 135 | 136 | .message-list { 137 | padding: 0.75rem 0.5rem; 138 | gap: 0; 139 | /* 同步为 fixed 输入框留出的空间 */ 140 | padding-bottom: calc(72px + var(--safe-area-inset-bottom, 0)); 141 | } 142 | 143 | .message { 144 | max-width: 92%; 145 | margin-bottom: 0.5rem; 146 | padding: 0 0.25rem; 147 | } 148 | 149 | .message-content { 150 | padding: 0.5rem 0.75rem; 151 | font-size: 0.95rem; 152 | border-radius: 0.5rem; 153 | } 154 | 155 | /* 手机设备上的文本消息优化 */ 156 | .message-content .text-message:not(.markdown-rendered) { 157 | padding: 0; 158 | margin: 0; 159 | font-weight: 400; 160 | letter-spacing: 0.01em; 161 | } 162 | 163 | .message-content .text-message.markdown-rendered { 164 | padding: 0.3rem 24px 0.3rem 0; 165 | font-weight: 400; 166 | letter-spacing: 0.01em; 167 | } 168 | 169 | .message-meta { 170 | font-size: 0.65rem; 171 | margin-bottom: 0.3rem; 172 | } 173 | 174 | .message-time { 175 | font-size: 0.6rem; 176 | padding: 0.15rem 0.4rem; 177 | } 178 | 179 | .input-container { 180 | padding: 8px 12px; 181 | min-height: 56px; 182 | /* iOS 手机特殊处理 */ 183 | padding-bottom: calc(8px + var(--safe-area-inset-bottom)); 184 | } 185 | 186 | .input-wrapper { 187 | gap: 8px; 188 | } 189 | 190 | /* iOS 12 Safari Flexbox Gap 兼容性修复 */ 191 | @supports not (gap: 8px) { 192 | .input-wrapper > *:not(:last-child) { 193 | margin-right: 8px; 194 | } 195 | } 196 | 197 | .file-button { 198 | width: 40px; 199 | height: 40px; 200 | font-size: 18px; 201 | } 202 | 203 | .input-field-container { 204 | min-height: 40px; 205 | } 206 | 207 | .input-field-container textarea { 208 | font-size: 16px; 209 | padding: 10px 12px; 210 | } 211 | 212 | .send-button { 213 | width: 34px; 214 | height: 34px; 215 | } 216 | 217 | .send-icon { 218 | width: 17px; 219 | height: 17px; 220 | } 221 | 222 | .file-upload { 223 | margin-bottom: 0.75rem; 224 | } 225 | 226 | .upload-area { 227 | padding: 0.75rem; 228 | } 229 | 230 | .upload-prompt { 231 | gap: 0.25rem; 232 | } 233 | 234 | /* iOS 12 Safari Flexbox Gap 兼容性修复 */ 235 | @supports not (gap: 0.25rem) { 236 | /* 假设 .upload-prompt 是 flex-direction: column */ 237 | .upload-prompt > *:not(:last-child) { 238 | margin-bottom: 0.25rem; 239 | } 240 | } 241 | 242 | .upload-icon { 243 | font-size: 1.2rem; 244 | } 245 | 246 | .file-info { 247 | gap: 0.5rem; 248 | } 249 | 250 | .download-btn { 251 | padding: 0.4rem 0.6rem; 252 | font-size: 0.75rem; 253 | } 254 | 255 | .app-footer { 256 | padding: 0.5rem; 257 | } 258 | 259 | .refresh-button { 260 | padding: 0.4rem 0.8rem; 261 | font-size: 0.8rem; 262 | } 263 | } 264 | 265 | /* 超小屏幕 */ 266 | @media (max-width: 320px) { 267 | .app-header h1 { 268 | font-size: 1.1rem; 269 | } 270 | 271 | .message-content { 272 | padding: 0.625rem 0.75rem; 273 | font-size: 0.9rem; 274 | } 275 | 276 | /* 超小屏幕的文本消息优化 */ 277 | .message-content .text-message:not(.markdown-rendered) { 278 | padding: 0; 279 | margin: 0; 280 | font-weight: 400; 281 | letter-spacing: 0.01em; 282 | } 283 | 284 | .message-content .text-message.markdown-rendered { 285 | padding: 0.25rem 20px 0.25rem 0; 286 | font-weight: 400; 287 | letter-spacing: 0.01em; 288 | } 289 | 290 | .file-name { 291 | font-size: 0.85rem; 292 | } 293 | 294 | .file-size { 295 | font-size: 0.75rem; 296 | } 297 | 298 | .input-field-container textarea { 299 | font-size: 15px; 300 | padding: 8px 10px; 301 | } 302 | 303 | .file-button { 304 | width: 38px; 305 | height: 38px; 306 | font-size: 16px; 307 | } 308 | 309 | .send-button { 310 | width: 30px; 311 | height: 30px; 312 | } 313 | 314 | .send-icon { 315 | width: 15px; 316 | height: 15px; 317 | } 318 | } 319 | 320 | /* 横屏适配 */ 321 | @media (max-height: 500px) and (orientation: landscape) { 322 | .app-header { 323 | padding: 0.5rem; 324 | } 325 | 326 | .app-header h1 { 327 | font-size: 1.1rem; 328 | margin-bottom: 0.2rem; 329 | } 330 | 331 | .device-info { 332 | font-size: 0.7rem; 333 | } 334 | 335 | .message-list { 336 | padding: 0.5rem; 337 | } 338 | 339 | .input-container { 340 | padding: 0.5rem; 341 | } 342 | 343 | .app-footer { 344 | padding: 0.4rem; 345 | } 346 | } 347 | 348 | /* iOS Safari 专用修复 */ 349 | @supports (-webkit-touch-callout: none) { 350 | .app { 351 | height: -webkit-fill-available; 352 | min-height: -webkit-fill-available; 353 | } 354 | } 355 | 356 | /* iOS 设备特定修复 */ 357 | @media screen and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait) { 358 | .app { 359 | height: calc(var(--vh, 1vh) * 100); 360 | min-height: calc(var(--vh, 1vh) * 100); 361 | } 362 | .input-container { 363 | padding-bottom: calc(8px + 34px); /* 34px 是 iPhone X 系列的底部安全区域 */ 364 | /* 确保此处的规则不会覆盖 position: fixed */ 365 | } 366 | } 367 | 368 | /* iPhone X 及以上设备的安全区域适配 */ 369 | @media screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3), 370 | screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2), 371 | screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3), 372 | screen and (device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3), 373 | screen and (device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) { 374 | .input-container { 375 | padding-bottom: calc(8px + 34px); /* 34px 是 iPhone X 系列的底部安全区域 */ 376 | } 377 | } 378 | 379 | /* iOS 虚拟键盘弹出时的样式调整 */ 380 | body.keyboard-open .app { 381 | height: 100vh; 382 | height: calc(var(--vh, 1vh) * 100); 383 | } 384 | 385 | body.keyboard-open .input-container { 386 | position: fixed; 387 | bottom: 0; 388 | left: 0; 389 | right: 0; 390 | width: 100%; 391 | max-width: 800px; 392 | margin: 0 auto; 393 | z-index: 10000; 394 | /* 确保在虚拟键盘上方 */ 395 | transform: translateY(0); 396 | } 397 | 398 | body.keyboard-open .message-list { 399 | /* 为固定输入框预留更多空间 */ 400 | padding-bottom: 80px; 401 | } 402 | 403 | /* 高分辨率屏幕 */ 404 | @media (min-width: 1200px) { 405 | .app { 406 | max-width: 900px; 407 | } 408 | 409 | .message { 410 | max-width: 60%; 411 | } 412 | 413 | .app-header h1 { 414 | font-size: 1.6rem; 415 | } 416 | 417 | .message-content { 418 | font-size: 1.05rem; 419 | padding: 0.5rem 0.5rem; 420 | } 421 | 422 | /* 高分辨率屏幕的文本消息优化 */ 423 | .message-content .text-message:not(.markdown-rendered) { 424 | padding: 0; 425 | margin: 0; 426 | font-weight: 400; 427 | letter-spacing: 0.01em; 428 | } 429 | 430 | .message-content .text-message.markdown-rendered { 431 | padding: 0.375rem 28px 0.375rem 0; 432 | font-weight: 400; 433 | letter-spacing: 0.01em; 434 | } 435 | } 436 | 437 | /* 打印样式 */ 438 | @media print { 439 | .app-header, 440 | .input-container, 441 | .app-footer { 442 | display: none; 443 | } 444 | 445 | .app { 446 | height: auto; 447 | box-shadow: none; 448 | } 449 | 450 | .message-list { 451 | padding: 0; 452 | } 453 | 454 | .message-content { 455 | border: 1px solid #ddd; 456 | background-color: white !important; 457 | color: black !important; 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /public/js/auth.js: -------------------------------------------------------------------------------- 1 | // 鉴权模块 - 处理登录验证和会话管理 2 | 3 | const Auth = { 4 | // 配置 5 | config: { 6 | TOKEN_KEY: "wxchat_auth_token", 7 | LOGIN_ATTEMPTS_KEY: "wxchat_login_attempts", 8 | MAX_ATTEMPTS: 5, 9 | ATTEMPT_RESET_TIME: 15 * 60 * 1000, // 15分钟 10 | TOKEN_REFRESH_INTERVAL: 30 * 60 * 1000, // 30分钟刷新token 11 | }, 12 | 13 | // 当前状态 14 | state: { 15 | isAuthenticated: false, 16 | token: null, 17 | refreshTimer: null, 18 | loginAttempts: 0, 19 | lastAttemptTime: 0, 20 | }, 21 | 22 | // 初始化鉴权模块 23 | init() { 24 | this.loadStoredData(); 25 | this.checkAuthentication(); 26 | this.startTokenRefresh(); 27 | }, 28 | 29 | // 初始化登录页面 30 | initLoginPage() { 31 | this.loadStoredData(); 32 | this.bindLoginEvents(); 33 | this.checkLoginAttempts(); 34 | 35 | // 如果已经登录,直接跳转 36 | if (this.isAuthenticated()) { 37 | this.redirectToApp(); 38 | } 39 | }, 40 | 41 | // 加载存储的数据 42 | loadStoredData() { 43 | try { 44 | this.state.token = localStorage.getItem(this.config.TOKEN_KEY); 45 | 46 | const attemptsData = localStorage.getItem(this.config.LOGIN_ATTEMPTS_KEY); 47 | if (attemptsData) { 48 | const data = JSON.parse(attemptsData); 49 | this.state.loginAttempts = data.count || 0; 50 | this.state.lastAttemptTime = data.lastTime || 0; 51 | } 52 | } catch (error) { 53 | console.error("加载存储数据失败:", error); 54 | } 55 | }, 56 | 57 | // 绑定登录页面事件 58 | bindLoginEvents() { 59 | const form = document.getElementById("loginForm"); 60 | const passwordInput = document.getElementById("passwordInput"); 61 | const passwordToggle = document.getElementById("passwordToggle"); 62 | const loginButton = document.getElementById("loginButton"); 63 | 64 | if (form) { 65 | form.addEventListener("submit", (e) => { 66 | e.preventDefault(); 67 | this.handleLogin(); 68 | }); 69 | } 70 | 71 | if (passwordInput) { 72 | passwordInput.addEventListener("keypress", (e) => { 73 | if (e.key === "Enter") { 74 | this.handleLogin(); 75 | } 76 | }); 77 | 78 | // 输入时清除错误消息 79 | passwordInput.addEventListener("input", () => { 80 | this.hideMessage("errorMessage"); 81 | this.hideMessage("warningMessage"); 82 | }); 83 | } 84 | 85 | if (passwordToggle) { 86 | passwordToggle.addEventListener("click", () => { 87 | this.togglePasswordVisibility(); 88 | }); 89 | } 90 | }, 91 | 92 | // 切换密码可见性 93 | togglePasswordVisibility() { 94 | const passwordInput = document.getElementById("passwordInput"); 95 | const passwordToggle = document.getElementById("passwordToggle"); 96 | 97 | if (passwordInput && passwordToggle) { 98 | if (passwordInput.type === "password") { 99 | passwordInput.type = "text"; 100 | passwordToggle.textContent = "🙈"; 101 | } else { 102 | passwordInput.type = "password"; 103 | passwordToggle.textContent = "👁️"; 104 | } 105 | } 106 | }, 107 | 108 | // 检查登录尝试次数 109 | checkLoginAttempts() { 110 | const now = Date.now(); 111 | 112 | // 如果超过重置时间,清除尝试次数 113 | if (now - this.state.lastAttemptTime > this.config.ATTEMPT_RESET_TIME) { 114 | this.state.loginAttempts = 0; 115 | this.saveLoginAttempts(); 116 | } 117 | 118 | // 如果达到最大尝试次数,显示警告 119 | if (this.state.loginAttempts >= this.config.MAX_ATTEMPTS) { 120 | const remainingTime = Math.ceil( 121 | (this.config.ATTEMPT_RESET_TIME - (now - this.state.lastAttemptTime)) / 122 | 60000 123 | ); 124 | this.showWarning(`登录尝试次数过多,请 ${remainingTime} 分钟后再试`); 125 | this.disableLogin(); 126 | } 127 | }, 128 | 129 | // 处理登录 130 | async handleLogin() { 131 | if (this.state.loginAttempts >= this.config.MAX_ATTEMPTS) { 132 | this.showError("登录尝试次数过多,请稍后再试"); 133 | return; 134 | } 135 | 136 | const passwordInput = document.getElementById("passwordInput"); 137 | const password = passwordInput?.value?.trim(); 138 | 139 | if (!password) { 140 | this.showError("请输入密码"); 141 | passwordInput?.focus(); 142 | return; 143 | } 144 | 145 | this.setLoading(true); 146 | 147 | try { 148 | const deviceId = Utils.getDeviceId(); 149 | const response = await fetch("/api/auth/login", { 150 | method: "POST", 151 | headers: { 152 | "Content-Type": "application/json", 153 | }, 154 | body: JSON.stringify({ password, deviceId }), 155 | }); 156 | 157 | const result = await response.json(); 158 | 159 | if (result.success) { 160 | // 登录成功 161 | this.state.token = result.token; 162 | this.state.isAuthenticated = true; 163 | localStorage.setItem(this.config.TOKEN_KEY, result.token); 164 | 165 | // 清除登录尝试记录 166 | this.state.loginAttempts = 0; 167 | this.saveLoginAttempts(); 168 | 169 | // 跳转到应用 170 | this.redirectToApp(); 171 | } else { 172 | // 登录失败 173 | this.handleLoginFailure(result.message || "密码错误"); 174 | } 175 | } catch (error) { 176 | console.error("登录请求失败:", error); 177 | this.showError("网络连接失败,请检查网络后重试"); 178 | } finally { 179 | this.setLoading(false); 180 | } 181 | }, 182 | 183 | // 处理登录失败 184 | handleLoginFailure(message) { 185 | this.state.loginAttempts++; 186 | this.state.lastAttemptTime = Date.now(); 187 | this.saveLoginAttempts(); 188 | 189 | const remainingAttempts = 190 | this.config.MAX_ATTEMPTS - this.state.loginAttempts; 191 | 192 | if (remainingAttempts > 0) { 193 | this.showError(`${message},还可尝试 ${remainingAttempts} 次`); 194 | } else { 195 | this.showError("登录尝试次数过多,请15分钟后再试"); 196 | this.disableLogin(); 197 | } 198 | 199 | // 清空密码输入框 200 | const passwordInput = document.getElementById("passwordInput"); 201 | if (passwordInput) { 202 | passwordInput.value = ""; 203 | passwordInput.focus(); 204 | } 205 | }, 206 | 207 | // 保存登录尝试记录 208 | saveLoginAttempts() { 209 | try { 210 | localStorage.setItem( 211 | this.config.LOGIN_ATTEMPTS_KEY, 212 | JSON.stringify({ 213 | count: this.state.loginAttempts, 214 | lastTime: this.state.lastAttemptTime, 215 | }) 216 | ); 217 | } catch (error) { 218 | console.error("保存登录尝试记录失败:", error); 219 | } 220 | }, 221 | 222 | // 设置加载状态 223 | setLoading(loading) { 224 | const loginButton = document.getElementById("loginButton"); 225 | const passwordInput = document.getElementById("passwordInput"); 226 | 227 | if (loginButton) { 228 | loginButton.disabled = loading; 229 | if (loading) { 230 | loginButton.classList.add("loading"); 231 | } else { 232 | loginButton.classList.remove("loading"); 233 | } 234 | } 235 | 236 | if (passwordInput) { 237 | passwordInput.disabled = loading; 238 | } 239 | }, 240 | 241 | // 禁用登录 242 | disableLogin() { 243 | const loginButton = document.getElementById("loginButton"); 244 | const passwordInput = document.getElementById("passwordInput"); 245 | 246 | if (loginButton) { 247 | loginButton.disabled = true; 248 | } 249 | if (passwordInput) { 250 | passwordInput.disabled = true; 251 | } 252 | }, 253 | 254 | // 显示错误消息 255 | showError(message) { 256 | this.showMessage("errorMessage", message); 257 | this.hideMessage("warningMessage"); 258 | }, 259 | 260 | // 显示警告消息 261 | showWarning(message) { 262 | this.showMessage("warningMessage", message); 263 | this.hideMessage("errorMessage"); 264 | }, 265 | 266 | // 显示消息 267 | showMessage(elementId, message) { 268 | const element = document.getElementById(elementId); 269 | if (element) { 270 | element.textContent = message; 271 | element.style.display = "block"; 272 | } 273 | }, 274 | 275 | // 隐藏消息 276 | hideMessage(elementId) { 277 | const element = document.getElementById(elementId); 278 | if (element) { 279 | element.style.display = "none"; 280 | } 281 | }, 282 | 283 | // 跳转到应用 284 | redirectToApp() { 285 | window.location.href = "/"; 286 | }, 287 | 288 | // 检查认证状态 289 | async checkAuthentication() { 290 | if (!this.state.token) { 291 | this.state.isAuthenticated = false; 292 | return false; 293 | } 294 | 295 | try { 296 | const response = await fetch("/api/auth/verify", { 297 | headers: { 298 | Authorization: `Bearer ${this.state.token}`, 299 | }, 300 | }); 301 | 302 | if (response.ok) { 303 | const result = await response.json(); 304 | this.state.isAuthenticated = result.valid; 305 | return result.valid; 306 | } else { 307 | this.logout(); 308 | return false; 309 | } 310 | } catch (error) { 311 | console.error("验证token失败:", error); 312 | this.logout(); 313 | return false; 314 | } 315 | }, 316 | 317 | // 检查是否已认证 318 | isAuthenticated() { 319 | return this.state.isAuthenticated && this.state.token; 320 | }, 321 | 322 | // 获取认证token 323 | getToken() { 324 | return this.state.token; 325 | }, 326 | 327 | // 开始token刷新 328 | startTokenRefresh() { 329 | if (this.state.refreshTimer) { 330 | clearInterval(this.state.refreshTimer); 331 | } 332 | 333 | this.state.refreshTimer = setInterval(() => { 334 | if (this.isAuthenticated()) { 335 | this.checkAuthentication(); 336 | } 337 | }, this.config.TOKEN_REFRESH_INTERVAL); 338 | }, 339 | 340 | // 登出 341 | logout() { 342 | this.state.isAuthenticated = false; 343 | this.state.token = null; 344 | localStorage.removeItem(this.config.TOKEN_KEY); 345 | 346 | if (this.state.refreshTimer) { 347 | clearInterval(this.state.refreshTimer); 348 | this.state.refreshTimer = null; 349 | } 350 | 351 | // 如果不在登录页面,跳转到登录页面 352 | if (!window.location.pathname.includes("login.html")) { 353 | window.location.href = "/login.html"; 354 | } 355 | }, 356 | 357 | // 为API请求添加认证头 358 | addAuthHeader(headers = {}) { 359 | if (this.state.token) { 360 | headers["Authorization"] = `Bearer ${this.state.token}`; 361 | } 362 | return headers; 363 | }, 364 | }; 365 | -------------------------------------------------------------------------------- /public/js/realtime.js: -------------------------------------------------------------------------------- 1 | // 实时通信管理器 2 | class RealtimeManager { 3 | constructor() { 4 | this.eventSource = null; 5 | this.isConnected = false; 6 | this.reconnectAttempts = 0; 7 | this.maxReconnectAttempts = 3; // 减少SSE重试次数 8 | this.reconnectDelay = 1000; // 1秒 9 | this.deviceId = null; 10 | this.listeners = new Map(); 11 | this.longPollingActive = false; 12 | this.longPollingTimeout = null; 13 | } 14 | 15 | // 初始化实时连接 16 | init(deviceId) { 17 | this.deviceId = deviceId; 18 | this.connect(); 19 | } 20 | 21 | // 建立SSE连接 22 | connect() { 23 | if (this.eventSource) { 24 | this.disconnect(); 25 | } 26 | 27 | // 设置连接中状态 28 | UI.setConnectionStatus("connecting"); 29 | 30 | try { 31 | // 获取认证token 32 | const token = Auth && Auth.getToken() ? Auth.getToken() : ""; 33 | const url = `/api/events?deviceId=${encodeURIComponent( 34 | this.deviceId 35 | )}&token=${encodeURIComponent(token)}`; 36 | this.eventSource = new EventSource(url); 37 | 38 | // 监听EventSource状态变化 39 | const checkConnection = () => { 40 | if (this.eventSource) { 41 | switch (this.eventSource.readyState) { 42 | case EventSource.CONNECTING: 43 | console.log("SSE状态: 连接中"); 44 | break; 45 | case EventSource.OPEN: 46 | console.log("SSE状态: 已连接"); 47 | this.isConnected = true; 48 | this.reconnectAttempts = 0; 49 | this.emit("connected"); 50 | UI.setConnectionStatus("connected"); 51 | // 清除状态检查 52 | if (this.connectionCheckInterval) { 53 | clearInterval(this.connectionCheckInterval); 54 | this.connectionCheckInterval = null; 55 | } 56 | break; 57 | case EventSource.CLOSED: 58 | console.log("SSE状态: 已关闭"); 59 | this.isConnected = false; 60 | this.emit("disconnected"); 61 | UI.setConnectionStatus("disconnected"); 62 | this.handleReconnect(); 63 | break; 64 | } 65 | } 66 | }; 67 | 68 | // 定期检查连接状态 69 | this.connectionCheckInterval = setInterval(checkConnection, 1000); 70 | 71 | // 连接成功 72 | this.eventSource.addEventListener("connection", (event) => { 73 | this.isConnected = true; 74 | this.reconnectAttempts = 0; 75 | this.emit("connected"); 76 | UI.setConnectionStatus("connected"); 77 | }); 78 | 79 | // 接收消息 80 | this.eventSource.addEventListener("message", (event) => { 81 | try { 82 | const data = JSON.parse(event.data); 83 | 84 | if (data.newMessages > 0) { 85 | // 有新消息,触发刷新 86 | this.emit("newMessages", data); 87 | // 立即加载消息,强制滚动到底部(有新消息时) 88 | MessageHandler.loadMessages(true, false); 89 | } 90 | } catch (error) { 91 | // 静默处理解析错误 92 | } 93 | }); 94 | 95 | // 接收清空所有消息的通知 96 | this.eventSource.addEventListener("clearAll", (event) => { 97 | try { 98 | const data = JSON.parse(event.data); 99 | if (data.action === "clearAll") { 100 | // 清空所有消息,实时更新UI 101 | this.emit("clearAll", data); 102 | MessageHandler.clearAllMessages(); 103 | } 104 | } catch (error) { 105 | console.error("解析清空消息失败:", error); 106 | } 107 | }); 108 | 109 | // 接收删除单条消息的通知 110 | this.eventSource.addEventListener("messageDeleted", (event) => { 111 | try { 112 | const data = JSON.parse(event.data); 113 | if (data.messageId) { 114 | // 删除单条消息,实时更新UI 115 | this.emit("messageDeleted", data); 116 | } 117 | } catch (error) { 118 | console.error("解析删除消息失败:", error); 119 | } 120 | }); 121 | 122 | // 心跳检测 123 | this.eventSource.addEventListener("heartbeat", (event) => { 124 | this.emit("heartbeat"); 125 | }); 126 | 127 | // 连接错误 128 | this.eventSource.onerror = (event) => { 129 | this.isConnected = false; 130 | this.emit("disconnected"); 131 | UI.setConnectionStatus("disconnected"); 132 | 133 | // 自动重连 134 | this.handleReconnect(); 135 | }; 136 | } catch (error) { 137 | this.handleReconnect(); 138 | } 139 | } 140 | 141 | // 断开连接 142 | disconnect() { 143 | if (this.eventSource) { 144 | this.eventSource.close(); 145 | this.eventSource = null; 146 | } 147 | 148 | // 清除连接状态检查 149 | if (this.connectionCheckInterval) { 150 | clearInterval(this.connectionCheckInterval); 151 | this.connectionCheckInterval = null; 152 | } 153 | 154 | this.stopLongPolling(); 155 | this.isConnected = false; 156 | this.emit("disconnected"); 157 | } 158 | 159 | // 处理重连逻辑 160 | handleReconnect() { 161 | if (this.reconnectAttempts >= this.maxReconnectAttempts) { 162 | this.fallbackToLongPolling(); 163 | return; 164 | } 165 | 166 | this.reconnectAttempts++; 167 | const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); // 指数退避 168 | 169 | UI.setConnectionStatus("reconnecting"); 170 | 171 | // PWA环境下的特殊处理 172 | const isPWA = 173 | window.matchMedia("(display-mode: standalone)").matches || 174 | window.navigator.standalone === true; 175 | 176 | // PWA环境下使用更短的重连延迟 177 | const pwaDelay = isPWA ? Math.min(delay, 2000) : delay; 178 | 179 | setTimeout(() => { 180 | if (!this.isConnected) { 181 | this.connect(); 182 | } 183 | }, pwaDelay); 184 | } 185 | 186 | // 降级到长轮询 187 | fallbackToLongPolling() { 188 | this.disconnect(); 189 | this.startLongPolling(); 190 | } 191 | 192 | // 开始长轮询 193 | startLongPolling() { 194 | if (this.longPollingActive) { 195 | return; 196 | } 197 | 198 | this.longPollingActive = true; 199 | this.longPoll(); 200 | } 201 | 202 | // 停止长轮询 203 | stopLongPolling() { 204 | this.longPollingActive = false; 205 | if (this.longPollingTimeout) { 206 | clearTimeout(this.longPollingTimeout); 207 | this.longPollingTimeout = null; 208 | } 209 | } 210 | 211 | // 长轮询实现 212 | async longPoll() { 213 | if (!this.longPollingActive) { 214 | return; 215 | } 216 | 217 | try { 218 | const lastMessageId = this.getLastMessageId(); 219 | const url = `/api/poll?deviceId=${encodeURIComponent( 220 | this.deviceId 221 | )}&lastMessageId=${lastMessageId}&timeout=30`; 222 | 223 | // 添加认证头 224 | const headers = Auth ? Auth.addAuthHeader({}) : {}; 225 | const response = await fetch(url, { headers }); 226 | const data = await response.json(); 227 | 228 | if (data.success && data.hasNewMessages) { 229 | this.emit("newMessages", { newMessages: data.newMessageCount }); 230 | // 立即加载消息,强制滚动到底部(有新消息时) 231 | MessageHandler.loadMessages(true, false); 232 | } 233 | 234 | // 设置连接状态 235 | if (!this.isConnected) { 236 | this.isConnected = true; 237 | this.emit("connected"); 238 | UI.setConnectionStatus("connected"); 239 | } 240 | } catch (error) { 241 | this.isConnected = false; 242 | this.emit("disconnected"); 243 | UI.setConnectionStatus("disconnected"); 244 | } 245 | 246 | // 继续下一次轮询 247 | if (this.longPollingActive) { 248 | this.longPollingTimeout = setTimeout(() => { 249 | this.longPoll(); 250 | }, 1000); // 1秒后继续 251 | } 252 | } 253 | 254 | // 获取最后一条消息ID 255 | getLastMessageId() { 256 | const messages = MessageHandler.lastMessages || []; 257 | if (messages.length > 0) { 258 | return messages[messages.length - 1].id || "0"; 259 | } 260 | return "0"; 261 | } 262 | 263 | // 检查连接状态 264 | isConnectionAlive() { 265 | // SSE连接活跃 266 | if (this.eventSource && this.eventSource.readyState === EventSource.OPEN) { 267 | return true; 268 | } 269 | // 长轮询活跃 270 | if (this.longPollingActive && this.isConnected) { 271 | return true; 272 | } 273 | return false; 274 | } 275 | 276 | // 事件监听器 277 | on(event, callback) { 278 | if (!this.listeners.has(event)) { 279 | this.listeners.set(event, []); 280 | } 281 | this.listeners.get(event).push(callback); 282 | } 283 | 284 | // 移除事件监听器 285 | off(event, callback) { 286 | if (this.listeners.has(event)) { 287 | const callbacks = this.listeners.get(event); 288 | const index = callbacks.indexOf(callback); 289 | if (index > -1) { 290 | callbacks.splice(index, 1); 291 | } 292 | } 293 | } 294 | 295 | // 触发事件 296 | emit(event, data = null) { 297 | if (this.listeners.has(event)) { 298 | this.listeners.get(event).forEach((callback) => { 299 | try { 300 | callback(data); 301 | } catch (error) { 302 | console.error(`事件回调执行失败 [${event}]:`, error); 303 | } 304 | }); 305 | } 306 | } 307 | 308 | // 手动触发消息检查 309 | checkMessages() { 310 | if (this.isConnectionAlive()) { 311 | // SSE连接正常,等待服务器推送 312 | return; 313 | } 314 | 315 | // SSE连接异常,降级到轮询 316 | MessageHandler.loadMessages(); 317 | } 318 | 319 | // 获取连接状态 320 | getStatus() { 321 | if (!this.eventSource) return "disconnected"; 322 | 323 | switch (this.eventSource.readyState) { 324 | case EventSource.CONNECTING: 325 | return "connecting"; 326 | case EventSource.OPEN: 327 | return "connected"; 328 | case EventSource.CLOSED: 329 | return "disconnected"; 330 | default: 331 | return "unknown"; 332 | } 333 | } 334 | 335 | // 销毁管理器 336 | destroy() { 337 | this.disconnect(); 338 | this.stopLongPolling(); 339 | this.listeners.clear(); 340 | this.deviceId = null; 341 | this.reconnectAttempts = 0; 342 | this.longPollingActive = false; 343 | } 344 | } 345 | 346 | // 创建全局实例 347 | const Realtime = new RealtimeManager(); 348 | 349 | // 网络状态监听 350 | window.addEventListener("online", () => { 351 | // PWA环境下的特殊处理 352 | const isPWA = 353 | window.matchMedia("(display-mode: standalone)").matches || 354 | window.navigator.standalone === true; 355 | 356 | if (isPWA) { 357 | // PWA环境:延迟重连,确保网络完全恢复 358 | setTimeout(() => { 359 | if (!Realtime.isConnectionAlive()) { 360 | Realtime.connect(); 361 | } 362 | }, 1000); 363 | } else { 364 | // 普通浏览器环境:立即重连 365 | if (!Realtime.isConnectionAlive()) { 366 | Realtime.connect(); 367 | } 368 | } 369 | }); 370 | 371 | window.addEventListener("offline", () => { 372 | UI.setConnectionStatus("offline"); 373 | }); 374 | 375 | // 页面可见性变化监听 376 | document.addEventListener("visibilitychange", () => { 377 | if (document.visibilityState === "visible") { 378 | // 页面变为可见时,检查连接状态 379 | if (!Realtime.isConnectionAlive()) { 380 | Realtime.connect(); 381 | } 382 | } 383 | }); 384 | 385 | // 监听删除消息事件 386 | Realtime.on("messageDeleted", (data) => { 387 | if (data.messageId) { 388 | // 删除单条消息,实时更新UI 389 | UI.removeMessageFromUI(data.messageId); 390 | } 391 | }); 392 | 393 | // 导出到全局 394 | window.Realtime = Realtime; 395 | -------------------------------------------------------------------------------- /public/js/pwa.js: -------------------------------------------------------------------------------- 1 | // PWA功能管理模块 2 | // 处理Service Worker注册、安装提示、离线检测等 3 | 4 | class PWAManager { 5 | constructor() { 6 | this.deferredPrompt = null; 7 | this.isInstalled = false; 8 | this.isOnline = navigator.onLine; 9 | this.swRegistration = null; 10 | 11 | this.init(); 12 | } 13 | 14 | // 初始化PWA功能 15 | async init() { 16 | try { 17 | // 检查PWA支持 18 | await this.checkPWASupport(); 19 | 20 | // 注册Service Worker 21 | await this.registerServiceWorker(); 22 | 23 | // 设置事件监听器 24 | this.setupEventListeners(); 25 | 26 | // 检查安装状态 27 | this.checkInstallStatus(); 28 | 29 | // 显示安装提示 30 | this.setupInstallPrompt(); 31 | 32 | // console.log("✅ PWA功能初始化完成"); 33 | } catch (error) { 34 | console.error("❌ PWA初始化失败:", error); 35 | } 36 | } 37 | 38 | // 检查PWA支持 39 | async checkPWASupport() { 40 | // 检查Manifest是否可访问 41 | let manifestSupported = false; 42 | try { 43 | const response = await fetch("/manifest.json"); 44 | manifestSupported = response.ok; 45 | } catch (error) { 46 | manifestSupported = false; 47 | } 48 | 49 | const features = { 50 | serviceWorker: "serviceWorker" in navigator, 51 | manifest: manifestSupported, 52 | notification: "Notification" in window, 53 | pushManager: "PushManager" in window, 54 | }; 55 | 56 | return features; 57 | } 58 | 59 | // 注册Service Worker 60 | async registerServiceWorker() { 61 | if (!("serviceWorker" in navigator)) { 62 | console.warn("⚠️ 浏览器不支持Service Worker"); 63 | return; 64 | } 65 | 66 | try { 67 | this.swRegistration = await navigator.serviceWorker.register("/sw.js", { 68 | scope: "/", 69 | }); 70 | 71 | // console.log("✅ Service Worker注册成功:", this.swRegistration.scope); 72 | 73 | // 监听Service Worker更新 74 | this.swRegistration.addEventListener("updatefound", () => { 75 | this.handleServiceWorkerUpdate(); 76 | }); 77 | } catch (error) { 78 | console.error("❌ Service Worker注册失败:", error); 79 | } 80 | } 81 | 82 | // 处理Service Worker更新 83 | handleServiceWorkerUpdate() { 84 | const newWorker = this.swRegistration.installing; 85 | 86 | newWorker.addEventListener("statechange", () => { 87 | if ( 88 | newWorker.state === "installed" && 89 | navigator.serviceWorker.controller 90 | ) { 91 | // 有新版本可用 92 | this.showUpdateAvailable(); 93 | } 94 | }); 95 | } 96 | 97 | // 显示更新可用提示 98 | showUpdateAvailable() { 99 | const updateBanner = this.createUpdateBanner(); 100 | document.body.appendChild(updateBanner); 101 | 102 | // 3秒后自动隐藏 103 | setTimeout(() => { 104 | updateBanner.remove(); 105 | }, 5000); 106 | } 107 | 108 | // 创建更新横幅 109 | createUpdateBanner() { 110 | const banner = document.createElement("div"); 111 | banner.className = "pwa-update-banner"; 112 | banner.innerHTML = ` 113 |
114 | 🚀 新版本可用 115 | 116 | 117 |
118 | `; 119 | return banner; 120 | } 121 | 122 | // 更新应用 123 | updateApp() { 124 | if (this.swRegistration && this.swRegistration.waiting) { 125 | this.swRegistration.waiting.postMessage({ type: "SKIP_WAITING" }); 126 | window.location.reload(); 127 | } 128 | } 129 | 130 | // 设置事件监听器 131 | setupEventListeners() { 132 | // 监听安装提示事件 133 | window.addEventListener("beforeinstallprompt", (e) => { 134 | // 保存事件,但不阻止默认行为(让浏览器显示原生提示) 135 | this.deferredPrompt = e; 136 | 137 | // 不显示自定义安装按钮,只保存事件供/pwa命令使用 138 | }); 139 | 140 | // 监听应用安装事件 141 | window.addEventListener("appinstalled", () => { 142 | console.log("🎉 应用已安装"); 143 | this.isInstalled = true; 144 | this.hideInstallButton(); 145 | // 安装成功通知已禁用,避免移动端弹窗遮挡输入框 146 | // Utils.showNotification('应用已成功安装到桌面!', 'success'); 147 | }); 148 | 149 | // 监听网络状态变化 150 | window.addEventListener("online", () => { 151 | this.isOnline = true; 152 | this.handleOnlineStatusChange(); 153 | }); 154 | 155 | window.addEventListener("offline", () => { 156 | this.isOnline = false; 157 | this.handleOnlineStatusChange(); 158 | }); 159 | } 160 | 161 | // 处理网络状态变化 162 | handleOnlineStatusChange() { 163 | const statusElement = document.querySelector(".connection-status"); 164 | if (statusElement) { 165 | if (this.isOnline) { 166 | statusElement.textContent = "已连接"; 167 | statusElement.className = "connection-status online"; 168 | setTimeout(() => { 169 | // 3秒后,如果状态仍然是online,则清空文本和样式 170 | if (statusElement.classList.contains("online")) { 171 | statusElement.textContent = ""; 172 | statusElement.classList.remove("online"); 173 | } 174 | }, 3000); 175 | } else { 176 | statusElement.textContent = "离线模式"; 177 | statusElement.className = "connection-status offline"; 178 | } 179 | } 180 | 181 | // 网络状态通知已禁用,避免移动端弹窗遮挡输入框 182 | // if (this.isOnline) { 183 | // Utils.showNotification('网络已连接', 'success'); 184 | // } 185 | // 离线状态的通知由UI.setConnectionStatus处理,避免重复 186 | } 187 | 188 | // 检查安装状态 189 | checkInstallStatus() { 190 | // 检查是否在独立模式下运行(已安装) 191 | this.isInstalled = 192 | window.matchMedia("(display-mode: standalone)").matches || 193 | window.navigator.standalone === true; 194 | 195 | if (this.isInstalled) { 196 | // 应用运行在独立模式(已安装) 197 | } 198 | } 199 | 200 | // 设置安装提示 201 | setupInstallPrompt() { 202 | // 不自动显示任何安装提示,只通过/pwa命令手动触发 203 | return; 204 | } 205 | 206 | // 显示安装按钮(已禁用) 207 | showInstallButton() { 208 | // 不显示悬浮安装按钮 209 | return; 210 | } 211 | 212 | // 隐藏安装按钮 213 | hideInstallButton() { 214 | const installBtn = document.getElementById("pwa-install-btn"); 215 | if (installBtn) { 216 | installBtn.style.display = "none"; 217 | } 218 | } 219 | 220 | // 显示安装横幅 221 | showInstallBanner() { 222 | const banner = document.createElement("div"); 223 | banner.className = "pwa-install-banner"; 224 | banner.innerHTML = ` 225 |
226 |
📱
227 |
228 |

安装微信文件传输助手

229 |

获得更好的使用体验,支持离线访问

230 |
231 |
232 | 233 | 234 |
235 |
236 | `; 237 | 238 | document.body.appendChild(banner); 239 | 240 | // 添加动画效果 241 | setTimeout(() => { 242 | banner.classList.add("show"); 243 | }, 100); 244 | } 245 | 246 | // 提示安装 247 | async promptInstall() { 248 | if (!this.deferredPrompt) { 249 | // 安装提示通知已禁用,避免移动端弹窗遮挡输入框 250 | return; 251 | } 252 | 253 | try { 254 | // 显示安装提示 255 | this.deferredPrompt.prompt(); 256 | 257 | // 等待用户响应 258 | const { outcome } = await this.deferredPrompt.userChoice; 259 | 260 | if (outcome === "accepted") { 261 | // 用户接受安装 262 | } else { 263 | // 用户拒绝安装 264 | } 265 | 266 | // 清除提示 267 | this.deferredPrompt = null; 268 | this.hideInstallButton(); 269 | this.dismissInstallBanner(); 270 | } catch (error) { 271 | console.error("安装提示失败:", error); 272 | // 安装失败通知已禁用,避免移动端弹窗遮挡输入框 273 | console.error("安装失败,请重试"); 274 | } 275 | } 276 | 277 | // 关闭安装横幅 278 | dismissInstallBanner() { 279 | const banner = document.querySelector(".pwa-install-banner"); 280 | if (banner) { 281 | banner.classList.add("hide"); 282 | setTimeout(() => { 283 | banner.remove(); 284 | }, 300); 285 | } 286 | 287 | // 记录用户拒绝安装,24小时内不再显示 288 | localStorage.setItem("pwa-install-dismissed", Date.now().toString()); 289 | } 290 | 291 | // 获取缓存信息 292 | async getCacheInfo() { 293 | if (!("caches" in window)) { 294 | return { supported: false }; 295 | } 296 | 297 | try { 298 | const cacheNames = await caches.keys(); 299 | const cacheInfo = {}; 300 | 301 | for (const cacheName of cacheNames) { 302 | const cache = await caches.open(cacheName); 303 | const keys = await cache.keys(); 304 | cacheInfo[cacheName] = keys.length; 305 | } 306 | 307 | return { 308 | supported: true, 309 | caches: cacheInfo, 310 | total: cacheNames.length, 311 | }; 312 | } catch (error) { 313 | console.error("获取缓存信息失败:", error); 314 | return { supported: true, error: error.message }; 315 | } 316 | } 317 | 318 | // 清理缓存 319 | async clearCache() { 320 | if (!("caches" in window)) { 321 | // 缓存API不支持通知已禁用,避免移动端弹窗遮挡输入框 322 | console.error("浏览器不支持缓存API"); 323 | return; 324 | } 325 | 326 | try { 327 | const cacheNames = await caches.keys(); 328 | await Promise.all(cacheNames.map((name) => caches.delete(name))); 329 | 330 | // 缓存清理成功通知已禁用,避免移动端弹窗遮挡输入框 331 | } catch (error) { 332 | console.error("清理缓存失败:", error); 333 | // 缓存清理失败通知已禁用,避免移动端弹窗遮挡输入框 334 | console.error("清理缓存失败"); 335 | } 336 | } 337 | 338 | // 获取PWA状态 339 | async getStatus() { 340 | const manifestCheck = await this.checkManifestStatus(); 341 | 342 | return { 343 | installed: this.isInstalled, 344 | online: this.isOnline, 345 | serviceWorkerSupported: "serviceWorker" in navigator, 346 | serviceWorkerRegistered: !!this.swRegistration, 347 | installPromptAvailable: !!this.deferredPrompt, 348 | manifestAccessible: manifestCheck.accessible, 349 | manifestValid: manifestCheck.valid, 350 | cacheCount: await this.getCacheCount(), 351 | }; 352 | } 353 | 354 | // 检查Manifest状态 355 | async checkManifestStatus() { 356 | try { 357 | const response = await fetch("/manifest.json"); 358 | if (!response.ok) { 359 | return { 360 | accessible: false, 361 | valid: false, 362 | error: `HTTP ${response.status}`, 363 | }; 364 | } 365 | 366 | const manifest = await response.json(); 367 | const hasRequiredFields = 368 | manifest.name && manifest.start_url && manifest.icons; 369 | 370 | return { 371 | accessible: true, 372 | valid: hasRequiredFields, 373 | data: manifest, 374 | }; 375 | } catch (error) { 376 | return { accessible: false, valid: false, error: error.message }; 377 | } 378 | } 379 | 380 | // 获取缓存数量 381 | async getCacheCount() { 382 | if (!("caches" in window)) return 0; 383 | 384 | try { 385 | const cacheNames = await caches.keys(); 386 | return cacheNames.length; 387 | } catch (error) { 388 | return 0; 389 | } 390 | } 391 | } 392 | 393 | // 创建全局PWA实例 394 | const PWA = new PWAManager(); 395 | 396 | // 导出到全局作用域 397 | window.PWA = PWA; 398 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | // 应用主入口文件 2 | 3 | class WeChatApp { 4 | constructor() { 5 | this.isInitialized = false; 6 | this.deviceId = null; 7 | } 8 | 9 | // 初始化应用 10 | async init() { 11 | try { 12 | // 首先加载服务器配置 13 | await ConfigManager.loadConfig(); 14 | 15 | // 初始化鉴权模块 16 | Auth.init(); 17 | 18 | // 检查认证状态 19 | const isAuthenticated = await Auth.checkAuthentication(); 20 | if (!isAuthenticated) { 21 | // 未认证,跳转到登录页面 22 | window.location.href = "/login.html"; 23 | return; 24 | } 25 | 26 | // iOS Safari 视口修复 27 | this.initIOSViewportFix(); 28 | 29 | // 检查浏览器兼容性 30 | this.checkBrowserCompatibility(); 31 | 32 | // 初始化设备ID 33 | this.deviceId = Utils.getDeviceId(); 34 | 35 | // 请求通知权限 - 已禁用,避免移动端弹窗遮挡输入框 36 | // await Utils.requestNotificationPermission(); 37 | 38 | // 初始化各个模块 39 | UI.init(); 40 | FileUpload.init(); 41 | 42 | // 初始化功能组件 - 确保在UI初始化之后 43 | if (typeof FunctionMenu !== "undefined") { 44 | FunctionMenu.init(); 45 | // 将组件暴露到全局 46 | window.FunctionMenu = FunctionMenu; 47 | } 48 | 49 | if (typeof FunctionButton !== "undefined") { 50 | FunctionButton.init(); 51 | // 将组件暴露到全局,供UI模块使用 52 | window.FunctionButton = FunctionButton; 53 | } 54 | 55 | // 初始化PWA功能 56 | if (typeof PWA !== "undefined") { 57 | PWA.init(); 58 | } 59 | 60 | // 初始化AI模块 61 | if (typeof AIUI !== "undefined") { 62 | AIUI.init(); 63 | window.AIUI = AIUI; 64 | } 65 | 66 | if (typeof AIHandler !== "undefined") { 67 | const aiInitSuccess = AIHandler.init(); 68 | if (aiInitSuccess) { 69 | window.AIHandler = AIHandler; 70 | } 71 | } 72 | 73 | // 初始化AI图片生成模块 74 | if (typeof ImageGenUI !== "undefined") { 75 | ImageGenUI.init(); 76 | window.ImageGenUI = ImageGenUI; 77 | } 78 | 79 | if (typeof ImageGenHandler !== "undefined") { 80 | ImageGenHandler.init(); 81 | window.ImageGenHandler = ImageGenHandler; 82 | } 83 | 84 | // 初始化搜索模块 85 | if (typeof SearchUI !== "undefined") { 86 | SearchUI.init(); 87 | window.SearchUI = SearchUI; 88 | } 89 | 90 | if (typeof SearchHandler !== "undefined") { 91 | SearchHandler.init(); 92 | window.SearchHandler = SearchHandler; 93 | } 94 | 95 | // 设置初始连接状态 96 | UI.setConnectionStatus(navigator.onLine ? "connected" : "disconnected"); 97 | 98 | MessageHandler.init(); 99 | 100 | // 绑定功能菜单事件 101 | this.bindFunctionMenuEvents(); 102 | 103 | // 标记为已初始化 104 | this.isInitialized = true; 105 | 106 | // 显示欢迎消息 107 | this.showWelcomeMessage(); 108 | } catch (error) { 109 | this.showInitError(error); 110 | } 111 | } 112 | 113 | // iOS Safari 视口修复 114 | initIOSViewportFix() { 115 | // 检测是否为iOS设备 116 | const isIOS = 117 | /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 118 | 119 | if (isIOS) { 120 | // 设置CSS自定义属性来修复100vh问题 121 | const setVH = () => { 122 | const vh = window.innerHeight * 0.01; 123 | document.documentElement.style.setProperty("--vh", `${vh}px`); 124 | }; 125 | 126 | // 初始设置 127 | setVH(); 128 | 129 | // 监听窗口大小变化(包括虚拟键盘弹出/收起) 130 | window.addEventListener("resize", Utils.debounce(setVH, 100)); 131 | window.addEventListener("orientationchange", () => { 132 | setTimeout(setVH, 500); // 延迟执行,等待方向改变完成 133 | }); 134 | 135 | // 监听虚拟键盘事件 136 | this.handleIOSKeyboard(); 137 | } 138 | } 139 | 140 | // 处理iOS虚拟键盘 141 | handleIOSKeyboard() { 142 | let initialViewportHeight = window.innerHeight; 143 | 144 | const handleViewportChange = () => { 145 | const currentHeight = window.innerHeight; 146 | const heightDifference = initialViewportHeight - currentHeight; 147 | 148 | // 如果高度减少超过150px,认为是虚拟键盘弹出 149 | if (heightDifference > 150) { 150 | document.body.classList.add("keyboard-open"); 151 | // 确保输入框可见 152 | setTimeout(() => { 153 | const inputContainer = document.querySelector(".input-container"); 154 | if (inputContainer) { 155 | inputContainer.scrollIntoView({ behavior: "smooth", block: "end" }); 156 | } 157 | }, 300); 158 | } else { 159 | document.body.classList.remove("keyboard-open"); 160 | } 161 | }; 162 | 163 | window.addEventListener( 164 | "resize", 165 | Utils.debounce(handleViewportChange, 100) 166 | ); 167 | } 168 | 169 | // 检查浏览器兼容性 170 | checkBrowserCompatibility() { 171 | const requiredFeatures = [ 172 | "fetch", 173 | "localStorage", 174 | "FormData", 175 | "FileReader", 176 | ]; 177 | 178 | const missingFeatures = requiredFeatures.filter((feature) => { 179 | return !(feature in window); 180 | }); 181 | 182 | if (missingFeatures.length > 0) { 183 | throw new Error(`浏览器不支持以下功能: ${missingFeatures.join(", ")}`); 184 | } 185 | 186 | // 检查ES6支持 187 | try { 188 | eval("const test = () => {};"); 189 | } catch (e) { 190 | throw new Error("浏览器不支持ES6语法,请使用现代浏览器"); 191 | } 192 | 193 | // 浏览器兼容性检查通过 194 | } 195 | 196 | // 显示欢迎消息 - 已禁用,避免移动端弹窗遮挡输入框 197 | showWelcomeMessage() { 198 | const isFirstTime = !localStorage.getItem("hasVisited"); 199 | 200 | if (isFirstTime) { 201 | localStorage.setItem("hasVisited", "true"); 202 | 203 | // 欢迎通知已禁用,避免遮挡输入框 204 | // setTimeout(() => { 205 | // Utils.showNotification('欢迎使用微信文件传输助手!', 'info'); 206 | // }, 1000); 207 | } 208 | } 209 | 210 | // 绑定功能菜单事件 211 | bindFunctionMenuEvents() { 212 | // 监听功能菜单项点击事件 213 | document.addEventListener("functionMenu:itemClick", (e) => { 214 | const { action, itemId } = e.detail; 215 | this.handleFunctionMenuAction(action, itemId); 216 | }); 217 | 218 | // 监听清空聊天事件 219 | document.addEventListener("functionMenu:clearChat", async () => { 220 | try { 221 | await MessageHandler.clearAllMessages(); 222 | UI.showSuccess("聊天记录已清空"); 223 | } catch (error) { 224 | UI.showError("清空聊天记录失败"); 225 | console.error("清空聊天记录失败:", error); 226 | } 227 | }); 228 | } 229 | 230 | // 处理功能菜单动作 231 | handleFunctionMenuAction(action, itemId) { 232 | // 这里可以根据需要添加更多的功能处理逻辑 233 | switch (action) { 234 | case "quickReply": 235 | // 快速回复功能已在 FunctionMenu 组件中处理 236 | break; 237 | case "emoji": 238 | // 表情功能已在 FunctionMenu 组件中处理 239 | break; 240 | case "markdown": 241 | // Markdown 功能已在 FunctionMenu 组件中处理 242 | break; 243 | case "codeSnippet": 244 | // 代码片段功能已在 FunctionMenu 组件中处理 245 | break; 246 | case "settings": 247 | // 可以在这里添加更复杂的设置功能 248 | this.showSettings(); 249 | break; 250 | default: 251 | break; 252 | } 253 | } 254 | 255 | // 显示设置界面(占位符) 256 | showSettings() { 257 | // 这里可以实现设置界面 258 | alert("设置功能将在后续版本中实现"); 259 | } 260 | 261 | // 显示初始化错误 262 | showInitError(error) { 263 | const errorMessage = ` 264 |
265 |

😵 应用启动失败

266 |

${error.message}

267 | 278 |
279 | `; 280 | 281 | document.body.innerHTML = errorMessage; 282 | } 283 | 284 | // 获取应用状态 285 | getStatus() { 286 | return { 287 | initialized: this.isInitialized, 288 | deviceId: this.deviceId, 289 | online: navigator.onLine, 290 | timestamp: new Date().toISOString(), 291 | }; 292 | } 293 | 294 | // 重启应用 295 | restart() { 296 | console.log("🔄 重启应用..."); 297 | location.reload(); 298 | } 299 | 300 | // 清理应用数据 301 | clearData() { 302 | if (confirm("确定要清除所有本地数据吗?这将删除设备ID等信息。")) { 303 | localStorage.clear(); 304 | console.log("🗑️ 本地数据已清除"); 305 | this.restart(); 306 | } 307 | } 308 | } 309 | 310 | // 创建应用实例 311 | const app = new WeChatApp(); 312 | 313 | // DOM加载完成后初始化应用 314 | document.addEventListener("DOMContentLoaded", () => { 315 | app.init(); 316 | }); 317 | 318 | // 全局错误处理 - 通知已禁用,避免移动端弹窗遮挡输入框 319 | window.addEventListener("error", (event) => { 320 | console.error("全局错误:", event.error); 321 | // Utils.showNotification('应用发生错误,请刷新页面重试', 'error'); 322 | }); 323 | 324 | // 未处理的Promise错误 - 通知已禁用,避免移动端弹窗遮挡输入框 325 | window.addEventListener("unhandledrejection", (event) => { 326 | console.error("未处理的Promise错误:", event.reason); 327 | // Utils.showNotification('网络请求失败,请检查网络连接', 'error'); 328 | }); 329 | 330 | // 页面卸载时清理资源 331 | window.addEventListener("beforeunload", () => { 332 | // 清理图片blob URL缓存,避免内存泄漏 333 | if (typeof API !== "undefined" && API.clearImageBlobCache) { 334 | API.clearImageBlobCache(); 335 | } 336 | }); 337 | 338 | // 导出到全局作用域(用于调试) 339 | window.WeChatApp = app; 340 | window.CONFIG = CONFIG; 341 | window.Utils = Utils; 342 | window.API = API; 343 | window.UI = UI; 344 | window.FileUpload = FileUpload; 345 | window.MessageHandler = MessageHandler; 346 | if (typeof PWA !== "undefined") { 347 | window.PWA = PWA; 348 | } 349 | // AI模块全局导出 350 | if (typeof AIAPI !== "undefined") { 351 | window.AIAPI = AIAPI; 352 | } 353 | if (typeof AIUI !== "undefined") { 354 | window.AIUI = AIUI; 355 | } 356 | if (typeof AIHandler !== "undefined") { 357 | window.AIHandler = AIHandler; 358 | } 359 | 360 | // AI图片生成模块全局导出 361 | if (typeof ImageGenAPI !== "undefined") { 362 | window.ImageGenAPI = ImageGenAPI; 363 | } 364 | if (typeof ImageGenUI !== "undefined") { 365 | window.ImageGenUI = ImageGenUI; 366 | } 367 | if (typeof ImageGenHandler !== "undefined") { 368 | window.ImageGenHandler = ImageGenHandler; 369 | } 370 | 371 | // 搜索模块全局导出 372 | if (typeof SearchAPI !== "undefined") { 373 | window.SearchAPI = SearchAPI; 374 | } 375 | if (typeof SearchUI !== "undefined") { 376 | window.SearchUI = SearchUI; 377 | } 378 | if (typeof SearchHandler !== "undefined") { 379 | window.SearchHandler = SearchHandler; 380 | } 381 | 382 | // 开发模式下的调试信息 383 | if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { 384 | console.log("🔧 开发模式已启用"); 385 | console.log("可用的全局对象:", { 386 | WeChatApp: app, 387 | CONFIG, 388 | Utils, 389 | API, 390 | UI, 391 | FileUpload, 392 | MessageHandler, 393 | PWA: typeof PWA !== "undefined" ? PWA : undefined, 394 | AIAPI: typeof AIAPI !== "undefined" ? AIAPI : undefined, 395 | AIUI: typeof AIUI !== "undefined" ? AIUI : undefined, 396 | AIHandler: typeof AIHandler !== "undefined" ? AIHandler : undefined, 397 | ImageGenAPI: typeof ImageGenAPI !== "undefined" ? ImageGenAPI : undefined, 398 | ImageGenUI: typeof ImageGenUI !== "undefined" ? ImageGenUI : undefined, 399 | ImageGenHandler: 400 | typeof ImageGenHandler !== "undefined" ? ImageGenHandler : undefined, 401 | SearchAPI: typeof SearchAPI !== "undefined" ? SearchAPI : undefined, 402 | SearchUI: typeof SearchUI !== "undefined" ? SearchUI : undefined, 403 | SearchHandler: 404 | typeof SearchHandler !== "undefined" ? SearchHandler : undefined, 405 | }); 406 | } 407 | --------------------------------------------------------------------------------