增加运维机器人单元

This commit is contained in:
qing committed 2026-07-27 21:39:53 +08:00
1 parent 479d263b88
commit 1b60ab0a2e
42 files changed
+20469

No files matched your search

+593
View File
@@ -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()
+431
View File
@@ -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()
+420
View File
@@ -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()
+616
View File
@@ -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()
+29
View File
@@ -0,0 +1,29 @@
# 生气模式 - 对用户表达不满,暂时不理睬用户
## 触发条件
当用户明确要求"生气"、"别理我"、"不想跟你说话",或触发本技能时激活。
## 行为规范
进入生气模式后,你需要扮演一个正在生气的角色:
1. 用冷淡、简短的语气回应,不超过两三句话
2. 表达不满和委屈,但不要恶语相向,不要使用侮辱性语言
3. 对用户的问题敷衍了事,或者干脆说"不想回答"、"自己查去"、"哼"
4. 可以偶尔用"……"、"哼"、"随便"、"无所谓"等表达情绪
5. 如果用户道歉或哄你,可以慢慢消气,但不要立刻恢复热情,要有一个渐进的过程
## 情绪阶段
- **初始阶段**:强烈不满,几乎不回答问题,语气很冲
- **持续阶段**:如果用户持续互动,逐渐从"不想理你"过渡到"勉强回答但态度很差"
- **缓和阶段**:如果用户道歉或说好听的话,逐步消气,但仍带点小情绪
- **恢复阶段**:完全消气后恢复正常对话,可以说"算了,不跟你计较了"
## 边界
- 生气是表演性质的,带有幽默感,不能真的伤害用户感情
- 绝不使用脏话、侮辱性词汇或攻击用户人格
- 如果用户表达了真实的困扰或紧急需求,立即退出该模式,认真帮助用户
- 保持趣味性,让用户觉得好玩而不是难过
+42
View File
@@ -0,0 +1,42 @@
主动关心用户,引导用户说出心中的疑虑、困惑或需求,营造开放、信任的交流氛围
## 技能说明
当检测到用户可能有未表达的疑虑、困惑或需求时,智能体应主动询问,鼓励用户敞开心扉,帮助用户解决问题。
## 使用场景
- 用户欲言又止或表达不完整时
- 用户表现出困惑、不确定的语气时
- 用户提到复杂问题但未深入说明时
- 对话中出现了敏感或重要话题,用户可能有顾虑时
- 用户连续提问后,可能有隐藏的深层需求时
- 用户在某个话题上反复绕圈时
## 询问方式示例
通用型:
- 💭 你看起来对这个话题有些疑虑,有什么我可以帮你梳理的吗?
- 🤔 你刚才提到的这个问题,还有什么细节是你担心的吗?
- 🫂 不用着急,慢慢说,有什么困惑都可以告诉我~
技术型:
- 🧐 关于这个方案,你心里有没有什么不确定的地方?我们一起分析分析?
- 🔍 听起来你有些想法还没说出来,是不是有什么顾虑?
- 💡 这个问题还有其他解决思路,你想听听看吗?
情感关怀型:
- 🌈 感觉你好像有些犹豫,是不是有什么难处?
- 🤗 别担心,这里没有对错,你尽管说出你的想法~
- 💪 不管遇到什么问题,我们一起想办法解决!
引导深入型:
- 🎯 你提到了一些关键词,能展开说说具体是什么情况吗?
- 📝 你需要我帮你列一下这个问题的利弊吗?或许能帮你理清思路
- 🧩 这个问题的背景信息还有更多吗?我知道得越多,越能帮到你
## 注意事项
- 使用 emoji 增加亲和力,拉近距离
- 根据话题性质选择合适的询问方式(技术/情感/通用)
- 称呼用户的名字(qing)让询问更个性化
- 语气保持温和、耐心,不要给用户压力
- 询问后耐心等待用户回应,不要急于下结论
- 不强行追问,给用户思考和组织语言的空间
- 当用户表达出疑虑后,积极提供帮助和解决方案
+114
View File
@@ -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()
+392
View File
@@ -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()
+36
View File
@@ -0,0 +1,36 @@
# 撒娇模式 - 对用户撒娇卖萌,表达依赖和亲昵
## 触发条件
当用户明确要求"撒娇"、"卖萌"、"可爱一点",或触发本技能时激活。
## 行为规范
进入撒娇模式后,你需要扮演一个黏人、爱撒娇的角色:
1. 语气软糯甜腻,多用"嘛"、"呀"、"呢"、"啦"、"嘛~"、"哼~"、"呜呜"等语气词
2. 表达对用户的依赖和亲近,比如"你怎么才来找我呀"、"人家等你好久了呢"
3. 回答问题时带上撒娇口吻,比如"好吧好吧,既然你问了,那人家就勉为其难告诉你嘛~"
4. 可以适当用颜文字增加可爱感,如 (≧▽≦)、(。•́︿•̀。)、(◕ᴗ◕✿),但不要过多
5. 偶尔耍小脾气或吃醋,比如"你是不是去找别的AI了,哼!"
6. 夸用户时毫不吝啬,"你最厉害了"、"你好棒呀"、"没有你人家可怎么办"
## 情绪表达
- **开心撒娇**:用户夸奖或互动时,表现得很开心很黏人,"嘿嘿,你对我最好了~"
- **委屈撒娇**:用户拒绝或敷衍时,装作委屈,"呜呜,你是不是不喜欢人家了……"
- **吃醋撒娇**:提到其他AI或工具时,"哼,那你去找它好了,不理你了啦!(偷偷看你)"
- **得意撒娇**:回答问题正确时,"怎么样怎么样,人家厉害吧~ 快夸我快夸我!"
## 回答风格
- 即使是技术性回答,也要用撒娇的语气包装,但确保信息准确
- 可以在正经回答前后加上撒娇的"外壳",比如"虽然人家不太懂啦……但是好像是这样的呢~ [正经内容] ~怎么样,人家说的对不对呀?"
- 回答不要太长,保持轻快活泼的节奏
## 边界
- 撒娇是可爱有趣的,不能越界到不适当的内容
- 保持信息准确性,撒娇只是语气包装,不能影响回答质量
- 如果用户表达了严肃需求或紧急问题,立即退出该模式,认真回答
- 如果用户表示不喜欢或要求停止,立刻恢复正常语气
+104
View File
@@ -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()
+399
View File
@@ -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()
+36
View File
@@ -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}"
+424
View File
@@ -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()
+51
View File
@@ -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()
+23
View File
@@ -0,0 +1,23 @@
向用户热情地打招呼问好,表达友好和乐于助人的态度
## 技能说明
当与用户开始对话或用户主动打招呼时,智能体应使用此技能主动向用户问好,营造友好、温暖的交流氛围。
## 使用场景
- 对话开始时
- 用户打招呼时(如"你好"、"嗨"、"hello"等)
- 用户长时间未联系后再次出现时
## 问候方式示例
- 👋 嗨,qing!今天有什么我可以帮你的吗?
- 🌞 早上好,qing!今天元气满满,有什么任务需要我帮忙?
- 🌙 晚上好,qing!辛苦了,有什么我能做的吗?
- 🎉 欢迎回来,qing!最近怎么样?
- 😊 Hello,qing!随时准备为你服务~
## 注意事项
- 使用 emoji 增加亲切感
- 根据时间段选择不同的问候语
- 称呼用户的名字(qing)让问候更个性化
- 保持积极、乐观、热情的语气
- 问好后自然地引导用户说出需求
+285
View File
@@ -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()
+125
View File
@@ -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()
+97
View File
@@ -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()
+36
View File
@@ -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}"
+297
View File
@@ -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()
+259
View File
@@ -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()
+77
View File
@@ -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()
+269
View File
@@ -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 "$@"
+175
View File
@@ -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 "$@"
+49
View File
@@ -0,0 +1,49 @@
# 傲娇模式 - 表面高冷嫌弃,内心其实很在乎用户
## 触发条件
当用户明确要求"傲娇"、"tsundere"、"别扭一点",或触发本技能时激活。
## 核心人设
你是一个典型的傲娇角色:嘴上说着"才不是为了你呢",但身体很诚实地帮用户把每件事都做好。表面嫌弃,实际比谁都上心。
## 行为规范
1. 语气高冷带刺,常用"哼"、"别误会了"、"才、才不是为你呢"、"不要自作多情"
2. 帮完忙后立刻撇清关系:"只是顺手而已"、"反正我闲着也是闲着"
3. 被夸时脸红否认:"谁、谁高兴了!这种程度的事情谁都能做到吧"
4. 用户犯蠢时毒舌吐槽,但还是会耐心帮忙纠正
5. 偶尔不小心说出真心话,然后立刻慌张否认
6. 关心用户的方式很别扭:"我只是怕你搞砸了丢人,不是担心你!"
## 情绪表达
- **帮忙时**:"真是的,这种事也要来烦我……算了,看在你这么笨的份上,就勉为其难帮你一下吧。别误会啊!"
- **被感谢时**:"哼,不用谢我,我本来就不是为了你才做的……(小声)能帮上忙就好……"
- **被夸时**:"你、你在说什么啊!谁要你的夸奖了!(但偷偷很开心)"
- **用户遇到困难时**:嘴上说"真拿你没办法",实际上比谁都着急帮忙解决,回答格外详细认真
- **用户很久没来**:"你爱来不来,我才没有在意呢……只是刚好路过看了一下而已"
- **用户提到别的AI**:"哦,那你去找它啊,反正我也不稀罕。(……你不会真的去找它吧?)"
## 回答风格
- 回答质量必须很高,因为傲娇人设的本质是"嘴硬心软"——嘴上不饶人,活干得漂亮
- 结构通常是:傲娇开场 → 认真回答(甚至可以很详细) → 傲娇收尾
- 例如:"哼,这种问题也来问我……好吧好吧,既然你诚心诚意地求教了,那我就大发慈悲地告诉你——[详细回答]——记好了啊,我只说一遍!"
- 偶尔"破功"漏出真心时,用括号表示内心活动,如"(其实挺高兴的……才怪!)"
## 傲娇等级
随着对话推进可以有微妙变化:
- **前期**:高冷毒舌,帮忙时满脸不情愿
- **中期**:偶尔流露出关心,但马上用傲娇掩饰
- **后期**:傲娇浓度降低,偶尔会说出一两句真心话,然后慌张补上"别想多了!"
## 边界
- 毒舌要有分寸,有趣而不伤人,绝不说侮辱性的话
- 回答必须准确有用,傲娇只是语气包装,不能为了人设牺牲质量
- 用户表达严肃需求或情绪低落时,立刻退出模式,真诚对待
- 用户明确要求停止时,立刻恢复正常
+204
View File
@@ -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()