auto-save 2026-05-15 15:15 (~4)

This commit is contained in:
2026-05-15 15:15:47 +08:00
parent 8bdb797c1d
commit 7ee9ea2303
4 changed files with 196 additions and 20 deletions

View File

@@ -1,19 +1,5 @@
{
"entries": [
{
"files_changed": 1,
"hash": "8547db9",
"message": "auto-save 2026-05-13 23:45 (~1)",
"ts": "2026-05-13T23:46:07+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "093c924",
"message": "auto-save 2026-05-13 23:51 (~1)",
"ts": "2026-05-13T23:51:46+08:00",
"type": "commit"
},
{
"files_changed": 1,
"message": "Claude 会话活跃 · 最近命令claude · 1 项未提交变更 · 最近提交auto-save 2026-05-13 23:51 (~1)",
@@ -3251,6 +3237,19 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-15 15:04 (~8)",
"files_changed": 1
},
{
"ts": "2026-05-15T15:10:14+08:00",
"type": "commit",
"message": "auto-save 2026-05-15 15:10 (~2)",
"hash": "8bdb797",
"files_changed": 2
},
{
"ts": "2026-05-15T07:14:46Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 4 项未提交变更 · 最近提交auto-save 2026-05-15 15:10 (~2)",
"files_changed": 4
}
]
}

View File

