koishi-plugin-quark-search 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -0
- package/lib/index.d.ts +32 -0
- package/lib/index.js +730 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# koishi-plugin-quark-search
|
|
2
|
+
|
|
3
|
+
夸克网盘资源搜索转存插件,支持自定义搜索API、备用API切换、广告过滤等功能。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 🔍 **自定义搜索API**:支持配置任意搜索接口
|
|
8
|
+
- 🔄 **备用API自动切换**:主API失败时自动使用备用API
|
|
9
|
+
- ✅ **链接有效性验证**:自动过滤失效的分享链接
|
|
10
|
+
- 📄 **分页浏览**:支持上一页/下一页浏览搜索结果
|
|
11
|
+
- 🚀 **自动转存分享**:一键转存到自己网盘并生成新分享链接
|
|
12
|
+
- 🛡️ **广告过滤**:自动删除或重命名包含广告关键词的文件
|
|
13
|
+
- 🔁 **自动重试**:网络异常时自动重试,提高成功率
|
|
14
|
+
- 📊 **详细日志**:记录所有操作,方便调试
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
### 方法一:本地插件(推荐)
|
|
19
|
+
|
|
20
|
+
1. 将 `quark-search` 文件夹放到 Koishi 的 `external` 目录
|
|
21
|
+
2. 在 Koishi 根目录执行 `yarn install`
|
|
22
|
+
3. 在控制台启用插件
|
|
23
|
+
|
|
24
|
+
### 方法二:npm 安装
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add koishi-plugin-quark-search
|
|
28
|
+
# 或
|
|
29
|
+
npm install koishi-plugin-quark-search
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 配置说明
|
|
33
|
+
|
|
34
|
+
### 必填配置
|
|
35
|
+
|
|
36
|
+
| 配置项 | 说明 | 示例 |
|
|
37
|
+
|--------|------|------|
|
|
38
|
+
| searchApiUrl | 搜索API地址 | `https://wzapi.com/api/jhsj` |
|
|
39
|
+
| quarkCookie | 夸克网盘Cookie | 从浏览器开发者工具获取 |
|
|
40
|
+
|
|
41
|
+
### 搜索API配置
|
|
42
|
+
|
|
43
|
+
| 配置项 | 默认值 | 说明 |
|
|
44
|
+
|--------|--------|------|
|
|
45
|
+
| searchApiMethod | `GET` | 请求方式 |
|
|
46
|
+
| searchApiParams | `{"kw": "{keyword}"}` | 请求参数,`{keyword}`会被替换 |
|
|
47
|
+
| searchApiHeaders | `{"User-Agent": "..."}` | 请求头 |
|
|
48
|
+
| searchResultPath | `data` | 结果在响应中的路径 |
|
|
49
|
+
| searchTitleField | `title` | 标题字段名 |
|
|
50
|
+
| searchUrlField | `url` | 链接字段名 |
|
|
51
|
+
|
|
52
|
+
### 备用API配置
|
|
53
|
+
|
|
54
|
+
| 配置项 | 默认值 | 说明 |
|
|
55
|
+
|--------|--------|------|
|
|
56
|
+
| enableBackupApi | `false` | 启用备用API |
|
|
57
|
+
| backupApiUrl | - | 备用API地址 |
|
|
58
|
+
| backupApiMethod | `GET` | 备用API请求方式 |
|
|
59
|
+
| backupApiParams | `{"kw": "{keyword}"}` | 备用API请求参数 |
|
|
60
|
+
| backupApiHeaders | `{"User-Agent": "..."}` | 备用API请求头 |
|
|
61
|
+
| backupResultPath | `data` | 备用API结果路径 |
|
|
62
|
+
| backupTitleField | `title` | 备用API标题字段 |
|
|
63
|
+
| backupUrlField | `url` | 备用API链接字段 |
|
|
64
|
+
|
|
65
|
+
### 广告过滤配置
|
|
66
|
+
|
|
67
|
+
| 配置项 | 默认值 | 说明 |
|
|
68
|
+
|--------|--------|------|
|
|
69
|
+
| enableAdFilter | `false` | 启用广告过滤 |
|
|
70
|
+
| deleteKeywords | `[]` | 删除关键词列表 |
|
|
71
|
+
| replaceRules | `{}` | 替换规则对象 |
|
|
72
|
+
| scanDepth | `3` | 扫描深度(层数) |
|
|
73
|
+
| scanDelay | `5` | 扫描延迟(秒) |
|
|
74
|
+
|
|
75
|
+
### 其他配置
|
|
76
|
+
|
|
77
|
+
| 配置项 | 默认值 | 说明 |
|
|
78
|
+
|--------|--------|------|
|
|
79
|
+
| watchGroups | `[]` | 监控的群号列表,留空监控所有群 |
|
|
80
|
+
| saveFolderId | `0` | 转存目标文件夹ID,0为根目录 |
|
|
81
|
+
| expiredType | `1` | 分享有效期:1=永久,2=7天,3=1天 |
|
|
82
|
+
| limit | `10` | 每次搜索返回的最大结果数 |
|
|
83
|
+
| sessionTimeout | `120` | 搜索会话超时时间(秒) |
|
|
84
|
+
| saveTimeout | `180` | 转存超时时间(秒) |
|
|
85
|
+
|
|
86
|
+
## 使用方法
|
|
87
|
+
|
|
88
|
+
### 基本搜索
|
|
89
|
+
|
|
90
|
+
在群聊中发送:
|
|
91
|
+
```
|
|
92
|
+
搜 流浪地球
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
插件会返回搜索结果列表,回复数字编号即可转存。
|
|
96
|
+
|
|
97
|
+
### 翻页浏览
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
下一页
|
|
101
|
+
上一页
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 获取 Cookie
|
|
105
|
+
|
|
106
|
+
1. 打开浏览器,访问 https://pan.quark.cn/
|
|
107
|
+
2. 登录你的夸克账号
|
|
108
|
+
3. 按 F12 打开开发者工具
|
|
109
|
+
4. 切换到 Network(网络)标签
|
|
110
|
+
5. 刷新页面,找到任意请求
|
|
111
|
+
6. 在请求头中找到 `Cookie` 字段
|
|
112
|
+
7. 复制完整的 Cookie 值到插件配置
|
|
113
|
+
|
|
114
|
+
## 广告过滤示例
|
|
115
|
+
|
|
116
|
+
### 删除包含关键词的文件
|
|
117
|
+
|
|
118
|
+
配置 `deleteKeywords`:
|
|
119
|
+
```json
|
|
120
|
+
["必看", "资源", "点击查看", "有惊喜", "广告"]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
转存后,插件会自动删除文件名包含这些关键词的文件或文件夹。
|
|
124
|
+
|
|
125
|
+
### 替换文件名中的关键词
|
|
126
|
+
|
|
127
|
+
配置 `replaceRules`:
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"【广告】": "",
|
|
131
|
+
"点击关注": "",
|
|
132
|
+
"VIP资源": "资源"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
转存后,插件会自动重命名文件,将关键词替换为指定内容。
|
|
137
|
+
|
|
138
|
+
## 工作流程
|
|
139
|
+
|
|
140
|
+
1. 用户发送搜索指令
|
|
141
|
+
2. 调用主搜索API(失败则尝试备用API)
|
|
142
|
+
3. 验证链接有效性,过滤失效链接
|
|
143
|
+
4. 返回有效结果列表
|
|
144
|
+
5. 用户选择编号
|
|
145
|
+
6. 转存到自己网盘
|
|
146
|
+
7. 创建新分享链接
|
|
147
|
+
8. 发送链接给用户
|
|
148
|
+
9. 后台执行广告过滤(如果启用)
|
|
149
|
+
|
|
150
|
+
## 日志说明
|
|
151
|
+
|
|
152
|
+
插件会记录详细的操作日志:
|
|
153
|
+
|
|
154
|
+
- `[INFO]` 正常操作信息
|
|
155
|
+
- `[WARN]` 警告信息(如重试)
|
|
156
|
+
- `[ERROR]` 错误信息
|
|
157
|
+
- `[DEBUG]` 调试信息(需开启调试模式)
|
|
158
|
+
|
|
159
|
+
查看日志可以了解:
|
|
160
|
+
- 搜索到的资源及链接
|
|
161
|
+
- 链接验证结果
|
|
162
|
+
- 转存过程
|
|
163
|
+
- 广告过滤操作
|
|
164
|
+
|
|
165
|
+
## 常见问题
|
|
166
|
+
|
|
167
|
+
### Q: 搜索无结果?
|
|
168
|
+
A:
|
|
169
|
+
1. 检查搜索API是否可用
|
|
170
|
+
2. 尝试启用备用API
|
|
171
|
+
3. 更换其他搜索API
|
|
172
|
+
|
|
173
|
+
### Q: 转存失败?
|
|
174
|
+
A:
|
|
175
|
+
1. 检查Cookie是否过期
|
|
176
|
+
2. 检查网盘容量是否充足
|
|
177
|
+
3. 查看日志了解具体错误
|
|
178
|
+
|
|
179
|
+
### Q: 链接都显示失效?
|
|
180
|
+
A: 可能是搜索API返回的链接质量问题,建议更换API源
|
|
181
|
+
|
|
182
|
+
### Q: 广告过滤不生效?
|
|
183
|
+
A:
|
|
184
|
+
1. 确认已启用 `enableAdFilter`
|
|
185
|
+
2. 检查关键词配置是否正确
|
|
186
|
+
3. 查看日志确认是否执行了过滤
|
|
187
|
+
|
|
188
|
+
## 开发
|
|
189
|
+
|
|
190
|
+
### 编译
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
yarn tsc
|
|
194
|
+
# 或
|
|
195
|
+
npm run tsc
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 打包
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm pack
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 调试
|
|
205
|
+
|
|
206
|
+
在 Koishi 配置中启用调试模式,可以看到更详细的日志。
|
|
207
|
+
|
|
208
|
+
## 许可证
|
|
209
|
+
|
|
210
|
+
MIT
|
|
211
|
+
|
|
212
|
+
## 更新日志
|
|
213
|
+
|
|
214
|
+
### v1.0.0
|
|
215
|
+
- 初始版本
|
|
216
|
+
- 支持自定义搜索API
|
|
217
|
+
- 支持备用API自动切换
|
|
218
|
+
- 支持链接有效性验证
|
|
219
|
+
- 支持广告过滤
|
|
220
|
+
- 支持自动重试
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "quark-search";
|
|
3
|
+
export interface Config {
|
|
4
|
+
searchApiUrl: string;
|
|
5
|
+
searchApiMethod: 'GET' | 'POST';
|
|
6
|
+
searchApiParams: string;
|
|
7
|
+
searchApiHeaders: string;
|
|
8
|
+
searchResultPath: string;
|
|
9
|
+
searchTitleField: string;
|
|
10
|
+
searchUrlField: string;
|
|
11
|
+
enableBackupApi: boolean;
|
|
12
|
+
backupApiUrl: string;
|
|
13
|
+
backupApiMethod: 'GET' | 'POST';
|
|
14
|
+
backupApiParams: string;
|
|
15
|
+
backupApiHeaders: string;
|
|
16
|
+
backupResultPath: string;
|
|
17
|
+
backupTitleField: string;
|
|
18
|
+
backupUrlField: string;
|
|
19
|
+
quarkCookie: string;
|
|
20
|
+
saveFolderId: string;
|
|
21
|
+
expiredType: number;
|
|
22
|
+
enableAdFilter: boolean;
|
|
23
|
+
deleteKeywords: string[];
|
|
24
|
+
replaceRules: Record<string, string>;
|
|
25
|
+
scanDepth: number;
|
|
26
|
+
watchGroups: string[];
|
|
27
|
+
limit: number;
|
|
28
|
+
sessionTimeout: number;
|
|
29
|
+
saveTimeout: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const Config: Schema<Config>;
|
|
32
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name2 in all)
|
|
8
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
Config: () => Config,
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
name: () => name
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
var import_koishi = require("koishi");
|
|
29
|
+
var name = "quark-search";
|
|
30
|
+
var Config = import_koishi.Schema.object({
|
|
31
|
+
searchApiUrl: import_koishi.Schema.string().required().description("搜索 API 地址,例如:https://wzapi.com/api/jhsj"),
|
|
32
|
+
searchApiMethod: import_koishi.Schema.union(["GET", "POST"]).default("GET").description("请求方式"),
|
|
33
|
+
searchApiParams: import_koishi.Schema.string().default('{"kw": "{keyword}"}').description("请求参数,JSON格式,{keyword}会被替换为搜索关键词"),
|
|
34
|
+
searchApiHeaders: import_koishi.Schema.string().default('{"User-Agent": "Mozilla/5.0"}').description("请求头,JSON格式"),
|
|
35
|
+
searchResultPath: import_koishi.Schema.string().default("data").description("搜索结果在响应中的路径,用点分隔,如 data.list"),
|
|
36
|
+
searchTitleField: import_koishi.Schema.string().default("title").description("标题字段名"),
|
|
37
|
+
searchUrlField: import_koishi.Schema.string().default("url").description("链接字段名"),
|
|
38
|
+
enableBackupApi: import_koishi.Schema.boolean().default(false).description("启用备用搜索API(当主API失败或无结果时自动切换)"),
|
|
39
|
+
backupApiUrl: import_koishi.Schema.string().description("备用搜索 API 地址"),
|
|
40
|
+
backupApiMethod: import_koishi.Schema.union(["GET", "POST"]).default("GET").description("备用API请求方式"),
|
|
41
|
+
backupApiParams: import_koishi.Schema.string().default('{"kw": "{keyword}"}').description("备用API请求参数,JSON格式,{keyword}会被替换为搜索关键词"),
|
|
42
|
+
backupApiHeaders: import_koishi.Schema.string().default('{"User-Agent": "Mozilla/5.0"}').description("备用API请求头,JSON格式"),
|
|
43
|
+
backupResultPath: import_koishi.Schema.string().default("data").description("备用API搜索结果在响应中的路径,用点分隔"),
|
|
44
|
+
backupTitleField: import_koishi.Schema.string().default("title").description("备用API标题字段名"),
|
|
45
|
+
backupUrlField: import_koishi.Schema.string().default("url").description("备用API链接字段名"),
|
|
46
|
+
quarkCookie: import_koishi.Schema.string().required().role("textarea").description("夸克网盘 Cookie(必填,从浏览器获取)"),
|
|
47
|
+
saveFolderId: import_koishi.Schema.string().default("0").description("转存目标文件夹ID,0表示根目录"),
|
|
48
|
+
expiredType: import_koishi.Schema.number().default(1).description("分享有效期:1=永久,2=7天,3=1天"),
|
|
49
|
+
watchGroups: import_koishi.Schema.array(String).default([]).description("监控的群号列表,留空则监控所有群"),
|
|
50
|
+
limit: import_koishi.Schema.number().default(10).description("每次搜索返回的最大结果数"),
|
|
51
|
+
sessionTimeout: import_koishi.Schema.number().default(120).description("搜索会话超时时间(秒)"),
|
|
52
|
+
saveTimeout: import_koishi.Schema.number().default(180).description("转存超时时间(秒)")
|
|
53
|
+
});
|
|
54
|
+
var PAGE_SIZE = 10;
|
|
55
|
+
var searchSessions = /* @__PURE__ */ new Map();
|
|
56
|
+
function getQuarkHeaders(cookie) {
|
|
57
|
+
return {
|
|
58
|
+
"Accept": "application/json, text/plain, */*",
|
|
59
|
+
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
60
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
61
|
+
"sec-ch-ua": '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
|
62
|
+
"sec-ch-ua-mobile": "?0",
|
|
63
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
64
|
+
"sec-fetch-dest": "empty",
|
|
65
|
+
"sec-fetch-mode": "cors",
|
|
66
|
+
"sec-fetch-site": "same-site",
|
|
67
|
+
"Referer": "https://pan.quark.cn/",
|
|
68
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
69
|
+
"Cookie": cookie
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
__name(getQuarkHeaders, "getQuarkHeaders");
|
|
73
|
+
async function checkQuarkLinkValid(ctx, url, logger) {
|
|
74
|
+
const match = url.match(/\/s\/([a-zA-Z0-9]+)/);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return { valid: false, message: "无法解析链接格式" };
|
|
77
|
+
}
|
|
78
|
+
const pwdId = match[1];
|
|
79
|
+
try {
|
|
80
|
+
const headers = {
|
|
81
|
+
"Accept": "application/json, text/plain, */*",
|
|
82
|
+
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
83
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
84
|
+
"Referer": "https://pan.quark.cn/",
|
|
85
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
86
|
+
"Origin": "https://pan.quark.cn"
|
|
87
|
+
};
|
|
88
|
+
const response = await ctx.http.post(
|
|
89
|
+
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token",
|
|
90
|
+
{ passcode: "", pwd_id: pwdId },
|
|
91
|
+
{
|
|
92
|
+
headers,
|
|
93
|
+
params: { pr: "ucpro", fr: "pc", uc_param_str: "" },
|
|
94
|
+
timeout: 1e4
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
if (response.status === 200 && response.data) {
|
|
98
|
+
return { valid: true, message: "链接有效" };
|
|
99
|
+
}
|
|
100
|
+
const message = response.message || "未知错误";
|
|
101
|
+
return { valid: false, message };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.response?.data) {
|
|
104
|
+
const data = error.response.data;
|
|
105
|
+
const message = data.message || "链接失效";
|
|
106
|
+
return { valid: false, message };
|
|
107
|
+
}
|
|
108
|
+
return { valid: false, message: "检测失败" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
__name(checkQuarkLinkValid, "checkQuarkLinkValid");
|
|
112
|
+
async function filterValidLinks(ctx, items, logger) {
|
|
113
|
+
const validItems = [];
|
|
114
|
+
const checkPromises = items.map(async (item) => {
|
|
115
|
+
const result = await checkQuarkLinkValid(ctx, item.url, logger);
|
|
116
|
+
return { item, valid: result.valid, message: result.message };
|
|
117
|
+
});
|
|
118
|
+
const results = await Promise.all(checkPromises);
|
|
119
|
+
for (const { item, valid, message } of results) {
|
|
120
|
+
if (valid) {
|
|
121
|
+
item.valid = true;
|
|
122
|
+
validItems.push(item);
|
|
123
|
+
} else {
|
|
124
|
+
logger.debug(`链接失效: ${item.title} - ${message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return validItems;
|
|
128
|
+
}
|
|
129
|
+
__name(filterValidLinks, "filterValidLinks");
|
|
130
|
+
function apply(ctx, config) {
|
|
131
|
+
const logger = ctx.logger("quark-search");
|
|
132
|
+
const cleanupInterval = setInterval(() => {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
for (const [key, session] of searchSessions) {
|
|
135
|
+
if (now - session.timestamp > config.sessionTimeout * 1e3) {
|
|
136
|
+
searchSessions.delete(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}, 3e4);
|
|
140
|
+
ctx.on("dispose", () => {
|
|
141
|
+
clearInterval(cleanupInterval);
|
|
142
|
+
searchSessions.clear();
|
|
143
|
+
});
|
|
144
|
+
ctx.guild().on("message", async (session) => {
|
|
145
|
+
const groupId = session.guildId || session.channelId;
|
|
146
|
+
if (config.watchGroups.length > 0 && !config.watchGroups.includes(groupId)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const content = session.content?.trim() || "";
|
|
150
|
+
const sessionKey = `${groupId}:${session.userId}`;
|
|
151
|
+
const bot = session.bot;
|
|
152
|
+
const internal = bot.internal;
|
|
153
|
+
if (/^\d+$/.test(content)) {
|
|
154
|
+
const userSession = searchSessions.get(sessionKey);
|
|
155
|
+
if (userSession && Date.now() - userSession.timestamp < config.sessionTimeout * 1e3) {
|
|
156
|
+
const index = parseInt(content, 10);
|
|
157
|
+
if (index >= 1 && index <= userSession.validResults.length) {
|
|
158
|
+
await handleResourceSelection(ctx, config, internal, groupId, session.userId, userSession, index, logger);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (content === "下一页" || content === "上一页") {
|
|
164
|
+
const userSession = searchSessions.get(sessionKey);
|
|
165
|
+
if (userSession && Date.now() - userSession.timestamp < config.sessionTimeout * 1e3) {
|
|
166
|
+
const totalPages = Math.ceil(userSession.validResults.length / PAGE_SIZE);
|
|
167
|
+
if (content === "下一页") {
|
|
168
|
+
if (userSession.currentPage < totalPages) {
|
|
169
|
+
userSession.currentPage++;
|
|
170
|
+
userSession.timestamp = Date.now();
|
|
171
|
+
await sendPageResults(ctx, internal, groupId, session.userId, userSession, config.sessionTimeout, logger);
|
|
172
|
+
} else {
|
|
173
|
+
await internal.sendGroupMsg(groupId, [
|
|
174
|
+
{ type: "at", data: { qq: session.userId } },
|
|
175
|
+
{ type: "text", data: { text: " 已经是最后一页了" } }
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
if (userSession.currentPage > 1) {
|
|
180
|
+
userSession.currentPage--;
|
|
181
|
+
userSession.timestamp = Date.now();
|
|
182
|
+
await sendPageResults(ctx, internal, groupId, session.userId, userSession, config.sessionTimeout, logger);
|
|
183
|
+
} else {
|
|
184
|
+
await internal.sendGroupMsg(groupId, [
|
|
185
|
+
{ type: "at", data: { qq: session.userId } },
|
|
186
|
+
{ type: "text", data: { text: " 已经是第一页了" } }
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!content.startsWith("搜")) return;
|
|
194
|
+
const keyword = content.slice(1).trim();
|
|
195
|
+
if (!keyword) {
|
|
196
|
+
await internal.sendGroupMsg(groupId, [
|
|
197
|
+
{ type: "at", data: { qq: session.userId } },
|
|
198
|
+
{ type: "text", data: { text: " 请输入要搜索的内容,例如:搜 流浪地球" } }
|
|
199
|
+
]);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
logger.info(`收到搜索请求: ${keyword}, 群: ${groupId}, 用户: ${session.userId}`);
|
|
203
|
+
try {
|
|
204
|
+
let results = await searchResources(ctx, config, keyword, logger, false);
|
|
205
|
+
let usedBackupApi = false;
|
|
206
|
+
if (!results.length && config.enableBackupApi && config.backupApiUrl) {
|
|
207
|
+
logger.info(`主API无结果,尝试备用API...`);
|
|
208
|
+
try {
|
|
209
|
+
results = await searchResources(ctx, config, keyword, logger, true);
|
|
210
|
+
usedBackupApi = true;
|
|
211
|
+
if (results.length > 0) {
|
|
212
|
+
logger.info(`备用API搜索成功,找到 ${results.length} 个资源`);
|
|
213
|
+
}
|
|
214
|
+
} catch (backupError) {
|
|
215
|
+
logger.error("备用API搜索失败:", backupError);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!results.length) {
|
|
219
|
+
await internal.sendGroupMsg(groupId, [
|
|
220
|
+
{ type: "at", data: { qq: session.userId } },
|
|
221
|
+
{ type: "text", data: { text: ` 未找到「${keyword}」相关资源,换个关键词试试吧~` } }
|
|
222
|
+
]);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const apiSource = usedBackupApi ? "(备用API)" : "";
|
|
226
|
+
logger.info(`搜索到 ${results.length} 个${apiSource},验证中...`);
|
|
227
|
+
const firstBatch = results.slice(0, PAGE_SIZE);
|
|
228
|
+
const validFirstBatch = await filterValidLinks(ctx, firstBatch, logger);
|
|
229
|
+
if (validFirstBatch.length === 0) {
|
|
230
|
+
const moreBatch = results.slice(PAGE_SIZE, PAGE_SIZE * 3);
|
|
231
|
+
const validMoreBatch = await filterValidLinks(ctx, moreBatch, logger);
|
|
232
|
+
if (validMoreBatch.length === 0) {
|
|
233
|
+
await internal.sendGroupMsg(groupId, [
|
|
234
|
+
{ type: "at", data: { qq: session.userId } },
|
|
235
|
+
{ type: "text", data: { text: ` 「${keyword}」的资源链接均已失效,换个关键词试试吧~` } }
|
|
236
|
+
]);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
validFirstBatch.push(...validMoreBatch);
|
|
240
|
+
}
|
|
241
|
+
logger.info(`验证完成,有效: ${validFirstBatch.length} 个`);
|
|
242
|
+
const userSession = {
|
|
243
|
+
results,
|
|
244
|
+
// 保存所有原始结果
|
|
245
|
+
validResults: validFirstBatch,
|
|
246
|
+
// 已验证有效的结果
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
keyword,
|
|
249
|
+
currentPage: 1,
|
|
250
|
+
validating: false
|
|
251
|
+
};
|
|
252
|
+
searchSessions.set(sessionKey, userSession);
|
|
253
|
+
await sendPageResults(ctx, internal, groupId, session.userId, userSession, config.sessionTimeout, logger);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.error("搜索失败:", error);
|
|
256
|
+
await internal.sendGroupMsg(groupId, [
|
|
257
|
+
{ type: "at", data: { qq: session.userId } },
|
|
258
|
+
{ type: "text", data: { text: " 搜索服务暂时不可用,请稍后再试" } }
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
__name(apply, "apply");
|
|
264
|
+
async function sendPageResults(ctx, internal, groupId, userId, userSession, sessionTimeout, logger) {
|
|
265
|
+
const validResults = userSession.validResults;
|
|
266
|
+
const totalPages = Math.ceil(validResults.length / PAGE_SIZE);
|
|
267
|
+
const startIndex = (userSession.currentPage - 1) * PAGE_SIZE;
|
|
268
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, validResults.length);
|
|
269
|
+
const pageResults = validResults.slice(startIndex, endIndex);
|
|
270
|
+
const lines = [];
|
|
271
|
+
lines.push(`🔍「${userSession.keyword}」搜索结果(第${userSession.currentPage}/${totalPages}页,共${validResults.length}条有效资源):`);
|
|
272
|
+
lines.push("");
|
|
273
|
+
pageResults.forEach((item, index) => {
|
|
274
|
+
const globalIndex = startIndex + index + 1;
|
|
275
|
+
lines.push(`${globalIndex}. ${item.title}`);
|
|
276
|
+
});
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push(`📝 回复数字编号获取对应资源`);
|
|
279
|
+
if (userSession.currentPage < totalPages) {
|
|
280
|
+
lines.push(`📄 回复"下一页"查看更多`);
|
|
281
|
+
}
|
|
282
|
+
if (userSession.currentPage > 1) {
|
|
283
|
+
lines.push(`📄 回复"上一页"返回`);
|
|
284
|
+
}
|
|
285
|
+
lines.push(`⏰ ${sessionTimeout}秒内有效`);
|
|
286
|
+
try {
|
|
287
|
+
await internal.sendGroupMsg(groupId, [
|
|
288
|
+
{ type: "at", data: { qq: userId } },
|
|
289
|
+
{ type: "text", data: { text: "\n" + lines.join("\n") } }
|
|
290
|
+
]);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error(`发送结果失败:`, error);
|
|
293
|
+
}
|
|
294
|
+
const validatedCount = validResults.length;
|
|
295
|
+
const totalCount = userSession.results.length;
|
|
296
|
+
if (validatedCount < totalCount && !userSession.validating) {
|
|
297
|
+
userSession.validating = true;
|
|
298
|
+
const nextBatchStart = validatedCount;
|
|
299
|
+
const nextBatchEnd = Math.min(nextBatchStart + PAGE_SIZE, totalCount);
|
|
300
|
+
const nextBatch = userSession.results.slice(nextBatchStart, nextBatchEnd);
|
|
301
|
+
const unvalidated = nextBatch.filter((item) => item.valid === void 0);
|
|
302
|
+
if (unvalidated.length > 0) {
|
|
303
|
+
filterValidLinks(ctx, unvalidated, logger).then((validItems) => {
|
|
304
|
+
userSession.validResults.push(...validItems);
|
|
305
|
+
userSession.validating = false;
|
|
306
|
+
}).catch(() => {
|
|
307
|
+
userSession.validating = false;
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
userSession.validating = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
__name(sendPageResults, "sendPageResults");
|
|
315
|
+
async function searchResources(ctx, config, keyword, logger, useBackup = false) {
|
|
316
|
+
try {
|
|
317
|
+
const apiUrl = useBackup ? config.backupApiUrl : config.searchApiUrl;
|
|
318
|
+
const apiMethod = useBackup ? config.backupApiMethod : config.searchApiMethod;
|
|
319
|
+
const apiParamsStr = useBackup ? config.backupApiParams : config.searchApiParams;
|
|
320
|
+
const apiHeadersStr = useBackup ? config.backupApiHeaders : config.searchApiHeaders;
|
|
321
|
+
const resultPath = useBackup ? config.backupResultPath : config.searchResultPath;
|
|
322
|
+
const titleField = useBackup ? config.backupTitleField : config.searchTitleField;
|
|
323
|
+
const urlField = useBackup ? config.backupUrlField : config.searchUrlField;
|
|
324
|
+
const maxTitleLength = keyword.length + 10;
|
|
325
|
+
let params = {};
|
|
326
|
+
try {
|
|
327
|
+
params = JSON.parse(apiParamsStr);
|
|
328
|
+
for (const key in params) {
|
|
329
|
+
if (typeof params[key] === "string") {
|
|
330
|
+
params[key] = params[key].replace("{keyword}", keyword);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
logger.error("解析搜索参数失败:", e);
|
|
335
|
+
}
|
|
336
|
+
let headers = {};
|
|
337
|
+
try {
|
|
338
|
+
headers = JSON.parse(apiHeadersStr);
|
|
339
|
+
} catch (e) {
|
|
340
|
+
logger.error("解析请求头失败:", e);
|
|
341
|
+
}
|
|
342
|
+
let responseText;
|
|
343
|
+
const queryString = new URLSearchParams(params).toString();
|
|
344
|
+
const url = apiUrl + (queryString ? "?" + queryString : "");
|
|
345
|
+
if (apiMethod === "GET") {
|
|
346
|
+
responseText = await ctx.http.get(url, {
|
|
347
|
+
headers,
|
|
348
|
+
timeout: 3e4,
|
|
349
|
+
responseType: "text"
|
|
350
|
+
});
|
|
351
|
+
} else {
|
|
352
|
+
responseText = await ctx.http.post(apiUrl, params, {
|
|
353
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
354
|
+
timeout: 3e4,
|
|
355
|
+
responseType: "text"
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const responseStr = String(responseText);
|
|
359
|
+
const results = [];
|
|
360
|
+
const quarkPattern = /https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/g;
|
|
361
|
+
const isValidTitle = /* @__PURE__ */ __name((title) => {
|
|
362
|
+
if (!title) return false;
|
|
363
|
+
if (title.length > maxTitleLength) {
|
|
364
|
+
logger.debug(`标题过长被过滤: "${title}" (${title.length} > ${maxTitleLength})`);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
}, "isValidTitle");
|
|
369
|
+
if (responseStr.includes("data:") && responseStr.includes("\n")) {
|
|
370
|
+
const lines = responseStr.split("\n");
|
|
371
|
+
let sseDataFound = false;
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
const trimmedLine = line.trim();
|
|
374
|
+
if (trimmedLine.startsWith("data:") && !trimmedLine.includes("[DONE]")) {
|
|
375
|
+
sseDataFound = true;
|
|
376
|
+
try {
|
|
377
|
+
const jsonStr = trimmedLine.substring(5).trim();
|
|
378
|
+
if (jsonStr) {
|
|
379
|
+
const item = JSON.parse(jsonStr);
|
|
380
|
+
const title = item.title || item.note || "";
|
|
381
|
+
let itemUrl = item.url || "";
|
|
382
|
+
const stoken = item.stoken || "";
|
|
383
|
+
const match = itemUrl.match(/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/);
|
|
384
|
+
if (match) {
|
|
385
|
+
itemUrl = match[0];
|
|
386
|
+
}
|
|
387
|
+
if (isValidTitle(title) && itemUrl && itemUrl.includes("pan.quark.cn")) {
|
|
388
|
+
results.push({ title, url: itemUrl, stoken });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch (e) {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (sseDataFound && results.length > 0) {
|
|
396
|
+
return results;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
let jsonData;
|
|
400
|
+
try {
|
|
401
|
+
jsonData = JSON.parse(responseStr);
|
|
402
|
+
} catch (e) {
|
|
403
|
+
const allMatches = responseStr.matchAll(quarkPattern);
|
|
404
|
+
let index = 0;
|
|
405
|
+
for (const m of allMatches) {
|
|
406
|
+
if (!results.find((r) => r.url === m[0])) {
|
|
407
|
+
results.push({ title: `资源 ${++index}`, url: m[0] });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return results;
|
|
411
|
+
}
|
|
412
|
+
let data = jsonData;
|
|
413
|
+
if (resultPath) {
|
|
414
|
+
const paths = resultPath.split(".");
|
|
415
|
+
for (const p of paths) {
|
|
416
|
+
if (data && typeof data === "object" && p in data) {
|
|
417
|
+
data = data[p];
|
|
418
|
+
} else {
|
|
419
|
+
data = null;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (Array.isArray(data)) {
|
|
425
|
+
for (const item of data) {
|
|
426
|
+
const title = item[titleField] || item.note || item.title || "";
|
|
427
|
+
let itemUrl = item[urlField] || item.url || "";
|
|
428
|
+
const stoken = item.stoken || "";
|
|
429
|
+
const match = itemUrl.match(/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/);
|
|
430
|
+
if (match) {
|
|
431
|
+
itemUrl = match[0];
|
|
432
|
+
}
|
|
433
|
+
if (isValidTitle(title) && itemUrl && itemUrl.includes("pan.quark.cn")) {
|
|
434
|
+
results.push({ title, url: itemUrl, stoken });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (results.length === 0) {
|
|
439
|
+
const itemPattern = /"(?:note|title)"\s*:\s*"([^"]+)"[^}]*"url"\s*:\s*"([^"]+pan\.quark\.cn[^"]+)"/g;
|
|
440
|
+
const itemPattern2 = /"url"\s*:\s*"([^"]+pan\.quark\.cn[^"]+)"[^}]*"(?:note|title)"\s*:\s*"([^"]+)"/g;
|
|
441
|
+
let match;
|
|
442
|
+
while ((match = itemPattern.exec(responseStr)) !== null) {
|
|
443
|
+
const title = match[1];
|
|
444
|
+
let itemUrl = match[2].replace(/\\\//g, "/");
|
|
445
|
+
const urlMatch = itemUrl.match(/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/);
|
|
446
|
+
if (isValidTitle(title) && urlMatch && !results.find((r) => r.url === urlMatch[0])) {
|
|
447
|
+
results.push({ title, url: urlMatch[0] });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
while ((match = itemPattern2.exec(responseStr)) !== null) {
|
|
451
|
+
let itemUrl = match[1].replace(/\\\//g, "/");
|
|
452
|
+
const title = match[2];
|
|
453
|
+
const urlMatch = itemUrl.match(/https:\/\/pan\.quark\.cn\/s\/[a-zA-Z0-9]+/);
|
|
454
|
+
if (isValidTitle(title) && urlMatch && !results.find((r) => r.url === urlMatch[0])) {
|
|
455
|
+
results.push({ title, url: urlMatch[0] });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (results.length === 0) {
|
|
459
|
+
const allMatches = responseStr.matchAll(quarkPattern);
|
|
460
|
+
let index = 0;
|
|
461
|
+
for (const m of allMatches) {
|
|
462
|
+
if (!results.find((r) => r.url === m[0])) {
|
|
463
|
+
results.push({ title: `资源 ${++index}`, url: m[0] });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
logger.info(`搜索到 ${results.length} 个资源`);
|
|
469
|
+
return results;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
logger.error("搜索请求失败:", error);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
__name(searchResources, "searchResources");
|
|
476
|
+
async function handleResourceSelection(ctx, config, internal, groupId, userId, userSession, index, logger) {
|
|
477
|
+
const sessionKey = `${groupId}:${userId}`;
|
|
478
|
+
const selectedItem = userSession.validResults[index - 1];
|
|
479
|
+
logger.info(`用户选择: ${selectedItem.title}`);
|
|
480
|
+
try {
|
|
481
|
+
await internal.sendGroupMsg(groupId, [
|
|
482
|
+
{ type: "at", data: { qq: userId } },
|
|
483
|
+
{ type: "text", data: { text: ` 正在转存「${selectedItem.title}」,请稍候...` } }
|
|
484
|
+
]);
|
|
485
|
+
const result = await transferAndShare(ctx, config, selectedItem.url, selectedItem.title, logger);
|
|
486
|
+
if (result.success) {
|
|
487
|
+
const lines = [];
|
|
488
|
+
lines.push(`✅ 转存成功!`);
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push(`📺 ${selectedItem.title}`);
|
|
491
|
+
lines.push(`🔗 ${result.shareUrl}`);
|
|
492
|
+
await internal.sendGroupMsg(groupId, [
|
|
493
|
+
{ type: "at", data: { qq: userId } },
|
|
494
|
+
{ type: "text", data: { text: "\n" + lines.join("\n") } }
|
|
495
|
+
]);
|
|
496
|
+
searchSessions.delete(sessionKey);
|
|
497
|
+
} else {
|
|
498
|
+
const lines = [];
|
|
499
|
+
lines.push(`⚠️ ${result.message},请重新选择资源。`);
|
|
500
|
+
await internal.sendGroupMsg(groupId, [
|
|
501
|
+
{ type: "at", data: { qq: userId } },
|
|
502
|
+
{ type: "text", data: { text: "\n" + lines.join("\n") } }
|
|
503
|
+
]);
|
|
504
|
+
}
|
|
505
|
+
} catch (error) {
|
|
506
|
+
logger.error("转存失败:", error);
|
|
507
|
+
let errorMessage = "转存服务异常";
|
|
508
|
+
if (error?.response?.data?.message) {
|
|
509
|
+
errorMessage = error.response.data.message;
|
|
510
|
+
} else if (error?.code === "ETIMEDOUT" || error?.message?.includes("timeout")) {
|
|
511
|
+
errorMessage = "转存请求超时,请稍后重试";
|
|
512
|
+
} else if (error?.message) {
|
|
513
|
+
errorMessage = error.message;
|
|
514
|
+
}
|
|
515
|
+
const lines = [];
|
|
516
|
+
lines.push(`⚠️ ${errorMessage},请重新选择资源。`);
|
|
517
|
+
await internal.sendGroupMsg(groupId, [
|
|
518
|
+
{ type: "at", data: { qq: userId } },
|
|
519
|
+
{ type: "text", data: { text: "\n" + lines.join("\n") } }
|
|
520
|
+
]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
__name(handleResourceSelection, "handleResourceSelection");
|
|
524
|
+
async function transferAndShare(ctx, config, shareUrl, title, logger) {
|
|
525
|
+
const headers = getQuarkHeaders(config.quarkCookie);
|
|
526
|
+
const match = shareUrl.match(/\/s\/([a-zA-Z0-9]+)/);
|
|
527
|
+
if (!match) {
|
|
528
|
+
return { success: false, message: "无效的分享链接" };
|
|
529
|
+
}
|
|
530
|
+
const pwdId = match[1];
|
|
531
|
+
try {
|
|
532
|
+
let stokenRes;
|
|
533
|
+
try {
|
|
534
|
+
stokenRes = await ctx.http.post(
|
|
535
|
+
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token",
|
|
536
|
+
{ passcode: "", pwd_id: pwdId },
|
|
537
|
+
{
|
|
538
|
+
headers,
|
|
539
|
+
params: { pr: "ucpro", fr: "pc", uc_param_str: "" },
|
|
540
|
+
timeout: 3e4
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error?.response?.data?.message) {
|
|
545
|
+
return { success: false, message: error.response.data.message };
|
|
546
|
+
}
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
if (stokenRes.status !== 200) {
|
|
550
|
+
const msg = stokenRes.message === "require login [guest]" ? "夸克Cookie已过期,请更新" : stokenRes.message;
|
|
551
|
+
return { success: false, message: msg };
|
|
552
|
+
}
|
|
553
|
+
const stoken = stokenRes.data.stoken.replace(/ /g, "+");
|
|
554
|
+
let detailRes;
|
|
555
|
+
try {
|
|
556
|
+
detailRes = await ctx.http.get(
|
|
557
|
+
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail",
|
|
558
|
+
{
|
|
559
|
+
headers,
|
|
560
|
+
params: {
|
|
561
|
+
pr: "ucpro",
|
|
562
|
+
fr: "pc",
|
|
563
|
+
uc_param_str: "",
|
|
564
|
+
pwd_id: pwdId,
|
|
565
|
+
stoken,
|
|
566
|
+
pdir_fid: "0",
|
|
567
|
+
force: "0",
|
|
568
|
+
_page: "1",
|
|
569
|
+
_size: "100",
|
|
570
|
+
_fetch_banner: "1",
|
|
571
|
+
_fetch_share: "1",
|
|
572
|
+
_fetch_total: "1",
|
|
573
|
+
_sort: "file_type:asc,updated_at:desc"
|
|
574
|
+
},
|
|
575
|
+
timeout: 3e4
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error?.response?.data?.message) {
|
|
580
|
+
return { success: false, message: error.response.data.message };
|
|
581
|
+
}
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
if (detailRes.status !== 200) {
|
|
585
|
+
return { success: false, message: detailRes.message || "获取分享详情失败" };
|
|
586
|
+
}
|
|
587
|
+
const fileList = detailRes.data.list || [];
|
|
588
|
+
if (fileList.length === 0) {
|
|
589
|
+
return { success: false, message: "分享内容为空" };
|
|
590
|
+
}
|
|
591
|
+
const fidList = fileList.map((f) => f.fid);
|
|
592
|
+
const fidTokenList = fileList.map((f) => f.share_fid_token);
|
|
593
|
+
const shareTitle = detailRes.data.share?.title || title;
|
|
594
|
+
let saveRes;
|
|
595
|
+
try {
|
|
596
|
+
saveRes = await ctx.http.post(
|
|
597
|
+
"https://drive-pc.quark.cn/1/clouddrive/share/sharepage/save",
|
|
598
|
+
{
|
|
599
|
+
fid_list: fidList,
|
|
600
|
+
fid_token_list: fidTokenList,
|
|
601
|
+
to_pdir_fid: config.saveFolderId || "0",
|
|
602
|
+
pwd_id: pwdId,
|
|
603
|
+
stoken,
|
|
604
|
+
pdir_fid: "0",
|
|
605
|
+
scene: "link"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
headers,
|
|
609
|
+
params: { entry: "update_share", pr: "ucpro", fr: "pc", uc_param_str: "" },
|
|
610
|
+
timeout: 6e4
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error?.response?.data?.message) {
|
|
615
|
+
return { success: false, message: error.response.data.message };
|
|
616
|
+
}
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
if (saveRes.status !== 200) {
|
|
620
|
+
if (saveRes.message?.includes("capacity limit")) {
|
|
621
|
+
return { success: false, message: "网盘容量不足" };
|
|
622
|
+
}
|
|
623
|
+
return { success: false, message: saveRes.message || "转存失败" };
|
|
624
|
+
}
|
|
625
|
+
const saveTaskId = saveRes.data.task_id;
|
|
626
|
+
let saveTaskData = null;
|
|
627
|
+
for (let i = 0; i < 50; i++) {
|
|
628
|
+
const taskRes = await ctx.http.get(
|
|
629
|
+
"https://drive-pc.quark.cn/1/clouddrive/task",
|
|
630
|
+
{
|
|
631
|
+
headers,
|
|
632
|
+
params: {
|
|
633
|
+
pr: "ucpro",
|
|
634
|
+
fr: "pc",
|
|
635
|
+
uc_param_str: "",
|
|
636
|
+
task_id: saveTaskId,
|
|
637
|
+
retry_index: i
|
|
638
|
+
},
|
|
639
|
+
timeout: 3e4
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
if (taskRes.status === 200 && taskRes.data?.status === 2) {
|
|
643
|
+
saveTaskData = taskRes.data;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
if (taskRes.message?.includes("capacity limit")) {
|
|
647
|
+
return { success: false, message: "网盘容量不足" };
|
|
648
|
+
}
|
|
649
|
+
await sleep(500);
|
|
650
|
+
}
|
|
651
|
+
if (!saveTaskData) {
|
|
652
|
+
return { success: false, message: "转存超时" };
|
|
653
|
+
}
|
|
654
|
+
const savedFids = saveTaskData.save_as?.save_as_top_fids || [];
|
|
655
|
+
if (savedFids.length === 0) {
|
|
656
|
+
return { success: false, message: "转存结果为空" };
|
|
657
|
+
}
|
|
658
|
+
const shareCreateRes = await ctx.http.post(
|
|
659
|
+
"https://drive-pc.quark.cn/1/clouddrive/share",
|
|
660
|
+
{
|
|
661
|
+
fid_list: savedFids,
|
|
662
|
+
expired_type: config.expiredType || 1,
|
|
663
|
+
title: shareTitle,
|
|
664
|
+
url_type: 1
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
headers,
|
|
668
|
+
params: { pr: "ucpro", fr: "pc", uc_param_str: "" },
|
|
669
|
+
timeout: 3e4
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
if (shareCreateRes.status !== 200) {
|
|
673
|
+
return { success: false, message: shareCreateRes.message || "创建分享失败" };
|
|
674
|
+
}
|
|
675
|
+
const shareTaskId = shareCreateRes.data.task_id;
|
|
676
|
+
let shareTaskData = null;
|
|
677
|
+
for (let i = 0; i < 50; i++) {
|
|
678
|
+
const taskRes = await ctx.http.get(
|
|
679
|
+
"https://drive-pc.quark.cn/1/clouddrive/task",
|
|
680
|
+
{
|
|
681
|
+
headers,
|
|
682
|
+
params: {
|
|
683
|
+
pr: "ucpro",
|
|
684
|
+
fr: "pc",
|
|
685
|
+
uc_param_str: "",
|
|
686
|
+
task_id: shareTaskId,
|
|
687
|
+
retry_index: i
|
|
688
|
+
},
|
|
689
|
+
timeout: 3e4
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
if (taskRes.status === 200 && taskRes.data?.status === 2) {
|
|
693
|
+
shareTaskData = taskRes.data;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
await sleep(500);
|
|
697
|
+
}
|
|
698
|
+
if (!shareTaskData || !shareTaskData.share_id) {
|
|
699
|
+
return { success: false, message: "获取分享ID超时" };
|
|
700
|
+
}
|
|
701
|
+
const passwordRes = await ctx.http.post(
|
|
702
|
+
"https://drive-pc.quark.cn/1/clouddrive/share/password",
|
|
703
|
+
{ share_id: shareTaskData.share_id },
|
|
704
|
+
{
|
|
705
|
+
headers,
|
|
706
|
+
params: { pr: "ucpro", fr: "pc", uc_param_str: "" },
|
|
707
|
+
timeout: 3e4
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
if (passwordRes.status !== 200) {
|
|
711
|
+
return { success: false, message: passwordRes.message || "获取分享链接失败" };
|
|
712
|
+
}
|
|
713
|
+
const finalShareUrl = passwordRes.data.share_url;
|
|
714
|
+
return { success: true, shareUrl: finalShareUrl };
|
|
715
|
+
} catch (error) {
|
|
716
|
+
logger.error("转存出错:", error);
|
|
717
|
+
return { success: false, message: error?.message || "转存过程出错" };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
__name(transferAndShare, "transferAndShare");
|
|
721
|
+
function sleep(ms) {
|
|
722
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
723
|
+
}
|
|
724
|
+
__name(sleep, "sleep");
|
|
725
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
726
|
+
0 && (module.exports = {
|
|
727
|
+
Config,
|
|
728
|
+
apply,
|
|
729
|
+
name
|
|
730
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-quark-search",
|
|
3
|
+
"description": "夸克网盘资源搜索转存插件",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"chatbot",
|
|
14
|
+
"koishi",
|
|
15
|
+
"plugin",
|
|
16
|
+
"quark",
|
|
17
|
+
"网盘"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc"
|
|
21
|
+
},
|
|
22
|
+
"koishi": {
|
|
23
|
+
"description": {
|
|
24
|
+
"zh": "夸克网盘资源搜索转存插件,支持自定义API和Cookie"
|
|
25
|
+
},
|
|
26
|
+
"service": {
|
|
27
|
+
"required": [
|
|
28
|
+
"http"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"koishi": "^4.18.9"
|
|
34
|
+
}
|
|
35
|
+
}
|