fix account profile and improve operation logs
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../core/http/dio_client.dart';
|
||||
import '../core/http/http.dart';
|
||||
import '../core/logging/app_logger.dart';
|
||||
import '../core/http/http_error.dart';
|
||||
import '../core/config/app_config.dart';
|
||||
|
||||
@@ -153,7 +154,36 @@ class GuangyaAPI {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> userInfo() async {
|
||||
return Http.accountRequest('/v1/user/me', body: null, authenticated: true);
|
||||
// The current account gateway exposes this REST resource as GET. Older
|
||||
// deployments accepted POST, so retain it as a compatibility fallback.
|
||||
try {
|
||||
return await Http.accountRequest(
|
||||
'/v1/user/me',
|
||||
method: 'GET',
|
||||
body: null,
|
||||
authenticated: true,
|
||||
);
|
||||
} on DioException catch (error) {
|
||||
final status = error.response?.statusCode;
|
||||
if (status != 404 && status != 405 && status != 501) rethrow;
|
||||
AppLogger.warning('Auth', '账户资料 GET 接口不可用($status),正在尝试兼容请求');
|
||||
return Http.accountRequest(
|
||||
'/v1/user/me',
|
||||
body: null,
|
||||
authenticated: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 云盘总容量、已用容量、会员与直链流量信息。
|
||||
Future<Map<String, dynamic>> cloudAssets() async {
|
||||
final response = await Http.apiRequest('/assets/v1/get_assets');
|
||||
final data = response['data'];
|
||||
return data is Map<String, dynamic>
|
||||
? data
|
||||
: data is Map
|
||||
? Map<String, dynamic>.from(data)
|
||||
: const {};
|
||||
}
|
||||
|
||||
// ── Cloud downloads ────────────────────────────────────────────────
|
||||
|
||||
+41
-26
@@ -1,4 +1,5 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
@@ -28,10 +29,6 @@ class Http {
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final endpoint = useAccountDio ? 'account' : 'api';
|
||||
log('[HTTP:$endpoint] -> $method $path');
|
||||
|
||||
try {
|
||||
final res = await client.request(
|
||||
path,
|
||||
@@ -40,23 +37,10 @@ class Http {
|
||||
cancelToken: cancelToken,
|
||||
options: mergedOptions,
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
log(
|
||||
'[HTTP:$endpoint] <- $method $path status=${res.statusCode} '
|
||||
'elapsed=${stopwatch.elapsedMilliseconds}ms',
|
||||
);
|
||||
|
||||
return res.data as T;
|
||||
} on DioException catch (e) {
|
||||
stopwatch.stop();
|
||||
final status = e.response?.statusCode;
|
||||
log(
|
||||
'[HTTP:$endpoint] !! $method $path status=${status ?? '-'} '
|
||||
'type=${e.type.name} elapsed=${stopwatch.elapsedMilliseconds}ms',
|
||||
);
|
||||
|
||||
// 将业务错误信息包装为 ApiException
|
||||
final status = e.response?.statusCode;
|
||||
final errorMsg = e.error is String ? (e.error as String) : '';
|
||||
if (errorMsg.isNotEmpty) {
|
||||
throw ApiException(status: status, message: errorMsg);
|
||||
@@ -64,10 +48,6 @@ class Http {
|
||||
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
stopwatch.stop();
|
||||
log(
|
||||
'[HTTP:$endpoint] !! $method $path elapsed=${stopwatch.elapsedMilliseconds}ms',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +101,7 @@ class Http {
|
||||
bool authenticated = false,
|
||||
}) async {
|
||||
final headers = <String, dynamic>{
|
||||
..._accountHeaders(),
|
||||
'did': _getDeviceID(),
|
||||
'dt': '4',
|
||||
...?extraHeaders,
|
||||
@@ -163,9 +144,7 @@ class Http {
|
||||
static void _checkTokenExpiry() {
|
||||
final expiresAt = StorageManager.get<int>(StorageKeys.tokenExpiresAt);
|
||||
if (expiresAt == null) return;
|
||||
if (DateTime.now().millisecondsSinceEpoch > expiresAt) {
|
||||
log('[HTTP] token expired, will refresh on next 401');
|
||||
}
|
||||
if (DateTime.now().millisecondsSinceEpoch > expiresAt) {}
|
||||
}
|
||||
|
||||
static String _getDeviceID() {
|
||||
@@ -173,6 +152,42 @@ class Http {
|
||||
'flutter-${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
/// 账号网关会校验网页客户端的设备标识;与源客户端保持一致。
|
||||
static Map<String, String> _accountHeaders() {
|
||||
final deviceID = _getDeviceID();
|
||||
return {
|
||||
'x-client-id': 'aMe-8VSlkrbQXpUR',
|
||||
'x-client-version': '0.0.1',
|
||||
'x-device-id': deviceID,
|
||||
'x-device-model': 'chrome%2F150.0.0.0',
|
||||
'x-device-name': 'PC-Chrome',
|
||||
'x-device-sign': 'wdi10.$deviceID${_randomHex(16)}',
|
||||
'x-net-work-type': 'NONE',
|
||||
'x-os-version': _webPlatformName(),
|
||||
'x-platform-version': '1',
|
||||
'x-protocol-version': '301',
|
||||
'x-provider-name': 'NONE',
|
||||
'x-sdk-version': '9.0.2',
|
||||
};
|
||||
}
|
||||
|
||||
static String _webPlatformName() {
|
||||
if (Platform.isMacOS) return 'MacIntel';
|
||||
if (Platform.isWindows) return 'Win32';
|
||||
if (Platform.isLinux) return 'Linux x86_64';
|
||||
if (Platform.isIOS) return 'iPhone';
|
||||
if (Platform.isAndroid) return 'Linux armv8l';
|
||||
return Platform.operatingSystem;
|
||||
}
|
||||
|
||||
static String _randomHex(int bytes) {
|
||||
final random = Random.secure();
|
||||
return List.generate(
|
||||
bytes,
|
||||
(_) => random.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
||||
).join();
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
static String _generateTraceparent() {
|
||||
|
||||
@@ -1,34 +1,52 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../http_error.dart';
|
||||
import '../../logging/app_logger.dart';
|
||||
|
||||
class AppLogInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
AppLogger.debug(
|
||||
'HTTP',
|
||||
'→ ${options.method} ${options.uri.path}${options.uri.hasQuery ? '?${options.uri.query}' : ''}',
|
||||
);
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
AppLogger.debug(
|
||||
'HTTP',
|
||||
'← ${response.statusCode ?? 0} ${response.requestOptions.method} ${response.requestOptions.uri.path}',
|
||||
);
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
final status = err.response?.statusCode;
|
||||
final serverMessage = err.response?.data is Map
|
||||
? extractHttpMessage(err.response?.data)
|
||||
: null;
|
||||
final detail = serverMessage?.trim().isNotEmpty == true
|
||||
? serverMessage!
|
||||
: (err.error?.toString().trim().isNotEmpty == true
|
||||
? err.error.toString()
|
||||
: err.type.name);
|
||||
AppLogger.error(
|
||||
'HTTP',
|
||||
'${err.requestOptions.method} ${err.requestOptions.uri.path} 失败',
|
||||
error: err.message,
|
||||
stackTrace: err.stackTrace,
|
||||
'${_actionName(err.requestOptions.path)}失败(状态码 ${status ?? '-'},原因:$detail)',
|
||||
);
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
static String _actionName(String path) {
|
||||
if (path.contains('/v1/user/me')) return '获取账户资料';
|
||||
if (path.contains('/assets/v1/get_assets')) return '获取云盘空间信息';
|
||||
if (path.contains('/search_files')) return '搜索云盘文件';
|
||||
if (path.contains('/get_file_list')) return '读取文件列表';
|
||||
if (path.contains('/get_file_detail')) return '读取文件详情';
|
||||
if (path.contains('/create_dir')) return '新建文件夹';
|
||||
if (path.contains('/move_file')) return '移动文件';
|
||||
if (path.contains('/copy_file')) return '复制文件';
|
||||
if (path.contains('/rename')) return '重命名文件';
|
||||
if (path.contains('/recycle_file')) return '移入回收站';
|
||||
if (path.contains('/delete_file')) return '删除文件';
|
||||
if (path.contains('/auth/token')) return '刷新登录凭据';
|
||||
if (path.contains('/auth/')) return '登录认证';
|
||||
if (path.contains('/tmdb')) return '获取影视资料';
|
||||
return '云盘服务请求';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../logging/app_logger.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../storage/storage_manager.dart';
|
||||
import '../http_error.dart';
|
||||
@@ -60,10 +59,6 @@ class AuthInterceptor extends Interceptor {
|
||||
final status = err.response?.statusCode;
|
||||
final alreadyLoggedOut = status == 401 && _isAlreadyLoggedOut;
|
||||
|
||||
if (!alreadyLoggedOut) {
|
||||
log('[Auth] error path=${err.requestOptions.path} status=$status');
|
||||
}
|
||||
|
||||
// 免认证路径的错误直接放行
|
||||
if (_isAuthExemptPath(err.requestOptions.path)) {
|
||||
return handler.next(err);
|
||||
@@ -127,7 +122,7 @@ class AuthInterceptor extends Interceptor {
|
||||
|
||||
// 开始刷新
|
||||
_isRefreshing = true;
|
||||
log('[Auth] refreshing access token');
|
||||
AppLogger.info('Auth', '登录凭据已过期,正在刷新');
|
||||
|
||||
try {
|
||||
final dio = Dio(
|
||||
@@ -158,7 +153,7 @@ class AuthInterceptor extends Interceptor {
|
||||
),
|
||||
);
|
||||
|
||||
log('[Auth] access token refreshed');
|
||||
AppLogger.info('Auth', '登录凭据刷新完成');
|
||||
|
||||
final data = res.data;
|
||||
final newAccess = _findStringDeep(data, ['access_token', 'accessToken']);
|
||||
@@ -202,7 +197,7 @@ class AuthInterceptor extends Interceptor {
|
||||
final response = await DioClient.dio.fetch(request);
|
||||
return handler.resolve(response);
|
||||
} catch (e) {
|
||||
log('[Auth] token refresh failed: $e');
|
||||
AppLogger.warning('Auth', '登录凭据刷新失败,将退出当前账号');
|
||||
|
||||
for (var c in _waitQueue) {
|
||||
c.complete();
|
||||
@@ -228,7 +223,7 @@ class AuthInterceptor extends Interceptor {
|
||||
if (_logoutScheduled) return;
|
||||
_logoutScheduled = true;
|
||||
|
||||
log('[Auth] logout scheduled');
|
||||
AppLogger.info('Auth', '登录状态已失效,正在退出当前账号');
|
||||
await StorageManager.delete(StorageKeys.accessToken);
|
||||
await StorageManager.delete(StorageKeys.refreshToken);
|
||||
await StorageManager.delete(StorageKeys.tokenExpiresAt);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../http_error.dart';
|
||||
@@ -72,27 +70,6 @@ class ResponseInterceptor extends Interceptor {
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
// 网络错误
|
||||
if (err.type == DioExceptionType.connectionError ||
|
||||
err.type == DioExceptionType.connectionTimeout ||
|
||||
err.type == DioExceptionType.sendTimeout ||
|
||||
err.type == DioExceptionType.receiveTimeout) {
|
||||
if (!suppressErrorToast(err.requestOptions)) {
|
||||
log(
|
||||
'[HTTP] ${requestToastMessage(err.requestOptions, '请求超时或连接失败,请检查网络')}',
|
||||
);
|
||||
}
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
// 其他错误
|
||||
if (!suppressErrorToast(err.requestOptions)) {
|
||||
final msg = err.response?.data is Map
|
||||
? extractHttpMessage(err.response?.data) ?? '请求失败'
|
||||
: '请求失败';
|
||||
log('[HTTP] ${requestToastMessage(err.requestOptions, msg)}');
|
||||
}
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,26 @@ class AppLogEntry {
|
||||
});
|
||||
|
||||
String get text =>
|
||||
'${timestamp.toIso8601String()} [${level.name.toUpperCase()}] '
|
||||
'[$scope] $message';
|
||||
'${timestamp.toIso8601String()} [${_levelLabel(level)}] '
|
||||
'[${_scopeLabel(scope)}] $message';
|
||||
|
||||
static String _levelLabel(AppLogLevel level) => switch (level) {
|
||||
AppLogLevel.debug => '调试',
|
||||
AppLogLevel.info => '信息',
|
||||
AppLogLevel.warning => '警告',
|
||||
AppLogLevel.error => '错误',
|
||||
};
|
||||
|
||||
static String _scopeLabel(String scope) => switch (scope) {
|
||||
'HTTP' => '网络',
|
||||
'Auth' => '认证',
|
||||
'Media' => '媒体库',
|
||||
'CloudIndex' => '全盘索引',
|
||||
'App' => '应用',
|
||||
'Flutter' => '界面',
|
||||
'Dart' => '运行时',
|
||||
_ => scope,
|
||||
};
|
||||
}
|
||||
|
||||
/// One log sink for debug console, release file diagnostics and the in-app log
|
||||
|
||||
+2
-2
@@ -35,8 +35,8 @@ void main() async {
|
||||
if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
|
||||
await windowManager.ensureInitialized();
|
||||
const options = WindowOptions(
|
||||
size: Size(1280, 820),
|
||||
minimumSize: Size(980, 640),
|
||||
size: Size(1600, 900),
|
||||
minimumSize: Size(1200, 800),
|
||||
center: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/legacy.dart';
|
||||
import '../core/logging/app_logger.dart';
|
||||
import '../core/http/http_error.dart';
|
||||
import '../core/storage/storage_manager.dart';
|
||||
import '../api/guangya_api.dart';
|
||||
|
||||
@@ -75,12 +78,19 @@ class AuthState {
|
||||
|
||||
String get memberLevel =>
|
||||
_findStringDeep(userInfo, ['vipName', 'memberName', 'memberLevelName']) ??
|
||||
'普通会员';
|
||||
_memberLevelFromAssets;
|
||||
|
||||
String get _memberLevelFromAssets {
|
||||
final svipStatus = _findInt64Deep(userInfo, ['svipStatus']) ?? 0;
|
||||
if (svipStatus > 0) return '超级会员';
|
||||
final vipStatus = _findInt64Deep(userInfo, ['vipStatus']) ?? 0;
|
||||
return vipStatus > 0 ? '会员' : '普通会员';
|
||||
}
|
||||
|
||||
int? get capacity =>
|
||||
_findInt64Deep(userInfo, ['capacity', 'totalCapacity']);
|
||||
_findInt64Deep(userInfo, ['totalSpaceSize', 'capacity', 'totalCapacity']);
|
||||
int? get usedCapacity =>
|
||||
_findInt64Deep(userInfo, ['usedCapacity', 'usedSpace']);
|
||||
_findInt64Deep(userInfo, ['usedSpaceSize', 'usedCapacity', 'usedSpace']);
|
||||
|
||||
String get capacityText {
|
||||
if (capacity == null) return '空间信息暂不可用';
|
||||
@@ -90,7 +100,9 @@ class AuthState {
|
||||
// ── Static helpers ─────────────────────────────────────────────
|
||||
|
||||
static String? _findStringDeep(
|
||||
Map<String, dynamic>? json, List<String> keys) {
|
||||
Map<String, dynamic>? json,
|
||||
List<String> keys,
|
||||
) {
|
||||
if (json == null) return null;
|
||||
for (final key in keys) {
|
||||
final v = json[key];
|
||||
@@ -98,16 +110,17 @@ class AuthState {
|
||||
}
|
||||
for (final entry in json.entries) {
|
||||
if (entry.value is Map<String, dynamic>) {
|
||||
final found =
|
||||
_findStringDeep(entry.value as Map<String, dynamic>, keys);
|
||||
final found = _findStringDeep(
|
||||
entry.value as Map<String, dynamic>,
|
||||
keys,
|
||||
);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int? _findInt64Deep(
|
||||
Map<String, dynamic>? json, List<String> keys) {
|
||||
static int? _findInt64Deep(Map<String, dynamic>? json, List<String> keys) {
|
||||
if (json == null) return null;
|
||||
for (final key in keys) {
|
||||
final v = json[key];
|
||||
@@ -122,7 +135,13 @@ class AuthState {
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
if (bytes < 1024 * 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
if (bytes < 1024 * 1024 * 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} TB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} PB';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,11 +168,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
if (access.isNotEmpty || refresh != null) {
|
||||
try {
|
||||
AppLogger.info('Auth', '正在恢复登录状态');
|
||||
await _api.refreshAccessToken();
|
||||
await _saveTokens();
|
||||
state = state.copyWith(isSignedIn: true);
|
||||
await loadAccount();
|
||||
AppLogger.info('Auth', '登录状态恢复完成');
|
||||
} catch (_) {
|
||||
AppLogger.warning('Auth', '登录状态恢复失败,已清除本地令牌');
|
||||
state = state.copyWith(isSignedIn: false);
|
||||
_api.clearTokens();
|
||||
await _clearTokens();
|
||||
@@ -163,10 +185,33 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}
|
||||
|
||||
Future<void> loadAccount() async {
|
||||
AppLogger.info('Auth', '正在获取账户资料与云盘空间信息');
|
||||
final userInfoFuture = _api.userInfo();
|
||||
final assetsFuture = _api.cloudAssets();
|
||||
try {
|
||||
final userInfo = await _api.userInfo();
|
||||
state = state.copyWith(userInfo: userInfo);
|
||||
final userInfo = await userInfoFuture;
|
||||
Map<String, dynamic> assets = const {};
|
||||
try {
|
||||
assets = await assetsFuture;
|
||||
} catch (_) {
|
||||
AppLogger.warning('Auth', '云盘空间信息暂时不可用');
|
||||
}
|
||||
state = state.copyWith(userInfo: {...userInfo, 'assets': assets});
|
||||
AppLogger.info('Auth', '账户资料与云盘空间信息获取完成');
|
||||
} on DioException catch (error) {
|
||||
await assetsFuture.catchError((_) => <String, dynamic>{});
|
||||
final status = error.response?.statusCode;
|
||||
if (status == 404 || status == 501) {
|
||||
AppLogger.warning('Auth', '账户信息接口暂不可用 ($status),已保留登录态');
|
||||
return;
|
||||
}
|
||||
final message = error.response?.data is Map
|
||||
? extractHttpMessage(error.response?.data) ?? error.type.name
|
||||
: error.type.name;
|
||||
AppLogger.error('Auth', '加载账户信息失败', error: message);
|
||||
state = state.copyWith(errorMessage: message);
|
||||
} catch (e) {
|
||||
AppLogger.error('Auth', '加载账户信息失败', error: e);
|
||||
state = state.copyWith(errorMessage: e.toString());
|
||||
}
|
||||
}
|
||||
@@ -183,8 +228,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
Future<void> sendVerificationCode() async {
|
||||
state = state.copyWith(clearError: true);
|
||||
final cleanPhone =
|
||||
state.phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
final cleanPhone = state.phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (cleanPhone.length < 8) {
|
||||
state = state.copyWith(errorMessage: '请输入有效的手机号');
|
||||
return;
|
||||
@@ -192,8 +236,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
try {
|
||||
final initResult = await _api.loginSMSInit(cleanPhone);
|
||||
final captcha =
|
||||
AuthState._findStringDeep(initResult, ['captcha_token', 'captchaToken']);
|
||||
final captcha = AuthState._findStringDeep(initResult, [
|
||||
'captcha_token',
|
||||
'captchaToken',
|
||||
]);
|
||||
if (captcha == null) {
|
||||
if (AuthState._findStringDeep(initResult, ['url', 'verify_url']) !=
|
||||
null) {
|
||||
@@ -227,11 +273,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
state = state.copyWith(clearError: true);
|
||||
try {
|
||||
final verifyResult = await _api.loginSMSVerify(
|
||||
state.verificationID, state.verificationCode);
|
||||
final vToken = AuthState._findStringDeep(
|
||||
verifyResult, ['verification_token', 'verificationToken']);
|
||||
final code =
|
||||
AuthState._findStringDeep(verifyResult, ['code']);
|
||||
state.verificationID,
|
||||
state.verificationCode,
|
||||
);
|
||||
final vToken = AuthState._findStringDeep(verifyResult, [
|
||||
'verification_token',
|
||||
'verificationToken',
|
||||
]);
|
||||
final code = AuthState._findStringDeep(verifyResult, ['code']);
|
||||
if (vToken == null || code == null) throw Exception('验证码验证失败');
|
||||
|
||||
await _api.loginSMSSignIn(
|
||||
@@ -254,11 +303,24 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
state = state.copyWith(clearError: true);
|
||||
try {
|
||||
final result = await _api.loginQRInit();
|
||||
final token = AuthState._findStringDeep(
|
||||
result, ['device_code', 'deviceCode']) ??
|
||||
final token =
|
||||
AuthState._findStringDeep(result, ['device_code', 'deviceCode']) ??
|
||||
'';
|
||||
final payload = AuthState._findStringDeep(
|
||||
result, ['verification_uri_complete', 'verificationUriComplete', 'qr_url', 'qrUrl', 'url', 'verification_uri', 'verificationUri', 'qrcode', 'qrCode', 'code', 'qr_payload', 'qrPayload']) ??
|
||||
final payload =
|
||||
AuthState._findStringDeep(result, [
|
||||
'verification_uri_complete',
|
||||
'verificationUriComplete',
|
||||
'qr_url',
|
||||
'qrUrl',
|
||||
'url',
|
||||
'verification_uri',
|
||||
'verificationUri',
|
||||
'qrcode',
|
||||
'qrCode',
|
||||
'code',
|
||||
'qr_payload',
|
||||
'qrPayload',
|
||||
]) ??
|
||||
token;
|
||||
state = state.copyWith(
|
||||
qrPayload: payload,
|
||||
@@ -273,24 +335,24 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
|
||||
void _startQRPolling() {
|
||||
_qrPollingTimer?.cancel();
|
||||
_qrPollingTimer =
|
||||
Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
_qrPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
if (state.qrToken.isEmpty) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final result = await _api.loginQRPoll(state.qrToken);
|
||||
final accessToken = AuthState._findStringDeep(
|
||||
result, ['access_token', 'accessToken']);
|
||||
final accessToken = AuthState._findStringDeep(result, [
|
||||
'access_token',
|
||||
'accessToken',
|
||||
]);
|
||||
if (accessToken != null) {
|
||||
timer.cancel();
|
||||
state = state.copyWith(isSignedIn: true);
|
||||
await _saveTokens();
|
||||
await loadAccount();
|
||||
} else {
|
||||
final error =
|
||||
AuthState._findStringDeep(result, ['error']);
|
||||
final error = AuthState._findStringDeep(result, ['error']);
|
||||
String? newStatus;
|
||||
if (error == 'slow_down') {
|
||||
newStatus = '轮询过快,请稍候';
|
||||
@@ -322,7 +384,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
Future<void> _saveTokens() async {
|
||||
await StorageManager.set(StorageKeys.accessToken, _api.accessToken);
|
||||
if (_api.refreshTokenValue != null) {
|
||||
await StorageManager.set(StorageKeys.refreshToken, _api.refreshTokenValue!);
|
||||
await StorageManager.set(
|
||||
StorageKeys.refreshToken,
|
||||
_api.refreshTokenValue!,
|
||||
);
|
||||
}
|
||||
if (_api.tokenExpiresAt != null) {
|
||||
await StorageManager.set(
|
||||
|
||||
@@ -546,7 +546,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
|
||||
void cancelDetailSync() {
|
||||
_cancelDetailSync = true;
|
||||
_appendScanLog('[同步识别][DEBUG] 已请求取消同步识别');
|
||||
_appendScanLog('[同步识别][调试] 已请求取消同步识别');
|
||||
}
|
||||
|
||||
Future<void> refreshGlobalCloudIndex({bool force = false}) async {
|
||||
@@ -574,6 +574,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
}
|
||||
_refreshingCloudIndex = true;
|
||||
try {
|
||||
AppLogger.info('CloudIndex', '开始刷新全盘文件索引');
|
||||
final files = await _allGlobalRemoteFiles();
|
||||
final folders = <String?, List<CloudFile>>{};
|
||||
for (final file in files) {
|
||||
@@ -584,6 +585,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
StorageKeys.cloudIndexLastUpdatedAt,
|
||||
DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
);
|
||||
AppLogger.info('CloudIndex', '全盘文件索引刷新完成,共 ${files.length} 项');
|
||||
_appendScanLog('[云盘索引] 已刷新 ${files.length} 个文件与目录缓存');
|
||||
} catch (error) {
|
||||
_appendScanLog('[云盘索引] 刷新失败:$error', isError: true);
|
||||
@@ -736,7 +738,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
if (_cancelDetailSync) break;
|
||||
try {
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 开始:${original.file.name} '
|
||||
'[同步识别][调试] 开始:${original.file.name} '
|
||||
'(fileId=${original.id}, gcid=${original.file.gcid ?? '-'})',
|
||||
);
|
||||
final latestFile = await _resolveCurrentCloudFile(
|
||||
@@ -747,7 +749,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
throw StateError('云盘中未找到该文件');
|
||||
}
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 云盘已同步:fileId ${original.id} -> '
|
||||
'[同步识别][调试] 云盘已同步:文件 ID ${original.id} -> '
|
||||
'${latestFile.id},名称=${latestFile.name},gcid=${latestFile.gcid ?? '-'}',
|
||||
);
|
||||
synced.add(
|
||||
@@ -763,7 +765,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
} catch (error) {
|
||||
failures.add('${original.file.name}: $error');
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 同步失败:${original.file.name},$error',
|
||||
'[同步识别][调试] 同步失败:${original.file.name},$error',
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
@@ -796,7 +798,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
directoryName: _parentDirectoryName(fallback.file.cloudPath),
|
||||
);
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 解析:${fallback.file.name} -> '
|
||||
'[同步识别][调试] 解析:${fallback.file.name} -> '
|
||||
'标题=${parsed.title},年份=${parsed.year ?? '-'},'
|
||||
'类型=${fallback.mediaKind?.name ?? 'automatic'}',
|
||||
);
|
||||
@@ -825,19 +827,19 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
MediaTMDBMatchRequest(items: [fallback], candidates: candidates),
|
||||
);
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 存在 ${candidates.length} 个 TMDB 候选,等待用户选择',
|
||||
'[同步识别][调试] 存在 ${candidates.length} 个 TMDB 候选,等待用户选择',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (updated.tmdbID != null) {
|
||||
recognizedCount++;
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] TMDB 命中:${updated.title} '
|
||||
'[同步识别][调试] TMDB 命中:${updated.title} '
|
||||
'(tmdbId=${updated.tmdbID}, ${updated.year.isEmpty ? '年份未知' : updated.year})',
|
||||
);
|
||||
} else {
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] TMDB 未命中:${parsed.title} '
|
||||
'[同步识别][调试] TMDB 未命中:${parsed.title} '
|
||||
'(年份=${parsed.year ?? '-'})',
|
||||
);
|
||||
}
|
||||
@@ -854,17 +856,17 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
if (updated.file.name != beforeName) {
|
||||
renamedCount++;
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 已规范命名:$beforeName -> ${updated.file.name}',
|
||||
'[同步识别][调试] 已规范命名:$beforeName -> ${updated.file.name}',
|
||||
);
|
||||
} else if (updated.tmdbID != null) {
|
||||
_appendScanLog('[同步识别][DEBUG] 跳过规范命名:未解析到分辨率或名称已符合规则');
|
||||
_appendScanLog('[同步识别][调试] 跳过规范命名:未解析到分辨率或名称已符合规则');
|
||||
}
|
||||
updates.add(updated);
|
||||
replacements['${original.libraryID}:${original.id}'] = updated;
|
||||
} catch (error) {
|
||||
failures.add('${original.file.name}: $error');
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 识别失败:${original.file.name},$error',
|
||||
'[同步识别][调试] 识别失败:${original.file.name},$error',
|
||||
isError: true,
|
||||
);
|
||||
}
|
||||
@@ -1584,6 +1586,8 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
const pageSize = 10000;
|
||||
final values = <CloudFile>[];
|
||||
for (var page = 0; !_cancelScan; page++) {
|
||||
final typeLabel = resType == 2 ? '目录' : '文件';
|
||||
AppLogger.info('CloudIndex', '正在获取全盘$typeLabel索引,第 ${page + 1} 页');
|
||||
final response = await _api!.fsFiles(
|
||||
parentID: '*',
|
||||
page: page,
|
||||
@@ -1594,6 +1598,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
);
|
||||
final batch = _extractFiles(response);
|
||||
values.addAll(batch);
|
||||
AppLogger.info(
|
||||
'CloudIndex',
|
||||
'全盘$typeLabel索引第 ${page + 1} 页完成,获取 ${batch.length} 项,累计 ${values.length} 项',
|
||||
);
|
||||
final total = _findIntDeep(response, const [
|
||||
'total',
|
||||
'totalCount',
|
||||
@@ -1667,6 +1675,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
if (cachedFolder != null) {
|
||||
files = cachedFolder;
|
||||
} else {
|
||||
AppLogger.info('Media', '正在读取目录「${folder.path}」第 ${page + 1} 页');
|
||||
final response = await _api!.fsFiles(
|
||||
parentID: folder.id,
|
||||
page: page,
|
||||
@@ -1675,6 +1684,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
sortType: 0,
|
||||
);
|
||||
files = _extractFiles(response);
|
||||
AppLogger.info(
|
||||
'Media',
|
||||
'目录「${folder.path}」第 ${page + 1} 页读取完成,获取 ${files.length} 项',
|
||||
);
|
||||
files = await _enrichAndCacheFiles(files);
|
||||
folderSnapshot.addAll(files);
|
||||
}
|
||||
@@ -1867,9 +1880,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
}
|
||||
} catch (error) {
|
||||
// A rename can replace the cloud record. Locate its new ID below.
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 旧 File ID 查询失败:${knownFile.id},$error;开始回退定位',
|
||||
);
|
||||
_appendScanLog('[同步识别][调试] 旧文件 ID 查询失败:${knownFile.id},$error;开始回退定位');
|
||||
}
|
||||
|
||||
final cached = await FileMetadataCache.file(knownFile.id);
|
||||
@@ -1881,7 +1892,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
cached.gcid == gcid &&
|
||||
cached.id != knownFile.id) {
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}',
|
||||
'[同步识别][调试] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}',
|
||||
);
|
||||
return _cacheResolvedCloudFile(
|
||||
knownFile,
|
||||
@@ -1953,7 +1964,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
if (located != null) return located;
|
||||
}
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库',
|
||||
'[同步识别][调试] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库',
|
||||
isError: true,
|
||||
);
|
||||
return null;
|
||||
@@ -1965,7 +1976,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
}) async {
|
||||
final gcid = knownFile.gcid?.trim();
|
||||
if (gcid == null || gcid.isEmpty) return null;
|
||||
_appendScanLog('[同步识别][DEBUG] 从当前文件目录扫描:parentId=$parentID,gcid=$gcid');
|
||||
_appendScanLog('[同步识别][调试] 从当前文件目录扫描:父目录 ID=$parentID,GCID=$gcid');
|
||||
final parentPath = _parentPath(knownFile.cloudPath);
|
||||
List<CloudFile> children;
|
||||
try {
|
||||
@@ -1998,7 +2009,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
if (resolved.gcid == gcid) {
|
||||
final exact = _withPath(resolved, '$parentPath/${resolved.name}');
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 当前目录定位成功:${knownFile.id} -> '
|
||||
'[同步识别][调试] 当前目录定位成功:${knownFile.id} -> '
|
||||
'${exact.id},路径=${exact.cloudPath}',
|
||||
);
|
||||
return _cacheResolvedCloudFile(
|
||||
@@ -2009,7 +2020,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
||||
}
|
||||
}
|
||||
_appendScanLog(
|
||||
'[同步识别][DEBUG] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次',
|
||||
'[同步识别][调试] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user