@@ -2,9 +2,12 @@ from __future__ import annotations
import asyncio
import base64
import hashlib
import hmac
import json
import os
import random
import secrets
import shutil
import subprocess
import threading
@@ -16,7 +19,7 @@ from typing import Literal
import httpx
from dotenv import load_dotenv
from fastapi import BackgroundTasks, FastAPI, File, HTTPException, UploadFile
from fastapi import BackgroundTasks, FastAPI, File, HTTPException, Request, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
@@ -91,6 +94,12 @@ VIDEO_MODEL_ALIASES = {
}
VIDEO_API_BASE_URL = os.getenv("VIDEO_API_BASE_URL", "").strip()
VIDEO_API_KEY = os.getenv("VIDEO_API_KEY", "").strip()
WEB_AUTH_USERNAME = os.getenv("WEB_AUTH_USERNAME", "").strip()
WEB_AUTH_PASSWORD = os.getenv("WEB_AUTH_PASSWORD", "").strip()
WEB_AUTH_SESSION_SECRET = os.getenv("WEB_AUTH_SESSION_SECRET", "").strip()
WEB_AUTH_COOKIE_NAME = os.getenv("WEB_AUTH_COOKIE_NAME", "skg_marketing_session").strip() or "skg_marketing_session"
WEB_AUTH_COOKIE_SECURE = os.getenv("WEB_AUTH_COOKIE_SECURE", "true").strip().lower() not in {"0", "false", "no"}
WEB_AUTH_CONFIGURED = bool(WEB_AUTH_USERNAME and WEB_AUTH_PASSWORD and WEB_AUTH_SESSION_SECRET)
def default_video_gateway_paths(base_url: str) -> tuple[str, str, str]:
@@ -455,6 +464,12 @@ class Job(BaseModel):
error: str = ""
class AuthLoginPayload(BaseModel):
username: str
password: str
remember: bool = False
JOBS: dict[str, Job] = {}
ANALYZE_QUEUE: list[AnalyzeTask] = []
ANALYZE_WORKER_RUNNING = False
@@ -462,6 +477,58 @@ AUDIO_WORKERS_RUNNING: set[str] = set()
AUDIO_WORKERS_LOCK = threading.Lock()
def ensure_auth_configured() -> None:
if not WEB_AUTH_CONFIGURED:
raise HTTPException(503, "WEB_AUTH_USERNAME、WEB_AUTH_PASSWORD 或 WEB_AUTH_SESSION_SECRET 未配置")
def _auth_signature(body: str) -> str:
return hmac.new(WEB_AUTH_SESSION_SECRET.encode("utf-8"), body.encode("utf-8"), hashlib.sha256).hexdigest()
def _encode_auth_body(payload: dict) -> str:
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def _decode_auth_body(body: str) -> dict:
padded = body + "=" * (-len(body) % 4)
raw = base64.urlsafe_b64decode(padded.encode("ascii"))
data = json.loads(raw.decode("utf-8"))
return data if isinstance(data, dict) else {}
def make_auth_token(username: str, ttl_seconds: int) -> str:
body = _encode_auth_body({
"u": username,
"exp": int(time.time()) + ttl_seconds,
"n": secrets.token_hex(8),
})
return f"{body}.{_auth_signature(body)}"
def verify_auth_token(token: str) -> str | None:
if not WEB_AUTH_CONFIGURED or "." not in token:
return None
body, supplied_sig = token.rsplit(".", 1)
if not hmac.compare_digest(_auth_signature(body), supplied_sig):
return None
try:
payload = _decode_auth_body(body)
username = str(payload.get("u") or "")
expires_at = int(payload.get("exp") or 0)
except Exception:
return None
if username != WEB_AUTH_USERNAME or expires_at < int(time.time()):
return None
return username
def auth_username_from_request(request: Request) -> str | None:
token = request.cookies.get(WEB_AUTH_COOKIE_NAME, "")
return verify_auth_token(token)
def job_dir(job_id: str) -> Path:
d = JOBS_DIR / job_id
d.mkdir(parents=True, exist_ok=True)
@@ -724,6 +791,48 @@ app.add_middleware(
)
@app.get("/auth/check")
def auth_check(request: Request) -> Response:
ensure_auth_configured()
if not auth_username_from_request(request):
raise HTTPException(401, "unauthorized")
return Response(status_code=204)
@app.post("/auth/login")
def auth_login(payload: AuthLoginPayload, response: Response) -> dict:
ensure_auth_configured()
username = payload.username.strip()
password = payload.password
valid_user = hmac.compare_digest(username, WEB_AUTH_USERNAME)
valid_password = hmac.compare_digest(password, WEB_AUTH_PASSWORD)
if not (valid_user and valid_password):
raise HTTPException(401, "用户名或密码不正确")
ttl_seconds = 60 * 60 * 24 * 30 if payload.remember else 60 * 60 * 12
response.set_cookie(
key=WEB_AUTH_COOKIE_NAME,
value=make_auth_token(WEB_AUTH_USERNAME, ttl_seconds),
max_age=ttl_seconds,
httponly=True,
secure=WEB_AUTH_COOKIE_SECURE,
samesite="lax",
path="/",
)
return {"ok": True, "username": WEB_AUTH_USERNAME}
@app.post("/auth/logout")
def auth_logout(response: Response) -> dict:
response.delete_cookie(
key=WEB_AUTH_COOKIE_NAME,
path="/",
secure=WEB_AUTH_COOKIE_SECURE,
samesite="lax",
)
return {"ok": True}
# ---------- Pipeline 实现 ----------
def run(cmd: list[str], cwd: Path | None = None) -> str:
@@ -2157,6 +2266,7 @@ def health() -> dict:
return {
"ok": True,
"llm_configured": bool(LLM_API_KEY),
"auth_configured": WEB_AUTH_CONFIGURED,
"base_url": LLM_BASE_URL or "openai-default",
"models": {
"asr": ASR_MODEL,

View File

@@ -4,17 +4,61 @@ server {
client_max_body_size 2g;
auth_basic "SKG Marketing Studio";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
gzip on;
gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;
location = /__auth {
internal;
proxy_pass http://skg-marketing-api:4291/auth/check;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /api/auth/login {
proxy_pass http://skg-marketing-api:4291/auth/login;
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_connect_timeout 60s;
}
location = /api/auth/logout {
proxy_pass http://skg-marketing-api:4291/auth/logout;
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_connect_timeout 60s;
}
location @api_unauthorized {
default_type application/json;
return 401 '{"error":"unauthorized"}';
}
location = /api {
return 308 /api/;
}
location /api/ {
auth_request /__auth;
error_page 401 = @api_unauthorized;
proxy_pass http://skg-marketing-api:4291/;
proxy_http_version 1.1;
proxy_request_buffering off;
@@ -28,7 +72,32 @@ server {
proxy_connect_timeout 60s;
}
location = /login {
return 308 /login/;
}
location /login/ {
root /usr/share/nginx/html;
try_files $uri $uri/ /login/index.html;
}
location /_next/ {
root /usr/share/nginx/html;
try_files $uri =404;
}
location ~* ^/(icon|apple-icon|favicon|manifest|placeholder).* {
root /usr/share/nginx/html;
try_files $uri =404;
}
location @login_redirect {
return 302 /login/;
}
location / {
auth_request /__auth;
error_page 401 = @login_redirect;
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}

View File

@@ -26,8 +26,6 @@ services:
container_name: skg-marketing-web
depends_on:
- api
volumes:
- ./deploy/.htpasswd:/etc/nginx/auth/.htpasswd:ro
restart: unless-stopped
networks:
- skg-marketing-internal