Skip to content

Commit 4b54b87

Browse files
authored
Merge branch 'Chevey339:master' into feat/granular-restore
2 parents d91df23 + 64b964b commit 4b54b87

File tree

12 files changed

+2640
-259
lines changed

12 files changed

+2640
-259
lines changed

lib/features/chat/widgets/chat_message_widget.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2950,7 +2950,7 @@ class _ReasoningSectionState extends State<_ReasoningSection> with SingleTickerP
29502950
}
29512951

29522952
// Enable long-press text selection in reasoning body
2953-
// body = SelectionArea(child: body);
2953+
body = SelectionArea(child: body);
29542954

29552955

29562956
return AnimatedSize(
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import 'dart:convert';
2+
3+
class RequestLogEntry {
4+
RequestLogEntry({
5+
required this.id,
6+
required this.sequence,
7+
this.startedAt,
8+
this.lastEventAt,
9+
this.method,
10+
this.rawUrl,
11+
this.uri,
12+
this.requestHeaders,
13+
this.requestBody,
14+
this.statusCode,
15+
this.responseHeaders,
16+
this.responseBody,
17+
List<String>? errors,
18+
List<String>? warnings,
19+
}) : errors = errors ?? <String>[],
20+
warnings = warnings ?? <String>[];
21+
22+
final int id;
23+
// Monotonic sequence to disambiguate duplicate ids across app restarts.
24+
final int sequence;
25+
26+
DateTime? startedAt;
27+
DateTime? lastEventAt;
28+
29+
String? method;
30+
String? rawUrl;
31+
Uri? uri;
32+
33+
Map<String, dynamic>? requestHeaders;
34+
String? requestBody;
35+
36+
int? statusCode;
37+
Map<String, dynamic>? responseHeaders;
38+
String? responseBody;
39+
40+
final List<String> errors;
41+
final List<String> warnings;
42+
43+
bool get hasError =>
44+
errors.isNotEmpty || (statusCode != null && statusCode! >= 400);
45+
bool get hasWarning =>
46+
warnings.isNotEmpty ||
47+
(statusCode != null && statusCode! >= 300 && statusCode! < 400);
48+
49+
Duration? get duration {
50+
final s = startedAt;
51+
final e = lastEventAt;
52+
if (s == null || e == null) return null;
53+
return e.difference(s);
54+
}
55+
}
56+
57+
class RequestLogParser {
58+
static final RegExp _tsRe = RegExp(
59+
r'^\[(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{3})\]\s+(.*)$',
60+
);
61+
62+
static final RegExp _reqStartRe = RegExp(
63+
r'^\[REQ (\d+)\]\s+([A-Z]+)\s+(.*)$',
64+
dotAll: true,
65+
);
66+
static final RegExp _reqHeadersRe = RegExp(
67+
r'^\[REQ (\d+)\]\s+headers=(.*)$',
68+
dotAll: true,
69+
);
70+
static final RegExp _reqBodyRe = RegExp(
71+
r'^\[REQ (\d+)\]\s+body=(.*)$',
72+
dotAll: true,
73+
);
74+
75+
static final RegExp _resStatusRe = RegExp(
76+
r'^\[RES (\d+)\]\s+status=(\d+)\s*$',
77+
dotAll: true,
78+
);
79+
static final RegExp _resHeadersRe = RegExp(
80+
r'^\[RES (\d+)\]\s+headers=(.*)$',
81+
dotAll: true,
82+
);
83+
static final RegExp _resChunkRe = RegExp(
84+
r'^\[RES (\d+)\]\s+chunk=(.*)$',
85+
dotAll: true,
86+
);
87+
static final RegExp _resDoneRe = RegExp(
88+
r'^\[RES (\d+)\]\s+done\s*$',
89+
dotAll: true,
90+
);
91+
static final RegExp _resErrRe = RegExp(
92+
r'^\[RES (\d+)\]\s+error=(.*)$',
93+
dotAll: true,
94+
);
95+
static final RegExp _resDioErrRe = RegExp(
96+
r'^\[RES (\d+)\]\s+dio_error=(.*)$',
97+
dotAll: true,
98+
);
99+
100+
static List<RequestLogEntry> parse(String content) {
101+
final records = _toRecords(content);
102+
103+
final List<RequestLogEntry> entries = <RequestLogEntry>[];
104+
final Map<int, int> currentIndexById = <int, int>{};
105+
int seq = 0;
106+
107+
RequestLogEntry ensureEntry(int id) {
108+
final idx = currentIndexById[id];
109+
if (idx != null) return entries[idx];
110+
final e = RequestLogEntry(id: id, sequence: ++seq);
111+
entries.add(e);
112+
currentIndexById[id] = entries.length - 1;
113+
return e;
114+
}
115+
116+
void touch(RequestLogEntry e, DateTime ts) {
117+
e.lastEventAt = ts;
118+
e.startedAt ??= ts;
119+
}
120+
121+
for (final record in records) {
122+
final ts = record.ts;
123+
final msg = record.message;
124+
125+
final mStart = _reqStartRe.firstMatch(msg);
126+
if (mStart != null) {
127+
final id = int.tryParse(mStart.group(1) ?? '');
128+
if (id == null) continue;
129+
130+
final e = RequestLogEntry(id: id, sequence: ++seq);
131+
e.startedAt = ts;
132+
e.lastEventAt = ts;
133+
e.method = (mStart.group(2) ?? '').trim();
134+
final url = (mStart.group(3) ?? '').trim();
135+
e.rawUrl = url;
136+
e.uri = Uri.tryParse(url);
137+
entries.add(e);
138+
currentIndexById[id] = entries.length - 1;
139+
continue;
140+
}
141+
142+
final mReqHeaders = _reqHeadersRe.firstMatch(msg);
143+
if (mReqHeaders != null) {
144+
final id = int.tryParse(mReqHeaders.group(1) ?? '');
145+
if (id == null) continue;
146+
final e = ensureEntry(id);
147+
touch(e, ts);
148+
final jsonText = (mReqHeaders.group(2) ?? '').trim();
149+
e.requestHeaders = _decodeJsonMap(jsonText);
150+
if (e.requestHeaders == null && jsonText.isNotEmpty) {
151+
e.warnings.add('Failed to parse request headers JSON');
152+
}
153+
continue;
154+
}
155+
156+
final mReqBody = _reqBodyRe.firstMatch(msg);
157+
if (mReqBody != null) {
158+
final id = int.tryParse(mReqBody.group(1) ?? '');
159+
if (id == null) continue;
160+
final e = ensureEntry(id);
161+
touch(e, ts);
162+
e.requestBody = unescape((mReqBody.group(2) ?? '').trim());
163+
continue;
164+
}
165+
166+
final mStatus = _resStatusRe.firstMatch(msg);
167+
if (mStatus != null) {
168+
final id = int.tryParse(mStatus.group(1) ?? '');
169+
final code = int.tryParse(mStatus.group(2) ?? '');
170+
if (id == null) continue;
171+
final e = ensureEntry(id);
172+
touch(e, ts);
173+
e.statusCode = code;
174+
continue;
175+
}
176+
177+
final mResHeaders = _resHeadersRe.firstMatch(msg);
178+
if (mResHeaders != null) {
179+
final id = int.tryParse(mResHeaders.group(1) ?? '');
180+
if (id == null) continue;
181+
final e = ensureEntry(id);
182+
touch(e, ts);
183+
final jsonText = (mResHeaders.group(2) ?? '').trim();
184+
e.responseHeaders = _decodeJsonMap(jsonText);
185+
if (e.responseHeaders == null && jsonText.isNotEmpty) {
186+
e.warnings.add('Failed to parse response headers JSON');
187+
}
188+
continue;
189+
}
190+
191+
final mChunk = _resChunkRe.firstMatch(msg);
192+
if (mChunk != null) {
193+
final id = int.tryParse(mChunk.group(1) ?? '');
194+
if (id == null) continue;
195+
final e = ensureEntry(id);
196+
touch(e, ts);
197+
final chunk = unescape(mChunk.group(2) ?? '');
198+
final prev = e.responseBody ?? '';
199+
e.responseBody = prev + chunk;
200+
continue;
201+
}
202+
203+
final mDone = _resDoneRe.firstMatch(msg);
204+
if (mDone != null) {
205+
final id = int.tryParse(mDone.group(1) ?? '');
206+
if (id == null) continue;
207+
final e = ensureEntry(id);
208+
touch(e, ts);
209+
continue;
210+
}
211+
212+
final mErr = _resErrRe.firstMatch(msg);
213+
if (mErr != null) {
214+
final id = int.tryParse(mErr.group(1) ?? '');
215+
if (id == null) continue;
216+
final e = ensureEntry(id);
217+
touch(e, ts);
218+
final err = unescape((mErr.group(2) ?? '').trim());
219+
if (err.isNotEmpty) e.errors.add(err);
220+
continue;
221+
}
222+
223+
final mDioErr = _resDioErrRe.firstMatch(msg);
224+
if (mDioErr != null) {
225+
final id = int.tryParse(mDioErr.group(1) ?? '');
226+
if (id == null) continue;
227+
final e = ensureEntry(id);
228+
touch(e, ts);
229+
final err = unescape((mDioErr.group(2) ?? '').trim());
230+
if (err.isNotEmpty) e.errors.add(err);
231+
continue;
232+
}
233+
}
234+
235+
// Newest first (when possible)
236+
entries.sort((a, b) {
237+
final at = a.startedAt ?? a.lastEventAt;
238+
final bt = b.startedAt ?? b.lastEventAt;
239+
if (at == null && bt == null) return b.sequence.compareTo(a.sequence);
240+
if (at == null) return 1;
241+
if (bt == null) return -1;
242+
final c = bt.compareTo(at);
243+
if (c != 0) return c;
244+
return b.sequence.compareTo(a.sequence);
245+
});
246+
247+
return entries;
248+
}
249+
250+
static List<_LogRecord> _toRecords(String content) {
251+
final List<_LogRecord> out = <_LogRecord>[];
252+
final lines = content.split('\n');
253+
for (final rawLine in lines) {
254+
final line = rawLine.trimRight();
255+
if (line.isEmpty && out.isEmpty) continue;
256+
257+
final m = _tsRe.firstMatch(line);
258+
if (m != null) {
259+
final ts = _parseTs(m);
260+
final msg = m.group(8) ?? '';
261+
out.add(_LogRecord(ts: ts, message: msg));
262+
continue;
263+
}
264+
265+
if (out.isEmpty) continue;
266+
out.last.message += '\n$line';
267+
}
268+
return out;
269+
}
270+
271+
static DateTime _parseTs(RegExpMatch m) {
272+
int g(int i) => int.tryParse(m.group(i) ?? '') ?? 0;
273+
return DateTime(g(1), g(2), g(3), g(4), g(5), g(6), g(7));
274+
}
275+
276+
static Map<String, dynamic>? _decodeJsonMap(String text) {
277+
try {
278+
final v = jsonDecode(text);
279+
if (v is Map<String, dynamic>) return v;
280+
if (v is Map) {
281+
return v.map((k, val) => MapEntry(k.toString(), val));
282+
}
283+
return null;
284+
} catch (_) {
285+
return null;
286+
}
287+
}
288+
289+
/// Reverses `RequestLogger.escape()` (handles `\\`, `\\r`, `\\n`, `\\t`).
290+
static String unescape(String input) {
291+
if (input.isEmpty) return input;
292+
final sb = StringBuffer();
293+
for (int i = 0; i < input.length; i++) {
294+
final ch = input[i];
295+
if (ch == '\\' && i + 1 < input.length) {
296+
final next = input[i + 1];
297+
switch (next) {
298+
case 'n':
299+
sb.write('\n');
300+
i++;
301+
continue;
302+
case 'r':
303+
sb.write('\r');
304+
i++;
305+
continue;
306+
case 't':
307+
sb.write('\t');
308+
i++;
309+
continue;
310+
case '\\':
311+
sb.write('\\');
312+
i++;
313+
continue;
314+
default:
315+
// Preserve unknown escape as-is.
316+
sb.write('\\');
317+
continue;
318+
}
319+
}
320+
sb.write(ch);
321+
}
322+
return sb.toString();
323+
}
324+
}
325+
326+
class _LogRecord {
327+
_LogRecord({required this.ts, required this.message});
328+
final DateTime ts;
329+
String message;
330+
}

0 commit comments

Comments
 (0)