Compare commits

...

16 Commits

Author SHA1 Message Date
jeremygan2021
e515395b55 action
All checks were successful
Deploy to Server / deploy (push) Successful in 4m27s
2026-03-02 15:37:39 +08:00
jeremygan2021
59f174322a action
All checks were successful
Deploy to Server / deploy (push) Successful in 1s
2026-03-02 15:33:04 +08:00
jeremygan2021
11e4de9071 action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 15:32:20 +08:00
jeremygan2021
75cfb27bb1 action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 15:30:46 +08:00
jeremygan2021
f91fabad0c action
Some checks failed
Deploy to Server / deploy (push) Failing after 2s
2026-03-02 15:29:38 +08:00
jeremygan2021
ae2da39496 action
Some checks failed
Deploy to Server / deploy (push) Failing after 2s
2026-03-02 15:26:43 +08:00
jeremygan2021
bb814061e7 action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 15:25:42 +08:00
jeremygan2021
d74eb795c3 action
Some checks failed
Deploy to Server / deploy (push) Failing after 3s
2026-03-02 14:27:01 +08:00
jeremygan2021
8b8e1d51ce action
Some checks failed
Deploy to Server / deploy (push) Failing after 8s
2026-03-02 13:14:19 +08:00
jeremygan2021
4a36952484 action
Some checks failed
Deploy to Server / deploy (push) Failing after 0s
2026-03-02 13:11:22 +08:00
jeremygan2021
8b672d026d action
Some checks failed
Deploy to Server / deploy (push) Failing after 0s
2026-03-02 13:10:36 +08:00
jeremygan2021
841bf23d4d action
Some checks failed
Deploy to Server / deploy (push) Failing after 6s
2026-03-02 13:09:17 +08:00
jeremygan2021
373ce8cb2e action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 13:07:34 +08:00
jeremygan2021
a1e8c042ca action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 13:01:04 +08:00
jeremygan2021
0d140cd75c action 2026-03-02 12:59:46 +08:00
jeremygan2021
9620a4138d AI生图 2026-03-02 12:53:36 +08:00
19 changed files with 283 additions and 37 deletions

4
.env
View File

@@ -1,10 +1,10 @@
# 环境变量配置文件 # 环境变量配置文件
# 数据库配置 # 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna # DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6433/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
# MQTT配置 # MQTT配置
MQTT_BROKER_HOST=luna-mqtt MQTT_BROKER_HOST=luna-mqtt

View File

@@ -1,7 +1,7 @@
# 环境变量配置文件 # 环境变量配置文件
# 数据库配置 # 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna # DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6433/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置 # MQTT配置
MQTT_BROKER_HOST=luna-mqtt MQTT_BROKER_HOST=luna-mqtt
@@ -9,7 +9,7 @@ MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025 MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021 MQTT_PASSWORD=123luna2021
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
# 应用配置 # 应用配置
APP_NAME=墨水屏桌面屏幕系统 APP_NAME=墨水屏桌面屏幕系统

View File

@@ -1,7 +1,7 @@
# 环境变量配置文件 # 环境变量配置文件
# 数据库配置 # 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna # DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6433/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置 # MQTT配置
MQTT_BROKER_HOST=luna-mqtt MQTT_BROKER_HOST=luna-mqtt
@@ -9,7 +9,7 @@ MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025 MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021 MQTT_PASSWORD=123luna2021
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd*******

View File

