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:
116
clients/flutter/lib/providers/model_provider.dart
Normal file
116
clients/flutter/lib/providers/model_provider.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/ai_model.dart';
|
||||
import '../models/api_response.dart';
|
||||
import '../services/http_api_service.dart';
|
||||
import 'debug_provider.dart';
|
||||
|
||||
class ModelProvider extends ChangeNotifier {
|
||||
ModelProvider({
|
||||
required HttpApiService httpApiService,
|
||||
required DebugProvider debugProvider,
|
||||
}) : _http = httpApiService,
|
||||
_debug = debugProvider;
|
||||
|
||||
final HttpApiService _http;
|
||||
final DebugProvider _debug;
|
||||
|
||||
ModelsSnapshot? _snapshot;
|
||||
bool _loading = false;
|
||||
bool _acting = false;
|
||||
String? _error;
|
||||
String? _status;
|
||||
Timer? _pollTimer;
|
||||
|
||||
ModelsSnapshot? get snapshot => _snapshot;
|
||||
bool get isLoading => _loading;
|
||||
bool get isActing => _acting;
|
||||
String? get error => _error;
|
||||
String? get status => _status;
|
||||
|
||||
Future<void> refresh() async {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
_snapshot = await _http.getModels();
|
||||
_debug.addHttpLog(
|
||||
'Loaded models',
|
||||
details: 'count=${_snapshot?.models.length ?? 0}',
|
||||
);
|
||||
} catch (error) {
|
||||
_error = error.toString();
|
||||
_debug.addHttpLog('Load models failed', details: _error);
|
||||
} finally {
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> download(String modelId) async {
|
||||
await _runAction(() => _http.downloadModel(modelId), '下载已启动: $modelId');
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
Future<void> switchTo({required String modelId, required String kind}) async {
|
||||
await _runAction(
|
||||
() => _http.switchModel(modelId: modelId, kind: kind),
|
||||
'已切换: $modelId',
|
||||
);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> delete(String modelId) async {
|
||||
await _runAction(() => _http.deleteModel(modelId), '已删除: $modelId');
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> _runAction(
|
||||
Future<ApiResponse> Function() action,
|
||||
String okStatus,
|
||||
) async {
|
||||
if (_acting) return;
|
||||
_acting = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final result = await action();
|
||||
_status = result.message.isNotEmpty ? result.message : okStatus;
|
||||
if (!result.isOk) {
|
||||
_error = _status;
|
||||
}
|
||||
_debug.addHttpLog('Model action ok', details: _status);
|
||||
} catch (error) {
|
||||
_error = error.toString();
|
||||
_status = _error;
|
||||
_debug.addHttpLog('Model action failed', details: _error);
|
||||
} finally {
|
||||
_acting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_pollTimer?.cancel();
|
||||
var ticks = 0;
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||
ticks += 1;
|
||||
await refresh();
|
||||
final downloading = _snapshot?.models.any(
|
||||
(m) => m.downloadProgress > 0 && m.downloadProgress < 100 && !m.downloaded,
|
||||
) ??
|
||||
false;
|
||||
if (!downloading || ticks > 90) {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user