用户定案:失效前用当前 cookie 换新登录态(用户无感);仅刷新失败才退扫码重登。 - core/ncm_req.c|h:eapi Cookie 头补齐会话锚点 MUSIC_U/MUSIC_A(对齐 request.rs, 补 §36.5 遗留——登录刷新/song_url 会员档依赖);新增 ncm_req_cookie_merge (patch 覆盖 base 同名 cookie,MUSIC_A/__remember_me 等不丢)。 - core/ncm_login.c|h:ncm_login_refresh(eapi /api/login/token/refresh 空体, code200+set-cookie→merge;code!=200→NCM_ERR_AUTH 退扫码;网络/TLS→负码退避)。 - app/logic/account.c:account.json 增 last_refresh_ts + 拆分 cvip/svip; account_cookie_maybe_refresh(年龄阈值 12h 可配 ncm.cookie_refresh_h,单飞/boot, 失败冷却 30min 跨 boot 持久化,AUTH 置回登录页标志)+ mark_fresh/need_relogin。 - app/ui/home.c:home_worker 先续期再拉歌单;app/ui/login.c:803 落盘后 mark_fresh; app/app.c:UI tick 消费回登录标志 → logout+切登录页。 - 新增测试:test_ncm_core 的 cookie_merge + eapi_musicu_hdr;bench/cookie_refresh_sim (端点语义,0 网络);bench/account_cookie_sim(临期/未临期/退避/失效/冷却,0 网络)。 验证:host+qemu 核心测试 PASS;build_ncm_sims.sh 全家桶 PASS;build_ncm.sh 0 warning。 真机只读探针 2 次(设备 10.78.193.50,不写回):refresh 走 eapi 域 interface.music.163.com 在当前网络被 TLS reset(alert 80)→ rc=-5 瞬断退避, session.enc/deviceId md5 前后一致(登录态零损伤)。新 ncm md5 c960244b。
255 lines
9.7 KiB
C
255 lines
9.7 KiB
C
/* ui/login.c — 扫码登录页(ACT_LOGIN,根页)。
|
||
*
|
||
* 流程:进入即起后台 worker(login_worker):
|
||
* unikey(eapi) → 本地二维码矩阵(qrcodegen) → 轮询(801/802/803) → 803 捕获 set-cookie 加密落盘
|
||
* → login_status 拿 uid/nickname → 置 g_login_done。
|
||
* UI 线程只在 tick 读 g_st.qr 状态与 login_state 重绘并检查 g_login_done → 切入 home。
|
||
* 800(过期)自动刷新二维码。UI 不卡:网络全在 worker 线程。
|
||
*/
|
||
#include "../ncm_app_internal.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
|
||
static pthread_t g_thread;
|
||
static volatile int g_polling; /* worker 运行中 */
|
||
static volatile int g_done; /* 登录成功(UI tick 切入 home) */
|
||
static volatile int g_err; /* 最后一次错误(网络不可达等) */
|
||
static int g_active;
|
||
|
||
static int g_custom_id; /* 绘制物 */
|
||
static ncm_rect_t r_scr; /* 整屏(沉浸式,hero 从 y=0 起) */
|
||
|
||
/* ---- 绘制 ---- */
|
||
static void login_draw(void *ctx)
|
||
{
|
||
(void)ctx;
|
||
if (meow_ui_activity_active() != ACT_LOGIN) return;
|
||
const meow_theme *th = meow_theme_get();
|
||
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||
int W = meow_ui_width(), H = meow_ui_height();
|
||
|
||
/* 品牌红 hero 带(沉浸,从 y=0 到 264;day=red / night=hero 深红压场)。 */
|
||
meow_ui_fill_rect(0, 0, W, 264, pa->hero);
|
||
|
||
/* 标题「网易云音乐」+ 红点 logo(用字体大小近似排版,host 字库用 ASCII 替文本)。 */
|
||
const char *title = "网易云音乐";
|
||
int tw = meow_ui_text_width(title, 34);
|
||
meow_ui_text_center(W/2, 112, title, 34, pa->on_red);
|
||
/* 24px 红点 logo(标题左前) */
|
||
ncm_fill_round(W/2 - tw/2 - 36, 112 + 4, 24, 24, 12, pa->red_lit);
|
||
|
||
meow_ui_text_center(W/2, 160, "扫码登录 / 音樂的力量", 22, pa->on_red_dim);
|
||
/* 弱分隔(hero 与内容衔接) */
|
||
meow_ui_fill_rect(0, 264, W, 1, th->outline);
|
||
|
||
/* QR 卡(128,228,224,224):day 纯白 / night surfaceContainer;圆角 16 + 内描边。 */
|
||
int qc = 224, qx = (W - qc) / 2, qy = 228;
|
||
ncm_fill_round(qx, qy, qc, qc, 16, th->surfaceContainer);
|
||
meow_ui_draw_rect_round(qx, qy, qc, qc, 16, th->outlineVariant);
|
||
|
||
int sz = g_st.qr_size;
|
||
if (sz > 0) {
|
||
/* 模块尺寸随内容缩放(二维码版本随内容长度变化,尺寸不固定——
|
||
* 按容器内边距算模块边长,绝不溢出卡片) */
|
||
int m = (qc - 32) / sz;
|
||
if (m < 1) m = 1;
|
||
int qs = m * sz;
|
||
int ox = qx + (qc - qs) / 2, oy = qy + (qc - qs) / 2;
|
||
/* 夜间:深灰底 + 浅灰码;白天:白底黑码 */
|
||
uint32_t qr_bg = g_dark ? 0x2A2B30 : 0xFFFFFF;
|
||
uint32_t qr_fg = g_dark ? 0xC9CAD1 : 0x000000;
|
||
meow_ui_fill_rect(ox, oy, qs, qs, qr_bg);
|
||
for (int y = 0; y < sz; y++)
|
||
for (int x = 0; x < sz; x++)
|
||
if (g_st.qr_mods[y*sz + x])
|
||
meow_ui_fill_rect(ox + x*m, oy + y*m, m, m, qr_fg);
|
||
} else {
|
||
meow_ui_text_center(W/2, qy + qc/2 - 10,
|
||
g_err ? "网络异常,点按重试" : "正在获取二维码...", 20, th->dim);
|
||
}
|
||
|
||
/* 状态文字:成功/等待用红(day red / night red_lit),失败红字提示。 */
|
||
const char *st = login_status_str(g_st.login_state);
|
||
if (g_st.login_state == NCM_LOGIN_OK && g_done) st = "登录成功";
|
||
uint32_t scol = (g_st.login_state == NCM_LOGIN_OK) ? pa->red : th->dim;
|
||
meow_ui_text_center(W/2, 476, st, 20, scol);
|
||
|
||
/* 底部提示 */
|
||
meow_ui_text_center(W/2, H - 56, "由 MeowOS 驱动 / 仅本地解码", 18, th->dim);
|
||
}
|
||
|
||
/* 重绘 login 内容(QR+状态变化时)。 */
|
||
static void login_repaint(void)
|
||
{
|
||
meow_ui_invalidate(r_scr.x, r_scr.y, r_scr.w, r_scr.h);
|
||
}
|
||
|
||
const char *login_status_str(int state)
|
||
{
|
||
switch (state) {
|
||
case NCM_LOGIN_PENDING: return "等待扫码...";
|
||
case NCM_LOGIN_SCANNED: return "已扫码,请在手机上确认";
|
||
case NCM_LOGIN_OK: return "登录成功";
|
||
case NCM_LOGIN_EXPIRED: return "二维码已过期,正在刷新";
|
||
default: return "出错了,点按重试";
|
||
}
|
||
}
|
||
|
||
static int get_unikey_and_qr(char *unikey, size_t usz)
|
||
|
||
{
|
||
int rc = ncm_login_unikey(g_st.ctx.device_id, unikey, usz);
|
||
if (rc != NCM_OK) { meow_log(MEOW_LOG_ERROR, "login: unikey fetch failed rc=%d", rc); return rc; }
|
||
meow_log(MEOW_LOG_INFO, "login: unikey ok, qr ready");
|
||
ncm_strlcpy(g_st.unikey, unikey, sizeof g_st.unikey);
|
||
ncm_login_qr_url(unikey, g_st.qr_url, sizeof g_st.qr_url);
|
||
int size = 0;
|
||
if (ncm_login_qr_matrix(g_st.qr_url, g_st.qr_mods, sizeof g_st.qr_mods / 177, &size) != NCM_OK)
|
||
g_st.qr_size = 0;
|
||
else
|
||
g_st.qr_size = size;
|
||
g_st.login_state = NCM_LOGIN_PENDING;
|
||
g_err = 0;
|
||
return NCM_OK;
|
||
}
|
||
|
||
static void *login_worker(void *arg)
|
||
{
|
||
(void)arg;
|
||
char unikey[64];
|
||
/* 自动重试:unikey 失败(网络未就绪/抖动)时 5s 退避重试,不再要求用户点重试 */
|
||
int retry = 0;
|
||
while (get_unikey_and_qr(unikey, sizeof unikey) != NCM_OK) {
|
||
g_err = 1;
|
||
if (g_app_quit) return NULL;
|
||
meow_log(MEOW_LOG_WARN, "login: unikey retry in 5s (attempt %d)", ++retry);
|
||
usleep(5 * 1000 * 1000);
|
||
}
|
||
meow_log(MEOW_LOG_INFO, "login: poll worker start");
|
||
|
||
char cookie[NCM_COOKIE_MAX];
|
||
ncm_strlcpy(cookie, g_st.ctx.cookie, sizeof cookie);
|
||
|
||
while (!g_app_quit) {
|
||
ncm_login_poll_t r;
|
||
char out_cookie[NCM_COOKIE_MAX];
|
||
int rc = ncm_login_poll(g_st.ctx.device_id, unikey, cookie, &r,
|
||
out_cookie, sizeof out_cookie);
|
||
if (rc != NCM_OK) { meow_log(MEOW_LOG_WARN, "login: poll failed rc=%d", rc); g_err = 1; usleep(2 * 1000 * 1000); continue; }
|
||
if (r.state != g_st.login_state)
|
||
meow_log(MEOW_LOG_INFO, "login: state -> %d", (int)r.state);
|
||
g_st.login_state = r.state;
|
||
|
||
if (r.state == NCM_LOGIN_OK) {
|
||
meow_log(MEOW_LOG_INFO, "login: 803 confirmed, saving cookie");
|
||
ncm_strlcpy(cookie, out_cookie, sizeof cookie);
|
||
ncm_ctx_set_cookie(&g_st.ctx, cookie);
|
||
account_cookie_mark_fresh(); /* §NEW:新登录态 cookie 视为全新(年龄=now,重置单飞/冷却) */
|
||
/* 803 后同步取 uid/nickname 并落盘(weapi 已修复:不再挂死、cookie 全量且去重发送,
|
||
* 服务端认可会话)。此处在 worker 线程执行(不卡 UI);失败不阻断跳页——home 加载
|
||
* 时 appstate_ensure_uid 会再验一次(也设了硬超时上限,不阻塞歌单页)。 */
|
||
char uid[32] = "", nick[128] = "";
|
||
int src = ncm_login_status_check(g_st.ctx.device_id, cookie,
|
||
uid, sizeof uid, nick, sizeof nick);
|
||
meow_log(MEOW_LOG_INFO, "login: status_check rc=%d uid=%s nick=%s", src, uid, nick);
|
||
appstate_set_logged_in(src == NCM_OK ? uid : "", src == NCM_OK ? nick : "");
|
||
g_st.logged_in = 1;
|
||
account_vip_maybe_refresh(1); /* §20:登录后查一次会员态(本 worker 线程,不卡 UI) */
|
||
g_done = 1;
|
||
meow_log(MEOW_LOG_INFO, "login: -> home");
|
||
break;
|
||
}
|
||
if (r.state == NCM_LOGIN_EXPIRED) {
|
||
/* 刷新二维码 */
|
||
usleep(500 * 1000);
|
||
get_unikey_and_qr(unikey, sizeof unikey);
|
||
continue;
|
||
}
|
||
usleep(1500 * 1000);
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
int login_start(void)
|
||
{
|
||
if (g_polling) return 0;
|
||
g_polling = 1;
|
||
g_done = 0;
|
||
g_err = 0;
|
||
if (ncm_thread_create(&g_thread, login_worker, NULL) != 0) {
|
||
g_polling = 0;
|
||
return -1;
|
||
}
|
||
pthread_detach(g_thread);
|
||
return 0;
|
||
}
|
||
|
||
int login_is_active(void) { return g_active; }
|
||
|
||
void login_on_tap(int x, int y)
|
||
{
|
||
(void)x; (void)y;
|
||
if (g_st.login_state == NCM_LOGIN_OK && g_done) return;
|
||
if (g_err) { login_start(); login_repaint(); } /* 网络失败重试 */
|
||
}
|
||
|
||
/* UI tick:重绘 QR/状态变化;登录成功 → 切 home。 */
|
||
void login_tick(uint64_t now)
|
||
{
|
||
(void)now;
|
||
chrome_tick();
|
||
static int last_state = -1, last_qr = 0;
|
||
if (g_st.login_state != last_state || g_st.qr_size != last_qr) {
|
||
last_state = g_st.login_state;
|
||
last_qr = g_st.qr_size;
|
||
login_repaint();
|
||
}
|
||
if (g_done)
|
||
meow_view_page_replace(ACT_HOME);
|
||
}
|
||
|
||
/* ---- 生命周期 ---- */
|
||
static void login_page_create(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
r_scr = NR(0, 0, meow_ui_width(), meow_ui_height());
|
||
/* 沉浸式:登录页整屏自绘(hero 从 y=0 起,覆盖状态栏区)。只画一次并监听绘制物重绘。 */
|
||
g_custom_id = meow_draw_custom_register(1, r_scr.x, r_scr.y, r_scr.w, r_scr.h,
|
||
login_draw, NULL);
|
||
g_active = 0;
|
||
}
|
||
|
||
static void login_page_enter(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
g_active = 1;
|
||
const meow_theme *th = meow_theme_get();
|
||
meow_ui_set_page_bg(th->bg);
|
||
meow_ui_paint_full();
|
||
if (!g_host_mock) login_start(); /* 进入即获取二维码 + 轮询 */
|
||
}
|
||
|
||
static void login_page_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
g_active = 0;
|
||
}
|
||
|
||
static void login_page_destroy(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
if (g_custom_id) { meow_draw_unregister(g_custom_id); g_custom_id = 0; }
|
||
}
|
||
|
||
meow_view *login_view_create(void)
|
||
{
|
||
static const meow_view_hooks h = {
|
||
.on_create = login_page_create,
|
||
.on_enter = login_page_enter,
|
||
.on_leave = login_page_leave,
|
||
.on_destroy = login_page_destroy,
|
||
};
|
||
return meow_view_create(ACT_LOGIN, MEOW_VIEW_ACTIVITY, &h, NULL);
|
||
}
|