mirror of
https://github.com/FloatTech/ZeroBot-Plugin.git
synced 2026-02-09 16:50:26 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b505d050a | ||
|
|
fc9a21d2d1 | ||
|
|
e84e44476a | ||
|
|
b012df4c23 | ||
|
|
08e02ab730 | ||
|
|
fb090839d6 | ||
|
|
ac2d53352c | ||
|
|
35292a69fc | ||
|
|
cd16a755d7 | ||
|
|
1e7b2d3335 | ||
|
|
20d49ccf15 | ||
|
|
1f66f47ce6 | ||
|
|
34f3b9ba2a | ||
|
|
2fa7868838 | ||
|
|
b6ddda1d51 | ||
|
|
a1621f34a0 | ||
|
|
21aa3bc49f | ||
|
|
617d4f50a4 | ||
|
|
cb0ffa0c17 | ||
|
|
0615993297 | ||
|
|
1c0d91424a | ||
|
|
c94ee365ce | ||
|
|
19e5e6636f | ||
|
|
cac3a4be81 | ||
|
|
beada7f4da | ||
|
|
43b45ce6c5 | ||
|
|
95dd5e6b94 | ||
|
|
566f6ecfd5 | ||
|
|
5b28ad75b7 | ||
|
|
997857a558 | ||
|
|
4269057283 | ||
|
|
f70cab80c2 | ||
|
|
609d819610 | ||
|
|
961fbb098e | ||
|
|
42fe124b09 | ||
|
|
076b113455 | ||
|
|
c888936489 | ||
|
|
e1d2dee881 | ||
|
|
39e1f56955 | ||
|
|
4151464bdc | ||
|
|
0b89312d9d | ||
|
|
2c607dedee | ||
|
|
30e9d04f74 | ||
|
|
4b90a0659b | ||
|
|
109b7661b7 | ||
|
|
8da52a2772 | ||
|
|
7515983b55 | ||
|
|
7519ea548d | ||
|
|
6a55d9b279 | ||
|
|
d5227f1159 | ||
|
|
62e9fe69ed | ||
|
|
2df52161e5 | ||
|
|
a29f4cb1f9 | ||
|
|
6a747d2f9d | ||
|
|
164c71a3a3 | ||
|
|
3f1b0ad67b | ||
|
|
6c6699a5d6 | ||
|
|
e292b69ee5 | ||
|
|
e6e6dd4565 | ||
|
|
28bfc3e71d | ||
|
|
d3975cf461 | ||
|
|
7430c41c1e |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@master
|
||||
with:
|
||||
version: latest
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -62,7 +62,7 @@ archives:
|
||||
name_template: "zbp_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: zip
|
||||
|
||||
nfpms:
|
||||
- license: AGPL 3.0
|
||||
|
||||
105
README.md
105
README.md
@@ -192,6 +192,18 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w]
|
||||
|
||||
- [x] 早安 | 晚安
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>违禁词检测</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/antiabuse"
|
||||
`
|
||||
- [x] 添加违禁词
|
||||
|
||||
- [x] 删除违禁词
|
||||
|
||||
- [x] 查看违禁词
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>ATRI</summary>
|
||||
@@ -255,6 +267,8 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w]
|
||||
- [x] 翻牌
|
||||
|
||||
- [x] 赞我
|
||||
|
||||
- [x] 群签到
|
||||
|
||||
- [x] [开启 | 关闭]入群验证
|
||||
|
||||
@@ -276,6 +290,20 @@ zerobot [-h] [-m] [-n nickname] [-t token] [-u url] [-g url] [-p prefix] [-d|w]
|
||||
|
||||
- 设置欢迎语可选添加参数说明:{at}可在发送时艾特被欢迎者 {nickname}是被欢迎者名字 {avatar}是被欢迎者头像 {uid}是被欢迎者QQ号 {gid}是当前群群号 {groupname} 是当前群群名
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>群应用:AI声聊</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord"`
|
||||
|
||||
- [x] 设置AI语音群号1048452984(tips:机器人任意所在群聊即可)
|
||||
|
||||
- [x] 设置AI语音模型
|
||||
|
||||
- [x] 查看AI语音配置
|
||||
|
||||
- [x] 发送AI语音xxx
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>定时指令触发器</summary>
|
||||
@@ -384,6 +412,18 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
|
||||
- [x] 设置默认限速为每 m [分钟 | 秒] n 次触发
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>aiimage</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiimage"`
|
||||
|
||||
- [x] 设置AI画图密钥xxx
|
||||
- [x] 设置AI画图接口地址https://api.siliconflow.cn/v1/images/generations
|
||||
- [x] 设置AI画图模型名Kwai-Kolors/Kolors
|
||||
- [x] 查看AI画图配置
|
||||
- [x] AI画图 [描述]
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>AIWife</summary>
|
||||
@@ -400,6 +440,18 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
|
||||
- [x] 支付宝到账 1
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>AnimeTrace 动画/Galgame识别</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/animetrace"`
|
||||
|
||||
基于[AnimeTrace](https://ai.animedb.cn/)API 的识图搜索插件
|
||||
|
||||
- [x] Gal识图 | Gal识图 [模型名]
|
||||
|
||||
- [x] 动漫识图 | 动漫识图 2 | 动漫识图 [模型名]
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>触发者撤回时也自动撤回</summary>
|
||||
@@ -593,6 +645,17 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
|
||||
- [x] 磕cp大老师 雪乃
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>奇怪语言加解密</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/crypter"`
|
||||
|
||||
- [x] 齁语加密 [文本] 或 h加密 [文本]
|
||||
- [x] 齁语解密 [密文] 或 h解密 [密文]
|
||||
- [x] fumo加密 [文本]
|
||||
- [x] fumo解密 [文本]
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>今日早报</summary>
|
||||
@@ -949,6 +1012,28 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
|
||||
- [x] 符号说明: C5是中央C,后面不写数字,默认接5,Cb6<1,b代表降调,#代表升调,6比5高八度,<1代表音长×2,<3代表音长×8,<-1代表音长×0.5,<-3代表音长×0.125,R是休止符
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>Minecraft服务器监控&订阅</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver"`
|
||||
|
||||
- [x] mc服务器状态 [服务器IP/URI]
|
||||
- [x] mc服务器添加订阅 [服务器IP/URI]
|
||||
- [x] mc服务器取消订阅 [服务器IP/URI]
|
||||
- [x] mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个)
|
||||
- 使用job插件设置定时, 对话例子如下::
|
||||
- 记录在"@every 1m"触发的指令
|
||||
- (机器人回答:您的下一条指令将被记录,在@@every 1m时触发)
|
||||
- mc服务器订阅拉取
|
||||
</details>
|
||||
<details>
|
||||
<summary>Movies猫眼电影查询</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies"`
|
||||
|
||||
- [x] 今日电影
|
||||
- [x] 预售电影
|
||||
</details>
|
||||
<details>
|
||||
<summary>摸鱼</summary>
|
||||
@@ -992,6 +1077,10 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
- [x] 酷我点歌[xxx]
|
||||
|
||||
- [x] 酷狗点歌[xxx]
|
||||
|
||||
- [x] qq点歌[xxx]
|
||||
|
||||
- [x] 咪咕点歌[xxx]
|
||||
|
||||
</details>
|
||||
<details>
|
||||
@@ -1430,7 +1519,7 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/word_count"`
|
||||
|
||||
- [x] 热词 [群号] [消息数目]|热词 123456 1000
|
||||
- [x] 热词 [消息数目]|热词 1000
|
||||
|
||||
</details>
|
||||
<details>
|
||||
@@ -1544,12 +1633,22 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
|
||||
- [x] 设置AI聊天触发概率10
|
||||
- [x] 设置AI聊天温度80
|
||||
- [x] 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]
|
||||
- [x] 设置AI聊天(不)支持系统提示词
|
||||
- [x] 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions
|
||||
- [x] 设置AI聊天密钥xxx
|
||||
- [x] 设置AI聊天模型名xxx
|
||||
- [x] 设置AI聊天系统提示词xxx
|
||||
- [x] 设置AI聊天模型名Qwen/Qwen3-8B
|
||||
- [x] 查看AI聊天系统提示词
|
||||
- [x] 重置AI聊天系统提示词
|
||||
- [x] 设置AI聊天系统提示词xxx
|
||||
- [x] 设置AI聊天分隔符`</think>`(留空则清除)
|
||||
- [x] 设置AI聊天(不)响应AT
|
||||
- [x] 设置AI聊天最大长度4096
|
||||
- [x] 设置AI聊天TopP 0.9
|
||||
- [x] 设置AI聊天(不)以AI语音输出
|
||||
- [x] 查看AI聊天配置
|
||||
- [x] 重置AI聊天
|
||||
- [x] 群聊总结 [消息数目]|群聊总结 1000
|
||||
|
||||
</details>
|
||||
<details>
|
||||
|
||||
@@ -38,12 +38,18 @@ func setConsoleTitle(title string) (err error) {
|
||||
}
|
||||
|
||||
func init() {
|
||||
debugMode := os.Getenv("DEBUG_MODE") == "1"
|
||||
stdin := windows.Handle(os.Stdin.Fd())
|
||||
|
||||
var mode uint32
|
||||
err := windows.GetConsoleMode(stdin, &mode)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if debugMode {
|
||||
logrus.Warnf("调试模式下忽略控制台模式获取失败: %v", err)
|
||||
return // 调试模式下直接返回,跳过后续配置
|
||||
} else {
|
||||
panic(err) // 非调试模式下 panic
|
||||
}
|
||||
}
|
||||
|
||||
mode &^= windows.ENABLE_QUICK_EDIT_MODE // 禁用快速编辑模式
|
||||
|
||||
2
data
2
data
Submodule data updated: ca3652920a...6bcac0faab
@@ -11,6 +11,7 @@
|
||||
}
|
||||
),
|
||||
buildGoApplication ? pkgs.buildGoApplication,
|
||||
...
|
||||
}:
|
||||
buildGoApplication {
|
||||
pname = "ZeroBot-Plugin";
|
||||
|
||||
37
flake.lock
generated
37
flake.lock
generated
@@ -5,11 +5,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -28,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705314449,
|
||||
"narHash": "sha256-yfQQ67dLejP0FLK76LKHbkzcQqNIrux6MFe32MMFGNQ=",
|
||||
"lastModified": 1742209644,
|
||||
"narHash": "sha256-jMy1XqXqD0/tJprEbUmKilTkvbDY/C0ZGSsJJH4TNCE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "30e3c3a9ec4ac8453282ca7f67fca9e1da12c3e6",
|
||||
"rev": "8f3534eb8f6c5c3fce799376dc3b91bae6b11884",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -43,11 +43,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1705856552,
|
||||
"narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=",
|
||||
"lastModified": 1745391562,
|
||||
"narHash": "sha256-sPwcCYuiEopaafePqlG826tBhctuJsLx/mhKKM5Fmjo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d",
|
||||
"rev": "8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -57,11 +57,28 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-with-go_1_20": {
|
||||
"locked": {
|
||||
"lastModified": 1710843028,
|
||||
"narHash": "sha256-CMbK45c4nSkGvayiEHFkGFH+doGPbgo3AWfecd2t1Fk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "33c51330782cb486764eb598d5907b43dc87b4c2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "33c51330782cb486764eb598d5907b43dc87b4c2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-with-go_1_20": "nixpkgs-with-go_1_20"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
|
||||
24
flake.nix
24
flake.nix
@@ -1,6 +1,7 @@
|
||||
{
|
||||
description = "基于 ZeroBot 的 OneBot 插件";
|
||||
|
||||
inputs.nixpkgs-with-go_1_20.url = "github:NixOS/nixpkgs/33c51330782cb486764eb598d5907b43dc87b4c2";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
inputs.gomod2nix.url = "github:nix-community/gomod2nix";
|
||||
@@ -10,14 +11,25 @@
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
nixpkgs-with-go_1_20,
|
||||
flake-utils,
|
||||
gomod2nix,
|
||||
}: let
|
||||
...
|
||||
} @ inputs: let
|
||||
allSystems = flake-utils.lib.allSystems;
|
||||
in (
|
||||
flake-utils.lib.eachSystem allSystems
|
||||
(system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
old-nixpkgs = nixpkgs-with-go_1_20.legacyPackages.${system};
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
|
||||
overlays = [
|
||||
(_: _: {
|
||||
go_1_20 = old-nixpkgs.go_1_20;
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
# The current default sdk for macOS fails to compile go projects, so we use a newer one for now.
|
||||
# This has no effect on other platforms.
|
||||
@@ -25,11 +37,10 @@
|
||||
in {
|
||||
# doCheck will fail at write files
|
||||
packages = rec {
|
||||
|
||||
ZeroBot-Plugin =
|
||||
(callPackage ./. {
|
||||
ZeroBot-Plugin = (callPackage ./. (inputs
|
||||
// {
|
||||
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
|
||||
})
|
||||
}))
|
||||
.overrideAttrs (_: {doCheck = false;});
|
||||
|
||||
default = ZeroBot-Plugin;
|
||||
@@ -42,7 +53,6 @@
|
||||
pkgs.cacert
|
||||
];
|
||||
};
|
||||
|
||||
};
|
||||
devShells.default = callPackage ./shell.nix {
|
||||
inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix;
|
||||
|
||||
21
go.mod
21
go.mod
@@ -4,24 +4,25 @@ go 1.20
|
||||
|
||||
require (
|
||||
github.com/Baidu-AIP/golang-sdk v1.1.1
|
||||
github.com/FloatTech/AnimeAPI v1.7.1-0.20250217140215-4856397458c9
|
||||
github.com/FloatTech/floatbox v0.0.0-20241106130736-5aea0a935024
|
||||
github.com/FloatTech/AnimeAPI v1.7.1-0.20250901143505-180d33844860
|
||||
github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80
|
||||
github.com/FloatTech/gg v1.1.3
|
||||
github.com/FloatTech/imgfactory v0.2.2-0.20230413152719-e101cc3606ef
|
||||
github.com/FloatTech/rendercard v0.2.0
|
||||
github.com/FloatTech/sqlite v1.7.1
|
||||
github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562
|
||||
github.com/FloatTech/zbpctrl v1.7.0
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20250222055844-5d403aa9cecf
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20250812085410-2741050f465f
|
||||
github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7
|
||||
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5
|
||||
github.com/Tnze/go-mc v1.20.2
|
||||
github.com/antchfx/htmlquery v1.3.4
|
||||
github.com/corona10/goimagehash v1.1.0
|
||||
github.com/davidscholberg/go-durationfmt v0.0.0-20170122144659-64843a2083d3
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/fumiama/ahsai v0.1.0
|
||||
github.com/fumiama/cron v1.3.0
|
||||
github.com/fumiama/deepinfra v0.0.0-20250222055014-e969fc5b4ccf
|
||||
github.com/fumiama/deepinfra v0.0.0-20250910022828-8cde75e137f4
|
||||
github.com/fumiama/go-base16384 v1.7.0
|
||||
github.com/fumiama/go-registry v0.2.7
|
||||
github.com/fumiama/gotracemoe v0.0.3
|
||||
@@ -29,7 +30,9 @@ require (
|
||||
github.com/fumiama/slowdo v0.0.0-20241001074058-27c4fe5259a4
|
||||
github.com/fumiama/terasu v0.0.0-20241027183601-987ab91031ce
|
||||
github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6
|
||||
github.com/go-ego/gse v0.80.3
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/jozsefsallai/gophersauce v1.0.1
|
||||
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5
|
||||
@@ -42,7 +45,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.0
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20
|
||||
gitlab.com/gomidi/midi/v2 v2.1.7
|
||||
golang.org/x/image v0.24.0
|
||||
golang.org/x/sys v0.30.0
|
||||
@@ -59,11 +62,10 @@ require (
|
||||
github.com/faiface/beep v1.1.0 // indirect
|
||||
github.com/fumiama/go-simple-protobuf v0.2.0 // indirect
|
||||
github.com/fumiama/gofastTEA v0.0.10 // indirect
|
||||
github.com/fumiama/imgsz v0.0.4 // indirect
|
||||
github.com/fumiama/imgsz v0.0.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.0.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.0 // indirect
|
||||
@@ -84,9 +86,10 @@ require (
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/vcaesar/cedar v0.20.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
modernc.org/libc v1.61.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
|
||||
39
go.sum
39
go.sum
@@ -1,10 +1,10 @@
|
||||
github.com/Baidu-AIP/golang-sdk v1.1.1 h1:RQsAmgDSAkiq22I6n7XJ2t3afgzFeqjY46FGhvrx4cw=
|
||||
github.com/Baidu-AIP/golang-sdk v1.1.1/go.mod h1:bXnGw7xPeKt8aF7UCELKrV6UZ/46spItONK1RQBQj1Y=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/FloatTech/AnimeAPI v1.7.1-0.20250217140215-4856397458c9 h1:tI9GgG8fdMK2WazFiEbMXAXjwMCckIfDaXbig9B6DdA=
|
||||
github.com/FloatTech/AnimeAPI v1.7.1-0.20250217140215-4856397458c9/go.mod h1:XXG1eBJf+eeWacQx5azsQKL5Gg7jDYTFyyZGIa/56js=
|
||||
github.com/FloatTech/floatbox v0.0.0-20241106130736-5aea0a935024 h1:mrvWpiwfRklt9AyiQjKgDGJjf4YL6FZ3yC+ydbkuF2o=
|
||||
github.com/FloatTech/floatbox v0.0.0-20241106130736-5aea0a935024/go.mod h1:+P3hs+Cvl10/Aj3SNE96TuBvKAXCe+XD1pKphTZyiwk=
|
||||
github.com/FloatTech/AnimeAPI v1.7.1-0.20250901143505-180d33844860 h1:ddthsMzYC2LZ517/71W//9VsXT82CSBALVt3sQY5vfA=
|
||||
github.com/FloatTech/AnimeAPI v1.7.1-0.20250901143505-180d33844860/go.mod h1:CzpSeo5Pvslnq7Ho14E438Yn/flFMKzjGeX2nbC1mzk=
|
||||
github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80 h1:lFD1pd8NkYCrw0QpTX/T5pJ67I7AL5eGxQ4v0r9f81Q=
|
||||
github.com/FloatTech/floatbox v0.0.0-20250513111443-adba80e84e80/go.mod h1:IWoFFqu+0FeaHHQdddyiTRL5z7gJME6qHC96qh0R2sc=
|
||||
github.com/FloatTech/gg v1.1.3 h1:+GlL02lTKsxJQr4WCuNwVxC1/eBZrCvypCIBtxuOFb4=
|
||||
github.com/FloatTech/gg v1.1.3/go.mod h1:/9oLP54CMfq4r+71XL26uaFTJ1uL1boAyX67680/1HE=
|
||||
github.com/FloatTech/imgfactory v0.2.2-0.20230413152719-e101cc3606ef h1:CJbK/2FRwPuZpeb6M4sWK2d7oXDnBEGhpkQuQrgc91A=
|
||||
@@ -17,13 +17,15 @@ github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562 h1:snfw7FNFym1eNnLrQ
|
||||
github.com/FloatTech/ttl v0.0.0-20240716161252-965925764562/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs=
|
||||
github.com/FloatTech/zbpctrl v1.7.0 h1:Hxo6EIhJo+pHjcQP9QgIJgluaT1pHH99zkk3njqTNMo=
|
||||
github.com/FloatTech/zbpctrl v1.7.0/go.mod h1:xmM4dSwHA02Gei3ogCRiG+RTrw/7Z69PfrN5NYf8BPE=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20250222055844-5d403aa9cecf h1:7LenXdFO5gFJ6YRjCdxOadm+JqQHpkAAHbagVMXLjkg=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20250222055844-5d403aa9cecf/go.mod h1:2nILgq7ps2fLsfhns1/L2yCAM2OfIwWbEl28yLztuzk=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20250812085410-2741050f465f h1:5jnrFe9FTydb/pcUhxkWHuQVCwmYIZmneOkvmgHOwGI=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20250812085410-2741050f465f/go.mod h1:HG/yZwExV3b1Vqu4chbqwhfX4hx7gDS07QO436JkwIg=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7 h1:S/ferNiehVjNaBMNNBxUjLtVmP/YWD6Yh79RfPv4ehU=
|
||||
github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w=
|
||||
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 h1:bBmmB7he0iVN4m5mcehfheeRUEer/Avo4ujnxI3uCqs=
|
||||
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5/go.mod h1:0UcFaCkhp6vZw6l5Dpq0Dp673CoF9GdvA8lTfst0GiU=
|
||||
github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
|
||||
github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
|
||||
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:ir/IFJU5xbja5UaBEQLjcvn7aAU01nqU/NUyOBEU+ew=
|
||||
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0yifz6XDPZu48aSld8BWwBfr2JKB2bGWiEd4=
|
||||
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA=
|
||||
@@ -57,8 +59,8 @@ github.com/fumiama/ahsai v0.1.0 h1:LXD61Kaj6kJHa3AEGsLIfKNzcgaVxg7JB72OR4yNNZ4=
|
||||
github.com/fumiama/ahsai v0.1.0/go.mod h1:fFeNnqgo44i8FIaguK659aQryuZeFy+4klYLQu/rfdk=
|
||||
github.com/fumiama/cron v1.3.0 h1:ZWlwuexF+HQHl3cYytEE5HNwD99q+3vNZF1GrEiXCFo=
|
||||
github.com/fumiama/cron v1.3.0/go.mod h1:bz5Izvgi/xEUI8tlBN8BI2jr9Moo8N4or0KV8xXuPDY=
|
||||
github.com/fumiama/deepinfra v0.0.0-20250222055014-e969fc5b4ccf h1:xi3K9hukyF34JTLZTNCwM42gxcWKGJXSVou/U0pTYKg=
|
||||
github.com/fumiama/deepinfra v0.0.0-20250222055014-e969fc5b4ccf/go.mod h1:wW05PQSn8mo1mZIoa6LBUE+3xIBjkoONvnfPTV5ZOhY=
|
||||
github.com/fumiama/deepinfra v0.0.0-20250910022828-8cde75e137f4 h1:cV3HXXLNudIL9rIEYt1RCgl6H4703nE3+jL4pJNsRtc=
|
||||
github.com/fumiama/deepinfra v0.0.0-20250910022828-8cde75e137f4/go.mod h1:wW05PQSn8mo1mZIoa6LBUE+3xIBjkoONvnfPTV5ZOhY=
|
||||
github.com/fumiama/go-base16384 v1.7.0 h1:6fep7XPQWxRlh4Hu+KsdH+6+YdUp+w6CwRXtMWSsXCA=
|
||||
github.com/fumiama/go-base16384 v1.7.0/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM=
|
||||
github.com/fumiama/go-registry v0.2.7 h1:tLEqgEpsiybQMqBv0dLHm5leia/z1DhajMupwnOHeNs=
|
||||
@@ -69,8 +71,8 @@ github.com/fumiama/gofastTEA v0.0.10 h1:JJJ+brWD4kie+mmK2TkspDXKzqq0IjXm89aGYfoG
|
||||
github.com/fumiama/gofastTEA v0.0.10/go.mod h1:RIdbYZyB4MbH6ZBlPymRaXn3cD6SedlCu5W/HHfMPBk=
|
||||
github.com/fumiama/gotracemoe v0.0.3 h1:iI5EbE9A3UUbfukG6+/soYPjp1S31eCNYf4tw7s6/Jc=
|
||||
github.com/fumiama/gotracemoe v0.0.3/go.mod h1:tyqahdUzHf0bQIAVY/GYmDWvYYe5ik1ZbhnGYh+zl40=
|
||||
github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI=
|
||||
github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E=
|
||||
github.com/fumiama/imgsz v0.0.2 h1:fAkC0FnIscdKOXwAxlyw3EUba5NzxZdSxGaq3Uyfxak=
|
||||
github.com/fumiama/imgsz v0.0.2/go.mod h1:dR71mI3I2O5u6+PCpd47M9TZptzP+39tRBcbdIkoqM4=
|
||||
github.com/fumiama/jieba v0.0.0-20221203025406-36c17a10b565 h1:sQuR2+N5HurnvsZhiKdEg+Ig354TaqgCQRxd/0KgIOQ=
|
||||
github.com/fumiama/jieba v0.0.0-20221203025406-36c17a10b565/go.mod h1:UUEvyLTJ7yoOA/viKG4wEis4ERydM7+Ny6gZUWgkS80=
|
||||
github.com/fumiama/libc v0.0.0-20240530081950-6f6d8586b5c5 h1:jDxsIupsT84A6WHcs6kWbst+KqrRQ8/o0VyoFMnbBOA=
|
||||
@@ -90,6 +92,8 @@ github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebK
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-ego/gse v0.80.3 h1:YNFkjMhlhQnUeuoFcUEd1ivh6SOB764rT8GDsEbDiEg=
|
||||
github.com/go-ego/gse v0.80.3/go.mod h1:Gt3A9Ry1Eso2Kza4MRaiZ7f2DTAvActmETY46Lxg0gU=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -120,8 +124,8 @@ github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
|
||||
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jozsefsallai/gophersauce v1.0.1 h1:BA3ovtQRrAb1qYU9JoRLbDHpxnDunlNcEkEfhCvDDCM=
|
||||
github.com/jozsefsallai/gophersauce v1.0.1/go.mod h1:YVEI7djliMTmZ1Vh01YPF8bUHi+oKhe3yXgKf1T49vg=
|
||||
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 h1:BXnB1Gz4y/zwQh+ZFNy7rgd+ZfMOrwRr4uZSHEI+ieY=
|
||||
@@ -190,10 +194,13 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/vcaesar/cedar v0.20.2 h1:TDx7AdZhilKcfE1WvdToTJf5VrC/FXcUOW+KY1upLZ4=
|
||||
github.com/vcaesar/cedar v0.20.2/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik=
|
||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.0 h1:v7m+0kGtL6XQlUH9O/LzmOntDJs2clzVj93YsAWWMbk=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.0/go.mod h1:C86nQ0gIdAri4K2vg8IIQIslt08zzrKMcqYt8zhkx1M=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20 h1:Yzd+cbiJQYtf6cZDP5ZB/LqjNWiV752+5P6Eua+wnic=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.2-0.20250804063440-ccc03e33ac20/go.mod h1:C86nQ0gIdAri4K2vg8IIQIslt08zzrKMcqYt8zhkx1M=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -208,16 +215,18 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8=
|
||||
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
|
||||
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
|
||||
@@ -5,11 +5,11 @@ schema = 3
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-hKshA0K92bKuK92mmtM0osVmqLJcSbeobeWSDpQoRCo="
|
||||
[mod."github.com/FloatTech/AnimeAPI"]
|
||||
version = "v1.7.1-0.20250217140215-4856397458c9"
|
||||
hash = "sha256-7TkWoVslfzO/aTx+F7UwttrtBGGMMqe4GHN0aF4JUd0="
|
||||
version = "v1.7.1-0.20250901143505-180d33844860"
|
||||
hash = "sha256-k1MlgaBGwpaqoVk+8WYfXoVLfzqyEQW5LQaJgBKlhUA="
|
||||
[mod."github.com/FloatTech/floatbox"]
|
||||
version = "v0.0.0-20241106130736-5aea0a935024"
|
||||
hash = "sha256-hSKmkzpNZwXRo0qm4G+1lXkNzWMwV9leYlYLQuzWx3M="
|
||||
version = "v0.0.0-20250513111443-adba80e84e80"
|
||||
hash = "sha256-Zt9zkUa3qqldrSttAq66YLPZPxrnkOR2MaU7oapIWEE="
|
||||
[mod."github.com/FloatTech/gg"]
|
||||
version = "v1.1.3"
|
||||
hash = "sha256-7K/R2mKjUHVnoJ3b1wDObJ5Un2Htj59Y97G1Ja1tuPo="
|
||||
@@ -29,14 +29,17 @@ schema = 3
|
||||
version = "v1.7.0"
|
||||
hash = "sha256-HDDnE0oktWJH1tkxuQwUUbeJhmVwY5fyc/vR72D2mkU="
|
||||
[mod."github.com/FloatTech/zbputils"]
|
||||
version = "v1.7.2-0.20250222055844-5d403aa9cecf"
|
||||
hash = "sha256-+Qm3/z3M3OzgknjVx5h+Px6WUEIcXjK53ZSLSnRInJ4="
|
||||
version = "v1.7.2-0.20250812085410-2741050f465f"
|
||||
hash = "sha256-NoCU7tqzihm2xEr1LelrfMzeg9RDQ9OsFBVXfNDcxvs="
|
||||
[mod."github.com/RomiChan/syncx"]
|
||||
version = "v0.0.0-20240418144900-b7402ffdebc7"
|
||||
hash = "sha256-L1j1vgiwqXpF9pjMoRRlrQUHzoULisw/01plaEAwxs4="
|
||||
[mod."github.com/RomiChan/websocket"]
|
||||
version = "v1.4.3-0.20220227141055-9b2c6168c9c5"
|
||||
hash = "sha256-Adx+gvqB+CCoUXx7ebIaBDjVkav+wS5qZPmaqcApBWA="
|
||||
[mod."github.com/Tnze/go-mc"]
|
||||
version = "v1.20.2"
|
||||
hash = "sha256-Nu4PXNxeARH0itm6yIIplFaywL2yQnPJFksmmuyIptI="
|
||||
[mod."github.com/adamzy/cedar-go"]
|
||||
version = "v0.0.0-20170805034717-80a9c64b256d"
|
||||
hash = "sha256-N19KTxh70IUBqnchFuWkrJD8uuFOIVqv1iSuN3YFIT0="
|
||||
@@ -74,8 +77,8 @@ schema = 3
|
||||
version = "v1.3.0"
|
||||
hash = "sha256-/sN7X8dKXQgv8J+EDzVUB+o+AY9gBC8e1C6sYhaTy1k="
|
||||
[mod."github.com/fumiama/deepinfra"]
|
||||
version = "v0.0.0-20250222055014-e969fc5b4ccf"
|
||||
hash = "sha256-OD/5pId1fCSy6BsXnlgUWUcQqDiz0xpKezmozMwtpBQ="
|
||||
version = "v0.0.0-20250910022828-8cde75e137f4"
|
||||
hash = "sha256-1CV8t3R91maqJztHg7whECqvS4+sxWcSvq+EyO4PyZ8="
|
||||
[mod."github.com/fumiama/go-base16384"]
|
||||
version = "v1.7.0"
|
||||
hash = "sha256-vTAsBBYe2ISzb2Nba5E96unodZSkhMcqo6hbwR01nz8="
|
||||
@@ -92,8 +95,8 @@ schema = 3
|
||||
version = "v0.0.3"
|
||||
hash = "sha256-O3cDkVXu5NG1ZtzubxhH+S91zfgu4uH1L+OiSGYSNXQ="
|
||||
[mod."github.com/fumiama/imgsz"]
|
||||
version = "v0.0.4"
|
||||
hash = "sha256-rrGx+v41OEl0ATwL6u5TNcpfkCQbj3jFNnGiQUNu2qs="
|
||||
version = "v0.0.2"
|
||||
hash = "sha256-eYUjP1TKWUrsY++rzg4rezOvmvmjADZFBizIIDHnZtY="
|
||||
[mod."github.com/fumiama/jieba"]
|
||||
version = "v0.0.0-20221203025406-36c17a10b565"
|
||||
hash = "sha256-DvDx1pdldkdaSszrbadM/VwqT9TTSmWl6G6a+ysXYEM="
|
||||
@@ -109,6 +112,9 @@ schema = 3
|
||||
[mod."github.com/gabriel-vasile/mimetype"]
|
||||
version = "v1.0.4"
|
||||
hash = "sha256-5hl9zBo3nkPt8dZfcLoOix8lAKLm3qIkWhopoS4V34E="
|
||||
[mod."github.com/go-ego/gse"]
|
||||
version = "v0.80.3"
|
||||
hash = "sha256-uxTQN4cxE/ZReZqjlIEQ3WYD9w2Ec37LRHQftJXsSZQ="
|
||||
[mod."github.com/go-ole/go-ole"]
|
||||
version = "v1.2.6"
|
||||
hash = "sha256-+oxitLeJxYF19Z6g+6CgmCHJ1Y5D8raMi2Cb3M6nXCs="
|
||||
@@ -214,27 +220,30 @@ schema = 3
|
||||
[mod."github.com/tklauser/numcpus"]
|
||||
version = "v0.6.1"
|
||||
hash = "sha256-8eFcw4YI0w6+GPhU5xMMQjiio94q/O5PpNO3QsvXve0="
|
||||
[mod."github.com/vcaesar/cedar"]
|
||||
version = "v0.20.2"
|
||||
hash = "sha256-3WblBdkR9AZcvZCKSteBV5kdhahiFHG2dbLWfwrVkwM="
|
||||
[mod."github.com/wcharczuk/go-chart/v2"]
|
||||
version = "v2.1.2"
|
||||
hash = "sha256-GXWWea/u6BezTsPPrWhTYiTetPP/YW6P+Sj4YdocPaM="
|
||||
[mod."github.com/wdvxdr1123/ZeroBot"]
|
||||
version = "v1.8.0"
|
||||
hash = "sha256-3xQ+5NqZpHJdge1vrh0/bttaZt6u2ZiGtdZA0m80NBc="
|
||||
version = "v1.8.2-0.20250804063440-ccc03e33ac20"
|
||||
hash = "sha256-2bFcPmcDsZxTD3sU3i2QD4M/ehSF43Ohf5ltuq1QtOQ="
|
||||
[mod."github.com/yusufpapurcu/wmi"]
|
||||
version = "v1.2.4"
|
||||
hash = "sha256-N+YDBjOW59YOsZ2lRBVtFsEEi48KhNQRb63/0ZSU3bA="
|
||||
[mod."gitlab.com/gomidi/midi/v2"]
|
||||
version = "v2.1.7"
|
||||
hash = "sha256-fbgxSMCk7PVII3sNEKuGWbN56fy3eM564Xb+lnYTxRQ="
|
||||
[mod."golang.org/x/exp"]
|
||||
version = "v0.0.0-20190306152737-a1d7652674e8"
|
||||
hash = "sha256-VJ0sxFsqnx2O/NmXamL2F5bQeUw5sizVQ7NLusceK5Q="
|
||||
[mod."golang.org/x/exp/shiny"]
|
||||
version = "v0.0.0-20250305212735-054e65f0b394"
|
||||
hash = "sha256-+xzaSlgRHFa+sGnQG90/72vcJMhletsob/L+KG24P/A="
|
||||
[mod."golang.org/x/image"]
|
||||
version = "v0.24.0"
|
||||
hash = "sha256-nhcznNf4ePM7d0Jy2Si0dpMt7KQfRF5Y5QzMpwFCAVg="
|
||||
[mod."golang.org/x/mobile"]
|
||||
version = "v0.0.0-20190415191353-3e0bab5405d6"
|
||||
hash = "sha256-Ds7JS9muxzDc7WgCncAd0rMSFeBI88/I0dQsk13/56k="
|
||||
version = "v0.0.0-20231127183840-76ac6878050a"
|
||||
hash = "sha256-GdXSvrqQiJX6pOqc2Yr8gG0ZWysEE81YRl5qkt3JCMA="
|
||||
[mod."golang.org/x/net"]
|
||||
version = "v0.33.0"
|
||||
hash = "sha256-9swkU9vp6IflUUqAzK+y8PytSmrKLuryidP3RmRfe0w="
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
package banner
|
||||
|
||||
// Version ...
|
||||
var Version = "v1.9.5"
|
||||
var Version = "v1.9.9"
|
||||
|
||||
// Copyright ...
|
||||
var Copyright = "© 2020 - 2025 FloatTech"
|
||||
|
||||
// Banner ...
|
||||
var Banner = "* OneBot + ZeroBot + Golang\n" +
|
||||
"* Version " + Version + " - 2025-02-22 15:34:28 +0900 JST\n" +
|
||||
"* Version " + Version + " - 2025-09-10 10:40:39 +0800 CST\n" +
|
||||
"* Copyright " + Copyright + ". All Rights Reserved.\n" +
|
||||
"* Project: https://github.com/FloatTech/ZeroBot-Plugin"
|
||||
|
||||
@@ -27,7 +27,7 @@ var Banner = "* OneBot + ZeroBot + Golang\n" +
|
||||
"* Project: https://github.com/FloatTech/ZeroBot-Plugin"
|
||||
`
|
||||
|
||||
const timeformat = `2006-01-02 15:04:05 +0900 JST`
|
||||
const timeformat = `2006-01-02 15:04:05 +0800 CST`
|
||||
|
||||
func main() {
|
||||
f, err := os.Create("banner/banner.go")
|
||||
|
||||
175
main.go
175
main.go
@@ -38,6 +38,8 @@ import (
|
||||
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/sleepmanage" // 统计睡眠时间
|
||||
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/airecord" // 群应用:AI声聊
|
||||
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/atri" // ATRI词库
|
||||
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/manager" // 群管
|
||||
@@ -62,90 +64,95 @@ import (
|
||||
// vvvvvvvvvvvvvv //
|
||||
// vvvv //
|
||||
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/custom" // 自定义插件合集
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/ahsai" // ahsai tts
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aifalse" // 服务器监控
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiwife" // 随机老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/bookreview" // 哀伤雪刃吧推书记录
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/chess" // 国际象棋
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/choose" // 选择困难症帮手
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/chouxianghua" // 说抽象话
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/chrev" // 英文字符翻转
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/coser" // 三次元小姐姐
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/cpstory" // cp短打
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/dailynews" // 今日早报
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/danbooru" // DeepDanbooru二次元图标签识别
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/diana" // 嘉心糖发病
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/dish" // 程序员做饭指南
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/drawlots" // 多功能抽签
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/driftbottle" // 漂流瓶
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/emojimix" // 合成emoji
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/emozi" // 颜文字抽象转写
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/event" // 好友申请群聊邀请事件处理
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/font" // 渲染任意文字到图片
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/fortune" // 运势
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/funny" // 笑话
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/genshin" // 原神抽卡
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/inject" // 注入指令
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan" // 煎蛋网无聊图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/jptingroom" // 日语听力学习材料
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/kfccrazythursday" // 疯狂星期四
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolicon" // lolicon 随机图片
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolimi" // 桑帛云 API
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/magicprompt" // magicprompt吟唱提示
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativesetu" // 本地涩图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nbnhhsh" // 拼音首字母缩写释义工具
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nihongo" // 日语语法学习
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/niuniu" // 牛牛大作战
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/novel" // 铅笔小说网搜索
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/robbery" // 打劫群友的ATRI币
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/saucenao" // 以图搜图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/score" // 分数
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/translation" // 翻译
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet" // 钱包
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wantquotes" // 据意查句
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi" // warframeAPI插件
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wenxinvilg" // 百度文心AI画图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wife" // 抽老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordcount" // 聊天热词
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/ygo" // 游戏王相关插件
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/yujn" // 遇见API
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/custom" // 自定义插件合集
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/ahsai" // ahsai tts
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aifalse" // 服务器监控
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiimage" // AI画图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/aiwife" // 随机老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/alipayvoice" // 支付宝到账语音
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/animetrace" // AnimeTrace 动画/Galgame识别
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/autowithdraw" // 触发者撤回时也自动撤回
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/baiduaudit" // 百度内容审核
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/base16384" // base16384加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/base64gua" // base64卦加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/baseamasiro" // base天城文加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/bilibili" // b站相关
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/bookreview" // 哀伤雪刃吧推书记录
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/chess" // 国际象棋
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/choose" // 选择困难症帮手
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/chouxianghua" // 说抽象话
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/chrev" // 英文字符翻转
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/coser" // 三次元小姐姐
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/cpstory" // cp短打
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/crypter" // 奇怪语言加解密
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/dailynews" // 今日早报
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/danbooru" // DeepDanbooru二次元图标签识别
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/diana" // 嘉心糖发病
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/dish" // 程序员做饭指南
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/drawlots" // 多功能抽签
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/driftbottle" // 漂流瓶
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/emojimix" // 合成emoji
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/emozi" // 颜文字抽象转写
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/event" // 好友申请群聊邀请事件处理
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/font" // 渲染任意文字到图片
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/fortune" // 运势
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/funny" // 笑话
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/genshin" // 原神抽卡
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/gif" // 制图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/github" // 搜索GitHub仓库
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic" // 猜歌
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/inject" // 注入指令
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/jandan" // 煎蛋网无聊图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/jptingroom" // 日语听力学习材料
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/kfccrazythursday" // 疯狂星期四
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolicon" // lolicon 随机图片
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/lolimi" // 桑帛云 API
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/magicprompt" // magicprompt吟唱提示
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/mcfish" // 钓鱼模拟器
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/midicreate" // 简易midi音乐制作
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/minecraftobserver" // Minecraft服务器监控&订阅
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/movies" // 电影插件
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyu" // 摸鱼
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/moyucalendar" // 摸鱼人日历
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/music" // 点歌
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nativesetu" // 本地涩图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nbnhhsh" // 拼音首字母缩写释义工具
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nihongo" // 日语语法学习
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/niuniu" // 牛牛大作战
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/novel" // 铅笔小说网搜索
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nsfw" // nsfw图片识别
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/realcugan" // realcugan清晰术
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/reborn" // 投胎
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/robbery" // 打劫群友的ATRI币
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/runcode" // 在线运行代码
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/saucenao" // 以图搜图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/score" // 分数
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/setutime" // 来份涩图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/shadiao" // 沙雕app
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/shindan" // 测定
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/steam" // steam相关
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tarot" // 抽塔罗牌
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tiangou" // 舔狗日记
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/tracemoe" // 搜番
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/translation" // 翻译
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wallet" // 钱包
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wantquotes" // 据意查句
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/warframeapi" // warframeAPI插件
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wenxinvilg" // 百度文心AI画图
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wife" // 抽老婆
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordcount" // 聊天热词
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/wordle" // 猜单词
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/ygo" // 游戏王相关插件
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/ymgal" // 月幕galgame
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/yujn" // 遇见API
|
||||
|
||||
// _ "github.com/FloatTech/ZeroBot-Plugin/plugin/wtf" // 鬼东西
|
||||
|
||||
|
||||
198
plugin/aichat/cfg.go
Normal file
198
plugin/aichat/cfg.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package aichat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/chat"
|
||||
"github.com/fumiama/deepinfra"
|
||||
"github.com/fumiama/deepinfra/model"
|
||||
"github.com/sirupsen/logrus"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg = newconfig()
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ModelName string
|
||||
Type int
|
||||
MaxN uint
|
||||
TopP float32
|
||||
SystemP string
|
||||
API string
|
||||
Key string
|
||||
Separator string
|
||||
NoReplyAT bool
|
||||
NoSystemP bool
|
||||
NoRecord bool
|
||||
}
|
||||
|
||||
func newconfig() config {
|
||||
return config{
|
||||
ModelName: model.ModelDeepDeek,
|
||||
SystemP: chat.SystemPrompt,
|
||||
API: deepinfra.OpenAIDeepInfra,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *config) isvalid() bool {
|
||||
return c.ModelName != "" && c.API != "" && c.Key != ""
|
||||
}
|
||||
|
||||
func ensureconfig(ctx *zero.Ctx) bool {
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !cfg.isvalid() {
|
||||
err := c.GetExtra(&cfg)
|
||||
if err != nil {
|
||||
logrus.Warnln("ERROR: get extra err:", err)
|
||||
}
|
||||
if !cfg.isvalid() {
|
||||
cfg = newconfig()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newextrasetstr(ptr *string) func(ctx *zero.Ctx) {
|
||||
return func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
ctx.SendChain(message.Text("ERROR: empty args"))
|
||||
return
|
||||
}
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
*ptr = args
|
||||
err := c.SetExtra(&cfg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
}
|
||||
}
|
||||
|
||||
func newextrasetbool(ptr *bool) func(ctx *zero.Ctx) {
|
||||
return func(ctx *zero.Ctx) {
|
||||
args := ctx.State["regex_matched"].([]string)
|
||||
isno := args[1] == "不"
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
*ptr = isno
|
||||
err := c.SetExtra(&cfg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
}
|
||||
}
|
||||
|
||||
func newextrasetuint(ptr *uint) func(ctx *zero.Ctx) {
|
||||
return func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
ctx.SendChain(message.Text("ERROR: empty args"))
|
||||
return
|
||||
}
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
n, err := strconv.ParseUint(args, 10, 64)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: parse args err: ", err))
|
||||
return
|
||||
}
|
||||
*ptr = uint(n)
|
||||
err = c.SetExtra(&cfg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
}
|
||||
}
|
||||
|
||||
func newextrasetfloat32(ptr *float32) func(ctx *zero.Ctx) {
|
||||
return func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
ctx.SendChain(message.Text("ERROR: empty args"))
|
||||
return
|
||||
}
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
n, err := strconv.ParseFloat(args, 32)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: parse args err: ", err))
|
||||
return
|
||||
}
|
||||
*ptr = float32(n)
|
||||
err = c.SetExtra(&cfg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
}
|
||||
}
|
||||
|
||||
func printConfig(rate int64, temperature int64, cfg config) string {
|
||||
maxn := cfg.MaxN
|
||||
if maxn == 0 {
|
||||
maxn = 4096
|
||||
}
|
||||
topp := cfg.TopP
|
||||
if topp == 0 {
|
||||
topp = 0.9
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.WriteString("当前AI聊天配置:\n")
|
||||
builder.WriteString(fmt.Sprintf("• 模型名:%s\n", cfg.ModelName))
|
||||
builder.WriteString(fmt.Sprintf("• 接口类型:%d(%s)\n", cfg.Type, apilist[cfg.Type]))
|
||||
builder.WriteString(fmt.Sprintf("• 触发概率:%d%%\n", rate))
|
||||
builder.WriteString(fmt.Sprintf("• 温度:%.2f\n", float32(temperature)/100))
|
||||
builder.WriteString(fmt.Sprintf("• 最大长度:%d\n", maxn))
|
||||
builder.WriteString(fmt.Sprintf("• TopP:%.1f\n", topp))
|
||||
builder.WriteString(fmt.Sprintf("• 系统提示词:%s\n", cfg.SystemP))
|
||||
builder.WriteString(fmt.Sprintf("• 接口地址:%s\n", cfg.API))
|
||||
builder.WriteString(fmt.Sprintf("• 密钥:%s\n", maskKey(cfg.Key)))
|
||||
builder.WriteString(fmt.Sprintf("• 分隔符:%s\n", cfg.Separator))
|
||||
builder.WriteString(fmt.Sprintf("• 响应@:%s\n", yesNo(!cfg.NoReplyAT)))
|
||||
builder.WriteString(fmt.Sprintf("• 支持系统提示词:%s\n", yesNo(!cfg.NoSystemP)))
|
||||
builder.WriteString(fmt.Sprintf("• 以AI语音输出:%s\n", yesNo(!cfg.NoRecord)))
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func maskKey(key string) string {
|
||||
if len(key) <= 4 {
|
||||
return "****"
|
||||
}
|
||||
return key[:2] + strings.Repeat("*", len(key)-4) + key[len(key)-2:]
|
||||
}
|
||||
|
||||
func yesNo(b bool) string {
|
||||
if b {
|
||||
return "是"
|
||||
}
|
||||
return "否"
|
||||
}
|
||||
@@ -1,86 +1,99 @@
|
||||
// Package aichat OpenAI聊天
|
||||
// Package aichat OpenAI聊天和群聊总结
|
||||
package aichat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
"time"
|
||||
|
||||
"github.com/fumiama/deepinfra"
|
||||
"github.com/fumiama/deepinfra/model"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
"github.com/FloatTech/AnimeAPI/airecord"
|
||||
"github.com/FloatTech/floatbox/process"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/chat"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
)
|
||||
|
||||
var (
|
||||
api *deepinfra.API
|
||||
en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
// en data [8 temp] [8 rate] LSB
|
||||
en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Extra: control.ExtraFromString("aichat"),
|
||||
Brief: "OpenAI聊天",
|
||||
Help: "- 设置AI聊天触发概率10\n" +
|
||||
"- 设置AI聊天温度80\n" +
|
||||
"- 设置AI聊天接口类型[OpenAI|OLLaMA|GenAI]\n" +
|
||||
"- 设置AI聊天(不)支持系统提示词\n" +
|
||||
"- 设置AI聊天接口地址https://api.siliconflow.cn/v1/chat/completions\n" +
|
||||
"- 设置AI聊天密钥xxx\n" +
|
||||
"- 设置AI聊天模型名xxx\n" +
|
||||
"- 设置AI聊天模型名Qwen/Qwen3-8B\n" +
|
||||
"- 查看AI聊天系统提示词\n" +
|
||||
"- 重置AI聊天系统提示词\n" +
|
||||
"- 设置AI聊天系统提示词xxx\n" +
|
||||
"- 设置AI聊天分隔符</think>(留空则清除)\n" +
|
||||
"- 设置AI聊天(不)响应AT",
|
||||
"- 设置AI聊天(不)响应AT\n" +
|
||||
"- 设置AI聊天最大长度4096\n" +
|
||||
"- 设置AI聊天TopP 0.9\n" +
|
||||
"- 设置AI聊天(不)以AI语音输出\n" +
|
||||
"- 查看AI聊天配置\n" +
|
||||
"- 重置AI聊天\n" +
|
||||
"- 群聊总结 [消息数目]|群聊总结 1000\n" +
|
||||
"- /gpt [内容] (使用大模型聊天)\n",
|
||||
|
||||
PrivateDataFolder: "aichat",
|
||||
})
|
||||
)
|
||||
|
||||
var (
|
||||
modelname = model.ModelDeepDeek
|
||||
systemprompt = chat.SystemPrompt
|
||||
sepstr = ""
|
||||
noreplyat = false
|
||||
apitypes = map[string]uint8{
|
||||
"OpenAI": 0,
|
||||
"OLLaMA": 1,
|
||||
"GenAI": 2,
|
||||
}
|
||||
apilist = [3]string{"OpenAI", "OLLaMA", "GenAI"}
|
||||
limit = ctxext.NewLimiterManager(time.Second*30, 1)
|
||||
)
|
||||
|
||||
func init() {
|
||||
mf := en.DataFolder() + "model.txt"
|
||||
sf := en.DataFolder() + "system.txt"
|
||||
pf := en.DataFolder() + "sep.txt"
|
||||
nf := en.DataFolder() + "NoReplyAT"
|
||||
if file.IsExist(mf) {
|
||||
data, err := os.ReadFile(mf)
|
||||
if err != nil {
|
||||
logrus.Warnln("read model", err)
|
||||
} else {
|
||||
modelname = string(data)
|
||||
}
|
||||
// getModelParams 获取模型参数:温度(float32(temp)/100)、TopP和最大长度
|
||||
func getModelParams(temp int64) (temperature float32, topp float32, maxn uint) {
|
||||
// 处理温度参数
|
||||
if temp <= 0 {
|
||||
temp = 70 // default setting
|
||||
}
|
||||
if file.IsExist(sf) {
|
||||
data, err := os.ReadFile(sf)
|
||||
if err != nil {
|
||||
logrus.Warnln("read system", err)
|
||||
} else {
|
||||
systemprompt = string(data)
|
||||
}
|
||||
if temp > 100 {
|
||||
temp = 100
|
||||
}
|
||||
if file.IsExist(pf) {
|
||||
data, err := os.ReadFile(pf)
|
||||
if err != nil {
|
||||
logrus.Warnln("read sep", err)
|
||||
} else {
|
||||
sepstr = string(data)
|
||||
}
|
||||
}
|
||||
noreplyat = file.IsExist(nf)
|
||||
temperature = float32(temp) / 100
|
||||
|
||||
en.OnMessage(func(ctx *zero.Ctx) bool {
|
||||
return ctx.ExtractPlainText() != "" && (!noreplyat || (noreplyat && !ctx.Event.IsToMe))
|
||||
// 处理TopP参数
|
||||
topp = cfg.TopP
|
||||
if topp == 0 {
|
||||
topp = 0.9
|
||||
}
|
||||
|
||||
// 处理最大长度参数
|
||||
maxn = cfg.MaxN
|
||||
if maxn == 0 {
|
||||
maxn = 4096
|
||||
}
|
||||
|
||||
return temperature, topp, maxn
|
||||
}
|
||||
|
||||
func init() {
|
||||
en.OnMessage(ensureconfig, func(ctx *zero.Ctx) bool {
|
||||
return ctx.ExtractPlainText() != "" &&
|
||||
(!cfg.NoReplyAT || (cfg.NoReplyAT && !ctx.Event.IsToMe))
|
||||
}).SetBlock(false).Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
if gid == 0 {
|
||||
@@ -99,40 +112,45 @@ func init() {
|
||||
if ctx.Event.IsToMe {
|
||||
ctx.Block()
|
||||
}
|
||||
key := ""
|
||||
err := c.GetExtra(&key)
|
||||
if err != nil {
|
||||
logrus.Warnln("ERROR: get extra err:", err)
|
||||
return
|
||||
}
|
||||
if key == "" {
|
||||
if cfg.Key == "" {
|
||||
logrus.Warnln("ERROR: get extra err: empty key")
|
||||
return
|
||||
}
|
||||
var x deepinfra.API
|
||||
y := &x
|
||||
if api == nil {
|
||||
x = deepinfra.NewAPI(deepinfra.APIDeepInfra, key)
|
||||
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&api)), unsafe.Pointer(&x))
|
||||
} else {
|
||||
y = api
|
||||
}
|
||||
if temp <= 0 {
|
||||
temp = 70 // default setting
|
||||
}
|
||||
if temp > 100 {
|
||||
temp = 100
|
||||
}
|
||||
|
||||
data, err := y.Request(chat.Ask(model.NewOpenAI(
|
||||
modelname, sepstr,
|
||||
float32(temp)/100, 0.9, 4096,
|
||||
), gid, systemprompt))
|
||||
if err != nil {
|
||||
logrus.Warnln("[niniqun] post err:", err)
|
||||
temperature, topp, maxn := getModelParams(temp)
|
||||
|
||||
x := deepinfra.NewAPI(cfg.API, cfg.Key)
|
||||
var mod model.Protocol
|
||||
switch cfg.Type {
|
||||
case 0:
|
||||
mod = model.NewOpenAI(
|
||||
cfg.ModelName, cfg.Separator,
|
||||
temperature, topp, maxn,
|
||||
).SetExtra(&map[string]bool{
|
||||
"enable_thinking": false,
|
||||
})
|
||||
case 1:
|
||||
mod = model.NewOLLaMA(
|
||||
cfg.ModelName, cfg.Separator,
|
||||
temperature, topp, maxn,
|
||||
)
|
||||
case 2:
|
||||
mod = model.NewGenAI(
|
||||
cfg.ModelName,
|
||||
temperature, topp, maxn,
|
||||
)
|
||||
default:
|
||||
logrus.Warnln("[aichat] unsupported AI type", cfg.Type)
|
||||
return
|
||||
}
|
||||
txt := strings.Trim(data, "\n ")
|
||||
|
||||
data, err := x.Request(chat.Ask(mod, gid, cfg.SystemP, cfg.NoSystemP))
|
||||
if err != nil {
|
||||
logrus.Warnln("[aichat] post err:", err)
|
||||
return
|
||||
}
|
||||
|
||||
txt := chat.Sanitize(strings.Trim(data, "\n "))
|
||||
if len(txt) > 0 {
|
||||
chat.Reply(gid, txt)
|
||||
nick := zero.BotConfig.NickName[rand.Intn(len(zero.BotConfig.NickName))]
|
||||
@@ -146,10 +164,20 @@ func init() {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
if id != nil {
|
||||
id = ctx.SendChain(message.Reply(id), message.Text(t))
|
||||
logrus.Infoln("[aichat] 回复内容:", t)
|
||||
recCfg := airecord.GetConfig()
|
||||
record := ""
|
||||
if !cfg.NoRecord {
|
||||
record = ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, t)
|
||||
}
|
||||
if record != "" {
|
||||
ctx.SendChain(message.Record(record))
|
||||
} else {
|
||||
id = ctx.SendChain(message.Text(t))
|
||||
if id != nil {
|
||||
id = ctx.SendChain(message.Reply(id), message.Text(t))
|
||||
} else {
|
||||
id = ctx.SendChain(message.Text(t))
|
||||
}
|
||||
}
|
||||
process.SleepAbout1sTo2s()
|
||||
}
|
||||
@@ -221,7 +249,7 @@ func init() {
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
en.OnPrefix("设置AI聊天密钥", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
en.OnPrefix("设置AI聊天接口类型", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
ctx.SendChain(message.Text("ERROR: empty args"))
|
||||
@@ -232,82 +260,286 @@ func init() {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
err := c.SetExtra(&args)
|
||||
typ, ok := apitypes[args]
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: 未知类型 ", args))
|
||||
return
|
||||
}
|
||||
cfg.Type = int(typ)
|
||||
err := c.SetExtra(&cfg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
en.OnPrefix("设置AI聊天模型名", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
ctx.SendChain(message.Text("ERROR: empty args"))
|
||||
en.OnPrefix("设置AI聊天接口地址", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetstr(&cfg.API))
|
||||
en.OnPrefix("设置AI聊天密钥", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetstr(&cfg.Key))
|
||||
en.OnPrefix("设置AI聊天模型名", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetstr(&cfg.ModelName))
|
||||
en.OnPrefix("设置AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetstr(&cfg.SystemP))
|
||||
en.OnFullMatch("查看AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SendChain(message.Text(cfg.SystemP))
|
||||
})
|
||||
en.OnFullMatch("重置AI聊天系统提示词", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
modelname = args
|
||||
err := os.WriteFile(mf, []byte(args), 0644)
|
||||
cfg.SystemP = chat.SystemPrompt
|
||||
err := c.SetExtra(&cfg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
en.OnPrefix("设置AI聊天系统提示词", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
ctx.SendChain(message.Text("ERROR: empty args"))
|
||||
return
|
||||
}
|
||||
systemprompt = args
|
||||
err := os.WriteFile(sf, []byte(args), 0644)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
en.OnFullMatch("重置AI聊天系统提示词", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
systemprompt = chat.SystemPrompt
|
||||
_ = os.Remove(sf)
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
en.OnPrefix("设置AI聊天分隔符", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if args == "" {
|
||||
sepstr = ""
|
||||
_ = os.Remove(pf)
|
||||
ctx.SendChain(message.Text("清除成功"))
|
||||
return
|
||||
}
|
||||
sepstr = args
|
||||
err := os.WriteFile(pf, []byte(args), 0644)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("设置成功"))
|
||||
})
|
||||
en.OnRegex("^设置AI聊天(不)?响应AT$", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := ctx.State["regex_matched"].([]string)
|
||||
isno := args[1] == "不"
|
||||
if isno {
|
||||
f, err := os.Create(nf)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
en.OnPrefix("设置AI聊天分隔符", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetstr(&cfg.Separator))
|
||||
en.OnRegex("^设置AI聊天(不)?响应AT$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetbool(&cfg.NoReplyAT))
|
||||
en.OnRegex("^设置AI聊天(不)?支持系统提示词$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetbool(&cfg.NoSystemP))
|
||||
en.OnPrefix("设置AI聊天最大长度", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetuint(&cfg.MaxN))
|
||||
en.OnPrefix("设置AI聊天TopP", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetfloat32(&cfg.TopP))
|
||||
en.OnRegex("^设置AI聊天(不)?以AI语音输出$", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(newextrasetbool(&cfg.NoRecord))
|
||||
en.OnFullMatch("查看AI聊天配置", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("ERROR: no such plugin"))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString("PLACEHOLDER")
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
gid := ctx.Event.GroupID
|
||||
rate := c.GetData(gid) & 0xff
|
||||
temp := (c.GetData(gid) >> 8) & 0xff
|
||||
if temp <= 0 {
|
||||
temp = 70 // default setting
|
||||
}
|
||||
noreplyat = true
|
||||
} else {
|
||||
_ = os.Remove(nf)
|
||||
noreplyat = false
|
||||
}
|
||||
if temp > 100 {
|
||||
temp = 100
|
||||
}
|
||||
ctx.SendChain(message.Text(printConfig(rate, temp, cfg)))
|
||||
})
|
||||
en.OnFullMatch("重置AI聊天", ensureconfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
chat.Reset()
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
|
||||
// 添加群聊总结功能
|
||||
en.OnRegex(`^群聊总结\s?(\d*)$`, ensureconfig, zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(limit.LimitByGroup).Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SendChain(message.Text("少女思考中..."))
|
||||
gid := ctx.Event.GroupID
|
||||
if gid == 0 {
|
||||
gid = -ctx.Event.UserID
|
||||
}
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rate := c.GetData(gid)
|
||||
temp := (rate >> 8) & 0xff
|
||||
p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64)
|
||||
if p > 1000 {
|
||||
p = 1000
|
||||
}
|
||||
if p == 0 {
|
||||
p = 200
|
||||
}
|
||||
group := ctx.GetGroupInfo(gid, false)
|
||||
if group.MemberCount == 0 {
|
||||
ctx.SendChain(message.Text(zero.BotConfig.NickName[0], "未加入", group.Name, "(", gid, "),无法获取总结"))
|
||||
return
|
||||
}
|
||||
|
||||
var messages []string
|
||||
|
||||
h := ctx.GetGroupMessageHistory(gid, 0, p, false)
|
||||
h.Get("messages").ForEach(func(_, msgObj gjson.Result) bool {
|
||||
nickname := msgObj.Get("sender.nickname").Str
|
||||
text := strings.TrimSpace(message.ParseMessageFromString(msgObj.Get("raw_message").Str).ExtractPlainText())
|
||||
if text != "" {
|
||||
messages = append(messages, nickname+": "+text)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(messages) == 0 {
|
||||
ctx.SendChain(message.Text("ERROR: 历史消息为空或者无法获得历史消息"))
|
||||
return
|
||||
}
|
||||
|
||||
// 构造总结请求提示
|
||||
summaryPrompt := "请总结这个群聊内容,要求按发言顺序梳理,明确标注每个发言者的昵称,并完整呈现其核心观点、提出的问题、发表的看法或做出的回应,确保不遗漏关键信息,且能体现成员间的对话逻辑和互动关系:\n" +
|
||||
strings.Join(messages, "\n")
|
||||
|
||||
// 调用大模型API进行总结
|
||||
summary, err := llmchat(summaryPrompt, temp)
|
||||
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("群 ")
|
||||
b.WriteString(group.Name)
|
||||
b.WriteByte('(')
|
||||
b.WriteString(strconv.FormatInt(gid, 10))
|
||||
b.WriteString(") 的 ")
|
||||
b.WriteString(strconv.FormatInt(p, 10))
|
||||
b.WriteString(" 条消息总结:\n\n")
|
||||
b.WriteString(summary)
|
||||
|
||||
// 分割总结内容为多段(按1000字符长度切割)
|
||||
summaryText := b.String()
|
||||
msg := make(message.Message, 0)
|
||||
for len(summaryText) > 0 {
|
||||
if len(summaryText) <= 1000 {
|
||||
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(summaryText)))
|
||||
break
|
||||
}
|
||||
|
||||
// 查找1000字符内的最后一个换行符,尽量在换行处分割
|
||||
chunk := summaryText[:1000]
|
||||
lastNewline := strings.LastIndex(chunk, "\n")
|
||||
if lastNewline > 0 {
|
||||
chunk = summaryText[:lastNewline+1]
|
||||
}
|
||||
|
||||
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk)))
|
||||
summaryText = summaryText[len(chunk):]
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
ctx.Send(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加 /gpt 命令处理(同时支持回复消息和直接使用)
|
||||
en.OnKeyword("/gpt", ensureconfig).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
if gid == 0 {
|
||||
gid = -ctx.Event.UserID
|
||||
}
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rate := c.GetData(gid)
|
||||
temp := (rate >> 8) & 0xff
|
||||
text := ctx.MessageString()
|
||||
|
||||
var query string
|
||||
var replyContent string
|
||||
|
||||
// 检查是否是回复消息 (使用MessageElement检查而不是CQ码)
|
||||
for _, elem := range ctx.Event.Message {
|
||||
if elem.Type == "reply" {
|
||||
// 提取被回复的消息ID
|
||||
replyIDStr := elem.Data["id"]
|
||||
replyID, err := strconv.ParseInt(replyIDStr, 10, 64)
|
||||
if err == nil {
|
||||
// 获取被回复的消息内容
|
||||
replyMsg := ctx.GetMessage(replyID)
|
||||
if replyMsg.Elements != nil {
|
||||
replyContent = replyMsg.Elements.ExtractPlainText()
|
||||
}
|
||||
}
|
||||
break // 找到回复元素后退出循环
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 /gpt 后面的内容
|
||||
parts := strings.SplitN(text, "/gpt", 2)
|
||||
|
||||
var gContent string
|
||||
if len(parts) > 1 {
|
||||
gContent = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
// 组合内容:优先使用回复内容,如果同时有/gpt内容则拼接
|
||||
switch {
|
||||
case replyContent != "" && gContent != "":
|
||||
query = replyContent + "\n" + gContent
|
||||
case replyContent != "":
|
||||
query = replyContent
|
||||
case gContent != "":
|
||||
query = gContent
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// 调用大模型API进行聊天
|
||||
reply, err := llmchat(query, temp)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 分割总结内容为多段(按1000字符长度切割)
|
||||
msg := make(message.Message, 0)
|
||||
for len(reply) > 0 {
|
||||
if len(reply) <= 1000 {
|
||||
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(reply)))
|
||||
break
|
||||
}
|
||||
|
||||
// 查找1000字符内的最后一个换行符,尽量在换行处分割
|
||||
chunk := reply[:1000]
|
||||
lastNewline := strings.LastIndex(chunk, "\n")
|
||||
if lastNewline > 0 {
|
||||
chunk = reply[:lastNewline+1]
|
||||
}
|
||||
|
||||
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text(chunk)))
|
||||
reply = reply[len(chunk):]
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
ctx.Send(msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// llmchat 调用大模型API包装
|
||||
func llmchat(prompt string, temp int64) (string, error) {
|
||||
temperature, topp, maxn := getModelParams(temp) // 使用默认温度70
|
||||
|
||||
x := deepinfra.NewAPI(cfg.API, cfg.Key)
|
||||
var mod model.Protocol
|
||||
switch cfg.Type {
|
||||
case 0:
|
||||
mod = model.NewOpenAI(
|
||||
cfg.ModelName, cfg.Separator,
|
||||
temperature, topp, maxn,
|
||||
).SetExtra(&map[string]bool{
|
||||
"enable_thinking": false,
|
||||
})
|
||||
case 1:
|
||||
mod = model.NewOLLaMA(
|
||||
cfg.ModelName, cfg.Separator,
|
||||
temperature, topp, maxn,
|
||||
)
|
||||
case 2:
|
||||
mod = model.NewGenAI(
|
||||
cfg.ModelName,
|
||||
temperature, topp, maxn,
|
||||
)
|
||||
default:
|
||||
logrus.Warnln("[aichat] unsupported AI type", cfg.Type)
|
||||
return "", errors.New("不支持的AI类型")
|
||||
}
|
||||
|
||||
data, err := x.Request(mod.User(prompt))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(data), nil
|
||||
}
|
||||
|
||||
56
plugin/aiimage/config.go
Normal file
56
plugin/aiimage/config.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Package aiimage 提供AI画图功能配置
|
||||
package aiimage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
sql "github.com/FloatTech/sqlite"
|
||||
)
|
||||
|
||||
// storage 管理画图配置存储
|
||||
type storage struct {
|
||||
sync.RWMutex
|
||||
db sql.Sqlite
|
||||
}
|
||||
|
||||
// imageConfig 存储AI画图配置信息
|
||||
type imageConfig struct {
|
||||
ID int64 `db:"id"` // 主键ID
|
||||
APIKey string `db:"apiKey"` // API密钥
|
||||
APIURL string `db:"apiUrl"` // API地址
|
||||
ModelName string `db:"modelName"` // 画图模型名称
|
||||
}
|
||||
|
||||
// getConfig 获取当前配置
|
||||
func (sdb *storage) getConfig() imageConfig {
|
||||
sdb.RLock()
|
||||
defer sdb.RUnlock()
|
||||
cfg := imageConfig{}
|
||||
_ = sdb.db.Find("config", &cfg, "WHERE id = 1")
|
||||
return cfg
|
||||
}
|
||||
|
||||
// setConfig 设置AI画图配置
|
||||
func (sdb *storage) setConfig(apiKey, apiURL, modelName string) error {
|
||||
sdb.Lock()
|
||||
defer sdb.Unlock()
|
||||
return sdb.db.Insert("config", &imageConfig{
|
||||
ID: 1,
|
||||
APIKey: apiKey,
|
||||
APIURL: apiURL,
|
||||
ModelName: modelName,
|
||||
})
|
||||
}
|
||||
|
||||
// PrintConfig 返回格式化后的配置信息
|
||||
func (sdb *storage) PrintConfig() string {
|
||||
cfg := sdb.getConfig()
|
||||
var builder strings.Builder
|
||||
builder.WriteString("当前AI画图配置:\n")
|
||||
builder.WriteString(fmt.Sprintf("• 密钥: %s\n", cfg.APIKey))
|
||||
builder.WriteString(fmt.Sprintf("• 接口地址: %s\n", cfg.APIURL))
|
||||
builder.WriteString(fmt.Sprintf("• 模型名: %s\n", cfg.ModelName))
|
||||
return builder.String()
|
||||
}
|
||||
171
plugin/aiimage/main.go
Normal file
171
plugin/aiimage/main.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// Package aiimage AI画图
|
||||
package aiimage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
fcext "github.com/FloatTech/floatbox/ctxext"
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
sql "github.com/FloatTech/sqlite"
|
||||
"github.com/tidwall/gjson"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
)
|
||||
|
||||
func init() {
|
||||
var sdb = &storage{}
|
||||
|
||||
en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Extra: control.ExtraFromString("aiimage"),
|
||||
Brief: "AI画图",
|
||||
Help: "- 设置AI画图密钥xxx\n" +
|
||||
"- 设置AI画图接口地址https://api.siliconflow.cn/v1/images/generations\n" +
|
||||
"- 设置AI画图模型名Kwai-Kolors/Kolors\n" +
|
||||
"- 查看AI画图配置\n" +
|
||||
"- AI画图 [描述]",
|
||||
PrivateDataFolder: "aiimage",
|
||||
})
|
||||
|
||||
getdb := fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool {
|
||||
sdb.db = sql.New(en.DataFolder() + "aiimage.db")
|
||||
err := sdb.db.Open(time.Hour)
|
||||
if err == nil {
|
||||
// 创建配置表
|
||||
err = sdb.db.Create("config", &imageConfig{})
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("[ERROR]:", err))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
ctx.SendChain(message.Text("[ERROR]:", err))
|
||||
return false
|
||||
})
|
||||
|
||||
en.OnPrefix("设置AI画图密钥", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
apiKey := strings.TrimSpace(ctx.State["args"].(string))
|
||||
cfg := sdb.getConfig()
|
||||
err := sdb.setConfig(apiKey, cfg.APIURL, cfg.ModelName)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 设置API密钥失败: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功设置API密钥"))
|
||||
})
|
||||
|
||||
en.OnPrefix("设置AI画图接口地址", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
apiURL := strings.TrimSpace(ctx.State["args"].(string))
|
||||
cfg := sdb.getConfig()
|
||||
err := sdb.setConfig(cfg.APIKey, apiURL, cfg.ModelName)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 设置API地址失败: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功设置API地址"))
|
||||
})
|
||||
|
||||
en.OnPrefix("设置AI画图模型名", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
modelName := strings.TrimSpace(ctx.State["args"].(string))
|
||||
cfg := sdb.getConfig()
|
||||
err := sdb.setConfig(cfg.APIKey, cfg.APIURL, modelName)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 设置模型失败: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("成功设置模型: ", modelName))
|
||||
})
|
||||
|
||||
en.OnFullMatch("查看AI画图配置", getdb, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SendChain(message.Text(sdb.PrintConfig()))
|
||||
})
|
||||
|
||||
en.OnPrefix("AI画图", getdb).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SendChain(message.Text("少女思考中..."))
|
||||
prompt := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if prompt == "" {
|
||||
ctx.SendChain(message.Text("请输入图片描述"))
|
||||
return
|
||||
}
|
||||
|
||||
cfg := sdb.getConfig()
|
||||
if cfg.APIKey == "" || cfg.APIURL == "" || cfg.ModelName == "" {
|
||||
ctx.SendChain(message.Text("请先配置API密钥、地址和模型"))
|
||||
return
|
||||
}
|
||||
|
||||
// 准备请求数据
|
||||
reqBytes, _ := json.Marshal(map[string]interface{}{
|
||||
"model": cfg.ModelName,
|
||||
"prompt": prompt,
|
||||
"image_size": "1024x1024",
|
||||
"batch_size": 4,
|
||||
"num_inference_steps": 20,
|
||||
"guidance_scale": 7.5,
|
||||
})
|
||||
|
||||
// 发送API请求
|
||||
data, err := web.RequestDataWithHeaders(
|
||||
web.NewDefaultClient(),
|
||||
cfg.APIURL,
|
||||
"POST",
|
||||
func(req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return nil
|
||||
},
|
||||
bytes.NewReader(reqBytes),
|
||||
)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("API请求失败: ", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析API响应
|
||||
jsonData := gjson.ParseBytes(data)
|
||||
images := jsonData.Get("images")
|
||||
if !images.Exists() {
|
||||
images = jsonData.Get("data")
|
||||
if !images.Exists() {
|
||||
ctx.SendChain(message.Text("未获取到图片URL"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 发送生成的图片和相关信息
|
||||
inferenceTime := jsonData.Get("timings.inference").Float()
|
||||
seed := jsonData.Get("seed").Int()
|
||||
msg := make(message.Message, 0, 1)
|
||||
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Text("图片生成成功!\n",
|
||||
"提示词: ", prompt, "\n",
|
||||
"模型: ", cfg.ModelName, "\n",
|
||||
"推理时间: ", inferenceTime, "秒\n",
|
||||
"种子: ", seed)))
|
||||
|
||||
// 添加所有图片
|
||||
images.ForEach(func(_, value gjson.Result) bool {
|
||||
url := value.Get("url").String()
|
||||
if url != "" {
|
||||
msg = append(msg, ctxext.FakeSenderForwardNode(ctx, message.Image(url)))
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(msg) > 0 {
|
||||
ctx.Send(msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
134
plugin/airecord/record.go
Normal file
134
plugin/airecord/record.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Package airecord 群应用:AI声聊
|
||||
package airecord
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
|
||||
"github.com/FloatTech/AnimeAPI/airecord"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
)
|
||||
|
||||
func init() {
|
||||
en := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Extra: control.ExtraFromString("airecord"),
|
||||
Brief: "群应用:AI声聊",
|
||||
Help: "- 设置AI语音群号1048452984(tips:机器人任意所在群聊即可)\n" +
|
||||
"- 设置AI语音模型\n" +
|
||||
"- 查看AI语音配置\n" +
|
||||
"- 发送AI语音xxx",
|
||||
PrivateDataFolder: "airecord",
|
||||
})
|
||||
|
||||
en.OnPrefix("设置AI语音群号", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
u := strings.TrimSpace(ctx.State["args"].(string))
|
||||
num, err := strconv.ParseInt(u, 10, 64)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: parse gid err: ", err))
|
||||
return
|
||||
}
|
||||
err = airecord.SetCustomGID(num)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set gid err: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("设置AI语音群号为", num))
|
||||
})
|
||||
en.OnFullMatch("设置AI语音模型", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
next := zero.NewFutureEvent("message", 999, false, ctx.CheckSession())
|
||||
recv, cancel := next.Repeat()
|
||||
defer cancel()
|
||||
jsonData := ctx.GetAICharacters(0, 1)
|
||||
|
||||
// 转换为字符串数组
|
||||
var names []string
|
||||
// 初始化两个映射表
|
||||
nameToID := make(map[string]string)
|
||||
nameToURL := make(map[string]string)
|
||||
characters := jsonData.Get("#.characters")
|
||||
|
||||
// 遍历每个角色对象
|
||||
characters.ForEach(func(_, group gjson.Result) bool {
|
||||
group.ForEach(func(_, character gjson.Result) bool {
|
||||
// 提取当前角色的三个字段
|
||||
name := character.Get("character_name").String()
|
||||
names = append(names, name)
|
||||
// 存入映射表(重复名称会覆盖,保留最后出现的条目)
|
||||
nameToID[name] = character.Get("character_id").String()
|
||||
nameToURL[name] = character.Get("preview_url").String()
|
||||
return true // 继续遍历
|
||||
})
|
||||
return true // 继续遍历
|
||||
})
|
||||
var builder strings.Builder
|
||||
// 写入开头文本
|
||||
builder.WriteString("请选择语音模型序号:\n")
|
||||
|
||||
// 遍历names数组,拼接序号和名称
|
||||
for i, v := range names {
|
||||
// 将数字转换为字符串(不依赖fmt)
|
||||
numStr := strconv.Itoa(i)
|
||||
// 拼接格式:"序号. 名称\n"
|
||||
builder.WriteString(numStr)
|
||||
builder.WriteString(". ")
|
||||
builder.WriteString(v)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
// 获取最终字符串
|
||||
ctx.SendChain(message.Text(builder.String()))
|
||||
for {
|
||||
select {
|
||||
case <-time.After(time.Second * 120):
|
||||
ctx.SendChain(message.Text("设置AI语音模型指令过期"))
|
||||
return
|
||||
case ct := <-recv:
|
||||
msg := ct.Event.Message.ExtractPlainText()
|
||||
num, err := strconv.Atoi(msg)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("请输入数字!"))
|
||||
continue
|
||||
}
|
||||
if num < 0 || num >= len(names) {
|
||||
ctx.SendChain(message.Text("序号非法!"))
|
||||
continue
|
||||
}
|
||||
err = airecord.SetRecordModel(names[num], nameToID[names[num]])
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set model err: ", err))
|
||||
continue
|
||||
}
|
||||
ctx.SendChain(message.Text("已选择语音模型: ", names[num]))
|
||||
ctx.SendChain(message.Record(nameToURL[names[num]]))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
en.OnFullMatch("查看AI语音配置", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SendChain(message.Text(airecord.PrintRecordConfig()))
|
||||
})
|
||||
en.OnPrefix("发送AI语音", zero.UserOrGrpAdmin).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
u := strings.TrimSpace(ctx.State["args"].(string))
|
||||
recCfg := airecord.GetConfig()
|
||||
record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, u)
|
||||
if record == "" {
|
||||
id := ctx.SendGroupAIRecord(recCfg.ModelID, ctx.Event.GroupID, u)
|
||||
if id == "" {
|
||||
ctx.SendChain(message.Text("ERROR: get record err: empty record"))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.SendChain(message.Record(record))
|
||||
})
|
||||
}
|
||||
145
plugin/animetrace/main.go
Normal file
145
plugin/animetrace/main.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Package animetrace AnimeTrace 动画/Galgame识别
|
||||
package animetrace
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
"github.com/FloatTech/imgfactory"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/tidwall/gjson"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine := control.Register("animetrace", &ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "AnimeTrace 动画/Galgame识别插件",
|
||||
Help: "- Gal识图\n- 动漫识图\n- 动漫识图 2\n- 动漫识图 [模型名]\n- Gal识图 [模型名]",
|
||||
})
|
||||
|
||||
engine.OnPrefix("gal识图", zero.OnlyGroup, zero.MustProvidePicture).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := ctx.State["args"].(string)
|
||||
var model string
|
||||
switch strings.TrimSpace(args) {
|
||||
case "":
|
||||
model = "full_game_model_kira" // 默认使用的模型
|
||||
default:
|
||||
model = args // 自定义设置模型
|
||||
}
|
||||
processImageRecognition(ctx, model)
|
||||
})
|
||||
|
||||
engine.OnPrefix("动漫识图", zero.OnlyGroup, zero.MustProvidePicture).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
args := ctx.State["args"].(string)
|
||||
var model string
|
||||
switch strings.TrimSpace(args) {
|
||||
case "":
|
||||
model = "anime_model_lovelive"
|
||||
case "2":
|
||||
model = "pre_stable"
|
||||
default:
|
||||
model = args
|
||||
}
|
||||
processImageRecognition(ctx, model)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理图片识别
|
||||
func processImageRecognition(ctx *zero.Ctx, model string) {
|
||||
urls := ctx.State["image_url"].([]string)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
imageData, err := imgfactory.Load(urls[0])
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("下载图片失败: ", err))
|
||||
return
|
||||
}
|
||||
// ctx.Send(message.Text(model))
|
||||
respBody, err := createAndSendMultipartRequest("https://api.animetrace.com/v1/search", imageData, map[string]string{
|
||||
"is_multi": "0",
|
||||
"model": model,
|
||||
"ai_detect": "0",
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("识别请求失败: ", err))
|
||||
return
|
||||
}
|
||||
code := gjson.Get(string(respBody), "code").Int()
|
||||
if code != 0 {
|
||||
ctx.Send(message.Text("错误: ", gjson.Get(string(respBody), "zh_message").String()))
|
||||
return
|
||||
}
|
||||
dataArray := gjson.Get(string(respBody), "data").Array()
|
||||
if len(dataArray) == 0 {
|
||||
ctx.Send(message.Text("未识别到任何角色"))
|
||||
return
|
||||
}
|
||||
var sk message.Message
|
||||
sk = append(sk, ctxext.FakeSenderForwardNode(ctx, message.Text("共识别到 ", len(dataArray), " 个角色,可能是以下来源")))
|
||||
for _, value := range dataArray {
|
||||
boxArray := value.Get("box").Array()
|
||||
imgWidth, imgHeight := imageData.Bounds().Dx(), imageData.Bounds().Dy() // 你可以从 `imageData.Bounds()` 获取
|
||||
box := []int{
|
||||
int(boxArray[0].Float() * float64(imgWidth)),
|
||||
int(boxArray[1].Float() * float64(imgHeight)),
|
||||
int(boxArray[2].Float() * float64(imgWidth)),
|
||||
int(boxArray[3].Float() * float64(imgHeight)),
|
||||
}
|
||||
croppedImg := imaging.Crop(imageData, image.Rect(box[0], box[1], box[2], box[3]))
|
||||
var buf bytes.Buffer
|
||||
if err := imaging.Encode(&buf, croppedImg, imaging.JPEG, imaging.JPEGQuality(80)); err != nil {
|
||||
ctx.Send(message.Text("图片编码失败: ", err))
|
||||
continue
|
||||
}
|
||||
|
||||
base64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
var sb strings.Builder
|
||||
value.Get("character").ForEach(func(_, character gjson.Result) bool {
|
||||
sb.WriteString(fmt.Sprintf("《%s》的角色 %s\n", character.Get("work").String(), character.Get("character").String()))
|
||||
return true
|
||||
})
|
||||
sk = append(sk, ctxext.FakeSenderForwardNode(ctx, message.Image("base64://"+base64Str), message.Text(sb.String())))
|
||||
}
|
||||
ctx.SendGroupForwardMessage(ctx.Event.GroupID, sk)
|
||||
}
|
||||
|
||||
// 发送图片识别请求
|
||||
func createAndSendMultipartRequest(url string, img image.Image, formFields map[string]string) ([]byte, error) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// 直接编码图片
|
||||
part, err := writer.CreateFormFile("file", "image.jpg")
|
||||
if err != nil {
|
||||
return nil, errors.New("创建文件字段失败: " + err.Error())
|
||||
}
|
||||
if err := jpeg.Encode(part, img, &jpeg.Options{Quality: 80}); err != nil {
|
||||
return nil, errors.New("图片编码失败: " + err.Error())
|
||||
}
|
||||
|
||||
// 写入其他字段
|
||||
for key, value := range formFields {
|
||||
if err := writer.WriteField(key, value); err != nil {
|
||||
return nil, errors.New("写入表单字段失败 (" + key + "): " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, errors.New("关闭 multipart writer 失败: " + err.Error())
|
||||
}
|
||||
|
||||
return web.PostData(url, writer.FormDataContentType(), body)
|
||||
}
|
||||
@@ -17,7 +17,12 @@ import (
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
const bandur time.Duration = time.Minute * 10
|
||||
const (
|
||||
bandur time.Duration = time.Minute * 2
|
||||
add = "添加违禁词"
|
||||
del = "删除违禁词"
|
||||
list = "查看违禁词"
|
||||
)
|
||||
|
||||
var (
|
||||
managers *ctrl.Manager[*zero.Ctx] // managers lazy load
|
||||
@@ -41,7 +46,7 @@ func init() {
|
||||
engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "违禁词检测",
|
||||
Help: "- /[添加|删除|查看]违禁词",
|
||||
Help: "- [添加|删除|查看]违禁词",
|
||||
PrivateDataFolder: "anti_abuse",
|
||||
})
|
||||
|
||||
@@ -56,10 +61,14 @@ func init() {
|
||||
return true
|
||||
})
|
||||
|
||||
engine.OnMessage(onceRule, zero.OnlyGroup, func(ctx *zero.Ctx) bool {
|
||||
if !ctx.Event.IsToMe {
|
||||
return true
|
||||
notAntiabuse := func(ctx *zero.Ctx) bool {
|
||||
if zero.PrefixRule(add)(ctx) || zero.PrefixRule(del)(ctx) || zero.PrefixRule(list)(ctx) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
engine.OnMessage(onceRule, notAntiabuse, zero.OnlyGroup, func(ctx *zero.Ctx) bool {
|
||||
uid := ctx.Event.UserID
|
||||
gid := ctx.Event.GroupID
|
||||
msg := strings.ReplaceAll(ctx.MessageString(), "\n", "")
|
||||
@@ -70,7 +79,8 @@ func init() {
|
||||
if err := ctx.State["manager"].(*ctrl.Control[*zero.Ctx]).Manager.DoBlock(uid); err == nil {
|
||||
t := time.Now().Unix()
|
||||
cache.Set(uid, struct{}{})
|
||||
ctx.SetThisGroupBan(uid, int64(bandur.Minutes()))
|
||||
ctx.SetThisGroupBan(uid, int64(bandur.Seconds()))
|
||||
ctx.DeleteMessage(ctx.Event.MessageID)
|
||||
ctx.SendChain(message.Text("检测到违禁词, 已封禁/屏蔽", bandur))
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
@@ -92,9 +102,9 @@ func init() {
|
||||
return true
|
||||
})
|
||||
|
||||
engine.OnCommand("添加违禁词", zero.OnlyGroup, zero.AdminPermission, onceRule).Handle(
|
||||
engine.OnPrefix(add, zero.OnlyGroup, zero.AdminPermission, onceRule).SetBlock(true).Handle(
|
||||
func(ctx *zero.Ctx) {
|
||||
args := ctx.State["args"].(string)
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if err := db.insertWord(ctx.Event.GroupID, args); err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
} else {
|
||||
@@ -102,9 +112,9 @@ func init() {
|
||||
}
|
||||
})
|
||||
|
||||
engine.OnCommand("删除违禁词", zero.OnlyGroup, zero.AdminPermission, onceRule).Handle(
|
||||
engine.OnPrefix(del, zero.OnlyGroup, zero.AdminPermission, onceRule).SetBlock(true).Handle(
|
||||
func(ctx *zero.Ctx) {
|
||||
args := ctx.State["args"].(string)
|
||||
args := strings.TrimSpace(ctx.State["args"].(string))
|
||||
if err := db.deleteWord(ctx.Event.GroupID, args); err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
} else {
|
||||
@@ -112,7 +122,7 @@ func init() {
|
||||
}
|
||||
})
|
||||
|
||||
engine.OnCommand("查看违禁词", zero.OnlyGroup, onceRule).Handle(
|
||||
engine.OnPrefix(list, zero.OnlyGroup, onceRule).SetBlock(true).Handle(
|
||||
func(ctx *zero.Ctx) {
|
||||
b, err := text.RenderToBase64(db.listWords(ctx.Event.GroupID), text.FontFile, 400, 20)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,25 +2,33 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bz "github.com/FloatTech/AnimeAPI/bilibili"
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
"github.com/pkg/errors"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
const (
|
||||
enableHex = 0x10
|
||||
unableHex = 0x7fffffff_fffffffd
|
||||
enableVideoSummary = int64(0x10)
|
||||
disableVideoSummary = ^enableVideoSummary
|
||||
enableVideoDownload = int64(0x20)
|
||||
disableVideoDownload = ^enableVideoDownload
|
||||
bilibiliparseReferer = "https://www.bilibili.com"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -33,6 +41,7 @@ var (
|
||||
searchDynamicRe = regexp.MustCompile(searchDynamic)
|
||||
searchArticleRe = regexp.MustCompile(searchArticle)
|
||||
searchLiveRoomRe = regexp.MustCompile(searchLiveRoom)
|
||||
cachePath string
|
||||
)
|
||||
|
||||
// 插件主体
|
||||
@@ -42,6 +51,9 @@ func init() {
|
||||
Brief: "b站链接解析",
|
||||
Help: "例:- t.bilibili.com/642277677329285174\n- bilibili.com/read/cv17134450\n- bilibili.com/video/BV13B4y1x7pS\n- live.bilibili.com/22603245 ",
|
||||
})
|
||||
cachePath = en.DataFolder() + "cache/"
|
||||
_ = os.RemoveAll(cachePath)
|
||||
_ = os.MkdirAll(cachePath, 0755)
|
||||
en.OnRegex(`((b23|acg).tv|bili2233.cn)\\?/[0-9a-zA-Z]+`).SetBlock(true).Limit(limit.LimitByGroup).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
u := ctx.State["regex_matched"].([]string)[0]
|
||||
@@ -82,9 +94,9 @@ func init() {
|
||||
data := c.GetData(ctx.Event.GroupID)
|
||||
switch option {
|
||||
case "开启", "打开", "启用":
|
||||
data |= enableHex
|
||||
data |= enableVideoSummary
|
||||
case "关闭", "关掉", "禁用":
|
||||
data &= unableHex
|
||||
data &= disableVideoSummary
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -95,6 +107,35 @@ func init() {
|
||||
}
|
||||
ctx.SendChain(message.Text("已", option, "视频总结"))
|
||||
})
|
||||
en.OnRegex(`^(开启|打开|启用|关闭|关掉|禁用)视频上传$`, zero.AdminPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
if gid <= 0 {
|
||||
// 个人用户设为负数
|
||||
gid = -ctx.Event.UserID
|
||||
}
|
||||
option := ctx.State["regex_matched"].([]string)[1]
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("找不到服务!"))
|
||||
return
|
||||
}
|
||||
data := c.GetData(ctx.Event.GroupID)
|
||||
switch option {
|
||||
case "开启", "打开", "启用":
|
||||
data |= enableVideoDownload
|
||||
case "关闭", "关掉", "禁用":
|
||||
data &= disableVideoDownload
|
||||
default:
|
||||
return
|
||||
}
|
||||
err := c.SetData(gid, data)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("出错啦: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("已", option, "视频上传"))
|
||||
})
|
||||
en.OnRegex(searchVideo).SetBlock(true).Limit(limit.LimitByGroup).Handle(handleVideo)
|
||||
en.OnRegex(searchDynamic).SetBlock(true).Limit(limit.LimitByGroup).Handle(handleDynamic)
|
||||
en.OnRegex(searchArticle).SetBlock(true).Limit(limit.LimitByGroup).Handle(handleArticle)
|
||||
@@ -117,7 +158,7 @@ func handleVideo(ctx *zero.Ctx) {
|
||||
return
|
||||
}
|
||||
c, ok := ctx.State["manager"].(*ctrl.Control[*zero.Ctx])
|
||||
if ok && c.GetData(ctx.Event.GroupID)&enableHex == enableHex {
|
||||
if ok && c.GetData(ctx.Event.GroupID)&enableVideoSummary == enableVideoSummary {
|
||||
summaryMsg, err := getVideoSummary(cfg, card)
|
||||
if err != nil {
|
||||
msg = append(msg, message.Text("ERROR: ", err))
|
||||
@@ -126,6 +167,14 @@ func handleVideo(ctx *zero.Ctx) {
|
||||
}
|
||||
}
|
||||
ctx.SendChain(msg...)
|
||||
if ok && c.GetData(ctx.Event.GroupID)&enableVideoDownload == enableVideoDownload {
|
||||
downLoadMsg, err := getVideoDownload(cfg, card, cachePath)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(downLoadMsg...)
|
||||
}
|
||||
}
|
||||
|
||||
func handleDynamic(ctx *zero.Ctx) {
|
||||
@@ -147,7 +196,12 @@ func handleArticle(ctx *zero.Ctx) {
|
||||
}
|
||||
|
||||
func handleLive(ctx *zero.Ctx) {
|
||||
card, err := bz.GetLiveRoomInfo(ctx.State["regex_matched"].([]string)[1])
|
||||
cookie, err := cfg.Load()
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
card, err := bz.GetLiveRoomInfo(ctx.State["regex_matched"].([]string)[1], cookie)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
@@ -189,3 +243,47 @@ func getVideoSummary(cookiecfg *bz.CookieConfig, card bz.Card) (msg []message.Se
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getVideoDownload(cookiecfg *bz.CookieConfig, card bz.Card, cachePath string) (msg []message.Segment, err error) {
|
||||
var (
|
||||
data []byte
|
||||
videoDownload bz.VideoDownload
|
||||
stderr bytes.Buffer
|
||||
)
|
||||
today := time.Now().Format("20060102")
|
||||
videoFile := fmt.Sprintf("%s%s%s.mp4", cachePath, card.BvID, today)
|
||||
if file.IsExist(videoFile) {
|
||||
msg = append(msg, message.Video("file:///"+file.BOTPATH+"/"+videoFile))
|
||||
return
|
||||
}
|
||||
data, err = web.RequestDataWithHeaders(web.NewDefaultClient(), bz.SignURL(fmt.Sprintf(bz.VideoDownloadURL, card.BvID, card.CID)), "GET", func(req *http.Request) error {
|
||||
if cookiecfg != nil {
|
||||
cookie := ""
|
||||
cookie, err = cookiecfg.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("cookie", cookie)
|
||||
}
|
||||
req.Header.Set("User-Agent", ua)
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &videoDownload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
headers := fmt.Sprintf("User-Agent: %s\nReferer: %s", ua, bilibiliparseReferer)
|
||||
// 限制最多下载8分钟视频
|
||||
cmd := exec.Command("ffmpeg", "-ss", "0", "-t", "480", "-headers", headers, "-i", videoDownload.Data.Durl[0].URL, "-c", "copy", videoFile)
|
||||
cmd.Stderr = &stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
err = errors.Errorf("未配置ffmpeg,%v", stderr)
|
||||
return
|
||||
}
|
||||
msg = append(msg, message.Video("file:///"+file.BOTPATH+"/"+videoFile))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package bilibili
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
bz "github.com/FloatTech/AnimeAPI/bilibili"
|
||||
"github.com/FloatTech/floatbox/binary"
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
@@ -303,7 +305,10 @@ func liveCard2msg(card bz.RoomCard) (msg []message.Segment) {
|
||||
|
||||
// videoCard2msg 视频卡片转消息
|
||||
func videoCard2msg(card bz.Card) (msg []message.Segment, err error) {
|
||||
var mCard bz.MemberCard
|
||||
var (
|
||||
mCard bz.MemberCard
|
||||
onlineTotal bz.OnlineTotal
|
||||
)
|
||||
msg = make([]message.Segment, 0, 16)
|
||||
mCard, err = bz.GetMemberCard(card.Owner.Mid)
|
||||
msg = append(msg, message.Text("标题: ", card.Title, "\n"))
|
||||
@@ -313,16 +318,25 @@ func videoCard2msg(card bz.Card) (msg []message.Segment, err error) {
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
err = nil
|
||||
msg = append(msg, message.Text("UP主: ", card.Owner.Name, "\n"))
|
||||
} else {
|
||||
msg = append(msg, message.Text("UP主: ", card.Owner.Name, " 粉丝: ", bz.HumanNum(mCard.Fans), "\n"))
|
||||
}
|
||||
}
|
||||
msg = append(msg, message.Text("播放: ", bz.HumanNum(card.Stat.View), " 弹幕: ", bz.HumanNum(card.Stat.Danmaku)))
|
||||
msg = append(msg, message.Image(card.Pic))
|
||||
msg = append(msg, message.Text("\n点赞: ", bz.HumanNum(card.Stat.Like), " 投币: ", bz.HumanNum(card.Stat.Coin), "\n",
|
||||
"收藏: ", bz.HumanNum(card.Stat.Favorite), " 分享: ", bz.HumanNum(card.Stat.Share), "\n",
|
||||
data, err := web.GetData(fmt.Sprintf(bz.OnlineTotalURL, card.BvID, card.CID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &onlineTotal)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg = append(msg, message.Text("👀播放: ", bz.HumanNum(card.Stat.View), " 💬弹幕: ", bz.HumanNum(card.Stat.Danmaku),
|
||||
"\n👍点赞: ", bz.HumanNum(card.Stat.Like), " 💰投币: ", bz.HumanNum(card.Stat.Coin),
|
||||
"\n📁收藏: ", bz.HumanNum(card.Stat.Favorite), " 🔗分享: ", bz.HumanNum(card.Stat.Share),
|
||||
"\n📝简介: ", card.Desc,
|
||||
"\n🏄♂️ 总共 ", onlineTotal.Data.Total, " 人在观看,", onlineTotal.Data.Count, " 人在网页端观看\n",
|
||||
bz.VURL, card.BvID, "\n\n"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestVideoInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLiveRoomInfo(t *testing.T) {
|
||||
card, err := bz.GetLiveRoomInfo("83171")
|
||||
card, err := bz.GetLiveRoomInfo("83171", "b_ut=7;buvid3=0;i-wanna-go-back=-1;innersign=0;")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -43,8 +43,15 @@ func init() {
|
||||
})
|
||||
|
||||
engine.OnPrefix(`查询水群`, zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
param := ctx.State["args"].(string)
|
||||
var uid int64
|
||||
if len(ctx.Event.Message) > 1 && ctx.Event.Message[1].Type == "at" {
|
||||
uid, _ = strconv.ParseInt(ctx.Event.Message[1].Data["qq"], 10, 64)
|
||||
} else if param == "" {
|
||||
uid = ctx.Event.UserID
|
||||
}
|
||||
name := ctx.NickName()
|
||||
todayTime, todayMessage, totalTime, totalMessage := ctdb.getChatTime(ctx.Event.GroupID, ctx.Event.UserID)
|
||||
todayTime, todayMessage, totalTime, totalMessage := ctdb.getChatTime(ctx.Event.GroupID, uid)
|
||||
ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(fmt.Sprintf("%s今天水了%d分%d秒,发了%d条消息;总计水了%d分%d秒,发了%d条消息。", name, todayTime/60, todayTime%60, todayMessage, totalTime/60, totalTime%60, totalMessage)))
|
||||
})
|
||||
engine.OnFullMatch("查看水群排名", zero.OnlyGroup).Limit(ctxext.LimitByGroup).SetBlock(true).
|
||||
|
||||
95
plugin/crypter/fumo.go
Normal file
95
plugin/crypter/fumo.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Package crypter Fumo语
|
||||
package crypter
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Base64字符表
|
||||
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
|
||||
// Fumo语字符表 - 使用各种fumo变体来表示base64字符
|
||||
var fumoChars = []string{
|
||||
"fumo-", "Fumo-", "fUmo-", "fuMo-", "fumO-", "FUmo-", "FuMo-", "FumO-",
|
||||
"fUMo-", "fUmO-", "fuMO-", "FUMo-", "FUmO-", "fUMO-", "FUMO-", "fumo.",
|
||||
"Fumo.", "fUmo.", "fuMo.", "fumO.", "FUmo.", "FuMo.", "FumO.", "fUMo.",
|
||||
"fUmO.", "fuMO.", "FUMo.", "FUmO.", "fUMO.", "FUMO.", "fumo,", "Fumo,",
|
||||
"fUmo,", "fuMo,", "fumO,", "FUmo,", "FuMo,", "FumO,", "fUMo,", "fUmO,",
|
||||
"fuMO,", "FUMo,", "FuMO,", "fUMO,", "FUMO,", "fumo+", "Fumo+", "fUmo+",
|
||||
"fuMo+", "fumO+", "FUmo+", "FuMo+", "FumO+", "fUMo+", "fUmO+", "fuMO+",
|
||||
"FUMo+", "FUmO+", "fUMO+", "FUMO+", "fumo|", "Fumo|", "fUmo|", "fuMo|",
|
||||
"fumO|", "FUmo|", "FuMo|", "FumO|", "fUMo|", "fUmO|", "fuMO|", "fumo/",
|
||||
"Fumo/", "fUmo/",
|
||||
}
|
||||
|
||||
// Base64 2 Fumo
|
||||
// 创建编码映射表
|
||||
var encodeMap = make(map[byte]string)
|
||||
|
||||
// 创建解码映射表
|
||||
var decodeMap = make(map[string]byte)
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 64 && i < len(fumoChars); i++ {
|
||||
base64Char := base64Chars[i]
|
||||
fumoChar := fumoChars[i]
|
||||
|
||||
encodeMap[base64Char] = fumoChar
|
||||
decodeMap[fumoChar] = base64Char
|
||||
}
|
||||
}
|
||||
|
||||
// 加密
|
||||
func encryptFumo(text string) string {
|
||||
if text == "" {
|
||||
return "请输入要加密的文本"
|
||||
}
|
||||
textBytes := []byte(text)
|
||||
base64String := base64.StdEncoding.EncodeToString(textBytes)
|
||||
base64Body := strings.TrimRight(base64String, "=")
|
||||
paddingCount := len(base64String) - len(base64Body)
|
||||
var fumoBody strings.Builder
|
||||
for _, char := range base64Body {
|
||||
if fumoChar, exists := encodeMap[byte(char)]; exists {
|
||||
fumoBody.WriteString(fumoChar)
|
||||
} else {
|
||||
return fmt.Sprintf("Fumo加密失败: 未知字符 %c", char)
|
||||
}
|
||||
}
|
||||
result := fumoBody.String() + strings.Repeat("=", paddingCount)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 解密
|
||||
func decryptFumo(fumoText string) string {
|
||||
if fumoText == "" {
|
||||
return "请输入要解密的Fumo语密文"
|
||||
}
|
||||
fumoBody := strings.TrimRight(fumoText, "=")
|
||||
paddingCount := len(fumoText) - len(fumoBody)
|
||||
fumoPattern := regexp.MustCompile(`(\w+[-.,+|/])`)
|
||||
fumoWords := fumoPattern.FindAllString(fumoBody, -1)
|
||||
reconstructed := strings.Join(fumoWords, "")
|
||||
if reconstructed != fumoBody {
|
||||
return "Fumo解密失败: 包含无效的Fumo字符或格式错误"
|
||||
}
|
||||
var base64Body strings.Builder
|
||||
for _, fumoWord := range fumoWords {
|
||||
if base64Char, exists := decodeMap[fumoWord]; exists {
|
||||
base64Body.WriteByte(base64Char)
|
||||
} else {
|
||||
return fmt.Sprintf("Fumo解密失败: 包含无效的Fumo字符 %s", fumoWord)
|
||||
}
|
||||
}
|
||||
base64String := base64Body.String() + strings.Repeat("=", paddingCount)
|
||||
decodedBytes, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Fumo解密失败: Base64解码错误 %v", err)
|
||||
}
|
||||
originalText := string(decodedBytes)
|
||||
return originalText
|
||||
}
|
||||
42
plugin/crypter/handlers.go
Normal file
42
plugin/crypter/handlers.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Package crypter 处理函数
|
||||
package crypter
|
||||
|
||||
import (
|
||||
"github.com/FloatTech/AnimeAPI/airecord"
|
||||
"github.com/sirupsen/logrus"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
// hou
|
||||
func houEncryptHandler(ctx *zero.Ctx) {
|
||||
text := ctx.State["regex_matched"].([]string)[1]
|
||||
result := encodeHou(text)
|
||||
logrus.Infoln("[crypter] 回复内容:", result)
|
||||
recCfg := airecord.GetConfig()
|
||||
record := ctx.GetAIRecord(recCfg.ModelID, recCfg.Customgid, result)
|
||||
if record != "" {
|
||||
ctx.SendChain(message.Record(record))
|
||||
} else {
|
||||
ctx.SendChain(message.Text(result))
|
||||
}
|
||||
}
|
||||
|
||||
func houDecryptHandler(ctx *zero.Ctx) {
|
||||
text := ctx.State["regex_matched"].([]string)[1]
|
||||
result := decodeHou(text)
|
||||
ctx.SendChain(message.Text(result))
|
||||
}
|
||||
|
||||
// fumo
|
||||
func fumoEncryptHandler(ctx *zero.Ctx) {
|
||||
text := ctx.State["regex_matched"].([]string)[1]
|
||||
result := encryptFumo(text)
|
||||
ctx.SendChain(message.Text(result))
|
||||
}
|
||||
|
||||
func fumoDecryptHandler(ctx *zero.Ctx) {
|
||||
text := ctx.State["regex_matched"].([]string)[1]
|
||||
result := decryptFumo(text)
|
||||
ctx.SendChain(message.Text(result))
|
||||
}
|
||||
88
plugin/crypter/hou.go
Normal file
88
plugin/crypter/hou.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Package crypter 齁语加解密
|
||||
package crypter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 齁语密码表
|
||||
var houCodebook = []string{
|
||||
"齁", "哦", "噢", "喔", "咕", "咿", "嗯", "啊",
|
||||
"~", "哈", "!", "唔", "哼", "❤", "呃", "呼",
|
||||
}
|
||||
|
||||
// 索引: 0 1 2 3 4 5 6 7
|
||||
// 8 9 10 11 12 13 14 15
|
||||
|
||||
// 创建映射表
|
||||
var houCodebookMap = make(map[string]int)
|
||||
|
||||
// 初始化映射表
|
||||
func init() {
|
||||
for idx, ch := range houCodebook {
|
||||
houCodebookMap[ch] = idx
|
||||
}
|
||||
}
|
||||
|
||||
func encodeHou(text string) string {
|
||||
if text == "" {
|
||||
return "请输入要加密的文本"
|
||||
}
|
||||
var encoded strings.Builder
|
||||
textBytes := []byte(text)
|
||||
for _, b := range textBytes {
|
||||
high := (b >> 4) & 0x0F
|
||||
low := b & 0x0F
|
||||
encoded.WriteString(houCodebook[high])
|
||||
encoded.WriteString(houCodebook[low])
|
||||
}
|
||||
|
||||
return encoded.String()
|
||||
}
|
||||
|
||||
func decodeHou(code string) string {
|
||||
if code == "" {
|
||||
return "请输入要解密的齁语密文"
|
||||
}
|
||||
|
||||
// 过滤出有效的齁语字符
|
||||
var validChars []string
|
||||
for _, r := range code {
|
||||
charStr := string(r)
|
||||
if _, exists := houCodebookMap[charStr]; exists {
|
||||
validChars = append(validChars, charStr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validChars)%2 != 0 {
|
||||
return "齁语密文长度错误,无法解密"
|
||||
}
|
||||
|
||||
// 解密过程
|
||||
var byteList []byte
|
||||
for i := 0; i < len(validChars); i += 2 {
|
||||
highIdx, highExists := houCodebookMap[validChars[i]]
|
||||
lowIdx, lowExists := houCodebookMap[validChars[i+1]]
|
||||
|
||||
if !highExists || !lowExists {
|
||||
return "齁语密文包含无效字符"
|
||||
}
|
||||
|
||||
originalByte := byte((highIdx << 4) | lowIdx)
|
||||
byteList = append(byteList, originalByte)
|
||||
}
|
||||
|
||||
result := string(byteList)
|
||||
|
||||
if !isValidUTF8(result) {
|
||||
return "齁语解密失败,结果不是有效的文本"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 检查字符串是否为有效的UTF-8编码
|
||||
func isValidUTF8(s string) bool {
|
||||
// Go的string类型默认就是UTF-8,如果转换没有出错说明是有效的
|
||||
return len(s) > 0 || s == ""
|
||||
}
|
||||
31
plugin/crypter/main.go
Normal file
31
plugin/crypter/main.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Package crypter 奇怪语言加解密
|
||||
package crypter
|
||||
|
||||
import (
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine := control.Register("crypter", &ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "奇怪语言加解密",
|
||||
Help: "多种语言加解密插件\n" +
|
||||
"- 齁语加解密:\n" +
|
||||
"- 齁语加密 [文本] 或 h加密 [文本]\n" +
|
||||
"- 齁语解密 [密文] 或 h解密 [密文]\n\n" +
|
||||
"- Fumo语加解密:\n" +
|
||||
"- fumo加密 [文本]\n" +
|
||||
"- fumo解密 [密文]\n\n",
|
||||
PublicDataFolder: "Crypter",
|
||||
})
|
||||
|
||||
// hou
|
||||
engine.OnRegex(`^(?:齁语加密|h加密)\s*(.+)$`).SetBlock(true).Handle(houEncryptHandler)
|
||||
engine.OnRegex(`^(?:齁语解密|h解密)\s*(.+)$`).SetBlock(true).Handle(houDecryptHandler)
|
||||
|
||||
// Fumo
|
||||
engine.OnRegex(`^fumo加密\s*(.+)$`).SetBlock(true).Handle(fumoEncryptHandler)
|
||||
engine.OnRegex(`^fumo解密\s*(.+)$`).SetBlock(true).Handle(fumoDecryptHandler)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func dlchan(name string, s *string, wg *sync.WaitGroup, exit func(error)) {
|
||||
defer wg.Done()
|
||||
target := datapath + `materials/` + name
|
||||
if file.IsNotExist(target) {
|
||||
data, err := web.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/main/` + name)
|
||||
data, err := web.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/master/` + name)
|
||||
if err != nil {
|
||||
_ = os.Remove(target)
|
||||
exit(err)
|
||||
@@ -48,7 +48,7 @@ func dlchan(name string, s *string, wg *sync.WaitGroup, exit func(error)) {
|
||||
func dlblock(name string) (string, error) {
|
||||
target := datapath + `materials/` + name
|
||||
if file.IsNotExist(target) {
|
||||
data, err := web.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/main/` + name)
|
||||
data, err := web.GetData(`https://gitea.seku.su/fumiama/ImageMaterials/raw/branch/master/` + name)
|
||||
if err != nil {
|
||||
_ = os.Remove(target)
|
||||
return "", err
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package kfccrazythursday
|
||||
|
||||
import (
|
||||
"github.com/FloatTech/floatbox/binary"
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
@@ -11,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
crazyURL = "http://api.jixs.cc/api/wenan-fkxqs/index.php"
|
||||
crazyURL = "https://api.pearktrue.cn/api/kfc/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -26,6 +25,8 @@ func init() {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text(binary.BytesToString(data)))
|
||||
|
||||
// 根据来源API修改返回方式到直接输出文本
|
||||
ctx.SendChain(message.Text(string(data)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ const (
|
||||
"- 列出所有提醒\n" +
|
||||
"- 翻牌\n" +
|
||||
"- 赞我\n" +
|
||||
"- 群签到\n" +
|
||||
"- 对信息回复: 回应表情 [表情]\n" +
|
||||
"- 设置欢迎语XXX 可选添加 [{at}] [{nickname}] [{avatar}] [{uid}] [{gid}] [{groupname}]\n" +
|
||||
"- 测试欢迎语\n" +
|
||||
@@ -156,7 +157,7 @@ func init() { // 插件主体
|
||||
ctx.SendChain(message.Text("全员自闭结束~"))
|
||||
})
|
||||
// 禁言
|
||||
engine.OnMessage(zero.NewPattern().Text("^禁言").At().Text("(\\d+)\\s*(.*)").AsRule(), zero.OnlyGroup, zero.AdminPermission).SetBlock(true).
|
||||
engine.OnMessage(zero.NewPattern(nil).Text("^禁言").At().Text("(\\d+)\\s*(.*)").AsRule(), zero.OnlyGroup, zero.AdminPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
parsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed)
|
||||
duration := math.Str2Int64(parsed[2].Text()[1])
|
||||
@@ -405,6 +406,12 @@ func init() { // 插件主体
|
||||
ctx.SendLike(ctx.Event.UserID, 10)
|
||||
ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("给你赞了10下哦,记得回我~"))
|
||||
})
|
||||
// 群签到
|
||||
engine.OnFullMatch("群签到", zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SetGroupSign(ctx.Event.GroupID)
|
||||
ctx.SendChain(message.Text("群签到成功,可在手机端输入框中的打卡查看"))
|
||||
})
|
||||
facere := regexp.MustCompile(`\[CQ:face,id=(\d+)\]`)
|
||||
// 给消息回应表情
|
||||
engine.OnRegex(`^\[CQ:reply,id=(-?\d+)\].*回应表情\s*(.+)\s*$`, zero.AdminPermission, zero.OnlyGroup).SetBlock(true).
|
||||
@@ -650,7 +657,7 @@ func init() { // 插件主体
|
||||
if rsp.RetCode == 0 {
|
||||
ctx.SendChain(message.Text(option, "成功"))
|
||||
} else {
|
||||
ctx.SendChain(message.Text(option, "失败, 信息: ", rsp.Msg, "解释: ", rsp.Wording))
|
||||
ctx.SendChain(message.Text(option, "失败, 信息: ", rsp.Message, "解释: ", rsp.Wording))
|
||||
}
|
||||
})
|
||||
engine.OnCommand("精华列表", zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) {
|
||||
@@ -699,7 +706,7 @@ func init() { // 插件主体
|
||||
if rsp.RetCode == 0 {
|
||||
ctx.SendChain(message.Text("取消成功"))
|
||||
} else {
|
||||
ctx.SendChain(message.Text("取消失败, 信息: ", rsp.Msg, "解释: ", rsp.Wording))
|
||||
ctx.SendChain(message.Text("取消失败, 信息: ", rsp.Message, "解释: ", rsp.Wording))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
301
plugin/minecraftobserver/minecraftobserver.go
Normal file
301
plugin/minecraftobserver/minecraftobserver.go
Normal file
@@ -0,0 +1,301 @@
|
||||
// Package minecraftobserver 通过mc服务器地址获取服务器状态信息并绘制图片发送到QQ群
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
zbpCtxExt "github.com/FloatTech/zbputils/ctxext"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "minecraftobserver"
|
||||
)
|
||||
|
||||
var (
|
||||
// 注册插件
|
||||
engine = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
// 默认不启动
|
||||
DisableOnDefault: false,
|
||||
Brief: "Minecraft服务器状态查询/订阅",
|
||||
// 详细帮助
|
||||
Help: "- mc服务器状态 [服务器IP/URI]\n" +
|
||||
"- mc服务器添加订阅 [服务器IP/URI]\n" +
|
||||
"- mc服务器取消订阅 [服务器IP/URI]\n" +
|
||||
"- mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个)" +
|
||||
"-----------------------\n" +
|
||||
"使用job插件设置定时, 例:" +
|
||||
"记录在\"@every 1m\"触发的指令\n" +
|
||||
"(机器人回答:您的下一条指令将被记录,在@@every 1m时触发)" +
|
||||
"mc服务器订阅拉取",
|
||||
// 插件数据存储路径
|
||||
PrivateDataFolder: name,
|
||||
}).ApplySingle(zbpCtxExt.DefaultSingle)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 状态查询
|
||||
engine.OnRegex("^[mM][cC]服务器状态 (.+)$").SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
// 关键词查找
|
||||
addr := ctx.State["regex_matched"].([]string)[1]
|
||||
resp, err := getMinecraftServerStatus(addr)
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("服务器状态获取失败... 错误信息: ", err))
|
||||
return
|
||||
}
|
||||
status := resp.genServerSubscribeSchema(addr, 0)
|
||||
textMsg, iconBase64 := status.generateServerStatusMsg()
|
||||
var msg message.Message
|
||||
if iconBase64 != "" {
|
||||
msg = append(msg, message.Image(iconBase64))
|
||||
}
|
||||
msg = append(msg, message.Text(textMsg))
|
||||
if id := ctx.Send(msg); id.ID() == 0 {
|
||||
// logrus.Errorln(logPrefix + "Send failed")
|
||||
return
|
||||
}
|
||||
})
|
||||
// 添加订阅
|
||||
engine.OnRegex(`^[mM][cC]服务器添加订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
// 关键词查找
|
||||
addr := ctx.State["regex_matched"].([]string)[1]
|
||||
status, err := getMinecraftServerStatus(addr)
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("服务器信息初始化失败,请检查服务器是否可用!\n错误信息: ", err))
|
||||
return
|
||||
}
|
||||
targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)
|
||||
err = dbInstance.newSubscribe(addr, targetID, targetType)
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("订阅添加失败... 错误信息: ", err))
|
||||
return
|
||||
}
|
||||
// 插入数据库(首条,需要更新状态)
|
||||
err = dbInstance.updateServerStatus(status.genServerSubscribeSchema(addr, 0))
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("服务器状态更新失败... 错误信息: ", err))
|
||||
return
|
||||
}
|
||||
if sid := ctx.Send(message.Text(fmt.Sprintf("服务器 %s 订阅添加成功", addr))); sid.ID() == 0 {
|
||||
// logrus.Errorln(logPrefix + "Send failed")
|
||||
return
|
||||
}
|
||||
// 成功后立即发送一次状态
|
||||
textMsg, iconBase64 := status.genServerSubscribeSchema(addr, 0).generateServerStatusMsg()
|
||||
var msg message.Message
|
||||
if iconBase64 != "" {
|
||||
msg = append(msg, message.Image(iconBase64))
|
||||
}
|
||||
msg = append(msg, message.Text(textMsg))
|
||||
if id := ctx.Send(msg); id.ID() == 0 {
|
||||
// logrus.Errorln(logPrefix + "Send failed")
|
||||
return
|
||||
}
|
||||
})
|
||||
// 删除
|
||||
engine.OnRegex(`^[mM][cC]服务器取消订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
addr := ctx.State["regex_matched"].([]string)[1]
|
||||
// 通过群组id和服务器地址获取服务器状态
|
||||
targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)
|
||||
err := dbInstance.deleteSubscribe(addr, targetID, targetType)
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("取消订阅失败...", fmt.Sprintf("错误信息: %v", err)))
|
||||
return
|
||||
}
|
||||
ctx.Send(message.Text("取消订阅成功"))
|
||||
})
|
||||
// 查看当前渠道的所有订阅
|
||||
engine.OnRegex(`^[mM][cC]服务器订阅列表$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
subList, err := dbInstance.getSubscribesByTarget(warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID))
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("获取订阅列表失败... 错误信息: ", err))
|
||||
return
|
||||
}
|
||||
if len(subList) == 0 {
|
||||
ctx.Send(message.Text("当前没有订阅哦"))
|
||||
return
|
||||
}
|
||||
stringBuilder := strings.Builder{}
|
||||
stringBuilder.WriteString("[订阅列表]\n")
|
||||
for _, v := range subList {
|
||||
stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", v.ServerAddr))
|
||||
}
|
||||
if sid := ctx.Send(message.Text(stringBuilder.String())); sid.ID() == 0 {
|
||||
// logrus.Errorln(logPrefix + "Send failed")
|
||||
return
|
||||
}
|
||||
})
|
||||
// 查看全局订阅情况(仅限管理员私聊可用)
|
||||
engine.OnRegex(`^[mM][cC]服务器全局订阅列表$`, zero.OnlyPrivate, zero.SuperUserPermission, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
subList, err := dbInstance.getAllSubscribes()
|
||||
if err != nil {
|
||||
ctx.Send(message.Text("获取全局订阅列表失败... 错误信息: ", err))
|
||||
return
|
||||
}
|
||||
if len(subList) == 0 {
|
||||
ctx.Send(message.Text("当前一个订阅都没有哦"))
|
||||
return
|
||||
}
|
||||
userID := ctx.Event.UserID
|
||||
userName := ctx.CardOrNickName(userID)
|
||||
msg := make(message.Message, 0)
|
||||
|
||||
// 按照群组or用户分组来定
|
||||
groupSubMap := make(map[int64][]serverSubscribe)
|
||||
userSubMap := make(map[int64][]serverSubscribe)
|
||||
for _, v := range subList {
|
||||
switch v.TargetType {
|
||||
case targetTypeGroup:
|
||||
groupSubMap[v.TargetID] = append(groupSubMap[v.TargetID], v)
|
||||
case targetTypeUser:
|
||||
userSubMap[v.TargetID] = append(userSubMap[v.TargetID], v)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// 群
|
||||
for k, v := range groupSubMap {
|
||||
stringBuilder := strings.Builder{}
|
||||
stringBuilder.WriteString(fmt.Sprintf("[群 %d]存在以下订阅:\n", k))
|
||||
for _, sub := range v {
|
||||
stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr))
|
||||
}
|
||||
msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String()))
|
||||
}
|
||||
// 个人
|
||||
for k, v := range userSubMap {
|
||||
stringBuilder := strings.Builder{}
|
||||
stringBuilder.WriteString(fmt.Sprintf("[用户 %d]存在以下订阅:\n", k))
|
||||
for _, sub := range v {
|
||||
stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr))
|
||||
}
|
||||
msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String()))
|
||||
}
|
||||
// 合并发送
|
||||
ctx.SendPrivateForwardMessage(ctx.Event.UserID, msg)
|
||||
})
|
||||
// 状态变更通知,全局触发,逐个服务器检查,检查到变更则逐个发送通知
|
||||
engine.OnRegex(`^[mM][cC]服务器订阅拉取$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
serverList, err := dbInstance.getAllSubscribes()
|
||||
if err != nil {
|
||||
su := zero.BotConfig.SuperUsers[0]
|
||||
// 如果订阅列表获取失败,通知管理员
|
||||
ctx.SendPrivateMessage(su, message.Text(logPrefix, "获取订阅列表失败..."))
|
||||
return
|
||||
}
|
||||
// logrus.Debugln(logPrefix+"global get ", len(serverList), " subscribe(s)")
|
||||
serverMap := make(map[string][]serverSubscribe)
|
||||
for _, v := range serverList {
|
||||
serverMap[v.ServerAddr] = append(serverMap[v.ServerAddr], v)
|
||||
}
|
||||
changedCount := 0
|
||||
for subAddr, oneServerSubList := range serverMap {
|
||||
// 查询当前存储的状态
|
||||
storedStatus, sErr := dbInstance.getServerStatus(subAddr)
|
||||
if sErr != nil {
|
||||
// logrus.Errorln(logPrefix+fmt.Sprintf("getServerStatus ServerAddr(%s) error: ", subAddr), sErr)
|
||||
continue
|
||||
}
|
||||
isChanged, changedNotifyMsg, sErr := singleServerScan(storedStatus)
|
||||
if sErr != nil {
|
||||
// logrus.Errorln(logPrefix+"singleServerScan error: ", sErr)
|
||||
continue
|
||||
}
|
||||
if !isChanged {
|
||||
continue
|
||||
}
|
||||
changedCount++
|
||||
// 发送变化信息
|
||||
for _, subInfo := range oneServerSubList {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if subInfo.TargetType == targetTypeUser {
|
||||
ctx.SendPrivateMessage(subInfo.TargetID, changedNotifyMsg)
|
||||
} else if subInfo.TargetType == targetTypeGroup {
|
||||
m, ok := control.Lookup(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !m.IsEnabledIn(subInfo.TargetID) {
|
||||
continue
|
||||
}
|
||||
ctx.SendGroupMessage(subInfo.TargetID, changedNotifyMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// singleServerScan 单个服务器状态扫描
|
||||
func singleServerScan(oldSubStatus *serverStatus) (changed bool, notifyMsg message.Message, err error) {
|
||||
notifyMsg = make(message.Message, 0)
|
||||
newSubStatus := &serverStatus{}
|
||||
// 获取服务器状态 & 检查是否需要更新
|
||||
rawServerStatus, err := getMinecraftServerStatus(oldSubStatus.ServerAddr)
|
||||
if err != nil {
|
||||
// logrus.Warnln(logPrefix+"getMinecraftServerStatus error: ", err)
|
||||
err = nil
|
||||
// 计数器没有超限,增加计数器并跳过
|
||||
if cnt, ts := addPingServerUnreachableCounter(oldSubStatus.ServerAddr, time.Now()); cnt < pingServerUnreachableCounterThreshold &&
|
||||
time.Since(ts) < pingServerUnreachableCounterTimeThreshold {
|
||||
// logrus.Warnln(logPrefix+"server ", oldSubStatus.ServerAddr, " unreachable, counter: ", cnt, " ts:", ts)
|
||||
return
|
||||
}
|
||||
// 不可达计数器已经超限,则更新服务器状态
|
||||
// 深拷贝,设置PingDelay为不可达
|
||||
newSubStatus = oldSubStatus.deepCopy()
|
||||
newSubStatus.PingDelay = pingDelayUnreachable
|
||||
} else {
|
||||
newSubStatus = rawServerStatus.genServerSubscribeSchema(oldSubStatus.ServerAddr, oldSubStatus.ID)
|
||||
}
|
||||
if newSubStatus == nil {
|
||||
// logrus.Errorln(logPrefix + "newSubStatus is nil")
|
||||
return
|
||||
}
|
||||
// 检查是否有订阅信息变化
|
||||
if oldSubStatus.isServerStatusSpecChanged(newSubStatus) {
|
||||
// logrus.Warnf(logPrefix+"server subscribe spec changed: (%+v) -> (%+v)", oldSubStatus, newSubStatus)
|
||||
changed = true
|
||||
// 更新数据库
|
||||
err = dbInstance.updateServerStatus(newSubStatus)
|
||||
if err != nil {
|
||||
// logrus.Errorln(logPrefix+"updateServerSubscribeStatus error: ", err)
|
||||
return
|
||||
}
|
||||
// 纯文本信息
|
||||
notifyMsg = append(notifyMsg, message.Text(formatSubStatusChangeText(oldSubStatus, newSubStatus)))
|
||||
// 如果有图标变更
|
||||
if oldSubStatus.FaviconMD5 != newSubStatus.FaviconMD5 {
|
||||
// 有图标变更
|
||||
notifyMsg = append(notifyMsg, message.Text("\n-----[图标变更]-----\n"))
|
||||
// 旧图标
|
||||
notifyMsg = append(notifyMsg, message.Text("[旧]\n"))
|
||||
if oldSubStatus.FaviconRaw != "" {
|
||||
notifyMsg = append(notifyMsg, message.Image(oldSubStatus.FaviconRaw.toBase64String()))
|
||||
} else {
|
||||
notifyMsg = append(notifyMsg, message.Text("(空)\n"))
|
||||
}
|
||||
// 新图标
|
||||
notifyMsg = append(notifyMsg, message.Text("[新]\n"))
|
||||
if newSubStatus.FaviconRaw != "" {
|
||||
notifyMsg = append(notifyMsg, message.Image(newSubStatus.FaviconRaw.toBase64String()))
|
||||
} else {
|
||||
notifyMsg = append(notifyMsg, message.Text("(空)\n"))
|
||||
}
|
||||
}
|
||||
notifyMsg = append(notifyMsg, message.Text("\n-------最新状态-------\n"))
|
||||
// 服务状态
|
||||
textMsg, iconBase64 := newSubStatus.generateServerStatusMsg()
|
||||
if iconBase64 != "" {
|
||||
notifyMsg = append(notifyMsg, message.Image(iconBase64))
|
||||
}
|
||||
notifyMsg = append(notifyMsg, message.Text(textMsg))
|
||||
}
|
||||
// 逻辑到达这里,说明状态已经变更 or 无变更且服务器可达,重置不可达计数器
|
||||
resetPingServerUnreachableCounter(oldSubStatus.ServerAddr)
|
||||
return
|
||||
}
|
||||
127
plugin/minecraftobserver/minecraftobserver_test.go
Normal file
127
plugin/minecraftobserver/minecraftobserver_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_singleServerScan(t *testing.T) {
|
||||
initErr := initializeDB("data/minecraftobserver/" + dbPath)
|
||||
if initErr != nil {
|
||||
t.Fatalf("initializeDB() error = %v", initErr)
|
||||
}
|
||||
if dbInstance == nil {
|
||||
t.Fatalf("initializeDB() got = %v, want not nil", dbInstance)
|
||||
}
|
||||
t.Run("状态变更", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS1 := &serverStatus{
|
||||
ServerAddr: "cn.nekoland.top",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "",
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("cn.nekoland.top", 123456, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("getServerSubscribeByTargetGroupAndAddr() error = %v", err)
|
||||
}
|
||||
changed, msg, err := singleServerScan(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("singleServerScan() error = %v", err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatalf("singleServerScan() got = %v, want true", changed)
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
t.Fatalf("singleServerScan() got = %v, want not empty", msg)
|
||||
}
|
||||
fmt.Printf("msg: %v\n", msg)
|
||||
})
|
||||
|
||||
t.Run("可达 -> 不可达", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS1 := &serverStatus{
|
||||
ServerAddr: "dx.123213213123123.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "",
|
||||
PingDelay: 123,
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.123213213123123.net", 123456, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("getServerSubscribeByTargetGroupAndAddr() error = %v", err)
|
||||
}
|
||||
var msg message.Message
|
||||
changed, _, err := singleServerScan(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("singleServerScan() error = %v", err)
|
||||
}
|
||||
if changed {
|
||||
t.Fatalf("singleServerScan() got = %v, want false", changed)
|
||||
}
|
||||
// 第二次
|
||||
changed, _, err = singleServerScan(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("singleServerScan() error = %v", err)
|
||||
}
|
||||
if changed {
|
||||
t.Fatalf("singleServerScan() got = %v, want false", changed)
|
||||
}
|
||||
// 第三次
|
||||
changed, msg, err = singleServerScan(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("singleServerScan() error = %v", err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatalf("singleServerScan() got = %v, want true", changed)
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
t.Fatalf("singleServerScan() got = %v, want not empty", msg)
|
||||
}
|
||||
fmt.Printf("msg: %v\n", msg)
|
||||
|
||||
})
|
||||
|
||||
t.Run("不可达 -> 可达", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS1 := &serverStatus{
|
||||
ServerAddr: "cn.nekoland.top",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "",
|
||||
PingDelay: pingDelayUnreachable,
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("cn.nekoland.top", 123456, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("newSubscribe() error = %v", err)
|
||||
}
|
||||
changed, msg, err := singleServerScan(newSS1)
|
||||
if err != nil {
|
||||
t.Fatalf("singleServerScan() error = %v", err)
|
||||
}
|
||||
if !changed {
|
||||
t.Fatalf("singleServerScan() got = %v, want true", changed)
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
t.Fatalf("singleServerScan() got = %v, want not empty", msg)
|
||||
}
|
||||
fmt.Printf("msg: %v\n", msg)
|
||||
})
|
||||
|
||||
}
|
||||
253
plugin/minecraftobserver/model.go
Normal file
253
plugin/minecraftobserver/model.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tnze/go-mc/chat"
|
||||
"github.com/google/uuid"
|
||||
"github.com/wdvxdr1123/ZeroBot/utils/helper"
|
||||
)
|
||||
|
||||
// ====================
|
||||
// DB Schema
|
||||
|
||||
// serverStatus 服务器状态
|
||||
type serverStatus struct {
|
||||
// ID 主键
|
||||
ID int64 `json:"id" gorm:"column:id;primary_key:pk_id;auto_increment;default:0"`
|
||||
// 服务器地址
|
||||
ServerAddr string `json:"server_addr" gorm:"column:server_addr;default:'';unique_index:udx_server_addr"`
|
||||
// 服务器描述
|
||||
Description string `json:"description" gorm:"column:description;default:null;type:CLOB"`
|
||||
// 在线玩家
|
||||
Players string `json:"players" gorm:"column:players;default:''"`
|
||||
// 版本
|
||||
Version string `json:"version" gorm:"column:version;default:''"`
|
||||
// FaviconMD5 Favicon MD5
|
||||
FaviconMD5 string `json:"favicon_md5" gorm:"column:favicon_md5;default:''"`
|
||||
// FaviconRaw 原始数据
|
||||
FaviconRaw icon `json:"favicon_raw" gorm:"column:favicon_raw;default:null;type:CLOB"`
|
||||
// 延迟,不可达时为-1
|
||||
PingDelay int64 `json:"ping_delay" gorm:"column:ping_delay;default:-1"`
|
||||
// 更新时间
|
||||
LastUpdate int64 `json:"last_update" gorm:"column:last_update;default:0"`
|
||||
}
|
||||
|
||||
// serverSubscribe 订阅信息
|
||||
type serverSubscribe struct {
|
||||
// ID 主键
|
||||
ID int64 `json:"id" gorm:"column:id;primary_key:pk_id;auto_increment;default:0"`
|
||||
// 服务器地址
|
||||
ServerAddr string `json:"server_addr" gorm:"column:server_addr;default:'';unique_index:udx_ait"`
|
||||
// 推送目标id
|
||||
TargetID int64 `json:"target_id" gorm:"column:target_id;default:0;unique_index:udx_ait"`
|
||||
// 类型 1:群组 2:个人
|
||||
TargetType int64 `json:"target_type" gorm:"column:target_type;default:0;unique_index:udx_ait"`
|
||||
// 更新时间
|
||||
LastUpdate int64 `json:"last_update" gorm:"column:last_update;default:0"`
|
||||
}
|
||||
|
||||
const (
|
||||
// pingDelayUnreachable 不可达
|
||||
pingDelayUnreachable = -1
|
||||
)
|
||||
|
||||
// isServerStatusSpecChanged 检查是否有状态变化
|
||||
func (ss *serverStatus) isServerStatusSpecChanged(newStatus *serverStatus) (res bool) {
|
||||
res = false
|
||||
if ss == nil || newStatus == nil {
|
||||
res = false
|
||||
return
|
||||
}
|
||||
// 描述变化、版本变化、Favicon变化
|
||||
if ss.Description != newStatus.Description || ss.Version != newStatus.Version || ss.FaviconMD5 != newStatus.FaviconMD5 {
|
||||
res = true
|
||||
return
|
||||
}
|
||||
// 状态由不可达变为可达 or 反之
|
||||
if (ss.PingDelay == pingDelayUnreachable && newStatus.PingDelay != pingDelayUnreachable) ||
|
||||
(ss.PingDelay != pingDelayUnreachable && newStatus.PingDelay == pingDelayUnreachable) {
|
||||
res = true
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// deepCopy 深拷贝
|
||||
func (ss *serverStatus) deepCopy() (dst *serverStatus) {
|
||||
if ss == nil {
|
||||
return
|
||||
}
|
||||
dst = &serverStatus{}
|
||||
*dst = *ss
|
||||
return
|
||||
}
|
||||
|
||||
// generateServerStatusMsg 生成服务器状态消息
|
||||
func (ss *serverStatus) generateServerStatusMsg() (msg string, iconBase64 string) {
|
||||
var msgBuilder strings.Builder
|
||||
if ss == nil {
|
||||
return
|
||||
}
|
||||
msgBuilder.WriteString(ss.Description)
|
||||
msgBuilder.WriteString("\n")
|
||||
msgBuilder.WriteString("服务器地址:")
|
||||
msgBuilder.WriteString(ss.ServerAddr)
|
||||
msgBuilder.WriteString("\n")
|
||||
// 版本
|
||||
msgBuilder.WriteString("版本:")
|
||||
msgBuilder.WriteString(ss.Version)
|
||||
msgBuilder.WriteString("\n")
|
||||
// Ping
|
||||
if ss.PingDelay < 0 {
|
||||
msgBuilder.WriteString("Ping延迟:超时\n")
|
||||
} else {
|
||||
msgBuilder.WriteString("Ping延迟:")
|
||||
msgBuilder.WriteString(fmt.Sprintf("%d 毫秒\n", ss.PingDelay))
|
||||
msgBuilder.WriteString("在线人数:")
|
||||
msgBuilder.WriteString(ss.Players)
|
||||
}
|
||||
// 图标
|
||||
if ss.FaviconRaw != "" && ss.FaviconRaw.checkPNG() {
|
||||
iconBase64 = ss.FaviconRaw.toBase64String()
|
||||
}
|
||||
msg = msgBuilder.String()
|
||||
return
|
||||
}
|
||||
|
||||
// DB Schema End
|
||||
|
||||
// ====================
|
||||
// Ping & List Response DTO
|
||||
|
||||
// serverPingAndListResp 服务器状态数据传输对象 From mc server response
|
||||
type serverPingAndListResp struct {
|
||||
Description chat.Message
|
||||
Players struct {
|
||||
Max int
|
||||
Online int
|
||||
Sample []struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
}
|
||||
}
|
||||
Version struct {
|
||||
Name string
|
||||
Protocol int
|
||||
}
|
||||
Favicon icon
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// icon should be a PNG image that is Base64 encoded
|
||||
// (without newlines: \n, new lines no longer work since 1.13)
|
||||
// and prepended with "data:image/png;base64,".
|
||||
type icon string
|
||||
|
||||
// func (i icon) toImage() (icon image.Image, err error) {
|
||||
// const prefix = "data:image/png;base64,"
|
||||
// if !strings.HasPrefix(string(i), prefix) {
|
||||
// return nil, errors.Errorf("server icon should prepended with %s", prefix)
|
||||
// }
|
||||
// base64png := strings.TrimPrefix(string(i), prefix)
|
||||
// r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64png))
|
||||
// icon, err = png.Decode(r)
|
||||
// return
|
||||
//}
|
||||
|
||||
// checkPNG 检查是否为PNG
|
||||
func (i icon) checkPNG() bool {
|
||||
const prefix = "data:image/png;base64,"
|
||||
return strings.HasPrefix(string(i), prefix)
|
||||
}
|
||||
|
||||
// toBase64String 转换为base64字符串
|
||||
func (i icon) toBase64String() string {
|
||||
return "base64://" + strings.TrimPrefix(string(i), "data:image/png;base64,")
|
||||
}
|
||||
|
||||
// genServerSubscribeSchema 将DTO转换为DB Schema
|
||||
func (dto *serverPingAndListResp) genServerSubscribeSchema(addr string, id int64) *serverStatus {
|
||||
if dto == nil {
|
||||
return nil
|
||||
}
|
||||
faviconMD5 := md5.Sum(helper.StringToBytes(string(dto.Favicon)))
|
||||
return &serverStatus{
|
||||
ID: id,
|
||||
ServerAddr: addr,
|
||||
Description: dto.Description.ClearString(),
|
||||
Version: dto.Version.Name,
|
||||
Players: fmt.Sprintf("%d/%d", dto.Players.Online, dto.Players.Max),
|
||||
FaviconMD5: hex.EncodeToString(faviconMD5[:]),
|
||||
FaviconRaw: dto.Favicon,
|
||||
PingDelay: dto.Delay.Milliseconds(),
|
||||
LastUpdate: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// Ping & List Response DTO End
|
||||
// ====================
|
||||
|
||||
// ====================
|
||||
// Biz Model
|
||||
const (
|
||||
logPrefix = "[minecraft observer] "
|
||||
)
|
||||
|
||||
// warpTargetIDAndType 转换消息信息到订阅的目标ID和类型
|
||||
func warpTargetIDAndType(groupID, userID int64) (int64, int64) {
|
||||
// 订阅
|
||||
var targetID int64
|
||||
var targetType int64
|
||||
if groupID == 0 {
|
||||
targetType = targetTypeUser
|
||||
targetID = userID
|
||||
} else {
|
||||
targetType = targetTypeGroup
|
||||
targetID = groupID
|
||||
}
|
||||
return targetID, targetType
|
||||
}
|
||||
|
||||
// formatSubStatusChangeText 格式化状态变更文本
|
||||
func formatSubStatusChangeText(oldStatus, newStatus *serverStatus) string {
|
||||
var msgBuilder strings.Builder
|
||||
if oldStatus == nil || newStatus == nil {
|
||||
return ""
|
||||
}
|
||||
// 变更通知
|
||||
msgBuilder.WriteString("[Minecraft服务器状态变更通知]\n")
|
||||
// 地址
|
||||
msgBuilder.WriteString(fmt.Sprintf("服务器地址: %v\n", oldStatus.ServerAddr))
|
||||
// 描述
|
||||
if oldStatus.Description != newStatus.Description {
|
||||
msgBuilder.WriteString("\n-----[描述变更]-----\n")
|
||||
msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v\n", oldStatus.Description))
|
||||
msgBuilder.WriteString(fmt.Sprintf("[新]\n%v\n", newStatus.Description))
|
||||
}
|
||||
// 版本
|
||||
if oldStatus.Version != newStatus.Version {
|
||||
msgBuilder.WriteString("\n-----[版本变更]-----\n")
|
||||
msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v\n", oldStatus.Version))
|
||||
msgBuilder.WriteString(fmt.Sprintf("[新]\n%v\n", newStatus.Version))
|
||||
}
|
||||
// 状态由不可达变为可达,反之
|
||||
if oldStatus.PingDelay == pingDelayUnreachable && newStatus.PingDelay != pingDelayUnreachable {
|
||||
msgBuilder.WriteString("\n-----[Ping延迟]-----\n")
|
||||
msgBuilder.WriteString("[旧]\n超时\n")
|
||||
msgBuilder.WriteString(fmt.Sprintf("[新]\n%v毫秒\n", newStatus.PingDelay))
|
||||
}
|
||||
if oldStatus.PingDelay != pingDelayUnreachable && newStatus.PingDelay == pingDelayUnreachable {
|
||||
msgBuilder.WriteString("\n-----[Ping延迟]-----\n")
|
||||
msgBuilder.WriteString(fmt.Sprintf("[旧]\n%v毫秒\n", oldStatus.PingDelay))
|
||||
msgBuilder.WriteString("[新]\n超时\n")
|
||||
}
|
||||
return msgBuilder.String()
|
||||
}
|
||||
|
||||
// Biz Model End
|
||||
// ====================
|
||||
63
plugin/minecraftobserver/ping.go
Normal file
63
plugin/minecraftobserver/ping.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/RomiChan/syncx"
|
||||
"github.com/Tnze/go-mc/bot"
|
||||
)
|
||||
|
||||
var (
|
||||
// pingServerUnreachableCounter Ping服务器不可达计数器,防止bot本体网络抖动导致误报
|
||||
pingServerUnreachableCounter = syncx.Map[string, pingServerUnreachableCounterDef]{}
|
||||
// 计数器阈值
|
||||
pingServerUnreachableCounterThreshold = int64(3)
|
||||
// 时间阈值
|
||||
pingServerUnreachableCounterTimeThreshold = time.Minute * 30
|
||||
)
|
||||
|
||||
type pingServerUnreachableCounterDef struct {
|
||||
count int64
|
||||
firstUnreachableTime time.Time
|
||||
}
|
||||
|
||||
func addPingServerUnreachableCounter(addr string, ts time.Time) (int64, time.Time) {
|
||||
key := addr
|
||||
get, ok := pingServerUnreachableCounter.Load(key)
|
||||
if !ok {
|
||||
pingServerUnreachableCounter.Store(key, pingServerUnreachableCounterDef{
|
||||
count: 1,
|
||||
firstUnreachableTime: ts,
|
||||
})
|
||||
return 1, ts
|
||||
}
|
||||
// 存在则更新,时间戳不变
|
||||
pingServerUnreachableCounter.Store(key, pingServerUnreachableCounterDef{
|
||||
count: get.count + 1,
|
||||
firstUnreachableTime: get.firstUnreachableTime,
|
||||
})
|
||||
return get.count + 1, get.firstUnreachableTime
|
||||
}
|
||||
|
||||
func resetPingServerUnreachableCounter(addr string) {
|
||||
key := addr
|
||||
pingServerUnreachableCounter.Delete(key)
|
||||
}
|
||||
|
||||
// getMinecraftServerStatus 获取Minecraft服务器状态
|
||||
func getMinecraftServerStatus(addr string) (*serverPingAndListResp, error) {
|
||||
var s serverPingAndListResp
|
||||
resp, delay, err := bot.PingAndListTimeout(addr, time.Second*5)
|
||||
if err != nil {
|
||||
// logrus.Errorln(logPrefix+"PingAndList error: ", err)
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(resp, &s)
|
||||
if err != nil {
|
||||
// logrus.Errorln(logPrefix+"Parse json response fail: ", err)
|
||||
return nil, err
|
||||
}
|
||||
s.Delay = delay
|
||||
return &s, nil
|
||||
}
|
||||
27
plugin/minecraftobserver/ping_test.go
Normal file
27
plugin/minecraftobserver/ping_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_PingListInfo(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
resp, err := getMinecraftServerStatus("cn.nekoland.top")
|
||||
if err != nil {
|
||||
t.Fatalf("getMinecraftServerStatus() error = %v", err)
|
||||
}
|
||||
msg, iconBase64 := resp.genServerSubscribeSchema("cn.nekoland.top", 123456).generateServerStatusMsg()
|
||||
fmt.Printf("msg: %v\n", msg)
|
||||
fmt.Printf("iconBase64: %v\n", iconBase64)
|
||||
})
|
||||
t.Run("不可达", func(t *testing.T) {
|
||||
ss, err := getMinecraftServerStatus("dx.123213213123123.net")
|
||||
if err == nil {
|
||||
t.Fatalf("getMinecraftServerStatus() error = %v", err)
|
||||
}
|
||||
if ss != nil {
|
||||
t.Fatalf("getMinecraftServerStatus() got = %v, want nil", ss)
|
||||
}
|
||||
})
|
||||
}
|
||||
220
plugin/minecraftobserver/store.go
Normal file
220
plugin/minecraftobserver/store.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
fcext "github.com/FloatTech/floatbox/ctxext"
|
||||
"github.com/jinzhu/gorm"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
const (
|
||||
dbPath = "minecraft_observer"
|
||||
|
||||
targetTypeGroup = 1
|
||||
targetTypeUser = 2
|
||||
)
|
||||
|
||||
var (
|
||||
// 数据库连接失败
|
||||
errDBConn = errors.New("数据库连接失败")
|
||||
// 参数错误
|
||||
errParam = errors.New("参数错误")
|
||||
)
|
||||
|
||||
type db struct {
|
||||
sdb *gorm.DB
|
||||
statusLock sync.RWMutex
|
||||
subscribeLock sync.RWMutex
|
||||
}
|
||||
|
||||
// initializeDB 初始化数据库
|
||||
func initializeDB(dbpath string) error {
|
||||
if _, err := os.Stat(dbpath); err != nil || os.IsNotExist(err) {
|
||||
// 生成文件
|
||||
f, err := os.Create(dbpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
gdb, err := gorm.Open("sqlite3", dbpath)
|
||||
if err != nil {
|
||||
// logrus.Errorln(logPrefix+"initializeDB ERROR: ", err)
|
||||
return err
|
||||
}
|
||||
gdb.AutoMigrate(&serverStatus{}, &serverSubscribe{})
|
||||
dbInstance = &db{
|
||||
sdb: gdb,
|
||||
statusLock: sync.RWMutex{},
|
||||
subscribeLock: sync.RWMutex{},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
// dbInstance 数据库实例
|
||||
dbInstance *db
|
||||
// 开启并检查数据库链接
|
||||
getDB = fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool {
|
||||
var err = initializeDB(engine.DataFolder() + dbPath)
|
||||
if err != nil {
|
||||
// logrus.Errorln(logPrefix+"initializeDB ERROR: ", err)
|
||||
ctx.SendChain(message.Text("[mc-ob] ERROR: ", err))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
// 通过群组id和服务器地址获取状态
|
||||
func (d *db) getServerStatus(addr string) (*serverStatus, error) {
|
||||
if d == nil {
|
||||
return nil, errDBConn
|
||||
}
|
||||
if addr == "" {
|
||||
return nil, errParam
|
||||
}
|
||||
var ss serverStatus
|
||||
if err := d.sdb.Model(&ss).Where("server_addr = ?", addr).First(&ss).Error; err != nil {
|
||||
// logrus.Errorln(logPrefix+"getServerStatus ERROR: ", err)
|
||||
return nil, err
|
||||
}
|
||||
return &ss, nil
|
||||
}
|
||||
|
||||
// 更新服务器状态
|
||||
func (d *db) updateServerStatus(ss *serverStatus) (err error) {
|
||||
if d == nil {
|
||||
return errDBConn
|
||||
}
|
||||
d.statusLock.Lock()
|
||||
defer d.statusLock.Unlock()
|
||||
if ss == nil || ss.ServerAddr == "" {
|
||||
return errParam
|
||||
}
|
||||
ss.LastUpdate = time.Now().Unix()
|
||||
ss2 := ss.deepCopy()
|
||||
if err = d.sdb.Where(&serverStatus{ServerAddr: ss.ServerAddr}).Assign(ss2).FirstOrCreate(ss).Debug().Error; err != nil {
|
||||
// logrus.Errorln(logPrefix, fmt.Sprintf("updateServerStatus %v ERROR: %v", ss, err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *db) delServerStatus(addr string) (err error) {
|
||||
if d == nil {
|
||||
return errDBConn
|
||||
}
|
||||
if addr == "" {
|
||||
return errParam
|
||||
}
|
||||
d.statusLock.Lock()
|
||||
defer d.statusLock.Unlock()
|
||||
if err = d.sdb.Where("server_addr = ?", addr).Delete(&serverStatus{}).Error; err != nil {
|
||||
// logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 新增订阅
|
||||
func (d *db) newSubscribe(addr string, targetID, targetType int64) (err error) {
|
||||
if d == nil {
|
||||
return errDBConn
|
||||
}
|
||||
if targetID == 0 || (targetType != 1 && targetType != 2) {
|
||||
// logrus.Errorln(logPrefix+"newSubscribe ERROR: 参数错误 ", targetID, " ", targetType)
|
||||
return errParam
|
||||
}
|
||||
d.subscribeLock.Lock()
|
||||
defer d.subscribeLock.Unlock()
|
||||
// 如果已经存在,需要报错
|
||||
existedRec := &serverSubscribe{}
|
||||
err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).First(existedRec).Error
|
||||
if err != nil && !gorm.IsRecordNotFoundError(err) {
|
||||
// logrus.Errorln(logPrefix+"newSubscribe ERROR: ", err)
|
||||
return
|
||||
}
|
||||
if existedRec.ID != 0 {
|
||||
return errors.New("已经存在的订阅")
|
||||
}
|
||||
ss := &serverSubscribe{
|
||||
ServerAddr: addr,
|
||||
TargetID: targetID,
|
||||
TargetType: targetType,
|
||||
LastUpdate: time.Now().Unix(),
|
||||
}
|
||||
if err = d.sdb.Model(&ss).Create(ss).Error; err != nil {
|
||||
// logrus.Errorln(logPrefix+"newSubscribe ERROR: ", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 删除订阅
|
||||
func (d *db) deleteSubscribe(addr string, targetID int64, targetType int64) (err error) {
|
||||
if d == nil {
|
||||
return errDBConn
|
||||
}
|
||||
if addr == "" || targetID == 0 || targetType == 0 {
|
||||
return errParam
|
||||
}
|
||||
d.subscribeLock.Lock()
|
||||
defer d.subscribeLock.Unlock()
|
||||
// 检查是否存在
|
||||
if err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).First(&serverSubscribe{}).Error; err != nil {
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
return errors.New("未找到订阅")
|
||||
}
|
||||
// logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = d.sdb.Where("server_addr = ? and target_id = ? and target_type = ?", addr, targetID, targetType).Delete(&serverSubscribe{}).Error; err != nil {
|
||||
// logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 扫描是否还有订阅,如果没有则删除服务器状态
|
||||
var cnt int
|
||||
err = d.sdb.Model(&serverSubscribe{}).Where("server_addr = ?", addr).Count(&cnt).Error
|
||||
if err != nil {
|
||||
// logrus.Errorln(logPrefix+"deleteSubscribe ERROR: ", err)
|
||||
return
|
||||
}
|
||||
if cnt == 0 {
|
||||
_ = d.delServerStatus(addr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有订阅
|
||||
func (d *db) getAllSubscribes() (subs []serverSubscribe, err error) {
|
||||
if d == nil {
|
||||
return nil, errDBConn
|
||||
}
|
||||
subs = []serverSubscribe{}
|
||||
if err = d.sdb.Find(&subs).Error; err != nil {
|
||||
// logrus.Errorln(logPrefix+"getAllSubscribes ERROR: ", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取渠道对应的订阅列表
|
||||
func (d *db) getSubscribesByTarget(targetID, targetType int64) (subs []serverSubscribe, err error) {
|
||||
if d == nil {
|
||||
return nil, errDBConn
|
||||
}
|
||||
subs = []serverSubscribe{}
|
||||
if err = d.sdb.Model(&serverSubscribe{}).Where("target_id = ? and target_type = ?", targetID, targetType).Find(&subs).Error; err != nil {
|
||||
// logrus.Errorln(logPrefix+"getSubscribesByTarget ERROR: ", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
317
plugin/minecraftobserver/store_test.go
Normal file
317
plugin/minecraftobserver/store_test.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package minecraftobserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func cleanTestData(t *testing.T) {
|
||||
err := dbInstance.sdb.Delete(&serverStatus{}).Where("id > 0").Error
|
||||
if err != nil {
|
||||
t.Fatalf("cleanTestData() error = %v", err)
|
||||
}
|
||||
err = dbInstance.sdb.Delete(&serverSubscribe{}).Where("id > 0").Error
|
||||
if err != nil {
|
||||
t.Fatalf("cleanTestData() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DAO(t *testing.T) {
|
||||
initErr := initializeDB("data/minecraftobserver/" + dbPath)
|
||||
if initErr != nil {
|
||||
t.Fatalf("initializeDB() error = %v", initErr)
|
||||
}
|
||||
if dbInstance == nil {
|
||||
t.Fatalf("initializeDB() got = %v, want not nil", dbInstance)
|
||||
}
|
||||
t.Run("insert", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS1 := &serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
}
|
||||
newSS2 := &serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.8",
|
||||
FaviconMD5: "1234567",
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS1)
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.updateServerStatus(newSS2)
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
|
||||
// check insert
|
||||
queryResult, err := dbInstance.getServerStatus("dx.zhaomc.net")
|
||||
if err != nil {
|
||||
t.Fatalf("getServerStatus() error = %v", err)
|
||||
}
|
||||
if queryResult == nil {
|
||||
t.Fatalf("getServerStatus() got = %v, want not nil", queryResult)
|
||||
}
|
||||
if queryResult.Version != "1.16.8" {
|
||||
t.Fatalf("getServerStatus() got = %v, want 1.16.8", queryResult.Version)
|
||||
}
|
||||
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeUser)
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
// check insert
|
||||
res, err := dbInstance.getAllSubscribes()
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
if len(res) != 2 {
|
||||
t.Fatalf("getAllServer() got = %v, want 2", len(res))
|
||||
}
|
||||
// 检查是否符合预期
|
||||
if res[0].ServerAddr != "dx.zhaomc.net" {
|
||||
t.Fatalf("getAllServer() got = %v, want dx.zhaomc.net", res[0].ServerAddr)
|
||||
}
|
||||
if res[0].TargetType != targetTypeGroup {
|
||||
t.Fatalf("getAllServer() got = %v, want %v", res[0].TargetType, targetTypeGroup)
|
||||
}
|
||||
if res[1].ServerAddr != "dx.zhaomc.net" {
|
||||
t.Fatalf("getAllServer() got = %v, want dx.zhaomc.net", res[1].ServerAddr)
|
||||
}
|
||||
if res[1].TargetType != targetTypeUser {
|
||||
t.Fatalf("getAllServer() got = %v, want %v", res[1].TargetType, targetTypeUser)
|
||||
}
|
||||
|
||||
// 顺带验证一下 byTarget
|
||||
res2, err := dbInstance.getSubscribesByTarget(123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("getSubscribesByTarget() error = %v", err)
|
||||
}
|
||||
if len(res2) != 1 {
|
||||
t.Fatalf("getSubscribesByTarget() got = %v, want 1", len(res2))
|
||||
}
|
||||
|
||||
})
|
||||
// 重复添加订阅
|
||||
t.Run("insert dup", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS := &serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS)
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err == nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
fmt.Printf("insert dup error: %+v", err)
|
||||
})
|
||||
|
||||
t.Run("update", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS := &serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS)
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.updateServerStatus(&serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "更新测试",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
// check update
|
||||
queryResult2, err := dbInstance.getServerStatus("dx.zhaomc.net")
|
||||
if err != nil {
|
||||
t.Errorf("getAllServer() error = %v", err)
|
||||
}
|
||||
if queryResult2.Description != "更新测试" {
|
||||
t.Errorf("getAllServer() got = %v, want 更新测试", queryResult2.Description)
|
||||
}
|
||||
})
|
||||
t.Run("delete status", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS := &serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS)
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
// check insert
|
||||
queryResult, err := dbInstance.getServerStatus("dx.zhaomc.net")
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
if queryResult == nil {
|
||||
t.Fatalf("getAllServer() got = %v, want not nil", queryResult)
|
||||
}
|
||||
err = dbInstance.delServerStatus("dx.zhaomc.net")
|
||||
if err != nil {
|
||||
t.Fatalf("deleteServerStatus() error = %v", err)
|
||||
}
|
||||
// check delete
|
||||
_, err = dbInstance.getServerStatus("dx.zhaomc.net")
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// 删除订阅
|
||||
t.Run("delete subscribe", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
newSS := &serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
}
|
||||
err := dbInstance.updateServerStatus(newSS)
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("deleteSubscribe() error = %v", err)
|
||||
}
|
||||
// check delete
|
||||
_, err = dbInstance.getServerStatus("dx.zhaomc.net")
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// 重复删除订阅
|
||||
t.Run("delete subscribe dup", func(t *testing.T) {
|
||||
cleanTestData(t)
|
||||
err := dbInstance.updateServerStatus(&serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("upsertServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("newSubscribe() error = %v", err)
|
||||
}
|
||||
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net123", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("newSubscribe() error = %v", err)
|
||||
}
|
||||
err = dbInstance.updateServerStatus(&serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net123",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("updateServerStatus() error = %v", err)
|
||||
}
|
||||
err = dbInstance.newSubscribe("dx.zhaomc.net4567", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("newSubscribe() error = %v", err)
|
||||
}
|
||||
err = dbInstance.updateServerStatus(&serverStatus{
|
||||
ServerAddr: "dx.zhaomc.net4567",
|
||||
Description: "测试服务器",
|
||||
Players: "1/20",
|
||||
Version: "1.16.5",
|
||||
FaviconMD5: "1234567",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("updateServerStatus() error = %v", err)
|
||||
}
|
||||
|
||||
// 检查是不是3个
|
||||
allSub, err := dbInstance.getAllSubscribes()
|
||||
if err != nil {
|
||||
t.Fatalf("getAllSubscribes() error = %v", err)
|
||||
}
|
||||
if len(allSub) != 3 {
|
||||
t.Fatalf("getAllSubscribes() got = %v, want 3", len(allSub))
|
||||
}
|
||||
err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("deleteSubscribe() error = %v", err)
|
||||
}
|
||||
err = dbInstance.deleteSubscribe("dx.zhaomc.net", 123456, targetTypeGroup)
|
||||
if err == nil {
|
||||
t.Fatalf("deleteSubscribe() error = %v", err)
|
||||
}
|
||||
fmt.Println("delete dup error: ", err)
|
||||
|
||||
// 检查其他的没有被删
|
||||
allSub, err = dbInstance.getAllSubscribes()
|
||||
if err != nil {
|
||||
t.Fatalf("getAllSubscribes() error = %v", err)
|
||||
}
|
||||
// 检查是否符合预期
|
||||
if len(allSub) != 2 {
|
||||
t.Fatalf("getAllSubscribes() got = %v, want 2", len(allSub))
|
||||
}
|
||||
// 状态
|
||||
_, err = dbInstance.getServerStatus("dx.zhaomc.net")
|
||||
if !gorm.IsRecordNotFoundError(err) {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
status1, err := dbInstance.getServerStatus("dx.zhaomc.net123")
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
status2, err := dbInstance.getServerStatus("dx.zhaomc.net4567")
|
||||
if err != nil {
|
||||
t.Fatalf("getAllServer() error = %v", err)
|
||||
}
|
||||
if status1 == nil || status2 == nil {
|
||||
t.Fatalf("getAllServer() want not nil")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
435
plugin/movies/main.go
Normal file
435
plugin/movies/main.go
Normal file
@@ -0,0 +1,435 @@
|
||||
// Package movies 电影查询
|
||||
package movies
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"image"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
"github.com/FloatTech/gg"
|
||||
"github.com/FloatTech/imgfactory"
|
||||
"github.com/FloatTech/rendercard"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/img/text"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
const (
|
||||
apiURL = "https://m.maoyan.com/ajax/"
|
||||
ua = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
todayPic = make([][]byte, 2)
|
||||
lasttime time.Time
|
||||
en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "电影查询",
|
||||
Help: "- 今日电影\n" +
|
||||
"- 预售电影",
|
||||
PrivateDataFolder: "movies",
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
en.OnFullMatch("今日电影").SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
if todayPic != nil && time.Since(lasttime) < 12*time.Hour {
|
||||
ctx.SendChain(message.ImageBytes(todayPic[0]))
|
||||
return
|
||||
}
|
||||
lasttime = time.Now()
|
||||
movieComingList, err := getMovieList("今日电影")
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("[ERROR]:", err))
|
||||
return
|
||||
}
|
||||
if len(movieComingList) == 0 {
|
||||
ctx.SendChain(message.Text("没有今日电影"))
|
||||
return
|
||||
}
|
||||
pic, err := drawOnListPic(movieComingList)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("[ERROR]:", err))
|
||||
return
|
||||
}
|
||||
todayPic[0] = pic
|
||||
ctx.SendChain(message.ImageBytes(pic))
|
||||
})
|
||||
en.OnFullMatch("预售电影").SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
if todayPic[1] != nil && time.Since(lasttime) < 12*time.Hour {
|
||||
ctx.SendChain(message.ImageBytes(todayPic[1]))
|
||||
return
|
||||
}
|
||||
lasttime = time.Now()
|
||||
movieComingList, err := getMovieList("预售电影")
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("[ERROR]:", err))
|
||||
return
|
||||
}
|
||||
if len(movieComingList) == 0 {
|
||||
ctx.SendChain(message.Text("没有预售电影"))
|
||||
return
|
||||
}
|
||||
pic, err := drawComListPic(movieComingList)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("[ERROR]:", err))
|
||||
return
|
||||
}
|
||||
todayPic[1] = pic
|
||||
ctx.SendChain(message.ImageBytes(pic))
|
||||
})
|
||||
}
|
||||
|
||||
type movieInfo struct {
|
||||
ID int64 `json:"id"` // 电影ID
|
||||
Img string `json:"img"` // 海报
|
||||
|
||||
Nm string `json:"nm"` // 名称
|
||||
|
||||
Dir string `json:"dir"` // 导演
|
||||
Star string `json:"star"` // 演员
|
||||
|
||||
OriLang string `json:"oriLang"` // 原语言
|
||||
Cat string `json:"cat"` // 类型
|
||||
|
||||
Version string `json:"version"` // 电影格式
|
||||
Rt string `json:"rt"` // 上映时间
|
||||
|
||||
ShowInfo string `json:"showInfo"` // 今日上映信息
|
||||
ComingTitle string `json:"comingTitle"` // 预售信息
|
||||
|
||||
Sc float64 `json:"sc"` // 评分
|
||||
Wish int64 `json:"wish"` // 观看人数
|
||||
Watched int64 `json:"watched"` // 观看数
|
||||
}
|
||||
type movieOnList struct {
|
||||
MovieList []movieInfo `json:"movieList"`
|
||||
}
|
||||
type comingList struct {
|
||||
MovieList []movieInfo `json:"coming"`
|
||||
}
|
||||
type movieShow struct {
|
||||
MovieInfo movieInfo `json:"detailMovie"`
|
||||
}
|
||||
|
||||
type cardInfo struct {
|
||||
Avatar image.Image
|
||||
TopLeftText string
|
||||
BottomLeftText []string
|
||||
RightText string
|
||||
Rank string
|
||||
}
|
||||
|
||||
func getMovieList(mode string) (movieList []movieInfo, err error) {
|
||||
var data []byte
|
||||
if mode == "今日电影" {
|
||||
data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"movieOnInfoList", "", "GET", ua, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var parsed movieOnList
|
||||
err = json.Unmarshal(data, &parsed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
movieList = parsed.MovieList
|
||||
} else {
|
||||
data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"comingList?token=", "", "GET", ua, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var parsed comingList
|
||||
err = json.Unmarshal(data, &parsed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
movieList = parsed.MovieList
|
||||
}
|
||||
if len(movieList) == 0 {
|
||||
return
|
||||
}
|
||||
for i, info := range movieList {
|
||||
movieID := strconv.FormatInt(info.ID, 10)
|
||||
data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL+"detailmovie?movieId="+movieID, "", "GET", ua, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var movieInfo movieShow
|
||||
err = json.Unmarshal(data, &movieInfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if mode != "今日电影" {
|
||||
movieInfo.MovieInfo.ComingTitle = movieList[i].ComingTitle
|
||||
}
|
||||
movieList[i] = movieInfo.MovieInfo
|
||||
}
|
||||
// 整理数据,进行排序
|
||||
sort.Slice(movieList, func(i, j int) bool {
|
||||
if movieList[i].Sc != movieList[j].Sc {
|
||||
return movieList[i].Sc > movieList[j].Sc
|
||||
}
|
||||
if mode == "今日电影" {
|
||||
return movieList[i].Watched > movieList[j].Watched
|
||||
}
|
||||
return movieList[i].Wish > movieList[j].Wish
|
||||
})
|
||||
return movieList, nil
|
||||
}
|
||||
func drawOnListPic(lits []movieInfo) (data []byte, err error) {
|
||||
rankinfo := make([]*cardInfo, len(lits))
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(lits))
|
||||
for i := 0; i < len(lits); i++ {
|
||||
go func(i int) {
|
||||
info := lits[i]
|
||||
defer wg.Done()
|
||||
img, err := avatar(&info)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
movieType := "2D"
|
||||
if info.Version != "" {
|
||||
movieType = info.Version
|
||||
}
|
||||
watched := ""
|
||||
switch {
|
||||
case info.Watched > 100000000:
|
||||
watched = strconv.FormatFloat(float64(info.Watched)/100000000, 'f', 2, 64) + "亿"
|
||||
case info.Watched > 10000:
|
||||
watched = strconv.FormatFloat(float64(info.Watched)/10000, 'f', 2, 64) + "万"
|
||||
default:
|
||||
watched = strconv.FormatInt(info.Watched, 10)
|
||||
}
|
||||
rankinfo[i] = &cardInfo{
|
||||
TopLeftText: info.Nm + " (" + strconv.FormatInt(info.ID, 10) + ")",
|
||||
BottomLeftText: []string{
|
||||
"导演:" + info.Dir,
|
||||
"演员:" + info.Star,
|
||||
"标签:" + info.Cat,
|
||||
"语言: " + info.OriLang + " 类型: " + movieType,
|
||||
"上映时间: " + info.Rt,
|
||||
},
|
||||
RightText: watched + "人已看",
|
||||
Avatar: img,
|
||||
Rank: strconv.FormatFloat(info.Sc, 'f', 1, 64),
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
img, err := drawRankingCard(fontbyte, "今日电影", rankinfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err = imgfactory.ToBytes(img)
|
||||
return
|
||||
}
|
||||
|
||||
func drawComListPic(lits []movieInfo) (data []byte, err error) {
|
||||
rankinfo := make([]*cardInfo, len(lits))
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(lits))
|
||||
for i := 0; i < len(lits); i++ {
|
||||
go func(i int) {
|
||||
info := lits[i]
|
||||
defer wg.Done()
|
||||
img, err := avatar(&info)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
movieType := "2D"
|
||||
if info.Version != "" {
|
||||
movieType = info.Version
|
||||
}
|
||||
wish := ""
|
||||
switch {
|
||||
case info.Wish > 100000000:
|
||||
wish = strconv.FormatFloat(float64(info.Wish)/100000000, 'f', 2, 64) + "亿"
|
||||
case info.Wish > 10000:
|
||||
wish = strconv.FormatFloat(float64(info.Wish)/10000, 'f', 2, 64) + "万"
|
||||
default:
|
||||
wish = strconv.FormatInt(info.Wish, 10)
|
||||
}
|
||||
rankinfo[i] = &cardInfo{
|
||||
TopLeftText: info.Nm + " (" + strconv.FormatInt(info.ID, 10) + ")",
|
||||
BottomLeftText: []string{
|
||||
"导演:" + info.Dir,
|
||||
"演员:" + info.Star,
|
||||
"标签:" + info.Cat,
|
||||
"语言: " + info.OriLang + " 类型: " + movieType,
|
||||
"上映时间: " + info.Rt + " 播放时间: " + info.ComingTitle,
|
||||
},
|
||||
RightText: wish + "人期待",
|
||||
Avatar: img,
|
||||
Rank: strconv.Itoa(i + 1),
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
fontbyte, err := file.GetLazyData(text.GlowSansFontFile, control.Md5File, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
img, err := drawRankingCard(fontbyte, "预售电影", rankinfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err = imgfactory.ToBytes(img)
|
||||
return
|
||||
}
|
||||
|
||||
func drawRankingCard(fontdata []byte, title string, rankinfo []*cardInfo) (img image.Image, err error) {
|
||||
line := len(rankinfo)
|
||||
const lineh = 130
|
||||
const w = 800
|
||||
h := 64 + (lineh+14)*line + 20 - 14
|
||||
canvas := gg.NewContext(w, h)
|
||||
canvas.SetRGBA255(255, 255, 255, 255)
|
||||
canvas.Clear()
|
||||
|
||||
cardh, cardw := lineh, 770
|
||||
cardspac := 14
|
||||
hspac, wspac := 64.0, 16.0
|
||||
r := 16.0
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(line)
|
||||
cardimgs := make([]image.Image, line)
|
||||
for i := 0; i < line; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
card := gg.NewContext(w, cardh)
|
||||
|
||||
card.NewSubPath()
|
||||
|
||||
card.MoveTo(wspac+float64(cardh)/2, 0)
|
||||
|
||||
card.LineTo(wspac+float64(cardw)-r, 0)
|
||||
card.DrawArc(wspac+float64(cardw)-r, r, r, gg.Radians(-90), gg.Radians(0))
|
||||
card.LineTo(wspac+float64(cardw), float64(cardh)-r)
|
||||
card.DrawArc(wspac+float64(cardw)-r, float64(cardh)-r, r, gg.Radians(0), gg.Radians(90))
|
||||
card.LineTo(wspac+float64(cardh)/2, float64(cardh))
|
||||
card.DrawArc(wspac+r, float64(cardh)-r, r, gg.Radians(90), gg.Radians(180))
|
||||
card.LineTo(wspac, r)
|
||||
card.DrawArc(wspac+r, r, r, gg.Radians(180), gg.Radians(270))
|
||||
|
||||
card.ClosePath()
|
||||
|
||||
card.ClipPreserve()
|
||||
|
||||
avatar := rankinfo[i].Avatar
|
||||
|
||||
PicH := cardh - 20
|
||||
picW := int(float64(avatar.Bounds().Dx()) * float64(PicH) / float64(avatar.Bounds().Dy()))
|
||||
card.DrawImageAnchored(imgfactory.Size(avatar, picW, PicH).Image(), int(wspac)+10+picW/2, cardh/2, 0.5, 0.5)
|
||||
|
||||
card.ResetClip()
|
||||
card.SetRGBA255(0, 0, 0, 127)
|
||||
card.Stroke()
|
||||
|
||||
card.SetRGBA255(240, 210, 140, 200)
|
||||
card.DrawRoundedRectangle(wspac+float64(cardw-8-250), (float64(cardh)-50)/2, 250, 50, 25)
|
||||
card.Fill()
|
||||
card.SetRGB255(rendercard.RandJPColor())
|
||||
card.DrawRoundedRectangle(wspac+float64(cardw-8-60), (float64(cardh)-50)/2, 60, 50, 25)
|
||||
card.Fill()
|
||||
cardimgs[i] = card.Image()
|
||||
}(i)
|
||||
}
|
||||
|
||||
canvas.SetRGBA255(0, 0, 0, 255)
|
||||
err = canvas.ParseFontFace(fontdata, 32)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
canvas.DrawStringAnchored(title, w/2, 64/2, 0.5, 0.5)
|
||||
|
||||
err = canvas.ParseFontFace(fontdata, 22)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wg.Wait()
|
||||
for i := 0; i < line; i++ {
|
||||
canvas.DrawImageAnchored(cardimgs[i], w/2, int(hspac)+((cardh+cardspac)*i), 0.5, 0)
|
||||
canvas.DrawStringAnchored(rankinfo[i].TopLeftText, wspac+10+80+10, hspac+float64((cardspac+cardh)*i+cardh*3/16), 0, 0.5)
|
||||
}
|
||||
|
||||
// canvas.SetRGBA255(63, 63, 63, 255)
|
||||
err = canvas.ParseFontFace(fontdata, 14)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < line; i++ {
|
||||
for j, text := range rankinfo[i].BottomLeftText {
|
||||
canvas.DrawStringAnchored(text, wspac+10+80+10, hspac+float64((cardspac+cardh)*i+cardh*6/16)+float64(j*16), 0, 0.5)
|
||||
}
|
||||
}
|
||||
canvas.SetRGBA255(0, 0, 0, 255)
|
||||
err = canvas.ParseFontFace(fontdata, 20)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < line; i++ {
|
||||
canvas.DrawStringAnchored(rankinfo[i].RightText, w-wspac-8-60-8, hspac+float64((cardspac+cardh)*i+cardh/2), 1, 0.5)
|
||||
}
|
||||
|
||||
canvas.SetRGBA255(255, 255, 255, 255)
|
||||
err = canvas.ParseFontFace(fontdata, 28)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := 0; i < line; i++ {
|
||||
canvas.DrawStringAnchored(rankinfo[i].Rank, w-wspac-8-30, hspac+float64((cardspac+cardh)*i+cardh/2), 0.5, 0.5)
|
||||
}
|
||||
|
||||
img = canvas.Image()
|
||||
return
|
||||
}
|
||||
|
||||
// avatar 获取电影海报,图片大且多,存本地增加响应速度
|
||||
func avatar(movieInfo *movieInfo) (pic image.Image, err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
aimgfile := filepath.Join(en.DataFolder(), movieInfo.Nm+"("+strconv.FormatInt(movieInfo.ID, 10)+").jpg")
|
||||
if file.IsNotExist(aimgfile) {
|
||||
err = file.DownloadTo(movieInfo.Img, aimgfile)
|
||||
if err != nil {
|
||||
return urlToImg(movieInfo.Img)
|
||||
}
|
||||
}
|
||||
f, err := os.Open(filepath.Join(file.BOTPATH, aimgfile))
|
||||
if err != nil {
|
||||
return urlToImg(movieInfo.Img)
|
||||
}
|
||||
defer f.Close()
|
||||
pic, _, err = image.Decode(f)
|
||||
return
|
||||
}
|
||||
|
||||
func urlToImg(url string) (img image.Image, err error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
img, _, err = image.Decode(resp.Body)
|
||||
return
|
||||
}
|
||||
@@ -21,6 +21,10 @@ import (
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
var (
|
||||
longZhuURL = "https://www.hhlqilongzhu.cn/api/joox/juhe_music.php?msg=%v"
|
||||
)
|
||||
|
||||
func init() {
|
||||
control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
@@ -29,7 +33,8 @@ func init() {
|
||||
"- 网易点歌[xxx]\n" +
|
||||
"- 酷我点歌[xxx]\n" +
|
||||
"- 酷狗点歌[xxx]\n" +
|
||||
"- 咪咕点歌[xxx]",
|
||||
"- 咪咕点歌[xxx]\n" +
|
||||
"- qq点歌[xxx]\n",
|
||||
}).OnRegex(`^(.{0,2})点歌\s?(.{1,25})$`).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
// switch 平台
|
||||
@@ -42,12 +47,37 @@ func init() {
|
||||
ctx.SendChain(kugou(ctx.State["regex_matched"].([]string)[2]))
|
||||
case "网易":
|
||||
ctx.SendChain(cloud163(ctx.State["regex_matched"].([]string)[2]))
|
||||
default: // 默认 QQ音乐
|
||||
case "qq":
|
||||
ctx.SendChain(qqmusic(ctx.State["regex_matched"].([]string)[2]))
|
||||
default: // 默认聚合点歌
|
||||
ctx.SendChain(longzhu(ctx.State["regex_matched"].([]string)[2]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// longzhu 聚合平台
|
||||
func longzhu(keyword string) message.Segment {
|
||||
data, _ := web.GetData(fmt.Sprintf(longZhuURL, url.QueryEscape(keyword)))
|
||||
// 假设 data 是包含整个 JSON 数组的字节切片
|
||||
results := gjson.ParseBytes(data).Array()
|
||||
for _, result := range results {
|
||||
if strings.Contains(strings.ToLower(result.Get("title").String()), strings.ToLower(keyword)) {
|
||||
if musicURL := result.Get("full_track").String(); musicURL != "" {
|
||||
return message.Record(musicURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = gjson.GetBytes(data, "#.full_track").Array()
|
||||
if len(results) > 0 {
|
||||
if musicURL := results[0].String(); musicURL != "" {
|
||||
return message.Record(musicURL)
|
||||
}
|
||||
}
|
||||
|
||||
return message.Text("点歌失败, 找不到 ", keyword, " 的相关结果")
|
||||
}
|
||||
|
||||
// migu 返回咪咕音乐卡片
|
||||
func migu(keyword string) message.Segment {
|
||||
headers := http.Header{
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
package niuniu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/FloatTech/AnimeAPI/niu"
|
||||
@@ -18,12 +20,6 @@ import (
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
type lastLength struct {
|
||||
TimeLimit time.Time
|
||||
Count int
|
||||
Length float64
|
||||
}
|
||||
|
||||
var (
|
||||
en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
@@ -47,8 +43,8 @@ var (
|
||||
})
|
||||
dajiaoLimiter = rate.NewManager[string](time.Second*90, 1)
|
||||
jjLimiter = rate.NewManager[string](time.Second*150, 1)
|
||||
jjCount = syncx.Map[string, *lastLength]{}
|
||||
register = syncx.Map[string, *lastLength]{}
|
||||
jjCount = syncx.Map[string, *niu.PKRecord]{}
|
||||
register = syncx.Map[string, *niu.PKRecord]{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -65,7 +61,7 @@ func init() {
|
||||
messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text("牛牛拍卖行有以下牛牛")))
|
||||
for _, info := range auction {
|
||||
msg := fmt.Sprintf("商品序号: %d\n牛牛原所属: %d\n牛牛价格: %d%s\n牛牛大小: %.2fcm",
|
||||
info.ID+1, info.UserID, info.Money, wallet.GetWalletName(), info.Length)
|
||||
info.ID, info.UserID, info.Money, wallet.GetWalletName(), info.Length)
|
||||
messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text(msg)))
|
||||
}
|
||||
if id := ctx.Send(messages).ID(); id == 0 {
|
||||
@@ -81,7 +77,7 @@ func init() {
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消"))
|
||||
ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消"))
|
||||
return
|
||||
case r := <-recv:
|
||||
answer = r.Event.Message.String()
|
||||
@@ -90,7 +86,6 @@ func init() {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
n--
|
||||
msg, err := niu.Auction(gid, uid, n)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR:", err))
|
||||
@@ -104,11 +99,21 @@ func init() {
|
||||
en.OnFullMatch("出售牛牛", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
uid := ctx.Event.UserID
|
||||
key := fmt.Sprintf("%d_%d", gid, uid)
|
||||
sell, err := niu.Sell(gid, uid)
|
||||
if err != nil {
|
||||
if errors.Is(err, niu.ErrCanceled) || errors.Is(err, niu.ErrNoNiuNiu) {
|
||||
ctx.SendChain(message.Text(err))
|
||||
jjCount.Delete(key)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR:", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 数据库操作成功之后,及时删除残留的缓存
|
||||
if _, ok := jjCount.Load(key); ok {
|
||||
jjCount.Delete(key)
|
||||
}
|
||||
ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(sell))
|
||||
})
|
||||
en.OnFullMatch("牛牛背包", zero.OnlyGroup).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
@@ -135,29 +140,33 @@ func init() {
|
||||
cost int
|
||||
scope string
|
||||
description string
|
||||
count int
|
||||
}{
|
||||
1: {"伟哥", 300, "打胶", "可以让你打胶每次都增长", 5},
|
||||
2: {"媚药", 300, "打胶", "可以让你打胶每次都减少", 5},
|
||||
3: {"击剑神器", 500, "jj", "可以让你每次击剑都立于不败之地", 2},
|
||||
4: {"击剑神稽", 500, "jj", "可以让你每次击剑都失败", 2},
|
||||
1: {"伟哥", 100, "打胶", "可以让你打胶每次都增长"},
|
||||
2: {"媚药", 100, "打胶", "可以让你打胶每次都减少"},
|
||||
3: {"击剑神器", 300, "jj", "可以让你每次击剑都立于不败之地"},
|
||||
4: {"击剑神稽", 300, "jj", "可以让你每次击剑都失败"},
|
||||
}
|
||||
|
||||
var messages message.Message
|
||||
messages = append(messages, ctxext.FakeSenderForwardNode(ctx,
|
||||
message.Text("输入对应序号进行购买商品"),
|
||||
message.Text(
|
||||
"使用说明:\n"+
|
||||
"商品id-商品数量\n"+
|
||||
"如想购买10个伟哥\n"+
|
||||
"即:1-10")))
|
||||
messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text("牛牛商店当前售卖的物品如下")))
|
||||
for id := range propMap {
|
||||
for id := 1; id <= len(propMap); id++ {
|
||||
product := propMap[id]
|
||||
productInfo := fmt.Sprintf("商品%d\n商品名: %s\n商品价格: %dATRI币\n商品作用域: %s\n商品描述: %s\n使用次数:%d",
|
||||
id, product.name, product.cost, product.scope, product.description, product.count)
|
||||
productInfo := fmt.Sprintf("商品%d\n商品名: %s\n商品价格: %dATRI币\n商品作用域: %s\n商品描述: %s",
|
||||
id, product.name, product.cost, product.scope, product.description)
|
||||
messages = append(messages, ctxext.FakeSenderForwardNode(ctx, message.Text(productInfo)))
|
||||
}
|
||||
if id := ctx.Send(messages).ID(); id == 0 {
|
||||
ctx.Send(message.Text("发送商店失败"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SendChain(message.Text("输入对应序号进行购买商品"))
|
||||
recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(\d+)$`)).Repeat()
|
||||
recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(\d+)-(\d+)$`)).Repeat()
|
||||
defer cancel()
|
||||
timer := time.NewTimer(120 * time.Second)
|
||||
answer := ""
|
||||
@@ -165,17 +174,17 @@ func init() {
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消"))
|
||||
ctx.SendChain(message.At(uid), message.Text(" 超时,已自动取消"))
|
||||
return
|
||||
case r := <-recv:
|
||||
answer = r.Event.Message.String()
|
||||
n, err := strconv.Atoi(answer)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = niu.Store(gid, uid, n); err != nil {
|
||||
// 解析输入的商品ID和数量
|
||||
parts := strings.Split(answer, "-")
|
||||
productID, _ := strconv.Atoi(parts[0])
|
||||
quantity, _ := strconv.Atoi(parts[1])
|
||||
|
||||
if err := niu.Store(gid, uid, productID, quantity); err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
return
|
||||
}
|
||||
@@ -196,16 +205,16 @@ func init() {
|
||||
}
|
||||
|
||||
if time.Since(last.TimeLimit) > time.Hour {
|
||||
ctx.SendChain(message.Text("时间已经过期了,牛牛已被收回!"))
|
||||
ctx.SendChain(message.Text("时间已经过期了,牛牛已被收回!"))
|
||||
jjCount.Delete(fmt.Sprintf("%d_%d", gid, uid))
|
||||
return
|
||||
}
|
||||
|
||||
if last.Count < 4 {
|
||||
ctx.SendChain(message.Text("你还没有被厥够4次呢,不能赎牛牛"))
|
||||
ctx.SendChain(message.Text("你还没有被厥够4次呢,不能赎牛牛"))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(message.Text("再次确认一下哦,这次赎牛牛,牛牛长度将会变成", last.Length, "cm\n还需要嘛【是|否】"))
|
||||
ctx.SendChain(message.Text("再次确认一下哦,这次赎牛牛,牛牛长度将会变成", last.Length, "cm\n还需要嘛【是|否】"))
|
||||
recv, cancel := zero.NewFutureEvent("message", 999, false, zero.CheckUser(uid), zero.CheckGroup(gid), zero.RegexRule(`^(是|否)$`)).Repeat()
|
||||
defer cancel()
|
||||
timer := time.NewTimer(2 * time.Minute)
|
||||
@@ -222,11 +231,11 @@ func init() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := niu.Redeem(gid, uid, last.Length); err == nil {
|
||||
if err := niu.Redeem(gid, uid, *last); err != nil {
|
||||
ctx.SendChain(message.Text("ERROR:", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 成功赎回,删除残留的缓存。
|
||||
jjCount.Delete(fmt.Sprintf("%d_%d", gid, uid))
|
||||
|
||||
ctx.SendChain(message.At(uid), message.Text(fmt.Sprintf("恭喜你!成功赎回牛牛,当前长度为:%.2fcm", last.Length)))
|
||||
@@ -308,7 +317,7 @@ func init() {
|
||||
}
|
||||
ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg))
|
||||
})
|
||||
en.OnMessage(zero.NewPattern().Text(`^(?:.*使用(.*))??jj`).At().AsRule(),
|
||||
en.OnMessage(zero.NewPattern(nil).Text(`^(?:.*使用(.*))??jj`).At().AsRule(),
|
||||
zero.OnlyGroup).SetBlock(true).Limit(func(ctx *zero.Ctx) *rate.Limiter {
|
||||
lt := jjLimiter.Load(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID))
|
||||
ctx.State["jj_last_touch"] = lt.LastTouch()
|
||||
@@ -332,7 +341,7 @@ func init() {
|
||||
}
|
||||
uid := ctx.Event.UserID
|
||||
gid := ctx.Event.GroupID
|
||||
msg, length, err := niu.JJ(gid, uid, adduser, patternParsed[0].Text()[1])
|
||||
msg, length, niuID, err := niu.JJ(gid, uid, adduser, patternParsed[0].Text()[1])
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
jjLimiter.Delete(fmt.Sprintf("%d_%d", ctx.Event.GroupID, ctx.Event.UserID))
|
||||
@@ -341,22 +350,27 @@ func init() {
|
||||
ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text(msg))
|
||||
j := fmt.Sprintf("%d_%d", gid, adduser)
|
||||
count, ok := jjCount.Load(j)
|
||||
var c lastLength
|
||||
// 按照最后一次被jj时的时间计算,超过60分钟则重置
|
||||
var c niu.PKRecord
|
||||
// 按照最后一次被 jj 时的时间计算,超过60分钟则重置
|
||||
if !ok {
|
||||
c = lastLength{
|
||||
// 第一次被 jj
|
||||
c = niu.PKRecord{
|
||||
NiuID: niuID,
|
||||
TimeLimit: time.Now(),
|
||||
Count: 1,
|
||||
Length: length,
|
||||
}
|
||||
} else {
|
||||
c = lastLength{
|
||||
c = niu.PKRecord{
|
||||
NiuID: niuID,
|
||||
TimeLimit: time.Now(),
|
||||
Count: count.Count + 1,
|
||||
Length: count.Length,
|
||||
}
|
||||
// 超时了,重置
|
||||
if time.Since(c.TimeLimit) > time.Hour {
|
||||
c = lastLength{
|
||||
c = niu.PKRecord{
|
||||
NiuID: niuID,
|
||||
TimeLimit: time.Now(),
|
||||
Count: 1,
|
||||
Length: length,
|
||||
@@ -372,6 +386,9 @@ func init() {
|
||||
)))
|
||||
|
||||
if c.Count >= 4 {
|
||||
if c.Count == 6 {
|
||||
return
|
||||
}
|
||||
id := ctx.SendPrivateMessage(adduser,
|
||||
message.Text(fmt.Sprintf("你在%d群里已经被厥冒烟了,快去群里赎回你原本的牛牛!\n发送:`赎牛牛`即可!", gid)))
|
||||
if id == 0 {
|
||||
@@ -386,8 +403,8 @@ func init() {
|
||||
key := fmt.Sprintf("%d_%d", gid, uid)
|
||||
data, ok := register.Load(key)
|
||||
switch {
|
||||
case !ok || time.Since(data.TimeLimit) > time.Hour*12:
|
||||
data = &lastLength{
|
||||
case !ok || time.Since(data.TimeLimit) > time.Hour*24:
|
||||
data = &niu.PKRecord{
|
||||
TimeLimit: time.Now(),
|
||||
Count: 1,
|
||||
}
|
||||
@@ -396,6 +413,7 @@ func init() {
|
||||
ctx.SendChain(message.Text("你的钱不够你注销牛牛了,这次注销需要", data.Count*50, wallet.GetWalletName()))
|
||||
return
|
||||
}
|
||||
data.Count++
|
||||
}
|
||||
register.Store(key, data)
|
||||
msg, err := niu.Cancel(gid, uid)
|
||||
|
||||
@@ -32,7 +32,7 @@ type favorability struct {
|
||||
|
||||
func init() {
|
||||
// 好感度系统
|
||||
engine.OnMessage(zero.NewPattern().Text(`^查好感度`).At().AsRule(), zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
engine.OnMessage(zero.NewPattern(nil).Text(`^查好感度`).At().AsRule(), zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
patternParsed := ctx.State[zero.KeyPattern].([]zero.PatternParsed)
|
||||
fiancee, _ := strconv.ParseInt(patternParsed[1].At(), 10, 64)
|
||||
@@ -49,7 +49,7 @@ func init() {
|
||||
)
|
||||
})
|
||||
// 礼物系统
|
||||
engine.OnMessage(zero.NewPattern().Text(`^买礼物给`).At().AsRule(), zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
engine.OnMessage(zero.NewPattern(nil).Text(`^买礼物给`).At().AsRule(), zero.OnlyGroup, getdb).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
uid := ctx.Event.UserID
|
||||
|
||||
@@ -93,7 +93,7 @@ func init() {
|
||||
ctx.SendChain(message.Text("设置成功"))
|
||||
})
|
||||
// 单身技能
|
||||
engine.OnMessage(zero.NewPattern().Text(`^(娶|嫁)`).At().AsRule(), zero.OnlyGroup, getdb, checkSingleDog).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
engine.OnMessage(zero.NewPattern(nil).Text(`^(娶|嫁)`).At().AsRule(), zero.OnlyGroup, getdb, checkSingleDog).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
uid := ctx.Event.UserID
|
||||
@@ -168,7 +168,7 @@ func init() {
|
||||
)
|
||||
})
|
||||
// NTR技能
|
||||
engine.OnMessage(zero.NewPattern().Text(`^当`).At().Text(`的小三`).AsRule(), zero.OnlyGroup, getdb, checkMistress).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
engine.OnMessage(zero.NewPattern(nil).Text(`^当`).At().Text(`的小三`).AsRule(), zero.OnlyGroup, getdb, checkMistress).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
uid := ctx.Event.UserID
|
||||
@@ -254,7 +254,7 @@ func init() {
|
||||
)
|
||||
})
|
||||
// 做媒技能
|
||||
engine.OnMessage(zero.NewPattern().Text(`做媒`).At().At().AsRule(), zero.OnlyGroup, zero.AdminPermission, getdb, checkMatchmaker).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
engine.OnMessage(zero.NewPattern(nil).Text(`做媒`).At().At().AsRule(), zero.OnlyGroup, zero.AdminPermission, getdb, checkMatchmaker).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
gid := ctx.Event.GroupID
|
||||
uid := ctx.Event.UserID
|
||||
|
||||
@@ -2,6 +2,7 @@ package score
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
@@ -11,7 +12,10 @@ import (
|
||||
var sdb *scoredb
|
||||
|
||||
// scoredb 分数数据库
|
||||
type scoredb gorm.DB
|
||||
type scoredb struct {
|
||||
db *gorm.DB
|
||||
scoremu sync.Mutex
|
||||
}
|
||||
|
||||
// scoretable 分数结构体
|
||||
type scoretable struct {
|
||||
@@ -52,25 +56,31 @@ func initialize(dbpath string) *scoredb {
|
||||
panic(err)
|
||||
}
|
||||
gdb.AutoMigrate(&scoretable{}).AutoMigrate(&signintable{})
|
||||
return (*scoredb)(gdb)
|
||||
return &scoredb{
|
||||
db: gdb,
|
||||
}
|
||||
}
|
||||
|
||||
// Close ...
|
||||
func (sdb *scoredb) Close() error {
|
||||
db := (*gorm.DB)(sdb)
|
||||
db := sdb.db
|
||||
return db.Close()
|
||||
}
|
||||
|
||||
// GetScoreByUID 取得分数
|
||||
func (sdb *scoredb) GetScoreByUID(uid int64) (s scoretable) {
|
||||
db := (*gorm.DB)(sdb)
|
||||
sdb.scoremu.Lock()
|
||||
defer sdb.scoremu.Unlock()
|
||||
db := sdb.db
|
||||
db.Model(&scoretable{}).FirstOrCreate(&s, "uid = ? ", uid)
|
||||
return s
|
||||
}
|
||||
|
||||
// InsertOrUpdateScoreByUID 插入或更新分数
|
||||
func (sdb *scoredb) InsertOrUpdateScoreByUID(uid int64, score int) (err error) {
|
||||
db := (*gorm.DB)(sdb)
|
||||
sdb.scoremu.Lock()
|
||||
defer sdb.scoremu.Unlock()
|
||||
db := sdb.db
|
||||
s := scoretable{
|
||||
UID: uid,
|
||||
Score: score,
|
||||
@@ -91,14 +101,18 @@ func (sdb *scoredb) InsertOrUpdateScoreByUID(uid int64, score int) (err error) {
|
||||
|
||||
// GetSignInByUID 取得签到次数
|
||||
func (sdb *scoredb) GetSignInByUID(uid int64) (si signintable) {
|
||||
db := (*gorm.DB)(sdb)
|
||||
sdb.scoremu.Lock()
|
||||
defer sdb.scoremu.Unlock()
|
||||
db := sdb.db
|
||||
db.Model(&signintable{}).FirstOrCreate(&si, "uid = ? ", uid)
|
||||
return si
|
||||
}
|
||||
|
||||
// InsertOrUpdateSignInCountByUID 插入或更新签到次数
|
||||
func (sdb *scoredb) InsertOrUpdateSignInCountByUID(uid int64, count int) (err error) {
|
||||
db := (*gorm.DB)(sdb)
|
||||
sdb.scoremu.Lock()
|
||||
defer sdb.scoremu.Unlock()
|
||||
db := sdb.db
|
||||
si := signintable{
|
||||
UID: uid,
|
||||
Count: count,
|
||||
@@ -118,7 +132,9 @@ func (sdb *scoredb) InsertOrUpdateSignInCountByUID(uid int64, count int) (err er
|
||||
}
|
||||
|
||||
func (sdb *scoredb) GetScoreRankByTopN(n int) (st []scoretable, err error) {
|
||||
db := (*gorm.DB)(sdb)
|
||||
sdb.scoremu.Lock()
|
||||
defer sdb.scoremu.Unlock()
|
||||
db := sdb.db
|
||||
err = db.Model(&scoretable{}).Order("score desc").Limit(n).Find(&st).Error
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
package thesaurus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/fumiama/jieba"
|
||||
"github.com/go-ego/gse"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
@@ -55,11 +54,8 @@ func init() {
|
||||
ctx.SendChain(message.Text("成功!"))
|
||||
})
|
||||
go func() {
|
||||
data, err := engine.GetLazyData("dict.txt", false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
seg, err := jieba.LoadDictionary(bytes.NewReader(data))
|
||||
var seg gse.Segmenter
|
||||
err := seg.LoadDictEmbed()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -102,10 +98,10 @@ func init() {
|
||||
ctx.SendChain(message.Text(r.Reply))
|
||||
}
|
||||
})
|
||||
engine.OnMessage(zero.OnlyToMe, canmatch(tDERE), match(chatListD, seg)).
|
||||
engine.OnMessage(zero.OnlyToMe, canmatch(tDERE), match(chatListD, &seg)).
|
||||
SetBlock(false).
|
||||
Handle(randreply(sm.D))
|
||||
engine.OnMessage(zero.OnlyToMe, canmatch(tKAWA), match(chatListK, seg)).
|
||||
engine.OnMessage(zero.OnlyToMe, canmatch(tKAWA), match(chatListK, &seg)).
|
||||
SetBlock(false).
|
||||
Handle(randreply(sm.K))
|
||||
}()
|
||||
@@ -122,7 +118,7 @@ const (
|
||||
tKAWA
|
||||
)
|
||||
|
||||
func match(l []string, seg *jieba.Segmenter) zero.Rule {
|
||||
func match(l []string, seg *gse.Segmenter) zero.Rule {
|
||||
return func(ctx *zero.Ctx) bool {
|
||||
return ctxext.JiebaSimilarity(0.66, seg, func(ctx *zero.Ctx) string {
|
||||
return ctx.ExtractPlainText()
|
||||
|
||||
@@ -8,9 +8,14 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-ego/gse"
|
||||
"github.com/golang/freetype"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
|
||||
"github.com/FloatTech/floatbox/binary"
|
||||
fcext "github.com/FloatTech/floatbox/ctxext"
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
@@ -18,30 +23,34 @@ import (
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
"github.com/FloatTech/zbputils/img/text"
|
||||
"github.com/golang/freetype"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
"github.com/wdvxdr1123/ZeroBot/utils/helper"
|
||||
)
|
||||
|
||||
var (
|
||||
re = regexp.MustCompile(`^[一-龥]+$`)
|
||||
stopwords []string
|
||||
seg gse.Segmenter
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "聊天热词",
|
||||
Help: "- 热词 [群号] [消息数目]|热词 123456 1000",
|
||||
Help: "- 热词 [消息数目]|热词 1000",
|
||||
PublicDataFolder: "WordCount",
|
||||
})
|
||||
cachePath := engine.DataFolder() + "cache/"
|
||||
// 读取gse内置中文词典
|
||||
err := seg.LoadDictEmbed()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = os.RemoveAll(cachePath)
|
||||
_ = os.MkdirAll(cachePath, 0755)
|
||||
engine.OnRegex(`^热词\s?(\d*)\s?(\d*)$`, zero.OnlyGroup, fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool {
|
||||
engine.OnRegex(`^热词\s?(\d*)$`, zero.OnlyGroup, fcext.DoOnceOnSuccess(func(ctx *zero.Ctx) bool {
|
||||
_, err := engine.GetLazyData("stopwords.txt", false)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: ", err))
|
||||
@@ -75,17 +84,14 @@ func init() {
|
||||
}
|
||||
|
||||
ctx.SendChain(message.Text("少女祈祷中..."))
|
||||
gid, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64)
|
||||
p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[2], 10, 64)
|
||||
p, _ := strconv.ParseInt(ctx.State["regex_matched"].([]string)[1], 10, 64)
|
||||
if p > 10000 {
|
||||
p = 10000
|
||||
}
|
||||
if p == 0 {
|
||||
p = 1000
|
||||
}
|
||||
if gid == 0 {
|
||||
gid = ctx.Event.GroupID
|
||||
}
|
||||
gid := ctx.Event.GroupID
|
||||
group := ctx.GetGroupInfo(gid, false)
|
||||
if group.MemberCount == 0 {
|
||||
ctx.SendChain(message.Text(zero.BotConfig.NickName[0], "未加入", group.Name, "(", gid, "),无法获得热词呢"))
|
||||
@@ -98,42 +104,22 @@ func init() {
|
||||
return
|
||||
}
|
||||
messageMap := make(map[string]int, 256)
|
||||
msghists := make(chan *gjson.Result, 256)
|
||||
go func() {
|
||||
h := ctx.GetLatestGroupMessageHistory(gid)
|
||||
messageSeq := h.Get("messages.0.message_seq").Int()
|
||||
msghists <- &h
|
||||
for i := 1; i < int(p/20) && messageSeq != 0; i++ {
|
||||
h := ctx.GetGroupMessageHistory(gid, messageSeq)
|
||||
msghists <- &h
|
||||
messageSeq = h.Get("messages.0.message_seq").Int()
|
||||
}
|
||||
close(msghists)
|
||||
}()
|
||||
var wg sync.WaitGroup
|
||||
var mapmu sync.Mutex
|
||||
for h := range msghists {
|
||||
wg.Add(1)
|
||||
go func(h *gjson.Result) {
|
||||
for _, v := range h.Get("messages.#.message").Array() {
|
||||
tex := strings.TrimSpace(message.ParseMessageFromString(v.Str).ExtractPlainText())
|
||||
if tex == "" {
|
||||
continue
|
||||
}
|
||||
for _, t := range ctx.GetWordSlices(tex).Get("slices").Array() {
|
||||
tex := strings.TrimSpace(t.Str)
|
||||
i := sort.SearchStrings(stopwords, tex)
|
||||
if re.MatchString(tex) && (i >= len(stopwords) || stopwords[i] != tex) {
|
||||
mapmu.Lock()
|
||||
messageMap[tex]++
|
||||
mapmu.Unlock()
|
||||
}
|
||||
h := ctx.GetGroupMessageHistory(gid, 0, p, false)
|
||||
h.Get("messages").ForEach(func(_, msgObj gjson.Result) bool {
|
||||
tex := strings.TrimSpace(message.ParseMessageFromString(msgObj.Get("raw_message").Str).ExtractPlainText())
|
||||
if tex != "" {
|
||||
segments := seg.Segment(helper.StringToBytes(tex))
|
||||
words := gse.ToSlice(segments, true)
|
||||
for _, word := range words {
|
||||
word = strings.TrimSpace(word)
|
||||
i := sort.SearchStrings(stopwords, word)
|
||||
if re.MatchString(word) && (i >= len(stopwords) || stopwords[i] != word) {
|
||||
messageMap[word]++
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(h)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
wc := rankByWordCount(messageMap)
|
||||
if len(wc) > 20 {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
mkGoEnv ? pkgs.mkGoEnv,
|
||||
gomod2nix ? pkgs.gomod2nix,
|
||||
}: let
|
||||
goEnv = mkGoEnv {pwd = ./.;};
|
||||
goEnv = mkGoEnv { pwd = ./.; go = pkgs.go_1_20; };
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"0409": {
|
||||
"identity": {
|
||||
"name": "ZeroBot-Plugin",
|
||||
"version": "1.9.5.2188"
|
||||
"version": "1.9.9.2250"
|
||||
},
|
||||
"description": "",
|
||||
"minimum-os": "vista",
|
||||
@@ -36,23 +36,23 @@
|
||||
"#1": {
|
||||
"0000": {
|
||||
"fixed": {
|
||||
"file_version": "1.9.5.2188",
|
||||
"product_version": "v1.9.5",
|
||||
"timestamp": "2025-02-22T15:34:40+08:00"
|
||||
"file_version": "1.9.9.2250",
|
||||
"product_version": "v1.9.9",
|
||||
"timestamp": "2025-09-10T10:40:54+08:00"
|
||||
},
|
||||
"info": {
|
||||
"0409": {
|
||||
"Comments": "OneBot plugins based on ZeroBot",
|
||||
"CompanyName": "FloatTech",
|
||||
"FileDescription": "https://github.com/FloatTech/ZeroBot-Plugin",
|
||||
"FileVersion": "1.9.5.2188",
|
||||
"FileVersion": "1.9.9.2250",
|
||||
"InternalName": "",
|
||||
"LegalCopyright": "© 2020 - 2025 FloatTech. All Rights Reserved.",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "ZBP.EXE",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "ZeroBot-Plugin",
|
||||
"ProductVersion": "v1.9.5",
|
||||
"ProductVersion": "v1.9.9",
|
||||
"SpecialBuild": ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user