From d26d406d140cafd44095d61352383ece38a466b9 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Mon, 11 May 2026 12:08:12 +0800 Subject: [PATCH 01/35] Add dependency Pocketpy --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 013e5592f..ed9181d03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -190,6 +190,7 @@ dependencies: health: ^13.1.4 logging: ^1.3.0 connectivity_plus: ^6.1.5 + pocketpy: ^0.8.0+2 dev_dependencies: flutter_test: From 124f2e11349beefb498c8c561563c8c27eff9c34 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Mon, 11 May 2026 12:16:27 +0800 Subject: [PATCH 02/35] Create python_service.dart --- lib/core/services/python_service.dart | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 lib/core/services/python_service.dart diff --git a/lib/core/services/python_service.dart b/lib/core/services/python_service.dart new file mode 100644 index 000000000..a659a59bc --- /dev/null +++ b/lib/core/services/python_service.dart @@ -0,0 +1,2 @@ +export 'python_service_stub.dart' + if (dart.library.io) 'python_service_native.dart'; From e561d21881fd382659af79534068b9d74bda3568 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Mon, 11 May 2026 12:17:24 +0800 Subject: [PATCH 03/35] Create python_service_stub.dart --- lib/core/services/python_service_stub.dart | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 lib/core/services/python_service_stub.dart diff --git a/lib/core/services/python_service_stub.dart b/lib/core/services/python_service_stub.dart new file mode 100644 index 000000000..8bba3e489 --- /dev/null +++ b/lib/core/services/python_service_stub.dart @@ -0,0 +1,7 @@ +Future initPython() async { + // Web 下什么都不做 +} + +Future evalPythonCode(String code) async { + // 空实现 +} From 006614c01de86a75969bff5df0d30a7927afc821 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Mon, 11 May 2026 12:30:26 +0800 Subject: [PATCH 04/35] Create python_service_native.dart --- lib/core/services/python_service_native.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/core/services/python_service_native.dart diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart new file mode 100644 index 000000000..1d33a2ab8 --- /dev/null +++ b/lib/core/services/python_service_native.dart @@ -0,0 +1,17 @@ +import 'package:pocketpy/pocketpy.dart' as pkpy; + +pkpy.VM? _vm; + +Future initPython() async { + _vm = pkpy.VM(); + // 可选:执行一些初始化脚本 + _vm?.exec('print("pocketpy ready")'); +} + +Future evalPythonCode(String code) async { + if (_vm == null) return; + _vm!.exec(code); + final out = _vm!.read_output(); + if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); + if (out.stderr.isNotEmpty) debugPrint('[Python stderr] ${out.stderr}'); +} From 275ba0c1593a3537ad3d1a67579b26c7333acaf3 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Mon, 11 May 2026 12:35:16 +0800 Subject: [PATCH 05/35] Update main.dart --- lib/main.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index afc539028..f99b503ab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,6 +37,7 @@ import 'package:window_manager/window_manager.dart'; import 'package:protocol_handler/protocol_handler.dart'; import 'package:island/core/services/unifiedpush_service.dart'; import 'package:media_kit/media_kit.dart'; +import 'core/services/python_service.dart' as python; @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { @@ -75,6 +76,15 @@ void main(List args) async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); } + if (!kIsWeb) { + try { + await python.initPython(); + Logger.root.info("[pocketpy] Initialized successfully"); + } catch (e) { + Logger.root.severe("[pocketpy] Init failed", e); + } + } + if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) { Logger.root.info("[SplashScreen] Initializing desktop window manager..."); await protocolHandler.register('solian'); From 4b346dbda8cebb75e7e576c67cb7db97480ad03e Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Tue, 12 May 2026 12:22:27 +0800 Subject: [PATCH 06/35] Update main.dart --- lib/main.dart | 99 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f99b503ab..d1cd4eec7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,7 +37,8 @@ import 'package:window_manager/window_manager.dart'; import 'package:protocol_handler/protocol_handler.dart'; import 'package:island/core/services/unifiedpush_service.dart'; import 'package:media_kit/media_kit.dart'; -import 'core/services/python_service.dart' as python; + +import 'package:island/core/services/python_service.dart' as python; @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { @@ -76,15 +77,6 @@ void main(List args) async { FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); } - if (!kIsWeb) { - try { - await python.initPython(); - Logger.root.info("[pocketpy] Initialized successfully"); - } catch (e) { - Logger.root.severe("[pocketpy] Init failed", e); - } - } - if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) { Logger.root.info("[SplashScreen] Initializing desktop window manager..."); await protocolHandler.register('solian'); @@ -157,6 +149,21 @@ void main(List args) async { final prefs = await SharedPreferences.getInstance(); + final container = ProviderContainer(); + + if (!kIsWeb) { + try { + await python.initPython(container); + if (python.isPythonAvailable()) { + Logger.root.info("[pocketpy] Initialized with valid token"); + } else { + Logger.root.info("[pocketpy] Skipped (no valid token)"); + } + } catch (e) { + Logger.root.severe("[pocketpy] Init error", e); + } + } + if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) { await windowManager.ensureInitialized(); @@ -230,6 +237,7 @@ void main(List args) async { runApp( ProviderScope( + parent: container, retry: (retryCount, error) { if (retryCount > 3) return null; if (error is DioException) { @@ -359,6 +367,77 @@ class IslandApp extends HookConsumerWidget { final router = ref.watch(routerProvider); + return MaterialApp.router( + scaffoldMessengerKey: globalScaffoldMessengerKey, + color: Colors.transparent, + theme: theme.light, + darkTheme: theme.dark, + themeMode: getThemeMode(), + routerConfig: router.config( + navigatorObservers: () { + return [ + if (kIsWeb || + Platform.isAndroid || + Platform.isIOS || + Platform.isMacOS) + FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), + ]; + }, + ), + supportedLocales: context.supportedLocales, + scrollBehavior: AppScrollBehavior(), + localizationsDelegates: [ + ...context.localizationDelegates, + RelativeTimeLocalizations.delegate, + ], + locale: context.locale, + builder: (context, child) { + return Overlay( + key: globalOverlay, + initialEntries: [ + OverlayEntry( + builder: (_) { + return WindowScaffold( + child: AppWrapper(child: child ?? const SizedBox.shrink()), + ); + }, + ), + ], + ); + }, + ); + } +} message, + ) { + Logger.root.info( + '[Notification] foreground message received: ${message.messageId}', + ); + handleMessage(message); + }); + + return () { + onMessageOpenedAppSubscription.cancel(); + onMessageSubscription.cancel(); + }; + }, []); + + useEffect(() { + ref.listen(websocketStateProvider, (_, state) { + Logger.root.info('[WebSocket] $state'); + if (state == WebSocketState.connected()) { + ref.read(realtimePostsProvider).startListening(); + } + }); + ref.listen(userInfoProvider, (_, user) { + if (user.value != null) { + WidgetSyncService().sendCfgToAppGroup(); + } + }); + return null; + }, []); + + final router = ref.watch(routerProvider); + return MaterialApp.router( scaffoldMessengerKey: globalScaffoldMessengerKey, color: Colors.transparent, From 88f5590a27eefcb439764b338c13709a1f756cd4 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Tue, 12 May 2026 12:25:34 +0800 Subject: [PATCH 07/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 53 ++++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index 1d33a2ab8..d67e1606c 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,15 +1,58 @@ +import 'dart:developer'; import 'package:pocketpy/pocketpy.dart' as pkpy; +import 'package:riverpod/riverpod.dart'; +import 'package:island/core/network.dart'; // 绝对路径 pkpy.VM? _vm; +String? _cachedToken; +bool _isInitialized = false; -Future initPython() async { - _vm = pkpy.VM(); - // 可选:执行一些初始化脚本 - _vm?.exec('print("pocketpy ready")'); +/// 是否已成功初始化(有 token 且 VM 已创建) +bool isPythonAvailable() => _isInitialized; + +/// 获取缓存的 token(仅当初始化成功时才有值) +String? getCachedToken() => _cachedToken; + +/// 初始化 PocketPy,只有拿到有效 token 时才初始化 +Future initPython(ProviderContainer container) async { + if (_isInitialized) return; + + try { + // 获取有效 token(自动处理刷新) + final token = await getValidAuthToken(container); + if (token == null || token.isEmpty) { + log('[python_service] No valid token, skip init.'); + return; + } + + _cachedToken = token; + _vm = pkpy.VM(); + + // 将 token 注入 Python 全局变量 + _vm!.exec('ACCESS_TOKEN = "$token"'); + // 可选:同时注入其他信息,比如 serverUrl + // 从 container 中读取 serverUrlProvider 可能需要额外的 provider,暂略 + _vm!.exec('print("PocketPy ready, ACCESS_TOKEN injected")'); + + final out = _vm!.read_output(); + if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); + + _isInitialized = true; + log('[python_service] Initialized'); + } catch (e) { + log('[python_service] Init failed: $e'); + _isInitialized = false; + _vm = null; + _cachedToken = null; + } } +/// 执行 Python 代码(仅在初始化成功时执行) Future evalPythonCode(String code) async { - if (_vm == null) return; + if (!_isInitialized || _vm == null) { + log('[python_service] Python not available'); + return; + } _vm!.exec(code); final out = _vm!.read_output(); if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); From 2936d3b616e74545f8e374b488c00a286d847366 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Wed, 13 May 2026 00:50:26 +0800 Subject: [PATCH 08/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 88 +++++++++++++------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index d67e1606c..f383ccd2c 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,59 +1,89 @@ +import 'dart:io'; import 'dart:developer'; import 'package:pocketpy/pocketpy.dart' as pkpy; -import 'package:riverpod/riverpod.dart'; -import 'package:island/core/network.dart'; // 绝对路径 +import 'package:path_provider/path_provider.dart'; pkpy.VM? _vm; -String? _cachedToken; bool _isInitialized = false; -/// 是否已成功初始化(有 token 且 VM 已创建) +/// 是否已成功初始化并执行了所有脚本 bool isPythonAvailable() => _isInitialized; -/// 获取缓存的 token(仅当初始化成功时才有值) -String? getCachedToken() => _cachedToken; - -/// 初始化 PocketPy,只有拿到有效 token 时才初始化 -Future initPython(ProviderContainer container) async { +/// 初始化 Python,设置 sys.path,并执行 SolianApp 下所有 .py 文件 +Future initPython() async { if (_isInitialized) return; try { - // 获取有效 token(自动处理刷新) - final token = await getValidAuthToken(container); - if (token == null || token.isEmpty) { - log('[python_service] No valid token, skip init.'); + // 1. 获取文档目录下的 SolianApp + final appDocDir = await getApplicationDocumentsDirectory(); + final solianAppDir = Directory('${appDocDir.path}/SolianApp'); + + if (!await solianAppDir.exists()) { + log('[python_service] SolianApp folder not found at ${solianAppDir.path}'); return; } - _cachedToken = token; + // 2. 创建 VM _vm = pkpy.VM(); - // 将 token 注入 Python 全局变量 - _vm!.exec('ACCESS_TOKEN = "$token"'); - // 可选:同时注入其他信息,比如 serverUrl - // 从 container 中读取 serverUrlProvider 可能需要额外的 provider,暂略 - _vm!.exec('print("PocketPy ready, ACCESS_TOKEN injected")'); + // 3. 将 SolianApp 目录添加到 sys.path(全局共享,用于 import) + _vm!.exec(''' +import sys +sys.path.insert(0, r"${solianAppDir.path}") +'''); - final out = _vm!.read_output(); - if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); + // 4. 执行所有 Python 脚本 + await _executeAllScripts(solianAppDir); _isInitialized = true; - log('[python_service] Initialized'); + log('[python_service] Initialized and executed all scripts in ${solianAppDir.path}'); } catch (e) { log('[python_service] Init failed: $e'); _isInitialized = false; _vm = null; - _cachedToken = null; } } -/// 执行 Python 代码(仅在初始化成功时执行) -Future evalPythonCode(String code) async { - if (!_isInitialized || _vm == null) { - log('[python_service] Python not available'); - return; +/// 执行 SolianApp 文件夹下的所有 .py 文件(不递归子目录,按文件名排序) +Future _executeAllScripts(Directory dir) async { + final files = await dir + .list() + .where((entity) => entity is File && entity.path.endsWith('.py')) + .toList(); + + // 按文件名排序,保证执行顺序可预测 + files.sort((a, b) => a.path.compareTo(b.path)); + + for (final file in files) { + await _executeSingleScript(file); + } +} + +/// 执行单个 Python 脚本,使用独立的全局字典 +Future _executeSingleScript(File script) async { + if (_vm == null) return; + + try { + final content = await script.readAsString(); + final globals = {}; // 独立环境 + _vm!.exec(content, globals); + + final out = _vm!.read_output(); + if (out.stdout.isNotEmpty) { + debugPrint('[Python stdout][${script.path.split('/').last}] ${out.stdout}'); + } + if (out.stderr.isNotEmpty) { + debugPrint('[Python stderr][${script.path.split('/').last}] ${out.stderr}'); + } + } catch (e) { + log('[python_service] Failed to execute ${script.path}: $e'); } - _vm!.exec(code); +} + +/// 可选:执行额外的 Python 代码字符串,可以指定是否复用某个全局字典 +Future evalPythonCode(String code, [Map? globals]) async { + if (!_isInitialized || _vm == null) return; + _vm!.exec(code, globals); final out = _vm!.read_output(); if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); if (out.stderr.isNotEmpty) debugPrint('[Python stderr] ${out.stderr}'); From 883857ac35223644518feca190d5bf60730d7009 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Wed, 13 May 2026 00:53:09 +0800 Subject: [PATCH 09/35] Update main.dart --- lib/main.dart | 92 +++------------------------------------------------ 1 file changed, 4 insertions(+), 88 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d1cd4eec7..fe77940e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -96,9 +96,6 @@ void main(List args) async { FirebaseMessaging.onBackgroundMessage( _firebaseMessagingBackgroundHandler, ); - // Although previous if case checked this. Still check is web or not - // Otherwise the web platform will broke due to there is no Platform api on the web - // Skip crashlytics setup on debug mode to prevent unexpected report to firebase if ((kIsWeb || !Platform.isWindows) && !kDebugMode) { FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; @@ -151,13 +148,14 @@ void main(List args) async { final container = ProviderContainer(); + // Initialize pocketpy (non-web only) if (!kIsWeb) { try { - await python.initPython(container); + await python.initPython(); if (python.isPythonAvailable()) { - Logger.root.info("[pocketpy] Initialized with valid token"); + Logger.root.info("[pocketpy] Initialized and executed all scripts in SolianApp"); } else { - Logger.root.info("[pocketpy] Skipped (no valid token)"); + Logger.root.info("[pocketpy] SolianApp not found or init failed"); } } catch (e) { Logger.root.severe("[pocketpy] Init error", e); @@ -169,7 +167,6 @@ void main(List args) async { const defaultSize = Size(360, 640); - // Get saved window size from preferences final savedSizeString = prefs.getString(kAppWindowSize); Size initialSize = defaultSize; @@ -272,8 +269,6 @@ void main(List args) async { ); } -// Router will be provided through Riverpod - final globalOverlay = GlobalKey(); final globalScaffoldMessengerKey = GlobalKey(); @@ -282,14 +277,11 @@ class IslandApp extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Make sure it's active final _ = ref.read(logsProvider); - // Theme data and prefs final theme = ref.watch(themeProvider); final settings = ref.watch(appSettingsProvider); - // Convert string theme mode to ThemeMode enum ThemeMode getThemeMode() { final themeMode = settings.themeMode ?? 'system'; switch (themeMode) { @@ -307,11 +299,9 @@ class IslandApp extends HookConsumerWidget { if (notification.data['meta']?['action_uri'] != null) { var uri = notification.data['meta']['action_uri'] as String; if (uri.startsWith('/')) { - // In-app routes final router = ref.read(routerProvider); router.push(notification.data['meta']['action_uri']); } else { - // External links launchUrlString(uri); } } @@ -322,19 +312,16 @@ class IslandApp extends HookConsumerWidget { return null; } - // When the app is opened from a terminated state. FirebaseMessaging.instance.getInitialMessage().then((message) { if (message != null) { handleMessage(message); } }); - // When the app is in the background and opened. final onMessageOpenedAppSubscription = FirebaseMessaging .onMessageOpenedApp .listen(handleMessage); - // When the app is in the foreground. final onMessageSubscription = FirebaseMessaging.onMessage.listen(( message, ) { @@ -367,77 +354,6 @@ class IslandApp extends HookConsumerWidget { final router = ref.watch(routerProvider); - return MaterialApp.router( - scaffoldMessengerKey: globalScaffoldMessengerKey, - color: Colors.transparent, - theme: theme.light, - darkTheme: theme.dark, - themeMode: getThemeMode(), - routerConfig: router.config( - navigatorObservers: () { - return [ - if (kIsWeb || - Platform.isAndroid || - Platform.isIOS || - Platform.isMacOS) - FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), - ]; - }, - ), - supportedLocales: context.supportedLocales, - scrollBehavior: AppScrollBehavior(), - localizationsDelegates: [ - ...context.localizationDelegates, - RelativeTimeLocalizations.delegate, - ], - locale: context.locale, - builder: (context, child) { - return Overlay( - key: globalOverlay, - initialEntries: [ - OverlayEntry( - builder: (_) { - return WindowScaffold( - child: AppWrapper(child: child ?? const SizedBox.shrink()), - ); - }, - ), - ], - ); - }, - ); - } -} message, - ) { - Logger.root.info( - '[Notification] foreground message received: ${message.messageId}', - ); - handleMessage(message); - }); - - return () { - onMessageOpenedAppSubscription.cancel(); - onMessageSubscription.cancel(); - }; - }, []); - - useEffect(() { - ref.listen(websocketStateProvider, (_, state) { - Logger.root.info('[WebSocket] $state'); - if (state == WebSocketState.connected()) { - ref.read(realtimePostsProvider).startListening(); - } - }); - ref.listen(userInfoProvider, (_, user) { - if (user.value != null) { - WidgetSyncService().sendCfgToAppGroup(); - } - }); - return null; - }, []); - - final router = ref.watch(routerProvider); - return MaterialApp.router( scaffoldMessengerKey: globalScaffoldMessengerKey, color: Colors.transparent, From 6b59270d8fa9fb4aff688bf3379380825ae1deb9 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Wed, 13 May 2026 01:18:39 +0800 Subject: [PATCH 10/35] Update main.dart --- lib/main.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index fe77940e7..cd5e080cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -146,8 +146,6 @@ void main(List args) async { final prefs = await SharedPreferences.getInstance(); - final container = ProviderContainer(); - // Initialize pocketpy (non-web only) if (!kIsWeb) { try { @@ -234,7 +232,6 @@ void main(List args) async { runApp( ProviderScope( - parent: container, retry: (retryCount, error) { if (retryCount > 3) return null; if (error is DioException) { From f4e8f1754015544e5200f43d4641a3669425afec Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Wed, 13 May 2026 01:20:14 +0800 Subject: [PATCH 11/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index f383ccd2c..a0db7c137 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,5 +1,7 @@ +// lib/core/services/python_service_native.dart import 'dart:io'; import 'dart:developer'; +import 'package:flutter/foundation.dart' show debugPrint; import 'package:pocketpy/pocketpy.dart' as pkpy; import 'package:path_provider/path_provider.dart'; @@ -14,25 +16,26 @@ Future initPython() async { if (_isInitialized) return; try { - // 1. 获取文档目录下的 SolianApp final appDocDir = await getApplicationDocumentsDirectory(); final solianAppDir = Directory('${appDocDir.path}/SolianApp'); - if (!await solianAppDir.exists()) { - log('[python_service] SolianApp folder not found at ${solianAppDir.path}'); + // 打印当前系统加载插件的文件夹(即 SolianApp 路径) + if (await solianAppDir.exists()) { + log('[python_service] Python scripts folder: ${solianAppDir.path}'); + } else { + // 无则打印其父路径(即文档目录) + log('[python_service] SolianApp not found, parent folder: ${appDocDir.path}'); return; } - // 2. 创建 VM _vm = pkpy.VM(); - // 3. 将 SolianApp 目录添加到 sys.path(全局共享,用于 import) + // 将 SolianApp 目录添加到 sys.path _vm!.exec(''' import sys sys.path.insert(0, r"${solianAppDir.path}") '''); - // 4. 执行所有 Python 脚本 await _executeAllScripts(solianAppDir); _isInitialized = true; @@ -44,14 +47,14 @@ sys.path.insert(0, r"${solianAppDir.path}") } } -/// 执行 SolianApp 文件夹下的所有 .py 文件(不递归子目录,按文件名排序) Future _executeAllScripts(Directory dir) async { - final files = await dir - .list() - .where((entity) => entity is File && entity.path.endsWith('.py')) - .toList(); - - // 按文件名排序,保证执行顺序可预测 + final entities = await dir.list().toList(); + final files = []; + for (final entity in entities) { + if (entity is File && entity.path.endsWith('.py')) { + files.add(entity); + } + } files.sort((a, b) => a.path.compareTo(b.path)); for (final file in files) { @@ -59,15 +62,12 @@ Future _executeAllScripts(Directory dir) async { } } -/// 执行单个 Python 脚本,使用独立的全局字典 Future _executeSingleScript(File script) async { if (_vm == null) return; try { final content = await script.readAsString(); - final globals = {}; // 独立环境 - _vm!.exec(content, globals); - + _vm!.exec(content); final out = _vm!.read_output(); if (out.stdout.isNotEmpty) { debugPrint('[Python stdout][${script.path.split('/').last}] ${out.stdout}'); @@ -80,10 +80,10 @@ Future _executeSingleScript(File script) async { } } -/// 可选:执行额外的 Python 代码字符串,可以指定是否复用某个全局字典 -Future evalPythonCode(String code, [Map? globals]) async { +/// 可选:执行额外的 Python 代码字符串 +Future evalPythonCode(String code) async { if (!_isInitialized || _vm == null) return; - _vm!.exec(code, globals); + _vm!.exec(code); final out = _vm!.read_output(); if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); if (out.stderr.isNotEmpty) debugPrint('[Python stderr] ${out.stderr}'); From 5e88bb9c0221e06ce9670e772cf760019a66de82 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Wed, 13 May 2026 01:41:24 +0800 Subject: [PATCH 12/35] Update python_service_stub.dart --- lib/core/services/python_service_stub.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/core/services/python_service_stub.dart b/lib/core/services/python_service_stub.dart index 8bba3e489..a9da65ad0 100644 --- a/lib/core/services/python_service_stub.dart +++ b/lib/core/services/python_service_stub.dart @@ -1,7 +1,6 @@ -Future initPython() async { - // Web 下什么都不做 -} +// Web 平台的空实现 +Future initPython() async {} -Future evalPythonCode(String code) async { - // 空实现 -} +bool isPythonAvailable() => false; + +Future evalPythonCode(String code) async {} From 2289fc34cd28e52216579fa62fd5a3bd3432fdff Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Thu, 14 May 2026 01:09:44 +0800 Subject: [PATCH 13/35] Update main.dart --- lib/main.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index cd5e080cc..d46dd8bae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -151,12 +151,12 @@ void main(List args) async { try { await python.initPython(); if (python.isPythonAvailable()) { - Logger.root.info("[pocketpy] Initialized and executed all scripts in SolianApp"); + log('[pocketpy] Initialized and executed all scripts in SolianApp'); } else { - Logger.root.info("[pocketpy] SolianApp not found or init failed"); + log('[pocketpy] SolianApp not found or init failed'); } } catch (e) { - Logger.root.severe("[pocketpy] Init error", e); + log('[pocketpy] Init error: $e'); } } From b6acc2af8a60079beafbd137032d9b04083edbfc Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Thu, 14 May 2026 01:12:16 +0800 Subject: [PATCH 14/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 104 +++++++++++-------- 1 file changed, 60 insertions(+), 44 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index a0db7c137..c26ff1e52 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,17 +1,15 @@ // lib/core/services/python_service_native.dart import 'dart:io'; import 'dart:developer'; -import 'package:flutter/foundation.dart' show debugPrint; import 'package:pocketpy/pocketpy.dart' as pkpy; import 'package:path_provider/path_provider.dart'; -pkpy.VM? _vm; bool _isInitialized = false; +String? _solianAppPath; -/// 是否已成功初始化并执行了所有脚本 bool isPythonAvailable() => _isInitialized; -/// 初始化 Python,设置 sys.path,并执行 SolianApp 下所有 .py 文件 +/// 初始化 Python 并并行执行 SolianApp 下所有 .py 文件 Future initPython() async { if (_isInitialized) return; @@ -19,72 +17,90 @@ Future initPython() async { final appDocDir = await getApplicationDocumentsDirectory(); final solianAppDir = Directory('${appDocDir.path}/SolianApp'); - // 打印当前系统加载插件的文件夹(即 SolianApp 路径) if (await solianAppDir.exists()) { log('[python_service] Python scripts folder: ${solianAppDir.path}'); + _solianAppPath = solianAppDir.path; } else { - // 无则打印其父路径(即文档目录) log('[python_service] SolianApp not found, parent folder: ${appDocDir.path}'); return; } - _vm = pkpy.VM(); - - // 将 SolianApp 目录添加到 sys.path - _vm!.exec(''' -import sys -sys.path.insert(0, r"${solianAppDir.path}") -'''); + final files = []; + await for (final entity in solianAppDir.list()) { + if (entity is File && entity.path.endsWith('.py')) { + files.add(entity); + } + } + files.sort((a, b) => a.path.compareTo(b.path)); - await _executeAllScripts(solianAppDir); + // 并行执行所有脚本,每个脚本使用独立的 ComputeThread + final futures = >[]; + for (int i = 0; i < files.length; i++) { + // vm_index 范围 1-15,循环使用 + final vmIndex = (i % 15) + 1; + futures.add(_executeScriptInThread(files[i], vmIndex)); + } + await Future.wait(futures); _isInitialized = true; - log('[python_service] Initialized and executed all scripts in ${solianAppDir.path}'); + log('[python_service] All scripts executed successfully'); } catch (e) { log('[python_service] Init failed: $e'); _isInitialized = false; - _vm = null; - } -} - -Future _executeAllScripts(Directory dir) async { - final entities = await dir.list().toList(); - final files = []; - for (final entity in entities) { - if (entity is File && entity.path.endsWith('.py')) { - files.add(entity); - } - } - files.sort((a, b) => a.path.compareTo(b.path)); - - for (final file in files) { - await _executeSingleScript(file); + _solianAppPath = null; } } -Future _executeSingleScript(File script) async { - if (_vm == null) return; - +/// 在独立线程中执行单个脚本 +Future _executeScriptInThread(File script, int vmIndex) async { + final thread = pkpy.ComputeThread(vmIndex); try { + // 设置模块搜索路径 + thread.exec(''' +import sys +sys.path.insert(0, r"$_solianAppPath") +'''); final content = await script.readAsString(); - _vm!.exec(content); - final out = _vm!.read_output(); + thread.exec(content); + thread.wait_for_done(); + + final out = thread.read_output(); if (out.stdout.isNotEmpty) { - debugPrint('[Python stdout][${script.path.split('/').last}] ${out.stdout}'); + log('[Python stdout][${script.path.split('/').last}] ${out.stdout}'); } if (out.stderr.isNotEmpty) { - debugPrint('[Python stderr][${script.path.split('/').last}] ${out.stderr}'); + log('[Python stderr][${script.path.split('/').last}] ${out.stderr}'); } } catch (e) { log('[python_service] Failed to execute ${script.path}: $e'); + } finally { + thread.close(); } } -/// 可选:执行额外的 Python 代码字符串 +/// 执行单条 Python 代码(简单封装,仍使用主线程) Future evalPythonCode(String code) async { - if (!_isInitialized || _vm == null) return; - _vm!.exec(code); - final out = _vm!.read_output(); - if (out.stdout.isNotEmpty) debugPrint('[Python stdout] ${out.stdout}'); - if (out.stderr.isNotEmpty) debugPrint('[Python stderr] ${out.stderr}'); + if (!_isInitialized) { + log('[python_service] Python not available'); + return; + } + // 为了演示,创建一个临时线程执行(或复用主 VM,这里简单处理) + final thread = pkpy.ComputeThread(1); + try { + if (_solianAppPath != null) { + thread.exec(''' +import sys +sys.path.insert(0, r"$_solianAppPath") +'''); + } + thread.exec(code); + thread.wait_for_done(); + final out = thread.read_output(); + if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); + if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); + } catch (e) { + log('[python_service] evalPythonCode error: $e'); + } finally { + thread.close(); + } } From 7c89033e101b7a3549262f3f7cc014d24055fb91 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Thu, 14 May 2026 01:23:30 +0800 Subject: [PATCH 15/35] Update app_startup_splash.dart --- lib/shared/widgets/app_startup_splash.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/shared/widgets/app_startup_splash.dart b/lib/shared/widgets/app_startup_splash.dart index e9e72dcc5..b1b161b75 100644 --- a/lib/shared/widgets/app_startup_splash.dart +++ b/lib/shared/widgets/app_startup_splash.dart @@ -9,6 +9,7 @@ import 'package:island/core/audio.dart'; import 'package:island/core/network.dart'; import 'package:island/core/services/notify.dart'; import 'package:island/core/websocket.dart'; +import 'package:island/core/services/python_service.dart' as python; // 新增导入 const kDefaultBootstrapRetryTimeouts = [ Duration(milliseconds: 1000), @@ -124,6 +125,14 @@ class StartupSplashScreen extends HookConsumerWidget { await ref.read(messageSfxProvider.future); }, ), + // 新增:Python 脚本加载阶段 + _BootstrapStage( + label: 'Initializing Python scripts', + isCritical: false, // 非关键,失败不影响主流程 + action: () async { + await python.initPython(); + }, + ), ], [], ); From 3a29fcf4f15bc2c90edc4378ab5060e3d3972a02 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Thu, 14 May 2026 01:35:58 +0800 Subject: [PATCH 16/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index c26ff1e52..e46fb5700 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,7 +1,7 @@ // lib/core/services/python_service_native.dart import 'dart:io'; import 'dart:developer'; -import 'package:pocketpy/pocketpy.dart' as pkpy; +import 'package:pocketpy/pocketpy.dart'; // 直接导入,ComputeThread 是顶级类 import 'package:path_provider/path_provider.dart'; bool _isInitialized = false; @@ -36,7 +36,6 @@ Future initPython() async { // 并行执行所有脚本,每个脚本使用独立的 ComputeThread final futures = >[]; for (int i = 0; i < files.length; i++) { - // vm_index 范围 1-15,循环使用 final vmIndex = (i % 15) + 1; futures.add(_executeScriptInThread(files[i], vmIndex)); } @@ -53,13 +52,14 @@ Future initPython() async { /// 在独立线程中执行单个脚本 Future _executeScriptInThread(File script, int vmIndex) async { - final thread = pkpy.ComputeThread(vmIndex); + final thread = ComputeThread(vmIndex); // 注意:直接使用 ComputeThread try { - // 设置模块搜索路径 - thread.exec(''' + if (_solianAppPath != null) { + thread.exec(''' import sys sys.path.insert(0, r"$_solianAppPath") '''); + } final content = await script.readAsString(); thread.exec(content); thread.wait_for_done(); @@ -78,14 +78,13 @@ sys.path.insert(0, r"$_solianAppPath") } } -/// 执行单条 Python 代码(简单封装,仍使用主线程) +/// 执行单条 Python 代码(简单封装,仍使用独立线程) Future evalPythonCode(String code) async { if (!_isInitialized) { log('[python_service] Python not available'); return; } - // 为了演示,创建一个临时线程执行(或复用主 VM,这里简单处理) - final thread = pkpy.ComputeThread(1); + final thread = ComputeThread(1); try { if (_solianAppPath != null) { thread.exec(''' From 3e6cc8ea5f48c56b1f473e11fd063ff91326af71 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Thu, 14 May 2026 01:50:33 +0800 Subject: [PATCH 17/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 79 ++++++++------------ 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index e46fb5700..268f035c2 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,15 +1,16 @@ // lib/core/services/python_service_native.dart import 'dart:io'; import 'dart:developer'; -import 'package:pocketpy/pocketpy.dart'; // 直接导入,ComputeThread 是顶级类 +import 'package:pocketpy/pocketpy.dart' as pkpy; import 'package:path_provider/path_provider.dart'; +pkpy.VM? _vm; bool _isInitialized = false; String? _solianAppPath; bool isPythonAvailable() => _isInitialized; -/// 初始化 Python 并并行执行 SolianApp 下所有 .py 文件 +/// 初始化 Python 环境,设置 sys.path,然后顺序执行所有 .py 文件 Future initPython() async { if (_isInitialized) return; @@ -25,6 +26,16 @@ Future initPython() async { return; } + // 创建主 VM + _vm = pkpy.VM(); + + // 将 SolianApp 目录添加到 sys.path(全局有效) + _vm!.exec(''' +import sys +sys.path.insert(0, r"${_solianAppPath}") +'''); + + // 收集所有 .py 文件 final files = []; await for (final entity in solianAppDir.list()) { if (entity is File && entity.path.endsWith('.py')) { @@ -33,38 +44,31 @@ Future initPython() async { } files.sort((a, b) => a.path.compareTo(b.path)); - // 并行执行所有脚本,每个脚本使用独立的 ComputeThread - final futures = >[]; - for (int i = 0; i < files.length; i++) { - final vmIndex = (i % 15) + 1; - futures.add(_executeScriptInThread(files[i], vmIndex)); + // 顺序执行每个脚本,每个脚本使用独立的全局字典 + for (final file in files) { + await _executeScriptWithIsolatedGlobals(file); } - await Future.wait(futures); _isInitialized = true; log('[python_service] All scripts executed successfully'); } catch (e) { log('[python_service] Init failed: $e'); _isInitialized = false; + _vm = null; _solianAppPath = null; } } -/// 在独立线程中执行单个脚本 -Future _executeScriptInThread(File script, int vmIndex) async { - final thread = ComputeThread(vmIndex); // 注意:直接使用 ComputeThread +/// 使用独立的全局字典执行单个脚本(环境隔离,但模块导入共享) +Future _executeScriptWithIsolatedGlobals(File script) async { + if (_vm == null) return; + try { - if (_solianAppPath != null) { - thread.exec(''' -import sys -sys.path.insert(0, r"$_solianAppPath") -'''); - } final content = await script.readAsString(); - thread.exec(content); - thread.wait_for_done(); - - final out = thread.read_output(); + // 创建一个新的空字典作为该脚本的全局命名空间 + final isolatedGlobals = {}; + _vm!.eval(content, isolatedGlobals); // 执行后,脚本中定义的变量存入 isolatedGlobals + final out = _vm!.read_output(); if (out.stdout.isNotEmpty) { log('[Python stdout][${script.path.split('/').last}] ${out.stdout}'); } @@ -73,33 +77,14 @@ sys.path.insert(0, r"$_solianAppPath") } } catch (e) { log('[python_service] Failed to execute ${script.path}: $e'); - } finally { - thread.close(); } } -/// 执行单条 Python 代码(简单封装,仍使用独立线程) -Future evalPythonCode(String code) async { - if (!_isInitialized) { - log('[python_service] Python not available'); - return; - } - final thread = ComputeThread(1); - try { - if (_solianAppPath != null) { - thread.exec(''' -import sys -sys.path.insert(0, r"$_solianAppPath") -'''); - } - thread.exec(code); - thread.wait_for_done(); - final out = thread.read_output(); - if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); - if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); - } catch (e) { - log('[python_service] evalPythonCode error: $e'); - } finally { - thread.close(); - } +/// 执行单条 Python 代码(使用主 VM,可指定全局字典) +Future evalPythonCode(String code, [Map? globals]) async { + if (!_isInitialized || _vm == null) return; + _vm!.eval(code, globals); + final out = _vm!.read_output(); + if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); + if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); } From 2455984908551b3a37d57778429f163b134e3e4b Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Thu, 14 May 2026 06:32:49 +0800 Subject: [PATCH 18/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 42 +++++--------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index 268f035c2..d234ac703 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,4 +1,3 @@ -// lib/core/services/python_service_native.dart import 'dart:io'; import 'dart:developer'; import 'package:pocketpy/pocketpy.dart' as pkpy; @@ -10,7 +9,6 @@ String? _solianAppPath; bool isPythonAvailable() => _isInitialized; -/// 初始化 Python 环境,设置 sys.path,然后顺序执行所有 .py 文件 Future initPython() async { if (_isInitialized) return; @@ -26,16 +24,13 @@ Future initPython() async { return; } - // 创建主 VM _vm = pkpy.VM(); - // 将 SolianApp 目录添加到 sys.path(全局有效) _vm!.exec(''' import sys sys.path.insert(0, r"${_solianAppPath}") '''); - // 收集所有 .py 文件 final files = []; await for (final entity in solianAppDir.list()) { if (entity is File && entity.path.endsWith('.py')) { @@ -44,9 +39,16 @@ sys.path.insert(0, r"${_solianAppPath}") } files.sort((a, b) => a.path.compareTo(b.path)); - // 顺序执行每个脚本,每个脚本使用独立的全局字典 for (final file in files) { - await _executeScriptWithIsolatedGlobals(file); + final content = await file.readAsString(); + _vm!.exec(content); + final out = _vm!.read_output(); + if (out.stdout.isNotEmpty) { + log('[Python stdout][${file.path.split('/').last}] ${out.stdout}'); + } + if (out.stderr.isNotEmpty) { + log('[Python stderr][${file.path.split('/').last}] ${out.stderr}'); + } } _isInitialized = true; @@ -59,31 +61,9 @@ sys.path.insert(0, r"${_solianAppPath}") } } -/// 使用独立的全局字典执行单个脚本(环境隔离,但模块导入共享) -Future _executeScriptWithIsolatedGlobals(File script) async { - if (_vm == null) return; - - try { - final content = await script.readAsString(); - // 创建一个新的空字典作为该脚本的全局命名空间 - final isolatedGlobals = {}; - _vm!.eval(content, isolatedGlobals); // 执行后,脚本中定义的变量存入 isolatedGlobals - final out = _vm!.read_output(); - if (out.stdout.isNotEmpty) { - log('[Python stdout][${script.path.split('/').last}] ${out.stdout}'); - } - if (out.stderr.isNotEmpty) { - log('[Python stderr][${script.path.split('/').last}] ${out.stderr}'); - } - } catch (e) { - log('[python_service] Failed to execute ${script.path}: $e'); - } -} - -/// 执行单条 Python 代码(使用主 VM,可指定全局字典) -Future evalPythonCode(String code, [Map? globals]) async { +Future evalPythonCode(String code) async { if (!_isInitialized || _vm == null) return; - _vm!.eval(code, globals); + _vm!.exec(code); final out = _vm!.read_output(); if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); From 35c4507b87b74b73713d491a8fe0c4c5c5b6693c Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 01:46:12 +0800 Subject: [PATCH 19/35] Create loader.py --- assets/python/loader.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/python/loader.py diff --git a/assets/python/loader.py b/assets/python/loader.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/assets/python/loader.py @@ -0,0 +1 @@ + From bf71e7a5940d9738aa00f1ed09f7a59fe0ec2beb Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 01:46:58 +0800 Subject: [PATCH 20/35] Delete assets/python directory --- assets/python/loader.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 assets/python/loader.py diff --git a/assets/python/loader.py b/assets/python/loader.py deleted file mode 100644 index 8b1378917..000000000 --- a/assets/python/loader.py +++ /dev/null @@ -1 +0,0 @@ - From 73736adff6796bd3e52de7677c2c4f05251664f5 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 01:47:16 +0800 Subject: [PATCH 21/35] Create loader.py --- assets/scripts/loader.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/scripts/loader.py diff --git a/assets/scripts/loader.py b/assets/scripts/loader.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/assets/scripts/loader.py @@ -0,0 +1 @@ + From d2cfe390480e517fad6af3f89ccca3ea7c22b9c0 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 01:49:50 +0800 Subject: [PATCH 22/35] Update pubspec.yaml --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 424cbc263..3bc6b72af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -229,6 +229,7 @@ flutter: - assets/images/michan/ - assets/icons/ - assets/audio/ + - assets/scripts/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images From 9e78d50b509a619e609843c414a668135842e100 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 02:00:05 +0800 Subject: [PATCH 23/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 73 ++++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index d234ac703..fa7b4b1a5 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'dart:developer'; +import 'package:flutter/services.dart'; import 'package:pocketpy/pocketpy.dart' as pkpy; import 'package:path_provider/path_provider.dart'; pkpy.VM? _vm; bool _isInitialized = false; -String? _solianAppPath; bool isPythonAvailable() => _isInitialized; @@ -13,54 +13,67 @@ Future initPython() async { if (_isInitialized) return; try { - final appDocDir = await getApplicationDocumentsDirectory(); - final solianAppDir = Directory('${appDocDir.path}/SolianApp'); + final appSupportDir = await getApplicationSupportDirectory(); + final pluginsDir = Directory('${appSupportDir.path}/plugins'); + final eventFile = File('${appSupportDir.path}/event.py'); - if (await solianAppDir.exists()) { - log('[python_service] Python scripts folder: ${solianAppDir.path}'); - _solianAppPath = solianAppDir.path; - } else { - log('[python_service] SolianApp not found, parent folder: ${appDocDir.path}'); - return; + if (!await pluginsDir.exists()) { + await pluginsDir.create(recursive: true); + log('[python_service] Created plugins directory: ${pluginsDir.path}'); } + final eventScript = await rootBundle.loadString('assets/scripts/event.py'); + await eventFile.writeAsString(eventScript); + _vm = pkpy.VM(); _vm!.exec(''' import sys -sys.path.insert(0, r"${_solianAppPath}") +sys.path.insert(0, r"${appSupportDir.path}") '''); - final files = []; - await for (final entity in solianAppDir.list()) { - if (entity is File && entity.path.endsWith('.py')) { - files.add(entity); - } - } - files.sort((a, b) => a.path.compareTo(b.path)); + _vm!.exec('import event'); - for (final file in files) { - final content = await file.readAsString(); - _vm!.exec(content); - final out = _vm!.read_output(); - if (out.stdout.isNotEmpty) { - log('[Python stdout][${file.path.split('/').last}] ${out.stdout}'); - } - if (out.stderr.isNotEmpty) { - log('[Python stderr][${file.path.split('/').last}] ${out.stderr}'); - } - } + final loadPluginsCode = ''' +import os +import sys +plugins_dir = os.path.join(sys.path[0], 'plugins') +if os.path.exists(plugins_dir): + for filename in os.listdir(plugins_dir): + if filename.endswith('.py'): + module_name = filename[:-3] + try: + __import__(module_name) + print(f"Imported plugin: {module_name}") + except Exception as e: + print(f"Failed to import plugin {module_name}: {e}") +'''; + _vm!.exec(loadPluginsCode); + + final out = _vm!.read_output(); + if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); + if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); _isInitialized = true; - log('[python_service] All scripts executed successfully'); + log('[python_service] Initialized'); } catch (e) { log('[python_service] Init failed: $e'); _isInitialized = false; _vm = null; - _solianAppPath = null; } } +Future callEvent(String eventName, List args) async { + if (!_isInitialized || _vm == null) return; + final argsStr = args.map((a) { + if (a is String) return '"${a.replaceAll('"', '\\"')}"'; + if (a is bool) return a ? 'True' : 'False'; + if (a == null) return 'None'; + return a.toString(); + }).join(', '); + await evalPythonCode('event.call("$eventName", $argsStr)'); +} + Future evalPythonCode(String code) async { if (!_isInitialized || _vm == null) return; _vm!.exec(code); From 2de2d136548e4c45f9ad0dfa834a0e6186e9a47c Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 02:00:58 +0800 Subject: [PATCH 24/35] Update loader.py --- assets/scripts/loader.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/assets/scripts/loader.py b/assets/scripts/loader.py index 8b1378917..511e07c61 100644 --- a/assets/scripts/loader.py +++ b/assets/scripts/loader.py @@ -1 +1,20 @@ +import os +import sys +def load_plugins(): + plugins_dir = os.path.join(sys.path[0], 'plugins') + if not os.path.exists(plugins_dir): + print(f"Plugins directory not found: {plugins_dir}") + return + + for filename in os.listdir(plugins_dir): + if filename.endswith('.py'): + module_name = filename[:-3] + try: + __import__(module_name) + print(f"Imported plugin: {module_name}") + except Exception as e: + print(f"Failed to import plugin {module_name}: {e}") + +if __name__ == '__main__': + load_plugins() From 3b17c39d47b8416171903316b56cafd82b7b6720 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 02:02:17 +0800 Subject: [PATCH 25/35] Create event.py --- assets/scripts/event.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 assets/scripts/event.py diff --git a/assets/scripts/event.py b/assets/scripts/event.py new file mode 100644 index 000000000..c217762f6 --- /dev/null +++ b/assets/scripts/event.py @@ -0,0 +1,14 @@ +_handlers = {} + +def listen(event_name, callback): + if event_name not in _handlers: + _handlers[event_name] = [] + _handlers[event_name].append(callback) + +def call(event_name, *args): + if event_name in _handlers: + for cb in _handlers[event_name]: + try: + cb(*args) + except Exception as e: + print(f"Error in event {event_name}: {e}") From ea33e2e6e3d875511882645ebbab1766e7324017 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 02:03:24 +0800 Subject: [PATCH 26/35] Update loader.py --- assets/scripts/loader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/assets/scripts/loader.py b/assets/scripts/loader.py index 511e07c61..699c6fac5 100644 --- a/assets/scripts/loader.py +++ b/assets/scripts/loader.py @@ -15,6 +15,3 @@ def load_plugins(): print(f"Imported plugin: {module_name}") except Exception as e: print(f"Failed to import plugin {module_name}: {e}") - -if __name__ == '__main__': - load_plugins() From 38f6b06a49b347c5b97a76a9b9a34776336be895 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sat, 16 May 2026 02:04:16 +0800 Subject: [PATCH 27/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 22 ++++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index fa7b4b1a5..b1af3c908 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -16,6 +16,7 @@ Future initPython() async { final appSupportDir = await getApplicationSupportDirectory(); final pluginsDir = Directory('${appSupportDir.path}/plugins'); final eventFile = File('${appSupportDir.path}/event.py'); + final loaderFile = File('${appSupportDir.path}/loader.py'); if (!await pluginsDir.exists()) { await pluginsDir.create(recursive: true); @@ -25,6 +26,9 @@ Future initPython() async { final eventScript = await rootBundle.loadString('assets/scripts/event.py'); await eventFile.writeAsString(eventScript); + final loaderScript = await rootBundle.loadString('assets/scripts/loader.py'); + await loaderFile.writeAsString(loaderScript); + _vm = pkpy.VM(); _vm!.exec(''' @@ -33,22 +37,8 @@ sys.path.insert(0, r"${appSupportDir.path}") '''); _vm!.exec('import event'); - - final loadPluginsCode = ''' -import os -import sys -plugins_dir = os.path.join(sys.path[0], 'plugins') -if os.path.exists(plugins_dir): - for filename in os.listdir(plugins_dir): - if filename.endswith('.py'): - module_name = filename[:-3] - try: - __import__(module_name) - print(f"Imported plugin: {module_name}") - except Exception as e: - print(f"Failed to import plugin {module_name}: {e}") -'''; - _vm!.exec(loadPluginsCode); + _vm!.exec('import loader'); + _vm!.exec('loader.load_plugins()'); final out = _vm!.read_output(); if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); From 851ed1b1f9300cd88f3096b7bc5bf6e9af0cbde8 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 01:07:57 +0800 Subject: [PATCH 28/35] Update main.dart --- lib/main.dart | 52 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d46dd8bae..8b1feef90 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -import 'dart:developer'; +import 'dart:developer' as developer; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; @@ -40,6 +40,9 @@ import 'package:media_kit/media_kit.dart'; import 'package:island/core/services/python_service.dart' as python; +// 早期日志缓存队列 +final List _earlyLogs = []; + @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); @@ -47,19 +50,15 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { } void main(List args) async { - // Initialize logging + // 第一步:立即设置日志根级别和临时监听器(收集早期日志到缓存) + Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { - log( - [ - '[${record.time}] [${record.level}] ${record.message}', - if (record.error != null) 'Error: ${record.error}', - ?record.stackTrace, - ].join('\n'), - time: record.time, - level: record.level.value, - ); + _earlyLogs.add(record); }); + // 此时早期日志已经被缓存,但还未正式输出到 DevTools + // 以下所有初始化代码中产生的 Logger 日志都会被缓存起来 + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); @@ -86,7 +85,6 @@ void main(List args) async { try { await EasyLocalization.ensureInitialized(); - // Disable logs EasyLocalization.logger.enableBuildModes = []; if (kIsWeb || !Platform.isLinux) { @@ -146,17 +144,16 @@ void main(List args) async { final prefs = await SharedPreferences.getInstance(); - // Initialize pocketpy (non-web only) if (!kIsWeb) { try { await python.initPython(); if (python.isPythonAvailable()) { - log('[pocketpy] Initialized and executed all scripts in SolianApp'); + Logger.root.info("[pocketpy] Initialized and executed all scripts in SolianApp"); } else { - log('[pocketpy] SolianApp not found or init failed'); + Logger.root.info("[pocketpy] SolianApp not found or init failed"); } } catch (e) { - log('[pocketpy] Init error: $e'); + Logger.root.severe("[pocketpy] Init error", e); } } @@ -164,7 +161,6 @@ void main(List args) async { await windowManager.ensureInitialized(); const defaultSize = Size(360, 640); - final savedSizeString = prefs.getString(kAppWindowSize); Size initialSize = defaultSize; @@ -230,6 +226,27 @@ void main(List args) async { Logger.root.info("[SplashScreen] Now hiding splash screen..."); } + // 正式日志系统配置:将缓存中的日志重放,并接管未来所有日志 + Logger.root.onRecord.listen((record) { + developer.log( + record.message, + time: record.time, + level: record.level.value, + name: record.loggerName, + zone: Zone.current, + ); + }); + // 重放早期缓存的日志 + for (final record in _earlyLogs) { + developer.log( + record.message, + time: record.time, + level: record.level.value, + name: record.loggerName, + zone: Zone.current, + ); + } + runApp( ProviderScope( retry: (retryCount, error) { @@ -266,6 +283,7 @@ void main(List args) async { ); } +// 以下 IslandApp 等代码保持原样不变 final globalOverlay = GlobalKey(); final globalScaffoldMessengerKey = GlobalKey(); From 66ee792eff8b4a0d8771e2a3877060a02cfd8aab Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 01:25:05 +0800 Subject: [PATCH 29/35] Update main.dart --- lib/main.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 8b1feef90..b2d40db1f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,7 +40,6 @@ import 'package:media_kit/media_kit.dart'; import 'package:island/core/services/python_service.dart' as python; -// 早期日志缓存队列 final List _earlyLogs = []; @pragma('vm:entry-point') @@ -50,15 +49,11 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { } void main(List args) async { - // 第一步:立即设置日志根级别和临时监听器(收集早期日志到缓存) Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { _earlyLogs.add(record); }); - // 此时早期日志已经被缓存,但还未正式输出到 DevTools - // 以下所有初始化代码中产生的 Logger 日志都会被缓存起来 - final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); @@ -226,24 +221,20 @@ void main(List args) async { Logger.root.info("[SplashScreen] Now hiding splash screen..."); } - // 正式日志系统配置:将缓存中的日志重放,并接管未来所有日志 Logger.root.onRecord.listen((record) { developer.log( record.message, time: record.time, level: record.level.value, name: record.loggerName, - zone: Zone.current, ); }); - // 重放早期缓存的日志 for (final record in _earlyLogs) { developer.log( record.message, time: record.time, level: record.level.value, name: record.loggerName, - zone: Zone.current, ); } @@ -283,7 +274,6 @@ void main(List args) async { ); } -// 以下 IslandApp 等代码保持原样不变 final globalOverlay = GlobalKey(); final globalScaffoldMessengerKey = GlobalKey(); From 7231ace8e0fa8cb04e1cd1f0822666c5e06e066e Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 11:58:48 +0800 Subject: [PATCH 30/35] Update app_startup_splash.dart --- lib/shared/widgets/app_startup_splash.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/shared/widgets/app_startup_splash.dart b/lib/shared/widgets/app_startup_splash.dart index b1b161b75..137d728bf 100644 --- a/lib/shared/widgets/app_startup_splash.dart +++ b/lib/shared/widgets/app_startup_splash.dart @@ -9,7 +9,7 @@ import 'package:island/core/audio.dart'; import 'package:island/core/network.dart'; import 'package:island/core/services/notify.dart'; import 'package:island/core/websocket.dart'; -import 'package:island/core/services/python_service.dart' as python; // 新增导入 +import 'package:island/core/services/python_service.dart' as python; const kDefaultBootstrapRetryTimeouts = [ Duration(milliseconds: 1000), @@ -99,6 +99,14 @@ class StartupSplashScreen extends HookConsumerWidget { await ref.read(websocketStateProvider.notifier).connect(); }, ), + // Python 初始化阶段 - 放在网络连接之后,推送通知之前 + _BootstrapStage( + label: 'Initializing Python scripts', + isCritical: false, + action: () async { + await python.initPython(); + }, + ), _BootstrapStage( label: 'Registering push notifications', isCritical: false, @@ -125,14 +133,6 @@ class StartupSplashScreen extends HookConsumerWidget { await ref.read(messageSfxProvider.future); }, ), - // 新增:Python 脚本加载阶段 - _BootstrapStage( - label: 'Initializing Python scripts', - isCritical: false, // 非关键,失败不影响主流程 - action: () async { - await python.initPython(); - }, - ), ], [], ); From 8f501e00b868ef51eaf6b2b3b27ef08944828e0f Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 14:05:08 +0800 Subject: [PATCH 31/35] Update main.dart --- lib/main.dart | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b2d40db1f..52c2c616c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,7 +38,7 @@ import 'package:protocol_handler/protocol_handler.dart'; import 'package:island/core/services/unifiedpush_service.dart'; import 'package:media_kit/media_kit.dart'; -import 'package:island/core/services/python_service.dart' as python; +// 注意:不再导入 python_service final List _earlyLogs = []; @@ -139,18 +139,7 @@ void main(List args) async { final prefs = await SharedPreferences.getInstance(); - if (!kIsWeb) { - try { - await python.initPython(); - if (python.isPythonAvailable()) { - Logger.root.info("[pocketpy] Initialized and executed all scripts in SolianApp"); - } else { - Logger.root.info("[pocketpy] SolianApp not found or init failed"); - } - } catch (e) { - Logger.root.severe("[pocketpy] Init error", e); - } - } + // 移除 Python 初始化代码 if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) { await windowManager.ensureInitialized(); @@ -274,6 +263,7 @@ void main(List args) async { ); } +// 以下是 IslandApp 等代码保持不变... final globalOverlay = GlobalKey(); final globalScaffoldMessengerKey = GlobalKey(); From 1ad2755ef19486f70c82c974239ade232a458d83 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 14:11:29 +0800 Subject: [PATCH 32/35] Update app_wrapper.dart --- lib/shared/widgets/app_wrapper.dart | 38 ++++++++++++----------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/lib/shared/widgets/app_wrapper.dart b/lib/shared/widgets/app_wrapper.dart index aa15dde45..2e169ae28 100644 --- a/lib/shared/widgets/app_wrapper.dart +++ b/lib/shared/widgets/app_wrapper.dart @@ -44,6 +44,8 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:island/core/services/python_service.dart' as python; + const kForceShowStartupSplashForTesting = false; const kOnboardingLastShownVersion = 'app_onboarding_last_shown_version'; @@ -67,19 +69,16 @@ class AppWrapper extends HookConsumerWidget { final startupGateResolved = useState(false); final onboardingChecked = useState(false); - // Initialize progression WebSocket listener useEffect(() { ref.read(progressionWebSocketProvider); return null; }, []); - // Initialize friend status listener for toast notifications useEffect(() { ref.read(friendStatusListenerProvider); return null; }, []); - // Handle network status modal useEffect(() { bool triedOpen = false; if (!hasConnectivity && !networkStateShowing.value && !triedOpen) { @@ -147,7 +146,20 @@ class AppWrapper extends HookConsumerWidget { return null; }, [hasConnectivity, token, websocketState]); - // Initialize services and listeners + useEffect(() { + if (!kIsWeb) { + Future(() async { + await python.initPython(); + if (python.isPythonAvailable()) { + Logger.root.info("[pocketpy] Initialized from AppWrapper"); + } else { + Logger.root.info("[pocketpy] Not available (folder missing or init failed)"); + } + }); + } + return null; + }, []); + useEffect(() { final ntySubs = setupNotificationListener(context, ref); final sharingService = SharingIntentService(); @@ -199,7 +211,6 @@ class AppWrapper extends HookConsumerWidget { ref.read(rpcServerStateProvider.notifier).start(); ref.read(webAuthServerStateProvider.notifier).start(); - // Listen to special action events final composeSheetSubs = eventBus.on().listen(( event, ) { @@ -221,7 +232,6 @@ class AppWrapper extends HookConsumerWidget { if (ctx.mounted) _showThoughtSheet(ctx, event); }); - // Web auth request listener final webAuthSubs = eventBus.on().listen((event) { final ctx = ref.read(routerProvider).navigatorKey.currentContext!; if (ctx.mounted) _showWebAuthSheet(ctx, event); @@ -466,23 +476,16 @@ class AppWrapper extends HookConsumerWidget { void _handleDeepLink(Uri uri, WidgetRef ref, BuildContext context) async { String path = '/${uri.host}${uri.path}'; - // Web auth deep links for native apps: - // 1) Request challenge: - // solian://auth/web?app=MyApp&redirect_uri=myapp://auth-callback - // 2) Exchange signed challenge: - // solian://auth/web?signed_challenge=...&redirect_uri=myapp://auth-callback if (path == '/auth/web') { await _handleProtocolWebAuth(uri, ref, context); return; } - // Special handling for OIDC auth callback if (path == '/auth/callback' && uri.queryParameters.containsKey('token')) { final token = uri.queryParameters['token']!; setToken(ref.read(sharedPreferencesProvider), token); ref.invalidate(tokenProvider); - // Do post login tasks await performPostLogin(context, ref); if (!kIsWeb && @@ -492,9 +495,6 @@ class AppWrapper extends HookConsumerWidget { return; } - // Special handling for share intent deep links - // Share intents are handled by SharingIntentService showing a modal, - // not by routing to a page if (path == '/share') { if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { @@ -508,7 +508,6 @@ class AppWrapper extends HookConsumerWidget { return; } - // Handle NFC tag deep links: solian://phpass/ if (path.startsWith('/phpass/')) { final tagId = path.substring('/phpass/'.length); if (tagId.isNotEmpty) { @@ -517,17 +516,13 @@ class AppWrapper extends HookConsumerWidget { } } - // final router = ref.read(routerProvider); if (path == '/dashboard') { context.router.navigate(const DashboardRoute()); return; } - // Handle bottom navigation routes properly to prevent navigation bar disappearance - // These routes should navigate within the bottom navigation shell final bottomNavRoutes = ['/', '/explore', '/chat', '/realms', '/account']; if (bottomNavRoutes.contains(path)) { - // Navigate within the bottom navigation shell using go() to maintain shell context context.router.navigatePath(path); if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { @@ -541,7 +536,6 @@ class AppWrapper extends HookConsumerWidget { path, ).replace(queryParameters: uri.queryParameters).toString(); } - // For non-bottom navigation routes, use push() to navigate outside the shell context.router.navigatePath(path); if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { From 15b3620339670b6d76d2c300546513541fe2210f Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 14:26:17 +0800 Subject: [PATCH 33/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 49 +++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index b1af3c908..0731d0f20 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'dart:developer'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart'; import 'package:pocketpy/pocketpy.dart' as pkpy; import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; pkpy.VM? _vm; bool _isInitialized = false; @@ -13,30 +15,44 @@ Future initPython() async { if (_isInitialized) return; try { - final appSupportDir = await getApplicationSupportDirectory(); - final pluginsDir = Directory('${appSupportDir.path}/plugins'); - final eventFile = File('${appSupportDir.path}/event.py'); - final loaderFile = File('${appSupportDir.path}/loader.py'); + Directory baseDir; + if (kIsWeb) { + log('[python_service] Web platform, skipping'); + return; + } else if (Platform.isAndroid || Platform.isIOS) { + baseDir = await getApplicationSupportDirectory(); + } else { + final exeDir = path.dirname(Platform.resolvedExecutable); + baseDir = Directory(exeDir); + } + final pluginsDir = Directory(path.join(baseDir.path, 'plugins')); if (!await pluginsDir.exists()) { await pluginsDir.create(recursive: true); log('[python_service] Created plugins directory: ${pluginsDir.path}'); } - final eventScript = await rootBundle.loadString('assets/scripts/event.py'); - await eventFile.writeAsString(eventScript); + // 动态获取 assets/scripts/ 下的所有 .py 文件 + final manifest = await AssetManifest.loadFromAssetBundle(rootBundle); + final assets = manifest.listAssets(); + final scriptPaths = assets.where((asset) => asset.startsWith('assets/scripts/') && asset.endsWith('.py')).toList(); - final loaderScript = await rootBundle.loadString('assets/scripts/loader.py'); - await loaderFile.writeAsString(loaderScript); + for (final assetPath in scriptPaths) { + final fileName = path.basename(assetPath); + final destFile = File(path.join(baseDir.path, fileName)); + final content = await rootBundle.loadString(assetPath); + await destFile.writeAsString(content); + log('[python_service] Wrote $fileName to ${destFile.path}'); + } _vm = pkpy.VM(); _vm!.exec(''' import sys -sys.path.insert(0, r"${appSupportDir.path}") +sys.path.insert(0, r"${baseDir.path}") '''); - _vm!.exec('import event'); + // 确保 loader.py 存在(它应该在 scriptPaths 中) _vm!.exec('import loader'); _vm!.exec('loader.load_plugins()'); @@ -45,7 +61,7 @@ sys.path.insert(0, r"${appSupportDir.path}") if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); _isInitialized = true; - log('[python_service] Initialized'); + log('[python_service] Initialized, base dir: ${baseDir.path}'); } catch (e) { log('[python_service] Init failed: $e'); _isInitialized = false; @@ -53,17 +69,6 @@ sys.path.insert(0, r"${appSupportDir.path}") } } -Future callEvent(String eventName, List args) async { - if (!_isInitialized || _vm == null) return; - final argsStr = args.map((a) { - if (a is String) return '"${a.replaceAll('"', '\\"')}"'; - if (a is bool) return a ? 'True' : 'False'; - if (a == null) return 'None'; - return a.toString(); - }).join(', '); - await evalPythonCode('event.call("$eventName", $argsStr)'); -} - Future evalPythonCode(String code) async { if (!_isInitialized || _vm == null) return; _vm!.exec(code); From dddc218c5b5a98c2ac96f6812219eb05670cc0ea Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 16:21:13 +0800 Subject: [PATCH 34/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index 0731d0f20..0b3368194 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -1,10 +1,10 @@ import 'dart:io'; -import 'dart:developer'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart'; import 'package:pocketpy/pocketpy.dart' as pkpy; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; +import 'package:logging/logging.dart'; pkpy.VM? _vm; bool _isInitialized = false; @@ -17,7 +17,7 @@ Future initPython() async { try { Directory baseDir; if (kIsWeb) { - log('[python_service] Web platform, skipping'); + Logger.root.info('[python_service] Web platform, skipping'); return; } else if (Platform.isAndroid || Platform.isIOS) { baseDir = await getApplicationSupportDirectory(); @@ -29,7 +29,7 @@ Future initPython() async { final pluginsDir = Directory(path.join(baseDir.path, 'plugins')); if (!await pluginsDir.exists()) { await pluginsDir.create(recursive: true); - log('[python_service] Created plugins directory: ${pluginsDir.path}'); + Logger.root.info('[python_service] Created plugins directory: ${pluginsDir.path}'); } // 动态获取 assets/scripts/ 下的所有 .py 文件 @@ -42,7 +42,7 @@ Future initPython() async { final destFile = File(path.join(baseDir.path, fileName)); final content = await rootBundle.loadString(assetPath); await destFile.writeAsString(content); - log('[python_service] Wrote $fileName to ${destFile.path}'); + Logger.root.info('[python_service] Wrote $fileName to ${destFile.path}'); } _vm = pkpy.VM(); @@ -57,13 +57,13 @@ sys.path.insert(0, r"${baseDir.path}") _vm!.exec('loader.load_plugins()'); final out = _vm!.read_output(); - if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); - if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); + if (out.stdout.isNotEmpty) Logger.root.info('[Python stdout] ${out.stdout}'); + if (out.stderr.isNotEmpty) Logger.root.warning('[Python stderr] ${out.stderr}'); _isInitialized = true; - log('[python_service] Initialized, base dir: ${baseDir.path}'); + Logger.root.info('[python_service] Initialized, base dir: ${baseDir.path}'); } catch (e) { - log('[python_service] Init failed: $e'); + Logger.root.severe('[python_service] Init failed: $e'); _isInitialized = false; _vm = null; } @@ -73,6 +73,6 @@ Future evalPythonCode(String code) async { if (!_isInitialized || _vm == null) return; _vm!.exec(code); final out = _vm!.read_output(); - if (out.stdout.isNotEmpty) log('[Python stdout] ${out.stdout}'); - if (out.stderr.isNotEmpty) log('[Python stderr] ${out.stderr}'); + if (out.stdout.isNotEmpty) Logger.root.info('[Python stdout] ${out.stdout}'); + if (out.stderr.isNotEmpty) Logger.root.warning('[Python stderr] ${out.stderr}'); } From b056d56bb5000e9e2a1fdacb6803bf7decc91f55 Mon Sep 17 00:00:00 2001 From: CNlongY-Py <79128694+CNlongY-Py@users.noreply.github.com> Date: Sun, 17 May 2026 17:29:01 +0800 Subject: [PATCH 35/35] Update python_service_native.dart --- lib/core/services/python_service_native.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/core/services/python_service_native.dart b/lib/core/services/python_service_native.dart index 0b3368194..e74ab2ba2 100644 --- a/lib/core/services/python_service_native.dart +++ b/lib/core/services/python_service_native.dart @@ -32,7 +32,6 @@ Future initPython() async { Logger.root.info('[python_service] Created plugins directory: ${pluginsDir.path}'); } - // 动态获取 assets/scripts/ 下的所有 .py 文件 final manifest = await AssetManifest.loadFromAssetBundle(rootBundle); final assets = manifest.listAssets(); final scriptPaths = assets.where((asset) => asset.startsWith('assets/scripts/') && asset.endsWith('.py')).toList(); @@ -47,12 +46,9 @@ Future initPython() async { _vm = pkpy.VM(); - _vm!.exec(''' -import sys -sys.path.insert(0, r"${baseDir.path}") -'''); + _vm!.exec('import sys'); + _vm!.exec('sys.path.insert(0, r"${baseDir.path}")'); - // 确保 loader.py 存在(它应该在 scriptPaths 中) _vm!.exec('import loader'); _vm!.exec('loader.load_plugins()');