docs: 更新协议文档,删除废弃计划文档

- 更新 http-error-codes, user-points-chat-data-protocol
- 更新 divination-run-protocol, profile-protocol
- 删除废弃的后端和前端设计计划文档
This commit is contained in:
qzl
2026-04-08 17:23:02 +08:00
parent 49fc9a116f
commit e80a82bef4
57 changed files with 4117 additions and 2269 deletions
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
@@ -7,6 +8,7 @@ import '../../../../core/logging/logger.dart';
import '../../../../core/network/api_problem.dart';
import '../../../../data/network/api_client.dart';
import '../models/divination_backend_models.dart';
import '../models/follow_up_message.dart';
import '../models/divination_params.dart';
import '../models/divination_result.dart';
@@ -55,6 +57,14 @@ class DivinationApi {
if (raw['role'] != 'assistant') {
continue;
}
final threadId = raw['threadId'];
if (threadId is! String || threadId.trim().isEmpty) {
_logger.warning(
message: 'Skip history item without threadId',
extra: <String, dynamic>{'messageId': raw['id']},
);
continue;
}
final agentOutputRaw = raw['agent_output'];
if (agentOutputRaw is! Map<String, dynamic>) {
continue;
@@ -75,6 +85,7 @@ class DivinationApi {
userId: userId,
);
final aggregate = DivinationRunAggregate(
threadId: threadId,
derived: derived,
signLevel: _asString(agentOutputRaw['sign_level']),
conclusion: _asStringList(agentOutputRaw['conclusion']),
@@ -98,6 +109,105 @@ class DivinationApi {
return records;
}
Future<List<FollowUpMessage>> getSessionMessages({
required String threadId,
}) async {
Map<String, dynamic> json;
try {
final response = await _apiClient.rawDio.get<Map<String, dynamic>>(
'/api/v1/agent/history',
queryParameters: <String, dynamic>{'threadId': threadId},
);
json = response.data ?? <String, dynamic>{};
} on DioException catch (error) {
throw _mapProblem(error);
}
final messagesRaw = json['messages'];
if (messagesRaw is! List<dynamic>) {
return const <FollowUpMessage>[];
}
final messages = <FollowUpMessage>[];
for (final raw in messagesRaw) {
if (raw is! Map<String, dynamic>) {
continue;
}
try {
final message = FollowUpMessage.fromJson(raw);
if (message.role != 'user' && message.role != 'assistant') {
continue;
}
messages.add(message);
} catch (error, stackTrace) {
_logger.warning(
message: 'Skip malformed follow-up history message',
extra: <String, dynamic>{
'error': error.toString(),
'stackTrace': stackTrace.toString(),
},
);
continue;
}
}
return messages;
}
Future<RunAcceptedData> enqueueFollowUp({
required String threadId,
required String runId,
required String question,
required DivinationResultData result,
}) async {
final payload = buildFollowUpRunPayload(
threadId: threadId,
runId: runId,
question: question,
result: result,
clientNow: DateTime.now(),
);
final json = await _apiClient.postJson('/api/v1/agent/runs', data: payload);
return RunAcceptedData.fromJson(json);
}
Future<void> deleteSession({required String threadId}) async {
await _apiClient.deleteNoContent('/api/v1/agent/sessions/$threadId');
}
Future<String> transcribeAudio(String audioPath) async {
final file = File(audioPath);
if (!await file.exists()) {
throw ApiProblem(
status: 400,
title: 'Audio file missing',
detail: 'Audio file does not exist',
);
}
try {
final response = await _apiClient.rawDio.post<Map<String, dynamic>>(
'/api/v1/agent/transcribe',
data: FormData.fromMap(<String, dynamic>{
'audio': await MultipartFile.fromFile(
audioPath,
filename: 'follow_up.wav',
contentType: DioMediaType('audio', 'wav'),
),
}),
);
final payload = response.data;
if (payload is! Map<String, dynamic>) {
throw const FormatException('Invalid transcribe response');
}
final transcript = payload['transcript'];
if (transcript is! String) {
throw const FormatException('Invalid transcribe response');
}
return transcript;
} on DioException catch (error) {
throw _mapProblem(error);
}
}
Stream<Map<String, dynamic>> streamEvents({
required String threadId,
required String runId,
@@ -107,7 +217,10 @@ class DivinationApi {
response = await _apiClient.rawDio.get<ResponseBody>(
'/api/v1/agent/runs/$threadId/events',
queryParameters: <String, dynamic>{'runId': runId},
options: Options(responseType: ResponseType.stream),
options: Options(
responseType: ResponseType.stream,
receiveTimeout: null,
),
);
} on DioException catch (error, stackTrace) {
_logger.error(
@@ -260,6 +373,45 @@ Map<String, dynamic> buildDivinationRunPayload({
};
}
Map<String, dynamic> buildFollowUpRunPayload({
required String threadId,
required String runId,
required String question,
required DivinationResultData result,
required DateTime clientNow,
}) {
final yaoStates = result.yaoLines
.map((line) => line.type)
.toList(growable: false);
return <String, dynamic>{
'threadId': threadId,
'runId': runId,
'state': <String, dynamic>{},
'messages': [
{'id': 'msg_${runId}_user_0', 'role': 'user', 'content': question},
],
'tools': const <Map<String, dynamic>>[],
'context': const <Map<String, dynamic>>[],
'forwardedProps': {
'runtime_mode': 'follow_up',
'client_time': {
'device_timezone': 'Asia/Shanghai',
'client_now_iso': _toRfc3339Utc(clientNow),
'client_epoch_ms': clientNow.millisecondsSinceEpoch,
},
'divinationPayload': {
'divinationMethod': result.params.method == DivinationMethod.manual
? '手动起卦'
: '自动起卦',
'questionType': _questionTypeToText(result.params.questionType),
'question': result.params.question,
'divinationTimeIso': _toRfc3339Utc(result.params.divinationTime),
'yaoLines': yaoStates.map(_yaoTypeToText).toList(growable: false),
},
},
};
}
String _toRfc3339Utc(DateTime value) {
return value.toUtc().toIso8601String();
}