chore: add local docker validation workflow
This commit is contained in:
@@ -24,3 +24,5 @@ data
|
||||
.env.local
|
||||
.env.production
|
||||
deploy/.env.production
|
||||
deploy/.env.local
|
||||
data-local
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,9 +15,11 @@ __pycache__/
|
||||
.logs/
|
||||
.pids/
|
||||
deploy/.env.production
|
||||
deploy/.env.local
|
||||
deploy/.htpasswd
|
||||
secrets/
|
||||
.backups/
|
||||
data-local/
|
||||
|
||||
# api
|
||||
api/.venv/
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
- 开发任务结束前必须执行并汇报 `git status -sb`
|
||||
- 功能、修复、规则或部署元数据变更完成后,必须创建人工语义 commit;`auto-save` 只算安全快照
|
||||
- Gitea 是主远端,`origin` 必须指向 Gitea;能联网和鉴权时必须推送完成提交
|
||||
- 默认先在本地 Docker 完整验证:`./scripts/start-local-docker.sh` 后运行 `./scripts/verify-local-docker.sh`;用户明确确认“可以推送 / 上推 / 部署”前,不要 `git push`,也不要运行生产部署脚本。
|
||||
- Gitea 是主远端,`origin` 必须指向 Gitea;只有在用户明确确认推送后,才把已验证的人工语义 commit 推送到 Gitea。
|
||||
- 当前主分支为 `main`,Gitea 仓库为 `https://git.kang-kang.com/kangwan/20260512-skg-tk`
|
||||
- `.memory/worklog.json` 是辅助日志,不代替人工语义 commit 和 Gitea 远端记录
|
||||
- 不能推送时,必须说明当前分支、本地领先/落后数量、最新未推送 commit 和失败原因
|
||||
@@ -26,6 +27,7 @@
|
||||
|
||||
## Completion Gate
|
||||
|
||||
- 普通代码修改完成后,默认收口在本地 Docker 验证和本地 commit;生产推送 / 部署必须等用户明确确认。
|
||||
- 部署完成后,不允许在 `.project.json` 缺少最新公网链接的状态下结束任务
|
||||
- 部署完成后,必须同步更新 `RULES.md` 的部署事实
|
||||
- 如果只更新了代码但没回写部署元数据,这个任务不算完成
|
||||
|
||||
8
RULES.md
8
RULES.md
@@ -1,12 +1,16 @@
|
||||
# SKG 营销内容生产平台
|
||||
|
||||
## 启动
|
||||
- 本地 Docker 启动:`./scripts/start-local-docker.sh`(默认 Web `http://localhost:4390`、API `http://localhost:4391`、Postgres 数据在 `data-local/postgres`;首次会从 `deploy/.env.local.example` 生成 gitignored 的 `deploy/.env.local`)
|
||||
- 本地 Docker 验证:`./scripts/verify-local-docker.sh`(检查容器、`/login/`、未登录 `/api/health`、容器内 `/health` + Postgres)
|
||||
- 本地 Docker 停止:`./scripts/stop-local-docker.sh`
|
||||
- 后台启动(不弹 Terminal):`./scripts/start-dev-background.sh`(通过 macOS launchd 后台托管;前端 4290 + 后端 4291,日志写入 `.logs/`)
|
||||
- 后台停止:`./scripts/stop-dev-background.sh`
|
||||
- 前端 dev:`cd web && npm run dev`(Next.js 16,端口 4290)
|
||||
- 画布 dev:`cd web && npm run dev:canvas`(Vue / Vite,端口 4292;生产构建会作为根域名工作台输出)
|
||||
- 后端 dev:`cd api && uvicorn main:app --host 127.0.0.1 --port 4291`(FastAPI,端口 4291,重任务用)
|
||||
- 注意:后端不要带 `--reload` 跑长下载 / 抽帧 / 音频任务;reload 会等待后台任务结束,导致 4291 端口占用但新请求卡住。
|
||||
- 发布流程新规则(2026-05-26):所有修改默认先在本地 Docker 跑通并验证,确认可用后只保留本地 commit;只有用户明确说“可以推送 / 上推 / 部署”时,才允许 `git push` 或执行生产部署。
|
||||
|
||||
## 立项决策快索引
|
||||
- 详见 `CLAUDE.md` 立项决策段 + `.memory/plan.md` 七步管线拆解
|
||||
@@ -85,11 +89,13 @@
|
||||
- 文档 / 解析:`docs/source-analysis.html`(项目内独立文档,不公开挂主应用路由)
|
||||
- 管理后台:待定
|
||||
- 服务器目录:`/opt/skg-marketing-studio`
|
||||
- 本地 Docker 是生产前默认验收口径:`./scripts/start-local-docker.sh` 构建本地 Web/API/Postgres,`./scripts/verify-local-docker.sh` 通过后才允许进入推送/部署讨论。本地 Docker 使用 `docker-compose.local.yml`、`deploy/.env.local` 和 `data-local/`,不能读取或覆盖生产 `deploy/.env.production`、服务器 `data/` 或 `secrets/`。
|
||||
- 生产部署唯一入口:`./scripts/deploy-prod-safe.sh`(先在服务器备份 `deploy/.env.production`、`data/jobs`、资源库和 `secrets`,如 Postgres 容器存在则额外导出 `pg_dump`,再用受保护 rsync 同步代码,最后 Docker 重建并运行 `verify-prod-docker.sh`)
|
||||
- 生产容器重建命令:`docker compose -f docker-compose.prod.yml --env-file deploy/.env.production up -d --build`;只允许脚本内部或明确只重启容器时使用,不允许再用裸 `rsync --delete` 手动同步。
|
||||
- 独立预览容器重建命令:服务器 `/opt/skg-marketing-studio` 下执行 `docker compose -f docker-compose.standalone.yml --env-file deploy/.env.production up -d --build`;Web 暴露 `0.0.0.0:4290->80`,后端仅在 compose 内部网络暴露,`/api/` 由 Web 容器 Nginx 反代并复用应用内登录校验。
|
||||
- 生产架构:`web` 容器用 Nginx 承载 Next 静态导出与根域名 Vue / Vite 画布静态应用;构建时先生成画布,再 Next 静态导出,最后用画布产物覆盖 `web/out/index.html` 和 `/assets/`,使登录后的 `/` 直接进入画布;`/canvas/` 只做 308 兼容跳转到 `/`。`/login/`、`/_next/`、`/assets/`、`/skg-logo-black.svg`、`/oasis-source/` 等登录页必需静态资源公开访问;未登录访问工作台跳转 `/login/`;`/api/` 通过 Nginx `auth_request` 校验 FastAPI 会话 Cookie 后反代到 `skg-marketing-api:4291`;FastAPI 通过内网 `DATABASE_URL` 连接 `skg-marketing-postgres:5432`,Postgres 不对公网暴露;Traefik 通过 `coolify` 外部网络接入 80/443
|
||||
- Web 验收必须以生产 Docker 形态为准:前端是 `next export` 静态产物 + Nginx,不是 `next dev` / `next start`。任何 Web 改动部署后必须运行 `./scripts/verify-prod-docker.sh`,确认 `/login/`、`/_next/`、`/api/health`、本地 API 地址泄漏和 API 镜像 `.env` 污染检查通过;不能只用本地 `npm run build` 作为上线依据。
|
||||
- 未经用户明确确认,不允许推送 Gitea 或部署生产;完成开发任务时报告本地 Docker 验证结果、当前分支、本地领先数量和待推送 commit。
|
||||
- 当前音频解析:`https://ai.skg.com/azure/v1` 的 `gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内多语言 `faster-whisper` 真实转写,默认语种为 `auto`,支持中文、英文和其他多语言原文识别,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true`,并保持 `ASR_LANGUAGE` 为空或 `auto`,除非明确只想强制单一语种。
|
||||
- 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library`、`./data/prompt_library` 和 `./data/_trash`;Postgres 数据目录为服务器 `./data/postgres`,部署脚本通过 `pg_dump` 产出 `/opt/skg-marketing-studio-backups/skg-marketing-postgres-*.sql.gz`
|
||||
- TikTok 下载登录态:公开视频默认不带 cookies 直接下载,生产环境变量必须显式保持 `YTDLP_COOKIES_FILE=`、`YTDLP_COOKIES_FROM_BROWSER=` 为空,防止容器读取不存在的浏览器 cookies。只有 TikTok 明确要求登录态时,才使用服务器私有 cookies 文件 `./secrets/tiktok_cookies.txt` 挂载到 API 容器 `/run/secrets/tiktok_cookies.txt` 并配置 `YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt`;`yt-dlp` 会在任务结束时回写 cookies,因此不要把该挂载设为只读;不要使用云端浏览器读取方案,也不要把 cookies 入库。生产容器严禁使用 `YTDLP_COOKIES_FROM_BROWSER=chrome`。
|
||||
@@ -118,7 +124,7 @@
|
||||
- Gitea 网页仓库:`https://git.kang-kang.com/kangwan/20260512-skg-tk`
|
||||
- 每次开发结束前必须执行并汇报 `git status -sb` 和变更范围
|
||||
- 代码、规则、部署或元数据变更必须形成 `feat:`、`fix:`、`docs:`、`chore:`、`release:` 等人工语义 commit;`auto-save` 只算安全快照
|
||||
- 能联网和鉴权时必须 `git push origin main`;如果不能推送,最终回复必须写清楚当前分支、领先/落后数量、最新未推送 commit 和失败原因
|
||||
- 用户明确确认“可以推送 / 上推 / 部署”前,不允许 `git push` 或生产部署;用户确认后,能联网和鉴权时再 `git push origin main`,如果届时不能推送,最终回复必须写清楚当前分支、领先/落后数量、最新未推送 commit 和失败原因
|
||||
|
||||
## 环境变量
|
||||
- `LLM_BASE_URL` / `LLM_API_KEY`:OpenAI 兼容网关,用于翻译、文案改写、音频分析等文本/多模态理解模型调用
|
||||
|
||||
91
deploy/.env.local.example
Normal file
91
deploy/.env.local.example
Normal file
@@ -0,0 +1,91 @@
|
||||
# Local Docker only. Copy to deploy/.env.local or run scripts/start-local-docker.sh.
|
||||
# Real production secrets stay in deploy/.env.production on the VPS.
|
||||
|
||||
# Local ports
|
||||
LOCAL_WEB_PORT=4390
|
||||
LOCAL_API_PORT=4391
|
||||
|
||||
# Local Postgres
|
||||
POSTGRES_DB=skg_marketing_local
|
||||
POSTGRES_USER=skg_marketing
|
||||
POSTGRES_PASSWORD=skg_marketing_local_password
|
||||
|
||||
# Local password login for Docker smoke tests
|
||||
PASSWORD_AUTH_ENABLED=true
|
||||
WEB_AUTH_USERNAME=skg
|
||||
WEB_AUTH_PASSWORD=local-skg
|
||||
WEB_AUTH_SESSION_SECRET=local-docker-session-secret-change-me
|
||||
WEB_AUTH_COOKIE_NAME=skg_marketing_local_session
|
||||
WEB_AUTH_COOKIE_SECURE=false
|
||||
AUTH_DATA_ISOLATION_ENABLED=true
|
||||
|
||||
# Feishu can be filled locally if OAuth needs to be tested from localhost.
|
||||
FEISHU_APP_ID=
|
||||
FEISHU_APP_SECRET=
|
||||
FEISHU_REDIRECT_URI=http://localhost:4390/api/auth/feishu/callback
|
||||
FEISHU_OAUTH_SCOPE=
|
||||
FEISHU_ALLOWED_EMAIL_DOMAINS=
|
||||
FEISHU_ALLOWED_EMAILS=
|
||||
FEISHU_ALLOWED_TENANT_KEYS=
|
||||
|
||||
# SKG AI gateway. Leave blank when only testing UI/login/database locally.
|
||||
LLM_BASE_URL=https://ai.skg.com/ezlink/v1
|
||||
LLM_API_KEY=
|
||||
IMAGE_BASE_URL=https://ai.skg.com/ezlink/v1
|
||||
IMAGE_API_KEY=
|
||||
IMAGE_MODEL=gpt-image-2
|
||||
IMAGE_REQUEST_TIMEOUT_SECONDS=60
|
||||
IMAGE_FALLBACK_ENABLED=true
|
||||
IMAGE_FALLBACK_MODEL=gemini-3-pro-image-preview
|
||||
IMAGE_CIRCUIT_FAILURE_THRESHOLD=2
|
||||
IMAGE_CIRCUIT_COOLDOWN_SECONDS=600
|
||||
GPT_IMAGE_MODEL=gpt-image-2
|
||||
SUBJECT_ASSET_IMAGE_MODEL=gpt-image-2
|
||||
SUBJECT_ASSET_IMAGE_MODELS=gpt-image-2,gemini-3-pro-image-preview
|
||||
AI_HTTP_PROXY=
|
||||
|
||||
# Text/vision/audio model names
|
||||
GPT_TEXT_MODEL=gpt-4o
|
||||
REWRITE_MODEL=gpt-4o
|
||||
VISION_MODEL=gpt-4o
|
||||
TRANSLATE_MODEL=gemini-2.5-flash
|
||||
ASR_BASE_URL=https://ai.skg.com/azure/v1
|
||||
ASR_API_KEY=
|
||||
ASR_MODEL=gpt-4o-transcribe
|
||||
ASR_LANGUAGE=auto
|
||||
ASR_REMOTE_ENABLED=false
|
||||
ASR_LOCAL_FALLBACK_ENABLED=true
|
||||
ASR_AUDIO_FALLBACK_ENABLED=false
|
||||
ASR_FALLBACK_MODEL=gemini-2.5-flash
|
||||
ASR_TIMEOUT_SECONDS=45
|
||||
FASTER_WHISPER_MODEL=tiny
|
||||
FASTER_WHISPER_DEVICE=cpu
|
||||
FASTER_WHISPER_COMPUTE_TYPE=int8
|
||||
|
||||
# Video generation. Fill VIDEO_API_KEY only when testing real video generation locally.
|
||||
VIDEO_API_BASE_URL=https://ai.skg.com/doubao
|
||||
VIDEO_API_KEY=
|
||||
VIDEO_MODEL=seedance
|
||||
VIDEO_MODEL_SEEDANCE=doubao-seedance-2-0-fast-260128
|
||||
VIDEO_MODEL_KLING=kling-omni
|
||||
VIDEO_MODEL_VEO3=veo-3.1-fast
|
||||
VIDEO_CREATE_PATHS=/api/v3/contents/generations/tasks
|
||||
VIDEO_STATUS_PATH=/api/v3/contents/generations/tasks/{id}
|
||||
VIDEO_CONTENT_PATH=/api/v3/contents/generations/tasks/{id}/content
|
||||
VIDEO_DURATION_FIELD=seconds
|
||||
VIDEO_POLL_TIMEOUT_SECONDS=900
|
||||
|
||||
# Azure OpenAI TTS. Leave blank unless testing voice locally.
|
||||
AUDIO_REWRITE_MODEL=gemini-2.5-pro
|
||||
VOICE_PROVIDER=azure_openai
|
||||
AZURE_OPENAI_BASE_URL=https://ai.skg.com/azure
|
||||
AZURE_OPENAI_API_KEY=
|
||||
AZURE_TTS_MODEL=gpt-4o-mini-tts
|
||||
AZURE_TTS_VOICE_ID=alloy
|
||||
AZURE_TTS_VOICE_POOL=alloy,verse,shimmer
|
||||
AZURE_TTS_PATH=/audio/speech
|
||||
AZURE_TTS_PATHS=/audio/speech,/v1/audio/speech
|
||||
|
||||
# Optional TikTok cookies. Keep files local and out of git.
|
||||
YTDLP_COOKIES_FILE=
|
||||
YTDLP_COOKIES_FROM_BROWSER=
|
||||
85
docker-compose.local.yml
Normal file
85
docker-compose.local.yml
Normal file
@@ -0,0 +1,85 @@
|
||||
name: skg-marketing-local
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: skg-marketing-local-postgres
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-skg_marketing_local}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-skg_marketing}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-skg_marketing_local_password}
|
||||
volumes:
|
||||
- ./data-local/postgres:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- skg-marketing-local
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-skg_marketing} -d ${POSTGRES_DB:-skg_marketing_local}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
api:
|
||||
image: skg-marketing-local-api:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
container_name: skg-marketing-local-api
|
||||
env_file:
|
||||
- ./deploy/.env.local
|
||||
environment:
|
||||
JOBS_DIR: /data/jobs
|
||||
AGENT_RUNS_DIR: /data/agent_runs
|
||||
ASSET_LIBRARY_DIR: /data/asset_library
|
||||
PROMPT_LIBRARY_DIR: /data/prompt_library
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-skg_marketing}:${POSTGRES_PASSWORD:-skg_marketing_local_password}@postgres:5432/${POSTGRES_DB:-skg_marketing_local}
|
||||
CORS_ORIGINS: http://localhost:${LOCAL_WEB_PORT:-4390},http://127.0.0.1:${LOCAL_WEB_PORT:-4390}
|
||||
PASSWORD_AUTH_ENABLED: ${PASSWORD_AUTH_ENABLED:-true}
|
||||
WEB_AUTH_USERNAME: ${WEB_AUTH_USERNAME:-skg}
|
||||
WEB_AUTH_PASSWORD: ${WEB_AUTH_PASSWORD:-local-skg}
|
||||
WEB_AUTH_SESSION_SECRET: ${WEB_AUTH_SESSION_SECRET:-local-docker-session-secret-change-me}
|
||||
WEB_AUTH_COOKIE_SECURE: "false"
|
||||
WEB_AUTH_COOKIE_NAME: ${WEB_AUTH_COOKIE_NAME:-skg_marketing_local_session}
|
||||
AUTH_DATA_ISOLATION_ENABLED: "true"
|
||||
FEISHU_REDIRECT_URI: http://localhost:${LOCAL_WEB_PORT:-4390}/api/auth/feishu/callback
|
||||
KEYFRAME_COUNT: ${KEYFRAME_COUNT:-12}
|
||||
VIDEO_QUEUE_MAX_CONCURRENT: ${VIDEO_QUEUE_MAX_CONCURRENT:-2}
|
||||
VIDEO_QUEUE_MAX_CONCURRENT_PER_USER: ${VIDEO_QUEUE_MAX_CONCURRENT_PER_USER:-1}
|
||||
YTDLP_COOKIES_FILE: ${YTDLP_COOKIES_FILE:-}
|
||||
YTDLP_COOKIES_FROM_BROWSER: ${YTDLP_COOKIES_FROM_BROWSER:-}
|
||||
volumes:
|
||||
- ./data-local/jobs:/data/jobs
|
||||
- ./data-local/agent_runs:/data/agent_runs
|
||||
- ./data-local/asset_library:/data/asset_library
|
||||
- ./data-local/prompt_library:/data/prompt_library
|
||||
- ./data-local/_trash:/data/_trash
|
||||
ports:
|
||||
- "127.0.0.1:${LOCAL_API_PORT:-4391}:4291"
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
skg-marketing-local:
|
||||
aliases:
|
||||
- skg-marketing-api
|
||||
|
||||
web:
|
||||
image: skg-marketing-local-web:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.web
|
||||
args:
|
||||
NEXT_PUBLIC_API_BASE: /api
|
||||
container_name: skg-marketing-local-web
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
- "127.0.0.1:${LOCAL_WEB_PORT:-4390}:80"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- skg-marketing-local
|
||||
|
||||
networks:
|
||||
skg-marketing-local:
|
||||
name: skg-marketing-local
|
||||
@@ -523,6 +523,16 @@
|
||||
<tr><th>项目</th><th>命令 / 入口</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>本地 Docker 启动</td>
|
||||
<td><code>./scripts/start-local-docker.sh</code></td>
|
||||
<td>生产前默认本地验收入口;使用 <code>docker-compose.local.yml</code> 构建 Web / API / Postgres,默认 Web 为 <code>http://localhost:4390</code>,API 为 <code>http://localhost:4391</code>,本地数据写入 <code>data-local/</code>。首次启动会从 <code>deploy/.env.local.example</code> 生成不入库的 <code>deploy/.env.local</code>,本地登录默认 <code>skg / local-skg</code>。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>本地 Docker 验证</td>
|
||||
<td><code>./scripts/verify-local-docker.sh</code></td>
|
||||
<td>检查本地 Docker 容器、登录页、未登录 API 保护和容器内 <code>/health</code> + Postgres 连接。以后代码改动先通过本地 Docker 验证并形成本地 commit,只有用户明确确认后才推送 Gitea 或执行生产部署。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>本地后台启动</td>
|
||||
<td><code>./scripts/start-dev-background.sh</code></td>
|
||||
@@ -546,7 +556,7 @@
|
||||
<tr>
|
||||
<td>生产部署</td>
|
||||
<td><code>./scripts/deploy-prod-safe.sh</code></td>
|
||||
<td>服务器目录为 <code>/opt/skg-marketing-studio</code>;后端任务文件挂载到 <code>./data/jobs</code>,全局资源中心挂载到 <code>./data/asset_library</code>、<code>./data/prompt_library</code> 和 <code>./data/_trash</code>,Postgres 数据目录为 <code>./data/postgres</code>,真实 Key 和数据库密码只放服务器 <code>deploy/.env.production</code>。生产部署唯一入口是 <code>deploy-prod-safe.sh</code>:先备份服务器 env、案例、资源库和 secrets,如 Postgres 容器存在则额外导出 <code>pg_dump</code>,再用 protect/exclude 保护 <code>data/</code>、<code>jobs/</code>、<code>secrets/</code>、<code>deploy/.env.production</code> 后同步代码,最后 Docker 重建并跑 <code>verify-prod-docker.sh</code>。禁止再用裸 <code>rsync --delete</code> 手动同步。</td>
|
||||
<td>服务器目录为 <code>/opt/skg-marketing-studio</code>;后端任务文件挂载到 <code>./data/jobs</code>,全局资源中心挂载到 <code>./data/asset_library</code>、<code>./data/prompt_library</code> 和 <code>./data/_trash</code>,Postgres 数据目录为 <code>./data/postgres</code>,真实 Key 和数据库密码只放服务器 <code>deploy/.env.production</code>。生产部署唯一入口是 <code>deploy-prod-safe.sh</code>:先备份服务器 env、案例、资源库和 secrets,如 Postgres 容器存在则额外导出 <code>pg_dump</code>,再用 protect/exclude 保护 <code>data/</code>、<code>jobs/</code>、<code>secrets/</code>、<code>deploy/.env.production</code> 后同步代码,最后 Docker 重建并跑 <code>verify-prod-docker.sh</code>。禁止再用裸 <code>rsync --delete</code> 手动同步;未得到用户明确确认前,不推送 Gitea、不部署生产。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>前端开发服务</td>
|
||||
@@ -1391,6 +1401,18 @@ ProductRefStateItem {
|
||||
<p><strong>影响:</strong>画布项目开始具备跨浏览器、跨设备恢复的服务端主存储;默认仍按 owner 私有隔离,后续可在同一表上扩展 team/company 可见性。完整 job state 和媒体文件仍保留在原有文件目录,避免把大文件一次性搬进数据库。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-26 · 本地 Docker 先验收再上推</h3>
|
||||
<span class="tag amber">Ops</span>
|
||||
<span class="tag blue">Docs</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>此前代码修改常直接走生产部署验证,容易把还在反复调整的模型、尺寸或界面配置带到线上,影响正在使用的团队成员。</p>
|
||||
<p><strong>改动:</strong>新增 <code>docker-compose.local.yml</code>、<code>deploy/.env.local.example</code>、<code>scripts/start-local-docker.sh</code>、<code>scripts/verify-local-docker.sh</code> 和 <code>scripts/stop-local-docker.sh</code>。本地 Docker 默认暴露 <code>localhost:4390</code>,使用独立 <code>data-local/</code> 和本地 Postgres,不读取生产 <code>deploy/.env.production</code>。</p>
|
||||
<p><strong>影响:</strong>后续开发流程改为“本地 Docker 启动 → 本地验证 → 本地 commit → 用户确认后才推送/部署”。<code>AGENTS.md</code> 与 <code>RULES.md</code> 已同步该约束,避免后续接手会话自动推送或直接上生产。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-26 · 恢复最初生图配置</h3>
|
||||
|
||||
20
scripts/start-local-docker.sh
Executable file
20
scripts/start-local-docker.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ ! -f deploy/.env.local ]; then
|
||||
cp deploy/.env.local.example deploy/.env.local
|
||||
echo "created deploy/.env.local from deploy/.env.local.example"
|
||||
fi
|
||||
|
||||
docker build -f Dockerfile.api -t skg-marketing-local-api:latest .
|
||||
docker build -f Dockerfile.web -t skg-marketing-local-web:latest --build-arg NEXT_PUBLIC_API_BASE=/api .
|
||||
docker compose -f docker-compose.local.yml --env-file deploy/.env.local up -d --no-build "$@"
|
||||
|
||||
WEB_PORT="$(grep -E '^LOCAL_WEB_PORT=' deploy/.env.local | tail -1 | cut -d= -f2-)"
|
||||
WEB_PORT="${WEB_PORT:-4390}"
|
||||
|
||||
echo "local Docker is starting: http://localhost:${WEB_PORT}"
|
||||
echo "login: skg / local-skg unless deploy/.env.local overrides it"
|
||||
7
scripts/stop-local-docker.sh
Executable file
7
scripts/stop-local-docker.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
docker compose -f docker-compose.local.yml --env-file deploy/.env.local down "$@"
|
||||
64
scripts/verify-local-docker.sh
Executable file
64
scripts/verify-local-docker.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [ ! -f deploy/.env.local ]; then
|
||||
echo "deploy/.env.local is missing. Run ./scripts/start-local-docker.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WEB_PORT="$(grep -E '^LOCAL_WEB_PORT=' deploy/.env.local | tail -1 | cut -d= -f2-)"
|
||||
WEB_PORT="${WEB_PORT:-4390}"
|
||||
WEB_URL="http://127.0.0.1:${WEB_PORT}"
|
||||
AUTH_USERNAME="$(grep -E '^WEB_AUTH_USERNAME=' deploy/.env.local | tail -1 | cut -d= -f2-)"
|
||||
AUTH_USERNAME="${AUTH_USERNAME:-skg}"
|
||||
AUTH_PASSWORD="$(grep -E '^WEB_AUTH_PASSWORD=' deploy/.env.local | tail -1 | cut -d= -f2-)"
|
||||
AUTH_PASSWORD="${AUTH_PASSWORD:-local-skg}"
|
||||
COMPOSE=(docker compose -f docker-compose.local.yml --env-file deploy/.env.local)
|
||||
|
||||
"${COMPOSE[@]}" ps
|
||||
|
||||
login_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-login.html -w '%{http_code}' "${WEB_URL}/login/")"
|
||||
if [ "$login_status" != "200" ]; then
|
||||
echo "ERROR: unexpected /login/ status ${login_status}" >&2
|
||||
head -40 /tmp/skg-local-login.html >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "web:/login/ 200"
|
||||
|
||||
root_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-root.html -w '%{http_code}' "${WEB_URL}/")"
|
||||
if [ "$root_status" != "302" ] && [ "$root_status" != "200" ]; then
|
||||
echo "ERROR: unexpected / status ${root_status}" >&2
|
||||
head -40 /tmp/skg-local-root.html >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "web:/ ${root_status}"
|
||||
|
||||
api_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-api-health.json -w '%{http_code}' "${WEB_URL}/api/health")"
|
||||
if [ "$api_status" != "401" ]; then
|
||||
echo "ERROR: unexpected unauthenticated /api/health status ${api_status}" >&2
|
||||
cat /tmp/skg-local-api-health.json >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "web:/api/health 401"
|
||||
|
||||
login_api_status="$(curl --noproxy '*' -sS -o /tmp/skg-local-login-api.json -w '%{http_code}' -c /tmp/skg-local-cookie.jar -X POST "${WEB_URL}/api/auth/login" -H 'content-type: application/json' --data "{\"username\":\"${AUTH_USERNAME}\",\"password\":\"${AUTH_PASSWORD}\"}")"
|
||||
if [ "$login_api_status" != "200" ]; then
|
||||
echo "ERROR: unexpected /api/auth/login status ${login_api_status}" >&2
|
||||
cat /tmp/skg-local-login-api.json >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "web:/api/auth/login 200"
|
||||
|
||||
"${COMPOSE[@]}" exec -T api python - <<'PY'
|
||||
import json
|
||||
import main
|
||||
|
||||
data = main.health()
|
||||
database = data.get("database") or {}
|
||||
if not data.get("ok") or not database.get("connected"):
|
||||
raise SystemExit(json.dumps(data, ensure_ascii=False)[:1000])
|
||||
print("api:health ok db connected")
|
||||
PY
|
||||
Reference in New Issue
Block a user