feat: 实现用户画像、占卜历史与后端用户管理模块
This commit is contained in:
@@ -8,6 +8,7 @@ import '../../../../core/network/api_problem.dart';
|
||||
import '../../../../data/network/api_client.dart';
|
||||
import '../models/divination_backend_models.dart';
|
||||
import '../models/divination_params.dart';
|
||||
import '../models/divination_result.dart';
|
||||
|
||||
class DivinationApi {
|
||||
const DivinationApi({required ApiClient apiClient}) : _apiClient = apiClient;
|
||||
@@ -37,6 +38,67 @@ class DivinationApi {
|
||||
return RunAcceptedData.fromJson(json);
|
||||
}
|
||||
|
||||
Future<List<DivinationResultData>> getHistoryRecords({
|
||||
required String userId,
|
||||
}) async {
|
||||
final json = await _apiClient.getJson('/api/v1/agent/history');
|
||||
final messagesRaw = json['messages'];
|
||||
if (messagesRaw is! List<dynamic>) {
|
||||
return const <DivinationResultData>[];
|
||||
}
|
||||
|
||||
final records = <DivinationResultData>[];
|
||||
for (final raw in messagesRaw) {
|
||||
if (raw is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
if (raw['role'] != 'assistant') {
|
||||
continue;
|
||||
}
|
||||
final agentOutputRaw = raw['agent_output'];
|
||||
if (agentOutputRaw is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
final derivedRaw = agentOutputRaw['divination_derived'];
|
||||
if (derivedRaw is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final derived = DerivedDivinationData.fromJson(derivedRaw);
|
||||
final divinationTime = _resolveHistoryTime(raw, derived);
|
||||
final params = DivinationParams(
|
||||
method: _methodFromText(derived.divinationMethod),
|
||||
questionType: _questionTypeFromText(derived.questionType),
|
||||
question: derived.question,
|
||||
divinationTime: divinationTime,
|
||||
coinBalance: 0,
|
||||
userId: userId,
|
||||
);
|
||||
final aggregate = DivinationRunAggregate(
|
||||
derived: derived,
|
||||
signLevel: _asString(agentOutputRaw['sign_level']),
|
||||
summary: _asString(agentOutputRaw['summary']),
|
||||
conclusion: _asStringList(agentOutputRaw['conclusion']),
|
||||
focusPoints: _asStringList(agentOutputRaw['focus_points']),
|
||||
advice: _asStringList(agentOutputRaw['advice']),
|
||||
keywords: _asStringList(agentOutputRaw['keywords']),
|
||||
answer: _asString(agentOutputRaw['answer']),
|
||||
);
|
||||
records.add(aggregate.toViewData(params));
|
||||
} catch (error, stackTrace) {
|
||||
_logger.warning(
|
||||
message: 'Skip malformed history assistant message',
|
||||
extra: <String, dynamic>{
|
||||
'error': error.toString(),
|
||||
'stackTrace': stackTrace.toString(),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
Stream<Map<String, dynamic>> streamEvents({
|
||||
required String threadId,
|
||||
required String runId,
|
||||
@@ -217,6 +279,55 @@ String _questionTypeToText(QuestionType type) {
|
||||
};
|
||||
}
|
||||
|
||||
QuestionType _questionTypeFromText(String raw) {
|
||||
return switch (raw) {
|
||||
'事业' => QuestionType.career,
|
||||
'情感' => QuestionType.love,
|
||||
'财富' => QuestionType.wealth,
|
||||
'运势' => QuestionType.fortune,
|
||||
'解梦' => QuestionType.dream,
|
||||
'健康' => QuestionType.health,
|
||||
'学业' => QuestionType.study,
|
||||
'寻物' => QuestionType.search,
|
||||
_ => QuestionType.other,
|
||||
};
|
||||
}
|
||||
|
||||
DivinationMethod _methodFromText(String raw) {
|
||||
return raw == '自动起卦' ? DivinationMethod.auto : DivinationMethod.manual;
|
||||
}
|
||||
|
||||
DateTime _resolveHistoryTime(
|
||||
Map<String, dynamic> message,
|
||||
DerivedDivinationData derived,
|
||||
) {
|
||||
final timestamp = message['timestamp'];
|
||||
if (timestamp is String) {
|
||||
final parsed = DateTime.tryParse(timestamp);
|
||||
if (parsed != null) {
|
||||
return parsed.toLocal();
|
||||
}
|
||||
}
|
||||
|
||||
final derivedTime = DateTime.tryParse(derived.divinationTime);
|
||||
if (derivedTime != null) {
|
||||
return derivedTime.toLocal();
|
||||
}
|
||||
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
String _asString(Object? value) {
|
||||
return value is String ? value : '';
|
||||
}
|
||||
|
||||
List<String> _asStringList(Object? value) {
|
||||
if (value is! List<dynamic>) {
|
||||
return const <String>[];
|
||||
}
|
||||
return value.whereType<String>().toList(growable: false);
|
||||
}
|
||||
|
||||
String _yaoTypeToText(YaoType type) {
|
||||
return switch (type) {
|
||||
YaoType.youngYang => '少阳',
|
||||
|
||||
@@ -60,6 +60,21 @@ class DivinationParams {
|
||||
};
|
||||
}
|
||||
|
||||
factory DivinationParams.fromPayload(Map<String, dynamic> payload) {
|
||||
return DivinationParams(
|
||||
method: divinationMethodFromName(_requiredString(payload, 'method')),
|
||||
questionType: questionTypeFromName(
|
||||
_requiredString(payload, 'questionType'),
|
||||
),
|
||||
question: _requiredString(payload, 'question'),
|
||||
divinationTime: DateTime.parse(
|
||||
_requiredString(payload, 'divinationTime'),
|
||||
),
|
||||
coinBalance: _requiredInt(payload, 'coinBalance'),
|
||||
userId: _requiredString(payload, 'userId'),
|
||||
);
|
||||
}
|
||||
|
||||
String toBinary(List<YaoType> yaoStates) {
|
||||
return yaoStates
|
||||
.map(
|
||||
@@ -85,3 +100,43 @@ class DivinationParams {
|
||||
.join();
|
||||
}
|
||||
}
|
||||
|
||||
DivinationMethod divinationMethodFromName(String raw) {
|
||||
return DivinationMethod.values.firstWhere(
|
||||
(value) => value.name == raw,
|
||||
orElse: () => DivinationMethod.manual,
|
||||
);
|
||||
}
|
||||
|
||||
QuestionType questionTypeFromName(String raw) {
|
||||
return QuestionType.values.firstWhere(
|
||||
(value) => value.name == raw,
|
||||
orElse: () => QuestionType.other,
|
||||
);
|
||||
}
|
||||
|
||||
YaoType yaoTypeFromName(String raw) {
|
||||
return YaoType.values.firstWhere(
|
||||
(value) => value.name == raw,
|
||||
orElse: () => YaoType.undetermined,
|
||||
);
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is! String || value.isEmpty) {
|
||||
throw FormatException('Missing required string: $key');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int _requiredInt(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
throw FormatException('Missing required int: $key');
|
||||
}
|
||||
|
||||
@@ -38,6 +38,79 @@ class DivinationResultData {
|
||||
final List<YaoLineData> targetYaoLines;
|
||||
|
||||
bool get hasChangingYao => binaryCode != changedBinaryCode;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'params': params.toPayload(),
|
||||
'binaryCode': binaryCode,
|
||||
'changedBinaryCode': changedBinaryCode,
|
||||
'guaName': guaName,
|
||||
'targetGuaName': targetGuaName,
|
||||
'upperName': upperName,
|
||||
'lowerName': lowerName,
|
||||
'signType': signType,
|
||||
'keywords': keywords,
|
||||
'conclusion': conclusion,
|
||||
'analysis': analysis,
|
||||
'suggestion': suggestion,
|
||||
'ganzhi': ganzhi.toJson(),
|
||||
'wuXingStatus': wuXingStatus,
|
||||
'yaoLines': yaoLines.map((line) => line.toJson()).toList(growable: false),
|
||||
'targetYaoLines': targetYaoLines
|
||||
.map((line) => line.toJson())
|
||||
.toList(growable: false),
|
||||
};
|
||||
}
|
||||
|
||||
factory DivinationResultData.fromJson(Map<String, dynamic> json) {
|
||||
final paramsRaw = json['params'];
|
||||
final ganzhiRaw = json['ganzhi'];
|
||||
final wuXingRaw = json['wuXingStatus'];
|
||||
final yaoLinesRaw = json['yaoLines'];
|
||||
final targetYaoLinesRaw = json['targetYaoLines'];
|
||||
if (paramsRaw is! Map<String, dynamic> ||
|
||||
ganzhiRaw is! Map<String, dynamic> ||
|
||||
wuXingRaw is! Map<String, dynamic> ||
|
||||
yaoLinesRaw is! List<dynamic> ||
|
||||
targetYaoLinesRaw is! List<dynamic>) {
|
||||
throw const FormatException('Invalid divination result payload');
|
||||
}
|
||||
|
||||
return DivinationResultData(
|
||||
params: DivinationParams.fromPayload(paramsRaw),
|
||||
binaryCode: _requiredString(json, 'binaryCode'),
|
||||
changedBinaryCode: _requiredString(json, 'changedBinaryCode'),
|
||||
guaName: _requiredString(json, 'guaName'),
|
||||
targetGuaName: _requiredString(json, 'targetGuaName'),
|
||||
upperName: _requiredString(json, 'upperName'),
|
||||
lowerName: _requiredString(json, 'lowerName'),
|
||||
signType: _requiredString(json, 'signType'),
|
||||
keywords: _requiredString(json, 'keywords'),
|
||||
conclusion: _requiredString(json, 'conclusion'),
|
||||
analysis: _requiredString(json, 'analysis'),
|
||||
suggestion: _requiredString(json, 'suggestion'),
|
||||
ganzhi: GanzhiData.fromJson(ganzhiRaw),
|
||||
wuXingStatus: wuXingRaw.map(
|
||||
(key, value) => MapEntry(key, value.toString()),
|
||||
),
|
||||
yaoLines: yaoLinesRaw
|
||||
.map((raw) {
|
||||
if (raw is! Map<String, dynamic>) {
|
||||
throw const FormatException('Invalid yao line payload');
|
||||
}
|
||||
return YaoLineData.fromJson(raw);
|
||||
})
|
||||
.toList(growable: false),
|
||||
targetYaoLines: targetYaoLinesRaw
|
||||
.map((raw) {
|
||||
if (raw is! Map<String, dynamic>) {
|
||||
throw const FormatException('Invalid target yao line payload');
|
||||
}
|
||||
return YaoLineData.fromJson(raw);
|
||||
})
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GanzhiData {
|
||||
@@ -68,6 +141,40 @@ class GanzhiData {
|
||||
final String riChen;
|
||||
final String yuePo;
|
||||
final String riChong;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'yearGanZhi': yearGanZhi,
|
||||
'monthGanZhi': monthGanZhi,
|
||||
'dayGanZhi': dayGanZhi,
|
||||
'timeGanZhi': timeGanZhi,
|
||||
'yearKongWang': yearKongWang,
|
||||
'monthKongWang': monthKongWang,
|
||||
'dayKongWang': dayKongWang,
|
||||
'timeKongWang': timeKongWang,
|
||||
'yueJian': yueJian,
|
||||
'riChen': riChen,
|
||||
'yuePo': yuePo,
|
||||
'riChong': riChong,
|
||||
};
|
||||
}
|
||||
|
||||
factory GanzhiData.fromJson(Map<String, dynamic> json) {
|
||||
return GanzhiData(
|
||||
yearGanZhi: _requiredString(json, 'yearGanZhi'),
|
||||
monthGanZhi: _requiredString(json, 'monthGanZhi'),
|
||||
dayGanZhi: _requiredString(json, 'dayGanZhi'),
|
||||
timeGanZhi: _requiredString(json, 'timeGanZhi'),
|
||||
yearKongWang: _requiredString(json, 'yearKongWang'),
|
||||
monthKongWang: _requiredString(json, 'monthKongWang'),
|
||||
dayKongWang: _requiredString(json, 'dayKongWang'),
|
||||
timeKongWang: _requiredString(json, 'timeKongWang'),
|
||||
yueJian: _requiredString(json, 'yueJian'),
|
||||
riChen: _requiredString(json, 'riChen'),
|
||||
yuePo: _requiredString(json, 'yuePo'),
|
||||
riChong: _requiredString(json, 'riChong'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class YaoLineData {
|
||||
@@ -88,4 +195,47 @@ class YaoLineData {
|
||||
final String element;
|
||||
final YaoType type;
|
||||
final String mark;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'index': index,
|
||||
'spirit': spirit,
|
||||
'relation': relation,
|
||||
'branch': branch,
|
||||
'element': element,
|
||||
'type': type.name,
|
||||
'mark': mark,
|
||||
};
|
||||
}
|
||||
|
||||
factory YaoLineData.fromJson(Map<String, dynamic> json) {
|
||||
return YaoLineData(
|
||||
index: _requiredInt(json, 'index'),
|
||||
spirit: _requiredString(json, 'spirit'),
|
||||
relation: _requiredString(json, 'relation'),
|
||||
branch: _requiredString(json, 'branch'),
|
||||
element: _requiredString(json, 'element'),
|
||||
type: yaoTypeFromName(_requiredString(json, 'type')),
|
||||
mark: _requiredString(json, 'mark'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is! String || value.isEmpty) {
|
||||
throw FormatException('Missing required string: $key');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int _requiredInt(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
throw FormatException('Missing required int: $key');
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ class DivinationRunService {
|
||||
final DivinationApi _api;
|
||||
static final Logger _logger = getLogger('features.divination.run_service');
|
||||
|
||||
Future<PointsBalanceData> getPointsBalance() {
|
||||
return _api.getPointsBalance();
|
||||
}
|
||||
|
||||
Future<DivinationRunAggregate> run({
|
||||
required DivinationParams params,
|
||||
required List<YaoType> yaoStates,
|
||||
|
||||
Reference in New Issue
Block a user