diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index b444297..5baddc0 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -3539,16 +3539,16 @@ MP3 ≤128k→标准,>128k→极高 HQ。列表显示**实际档**(如 hires |---|---| | `core/ncm_session.h/.c`(新) | 纯逻辑分类层:`ncm_err_classify`(NETWORK/AUTH/OTHER;`-4/-5/-6/-17` 与历史 mbedtls 裸负码→NETWORK,仅 `-13`→AUTH)、`ncm_http_status_class/err`(401/403→AUTH;408/425/429/5xx→SVC;其它非 2xx→HTTP)、`ncm_gate_decide`(一次请求→在线/离线/退登录/保持)、`ncm_session_boot_decide`(有本地会话就先信它,无网→离线,绝不因连不上进扫码)。 | | `core/ncm_common.h` | 新增 `NCM_ERR_SVC = -17`(服务端 5xx/429/-460:暂不可用,可重试,**不是**登录失效)。 | -| `core/ncm_http.c` | 新增 `http_ret_norm`:超时→`NCM_ERR_TIMEOUT`、证书→`NCM_ERR_TLS`、其它 mbedtls 负码→`NCM_ERR_NET`,在 `done:` 统一归一(532 行)。 | -| `core/ncm_api.c` | `api_status_gate`:非 2xx 先按状态定性(401/403→AUTH、5xx/429→SVC、其它→HTTP),不再把 5xx HTML 丢给 JSON 解析;业务码映射加 302→AUTH、`-460`→SVC(113-141 行)。 | +| `core/ncm_http.c` | 新增 `http_ret_norm`:超时→`NCM_ERR_TIMEOUT`、证书→`NCM_ERR_TLS`、其它 mbedtls 负码→`NCM_ERR_NET`,在 `done:` 统一归一(532 行)。**已是 NCM 错误码的一律原样返回**(含 `NCM_ERR_JSON`:畸形分块/帧体是数据错误,不得改判成网络不可达——评审 P2-1)。 | +| `core/ncm_api.c` | `api_status_gate`:非 2xx 先按状态定性(401/403→AUTH、5xx/429→SVC、其它→HTTP),不再把 5xx HTML 丢给 JSON 解析;业务码映射加 302→AUTH、`-460`→SVC(113-141 行)。`api_post_cached` 新增 `from_cache` 出参;新增 `ncm_api_user_playlist_ex`(回填「本次是否接口缓存命中」;`out_n` 与原接口一致必填、`out_from_cache` 可 NULL),旧接口为薄包装(评审 P2-2)。 | | `core/ncm_login.c` | `ncm_login_status_check` / `ncm_login_refresh` 先过 HTTP 状态门,再按业务码:301/302→AUTH、`-460`→SVC、其它→HTTP(163-249 行)。`-460` 不再触发退扫码。 | | `core/ncm_store.c/.h` | 会话密钥改为设备私有 `/.mkey` 派生(`key_from_mkey`/`session_key`,175-193 行);旧 MAC 派生密文仍可解(`cookie_decrypt_sel`,270 行)且 `load_session` 命中旧密钥时自动重写迁移(306-317 行)。 | -| `core/ncm_apicache.c/.h` | 新增命中计数 `ncm_apicache_hits()`:离线时读到 120s 旧缓存仍是 `NCM_OK`,不能据此判「已回在线」。 | +| `core/ncm_apicache.c/.h` | 盘缓存命中分支的两行合并为一行(行为不变,重构)。**不再提供全局命中计数**(评审 P2-2:跨 worker 差值会误判、自增在锁外有竞争;改为单请求出参,见下)。 | | `app/ncm_app_internal.h` | `ncm_app_state_t` 增 `offline`;新增 `appstate_session_gate/is_offline/offline_set`。 | -| `app/logic/appstate.c` | 新增离线状态机(177-197 行);`appstate_ensure_uid` 仅 AUTH 清 cookie,网络类只切离线(231-256 行);init/logout 清零离线态。 | +| `app/logic/appstate.c` | 新增离线状态机(177-197 行);`appstate_ensure_uid` 仅 AUTH 清 cookie,网络类只切离线(231-256 行);init/logout 清零离线态;新增 `appstate_load_playlists_ex(from_cache)` 透传单请求缓存命中标志。 | | `app/app.c` | 启动根页改由 `ncm_session_boot_decide` 决定并打日志(245-252 行);`need_relogin` 路径保持(仅 AUTH 会置位)。 | -| `app/ui/home.c` | worker 结果经 `appstate_session_gate`(缓存命中不算回在线,271-272 行);个人卡右侧「离线模式」红标 + 页脚提示 + 空态「离线模式/可播放已缓存」;离线时每 30s 静默重试一次(强制绕过接口缓存,301-312 行)。 | -| `app/ui/playlist.c` | 首屏结果同步会话/离线态(锁外,333 行);空态「离线模式」。 | +| `app/ui/home.c` | worker 结果经 `appstate_session_gate`;「本次是否接口缓存命中」用 `appstate_load_playlists_ex(&from_cache)` 的单请求出参判定(缓存命中→不判回在线),个人卡右侧「离线模式」红标 + 页脚提示 + 空态「离线模式/可播放已缓存」;离线时每 30s 静默重试一次(强制绕过接口缓存)。 | +| `app/ui/playlist.c` | 首屏**失败**时同步离线态(成功不判回在线:歌单详情可能命中接口缓存;锁外应用);空态「离线模式」。 | | `app/ui/search.c` / `app/ui/player.c` / `app/ui/cache.c` | 离线提示文案:搜索需网络、播放页「离线模式:网络不可用,可播放已缓存」、缓存页页脚「· 离线模式(可播放)」。 | | `build_core.sh` / `build_ncm.sh` | 核心源列表加 `core/ncm_session.c`。 | | `build_ncm_sims.sh` | window/search/plpage/apicache/cookie_refresh 链入 `ncm_session.o`;新增 `session_gate_sim` 构建/运行。 | @@ -3562,8 +3562,8 @@ MP3 ≤128k→标准,>128k→极高 HQ。列表显示**实际档**(如 hires | 项 | 结果 | |---|---| -| `bench/session_gate_sim`(新,真实 core + `ncm_http_request` 桩) | **83 PASS / 0 FAIL**:A 分类矩阵 11 例(-5/-4/-6/-17/裸 mbedtls/…)、B HTTP 状态 14 例、C 登录门 10 + 启动门 6 例、D `ncm_api` 15 例(401/403、5xx/429、业务 301/302、-460、非 JSON、传输码)、E `login_status` 8 + `login_refresh` 8 例、F 会话密钥 9 例(换 MAC 仍可解、旧密文可解并自动迁移) | -| `bench/http_truncate_sim` | **12 PASS**(原 8 截断用例零回归 + 新 4 个传输层归一用例) | +| `bench/session_gate_sim`(新,真实 core + `ncm_http_request` 桩) | **88 PASS / 0 FAIL**:A 分类矩阵 11 例(-5/-4/-6/-17/裸 mbedtls/…)、B HTTP 状态 14 例、C 登录门 10 + 启动门 6 例、D `ncm_api` 15 例(401/403、5xx/429、业务 301/302、-460、非 JSON、传输码)、D2 `from_cache` 出参 5 例(miss→0/命中→1 且 0 请求/失败→0/兼容入口/out_n 必填契约)、E `login_status` 8 + `login_refresh` 8 例、F 会话密钥 9 例(换 MAC 仍可解、旧密文可解并自动迁移) | +| `bench/http_truncate_sim` | **13 PASS**(原 8 截断用例零回归 + 4 个传输层归一用例 + 1 个畸形分块(NCM_ERR_JSON 不被改判 NET)用例) | | `test/test_ncm_core_host`(全量核心库) | **139 PASS / 0 FAIL**(`TMPDIR` 指向可写目录;含 crypto/store/cache/tag/api/降级链全量) | | `bench/window_sim` / `bench/search_sim` | PASS(window 需可写 TMPDIR:本机 `/tmp` 不可写时盘缓存用例环境性失败,换 `$TMPDIR` 后 6000/6000 落盘通过) | | `bench/apicache_sim` / `bench/cookie_refresh_sim` | PASS(apicache 同样仅盘缓存文件路径受 `/tmp` 影响,换可写目录后通过) | @@ -3576,8 +3576,12 @@ MP3 ≤128k→标准,>128k→极高 HQ。列表显示**实际档**(如 hires 只是服务端暂不可用;文案取「可播放已缓存」这一用户可执行动作,未再细分。 - **HTTP 403 仍按 AUTH**(标准语义:服务端理解请求但拒绝)。若真机遇到 CDN/WAF 的 403,可能造成 一次误登出;NCM 的风控通常以业务码 `-460` 表达(已改为 SVC)。如真机复现可再收窄为「仅 401」。 -- **离线时接口缓存命中不算「回在线」**:`ncm_apicache_hits()` 只解决 UI/重试节奏误判;离线恢复 - 探测(home 每 30s)强制 `ncm_api_cache_refresh_path` 绕过缓存,确保成功即真在线。 +- **离线时接口缓存命中不算「回在线」**:用 `ncm_api_user_playlist_ex` 的 `from_cache` 出参判定本次 + 结果是否真走网络(评审 P2-2:不用全局计数差值);离线恢复探测(home 每 30s)强制 + `ncm_api_cache_refresh_path` 绕过缓存,确保成功即真在线。歌单页的缓存命中成功同样不判回在线 + (该路径只在失败时更新离线态,在线恢复由 home 探测统一裁决)。 +- **畸形分块/帧体归 `NCM_ERR_JSON`(数据错误)**,`http_ret_norm` 不再把它改判为网络不可达 + (评审 P2-1:否则会误报离线)。 - **离线播放未缓存曲目**仍走原 `song_url` 重试(15s 超时 ×3),未改播放重试策略(不属本任务); 已缓存曲目离线可播(`song_local_file` 先查缓存)。 - **`app/app.c` 未能宿主编译**:其 SIGSEGV 定位处理器用 MIPS `ucontext.gregs[]`,aarch64 宿主 diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index 05b0c64..f1eb3ca 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -259,8 +259,11 @@ int appstate_ensure_uid(void) * 之间的发布点。持久化(playlists.bin)同单写者(home worker)在发布后读,无相互竞态。 */ static pthread_mutex_t g_pls_mu = PTHREAD_MUTEX_INITIALIZER; -int appstate_load_playlists(void) +/* from_cache(可 NULL):透传 ncm_api_user_playlist_ex 的「本次是否接口缓存命中」。 + * 仅真正走网络成功时才应把 App 判为在线(M9 评审 P2-2:缓存命中不代表网络恢复)。 */ +int appstate_load_playlists_ex(int *from_cache) { + if (from_cache) *from_cache = 0; if (!g_st.logged_in) return NCM_ERR_AUTH; int rc = appstate_ensure_uid(); if (rc != NCM_OK) return rc; @@ -273,8 +276,9 @@ int appstate_load_playlists(void) * 单飞),免 96KB 栈。 */ static ncm_playlist_t s_pls[NCM_PLAYLIST_MAX]; int n = 0; - int rc2 = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, - NCM_PLAYLIST_MAX, 0, s_pls, NCM_PLAYLIST_MAX, &n); + int rc2 = ncm_api_user_playlist_ex(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid, + NCM_PLAYLIST_MAX, 0, s_pls, NCM_PLAYLIST_MAX, &n, + from_cache); if (rc2 == NCM_OK) { int cnt = (n > NCM_PLAYLIST_MAX) ? NCM_PLAYLIST_MAX : n; pthread_mutex_lock(&g_pls_mu); @@ -283,11 +287,14 @@ int appstate_load_playlists(void) pthread_mutex_unlock(&g_pls_mu); appstate_playlists_cache_save(); /* §26:网络成功 → 覆写盘缓存(stale-while-revalidate) */ } - meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc2, - (rc2 == NCM_OK) ? n : g_st.playlist_count, g_st.uid); + meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s%s", rc2, + (rc2 == NCM_OK) ? n : g_st.playlist_count, g_st.uid, + (from_cache && *from_cache) ? " (cache)" : ""); return rc2; } +int appstate_load_playlists(void) { return appstate_load_playlists_ex(NULL); } + /* ---- 歌单列表磁盘缓存(§26,NCM_CACHE_DESIGN §2.2)---- * 快照 /playlists.bin:magic(4) + ver(2) + 时间戳(8) + count(4) + 数组(ncm_playlist_t)。 * 进首页先渲染缓存秒开(断网可离线浏览),后台 worker 拉网络成功后覆写;结构体改版靠 ver 判 miss。 diff --git a/patch_v2/ncm_app/app/ncm_app_internal.h b/patch_v2/ncm_app/app/ncm_app_internal.h index 70cb826..4f412ee 100644 --- a/patch_v2/ncm_app/app/ncm_app_internal.h +++ b/patch_v2/ncm_app/app/ncm_app_internal.h @@ -447,6 +447,10 @@ int appstate_session_gate(int rc); int appstate_is_offline(void); /* 1=离线模式(UI 画「离线模式」提示) */ void appstate_offline_set(int on); /* 直接置离线态(登出/初始化清零) */ int appstate_load_playlists(void); /* 拉我的歌单 */ +int appstate_load_playlists_ex(int *from_cache); + /* 同上,额外回填「本次是否接口缓存命中」: + * 1=缓存(未走网络,不能判为已回在线);0=走网络/失败。 + * from_cache 可为 NULL(等价 appstate_load_playlists)。 */ int appstate_load_playlist_songs(int idx); /* 兼容:载索引 + 首个窗口(内部走 ensure) */ int appstate_load_playlist_songs_page(int idx, int offset, int limit); /* 兼容入口(滑动窗口语义,REPORT §17): diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index a685bec..c2e21d7 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -255,21 +255,21 @@ static void *home_worker(void *arg) { (void)arg; int rc; - uint64_t hits0 = ncm_apicache_hits(); + int from_cache = 0; if (!g_st.logged_in) rc = NCM_ERR_AUTH; else { /* §NEW MUSIC_U 临期续期:先于歌单网络请求(后续 eapi/weapi 用新 cookie)。 * AUTH=会话确失效(置回登录页标志,UI 线程消费);瞬断=保持旧 cookie 走缓存/旧会话。 */ int crc = account_cookie_maybe_refresh(); if (crc == NCM_ERR_AUTH) rc = NCM_ERR_AUTH; - else rc = appstate_load_playlists(); + else rc = appstate_load_playlists_ex(&from_cache); } if (rc == NCM_OK) account_vip_maybe_refresh(0); /* §20:超 1 天/到期后各补查一次(worker 线程,不卡 UI) */ /* M9:网络类失败→离线(保留登录态);成功→回在线。但「接口缓存命中」不算回在线—— - * 离线时读到 120s 内的旧缓存同样返回 NCM_OK,不能据此清掉离线提示/重试节奏。 */ - int from_cache = (rc == NCM_OK) && (ncm_apicache_hits() != hits0); - if (!from_cache) appstate_session_gate(rc); + * 离线时读到 120s 内的旧缓存同样返回 NCM_OK,不能据此清掉离线提示/重试节奏。 + * 判定用本次请求的出参(评审 P2-2:不再用全局命中计数做前后差值)。 */ + if (!(rc == NCM_OK && from_cache)) appstate_session_gate(rc); pthread_mutex_lock(&g_home_mu); g_load_err = (g_st.playlist_count > 0) ? 0 : rc; /* 有数据(缓存/已载)即不置错误态 */ g_home_loading = 0; diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index 0580a19..9564f30 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -314,7 +314,8 @@ static void *playlist_worker(void *arg) } else { g_load_err = rc; /* 索引失败/鉴权失败 → 整页空态(点按重试) */ } - /* M9:本次首屏结果同步会话/离线态(网络类→离线提示;成功→回在线;STALE 不采纳)。 */ + /* M9:本次首屏失败(非 STALE)同步离线态;成功不在此判「回在线」——歌单详情可能命中 + * 接口缓存,缓存命中不代表网络恢复(回在线由 home 的离线探测/真实成功判定)。 */ gate_rc = rc; g_plloaded_idx = tidx; g_plloaded_gen = tgen; @@ -330,7 +331,7 @@ static void *playlist_worker(void *arg) g_plloading = 0; g_plgen++; pthread_mutex_unlock(&g_plmu); - if (gate_rc != NCM_ERR_STALE) appstate_session_gate(gate_rc); + if (gate_rc != NCM_ERR_STALE && gate_rc != NCM_OK) appstate_session_gate(gate_rc); return NULL; } diff --git a/patch_v2/ncm_app/bench/playlist_race_sim.c b/patch_v2/ncm_app/bench/playlist_race_sim.c index ed004b2..3aeaa25 100644 --- a/patch_v2/ncm_app/bench/playlist_race_sim.c +++ b/patch_v2/ncm_app/bench/playlist_race_sim.c @@ -137,6 +137,15 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char return NCM_OK; } +/* M9 出参变体(appstate_load_playlists_ex 引用;本 sim 恒「走网络」) */ +int ncm_api_user_playlist_ex(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n, + int *out_from_cache) +{ + if (out_from_cache) *out_from_cache = 0; + return ncm_api_user_playlist(device_id, cookie, uid, limit, offset, out, max, out_n); +} + static int g_fail = 0; #define CHECK(cond, fmt, ...) do { \ if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \ diff --git a/patch_v2/ncm_app/bench/plpage_sim.c b/patch_v2/ncm_app/bench/plpage_sim.c index 5c4788e..0ee9320 100644 --- a/patch_v2/ncm_app/bench/plpage_sim.c +++ b/patch_v2/ncm_app/bench/plpage_sim.c @@ -69,6 +69,12 @@ int ncm_ctx_clear_cookie(ncm_ctx_t *ctx) { (void)ctx; return NCM_OK; } int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, int limit, int offset, ncm_playlist_t *out, int max, int *out_n) { (void)device_id;(void)cookie;(void)uid;(void)limit;(void)offset;(void)out;(void)max;(void)out_n; return NCM_ERR_NET; } +/* M9 出参变体(appstate_load_playlists_ex 引用;本 sim 不到达) */ +int ncm_api_user_playlist_ex(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n, + int *out_from_cache) +{ if (out_from_cache) *out_from_cache = 0; + return ncm_api_user_playlist(device_id, cookie, uid, limit, offset, out, max, out_n); } /* ---- 网络端点 stub(离线,合成数据;计调用次数 + 可注入单批失败) ---- */ #define SIM_TOTAL 400 /* 4 页:100 × 4(> NCM_SONG_WINDOW 256,回滚必滑窗) */ diff --git a/patch_v2/ncm_app/bench/session_gate_sim.c b/patch_v2/ncm_app/bench/session_gate_sim.c index 0db42ea..13da7c5 100644 --- a/patch_v2/ncm_app/bench/session_gate_sim.c +++ b/patch_v2/ncm_app/bench/session_gate_sim.c @@ -9,6 +9,9 @@ * D. 真实 ncm_api(ncm_http_request 桩)HTTP 状态与业务码映射: * 401/403 与业务 301/302 → AUTH(退登录);5xx/429、业务 -460 → SVC(保登录态/离线); * HTML 体 → JSON(other);其它业务码 → HTTP(other)。 + * D2. `ncm_api_user_playlist_ex` 的 from_cache 出参:miss→0(真网络 +1 次)、命中→1(0 请求)、 + * 失败→0;兼容入口不崩;out_n 必填契约(NULL→NCM_ERR_ARG)(离线判定不得用全局计数, + * 评审 P2-2/P2-1)。 * E. 真实 ncm_login_status_check / ncm_login_refresh 同上(AUTH 只在服务端明确否定时)。 * F. 会话加密密钥稳定性(M9 根因回归):session.enc 保存后改变网卡 MAC(NCM_MAC 桩)仍能解出 * cookie;旧版 MAC 派生密文可解且自动迁移到 .mkey 密钥。 @@ -259,6 +262,51 @@ static void test_api_map(void) } } +/* ============================================================ + * D2. user_playlist_ex:本次结果是否来自接口缓存(评审 P2-2) + * ============================================================ */ +static void test_api_from_cache(void) +{ + printf("== D2. ncm_api_user_playlist_ex 缓存命中出参(离线判定用)==\n"); + ncm_apicache_init(NULL); + ncm_apicache_clear(); + const char *body = "{\"code\":200,\"playlist\":[{\"id\":1,\"name\":\"a\",\"trackCount\":1}]}"; + ncm_playlist_t pls[4]; + int n = 0, fc = -1, rc; + + stub(200, body, NULL); + int calls0 = g_http_calls; + rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n, &fc); + CHECK(rc == NCM_OK && fc == 0 && g_http_calls == calls0 + 1 && n == 1, + "首次(缓存 miss):rc=0 n=%d from_cache=%d,真网络 +1 次(calls %d→%d)", + n, fc, calls0, g_http_calls); + + fc = -1; + calls0 = g_http_calls; + rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n, &fc); + CHECK(rc == NCM_OK && fc == 1 && g_http_calls == calls0 && n == 1, + "二次同参(缓存命中):rc=0 from_cache=%d,未发网络请求(calls=%d)", + fc, g_http_calls); + + /* 失败不得把 from_cache 留成 1(App 会据此判离线,方向必须正确) */ + ncm_apicache_clear(); + stub_err(NCM_ERR_NET); + fc = -1; + rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n, &fc); + CHECK(rc == NCM_ERR_NET && fc == 0, "网络失败:rc=%d from_cache=%d(期望 -4/0)", rc, fc); + + /* 兼容入口仍可用(out_from_cache=NULL 不崩) */ + ncm_apicache_clear(); + stub(200, body, NULL); + rc = ncm_api_user_playlist("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, &n); + CHECK(rc == NCM_OK && n == 1, "兼容入口 ncm_api_user_playlist 正常(rc=%d n=%d)", rc, n); + + /* 契约:out_n 必填(与原接口一致);out_from_cache 可 NULL(评审 P2-1) */ + rc = ncm_api_user_playlist_ex("dev", "MUSIC_U=x", "1", 30, 0, pls, 4, NULL, &fc); + CHECK(rc == NCM_ERR_ARG, "out_n=NULL → NCM_ERR_ARG(契约与原接口一致)rc=%d", rc); + ncm_apicache_clear(); +} + /* ============================================================ * E. 真实 ncm_login:cookie 校验 / 续期 * ============================================================ */ @@ -414,6 +462,7 @@ int main(void) test_http_class(); test_gate(); test_api_map(); + test_api_from_cache(); test_login_map(); test_session_key_stability(); printf("\n== SESSION GATE SIM: %s (http stubs called %d times) ==\n", diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index 967b6f0..79629be 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -174,9 +174,14 @@ static void api_cache_key(const char *path, const char *json, char *out, size_t } /* 带缓存的 POST:先查内存/盘缓存(TTL 内命中则直接解析回填);miss/过期/手动刷新 → 真网络, - * 成功且非手动刷新 → 写缓存。 */ + * 成功且非手动刷新 → 写缓存。 + * from_cache(可 NULL):1=本次结果来自接口缓存(**没有**真正走网络),0=走了网络/失败。 + * 离线判定用它——缓存命中返回 NCM_OK 不代表网络已恢复(M9 评审 P2-2:不能用全局计数 + * 前后差值判定,其它 worker 的命中会污染,且计数自增存在跨线程竞争)。 */ static int api_post_cached(const char *device_id, const char *cookie, const char *path, - const char *json, int kind, size_t resp_cap, api_json_t *c) { + const char *json, int kind, size_t resp_cap, api_json_t *c, + int *from_cache) { + if (from_cache) *from_cache = 0; int refresh = api_refresh_match(path); char key[NCM_APICACHE_KEY_MAX]; api_cache_key(path, json, key, sizeof key); @@ -186,6 +191,7 @@ static int api_post_cached(const char *device_id, const char *cookie, const char if (buf) { size_t len = 0; if (ncm_apicache_get(key, buf, resp_cap, &len) == NCM_OK) { + if (from_cache) *from_cache = 1; c->rbuf = buf; c->resp.body = buf; /* 缓存的 body 从缓冲头开始 */ c->resp.body_len = len; @@ -300,10 +306,13 @@ static int parse_songs_array(const ncm_json_t *j, int arr, ncm_song_t *out, int return NCM_OK; } -int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, - int limit, int offset, ncm_playlist_t *out, int max, int *out_n) { +int ncm_api_user_playlist_ex(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n, + int *out_from_cache) { + /* 与原接口契约一致:out_n 必填(成功路径无条件回填);out_from_cache 可 NULL。 */ if (!out_n) return NCM_ERR_ARG; *out_n = 0; + if (out_from_cache) *out_from_cache = 0; if (!uid) return NCM_ERR_ARG; char json[256]; snprintf(json, sizeof json, "{\"uid\":\"%s\",\"limit\":%d,\"offset\":%d,\"includeVideo\":true}", @@ -314,7 +323,7 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char * (日志 home: playlists rc=-11)。该响应不含 tracks(每首 tracks:null),1MB 足够, * 用 NCM_RESP_MAX(与 vip/info 同口径的堆瞬态,解析后即释,非 bss)。 */ int rc = api_post_cached(device_id, cookie, "/api/user/playlist", json, NCM_CRYPT_WEAPI, - NCM_RESP_MAX, &c); + NCM_RESP_MAX, &c, out_from_cache); if (rc != NCM_OK) return rc; int arr = ncm_json_path(&c.j, "playlist"); @@ -337,6 +346,12 @@ int ncm_api_user_playlist(const char *device_id, const char *cookie, const char return NCM_OK; } +/* 兼容入口(不关心是否缓存命中):等价 ncm_api_user_playlist_ex(..., NULL)。 */ +int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n) { + return ncm_api_user_playlist_ex(device_id, cookie, uid, limit, offset, out, max, out_n, NULL); +} + int ncm_api_playlist_detail(const char *device_id, const char *cookie, const char *playlist_id, char *name, size_t name_sz, char *cover, size_t cover_sz, int64_t *ids, int id_cap, int *out_id_n) { @@ -350,7 +365,7 @@ int ncm_api_playlist_detail(const char *device_id, const char *cookie, const cha api_json_t c; memset(&c, 0, sizeof c); int rc = api_post_cached(device_id, cookie, "/api/v6/playlist/detail", json, NCM_CRYPT_EAPI, - 2 * 1024 * 1024, &c); + 2 * 1024 * 1024, &c, NULL); if (rc != NCM_OK) return rc; int pl = ncm_json_path(&c.j, "playlist"); @@ -394,7 +409,7 @@ int ncm_api_song_detail(const char *device_id, const char *cookie, const int64_t api_json_t c; memset(&c, 0, sizeof c); int rc = api_post_cached(device_id, cookie, "/api/v3/song/detail", json, NCM_CRYPT_EAPI, - RESP_MED, &c); + RESP_MED, &c, NULL); if (rc != NCM_OK) return rc; int arr = ncm_json_path(&c.j, "songs"); @@ -677,7 +692,7 @@ static int cloudsearch_post(const char *device_id, const char *cookie, const cha snprintf(json, sizeof json, "{\"s\":\"%s\",\"type\":%d,\"offset\":%d,\"limit\":%d,\"total\":true}", esc, type, offset > 0 ? offset : 0, limit > 0 ? limit : 30); return api_post_cached(device_id, cookie, "/api/cloudsearch/pc", json, NCM_CRYPT_WEAPI, - RESP_MED, c); + RESP_MED, c, NULL); } /* 解析一处「歌单对象数组」(cloudsearch type=1000)。id 可为数字/字符串(均文本拷贝)。 */ diff --git a/patch_v2/ncm_app/core/ncm_api.h b/patch_v2/ncm_app/core/ncm_api.h index 69bcd21..bb2d0f2 100644 --- a/patch_v2/ncm_app/core/ncm_api.h +++ b/patch_v2/ncm_app/core/ncm_api.h @@ -89,7 +89,13 @@ typedef struct ncm_play_url { } ncm_play_url_t; /* 用户歌单列表(weapi /user/playlist)。uid 必填;limit/offset 翻页。 - * 返回 NCM_OK;songs/out_n 为列表。 */ + * 返回 NCM_OK;songs/out_n 为列表。 + * _ex 变体额外回填 *out_from_cache(可 NULL):1=本次结果来自接口缓存(未走网络), + * 0=走了网络或失败。离线判定用——缓存命中返回 NCM_OK 不代表网络已恢复(M9)。 + * 契约与原接口一致:out_n 必填(NULL → NCM_ERR_ARG)。 */ +int ncm_api_user_playlist_ex(const char *device_id, const char *cookie, const char *uid, + int limit, int offset, ncm_playlist_t *out, int max, int *out_n, + int *out_from_cache); int ncm_api_user_playlist(const char *device_id, const char *cookie, const char *uid, int limit, int offset, ncm_playlist_t *out, int max, int *out_n); diff --git a/patch_v2/ncm_app/core/ncm_apicache.c b/patch_v2/ncm_app/core/ncm_apicache.c index c2ee9aa..9aec7bf 100644 --- a/patch_v2/ncm_app/core/ncm_apicache.c +++ b/patch_v2/ncm_app/core/ncm_apicache.c @@ -44,7 +44,6 @@ static char g_disk_dir[DISK_DIR_MAX]; static int g_disk_on = 0; static int g_ttl_s = NCM_APICACHE_TTL_S; /* 默认 120s */ static int64_t g_last_purge_ms = 0; /* 盘过期清理节流(单调钟)*/ -static uint64_t g_hits = 0; /* 命中计数(M9 离线判定用,仅单调递增) */ static int64_t clock_ms(clockid_t id) { struct timespec ts; @@ -214,7 +213,6 @@ int ncm_apicache_get(const char *key, char *buf, size_t buf_sz, size_t *len) memcpy(buf, g_e[idx].body, n); g_e[idx].last_ms = now; /* LRU touch */ if (len) *len = n; - g_hits++; pthread_mutex_unlock(&g_mu); return NCM_OK; } @@ -224,23 +222,13 @@ int ncm_apicache_get(const char *key, char *buf, size_t buf_sz, size_t *len) /* 内存 miss → 盘缓存兜底(命中回填内存)。 */ if (g_disk_on) { int rc = disk_get(key, buf, buf_sz, len); - if (rc == NCM_OK) { - if (len) ncm_apicache_put(key, buf, *len); - g_hits++; - } + /* 合并为一行,行为不变(重构):len 为空时不回填内存。 */ + if (rc == NCM_OK && len) ncm_apicache_put(key, buf, *len); return rc; } return NCM_ERR_NOENT; } -uint64_t ncm_apicache_hits(void) -{ - pthread_mutex_lock(&g_mu); - uint64_t v = g_hits; - pthread_mutex_unlock(&g_mu); - return v; -} - void ncm_apicache_put(const char *key, const char *body, size_t len) { if (!key || !body || len == 0) return; diff --git a/patch_v2/ncm_app/core/ncm_apicache.h b/patch_v2/ncm_app/core/ncm_apicache.h index ba4ef18..af20575 100644 --- a/patch_v2/ncm_app/core/ncm_apicache.h +++ b/patch_v2/ncm_app/core/ncm_apicache.h @@ -55,10 +55,6 @@ void ncm_apicache_ttl_set(int seconds); /* 当前内存条目数(诊断/测试)。 */ int ncm_apicache_mem_count(void); -/* 命中计数(单调递增;M9 离线判定用:App 据此区分「真·网络成功」与「离线时读到旧缓存」, - * 避免离线提示/账号态被缓存命中误判为「已回在线」)。 */ -uint64_t ncm_apicache_hits(void); - #ifdef __cplusplus } #endif