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

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/video_item.dart';
@@ -80,6 +81,34 @@ class _SettingsScreenState extends State<SettingsScreen> {
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(AppSpacing.md),
children: [
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.memory_rounded),
title: const Text('模型管理'),
subtitle: const Text('下载 / 切换 / 删除 AI 模型'),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => context.push('/settings/models'),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.bolt_rounded),
title: const Text('状态机触发'),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => context.push('/settings/trigger'),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.bug_report_rounded),
title: const Text('调试日志'),
trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => context.push('/settings/debug'),
),
],
),
),
const SizedBox(height: AppSpacing.lg),
Card(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
@@ -464,9 +493,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
Map<String, dynamic>.from(results[0] as Map<String, dynamic>);
final available =
Map<String, dynamic>.from(results[1] as Map<String, dynamic>);
// M2.1: configs 为 {filename, character} 对象列表;兼容旧版纯字符串列表
_availableConfigs =
(available['configs'] as List<dynamic>? ?? const <dynamic>[])
.map((item) => item.toString())
.map((item) {
if (item is String) return item;
if (item is Map) {
return item['filename']?.toString() ?? item.toString();
}
return item.toString();
})
.where((name) => name.isNotEmpty)
.toList(growable: false);
_activeConfig = available['active']?.toString();
_applyDisplayConfig(Map<String, dynamic>.from(