@@ -0,0 +1,147 @@
name: Deploy to Server
on:
push:
branches:
- main
- master
jobs:
deploy:
runs-on: ubuntu
steps:
# 直接使用 expect 脚本处理交互,比 sshpass 更稳定,尤其是在 Alpine 上
- name: Install dependencies
run: |
if command -v apt-get &> /dev/null; then
apt-get update
apt-get install -y expect openssh-client
elif command -v apk &> /dev/null; then
apk update
apk add expect openssh-client bash
else
echo "Unknown package manager"
exit 1
fi
- name: Deploy to server
env:
HOST: "6.6.6.66"
USER: "quant"
PASS: "123quant-speed"
TARGET_DIR: "/home/quant-speed/data/dev/ESP32_GDEY042T81_server"
REPO_URL: "https://gitea.tangledup-ai.com/Tangledup-ai/ESP32_GDEY042T81_server.git"
run: |
# 创建一个 shell 脚本,包含所有需要在服务器上执行的逻辑
# 这样可以避免在 expect 中处理复杂的条件判断和转义
cat > remote_script.sh <<'EOS'
#!/bin/bash
# 获取密码
PASSWORD="$1"
TARGET_DIR="$2"
RUN_USER="$3"
REPO_URL="$4"
# 1. 确保目录存在 (脚本已通过 sudo 运行)
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
fi
# 2. 修正权限,确保用户拥有目录
chown -R "$RUN_USER:$RUN_USER" "$TARGET_DIR"
# 3. 进入目录
cd "$TARGET_DIR"
# 4. 执行 Git 操作 (以用户身份执行,避免 .git 权限问题)
# 使用 sudo -u 切换到普通用户执行 git
echo "Deploying code as $RUN_USER..."
FRESH_CLONE=0
if [ ! -d ".git" ]; then
echo "Not a git repository. Cloning from $REPO_URL..."
# Handle non-empty dir if necessary
if [ "$(ls -A)" ]; then
echo "Directory not empty. initializing git..."
sudo -u "$RUN_USER" git init
sudo -u "$RUN_USER" git remote add origin "$REPO_URL"
sudo -u "$RUN_USER" git fetch origin
sudo -u "$RUN_USER" git checkout -b main --track origin/main || sudo -u "$RUN_USER" git reset --hard origin/main
else
sudo -u "$RUN_USER" git clone "$REPO_URL" .
fi
FRESH_CLONE=1
fi
if [ "$FRESH_CLONE" -eq 0 ]; then
OLD_HEAD=$(sudo -u "$RUN_USER" git rev-parse HEAD 2>/dev/null || echo "")
echo "Pulling latest code..."
if ! sudo -u "$RUN_USER" git pull origin main; then
echo "Git pull failed"
exit 1
fi
NEW_HEAD=$(sudo -u "$RUN_USER" git rev-parse HEAD)
if [ "$OLD_HEAD" == "$NEW_HEAD" ]; then
echo "No changes detected, skipping deploy"
exit 0
fi
else
OLD_HEAD=""
NEW_HEAD=$(sudo -u "$RUN_USER" git rev-parse HEAD)
fi
# 5. 执行 Docker 操作 (以 root 身份执行)
# 检查构建文件变动
FORCE_REBUILD=0
if [ "$FRESH_CLONE" -eq 1 ]; then
FORCE_REBUILD=1
fi
if [ "$FORCE_REBUILD" -eq 1 ] || sudo -u "$RUN_USER" git diff --name-only $OLD_HEAD $NEW_HEAD | grep -E 'Dockerfile|requirements.txt'; then
echo "Build files changed or fresh clone, rebuilding..."
docker compose down --rmi local
docker rmi epaper_server:latest || true
docker compose up -d --build
else
echo "Only code changed, restarting container..."
docker compose down
docker compose up -d
fi
EOS
# 创建 expect 脚本,负责上传到 /tmp 并 sudo 执行
cat > deploy_script.exp <<EOF
#!/usr/bin/expect -f
set timeout 300
set host "$HOST"
set user "$USER"
set password "$PASS"
set target_dir "$TARGET_DIR"
set repo_url "$REPO_URL"
# 1. 上传脚本到 /tmp (避免目标目录权限问题)
spawn scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null remote_script.sh \$user@\$host:/tmp/luna_deploy.sh
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "\$password\r" }
}
expect eof
# 2. SSH 登录并执行 sudo bash /tmp/luna_deploy.sh
# 我们把密码传给脚本,让脚本内部决定怎么用,或者直接用 sudo 执行脚本
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t \$user@\$host "echo '\$password' | sudo -S bash /tmp/luna_deploy.sh '\$password' '\$target_dir' '\$user' '\$repo_url'"
expect {
"password:" { send "\$password\r" }
}
# 保持交互直到脚本执行完毕
expect eof
EOF
# 执行 expect 脚本
chmod +x deploy_script.exp
./deploy_script.exp

View File

