2026-04-03 19:04:46 +08:00
|
|
|
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';
|
2026-04-29 14:26:15 +08:00
|
|
|
import '../models/divination_result.dart';
|
2026-04-03 19:04:46 +08:00
|
|
|
|
|
|
|
|
class DivinationRunService {
|
|
|
|
|
const DivinationRunService({required DivinationApi api}) : _api = api;
|
|
|
|
|
|
|
|
|
|
final DivinationApi _api;
|
|
|
|
|
static final Logger _logger = getLogger('features.divination.run_service');
|
|
|
|
|
|
2026-04-06 01:28:10 +08:00
|
|
|
Future<PointsBalanceData> getPointsBalance() {
|
|
|
|
|
return _api.getPointsBalance();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:04:46 +08:00
|
|
|
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}';
|
2026-04-08 17:23:02 +08:00
|
|
|
final accepted = await _api.enqueueRun(
|
2026-04-03 19:04:46 +08:00
|
|
|
params: params,
|
|
|
|
|
yaoStates: yaoStates,
|
|
|
|
|
threadId: threadId,
|
|
|
|
|
runId: runId,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
DerivedDivinationData? derived;
|
|
|
|
|
String signLevel = '';
|
|
|
|
|
List<String> conclusion = const <String>[];
|
|
|
|
|
List<String> focusPoints = const <String>[];
|
|
|
|
|
List<String> advice = const <String>[];
|
|
|
|
|
List<String> keywords = const <String>[];
|
|
|
|
|
String answer = '';
|
2026-04-29 14:26:15 +08:00
|
|
|
DivinationRunStatus status = DivinationRunStatus.success;
|
2026-04-03 19:04:46 +08:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
conclusion = _requiredStringList(event, 'conclusion');
|
|
|
|
|
focusPoints = _requiredStringList(event, 'focus_points');
|
|
|
|
|
advice = _requiredStringList(event, 'advice');
|
|
|
|
|
keywords = _requiredStringList(event, 'keywords');
|
|
|
|
|
answer = _requiredString(event, 'answer');
|
2026-04-29 14:26:15 +08:00
|
|
|
status = _parseStatus(event['status']);
|
2026-04-03 19:04:46 +08:00
|
|
|
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(
|
2026-04-08 17:23:02 +08:00
|
|
|
threadId: accepted.threadId,
|
2026-04-03 19:04:46 +08:00
|
|
|
derived: derived,
|
|
|
|
|
signLevel: signLevel,
|
|
|
|
|
conclusion: conclusion,
|
|
|
|
|
focusPoints: focusPoints,
|
|
|
|
|
advice: advice,
|
|
|
|
|
keywords: keywords,
|
|
|
|
|
answer: answer,
|
2026-04-29 14:26:15 +08:00
|
|
|
status: status,
|
2026-04-03 19:04:46 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 14:26:15 +08:00
|
|
|
DivinationRunStatus _parseStatus(Object? value) {
|
|
|
|
|
if (value is! String) {
|
|
|
|
|
return DivinationRunStatus.success;
|
|
|
|
|
}
|
|
|
|
|
return switch (value) {
|
|
|
|
|
'success' => DivinationRunStatus.success,
|
|
|
|
|
'failed' => DivinationRunStatus.failed,
|
|
|
|
|
'refused' => DivinationRunStatus.refused,
|
|
|
|
|
_ => DivinationRunStatus.success,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 19:04:46 +08:00
|
|
|
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]}';
|
|
|
|
|
}
|
|
|
|
|
}
|