feat: M2.1 客户端骨架、BLE 修复、仓库整理与现状板

- BLE: 修复 BlueZ LocalName 与 Includes 冲突,串行注册与退避
- HTTP: 语音对话补 Live2D talking 标志与 session;Web 补 X-Session-Id
- Flutter: 角色/对话/模型页 + API,Gradle/依赖升级,麦克风权限
- 文档: 新增 docs/STATUS.md,校准 CLAUDE/PROGRESS/README
- 清理: 移除根目录截图与临时模型,运维脚本迁入 scripts/device
This commit is contained in:
2026-07-09 12:41:34 +08:00
parent 7cb8cee70d
commit 6f7e24db63
51 changed files with 2674 additions and 351 deletions

View File

@@ -5,8 +5,11 @@ import 'dart:typed_data';
import 'package:http/http.dart' as http;
import '../models/ai_model.dart';
import '../models/api_response.dart';
import '../models/ble_status.dart';
import '../models/character_pack.dart';
import '../models/chat_models.dart';
import '../models/player_status.dart';
import '../models/video_item.dart';
import '../models/wifi_network.dart';
@@ -300,6 +303,118 @@ class HttpApiService {
return ApiResponse.fromJson(_decodeMap(response.body));
}
Future<AvailableConfigs> getAvailableCharacterConfigs() async {
final map = await getAvailableConfigs();
return AvailableConfigs.fromJson(map);
}
// ── M2.1 AI 对话 / 模型管理 ──
Future<ChatTurnResponse> chatText({
required String text,
String? sessionId,
}) async {
final response = await _client.post(
_uri('/api/chat/text'),
headers: _jsonHeaders,
body: jsonEncode(<String, dynamic>{
'text': text,
if (sessionId != null && sessionId.isNotEmpty) 'session_id': sessionId,
}),
);
// AI 错误也以 JSON ChatResponse 返回(可能是 5xx
final map = _decodeMap(response.body);
final result = ChatTurnResponse.fromJson(map);
if (response.statusCode >= 200 && response.statusCode < 300) {
return result;
}
if (result.error != null && result.error!.isNotEmpty) {
return result;
}
throw ApiException(
map['message']?.toString() ?? '对话失败 (${response.statusCode})',
statusCode: response.statusCode,
);
}
Future<ChatTurnResponse> chatAudio({
required List<int> bytes,
String format = 'wav',
String? sessionId,
}) async {
final response = await _client.post(
_uri('/api/chat/audio'),
headers: <String, String>{
'Content-Type': 'application/octet-stream',
'Accept': 'application/json',
'X-Audio-Format': format,
if (sessionId != null && sessionId.isNotEmpty) 'X-Session-Id': sessionId,
},
body: bytes,
);
final map = _decodeMap(response.body);
final result = ChatTurnResponse.fromJson(map);
if (response.statusCode >= 200 && response.statusCode < 300) {
return result;
}
if (result.error != null && result.error!.isNotEmpty) {
return result;
}
throw ApiException(
map['message']?.toString() ?? '语音对话失败 (${response.statusCode})',
statusCode: response.statusCode,
);
}
String chatAudioFileUrl(String absolutePath) {
return _uri(
'/api/chat/audio/file',
<String, String>{'path': absolutePath},
).toString();
}
Future<ModelsSnapshot> getModels() async {
final response = await _client.get(_uri('/api/models'));
_ensureSuccess(response);
return ModelsSnapshot.fromJson(_decodeMap(response.body));
}
Future<ApiResponse> downloadModel(String modelId) async {
final response = await _client.post(
_uri('/api/models/download'),
headers: _jsonHeaders,
body: jsonEncode(<String, dynamic>{'model_id': modelId}),
);
_ensureSuccess(response);
return ApiResponse.fromJson(_decodeMap(response.body));
}
Future<ApiResponse> switchModel({
required String modelId,
required String kind,
}) async {
final response = await _client.post(
_uri('/api/models/switch'),
headers: _jsonHeaders,
body: jsonEncode(<String, dynamic>{
'model_id': modelId,
'kind': kind,
}),
);
_ensureSuccess(response);
return ApiResponse.fromJson(_decodeMap(response.body));
}
Future<ApiResponse> deleteModel(String modelId) async {
final response = await _client.post(
_uri('/api/models/delete'),
headers: _jsonHeaders,
body: jsonEncode(<String, dynamic>{'model_id': modelId}),
);
_ensureSuccess(response);
return ApiResponse.fromJson(_decodeMap(response.body));
}
Future<List<Map<String, dynamic>>> listFiles(String dirKey, [String? path]) async {
final response = await _client.get(
_uri('/api/files/$dirKey', _pathQuery(path)),