Files
Henry_Du af50ff1d95 feat(kugou): 支持粘贴浏览器 Cookie 绑定酷狗(不再必须扫码)
站长要求去掉扫码:扫码必须拿另一台装了酷狗 App 的手机,很别扭。

技术上完全可行 —— kuGouFetch() 本来就是**无状态**的:它把库里
kugouAdminCookie 放进请求头、kugouAdminToken 放进 query 去调 sidecar。
扫码只是获取 cookie 的一种方式,不是同步的前置条件。

新增 POST /api/admin/kugou/bind-cookie {cookie, token?}:
- 写入前先用这份凭证探一次 /login/status,无效/过期的凭证**不落库**
  (避免出现看起来绑定了但同步全失败的假绑定)
- 有效则写入 systemSettings 的 kugouAdmin* 字段(与 qr-check 完全相同的落点)+ 刷新缓存
- 连不上 sidecar 时返回 502 并提示去「服务设置」检查酷狗 API 地址
- 只保存会话 Cookie,**不接收也不保存酷狗账号密码**

qr-create / qr-check 保留未删(网页版可能还在用)。
2026-09-30 19:09:26 +08:00

92 lines
3.5 KiB
TypeScript

import { eq } from 'drizzle-orm'
import { db } from '~/drizzle/db'
import { systemSettings } from '~/drizzle/schema'
import { CacheService } from '~~/server/services/cacheService'
import { kuGouFetch } from '~~/server/services/kugouService'
/**
* 粘贴 Cookie / Token 绑定酷狗 —— 不再需要扫码。
*
* 为什么可以这样做:`kuGouFetch()` 是**无状态**的 —— 它把库里存的
* `kugouAdminCookie` 放进请求头、`kugouAdminToken` 放进 query,去调 sidecar。
* 也就是说"扫码"只是**获取 cookie 的一种方式**;只要拿到一份有效 cookie,
* 后续同步的行为与扫码绑定完全一致。
*
* 好处:管理员在**电脑浏览器**上登录酷狗(滑块/短信验证码都能过)→
* 从 devtools 复制 Cookie 粘贴过来即可,不依赖另一台装了酷狗 App 的手机。
*
* 安全:这里**不接收也不保存酷狗账号密码**,只保存浏览器会话 Cookie(与扫码同款),
* 且写入前会先用它探一次 `/login/status`,无效的凭证不会落库。
*/
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) throw createError({ statusCode: 401, message: '未登录' })
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role))
throw createError({ statusCode: 403, message: '仅管理员可绑定酷狗账号' })
const body = await readBody(event)
const cookie = String(body?.cookie || '').trim()
const token = String(body?.token || '').trim()
if (!cookie && !token) {
throw createError({ statusCode: 400, message: '请粘贴 Cookie(或 token)后再保存' })
}
if (cookie.length > 8000) {
throw createError({ statusCode: 400, message: 'Cookie 太长(超过 8000 字),可能粘贴错了内容' })
}
// 先用这份凭证探一次登录状态:有效才写库,避免把废值存进去导致"看起来绑定了但同步全失败"
let loginStatus: any = null
try {
loginStatus = await kuGouFetch(
'/login/status',
{ timestamp: Date.now() },
{ forceCookie: cookie || undefined, forceToken: token || undefined },
)
} catch (e: any) {
throw createError({
statusCode: 502,
message:
'用这份 Cookie 连接酷狗失败:' +
(e?.message || String(e)) +
'(如果提示连不上 sidecar,请先到「服务设置」里确认酷狗 API 地址)',
})
}
const statusOk =
Number(loginStatus?.status ?? 0) === 1 || !!(loginStatus?.data?.profile || loginStatus?.data?.account)
if (!statusOk) {
throw createError({
statusCode: 400,
message:
'这份 Cookie 无效或已过期(酷狗侧显示未登录):' +
(loginStatus?.error || loginStatus?.message || '请在浏览器里重新登录酷狗后重新复制'),
})
}
const profile = loginStatus?.data?.profile || loginStatus?.data?.account || null
const rows = await db.select().from(systemSettings).limit(1)
const patch: Record<string, any> = {
kugouAdminToken: token || null,
kugouAdminCookie: cookie || null,
kugouAdminUser: profile || null,
kugouAdminBoundAt: new Date(),
updatedAt: new Date(),
}
if (!rows[0]) {
await db.insert(systemSettings).values(patch as any)
} else {
await db.update(systemSettings).set(patch).where(eq(systemSettings.id, rows[0].id))
}
try {
const cs = CacheService.getInstance()
await cs.setSystemSettings?.({ ...(rows[0] || {}), ...patch })
} catch {}
return {
ok: true,
bound: true,
user: profile,
hint: '绑定成功,可以直接去同步歌单了',
}
})