feat: add HA infrastructure, CI/CD pipeline, and Redis/Celery hardening

- Add docker-compose.ha.yml for PostgreSQL/Redis HA setup with Patroni and Sentinel
- Add docker-compose.prod.yml for production deployment
- Add GitHub Actions CI/CD workflow (build.yml)
- Add install.cmd for Windows one-click setup
- Harden Redis connection with retry logic and health checks
- Add Celery HA config with Redis Sentinel support
- Add HA operations runbook
- Update README with deployment and architecture docs
- Move landing page spec to .claude/specs/design/
- Update memory intelligence PRD
This commit is contained in:
zhangtuo
2026-02-28 14:57:02 +08:00
parent 9cdff18336
commit c82d408ea1
14 changed files with 1301 additions and 24 deletions

View File

@@ -3,9 +3,15 @@
## 概述
**功能名称**: 记忆智能 (Memory Intelligence)
**版本**: v1.0
**版本**: v1.1
**优先级**: Phase 2.5 (体验增强后、社区化前)
**目标用户**: 家长 + 3-8 岁儿童
**更新记录**: 2025-01-22 合并 `backend/docs/memory_system_prd.md`
### 核心愿景
将当前的"数据存储"升级为有温度的**"情感连接系统"**。
我们不只是在记住数据,而是在**维护孩子与故事世界的关系**。让每一个故事不再是孤立的碎片,而是构建孩子专属"故事宇宙"的砖瓦。
### 核心价值
@@ -14,10 +20,39 @@
- **延续故事**: 角色、世界观跨故事延续
- **主动关怀**: 适时推送个性化故事建议
### 产品痛点与解决方案
| 用户角色 | 核心痛点 | 解决方案 | 预期价值 |
|---------|---------|---------|---------|
| **孩子** | "上次的小兔子怎么不认识我了?" 故事之间缺乏连续性。 | **角色一致性与记忆注入** 故事开头主动提及往事,角色性格延续。 | 建立情感依恋,提升沉浸感。 |
| **家长** | "这App除了生成故事还能干嘛" 无法感知产品的长期教育价值。 | **显性化成长轨迹** 词汇量统计、主题变化、成就徽章可视化。 | 提高付费意愿,提供社交货币。 |
| **平台** | 用户用完即走,缺乏留存壁垒。 | **沉没成本与情感资产** 积累的记忆越多,越舍不得离开。 | 提升长期留存率 (LTV)。 |
---
## 一、功能模块
### 1.0 记忆分层模型
#### 层级 1: 核心档案 (Identity Layer)
*性质:永久、静态、显性*
- **数据**: 姓名、年龄、性别
- **输入**: 家长在 Onboarding 阶段手动输入
- **作用**: 决定故事的基础适龄性和称呼
#### 层级 2: 故事宇宙 (Universe Layer)
*性质:长期、动态积累、半显性*
- **主角设定**: 姓名、性格特征(勇敢/害羞)、外貌特征(戴眼镜/卷发)
- **常驻配角**: 从随机故事中涌现出的固定伙伴(如"爱吃胡萝卜的松鼠奇奇"
- **世界观**: 故事发生的背景(魔法森林、未来城市、海底世界)
- **成就系统**: 孩子获得的虚拟奖励(勇气勋章、小小探险家)
#### 层级 3: 工作记忆 (Working Memory)
*性质:短期、自动衰减、隐性*
- **关键情节**: 最近 3 个故事的结局和核心冲突
- **情感标记**: 孩子对特定内容的反应(根据"重播"、"跳过"推断)
- **新学词汇**: 故事中出现的高级词汇
### 1.1 孩子档案系统 (Child Profile)
| 字段 | 类型 | 说明 |
@@ -244,7 +279,13 @@ CREATE TABLE memory_items (
请创作一个适合{age}岁儿童的故事,约{word_count}字。
```
### 5.2 成就提取 Prompt
### 5.2 智能开场白 (Memory Injection)
在生成新故事时Prompt 必须包含一段"记忆唤醒"指令:
- **示例**: "小明,还记得上周我们帮小松鼠找回了松果吗?今天,小松鼠带来了一位新朋友..."
- **策略**: 提取权重最高的 Top 3 记忆注入 Prompt
### 5.3 成就提取 Prompt
```
请分析以下故事,提取主角获得的成长/成就:
@@ -412,7 +453,43 @@ def check_push_notifications():
---
## 八、里程碑
## 八、关键功能特性
### 8.1 成长时间轴 (Growth Timeline)
一个可视化的 H5 页面或 App 模块,以时间轴形式展示里程碑:
- 🌟 **初次相遇**: 创建角色的第一天
- 📖 **阅读打卡**: 累计阅读 10/50/100 本
- 🏅 **获得成就**: 获得"诚实勋章"
- 🧠 **能力解锁**: 第一次阅读"科幻"题材
### 8.2 成就仪式感 (Achievement Ceremony)
- **触发**: 故事生成并分析后,如果获得新成就
- **表现**: 弹窗动画 + 音效 + "恭喜获得 [勇气] 徽章"
- **分享**: 允许生成带二维码的成就海报
---
## 九、记忆类型扩展
| 类型 Key | 描述 | 来源 | 过期策略 |
|---------|------|------|---------|
| `recent_story` | 最近读过的故事梗概 | 阅读事件 | 30天衰减 |
| `favorite_character` | 孩子喜欢的角色 | 重播/高评分 | 长期有效 |
| `scary_element` | 孩子害怕/不喜欢的元素 | 跳过/负反馈 | 长期有效 (避雷) |
| `vocabulary_growth` | 新掌握的词汇 | 故事分析 | 90天衰减 |
| `emotional_highlight` | 高光时刻 (如: 特别开心的情节) | 互动数据 | 60天衰减 |
---
## 十、里程碑
### Phase 1: 基础建设 (v0.3.0)
- [x] 数据库 `MemoryItem` 表 (已存在)
- [ ] 扩展 `MemoryItem` 类型字段,支持更多维度
- [ ] 优化 `_build_memory_context`,支持更自然的 Prompt 注入
- [ ] 前端:简单的"近期回忆"展示列表
### M1: 孩子档案基础
- [ ] 数据库模型
@@ -436,9 +513,18 @@ def check_push_notifications():
- [ ] 偏好学习算法
- [ ] 推荐优化
### Phase 2: 可视化与成就 (v0.4.0)
- [ ] 实现"成就提取器" (Achievement Extractor) 的闭环通知
- [ ] 前端:开发"我的成就"和"成长时间轴"页面
- [ ] 增加故事开场白的动态生成逻辑
### Phase 3: 深度智能 (v0.5.0+)
- [ ] 引入向量数据库,实现基于语义的记忆检索 (不仅是时间最近)
- [ ] 情感分析模型:分析用户行为推断情感倾向
---
## 、风险与应对
## 十一、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
@@ -448,7 +534,7 @@ def check_push_notifications():
---
## 十、相关文档
## 十、相关文档
- [孩子档案数据模型](./CHILD-PROFILE-MODEL.md)
- [故事宇宙记忆结构](./STORY-UNIVERSE-MODEL.md)

189
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,189 @@
# .github/workflows/build.yml
# 构建并推送 Docker 镜像到 GitHub Container Registry
#
# 触发条件:
# - push 到 main 分支
# - 手动触发 (workflow_dispatch)
# - 创建版本标签 (v*)
#
# 镜像命名:
# ghcr.io/<owner>/dreamweaver-backend:latest
# ghcr.io/<owner>/dreamweaver-frontend:latest
# ghcr.io/<owner>/dreamweaver-admin-frontend:latest
name: Build and Push Docker Images
on:
push:
branches: [main]
tags: ['v*']
paths:
- 'backend/**'
- 'frontend/**'
- 'admin-frontend/**'
- '.github/workflows/build.yml'
workflow_dispatch:
inputs:
force_build:
description: 'Force rebuild all images'
required: false
default: 'false'
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository_owner }}/dreamweaver
jobs:
# ==============================================
# 检测变更的目录
# ==============================================
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
admin-frontend: ${{ steps.filter.outputs.admin-frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
admin-frontend:
- 'admin-frontend/**'
# ==============================================
# 构建后端镜像
# ==============================================
build-backend:
needs: changes
if: needs.changes.outputs.backend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==============================================
# 构建前端镜像
# ==============================================
build-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-frontend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==============================================
# 构建管理后台前端镜像
# ==============================================
build-admin-frontend:
needs: changes
if: needs.changes.outputs.admin-frontend == 'true' || github.event.inputs.force_build == 'true' || startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-admin-frontend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./admin-frontend
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

118
README.md
View File

@@ -51,6 +51,124 @@ npm run dev
- 后端 APIhttp://localhost:8000
- Swagger 文档http://localhost:8000/docs
## Docker Compose 使用说明
本项目包含 3 个 compose 文件:
- `docker-compose.yml`:开发基线,包含本地构建(`build`)配置,适合日常开发调试。
- `docker-compose.prod.yml`:生产基线,使用预构建镜像(不本地构建),适合部署环境。
- `docker-compose.ha.yml`HA 覆盖层,提供 PostgreSQL 主从、Redis 主从 + Sentinel、备份任务。
### 使用选择
- 本地开发:使用 `docker-compose.yml`
- 生产部署:使用 `docker-compose.prod.yml`
- 需要高可用:在上面任一基线上叠加 `docker-compose.ha.yml`
> 注意:`docker-compose.ha.yml` 是覆盖文件,不能单独使用。
### 常用命令
#### 开发模式(单机)
```bash
docker compose -f docker-compose.yml up -d
```
#### 开发 + HA主从/哨兵演练)
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
```
#### 生产模式(预构建镜像)
```bash
docker compose -f docker-compose.prod.yml up -d
```
#### 生产 + HA
```bash
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml up -d
```
#### 查看状态 / 日志
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml ps
docker compose -f docker-compose.yml -f docker-compose.ha.yml logs -f backend
```
#### 停止并清理(含卷)
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml down -v
```
### `docker-compose.prod.yml` 镜像标签
`docker-compose.prod.yml` 使用以下镜像格式:
- `${REGISTRY:-}dreamweaver-backend:${TAG:-latest}`
- `${REGISTRY:-}dreamweaver-frontend:${TAG:-latest}`
- `${REGISTRY:-}dreamweaver-admin-frontend:${TAG:-latest}`
Linux 部署示例(推荐):
```bash
export REGISTRY=my-registry.example.com/
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml up -d
```
Windows PowerShell 示例:
```powershell
$env:REGISTRY="my-registry.example.com/"
$env:TAG="2026.02.12"
docker compose -f docker-compose.prod.yml up -d
```
### Linux 服务器部署流程(推荐)
以下流程适用于 Ubuntu/CentOS 等 Linux 服务器:
#### 1) 准备配置
```bash
cp backend/.env.example backend/.env
# 编辑 backend/.env至少配置 SECRET_KEY、OAuth/API Key 等
```
#### 2) 启动(非 HA
```bash
export REGISTRY=my-registry.example.com/
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
#### 3) 启动HA
```bash
export REGISTRY=my-registry.example.com/
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml pull
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml up -d
```
#### 4) 运行状态检查
```bash
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f backend
```
HA 场景使用:
```bash
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml ps
docker compose -f docker-compose.prod.yml -f docker-compose.ha.yml logs -f backend
```
#### 5) 版本升级
```bash
export TAG=2026.02.13
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
HA 场景同理,在命令中额外叠加 `-f docker-compose.ha.yml`
#### 6) 版本回滚
```bash
export TAG=2026.02.12
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```
## 供应商路由与管理后台
- 路由按配置顺序尝试:`TEXT_PROVIDERS`(默认 `text_primary`)、`IMAGE_PROVIDERS`(默认 `image_primary`)、`TTS_PROVIDERS`(默认 `tts_primary`)。失败会自动切换下一个。
- 管理后台(默认关闭):`ENABLE_ADMIN_CONSOLE=true` 时启用,接口在 `/admin/providers`CRUD`/admin/providers/reload`。鉴权使用 Basic Auth账号密码由 `ADMIN_USERNAME`/`ADMIN_PASSWORD` 设置(请覆盖默认值)。

View File

@@ -5,11 +5,33 @@ from celery.schedules import crontab
from app.core.config import settings
celery_app = Celery(
"dreamweaver",
broker=settings.celery_broker_url,
backend=settings.celery_result_backend,
)
if settings.redis_sentinel_enabled and settings.redis_sentinel_urls:
sentinel_broker = ";".join(settings.redis_sentinel_urls)
celery_app = Celery(
"dreamweaver",
broker=sentinel_broker,
backend=sentinel_broker,
)
celery_app.conf.broker_transport_options = {
"master_name": settings.redis_sentinel_master_name,
"sentinel_kwargs": {
"password": settings.redis_sentinel_password or None,
"socket_timeout": settings.redis_sentinel_socket_timeout,
},
}
celery_app.conf.result_backend_transport_options = {
"master_name": settings.redis_sentinel_master_name,
"sentinel_kwargs": {
"password": settings.redis_sentinel_password or None,
"socket_timeout": settings.redis_sentinel_socket_timeout,
},
}
else:
celery_app = Celery(
"dreamweaver",
broker=settings.celery_broker_url,
backend=settings.celery_result_backend,
)
celery_app.conf.update(
task_track_started=True,

View File

@@ -55,6 +55,18 @@ class Settings(BaseSettings):
# Generic Redis
redis_url: str = Field("redis://localhost:6379/0", description="Redis connection URL")
redis_sentinel_enabled: bool = Field(False, description="Whether to enable Redis Sentinel")
redis_sentinel_nodes: str = Field(
"",
description="Comma-separated Redis Sentinel nodes, e.g. host1:26379,host2:26379",
)
redis_sentinel_master_name: str = Field("mymaster", description="Redis Sentinel master name")
redis_sentinel_password: str = Field("", description="Password for Redis Sentinel (optional)")
redis_sentinel_db: int = Field(0, description="Redis DB index when using Sentinel")
redis_sentinel_socket_timeout: float = Field(
0.5,
description="Socket timeout in seconds for Sentinel clients",
)
# Admin console
enable_admin_console: bool = False
@@ -71,9 +83,43 @@ class Settings(BaseSettings):
missing.append("SECRET_KEY")
if not self.database_url:
missing.append("DATABASE_URL")
if self.redis_sentinel_enabled and not self.redis_sentinel_nodes.strip():
missing.append("REDIS_SENTINEL_NODES")
if missing:
raise ValueError(f"Missing required settings: {', '.join(missing)}")
return self
@property
def redis_sentinel_hosts(self) -> list[tuple[str, int]]:
"""Parse Redis Sentinel nodes into (host, port) tuples."""
nodes = []
raw = self.redis_sentinel_nodes.strip()
if not raw:
return nodes
for item in raw.split(","):
value = item.strip()
if not value:
continue
if ":" not in value:
raise ValueError(f"Invalid sentinel node format: {value}")
host, port_text = value.rsplit(":", 1)
if not host:
raise ValueError(f"Invalid sentinel node host: {value}")
try:
port = int(port_text)
except ValueError as exc:
raise ValueError(f"Invalid sentinel node port: {value}") from exc
nodes.append((host, port))
return nodes
@property
def redis_sentinel_urls(self) -> list[str]:
"""Build Celery-compatible Sentinel URLs with DB index."""
return [
f"sentinel://{host}:{port}/{self.redis_sentinel_db}"
for host, port in self.redis_sentinel_hosts
]
settings = Settings()

View File

@@ -1,25 +1,46 @@
"""Redis client module."""
from typing import AsyncGenerator
from redis.asyncio import Redis, from_url
from redis.asyncio.sentinel import Sentinel
from app.core.config import settings
from app.core.logging import get_logger
_redis_pool: Redis | None = None
_sentinel_pool: Sentinel | None = None
logger = get_logger(__name__)
async def get_redis() -> Redis:
"""Get global Redis client instance."""
global _redis_pool
global _redis_pool, _sentinel_pool
if _redis_pool is None:
_redis_pool = from_url(settings.redis_url, encoding="utf-8", decode_responses=True)
if settings.redis_sentinel_enabled:
_sentinel_pool = Sentinel(
settings.redis_sentinel_hosts,
socket_timeout=settings.redis_sentinel_socket_timeout,
password=settings.redis_sentinel_password or None,
decode_responses=True,
)
_redis_pool = _sentinel_pool.master_for(
settings.redis_sentinel_master_name,
db=settings.redis_sentinel_db,
decode_responses=True,
)
logger.info(
"redis_connected_via_sentinel",
master_name=settings.redis_sentinel_master_name,
sentinel_nodes=settings.redis_sentinel_nodes,
)
else:
_redis_pool = from_url(settings.redis_url, encoding="utf-8", decode_responses=True)
return _redis_pool
async def close_redis():
"""Close Redis connection."""
global _redis_pool
global _redis_pool, _sentinel_pool
if _redis_pool:
await _redis_pool.close()
_redis_pool = None
_sentinel_pool = None

View File

@@ -0,0 +1,89 @@
# HA 部署与验证 RunbookPhase 3 MVP
本文档对应 `docker-compose.ha.yml`,用于本地/测试环境验证高可用基础能力。
## 1. 启动方式
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
```
说明:
- 基础业务服务仍来自 `docker-compose.yml`
- `docker-compose.ha.yml` 覆盖了 `db``redis`,并新增 `db-replica``postgres-backup``redis-replica``redis-sentinel-*`
## 2. 核心环境变量建议
`backend/.env`(或 shell 环境)中至少配置:
```env
# PostgreSQL
POSTGRES_USER=dreamweaver
POSTGRES_PASSWORD=dreamweaver_password
POSTGRES_DB=dreamweaver_db
POSTGRES_REPMGR_PASSWORD=repmgr_password
# Redis Sentinel
REDIS_SENTINEL_ENABLED=true
REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
REDIS_SENTINEL_MASTER_NAME=mymaster
REDIS_SENTINEL_DB=0
REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
# 可选:若 Sentinel/Redis 设置了密码
REDIS_SENTINEL_PASSWORD=
# 备份周期,默认 86400 秒1 天)
BACKUP_INTERVAL_SECONDS=86400
```
## 3. 健康检查
### 3.1 PostgreSQL 主从
```bash
docker compose -f docker-compose.yml -f docker-compose.ha.yml ps
docker exec -it dreamweaver_db_primary psql -U dreamweaver -d dreamweaver_db -c "select now();"
docker exec -it dreamweaver_db_replica psql -U dreamweaver -d dreamweaver_db -c "select pg_is_in_recovery();"
```
期望:
- 主库可读写;
- 从库 `pg_is_in_recovery()` 返回 `t`
### 3.2 Redis Sentinel
```bash
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel masters
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel replicas mymaster
```
期望:
- `mymaster` 存在;
- 至少 1 个 replica 被发现。
### 3.3 备份任务
```bash
docker exec -it dreamweaver_postgres_backup sh -c "ls -lh /backups"
```
期望:
- `/backups` 下出现 `.dump` 文件;
- 旧于 7 天的备份会被自动清理。
## 4. 故障切换演练(最小)
```bash
# 模拟 Redis 主节点故障
docker stop dreamweaver_redis_master
# 等待 Sentinel 选主后查看
docker exec -it dreamweaver_redis_sentinel_1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
```
提示:应用与 Celery 已支持 Sentinel 配置。若未启用 Sentinel仍可回退到 `REDIS_URL` / `CELERY_BROKER_URL` / `CELERY_RESULT_BACKEND` 直连模式。
## 5. 当前已知限制(下一步)
- PostgreSQL 侧当前仅完成主从拓扑读写分离PgBouncer/路由)待后续迭代。

View File

@@ -19,25 +19,25 @@
目前 `backend`, `backend-admin`, `worker`, `celery-beat` 重复构建 4 次,浪费资源且镜像版本可能不一致。
- **Action Items**:
- [ ] 修改 `backend/Dockerfile` 为通用基础镜像。
- [ ] 更新 `docker-compose.yml`,定义 `backend-base` 服务或使用 `image` 标签共享镜像。
- [ ] 确保所有 Python 服务共用同一构建产物,仅启动命令不同。
- [x] 修改 `backend/Dockerfile` 为通用基础镜像。
- [x] 更新 `docker-compose.yml`,定义 `backend-base` 服务或使用 `image` 标签共享镜像。
- [x] 确保所有 Python 服务共用同一构建产物,仅启动命令不同。
### 2.2 修复 Provider 缓存与限流 (High Priority)
内存缓存 (`TTLCache`, `_latency_cache`) 在多进程/多实例下失效。
- **Action Items**:
- [ ] 引入 Redis 作为共享缓存后端。
- [ ] 重构 `_load_provider_cache`,将 Provider 配置缓存至 Redis。
- [ ] 重构 `stories.py` 中的限流逻辑,使用 `redis-cell` 或简单的 Redis 计数器替代 `TTLCache`
- [x] 引入 Redis 作为共享缓存后端。
- [x] 重构 `_load_provider_cache`,将 Provider 配置缓存至 Redis。
- [x] 重构 `stories.py` 中的限流逻辑,使用 `redis-cell` 或简单的 Redis 计数器替代 `TTLCache`
### 2.3 拆分 `stories.py` (Medium Priority)
`app/api/stories.py` 超过 600 行,包含 API 定义、业务逻辑、验证逻辑,维护困难。
- **Action Items**:
- [ ] 创建 `app/services/story_service.py`迁移生成、润色、PDF生成等核心逻辑。
- [ ] 创建 `app/schemas/story_schema.py`,迁移 Pydantic 模型(`GenerateRequest`, `StoryResponse` 等)。
- [ ] API 层 `stories.py` 仅保留路由定义和依赖注入,调用 Service 层。
- [x] 创建 `app/services/story_service.py`迁移生成、润色、PDF生成等核心逻辑。
- [x] 创建 `app/schemas/story_schemas.py`,迁移 Pydantic 模型(`GenerateRequest`, `StoryResponse` 等)。
- [x] API 层 `stories.py` 仅保留路由定义和依赖注入,调用 Service 层。
---
@@ -68,6 +68,17 @@ Redis 单点故障将导致 Celery 任务全盘停摆。
- [ ] 部署 Grafana + Prometheus监控 API 延迟、QPS、Celery 队列积压情况。
- [ ] 完善 `ProviderMetrics`,增加可视化大盘,实时监控 AI 供应商的成本与成功率。
### 3.4 Phase 3 最小可执行任务清单 (MVP)
目标:在不大改业务代码的前提下,于一个迭代内完成高可用基础设施闭环。
- [x] PostgreSQL 主从:新增 `docker-compose.ha.yml`,包含 1 主 1 从与健康检查。
- [x] PostgreSQL 备份:新增每日备份任务(`pg_dump`)与 7 天保留策略。
- [x] Redis Sentinel新增 1 主 2 哨兵最小拓扑,并验证故障切换。
- [x] Celery 连接:更新 Celery broker/result backend 配置,支持 Sentinel 连接串。
- [x] 回归验证:执行一次故事生成 + 异步任务链路worker/beat冒烟测试。
- [x] 运行手册补充故障切换与恢复步骤文档PostgreSQL/Redis/Celery
---
## 4. 长期架构演进 (季度规划)

View File

@@ -20,6 +20,7 @@ dependencies = [
"sse-starlette>=2.0.0",
"celery>=5.4.0",
"redis>=5.0.0",
"edge-tts>=6.1.0",
"openai>=1.0.0",
]

310
docker-compose.ha.yml Normal file
View File

@@ -0,0 +1,310 @@
# docker-compose.ha.yml
# HA 覆盖配置(建议与 docker-compose.yml 叠加使用)
#
# 启动示例:
# docker compose -f docker-compose.yml -f docker-compose.ha.yml up -d
services:
# ==============================================
# 应用服务 Sentinel 配置覆盖
# ==============================================
backend:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
backend-admin:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
worker:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
celery-beat:
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- REDIS_SENTINEL_ENABLED=true
- REDIS_SENTINEL_NODES=redis-sentinel-1:26379,redis-sentinel-2:26379,redis-sentinel-3:26379
- REDIS_SENTINEL_MASTER_NAME=mymaster
- REDIS_SENTINEL_DB=0
- REDIS_SENTINEL_SOCKET_TIMEOUT=0.5
# ==============================================
# PostgreSQL 主库(覆盖默认 db
# ==============================================
db:
image: postgres:15-alpine
container_name: dreamweaver_db_primary
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
command:
- postgres
- -c
- wal_level=replica
- -c
- max_wal_senders=10
- -c
- max_replication_slots=10
- -c
- hot_standby=on
- -c
- hba_file=/etc/postgresql/pg_hba.conf
ports:
- "52432:5432"
volumes:
- postgres_primary_data:/var/lib/postgresql/data
- ./ops/postgres-ha/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db}"]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# PostgreSQL 从库(基于 pg_basebackup 初始化)
# ==============================================
db-replica:
image: postgres:15-alpine
container_name: dreamweaver_db_replica
restart: unless-stopped
user: "postgres"
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
PGDATA: /var/lib/postgresql/data
depends_on:
db:
condition: service_healthy
volumes:
- postgres_replica_data:/var/lib/postgresql/data
command:
- /bin/sh
- -ec
- |
if [ ! -s "$$PGDATA/PG_VERSION" ]; then
echo "Initializing replica from primary..."
until pg_isready -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"; do sleep 2; done
export PGPASSWORD="$$POSTGRES_PASSWORD"
rm -rf "$$PGDATA"/*
pg_basebackup -h db -D "$$PGDATA" -U "$$POSTGRES_USER" -Fp -Xs -P -R
fi
chmod 700 "$$PGDATA"
exec postgres -c hot_standby=on
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db} && psql -U ${POSTGRES_USER:-dreamweaver} -d ${POSTGRES_DB:-dreamweaver_db} -tAc 'select pg_is_in_recovery();' | grep -q t",
]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# PostgreSQL 备份任务(每日一次,保留 7 天)
# ==============================================
postgres-backup:
image: postgres:15-alpine
container_name: dreamweaver_postgres_backup
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
PGPASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
BACKUP_INTERVAL_SECONDS: ${BACKUP_INTERVAL_SECONDS:-86400}
depends_on:
db:
condition: service_healthy
volumes:
- postgres_backups:/backups
command:
- /bin/sh
- -ec
- |
while true; do
ts=$$(date +%Y%m%d_%H%M%S);
pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -F c -f "/backups/dreamweaver_$${ts}.dump";
find /backups -type f -name '*.dump' -mtime +7 -delete;
sleep "$$BACKUP_INTERVAL_SECONDS";
done
# ==============================================
# Redis 主库(覆盖默认 redis
# ==============================================
redis:
image: redis:7-alpine
container_name: dreamweaver_redis_master
restart: unless-stopped
ports:
- "52379:6379"
volumes:
- redis_master_data:/data
command: ["redis-server", "--appendonly", "yes", "--protected-mode", "no"]
networks:
default:
ipv4_address: 172.29.0.10
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# Redis 从库
# ==============================================
redis-replica:
image: redis:7-alpine
container_name: dreamweaver_redis_replica
restart: unless-stopped
depends_on:
redis:
condition: service_healthy
volumes:
- redis_replica_data:/data
command:
[
"redis-server",
"--appendonly",
"yes",
"--protected-mode",
"no",
"--replicaof",
"172.29.0.10",
"6379",
]
networks:
default:
ipv4_address: 172.29.0.11
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
# ==============================================
# Redis Sentinel (3 节点)
# ==============================================
redis-sentinel-1:
image: redis:7-alpine
container_name: dreamweaver_redis_sentinel_1
restart: unless-stopped
ports:
- "52631:26379"
depends_on:
redis:
condition: service_healthy
redis-replica:
condition: service_healthy
networks:
default:
ipv4_address: 172.29.0.21
command:
- /bin/sh
- -ec
- |
cat > /tmp/sentinel.conf <<EOF
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor mymaster 172.29.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
protected-mode no
dir /tmp
EOF
exec redis-sentinel /tmp/sentinel.conf
redis-sentinel-2:
image: redis:7-alpine
container_name: dreamweaver_redis_sentinel_2
restart: unless-stopped
ports:
- "52632:26379"
depends_on:
redis:
condition: service_healthy
redis-replica:
condition: service_healthy
networks:
default:
ipv4_address: 172.29.0.22
command:
- /bin/sh
- -ec
- |
cat > /tmp/sentinel.conf <<EOF
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor mymaster 172.29.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
protected-mode no
dir /tmp
EOF
exec redis-sentinel /tmp/sentinel.conf
redis-sentinel-3:
image: redis:7-alpine
container_name: dreamweaver_redis_sentinel_3
restart: unless-stopped
ports:
- "52633:26379"
depends_on:
redis:
condition: service_healthy
redis-replica:
condition: service_healthy
networks:
default:
ipv4_address: 172.29.0.23
command:
- /bin/sh
- -ec
- |
cat > /tmp/sentinel.conf <<EOF
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor mymaster 172.29.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
protected-mode no
dir /tmp
EOF
exec redis-sentinel /tmp/sentinel.conf
volumes:
postgres_primary_data:
postgres_replica_data:
postgres_backups:
redis_master_data:
redis_replica_data:
networks:
default:
ipam:
config:
- subnet: 172.29.0.0/24

158
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,158 @@
# docker-compose.prod.yml
# 生产部署配置 - 使用预构建镜像,不包含 build 指令
# 镜像通过 GitHub Actions 或本地 docker build 预先构建
#
# 使用方式:
# docker compose -f docker-compose.prod.yml up -d
#
# 镜像构建 (手动):
# docker build -t dreamweaver-backend:latest ./backend
# docker build -t dreamweaver-frontend:latest ./frontend
# docker build -t dreamweaver-admin-frontend:latest ./admin-frontend
services:
# ==============================================
# 前端服务 (C端用户 App)
# ==============================================
frontend:
image: ${REGISTRY:-}dreamweaver-frontend:${TAG:-latest}
container_name: dreamweaver_frontend
restart: always
ports:
- "52080:80"
depends_on:
- backend
# ==============================================
# 管理后台前端 (Admin Console)
# ==============================================
frontend-admin:
image: ${REGISTRY:-}dreamweaver-admin-frontend:${TAG:-latest}
container_name: dreamweaver_frontend_admin
restart: always
ports:
- "52888:80"
depends_on:
- backend-admin
# ==============================================
# 后端服务 (FastAPI)
# ==============================================
backend:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_backend
restart: always
ports:
- "52000:8000"
env_file:
- ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
volumes:
- backend_static:/app/static
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# ==============================================
# 管理后台后端 (Admin Backend)
# ==============================================
backend-admin:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_backend_admin
restart: always
ports:
- "52800:8001"
command: ["uvicorn", "app.admin_main:app", "--host", "0.0.0.0", "--port", "8001"]
env_file:
- ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
volumes:
- backend_static:/app/static
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
# ==============================================
# 工作节点 (Celery Worker)
# ==============================================
worker:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_worker
command: celery -A app.core.celery_app worker --loglevel=info
restart: always
env_file: ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
redis:
condition: service_started
db:
condition: service_healthy
# ==============================================
# 调度节点 (Celery Beat)
# ==============================================
celery-beat:
image: ${REGISTRY:-}dreamweaver-backend:${TAG:-latest}
container_name: dreamweaver_beat
command: celery -A app.core.celery_app beat --loglevel=info
restart: always
env_file: ./backend/.env
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-dreamweaver}:${POSTGRES_PASSWORD:-dreamweaver_password}@db:5432/${POSTGRES_DB:-dreamweaver_db}
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
redis:
condition: service_started
# ==============================================
# 数据库 (PostgreSQL)
# ==============================================
db:
image: postgres:15-alpine
container_name: dreamweaver_db
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER:-dreamweaver}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dreamweaver_password}
POSTGRES_DB: ${POSTGRES_DB:-dreamweaver_db}
ports:
- "52432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dreamweaver}"]
interval: 10s
timeout: 5s
retries: 5
# ==============================================
# 缓存 (Redis)
# ==============================================
redis:
image: redis:7-alpine
container_name: dreamweaver_redis
restart: always
ports:
- "52379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
postgres_data:
redis_data:
backend_static:

216
install.cmd Normal file
View File

@@ -0,0 +1,216 @@
@echo off
setlocal enabledelayedexpansion
REM Claude Code Windows CMD Bootstrap Script
REM Installs Claude Code for environments where PowerShell is not available
REM Parse command line argument
set "TARGET=%~1"
if "!TARGET!"=="" set "TARGET=latest"
REM Validate target parameter
if /i "!TARGET!"=="stable" goto :target_valid
if /i "!TARGET!"=="latest" goto :target_valid
echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul
if !ERRORLEVEL! equ 0 goto :target_valid
echo Usage: %0 [stable^|latest^|VERSION] >&2
echo Example: %0 1.0.58 >&2
exit /b 1
:target_valid
REM Check for 64-bit Windows
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid
if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid
echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2
exit /b 1
:arch_valid
REM Set constants
set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads"
set "PLATFORM=win32-x64"
REM Create download directory
if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!"
REM Check for curl availability
curl --version >nul 2>&1
if !ERRORLEVEL! neq 0 (
echo curl is required but not available. Please install curl or use PowerShell installer. >&2
exit /b 1
)
REM Always download latest version (which has the most up-to-date installer)
call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest"
if !ERRORLEVEL! neq 0 (
echo Failed to get latest version >&2
exit /b 1
)
REM Read version from file
set /p VERSION=<"!DOWNLOAD_DIR!\latest"
del "!DOWNLOAD_DIR!\latest"
REM Download manifest
call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json"
if !ERRORLEVEL! neq 0 (
echo Failed to get manifest >&2
exit /b 1
)
REM Extract checksum from manifest
call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!"
if !ERRORLEVEL! neq 0 (
echo Platform !PLATFORM! not found in manifest >&2
del "!DOWNLOAD_DIR!\manifest.json" 2>nul
exit /b 1
)
del "!DOWNLOAD_DIR!\manifest.json"
REM Download binary
set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe"
call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!"
if !ERRORLEVEL! neq 0 (
echo Failed to download binary >&2
if exist "!BINARY_PATH!" del "!BINARY_PATH!"
exit /b 1
)
REM Verify checksum
call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!"
if !ERRORLEVEL! neq 0 (
echo Checksum verification failed >&2
del "!BINARY_PATH!"
exit /b 1
)
REM Run claude install to set up launcher and shell integration
echo Setting up Claude Code...
"!BINARY_PATH!" install "!TARGET!"
set "INSTALL_RESULT=!ERRORLEVEL!"
REM Clean up downloaded file
REM Wait a moment for any file handles to be released
timeout /t 1 /nobreak >nul 2>&1
del /f "!BINARY_PATH!" >nul 2>&1
if exist "!BINARY_PATH!" (
echo Warning: Could not remove temporary file: !BINARY_PATH!
)
if !INSTALL_RESULT! neq 0 (
echo Installation failed >&2
exit /b 1
)
echo.
echo Installation complete^^!
echo.
exit /b 0
REM ============================================================================
REM SUBROUTINES
REM ============================================================================
:download_file
REM Downloads a file using curl
REM Args: %1=URL, %2=OutputPath
set "URL=%~1"
set "OUTPUT=%~2"
curl -fsSL "!URL!" -o "!OUTPUT!"
exit /b !ERRORLEVEL!
:parse_manifest
REM Parse JSON manifest to extract checksum for platform
REM Args: %1=ManifestPath, %2=Platform
set "MANIFEST_PATH=%~1"
set "PLATFORM_NAME=%~2"
set "EXPECTED_CHECKSUM="
REM Use findstr to find platform section, then look for checksum
set "FOUND_PLATFORM="
set "IN_PLATFORM_SECTION="
REM Read the manifest line by line
for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do (
set "LINE=%%i"
REM Check if this line contains our platform
echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul
if !ERRORLEVEL! equ 0 (
set "IN_PLATFORM_SECTION=1"
)
REM If we're in the platform section, look for checksum
if defined IN_PLATFORM_SECTION (
echo !LINE! | findstr /c:"\"checksum\":" >nul
if !ERRORLEVEL! equ 0 (
REM Extract checksum value
for /f "tokens=2 delims=:" %%j in ("!LINE!") do (
set "CHECKSUM_PART=%%j"
REM Remove quotes, whitespace, and comma
set "CHECKSUM_PART=!CHECKSUM_PART: =!"
set "CHECKSUM_PART=!CHECKSUM_PART:"=!"
set "CHECKSUM_PART=!CHECKSUM_PART:,=!"
REM Check if it looks like a SHA256 (64 hex chars)
if not "!CHECKSUM_PART!"=="" (
call :check_length "!CHECKSUM_PART!" 64
if !ERRORLEVEL! equ 0 (
set "EXPECTED_CHECKSUM=!CHECKSUM_PART!"
exit /b 0
)
)
)
)
REM Check if we've left the platform section (closing brace)
echo !LINE! | findstr /c:"}" >nul
if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION="
)
)
if "!EXPECTED_CHECKSUM!"=="" exit /b 1
exit /b 0
:check_length
REM Check if string length equals expected length
REM Args: %1=String, %2=ExpectedLength
set "STR=%~1"
set "EXPECTED_LEN=%~2"
set "LEN=0"
:count_loop
if "!STR:~%LEN%,1!"=="" goto :count_done
set /a LEN+=1
goto :count_loop
:count_done
if %LEN%==%EXPECTED_LEN% exit /b 0
exit /b 1
:verify_checksum
REM Verify file checksum using certutil
REM Args: %1=FilePath, %2=ExpectedChecksum
set "FILE_PATH=%~1"
set "EXPECTED=%~2"
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do (
set "ACTUAL=%%i"
set "ACTUAL=!ACTUAL: =!"
if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done
if "!ACTUAL!" neq "" (
if /i "!ACTUAL!"=="!EXPECTED!" (
exit /b 0
) else (
exit /b 1
)
)
)
:verify_done
exit /b 1

View File

@@ -0,0 +1,10 @@
# Allow local socket access
local all all trust
# Allow all IPv4/IPv6 client access in local docker network
host all all 0.0.0.0/0 trust
host all all ::/0 trust
# Allow streaming replication connections
host replication all 0.0.0.0/0 trust
host replication all ::/0 trust