Skip to content

Commit 7abe9df

Browse files
committed
添加镜像网站与检查应用版本
1 parent ba4c0dd commit 7abe9df

9 files changed

Lines changed: 1019 additions & 24 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/// GitHub 镜像预设模型。
2+
class GithubMirrorPreset {
3+
final String id;
4+
final String name;
5+
final String description;
6+
final String baseUrl;
7+
8+
const GithubMirrorPreset({
9+
required this.id,
10+
required this.name,
11+
required this.description,
12+
required this.baseUrl,
13+
});
14+
}
15+
16+
/// GitHub 镜像配置中心。
17+
///
18+
/// 插件中心与应用更新检查都应从该处读取镜像清单,避免分散维护。
19+
class GithubMirrorConfig {
20+
static const String directId = 'direct';
21+
static const String ghfastId = 'ghfast';
22+
static const String ghllkkId = 'ghllkk';
23+
static const String ghproxyNetId = 'ghproxy_net';
24+
static const String customId = 'custom';
25+
26+
static const List<GithubMirrorPreset> presets = <GithubMirrorPreset>[
27+
GithubMirrorPreset(
28+
id: directId,
29+
name: '不走镜像',
30+
description: '直接访问 GitHub',
31+
baseUrl: '',
32+
),
33+
GithubMirrorPreset(
34+
id: ghfastId,
35+
name: 'ghfast.top',
36+
description: '前缀代理:ghfast.top/https://github.com/...',
37+
baseUrl: 'https://ghfast.top',
38+
),
39+
GithubMirrorPreset(
40+
id: ghllkkId,
41+
name: 'gh.llkk.cc',
42+
description: '前缀代理:gh.llkk.cc/https://github.com/...',
43+
baseUrl: 'https://gh.llkk.cc',
44+
),
45+
GithubMirrorPreset(
46+
id: ghproxyNetId,
47+
name: 'ghproxy.net',
48+
description: '前缀代理:ghproxy.net/https://github.com/...',
49+
baseUrl: 'https://ghproxy.net',
50+
),
51+
GithubMirrorPreset(
52+
id: customId,
53+
name: '自定义代理',
54+
description: '前缀代理:<你的地址>/https://github.com/...',
55+
baseUrl: '',
56+
),
57+
];
58+
59+
/// 自动回退链路(用于更新检查):不包含自定义项。
60+
static const List<String> automaticFallbackMirrorIds = <String>[
61+
directId,
62+
ghfastId,
63+
ghllkkId,
64+
ghproxyNetId,
65+
];
66+
67+
static GithubMirrorPreset? findById(String id) {
68+
for (final preset in presets) {
69+
if (preset.id == id) return preset;
70+
}
71+
return null;
72+
}
73+
74+
/// 规范化用户输入的自定义代理地址。
75+
static String normalizeCustomBaseUrl(String input) {
76+
var value = input.trim();
77+
if (value.isEmpty) return '';
78+
if (!value.startsWith('http://') && !value.startsWith('https://')) {
79+
value = 'https://$value';
80+
}
81+
Uri uri;
82+
try {
83+
uri = Uri.parse(value);
84+
} catch (_) {
85+
return '';
86+
}
87+
if (uri.host.trim().isEmpty) return '';
88+
var normalized = uri.toString();
89+
while (normalized.endsWith('/')) {
90+
normalized = normalized.substring(0, normalized.length - 1);
91+
}
92+
return normalized;
93+
}
94+
95+
/// 对 URL 应用镜像。
96+
///
97+
/// [onlyGithubHosts] 为 true 时,仅对 GitHub 相关域名生效。
98+
static String applyMirrorToUrl({
99+
required String url,
100+
required String mirrorId,
101+
String customMirrorBaseUrl = '',
102+
bool onlyGithubHosts = true,
103+
}) {
104+
final normalizedId = mirrorId.trim().isEmpty ? directId : mirrorId.trim();
105+
if (normalizedId == directId) return url;
106+
107+
if (onlyGithubHosts && !_isGithubRelatedUrl(url)) {
108+
return url;
109+
}
110+
111+
final baseUrl =
112+
normalizedId == customId
113+
? normalizeCustomBaseUrl(customMirrorBaseUrl)
114+
: (findById(normalizedId)?.baseUrl ?? '');
115+
if (baseUrl.trim().isEmpty) return url;
116+
return '$baseUrl/$url';
117+
}
118+
119+
static bool _isGithubRelatedUrl(String rawUrl) {
120+
Uri uri;
121+
try {
122+
uri = Uri.parse(rawUrl);
123+
} catch (_) {
124+
return false;
125+
}
126+
final host = uri.host.toLowerCase();
127+
return host == 'github.com' ||
128+
host == 'raw.githubusercontent.com' ||
129+
host == 'codeload.github.com' ||
130+
host == 'api.github.com';
131+
}
132+
}

lib/core/plugin/plugin_service.dart

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import 'package:crypto/crypto.dart';
66
import 'package:dio/dio.dart';
77
import 'package:file_picker/file_picker.dart';
88
import 'package:flutter/services.dart';
9+
import 'package:now_chat/core/network/github_mirror_config.dart';
910
import 'package:now_chat/core/models/plugin_manifest_v2.dart';
1011
import 'package:now_chat/util/app_logger.dart';
1112
import 'package:path/path.dart' as p;
1213

14+
typedef PluginGithubMirrorPreset = GithubMirrorPreset;
15+
1316
/// 插件包校验失败异常,包含期望与实际 SHA256。
1417
class PluginPackageChecksumException implements Exception {
1518
final String packageId;
@@ -41,12 +44,26 @@ class LocalPluginImportPayload {
4144

4245
/// 插件服务:负责清单拉取、zip 安装与本地导入解析。
4346
class 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

Comments
 (0)