mirror of
https://github.com/FloatTech/ZeroBot-Plugin.git
synced 2026-02-08 08:00:25 +00:00
Compare commits
1 Commits
master
...
create-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b56f361dc8 |
4
.github/workflows/push.yml
vendored
4
.github/workflows/push.yml
vendored
@@ -20,10 +20,10 @@ jobs:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Prepare Necessary Runtime Files
|
||||
- name: Tidy Modules
|
||||
run: |
|
||||
go generate main.go
|
||||
go mod tidy
|
||||
go generate main.go
|
||||
|
||||
- name: Run Lint
|
||||
uses: golangci/golangci-lint-action@master
|
||||
|
||||
14
README.md
14
README.md
@@ -845,16 +845,6 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
- [x] 下载歌单[网易云歌单链接/ID]到[歌单名称]
|
||||
- [x] 解除绑定 [歌单名称]
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>猜成语</summary>
|
||||
|
||||
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/handou"`
|
||||
|
||||
- [x] 个人猜成语
|
||||
|
||||
- [x] 团队猜成语
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>一言</summary>
|
||||
@@ -1645,9 +1635,7 @@ print("run[CQ:image,file="+j["img"]+"]")
|
||||
- [x] 设置AI聊天(不)以AI语音输出
|
||||
- [x] 设置AI聊天Agent性格
|
||||
- [x] 查看AI聊天Agent性格
|
||||
- [x] 设置AI聊天Agent性别
|
||||
- [x] 查看AI聊天Agent性别
|
||||
- [x] 重置AI聊天Agent性格性别
|
||||
- [x] 重置AI聊天Agent性格
|
||||
- [x] 重置AI聊天Agent
|
||||
- [x] 查看AI聊天配置
|
||||
- [x] 重置AI聊天
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//
|
||||
// Place this package at the very top of top-level main.go so its init (present
|
||||
// or future) executes before other plugin packages, filling in a predictable
|
||||
// plugin priority and setup console properties.
|
||||
// plugin priority.
|
||||
//
|
||||
// Typical usage:
|
||||
//
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package abineundo
|
||||
// Package console sets console's behavior on init
|
||||
package console
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,5 @@
|
||||
package abineundo
|
||||
// Package console sets console's behavior on init
|
||||
package console
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -18,9 +18,9 @@ buildGoApplication {
|
||||
version = "1.8.0";
|
||||
pwd = ./.;
|
||||
src = ./.;
|
||||
go = pkgs.go_1_24;
|
||||
preBuild = ''
|
||||
go generate main.go
|
||||
'';
|
||||
# spec go version manually bcs
|
||||
# https://github.com/nix-community/gomod2nix/blob/30e3c3a9ec4ac8453282ca7f67fca9e1da12c3e6/builder/default.nix#L130
|
||||
# do not work
|
||||
go = pkgs.go_1_20;
|
||||
modules = ./gomod2nix.toml;
|
||||
}
|
||||
|
||||
31
flake.lock
generated
31
flake.lock
generated
@@ -28,11 +28,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1767019875,
|
||||
"narHash": "sha256-NodN+lhWTD59b44Q2bPjE1edINfjfRkQYdZsrxifCeU=",
|
||||
"lastModified": 1742209644,
|
||||
"narHash": "sha256-jMy1XqXqD0/tJprEbUmKilTkvbDY/C0ZGSsJJH4TNCE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "49662a44272806ff785df2990a420edaaca15db4",
|
||||
"rev": "8f3534eb8f6c5c3fce799376dc3b91bae6b11884",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -43,11 +43,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769461804,
|
||||
"narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=",
|
||||
"lastModified": 1745391562,
|
||||
"narHash": "sha256-sPwcCYuiEopaafePqlG826tBhctuJsLx/mhKKM5Fmjo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d",
|
||||
"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": {
|
||||
|
||||
109
flake.nix
109
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,85 +11,53 @@
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
nixpkgs-with-go_1_20,
|
||||
flake-utils,
|
||||
gomod2nix,
|
||||
}: (flake-utils.lib.eachDefaultSystem (
|
||||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
...
|
||||
} @ inputs: let
|
||||
allSystems = flake-utils.lib.allSystems;
|
||||
in (
|
||||
flake-utils.lib.eachSystem allSystems
|
||||
(system: let
|
||||
old-nixpkgs = nixpkgs-with-go_1_20.legacyPackages.${system};
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
|
||||
callPackage = pkgs.callPackage;
|
||||
# Simple test check added to nix flake check
|
||||
go-test = pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "go-test";
|
||||
dontBuild = true;
|
||||
src = ./.;
|
||||
doCheck = true;
|
||||
nativeBuildInputs = with pkgs; [
|
||||
go
|
||||
writableTmpDirAsHomeHook
|
||||
overlays = [
|
||||
(_: _: {
|
||||
go_1_20 = old-nixpkgs.go_1_20;
|
||||
})
|
||||
];
|
||||
checkPhase = ''
|
||||
go test -v ./...
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir "$out"
|
||||
'';
|
||||
};
|
||||
# Simple lint check added to nix flake check
|
||||
go-lint = pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "go-lint";
|
||||
dontBuild = true;
|
||||
src = ./.;
|
||||
doCheck = true;
|
||||
nativeBuildInputs = with pkgs; [
|
||||
golangci-lint
|
||||
go
|
||||
writableTmpDirAsHomeHook
|
||||
];
|
||||
checkPhase = ''
|
||||
golangci-lint run
|
||||
'';
|
||||
installPhase = ''
|
||||
mkdir "$out"
|
||||
'';
|
||||
};
|
||||
# doCheck will fail at download files
|
||||
ZeroBot-Plugin = (callPackage ./. {
|
||||
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
|
||||
}).overrideAttrs (_: {doCheck = false;});
|
||||
# Build container layered image, useful overtime to save storage on duplicated layers
|
||||
containerImage = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "ZeroBot-Plugin";
|
||||
tag = "latest";
|
||||
created = "now";
|
||||
contents = [
|
||||
pkgs.cacert
|
||||
pkgs.openssl
|
||||
];
|
||||
config = {
|
||||
Cmd = ["${ZeroBot-Plugin}/bin/ZeroBot-Plugin"];
|
||||
|
||||
# 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.
|
||||
callPackage = pkgs.darwin.apple_sdk_11_0.callPackage or pkgs.callPackage;
|
||||
in {
|
||||
# doCheck will fail at write files
|
||||
packages = rec {
|
||||
ZeroBot-Plugin = (callPackage ./. (inputs
|
||||
// {
|
||||
inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
|
||||
}))
|
||||
.overrideAttrs (_: {doCheck = false;});
|
||||
|
||||
default = ZeroBot-Plugin;
|
||||
|
||||
docker_builder = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "ZeroBot-Plugin";
|
||||
tag = "latest";
|
||||
contents = [
|
||||
self.packages.${system}.ZeroBot-Plugin
|
||||
pkgs.cacert
|
||||
];
|
||||
};
|
||||
};
|
||||
in {
|
||||
inherit containerImage;
|
||||
checks = {
|
||||
inherit go-test go-lint;
|
||||
};
|
||||
packages.default = ZeroBot-Plugin;
|
||||
devShells.default = callPackage ./shell.nix {
|
||||
inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix;
|
||||
};
|
||||
# Custom application to build and load container image into the docker daemon
|
||||
# For now docker is a requirement
|
||||
apps.build-and-load = {
|
||||
type = "app";
|
||||
program = "${pkgs.writeShellScriptBin "build-and-load" ''
|
||||
nix build .#containerImage.${system}
|
||||
docker load < result
|
||||
echo "Container image loaded"
|
||||
''}/bin/build-and-load";
|
||||
};
|
||||
formatter = pkgs.alejandra;
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
9
go.mod
9
go.mod
@@ -12,7 +12,7 @@ require (
|
||||
github.com/FloatTech/sqlite v1.7.2
|
||||
github.com/FloatTech/ttl v0.0.0-20250224045156-012b1463287d
|
||||
github.com/FloatTech/zbpctrl v1.7.1
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20260131170726-494cb1776a47
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20260106131604-3ff237cdb792
|
||||
github.com/RomiChan/syncx v0.0.0-20240418144900-b7402ffdebc7
|
||||
github.com/RomiChan/websocket v1.4.3-0.20251002072000-d3eb41798438
|
||||
github.com/Tnze/go-mc v1.20.2
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/fumiama/cron v1.3.0
|
||||
github.com/fumiama/deepinfra v0.0.0-20251221163610-e98ee3ba437a
|
||||
github.com/fumiama/go-base16384 v1.7.1
|
||||
github.com/fumiama/go-onebot-agent v0.0.0-20260128132028-05e6b4809f0a
|
||||
github.com/fumiama/go-onebot-agent v0.0.0-20260106123931-a0c81601f2d7
|
||||
github.com/fumiama/go-registry v0.2.7
|
||||
github.com/fumiama/gotracemoe v0.0.3
|
||||
github.com/fumiama/imgsz v0.0.4
|
||||
@@ -34,7 +34,6 @@ require (
|
||||
github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/guohuiyuan/music-lib v1.0.2-0.20260121020416-53f6cb24629d
|
||||
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
|
||||
@@ -45,10 +44,10 @@ require (
|
||||
github.com/notnil/chess v1.10.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
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.3-0.20260117102541-393033a35adb
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.3-0.20260103120253-8a8f1347f983
|
||||
gitlab.com/gomidi/midi/v2 v2.3.18
|
||||
golang.org/x/image v0.34.0
|
||||
golang.org/x/sys v0.39.0
|
||||
|
||||
20
go.sum
20
go.sum
@@ -16,8 +16,8 @@ github.com/FloatTech/ttl v0.0.0-20250224045156-012b1463287d h1:mUQ/c3wXKsUGa4Sg9
|
||||
github.com/FloatTech/ttl v0.0.0-20250224045156-012b1463287d/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs=
|
||||
github.com/FloatTech/zbpctrl v1.7.1 h1:0yPEmCForhyMbnhTckmjDUFFDZgQp1RjO2bVF4ZVqOs=
|
||||
github.com/FloatTech/zbpctrl v1.7.1/go.mod h1:xmM4dSwHA02Gei3ogCRiG+RTrw/7Z69PfrN5NYf8BPE=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20260131170726-494cb1776a47 h1:slMr6r4XDKnYCFmWhcHA02O3MTAUnU8p2gEe843JyQA=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20260131170726-494cb1776a47/go.mod h1:W2kaR/A5oUtEb7DnveXCc0T374VjI+f3KmOWH9FE5vU=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20260106131604-3ff237cdb792 h1:LHJ75vp0gizrO1ko/2t8/98mPHD1bQSXkonn02VRB4A=
|
||||
github.com/FloatTech/zbputils v1.7.2-0.20260106131604-3ff237cdb792/go.mod h1:TKnvn/rgLBlcizp8XtseduIkAw1Mq3cwpvT5GAG69fA=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
@@ -67,8 +67,8 @@ github.com/fumiama/deepinfra v0.0.0-20251221163610-e98ee3ba437a h1:a0+2vaXajfxsN
|
||||
github.com/fumiama/deepinfra v0.0.0-20251221163610-e98ee3ba437a/go.mod h1:uqsWK/GM9OvKV0pXZOQB63rWugBbiXInY8E1JoRKhkg=
|
||||
github.com/fumiama/go-base16384 v1.7.1 h1:1P1x6FWRvd7PtbH4idDAGWAjKKcVxggxlROYKRXbw58=
|
||||
github.com/fumiama/go-base16384 v1.7.1/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM=
|
||||
github.com/fumiama/go-onebot-agent v0.0.0-20260128132028-05e6b4809f0a h1:8GYo5nctK2si5WDNX0WmZTxY7TWXRjAOBu5pjK7GDW0=
|
||||
github.com/fumiama/go-onebot-agent v0.0.0-20260128132028-05e6b4809f0a/go.mod h1:rTrS23rvTYuZcSngENJTvcBFTz1nGsImSv+bW7yfhqs=
|
||||
github.com/fumiama/go-onebot-agent v0.0.0-20260106123931-a0c81601f2d7 h1:QYlLOAWxH3PU/vFiTmhI+TnsA4i+ahk+UxSbqBxkEOU=
|
||||
github.com/fumiama/go-onebot-agent v0.0.0-20260106123931-a0c81601f2d7/go.mod h1:rTrS23rvTYuZcSngENJTvcBFTz1nGsImSv+bW7yfhqs=
|
||||
github.com/fumiama/go-registry v0.2.7 h1:tLEqgEpsiybQMqBv0dLHm5leia/z1DhajMupwnOHeNs=
|
||||
github.com/fumiama/go-registry v0.2.7/go.mod h1:m+wp5fF8dYgVoFkBPZl+vlK90loymaJE0JCtocVQLEs=
|
||||
github.com/fumiama/go-simple-protobuf v0.2.0 h1:ACyN1MAlu7pDR3EszWgzUeNP+IRsSHwH6V9JCJA5R5o=
|
||||
@@ -117,8 +117,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopxl/beep/v2 v2.1.1 h1:6FYIYMm2qPAdWkjX+7xwKrViS1x0Po5kDMdRkq8NVbU=
|
||||
github.com/gopxl/beep/v2 v2.1.1/go.mod h1:ZAm9TGQ9lvpoiFLd4zf5B1IuyxZhgRACMId1XJbaW0E=
|
||||
github.com/guohuiyuan/music-lib v1.0.2-0.20260121020416-53f6cb24629d h1:6Cw52c4JaYvq55yAa9ZgUQeBL6b3ZWErQqkbeMZiAYw=
|
||||
github.com/guohuiyuan/music-lib v1.0.2-0.20260121020416-53f6cb24629d/go.mod h1:D/6kQDwhQFDNZEMjN8y760DQSVYpOGlQXrYzhKz0rCQ=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
|
||||
@@ -184,10 +182,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
@@ -207,8 +206,8 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
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.3-0.20260117102541-393033a35adb h1:pwmyrnnMzb0WygAwUgXoZckzJkusK4WSaXxXqsIJYmU=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.3-0.20260117102541-393033a35adb/go.mod h1:kCLja2sXXgbBTsEOyBNCuT4z9tI+URQ2y0q/GGXprzU=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.3-0.20260103120253-8a8f1347f983 h1:hb2FUDooAf3u32wCTgJcBBuGPZF1sjgj8NfJaSy529s=
|
||||
github.com/wdvxdr1123/ZeroBot v1.8.3-0.20260103120253-8a8f1347f983/go.mod h1:trueIIVRywKJa3ov4QphzVvzYzgCNrlXdf9JvPJOFW8=
|
||||
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=
|
||||
@@ -269,6 +268,7 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -29,8 +29,8 @@ schema = 3
|
||||
version = "v1.7.1"
|
||||
hash = "sha256-wkeiaUTpPVbpH7fcXeoLtG+aGIMJbvoc/9sbi2IXK0I="
|
||||
[mod."github.com/FloatTech/zbputils"]
|
||||
version = "v1.7.2-0.20260131170726-494cb1776a47"
|
||||
hash = "sha256-KLhPEHoJiq2qVkK+7OVy7Wc2wpCFEJOqa/0zZGQ8TnM="
|
||||
version = "v1.7.2-0.20260106131604-3ff237cdb792"
|
||||
hash = "sha256-gI8wWI5hjyhYLnj85egm4ARhEnsX16DixW20W6PXYQk="
|
||||
[mod."github.com/PuerkitoBio/goquery"]
|
||||
version = "v1.8.0"
|
||||
hash = "sha256-I3QaPWATvBOL/F26fIiYWKS13yBUYo+9o3tcsGIu8tY="
|
||||
@@ -92,8 +92,8 @@ schema = 3
|
||||
version = "v1.7.1"
|
||||
hash = "sha256-Fd1QaeYx+3q4C3XQXlPFnDmKPsoZH6837fN/7rn8i9s="
|
||||
[mod."github.com/fumiama/go-onebot-agent"]
|
||||
version = "v0.0.0-20260128132028-05e6b4809f0a"
|
||||
hash = "sha256-ratY7o52v0KuxgZC4wqHNXdgGXzliEecs8egE3SBLeo="
|
||||
version = "v0.0.0-20260106123931-a0c81601f2d7"
|
||||
hash = "sha256-YqG/SnqOIkWDEKHPL79mbccHY8iwW4acVdTiWIdKeVM="
|
||||
[mod."github.com/fumiama/go-registry"]
|
||||
version = "v0.2.7"
|
||||
hash = "sha256-Rjl+z0Hlp2LMi8+pnFe5HrxctyHMi7UPiK33g/OgLdA="
|
||||
@@ -142,9 +142,6 @@ schema = 3
|
||||
[mod."github.com/gopxl/beep/v2"]
|
||||
version = "v2.1.1"
|
||||
hash = "sha256-JLCUJCG+VvNlVF296JWIOUvvUFHlqEAJvZfw853qwwU="
|
||||
[mod."github.com/guohuiyuan/music-lib"]
|
||||
version = "v1.0.2-0.20260121020416-53f6cb24629d"
|
||||
hash = "sha256-juVJ/nh6zA5Gu5+dRzIx8tElXLscRQYwY9vLvVKh078="
|
||||
[mod."github.com/jfreymuth/oggvorbis"]
|
||||
version = "v1.0.5"
|
||||
hash = "sha256-jphTCaPr34ZT9Id4ZZ6zU9Vnxzy6cTjCwjpQ819eGV0="
|
||||
@@ -227,8 +224,8 @@ schema = 3
|
||||
version = "v4.25.12"
|
||||
hash = "sha256-gzk9GW4+tXUWmxAVD3by/k4D/+l++TvajRVTkQJvwmM="
|
||||
[mod."github.com/sirupsen/logrus"]
|
||||
version = "v1.9.4"
|
||||
hash = "sha256-ltRvmtM3XTCAFwY0IesfRqYIivyXPPuvkFjL4ARh1wg="
|
||||
version = "v1.9.3"
|
||||
hash = "sha256-EnxsWdEUPYid+aZ9H4/iMTs1XMvCLbXZRDyvj89Ebms="
|
||||
[mod."github.com/tetratelabs/wazero"]
|
||||
version = "v1.5.0"
|
||||
hash = "sha256-fGdJM4LJrZA9jxHuYVo4EUQ3I1k0IVG3QQCBCgZkeZI="
|
||||
@@ -251,8 +248,8 @@ schema = 3
|
||||
version = "v2.1.2"
|
||||
hash = "sha256-GXWWea/u6BezTsPPrWhTYiTetPP/YW6P+Sj4YdocPaM="
|
||||
[mod."github.com/wdvxdr1123/ZeroBot"]
|
||||
version = "v1.8.3-0.20260117102541-393033a35adb"
|
||||
hash = "sha256-Yz2OTU05kDZOHX8J04jX5Jg5ya9rwqsH0TySSBhMOp0="
|
||||
version = "v1.8.3-0.20260103120253-8a8f1347f983"
|
||||
hash = "sha256-/6biuhXUIYxyJ0uT1R5Yk8RD2cm9/SQWtFvcyQgnWJE="
|
||||
[mod."github.com/yusufpapurcu/wmi"]
|
||||
version = "v1.2.4"
|
||||
hash = "sha256-N+YDBjOW59YOsZ2lRBVtFsEEi48KhNQRb63/0ZSU3bA="
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
package banner
|
||||
|
||||
// Version ...
|
||||
var Version = "v1.10.18"
|
||||
var Version = "v1.10.8"
|
||||
|
||||
// Copyright ...
|
||||
var Copyright = "© 2020 - 2026 FloatTech"
|
||||
|
||||
// Banner ...
|
||||
var Banner = "* OneBot + ZeroBot + Golang\n" +
|
||||
"* Version " + Version + " - 2026-02-01 01:08:19 +0800 CST\n" +
|
||||
"* Version " + Version + " - 2026-01-05 00:50:21 +0800 CST\n" +
|
||||
"* Copyright " + Copyright + ". All Rights Reserved.\n" +
|
||||
"* Project: https://github.com/FloatTech/ZeroBot-Plugin"
|
||||
|
||||
4
main.go
4
main.go
@@ -14,7 +14,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/abineundo" // 设置插件优先级&更改控制台属性
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/abineundo" // 设置插件优先级
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/console" // 更改控制台属性
|
||||
"github.com/FloatTech/ZeroBot-Plugin/kanban" // 打印 banner
|
||||
|
||||
// ---------以下插件均可通过前面加 // 注释,注释后停用并不加载插件--------- //
|
||||
@@ -104,7 +105,6 @@ import (
|
||||
_ "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/handou" // 猜成语
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hitokoto" // 一言
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hs" // 炉石
|
||||
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/hyaku" // 百人一首
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
package aichat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/RomiChan/syncx"
|
||||
"github.com/fumiama/deepinfra"
|
||||
goba "github.com/fumiama/go-onebot-agent"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -53,11 +51,6 @@ func init() {
|
||||
logrus.Warnln("ERROR: cannot get stor")
|
||||
return false
|
||||
}
|
||||
mp := ctx.State[control.StateKeySyncxState].(*syncx.Map[string, any])
|
||||
if _, ok := mp.Load(chat.StateKeyAgentHooked); !ok && !stor.NoAgent() {
|
||||
logrus.Infoln("[aichat] skip agent for ctx has not been hooked by agent")
|
||||
return false
|
||||
}
|
||||
if !(ctx.ExtractPlainText() != "" &&
|
||||
(!stor.NoReplyAt() || (stor.NoReplyAt() && !ctx.Event.IsToMe))) {
|
||||
return false
|
||||
@@ -66,6 +59,10 @@ func init() {
|
||||
if !ctx.Event.IsToMe && rand.Intn(100) >= int(rate) {
|
||||
return false
|
||||
}
|
||||
if chat.AC.Key == "" {
|
||||
logrus.Warnln("ERROR: get extra err: empty key")
|
||||
return false
|
||||
}
|
||||
if ctx.Event.IsToMe {
|
||||
ctx.Block()
|
||||
}
|
||||
@@ -78,10 +75,9 @@ func init() {
|
||||
stor := ctx.State[zero.StateKeyPrefixKeep+"aichatcfg_stor__"].(chat.Storage)
|
||||
temperature := stor.Temp()
|
||||
topp, maxn := chat.AC.MParams()
|
||||
mp := ctx.State[control.StateKeySyncxState].(*syncx.Map[string, any])
|
||||
|
||||
logrus.Debugln("[aichat] agent mode test: noagent", stor.NoAgent(), "hasapi", chat.AC.AgentAPI != "", "hasmodel", chat.AC.AgentModelName != "")
|
||||
if !stor.NoAgent() && chat.AC.AgentAPI != "" && chat.AC.AgentModelName != "" && chat.AC.Key != "" {
|
||||
if !stor.NoAgent() && chat.AC.AgentAPI != "" && chat.AC.AgentModelName != "" {
|
||||
logrus.Debugln("[aichat] enter agent mode")
|
||||
x := deepinfra.NewAPI(chat.AC.AgentAPI, string(chat.AC.AgentKey))
|
||||
mod, err := chat.AC.Type.Protocol(chat.AC.AgentModelName, temperature, topp, maxn)
|
||||
@@ -114,45 +110,18 @@ func init() {
|
||||
ctx.NoTimeout()
|
||||
logrus.Debugln("[aichat] agent set no timeout")
|
||||
hasresp := false
|
||||
// ispuremsg := false
|
||||
// hassavemem := false
|
||||
for i := 0; i < 8; i++ { // 最大运行 8 轮因为问答上下文只有 16
|
||||
reqs := chat.CallAgent(ag, zero.SuperUserPermission(ctx), i+1, x, mod, gid, role)
|
||||
reqs := chat.CallAgent(ag, zero.SuperUserPermission(ctx), x, mod, gid, role)
|
||||
if len(reqs) == 0 {
|
||||
logrus.Debugln("[aichat] agent call got empty response")
|
||||
break
|
||||
}
|
||||
hasresp = true
|
||||
mp.Store(chat.StateKeyAgentTriggered, struct{}{})
|
||||
for _, req := range reqs {
|
||||
if req.Action == goba.SVM { // is a fake action
|
||||
/*if hassavemem {
|
||||
ag.AddTerminus(gid)
|
||||
logrus.Warnln("[aichat] agent call save mem multi times, force inserting EOA")
|
||||
return
|
||||
}
|
||||
hassavemem = true*/
|
||||
continue
|
||||
}
|
||||
/*if req.Action == "send_private_msg" || req.Action == "send_group_msg" {
|
||||
if ispuremsg {
|
||||
ag.AddTerminus(gid)
|
||||
logrus.Warnln("[aichat] agent call send msg multi times, force inserting EOA")
|
||||
return
|
||||
}
|
||||
ispuremsg = true
|
||||
}*/
|
||||
logrus.Debugln("[chat] agent triggered", gid, "add requ:", &req)
|
||||
ag.AddRequest(gid, &req)
|
||||
rsp := ctx.CallAction(req.Action, req.Params)
|
||||
logrus.Debugln("[chat] agent triggered", gid, "add resp:", &rsp)
|
||||
ag.AddResponse(gid, &goba.APIResponse{
|
||||
Status: rsp.Status,
|
||||
Data: json.RawMessage(rsp.Data.Raw),
|
||||
Message: rsp.Message,
|
||||
Wording: rsp.Wording,
|
||||
RetCode: rsp.RetCode,
|
||||
})
|
||||
_ = ctx.CallAction(req.Action, req.Params)
|
||||
}
|
||||
}
|
||||
if hasresp {
|
||||
@@ -188,7 +157,7 @@ func init() {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
logrus.Debugln("[aichat] 回复内容:", t)
|
||||
logrus.Infoln("[aichat] 回复内容:", t)
|
||||
recCfg := airecord.GetConfig()
|
||||
record := ""
|
||||
if !fastfailnorecord && !stor.NoRecord() {
|
||||
|
||||
@@ -29,11 +29,9 @@ var (
|
||||
"- 查看AI聊天系统提示词\n" +
|
||||
"- 重置AI聊天系统提示词\n" +
|
||||
"- 设置AI聊天系统提示词xxx\n" +
|
||||
"- 设置AI聊天Agent性格xxx" +
|
||||
"- 查看AI聊天Agent性格" +
|
||||
"- 设置AI聊天Agent性别xxx" +
|
||||
"- 查看AI聊天Agent性别" +
|
||||
"- 重置AI聊天Agent性格性别\n" +
|
||||
"- 设置AI聊天Agent性格" +
|
||||
"- 查看AI聊天Agent性格xxx" +
|
||||
"- 重置AI聊天Agent性格\n" +
|
||||
"- 设置AI聊天分隔符</think>(留空则清除)\n" +
|
||||
"- 设置AI聊天(不)响应AT\n" +
|
||||
"- 设置AI聊天最大长度4096\n" +
|
||||
@@ -47,6 +45,12 @@ var (
|
||||
|
||||
func init() {
|
||||
en.UsePreHandler(chat.EnsureConfig, func(ctx *zero.Ctx) bool {
|
||||
if !chat.IsAgentCharReady {
|
||||
if chat.AC.AgentChar != "" {
|
||||
chat.AgentChar = []byte(chat.AC.AgentChar)
|
||||
}
|
||||
chat.IsAgentCharReady = true
|
||||
}
|
||||
k := zero.StateKeyPrefixKeep + "aichatcfg_stor__"
|
||||
if _, ok := ctx.State[k]; ok {
|
||||
return true
|
||||
@@ -94,13 +98,7 @@ func init() {
|
||||
en.OnPrefix("设置AI聊天系统提示词", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetStr(&chat.AC.SystemP))
|
||||
en.OnPrefix("设置AI聊天Agent性格", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetStr(&chat.AC.AgentChar), func(_ *zero.Ctx) {
|
||||
chat.AgentCharConfig.Chars = chat.AC.AgentChar
|
||||
})
|
||||
en.OnPrefix("设置AI聊天Agent性别", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetStr(&chat.AC.AgentSex), func(_ *zero.Ctx) {
|
||||
chat.AgentCharConfig.Sex = chat.AC.AgentSex
|
||||
})
|
||||
Handle(chat.NewExtraSetStr(&chat.AC.AgentChar))
|
||||
en.OnFullMatch("查看AI聊天系统提示词", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
ctx.SendChain(message.Text(chat.AC.SystemP))
|
||||
})
|
||||
@@ -121,13 +119,13 @@ func init() {
|
||||
}
|
||||
ctx.SendChain(message.Text("成功"))
|
||||
})
|
||||
en.OnFullMatch("重置AI聊天Agent性格性别", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
|
||||
en.OnFullMatch("重置AI聊天Agent性格", chat.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
|
||||
}
|
||||
chat.ResetAgentCharConfig()
|
||||
chat.AC.AgentChar = ""
|
||||
err := c.SetExtra(&chat.AC)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: set extra err: ", err))
|
||||
@@ -137,17 +135,17 @@ func init() {
|
||||
})
|
||||
en.OnPrefix("设置AI聊天分隔符", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetStr(&chat.AC.Separator))
|
||||
en.OnRegex("^设置AI聊天(不)?响应AT$", zero.SuperUserPermission).SetBlock(true).
|
||||
en.OnRegex("^设置AI聊天(不)?响应AT$", chat.EnsureConfig, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(ctxext.NewStorageSaveBoolHandler(chat.BitmapNrat))
|
||||
en.OnRegex("^设置AI聊天(不)?支持系统提示词$", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetBool(&chat.AC.NoSystemP))
|
||||
en.OnRegex("^设置AI聊天(不)?使用Agent模式$", zero.SuperUserPermission).SetBlock(true).
|
||||
en.OnRegex("^设置AI聊天(不)?使用Agent模式$", chat.EnsureConfig, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(ctxext.NewStorageSaveBoolHandler(chat.BitmapNagt))
|
||||
en.OnPrefix("设置AI聊天最大长度", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetUint(&chat.AC.MaxN))
|
||||
en.OnPrefix("设置AI聊天TopP", chat.EnsureConfig, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(chat.NewExtraSetFloat32(&chat.AC.TopP))
|
||||
en.OnRegex("^设置AI聊天(不)?以AI语音输出$", zero.AdminPermission).SetBlock(true).
|
||||
en.OnRegex("^设置AI聊天(不)?以AI语音输出$", chat.EnsureConfig, zero.AdminPermission).SetBlock(true).
|
||||
Handle(ctxext.NewStorageSaveBoolHandler(chat.BitmapNrec))
|
||||
en.OnFullMatch("查看AI聊天配置", chat.EnsureConfig, zero.SuperUserPermission).SetBlock(true).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package crypter
|
||||
|
||||
import (
|
||||
"github.com/FloatTech/AnimeAPI/airecord"
|
||||
"github.com/sirupsen/logrus"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
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 != "" {
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
lotsList = func() map[string]info {
|
||||
lotsList, err := getList()
|
||||
if err != nil {
|
||||
logrus.Infoln("[drawlots]加载失败:", err, "(如果从未使用过该插件, 这是正常现象)")
|
||||
logrus.Infoln("[drawlots]加载失败:", err)
|
||||
} else {
|
||||
logrus.Infoln("[drawlots]加载", len(lotsList), "个抽签")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
"github.com/pkg/errors"
|
||||
@@ -28,7 +30,7 @@ func init() {
|
||||
var err1 error
|
||||
t2s, err1 = gocc.New("t2s")
|
||||
if err1 != nil {
|
||||
panic(err1)
|
||||
log.Infof("[guessmusic]:%s", err1)
|
||||
}
|
||||
|
||||
engine.OnRegex(`^(个人|团队)猜歌(-(.*))?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup).
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
// Package handou 猜成语
|
||||
package handou
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type baiduAPIData struct {
|
||||
Errno int `json:"errno"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Data struct {
|
||||
IdiomVersion int `json:"idiomVersion"`
|
||||
Name string `json:"name"`
|
||||
Sid string `json:"sid"`
|
||||
Type string `json:"type"`
|
||||
LessonInfo any `json:"lessonInfo"`
|
||||
RelationInfo struct {
|
||||
RelationName string `json:"relationName"`
|
||||
RelationList []struct {
|
||||
Name string `json:"name"`
|
||||
Imgs []string `json:"imgs"`
|
||||
} `json:"relationList"`
|
||||
} `json:"relationInfo"`
|
||||
Imgs []string `json:"imgs"`
|
||||
Definition []struct {
|
||||
Pinyin string `json:"pinyin"`
|
||||
Voice string `json:"voice"`
|
||||
Definition []string `json:"definition"`
|
||||
DetailDefinition any `json:"detailDefinition"`
|
||||
} `json:"definition"`
|
||||
DefinitionInfo struct {
|
||||
Definition string `json:"definition"`
|
||||
SimilarDefinition string `json:"similarDefinition"`
|
||||
AncientDefinition string `json:"ancientDefinition"`
|
||||
ModernDefinition string `json:"modernDefinition"`
|
||||
DetailMeans []struct {
|
||||
Word string `json:"word"`
|
||||
Definition string `json:"definition"`
|
||||
} `json:"detailMeans"`
|
||||
UsageTips any `json:"usageTips"`
|
||||
Yicuodian any `json:"yicuodian"`
|
||||
Baobian string `json:"baobian"`
|
||||
WordFormation string `json:"wordFormation"`
|
||||
} `json:"definitionInfo"`
|
||||
Liju []struct {
|
||||
Name string `json:"name"`
|
||||
ShowName string `json:"showName"`
|
||||
} `json:"liju"`
|
||||
Source string `json:"source"`
|
||||
Story any `json:"story"`
|
||||
Antonym []struct {
|
||||
Name string `json:"name"`
|
||||
IsClick bool `json:"isClick"`
|
||||
} `json:"antonym"`
|
||||
Synonym []string `json:"synonym"`
|
||||
Synonyms []struct {
|
||||
Name string `json:"name"`
|
||||
IsClick bool `json:"isClick"`
|
||||
} `json:"synonyms"`
|
||||
Tongyiyixing []struct {
|
||||
Name string `json:"name"`
|
||||
IsClick bool `json:"isClick"`
|
||||
} `json:"tongyiyixing"`
|
||||
ChuChu []struct {
|
||||
SourceChapter string `json:"sourceChapter"`
|
||||
Source string `json:"source"`
|
||||
Dynasty string `json:"dynasty"`
|
||||
CiteOriginalText string `json:"citeOriginalText"`
|
||||
Author string `json:"author"`
|
||||
} `json:"chuChu"`
|
||||
YinZheng []struct {
|
||||
SourceChapter string `json:"sourceChapter"`
|
||||
Source string `json:"source"`
|
||||
Dynasty string `json:"dynasty"`
|
||||
CiteOriginalText string `json:"citeOriginalText"`
|
||||
Author string `json:"author"`
|
||||
} `json:"yinZheng"`
|
||||
PictureList []any `json:"pictureList"`
|
||||
LessonTerms struct {
|
||||
TermList any `json:"termList"`
|
||||
HasTerms int `json:"hasTerms"`
|
||||
} `json:"lessonTerms"`
|
||||
LessonTermsNew struct {
|
||||
TermList any `json:"termList"`
|
||||
HasTerms int `json:"hasTerms"`
|
||||
} `json:"lessonTermsNew"`
|
||||
Baobian string `json:"baobian"`
|
||||
Structure string `json:"structure"`
|
||||
Pinyin string `json:"pinyin"`
|
||||
Voice string `json:"voice"`
|
||||
ZuowenQuery string `json:"zuowen_query"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func geiAPIdata(s string) (*idiomJSON, error) {
|
||||
url := "https://hanyuapp.baidu.com/dictapp/swan/termdetail?wd=" + url.QueryEscape(s) + "&client=pc&source_tag=2&lesson_from=xiaodu"
|
||||
logrus.Debugln(url)
|
||||
data, err := web.GetData(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiData baiduAPIData
|
||||
err = json.Unmarshal(data, &apiData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiData.Data.Name == "" {
|
||||
return nil, errors.New("未找到该成语")
|
||||
}
|
||||
derivation := ""
|
||||
for _, v := range apiData.Data.ChuChu {
|
||||
if derivation != "" {
|
||||
derivation += "\n"
|
||||
}
|
||||
derivation += v.Dynasty + "·" + v.Author + " " + v.Source + ":" + v.CiteOriginalText
|
||||
}
|
||||
|
||||
explanation := apiData.Data.DefinitionInfo.Definition + apiData.Data.DefinitionInfo.ModernDefinition
|
||||
if derivation == "" && explanation == "" {
|
||||
return nil, errors.New("无法获取成语词源和解释")
|
||||
}
|
||||
synonyms := make([]string, len(apiData.Data.Synonyms))
|
||||
for i, synonym := range apiData.Data.Synonyms {
|
||||
synonyms[i] = synonym.Name
|
||||
}
|
||||
for i, synonym := range apiData.Data.Synonym {
|
||||
if !slices.Contains(synonyms, synonym) {
|
||||
synonyms[i] = synonym
|
||||
}
|
||||
}
|
||||
liju := ""
|
||||
if len(apiData.Data.Liju) > 0 {
|
||||
liju = apiData.Data.Liju[0].Name
|
||||
}
|
||||
|
||||
// 生成字符切片
|
||||
chars := make([]string, 0, len(s))
|
||||
for _, r := range s {
|
||||
chars = append(chars, string(r))
|
||||
}
|
||||
// 分割拼音
|
||||
pinyinSlice := strings.Split(apiData.Data.Pinyin, " ")
|
||||
if len(pinyinSlice) != len(chars) {
|
||||
pinyinSlice = strings.Split(apiData.Data.Definition[0].Pinyin, " ")
|
||||
}
|
||||
|
||||
newIdiom := idiomJSON{
|
||||
Word: apiData.Data.Name,
|
||||
Chars: chars,
|
||||
Pinyin: pinyinSlice,
|
||||
Baobian: apiData.Data.Baobian,
|
||||
Explanation: explanation,
|
||||
Derivation: derivation,
|
||||
Example: liju,
|
||||
Abbreviation: apiData.Data.Structure,
|
||||
Synonyms: synonyms,
|
||||
}
|
||||
return &newIdiom, nil
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func saveIdiomJSON() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
f, err := os.Create(idiomFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewEncoder(f).Encode(&idiomInfoMap)
|
||||
}
|
||||
@@ -1,676 +0,0 @@
|
||||
// Package handou 猜成语
|
||||
package handou
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/FloatTech/imgfactory"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
fcext "github.com/FloatTech/floatbox/ctxext"
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
"github.com/FloatTech/gg"
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
"github.com/FloatTech/zbputils/img/text"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
type idiomJSON struct {
|
||||
Word string `json:"word"` // 成语
|
||||
Chars []string `json:"chars"` // 成语
|
||||
Pinyin []string `json:"pinyin"` // 拼音
|
||||
Baobian string `json:"baobian"` // 褒贬义
|
||||
Explanation string `json:"explanation"` // 解释
|
||||
Derivation string `json:"derivation"` // 词源
|
||||
Example string `json:"example"` // 例句
|
||||
Abbreviation string `json:"abbreviation"` // 结构
|
||||
Synonyms []string `json:"synonyms"` // 近义词
|
||||
}
|
||||
|
||||
const (
|
||||
kong = rune(' ')
|
||||
pinFontSize = 45.0
|
||||
hanFontSize = 150.0
|
||||
)
|
||||
|
||||
const (
|
||||
match = iota
|
||||
exist
|
||||
notexist
|
||||
blockmatch
|
||||
blockexist
|
||||
)
|
||||
|
||||
var colors = [...]color.RGBA{
|
||||
{0, 153, 0, 255},
|
||||
{255, 128, 0, 255},
|
||||
{123, 123, 123, 255},
|
||||
{125, 166, 108, 255},
|
||||
{199, 183, 96, 255},
|
||||
}
|
||||
|
||||
var (
|
||||
en = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "猜成语",
|
||||
Help: "- 个人猜成语\n" +
|
||||
"- 团队猜成语\n",
|
||||
PublicDataFolder: "Handou",
|
||||
}).ApplySingle(ctxext.NewGroupSingle("已经有正在进行的游戏..."))
|
||||
userHabitsFile = file.BOTPATH + "/" + en.DataFolder() + "userHabits.json"
|
||||
idiomFilePath = file.BOTPATH + "/" + en.DataFolder() + "idiom.json"
|
||||
initialized = fcext.DoOnceOnSuccess(
|
||||
func(ctx *zero.Ctx) bool {
|
||||
idiomFile, err := en.GetLazyData("idiom.json", true)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 下载字典时发生错误.\n", err))
|
||||
return false
|
||||
}
|
||||
err = json.Unmarshal(idiomFile, &idiomInfoMap)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 解析字典时发生错误.\n", err))
|
||||
return false
|
||||
}
|
||||
habitsIdiomKeys = make([]string, 0, len(idiomInfoMap))
|
||||
for k := range idiomInfoMap {
|
||||
habitsIdiomKeys = append(habitsIdiomKeys, k)
|
||||
}
|
||||
// 构建用户习惯库(全局高频N-gram)
|
||||
err = initUserHabits()
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 构建用户习惯库时发生错误.\n", err))
|
||||
return false
|
||||
}
|
||||
// 下载字体
|
||||
data, err := file.GetLazyData(text.BoldFontFile, control.Md5File, true)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("ERROR: 加载字体时发生错误.\n", err))
|
||||
return false
|
||||
}
|
||||
pinyinFont = data
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
pinyinFont []byte
|
||||
idiomInfoMap = make(map[string]idiomJSON)
|
||||
habitsIdiomKeys = make([]string, 0)
|
||||
|
||||
errHadGuessed = errors.New("had guessed")
|
||||
errLengthNotEnough = errors.New("length not enough")
|
||||
errUnknownWord = errors.New("unknown word")
|
||||
errTimesRunOut = errors.New("times run out")
|
||||
)
|
||||
|
||||
func init() {
|
||||
en.OnRegex(`^猜成语热门(汉字|成语)$`, zero.OnlyGroup, initialized).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) {
|
||||
if ctx.State["regex_matched"].([]string)[1] == "汉字" {
|
||||
topChars := getTopCharacters(10)
|
||||
ctx.SendChain(message.Text("热门汉字:\n", strings.Join(topChars, "\n")))
|
||||
} else {
|
||||
topIdioms := getTopIdioms(10)
|
||||
ctx.SendChain(message.Text("热门成语:\n", strings.Join(topIdioms, "\n")))
|
||||
}
|
||||
})
|
||||
en.OnRegex(`^(个人|团队)猜成语$`, zero.OnlyGroup, initialized).SetBlock(true).Limit(ctxext.LimitByUser).Handle(func(ctx *zero.Ctx) {
|
||||
target := poolIdiom()
|
||||
idiomData := idiomInfoMap[target]
|
||||
game := newHandouGame(idiomData)
|
||||
_, img, _ := game("")
|
||||
anser := anserOutString(idiomData)
|
||||
worldLength := len(idiomData.Chars)
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(ctx.Event.MessageID,
|
||||
message.ImageBytes(img),
|
||||
message.Text("你有", 7, "次机会猜出", worldLength, "字成语\n首字拼音为:", idiomData.Pinyin[0]),
|
||||
),
|
||||
)
|
||||
var next *zero.FutureEvent
|
||||
if ctx.State["regex_matched"].([]string)[1] == "个人" {
|
||||
next = zero.NewFutureEvent("message", 999, false, zero.RegexRule(fmt.Sprintf(`^([\p{Han},,]){%d}$`, worldLength)),
|
||||
zero.OnlyGroup, ctx.CheckSession())
|
||||
} else {
|
||||
next = zero.NewFutureEvent("message", 999, false, zero.RegexRule(fmt.Sprintf(`^([\p{Han},,]){%d}$`, worldLength)),
|
||||
zero.OnlyGroup, zero.CheckGroup(ctx.Event.GroupID))
|
||||
}
|
||||
var err error
|
||||
var win bool
|
||||
recv, cancel := next.Repeat()
|
||||
defer cancel()
|
||||
tick := time.NewTimer(105 * time.Second)
|
||||
after := time.NewTimer(120 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
ctx.SendChain(message.Text("猜成语,你还有15s作答时间"))
|
||||
case <-after.C:
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(ctx.Event.MessageID,
|
||||
message.Text("猜成语超时,游戏结束...\n答案是: ", anser),
|
||||
),
|
||||
)
|
||||
return
|
||||
case c := <-recv:
|
||||
tick.Reset(105 * time.Second)
|
||||
after.Reset(120 * time.Second)
|
||||
err = updateHabits(c.Event.Message.String())
|
||||
if err != nil {
|
||||
logrus.Warn("更新用户习惯库时发生错误: ", err)
|
||||
}
|
||||
win, img, err = game(c.Event.Message.String())
|
||||
switch {
|
||||
case win:
|
||||
tick.Stop()
|
||||
after.Stop()
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.ImageBytes(img),
|
||||
message.Text("太棒了,你猜出来了!\n答案是: ", anser),
|
||||
),
|
||||
)
|
||||
return
|
||||
case err == errTimesRunOut:
|
||||
tick.Stop()
|
||||
after.Stop()
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.ImageBytes(img),
|
||||
message.Text("游戏结束...\n答案是: ", anser),
|
||||
),
|
||||
)
|
||||
return
|
||||
case err == errLengthNotEnough:
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.Text("成语长度错误"),
|
||||
),
|
||||
)
|
||||
case err == errHadGuessed:
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.Text("该成语已经猜过了"),
|
||||
),
|
||||
)
|
||||
case err == errUnknownWord:
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.Text("你确定存在这样的成语吗?"),
|
||||
),
|
||||
)
|
||||
default:
|
||||
if img != nil {
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.ImageBytes(img),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ctx.Send(
|
||||
message.ReplyWithMessage(c.Event.MessageID,
|
||||
message.Text("回答错误。"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func poolIdiom() string {
|
||||
prioritizedData := prioritizeData(habitsIdiomKeys)
|
||||
if len(prioritizedData) > 0 {
|
||||
return prioritizedData[rand.Intn(len(prioritizedData))]
|
||||
}
|
||||
// 如果没有优先级数据,则随机选择一个成语
|
||||
keys := make([]string, 0, len(idiomInfoMap))
|
||||
for k := range idiomInfoMap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys[rand.Intn(len(keys))]
|
||||
}
|
||||
|
||||
func newHandouGame(target idiomJSON) func(string) (bool, []byte, error) {
|
||||
var (
|
||||
class = len(target.Chars)
|
||||
words = target.Word
|
||||
chars = target.Chars
|
||||
pinyin = target.Pinyin
|
||||
|
||||
tickTruePinyin = make([]string, class)
|
||||
tickExistChars = make([]string, class)
|
||||
tickExistPinyin = make([]string, 0, class)
|
||||
|
||||
record = make([]string, 0, 7)
|
||||
)
|
||||
// 初始化 tick, 第一个是已知的拼音
|
||||
for i := range class {
|
||||
if i == 0 {
|
||||
tickTruePinyin[i] = pinyin[0]
|
||||
} else {
|
||||
tickTruePinyin[i] = ""
|
||||
}
|
||||
tickExistChars[i] = "?"
|
||||
}
|
||||
|
||||
return func(s string) (win bool, data []byte, err error) {
|
||||
answer := []rune(s)
|
||||
var answerData idiomJSON
|
||||
|
||||
if s != "" {
|
||||
if words == s {
|
||||
win = true
|
||||
}
|
||||
|
||||
if len(answer) != len(chars) {
|
||||
err = errLengthNotEnough
|
||||
return
|
||||
}
|
||||
if slices.Contains(record, s) {
|
||||
err = errHadGuessed
|
||||
return
|
||||
}
|
||||
|
||||
answerInfo, ok := idiomInfoMap[s]
|
||||
if !ok {
|
||||
newIdiom, err1 := geiAPIdata(s)
|
||||
if err1 != nil {
|
||||
logrus.Debugln("通过API获取成语信息时发生错误: ", err1)
|
||||
err = errUnknownWord
|
||||
return
|
||||
}
|
||||
logrus.Debugln("通过API获取成语信息: ", newIdiom.Word)
|
||||
if newIdiom.Word != "" {
|
||||
idiomInfoMap[newIdiom.Word] = *newIdiom
|
||||
go func() { _ = saveIdiomJSON() }()
|
||||
}
|
||||
if newIdiom.Word != s {
|
||||
err = errUnknownWord
|
||||
return
|
||||
}
|
||||
answerData = *newIdiom
|
||||
} else {
|
||||
answerData = answerInfo
|
||||
}
|
||||
if len(record) >= 6 || win {
|
||||
// 结束了显示答案
|
||||
tickTruePinyin = target.Pinyin
|
||||
tickExistChars = target.Chars
|
||||
} else {
|
||||
// 处理汉字匹配逻辑
|
||||
for i := range class {
|
||||
char := answerData.Chars[i]
|
||||
if char == chars[i] {
|
||||
tickExistChars[i] = char
|
||||
} else {
|
||||
tickExistChars[i] = "?"
|
||||
}
|
||||
}
|
||||
|
||||
// 确保 tickExistPinyin 有足够的长度
|
||||
if len(tickExistPinyin) < class {
|
||||
for i := len(tickExistPinyin); i < class; i++ {
|
||||
tickExistPinyin = append(tickExistPinyin, "")
|
||||
}
|
||||
}
|
||||
|
||||
// 处理拼音匹配逻辑
|
||||
minPinyinLen := min(len(pinyin), len(answerData.Pinyin))
|
||||
for i := range minPinyinLen {
|
||||
pyChar := pinyin[i]
|
||||
answerPinyinChar := []rune(pyChar)
|
||||
tickTruePinyinChar := make([]rune, len(answerPinyinChar))
|
||||
tickExistPinyinChar := []rune(tickExistPinyin[i])
|
||||
|
||||
if tickTruePinyin[i] != "" {
|
||||
copy(tickTruePinyinChar, []rune(tickTruePinyin[i]))
|
||||
} else {
|
||||
for k := range answerPinyinChar {
|
||||
tickTruePinyinChar[k] = kong
|
||||
}
|
||||
}
|
||||
|
||||
PinyinChar := answerData.Pinyin[i]
|
||||
for j, c := range []rune(PinyinChar) {
|
||||
if c == kong {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case j < len(answerPinyinChar) && c == answerPinyinChar[j]:
|
||||
tickTruePinyinChar[j] = c
|
||||
case slices.Contains(answerPinyinChar, c):
|
||||
// 如果字符存在但位置不对,添加到 tickExistPinyinChar
|
||||
if !slices.Contains(tickExistPinyinChar, c) {
|
||||
tickExistPinyinChar = append(tickExistPinyinChar, c)
|
||||
}
|
||||
default:
|
||||
if j < len(tickTruePinyinChar) {
|
||||
tickTruePinyinChar[j] = kong
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理提示逻辑,将非匹配位置设为下划线
|
||||
matchIndex := -1
|
||||
for j, v := range tickTruePinyinChar {
|
||||
if v != kong && v != '_' {
|
||||
matchIndex = j
|
||||
}
|
||||
}
|
||||
for j := range tickTruePinyinChar {
|
||||
if j > matchIndex {
|
||||
break
|
||||
}
|
||||
if tickTruePinyinChar[j] == kong {
|
||||
tickTruePinyinChar[j] = '_'
|
||||
}
|
||||
}
|
||||
// 更新提示拼音
|
||||
tickTruePinyin[i] = string(tickTruePinyinChar)
|
||||
tickExistPinyin[i] = string(tickExistPinyinChar)
|
||||
}
|
||||
if len(record) >= 2 {
|
||||
tickTruePinyin[0] = pinyin[0]
|
||||
tickExistChars[0] = chars[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 准备绘制数据
|
||||
existPinyin := make([]string, 0, class)
|
||||
for _, v := range tickExistPinyin {
|
||||
if v != "" {
|
||||
v = "?" + v
|
||||
}
|
||||
existPinyin = append(existPinyin, v)
|
||||
}
|
||||
tickIdiom := idiomJSON{
|
||||
Chars: tickExistChars,
|
||||
Pinyin: tickTruePinyin,
|
||||
}
|
||||
|
||||
// 确保所有切片长度一致
|
||||
if len(tickIdiom.Chars) < class {
|
||||
// 如果答案字符数不足,用问号填充
|
||||
for i := len(tickIdiom.Chars); i < class; i++ {
|
||||
tickIdiom.Chars = append(tickIdiom.Chars, "?")
|
||||
}
|
||||
}
|
||||
if len(tickIdiom.Pinyin) < class {
|
||||
// 如果答案拼音数不足,用空字符串填充
|
||||
for i := len(tickIdiom.Pinyin); i < class; i++ {
|
||||
tickIdiom.Pinyin = append(tickIdiom.Pinyin, "")
|
||||
}
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
answerData = tickIdiom
|
||||
}
|
||||
|
||||
var (
|
||||
tickImage image.Image
|
||||
answerImage image.Image
|
||||
imgHistery = make([]image.Image, 0)
|
||||
hisH = 0
|
||||
wg = &sync.WaitGroup{}
|
||||
)
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
tickImage = drawHanBlock(hanFontSize/2, pinFontSize/2, tickIdiom, target, existPinyin...)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
answerImage = drawHanBlock(hanFontSize, pinFontSize, answerData, target)
|
||||
}()
|
||||
if len(record) > 0 {
|
||||
wg.Add(len(record))
|
||||
for i, v := range record {
|
||||
imgHistery = append(imgHistery, nil)
|
||||
go func(i int, v string) {
|
||||
defer wg.Done()
|
||||
idiom, ok := idiomInfoMap[v]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
hisImage := drawHanBlock(hanFontSize/3, pinFontSize/3, idiom, target)
|
||||
imgHistery[i] = hisImage
|
||||
if i == 0 {
|
||||
hisH = hisImage.Bounds().Dy()
|
||||
}
|
||||
}(i, v)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 记录猜过的成语
|
||||
if s != "" && !win {
|
||||
record = append(record, s)
|
||||
}
|
||||
|
||||
if tickImage == nil || answerImage == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tickW, tickH := tickImage.Bounds().Dx(), tickImage.Bounds().Dy()
|
||||
answerW, answerH := answerImage.Bounds().Dx(), answerImage.Bounds().Dy()
|
||||
|
||||
ctx := gg.NewContext(1, 1)
|
||||
_ = ctx.ParseFontFace(pinyinFont, pinFontSize/2)
|
||||
wordH, _ := ctx.MeasureString("M")
|
||||
|
||||
ctxWidth := max(tickW, answerW)
|
||||
ctxHeight := tickH + answerH + int(wordH) + hisH*(len(imgHistery)+1)/2
|
||||
|
||||
ctx = gg.NewContext(ctxWidth, ctxHeight)
|
||||
ctx.SetColor(color.RGBA{255, 255, 255, 255})
|
||||
ctx.Clear()
|
||||
|
||||
ctx.SetColor(color.RGBA{0, 0, 0, 255})
|
||||
_ = ctx.ParseFontFace(pinyinFont, hanFontSize/2)
|
||||
ctx.DrawStringAnchored("题目:", float64(ctxWidth-tickW)/4, float64(tickH)/2, 0.5, 0.5)
|
||||
|
||||
ctx.DrawImageAnchored(tickImage, ctxWidth/2, tickH/2, 0.5, 0.5)
|
||||
ctx.DrawImageAnchored(answerImage, ctxWidth/2, tickH+int(wordH)+answerH/2, 0.5, 0.5)
|
||||
|
||||
k := 0
|
||||
for i, v := range imgHistery {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
x := ctxWidth / 4
|
||||
y := tickH + int(wordH) + answerH + hisH*k
|
||||
|
||||
if i%2 == 1 {
|
||||
x = ctxWidth * 3 / 4
|
||||
y = tickH + int(wordH) + answerH + hisH*k
|
||||
k++
|
||||
}
|
||||
ctx.DrawImageAnchored(v, x, y+hisH/2, 0.5, 0.5)
|
||||
}
|
||||
|
||||
data, err = imgfactory.ToBytes(ctx.Image())
|
||||
if len(record) >= cap(record) {
|
||||
err = errTimesRunOut
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// drawHanBlock 绘制汉字方块,支持多行显示(6字以上时分成两行)
|
||||
func drawHanBlock(hanFontSize, pinFontSize float64, idiom, target idiomJSON, exitPinyin ...string) image.Image {
|
||||
class := len(target.Chars)
|
||||
|
||||
// 确保切片长度一致
|
||||
if len(idiom.Chars) < class {
|
||||
temp := make([]string, class)
|
||||
copy(temp, idiom.Chars)
|
||||
for i := len(idiom.Chars); i < class; i++ {
|
||||
temp[i] = "?"
|
||||
}
|
||||
idiom.Chars = temp
|
||||
}
|
||||
if len(idiom.Pinyin) < class {
|
||||
temp := make([]string, class)
|
||||
copy(temp, idiom.Pinyin)
|
||||
for i := len(idiom.Pinyin); i < class; i++ {
|
||||
temp[i] = ""
|
||||
}
|
||||
idiom.Pinyin = temp
|
||||
}
|
||||
|
||||
chars := idiom.Chars
|
||||
pinyin := idiom.Pinyin
|
||||
|
||||
// 确定行数和每行字数
|
||||
rows := 1
|
||||
charsPerRow := class
|
||||
if class > 6 {
|
||||
rows = 2
|
||||
charsPerRow = (class + 1) / 2
|
||||
}
|
||||
|
||||
ctx := gg.NewContext(1, 1)
|
||||
_ = ctx.ParseFontFace(pinyinFont, pinFontSize)
|
||||
pinWidth, pinHeight := ctx.MeasureString("w")
|
||||
_ = ctx.ParseFontFace(pinyinFont, hanFontSize)
|
||||
hanWidth, hanHeight := ctx.MeasureString("拼")
|
||||
|
||||
space := int(pinHeight / 2)
|
||||
blockPinWidth := int(pinWidth*6) + space
|
||||
boxPadding := math.Min(math.Abs(float64(blockPinWidth)-hanWidth)/2, hanHeight*0.3)
|
||||
|
||||
// 计算总宽度和高度
|
||||
width := space + charsPerRow*blockPinWidth + space
|
||||
height := space + rows*(int(pinHeight+hanHeight+boxPadding*2)+space*2) + space
|
||||
if len(exitPinyin) > 0 {
|
||||
height = space + rows*(int(pinHeight+hanHeight+boxPadding*2+pinHeight)+space*2) + space
|
||||
}
|
||||
|
||||
ctx = gg.NewContext(width, height)
|
||||
ctx.SetColor(color.RGBA{255, 255, 255, 255})
|
||||
ctx.Clear()
|
||||
|
||||
for i := range class {
|
||||
// 边界检查
|
||||
if i >= len(chars) || i >= len(pinyin) || i >= len(target.Pinyin) || i >= len(target.Chars) {
|
||||
break
|
||||
}
|
||||
|
||||
// 计算当前字符在哪一行哪一列
|
||||
idiomRows := 0
|
||||
col := i
|
||||
if rows > 1 {
|
||||
idiomRows = i / charsPerRow
|
||||
col = i % charsPerRow
|
||||
}
|
||||
|
||||
x := float64(space + col*blockPinWidth)
|
||||
// 如果上一层字数是奇数就额外移位
|
||||
if idiomRows%2 == 1 {
|
||||
x += float64(blockPinWidth) / 2
|
||||
}
|
||||
y := float64(idiomRows*(int(pinHeight+hanHeight+boxPadding*2)+space*2) + space)
|
||||
if len(exitPinyin) > 0 {
|
||||
y = float64(idiomRows*(int(pinHeight+hanHeight+boxPadding*2+pinHeight)+space*2) + space)
|
||||
}
|
||||
|
||||
// 绘制拼音
|
||||
_ = ctx.ParseFontFace(pinyinFont, pinFontSize)
|
||||
if i < len(pinyin) {
|
||||
targetPinyinByte := []rune(target.Pinyin[i])
|
||||
pinyinByte := []rune(pinyin[i])
|
||||
|
||||
// 取两者中的最大长度
|
||||
pinTotalWidth := pinWidth * float64(len(pinyinByte))
|
||||
pinX := x + float64(blockPinWidth)/2 - pinTotalWidth/2
|
||||
pinY := y + pinHeight/2
|
||||
|
||||
for k, ch := range pinyinByte {
|
||||
ctx.SetColor(colors[notexist])
|
||||
for m, c := range targetPinyinByte {
|
||||
if k == m && ch == c {
|
||||
ctx.SetColor(colors[match])
|
||||
break
|
||||
} else if ch == c {
|
||||
ctx.SetColor(colors[exist])
|
||||
}
|
||||
}
|
||||
ctx.DrawStringAnchored(string(ch), pinX+pinWidth*float64(k)+pinWidth/2, pinY, 0.5, 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制汉字方框
|
||||
boxX := x + boxPadding
|
||||
boxY := y + pinHeight + float64(space)
|
||||
boxWidth := float64(blockPinWidth) - boxPadding*2
|
||||
boxHeight := float64(hanHeight) + boxPadding*2
|
||||
ctx.DrawRectangle(boxX, boxY, boxWidth, boxHeight)
|
||||
|
||||
// 设置方框颜色
|
||||
char := chars[i]
|
||||
switch {
|
||||
case char == target.Chars[i]:
|
||||
ctx.SetColor(colors[blockmatch])
|
||||
case char != "" && strings.Contains(target.Word, char):
|
||||
ctx.SetColor(colors[blockexist])
|
||||
default:
|
||||
ctx.SetColor(colors[notexist])
|
||||
}
|
||||
ctx.Fill()
|
||||
|
||||
// 绘制汉字
|
||||
_ = ctx.ParseFontFace(pinyinFont, hanFontSize)
|
||||
ctx.SetColor(color.RGBA{255, 255, 255, 255})
|
||||
hanX := boxX + boxWidth/2
|
||||
hanY := boxY + boxHeight/2
|
||||
ctx.DrawStringAnchored(char, hanX, hanY, 0.5, 0.5)
|
||||
|
||||
// 绘制题目的拼音提示
|
||||
ctx.SetColor(colors[exist])
|
||||
_ = ctx.ParseFontFace(pinyinFont, pinFontSize)
|
||||
if len(exitPinyin) > i && exitPinyin[i] != "" {
|
||||
tickY := boxY + boxHeight + float64(space) + pinHeight/2
|
||||
ctx.DrawStringAnchored(exitPinyin[i], hanX, tickY, 0.5, 0.5)
|
||||
}
|
||||
}
|
||||
return ctx.Image()
|
||||
}
|
||||
|
||||
func anserOutString(s idiomJSON) string {
|
||||
msg := s.Word
|
||||
if s.Baobian != "" && s.Baobian != "-" {
|
||||
msg += "\n" + s.Baobian + "词"
|
||||
}
|
||||
if s.Derivation != "" && s.Derivation != "-" {
|
||||
msg += "\n词源:\n" + s.Derivation
|
||||
} else {
|
||||
msg += "\n词源:无"
|
||||
}
|
||||
if s.Explanation != "" && s.Explanation != "-" {
|
||||
msg += "\n解释:\n" + s.Explanation
|
||||
} else {
|
||||
msg += "\n解释:无"
|
||||
}
|
||||
if len(s.Synonyms) > 0 {
|
||||
msg += "\n近义词:\n" + strings.Join(s.Synonyms, ",")
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
// Package handou 猜成语
|
||||
package handou
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/FloatTech/floatbox/file"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// UserHabits 用户习惯
|
||||
type UserHabits struct {
|
||||
mu sync.RWMutex
|
||||
habits map[string]int // 单字频率
|
||||
bigrams map[string]int // 二元组频率
|
||||
idioms map[string]int // 成语出现频率
|
||||
totalWords int // 总字数
|
||||
totalIdioms int // 总成语数
|
||||
lastUpdate time.Time // 最后更新时间
|
||||
}
|
||||
|
||||
var userHabits *UserHabits
|
||||
|
||||
// 初始化用户习惯
|
||||
func initUserHabits() error {
|
||||
userHabits = &UserHabits{
|
||||
habits: make(map[string]int),
|
||||
bigrams: make(map[string]int),
|
||||
idioms: make(map[string]int),
|
||||
}
|
||||
|
||||
if file.IsNotExist(userHabitsFile) {
|
||||
f, err := os.Create(userHabitsFile)
|
||||
if err != nil {
|
||||
return errors.New("创建用户习惯库时发生错误: " + err.Error())
|
||||
}
|
||||
_ = f.Close()
|
||||
return saveHabits()
|
||||
}
|
||||
|
||||
// 读取现有习惯数据
|
||||
habitsFile, err := os.ReadFile(userHabitsFile)
|
||||
if err != nil {
|
||||
return errors.New("读取用户习惯库时发生错误: " + err.Error())
|
||||
}
|
||||
|
||||
var savedData struct {
|
||||
Habits map[string]int `json:"habits"`
|
||||
Bigrams map[string]int `json:"bigrams"`
|
||||
Idioms map[string]int `json:"idioms"`
|
||||
TotalWords int `json:"total_words"`
|
||||
TotalIdioms int `json:"total_idioms"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(habitsFile, &savedData); err != nil {
|
||||
// 如果是旧格式,尝试兼容
|
||||
var oldHabits map[string]int
|
||||
if err := json.Unmarshal(habitsFile, &oldHabits); err == nil {
|
||||
savedData.Habits = oldHabits
|
||||
// 从旧数据重新计算统计信息
|
||||
for _, count := range oldHabits {
|
||||
savedData.TotalWords += count
|
||||
}
|
||||
} else {
|
||||
return errors.New("解析用户习惯库时发生错误: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
userHabits.mu.Lock()
|
||||
defer userHabits.mu.Unlock()
|
||||
|
||||
userHabits.habits = savedData.Habits
|
||||
userHabits.bigrams = savedData.Bigrams
|
||||
userHabits.idioms = savedData.Idioms
|
||||
userHabits.totalWords = savedData.TotalWords
|
||||
userHabits.totalIdioms = savedData.TotalIdioms
|
||||
userHabits.lastUpdate = savedData.LastUpdate
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 保存习惯数据
|
||||
func saveHabits() error {
|
||||
userHabits.mu.RLock()
|
||||
defer userHabits.mu.RUnlock()
|
||||
|
||||
data := struct {
|
||||
Habits map[string]int `json:"habits"`
|
||||
Bigrams map[string]int `json:"bigrams"`
|
||||
Idioms map[string]int `json:"idioms"`
|
||||
TotalWords int `json:"total_words"`
|
||||
TotalIdioms int `json:"total_idioms"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}{
|
||||
Habits: userHabits.habits,
|
||||
Bigrams: userHabits.bigrams,
|
||||
Idioms: userHabits.idioms,
|
||||
TotalWords: userHabits.totalWords,
|
||||
TotalIdioms: userHabits.totalIdioms,
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
|
||||
f, err := os.Create(userHabitsFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(data)
|
||||
}
|
||||
|
||||
// 更新用户习惯(累加频率)
|
||||
func updateHabits(input string) error {
|
||||
if userHabits == nil {
|
||||
if err := initUserHabits(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
userHabits.mu.Lock()
|
||||
defer userHabits.mu.Unlock()
|
||||
|
||||
// 统计单字和二元组
|
||||
chars := []rune(input)
|
||||
userHabits.totalWords += len(chars)
|
||||
|
||||
// 更新单字频率
|
||||
for _, char := range chars {
|
||||
charStr := string(char)
|
||||
userHabits.habits[charStr]++
|
||||
}
|
||||
|
||||
// 仅当成语存在时,更新成语相关频率
|
||||
if slices.Contains(habitsIdiomKeys, input) {
|
||||
// 更新二元组频率(N=2的gram)
|
||||
for i := 0; i < len(chars)-1; i++ {
|
||||
bigram := string(chars[i]) + string(chars[i+1])
|
||||
userHabits.bigrams[bigram]++
|
||||
}
|
||||
// 更新成语频率
|
||||
userHabits.idioms[input]++
|
||||
userHabits.totalIdioms++
|
||||
}
|
||||
|
||||
// 异步保存到文件
|
||||
go func() {
|
||||
if err := saveHabits(); err != nil {
|
||||
logrus.Debugln("保存用户习惯时发生错误: ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算成语的优先级分数
|
||||
func calculatePriorityScore(idiom string) float64 {
|
||||
if userHabits == nil || userHabits.totalWords == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
userHabits.mu.RLock()
|
||||
defer userHabits.mu.RUnlock()
|
||||
|
||||
chars := []rune(idiom)
|
||||
charsLenght := len(chars)
|
||||
|
||||
// 1. 基于单字频率的分数
|
||||
charsScore := 0.0
|
||||
for _, char := range chars {
|
||||
charStr := string(char)
|
||||
if count, exists := userHabits.habits[charStr]; exists {
|
||||
// 使用TF-IDF思想:频率越高,权重越高,但通过总字数归一化
|
||||
tf := float64(count*10) / float64(userHabits.totalWords)
|
||||
// score += tf * 100
|
||||
charsScore += 100 / (1 + 10*math.Abs(tf-5)) // 规避一直是最热门的汉字
|
||||
}
|
||||
}
|
||||
charsScore = charsScore / float64(charsLenght) * 60 / 100
|
||||
|
||||
// 2. 基于二元组频率的分数(词序的重要性)
|
||||
bigramScore := 0.0
|
||||
for i := 0; i < charsLenght-1; i++ {
|
||||
bigram := string(chars[i]) + string(chars[i+1])
|
||||
if count, exists := userHabits.bigrams[bigram]; exists {
|
||||
tf := float64(count*10) / float64(userHabits.totalWords)
|
||||
// score += tf * 150 // 二元组比单字更重要
|
||||
bigramScore += 100 / (1 + 2*math.Abs(tf-5)) // 规避一直是最热门的词组
|
||||
}
|
||||
}
|
||||
bigramScore = bigramScore / float64(charsLenght-1) * 40 / 100
|
||||
|
||||
// 3. 基于成语本身的频率(降低常见成语的优先级,增加多样性)
|
||||
penaltyScore := 0.0
|
||||
if idiomCount, exists := userHabits.idioms[idiom]; exists {
|
||||
// 出现次数越多,优先级越低(避免总是出现相同的成语)
|
||||
penalty := float64(idiomCount) / float64(userHabits.totalIdioms) * 100
|
||||
penaltyScore -= penalty
|
||||
}
|
||||
|
||||
// 4. 考虑成语长度, 让长成语也有机会被选中
|
||||
idiomScore := 0.0
|
||||
if rand.Intn(100) < 60 {
|
||||
idiomScore = 20 / (1 + 1*math.Abs(float64(charsLenght)-4))
|
||||
} else {
|
||||
count := 2.0 + float64(rand.Intn(18))
|
||||
idiomScore = 100 / (1 + 1*math.Abs(float64(charsLenght)-count))
|
||||
}
|
||||
|
||||
finalScore := charsScore + bigramScore + penaltyScore + idiomScore
|
||||
|
||||
return finalScore
|
||||
}
|
||||
|
||||
// 优先抽取数据
|
||||
func prioritizeData(data []string) []string {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
// 计算每个成语的优先级分数
|
||||
idiomScores := make([]struct {
|
||||
idiom string
|
||||
score float64
|
||||
}, len(data))
|
||||
|
||||
for i, idiom := range data {
|
||||
idiomScores[i] = struct {
|
||||
idiom string
|
||||
score float64
|
||||
}{
|
||||
idiom: idiom,
|
||||
score: calculatePriorityScore(idiom),
|
||||
}
|
||||
}
|
||||
|
||||
// 按分数排序(从高到低)
|
||||
slices.SortFunc(idiomScores, func(a, b struct {
|
||||
idiom string
|
||||
score float64
|
||||
}) int {
|
||||
if a.score > b.score {
|
||||
return -1
|
||||
} else if a.score < b.score {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
// 排除的前1/3的数量, 去除分数太高的成语
|
||||
excludeCount := int(float64(len(idiomScores)) * 0.333)
|
||||
if excludeCount < 1 && len(idiomScores) > 1 {
|
||||
excludeCount = 1
|
||||
}
|
||||
startIndex := excludeCount
|
||||
if startIndex >= len(idiomScores) {
|
||||
startIndex = 0
|
||||
}
|
||||
|
||||
// 选择接下来前10个作为优先数据
|
||||
limit := min(len(idiomScores)-startIndex, 10)
|
||||
|
||||
prioritized := make([]string, limit)
|
||||
for i := range limit {
|
||||
prioritized[i] = idiomScores[startIndex+i].idiom
|
||||
logrus.Debugf("成语 '%s' 分数=%.2f",
|
||||
idiomScores[startIndex+i].idiom, idiomScores[startIndex+i].score)
|
||||
}
|
||||
|
||||
return prioritized
|
||||
}
|
||||
|
||||
// 获取热门汉字(用于调试或展示)
|
||||
func getTopCharacters(limit int) []string {
|
||||
if userHabits == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
userHabits.mu.RLock()
|
||||
defer userHabits.mu.RUnlock()
|
||||
|
||||
type charFreq struct {
|
||||
char string
|
||||
count int
|
||||
}
|
||||
|
||||
chars := make([]charFreq, 0, len(userHabits.habits))
|
||||
for char, count := range userHabits.habits {
|
||||
chars = append(chars, charFreq{char, count})
|
||||
}
|
||||
|
||||
slices.SortFunc(chars, func(a, b charFreq) int {
|
||||
return b.count - a.count
|
||||
})
|
||||
|
||||
if len(chars) > limit {
|
||||
chars = chars[:limit]
|
||||
}
|
||||
|
||||
result := make([]string, len(chars))
|
||||
for i, cf := range chars {
|
||||
result[i] = fmt.Sprintf("%s:%d", cf.char, cf.count)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取热门成语(用于调试或展示)
|
||||
func getTopIdioms(limit int) []string {
|
||||
if userHabits == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
userHabits.mu.RLock()
|
||||
defer userHabits.mu.RUnlock()
|
||||
|
||||
type idiomFreq struct {
|
||||
idiom string
|
||||
count int
|
||||
}
|
||||
|
||||
idioms := make([]idiomFreq, 0, len(userHabits.idioms))
|
||||
for char, count := range userHabits.idioms {
|
||||
idioms = append(idioms, idiomFreq{char, count})
|
||||
}
|
||||
|
||||
slices.SortFunc(idioms, func(a, b idiomFreq) int {
|
||||
return b.count - a.count
|
||||
})
|
||||
|
||||
if len(idioms) > limit {
|
||||
idioms = idioms[:limit]
|
||||
}
|
||||
|
||||
result := make([]string, len(idioms))
|
||||
for i, cf := range idioms {
|
||||
result[i] = fmt.Sprintf("%s:%d", cf.idiom, cf.count)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -3,9 +3,8 @@ package minecraftobserver
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func cleanTestData(t *testing.T) {
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
// Package music 整合多平台音乐点播能力
|
||||
// Package music QQ音乐、网易云、酷狗、酷我、咪咕 点歌
|
||||
package music
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/FloatTech/floatbox/web"
|
||||
|
||||
ctrl "github.com/FloatTech/zbpctrl"
|
||||
"github.com/FloatTech/zbputils/control"
|
||||
"github.com/FloatTech/zbputils/ctxext"
|
||||
"github.com/guohuiyuan/music-lib/kugou"
|
||||
"github.com/guohuiyuan/music-lib/kuwo"
|
||||
"github.com/guohuiyuan/music-lib/migu"
|
||||
"github.com/guohuiyuan/music-lib/netease"
|
||||
"github.com/guohuiyuan/music-lib/qq"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
zero "github.com/wdvxdr1123/ZeroBot"
|
||||
"github.com/wdvxdr1123/ZeroBot/message"
|
||||
)
|
||||
|
||||
var platformMap = map[string]func(string) (message.Segment, error){
|
||||
"咪咕": getMiguMusic,
|
||||
"酷我": getKuwoMusic,
|
||||
"酷狗": getKugouMusic,
|
||||
"网易": getNeteaseMusic,
|
||||
"qq": getQQMusic,
|
||||
"": getKuwoMusic, // 默认点歌指向酷我
|
||||
}
|
||||
var (
|
||||
longZhuURL = "https://www.hhlqilongzhu.cn/api/joox/juhe_music.php?msg=%v"
|
||||
)
|
||||
|
||||
func init() {
|
||||
control.AutoRegister(&ctrl.Options[*zero.Ctx]{
|
||||
DisableOnDefault: false,
|
||||
Brief: "点歌",
|
||||
Help: "- 点歌[xxx] (默认酷我)\n" +
|
||||
Help: "- 点歌[xxx]\n" +
|
||||
"- 网易点歌[xxx]\n" +
|
||||
"- 酷我点歌[xxx]\n" +
|
||||
"- 酷狗点歌[xxx]\n" +
|
||||
@@ -38,146 +37,203 @@ func init() {
|
||||
"- qq点歌[xxx]\n",
|
||||
}).OnRegex(`^(.{0,2})点歌\s?(.{1,25})$`).SetBlock(true).Limit(ctxext.LimitByUser).
|
||||
Handle(func(ctx *zero.Ctx) {
|
||||
matches := ctx.State["regex_matched"].([]string)
|
||||
platformPrefix := matches[1]
|
||||
keyword := matches[2]
|
||||
|
||||
processFunc, ok := platformMap[platformPrefix]
|
||||
if !ok {
|
||||
ctx.SendChain(message.Text("不支持的点播平台:", platformPrefix))
|
||||
return
|
||||
// switch 平台
|
||||
switch ctx.State["regex_matched"].([]string)[1] {
|
||||
case "咪咕":
|
||||
ctx.SendChain(migu(ctx.State["regex_matched"].([]string)[2]))
|
||||
case "酷我":
|
||||
ctx.SendChain(kuwo(ctx.State["regex_matched"].([]string)[2]))
|
||||
case "酷狗":
|
||||
ctx.SendChain(kugou(ctx.State["regex_matched"].([]string)[2]))
|
||||
case "网易":
|
||||
ctx.SendChain(cloud163(ctx.State["regex_matched"].([]string)[2]))
|
||||
case "qq":
|
||||
ctx.SendChain(qqmusic(ctx.State["regex_matched"].([]string)[2]))
|
||||
default: // 默认聚合点歌
|
||||
ctx.SendChain(longzhu(ctx.State["regex_matched"].([]string)[2]))
|
||||
}
|
||||
|
||||
seg, err := processFunc(keyword)
|
||||
if err != nil {
|
||||
ctx.SendChain(message.Text("点歌失败:", err))
|
||||
return
|
||||
}
|
||||
ctx.SendChain(seg)
|
||||
})
|
||||
}
|
||||
|
||||
func getMiguMusic(keyword string) (message.Segment, error) {
|
||||
songs, err := migu.Search(keyword)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "咪咕音乐搜索失败")
|
||||
}
|
||||
if len(songs) == 0 {
|
||||
return message.Segment{}, errors.New("咪咕音乐未找到相关歌曲:" + keyword)
|
||||
}
|
||||
song := songs[0]
|
||||
|
||||
playURL, err := migu.GetDownloadURL(&song)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "获取咪咕播放链接失败")
|
||||
}
|
||||
if playURL == "" {
|
||||
return message.Segment{}, errors.New("获取咪咕播放链接失败:链接为空")
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message.CustomMusic(
|
||||
fmt.Sprintf("https://music.migu.cn/v3/music/song/%s", song.ID),
|
||||
playURL,
|
||||
song.Name,
|
||||
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "migu"), nil
|
||||
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, " 的相关结果")
|
||||
}
|
||||
|
||||
func getKuwoMusic(keyword string) (message.Segment, error) {
|
||||
songs, err := kuwo.Search(keyword)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "酷我音乐搜索失败")
|
||||
// migu 返回咪咕音乐卡片
|
||||
func migu(keyword string) message.Segment {
|
||||
headers := http.Header{
|
||||
"Cookie": []string{"audioplayer_exist=1; audioplayer_open=0; migu_cn_cookie_id=3ad476db-f021-4bda-ab91-c485ac3d56a0; Hm_lvt_ec5a5474d9d871cb3d82b846d861979d=1671119573; Hm_lpvt_ec5a5474d9d871cb3d82b846d861979d=1671119573; WT_FPC=id=279ef92eaf314cbb8d01671116477485:lv=1671119583092:ss=1671116477485"},
|
||||
"csrf": []string{"LWKACV45JSQ"},
|
||||
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"},
|
||||
"Referer": []string{"http://m.music.migu.cn"},
|
||||
"proxy": []string{"false"},
|
||||
}
|
||||
if len(songs) == 0 {
|
||||
return message.Segment{}, errors.New("酷我音乐未找到相关歌曲:" + keyword)
|
||||
}
|
||||
song := songs[0]
|
||||
|
||||
playURL, err := kuwo.GetDownloadURL(&song)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "获取酷我播放链接失败")
|
||||
}
|
||||
if playURL == "" {
|
||||
return message.Segment{}, errors.New("获取酷我播放链接失败:链接为空")
|
||||
}
|
||||
|
||||
// 搜索音乐信息 第一首歌
|
||||
search, _ := url.Parse("http://m.music.migu.cn/migu/remoting/scr_search_tag")
|
||||
search.RawQuery = url.Values{
|
||||
"keyword": []string{keyword},
|
||||
"type": []string{"2"},
|
||||
"pgc": []string{"1"},
|
||||
"rows": []string{"10"},
|
||||
}.Encode()
|
||||
info := gjson.ParseBytes(netGet(search.String(), headers)).Get("musics.0")
|
||||
// 返回音乐卡片
|
||||
return message.CustomMusic(
|
||||
fmt.Sprintf("https://www.kuwo.cn/play_detail/%s", song.ID),
|
||||
playURL,
|
||||
song.Name,
|
||||
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "kuwo"), nil
|
||||
fmt.Sprintf("https://music.migu.cn/v3/music/song/%s", info.Get("copyrightId").String()),
|
||||
info.Get("mp3").String(),
|
||||
info.Get("songName").String(),
|
||||
).Add("content", info.Get("artist").Str).Add("image", info.Get("cover").Str).Add("subtype", "migu")
|
||||
}
|
||||
|
||||
func getKugouMusic(keyword string) (message.Segment, error) {
|
||||
songs, err := kugou.Search(keyword)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "酷狗音乐搜索失败")
|
||||
// kuwo 返回酷我音乐卡片
|
||||
func kuwo(keyword string) message.Segment {
|
||||
headers := http.Header{
|
||||
"Cookie": []string{"Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1610284708,1610699237; _ga=GA1.2.1289529848.1591618534; kw_token=LWKACV45JSQ; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1610699468; _gid=GA1.2.1868980507.1610699238; _gat=1"},
|
||||
"csrf": []string{"LWKACV45JSQ"},
|
||||
"User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},
|
||||
"Referer": []string{"https://www.kuwo.cn/search/list?key="},
|
||||
}
|
||||
if len(songs) == 0 {
|
||||
return message.Segment{}, errors.New("酷狗音乐未找到相关歌曲:" + keyword)
|
||||
}
|
||||
song := songs[0]
|
||||
|
||||
playURL, err := kugou.GetDownloadURL(&song)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "获取酷狗播放链接失败")
|
||||
}
|
||||
if playURL == "" {
|
||||
return message.Segment{}, errors.New("获取酷狗播放链接失败:链接为空")
|
||||
}
|
||||
|
||||
// 搜索音乐信息 第一首歌
|
||||
search, _ := url.Parse("https://www.kuwo.cn/api/www/search/searchMusicBykeyWord")
|
||||
search.RawQuery = url.Values{
|
||||
"key": []string{keyword},
|
||||
"pn": []string{"1"},
|
||||
"rn": []string{"1"},
|
||||
"httpsStatus": []string{"1"},
|
||||
}.Encode()
|
||||
info := gjson.ParseBytes(netGet(search.String(), headers)).Get("data.list.0")
|
||||
// 获得音乐直链
|
||||
music, _ := url.Parse("http://www.kuwo.cn/api/v1/www/music/playUrl")
|
||||
music.RawQuery = url.Values{
|
||||
"mid": []string{fmt.Sprintf("%d", info.Get("rid").Int())},
|
||||
"type": []string{"convert_url3"},
|
||||
"br": []string{"320kmp3"},
|
||||
"httpsStatus": []string{"1"},
|
||||
}.Encode()
|
||||
audio := gjson.ParseBytes(netGet(music.String(), headers))
|
||||
// 返回音乐卡片
|
||||
return message.CustomMusic(
|
||||
"https://www.kugou.com/",
|
||||
playURL,
|
||||
song.Name,
|
||||
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "kugou"), nil
|
||||
fmt.Sprintf("https://www.kuwo.cn/play_detail/%d", info.Get("rid").Int()),
|
||||
audio.Get("data.url").Str,
|
||||
info.Get("name").Str,
|
||||
).Add("content", info.Get("artist").Str).Add("image", info.Get("pic").Str).Add("subtype", "kuwo")
|
||||
}
|
||||
|
||||
func getNeteaseMusic(keyword string) (message.Segment, error) {
|
||||
songs, err := netease.Search(keyword)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "网易云音乐搜索失败")
|
||||
// kugou 返回酷狗音乐卡片
|
||||
func kugou(keyword string) message.Segment {
|
||||
stamp := time.Now().UnixNano() / 1e6
|
||||
hash := md5str(
|
||||
fmt.Sprintf(
|
||||
"NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtbitrate=0callback=callback123clienttime=%dclientver=2000dfid=-inputtype=0iscorrection=1isfuzzy=0keyword=%smid=%dpage=1pagesize=30platform=WebFilterprivilege_filter=0srcappid=2919tag=emuserid=-1uuid=%dNVPh5oo715z5DIWAeQlhMDsWXXQV4hwt",
|
||||
stamp, keyword, stamp, stamp,
|
||||
),
|
||||
)
|
||||
// 搜索音乐信息 第一首歌
|
||||
h1 := http.Header{
|
||||
"User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},
|
||||
}
|
||||
if len(songs) == 0 {
|
||||
return message.Segment{}, errors.New("网易云音乐未找到相关歌曲:" + keyword)
|
||||
search, _ := url.Parse("https://complexsearch.kugou.com/v2/search/song")
|
||||
search.RawQuery = url.Values{
|
||||
"callback": []string{"callback123"},
|
||||
"keyword": []string{keyword},
|
||||
"page": []string{"1"},
|
||||
"pagesize": []string{"30"},
|
||||
"bitrate": []string{"0"},
|
||||
"isfuzzy": []string{"0"},
|
||||
"tag": []string{"em"},
|
||||
"inputtype": []string{"0"},
|
||||
"platform": []string{"WebFilter"},
|
||||
"userid": []string{"-1"},
|
||||
"clientver": []string{"2000"},
|
||||
"iscorrection": []string{"1"},
|
||||
"privilege_filter": []string{"0"},
|
||||
"srcappid": []string{"2919"},
|
||||
"clienttime": []string{fmt.Sprintf("%d", stamp)},
|
||||
"mid": []string{fmt.Sprintf("%d", stamp)},
|
||||
"uuid": []string{fmt.Sprintf("%d", stamp)},
|
||||
"dfid": []string{"-"},
|
||||
"signature": []string{hash},
|
||||
}.Encode()
|
||||
res := netGet(search.String(), h1)
|
||||
info := gjson.ParseBytes(res[12 : len(res)-2]).Get("data.lists.0")
|
||||
// 获得音乐直链
|
||||
h2 := http.Header{
|
||||
"Cookie": []string{"kg_mid=d8e70a262c93d47599c6196c612d6f4f; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1610278505,1611631363,1611722252; kg_dfid=33ZWee1kircl0jcJ1h0WF1fX; Hm_lpvt_aedee6983d4cfc62f509129360d6bb3d=1611727348; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e"},
|
||||
"Host": []string{"wwwapi.kugou.com"},
|
||||
"TE": []string{"Trailers"},
|
||||
"User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},
|
||||
}
|
||||
song := songs[0]
|
||||
|
||||
playURL, err := netease.GetDownloadURL(&song)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "获取网易云播放链接失败")
|
||||
}
|
||||
if playURL == "" {
|
||||
return message.Segment{}, errors.New("获取网易云播放链接失败:链接为空")
|
||||
}
|
||||
|
||||
music := "https://wwwapi.kugou.com/yy/index.php?r=play%2Fgetdata&hash=" + info.Get("FileHash").Str + "&album_id=" + info.Get("AlbumID").Str
|
||||
audio := gjson.ParseBytes(netGet(music, h2)).Get("data")
|
||||
// 返回音乐卡片
|
||||
return message.CustomMusic(
|
||||
fmt.Sprintf("https://music.163.com/#/song?id=%s", song.ID),
|
||||
playURL,
|
||||
song.Name,
|
||||
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "163"), nil
|
||||
"https://www.kugou.com/song/#hash="+audio.Get("hash").Str+"&album_id="+audio.Get("album_id").Str,
|
||||
strings.ReplaceAll(audio.Get("play_backup_url").Str, "\\/", "/"),
|
||||
audio.Get("audio_name").Str,
|
||||
).Add("content", audio.Get("author_name").Str).Add("image", audio.Get("img").Str).Add("subtype", "kugou")
|
||||
}
|
||||
|
||||
func getQQMusic(keyword string) (message.Segment, error) {
|
||||
songs, err := qq.Search(keyword)
|
||||
// cloud163 返回网易云音乐卡片
|
||||
func cloud163(keyword string) (msg message.Segment) {
|
||||
requestURL := "http://music.163.com/api/search/get/web?type=1&limit=1&s=" + url.QueryEscape(keyword)
|
||||
data, err := web.GetData(requestURL)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "QQ音乐搜索失败")
|
||||
msg = message.Text("ERROR: ", err)
|
||||
return
|
||||
}
|
||||
if len(songs) == 0 {
|
||||
return message.Segment{}, errors.New("QQ音乐未找到相关歌曲:" + keyword)
|
||||
}
|
||||
song := songs[0]
|
||||
|
||||
playURL, err := qq.GetDownloadURL(&song)
|
||||
if err != nil {
|
||||
return message.Segment{}, errors.Wrap(err, "获取QQ音乐播放链接失败")
|
||||
}
|
||||
if playURL == "" {
|
||||
return message.Segment{}, errors.New("获取QQ音乐播放链接失败:链接为空")
|
||||
}
|
||||
|
||||
return message.CustomMusic(
|
||||
fmt.Sprintf("https://y.qq.com/n/ryqq/songDetail/%s", song.ID),
|
||||
playURL,
|
||||
song.Name,
|
||||
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "qq"), nil
|
||||
msg = message.Music("163", gjson.ParseBytes(data).Get("result.songs.0.id").Int())
|
||||
return
|
||||
}
|
||||
|
||||
// qqmusic 返回QQ音乐卡片
|
||||
func qqmusic(keyword string) (msg message.Segment) {
|
||||
requestURL := "https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?platform=yqq.json&key=" + url.QueryEscape(keyword)
|
||||
data, err := web.RequestDataWith(web.NewDefaultClient(), requestURL, "GET", "", web.RandUA(), nil)
|
||||
if err != nil {
|
||||
msg = message.Text("ERROR: ", err)
|
||||
return
|
||||
}
|
||||
msg = message.Music("qq", gjson.ParseBytes(data).Get("data.song.itemlist.0.id").Int())
|
||||
return
|
||||
}
|
||||
|
||||
// md5str 返回字符串 MD5
|
||||
func md5str(s string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(s))
|
||||
result := strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
return result
|
||||
}
|
||||
|
||||
// netGet 返回请求数据
|
||||
func netGet(url string, header http.Header) []byte {
|
||||
client := &http.Client{}
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
request.Header = header
|
||||
res, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer res.Body.Close()
|
||||
result, _ := io.ReadAll(res.Body)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (repo *RssDomain) syncRss(ctx context.Context) (updated map[int64]*RssClien
|
||||
feed, err = repo.rssHubClient.FetchFeed(channel.RssHubFeedPath)
|
||||
// 如果获取失败,则跳过
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] fetch path(%+v) error: %v", channel.RssHubFeedPath, err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] fetch path(%+v) error: %v", channel.RssHubFeedPath, err)
|
||||
continue
|
||||
}
|
||||
rv := convertFeedToRssView(0, channel.RssHubFeedPath, feed)
|
||||
@@ -42,27 +42,27 @@ func (repo *RssDomain) syncRss(ctx context.Context) (updated map[int64]*RssClien
|
||||
var needUpdate bool
|
||||
needUpdate, err = repo.checkSourceNeedUpdate(ctx, cv.Source)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] checkSourceNeedUpdate error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] checkSourceNeedUpdate error: %v", err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
// 保存
|
||||
logrus.WithContext(ctx).Debugf("[rsshub syncRss] cv %+v, need update(real): %v", cv.Source, needUpdate)
|
||||
logrus.WithContext(ctx).Infof("[rsshub syncRss] cv %+v, need update(real): %v", cv.Source, needUpdate)
|
||||
// 如果需要更新,更新channel 和 content
|
||||
if needUpdate {
|
||||
err = repo.storage.UpsertSource(ctx, cv.Source)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] upsert source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] upsert source error: %v", err)
|
||||
}
|
||||
}
|
||||
var updateChannelView = &RssClientView{Source: cv.Source, Contents: []*RssContent{}}
|
||||
err = repo.processContentsUpdate(ctx, cv, updateChannelView)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] processContentsUpdate error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] processContentsUpdate error: %v", err)
|
||||
continue
|
||||
}
|
||||
if len(updateChannelView.Contents) == 0 {
|
||||
logrus.WithContext(ctx).Debugf("[rsshub syncRss] cv %s, no new content", cv.Source.RssHubFeedPath)
|
||||
logrus.WithContext(ctx).Infof("[rsshub syncRss] cv %s, no new content", cv.Source.RssHubFeedPath)
|
||||
continue
|
||||
}
|
||||
updateChannelView.Sort()
|
||||
@@ -80,7 +80,7 @@ func (repo *RssDomain) checkSourceNeedUpdate(ctx context.Context, source *RssSou
|
||||
return
|
||||
}
|
||||
if sourceInDB == nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] source not found: %v", source.RssHubFeedPath)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] source not found: %v", source.RssHubFeedPath)
|
||||
return
|
||||
}
|
||||
source.ID = sourceInDB.ID
|
||||
@@ -102,13 +102,13 @@ func (repo *RssDomain) processContentsUpdate(ctx context.Context, cv *RssClientV
|
||||
var existed bool
|
||||
existed, err = repo.processContentItemUpdate(ctx, content)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] upsert content error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] upsert content error: %v", err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
if !existed {
|
||||
updateChannelView.Contents = append(updateChannelView.Contents, content)
|
||||
logrus.WithContext(ctx).Debugf("[rsshub syncRss] cv %s, add new content: %v", cv.Source.RssHubFeedPath, content.Title)
|
||||
logrus.WithContext(ctx).Infof("[rsshub syncRss] cv %s, add new content: %v", cv.Source.RssHubFeedPath, content.Title)
|
||||
}
|
||||
}
|
||||
return err
|
||||
@@ -127,7 +127,7 @@ func (repo *RssDomain) processContentItemUpdate(ctx context.Context, content *Rs
|
||||
// 保存
|
||||
err = repo.storage.UpsertContent(ctx, content)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub syncRss] upsert content error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub syncRss] upsert content error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
@@ -36,11 +36,11 @@ func (c *RssHubClient) FetchFeed(path string) (feed *gofeed.Feed, err error) {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Warnf("[rsshub FetchFeed] fetch feed error: %v", err)
|
||||
logrus.Errorf("[rsshub FetchFeed] fetch feed error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
logrus.Warnf("[rsshub FetchFeed] fetch feed error: data is empty")
|
||||
logrus.Errorf("[rsshub FetchFeed] fetch feed error: data is empty")
|
||||
return nil, errors.New("feed data is empty")
|
||||
}
|
||||
feed, err = gofeed.NewParser().Parse(bytes.NewBuffer(data))
|
||||
|
||||
@@ -33,7 +33,7 @@ func newRssDomain(dbPath string) (*RssDomain, error) {
|
||||
}
|
||||
orm, err := gorm.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
logrus.Warnf("[rsshub NewRssDomain] open db error: %v", err)
|
||||
logrus.Errorf("[rsshub NewRssDomain] open db error: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
repo := &RssDomain{
|
||||
@@ -42,7 +42,7 @@ func newRssDomain(dbPath string) (*RssDomain, error) {
|
||||
}
|
||||
err = repo.storage.initDB()
|
||||
if err != nil {
|
||||
logrus.Warnf("[rsshub NewRssDomain] open db error: %v", err)
|
||||
logrus.Errorf("[rsshub NewRssDomain] open db error: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
return repo, nil
|
||||
@@ -54,15 +54,15 @@ func (repo *RssDomain) Subscribe(ctx context.Context, gid int64, feedPath string
|
||||
// 验证
|
||||
feed, err := repo.rssHubClient.FetchFeed(feedPath)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] add source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] add source error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Subscribe] try get source success: %v", len(feed.Title))
|
||||
logrus.WithContext(ctx).Infof("[rsshub Subscribe] try get source success: %v", len(feed.Title))
|
||||
// 新建source结构体
|
||||
rv = convertFeedToRssView(0, feedPath, feed)
|
||||
feedChannel, err := repo.storage.GetSourceByRssHubFeedLink(ctx, feedPath)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] query source by feedPath error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query source by feedPath error: %v", err)
|
||||
return
|
||||
}
|
||||
// 如果已经存在
|
||||
@@ -76,30 +76,30 @@ func (repo *RssDomain) Subscribe(ctx context.Context, gid int64, feedPath string
|
||||
// 保存
|
||||
err = repo.storage.UpsertSource(ctx, rv.Source)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] save source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] save source error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.Debugf("[rsshub Subscribe] save/update source success %v", rv.Source.ID)
|
||||
logrus.Infof("[rsshub Subscribe] save/update source success %v", rv.Source.ID)
|
||||
// 添加群号到订阅
|
||||
subscribe, err := repo.storage.GetSubscribeByID(ctx, gid, rv.Source.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] query subscribe error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query subscribe error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Subscribe] query subscribe success: %v", subscribe)
|
||||
logrus.WithContext(ctx).Infof("[rsshub Subscribe] query subscribe success: %v", subscribe)
|
||||
// 如果已经存在,直接返回
|
||||
if subscribe != nil {
|
||||
isSubExisted = true
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Subscribe] subscribe existed: %v", subscribe)
|
||||
logrus.WithContext(ctx).Infof("[rsshub Subscribe] subscribe existed: %v", subscribe)
|
||||
return
|
||||
}
|
||||
// 如果不存在,保存
|
||||
err = repo.storage.CreateSubscribe(ctx, gid, rv.Source.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] save subscribe error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] save subscribe error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Subscribe] success: %v", len(rv.Contents))
|
||||
logrus.WithContext(ctx).Infof("[rsshub Subscribe] success: %v", len(rv.Contents))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,31 +107,31 @@ func (repo *RssDomain) Subscribe(ctx context.Context, gid int64, feedPath string
|
||||
func (repo *RssDomain) Unsubscribe(ctx context.Context, gid int64, feedPath string) (err error) {
|
||||
existedSubscribes, ifExisted, err := repo.storage.GetIfExistedSubscribe(ctx, gid, feedPath)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] query sub by route error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query sub by route error: %v", err)
|
||||
return errors.New("数据库错误")
|
||||
}
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Subscribe] query source by route success: %v", existedSubscribes)
|
||||
logrus.WithContext(ctx).Infof("[rsshub Subscribe] query source by route success: %v", existedSubscribes)
|
||||
// 如果不存在订阅关系,直接返回
|
||||
if !ifExisted || existedSubscribes == nil {
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Subscribe] source existed: %v", ifExisted)
|
||||
logrus.WithContext(ctx).Infof("[rsshub Subscribe] source existed: %v", ifExisted)
|
||||
return errors.New("频道不存在")
|
||||
}
|
||||
err = repo.storage.DeleteSubscribe(ctx, existedSubscribes.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] delete source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] delete source error: %v", err)
|
||||
return errors.New("删除失败")
|
||||
}
|
||||
// 查询是否还有群订阅这个频道
|
||||
subscribesNeedsToDel, err := repo.storage.GetSubscribesBySource(ctx, feedPath)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] query source by route error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] query source by route error: %v", err)
|
||||
return
|
||||
}
|
||||
// 没有群订阅的时候,把频道删除
|
||||
if len(subscribesNeedsToDel) == 0 {
|
||||
err = repo.storage.DeleteSource(ctx, existedSubscribes.RssSourceID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Subscribe] delete source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Subscribe] delete source error: %v", err)
|
||||
return errors.New("清除频道信息失败")
|
||||
}
|
||||
}
|
||||
@@ -142,11 +142,11 @@ func (repo *RssDomain) Unsubscribe(ctx context.Context, gid int64, feedPath stri
|
||||
func (repo *RssDomain) GetSubscribedChannelsByGroupID(ctx context.Context, gid int64) ([]*RssClientView, error) {
|
||||
channels, err := repo.storage.GetSubscribedChannelsByGroupID(ctx, gid)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub GetSubscribedChannelsByGroupID] GetSubscribedChannelsByGroupID error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub GetSubscribedChannelsByGroupID] GetSubscribedChannelsByGroupID error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
rv := make([]*RssClientView, len(channels))
|
||||
logrus.WithContext(ctx).Debugf("[rsshub GetSubscribedChannelsByGroupID] query subscribe success: %v", len(channels))
|
||||
logrus.WithContext(ctx).Infof("[rsshub GetSubscribedChannelsByGroupID] query subscribe success: %v", len(channels))
|
||||
for i, cn := range channels {
|
||||
rv[i] = &RssClientView{
|
||||
Source: cn,
|
||||
@@ -162,13 +162,13 @@ func (repo *RssDomain) Sync(ctx context.Context) (groupView map[int64][]*RssClie
|
||||
// 获取所有频道
|
||||
updatedViews, err := repo.syncRss(ctx)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Sync] sync rss feed error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Sync] sync rss feed error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Debugf("[rsshub Sync] updated channels: %v", len(updatedViews))
|
||||
logrus.WithContext(ctx).Infof("[rsshub Sync] updated channels: %v", len(updatedViews))
|
||||
subscribes, err := repo.storage.GetSubscribes(ctx)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub Sync] get subscribes error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub Sync] get subscribes error: %v", err)
|
||||
return
|
||||
}
|
||||
for _, subscribe := range subscribes {
|
||||
|
||||
@@ -19,7 +19,7 @@ type repoStorage struct {
|
||||
func (s *repoStorage) initDB() (err error) {
|
||||
err = s.orm.AutoMigrate(&RssSource{}, &RssContent{}, &RssSubscribe{}).Error
|
||||
if err != nil {
|
||||
logrus.Warnf("[rsshub initDB] error: %v", err)
|
||||
logrus.Errorf("[rsshub initDB] error: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -28,7 +28,7 @@ func (s *repoStorage) initDB() (err error) {
|
||||
|
||||
// GetSubscribesBySource Impl
|
||||
func (s *repoStorage) GetSubscribesBySource(ctx context.Context, feedPath string) ([]*RssSubscribe, error) {
|
||||
logrus.WithContext(ctx).Debugf("[rsshub GetSubscribesBySource] feedPath: %s", feedPath)
|
||||
logrus.WithContext(ctx).Infof("[rsshub GetSubscribesBySource] feedPath: %s", feedPath)
|
||||
rs := make([]*RssSubscribe, 0)
|
||||
err := s.orm.Model(&RssSubscribe{}).Joins(fmt.Sprintf("%s left join %s on %s.rss_source_id=%s.id", tableNameRssSubscribe, tableNameRssSource, tableNameRssSubscribe, tableNameRssSource)).
|
||||
Where("rss_source.rss_hub_feed_path = ?", feedPath).Select("rss_subscribe.*").Find(&rs).Error
|
||||
@@ -36,7 +36,7 @@ func (s *repoStorage) GetSubscribesBySource(ctx context.Context, feedPath string
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub GetSubscribesBySource] error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub GetSubscribesBySource] error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return rs, nil
|
||||
@@ -56,7 +56,7 @@ func (s *repoStorage) GetIfExistedSubscribe(ctx context.Context, gid int64, feed
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub GetIfExistedSubscribe] error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub GetIfExistedSubscribe] error: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
if rs.ID == 0 {
|
||||
@@ -76,14 +76,14 @@ func (s *repoStorage) UpsertSource(ctx context.Context, source *RssSource) (err
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = s.orm.Create(source).Omit("id").Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] add source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] add source error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
source.ID = querySource.ID
|
||||
logrus.WithContext(ctx).Debugf("[rsshub] update source: %+v", source.UpdatedParsed)
|
||||
logrus.WithContext(ctx).Infof("[rsshub] update source: %+v", source.UpdatedParsed)
|
||||
err = s.orm.Model(&source).Where(&RssSource{ID: source.ID}).
|
||||
Updates(&RssSource{
|
||||
Title: source.Title,
|
||||
@@ -94,7 +94,7 @@ func (s *repoStorage) UpsertSource(ctx context.Context, source *RssSource) (err
|
||||
Mtime: time.Now(),
|
||||
}).Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] update source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] update source error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.Println("[rsshub] add source success: ", source.ID)
|
||||
@@ -109,10 +109,10 @@ func (s *repoStorage) GetSources(ctx context.Context) (sources []RssSource, err
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("source not found")
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] get sources error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] get sources error: %v", err)
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Debugf("[rsshub] get sources success: %d", len(sources))
|
||||
logrus.WithContext(ctx).Infof("[rsshub] get sources success: %d", len(sources))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (s *repoStorage) GetSourceByRssHubFeedLink(ctx context.Context, rssHubFeedL
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] get source error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] get source error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -134,7 +134,7 @@ func (s *repoStorage) GetSourceByRssHubFeedLink(ctx context.Context, rssHubFeedL
|
||||
func (s *repoStorage) DeleteSource(ctx context.Context, fID int64) (err error) {
|
||||
err = s.orm.Delete(&RssSource{}, "id = ?", fID).Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.DeleteSource: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.DeleteSource: %v", err)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("source not found")
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (s *repoStorage) UpsertContent(ctx context.Context, content *RssContent) (e
|
||||
}
|
||||
err = s.orm.Create(content).Omit("id").Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.UpsertContent: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.UpsertContent: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -171,7 +171,7 @@ func (s *repoStorage) UpsertContent(ctx context.Context, content *RssContent) (e
|
||||
func (s *repoStorage) DeleteSourceContents(ctx context.Context, channelID int64) (rows int64, err error) {
|
||||
err = s.orm.Delete(&RssSubscribe{}).Where(&RssSubscribe{RssSourceID: channelID}).Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.DeleteSourceContents: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.DeleteSourceContents: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -185,7 +185,7 @@ func (s *repoStorage) IsContentHashIDExist(ctx context.Context, hashID string) (
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.IsContentHashIDExist: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.IsContentHashIDExist: %v", err)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
@@ -204,7 +204,7 @@ func (s *repoStorage) CreateSubscribe(ctx context.Context, gid, rssSourceID int6
|
||||
}
|
||||
err = s.orm.Create(&RssSubscribe{GroupID: gid, RssSourceID: rssSourceID}).Omit("id").Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.CreateSubscribe: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.CreateSubscribe: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -214,7 +214,7 @@ func (s *repoStorage) CreateSubscribe(ctx context.Context, gid, rssSourceID int6
|
||||
func (s *repoStorage) DeleteSubscribe(ctx context.Context, subscribeID int64) (err error) {
|
||||
err = s.orm.Delete(&RssSubscribe{}, "id = ?", subscribeID).Error
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.DeleteSubscribe error: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.DeleteSubscribe error: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -228,7 +228,7 @@ func (s *repoStorage) GetSubscribeByID(ctx context.Context, gid int64, subscribe
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.GetSubscribeByID: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.GetSubscribeByID: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
@@ -247,7 +247,7 @@ func (s *repoStorage) GetSubscribedChannelsByGroupID(ctx context.Context, gid in
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.GetSubscribedChannelsByGroupID: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.GetSubscribedChannelsByGroupID: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
@@ -262,7 +262,7 @@ func (s *repoStorage) GetSubscribes(ctx context.Context) (res []*RssSubscribe, e
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
logrus.WithContext(ctx).Warnf("[rsshub] storage.GetSubscribes: %v", err)
|
||||
logrus.WithContext(ctx).Errorf("[rsshub] storage.GetSubscribes: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
mkGoEnv ? pkgs.mkGoEnv,
|
||||
gomod2nix ? pkgs.gomod2nix,
|
||||
}: let
|
||||
goEnv = mkGoEnv {
|
||||
pwd = ./.;
|
||||
go = pkgs.go_1_24;
|
||||
};
|
||||
goEnv = mkGoEnv { pwd = ./.; go = pkgs.go_1_20; };
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"0409": {
|
||||
"identity": {
|
||||
"name": "ZeroBot-Plugin",
|
||||
"version": "1.10.18.2382"
|
||||
"version": "1.10.8.2334"
|
||||
},
|
||||
"description": "",
|
||||
"minimum-os": "vista",
|
||||
@@ -36,23 +36,23 @@
|
||||
"#1": {
|
||||
"0000": {
|
||||
"fixed": {
|
||||
"file_version": "1.10.18.2382",
|
||||
"product_version": "v1.10.18",
|
||||
"timestamp": "2026-02-01T01:08:28+08:00"
|
||||
"file_version": "1.10.8.2334",
|
||||
"product_version": "v1.10.8",
|
||||
"timestamp": "2026-01-05T00:50:29+08:00"
|
||||
},
|
||||
"info": {
|
||||
"0409": {
|
||||
"Comments": "OneBot plugins based on ZeroBot",
|
||||
"CompanyName": "FloatTech",
|
||||
"FileDescription": "https://github.com/FloatTech/ZeroBot-Plugin",
|
||||
"FileVersion": "1.10.18.2382",
|
||||
"FileVersion": "1.10.8.2334",
|
||||
"InternalName": "",
|
||||
"LegalCopyright": "© 2020 - 2026 FloatTech. All Rights Reserved.",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "ZBP.EXE",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "ZeroBot-Plugin",
|
||||
"ProductVersion": "v1.10.18",
|
||||
"ProductVersion": "v1.10.8",
|
||||
"SpecialBuild": ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user