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

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/ai_model.dart';
import '../providers/model_provider.dart';
import '../theme/app_colors.dart';
class ModelsScreen extends StatefulWidget {
const ModelsScreen({super.key});
@override
State<ModelsScreen> createState() => _ModelsScreenState();
}
class _ModelsScreenState extends State<ModelsScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ModelProvider>().refresh();
});
}
@override
Widget build(BuildContext context) {
final provider = context.watch<ModelProvider>();
final snapshot = provider.snapshot;
return Scaffold(
appBar: AppBar(
title: const Text('模型管理'),
actions: [
IconButton(
onPressed: provider.isLoading ? null : () => provider.refresh(),
icon: const Icon(Icons.refresh_rounded),
),
],
),
body: RefreshIndicator(
onRefresh: provider.refresh,
child: ListView(
padding: const EdgeInsets.all(AppSpacing.md),
children: [
if (provider.error != null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Text(
provider.error!,
style: const TextStyle(color: AppColors.error),
),
),
if (provider.status != null)
Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: Text(
provider.status!,
style: const TextStyle(color: AppColors.textSecondary),
),
),
if (snapshot != null) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'存储 ${snapshot.usedLabel} / ${snapshot.quotaLabel}',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
LinearProgressIndicator(value: snapshot.usageRatio),
const SizedBox(height: 8),
Text(
'激活: LLM=${snapshot.active['llm'] ?? '-'} · '
'ASR=${snapshot.active['asr'] ?? '-'} · '
'TTS=${snapshot.active['tts'] ?? '-'}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
),
const SizedBox(height: AppSpacing.md),
],
if (provider.isLoading && snapshot == null)
const Padding(
padding: EdgeInsets.only(top: 48),
child: Center(child: CircularProgressIndicator()),
)
else if (snapshot == null || snapshot.models.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 48),
child: Center(child: Text('暂无模型清单AI 插件可能未就绪)')),
)
else
...snapshot.models.map((model) {
final active = snapshot.isActive(model);
return Card(
margin: const EdgeInsets.only(bottom: AppSpacing.md),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
model.name,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (active)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color:
AppColors.success.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(999),
),
child: const Text(
'使用中',
style: TextStyle(
color: AppColors.success,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 4),
Text(
'${model.kindLabel} · ${model.sizeLabel} · 内存 ${model.memoryLabel}',
style:
Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppColors.textSecondary,
),
),
if (model.recommendedTier.isNotEmpty)
Text(
model.recommendedTier,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: AppColors.textSecondary),
),
if (!model.downloaded && model.downloadProgress > 0)
Padding(
padding: const EdgeInsets.only(top: 8),
child: LinearProgressIndicator(
value: model.downloadProgress / 100.0,
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (!model.downloaded)
FilledButton.tonal(
onPressed: provider.isActing
? null
: () => provider.download(model.id),
child: Text(
model.downloadProgress > 0 &&
model.downloadProgress < 100
? '下载中 ${model.downloadProgress}%'
: '下载',
),
),
if (model.downloaded && !active)
FilledButton(
onPressed: provider.isActing
? null
: () => provider.switchTo(
modelId: model.id,
kind: model.kind,
),
child: const Text('切换使用'),
),
if (model.downloaded && !active)
OutlinedButton(
onPressed: provider.isActing
? null
: () => _confirmDelete(context, model),
child: const Text('删除'),
),
],
),
],
),
),
);
}),
],
),
),
);
}
Future<void> _confirmDelete(BuildContext context, AiModelInfo model) async {
final ok = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除模型'),
content: Text('确定删除 ${model.name}?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('删除'),
),
],
),
);
if (ok == true && context.mounted) {
await context.read<ModelProvider>().delete(model.id);
}
}
}