Files
Pine 985b85cb4e feat(oss): 统一OSS/CDN对象存储与直链出口体系
- infrastructure/oss.py 重写:服务端上传/预签名上下行/CDN鉴权直链(方式A)/resolve_url·to_object_path 出入口规范
- config.py 新增 OSS_REGION/OSS_CDN_BASE_URL/OSS_CDN_AUTH_KEY
- media_upload 统一走 oss 单例,返回 CDN 直链;dispatcher /oss 前缀路由
- 管理端用户列表头像出口 CDN 化
2026-08-28 17:17:38 +08:00

181 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""全局配置:端口、数据目录、令牌有效期、认证开关。
所有路径/开关均可通过环境变量覆盖,便于部署和测试:
PINEAGENTS_DEMO_HOST / PINEAGENTS_DEMO_PORT 监听地址(默认 127.0.0.1:8090
PINEAGENTS_DEMO_DATA_DIR JSON 数据目录(默认 <项目根>/data
PINEAGENTS_DEMO_AUTH_ENABLED 认证开关(默认 true)
"""
from __future__ import annotations
import os
from pathlib import Path
# 启动第一步即加载项目根 .env(含 PINEAGENTS_INTERNAL_TOKEN / DATABASE_URL 等),
# 否则后续 os.environ.get 读到空串(.env 后续才会被 app.park/app.training 各自加载)。
# 必须在下方任何 os.environ.get 之前执行。
try: # python-dotenv 为可选依赖;缺失时仍可用系统环境变量
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
except Exception: # noqa: BLE001
pass
# 项目根目录 = 本文件的上上级(app/config.py -> <根>
BASE_DIR = Path(__file__).resolve().parent.parent
# ---------------------------------------------------------------------------
# 数据目录(serverdata/ 统一承载所有资源:data/logs/files/keys/prompts/uploads
# ---------------------------------------------------------------------------
SERVERDATA_DIR = Path(
os.environ.get("PINEAGENTS_SERVERDATA_DIR", str(BASE_DIR / "serverdata")),
).resolve()
# 数据子目录(缺失自动创建,start.sh 亦负责初始化)
DATA_DIR = SERVERDATA_DIR / "data"
LOG_DIR = SERVERDATA_DIR / "logs"
FILES_DIR = SERVERDATA_DIR / "files"
KEYS_DIR = SERVERDATA_DIR / "keys"
PROMPTS_DIR = SERVERDATA_DIR / "prompts"
UPLOADS_DIR = SERVERDATA_DIR / "uploads"
for _d in (DATA_DIR, LOG_DIR, FILES_DIR, KEYS_DIR, PROMPTS_DIR, UPLOADS_DIR):
_d.mkdir(parents=True, exist_ok=True)
# 数据库连接(异步)。开发默认 SQLite+aiosqlite;生产用 MySQL+asyncmy(环境变量覆盖)。
# 示例 MySQLPINEAGENTS_DEMO_DATABASE_URL=mysql+asyncmy://user:pass@127.0.0.1:3306/opc?charset=utf8mb4
DATABASE_URL = os.environ.get(
"PINEAGENTS_DEMO_DATABASE_URL",
f"sqlite+aiosqlite:///{DATA_DIR / 'app.db'}",
)
# Redis(缓存/会话/限流/分布式锁),空则降级内存。
REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0")
# OSS 对象存储(aioboto3,兼容阿里云 OSS / MinIO);空则文件落 serverdata/uploads。
OSS_ENDPOINT = os.environ.get("OSS_ENDPOINT", "")
OSS_ACCESS_KEY = os.environ.get("OSS_ACCESS_KEY", "")
OSS_SECRET_KEY = os.environ.get("OSS_SECRET_KEY", "")
OSS_BUCKET = os.environ.get("OSS_BUCKET", "opc-files")
OSS_REGION = os.environ.get("OSS_REGION", "cn-heyuan")
# CDN 加速域名(如 https://opcfiles.pinesound.cn);配置后下载统一走 CDN,空则退回预签名直链
OSS_CDN_BASE_URL = os.environ.get("OSS_CDN_BASE_URL", "").rstrip("/")
# CDN URL 鉴权密钥(鉴权方式C;须与阿里云 CDN 控制台主密钥一致),空则生成不带鉴权的 CDN 链接
OSS_CDN_AUTH_KEY = os.environ.get("OSS_CDN_AUTH_KEY", "")
USERS_FILE = DATA_DIR / "users.json"
TOKENS_FILE = DATA_DIR / "tokens.json"
AGENTS_FILE = DATA_DIR / "agents.json"
# 智能体初始化 md 模板(镜像本地 md_files 结构;qa 已统一为 PineAgents 品牌)。
# 目录:<PROMPTS_DIR>/<lang>/*.md(通用)、<PROMPTS_DIR>/qa/<lang>/*.md、
# <PROMPTS_DIR>/local/<lang>/*.md
TEMPLATES_DIR = PROMPTS_DIR
# 首次启动时用于初始化 data/ 的种子数据(提交到仓库,数据可读)。
SEED_DIR = BASE_DIR / "data" / "seed"
SEED_USERS_FILE = SEED_DIR / "demo_users.json"
# ---------------------------------------------------------------------------
# 服务监听
# ---------------------------------------------------------------------------
HOST = os.environ.get("PINEAGENTS_DEMO_HOST", "127.0.0.1")
PORT = int(os.environ.get("PINEAGENTS_DEMO_PORT", "8090"))
# ---------------------------------------------------------------------------
# 认证与令牌
# ---------------------------------------------------------------------------
# 演示服务默认开启认证(PineAgents 主后端把 /auth/* 转发到本服务)。
AUTH_ENABLED = (
os.environ.get("PINEAGENTS_DEMO_AUTH_ENABLED", "true").strip().lower()
in ("true", "1", "yes")
)
# 令牌有效期(秒):默认 7 天;-1/0 表示永久(按 100 年封顶)。
DEFAULT_TOKEN_EXPIRY_SECONDS = 7 * 24 * 3600
PERMANENT_TOKEN_SECONDS = 100 * 365 * 24 * 3600
TOKEN_EXPIRY_MAX = PERMANENT_TOKEN_SECONDS
# ---------------------------------------------------------------------------
# JWT
# ---------------------------------------------------------------------------
JWT_SECRET = os.environ.get(
"PINEAGENTS_DEMO_JWT_SECRET",
"dev-insecure-change-me-0123456789abcdef",
)
JWT_ALGORITHM = os.environ.get("PINEAGENTS_DEMO_JWT_ALGORITHM", "HS256")
JWT_ACCESS_EXPIRY_SECONDS = DEFAULT_TOKEN_EXPIRY_SECONDS
# ---------------------------------------------------------------------------
# 算力引擎(compute-engine / new-api)管理接入(loopback,仅服务)
# 模型调用与额度/令牌/流水均经此引擎计量;前端不直连引擎。
# ---------------------------------------------------------------------------
COMPUTE_BASE_URL = os.environ.get(
"PINEAGENTS_COMPUTE_BASE_URL", "http://127.0.0.1:3000",
)
# 受信任内部令牌:server-core 作为唯一入口,无权限限制访问引擎管理/root 端点。
# 与引擎侧 PINEAGENTS_INTERNAL_TOKEN 保持一致;回退兼容旧名 PINEAGENTS_COMPUTE_ADMIN_TOKEN。
COMPUTE_ADMIN_TOKEN = os.environ.get("PINEAGENTS_INTERNAL_TOKEN") or os.environ.get("PINEAGENTS_COMPUTE_ADMIN_TOKEN", "")
# /v1 模型中继转发(relay.py)用的引擎消费令牌:需是引擎 /v1 能识别的真实用户/渠道令牌,
# 与聚合 root 的 COMPUTE_ADMIN_TOKEN 区分开(后者仅用于管理/root 端点)。回退 COMPUTE_ADMIN_TOKEN。
COMPUTE_RELAY_TOKEN = os.environ.get("PINEAGENTS_COMPUTE_RELAY_TOKEN", "")
# 用户端算力中心「基础 API 地址」:对外公网地址(如 https://opc.pinesound.cn),供用户其它工具接入。
# 未设置时回落请求的 base_url。经代理时 request.base_url 可能为内部地址,故用此配置给公网地址。
COMPUTE_PUBLIC_BASE = os.environ.get("PINEAGENTS_COMPUTE_PUBLIC_BASE", "").rstrip("/")
COMPUTE_TIMEOUT = float(os.environ.get("PINEAGENTS_COMPUTE_TIMEOUT", "10"))
COMPUTE_RETRIES = int(os.environ.get("PINEAGENTS_COMPUTE_RETRIES", "2"))
# ---------------------------------------------------------------------------
# 短信验证码(统一登录)
# ---------------------------------------------------------------------------
# stub = 仅打印验证码不真正发送(默认,便于联调);生产接阿里云/腾讯云短信。
SMS_PROVIDER = os.environ.get("PINEAGENTS_SMS_PROVIDER", "stub").strip().lower()
SMS_CODE_TTL_SECONDS = int(os.environ.get("PINEAGENTS_SMS_CODE_TTL_SECONDS", "300"))
SMS_RATE_LIMIT = int(os.environ.get("PINEAGENTS_SMS_RATE_LIMIT", "5"))
# ---------------------------------------------------------------------------
# 微信登录(小程序 code2session + 开放平台网站应用扫码登录)
# 未配置 appid/secret 时走「测试直通」:扫码登录把 code 当 openid 返回,便于无真实凭据联调。
# WECHAT_APPID/SECRET 为小程序;WECHAT_OPEN_APPID/SECRET 为开放平台「网站应用」(扫码登录),缺省回退前者。
# ---------------------------------------------------------------------------
WECHAT_APPID = os.environ.get("PINEAGENTS_WECHAT_APPID", "")
WECHAT_SECRET = os.environ.get("PINEAGENTS_WECHAT_SECRET", "")
WECHAT_OPEN_APPID = os.environ.get("PINEAGENTS_WECHAT_OPEN_APPID", "") or WECHAT_APPID
WECHAT_OPEN_SECRET = os.environ.get("PINEAGENTS_WECHAT_OPEN_SECRET", "") or WECHAT_SECRET
# 扫码登录授权后的回跳地址(须在开放平台后台配置为该 URL):默认服务端 /auth/wx-callback。
# 未设置时由前端 request.base_url 推导。
WECHAT_QR_REDIRECT_URI = os.environ.get("PINEAGENTS_WECHAT_QR_REDIRECT_URI", "")
# 微信开放平台「扫码登录」(网页/桌面微信扫码 OAuth):**默认关闭** —— 只保留小程序登录。
# true 才返回可用的 /auth/wx-qr/startfalse 时前端隐藏「微信扫码」入口。
AUTH_WECHAT_QR = os.environ.get("PINEAGENTS_WECHAT_QR", "false").strip().lower() in ("true", "1", "yes")
# 小程序登录(wx.login code2session + 小程序码 mp-qr):**默认开启** —— 唯一保留的微信登录方式。
AUTH_WECHAT_LOGIN = os.environ.get("PINEAGENTS_WECHAT_LOGIN", "true").strip().lower() in ("true", "1", "yes")
# ---------------------------------------------------------------------------
# 运行环境(生产/开发)—— 供业务判断与小程序码版本选择,亦在 /auth/status 回显以便确认
# ---------------------------------------------------------------------------
# APP_ENV: prod | dev(默认 prod)。dev 时小程序码默认指向「体验版」(trial)prod 指向正式版(release)。
APP_ENV = os.environ.get("PINEAGENTS_ENV", "prod").strip().lower()
APP_ENV = "prod" if APP_ENV not in ("prod", "dev", "development", "test") else ("prod" if APP_ENV == "prod" else "dev")
# 小程序码(wxacode) 的版本类型:release(正式版) | trial(体验版) | develop(开发版)
# 默认按 APP_ENVprod→releasedev→trial;可显式 PINEAGENTS_WXACODE_ENV 覆盖。
_WXACODE_DEFAULT = "release" if APP_ENV == "prod" else "trial"
WECHAT_WXACODE_ENV = os.environ.get("PINEAGENTS_WXACODE_ENV", "").strip().lower() or _WXACODE_DEFAULT
if WECHAT_WXACODE_ENV not in ("release", "trial", "develop"):
WECHAT_WXACODE_ENV = _WXACODE_DEFAULT
def resolve_token_expiry(expires_in: int | None) -> int:
"""把前端传来的 ``expires_in`` 归一化为秒数。
- None → 默认 7 天
- <= 0(含 -1/0)→ 永久(100 年)
- 其他 → 按最大值封顶
"""
if expires_in is None:
return DEFAULT_TOKEN_EXPIRY_SECONDS
if expires_in <= 0:
return PERMANENT_TOKEN_SECONDS
return min(expires_in, TOKEN_EXPIRY_MAX)