From ac6d641f17728082b9aaf124dfa931e06d46afeb Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 24 Sep 2026 16:49:22 +0800 Subject: [PATCH] v 0.0.1 --- .env | 2 + .env.example | 8 + Dockerfile | 15 +- README.md | 37 +++- app/__pycache__/ai_parser.cpython-313.pyc | Bin 20256 -> 20256 bytes app/__pycache__/constants.cpython-313.pyc | Bin 21760 -> 21760 bytes app/__pycache__/main.cpython-313.pyc | Bin 4297 -> 4264 bytes app/__pycache__/models.cpython-313.pyc | Bin 36791 -> 36791 bytes app/__pycache__/schema_patch.cpython-313.pyc | Bin 2433 -> 2433 bytes app/routers/__pycache__/board.cpython-313.pyc | Bin 27025 -> 27723 bytes .../__pycache__/custom.cpython-313.pyc | Bin 46166 -> 46166 bytes .../__pycache__/export.cpython-313.pyc | Bin 20076 -> 20222 bytes app/routers/__pycache__/focus.cpython-313.pyc | Bin 8682 -> 8682 bytes app/routers/__pycache__/staff.cpython-313.pyc | Bin 17188 -> 17188 bytes .../__pycache__/weekly.cpython-313.pyc | Bin 17073 -> 17647 bytes deploy.sh | 164 +++++++++++++++++- docker-compose.yml | 6 +- 17 files changed, 217 insertions(+), 15 deletions(-) diff --git a/.env b/.env index 103dbfa..1d10a27 100644 --- a/.env +++ b/.env @@ -10,3 +10,5 @@ AI_MODEL=deepseek-chat AI_TEMPERATURE=0.2 PORT=8770 +PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +BASE_IMAGE=docker.1ms.run/library/python:3.12-slim diff --git a/.env.example b/.env.example index b64918d..28ad250 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,11 @@ SESSION_DAYS=30 HOST_PORT=8770 # 周期/周次/入库时间等日期按此时区计算,别留 UTC,否则整体差 8 小时 TZ=Asia/Shanghai + +# ---- 国内网络加速(构建镜像用,deploy.sh 首次 up 时自动探测并写回这里)---- +# 国内直连 docker.io / pypi.org 基本超时,deploy.sh 会先探一个能真拉下镜像的加速站。 +# 手工钉死(推荐内网环境写死,省掉每次探测): +# BASE_IMAGE=docker.1ms.run/library/python:3.12-slim +# PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +# 关掉自动探测(源已在上面配好,或机器能直连官方源):CN_MIRROR=0 ./deploy.sh up +# 跳过构建、直接用本地已有镜像(离线机器上 docker load 完之后用):NO_BUILD=1 ./deploy.sh up diff --git a/Dockerfile b/Dockerfile index 0e358a5..4f27c35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,12 @@ # # 镜像里只有依赖和代码,数据库/附件都在 data/,靠 bind mount 挂进来。 # data/ 已被 .dockerignore 排除,不会把本机 board.db 打进镜像。 -FROM python:3.12-slim +# +# 基础镜像。国内直连 docker.io 会在 load metadata 阶段 TLS 超时,所以支持换仓库前缀: +# docker build --build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim . +# ./deploy.sh up 会自动探测可用加速站并把结果写进 .env,正常情况下不用手工传。 +ARG BASE_IMAGE=python:3.12-slim +FROM ${BASE_IMAGE} ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ @@ -20,9 +25,11 @@ WORKDIR /app # 依赖单独一层:改业务代码不触发重装依赖 COPY requirements.txt ./ -# 国内网络直连 PyPI 容易超时,构建时换源即可: -# PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple ./deploy.sh up -ARG PIP_INDEX_URL=https://pypi.org/simple +# pip 源:默认清华源(国内直连 pypi.org 基本超时,而且换基础镜像/代理都救不了它, +# 构建容器里的请求只能靠这里换源)。境外机器或内网自建源就覆盖: +# docker build --build-arg PIP_INDEX_URL=https://pypi.org/simple . +# ./deploy.sh up 会自动探测可用源并写进 .env,正常不用手工传。 +ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple RUN pip install --retries 5 --timeout 60 -i "$PIP_INDEX_URL" -r requirements.txt # 用 MySQL 部署时放开下面这行(与 requirements.txt 的注释一致) # RUN pip install "pymysql>=1.1" "cryptography>=42.0" diff --git a/README.md b/README.md index 12d21d7..e847dd1 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ cd /path/to/unis_manager |---|---| | `Dockerfile` | 运行镜像:装依赖 + 拷 `app/ web/ scripts/`;启动时先跑 `init_db.py`(幂等)再起 uvicorn | | `docker-compose.yml` | 编排:`./data` 挂进容器存数据库、`HOST_PORT` 控对外端口、`restart: unless-stopped` 随 Docker 自启 | -| `deploy.sh` | 命令封装:`up` / `update` / `restart` / `down` / `logs` / `status`,顺带检查 docker 是否在跑、`.env` 与 `data/` 是否存在 | +| `deploy.sh` | 命令封装:`up` / `update` / `restart` / `down` / `logs` / `status` / `backup` / `mirror`,顺带检查 docker 是否在跑、`.env` 与 `data/` 是否存在,并在构建前探测国内可用源 | | `.dockerignore` | 保证 `data/`、`.env`、`.git` 不会被烤进镜像 | | 场景 | 命令 | @@ -122,9 +122,42 @@ cd /path/to/unis_manager | 停止(数据保留) | `./deploy.sh down` | | 换对外端口 | `.env` 里 `HOST_PORT=8080`,再 `./deploy.sh up` | | 裸 compose 命令 | `docker compose up -d --build` / `docker compose down` | -| 构建时 pip 下载超时 | `PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple ./deploy.sh up`(也可写进 `.env`) | +| 构建时基础镜像 / pip 下载超时 | `./deploy.sh mirror` 重探,或手工指定 `PIP_INDEX_URL=... BASE_IMAGE=... ./deploy.sh up`(见下「国内网络加速」) | | 把现有数据带到新机器 | `./deploy.sh backup` → 拷到目标机 `data/board.db` → `./deploy.sh up` | +#### 国内网络加速(docker.io / pypi.org 连不上时) + +国内构建会撞两道**互不相干**的墙,得分开治,只治一道必然还剩一道: + +| 卡在哪 | 典型报错 | 治法 | +|---|---|---| +| ① 守护进程拉基础镜像 | `failed to resolve source metadata for docker.io/library/python:3.12-slim: ... TLS handshake timeout` | 换仓库前缀 `BASE_IMAGE`(走加速站,或给守护进程配 registry-mirrors) | +| ② 构建容器里装依赖 | `[4/8] RUN pip install ... -i https://pypi.org/simple` 非零退出 | 换 pip 源 `PIP_INDEX_URL`(清华 / 阿里 / 腾讯) | + +`./deploy.sh up` 在构建前会自动把两道源都探出来(判据是真的 `docker pull`、是在容器里 `urlopen`,不信宿主机 curl,也不信 `docker manifest inspect` 的假 `denied`),结果写进 `.env`,下次以及裸跑 compose 都复用: + +```bash +./deploy.sh up # 自动探测 → 写 .env → 构建 → 等健康检查 +./deploy.sh mirror # 只想重探一遍源(不会冲掉 .env 里手工钉死的 BASE_IMAGE) +``` + +不想用脚本,等价的两条 build-arg: + +```bash +docker compose build \ + --build-arg BASE_IMAGE=docker.1ms.run/library/python:3.12-slim \ + --build-arg PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +docker compose up -d +``` + +几个必须知道的点: + +- **基础镜像拉成功 ≠ 构建能过**:`docker pull` 只治①,容器里的 `pip install` 照旧撞 pypi.org,必须另外给②换源。 +- **shell 里的 `HTTP(S)_PROXY` 对构建没用**:Docker 守护进程和构建容器都不读宿主机的代理变量。要走代理得在 Docker Desktop → Settings → Resources → Proxies 里配 `http://host.docker.internal:7897`(端口换成你本机代理的)。 +- **境外机器 / 内网有自建源**:`CN_MIRROR=0 ./deploy.sh up` 关掉探测,源完全由 `.env` 说了算。 +- **目标机完全不能上网**:走离线路。`docker save -o unis-board.tar unis-board:latest` → 拷过去 `docker load -i unis-board.tar` → `NO_BUILD=1 ./deploy.sh up`(跳过构建直接用本地镜像)。 +- 构建失败时脚本会自己打印一份排查清单(`deploy.sh` 的 `build_hint`),照抄命令即可。 + 几个坑: - **数据只落在 `./data`**:重建镜像、`down` 再 `up` 都不动数据;备份依旧是拷走 `data/board.db`。 diff --git a/app/__pycache__/ai_parser.cpython-313.pyc b/app/__pycache__/ai_parser.cpython-313.pyc index e8d4e1492a57684b30fa2d0531486da912479fe8..8c30824bba0b990397f6679b54dfafc90afdaafe 100644 GIT binary patch delta 48 zcmZ25k8!~~M()qNyj%=G@a^W7joi)JOdZ=MPtXnqQj4Ezvw(P9racXlCuj!)sl`vVSwK8acGb;11|sqRlA#az diff --git a/app/__pycache__/constants.cpython-313.pyc b/app/__pycache__/constants.cpython-313.pyc index 01bba388a1f594beec78cf7b3f81b233fcdfc63f..7d37c08af7d5c86873ee901f4db8b31c884a471f 100644 GIT binary patch delta 22 ccmZoz#n`Zlk^3_*FBbz4e7m`2Blp)308`uu0RR91 delta 22 ccmZoz#n`Zlk^3_*FBbz4be!L?k^5^108oktg#Z8m diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index 7602ede8302f2ca1ab4d0871810240c6f8649af3..8c1b9e9f115a09f0ce1bc1a4be52a4990a0c5300 100644 GIT binary patch delta 118 zcmX@9xI&TlGcPX}0|=bolC^y!?=v=GejvxiH~9Iwh0iCnJ>9waY5#`FqU=7x!kFTl zo7ubB7#C0G=i9}mx}y9doB8DPe0n09JH-!39G5#NciA%Ff<@p(wx9;)$rt%mH(T;g GU<3fBO)h}| delta 145 zcmZ3Xcv6w~GcPX}0}$L&-jsE9BkwadB{?APW%G)c(^oug-TZROx+lALKkZuhV*lP3 zJ61iP)BSAj{>g0YK1xaim2FOC?`C6MHu)p(E;f@L=@;4DCU4}^6SX+Wejy;@dO+O8 VfVj&Z@fX|^F0v(V{>ZnT5dgg|NqhhR diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc index 8a190d314f5bdffc3d753603c073c54086337579..f42c98bf63925e3eb0b3077e002dc9513a2ca99e 100644 GIT binary patch delta 54 zcmdl!pK1GiChpI?yj%=G@a^W7jodE;m^!v?{wlD?7DR9M>lFjhPN`40K(t0dzdQg? CY!*`h delta 54 zcmdl!pK1GiChpI?yj%=G(4f3&BlimdracXtzY6TJ1<{-Rdc{DrQ|c2g5Uo+rFAo4C CWEC9% diff --git a/app/__pycache__/schema_patch.cpython-313.pyc b/app/__pycache__/schema_patch.cpython-313.pyc index 044a280bb9479a8defd19d163653410fa29341e8..e697d7395c78907931343f4e9d4e87bc9f817803 100644 GIT binary patch delta 26 gcmZn^ZWQMJ%*)Hg00iG|Zkfpan5kpi#t#!Y0BwN@#{d8T delta 26 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 Yd)laxCc#|zH#3oB?Y=D%sg00-1CT)6u>b%7 diff --git a/app/routers/__pycache__/custom.cpython-313.pyc b/app/routers/__pycache__/custom.cpython-313.pyc index 6abc1f6e4ea1fa7f9423dc6f21146d03697c4a11..b15675079813bc5640c2c99dedc9eb953d5efa31 100644 GIT binary patch delta 70 zcmccig6Y}|ChpI?yj%=G@a^W7joe1;OdZ=c+p|ZCgXmdufl46yt%Dp7i01V@$O@t_ N1f8$}%lGe50RX#Z98>@R delta 70 zcmccig6Y}|ChpI?yj%=GaQ6C!joe1;OnVwO+p|ZCgXmdufl46yt%Dp7i01V@$O@t_ N1f8$}%lGe50RXXA92o!r diff --git a/app/routers/__pycache__/export.cpython-313.pyc b/app/routers/__pycache__/export.cpython-313.pyc index 6e1fb64f9cd29ae866dd1514e223d63c39c17184..257e635526c6e739e629633a3716b0ba1eb285b3 100644 GIT binary patch delta 2069 zcmZvddrVVz6vuzRJ}$S9+t+P*mC74hmB9dw1LlCJAgCZ+Rd7O3sur-oudQh2q_`Pf z<^yx)9uCJIG261~9NkRzO15PGxVb;f{?Ij>ZZWtJmsrNK#~&{Hom)`3?Tw@vJ2234i_dG0g4v*R=}L)LyO9R15JC8=oX3X!IG{Tu@i$fSDW_2J@|2!Dno< zE1sde2c8`G$WTa)Fq&b_${b<~_2PCnY%``g`E|aO2iIf- zT+CO}C^s`@RajgWyWa98rA z8TXjIvd>&ey}C@&EB0!FlJaOXbu!eO>}q^WwEIoQV%_|anuym}q4EmJw@=C=UTI$Q zNibom$kzJ|{OBwjsn_hadToyhLDSai`RDPOV8$dy|8_xk(6DM#TQdAZZV+W3Cv<%kQRaCbNoifb^AZGu1KTCIf(Ik4PbU<-1K zH=2Y#R}u10lq6E2-#(dcxNekAHa_3@;;A> zv^yFs;-0fS;{Z=Fy@-Q7!x2g6n7VLgimQ!TC3Fb(X4U4lb7wqdkPYwI9=>3k;ab*eAKQh3F6iGE?h1wdgG@rLn8PEnC~BJh{%#i6?imP%ce4PhL+I8g zb9DuJLaY}Z>d0V-6`p@FMQ?Xh6?8v+f3xhP5_FjjP)Mqy=f zJ-q^>#glY1RJ!ZwH5hgm&|w&Nm(hdpz1zik$KA4MGv?)Acft;F#DzeptIxkbuIcfI zcCdEX;>j1HsCK}pr;HwfNl&?BH#%AoO4V2->w|wi`LrKgi|gozv986wlqG}cKaF?^ z@iHQUcm?q);tYpJ#~9pv)KwTlPZ$9RC3G*hA_B+lv*&JqHpYe_w_FzXp}rrg%D0YS zo2FB7{WN0beW0K;G3on&FFj5+f?hmkwgKTlv>>90Q4W6o$MWppR8ap~Qc5M2sv+k+G&w3sbV7%%ETqxHY zl&P-7ga_edbr(GZcdH#vWsY_JU}TS)6HR-&$!!=dZ z6`QQNk-{hJ)T;G#2i#osGoPw2SD%n^h~h$zKinPYXI9+S4xMY#=f~&3nht|9ILVoT zU)MM+{TTHV0{`3CVGtTt(AL-!4Ns}41;*EA!|P3Y!zR@3B6dS=lPoC-M85WjEDE>R z7NkAwIgf!e3q|XBbX`;NL(!V#6gOjZ07ja!lNBoW3c!0!BQyY>=4Ltqk>+w5gm;^3 z=y8~84ye+cuzp>}PU|{K%dQ(!6KXVm;f5BCgg~UUM2cdH#NbHi6jM)ZSqXz;8i^@A iAt|Pbm~9EOVp@pK@~4t>R!vlk5-P>p8T)$O`}BX)9{wr- delta 2083 zcmaJ?du)?c6u-Ca`tDcO=Qj3$QPy=EY>W|LOdLzbaAN|iotvn$uus-)ZRc%^JY+4A zAoxJY2~QzNBL4w0>K}sqW7HVJA3QV$8s!lKW(mfU86P1K@40QaM2TP0-#zDd&OPVc zd(QXu$uauiD0NSmOnSnf3%kw^rsomzGb*{qkP+yFW1ssmRM6>ElUz=BDHInIa8+5& zm*ivRY*m>iw}G!V{!&xb(=N5JTGnRA;e!xh))A|wQZ1i){?JT z)xa%EVVRq?^yX`Pq%s@SU!G;lpr+XdqdJLO=~Jwu%~rUgGjprrDOO2e4vx7NXwf%w z57necE@v5hJJUVGr&>TWd2?$>v(}ld1k0Yb*amkBv=x(PZ7uQUt)U@ZEeYv;S}SRz zD_$TZWbo;`rh{K^r3N@*aC02f+c|z&YlFRu@|9|z0d5OEzfLfDoariuyN20HLzfG- z8=YePxX>r8t-Q)^Lqt(a zA_^;6VId^^1*Bc#V6N4QPToQTnZb=ALs0TseNqGU>Wavqc2E^EiersQRyZQrlz7B3 zsf=pjn&dPa%Zb;NyBMDl{*>m=*ZK7Pf~KvZUb8pfYgw!gnbt0uyib$Yyg-rBTs&VU zXYv)ZS3w70EI&avK)q!hj@tptEl61720aTzhWdu(jq)%&u`aZmSr^~Wyc|;MNYo#V zMVJp(+nQBoPQiYg%M#=Q?=p!T=c)cmkhBdh+s-8q+kd6n%g(}1muiaxZYv$hPn_3P zm}^sUec@KVVZ4+Bdy7M?fWy5rNA7X!SH*Ma2s|uqp2>%qq%5GBQgtw z`&mC%ST}T)w5n4oEE$fL6fBCPi)yp%XQ{kkI20XV1L!w`v_7-5FVG)h0VI^MkqC34 zeFz?uw9u`|+S22c_9U;AombNc?5;@ADCpg%X&gRxx6n)Q$bF7>!)OD0^0wc4N2~5w>Frbs*9=ByUcs`sK)a)&ti)v(#84>xD;r=T4}ps<&@I zVl{#&7HeaJ5UH9)hu~n}4C#Qj9PM0h`F)MA!tM)vt|XXT}|~%W$W- z>Y2_)k;S`a&m-6oRw0NZC%D9$kZC6Gg18}e7VX$)(O$L#CDGfr@hitWyl=iu-id4p z!s`gT5#Hs%2QnOE>t$v_#?8SaWs<#z#QO+eBTP;K`v6sO#d}b?h>)EDeM2L?pKNV7 zskUBBjGs9F%SUHo{7ub`<>N*t&aPw0Ej!LGn}qQORJVbysc)X>x;_$S(Y`S2m)X?$ zQaU*l3(CwNl`}^XqtXk9n;KlAyh|7bD@$pDa&Vo@A}Q5y*x#Qk?_pCTElSorb4$bL z(a_pSUxVJ(pZE+eT)t->j$BIJFGu}>VU~}P#GQ)U9Ygg81aXDg`TJw}OLb!MriShU z2FVLVbdVX?e1k$yH%Oj<3BN{GKsnPR~-W|rz6fYp6=+Nqwse}Jq<$5 z$|in$ft7)HY59$!(wnxzL+-29&t9`N-?S7Ss({+G`8i7KZWtYDB?@=AkcUEAO-#*X@y{W6hxtYr)Yc8Rio>>WMAiT F`VSQC0B8UJ diff --git a/app/routers/__pycache__/focus.cpython-313.pyc b/app/routers/__pycache__/focus.cpython-313.pyc index 552229c73e7732184380031f82804841f1872a94..a620997e731408b2f00c72936167822c94ff5a0c 100644 GIT binary patch delta 28 icmaFm{K}d8GcPX}0}y<>xn(1F12a>{w#~iFv2p;I)CxTS delta 28 icmaFm{K}d8GcPX}0}$-$*szhifthJf!{%P*SUCWY)(NTr diff --git a/app/routers/__pycache__/staff.cpython-313.pyc b/app/routers/__pycache__/staff.cpython-313.pyc index 214587a3881ec2ecd7a0fcccd3e4896d540d4f23..077361072182dfd0606824e37b7e3ab7b4e8822b 100644 GIT binary patch delta 46 wcmZ3|#<--7k^3_*FBbz4e7m`2BX=SbQ^&T=c}$@^Ao?iZbv+P0&$f*f0Cq?b0{{R3 delta 46 wcmZ3|#<--7k^3_*FBbz4?CRLCkvoxzX-~uEJf=_{5Pg*Ix*mw0XWPaK0A*SbYybcN diff --git a/app/routers/__pycache__/weekly.cpython-313.pyc b/app/routers/__pycache__/weekly.cpython-313.pyc index dd7adc6bba4b16c2df5d78f1fd87cbe06da975d1..7eba17591343c0cbaff25e60e1b7784ad766ac49 100644 GIT binary patch delta 1151 zcmcJMUrbwd6vuz(_7CNj-nOGH+?I~p0)tX0(7M$P6bRYemUNjkHJDZYj7=jOx5=+4 ziJBCRn>r(roh&L3xcM-3FAneAgE4L-OkbAl4~ZE_%*N(}55|%jqfdH%Y#QggH}`(d z_j`WlcTP_3{4x0DEl^4hhs@~tZTZ63*@k)LQIOueh`V7?1Vsu^Frw0|B=4$XY|t_o z9c(eKnG*m}!(`nKl`ZMVGto@TBU0MpXJfW8v##KbGiV$ws|V;dW^6ep^cYZm0`MI* zEa^@7nHo0hk<1qSQLV%Cstnt3T@B%?>VYJwtvKPYq9Qw|EN7ZBk@RCRMQ_IPKp!+?DliGX#+AVD?t+?xxiC|yosP`8mB&tYXTv{m-Lmk5Po7)?7BNUn|&Y7 zKmA45CnJ{!7CNu@AGpzX;JwgHXufMEG@JdSWTVp8JdH5xXoOK9^Y`=c!iE2JW3JQG ze{IW;k$-4G+!a&G_;FG827VYj3y)r{w*8s)cG5 zh!OIw7)Z9@&g3R}nXD>OdyJ8!&!*i|+&A3W53)<{?2?k5dSdlI9w)o!oVQes-u0l< zZk_5|598*ZW+>pEp6!K$z&!E%Fg08>dB>yX#U^FE-Fj7Vk8iVHt#c8t#eL(Qn{HaY zI#X`ex&)J6-J&eHT!Kx0T~^jSUftM&Cjx`Z#G9Nc#_8O z(*pGmR2}0D|0MU4dP&YtaP1XsUrs;F7sSy#*7tq|%Xp_Z2y6If?{NR51lC(lwP+IJ uw(KcdgeY2Bg|Aq)A#FmIZ%CQBy+!+mtYGD?UmZf0OiZd;mxSaUG~_Rtksm4m delta 681 zcmYk1O=uHQ5XW~m$u`*}+l^+^?j~uXP0g20yGd#qwHA{eq=FVhqz9=*@KW3qHi{^s z6|C0kpW(yXD8MBU=O9ED!{l>Yb5I~<%v5q&xAp=^cfi3RAJMwN^Abwo2V@Pr1n%s&* zN*nCMiqc}&=tHO#G9-^OY09Hj^ry?Kb>ocAL18!q2Z z7hVn9@NV~h{MI#uGhJrPbw4oQAWQMO8iXqHY8WQ56qfO*dJ>-Fy!sGc;$ZSIyux3} z08FEtdJ3;`DK!soa6IjYsqJYM-={mEhJVt2Zi+07IxyIC6Kg#_>nu%Kea`sO^9~?x zyvPiL*h-3eCzS+|avas{UOb%@`4y7NwA5}~%6hE0aQMEhAv^8dM8466r*bj4fVXnl zYs0{}ljTt|Su;tR#axRB8qX~WE=}Z?+C`$vNslJ&Smh*55mo~t)r_PCgtwwdHKJ%C zVcjihQTKYMNOXhjNq9DVBGJ#JoAP}P$y%>D>d-Ufj86ZdhqkGm(L4SX2MJx~%jfmr zmEf`Q(pmi(Z9T#>`6{enV89O_v2S4Xgw6u@p;@k*D6H}Bx`jfWV;r7((-vAO+P08q aTvFY>g$~9h{%}&/dev/null || true)" + case "$code" in 000|"") return 1 ;; 5*) return 1 ;; *) return 0 ;; esac +} + +# with_timeout 秒 命令…(macOS 没有 timeout 命令,自己搭一个,防止探测卡死) +with_timeout() { + local t="$1" rc=0 pid wpid; shift + "$@" >/dev/null 2>&1 & pid=$! + ( sleep "$t"; kill "$pid" 2>/dev/null ) >/dev/null 2>&1 & wpid=$! + wait "$pid" || rc=$? + kill "$wpid" 2>/dev/null || true; wait "$wpid" 2>/dev/null || true + return "$rc" +} + +# ---------- 1. 国内加速:基础镜像 + pip 源 ---------- +# 国内直连 docker.io / pypi.org 会 TLS 超时(报错形如 failed to resolve source +# metadata for docker.io/library/python),所以构建前先把「真能拉到东西的源」探出来, +# 并写回 .env —— 之后裸跑 docker compose up -d --build 用的也是同一套配置。 + +BASE_IMAGE="${BASE_IMAGE:-$(env_get BASE_IMAGE)}" +BASE_IMAGE="${BASE_IMAGE:-python:3.12-slim}" # 想换加速站就写进 .env,例:docker.1ms.run/library/python:3.12-slim +BASE_TAG="${BASE_TAG:-${BASE_IMAGE##*/}}" # 加速站上的固定路径是 library/ +MIRROR_HOSTS="${MIRROR_HOSTS:-docker.1ms.run docker.m.daocloud.io docker.1panel.live docker.xuanyuan.me}" +PIP_URLS="${PIP_URLS:-https://pypi.tuna.tsinghua.edu.cn/simple https://mirrors.aliyun.com/pypi/simple https://mirrors.cloud.tencent.com/pypi/simple https://pypi.org/simple}" + +configure_mirror() { + local force="${1:-}" host url ref tag pip_picked=0 base_picked=0 + PIP_INDEX_URL="${PIP_INDEX_URL:-$(env_get PIP_INDEX_URL)}" + if [ "$force" = "--force" ]; then + PIP_INDEX_URL="" + # 手工钉死过的基础镜像不冲掉,只重探 pip 源 + if [ -z "$(env_get BASE_IMAGE)" ]; then BASE_IMAGE="python:3.12-slim"; fi # .env 里钉死过的基础镜像不冲掉 + fi + BASE_IMAGE="${BASE_IMAGE:-python:3.12-slim}" + + if [ "${CN_MIRROR:-1}" = "0" ] || [ "${NO_BUILD:-0}" = "1" ]; then + export BASE_IMAGE PIP_INDEX_URL + echo "==> 跳过加速探测(CN_MIRROR=0 或 NO_BUILD=1):${BASE_IMAGE:-官方 python} / ${PIP_INDEX_URL:-官方 pypi}" + return 0 + fi + + # 基础镜像的判据是「Docker 守护进程能不能拿到它」,不是宿主机 curl 能不能通: + # 宿主机 curl 走 HTTP_PROXY,守护进程和构建容器都不认 shell 的代理变量。 + tag="${BASE_IMAGE##*/}" + if docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then + echo "==> 基础镜像本地已有,不联网:$BASE_IMAGE" + elif with_timeout 25 docker pull --quiet "$BASE_IMAGE"; then + echo "==> 基础镜像可从当前配置源拉取:$BASE_IMAGE" + else + echo " $BASE_IMAGE 守护进程侧拉不到,开始找加速站" + for host in $MIRROR_HOSTS; do + ref="$host/library/$tag" + echo -n "==> 测试加速站 $host " + if with_timeout 120 docker pull --quiet "$ref"; then + BASE_IMAGE="$ref"; base_picked=1; echo " 可用"; break + else + echo " 不可用" + fi + done + if [ "$base_picked" = 0 ]; then + echo "!! 所有加速站都拉不到 $tag;可以先在服务器上手拉,再 ./deploy.sh up(本地有就不会再联网):" >&2 + echo " docker pull docker.1ms.run/library/$tag && docker tag docker.1ms.run/library/$tag $BASE_IMAGE" >&2 + fi + fi + + # pip 源:装依赖是在构建容器里发生的,容器不继承 shell 的代理, + # 所以拿刚选好的基础镜像真跑一次访问探测,比在宿主机上 curl 靠谱。 + if [ -n "${PIP_INDEX_URL:-}" ]; then + echo "==> pip 源(沿用已有配置):$PIP_INDEX_URL" + else + docker image inspect "$BASE_IMAGE" >/dev/null 2>&1 \ + || with_timeout 120 docker pull --quiet "$BASE_IMAGE" \ + || BASE_IMAGE="$(docker images --format '{{.Repository}}:{{.Tag}}' | grep -m1 '^python:3' || true)" + for url in $PIP_URLS; do + if [ -n "${BASE_IMAGE:-}" ] && with_timeout 40 docker run --rm --entrypoint python "$BASE_IMAGE" \ + -c "import sys,urllib.request;urllib.request.urlopen(sys.argv[1],timeout=8)" "$url/"; then + PIP_INDEX_URL="$url"; pip_picked=1; break + fi + done + if [ -z "${PIP_INDEX_URL:-}" ]; then + PIP_INDEX_URL="https://pypi.org/simple" + echo "!! 构建容器里访问不到任何国内 pip 源,先用官方源;超时就手工指定:" >&2 + echo " PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple ./deploy.sh up" >&2 + fi + fi + + # 探出来的结果落盘,下次(以及裸 compose 命令)直接复用 + if [ "$pip_picked" = 1 ]; then env_set PIP_INDEX_URL "$PIP_INDEX_URL"; fi + if [ "$base_picked" = 1 ]; then env_set BASE_IMAGE "$BASE_IMAGE"; fi + export BASE_IMAGE PIP_INDEX_URL + echo "==> 本次构建用:基础镜像 $BASE_IMAGE | pip 源 $PIP_INDEX_URL" +} + +# 构建失败时给一份可操作的排查清单(国内环境九成是源的问题) +build_hint() { + if [ -n "${HTTPS_PROXY:-}${HTTP_PROXY:-}${https_proxy:-}${http_proxy:-}" ]; then + echo "!! 注意:当前 shell 配了 HTTP(S)_PROXY,但 Docker 守护进程不认 shell 环境变量。" >&2 + echo " 要用代理出网,得在 Docker Desktop → Settings → Resources → Proxies 里配" >&2 + echo " http://host.docker.internal:7897(端口换成你本机代理的),否则只能走加速站。" >&2 + fi + cat >&2 <<'HINT' +!! 构建失败。国内环境最常见的两个原因: + 1) 基础镜像拉不到(failed to resolve source metadata / TLS handshake timeout) + ./deploy.sh mirror # 重跑自动探测 + BASE_IMAGE=docker.1ms.run/library/python:3.12-slim ./deploy.sh up # 手工指定加速站 + 也可以给 Docker 守护进程配 registry-mirrors(Docker Desktop → Settings → Docker Engine): + { "registry-mirrors": ["https://docker.1ms.run"] } + 保存并重启 Docker 后,把 .env 里的 BASE_IMAGE 那行删掉再 ./deploy.sh up + 2) pip 装依赖超时 + PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple ./deploy.sh up + 完全不能上网的机器:在能上网的机器上构建后拷镜像过去 + docker save -o unis-board.tar unis-board:latest && scp unis-board.tar host:/srv/unis_manager/ + ssh host 'docker load -i /srv/unis_manager/unis-board.tar' + 然后在目标机 NO_BUILD=1 ./deploy.sh up +HINT +} + +# ---------- 2. 准备 .env 与 data 目录 ---------- prepare() { if [ ! -f .env ]; then cp .env.example .env @@ -42,7 +172,7 @@ prepare() { mkdir -p data/uploads data/exports } -# ---------- 2. 等服务就绪 ---------- +# ---------- 3. 等服务就绪 ---------- wait_ready() { echo -n "==> 等待服务就绪 " for _ in $(seq 1 30); do @@ -58,13 +188,26 @@ wait_ready() { return 1 } -# ---------- 3. 分发命令 ---------- +# ---------- 4. 构建并启动(NO_BUILD=1 时直接用本地镜像)---------- +up_or_exit() { + if [ "${NO_BUILD:-0}" = "1" ]; then + echo "==> NO_BUILD=1:跳过构建,直接用本地镜像 unis-board:latest" + "${COMPOSE[@]}" up -d + elif "${COMPOSE[@]}" up -d --build; then + return 0 + else + build_hint; return 1 + fi +} + +# ---------- 5. 分发命令 ---------- case "${1:-up}" in up|start) first_run=0 [ -f data/board.db ] || first_run=1 prepare - "${COMPOSE[@]}" up -d --build + configure_mirror + up_or_exit || exit 1 wait_ready echo " API 文档:$URL/api/docs" if [ "$first_run" = "1" ]; then @@ -73,7 +216,10 @@ case "${1:-up}" in ;; update) prepare - "${COMPOSE[@]}" up -d --build --force-recreate + configure_mirror + if [ "${NO_BUILD:-0}" = "1" ]; then "${COMPOSE[@]}" up -d --force-recreate + else "${COMPOSE[@]}" up -d --build --force-recreate || { build_hint; exit 1; } + fi wait_ready ;; restart) @@ -104,6 +250,12 @@ print(f"[ok] 已导出 {sys.argv[1]}({os.path.getsize(sys.argv[1]) // 1024} KB PYEOF echo "==> 部署到新机器:把这个文件放到目标机的 data/board.db,再执行 ./deploy.sh up" ;; + mirror) + prepare + configure_mirror --force + echo "==> 已写入 .env 的配置:" + grep -E '^(BASE_IMAGE|PIP_INDEX_URL)=' .env 2>/dev/null || echo " (无:说明用的是默认源或 CN_MIRROR=0)" + ;; status) "${COMPOSE[@]}" ps echo -n "==> /health:" @@ -112,7 +264,7 @@ PYEOF echo "==> 访问地址:$URL" ;; *) - sed -n '3,9p' "$0" + sed -n '3,11p' "$0" exit 1 ;; esac diff --git a/docker-compose.yml b/docker-compose.yml index b5f3967..45f3875 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,9 @@ services: build: context: . args: - # 传给 Dockerfile 的 pip 源,默认官方;国内网络可在 .env 里设 - # PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple - PIP_INDEX_URL: "${PIP_INDEX_URL:-https://pypi.org/simple}" + # 基础镜像仓库前缀 + pip 源,都由 deploy.sh 探测后写进 .env(也可手工在 .env 里钉死) + BASE_IMAGE: "${BASE_IMAGE:-python:3.12-slim}" + PIP_INDEX_URL: "${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}" image: unis-board:latest container_name: unis-board restart: unless-stopped