Compare commits
18 Commits
972ef57337
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e515395b55 | ||
|
|
59f174322a | ||
|
|
11e4de9071 | ||
|
|
75cfb27bb1 | ||
|
|
f91fabad0c | ||
|
|
ae2da39496 | ||
|
|
bb814061e7 | ||
|
|
d74eb795c3 | ||
|
|
8b8e1d51ce | ||
|
|
4a36952484 | ||
|
|
8b672d026d | ||
|
|
841bf23d4d | ||
|
|
373ce8cb2e | ||
|
|
a1e8c042ca | ||
|
|
0d140cd75c | ||
|
|
9620a4138d | ||
|
|
37b2cf6ba6 | ||
|
|
82bba110ee |
4
.env
4
.env
@@ -1,9 +1,11 @@
|
||||
# 环境变量配置文件
|
||||
|
||||
# 数据库配置
|
||||
# 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-a294f382488d46a1aa0d7cd8e750729b
|
||||
|
||||
# MQTT配置
|
||||
MQTT_BROKER_HOST=luna-mqtt
|
||||
MQTT_BROKER_PORT=1883
|
||||
|
||||
@@ -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,6 +9,8 @@ MQTT_BROKER_PORT=1883
|
||||
MQTT_USERNAME=luna2025
|
||||
MQTT_PASSWORD=123luna2021
|
||||
|
||||
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=墨水屏桌面屏幕系统
|
||||
DEBUG=false
|
||||
|
||||
@@ -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,6 +9,10 @@ MQTT_BROKER_PORT=1883
|
||||
MQTT_USERNAME=luna2025
|
||||
MQTT_PASSWORD=123luna2021
|
||||
|
||||
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd*******
|
||||
|
||||
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=墨水屏桌面屏幕系统
|
||||
DEBUG=false
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
from api import devices, contents, todos
|
||||
from api import devices, contents, todos, ai
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# 注册所有路由,并添加全局安全要求
|
||||
api_router.include_router(devices.router, prefix="/devices")
|
||||
api_router.include_router(contents.router, prefix="/contents")
|
||||
api_router.include_router(todos.router, prefix="/todos")
|
||||
api_router.include_router(todos.router, prefix="/todos")
|
||||
api_router.include_router(ai.router, prefix="/ai", tags=["AI生成"])
|
||||
Binary file not shown.
Binary file not shown.
BIN
api/__pycache__/ai.cpython-312.pyc
Normal file
BIN
api/__pycache__/ai.cpython-312.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/ai_schemas.cpython-312.pyc
Normal file
BIN
api/__pycache__/ai_schemas.cpython-312.pyc
Normal file
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.
Binary file not shown.
233
api/ai.py
Normal file
233
api/ai.py
Normal file
@@ -0,0 +1,233 @@
|
||||
import os
|
||||
import uuid
|
||||
import httpx
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
from config import settings
|
||||
from api.ai_schemas import AIGenerationRequest, AITaskResponse, AITaskResult, AITemplateGenerationRequest
|
||||
from api.prompts import get_prompt
|
||||
|
||||
router = APIRouter()
|
||||
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" # 移除强制异步,因为某些账号/模型不支持
|
||||
}
|
||||
|
||||
|
||||
# 构建请求体
|
||||
payload = {
|
||||
"model": model,
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"text": prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": {
|
||||
"prompt_extend": True,
|
||||
"watermark": False,
|
||||
"n": n,
|
||||
"size": size
|
||||
}
|
||||
}
|
||||
|
||||
if negative_prompt:
|
||||
payload["parameters"]["negative_prompt"] = negative_prompt
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
DASHSCOPE_API_URL,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=60.0 # 增加超时时间,同步请求可能较慢
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"DashScope API error: {response.text}")
|
||||
try:
|
||||
error_detail = response.json()
|
||||
except:
|
||||
error_detail = {"message": response.text}
|
||||
raise HTTPException(status_code=response.status_code, detail=error_detail)
|
||||
|
||||
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"]
|
||||
|
||||
# 如果没有task_id,检查是否有直接结果(同步模式)
|
||||
if not task_id:
|
||||
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"),
|
||||
status="PENDING" # 异步任务初始状态
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Request error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")
|
||||
|
||||
@router.post("/generate", response_model=AITaskResponse, summary="提交AI图片生成任务")
|
||||
async def generate_image(request: AIGenerationRequest):
|
||||
"""
|
||||
提交AI图片生成任务,使用阿里云DashScope服务
|
||||
"""
|
||||
return await _submit_dashscope_task(
|
||||
prompt=request.prompt,
|
||||
negative_prompt=request.negative_prompt,
|
||||
size=request.size,
|
||||
n=request.n,
|
||||
model=request.model
|
||||
)
|
||||
|
||||
@router.post("/generate_from_template", response_model=AITaskResponse, summary="使用模板提交AI图片生成任务")
|
||||
async def generate_image_from_template(request: AITemplateGenerationRequest):
|
||||
"""
|
||||
使用预设模板提交AI图片生成任务
|
||||
"""
|
||||
try:
|
||||
prompt = get_prompt(request.template_id, **request.params)
|
||||
if prompt is None:
|
||||
raise HTTPException(status_code=404, detail=f"未找到模板ID: {request.template_id}")
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
return await _submit_dashscope_task(
|
||||
prompt=prompt,
|
||||
negative_prompt=request.negative_prompt,
|
||||
size=request.size,
|
||||
n=request.n,
|
||||
model=request.model
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}", response_model=AITaskResult, summary="查询AI任务结果")
|
||||
async def get_task_result(task_id: str):
|
||||
"""
|
||||
查询AI生成任务的结果
|
||||
"""
|
||||
if not settings.dashscope_api_key:
|
||||
raise HTTPException(status_code=500, detail="DashScope API Key not configured")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {settings.dashscope_api_key}"
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{DASHSCOPE_TASK_URL}/{task_id}",
|
||||
headers=headers,
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error(f"DashScope Task API error: {response.text}")
|
||||
raise HTTPException(status_code=response.status_code, detail="Failed to fetch task status")
|
||||
|
||||
result = response.json()
|
||||
|
||||
# 构建返回结果
|
||||
# DashScope 任务查询返回结构:
|
||||
# {
|
||||
# "request_id": "...",
|
||||
# "output": {
|
||||
# "task_id": "...",
|
||||
# "task_status": "SUCCEEDED",
|
||||
# "results": [...]
|
||||
# },
|
||||
# "usage": ...
|
||||
# }
|
||||
|
||||
task_status = result.get("output", {}).get("task_status", "UNKNOWN")
|
||||
|
||||
return AITaskResult(
|
||||
task_id=task_id,
|
||||
status=task_status,
|
||||
code=result.get("code"),
|
||||
message=result.get("message"),
|
||||
output=result.get("output"),
|
||||
usage=result.get("usage")
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Request error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")
|
||||
33
api/ai_schemas.py
Normal file
33
api/ai_schemas.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
# AI生成相关模型
|
||||
class AIGenerationRequest(BaseModel):
|
||||
prompt: str = Field(..., description="生成图片的提示词")
|
||||
negative_prompt: Optional[str] = Field(None, description="反向提示词")
|
||||
size: str = Field("1024*1024", description="图片尺寸")
|
||||
n: int = Field(1, description="生成数量", ge=1, le=4)
|
||||
model: str = Field("wan2.6-t2i", description="使用的模型")
|
||||
|
||||
class AITemplateGenerationRequest(BaseModel):
|
||||
template_id: str = Field(..., description="提示词模板ID")
|
||||
params: Dict[str, str] = Field(default_factory=dict, description="提示词参数")
|
||||
negative_prompt: Optional[str] = Field(None, description="反向提示词")
|
||||
size: str = Field("1024*1024", description="图片尺寸")
|
||||
n: int = Field(1, description="生成数量", ge=1, le=4)
|
||||
model: str = Field("wan2.6-t2i", description="使用的模型")
|
||||
|
||||
class AITaskResponse(BaseModel):
|
||||
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
|
||||
status: str
|
||||
code: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
output: Optional[Dict[str, Any]] = None
|
||||
usage: Optional[Dict[str, Any]] = None
|
||||
31
api/prompts.py
Normal file
31
api/prompts.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# 预设提示词模板库
|
||||
# 使用Python的format语法进行参数替换,例如 {keyword}
|
||||
|
||||
PROMPTS = {
|
||||
"flower_shop": "一间有着精致窗户的花店,漂亮的木质门,摆放着{flower_type},风格为{style}",
|
||||
"landscape": "宏伟的{season}自然风景,包含{element},光线为{lighting},高分辨率,写实风格",
|
||||
"cyberpunk_city": "赛博朋克风格的未来城市,霓虹灯闪烁,{weather}天气,街道上有{vehicle}",
|
||||
"portrait": "一张{gender}的肖像照,{expression}表情,背景是{background},专业摄影布光",
|
||||
"default": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵"
|
||||
}
|
||||
|
||||
def get_prompt(template_id: str, **kwargs) -> str:
|
||||
"""
|
||||
获取并格式化提示词
|
||||
:param template_id: 提示词模板ID
|
||||
:param kwargs: 替换参数
|
||||
:return: 格式化后的提示词
|
||||
"""
|
||||
template = PROMPTS.get(template_id)
|
||||
if not template:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 使用 safe_substitute 避免参数缺失报错?
|
||||
# Python的format如果缺参数会报错,这里直接用format,让调用者负责提供完整参数
|
||||
# 或者我们可以在这里处理默认值
|
||||
return template.format(**kwargs)
|
||||
except KeyError as e:
|
||||
# 如果缺少参数,抛出异常或返回包含未替换占位符的字符串
|
||||
# 这里为了简单,如果出错,我们尽量保留原样或者报错
|
||||
raise ValueError(f"缺少提示词参数: {e}")
|
||||
@@ -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
|
||||
@@ -34,6 +35,9 @@ class Settings(BaseSettings):
|
||||
# 管理员配置
|
||||
admin_username: str = "admin"
|
||||
admin_password: str = "123456"
|
||||
|
||||
# DashScope配置
|
||||
dashscope_api_key: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
@@ -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