mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-15 12:15:11 +00:00
feat: restore bili plugin, merge only web+webfetch into browser plugin
This commit is contained in:
@ -160,7 +160,8 @@ enabled := sdk.AutoRestart()
|
|||||||
| 插件 | 说明 |
|
| 插件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| a2a | Agent-to-Agent 协议通信 |
|
| a2a | Agent-to-Agent 协议通信 |
|
||||||
| browser | 网络搜索、网页抓取、浏览器渲染、视频下载(合并自 web/webfetch/bili) |
|
| bili | Bilibili 视频下载 |
|
||||||
|
| browser | 网络搜索、网页抓取、浏览器渲染(合并自 web/webfetch) |
|
||||||
| editdoc | 文档编辑 |
|
| editdoc | 文档编辑 |
|
||||||
| files | 文件管理 |
|
| files | 文件管理 |
|
||||||
| memo | 备忘录/记忆 |
|
| memo | 备忘录/记忆 |
|
||||||
|
|||||||
7
example/bili/go.mod
Normal file
7
example/bili/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module bili
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||||
|
|
||||||
|
replace gitcode.com/JianFeeeee/homeagent-sdk => ../..
|
||||||
11
example/bili/plg.json
Normal file
11
example/bili/plg.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "bili",
|
||||||
|
"name_zh": "B站视频下载",
|
||||||
|
"name_en": "Bilibili Video Downloader",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "B站视频下载工具,基于 yt-dlp 引擎。支持查看视频清晰度列表、指定格式下载、可配置下载目录。",
|
||||||
|
"author": "HomeAgent",
|
||||||
|
"entry": "plugin.so",
|
||||||
|
"tags": ["bili", "video", "download"],
|
||||||
|
"targets": "linux/amd64"
|
||||||
|
}
|
||||||
234
example/bili/plugin.go
Normal file
234
example/bili/plugin.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Plugin struct {
|
||||||
|
name string
|
||||||
|
sdk *sdk.PluginSDK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) Name() string { return p.name }
|
||||||
|
|
||||||
|
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||||
|
s.SetAutoRestart(true)
|
||||||
|
p.sdk = s
|
||||||
|
tp := p.name + "_"
|
||||||
|
|
||||||
|
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||||
|
Key: "plugin." + p.name + ".output_dir", Default: "/tmp/bili_videos",
|
||||||
|
Type: "string", DisplayName: "下载目录",
|
||||||
|
Description: "B站视频下载后的保存目录",
|
||||||
|
Category: p.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
s.RegisterTool(tp+"video", sdk.ToolDef{
|
||||||
|
Name: tp + "video",
|
||||||
|
Description: "使用 yt-dlp 下载B站视频到本地。支持查看视频信息后再下载。下载后返回文件路径。",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"url": map[string]interface{}{"type": "string", "description": "B站视频分享链接"},
|
||||||
|
"info_only": map[string]interface{}{"type": "boolean", "description": "仅获取视频信息(标题、清晰度列表),不下载"},
|
||||||
|
"format": map[string]interface{}{"type": "string", "description": "视频格式ID(如 30112=高清1080P, 30080=高清1080P, 30064=高清720P, 30032=清晰480P, 30016=流畅360P),不指定则自动选最优"},
|
||||||
|
},
|
||||||
|
"required": []string{"url"},
|
||||||
|
},
|
||||||
|
}, p.handleBiliVideo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) Stop() error { return nil }
|
||||||
|
|
||||||
|
type ytdlpFormat struct {
|
||||||
|
FormatID string `json:"format_id"`
|
||||||
|
FormatNote string `json:"format_note"`
|
||||||
|
Ext string `json:"ext"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
TBR float64 `json:"tbr"`
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
FilesizeApprox int64 `json:"filesize_approx"`
|
||||||
|
VCodec string `json:"vcodec"`
|
||||||
|
ACodec string `json:"acodec"`
|
||||||
|
FPS float64 `json:"fps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ytdlpInfo struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
WebpageURL string `json:"webpage_url"`
|
||||||
|
Filename string `json:"_filename"`
|
||||||
|
Formats []ytdlpFormat `json:"formats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) handleBiliVideo(args map[string]interface{}) (interface{}, error) {
|
||||||
|
url, _ := args["url"].(string)
|
||||||
|
if url == "" {
|
||||||
|
return nil, fmt.Errorf("url is required")
|
||||||
|
}
|
||||||
|
infoOnly, _ := args["info_only"].(bool)
|
||||||
|
format, _ := args["format"].(string)
|
||||||
|
|
||||||
|
outputDir := "/tmp/bili_videos"
|
||||||
|
if p.sdk != nil {
|
||||||
|
if v, _ := p.sdk.Settings().Get("plugin." + p.name + ".output_dir"); v != nil {
|
||||||
|
if s, ok := v.(string); ok && s != "" {
|
||||||
|
outputDir = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.MkdirAll(outputDir, 0755)
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
ytdlpArgs := []string{"--no-warnings", "--dump-json", url}
|
||||||
|
cmd := exec.Command("yt-dlp", ytdlpArgs...)
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &out
|
||||||
|
cmd.Env = append(os.Environ(), "HTTP_PROXY=http://127.0.0.1:7890", "HTTPS_PROXY=http://127.0.0.1:7890")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("yt-dlp info: %w\n%s", err, strings.TrimSpace(out.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var info ytdlpInfo
|
||||||
|
if err := json.Unmarshal(out.Bytes(), &info); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse yt-dlp output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if infoOnly {
|
||||||
|
var filtered []ytdlpFormat
|
||||||
|
for _, f := range info.Formats {
|
||||||
|
if f.VCodec != "none" || f.ACodec != "none" {
|
||||||
|
filtered = append(filtered, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.Formats = filtered
|
||||||
|
|
||||||
|
lines := []string{fmt.Sprintf("标题: %s", info.Title)}
|
||||||
|
if info.Duration > 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("时长: %.0f 秒", info.Duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
type fmtLine struct {
|
||||||
|
ID string
|
||||||
|
Note string
|
||||||
|
Res string
|
||||||
|
Ext string
|
||||||
|
Size string
|
||||||
|
}
|
||||||
|
var seen []string
|
||||||
|
var display []fmtLine
|
||||||
|
for _, f := range info.Formats {
|
||||||
|
if f.FormatNote == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := f.FormatNote + f.Ext
|
||||||
|
if contains(seen, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen = append(seen, key)
|
||||||
|
res := ""
|
||||||
|
if f.Width > 0 && f.Height > 0 {
|
||||||
|
res = fmt.Sprintf("%dx%d", f.Width, f.Height)
|
||||||
|
}
|
||||||
|
sz := ""
|
||||||
|
fs := f.Filesize
|
||||||
|
if fs == 0 {
|
||||||
|
fs = f.FilesizeApprox
|
||||||
|
}
|
||||||
|
if fs > 0 {
|
||||||
|
sz = fmt.Sprintf(" (%.1f MB)", float64(fs)/1048576)
|
||||||
|
}
|
||||||
|
display = append(display, fmtLine{ID: f.FormatID, Note: f.FormatNote, Res: res, Ext: f.Ext, Size: sz})
|
||||||
|
}
|
||||||
|
if len(display) > 0 {
|
||||||
|
lines = append(lines, "清晰度列表:")
|
||||||
|
for _, d := range display {
|
||||||
|
r := d.Res
|
||||||
|
if r != "" {
|
||||||
|
r = " " + r
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf(" [%s] %s%s | %s%s", d.ID, d.Note, r, d.Ext, d.Size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dlArgs := []string{
|
||||||
|
"--no-warnings",
|
||||||
|
"--socket-timeout", "30",
|
||||||
|
"--retries", "3",
|
||||||
|
"--fragment-retries", "3",
|
||||||
|
"-o", filepath.Join(outputDir, "%(title)s.%(ext)s"),
|
||||||
|
"--no-overwrites",
|
||||||
|
}
|
||||||
|
if format != "" {
|
||||||
|
dlArgs = append(dlArgs, "-f", format)
|
||||||
|
}
|
||||||
|
dlArgs = append(dlArgs, url)
|
||||||
|
cmd2 := exec.Command("yt-dlp", dlArgs...)
|
||||||
|
cmd2.Env = append(os.Environ(), "HTTP_PROXY=http://127.0.0.1:7890", "HTTPS_PROXY=http://127.0.0.1:7890")
|
||||||
|
var dlOut bytes.Buffer
|
||||||
|
cmd2.Stdout = &dlOut
|
||||||
|
cmd2.Stderr = &dlOut
|
||||||
|
if err := cmd2.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("yt-dlp download: %w\n%s", err, strings.TrimSpace(dlOut.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, _ := os.ReadDir(outputDir)
|
||||||
|
var newest string
|
||||||
|
var newestTime int64
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fi, _ := e.Info()
|
||||||
|
if fi == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := fi.ModTime().Unix()
|
||||||
|
if t > newestTime {
|
||||||
|
newestTime = t
|
||||||
|
newest = e.Name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newest == "" {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"content": "下载完成,但未找到视频文件",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
dlPath := filepath.Join(outputDir, newest)
|
||||||
|
fi, _ := os.Stat(dlPath)
|
||||||
|
var fileSize int64
|
||||||
|
if fi != nil {
|
||||||
|
fileSize = fi.Size()
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"content": fmt.Sprintf("下载完成: %s (%.1f MB)\n路径: %s", newest, float64(fileSize)/1048576, dlPath),
|
||||||
|
"file": dlPath,
|
||||||
|
"filename": newest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(slice []string, s string) bool {
|
||||||
|
for _, v := range slice {
|
||||||
|
if v == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
|
return &Plugin{name: name}, nil
|
||||||
|
}
|
||||||
101
example/bili/plugin.h
Normal file
101
example/bili/plugin.h
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
||||||
|
|
||||||
|
/* package bili */
|
||||||
|
|
||||||
|
|
||||||
|
#line 1 "cgo-builtin-export-prolog"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
||||||
|
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||||
|
|
||||||
|
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||||
|
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||||
|
extern size_t _GoStringLen(_GoString_ s);
|
||||||
|
extern const char *_GoStringPtr(_GoString_ s);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Start of preamble from import "C" comments. */
|
||||||
|
|
||||||
|
|
||||||
|
#line 3 "z_bridge_gen.go"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
int ha_dispatch(int method_id, void* core_api, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error);
|
||||||
|
|
||||||
|
#line 1 "cgo-generated-wrapper"
|
||||||
|
|
||||||
|
|
||||||
|
/* End of preamble from import "C" comments. */
|
||||||
|
|
||||||
|
|
||||||
|
/* Start of boilerplate cgo prologue. */
|
||||||
|
#line 1 "cgo-gcc-export-header-prolog"
|
||||||
|
|
||||||
|
#ifndef GO_CGO_PROLOGUE_H
|
||||||
|
#define GO_CGO_PROLOGUE_H
|
||||||
|
|
||||||
|
typedef signed char GoInt8;
|
||||||
|
typedef unsigned char GoUint8;
|
||||||
|
typedef short GoInt16;
|
||||||
|
typedef unsigned short GoUint16;
|
||||||
|
typedef int GoInt32;
|
||||||
|
typedef unsigned int GoUint32;
|
||||||
|
typedef long long GoInt64;
|
||||||
|
typedef unsigned long long GoUint64;
|
||||||
|
typedef GoInt64 GoInt;
|
||||||
|
typedef GoUint64 GoUint;
|
||||||
|
typedef size_t GoUintptr;
|
||||||
|
typedef float GoFloat32;
|
||||||
|
typedef double GoFloat64;
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
|
||||||
|
#include <complex.h>
|
||||||
|
typedef _Fcomplex GoComplex64;
|
||||||
|
typedef _Dcomplex GoComplex128;
|
||||||
|
#else
|
||||||
|
#include <complex>
|
||||||
|
typedef std::complex<float> GoComplex64;
|
||||||
|
typedef std::complex<double> GoComplex128;
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
typedef float _Complex GoComplex64;
|
||||||
|
typedef double _Complex GoComplex128;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
static assertion to make sure the file is being used on architecture
|
||||||
|
at least with matching size of GoInt.
|
||||||
|
*/
|
||||||
|
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||||
|
|
||||||
|
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||||
|
typedef _GoString_ GoString;
|
||||||
|
#endif
|
||||||
|
typedef void *GoMap;
|
||||||
|
typedef void *GoChan;
|
||||||
|
typedef struct { void *t; void *v; } GoInterface;
|
||||||
|
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* End of boilerplate cgo prologue. */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern int go_init_plugin(char* name, char* configJSON, char** errorOut);
|
||||||
|
extern int go_start_plugin(void* coreAPIptr, int coreVersion, char** errorOut);
|
||||||
|
extern int go_stop_plugin(char** errorOut);
|
||||||
|
extern int go_invoke_tool(char* name, char* argsJSON, char** resultOut, char** errorOut);
|
||||||
|
extern int go_invoke_stage(char* stage, char* ctxJSON, char** errorOut);
|
||||||
|
extern int go_invoke_output(char* channel, char* msgType, char* payloadJSON, char** errorOut);
|
||||||
|
extern void go_free_string(char* ptr);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -3,9 +3,9 @@
|
|||||||
"name_zh": "浏览器",
|
"name_zh": "浏览器",
|
||||||
"name_en": "browser",
|
"name_en": "browser",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "网络资源搜索与获取:搜索引擎查询(browser_search)、网页抓取(browser_fetch,SSRF防护)、无头浏览器渲染(browser_render)、视频下载(browser_video,基于yt-dlp)",
|
"description": "网络资源搜索与获取:搜索引擎查询(browser_search)、网页抓取(browser_fetch,SSRF防护)、无头浏览器渲染(browser_render)",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
"tags": ["web", "search", "fetch", "video", "browser"],
|
"tags": ["web", "search", "fetch", "browser"],
|
||||||
"targets": "linux/amd64"
|
"targets": "linux/amd64"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -28,7 +27,6 @@ type Plugin struct {
|
|||||||
timeout int
|
timeout int
|
||||||
proxy string
|
proxy string
|
||||||
client *http.Client
|
client *http.Client
|
||||||
outputDir string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPClient(timeout int, proxyURL string) *http.Client {
|
func newHTTPClient(timeout int, proxyURL string) *http.Client {
|
||||||
@ -74,12 +72,6 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
DisplayName: "HTTP 代理", Description: "HTTP 代理地址,如 http://proxy:port。为空则不使用代理",
|
DisplayName: "HTTP 代理", Description: "HTTP 代理地址,如 http://proxy:port。为空则不使用代理",
|
||||||
Category: "browser",
|
Category: "browser",
|
||||||
})
|
})
|
||||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
|
||||||
Key: "plugin.browser.output_dir", Default: "/tmp/browser_videos", Type: "string",
|
|
||||||
DisplayName: "视频下载目录", Description: "视频下载后的保存目录",
|
|
||||||
Category: "browser",
|
|
||||||
})
|
|
||||||
|
|
||||||
t := getSetting[float64](s.Settings(), "timeout", 30)
|
t := getSetting[float64](s.Settings(), "timeout", 30)
|
||||||
p.timeout = int(t)
|
p.timeout = int(t)
|
||||||
if p.timeout < 5 { p.timeout = 5 }
|
if p.timeout < 5 { p.timeout = 5 }
|
||||||
@ -88,8 +80,6 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
p.proxy = getSetting[string](s.Settings(), "proxy", "")
|
p.proxy = getSetting[string](s.Settings(), "proxy", "")
|
||||||
p.client = newHTTPClient(p.timeout, p.proxy)
|
p.client = newHTTPClient(p.timeout, p.proxy)
|
||||||
|
|
||||||
p.outputDir = getSetting[string](s.Settings(), "output_dir", "/tmp/browser_videos")
|
|
||||||
|
|
||||||
tp := p.name + "_"
|
tp := p.name + "_"
|
||||||
|
|
||||||
s.RegisterTool(tp+"search", sdk.ToolDef{
|
s.RegisterTool(tp+"search", sdk.ToolDef{
|
||||||
@ -131,21 +121,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
},
|
},
|
||||||
}, p.handleRender)
|
}, p.handleRender)
|
||||||
|
|
||||||
s.RegisterTool(tp+"video", sdk.ToolDef{
|
log.Printf("[%s] started, timeout=%ds proxy=%q", p.name, p.timeout, p.proxy)
|
||||||
Name: tp + "video",
|
|
||||||
Description: "Download a video from supported sites (Bilibili, YouTube, etc.) using yt-dlp. Supports viewing video info before downloading.",
|
|
||||||
Parameters: map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"url": map[string]interface{}{"type": "string", "description": "Video URL (Bilibili, YouTube, etc.)"},
|
|
||||||
"info_only": map[string]interface{}{"type": "boolean", "description": "Only fetch video info (title, available formats), do not download"},
|
|
||||||
"format": map[string]interface{}{"type": "string", "description": "Format ID (e.g. 30112=1080P), auto-selects best if not specified"},
|
|
||||||
},
|
|
||||||
"required": []string{"url"},
|
|
||||||
},
|
|
||||||
}, p.handleVideo)
|
|
||||||
|
|
||||||
log.Printf("[%s] started, timeout=%ds proxy=%q output=%s", p.name, p.timeout, p.proxy, p.outputDir)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,136 +442,6 @@ sys.stdout.write(text)
|
|||||||
return map[string]interface{}{"content": result, "title": title}, nil
|
return map[string]interface{}{"content": result, "title": title}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Video Download ────────────────────────────────────────
|
|
||||||
|
|
||||||
type ytdlpFormat struct {
|
|
||||||
FormatID string `json:"format_id"`
|
|
||||||
FormatNote string `json:"format_note"`
|
|
||||||
Ext string `json:"ext"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
TBR float64 `json:"tbr"`
|
|
||||||
Filesize int64 `json:"filesize"`
|
|
||||||
FilesizeApprox int64 `json:"filesize_approx"`
|
|
||||||
VCodec string `json:"vcodec"`
|
|
||||||
ACodec string `json:"acodec"`
|
|
||||||
FPS float64 `json:"fps"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ytdlpInfo struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Duration float64 `json:"duration"`
|
|
||||||
WebpageURL string `json:"webpage_url"`
|
|
||||||
Filename string `json:"_filename"`
|
|
||||||
Formats []ytdlpFormat `json:"formats"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Plugin) handleVideo(args map[string]interface{}) (interface{}, error) {
|
|
||||||
rawURL, _ := args["url"].(string)
|
|
||||||
if rawURL == "" { return nil, fmt.Errorf("url is required") }
|
|
||||||
infoOnly, _ := args["info_only"].(bool)
|
|
||||||
format, _ := args["format"].(string)
|
|
||||||
|
|
||||||
os.MkdirAll(p.outputDir, 0755)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
ytdlpArgs := []string{"--no-warnings", "--dump-json", rawURL}
|
|
||||||
cmd := exec.Command("yt-dlp", ytdlpArgs...)
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &out
|
|
||||||
proxyEnv := "http://127.0.0.1:7890"
|
|
||||||
if p.proxy != "" { proxyEnv = p.proxy }
|
|
||||||
cmd.Env = append(os.Environ(), "HTTP_PROXY="+proxyEnv, "HTTPS_PROXY="+proxyEnv)
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return nil, fmt.Errorf("yt-dlp info: %w\n%s", err, strings.TrimSpace(out.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
var info ytdlpInfo
|
|
||||||
if err := json.Unmarshal(out.Bytes(), &info); err != nil {
|
|
||||||
return nil, fmt.Errorf("parse yt-dlp output: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if infoOnly {
|
|
||||||
var filtered []ytdlpFormat
|
|
||||||
for _, f := range info.Formats {
|
|
||||||
if f.VCodec != "none" || f.ACodec != "none" { filtered = append(filtered, f) }
|
|
||||||
}
|
|
||||||
info.Formats = filtered
|
|
||||||
|
|
||||||
lines := []string{fmt.Sprintf("标题: %s", info.Title)}
|
|
||||||
if info.Duration > 0 { lines = append(lines, fmt.Sprintf("时长: %.0f 秒", info.Duration)) }
|
|
||||||
|
|
||||||
type fmtLine struct{ ID, Note, Res, Ext, Size string }
|
|
||||||
var seen []string
|
|
||||||
var display []fmtLine
|
|
||||||
for _, f := range info.Formats {
|
|
||||||
if f.FormatNote == "" { continue }
|
|
||||||
key := f.FormatNote + f.Ext
|
|
||||||
if contains(seen, key) { continue }
|
|
||||||
seen = append(seen, key)
|
|
||||||
res := ""
|
|
||||||
if f.Width > 0 && f.Height > 0 { res = fmt.Sprintf("%dx%d", f.Width, f.Height) }
|
|
||||||
sz := ""
|
|
||||||
fs := f.Filesize
|
|
||||||
if fs == 0 { fs = f.FilesizeApprox }
|
|
||||||
if fs > 0 { sz = fmt.Sprintf(" (%.1f MB)", float64(fs)/1048576) }
|
|
||||||
display = append(display, fmtLine{ID: f.FormatID, Note: f.FormatNote, Res: res, Ext: f.Ext, Size: sz})
|
|
||||||
}
|
|
||||||
if len(display) > 0 {
|
|
||||||
lines = append(lines, "清晰度列表:")
|
|
||||||
for _, d := range display {
|
|
||||||
r := d.Res
|
|
||||||
if r != "" { r = " " + r }
|
|
||||||
lines = append(lines, fmt.Sprintf(" [%s] %s%s | %s%s", d.ID, d.Note, r, d.Ext, d.Size))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map[string]interface{}{"content": strings.Join(lines, "\n")}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dlArgs := []string{
|
|
||||||
"--no-warnings", "--socket-timeout", "30",
|
|
||||||
"--retries", "3", "--fragment-retries", "3",
|
|
||||||
"-o", filepath.Join(p.outputDir, "%(title)s.%(ext)s"),
|
|
||||||
"--no-overwrites",
|
|
||||||
}
|
|
||||||
if format != "" { dlArgs = append(dlArgs, "-f", format) }
|
|
||||||
dlArgs = append(dlArgs, rawURL)
|
|
||||||
cmd2 := exec.Command("yt-dlp", dlArgs...)
|
|
||||||
cmd2.Env = append(os.Environ(), "HTTP_PROXY="+proxyEnv, "HTTPS_PROXY="+proxyEnv)
|
|
||||||
var dlOut bytes.Buffer
|
|
||||||
cmd2.Stdout = &dlOut
|
|
||||||
cmd2.Stderr = &dlOut
|
|
||||||
if err := cmd2.Run(); err != nil {
|
|
||||||
return nil, fmt.Errorf("yt-dlp download: %w\n%s", err, strings.TrimSpace(dlOut.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
var newest string
|
|
||||||
var newestTime int64
|
|
||||||
entries, _ := os.ReadDir(p.outputDir)
|
|
||||||
for _, e := range entries {
|
|
||||||
if e.IsDir() { continue }
|
|
||||||
if fi, _ := e.Info(); fi != nil {
|
|
||||||
if t := fi.ModTime().Unix(); t > newestTime { newestTime = t; newest = e.Name() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if newest == "" {
|
|
||||||
return map[string]interface{}{"content": "下载完成,但未找到视频文件"}, nil
|
|
||||||
}
|
|
||||||
dlPath := filepath.Join(p.outputDir, newest)
|
|
||||||
fi, _ := os.Stat(dlPath)
|
|
||||||
var fileSize int64
|
|
||||||
if fi != nil { fileSize = fi.Size() }
|
|
||||||
return map[string]interface{}{
|
|
||||||
"content": fmt.Sprintf("下载完成: %s (%.1f MB)\n路径: %s", newest, float64(fileSize)/1048576, dlPath),
|
|
||||||
"file": dlPath, "filename": newest,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(slice []string, s string) bool {
|
|
||||||
for _, v := range slice { if v == s { return true } }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
return &Plugin{name: name}, nil
|
return &Plugin{name: name}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user