PanSou Gying 插件 HTML/数据结构深度剖析:从挑战页求解到网盘链接归一化
PanSou Gying 插件 HTML/数据结构深度剖析:从挑战页求解到网盘链接归一化
本篇技术指南以 PanSou 仓库中 plugin/gying/html结构分析.md 为骨架,结合 gying.go 的完整实现,系统讲解 Gying 搜索插件的目标站点页面/JSON 结构、三层反爬挑战(远程 PoW、内嵌 PoW、旧版哈希枚举)的求解原理、登录链路与 Cookie 生命周期、搜索页与详情接口的数据映射,以及插件内置管理页的前端行为。读完本文,你将掌握如何从 HTML 页面中提取内嵌 JSON、如何在 Go 中复现浏览器 PoW 计算、如何识别网盘类型并归一化链接,以及 PanSou 插件体系下多用户并发搜索与去重的实现思路。
1. 前提:baseURL 是当前实现的核心抽象
gying.go 当前实现不再把目标站点"死写"成一个固定域名直接抓取。代码里保留了默认值 https://www.gying.net,但运行时允许通过管理页把目标站点改成自定义域名,域名会持久化保存到 gying_config.json,后续登录、搜索、详情、预热请求全部从 baseURL 派生(见 gying.go 的 DefaultGyingBaseURL 与 GyingConfigFileName 常量)。
baseURL 在保存前会经过 normalizeBaseURL() 标准化(gying.go):
- 自动补全协议:未写
http://或https://时自动补https:// - 只允许纯域名:不允许带路径(
Path非空报错)、查询串(RawQuery非空报错)、锚点(Fragment非空报错) - 去尾部
/,空值报错
代码中直接体现这一点的核心方法有 getBaseURL()、getLoginPageURL()、getLoginAPIURL()、getWarmupDetailURL()、updateBaseURL()(gying.go)。管理页前端对应的配置动作是 get_config 与 update_config。
一个关键行为:当 updateBaseURL 检测到域名发生变化时,会调用 resetSessionsForBaseURLChange() 清空所有 scraper 实例、把用户状态置回 pending 并清空搜索缓存(gying.go),所以切换站点地址后必须重新登录。
2. 目标站点基础入口
当前插件把站点地址抽象为可配置的 baseURL,主要入口如下:
| 功能 | URL |
|---|---|
| 登录页 / 初始会话页 | {baseURL} |
| 登录接口 | {baseURL}/user/login |
| 搜索页 | {baseURL}/search?q={keyword}&type=0&mode=2 |
| 详情接口 | {baseURL}/res/downurl/{type}/{id} |
| 预热详情页 | {baseURL}/mv/wkMn |
搜索与详情请求分别拼接为 {baseURL}/search?q={keyword}&type=0&mode=2 与 {baseURL}/res/downurl/{type}/{id}(源码见 gying.go 与 gying.go)。注意 {type} 来自搜索结果中的资源类型字段(如 mv、ac、tv),{id} 是资源 ID,例如 {baseURL}/res/downurl/mv/xJe3。
3. 反爬挑战页结构
3.1 识别方式
插件通过 isBotChallengePage()(gying.go)判定挑战页,条件是"页面中存在以下 JS 片段"且正文包含任一验证文案:
正在确认你是不是机器人浏览器安全验证安全验证正在进行浏览器计算验证
挑战页可能内嵌以下 JS 片段:
const json={...};const jss=
也可能只出现远程 PoW 脚本配置:
const jss={"worker":"//static.filejin.ru/static/file/js/pow.worker-...js"};
后一种流程会通过响应头下发 browser_pow Cookie,再由前端请求 /res/pow 获取真实挑战数据。此外,isBotChallengePage 还会检查 powSolve-、pow.worker-、const jss=、/res/pow 等特征串。
当前用于提取挑战数据的正则:
challengeJSONPattern = regexp.MustCompile(`(?s)const\s+json\s*=\s*(\{.*?\})\s*;\s*const\s+jss\s*=`)
3.2 挑战数据结构
挑战 JSON 被解析为 ChallengePageData(gying.go):
type ChallengePageData struct {
ID string `json:"id"`
Challenge []string `json:"challenge"`
Diff int `json:"diff"`
Salt string `json:"salt"`
N string `json:"N"`
X string `json:"x"`
T int `json:"t"`
}
当前兼容三套验证流程:远程 PoW、内嵌 PoW、旧版哈希枚举。
旧版哈希枚举字段含义:
id:本次挑战标识challenge:目标哈希列表diff:枚举上限salt:参与哈希运算的盐值
PoW 字段含义:
id:本次挑战标识N:十六进制大整数模数x:十六进制初始值t:平方取模迭代次数
远程 PoW 的 id 不在 JSON 中返回,而是由 browser_pow Cookie 标记;验证成功后服务端返回 challenge_id 并下发 browser_verified Cookie。
solveBotChallenge()(gying.go)的分流逻辑是:先用正则尝试匹配内嵌 JSON,匹配不到则走远程 PoW;匹配到后若同时具备 id/N/x/t 字段则走内嵌 PoW,否则走旧版哈希枚举。
3.3 插件求解方式:远程 PoW
当站点只在验证页中加载 powSolve-*.js 时,真实挑战数据需要通过接口获取:
GET {baseURL}/res/pow
返回 JSON 形如:
{"N":"...","x":"...","t":800000}
插件计算完成后提交:
POST {baseURL}/res/pow
Content-Type: application/x-www-form-urlencoded
y={hex(y)}
成功响应形如:
{"success":true,"challenge_id":"..."}
对应的实现是 solveRemotePowChallenge()(gying.go),其中 buildPowURL()(gying.go)把当前请求 URL 的 Path 改写为 /res/pow 并清空 query 与 fragment。
3.4 插件求解方式:内嵌 PoW
如果页面直接内嵌 const json={id,N,x,t},worker 的核心逻辑等价于:
N = BigInt("0x" + json.N)
y = BigInt("0x" + json.x)
repeat json.t times:
y = (y * y) % N
当前插件不启动浏览器,也不使用 Playwright / Puppeteer / Selenium,而是在 Go 中用 math/big 直接复现上述计算(computePowResult(),gying.go)。计算结束后,为贴近前端脚本行为,如果总耗时不足 3 秒,会补齐等待时间。内嵌 PoW 会向当前请求 URL 提交:
action=verify&id={id}&y={hex(y)}
验证成功后,服务端下发 browser_verified,原请求再重试一次。solvePowChallenge()(gying.go)负责该流程。
3.5 插件求解方式:旧版哈希枚举
solveLegacyHashChallenge()(gying.go)的策略很直接:
- 从
0..diff顺序枚举nonce - 计算
sha256(strconv.Itoa(nonce) + salt) - 把命中的
nonce按原顺序回填(支持多个目标哈希,用runtime.GOMAXPROCS(0)数量的 worker 并行枚举,命中数达到目标即提前结束) - 向当前请求 URL 提交:
action=verify&id={id}&nonce[]={n1}&nonce[]={n2}...
验证成功后,原请求会再重试一次。挑战提交统一走 submitChallengeVerification()(gying.go),它会检查响应是否为新的挑战页(防止验证循环)、解析 {"success":...} JSON 并校验 success 字段。
3.6 挑战重试的统一入口
所有关键请求都经过 requestWithChallengeRetry()(gying.go),最多重试 2 次:首次请求若命中挑战页则求解后重试,第二次仍命中则报"重试后仍然进入机器人验证页"。值得注意的实现细节:GET 请求统一使用 scraper.Get、POST 使用 scraper.Post,让"访问验证页 / 提交验证 / 重试原请求"完整经过 cloudscraper 的同一套浏览器化请求处理——这是 2026-05 登录验证循环问题修复的关键(详见 README.md)。
4. 登录链路与 Cookie 生命周期
4.1 三步登录流程
doLogin()(gying.go)完整流程:
第一步:登录页
GET {baseURL}
作用:建立会话、收集初始 Cookie(如 PHPSESSID);如果登录页本身触发挑战,也会先经过挑战求解。
第二步:登录接口
POST {baseURL}/user/login
Content-Type: application/x-www-form-urlencoded
表单参数固定为:
code=
siteid=1
dosubmit=1
cookietime=10506240
username={用户名}
password={密码}
插件以 JSON 响应中的 code == 200 作为登录成功条件。注意响应中 code 字段存在类型漂移(int/float64/int64/字符串),登录代码对此做了兼容解析(gying.go)。
第三步:预热详情页
GET {baseURL}/mv/wkMn
这一步不是取业务数据,而是为了补齐反爬链路中的额外 Cookie(如 vrg_sc、vrg_go 等)。当前实现里,登录和搜索后都会主动访问一次这个页面。
4.2 登录失效识别
插件把以下响应都视为登录失效或需重登(isLoginShell(),gying.go,配合 searchWithScraperWithRetry() 使用):
- HTTP 403
- 返回登录壳页面:
_BT.PC.HTML('login') - 返回未登录壳页面:
_BT.PC.HTML('nologin') - 页面标题或正文出现
未登录,访问受限 - 详情 JSON 里的
code == 403
一旦触发上述信号,searchWithScraperWithRetry()(gying.go)会先用已保存的 AES-GCM 加密密码重新登录(reloginUser()),再用新 scraper 重试一次搜索。
4.3 Cookie 生命周期
目标站点至少有两类不同作用的 Cookie:
- 登录态 Cookie:例如
PHPSESSID、app_auth - 验证态 Cookie:例如
browser_verified
两者不是同一层状态:
- 只有登录态,没有验证态:搜索时仍可能进入"浏览器安全验证"
- 只有验证态,没有有效登录态:搜索可能不再 challenge,但会落到
nologin
当前插件的处理链路是:
- 登录 / 重登成功后,通过
exportCookies()(gying.go,用反射访问 cloudscraper 内部http.Client的 cookie jar)导出整套 Cookie 快照并保存到用户文件 - 搜索过程中如果 challenge 求解成功,服务端补发的
browser_verified会进入 scraper 的 cookie jar - 单次搜索成功后,
syncUserCookiesFromScraper()(gying.go)会把 scraper 中最新 Cookie 再次导出并回写到用户文件 - 插件重启后,
initOrRestoreUser()(gying.go)优先用已保存 Cookie 通过createScraperWithCookies()恢复会话(反射写入 cookie jar,且用refreshOn403=false防止 cloudscraper 清空 Cookie);只有恢复失败时才回退到用户名密码重新登录
这样做的目的是尽量复用已经通过的浏览器验证结果,减少不必要的重复 challenge。
4.4 代理前提
目标站点对网络出口非常敏感。实测里,同一个 /search 地址在不同请求链上可能出现两种完全不同的表现:
- 可访问网络出口:先返回 challenge,再返回
_obj.search - 受限网络出口:直接返回由
Angie输出的404 Not Found假页
也就是说,这类 404 不能直接解释为"搜索参数错误"或"路由不存在",它很可能只是站点对当前网络出口的伪装拒绝。
为了解决这一点,applyProxyToScraper()(gying.go)会显式把主程序的 PROXY 配置(读取自 config.AppConfig.ProxyURL,对应环境变量 PROXY,见 config/config.go)应用到 cloudscraper 内部 transport。代理应用点包括:doLogin() 创建 scraper 时、createScraperWithCookies() 恢复 scraper 时、以及随后的搜索页、详情页、challenge 提交请求。
支持的代理类型与主程序一致:socks5://...、http://...、https://...。其中 socks5 通过 golang.org/x/net/proxy 的 proxy.FromURL 改写 DialContext,http/https 则直接设置 http.Transport.Proxy。
启动示例:
PROXY=socks5://127.0.0.1:7897 ENABLED_PLUGINS=gying go run .
5. 搜索页 HTML 结构
5.1 搜索地址与数据载体
GET {baseURL}/search?q={url.QueryEscape(keyword)}&type=0&mode=2
返回值不是纯 JSON,而是一个 HTML 页面,真正的数据被嵌在 JavaScript 变量里:
_obj.search={...};
对应正则(gying.go):
searchDataPattern = regexp.MustCompile(`(?s)_obj\s*\.\s*search\s*=\s*(\{.*?\})\s*;`)
5.2 SearchData 结构
插件当前真正依赖的字段如下(gying.go):
type SearchData struct {
Q string `json:"q"`
WD []string `json:"wd"`
N string `json:"n"`
L struct {
Title []string `json:"title"`
Year []int `json:"year"`
D []string `json:"d"`
I []string `json:"i"`
Info []string `json:"info"`
Daoyan []string `json:"daoyan"`
Zhuyan []string `json:"zhuyan"`
} `json:"l"`
}
关键字段含义:
| 字段 | 说明 | 当前用途 |
|---|---|---|
q |
搜索关键词 | 调试与校验 |
n |
结果数,字符串 | 调试参考 |
l.title |
标题列表 | 构建结果标题 |
l.year |
年份列表 | 标题后缀、标签 |
l.d |
资源类型列表(mv/ac/tv) | 构造详情接口 URL |
l.i |
资源 ID 列表 | 构造详情接口 URL |
l.info |
基础说明 | 生成 Content |
l.daoyan |
导演 | 生成 Content |
l.zhuyan |
主演 | 生成 Content |
5.3 插件在搜索页上的额外过滤
虽然 _obj.search 里可能返回很多资源,但 fetchAllDetails()(gying.go)还会做一层过滤:只有 title 中包含搜索关键词的条目才继续取详情,使用 strings.Contains(strings.ToLower(title), strings.ToLower(keyword))(不区分大小写,关键词提前转小写)。
6. 详情接口 JSON 结构
6.1 详情地址
GET {baseURL}/res/downurl/{resourceType}/{resourceID}
例如 {baseURL}/res/downurl/mv/xJe3。
6.2 DetailData 结构
插件当前依赖的详情结构如下(gying.go):
type DetailData struct {
Code int `json:"code"`
WP bool `json:"wp"`
Downlist struct {
IMDB string `json:"imdb"`
Type struct {
A []string `json:"a"`
B []string `json:"b"`
} `json:"type"`
Hex string `json:"hex"`
List struct {
M []string `json:"m"` // 磁力hash
T []string `json:"t"` // 资源名称
S []string `json:"s"` // 文件大小
E []interface{} `json:"e"`
P []string `json:"p"`
U []string `json:"u"`
K []interface{} `json:"k"`
N []string `json:"n"` // 更新时间
} `json:"list"`
} `json:"downlist"`
Panlist struct {
ID []string `json:"id"`
Name []string `json:"name"`
P []string `json:"p"` // 提取码数组
URL []string `json:"url"`
Type []int `json:"type"`
User []string `json:"user"`
Time []string `json:"time"`
TName []string `json:"tname"`
} `json:"panlist"`
}
6.3 插件真正使用的字段
| 字段 | 说明 | 当前用途 |
|---|---|---|
code |
状态码 | 403 时触发重登 |
panlist.url |
网盘原始链接 | 提取并归一化 |
panlist.p |
提取码 | 作为密码兜底 |
panlist.type |
类型编码 | 识别网盘类型 |
panlist.tname |
类型名称 | 类型识别兜底 |
panlist.name |
链接标题 | 构造 WorkTitle |
panlist.time |
更新时间 | 构造链接时间、结果时间 |
downlist.list.m |
磁力 hash | 构造 magnet 链接 |
downlist.list.t |
资源名 | 作为 magnet dn |
downlist.list.s |
文件大小 | 资源名兜底 |
downlist.list.n |
更新时间 | 链接时间、结果时间 |
6.4 结构兼容性说明
当前站点的详情 JSON 并不是所有字段都严格稳定。实测里 downlist.list.e 和 downlist.list.k 可能混用 int 与 string 类型,例如某些条目会返回:
e: [1, 0, 0, 9, 0]e: ["-1"]e: [0, 10, "-1", "-1"]
因此当前实现把这两个字段按 <a href="https://link.gitcode.com/i/33d0731f80d6ad4c9b134c60028417ef" target="_blank">]interface{} 宽松类型接收(见 [gying.go),避免因为单个字段的类型漂移导致整条详情 JSON 反序列化失败。
7. SearchResult 映射逻辑
buildResult()(gying.go)把搜索页和详情页数据拼装为:
model.SearchResult{
UniqueID: fmt.Sprintf("gying-%s-%s", resourceType, resourceID),
Title: titleWithYear,
Content: "info | 导演: ... | 主演: ...",
Links: extractLinks(detail, titleWithYear),
Tags: []string{year},
Channel: "",
Datetime: parsedTime,
}
具体规则:
- 如果存在年份,标题会变成
标题(2024)(中文括号拼接) Content由info / 导演: daoyan / 主演: zhuyan拼接,各部分用|连接Datetime不是直接取详情单值,而是由collectDetailTimes()(gying.go)汇总panlist.time + downlist.list.n,再经parseUpdateTime()取"最接近当前时间"(即最新)的值;若全部解析失败则回退time.Now()
model.SearchResult 与 model.Link 的定义见 model/response.go,Link.WorkTitle 用于区分同一结果下多个作品的链接。
8. 相对时间解析规则
parseRelativeTime()(gying.go)当前支持:
今天:取当天零点(baseTime.Truncate(24 * time.Hour))昨天:前一天零点N天前:baseTime.AddDate(0, 0, -days)后截断到零点N月前:AddDate(0, -months, 0)N年前:AddDate(-years, 0, 0)
结果时间和链接时间都建立在这套解析逻辑上。如果时间文本无法识别,则:
- 链接时间返回零值
time.Time{}(parseLinkTime(),gying.go) - 结果时间回退到
time.Now()
9. 网盘链接识别与归一化
9.1 类型识别优先级
determineLinkType()(gying.go)的判断顺序是:
- 先看 URL 域名 / 前缀(
pan.quark.cn→quark、drive.uc.cn→uc、pan.baidu.com→baidu、aliyundrive.com/alipan.com→aliyun、pan.xunlei.com→xunlei、cloud.189.cn/tianyi.cloud→tianyi、yun.139.com/caiyun.139.com/feixin.10086.cn→mobile、115.com/115cdn.com/anxia.com→115、123684.com等 123 系列→123、magnet:?xt=urn:btih:→magnet) - 再看
panlist.type对应的内置映射gyingPanTypeMap(gying.go:0→xunlei、1→baidu、2→quark、3→tianyi、4→mobile、5→115、6→123、7→uc、8→aliyun) - 最后用
panlist.tname做兜底(匹配"天翼/移动/彩云/百度/夸克/迅雷/阿里/115/123/uc"等中文名称)
当前映射到的标准类型包括:quark、uc、baidu、aliyun、xunlei、tianyi、mobile、115、123、magnet。
9.2 URL 归一化
normalizePanURL()(gying.go)会先删除诸如 (访问码:xxxx) 的访问码块(正则 accessCodeBlockRegex,gying.go),再按不同类型执行专用正则提取:
- 百度网盘:
pan.baidu.com/s/{shareId}(?pwd=xxxx)? - 夸克网盘:
pan.quark.cn/s/{id} - 阿里云盘 / alipan:
(www.)?(alipan|aliyundrive).com/s/{id} - 迅雷网盘:
pan.xunlei.com/s/{id}(?pwd=xxxx)? - 天翼云盘:优先提取
sharecode=重组为https://cloud.189.cn/t/{code},再尝试cloud.189.cn与tianyi.cloud正则 - 中国移动云盘 / 彩云 / 飞信:
yun.139.com/shareweb/#/w/i/{id}、caiyun.139.com、caiyun.feiXin.10086.cn - 115:
115.com、115cdn.com、anxia.com - 123 系列域名:
123684/123685/123865/123912/123pan/123592的.com或.cn - UC 网盘:
drive.uc.cn/s/{id}(?public=\d+)?
9.3 提取码逻辑
extractPassword()(gying.go)优先从原始文本里提取(extractPasswordFromText),支持:
?pwd=xxxx?password=xxxx访问码: xxxx提取码: xxxx密码: xxxx
如果原始文本里拿不到,再退回 panlist.p。normalizePassword()(gying.go)会剔除首尾标点,过滤"无提取码/无密码/无需"等语义,并校验密码为 4-8 位字母数字(exactPasswordRegex)。
10. 磁力链接构造逻辑
磁力链接来自 downlist.list.m 的 40 位十六进制 hash(magnetHashRegex,gying.go)。插件不会直接使用现成 URL,而是通过 buildMagnetURL()(gying.go)手动组装:
magnet:?xt=urn:btih:{hash}&dn={资源名}
其中资源名优先取 downlist.list.t,为空则退回 downlist.list.s(extractMagnetLinks(),gying.go);dn 参数经 url.QueryEscape 转义。hash 不匹配 40 位十六进制规则的条目会被跳过。
11. 去重策略
当前去重分两层:
链接级去重
- 网盘:
{type}:{lowercase(url)} - 磁力:
magnet:{infoHash}
同一详情内先通过 seen map 去重(extractLinks(),gying.go)。
结果级去重
最终结果以 UniqueID 去重(deduplicateResults(),gying.go):
gying-{resourceType}-{resourceID}
多用户并发搜索时,不同用户返回的同一资源会因相同 UniqueID 被合并。
12. 搜索执行链路
当前完整链路如下(入口为 SearchWithResult(),gying.go):
SearchWithResult:先查插件级关键词缓存(searchCache,可通过ext["refresh"]强制跳过)- 读取插件内关键词缓存
- 获取全部活跃用户(
getActiveUsers(),只取status == "active"且有 Cookie 的用户) - 为每个用户取 scraper,没有 scraper 时用已保存 Cookie 恢复(
createScraperWithCookies) - 请求搜索页;如果遇到浏览器验证页则自动求解后重试
- 再访问一次预热详情页刷新反爬 Cookie
- 提取
_obj.search - 并发拉取详情接口(
fetchAllDetails,带标题关键词过滤,捕获 403 信号) - 构造结果和链接(
buildResult→extractLinks) - 聚合多用户结果并按
UniqueID去重(executeSearchTasks,gying.go)
其中并发限制由两个常量控制(gying.go):
MaxConcurrentUsers = 10
MaxConcurrentDetails = 50
DebugLog = false // 排查问题时改为 true
用户数超过 MaxConcurrentUsers 时按 LastAccessAt 排序取最近活跃的前 10 个;详情请求通过容量为 MaxConcurrentDetails 的 semaphore channel 限流。
另外,插件还有后台任务:startSessionKeepAlive()(gying.go)每 3 分钟为 active 用户访问一次 {baseURL}/ 保持会话;startCleanupTask()(gying.go)每天清理 30 天未访问的 expired 用户、标记 90 天未活跃的用户为 expired。
13. 插件内置管理页 HTML 结构
gying.go 内嵌了一套插件管理页 HTML 模板(HTMLTemplate,gying.go),路由注册为 /gying/:param(RegisterWebRoutes,gying.go)。
DOM 分区
.header:标题 + 当前访问 hash(HASH_PLACEHOLDER替换)#site-section:当前站点地址显示、站点地址输入框、保存按钮(修改后清空登录状态需重登的提示)#login-section:#logged-in-view(状态/用户名/登录时间/有效期 + 退出按钮)与#not-logged-in-view(用户名密码表单 + 登录按钮)#test-section:搜索输入框 +#search-results(限制返回 10 条)- API 说明区:内嵌 curl 示例
前端行为
管理页前端通过统一的 postAction(action, extraData) 调后台:
get_statusget_configloginlogoutupdate_configtest_search
启动逻辑:
window.onload = function() {
updateStatus();
loadConfig();
startStatusPolling();
};
其中 startStatusPolling() 每 5 秒调用一次 updateStatus()。testSearch() 渲染结果时展示每条链接的类型、work_title、URL 与密码。
后端对应 handleManagePagePOST()(gying.go)的 action 分发;GET 路由对 64 位十六进制 hash 直接渲染模板,否则用 generateHash(用户名) 生成 SHA-256 hash(默认 salt 为 pansou_gying_secret_2025,可用环境变量 GYING_HASH_SALT 覆盖,gying.go)后 302 重定向。
管理页定位
这个页面的定位不是"多账号总览控制台",而是:
- 一个 hash 对应一个账号槽位
- 可在该槽位中登录、退出、测搜索
- 后端搜索时再把所有活跃槽位统一聚合(多用户结果汇总去重)
这也是当前 README.md 中最需要与旧版分析区分的地方。
14. 运行与配置实践
快速启动
# 编译并运行
go build -o pansou main.go
./pansou
# 或直接运行
go run main.go
访问管理页
http://localhost:8888/gying/你的用户名
系统自动根据用户名生成专属 64 位 hash 并重定向到 /gying/{hash}。建议收藏 hash 后的 URL 以便下次访问。使用顺序建议:先配置站点地址 → 再登录 → 最后 test_search 或正式搜索。
环境变量
# 缓存目录(默认 ./cache)
export CACHE_PATH="./cache"
# 目标站点需要海外网络时显式配置 PROXY;gying 会复用这条代理到自己的 cloudscraper 请求链
export PROXY="socks5://127.0.0.1:7897"
# Hash Salt(推荐自定义,增强安全性)
export GYING_HASH_SALT="your-custom-salt-here"
# 当前主流程的密码加解密未使用该变量;只影响可选的 Cookie 加密辅助函数
export GYING_ENCRYPTION_KEY="your-32-byte-key-here!!!!!!!!!!"
# 只启用 gying 插件
export ENABLED_PLUGINS=gying
数据存储
cache/gying_users/gying_config.json # 站点配置:base_url、updated_at
cache/gying_users/{hash}.json # 用户:hash、username、encrypted_password、cookie、status 等
其中密码使用 AES-256-GCM 加密(32 字节固定密钥)后 Base64 存储,cookie 为登录态 + 验证态 Cookie 快照,status 取值 pending/active/expired,expire_at 默认登录后 121 天。
搜索 API
# 全量搜索(含 TG 频道与插件)
curl "http://localhost:8888/api/search?kw=遮天"
# 只搜索插件(包括 gying)
curl "http://localhost:8888/api/search?kw=遮天&src=plugin"
# 管理页统一 POST 接口(hash 版)
curl -X POST "http://localhost:8888/gying/{hash}" \
-H "Content-Type: application/json" \
-d '{"action": "login", "username": "xxx", "password": "xxx"}'
以上配置、存储格式与 API 细节均可在 README.md 中查证。若要深入调试,可将 gying.go 中 DebugLog 改为 true,即可看到挑战识别、PoW 计算耗时、Cookie 收集与请求重试的完整日志链路。