@@ -6,10 +6,13 @@ import 'package:crypto/crypto.dart';
66import 'package:dio/dio.dart' ;
77import 'package:file_picker/file_picker.dart' ;
88import 'package:flutter/services.dart' ;
9+ import 'package:now_chat/core/network/github_mirror_config.dart' ;
910import 'package:now_chat/core/models/plugin_manifest_v2.dart' ;
1011import 'package:now_chat/util/app_logger.dart' ;
1112import 'package:path/path.dart' as p;
1213
14+ typedef PluginGithubMirrorPreset = GithubMirrorPreset ;
15+
1316/// 插件包校验失败异常,包含期望与实际 SHA256。
1417class PluginPackageChecksumException implements Exception {
1518 final String packageId;
@@ -41,12 +44,26 @@ class LocalPluginImportPayload {
4144
4245/// 插件服务:负责清单拉取、zip 安装与本地导入解析。
4346class PluginService {
47+ static const String githubMirrorDirect = GithubMirrorConfig .directId;
48+ static const String githubMirrorGhfast = GithubMirrorConfig .ghfastId;
49+ static const String githubMirrorGhLlkk = GithubMirrorConfig .ghllkkId;
50+ static const String githubMirrorGhproxyNet = GithubMirrorConfig .ghproxyNetId;
51+ static const String githubMirrorCustom = GithubMirrorConfig .customId;
52+
53+ /// 插件中心镜像预设由统一配置中心维护。
54+ static const List <PluginGithubMirrorPreset > githubMirrorPresets =
55+ GithubMirrorConfig .presets;
56+
4457 final Dio _dio;
4558
4659 PluginService ({Dio ? dio}) : _dio = dio ?? Dio ();
4760
4861 /// 读取并解析通用插件清单。
49- Future <PluginManifestV2 > fetchManifest (String manifestUrl) async {
62+ Future <PluginManifestV2 > fetchManifest (
63+ String manifestUrl, {
64+ String mirrorId = githubMirrorDirect,
65+ String customMirrorBaseUrl = '' ,
66+ }) async {
5067 final normalizedUrl = manifestUrl.trim ();
5168 if (normalizedUrl.isEmpty) {
5269 throw const FormatException ('清单地址不能为空' );
@@ -62,7 +79,12 @@ class PluginService {
6279 final raw = await rootBundle.loadString (assetPath);
6380 data = raw;
6481 } else {
65- final response = await _dio.getUri <dynamic >(Uri .parse (normalizedUrl));
82+ final requestUrl = _applyMirrorToUrl (
83+ normalizedUrl,
84+ mirrorId: mirrorId,
85+ customMirrorBaseUrl: customMirrorBaseUrl,
86+ );
87+ final response = await _dio.getUri <dynamic >(Uri .parse (requestUrl));
6688 if (response.statusCode != 200 ) {
6789 throw Exception ('清单请求失败: ${response .statusCode }' );
6890 }
@@ -90,7 +112,11 @@ class PluginService {
90112 continue ;
91113 }
92114 try {
93- final repoPlugin = await fetchPluginDefinitionFromRepo (repoUrl);
115+ final repoPlugin = await fetchPluginDefinitionFromRepo (
116+ repoUrl,
117+ mirrorId: mirrorId,
118+ customMirrorBaseUrl: customMirrorBaseUrl,
119+ );
94120 // 清单 ID 优先,避免仓库变更 ID 导致本地记录对不上。
95121 final merged = repoPlugin.copyWith (
96122 id: plugin.id,
@@ -173,6 +199,8 @@ class PluginService {
173199 required Directory pluginRootDir,
174200 required String targetDir,
175201 void Function (double progress)? onProgress,
202+ String mirrorId = githubMirrorDirect,
203+ String customMirrorBaseUrl = '' ,
176204 }) async {
177205 AppLogger .i ('开始从仓库安装插件: repo=$repoUrl , targetDir=$targetDir ' );
178206 final tempDir = await Directory .systemTemp.createTemp ('now_chat_plugin_git_' );
@@ -188,6 +216,8 @@ class PluginService {
188216 repoUrl: repoUrl,
189217 outputZipPath: tempZipFile.path,
190218 onProgress: onProgress,
219+ mirrorId: mirrorId,
220+ customMirrorBaseUrl: customMirrorBaseUrl,
191221 );
192222
193223 if (installDir.existsSync ()) {
@@ -316,7 +346,11 @@ class PluginService {
316346 }
317347
318348 /// 从 GitHub 仓库拉取 README 文本。
319- Future <String > fetchReadmeFromRepo (String repoUrl) async {
349+ Future <String > fetchReadmeFromRepo (
350+ String repoUrl, {
351+ String mirrorId = githubMirrorDirect,
352+ String customMirrorBaseUrl = '' ,
353+ }) async {
320354 final (owner, repo) = _parseGithubOwnerRepo (repoUrl);
321355 final candidates = < String > [
322356 'https://raw.githubusercontent.com/$owner /$repo /main/README.md' ,
@@ -328,8 +362,13 @@ class PluginService {
328362 Object ? lastError;
329363 for (final url in candidates) {
330364 try {
365+ final requestUrl = _applyMirrorToUrl (
366+ url,
367+ mirrorId: mirrorId,
368+ customMirrorBaseUrl: customMirrorBaseUrl,
369+ );
331370 final response = await _dio.getUri <String >(
332- Uri .parse (url ),
371+ Uri .parse (requestUrl ),
333372 options: Options (responseType: ResponseType .plain),
334373 );
335374 if (response.statusCode == 200 ) {
@@ -355,7 +394,11 @@ class PluginService {
355394 }
356395
357396 /// 从 GitHub 仓库拉取并解析 `plugin.json` 。
358- Future <PluginDefinition > fetchPluginDefinitionFromRepo (String repoUrl) async {
397+ Future <PluginDefinition > fetchPluginDefinitionFromRepo (
398+ String repoUrl, {
399+ String mirrorId = githubMirrorDirect,
400+ String customMirrorBaseUrl = '' ,
401+ }) async {
359402 AppLogger .i ('开始读取仓库插件定义: $repoUrl ' );
360403 final (owner, repo) = _parseGithubOwnerRepo (repoUrl);
361404 final candidates = < String > [
@@ -365,8 +408,13 @@ class PluginService {
365408 Object ? lastError;
366409 for (final url in candidates) {
367410 try {
411+ final requestUrl = _applyMirrorToUrl (
412+ url,
413+ mirrorId: mirrorId,
414+ customMirrorBaseUrl: customMirrorBaseUrl,
415+ );
368416 final response = await _dio.getUri <String >(
369- Uri .parse (url ),
417+ Uri .parse (requestUrl ),
370418 options: Options (responseType: ResponseType .plain),
371419 );
372420 if (response.statusCode == 200 ) {
@@ -459,6 +507,8 @@ class PluginService {
459507 required String repoUrl,
460508 required String outputZipPath,
461509 void Function (double progress)? onProgress,
510+ String mirrorId = githubMirrorDirect,
511+ String customMirrorBaseUrl = '' ,
462512 }) async {
463513 final normalizedRepoUrl = _normalizeGitRepoUrl (repoUrl);
464514 final candidates = < String > [
@@ -468,8 +518,13 @@ class PluginService {
468518 DioException ? lastDioError;
469519 for (final candidateUrl in candidates) {
470520 try {
471- await _dio. download (
521+ final requestUrl = _applyMirrorToUrl (
472522 candidateUrl,
523+ mirrorId: mirrorId,
524+ customMirrorBaseUrl: customMirrorBaseUrl,
525+ );
526+ await _dio.download (
527+ requestUrl,
473528 outputZipPath,
474529 onReceiveProgress: (received, total) {
475530 if (onProgress == null || total <= 0 ) return ;
@@ -518,6 +573,60 @@ class PluginService {
518573 return (segments[0 ], segments[1 ]);
519574 }
520575
576+ /// 对 GitHub 相关链接应用镜像规则;非 GitHub 链接保持原样。
577+ String _applyMirrorToUrl (
578+ String url, {
579+ required String mirrorId,
580+ String customMirrorBaseUrl = '' ,
581+ }) {
582+ return GithubMirrorConfig .applyMirrorToUrl (
583+ url: url,
584+ mirrorId: mirrorId,
585+ customMirrorBaseUrl: customMirrorBaseUrl,
586+ onlyGithubHosts: true ,
587+ );
588+ }
589+
590+ /// 规范化用户输入的自定义代理地址。
591+ /// 返回空字符串代表输入无效。
592+ static String normalizeCustomMirrorBaseUrl (String input) {
593+ return GithubMirrorConfig .normalizeCustomBaseUrl (input);
594+ }
595+
596+ /// 对镜像做轻量测速(返回耗时毫秒,失败返回 null)。
597+ Future <int ?> probeMirrorLatency ({
598+ required String mirrorId,
599+ String customMirrorBaseUrl = '' ,
600+ Duration timeout = const Duration (seconds: 6 ),
601+ }) async {
602+ final probeTarget = 'https://raw.githubusercontent.com/CikeSeven/NowChat/main/plugin_manifest.json' ;
603+ final requestUrl = _applyMirrorToUrl (
604+ probeTarget,
605+ mirrorId: mirrorId,
606+ customMirrorBaseUrl: customMirrorBaseUrl,
607+ );
608+ final stopwatch = Stopwatch ()..start ();
609+ try {
610+ final response = await _dio.getUri <String >(
611+ Uri .parse (requestUrl),
612+ options: Options (
613+ responseType: ResponseType .plain,
614+ sendTimeout: timeout,
615+ receiveTimeout: timeout,
616+ validateStatus: (status) => status != null && status >= 200 && status < 500 ,
617+ ),
618+ );
619+ stopwatch.stop ();
620+ final statusCode = response.statusCode ?? 0 ;
621+ if (statusCode >= 200 && statusCode < 400 ) {
622+ return stopwatch.elapsedMilliseconds;
623+ }
624+ return null ;
625+ } catch (_) {
626+ return null ;
627+ }
628+ }
629+
521630 String _normalizeRelativePath (String path) {
522631 final normalized = path.replaceAll ('\\ ' , '/' );
523632 final segments =
0 commit comments