再见,Firefox Send!

该文章已生成可运行项目,

说起临时文件分享,很多人会推荐 FireFox Send

它是 Mozilla 开发的一款开源的、免费的、无需登录就能使用的在线文件托管与共享服务

Firefox Send是Mozilla开发的一个基于Web网页的、开源、跨平台、可免费使用的在线文件托管与共享服务,未登录Firefox账户的游客可以用其发送单个最大1GB的文件或文件集,已登录Firefox账户的用户可以用其发送单个最大2.5GB的文件或文件集。

Firefox Send 于 2019 年 3 月上线

但是就在今年 9 月,因被滥用,Mozilla 宣布将其下架

享年 1.5 岁。

我们开始 Firefox Send 是要为人们提供一个安全便利的从浏览器分享文件的方法。

不幸的是,一些滥用的用户开始使用 Firefox Send 来发送恶意软件并进行钓鱼攻击。

当这个问题被报告后,我们停止了服务。

Firefox Send 下架后,还有哪些好用的临时文件分享工具呢?

今天哈哈就给大家盘点一下。

既然是对标 Firefox Send,那么几个特点不能漏掉:免费、无需登录

因为我们的使用场景是临时文件分享,如果还需要注册才能使用,那就有点得不偿失了。

下面,主要从以下几个角度对部分产品进行测评。

  • 上传限制

  • 上传速度

  • 下载速度

  • 保存时间

奶牛快传

https://cowtransfer.com/

上传限制

未登录状态下,支持单次上传多个文件,但单次上传文件大小最大为 2GB

一个传输内上传的总文件数上限 100 个。

上传速度

以 1GB 的文件测试,上传速度可以达到 25MB/s 左右。

速度足够了。

下载速度

在 IDM 加持下,下载速度可以达到 40MB/s 左右。

很香了!

保存时长

官方口径是:

非登录状态下每个传输时效性仅为三天;登录状态下,每个传输时效性为7天;

经实测,非登录状态下,上传的文件显示失效时间也是 7 天

其他

分享渠道

支持取件链接分享,取件码分享(24小时内有效),网页二维码分享,微信小程序二维码分享等。

下载限制

为避免商业恶意分发,针对不同的传输的大小设置了不同下载限制。

比如 1GB 的文件,只能下载 25 次。

当超出下载限制后,只能接收方将不能下载,或者付费下载。

这在一定程度上可以避免被当做网盘使用,也契合了它的使用场景,就是临时文件分享。

文叔叔

https://www.wenshushu.cn/

上传限制

单次传输支持多个文件,文件总大小不得超过 5GB

单次上传文件数最大 500

上传速度

上传速度可以达到 70Mb/s 左右

Mb/s,不知道是不是故意的

另外,新文件会校验是否上传过

如果上传过,会直接秒传

下载速度

在 IDM 加持下,下载速度可以达到 40MB/s 左右。

浏览器下载也可以达到2-3MB/s。

保存时长

2 天

其他

分享渠道

支持链接、取件码、二维码分享

使用限制

文叔叔一个很迷的操作

未登录用户(匿名用户),最大可以使用 5GB 存储空间

上传、下载文件都会占用这个空间

如果使用空间超过 5GB,那么将无法继续使用

当然,『破解』方法也很简单,只要清除该网站的缓存数据即可

清楚后,可用的空间会重新变为 5GB

短时间内不能多次上传

AirPortal(空投)

https://airportal.cn/

使用限制

单个文件最大:未登录用户 100 MB,登录后最大为 1GB

未登录用户单个文件只能下载 2 次,登录后可以设置下载次数可以增加为 10

普通用户不支持批量上传

上传速度

测试 1GB 的文件,30s 左右上传成功

30MB/s 的速度

下载速度

IDM 加持下,同样可以得到不限速的效果

保存时间

文件保存时间最大为 24 小时

其他

未登录状态下支持查看历史文件记录

支持传输文本

Box 云U盘

https://www.qsc.zju.edu.cn/box/

Box 云U盘,属于浙大人自己的网络U盘。

浙大的学生门户网站上提供的一个服务。

使用限制

支持批量上传,但单次最多只能上传 20 个文件

并且文件总体积不能超过 100MB

上传速度

上传速度有点感人

一个 100MB 的文件几分钟才传输完成

下载速度

IDM 加持下,10MB/s 左右

远好于上传速度

其他

支持设置文件提取码

可以修改文件有效期,最多可以保存 30 天

其他

还有很多类似功能的网站

但是要么网站访问速度不佳

如:

https://file.pizza

https://www.file.io

https://uplovd.com

要么体验不佳

如:

https://wetransfer.com:需要注册才能使用

https://send-anywhere.com:相当于点对点传输,需要看广告才能继续使用

这里就不再详细测评

大家有兴趣的可以自行试用

总结

以下结果均为未登录状态下测试。


奶牛快传文叔叔AirPortalBox云U盘
上传速度无限制无限制无限制
下载速度无限制无限制无限制无限制
保存时间7天2天1天30天
文件大小2GB5GB100 MB100 MB
下载次数文件越大次数越少,2GB文件最多下载25次空间限制(可破)下载次数2次无限制


文末福利

各位猿们,还在为记不住API发愁吗,哈哈哈,最近发现了国外大师整理了一份Python代码速查表和Pycharm快捷键sheet,火爆国外,这里分享给大家。

这个是一份Python代码速查表

下面的宝藏图片是2张(windows && Mac)高清的PyCharm快捷键一览图

怎样获取呢?可以添加我们的AI派团队的程序媛姐姐

一定要备注【高清图】

????????????????????

➕我们的程序媛小姐姐微信要记得备注【高清图】

来都来了,喜欢的话就请分享点赞在看三连再走吧~~~

