Flutter + 浏览器插件 透析系统语音输入完整技术方案
Flutter + 浏览器插件 透析系统语音输入完整技术方案
整理时间: 2026-02-16 08:38
来源: 群聊消息
整理人: AI助手
摘要
本文档提供了一套完整的、基于 Flutter Desktop + 浏览器插件(方案 B)的透析管理系统语音输入技术解决方案。方案采用 Flutter 作为控制中枢,负责语音采集、ASR 语音识别和 LLM 语义提取;浏览器插件作为执行末端,通过 WebSocket 与 Flutter 通信,实现 DOM 精准填充。文档涵盖了完整的软件架构、技术选型、实现代码示例,以及常见问题的详细解决方案。
一、软件架构全景图
1.1 系统架构
┌─────────────────────────────────────────────────────────────────────────┐
│ 医生工作站 │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Flutter App (控制中枢) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │
│ │ │ 系统托盘 │ │ 全局热键 │ │ 录音模块 │ │ │
│ │ │ (常驻后台) │ │ (F4/Alt+S) │ │ (record + ffmpeg) │ │ │
│ │ └─────────────┘ └─────────────┘ └───────────┬─────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────────────────────────────────▼─────────────┐ │ │
│ │ │ AI 处理层 │ │ │
│ │ │ ┌─────────────────┐ ┌─────────────────────┐ │ │ │
│ │ │ │ FunASR │ │ Kimi/Minimax │ │ │ │
│ │ │ │ (语音转文字) │ │ (语义提取 JSON) │ │ │ │
│ │ │ └─────────────────┘ └─────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────────────────────────────▼───────────┐ │ │
│ │ │ WebSocket Server (127.0.0.1:9999) │ │ │
│ │ │ 向浏览器插件广播结构化数据 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ WebSocket (ws://127.0.0.1:9999) │
│ │ │
│ ┌────────────────────────────────▼──────────────────────────────┐ │
│ │ Chrome 浏览器 │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 浏览器插件 (执行末端) │ │ │
│ │ │ • 连接 WebSocket Server │ │ │
│ │ │ • 识别当前页面 URL 与 DOM 结构 │ │ │
│ │ │ • 执行 JavaScript 自动填充表单 │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 透析管理系统 (BS) │ │ │
│ │ │ [干体重: 65.5] [收缩压: 120] [舒张压: 80] ... │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
1.2 核心工作流程
┌─────────────────────────────────────────────────────────────────────┐
│ 完整工作流程 │
└─────────────────────────────────────────────────────────────────────┘
1. 热键触发
医生按下 F4 或 Alt+S
│
▼
2. 录音开始
Flutter 弹出半透明悬浮条,显示"正在录音..."
音频流实时传输给 FunASR
│
▼
3. 流式识别 (可选)
FunASR 实时返回识别文字
悬浮条显示识别的文字(边说边出)
│
▼
4. 录音结束
医生松开热键或停止说话
完整文本发送给 Kimi/Minimax
│
▼
5. 语义提取
LLM 返回结构化 JSON
{"dry_weight": 65.5, "bp_high": 120, "bp_low": 80}
│
▼
6. WebSocket 广播
Flutter 通过 WebSocket 推送给浏览器插件
│
▼
7. DOM 填充
插件接收 JSON,定位输入框
自动填入对应值,触发 input 事件
│
▼
8. 医生确认
悬浮条显示"已填入 3 个字段"
医生检查并提交表单
二、技术栈选型
2.1 完整技术栈表
| 维度 | 选型 | 理由 |
|---|---|---|
| UI 框架 | Flutter Desktop (Windows) | 性能好、内存占用极低、UI 开发效率高 |
| 热键管理 | hotkey_manager | 支持全局监听,应用在后台也能触发 |
| 音频处理 | record + ffmpeg | 支持主流音频格式,占用极低 |
| 通信协议 | shelf_web_socket | Flutter 内部集成轻量级服务器,低延迟 |
| ASR 服务 | FunASR (阿里) | 支持私有化部署,医疗专有名词可定制 |
| LLM 接口 | Kimi / Minimax | 中文语义理解强,用于提取 JSON 字段 |
| 插件开发 | Chrome Extension MV3 | 兼容所有现代主流浏览器 |
| 打包发布 | Inno Setup / MSIX | 打包 VC++ 运行库,一键安装 |
2.2 技术选型理由详解
Flutter Desktop 优势:
- 打包体积远小于 Electron(~10MB vs ~100MB)
- 内存占用低,适合医疗场景老旧电脑
- UI 开发效率高,跨平台代码可复用
FunASR 优势:
- 阿里开源,工业级性能
- Paraformer 模型中文识别精准
- 支持 Docker 私有化部署
- 支持流式识别,降低延迟感知
浏览器插件(方案 B)优势:
- 不依赖模拟按键,不受焦点位置影响
- 可精准定位任意 DOM 元素
- 可处理复杂表单结构和动态字段
三、关键实现代码
3.1 第一阶段:Flutter 语音采集与处理
3.1.1 热键监听
import 'package:hotkey_manager/hotkey_manager.dart';
class HotkeyService {
Future<void> init() async {
// 注册全局热键 F4
final hotKey = HotKey(
key: LogicalKeyboardKey.f4,
scope: HotKeyScope.system,
);
await hotKeyManager.register(
hotKey,
keyDownHandler: (hotKey) {
print('热键触发,开始录音');
AudioService.startRecording();
},
);
}
}
3.1.2 录音服务
import 'package:record/record.dart';
class AudioService {
final AudioRecorder _recorder = AudioRecorder();
Future<void> startRecording() async {
if (await _recorder.hasPermission()) {
await _recorder.start(
const RecordConfig(
encoder: AudioEncoder.aacLc,
sampleRate: 16000,
numChannels: 1,
),
path: 'temp_recording.m4a',
);
}
}
Future<String?> stopRecording() async {
final path = await _recorder.stop();
return path;
}
}
3.1.3 FunASR 调用
import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:convert';
class FunASRService {
final String _apiUrl = 'http://localhost:8000/asr';
Future<String> transcribe(String audioPath) async {
final file = File(audioPath);
final bytes = await file.readAsBytes();
final request = http.MultipartRequest('POST', Uri.parse(_apiUrl))
..files.add(http.MultipartFile.fromBytes('file', bytes, filename: 'audio.m4a'));
final response = await request.send();
final result = await http.Response.fromStream(response);
final data = jsonDecode(result.body);
return data['text'] ?? '';
}
// 流式识别版本
Stream<String> transcribeStream(String audioPath) async* {
// 实现流式返回识别文字
// 用于边说边显示的实时效果
}
}
3.1.4 LLM 语义提取
import 'package:http/http.dart' as http;
import 'dart:convert';
class LLmService {
final String _apiKey = 'your-kimi-api-key';
Future<Map<String, dynamic>> extractDialysisData(String text) async {
final prompt = '''
你是一个医疗助手,请从以下文本提取透析数据:
字段说明:
- dry_weight: 干体重 (kg)
- bp_high: 收缩压 (mmHg)
- bp_low: 舒张压 (mmHg)
- uf: 超滤量 (ml)
- flow: 透析流量 (ml/min)
医生口述:"$text"
请输出严格 JSON 格式,不要输出任何解释。
''';
final response = await http.post(
Uri.parse('https://api.moonshot.cn/v1/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_apiKey',
},
body: jsonEncode({
'model': 'kimi-flash',
'messages': [
{'role': 'system', 'content': '你是一个专业的医疗数据提取助手。'},
{'role': 'user', 'content': prompt},
],
'temperature': 0.1,
}),
);
final data = jsonDecode(response.body);
final content = data['choices'][0]['message']['content'];
// 提取 JSON 部分
final jsonMatch = RegExp(r'\{.*\}').firstMatch(content);
if (jsonMatch != null) {
return jsonDecode(jsonMatch.group(0)!);
}
return {};
}
}
3.2 第二阶段:WebSocket 数据桥接
3.2.1 Flutter WebSocket Server
import 'package:shelf/shelf.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketService {
late HttpServer _server;
final List<WebSocketChannel> _clients = [];
Future<void> startServer() async {
final handler = webSocketHandler((webSocket) {
_clients.add(webSocket);
// 监听来自插件的消息(如:当前页面信息)
webSocket.stream.listen((message) {
print('收到插件消息: $message');
// 处理插件发来的消息
});
// 清理断开连接
webSocket.sink.done.then(() {
_clients.remove(webSocket);
});
});
_server = await serve(handler, '127.0.0.1', 9999);
print('WebSocket 服务启动: ws://127.0.0.1:9999');
}
// 广播结构化数据给所有插件客户端
void broadcastData(Map<String, dynamic> data) {
final message = jsonEncode(data);
for (final client in _clients) {
client.sink.add(message);
}
}
}
3.2.2 完整的语音处理流程
class VoiceAssistantService {
final HotkeyService _hotkey = HotkeyService();
final AudioService _audio = AudioService();
final FunASRService _asr = FunASRService();
final LLmService _llm = LLmService();
final WebSocketService _ws = WebSocketService();
Future<void> init() async {
await _ws.startServer();
await _hotkey.init();
}
Future<void> processVoice() async {
// 1. 停止录音
final audioPath = await _audio.stopRecording();
if (audioPath == null) return;
// 2. 语音识别
final text = await _asr.transcribe(audioPath);
print('识别结果: $text');
// 3. 语义提取
final data = await _llm.extractDialysisData(text);
print('提取数据: $data');
// 4. 广播给插件
_ws.broadcastData({
'type': 'fill_form',
'text': text,
'data': data,
'timestamp': DateTime.now().toIso8601String(),
});
}
}
3.3 第三阶段:浏览器插件(方案 B)
3.3.1 manifest.json (Manifest V3)
{
"manifest_version": 3,
"name": "透析系统语音填表助手",
"version": "1.0",
"description": "配合 Flutter 语音助手自动填写透析表单",
"permissions": [
"activeTab",
"scripting"
],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}
3.3.2 content_script.js (DOM 填充核心逻辑)
// content_script.js
// 建立 WebSocket 连接
const socket = new WebSocket('ws://127.0.0.1:9999');
socket.onopen = () => {
console.log('已连接到 Flutter 语音助手');
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'fill_form') {
fillDialysisForm(message.data);
}
} catch (e) {
console.error('解析消息失败:', e);
}
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
socket.onclose = () => {
console.log('连接已断开,尝试重连...');
setTimeout(() => {
// 简单的重连逻辑
location.reload();
}, 3000);
};
// 透析表单字段映射配置
const fieldMapping = {
'dry_weight': ['干体重', 'dryWeight', 'weight', 'dry_weight'],
'bp_high': ['收缩压', 'systolic', 'bp_sys', 'sbp'],
'bp_low': ['舒张压', 'diastolic', 'bp_dia', 'dbp'],
'uf': ['超滤量', 'ultrafiltration', 'uf', 'ultra'],
'flow': ['透析流量', 'bloodFlow', 'flow', 'bf'],
'conductivity': ['电导率', 'conductivity', 'cond'],
'temp': ['温度', 'temperature', 'temp'],
'venous_pressure': ['静脉压', 'venous', 'vp'],
'arterial_pressure': ['动脉压', 'arterial', 'ap'],
'heparin': ['肝素', 'heparin', 'hep'],
'duration': ['时长', 'duration', 'time'],
'weight_before': ['透析前体重', 'weightBefore', 'pre_weight'],
'weight_after': ['透析后体重', 'weightAfter', 'post_weight']
};
// 自动填充表单
function fillDialysisForm(data) {
let filledCount = 0;
for (const [key, value] of Object.entries(data)) {
if (value === undefined || value === null) continue;
const selectors = fieldMapping[key] || [key];
let inputElement = null;
// 尝试多种方式定位输入框
for (const selector of selectors) {
// 1. 通过 id 查找
inputElement = document.querySelector(`#${selector}`);
if (inputElement) break;
// 2. 通过 name 查找
inputElement = document.querySelector(`[name="${selector}"]`);
if (inputElement) break;
// 3. 通过 placeholder 模糊匹配
inputElement = document.querySelector(`input[placeholder*="${selector}"]`);
if (inputElement) break;
// 4. 通过 label 文字找到关联的 input
const labels = document.querySelectorAll(`label`);
for (const label of labels) {
if (label.textContent.includes(selector)) {
const labelFor = label.getAttribute('for');
if (labelFor) {
inputElement = document.querySelector(`#${labelFor}`);
}
// 查找 label 内部的 input
if (!inputElement) {
inputElement = label.querySelector('input');
}
if (inputElement) break;
}
}
if (inputElement) break;
}
if (inputElement) {
// 设置值
inputElement.value = String(value);
// 触发 input 事件,确保前端框架(Vue/React)监听到变化
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
// 高亮显示已填入的字段
inputElement.style.backgroundColor = '#d4edda';
inputElement.style.border = '2px solid #28a745';
setTimeout(() => {
inputElement.style.backgroundColor = '';
inputElement.style.border = '';
}, 3000);
filledCount++;
console.log(`已填入 ${key}: ${value}`);
} else {
console.warn(`未找到字段 ${key} 的输入框`);
}
}
// 显示填充结果通知
showNotification(`已填入 ${filledCount} 个字段`);
}
// 显示通知
function showNotification(message) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 15px 20px;
border-radius: 5px;
z-index: 99999;
font-family: sans-serif;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
3.3.3 background.js (服务 worker)
// background.js
// 监听插件安装/更新
chrome.runtime.onInstalled.addListener(() => {
console.log('透析语音填表助手已安装');
});
// 可以在这里处理一些全局状态
四、痛点问题及解决方案
4.1 跨域与连接安全 (Mixed Content)
| 问题 | 解决方案 |
|---|---|
| HTTPS 页面禁止连接不安全的 ws:// | 在插件 manifest.json 中声明 host_permissions |
| 需要使用 wss(WebSocket Secure) | 在 Flutter 端集成自签名证书,或使用 Native Messaging |
具体配置:
// manifest.json
{
"host_permissions": [
"http://*/*",
"https://*/*"
]
}
Native Messaging 替代方案(更安全):
- 使用 Chrome Native Messaging API 替代 WebSocket
- 需要开发本地 native host 程序
- 适合对安全性要求更高的医疗场景
4.2 表单字段匹配准确性
| 问题 | 解决方案 |
|---|---|
| 透析系统页面复杂,input 标签可能没有 id | 模糊匹配:通过 label 文字定位相邻 input |
| 不同页面字段顺序不同 | 配置模式:允许医生”点选”方式录入映射关系 |
解决方案 1:模糊匹配逻辑
// 通过 label 文字模糊匹配
function findInputByLabel(keyword) {
const labels = document.querySelectorAll('label, .label, .field-label, th');
for (const label of labels) {
if (label.textContent.includes(keyword)) {
// 1. 查找 label 的 for 属性
if (label.for) {
const input = document.querySelector(`#${label.for}`);
if (input) return input;
}
// 2. 查找 label 内部的 input
const input = label.querySelector('input, select, textarea');
if (input) return input;
// 3. 查找相邻的 input
const nextElement = label.nextElementSibling;
if (nextElement && nextElement.tagName === 'INPUT') {
return nextElement;
}
}
}
return null;
}
解决方案 2:配置模式(点选映射)
// 医生手动配置字段映射
const customMapping = JSON.parse(localStorage.getItem('dialysis_field_mapping') || '{}');
function fillWithCustomMapping(data) {
for (const [key, value] of Object.entries(data)) {
const customSelector = customMapping[key];
if (customSelector) {
const input = document.querySelector(customSelector);
if (input) {
input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
}
}
// 医生配置模式:在页面上点击输入框,保存选择器
function enableMappingMode() {
document.body.style.cursor = 'crosshair';
document.addEventListener('click', (e) => {
if (e.target.matches('input, select, textarea')) {
const selector = generateSelector(e.target);
const fieldName = prompt('请输入这个字段对应的名称(如 dry_weight):');
if (fieldName) {
customMapping[fieldName] = selector;
localStorage.setItem('dialysis_field_mapping', JSON.stringify(customMapping));
alert('映射已保存!');
}
}
});
}
4.3 系统权限与打包
| 问题 | 解决方案 |
|---|---|
| 缺少 C++ 环境导致应用打不开 | 使用 Inno Setup 制作安装包,集成 VC_redist.x64.exe 静默安装 |
| 录音权限被拦截 | 在 windows/runner/main.cpp 中配置应用 Manifest |
Inno Setup 打包脚本:
; setup.iss
[Setup]
AppName=透析语音助手
AppVersion=1.0.0
DefaultDirName={pf}\DialysisVoiceAssistant
OutputDir=.
OutputBaseFilename=DialysisVoiceAssistant_Setup
[Files]
Source: "build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Run]
; 静默安装 VC++ 运行库
Filename: "vc_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "正在安装运行库..."; Flags: waituntilterminated
[Code]
function InitializeSetup(): Boolean;
var
ResultCode: Integer;
begin
// 检查是否已安装 VC++ 运行库
if not RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64') then
begin
// 安装 VC++ 运行库
Exec('vc_redist.x64.exe', '/install /quiet /norestart', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
Result := True;
end;
Windows 录音权限配置:
在 windows/runner/main.cpp 中添加:
// 声明麦克风权限
int main(int argc, char** argv) {
// 初始化 COM
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
// 其他初始化...
return app->Run();
}
在 windows/runner/CMakeLists.txt 中添加:
# 添加麦克风权限声明
set_target_properties(${BINARY_NAME} PROPERTIES
VS_GLOBAL_KEYWORD "Win32Project"
)
4.4 LLM 响应速度优化
| 问题 | 解决方案 |
|---|---|
| 等待 LLM 返回 JSON 耗时较长 | 预处理:ASR 识别出关键词时立即构建上下文 |
| 网络延迟影响体验 | 本地过滤:简单数值直接正则匹配,复杂描述才走 LLM |
解决方案:混合处理策略
class FastExtractionService {
// 本地正则匹配简单数值
final Map<String, RegExp> _patterns = {
'bp_high': RegExp(r'(\d{2,3})/\d{2,3}'), // 匹配 120/80 中的 120
'bp_low': RegExp(r'\d{2,3}/(\d{2,3})'), // 匹配 120/80 中的 80
'uf': RegExp(r'(\d+\.?\d*)\s*(kg|ml|毫升)'), // 匹配 2.5kg 或 2500ml
'flow': RegExp(r'流量\s*(\d+)'), // 匹配 "流量200"
'temp': RegExp(r'温度\s*(\d+\.?\d*)'), // 匹配 "温度36.5"
};
Map<String, dynamic> extractFast(String text) {
final result = <String, dynamic>{};
// 血压:尝试同时提取高低压
final bpMatch = RegExp(r'(\d{2,3})/(\d{2,3})').firstMatch(text);
if (bpMatch != null) {
result['bp_high'] = int.tryParse(bpMatch.group(1) ?? '');
result['bp_low'] = int.tryParse(bpMatch.group(2) ?? '');
}
// 其他字段逐个匹配
for (final entry in _patterns.entries) {
final match = entry.value.firstMatch(text);
if (match != null) {
final value = match.group(1);
if (value != null) {
result[entry.key] = double.tryParse(value) ?? value;
}
}
}
// 如果提取到关键数据,直接返回
if (result.isNotEmpty) {
print('本地快速提取: $result');
return result;
}
// 没有匹配到,走 LLM
return {};
}
}
4.5 多屏幕与 DPI 缩放
| 问题 | 解决方案 |
|---|---|
| 医疗环境多显示器配置常见 | 使用 screen_retriever 库获取精确屏幕参数 |
| 150% 缩放下悬浮窗位置偏移 | 使用逻辑像素,多分辨率适配测试 |
Flutter 多屏幕处理:
import 'package:screen_retriever/screen_retriever.dart';
class DisplayService {
Future<void> init() async {
// 获取主显示器信息
final primaryDisplay = await ScreenRetriever.instance.getPrimaryDisplay();
print('主显示器: ${primaryDisplay.size}');
print('缩放比例: ${primaryDisplay.scaleFactor}');
// 获取所有显示器
final displays = await ScreenRetriever.instance.getAllDisplays();
for (final display in displays) {
print('显示器: ${display.name}, 尺寸: ${display.size}, 缩放: ${display.scaleFactor}');
}
}
// 根据当前鼠标位置确定在哪个显示器显示悬浮窗
Future<Offset> getCursorPosition() async {
return await ScreenRetriever.instance.getCursorPosition();
}
}
4.6 音频驱动与采集干扰
| 问题 | 解决方案 |
|---|---|
| Windows 音频环境复杂 | UI 上提供麦克风设备选择 |
| 录音编码阻塞主线程 | 录音逻辑放在独立 Isolate 中 |
Flutter 麦克风选择与 Isolate 处理:
import 'dart:isolate';
class AudioService {
// 获取可用麦克风列表
Future<List<AudioDevice>> getInputDevices() async {
final recorder = AudioRecorder();
final devices = await recorder.listDevices();
return devices.where((d) => d.type == AudioDevice.input).toList();
}
// 在独立 Isolate 中处理录音
Future<void> recordInIsolate() async {
final receivePort = ReceivePort();
await Isolate.spawn((sendPort) async {
final recorder = AudioRecorder();
await recorder.start(config, path: 'temp.m4a');
// ... 录音处理逻辑
sendPort.send('done');
}, receivePort.sendPort);
receivePort.listen((message) {
print('Isolate 结果: $message');
});
}
// 显示音量波动
Stream<double> get volumeStream async* {
final recorder = AudioRecorder();
await recorder.start(config);
while (true) {
final amplitude = await recorder.getAmplitude();
// 将 dB 转换为 0-1 的范围
final normalized = ((amplitude.current + 60) / 60).clamp(0.0, 1.0);
yield normalized;
await Future.delayed(Duration(milliseconds: 100));
}
}
}
五、UI 设计规范
5.1 悬浮球 UI 状态
| 状态 | 外观 | 说明 |
|---|---|---|
| 闲置 | 灰色半透明圆形 | 等待热键触发 |
| 录音中 | 红色 + 音量波动动画 | 正在录音 |
| 识别中 | 黄色旋转 | FunASR 处理中 |
| 提取中 | 蓝色闪烁 | LLM 提取中 |
| 完成 | 绿色勾 | 成功填入字段 |
| 错误 | 红色叹号 | 处理失败 |
5.2 悬浮预览窗
┌────────────────────────────────────────┐
│ 🎤 透析语音助手 ✕ │
├────────────────────────────────────────┤
│ 识别文字: 今天透析流量200,超滤3000 │
│ │
│ 提取结果: │
│ ┌──────────────────────────────────┐ │
│ │ 透析流量: 200 ml/min ✓ │ │
│ │ 超滤量: 3000 ml ✓ │ │
│ └──────────────────────────────────┘ │
│ │
│ [ ✓ 确认填入 ] [ ✕ 取消 ] │
└────────────────────────────────────────┘
六、分阶段实施规划
6.1 开发周期
| 阶段 | 内容 | 时间 | 交付物 |
|---|---|---|---|
| MVP | Flutter 录音 + FunASR 转写 + Ctrl+V 模拟粘贴 | 第 1-2 周 | 语音转文字基础功能 |
| 进阶版 | 开发浏览器插件 + WebSocket 通道 + DOM 填充 | 第 3-4 周 | 说一段话填两个框 |
| 专业版 | 适配全部表单 + 私有化部署 + 优化打包 | 第 5-8 周 | 可分发的正式产品 |
6.2 MVP 验收标准
- [ ] 热键 F4 可以触发录音
- [ ] 录音文件可以正确转写为文字
- [ ] 文字可以通过 Ctrl+V 粘贴到光标位置
- [ ] 在透析系统页面验证通过
6.3 进阶版验收标准
- [ ] WebSocket 连接稳定
- [ ] 浏览器插件可以正确接收 JSON
- [ ] 可以自动填入至少 5 个核心字段
- [ ] 字段匹配准确率 > 90%
七、相关资源
7.1 开源项目
7.2 商业服务
整理备注
本文档提供了完整的 Flutter + 浏览器插件技术方案,包含详细的代码示例、问题解决方案和实施规划。如需浏览器插件 Manifest V3 完整代码框架或其他具体模块的实现细节,请进一步提出。