Files
eryao/apps/lib/features/divination/data/services/divination_run_service.dart
T

148 lines
4.6 KiB
Dart

import 'dart:math';
import '../../../../core/logging/logger.dart';
import '../../../../core/network/api_problem.dart';
import '../apis/divination_api.dart';
import '../models/divination_backend_models.dart';
import '../models/divination_params.dart';
class DivinationRunService {
const DivinationRunService({required DivinationApi api}) : _api = api;
final DivinationApi _api;
static final Logger _logger = getLogger('features.divination.run_service');
Future<DivinationRunAggregate> run({
required DivinationParams params,
required List<YaoType> yaoStates,
void Function()? onDerived,
void Function()? onTextMessageEnd,
}) async {
final threadId = _uuidV4();
final runId = 'run_${DateTime.now().millisecondsSinceEpoch}';
await _api.enqueueRun(
params: params,
yaoStates: yaoStates,
threadId: threadId,
runId: runId,
);
DerivedDivinationData? derived;
String signLevel = '';
String summary = '';
List<String> conclusion = const <String>[];
List<String> focusPoints = const <String>[];
List<String> advice = const <String>[];
List<String> keywords = const <String>[];
String answer = '';
await for (final event in _api.streamEvents(
threadId: threadId,
runId: runId,
)) {
final type = event['type'] as String? ?? '';
_logger.debug(
message: 'Received run SSE event',
extra: <String, dynamic>{
'threadId': threadId,
'runId': runId,
'type': type,
},
);
if (type == 'DIVINATION_DERIVED') {
final payload = event['divination'];
if (payload is! Map<String, dynamic>) {
throw const FormatException('DIVINATION_DERIVED 缺少 divination 结构');
}
derived = DerivedDivinationData.fromJson(payload);
onDerived?.call();
continue;
}
if (type == 'TEXT_MESSAGE_END') {
signLevel = _requiredString(event, 'sign_level');
summary = _requiredString(event, 'summary');
conclusion = _requiredStringList(event, 'conclusion');
focusPoints = _requiredStringList(event, 'focus_points');
advice = _requiredStringList(event, 'advice');
keywords = _requiredStringList(event, 'keywords');
answer = _requiredString(event, 'answer');
onTextMessageEnd?.call();
continue;
}
if (type == 'RUN_ERROR') {
_logger.warning(
message: 'Run ended with RUN_ERROR event',
extra: <String, dynamic>{
'threadId': threadId,
'runId': runId,
'code': event['code'],
'detail': event['detail'],
'message': event['message'],
},
);
throw ApiProblem(
status: 500,
title: 'Run failed',
detail: event['detail'] as String? ?? '解卦失败',
code: event['code'] as String?,
);
}
if (type == 'RUN_FINISHED') {
break;
}
}
if (derived == null) {
_logger.warning(
message: 'Missing DIVINATION_DERIVED before RUN_FINISHED',
extra: <String, dynamic>{'threadId': threadId, 'runId': runId},
);
throw const FormatException('未收到 DIVINATION_DERIVED 事件');
}
return DivinationRunAggregate(
derived: derived,
signLevel: signLevel,
summary: summary,
conclusion: conclusion,
focusPoints: focusPoints,
advice: advice,
keywords: keywords,
answer: answer,
);
}
String _requiredString(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is! String || value.isEmpty) {
throw FormatException('缺少字段: $key');
}
return value;
}
List<String> _requiredStringList(Map<String, dynamic> json, String key) {
final raw = json[key];
if (raw is! List<dynamic>) {
throw FormatException('缺少列表字段: $key');
}
return raw
.map((item) {
if (item is! String || item.isEmpty) {
throw FormatException('字段 $key 存在非法项');
}
return item;
})
.toList(growable: false);
}
String _uuidV4() {
final random = Random.secure();
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
String toHex(int b) => b.toRadixString(16).padLeft(2, '0');
final b = bytes.map(toHex).toList(growable: false);
return '${b[0]}${b[1]}${b[2]}${b[3]}-${b[4]}${b[5]}-${b[6]}${b[7]}-${b[8]}${b[9]}-${b[10]}${b[11]}${b[12]}${b[13]}${b[14]}${b[15]}';
}
}