diff --git a/builtin/python/baihu/__init__.py b/builtin/python/baihu/__init__.py new file mode 100644 index 0000000..a271645 --- /dev/null +++ b/builtin/python/baihu/__init__.py @@ -0,0 +1,56 @@ +import os +from .notify import notify as _notify +from .env import ( + get_envs, + get_env, + add_envs, + add_env, + update_env, + delete_envs, + delete_env +) +from .task import ( + get_tasks, + get_task, + update_task, + delete_task, + execute_task, + stop_task, + get_last_results +) + +def notify(title, text): + """ + 发送内建通知。 + 会在调用时校验环境变量:BHPKG_NOTIFY_TOKEN, BHPKG_NOTIFY_CHANNEL + """ + _TOKEN = os.environ.get("BHPKG_NOTIFY_TOKEN") + _CHANNEL = os.environ.get("BHPKG_NOTIFY_CHANNEL") + + if not _TOKEN or not _CHANNEL: + missing = [] + if not _TOKEN: missing.append("BHPKG_NOTIFY_TOKEN") + if not _CHANNEL: missing.append("BHPKG_NOTIFY_CHANNEL") + + error_msg = f"缺少必要的环境变量以使用 baihu 模块: {', '.join(missing)}。请在白虎面板的任务设置中配置指定的 Key。" + raise RuntimeError(error_msg) + + return _notify(title, text) + +__all__ = [ + 'notify', + 'get_envs', + 'get_env', + 'add_envs', + 'add_env', + 'update_env', + 'delete_envs', + 'delete_env', + 'get_tasks', + 'get_task', + 'update_task', + 'delete_task', + 'execute_task', + 'stop_task', + 'get_last_results' +] diff --git a/builtin/python/baihu/env.py b/builtin/python/baihu/env.py new file mode 100644 index 0000000..c0a4564 --- /dev/null +++ b/builtin/python/baihu/env.py @@ -0,0 +1,133 @@ +import os +import json +import urllib.request +import urllib.error + +def _get_headers(): + token = os.environ.get("BHPKG_OPENAPI_TOKEN") or os.environ.get("OPENAPI_TOKEN") or os.environ.get("BHPKG_NOTIFY_TOKEN") + if not token: + raise RuntimeError("没有正确配置或缺少 BHPKG_OPENAPI_TOKEN 环境变量以使用 env 函数。请在白虎面板的任务设置中配置这些 Key。") + return { + "Content-Type": "application/json", + "Authorization": f"Bearer {token}" + } + +def _get_envs_url(): + url = os.environ.get("BHPKG_OPENAPI_URL") or os.environ.get("OPENAPI_URL") + if url: + return url + + notify_url = os.environ.get("BHPKG_NOTIFY_URL", "http://localhost:8052/api/v1/notify/send") + for target in ["/api/v1/notify/send/", "/api/v1/notify/send", "/api/v1/notify/", "/api/v1/notify"]: + if target in notify_url: + return notify_url.replace(target, "/open2api/v1/env") + + return "http://localhost:8052/open2api/v1/env" + +def _request(url, method="GET", data=None): + headers = _get_headers() + payload = None + if data is not None: + payload = json.dumps(data).encode("utf-8") + + req = urllib.request.Request(url, data=payload, headers=headers, method=method) + try: + with urllib.request.urlopen(req) as resp: + body = resp.read().decode("utf-8") + if not body: + return {} + parsed = json.loads(body) + if isinstance(parsed, dict) and parsed.get("code") is not None and parsed.get("code") != 200: + msg = parsed.get("msg") or parsed.get("message") or "未知错误" + raise RuntimeError(f"请求失败 [{parsed.get('code')}]: {msg}") + return parsed + except urllib.error.HTTPError as e: + err_body = e.read().decode("utf-8") + try: + err_json = json.loads(err_body) + msg = err_json.get("msg") or err_json.get("message") or err_body + except Exception: + msg = err_body + raise RuntimeError(f"请求失败 [{e.code}]: {msg}") + except Exception as e: + raise RuntimeError(f"请求发生异常: {e}") + +def get_envs(): + """ + 获取所有的环境变量列表。 + """ + url = f"{_get_envs_url()}/all" + res = _request(url, "GET") + return res.get("data", []) + +def get_env(name): + """ + 根据变量名获取环境变量。如果不存在则返回 None。 + """ + envs = get_envs() + for env in envs: + if env.get("name") == name: + return env + return None + +def add_envs(envs_list): + """ + 批量添加环境变量。 + envs_list: 包含环境变量字典的列表,如 [{"name": "KEY", "value": "VAL", "remark": "备注"}] + """ + url = _get_envs_url() + added_envs = [] + for env in envs_list: + if "name" not in env or "value" not in env: + raise ValueError("环境变量必须包含 'name' 和 'value'") + res = _request(url, "POST", env) + if "data" in res: + added_envs.append(res["data"]) + return added_envs + +def add_env(name, value, remark="", type="normal", hidden=True, enabled=True): + """ + 添加单个环境变量。 + """ + url = _get_envs_url() + payload = { + "name": name, + "value": value, + "remark": remark, + "type": type, + "hidden": hidden, + "enabled": enabled + } + res = _request(url, "POST", payload) + return res.get("data") + +def update_env(id, name, value, remark=None, type=None, hidden=None, enabled=None): + """ + 根据 ID 更新环境变量。 + """ + url = f"{_get_envs_url()}/{id}" + payload = {} + if name is not None: payload["name"] = name + if value is not None: payload["value"] = value + if remark is not None: payload["remark"] = remark + if type is not None: payload["type"] = type + if hidden is not None: payload["hidden"] = hidden + if enabled is not None: payload["enabled"] = enabled + + res = _request(url, "PUT", payload) + return res.get("data") + +def delete_envs(ids): + """ + 批量删除环境变量。 + """ + for fid in ids: + delete_env(fid) + +def delete_env(id): + """ + 根据 ID 删除指定的环境变量。 + """ + url = f"{_get_envs_url()}/{id}" + _request(url, "DELETE") + return True diff --git a/builtin/python/baihu/notify.py b/builtin/python/baihu/notify.py new file mode 100644 index 0000000..bd96b77 --- /dev/null +++ b/builtin/python/baihu/notify.py @@ -0,0 +1,32 @@ +import os +import json +import urllib.request + +def notify(title, text, channel_id=None): + """ + 发送内建通知。 + """ + token = os.environ.get("BHPKG_NOTIFY_TOKEN") + url = os.environ.get("BHPKG_NOTIFY_URL", "http://localhost:8052/api/v1/notify/send") + default_channel = os.environ.get("BHPKG_NOTIFY_CHANNEL") + + cid = channel_id or default_channel + + if not url or not token or not cid: + return + + payload = { + "channel_id": cid, + "title": title, + "text": text + } + + data = json.dumps(payload).encode('utf-8') + req = urllib.request.Request(url, data=data, method='POST') + req.add_header('Content-Type', 'application/json') + req.add_header('notify-token', token) + + + with urllib.request.urlopen(req) as resp: + return resp.read().decode('utf-8') + diff --git a/builtin/python/baihu/task.py b/builtin/python/baihu/task.py new file mode 100644 index 0000000..05bc618 --- /dev/null +++ b/builtin/python/baihu/task.py @@ -0,0 +1,128 @@ +import os +import json +import urllib.request +import urllib.error + +def _get_headers(): + token = os.environ.get("BHPKG_OPENAPI_TOKEN") or os.environ.get("OPENAPI_TOKEN") or os.environ.get("BHPKG_NOTIFY_TOKEN") + if not token: + raise RuntimeError("没有正确配置或缺少 BHPKG_OPENAPI_TOKEN 环境变量以使用 task 函数。请在白虎面板的任务设置中配置这些 Key。") + return { + "Content-Type": "application/json", + "Authorization": f"Bearer {token}" + } + +def _get_base_url(): + url = os.environ.get("BHPKG_OPENAPI_URL") or os.environ.get("OPENAPI_URL") + if url: + # If openapi_url ends with /env, replace it with nothing or use base + if url.endswith("/env"): + return url[:-4] + elif url.endswith("/env/"): + return url[:-5] + return url + + notify_url = os.environ.get("BHPKG_NOTIFY_URL", "http://localhost:8052/api/v1/notify/send") + for target in ["/api/v1/notify/send/", "/api/v1/notify/send", "/api/v1/notify/", "/api/v1/notify"]: + if target in notify_url: + return notify_url.replace(target, "/open2api/v1") + + return "http://localhost:8052/open2api/v1" + +def _request(url, method="GET", data=None): + headers = _get_headers() + payload = None + if data is not None: + payload = json.dumps(data).encode("utf-8") + + req = urllib.request.Request(url, data=payload, headers=headers, method=method) + try: + with urllib.request.urlopen(req) as resp: + body = resp.read().decode("utf-8") + if not body: + return {} + parsed = json.loads(body) + if isinstance(parsed, dict) and parsed.get("code") is not None and parsed.get("code") != 200: + msg = parsed.get("msg") or parsed.get("message") or "未知错误" + raise RuntimeError(f"请求失败 [{parsed.get('code')}]: {msg}") + return parsed + except urllib.error.HTTPError as e: + err_body = e.read().decode("utf-8") + try: + err_json = json.loads(err_body) + msg = err_json.get("msg") or err_json.get("message") or err_body + except Exception: + msg = err_body + raise RuntimeError(f"请求失败 [{e.code}]: {msg}") + except Exception as e: + raise RuntimeError(f"请求发生异常: {e}") + +def get_tasks(): + """ + 获取全部任务列表。 + """ + url = f"{_get_base_url()}/tasks" + res = _request(url, "GET") + return res.get("data", []) + +def get_task(id): + """ + 根据 ID 获取单个任务的详细信息。 + """ + url = f"{_get_base_url()}/tasks/{id}" + res = _request(url, "GET") + return res.get("data") + +def update_task(id, name=None, command=None, remark=None, pin_type=None, trigger_type=None, schedule=None, timeout=None, work_dir=None, retry_count=None, retry_interval=None, random_range=None, enabled=None): + """ + 根据 ID 更新任务。 + """ + url = f"{_get_base_url()}/tasks/{id}" + payload = {} + if name is not None: payload["name"] = name + if command is not None: payload["command"] = command + if remark is not None: payload["remark"] = remark + if pin_type is not None: payload["pin_type"] = pin_type + if trigger_type is not None: payload["trigger_type"] = trigger_type + if schedule is not None: payload["schedule"] = schedule + if timeout is not None: payload["timeout"] = timeout + if work_dir is not None: payload["work_dir"] = work_dir + if retry_count is not None: payload["retry_count"] = retry_count + if retry_interval is not None: payload["retry_interval"] = retry_interval + if random_range is not None: payload["random_range"] = random_range + if enabled is not None: payload["enabled"] = enabled + + res = _request(url, "PUT", payload) + return res.get("data") + +def delete_task(id): + """ + 根据 ID 删除指定任务。 + """ + url = f"{_get_base_url()}/tasks/{id}" + _request(url, "DELETE") + return True + +def execute_task(id): + """ + 触发执行特定任务。 + """ + url = f"{_get_base_url()}/execute/task/{id}" + res = _request(url, "POST") + return res.get("data") + +def stop_task(log_id): + """ + 根据日志 ID 停止正在运行的任务。 + """ + url = f"{_get_base_url()}/tasks/stop/{log_id}" + res = _request(url, "POST") + return res.get("data") + +def get_last_results(): + """ + 获取最近的执行结果列表。 + """ + url = f"{_get_base_url()}/execute/results" + res = _request(url, "GET") + return res.get("data", [])