feat(role): 增加用户绑定角色信息查询与作战表展示

- 逆向并实现虎牙活动绑角色接口 webActUI.checkUserBindGameAccount,支持精准解析游戏角色名、区服和平台
- 增加本地持久化 roles.json 存储,启动即回显角色信息无需重复请求
- 前端账号作战表与 CK 管理列表整合角色展示([微信-安卓] 角色名),直观区分账号归属
- 工具栏增加「查询角色」按钮,并以兑换记录中的历史账号作为双重兜底
- 升级版本至 v0.5.5 并成功交叉编译 Windows 产物
This commit is contained in:
yml2213
2026-10-19 12:09:24 +08:00
parent c0be784be9
commit 7dadcd1644
19 changed files with 1032 additions and 31 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "huya-go-frontend",
"version": "0.5.4",
"version": "0.5.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "huya-go-frontend",
"version": "0.5.4",
"version": "0.5.5",
"dependencies": {
"antd": "^5.26.2",
"lucide-react": "^0.468.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "huya-go-frontend",
"private": true,
"version": "0.5.4",
"version": "0.5.5",
"type": "module",
"scripts": {
"dev": "vite --host 127.0.0.1",
+1 -1
View File
@@ -1 +1 @@
e0e233c05b00ef1f530e32412926f966
a9d40923be4354a749fbbe0c2b9f924e
+176 -25
View File
@@ -36,12 +36,14 @@ import {
Square,
Settings,
Trash2,
UserCheck,
Zap,
} from 'lucide-react'
import {
CheckCookies,
DefaultConfig,
Exchange,
GetCachedRoles,
GetInstanceInfo,
GetLogFilePath,
GetLogs,
@@ -52,6 +54,7 @@ import {
ListPrizes,
OpenLogDir,
PreviewAccounts,
QueryRoles,
QueryScores,
ReconcileScores,
SetLogLevel,
@@ -144,6 +147,17 @@ type CookieCheckResult = {
checkedAt: number
}
type RoleItem = {
uid: number
role_name?: string
area_name?: string
plat_name?: string
display_name?: string
game_name?: string
is_bound?: boolean
updated_at?: number
}
type AccountPreview = {
line: number
uid: number
@@ -153,6 +167,7 @@ type AccountPreview = {
checkStatus?: CookieCheckStatus
checkMessage?: string
checkedAt?: number
role?: RoleItem
}
type CookieEntry = {
@@ -165,6 +180,7 @@ type CookieEntry = {
checkMessage?: string
checkedAt?: number
updatedAt: number
role?: RoleItem
}
type LogEntry = {
@@ -752,6 +768,7 @@ export default function Dashboard() {
const [productSearch, setProductSearch] = useState('')
const [scores, setScores] = useState<ScoreResult[]>([])
const [records, setRecords] = useState<RecordResult[]>([])
const [rolesByUID, setRolesByUID] = useState<Map<number, RoleItem>>(new Map())
const [panelTab, setPanelTab] = useState<string>('logs')
const [recordModalData, setRecordModalData] = useState<{
open: boolean
@@ -922,6 +939,20 @@ export default function Dashboard() {
if (savedCookies.length > 0) {
applyCookieEntries(savedCookies, true, true).catch((err: unknown) => message.error(String(err)))
}
GetCachedRoles()
.then((cachedRoles) => {
if (!cachedRoles) return
const m = new Map<number, RoleItem>()
for (const [k, v] of Object.entries(cachedRoles)) {
const uid = Number(k)
if (uid) m.set(uid, v as RoleItem)
}
setRolesByUID(m)
})
.catch((err: unknown) => {
console.warn('读取本地角色缓存失败:', err)
})
})
.catch((err: unknown) => message.error(String(err)))
@@ -1336,6 +1367,56 @@ export default function Dashboard() {
}
}
const queryAccountRoles = async () => {
if (running) {
message.warning('抢购运行中,请先停止再查询绑定角色')
return
}
const targetAccounts =
selectedAccountLines.length > 0
? accounts.filter((item) => selectedAccountLines.includes(item.line))
: accounts
if (targetAccounts.length === 0) {
message.warning('没有可查询的账号凭据')
return
}
setLoading('roles')
try {
const values = config()
if (selectedAccountLines.length > 0) {
const lines = targetAccounts
.map((a) => cookieEntries[a.line - 1]?.text || '')
.filter(Boolean)
values.cookieText = lines.join('\n')
}
const results = (await QueryRoles(values)) as RoleItem[]
setRolesByUID((prev) => {
const next = new Map(prev)
results.forEach((r) => {
if (r.uid) {
next.set(Number(r.uid), {
uid: Number(r.uid),
role_name: r.role_name,
area_name: r.area_name,
plat_name: r.plat_name,
display_name: r.display_name,
game_name: r.game_name,
is_bound: r.is_bound,
})
}
})
return next
})
const boundCount = results.filter((r) => r.is_bound && r.display_name).length
message.success(`角色查询完成:${results.length} 个账号,${boundCount} 个已绑定角色`)
} catch (err: unknown) {
message.error(`查询角色失败: ${String(err)}`)
} finally {
setLoading('')
}
}
const clearExpiredCookies = () => {
if (failedCookieCount === 0) return
modal.confirm({
@@ -1607,22 +1688,42 @@ export default function Dashboard() {
const accountColumns: ColumnsType<AccountPreview> = useMemo(
() => [
{
title: '账号',
width: 130,
render: (_, row) => (
<div>
<strong>{row.uid || `第${row.line}行`}</strong>
<div className="muted">
{isCookieCheckUnusable(row.checkStatus)
? row.checkMessage || 'CK 已失效'
: row.valid
? row.checkStatus === 'valid'
? 'CK 可用'
: '已加入任务'
: row.error || '未选择'}
title: '账号 / 角色',
width: 155,
render: (_, row) => {
const role = rolesByUID.get(row.uid)
return (
<div>
<strong>{row.uid || `第${row.line}行`}</strong>
{role?.display_name ? (
<div style={{ marginTop: 2 }}>
<Tag
color="cyan"
style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}
title={`游戏: ${role.game_name || '和平精英'}\n角色: ${role.role_name || ''}\n区服: ${role.area_name || ''} ${role.plat_name || ''}`}
>
{role.display_name}
</Tag>
</div>
) : role?.is_bound === false ? (
<div style={{ marginTop: 2 }}>
<Tag color="default" style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}>
未绑定角色
</Tag>
</div>
) : null}
<div className="muted" style={{ marginTop: 2 }}>
{isCookieCheckUnusable(row.checkStatus)
? row.checkMessage || 'CK 已失效'
: row.valid
? row.checkStatus === 'valid'
? 'CK 可用'
: '已加入任务'
: row.error || '未选择'}
</div>
</div>
</div>
),
)
},
},
{
title: '凭据',
@@ -1748,22 +1849,43 @@ export default function Dashboard() {
running,
scoreMap,
watchedName,
rolesByUID,
],
)
const cookieColumns: ColumnsType<CookieEntry> = useMemo(
() => [
{
title: '账号',
width: 140,
render: (_, row) => (
<div>
<strong>{row.uid || '-'}</strong>
<div className="muted">
{isCookieCheckUnusable(row.checkStatus) ? 'CK 已失效' : row.valid ? '可解析 UID' : row.error || '待校验'}
title: '账号 / 角色',
width: 155,
render: (_, row) => {
const role = rolesByUID.get(row.uid)
return (
<div>
<strong>{row.uid || '-'}</strong>
{role?.display_name ? (
<div style={{ marginTop: 2 }}>
<Tag
color="cyan"
style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}
title={`游戏: ${role.game_name || '和平精英'}\n角色: ${role.role_name || ''}\n区服: ${role.area_name || ''} ${role.plat_name || ''}`}
>
{role.display_name}
</Tag>
</div>
) : role?.is_bound === false ? (
<div style={{ marginTop: 2 }}>
<Tag color="default" style={{ fontSize: 11, padding: '0 4px', margin: 0, lineHeight: '18px' }}>
未绑定角色
</Tag>
</div>
) : null}
<div className="muted" style={{ marginTop: 2 }}>
{isCookieCheckUnusable(row.checkStatus) ? 'CK 已失效' : row.valid ? '可解析 UID' : row.error || '待校验'}
</div>
</div>
</div>
),
)
},
},
{
title: '格式',
@@ -1812,7 +1934,7 @@ export default function Dashboard() {
),
},
],
[cookieEntries],
[cookieEntries, rolesByUID],
)
const prizeColumns: ColumnsType<PrizeInfo> = useMemo(
@@ -2266,6 +2388,14 @@ export default function Dashboard() {
>
CK 有效性
</Button>
<Button
icon={<UserCheck size={16} />}
loading={loading === 'roles'}
disabled={running || cookieEntries.length === 0}
onClick={queryAccountRoles}
>
查询角色
</Button>
<Button
icon={<ClipboardList size={16} />}
loading={loading === 'records'}
@@ -2280,6 +2410,27 @@ export default function Dashboard() {
const list = (data || []) as RecordResult[]
setRecords(list)
setPanelTab('records')
setRolesByUID((prev) => {
const next = new Map(prev)
let changed = false
list.forEach((item) => {
if (!next.get(item.uid)?.role_name && item.records?.length) {
for (const rec of item.records) {
if (rec.account && rec.account.trim()) {
next.set(item.uid, {
uid: item.uid,
role_name: rec.account.trim(),
display_name: rec.account.trim() + ' (历史)',
is_bound: true,
})
changed = true
break
}
}
}
})
return changed ? next : prev
})
const total = list.reduce((sum, item) => sum + (item.records?.length || 0), 0)
message.success(`查询完成:${list.length} 个账号,共 ${total} 条兑换记录`)
})
+6
View File
@@ -10,6 +10,8 @@ export function DefaultConfig():Promise<gui.Config>;
export function Exchange(arg1:gui.Config):Promise<Array<gui.ExchangeResult>>;
export function GetCachedRoles():Promise<Record<string, gui.RoleInfo>>;
export function GetInstanceInfo():Promise<gui.InstanceInfo>;
export function GetLogFilePath():Promise<string>;
@@ -30,6 +32,10 @@ export function OpenLogDir():Promise<void>;
export function PreviewAccounts(arg1:string):Promise<Array<gui.AccountPreview>>;
export function QueryAccountRoles(arg1:gui.Config):Promise<Array<gui.RoleResult>>;
export function QueryRoles(arg1:gui.Config):Promise<Array<gui.RoleResult>>;
export function QueryScores(arg1:gui.Config):Promise<Array<gui.ScoreResult>>;
export function ReconcileScores(arg1:gui.Config):Promise<Array<score.ReconcileItem>>;
+12
View File
@@ -14,6 +14,10 @@ export function Exchange(arg1) {
return window['go']['gui']['App']['Exchange'](arg1);
}
export function GetCachedRoles() {
return window['go']['gui']['App']['GetCachedRoles']();
}
export function GetInstanceInfo() {
return window['go']['gui']['App']['GetInstanceInfo']();
}
@@ -54,6 +58,14 @@ export function PreviewAccounts(arg1) {
return window['go']['gui']['App']['PreviewAccounts'](arg1);
}
export function QueryAccountRoles(arg1) {
return window['go']['gui']['App']['QueryAccountRoles'](arg1);
}
export function QueryRoles(arg1) {
return window['go']['gui']['App']['QueryRoles'](arg1);
}
export function QueryScores(arg1) {
return window['go']['gui']['App']['QueryScores'](arg1);
}
+171
View File
@@ -260,6 +260,52 @@ export namespace gui {
return a;
}
}
export class RoleResult {
uid: number;
role_name: string;
area_name: string;
plat_name: string;
display_name: string;
game_name: string;
is_bound: boolean;
resp?: huya.CheckUserBindGameAccountResp;
error?: string;
static createFrom(source: any = {}) {
return new RoleResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.uid = source["uid"];
this.role_name = source["role_name"];
this.area_name = source["area_name"];
this.plat_name = source["plat_name"];
this.display_name = source["display_name"];
this.game_name = source["game_name"];
this.is_bound = source["is_bound"];
this.resp = this.convertValues(source["resp"], huya.CheckUserBindGameAccountResp);
this.error = source["error"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class ScoreResult {
uid: number;
score: number;
@@ -341,6 +387,128 @@ export namespace huya {
this.total = source["total"];
}
}
export class GameRole {
role_id: string;
role_name: string;
area_name: string;
plat_name: string;
partition_name: string;
game_serial: string;
static createFrom(source: any = {}) {
return new GameRole(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.role_id = source["role_id"];
this.role_name = source["role_name"];
this.area_name = source["area_name"];
this.plat_name = source["plat_name"];
this.partition_name = source["partition_name"];
this.game_serial = source["game_serial"];
}
}
export class GameAccount {
open_id: string;
app_id: string;
type: string;
face_url: string;
nick: string;
access_token: string;
extend_info: string;
static createFrom(source: any = {}) {
return new GameAccount(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.open_id = source["open_id"];
this.app_id = source["app_id"];
this.type = source["type"];
this.face_url = source["face_url"];
this.nick = source["nick"];
this.access_token = source["access_token"];
this.extend_info = source["extend_info"];
}
}
export class GameAccountData {
is_bind_account: number;
is_bind_role: number;
is_need_act_check: number;
game_account: GameAccount;
game_role: GameRole;
change_bind_time: number;
static createFrom(source: any = {}) {
return new GameAccountData(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.is_bind_account = source["is_bind_account"];
this.is_bind_role = source["is_bind_role"];
this.is_need_act_check = source["is_need_act_check"];
this.game_account = this.convertValues(source["game_account"], GameAccount);
this.game_role = this.convertValues(source["game_role"], GameRole);
this.change_bind_time = source["change_bind_time"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class CheckUserBindGameAccountResp {
status: number;
msg: string;
account_data: GameAccountData;
game_name: string;
static createFrom(source: any = {}) {
return new CheckUserBindGameAccountResp(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.status = source["status"];
this.msg = source["msg"];
this.account_data = this.convertValues(source["account_data"], GameAccountData);
this.game_name = source["game_name"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class ExchangeInfo {
pid: number;
exchange_score: number;
@@ -357,6 +525,9 @@ export namespace huya {
this.desc = source["desc"];
}
}
export class PrizeTag {
id: number;
name: string;