Fix school browser initialization and redirect detection; improve diagnostics

This commit is contained in:
Codex
2026-10-15 12:23:35 +08:00
parent 46e26ec548
commit a4c2df838e
20 changed files with 530 additions and 17 deletions
@@ -121,7 +121,10 @@ class _SchoolAuthPageState extends State<SchoolAuthPage> {
padding: const EdgeInsets.all(12),
color: const Color(0xFFEFF6FF),
child: Text(
s.message,
[
s.message,
if (s.browserVersion != null) 'Chromium ${s.browserVersion}',
].join('\n'),
style: const TextStyle(fontSize: 12, height: 1.5),
),
),
@@ -0,0 +1,75 @@
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import '../../services/diagnostic_log.dart';
class SchoolBrowserInfo {
const SchoolBrowserInfo({this.chromiumVersion});
final String? chromiumVersion;
}
typedef SchoolBrowserConfigurator = Future<SchoolBrowserInfo> Function(
WebViewController controller,
String school,
);
/// Configure only the dedicated school browser before its first request.
Future<SchoolBrowserInfo> configureSchoolBrowser(
WebViewController controller,
String school,
) async {
final android = controller.platform;
if (android is! AndroidWebViewController) return const SchoolBrowserInfo();
try {
await android.setUseWideViewPort(true);
final cookies = WebViewCookieManager().platform;
if (cookies is AndroidWebViewCookieManager) {
await cookies.setAcceptThirdPartyCookies(android, true);
}
await controller.setOnConsoleMessage((message) {
final text = message.message.toLowerCase();
final code = text.contains('mixed content')
? 'mixed_content'
: text.contains('content security policy')
? 'content_security_policy'
: text.contains('cors')
? 'cross_origin'
: text.contains('refused to')
? 'resource_refused'
: message.level == JavaScriptLogLevel.error
? 'script_error'
: null;
if (code != null) {
DiagnosticLog.instance.add(
'school_console_error',
fields: {'school': school, 'code': code},
);
}
});
String? version;
try {
version = RegExp(r'\bChrome/(\d+(?:\.\d+){0,3})\b')
.firstMatch(await android.getUserAgent() ?? '')
?.group(1);
} catch (_) {
/* Version inspection must not block authentication. */
}
DiagnosticLog.instance.add(
'browser_configured',
fields: {
'school': school,
'phase': 'wide_viewport',
'result': true,
'code': 'third_party_cookies',
'version': version,
},
);
return SchoolBrowserInfo(chromiumVersion: version);
} catch (error) {
DiagnosticLog.instance.add(
'browser_setup_failed',
fields: {'school': school, 'code': error.runtimeType.toString()},
);
rethrow;
}
}
+46 -5
View File
@@ -12,6 +12,7 @@ import 'school_page_adapter.dart';
import 'school_exam_adapter.dart';
import 'school_login_flow.dart';
import 'school_page_probe.dart';
import 'school_browser.dart';
enum SchoolConnectionState {
signedOut,
@@ -22,7 +23,28 @@ enum SchoolConnectionState {
}
class SchoolSession extends ChangeNotifier {
SchoolSession();
SchoolSession({this.configureBrowser = configureSchoolBrowser});
final SchoolBrowserConfigurator configureBrowser;
Future<SchoolBrowserInfo>? _browserReady;
String? browserVersion;
Future<void> _loadRequest(Uri uri) async {
final controller = web, generation = _pageGeneration, serial = _openSerial;
try {
final info = await (_browserReady ??= configureBrowser(
controller,
'graduate',
));
if (!hasOpened || generation != _pageGeneration || serial != _openSerial) {
return;
}
browserVersion = info.chromiumVersion;
await controller.loadRequest(uri);
} catch (_) {
_browserReady = null;
rethrow;
}
}
// Creating Android WebView starts its browser engine. Do it on school access,
// not while the student is opening an offline timetable.
late final WebViewController web = _createWeb();
@@ -38,7 +60,7 @@ class SchoolSession extends ChangeNotifier {
if (upgrade != null) {
final generation = _pageGeneration;
try {
await web.loadRequest(upgrade);
await _loadRequest(upgrade);
} catch (_) {
if (generation == _pageGeneration) {
_failure(
@@ -54,6 +76,12 @@ class SchoolSession extends ChangeNotifier {
uri,
isMainFrame: request.isMainFrame,
)) {
DiagnosticLog.instance.resourceFailure(
'graduate',
request.url,
code: 'blocked_navigation',
mainFrame: request.isMainFrame,
);
if (kDebugMode && request.isMainFrame) {
debugPrint(
'Blocked school navigation origin: ${uri?.scheme}://${uri?.host}',
@@ -80,6 +108,13 @@ class SchoolSession extends ChangeNotifier {
},
onPageFinished: _loaded,
onWebResourceError: (error) async {
DiagnosticLog.instance.resourceFailure(
'graduate',
error.url,
code: 'web_resource_error',
status: error.errorCode,
mainFrame: error.isForMainFrame,
);
if (error.isForMainFrame != true || error.url == null) return;
final generation = _pageGeneration;
final current = await web.currentUrl();
@@ -94,6 +129,12 @@ class SchoolSession extends ChangeNotifier {
}
},
onHttpError: (error) async {
DiagnosticLog.instance.resourceFailure(
'graduate',
error.request?.uri.toString(),
code: 'http_error',
status: error.response?.statusCode,
);
final generation = _pageGeneration;
final current = Uri.tryParse(await web.currentUrl() ?? '');
if (hasOpened &&
@@ -231,7 +272,7 @@ class SchoolSession extends ChangeNotifier {
notifyListeners();
if (needsTermSelection) return;
try {
await web.loadRequest(connected ? _target : authUrl);
await _loadRequest(connected ? _target : authUrl);
} catch (_) {
if (serial == _openSerial && generation == _pageGeneration) {
_failure('学校入口无法打开,请检查校园网络后重试。', code: 'entry_load_failed');
@@ -264,7 +305,7 @@ class SchoolSession extends ChangeNotifier {
_armTimeout();
notifyListeners();
try {
await web.loadRequest(_target);
await _loadRequest(_target);
} catch (_) {
if (serial == _openSerial && generation == _pageGeneration) {
_failure('学校学期查询页面暂时无法打开,请重试。', code: 'query_load_failed');
@@ -499,7 +540,7 @@ class SchoolSession extends ChangeNotifier {
_armTimeout();
notifyListeners();
try {
await web.loadRequest(next);
await _loadRequest(next);
} catch (_) {
if (generation == _pageGeneration) {
_failure('课表查询页面暂时无法打开,请重试。', code: 'query_load_failed');
@@ -168,7 +168,10 @@ class _UndergraduateAuthPageState extends State<UndergraduateAuthPage> {
padding: const EdgeInsets.all(12),
color: const Color(0xFFEFF6FF),
child: Text(
s.message,
[
s.message,
if (s.browserVersion != null) 'Chromium ${s.browserVersion}',
].join('\n'),
style: const TextStyle(fontSize: 12, height: 1.5),
),
),
@@ -11,8 +11,13 @@ import 'navigation_policy.dart';
import 'page_probe.dart';
import 'timetable_adapter.dart';
import 'exam_adapter.dart';
import '../school_browser.dart';
class UndergraduateSession extends ChangeNotifier {
UndergraduateSession({this.configureBrowser = configureSchoolBrowser});
final SchoolBrowserConfigurator configureBrowser;
Future<SchoolBrowserInfo>? _browserReady;
String? browserVersion;
static final loginUri = Uri.parse('https://eams.uestc.edu.cn/eams/');
static const manualMessage = '请在学校网页打开课表并选择学期,然后点击导入当前课表。';
WebViewController? _web;
@@ -99,6 +104,12 @@ class UndergraduateSession extends ChangeNotifier {
uri,
isMainFrame: request.isMainFrame,
)) {
DiagnosticLog.instance.resourceFailure(
'undergraduate',
request.url,
code: 'blocked_navigation',
mainFrame: request.isMainFrame,
);
if (request.isMainFrame) {
_failure('学校跳转未完成,请重新认证。', code: 'blocked_navigation');
}
@@ -129,6 +140,13 @@ class UndergraduateSession extends ChangeNotifier {
},
onPageFinished: _loaded,
onWebResourceError: (error) async {
DiagnosticLog.instance.resourceFailure(
'undergraduate',
error.url,
code: 'web_resource_error',
status: error.errorCode,
mainFrame: error.isForMainFrame,
);
if (error.isForMainFrame != true || error.url == null) return;
final g = _generation,
d = _document,
@@ -142,6 +160,12 @@ class UndergraduateSession extends ChangeNotifier {
}
},
onHttpError: (error) async {
DiagnosticLog.instance.resourceFailure(
'undergraduate',
error.request?.uri.toString(),
code: 'http_error',
status: error.response?.statusCode,
);
final g = _generation,
d = _document,
current = await web.currentUrl();
@@ -202,8 +226,16 @@ class UndergraduateSession extends ChangeNotifier {
_loading = true;
_emit();
try {
await web.loadRequest(uri);
final controller = web;
final info = await (_browserReady ??= configureBrowser(
controller,
'undergraduate',
));
if (!_valid(g)) return;
browserVersion = info.chromiumVersion;
await controller.loadRequest(uri);
} catch (_) {
_browserReady = null;
if (_valid(g)) _failure('学校页面打开未完成,请重试。');
}
}
@@ -283,6 +315,11 @@ class UndergraduateSession extends ChangeNotifier {
!UndergraduateNavigationPolicy.canNavigate(uri, isMainFrame: true)) {
return;
}
if (!UndergraduateNavigationPolicy.isDataPage(uri)) {
_loading = false;
_waitForLogin();
return;
}
final status = await _evidence(uri, false);
DiagnosticLog.instance.add(
'school_evidence',
@@ -346,8 +383,17 @@ class UndergraduateSession extends ChangeNotifier {
await _requestExam(g);
}
} catch (error) {
final current = await web.currentUrl();
if (_valid(g) && d == _document) {
_failure('$error', code: error.runtimeType.toString());
if (current != url) {
DiagnosticLog.instance.add(
'school_probe_stale',
fields: {'school': 'undergraduate', 'code': 'redirect_in_progress'},
);
if (current != null) _schedulePoll(g, d, current);
} else {
_failure('$error', code: error.runtimeType.toString());
}
}
} finally {
if (_probeKey == key) _probeKey = null;