- 扩展 ToolConfig 配置类型,新增 external_api 类型
- 实现接口注册表,包含 90+ 睿美云开放接口定义
- 实现 TPOS SHA256WithRSA 签名鉴权
- 实现睿美云 API 客户端,支持多租户配置
- 新增代理路由 /api/ruimeiyun/call/{api_name}
- 支持接口权限控制和健康检查
This commit is contained in:
@@ -1,479 +1,479 @@
|
||||
"""脚本执行SDK - 为Python脚本提供内置功能"""
|
||||
import json
|
||||
import os
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class ScriptSDK:
|
||||
"""脚本SDK - 提供AI、通知、数据库、HTTP、变量存储等功能"""
|
||||
|
||||
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.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_param(self, key: str, default: Any = None) -> Any:
|
||||
"""获取任务参数
|
||||
|
||||
Args:
|
||||
key: 参数名
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
参数值
|
||||
"""
|
||||
return self.params.get(key, default)
|
||||
|
||||
def get_params(self) -> Dict[str, Any]:
|
||||
"""获取所有任务参数
|
||||
|
||||
Returns:
|
||||
所有参数字典
|
||||
"""
|
||||
return self.params.copy()
|
||||
|
||||
# ==================== 日志 ====================
|
||||
|
||||
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: 温度参数
|
||||
max_tokens: 最大token数
|
||||
|
||||
Returns:
|
||||
AI响应内容
|
||||
"""
|
||||
model = model or self._ai_model
|
||||
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=60) as client:
|
||||
response = client.post(
|
||||
f"{self._ai_base_url}/chat/completions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._ai_api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
content = data['choices'][0]['message']['content']
|
||||
self.log(f"AI调用成功: {len(content)} 字符")
|
||||
return content
|
||||
except Exception as e:
|
||||
self.log(f"AI调用失败: {str(e)}", 'ERROR')
|
||||
raise
|
||||
|
||||
# ==================== 通知 ====================
|
||||
|
||||
def dingtalk(self, webhook: str, content: str, title: Optional[str] = None, at_all: bool = False) -> bool:
|
||||
"""发送钉钉消息
|
||||
|
||||
Args:
|
||||
webhook: 钉钉机器人webhook地址
|
||||
content: 消息内容(支持Markdown)
|
||||
title: 消息标题
|
||||
at_all: 是否@所有人
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": title or "通知",
|
||||
"text": content + ("\n@所有人" if at_all else "")
|
||||
},
|
||||
"at": {"isAtAll": at_all}
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=10) as client:
|
||||
response = client.post(webhook, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
success = result.get('errcode') == 0
|
||||
self.log(f"钉钉消息发送{'成功' if success else '失败'}")
|
||||
return success
|
||||
except Exception as e:
|
||||
self.log(f"钉钉消息发送失败: {str(e)}", 'ERROR')
|
||||
return False
|
||||
|
||||
def wecom(self, webhook: str, content: str, msg_type: str = 'markdown') -> bool:
|
||||
"""发送企业微信消息
|
||||
|
||||
Args:
|
||||
webhook: 企微机器人webhook地址
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 (text, markdown)
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
if msg_type == 'markdown':
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {"content": content}
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"msgtype": "text",
|
||||
"text": {"content": content}
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=10) as client:
|
||||
response = client.post(webhook, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
success = result.get('errcode') == 0
|
||||
self.log(f"企微消息发送{'成功' if success else '失败'}")
|
||||
return success
|
||||
except Exception as e:
|
||||
self.log(f"企微消息发送失败: {str(e)}", 'ERROR')
|
||||
return False
|
||||
|
||||
# ==================== HTTP 请求 ====================
|
||||
|
||||
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": "..."}
|
||||
"""
|
||||
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: 参数字典
|
||||
|
||||
Returns:
|
||||
查询结果列表
|
||||
"""
|
||||
sql_upper = sql.strip().upper()
|
||||
if not sql_upper.startswith('SELECT'):
|
||||
raise ValueError("只允许执行SELECT查询")
|
||||
|
||||
# 禁止危险操作
|
||||
forbidden = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'TRUNCATE', 'ALTER', 'CREATE']
|
||||
for word in forbidden:
|
||||
if word in sql_upper:
|
||||
raise ValueError(f"禁止执行 {word} 操作")
|
||||
|
||||
try:
|
||||
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)}", 'ERROR')
|
||||
raise
|
||||
|
||||
# ==================== 变量存储 ====================
|
||||
|
||||
def get_var(self, key: str, default: Any = None) -> Any:
|
||||
"""获取持久化变量
|
||||
|
||||
Args:
|
||||
key: 变量名
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
变量值
|
||||
"""
|
||||
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) -> None:
|
||||
"""设置持久化变量
|
||||
|
||||
Args:
|
||||
key: 变量名
|
||||
value: 变量值(会JSON序列化)
|
||||
"""
|
||||
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
|
||||
)
|
||||
self.db.add(var)
|
||||
|
||||
self.db.commit()
|
||||
self.log(f"变量 {key} 已保存")
|
||||
|
||||
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:
|
||||
app_code: 可选,按应用代码筛选
|
||||
|
||||
Returns:
|
||||
租户列表 [{"tenant_id": ..., "tenant_name": ...}, ...]
|
||||
"""
|
||||
from ..models.tenant import Tenant
|
||||
from ..models.tenant_app import TenantApp
|
||||
|
||||
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]
|
||||
|
||||
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: Optional[str] = None) -> Any:
|
||||
"""获取租户的应用配置
|
||||
|
||||
Args:
|
||||
tenant_id: 租户ID
|
||||
app_code: 应用代码
|
||||
key: 配置键(可选,不提供则返回所有配置)
|
||||
|
||||
Returns:
|
||||
配置值或配置字典
|
||||
"""
|
||||
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": ..., "tenant_name": ..., "configs": {...}}, ...]
|
||||
"""
|
||||
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
|
||||
|
||||
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: 密钥名
|
||||
|
||||
Returns:
|
||||
密钥值
|
||||
"""
|
||||
from ..models.scheduled_task import Secret
|
||||
|
||||
# 先查租户级
|
||||
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
|
||||
|
||||
# 再查全局
|
||||
secret = self.db.query(Secret).filter(
|
||||
Secret.tenant_id.is_(None),
|
||||
Secret.secret_key == key
|
||||
).first()
|
||||
|
||||
return secret.secret_value if secret else None
|
||||
"""脚本执行SDK - 为Python脚本提供内置功能"""
|
||||
import json
|
||||
import os
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
class ScriptSDK:
|
||||
"""脚本SDK - 提供AI、通知、数据库、HTTP、变量存储等功能"""
|
||||
|
||||
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.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_param(self, key: str, default: Any = None) -> Any:
|
||||
"""获取任务参数
|
||||
|
||||
Args:
|
||||
key: 参数名
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
参数值
|
||||
"""
|
||||
return self.params.get(key, default)
|
||||
|
||||
def get_params(self) -> Dict[str, Any]:
|
||||
"""获取所有任务参数
|
||||
|
||||
Returns:
|
||||
所有参数字典
|
||||
"""
|
||||
return self.params.copy()
|
||||
|
||||
# ==================== 日志 ====================
|
||||
|
||||
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: 温度参数
|
||||
max_tokens: 最大token数
|
||||
|
||||
Returns:
|
||||
AI响应内容
|
||||
"""
|
||||
model = model or self._ai_model
|
||||
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=60) as client:
|
||||
response = client.post(
|
||||
f"{self._ai_base_url}/chat/completions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._ai_api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
content = data['choices'][0]['message']['content']
|
||||
self.log(f"AI调用成功: {len(content)} 字符")
|
||||
return content
|
||||
except Exception as e:
|
||||
self.log(f"AI调用失败: {str(e)}", 'ERROR')
|
||||
raise
|
||||
|
||||
# ==================== 通知 ====================
|
||||
|
||||
def dingtalk(self, webhook: str, content: str, title: Optional[str] = None, at_all: bool = False) -> bool:
|
||||
"""发送钉钉消息
|
||||
|
||||
Args:
|
||||
webhook: 钉钉机器人webhook地址
|
||||
content: 消息内容(支持Markdown)
|
||||
title: 消息标题
|
||||
at_all: 是否@所有人
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"title": title or "通知",
|
||||
"text": content + ("\n@所有人" if at_all else "")
|
||||
},
|
||||
"at": {"isAtAll": at_all}
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=10) as client:
|
||||
response = client.post(webhook, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
success = result.get('errcode') == 0
|
||||
self.log(f"钉钉消息发送{'成功' if success else '失败'}")
|
||||
return success
|
||||
except Exception as e:
|
||||
self.log(f"钉钉消息发送失败: {str(e)}", 'ERROR')
|
||||
return False
|
||||
|
||||
def wecom(self, webhook: str, content: str, msg_type: str = 'markdown') -> bool:
|
||||
"""发送企业微信消息
|
||||
|
||||
Args:
|
||||
webhook: 企微机器人webhook地址
|
||||
content: 消息内容
|
||||
msg_type: 消息类型 (text, markdown)
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
if msg_type == 'markdown':
|
||||
payload = {
|
||||
"msgtype": "markdown",
|
||||
"markdown": {"content": content}
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"msgtype": "text",
|
||||
"text": {"content": content}
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=10) as client:
|
||||
response = client.post(webhook, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
success = result.get('errcode') == 0
|
||||
self.log(f"企微消息发送{'成功' if success else '失败'}")
|
||||
return success
|
||||
except Exception as e:
|
||||
self.log(f"企微消息发送失败: {str(e)}", 'ERROR')
|
||||
return False
|
||||
|
||||
# ==================== HTTP 请求 ====================
|
||||
|
||||
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": "..."}
|
||||
"""
|
||||
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: 参数字典
|
||||
|
||||
Returns:
|
||||
查询结果列表
|
||||
"""
|
||||
sql_upper = sql.strip().upper()
|
||||
if not sql_upper.startswith('SELECT'):
|
||||
raise ValueError("只允许执行SELECT查询")
|
||||
|
||||
# 禁止危险操作
|
||||
forbidden = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'TRUNCATE', 'ALTER', 'CREATE']
|
||||
for word in forbidden:
|
||||
if word in sql_upper:
|
||||
raise ValueError(f"禁止执行 {word} 操作")
|
||||
|
||||
try:
|
||||
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)}", 'ERROR')
|
||||
raise
|
||||
|
||||
# ==================== 变量存储 ====================
|
||||
|
||||
def get_var(self, key: str, default: Any = None) -> Any:
|
||||
"""获取持久化变量
|
||||
|
||||
Args:
|
||||
key: 变量名
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
变量值
|
||||
"""
|
||||
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) -> None:
|
||||
"""设置持久化变量
|
||||
|
||||
Args:
|
||||
key: 变量名
|
||||
value: 变量值(会JSON序列化)
|
||||
"""
|
||||
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
|
||||
)
|
||||
self.db.add(var)
|
||||
|
||||
self.db.commit()
|
||||
self.log(f"变量 {key} 已保存")
|
||||
|
||||
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:
|
||||
app_code: 可选,按应用代码筛选
|
||||
|
||||
Returns:
|
||||
租户列表 [{"tenant_id": ..., "tenant_name": ...}, ...]
|
||||
"""
|
||||
from ..models.tenant import Tenant
|
||||
from ..models.tenant_app import TenantApp
|
||||
|
||||
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]
|
||||
|
||||
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: Optional[str] = None) -> Any:
|
||||
"""获取租户的应用配置
|
||||
|
||||
Args:
|
||||
tenant_id: 租户ID
|
||||
app_code: 应用代码
|
||||
key: 配置键(可选,不提供则返回所有配置)
|
||||
|
||||
Returns:
|
||||
配置值或配置字典
|
||||
"""
|
||||
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": ..., "tenant_name": ..., "configs": {...}}, ...]
|
||||
"""
|
||||
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
|
||||
|
||||
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: 密钥名
|
||||
|
||||
Returns:
|
||||
密钥值
|
||||
"""
|
||||
from ..models.scheduled_task import Secret
|
||||
|
||||
# 先查租户级
|
||||
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
|
||||
|
||||
# 再查全局
|
||||
secret = self.db.query(Secret).filter(
|
||||
Secret.tenant_id.is_(None),
|
||||
Secret.secret_key == key
|
||||
).first()
|
||||
|
||||
return secret.secret_value if secret else None
|
||||
|
||||
Reference in New Issue
Block a user