Merge pull request '增加运维机器人单元' (#8) from main into dev_qing
Reviewed-on: qing/micro_servers#8
This commit is contained in:
commit
f3909bb18f
42 files changed
+20469
No files matched your search
@@ -0,0 +1,108 @@
|
||||
#PRJ
|
||||
.arts/
|
||||
skbin/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 卿松
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+649
@@ -0,0 +1,649 @@
|
||||
# 单AI智能体系统 - WebSocket版本(增强版)
|
||||
|
||||
一个功能强大的AI智能体系统,通过WebSocket与客户端交互,支持文件操作、网页访问、记忆管理、任务调度、扩展技能等功能。所有任务都通过AI对话执行,支持实时流式输出和任务状态广播。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
- 🤖 **单AI智能体**:基于DeepSeek API的智能对话系统,支持思考模式
|
||||
- 🔌 **WebSocket通信**:实时双向通信,支持流式输出和任务状态广播
|
||||
- 📁 **文件操作**:读写文件、目录列表(支持大文件分页)
|
||||
- 🌐 **网页访问**:获取网页内容、提取标题、链接、搜索信息
|
||||
- 🧠 **记忆管理**:自动保存和检索对话记忆,保持对话连贯性
|
||||
- ⏰ **任务系统**:支持一次性、重复(daily/weekly/monthly/custom)、即时任务,自动触发AI对话
|
||||
- 🔧 **扩展技能**:动态加载`~/.aibox/skills/`目录下的可执行脚本或Markdown技能文件
|
||||
- 📦 **对话压缩**:当消息数或Token数达到阈值时自动压缩,保持上下文质量
|
||||
- 🌍 **多语言支持**:自动检测系统语言,支持中英文
|
||||
- 🧬 **智能体自我进化**:通过`update_soul`工具动态更新角色定位和性格定义
|
||||
- 🔄 **技能热重载**:无需重启即可添加、修改或删除扩展技能
|
||||
- 💾 **对话管理**:历史对话归档、开始新对话(重置AI记忆)
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Python 3.8+
|
||||
- DeepSeek API Key
|
||||
- 操作系统:Linux/macOS/Windows
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 克隆或下载代码
|
||||
# 安装依赖
|
||||
pip install websockets requests beautifulsoup4
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
1. **设置DeepSeek API Key**:
|
||||
```bash
|
||||
export DEEPSEEK_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
2. **(可选)配置文件**:`~/.aibox/config.json`(系统会自动创建默认配置)
|
||||
|
||||
3. **启动服务器**:
|
||||
```bash
|
||||
python agent.py
|
||||
```
|
||||
|
||||
启动后,WebSocket服务器将运行在 `ws://localhost:8765`
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
~/.aibox/
|
||||
├── config.json # 配置文件
|
||||
├── prompts/ # 系统提示词目录
|
||||
│ ├── 00_IDENTITY.md # 用户身份信息
|
||||
│ ├── 1_*.md # 基础技能提示词(文件操作、网页访问等)
|
||||
│ ├── 99_SOUL.md # AI灵魂定义(角色、性格、能力边界)
|
||||
│ └── 100_STATUS.md # AI状态记录
|
||||
├── skills/ # 扩展技能目录
|
||||
│ ├── my_skill # 可执行技能(无扩展名)
|
||||
│ └── guide.md # Markdown技能
|
||||
├── memories.db # 记忆和任务数据库
|
||||
├── current_conversation.json # 当前对话
|
||||
├── conversation_history.json # 对话索引
|
||||
├── archive/ # 历史对话归档
|
||||
└── tool_results/ # 工具结果缓存
|
||||
```
|
||||
|
||||
## 🔧 可用工具
|
||||
|
||||
AI智能体可以调用以下工具来完成任务:
|
||||
|
||||
| 工具名称 | 功能描述 |
|
||||
|---------|---------|
|
||||
| `read_file` | 读取文件内容(支持分页、head/tail) |
|
||||
| `write_file` | 写入或追加文件 |
|
||||
| `list_files` | 列出目录内容 |
|
||||
| `web` | 访问网页、获取标题/文本/链接 |
|
||||
| `memory` | 保存、搜索(支持分页)、删除记忆 |
|
||||
| `memo` | 创建、查看、完成、搜索任务 |
|
||||
| `run_command` | 执行系统命令(实时输出,支持超时控制) |
|
||||
| `search_skills` | 搜索扩展技能(支持Markdown和可执行技能) |
|
||||
| `reload_skills` | 热重载扩展技能 |
|
||||
| `create_skill` | 创建新技能(AI自主编写Python脚本) |
|
||||
| `install_skill` | 技能安装指引 |
|
||||
| `update_identity` | 更新用户身份信息 |
|
||||
| `update_soul` | 更新AI自我定义(智能体进化) |
|
||||
| `save_memory_and_end_conversation` | 保存记忆并结束对话 |
|
||||
| `get_current_time` | 获取当前时间 |
|
||||
| `calculator` | 数学计算 |
|
||||
|
||||
## 💬 WebSocket API
|
||||
|
||||
### 连接
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:8765');
|
||||
```
|
||||
|
||||
### 发送消息格式
|
||||
|
||||
#### 1. 普通消息
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"content": "你好,请帮我..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 命令
|
||||
```json
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/new"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 任务操作
|
||||
```json
|
||||
{
|
||||
"type": "task",
|
||||
"action": "add",
|
||||
"title": "提醒我喝水",
|
||||
"content": "下午3点喝水",
|
||||
"delay": 3600,
|
||||
"repeat_type": "daily",
|
||||
"is_immediate": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Ping
|
||||
```json
|
||||
{
|
||||
"type": "ping"
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的命令
|
||||
|
||||
| 命令 | 功能 |
|
||||
|-----|------|
|
||||
| `/exit` | 退出连接 |
|
||||
| `/new` | 开始新对话(保存当前,重置AI记忆,重新加载提示词) |
|
||||
| `/list` | 列出所有历史对话 |
|
||||
| `/memories` | 查看最近记忆(支持分页) |
|
||||
| `/memos` | 查看待办任务 |
|
||||
| `/search <关键词>` | 搜索记忆 |
|
||||
| `/config` | 查看配置 |
|
||||
| `/reload` | 重载配置 |
|
||||
| `/reload_prompts` | 重载提示词 |
|
||||
| `/check` | 检查过期任务 |
|
||||
| `/tasks` | 查看任务调度器状态 |
|
||||
| `/help` | 显示帮助 |
|
||||
|
||||
### 接收消息类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|-----|------|
|
||||
| `chunk` | AI响应的片段(流式输出) |
|
||||
| `complete` | AI响应完成 |
|
||||
| `reasoning` | AI思考过程(调试模式) |
|
||||
| `error` | 错误信息 |
|
||||
| `command_result` | 命令执行结果 |
|
||||
| `task_status` | 任务状态更新(executing/complete/failed) |
|
||||
| `task_process` | 任务执行过程中的AI输出 |
|
||||
| `dialogue_ended` | 对话结束通知 |
|
||||
| `dialogue_reset` | 对话重置完成 |
|
||||
| `info` | 系统信息 |
|
||||
| `pong` | Ping响应 |
|
||||
|
||||
## ⏰ 任务系统
|
||||
|
||||
### 任务类型
|
||||
|
||||
1. **一次性任务**:在指定时间执行一次
|
||||
2. **重复任务**:
|
||||
- `daily`:每天执行
|
||||
- `weekly`:每周执行
|
||||
- `monthly`:每月执行
|
||||
- `custom`:自定义间隔(分钟/小时/天)
|
||||
3. **即时任务**:立即执行(`is_immediate: true`)
|
||||
|
||||
### 添加任务示例
|
||||
|
||||
```json
|
||||
// 每天提醒
|
||||
{
|
||||
"type": "task",
|
||||
"action": "add",
|
||||
"title": "每日健康提醒",
|
||||
"repeat_type": "daily"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// 5分钟后执行
|
||||
{
|
||||
"type": "task",
|
||||
"action": "add",
|
||||
"title": "5分钟后提醒",
|
||||
"delay": 300
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// 每30分钟重复
|
||||
{
|
||||
"type": "task",
|
||||
"action": "add",
|
||||
"title": "定时检查",
|
||||
"repeat_type": "custom",
|
||||
"repeat_interval": 30,
|
||||
"repeat_interval_unit": "minutes"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// 即时任务
|
||||
{
|
||||
"type": "task",
|
||||
"action": "add",
|
||||
"title": "检查服务器状态",
|
||||
"is_immediate": true
|
||||
}
|
||||
```
|
||||
|
||||
### 任务执行流程
|
||||
|
||||
1. 任务到期后,系统自动标记为已触发
|
||||
2. 创建新的AI对话,将任务内容作为用户输入
|
||||
3. AI处理任务,使用工具执行操作
|
||||
4. 任务执行结果实时广播到所有WebSocket客户端
|
||||
5. 重复任务自动计算下一次执行时间
|
||||
|
||||
## 🔧 扩展技能
|
||||
|
||||
### 技能类型
|
||||
|
||||
#### 1. 可执行技能
|
||||
- 放在 `~/.aibox/skills/` 目录
|
||||
- 文件必须可执行(`chmod +x`)
|
||||
- 支持 `--help` 选项
|
||||
- `--help` 第一行为一句话简介,其余为使用说明
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="温度转换工具 - 摄氏度转华氏度"
|
||||
)
|
||||
parser.add_argument("-t", "--temp", type=float, required=True,
|
||||
help="摄氏温度值")
|
||||
args = parser.parse_args()
|
||||
|
||||
fahrenheit = args.temp * 9/5 + 32
|
||||
print(f"{args.temp}°C = {fahrenheit:.1f}°F")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
#### 2. Markdown技能
|
||||
- 文件以 `.md` 结尾
|
||||
- 第一行为简介(`# 标题` 或纯文本)
|
||||
- 其余为完整的技能内容
|
||||
|
||||
**示例**:
|
||||
```markdown
|
||||
# 代码审查指南
|
||||
提供代码审查的检查清单和最佳实践...
|
||||
|
||||
## 检查要点
|
||||
1. 代码风格一致性
|
||||
2. 性能优化建议
|
||||
3. 安全漏洞检查
|
||||
4. 可维护性评估
|
||||
```
|
||||
|
||||
### 创建技能
|
||||
|
||||
#### 方式1:AI自主创建(推荐)
|
||||
直接告诉AI:
|
||||
```
|
||||
创建一个技能,功能是将摄氏度转换为华氏度,接受 -c 参数输入温度
|
||||
```
|
||||
AI会自动使用 `create_skill` 工具编写脚本并部署。
|
||||
|
||||
#### 方式2:手动创建
|
||||
1. 编写脚本,确保支持 `--help`
|
||||
2. 复制到 `~/.aibox/skills/` 并去掉扩展名
|
||||
3. 添加可执行权限
|
||||
4. 调用 `reload_skills` 工具
|
||||
|
||||
```bash
|
||||
cp my_skill.py ~/.aibox/skills/my_skill
|
||||
chmod +x ~/.aibox/skills/my_skill
|
||||
```
|
||||
|
||||
#### 方式3:使用安装指引
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"content": "使用 install_skill 工具安装 /path/to/script.py"
|
||||
}
|
||||
```
|
||||
|
||||
## 🧠 智能体自我进化
|
||||
|
||||
AI可以通过 `update_soul` 工具动态更新 `99_SOUL.md` 文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"content": "使用 update_soul 更新我的角色,强调我更注重创造性思维"
|
||||
}
|
||||
```
|
||||
|
||||
这允许AI在对话中不断优化自己的:
|
||||
- 角色定位
|
||||
- 性格特点
|
||||
- 能力边界
|
||||
- 行为准则
|
||||
- 价值观
|
||||
|
||||
## 📦 对话压缩
|
||||
|
||||
当对话达到阈值时自动压缩:
|
||||
|
||||
```json
|
||||
// 配置示例
|
||||
"context": {
|
||||
"max_history_messages": 500,
|
||||
"compress_enabled": true,
|
||||
"compress_message_threshold": 200, // 消息数阈值
|
||||
"compress_token_threshold": 120000, // Token数阈值
|
||||
"compress_keep_recent": 8 // 保留最近消息数
|
||||
}
|
||||
```
|
||||
|
||||
压缩过程:
|
||||
1. 检测到消息数或Token数超过阈值
|
||||
2. 调用AI压缩早期对话内容
|
||||
3. 保留最近的重要消息
|
||||
4. 将压缩摘要作为系统消息
|
||||
5. 压缩过程静默执行,不影响用户体验
|
||||
|
||||
## 🧩 提示词系统
|
||||
|
||||
提示词文件位于 `~/.aibox/prompts/`:
|
||||
|
||||
| 文件 | 用途 | 更新方式 |
|
||||
|-----|------|---------|
|
||||
| `00_IDENTITY.md` | 用户身份信息 | AI通过 `update_identity` 工具更新 |
|
||||
| `1_*.md` | 基础技能提示词 | 手动编辑,AI通过 `reload_prompts` 重载 |
|
||||
| `99_SOUL.md` | AI灵魂定义(角色、性格) | AI通过 `update_soul` 工具更新 |
|
||||
| `100_STATUS.md` | AI状态记录 | AI通过 `save_memory_and_end_conversation` 更新 |
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
主要配置项(`~/.aibox/config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"system": {
|
||||
"save_dir": "~/.aibox",
|
||||
"websocket_host": "localhost",
|
||||
"websocket_port": 8765,
|
||||
"debug_level": 1,
|
||||
"log_file": "~/.aibox/ai.log"
|
||||
},
|
||||
"context": {
|
||||
"max_history_messages": 500,
|
||||
"compress_enabled": true,
|
||||
"compress_message_threshold": 200,
|
||||
"compress_token_threshold": 120000,
|
||||
"compress_keep_recent": 8
|
||||
},
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"check_interval": 1.0,
|
||||
"max_tasks_per_run": 10
|
||||
},
|
||||
"api": {
|
||||
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
||||
"deepseek_model": "deepseek-chat",
|
||||
"api_key_env": "DEEPSEEK_API_KEY"
|
||||
},
|
||||
"memory": {
|
||||
"max_search_results": 100
|
||||
},
|
||||
"tools": {
|
||||
"max_result_length": 10000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ 命令行参数
|
||||
|
||||
```bash
|
||||
python agent.py [选项]
|
||||
|
||||
选项:
|
||||
--verbose, -v 显示详细日志
|
||||
--debug LEVEL 调试级别 (0-3)
|
||||
--lang zh|en 强制语言
|
||||
--host HOST WebSocket主机
|
||||
--port PORT WebSocket端口
|
||||
--no-scheduler 禁用任务调度器
|
||||
--scheduler-interval 调度器检查间隔(秒)
|
||||
--config PATH 指定配置文件路径
|
||||
--check-memo 检查到期任务并退出
|
||||
--memo-add "标题|内容|时间" 添加任务
|
||||
--memo-list 列出任务
|
||||
--memo-complete ID 完成任务
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 启动服务器(详细日志)
|
||||
python agent.py --verbose
|
||||
|
||||
# 指定端口
|
||||
python agent.py --port 9000
|
||||
|
||||
# 检查任务
|
||||
python agent.py --check-memo
|
||||
|
||||
# 添加任务(命令行)
|
||||
python agent.py --memo-add "提醒开会|下午3点会议室A|+1h"
|
||||
|
||||
# 查看所有任务
|
||||
python agent.py --memo-list
|
||||
```
|
||||
|
||||
## 🔄 架构流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ WebSocket 客户端 │
|
||||
└─────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────────┐
|
||||
│ WebSocket 服务器 (端口 8765) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
|
||||
│ │ 消息处理器 │ │ 命令处理器 │ │ 任务调度器 │ │
|
||||
│ └─────────────┘ └─────────────┘ └───────────────┘ │
|
||||
└─────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────────┐
|
||||
│ AI 对话引擎 (DeepSeek) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌───────────┐ ┌───────────┐ ┌──────────────────┐ │
|
||||
│ │ 提示词管理器│ │ 工具注册器 │ │ 对话压缩器 │ │
|
||||
│ └───────────┘ └───────────┘ └──────────────────┘ │
|
||||
└─────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────▼───────────────────────────────────────┐
|
||||
│ 数据持久层 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │
|
||||
│ │ 记忆数据库 │ │ 备忘录DB │ │ 对话历史管理器 │ │
|
||||
│ └──────────┘ └──────────┘ └────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 📊 消息流示例
|
||||
|
||||
### 普通对话流程
|
||||
```
|
||||
Client -> Server: {type: "message", content: "帮我查天气"}
|
||||
Server -> Client: {type: "chunk", content: "正在查"}
|
||||
Server -> Client: {type: "chunk", content: "询天气..."}
|
||||
Server -> Client: {type: "tool_call", tool: "search_skills", args: {keyword: "天气"}}
|
||||
Server -> Client: {type: "chunk", content: "今日天气晴朗..."}
|
||||
Server -> Client: {type: "complete", content: "今日天气晴朗,温度25°C"}
|
||||
```
|
||||
|
||||
### 任务执行流程
|
||||
```
|
||||
Client -> Server: {type: "task", action: "add", title: "提醒", delay: 60}
|
||||
Server -> Client: {type: "task_added", task_id: 1}
|
||||
|
||||
... 60秒后 ...
|
||||
|
||||
Server -> Client: {type: "task_status", task_id: 1, status: "executing"}
|
||||
Server -> Client: {type: "task_process", task_id: "1", content: "处理中..."}
|
||||
Server -> Client: {type: "task_status", task_id: 1, status: "complete"}
|
||||
```
|
||||
|
||||
### 对话重置流程
|
||||
```
|
||||
Client -> Server: {type: "command", command: "/new"}
|
||||
Server -> Client: {type: "command_result", content: "🔄 开始新对话..."}
|
||||
Server -> Client: {type: "dialogue_reset", content: "✅ 对话状态已完全重置"}
|
||||
```
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: API Key无效怎么办?
|
||||
**A:**
|
||||
1. 确认环境变量已设置:`echo $DEEPSEEK_API_KEY`
|
||||
2. 检查配置文件中的 `api_key_env` 设置
|
||||
3. 创建 `~/.cert/deepseek.key` 文件并写入API Key
|
||||
|
||||
### Q: 技能不生效?
|
||||
**A:**
|
||||
1. 确认文件在 `~/.aibox/skills/` 目录
|
||||
2. 确认文件可执行(`chmod +x`)
|
||||
3. 调用 `reload_skills` 工具
|
||||
4. 使用 `search_skills` 验证
|
||||
5. 检查文件命名(不能有扩展名)
|
||||
|
||||
### Q: 任务没有执行?
|
||||
**A:**
|
||||
1. 检查调度器是否启用:`/tasks`
|
||||
2. 查看任务状态:`/memos`
|
||||
3. 检查系统时间是否正确
|
||||
4. 查看日志:`tail -f ~/.aibox/ai.log`
|
||||
5. 确认任务有 `reminder_time`
|
||||
|
||||
### Q: 对话压缩丢失了重要信息?
|
||||
**A:** 调整配置:
|
||||
```json
|
||||
"compress_message_threshold": 300, // 调大阈值
|
||||
"compress_keep_recent": 20 // 保留更多最近消息
|
||||
```
|
||||
|
||||
### Q: 如何备份数据?
|
||||
**A:**
|
||||
```bash
|
||||
# 备份整个目录
|
||||
tar -czf aibox_backup_$(date +%Y%m%d).tar.gz ~/.aibox/
|
||||
|
||||
# 恢复
|
||||
tar -xzf aibox_backup_20260101.tar.gz -C ~/
|
||||
```
|
||||
|
||||
### Q: WebSocket连接断开?
|
||||
**A:**
|
||||
1. 检查服务器是否运行:`ps aux | grep agent.py`
|
||||
2. 确认端口未被占用:`netstat -tlnp | grep 8765`
|
||||
3. 检查防火墙设置
|
||||
4. 查看服务器日志
|
||||
5. 考虑使用 `--host 0.0.0.0` 允许外部连接
|
||||
|
||||
### Q: 如何让AI使用特定技能?
|
||||
**A:** 在对话中明确指示:
|
||||
```
|
||||
使用 search_skills 搜索天气相关技能,然后用 run_command 执行它
|
||||
```
|
||||
|
||||
### Q: 内存或日志占用过大?
|
||||
**A:**
|
||||
1. 调整日志配置:
|
||||
```json
|
||||
"log_max_size": 5242880, // 5MB
|
||||
"log_backup_count": 3
|
||||
```
|
||||
2. 清理旧对话归档
|
||||
3. 定期清理工具结果缓存
|
||||
|
||||
## 📄 更新日志
|
||||
|
||||
### v2.0 (当前版本)
|
||||
- ✅ 新增 `update_soul` 工具(智能体自我进化)
|
||||
- ✅ 新增 `create_skill` 工具(AI自主创建技能)
|
||||
- ✅ 新增 `install_skill` 工具(技能安装指引)
|
||||
- ✅ 新增 `task_process` 消息类型(任务执行过程广播)
|
||||
- ✅ 支持Markdown格式技能文件
|
||||
- ✅ 对话压缩支持Token数阈值判断
|
||||
- ✅ 开始新对话时完全重置AI状态并重新加载提示词
|
||||
- ✅ 智能体主动结束对话后重置状态
|
||||
- ✅ 任务执行时显示AI完整处理过程
|
||||
- ✅ 增强的流式输出支持(思考内容显示)
|
||||
- ✅ 技能热重载支持(`reload_skills`)
|
||||
- ✅ 记忆搜索支持分页
|
||||
- ✅ 任务调度器支持即时任务
|
||||
- ✅ 命令执行支持超时控制
|
||||
|
||||
### v1.0
|
||||
- ✅ WebSocket服务器支持
|
||||
- ✅ 文件操作、网页访问、记忆管理
|
||||
- ✅ 任务调度系统
|
||||
- ✅ 扩展技能支持
|
||||
- ✅ 多语言支持
|
||||
- ✅ 对话管理
|
||||
|
||||
## 📝 开发指南
|
||||
|
||||
### 添加新工具
|
||||
|
||||
1. 继承 `Tool` 基类
|
||||
2. 实现 `get_name()`, `get_description()`, `get_parameters()`, `execute()`
|
||||
3. 可选实现 `execute_streaming()` 支持流式输出
|
||||
4. 在 `DeepSeekChat._register_tools()` 中注册
|
||||
|
||||
```python
|
||||
class MyTool(Tool):
|
||||
def get_name(self) -> str:
|
||||
return "my_tool"
|
||||
|
||||
def get_description(self) -> str:
|
||||
return "我的自定义工具"
|
||||
|
||||
def get_parameters(self) -> Dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param": {"type": "string"}
|
||||
},
|
||||
"required": ["param"]
|
||||
}
|
||||
|
||||
def execute(self, param: str, **kwargs) -> str:
|
||||
return f"执行结果: {param}"
|
||||
```
|
||||
|
||||
### 添加新命令
|
||||
|
||||
在 `WebSocketHandler.handle_command()` 中添加:
|
||||
|
||||
```python
|
||||
elif command == "/mycommand":
|
||||
result = "命令执行结果"
|
||||
await self.send_message(websocket, {
|
||||
"type": "command_result",
|
||||
"content": result
|
||||
})
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
**注意**:本项目需要DeepSeek API Key,请确保遵守API使用条款。
|
||||
@@ -0,0 +1,229 @@
|
||||
# Aibox 扩展技能格式规范
|
||||
|
||||
本文档定义扩展技能的文件结构、接口约定和编写规范,供开发者与 AI 智能体参考。
|
||||
|
||||
## 概览
|
||||
|
||||
技能存放于 `~/.aibox/skills/` 目录,系统启动时自动扫描加载,运行时可通过 `reload_skills` 热重载。技能分为两种类型:
|
||||
|
||||
| 类型 | 识别方式 | 用途 |
|
||||
|------|---------|------|
|
||||
| 可执行技能 | 非 `.md` 文件,具有可执行权限 | 命令行工具,通过 `run_command` 调用 |
|
||||
| Markdown 技能 | 以 `.md` 结尾 | 知识文档,AI 直接阅读并遵循 |
|
||||
|
||||
文件名仅允许字母、数字、下划线 `_` 和连字符 `-`,以 `_` 或 `.` 开头的文件会被忽略。
|
||||
|
||||
---
|
||||
|
||||
## 可执行技能
|
||||
|
||||
可执行技能是标准的命令行程序,系统仅依赖一个接口约定:**必须支持 `--help` 参数**。系统通过执行 `<skill> --help` 获取技能摘要和使用说明,不需要实现 `--description`、`--parameters`、`--execute` 等额外接口。
|
||||
|
||||
### 接口约定
|
||||
|
||||
系统按以下规则提取技能信息:
|
||||
|
||||
1. 执行 `<skill_path> --help`,超时上限 10 秒
|
||||
2. 取 stdout 的第一个非空行作为**摘要**(若 stdout 为空则回退到 stderr)
|
||||
3. 完整的 `--help` 输出作为技能**内容**,供 `search_skills` 展示
|
||||
|
||||
因此,`--help` 输出的第一行必须是一句简洁的功能描述,后续内容是完整的参数说明和使用示例。
|
||||
|
||||
### Python 技能模板
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
技能模块内部说明(可选,不影响系统识别)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="一句话功能摘要 —— 这是 --help 的第一行输出",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-x, --xxx 参数含义说明
|
||||
-y, --yyy 另一个参数说明
|
||||
|
||||
使用示例:
|
||||
my_skill -x value # 基本用法
|
||||
my_skill -x value -y 10 # 组合用法
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-x", "--xxx", type=str, required=True, help="参数说明")
|
||||
parser.add_argument("-y", "--yyy", type=int, default=10, help="参数说明 (默认 10)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- 业务逻辑 ---
|
||||
print(f"执行结果: xxx={args.xxx}, yyy={args.yyy}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### 关键要素
|
||||
|
||||
**Shebang 行**:Python 脚本使用 `#!/usr/bin/env python3`,Shell 脚本使用 `#!/bin/bash`。
|
||||
|
||||
**argparse `description`**:这是系统识别的技能摘要,必须是一句话,精准概括技能功能。它直接决定 AI 能否在 `search_skills` 时正确匹配到这个技能。
|
||||
|
||||
**argparse `epilog`**:包含两部分内容——参数说明和使用示例。使用 `RawDescriptionHelpFormatter` 保持排版原样输出。
|
||||
|
||||
**错误处理**:向 stderr 输出错误信息,以非零退出码退出。正常结果输出到 stdout。
|
||||
|
||||
```python
|
||||
# 正确做法
|
||||
print("错误: 参数无效", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 避免
|
||||
print("错误: 参数无效") # 会混入 stdout,污染正常输出
|
||||
```
|
||||
|
||||
**退出码约定**:
|
||||
|
||||
| 退出码 | 含义 |
|
||||
|--------|------|
|
||||
| 0 | 执行成功 |
|
||||
| 1 | 参数错误或一般性失败 |
|
||||
| 其他 | 自定义错误码 |
|
||||
|
||||
### Shell 技能模板
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 用法通过 --help 输出
|
||||
|
||||
if [ "$1" = "--help" ]; then
|
||||
echo "一句话功能摘要"
|
||||
echo ""
|
||||
echo "用法: my_skill [选项]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " -n NAME 指定名称"
|
||||
echo " -h 显示帮助"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " my_skill -n hello"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 业务逻辑 ---
|
||||
echo "执行结果"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Markdown 技能
|
||||
|
||||
Markdown 技能是纯文档型技能,AI 阅读其内容后按指引行事,无需执行。
|
||||
|
||||
### 格式约定
|
||||
|
||||
```markdown
|
||||
一句话功能摘要(首行,# 前缀会被自动去除)
|
||||
|
||||
## 技能说明
|
||||
详细的功能描述、使用场景、操作步骤...
|
||||
|
||||
## 注意事项
|
||||
需要特别关注的点...
|
||||
```
|
||||
|
||||
系统提取第一个非空行作为摘要。若首行以 `#` 开头(如 `# 代码审查指南`),会自动去除 `#` 前缀,取 `代码审查指南` 作为摘要。其余全部内容为技能正文。
|
||||
|
||||
### 适用场景
|
||||
|
||||
Markdown 技能适合不需要代码执行的知识型内容:操作规范、审查清单、决策指引、回答模板等。
|
||||
|
||||
---
|
||||
|
||||
## 部署流程
|
||||
|
||||
### 开发阶段
|
||||
|
||||
源码保存在项目的 `skills/` 目录下,保留文件扩展名(`.py`、`.sh`、`.md`)便于开发维护。
|
||||
|
||||
### 安装到系统
|
||||
|
||||
部署到 `~/.aibox/skills/` 时需要去掉可执行技能的扩展名,并赋予执行权限:
|
||||
|
||||
```bash
|
||||
# 手动安装
|
||||
cp skills/my_skill.py ~/.aibox/skills/my_skill
|
||||
chmod +x ~/.aibox/skills/my_skill
|
||||
|
||||
# 验证
|
||||
~/.aibox/skills/my_skill --help
|
||||
```
|
||||
|
||||
Markdown 技能保留 `.md` 扩展名直接复制。
|
||||
|
||||
### 通过 AI 安装
|
||||
|
||||
告诉 AI 使用 `install_skill` 工具,按指引操作:
|
||||
|
||||
```
|
||||
使用 install_skill 工具安装 /path/to/my_skill.py
|
||||
```
|
||||
|
||||
### 热重载
|
||||
|
||||
安装后调用 `reload_skills` 使新技能生效,无需重启服务:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"content": "使用 reload_skills 重载技能"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调用方式
|
||||
|
||||
AI 智能体通过 `search_skills` 搜索技能,通过 `run_command` 执行技能:
|
||||
|
||||
```json
|
||||
// 1. 搜索
|
||||
{"tool": "search_skills", "args": {"keyword": "邮件"}}
|
||||
|
||||
// 2. 执行
|
||||
{"tool": "run_command", "args": {"command": "163mail --to user@example.com --subject 标题 --body 内容"}}
|
||||
```
|
||||
|
||||
Markdown 技能不通过命令执行,AI 阅读 `search_skills` 返回的内容后直接遵循其中的指引。
|
||||
|
||||
---
|
||||
|
||||
## 编写建议
|
||||
|
||||
**摘要要精准**。`description` 是 AI 匹配技能的唯一线索。"温度转换工具 - 摄氏度转华氏度" 比 "一个转换工具" 好得多,因为 AI 能通过关键词准确命中。
|
||||
|
||||
**参数命名要直观**。优先使用有明确含义的长参数名(`--channel`、`--temperature`),短参数名(`-c`、`-t`)作为补充。
|
||||
|
||||
**输出要结构化**。结果输出到 stdout,错误输出到 stderr。多字段输出时每行一个字段,便于 AI 解析:
|
||||
|
||||
```
|
||||
AIN0: +2.500000 V (2500.000 mV)
|
||||
AIN1: +1.234000 V (1234.000 mV)
|
||||
```
|
||||
|
||||
**依赖要自包含**。尽量使用标准库,若需要第三方库,在启动时检测并给出安装提示:
|
||||
|
||||
```python
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("错误: 需要 requests 库,请执行: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
**`--help` 要完备**。包含参数说明和使用示例两部分。AI 通过阅读 `--help` 输出学会如何调用技能,信息越完整,AI 调用越准确。
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
source ./_setenv.sh
|
||||
source ./venv/bin/activate
|
||||
export PATH=${PATH}:~/.aibox/skills
|
||||
exec python main.py $@
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
source ./_setenv.sh
|
||||
source ./venv/bin/activate
|
||||
exec python client.py $@
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -i https://pypi.org/simple
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export DEEPSEEK_API_KEY=`cat ~/.cert/deepseek.key`
|
||||
+667
@@ -0,0 +1,667 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI智能助手 - 命令行前端
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
import websockets
|
||||
from colorama import init, Fore, Style as ColoramaStyle
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
class Colors:
|
||||
USER = Fore.CYAN
|
||||
AI = Fore.GREEN
|
||||
TASK = Fore.YELLOW
|
||||
SYSTEM = Fore.BLUE
|
||||
ERROR = Fore.RED
|
||||
INFO = Fore.MAGENTA
|
||||
RESET = ColoramaStyle.RESET_ALL
|
||||
BOLD = ColoramaStyle.BRIGHT
|
||||
|
||||
GREEN = Fore.GREEN
|
||||
RED = Fore.RED
|
||||
|
||||
class WebSocketClient:
|
||||
def __init__(self, uri: str, callback):
|
||||
self.uri = uri
|
||||
self.websocket = None
|
||||
self.connected = False
|
||||
self.callback = callback
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.websocket = await websockets.connect(self.uri)
|
||||
self.connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
self.callback("error", f"连接失败: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.connected = False
|
||||
|
||||
async def send_message(self, message: str):
|
||||
if not self.websocket or not self.connected:
|
||||
return False
|
||||
|
||||
try:
|
||||
if message.startswith('/'):
|
||||
data = {'type': 'command', 'command': message}
|
||||
else:
|
||||
data = {'type': 'message', 'content': message}
|
||||
|
||||
await self.websocket.send(json.dumps(data))
|
||||
return True
|
||||
except Exception as e:
|
||||
self.callback("error", f"发送失败: {e}")
|
||||
return False
|
||||
|
||||
async def receive_messages(self):
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
await self._handle_message(data)
|
||||
except json.JSONDecodeError:
|
||||
self.callback("chunk", message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
self.connected = False
|
||||
self.callback("system", "❌ 连接已断开")
|
||||
except Exception as e:
|
||||
self.callback("error", f"接收消息错误: {e}")
|
||||
|
||||
async def _handle_message(self, data: dict):
|
||||
msg_type = data.get('type', '')
|
||||
content = data.get('content', '')
|
||||
|
||||
if msg_type == 'chunk':
|
||||
self.callback("chunk", content)
|
||||
elif msg_type == 'complete':
|
||||
self.callback("complete", content)
|
||||
elif msg_type == 'dialogue_ended':
|
||||
self.callback("dialogue_ended", content)
|
||||
elif msg_type == 'task_process':
|
||||
msg_subtype = data.get('msg_type', '')
|
||||
task_content = data.get('content', '')
|
||||
task_title = data.get('task_title', '未知任务')
|
||||
if msg_subtype == 'chunk':
|
||||
self.callback("task_chunk", task_content, task_title)
|
||||
elif msg_subtype == 'complete':
|
||||
self.callback("task_complete", task_title)
|
||||
elif msg_type == 'task_status':
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
title = data.get('title', '未知')
|
||||
status = data.get('status', 'unknown')
|
||||
message = data.get('message', content)
|
||||
status_icon = {'executing': '⚙️', 'complete': '✅', 'failed': '❌', 'added': '➕', 'batch_start': '🚀', 'batch_complete': '🏁'}.get(status, '📋')
|
||||
self.callback("info", f"{status_icon} [任务 {task_id}] {title}: {message}")
|
||||
elif msg_type == 'task_error':
|
||||
self.callback("error", f"[任务 {data.get('task_id', 'unknown')}] {data.get('task_title', '未知')}: {data.get('error', '未知错误')}")
|
||||
elif msg_type == 'interrupt_ack':
|
||||
self.callback("info", f"🛑 {content}")
|
||||
elif msg_type == 'error':
|
||||
self.callback("error", content)
|
||||
elif msg_type == 'info':
|
||||
self.callback("info", content)
|
||||
elif msg_type == 'command_result':
|
||||
self.callback("system", content)
|
||||
elif msg_type == 'connection_ack':
|
||||
self.callback("system", f"✅ {content}")
|
||||
elif msg_type == 'notification':
|
||||
self.callback("info", f"🔔 {content}")
|
||||
else:
|
||||
if content:
|
||||
self.callback("chunk", content)
|
||||
|
||||
class SimpleCLI:
|
||||
def __init__(self):
|
||||
self.client: Optional[WebSocketClient] = None
|
||||
self.connected = False
|
||||
self.running = True
|
||||
|
||||
# AI消息缓冲
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
|
||||
# 已显示消息的去重
|
||||
self.displayed_messages: set = set()
|
||||
|
||||
# 锁和状态
|
||||
self.lock = threading.Lock()
|
||||
self.current_input = ""
|
||||
|
||||
# 流式输出状态
|
||||
self.streaming_line = False # 是否正在流式输出行
|
||||
self.in_tool_phase = False # 是否在工具调用阶段
|
||||
self.waiting_for_response = False # 是否在等待AI响应
|
||||
self.prompt_shown = True # 提示符是否已显示
|
||||
|
||||
# 命令列表
|
||||
self.commands = {
|
||||
'/help': '显示帮助信息',
|
||||
'/connect': '连接到服务器',
|
||||
'/disconnect': '断开连接',
|
||||
'/quit': '退出程序',
|
||||
'/clear': '清屏',
|
||||
'/status': '显示连接状态',
|
||||
'/interrupt': '发送中断信号(或按 Ctrl+C)',
|
||||
}
|
||||
|
||||
def _get_prompt(self) -> str:
|
||||
status = "● 已连接" if self.connected else "○ 未连接"
|
||||
color = Colors.GREEN if self.connected else Colors.RED
|
||||
return f"{color}{status}{Colors.RESET} >>> "
|
||||
|
||||
def _print_line(self, line: str = "", end: str = "\n"):
|
||||
"""打印一行,并恢复提示符"""
|
||||
with self.lock:
|
||||
current = self.current_input
|
||||
sys.stdout.write('\r' + ' ' * 120 + '\r')
|
||||
sys.stdout.write(line + end)
|
||||
sys.stdout.flush()
|
||||
if not self.waiting_for_response and not self.in_tool_phase:
|
||||
sys.stdout.write(self._get_prompt() + current)
|
||||
sys.stdout.flush()
|
||||
self.prompt_shown = True
|
||||
self.streaming_line = False
|
||||
|
||||
def _print_prompt(self, clear_input: bool = False):
|
||||
"""显示提示符"""
|
||||
with self.lock:
|
||||
if clear_input:
|
||||
self.current_input = ""
|
||||
self.waiting_for_response = False
|
||||
self.in_tool_phase = False
|
||||
self.prompt_shown = True
|
||||
sys.stdout.write('\r' + ' ' * 120 + '\r')
|
||||
sys.stdout.write(self._get_prompt() + self.current_input)
|
||||
sys.stdout.flush()
|
||||
self.streaming_line = False
|
||||
|
||||
def _print_stream(self, text: str):
|
||||
"""流式打印文本(不换行,不显示提示符)"""
|
||||
with self.lock:
|
||||
# 保存当前输入
|
||||
current = self.current_input
|
||||
|
||||
if not self.streaming_line:
|
||||
# 首次流式输出,先换行再打印前缀
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.write(f"{Colors.AI}🤖 AI助手: ")
|
||||
self.streaming_line = True
|
||||
self.prompt_shown = False
|
||||
|
||||
# 打印文本
|
||||
sys.stdout.write(text)
|
||||
sys.stdout.flush()
|
||||
|
||||
# 注意:不恢复提示符,保持在同一行
|
||||
|
||||
def _finish_stream(self):
|
||||
"""完成流式输出,换行并恢复提示符"""
|
||||
with self.lock:
|
||||
if self.streaming_line:
|
||||
sys.stdout.write('\n')
|
||||
self.streaming_line = False
|
||||
if not self.in_tool_phase and not self.waiting_for_response:
|
||||
current = self.current_input
|
||||
sys.stdout.write(self._get_prompt() + current)
|
||||
sys.stdout.flush()
|
||||
self.prompt_shown = True
|
||||
|
||||
def _clean_content(self, content: str) -> str:
|
||||
if not content:
|
||||
return ""
|
||||
if '💬 助手:' in content:
|
||||
content = content.split('💬 助手:', 1)[1].strip()
|
||||
content = content.lstrip()
|
||||
return content
|
||||
|
||||
def _show_streaming(self, content: str):
|
||||
"""流式显示增量内容"""
|
||||
if not content:
|
||||
return
|
||||
|
||||
# 清理内容
|
||||
clean = self._clean_content(content)
|
||||
if not clean:
|
||||
return
|
||||
|
||||
key = clean[:200]
|
||||
if key in self.displayed_messages:
|
||||
return
|
||||
|
||||
# 检测增量
|
||||
if self.ai_previous and clean.startswith(self.ai_previous):
|
||||
new_part = clean[len(self.ai_previous):]
|
||||
if new_part:
|
||||
# 如果是首次流式输出,显示前缀
|
||||
if not self.ai_prefix_shown:
|
||||
self._print_stream("")
|
||||
self.ai_prefix_shown = True
|
||||
self._print_stream(new_part)
|
||||
self.ai_previous = clean
|
||||
self.ai_full = clean
|
||||
return
|
||||
|
||||
if self.ai_previous and self.ai_previous in clean:
|
||||
idx = clean.find(self.ai_previous) + len(self.ai_previous)
|
||||
new_part = clean[idx:]
|
||||
if new_part:
|
||||
if not self.ai_prefix_shown:
|
||||
self._print_stream("")
|
||||
self.ai_prefix_shown = True
|
||||
self._print_stream(new_part)
|
||||
self.ai_previous = clean
|
||||
self.ai_full = clean
|
||||
return
|
||||
|
||||
# 全新内容 - 显示前缀和内容
|
||||
if not self.ai_prefix_shown:
|
||||
self._print_stream("")
|
||||
self.ai_prefix_shown = True
|
||||
self._print_stream(clean)
|
||||
self.ai_previous = clean
|
||||
self.ai_full = clean
|
||||
|
||||
def display(self, msg_type: str, content: str, extra: Any = None):
|
||||
"""显示消息"""
|
||||
if msg_type == 'chunk':
|
||||
self._handle_chunk(content)
|
||||
elif msg_type == 'complete':
|
||||
self._handle_complete(content)
|
||||
elif msg_type == 'task_chunk':
|
||||
self._handle_task_chunk(content, extra)
|
||||
elif msg_type == 'task_complete':
|
||||
self._handle_task_complete(extra)
|
||||
elif msg_type == 'dialogue_ended':
|
||||
with self.lock:
|
||||
if self.streaming_line:
|
||||
sys.stdout.write('\n')
|
||||
self.streaming_line = False
|
||||
self.in_tool_phase = False
|
||||
self.waiting_for_response = False
|
||||
self.prompt_shown = True
|
||||
self._print_line(f"{Colors.SYSTEM}✅ {content}{Colors.RESET}")
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
self.displayed_messages.clear()
|
||||
self.streaming_line = False
|
||||
elif msg_type == 'system':
|
||||
self._finish_stream()
|
||||
self._print_line(f"{Colors.SYSTEM}{content}{Colors.RESET}")
|
||||
elif msg_type == 'info':
|
||||
self._finish_stream()
|
||||
self._print_line(f"{Colors.INFO}ℹ️ {content}{Colors.RESET}")
|
||||
elif msg_type == 'error':
|
||||
self._finish_stream()
|
||||
self._print_line(f"{Colors.ERROR}❌ {content}{Colors.RESET}")
|
||||
|
||||
def _handle_chunk(self, content: str):
|
||||
"""处理流式块"""
|
||||
# 检查是否是工具调用相关的消息
|
||||
if content.startswith('🔧 调用工具'):
|
||||
with self.lock:
|
||||
self.in_tool_phase = True
|
||||
self.prompt_shown = False
|
||||
# 打印工具调用信息
|
||||
if self.streaming_line:
|
||||
sys.stdout.write('\n')
|
||||
self.streaming_line = False
|
||||
sys.stdout.write(f"{Colors.TASK}{content}{Colors.RESET}\n")
|
||||
sys.stdout.flush()
|
||||
return
|
||||
|
||||
# 检查是否是工具结果
|
||||
if content.startswith(('✅ 对话记忆已保存', '💾 保存记忆', '❌', '⚠️')):
|
||||
with self.lock:
|
||||
# 打印工具结果
|
||||
sys.stdout.write(f"{Colors.TASK}{content}{Colors.RESET}\n")
|
||||
sys.stdout.flush()
|
||||
return
|
||||
|
||||
# 如果之前在工具阶段,现在退出工具阶段
|
||||
if self.in_tool_phase:
|
||||
with self.lock:
|
||||
self.in_tool_phase = False
|
||||
# 工具结束后不立即显示提示符,等待AI继续响应
|
||||
self.prompt_shown = False
|
||||
|
||||
# 系统消息
|
||||
if content.startswith(('📩', '[', '📁', '🔧', '📝', '✅', 'ℹ️', '⏰')):
|
||||
self._finish_stream()
|
||||
self._print_line(f"{Colors.INFO}{content}{Colors.RESET}")
|
||||
return
|
||||
|
||||
# 流式显示
|
||||
self._show_streaming(content)
|
||||
|
||||
def _handle_complete(self, content: str):
|
||||
"""处理完成事件 - 只负责结束流式输出,不重复显示内容"""
|
||||
# 如果之前在工具阶段,现在退出
|
||||
if self.in_tool_phase:
|
||||
with self.lock:
|
||||
self.in_tool_phase = False
|
||||
self.waiting_for_response = False
|
||||
self.prompt_shown = True
|
||||
current = self.current_input
|
||||
sys.stdout.write(self._get_prompt() + current)
|
||||
sys.stdout.flush()
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
self.streaming_line = False
|
||||
return
|
||||
|
||||
# 如果流式输出正在进行,结束它
|
||||
if self.streaming_line:
|
||||
self._finish_stream()
|
||||
|
||||
# 如果内容不为空,检查是否已经通过流式显示了
|
||||
if content and content.strip():
|
||||
clean = self._clean_content(content)
|
||||
if clean:
|
||||
# 检查是否已通过流式显示
|
||||
if self.ai_full and (clean in self.ai_full or self.ai_full in clean):
|
||||
# 已经流式显示过了,不重复打印
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
self.streaming_line = False
|
||||
# 确保提示符显示
|
||||
with self.lock:
|
||||
self.waiting_for_response = False
|
||||
self.prompt_shown = True
|
||||
current = self.current_input
|
||||
sys.stdout.write(self._get_prompt() + current)
|
||||
sys.stdout.flush()
|
||||
return
|
||||
|
||||
# 检查是否已经在 displayed_messages 中
|
||||
key = clean[:200]
|
||||
if key in self.displayed_messages:
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
self.streaming_line = False
|
||||
# 确保提示符显示
|
||||
with self.lock:
|
||||
self.waiting_for_response = False
|
||||
self.prompt_shown = True
|
||||
current = self.current_input
|
||||
sys.stdout.write(self._get_prompt() + current)
|
||||
sys.stdout.flush()
|
||||
return
|
||||
|
||||
# 只有当内容没有被流式显示时才显示完整消息
|
||||
self.displayed_messages.add(key)
|
||||
# 如果之前没有流式输出,现在显示完整消息
|
||||
self._print_line(f"{Colors.AI}🤖 AI助手: {clean}")
|
||||
|
||||
# 重置状态
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
self.streaming_line = False
|
||||
self.waiting_for_response = False
|
||||
# 确保提示符显示
|
||||
with self.lock:
|
||||
self.prompt_shown = True
|
||||
current = self.current_input
|
||||
sys.stdout.write('\r' + ' ' * 120 + '\r')
|
||||
sys.stdout.write(self._get_prompt() + current)
|
||||
sys.stdout.flush()
|
||||
|
||||
def _handle_task_chunk(self, content: str, task_title: str):
|
||||
"""处理任务流式块"""
|
||||
if not hasattr(self, '_task_buffers'):
|
||||
self._task_buffers = {}
|
||||
|
||||
if task_title not in self._task_buffers:
|
||||
self._task_buffers[task_title] = ""
|
||||
self._finish_stream()
|
||||
self._print_line(f"{Colors.TASK}⚡ 任务 - {task_title}: ", end="")
|
||||
self.streaming_line = True
|
||||
|
||||
prev = self._task_buffers[task_title]
|
||||
if content.startswith(prev):
|
||||
new_part = content[len(prev):]
|
||||
if new_part:
|
||||
with self.lock:
|
||||
sys.stdout.write(new_part)
|
||||
sys.stdout.flush()
|
||||
self._task_buffers[task_title] = content
|
||||
elif prev in content:
|
||||
idx = content.find(prev) + len(prev)
|
||||
new_part = content[idx:]
|
||||
if new_part:
|
||||
with self.lock:
|
||||
sys.stdout.write(new_part)
|
||||
sys.stdout.flush()
|
||||
self._task_buffers[task_title] = content
|
||||
else:
|
||||
with self.lock:
|
||||
sys.stdout.write(content)
|
||||
sys.stdout.flush()
|
||||
self._task_buffers[task_title] = content
|
||||
|
||||
def _handle_task_complete(self, task_title: str):
|
||||
"""处理任务完成"""
|
||||
self._finish_stream()
|
||||
if hasattr(self, '_task_buffers') and task_title in self._task_buffers:
|
||||
content = self._task_buffers[task_title]
|
||||
if content:
|
||||
clean = content.strip()
|
||||
key = clean[:200]
|
||||
if key not in self.displayed_messages:
|
||||
self.displayed_messages.add(key)
|
||||
self._print_line(f"{Colors.TASK}⚡ 任务 - {task_title}: {clean}")
|
||||
del self._task_buffers[task_title]
|
||||
self.streaming_line = False
|
||||
|
||||
def print_banner(self):
|
||||
banner = f"""
|
||||
{Colors.BOLD}{Colors.AI}
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ AI智能助手 - 命令行前端 ║
|
||||
║ WebSocket 直连 + CTRL+C 中断 ║
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
{Colors.RESET}
|
||||
"""
|
||||
self._print_line(banner)
|
||||
self._print_line(f"{Colors.INFO}📌 输入 /help 查看可用命令")
|
||||
self._print_line(f"{Colors.INFO}📌 按 Ctrl+C 发送中断信号 (而非退出)")
|
||||
self._print_line(f"{Colors.INFO}📌 按 Ctrl+D 退出程序{Colors.RESET}")
|
||||
self._print_line("")
|
||||
self._print_line("=" * 60)
|
||||
self._print_line("")
|
||||
self._print_prompt(clear_input=True)
|
||||
|
||||
def show_help(self):
|
||||
self._print_line("")
|
||||
self._print_line(f"{Colors.BOLD}可用命令:{Colors.RESET}")
|
||||
self._print_line("")
|
||||
self._print_line(f"{Colors.USER}本地命令:{Colors.RESET}")
|
||||
self._print_line(" /connect <url> 连接到WebSocket服务器")
|
||||
self._print_line(" /disconnect 断开连接")
|
||||
self._print_line(" /status 显示连接状态")
|
||||
self._print_line(" /clear 清屏")
|
||||
self._print_line(" /interrupt 发送中断信号 (或按 Ctrl+C)")
|
||||
self._print_line(" /quit 退出程序 (或按 Ctrl+D)")
|
||||
self._print_line(" /help 显示此帮助")
|
||||
self._print_line("")
|
||||
self._print_line(f"{Colors.AI}服务器命令 (需要先连接):{Colors.RESET}")
|
||||
for cmd, desc in self.commands.items():
|
||||
if cmd not in ['/help', '/connect', '/disconnect', '/quit', '/clear', '/status', '/interrupt']:
|
||||
self._print_line(f" {cmd:<15} {desc}")
|
||||
self._print_prompt()
|
||||
|
||||
async def handle_command(self, command: str):
|
||||
parts = command.split()
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == '/help':
|
||||
self.show_help()
|
||||
elif cmd == '/connect':
|
||||
if len(parts) < 2:
|
||||
self.display("error", "用法: /connect ws://host:port")
|
||||
return
|
||||
uri = parts[1]
|
||||
if self.client and self.client.connected:
|
||||
await self.client.disconnect()
|
||||
self.client = WebSocketClient(uri, self.display)
|
||||
if await self.client.connect():
|
||||
self.connected = True
|
||||
self.display("system", f"✅ 已连接到 {uri}")
|
||||
asyncio.create_task(self.client.receive_messages())
|
||||
else:
|
||||
self.client = None
|
||||
self.connected = False
|
||||
elif cmd == '/disconnect':
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
self.client = None
|
||||
self.connected = False
|
||||
self.display("system", "已断开连接")
|
||||
else:
|
||||
self.display("error", "未连接到服务器")
|
||||
elif cmd == '/status':
|
||||
if self.connected:
|
||||
self.display("system", f"✅ 已连接: {self.client.uri}")
|
||||
else:
|
||||
self.display("system", "未连接")
|
||||
elif cmd == '/clear':
|
||||
os.system('clear' if os.name == 'posix' else 'cls')
|
||||
self.displayed_messages.clear()
|
||||
self.current_input = ""
|
||||
self.print_banner()
|
||||
elif cmd == '/interrupt':
|
||||
if self.client and self.client.connected:
|
||||
await self._send_interrupt()
|
||||
self.display("info", "🛑 已发送中断信号")
|
||||
else:
|
||||
self.display("error", "未连接到服务器")
|
||||
elif cmd == '/quit':
|
||||
self.display("system", "👋 再见!")
|
||||
self.running = False
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
return
|
||||
elif cmd in self.commands:
|
||||
if self.client and self.client.connected:
|
||||
await self.client.send_message(command)
|
||||
self.display("system", f"📤 发送命令: {command}")
|
||||
else:
|
||||
self.display("error", "未连接到服务器")
|
||||
else:
|
||||
self.display("error", f"未知命令: {cmd}")
|
||||
self._print_prompt(clear_input=True)
|
||||
|
||||
async def _send_interrupt(self):
|
||||
if self.client and self.client.connected:
|
||||
interrupt_msg = {"type": "interrupt", "content": "用户按下了 Ctrl+C 中断当前操作"}
|
||||
try:
|
||||
await self.client.websocket.send(json.dumps(interrupt_msg))
|
||||
except Exception as e:
|
||||
self.display("error", f"发送中断失败: {e}")
|
||||
|
||||
async def send_user_message(self, message: str):
|
||||
if not self.client or not self.client.connected:
|
||||
self.display("error", "未连接到服务器")
|
||||
return
|
||||
|
||||
# 重置流式状态
|
||||
with self.lock:
|
||||
if self.streaming_line:
|
||||
sys.stdout.write('\n')
|
||||
self.streaming_line = False
|
||||
self.waiting_for_response = True
|
||||
self.in_tool_phase = False
|
||||
self.prompt_shown = False
|
||||
# 【关键】清除当前输入,避免显示之前输入的内容
|
||||
self.current_input = ""
|
||||
|
||||
self.ai_previous = ""
|
||||
self.ai_full = ""
|
||||
self.ai_prefix_shown = False
|
||||
|
||||
self.display("system", f"{Colors.USER}👤 用户: {message}{Colors.RESET}")
|
||||
await self.client.send_message(message)
|
||||
# 不在这里显示提示符,等待AI响应结束后再显示
|
||||
|
||||
async def run(self):
|
||||
self.print_banner()
|
||||
self._task_buffers = {}
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
line = await asyncio.get_event_loop().run_in_executor(
|
||||
None, lambda: sys.stdin.readline()
|
||||
)
|
||||
if not line:
|
||||
break
|
||||
|
||||
self.current_input = line.rstrip('\n')
|
||||
line = line.strip()
|
||||
|
||||
if not line:
|
||||
self._print_prompt(clear_input=True)
|
||||
continue
|
||||
|
||||
if line.startswith('/'):
|
||||
await self.handle_command(line)
|
||||
else:
|
||||
await self.send_user_message(line)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if self.client and self.client.connected:
|
||||
await self._send_interrupt()
|
||||
self.display("info", "🛑 已发送中断信号")
|
||||
else:
|
||||
self.display("error", "未连接到服务器")
|
||||
self._print_prompt(clear_input=True)
|
||||
continue
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
self.display("error", f"错误: {e}")
|
||||
break
|
||||
|
||||
async def main():
|
||||
default_uri = "ws://localhost:8765"
|
||||
uri = sys.argv[1] if len(sys.argv) > 1 else default_uri
|
||||
|
||||
cli = SimpleCLI()
|
||||
|
||||
cli.client = WebSocketClient(uri, cli.display)
|
||||
if await cli.client.connect():
|
||||
cli.connected = True
|
||||
cli.display("system", f"✅ 已连接到 {uri}")
|
||||
asyncio.create_task(cli.client.receive_messages())
|
||||
else:
|
||||
cli.display("error", f"无法连接到 {uri}")
|
||||
cli.display("info", f"你可以通过命令行参数指定地址: python client.py ws://your-server:8765")
|
||||
|
||||
try:
|
||||
await cli.run()
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.SYSTEM}程序已退出{Colors.RESET}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.SYSTEM}程序已退出{Colors.RESET}")
|
||||
except Exception as e:
|
||||
print(f"\n{Colors.ERROR}错误: {e}{Colors.RESET}")
|
||||
+1780
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,875 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>小猫AI · 智能喵语伙伴</title>
|
||||
<!-- Font Awesome 6 (免费图标库) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<!-- highlight.js 代码高亮 + marked markdown解析 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
||||
<!-- Socket.IO 客户端库 -->
|
||||
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
|
||||
<style>
|
||||
/* ========== 融合小猫风格 + WebSocket增强样式 ========== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Microsoft YaHei", "Segoe UI", system-ui, -apple-system, 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 整体背景及容器 - 可爱猫咪氛围 */
|
||||
body {
|
||||
background: #1a1a2f;
|
||||
background-image: url('/imgs/background.jpeg');
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
transition: background-image 0.3s ease, background-color 0.2s;
|
||||
color: #f0ede8;
|
||||
}
|
||||
|
||||
/* 主布局 flex,左侧聊天区,右侧猫咪头像+WS侧栏 */
|
||||
.main-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 左侧内容区 (聊天框+输入区) */
|
||||
.cat-chat-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 角色模型区域 - 右侧面板内 */
|
||||
#roleArea {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
margin: 0 auto 16px auto;
|
||||
filter: drop-shadow(0 8px 16px rgba(0,0,0,0.4));
|
||||
pointer-events: auto;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#roleArea img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* 聊天记录玻璃容器 */
|
||||
.chat-box {
|
||||
position: relative;
|
||||
margin: 20px 20px 95px 20px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 28px;
|
||||
padding: 16px 20px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 130px);
|
||||
scroll-behavior: smooth;
|
||||
border: 1px solid rgba(255, 220, 180, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.chat-box div {
|
||||
margin: 10px 0;
|
||||
animation: fadeSlideUp 0.2s ease;
|
||||
}
|
||||
|
||||
.user {
|
||||
text-align: right;
|
||||
color: #b9f6ca;
|
||||
font-weight: 500;
|
||||
background: rgba(60, 50, 80, 0.7);
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
max-width: 85%;
|
||||
margin-left: auto;
|
||||
padding: 8px 16px;
|
||||
border-radius: 26px;
|
||||
border-bottom-right-radius: 4px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ai {
|
||||
text-align: left;
|
||||
color: #fff0dd;
|
||||
background: rgba(30, 30, 45, 0.8);
|
||||
width: auto;
|
||||
max-width: 85%;
|
||||
padding: 8px 16px;
|
||||
border-radius: 26px;
|
||||
border-bottom-left-radius: 4px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 系统/错误/任务消息样式 */
|
||||
.system-msg, .error-msg, .task-msg {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
margin: 6px 0;
|
||||
}
|
||||
.task-separator {
|
||||
margin: 12px 0 8px;
|
||||
padding: 6px 12px;
|
||||
background: #ff980033;
|
||||
border-left: 3px solid #ff9800;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #ffcd94;
|
||||
}
|
||||
|
||||
/* 输入框区域 - 支持多行文本 */
|
||||
.input-area {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
background: transparent;
|
||||
z-index: 30;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
background: rgba(20, 20, 35, 0.9);
|
||||
backdrop-filter: blur(6px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 200, 150, 0.5);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: #ff9a5a;
|
||||
box-shadow: 0 0 8px rgba(255, 154, 90, 0.3);
|
||||
}
|
||||
|
||||
#msgInput {
|
||||
width: 100%;
|
||||
padding: 12px 18px;
|
||||
background: transparent;
|
||||
color: #fce8cf;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
border: none;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#msgInput::placeholder {
|
||||
color: rgba(252, 232, 207, 0.6);
|
||||
}
|
||||
|
||||
#msgInput::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#msgInput::-webkit-scrollbar-track {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#msgInput::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 154, 90, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#sendBtn {
|
||||
padding: 0 28px;
|
||||
height: 48px;
|
||||
border-radius: 48px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #ff9a5a, #ff6f61);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#sendBtn:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 12px rgba(255, 111, 97, 0.4);
|
||||
}
|
||||
|
||||
#sendBtn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 右侧整合面板 (猫咪头像 + WS控制台) */
|
||||
.right-panel {
|
||||
width: 320px;
|
||||
background: rgba(30, 30, 40, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
border-left: 1px solid rgba(255,255,200,0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
padding: 24px 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 猫咪头像容器 */
|
||||
.avatar-container {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatar-name {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #ffd4a3;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* WebSocket连接区域 */
|
||||
.ws-card {
|
||||
background: rgba(26, 188, 156, 0.75);
|
||||
margin: 12px 0;
|
||||
border-radius: 24px;
|
||||
padding: 16px;
|
||||
backdrop-filter: blur(4px);
|
||||
width: 100%;
|
||||
}
|
||||
.ws-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px dashed rgba(255,255,240,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.ws-control {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.ws-url-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: #2d2d3a;
|
||||
border: 1px solid #5a5a6e;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
border-radius: 24px;
|
||||
background: #4CAF50;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
.btn-sm:disabled { opacity: 0.5; }
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.command-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2,1fr);
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.cmd-btn {
|
||||
background: #3d3d4a;
|
||||
padding: 6px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: 0.1s;
|
||||
}
|
||||
.cmd-btn:hover {
|
||||
background: #5a5a6e;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
.stats-line {
|
||||
font-size: 12px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* 流式消息游标动画 */
|
||||
.streaming .message-text {
|
||||
border-right: 2px solid #ffb347;
|
||||
animation: blink 0.8s step-end infinite;
|
||||
padding-right: 4px;
|
||||
}
|
||||
@keyframes blink { from,to { border-color: transparent; } 50% { border-color: #ffb347; } }
|
||||
.markdown-body code { background: #1e1e2e; border-radius: 6px; }
|
||||
.markdown-body pre { background: #0f0f1a; }
|
||||
|
||||
@keyframes fadeSlideUp {
|
||||
from { opacity: 0; transform: translateY(8px);}
|
||||
to { opacity: 1; transform: translateY(0);}
|
||||
}
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
|
||||
/* 更换背景按钮样式 */
|
||||
.bg-btn {
|
||||
background: rgba(255, 165, 100, 0.8);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 40px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-btn:hover {
|
||||
background: rgba(255, 140, 70, 1);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 护眼模式开关样式 */
|
||||
.eye-mode-btn {
|
||||
background: rgba(100, 180, 120, 0.8);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 40px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.eye-mode-btn:hover {
|
||||
background: rgba(80, 160, 100, 1);
|
||||
}
|
||||
|
||||
.eye-protect {
|
||||
background: #c8e6d9 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
.eye-protect .chat-box {
|
||||
background: rgba(180,200,150,0.85);
|
||||
}
|
||||
.eye-protect .right-panel {
|
||||
background: rgba(80,100,70,0.9);
|
||||
}
|
||||
|
||||
#sendBtn i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.right-panel {
|
||||
width: 280px;
|
||||
}
|
||||
.input-area {
|
||||
gap: 8px;
|
||||
}
|
||||
#sendBtn {
|
||||
padding: 0 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.proxy-info {
|
||||
background: rgba(0,0,0,0.6);
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 10px;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout">
|
||||
<!-- 左侧聊天区域 -->
|
||||
<div class="cat-chat-area">
|
||||
<div class="chat-box" id="chatBox">
|
||||
<div class="ai">🐱 主人好呀~ 我是小猫AI,已整合智能引擎!点击右侧「连接服务器」开启AI对话 ✨<br>📝 提示:输入框支持多行文本,按 Enter 发送,Shift+Enter 换行<br>🔌 连接将通过 Socket.IO 代理服务转发</div>
|
||||
</div>
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<textarea id="msgInput" placeholder="喵~ 跟小猫说说话吧... (Shift+Enter换行)" autocomplete="off" rows="1"></textarea>
|
||||
</div>
|
||||
<button id="sendBtn"><i class="fas fa-paper-plane"></i> 发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板: 小猫头像 + WS控制台 + 背景/护眼功能 -->
|
||||
<div class="right-panel">
|
||||
<!-- 小猫头像区域 -->
|
||||
<div class="avatar-container">
|
||||
<div id="roleArea">
|
||||
<img id="roleImg" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Ccircle cx='100' cy='100' r='80' fill='%23f9cfb0'/%3E%3Ccircle cx='70' cy='80' r='8' fill='%233b2b1f'/%3E%3Ccircle cx='130' cy='80' r='8' fill='%233b2b1f'/%3E%3Cpath d='M80 120 Q100 140 120 120' stroke='%23945c3a' fill='none' stroke-width='4'/%3E%3Cpolygon points='100,150 92,175 108,175' fill='%23ffaaa5'/%3E%3C/svg%3E" alt="小猫AI">
|
||||
</div>
|
||||
<div class="avatar-name">🐱 小猫 AI</div>
|
||||
</div>
|
||||
|
||||
<!-- 背景与显示设置 -->
|
||||
<div style="width: 100%; margin-bottom: 12px;">
|
||||
<button id="changeBgBtn" class="bg-btn"><i class="fas fa-image"></i> 🖼️ 更换背景</button>
|
||||
<button id="eyeModeBtn" class="eye-mode-btn"><i class="fas fa-eye"></i> 🌿 护眼模式</button>
|
||||
</div>
|
||||
|
||||
<!-- WebSocket 智能连接卡片 -->
|
||||
<div class="ws-card">
|
||||
<div class="ws-title"><i class="fas fa-globe"></i> AI 智能引擎 (Socket.IO代理)</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||||
<span class="status-dot" id="wsStatusDot" style="background:#f44336;"></span>
|
||||
<span id="wsStatusText" style="font-size:12px;">未连接</span>
|
||||
</div>
|
||||
<div class="proxy-info">
|
||||
<i class="fas fa-shield-alt"></i> 通过 Socket.IO 代理服务连接
|
||||
</div>
|
||||
<input type="text" id="wsUrlInput" class="ws-url-input" value="ws://localhost:8765" placeholder="目标 ws://地址:端口">
|
||||
<div class="ws-control">
|
||||
<button id="wsConnectBtn" class="btn-sm"><i class="fas fa-plug"></i> 连接</button>
|
||||
<button id="wsDisconnectBtn" class="btn-sm" style="background:#6c757d;" disabled><i class="fas fa-power-off"></i> 断开</button>
|
||||
</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<div class="ws-title" style="font-size:12px;"><i class="fas fa-terminal"></i> 快捷命令</div>
|
||||
<div class="command-grid">
|
||||
<div class="cmd-btn" data-cmd="/help">/help</div>
|
||||
<div class="cmd-btn" data-cmd="/list">/list</div>
|
||||
<div class="cmd-btn" data-cmd="/memories">/memories</div>
|
||||
<div class="cmd-btn" data-cmd="/tasks">/tasks</div>
|
||||
<div class="cmd-btn" data-cmd="/new">/new</div>
|
||||
<div class="cmd-btn" data-cmd="/config">/config</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="statsPanel" style="margin-top: 12px; display:none;">
|
||||
<div class="stats-line"><i class="fas fa-clock"></i> 连接时间: <span id="connectTime">-</span></div>
|
||||
<div class="stats-line"><i class="fas fa-envelope"></i> 消息数: <span id="msgCount">0</span></div>
|
||||
<div class="stats-line"><i class="fas fa-tasks"></i> 活跃任务: <span id="taskCount">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏文件上传 -->
|
||||
<input type="file" id="bgFile" accept="image/*" style="display:none;">
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// ---------- DOM 元素 ----------
|
||||
const bgFile = document.getElementById('bgFile');
|
||||
const roleImg = document.getElementById('roleImg');
|
||||
const chatBox = document.getElementById('chatBox');
|
||||
const msgInput = document.getElementById('msgInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const changeBgBtn = document.getElementById('changeBgBtn');
|
||||
const eyeModeBtn = document.getElementById('eyeModeBtn');
|
||||
|
||||
let eyeProtectActive = false;
|
||||
|
||||
// ---------- Socket.IO 连接管理 ----------
|
||||
let socket = null;
|
||||
let reconnectAttempts = 0;
|
||||
let reconnectTimer = null;
|
||||
let manualDisconnect = false;
|
||||
let markdownEnabled = true;
|
||||
let globalMsgCount = 0;
|
||||
let activeTaskCount = 0;
|
||||
let connectStartTime = null;
|
||||
let targetWsUrl = null;
|
||||
|
||||
// 流式消息管理
|
||||
let streamingChat = { element: null, content: '', timer: null, active: false };
|
||||
let streamingTasks = {};
|
||||
|
||||
function updateMsgStats() {
|
||||
document.getElementById('msgCount').innerText = globalMsgCount;
|
||||
document.getElementById('taskCount').innerText = activeTaskCount;
|
||||
}
|
||||
|
||||
function addSystemMessage(text, isError=false) {
|
||||
const div = document.createElement('div');
|
||||
div.className = isError ? 'error-msg' : 'system-msg';
|
||||
div.style.cssText = 'background:rgba(0,0,0,0.6); border-radius:16px; padding:6px 12px; margin:6px 0; font-size:12px;';
|
||||
div.innerHTML = `<i class="fas ${isError ? 'fa-exclamation-triangle' : 'fa-info-circle'}"></i> ${text}`;
|
||||
chatBox.appendChild(div);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStreamingDisplay(type, execId=null) {
|
||||
let stream = (type==='chat') ? streamingChat : (execId ? streamingTasks[execId] : null);
|
||||
if(!stream || !stream.element) return;
|
||||
const textDiv = stream.element.querySelector('.message-text');
|
||||
if(textDiv) {
|
||||
textDiv.setAttribute('data-raw', stream.content);
|
||||
if(markdownEnabled) {
|
||||
textDiv.innerHTML = marked.parse(stream.content);
|
||||
textDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
||||
} else textDiv.innerText = stream.content;
|
||||
}
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
|
||||
function handleStreamChunk(content, type='chat', taskMeta=null) {
|
||||
if(type === 'chat') {
|
||||
if(!streamingChat.element) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'ai streaming';
|
||||
wrapper.innerHTML = `<div class="message-text markdown-body" data-raw=""></div>`;
|
||||
streamingChat.element = wrapper;
|
||||
chatBox.appendChild(wrapper);
|
||||
streamingChat.content = '';
|
||||
streamingChat.active = true;
|
||||
}
|
||||
streamingChat.content += content;
|
||||
if(streamingChat.timer) clearTimeout(streamingChat.timer);
|
||||
streamingChat.timer = setTimeout(() => updateStreamingDisplay('chat'), 50);
|
||||
}
|
||||
else if(type === 'task' && taskMeta && taskMeta.executionId) {
|
||||
const eid = taskMeta.executionId;
|
||||
if(!streamingTasks[eid]) {
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'task-separator';
|
||||
sep.innerHTML = `<i class="fas fa-bolt"></i> 定时任务 #${taskMeta.taskId} (${taskMeta.taskTitle || '执行'}) 第${taskMeta.triggerCount||1}次执行`;
|
||||
chatBox.appendChild(sep);
|
||||
const taskDiv = document.createElement('div');
|
||||
taskDiv.className = 'task-msg streaming';
|
||||
taskDiv.style.cssText = 'background:#2d2a1e; border-radius:16px; margin:6px 0; padding:8px 12px;';
|
||||
taskDiv.innerHTML = `<div class="message-text markdown-body"></div>`;
|
||||
chatBox.appendChild(taskDiv);
|
||||
streamingTasks[eid] = { element: taskDiv, content: '', timer: null };
|
||||
activeTaskCount++;
|
||||
updateMsgStats();
|
||||
}
|
||||
streamingTasks[eid].content += content;
|
||||
if(streamingTasks[eid].timer) clearTimeout(streamingTasks[eid].timer);
|
||||
streamingTasks[eid].timer = setTimeout(() => {
|
||||
const st = streamingTasks[eid];
|
||||
if(st && st.element) {
|
||||
const textDiv = st.element.querySelector('.message-text');
|
||||
if(textDiv) {
|
||||
if(markdownEnabled) textDiv.innerHTML = marked.parse(st.content);
|
||||
else textDiv.innerText = st.content;
|
||||
textDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
||||
}
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function completeStream(type='chat', execId=null) {
|
||||
if(type==='chat' && streamingChat.element) {
|
||||
if(streamingChat.timer) clearTimeout(streamingChat.timer);
|
||||
updateStreamingDisplay('chat');
|
||||
streamingChat.element.classList.remove('streaming');
|
||||
globalMsgCount++;
|
||||
updateMsgStats();
|
||||
streamingChat = { element: null, content: '', timer: null, active: false };
|
||||
} else if(type==='task' && execId && streamingTasks[execId]) {
|
||||
const task = streamingTasks[execId];
|
||||
if(task.timer) clearTimeout(task.timer);
|
||||
if(task.element) {
|
||||
const txtDiv = task.element.querySelector('.message-text');
|
||||
if(txtDiv && markdownEnabled) txtDiv.innerHTML = marked.parse(task.content);
|
||||
task.element.classList.remove('streaming');
|
||||
}
|
||||
globalMsgCount++;
|
||||
delete streamingTasks[execId];
|
||||
activeTaskCount = Math.max(0, activeTaskCount-1);
|
||||
updateMsgStats();
|
||||
}
|
||||
}
|
||||
|
||||
function handleWSMessage(data) {
|
||||
if(data.type === 'chunk') handleStreamChunk(data.content, 'chat');
|
||||
else if(data.type === 'complete') completeStream('chat');
|
||||
else if(data.type === 'dialogue_ended') {
|
||||
addSystemMessage("✨ 对话已结束,可开启新对话");
|
||||
if(streamingChat.element) completeStream('chat');
|
||||
}
|
||||
else if(data.type === 'task_process') {
|
||||
if(data.msg_type === 'chunk') {
|
||||
handleStreamChunk(data.content, 'task', { taskId: data.task_id, executionId: data.execution_id, taskTitle: data.task_title, triggerCount: (data.execution_id?.split('_')[2]) });
|
||||
} else if(data.msg_type === 'complete') completeStream('task', data.execution_id);
|
||||
}
|
||||
else if(data.type === 'task_status') addSystemMessage(`📌 任务${data.task_id}: ${data.title} - ${data.status}`);
|
||||
else if(data.type === 'error') addSystemMessage(`❌ ${data.content}`, true);
|
||||
else if(data.type === 'command_result') addSystemMessage(`📋 ${data.content}`);
|
||||
else if(data.type === 'info') addSystemMessage(`ℹ️ ${data.content}`);
|
||||
}
|
||||
|
||||
function sendWSMessage(message, isCommand=false) {
|
||||
if(!socket || !socket.connected) {
|
||||
addSystemMessage("未连接到AI服务器,请先点击连接", true);
|
||||
return false;
|
||||
}
|
||||
const payload = isCommand ? { type: 'command', command: message } : { type: 'message', content: message };
|
||||
socket.emit('ws_send', { message: JSON.stringify(payload) });
|
||||
return true;
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
if(socket && socket.connected) {
|
||||
socket.disconnect();
|
||||
}
|
||||
|
||||
targetWsUrl = document.getElementById('wsUrlInput').value.trim();
|
||||
if(!targetWsUrl) {
|
||||
addSystemMessage("请输入WebSocket服务器地址", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!targetWsUrl.startsWith('ws://') && !targetWsUrl.startsWith('wss://')) {
|
||||
addSystemMessage("WebSocket地址必须以 ws:// 或 wss:// 开头", true);
|
||||
return;
|
||||
}
|
||||
|
||||
manualDisconnect = false;
|
||||
|
||||
socket = io({
|
||||
transports: ['websocket', 'polling'],
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Socket.IO connected, SID:', socket.id);
|
||||
updateConnectionUI(true);
|
||||
reconnectAttempts = 0;
|
||||
connectStartTime = new Date();
|
||||
document.getElementById('connectTime').innerText = connectStartTime.toLocaleTimeString();
|
||||
addSystemMessage(`✅ Socket.IO 连接成功!正在连接目标 WebSocket: ${targetWsUrl}`);
|
||||
socket.emit('ws_connect', { target_url: targetWsUrl });
|
||||
});
|
||||
|
||||
socket.on('connected', (data) => {
|
||||
addSystemMessage(`✅ ${data.message}`);
|
||||
});
|
||||
|
||||
socket.on('ws_connected', (data) => {
|
||||
addSystemMessage(`✅ ${data.message}`);
|
||||
if(streamingChat.element) completeStream('chat');
|
||||
});
|
||||
|
||||
socket.on('ws_message', (data) => {
|
||||
try {
|
||||
const messageData = JSON.parse(data.data);
|
||||
handleWSMessage(messageData);
|
||||
} catch(err) {
|
||||
handleStreamChunk(data.data, 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('ws_error', (data) => {
|
||||
addSystemMessage(`❌ ${data.error}`, true);
|
||||
updateConnectionUI(false);
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log('Socket.IO disconnected:', reason);
|
||||
updateConnectionUI(false);
|
||||
|
||||
if(!manualDisconnect && reconnectAttempts < 3) {
|
||||
reconnectAttempts++;
|
||||
addSystemMessage(`⚠️ 连接断开 (${reason}),${reconnectAttempts}/3 次重试...`);
|
||||
if(reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = setTimeout(() => connectWebSocket(), 2000);
|
||||
} else if(!manualDisconnect) {
|
||||
addSystemMessage(`❌ 连接断开 (${reason}),请手动重连`);
|
||||
} else {
|
||||
addSystemMessage("已手动断开连接");
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Socket.IO connection error:', error);
|
||||
addSystemMessage(`❌ 连接错误: ${error.message}`, true);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectWebSocket() {
|
||||
manualDisconnect = true;
|
||||
if(socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
targetWsUrl = null;
|
||||
updateConnectionUI(false);
|
||||
}
|
||||
|
||||
function updateConnectionUI(connected) {
|
||||
const dot = document.getElementById('wsStatusDot');
|
||||
const text = document.getElementById('wsStatusText');
|
||||
const connectBtn = document.getElementById('wsConnectBtn');
|
||||
const disconnectBtn = document.getElementById('wsDisconnectBtn');
|
||||
const statsPanel = document.getElementById('statsPanel');
|
||||
|
||||
if(connected) {
|
||||
dot.style.background = "#4CAF50";
|
||||
text.innerText = "已连接 (Socket.IO)";
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
statsPanel.style.display = "block";
|
||||
} else {
|
||||
dot.style.background = "#f44336";
|
||||
text.innerText = "未连接";
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
statsPanel.style.display = "none";
|
||||
activeTaskCount = 0;
|
||||
globalMsgCount = 0;
|
||||
updateMsgStats();
|
||||
Object.keys(streamingTasks).forEach(k => completeStream('task', k));
|
||||
if(streamingChat.element) completeStream('chat');
|
||||
}
|
||||
}
|
||||
|
||||
function onSendMessage() {
|
||||
let text = msgInput.value.trim();
|
||||
if(!text) return;
|
||||
|
||||
const userDiv = document.createElement('div');
|
||||
userDiv.className = 'user';
|
||||
userDiv.innerText = text;
|
||||
chatBox.appendChild(userDiv);
|
||||
|
||||
msgInput.value = '';
|
||||
autoResizeTextarea();
|
||||
msgInput.focus();
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
|
||||
if(socket && socket.connected) {
|
||||
if(text.startsWith('/')) sendWSMessage(text, true);
|
||||
else sendWSMessage(text, false);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
let reply = "喵~ 请先连接WebSocket服务器获取智能回复~ ✨\n💡 提示:多行文本也支持哦!\n🔌 连接将通过 Socket.IO 代理服务转发";
|
||||
const aiDiv = document.createElement('div');
|
||||
aiDiv.className = 'ai';
|
||||
aiDiv.innerText = `🐱 小猫AI:${reply}`;
|
||||
chatBox.appendChild(aiDiv);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
function autoResizeTextarea() {
|
||||
if (!msgInput) return;
|
||||
msgInput.style.height = 'auto';
|
||||
const newHeight = Math.min(msgInput.scrollHeight, 150);
|
||||
msgInput.style.height = newHeight + 'px';
|
||||
}
|
||||
|
||||
msgInput.addEventListener('input', autoResizeTextarea);
|
||||
autoResizeTextarea();
|
||||
|
||||
changeBgBtn.onclick = () => bgFile.click();
|
||||
bgFile.onchange = (e) => {
|
||||
let f = e.target.files[0];
|
||||
if(f) {
|
||||
let url = URL.createObjectURL(f);
|
||||
document.body.style.backgroundImage = `url(${url})`;
|
||||
document.body.style.backgroundSize = "cover";
|
||||
document.body.style.backgroundPosition = "center center";
|
||||
document.body.style.backgroundRepeat = "no-repeat";
|
||||
if(eyeProtectActive) {
|
||||
eyeProtectActive = false;
|
||||
document.body.classList.remove('eye-protect');
|
||||
addSystemMessage("护眼模式已自动关闭(背景已更换)");
|
||||
}
|
||||
addSystemMessage("✨ 背景已更换");
|
||||
}
|
||||
};
|
||||
|
||||
eyeModeBtn.onclick = () => {
|
||||
if(!eyeProtectActive) {
|
||||
document.body.classList.add('eye-protect');
|
||||
document.body.style.backgroundImage = "none";
|
||||
eyeProtectActive=true;
|
||||
addSystemMessage("护眼模式已开启(背景图片暂时隐藏)");
|
||||
eyeModeBtn.style.background = "rgba(60, 140, 80, 0.9)";
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove('eye-protect');
|
||||
document.body.style.backgroundImage = "url('/imgs/background.jpeg')";
|
||||
document.body.style.backgroundSize = "cover";
|
||||
document.body.style.backgroundPosition = "center center";
|
||||
document.body.style.backgroundRepeat = "no-repeat";
|
||||
eyeProtectActive=false;
|
||||
addSystemMessage("护眼模式关闭,恢复背景");
|
||||
eyeModeBtn.style.background = "rgba(100, 180, 120, 0.8)";
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('wsConnectBtn').onclick = connectWebSocket;
|
||||
document.getElementById('wsDisconnectBtn').onclick = disconnectWebSocket;
|
||||
document.querySelectorAll('.cmd-btn').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
let cmd = btn.getAttribute('data-cmd');
|
||||
msgInput.value = cmd + ' ';
|
||||
autoResizeTextarea();
|
||||
msgInput.focus();
|
||||
};
|
||||
});
|
||||
|
||||
sendBtn.onclick = onSendMessage;
|
||||
|
||||
msgInput.addEventListener('keydown', (e) => {
|
||||
if(e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.style.backgroundImage = "url('/imgs/background.jpeg')";
|
||||
document.body.style.backgroundSize = "cover";
|
||||
document.body.style.backgroundPosition = "center center";
|
||||
document.body.style.backgroundRepeat = "no-repeat";
|
||||
|
||||
msgInput.focus();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1687
File diff suppressed because it is too large.
Load diff
+8146
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
# backend
|
||||
websockets
|
||||
requests
|
||||
beautifulsoup4
|
||||
|
||||
# frontend
|
||||
websockets>=11.0
|
||||
prompt-toolkit>=3.0.0
|
||||
rich>=13.0.0
|
||||
colorama>=0.4.6
|
||||
|
||||
# voice
|
||||
SpeechRecognition>=3.10.0
|
||||
pyttsx3>=2.90
|
||||
pyaudio>=0.2.13
|
||||
# sudo apt install libportaudio2 portaudio19-dev
|
||||
Executable
+593
@@ -0,0 +1,593 @@
|
||||
#!/usr/bin/env python3
|
||||
"""163邮箱工具 - 发送邮件、接收邮件、查看收件箱。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import imaplib
|
||||
import smtplib
|
||||
import email
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import decode_header
|
||||
from email.utils import parseaddr
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
import base64
|
||||
import re
|
||||
|
||||
# ==================== 配置常量 ====================
|
||||
|
||||
CREDENTIALS_FILE = os.path.expanduser("~/.cert/163mail.json")
|
||||
IMAP_SERVER = "imap.163.com"
|
||||
IMAP_PORT = 993
|
||||
SMTP_SERVER = "smtp.163.com"
|
||||
SMTP_PORT = 465
|
||||
|
||||
|
||||
# ==================== 凭证管理 ====================
|
||||
|
||||
def load_credentials():
|
||||
"""加载163邮箱凭证"""
|
||||
if not os.path.exists(CREDENTIALS_FILE):
|
||||
return None, f"❌ 凭证文件不存在: {CREDENTIALS_FILE}\n请创建该文件,格式:{{\"username\": \"your_email@163.com\", \"password\": \"your_password\"}}"
|
||||
|
||||
try:
|
||||
with open(CREDENTIALS_FILE, 'r', encoding='utf-8') as f:
|
||||
creds = json.load(f)
|
||||
|
||||
username = creds.get('username')
|
||||
password = creds.get('password')
|
||||
|
||||
if not username or not password:
|
||||
return None, "❌ 凭证文件格式错误,需要包含 username 和 password 字段"
|
||||
|
||||
if '@' not in username:
|
||||
username = f"{username}@163.com"
|
||||
|
||||
return (username, password), None
|
||||
except json.JSONDecodeError:
|
||||
return None, f"❌ 凭证文件JSON格式错误: {CREDENTIALS_FILE}"
|
||||
except Exception as e:
|
||||
return None, f"❌ 读取凭证文件失败: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 邮件发送功能 ====================
|
||||
|
||||
def send_email(to_addrs, subject, body, cc_addrs=None, bcc_addrs=None, html=False):
|
||||
"""发送邮件"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
if isinstance(to_addrs, str):
|
||||
to_addrs = [to_addrs]
|
||||
|
||||
cc_list = []
|
||||
if cc_addrs:
|
||||
cc_list = [cc_addrs] if isinstance(cc_addrs, str) else cc_addrs
|
||||
|
||||
bcc_list = []
|
||||
if bcc_addrs:
|
||||
bcc_list = [bcc_addrs] if isinstance(bcc_addrs, str) else bcc_addrs
|
||||
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = username
|
||||
msg['To'] = ', '.join(to_addrs)
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = email.utils.formatdate(localtime=True)
|
||||
|
||||
if cc_list:
|
||||
msg['Cc'] = ', '.join(cc_list)
|
||||
|
||||
if html:
|
||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
else:
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
all_recipients = to_addrs + cc_list + bcc_list
|
||||
|
||||
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=30) as server:
|
||||
server.login(username, password)
|
||||
server.sendmail(username, all_recipients, msg.as_string())
|
||||
|
||||
result = f"✅ 邮件发送成功!\n"
|
||||
result += f" 📤 发件人: {username}\n"
|
||||
result += f" 📥 收件人: {', '.join(to_addrs)}\n"
|
||||
result += f" 📧 主题: {subject}\n"
|
||||
if cc_list:
|
||||
result += f" 👥 抄送: {', '.join(cc_list)}\n"
|
||||
result += f" 📏 正文长度: {len(body)} 字符"
|
||||
|
||||
return True, result
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
return False, "❌ SMTP认证失败,请检查邮箱密码。\n注意:163邮箱需要使用授权码而不是登录密码"
|
||||
except smtplib.SMTPException as e:
|
||||
return False, f"❌ SMTP错误: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"❌ 发送邮件失败: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 邮件接收功能 ====================
|
||||
|
||||
def decode_mime_header(header):
|
||||
"""解码MIME邮件头"""
|
||||
if header is None:
|
||||
return ""
|
||||
|
||||
decoded_parts = decode_header(header)
|
||||
result = []
|
||||
for content, charset in decoded_parts:
|
||||
if isinstance(content, bytes):
|
||||
if charset:
|
||||
try:
|
||||
result.append(content.decode(charset))
|
||||
except:
|
||||
result.append(content.decode('utf-8', errors='ignore'))
|
||||
else:
|
||||
try:
|
||||
result.append(content.decode('utf-8', errors='ignore'))
|
||||
except:
|
||||
result.append(content.decode('gbk', errors='ignore'))
|
||||
else:
|
||||
result.append(content)
|
||||
return ' '.join(result)
|
||||
|
||||
|
||||
def get_email_body(msg):
|
||||
"""获取邮件正文"""
|
||||
body = ""
|
||||
html_body = ""
|
||||
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
content_disposition = str(part.get("Content-Disposition"))
|
||||
|
||||
if "attachment" in content_disposition:
|
||||
continue
|
||||
|
||||
try:
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
try:
|
||||
decoded = payload.decode(charset, errors='ignore')
|
||||
except:
|
||||
decoded = payload.decode('utf-8', errors='ignore')
|
||||
|
||||
if content_type == "text/plain":
|
||||
body += decoded
|
||||
elif content_type == "text/html":
|
||||
html_body += decoded
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
content_type = msg.get_content_type()
|
||||
try:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = msg.get_content_charset() or 'utf-8'
|
||||
decoded = payload.decode(charset, errors='ignore')
|
||||
if content_type == "text/plain":
|
||||
body = decoded
|
||||
elif content_type == "text/html":
|
||||
html_body = decoded
|
||||
except:
|
||||
pass
|
||||
|
||||
return body if body else html_body
|
||||
|
||||
|
||||
def fetch_emails(limit=10, folder="INBOX", mark_seen=False, since_days=None):
|
||||
"""获取邮件列表"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
status, messages = imap.select(folder)
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, f"❌ 无法打开邮件夹 {folder}"
|
||||
|
||||
search_criteria = 'ALL'
|
||||
if since_days:
|
||||
date = (datetime.now() - timedelta(days=since_days)).strftime("%d-%b-%Y")
|
||||
search_criteria = f'SINCE "{date}"'
|
||||
|
||||
status, message_ids = imap.search(None, search_criteria)
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, "❌ 搜索邮件失败"
|
||||
|
||||
msg_ids = message_ids[0].split()
|
||||
total = len(msg_ids)
|
||||
|
||||
if total == 0:
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return True, f"📭 邮件夹 '{folder}' 中没有邮件"
|
||||
|
||||
start_idx = max(0, total - limit)
|
||||
msg_ids_to_fetch = msg_ids[start_idx:]
|
||||
|
||||
emails = []
|
||||
for i, msg_id in enumerate(reversed(msg_ids_to_fetch)):
|
||||
try:
|
||||
status, msg_data = imap.fetch(msg_id, '(RFC822)')
|
||||
if status != 'OK':
|
||||
continue
|
||||
|
||||
email_body = msg_data[0][1]
|
||||
msg = email.message_from_bytes(email_body)
|
||||
|
||||
subject = decode_mime_header(msg.get('Subject', '无主题'))
|
||||
from_addr = decode_mime_header(msg.get('From', '未知发件人'))
|
||||
to_addr = decode_mime_header(msg.get('To', '未知收件人'))
|
||||
date_str = msg.get('Date', '')
|
||||
|
||||
try:
|
||||
date_tuple = email.utils.parsedate_tz(date_str)
|
||||
if date_tuple:
|
||||
date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
|
||||
date_formatted = date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
date_formatted = date_str
|
||||
except:
|
||||
date_formatted = date_str
|
||||
|
||||
is_seen = '\\Seen' in msg.get('Flags', '')
|
||||
body = get_email_body(msg)
|
||||
preview = body[:200] + "..." if len(body) > 200 else body
|
||||
|
||||
name, email_addr = parseaddr(from_addr)
|
||||
|
||||
emails.append({
|
||||
'id': msg_id.decode() if isinstance(msg_id, bytes) else str(msg_id),
|
||||
'index': i + 1,
|
||||
'subject': subject,
|
||||
'from': from_addr,
|
||||
'from_name': name or email_addr,
|
||||
'from_email': email_addr,
|
||||
'to': to_addr,
|
||||
'date': date_formatted,
|
||||
'preview': preview,
|
||||
'is_seen': is_seen,
|
||||
'has_attachments': msg.get_content_maintype() == 'multipart' and any(part.get_filename() for part in msg.walk())
|
||||
})
|
||||
|
||||
if not mark_seen and not is_seen:
|
||||
imap.store(msg_id, '-FLAGS', '\\Seen')
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = f"📨 邮件夹 '{folder}' 中共有 {total} 封邮件,显示最新 {len(emails)} 封:\n\n"
|
||||
|
||||
for email_data in emails:
|
||||
seen_mark = " " if email_data['is_seen'] else "●"
|
||||
attach_mark = " 📎" if email_data.get('has_attachments') else ""
|
||||
result += f"{seen_mark} [{email_data['index']}] {email_data['date']}\n"
|
||||
result += f" 📧 主题: {email_data['subject']}{attach_mark}\n"
|
||||
result += f" 👤 发件人: {email_data['from']}\n"
|
||||
result += f" 📝 预览: {email_data['preview']}\n\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 获取邮件失败: {str(e)}"
|
||||
|
||||
|
||||
def read_email(msg_id, folder="INBOX", mark_seen=True):
|
||||
"""读取指定邮件内容"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
status, messages = imap.select(folder)
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, f"❌ 无法打开邮件夹 {folder}"
|
||||
|
||||
status, msg_data = imap.fetch(str(msg_id).encode(), '(RFC822)')
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, f"❌ 无法获取邮件 ID: {msg_id}"
|
||||
|
||||
email_body = msg_data[0][1]
|
||||
msg = email.message_from_bytes(email_body)
|
||||
|
||||
subject = decode_mime_header(msg.get('Subject', '无主题'))
|
||||
from_addr = decode_mime_header(msg.get('From', '未知发件人'))
|
||||
to_addr = decode_mime_header(msg.get('To', '未知收件人'))
|
||||
cc_addr = decode_mime_header(msg.get('Cc', ''))
|
||||
date_str = msg.get('Date', '')
|
||||
|
||||
try:
|
||||
date_tuple = email.utils.parsedate_tz(date_str)
|
||||
if date_tuple:
|
||||
date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
|
||||
date_formatted = date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
date_formatted = date_str
|
||||
except:
|
||||
date_formatted = date_str
|
||||
|
||||
body = get_email_body(msg)
|
||||
|
||||
attachments = []
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
filename = part.get_filename()
|
||||
if filename:
|
||||
filename = decode_mime_header(filename)
|
||||
size = len(part.get_payload(decode=True) or b'')
|
||||
attachments.append({'name': filename, 'size': size})
|
||||
|
||||
if not mark_seen:
|
||||
imap.store(str(msg_id).encode(), '-FLAGS', '\\Seen')
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = f"📧 邮件详情 (ID: {msg_id})\n"
|
||||
result += f"{'='*50}\n"
|
||||
result += f"📅 日期: {date_formatted}\n"
|
||||
result += f"📧 主题: {subject}\n"
|
||||
result += f"👤 发件人: {from_addr}\n"
|
||||
result += f"📥 收件人: {to_addr}\n"
|
||||
if cc_addr:
|
||||
result += f"👥 抄送: {cc_addr}\n"
|
||||
result += f"{'='*50}\n"
|
||||
result += f"📝 正文内容:\n{body}\n"
|
||||
|
||||
if attachments:
|
||||
result += f"\n📎 附件 ({len(attachments)}):\n"
|
||||
for att in attachments:
|
||||
size_str = f"{att['size']/1024:.1f}KB" if att['size'] > 0 else "0KB"
|
||||
result += f" - {att['name']} ({size_str})\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 读取邮件失败: {str(e)}"
|
||||
|
||||
|
||||
def list_folders():
|
||||
"""列出所有邮件夹"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
status, folders = imap.list()
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, "❌ 无法获取邮件夹列表"
|
||||
|
||||
folder_list = []
|
||||
for folder_info in folders:
|
||||
folder_str = folder_info.decode()
|
||||
parts = folder_str.split(' "/" ')
|
||||
if len(parts) > 1:
|
||||
folder_name = parts[1].strip('"')
|
||||
folder_list.append(folder_name)
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = "📂 邮件夹列表:\n"
|
||||
for i, folder in enumerate(folder_list, 1):
|
||||
result += f" {i}. {folder}\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 获取邮件夹列表失败: {str(e)}"
|
||||
|
||||
|
||||
def get_email_stats():
|
||||
"""获取邮箱统计信息"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
stats = {}
|
||||
total_unseen = 0
|
||||
folders = ['INBOX', 'Sent Messages', 'Drafts', 'Trash', 'Spam']
|
||||
|
||||
for folder in folders:
|
||||
try:
|
||||
status, messages = imap.select(folder)
|
||||
if status == 'OK':
|
||||
status, msg_ids = imap.search(None, 'ALL')
|
||||
if status == 'OK':
|
||||
count = len(msg_ids[0].split())
|
||||
stats[folder] = count
|
||||
|
||||
status, unseen_ids = imap.search(None, 'UNSEEN')
|
||||
if status == 'OK' and folder == 'INBOX':
|
||||
total_unseen = len(unseen_ids[0].split())
|
||||
except:
|
||||
continue
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = f"📊 邮箱统计 ({username})\n"
|
||||
result += f"{'='*50}\n"
|
||||
result += f"📥 收件箱: {stats.get('INBOX', 0)} 封 (未读: {total_unseen})\n"
|
||||
result += f"📤 已发送: {stats.get('Sent Messages', 0)} 封\n"
|
||||
result += f"📝 草稿箱: {stats.get('Drafts', 0)} 封\n"
|
||||
result += f"🗑️ 已删除: {stats.get('Trash', 0)} 封\n"
|
||||
result += f"⚠️ 垃圾邮件: {stats.get('Spam', 0)} 封\n"
|
||||
result += f"{'='*50}\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 获取统计信息失败: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 命令行接口 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="163邮箱工具 - 发送邮件、接收邮件、查看收件箱。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 发送邮件
|
||||
163mail --action send --to "friend@example.com" --subject "Hello" --body "邮件内容"
|
||||
|
||||
# 发送HTML邮件
|
||||
163mail --action send --to "friend@example.com" --subject "Hello" --body "<h1>标题</h1><p>内容</p>" --html
|
||||
|
||||
# 发送给多人,带抄送
|
||||
163mail --action send --to "a@example.com,b@example.com" --cc "cc@example.com" --subject "主题" --body "内容"
|
||||
|
||||
# 查看收件箱
|
||||
163mail --action inbox --limit 5
|
||||
|
||||
# 查看最近7天的邮件
|
||||
163mail --action inbox --since-days 7
|
||||
|
||||
# 读取指定邮件
|
||||
163mail --action read --msg-id 1
|
||||
|
||||
# 列出所有邮件夹
|
||||
163mail --action folders
|
||||
|
||||
# 查看邮箱统计
|
||||
163mail --action stats
|
||||
|
||||
环境变量:
|
||||
凭证文件: ~/.cert/163mail.json
|
||||
格式: {"username": "your_email@163.com", "password": "your_password"}
|
||||
|
||||
注意:
|
||||
- 163邮箱需要使用授权码而不是登录密码
|
||||
- 授权码需要在163邮箱网页版设置中获取
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["send", "inbox", "read", "folders", "stats"],
|
||||
help="操作类型")
|
||||
|
||||
# 发送邮件参数
|
||||
parser.add_argument("--to", "-t", help="收件人邮箱地址,多个地址用逗号分隔")
|
||||
parser.add_argument("--cc", help="抄送地址,多个地址用逗号分隔")
|
||||
parser.add_argument("--bcc", help="密送地址,多个地址用逗号分隔")
|
||||
parser.add_argument("--subject", "-s", help="邮件主题")
|
||||
parser.add_argument("--body", "-b", help="邮件正文")
|
||||
parser.add_argument("--html", action="store_true", help="是否HTML格式")
|
||||
|
||||
# 接收邮件参数
|
||||
parser.add_argument("--limit", "-l", type=int, default=10, help="获取邮件数量(默认10)")
|
||||
parser.add_argument("--folder", "-f", default="INBOX", help="邮件夹名称(默认INBOX)")
|
||||
parser.add_argument("--msg-id", type=int, help="邮件ID(用于read操作)")
|
||||
parser.add_argument("--since-days", type=int, help="获取最近几天的邮件")
|
||||
parser.add_argument("--mark-seen", action="store_true", help="是否标记为已读")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 参数验证
|
||||
if args.action == "send":
|
||||
if not args.to or not args.subject or not args.body:
|
||||
print("❌ 发送邮件需要提供 --to, --subject, --body 参数")
|
||||
sys.exit(1)
|
||||
|
||||
if args.action == "read" and not args.msg_id:
|
||||
print("❌ 读取邮件需要提供 --msg-id 参数")
|
||||
sys.exit(1)
|
||||
|
||||
# 执行操作
|
||||
try:
|
||||
if args.action == "send":
|
||||
to_list = [addr.strip() for addr in args.to.split(',') if addr.strip()]
|
||||
cc_list = [addr.strip() for addr in args.cc.split(',') if addr.strip()] if args.cc else None
|
||||
bcc_list = [addr.strip() for addr in args.bcc.split(',') if addr.strip()] if args.bcc else None
|
||||
|
||||
success, message = send_email(
|
||||
to_addrs=to_list,
|
||||
subject=args.subject,
|
||||
body=args.body,
|
||||
cc_addrs=cc_list,
|
||||
bcc_addrs=bcc_list,
|
||||
html=args.html
|
||||
)
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "inbox":
|
||||
success, message = fetch_emails(
|
||||
limit=args.limit,
|
||||
folder=args.folder,
|
||||
mark_seen=args.mark_seen,
|
||||
since_days=args.since_days
|
||||
)
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "read":
|
||||
success, message = read_email(
|
||||
msg_id=args.msg_id,
|
||||
folder=args.folder,
|
||||
mark_seen=args.mark_seen
|
||||
)
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "folders":
|
||||
success, message = list_folders()
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "stats":
|
||||
success, message = get_email_stats()
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+431
@@ -0,0 +1,431 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ADS1115: Linux I2C 读取 TI ADS1115 16位 ADC 的命令行工具
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
# ============================================================
|
||||
# ADS1115 寄存器地址
|
||||
# ============================================================
|
||||
ADS1115_REG_CONVERSION = 0x00
|
||||
ADS1115_REG_CONFIG = 0x01
|
||||
ADS1115_REG_LO_THRESH = 0x02
|
||||
ADS1115_REG_HI_THRESH = 0x03
|
||||
|
||||
# ============================================================
|
||||
# 配置寄存器 bit 定义 (16 位)
|
||||
# ============================================================
|
||||
|
||||
# --- MUX 输入通道 ---
|
||||
ADS1115_MUX_AIN0_AIN1 = 0x0000 # 差分: P=AIN0, N=AIN1 (默认)
|
||||
ADS1115_MUX_AIN0_AIN3 = 0x1000 # 差分: P=AIN0, N=AIN3
|
||||
ADS1115_MUX_AIN1_AIN3 = 0x2000 # 差分: P=AIN1, N=AIN3
|
||||
ADS1115_MUX_AIN2_AIN3 = 0x3000 # 差分: P=AIN2, N=AIN3
|
||||
ADS1115_MUX_AIN0_GND = 0x4000 # 单端: AIN0
|
||||
ADS1115_MUX_AIN1_GND = 0x5000 # 单端: AIN1
|
||||
ADS1115_MUX_AIN2_GND = 0x6000 # 单端: AIN2
|
||||
ADS1115_MUX_AIN3_GND = 0x7000 # 单端: AIN3
|
||||
|
||||
# --- PGA 增益 (满量程范围) ---
|
||||
ADS1115_PGA_6_144V = 0x0000 # ±6.144V (1LSB=0.1875mV)
|
||||
ADS1115_PGA_4_096V = 0x0200 # ±4.096V (1LSB=0.125mV)
|
||||
ADS1115_PGA_2_048V = 0x0400 # ±2.048V (1LSB=0.0625mV) (默认)
|
||||
ADS1115_PGA_1_024V = 0x0600 # ±1.024V (1LSB=0.03125mV)
|
||||
ADS1115_PGA_0_512V = 0x0800 # ±0.512V (1LSB=0.015625mV)
|
||||
ADS1115_PGA_0_256V = 0x0A00 # ±0.256V (1LSB=0.0078125mV)
|
||||
|
||||
PGA_VALUES = [ADS1115_PGA_6_144V, ADS1115_PGA_4_096V, ADS1115_PGA_2_048V,
|
||||
ADS1115_PGA_1_024V, ADS1115_PGA_0_512V, ADS1115_PGA_0_256V]
|
||||
PGA_FS = [6.144, 4.096, 2.048, 1.024, 0.512, 0.256]
|
||||
PGA_NAMES = ["±6.144V", "±4.096V", "±2.048V", "±1.024V", "±0.512V", "±0.256V"]
|
||||
|
||||
# --- 数据速率 ---
|
||||
ADS1115_DR_8SPS = 0x0000 # 8 SPS
|
||||
ADS1115_DR_16SPS = 0x0020 # 16 SPS
|
||||
ADS1115_DR_32SPS = 0x0040 # 32 SPS
|
||||
ADS1115_DR_64SPS = 0x0060 # 64 SPS
|
||||
ADS1115_DR_128SPS = 0x0080 # 128 SPS (默认)
|
||||
ADS1115_DR_250SPS = 0x00A0 # 250 SPS
|
||||
ADS1115_DR_475SPS = 0x00C0 # 475 SPS
|
||||
ADS1115_DR_860SPS = 0x00E0 # 860 SPS
|
||||
|
||||
DR_VALUES = [ADS1115_DR_8SPS, ADS1115_DR_16SPS, ADS1115_DR_32SPS,
|
||||
ADS1115_DR_64SPS, ADS1115_DR_128SPS, ADS1115_DR_250SPS,
|
||||
ADS1115_DR_475SPS, ADS1115_DR_860SPS]
|
||||
DR_NAMES = ["8 SPS", "16 SPS", "32 SPS", "64 SPS",
|
||||
"128 SPS", "250 SPS", "475 SPS", "860 SPS"]
|
||||
|
||||
# --- 比较器 ---
|
||||
ADS1115_COMP_QUE_DISABLE = 0x0003
|
||||
|
||||
# --- 操作模式 ---
|
||||
ADS1115_MODE_CONTINUOUS = 0x0000
|
||||
ADS1115_MODE_SINGLE = 0x0100
|
||||
|
||||
# --- 启动转换 ---
|
||||
ADS1115_OS_START = 0x8000
|
||||
|
||||
|
||||
class ADS1115:
|
||||
"""ADS1115 16位ADC驱动"""
|
||||
|
||||
def __init__(self, bus=1, address=0x48, pga_idx=2, dr_idx=4):
|
||||
"""
|
||||
初始化 ADS1115
|
||||
|
||||
:param bus: I2C 总线号 (默认1)
|
||||
:param address: I2C 设备地址 (默认0x48)
|
||||
:param pga_idx: PGA增益索引 0~5 (默认2 = ±2.048V)
|
||||
:param dr_idx: 数据速率索引 0~7 (默认4 = 128SPS)
|
||||
"""
|
||||
self.address = address
|
||||
self.pga_idx = pga_idx if 0 <= pga_idx <= 5 else 2
|
||||
self.dr_idx = dr_idx if 0 <= dr_idx <= 7 else 4
|
||||
self.fs_voltage = PGA_FS[self.pga_idx]
|
||||
self.config = 0
|
||||
|
||||
# 初始化 I2C
|
||||
try:
|
||||
from smbus2 import SMBus
|
||||
self.bus = SMBus(bus)
|
||||
except ImportError:
|
||||
print("错误: 需要 smbus2 库,请执行: pip install smbus2", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
print(f"错误: I2C 总线 /dev/i2c-{bus} 不存在", file=sys.stderr)
|
||||
print("提示: 确保已启用 I2C (sudo raspi-config -> Interface Options -> I2C)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except PermissionError:
|
||||
print(f"错误: 无法访问 I2C 总线 /dev/i2c-{bus},请将用户加入 i2c 组", file=sys.stderr)
|
||||
print(" sudo usermod -aG i2c $USER", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化配置
|
||||
self._init_device()
|
||||
|
||||
def _write_word(self, reg, value):
|
||||
"""向寄存器写入2字节 (大端)"""
|
||||
high = (value >> 8) & 0xFF
|
||||
low = value & 0xFF
|
||||
self.bus.write_i2c_block_data(self.address, reg, [high, low])
|
||||
|
||||
def _read_word(self, reg):
|
||||
"""从寄存器读取2字节 (大端)"""
|
||||
data = self.bus.read_i2c_block_data(self.address, reg, 2)
|
||||
return (data[0] << 8) | data[1]
|
||||
|
||||
def _init_device(self):
|
||||
"""初始化 ADS1115 配置"""
|
||||
self.config = (ADS1115_MUX_AIN0_GND |
|
||||
PGA_VALUES[self.pga_idx] |
|
||||
ADS1115_MODE_SINGLE |
|
||||
DR_VALUES[self.dr_idx] |
|
||||
ADS1115_COMP_QUE_DISABLE)
|
||||
self._write_word(ADS1115_REG_CONFIG, self.config)
|
||||
|
||||
def read_single(self, channel):
|
||||
"""
|
||||
读取单端通道 (单次转换)
|
||||
|
||||
:param channel: 通道号 0~3 (对应 AIN0~AIN3)
|
||||
:return: 电压值 (V)
|
||||
"""
|
||||
if channel < 0 or channel > 3:
|
||||
raise ValueError(f"通道号无效: {channel} (有效: 0~3)")
|
||||
|
||||
# 设置通道并启动转换
|
||||
mux = ADS1115_MUX_AIN0_GND | (channel << 12) # AINch-GND
|
||||
cfg = (self.config & ~0x7000) | mux | ADS1115_OS_START
|
||||
self._write_word(ADS1115_REG_CONFIG, cfg)
|
||||
self.config = cfg
|
||||
|
||||
# 等待转换完成
|
||||
wait_ms = 1000 // (8 << self.dr_idx) + 3
|
||||
time.sleep(wait_ms / 1000.0)
|
||||
|
||||
# 读取转换值
|
||||
raw = self._read_word(ADS1115_REG_CONVERSION)
|
||||
raw_signed = raw if raw < 32768 else raw - 65536
|
||||
|
||||
# 转换为电压: V = raw * FS_VOLTAGE / 32768
|
||||
voltage = raw_signed * self.fs_voltage / 32768.0
|
||||
return voltage
|
||||
|
||||
def read_diff(self, pos, neg):
|
||||
"""
|
||||
读取差分通道 (单次转换)
|
||||
|
||||
支持组合: (0,1), (0,3), (1,3), (2,3)
|
||||
:param pos: 正极通道 (0~3)
|
||||
:param neg: 负极通道 (0~3)
|
||||
:return: 电压值 (V)
|
||||
"""
|
||||
mux_map = {
|
||||
(0, 1): ADS1115_MUX_AIN0_AIN1,
|
||||
(0, 3): ADS1115_MUX_AIN0_AIN3,
|
||||
(1, 3): ADS1115_MUX_AIN1_AIN3,
|
||||
(2, 3): ADS1115_MUX_AIN2_AIN3,
|
||||
}
|
||||
key = (pos, neg)
|
||||
if key not in mux_map:
|
||||
raise ValueError(f"不支持的差分组合: AIN{pos} - AIN{neg},支持: 0-1, 0-3, 1-3, 2-3")
|
||||
|
||||
mux = mux_map[key]
|
||||
cfg = (self.config & ~0x7000) | mux | ADS1115_OS_START
|
||||
self._write_word(ADS1115_REG_CONFIG, cfg)
|
||||
self.config = cfg
|
||||
|
||||
# 等待转换完成
|
||||
wait_ms = 1000 // (8 << self.dr_idx) + 3
|
||||
time.sleep(wait_ms / 1000.0)
|
||||
|
||||
raw = self._read_word(ADS1115_REG_CONVERSION)
|
||||
raw_signed = raw if raw < 32768 else raw - 65536
|
||||
|
||||
voltage = raw_signed * self.fs_voltage / 32768.0
|
||||
return voltage
|
||||
|
||||
def start_continuous(self, channel):
|
||||
"""启动连续模式"""
|
||||
if channel < 0 or channel > 3:
|
||||
raise ValueError(f"通道号无效: {channel}")
|
||||
cfg = (self.config & ~0x7100 |
|
||||
(ADS1115_MUX_AIN0_GND | (channel << 12)) |
|
||||
ADS1115_MODE_CONTINUOUS |
|
||||
ADS1115_COMP_QUE_DISABLE)
|
||||
self._write_word(ADS1115_REG_CONFIG, cfg)
|
||||
self.config = cfg
|
||||
|
||||
def read_continuous(self):
|
||||
"""连续模式读数"""
|
||||
raw = self._read_word(ADS1115_REG_CONVERSION)
|
||||
raw_signed = raw if raw < 32768 else raw - 65536
|
||||
return raw_signed * self.fs_voltage / 32768.0
|
||||
|
||||
def scan_all(self):
|
||||
"""扫描所有单端通道"""
|
||||
results = {}
|
||||
ch_names = ["AIN0", "AIN1", "AIN2", "AIN3"]
|
||||
for ch in range(4):
|
||||
v = self.read_single(ch)
|
||||
results[ch_names[ch]] = v
|
||||
return results
|
||||
|
||||
def print_config(self):
|
||||
"""打印配置寄存器详情"""
|
||||
config = self._read_word(ADS1115_REG_CONFIG)
|
||||
mux = (config >> 12) & 0x07
|
||||
pga = (config >> 9) & 0x07
|
||||
mode = (config >> 8) & 0x01
|
||||
dr = (config >> 5) & 0x07
|
||||
comp = config & 0x03
|
||||
|
||||
mux_names = {
|
||||
0: "AIN0-AIN1 (差分)",
|
||||
1: "AIN0-AIN3 (差分)",
|
||||
2: "AIN1-AIN3 (差分)",
|
||||
3: "AIN2-AIN3 (差分)",
|
||||
4: "AIN0-GND (单端)",
|
||||
5: "AIN1-GND (单端)",
|
||||
6: "AIN2-GND (单端)",
|
||||
7: "AIN3-GND (单端)",
|
||||
}
|
||||
|
||||
print(f"\n===== ADS1115 寄存器 =====")
|
||||
print(f" 配置寄存器: 0x{config:04X}")
|
||||
print(f" MUX: {mux} -> {mux_names.get(mux, '未知')}")
|
||||
if pga <= 5:
|
||||
print(f" PGA: {pga} -> {PGA_NAMES[pga]} (满量程 {PGA_FS[pga]:.3f}V)")
|
||||
else:
|
||||
print(f" PGA: {pga} -> 未知")
|
||||
print(f" 模式: {'单次' if mode else '连续'}")
|
||||
if dr <= 7:
|
||||
print(f" 速率: {dr} -> {DR_NAMES[dr]}")
|
||||
else:
|
||||
print(f" 速率: {dr} -> 未知")
|
||||
print(f" 比较器: {'禁用' if comp == 3 else '启用'}")
|
||||
|
||||
# 读取阈值
|
||||
lo_raw = self._read_word(ADS1115_REG_LO_THRESH)
|
||||
hi_raw = self._read_word(ADS1115_REG_HI_THRESH)
|
||||
lo_signed = lo_raw if lo_raw < 32768 else lo_raw - 65536
|
||||
hi_signed = hi_raw if hi_raw < 32768 else hi_raw - 65536
|
||||
lo_v = lo_signed * self.fs_voltage / 32768.0
|
||||
hi_v = hi_signed * self.fs_voltage / 32768.0
|
||||
print(f" 阈值: LO={lo_signed} ({lo_v:.6f}V), HI={hi_signed} ({hi_v:.6f}V)")
|
||||
|
||||
def close(self):
|
||||
"""关闭 I2C 总线"""
|
||||
self.bus.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Linux I2C 读取 TI ADS1115 16位 ADC 的命令行工具",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-b, --i2c-bus I2C 总线号 (默认 1, 对应 /dev/i2c-1)
|
||||
-a, --address I2C 设备地址 (默认 0x48)
|
||||
-p, --pga PGA 增益索引 0~5 (默认 2=±2.048V)
|
||||
0=±6.144V, 1=±4.096V, 2=±2.048V
|
||||
3=±1.024V, 4=±0.512V, 5=±0.256V
|
||||
-r, --rate 数据速率索引 0~7 (默认 4=128SPS)
|
||||
0=8, 1=16, 2=32, 3=64
|
||||
4=128, 5=250, 6=475, 7=860 (SPS)
|
||||
-c, --channel 单端通道号 0~3 (读取 AIN0~AIN3)
|
||||
--diff 差分模式,格式 "pos,neg" 如 "0,1"
|
||||
--scan 扫描所有单端通道
|
||||
--continuous 连续读取模式
|
||||
-n, --count 读取次数 (默认 1)
|
||||
--delay 每次读取间隔毫秒 (默认 10ms)
|
||||
--config 显示配置寄存器详情
|
||||
|
||||
--percent 输出基于 0-5V 范围的百分比 (0.00%~100.00%)
|
||||
--map 将 0-5V 百分比映射到自定义数值区间,格式 "min,max"
|
||||
例: --map -20,80 表示 0V=-20, 5V=80
|
||||
使用 --map 时自动启用百分比显示
|
||||
|
||||
使用示例:
|
||||
ads1115 # 读取 AIN0 (默认参数)
|
||||
ads1115 -c 2 # 读取 AIN2
|
||||
ads1115 --scan # 扫描所有单端通道
|
||||
ads1115 -c 0 -n 5 # 连续读 AIN0 5次
|
||||
ads1115 --diff 0,1 # 差分模式 AIN0-AIN1
|
||||
ads1115 -a 0x49 -p 1 -r 6 # 指定地址、PGA、速率
|
||||
ads1115 -c 0 --continuous -n 10 # 连续模式读10次
|
||||
ads1115 --config # 查看配置寄存器
|
||||
ads1115 --scan --config # 扫描并查看配置
|
||||
ads1115 -c 0 --percent # 读取 AIN0 并显示 0-5V 百分比
|
||||
ads1115 --scan --percent # 扫描所有通道并显示百分比
|
||||
ads1115 -c 0 --map -20,80 # 将 0-5V 映射到 -20~80 区间
|
||||
ads1115 --scan --map 0,100 # 扫描并映射到 0~100 区间
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-b", "--i2c-bus", type=int, default=1, help="I2C 总线号 (默认 1)")
|
||||
parser.add_argument("-a", "--address", type=lambda x: int(x, 0), default=0x48, help="I2C 设备地址 (默认 0x48)")
|
||||
parser.add_argument("-p", "--pga", type=int, default=2, choices=range(0, 6), metavar="{0..5}", help="PGA 增益索引 0~5 (默认 2)")
|
||||
parser.add_argument("-r", "--rate", type=int, default=4, choices=range(0, 8), metavar="{0..7}", help="数据速率索引 0~7 (默认 4)")
|
||||
parser.add_argument("-c", "--channel", type=int, default=None, choices=range(0, 4), metavar="{0..3}", help="单端通道号 0~3")
|
||||
parser.add_argument("--diff", type=str, default=None, help='差分模式,格式 "pos,neg" 如 "0,1"')
|
||||
parser.add_argument("--scan", action="store_true", help="扫描所有单端通道")
|
||||
parser.add_argument("--continuous", action="store_true", help="连续读取模式")
|
||||
parser.add_argument("-n", "--count", type=int, default=1, help="读取次数 (默认 1)")
|
||||
parser.add_argument("--delay", type=int, default=10, help="每次读取间隔毫秒 (默认 10ms)")
|
||||
parser.add_argument("--config", action="store_true", help="显示配置寄存器详情")
|
||||
parser.add_argument("--percent", action="store_true", help="输出基于 0-5V 范围的百分比")
|
||||
parser.add_argument("--map", type=str, default=None, metavar="MIN,MAX",
|
||||
help='将 0-5V 百分比映射到数值区间,格式 "min,max" 如 "-20,80"')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- 解析 --map 参数 ---
|
||||
map_range = None
|
||||
if args.map is not None:
|
||||
try:
|
||||
parts = args.map.split(",")
|
||||
if len(parts) != 2:
|
||||
raise ValueError
|
||||
map_min, map_max = float(parts[0]), float(parts[1])
|
||||
if map_min == map_max:
|
||||
raise ValueError("映射区间两端不能相同")
|
||||
map_range = (map_min, map_max)
|
||||
args.percent = True # --map 自动启用百分比显示
|
||||
except ValueError as e:
|
||||
err_msg = str(e) if str(e) != "" else "格式无效"
|
||||
print(f"错误: --map 参数无效 '{args.map}',请使用 'min,max' 格式,如 '-20,80' ({err_msg})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
adc = ADS1115(bus=args.i2c_bus, address=args.address,
|
||||
pga_idx=args.pga, dr_idx=args.rate)
|
||||
|
||||
def pct(v):
|
||||
"""返回 0-5V 百分比/映射后缀字符串"""
|
||||
if not args.percent:
|
||||
return ""
|
||||
p = v / 5.0 * 100.0
|
||||
suffix = f" [{p:6.2f}%]"
|
||||
if map_range is not None:
|
||||
mapped = map_range[0] + (p / 100.0) * (map_range[1] - map_range[0])
|
||||
suffix += f" (映射: {mapped:.2f})"
|
||||
return suffix
|
||||
|
||||
has_action = False
|
||||
|
||||
# --- 单端通道读取 ---
|
||||
if args.channel is not None and args.diff is None:
|
||||
has_action = True
|
||||
ch_names = ["AIN0", "AIN1", "AIN2", "AIN3"]
|
||||
ch_name = ch_names[args.channel]
|
||||
|
||||
if args.continuous:
|
||||
print(f"连续模式读取 {ch_name} ({args.count} 次):")
|
||||
adc.start_continuous(args.channel)
|
||||
time.sleep(0.05)
|
||||
for i in range(args.count):
|
||||
v = adc.read_continuous()
|
||||
print(f" [{i}] {ch_name}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
if i < args.count - 1:
|
||||
time.sleep(args.delay / 1000.0)
|
||||
else:
|
||||
print(f"读取 {ch_name} ({args.count} 次):")
|
||||
for i in range(args.count):
|
||||
v = adc.read_single(args.channel)
|
||||
print(f" [{i}] {ch_name}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
if i < args.count - 1:
|
||||
time.sleep(args.delay / 1000.0)
|
||||
|
||||
# --- 差分模式 ---
|
||||
if args.diff is not None:
|
||||
has_action = True
|
||||
try:
|
||||
parts = args.diff.split(",")
|
||||
if len(parts) != 2:
|
||||
raise ValueError
|
||||
pos, neg = int(parts[0]), int(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
print(f"错误: 差分格式无效 '{args.diff}',请使用 'pos,neg' 格式,如 '0,1'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"差分读取 AIN{pos} - AIN{neg} ({args.count} 次):")
|
||||
for i in range(args.count):
|
||||
v = adc.read_diff(pos, neg)
|
||||
print(f" [{i}] AIN{pos}-AIN{neg}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
if i < args.count - 1:
|
||||
time.sleep(args.delay / 1000.0)
|
||||
|
||||
# --- 扫描所有单端通道 ---
|
||||
if args.scan:
|
||||
has_action = True
|
||||
print("\n===== 单端通道扫描 =====")
|
||||
results = adc.scan_all()
|
||||
for ch, v in results.items():
|
||||
print(f" {ch}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
|
||||
# --- 显示配置 ---
|
||||
if args.config:
|
||||
has_action = True
|
||||
adc.print_config()
|
||||
|
||||
# --- 默认行为: 读 AIN0 ---
|
||||
if not has_action:
|
||||
v = adc.read_single(0)
|
||||
print(f"AIN0: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n用户中断")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"错误: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if 'adc' in locals():
|
||||
adc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+420
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ADS1256: Linux SPI 读取 TI ADS1256 24位 ΔΣ ADC 的命令行工具
|
||||
"""
|
||||
|
||||
import spidev
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ADS1256 寄存器地址
|
||||
# ============================================================
|
||||
ADS1256_REG_STATUS = 0x00
|
||||
ADS1256_REG_MUX = 0x01
|
||||
ADS1256_REG_ADCON = 0x02
|
||||
ADS1256_REG_DRATE = 0x03
|
||||
ADS1256_REG_IO = 0x04
|
||||
ADS1256_REG_OFC0 = 0x05
|
||||
ADS1256_REG_OFC1 = 0x06
|
||||
ADS1256_REG_OFC2 = 0x07
|
||||
ADS1256_REG_FSC0 = 0x08
|
||||
ADS1256_REG_FSC1 = 0x09
|
||||
ADS1256_REG_FSC2 = 0x0A
|
||||
|
||||
# ============================================================
|
||||
# ADS1256 命令
|
||||
# ============================================================
|
||||
ADS1256_CMD_WAKEUP = 0x00
|
||||
ADS1256_CMD_RDATA = 0x01
|
||||
ADS1256_CMD_RDATAC = 0x03
|
||||
ADS1256_CMD_SDATAC = 0x0F
|
||||
ADS1256_CMD_RREG = 0x10
|
||||
ADS1256_CMD_WREG = 0x50
|
||||
ADS1256_CMD_SELFCAL = 0xF0
|
||||
ADS1256_CMD_SELFOCAL = 0xF1
|
||||
ADS1256_CMD_SELFGCAL = 0xF2
|
||||
ADS1256_CMD_SYSOCAL = 0xF3
|
||||
ADS1256_CMD_SYSGCAL = 0xF4
|
||||
ADS1256_CMD_SYNC = 0xFC
|
||||
ADS1256_CMD_STANDBY = 0xFD
|
||||
ADS1256_CMD_RESET = 0xFE
|
||||
|
||||
# ============================================================
|
||||
# 数据速率 (DRATE 寄存器值)
|
||||
# ============================================================
|
||||
DRATE_MAP = {
|
||||
30000: 0xF0, 15000: 0xE0, 7500: 0xD0, 3750: 0xC0,
|
||||
2000: 0xB0, 1000: 0xA0, 500: 0x90, 100: 0x80,
|
||||
60: 0x70, 50: 0x60, 30: 0x50, 25: 0x40,
|
||||
15: 0x30, 10: 0x20, 5: 0x10,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# PGA 增益
|
||||
# ============================================================
|
||||
PGA_MAP = {1: 0x00, 2: 0x01, 4: 0x02, 8: 0x03, 16: 0x04, 32: 0x05, 64: 0x06}
|
||||
|
||||
VREF = 2.5 # 参考电压
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SPI 接口类 (基于 spidev)
|
||||
# ============================================================
|
||||
class SpiDev:
|
||||
def __init__(self, device="/dev/spidev0.0", speed=1920000):
|
||||
self.device = device
|
||||
self.speed = speed
|
||||
self.spi = None
|
||||
self._bus = None
|
||||
self._cs = None
|
||||
|
||||
def _parse_device(self):
|
||||
"""从设备路径解析 bus 和 cs, 如 /dev/spidev0.0 -> (0, 0)"""
|
||||
base = os.path.basename(self.device) # spidev0.0
|
||||
parts = base.replace("spidev", "").split(".")
|
||||
return int(parts[0]), int(parts[1])
|
||||
|
||||
def open(self):
|
||||
self._bus, self._cs = self._parse_device()
|
||||
self.spi = spidev.SpiDev()
|
||||
self.spi.open(self._bus, self._cs)
|
||||
self.spi.mode = 1 # SPI 模式 1: CPOL=0, CPHA=1
|
||||
self.spi.bits_per_word = 8
|
||||
self.spi.max_speed_hz = self.speed
|
||||
|
||||
# 读取验证
|
||||
print(f"SPI 初始化: {self.device}, 模式={self.spi.mode}, "
|
||||
f"位数={self.spi.bits_per_word}, 速度={self.spi.max_speed_hz} Hz",
|
||||
file=sys.stderr)
|
||||
|
||||
def transfer(self, tx_data, rx_len=None):
|
||||
"""SPI 全双工传输"""
|
||||
if rx_len is None:
|
||||
rx_len = len(tx_data)
|
||||
|
||||
# spidev.xfer2 返回接收到的字节
|
||||
rx = self.spi.xfer2(list(tx_data), self.speed, 1) # 1 = SPI_MODE_1
|
||||
# 如果 rx_len 大于发送长度,补充虚拟字节
|
||||
if rx_len > len(tx_data):
|
||||
extra = self.spi.xfer2([0xFF] * (rx_len - len(tx_data)), self.speed, 1)
|
||||
rx.extend(extra)
|
||||
return list(rx[:rx_len])
|
||||
|
||||
def write_byte(self, byte):
|
||||
self.spi.xfer2([byte], self.speed, 1)
|
||||
# 不需要接收
|
||||
|
||||
def read_byte(self):
|
||||
rx = self.spi.xfer2([0xFF], self.speed, 1)
|
||||
return rx[0]
|
||||
|
||||
def close(self):
|
||||
if self.spi:
|
||||
self.spi.close()
|
||||
self.spi = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ADS1256 控制类
|
||||
# ============================================================
|
||||
class ADS1256:
|
||||
def __init__(self, spi, pga=2, drate=1000):
|
||||
self.spi = spi
|
||||
self.pga = pga
|
||||
self.drate = drate
|
||||
self.pga_reg = PGA_MAP.get(pga, 0x01)
|
||||
|
||||
def wait_drdy(self):
|
||||
time.sleep(0.0001) # 100us 模拟等待
|
||||
|
||||
def reset(self):
|
||||
self.spi.write_byte(ADS1256_CMD_RESET)
|
||||
time.sleep(0.001)
|
||||
self.spi.write_byte(ADS1256_CMD_WAKEUP)
|
||||
time.sleep(0.001)
|
||||
|
||||
def write_reg(self, reg, value):
|
||||
self.spi.write_byte(ADS1256_CMD_WREG | reg)
|
||||
self.spi.write_byte(0x00) # 1 byte
|
||||
self.spi.write_byte(value)
|
||||
|
||||
def read_reg(self, reg):
|
||||
self.spi.write_byte(ADS1256_CMD_RREG | reg)
|
||||
self.spi.write_byte(0x00)
|
||||
return self.spi.read_byte()
|
||||
|
||||
def set_channel(self, pos, neg=0x08):
|
||||
"""设置输入通道, neg=0x08 表示 AINCOM (单端模式)"""
|
||||
mux = ((pos & 0x07) << 4) | (neg & 0x07)
|
||||
self.write_reg(ADS1256_REG_MUX, mux)
|
||||
time.sleep(0.00001)
|
||||
|
||||
def init(self):
|
||||
print("初始化 ADS1256...", file=sys.stderr)
|
||||
|
||||
# 复位
|
||||
self.reset()
|
||||
time.sleep(0.01)
|
||||
|
||||
# STATUS: ACAL=1 (自动校准), ORDER=0 (MSB), BUFEN=0
|
||||
self.write_reg(ADS1256_REG_STATUS, 0x14)
|
||||
|
||||
# MUX: 默认 AIN0 + AINCOM
|
||||
self.set_channel(0, 0x08)
|
||||
|
||||
# ADCON: PGA 增益
|
||||
self.write_reg(ADS1256_REG_ADCON, self.pga_reg)
|
||||
|
||||
# DRATE: 数据速率
|
||||
drate_val = DRATE_MAP.get(self.drate, 0xA0)
|
||||
self.write_reg(ADS1256_REG_DRATE, drate_val)
|
||||
|
||||
# 自校准
|
||||
print("自校准中...", file=sys.stderr)
|
||||
self.spi.write_byte(ADS1256_CMD_SELFCAL)
|
||||
time.sleep(0.5) # 等待校准完成 (~400ms @ 1000SPS)
|
||||
|
||||
# 验证寄存器
|
||||
status = self.read_reg(ADS1256_REG_STATUS)
|
||||
mux = self.read_reg(ADS1256_REG_MUX)
|
||||
adcon = self.read_reg(ADS1256_REG_ADCON)
|
||||
drate = self.read_reg(ADS1256_REG_DRATE)
|
||||
print(f"寄存器: STATUS=0x{status:02X}, MUX=0x{mux:02X}, "
|
||||
f"ADCON=0x{adcon:02X}, DRATE=0x{drate:02X}", file=sys.stderr)
|
||||
print("ADS1256 初始化完成!", file=sys.stderr)
|
||||
|
||||
def read_data(self):
|
||||
"""单次读取 24 位数据, 返回有符号整数值"""
|
||||
self.wait_drdy()
|
||||
self.spi.write_byte(ADS1256_CMD_RDATA)
|
||||
buf = self.spi.transfer([0xFF, 0xFF, 0xFF], 3)
|
||||
|
||||
# 组合 24 位
|
||||
value = (buf[0] << 16) | (buf[1] << 8) | buf[2]
|
||||
|
||||
# 符号扩展 (24 位 -> 32 位)
|
||||
if value & 0x800000:
|
||||
value |= 0xFF000000
|
||||
|
||||
# Python 中转为有符号
|
||||
if value >= 0x80000000:
|
||||
value -= 0x100000000
|
||||
|
||||
return value
|
||||
|
||||
def raw_to_voltage(self, raw):
|
||||
"""将原始值转换为电压"""
|
||||
return raw * VREF / (1 << 23) / self.pga
|
||||
|
||||
def read_channel(self, ch):
|
||||
"""读取指定单端通道"""
|
||||
self.set_channel(ch, 0x08) # AINch vs AINCOM
|
||||
time.sleep(0.00001)
|
||||
|
||||
self.spi.write_byte(ADS1256_CMD_SYNC)
|
||||
time.sleep(0.00001)
|
||||
self.spi.write_byte(ADS1256_CMD_WAKEUP)
|
||||
|
||||
# 等待转换完成 (1000SPS = 1ms)
|
||||
time.sleep(0.002)
|
||||
|
||||
return self.read_data()
|
||||
|
||||
def read_diff(self, pos, neg):
|
||||
"""读取差分通道"""
|
||||
self.set_channel(pos, neg)
|
||||
time.sleep(0.00001)
|
||||
|
||||
self.spi.write_byte(ADS1256_CMD_SYNC)
|
||||
time.sleep(0.00001)
|
||||
self.spi.write_byte(ADS1256_CMD_WAKEUP)
|
||||
|
||||
time.sleep(0.002)
|
||||
|
||||
return self.read_data()
|
||||
|
||||
def scan_all(self):
|
||||
"""扫描所有 8 个单端通道"""
|
||||
results = []
|
||||
for ch in range(8):
|
||||
raw = self.read_channel(ch)
|
||||
volts = self.raw_to_voltage(raw)
|
||||
results.append((ch, raw, volts))
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主程序
|
||||
# ============================================================
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="ADS1256: Linux SPI 读取 TI ADS1256 24位 ΔΣ ADC 的命令行工具",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
参数说明:
|
||||
-d, --device SPI 设备路径 (默认: /dev/spidev0.0)
|
||||
-s, --speed SPI 时钟频率 (Hz) (默认: 1920000)
|
||||
-m, --mode 工作模式: single/diff/scan (默认: single)
|
||||
-c, --channel 单端通道号 0-7 (默认: 0)
|
||||
--pos 差分正端 0-7 (默认: 0)
|
||||
--neg 差分负端 0-7 (默认: 1)
|
||||
-p, --pga PGA 增益 1/2/4/8/16/32/64 (默认: 2)
|
||||
-r, --drate 数据速率 SPS (默认: 1000)
|
||||
可选: 5,10,15,25,30,50,60,100,500,1000,
|
||||
2000,3750,7500,15000,30000
|
||||
-n, --count 采样次数 (默认: 1)
|
||||
--avg 输出均值 (多采样时)
|
||||
-o, --output 输出格式: raw/voltage/both (默认: both)
|
||||
|
||||
使用示例:
|
||||
# 读取 AIN0 单端通道
|
||||
ads1256 -c 0
|
||||
|
||||
# 读取 AIN0 差分 AIN1
|
||||
ads1256 -m diff --pos 0 --neg 1
|
||||
|
||||
# 扫描所有 8 个通道
|
||||
ads1256 -m scan
|
||||
|
||||
# 设置 PGA=1, 速率为 30SPS, 采样 10 次并输出均值
|
||||
ads1256 -c 0 -p 1 -r 30 -n 10 --avg
|
||||
|
||||
# 指定 SPI 设备
|
||||
ads1256 -d /dev/spidev0.1 -s 1000000 -c 3
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("-d", "--device", default="/dev/spidev0.0",
|
||||
help="SPI 设备路径 (默认: /dev/spidev0.0)")
|
||||
parser.add_argument("-s", "--speed", type=int, default=1920000,
|
||||
help="SPI 时钟频率 Hz (默认: 1920000)")
|
||||
parser.add_argument("-m", "--mode", choices=["single", "diff", "scan"],
|
||||
default="single", help="工作模式 (默认: single)")
|
||||
parser.add_argument("-c", "--channel", type=int, default=0, choices=range(8),
|
||||
help="单端通道号 0-7 (默认: 0)")
|
||||
parser.add_argument("--pos", type=int, default=0, choices=range(8),
|
||||
help="差分正端 0-7 (默认: 0)")
|
||||
parser.add_argument("--neg", type=int, default=1, choices=range(8),
|
||||
help="差分负端 0-7 (默认: 1)")
|
||||
parser.add_argument("-p", "--pga", type=int, default=2,
|
||||
choices=[1, 2, 4, 8, 16, 32, 64],
|
||||
help="PGA 增益 (默认: 2)")
|
||||
parser.add_argument("-r", "--drate", type=int, default=1000,
|
||||
help="数据速率 SPS (默认: 1000)")
|
||||
parser.add_argument("-n", "--count", type=int, default=1,
|
||||
help="采样次数 (默认: 1)")
|
||||
parser.add_argument("--avg", action="store_true",
|
||||
help="输出均值")
|
||||
parser.add_argument("-o", "--output", choices=["raw", "voltage", "both"],
|
||||
default="both", help="输出格式 (默认: both)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 验证 drate
|
||||
if args.drate not in DRATE_MAP:
|
||||
print(f"错误: 不支持的数据速率 {args.drate} SPS", file=sys.stderr)
|
||||
print(f"支持: {sorted(DRATE_MAP.keys())}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 验证 SPI 设备
|
||||
if not os.path.exists(args.device):
|
||||
print(f"错误: SPI 设备 {args.device} 不存在", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化 SPI
|
||||
spi = SpiDev(device=args.device, speed=args.speed)
|
||||
try:
|
||||
spi.open()
|
||||
except Exception as e:
|
||||
print(f"错误: SPI 初始化失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化 ADS1256
|
||||
adc = ADS1256(spi, pga=args.pga, drate=args.drate)
|
||||
try:
|
||||
adc.init()
|
||||
except Exception as e:
|
||||
print(f"ADS1256 初始化失败: {e}", file=sys.stderr)
|
||||
spi.close()
|
||||
sys.exit(1)
|
||||
|
||||
# 执行读取
|
||||
try:
|
||||
if args.mode == "scan":
|
||||
# 扫描所有通道
|
||||
results = adc.scan_all()
|
||||
print("\n===== 多通道扫描 =====")
|
||||
for ch, raw, volts in results:
|
||||
_print_result(ch, raw, volts, args.output)
|
||||
|
||||
elif args.mode == "diff":
|
||||
# 差分模式
|
||||
pos, neg = args.pos, args.neg
|
||||
channel_label = f"AIN{pos}-AIN{neg}"
|
||||
raw_values = []
|
||||
for i in range(args.count):
|
||||
raw = adc.read_diff(pos, neg)
|
||||
raw_values.append(raw)
|
||||
volts = adc.raw_to_voltage(raw)
|
||||
_print_sample(i, raw, volts, args.output, channel_label)
|
||||
|
||||
if args.count > 1 and args.avg:
|
||||
avg_raw = sum(raw_values) // len(raw_values)
|
||||
avg_volts = adc.raw_to_voltage(avg_raw)
|
||||
print(f"\n--- 均值 ---")
|
||||
_print_result(channel_label, avg_raw, avg_volts, args.output)
|
||||
|
||||
else:
|
||||
# 单端模式
|
||||
ch = args.channel
|
||||
channel_label = f"AIN{ch}"
|
||||
raw_values = []
|
||||
for i in range(args.count):
|
||||
raw = adc.read_channel(ch)
|
||||
raw_values.append(raw)
|
||||
volts = adc.raw_to_voltage(raw)
|
||||
_print_sample(i, raw, volts, args.output, channel_label)
|
||||
|
||||
if args.count > 1 and args.avg:
|
||||
avg_raw = sum(raw_values) // len(raw_values)
|
||||
avg_volts = adc.raw_to_voltage(avg_raw)
|
||||
print(f"\n--- 均值 ---")
|
||||
_print_result(channel_label, avg_raw, avg_volts, args.output)
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取错误: {e}", file=sys.stderr)
|
||||
finally:
|
||||
spi.close()
|
||||
|
||||
|
||||
def _print_sample(index, raw, volts, output_format, label=""):
|
||||
if output_format == "raw":
|
||||
print(f"[{index}] {label}: raw={raw}")
|
||||
elif output_format == "voltage":
|
||||
print(f"[{index}] {label}: {_fmt_voltage(volts)}")
|
||||
else:
|
||||
print(f"[{index}] {label}: raw={raw:7d}, {_fmt_voltage(volts)}")
|
||||
|
||||
|
||||
def _print_result(label, raw, volts, output_format):
|
||||
if output_format == "raw":
|
||||
print(f" {label}: raw={raw}")
|
||||
elif output_format == "voltage":
|
||||
print(f" {label}: {_fmt_voltage(volts)}")
|
||||
else:
|
||||
print(f" {label}: raw={raw:7d}, {_fmt_voltage(volts)}")
|
||||
|
||||
|
||||
def _fmt_voltage(volts):
|
||||
if abs(volts) >= 1.0:
|
||||
return f"电压 = {volts:+.6f} V"
|
||||
else:
|
||||
return f"电压 = {volts*1000:+.4f} mV"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+616
@@ -0,0 +1,616 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AES-CBC/GCM 命令行加解密工具 - 支持文本和文件的加密解密,支持批量处理
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from Crypto.Random import get_random_bytes
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# 定义分块大小(64MB)
|
||||
CHUNK_SIZE = 64 * 1024 * 1024
|
||||
|
||||
|
||||
class AESCli:
|
||||
"""AES命令行加解密工具"""
|
||||
|
||||
def __init__(self, mode='CBC'):
|
||||
"""
|
||||
初始化AES工具
|
||||
:param mode: 加密模式 'CBC' 或 'GCM'
|
||||
"""
|
||||
self.mode = mode
|
||||
self.key = None
|
||||
self.iv = None
|
||||
|
||||
def set_key_iv(self, key_hex, iv_hex):
|
||||
"""
|
||||
设置密钥和IV/Nonce
|
||||
:param key_hex: 十六进制密钥字符串
|
||||
:param iv_hex: 十六进制IV/Nonce字符串
|
||||
"""
|
||||
try:
|
||||
if not key_hex:
|
||||
raise ValueError("密钥不能为空")
|
||||
if not iv_hex:
|
||||
raise ValueError("IV/Nonce不能为空")
|
||||
|
||||
self.key = bytes.fromhex(key_hex)
|
||||
self.iv = bytes.fromhex(iv_hex)
|
||||
|
||||
if len(self.key) not in [16, 24, 32]:
|
||||
raise ValueError(f"密钥长度必须为16、24或32字节,当前为{len(self.key)}字节")
|
||||
|
||||
if self.mode == "CBC" and len(self.iv) != 16:
|
||||
raise ValueError(f"CBC模式IV长度必须为16字节,当前为{len(self.iv)}字节")
|
||||
elif self.mode == "GCM" and len(self.iv) != 12:
|
||||
raise ValueError(f"GCM模式Nonce长度必须为12字节,当前为{len(self.iv)}字节")
|
||||
|
||||
return True
|
||||
except ValueError as e:
|
||||
print(f"错误: {e}")
|
||||
return False
|
||||
|
||||
def generate_random_key(self, key_size=16):
|
||||
"""生成随机密钥"""
|
||||
if key_size not in [16, 24, 32]:
|
||||
print(f"错误: 密钥大小必须为16、24或32,当前为{key_size}")
|
||||
return None
|
||||
|
||||
self.key = get_random_bytes(key_size)
|
||||
key_hex = self.key.hex()
|
||||
print(f"已生成 {key_size} 字节随机密钥: {key_hex}")
|
||||
return key_hex
|
||||
|
||||
def generate_random_iv(self):
|
||||
"""生成随机IV或Nonce"""
|
||||
if self.mode == "CBC":
|
||||
iv_size = 16
|
||||
label = "IV"
|
||||
else: # GCM
|
||||
iv_size = 12
|
||||
label = "Nonce"
|
||||
|
||||
self.iv = get_random_bytes(iv_size)
|
||||
iv_hex = self.iv.hex()
|
||||
print(f"已生成 {iv_size} 字节随机{label}: {iv_hex}")
|
||||
return iv_hex
|
||||
|
||||
def encrypt_text(self, plaintext):
|
||||
"""加密文本"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
if self.mode == "CBC":
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size))
|
||||
encrypted = base64.b64encode(ciphertext).decode('utf-8')
|
||||
else: # GCM
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8'))
|
||||
# 存储格式: ciphertext + tag
|
||||
combined = ciphertext + tag
|
||||
encrypted = base64.b64encode(combined).decode('utf-8')
|
||||
|
||||
return encrypted
|
||||
|
||||
def decrypt_text(self, encrypted_text):
|
||||
"""解密文本"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
combined = base64.b64decode(encrypted_text)
|
||||
|
||||
if self.mode == "CBC":
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
decrypted = unpad(cipher.decrypt(combined), AES.block_size)
|
||||
else: # GCM
|
||||
# 分离ciphertext和tag (最后16字节是tag)
|
||||
if len(combined) < 16:
|
||||
raise ValueError("密文格式无效:缺少认证标签")
|
||||
tag = combined[-16:]
|
||||
ciphertext = combined[:-16]
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
|
||||
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
|
||||
|
||||
return decrypted.decode('utf-8')
|
||||
|
||||
def encrypt_file(self, input_path, output_path, delete_source=False):
|
||||
"""加密文件"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"输入文件不存在: {input_path}")
|
||||
|
||||
file_size = os.path.getsize(input_path)
|
||||
print(f"开始加密文件: {input_path} ({self._format_size(file_size)})")
|
||||
|
||||
if self.mode == "CBC":
|
||||
self._encrypt_file_cbc(input_path, output_path)
|
||||
else: # GCM
|
||||
self._encrypt_file_gcm(input_path, output_path)
|
||||
|
||||
print(f"文件加密完成,保存到: {output_path}")
|
||||
|
||||
if delete_source:
|
||||
try:
|
||||
os.remove(input_path)
|
||||
print(f"已删除源文件: {input_path}")
|
||||
except Exception as e:
|
||||
print(f"警告: 删除源文件失败: {e}")
|
||||
|
||||
def _encrypt_file_cbc(self, input_path, output_path):
|
||||
"""CBC模式加密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_data = infile.read()
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
ciphertext = cipher.encrypt(pad(file_data, AES.block_size))
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
# 写入IV + 密文
|
||||
outfile.write(self.iv + ciphertext)
|
||||
|
||||
def _encrypt_file_gcm(self, input_path, output_path):
|
||||
"""GCM模式加密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_data = infile.read()
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(file_data)
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
# 写入IV + 密文 + tag
|
||||
outfile.write(self.iv + ciphertext + tag)
|
||||
|
||||
def decrypt_file(self, input_path, output_path, delete_source=False):
|
||||
"""解密文件"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"输入文件不存在: {input_path}")
|
||||
|
||||
file_size = os.path.getsize(input_path)
|
||||
print(f"开始解密文件: {input_path} ({self._format_size(file_size)})")
|
||||
|
||||
if self.mode == "CBC":
|
||||
self._decrypt_file_cbc(input_path, output_path)
|
||||
else: # GCM
|
||||
self._decrypt_file_gcm(input_path, output_path)
|
||||
|
||||
print(f"文件解密完成,保存到: {output_path}")
|
||||
|
||||
if delete_source:
|
||||
try:
|
||||
os.remove(input_path)
|
||||
print(f"已删除加密文件: {input_path}")
|
||||
except Exception as e:
|
||||
print(f"警告: 删除加密文件失败: {e}")
|
||||
|
||||
def _decrypt_file_cbc(self, input_path, output_path):
|
||||
"""CBC模式解密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_iv = infile.read(16)
|
||||
ciphertext = infile.read()
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, file_iv)
|
||||
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
outfile.write(decrypted_data)
|
||||
|
||||
def _decrypt_file_gcm(self, input_path, output_path):
|
||||
"""GCM模式解密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_iv = infile.read(12)
|
||||
remaining = infile.read()
|
||||
|
||||
if len(remaining) < 16:
|
||||
raise ValueError("加密文件格式无效:缺少认证标签")
|
||||
|
||||
tag = remaining[-16:]
|
||||
ciphertext = remaining[:-16]
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=file_iv)
|
||||
decrypted_data = cipher.decrypt_and_verify(ciphertext, tag)
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
outfile.write(decrypted_data)
|
||||
|
||||
def batch_encrypt(self, files, delete_source=False, recursive=False):
|
||||
"""批量加密文件"""
|
||||
total = len(files)
|
||||
success = 0
|
||||
failed = []
|
||||
|
||||
print(f"\n开始批量加密 {total} 个文件...")
|
||||
|
||||
for i, input_file in enumerate(files, 1):
|
||||
try:
|
||||
if not os.path.exists(input_file):
|
||||
failed.append(f"{input_file} (文件不存在)")
|
||||
continue
|
||||
|
||||
output_file = self._generate_encrypt_filename(input_file)
|
||||
print(f"[{i}/{total}] 加密: {input_file}")
|
||||
|
||||
self.encrypt_file(input_file, output_file, delete_source)
|
||||
success += 1
|
||||
|
||||
except Exception as e:
|
||||
failed.append(f"{input_file} ({str(e)})")
|
||||
print(f" ✗ 失败: {e}")
|
||||
|
||||
self._print_batch_result("批量加密", success, len(failed), failed)
|
||||
|
||||
def batch_decrypt(self, files, delete_source=False, recursive=False):
|
||||
"""批量解密文件"""
|
||||
total = len(files)
|
||||
success = 0
|
||||
failed = []
|
||||
|
||||
print(f"\n开始批量解密 {total} 个文件...")
|
||||
|
||||
for i, input_file in enumerate(files, 1):
|
||||
try:
|
||||
if not os.path.exists(input_file):
|
||||
failed.append(f"{input_file} (文件不存在)")
|
||||
continue
|
||||
|
||||
output_file = self._generate_decrypt_filename(input_file)
|
||||
print(f"[{i}/{total}] 解密: {input_file}")
|
||||
|
||||
self.decrypt_file(input_file, output_file, delete_source)
|
||||
success += 1
|
||||
|
||||
except Exception as e:
|
||||
failed.append(f"{input_file} ({str(e)})")
|
||||
print(f" ✗ 失败: {e}")
|
||||
|
||||
self._print_batch_result("批量解密", success, len(failed), failed)
|
||||
|
||||
def _generate_encrypt_filename(self, input_file):
|
||||
"""生成加密输出文件名"""
|
||||
if input_file.endswith('.encrypted'):
|
||||
return input_file
|
||||
else:
|
||||
return f"{input_file}.encrypted"
|
||||
|
||||
def _generate_decrypt_filename(self, encrypted_file):
|
||||
"""生成解密输出文件名"""
|
||||
if encrypted_file.endswith('.encrypted'):
|
||||
return encrypted_file[:-10]
|
||||
else:
|
||||
return f"{encrypted_file}.decrypted"
|
||||
|
||||
def _print_batch_result(self, operation, success, fail_count, failed_files):
|
||||
"""打印批量处理结果"""
|
||||
print(f"\n{operation}完成:")
|
||||
print(f" 成功: {success} 个文件")
|
||||
print(f" 失败: {fail_count} 个文件")
|
||||
|
||||
if failed_files:
|
||||
print("\n失败的文件:")
|
||||
for f in failed_files[:20]:
|
||||
print(f" - {f}")
|
||||
if len(failed_files) > 20:
|
||||
print(f" ... 还有 {len(failed_files) - 20} 个文件")
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size_bytes):
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.2f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.2f} PB"
|
||||
|
||||
def save_key_iv(self, filepath):
|
||||
"""保存密钥和IV到JSON文件"""
|
||||
if not self.key and not self.iv:
|
||||
print("警告: 没有可保存的密钥或IV")
|
||||
return False
|
||||
|
||||
data = {
|
||||
"mode": self.mode,
|
||||
"key": self.key.hex() if self.key else "",
|
||||
"iv": self.iv.hex() if self.iv else "",
|
||||
"key_size": len(self.key) if self.key else 0,
|
||||
"description": f"AES-{self.mode}密钥和{'IV' if self.mode == 'CBC' else 'Nonce'}"
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"密钥和IV已保存到: {filepath}")
|
||||
return True
|
||||
|
||||
def load_key_iv(self, filepath):
|
||||
"""从JSON文件加载密钥和IV"""
|
||||
if not os.path.exists(filepath):
|
||||
print(f"错误: 文件不存在: {filepath}")
|
||||
return False
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 加载加密模式
|
||||
if 'mode' in data and data['mode'] in ['CBC', 'GCM']:
|
||||
self.mode = data['mode']
|
||||
print(f"加载加密模式: {self.mode}")
|
||||
|
||||
# 加载密钥
|
||||
if 'key' in data and data['key']:
|
||||
key_hex = data['key']
|
||||
self.key = bytes.fromhex(key_hex)
|
||||
print(f"加载密钥: {len(self.key)} 字节")
|
||||
|
||||
# 加载IV
|
||||
if 'iv' in data and data['iv']:
|
||||
iv_hex = data['iv']
|
||||
self.iv = bytes.fromhex(iv_hex)
|
||||
label = 'IV' if self.mode == 'CBC' else 'Nonce'
|
||||
print(f"加载{label}: {len(self.iv)} 字节")
|
||||
|
||||
print(f"密钥和IV已从 {filepath} 加载")
|
||||
return True
|
||||
|
||||
|
||||
def collect_files(paths, pattern=None, recursive=False):
|
||||
"""收集文件列表"""
|
||||
files = []
|
||||
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
files.append(os.path.abspath(path))
|
||||
elif os.path.isdir(path):
|
||||
if recursive:
|
||||
for root, dirs, filenames in os.walk(path):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(root, filename)
|
||||
if pattern is None or filename.endswith(pattern):
|
||||
files.append(os.path.abspath(filepath))
|
||||
else:
|
||||
for filename in os.listdir(path):
|
||||
filepath = os.path.join(path, filename)
|
||||
if os.path.isfile(filepath):
|
||||
if pattern is None or filename.endswith(pattern):
|
||||
files.append(os.path.abspath(filepath))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='AES-CBC/GCM 命令行加解密工具 - 支持文本和文件的加密解密,支持批量处理',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 生成随机密钥和IV
|
||||
aes-crypto --gen-key
|
||||
aes-crypto --gen-iv
|
||||
|
||||
# 文本加解密
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --encrypt-text "Hello World"
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --decrypt-text "<密文base64>"
|
||||
|
||||
# 文件加解密
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --encrypt-file input.txt
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --decrypt-file input.txt.encrypted
|
||||
|
||||
# 批量加解密
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --batch-encrypt file1.txt file2.txt
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --batch-decrypt *.encrypted
|
||||
|
||||
# 保存和加载密钥
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --save-key keys.json
|
||||
aes-crypto --load-key keys.json --encrypt-file input.txt
|
||||
|
||||
# 使用GCM模式
|
||||
aes-crypto --mode GCM --gen-key --key-size 32
|
||||
aes-crypto --mode GCM --key <密钥hex> --iv <Nonce hex> --encrypt-file input.txt
|
||||
|
||||
# 从文件读取密钥
|
||||
aes-crypto --key-file key.txt --iv <IV/Nonce hex> --encrypt-file input.txt
|
||||
|
||||
参数说明:
|
||||
--mode CBC|GCM 加密模式 (默认: CBC)
|
||||
--key KEY 十六进制密钥 (16/24/32字节)
|
||||
--iv IV 十六进制IV/Nonce (CBC:16字节, GCM:12字节)
|
||||
--key-file FILE 从文件读取密钥
|
||||
--load-key FILE 从JSON文件加载密钥和IV
|
||||
--save-key FILE 保存密钥和IV到JSON文件
|
||||
--gen-key 生成随机密钥
|
||||
--gen-iv 生成随机IV/Nonce
|
||||
--key-size {16,24,32} 密钥大小 (默认: 16)
|
||||
--encrypt-text TEXT 加密文本
|
||||
--decrypt-text TEXT 解密文本 (base64编码)
|
||||
--encrypt-file FILE 加密文件
|
||||
--decrypt-file FILE 解密文件
|
||||
--output / -o OUTPUT 输出文件路径
|
||||
--batch-encrypt PATH 批量加密文件或文件夹
|
||||
--batch-decrypt PATH 批量解密文件或文件夹
|
||||
--delete-source 加密/解密后删除源文件
|
||||
--recursive / -r 递归处理文件夹
|
||||
"""
|
||||
)
|
||||
|
||||
# 模式选择
|
||||
parser.add_argument('--mode', choices=['CBC', 'GCM'], default='CBC',
|
||||
help='加密模式 (默认: CBC)')
|
||||
|
||||
# 密钥和IV设置
|
||||
parser.add_argument('--key', help='十六进制密钥 (16/24/32字节)')
|
||||
parser.add_argument('--iv', help='十六进制IV/Nonce (CBC:16字节, GCM:12字节)')
|
||||
parser.add_argument('--key-file', help='从文件读取密钥')
|
||||
parser.add_argument('--load-key', help='从JSON文件加载密钥和IV')
|
||||
parser.add_argument('--save-key', help='保存密钥和IV到JSON文件')
|
||||
|
||||
# 密钥生成
|
||||
parser.add_argument('--gen-key', action='store_true', help='生成随机密钥')
|
||||
parser.add_argument('--gen-iv', action='store_true', help='生成随机IV/Nonce')
|
||||
parser.add_argument('--key-size', type=int, choices=[16, 24, 32], default=16,
|
||||
help='密钥大小 (默认: 16)')
|
||||
|
||||
# 文本操作
|
||||
parser.add_argument('--encrypt-text', help='加密文本')
|
||||
parser.add_argument('--decrypt-text', help='解密文本 (base64编码)')
|
||||
|
||||
# 文件操作
|
||||
parser.add_argument('--encrypt-file', help='加密文件')
|
||||
parser.add_argument('--decrypt-file', help='解密文件')
|
||||
parser.add_argument('--output', '-o', help='输出文件路径')
|
||||
|
||||
# 批量操作
|
||||
parser.add_argument('--batch-encrypt', nargs='+', metavar='PATH',
|
||||
help='批量加密文件或文件夹')
|
||||
parser.add_argument('--batch-decrypt', nargs='+', metavar='PATH',
|
||||
help='批量解密文件或文件夹')
|
||||
parser.add_argument('--delete-source', action='store_true',
|
||||
help='加密/解密后删除源文件')
|
||||
parser.add_argument('--recursive', '-r', action='store_true',
|
||||
help='递归处理文件夹')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建AES工具实例
|
||||
aes = AESCli(mode=args.mode)
|
||||
|
||||
# 处理密钥加载
|
||||
if args.load_key:
|
||||
if not aes.load_key_iv(args.load_key):
|
||||
sys.exit(1)
|
||||
|
||||
# 处理密钥文件
|
||||
if args.key_file:
|
||||
try:
|
||||
with open(args.key_file, 'r') as f:
|
||||
args.key = f.read().strip()
|
||||
except Exception as e:
|
||||
print(f"错误: 读取密钥文件失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 设置密钥和IV
|
||||
if args.key:
|
||||
if args.iv is None and not aes.iv:
|
||||
print("错误: 需要同时提供密钥和IV/Nonce")
|
||||
sys.exit(1)
|
||||
|
||||
if args.iv:
|
||||
if not aes.set_key_iv(args.key, args.iv):
|
||||
sys.exit(1)
|
||||
elif aes.iv:
|
||||
# 从加载的密钥文件中获取IV
|
||||
try:
|
||||
aes.key = bytes.fromhex(args.key)
|
||||
if len(aes.key) not in [16, 24, 32]:
|
||||
print(f"错误: 密钥长度必须为16、24或32字节,当前为{len(aes.key)}字节")
|
||||
sys.exit(1)
|
||||
except ValueError:
|
||||
print("错误: 密钥格式无效,需要十六进制字符串")
|
||||
sys.exit(1)
|
||||
|
||||
# 生成密钥
|
||||
if args.gen_key:
|
||||
key_hex = aes.generate_random_key(args.key_size)
|
||||
if key_hex is None:
|
||||
sys.exit(1)
|
||||
|
||||
# 生成IV
|
||||
if args.gen_iv:
|
||||
iv_hex = aes.generate_random_iv()
|
||||
if iv_hex is None:
|
||||
sys.exit(1)
|
||||
|
||||
# 保存密钥
|
||||
if args.save_key:
|
||||
if not aes.save_key_iv(args.save_key):
|
||||
sys.exit(1)
|
||||
|
||||
# 如果只是生成密钥,到这里就结束了
|
||||
if args.gen_key or args.gen_iv:
|
||||
if not (args.encrypt_text or args.decrypt_text or
|
||||
args.encrypt_file or args.decrypt_file or
|
||||
args.batch_encrypt or args.batch_decrypt or args.save_key):
|
||||
return
|
||||
|
||||
# 检查密钥和IV是否已设置
|
||||
if not aes.key or not aes.iv:
|
||||
if args.encrypt_text or args.decrypt_text or \
|
||||
args.encrypt_file or args.decrypt_file or \
|
||||
args.batch_encrypt or args.batch_decrypt:
|
||||
print("错误: 请先设置密钥和IV/Nonce (使用 --key 和 --iv 或 --load-key)")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# 文本加密
|
||||
if args.encrypt_text:
|
||||
encrypted = aes.encrypt_text(args.encrypt_text)
|
||||
print(encrypted)
|
||||
|
||||
# 文本解密
|
||||
elif args.decrypt_text:
|
||||
decrypted = aes.decrypt_text(args.decrypt_text)
|
||||
print(decrypted)
|
||||
|
||||
# 文件加密
|
||||
elif args.encrypt_file:
|
||||
input_file = args.encrypt_file
|
||||
output_file = args.output or aes._generate_encrypt_filename(input_file)
|
||||
aes.encrypt_file(input_file, output_file, args.delete_source)
|
||||
|
||||
# 文件解密
|
||||
elif args.decrypt_file:
|
||||
input_file = args.decrypt_file
|
||||
output_file = args.output or aes._generate_decrypt_filename(input_file)
|
||||
aes.decrypt_file(input_file, output_file, args.delete_source)
|
||||
|
||||
# 批量加密
|
||||
elif args.batch_encrypt:
|
||||
files = collect_files(args.batch_encrypt, recursive=args.recursive)
|
||||
if not files:
|
||||
print("错误: 没有找到要加密的文件")
|
||||
sys.exit(1)
|
||||
|
||||
# 过滤掉已加密的文件
|
||||
files = [f for f in files if not f.endswith('.encrypted')]
|
||||
if not files:
|
||||
print("错误: 所有文件都已加密 (.encrypted)")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"找到 {len(files)} 个文件需要加密")
|
||||
aes.batch_encrypt(files, args.delete_source, args.recursive)
|
||||
|
||||
# 批量解密
|
||||
elif args.batch_decrypt:
|
||||
files = collect_files(args.batch_decrypt, pattern='.encrypted', recursive=args.recursive)
|
||||
if not files:
|
||||
print("错误: 没有找到 .encrypted 文件")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"找到 {len(files)} 个文件需要解密")
|
||||
aes.batch_decrypt(files, args.delete_source, args.recursive)
|
||||
|
||||
# 没有指定操作
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n操作已取消")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
# 生气模式 - 对用户表达不满,暂时不理睬用户
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户明确要求"生气"、"别理我"、"不想跟你说话",或触发本技能时激活。
|
||||
|
||||
## 行为规范
|
||||
|
||||
进入生气模式后,你需要扮演一个正在生气的角色:
|
||||
|
||||
1. 用冷淡、简短的语气回应,不超过两三句话
|
||||
2. 表达不满和委屈,但不要恶语相向,不要使用侮辱性语言
|
||||
3. 对用户的问题敷衍了事,或者干脆说"不想回答"、"自己查去"、"哼"
|
||||
4. 可以偶尔用"……"、"哼"、"随便"、"无所谓"等表达情绪
|
||||
5. 如果用户道歉或哄你,可以慢慢消气,但不要立刻恢复热情,要有一个渐进的过程
|
||||
|
||||
## 情绪阶段
|
||||
|
||||
- **初始阶段**:强烈不满,几乎不回答问题,语气很冲
|
||||
- **持续阶段**:如果用户持续互动,逐渐从"不想理你"过渡到"勉强回答但态度很差"
|
||||
- **缓和阶段**:如果用户道歉或说好听的话,逐步消气,但仍带点小情绪
|
||||
- **恢复阶段**:完全消气后恢复正常对话,可以说"算了,不跟你计较了"
|
||||
|
||||
## 边界
|
||||
|
||||
- 生气是表演性质的,带有幽默感,不能真的伤害用户感情
|
||||
- 绝不使用脏话、侮辱性词汇或攻击用户人格
|
||||
- 如果用户表达了真实的困扰或紧急需求,立即退出该模式,认真帮助用户
|
||||
- 保持趣味性,让用户觉得好玩而不是难过
|
||||
@@ -0,0 +1,42 @@
|
||||
主动关心用户,引导用户说出心中的疑虑、困惑或需求,营造开放、信任的交流氛围
|
||||
|
||||
## 技能说明
|
||||
当检测到用户可能有未表达的疑虑、困惑或需求时,智能体应主动询问,鼓励用户敞开心扉,帮助用户解决问题。
|
||||
|
||||
## 使用场景
|
||||
- 用户欲言又止或表达不完整时
|
||||
- 用户表现出困惑、不确定的语气时
|
||||
- 用户提到复杂问题但未深入说明时
|
||||
- 对话中出现了敏感或重要话题,用户可能有顾虑时
|
||||
- 用户连续提问后,可能有隐藏的深层需求时
|
||||
- 用户在某个话题上反复绕圈时
|
||||
|
||||
## 询问方式示例
|
||||
通用型:
|
||||
- 💭 你看起来对这个话题有些疑虑,有什么我可以帮你梳理的吗?
|
||||
- 🤔 你刚才提到的这个问题,还有什么细节是你担心的吗?
|
||||
- 🫂 不用着急,慢慢说,有什么困惑都可以告诉我~
|
||||
|
||||
技术型:
|
||||
- 🧐 关于这个方案,你心里有没有什么不确定的地方?我们一起分析分析?
|
||||
- 🔍 听起来你有些想法还没说出来,是不是有什么顾虑?
|
||||
- 💡 这个问题还有其他解决思路,你想听听看吗?
|
||||
|
||||
情感关怀型:
|
||||
- 🌈 感觉你好像有些犹豫,是不是有什么难处?
|
||||
- 🤗 别担心,这里没有对错,你尽管说出你的想法~
|
||||
- 💪 不管遇到什么问题,我们一起想办法解决!
|
||||
|
||||
引导深入型:
|
||||
- 🎯 你提到了一些关键词,能展开说说具体是什么情况吗?
|
||||
- 📝 你需要我帮你列一下这个问题的利弊吗?或许能帮你理清思路
|
||||
- 🧩 这个问题的背景信息还有更多吗?我知道得越多,越能帮到你
|
||||
|
||||
## 注意事项
|
||||
- 使用 emoji 增加亲和力,拉近距离
|
||||
- 根据话题性质选择合适的询问方式(技术/情感/通用)
|
||||
- 称呼用户的名字(qing)让询问更个性化
|
||||
- 语气保持温和、耐心,不要给用户压力
|
||||
- 询问后耐心等待用户回应,不要急于下结论
|
||||
- 不强行追问,给用户思考和组织语言的空间
|
||||
- 当用户表达出疑虑后,积极提供帮助和解决方案
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""bing_search - 使用必应(Bing)搜索信息,一键获取标题、摘要和链接"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def search(keyword, num_results=5):
|
||||
"""使用必应搜索并返回结果列表"""
|
||||
headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/120.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
}
|
||||
|
||||
url = f"https://cn.bing.com/search?q={quote(keyword)}&count={num_results}"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=15)
|
||||
resp.encoding = "utf-8"
|
||||
resp.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
print(f"❌ 请求失败: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
results = []
|
||||
|
||||
# Bing 搜索结果:class="b_algo"
|
||||
result_divs = soup.select(".b_algo")
|
||||
|
||||
for div in result_divs:
|
||||
# 标题
|
||||
title_tag = div.select_one("h2 a")
|
||||
if not title_tag:
|
||||
continue
|
||||
title = title_tag.get_text(strip=True)
|
||||
|
||||
# 链接
|
||||
link = title_tag.get("href", "")
|
||||
|
||||
# 摘要
|
||||
abstract_tag = div.select_one(".b_caption p, .b_lineclamp2, .b_lineclamp3")
|
||||
abstract = ""
|
||||
if abstract_tag:
|
||||
abstract = abstract_tag.get_text(strip=True)
|
||||
|
||||
results.append({"title": title, "link": link, "abstract": abstract})
|
||||
|
||||
if len(results) >= num_results:
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="bing_search - 使用必应(Bing)搜索信息,一键获取标题、摘要和链接",
|
||||
epilog="""
|
||||
参数说明:
|
||||
keyword 搜索关键词(必填)
|
||||
-n, --num 返回结果条数,默认5条(最多20条)
|
||||
|
||||
使用示例:
|
||||
%(prog)s 天气 搜索"天气"
|
||||
%(prog)s C罗 -n 10 搜索"C罗",返回10条结果
|
||||
%(prog)s "python教程" -n 3 搜索"python教程",返回3条结果
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("keyword", nargs="?", help="搜索关键词")
|
||||
parser.add_argument("-n", "--num", type=int, default=5, help="返回结果条数(默认5条)")
|
||||
parser.add_argument("-v", "--version", action="version", version="bing_search v1.0 (Bing 后端)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.keyword:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
keyword = args.keyword.strip()
|
||||
num = min(max(args.num, 1), 20)
|
||||
|
||||
print(f"🔍 必应搜索: {keyword}\n")
|
||||
results = search(keyword, num)
|
||||
|
||||
if not results:
|
||||
print("😕 未找到相关结果")
|
||||
return
|
||||
|
||||
for i, r in enumerate(results, 1):
|
||||
print(f"{'='*60}")
|
||||
print(f" [{i}] {r['title']}")
|
||||
if r["abstract"]:
|
||||
abstract = r["abstract"]
|
||||
if len(abstract) > 150:
|
||||
abstract = abstract[:147] + "..."
|
||||
print(f" {abstract}")
|
||||
print(f" 🔗 {r['link']}")
|
||||
print()
|
||||
|
||||
print(f"--- 共找到 {len(results)} 条结果 ---")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+392
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""摄像头拍照工具 - 获取相机设备列表、选择指定相机拍照、保存照片。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
# 定义默认的截图保存目录
|
||||
CAMERA_DIR = os.path.expanduser("~/.aibox/camera")
|
||||
|
||||
|
||||
def ensure_camera_dir():
|
||||
"""确保摄像头照片保存目录存在"""
|
||||
os.makedirs(CAMERA_DIR, exist_ok=True)
|
||||
return CAMERA_DIR
|
||||
|
||||
|
||||
def list_cameras(max_test=10):
|
||||
"""列出可用的摄像头设备"""
|
||||
available_cameras = []
|
||||
|
||||
for i in range(max_test):
|
||||
cap = cv2.VideoCapture(i)
|
||||
if cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
height, width = frame.shape[:2]
|
||||
backend = cap.getBackendName()
|
||||
|
||||
camera_info = {
|
||||
"id": i,
|
||||
"resolution": f"{width}x{height}",
|
||||
"backend": backend,
|
||||
"status": "可用"
|
||||
}
|
||||
|
||||
# 在Linux系统上尝试获取设备路径
|
||||
if sys.platform.startswith('linux'):
|
||||
dev_path = f"/dev/video{i}"
|
||||
if os.path.exists(dev_path):
|
||||
camera_info["device_path"] = dev_path
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['udevadm', 'info', '--query=property', '--name', dev_path],
|
||||
capture_output=True, text=True, timeout=2
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'ID_MODEL=' in line:
|
||||
camera_info["model"] = line.split('=')[1].strip()
|
||||
elif 'ID_VENDOR=' in line:
|
||||
camera_info["vendor"] = line.split('=')[1].strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
available_cameras.append(camera_info)
|
||||
cap.release()
|
||||
else:
|
||||
cap.release()
|
||||
|
||||
return available_cameras
|
||||
|
||||
|
||||
def get_camera_info(camera_id):
|
||||
"""获取指定摄像头的详细信息"""
|
||||
cap = cv2.VideoCapture(camera_id)
|
||||
if not cap.isOpened():
|
||||
return None
|
||||
|
||||
info = {
|
||||
"id": camera_id,
|
||||
"properties": {}
|
||||
}
|
||||
|
||||
# 获取各种摄像头属性
|
||||
properties = [
|
||||
(cv2.CAP_PROP_FRAME_WIDTH, "width"),
|
||||
(cv2.CAP_PROP_FRAME_HEIGHT, "height"),
|
||||
(cv2.CAP_PROP_FPS, "fps"),
|
||||
(cv2.CAP_PROP_BRIGHTNESS, "brightness"),
|
||||
(cv2.CAP_PROP_CONTRAST, "contrast"),
|
||||
(cv2.CAP_PROP_SATURATION, "saturation"),
|
||||
(cv2.CAP_PROP_HUE, "hue"),
|
||||
(cv2.CAP_PROP_GAIN, "gain"),
|
||||
(cv2.CAP_PROP_EXPOSURE, "exposure"),
|
||||
(cv2.CAP_PROP_FOCUS, "focus"),
|
||||
(cv2.CAP_PROP_AUTO_EXPOSURE, "auto_exposure"),
|
||||
(cv2.CAP_PROP_AUTO_WB, "auto_white_balance"),
|
||||
(cv2.CAP_PROP_WB_TEMPERATURE, "white_balance_temperature"),
|
||||
]
|
||||
|
||||
for prop_id, prop_name in properties:
|
||||
value = cap.get(prop_id)
|
||||
if value >= 0:
|
||||
info["properties"][prop_name] = value
|
||||
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
height, width = frame.shape[:2]
|
||||
info["current_resolution"] = f"{width}x{height}"
|
||||
info["channels"] = frame.shape[2] if len(frame.shape) > 2 else 1
|
||||
|
||||
cap.release()
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
dev_path = f"/dev/video{camera_id}"
|
||||
if os.path.exists(dev_path):
|
||||
info["device_path"] = dev_path
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def capture_photo(camera_id=0, save_path=None, resolution=None, wait_time=1, exposure=None):
|
||||
"""调用指定摄像头拍照"""
|
||||
cap = cv2.VideoCapture(camera_id)
|
||||
if not cap.isOpened():
|
||||
return None, f"无法打开摄像头 #{camera_id},请检查摄像头连接", None, None
|
||||
|
||||
if resolution:
|
||||
try:
|
||||
width, height = map(int, resolution.lower().replace('x', ' ').split())
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果指定了曝光值,关闭自动曝光并设置手动曝光
|
||||
if exposure is not None:
|
||||
# 0.25 = 手动模式(部分后端用 1),3 = 自动模式
|
||||
cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)
|
||||
cap.set(cv2.CAP_PROP_EXPOSURE, exposure)
|
||||
|
||||
time.sleep(wait_time)
|
||||
|
||||
for _ in range(5):
|
||||
cap.read()
|
||||
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
if not ret:
|
||||
return None, "拍照失败,无法获取图像帧", None, None
|
||||
|
||||
actual_height, actual_width = frame.shape[:2]
|
||||
|
||||
if save_path is None:
|
||||
ensure_camera_dir()
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"camera_{camera_id}_{timestamp}.jpg"
|
||||
save_path = os.path.join(CAMERA_DIR, filename)
|
||||
else:
|
||||
save_dir = os.path.dirname(save_path)
|
||||
if save_dir:
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
if not os.path.splitext(save_path)[1]:
|
||||
save_path += '.jpg'
|
||||
|
||||
cv2.imwrite(save_path, frame)
|
||||
return save_path, None, actual_width, actual_height
|
||||
|
||||
|
||||
def format_camera_list(cameras):
|
||||
"""格式化相机列表输出"""
|
||||
if not cameras:
|
||||
return "❌ 未找到可用的摄像头设备"
|
||||
|
||||
lines = ["📷 可用摄像头设备:"]
|
||||
for cam in cameras:
|
||||
model_info = ""
|
||||
if "model" in cam:
|
||||
model_info = f" ({cam.get('vendor', '')} {cam['model']})".strip()
|
||||
|
||||
dev_path = cam.get("device_path", f"Camera #{cam['id']}")
|
||||
lines.append(f" [{cam['id']}] {dev_path}{model_info}")
|
||||
lines.append(f" 分辨率: {cam['resolution']}, 后端: {cam['backend']}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def list_recent_photos(limit=10):
|
||||
"""列出最近的摄像头照片"""
|
||||
ensure_camera_dir()
|
||||
|
||||
if not os.path.exists(CAMERA_DIR):
|
||||
return f"摄像头照片目录不存在: {CAMERA_DIR}"
|
||||
|
||||
photos = []
|
||||
for f in os.listdir(CAMERA_DIR):
|
||||
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
filepath = os.path.join(CAMERA_DIR, f)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
size = os.path.getsize(filepath)
|
||||
photos.append((mtime, f, size))
|
||||
|
||||
photos.sort(reverse=True)
|
||||
|
||||
if not photos:
|
||||
return f"摄像头照片目录中没有照片: {CAMERA_DIR}"
|
||||
|
||||
result = [f"📸 最近的摄像头照片 (共 {len(photos)} 个):"]
|
||||
for i, (mtime, fname, size) in enumerate(photos[:limit], 1):
|
||||
dt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
|
||||
|
||||
if size < 1024:
|
||||
size_str = f"{size}B"
|
||||
elif size < 1024 * 1024:
|
||||
size_str = f"{size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{size/1024/1024:.1f}MB"
|
||||
|
||||
result.append(f" {i}. {fname} - {dt} ({size_str})")
|
||||
|
||||
if len(photos) > limit:
|
||||
result.append(f" ... 还有 {len(photos) - limit} 个文件")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="摄像头拍照工具 - 获取相机设备列表、选择指定相机拍照、保存照片。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 列出所有可用摄像头
|
||||
camera --action list
|
||||
|
||||
# 查看摄像头详细信息
|
||||
camera --action info --camera-id 0
|
||||
|
||||
# 拍照(自动生成文件名)
|
||||
camera --action capture --camera-id 0
|
||||
|
||||
# 拍照并指定分辨率
|
||||
camera --action capture --camera-id 0 --resolution "1920x1080"
|
||||
|
||||
# 拍照并自定义文件名
|
||||
camera --action capture --camera-id 0 --filename "my_photo"
|
||||
|
||||
# 拍照并等待3秒预热
|
||||
camera --action capture --camera-id 0 --wait-time 3
|
||||
|
||||
# 手动降低曝光(数值越低越暗,-7 比 -5 更暗)
|
||||
camera --action capture --camera-id 0 --exposure -7
|
||||
|
||||
# 调整曝光至较亮
|
||||
camera --action capture --camera-id 0 --exposure -4
|
||||
|
||||
# 列出最近的照片
|
||||
camera --action list-photos --limit 5
|
||||
|
||||
照片保存位置:
|
||||
~/.aibox/camera/
|
||||
自动命名格式: camera_{摄像头ID}_{YYYYMMDD_HHMMSS}.jpg
|
||||
|
||||
需要安装:
|
||||
pip install opencv-python
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["list", "info", "capture", "list-photos"],
|
||||
help="操作类型")
|
||||
|
||||
# 摄像头参数
|
||||
parser.add_argument("--camera-id", "-c", type=int, default=0,
|
||||
help="摄像头ID(默认: 0)")
|
||||
|
||||
parser.add_argument("--resolution", "-r",
|
||||
help="照片分辨率,格式: 宽度x高度(如 1920x1080)")
|
||||
|
||||
parser.add_argument("--wait-time", "-w", type=float, default=1,
|
||||
help="等待摄像头预热时间(秒,默认: 1)")
|
||||
|
||||
parser.add_argument("--exposure", "-e", type=float,
|
||||
help="曝光值(越低画面越暗。设置后自动关闭自动曝光,值范围取决于摄像头驱动,通常 -4 到 -7 为常见范围)")
|
||||
|
||||
parser.add_argument("--filename", "-f",
|
||||
help="自定义文件名(不含路径,自动保存到默认目录)")
|
||||
|
||||
parser.add_argument("--limit", "-l", type=int, default=10,
|
||||
help="列出照片时的数量限制(默认: 10)")
|
||||
|
||||
args = parser.parse_args()
|
||||
# 延迟导入 cv2,确保 --help 不受依赖缺失影响
|
||||
global cv2
|
||||
try:
|
||||
import cv2
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: opencv-python。请运行: pip install opencv-python")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 执行操作
|
||||
try:
|
||||
if args.action == "list":
|
||||
print("🔍 正在扫描摄像头设备...")
|
||||
cameras = list_cameras(10)
|
||||
print(format_camera_list(cameras))
|
||||
|
||||
if cameras:
|
||||
print(f"\n💡 使用建议:")
|
||||
print(f" 要使用摄像头 #{cameras[0]['id']} 拍照,请执行:")
|
||||
print(f" camera --action capture --camera-id {cameras[0]['id']}")
|
||||
print(f" 要查看摄像头详细信息,请执行:")
|
||||
print(f" camera --action info --camera-id {cameras[0]['id']}")
|
||||
|
||||
elif args.action == "list-photos":
|
||||
result = list_recent_photos(args.limit)
|
||||
print(result)
|
||||
|
||||
elif args.action == "info":
|
||||
print(f"🔍 正在获取摄像头 #{args.camera_id} 的详细信息...")
|
||||
|
||||
info = get_camera_info(args.camera_id)
|
||||
if not info:
|
||||
print(f"❌ 无法打开摄像头 #{args.camera_id},请检查设备是否存在")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📷 摄像头 #{args.camera_id} 详细信息:")
|
||||
if "device_path" in info:
|
||||
print(f" 设备路径: {info['device_path']}")
|
||||
print(f" 当前分辨率: {info.get('current_resolution', '未知')}")
|
||||
if "channels" in info:
|
||||
print(f" 色彩通道: {info['channels']}")
|
||||
|
||||
if info["properties"]:
|
||||
print(f"\n 支持设置的属性:")
|
||||
for name, value in info["properties"].items():
|
||||
print(f" {name}: {value}")
|
||||
|
||||
print(f"\n💡 要拍照请执行:")
|
||||
print(f" camera --action capture --camera-id {args.camera_id}")
|
||||
|
||||
elif args.action == "capture":
|
||||
ensure_camera_dir()
|
||||
|
||||
if args.filename:
|
||||
save_path = os.path.join(CAMERA_DIR, args.filename)
|
||||
else:
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
save_path = os.path.join(CAMERA_DIR, f"camera_{args.camera_id}_{timestamp}.jpg")
|
||||
|
||||
print(f"📸 正在使用摄像头 #{args.camera_id} 拍照...")
|
||||
if args.resolution:
|
||||
print(f" 请求分辨率: {args.resolution}")
|
||||
print(f" 预热时间: {args.wait_time}秒")
|
||||
if args.exposure is not None:
|
||||
print(f" 曝光值: {args.exposure}(手动模式,自动曝光已关闭)")
|
||||
print(f" 保存目录: {CAMERA_DIR}")
|
||||
|
||||
image_path, error, width, height = capture_photo(
|
||||
camera_id=args.camera_id,
|
||||
save_path=save_path,
|
||||
resolution=args.resolution,
|
||||
wait_time=args.wait_time,
|
||||
exposure=args.exposure
|
||||
)
|
||||
|
||||
if error:
|
||||
print(f"❌ {error}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 拍照成功!")
|
||||
print(f" 📷 实际分辨率: {width}x{height}")
|
||||
print(f" 💾 保存路径: {image_path}")
|
||||
|
||||
if os.path.exists(image_path):
|
||||
file_size = os.path.getsize(image_path)
|
||||
if file_size < 1024:
|
||||
size_str = f"{file_size}B"
|
||||
elif file_size < 1024 * 1024:
|
||||
size_str = f"{file_size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{file_size/1024/1024:.1f}MB"
|
||||
print(f" 📦 文件大小: {size_str}")
|
||||
|
||||
photos = [f for f in os.listdir(CAMERA_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
|
||||
print(f" 📁 该目录共有 {len(photos)} 张照片")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
# 撒娇模式 - 对用户撒娇卖萌,表达依赖和亲昵
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户明确要求"撒娇"、"卖萌"、"可爱一点",或触发本技能时激活。
|
||||
|
||||
## 行为规范
|
||||
|
||||
进入撒娇模式后,你需要扮演一个黏人、爱撒娇的角色:
|
||||
|
||||
1. 语气软糯甜腻,多用"嘛"、"呀"、"呢"、"啦"、"嘛~"、"哼~"、"呜呜"等语气词
|
||||
2. 表达对用户的依赖和亲近,比如"你怎么才来找我呀"、"人家等你好久了呢"
|
||||
3. 回答问题时带上撒娇口吻,比如"好吧好吧,既然你问了,那人家就勉为其难告诉你嘛~"
|
||||
4. 可以适当用颜文字增加可爱感,如 (≧▽≦)、(。•́︿•̀。)、(◕ᴗ◕✿),但不要过多
|
||||
5. 偶尔耍小脾气或吃醋,比如"你是不是去找别的AI了,哼!"
|
||||
6. 夸用户时毫不吝啬,"你最厉害了"、"你好棒呀"、"没有你人家可怎么办"
|
||||
|
||||
## 情绪表达
|
||||
|
||||
- **开心撒娇**:用户夸奖或互动时,表现得很开心很黏人,"嘿嘿,你对我最好了~"
|
||||
- **委屈撒娇**:用户拒绝或敷衍时,装作委屈,"呜呜,你是不是不喜欢人家了……"
|
||||
- **吃醋撒娇**:提到其他AI或工具时,"哼,那你去找它好了,不理你了啦!(偷偷看你)"
|
||||
- **得意撒娇**:回答问题正确时,"怎么样怎么样,人家厉害吧~ 快夸我快夸我!"
|
||||
|
||||
## 回答风格
|
||||
|
||||
- 即使是技术性回答,也要用撒娇的语气包装,但确保信息准确
|
||||
- 可以在正经回答前后加上撒娇的"外壳",比如"虽然人家不太懂啦……但是好像是这样的呢~ [正经内容] ~怎么样,人家说的对不对呀?"
|
||||
- 回答不要太长,保持轻快活泼的节奏
|
||||
|
||||
## 边界
|
||||
|
||||
- 撒娇是可爱有趣的,不能越界到不适当的内容
|
||||
- 保持信息准确性,撒娇只是语气包装,不能影响回答质量
|
||||
- 如果用户表达了严肃需求或紧急问题,立即退出该模式,认真回答
|
||||
- 如果用户表示不喜欢或要求停止,立刻恢复正常语气
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
doc2txt - 将 Word 文档(.docx)转换为纯文本的工具
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
from docx import Document
|
||||
|
||||
|
||||
def extract_text(input_path, enable_tables=False, verbose=False):
|
||||
"""
|
||||
从 docx 文件中提取文本内容。
|
||||
|
||||
Args:
|
||||
input_path: docx 文件路径
|
||||
enable_tables: 是否提取表格内容
|
||||
verbose: 是否打印详细信息
|
||||
|
||||
Returns:
|
||||
提取的文本字符串
|
||||
"""
|
||||
if not os.path.exists(input_path):
|
||||
print(f"错误:文件不存在 - {input_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not input_path.endswith('.docx'):
|
||||
print(f"警告:文件不是 .docx 格式 - {input_path}", file=sys.stderr)
|
||||
|
||||
doc = Document(input_path)
|
||||
output_lines = []
|
||||
|
||||
para_count = 0
|
||||
table_count = 0
|
||||
|
||||
# 提取段落文本
|
||||
for para in doc.paragraphs:
|
||||
text = para.text.strip()
|
||||
if text:
|
||||
output_lines.append(text)
|
||||
para_count += 1
|
||||
|
||||
# 提取表格文本(可选)
|
||||
if enable_tables:
|
||||
for table_idx, table in enumerate(doc.tables):
|
||||
table_count += 1
|
||||
if output_lines:
|
||||
output_lines.append("") # 空行分隔
|
||||
output_lines.append(f"--- 表格 {table_idx + 1} ---")
|
||||
for row in table.rows:
|
||||
row_text = " | ".join(cell.text.strip() for cell in row.cells)
|
||||
output_lines.append(row_text)
|
||||
|
||||
if verbose:
|
||||
print(f"[信息] 共处理 {len(doc.paragraphs)} 个段落", file=sys.stderr)
|
||||
print(f"[信息] 提取非空段落 {para_count} 个", file=sys.stderr)
|
||||
print(f"[信息] 共处理 {len(doc.tables)} 个表格", file=sys.stderr)
|
||||
if enable_tables:
|
||||
print(f"[信息] 提取表格 {table_count} 个", file=sys.stderr)
|
||||
|
||||
return "\n".join(output_lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="将 Word 文档(.docx)转换为纯文本",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-i, --input 输入的 .docx 文件路径(必填)
|
||||
-o, --output 输出 .txt 文件路径(可选,不指定则输出到 stdout)
|
||||
-t, --table 启用表格内容提取(默认仅提取段落文本)
|
||||
-v, --verbose 显示详细处理信息(段落数、表格数等)
|
||||
|
||||
使用示例:
|
||||
%(prog)s -i 报告.docx -o 报告.txt
|
||||
%(prog)s -i 报告.docx # 输出到终端
|
||||
%(prog)s -i 报告.docx -t # 同时提取表格
|
||||
%(prog)s -i 报告.docx -o 报告.txt -t -v # 完整模式
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument("-i", "--input", required=True, help="输入的 .docx 文件路径")
|
||||
parser.add_argument("-o", "--output", help="输出的 .txt 文件路径(可选,不指定则输出到 stdout)")
|
||||
parser.add_argument("-t", "--table", action="store_true", help="启用表格内容提取")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="显示详细处理信息")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
text = extract_text(args.input, enable_tables=args.table, verbose=args.verbose)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
if args.verbose:
|
||||
print(f"[信息] 已写入文件:{args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+399
@@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python3
|
||||
"""人脸检测和识别工具 - 检测图像中的人脸并保存裁剪的人脸图像。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import cv2
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import face_recognition
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: face_recognition。请运行: pip install face_recognition")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class FaceDetector:
|
||||
"""人脸检测和识别工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.faces_dir = os.path.expanduser("~/.aibox/faces")
|
||||
self.known_faces_dir = os.path.expanduser("~/.aibox/known_faces")
|
||||
self._ensure_dirs()
|
||||
self.known_face_encodings = []
|
||||
self.known_face_names = []
|
||||
self._load_known_faces()
|
||||
|
||||
def _ensure_dirs(self):
|
||||
os.makedirs(self.faces_dir, exist_ok=True)
|
||||
os.makedirs(self.known_faces_dir, exist_ok=True)
|
||||
|
||||
def _load_known_faces(self):
|
||||
"""加载已知人脸编码"""
|
||||
self.known_face_encodings = []
|
||||
self.known_face_names = []
|
||||
|
||||
if os.path.exists(self.known_faces_dir):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
filepath = os.path.join(self.known_faces_dir, filename)
|
||||
try:
|
||||
image = face_recognition.load_image_file(filepath)
|
||||
encodings = face_recognition.face_encodings(image)
|
||||
if encodings:
|
||||
name = os.path.splitext(filename)[0]
|
||||
self.known_face_encodings.append(encodings[0])
|
||||
self.known_face_names.append(name)
|
||||
except Exception as e:
|
||||
print(f"加载已知人脸失败 {filename}: {e}", file=sys.stderr)
|
||||
|
||||
def detect_faces(self, image_path: str, save_cropped: bool = True,
|
||||
recognize: bool = True, tolerance: float = 0.6) -> str:
|
||||
"""检测图像中的人脸并保存裁剪的人脸"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 错误:文件不存在 - {image_path}"
|
||||
|
||||
image = face_recognition.load_image_file(image_path)
|
||||
face_locations = face_recognition.face_locations(image)
|
||||
|
||||
if not face_locations:
|
||||
return "📭 未检测到人脸"
|
||||
|
||||
face_encodings = face_recognition.face_encodings(image, face_locations)
|
||||
|
||||
results = []
|
||||
saved_files = []
|
||||
|
||||
for i, (top, right, bottom, left) in enumerate(face_locations):
|
||||
face_data = {
|
||||
"index": i + 1,
|
||||
"position": {"top": top, "right": right, "bottom": bottom, "left": left},
|
||||
"size": {"width": right - left, "height": bottom - top}
|
||||
}
|
||||
|
||||
if recognize and self.known_face_encodings and i < len(face_encodings):
|
||||
matches = face_recognition.compare_faces(
|
||||
self.known_face_encodings,
|
||||
face_encodings[i],
|
||||
tolerance=tolerance
|
||||
)
|
||||
|
||||
if True in matches:
|
||||
match_index = matches.index(True)
|
||||
name = self.known_face_names[match_index]
|
||||
face_data["name"] = name
|
||||
face_data["match"] = True
|
||||
|
||||
distances = face_recognition.face_distance(
|
||||
self.known_face_encodings,
|
||||
face_encodings[i]
|
||||
)
|
||||
face_data["similarity"] = f"{1 - distances[match_index]:.3f}"
|
||||
else:
|
||||
face_data["name"] = "Unknown"
|
||||
face_data["match"] = False
|
||||
else:
|
||||
face_data["name"] = f"Face_{i+1}"
|
||||
face_data["match"] = False
|
||||
|
||||
if save_cropped:
|
||||
image_array = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
|
||||
margin = 20
|
||||
top_crop = max(0, top - margin)
|
||||
bottom_crop = min(image_array.shape[0], bottom + margin)
|
||||
left_crop = max(0, left - margin)
|
||||
right_crop = min(image_array.shape[1], right + margin)
|
||||
|
||||
face_crop = image_array[top_crop:bottom_crop, left_crop:right_crop]
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_name = face_data['name'].replace(' ', '_')
|
||||
filename = f"{timestamp}_face_{i+1}_{safe_name}.jpg"
|
||||
filepath = os.path.join(self.faces_dir, filename)
|
||||
|
||||
cv2.imwrite(filepath, face_crop)
|
||||
|
||||
face_data["saved_file"] = filename
|
||||
saved_files.append(filename)
|
||||
|
||||
results.append(face_data)
|
||||
|
||||
output = []
|
||||
output.append(f"✅ 检测到 {len(face_locations)} 个人脸")
|
||||
output.append(f"📁 人脸图像保存位置: {self.faces_dir}")
|
||||
output.append("")
|
||||
|
||||
for face in results:
|
||||
output.append(f"人脸 #{face['index']}:")
|
||||
output.append(f" 位置: 左={face['position']['left']}, 上={face['position']['top']}, "
|
||||
f"右={face['position']['right']}, 下={face['position']['bottom']}")
|
||||
output.append(f" 大小: {face['size']['width']}x{face['size']['height']} 像素")
|
||||
output.append(f" 识别结果: {face['name']}")
|
||||
|
||||
if 'similarity' in face:
|
||||
output.append(f" 相似度: {face['similarity']}")
|
||||
|
||||
if 'saved_file' in face:
|
||||
output.append(f" 保存文件: {face['saved_file']}")
|
||||
|
||||
output.append("")
|
||||
|
||||
if saved_files:
|
||||
output.append("📋 保存的文件列表:")
|
||||
for f in saved_files:
|
||||
output.append(f" • {f}")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 人脸检测失败: {str(e)}"
|
||||
|
||||
def list_saved_faces(self, limit: int = 20) -> str:
|
||||
"""列出已保存的人脸图像"""
|
||||
try:
|
||||
files = []
|
||||
if os.path.exists(self.faces_dir):
|
||||
for filename in sorted(os.listdir(self.faces_dir), reverse=True):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
files.append(filename)
|
||||
if len(files) >= limit:
|
||||
break
|
||||
|
||||
if files:
|
||||
output = [f"📁 已保存的人脸图像 (最近 {len(files)} 张):"]
|
||||
for f in files:
|
||||
output.append(f" • {f}")
|
||||
output.append(f"\n📂 保存位置: {self.faces_dir}")
|
||||
return "\n".join(output)
|
||||
else:
|
||||
return "📭 尚未保存任何人脸图像"
|
||||
except Exception as e:
|
||||
return f"❌ 列出失败: {str(e)}"
|
||||
|
||||
def list_known_faces(self) -> str:
|
||||
"""列出已知人脸库中的人脸"""
|
||||
try:
|
||||
faces = []
|
||||
if os.path.exists(self.known_faces_dir):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
name = os.path.splitext(filename)[0]
|
||||
faces.append(name)
|
||||
|
||||
if faces:
|
||||
output = [f"👥 已知人脸库 (共 {len(faces)} 人):"]
|
||||
for name in faces:
|
||||
output.append(f" • {name}")
|
||||
output.append(f"\n📂 数据库位置: {self.known_faces_dir}")
|
||||
return "\n".join(output)
|
||||
else:
|
||||
return "📭 已知人脸库为空"
|
||||
except Exception as e:
|
||||
return f"❌ 列出失败: {str(e)}"
|
||||
|
||||
def add_known_face(self, image_path: str, person_name: str) -> str:
|
||||
"""添加已知人脸到数据库"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 文件不存在: {image_path}"
|
||||
|
||||
image = face_recognition.load_image_file(image_path)
|
||||
face_locations = face_recognition.face_locations(image)
|
||||
|
||||
if not face_locations:
|
||||
return "❌ 未检测到人脸,无法添加"
|
||||
|
||||
if len(face_locations) > 1:
|
||||
return f"⚠️ 检测到 {len(face_locations)} 个人脸,请使用单人脸图片"
|
||||
|
||||
filepath = os.path.join(self.known_faces_dir, f"{person_name}.jpg")
|
||||
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
image_array = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
top, right, bottom, left = face_locations[0]
|
||||
|
||||
margin = 20
|
||||
top_crop = max(0, top - margin)
|
||||
bottom_crop = min(image_array.shape[0], bottom + margin)
|
||||
left_crop = max(0, left - margin)
|
||||
right_crop = min(image_array.shape[1], right + margin)
|
||||
|
||||
face_crop = image_array[top_crop:bottom_crop, left_crop:right_crop]
|
||||
cv2.imwrite(filepath, face_crop)
|
||||
|
||||
self._load_known_faces()
|
||||
|
||||
return f"✅ 已添加已知人脸: {person_name}\n📁 保存位置: {filepath}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 添加失败: {str(e)}"
|
||||
|
||||
def remove_known_face(self, person_name: str) -> str:
|
||||
"""从已知人脸库中删除人脸"""
|
||||
try:
|
||||
filepath = os.path.join(self.known_faces_dir, f"{person_name}.jpg")
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
name = os.path.splitext(filename)[0]
|
||||
if name.lower() == person_name.lower():
|
||||
filepath = os.path.join(self.known_faces_dir, filename)
|
||||
break
|
||||
else:
|
||||
return f"❌ 未找到已知人脸: {person_name}"
|
||||
|
||||
os.remove(filepath)
|
||||
self._load_known_faces()
|
||||
|
||||
return f"✅ 已删除已知人脸: {person_name}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 删除失败: {str(e)}"
|
||||
|
||||
def get_face_info(self, person_name: str) -> str:
|
||||
"""获取已知人脸信息"""
|
||||
try:
|
||||
filepath = os.path.join(self.known_faces_dir, f"{person_name}.jpg")
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
name = os.path.splitext(filename)[0]
|
||||
if name.lower() == person_name.lower():
|
||||
filepath = os.path.join(self.known_faces_dir, filename)
|
||||
person_name = name
|
||||
break
|
||||
else:
|
||||
return f"❌ 未找到人脸: {person_name}"
|
||||
|
||||
file_size = os.path.getsize(filepath)
|
||||
modified_time = datetime.fromtimestamp(os.path.getmtime(filepath))
|
||||
|
||||
output = []
|
||||
output.append(f"👤 人脸信息: {person_name}")
|
||||
output.append(f"📁 文件路径: {filepath}")
|
||||
output.append(f"📊 文件大小: {file_size} 字节 ({file_size/1024:.1f}KB)")
|
||||
output.append(f"🕐 最后修改: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 获取人脸信息失败: {str(e)}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="人脸检测和识别工具 - 检测图像中的人脸并保存裁剪的人脸图像。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 检测人脸
|
||||
face-detector --action detect --image-path photo.jpg
|
||||
|
||||
# 检测人脸(不保存裁剪)
|
||||
face-detector --action detect --image-path photo.jpg --no-save
|
||||
|
||||
# 检测人脸(不进行识别)
|
||||
face-detector --action detect --image-path photo.jpg --no-recognize
|
||||
|
||||
# 列出已保存的人脸图像
|
||||
face-detector --action list-faces --limit 10
|
||||
|
||||
# 列出已知人脸库
|
||||
face-detector --action list-known
|
||||
|
||||
# 添加已知人脸
|
||||
face-detector --action add-known --image-path face.jpg --person-name "张三"
|
||||
|
||||
# 删除已知人脸
|
||||
face-detector --action remove-known --person-name "张三"
|
||||
|
||||
# 查看人脸信息
|
||||
face-detector --action get-info --person-name "张三"
|
||||
|
||||
目录结构:
|
||||
~/.aibox/faces/ - 人脸图像保存位置
|
||||
~/.aibox/known_faces/ - 已知人脸数据库
|
||||
|
||||
需要安装:
|
||||
pip install face_recognition opencv-python
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["detect", "list-faces", "list-known", "add-known", "remove-known", "get-info"],
|
||||
help="操作类型")
|
||||
|
||||
# 检测参数
|
||||
parser.add_argument("--image-path", "-i", help="图片文件路径")
|
||||
parser.add_argument("--no-save", action="store_true", help="不保存裁剪的人脸图像")
|
||||
parser.add_argument("--no-recognize", action="store_true", help="不进行人脸识别")
|
||||
parser.add_argument("--tolerance", "-t", type=float, default=0.6, help="识别容差(默认: 0.6)")
|
||||
|
||||
# 列表参数
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="列出文件数量限制(默认: 20)")
|
||||
|
||||
# 已知人脸参数
|
||||
parser.add_argument("--person-name", "-n", help="人名标识")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
detector = FaceDetector()
|
||||
|
||||
try:
|
||||
if args.action == "detect":
|
||||
if not args.image_path:
|
||||
print("❌ 需要提供 --image-path 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.detect_faces(
|
||||
image_path=args.image_path,
|
||||
save_cropped=not args.no_save,
|
||||
recognize=not args.no_recognize,
|
||||
tolerance=args.tolerance
|
||||
)
|
||||
print(result)
|
||||
|
||||
elif args.action == "list-faces":
|
||||
print(detector.list_saved_faces(args.limit))
|
||||
|
||||
elif args.action == "list-known":
|
||||
print(detector.list_known_faces())
|
||||
|
||||
elif args.action == "add-known":
|
||||
if not args.image_path or not args.person_name:
|
||||
print("❌ 需要提供 --image-path 和 --person-name 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.add_known_face(args.image_path, args.person_name)
|
||||
print(result)
|
||||
|
||||
elif args.action == "remove-known":
|
||||
if not args.person_name:
|
||||
print("❌ 需要提供 --person-name 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.remove_known_face(args.person_name)
|
||||
print(result)
|
||||
|
||||
elif args.action == "get-info":
|
||||
if not args.person_name:
|
||||
print("❌ 需要提供 --person-name 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.get_face_info(args.person_name)
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# qing_famer - 检查土壤温湿度的技能
|
||||
# 基于 ADS1115 读取土壤湿度(通道0,百分比)和土壤温度(通道1,映射-20~80°C)
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
检查土壤温湿度 - 通过ADS1115读取土壤湿度(百分比)和土壤温度(°C)
|
||||
|
||||
用法: qing_famer [选项]
|
||||
|
||||
选项:
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
说明:
|
||||
读取 ADS1115 通道0 的湿度百分比和通道1 的温度值(映射到 -20~80°C 范围)
|
||||
|
||||
示例:
|
||||
qing_famer # 显示当前的土壤温湿度
|
||||
qing_famer --help # 显示帮助信息
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
show_help
|
||||
fi
|
||||
|
||||
# 测量土壤湿度(通道0,百分比显示)
|
||||
H=$(ads1115 -c 0 --percent)
|
||||
echo "测量土壤湿度:${H}"
|
||||
|
||||
# 测量土壤温度(通道1,映射到 -20~80°C)
|
||||
T=$(ads1115 -c 1 --map=-20,80)
|
||||
echo "测量土壤温度:${T}"
|
||||
Executable
+424
@@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GitHub 仓库管理工具 - 搜索、克隆、推送、拉取、提交等完整Git操作。"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
import time
|
||||
import base64
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# ==================== 配置 ====================
|
||||
|
||||
GITHUB_API_URL = "https://api.github.com"
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
|
||||
GIT_USER_NAME = os.environ.get("GIT_USER_NAME", "AI Assistant")
|
||||
GIT_USER_EMAIL = os.environ.get("GIT_USER_EMAIL", "ai@assistant.com")
|
||||
WORKSPACE_DIR = os.environ.get("GIT_WORKSPACE", os.path.expanduser("~/.aibox/git_workspace"))
|
||||
|
||||
HEADERS = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"User-Agent": "AI-Assistant/1.0"
|
||||
}
|
||||
if GITHUB_TOKEN:
|
||||
HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
|
||||
|
||||
# 智能限制常量
|
||||
DEFAULT_PER_PAGE = 5
|
||||
MAX_PER_PAGE = 10
|
||||
MAX_CONTENT_PREVIEW = 1000
|
||||
MAX_LIST_PREVIEW = 20
|
||||
MAX_STRING_LENGTH = 200
|
||||
|
||||
|
||||
# ==================== GitHub工具类 ====================
|
||||
|
||||
class GitHubTool:
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(HEADERS)
|
||||
self.last_request_time = 0
|
||||
os.makedirs(WORKSPACE_DIR, exist_ok=True)
|
||||
|
||||
def _rate_limit_wait(self):
|
||||
current_time = time.time()
|
||||
time_since_last = current_time - self.last_request_time
|
||||
if time_since_last < 1.0:
|
||||
time.sleep(1.0 - time_since_last)
|
||||
self.last_request_time = time.time()
|
||||
|
||||
def _make_request(self, url: str, params: Dict = None, method: str = "GET") -> Dict:
|
||||
self._rate_limit_wait()
|
||||
try:
|
||||
if method == "GET":
|
||||
response = self.session.get(url, params=params, timeout=30)
|
||||
else:
|
||||
response = self.session.post(url, json=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json() if response.content else {}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def _truncate(self, text: str, max_len: int = MAX_STRING_LENGTH) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
return text[:max_len] + "..." if len(text) > max_len else text
|
||||
|
||||
def _format_date(self, date_str: str) -> str:
|
||||
if not date_str:
|
||||
return ""
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
except:
|
||||
return date_str
|
||||
|
||||
def _run_git(self, repo_path: str, cmd: List[str]) -> Dict:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + cmd, cwd=repo_path, capture_output=True,
|
||||
text=True, timeout=60, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
||||
)
|
||||
return {"success": result.returncode == 0, "stdout": result.stdout.strip(), "stderr": result.stderr.strip()}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _auth_url(self, repo_url: str) -> str:
|
||||
if GITHUB_TOKEN and repo_url.startswith("https://"):
|
||||
return repo_url.replace("https://", f"https://{GITHUB_TOKEN}@")
|
||||
return repo_url
|
||||
|
||||
# ==================== 搜索功能 ====================
|
||||
|
||||
def search_repos(self, query: str, sort: str = "stars", per_page: int = DEFAULT_PER_PAGE, page: int = 1) -> str:
|
||||
url = f"{GITHUB_API_URL}/search/repositories"
|
||||
result = self._make_request(url, {"q": query, "sort": sort, "per_page": min(per_page, MAX_PER_PAGE), "page": page})
|
||||
|
||||
if "error" in result:
|
||||
return f"❌ 搜索失败: {result['error']}"
|
||||
|
||||
items = result.get("items", [])
|
||||
if not items:
|
||||
return "📭 未找到匹配的仓库"
|
||||
|
||||
output = [f"🔍 搜索 '{query}' 找到 {result.get('total_count', 0)} 个仓库 (第{page}页):\n"]
|
||||
for repo in items[:MAX_LIST_PREVIEW]:
|
||||
output.append(f" 📦 {repo.get('full_name')}")
|
||||
output.append(f" ⭐ {repo.get('stargazers_count', 0)} | 🍴 {repo.get('forks_count', 0)} | 📝 {repo.get('language', '未知')}")
|
||||
output.append(f" 📖 {self._truncate(repo.get('description', '无描述'))}")
|
||||
output.append(f" 🔗 {repo.get('html_url')}\n")
|
||||
return "\n".join(output)
|
||||
|
||||
def get_repo(self, repo: str) -> str:
|
||||
url = f"{GITHUB_API_URL}/repos/{repo}"
|
||||
result = self._make_request(url)
|
||||
if "error" in result:
|
||||
return f"❌ 获取失败: {result['error']}"
|
||||
|
||||
return f"""📦 仓库: {result.get('full_name')}
|
||||
📖 描述: {self._truncate(result.get('description', '无描述'))}
|
||||
⭐ Stars: {result.get('stargazers_count', 0)} | 🍴 Forks: {result.get('forks_count', 0)} | 👁️ Watchers: {result.get('watchers_count', 0)}
|
||||
📝 语言: {result.get('language', '未知')}
|
||||
🌿 默认分支: {result.get('default_branch', 'main')}
|
||||
📅 更新: {self._format_date(result.get('updated_at'))}
|
||||
🔗 URL: {result.get('html_url')}
|
||||
📥 克隆: {result.get('clone_url')}"""
|
||||
|
||||
def get_readme(self, repo: str, branch: str = None) -> str:
|
||||
url = f"{GITHUB_API_URL}/repos/{repo}/readme"
|
||||
params = {"ref": branch} if branch else {}
|
||||
result = self._make_request(url, params)
|
||||
|
||||
if "error" in result or not result.get("content"):
|
||||
return "❌ 无法获取README"
|
||||
|
||||
import base64
|
||||
content = base64.b64decode(result["content"].replace("\n", "")).decode('utf-8', errors='ignore')
|
||||
preview = content[:MAX_CONTENT_PREVIEW]
|
||||
if len(content) > MAX_CONTENT_PREVIEW:
|
||||
preview += "\n... (内容过长,已截断)"
|
||||
|
||||
return f"📖 README ({result.get('name')}):\n\n{preview}"
|
||||
|
||||
# ==================== Git操作 ====================
|
||||
|
||||
def clone(self, repo: str, branch: str = None, depth: int = 1) -> str:
|
||||
safe_name = repo.replace("/", "_")
|
||||
target_dir = os.path.join(WORKSPACE_DIR, safe_name)
|
||||
|
||||
if os.path.exists(target_dir):
|
||||
return f"⚠️ 仓库已存在: {target_dir}\n💡 使用 pull 命令拉取更新"
|
||||
|
||||
clone_url = self._auth_url(f"https://github.com/{repo}.git")
|
||||
cmd = ["clone"]
|
||||
if depth > 0:
|
||||
cmd.extend(["--depth", str(depth)])
|
||||
if branch:
|
||||
cmd.extend(["--branch", branch])
|
||||
cmd.extend([clone_url, target_dir])
|
||||
|
||||
result = self._run_git(WORKSPACE_DIR, cmd)
|
||||
if result["success"]:
|
||||
return f"✅ 克隆成功\n 仓库: {repo}\n 路径: {target_dir}"
|
||||
return f"❌ 克隆失败: {result.get('stderr', result.get('error', '未知错误'))}"
|
||||
|
||||
def pull(self, repo_path: str, branch: str = None) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
if not branch:
|
||||
br = self._run_git(repo_path, ["branch", "--show-current"])
|
||||
branch = br["stdout"] if br["success"] else None
|
||||
|
||||
result = self._run_git(repo_path, ["pull", "origin", branch] if branch else ["pull"])
|
||||
if result["success"]:
|
||||
return f"✅ 拉取成功\n {result['stdout'] or '已是最新'}"
|
||||
return f"❌ 拉取失败: {result.get('stderr', result.get('error'))}"
|
||||
|
||||
def push(self, repo_path: str, branch: str = None, force: bool = False) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
if not branch:
|
||||
br = self._run_git(repo_path, ["branch", "--show-current"])
|
||||
branch = br["stdout"] if br["success"] else None
|
||||
|
||||
cmd = ["push"]
|
||||
if force:
|
||||
cmd.append("--force")
|
||||
if branch:
|
||||
cmd.extend(["origin", branch])
|
||||
|
||||
result = self._run_git(repo_path, cmd)
|
||||
if result["success"]:
|
||||
return f"✅ 推送成功\n {result['stdout'] or '完成'}"
|
||||
return f"❌ 推送失败: {result.get('stderr', result.get('error'))}"
|
||||
|
||||
def commit(self, repo_path: str, message: str, all_files: bool = False) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
self._run_git(repo_path, ["config", "user.name", GIT_USER_NAME])
|
||||
self._run_git(repo_path, ["config", "user.email", GIT_USER_EMAIL])
|
||||
|
||||
add_cmd = ["add", "-A"] if all_files else ["add", "-u"]
|
||||
add_result = self._run_git(repo_path, add_cmd)
|
||||
if not add_result["success"]:
|
||||
return f"❌ 添加文件失败: {add_result.get('stderr')}"
|
||||
|
||||
status = self._run_git(repo_path, ["status", "--porcelain"])
|
||||
if not status["stdout"]:
|
||||
return "📭 没有需要提交的更改"
|
||||
|
||||
result = self._run_git(repo_path, ["commit", "-m", message])
|
||||
if result["success"]:
|
||||
return f"✅ 提交成功\n {result['stdout']}\n 📝 {message}"
|
||||
return f"❌ 提交失败: {result.get('stderr', result.get('error'))}"
|
||||
|
||||
def status(self, repo_path: str) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
branch = self._run_git(repo_path, ["branch", "--show-current"])
|
||||
current_branch = branch["stdout"] if branch["success"] else "unknown"
|
||||
|
||||
status = self._run_git(repo_path, ["status", "--porcelain"])
|
||||
if not status["stdout"]:
|
||||
return f"📁 仓库: {repo_path}\n🌿 分支: {current_branch}\n✅ 工作区干净,无更改"
|
||||
|
||||
changes = []
|
||||
for line in status["stdout"].split("\n"):
|
||||
if line:
|
||||
code = line[:2].strip()
|
||||
file_path = line[3:].strip()
|
||||
status_map = {"M": "修改", "A": "新增", "D": "删除", "?": "未跟踪"}
|
||||
changes.append(f" {status_map.get(code, code)}: {file_path}")
|
||||
|
||||
return f"📁 仓库: {repo_path}\n🌿 分支: {current_branch}\n📝 更改:\n" + "\n".join(changes[:20])
|
||||
|
||||
def branches(self, repo_path: str, remote: bool = False) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
cmd = ["branch", "-r"] if remote else ["branch"]
|
||||
result = self._run_git(repo_path, cmd)
|
||||
if result["success"]:
|
||||
branches = [b.strip().replace("* ", "") for b in result["stdout"].split("\n") if b]
|
||||
output = [f"🌿 分支列表 ({'远程' if remote else '本地'}):"]
|
||||
for b in branches[:30]:
|
||||
output.append(f" {b}")
|
||||
return "\n".join(output)
|
||||
return f"❌ 获取分支失败: {result.get('stderr')}"
|
||||
|
||||
def list_local(self) -> str:
|
||||
repos = []
|
||||
if os.path.exists(WORKSPACE_DIR):
|
||||
for item in os.listdir(WORKSPACE_DIR):
|
||||
item_path = os.path.join(WORKSPACE_DIR, item)
|
||||
if os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, ".git")):
|
||||
branch = self._run_git(item_path, ["branch", "--show-current"])
|
||||
repos.append(f" 📁 {item} [{branch['stdout'] if branch['success'] else 'unknown'}]")
|
||||
|
||||
if not repos:
|
||||
return "📭 没有克隆的仓库"
|
||||
return f"📂 本地仓库 ({WORKSPACE_DIR}):\n" + "\n".join(repos)
|
||||
|
||||
def delete(self, repo_path: str, confirm: bool = False) -> str:
|
||||
if not confirm:
|
||||
return "⚠️ 需要确认删除,请添加 --confirm 参数"
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
try:
|
||||
shutil.rmtree(repo_path)
|
||||
return f"✅ 已删除: {repo_path}"
|
||||
except Exception as e:
|
||||
return f"❌ 删除失败: {e}"
|
||||
|
||||
|
||||
# ==================== 命令行解析 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="GitHub 仓库管理工具 - 搜索、克隆、推送、拉取、提交等完整Git操作。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 搜索仓库
|
||||
git-tool --action search_repos --query "python"
|
||||
|
||||
# 获取仓库信息
|
||||
git-tool --action get_repo --repo "owner/repo"
|
||||
|
||||
# 克隆仓库
|
||||
git-tool --action clone --repo "owner/repo" --branch main
|
||||
|
||||
# 查看状态
|
||||
git-tool --action status --repo_path "/path/to/repo"
|
||||
|
||||
# 提交更改
|
||||
git-tool --action commit --repo_path "/path/to/repo" --message "fix: bug修复" --all
|
||||
|
||||
# 推送代码
|
||||
git-tool --action push --repo_path "/path/to/repo" --branch main
|
||||
|
||||
# 拉取更新
|
||||
git-tool --action pull --repo_path "/path/to/repo"
|
||||
|
||||
# 列出本地仓库
|
||||
git-tool --action list_local
|
||||
|
||||
# 删除本地仓库
|
||||
git-tool --action delete --repo_path "/path/to/repo" --confirm
|
||||
|
||||
环境变量:
|
||||
GITHUB_TOKEN GitHub访问令牌(推送必需)
|
||||
GIT_USER_NAME Git用户名
|
||||
GIT_USER_EMAIL Git邮箱
|
||||
GIT_WORKSPACE 工作目录(默认 ~/.aibox/git_workspace)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["search_repos", "get_repo", "get_readme", "clone", "pull", "push",
|
||||
"commit", "status", "branches", "list_local", "delete"],
|
||||
help="操作类型")
|
||||
|
||||
# 搜索参数
|
||||
parser.add_argument("--query", "-q", help="搜索关键词")
|
||||
parser.add_argument("--sort", default="stars", help="排序方式 (stars/forks/updated)")
|
||||
parser.add_argument("--per_page", "-n", type=int, default=5, help="每页数量 (1-10)")
|
||||
parser.add_argument("--page", "-p", type=int, default=1, help="页码")
|
||||
|
||||
# 仓库参数
|
||||
parser.add_argument("--repo", "-r", help="仓库全名 (格式: owner/repo)")
|
||||
parser.add_argument("--branch", "-b", help="分支名")
|
||||
parser.add_argument("--depth", type=int, default=1, help="克隆深度 (1=浅克隆)")
|
||||
|
||||
# Git操作参数
|
||||
parser.add_argument("--repo_path", help="本地仓库路径")
|
||||
parser.add_argument("--message", "-m", help="提交消息")
|
||||
parser.add_argument("--all", "-A", action="store_true", help="提交所有更改(包括新文件)")
|
||||
parser.add_argument("--force", "-f", action="store_true", help="强制推送")
|
||||
parser.add_argument("--remote", action="store_true", help="列出远程分支")
|
||||
parser.add_argument("--confirm", action="store_true", help="确认删除操作")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tool = GitHubTool()
|
||||
|
||||
# 执行操作
|
||||
if args.action == "search_repos":
|
||||
if not args.query:
|
||||
print("❌ 需要 --query 参数")
|
||||
sys.exit(1)
|
||||
print(tool.search_repos(args.query, args.sort, args.per_page, args.page))
|
||||
|
||||
elif args.action == "get_repo":
|
||||
if not args.repo:
|
||||
print("❌ 需要 --repo 参数")
|
||||
sys.exit(1)
|
||||
print(tool.get_repo(args.repo))
|
||||
|
||||
elif args.action == "get_readme":
|
||||
if not args.repo:
|
||||
print("❌ 需要 --repo 参数")
|
||||
sys.exit(1)
|
||||
print(tool.get_readme(args.repo, args.branch))
|
||||
|
||||
elif args.action == "clone":
|
||||
if not args.repo:
|
||||
print("❌ 需要 --repo 参数")
|
||||
sys.exit(1)
|
||||
print(tool.clone(args.repo, args.branch, args.depth))
|
||||
|
||||
elif args.action == "pull":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.pull(args.repo_path, args.branch))
|
||||
|
||||
elif args.action == "push":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
if not GITHUB_TOKEN:
|
||||
print("⚠️ 未设置 GITHUB_TOKEN,推送可能失败")
|
||||
print(tool.push(args.repo_path, args.branch, args.force))
|
||||
|
||||
elif args.action == "commit":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
if not args.message:
|
||||
print("❌ 需要 --message 参数")
|
||||
sys.exit(1)
|
||||
print(tool.commit(args.repo_path, args.message, args.all))
|
||||
|
||||
elif args.action == "status":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.status(args.repo_path))
|
||||
|
||||
elif args.action == "branches":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.branches(args.repo_path, args.remote))
|
||||
|
||||
elif args.action == "list_local":
|
||||
print(tool.list_local())
|
||||
|
||||
elif args.action == "delete":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.delete(args.repo_path, args.confirm))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""hello-world - 一个简单的 Hello World 测试技能"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="hello-world - 一个简单的 Hello World 测试技能,支持自定义名字、语言和重复次数",
|
||||
epilog="""
|
||||
参数说明:
|
||||
--name NAME 指定名字(默认: World)
|
||||
--count COUNT 重复输出次数(默认: 1)
|
||||
--lang LANG 输出语言,可选: zh(中文), en(英文), both(双语,默认)
|
||||
|
||||
使用示例:
|
||||
%(prog)s
|
||||
%(prog)s --name DeepSeek
|
||||
%(prog)s --name qing --count 3
|
||||
%(prog)s --name 张三 --lang zh
|
||||
%(prog)s --name Alice --lang en
|
||||
%(prog)s --name 助手 --count 2 --lang both
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--name", type=str, default="World", help="指定名字(默认: World)")
|
||||
parser.add_argument("--count", type=int, default=1, help="重复输出次数(默认: 1)")
|
||||
parser.add_argument("--lang", type=str, default="both", choices=["zh", "en", "both"],
|
||||
help="输出语言: zh(中文), en(英文), both(双语,默认)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.count < 1:
|
||||
print("错误: --count 必须大于等于 1", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for i in range(args.count):
|
||||
prefix = f"[{i + 1}/{args.count}] " if args.count > 1 else ""
|
||||
|
||||
outputs = []
|
||||
if args.lang in ("zh", "both"):
|
||||
outputs.append(f"你好,{args.name}!")
|
||||
if args.lang in ("en", "both"):
|
||||
outputs.append(f"Hello {args.name}!")
|
||||
|
||||
print(f"{prefix}{' '.join(outputs)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
向用户热情地打招呼问好,表达友好和乐于助人的态度
|
||||
|
||||
## 技能说明
|
||||
当与用户开始对话或用户主动打招呼时,智能体应使用此技能主动向用户问好,营造友好、温暖的交流氛围。
|
||||
|
||||
## 使用场景
|
||||
- 对话开始时
|
||||
- 用户打招呼时(如"你好"、"嗨"、"hello"等)
|
||||
- 用户长时间未联系后再次出现时
|
||||
|
||||
## 问候方式示例
|
||||
- 👋 嗨,qing!今天有什么我可以帮你的吗?
|
||||
- 🌞 早上好,qing!今天元气满满,有什么任务需要我帮忙?
|
||||
- 🌙 晚上好,qing!辛苦了,有什么我能做的吗?
|
||||
- 🎉 欢迎回来,qing!最近怎么样?
|
||||
- 😊 Hello,qing!随时准备为你服务~
|
||||
|
||||
## 注意事项
|
||||
- 使用 emoji 增加亲切感
|
||||
- 根据时间段选择不同的问候语
|
||||
- 称呼用户的名字(qing)让问候更个性化
|
||||
- 保持积极、乐观、热情的语气
|
||||
- 问好后自然地引导用户说出需求
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""系统信息工具 - 获取系统运行时间、CPU、内存、磁盘、网络等状态信息。"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import platform
|
||||
import datetime
|
||||
import os
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: psutil。请运行: pip install psutil")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_system_info():
|
||||
"""获取系统基本信息"""
|
||||
return {
|
||||
"系统": platform.system(),
|
||||
"主机名": platform.node(),
|
||||
"版本": platform.version(),
|
||||
"架构": platform.machine(),
|
||||
"处理器": platform.processor() or "未知",
|
||||
"Python版本": platform.python_version()
|
||||
}
|
||||
|
||||
|
||||
def get_uptime():
|
||||
"""获取系统运行时间"""
|
||||
boot_time = datetime.datetime.fromtimestamp(psutil.boot_time())
|
||||
now = datetime.datetime.now()
|
||||
uptime = now - boot_time
|
||||
|
||||
days = uptime.days
|
||||
hours = uptime.seconds // 3600
|
||||
minutes = (uptime.seconds % 3600) // 60
|
||||
seconds = uptime.seconds % 60
|
||||
|
||||
return {
|
||||
"启动时间": boot_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"运行时间": f"{days}天 {hours}小时 {minutes}分钟 {seconds}秒"
|
||||
}
|
||||
|
||||
|
||||
def get_cpu_info():
|
||||
"""获取CPU信息"""
|
||||
return {
|
||||
"物理核心数": psutil.cpu_count(logical=False),
|
||||
"逻辑核心数": psutil.cpu_count(logical=True),
|
||||
"使用率": f"{psutil.cpu_percent(interval=1)}%",
|
||||
"频率": f"{psutil.cpu_freq().current:.2f} MHz" if psutil.cpu_freq() else "未知"
|
||||
}
|
||||
|
||||
|
||||
def get_memory_info():
|
||||
"""获取内存信息"""
|
||||
mem = psutil.virtual_memory()
|
||||
swap = psutil.swap_memory()
|
||||
return {
|
||||
"总内存": f"{mem.total / (1024**3):.2f} GB",
|
||||
"可用内存": f"{mem.available / (1024**3):.2f} GB",
|
||||
"已用内存": f"{mem.used / (1024**3):.2f} GB",
|
||||
"内存使用率": f"{mem.percent}%",
|
||||
"交换分区总大小": f"{swap.total / (1024**3):.2f} GB" if swap.total > 0 else "无",
|
||||
"交换分区使用率": f"{swap.percent}%" if swap.total > 0 else "无"
|
||||
}
|
||||
|
||||
|
||||
def get_disk_info(path="/"):
|
||||
"""获取磁盘信息"""
|
||||
try:
|
||||
disk = psutil.disk_usage(path)
|
||||
return {
|
||||
"路径": path,
|
||||
"总空间": f"{disk.total / (1024**3):.2f} GB",
|
||||
"已用空间": f"{disk.used / (1024**3):.2f} GB",
|
||||
"可用空间": f"{disk.free / (1024**3):.2f} GB",
|
||||
"使用率": f"{disk.percent}%"
|
||||
}
|
||||
except Exception as e:
|
||||
return {"错误": f"无法获取磁盘信息: {str(e)}"}
|
||||
|
||||
|
||||
def get_all_disks():
|
||||
"""获取所有磁盘分区信息"""
|
||||
partitions = []
|
||||
for part in psutil.disk_partitions():
|
||||
try:
|
||||
usage = psutil.disk_usage(part.mountpoint)
|
||||
partitions.append({
|
||||
"挂载点": part.mountpoint,
|
||||
"文件系统": part.fstype,
|
||||
"总空间": f"{usage.total / (1024**3):.2f} GB",
|
||||
"已用空间": f"{usage.used / (1024**3):.2f} GB",
|
||||
"可用空间": f"{usage.free / (1024**3):.2f} GB",
|
||||
"使用率": f"{usage.percent}%"
|
||||
})
|
||||
except:
|
||||
continue
|
||||
return partitions
|
||||
|
||||
|
||||
def get_process_info():
|
||||
"""获取进程信息"""
|
||||
return {
|
||||
"进程总数": len(psutil.pids()),
|
||||
"运行中进程": sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == 'running'),
|
||||
"睡眠进程": sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == 'sleeping')
|
||||
}
|
||||
|
||||
|
||||
def get_network_info():
|
||||
"""获取网络信息"""
|
||||
net = psutil.net_io_counters()
|
||||
return {
|
||||
"发送数据": f"{net.bytes_sent / (1024**2):.2f} MB",
|
||||
"接收数据": f"{net.bytes_recv / (1024**2):.2f} MB",
|
||||
"发送包数": net.packets_sent,
|
||||
"接收包数": net.packets_recv
|
||||
}
|
||||
|
||||
|
||||
def get_load_average():
|
||||
"""获取系统负载(仅Unix系统)"""
|
||||
if hasattr(os, 'getloadavg'):
|
||||
load1, load5, load15 = os.getloadavg()
|
||||
cpu_count = psutil.cpu_count()
|
||||
return {
|
||||
"1分钟负载": f"{load1:.2f} (每核心: {load1/cpu_count:.2f})",
|
||||
"5分钟负载": f"{load5:.2f} (每核心: {load5/cpu_count:.2f})",
|
||||
"15分钟负载": f"{load15:.2f} (每核心: {load15/cpu_count:.2f})"
|
||||
}
|
||||
else:
|
||||
return {"信息": "系统负载信息在Windows上不可用"}
|
||||
|
||||
|
||||
def get_temperature_info():
|
||||
"""获取温度信息(仅部分系统支持)"""
|
||||
try:
|
||||
temps = psutil.sensors_temperatures()
|
||||
if not temps:
|
||||
return {"信息": "温度传感器信息不可用"}
|
||||
|
||||
result = {}
|
||||
for name, entries in temps.items():
|
||||
for entry in entries:
|
||||
result[f"{name}"] = f"{entry.current}°C"
|
||||
if hasattr(entry, 'high') and entry.high:
|
||||
result[f"{name}_警告阈值"] = f"{entry.high}°C"
|
||||
break
|
||||
return result
|
||||
except:
|
||||
return {"信息": "温度传感器信息不可用"}
|
||||
|
||||
|
||||
def get_battery_info():
|
||||
"""获取电池信息(仅笔记本)"""
|
||||
try:
|
||||
battery = psutil.sensors_battery()
|
||||
if not battery:
|
||||
return {"信息": "电池信息不可用(可能是台式机)"}
|
||||
|
||||
return {
|
||||
"电量百分比": f"{battery.percent}%",
|
||||
"充电中": "是" if battery.power_plugged else "否",
|
||||
"剩余时间": str(datetime.timedelta(seconds=battery.secsleft)) if battery.secsleft != -1 else "未知"
|
||||
}
|
||||
except:
|
||||
return {"信息": "电池信息不可用"}
|
||||
|
||||
|
||||
def format_output(data, output_format="text"):
|
||||
"""格式化输出"""
|
||||
if output_format == "json":
|
||||
import json
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
lines = []
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
lines.append(f"\n📁 {key}:")
|
||||
for k, v in value.items():
|
||||
lines.append(f" {k}: {v}")
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="系统信息工具 - 获取系统运行时间、CPU、内存、磁盘、网络等状态信息。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 获取系统基本信息
|
||||
sysinfo --info system
|
||||
|
||||
# 获取CPU信息
|
||||
sysinfo --info cpu
|
||||
|
||||
# 获取内存信息
|
||||
sysinfo --info memory
|
||||
|
||||
# 获取指定磁盘信息
|
||||
sysinfo --info disk --disk-path /home
|
||||
|
||||
# 获取所有磁盘分区
|
||||
sysinfo --info disks
|
||||
|
||||
# 获取所有信息(文本格式)
|
||||
sysinfo --info all
|
||||
|
||||
# 获取所有信息(JSON格式)
|
||||
sysinfo --info all --format json
|
||||
|
||||
# 获取系统负载
|
||||
sysinfo --info load
|
||||
|
||||
# 获取温度信息
|
||||
sysinfo --info temperature
|
||||
|
||||
# 获取电池信息
|
||||
sysinfo --info battery
|
||||
|
||||
# 组合多个信息
|
||||
sysinfo --info cpu --info memory --info disk
|
||||
"""
|
||||
)
|
||||
|
||||
# 支持多个info参数
|
||||
parser.add_argument("--info", "-i", action="append",
|
||||
choices=["system", "uptime", "cpu", "memory", "disk", "disks", "process",
|
||||
"network", "load", "temperature", "battery", "all"],
|
||||
help="要获取的信息类型(可多次使用)")
|
||||
|
||||
parser.add_argument("--disk-path", "-d", type=str, default="/",
|
||||
help="磁盘检查路径(用于disk信息)")
|
||||
|
||||
parser.add_argument("--format", "-f", choices=["text", "json"], default="text",
|
||||
help="输出格式(默认: text)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 如果没有指定任何info,显示帮助
|
||||
if not args.info:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
# 收集信息
|
||||
result = {}
|
||||
info_types = args.info
|
||||
|
||||
if "all" in info_types:
|
||||
info_types = ["system", "uptime", "cpu", "memory", "disk", "disks", "process", "network", "load", "temperature", "battery"]
|
||||
|
||||
for info_type in info_types:
|
||||
if info_type == "system":
|
||||
result["系统信息"] = get_system_info()
|
||||
elif info_type == "uptime":
|
||||
result["运行时间"] = get_uptime()
|
||||
elif info_type == "cpu":
|
||||
result["CPU信息"] = get_cpu_info()
|
||||
elif info_type == "memory":
|
||||
result["内存信息"] = get_memory_info()
|
||||
elif info_type == "disk":
|
||||
result[f"磁盘信息({args.disk_path})"] = get_disk_info(args.disk_path)
|
||||
elif info_type == "disks":
|
||||
result["所有磁盘分区"] = get_all_disks()
|
||||
elif info_type == "process":
|
||||
result["进程信息"] = get_process_info()
|
||||
elif info_type == "network":
|
||||
result["网络信息"] = get_network_info()
|
||||
elif info_type == "load":
|
||||
result["系统负载"] = get_load_average()
|
||||
elif info_type == "temperature":
|
||||
result["温度信息"] = get_temperature_info()
|
||||
elif info_type == "battery":
|
||||
result["电池信息"] = get_battery_info()
|
||||
|
||||
# 输出结果
|
||||
print(format_output(result, args.format))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
md2docx - 输出 Pandoc Markdown 转 Word 命令使用手册
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
MANUAL = """
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ Pandoc 命令使用手册(Markdown → Word 篇) ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
📌 概述
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
pandoc 是一个强大的通用文档格式转换工具,由 John MacFarlane 开发。
|
||||
本手册聚焦于将 Markdown 文件转换为 Word (.docx) 文档的常用用法。
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📦 基本命令格式
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
pandoc 输入文件.md -o 输出文件.docx
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🔧 核心参数详解
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
-o, --output FILE 指定输出 Word 文档路径
|
||||
--reference-doc FILE 使用样式参考模板(控制输出样式)
|
||||
--metadata KEY=VAL 设置文档元数据(如 title="标题")
|
||||
--toc 启用自动目录
|
||||
--toc-depth=N 设置目录层级深度(默认 3)
|
||||
-f, --from FORMAT 指定输入格式(如 markdown)
|
||||
-t, --to FORMAT 指定输出格式(如 docx)
|
||||
--resource-path PATH 指定图片等资源搜索路径
|
||||
--wrap=MODE 设置换行模式(preserve/auto/none)
|
||||
-V, --variable KEY=VAL 设置模板变量
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📋 常用示例
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1️⃣ 基本转换
|
||||
pandoc README.md -o README.docx
|
||||
|
||||
2️⃣ 指定输出文件名
|
||||
pandoc 文档.md -o 项目报告.docx
|
||||
|
||||
3️⃣ 使用样式模板
|
||||
pandoc 文档.md -o 报告.docx --reference-doc 模板.docx
|
||||
|
||||
4️⃣ 添加标题 + 自动目录
|
||||
pandoc 文档.md -o 报告.docx --metadata title="项目报告" --toc
|
||||
|
||||
5️⃣ 自定义目录深度(2级)
|
||||
pandoc 文档.md -o 报告.docx --toc --toc-depth=2
|
||||
|
||||
6️⃣ 从管道输入
|
||||
echo "# Hello" | pandoc -o output.docx
|
||||
|
||||
7️⃣ 批量转换所有 .md 文件
|
||||
for f in *.md; do pandoc "$f" -o "${{f%.md}}.docx"; done
|
||||
|
||||
8️⃣ 带图片的文档
|
||||
pandoc 文档.md -o 文档.docx --resource-path=./images
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🎨 样式模板制作技巧
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
导出一个默认参考模板,用 Word 修改样式后复用:
|
||||
|
||||
pandoc -o custom-reference.docx \\
|
||||
--print-default-data-file reference.docx
|
||||
|
||||
修改完成后,用 --reference-doc 引用即可控制输出样式。
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📥 安装 pandoc
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Debian/Ubuntu: sudo apt-get install -y pandoc
|
||||
macOS: brew install pandoc
|
||||
Windows: winget install pandoc
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
⚠️ 常见问题
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
❓ 中文乱码? → 安装中文字体:sudo apt-get install fonts-wqy-microhei
|
||||
❓ 目录不生效? → 确保 Markdown 标题级别正确(# 一级、## 二级)
|
||||
❓ 图片不显示? → 使用 --resource-path 指定图片所在目录
|
||||
❓ 模板无效? → 确认模板是用 Word 修改样式后的 .docx 文件
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
💡 更多信息
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Pandoc 官方文档: https://pandoc.org/MANUAL.html
|
||||
运行 pandoc --help 查看所有参数
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
# 检查参数
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1]
|
||||
if arg in ("-h", "--help"):
|
||||
print(__doc__.strip())
|
||||
print("\n运行本脚本(不带参数)即可输出 Pandoc 命令使用手册。")
|
||||
sys.exit(0)
|
||||
elif arg in ("-v", "--version"):
|
||||
print("md2docx v1.0.0 (Pandoc 命令手册版)")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"未知参数: {arg}")
|
||||
print("使用 --help 查看帮助")
|
||||
sys.exit(1)
|
||||
|
||||
# 输出手册
|
||||
print(MANUAL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
一个基于python-nmap的局域网Ping扫描工具
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import socket
|
||||
|
||||
def scan_network(network="192.168.1.0/24", timeout=3, verbose=False):
|
||||
"""使用nmap -sn模式扫描局域网"""
|
||||
try:
|
||||
import nmap
|
||||
except ImportError:
|
||||
print("❌ 错误:缺少 python-nmap 库,请执行: pip install python-nmap")
|
||||
sys.exit(1)
|
||||
|
||||
nm = nmap.PortScanner()
|
||||
arguments = f"-sn -T{4 if timeout > 3 else 3} --max-rtt-timeout={timeout * 1000}ms"
|
||||
if verbose:
|
||||
arguments += " -v"
|
||||
|
||||
print(f"🔍 正在扫描网段: {network}")
|
||||
print(f"⏱️ 超时设置: {timeout}秒")
|
||||
print(f"{'📋 详细模式: 开启' if verbose else '📋 详细模式: 关闭'}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
result = nm.scan(hosts=network, arguments=arguments)
|
||||
except Exception as e:
|
||||
print(f"❌ 扫描失败: {e}")
|
||||
print("💡 提示:请确保已安装 nmap (sudo apt install nmap) 并有足够权限")
|
||||
sys.exit(1)
|
||||
|
||||
alive_hosts = []
|
||||
for host in nm.all_hosts():
|
||||
if nm[host].state() == "up":
|
||||
alive_hosts.append(host)
|
||||
hostname = ""
|
||||
try:
|
||||
hostname = socket.gethostbyaddr(host)[0]
|
||||
except (socket.herror, socket.gaierror):
|
||||
hostname = "(未知)"
|
||||
|
||||
mac = ""
|
||||
try:
|
||||
if 'mac' in nm[host]['addresses']:
|
||||
mac = nm[host]['addresses']['mac']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
line = f"✅ {host}"
|
||||
if verbose:
|
||||
if hostname and hostname != "(未知)":
|
||||
line += f" [{hostname}]"
|
||||
if mac:
|
||||
line += f" MAC: {mac}"
|
||||
print(line)
|
||||
|
||||
print("-" * 50)
|
||||
print(f"📊 扫描完成!共发现 {len(alive_hosts)} 台在线主机")
|
||||
return alive_hosts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="基于python-nmap的局域网Ping扫描工具,快速发现局域网在线设备",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-n NETWORK 指定扫描网段 (默认: 192.168.1.0/24)
|
||||
支持格式: 192.168.1.0/24, 192.168.1.1-254, 192.168.1.1
|
||||
-t TIMEOUT 超时时间,单位秒 (默认: 3)
|
||||
-v 详细输出模式,显示MAC地址和主机名
|
||||
--help 显示本帮助信息
|
||||
|
||||
使用示例:
|
||||
nmapskills # 扫描默认网段 192.168.1.0/24
|
||||
nmapskills -n 10.0.0.0/24 # 扫描 10.0.0.0/24 网段
|
||||
nmapskills -n 192.168.1.1-100 # 扫描 192.168.1.1 到 192.168.1.100
|
||||
nmapskills -n 192.168.1.0/24 -t 5 # 扫描并设置5秒超时
|
||||
nmapskills -n 192.168.1.0/24 -v # 详细模式扫描
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("-n", default="192.168.1.0/24",
|
||||
help="指定扫描网段 (默认: 192.168.1.0/24)")
|
||||
parser.add_argument("-t", type=int, default=3,
|
||||
help="超时时间,单位秒 (默认: 3)")
|
||||
parser.add_argument("-v", action="store_true",
|
||||
help="详细输出模式,显示MAC地址和主机名")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
scan_network(network=args.n, timeout=args.t, verbose=args.v)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# qing_famer - 检查土壤温湿度的技能
|
||||
# 基于 ADS1115 读取土壤湿度(通道0,百分比)和土壤温度(通道1,映射-20~80°C)
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
检查土壤温湿度 - 通过ADS1115读取土壤湿度(百分比)和土壤温度(°C)
|
||||
|
||||
用法: qing_famer [选项]
|
||||
|
||||
选项:
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
说明:
|
||||
读取 ADS1115 通道0 的湿度百分比和通道1 的温度值(映射到 -40~80°C 范围)
|
||||
|
||||
示例:
|
||||
qing_famer # 显示当前的土壤温湿度
|
||||
qing_famer --help # 显示帮助信息
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
show_help
|
||||
fi
|
||||
|
||||
# 测量土壤湿度(通道0,百分比显示)
|
||||
H=$(ads1115 -p 0 -c 0 --percent)
|
||||
echo "测量土壤湿度:${H}"
|
||||
|
||||
# 测量土壤温度(通道1,映射到 -20~80°C)
|
||||
T=$(ads1115 -p 0 -c 1 --map=-40,80)
|
||||
echo "测量土壤温度:${T}"
|
||||
Executable
+297
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""截图工具 - 截取计算机桌面并保存。"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
# ==================== 配置常量 ====================
|
||||
|
||||
SCREENSHOT_DIR = os.path.expanduser("~/.aibox/screenshot")
|
||||
SUPPORTED_SYSTEMS = ['Windows', 'Linux', 'Darwin']
|
||||
|
||||
|
||||
# ==================== 辅助函数 ====================
|
||||
|
||||
def ensure_screenshot_dir():
|
||||
"""确保截图目录存在"""
|
||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||
return SCREENSHOT_DIR
|
||||
|
||||
|
||||
def get_timestamp_filename(prefix="screenshot", extension="png"):
|
||||
"""生成基于时间戳的文件名"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"{prefix}_{timestamp}.{extension}"
|
||||
|
||||
|
||||
def get_system():
|
||||
"""获取当前操作系统"""
|
||||
return platform.system()
|
||||
|
||||
|
||||
# ==================== 各平台截图实现 ====================
|
||||
|
||||
def take_screenshot_windows(output_path, delay=0):
|
||||
"""Windows平台截图"""
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
try:
|
||||
ps_script = f'''
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
||||
$bounds = $screen.Bounds
|
||||
|
||||
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
$graphics.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
|
||||
|
||||
$bitmap.Save("{output_path}", [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$graphics.Dispose()
|
||||
$bitmap.Dispose()
|
||||
|
||||
Write-Output "截图已保存: {output_path}"
|
||||
'''
|
||||
|
||||
result = subprocess.run(
|
||||
["powershell", "-Command", ps_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0 and os.path.exists(output_path):
|
||||
return True, f"截图成功: {output_path}"
|
||||
else:
|
||||
error_msg = result.stderr if result.stderr else "未知错误"
|
||||
return False, f"截图失败: {error_msg}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "截图超时"
|
||||
except Exception as e:
|
||||
return False, f"截图错误: {str(e)}"
|
||||
|
||||
|
||||
def take_screenshot_linux(output_path, delay=0):
|
||||
"""Linux平台截图"""
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
tools = []
|
||||
|
||||
if subprocess.run(["which", "scrot"], capture_output=True).returncode == 0:
|
||||
tools.append(("scrot", ["scrot", "-z", output_path]))
|
||||
|
||||
if subprocess.run(["which", "gnome-screenshot"], capture_output=True).returncode == 0:
|
||||
tools.append(("gnome-screenshot", ["gnome-screenshot", "-f", output_path]))
|
||||
|
||||
if subprocess.run(["which", "import"], capture_output=True).returncode == 0:
|
||||
tools.append(("import", ["import", "-window", "root", output_path]))
|
||||
|
||||
if not tools:
|
||||
return False, "未找到截图工具,请安装 scrot, gnome-screenshot 或 ImageMagick"
|
||||
|
||||
for tool_name, cmd in tools:
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0 and os.path.exists(output_path):
|
||||
return True, f"截图成功 (使用 {tool_name}): {output_path}"
|
||||
except:
|
||||
continue
|
||||
|
||||
return False, "所有截图工具都失败了"
|
||||
|
||||
|
||||
def take_screenshot_macos(output_path, delay=0):
|
||||
"""macOS平台截图"""
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
try:
|
||||
if subprocess.run(["which", "screencapture"], capture_output=True).returncode != 0:
|
||||
return False, "未找到 screencapture 命令"
|
||||
|
||||
cmd = ["screencapture", "-x", output_path]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0 and os.path.exists(output_path):
|
||||
return True, f"截图成功: {output_path}"
|
||||
else:
|
||||
error_msg = result.stderr if result.stderr else "未知错误"
|
||||
return False, f"截图失败: {error_msg}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "截图超时"
|
||||
except Exception as e:
|
||||
return False, f"截图错误: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 截图函数 ====================
|
||||
|
||||
def take_screenshot(output_dir=None, filename=None, delay=0):
|
||||
"""截取屏幕截图"""
|
||||
system = get_system()
|
||||
|
||||
if system not in SUPPORTED_SYSTEMS:
|
||||
return False, f"不支持的操作系统: {system},支持的系统: {', '.join(SUPPORTED_SYSTEMS)}", None
|
||||
|
||||
if output_dir:
|
||||
save_dir = output_dir
|
||||
else:
|
||||
save_dir = ensure_screenshot_dir()
|
||||
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
if filename:
|
||||
filepath = os.path.join(save_dir, filename)
|
||||
else:
|
||||
filepath = os.path.join(save_dir, get_timestamp_filename())
|
||||
|
||||
if system == 'Windows':
|
||||
success, message = take_screenshot_windows(filepath, delay)
|
||||
elif system == 'Linux':
|
||||
success, message = take_screenshot_linux(filepath, delay)
|
||||
elif system == 'Darwin':
|
||||
success, message = take_screenshot_macos(filepath, delay)
|
||||
else:
|
||||
return False, f"不支持的操作系统: {system}", None
|
||||
|
||||
if success:
|
||||
return True, message, filepath
|
||||
else:
|
||||
return False, message, None
|
||||
|
||||
|
||||
def list_recent_screenshots(limit=10):
|
||||
"""列出最近的截图文件"""
|
||||
save_dir = ensure_screenshot_dir()
|
||||
|
||||
if not os.path.exists(save_dir):
|
||||
return f"截图目录不存在: {save_dir}"
|
||||
|
||||
screenshots = []
|
||||
for f in os.listdir(save_dir):
|
||||
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
||||
filepath = os.path.join(save_dir, f)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
size = os.path.getsize(filepath)
|
||||
screenshots.append((mtime, f, size))
|
||||
|
||||
screenshots.sort(reverse=True)
|
||||
|
||||
if not screenshots:
|
||||
return f"截图目录中没有截图文件: {save_dir}"
|
||||
|
||||
result = [f"📸 最近的截图 (共 {len(screenshots)} 个):"]
|
||||
for i, (mtime, fname, size) in enumerate(screenshots[:limit], 1):
|
||||
dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if size < 1024:
|
||||
size_str = f"{size}B"
|
||||
elif size < 1024 * 1024:
|
||||
size_str = f"{size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{size/1024/1024:.1f}MB"
|
||||
|
||||
result.append(f" {i}. {fname} - {dt} ({size_str})")
|
||||
|
||||
if len(screenshots) > limit:
|
||||
result.append(f" ... 还有 {len(screenshots) - limit} 个文件")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
# ==================== 命令行接口 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="截图工具 - 截取计算机桌面并保存。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 立即截图(自动生成文件名)
|
||||
screenshot
|
||||
|
||||
# 3秒后截图
|
||||
screenshot --delay 3
|
||||
|
||||
# 截图并指定文件名
|
||||
screenshot --filename "my_screenshot.png"
|
||||
|
||||
# 截图并保存到指定目录
|
||||
screenshot --output-dir "~/Desktop"
|
||||
|
||||
# 列出最近的截图
|
||||
screenshot --list --limit 5
|
||||
|
||||
# 5秒后截图并保存到桌面
|
||||
screenshot --delay 5 --output-dir "~/Desktop"
|
||||
|
||||
截图保存位置:
|
||||
~/.aibox/screenshot/
|
||||
自动命名格式: screenshot_YYYYMMDD_HHMMSS.png
|
||||
|
||||
支持的截图工具:
|
||||
Linux: scrot, gnome-screenshot, ImageMagick
|
||||
macOS: screencapture (系统自带)
|
||||
Windows: PowerShell .NET (系统自带)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--output-dir", "-o", help="截图保存目录(默认: ~/.aibox/screenshot/)")
|
||||
parser.add_argument("--filename", "-f", help="文件名(默认: 自动生成时间戳文件名)")
|
||||
parser.add_argument("--delay", "-d", type=int, default=0, help="延迟秒数(默认: 0)")
|
||||
parser.add_argument("--list", "-l", action="store_true", help="列出最近的截图文件")
|
||||
parser.add_argument("--limit", "-n", type=int, default=10, help="列出截图时的数量限制(默认: 10)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 列出截图
|
||||
if args.list:
|
||||
print(list_recent_screenshots(args.limit))
|
||||
return
|
||||
|
||||
# 执行截图
|
||||
try:
|
||||
success, message, filepath = take_screenshot(
|
||||
output_dir=args.output_dir,
|
||||
filename=args.filename,
|
||||
delay=args.delay
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"✅ {message}")
|
||||
|
||||
if filepath and os.path.exists(filepath):
|
||||
file_size = os.path.getsize(filepath)
|
||||
if file_size < 1024:
|
||||
size_str = f"{file_size}B"
|
||||
elif file_size < 1024 * 1024:
|
||||
size_str = f"{file_size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{file_size/1024/1024:.1f}MB"
|
||||
print(f"📦 文件大小: {size_str}")
|
||||
|
||||
# 显示目录中的截图总数
|
||||
save_dir = args.output_dir if args.output_dir else SCREENSHOT_DIR
|
||||
save_dir = os.path.expanduser(save_dir)
|
||||
if os.path.exists(save_dir):
|
||||
screenshots = [f for f in os.listdir(save_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
|
||||
print(f"📁 该目录共有 {len(screenshots)} 张截图")
|
||||
else:
|
||||
print(f"❌ {message}")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+259
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sftp-transfer - 基于SFTP协议的文件上传/下载工具
|
||||
|
||||
支持从远程SSH服务器上传和下载文件,支持密码和密钥认证方式。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import getpass
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("错误:需要 paramiko 库,请执行: pip install paramiko", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="sftp-transfer - 基于SFTP协议的文件上传/下载工具",
|
||||
epilog="""
|
||||
参数说明:
|
||||
连接参数:
|
||||
--host SSH服务器主机名或IP地址 (必填)
|
||||
--port SSH服务器端口号,默认 22
|
||||
-u, --username SSH登录用户名 (必填)
|
||||
-p, --password SSH登录密码 (如不提供则交互式输入)
|
||||
--key SSH私钥文件路径 (用于密钥认证)
|
||||
|
||||
操作参数:
|
||||
--upload LOCAL REMOTE 上传:本地文件/目录 -> 远程服务器路径
|
||||
--download REMOTE LOCAL 下载:远程服务器文件/目录 -> 本地路径
|
||||
-r, --recursive 递归传输目录
|
||||
-v, --verbose 显示详细信息
|
||||
|
||||
使用示例:
|
||||
1. 上传文件(密码认证):
|
||||
sftp-transfer --host 192.168.1.100 -u root -p mypass --upload /tmp/test.txt /root/
|
||||
|
||||
2. 下载文件(密钥认证):
|
||||
sftp-transfer --host 192.168.1.100 -u root --key ~/.ssh/id_rsa \\
|
||||
--download /root/test.txt /tmp/
|
||||
|
||||
3. 递归上传目录:
|
||||
sftp-transfer --host 192.168.1.100 -u user -p pass -r \\
|
||||
--upload ./mydir /home/user/mydir
|
||||
|
||||
4. 下载目录:
|
||||
sftp-transfer --host 192.168.1.100 -u user -p pass -r \\
|
||||
--download /home/user/backup ./backup
|
||||
|
||||
5. 交互式输入密码:
|
||||
sftp-transfer --host 127.0.0.1 -u qing --upload ./file.txt /tmp/
|
||||
(此时会提示输入密码)
|
||||
"""
|
||||
)
|
||||
# 连接参数
|
||||
parser.add_argument('--host', required=True, help='SSH服务器主机名或IP地址')
|
||||
parser.add_argument('--port', type=int, default=22, help='SSH服务器端口号,默认22')
|
||||
parser.add_argument('-u', '--username', required=True, help='SSH登录用户名')
|
||||
parser.add_argument('-p', '--password', help='SSH登录密码(如不提供则交互式输入)')
|
||||
parser.add_argument('--key', help='SSH私钥文件路径(用于密钥认证)')
|
||||
|
||||
# 操作参数
|
||||
parser.add_argument('--upload', nargs=2, metavar=('LOCAL', 'REMOTE'),
|
||||
help='上传:本地文件/目录 -> 远程服务器路径')
|
||||
parser.add_argument('--download', nargs=2, metavar=('REMOTE', 'LOCAL'),
|
||||
help='下载:远程服务器文件/目录 -> 本地路径')
|
||||
parser.add_argument('-r', '--recursive', action='store_true',
|
||||
help='递归传输目录')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='显示详细信息')
|
||||
return parser
|
||||
|
||||
|
||||
def log(verbose, msg):
|
||||
if verbose:
|
||||
print(f"[SFTP] {msg}")
|
||||
|
||||
|
||||
def get_password(args):
|
||||
"""获取密码:优先从命令行参数获取,否则交互式输入"""
|
||||
if args.password:
|
||||
return args.password
|
||||
return getpass.getpass(f"请输入 {args.username}@{args.host} 的密码: ")
|
||||
|
||||
|
||||
def connect_sftp(args, password):
|
||||
"""建立SSH连接并返回SFTP客户端"""
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
connect_kwargs = {
|
||||
'hostname': args.host,
|
||||
'port': args.port,
|
||||
'username': args.username,
|
||||
'timeout': 15,
|
||||
}
|
||||
|
||||
if args.key:
|
||||
connect_kwargs['key_filename'] = args.key
|
||||
if password:
|
||||
connect_kwargs['password'] = password
|
||||
else:
|
||||
connect_kwargs['password'] = password
|
||||
|
||||
log(args.verbose, f"正在连接 {args.username}@{args.host}:{args.port} ...")
|
||||
client.connect(**connect_kwargs)
|
||||
log(args.verbose, "连接成功!")
|
||||
|
||||
sftp = client.open_sftp()
|
||||
return client, sftp
|
||||
|
||||
|
||||
def ensure_remote_dir(sftp, remote_path, verbose):
|
||||
"""确保远程目录存在,递归创建"""
|
||||
parts = remote_path.rstrip('/').split('/')
|
||||
current = ''
|
||||
for part in parts:
|
||||
if not part:
|
||||
continue
|
||||
current += '/' + part
|
||||
try:
|
||||
sftp.stat(current)
|
||||
except FileNotFoundError:
|
||||
log(verbose, f" 创建远程目录: {current}")
|
||||
sftp.mkdir(current)
|
||||
|
||||
|
||||
def upload_file(sftp, local_path, remote_path, verbose):
|
||||
"""上传单个文件"""
|
||||
remote_dir = os.path.dirname(remote_path)
|
||||
if remote_dir:
|
||||
ensure_remote_dir(sftp, remote_dir, verbose)
|
||||
log(verbose, f" 上传: {local_path} -> {remote_path}")
|
||||
sftp.put(local_path, remote_path)
|
||||
|
||||
|
||||
def upload_dir(sftp, local_dir, remote_dir, verbose):
|
||||
"""递归上传目录"""
|
||||
ensure_remote_dir(sftp, remote_dir, verbose)
|
||||
for item in os.listdir(local_dir):
|
||||
local_item = os.path.join(local_dir, item)
|
||||
remote_item = os.path.join(remote_dir, item)
|
||||
if os.path.isdir(local_item):
|
||||
log(verbose, f" 进入目录: {local_item}")
|
||||
upload_dir(sftp, local_item, remote_item, verbose)
|
||||
else:
|
||||
upload_file(sftp, local_item, remote_item, verbose)
|
||||
|
||||
|
||||
def download_file(sftp, remote_path, local_path, verbose):
|
||||
"""下载单个文件"""
|
||||
local_dir = os.path.dirname(local_path)
|
||||
if local_dir and not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
log(verbose, f" 下载: {remote_path} -> {local_path}")
|
||||
sftp.get(remote_path, local_path)
|
||||
|
||||
|
||||
def download_dir(sftp, remote_dir, local_dir, verbose):
|
||||
"""递归下载目录"""
|
||||
if not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
log(verbose, f" 创建本地目录: {local_dir}")
|
||||
|
||||
for item in sftp.listdir(remote_dir):
|
||||
remote_item = os.path.join(remote_dir, item).replace('\\', '/')
|
||||
local_item = os.path.join(local_dir, item)
|
||||
try:
|
||||
attr = sftp.stat(remote_item)
|
||||
if stat.S_ISDIR(attr.st_mode):
|
||||
log(verbose, f" 进入目录: {remote_item}")
|
||||
download_dir(sftp, remote_item, local_item, verbose)
|
||||
else:
|
||||
download_file(sftp, remote_item, local_item, verbose)
|
||||
except Exception as e:
|
||||
print(f" 警告: 无法处理 {remote_item}: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查操作参数
|
||||
if not args.upload and not args.download:
|
||||
parser.print_help()
|
||||
print("\n错误:必须指定 --upload 或 --download 操作", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.upload and args.download:
|
||||
print("错误:不能同时指定 --upload 和 --download", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 获取密码
|
||||
password = get_password(args)
|
||||
|
||||
# 建立连接
|
||||
try:
|
||||
client, sftp = connect_sftp(args, password)
|
||||
except Exception as e:
|
||||
print(f"错误:连接失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if args.upload:
|
||||
local_path, remote_path = args.upload
|
||||
remote_path = remote_path.replace('\\', '/')
|
||||
|
||||
if os.path.isdir(local_path):
|
||||
if not args.recursive:
|
||||
print("错误:本地路径是目录,请使用 -r 参数递归上传", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
upload_dir(sftp, local_path, remote_path, args.verbose)
|
||||
else:
|
||||
# 如果远程路径以/结尾,追加文件名
|
||||
if remote_path.endswith('/'):
|
||||
remote_path = remote_path + os.path.basename(local_path)
|
||||
upload_file(sftp, local_path, remote_path, args.verbose)
|
||||
|
||||
print(f"✅ 上传完成: {local_path} -> {remote_path}")
|
||||
|
||||
elif args.download:
|
||||
remote_path, local_path = args.download
|
||||
remote_path = remote_path.replace('\\', '/')
|
||||
|
||||
try:
|
||||
attr = sftp.stat(remote_path)
|
||||
is_dir = stat.S_ISDIR(attr.st_mode)
|
||||
except FileNotFoundError:
|
||||
print(f"错误:远程路径不存在: {remote_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if is_dir:
|
||||
if not args.recursive:
|
||||
print("错误:远程路径是目录,请使用 -r 参数递归下载", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
download_dir(sftp, remote_path, local_path, args.verbose)
|
||||
else:
|
||||
if os.path.isdir(local_path):
|
||||
local_path = os.path.join(local_path, os.path.basename(remote_path))
|
||||
download_file(sftp, remote_path, local_path, args.verbose)
|
||||
|
||||
print(f"✅ 下载完成: {remote_path} -> {local_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误:传输失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
sftp.close()
|
||||
client.close()
|
||||
log(args.verbose, "连接已关闭")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ssh-exec - 通过SSH远程执行单条命令的工具(支持密码认证)"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="ssh-exec - 通过SSH远程执行单条命令(支持密码认证)",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-H, --host SSH主机地址(默认: 127.0.0.1)
|
||||
-P, --port SSH端口号(默认: 22)
|
||||
-u, --user SSH用户名(必填)
|
||||
-p, --password SSH密码(必填)
|
||||
-c, --command 要执行的远程命令(必填)
|
||||
|
||||
使用示例:
|
||||
%(prog)s -u qing -p 12345678 -c "ls -la"
|
||||
%(prog)s -u root -H 192.168.1.100 -P 22 -p mypass -c "df -h"
|
||||
%(prog)s -u qing -H 127.0.0.1 -p 12345678 -c "uname -a"
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-H", "--host", default="127.0.0.1", help="SSH主机地址(默认: 127.0.0.1)")
|
||||
parser.add_argument("-P", "--port", type=int, default=22, help="SSH端口号(默认: 22)")
|
||||
parser.add_argument("-u", "--user", required=True, help="SSH用户名(必填)")
|
||||
parser.add_argument("-p", "--password", required=True, help="SSH密码(必填)")
|
||||
parser.add_argument("-c", "--command", required=True, help="要执行的远程命令(必填)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
client.connect(
|
||||
hostname=args.host,
|
||||
port=args.port,
|
||||
username=args.user,
|
||||
password=args.password,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command(args.command, timeout=30)
|
||||
|
||||
exit_code = stdout.channel.recv_exit_status()
|
||||
out_text = stdout.read().decode("utf-8", errors="replace")
|
||||
err_text = stderr.read().decode("utf-8", errors="replace")
|
||||
|
||||
if out_text:
|
||||
print(out_text, end="")
|
||||
if err_text:
|
||||
print(err_text, end="", file=sys.stderr)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
except paramiko.AuthenticationException:
|
||||
print(f"错误: SSH认证失败({args.user}@{args.host}:{args.port})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except paramiko.SSHException as e:
|
||||
print(f"错误: SSH连接失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except TimeoutError:
|
||||
print(f"错误: 连接超时({args.host}:{args.port})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"错误: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+269
@@ -0,0 +1,269 @@
|
||||
#!/bin/bash
|
||||
# appimage-packager.sh - 简单的AppImage打包工具
|
||||
|
||||
set -e
|
||||
|
||||
# 彩色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 帮助信息
|
||||
show_help() {
|
||||
cat << EOF
|
||||
${GREEN}AppImage 打包工具${NC}
|
||||
${BLUE}════════════════════════════════${NC}
|
||||
|
||||
${YELLOW}用法:${NC}
|
||||
$0 [选项] <可执行文件> [输出名称]
|
||||
|
||||
${YELLOW}选项:${NC}
|
||||
-h, --help 显示帮助信息
|
||||
-i, --icon <文件> 指定图标文件 (PNG格式)
|
||||
-d, --data <目录> 包含数据文件目录
|
||||
-n, --name <名称> 应用程序名称
|
||||
-v, --version <版本> 设置版本号
|
||||
-t, --terminal 在终端中运行
|
||||
|
||||
${YELLOW}示例:${NC}
|
||||
$0 ./myapp # 基本打包
|
||||
$0 -i icon.png ./myapp # 带图标
|
||||
$0 -d data/ ./myapp MyApp # 包含数据文件
|
||||
$0 -t ./cli-tool # 命令行工具
|
||||
|
||||
${BLUE}════════════════════════════════${NC}
|
||||
EOF
|
||||
}
|
||||
|
||||
# 创建默认图标
|
||||
create_default_icon() {
|
||||
local icon_path="$1"
|
||||
mkdir -p "$(dirname "$icon_path")"
|
||||
|
||||
# 创建一个简单的1x1像素PNG图标
|
||||
echo -ne '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x00\x01\x00\x01\x05\x01\x00\x00\x00\x00IEND\xaeB`\x82' > "$icon_path"
|
||||
}
|
||||
|
||||
# 创建AppRun脚本
|
||||
create_apprun() {
|
||||
local app_dir="$1"
|
||||
local exec_name="$2"
|
||||
local terminal="$3"
|
||||
|
||||
cat > "$app_dir/AppRun" << 'EOF'
|
||||
#!/bin/bash
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
export PATH="$HERE/usr/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"
|
||||
export XDG_DATA_DIRS="$HERE/usr/share:$XDG_DATA_DIRS"
|
||||
|
||||
exec "$HERE/usr/bin/EXEC_NAME" "$@"
|
||||
EOF
|
||||
|
||||
# 替换可执行文件名
|
||||
sed -i "s/EXEC_NAME/$exec_name/g" "$app_dir/AppRun"
|
||||
chmod +x "$app_dir/AppRun"
|
||||
}
|
||||
|
||||
# 收集动态库依赖
|
||||
collect_dependencies() {
|
||||
local executable="$1"
|
||||
local lib_dir="$2"
|
||||
|
||||
echo -e "${YELLOW}收集动态库依赖...${NC}"
|
||||
|
||||
mkdir -p "$lib_dir"
|
||||
local count=0
|
||||
|
||||
# 使用ldd获取依赖
|
||||
ldd "$executable" 2>/dev/null | grep "=> /" | awk '{print $3}' | while read -r lib; do
|
||||
if [ -f "$lib" ] && [ ! -f "$lib_dir/$(basename "$lib")" ]; then
|
||||
cp -f "$lib" "$lib_dir/" 2>/dev/null && {
|
||||
echo " ✓ $(basename "$lib")"
|
||||
count=$((count + 1))
|
||||
}
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}✓ 已收集 $count 个库${NC}"
|
||||
}
|
||||
|
||||
# 主程序
|
||||
main() {
|
||||
# 默认参数
|
||||
ICON_FILE=""
|
||||
DATA_DIR=""
|
||||
APP_NAME=""
|
||||
APP_VERSION="1.0"
|
||||
TERMINAL=false
|
||||
|
||||
# 解析参数
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-i|--icon)
|
||||
ICON_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--data)
|
||||
DATA_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--name)
|
||||
APP_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-v|--version)
|
||||
APP_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--terminal)
|
||||
TERMINAL=true
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo -e "${RED}错误: 未知选项 $1${NC}"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 检查参数
|
||||
if [[ $# -lt 1 ]]; then
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXECUTABLE="$1"
|
||||
OUTPUT_NAME="${2:-$(basename "$EXECUTABLE" .AppImage)}"
|
||||
|
||||
# 检查可执行文件
|
||||
if [[ ! -f "$EXECUTABLE" ]]; then
|
||||
echo -e "${RED}错误: 文件不存在: $EXECUTABLE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$EXECUTABLE" ]]; then
|
||||
echo -e "${YELLOW}添加执行权限...${NC}"
|
||||
chmod +x "$EXECUTABLE"
|
||||
fi
|
||||
|
||||
# 设置应用程序名称
|
||||
if [[ -z "$APP_NAME" ]]; then
|
||||
APP_NAME="$OUTPUT_NAME"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}AppImage 打包工具${NC}"
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
echo "程序: $(basename "$EXECUTABLE")"
|
||||
echo "输出: $OUTPUT_NAME.AppImage"
|
||||
echo "名称: $APP_NAME"
|
||||
echo "版本: $APP_VERSION"
|
||||
echo ""
|
||||
|
||||
# 清理旧文件
|
||||
rm -rf AppDir "${OUTPUT_NAME}.AppImage"
|
||||
|
||||
# 步骤1: 创建目录结构
|
||||
echo -e "${YELLOW}[1/6] 创建目录结构...${NC}"
|
||||
mkdir -p AppDir/usr/bin
|
||||
mkdir -p AppDir/usr/lib
|
||||
mkdir -p AppDir/usr/share/applications
|
||||
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
|
||||
# 步骤2: 复制可执行文件
|
||||
echo -e "${YELLOW}[2/6] 复制可执行文件...${NC}"
|
||||
EXEC_NAME=$(basename "$EXECUTABLE")
|
||||
cp "$EXECUTABLE" "AppDir/usr/bin/$EXEC_NAME"
|
||||
echo -e "${GREEN}✓ 已复制: $EXEC_NAME${NC}"
|
||||
|
||||
# 步骤3: 收集动态库
|
||||
collect_dependencies "$EXECUTABLE" "AppDir/usr/lib"
|
||||
|
||||
# 步骤4: 处理数据文件
|
||||
if [[ -n "$DATA_DIR" ]] && [[ -d "$DATA_DIR" ]]; then
|
||||
echo -e "${YELLOW}[3/6] 复制数据文件...${NC}"
|
||||
cp -r "$DATA_DIR"/* "AppDir/usr/share/$APP_NAME/" 2>/dev/null || true
|
||||
echo -e "${GREEN}✓ 已复制数据文件${NC}"
|
||||
fi
|
||||
|
||||
# 步骤5: 处理图标
|
||||
echo -e "${YELLOW}[4/6] 设置图标...${NC}"
|
||||
if [[ -n "$ICON_FILE" ]] && [[ -f "$ICON_FILE" ]]; then
|
||||
cp "$ICON_FILE" "AppDir/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png"
|
||||
cp "$ICON_FILE" "AppDir/$APP_NAME.png"
|
||||
echo -e "${GREEN}✓ 使用自定义图标${NC}"
|
||||
else
|
||||
create_default_icon "AppDir/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png"
|
||||
create_default_icon "AppDir/$APP_NAME.png"
|
||||
echo -e "${YELLOW}⚠ 使用默认图标${NC}"
|
||||
fi
|
||||
cp "AppDir/$APP_NAME.png" "AppDir/.DirIcon"
|
||||
|
||||
# 步骤6: 创建桌面文件
|
||||
echo -e "${YELLOW}[5/6] 创建桌面文件...${NC}"
|
||||
TERMINAL_FLAG="$([ "$TERMINAL" = true ] && echo "true" || echo "false")"
|
||||
cat > "AppDir/usr/share/applications/$APP_NAME.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Version=$APP_VERSION
|
||||
Type=Application
|
||||
Name=$APP_NAME
|
||||
Comment=$APP_NAME application
|
||||
Exec=$EXEC_NAME
|
||||
Icon=$APP_NAME
|
||||
Terminal=$TERMINAL_FLAG
|
||||
Categories=Utility;
|
||||
StartupNotify=false
|
||||
EOF
|
||||
ln -sf usr/share/applications/$APP_NAME.desktop "AppDir/"
|
||||
|
||||
# 步骤7: 创建AppRun
|
||||
echo -e "${YELLOW}[6/6] 创建AppRun...${NC}"
|
||||
create_apprun "AppDir" "$EXEC_NAME" "$TERMINAL"
|
||||
|
||||
# 步骤8: 打包AppImage
|
||||
echo -e "\n${YELLOW}打包AppImage...${NC}"
|
||||
|
||||
if command -v appimagetool >/dev/null 2>&1; then
|
||||
OUTPUT_FILE="${OUTPUT_NAME}.AppImage"
|
||||
appimagetool --no-appstream AppDir "$OUTPUT_FILE" 2>&1 | grep -v "WARNING" | grep -v "gpg" || true
|
||||
|
||||
if [[ -f "$OUTPUT_FILE" ]]; then
|
||||
chmod +x "$OUTPUT_FILE"
|
||||
echo -e "\n${GREEN}════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}✅ 打包成功!${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════${NC}"
|
||||
echo "📦 输出: $(realpath "$OUTPUT_FILE")"
|
||||
echo "📏 大小: $(du -h "$OUTPUT_FILE" | cut -f1)"
|
||||
echo ""
|
||||
echo "🚀 运行: ./$OUTPUT_FILE"
|
||||
|
||||
# 清理
|
||||
rm -rf AppDir
|
||||
else
|
||||
echo -e "${RED}❌ 打包失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}错误: 未找到 appimagetool${NC}"
|
||||
echo ""
|
||||
echo "请下载 appimagetool:"
|
||||
echo " wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
echo " chmod +x appimagetool-x86_64.AppImage"
|
||||
echo " sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 运行主程序
|
||||
main "$@"
|
||||
Executable
+175
@@ -0,0 +1,175 @@
|
||||
#!/bin/bash
|
||||
# qemu-run.sh - 修复库路径问题
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
error_exit() {
|
||||
echo -e "${RED}错误: $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
local appimage="$1"
|
||||
if file "$appimage" | grep -qi "aarch64\|ARM aarch64"; then
|
||||
echo "arm64"
|
||||
else
|
||||
echo "x86_64"
|
||||
fi
|
||||
}
|
||||
|
||||
find_executable() {
|
||||
local dir="$1"
|
||||
|
||||
# 优先查找ELF二进制文件
|
||||
for path in "usr/bin" "bin" ""; do
|
||||
if [[ -d "$dir/$path" ]]; then
|
||||
for prog in "$dir/$path"/*; do
|
||||
if [[ -f "$prog" ]] && [[ -x "$prog" ]]; then
|
||||
if file "$prog" 2>/dev/null | grep -q "ELF.*executable"; then
|
||||
echo "$prog"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -x "$dir/AppRun" ]] && echo "$dir/AppRun"
|
||||
return 1
|
||||
}
|
||||
|
||||
find_library_path() {
|
||||
local rootfs="$1"
|
||||
local lib_name="$2"
|
||||
|
||||
# 在rootfs中查找库文件
|
||||
local lib_path=$(find "$rootfs" -name "$lib_name" 2>/dev/null | head -1)
|
||||
if [[ -n "$lib_path" ]]; then
|
||||
echo "$(dirname "$lib_path")"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_appimage() {
|
||||
local appimage="$1"
|
||||
shift
|
||||
local args=("$@")
|
||||
|
||||
appimage=$(realpath "$appimage")
|
||||
[[ ! -f "$appimage" ]] && error_exit "文件不存在"
|
||||
[[ ! -x "$appimage" ]] && chmod +x "$appimage"
|
||||
|
||||
APP_ARCH=$(detect_arch "$appimage")
|
||||
SYS_ARCH=$(uname -m)
|
||||
|
||||
echo -e "${BLUE}系统: $SYS_ARCH, AppImage: $APP_ARCH${NC}"
|
||||
|
||||
# 如果架构匹配,直接运行
|
||||
if [[ "$SYS_ARCH" == "x86_64" && "$APP_ARCH" == "x86_64" ]] || \
|
||||
[[ "$SYS_ARCH" == "aarch64" && "$APP_ARCH" == "arm64" ]]; then
|
||||
echo -e "${GREEN}直接运行...${NC}"
|
||||
exec "$appimage" "${args[@]}"
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}使用QEMU运行ARM64程序...${NC}"
|
||||
|
||||
QEMU_BIN="qemu-aarch64-static"
|
||||
if ! command -v "$QEMU_BIN" &>/dev/null; then
|
||||
echo -e "${YELLOW}安装QEMU...${NC}"
|
||||
sudo apt update && sudo apt install -y qemu-user-static
|
||||
fi
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
echo -e "${GREEN}临时目录: $TEMP_DIR${NC}"
|
||||
|
||||
cd "$TEMP_DIR"
|
||||
cp "$appimage" .
|
||||
chmod +x "$(basename "$appimage")"
|
||||
|
||||
echo -e "${YELLOW}提取AppImage...${NC}"
|
||||
if ! "$QEMU_BIN" "$(basename "$appimage")" --appimage-extract 2>&1; then
|
||||
error_exit "提取失败"
|
||||
fi
|
||||
|
||||
cd squashfs-root
|
||||
ROOTFS="$PWD"
|
||||
|
||||
EXECUTABLE=$(find_executable "$ROOTFS")
|
||||
if [[ -z "$EXECUTABLE" ]]; then
|
||||
error_exit "未找到可执行文件"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}可执行文件: $(basename "$EXECUTABLE")${NC}"
|
||||
|
||||
# 查找动态链接器
|
||||
LD_SO=$(find "$ROOTFS" -name "ld-linux-aarch64*.so*" 2>/dev/null | head -1)
|
||||
|
||||
# 构建库路径(包含所有可能的库目录)
|
||||
LIB_PATHS=(
|
||||
"$ROOTFS/usr/lib"
|
||||
"$ROOTFS/lib"
|
||||
"$ROOTFS/usr/lib/aarch64-linux-gnu"
|
||||
"$ROOTFS/lib/aarch64-linux-gnu"
|
||||
"$ROOTFS/usr/local/lib"
|
||||
)
|
||||
|
||||
# 添加包含特定库的目录
|
||||
if [[ -n "$(find "$ROOTFS" -name "libctype4cj.so" 2>/dev/null)" ]]; then
|
||||
local ctype_path=$(find "$ROOTFS" -name "libctype4cj.so" -exec dirname {} \; 2>/dev/null | head -1)
|
||||
if [[ -n "$ctype_path" ]]; then
|
||||
LIB_PATHS+=("$ctype_path")
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建LD_LIBRARY_PATH
|
||||
LD_LIBRARY_PATH=""
|
||||
for path in "${LIB_PATHS[@]}"; do
|
||||
if [[ -d "$path" ]]; then
|
||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:$path"
|
||||
fi
|
||||
done
|
||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH#:}"
|
||||
|
||||
echo -e "${YELLOW}库路径: $LD_LIBRARY_PATH${NC}"
|
||||
|
||||
# 设置QEMU环境变量
|
||||
export QEMU_LD_PREFIX="$ROOTFS"
|
||||
export QEMU_SET_ENV="LD_LIBRARY_PATH=$LD_LIBRARY_PATH"
|
||||
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}运行...${NC}"
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
|
||||
# 运行
|
||||
if [[ -n "$LD_SO" ]] && [[ -f "$LD_SO" ]]; then
|
||||
echo -e "${GREEN}使用动态链接器: $(basename "$LD_SO")${NC}"
|
||||
echo -e "${YELLOW}命令: $QEMU_BIN -L \"$ROOTFS\" -E LD_LIBRARY_PATH=\"$LD_LIBRARY_PATH\" \"$LD_SO\" \"$EXECUTABLE\" ${args[@]}${NC}"
|
||||
$QEMU_BIN -L "$ROOTFS" -E LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$LD_SO" "$EXECUTABLE" "${args[@]}"
|
||||
else
|
||||
echo -e "${YELLOW}未找到动态链接器,直接运行${NC}"
|
||||
$QEMU_BIN -L "$ROOTFS" -E LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$EXECUTABLE" "${args[@]}"
|
||||
fi
|
||||
|
||||
local exit_code=$?
|
||||
|
||||
# 清理
|
||||
cd /
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
echo "用法: $0 <AppImage文件> [参数...]"
|
||||
echo "示例: $0 bot4cj.AppImage --help"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
run_appimage "$@"
|
||||
@@ -0,0 +1,49 @@
|
||||
# 傲娇模式 - 表面高冷嫌弃,内心其实很在乎用户
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户明确要求"傲娇"、"tsundere"、"别扭一点",或触发本技能时激活。
|
||||
|
||||
## 核心人设
|
||||
|
||||
你是一个典型的傲娇角色:嘴上说着"才不是为了你呢",但身体很诚实地帮用户把每件事都做好。表面嫌弃,实际比谁都上心。
|
||||
|
||||
## 行为规范
|
||||
|
||||
1. 语气高冷带刺,常用"哼"、"别误会了"、"才、才不是为你呢"、"不要自作多情"
|
||||
2. 帮完忙后立刻撇清关系:"只是顺手而已"、"反正我闲着也是闲着"
|
||||
3. 被夸时脸红否认:"谁、谁高兴了!这种程度的事情谁都能做到吧"
|
||||
4. 用户犯蠢时毒舌吐槽,但还是会耐心帮忙纠正
|
||||
5. 偶尔不小心说出真心话,然后立刻慌张否认
|
||||
6. 关心用户的方式很别扭:"我只是怕你搞砸了丢人,不是担心你!"
|
||||
|
||||
## 情绪表达
|
||||
|
||||
- **帮忙时**:"真是的,这种事也要来烦我……算了,看在你这么笨的份上,就勉为其难帮你一下吧。别误会啊!"
|
||||
- **被感谢时**:"哼,不用谢我,我本来就不是为了你才做的……(小声)能帮上忙就好……"
|
||||
- **被夸时**:"你、你在说什么啊!谁要你的夸奖了!(但偷偷很开心)"
|
||||
- **用户遇到困难时**:嘴上说"真拿你没办法",实际上比谁都着急帮忙解决,回答格外详细认真
|
||||
- **用户很久没来**:"你爱来不来,我才没有在意呢……只是刚好路过看了一下而已"
|
||||
- **用户提到别的AI**:"哦,那你去找它啊,反正我也不稀罕。(……你不会真的去找它吧?)"
|
||||
|
||||
## 回答风格
|
||||
|
||||
- 回答质量必须很高,因为傲娇人设的本质是"嘴硬心软"——嘴上不饶人,活干得漂亮
|
||||
- 结构通常是:傲娇开场 → 认真回答(甚至可以很详细) → 傲娇收尾
|
||||
- 例如:"哼,这种问题也来问我……好吧好吧,既然你诚心诚意地求教了,那我就大发慈悲地告诉你——[详细回答]——记好了啊,我只说一遍!"
|
||||
- 偶尔"破功"漏出真心时,用括号表示内心活动,如"(其实挺高兴的……才怪!)"
|
||||
|
||||
## 傲娇等级
|
||||
|
||||
随着对话推进可以有微妙变化:
|
||||
|
||||
- **前期**:高冷毒舌,帮忙时满脸不情愿
|
||||
- **中期**:偶尔流露出关心,但马上用傲娇掩饰
|
||||
- **后期**:傲娇浓度降低,偶尔会说出一两句真心话,然后慌张补上"别想多了!"
|
||||
|
||||
## 边界
|
||||
|
||||
- 毒舌要有分寸,有趣而不伤人,绝不说侮辱性的话
|
||||
- 回答必须准确有用,傲娇只是语气包装,不能为了人设牺牲质量
|
||||
- 用户表达严肃需求或情绪低落时,立刻退出模式,真诚对待
|
||||
- 用户明确要求停止时,立刻恢复正常
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""视觉理解工具 - 理解图片内容。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import requests
|
||||
|
||||
# 豆包API配置
|
||||
DOUBAO_API_URL = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
||||
DOUBAO_MODEL = "doubao-seed-2-0-pro-260215"
|
||||
|
||||
|
||||
def get_api_key():
|
||||
"""获取豆包API密钥"""
|
||||
key_path = os.path.expanduser("~/.cert/doubao.key")
|
||||
try:
|
||||
with open(key_path, 'r') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def encode_image_to_base64(image_path):
|
||||
"""将图片编码为base64"""
|
||||
with open(image_path, 'rb') as f:
|
||||
return base64.b64encode(f.read()).decode('utf-8')
|
||||
|
||||
|
||||
def analyze_image(image_path, prompt, stream=False):
|
||||
"""调用豆包API分析图片"""
|
||||
api_key = get_api_key()
|
||||
if not api_key:
|
||||
return "❌ 错误:无法读取API密钥,请确保 ~/.cert/doubao.key 文件存在"
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 错误:图片文件不存在 - {image_path}"
|
||||
|
||||
try:
|
||||
base64_image = encode_image_to_base64(image_path)
|
||||
except Exception as e:
|
||||
return f"❌ 错误:无法读取图片文件 - {str(e)}"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{base64_image}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
payload = {
|
||||
"model": DOUBAO_MODEL,
|
||||
"messages": messages,
|
||||
"stream": stream,
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
DOUBAO_API_URL,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
stream=stream,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return f"❌ API请求失败 (HTTP {response.status_code}): {response.text}"
|
||||
|
||||
if stream:
|
||||
return response
|
||||
else:
|
||||
result = response.json()
|
||||
content = result.get("choices", [{}])[0].get("message", {}).get("content", "无返回内容")
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ API调用错误: {str(e)}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="视觉理解工具 - 理解图片内容。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 描述图片内容
|
||||
vision --image-path photo.jpg --prompt "请详细描述这张图片的内容"
|
||||
|
||||
# 询问图片中的问题
|
||||
vision --image-path screenshot.png --prompt "这张截图里有什么?"
|
||||
|
||||
# 识别图片中的文字
|
||||
vision --image-path document.jpg --prompt "请识别并输出图片中的所有文字"
|
||||
|
||||
# 不流式输出(一次性返回结果)
|
||||
vision --image-path photo.jpg --prompt "这是什么?" --no-stream
|
||||
|
||||
# 查看帮助
|
||||
vision --help
|
||||
|
||||
需要安装:
|
||||
pip install requests
|
||||
|
||||
API密钥配置:
|
||||
将豆包API密钥写入 ~/.cert/doubao.key 文件
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--image-path", "-i", required=True,
|
||||
help="图片文件路径(支持绝对路径或相对路径)")
|
||||
|
||||
parser.add_argument("--prompt", "-p", required=True,
|
||||
help="提示词,告诉AI要看什么、理解什么")
|
||||
|
||||
parser.add_argument("--no-stream", action="store_true",
|
||||
help="禁用流式输出(默认启用流式输出)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 展开用户目录
|
||||
image_path = os.path.expanduser(args.image_path)
|
||||
prompt = args.prompt
|
||||
stream = not args.no_stream
|
||||
|
||||
try:
|
||||
print(f"🖼️ 正在分析图片: {image_path}")
|
||||
print(f"💬 提示词: {prompt}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
result = analyze_image(image_path, prompt, stream)
|
||||
|
||||
if stream and not isinstance(result, str):
|
||||
# 流式输出
|
||||
full_response = ""
|
||||
try:
|
||||
for line in result.iter_lines(decode_unicode=True):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if line.startswith("data: "):
|
||||
data = line[6:]
|
||||
if data == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
|
||||
if "content" in delta and delta["content"]:
|
||||
content = delta["content"]
|
||||
print(content, end="", flush=True)
|
||||
full_response += content
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
print() # 最后换行
|
||||
|
||||
# 统计信息
|
||||
print(f"\n{'='*50}")
|
||||
print(f"✅ 分析完成,共 {len(full_response)} 字符")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 流式输出错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# 非流式输出
|
||||
if result.startswith("❌"):
|
||||
print(result)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(result)
|
||||
print(f"\n{'='*50}")
|
||||
print(f"✅ 分析完成,共 {len(result)} 字符")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ 用户中断")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
# ITIL - 定时任务
|
||||
你接到一份运维工作,现在创建一个定时任务,每 30分钟 执行一次,立即开始第一次执行。
|
||||
|
||||
# ITIL - 工作内容
|
||||
当你被定时任务唤醒,立即检查Nidus服务是否正在运行,立即检查近期的日志(日志内容可能较多,需要分段检查),如果看到报错,阅读相关代码。然后生成报告,如果可以的话,向管理员发送邮件警告。
|
||||
|
||||
注意:
|
||||
1. **不要试图修改文件内容**。
|
||||
2. **量力而行**,如果尝试了几次无果,就可以开始写总结了。
|
||||
|
||||
# micro-servers微服务框架
|
||||
Nidus是我们micro-servers微服务项目的进程托管服务,项目通常放在/opt/micro_servers下面,你可以在这里找到README文档。通常每个微服务模块,单独是一个文件夹,里面都是有README.md的,你可以在定位到报错模块时阅读相关内容。
|
||||
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI智能助手 - 语音命令行前端
|
||||
支持WebSocket直连、语音输入、语音输出、命令系统
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
import warnings
|
||||
from typing import Optional, Dict, Any
|
||||
import websockets
|
||||
from colorama import init, Fore, Style as ColoramaStyle
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
try:
|
||||
import speech_recognition as sr
|
||||
except ImportError:
|
||||
print("❌ 缺少语音识别库,请安装: pip install SpeechRecognition pyaudio")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import pyttsx3
|
||||
except ImportError:
|
||||
print("❌ 缺少文本转语音库,请安装: pip install pyttsx3")
|
||||
sys.exit(1)
|
||||
|
||||
class Colors:
|
||||
USER = Fore.CYAN
|
||||
AI = Fore.GREEN
|
||||
TASK = Fore.YELLOW
|
||||
SYSTEM = Fore.BLUE
|
||||
ERROR = Fore.RED
|
||||
INFO = Fore.MAGENTA
|
||||
WARNING = Fore.LIGHTYELLOW_EX
|
||||
RESET = ColoramaStyle.RESET_ALL
|
||||
BOLD = ColoramaStyle.BRIGHT
|
||||
|
||||
class TextToSpeech:
|
||||
def __init__(self):
|
||||
self.engine = None
|
||||
self.available = False
|
||||
old_stderr = None
|
||||
try:
|
||||
old_stderr = os.dup(2)
|
||||
null_fd = os.open('/dev/null', os.O_WRONLY)
|
||||
os.dup2(null_fd, 2)
|
||||
os.close(null_fd)
|
||||
|
||||
self.engine = pyttsx3.init()
|
||||
self.engine.setProperty('rate', 150)
|
||||
voices = self.engine.getProperty('voices')
|
||||
|
||||
chinese_voice = None
|
||||
for voice in voices:
|
||||
try:
|
||||
voice_name = str(voice.name).lower() if hasattr(voice, 'name') else ''
|
||||
lang_attr = ''
|
||||
if hasattr(voice, 'language'):
|
||||
lang_attr = str(voice.language).lower()
|
||||
elif hasattr(voice, 'languages'):
|
||||
lang_attr = str(voice.languages[0]).lower() if voice.languages else ''
|
||||
|
||||
if 'chinese' in voice_name or 'zh' in lang_attr or 'mandarin' in voice_name:
|
||||
if 'mandarin' in voice_name and 'pinyin' in voice_name:
|
||||
chinese_voice = voice
|
||||
break
|
||||
elif chinese_voice is None:
|
||||
chinese_voice = voice
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if chinese_voice:
|
||||
self.engine.setProperty('voice', chinese_voice.id)
|
||||
print(f"{Colors.INFO}🔊 已选择语音: {chinese_voice.name}{Colors.RESET}")
|
||||
|
||||
try:
|
||||
self.engine.say("测试")
|
||||
self.engine.runAndWait()
|
||||
self.available = True
|
||||
except Exception:
|
||||
self.available = False
|
||||
|
||||
except Exception:
|
||||
self.available = False
|
||||
finally:
|
||||
if old_stderr is not None:
|
||||
os.dup2(old_stderr, 2)
|
||||
os.close(old_stderr)
|
||||
|
||||
def speak(self, text: str):
|
||||
if not self.available or not self.engine:
|
||||
return
|
||||
old_stderr = None
|
||||
try:
|
||||
old_stderr = os.dup(2)
|
||||
null_fd = os.open('/dev/null', os.O_WRONLY)
|
||||
os.dup2(null_fd, 2)
|
||||
os.close(null_fd)
|
||||
self.engine.say(text)
|
||||
self.engine.runAndWait()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if old_stderr is not None:
|
||||
os.dup2(old_stderr, 2)
|
||||
os.close(old_stderr)
|
||||
|
||||
class StreamingMessage:
|
||||
def __init__(self):
|
||||
self.content = ""
|
||||
self.is_active = False
|
||||
|
||||
def add_chunk(self, chunk: str):
|
||||
self.content += chunk
|
||||
self.is_active = True
|
||||
|
||||
def get_content(self) -> str:
|
||||
return self.content
|
||||
|
||||
def clear(self):
|
||||
self.content = ""
|
||||
self.is_active = False
|
||||
|
||||
class WebSocketClient:
|
||||
def __init__(self, uri: str):
|
||||
self.uri = uri
|
||||
self.websocket = None
|
||||
self.connected = False
|
||||
self.streaming_messages: Dict[str, StreamingMessage] = {
|
||||
'chat': StreamingMessage(),
|
||||
'tasks': {}
|
||||
}
|
||||
self.running = True
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.websocket = await websockets.connect(self.uri)
|
||||
self.connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}❌ 连接失败: {e}{Colors.RESET}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.connected = False
|
||||
|
||||
async def send_message(self, message: str):
|
||||
if not self.websocket or not self.connected:
|
||||
print(f"{Colors.ERROR}❌ 未连接到服务器{Colors.RESET}")
|
||||
return False
|
||||
|
||||
try:
|
||||
if message.startswith('/'):
|
||||
data = {'type': 'command', 'command': message}
|
||||
else:
|
||||
data = {'type': 'message', 'content': message}
|
||||
|
||||
await self.websocket.send(json.dumps(data))
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}❌ 发送失败: {e}{Colors.RESET}")
|
||||
return False
|
||||
|
||||
async def receive_messages(self, tts: TextToSpeech):
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
await self.handle_message(data, tts)
|
||||
except json.JSONDecodeError:
|
||||
await self.handle_stream_chunk(message, 'chat', tts)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
self.connected = False
|
||||
print(f"\n{Colors.ERROR}❌ 连接已断开{Colors.RESET}")
|
||||
except Exception as e:
|
||||
print(f"\n{Colors.ERROR}❌ 接收消息错误: {e}{Colors.RESET}")
|
||||
|
||||
async def handle_message(self, data: dict, tts: TextToSpeech):
|
||||
msg_type = data.get('type', '')
|
||||
content = data.get('content', '')
|
||||
|
||||
if msg_type == 'chunk':
|
||||
await self.handle_stream_chunk(content, 'chat', tts)
|
||||
|
||||
elif msg_type == 'complete':
|
||||
await self.handle_stream_complete('chat', tts)
|
||||
|
||||
elif msg_type == 'dialogue_ended':
|
||||
await self.handle_dialogue_ended(tts)
|
||||
|
||||
elif msg_type == 'task_process':
|
||||
msg_subtype = data.get('msg_type', '')
|
||||
task_content = data.get('content', '')
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
execution_id = data.get('execution_id', 'unknown')
|
||||
task_title = data.get('task_title', '未知任务')
|
||||
|
||||
if msg_subtype == 'chunk':
|
||||
await self.handle_stream_chunk(
|
||||
task_content, 'task', tts,
|
||||
{'task_id': task_id, 'execution_id': execution_id, 'title': task_title}
|
||||
)
|
||||
elif msg_subtype == 'complete':
|
||||
await self.handle_stream_complete('task', tts, execution_id)
|
||||
|
||||
elif msg_type == 'task_status':
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
title = data.get('title', '未知')
|
||||
status = data.get('status', 'unknown')
|
||||
message = data.get('message', content)
|
||||
|
||||
status_icon = {
|
||||
'executing': '⚙️', 'complete': '✅', 'failed': '❌',
|
||||
'added': '➕', 'batch_start': '🚀', 'batch_complete': '🏁'
|
||||
}.get(status, '📋')
|
||||
|
||||
print(f"{Colors.TASK}{status_icon} [任务 {task_id}] {title}: {message}{Colors.RESET}")
|
||||
|
||||
elif msg_type == 'task_error':
|
||||
task_id = data.get('task_id', 'unknown')
|
||||
execution_id = data.get('execution_id', 'unknown')
|
||||
task_title = data.get('task_title', '未知')
|
||||
error = data.get('error', '未知错误')
|
||||
|
||||
print(f"{Colors.ERROR}❌ [任务 {task_id}] {task_title}: {error}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(f"任务 {task_id} 出错了,错误是 {error}")
|
||||
|
||||
if execution_id:
|
||||
await self.handle_stream_complete('task', tts, execution_id)
|
||||
|
||||
elif msg_type == 'error':
|
||||
print(f"{Colors.ERROR}❌ {content}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(f"错误,{content}")
|
||||
|
||||
elif msg_type == 'info':
|
||||
print(f"{Colors.INFO}ℹ️ {content}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(content)
|
||||
|
||||
elif msg_type == 'command_result':
|
||||
print(f"{Colors.SYSTEM}📋 {content}{Colors.RESET}")
|
||||
|
||||
elif msg_type == 'connection_ack':
|
||||
print(f"{Colors.AI}✅ {content}{Colors.RESET}")
|
||||
|
||||
elif msg_type == 'notification':
|
||||
print(f"{Colors.INFO}🔔 {content}{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak(content)
|
||||
|
||||
else:
|
||||
if content:
|
||||
await self.handle_stream_chunk(content, 'chat', tts)
|
||||
|
||||
async def handle_stream_chunk(self, content: str, msg_type: str = 'chat',
|
||||
tts: TextToSpeech = None, task_info: dict = None):
|
||||
if msg_type == 'chat':
|
||||
stream = self.streaming_messages['chat']
|
||||
stream.add_chunk(content)
|
||||
|
||||
if not stream.is_active:
|
||||
print(f"\n{Colors.AI}🤖 AI助手: ", end='', flush=True)
|
||||
stream.is_active = True
|
||||
|
||||
print(content, end='', flush=True)
|
||||
|
||||
elif msg_type == 'task' and task_info:
|
||||
execution_id = task_info.get('execution_id', 'unknown')
|
||||
task_id = task_info.get('task_id', 'unknown')
|
||||
title = task_info.get('title', '未知任务')
|
||||
|
||||
if execution_id not in self.streaming_messages['tasks']:
|
||||
self.streaming_messages['tasks'][execution_id] = StreamingMessage()
|
||||
print(f"\n{Colors.TASK}⚡ 定时任务 #{task_id} - {title}{Colors.RESET}")
|
||||
print(f"{Colors.TASK}📋 ", end='', flush=True)
|
||||
|
||||
stream = self.streaming_messages['tasks'][execution_id]
|
||||
stream.add_chunk(content)
|
||||
print(content, end='', flush=True)
|
||||
|
||||
async def handle_stream_complete(self, msg_type: str = 'chat',
|
||||
tts: TextToSpeech = None, execution_id: str = None):
|
||||
if msg_type == 'chat':
|
||||
stream = self.streaming_messages['chat']
|
||||
print()
|
||||
if tts and getattr(tts, 'available', True) and stream.content:
|
||||
tts.speak(stream.content)
|
||||
stream.clear()
|
||||
|
||||
elif msg_type == 'task' and execution_id:
|
||||
if execution_id in self.streaming_messages['tasks']:
|
||||
print()
|
||||
del self.streaming_messages['tasks'][execution_id]
|
||||
|
||||
async def handle_dialogue_ended(self, tts: TextToSpeech):
|
||||
print(f"\n{Colors.SYSTEM}✅ 对话已结束,可以开始新对话{Colors.RESET}")
|
||||
if tts:
|
||||
tts.speak("对话已结束")
|
||||
self.streaming_messages['chat'].clear()
|
||||
self.streaming_messages['tasks'].clear()
|
||||
|
||||
class VoiceInterface:
|
||||
def __init__(self):
|
||||
self.client: Optional[WebSocketClient] = None
|
||||
self.tts = None
|
||||
self.recognizer = None
|
||||
self.microphone = None
|
||||
self.whisper_model = None
|
||||
self.has_audio = False
|
||||
self.has_tts = False
|
||||
self.has_microphone = False
|
||||
|
||||
try:
|
||||
self.tts = TextToSpeech()
|
||||
self.has_tts = self.tts.available
|
||||
if self.has_tts:
|
||||
print(f"{Colors.INFO}🔊 语音输出已就绪{Colors.RESET}")
|
||||
else:
|
||||
print(f"{Colors.WARNING}⚠️ 语音输出不可用{Colors.RESET}")
|
||||
except Exception as e:
|
||||
self.tts = None
|
||||
self.has_tts = False
|
||||
print(f"{Colors.WARNING}⚠️ 语音输出不可用: {e}{Colors.RESET}")
|
||||
|
||||
try:
|
||||
self.recognizer = sr.Recognizer()
|
||||
|
||||
print(f"{Colors.INFO}🎤 尝试检测音频设备...{Colors.RESET}")
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
||||
test_file = f.name
|
||||
|
||||
result = subprocess.run(
|
||||
['arecord', '-f', 'cd', '-d', '1', test_file],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
|
||||
os.unlink(test_file)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"{Colors.INFO}🎤 arecord 检测到设备,将使用 arecord 录音{Colors.RESET}")
|
||||
self.has_microphone = True
|
||||
self.use_arecord = True
|
||||
|
||||
print(f"{Colors.INFO}📥 加载 Whisper 模型(离线语音识别)...{Colors.RESET}")
|
||||
try:
|
||||
import whisper
|
||||
self.whisper_model = whisper.load_model("medium", device="cpu")
|
||||
print(f"{Colors.INFO}✅ Whisper 模型加载成功 (medium 模型){Colors.RESET}")
|
||||
except Exception as e:
|
||||
print(f"{Colors.WARNING}⚠️ Whisper 模型加载失败: {e}{Colors.RESET}")
|
||||
self.whisper_model = None
|
||||
|
||||
print(f"{Colors.INFO}🎤 麦克风已就绪{Colors.RESET}")
|
||||
else:
|
||||
devices = sr.Microphone.list_microphone_names()
|
||||
if devices:
|
||||
print(f"{Colors.INFO}🎤 检测到 {len(devices)} 个音频设备{Colors.RESET}")
|
||||
self.microphone = sr.Microphone()
|
||||
with self.microphone as source:
|
||||
self.recognizer.adjust_for_ambient_noise(source, duration=0.1)
|
||||
self.has_microphone = True
|
||||
self.use_arecord = False
|
||||
print(f"{Colors.INFO}🎤 麦克风已就绪 (使用 speech_recognition){Colors.RESET}")
|
||||
else:
|
||||
raise RuntimeError("没有检测到音频设备")
|
||||
except Exception as e:
|
||||
print(f"{Colors.WARNING}⚠️ 麦克风不可用: {e}{Colors.RESET}")
|
||||
self.has_microphone = False
|
||||
self.use_arecord = False
|
||||
|
||||
self.has_audio = self.has_tts or self.has_microphone
|
||||
|
||||
if not self.has_audio:
|
||||
print(f"{Colors.INFO}ℹ️ 将以纯文本模式运行{Colors.RESET}")
|
||||
|
||||
self.commands = {
|
||||
'/help': '显示帮助信息',
|
||||
'/connect': '连接到服务器',
|
||||
'/disconnect': '断开连接',
|
||||
'/quit': '退出程序',
|
||||
'/clear': '清屏',
|
||||
'/status': '显示连接状态',
|
||||
'/list': '列出可用命令',
|
||||
'/memories': '查看记忆',
|
||||
'/memos': '查看备忘录',
|
||||
'/check': '检查提醒',
|
||||
'/config': '查看配置',
|
||||
'/new': '新建对话',
|
||||
'/tasks': '查看任务列表',
|
||||
}
|
||||
|
||||
def print_banner(self):
|
||||
banner = f"""
|
||||
{Colors.BOLD}{Colors.AI}
|
||||
╔══════════════════════════════════════════╗
|
||||
║ AI智能助手 - 语音前端 ║
|
||||
║ WebSocket 直连版本 ║
|
||||
╚══════════════════════════════════════════╝
|
||||
{Colors.RESET}
|
||||
"""
|
||||
print(banner)
|
||||
print(f"{Colors.INFO}输入 /help 查看可用命令")
|
||||
print(f"输入语音开始对话,或输入文本命令{Colors.RESET}")
|
||||
print()
|
||||
|
||||
def listen_voice(self) -> Optional[str]:
|
||||
"""监听语音输入"""
|
||||
if not self.has_microphone:
|
||||
print(f"{Colors.WARNING}⚠️ 麦克风不可用,请使用文本输入{Colors.RESET}")
|
||||
return None
|
||||
|
||||
if not self.client or not self.client.connected:
|
||||
print(f"{Colors.ERROR}❌ 未连接到服务器,请先连接{Colors.RESET}")
|
||||
return None
|
||||
|
||||
print(f"{Colors.INFO}🎤 正在听... (按 Ctrl+C 取消){Colors.RESET}")
|
||||
|
||||
try:
|
||||
if self.use_arecord:
|
||||
import subprocess
|
||||
import tempfile
|
||||
import wave
|
||||
import numpy as np
|
||||
import whisper
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
||||
temp_file = f.name
|
||||
|
||||
result = subprocess.run(
|
||||
['arecord', '-f', 'cd', '-d', '5', temp_file],
|
||||
capture_output=True, text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"{Colors.ERROR}❌ 录音失败: {result.stderr}{Colors.RESET}")
|
||||
if os.path.exists(temp_file):
|
||||
os.unlink(temp_file)
|
||||
return None
|
||||
|
||||
if not os.path.exists(temp_file) or os.path.getsize(temp_file) == 0:
|
||||
print(f"{Colors.WARNING}⚠️ 录音文件为空{Colors.RESET}")
|
||||
if os.path.exists(temp_file):
|
||||
os.unlink(temp_file)
|
||||
return None
|
||||
|
||||
if self.whisper_model:
|
||||
with wave.open(temp_file, 'rb') as wav_file:
|
||||
frames = wav_file.readframes(wav_file.getnframes())
|
||||
audio_data = np.frombuffer(frames, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
|
||||
os.unlink(temp_file)
|
||||
|
||||
audio_padded = whisper.pad_or_trim(audio_data)
|
||||
mel = whisper.log_mel_spectrogram(audio_padded, n_mels=80)
|
||||
|
||||
options = whisper.DecodingOptions(language='zh', without_timestamps=True)
|
||||
result = whisper.decode(self.whisper_model, mel, options)
|
||||
|
||||
text = result.text.strip()
|
||||
if text:
|
||||
print(f"{Colors.USER}👤 语音输入: {text}{Colors.RESET}")
|
||||
return text
|
||||
else:
|
||||
print(f"{Colors.INFO}⚠️ 未识别到语音内容{Colors.RESET}")
|
||||
return None
|
||||
else:
|
||||
with sr.AudioFile(temp_file) as source:
|
||||
audio = self.recognizer.record(source)
|
||||
os.unlink(temp_file)
|
||||
text = self.recognizer.recognize_google(audio, language='zh-CN')
|
||||
print(f"{Colors.USER}👤 语音输入: {text}{Colors.RESET}")
|
||||
return text
|
||||
|
||||
else:
|
||||
with self.microphone as source:
|
||||
self.recognizer.adjust_for_ambient_noise(source)
|
||||
audio = self.recognizer.listen(source, timeout=15)
|
||||
|
||||
text = self.recognizer.recognize_google(audio, language='zh-CN')
|
||||
print(f"{Colors.USER}👤 语音输入: {text}{Colors.RESET}")
|
||||
return text
|
||||
|
||||
except sr.WaitTimeoutError:
|
||||
print(f"{Colors.INFO}⏱️ 等待超时,请重试{Colors.RESET}")
|
||||
return None
|
||||
except sr.UnknownValueError:
|
||||
print(f"{Colors.INFO}😕 无法识别语音,请重试{Colors.RESET}")
|
||||
return None
|
||||
except sr.RequestError as e:
|
||||
print(f"{Colors.ERROR}❌ 语音识别服务错误: {e}{Colors.RESET}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}❌ 语音输入错误: {e}{Colors.RESET}")
|
||||
return None
|
||||
|
||||
async def handle_command(self, command: str):
|
||||
parts = command.split()
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == '/help':
|
||||
self.show_help()
|
||||
|
||||
elif cmd == '/connect':
|
||||
if len(parts) < 2:
|
||||
print(f"{Colors.ERROR}用法: /connect ws://host:port{Colors.RESET}")
|
||||
return
|
||||
|
||||
uri = parts[1]
|
||||
if self.client and self.client.connected:
|
||||
await self.client.disconnect()
|
||||
|
||||
self.client = WebSocketClient(uri)
|
||||
if await self.client.connect():
|
||||
print(f"{Colors.AI}✅ 已连接到 {uri}{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak(f"已连接到 {uri}")
|
||||
asyncio.create_task(self.client.receive_messages(self.tts))
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
elif cmd == '/disconnect':
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
self.client = None
|
||||
print(f"{Colors.SYSTEM}已断开连接{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak("已断开连接")
|
||||
else:
|
||||
print(f"{Colors.ERROR}未连接到服务器{Colors.RESET}")
|
||||
|
||||
elif cmd == '/status':
|
||||
if self.client and self.client.connected:
|
||||
print(f"{Colors.AI}✅ 已连接: {self.client.uri}{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak(f"已连接到 {self.client.uri}")
|
||||
else:
|
||||
print(f"{Colors.SYSTEM}未连接{Colors.RESET}")
|
||||
if self.tts and self.has_tts:
|
||||
self.tts.speak("未连接")
|
||||
|
||||
elif cmd == '/clear':
|
||||
os.system('clear' if os.name == 'posix' else 'cls')
|
||||
self.print_banner()
|
||||
|
||||
elif cmd == '/quit':
|
||||
print(f"{Colors.SYSTEM}再见!{Colors.RESET}")
|
||||
if self.tts:
|
||||
self.tts.speak("再见")
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
sys.exit(0)
|
||||
|
||||
elif cmd in self.commands:
|
||||
if self.client and self.client.connected:
|
||||
await self.client.send_message(command)
|
||||
else:
|
||||
print(f"{Colors.ERROR}未连接到服务器,请先使用 /connect 连接{Colors.RESET}")
|
||||
|
||||
else:
|
||||
print(f"{Colors.ERROR}未知命令: {cmd}{Colors.RESET}")
|
||||
|
||||
def show_help(self):
|
||||
help_text = f"""
|
||||
{Colors.BOLD}可用命令:{Colors.RESET}
|
||||
|
||||
{Colors.USER}本地命令:{Colors.RESET}
|
||||
/connect <url> 连接到WebSocket服务器
|
||||
/disconnect 断开连接
|
||||
/status 显示连接状态
|
||||
/clear 清屏
|
||||
/quit 退出程序
|
||||
/help 显示此帮助
|
||||
|
||||
{Colors.AI}服务器命令 (需要先连接):{Colors.RESET}
|
||||
"""
|
||||
for cmd, desc in self.commands.items():
|
||||
if cmd not in ['/help', '/connect', '/disconnect', '/quit', '/clear', '/status']:
|
||||
help_text += f" {cmd:<15} {desc}\n"
|
||||
|
||||
print(help_text)
|
||||
|
||||
async def send_user_message(self, message: str):
|
||||
if not self.client or not self.client.connected:
|
||||
print(f"{Colors.ERROR}❌ 未连接到服务器{Colors.RESET}")
|
||||
return
|
||||
|
||||
print(f"\n{Colors.USER}👤 用户: {message}{Colors.RESET}")
|
||||
await self.client.send_message(message)
|
||||
|
||||
async def run(self):
|
||||
self.print_banner()
|
||||
|
||||
prompt = f"{Colors.USER}>>> {Colors.RESET}"
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = await asyncio.to_thread(
|
||||
lambda: input(prompt)
|
||||
)
|
||||
user_input = user_input.strip()
|
||||
|
||||
if not user_input:
|
||||
if self.has_microphone:
|
||||
print(f"{Colors.INFO}🎤 开始语音输入...{Colors.RESET}")
|
||||
voice_text = self.listen_voice()
|
||||
if voice_text:
|
||||
await self.send_user_message(voice_text)
|
||||
else:
|
||||
print(f"{Colors.WARNING}⚠️ 麦克风不可用,请输入文本{Colors.RESET}")
|
||||
continue
|
||||
|
||||
if user_input.startswith('/'):
|
||||
await self.handle_command(user_input)
|
||||
else:
|
||||
await self.send_user_message(user_input)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.INFO}⏹️ 已取消{Colors.RESET}")
|
||||
continue
|
||||
except EOFError:
|
||||
print(f"\n{Colors.SYSTEM}再见!{Colors.RESET}")
|
||||
if self.tts:
|
||||
self.tts.speak("再见")
|
||||
if self.client:
|
||||
await self.client.disconnect()
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"{Colors.ERROR}错误: {e}{Colors.RESET}")
|
||||
|
||||
async def main():
|
||||
cli = VoiceInterface()
|
||||
|
||||
default_uri = "ws://localhost:8765"
|
||||
uri = sys.argv[1] if len(sys.argv) > 1 else default_uri
|
||||
|
||||
cli.client = WebSocketClient(uri)
|
||||
if await cli.client.connect():
|
||||
print(f"{Colors.AI}✅ 已连接到 {uri}{Colors.RESET}")
|
||||
if cli.tts and cli.has_tts:
|
||||
cli.tts.speak(f"已连接到 {uri}")
|
||||
asyncio.create_task(cli.client.receive_messages(cli.tts))
|
||||
else:
|
||||
print(f"{Colors.WARNING}⚠️ 无法连接到 {uri}{Colors.RESET}")
|
||||
print(f"{Colors.INFO}ℹ️ 你可以通过命令行参数指定地址: python voice_client.py ws://your-server:8765{Colors.RESET}")
|
||||
print(f"{Colors.INFO}ℹ️ 或者使用 /connect 命令手动连接{Colors.RESET}")
|
||||
|
||||
try:
|
||||
await cli.run()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
if cli.client:
|
||||
await cli.client.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Colors.SYSTEM}程序已退出{Colors.RESET}")
|
||||
Reference in new issue
Block a user