@@ -1,7 +1,7 @@
使用fastAPI框架 使用fastAPI框架
python 3.12 python 3.12
- 使用pg数据库 url http://121.43.104.161:6432 - 使用pg数据库 url http://121.43.104.161:6433
- 用户名luna - 用户名luna
- 密码123luna - 密码123luna

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,8 @@
import os
import uuid
import httpx
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from typing import Dict, Any from typing import Dict, Any
import httpx
import json
import logging import logging
from config import settings from config import settings
from api.ai_schemas import AIGenerationRequest, AITaskResponse, AITaskResult, AITemplateGenerationRequest from api.ai_schemas import AIGenerationRequest, AITaskResponse, AITaskResult, AITemplateGenerationRequest
@@ -13,16 +14,45 @@ logger = logging.getLogger(__name__)
DASHSCOPE_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation" DASHSCOPE_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
DASHSCOPE_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks" DASHSCOPE_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks"
async def _download_image(url: str) -> str:
"""
下载图片并保存到media目录
:param url: 图片URL
:return: 本地文件相对路径 (e.g., "/media/xxx.png")
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=30.0)
if response.status_code != 200:
logger.error(f"Failed to download image: {url}, status: {response.status_code}")
return url # 下载失败返回原URL
# 生成文件名
filename = f"{uuid.uuid4()}.png"
filepath = os.path.join(settings.media_dir, filename)
# 写入文件
with open(filepath, "wb") as f:
f.write(response.content)
# 返回可访问的URL路径
return f"/media/{filename}"
except Exception as e:
logger.error(f"Error downloading image: {str(e)}")
return url
async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n: int, model: str): async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n: int, model: str):
if not settings.dashscope_api_key: if not settings.dashscope_api_key:
raise HTTPException(status_code=500, detail="DashScope API Key not configured") raise HTTPException(status_code=500, detail="DashScope API Key not configured")
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {settings.dashscope_api_key}", "Authorization": f"Bearer {settings.dashscope_api_key}"
"X-DashScope-Async": "enable" # 确保异步任务提交 # "X-DashScope-Async": "enable" # 移除强制异步,因为某些账号/模型不支持
} }
# 构建请求体 # 构建请求体
payload = { payload = {
"model": model, "model": model,
@@ -55,7 +85,7 @@ async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n
DASHSCOPE_API_URL, DASHSCOPE_API_URL,
headers=headers, headers=headers,
json=payload, json=payload,
timeout=30.0 timeout=60.0 # 增加超时时间,同步请求可能较慢
) )
if response.status_code != 200: if response.status_code != 200:
@@ -68,23 +98,47 @@ async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n
result = response.json() result = response.json()
# 检查是异步任务返回还是同步结果返回
task_id = None
if "output" in result and "task_id" in result["output"]: if "output" in result and "task_id" in result["output"]:
task_id = result["output"]["task_id"] task_id = result["output"]["task_id"]
elif "task_id" in result: elif "task_id" in result:
task_id = result["task_id"] task_id = result["task_id"]
else:
if "output" in result and "task_id" in result["output"]: # 如果没有task_id检查是否有直接结果同步模式
task_id = result["output"]["task_id"] if not task_id:
else: if "output" in result and "choices" in result["output"]:
# 同步返回成功
# 提取结果以便直接返回
choices = result["output"]["choices"]
results = []
for choice in choices:
msg_content = choice.get("message", {}).get("content", [])
for item in msg_content:
if "image" in item:
# 下载图片到本地
local_url = await _download_image(item["image"])
results.append({"url": local_url, "origin_url": item["image"]})
return AITaskResponse(
request_id=result.get("request_id"),
status="SUCCEEDED",
results=results
)
# 既没有task_id也没有choices可能是其他结构或错误
logger.warning(f"Unexpected response structure: {result}") logger.warning(f"Unexpected response structure: {result}")
# 尝试从output.task_id再找一次
task_id = result.get("output", {}).get("task_id") task_id = result.get("output", {}).get("task_id")
if not task_id: if not task_id:
raise HTTPException(status_code=500, detail="Failed to retrieve task_id from DashScope response") # 依然找不到,报错
raise HTTPException(status_code=500, detail="Failed to retrieve task_id or results from DashScope response")
return AITaskResponse( return AITaskResponse(
task_id=task_id, task_id=task_id,
request_id=result.get("request_id") request_id=result.get("request_id"),
status="PENDING" # 异步任务初始状态
) )
except httpx.RequestError as e: except httpx.RequestError as e:

View File

@@ -18,8 +18,11 @@ class AITemplateGenerationRequest(BaseModel):
model: str = Field("wan2.6-t2i", description="使用的模型") model: str = Field("wan2.6-t2i", description="使用的模型")
class AITaskResponse(BaseModel): class AITaskResponse(BaseModel):
task_id: str = Field(..., description="任务ID") task_id: Optional[str] = Field(None, description="任务ID (异步任务时存在)")
request_id: Optional[str] = Field(None, description="请求ID") request_id: Optional[str] = Field(None, description="请求ID")
status: Optional[str] = Field(None, description="任务状态")
results: Optional[List[Dict[str, Any]]] = Field(None, description="同步生成的直接结果")
# results结构: [{"url": "/media/xxx.png", "origin_url": "https://..."}]
class AITaskResult(BaseModel): class AITaskResult(BaseModel):
task_id: str task_id: str

View File

@@ -3,7 +3,7 @@ from typing import Optional
class Settings(BaseSettings): class Settings(BaseSettings):
# 数据库配置1 # 数据库配置1
#database_url: str = "postgresql://luna:123luna@121.43.104.161:6432/luna" #database_url: str = "postgresql://luna:123luna@121.43.104.161:6433/luna"
database_url: str = "postgresql://luna:123luna@6.6.6.66:5432/luna" database_url: str = "postgresql://luna:123luna@6.6.6.66:5432/luna"
@@ -21,6 +21,7 @@ class Settings(BaseSettings):
static_dir: str = "static" static_dir: str = "static"
upload_dir: str = "static/uploads" upload_dir: str = "static/uploads"
processed_dir: str = "static/processed" processed_dir: str = "static/processed"
media_dir: str = "media" # 新增媒体文件目录
# 墨水屏配置 # 墨水屏配置
ink_width: int = 400 ink_width: int = 400

View File

@@ -1,14 +1,16 @@
services: services:
# 主应用服务 # 主应用服务
luna-app: epaper_server:
image: epaper_server:latest
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: luna-app container_name: epaper_server
ports: ports:
- "8199:8199" - "8199:8199"
volumes: volumes:
- ./static:/app/static - ./static:/app/static
- ./:/app
env_file: env_file:
- .env.docker - .env.docker
depends_on: depends_on:

View File

@@ -17,9 +17,9 @@ SERVER_HOST="6.6.6.66" # 服务器IP地址
SERVER_USER="ubuntu" # 服务器用户名 SERVER_USER="ubuntu" # 服务器用户名
SERVER_PASSWORD="qweasdzxc1" # 服务器密码 SERVER_PASSWORD="qweasdzxc1" # 服务器密码
SERVER_PORT="22" # SSH端口默认22 SERVER_PORT="22" # SSH端口默认22
IMAGE_NAME="epage_server" # Docker镜像名称 IMAGE_NAME="epaper_server" # Docker镜像名称
IMAGE_TAG="latest" # Docker镜像标签 IMAGE_TAG="latest" # Docker镜像标签
CONTAINER_NAME="epage_server-container" # 容器名称 CONTAINER_NAME="epaper_server-container" # 容器名称
LOCAL_PORT="8199" # 本地端口 LOCAL_PORT="8199" # 本地端口
CONTAINER_PORT="8199" # 容器端口 CONTAINER_PORT="8199" # 容器端口
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名 TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名

53
main.py
View File

@@ -1,6 +1,7 @@
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import logging import logging
@@ -41,6 +42,7 @@ async def lifespan(app: FastAPI):
# 确保静态文件目录存在 # 确保静态文件目录存在
os.makedirs(settings.static_dir, exist_ok=True) os.makedirs(settings.static_dir, exist_ok=True)
os.makedirs(settings.media_dir, exist_ok=True)
yield yield
@@ -57,18 +59,54 @@ app = FastAPI(
description="用于管理墨水屏设备、内容和待办事项的API", description="用于管理墨水屏设备、内容和待办事项的API",
version="1.0.0", version="1.0.0",
lifespan=lifespan, lifespan=lifespan,
openapi_components={ )
"securitySchemes": {
"APIKeyHeader": { # 自定义OpenAPI模式以显示API Key鉴权按钮
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# 添加安全方案
if "components" not in openapi_schema:
openapi_schema["components"] = {}
security_scheme = {
"type": "apiKey", "type": "apiKey",
"in": "header", "in": "header",
"name": "X-API-Key", "name": "X-API-Key",
"description": "API Key鉴权请在下方输入正确的API Key" "description": "API Key鉴权请在下方输入正确的API Key"
} }
}
}, if "securitySchemes" not in openapi_schema["components"]:
security=[{"APIKeyHeader": []}] openapi_schema["components"]["securitySchemes"] = {}
)
openapi_schema["components"]["securitySchemes"]["APIKeyHeader"] = security_scheme
# 添加全局安全要求
if "security" not in openapi_schema:
openapi_schema["security"] = []
# 避免重复添加
has_apikey_security = False
for security_req in openapi_schema["security"]:
if "APIKeyHeader" in security_req:
has_apikey_security = True
break
if not has_apikey_security:
openapi_schema["security"].append({"APIKeyHeader": []})
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
# 添加CORS中间件 # 添加CORS中间件
app.add_middleware( app.add_middleware(
@@ -90,6 +128,7 @@ app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
# 挂载静态文件 # 挂载静态文件
app.mount("/static", StaticFiles(directory=settings.static_dir), name="static") app.mount("/static", StaticFiles(directory=settings.static_dir), name="static")
app.mount("/media", StaticFiles(directory=settings.media_dir), name="media")
# 注册API路由 # 注册API路由
app.include_router(api_router, prefix="/api") app.include_router(api_router, prefix="/api")

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB