"""脚本执行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