feat: 脚本执行平台增强功能
Some checks failed
continuous-integration/drone/push Build is failing

- 新增重试和失败告警功能(支持自动重试N次,失败后钉钉/企微通知)
- 新增密钥管理(安全存储API Key等敏感信息)
- 新增脚本模板库(预置常用脚本模板)
- 新增脚本版本管理(自动保存历史版本,支持回滚)
- 新增执行统计(成功率、平均耗时、7日趋势)
- SDK 新增多租户遍历能力(get_tenants/get_tenant_config/get_all_tenant_configs)
- SDK 新增密钥读取方法(get_secret)
This commit is contained in:
2026-01-28 11:59:50 +08:00
parent 644255891e
commit 9b72e6127f
4 changed files with 1142 additions and 28 deletions

View File

@@ -24,6 +24,8 @@ class ScriptSDK:
- HTTP 请求
- 变量存储(跨执行持久化)
- 日志记录
- 多租户遍历
- 密钥管理
"""
def __init__(self, tenant_id: str, task_id: int, trace_id: str = None):
@@ -32,6 +34,7 @@ class ScriptSDK:
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 # 租户列表缓存
def _get_db(self) -> Session:
"""获取数据库会话"""
@@ -393,6 +396,171 @@ class ScriptSDK:
self.log(f"删除变量失败: {str(e)}", level="ERROR")
return False
# ============ 多租户遍历 ============
def get_tenants(self, app_code: str = None) -> List[Dict]:
"""
获取租户列表(用于多租户任务遍历)
Args:
app_code: 应用代码(可选),筛选订阅了该应用的租户
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 []
def get_tenant_config(self, tenant_id: str, app_code: str, key: str = None) -> Any:
"""
获取指定租户的应用配置
Args:
tenant_id: 租户ID
app_code: 应用代码
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")
return None if key else {}
def get_all_tenant_configs(self, app_code: str) -> List[Dict]:
"""
获取所有租户的应用配置(便捷方法,用于批量操作)
Args:
app_code: 应用代码
Returns:
[{"tenant_id": "xxx", "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}
)
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 []
# ============ 密钥管理 ============
def get_secret(self, key: str) -> Optional[str]:
"""
获取密钥(优先读取租户级密钥,其次读取全局密钥)
Args:
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"):