diff --git a/patch_v2/ncm_app/build_core.sh b/patch_v2/ncm_app/build_core.sh index 760cb60..49aff63 100755 --- a/patch_v2/ncm_app/build_core.sh +++ b/patch_v2/ncm_app/build_core.sh @@ -7,7 +7,7 @@ INC_CORE="-Icore" INC_VENDOR="-I$VENDOR/mbedtls/include -I$VENDOR/jsmn -I$VENDOR/qrcodegen/c" CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \ core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \ - core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c core/ncm_tag.c" + core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c core/ncm_tag.c core/ncm_session.c" VENDOR_C="vendor/qrcodegen/c/qrcodegen.c" # qrcodegen 单独编译 JSMN_IMPL="core/jsmn_impl.c" # jsmn 唯一实现 TU diff --git a/patch_v2/ncm_app/build_ncm.sh b/patch_v2/ncm_app/build_ncm.sh index 189ae09..9b5721b 100755 --- a/patch_v2/ncm_app/build_ncm.sh +++ b/patch_v2/ncm_app/build_ncm.sh @@ -16,7 +16,7 @@ STRICT="-Wall -Wextra -Os -DJSMN_HEADER" CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \ core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \ - core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c core/ncm_tag.c" + core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c core/ncm_tag.c core/ncm_session.c" APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \ app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/cache.c \ app/ui/ncm_colors.c app/ui/chrome.c \ diff --git a/patch_v2/ncm_app/core/ncm.h b/patch_v2/ncm_app/core/ncm.h index 90f7043..52a13c0 100644 --- a/patch_v2/ncm_app/core/ncm.h +++ b/patch_v2/ncm_app/core/ncm.h @@ -7,6 +7,7 @@ #define NCM_H #include "ncm_common.h" +#include "ncm_session.h" #include "ncm_crypto.h" #include "ncm_http.h" #include "ncm_json.h" diff --git a/patch_v2/ncm_app/core/ncm_api.c b/patch_v2/ncm_app/core/ncm_api.c index e8be2fb..967b6f0 100644 --- a/patch_v2/ncm_app/core/ncm_api.c +++ b/patch_v2/ncm_app/core/ncm_api.c @@ -5,6 +5,7 @@ #include "ncm_http.h" #include "ncm_json.h" #include "ncm_req.h" +#include "ncm_session.h" #include #include @@ -101,13 +102,24 @@ static int api_parse_body(api_json_t *c) { long code; if (ncm_json_path_int(&c->j, "code", &code) == NCM_OK && code != 200) { int ec = NCM_ERR_HTTP; - if (code == 301 || code == -460 || code == 2000 || code == 201) ec = NCM_ERR_AUTH; + if (code == 301 || code == 302 || code == 2000 || code == 201) ec = NCM_ERR_AUTH; + else if (code == -460) ec = NCM_ERR_SVC; /* 风控/请求被拒:可重试,不是登录失效(M9) */ api_free(c); return ec; } return NCM_OK; } +/* HTTP 状态门(M9):非 2xx 先按状态定性,不再一律丢给 JSON 解析。 + * 401/403 → NCM_ERR_AUTH(回扫码) + * 408/425/429/5xx → NCM_ERR_SVC(暂不可用:保留登录态、进离线/重试) + * 其它 3xx/4xx → NCM_ERR_HTTP(请求/服务异常,不登出) + * status<100 = 响应头未解析(旧桩/异常):返回 NCM_OK,沿用 body 判定。 */ +static int api_status_gate(const ncm_http_resp_t *r) { + if (r->status < 100) return NCM_OK; + return ncm_http_status_err(r->status); +} + static int api_post(const char *device_id, const char *cookie, const char *path, const char *json, int kind, size_t resp_cap, api_json_t *c) { char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; @@ -124,6 +136,8 @@ static int api_post(const char *device_id, const char *cookie, const char *path, if (!c->rbuf) return NCM_ERR_OOM; rc = ncm_http_request(&req, c->rbuf, resp_cap, &c->resp); if (rc != NCM_OK) { free(c->rbuf); c->rbuf = NULL; return rc; } + rc = api_status_gate(&c->resp); + if (rc != NCM_OK) { free(c->rbuf); c->rbuf = NULL; return rc; } return api_parse_body(c); } @@ -570,7 +584,9 @@ int ncm_api_parse_vip_info(const char *json, size_t len, int64_t now_ms, ncm_vip long code = 200; if (ncm_json_path_int(&j, "code", &code) == NCM_OK && code != 200) { free(toks); free(parent); - return (code == 301 || code == -460) ? NCM_ERR_AUTH : NCM_ERR_HTTP; + if (code == 301 || code == 302) return NCM_ERR_AUTH; + if (code == -460) return NCM_ERR_SVC; /* 风控:暂不可用,不是登录失效 */ + return NCM_ERR_HTTP; } /* root = data 对象优先,否则 body 本身(v2 挂 data,v1 字段直接在根部) */ diff --git a/patch_v2/ncm_app/core/ncm_apicache.c b/patch_v2/ncm_app/core/ncm_apicache.c index d4548ef..c2ee9aa 100644 --- a/patch_v2/ncm_app/core/ncm_apicache.c +++ b/patch_v2/ncm_app/core/ncm_apicache.c @@ -44,6 +44,7 @@ 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; @@ -213,6 +214,7 @@ 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; } @@ -222,12 +224,23 @@ 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) ncm_apicache_put(key, buf, *len); + if (rc == NCM_OK) { + if (len) ncm_apicache_put(key, buf, *len); + g_hits++; + } 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 af20575..ba4ef18 100644 --- a/patch_v2/ncm_app/core/ncm_apicache.h +++ b/patch_v2/ncm_app/core/ncm_apicache.h @@ -55,6 +55,10 @@ void ncm_apicache_ttl_set(int seconds); /* 当前内存条目数(诊断/测试)。 */ int ncm_apicache_mem_count(void); +/* 命中计数(单调递增;M9 离线判定用:App 据此区分「真·网络成功」与「离线时读到旧缓存」, + * 避免离线提示/账号态被缓存命中误判为「已回在线」)。 */ +uint64_t ncm_apicache_hits(void); + #ifdef __cplusplus } #endif diff --git a/patch_v2/ncm_app/core/ncm_common.h b/patch_v2/ncm_app/core/ncm_common.h index b12055e..e6059c0 100644 --- a/patch_v2/ncm_app/core/ncm_common.h +++ b/patch_v2/ncm_app/core/ncm_common.h @@ -32,6 +32,8 @@ enum { NCM_ERR_LIMIT = -14, /* 超出上限级 */ NCM_ERR_VIP = -15, /* VIP 专享(fee 门槛命中,未发 song_url 请求,§20) */ NCM_ERR_STALE = -16, /* 异步结果过期(打开歌单 A 未完→打开 B,A 的迟到结果被丢弃,§26) */ + NCM_ERR_SVC = -17, /* 服务端暂不可用(HTTP 5xx/429、业务码 -460 风控):可重试, + * **不是**登录失效(M9:不得据此清登录态/退扫码) */ }; /* ---------- 常用尺寸 ---------- */ diff --git a/patch_v2/ncm_app/core/ncm_http.c b/patch_v2/ncm_app/core/ncm_http.c index 524da1d..5fbfe2d 100644 --- a/patch_v2/ncm_app/core/ncm_http.c +++ b/patch_v2/ncm_app/core/ncm_http.c @@ -322,6 +322,30 @@ static int body_feed(body_st *b, const unsigned char *data, size_t len, sink_t * return (int)i; } +/* 传输层返回值归一(M9):mbedtls 的裸负码(如握手失败 -0x7280)此前会原样穿透给上层, + * 上层按 NCM_ERR_* 分类时落到「未知错误」,既判不了离线也判不了鉴权——断网时的失败因此 + * 不可信。此处统一收敛:超时 → NCM_ERR_TIMEOUT;证书校验失败 → NCM_ERR_TLS;其它传输失败 + * (DNS/连接/握手/读写/对端 reset)→ NCM_ERR_NET。 */ +static int http_ret_norm(int ret) +{ + switch (ret) { + case NCM_OK: + case NCM_ERR_ARG: + case NCM_ERR_OOM: + case NCM_ERR_IO: + case NCM_ERR_NET: + case NCM_ERR_TLS: + case NCM_ERR_TIMEOUT: + case NCM_ERR_FULL: + return ret; + default: + break; + } + if (ret == MBEDTLS_ERR_SSL_TIMEOUT) return NCM_ERR_TIMEOUT; + if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) return NCM_ERR_TLS; + return NCM_ERR_NET; +} + /* ---------- 核心请求 ---------- */ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *resp) { int ret; @@ -505,7 +529,7 @@ done: mbedtls_ssl_config_free(&conf); mbedtls_x509_crt_free(&cacert); if (s_rng_fp) { fclose(s_rng_fp); s_rng_fp = NULL; } - return ret; + return (ret == NCM_OK) ? NCM_OK : http_ret_norm(ret); } int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_http_resp_t *resp) { diff --git a/patch_v2/ncm_app/core/ncm_login.c b/patch_v2/ncm_app/core/ncm_login.c index 000d6af..03e8e2d 100644 --- a/patch_v2/ncm_app/core/ncm_login.c +++ b/patch_v2/ncm_app/core/ncm_login.c @@ -4,6 +4,7 @@ #include "ncm_http.h" #include "ncm_json.h" #include "ncm_req.h" +#include "ncm_session.h" #include #include @@ -161,6 +162,12 @@ int ncm_login_status_check(const char *device_id, const char *cookie, ncm_http_resp_t resp; rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); if (rc != NCM_OK) return rc; + /* M9:HTTP 状态先定性——401/403 才是鉴权失败;5xx/429 是暂不可用(不得清登录态); + * 其它非 2xx 是请求/服务异常。status<100(响应头未解析)沿用 body 判定。 */ + if (resp.status >= 100) { + int se = ncm_http_status_err(resp.status); + if (se != NCM_OK) return se; + } ncm_json_t j; jsmntok_t toks[512]; @@ -170,7 +177,12 @@ int ncm_login_status_check(const char *device_id, const char *cookie, long code; if (ncm_json_path_int(&j, "code", &code) != NCM_OK) return NCM_ERR_JSON; - if (code != 200) return NCM_ERR_AUTH; + if (code != 200) { + /* 服务端明确否定会话(301/302)→ AUTH;-460 风控 → SVC(可重试,不是过期)。 */ + if (code == 301 || code == 302) return NCM_ERR_AUTH; + if (code == -460) return NCM_ERR_SVC; + return NCM_ERR_HTTP; + } /* 未登录(guest)时 code 可能仍为 200 但 profile 为 null —— 必须 profile 是对象才算已登录 */ int ptok = ncm_json_path(&j, "profile"); if (ptok < 0 || ncm_json_type(&j, ptok) != JSMN_OBJECT) return NCM_ERR_AUTH; @@ -210,7 +222,11 @@ int ncm_login_refresh(const char *device_id, const char *cookie, ncm_http_resp_t resp; rc = ncm_http_request(&req, respbuf, sizeof respbuf, &resp); if (rc != NCM_OK) return rc; /* 网络/TLS/超时 → 调用方退避 */ - if (resp.status != 200) return NCM_ERR_HTTP; /* 异常 HTTP → 调用方退避 */ + /* M9:401/403 → 会话确失效(AUTH);5xx/429 → SVC 暂不可用(退避,绝不清登录态)。 */ + if (resp.status >= 100) { + int se = ncm_http_status_err(resp.status); + if (se != NCM_OK) return se; + } /* 提取响应 set-cookie(会话轮换:MUSIC_U/__csrf),patch 进现存 cookie */ char captured[4096] = ""; @@ -224,7 +240,13 @@ int ncm_login_refresh(const char *device_id, const char *cookie, long code = -1; if (ncm_json_path_int(&j, "code", &code) != NCM_OK) return NCM_ERR_JSON; - if (code != 200) return NCM_ERR_AUTH; /* 301=未登录 / -460=风控 / 其它 → 会话失效,重登 */ + if (code != 200) { + /* 301/302=未登录/会话失效 → AUTH(重登);-460=风控 → SVC(退避,保留旧 cookie); + * 其它业务码语义不明 → HTTP(退避,不因一次未知响应把用户踢回扫码)。 */ + if (code == 301 || code == 302) return NCM_ERR_AUTH; + if (code == -460) return NCM_ERR_SVC; + return NCM_ERR_HTTP; + } if (out_cookie) ncm_req_cookie_merge(cookie, captured, out_cookie, out_cookie_sz); diff --git a/patch_v2/ncm_app/core/ncm_login.h b/patch_v2/ncm_app/core/ncm_login.h index add1ccb..8965d8a 100644 --- a/patch_v2/ncm_app/core/ncm_login.h +++ b/patch_v2/ncm_app/core/ncm_login.h @@ -43,18 +43,22 @@ typedef struct ncm_login_poll { int ncm_login_poll(const char *device_id, const char *unikey, const char *cur_cookie, ncm_login_poll_t *r, char *out_cookie, size_t out_cookie_sz); -/* login_status(weapi):校验 cookie 是否仍有效。有效写 uid/nickname 并返回 NCM_OK;失效返回 NCM_ERR_AUTH。 */ +/* login_status(weapi):校验 cookie 是否仍有效。有效写 uid/nickname 并返回 NCM_OK; + * 服务端明确否定(HTTP 401/403、业务码 301/302、profile 为空)返回 NCM_ERR_AUTH; + * 网络/超时/TLS → NCM_ERR_NET/TIMEOUT/TLS;5xx/429、业务码 -460 → NCM_ERR_SVC + * (调用方**不得**据此清登录态,见 core/ncm_session.h 的登录门语义)。 */ int ncm_login_status_check(const char *device_id, const char *cookie, char *uid, size_t usz, char *nickname, size_t nsz); /* 登录续期(eapi /api/login/token/refresh,空体):带当前 cookie 换取新登录态。 * 成功(code==200)把响应 set-cookie 的新 MUSIC_U/__csrf patch 进 cookie 写 out_cookie 并返 - * NCM_OK;code!=200(301/-460/会话失效)返 NCM_ERR_AUTH(调用方应清登录态进扫码);网络/TLS/ - * 超时/HTTP 异常返负错误码(调用方退避,保持旧 cookie)。 */ + * NCM_OK;服务端明确否定(301/302)返 NCM_ERR_AUTH(调用方清登录态进扫码); + * -460(风控)/其它非 200 返 NCM_ERR_SVC/NCM_ERR_HTTP(退避,保持旧 cookie); + * 网络/TLS/超时/HTTP 5xx 返负错误码(调用方退避,保持旧 cookie)。 */ int ncm_login_refresh(const char *device_id, const char *cookie, char *out_cookie, size_t out_cookie_sz); -/* 会话 cookie 加密落盘/读回(机器绑定密钥,仅防裸读)。 */ +/* 会话 cookie 加密落盘/读回(设备私有 .mkey 派生密钥,仅防裸读;旧 MAC 密文自动迁移)。 */ int ncm_login_save_cookie(const ncm_store_t *store, const char *cookie); int ncm_login_load_cookie(const ncm_store_t *store, char *cookie, size_t sz); int ncm_login_clear_cookie(const ncm_store_t *store); diff --git a/patch_v2/ncm_app/core/ncm_session.c b/patch_v2/ncm_app/core/ncm_session.c new file mode 100644 index 0000000..0cadcda --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_session.c @@ -0,0 +1,87 @@ +/* ncm_session.c — 会话保持 / 离线模式的错误分类与登录门决策(纯逻辑,见头文件语义) */ +#include "ncm_session.h" + +ncm_fail_class_t ncm_err_classify(int rc) +{ + switch (rc) { + case NCM_OK: return NCM_FAIL_NONE; + case NCM_ERR_AUTH: return NCM_FAIL_AUTH; + case NCM_ERR_NET: + case NCM_ERR_TLS: + case NCM_ERR_TIMEOUT: + case NCM_ERR_SVC: return NCM_FAIL_NETWORK; + default: break; + } + /* 防御:传输层历史泄漏的 mbedtls 裸负码(远小于 NCM 错误码区间)仍按网络类处理, + * 避免「连不上」被上层当未知错误而走上退出登录/报错数据的分支。 */ + if (rc < -100) return NCM_FAIL_NETWORK; + return NCM_FAIL_OTHER; +} + +int ncm_err_is_network(int rc) +{ + return ncm_err_classify(rc) == NCM_FAIL_NETWORK; +} + +ncm_http_cls_t ncm_http_status_class(int status) +{ + if (status >= 200 && status <= 299) return NCM_HTTP_CLS_OK; + if (status == 401 || status == 403) return NCM_HTTP_CLS_AUTH; + if (status == 408 || status == 425 || status == 429) return NCM_HTTP_CLS_TRANSIENT; + if (status >= 500 && status <= 599) return NCM_HTTP_CLS_TRANSIENT; + return NCM_HTTP_CLS_ERROR; +} + +int ncm_http_status_err(int status) +{ + switch (ncm_http_status_class(status)) { + case NCM_HTTP_CLS_OK: return NCM_OK; + case NCM_HTTP_CLS_AUTH: return NCM_ERR_AUTH; + case NCM_HTTP_CLS_TRANSIENT: return NCM_ERR_SVC; + default: return NCM_ERR_HTTP; + } +} + +ncm_gate_t ncm_gate_decide(int logged_in, int rc) +{ + if (!logged_in) return NCM_GATE_RELOGIN; + if (rc == NCM_OK) return NCM_GATE_ONLINE; + switch (ncm_err_classify(rc)) { + case NCM_FAIL_AUTH: return NCM_GATE_RELOGIN; + case NCM_FAIL_NETWORK: return NCM_GATE_OFFLINE; + default: return NCM_GATE_HOLD; + } +} + +ncm_gate_t ncm_session_boot_decide(int has_cookie, int probe_rc) +{ + if (!has_cookie) return NCM_GATE_RELOGIN; + if (probe_rc == NCM_OK) return NCM_GATE_ONLINE; + switch (ncm_err_classify(probe_rc)) { + case NCM_FAIL_AUTH: return NCM_GATE_RELOGIN; + case NCM_FAIL_NETWORK: return NCM_GATE_OFFLINE; + default: return NCM_GATE_ONLINE; /* 不确定就不赶人 */ + } +} + +const char *ncm_gate_name(ncm_gate_t g) +{ + switch (g) { + case NCM_GATE_ONLINE: return "online"; + case NCM_GATE_OFFLINE: return "offline"; + case NCM_GATE_RELOGIN: return "relogin"; + case NCM_GATE_HOLD: return "hold"; + default: return "?"; + } +} + +const char *ncm_fail_class_name(ncm_fail_class_t c) +{ + switch (c) { + case NCM_FAIL_NONE: return "none"; + case NCM_FAIL_NETWORK: return "network"; + case NCM_FAIL_AUTH: return "auth"; + case NCM_FAIL_OTHER: return "other"; + default: return "?"; + } +} diff --git a/patch_v2/ncm_app/core/ncm_session.h b/patch_v2/ncm_app/core/ncm_session.h new file mode 100644 index 0000000..606ad4f --- /dev/null +++ b/patch_v2/ncm_app/core/ncm_session.h @@ -0,0 +1,73 @@ +/* ncm_session.h — 会话保持 / 离线模式:错误分类与登录门决策(纯逻辑,无 IO/无 UI) + * + * 用户定案(M9):网络不可达 / 超时 / DNS / 连接失败 / 服务端 5xx 等**传输层或服务端暂不可用** + * 一律保持账号态、进入离线模式(可看可播缓存列表,不弹扫码);只有服务端**明确**否定会话 + * (HTTP 401/403、业务码 301/302 等)才沿用原退出登录逻辑回扫码。 + * + * 分层:http 层把 mbedtls 裸负码归一为 NCM_ERR_*(ncm_http.c);api/login 层把 HTTP 状态与 + * 业务码映射为 NCM_ERR_*(ncm_api.c / ncm_login.c);本模块只做「错误码 → 语义分类」与 + * 「分类 → 登录门动作」的纯判定,供 app 层(g_st.offline / 回登录页)与宿主单测共用。 + */ +#ifndef NCM_SESSION_H +#define NCM_SESSION_H + +#include "ncm_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* 失败分类 */ +typedef enum { + NCM_FAIL_NONE = 0, /* 成功 */ + NCM_FAIL_NETWORK, /* 传输层/服务端暂不可用:可重试;保留账号态,进离线 */ + NCM_FAIL_AUTH, /* 服务端明确未登录/鉴权失败:退出登录,回扫码 */ + NCM_FAIL_OTHER, /* 参数/解析/数据/文件错误:既不算离线也不登出 */ +} ncm_fail_class_t; + +/* 登录门动作(一次网络动作的结果落到哪) */ +typedef enum { + NCM_GATE_ONLINE = 0, /* 成功:在线(离线模式随之解除) */ + NCM_GATE_OFFLINE, /* 网络类失败:保持登录态 + 离线模式 */ + NCM_GATE_RELOGIN, /* 鉴权失败(或本地无会话):清登录态,回扫码 */ + NCM_GATE_HOLD, /* 其它错误:保持现状(不改登录态/离线态) */ +} ncm_gate_t; + +/* HTTP 状态分类(api/login 层的状态门用) */ +typedef enum { + NCM_HTTP_CLS_OK = 0, /* 2xx */ + NCM_HTTP_CLS_AUTH, /* 401/403:明确鉴权失败 */ + NCM_HTTP_CLS_TRANSIENT, /* 408/425/429/5xx:暂不可用,可重试 */ + NCM_HTTP_CLS_ERROR, /* 其它非 2xx(含 3xx):请求/服务异常,非鉴权 */ +} ncm_http_cls_t; + +/* NCM_ERR_* → 失败分类。未知负码(含历史泄漏的 mbedtls 裸码)按 NETWORK 兜底: + * 宁可保留登录态/进离线,也不把「连不上」误判成「会话过期」。 */ +ncm_fail_class_t ncm_err_classify(int rc); +/* 便捷判定:rc 是否属于网络/服务端暂不可用类。 */ +int ncm_err_is_network(int rc); + +/* HTTP 状态 → 分类;→ 对应 NCM_ERR_*(AUTH / SVC / HTTP / OK)。 */ +ncm_http_cls_t ncm_http_status_class(int status); +int ncm_http_status_err(int status); + +/* 登录门决策:logged_in=本地是否有会话(cookie 非空)。 + * !logged_in → RELOGIN(无会话可用) + * rc==NCM_OK → ONLINE + * AUTH → RELOGIN + * NETWORK → OFFLINE(保持账号态) + * 其它 → HOLD(保持现状,UI 自行提示重试) */ +ncm_gate_t ncm_gate_decide(int logged_in, int rc); + +/* 启动决策:本地会话是唯一凭据——有会话就先信它进 App(无网也进 home/缓存), + * 只有服务端明确否定才回扫码。probe_rc 为启动探测/首个请求的返回码(无探测=NCM_OK)。 */ +ncm_gate_t ncm_session_boot_decide(int has_cookie, int probe_rc); + +/* 可读名(日志/单测断言输出)。 */ +const char *ncm_gate_name(ncm_gate_t g); +const char *ncm_fail_class_name(ncm_fail_class_t c); + +#ifdef __cplusplus +} +#endif +#endif /* NCM_SESSION_H */