528 lines
13 KiB
Go
528 lines
13 KiB
Go
package core
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cdle/sillyplus/core/common"
|
|
"github.com/cdle/sillyplus/core/storage"
|
|
"github.com/cdle/sillyplus/utils"
|
|
"github.com/dop251/goja"
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/google/uuid"
|
|
cron "github.com/robfig/cron/v3"
|
|
)
|
|
|
|
func init() {
|
|
go initNodePlugins()
|
|
}
|
|
|
|
var processes sync.Map
|
|
|
|
func initNodePlugins() {
|
|
initLanguage()
|
|
root := strings.ReplaceAll(utils.ExecPath+"/plugins", "\\", "/")
|
|
plugins := []string{root}
|
|
os.Mkdir(root, 0755)
|
|
// fmt.Println("root", root)
|
|
files, _ := ioutil.ReadDir(root)
|
|
for _, file := range files {
|
|
if !file.IsDir() {
|
|
continue
|
|
}
|
|
name := file.Name()
|
|
path := root + "/" + name
|
|
plugins = append(plugins, path)
|
|
index, class := FindMainIndex(path)
|
|
|
|
if index != "" {
|
|
AddNodePlugin(index, name, class)
|
|
}
|
|
}
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
fmt.Println("创建监视器失败:", err)
|
|
return
|
|
}
|
|
defer watcher.Close()
|
|
// 要监控的文件夹路径
|
|
for _, dir := range plugins {
|
|
err = watcher.Add(dir)
|
|
if err != nil {
|
|
fmt.Println("添加监视目录失败:", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case event, ok := <-watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
// fmt.Println(event.Name, "op", event.Op.String())
|
|
event.Name = strings.ReplaceAll(event.Name, "\\", "/")
|
|
files := strings.Split(strings.Replace(event.Name, root+"/", "", 1), "/")
|
|
var plugin_dir = false
|
|
var plugin_index = false
|
|
var plugin_name = ""
|
|
var class = ""
|
|
switch len(files) {
|
|
case 1:
|
|
plugin_dir = true
|
|
// fmt.Println("目录事件")
|
|
plugin_name = files[0]
|
|
case 2:
|
|
class, plugin_index = CheckMainIndex(files[1])
|
|
// if files[1] == "main.js" {
|
|
// if files[1] == "main.js" {
|
|
// plugin_index = true
|
|
// }
|
|
// // fmt.Println("入口文件事件")
|
|
// }
|
|
plugin_name = files[0]
|
|
}
|
|
if plugin_name == "." {
|
|
continue
|
|
}
|
|
switch event.Op.String() {
|
|
case "CREATE":
|
|
if plugin_dir {
|
|
info, err := os.Stat(event.Name)
|
|
// fmt.Println(err)
|
|
if err == nil && info.IsDir() {
|
|
index, class := FindMainIndex(event.Name)
|
|
switch class {
|
|
case NODE:
|
|
AddNodePlugin(index, plugin_name, class)
|
|
case PYTHON:
|
|
case "":
|
|
// time.Sleep(time.Millisecond * 100) //如果没有
|
|
// if info, err := os.Stat(event.Name + "/demo.main.js"); err == nil && !info.IsDir() {
|
|
|
|
// }
|
|
}
|
|
// event_name := event.Name + "/main.js"
|
|
// if info, err := os.Stat(event_name); err == nil && !info.IsDir() {
|
|
// AddNodePlugin(event_name, plugin_name)
|
|
// } else {
|
|
// time.Sleep(time.Millisecond * 100)
|
|
// if info, err := os.Stat(event_name); err == nil && !info.IsDir() {
|
|
// AddNodePlugin(event_name, plugin_name)
|
|
// }
|
|
// }
|
|
watcher.Add(event.Name)
|
|
// fmt.Println("增加插件目录", event.Name)
|
|
} else {
|
|
// fmt.Println("非插件目录", event.Name)
|
|
}
|
|
tf := event.Name + "/node_modules/sillygirl.d.ts"
|
|
ti := event.Name + "/demo.main.js"
|
|
if _, err := os.Stat(tf); err != nil {
|
|
os.Mkdir(event.Name+"/node_modules", 0700)
|
|
os.WriteFile(tf, []byte(typeat), 0700)
|
|
}
|
|
go func() {
|
|
time.Sleep(time.Second)
|
|
if _, err := os.Stat(ti); err != nil {
|
|
os.Mkdir(event.Name+"/node_modules", 0700)
|
|
os.WriteFile(ti, []byte(defaultScript(plugin_name)), 0700)
|
|
}
|
|
}()
|
|
} else if plugin_index {
|
|
// fmt.Println("增加插件", event.Name)
|
|
// RemNodePlugin(plugin_name)
|
|
AddNodePlugin(event.Name, plugin_name, class)
|
|
}
|
|
case "REMOVE", "RENAME", "REMOVE|RENAME", "REMOVE|WRITE":
|
|
if plugin_dir {
|
|
watcher.Remove(event.Name)
|
|
// fmt.Println("移除插件目录", event.Name)
|
|
// fmt.Println("移除插件", plugin_name)
|
|
AddNodePlugin(event.Name, plugin_name, UNKNOWN)
|
|
} else if plugin_index {
|
|
// fmt.Println("移除插件", plugin_name)
|
|
AddNodePlugin(event.Name, plugin_name, class)
|
|
}
|
|
case "WRITE": //, "CHMOD"
|
|
if plugin_index {
|
|
AddNodePlugin(event.Name, plugin_name, class)
|
|
// fmt.Println("变更插件", event.Name, plugin_name)
|
|
}
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
fmt.Println("错误:", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func nameUuid(name string) string {
|
|
hash := sha1.Sum([]byte(name))
|
|
return strings.ReplaceAll(uuid.NewSHA1(uuid.Nil, hash[:]).String(), "-", "_")
|
|
}
|
|
|
|
func isNameUuid(uuid string) bool {
|
|
return strings.Contains(uuid, "_")
|
|
}
|
|
|
|
func AddNodePlugin(path, name, class string) error {
|
|
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
uuid := nameUuid(name)
|
|
pluginLock.Lock()
|
|
defer pluginLock.Unlock()
|
|
//移除
|
|
var rf *common.Function
|
|
for i := range Functions {
|
|
if Functions[i].UUID == uuid {
|
|
rf = Functions[i]
|
|
DestroyAdapterByUUID(uuid)
|
|
Functions[i].Running = false
|
|
if len(Functions[i].CronIds) != 0 {
|
|
for _, id := range Functions[i].CronIds {
|
|
CRON.Remove(cron.EntryID(id))
|
|
}
|
|
}
|
|
Functions = append(Functions[:i], Functions[i+1:]...)
|
|
CancelPluginCrons(uuid)
|
|
CancelPluginWebs(uuid)
|
|
CancelPluginlistening(uuid)
|
|
CancelHttpListen(uuid)
|
|
remStatic(uuid)
|
|
storage.DisableHandle(uuid)
|
|
break
|
|
}
|
|
}
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
if rf != nil {
|
|
console.Log("已卸载 %s%s", rf.Title, rf.Suffix)
|
|
}
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
data, err := ioutil.ReadAll(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
script := string(data)
|
|
if script == "" {
|
|
return nil
|
|
}
|
|
// plugins_id.Store(uuid, path)
|
|
// fmt.Println("add,", uuid, name)
|
|
f, cbs := pluginParse(script, uuid)
|
|
f.Reload = func() { //重载
|
|
AddNodePlugin(path, name, class)
|
|
}
|
|
|
|
f.Type = class
|
|
switch f.Type {
|
|
case NODE:
|
|
f.Suffix = ".js"
|
|
case PYTHON:
|
|
f.Suffix = ".py"
|
|
}
|
|
f.Path = path
|
|
f.Handle = func(s common.Sender, _ func(vm *goja.Runtime)) interface{} {
|
|
console := &Console{UUID: uuid}
|
|
s.SetPluginID(uuid)
|
|
plt := s.GetImType()
|
|
bin := ""
|
|
var cmd *exec.Cmd
|
|
switch class {
|
|
case NODE:
|
|
bin = utils.ExecPath + "/language/node/node"
|
|
cmd = exec.Command(bin, path)
|
|
case PYTHON:
|
|
bin = "python3"
|
|
cmd = exec.Command(bin, "-u", path)
|
|
cmd.Env = append(cmd.Env, "PYTHONPATH=/Users/a1-6/Code/sillyplus/proto3")
|
|
}
|
|
|
|
cmd.Dir = filepath.Dir(path)
|
|
RUNTIME_ID := utils.GenUUID()
|
|
cmd.Env = append(cmd.Env, "RUNTIME_ID="+RUNTIME_ID)
|
|
cmd.Env = append(cmd.Env, "PLUGIN_ID="+uuid)
|
|
// 获取标准输出和标准错误输出的管道
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
// fmt.Printf("获取标准输出管道失败:%v\n", err)
|
|
// os.Exit(1)
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
// fmt.Printf("获取标准错误输出管道失败:%v\n", err)
|
|
// os.Exit(1)
|
|
}
|
|
|
|
// file, err := os.Create("output.log")
|
|
// if err != nil {
|
|
// fmt.Printf("创建文件失败:%v\n", err)
|
|
// os.Exit(1)
|
|
// }
|
|
// defer file.Close()
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
// 处理标准输出
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
data := scanner.Text()
|
|
fmt.Println(data)
|
|
|
|
// if _, err := file.WriteString(data + "\n"); err != nil {
|
|
// fmt.Printf("写入文件失败:%v\n", err)
|
|
// }
|
|
}
|
|
}()
|
|
// 处理标准错误输出
|
|
go func() {
|
|
defer wg.Done()
|
|
scanner := bufio.NewScanner(stderr)
|
|
if f.OnStart {
|
|
for scanner.Scan() {
|
|
fmt.Println(scanner.Text())
|
|
}
|
|
} else {
|
|
lines := []string{}
|
|
for scanner.Scan() {
|
|
data := scanner.Text()
|
|
lines = append(lines, data)
|
|
}
|
|
if len(lines) != 0 {
|
|
console.Error(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
}
|
|
}()
|
|
processes.Store(cmd, s)
|
|
register := createSenderRegister(RUNTIME_ID)
|
|
if (plt) != "*" {
|
|
cmd.Env = append(cmd.Env, "SENDER_ID="+register(s))
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
|
|
}
|
|
defer deleteSenderRegister(RUNTIME_ID)
|
|
defer processes.Delete(cmd)
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
fmt.Println("命令执行失败:", err)
|
|
return nil
|
|
}
|
|
} else {
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
|
|
}
|
|
processes.Range(func(key, value any) bool {
|
|
p := key.(*exec.Cmd)
|
|
if p == cmd {
|
|
return true
|
|
}
|
|
s := value.(common.Sender)
|
|
if s.GetPluginID() == uuid {
|
|
func() {
|
|
defer func() {
|
|
recover()
|
|
}()
|
|
if p.Process.Kill() == nil {
|
|
processes.Delete(key)
|
|
}
|
|
}()
|
|
}
|
|
return true
|
|
})
|
|
go func() {
|
|
defer deleteSenderRegister(RUNTIME_ID)
|
|
defer processes.Delete(cmd)
|
|
err = cmd.Wait()
|
|
}()
|
|
}
|
|
return nil
|
|
}
|
|
for _, cb := range cbs {
|
|
cb()
|
|
}
|
|
if !f.Disable { //!f.OnStart &&
|
|
if rf == nil {
|
|
// console.Log("已加载 %s%s", f.Title, f.Suffix)
|
|
} else {
|
|
console.Log("已重载 %s%s", f.Title, f.Suffix)
|
|
}
|
|
}
|
|
AddCommand([]*common.Function{f})
|
|
return nil
|
|
}
|
|
|
|
var typeat = `declare class Sender {
|
|
private uuid;
|
|
private destoried;
|
|
constructor(uuid: string);
|
|
destroy(): void;
|
|
getUserId(): Promise<string>;
|
|
getUserName(): Promise<string>;
|
|
getChatId(): Promise<string>;
|
|
getChatName(): Promise<string>;
|
|
getMessageId(): Promise<string>;
|
|
getPlatform(): Promise<string>;
|
|
getBotId(): Promise<string>;
|
|
getContent(): Promise<string>;
|
|
isAdmin(): Promise<boolean>;
|
|
param(key: number | string): Promise<string>;
|
|
setContent(content: string): Promise<undefined>;
|
|
continue(): Promise<undefined>;
|
|
getAdapter(): Promise<Adapter>;
|
|
listen(options?: {
|
|
rules?: string[];
|
|
timeout?: number;
|
|
handle?: (s: Sender) => Promise<string | void> | string | void;
|
|
listen_private?: boolean;
|
|
listen_group?: boolean;
|
|
allow_platforms?: string[];
|
|
prohibit_platforms?: string[];
|
|
allow_groups?: string[];
|
|
prohibit_groups?: string[];
|
|
allow_users?: string[];
|
|
prohibit_users?: string[];
|
|
}): Promise<Sender | undefined>;
|
|
holdOn(str: string): string;
|
|
reply(content: string): Promise<string>;
|
|
doAction(options: Record<string, any>): Promise<any>;
|
|
getEvent(): Promise<Record<string, any>>;
|
|
}
|
|
declare class Bucket {
|
|
private name;
|
|
constructor(name: string);
|
|
transform(v: string | undefined): string | number | boolean | undefined;
|
|
reverseTransform(value: any): string;
|
|
get(key: string, defaultValue?: any): Promise<any>;
|
|
set(key: string, value: any): Promise<{
|
|
message?: string;
|
|
changed?: boolean;
|
|
}>;
|
|
getAll(): Promise<Record<string, any>>;
|
|
delete(key: string): Promise<{
|
|
message?: string;
|
|
changed?: boolean;
|
|
}>;
|
|
deleteAll(): Promise<undefined>;
|
|
keys(): Promise<string[]>;
|
|
len(): Promise<number>;
|
|
buckets(): Promise<string[]>;
|
|
watch(key: string, handle: (old: any, now: any, key: string) => StorageModifier | void): void;
|
|
getName(): Promise<string>;
|
|
}
|
|
interface StorageModifier {
|
|
echo?: string;
|
|
now?: any;
|
|
message?: string;
|
|
error?: string;
|
|
}
|
|
interface Message {
|
|
message_id?: string;
|
|
user_id: string;
|
|
chat_id?: string;
|
|
content: string;
|
|
user_name?: string;
|
|
chat_name?: string;
|
|
}
|
|
declare class Adapter {
|
|
platform: string;
|
|
bot_id: string;
|
|
call: any;
|
|
constructor(options: {
|
|
platform: string;
|
|
bot_id: string;
|
|
replyHandler?: (message: Message) => Promise<string | undefined>;
|
|
actionHandler?: (message: Message) => Promise<string | undefined>;
|
|
});
|
|
receive(message: Message): Promise<undefined>;
|
|
push(message: Message): Promise<string>;
|
|
destroy(): Promise<void>;
|
|
sender(options: any): Promise<Sender>;
|
|
}
|
|
declare let sender: Sender;
|
|
declare function sleep(ms?: number): Promise<unknown>;
|
|
interface CQItem {
|
|
type: string;
|
|
params: Record<string, string>;
|
|
}
|
|
interface CQParams {
|
|
[key: string]: string | number | boolean;
|
|
}
|
|
declare let utils: {
|
|
buildCQTag: (type: string, params: CQParams, prefix?: string) => string;
|
|
parseCQText: (text: string, prefix?: string) => (string | CQItem)[];
|
|
image: (url: string) => string;
|
|
video: (url: string) => string;
|
|
};
|
|
declare let console: {
|
|
log(...args: any[]): void;
|
|
info(...args: any[]): void;
|
|
error(...args: any[]): void;
|
|
debug(...args: any[]): void;
|
|
};
|
|
export { Adapter, Bucket, sender, sleep, utils, console };
|
|
`
|
|
|
|
func defaultScript(title string) string {
|
|
create_at := time.Now().Format("2006-01-02 15:04:05")
|
|
return `/**
|
|
* @title ` + title + `
|
|
* @create_at ` + create_at + `
|
|
* @description 🐒这个人很懒什么都没有留下
|
|
* @author ` + sillyGirl.GetString("author", "佚名") + `
|
|
* @version v1.0.0
|
|
*/
|
|
|
|
const {
|
|
sender: s,
|
|
Bucket,
|
|
utils: { buildCQTag, image, video },
|
|
} = require("sillygirl");
|
|
`
|
|
}
|
|
|
|
const (
|
|
NODE = "node"
|
|
PYTHON = "python3"
|
|
UNKNOWN = "unknown"
|
|
)
|
|
|
|
func FindMainIndex(home string) (string, string) {
|
|
if info, err := os.Stat(home + "/main.js"); err == nil && !info.IsDir() {
|
|
return home + "/main.js", NODE
|
|
}
|
|
if info, err := os.Stat(home + "/main.py"); err == nil && !info.IsDir() {
|
|
return home + "/main.py", PYTHON
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func CheckMainIndex(filename string) (string, bool) {
|
|
switch filename {
|
|
case "main.js":
|
|
return NODE, true
|
|
case "main.py":
|
|
return PYTHON, true
|
|
}
|
|
return "", false
|
|
}
|