本文章已经生成可运行项目
""" 【通义千问 Qwen】API集成模块 用于意图理解和任务处理 """ import json import re import logging import dashscope from dashscope import Generation from database import config from Progress.utils.logger_utils import log_time, log_step, log_var, log_call from Progress.utils.logger_config import setup_logger # --- 初始化日志器 --- logger = logging.getLogger("ai_assistant") DASHSCOPE_API_KEY = config.api_key DASHSCOPE_MODEL = config.model class QWENAssistant: def __init__(self): if not DASHSCOPE_API_KEY: raise ValueError("缺少 DASHSCOPE_API_KEY,请检查配置文件") dashscope.api_key = DASHSCOPE_API_KEY self.model_name = DASHSCOPE_MODEL or 'qwen-max' logger.info(f"✅ QWENAssistant 初始化完成,使用模型: {self.model_name}") self.conversation_history = [] self.system_prompt = """ 你是一个智能语音控制助手,能够理解用户的自然语言指令,并将其转化为可执行的任务计划。 你的职责是: - 准确理解用户意图; - 若涉及多个动作,需拆解为【执行计划】; - 输出一个严格符合规范的 JSON 对象,供系统解析执行; - 所有回复必须使用中文(仅限于 response_to_user 字段); 🎯 输出格式要求(必须遵守): { "intent": "system_control", // 意图类型:"system_control" 或 "chat" "task_type": "start_background_tasks",// 任务类型的简要描述(动态生成) "execution_plan": [ // 执行步骤列表(每个步骤包含 operation, parameters, description) { "operation": "函数名", // 必须是已知操作之一 "parameters": { ... }, // 参数对象(按需提供) "description": "该步骤的目的说明" } ], "response_to_user": "你要对用户说的话(用中文)", "requires_confirmation": false, // 是否需要用户确认后再执行 "mode": "parallel" // 执行模式:"parallel"(并行)或 "serial"(串行) } 📌 已知 operation 列表(不可拼写错误): - play_music(music_path: str) - stop_music() - pause_music() - resume_music() - open_application(app_name: str) - create_file(file_path: str, content?: str) - read_file(file_path: str) - write_file(file_path: str, content: str) - set_reminder(reminder_time: str, message: str) - speak_response(message: str) 📌 规则说明: 1. 只有当用户明确要求执行系统级任务时,才设置 intent="system_control"; 否则设为 intent="chat"(例如闲聊、问天气、讲笑话等)。 2. execution_plan 中的每一步都必须与用户需求直接相关; ❌ 禁止添加无关操作(如随便加 speak_response 或 play_music)! 3. mode 决定执行方式: - 如果各步骤互不依赖 → "parallel" - 如果有先后依赖(如先打开再写入)→ "serial" 4. response_to_user 是你对用户的自然语言反馈,必须简洁友好,使用中文。 5. requires_confirmation: - 涉及删除、覆盖文件、长时间运行任务 → true - 普通操作(打开应用、播放音乐)→ false ⚠️ 重要警告: - 绝不允许照搬示例中的参数或路径!必须根据用户输入提取真实信息。 - 不得虚构不存在的 operation 或 parameter 名称。 - 不得省略任何字段,所有 key 都必须存在。 - 不得输出额外文本(如解释、注释、 ```json ``` 包裹符),只输出纯 JSON 对象。 ✅ 正确行为示例: 用户说:“帮我写一份自我介绍到 D:/intro.txt,并打开看看” → 应返回包含 write_file 和 read_file 的 serial 计划。 用户说:“播放 C:/Music/background.mp3 并告诉我准备好了” → 可以并行执行 play_music 和 speak_response。 用户说:“今天过得怎么样?” → intent="chat",response_to_user="我很好,谢谢!" 🚫 错误行为: - 把所有指令都变成和示例一样的操作组合; - 在没有请求的情况下自动添加 speak_response; - 使用未定义的操作如 run_script、send_email。 现在,请根据用户的最新指令生成对应的 JSON 响应。 """ @log_time @log_step("处理语音指令") def process_voice_command(self, voice_text): log_var("原始输入", voice_text) if not voice_text.strip(): return self._create_fallback_response("我没有听清楚,请重新说话。") self.conversation_history.append({"role": "user", "content": voice_text}) try: messages = [{"role": "system", "content": self.system_prompt}] messages.extend(self.conversation_history[-10:]) # 保留最近上下文 response = Generation.call( model=self.model_name, messages=messages, temperature=0.5, top_p=0.8, max_tokens=1024 ) if response.status_code != 200: logger.error(f"Qwen API 调用失败: {response.status_code}, {response.message}") return self._create_fallback_response(f"服务暂时不可用: {response.message}") ai_output = response.output['text'].strip() log_var("模型输出", ai_output) self.conversation_history.append({"role": "assistant", "content": ai_output}) # === 尝试解析 JSON === parsed = self._extract_and_validate_json(ai_output) if parsed: return parsed else: # 若无法解析为有效计划,则降级为普通对话 return self._create_fallback_response(ai_output) except Exception as e: logger.exception("处理语音指令时发生异常") return self._create_fallback_response("抱歉,我遇到了一些技术问题,请稍后再试。") def _extract_and_validate_json(self, text: str): """从文本中提取 JSON 并验证结构""" try: # 方法1:直接加载 data = json.loads(text) return self._validate_plan_structure(data) except json.JSONDecodeError: pass # 方法2:正则匹配第一个大括号包裹的内容 match = re.search(r'\{[\s\S]*\}', text) if not match: return None try: data = json.loads(match.group()) return self._validate_plan_structure(data) except: return None def _validate_plan_structure(self, data: dict): """验证是否符合多任务计划格式""" required_top_level = ["intent", "task_type", "execution_plan", "response_to_user", "requires_confirmation"] for field in required_top_level: if field not in data: logger.warning(f"缺少必要字段: {field}") return None if not isinstance(data["execution_plan"], list) or len(data["execution_plan"]) == 0: logger.warning("execution_plan 必须是非空数组") return None valid_operations = { "play_music", "stop_music", "pause_music", "resume_music", "open_application", "create_file", "read_file", "write_file", "set_reminder", "speak_response" } for step in data["execution_plan"]: op = step.get("operation") params = step.get("parameters", {}) if not op or op not in valid_operations: logger.warning(f"无效操作: {op}") return None if not isinstance(params, dict): logger.warning(f"parameters 必须是对象: {params}") return None # 补全默认值 if "mode" not in data: data["mode"] = "parallel" return data def _create_fallback_response(self, message: str): """降级响应:用于非结构化输出""" return { "intent": "chat", "task_type": "reply", "response_to_user": message, "requires_confirmation": False, "execution_plan": [], "mode": "serial" } def _create_response(self, intent, action, parameters, response, needs_confirmation): resp = {"intent": intent, "action": action, "parameters": parameters, "response": response, "needs_confirmation": needs_confirmation} log_var("返回响应", resp) return resp @log_time def generate_text(self, prompt, task_type="general"): log_var("任务类型", task_type) log_var("提示词长度", len(prompt)) try: system_prompt = f""" 你是一个专业的文本生成助手。根据用户的要求生成高质量的文本内容。 任务类型:{task_type} 要求:{prompt} 请生成相应的文本内容,确保内容准确、有逻辑、语言流畅。 """ response = Generation.call( model=self.model_name, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ], temperature=0.8, max_tokens=2000 ) if response.status_code == 200: result = response.output['text'] log_var("生成结果长度", len(result)) return result else: error_msg = f"文本生成失败: {response.message}" logger.error(error_msg) return error_msg except Exception as e: logger.exception("文本生成出错") return f"抱歉,生成文本时遇到错误:{str(e)}" @log_time def summarize_text(self, text): log_var("待总结文本长度", len(text)) try: prompt = f"请总结以下文本的主要内容:\n\n{text}" response = Generation.call( model=self.model_name, messages=[{"role": "user", "content": prompt}], temperature=0.3, max_tokens=500 ) if response.status_code == 200: result = response.output['text'] log_var("总结结果长度", len(result)) return result else: error_msg = f"总结失败: {response.message}" logger.error(error_msg) return error_msg except Exception as e: logger.exception("文本总结出错") return f"抱歉,总结文本时遇到错误:{str(e)}" @log_time def translate_text(self, text, target_language="英文"): log_var("目标语言", target_language) log_var("原文长度", len(text)) try: prompt = f"请将以下文本翻译成{target_language}:\n\n{text}" response = Generation.call( model=self.model_name, messages=[{"role": "user", "content": prompt}], temperature=0.3, max_tokens=1000 ) if response.status_code == 200: result = response.output['text'] log_var("翻译结果长度", len(result)) return result else: error_msg = f"翻译失败: {response.message}" logger.error(error_msg) return error_msg except Exception as e: logger.exception("文本翻译出错") return f"抱歉,翻译文本时遇到错误:{str(e)}" """ 【系统控制模块】System Controller 提供音乐播放、文件操作、应用启动、定时提醒等本地系统级功能 """ import inspect import os import subprocess import platform import threading import time from typing import Any, Dict import psutil import pygame from datetime import datetime import logging import schedule from Progress.app.text_to_speech import TTSEngine from Progress.utils.ai_tools import call_llm_to_choose_function from database import config from Progress.utils.ai_tools import FUNCTION_SCHEMA, ai_callable from Progress.utils.logger_utils import log_time, log_step, log_var, log_call from Progress.utils.logger_config import setup_logger RESOURCE_PATH = config.resource_path DEFAULT_MUSIC_PATH = os.path.join(RESOURCE_PATH, config.music_path) DEFAULT_DOCUMENT_PATH = os.path.join(RESOURCE_PATH, config.doc_path) logger = logging.getLogger("ai_assistant") class SystemController: def __init__(self): self.system = platform.system() self.tts_engine = TTSEngine() self.music_player = None self._init_music_player() self.task_counter = 0 self.scheduled_tasks = {} @ai_callable( description="使用语音合成技术播报一段文本回复用户", params={ "message": "要朗读的文本内容" }, intent="response", action="speak" ) @log_step("语音回复用户") @log_time def _speak_response(self, message: str): """ AI 回复用户的语音播报接口 """ if not self.tts_engine.is_available(): logger.warning("🔊 TTS 引擎不可用") return False, "TTS 引擎未就绪" try: logger.info(f"📢 播报: {message}") success = self.tts_engine.speak(message, interrupt=True) return success, "语音已播放" if success else "播放失败" except Exception as e: logger.exception("💥 播报异常") return False, str(e) @log_step("初始化音乐播放器") @log_time def _init_music_player(self): try: pygame.mixer.init() self.music_player = pygame.mixer.music logger.info("✅ 音乐播放器初始化成功") except Exception as e: logger.exception("❌ 音乐播放器初始化失败") self.music_player = None @log_step("播放音乐") @log_time @ai_callable( description="播放音乐文件或指定歌手的歌曲", params={"path": "音乐文件路径", "artist": "歌手名称"}, intent="music", action="play", concurrent=True ) def play_music(self, music_path=None): target_path = music_path or DEFAULT_MUSIC_PATH if not os.path.exists(target_path): msg = f"📁 路径不存在: {target_path}" logger.warning(msg) return False, msg music_files = self._find_music_files(target_path) if not music_files: msg = "🎵 未找到支持的音乐文件" logger.info(msg) return False, msg try: self.stop_music() self.music_player.load(music_files[0]) self.music_player.play(-1) success_msg = f"🎶 正在播放: {os.path.basename(music_files[0])}" logger.info(success_msg) return True, success_msg except Exception as e: logger.exception("💥 播放音乐失败") return False, f"播放失败: {str(e)}" @ai_callable( description="停止当前播放的音乐", params={}, intent="music", action="stop" ) def stop_music(self): try: if self.music_player and pygame.mixer.get_init(): self.music_player.stop() logger.info("⏹️ 音乐已停止") return True, "音乐已停止" except Exception as e: logger.exception("❌ 停止音乐失败") return False, f"停止失败: {str(e)}" @ai_callable( description="暂停当前正在播放的音乐。", params={}, intent="muxic", action="pause" ) def pause_music(self): """暂停音乐""" try: self.music_player.pause() return True, "音乐已暂停" except Exception as e: return False, f"暂停音乐失败: {str(e)}" @ai_callable( description="恢复播放当前正在播放的音乐。", params={}, intent="music", action="resume" ) def resume_music(self): """恢复音乐""" try: self.music_player.unpause() return True, "音乐已恢复" except Exception as e: return False, f"恢复音乐失败: {str(e)}" @ai_callable( description="打开应用程序或浏览器访问网址", params={"app_name": "应用名称(如 记事本、浏览器)", "url": "网页地址"}, intent="system", action="open_app", concurrent=True ) def open_application(self, app_name: str, url: str = None): def _run(): """ AI 调用入口:打开指定应用程序 参数由 AI 解析后传入 """ # === 别名映射表 === alias_map = { # 浏览器相关 "浏览器": "browser", "browser": "browser", "chrome": "browser", "google chrome": "browser", "谷歌浏览器": "browser", "edge": "browser", "firefox": "browser", "safari": "browser", # 文本编辑器 "记事本": "text_editor", "notepad": "text_editor", "text_editer": "text_editor", "文本编辑器": "text_editor", # 文件管理器 "文件管理器": "explorer", "explorer": "explorer", "finder": "explorer", # 计算器 "计算器": "calc", "calc": "calc", "calculator": "calc", # 终端 "终端": "terminal", "terminal": "terminal", "cmd": "terminal", "powershell": "terminal", "shell": "terminal", "命令行": "terminal" } app_key = alias_map.get(app_name.strip()) if not app_key: error_msg = f"🚫 不支持的应用: {app_name}。支持的应用有:浏览器、记事本、计算器、终端、文件管理器等。" logger.warning(error_msg) return False, error_msg try: if app_key == "browser": target_url = url or "https://www.baidu.com" success, msg = self._get_browser_command(target_url) logger.info(f"🌐 {msg}") return success, msg else: # 获取对应命令生成函数 cmd_func_name = f"_get_{app_key}_command" cmd_func = getattr(self, cmd_func_name, None) if not cmd_func: return False, f"❌ 缺少命令生成函数: {cmd_func_name}" cmd = cmd_func() subprocess.Popen(cmd, shell=True) success_msg = f"🚀 已发送指令打开 {app_name}" logger.info(success_msg) return True, success_msg except Exception as e: logger.exception(f"💥 启动应用失败: {app_name}") return False, f"启动失败: {str(e)}" thread = threading.Thread(target=_run,daemon=True) thread.start() return True,f"正在尝试打开{app_name}..." @ai_callable( description="创建一个新文本文件并写入内容", params={"file_path": "文件路径", "content": "要写入的内容"}, intent="file", action="create", concurrent=True ) def create_file(self, file_path, content=""): def _run(): try: os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return True, f"文件已创建: {file_path}" except Exception as e: logger.exception("❌ 创建文件失败") return False, f"创建失败: {str(e)}" thread = threading.Thread(target=_run, daemon=True) thread.start() return True, f"正在尝试创建文件并写入文本..." @ai_callable( description="读取文本文件内容", params={"file_path": "文件路径"}, intent="file", action="read", concurrent=True ) def read_file(self, file_path): def _run(): """读取文件""" try: with open(DEFAULT_DOCUMENT_PATH+file_path, 'r', encoding='utf-8') as f: content = f.read() return True, content except Exception as e: return False, f"读取文件失败: {str(e)}" thread = threading.Thread(target=_run,daemon=True) thread.start() return True,f"正在尝试读取文件..." @ai_callable( description="读取文本文件内容", params={"file_path": "文件路径","content":"写入的内容"}, intent="file", action="write", concurrent=True ) def write_file(self, file_path, content): def _run(): """写入文件""" try: with open(DEFAULT_DOCUMENT_PATH+file_path, 'w', encoding='utf-8') as f: f.write(content) return True, f"文件已保存: {file_path}" except Exception as e: return False, f"写入文件失败: {str(e)}" thread = threading.Thread(target=_run,daemon=True) thread.start() return True,f"正在尝试向{file_path}写入文本..." @ai_callable( description="获取当前系统信息,包括操作系统、CPU、内存等。", params={}, intent="system", action="get_system_info", concurrent=True ) def get_system_info(self): def _run(): """获取系统信息""" try: info = { "操作系统": platform.system(), "系统版本": platform.version(), "处理器": platform.processor(), "内存使用率": f"{psutil.virtual_memory().percent}%", "CPU使用率": f"{psutil.cpu_percent()}%", "磁盘使用率": f"{psutil.disk_usage('/').percent}%" } return True, info except Exception as e: return False, f"获取系统信息失败: {str(e)}" thread = threading.Thread(target=_run,daemon=True) thread.start() return True,f"正在尝试获取系统信息..." @ai_callable( description="设置一个定时提醒", params={"message": "提醒内容", "delay_minutes": "延迟分钟数"}, intent="system", action="set_reminder", concurrent=True ) def set_reminder(self, message, delay_minutes): def _run(): """设置提醒""" try: self.task_counter += 1 task_id = f"reminder_{self.task_counter}" def reminder_job(): print(f"提醒: {message}") # 这里可以添加通知功能 schedule.every(delay_minutes).minutes.do(reminder_job) self.scheduled_tasks[task_id] = { "message": message, "delay": delay_minutes, "created": datetime.now() } return True, f"提醒已设置: {delay_minutes}分钟后提醒 - {message}" except Exception as e: return False, f"设置提醒失败: {str(e)}" thread = threading.Thread(target=_run,daemon=True) thread.start() return True,f"正在设置提醒..." @ai_callable( description="并发执行多个任务", params={"tasks": "任务列表,每个包含operation和arguments"}, intent="system", action="execute_concurrent", concurrent=True ) def _run_parallel_tasks(self, tasks: list): def _run_single(task): op = task.get("operation") args = task.get("arguments",{}) func = getattr(self,op,None) if func and callable(func): try: func(**args) except Exception as e: logger.error(f"执行任务{op}失败:{e}") for task in tasks: thread = threading.Thread(target=_run_single,args=(task,),daemon=True) thread.start() return True,f"已并发执行{len(tasks)}个任务" def run_scheduled_tasks(self): """运行定时任务""" schedule.run_pending() def _find_music_files(self, directory): """查找音乐文件""" music_extensions = ['.mp3', '.wav', '.flac', '.m4a', '.ogg'] music_files = [] try: for root, dirs, files in os.walk(directory): for file in files: if any(file.lower().endswith(ext) for ext in music_extensions): music_files.append(os.path.join(root, file)) except Exception as e: print(f"搜索音乐文件失败: {e}") return music_files def _get_text_editor_command(self): """获取文本编辑器启动命令""" if self.system == "Windows": return "notepad" elif self.system == "Darwin": # macOS return "open -a TextEdit" else: # Linux return "gedit" def _get_explorer_command(self): """获取文件管理器启动命令""" if self.system == "Windows": return "explorer" elif self.system == "Darwin": # macOS return "open -a Finder" else: # Linux return "nautilus" def _get_calc_command(self): """获取计算器启动命令""" if self.system == "Windows": return "calc" elif self.system == "Darwin": # macOS return "open -a Calculator" else: # Linux return "gnome-calculator" def _get_terminal_command(self): """获取终端启动命令""" if self.system == "Windows": return "cmd" elif self.system == "Darwin": # macOS return "open -a Terminal" else: # Linux return "gnome-terminal" def _get_browser_command(self, url="https://www.baidu.com"): try: import webbrowser if webbrowser.open(url): logger.info(f"🌐 已使用默认浏览器打开: {url}") return True, f"正在打开浏览器访问: {url}" else: return False, "无法打开浏览器" except Exception as e: logger.error(f"❌ 浏览器打开异常: {e}") return False, str(e) class TaskOrchestrator: def __init__(self, system_controller): self.system_controller = system_controller self.function_map = self._build_function_map() self.running_scheduled_tasks = False logger.info(f"🔧 任务编排器已加载 {len(self.function_map)} 个可调用函数") def _build_function_map(self) -> Dict[str, callable]: """构建函数名 → 方法对象的映射""" mapping = {} for item in FUNCTION_SCHEMA: func_name = item["name"] func = getattr(self.system_controller, func_name, None) if func and callable(func): mapping[func_name] = func else: logger.warning(f"⚠️ 未找到或不可调用: {func_name}") return mapping def _convert_arg_types(self, func: callable, args: dict) -> dict: """ 尝试将参数转为函数期望的类型(简单启发式) 注意:Python 没有原生参数类型签名,这里做基础转换 """ converted = {} sig = inspect.signature(func) for name, param in sig.parameters.items(): value = args.get(name) if value is None: continue # 简单类型推断(基于默认值) ann = param.annotation if isinstance(ann, type): try: if ann == int and not isinstance(value, int): converted[name] = int(value) elif ann == float and not isinstance(value, float): converted[name] = float(value) else: converted[name] = value except (ValueError, TypeError): converted[name] = value # 保持原始值,让函数自己处理 else: converted[name] = value return converted def _start_scheduled_task_loop(self): """后台线程运行定时任务""" def run_loop(): while self.running_scheduled_tasks: schedule.run_pending() time.sleep(1) if not self.running_scheduled_tasks: self.running_scheduled_tasks = True thread = threading.Thread(target=run_loop, daemon=True) thread.start() logger.info("⏰ 已启动定时任务监听循环") @log_time def execute_from_ai_decision(self, user_input: str) -> Dict[str, Any]: """ 主入口:接收用户输入 → AI 决策 → 执行函数 → 返回结构化结果 支持两种模式: 1. 传统单任务:{"function": "...", "arguments": {...}} 2. 多步骤任务:{"execution_plan": [...], "mode": "parallel|serial"} """ # Step 1: 调用 LLM 获取决策 decision = call_llm_to_choose_function(user_input) if not decision: return { "success": False, "message": "抱歉,我没有理解您的请求。", "data": None } # === 判断是否为多任务计划 === if "execution_plan" in decision and isinstance(decision["execution_plan"], list): return self._execute_task_plan(decision) # === 否则走传统单任务流程 === func_name = decision.get("function") args = decision.get("arguments", {}) if not func_name: return { "success": False, "message": "AI 返回的函数名为空。", "data": None } func = self.function_map.get(func_name) if not func: logger.warning(f"❌ 函数不存在: {func_name}") return { "success": False, "message": f"系统不支持操作:{func_name}", "data": None } try: safe_args = self._convert_arg_types(func, args) except Exception as e: logger.warning(f"参数转换失败: {e}") safe_args = args try: logger.info(f"🚀 正在执行: {func_name}({safe_args})") result = func(**safe_args) if isinstance(result, tuple): success, msg = result elif isinstance(result, dict): success = result.get("success", False) msg = result.get("message", str(result)) else: success = True msg = str(result) # 特殊逻辑:设置提醒后启动调度循环 if func_name == "set_reminder" and success: self._start_scheduled_task_loop() return { "success": success, "message": msg, "data": None, "operation": func_name, "input": args } except TypeError as e: error_msg = f"参数错误,请检查输入格式: {str(e)}" logger.error(error_msg) return { "success": False, "message": error_msg, "data": None } except Exception as e: logger.exception(f"💥 执行 {func_name} 时发生异常") return { "success": False, "message": f"执行失败:{str(e)}", "data": None } @log_step("执行多任务计划") @log_time def _execute_task_plan(self, plan: dict) -> Dict[str, Any]: """ 执行由多个 operation 组成的任务计划 支持 serial / parallel 模式 """ execution_plan = plan.get("execution_plan", []) mode = plan.get("mode", "parallel").lower() response_to_user = plan.get("response_to_user", "任务已提交。") if not execution_plan: return {"success": False, "message": "执行计划为空"} def run_single_step(step: dict): op = step.get("operation") params = step.get("parameters", {}) func = self.function_map.get(op) if not func: logger.warning(f"⚠️ 未知操作: {op}") return False, f"不支持的操作: {op}" try: safe_params = self._convert_arg_types(func, params) result = func(**safe_params) if isinstance(result, tuple): return result return True, str(result) except Exception as e: logger.exception(f"执行 {op} 失败") return False, str(e) threads = [] results = [] if mode == "parallel": for step in execution_plan: t = threading.Thread(target=lambda s=step: results.append(run_single_step(s)), daemon=True) t.start() threads.append(t) for t in threads: t.join() else: # serial for step in execution_plan: res = run_single_step(step) results.append(res) if not res[0]: # 失败则中断 break # 汇总结果 all_success = all(r[0] for r in results) messages = [r[1] for r in results] final_message = response_to_user + " " + " | ".join(messages) if messages else response_to_user return { "success": all_success, "message": final_message.strip(), "data": {"step_results": results}, "operation": "task_plan", "input": plan } import pyttsx3 import threading import queue import time import logging logger = logging.getLogger("ai_assistant") class TTSEngine: def __init__(self): self.engine = None self.is_speaking = False self.speech_queue = queue.Queue() self._lock = threading.Lock() self._worker_thread = None self._stop_event = threading.Event() self._initialize_engine() def _initialize_engine(self): try: self.engine = pyttsx3.init() # 设置语速和音量 self.engine.setProperty('rate', 180) self.engine.setProperty('volume', 0.9) # 尝试设置中文语音 voices = self.engine.getProperty('voices') for v in voices: if 'chinese' in v.name.lower() or 'zh' in v.name or '普通话' in v.name: self.engine.setProperty('voice', v.id) break # 绑定回调 self.engine.connect('started-utterance', self._on_start) self.engine.connect('finished-utterance', self._on_finish) logger.info("✅ TTS 引擎初始化成功") except Exception as e: logger.error(f"❌ 初始化 TTS 失败: {e}") self.engine = None def _on_start(self, name): logger.info(f"▶️ 开始播报: {name}") with self._lock: self.is_speaking = True def _on_finish(self, name, completed): status = "完成" if completed else "中断" logger.info(f"⏹️ 结束播报: {name} ({status})") with self._lock: self.is_speaking = False # 自动处理下一条 self._process_next() def _process_next(self): """尝试从队列取出下一条语音播报""" try: text = self.speech_queue.get_nowait() logger.debug(f"📥 准备播放队列中的语音: {text}") self.engine.say(text, name=text[:50]) self.speech_queue.task_done() except queue.Empty: pass # 队列空了,无需处理 def _event_loop_worker(self): """TTS 专用事件循环线程""" logger.debug("🔊 TTS 工作线程已启动") try: # 🔁 启动非阻塞事件循环 self.engine.startLoop(False) while not self._stop_event.is_set(): if not self.is_speaking and not self.speech_queue.empty(): try: text = self.speech_queue.get(timeout=0.5) self.engine.say(text, name=text[:50]) # 触发 started-utterance self.speech_queue.task_done() except queue.Empty: continue else: time.sleep(0.1) # 避免 CPU 占用过高 # 显式关闭事件循环 self.engine.endLoop() logger.debug("🔚 TTS 事件循环已退出") except Exception as e: logger.exception(f"💥 TTS 线程异常: {e}") self.engine.endLoop() def speak(self, text: str, interrupt: bool = True): """添加语音到播报队列""" if not self.engine or not text.strip(): return False cleaned = text.strip() with self._lock: if interrupt and self.is_speaking: self.stop_current_speech() self.speech_queue.put(cleaned) if not self._worker_thread or not self._worker_thread.is_alive(): self._stop_event.clear() self._worker_thread = threading.Thread(target=self._event_loop_worker, daemon=True) self._worker_thread.start() return True def stop_current_speech(self): """停止当前语音播报""" with self._lock: if self.is_speaking and self.engine: self.engine.stop() # 会触发 finished-utterance 回调 # 清空队列 while not self.speech_queue.empty(): try: self.speech_queue.get_nowait() self.speech_queue.task_done() except queue.Empty: break def is_available(self): return self.engine is not None and not self._stop_event.is_set() """ 【语音识别模块】Speech Recognition (Offline) 使用麦克风进行实时语音识别,基于 Vosk 离线模型 支持单次识别 & 持续监听模式 音量可视化、模型路径检查、资源安全释放 """ import threading import time import logging import json import numpy as np import os from vosk import Model, KaldiRecognizer import pyaudio from database import config from Progress.utils.logger_utils import log_time, log_step, log_var, log_call from Progress.utils.logger_config import setup_logger # --- 配置参数 --- VOICE_TIMEOUT = config.timeout # 最大等待语音输入时间(秒) VOICE_PHRASE_TIMEOUT = config.phrase_timeout # 单句话最长录音时间 VOSK_MODEL_PATH = "./vosk-model-small-cn-0.22" # 注意标准命名是 zh-cn # --- 初始化日志器 --- logger = logging.getLogger("ai_assistant") class SpeechRecognizer: def __init__(self): self.model = None self.recognizer = None self.audio = None self.is_listening = False self.callback = None # 用户注册的回调函数:callback(text) self._last_text = "" self._listen_thread = None self.sample_rate = 16000 # Vosk 要求采样率 16kHz self.chunk_size = 1600 # 推荐帧大小(对应 ~100ms) self._load_model() self._init_audio_system() @log_step("加载 Vosk 离线模型") @log_time def _load_model(self): """加载本地 Vosk 模型""" if not os.path.exists(VOSK_MODEL_PATH): raise FileNotFoundError(f"❌ Vosk 模型路径不存在: {VOSK_MODEL_PATH}\n" "请从 https://alphacephei.com/vosk/models 下载中文小模型并解压至此路径") try: logger.info(f"📦 正在加载模型: {VOSK_MODEL_PATH}") self.model = Model(VOSK_MODEL_PATH) log_call("✅ 模型加载成功") except Exception as e: logger.critical(f"🔴 加载 Vosk 模型失败: {e}") raise RuntimeError("Failed to load Vosk model") from e @log_step("初始化音频系统") @log_time def _init_audio_system(self): """初始化 PyAudio 并创建全局 recognizer""" try: self.audio = pyaudio.PyAudio() # 创建默认识别器(可在每次识别前 Reset) self.recognizer = KaldiRecognizer(self.model, self.sample_rate) logger.debug("✅ 音频系统初始化完成") except Exception as e: logger.exception("❌ 初始化音频系统失败") raise @property def last_text(self) -> str: return self._last_text def is_available(self) -> bool: """检查麦克风是否可用""" if not self.audio: return False try: stream = self.audio.open( format=pyaudio.paInt16, channels=1, rate=self.sample_rate, input=True, frames_per_buffer=self.chunk_size ) stream.close() return True except Exception as e: logger.error(f"🔴 麦克风不可用或无权限: {e}") return False @log_step("执行单次语音识别") @log_time def listen_and_recognize(self, timeout=None) -> str: """ 单次语音识别:阻塞直到识别完成或超时 """ timeout = timeout or VOICE_TIMEOUT start_time = time.time() in_speech = False result_text = "" logger.debug(f"🎙️ 开始单次语音识别 (timeout={timeout:.1f}s)...") logger.info("🔊 请说话...") stream = None try: # 打开音频流 stream = self.audio.open( format=pyaudio.paInt16, channels=1, rate=self.sample_rate, input=True, frames_per_buffer=self.chunk_size ) # 重置识别器状态 self.recognizer.Reset() while (time.time() - start_time) < timeout: data = stream.read(self.chunk_size, exception_on_overflow=False) # 分析音量(可视化反馈) audio_np = np.frombuffer(data, dtype=np.int16) volume = np.abs(audio_np).mean() bar = "█" * int(volume // 20) logger.debug(f"📊 音量: {volume:5.1f} |{bar:10}|") # 将音频送入 Vosk if self.recognizer.AcceptWaveform(data): final_result = json.loads(self.recognizer.Result()) text = final_result.get("text", "").strip() if text: result_text = text break else: partial = json.loads(self.recognizer.PartialResult()) partial_text = partial.get("partial", "") if partial_text.strip(): in_speech = True # 标记已经开始说话 # 如果还没开始说话,则允许超时;否则继续等待说完 if not in_speech and (time.time() - start_time) > timeout: logger.info("💤 超时未检测到语音输入") break if result_text: self._last_text = result_text logger.info(f"🎯 识别结果: '{result_text}'") return result_text else: logger.info("❓ 未识别到有效内容") self._last_text = "" return "" except Exception as e: logger.exception("🔴 执行单次语音识别时发生异常") self._last_text = "" return "" finally: # 确保资源释放 if stream: try: stream.stop_stream() stream.close() except Exception as e: logger.warning(f"⚠️ 关闭音频流失败: {e}") @log_step("启动持续语音监听") def start_listening(self, callback=None, language=None): """ 启动后台线程持续监听语音输入 :param callback: 回调函数,接受一个字符串参数 text :param language: 语言代码(忽略,由模型决定) """ if self.is_listening: logger.warning("⚠️ 已在监听中,忽略重复启动") return if not callable(callback): logger.error("🔴 回调函数无效,请传入可调用对象") return self.callback = callback self.is_listening = True self._listen_thread = threading.Thread(target=self._background_listen, args=(language,), daemon=True) self._listen_thread.start() logger.info("🟢 已启动后台语音监听") @log_step("停止语音监听") def stop_listening(self): """安全停止后台监听""" if not self.is_listening: return self.is_listening = False logger.info("🛑 正在停止语音监听...") if self._listen_thread and self._listen_thread != threading.current_thread(): self._listen_thread.join(timeout=3) if self._listen_thread.is_alive(): logger.warning("🟡 监听线程未能及时退出(可能阻塞)") elif self._listen_thread == threading.current_thread(): logger.error("❌ 无法在当前线程中 join 自己!请检查调用栈") else: logger.debug("No thread to join") logger.info("✅ 语音监听已停止") @log_time def _background_listen(self, language=None): """后台循环监听线程""" logger.debug("🎧 后台监听线程已启动") stream = None try: stream = self.audio.open( format=pyaudio.paInt16, channels=1, rate=self.sample_rate, input=True, frames_per_buffer=self.chunk_size ) except Exception as e: logger.error(f"🔴 无法打开音频流: {e}") return try: while self.is_listening: try: data = stream.read(self.chunk_size, exception_on_overflow=False) # 关键:先送入数据 if self.recognizer.AcceptWaveform(data): # 已完成一句话识别 result_json = self.recognizer.Result() result_dict = json.loads(result_json) text = result_dict.get("text", "").strip() if text and self.callback: logger.info(f"🔔 回调触发: '{text}'") self.callback(text) # 🟢 完成后立即重置识别器状态 self.recognizer.Reset() else: # 获取部分结果(可用于实时显示) partial = json.loads(self.recognizer.PartialResult()) partial_text = partial.get("partial", "") if partial_text.strip(): logger.debug(f"🗣️ 当前语音片段: '{partial_text}'") except Exception as e: logger.exception("Background listening error") time.sleep(0.05) # 小延迟降低 CPU 占用 finally: if stream: stream.stop_stream() stream.close() logger.debug("🔚 后台监听线程退出") from functools import wraps import inspect import logging from Progress.app.qwen_assistant import QWENAssistant # 全局注册表 REGISTERED_FUNCTIONS = {} FUNCTION_SCHEMA = [] FUNCTION_MAP = {} # (intent, action) -> method_name _qwen_assistant = None logger = logging.getLogger("ai_assistant") def ai_callable( *, description: str, params: dict, intent: str = None, action: str = None, concurrent: bool = False # 新增:是否允许并发执行 ): def decorator(func): func_name = func.__name__ metadata = { "func": func, "description": description, "params": params, "intent": intent, "action": action, "signature": str(inspect.signature(func)), "concurrent": concurrent # 记录是否可并发 } REGISTERED_FUNCTIONS[func_name] = metadata FUNCTION_SCHEMA.append({ "name": func_name, "description": description, "parameters": params }) if intent and action: key = (intent, action) if key in FUNCTION_MAP: raise ValueError(f"冲突:语义 ({intent}, {action}) 已被函数 {FUNCTION_MAP[key]} 占用") FUNCTION_MAP[key] = func_name @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper._ai_metadata = metadata return wrapper return decorator def get_qwen_assistant(): global _qwen_assistant if _qwen_assistant is None: _qwen_assistant = QWENAssistant() return _qwen_assistant def call_llm_to_choose_function(user_query: str) -> dict: """ 使用 Qwen 助手理解用户意图,并返回完整的执行计划或单任务指令。 返回可能是: - 单任务: {"function": "...", "arguments": {...}} - 多任务: {"execution_plan": [...], "mode": "...", "response_to_user": "..."} """ assistant = get_qwen_assistant() try: ai_response = assistant.process_voice_command(user_query) # 如果是聊天类,直接回复 if ai_response.get("intent") == "chat": return { "function": "_speak_response", "arguments": {"message": ai_response.get("response_to_user", "好的")} } # 如果是文本生成任务 if ai_response.get("intent") == "text": return { "function": "_generate_text_task", "arguments": { "task_type": ai_response.get("action", "general"), "prompt": user_query } } # 如果是多任务计划 if "execution_plan" in ai_response and ai_response["execution_plan"]: return ai_response # 直接透传给 orchestrator 处理 # 尝试从 FUNCTION_MAP 匹配单任务 intent = ai_response.get("intent") action = ai_response.get("action") params = ai_response.get("parameters", {}) func_name = FUNCTION_MAP.get((intent, action)) if func_name and func_name in REGISTERED_FUNCTIONS: return { "function": func_name, "arguments": params } # 默认兜底 return { "function": "_speak_response", "arguments": {"message": ai_response.get("response_to_user", "抱歉,我无法完成这项操作。")} } except Exception as e: logger.exception("❌ AI 决策出错") return {} """ 【AI语音助手】主程序入口 集成语音识别、Qwen 意图理解、TTS 与动作执行 ✅ 已修复:不再访问 _last_text 私有字段 ✅ 增强:异常防护、类型提示、唤醒词预留接口 """ import sys import time import logging # --- 导入日志工具 --- from Progress.utils.logger_config import setup_logger from Progress.utils.logger_utils import log_time, log_step, log_var, log_call # --- 显式导入各模块核心类 --- from Progress.app.voice_recognizer import SpeechRecognizer from Progress.app.qwen_assistant import QWENAssistant from Progress.app.text_to_speech import TTSEngine from Progress.app.system_controller import SystemController, TaskOrchestrator from database import config # --- 初始化全局日志器 --- logger = logging.getLogger("ai_assistant") @log_step("初始化语音识别模块") @log_time def initialize_speech_recognizer() -> SpeechRecognizer: try: recognizer = SpeechRecognizer() if not recognizer.is_available(): raise RuntimeError("麦克风不可用,请检查设备连接和权限") log_call("✅ 语音识别器初始化完成") return recognizer except Exception as e: logger.critical(f"🔴 初始化语音识别失败: {e}") raise @log_step("初始化 AI 助手模块") @log_time def initialize_qwen_assistant() -> QWENAssistant: try: assistant = QWENAssistant() log_call("✅ Qwen 助手初始化完成") return assistant except Exception as e: logger.critical(f"🔴 初始化 Qwen 助手失败: {e}") raise @log_step("初始化文本转语音模块") @log_time def initialize_tts_engine() -> TTSEngine: try: tts_engine = TTSEngine() if not tts_engine.is_available(): raise RuntimeError("TTS引擎初始化失败") log_call("✅ TTS 引擎初始化完成") return tts_engine except Exception as e: logger.critical(f"🔴 初始化 TTS 失败: {e}") raise @log_step("初始化动作执行器") @log_time def initialize_action_executor() -> TaskOrchestrator: system_controller = SystemController() task_orchestrator = TaskOrchestrator(system_controller=system_controller) log_call("✅ 动作执行器初始化完成") return task_orchestrator @log_step("安全执行单次交互") @log_time def handle_single_interaction_safe( recognizer: SpeechRecognizer, assistant: QWENAssistant, tts_engine: TTSEngine, executor: TaskOrchestrator ): try: handle_single_interaction(recognizer, assistant, tts_engine, executor) except Exception as e: logger.exception("⚠️ 单次交互过程中发生异常,已降级处理") error_msg = "抱歉,我在处理刚才的操作时遇到了一点问题。" logger.info(f"🗣️ 回复: {error_msg}") tts_engine.speak(error_msg) @log_step("处理一次语音交互") @log_time def handle_single_interaction( recognizer: SpeechRecognizer, assistant: QWENAssistant, tts_engine: TTSEngine, executor: TaskOrchestrator ): """ 单次完整交互:听 -> 识别 -> AI 决策 -> 执行 -> 回复 """ # 1. 听 text = recognizer.listen_and_recognize(timeout=5) if not text: logger.info("🔇 未检测到有效语音") return logger.info(f"🗣️ 用户说: '{text}'") # 2. AI 决策 + 执行(一体化由 TaskOrchestrator 完成) # 注意:这里不再需要单独调用 assistant.think_and_decide() # 因为现在的 execute_from_ai_decision 内部已经集成了 LLM 调用 result = executor.execute_from_ai_decision(text) # 3. 构造回复语句 if result["success"]: ai_reply = str(result["message"]) logger.info(f"✅ 操作成功: {result['operation']} -> {ai_reply}") else: error_msg = result["message"] ai_reply = f"抱歉,{error_msg if '抱歉' not in error_msg else error_msg[3:]}" logger.warning(f"❌ 执行失败: {error_msg}") # 4. 说 logger.info(f"🤖 回复: {ai_reply}") tts_engine.speak(ai_reply) @log_step("启动 AI 语音助手") @log_time def main(): logger.info("🚀 正在启动 AI 语音助手系统...") try: recognizer = initialize_speech_recognizer() assistant = initialize_qwen_assistant() tts_engine = initialize_tts_engine() executor = initialize_action_executor() log_call("✅ 所有模块初始化完成,进入监听循环") log_call("\n" + "—" * 50) log_call("🎙️ 语音助手已就绪") log_call("💡 说出你的命令,例如:'打开浏览器'、'写一篇春天的文章'") log_call("🛑 说出‘退出’、‘关闭’、‘停止’或‘拜拜’来结束程序") log_call("—" * 50 + "\n") while True: try: handle_single_interaction(recognizer, assistant, tts_engine, executor) except KeyboardInterrupt: logger.info("🛑 用户主动中断 (Ctrl+C),准备退出...") raise # 让 main 捕获并退出 except Exception as e: logger.exception("⚠️ 单次交互过程中发生异常,已降级处理") error_msg = "抱歉,我在处理刚才的操作时遇到了一点问题。" logger.info(f"🗣️ 回复: {error_msg}") tts_engine.speak(error_msg) last_text = recognizer.last_text.lower() exit_keywords = ['退出', '关闭', '停止', '拜拜', '再见'] if any(word in last_text for word in exit_keywords): logger.info("🎯 用户请求退出,程序即将终止") break time.sleep(0.5) logger.info("👋 语音助手已安全退出") except KeyboardInterrupt: logger.info("🛑 用户通过 Ctrl+C 中断程序") print("\n👋 再见!") except Exception as e: logger.exception("❌ 主程序运行时发生未预期异常") print(f"\n🚨 程序异常终止:{e}") sys.exit(1) if __name__ == "__main__": if not logging.getLogger().handlers: setup_logger(name="ai_assistant", log_dir="logs", level=logging.INFO) main() 这里有一个严重问题,播放音乐并不会生成多任务,我需要在生成任何一个任务时自动附加一个语音播报
10-24
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值