Compare commits
16 Commits
37b2cf6ba6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e515395b55 | ||
|
|
59f174322a | ||
|
|
11e4de9071 | ||
|
|
75cfb27bb1 | ||
|
|
f91fabad0c | ||
|
|
ae2da39496 | ||
|
|
bb814061e7 | ||
|
|
d74eb795c3 | ||
|
|
8b8e1d51ce | ||
|
|
4a36952484 | ||
|
|
8b672d026d | ||
|
|
841bf23d4d | ||
|
|
373ce8cb2e | ||
|
|
a1e8c042ca | ||
|
|
0d140cd75c | ||
|
|
9620a4138d |
4
.env
4
.env
@@ -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
|
||||
|
||||
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f
|
||||
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
|
||||
|
||||
# MQTT配置
|
||||
MQTT_BROKER_HOST=luna-mqtt
|
||||
|
||||
@@ -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
|
||||
# MQTT配置
|
||||
MQTT_BROKER_HOST=luna-mqtt
|
||||
@@ -9,7 +9,7 @@ MQTT_BROKER_PORT=1883
|
||||
MQTT_USERNAME=luna2025
|
||||
MQTT_PASSWORD=123luna2021
|
||||
|
||||
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f
|
||||
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=墨水屏桌面屏幕系统
|
||||
|
||||
@@ -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
|
||||
# MQTT配置
|
||||
MQTT_BROKER_HOST=luna-mqtt
|
||||
@@ -9,7 +9,7 @@ MQTT_BROKER_PORT=1883
|
||||
MQTT_USERNAME=luna2025
|
||||
MQTT_PASSWORD=123luna2021
|
||||
|
||||
DASHSCOPE_API_KEY=sk-657968d48d0249099f3809f796f80a4f
|
||||
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd*******
|
||||
|
||||
|
||||
|
||||
|
||||
147
.gitea/workflows/deploy.yaml
Normal file
147
.gitea/workflows/deploy.yaml
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
使用fastAPI框架
|
||||
python 3.12
|
||||
|
||||
- 使用pg数据库 url http://121.43.104.161:6432
|
||||
- 使用pg数据库 url http://121.43.104.161:6433
|
||||
- 用户名:luna
|
||||
- 密码:123luna
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
api/__pycache__/prompts.cpython-312.pyc
Normal file
BIN
api/__pycache__/prompts.cpython-312.pyc
Normal file
Binary file not shown.
80
api/ai.py
80
api/ai.py
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import uuid
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import Dict, Any
|
||||
import httpx
|
||||
import json
|
||||
import logging
|
||||
from config import settings
|
||||
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_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):
|
||||
if not settings.dashscope_api_key:
|
||||
raise HTTPException(status_code=500, detail="DashScope API Key not configured")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {settings.dashscope_api_key}",
|
||||
"X-DashScope-Async": "enable" # 确保异步任务提交
|
||||
"Authorization": f"Bearer {settings.dashscope_api_key}"
|
||||
# "X-DashScope-Async": "enable" # 移除强制异步,因为某些账号/模型不支持
|
||||
}
|
||||
|
||||
|
||||
# 构建请求体
|
||||
payload = {
|
||||
"model": model,
|
||||
@@ -55,7 +85,7 @@ async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n
|
||||
DASHSCOPE_API_URL,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=30.0
|
||||
timeout=60.0 # 增加超时时间,同步请求可能较慢
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
# 检查是异步任务返回还是同步结果返回
|
||||
task_id = None
|
||||
if "output" in result and "task_id" in result["output"]:
|
||||
task_id = result["output"]["task_id"]
|
||||
elif "task_id" in result:
|
||||
task_id = result["task_id"]
|
||||
else:
|
||||
if "output" in result and "task_id" in result["output"]:
|
||||
task_id = result["output"]["task_id"]
|
||||
else:
|
||||
logger.warning(f"Unexpected response structure: {result}")
|
||||
task_id = result.get("output", {}).get("task_id")
|
||||
|
||||
# 如果没有task_id,检查是否有直接结果(同步模式)
|
||||
if not task_id:
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve task_id from DashScope response")
|
||||
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}")
|
||||
# 尝试从output.task_id再找一次
|
||||
task_id = result.get("output", {}).get("task_id")
|
||||
|
||||
if not task_id:
|
||||
# 依然找不到,报错
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve task_id or results from DashScope response")
|
||||
|
||||
return AITaskResponse(
|
||||
task_id=task_id,
|
||||
request_id=result.get("request_id")
|
||||
request_id=result.get("request_id"),
|
||||
status="PENDING" # 异步任务初始状态
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
|
||||
@@ -18,8 +18,11 @@ class AITemplateGenerationRequest(BaseModel):
|
||||
model: str = Field("wan2.6-t2i", description="使用的模型")
|
||||
|
||||
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")
|
||||
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):
|
||||
task_id: str
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 数据库配置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"
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class Settings(BaseSettings):
|
||||
static_dir: str = "static"
|
||||
upload_dir: str = "static/uploads"
|
||||
processed_dir: str = "static/processed"
|
||||
media_dir: str = "media" # 新增媒体文件目录
|
||||
|
||||
# 墨水屏配置
|
||||
ink_width: int = 400
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
services:
|
||||
# 主应用服务
|
||||
luna-app:
|
||||
epaper_server:
|
||||
image: epaper_server:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: luna-app
|
||||
container_name: epaper_server
|
||||
ports:
|
||||
- "8199:8199"
|
||||
volumes:
|
||||
- ./static:/app/static
|
||||
- ./:/app
|
||||
env_file:
|
||||
- .env.docker
|
||||
depends_on:
|
||||
|
||||
@@ -17,9 +17,9 @@ SERVER_HOST="6.6.6.66" # 服务器IP地址
|
||||
SERVER_USER="ubuntu" # 服务器用户名
|
||||
SERVER_PASSWORD="qweasdzxc1" # 服务器密码
|
||||
SERVER_PORT="22" # SSH端口,默认22
|
||||
IMAGE_NAME="epage_server" # Docker镜像名称
|
||||
IMAGE_NAME="epaper_server" # Docker镜像名称
|
||||
IMAGE_TAG="latest" # Docker镜像标签
|
||||
CONTAINER_NAME="epage_server-container" # 容器名称
|
||||
CONTAINER_NAME="epaper_server-container" # 容器名称
|
||||
LOCAL_PORT="8199" # 本地端口
|
||||
CONTAINER_PORT="8199" # 容器端口
|
||||
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名
|
||||
|
||||
61
main.py
61
main.py
@@ -1,6 +1,7 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
@@ -41,6 +42,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
# 确保静态文件目录存在
|
||||
os.makedirs(settings.static_dir, exist_ok=True)
|
||||
os.makedirs(settings.media_dir, exist_ok=True)
|
||||
|
||||
yield
|
||||
|
||||
@@ -57,19 +59,55 @@ app = FastAPI(
|
||||
description="用于管理墨水屏设备、内容和待办事项的API",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
openapi_components={
|
||||
"securitySchemes": {
|
||||
"APIKeyHeader": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "API Key鉴权,请在下方输入正确的API Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
security=[{"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",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "API Key鉴权,请在下方输入正确的API Key"
|
||||
}
|
||||
|
||||
if "securitySchemes" not in openapi_schema["components"]:
|
||||
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中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -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("/media", StaticFiles(directory=settings.media_dir), name="media")
|
||||
|
||||
# 注册API路由
|
||||
app.include_router(api_router, prefix="/api")
|
||||
|
||||
BIN
media/d295c74a-84e7-4520-8cf1-62a763fde49e.png
Normal file
BIN
media/d295c74a-84e7-4520-8cf1-62a763fde49e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 411 KiB |
BIN
test_image.png
BIN
test_image.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
Reference in New Issue
Block a user