Skip to content

Commit 740ccb7

Browse files
authored
Merge pull request #49 from shadowfish07/issue-39
refactor: 重构 OpenRouter API 客户端并改进 AI 翻译功能
2 parents da86932 + 3103c2d commit 740ccb7

File tree

10 files changed

+2288
-17
lines changed

10 files changed

+2288
-17
lines changed

.releaserc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
{ "type": "docs", "release": false },
2121
{ "type": "style", "release": false },
2222
{ "type": "chore", "release": false },
23-
{ "type": "refactor", "release": "patch" },
23+
{ "type": "refactor", "release": false },
2424
{ "type": "test", "release": false },
2525
{ "type": "build", "release": false },
2626
{ "type": "ci", "release": false },

lib/data/service/openrouter_api_client.dart

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@ import 'package:result_dart/result_dart.dart';
1010
/// OpenRouter API 客户端
1111
/// 提供与 OpenRouter API 的交互功能,支持流式聊天完成
1212
class OpenRouterApiClient {
13-
OpenRouterApiClient(this._sharedPreferencesService, {String? baseUrl})
14-
: _baseUrl = baseUrl ?? 'https://openrouter.ai/api/v1';
13+
OpenRouterApiClient(this._sharedPreferencesService,
14+
{String? baseUrl, http.Client? httpClient})
15+
: _baseUrl = baseUrl ?? 'https://openrouter.ai/api/v1',
16+
_httpClient = httpClient ?? http.Client();
1517

1618
final String _baseUrl;
1719
final SharedPreferencesService _sharedPreferencesService;
20+
final http.Client _httpClient;
1821
String? _apiKey;
1922

23+
/// 释放资源
24+
void dispose() {
25+
_httpClient.close();
26+
}
27+
2028
/// 初始化API密钥
2129
Future<void> _initApiKey() async {
2230
if (_apiKey == null) {
@@ -37,7 +45,6 @@ class OpenRouterApiClient {
3745
Map<String, String> get _headers => {
3846
'Authorization': 'Bearer $_apiKey',
3947
'Content-Type': 'application/json',
40-
// 'HTTP-Referer': 'https://readeck-app.zqydev.me',
4148
'X-Title': 'ReadeckApp',
4249
};
4350

@@ -88,7 +95,7 @@ class OpenRouterApiClient {
8895
appLogger.d('发送流式聊天请求到 OpenRouter: $uri');
8996
appLogger.d('请求体: ${jsonEncode(requestBody)}');
9097

91-
final streamedResponse = await request.send();
98+
final streamedResponse = await _httpClient.send(request);
9299

93100
if (streamedResponse.statusCode != 200) {
94101
appLogger.w('OpenRouter API 请求失败。状态码: ${streamedResponse.statusCode}');
@@ -180,7 +187,7 @@ class OpenRouterApiClient {
180187

181188
appLogger.d('发送聊天请求到 OpenRouter: $uri');
182189

183-
final response = await http.post(
190+
final response = await _httpClient.post(
184191
uri,
185192
headers: _headers,
186193
body: jsonEncode(requestBody),
@@ -262,7 +269,7 @@ class OpenRouterApiClient {
262269
appLogger.d('发送流式文本完成请求到 OpenRouter: $uri');
263270
appLogger.d('请求体: ${jsonEncode(requestBody)}');
264271

265-
final streamedResponse = await request.send();
272+
final streamedResponse = await _httpClient.send(request);
266273

267274
if (streamedResponse.statusCode != 200) {
268275
appLogger.w('OpenRouter API 请求失败。状态码: ${streamedResponse.statusCode}');
@@ -357,7 +364,7 @@ class OpenRouterApiClient {
357364

358365
appLogger.d('发送文本完成请求到 OpenRouter: $uri');
359366

360-
final response = await http.post(
367+
final response = await _httpClient.post(
361368
uri,
362369
headers: _headers,
363370
body: jsonEncode(requestBody),
@@ -400,7 +407,7 @@ class OpenRouterApiClient {
400407

401408
appLogger.d('获取 OpenRouter 模型列表: $uri');
402409

403-
final response = await http.get(uri, headers: _headers);
410+
final response = await _httpClient.get(uri, headers: _headers);
404411

405412
if (response.statusCode == 200) {
406413
final responseData = jsonDecode(response.body);

lib/routing/router.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ GoRouter router(SettingsRepository settingsRepository) => GoRouter(
198198
context.read(),
199199
context.read(),
200200
context.read(),
201+
context.read(),
201202
bookmark,
202203
);
203204
return ChangeNotifierProvider.value(

lib/ui/bookmarks/view_models/bookmark_detail_viewmodel.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import 'package:readeck_app/data/repository/bookmark/bookmark_repository.dart';
44
import 'package:readeck_app/domain/models/bookmark/bookmark.dart';
55
import 'package:readeck_app/domain/use_cases/bookmark_operation_use_cases.dart';
66
import 'package:readeck_app/data/repository/label/label_repository.dart';
7+
import 'package:readeck_app/data/repository/settings/settings_repository.dart';
78
import 'package:readeck_app/main.dart';
89

910
class BookmarkDetailViewModel extends ChangeNotifier {
10-
BookmarkDetailViewModel(this._bookmarkRepository,
11-
this._bookmarkOperationUseCases, this._labelRepository, this._bookmark) {
11+
BookmarkDetailViewModel(
12+
this._bookmarkRepository,
13+
this._bookmarkOperationUseCases,
14+
this._labelRepository,
15+
this._settingsRepository,
16+
this._bookmark) {
1217
// 注册标签数据变化监听器
1318
_labelRepository.addListener(_onLabelsChanged);
1419
// 注册书签数据变化监听器
@@ -41,6 +46,7 @@ class BookmarkDetailViewModel extends ChangeNotifier {
4146
final BookmarkRepository _bookmarkRepository;
4247
final BookmarkOperationUseCases _bookmarkOperationUseCases;
4348
final LabelRepository _labelRepository;
49+
final SettingsRepository _settingsRepository;
4450
Bookmark _bookmark;
4551

4652
// AI翻译相关状态
@@ -230,6 +236,13 @@ class BookmarkDetailViewModel extends ChangeNotifier {
230236
try {
231237
appLogger.i('开始AI翻译内容');
232238

239+
// 检查OpenRouter API Key是否已配置
240+
final apiKeyResult = await _settingsRepository.getOpenRouterApiKey();
241+
if (apiKeyResult.isError() || apiKeyResult.getOrNull()?.isEmpty == true) {
242+
appLogger.w('OpenRouter API Key未配置,无法进行AI翻译');
243+
throw '请先在设置中配置OpenRouter API Key';
244+
}
245+
233246
_isTranslateMode = true;
234247
_isTranslating = true;
235248
_translatedContent = ''; // 清空之前的翻译内容

lib/ui/settings/widgets/ai_settings_screen.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,6 @@ class _AiSettingsScreenState extends State<AiSettingsScreen> {
131131
prefixIcon: Icon(Icons.key),
132132
),
133133
obscureText: true,
134-
validator: (value) {
135-
if (value == null || value.trim().isEmpty) {
136-
return '请输入 API 密钥';
137-
}
138-
return null;
139-
},
140134
onFieldSubmitted: (_) => _saveApiKey(),
141135
),
142136
const SizedBox(height: 16),

test/fixtures/test_data.dart

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'package:readeck_app/domain/models/daily_read_history/daily_read_history.dart';
22
import 'package:readeck_app/domain/models/bookmark_article/bookmark_article.dart';
3+
import 'package:readeck_app/domain/models/bookmark/bookmark.dart';
4+
import 'package:readeck_app/domain/models/bookmark/label_info.dart';
35

46
/// 测试用的书签文章数据
57
class TestBookmarkArticleData {
@@ -52,6 +54,113 @@ class TestDailyReadHistoryData {
5254
}
5355
}
5456

57+
/// 测试用的书签数据
58+
class TestBookmarkData {
59+
static Bookmark createSample({
60+
String? id,
61+
String? title,
62+
String? url,
63+
String? siteName,
64+
String? description,
65+
bool? isMarked,
66+
bool? isArchived,
67+
int? readProgress,
68+
List<String>? labels,
69+
DateTime? created,
70+
String? imageUrl,
71+
}) {
72+
return Bookmark(
73+
id: id ?? 'test-bookmark-1',
74+
title: title ?? 'Test Bookmark Title',
75+
url: url ?? 'https://example.com/test',
76+
siteName: siteName ?? 'example.com',
77+
description: description ?? 'Test bookmark description',
78+
isMarked: isMarked ?? false,
79+
isArchived: isArchived ?? false,
80+
readProgress: readProgress ?? 0,
81+
labels: labels ?? ['tech', 'flutter'],
82+
created: created ?? DateTime.parse('2024-01-01T00:00:00Z'),
83+
imageUrl: imageUrl,
84+
);
85+
}
86+
87+
static List<Bookmark> createMultipleSamples(int count) {
88+
return List.generate(
89+
count,
90+
(index) => createSample(
91+
id: 'test-bookmark-${index + 1}',
92+
title: 'Test Bookmark ${index + 1}',
93+
url: 'https://example.com/test-${index + 1}',
94+
),
95+
);
96+
}
97+
98+
static Map<String, dynamic> createSampleJson({
99+
String? id,
100+
String? title,
101+
String? url,
102+
String? siteName,
103+
String? description,
104+
bool? isMarked,
105+
bool? isArchived,
106+
int? readProgress,
107+
List<String>? labels,
108+
String? created,
109+
String? imageUrl,
110+
}) {
111+
return {
112+
'id': id ?? 'test-bookmark-1',
113+
'title': title ?? 'Test Bookmark Title',
114+
'url': url ?? 'https://example.com/test',
115+
'site_name': siteName ?? 'example.com',
116+
'description': description ?? 'Test bookmark description',
117+
'is_marked': isMarked ?? false,
118+
'is_archived': isArchived ?? false,
119+
'read_progress': readProgress ?? 0,
120+
'labels': labels ?? ['tech', 'flutter'],
121+
'created': created ?? '2024-01-01T00:00:00Z',
122+
'image_url': imageUrl,
123+
};
124+
}
125+
}
126+
127+
/// 测试用的标签数据
128+
class TestLabelData {
129+
static LabelInfo createSample({
130+
String? name,
131+
int? count,
132+
String? href,
133+
String? hrefBookmarks,
134+
}) {
135+
return LabelInfo(
136+
name: name ?? 'tech',
137+
count: count ?? 10,
138+
href: href ?? '/api/labels/tech',
139+
hrefBookmarks: hrefBookmarks ?? '/api/bookmarks?labels=tech',
140+
);
141+
}
142+
143+
static List<LabelInfo> createMultipleSamples(int count) {
144+
return List.generate(
145+
count,
146+
(index) => createSample(
147+
name: 'label-${index + 1}',
148+
count: (index + 1) * 5,
149+
),
150+
);
151+
}
152+
153+
static Map<String, dynamic> createSampleJson({
154+
String? name,
155+
int? count,
156+
}) {
157+
return {
158+
'name': name ?? 'tech',
159+
'count': count ?? 10,
160+
};
161+
}
162+
}
163+
55164
/// 测试用的书签ID列表
56165
class TestBookmarkIds {
57166
static const List<String> sample1 = ['bookmark1', 'bookmark2', 'bookmark3'];

0 commit comments

Comments
 (0)