feat: 新增追问模式和iOS本地化,重构后端输出模型

This commit is contained in:
qzl
2026-04-29 14:26:15 +08:00
parent f497afbff2
commit 16cb47e75a
39 changed files with 1346 additions and 600 deletions
@@ -93,6 +93,7 @@ class DivinationApi {
advice: _asStringList(agentOutputRaw['advice']),
keywords: _asStringList(agentOutputRaw['keywords']),
answer: _asString(agentOutputRaw['answer']),
status: _parseStatus(agentOutputRaw['status']),
);
records.add(aggregate.toViewData(params));
} catch (error, stackTrace) {
@@ -472,12 +473,24 @@ String _asString(Object? value) {
return value is String ? value : '';
}
List<String> _asStringList(Object? value) {
if (value is! List<dynamic>) {
return const <String>[];
List<String> _asStringList(Object? value) {
if (value is! List<dynamic>) {
return const <String>[];
}
return value.whereType<String>().toList(growable: false);
}
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,
};
}
return value.whereType<String>().toList(growable: false);
}
String _yaoTypeToText(YaoType type) {
return switch (type) {
@@ -60,6 +60,7 @@ class DivinationRunAggregate {
required this.advice,
required this.keywords,
required this.answer,
this.status = DivinationRunStatus.success,
});
final DerivedDivinationData derived;
@@ -70,6 +71,7 @@ class DivinationRunAggregate {
final List<String> advice;
final List<String> keywords;
final String answer;
final DivinationRunStatus status;
DivinationResultData toViewData(DivinationParams params) {
return DivinationResultData(
@@ -84,9 +86,9 @@ class DivinationRunAggregate {
signType: signLevel,
keywords: keywords.join(''),
focusPoints: focusPoints,
conclusion: _asBullet(conclusion),
conclusion: conclusion.join('\n'),
analysis: answer,
suggestion: _asBullet(advice),
suggestion: advice.join('\n'),
ganzhi: GanzhiData(
yearGanZhi: derived.ganzhi.yearGanZhi,
monthGanZhi: derived.ganzhi.monthGanZhi,
@@ -108,19 +110,9 @@ class DivinationRunAggregate {
targetYaoLines: derived.targetYaoInfoList
.map((line) => line.toViewModel())
.toList(growable: false),
status: status,
);
}
String _asBullet(List<String> lines) {
if (lines.isEmpty) {
return '';
}
return List<String>.generate(
lines.length,
(i) => '${i + 1}. ${lines[i]}',
growable: false,
).join('\n');
}
}
class DerivedDivinationData {
@@ -1,5 +1,7 @@
import 'divination_params.dart';
enum DivinationRunStatus { success, failed, refused }
class DivinationResultData {
const DivinationResultData({
this.threadId,
@@ -20,6 +22,7 @@ class DivinationResultData {
required this.wuXingStatus,
required this.yaoLines,
required this.targetYaoLines,
this.status = DivinationRunStatus.success,
});
final DivinationParams params;
@@ -40,8 +43,10 @@ class DivinationResultData {
final Map<String, String> wuXingStatus;
final List<YaoLineData> yaoLines;
final List<YaoLineData> targetYaoLines;
final DivinationRunStatus status;
bool get hasChangingYao => binaryCode != changedBinaryCode;
bool get isSuccess => status == DivinationRunStatus.success;
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -65,6 +70,7 @@ class DivinationResultData {
'targetYaoLines': targetYaoLines
.map((line) => line.toJson())
.toList(growable: false),
'status': status.name,
};
}
@@ -117,8 +123,21 @@ class DivinationResultData {
return YaoLineData.fromJson(raw);
})
.toList(growable: false),
status: _parseStatus(json['status']),
);
}
static 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,
};
}
}
List<String> _requiredStringList(Map<String, dynamic> json, String key) {
@@ -5,6 +5,7 @@ import '../../../../core/network/api_problem.dart';
import '../apis/divination_api.dart';
import '../models/divination_backend_models.dart';
import '../models/divination_params.dart';
import '../models/divination_result.dart';
class DivinationRunService {
const DivinationRunService({required DivinationApi api}) : _api = api;
@@ -38,6 +39,7 @@ class DivinationRunService {
List<String> advice = const <String>[];
List<String> keywords = const <String>[];
String answer = '';
DivinationRunStatus status = DivinationRunStatus.success;
await for (final event in _api.streamEvents(
threadId: threadId,
@@ -68,6 +70,7 @@ class DivinationRunService {
advice = _requiredStringList(event, 'advice');
keywords = _requiredStringList(event, 'keywords');
answer = _requiredString(event, 'answer');
status = _parseStatus(event['status']);
onTextMessageEnd?.call();
continue;
}
@@ -111,9 +114,22 @@ class DivinationRunService {
advice: advice,
keywords: keywords,
answer: answer,
status: status,
);
}
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,
};
}
String _requiredString(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is! String || value.isEmpty) {
@@ -207,8 +207,6 @@ class _DivinationResultScreenState extends State<DivinationResultScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ResultHeader(data: widget.data),
const SizedBox(height: AppSpacing.md),
_SignCard(
key: _finalSignCardKey,
signType: widget.data.signType,
@@ -221,17 +219,17 @@ class _DivinationResultScreenState extends State<DivinationResultScreen> {
content: widget.data.conclusion,
),
const SizedBox(height: AppSpacing.md),
_FocusPointsCard(points: widget.data.focusPoints),
_AnalysisCard(
title: l10n.resultSuggestion,
content: widget.data.suggestion,
),
const SizedBox(height: AppSpacing.md),
_AnalysisCard(
title: l10n.resultAnalysis,
content: widget.data.analysis,
),
const SizedBox(height: AppSpacing.md),
_AnalysisCard(
title: l10n.resultSuggestion,
content: widget.data.suggestion,
),
_FocusPointsCard(points: widget.data.focusPoints),
const SizedBox(height: AppSpacing.md),
Container(
width: double.infinity,
@@ -270,13 +268,16 @@ class _DivinationResultScreenState extends State<DivinationResultScreen> {
),
const SizedBox(height: AppSpacing.md),
_InfoCard(data: widget.data),
const SizedBox(height: AppSpacing.xl),
Text(
l10n.resultHexagramDetail,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: AppSpacing.md),
_HexagramDetailCard(data: widget.data),
if (widget.data.isSuccess) ...[
const SizedBox(height: AppSpacing.xl),
Text(
l10n.resultHexagramDetail,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: AppSpacing.md),
_HexagramDetailCard(data: widget.data),
] else
_HexagramDetailPlaceholder(status: widget.data.status),
],
),
),
@@ -412,27 +413,6 @@ class _DivinationResultScreenState extends State<DivinationResultScreen> {
}
}
class _ResultHeader extends StatelessWidget {
const _ResultHeader({required this.data});
final DivinationResultData data;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Row(
children: [
Text(
l10n.resultAIAnalysis,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
),
],
);
}
}
class _SignCard extends StatelessWidget {
const _SignCard({super.key, required this.signType});
@@ -543,8 +523,7 @@ class _FocusPointsCard extends StatelessWidget {
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
final languageCode = Localizations.localeOf(context).languageCode;
final title = languageCode == 'en' ? 'Focus Points' : '断卦要点';
final title = l10n.resultFocusPoints;
if (points.isEmpty) {
return const SizedBox.shrink();
}
@@ -572,11 +551,7 @@ class _FocusPointsCard extends StatelessWidget {
const Spacer(),
TextButton(
onPressed: () {
final content = points
.asMap()
.entries
.map((e) => '${e.key + 1}. ${e.value}')
.join('\n');
final content = points.join('\n');
Clipboard.setData(ClipboardData(text: content));
Toast.show(
context,
@@ -589,28 +564,12 @@ class _FocusPointsCard extends StatelessWidget {
],
),
const SizedBox(height: AppSpacing.sm),
...List<Widget>.generate(points.length, (index) {
...points.map((point) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${index + 1}. ',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
Expanded(
child: Text(
points[index],
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(height: 1.55),
),
),
],
child: Text(
point,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.55),
),
);
}),
@@ -781,6 +740,46 @@ class _InfoCard extends StatelessWidget {
}
}
class _HexagramDetailPlaceholder extends StatelessWidget {
const _HexagramDetailPlaceholder({required this.status});
final DivinationRunStatus status;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context)!;
final message = switch (status) {
DivinationRunStatus.failed => l10n.resultHexagramDetailFailed,
DivinationRunStatus.refused => l10n.resultHexagramDetailRefused,
DivinationRunStatus.success => '',
};
return Padding(
padding: const EdgeInsets.only(top: AppSpacing.xl),
child: Card(
margin: EdgeInsets.zero,
color: colors.surfaceContainerHighest,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: Center(
child: Text(
message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
),
),
);
}
}
class _HexagramDetailCard extends StatelessWidget {
const _HexagramDetailCard({required this.data});
+24 -4
View File
@@ -382,9 +382,9 @@
"processingCardGenQuote": "Stillness at the proper time keeps one centered and steady in place.",
"processingCardKunTitle": "Kun • The Receptive Earth",
"processingCardKunQuote": "The Earth's condition is devoted receptivity; the noble one carries all with broad virtue.",
"ganZhiInfo": "干支信息",
"wuXingWangShuai": "五行旺衰",
"ganZhiKongWang": "空亡信息",
"ganZhiInfo": "Stem-Branch",
"wuXingWangShuai": "Five Elements",
"ganZhiKongWang": "Void",
"resultPillarColumn": "四柱",
"resultYearPillar": "年柱",
"resultMonthPillar": "月柱",
@@ -553,5 +553,25 @@
}
}
},
"retry": "Retry"
"retry": "Retry",
"resultFocusPoints": "Focus Points",
"wuXingMu": "Wood",
"wuXingHuo": "Fire",
"wuXingTu": "Earth",
"wuXingJin": "Metal",
"wuXingShui": "Water",
"wuXingWang": "Prosperous",
"wuXingXiang": "Strong",
"wuXingXiu": "Resting",
"wuXingQiu": "Imprisoned",
"wuXingSi": "Dead",
"yaoLegendTitle": "Symbol Guide",
"yaoColSpirit": "Spirit",
"yaoColRelation": "Relation",
"yaoColBranch": "Branch",
"yaoColElement": "Element",
"yaoColChange": "Chg",
"yaoColMark": "Mark",
"resultHexagramDetailFailed": "Interpretation failed. Hexagram details are unavailable.",
"resultHexagramDetailRefused": "Interpretation not supported. Please adjust your question and try again."
}
+120
View File
@@ -2558,6 +2558,126 @@ abstract class AppLocalizations {
/// In zh, this message translates to:
/// **'重试'**
String get retry;
/// No description provided for @resultFocusPoints.
///
/// In zh, this message translates to:
/// **'断卦要点'**
String get resultFocusPoints;
/// No description provided for @wuXingMu.
///
/// In zh, this message translates to:
/// **'木'**
String get wuXingMu;
/// No description provided for @wuXingHuo.
///
/// In zh, this message translates to:
/// **'火'**
String get wuXingHuo;
/// No description provided for @wuXingTu.
///
/// In zh, this message translates to:
/// **'土'**
String get wuXingTu;
/// No description provided for @wuXingJin.
///
/// In zh, this message translates to:
/// **'金'**
String get wuXingJin;
/// No description provided for @wuXingShui.
///
/// In zh, this message translates to:
/// **'水'**
String get wuXingShui;
/// No description provided for @wuXingWang.
///
/// In zh, this message translates to:
/// **'旺'**
String get wuXingWang;
/// No description provided for @wuXingXiang.
///
/// In zh, this message translates to:
/// **'相'**
String get wuXingXiang;
/// No description provided for @wuXingXiu.
///
/// In zh, this message translates to:
/// **'休'**
String get wuXingXiu;
/// No description provided for @wuXingQiu.
///
/// In zh, this message translates to:
/// **'囚'**
String get wuXingQiu;
/// No description provided for @wuXingSi.
///
/// In zh, this message translates to:
/// **'死'**
String get wuXingSi;
/// No description provided for @yaoLegendTitle.
///
/// In zh, this message translates to:
/// **'符号对照'**
String get yaoLegendTitle;
/// No description provided for @yaoColSpirit.
///
/// In zh, this message translates to:
/// **'六神'**
String get yaoColSpirit;
/// No description provided for @yaoColRelation.
///
/// In zh, this message translates to:
/// **'六亲'**
String get yaoColRelation;
/// No description provided for @yaoColBranch.
///
/// In zh, this message translates to:
/// **'地支'**
String get yaoColBranch;
/// No description provided for @yaoColElement.
///
/// In zh, this message translates to:
/// **'五行'**
String get yaoColElement;
/// No description provided for @yaoColChange.
///
/// In zh, this message translates to:
/// **'动'**
String get yaoColChange;
/// No description provided for @yaoColMark.
///
/// In zh, this message translates to:
/// **'标'**
String get yaoColMark;
/// No description provided for @resultHexagramDetailFailed.
///
/// In zh, this message translates to:
/// **'解卦失败,卦象详情暂不可用'**
String get resultHexagramDetailFailed;
/// No description provided for @resultHexagramDetailRefused.
///
/// In zh, this message translates to:
/// **'暂不支持解卦,请调整问题后重试'**
String get resultHexagramDetailRefused;
}
class _AppLocalizationsDelegate
+65 -3
View File
@@ -980,13 +980,13 @@ class AppLocalizationsEn extends AppLocalizations {
'The Earth\'s condition is devoted receptivity; the noble one carries all with broad virtue.';
@override
String get ganZhiInfo => '干支信息';
String get ganZhiInfo => 'Stem-Branch';
@override
String get wuXingWangShuai => '五行旺衰';
String get wuXingWangShuai => 'Five Elements';
@override
String get ganZhiKongWang => '空亡信息';
String get ganZhiKongWang => 'Void';
@override
String get resultPillarColumn => '四柱';
@@ -1353,4 +1353,66 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get retry => 'Retry';
@override
String get resultFocusPoints => 'Focus Points';
@override
String get wuXingMu => 'Wood';
@override
String get wuXingHuo => 'Fire';
@override
String get wuXingTu => 'Earth';
@override
String get wuXingJin => 'Metal';
@override
String get wuXingShui => 'Water';
@override
String get wuXingWang => 'Prosperous';
@override
String get wuXingXiang => 'Strong';
@override
String get wuXingXiu => 'Resting';
@override
String get wuXingQiu => 'Imprisoned';
@override
String get wuXingSi => 'Dead';
@override
String get yaoLegendTitle => 'Symbol Guide';
@override
String get yaoColSpirit => 'Spirit';
@override
String get yaoColRelation => 'Relation';
@override
String get yaoColBranch => 'Branch';
@override
String get yaoColElement => 'Element';
@override
String get yaoColChange => 'Chg';
@override
String get yaoColMark => 'Mark';
@override
String get resultHexagramDetailFailed =>
'Interpretation failed. Hexagram details are unavailable.';
@override
String get resultHexagramDetailRefused =>
'Interpretation not supported. Please adjust your question and try again.';
}
+120
View File
@@ -1294,6 +1294,66 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get retry => '重试';
@override
String get resultFocusPoints => '断卦要点';
@override
String get wuXingMu => '';
@override
String get wuXingHuo => '';
@override
String get wuXingTu => '';
@override
String get wuXingJin => '';
@override
String get wuXingShui => '';
@override
String get wuXingWang => '';
@override
String get wuXingXiang => '';
@override
String get wuXingXiu => '';
@override
String get wuXingQiu => '';
@override
String get wuXingSi => '';
@override
String get yaoLegendTitle => '符号对照';
@override
String get yaoColSpirit => '六神';
@override
String get yaoColRelation => '六亲';
@override
String get yaoColBranch => '地支';
@override
String get yaoColElement => '五行';
@override
String get yaoColChange => '';
@override
String get yaoColMark => '';
@override
String get resultHexagramDetailFailed => '解卦失败,卦象详情暂不可用';
@override
String get resultHexagramDetailRefused => '暂不支持解卦,请调整问题后重试';
}
/// The translations for Chinese, using the Han script (`zh_Hant`).
@@ -2353,4 +2413,64 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get retry => '重試';
@override
String get resultFocusPoints => '斷卦要點';
@override
String get wuXingMu => '';
@override
String get wuXingHuo => '';
@override
String get wuXingTu => '';
@override
String get wuXingJin => '';
@override
String get wuXingShui => '';
@override
String get wuXingWang => '';
@override
String get wuXingXiang => '';
@override
String get wuXingXiu => '';
@override
String get wuXingQiu => '';
@override
String get wuXingSi => '';
@override
String get yaoLegendTitle => '符號對照';
@override
String get yaoColSpirit => '六神';
@override
String get yaoColRelation => '六親';
@override
String get yaoColBranch => '地支';
@override
String get yaoColElement => '五行';
@override
String get yaoColChange => '';
@override
String get yaoColMark => '';
@override
String get resultHexagramDetailFailed => '解卦失敗,卦象詳情暫不可用';
@override
String get resultHexagramDetailRefused => '暫不支持解卦,請調整問題後重試';
}
+21 -1
View File
@@ -553,5 +553,25 @@
}
}
},
"retry": "重试"
"retry": "重试",
"resultFocusPoints": "断卦要点",
"wuXingMu": "木",
"wuXingHuo": "火",
"wuXingTu": "土",
"wuXingJin": "金",
"wuXingShui": "水",
"wuXingWang": "旺",
"wuXingXiang": "相",
"wuXingXiu": "休",
"wuXingQiu": "囚",
"wuXingSi": "死",
"yaoLegendTitle": "符号对照",
"yaoColSpirit": "六神",
"yaoColRelation": "六亲",
"yaoColBranch": "地支",
"yaoColElement": "五行",
"yaoColChange": "动",
"yaoColMark": "标",
"resultHexagramDetailFailed": "解卦失败,卦象详情暂不可用",
"resultHexagramDetailRefused": "暂不支持解卦,请调整问题后重试"
}
+15 -1
View File
@@ -436,5 +436,19 @@
"type": "int"
}
}
}
},
"wuXingWang": "旺",
"wuXingXiang": "相",
"wuXingXiu": "休",
"wuXingQiu": "囚",
"wuXingSi": "死",
"yaoLegendTitle": "符號對照",
"yaoColSpirit": "六神",
"yaoColRelation": "六親",
"yaoColBranch": "地支",
"yaoColElement": "五行",
"yaoColChange": "動",
"yaoColMark": "標",
"resultHexagramDetailFailed": "解卦失敗,卦象詳情暫不可用",
"resultHexagramDetailRefused": "暫不支持解卦,請調整問題後重試"
}