Skip to content

Commit 887d859

Browse files
committed
Merge branch 'aiTian' into flutter3.19
2 parents d520718 + a2a2542 commit 887d859

File tree

9 files changed

+643
-481
lines changed

9 files changed

+643
-481
lines changed

lib/commons/environment/config.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class EnvConfig {
3939
/// 微北洋版本信息,请勿修改代码,这里的默认值由脚本生成
4040
static const VERSIONCODE = int.fromEnvironment(
4141
"VERSIONCODE",
42-
defaultValue : 156, // 设置非常小方便做更新测试
42+
defaultValue : 163, // 设置非常小方便做更新测试
4343
);
4444

4545
/// 青年湖底域名 "https://www.zrzz.site:7013/" (DEFAULT) 或 "https://qnhd.twt.edu.cn/"

lib/commons/themes/scheme/dark_scheme.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ final Map<WpyColorKey, Color> darkSchemeDetail = {
4646
WpyColorKey.beanDarkColor: Color(0xFF3687E5),
4747
WpyColorKey.beanLightColor: Color(0xFF4B81C7),
4848

49-
WpyColorKey.lightPrimaryContainer: Color(0xFF3687E5),
50-
WpyColorKey.lighterPrimaryBackGround: Color(0xFF3687E5),
49+
WpyColorKey.lightPrimaryContainer: Color(0xFF515F70),
50+
WpyColorKey.lighterPrimaryBackGround: Color(0xFF101010),
5151

5252
// schedule page background color
5353
WpyColorKey.primaryLighterActionColor: Color(0xFF183C50),

lib/home/view/wpy_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ class SliverCardsWidget extends StatelessWidget {
288288
'地图·校历',
289289
'成绩',
290290
// '小游戏'
291-
'失物招领'
291+
// '失物招领'
292292
];
293293

294294
SliverCardsWidget(this.cards) :

lib/xiaotian/model/xiaotian_dio.dart

Lines changed: 174 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import 'dart:async';
22
import 'dart:convert';
3+
import 'package:http/http.dart' as http;
4+
import 'dart:io';
35
import 'package:dio/dio.dart';
6+
import 'package:dio/io.dart';
7+
import 'package:we_pei_yang_flutter/commons/preferences/common_prefs.dart';
48
import 'dart:math';
59
import 'xiaotian_model.dart';
610

11+
712
const XIAOTIAN_URL = 'https://student.tju.edu.cn/ai';
813

914
/// API 单例
@@ -21,131 +26,195 @@ class AiTjuApi {
2126
),
2227
);
2328

24-
// 2. 添加 LogInterceptor 拦截器
25-
void setupDio() {
26-
dio.interceptors.add(
27-
LogInterceptor(
28-
requestHeader: true, // 是否打印请求头
29-
requestBody: true, // 是否打印请求体
30-
responseHeader: true, // 是否打印响应头
31-
responseBody: false, // 是否打印响应体
32-
error: true, // 是否打印错误信息
33-
logPrint: (obj) => print(obj.toString()), // 使用 print 函数来输出日志
34-
),
35-
);
36-
}
37-
38-
/// Cho phép cập nhật header mặc định (Cookie/Authorization...) nếu muốn
39-
void updateDefaultHeaders(Map<String, String> headers) {
40-
dio.options.headers.addAll(headers);
41-
}
42-
43-
/* SSE 流式对话 */
4429
Stream<ChatEvent> streamChat({
4530
required String prompt,
4631
required String sessionId,
47-
required String userId,
48-
List<String>? files,
4932
String? searchTime,
5033
String? searchType,
51-
Map<String, String>? headers, // header
52-
}) async* {
53-
// 1. 创建并启动计时器
54-
final stopwatch = Stopwatch()..start();
55-
bool firstLineReceived = false;
56-
bool firstDataEventYielded = false;
57-
58-
print(" T0: [${stopwatch.elapsedMilliseconds}ms] 开始执行 streamChat 方法...");
34+
Map<String, String>? headers,
35+
}) {
36+
final streamController = StreamController<ChatEvent>();
5937

60-
final params = <String, dynamic>{
38+
final params = {
6139
'prompt': prompt,
62-
'sessionId': sessionId,
63-
'userId': userId,
64-
if (files != null) 'files': files,
65-
if (searchTime != null) 'searchTime': searchTime,
66-
if (searchType != null) 'searchType': searchType,
40+
'session_id': sessionId,
41+
'user_id': CommonPreferences.userNumber.value, //获取账号学号
42+
'searchTime': searchTime ?? 'noLimit',
43+
'searchType': searchType ?? 'no',
6744
};
6845

69-
try {
70-
final rs = await dio.get(
71-
'/ai-api/ai/stream',
72-
queryParameters: params,
73-
options: Options(
74-
responseType: ResponseType.stream,
75-
headers: {
76-
'Accept': 'text/event-stream',
77-
...?headers,
78-
},
79-
),
80-
);
81-
82-
// 2. 记录 dio.get 完成的时间点
83-
// 这代表服务器已响应,HTTP头部已收到,准备开始接收数据流
84-
print(" T1: [${stopwatch.elapsedMilliseconds}ms] Dio请求成功,收到服务器响应头(Headers)。");
46+
final url = Uri.https('student.tju.edu.cn', '/ai-rag/api/chat/stream');
47+
var request = http.Request("POST", url)
48+
..bodyFields = params
49+
..headers.addAll({
50+
"Authorization": CommonPreferences.token.value, //获取账号token
51+
"Accept": "text/event-stream",
52+
'Content-Type': 'application/x-www-form-urlencoded',
53+
...?headers,
54+
});
8555

86-
final lines = rs.data.stream
87-
.cast<List<int>>()
88-
.transform(utf8.decoder)
89-
.transform(const LineSplitter());
56+
http.Client().send(request).then((response) {
57+
final stream = response.stream.transform(utf8.decoder);
58+
bool firstDataEventYielded = false;
9059

91-
await for (final line in lines) {
92-
// 3. 记录收到第一行数据的时间点
93-
if (!firstLineReceived) {
94-
firstLineReceived = true;
95-
print(" T2: [${stopwatch.elapsedMilliseconds}ms] 收到第一行流数据。");
96-
}
97-
98-
if (!line.startsWith('data:')) {
99-
continue;
100-
}
60+
stream.listen(
61+
(data) {
62+
final dataLines = data.split("\n").where((element) => element.trim().isNotEmpty).toList();
63+
for (String line in dataLines) {
64+
line = line.trim();
65+
if (line.startsWith('event:')) continue;
66+
if (!line.startsWith('data:')) continue;
10167

102-
final payload = line.substring(5).trimLeft();
103-
if (payload.isEmpty) continue;
104-
if (payload == '[DONE]') break;
68+
final payload = line.substring(5).trimLeft();
69+
if (payload.isEmpty || payload == '[DONE]') continue;
10570

106-
try {
107-
final map = jsonDecode(payload);
71+
try {
72+
final map = jsonDecode(payload);
73+
if (!firstDataEventYielded && map.keys.any((k) => ['token', 'sources', 'question', 'trace_id', 'error'].contains(k))) {
74+
firstDataEventYielded = true;
75+
}
10876

109-
// 4. 记录解析并准备推送第一个有效事件的时间点
110-
if (!firstDataEventYielded) {
111-
// 确保这是一个有内容的事件,而不是空的 keep-alive 包
112-
if (map.keys.any((k) => ['token', 'sources', 'question', 'trace_id', 'error'].contains(k))) {
113-
firstDataEventYielded = true;
114-
print(" T3: [${stopwatch.elapsedMilliseconds}ms] 解析并产出(yield)第一个有效数据事件。");
77+
if (map['token'] != null) streamController.add(ChatEvent.token(map['token']));
78+
if (map['question'] != null) streamController.add(ChatEvent.followup(map['question']));
79+
if (map['sources'] != null) {
80+
final list = (map['sources'] as List).map((e) => Source.fromJson(e as Map<String, dynamic>)).toList();
81+
streamController.add(ChatEvent.source(list));
82+
}
83+
if (map['trace_id'] != null) streamController.add(ChatEvent.traceId(map['trace_id'].toString()));
84+
if (map['error'] != null) streamController.add(ChatEvent.error(map['error'].toString()));
85+
} catch (e) {
86+
// Ignore json parsing errors for incomplete data chunks
11587
}
11688
}
117-
118-
// --- 原有逻辑 ---
119-
if (map['token'] != null) yield ChatEvent.token(map['token']);
120-
if (map['sources'] != null) {
121-
final list = (map['sources'] as List)
122-
.map((e) => Source.fromJson(e as Map<String, dynamic>))
123-
.toList();
124-
yield ChatEvent.source(list);
125-
}
126-
if (map['question'] != null) {
127-
yield ChatEvent.followup(map['question'].toString());
128-
}
129-
if (map['trace_id'] != null) {
130-
yield ChatEvent.traceId(map['trace_id'].toString());
131-
}
132-
if (map['error'] != null) {
133-
yield ChatEvent.error(map['error'].toString());
89+
},
90+
onDone: () {
91+
if (!streamController.isClosed) streamController.close();
92+
},
93+
onError: (e, st) {
94+
if (!streamController.isClosed) {
95+
streamController.add(ChatEvent.error('Stream failed: $e'));
96+
streamController.close();
13497
}
135-
} catch (e, st) {
136-
// print("解析失败: $e\n$st");
137-
}
98+
},
99+
cancelOnError: true,
100+
);
101+
}).catchError((e, st) {
102+
if (!streamController.isClosed) {
103+
streamController.add(ChatEvent.error('Failed to send request: $e'));
104+
streamController.close();
138105
}
139-
} on DioException catch(e) {
140-
print("Dio Error at [${stopwatch.elapsedMilliseconds}ms]: $e");
141-
// 重新抛出异常,让上层能捕获
142-
rethrow;
143-
} finally {
144-
stopwatch.stop();
145-
print(" T_End: [${stopwatch.elapsedMilliseconds}ms] streamChat 方法执行完毕。");
146-
}
106+
});
107+
108+
return streamController.stream;
147109
}
148110

111+
112+
/// Cho phép cập nhật header mặc định (Cookie/Authorization...) nếu muốn
113+
// void updateDefaultHeaders(Map<String, String> headers) {
114+
// dio.options.headers.addAll(headers);
115+
// }
116+
//
117+
// Stream<ChatEvent> streamChat({
118+
// required String prompt,
119+
// required String sessionId,
120+
// required String userId,
121+
// List<String>? files,
122+
// String? searchTime,
123+
// String? searchType,
124+
// Map<String, String>? headers, // header
125+
// }) async* {
126+
// // 1. 创建并启动计时器
127+
// final stopwatch = Stopwatch()..start();
128+
// bool firstLineReceived = false;
129+
// bool firstDataEventYielded = false;
130+
//
131+
// print(" T0: [${stopwatch.elapsedMilliseconds}ms] 开始执行 streamChat 方法...");
132+
//
133+
// final params = <String, dynamic>{
134+
// 'prompt': prompt,
135+
// 'sessionId': sessionId,
136+
// 'userId': userId,
137+
// if (files != null) 'files': files,
138+
// if (searchTime != null) 'searchTime': searchTime,
139+
// if (searchType != null) 'searchType': searchType,
140+
// };
141+
//
142+
// try {
143+
// final rs = await dio.get(
144+
// '/ai-api/ai/stream',
145+
// queryParameters: params,
146+
// options: Options(
147+
// responseType: ResponseType.stream,
148+
// headers: {
149+
// 'Accept': 'text/event-stream',
150+
// ...?headers,
151+
// },
152+
// ),
153+
// );
154+
//
155+
//
156+
// final lines = rs.data.stream
157+
// .cast<List<int>>()
158+
// .transform(utf8.decoder)
159+
// .transform(const LineSplitter());
160+
//
161+
// await for (final line in lines) {
162+
// // 3. 记录收到第一行数据的时间点
163+
// if (!firstLineReceived) {
164+
// firstLineReceived = true;
165+
// }
166+
//
167+
// if (!line.startsWith('data:')) {
168+
// continue;
169+
// }
170+
//
171+
// final payload = line.substring(5).trimLeft();
172+
// if (payload.isEmpty) continue;
173+
// if (payload == '[DONE]') break;
174+
//
175+
// try {
176+
// final map = jsonDecode(payload);
177+
//
178+
// // 4. 记录解析并准备推送第一个有效事件的时间点
179+
// if (!firstDataEventYielded) {
180+
// // 确保这是一个有内容的事件,而不是空的 keep-alive 包
181+
// if (map.keys.any((k) => ['token', 'sources', 'question', 'trace_id', 'error'].contains(k))) {
182+
// firstDataEventYielded = true;
183+
// }
184+
// }
185+
//
186+
// // --- 原有逻辑 ---
187+
// if (map['token'] != null) yield ChatEvent.token(map['token']);
188+
// if (map['sources'] != null) {
189+
// final list = (map['sources'] as List)
190+
// .map((e) => Source.fromJson(e as Map<String, dynamic>))
191+
// .toList();
192+
// yield ChatEvent.source(list);
193+
// }
194+
// if (map['question'] != null) {
195+
// yield ChatEvent.followup(map['question'].toString());
196+
// }
197+
// if (map['trace_id'] != null) {
198+
// yield ChatEvent.traceId(map['trace_id'].toString());
199+
// }
200+
// if (map['error'] != null) {
201+
// yield ChatEvent.error(map['error'].toString());
202+
// }
203+
// } catch (e, st) {
204+
// // print("解析失败: $e\n$st");
205+
// }
206+
// }
207+
// } on DioException catch(e) {
208+
// print("Dio Error at [${stopwatch.elapsedMilliseconds}ms]: $e");
209+
// // 重新抛出异常,让上层能捕获
210+
// rethrow;
211+
// } finally {
212+
// stopwatch.stop();
213+
// print(" T_End: [${stopwatch.elapsedMilliseconds}ms] streamChat 方法执行完毕。");
214+
// }
215+
// }
216+
217+
149218
/// ============== 1b. Fetch full Answer 链接token==============
150219
/// Return fullText (token) + rawSse (所有 SSE )
151220
Future<({String fullText, String rawSse})> fetchFullAnswer({

0 commit comments

Comments
 (0)