mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2025-12-07 08:42:49 +08:00
🌱 comboToken登录
This commit is contained in:
BIN
public/platforms/mhy/launcher.webp
Normal file
BIN
public/platforms/mhy/launcher.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ToGameLogin v-model="showLoginQr" @success="tryGetTokens" />
|
||||
<ToGameLogin v-model="showLoginQr" @success="tryGetTokens" v-model:launcher="isLauncherQr" />
|
||||
<v-card class="tcu-box">
|
||||
<template #prepend>
|
||||
<v-avatar :image="userInfo.avatar" />
|
||||
@@ -100,8 +100,15 @@
|
||||
<v-list-item-title>验证码登录</v-list-item-title>
|
||||
<v-list-item-subtitle>使用手机号登录</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showLoginQr = true" append-icon="mdi-qrcode-scan">
|
||||
<v-list-item-title>扫码登录</v-list-item-title>
|
||||
<v-list-item @click="tryCodeLogin(true)">
|
||||
<v-list-item-title>扫码登录(启动器)</v-list-item-title>
|
||||
<v-list-item-subtitle>使用米游社扫码登录</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<img src="/platforms/mhy/launcher.webp" alt="launcher" class="menu-icon" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item @click="tryCodeLogin(false)" append-icon="mdi-qrcode-scan" v-if="false">
|
||||
<v-list-item-title>扫码登录(游戏)</v-list-item-title>
|
||||
<v-list-item-subtitle>使用米游社扫码登录</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -131,6 +138,7 @@ const { isLogin } = storeToRefs(useAppStore());
|
||||
const { uid, briefInfo, cookie, account } = storeToRefs(useUserStore());
|
||||
|
||||
const showLoginQr = ref<boolean>(false);
|
||||
const isLauncherQr = ref<boolean>(true);
|
||||
const accounts = shallowRef<Array<TGApp.App.Account.User>>([]);
|
||||
const gameAccounts = shallowRef<Array<TGApp.Sqlite.Account.Game>>([]);
|
||||
const userInfo = computed<TGApp.App.Account.BriefInfo>(() => {
|
||||
@@ -247,6 +255,11 @@ async function tryCaptchaLogin(): Promise<void> {
|
||||
await tryGetTokens(ck);
|
||||
}
|
||||
|
||||
async function tryCodeLogin(isLauncher: boolean): Promise<void> {
|
||||
isLauncherQr.value = isLauncher;
|
||||
showLoginQr.value = true;
|
||||
}
|
||||
|
||||
async function refreshUser(uid: string) {
|
||||
let account = await TSUserAccount.account.getAccount(uid);
|
||||
if (!account) {
|
||||
@@ -554,4 +567,9 @@ async function clearUser(user: TGApp.App.Account.User): Promise<void> {
|
||||
.tcu-btn {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
</div>
|
||||
<div class="tog-mid">
|
||||
<qrcode-vue
|
||||
v-if="codeData"
|
||||
v-if="codeUrl"
|
||||
class="tog-qr"
|
||||
:value="codeData.url"
|
||||
:value="codeUrl"
|
||||
render-as="svg"
|
||||
:background="'var(--box-bg-1)'"
|
||||
foreground="var(--box-text-1)"
|
||||
@@ -19,11 +19,14 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import TOverlay from "@comp/app/t-overlay.vue";
|
||||
import showLoading from "@comp/func/loading.js";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import { onUnmounted, shallowRef, watch } from "vue";
|
||||
import { computed, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
import hk4eReq from "@/web/request/hk4eReq.js";
|
||||
import PassportReq from "@/web/request/passportReq.js";
|
||||
import takumiReq from "@/web/request/takumiReq.js";
|
||||
|
||||
type ToGameLoginEmits = (e: "success", data: TGApp.App.Account.Cookie) => void;
|
||||
|
||||
@@ -31,8 +34,14 @@ type ToGameLoginEmits = (e: "success", data: TGApp.App.Account.Cookie) => void;
|
||||
let cycleTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
const model = defineModel<boolean>({ default: false });
|
||||
const isLauncherCode = defineModel<boolean>("launcher", { default: false });
|
||||
const emits = defineEmits<ToGameLoginEmits>();
|
||||
const codeData = shallowRef<TGApp.BBS.GameLogin.GetLoginQrData>();
|
||||
const codeUrl = ref<string>();
|
||||
const codeTicket = computed<string>(() => {
|
||||
if (!codeUrl.value) return "";
|
||||
const url = new URL(codeUrl.value);
|
||||
return url.searchParams.get("ticket") || "";
|
||||
});
|
||||
|
||||
watch(model, async (value) => {
|
||||
if (value) {
|
||||
@@ -42,24 +51,33 @@ watch(model, async (value) => {
|
||||
});
|
||||
|
||||
async function freshQr(): Promise<void> {
|
||||
const res = await PassportReq.qrLogin.create();
|
||||
let res;
|
||||
if (isLauncherCode.value) res = await PassportReq.qrLogin.create();
|
||||
else res = await hk4eReq.loginQr.create();
|
||||
console.log(res);
|
||||
if ("retcode" in res) {
|
||||
showSnackbar.error(`[${res.retcode}] ${res.message}`);
|
||||
return;
|
||||
}
|
||||
codeData.value = res;
|
||||
codeUrl.value = res.url;
|
||||
}
|
||||
|
||||
async function cycleGetData() {
|
||||
if (cycleTimer === null || !codeData.value) return;
|
||||
const res = await PassportReq.qrLogin.query(codeData.value.ticket);
|
||||
if (cycleTimer === null || codeTicket.value === "") return;
|
||||
if (isLauncherCode.value) await cycleGetDataLauncher(cycleTimer);
|
||||
else await cycleGetDataGame(cycleTimer);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
async function cycleGetDataLauncher(timer: NodeJS.Timeout): Promise<void> {
|
||||
const res = await PassportReq.qrLogin.query(codeTicket.value);
|
||||
console.log(res);
|
||||
if ("retcode" in res) {
|
||||
showSnackbar.error(`[${res.retcode}] ${res.message}`);
|
||||
if (res.retcode === -106) {
|
||||
await freshQr();
|
||||
} else {
|
||||
clearInterval(cycleTimer);
|
||||
clearInterval(timer);
|
||||
cycleTimer = null;
|
||||
model.value = false;
|
||||
}
|
||||
@@ -67,7 +85,7 @@ async function cycleGetData() {
|
||||
}
|
||||
if (res.status === "Created" || res.status === "Scanned") return;
|
||||
if (res.status === "Confirmed") {
|
||||
clearInterval(cycleTimer);
|
||||
clearInterval(timer);
|
||||
cycleTimer = null;
|
||||
const ck: TGApp.App.Account.Cookie = {
|
||||
account_id: res.user_info.aid,
|
||||
@@ -83,6 +101,54 @@ async function cycleGetData() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
async function cycleGetDataGame(timer: NodeJS.Timeout): Promise<void> {
|
||||
const res = await hk4eReq.loginQr.state(codeTicket.value);
|
||||
console.log(res);
|
||||
if ("retcode" in res) {
|
||||
showSnackbar.error(`[${res.retcode}] ${res.message}`);
|
||||
if (res.retcode === -106) {
|
||||
await freshQr();
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
cycleTimer = null;
|
||||
model.value = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (res.stat === "Init" || res.stat === "Scanned") return;
|
||||
if (res.stat === "Confirmed") {
|
||||
clearInterval(timer);
|
||||
cycleTimer = null;
|
||||
if (res.payload.proto === "Raw") {
|
||||
showSnackbar.error(`返回数据异常:${res.payload}`);
|
||||
model.value = false;
|
||||
return;
|
||||
}
|
||||
const statusRaw: TGApp.Game.Login.StatusPayloadRaw = JSON.parse(res.payload.raw);
|
||||
await showLoading.start("正在获取SToken");
|
||||
const stResp = await takumiReq.game.stoken(statusRaw);
|
||||
console.log(stResp);
|
||||
await showLoading.end();
|
||||
if ("retcode" in stResp) {
|
||||
showSnackbar.error(`[${stResp.retcode}] ${stResp.message}`);
|
||||
model.value = false;
|
||||
return;
|
||||
}
|
||||
// const ck: TGApp.App.Account.Cookie = {
|
||||
// account_id: statusRaw.uid,
|
||||
// ltuid: statusRaw.uid,
|
||||
// stuid: statusRaw.uid,
|
||||
// mid: res.user_info.mid,
|
||||
// cookie_token: "",
|
||||
// stoken: res.tokens[0].token,
|
||||
// ltoken: "",
|
||||
// };
|
||||
// emits("success", ck);
|
||||
model.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cycleTimer !== null) clearInterval(cycleTimer);
|
||||
cycleTimer = null;
|
||||
|
||||
75
src/types/Game/Login.d.ts
vendored
Normal file
75
src/types/Game/Login.d.ts
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @file types/Game/Login.d.ts
|
||||
* @description 扫码登录模块类型定义文件
|
||||
* @since Beta v0.7.0
|
||||
*/
|
||||
|
||||
declare namespace TGApp.Game.Login {
|
||||
/**
|
||||
* @description 获取登录二维码返回数据
|
||||
* @since Beta v0.7.0
|
||||
* @interface QrResp
|
||||
* @extends TGApp.BBS.Response.BaseWithData
|
||||
* @property {QrRes} data 数据
|
||||
* @return QrResp
|
||||
*/
|
||||
type QrResp = TGApp.BBS.Response.BaseWithData & { data: QrRes };
|
||||
|
||||
/**
|
||||
* @description 获取登录二维码返回数据
|
||||
* @since Beta v0.7.0
|
||||
* @interface QrRes
|
||||
* @property {string} url 二维码链接
|
||||
* @return QrRes
|
||||
*/
|
||||
type QrRes = { url: string };
|
||||
|
||||
/**
|
||||
* @description 获取登录状态返回数据
|
||||
* @since Beta v0.7.0
|
||||
* @interface StatusResp
|
||||
* @extends TGApp.BBS.Response.BaseWithData
|
||||
* @property {StatusRes} data 数据
|
||||
* @return StatusResp
|
||||
*/
|
||||
type StatusResp = TGApp.BBS.Response.BaseWithData & { data: StatusRes };
|
||||
|
||||
/**
|
||||
* @description 二维码状态
|
||||
* @since Beta v0.7.0
|
||||
* @interface QrStatus
|
||||
* @return QrStatus
|
||||
*/
|
||||
type QrStatus = "Init" | "Scanned" | "Confirmed";
|
||||
|
||||
/**
|
||||
* @description 获取登录状态返回数据
|
||||
* @since Beta v0.7.0
|
||||
* @interface StatusRes
|
||||
* @property {string} stat 状态 // Init: 未扫码,Scanned: 已扫码,Confirmed: 已确认
|
||||
* @property {string} payload 状态数据
|
||||
* @return StatusRes
|
||||
*/
|
||||
type StatusRes = { stat: QrStatus; payload: StatusPayload };
|
||||
|
||||
/**
|
||||
* @description 获取登录状态返回数据
|
||||
* @since Beta v0.7.0
|
||||
* @interface StatusPayload
|
||||
* @property {string} ext 未知
|
||||
* @property {string} proto 未知
|
||||
* @property {string} raw 序列化数据,反序列化后是 StatusPayloadRaw
|
||||
* @return StatusPayload
|
||||
*/
|
||||
type StatusPayload = { ext: string; proto: string; raw: string };
|
||||
|
||||
/**
|
||||
* @description 反序列化后的登录状态数据
|
||||
* @since Beta v0.7.0
|
||||
* @interface StatusPayloadRaw
|
||||
* @property {string} uid 用户 UID
|
||||
* @property {string} token 用户 token
|
||||
* @return StatusPayloadRaw
|
||||
*/
|
||||
type StatusPayloadRaw = { uid: string; token: string };
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* @file web/request/hk4eReq.ts
|
||||
* @description Hk4eApi 请求模块
|
||||
* @since Beta v0.6.3
|
||||
* @since Beta v0.7.0
|
||||
*/
|
||||
|
||||
import TGHttp from "@/utils/TGHttp.js";
|
||||
import { getDeviceInfo } from "@/utils/toolFunc.js";
|
||||
|
||||
export enum AnnoServer {
|
||||
CN_ISLAND = "cn_gf01",
|
||||
@@ -20,6 +21,7 @@ export type AnnoLang = "zh-cn" | "zh-tw" | "en" | "ja";
|
||||
const AnnoApi: Readonly<string> = "https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api";
|
||||
const AnnoApiGlobal: Readonly<string> =
|
||||
"https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api";
|
||||
const SdkApi: Readonly<string> = "https://hk4e-sdk.mihoyo.com/hk4e_cn/";
|
||||
|
||||
/**
|
||||
* @description 判断是否为国内服务器
|
||||
@@ -137,9 +139,48 @@ async function getGachaLog(
|
||||
return resp.data.list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取登录二维码
|
||||
* @since Beta v0.7.0
|
||||
* @param {string} appId 应用 ID // 目前只有2/7能用
|
||||
* @returns {Promise<TGApp.Game.Login.QrRes|TGApp.BBS.Response.Base>}
|
||||
*/
|
||||
async function fetchPandaQr(
|
||||
appId: string = "2",
|
||||
): Promise<TGApp.Game.Login.QrRes | TGApp.BBS.Response.Base> {
|
||||
const data = { app_id: appId, device: getDeviceInfo("device_id") };
|
||||
const resp = await TGHttp<TGApp.Game.Login.QrResp>(`${SdkApi}combo/panda/qrcode/fetch`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取登录状态
|
||||
* @since Beta v0.7.0
|
||||
* @param {string} ticket 二维码 ticket
|
||||
* @param {string} appId 应用 ID
|
||||
* @returns {Promise<TGApp.BBS.Response.Base|TGApp.Game.Login.StatusRes>}
|
||||
*/
|
||||
async function queryPandaQr(
|
||||
ticket: string,
|
||||
appId: string = "2",
|
||||
): Promise<TGApp.BBS.Response.Base | TGApp.Game.Login.StatusRes> {
|
||||
const data = { app_id: appId, ticket, device: getDeviceInfo("device_id") };
|
||||
const resp = await TGHttp<TGApp.Game.Login.StatusResp>(`${SdkApi}combo/panda/qrcode/query`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
const Hk4eApi = {
|
||||
anno: { list: getAnnoList, content: getAnnoContent },
|
||||
gacha: getGachaLog,
|
||||
loginQr: { create: fetchPandaQr, state: queryPandaQr },
|
||||
};
|
||||
|
||||
export default Hk4eApi;
|
||||
|
||||
@@ -3,12 +3,58 @@
|
||||
* @description Takumi 相关请求函数
|
||||
* @since Beta v0.7.0
|
||||
*/
|
||||
import TGBbs from "@/utils/TGBbs.js";
|
||||
import TGHttp from "@/utils/TGHttp.js";
|
||||
import { getRequestHeader } from "@/web/utils/getRequestHeader.js";
|
||||
import { getDeviceInfo } from "@/utils/toolFunc.js";
|
||||
import { getDS, getRequestHeader } from "@/web/utils/getRequestHeader.js";
|
||||
|
||||
// TakumiApiBaseUrl => taBu
|
||||
const taBu: Readonly<string> = "https://api-takumi.mihoyo.com/";
|
||||
|
||||
/**
|
||||
* @description 根据gameToken获取stoken
|
||||
* @todo -100
|
||||
* @param {TGApp.Game.Login.StatusPayloadRaw} raw 状态数据
|
||||
* @returns {Promise<TGApp.BBS.Response.Base|string>}
|
||||
*/
|
||||
async function getSTokenByGameToken(
|
||||
raw: TGApp.Game.Login.StatusPayloadRaw,
|
||||
): Promise<TGApp.BBS.Response.Base> {
|
||||
const data = { account_id: raw.uid, game_token: raw.token };
|
||||
// const header = {
|
||||
// ...getRequestHeader(ck, "POST", JSON.stringify(data), "X6"),
|
||||
// "x-rpc-client_type": "4",
|
||||
// "x-rpc-app_id": "bll8iq97cem8",
|
||||
// "x-rpc-game_biz": "bbs_cn",
|
||||
// };
|
||||
const header = {
|
||||
"x-rpc-app_version": TGBbs.version,
|
||||
"x-rpc-aigis": "",
|
||||
"Content-Type": "application/json",
|
||||
"x-rpc-game_biz": "bbs_cn",
|
||||
"x-rpc-sys_version": "12",
|
||||
"x-rpc-device_id": getDeviceInfo("device_id"),
|
||||
"x-rpc-device_name": getDeviceInfo("device_name"),
|
||||
"x-rpc-device_model": getDeviceInfo("product"),
|
||||
"x-rpc-app_id": "bll8iq97cem8",
|
||||
"x-rpc-client_type": "4",
|
||||
"User-Agent": "okhttp/4.9.3",
|
||||
ds: getDS("POST", JSON.stringify(data), "X6", false),
|
||||
cookie: `account_id=${raw.uid};ltuid=${raw.uid};stuid=${raw.uid};game_token=${raw.token};`,
|
||||
};
|
||||
const resp = await TGHttp<TGApp.BBS.Response.Base>(
|
||||
`${taBu}account/ma-cn-session/app/getTokenByGameToken`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: header,
|
||||
body: JSON.stringify(data),
|
||||
},
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
console.log(resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据stoken获取action_ticket
|
||||
* @since Beta v0.6.3
|
||||
@@ -102,6 +148,7 @@ async function getUserGameRolesByCookie(
|
||||
const TakumiApi = {
|
||||
auth: { actionTicket: getActionTicketBySToken },
|
||||
bind: { authKey: genAuthKey, authKey2: genAuthKey2, gameRoles: getUserGameRolesByCookie },
|
||||
game: { stoken: getSTokenByGameToken },
|
||||
};
|
||||
|
||||
export default TakumiApi;
|
||||
|
||||
@@ -57,7 +57,7 @@ function getRandomNumber(min: number, max: number): number {
|
||||
* @param {boolean} isSign 是否为签名
|
||||
* @returns {string} ds
|
||||
*/
|
||||
function getDS(
|
||||
export function getDS(
|
||||
method: string,
|
||||
data: string,
|
||||
saltType: keyof typeof SaltType,
|
||||
|
||||
Reference in New Issue
Block a user