feat: add baihu notify

This commit is contained in:
duorameng
2026-04-16 21:17:09 +08:00
parent e6864373bc
commit 3f5d11a8ef
13 changed files with 382 additions and 123 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ agent/config.ini
agent/agent.pid
agent/baihu-agent
web/dist/
web/dev-dist/
# IDE
.idea/
+57
View File
@@ -0,0 +1,57 @@
const http = require('http');
const https = require('https');
const { URL } = require('url');
/**
* 环境变量强校验:导入期进行
*/
const TOKEN = process.env.BHPKG_NOTIFY_TOKEN;
const CHANNEL = process.env.BHPKG_NOTIFY_CHANNEL;
if (!TOKEN || !CHANNEL) {
const missing = [];
if (!TOKEN) missing.push("BHPKG_NOTIFY_TOKEN");
if (!CHANNEL) missing.push("BHPKG_NOTIFY_CHANNEL");
throw new Error(`缺少必要的环境变量以使用 baihu 模块: ${missing.join(", ")}。请在白虎面板的任务设置中配置这些 Key。`);
}
/**
* 发送通知的辅助函数 (仅使用 Node.js 标准库)
*/
function notify(title, text, channelId) {
const notifyUrl = process.env.BHPKG_NOTIFY_URL || 'http://localhost:8052/api/v1/notify/send';
const cid = channelId || CHANNEL;
if (!notifyUrl || !TOKEN || !cid) return;
try {
const parsedUrl = new URL(notifyUrl);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const data = JSON.stringify({
channel_id: cid,
title: title || '系统通知',
text: text
});
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + (parsedUrl.search || ''),
method: 'POST',
headers: {
'Content-Type': 'application/json',
'notify-token': TOKEN,
'Content-Length': Buffer.byteLength(data)
}
};
const req = protocol.request(options);
req.on('error', (e) => {});
req.write(data);
req.end();
} catch (e) {}
}
module.exports = { notify };
+7
View File
@@ -0,0 +1,7 @@
{
"name": "baihu",
"version": "1.0.0",
"description": "Baihu Panel internal helper for Node.js",
"main": "index.js",
"license": "MIT"
}
+9
View File
@@ -0,0 +1,9 @@
from setuptools import setup, find_packages
setup(
name='baihu',
version='1.0.0',
description='Baihu Panel internal helper for Python',
packages=find_packages(),
python_requires='>=3.6',
)
+87
View File
@@ -0,0 +1,87 @@
package builtininstall
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/utils"
)
// Run 执行内建包安装逻辑
func Run(args []string) {
logger.Infof("[Builtin] 开始为 mise 环境安装内建包...")
// 1. 确定内建包路径
// 优先使用 /www/builtin (Docker 环境),否则尝试相对于二进制文件的当前目录
builtinPath := "/www/builtin"
if _, err := os.Stat(builtinPath); os.IsNotExist(err) {
// 回退到当前目录下的 builtin
pwd, _ := os.Getwd()
builtinPath = filepath.Join(pwd, "builtin")
}
if _, err := os.Stat(builtinPath); os.IsNotExist(err) {
logger.Errorf("[Builtin] 找不到内建包目录: %s", builtinPath)
return
}
// 2. 安装 Node.js 包
installForLanguage("node", filepath.Join(builtinPath, "nodejs"), "npm install")
// 3. 安装 Python 包
installForLanguage("python", filepath.Join(builtinPath, "python"), "pip install -e")
logger.Infof("[Builtin] 内建包安装流程完成")
}
func installForLanguage(lang, pkgPath, installBaseCmd string) {
if _, err := os.Stat(pkgPath); os.IsNotExist(err) {
logger.Warnf("[Builtin] %s 的内建包目录不存在: %s", lang, pkgPath)
return
}
versions, err := utils.ListMiseInstalledVersions(lang)
if err != nil {
logger.Errorf("[Builtin] 获取 %s 的 mise 版本列表失败: %v", lang, err)
return
}
if len(versions) == 0 {
logger.Infof("[Builtin] 未发现已安装的 %s 版本,跳过", lang)
return
}
for _, v := range versions {
logger.Infof("[Builtin] 正在为 %s@%s 安装内建包...", lang, v)
var cmdStr string
if lang == "node" {
// npm install 不需要 -e
cmdStr = fmt.Sprintf("npm install %s", pkgPath)
} else {
// python 建议使用 -e (editable) 或直接安装
cmdStr = fmt.Sprintf("pip install -e %s", pkgPath)
}
// 使用 mise exec 跨版本执行
fullCmd := utils.BuildMiseCommandSimple(cmdStr, lang, v)
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", fullCmd)
} else {
cmd = exec.Command("sh", "-c", fullCmd)
}
out, err := cmd.CombinedOutput()
if err != nil {
logger.Errorf("[Builtin] 为 %s@%s 安装失败: %v\n输出: %s", lang, v, err, string(out))
} else {
logger.Infof("[Builtin] 为 %s@%s 安装成功", lang, v)
}
}
}
+5 -3
View File
@@ -1,6 +1,7 @@
package cmd
import (
"github.com/engigu/baihu-panel/cmd/builtininstall"
"github.com/engigu/baihu-panel/cmd/reposync"
"github.com/engigu/baihu-panel/cmd/resetpwd"
"github.com/engigu/baihu-panel/cmd/restore"
@@ -12,8 +13,9 @@ type CommandHandler func(args []string)
// Handlers 维护了除了 server 之外的命令的执行入口
var Handlers = map[string]CommandHandler{
"reposync": reposync.Run,
"resetpwd": resetpwd.Run,
"restore": restore.Run,
"reposync": reposync.Run,
"resetpwd": resetpwd.Run,
"restore": restore.Run,
"builtininstall": builtininstall.Run,
// "migrate": migrate.Run,
}
+1
View File
@@ -154,6 +154,7 @@ COPY --from=frontend-builder /app/web/dist /www/baihu
# Copy configs and entrypoint
COPY --from=backend-builder /app/configs ./configs
COPY --from=backend-builder /app/example ./example
COPY builtin/ /www/builtin
COPY docker/docker-entrypoint.sh .
+44 -35
View File
@@ -35,56 +35,65 @@
---
### 路径二:脚本手动调用 (API)
### 路径二:脚本手动调用 (内置助手库 - 推荐)
如果您需要在脚本逻辑内部(例如:当抓取到特定数据时)主动触发通知,可以使用此方式
白虎面板提供了一套**零配置**的内建助手库(Built-in SDK),支持 Python 和 Node.js。它会自动读取系统注入的环境变量,让您在脚本中只需一行代码即可实现通知投递
#### 1. 环境初始化
在开始编写脚本前,您需要在终端执行以下命令,为面板管理的所有语言环境安装 `baihu` 包:
```bash
baihu builtininstall
```
*该操作会将助手库安装到 mise 管理的所有版本中,确保 import 成功。*
#### 2. 代码示例
##### Python (同步调用)
```python
import baihu
# 内部自动通过环境变量鉴权,无需填 TOKEN 和 URL
baihu.notify("任务标题", "通知正文内容")
```
##### Node.js (异步调用)
```javascript
const baihu = require('baihu');
// 极简调用,支持在 CommonJS/ESM 中使用
baihu.notify("任务标题", "通知正文内容");
```
---
### 路径三:其他语言/高级调用 (原始 API)
如果您使用 Shell 或其他尚未提供助手库的语言,可以通过标准 HTTP POST 请求调用。
#### 1. 快速获取代码
为了极大降低集成门槛,面板内置了代码生成器:
- 进入 **「消息推送」** -> **「脚本调用」** 标签。
- 页面会根据您已配置的渠道,自动生成包含 **通知 Token****渠道 ID** 的完整代码示例。
- 支持 **Python**、**Node.js** 和 **Shell (Curl)** 格式,直接复制即可使用。
进入 **「消息推送」** -> **「脚本调用说明」** 标签,页面会根据您的配置自动生成包含 **通知 Token****默认渠道 ID** 的完整代码。
#### 2. 代码参考示例
如果您需要手动编写逻辑,请参考以下实现:
##### Python 示例
##### Shell (Curl)
```bash
curl -X POST "http://localhost:8052/api/v1/notify/send" \
-H "notify-token: 您的_NOTIFY_TOKEN" \
-d '{"channel_id":"渠道ID", "title":"标题", "text":"内容"}'
```
##### 基础 Python (requests)
```python
import requests
def send_notify(title, content):
url = "http://localhost:8052/api/v1/notify/send"
headers = { "notify-token": "您的_NOTIFY_TOKEN" }
data = {
"channel_id": "您的_渠道_ID",
"title": title,
"text": content
}
data = {"channel_id": "您的_渠道_ID", "title": title, "text": content}
requests.post(url, headers=headers, json=data)
```
##### Node.js 示例
```javascript
const axios = require('axios');
async function sendNotify(title, content) {
await axios.post('http://localhost:8052/api/v1/notify/send', {
channel_id: '您的_渠道_ID',
title: title,
text: content
}, {
headers: { 'notify-token': '您的_NOTIFY_TOKEN' }
});
}
```
##### Shell 示例
```bash
curl -X POST "http://localhost:8052/api/v1/notify/send" \
-H "notify-token: 您的_NOTIFY_TOKEN" \
-d '{"channel_id":"渠道ID", "title":"标题", "text":"内容"}'
```
---
## 消息中心管理
+26
View File
@@ -0,0 +1,26 @@
const baihu = require('baihu');
/**
* 内建通知测试示例 (Node.js)
*
* 使用说明:
* 1. 确保已在该 Node.js 环境下安装过内建包。
* 2. 系统会自动注入 BHPKG_NOTIFY_TOKEN 和 BHPKG_NOTIFY_CHANNEL。
*/
console.log("正在尝试发送 Node.js 内建通知...");
try {
// 简单的一行代码即可完成推送
baihu.notify(
"Node.js 任务提醒",
"这是一条来自 Node.js 示例脚本的通知消息。无需配置 API 地址或 Token。"
);
console.log("发送请求已提交。");
console.log("提示:内建包采用异步非阻塞发送,不会干扰主逻辑执行。");
} catch (e) {
console.error(`通知失败: ${e.message}`);
console.error("请检查环境变量是否注入,或是否已运行过 'baihu builtininstall'。");
}
+30
View File
@@ -0,0 +1,30 @@
import baihu
# 内建通知测试示例 (Python)
#
# 前提条件:
# 1. 已经在环境中安装了 baihu 包(例如通过 `baihu builtininstall`
# 2. 环境中已注入有效环境变量:
# - BHPKG_NOTIFY_TOKEN
# - BHPKG_NOTIFY_CHANNEL
def main():
print("正在尝试发送 Python 内建通知...")
try:
# 调用内建 notify 函数
# 内部会自动使用 BHPKG_NOTIFY_TOKEN & BHPKG_NOTIFY_CHANNEL 进行鉴权和投递
response = baihu.notify(
title="Python 任务提醒",
text="这是一条来自 Python 示例脚本的通知消息。调用非常简单!"
)
print("发送请求已处理。")
if response:
print(f"服务器响应: {response}")
except ImportError as e:
print(f"错误: 库未正确加载。请确保已执行过环境初始化。详情: {e}")
except Exception as e:
print(f"发送过程发生异常: {e}")
if __name__ == "__main__":
main()
+4
View File
@@ -24,4 +24,8 @@ var Commands = []CommandInfo{
Name: "restore",
Description: "从本地 zip 文件中全量恢复系统级备份数据",
},
{
Name: "builtininstall",
Description: "为所有 mise 管理的 Node.js 和 Python 环境安装内建助手库",
},
}
+28
View File
@@ -117,3 +117,31 @@ func BuildMiseCommandArgsSimple(cmdArgs []string, language, version string) []st
}
return append([]string{"mise", "exec", spec, "--"}, cmdArgs...)
}
// ListMiseInstalledVersions 获取指定语言已安装的所有版本列表
func ListMiseInstalledVersions(language string) ([]string, error) {
// 执行 mise ls <language> 命令
cmd := exec.Command("mise", "ls", language)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
var versions []string
lines := strings.Split(string(out), "\n")
for _, line := range lines {
v := strings.TrimSpace(line)
if v == "" {
continue
}
// mise ls 的输出类似:
// 3.12.1
// 3.11.5
// 我们取第一个字段即可
fields := strings.Fields(v)
if len(fields) > 0 {
versions = append(versions, fields[0])
}
}
return versions, nil
}
+83 -84
View File
@@ -332,75 +332,106 @@ const currentExample = computed(() => {
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- API 接口规格 -->
<Card class="border bg-card shadow-sm flex flex-col overflow-hidden h-[520px]">
<CardHeader class="pb-3 shrink-0">
<CardHeader class="pb-2 shrink-0">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="p-1.5 rounded-md bg-emerald-500/10 text-emerald-600">
<FileJson class="w-4 h-4" />
<Code2 class="w-4 h-4" />
</div>
<CardTitle class="text-sm font-bold uppercase tracking-wider whitespace-nowrap">API 接口说明</CardTitle>
<CardTitle class="text-sm font-bold uppercase tracking-wider whitespace-nowrap">内建助手库说明</CardTitle>
</div>
</div>
</CardHeader>
<CardContent class="p-0 flex-1 overflow-y-auto">
<div
class="bg-zinc-50 dark:bg-zinc-950/50 p-5 font-code text-xs sm:text-sm leading-relaxed text-zinc-800 dark:text-zinc-300 relative group min-h-full">
<div class="flex items-center justify-between mb-6 border-b border-zinc-200 dark:border-zinc-800/50 pb-3">
<div class="flex items-center gap-2">
<Badge class="bg-emerald-600 text-white border-none py-0 px-2 text-[10px]">POST</Badge>
<code
class="px-2 py-0.5 bg-zinc-200/50 dark:bg-zinc-800/60 border border-zinc-300/50 dark:border-zinc-700/50 rounded-md text-[13px] text-zinc-700 dark:text-zinc-300 font-mono shadow-sm tracking-tight">/api/v1/notify/send</code>
class="bg-zinc-50 dark:bg-zinc-950/50 p-4 text-xs sm:text-sm leading-relaxed text-zinc-800 dark:text-zinc-300 relative group min-h-full">
<div class="mb-4 border-b border-zinc-200 dark:border-zinc-800/50 pb-3">
<div class="flex items-center gap-2 mb-1.5">
<Badge class="bg-primary text-primary-foreground border-none py-0 px-2 text-[10px]">RECOMMENDED</Badge>
<span class="text-sm font-bold text-zinc-900 dark:text-zinc-100">内建助手库 (Built-in)</span>
</div>
<Button variant="ghost" size="icon"
class="h-7 w-7 text-zinc-500 hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-all rounded-md"
@click="copyToClipboard(apiExample, 'api')">
<Check v-if="copiedBlock === 'api'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" />
</Button>
<p class="text-[12px] text-zinc-500 leading-normal">
白虎面板内置了跨语言的脚本推送工具通过环境层预装该库您的脚本可以实现完全的零配置调用
</p>
</div>
<div class="space-y-6">
<div>
<span class="block mb-2 text-zinc-500 uppercase text-[10px] font-bold tracking-widest">Headers</span>
<div class="space-y-2">
<div class="flex items-center gap-2">
<span class="text-zinc-500">Content-Type:</span>
<span>application/json</span>
<div class="space-y-5">
<!-- 一键安装说明 -->
<div class="space-y-2">
<span class="block text-zinc-500 uppercase text-[10px] font-bold tracking-widest flex items-center gap-1.5">
<Terminal class="w-3.5 h-3.5" />
环境初始化 (一键安装)
</span>
<div class="group relative">
<div class="bg-zinc-900 dark:bg-black p-3 rounded-lg font-mono text-[12px] text-emerald-500 shadow-inner">
<span class="text-zinc-500 select-none">$ </span>baihu builtininstall
</div>
<div class="flex items-center gap-2">
<span class="text-zinc-500">notify-token:</span>
<span class="text-primary">&lt;TOKEN&gt;</span>
<Button variant="ghost" size="icon"
class="absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 text-zinc-500 hover:text-white hover:bg-white/10 opacity-0 group-hover:opacity-100 transition-all"
@click="copyToClipboard('baihu builtininstall', 'install-cmd')">
<Check v-if="copiedBlock === 'install-cmd'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" />
</Button>
</div>
<p class="text-[10px] text-zinc-500 italic pl-1">
* 该命令会为 mise 管理的所有 Python Node.js 版本安装 <code class="text-primary font-bold">baihu</code>
</p>
</div>
<!-- 脚本调用方案 -->
<div class="space-y-3">
<span class="block text-zinc-500 uppercase text-[10px] font-bold tracking-widest flex items-center gap-1.5">
<Code2 class="w-3.5 h-3.5" />
导入并使用
</span>
<div class="grid grid-cols-1 gap-3">
<!-- Python 示例 -->
<div class="space-y-1.5">
<div class="flex items-center justify-between px-1">
<span class="text-[10px] font-medium text-zinc-400">Python</span>
<badge variant="outline" class="text-[8px] h-3.5 px-1 border-zinc-700 text-zinc-500">BAIHU-PY</badge>
</div>
<div class="bg-zinc-200/50 dark:bg-zinc-800/60 p-3 rounded-lg border border-zinc-200 dark:border-zinc-700/50 relative group shadow-sm">
<pre class="text-[11px] leading-snug"><span class="text-violet-500">import</span> baihu<br/>baihu.notify(<span class="text-emerald-600">"标题"</span>, <span class="text-emerald-600">"内容"</span>)</pre>
<Button variant="ghost" size="icon"
class="absolute right-2 top-2 h-6 w-6 text-zinc-400 opacity-0 group-hover:opacity-100 transition-all"
@click="copyToClipboard('import baihu\nbaihu.notify(\'标题\', \'内容\')', 'py-builtin')">
<Check v-if="copiedBlock === 'py-builtin'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" />
</Button>
</div>
</div>
<!-- Node.js 示例 -->
<div class="space-y-1.5">
<div class="flex items-center justify-between px-1">
<span class="text-[10px] font-medium text-zinc-400">Node.js</span>
<badge variant="outline" class="text-[8px] h-3.5 px-1 border-zinc-700 text-zinc-500">BAIHU-JS</badge>
</div>
<div class="bg-zinc-200/50 dark:bg-zinc-800/60 p-3 rounded-lg border border-zinc-200 dark:border-zinc-700/50 relative group shadow-sm">
<pre class="text-[11px] leading-snug"><span class="text-violet-500">const</span> baihu = require(<span class="text-emerald-600">'baihu'</span>);<br/>baihu.notify(<span class="text-emerald-600">"标题"</span>, <span class="text-emerald-600">"内容"</span>);</pre>
<Button variant="ghost" size="icon"
class="absolute right-2 top-2 h-6 w-6 text-zinc-400 opacity-0 group-hover:opacity-100 transition-all"
@click="copyToClipboard('const baihu = require(\'baihu\');\nbaihu.notify(\'标题\', \'内容\');', 'js-builtin')">
<Check v-if="copiedBlock === 'js-builtin'" class="w-3.5 h-3.5 text-emerald-500" />
<Copy v-else class="w-3.5 h-3.5" />
</Button>
</div>
</div>
</div>
</div>
<div>
<span class="block mb-2 text-zinc-500 uppercase text-[10px] font-bold tracking-widest">Body
(JSON)</span>
<div class="pl-2 font-mono">
<p class="text-zinc-600 dark:text-zinc-400 mb-1">{</p>
<div class="pl-4 space-y-2">
<div class="flex flex-wrap items-center gap-x-2">
<span class="text-orange-600 dark:text-orange-400">"channel_id":</span>
<span class="text-emerald-600 dark:text-emerald-400">"ID"</span><span
class="text-zinc-600 dark:text-zinc-400">,</span>
<span class="text-zinc-500 font-sans italic ml-auto whitespace-nowrap">//
渠道唯一标识</span>
</div>
<div class="flex flex-wrap items-center gap-x-2">
<span class="text-orange-600 dark:text-orange-400">"title":</span>
<span class="text-emerald-600 dark:text-emerald-400">"标题"</span><span
class="text-zinc-600 dark:text-zinc-400">,</span>
<span class="text-zinc-500 font-sans italic ml-auto whitespace-nowrap">// 可选</span>
</div>
<div class="flex flex-wrap items-center gap-x-2">
<span class="text-orange-600 dark:text-orange-400">"text":</span>
<span class="text-emerald-600 dark:text-emerald-400">"内容"</span>
<span class="text-zinc-500 font-sans italic ml-auto whitespace-nowrap">// 必填</span>
</div>
</div>
<p class="text-zinc-600 dark:text-zinc-400 mt-1">}</p>
<!-- 核心机制说明 -->
<div class="p-3 rounded-lg bg-orange-500/5 border border-orange-500/10 space-y-1.5">
<div class="flex items-center gap-2 text-[11px] font-bold text-orange-600 dark:text-orange-400 uppercase tracking-tight">
<AlertTriangle class="w-3 h-3" />
核心机制
</div>
<p class="text-[10px] text-zinc-500 leading-normal">
系统在执行脚本时会默认注入 <code class="text-zinc-700 dark:text-zinc-300">BHPKG_NOTIFY_TOKEN</code> & <code class="text-zinc-700 dark:text-zinc-300">BHPKG_NOTIFY_CHANNEL</code>库会自动读取这些值实现真正的免配置调用
</p>
</div>
</div>
</div>
@@ -439,7 +470,7 @@ const currentExample = computed(() => {
</CardHeader>
<CardContent class="p-0 flex-1 flex flex-col overflow-hidden">
<!-- 脚本区域独立滚动 -->
<div class="flex-1 overflow-y-auto p-5 font-code text-[12px] sm:text-[13px] leading-relaxed text-zinc-800 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-950/50">
<div class="flex-1 overflow-y-auto p-5 text-[12px] sm:text-[13px] leading-relaxed text-zinc-800 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-950/50">
<pre class="whitespace-pre-wrap break-all" v-html="currentExample" />
</div>
@@ -492,36 +523,4 @@ const currentExample = computed(() => {
</template>
<style scoped>
.font-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
position: relative;
}
.font-code ::selection {
background-color: rgba(59, 130, 246, 0.4);
/* 鲜亮的蓝色选中背景 */
color: #fff;
/* 选中时文字为白色 */
}
/* 兼容 Safari */
.font-code ::-moz-selection {
background-color: rgba(59, 130, 246, 0.4);
color: #fff;
}
/* 优化滚动条样式 if needed */
.font-code::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.font-code::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.font-code::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>