refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to Trigger(handlers) → Service(services) → Model(repository/models) + utils): - Split monolithic __init__.py into handlers/services/utils across dailywife, deer_pipe, dice, galgame_card, helldivers_tools, huoziyinshua, learning_chat, makeaquote, mc_server_status, ncm_saying, picfinder_take, picstatus, random_jm_code, regif, steam_info, video_analysis, group_tools - Move static assets under res/: deer_pipe font/img, makeaquote font, helldivers img/templates, huoziyinshua HuoZiYinShua - Add config.py + register_config_items to ncm_saying, random_jm_code, group_tools; learning_chat unified config bridge - Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp - Disable brash_general_supercredits_tools (stub comment only) - bot.py: optional stdout/stderr redirect to log file for Web log viewer, force ANSI colorize on non-TTY sinks - Move runtime data (jm_code.json) out of plugin dir into hexi/data - Docs: plugin-audit-report.md; README reflects removed plugins Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"""统一 S3 存储模块 — 合并本地局域网 S3 和公网 MinIO"""
|
||||
|
||||
from pathlib import Path
|
||||
from time import strftime, localtime
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
from nonebot import logger
|
||||
|
||||
from hexi.web_config import get_effective_value
|
||||
|
||||
# 该插件模块名,用于读取统一配置值库(Web 修改后生效)
|
||||
_PLUGIN_ID = "hexi.plugins.nonebot_plugin_video_analysis"
|
||||
|
||||
|
||||
def _ev(name: str, default):
|
||||
"""读取统一配置生效值(用户值库 > getter > 默认),来源无关。"""
|
||||
return get_effective_value(_PLUGIN_ID, name, default)
|
||||
|
||||
|
||||
# ============================= 本地局域网 PLANA_S3 配置 =============================
|
||||
LOCAL_S3_PLANA_ENDPOINT = "http://192.168.2.15:5246"
|
||||
LOCAL_S3_PLANA_ACCESS_KEY = "vS8Lf7UoiS5cxL+mz+CH"
|
||||
LOCAL_S3_PLANA_SECRET_KEY = "Fi3phPW10CsZWuiQCerK4gxaIIJp41w5SQNlnDRb"
|
||||
LOCAL_S3_PLANA_BUCKET = "PLANA"
|
||||
|
||||
# ============================= 本地局域网 PLANB_S3 配置 =============================
|
||||
LOCAL_S3_PLANB_ENDPOINT = "http://192.168.2.15:5246"
|
||||
LOCAL_S3_PLANB_ACCESS_KEY = "vS8Lf7UoiS5cxL+mz+CH"
|
||||
LOCAL_S3_PLANB_SECRET_KEY = "Fi3phPW10CsZWuiQCerK4gxaIIJp41w5SQNlnDRb"
|
||||
LOCAL_S3_PLANB_BUCKET = "PLANB"
|
||||
|
||||
# ============================= 本地局域网 S3 配置 =============================
|
||||
LOCAL_S3_PLANC_ENDPOINT = "http://192.168.2.15:5246"
|
||||
LOCAL_S3_PLANC_ACCESS_KEY = "vS8Lf7UoiS5cxL+mz+CH"
|
||||
LOCAL_S3_PLANC_SECRET_KEY = "Fi3phPW10CsZWuiQCerK4gxaIIJp41w5SQNlnDRb"
|
||||
LOCAL_S3_PLANC_BUCKET = "PLANC"
|
||||
|
||||
# ============================= 公网 MinIO 配置 =============================
|
||||
PUBLIC_S3_ENDPOINT = "s3.sansenhoshi.top"
|
||||
PUBLIC_S3_ACCESS_KEY = "JDMynACSjPaN8JRwriwS"
|
||||
PUBLIC_S3_SECRET_KEY = "JRl4bIGxeiqwqvfeBTlAWQbUKMpNNHSZPm6ne93j"
|
||||
PUBLIC_S3_BUCKET = "s-file-trans"
|
||||
PUBLIC_S3_REGION = "bot"
|
||||
PUBLIC_S3_SECURE = True
|
||||
PUBLIC_S3_DOMAIN = f"https://{PUBLIC_S3_ENDPOINT}/{PUBLIC_S3_BUCKET}"
|
||||
|
||||
# ============================= 客户端实例(懒加载) =============================
|
||||
_local_s3_plana = None
|
||||
_local_s3_planb = None
|
||||
_local_s3_planc = None
|
||||
_public_s3 = None
|
||||
|
||||
|
||||
class S3Client:
|
||||
"""通用 S3 客户端"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint_url: str,
|
||||
access_key: str,
|
||||
secret_key: str,
|
||||
bucket: str,
|
||||
region: str = "us-east-1",
|
||||
secure: bool = False,
|
||||
):
|
||||
self.bucket = bucket
|
||||
self.client = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=endpoint_url,
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
region_name=region,
|
||||
verify=secure,
|
||||
config=Config(
|
||||
signature_version="s3v4",
|
||||
s3={"addressing_style": "path"},
|
||||
retries={"max_attempts": 3},
|
||||
connect_timeout=10,
|
||||
read_timeout=60,
|
||||
),
|
||||
)
|
||||
|
||||
def upload_and_get_url(
|
||||
self, local_path: str, key: str, expire: int = 3600
|
||||
) -> str:
|
||||
self.client.upload_file(local_path, self.bucket, key)
|
||||
url = self.client.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": self.bucket, "Key": key},
|
||||
ExpiresIn=expire,
|
||||
)
|
||||
return url
|
||||
|
||||
def upload_public(self, local_path: str, key: str) -> str:
|
||||
"""上传并设为公开读"""
|
||||
self.client.upload_file(
|
||||
local_path,
|
||||
self.bucket,
|
||||
key,
|
||||
ExtraArgs={"ACL": "public-read"},
|
||||
)
|
||||
logger.info(f"文件 {key} 已成功上传至云端S3")
|
||||
return f"{_ev('PUBLIC_S3_DOMAIN', PUBLIC_S3_DOMAIN)}/{key}"
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
try:
|
||||
self.client.delete_object(Bucket=self.bucket, Key=key)
|
||||
logger.info(f"成功删除 S3 文件: {key}")
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception(f"删除 S3 文件失败: {key}")
|
||||
return False
|
||||
|
||||
|
||||
def _get_local_s3_plana() -> S3Client:
|
||||
global _local_s3_plana
|
||||
if _local_s3_plana is None:
|
||||
_local_s3_plana = S3Client(
|
||||
_ev("LOCAL_S3_PLANA_ENDPOINT", LOCAL_S3_PLANA_ENDPOINT),
|
||||
_ev("LOCAL_S3_PLANA_ACCESS_KEY", LOCAL_S3_PLANA_ACCESS_KEY),
|
||||
_ev("LOCAL_S3_PLANA_SECRET_KEY", LOCAL_S3_PLANA_SECRET_KEY),
|
||||
_ev("LOCAL_S3_PLANA_BUCKET", LOCAL_S3_PLANA_BUCKET),
|
||||
)
|
||||
return _local_s3_plana
|
||||
|
||||
|
||||
def _get_local_s3_planb() -> S3Client:
|
||||
global _local_s3_planb
|
||||
if _local_s3_planb is None:
|
||||
_local_s3_planb = S3Client(
|
||||
_ev("LOCAL_S3_PLANB_ENDPOINT", LOCAL_S3_PLANB_ENDPOINT),
|
||||
_ev("LOCAL_S3_PLANB_ACCESS_KEY", LOCAL_S3_PLANB_ACCESS_KEY),
|
||||
_ev("LOCAL_S3_PLANB_SECRET_KEY", LOCAL_S3_PLANB_SECRET_KEY),
|
||||
_ev("LOCAL_S3_PLANB_BUCKET", LOCAL_S3_PLANB_BUCKET),
|
||||
)
|
||||
return _local_s3_planb
|
||||
|
||||
|
||||
def _get_local_s3_planc() -> S3Client:
|
||||
global _local_s3_planc
|
||||
if _local_s3_planc is None:
|
||||
_local_s3_planc = S3Client(
|
||||
_ev("LOCAL_S3_PLANC_ENDPOINT", LOCAL_S3_PLANC_ENDPOINT),
|
||||
_ev("LOCAL_S3_PLANC_ACCESS_KEY", LOCAL_S3_PLANC_ACCESS_KEY),
|
||||
_ev("LOCAL_S3_PLANC_SECRET_KEY", LOCAL_S3_PLANC_SECRET_KEY),
|
||||
_ev("LOCAL_S3_PLANC_BUCKET", LOCAL_S3_PLANC_BUCKET),
|
||||
)
|
||||
return _local_s3_planc
|
||||
|
||||
|
||||
def _get_public_s3() -> S3Client:
|
||||
global _public_s3
|
||||
if _public_s3 is None:
|
||||
secure = _ev("PUBLIC_S3_SECURE", PUBLIC_S3_SECURE)
|
||||
endpoint = _ev("PUBLIC_S3_ENDPOINT", PUBLIC_S3_ENDPOINT)
|
||||
_public_s3 = S3Client(
|
||||
f"{'https' if secure else 'http'}://{endpoint}",
|
||||
_ev("PUBLIC_S3_ACCESS_KEY", PUBLIC_S3_ACCESS_KEY),
|
||||
_ev("PUBLIC_S3_SECRET_KEY", PUBLIC_S3_SECRET_KEY),
|
||||
_ev("PUBLIC_S3_BUCKET", PUBLIC_S3_BUCKET),
|
||||
region=_ev("PUBLIC_S3_REGION", PUBLIC_S3_REGION),
|
||||
secure=secure,
|
||||
)
|
||||
return _public_s3
|
||||
|
||||
|
||||
# ============================= 对外接口 =============================
|
||||
|
||||
def upload_to_public_s3(file_path: str | Path) -> tuple[str, str]:
|
||||
"""
|
||||
上传到公网 MinIO,返回 (公网URL, file_key)
|
||||
|
||||
私聊场景使用,生成可公网访问的链接
|
||||
"""
|
||||
file_path = Path(file_path)
|
||||
current_day = strftime("%Y-%m-%d", localtime())
|
||||
file_key = f"{current_day}/{file_path.name}"
|
||||
try:
|
||||
url = _get_public_s3().upload_public(str(file_path), file_key)
|
||||
return url, file_key
|
||||
except Exception:
|
||||
logger.exception("上传到公网 MinIO 失败")
|
||||
return "", ""
|
||||
|
||||
|
||||
def upload_to_local_s3(
|
||||
title: str, image_post: bool, file_path: str | Path, plan: str | None = None
|
||||
) -> str:
|
||||
"""
|
||||
上传到局域网 S3,返回预签名 URL
|
||||
|
||||
plan="A" → PLANA 桶
|
||||
plan="B" → PLANB 桶
|
||||
None → PLANC 桶(默认)
|
||||
"""
|
||||
file_path = Path(file_path)
|
||||
current_day = strftime("%Y-%m-%d", localtime())
|
||||
if image_post:
|
||||
file_key = f"{current_day}/{title}/{file_path.name}"
|
||||
else:
|
||||
file_key = f"{current_day}/{file_path.name}"
|
||||
|
||||
if plan == "A":
|
||||
client = _get_local_s3_plana()
|
||||
elif plan == "B":
|
||||
client = _get_local_s3_planb()
|
||||
else:
|
||||
client = _get_local_s3_planc()
|
||||
|
||||
try:
|
||||
logger.info(f"文件 {file_key} 已成功上传至本地S3")
|
||||
return client.upload_and_get_url(str(file_path), file_key)
|
||||
except Exception:
|
||||
logger.exception("上传到本地 S3 失败")
|
||||
return ""
|
||||
|
||||
|
||||
def delete_from_public_s3(file_key: str) -> bool:
|
||||
"""从公网 MinIO 删除指定 key 的对象"""
|
||||
return _get_public_s3().delete(file_key)
|
||||
|
||||
|
||||
def upload_with_plan(
|
||||
file_path: str | Path,
|
||||
*,
|
||||
plan: str | None = None,
|
||||
is_private: bool = False,
|
||||
title: str = "",
|
||||
image_post: bool = False,
|
||||
) -> tuple[str, str | None]:
|
||||
"""
|
||||
统一上传入口:按 plan 路由到对应的本地桶,并按需上传公网
|
||||
|
||||
plan="A" → PLANA(仅本地)
|
||||
plan="B" → PLANB + 公网
|
||||
私聊 → PLANB + 公网
|
||||
默认 → PLANC(仅本地)
|
||||
|
||||
Returns:
|
||||
(local_url, public_url_or_none)
|
||||
"""
|
||||
# 本地上传
|
||||
if plan == "A":
|
||||
local_url = upload_to_local_s3(title, image_post, file_path, plan="A")
|
||||
elif plan == "B":
|
||||
local_url = upload_to_local_s3(title, image_post, file_path, plan="B")
|
||||
elif is_private:
|
||||
local_url = upload_to_local_s3(title, image_post, file_path, plan="B")
|
||||
else:
|
||||
local_url = upload_to_local_s3(title, image_post, file_path) # PLANC
|
||||
|
||||
# 公网上传(仅 PLANB 或私聊)
|
||||
public_url = None
|
||||
if plan == "B" or is_private:
|
||||
public_url, _ = upload_to_public_s3(file_path)
|
||||
|
||||
return local_url, public_url
|
||||
Reference in New Issue
Block a user