feat: 实现定时任务系统
All checks were successful
continuous-integration/drone/push Build is passing

- 新增 platform_scheduled_tasks, platform_task_logs, platform_script_vars, platform_secrets 数据库表
- 实现 ScriptSDK 提供 AI/通知/DB/HTTP/变量存储/参数获取等功能
- 实现安全的脚本执行器,支持沙箱环境和禁止危险操作
- 实现 APScheduler 调度服务,支持简单时间点和 CRON 表达式
- 新增定时任务 API 路由,包含 CRUD、执行、日志、密钥管理
- 新增定时任务前端页面,支持脚本编辑、测试运行、日志查看
This commit is contained in:
2026-01-28 16:38:19 +08:00
parent 7806072b17
commit 104487f082
19 changed files with 1870 additions and 5406 deletions

View File

@@ -1,79 +1,113 @@
"""脚本执行 SDK - 提供给 Python 脚本使用的内置能力"""
"""脚本执行SDK - Python脚本提供内置功能"""
import json
import logging
import os
import httpx
from datetime import datetime
from typing import Any, Dict, List, Optional
import httpx
from sqlalchemy import text
from sqlalchemy.orm import Session
from ..database import SessionLocal
logger = logging.getLogger(__name__)
class ScriptSDK:
"""
脚本执行 SDK
"""脚本SDK - 提供AI、通知、数据库、HTTP、变量存储等功能"""
提供以下能力:
- AI 大模型调用
- 钉钉/企微通知
- 数据库查询(只读)
- HTTP 请求
- 变量存储(跨执行持久化)
- 日志记录
- 多租户遍历
- 密钥管理
"""
def __init__(self, tenant_id: str, task_id: int, trace_id: str = None):
self.tenant_id = tenant_id
def __init__(
self,
db: Session,
task_id: int,
tenant_id: Optional[str] = None,
trace_id: Optional[str] = None,
params: Optional[Dict[str, Any]] = None
):
self.db = db
self.task_id = task_id
self.trace_id = trace_id or f"script_{task_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
self._logs: List[str] = []
self._db: Optional[Session] = None
self._tenants_cache: Optional[List[Dict]] = None # 租户列表缓存
self.tenant_id = tenant_id
self.trace_id = trace_id
self.params = params or {}
self._logs: List[Dict] = []
self._output: List[str] = []
self._tenants_cache: Dict = {}
# AI 配置
self._ai_base_url = os.getenv('OPENAI_BASE_URL', 'https://api.4sapi.net/v1')
self._ai_api_key = os.getenv('OPENAI_API_KEY', 'sk-9yMCXjRGANbacz20kJY8doSNy6Rf446aYwmgGIuIXQ7DAyBw')
self._ai_model = os.getenv('OPENAI_MODEL', 'gemini-2.5-flash')
def _get_db(self) -> Session:
"""获取数据库会话"""
if self._db is None:
self._db = SessionLocal()
return self._db
# ==================== 参数获取 ====================
def _close_db(self):
"""关闭数据库会话"""
if self._db:
self._db.close()
self._db = None
def get_param(self, key: str, default: Any = None) -> Any:
"""获取任务参数
Args:
key: 参数名
default: 默认值
Returns:
参数值
"""
return self.params.get(key, default)
# ============ AI 服务 ============
def get_params(self) -> Dict[str, Any]:
"""获取所有任务参数
Returns:
所有参数字典
"""
return self.params.copy()
async def ai_chat(
self,
prompt: str,
system: str = None,
model: str = "gemini-2.5-flash",
# ==================== 日志 ====================
def log(self, message: str, level: str = 'INFO') -> None:
"""记录日志
Args:
message: 日志内容
level: 日志级别 (INFO, WARN, ERROR)
"""
log_entry = {
'time': datetime.now().isoformat(),
'level': level.upper(),
'message': message
}
self._logs.append(log_entry)
self._output.append(f"[{level.upper()}] {message}")
def print(self, *args, **kwargs) -> None:
"""打印输出兼容print"""
message = ' '.join(str(arg) for arg in args)
self._output.append(message)
def get_logs(self) -> List[Dict]:
"""获取所有日志"""
return self._logs
def get_output(self) -> str:
"""获取所有输出"""
return '\n'.join(self._output)
# ==================== AI 调用 ====================
def ai(
self,
prompt: str,
system: Optional[str] = None,
model: Optional[str] = None,
temperature: float = 0.7,
max_tokens: int = 2000
) -> str:
"""
调用大模型
"""调用AI模型
Args:
prompt: 用户提示词
system: 系统提示词(可选)
model: 模型名称默认 gemini-2.5-flash
temperature: 温度,默认 0.7
max_tokens: 最大 token 数,默认 2000
system: 系统提示词
model: 模型名称默认gemini-2.5-flash
temperature: 温度参数
max_tokens: 最大token
Returns:
AI 生成的文本
AI响应内容
"""
# 使用 4sapi 作为 AI 服务
api_key = "sk-9yMCXjRGANbacz20kJY8doSNy6Rf446aYwmgGIuIXQ7DAyBw"
base_url = "https://4sapi.com/v1"
model = model or self._ai_model
messages = []
if system:
@@ -81,11 +115,11 @@ class ScriptSDK:
messages.append({"role": "user", "content": prompt})
try:
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(
f"{base_url}/chat/completions",
with httpx.Client(timeout=60) as client:
response = client.post(
f"{self._ai_base_url}/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Authorization": f"Bearer {self._ai_api_key}",
"Content-Type": "application/json"
},
json={
@@ -97,515 +131,349 @@ class ScriptSDK:
)
response.raise_for_status()
data = response.json()
content = data["choices"][0]["message"]["content"]
self.log(f"AI 调用成功,模型: {model},响应长度: {len(content)}")
content = data['choices'][0]['message']['content']
self.log(f"AI调用成功: {len(content)} 字符")
return content
except Exception as e:
self.log(f"AI 调用失败: {str(e)}", level="ERROR")
self.log(f"AI调用失败: {str(e)}", 'ERROR')
raise
# ============ 通知服务 ============
# ==================== 通知 ====================
async def send_dingtalk(
self,
webhook: str,
content: str,
msg_type: str = "text",
at_mobiles: List[str] = None,
at_all: bool = False
) -> bool:
"""
发送钉钉群消息
def dingtalk(self, webhook: str, content: str, title: Optional[str] = None, at_all: bool = False) -> bool:
"""发送钉钉消息
Args:
webhook: 钉钉机器人 Webhook URL
content: 消息内容
msg_type: 消息类型text 或 markdown
at_mobiles: @的手机号列表
webhook: 钉钉机器人webhook地址
content: 消息内容支持Markdown
title: 消息标题
at_all: 是否@所有人
Returns:
是否发送成功
"""
try:
if msg_type == "text":
data = {
"msgtype": "text",
"text": {"content": content},
"at": {
"atMobiles": at_mobiles or [],
"isAtAll": at_all
}
}
else:
data = {
"msgtype": "markdown",
"markdown": {
"title": content[:20] if len(content) > 20 else content,
"text": content
},
"at": {
"atMobiles": at_mobiles or [],
"isAtAll": at_all
}
}
payload = {
"msgtype": "markdown",
"markdown": {
"title": title or "通知",
"text": content + ("\n@所有人" if at_all else "")
},
"at": {"isAtAll": at_all}
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(webhook, json=data)
with httpx.Client(timeout=10) as client:
response = client.post(webhook, json=payload)
response.raise_for_status()
result = response.json()
if result.get("errcode") == 0:
self.log(f"钉钉消息发送成功")
return True
else:
self.log(f"钉钉消息发送失败: {result.get('errmsg')}", level="ERROR")
return False
success = result.get('errcode') == 0
self.log(f"钉钉消息发送{'成功' if success else '失败'}")
return success
except Exception as e:
self.log(f"钉钉消息发送异常: {str(e)}", level="ERROR")
self.log(f"钉钉消息发送失败: {str(e)}", 'ERROR')
return False
async def send_wecom(
self,
webhook: str,
content: str,
msg_type: str = "text"
) -> bool:
"""
发送企业微信群消息
def wecom(self, webhook: str, content: str, msg_type: str = 'markdown') -> bool:
"""发送企业微信消息
Args:
webhook: 企微机器人 Webhook URL
webhook: 企微机器人webhook地址
content: 消息内容
msg_type: 消息类型text markdown
msg_type: 消息类型 (text, markdown)
Returns:
是否发送成功
"""
try:
if msg_type == "text":
data = {
"msgtype": "text",
"text": {"content": content}
}
else:
data = {
if msg_type == 'markdown':
payload = {
"msgtype": "markdown",
"markdown": {"content": content}
}
else:
payload = {
"msgtype": "text",
"text": {"content": content}
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(webhook, json=data)
with httpx.Client(timeout=10) as client:
response = client.post(webhook, json=payload)
response.raise_for_status()
result = response.json()
if result.get("errcode") == 0:
self.log(f"企微消息发送成功")
return True
else:
self.log(f"企微消息发送失败: {result.get('errmsg')}", level="ERROR")
return False
success = result.get('errcode') == 0
self.log(f"企微消息发送{'成功' if success else '失败'}")
return success
except Exception as e:
self.log(f"企微消息发送异常: {str(e)}", level="ERROR")
self.log(f"企微消息发送失败: {str(e)}", 'ERROR')
return False
# ============ 数据库查询 ============
# ==================== HTTP 请求 ====================
def db_query(self, sql: str, params: Dict[str, Any] = None) -> List[Dict]:
def http_get(self, url: str, headers: Optional[Dict] = None, params: Optional[Dict] = None, timeout: int = 30) -> Dict:
"""发起HTTP GET请求
Returns:
{"status": 200, "data": ..., "text": "..."}
"""
执行 SQL 查询(只读)
try:
with httpx.Client(timeout=timeout) as client:
response = client.get(url, headers=headers, params=params)
return {
"status": response.status_code,
"data": response.json() if response.headers.get('content-type', '').startswith('application/json') else None,
"text": response.text
}
except Exception as e:
self.log(f"HTTP GET 失败: {str(e)}", 'ERROR')
raise
def http_post(self, url: str, data: Any = None, headers: Optional[Dict] = None, timeout: int = 30) -> Dict:
"""发起HTTP POST请求
Returns:
{"status": 200, "data": ..., "text": "..."}
"""
try:
with httpx.Client(timeout=timeout) as client:
response = client.post(url, json=data, headers=headers)
return {
"status": response.status_code,
"data": response.json() if response.headers.get('content-type', '').startswith('application/json') else None,
"text": response.text
}
except Exception as e:
self.log(f"HTTP POST 失败: {str(e)}", 'ERROR')
raise
# ==================== 数据库查询(只读)====================
def db_query(self, sql: str, params: Optional[Dict] = None) -> List[Dict]:
"""执行只读SQL查询
Args:
sql: SQL 语句(仅支持 SELECT
params: 查询参数
sql: SQL语句必须是SELECT
params: 参数字典
Returns:
查询结果列表
"""
# 安全检查:只允许 SELECT
sql_upper = sql.strip().upper()
if not sql_upper.startswith("SELECT"):
raise ValueError("只允许 SELECT 查询")
if not sql_upper.startswith('SELECT'):
raise ValueError("只允许执行SELECT查询")
forbidden = ["INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE"]
# 禁止危险操作
forbidden = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'TRUNCATE', 'ALTER', 'CREATE']
for word in forbidden:
if word in sql_upper:
raise ValueError(f"禁止使用 {word} 语句")
raise ValueError(f"禁止执行 {word} 操作")
db = self._get_db()
try:
result = db.execute(text(sql), params or {})
rows = [dict(row) for row in result.mappings().all()]
self.log(f"SQL 查询成功,返回 {len(rows)} 条记录")
from sqlalchemy import text
result = self.db.execute(text(sql), params or {})
columns = result.keys()
rows = [dict(zip(columns, row)) for row in result.fetchall()]
self.log(f"SQL查询返回 {len(rows)} 条记录")
return rows
except Exception as e:
self.log(f"SQL 查询失败: {str(e)}", level="ERROR")
self.log(f"SQL查询失败: {str(e)}", 'ERROR')
raise
# ============ HTTP 请求 ============
async def http_get(
self,
url: str,
headers: Dict[str, str] = None,
params: Dict[str, Any] = None,
timeout: int = 30
) -> Dict:
"""
发送 HTTP GET 请求
Args:
url: 请求 URL
headers: 请求头
params: 查询参数
timeout: 超时时间(秒)
Returns:
响应数据JSON 解析后)
"""
try:
async with httpx.AsyncClient(timeout=float(timeout)) as client:
response = await client.get(url, headers=headers, params=params)
self.log(f"HTTP GET {url} -> {response.status_code}")
try:
return response.json()
except:
return {"text": response.text, "status_code": response.status_code}
except Exception as e:
self.log(f"HTTP GET 失败: {str(e)}", level="ERROR")
raise
async def http_post(
self,
url: str,
data: Dict[str, Any] = None,
json_data: Dict[str, Any] = None,
headers: Dict[str, str] = None,
timeout: int = 30
) -> Dict:
"""
发送 HTTP POST 请求
Args:
url: 请求 URL
data: 表单数据
json_data: JSON 数据
headers: 请求头
timeout: 超时时间(秒)
Returns:
响应数据JSON 解析后)
"""
try:
async with httpx.AsyncClient(timeout=float(timeout)) as client:
response = await client.post(
url,
data=data,
json=json_data,
headers=headers
)
self.log(f"HTTP POST {url} -> {response.status_code}")
try:
return response.json()
except:
return {"text": response.text, "status_code": response.status_code}
except Exception as e:
self.log(f"HTTP POST 失败: {str(e)}", level="ERROR")
raise
# ============ 变量存储 ============
# ==================== 变量存储 ====================
def get_var(self, key: str, default: Any = None) -> Any:
"""
获取存储的变量
"""获取持久化变量
Args:
key: 变量名
default: 默认值
Returns:
变量值JSON 解析后)
变量值
"""
db = self._get_db()
try:
result = db.execute(
text("SELECT var_value FROM platform_script_vars WHERE tenant_id = :tid AND var_key = :key"),
{"tid": self.tenant_id, "key": key}
)
row = result.first()
if row:
try:
return json.loads(row[0])
except:
return row[0]
return default
except Exception as e:
self.log(f"获取变量失败: {str(e)}", level="ERROR")
return default
from ..models.scheduled_task import ScriptVar
var = self.db.query(ScriptVar).filter(
ScriptVar.task_id == self.task_id,
ScriptVar.tenant_id == self.tenant_id,
ScriptVar.var_key == key
).first()
if var and var.var_value:
try:
return json.loads(var.var_value)
except:
return var.var_value
return default
def set_var(self, key: str, value: Any) -> bool:
"""
存储变量(跨执行持久化)
def set_var(self, key: str, value: Any) -> None:
"""设置持久化变量
Args:
key: 变量名
value: 变量值(会 JSON 序列化)
Returns:
是否成功
value: 变量值会JSON序列化
"""
db = self._get_db()
try:
value_str = json.dumps(value, ensure_ascii=False) if not isinstance(value, str) else value
# 使用 REPLACE INTO 实现 upsert
db.execute(
text("""
REPLACE INTO platform_script_vars (tenant_id, var_key, var_value, updated_at)
VALUES (:tid, :key, :value, NOW())
"""),
{"tid": self.tenant_id, "key": key, "value": value_str}
from ..models.scheduled_task import ScriptVar
var = self.db.query(ScriptVar).filter(
ScriptVar.task_id == self.task_id,
ScriptVar.tenant_id == self.tenant_id,
ScriptVar.var_key == key
).first()
value_json = json.dumps(value, ensure_ascii=False)
if var:
var.var_value = value_json
else:
var = ScriptVar(
task_id=self.task_id,
tenant_id=self.tenant_id,
var_key=key,
var_value=value_json
)
db.commit()
self.log(f"变量已存储: {key}")
return True
except Exception as e:
self.log(f"存储变量失败: {str(e)}", level="ERROR")
return False
self.db.add(var)
self.db.commit()
self.log(f"变量 {key} 已保存")
def delete_var(self, key: str) -> bool:
"""
删除变量
def del_var(self, key: str) -> bool:
"""删除持久化变量"""
from ..models.scheduled_task import ScriptVar
result = self.db.query(ScriptVar).filter(
ScriptVar.task_id == self.task_id,
ScriptVar.tenant_id == self.tenant_id,
ScriptVar.var_key == key
).delete()
self.db.commit()
return result > 0
# ==================== 租户配置 ====================
def get_tenants(self, app_code: Optional[str] = None) -> List[Dict]:
"""获取租户列表
Args:
key: 变量名
app_code: 可选,按应用代码筛选
Returns:
是否成功
租户列表 [{"tenant_id": ..., "tenant_name": ...}, ...]
"""
db = self._get_db()
try:
db.execute(
text("DELETE FROM platform_script_vars WHERE tenant_id = :tid AND var_key = :key"),
{"tid": self.tenant_id, "key": key}
)
db.commit()
self.log(f"变量已删除: {key}")
return True
except Exception as e:
self.log(f"删除变量失败: {str(e)}", level="ERROR")
return False
# ============ 多租户遍历 ============
def get_tenants(self, app_code: str = None) -> List[Dict]:
"""
获取租户列表(用于多租户任务遍历)
from ..models.tenant import Tenant
from ..models.tenant_app import TenantApp
Args:
app_code: 应用代码(可选),筛选订阅了该应用的租户
if app_code:
# 获取订阅了该应用的租户
tenant_ids = self.db.query(TenantApp.tenant_id).filter(
TenantApp.app_code == app_code,
TenantApp.status == 1
).all()
tenant_ids = [t[0] for t in tenant_ids]
Returns:
租户列表 [{"code": "xxx", "name": "租户名", "custom_configs": {...}}]
"""
db = self._get_db()
try:
if app_code:
# 筛选订阅了指定应用的租户
result = db.execute(
text("""
SELECT DISTINCT t.code, t.name, ta.custom_configs
FROM platform_tenants t
INNER JOIN platform_tenant_apps ta ON t.code = ta.tenant_id
WHERE ta.app_code = :app_code AND t.status = 1
"""),
{"app_code": app_code}
)
else:
# 获取所有启用的租户
result = db.execute(
text("SELECT code, name FROM platform_tenants WHERE status = 1")
)
tenants = []
for row in result.mappings().all():
tenant = dict(row)
# 解析 custom_configs
if "custom_configs" in tenant and tenant["custom_configs"]:
try:
tenant["custom_configs"] = json.loads(tenant["custom_configs"])
except:
pass
tenants.append(tenant)
self.log(f"获取租户列表成功,共 {len(tenants)} 个租户")
return tenants
except Exception as e:
self.log(f"获取租户列表失败: {str(e)}", level="ERROR")
return []
tenants = self.db.query(Tenant).filter(
Tenant.code.in_(tenant_ids),
Tenant.status == 'active'
).all()
else:
tenants = self.db.query(Tenant).filter(Tenant.status == 'active').all()
return [{"tenant_id": t.code, "tenant_name": t.name} for t in tenants]
def get_tenant_config(self, tenant_id: str, app_code: str, key: str = None) -> Any:
"""
获取指定租户的应用配置
def get_tenant_config(self, tenant_id: str, app_code: str, key: Optional[str] = None) -> Any:
"""获取租户的应用配置
Args:
tenant_id: 租户ID
app_code: 应用代码
key: 配置项键名(可选,不传返回全部配置)
key: 配置(可选,不提供则返回所有配置)
Returns:
配置值或配置字典
"""
db = self._get_db()
try:
result = db.execute(
text("""
SELECT custom_configs FROM platform_tenant_apps
WHERE tenant_id = :tenant_id AND app_code = :app_code
"""),
{"tenant_id": tenant_id, "app_code": app_code}
)
row = result.first()
if not row or not row[0]:
return None if key else {}
try:
configs = json.loads(row[0])
except:
configs = {}
if key:
return configs.get(key)
return configs
except Exception as e:
self.log(f"获取租户配置失败: {str(e)}", level="ERROR")
from ..models.tenant_app import TenantApp
tenant_app = self.db.query(TenantApp).filter(
TenantApp.tenant_id == tenant_id,
TenantApp.app_code == app_code
).first()
if not tenant_app:
return None if key else {}
# 解析 custom_configs
configs = {}
if hasattr(tenant_app, 'custom_configs') and tenant_app.custom_configs:
try:
configs = json.loads(tenant_app.custom_configs) if isinstance(tenant_app.custom_configs, str) else tenant_app.custom_configs
except:
pass
if key:
return configs.get(key)
return configs
def get_all_tenant_configs(self, app_code: str) -> List[Dict]:
"""
获取所有租户的应用配置(便捷方法,用于批量操作)
"""获取所有租户的应用配置
Args:
app_code: 应用代码
Returns:
[{"tenant_id": "xxx", "tenant_name": "租户名", "configs": {...}}]
[{"tenant_id": ..., "tenant_name": ..., "configs": {...}}, ...]
"""
db = self._get_db()
try:
result = db.execute(
text("""
SELECT t.code as tenant_id, t.name as tenant_name, ta.custom_configs
FROM platform_tenants t
INNER JOIN platform_tenant_apps ta ON t.code = ta.tenant_id
WHERE ta.app_code = :app_code AND t.status = 1
"""),
{"app_code": app_code}
)
from ..models.tenant import Tenant
from ..models.tenant_app import TenantApp
tenant_apps = self.db.query(TenantApp).filter(
TenantApp.app_code == app_code,
TenantApp.status == 1
).all()
result = []
for ta in tenant_apps:
tenant = self.db.query(Tenant).filter(Tenant.code == ta.tenant_id).first()
configs = {}
if hasattr(ta, 'custom_configs') and ta.custom_configs:
try:
configs = json.loads(ta.custom_configs) if isinstance(ta.custom_configs, str) else ta.custom_configs
except:
pass
tenants = []
for row in result.mappings().all():
configs = {}
if row["custom_configs"]:
try:
configs = json.loads(row["custom_configs"])
except:
pass
tenants.append({
"tenant_id": row["tenant_id"],
"tenant_name": row["tenant_name"],
"configs": configs
})
self.log(f"获取 {app_code} 应用的租户配置,共 {len(tenants)}")
return tenants
except Exception as e:
self.log(f"获取租户配置失败: {str(e)}", level="ERROR")
return []
result.append({
"tenant_id": ta.tenant_id,
"tenant_name": tenant.name if tenant else ta.tenant_id,
"configs": configs
})
return result
# ============ 密钥管理 ============
# ==================== 密钥管理 ====================
def get_secret(self, key: str) -> Optional[str]:
"""
获取密钥(优先读取租户级密钥,其次读取全局密钥)
"""获取密钥(优先租户级,其次全局)
Args:
key: 密钥名
key: 密钥名
Returns:
密钥值(如不存在返回 None
密钥值
"""
db = self._get_db()
try:
# 优先查询租户级密钥
result = db.execute(
text("""
SELECT secret_value FROM platform_secrets
WHERE (tenant_id = :tenant_id OR tenant_id IS NULL)
AND secret_key = :key
ORDER BY tenant_id DESC
LIMIT 1
"""),
{"tenant_id": self.tenant_id, "key": key}
)
row = result.first()
if row:
self.log(f"获取密钥成功: {key}")
return row[0]
self.log(f"密钥不存在: {key}", level="WARN")
return None
except Exception as e:
self.log(f"获取密钥失败: {str(e)}", level="ERROR")
return None
# ============ 日志 ============
def log(self, message: str, level: str = "INFO"):
"""
记录日志
from ..models.scheduled_task import Secret
Args:
message: 日志内容
level: 日志级别INFO, WARN, ERROR
"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_entry = f"[{timestamp}] [{level}] {message}"
self._logs.append(log_entry)
# 先查租户级
if self.tenant_id:
secret = self.db.query(Secret).filter(
Secret.tenant_id == self.tenant_id,
Secret.secret_key == key
).first()
if secret:
return secret.secret_value
# 同时输出到标准日志
if level == "ERROR":
logger.error(f"[Script {self.task_id}] {message}")
else:
logger.info(f"[Script {self.task_id}] {message}")
# 再查全局
secret = self.db.query(Secret).filter(
Secret.tenant_id.is_(None),
Secret.secret_key == key
).first()
# 写入 platform_logs
try:
db = self._get_db()
db.execute(
text("""
INSERT INTO platform_logs
(trace_id, app_code, module, level, message, created_at)
VALUES (:trace_id, :app_code, :module, :level, :message, NOW())
"""),
{
"trace_id": self.trace_id,
"app_code": "000-platform",
"module": "script",
"level": level,
"message": message[:2000] # 限制长度
}
)
db.commit()
except Exception as e:
logger.warning(f"Failed to write log to database: {e}")
def get_logs(self) -> List[str]:
"""获取所有日志"""
return self._logs.copy()
def cleanup(self):
"""清理资源"""
self._close_db()
return secret.secret_value if secret else None