From 8998bfd007752ebd4ce52eb855ed912b5ccbf494 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E5=AE=87=E5=AE=87?= <3061196825@qq.com>
Date: Thu, 18 Jun 2026 14:29:26 +0800
Subject: [PATCH 1/3] =?UTF-8?q?[feat]=E6=9C=80=E5=A5=BD=E7=94=A8=E7=9A=84p?=
=?UTF-8?q?ixiv=E5=9B=BE=E7=89=87=E6=90=9C=E7=B4=A2=E6=8F=92=E4=BB=B6!?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 26 +++
go.mod | 8 +-
go.sum | 40 +++-
main.go | 1 +
plugin/pixiv/api/client.go | 149 ++++++++++++++
plugin/pixiv/api/pixiv_api.go | 212 +++++++++++++++++++
plugin/pixiv/api/token.go | 83 ++++++++
plugin/pixiv/api/utils.go | 157 ++++++++++++++
plugin/pixiv/cache/db.go | 212 +++++++++++++++++++
plugin/pixiv/main.go | 298 +++++++++++++++++++++++++++
plugin/pixiv/model/model.go | 126 ++++++++++++
plugin/pixiv/service.go | 373 ++++++++++++++++++++++++++++++++++
12 files changed, 1680 insertions(+), 5 deletions(-)
create mode 100644 plugin/pixiv/api/client.go
create mode 100644 plugin/pixiv/api/pixiv_api.go
create mode 100644 plugin/pixiv/api/token.go
create mode 100644 plugin/pixiv/api/utils.go
create mode 100644 plugin/pixiv/cache/db.go
create mode 100644 plugin/pixiv/main.go
create mode 100644 plugin/pixiv/model/model.go
create mode 100644 plugin/pixiv/service.go
diff --git a/README.md b/README.md
index 6154dc7b28..124a1411ea 100644
--- a/README.md
+++ b/README.md
@@ -1223,6 +1223,32 @@ print("run[CQ:image,file="+j["img"]+"]")
- [x] 解签
+
+
+ Pixiv图片搜索
+
+ `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv"`
+
+ - [x] [x张]涩图 [关键词]
+
+ - [x] 每日涩图
+
+ - [x] [x张]画师[画师的uid]
+
+ - [x] p站搜图[插画pid]
+
+ - [x] 设置p站token [token] (仅私聊,超级用户)
+
+ - [x] 授权此处使用p站18 (仅超级用户)
+
+ - [x] 设置pixiv代理 [http://127.0.0.1:7890] (仅私聊,超级用户)
+
+ - [x] 查看pixiv代理 (仅私聊,超级用户)
+
+ - [x] 清除pixiv代理 (仅私聊,超级用户)
+
+ - 注:[]不用打出来这只是一个占位符,可添加多个关键词每个关键词用空格隔开,默认不发R-18如果要发就加一个R-18关键词
+
抽扑克
diff --git a/go.mod b/go.mod
index 713a4087a4..54d1b995f6 100644
--- a/go.mod
+++ b/go.mod
@@ -26,7 +26,6 @@ require (
github.com/fumiama/go-onebot-agent v0.0.0-20260425093914-9c92a03776e3
github.com/fumiama/go-registry v0.2.7
github.com/fumiama/gotracemoe v0.0.3
- github.com/fumiama/imgsz v0.0.4
github.com/fumiama/jieba v0.0.0-20221203025406-36c17a10b565
github.com/fumiama/slowdo v0.0.0-20241001074058-27c4fe5259a4
github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6
@@ -51,9 +50,11 @@ require (
golang.org/x/image v0.38.0
golang.org/x/sys v0.41.0
golang.org/x/text v0.35.0
+ gorm.io/datatypes v1.2.7
)
require (
+ filippo.io/edwards25519 v1.1.0 // indirect
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca // indirect
@@ -66,15 +67,18 @@ require (
github.com/fumiama/go-simple-protobuf v0.2.0 // indirect
github.com/fumiama/gofastTEA v0.1.3 // indirect
github.com/fumiama/gozel v0.0.0-20260329105205-a95fde52433a // indirect
+ github.com/fumiama/imgsz v0.0.4 // indirect
github.com/fumiama/orbyte v0.0.0-20251002065953-3bb358367eb5 // indirect
github.com/fumiama/terasu v1.0.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/gopxl/beep/v2 v2.1.1 // indirect
github.com/jfreymuth/oggvorbis v1.0.5 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liuzl/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect
github.com/liuzl/da v0.0.0-20180704015230-14771aad5b1d // indirect
@@ -97,6 +101,8 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/net v0.50.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+ gorm.io/driver/mysql v1.5.6 // indirect
+ gorm.io/gorm v1.30.0 // indirect
modernc.org/libc v1.67.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
diff --git a/go.sum b/go.sum
index 976028c4ae..7634d8e77f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Baidu-AIP/golang-sdk v1.1.1 h1:RQsAmgDSAkiq22I6n7XJ2t3afgzFeqjY46FGhvrx4cw=
github.com/Baidu-AIP/golang-sdk v1.1.1/go.mod h1:bXnGw7xPeKt8aF7UCELKrV6UZ/46spItONK1RQBQj1Y=
github.com/FloatTech/AnimeAPI v1.7.1-0.20260408142737-1e9f2594e3cd h1:1L91nA4W2FNCFlvxAT5UfIvmW40zeH3ho4KkHrlAt0c=
@@ -100,10 +102,15 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -119,6 +126,14 @@ 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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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=
@@ -127,8 +142,9 @@ github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jozsefsallai/gophersauce v1.0.1 h1:BA3ovtQRrAb1qYU9JoRLbDHpxnDunlNcEkEfhCvDDCM=
github.com/jozsefsallai/gophersauce v1.0.1/go.mod h1:YVEI7djliMTmZ1Vh01YPF8bUHi+oKhe3yXgKf1T49vg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -149,8 +165,11 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
+github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
+github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
+github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
@@ -316,6 +335,19 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
+gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
+gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
+gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
+gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
+gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
+gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
+gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
+gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.17.8 h1:yyWBf2ipA0Y9GGz/MmCmi3EFpKgeS7ICrAFes+suEbs=
diff --git a/main.go b/main.go
index 4ee2794a68..d2660f0192 100644
--- a/main.go
+++ b/main.go
@@ -132,6 +132,7 @@ import (
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/nwife" // 本地老婆
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/omikuji" // 浅草寺求签
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/pig" // 来份猪猪
+ _ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv" // pixiv
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/poker" // 抽扑克
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/qqwife" // 一群一天一夫一妻制群老婆
_ "github.com/FloatTech/ZeroBot-Plugin/plugin/qzone" // qq空间表白墙
diff --git a/plugin/pixiv/api/client.go b/plugin/pixiv/api/client.go
new file mode 100644
index 0000000000..c1e4f067c0
--- /dev/null
+++ b/plugin/pixiv/api/client.go
@@ -0,0 +1,149 @@
+package api
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/model"
+)
+
+// HTTPStatusError 图片下载时的 HTTP 状态码
+type HTTPStatusError struct {
+ StatusCode int
+ URL string
+}
+
+func (e *HTTPStatusError) Error() string {
+ return fmt.Sprintf("下载图片失败: HTTP %d", e.StatusCode)
+}
+
+// Client 封装 HTTP 客户端与 Pixiv 请求逻辑
+type Client struct {
+ *http.Client
+ transport *http.Transport
+}
+
+func NewClient() *Client {
+ transport := &http.Transport{
+ TLSClientConfig: &tls.Config{MaxVersion: tls.VersionTLS13},
+ Proxy: http.ProxyFromEnvironment,
+ }
+ return &Client{
+ Client: &http.Client{
+ Transport: transport,
+ Timeout: time.Minute,
+ },
+ transport: transport,
+ }
+}
+
+// SetProxy 设置代理
+func (c *Client) SetProxy(proxyURL string) error {
+ if c == nil || c.transport == nil {
+ return fmt.Errorf("pixiv client is nil")
+ }
+ proxyURL = strings.TrimSpace(proxyURL)
+ if proxyURL == "" {
+ c.transport.Proxy = http.ProxyFromEnvironment
+ return nil
+ }
+ u, err := url.Parse(proxyURL)
+ if err != nil {
+ return err
+ }
+ c.transport.Proxy = http.ProxyURL(u)
+ return nil
+}
+
+func (c *Client) SearchPixivIllustrations(accessToken, url string) (*model.RootEntity, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+accessToken)
+
+ req.Header.Set("User-Agent", "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)")
+ req.Header.Set("App-OS", "android")
+ req.Header.Set("App-OS-Version", "11")
+ req.Header.Set("App-Version", "5.0.234")
+
+ req.Header.Set("Accept-Language", "en_US")
+ req.Header.Set("Referer", "https://app-api.pixiv.net/")
+ req.Header.Set("Connection", "keep-alive")
+
+ req.Host = "app-api.pixiv.net"
+
+ resp, err := c.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("搜索失败: %s\nbody: %s", resp.Status, string(body))
+ }
+
+ var result model.RootEntity
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (c *Client) fetchOnce(targetURL, referer string) ([]byte, int, error) {
+ req, err := http.NewRequest("GET", targetURL, nil)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ req.Header.Set("Referer", referer)
+ req.Header.Set("User-Agent", "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)")
+
+ resp, err := c.Do(req)
+ if err != nil {
+ return nil, 0, fmt.Errorf("请求失败: %w", err)
+ }
+ defer resp.Body.Close()
+
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, resp.StatusCode, err
+ }
+
+ return data, resp.StatusCode, nil
+}
+
+// FetchPixivImage 直接从 Pixiv 下载图片
+func (c *Client) FetchPixivImage(illust model.IllustCache, url string) ([]byte, error) {
+ fmt.Println("下载", illust.PID)
+
+ if c == nil {
+ fmt.Println("FetchPixivImage called on nil IllustCache")
+ return nil, nil
+ }
+
+ data, status, err := c.fetchOnce(url, "https://www.pixiv.net/")
+ if err == nil && status == http.StatusOK {
+ return data, nil
+ }
+ if status == http.StatusNotFound {
+ return nil, &HTTPStatusError{StatusCode: status, URL: url}
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ if status != 0 {
+ return nil, &HTTPStatusError{StatusCode: status, URL: url}
+ }
+
+ return nil, fmt.Errorf("下载图片失败")
+}
diff --git a/plugin/pixiv/api/pixiv_api.go b/plugin/pixiv/api/pixiv_api.go
new file mode 100644
index 0000000000..8fab7bbeb0
--- /dev/null
+++ b/plugin/pixiv/api/pixiv_api.go
@@ -0,0 +1,212 @@
+package api
+
+import (
+ "fmt"
+ "sort"
+
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/model"
+ log "github.com/sirupsen/logrus"
+)
+
+type PixivAPI struct {
+ Client *Client
+ Token *TokenStore
+}
+
+func NewPixivAPI(refreshToken string) *PixivAPI {
+ c := NewClient()
+ return &PixivAPI{
+ Client: c,
+ Token: NewTokenStore(refreshToken, c),
+ }
+}
+
+func (p *PixivAPI) FetchPixivByPID(pid int64) (*model.IllustCache, error) {
+ url := fmt.Sprintf("https://app-api.pixiv.net/v1/illust/detail?illust_id=%d", pid)
+ accessToken, err := p.Token.GetAccessToken()
+ if err != nil {
+ return nil, err
+ }
+ rawData, err := p.Client.SearchPixivIllustrations(accessToken, url)
+ if err != nil {
+ return nil, err
+ }
+ if rawData == nil || rawData.Illust == nil {
+ return nil, fmt.Errorf("pixiv 返回数据为空或结构不匹配")
+ }
+ return convertToIllustCache(rawData.Illust)
+}
+
+func (p *PixivAPI) FetchPixivByUser(uid int64, limit int, pids []int64) ([]model.IllustCache, error) {
+ url := fmt.Sprintf("https://app-api.pixiv.net/v1/user/illusts?user_id=%d&type=illust", uid)
+
+ excludeCache := make(map[int64]struct{})
+
+ for _, pid := range pids {
+ excludeCache[pid] = struct{}{}
+ }
+ return p.fetchPixivCommon(url, limit, nil, excludeCache)
+}
+
+func (p *PixivAPI) FetchPixivRecommend(limit int) ([]model.IllustCache, error) {
+ firstURL := "https://app-api.pixiv.net/v1/illust/recommended?filter=for_ios"
+ return p.fetchPixivCommon(firstURL, limit, nil, nil) // 不做R18过滤,不排缓存
+}
+
+func (p *PixivAPI) FetchPixivIllusts(keyword string, isR18Req bool, limit int, cachedIds []int64) ([]model.IllustCache, error) {
+ cachedMap := make(map[int64]struct{}, len(cachedIds))
+ if len(cachedIds) > 0 {
+ for _, id := range cachedIds {
+ cachedMap[id] = struct{}{}
+ }
+ }
+
+ firstURL := buildPixivSearchURL(keyword)
+ return p.fetchPixivCommon(firstURL, limit, &isR18Req, cachedMap, keyword)
+}
+
+func (p *PixivAPI) GetIllustsByKeyword(keyword string, limit int, cachedIllust []model.IllustCache, cached []int64) ([]model.IllustCache, error) {
+
+ r18Req := IsR18(keyword)
+ keyword = RemoveR18Keywords(keyword)
+
+ // 如果查到了,直接返回
+ if len(cachedIllust) == limit {
+ return cachedIllust, nil
+ }
+
+ // 设置一个保底的关键词
+ if keyword == "" && r18Req {
+ keyword = "R-18"
+ }
+
+ // 计算还需要几张图片
+ needed := 0
+ if len(cachedIllust) < limit {
+ needed = limit - len(cachedIllust)
+ }
+
+ log.Printf("从数据库读到%d,还需要下载%d\n", len(cachedIllust), needed)
+ // 缓存没数据 -> 调用Pixiv API拉取
+ pixivResults, err := p.FetchPixivIllusts(keyword, r18Req, needed, cached)
+ if err != nil && len(cachedIllust) == 0 {
+ return nil, err
+ }
+
+ // 如果Pixiv也没查到直接返回空
+ if len(pixivResults) == 0 && len(cachedIllust) == 0 {
+ return nil, fmt.Errorf("这个关键词可能没有找到符合条件的图片或出现未知错误")
+ }
+
+ if len(cachedIllust) > 0 && len(pixivResults) == 0 {
+ log.Println("http没有找到图片")
+ return cachedIllust, nil
+ }
+
+ pixivResults = append(pixivResults, cachedIllust...)
+
+ if len(pixivResults) >= limit {
+ pixivResults = pixivResults[:limit]
+ }
+
+ log.Println("预计发送", len(pixivResults), "张图片")
+
+ return pixivResults, nil
+}
+
+func (p *PixivAPI) fetchPixivCommon(
+ firstURL string,
+ limit int,
+ isR18Req *bool,
+ excludeCache map[int64]struct{},
+ keywords ...string,
+) ([]model.IllustCache, error) {
+
+ accessToken, err := p.Token.GetAccessToken()
+ if err != nil {
+ return nil, err
+ }
+
+ // 高质量图(≥1000)
+ high := make([]model.IllustCache, 0, limit)
+ // 低质量图(<1000)
+ low := make([]model.IllustCache, 0, limit)
+
+ seen := make(map[int64]struct{})
+ url := firstURL
+
+ for url != "" {
+ rawData, err := p.Client.SearchPixivIllustrations(accessToken, url)
+ if err != nil {
+ return nil, err
+ }
+
+ for i := range rawData.Illusts {
+ raw := &rawData.Illusts[i]
+
+ // 去重
+ if _, ok := seen[raw.Id]; ok {
+ continue
+ }
+ if excludeCache != nil {
+ if _, ok := excludeCache[raw.Id]; ok {
+ continue
+ }
+ }
+ seen[raw.Id] = struct{}{}
+
+ tagNames := extractTagNames(raw.Tags)
+
+ if isR18Req != nil && *isR18Req && !hasR18Tag(tagNames) {
+ continue
+ }
+
+ // 转换
+ ill, err := convertToIllustCache(raw)
+ if err != nil {
+ continue
+ }
+ if len(keywords) > 0 {
+ ill.Keyword = keywords[0]
+ }
+
+ // R18过滤
+ if isR18Req != nil && ill.R18 != *isR18Req {
+ continue
+ }
+
+ // 判断高质量
+ if ill.Bookmarks >= 1000 {
+ high = append(high, *ill)
+ // 高质量够了就直接返回
+ if len(high) >= limit {
+ return high[:limit], nil
+ }
+ } else {
+ // 低质量池还没满 → 接受
+ if len(low) <= limit {
+ low = append(low, *ill)
+ }
+ // 低质量够 limit 就不再放入,避免爆炸增长
+ }
+ }
+
+ // 下一页继续
+ url = rawData.NextUrl
+ }
+
+ // ==== 翻页结束:如果高质量不足,就从低质量中挑收藏最多的 ====
+
+ // 低质量排序(按收藏数倒序)
+ sort.Slice(low, func(i, j int) bool {
+ return low[i].Bookmarks > low[j].Bookmarks
+ })
+
+ // 用低质量中的高质量去补齐不足的图
+ all := append(high, low...)
+ if len(all) > limit {
+ all = all[:limit]
+ }
+
+ return all, nil
+}
diff --git a/plugin/pixiv/api/token.go b/plugin/pixiv/api/token.go
new file mode 100644
index 0000000000..0ad32f966f
--- /dev/null
+++ b/plugin/pixiv/api/token.go
@@ -0,0 +1,83 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+type TokenStore struct {
+ client *Client
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ ExpiresIn int64 `json:"expires_in"`
+ TokenType string `json:"token_type"`
+ ExpiresAt time.Time `json:"-"`
+ mu sync.Mutex
+}
+
+func NewTokenStore(refreshToken string, c *Client) *TokenStore {
+ return &TokenStore{
+ RefreshToken: refreshToken,
+ client: c,
+ }
+}
+
+func (t *TokenStore) GetAccessToken() (string, error) {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if time.Now().Before(t.ExpiresAt) && t.AccessToken != "" {
+ log.Println("access_token is valid")
+ return t.AccessToken, nil
+ }
+
+ if err := t.refreshPixivAccessToken(); err != nil {
+ return "", err
+ }
+
+ t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn/2) * time.Second)
+ return t.AccessToken, nil
+}
+
+// refreshPixivAccessToken 用 refresh_token 刷新 access_token
+func (t *TokenStore) refreshPixivAccessToken() error {
+ endpoint := "https://oauth.secure.pixiv.net/auth/token"
+
+ data := url.Values{}
+ data.Set("grant_type", "refresh_token")
+ data.Set("client_id", "MOBrBDS8blbauoSck0ZfDbtuzpyT")
+ data.Set("client_secret", "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj")
+ data.Set("refresh_token", t.RefreshToken)
+
+ req, _ := http.NewRequest("POST", endpoint, bytes.NewBufferString(data.Encode()))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("User-Agent", "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)")
+
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode != 200 {
+ return fmt.Errorf("刷新失败: %s\nbody: %s", resp.Status, string(body))
+ }
+
+ var tokenRes TokenStore
+ err = json.Unmarshal(body, &tokenRes)
+
+ t.AccessToken = tokenRes.AccessToken
+ t.ExpiresIn = tokenRes.ExpiresIn
+
+ return err
+}
diff --git a/plugin/pixiv/api/utils.go b/plugin/pixiv/api/utils.go
new file mode 100644
index 0000000000..fc150c3c15
--- /dev/null
+++ b/plugin/pixiv/api/utils.go
@@ -0,0 +1,157 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/model"
+)
+
+func RemoveR18Keywords(keyword string) string {
+ if keyword == "" {
+ return keyword
+ }
+
+ words := strings.Fields(keyword) // 按空格分割成单词
+ var result []string
+
+ for _, word := range words {
+ lowerWord := strings.ToLower(word)
+ // 只删除完全匹配的R-18 关键词
+ if lowerWord != "r-18" && lowerWord != "r18" && lowerWord != "r_18" {
+ result = append(result, word)
+ }
+ }
+
+ return strings.Join(result, " ")
+}
+
+func buildPixivSearchURL(keyword string) string {
+
+ baseURL := &url.URL{
+ Scheme: "https",
+ Host: "app-api.pixiv.net",
+ Path: "/v1/search/illust",
+ }
+
+ params := url.Values{}
+ params.Set("word", keyword)
+ // 严格匹配 exact_match_for_tags
+ // 标题简介有相同的 title_and_caption
+ // 宽松匹配 partial_match_for_tags
+ // 暂时使用宽松匹配
+ params.Set("search_target", "partial_match_for_tags")
+ params.Set("sort", "popular_desc")
+ //params.Set("offset", fmt.Sprintf("%d", offset))
+ params.Set("order", "date_desc")
+ params.Set("filter", "for_android")
+ // params.Set("filter", "for_ios")
+ // params.Set("bookmark_num_min", "1000")
+
+ baseURL.RawQuery = params.Encode()
+ return baseURL.String()
+}
+
+func hasR18Tag(tags []string) bool {
+ for _, tag := range tags {
+ if IsR18(tag) {
+ return true
+ }
+ }
+ return false
+}
+
+func extractTagNames(tags []model.TagsEntity) []string {
+ tagNames := make([]string, 0, len(tags))
+ for _, tag := range tags {
+ tagNames = append(tagNames, tag.Name)
+ }
+ return tagNames
+}
+
+func IsR18(s string) bool {
+ if s == "" {
+ return false
+ }
+ lower := strings.ToLower(s)
+ r18Keywords := []string{"r-18", "r18", "r_18"}
+ for _, keyword := range r18Keywords {
+ if strings.Contains(lower, keyword) {
+ return true
+ }
+ }
+ return false
+}
+
+func ModifyPageGeneric(originalURL string, pageNum int) string {
+ u, err := url.Parse(originalURL)
+ if err != nil {
+ return originalURL
+ }
+
+ // 获取路径的最后一部分(文件名)
+ pathParts := strings.Split(u.Path, "/")
+ if len(pathParts) == 0 {
+ return originalURL
+ }
+
+ fileName := pathParts[len(pathParts)-1]
+ parts := strings.Split(fileName, "_")
+ if len(parts) < 2 {
+ return originalURL
+ }
+
+ // 分离页码和扩展名
+ pageAndExt := strings.Split(parts[1], ".")
+ if len(pageAndExt) < 2 {
+ return originalURL
+ }
+
+ // 修改页码部分,保留扩展名
+ parts[1] = fmt.Sprintf("p%d.%s", pageNum, pageAndExt[1])
+
+ // 更新文件名
+ pathParts[len(pathParts)-1] = strings.Join(parts, "_")
+ u.Path = strings.Join(pathParts, "/")
+
+ return u.String()
+}
+
+func convertToIllustCache(raw *model.IllustsEntity) (*model.IllustCache, error) {
+ tagNames := extractTagNames(raw.Tags)
+
+ jsonTags, err := json.Marshal(tagNames)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(tagNames) == 0 {
+ tagNames = []string{""}
+ }
+
+ illust := &model.IllustCache{
+ PID: raw.Id,
+ UID: raw.User.Id,
+ Keyword: tagNames[0], // 默认为第1个标签后续在其他函数里自定义
+ Title: raw.Title,
+ AuthorName: raw.User.Name,
+ ImageURL: raw.ImageUrls.Large,
+ R18: hasR18Tag(tagNames),
+ Bookmarks: raw.TotalBookmarks,
+ TotalView: raw.TotalView,
+ CreateDate: raw.CreateDate,
+ PageCount: raw.PageCount,
+ Tags: jsonTags,
+ }
+
+ originalImageURL := raw.MetaSinglePage.OriginalImageUrl
+ if originalImageURL == "" && len(raw.MetaPages) > 0 {
+ originalImageURL = raw.MetaPages[0].ImageURLs.Original
+ }
+
+ illust.OriginalURL = originalImageURL
+
+ return illust, nil
+}
diff --git a/plugin/pixiv/cache/db.go b/plugin/pixiv/cache/db.go
new file mode 100644
index 0000000000..d1fbc0e34a
--- /dev/null
+++ b/plugin/pixiv/cache/db.go
@@ -0,0 +1,212 @@
+package cache
+
+import (
+ "errors"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/model"
+ "github.com/jinzhu/gorm"
+)
+
+type DB struct {
+ *gorm.DB
+ mu sync.Mutex
+}
+
+func NewDB(path string) *DB {
+ db, err := gorm.Open("sqlite3", path)
+ if err != nil {
+ panic(err)
+ }
+ if err = db.AutoMigrate(&model.IllustCache{}, &model.SentImage{}, &model.RefreshToken{}, &model.GroupR18Permission{}, &model.PixivProxyConfig{}).Error; err != nil {
+ panic(err)
+ }
+ sqlDB := db.DB()
+ sqlDB.SetMaxOpenConns(10)
+ sqlDB.SetMaxIdleConns(5)
+ sqlDB.SetConnMaxLifetime(time.Hour)
+ db.LogMode(false)
+ return &DB{db, sync.Mutex{}}
+}
+
+func (db *DB) CheckGroupR18Permission(gid int64) bool {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ var count int64
+ db.Model(&model.GroupR18Permission{}).Where("group_id = ?", gid).Count(&count)
+ return count > 0
+}
+
+func (db *DB) findByKeyword(gid int64, keyword string, limit int, r18Req bool) ([]model.IllustCache, error) {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ var results []model.IllustCache
+ query := db.Model(&model.IllustCache{}).
+ Where("keyword = ?", keyword).
+ Where("pid NOT IN (?)", db.Model(&model.SentImage{}).Where("group_id = ?", gid).Select("pid").SubQuery()).
+ Order("bookmarks DESC").
+ Limit(limit)
+
+ query = query.Where("r18 = ?", r18Req)
+
+ err := query.Find(&results).Error
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+ return results, nil
+}
+
+func (db *DB) findByTag(gid int64, tag string, needed int, r18Req bool) ([]model.IllustCache, error) {
+ if needed <= 0 {
+ return nil, nil
+ }
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ var results []model.IllustCache
+ escapedTag := strings.ReplaceAll(tag, "\"", "\\\"")
+ likePattern := "%\"" + escapedTag + "\"%"
+
+ query := db.Model(&model.IllustCache{}).
+ Where("tags LIKE ?", likePattern).
+ Where("pid NOT IN (?)", db.Model(&model.SentImage{}).Where("group_id = ?", gid).Select("pid").SubQuery()).
+ Order("bookmarks DESC").
+ Limit(needed)
+
+ if !r18Req {
+ query = query.Where("r18 = ?", false)
+ }
+
+ err := query.Find(&results).Error
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+
+ return results, nil
+}
+func (db *DB) CountIllustsSmart(gid int64, keyword string, r18Req bool) (int64, error) {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ var count int64
+
+ query := db.Model(&model.IllustCache{}).
+ Where("keyword = ?", keyword).
+ Where("pid NOT IN (?)", db.Model(&model.SentImage{}).Where("group_id = ?", gid).Select("pid").SubQuery())
+
+ if !r18Req {
+ query = query.Where("r18 = ?", false)
+ }
+ err := query.Count(&count).Error
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return 0, err
+ }
+ return count, nil
+}
+
+func (db *DB) FindIllustsSmart(gid int64, keyword string, limit int, r18Req bool) ([]model.IllustCache, error) {
+ seen := make(map[int64]struct{})
+ var results []model.IllustCache
+
+ // 1. keyword 严格查询
+ kwRes, err := db.findByKeyword(gid, keyword, limit, r18Req)
+ if err != nil {
+ return nil, err
+ }
+ for _, ill := range kwRes {
+ results = append(results, ill)
+ seen[ill.PID] = struct{}{}
+ }
+
+ if len(results) >= limit {
+ return results[:limit], nil
+ }
+
+ // 2. tag 查询补齐
+ need := limit - len(results)
+ for _, tagKeyword := range strings.Fields(keyword) {
+ tagRes, err := db.findByTag(gid, tagKeyword, need, r18Req)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, ill := range tagRes {
+ if _, ok := seen[ill.PID]; ok {
+ continue
+ }
+ results = append(results, ill)
+ seen[ill.PID] = struct{}{}
+ if len(results) >= limit {
+ break
+ }
+ }
+
+ if len(results) >= limit {
+ break
+ }
+
+ need = limit - len(results)
+ }
+
+ return results, nil
+}
+
+func (db *DB) GetIllustIDsByKeyword(keyword string) ([]int64, error) {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ var illustIDs []int64
+ err := db.Model(&model.IllustCache{}).Where("keyword = ?", keyword).Pluck("pid", &illustIDs).Error
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+ return illustIDs, nil
+}
+
+func (db *DB) GetSentPictureIDs(gid int64) ([]int64, error) {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ var pictureIDs []int64
+ err := db.Model(&model.SentImage{}).Where("group_id = ?", gid).Pluck("pid", &pictureIDs).Error
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, err
+ }
+ return pictureIDs, nil
+}
+
+// DeleteIllustByPID 删除指定 PID 的插画缓存记录
+func (db *DB) DeleteIllustByPID(pid int64) error {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ return db.Where("pid = ?", pid).Delete(&model.IllustCache{}).Error
+}
+
+// Create wraps gorm.DB.Create with a mutex to avoid SQLITE_BUSY when requests are concurrent.
+func (db *DB) Create(value interface{}) *gorm.DB {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ return db.DB.Create(value)
+}
+
+// Save wraps gorm.DB.Save with a mutex to avoid SQLITE_BUSY when requests are concurrent.
+func (db *DB) Save(value interface{}) *gorm.DB {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ return db.DB.Save(value)
+}
+
+// Delete wraps gorm.DB.Delete with a mutex to avoid SQLITE_BUSY when requests are concurrent.
+func (db *DB) Delete(value interface{}, where ...interface{}) *gorm.DB {
+ db.mu.Lock()
+ defer db.mu.Unlock()
+
+ return db.DB.Delete(value, where...)
+}
diff --git a/plugin/pixiv/main.go b/plugin/pixiv/main.go
new file mode 100644
index 0000000000..cedfa07a89
--- /dev/null
+++ b/plugin/pixiv/main.go
@@ -0,0 +1,298 @@
+package pixiv
+
+import (
+ "math/rand"
+ "os"
+ "strconv"
+
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/api"
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/cache"
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/model"
+ "github.com/FloatTech/floatbox/file"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/FloatTech/zbputils/control"
+ log "github.com/sirupsen/logrus"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+)
+
+var defaultKeyword = []string{"萝莉", "御姐", "妹妹", "姐姐", "车万"}
+
+var (
+ service *Service
+)
+
+func init() {
+ if file.IsNotExist("data/pixiv") {
+ err := os.MkdirAll("data/pixiv", 0775)
+ if err != nil {
+ panic(err)
+ }
+ }
+ if file.IsNotExist(pixivTempDir) {
+ err := os.MkdirAll(pixivTempDir, 0775)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ db := cache.NewDB("data/pixiv/pixiv.db")
+
+ var t1 model.RefreshToken
+ if err := db.First(&t1).Error; err != nil {
+ log.Warning("Fail fetching token store from database")
+ }
+
+ pixivAPI := api.NewPixivAPI(t1.Token)
+
+ var proxyCfg model.PixivProxyConfig
+ proxyCfg.Name = "global"
+ if err := db.Where("name = ?", proxyCfg.Name).FirstOrCreate(&proxyCfg).Error; err == nil {
+ if err := pixivAPI.Client.SetProxy(proxyCfg.Proxy); err != nil {
+ log.Warning("设置pixiv代理错误: ", err)
+ }
+ }
+
+ service = NewService(db, pixivAPI)
+}
+
+const help = `
+- [x张]涩图 [关键词]
+- 每日涩图
+- [x张]画师[画师的uid]
+- p站搜图[插画pid]
+- 设置p站token [token]
+- 授权此处使用p站18
+- 设置pixiv代理 [http://127.0.0.1:7890]
+- 查看pixiv代理
+- 清除pixiv代理
+[]不用打出来这只是一个占位符
+可添加多个关键词每个关键词用空格隔开
+默认不发R-18如果要发就加一个R-18关键词
+`
+
+func init() {
+ engine := control.AutoRegister(&ctrl.Options[*zero.Ctx]{
+ DisableOnDefault: false,
+ Brief: "Pixiv 图片搜索",
+ Help: help,
+ })
+
+ engine.OnFullMatch("授权此处使用p站18", zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ id := ctx.Event.GroupID
+ if id == 0 {
+ id = -ctx.Event.UserID
+ }
+ if err := service.DB.Create(&model.GroupR18Permission{GroupID: id}).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ ctx.SendChain(message.Text("已允许"))
+ })
+
+ engine.OnRegex(`^设置p站token\s+(.*)`, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ token := ctx.State["regex_matched"].([]string)[1]
+ var refreshToken model.RefreshToken
+ refreshToken.User = ctx.Event.UserID
+ refreshToken.Token = token
+ if err := service.DB.Save(&refreshToken).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ service.API.Token.RefreshToken = token
+
+ ctx.SendChain(message.Text("Pixiv Token: ", token))
+ })
+
+ engine.OnRegex(`^设置pixiv代理\s+([\s\S]+)$`, zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ proxy := ctx.State["regex_matched"].([]string)[1]
+
+ var proxyCfg model.PixivProxyConfig
+ proxyCfg.Name = "global"
+ if err := service.DB.Where("name = ?", proxyCfg.Name).FirstOrCreate(&proxyCfg).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ proxyCfg.Proxy = proxy
+ if err := service.DB.Save(&proxyCfg).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ if err := service.API.Client.SetProxy(proxy); err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+
+ ctx.SendChain(message.Text("Pixiv client proxy 已设置为: ", proxy))
+ })
+
+ engine.OnFullMatch("清除pixiv代理", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ var proxyCfg model.PixivProxyConfig
+ proxyCfg.Name = "global"
+ if err := service.DB.Where("name = ?", proxyCfg.Name).FirstOrCreate(&proxyCfg).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ proxyCfg.Proxy = ""
+ if err := service.DB.Save(&proxyCfg).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ if err := service.API.Client.SetProxy(""); err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ ctx.SendChain(message.Text("Pixiv client proxy 已清除"))
+ })
+
+ engine.OnFullMatch("查看pixiv代理", zero.OnlyPrivate, zero.SuperUserPermission).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ var proxyCfg model.PixivProxyConfig
+ proxyCfg.Name = "global"
+ if err := service.DB.Where("name = ?", proxyCfg.Name).FirstOrCreate(&proxyCfg).Error; err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ if proxyCfg.Proxy == "" {
+ ctx.SendChain(message.Text("Pixiv client proxy: 未设置"))
+ return
+ }
+ ctx.SendChain(message.Text("Pixiv client proxy: ", proxyCfg.Proxy))
+ })
+
+ engine.OnRegex(`^p站搜图(\d+)`).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ rawPID := ctx.State["regex_matched"].([]string)[1]
+ pid, err := strconv.ParseInt(rawPID, 10, 64)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ illust, err := service.API.FetchPixivByPID(pid)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ _ = service.DB.Create(illust)
+ service.SendIllusts(ctx, []model.IllustCache{*illust})
+ })
+
+ engine.OnRegex(`^(\d+)?张?画师(\d+)`).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ if !service.Acquire(ctx.Event.UserID) {
+ ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("上一个任务还没结束,请稍后再试"))
+ return
+ }
+ defer service.Release(ctx.Event.UserID)
+
+ limit := ctx.State["regex_matched"].([]string)[1]
+ if limit == "" {
+ limit = "1"
+ }
+ rawUid := ctx.State["regex_matched"].([]string)[2]
+ limitInt, err := strconv.Atoi(limit)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ uid, err := strconv.ParseInt(rawUid, 10, 64)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ pictureIDs, err := service.DB.GetSentPictureIDs(ctx.Event.GroupID)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ illustInfos, err := service.API.FetchPixivByUser(uid, limitInt, pictureIDs)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+ if len(illustInfos) == 0 {
+ ctx.SendChain(message.Text("没有找到图片"))
+ return
+ }
+
+ service.SendIllusts(ctx, illustInfos)
+ })
+
+ engine.OnRegex(`^每日[色|涩|瑟]图$`).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ illusts, err := service.API.FetchPixivRecommend(1)
+ if err != nil {
+ ctx.SendChain(message.Text("发送涩图失败惹"))
+ return
+ }
+ service.SendIllusts(ctx, []model.IllustCache{illusts[0]})
+ })
+
+ engine.OnRegex(`^(\d+)?张?[色|瑟|涩]图\s*(.+)?`).SetBlock(true).Handle(func(ctx *zero.Ctx) {
+ limit := ctx.State["regex_matched"].([]string)[1]
+ keyword := ctx.State["regex_matched"].([]string)[2]
+
+ if !service.Acquire(ctx.Event.UserID) {
+ ctx.SendChain(message.Reply(ctx.Event.MessageID), message.Text("上一个任务还没结束,请稍后再试"))
+ return
+ }
+ defer service.Release(ctx.Event.UserID)
+
+ if limit == "" {
+ limit = "1"
+ }
+
+ if keyword == "" {
+ keyword = defaultKeyword[rand.Intn(len(defaultKeyword))]
+ }
+
+ limitInt, err := strconv.Atoi(limit)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+
+ if limitInt > 10 {
+ ctx.SendChain(message.Text("图片太多了"))
+ return
+ }
+
+ gid := ctx.Event.GroupID
+ r18Req := api.IsR18(keyword)
+ cleanKeyword := api.RemoveR18Keywords(keyword)
+
+ if gid == 0 {
+ gid = -ctx.Event.UserID
+ }
+
+ if r18Req && !service.DB.CheckGroupR18Permission(gid) {
+ ctx.SendChain(message.Text([]string{
+ "笨蛋笨蛋大笨蛋",
+ "此处未授权r18",
+ }[rand.Intn(2)]))
+ return
+ }
+
+ cachedIllusts, err := service.DB.FindIllustsSmart(gid, cleanKeyword, limitInt, r18Req)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+
+ cached, _ := service.DB.GetSentPictureIDs(gid)
+
+ for _, ill := range cachedIllusts {
+ cached = append(cached, ill.PID)
+ }
+
+ illusts, err := service.API.GetIllustsByKeyword(keyword, limitInt, cachedIllusts, cached)
+ if err != nil {
+ ctx.SendChain(message.Text("ERROR: ", err))
+ return
+ }
+
+ for _, illust := range illusts {
+ log.Print("获取到图片 PID: ", illust.PID)
+ _ = service.DB.Create(&illust).Error
+ }
+
+ service.SendIllusts(ctx, illusts)
+ service.BackgroundCacheFiller(cleanKeyword, 10, r18Req, 5, ctx.Event.GroupID)
+ })
+}
diff --git a/plugin/pixiv/model/model.go b/plugin/pixiv/model/model.go
new file mode 100644
index 0000000000..5f8417060d
--- /dev/null
+++ b/plugin/pixiv/model/model.go
@@ -0,0 +1,126 @@
+package model
+
+import (
+ "github.com/jinzhu/gorm"
+ "gorm.io/datatypes"
+)
+
+// IllustCache 插画缓存表
+type IllustCache struct {
+ gorm.Model
+
+ PID int64 `gorm:"unique_index:idx_keyword_pid;not null;column:pid"` // Pixiv 作品 ID
+ UID int64 `gorm:"default:0;not null;column:uid"` // 插画作者的id
+ Keyword string `gorm:"unique_index:idx_keyword_pid;type:varchar(255)"` // 搜索关键词
+ Title string `gorm:"type:varchar(255)"` // 标题
+ AuthorName string `gorm:"type:varchar(255)"` // 用户名
+ ImageURL string `gorm:"type:varchar(500)"` // 大图地址
+ OriginalURL string `gorm:"type:varchar(500)"` // 原图地址
+ R18 bool `gorm:"not null;default:false"` // 是否为 R-18 作品
+ Bookmarks int64 // 收藏数
+ TotalView int64 // 总浏览数
+ CreateDate string // 创建日期
+ PageCount int64 `gorm:"default:1"` // 页数
+ Tags datatypes.JSON `gorm:"type:json"` // 插画的所有标签 方便后续查找
+}
+
+// SentImage 已发送记录表
+type SentImage struct {
+ gorm.Model
+
+ GroupID int64 `gorm:"index:idx_group_pid;not null"` // 群组 ID
+ PID int64 `gorm:"index:idx_group_pid;not null;column:pid"` // 插画 PID
+}
+
+// GroupR18Permission 群组R18权限表
+type GroupR18Permission struct {
+ gorm.Model
+ GroupID int64 `gorm:"unique_index"`
+}
+
+type RefreshToken struct {
+ gorm.Model
+
+ User int64 `gorm:"unique"`
+
+ Token string
+}
+
+// PixivProxyConfig 保存 Pixiv API 客户端的代理配置
+type PixivProxyConfig struct {
+ gorm.Model
+
+ Name string `gorm:"unique_index"`
+ Proxy string
+}
+
+type RootEntity struct {
+ Illusts []IllustsEntity `json:"illusts"`
+ Illust *IllustsEntity `json:"illust"`
+ NextUrl string `json:"next_url"`
+ SearchSpanLimit int64 `json:"search_span_limit"`
+ ShowAi bool `json:"show_ai"`
+}
+
+type IllustsEntity struct {
+ Id int64 `json:"id"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ ImageUrls ImageUrlsEntity `json:"image_urls"`
+ Caption string `json:"caption"`
+ Restrict int64 `json:"restrict"`
+ User UserEntity `json:"user"`
+ Tags []TagsEntity `json:"tags"`
+ CreateDate string `json:"create_date"`
+ PageCount int64 `json:"page_count"`
+ Width int64 `json:"width"`
+ Height int64 `json:"height"`
+ SanityLevel int64 `json:"sanity_level"`
+ XRestrict int64 `json:"x_restrict"`
+ MetaSinglePage MetaSinglePageEntity `json:"meta_single_page"`
+ MetaPages []MetaPage `json:"meta_pages"`
+ TotalView int64 `json:"total_view"`
+ TotalBookmarks int64 `json:"total_bookmarks"`
+ IsBookmarked bool `json:"is_bookmarked"`
+ Visible bool `json:"visible"`
+ IsMuted bool `json:"is_muted"`
+ IllustAiType int64 `json:"illust_ai_type"`
+ IllustBookStyle int64 `json:"illust_book_style"`
+}
+
+type MetaPage struct {
+ ImageURLs struct {
+ SquareMedium string `json:"square_medium"`
+ Medium string `json:"medium"`
+ Large string `json:"large"`
+ Original string `json:"original"`
+ } `json:"image_urls"`
+}
+
+type ImageUrlsEntity struct {
+ SquareMedium string `json:"square_medium"`
+ Medium string `json:"medium"`
+ Large string `json:"large"`
+}
+
+type UserEntity struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Account string `json:"account"`
+ ProfileImageUrls ProfileImageUrlsEntity `json:"profile_image_urls"`
+ IsFollowed bool `json:"is_followed"`
+ IsAcceptRequest bool `json:"is_accept_request"`
+}
+
+type ProfileImageUrlsEntity struct {
+ Medium string `json:"medium"`
+}
+
+type TagsEntity struct {
+ Name string `json:"name"`
+ TranslatedName interface{} `json:"translated_name"`
+}
+
+type MetaSinglePageEntity struct {
+ OriginalImageUrl string `json:"original_image_url"`
+}
diff --git a/plugin/pixiv/service.go b/plugin/pixiv/service.go
new file mode 100644
index 0000000000..8c4a889bb4
--- /dev/null
+++ b/plugin/pixiv/service.go
@@ -0,0 +1,373 @@
+package pixiv
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/api"
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/cache"
+ "github.com/FloatTech/ZeroBot-Plugin/plugin/pixiv/model"
+ "github.com/FloatTech/floatbox/file"
+ log "github.com/sirupsen/logrus"
+ zero "github.com/wdvxdr1123/ZeroBot"
+ "github.com/wdvxdr1123/ZeroBot/message"
+)
+
+var cacheFilling sync.Map
+
+// Service 用于封装整个 Pixiv 模块的依赖与接口
+type Service struct {
+ DB *cache.DB
+ API *api.PixivAPI
+ // 内部任务锁:限制每个人同一时间只能执行一个请求
+ taskMu sync.Mutex
+ tasks map[int64]*taskState
+
+ // 并发控制
+ DownloadWorkers int
+ SendWorkers int
+}
+
+type taskState struct {
+ Running bool
+}
+
+const pixivTempDir = "data/pixiv/temp"
+
+func NewService(db *cache.DB, api *api.PixivAPI) *Service {
+ return &Service{
+ DB: db,
+ API: api,
+ tasks: make(map[int64]*taskState),
+ DownloadWorkers: 4,
+ SendWorkers: 2,
+ }
+}
+
+func (s *Service) Acquire(userID int64) bool {
+ s.taskMu.Lock()
+ defer s.taskMu.Unlock()
+
+ t, ok := s.tasks[userID]
+ if ok && t.Running {
+ return false
+ }
+
+ if !ok {
+ t = &taskState{}
+ s.tasks[userID] = t
+ }
+ t.Running = true
+ return true
+}
+
+func (s *Service) Release(userID int64) {
+ s.taskMu.Lock()
+ defer s.taskMu.Unlock()
+
+ if t, ok := s.tasks[userID]; ok {
+ t.Running = false
+ }
+ delete(s.tasks, userID)
+}
+
+func removeTempImages(paths []string) {
+ for _, p := range paths {
+ if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
+ log.Warnf("删除 pixiv 临时文件失败: %s: %v", p, err)
+ }
+ }
+}
+
+func scheduleCleanupPixivImages(paths []string, delay time.Duration) {
+ if len(paths) == 0 {
+ return
+ }
+ local := append([]string(nil), paths...)
+ go func() {
+ time.Sleep(delay)
+ removeTempImages(local)
+ }()
+}
+
+func pixivImageExt(rawURL string) string {
+ parsed, err := url.Parse(rawURL)
+ if err != nil {
+ return ".jpg"
+ }
+ ext := path.Ext(parsed.Path)
+ if ext == "" {
+ return ".jpg"
+ }
+ return ext
+}
+
+func createPixivTempImagePath(pid int64, index int, rawURL string) (string, error) {
+ baseDir := filepath.Join(file.BOTPATH, pixivTempDir)
+ if err := os.MkdirAll(baseDir, 0o775); err != nil {
+ return "", err
+ }
+ pattern := fmt.Sprintf("%d-", pid)
+ if index > 0 {
+ pattern = fmt.Sprintf("%d-%d-", pid, index)
+ }
+ tmp, err := os.CreateTemp(baseDir, pattern+"*"+pixivImageExt(rawURL))
+ if err != nil {
+ return "", err
+ }
+ name := tmp.Name()
+ if err = tmp.Close(); err != nil {
+ return "", err
+ }
+ return name, nil
+}
+
+func writeTempImages(pid int64, images [][]byte, urls []string) ([]string, error) {
+ paths := make([]string, 0, len(images))
+ cleanup := func() {
+ removeTempImages(paths)
+ }
+
+ for i, img := range images {
+ if len(img) == 0 {
+ continue
+ }
+ rawURL := ""
+ if i < len(urls) {
+ rawURL = urls[i]
+ }
+ imagePath, err := createPixivTempImagePath(pid, i+1, rawURL)
+ if err != nil {
+ cleanup()
+ return nil, err
+ }
+ if err = os.WriteFile(imagePath, img, 0o644); err != nil {
+ cleanup()
+ return nil, err
+ }
+ paths = append(paths, imagePath)
+ }
+
+ return paths, nil
+}
+
+func toFileImage(path string) message.Segment {
+ normalized := strings.ReplaceAll(path, "\\", "/")
+ if strings.HasPrefix(normalized, "/") {
+ return message.Image("file://" + normalized)
+ }
+ return message.Image("file:///" + normalized)
+}
+
+func (s *Service) SendIllusts(ctx *zero.Ctx, illusts []model.IllustCache) {
+ downloadSem := make(chan struct{}, s.DownloadWorkers)
+ type DLResult struct {
+ Ill model.IllustCache
+ Images [][]byte
+ Err error
+ }
+
+ gid := ctx.Event.GroupID
+ if gid == 0 {
+ gid = -ctx.Event.UserID
+ }
+
+ results := make(chan DLResult, len(illusts))
+
+ // 并发下载
+ for _, ill := range illusts {
+ ill1 := ill
+ downloadSem <- struct{}{}
+
+ go func() {
+ defer func() { <-downloadSem }()
+
+ d := DLResult{
+ Ill: ill1,
+ }
+
+ if ill1.PageCount > 1 {
+
+ type pageResult struct {
+ index int
+ data []byte
+ err error
+ }
+
+ pageCh := make(chan pageResult, ill1.PageCount)
+ var wg sync.WaitGroup
+
+ pageSem := make(chan struct{}, s.DownloadWorkers)
+
+ for i := 0; int64(i) < ill1.PageCount; i++ {
+ wg.Add(1)
+ pageSem <- struct{}{}
+
+ go func(page int) {
+ defer wg.Done()
+ defer func() { <-pageSem }()
+
+ u := api.ModifyPageGeneric(ill1.OriginalURL, page)
+ img, err := s.API.Client.FetchPixivImage(ill1, u)
+ if err != nil {
+ pageCh <- pageResult{
+ index: page,
+ err: err,
+ }
+ return
+ }
+ pageCh <- pageResult{
+ index: page,
+ data: img,
+ err: err,
+ }
+ }(i)
+ }
+
+ // 关闭 channel
+ go func() {
+ wg.Wait()
+ close(pageCh)
+ }()
+
+ // 预分配,保证顺序
+ d.Images = make([][]byte, ill1.PageCount)
+
+ for r := range pageCh {
+ if r.err != nil && d.Err == nil {
+ d.Err = r.err
+ }
+ if len(r.data) == 0 {
+ continue
+ }
+ d.Images[r.index] = r.data
+ }
+
+ } else {
+ img, err := s.API.Client.FetchPixivImage(ill1, ill1.OriginalURL)
+ if err == nil {
+ d.Images = append(d.Images, img)
+ } else {
+ d.Err = err
+ }
+ }
+
+ log.Print("下载图片完成:", ill1.PID)
+ results <- d
+ }()
+
+ }
+
+ // 发送(顺序)
+ for range illusts {
+ res := <-results
+
+ if res.Err != nil {
+ if httpErr, ok := errors.AsType[*api.HTTPStatusError](res.Err); ok && httpErr.StatusCode == http.StatusNotFound {
+ if err := s.DB.DeleteIllustByPID(res.Ill.PID); err != nil {
+ ctx.SendChain(message.Text("清理已失效图片失败: ", err))
+ } else {
+ ctx.SendChain(message.Text("图片已被删除,已移除缓存,PID: ", res.Ill.PID))
+ }
+ } else {
+ ctx.SendChain(message.Text("下载失败: ", res.Err))
+ }
+
+ continue
+ }
+
+ var msg message.Message
+ msg = append(msg, message.Text(
+ "PID:", res.Ill.PID,
+ "\n标题:", res.Ill.Title,
+ "\n画师:", res.Ill.AuthorName,
+ "\ntag:", res.Ill.Tags,
+ "\n收藏数:", res.Ill.Bookmarks,
+ "\n浏览数:", res.Ill.TotalView,
+ "\n发布时间:", res.Ill.CreateDate,
+ ))
+
+ imageURLs := make([]string, len(res.Images))
+ for i := range imageURLs {
+ if i == 0 {
+ imageURLs[i] = res.Ill.OriginalURL
+ continue
+ }
+ imageURLs[i] = api.ModifyPageGeneric(res.Ill.OriginalURL, i)
+ }
+
+ tempPaths, err := writeTempImages(res.Ill.PID, res.Images, imageURLs)
+ if err != nil {
+ ctx.SendChain(message.Text("暂存图片失败: ", err))
+ continue
+ }
+ for _, tpath := range tempPaths {
+ msg = append(msg, toFileImage(tpath))
+ }
+
+ ctx.Send(msg)
+ scheduleCleanupPixivImages(tempPaths, 15*time.Second)
+
+ s.DB.Create(&model.SentImage{
+ GroupID: gid,
+ PID: res.Ill.PID,
+ })
+ }
+}
+
+func (s *Service) BackgroundCacheFiller(keyword string, minCache int, r18Req bool, fetchCount int, gid int64) {
+ if _, loaded := cacheFilling.LoadOrStore(keyword, struct{}{}); loaded {
+ log.Print("已有后台任务在补缓存: ", keyword)
+ return
+ }
+
+ go func() {
+ defer cacheFilling.Delete(keyword)
+
+ count, err := s.DB.CountIllustsSmart(gid, keyword, r18Req)
+ if err != nil {
+ log.Print("查询数据库发生错误: ", err)
+ return
+ }
+
+ if count >= int64(minCache) {
+ log.Print("缓存足够,无需补充: ", keyword)
+ return
+ }
+
+ log.Printf("后台补充关键词 %s, 数量 %d\n", keyword, fetchCount)
+
+ sendedcache, err := s.DB.GetSentPictureIDs(gid)
+ if err != nil {
+ log.Print("后台补充缓存失败: ", err)
+ return
+ }
+ s1, err := s.DB.GetIllustIDsByKeyword(keyword)
+ sendedcache = append(sendedcache, s1...)
+ newIllusts, err := s.API.FetchPixivIllusts(keyword, r18Req, fetchCount, sendedcache)
+ if err != nil {
+ log.Print("后台补充缓存失败: ", err)
+ return
+ }
+
+ if len(newIllusts) == 0 {
+ log.Print("后台补充缓存:没有新图")
+ return
+ }
+
+ for _, illust := range newIllusts {
+ s.DB.Create(&illust)
+ log.Print("后台补充缓存:", illust.PID)
+ }
+
+ log.Printf("后台成功补充 %d 张图片到关键词 %s 缓存\n", len(newIllusts), keyword)
+ }()
+}
From 532f30de6dea513f6689d08ba7254c00ad65b891 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E5=AE=87=E5=AE=87?= <3061196825@qq.com>
Date: Thu, 18 Jun 2026 21:21:34 +0800
Subject: [PATCH 2/3] =?UTF-8?q?[fix]=E4=BF=AE=E5=A4=8Dpr=E6=97=B6=E7=9A=84?=
=?UTF-8?q?ci=20bug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/pull.yml | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml
index af61750c1d..b061da8099 100644
--- a/.github/workflows/pull.yml
+++ b/.github/workflows/pull.yml
@@ -1,11 +1,14 @@
name: PullLint
on:
+ pull_request:
+ types: [opened, synchronize, reopened]
pull_request_target:
types: [assigned, opened, synchronize, reopened]
jobs:
# This workflow closes invalid PR
close-pr:
name: closepr
+ if: ${{ github.event_name == 'pull_request_target' }}
# The type of runner that the job will run on
runs-on: ubuntu-latest
permissions: write-all
@@ -20,8 +23,7 @@ jobs:
comment: "非法PR. 请`fork`后修改自己的仓库, 而不是向主仓库提交更改. 如果您确信您的PR是为了给主仓库新增功能或修复bug, 请更改默认PR标题. **注意**: 如果您再次触发本提示, 则有可能导致账号被封禁."
golangci:
- needs: close-pr
- if: ${{ !contains(github.event.pull_request.title, '.go') }}
+ if: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.title, '.go') }}
uses: ./.github/workflows/lib_lint.yml
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -29,13 +31,14 @@ jobs:
runmain:
needs: golangci
- if: ${{ !contains(github.event.pull_request.title, '.go') }}
+ if: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.title, '.go') }}
uses: ./.github/workflows/lib_run.yml
with:
ref: ${{ github.event.pull_request.head.sha }}
build:
needs: [runmain]
+ if: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.title, '.go') }}
uses: ./.github/workflows/lib_build.yml
with:
prefix: "zbp_"
From 1636e462376fefd812db2afd38416c6e515952f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=B0=8F=E5=AE=87=E5=AE=87?= <3061196825@qq.com>
Date: Thu, 18 Jun 2026 21:40:07 +0800
Subject: [PATCH 3/3] fix:workflows
---
.github/workflows/lib_build.yml | 1 +
.github/workflows/lib_lint.yml | 1 +
.github/workflows/lib_run.yml | 1 +
3 files changed, 3 insertions(+)
diff --git a/.github/workflows/lib_build.yml b/.github/workflows/lib_build.yml
index ac59d51bf5..0704c6fab9 100644
--- a/.github/workflows/lib_build.yml
+++ b/.github/workflows/lib_build.yml
@@ -36,6 +36,7 @@ jobs:
- uses: actions/checkout@master
with:
fetch-depth: 0
+ allow-unsafe-pr-checkout: true
- name: Setup Go environment
uses: actions/setup-go@master
diff --git a/.github/workflows/lib_lint.yml b/.github/workflows/lib_lint.yml
index 29bfacd181..3200b1eebf 100644
--- a/.github/workflows/lib_lint.yml
+++ b/.github/workflows/lib_lint.yml
@@ -36,6 +36,7 @@ jobs:
ref: ${{ inputs.ref }}
fetch-depth: ${{ inputs.fetch-depth }}
submodules: ${{ inputs.submodules }}
+ allow-unsafe-pr-checkout: true
- name: Prepare Necessary Runtime Files
run: |
diff --git a/.github/workflows/lib_run.yml b/.github/workflows/lib_run.yml
index 56be97e439..5db5ae5a24 100644
--- a/.github/workflows/lib_run.yml
+++ b/.github/workflows/lib_run.yml
@@ -38,6 +38,7 @@ jobs:
ref: ${{ inputs.ref }}
fetch-depth: ${{ inputs.fetch-depth }}
submodules: ${{ inputs.submodules }}
+ allow-unsafe-pr-checkout: true
- name: Prepare and Build
shell: bash