优化:日志输出配置

This commit is contained in:
ivan 2026-04-16 17:00:55 +08:00
parent 59ab3b247a
commit 66bbf38c64
11 changed files with 346 additions and 8 deletions

View File

@ -4,7 +4,9 @@ import android.app.ActivityManager
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.facebook.FacebookSdk
import com.facebook.LoggingBehavior
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -54,12 +56,25 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"setFacebookSdkDebugLogging" -> {
val ctx = applicationContext
if (ctx == null) {
result.error("NO_CONTEXT", "applicationContext null", null)
return
}
val enabled = call.argument<Boolean>("enabled") ?: false
applyFacebookSdkDebugLogging(enabled)
result.success(null)
}
"waitForFacebookSdkInit" -> {
val ctx = applicationContext
if (ctx == null) {
result.error("NO_CONTEXT", "applicationContext null", null)
return
}
val args = call.arguments as? Map<*, *>
val debugLogs = coerceFacebookSdkDebugLogsArg(args?.get("facebookSdkDebugLogs"))
applyFacebookSdkDebugLogging(debugLogs)
val mainHandler = Handler(Looper.getMainLooper())
try {
// SDK 18 无 addInitializedCallback用 sdkInitialize(..., InitializeCallback) 在就绪后回调
@ -71,6 +86,8 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle
override fun onInitialized() {
mainHandler.post {
try {
// 再应用一次:避免 FacebookInitProvider / 其他插件抢先初始化导致标志被吃掉
applyFacebookSdkDebugLogging(debugLogs)
channel?.invokeMethod("onFacebookSdkInitialized", null)
result.success(true)
} catch (e: Exception) {
@ -88,6 +105,41 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle
}
}
/**
* 由宿主 `skin_config` `facebook.debugLogs` MethodChannel `setFacebookSdkDebugLogging` 控制
* 关闭时恢复为 SDK 默认仅保留 [LoggingBehavior.DEVELOPER_ERRORS]
*/
private fun applyFacebookSdkDebugLogging(enabled: Boolean) {
FacebookSdk.setIsDebugEnabled(enabled)
FacebookSdk.clearLoggingBehaviors()
if (enabled) {
FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS)
FacebookSdk.addLoggingBehavior(LoggingBehavior.REQUESTS)
FacebookSdk.addLoggingBehavior(LoggingBehavior.DEVELOPER_ERRORS)
FacebookSdk.addLoggingBehavior(LoggingBehavior.GRAPH_API_DEBUG_INFO)
Log.i(
"ClientProxyFB",
"Facebook SDK debug logging ON — also filter Logcat: tag FacebookSDK or Request",
)
} else {
FacebookSdk.addLoggingBehavior(LoggingBehavior.DEVELOPER_ERRORS)
Log.i("ClientProxyFB", "Facebook SDK debug logging OFF (verbose behaviors cleared)")
}
}
/** Flutter Map 里可能是 Bool / Int / String避免一直落在默认 false。 */
private fun coerceFacebookSdkDebugLogsArg(raw: Any?): Boolean {
return when (raw) {
is Boolean -> raw
is Number -> raw.toInt() != 0
is String -> {
val s = raw.trim().lowercase()
s == "true" || s == "1" || s == "yes"
}
else -> false
}
}
companion object {
const val CHANNEL_NAME = "client_proxy_framework/facebook_sdk"
const val DEVICE_MEMORY_CHANNEL_NAME = "client_proxy_framework/device_memory"

View File

@ -19,6 +19,8 @@
按模块整理的“功能 + 最小调用示例”请见 **[《Framework 功能与用法总览》](framework_feature_usage.md)**。
以首款换皮应用 **FunyMee** 为参考的 **视频首页**`common_info` → 分类 → 按分类拉模板)数据流说明,见 **[《视频首页数据获取流程》](video_home_data_flow.md)**。
---
## 2. 快速开始

View File

@ -0,0 +1,134 @@
# 视频首页数据获取流程(以首款换皮应用 FunyMee 为参考)
本文描述**框架侧**在典型换皮应用(首款为 **FunyMee**)中,从冷启动到「视频首页」可展示分类与模板列表的**数据链路与调用顺序**。FunyMee 宿主 UI 不在本仓库内;流程以 `client_proxy_framework` 中的 `FrameworkAuthService``ExtConfigRuntime``VideoHomeRuntime``ImageApi` 为准,与代码注释中引用的线网接口(如 `GET /v1/image/img2video/tasks`)一致。
---
## 1. 前提与配置
1. **`ClientBootstrap.initFromAsset`**:加载 `skin_config.json`,构建 `SkinConfig``ApiClient.init`使后续请求走统一代理、AES 与 V2 包装(见 `lib/src/bootstrap/client_bootstrap.dart`)。
2. **`FrameworkAuthService.init` + `start`**:宿主实现 `AuthServiceCallbacks`(设备 ID、签名等由框架编排登录与 `common_info`(见 `lib/src/services/auth_service.dart`)。
3. **`skin_config.json`** 中与首页相关的片段:
- **`extConfig`**`keys.showVideoMenu` 映射到线网布尔(示例中为 `go_run` / `need_wait` 等,见 `lib/src/config/skin_config.example.json``items``taskItemMapping` 决定 `common_info` 下发的运营位卡片如何解析为与任务列表同形的 `ExtConfigItem`
- **`videoHome`**`imagesTabLabel``imagesTabFirst` 控制「Images」虚拟 Tab 的文案及与接口分类的排序(见 `lib/src/config/skin_config.dart`)。
4. **`fieldMapping`**FunyMee 等换皮下线网字段与逻辑字段不一致时,响应体会先经映射再进入实体解析(注释示例见 `lib/src/services/image_api.dart`)。
---
## 2. 总览时序
```mermaid
sequenceDiagram
participant Host as 宿主 main / UI
participant Boot as ClientBootstrap
participant Auth as FrameworkAuthService
participant User as UserApi / ProxyClient
participant Ext as ExtConfigRuntime
participant VH as VideoHomeRuntime
participant Img as ImageApi
Host->>Boot: initFromAsset(skin_config)
Host->>Auth: init(callbacks); start()
Auth->>User: POST /v1/user/fast_login
User-->>Auth: userToken, userId
Auth->>User: GET /v1/user/common_info
User-->>Auth: extConfig 等
Auth->>Ext: applyCommonInfoSuccess
Auth->>VH: hydrateAfterCommonInfo (异步)
VH->>Img: GET /v1/image/img2video/categories
Img-->>VH: 分类列表
VH->>VH: 合并 Tabs接口分类 + ext items → Images
VH->>Img: GET /v1/image/img2video/tasks?categoryId=…
Img-->>VH: 当前分类模板列表
```
---
## 3. 分阶段说明
### 3.1 启动与快速登录
- 延迟与重试策略由 `FrameworkAuthService.start` 控制(默认启动延迟、登录重试等)。
- **`UserApi.fastLogin`**`POST /v1/user/fast_login`。请求头**仅**带 `pkg`**不带** `User_token`(见 `UserApi` 文档注释)。
- 成功后框架将返回的 `userToken` 写入 **`ApiClient.instance.setUserToken`**,此后代理请求自动附带 `pkg``User_token`(见 `ProxyClient` 行为与 `UserApi` 说明)。
### 3.2 归因上报(与首页数据并行准备)
- 在拉取 `common_info` 之前,若存在 Adjust / Play 等 referrer会依次调用 **`UserApi.referrer`**`POST /v1/user/referrer`)上报,不阻塞后续 `common_info` 的成功与否结论,但属于同一 `_reportReferrersAndLoadCommonInfo` 流程(见 `auth_service.dart`)。
### 3.3 通用信息与 extConfig
- **`UserApi.getCommonInfo`**`GET /v1/user/common_info`query 含 `app`iOS/Android 后端渠道,与 `skin_config.backend` 一致)、`pkg``userId``deviceId` 等(见 `user_api.dart`)。
- 成功时调用 **`ExtConfigRuntime.applyCommonInfoSuccess`**
- 将 `extConfig` 与本地 `extConfig.defaults` 浅合并;
- 按 `extConfigKeySchema` 解析为 **`ExtConfigData`**(含 `showVideoMenu``items` 等)(见 `ext_config_runtime.dart``ext_config_models.dart`)。
- **`ExtConfigRuntime.commonInfoSucceeded`**:宿主可用「登录完成且 `common_info` 成功」再展示主业务界面(见 `ext_config_runtime.dart` 注释建议)。
### 3.4 视频首页运行时水合hydrate
`common_info` **成功**后,`FrameworkAuthService` 会 **fire-and-forget** 调用 **`VideoHomeRuntime.hydrateAfterCommonInfo`**(不阻塞 `loginComplete` 的完成)(见 `auth_service.dart`)。
`hydrateAfterCommonInfo` 的进入条件与行为(见 `video_home_runtime.dart`
| 条件 | 行为 |
|------|------|
| `userId` 为空,或 `ExtConfigData.showVideoMenu != true` | **`VideoHomeRuntime.reset`**,不拉分类、无 images Tab |
| 否则 | 置 `snapshot.loading = true`,再请求分类列表 |
水合步骤概要:
1. **`ImageApi.getCategoryList`** → `GET /v1/image/img2video/categories`,得到服务端分类(`CategoryItem`,含 `id` / `name`)。
2. 从 **`ExtConfigRuntime.data`** 读取 `items`,过滤 **`ExtConfigItem.isUsableOnHome`**,非空则存在 **Images** 虚拟 Tab文案为 `AppConfig.videoHomeImagesTabLabel`(来自 `skin_config.videoHome.imagesTabLabel`)。
3. **Tab 顺序**:由 `videoHomeImagesTabFirst` 决定是先 Images 还是先接口分类。
4. **默认选中 Tab**:默认选中**第一个服务端分类**(非 Images若仅有 Images Tab 则下标为 0并发水合与用户提前切换时有保护逻辑`video_home_runtime.dart` 内注释)。
5. 水合结束后调用 **`VideoHomeRuntime.ensureTabItems(selectedTabIndex)`**,为当前 Tab 拉取模板数据(见下节)。
若分类接口失败且没有任何 Tab 可构建,`snapshot.error` 会携带失败信息。
### 3.5 按 Tab 拉取模板列表
- **Images Tab**:数据直接来自 **`ExtConfigData.items`**(已在水合前解析),**不再**请求 `img2video/tasks`
- **服务端分类 Tab**:首次选中该 Tab 时,**`VideoHomeRuntime.ensureTabItems`** 调用 **`ImageApi.getImg2VideoTasks(categoryId: id)`** → `GET /v1/image/img2video/tasks?categoryId=...`(与 FunyMee 文档一致)。结果通过 `ExtConfigItem.fromTaskItem` 写入 **`VideoHomeSnapshot.networkItemsByCategoryId`**,并按分类 id 缓存,避免重复请求。
---
## 4. 宿主 UI 对接要点
- **监听状态**`VideoHomeRuntime.snapshot``VideoHomeRuntime.selectedTabIndex``ValueNotifier`,可用 `ValueListenableBuilder` 或类似方式驱动顶栏 Tab 与内容区。
- **切换 Tab**:切换 `selectedTabIndex` 后应调用 **`VideoHomeRuntime.ensureTabItems(newIndex)`**(框架水合末尾已对初始 Tab 调用一次),以按需加载该分类下的模板列表。
- **登录 / common_info 失败**`ExtConfigRuntime.commonInfoSucceeded == false``userId` 为空时,不会进入视频首页水合;宿主应展示错误态或重试入口。
---
## 5. 相关 HTTP 接口汇总
| 顺序 | 方法 | 路径 | 用途 |
|------|------|------|------|
| 1 | POST | `/v1/user/fast_login` | 设备登录,取得 token |
| 2 | POST | `/v1/user/referrer` | 可选,归因上报 |
| 3 | GET | `/v1/user/common_info` | 开关与 `extConfig`(含首页运营位 `items` |
| 4 | GET | `/v1/image/img2video/categories` | 视频首页顶栏服务端分类 |
| 5 | GET | `/v1/image/img2video/tasks` | 某分类下模板列表query`categoryId` |
所有经 `ProxyClient` 的业务请求均走 **`skin_config` 中的 `proxyPath`**body 为加密后的代理载荷;响应经解密与 **`fieldMapping`** 还原为逻辑字段后再解析实体。
---
## 6. 代码索引(便于跳转)
| 模块 | 路径 |
|------|------|
| 启动换皮配置 | `lib/src/bootstrap/client_bootstrap.dart` |
| 登录与 common_info 编排 | `lib/src/services/auth_service.dart` |
| 用户接口 | `lib/src/services/user_api.dart` |
| 图/视频分类与任务列表 | `lib/src/services/image_api.dart` |
| extConfig 运行时 | `lib/src/config/ext_config_runtime.dart` |
| 视频首页 Tab 与缓存 | `lib/src/config/video_home_runtime.dart` |
| extConfig / items 解析 | `lib/src/config/ext_config_models.dart` |
| JSON 配置示例 | `lib/src/config/skin_config.example.json` |
---
## 7. 与《创建新换皮应用》的关系
从零搭建宿主工程、资产路径与 `main` 初始化顺序,仍以 **[create_new_skin_app.md](create_new_skin_app.md)** 为准;本文仅补充 **「视频首页」在框架内的数据流**,与 FunyMee 首推路线一致。

View File

@ -1,5 +1,7 @@
import Flutter
import UIKit
import FBSDKCoreKit
import os.log
public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
private var channel: FlutterMethodChannel?
@ -14,11 +16,53 @@ public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "waitForFacebookSdkInit" {
switch call.method {
case "setFacebookSdkDebugLogging":
let args = call.arguments as? [String: Any]
let enabled = Self.coerceFacebookSdkDebugLogsArg(args?["enabled"])
Self.applyFacebookSdkDebugLogging(enabled: enabled)
result(nil)
case "waitForFacebookSdkInit":
let args = call.arguments as? [String: Any]
let debugLogs = Self.coerceFacebookSdkDebugLogsArg(args?["facebookSdkDebugLogs"])
Self.applyFacebookSdkDebugLogging(enabled: debugLogs)
result(true)
channel?.invokeMethod("onFacebookSdkInitialized", arguments: nil)
} else {
default:
result(FlutterMethodNotImplemented)
}
}
/// Android `facebook.debugLogs` Dart `setFacebookSdkDebugLogging`
private static func applyFacebookSdkDebugLogging(enabled: Bool) {
if enabled {
Settings.shared.loggingBehaviors = Set([
.appEvents,
.networkRequests,
.informational,
.developerErrors,
.graphAPIDebugInfo,
])
os_log("Facebook SDK debug logging ON (filter console for FBSDK / Facebook)", log: .default, type: .info)
print("ClientProxyFB: Facebook SDK debug logging ON — also search Xcode console for FBSDK / FacebookSDK")
} else {
Settings.shared.loggingBehaviors = []
os_log("Facebook SDK debug logging OFF", log: .default, type: .info)
print("ClientProxyFB: Facebook SDK debug logging OFF")
}
}
private static func coerceFacebookSdkDebugLogsArg(_ raw: Any?) -> Bool {
switch raw {
case let b as Bool:
return b
case let n as NSNumber:
return n.intValue != 0
case let s as String:
let v = s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return v == "true" || v == "1" || v == "yes"
default:
return false
}
}
}

View File

@ -11,6 +11,8 @@ Pod::Spec.new do |s|
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
# 与 facebook_app_events 一致,便于在桥接层开启 FBSDK 调试日志
s.dependency 'FBSDKCoreKit', '~> 18.0'
s.platform = :ios, '13.0'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'

View File

@ -32,6 +32,7 @@ class FacebookConfig {
const FacebookConfig({
required this.appId,
this.clientToken,
/// ** Facebook SDK** Logcat / Xcode [AnalyticsConfig.debugLogs]
this.debugLogs = false,
});

View File

@ -351,7 +351,7 @@ class SkinConfig implements AppConfig {
fbCfg = FacebookConfig(
appId: id.trim(),
clientToken: ct.isEmpty ? null : ct,
debugLogs: fb['debugLogs'] as bool? ?? false,
debugLogs: _coerceConfigBool(fb['debugLogs']),
);
} else if (id.trim().isNotEmpty || clientTok.trim().isNotEmpty) {
SdkReminderLog.facebook(
@ -415,4 +415,16 @@ class SkinConfig implements AppConfig {
}
return const PlatformAttributionConfig();
}
/// JSON / `1`/`"true"` `as bool?` false
static bool _coerceConfigBool(dynamic value, {bool fallback = false}) {
if (value == true || value == 1) return true;
if (value == false || value == 0) return false;
if (value is String) {
final s = value.trim().toLowerCase();
if (s == 'true' || s == '1' || s == 'yes') return true;
if (s == 'false' || s == '0' || s == 'no') return false;
}
return fallback;
}
}

View File

@ -96,7 +96,14 @@ void _logLong(String text) {
/// JSON
void logWithEmbeddedJson(Object? msg) {
if (!kDebugMode) return;
if (kReleaseMode) {
final releaseLevel =
const String.fromEnvironment('APP_LOG_LEVEL').trim().toLowerCase();
final allowVerbose = releaseLevel == 'all' ||
releaseLevel == 'trace' ||
releaseLevel == 'debug';
if (!allowVerbose) return;
}
if (msg is! String) {
_proxyLog.d(msg);
@ -184,6 +191,38 @@ class AppLogger {
static Logger? _logger;
static bool _releaseVerboseEnabled() {
if (!kReleaseMode) return false;
final raw = const String.fromEnvironment('APP_LOG_LEVEL').trim().toLowerCase();
return raw == 'all' || raw == 'trace' || raw == 'debug';
}
static Level _resolveLogLevel() {
final raw = const String.fromEnvironment('APP_LOG_LEVEL').trim().toLowerCase();
switch (raw) {
case 'all':
case 'trace':
return Level.trace;
case 'debug':
return Level.debug;
case 'info':
return Level.info;
case 'warning':
case 'warn':
return Level.warning;
case 'error':
return Level.error;
case 'fatal':
case 'wtf':
return Level.fatal;
case 'off':
case 'none':
return Level.off;
default:
return kReleaseMode ? Level.warning : Level.trace;
}
}
static Logger get _instance {
_logger ??= Logger(
printer: PrettyPrinter(
@ -194,14 +233,22 @@ class AppLogger {
printEmojis: true,
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
),
level: kReleaseMode ? Level.warning : Level.trace,
level: _resolveLogLevel(),
);
return _logger!;
}
String _msg(Object? message) => '[$tag] $message';
void d(Object? message) => _instance.d(_msg(message));
void d(Object? message) {
final m = _msg(message);
if (_releaseVerboseEnabled()) {
// Release + trace/debug logger
debugPrint(m);
return;
}
_instance.d(m);
}
void i(Object? message) => _instance.i(_msg(message));
void w(Object? message) => _instance.w(_msg(message));
void e(Object? message, [Object? error, StackTrace? stackTrace]) =>

View File

@ -78,7 +78,7 @@ abstract final class AnalyticsEvents {
}
}
/// Adjust `purchase` `firstPurchase`Facebook [AnalyticsService.trackPurchase]
/// Adjust `purchase` `firstPurchase` + Facebook `FirstRecharge` app_client [AdjustEvents.trackFirstPurchase] Facebook [AnalyticsService.trackPurchase]
static Future<void> trackPurchaseSuccess(double amount) async {
_skin.trackAdjustEvent('purchase');
final prefs = await SharedPreferences.getInstance();
@ -87,6 +87,10 @@ abstract final class AnalyticsEvents {
final today = DateTime.now().toIso8601String().substring(0, 10);
if (regDate != null && regDate == today) {
_skin.trackAdjustEvent('firstPurchase');
FacebookService.logEvent(
'FirstRecharge',
parameters: <String, dynamic>{'amount': amount},
);
}
if (amount > 0) {
AnalyticsService.trackPurchase(amount: amount, currency: 'USD');

View File

@ -152,6 +152,8 @@ abstract class FrameworkAuthService {
}
if (res == null) {
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
completer.complete();
return;
}
@ -180,9 +182,13 @@ abstract class FrameworkAuthService {
deviceId: deviceId,
);
} else {
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
_callbacks!.onLoginFailed(res.msg);
}
} catch (e, st) {
VideoHomeRuntime.reset();
ExtConfigRuntime.applyCommonInfoFailure();
if (kDebugMode) {
debugPrint('[AuthService] start: 异常 $e\n$st');
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../config/attribution_config.dart';
@ -43,9 +44,22 @@ class FacebookService {
});
await _channel!
.invokeMethod<void>('waitForFacebookSdkInit')
.invokeMethod<void>(
'waitForFacebookSdkInit',
<String, dynamic>{
'facebookSdkDebugLogs': config.debugLogs,
},
)
.timeout(_nativeInitTimeout);
_log.d('waitForFacebookSdkInit finished');
if (config.debugLogs) {
debugPrint(
'[Facebook] 已请求原生打开 Facebook SDK 调试日志skin: analytics.facebook.debugLogs'
' 若看不到 FacebookSDK 相关行:请「完全重启」应用(改 asset 后热重载无效)、'
'Android Logcat 级别选 Verbose/Debug 并搜索 FacebookSDK 或 ClientProxyFB'
' 关闭自动 App Events 时需有网络请求或手动 logEvent/activateApp 才会有大量 REQUESTS 日志。',
);
}
} on TimeoutException catch (e, st) {
SdkReminderLog.facebook(
'原生初始化超时 ($e),已跳过。请检查是否集成框架 Android/iOS 插件、`AndroidManifest` / Info.plist 中 Facebook 配置是否完整。\n$st',
@ -95,6 +109,26 @@ class FacebookService {
}
}
/// 宿 Facebook SDK [FacebookConfig.debugLogs]
///
/// **** [init] [init]
/// `skin_config` `analytics.facebook.debugLogs` `true`
static Future<void> setFacebookSdkDebugLogging(bool enabled) async {
try {
_channel ??= MethodChannel(kFacebookSdkChannelName);
await _channel!.invokeMethod<void>(
'setFacebookSdkDebugLogging',
<String, dynamic>{'enabled': enabled},
);
} on MissingPluginException catch (_) {
SdkReminderLog.facebook(
'setFacebookSdkDebugLogging未注册原生插件已忽略。',
);
} catch (e, st) {
SdkReminderLog.facebook('setFacebookSdkDebugLogging 失败: $e\n$st');
}
}
/// `AndroidManifest` / `Info.plist` App Events
static void activateApp() {
try {