Skip to content

Commit 9c6fcaf

Browse files
committed
fix(network): 添加Dio基础URL变更日志便于调试
feat(model): 增加ChatSessionModel.summary的安全解析方法 refactor(datasource): 重构AppRemoteDataSource.getAppInfo以正确处理网络错误 feat(settings): 在服务器设置页面添加本地存储支持和Android模拟器主机映射
1 parent b2e28d3 commit 9c6fcaf

5 files changed

Lines changed: 91 additions & 27 deletions

File tree

lib/core/network/dio_client.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class DioClient {
2323

2424
void updateBaseUrl(String baseUrl) {
2525
_dio.options.baseUrl = baseUrl;
26+
// Log base URL change for easier debugging during configuration updates
27+
if (const bool.fromEnvironment('dart.vm.product') == false) {
28+
print('[Dio] Base URL updated: $baseUrl');
29+
}
2630
}
2731

2832
void _setupInterceptors() {

lib/data/datasources/app_remote_datasource.dart

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,40 @@ class AppRemoteDataSourceImpl implements AppRemoteDataSource {
2424

2525
@override
2626
Future<AppInfoModel> getAppInfo({String? directory}) async {
27-
try {
28-
final queryParams = directory != null ? {'directory': directory} : <String, dynamic>{};
29-
final response = await dio.get('/app/info', queryParameters: queryParams);
30-
return AppInfoModel.fromJson(response.data);
31-
} catch (e) {
32-
print('获取应用信息时出错: $e');
33-
// 返回默认的应用信息
34-
return AppInfoModel(
35-
hostname: 'localhost',
36-
git: false,
37-
path: AppPathModel(
38-
config: '/config',
39-
data: '/data',
40-
root: '/',
41-
cwd: '/app',
42-
state: '/state',
43-
),
44-
);
45-
}
27+
// Do NOT swallow network errors here.
28+
// Align to OpenAPI: use /path and /config instead of /app/info.
29+
// Let Dio throw exceptions so upper layers can handle connection status correctly.
30+
final queryParams = directory != null
31+
? {'directory': directory}
32+
: <String, dynamic>{};
33+
34+
// Fetch path info
35+
final pathResp = await dio.get('/path', queryParameters: queryParams);
36+
37+
// Fetch config info (optional for future use)
38+
final configResp = await dio.get('/config', queryParameters: queryParams);
39+
40+
final Map<String, dynamic> pathJson = pathResp.data as Map<String, dynamic>;
41+
42+
// Map OpenAPI Path schema to AppInfoModel expected structure.
43+
// Where exact fields don't exist, provide sensible defaults.
44+
final mapped = <String, dynamic>{
45+
'hostname': 'OpenCode',
46+
'git': false,
47+
'path': {
48+
'config': pathJson['config'] ?? '',
49+
// There is no 'data' in Path schema; use worktree as a reasonable default.
50+
'data': pathJson['worktree'] ?? '',
51+
// Root is not explicitly defined; use worktree as root.
52+
'root': pathJson['worktree'] ?? '',
53+
// Use directory as current working directory.
54+
'cwd': pathJson['directory'] ?? '',
55+
'state': pathJson['state'] ?? '',
56+
},
57+
'time': null,
58+
};
59+
60+
return AppInfoModel.fromJson(mapped);
4661
}
4762

4863
@override

lib/data/models/chat_session_model.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ChatSessionModel {
2424
final String? title;
2525
final String? version;
2626
final bool shared;
27+
@JsonKey(fromJson: _summaryFromJson)
2728
final String? summary;
2829
final SessionPathModel? path;
2930
final SessionShareModel? share;
@@ -33,6 +34,20 @@ class ChatSessionModel {
3334

3435
Map<String, dynamic> toJson() => _$ChatSessionModelToJson(this);
3536

37+
/// Safely parse summary from API which may return Map or String
38+
static String? _summaryFromJson(dynamic value) {
39+
if (value == null) return null;
40+
if (value is String) return value;
41+
if (value is Map<String, dynamic>) {
42+
final additions = value['additions'];
43+
final deletions = value['deletions'];
44+
// Convert to a compact string for display
45+
return 'additions: ${additions ?? 0}, deletions: ${deletions ?? 0}';
46+
}
47+
// Fallback to string conversion
48+
return value.toString();
49+
}
50+
3651
/// 转换为领域实体
3752
ChatSession toDomain() {
3853
return ChatSession(

lib/data/models/chat_session_model.g.dart

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/presentation/pages/server_settings_page.dart

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter/foundation.dart';
23
import 'package:provider/provider.dart';
34
import '../providers/app_provider.dart';
45
import '../../core/constants/app_constants.dart';
56
import '../../core/constants/api_constants.dart';
7+
import '../../core/di/injection_container.dart';
8+
import '../../data/datasources/app_local_datasource.dart';
69

710
/// Server settings page
811
class ServerSettingsPage extends StatefulWidget {
@@ -23,6 +26,19 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
2326
final appProvider = context.read<AppProvider>();
2427
_hostController.text = appProvider.serverHost;
2528
_portController.text = appProvider.serverPort.toString();
29+
30+
// Load saved server config from local storage and sync to UI/provider
31+
Future.microtask(() async {
32+
final local = sl<AppLocalDataSource>();
33+
final savedHost = await local.getServerHost();
34+
final savedPort = await local.getServerPort();
35+
if (savedHost != null && savedPort != null && mounted) {
36+
_hostController.text = savedHost;
37+
_portController.text = savedPort.toString();
38+
// Keep provider state consistent so other parts reflect the same values
39+
appProvider.setServerConfig(savedHost, savedPort);
40+
}
41+
});
2642
}
2743

2844
@override
@@ -228,13 +244,27 @@ class _ServerSettingsPageState extends State<ServerSettingsPage> {
228244
final host = _hostController.text.trim();
229245
final port = int.parse(_portController.text.trim());
230246

247+
// On Android emulator, localhost/127.0.0.1 should point to 10.0.2.2
248+
// This mapping avoids accidental calls to the host machine's loopback.
249+
var mappedHost = host;
250+
final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
251+
if (isAndroid && (host == '127.0.0.1' || host.toLowerCase() == 'localhost')) {
252+
mappedHost = '10.0.2.2';
253+
}
254+
231255
final appProvider = context.read<AppProvider>();
232-
final success = await appProvider.updateServerConfig(host, port);
256+
final success = await appProvider.updateServerConfig(mappedHost, port);
233257

234258
if (success && mounted) {
235-
ScaffoldMessenger.of(
236-
context,
237-
).showSnackBar(const SnackBar(content: Text('Settings saved')));
259+
final info = 'Settings saved: http://$mappedHost:$port';
260+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(info)));
261+
if (mappedHost != host && mounted) {
262+
ScaffoldMessenger.of(context).showSnackBar(
263+
const SnackBar(
264+
content: Text('Android emulator detected: mapped localhost to 10.0.2.2'),
265+
),
266+
);
267+
}
238268
} else if (mounted) {
239269
ScaffoldMessenger.of(context).showSnackBar(
240270
SnackBar(

0 commit comments

Comments
 (0)