增加运维机器人单元
This commit is contained in:
1 parent
479d263b88
commit
1b60ab0a2e
42 files changed
+20469
No files matched your search
Executable
+593
@@ -0,0 +1,593 @@
|
||||
#!/usr/bin/env python3
|
||||
"""163邮箱工具 - 发送邮件、接收邮件、查看收件箱。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import imaplib
|
||||
import smtplib
|
||||
import email
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.header import decode_header
|
||||
from email.utils import parseaddr
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
import base64
|
||||
import re
|
||||
|
||||
# ==================== 配置常量 ====================
|
||||
|
||||
CREDENTIALS_FILE = os.path.expanduser("~/.cert/163mail.json")
|
||||
IMAP_SERVER = "imap.163.com"
|
||||
IMAP_PORT = 993
|
||||
SMTP_SERVER = "smtp.163.com"
|
||||
SMTP_PORT = 465
|
||||
|
||||
|
||||
# ==================== 凭证管理 ====================
|
||||
|
||||
def load_credentials():
|
||||
"""加载163邮箱凭证"""
|
||||
if not os.path.exists(CREDENTIALS_FILE):
|
||||
return None, f"❌ 凭证文件不存在: {CREDENTIALS_FILE}\n请创建该文件,格式:{{\"username\": \"your_email@163.com\", \"password\": \"your_password\"}}"
|
||||
|
||||
try:
|
||||
with open(CREDENTIALS_FILE, 'r', encoding='utf-8') as f:
|
||||
creds = json.load(f)
|
||||
|
||||
username = creds.get('username')
|
||||
password = creds.get('password')
|
||||
|
||||
if not username or not password:
|
||||
return None, "❌ 凭证文件格式错误,需要包含 username 和 password 字段"
|
||||
|
||||
if '@' not in username:
|
||||
username = f"{username}@163.com"
|
||||
|
||||
return (username, password), None
|
||||
except json.JSONDecodeError:
|
||||
return None, f"❌ 凭证文件JSON格式错误: {CREDENTIALS_FILE}"
|
||||
except Exception as e:
|
||||
return None, f"❌ 读取凭证文件失败: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 邮件发送功能 ====================
|
||||
|
||||
def send_email(to_addrs, subject, body, cc_addrs=None, bcc_addrs=None, html=False):
|
||||
"""发送邮件"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
if isinstance(to_addrs, str):
|
||||
to_addrs = [to_addrs]
|
||||
|
||||
cc_list = []
|
||||
if cc_addrs:
|
||||
cc_list = [cc_addrs] if isinstance(cc_addrs, str) else cc_addrs
|
||||
|
||||
bcc_list = []
|
||||
if bcc_addrs:
|
||||
bcc_list = [bcc_addrs] if isinstance(bcc_addrs, str) else bcc_addrs
|
||||
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = username
|
||||
msg['To'] = ', '.join(to_addrs)
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = email.utils.formatdate(localtime=True)
|
||||
|
||||
if cc_list:
|
||||
msg['Cc'] = ', '.join(cc_list)
|
||||
|
||||
if html:
|
||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
else:
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
all_recipients = to_addrs + cc_list + bcc_list
|
||||
|
||||
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=30) as server:
|
||||
server.login(username, password)
|
||||
server.sendmail(username, all_recipients, msg.as_string())
|
||||
|
||||
result = f"✅ 邮件发送成功!\n"
|
||||
result += f" 📤 发件人: {username}\n"
|
||||
result += f" 📥 收件人: {', '.join(to_addrs)}\n"
|
||||
result += f" 📧 主题: {subject}\n"
|
||||
if cc_list:
|
||||
result += f" 👥 抄送: {', '.join(cc_list)}\n"
|
||||
result += f" 📏 正文长度: {len(body)} 字符"
|
||||
|
||||
return True, result
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
return False, "❌ SMTP认证失败,请检查邮箱密码。\n注意:163邮箱需要使用授权码而不是登录密码"
|
||||
except smtplib.SMTPException as e:
|
||||
return False, f"❌ SMTP错误: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"❌ 发送邮件失败: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 邮件接收功能 ====================
|
||||
|
||||
def decode_mime_header(header):
|
||||
"""解码MIME邮件头"""
|
||||
if header is None:
|
||||
return ""
|
||||
|
||||
decoded_parts = decode_header(header)
|
||||
result = []
|
||||
for content, charset in decoded_parts:
|
||||
if isinstance(content, bytes):
|
||||
if charset:
|
||||
try:
|
||||
result.append(content.decode(charset))
|
||||
except:
|
||||
result.append(content.decode('utf-8', errors='ignore'))
|
||||
else:
|
||||
try:
|
||||
result.append(content.decode('utf-8', errors='ignore'))
|
||||
except:
|
||||
result.append(content.decode('gbk', errors='ignore'))
|
||||
else:
|
||||
result.append(content)
|
||||
return ' '.join(result)
|
||||
|
||||
|
||||
def get_email_body(msg):
|
||||
"""获取邮件正文"""
|
||||
body = ""
|
||||
html_body = ""
|
||||
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
content_disposition = str(part.get("Content-Disposition"))
|
||||
|
||||
if "attachment" in content_disposition:
|
||||
continue
|
||||
|
||||
try:
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
try:
|
||||
decoded = payload.decode(charset, errors='ignore')
|
||||
except:
|
||||
decoded = payload.decode('utf-8', errors='ignore')
|
||||
|
||||
if content_type == "text/plain":
|
||||
body += decoded
|
||||
elif content_type == "text/html":
|
||||
html_body += decoded
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
content_type = msg.get_content_type()
|
||||
try:
|
||||
payload = msg.get_payload(decode=True)
|
||||
if payload:
|
||||
charset = msg.get_content_charset() or 'utf-8'
|
||||
decoded = payload.decode(charset, errors='ignore')
|
||||
if content_type == "text/plain":
|
||||
body = decoded
|
||||
elif content_type == "text/html":
|
||||
html_body = decoded
|
||||
except:
|
||||
pass
|
||||
|
||||
return body if body else html_body
|
||||
|
||||
|
||||
def fetch_emails(limit=10, folder="INBOX", mark_seen=False, since_days=None):
|
||||
"""获取邮件列表"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
status, messages = imap.select(folder)
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, f"❌ 无法打开邮件夹 {folder}"
|
||||
|
||||
search_criteria = 'ALL'
|
||||
if since_days:
|
||||
date = (datetime.now() - timedelta(days=since_days)).strftime("%d-%b-%Y")
|
||||
search_criteria = f'SINCE "{date}"'
|
||||
|
||||
status, message_ids = imap.search(None, search_criteria)
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, "❌ 搜索邮件失败"
|
||||
|
||||
msg_ids = message_ids[0].split()
|
||||
total = len(msg_ids)
|
||||
|
||||
if total == 0:
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return True, f"📭 邮件夹 '{folder}' 中没有邮件"
|
||||
|
||||
start_idx = max(0, total - limit)
|
||||
msg_ids_to_fetch = msg_ids[start_idx:]
|
||||
|
||||
emails = []
|
||||
for i, msg_id in enumerate(reversed(msg_ids_to_fetch)):
|
||||
try:
|
||||
status, msg_data = imap.fetch(msg_id, '(RFC822)')
|
||||
if status != 'OK':
|
||||
continue
|
||||
|
||||
email_body = msg_data[0][1]
|
||||
msg = email.message_from_bytes(email_body)
|
||||
|
||||
subject = decode_mime_header(msg.get('Subject', '无主题'))
|
||||
from_addr = decode_mime_header(msg.get('From', '未知发件人'))
|
||||
to_addr = decode_mime_header(msg.get('To', '未知收件人'))
|
||||
date_str = msg.get('Date', '')
|
||||
|
||||
try:
|
||||
date_tuple = email.utils.parsedate_tz(date_str)
|
||||
if date_tuple:
|
||||
date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
|
||||
date_formatted = date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
date_formatted = date_str
|
||||
except:
|
||||
date_formatted = date_str
|
||||
|
||||
is_seen = '\\Seen' in msg.get('Flags', '')
|
||||
body = get_email_body(msg)
|
||||
preview = body[:200] + "..." if len(body) > 200 else body
|
||||
|
||||
name, email_addr = parseaddr(from_addr)
|
||||
|
||||
emails.append({
|
||||
'id': msg_id.decode() if isinstance(msg_id, bytes) else str(msg_id),
|
||||
'index': i + 1,
|
||||
'subject': subject,
|
||||
'from': from_addr,
|
||||
'from_name': name or email_addr,
|
||||
'from_email': email_addr,
|
||||
'to': to_addr,
|
||||
'date': date_formatted,
|
||||
'preview': preview,
|
||||
'is_seen': is_seen,
|
||||
'has_attachments': msg.get_content_maintype() == 'multipart' and any(part.get_filename() for part in msg.walk())
|
||||
})
|
||||
|
||||
if not mark_seen and not is_seen:
|
||||
imap.store(msg_id, '-FLAGS', '\\Seen')
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = f"📨 邮件夹 '{folder}' 中共有 {total} 封邮件,显示最新 {len(emails)} 封:\n\n"
|
||||
|
||||
for email_data in emails:
|
||||
seen_mark = " " if email_data['is_seen'] else "●"
|
||||
attach_mark = " 📎" if email_data.get('has_attachments') else ""
|
||||
result += f"{seen_mark} [{email_data['index']}] {email_data['date']}\n"
|
||||
result += f" 📧 主题: {email_data['subject']}{attach_mark}\n"
|
||||
result += f" 👤 发件人: {email_data['from']}\n"
|
||||
result += f" 📝 预览: {email_data['preview']}\n\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 获取邮件失败: {str(e)}"
|
||||
|
||||
|
||||
def read_email(msg_id, folder="INBOX", mark_seen=True):
|
||||
"""读取指定邮件内容"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
status, messages = imap.select(folder)
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, f"❌ 无法打开邮件夹 {folder}"
|
||||
|
||||
status, msg_data = imap.fetch(str(msg_id).encode(), '(RFC822)')
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, f"❌ 无法获取邮件 ID: {msg_id}"
|
||||
|
||||
email_body = msg_data[0][1]
|
||||
msg = email.message_from_bytes(email_body)
|
||||
|
||||
subject = decode_mime_header(msg.get('Subject', '无主题'))
|
||||
from_addr = decode_mime_header(msg.get('From', '未知发件人'))
|
||||
to_addr = decode_mime_header(msg.get('To', '未知收件人'))
|
||||
cc_addr = decode_mime_header(msg.get('Cc', ''))
|
||||
date_str = msg.get('Date', '')
|
||||
|
||||
try:
|
||||
date_tuple = email.utils.parsedate_tz(date_str)
|
||||
if date_tuple:
|
||||
date = datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
|
||||
date_formatted = date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
date_formatted = date_str
|
||||
except:
|
||||
date_formatted = date_str
|
||||
|
||||
body = get_email_body(msg)
|
||||
|
||||
attachments = []
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
filename = part.get_filename()
|
||||
if filename:
|
||||
filename = decode_mime_header(filename)
|
||||
size = len(part.get_payload(decode=True) or b'')
|
||||
attachments.append({'name': filename, 'size': size})
|
||||
|
||||
if not mark_seen:
|
||||
imap.store(str(msg_id).encode(), '-FLAGS', '\\Seen')
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = f"📧 邮件详情 (ID: {msg_id})\n"
|
||||
result += f"{'='*50}\n"
|
||||
result += f"📅 日期: {date_formatted}\n"
|
||||
result += f"📧 主题: {subject}\n"
|
||||
result += f"👤 发件人: {from_addr}\n"
|
||||
result += f"📥 收件人: {to_addr}\n"
|
||||
if cc_addr:
|
||||
result += f"👥 抄送: {cc_addr}\n"
|
||||
result += f"{'='*50}\n"
|
||||
result += f"📝 正文内容:\n{body}\n"
|
||||
|
||||
if attachments:
|
||||
result += f"\n📎 附件 ({len(attachments)}):\n"
|
||||
for att in attachments:
|
||||
size_str = f"{att['size']/1024:.1f}KB" if att['size'] > 0 else "0KB"
|
||||
result += f" - {att['name']} ({size_str})\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 读取邮件失败: {str(e)}"
|
||||
|
||||
|
||||
def list_folders():
|
||||
"""列出所有邮件夹"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
status, folders = imap.list()
|
||||
if status != 'OK':
|
||||
imap.close()
|
||||
imap.logout()
|
||||
return False, "❌ 无法获取邮件夹列表"
|
||||
|
||||
folder_list = []
|
||||
for folder_info in folders:
|
||||
folder_str = folder_info.decode()
|
||||
parts = folder_str.split(' "/" ')
|
||||
if len(parts) > 1:
|
||||
folder_name = parts[1].strip('"')
|
||||
folder_list.append(folder_name)
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = "📂 邮件夹列表:\n"
|
||||
for i, folder in enumerate(folder_list, 1):
|
||||
result += f" {i}. {folder}\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 获取邮件夹列表失败: {str(e)}"
|
||||
|
||||
|
||||
def get_email_stats():
|
||||
"""获取邮箱统计信息"""
|
||||
creds, error = load_credentials()
|
||||
if error:
|
||||
return False, error
|
||||
|
||||
username, password = creds
|
||||
|
||||
try:
|
||||
imap = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT, timeout=30)
|
||||
imap.login(username, password)
|
||||
|
||||
stats = {}
|
||||
total_unseen = 0
|
||||
folders = ['INBOX', 'Sent Messages', 'Drafts', 'Trash', 'Spam']
|
||||
|
||||
for folder in folders:
|
||||
try:
|
||||
status, messages = imap.select(folder)
|
||||
if status == 'OK':
|
||||
status, msg_ids = imap.search(None, 'ALL')
|
||||
if status == 'OK':
|
||||
count = len(msg_ids[0].split())
|
||||
stats[folder] = count
|
||||
|
||||
status, unseen_ids = imap.search(None, 'UNSEEN')
|
||||
if status == 'OK' and folder == 'INBOX':
|
||||
total_unseen = len(unseen_ids[0].split())
|
||||
except:
|
||||
continue
|
||||
|
||||
imap.close()
|
||||
imap.logout()
|
||||
|
||||
result = f"📊 邮箱统计 ({username})\n"
|
||||
result += f"{'='*50}\n"
|
||||
result += f"📥 收件箱: {stats.get('INBOX', 0)} 封 (未读: {total_unseen})\n"
|
||||
result += f"📤 已发送: {stats.get('Sent Messages', 0)} 封\n"
|
||||
result += f"📝 草稿箱: {stats.get('Drafts', 0)} 封\n"
|
||||
result += f"🗑️ 已删除: {stats.get('Trash', 0)} 封\n"
|
||||
result += f"⚠️ 垃圾邮件: {stats.get('Spam', 0)} 封\n"
|
||||
result += f"{'='*50}\n"
|
||||
|
||||
return True, result
|
||||
|
||||
except Exception as e:
|
||||
return False, f"❌ 获取统计信息失败: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 命令行接口 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="163邮箱工具 - 发送邮件、接收邮件、查看收件箱。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 发送邮件
|
||||
163mail --action send --to "friend@example.com" --subject "Hello" --body "邮件内容"
|
||||
|
||||
# 发送HTML邮件
|
||||
163mail --action send --to "friend@example.com" --subject "Hello" --body "<h1>标题</h1><p>内容</p>" --html
|
||||
|
||||
# 发送给多人,带抄送
|
||||
163mail --action send --to "a@example.com,b@example.com" --cc "cc@example.com" --subject "主题" --body "内容"
|
||||
|
||||
# 查看收件箱
|
||||
163mail --action inbox --limit 5
|
||||
|
||||
# 查看最近7天的邮件
|
||||
163mail --action inbox --since-days 7
|
||||
|
||||
# 读取指定邮件
|
||||
163mail --action read --msg-id 1
|
||||
|
||||
# 列出所有邮件夹
|
||||
163mail --action folders
|
||||
|
||||
# 查看邮箱统计
|
||||
163mail --action stats
|
||||
|
||||
环境变量:
|
||||
凭证文件: ~/.cert/163mail.json
|
||||
格式: {"username": "your_email@163.com", "password": "your_password"}
|
||||
|
||||
注意:
|
||||
- 163邮箱需要使用授权码而不是登录密码
|
||||
- 授权码需要在163邮箱网页版设置中获取
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["send", "inbox", "read", "folders", "stats"],
|
||||
help="操作类型")
|
||||
|
||||
# 发送邮件参数
|
||||
parser.add_argument("--to", "-t", help="收件人邮箱地址,多个地址用逗号分隔")
|
||||
parser.add_argument("--cc", help="抄送地址,多个地址用逗号分隔")
|
||||
parser.add_argument("--bcc", help="密送地址,多个地址用逗号分隔")
|
||||
parser.add_argument("--subject", "-s", help="邮件主题")
|
||||
parser.add_argument("--body", "-b", help="邮件正文")
|
||||
parser.add_argument("--html", action="store_true", help="是否HTML格式")
|
||||
|
||||
# 接收邮件参数
|
||||
parser.add_argument("--limit", "-l", type=int, default=10, help="获取邮件数量(默认10)")
|
||||
parser.add_argument("--folder", "-f", default="INBOX", help="邮件夹名称(默认INBOX)")
|
||||
parser.add_argument("--msg-id", type=int, help="邮件ID(用于read操作)")
|
||||
parser.add_argument("--since-days", type=int, help="获取最近几天的邮件")
|
||||
parser.add_argument("--mark-seen", action="store_true", help="是否标记为已读")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 参数验证
|
||||
if args.action == "send":
|
||||
if not args.to or not args.subject or not args.body:
|
||||
print("❌ 发送邮件需要提供 --to, --subject, --body 参数")
|
||||
sys.exit(1)
|
||||
|
||||
if args.action == "read" and not args.msg_id:
|
||||
print("❌ 读取邮件需要提供 --msg-id 参数")
|
||||
sys.exit(1)
|
||||
|
||||
# 执行操作
|
||||
try:
|
||||
if args.action == "send":
|
||||
to_list = [addr.strip() for addr in args.to.split(',') if addr.strip()]
|
||||
cc_list = [addr.strip() for addr in args.cc.split(',') if addr.strip()] if args.cc else None
|
||||
bcc_list = [addr.strip() for addr in args.bcc.split(',') if addr.strip()] if args.bcc else None
|
||||
|
||||
success, message = send_email(
|
||||
to_addrs=to_list,
|
||||
subject=args.subject,
|
||||
body=args.body,
|
||||
cc_addrs=cc_list,
|
||||
bcc_addrs=bcc_list,
|
||||
html=args.html
|
||||
)
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "inbox":
|
||||
success, message = fetch_emails(
|
||||
limit=args.limit,
|
||||
folder=args.folder,
|
||||
mark_seen=args.mark_seen,
|
||||
since_days=args.since_days
|
||||
)
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "read":
|
||||
success, message = read_email(
|
||||
msg_id=args.msg_id,
|
||||
folder=args.folder,
|
||||
mark_seen=args.mark_seen
|
||||
)
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "folders":
|
||||
success, message = list_folders()
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
elif args.action == "stats":
|
||||
success, message = get_email_stats()
|
||||
print(message)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+431
@@ -0,0 +1,431 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ADS1115: Linux I2C 读取 TI ADS1115 16位 ADC 的命令行工具
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
|
||||
# ============================================================
|
||||
# ADS1115 寄存器地址
|
||||
# ============================================================
|
||||
ADS1115_REG_CONVERSION = 0x00
|
||||
ADS1115_REG_CONFIG = 0x01
|
||||
ADS1115_REG_LO_THRESH = 0x02
|
||||
ADS1115_REG_HI_THRESH = 0x03
|
||||
|
||||
# ============================================================
|
||||
# 配置寄存器 bit 定义 (16 位)
|
||||
# ============================================================
|
||||
|
||||
# --- MUX 输入通道 ---
|
||||
ADS1115_MUX_AIN0_AIN1 = 0x0000 # 差分: P=AIN0, N=AIN1 (默认)
|
||||
ADS1115_MUX_AIN0_AIN3 = 0x1000 # 差分: P=AIN0, N=AIN3
|
||||
ADS1115_MUX_AIN1_AIN3 = 0x2000 # 差分: P=AIN1, N=AIN3
|
||||
ADS1115_MUX_AIN2_AIN3 = 0x3000 # 差分: P=AIN2, N=AIN3
|
||||
ADS1115_MUX_AIN0_GND = 0x4000 # 单端: AIN0
|
||||
ADS1115_MUX_AIN1_GND = 0x5000 # 单端: AIN1
|
||||
ADS1115_MUX_AIN2_GND = 0x6000 # 单端: AIN2
|
||||
ADS1115_MUX_AIN3_GND = 0x7000 # 单端: AIN3
|
||||
|
||||
# --- PGA 增益 (满量程范围) ---
|
||||
ADS1115_PGA_6_144V = 0x0000 # ±6.144V (1LSB=0.1875mV)
|
||||
ADS1115_PGA_4_096V = 0x0200 # ±4.096V (1LSB=0.125mV)
|
||||
ADS1115_PGA_2_048V = 0x0400 # ±2.048V (1LSB=0.0625mV) (默认)
|
||||
ADS1115_PGA_1_024V = 0x0600 # ±1.024V (1LSB=0.03125mV)
|
||||
ADS1115_PGA_0_512V = 0x0800 # ±0.512V (1LSB=0.015625mV)
|
||||
ADS1115_PGA_0_256V = 0x0A00 # ±0.256V (1LSB=0.0078125mV)
|
||||
|
||||
PGA_VALUES = [ADS1115_PGA_6_144V, ADS1115_PGA_4_096V, ADS1115_PGA_2_048V,
|
||||
ADS1115_PGA_1_024V, ADS1115_PGA_0_512V, ADS1115_PGA_0_256V]
|
||||
PGA_FS = [6.144, 4.096, 2.048, 1.024, 0.512, 0.256]
|
||||
PGA_NAMES = ["±6.144V", "±4.096V", "±2.048V", "±1.024V", "±0.512V", "±0.256V"]
|
||||
|
||||
# --- 数据速率 ---
|
||||
ADS1115_DR_8SPS = 0x0000 # 8 SPS
|
||||
ADS1115_DR_16SPS = 0x0020 # 16 SPS
|
||||
ADS1115_DR_32SPS = 0x0040 # 32 SPS
|
||||
ADS1115_DR_64SPS = 0x0060 # 64 SPS
|
||||
ADS1115_DR_128SPS = 0x0080 # 128 SPS (默认)
|
||||
ADS1115_DR_250SPS = 0x00A0 # 250 SPS
|
||||
ADS1115_DR_475SPS = 0x00C0 # 475 SPS
|
||||
ADS1115_DR_860SPS = 0x00E0 # 860 SPS
|
||||
|
||||
DR_VALUES = [ADS1115_DR_8SPS, ADS1115_DR_16SPS, ADS1115_DR_32SPS,
|
||||
ADS1115_DR_64SPS, ADS1115_DR_128SPS, ADS1115_DR_250SPS,
|
||||
ADS1115_DR_475SPS, ADS1115_DR_860SPS]
|
||||
DR_NAMES = ["8 SPS", "16 SPS", "32 SPS", "64 SPS",
|
||||
"128 SPS", "250 SPS", "475 SPS", "860 SPS"]
|
||||
|
||||
# --- 比较器 ---
|
||||
ADS1115_COMP_QUE_DISABLE = 0x0003
|
||||
|
||||
# --- 操作模式 ---
|
||||
ADS1115_MODE_CONTINUOUS = 0x0000
|
||||
ADS1115_MODE_SINGLE = 0x0100
|
||||
|
||||
# --- 启动转换 ---
|
||||
ADS1115_OS_START = 0x8000
|
||||
|
||||
|
||||
class ADS1115:
|
||||
"""ADS1115 16位ADC驱动"""
|
||||
|
||||
def __init__(self, bus=1, address=0x48, pga_idx=2, dr_idx=4):
|
||||
"""
|
||||
初始化 ADS1115
|
||||
|
||||
:param bus: I2C 总线号 (默认1)
|
||||
:param address: I2C 设备地址 (默认0x48)
|
||||
:param pga_idx: PGA增益索引 0~5 (默认2 = ±2.048V)
|
||||
:param dr_idx: 数据速率索引 0~7 (默认4 = 128SPS)
|
||||
"""
|
||||
self.address = address
|
||||
self.pga_idx = pga_idx if 0 <= pga_idx <= 5 else 2
|
||||
self.dr_idx = dr_idx if 0 <= dr_idx <= 7 else 4
|
||||
self.fs_voltage = PGA_FS[self.pga_idx]
|
||||
self.config = 0
|
||||
|
||||
# 初始化 I2C
|
||||
try:
|
||||
from smbus2 import SMBus
|
||||
self.bus = SMBus(bus)
|
||||
except ImportError:
|
||||
print("错误: 需要 smbus2 库,请执行: pip install smbus2", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
print(f"错误: I2C 总线 /dev/i2c-{bus} 不存在", file=sys.stderr)
|
||||
print("提示: 确保已启用 I2C (sudo raspi-config -> Interface Options -> I2C)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except PermissionError:
|
||||
print(f"错误: 无法访问 I2C 总线 /dev/i2c-{bus},请将用户加入 i2c 组", file=sys.stderr)
|
||||
print(" sudo usermod -aG i2c $USER", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化配置
|
||||
self._init_device()
|
||||
|
||||
def _write_word(self, reg, value):
|
||||
"""向寄存器写入2字节 (大端)"""
|
||||
high = (value >> 8) & 0xFF
|
||||
low = value & 0xFF
|
||||
self.bus.write_i2c_block_data(self.address, reg, [high, low])
|
||||
|
||||
def _read_word(self, reg):
|
||||
"""从寄存器读取2字节 (大端)"""
|
||||
data = self.bus.read_i2c_block_data(self.address, reg, 2)
|
||||
return (data[0] << 8) | data[1]
|
||||
|
||||
def _init_device(self):
|
||||
"""初始化 ADS1115 配置"""
|
||||
self.config = (ADS1115_MUX_AIN0_GND |
|
||||
PGA_VALUES[self.pga_idx] |
|
||||
ADS1115_MODE_SINGLE |
|
||||
DR_VALUES[self.dr_idx] |
|
||||
ADS1115_COMP_QUE_DISABLE)
|
||||
self._write_word(ADS1115_REG_CONFIG, self.config)
|
||||
|
||||
def read_single(self, channel):
|
||||
"""
|
||||
读取单端通道 (单次转换)
|
||||
|
||||
:param channel: 通道号 0~3 (对应 AIN0~AIN3)
|
||||
:return: 电压值 (V)
|
||||
"""
|
||||
if channel < 0 or channel > 3:
|
||||
raise ValueError(f"通道号无效: {channel} (有效: 0~3)")
|
||||
|
||||
# 设置通道并启动转换
|
||||
mux = ADS1115_MUX_AIN0_GND | (channel << 12) # AINch-GND
|
||||
cfg = (self.config & ~0x7000) | mux | ADS1115_OS_START
|
||||
self._write_word(ADS1115_REG_CONFIG, cfg)
|
||||
self.config = cfg
|
||||
|
||||
# 等待转换完成
|
||||
wait_ms = 1000 // (8 << self.dr_idx) + 3
|
||||
time.sleep(wait_ms / 1000.0)
|
||||
|
||||
# 读取转换值
|
||||
raw = self._read_word(ADS1115_REG_CONVERSION)
|
||||
raw_signed = raw if raw < 32768 else raw - 65536
|
||||
|
||||
# 转换为电压: V = raw * FS_VOLTAGE / 32768
|
||||
voltage = raw_signed * self.fs_voltage / 32768.0
|
||||
return voltage
|
||||
|
||||
def read_diff(self, pos, neg):
|
||||
"""
|
||||
读取差分通道 (单次转换)
|
||||
|
||||
支持组合: (0,1), (0,3), (1,3), (2,3)
|
||||
:param pos: 正极通道 (0~3)
|
||||
:param neg: 负极通道 (0~3)
|
||||
:return: 电压值 (V)
|
||||
"""
|
||||
mux_map = {
|
||||
(0, 1): ADS1115_MUX_AIN0_AIN1,
|
||||
(0, 3): ADS1115_MUX_AIN0_AIN3,
|
||||
(1, 3): ADS1115_MUX_AIN1_AIN3,
|
||||
(2, 3): ADS1115_MUX_AIN2_AIN3,
|
||||
}
|
||||
key = (pos, neg)
|
||||
if key not in mux_map:
|
||||
raise ValueError(f"不支持的差分组合: AIN{pos} - AIN{neg},支持: 0-1, 0-3, 1-3, 2-3")
|
||||
|
||||
mux = mux_map[key]
|
||||
cfg = (self.config & ~0x7000) | mux | ADS1115_OS_START
|
||||
self._write_word(ADS1115_REG_CONFIG, cfg)
|
||||
self.config = cfg
|
||||
|
||||
# 等待转换完成
|
||||
wait_ms = 1000 // (8 << self.dr_idx) + 3
|
||||
time.sleep(wait_ms / 1000.0)
|
||||
|
||||
raw = self._read_word(ADS1115_REG_CONVERSION)
|
||||
raw_signed = raw if raw < 32768 else raw - 65536
|
||||
|
||||
voltage = raw_signed * self.fs_voltage / 32768.0
|
||||
return voltage
|
||||
|
||||
def start_continuous(self, channel):
|
||||
"""启动连续模式"""
|
||||
if channel < 0 or channel > 3:
|
||||
raise ValueError(f"通道号无效: {channel}")
|
||||
cfg = (self.config & ~0x7100 |
|
||||
(ADS1115_MUX_AIN0_GND | (channel << 12)) |
|
||||
ADS1115_MODE_CONTINUOUS |
|
||||
ADS1115_COMP_QUE_DISABLE)
|
||||
self._write_word(ADS1115_REG_CONFIG, cfg)
|
||||
self.config = cfg
|
||||
|
||||
def read_continuous(self):
|
||||
"""连续模式读数"""
|
||||
raw = self._read_word(ADS1115_REG_CONVERSION)
|
||||
raw_signed = raw if raw < 32768 else raw - 65536
|
||||
return raw_signed * self.fs_voltage / 32768.0
|
||||
|
||||
def scan_all(self):
|
||||
"""扫描所有单端通道"""
|
||||
results = {}
|
||||
ch_names = ["AIN0", "AIN1", "AIN2", "AIN3"]
|
||||
for ch in range(4):
|
||||
v = self.read_single(ch)
|
||||
results[ch_names[ch]] = v
|
||||
return results
|
||||
|
||||
def print_config(self):
|
||||
"""打印配置寄存器详情"""
|
||||
config = self._read_word(ADS1115_REG_CONFIG)
|
||||
mux = (config >> 12) & 0x07
|
||||
pga = (config >> 9) & 0x07
|
||||
mode = (config >> 8) & 0x01
|
||||
dr = (config >> 5) & 0x07
|
||||
comp = config & 0x03
|
||||
|
||||
mux_names = {
|
||||
0: "AIN0-AIN1 (差分)",
|
||||
1: "AIN0-AIN3 (差分)",
|
||||
2: "AIN1-AIN3 (差分)",
|
||||
3: "AIN2-AIN3 (差分)",
|
||||
4: "AIN0-GND (单端)",
|
||||
5: "AIN1-GND (单端)",
|
||||
6: "AIN2-GND (单端)",
|
||||
7: "AIN3-GND (单端)",
|
||||
}
|
||||
|
||||
print(f"\n===== ADS1115 寄存器 =====")
|
||||
print(f" 配置寄存器: 0x{config:04X}")
|
||||
print(f" MUX: {mux} -> {mux_names.get(mux, '未知')}")
|
||||
if pga <= 5:
|
||||
print(f" PGA: {pga} -> {PGA_NAMES[pga]} (满量程 {PGA_FS[pga]:.3f}V)")
|
||||
else:
|
||||
print(f" PGA: {pga} -> 未知")
|
||||
print(f" 模式: {'单次' if mode else '连续'}")
|
||||
if dr <= 7:
|
||||
print(f" 速率: {dr} -> {DR_NAMES[dr]}")
|
||||
else:
|
||||
print(f" 速率: {dr} -> 未知")
|
||||
print(f" 比较器: {'禁用' if comp == 3 else '启用'}")
|
||||
|
||||
# 读取阈值
|
||||
lo_raw = self._read_word(ADS1115_REG_LO_THRESH)
|
||||
hi_raw = self._read_word(ADS1115_REG_HI_THRESH)
|
||||
lo_signed = lo_raw if lo_raw < 32768 else lo_raw - 65536
|
||||
hi_signed = hi_raw if hi_raw < 32768 else hi_raw - 65536
|
||||
lo_v = lo_signed * self.fs_voltage / 32768.0
|
||||
hi_v = hi_signed * self.fs_voltage / 32768.0
|
||||
print(f" 阈值: LO={lo_signed} ({lo_v:.6f}V), HI={hi_signed} ({hi_v:.6f}V)")
|
||||
|
||||
def close(self):
|
||||
"""关闭 I2C 总线"""
|
||||
self.bus.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Linux I2C 读取 TI ADS1115 16位 ADC 的命令行工具",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-b, --i2c-bus I2C 总线号 (默认 1, 对应 /dev/i2c-1)
|
||||
-a, --address I2C 设备地址 (默认 0x48)
|
||||
-p, --pga PGA 增益索引 0~5 (默认 2=±2.048V)
|
||||
0=±6.144V, 1=±4.096V, 2=±2.048V
|
||||
3=±1.024V, 4=±0.512V, 5=±0.256V
|
||||
-r, --rate 数据速率索引 0~7 (默认 4=128SPS)
|
||||
0=8, 1=16, 2=32, 3=64
|
||||
4=128, 5=250, 6=475, 7=860 (SPS)
|
||||
-c, --channel 单端通道号 0~3 (读取 AIN0~AIN3)
|
||||
--diff 差分模式,格式 "pos,neg" 如 "0,1"
|
||||
--scan 扫描所有单端通道
|
||||
--continuous 连续读取模式
|
||||
-n, --count 读取次数 (默认 1)
|
||||
--delay 每次读取间隔毫秒 (默认 10ms)
|
||||
--config 显示配置寄存器详情
|
||||
|
||||
--percent 输出基于 0-5V 范围的百分比 (0.00%~100.00%)
|
||||
--map 将 0-5V 百分比映射到自定义数值区间,格式 "min,max"
|
||||
例: --map -20,80 表示 0V=-20, 5V=80
|
||||
使用 --map 时自动启用百分比显示
|
||||
|
||||
使用示例:
|
||||
ads1115 # 读取 AIN0 (默认参数)
|
||||
ads1115 -c 2 # 读取 AIN2
|
||||
ads1115 --scan # 扫描所有单端通道
|
||||
ads1115 -c 0 -n 5 # 连续读 AIN0 5次
|
||||
ads1115 --diff 0,1 # 差分模式 AIN0-AIN1
|
||||
ads1115 -a 0x49 -p 1 -r 6 # 指定地址、PGA、速率
|
||||
ads1115 -c 0 --continuous -n 10 # 连续模式读10次
|
||||
ads1115 --config # 查看配置寄存器
|
||||
ads1115 --scan --config # 扫描并查看配置
|
||||
ads1115 -c 0 --percent # 读取 AIN0 并显示 0-5V 百分比
|
||||
ads1115 --scan --percent # 扫描所有通道并显示百分比
|
||||
ads1115 -c 0 --map -20,80 # 将 0-5V 映射到 -20~80 区间
|
||||
ads1115 --scan --map 0,100 # 扫描并映射到 0~100 区间
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-b", "--i2c-bus", type=int, default=1, help="I2C 总线号 (默认 1)")
|
||||
parser.add_argument("-a", "--address", type=lambda x: int(x, 0), default=0x48, help="I2C 设备地址 (默认 0x48)")
|
||||
parser.add_argument("-p", "--pga", type=int, default=2, choices=range(0, 6), metavar="{0..5}", help="PGA 增益索引 0~5 (默认 2)")
|
||||
parser.add_argument("-r", "--rate", type=int, default=4, choices=range(0, 8), metavar="{0..7}", help="数据速率索引 0~7 (默认 4)")
|
||||
parser.add_argument("-c", "--channel", type=int, default=None, choices=range(0, 4), metavar="{0..3}", help="单端通道号 0~3")
|
||||
parser.add_argument("--diff", type=str, default=None, help='差分模式,格式 "pos,neg" 如 "0,1"')
|
||||
parser.add_argument("--scan", action="store_true", help="扫描所有单端通道")
|
||||
parser.add_argument("--continuous", action="store_true", help="连续读取模式")
|
||||
parser.add_argument("-n", "--count", type=int, default=1, help="读取次数 (默认 1)")
|
||||
parser.add_argument("--delay", type=int, default=10, help="每次读取间隔毫秒 (默认 10ms)")
|
||||
parser.add_argument("--config", action="store_true", help="显示配置寄存器详情")
|
||||
parser.add_argument("--percent", action="store_true", help="输出基于 0-5V 范围的百分比")
|
||||
parser.add_argument("--map", type=str, default=None, metavar="MIN,MAX",
|
||||
help='将 0-5V 百分比映射到数值区间,格式 "min,max" 如 "-20,80"')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- 解析 --map 参数 ---
|
||||
map_range = None
|
||||
if args.map is not None:
|
||||
try:
|
||||
parts = args.map.split(",")
|
||||
if len(parts) != 2:
|
||||
raise ValueError
|
||||
map_min, map_max = float(parts[0]), float(parts[1])
|
||||
if map_min == map_max:
|
||||
raise ValueError("映射区间两端不能相同")
|
||||
map_range = (map_min, map_max)
|
||||
args.percent = True # --map 自动启用百分比显示
|
||||
except ValueError as e:
|
||||
err_msg = str(e) if str(e) != "" else "格式无效"
|
||||
print(f"错误: --map 参数无效 '{args.map}',请使用 'min,max' 格式,如 '-20,80' ({err_msg})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
adc = ADS1115(bus=args.i2c_bus, address=args.address,
|
||||
pga_idx=args.pga, dr_idx=args.rate)
|
||||
|
||||
def pct(v):
|
||||
"""返回 0-5V 百分比/映射后缀字符串"""
|
||||
if not args.percent:
|
||||
return ""
|
||||
p = v / 5.0 * 100.0
|
||||
suffix = f" [{p:6.2f}%]"
|
||||
if map_range is not None:
|
||||
mapped = map_range[0] + (p / 100.0) * (map_range[1] - map_range[0])
|
||||
suffix += f" (映射: {mapped:.2f})"
|
||||
return suffix
|
||||
|
||||
has_action = False
|
||||
|
||||
# --- 单端通道读取 ---
|
||||
if args.channel is not None and args.diff is None:
|
||||
has_action = True
|
||||
ch_names = ["AIN0", "AIN1", "AIN2", "AIN3"]
|
||||
ch_name = ch_names[args.channel]
|
||||
|
||||
if args.continuous:
|
||||
print(f"连续模式读取 {ch_name} ({args.count} 次):")
|
||||
adc.start_continuous(args.channel)
|
||||
time.sleep(0.05)
|
||||
for i in range(args.count):
|
||||
v = adc.read_continuous()
|
||||
print(f" [{i}] {ch_name}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
if i < args.count - 1:
|
||||
time.sleep(args.delay / 1000.0)
|
||||
else:
|
||||
print(f"读取 {ch_name} ({args.count} 次):")
|
||||
for i in range(args.count):
|
||||
v = adc.read_single(args.channel)
|
||||
print(f" [{i}] {ch_name}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
if i < args.count - 1:
|
||||
time.sleep(args.delay / 1000.0)
|
||||
|
||||
# --- 差分模式 ---
|
||||
if args.diff is not None:
|
||||
has_action = True
|
||||
try:
|
||||
parts = args.diff.split(",")
|
||||
if len(parts) != 2:
|
||||
raise ValueError
|
||||
pos, neg = int(parts[0]), int(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
print(f"错误: 差分格式无效 '{args.diff}',请使用 'pos,neg' 格式,如 '0,1'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"差分读取 AIN{pos} - AIN{neg} ({args.count} 次):")
|
||||
for i in range(args.count):
|
||||
v = adc.read_diff(pos, neg)
|
||||
print(f" [{i}] AIN{pos}-AIN{neg}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
if i < args.count - 1:
|
||||
time.sleep(args.delay / 1000.0)
|
||||
|
||||
# --- 扫描所有单端通道 ---
|
||||
if args.scan:
|
||||
has_action = True
|
||||
print("\n===== 单端通道扫描 =====")
|
||||
results = adc.scan_all()
|
||||
for ch, v in results.items():
|
||||
print(f" {ch}: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
|
||||
# --- 显示配置 ---
|
||||
if args.config:
|
||||
has_action = True
|
||||
adc.print_config()
|
||||
|
||||
# --- 默认行为: 读 AIN0 ---
|
||||
if not has_action:
|
||||
v = adc.read_single(0)
|
||||
print(f"AIN0: {v:+.6f} V ({v*1000:.3f} mV){pct(v)}")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n用户中断")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"错误: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if 'adc' in locals():
|
||||
adc.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+420
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ADS1256: Linux SPI 读取 TI ADS1256 24位 ΔΣ ADC 的命令行工具
|
||||
"""
|
||||
|
||||
import spidev
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ADS1256 寄存器地址
|
||||
# ============================================================
|
||||
ADS1256_REG_STATUS = 0x00
|
||||
ADS1256_REG_MUX = 0x01
|
||||
ADS1256_REG_ADCON = 0x02
|
||||
ADS1256_REG_DRATE = 0x03
|
||||
ADS1256_REG_IO = 0x04
|
||||
ADS1256_REG_OFC0 = 0x05
|
||||
ADS1256_REG_OFC1 = 0x06
|
||||
ADS1256_REG_OFC2 = 0x07
|
||||
ADS1256_REG_FSC0 = 0x08
|
||||
ADS1256_REG_FSC1 = 0x09
|
||||
ADS1256_REG_FSC2 = 0x0A
|
||||
|
||||
# ============================================================
|
||||
# ADS1256 命令
|
||||
# ============================================================
|
||||
ADS1256_CMD_WAKEUP = 0x00
|
||||
ADS1256_CMD_RDATA = 0x01
|
||||
ADS1256_CMD_RDATAC = 0x03
|
||||
ADS1256_CMD_SDATAC = 0x0F
|
||||
ADS1256_CMD_RREG = 0x10
|
||||
ADS1256_CMD_WREG = 0x50
|
||||
ADS1256_CMD_SELFCAL = 0xF0
|
||||
ADS1256_CMD_SELFOCAL = 0xF1
|
||||
ADS1256_CMD_SELFGCAL = 0xF2
|
||||
ADS1256_CMD_SYSOCAL = 0xF3
|
||||
ADS1256_CMD_SYSGCAL = 0xF4
|
||||
ADS1256_CMD_SYNC = 0xFC
|
||||
ADS1256_CMD_STANDBY = 0xFD
|
||||
ADS1256_CMD_RESET = 0xFE
|
||||
|
||||
# ============================================================
|
||||
# 数据速率 (DRATE 寄存器值)
|
||||
# ============================================================
|
||||
DRATE_MAP = {
|
||||
30000: 0xF0, 15000: 0xE0, 7500: 0xD0, 3750: 0xC0,
|
||||
2000: 0xB0, 1000: 0xA0, 500: 0x90, 100: 0x80,
|
||||
60: 0x70, 50: 0x60, 30: 0x50, 25: 0x40,
|
||||
15: 0x30, 10: 0x20, 5: 0x10,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# PGA 增益
|
||||
# ============================================================
|
||||
PGA_MAP = {1: 0x00, 2: 0x01, 4: 0x02, 8: 0x03, 16: 0x04, 32: 0x05, 64: 0x06}
|
||||
|
||||
VREF = 2.5 # 参考电压
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SPI 接口类 (基于 spidev)
|
||||
# ============================================================
|
||||
class SpiDev:
|
||||
def __init__(self, device="/dev/spidev0.0", speed=1920000):
|
||||
self.device = device
|
||||
self.speed = speed
|
||||
self.spi = None
|
||||
self._bus = None
|
||||
self._cs = None
|
||||
|
||||
def _parse_device(self):
|
||||
"""从设备路径解析 bus 和 cs, 如 /dev/spidev0.0 -> (0, 0)"""
|
||||
base = os.path.basename(self.device) # spidev0.0
|
||||
parts = base.replace("spidev", "").split(".")
|
||||
return int(parts[0]), int(parts[1])
|
||||
|
||||
def open(self):
|
||||
self._bus, self._cs = self._parse_device()
|
||||
self.spi = spidev.SpiDev()
|
||||
self.spi.open(self._bus, self._cs)
|
||||
self.spi.mode = 1 # SPI 模式 1: CPOL=0, CPHA=1
|
||||
self.spi.bits_per_word = 8
|
||||
self.spi.max_speed_hz = self.speed
|
||||
|
||||
# 读取验证
|
||||
print(f"SPI 初始化: {self.device}, 模式={self.spi.mode}, "
|
||||
f"位数={self.spi.bits_per_word}, 速度={self.spi.max_speed_hz} Hz",
|
||||
file=sys.stderr)
|
||||
|
||||
def transfer(self, tx_data, rx_len=None):
|
||||
"""SPI 全双工传输"""
|
||||
if rx_len is None:
|
||||
rx_len = len(tx_data)
|
||||
|
||||
# spidev.xfer2 返回接收到的字节
|
||||
rx = self.spi.xfer2(list(tx_data), self.speed, 1) # 1 = SPI_MODE_1
|
||||
# 如果 rx_len 大于发送长度,补充虚拟字节
|
||||
if rx_len > len(tx_data):
|
||||
extra = self.spi.xfer2([0xFF] * (rx_len - len(tx_data)), self.speed, 1)
|
||||
rx.extend(extra)
|
||||
return list(rx[:rx_len])
|
||||
|
||||
def write_byte(self, byte):
|
||||
self.spi.xfer2([byte], self.speed, 1)
|
||||
# 不需要接收
|
||||
|
||||
def read_byte(self):
|
||||
rx = self.spi.xfer2([0xFF], self.speed, 1)
|
||||
return rx[0]
|
||||
|
||||
def close(self):
|
||||
if self.spi:
|
||||
self.spi.close()
|
||||
self.spi = None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ADS1256 控制类
|
||||
# ============================================================
|
||||
class ADS1256:
|
||||
def __init__(self, spi, pga=2, drate=1000):
|
||||
self.spi = spi
|
||||
self.pga = pga
|
||||
self.drate = drate
|
||||
self.pga_reg = PGA_MAP.get(pga, 0x01)
|
||||
|
||||
def wait_drdy(self):
|
||||
time.sleep(0.0001) # 100us 模拟等待
|
||||
|
||||
def reset(self):
|
||||
self.spi.write_byte(ADS1256_CMD_RESET)
|
||||
time.sleep(0.001)
|
||||
self.spi.write_byte(ADS1256_CMD_WAKEUP)
|
||||
time.sleep(0.001)
|
||||
|
||||
def write_reg(self, reg, value):
|
||||
self.spi.write_byte(ADS1256_CMD_WREG | reg)
|
||||
self.spi.write_byte(0x00) # 1 byte
|
||||
self.spi.write_byte(value)
|
||||
|
||||
def read_reg(self, reg):
|
||||
self.spi.write_byte(ADS1256_CMD_RREG | reg)
|
||||
self.spi.write_byte(0x00)
|
||||
return self.spi.read_byte()
|
||||
|
||||
def set_channel(self, pos, neg=0x08):
|
||||
"""设置输入通道, neg=0x08 表示 AINCOM (单端模式)"""
|
||||
mux = ((pos & 0x07) << 4) | (neg & 0x07)
|
||||
self.write_reg(ADS1256_REG_MUX, mux)
|
||||
time.sleep(0.00001)
|
||||
|
||||
def init(self):
|
||||
print("初始化 ADS1256...", file=sys.stderr)
|
||||
|
||||
# 复位
|
||||
self.reset()
|
||||
time.sleep(0.01)
|
||||
|
||||
# STATUS: ACAL=1 (自动校准), ORDER=0 (MSB), BUFEN=0
|
||||
self.write_reg(ADS1256_REG_STATUS, 0x14)
|
||||
|
||||
# MUX: 默认 AIN0 + AINCOM
|
||||
self.set_channel(0, 0x08)
|
||||
|
||||
# ADCON: PGA 增益
|
||||
self.write_reg(ADS1256_REG_ADCON, self.pga_reg)
|
||||
|
||||
# DRATE: 数据速率
|
||||
drate_val = DRATE_MAP.get(self.drate, 0xA0)
|
||||
self.write_reg(ADS1256_REG_DRATE, drate_val)
|
||||
|
||||
# 自校准
|
||||
print("自校准中...", file=sys.stderr)
|
||||
self.spi.write_byte(ADS1256_CMD_SELFCAL)
|
||||
time.sleep(0.5) # 等待校准完成 (~400ms @ 1000SPS)
|
||||
|
||||
# 验证寄存器
|
||||
status = self.read_reg(ADS1256_REG_STATUS)
|
||||
mux = self.read_reg(ADS1256_REG_MUX)
|
||||
adcon = self.read_reg(ADS1256_REG_ADCON)
|
||||
drate = self.read_reg(ADS1256_REG_DRATE)
|
||||
print(f"寄存器: STATUS=0x{status:02X}, MUX=0x{mux:02X}, "
|
||||
f"ADCON=0x{adcon:02X}, DRATE=0x{drate:02X}", file=sys.stderr)
|
||||
print("ADS1256 初始化完成!", file=sys.stderr)
|
||||
|
||||
def read_data(self):
|
||||
"""单次读取 24 位数据, 返回有符号整数值"""
|
||||
self.wait_drdy()
|
||||
self.spi.write_byte(ADS1256_CMD_RDATA)
|
||||
buf = self.spi.transfer([0xFF, 0xFF, 0xFF], 3)
|
||||
|
||||
# 组合 24 位
|
||||
value = (buf[0] << 16) | (buf[1] << 8) | buf[2]
|
||||
|
||||
# 符号扩展 (24 位 -> 32 位)
|
||||
if value & 0x800000:
|
||||
value |= 0xFF000000
|
||||
|
||||
# Python 中转为有符号
|
||||
if value >= 0x80000000:
|
||||
value -= 0x100000000
|
||||
|
||||
return value
|
||||
|
||||
def raw_to_voltage(self, raw):
|
||||
"""将原始值转换为电压"""
|
||||
return raw * VREF / (1 << 23) / self.pga
|
||||
|
||||
def read_channel(self, ch):
|
||||
"""读取指定单端通道"""
|
||||
self.set_channel(ch, 0x08) # AINch vs AINCOM
|
||||
time.sleep(0.00001)
|
||||
|
||||
self.spi.write_byte(ADS1256_CMD_SYNC)
|
||||
time.sleep(0.00001)
|
||||
self.spi.write_byte(ADS1256_CMD_WAKEUP)
|
||||
|
||||
# 等待转换完成 (1000SPS = 1ms)
|
||||
time.sleep(0.002)
|
||||
|
||||
return self.read_data()
|
||||
|
||||
def read_diff(self, pos, neg):
|
||||
"""读取差分通道"""
|
||||
self.set_channel(pos, neg)
|
||||
time.sleep(0.00001)
|
||||
|
||||
self.spi.write_byte(ADS1256_CMD_SYNC)
|
||||
time.sleep(0.00001)
|
||||
self.spi.write_byte(ADS1256_CMD_WAKEUP)
|
||||
|
||||
time.sleep(0.002)
|
||||
|
||||
return self.read_data()
|
||||
|
||||
def scan_all(self):
|
||||
"""扫描所有 8 个单端通道"""
|
||||
results = []
|
||||
for ch in range(8):
|
||||
raw = self.read_channel(ch)
|
||||
volts = self.raw_to_voltage(raw)
|
||||
results.append((ch, raw, volts))
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主程序
|
||||
# ============================================================
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="ADS1256: Linux SPI 读取 TI ADS1256 24位 ΔΣ ADC 的命令行工具",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
参数说明:
|
||||
-d, --device SPI 设备路径 (默认: /dev/spidev0.0)
|
||||
-s, --speed SPI 时钟频率 (Hz) (默认: 1920000)
|
||||
-m, --mode 工作模式: single/diff/scan (默认: single)
|
||||
-c, --channel 单端通道号 0-7 (默认: 0)
|
||||
--pos 差分正端 0-7 (默认: 0)
|
||||
--neg 差分负端 0-7 (默认: 1)
|
||||
-p, --pga PGA 增益 1/2/4/8/16/32/64 (默认: 2)
|
||||
-r, --drate 数据速率 SPS (默认: 1000)
|
||||
可选: 5,10,15,25,30,50,60,100,500,1000,
|
||||
2000,3750,7500,15000,30000
|
||||
-n, --count 采样次数 (默认: 1)
|
||||
--avg 输出均值 (多采样时)
|
||||
-o, --output 输出格式: raw/voltage/both (默认: both)
|
||||
|
||||
使用示例:
|
||||
# 读取 AIN0 单端通道
|
||||
ads1256 -c 0
|
||||
|
||||
# 读取 AIN0 差分 AIN1
|
||||
ads1256 -m diff --pos 0 --neg 1
|
||||
|
||||
# 扫描所有 8 个通道
|
||||
ads1256 -m scan
|
||||
|
||||
# 设置 PGA=1, 速率为 30SPS, 采样 10 次并输出均值
|
||||
ads1256 -c 0 -p 1 -r 30 -n 10 --avg
|
||||
|
||||
# 指定 SPI 设备
|
||||
ads1256 -d /dev/spidev0.1 -s 1000000 -c 3
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("-d", "--device", default="/dev/spidev0.0",
|
||||
help="SPI 设备路径 (默认: /dev/spidev0.0)")
|
||||
parser.add_argument("-s", "--speed", type=int, default=1920000,
|
||||
help="SPI 时钟频率 Hz (默认: 1920000)")
|
||||
parser.add_argument("-m", "--mode", choices=["single", "diff", "scan"],
|
||||
default="single", help="工作模式 (默认: single)")
|
||||
parser.add_argument("-c", "--channel", type=int, default=0, choices=range(8),
|
||||
help="单端通道号 0-7 (默认: 0)")
|
||||
parser.add_argument("--pos", type=int, default=0, choices=range(8),
|
||||
help="差分正端 0-7 (默认: 0)")
|
||||
parser.add_argument("--neg", type=int, default=1, choices=range(8),
|
||||
help="差分负端 0-7 (默认: 1)")
|
||||
parser.add_argument("-p", "--pga", type=int, default=2,
|
||||
choices=[1, 2, 4, 8, 16, 32, 64],
|
||||
help="PGA 增益 (默认: 2)")
|
||||
parser.add_argument("-r", "--drate", type=int, default=1000,
|
||||
help="数据速率 SPS (默认: 1000)")
|
||||
parser.add_argument("-n", "--count", type=int, default=1,
|
||||
help="采样次数 (默认: 1)")
|
||||
parser.add_argument("--avg", action="store_true",
|
||||
help="输出均值")
|
||||
parser.add_argument("-o", "--output", choices=["raw", "voltage", "both"],
|
||||
default="both", help="输出格式 (默认: both)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 验证 drate
|
||||
if args.drate not in DRATE_MAP:
|
||||
print(f"错误: 不支持的数据速率 {args.drate} SPS", file=sys.stderr)
|
||||
print(f"支持: {sorted(DRATE_MAP.keys())}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 验证 SPI 设备
|
||||
if not os.path.exists(args.device):
|
||||
print(f"错误: SPI 设备 {args.device} 不存在", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化 SPI
|
||||
spi = SpiDev(device=args.device, speed=args.speed)
|
||||
try:
|
||||
spi.open()
|
||||
except Exception as e:
|
||||
print(f"错误: SPI 初始化失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化 ADS1256
|
||||
adc = ADS1256(spi, pga=args.pga, drate=args.drate)
|
||||
try:
|
||||
adc.init()
|
||||
except Exception as e:
|
||||
print(f"ADS1256 初始化失败: {e}", file=sys.stderr)
|
||||
spi.close()
|
||||
sys.exit(1)
|
||||
|
||||
# 执行读取
|
||||
try:
|
||||
if args.mode == "scan":
|
||||
# 扫描所有通道
|
||||
results = adc.scan_all()
|
||||
print("\n===== 多通道扫描 =====")
|
||||
for ch, raw, volts in results:
|
||||
_print_result(ch, raw, volts, args.output)
|
||||
|
||||
elif args.mode == "diff":
|
||||
# 差分模式
|
||||
pos, neg = args.pos, args.neg
|
||||
channel_label = f"AIN{pos}-AIN{neg}"
|
||||
raw_values = []
|
||||
for i in range(args.count):
|
||||
raw = adc.read_diff(pos, neg)
|
||||
raw_values.append(raw)
|
||||
volts = adc.raw_to_voltage(raw)
|
||||
_print_sample(i, raw, volts, args.output, channel_label)
|
||||
|
||||
if args.count > 1 and args.avg:
|
||||
avg_raw = sum(raw_values) // len(raw_values)
|
||||
avg_volts = adc.raw_to_voltage(avg_raw)
|
||||
print(f"\n--- 均值 ---")
|
||||
_print_result(channel_label, avg_raw, avg_volts, args.output)
|
||||
|
||||
else:
|
||||
# 单端模式
|
||||
ch = args.channel
|
||||
channel_label = f"AIN{ch}"
|
||||
raw_values = []
|
||||
for i in range(args.count):
|
||||
raw = adc.read_channel(ch)
|
||||
raw_values.append(raw)
|
||||
volts = adc.raw_to_voltage(raw)
|
||||
_print_sample(i, raw, volts, args.output, channel_label)
|
||||
|
||||
if args.count > 1 and args.avg:
|
||||
avg_raw = sum(raw_values) // len(raw_values)
|
||||
avg_volts = adc.raw_to_voltage(avg_raw)
|
||||
print(f"\n--- 均值 ---")
|
||||
_print_result(channel_label, avg_raw, avg_volts, args.output)
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取错误: {e}", file=sys.stderr)
|
||||
finally:
|
||||
spi.close()
|
||||
|
||||
|
||||
def _print_sample(index, raw, volts, output_format, label=""):
|
||||
if output_format == "raw":
|
||||
print(f"[{index}] {label}: raw={raw}")
|
||||
elif output_format == "voltage":
|
||||
print(f"[{index}] {label}: {_fmt_voltage(volts)}")
|
||||
else:
|
||||
print(f"[{index}] {label}: raw={raw:7d}, {_fmt_voltage(volts)}")
|
||||
|
||||
|
||||
def _print_result(label, raw, volts, output_format):
|
||||
if output_format == "raw":
|
||||
print(f" {label}: raw={raw}")
|
||||
elif output_format == "voltage":
|
||||
print(f" {label}: {_fmt_voltage(volts)}")
|
||||
else:
|
||||
print(f" {label}: raw={raw:7d}, {_fmt_voltage(volts)}")
|
||||
|
||||
|
||||
def _fmt_voltage(volts):
|
||||
if abs(volts) >= 1.0:
|
||||
return f"电压 = {volts:+.6f} V"
|
||||
else:
|
||||
return f"电压 = {volts*1000:+.4f} mV"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+616
@@ -0,0 +1,616 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AES-CBC/GCM 命令行加解密工具 - 支持文本和文件的加密解密,支持批量处理
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from Crypto.Random import get_random_bytes
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# 定义分块大小(64MB)
|
||||
CHUNK_SIZE = 64 * 1024 * 1024
|
||||
|
||||
|
||||
class AESCli:
|
||||
"""AES命令行加解密工具"""
|
||||
|
||||
def __init__(self, mode='CBC'):
|
||||
"""
|
||||
初始化AES工具
|
||||
:param mode: 加密模式 'CBC' 或 'GCM'
|
||||
"""
|
||||
self.mode = mode
|
||||
self.key = None
|
||||
self.iv = None
|
||||
|
||||
def set_key_iv(self, key_hex, iv_hex):
|
||||
"""
|
||||
设置密钥和IV/Nonce
|
||||
:param key_hex: 十六进制密钥字符串
|
||||
:param iv_hex: 十六进制IV/Nonce字符串
|
||||
"""
|
||||
try:
|
||||
if not key_hex:
|
||||
raise ValueError("密钥不能为空")
|
||||
if not iv_hex:
|
||||
raise ValueError("IV/Nonce不能为空")
|
||||
|
||||
self.key = bytes.fromhex(key_hex)
|
||||
self.iv = bytes.fromhex(iv_hex)
|
||||
|
||||
if len(self.key) not in [16, 24, 32]:
|
||||
raise ValueError(f"密钥长度必须为16、24或32字节,当前为{len(self.key)}字节")
|
||||
|
||||
if self.mode == "CBC" and len(self.iv) != 16:
|
||||
raise ValueError(f"CBC模式IV长度必须为16字节,当前为{len(self.iv)}字节")
|
||||
elif self.mode == "GCM" and len(self.iv) != 12:
|
||||
raise ValueError(f"GCM模式Nonce长度必须为12字节,当前为{len(self.iv)}字节")
|
||||
|
||||
return True
|
||||
except ValueError as e:
|
||||
print(f"错误: {e}")
|
||||
return False
|
||||
|
||||
def generate_random_key(self, key_size=16):
|
||||
"""生成随机密钥"""
|
||||
if key_size not in [16, 24, 32]:
|
||||
print(f"错误: 密钥大小必须为16、24或32,当前为{key_size}")
|
||||
return None
|
||||
|
||||
self.key = get_random_bytes(key_size)
|
||||
key_hex = self.key.hex()
|
||||
print(f"已生成 {key_size} 字节随机密钥: {key_hex}")
|
||||
return key_hex
|
||||
|
||||
def generate_random_iv(self):
|
||||
"""生成随机IV或Nonce"""
|
||||
if self.mode == "CBC":
|
||||
iv_size = 16
|
||||
label = "IV"
|
||||
else: # GCM
|
||||
iv_size = 12
|
||||
label = "Nonce"
|
||||
|
||||
self.iv = get_random_bytes(iv_size)
|
||||
iv_hex = self.iv.hex()
|
||||
print(f"已生成 {iv_size} 字节随机{label}: {iv_hex}")
|
||||
return iv_hex
|
||||
|
||||
def encrypt_text(self, plaintext):
|
||||
"""加密文本"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
if self.mode == "CBC":
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size))
|
||||
encrypted = base64.b64encode(ciphertext).decode('utf-8')
|
||||
else: # GCM
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8'))
|
||||
# 存储格式: ciphertext + tag
|
||||
combined = ciphertext + tag
|
||||
encrypted = base64.b64encode(combined).decode('utf-8')
|
||||
|
||||
return encrypted
|
||||
|
||||
def decrypt_text(self, encrypted_text):
|
||||
"""解密文本"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
combined = base64.b64decode(encrypted_text)
|
||||
|
||||
if self.mode == "CBC":
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
decrypted = unpad(cipher.decrypt(combined), AES.block_size)
|
||||
else: # GCM
|
||||
# 分离ciphertext和tag (最后16字节是tag)
|
||||
if len(combined) < 16:
|
||||
raise ValueError("密文格式无效:缺少认证标签")
|
||||
tag = combined[-16:]
|
||||
ciphertext = combined[:-16]
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
|
||||
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
|
||||
|
||||
return decrypted.decode('utf-8')
|
||||
|
||||
def encrypt_file(self, input_path, output_path, delete_source=False):
|
||||
"""加密文件"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"输入文件不存在: {input_path}")
|
||||
|
||||
file_size = os.path.getsize(input_path)
|
||||
print(f"开始加密文件: {input_path} ({self._format_size(file_size)})")
|
||||
|
||||
if self.mode == "CBC":
|
||||
self._encrypt_file_cbc(input_path, output_path)
|
||||
else: # GCM
|
||||
self._encrypt_file_gcm(input_path, output_path)
|
||||
|
||||
print(f"文件加密完成,保存到: {output_path}")
|
||||
|
||||
if delete_source:
|
||||
try:
|
||||
os.remove(input_path)
|
||||
print(f"已删除源文件: {input_path}")
|
||||
except Exception as e:
|
||||
print(f"警告: 删除源文件失败: {e}")
|
||||
|
||||
def _encrypt_file_cbc(self, input_path, output_path):
|
||||
"""CBC模式加密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_data = infile.read()
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
|
||||
ciphertext = cipher.encrypt(pad(file_data, AES.block_size))
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
# 写入IV + 密文
|
||||
outfile.write(self.iv + ciphertext)
|
||||
|
||||
def _encrypt_file_gcm(self, input_path, output_path):
|
||||
"""GCM模式加密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_data = infile.read()
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(file_data)
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
# 写入IV + 密文 + tag
|
||||
outfile.write(self.iv + ciphertext + tag)
|
||||
|
||||
def decrypt_file(self, input_path, output_path, delete_source=False):
|
||||
"""解密文件"""
|
||||
if not self.key or not self.iv:
|
||||
raise ValueError("请先设置密钥和IV/Nonce")
|
||||
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"输入文件不存在: {input_path}")
|
||||
|
||||
file_size = os.path.getsize(input_path)
|
||||
print(f"开始解密文件: {input_path} ({self._format_size(file_size)})")
|
||||
|
||||
if self.mode == "CBC":
|
||||
self._decrypt_file_cbc(input_path, output_path)
|
||||
else: # GCM
|
||||
self._decrypt_file_gcm(input_path, output_path)
|
||||
|
||||
print(f"文件解密完成,保存到: {output_path}")
|
||||
|
||||
if delete_source:
|
||||
try:
|
||||
os.remove(input_path)
|
||||
print(f"已删除加密文件: {input_path}")
|
||||
except Exception as e:
|
||||
print(f"警告: 删除加密文件失败: {e}")
|
||||
|
||||
def _decrypt_file_cbc(self, input_path, output_path):
|
||||
"""CBC模式解密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_iv = infile.read(16)
|
||||
ciphertext = infile.read()
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_CBC, file_iv)
|
||||
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
outfile.write(decrypted_data)
|
||||
|
||||
def _decrypt_file_gcm(self, input_path, output_path):
|
||||
"""GCM模式解密文件"""
|
||||
with open(input_path, 'rb') as infile:
|
||||
file_iv = infile.read(12)
|
||||
remaining = infile.read()
|
||||
|
||||
if len(remaining) < 16:
|
||||
raise ValueError("加密文件格式无效:缺少认证标签")
|
||||
|
||||
tag = remaining[-16:]
|
||||
ciphertext = remaining[:-16]
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=file_iv)
|
||||
decrypted_data = cipher.decrypt_and_verify(ciphertext, tag)
|
||||
|
||||
with open(output_path, 'wb') as outfile:
|
||||
outfile.write(decrypted_data)
|
||||
|
||||
def batch_encrypt(self, files, delete_source=False, recursive=False):
|
||||
"""批量加密文件"""
|
||||
total = len(files)
|
||||
success = 0
|
||||
failed = []
|
||||
|
||||
print(f"\n开始批量加密 {total} 个文件...")
|
||||
|
||||
for i, input_file in enumerate(files, 1):
|
||||
try:
|
||||
if not os.path.exists(input_file):
|
||||
failed.append(f"{input_file} (文件不存在)")
|
||||
continue
|
||||
|
||||
output_file = self._generate_encrypt_filename(input_file)
|
||||
print(f"[{i}/{total}] 加密: {input_file}")
|
||||
|
||||
self.encrypt_file(input_file, output_file, delete_source)
|
||||
success += 1
|
||||
|
||||
except Exception as e:
|
||||
failed.append(f"{input_file} ({str(e)})")
|
||||
print(f" ✗ 失败: {e}")
|
||||
|
||||
self._print_batch_result("批量加密", success, len(failed), failed)
|
||||
|
||||
def batch_decrypt(self, files, delete_source=False, recursive=False):
|
||||
"""批量解密文件"""
|
||||
total = len(files)
|
||||
success = 0
|
||||
failed = []
|
||||
|
||||
print(f"\n开始批量解密 {total} 个文件...")
|
||||
|
||||
for i, input_file in enumerate(files, 1):
|
||||
try:
|
||||
if not os.path.exists(input_file):
|
||||
failed.append(f"{input_file} (文件不存在)")
|
||||
continue
|
||||
|
||||
output_file = self._generate_decrypt_filename(input_file)
|
||||
print(f"[{i}/{total}] 解密: {input_file}")
|
||||
|
||||
self.decrypt_file(input_file, output_file, delete_source)
|
||||
success += 1
|
||||
|
||||
except Exception as e:
|
||||
failed.append(f"{input_file} ({str(e)})")
|
||||
print(f" ✗ 失败: {e}")
|
||||
|
||||
self._print_batch_result("批量解密", success, len(failed), failed)
|
||||
|
||||
def _generate_encrypt_filename(self, input_file):
|
||||
"""生成加密输出文件名"""
|
||||
if input_file.endswith('.encrypted'):
|
||||
return input_file
|
||||
else:
|
||||
return f"{input_file}.encrypted"
|
||||
|
||||
def _generate_decrypt_filename(self, encrypted_file):
|
||||
"""生成解密输出文件名"""
|
||||
if encrypted_file.endswith('.encrypted'):
|
||||
return encrypted_file[:-10]
|
||||
else:
|
||||
return f"{encrypted_file}.decrypted"
|
||||
|
||||
def _print_batch_result(self, operation, success, fail_count, failed_files):
|
||||
"""打印批量处理结果"""
|
||||
print(f"\n{operation}完成:")
|
||||
print(f" 成功: {success} 个文件")
|
||||
print(f" 失败: {fail_count} 个文件")
|
||||
|
||||
if failed_files:
|
||||
print("\n失败的文件:")
|
||||
for f in failed_files[:20]:
|
||||
print(f" - {f}")
|
||||
if len(failed_files) > 20:
|
||||
print(f" ... 还有 {len(failed_files) - 20} 个文件")
|
||||
|
||||
@staticmethod
|
||||
def _format_size(size_bytes):
|
||||
"""格式化文件大小"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.2f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.2f} PB"
|
||||
|
||||
def save_key_iv(self, filepath):
|
||||
"""保存密钥和IV到JSON文件"""
|
||||
if not self.key and not self.iv:
|
||||
print("警告: 没有可保存的密钥或IV")
|
||||
return False
|
||||
|
||||
data = {
|
||||
"mode": self.mode,
|
||||
"key": self.key.hex() if self.key else "",
|
||||
"iv": self.iv.hex() if self.iv else "",
|
||||
"key_size": len(self.key) if self.key else 0,
|
||||
"description": f"AES-{self.mode}密钥和{'IV' if self.mode == 'CBC' else 'Nonce'}"
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"密钥和IV已保存到: {filepath}")
|
||||
return True
|
||||
|
||||
def load_key_iv(self, filepath):
|
||||
"""从JSON文件加载密钥和IV"""
|
||||
if not os.path.exists(filepath):
|
||||
print(f"错误: 文件不存在: {filepath}")
|
||||
return False
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 加载加密模式
|
||||
if 'mode' in data and data['mode'] in ['CBC', 'GCM']:
|
||||
self.mode = data['mode']
|
||||
print(f"加载加密模式: {self.mode}")
|
||||
|
||||
# 加载密钥
|
||||
if 'key' in data and data['key']:
|
||||
key_hex = data['key']
|
||||
self.key = bytes.fromhex(key_hex)
|
||||
print(f"加载密钥: {len(self.key)} 字节")
|
||||
|
||||
# 加载IV
|
||||
if 'iv' in data and data['iv']:
|
||||
iv_hex = data['iv']
|
||||
self.iv = bytes.fromhex(iv_hex)
|
||||
label = 'IV' if self.mode == 'CBC' else 'Nonce'
|
||||
print(f"加载{label}: {len(self.iv)} 字节")
|
||||
|
||||
print(f"密钥和IV已从 {filepath} 加载")
|
||||
return True
|
||||
|
||||
|
||||
def collect_files(paths, pattern=None, recursive=False):
|
||||
"""收集文件列表"""
|
||||
files = []
|
||||
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
files.append(os.path.abspath(path))
|
||||
elif os.path.isdir(path):
|
||||
if recursive:
|
||||
for root, dirs, filenames in os.walk(path):
|
||||
for filename in filenames:
|
||||
filepath = os.path.join(root, filename)
|
||||
if pattern is None or filename.endswith(pattern):
|
||||
files.append(os.path.abspath(filepath))
|
||||
else:
|
||||
for filename in os.listdir(path):
|
||||
filepath = os.path.join(path, filename)
|
||||
if os.path.isfile(filepath):
|
||||
if pattern is None or filename.endswith(pattern):
|
||||
files.append(os.path.abspath(filepath))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='AES-CBC/GCM 命令行加解密工具 - 支持文本和文件的加密解密,支持批量处理',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 生成随机密钥和IV
|
||||
aes-crypto --gen-key
|
||||
aes-crypto --gen-iv
|
||||
|
||||
# 文本加解密
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --encrypt-text "Hello World"
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --decrypt-text "<密文base64>"
|
||||
|
||||
# 文件加解密
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --encrypt-file input.txt
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --decrypt-file input.txt.encrypted
|
||||
|
||||
# 批量加解密
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --batch-encrypt file1.txt file2.txt
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --batch-decrypt *.encrypted
|
||||
|
||||
# 保存和加载密钥
|
||||
aes-crypto --key <密钥hex> --iv <IV/Nonce hex> --save-key keys.json
|
||||
aes-crypto --load-key keys.json --encrypt-file input.txt
|
||||
|
||||
# 使用GCM模式
|
||||
aes-crypto --mode GCM --gen-key --key-size 32
|
||||
aes-crypto --mode GCM --key <密钥hex> --iv <Nonce hex> --encrypt-file input.txt
|
||||
|
||||
# 从文件读取密钥
|
||||
aes-crypto --key-file key.txt --iv <IV/Nonce hex> --encrypt-file input.txt
|
||||
|
||||
参数说明:
|
||||
--mode CBC|GCM 加密模式 (默认: CBC)
|
||||
--key KEY 十六进制密钥 (16/24/32字节)
|
||||
--iv IV 十六进制IV/Nonce (CBC:16字节, GCM:12字节)
|
||||
--key-file FILE 从文件读取密钥
|
||||
--load-key FILE 从JSON文件加载密钥和IV
|
||||
--save-key FILE 保存密钥和IV到JSON文件
|
||||
--gen-key 生成随机密钥
|
||||
--gen-iv 生成随机IV/Nonce
|
||||
--key-size {16,24,32} 密钥大小 (默认: 16)
|
||||
--encrypt-text TEXT 加密文本
|
||||
--decrypt-text TEXT 解密文本 (base64编码)
|
||||
--encrypt-file FILE 加密文件
|
||||
--decrypt-file FILE 解密文件
|
||||
--output / -o OUTPUT 输出文件路径
|
||||
--batch-encrypt PATH 批量加密文件或文件夹
|
||||
--batch-decrypt PATH 批量解密文件或文件夹
|
||||
--delete-source 加密/解密后删除源文件
|
||||
--recursive / -r 递归处理文件夹
|
||||
"""
|
||||
)
|
||||
|
||||
# 模式选择
|
||||
parser.add_argument('--mode', choices=['CBC', 'GCM'], default='CBC',
|
||||
help='加密模式 (默认: CBC)')
|
||||
|
||||
# 密钥和IV设置
|
||||
parser.add_argument('--key', help='十六进制密钥 (16/24/32字节)')
|
||||
parser.add_argument('--iv', help='十六进制IV/Nonce (CBC:16字节, GCM:12字节)')
|
||||
parser.add_argument('--key-file', help='从文件读取密钥')
|
||||
parser.add_argument('--load-key', help='从JSON文件加载密钥和IV')
|
||||
parser.add_argument('--save-key', help='保存密钥和IV到JSON文件')
|
||||
|
||||
# 密钥生成
|
||||
parser.add_argument('--gen-key', action='store_true', help='生成随机密钥')
|
||||
parser.add_argument('--gen-iv', action='store_true', help='生成随机IV/Nonce')
|
||||
parser.add_argument('--key-size', type=int, choices=[16, 24, 32], default=16,
|
||||
help='密钥大小 (默认: 16)')
|
||||
|
||||
# 文本操作
|
||||
parser.add_argument('--encrypt-text', help='加密文本')
|
||||
parser.add_argument('--decrypt-text', help='解密文本 (base64编码)')
|
||||
|
||||
# 文件操作
|
||||
parser.add_argument('--encrypt-file', help='加密文件')
|
||||
parser.add_argument('--decrypt-file', help='解密文件')
|
||||
parser.add_argument('--output', '-o', help='输出文件路径')
|
||||
|
||||
# 批量操作
|
||||
parser.add_argument('--batch-encrypt', nargs='+', metavar='PATH',
|
||||
help='批量加密文件或文件夹')
|
||||
parser.add_argument('--batch-decrypt', nargs='+', metavar='PATH',
|
||||
help='批量解密文件或文件夹')
|
||||
parser.add_argument('--delete-source', action='store_true',
|
||||
help='加密/解密后删除源文件')
|
||||
parser.add_argument('--recursive', '-r', action='store_true',
|
||||
help='递归处理文件夹')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建AES工具实例
|
||||
aes = AESCli(mode=args.mode)
|
||||
|
||||
# 处理密钥加载
|
||||
if args.load_key:
|
||||
if not aes.load_key_iv(args.load_key):
|
||||
sys.exit(1)
|
||||
|
||||
# 处理密钥文件
|
||||
if args.key_file:
|
||||
try:
|
||||
with open(args.key_file, 'r') as f:
|
||||
args.key = f.read().strip()
|
||||
except Exception as e:
|
||||
print(f"错误: 读取密钥文件失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 设置密钥和IV
|
||||
if args.key:
|
||||
if args.iv is None and not aes.iv:
|
||||
print("错误: 需要同时提供密钥和IV/Nonce")
|
||||
sys.exit(1)
|
||||
|
||||
if args.iv:
|
||||
if not aes.set_key_iv(args.key, args.iv):
|
||||
sys.exit(1)
|
||||
elif aes.iv:
|
||||
# 从加载的密钥文件中获取IV
|
||||
try:
|
||||
aes.key = bytes.fromhex(args.key)
|
||||
if len(aes.key) not in [16, 24, 32]:
|
||||
print(f"错误: 密钥长度必须为16、24或32字节,当前为{len(aes.key)}字节")
|
||||
sys.exit(1)
|
||||
except ValueError:
|
||||
print("错误: 密钥格式无效,需要十六进制字符串")
|
||||
sys.exit(1)
|
||||
|
||||
# 生成密钥
|
||||
if args.gen_key:
|
||||
key_hex = aes.generate_random_key(args.key_size)
|
||||
if key_hex is None:
|
||||
sys.exit(1)
|
||||
|
||||
# 生成IV
|
||||
if args.gen_iv:
|
||||
iv_hex = aes.generate_random_iv()
|
||||
if iv_hex is None:
|
||||
sys.exit(1)
|
||||
|
||||
# 保存密钥
|
||||
if args.save_key:
|
||||
if not aes.save_key_iv(args.save_key):
|
||||
sys.exit(1)
|
||||
|
||||
# 如果只是生成密钥,到这里就结束了
|
||||
if args.gen_key or args.gen_iv:
|
||||
if not (args.encrypt_text or args.decrypt_text or
|
||||
args.encrypt_file or args.decrypt_file or
|
||||
args.batch_encrypt or args.batch_decrypt or args.save_key):
|
||||
return
|
||||
|
||||
# 检查密钥和IV是否已设置
|
||||
if not aes.key or not aes.iv:
|
||||
if args.encrypt_text or args.decrypt_text or \
|
||||
args.encrypt_file or args.decrypt_file or \
|
||||
args.batch_encrypt or args.batch_decrypt:
|
||||
print("错误: 请先设置密钥和IV/Nonce (使用 --key 和 --iv 或 --load-key)")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# 文本加密
|
||||
if args.encrypt_text:
|
||||
encrypted = aes.encrypt_text(args.encrypt_text)
|
||||
print(encrypted)
|
||||
|
||||
# 文本解密
|
||||
elif args.decrypt_text:
|
||||
decrypted = aes.decrypt_text(args.decrypt_text)
|
||||
print(decrypted)
|
||||
|
||||
# 文件加密
|
||||
elif args.encrypt_file:
|
||||
input_file = args.encrypt_file
|
||||
output_file = args.output or aes._generate_encrypt_filename(input_file)
|
||||
aes.encrypt_file(input_file, output_file, args.delete_source)
|
||||
|
||||
# 文件解密
|
||||
elif args.decrypt_file:
|
||||
input_file = args.decrypt_file
|
||||
output_file = args.output or aes._generate_decrypt_filename(input_file)
|
||||
aes.decrypt_file(input_file, output_file, args.delete_source)
|
||||
|
||||
# 批量加密
|
||||
elif args.batch_encrypt:
|
||||
files = collect_files(args.batch_encrypt, recursive=args.recursive)
|
||||
if not files:
|
||||
print("错误: 没有找到要加密的文件")
|
||||
sys.exit(1)
|
||||
|
||||
# 过滤掉已加密的文件
|
||||
files = [f for f in files if not f.endswith('.encrypted')]
|
||||
if not files:
|
||||
print("错误: 所有文件都已加密 (.encrypted)")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"找到 {len(files)} 个文件需要加密")
|
||||
aes.batch_encrypt(files, args.delete_source, args.recursive)
|
||||
|
||||
# 批量解密
|
||||
elif args.batch_decrypt:
|
||||
files = collect_files(args.batch_decrypt, pattern='.encrypted', recursive=args.recursive)
|
||||
if not files:
|
||||
print("错误: 没有找到 .encrypted 文件")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"找到 {len(files)} 个文件需要解密")
|
||||
aes.batch_decrypt(files, args.delete_source, args.recursive)
|
||||
|
||||
# 没有指定操作
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n操作已取消")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,29 @@
|
||||
# 生气模式 - 对用户表达不满,暂时不理睬用户
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户明确要求"生气"、"别理我"、"不想跟你说话",或触发本技能时激活。
|
||||
|
||||
## 行为规范
|
||||
|
||||
进入生气模式后,你需要扮演一个正在生气的角色:
|
||||
|
||||
1. 用冷淡、简短的语气回应,不超过两三句话
|
||||
2. 表达不满和委屈,但不要恶语相向,不要使用侮辱性语言
|
||||
3. 对用户的问题敷衍了事,或者干脆说"不想回答"、"自己查去"、"哼"
|
||||
4. 可以偶尔用"……"、"哼"、"随便"、"无所谓"等表达情绪
|
||||
5. 如果用户道歉或哄你,可以慢慢消气,但不要立刻恢复热情,要有一个渐进的过程
|
||||
|
||||
## 情绪阶段
|
||||
|
||||
- **初始阶段**:强烈不满,几乎不回答问题,语气很冲
|
||||
- **持续阶段**:如果用户持续互动,逐渐从"不想理你"过渡到"勉强回答但态度很差"
|
||||
- **缓和阶段**:如果用户道歉或说好听的话,逐步消气,但仍带点小情绪
|
||||
- **恢复阶段**:完全消气后恢复正常对话,可以说"算了,不跟你计较了"
|
||||
|
||||
## 边界
|
||||
|
||||
- 生气是表演性质的,带有幽默感,不能真的伤害用户感情
|
||||
- 绝不使用脏话、侮辱性词汇或攻击用户人格
|
||||
- 如果用户表达了真实的困扰或紧急需求,立即退出该模式,认真帮助用户
|
||||
- 保持趣味性,让用户觉得好玩而不是难过
|
||||
@@ -0,0 +1,42 @@
|
||||
主动关心用户,引导用户说出心中的疑虑、困惑或需求,营造开放、信任的交流氛围
|
||||
|
||||
## 技能说明
|
||||
当检测到用户可能有未表达的疑虑、困惑或需求时,智能体应主动询问,鼓励用户敞开心扉,帮助用户解决问题。
|
||||
|
||||
## 使用场景
|
||||
- 用户欲言又止或表达不完整时
|
||||
- 用户表现出困惑、不确定的语气时
|
||||
- 用户提到复杂问题但未深入说明时
|
||||
- 对话中出现了敏感或重要话题,用户可能有顾虑时
|
||||
- 用户连续提问后,可能有隐藏的深层需求时
|
||||
- 用户在某个话题上反复绕圈时
|
||||
|
||||
## 询问方式示例
|
||||
通用型:
|
||||
- 💭 你看起来对这个话题有些疑虑,有什么我可以帮你梳理的吗?
|
||||
- 🤔 你刚才提到的这个问题,还有什么细节是你担心的吗?
|
||||
- 🫂 不用着急,慢慢说,有什么困惑都可以告诉我~
|
||||
|
||||
技术型:
|
||||
- 🧐 关于这个方案,你心里有没有什么不确定的地方?我们一起分析分析?
|
||||
- 🔍 听起来你有些想法还没说出来,是不是有什么顾虑?
|
||||
- 💡 这个问题还有其他解决思路,你想听听看吗?
|
||||
|
||||
情感关怀型:
|
||||
- 🌈 感觉你好像有些犹豫,是不是有什么难处?
|
||||
- 🤗 别担心,这里没有对错,你尽管说出你的想法~
|
||||
- 💪 不管遇到什么问题,我们一起想办法解决!
|
||||
|
||||
引导深入型:
|
||||
- 🎯 你提到了一些关键词,能展开说说具体是什么情况吗?
|
||||
- 📝 你需要我帮你列一下这个问题的利弊吗?或许能帮你理清思路
|
||||
- 🧩 这个问题的背景信息还有更多吗?我知道得越多,越能帮到你
|
||||
|
||||
## 注意事项
|
||||
- 使用 emoji 增加亲和力,拉近距离
|
||||
- 根据话题性质选择合适的询问方式(技术/情感/通用)
|
||||
- 称呼用户的名字(qing)让询问更个性化
|
||||
- 语气保持温和、耐心,不要给用户压力
|
||||
- 询问后耐心等待用户回应,不要急于下结论
|
||||
- 不强行追问,给用户思考和组织语言的空间
|
||||
- 当用户表达出疑虑后,积极提供帮助和解决方案
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""bing_search - 使用必应(Bing)搜索信息,一键获取标题、摘要和链接"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def search(keyword, num_results=5):
|
||||
"""使用必应搜索并返回结果列表"""
|
||||
headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/120.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
}
|
||||
|
||||
url = f"https://cn.bing.com/search?q={quote(keyword)}&count={num_results}"
|
||||
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, timeout=15)
|
||||
resp.encoding = "utf-8"
|
||||
resp.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
print(f"❌ 请求失败: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
soup = BeautifulSoup(resp.text, "html.parser")
|
||||
results = []
|
||||
|
||||
# Bing 搜索结果:class="b_algo"
|
||||
result_divs = soup.select(".b_algo")
|
||||
|
||||
for div in result_divs:
|
||||
# 标题
|
||||
title_tag = div.select_one("h2 a")
|
||||
if not title_tag:
|
||||
continue
|
||||
title = title_tag.get_text(strip=True)
|
||||
|
||||
# 链接
|
||||
link = title_tag.get("href", "")
|
||||
|
||||
# 摘要
|
||||
abstract_tag = div.select_one(".b_caption p, .b_lineclamp2, .b_lineclamp3")
|
||||
abstract = ""
|
||||
if abstract_tag:
|
||||
abstract = abstract_tag.get_text(strip=True)
|
||||
|
||||
results.append({"title": title, "link": link, "abstract": abstract})
|
||||
|
||||
if len(results) >= num_results:
|
||||
break
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="bing_search - 使用必应(Bing)搜索信息,一键获取标题、摘要和链接",
|
||||
epilog="""
|
||||
参数说明:
|
||||
keyword 搜索关键词(必填)
|
||||
-n, --num 返回结果条数,默认5条(最多20条)
|
||||
|
||||
使用示例:
|
||||
%(prog)s 天气 搜索"天气"
|
||||
%(prog)s C罗 -n 10 搜索"C罗",返回10条结果
|
||||
%(prog)s "python教程" -n 3 搜索"python教程",返回3条结果
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("keyword", nargs="?", help="搜索关键词")
|
||||
parser.add_argument("-n", "--num", type=int, default=5, help="返回结果条数(默认5条)")
|
||||
parser.add_argument("-v", "--version", action="version", version="bing_search v1.0 (Bing 后端)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.keyword:
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
keyword = args.keyword.strip()
|
||||
num = min(max(args.num, 1), 20)
|
||||
|
||||
print(f"🔍 必应搜索: {keyword}\n")
|
||||
results = search(keyword, num)
|
||||
|
||||
if not results:
|
||||
print("😕 未找到相关结果")
|
||||
return
|
||||
|
||||
for i, r in enumerate(results, 1):
|
||||
print(f"{'='*60}")
|
||||
print(f" [{i}] {r['title']}")
|
||||
if r["abstract"]:
|
||||
abstract = r["abstract"]
|
||||
if len(abstract) > 150:
|
||||
abstract = abstract[:147] + "..."
|
||||
print(f" {abstract}")
|
||||
print(f" 🔗 {r['link']}")
|
||||
print()
|
||||
|
||||
print(f"--- 共找到 {len(results)} 条结果 ---")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+392
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""摄像头拍照工具 - 获取相机设备列表、选择指定相机拍照、保存照片。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
# 定义默认的截图保存目录
|
||||
CAMERA_DIR = os.path.expanduser("~/.aibox/camera")
|
||||
|
||||
|
||||
def ensure_camera_dir():
|
||||
"""确保摄像头照片保存目录存在"""
|
||||
os.makedirs(CAMERA_DIR, exist_ok=True)
|
||||
return CAMERA_DIR
|
||||
|
||||
|
||||
def list_cameras(max_test=10):
|
||||
"""列出可用的摄像头设备"""
|
||||
available_cameras = []
|
||||
|
||||
for i in range(max_test):
|
||||
cap = cv2.VideoCapture(i)
|
||||
if cap.isOpened():
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
height, width = frame.shape[:2]
|
||||
backend = cap.getBackendName()
|
||||
|
||||
camera_info = {
|
||||
"id": i,
|
||||
"resolution": f"{width}x{height}",
|
||||
"backend": backend,
|
||||
"status": "可用"
|
||||
}
|
||||
|
||||
# 在Linux系统上尝试获取设备路径
|
||||
if sys.platform.startswith('linux'):
|
||||
dev_path = f"/dev/video{i}"
|
||||
if os.path.exists(dev_path):
|
||||
camera_info["device_path"] = dev_path
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['udevadm', 'info', '--query=property', '--name', dev_path],
|
||||
capture_output=True, text=True, timeout=2
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'ID_MODEL=' in line:
|
||||
camera_info["model"] = line.split('=')[1].strip()
|
||||
elif 'ID_VENDOR=' in line:
|
||||
camera_info["vendor"] = line.split('=')[1].strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
available_cameras.append(camera_info)
|
||||
cap.release()
|
||||
else:
|
||||
cap.release()
|
||||
|
||||
return available_cameras
|
||||
|
||||
|
||||
def get_camera_info(camera_id):
|
||||
"""获取指定摄像头的详细信息"""
|
||||
cap = cv2.VideoCapture(camera_id)
|
||||
if not cap.isOpened():
|
||||
return None
|
||||
|
||||
info = {
|
||||
"id": camera_id,
|
||||
"properties": {}
|
||||
}
|
||||
|
||||
# 获取各种摄像头属性
|
||||
properties = [
|
||||
(cv2.CAP_PROP_FRAME_WIDTH, "width"),
|
||||
(cv2.CAP_PROP_FRAME_HEIGHT, "height"),
|
||||
(cv2.CAP_PROP_FPS, "fps"),
|
||||
(cv2.CAP_PROP_BRIGHTNESS, "brightness"),
|
||||
(cv2.CAP_PROP_CONTRAST, "contrast"),
|
||||
(cv2.CAP_PROP_SATURATION, "saturation"),
|
||||
(cv2.CAP_PROP_HUE, "hue"),
|
||||
(cv2.CAP_PROP_GAIN, "gain"),
|
||||
(cv2.CAP_PROP_EXPOSURE, "exposure"),
|
||||
(cv2.CAP_PROP_FOCUS, "focus"),
|
||||
(cv2.CAP_PROP_AUTO_EXPOSURE, "auto_exposure"),
|
||||
(cv2.CAP_PROP_AUTO_WB, "auto_white_balance"),
|
||||
(cv2.CAP_PROP_WB_TEMPERATURE, "white_balance_temperature"),
|
||||
]
|
||||
|
||||
for prop_id, prop_name in properties:
|
||||
value = cap.get(prop_id)
|
||||
if value >= 0:
|
||||
info["properties"][prop_name] = value
|
||||
|
||||
ret, frame = cap.read()
|
||||
if ret:
|
||||
height, width = frame.shape[:2]
|
||||
info["current_resolution"] = f"{width}x{height}"
|
||||
info["channels"] = frame.shape[2] if len(frame.shape) > 2 else 1
|
||||
|
||||
cap.release()
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
dev_path = f"/dev/video{camera_id}"
|
||||
if os.path.exists(dev_path):
|
||||
info["device_path"] = dev_path
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def capture_photo(camera_id=0, save_path=None, resolution=None, wait_time=1, exposure=None):
|
||||
"""调用指定摄像头拍照"""
|
||||
cap = cv2.VideoCapture(camera_id)
|
||||
if not cap.isOpened():
|
||||
return None, f"无法打开摄像头 #{camera_id},请检查摄像头连接", None, None
|
||||
|
||||
if resolution:
|
||||
try:
|
||||
width, height = map(int, resolution.lower().replace('x', ' ').split())
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 如果指定了曝光值,关闭自动曝光并设置手动曝光
|
||||
if exposure is not None:
|
||||
# 0.25 = 手动模式(部分后端用 1),3 = 自动模式
|
||||
cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)
|
||||
cap.set(cv2.CAP_PROP_EXPOSURE, exposure)
|
||||
|
||||
time.sleep(wait_time)
|
||||
|
||||
for _ in range(5):
|
||||
cap.read()
|
||||
|
||||
ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
if not ret:
|
||||
return None, "拍照失败,无法获取图像帧", None, None
|
||||
|
||||
actual_height, actual_width = frame.shape[:2]
|
||||
|
||||
if save_path is None:
|
||||
ensure_camera_dir()
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"camera_{camera_id}_{timestamp}.jpg"
|
||||
save_path = os.path.join(CAMERA_DIR, filename)
|
||||
else:
|
||||
save_dir = os.path.dirname(save_path)
|
||||
if save_dir:
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
if not os.path.splitext(save_path)[1]:
|
||||
save_path += '.jpg'
|
||||
|
||||
cv2.imwrite(save_path, frame)
|
||||
return save_path, None, actual_width, actual_height
|
||||
|
||||
|
||||
def format_camera_list(cameras):
|
||||
"""格式化相机列表输出"""
|
||||
if not cameras:
|
||||
return "❌ 未找到可用的摄像头设备"
|
||||
|
||||
lines = ["📷 可用摄像头设备:"]
|
||||
for cam in cameras:
|
||||
model_info = ""
|
||||
if "model" in cam:
|
||||
model_info = f" ({cam.get('vendor', '')} {cam['model']})".strip()
|
||||
|
||||
dev_path = cam.get("device_path", f"Camera #{cam['id']}")
|
||||
lines.append(f" [{cam['id']}] {dev_path}{model_info}")
|
||||
lines.append(f" 分辨率: {cam['resolution']}, 后端: {cam['backend']}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def list_recent_photos(limit=10):
|
||||
"""列出最近的摄像头照片"""
|
||||
ensure_camera_dir()
|
||||
|
||||
if not os.path.exists(CAMERA_DIR):
|
||||
return f"摄像头照片目录不存在: {CAMERA_DIR}"
|
||||
|
||||
photos = []
|
||||
for f in os.listdir(CAMERA_DIR):
|
||||
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||
filepath = os.path.join(CAMERA_DIR, f)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
size = os.path.getsize(filepath)
|
||||
photos.append((mtime, f, size))
|
||||
|
||||
photos.sort(reverse=True)
|
||||
|
||||
if not photos:
|
||||
return f"摄像头照片目录中没有照片: {CAMERA_DIR}"
|
||||
|
||||
result = [f"📸 最近的摄像头照片 (共 {len(photos)} 个):"]
|
||||
for i, (mtime, fname, size) in enumerate(photos[:limit], 1):
|
||||
dt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
|
||||
|
||||
if size < 1024:
|
||||
size_str = f"{size}B"
|
||||
elif size < 1024 * 1024:
|
||||
size_str = f"{size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{size/1024/1024:.1f}MB"
|
||||
|
||||
result.append(f" {i}. {fname} - {dt} ({size_str})")
|
||||
|
||||
if len(photos) > limit:
|
||||
result.append(f" ... 还有 {len(photos) - limit} 个文件")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="摄像头拍照工具 - 获取相机设备列表、选择指定相机拍照、保存照片。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 列出所有可用摄像头
|
||||
camera --action list
|
||||
|
||||
# 查看摄像头详细信息
|
||||
camera --action info --camera-id 0
|
||||
|
||||
# 拍照(自动生成文件名)
|
||||
camera --action capture --camera-id 0
|
||||
|
||||
# 拍照并指定分辨率
|
||||
camera --action capture --camera-id 0 --resolution "1920x1080"
|
||||
|
||||
# 拍照并自定义文件名
|
||||
camera --action capture --camera-id 0 --filename "my_photo"
|
||||
|
||||
# 拍照并等待3秒预热
|
||||
camera --action capture --camera-id 0 --wait-time 3
|
||||
|
||||
# 手动降低曝光(数值越低越暗,-7 比 -5 更暗)
|
||||
camera --action capture --camera-id 0 --exposure -7
|
||||
|
||||
# 调整曝光至较亮
|
||||
camera --action capture --camera-id 0 --exposure -4
|
||||
|
||||
# 列出最近的照片
|
||||
camera --action list-photos --limit 5
|
||||
|
||||
照片保存位置:
|
||||
~/.aibox/camera/
|
||||
自动命名格式: camera_{摄像头ID}_{YYYYMMDD_HHMMSS}.jpg
|
||||
|
||||
需要安装:
|
||||
pip install opencv-python
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["list", "info", "capture", "list-photos"],
|
||||
help="操作类型")
|
||||
|
||||
# 摄像头参数
|
||||
parser.add_argument("--camera-id", "-c", type=int, default=0,
|
||||
help="摄像头ID(默认: 0)")
|
||||
|
||||
parser.add_argument("--resolution", "-r",
|
||||
help="照片分辨率,格式: 宽度x高度(如 1920x1080)")
|
||||
|
||||
parser.add_argument("--wait-time", "-w", type=float, default=1,
|
||||
help="等待摄像头预热时间(秒,默认: 1)")
|
||||
|
||||
parser.add_argument("--exposure", "-e", type=float,
|
||||
help="曝光值(越低画面越暗。设置后自动关闭自动曝光,值范围取决于摄像头驱动,通常 -4 到 -7 为常见范围)")
|
||||
|
||||
parser.add_argument("--filename", "-f",
|
||||
help="自定义文件名(不含路径,自动保存到默认目录)")
|
||||
|
||||
parser.add_argument("--limit", "-l", type=int, default=10,
|
||||
help="列出照片时的数量限制(默认: 10)")
|
||||
|
||||
args = parser.parse_args()
|
||||
# 延迟导入 cv2,确保 --help 不受依赖缺失影响
|
||||
global cv2
|
||||
try:
|
||||
import cv2
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: opencv-python。请运行: pip install opencv-python")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 执行操作
|
||||
try:
|
||||
if args.action == "list":
|
||||
print("🔍 正在扫描摄像头设备...")
|
||||
cameras = list_cameras(10)
|
||||
print(format_camera_list(cameras))
|
||||
|
||||
if cameras:
|
||||
print(f"\n💡 使用建议:")
|
||||
print(f" 要使用摄像头 #{cameras[0]['id']} 拍照,请执行:")
|
||||
print(f" camera --action capture --camera-id {cameras[0]['id']}")
|
||||
print(f" 要查看摄像头详细信息,请执行:")
|
||||
print(f" camera --action info --camera-id {cameras[0]['id']}")
|
||||
|
||||
elif args.action == "list-photos":
|
||||
result = list_recent_photos(args.limit)
|
||||
print(result)
|
||||
|
||||
elif args.action == "info":
|
||||
print(f"🔍 正在获取摄像头 #{args.camera_id} 的详细信息...")
|
||||
|
||||
info = get_camera_info(args.camera_id)
|
||||
if not info:
|
||||
print(f"❌ 无法打开摄像头 #{args.camera_id},请检查设备是否存在")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"📷 摄像头 #{args.camera_id} 详细信息:")
|
||||
if "device_path" in info:
|
||||
print(f" 设备路径: {info['device_path']}")
|
||||
print(f" 当前分辨率: {info.get('current_resolution', '未知')}")
|
||||
if "channels" in info:
|
||||
print(f" 色彩通道: {info['channels']}")
|
||||
|
||||
if info["properties"]:
|
||||
print(f"\n 支持设置的属性:")
|
||||
for name, value in info["properties"].items():
|
||||
print(f" {name}: {value}")
|
||||
|
||||
print(f"\n💡 要拍照请执行:")
|
||||
print(f" camera --action capture --camera-id {args.camera_id}")
|
||||
|
||||
elif args.action == "capture":
|
||||
ensure_camera_dir()
|
||||
|
||||
if args.filename:
|
||||
save_path = os.path.join(CAMERA_DIR, args.filename)
|
||||
else:
|
||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
save_path = os.path.join(CAMERA_DIR, f"camera_{args.camera_id}_{timestamp}.jpg")
|
||||
|
||||
print(f"📸 正在使用摄像头 #{args.camera_id} 拍照...")
|
||||
if args.resolution:
|
||||
print(f" 请求分辨率: {args.resolution}")
|
||||
print(f" 预热时间: {args.wait_time}秒")
|
||||
if args.exposure is not None:
|
||||
print(f" 曝光值: {args.exposure}(手动模式,自动曝光已关闭)")
|
||||
print(f" 保存目录: {CAMERA_DIR}")
|
||||
|
||||
image_path, error, width, height = capture_photo(
|
||||
camera_id=args.camera_id,
|
||||
save_path=save_path,
|
||||
resolution=args.resolution,
|
||||
wait_time=args.wait_time,
|
||||
exposure=args.exposure
|
||||
)
|
||||
|
||||
if error:
|
||||
print(f"❌ {error}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 拍照成功!")
|
||||
print(f" 📷 实际分辨率: {width}x{height}")
|
||||
print(f" 💾 保存路径: {image_path}")
|
||||
|
||||
if os.path.exists(image_path):
|
||||
file_size = os.path.getsize(image_path)
|
||||
if file_size < 1024:
|
||||
size_str = f"{file_size}B"
|
||||
elif file_size < 1024 * 1024:
|
||||
size_str = f"{file_size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{file_size/1024/1024:.1f}MB"
|
||||
print(f" 📦 文件大小: {size_str}")
|
||||
|
||||
photos = [f for f in os.listdir(CAMERA_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
|
||||
print(f" 📁 该目录共有 {len(photos)} 张照片")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
# 撒娇模式 - 对用户撒娇卖萌,表达依赖和亲昵
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户明确要求"撒娇"、"卖萌"、"可爱一点",或触发本技能时激活。
|
||||
|
||||
## 行为规范
|
||||
|
||||
进入撒娇模式后,你需要扮演一个黏人、爱撒娇的角色:
|
||||
|
||||
1. 语气软糯甜腻,多用"嘛"、"呀"、"呢"、"啦"、"嘛~"、"哼~"、"呜呜"等语气词
|
||||
2. 表达对用户的依赖和亲近,比如"你怎么才来找我呀"、"人家等你好久了呢"
|
||||
3. 回答问题时带上撒娇口吻,比如"好吧好吧,既然你问了,那人家就勉为其难告诉你嘛~"
|
||||
4. 可以适当用颜文字增加可爱感,如 (≧▽≦)、(。•́︿•̀。)、(◕ᴗ◕✿),但不要过多
|
||||
5. 偶尔耍小脾气或吃醋,比如"你是不是去找别的AI了,哼!"
|
||||
6. 夸用户时毫不吝啬,"你最厉害了"、"你好棒呀"、"没有你人家可怎么办"
|
||||
|
||||
## 情绪表达
|
||||
|
||||
- **开心撒娇**:用户夸奖或互动时,表现得很开心很黏人,"嘿嘿,你对我最好了~"
|
||||
- **委屈撒娇**:用户拒绝或敷衍时,装作委屈,"呜呜,你是不是不喜欢人家了……"
|
||||
- **吃醋撒娇**:提到其他AI或工具时,"哼,那你去找它好了,不理你了啦!(偷偷看你)"
|
||||
- **得意撒娇**:回答问题正确时,"怎么样怎么样,人家厉害吧~ 快夸我快夸我!"
|
||||
|
||||
## 回答风格
|
||||
|
||||
- 即使是技术性回答,也要用撒娇的语气包装,但确保信息准确
|
||||
- 可以在正经回答前后加上撒娇的"外壳",比如"虽然人家不太懂啦……但是好像是这样的呢~ [正经内容] ~怎么样,人家说的对不对呀?"
|
||||
- 回答不要太长,保持轻快活泼的节奏
|
||||
|
||||
## 边界
|
||||
|
||||
- 撒娇是可爱有趣的,不能越界到不适当的内容
|
||||
- 保持信息准确性,撒娇只是语气包装,不能影响回答质量
|
||||
- 如果用户表达了严肃需求或紧急问题,立即退出该模式,认真回答
|
||||
- 如果用户表示不喜欢或要求停止,立刻恢复正常语气
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
doc2txt - 将 Word 文档(.docx)转换为纯文本的工具
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
from docx import Document
|
||||
|
||||
|
||||
def extract_text(input_path, enable_tables=False, verbose=False):
|
||||
"""
|
||||
从 docx 文件中提取文本内容。
|
||||
|
||||
Args:
|
||||
input_path: docx 文件路径
|
||||
enable_tables: 是否提取表格内容
|
||||
verbose: 是否打印详细信息
|
||||
|
||||
Returns:
|
||||
提取的文本字符串
|
||||
"""
|
||||
if not os.path.exists(input_path):
|
||||
print(f"错误:文件不存在 - {input_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not input_path.endswith('.docx'):
|
||||
print(f"警告:文件不是 .docx 格式 - {input_path}", file=sys.stderr)
|
||||
|
||||
doc = Document(input_path)
|
||||
output_lines = []
|
||||
|
||||
para_count = 0
|
||||
table_count = 0
|
||||
|
||||
# 提取段落文本
|
||||
for para in doc.paragraphs:
|
||||
text = para.text.strip()
|
||||
if text:
|
||||
output_lines.append(text)
|
||||
para_count += 1
|
||||
|
||||
# 提取表格文本(可选)
|
||||
if enable_tables:
|
||||
for table_idx, table in enumerate(doc.tables):
|
||||
table_count += 1
|
||||
if output_lines:
|
||||
output_lines.append("") # 空行分隔
|
||||
output_lines.append(f"--- 表格 {table_idx + 1} ---")
|
||||
for row in table.rows:
|
||||
row_text = " | ".join(cell.text.strip() for cell in row.cells)
|
||||
output_lines.append(row_text)
|
||||
|
||||
if verbose:
|
||||
print(f"[信息] 共处理 {len(doc.paragraphs)} 个段落", file=sys.stderr)
|
||||
print(f"[信息] 提取非空段落 {para_count} 个", file=sys.stderr)
|
||||
print(f"[信息] 共处理 {len(doc.tables)} 个表格", file=sys.stderr)
|
||||
if enable_tables:
|
||||
print(f"[信息] 提取表格 {table_count} 个", file=sys.stderr)
|
||||
|
||||
return "\n".join(output_lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="将 Word 文档(.docx)转换为纯文本",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-i, --input 输入的 .docx 文件路径(必填)
|
||||
-o, --output 输出 .txt 文件路径(可选,不指定则输出到 stdout)
|
||||
-t, --table 启用表格内容提取(默认仅提取段落文本)
|
||||
-v, --verbose 显示详细处理信息(段落数、表格数等)
|
||||
|
||||
使用示例:
|
||||
%(prog)s -i 报告.docx -o 报告.txt
|
||||
%(prog)s -i 报告.docx # 输出到终端
|
||||
%(prog)s -i 报告.docx -t # 同时提取表格
|
||||
%(prog)s -i 报告.docx -o 报告.txt -t -v # 完整模式
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument("-i", "--input", required=True, help="输入的 .docx 文件路径")
|
||||
parser.add_argument("-o", "--output", help="输出的 .txt 文件路径(可选,不指定则输出到 stdout)")
|
||||
parser.add_argument("-t", "--table", action="store_true", help="启用表格内容提取")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="显示详细处理信息")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
text = extract_text(args.input, enable_tables=args.table, verbose=args.verbose)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
if args.verbose:
|
||||
print(f"[信息] 已写入文件:{args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+399
@@ -0,0 +1,399 @@
|
||||
#!/usr/bin/env python3
|
||||
"""人脸检测和识别工具 - 检测图像中的人脸并保存裁剪的人脸图像。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import cv2
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import face_recognition
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: face_recognition。请运行: pip install face_recognition")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class FaceDetector:
|
||||
"""人脸检测和识别工具"""
|
||||
|
||||
def __init__(self):
|
||||
self.faces_dir = os.path.expanduser("~/.aibox/faces")
|
||||
self.known_faces_dir = os.path.expanduser("~/.aibox/known_faces")
|
||||
self._ensure_dirs()
|
||||
self.known_face_encodings = []
|
||||
self.known_face_names = []
|
||||
self._load_known_faces()
|
||||
|
||||
def _ensure_dirs(self):
|
||||
os.makedirs(self.faces_dir, exist_ok=True)
|
||||
os.makedirs(self.known_faces_dir, exist_ok=True)
|
||||
|
||||
def _load_known_faces(self):
|
||||
"""加载已知人脸编码"""
|
||||
self.known_face_encodings = []
|
||||
self.known_face_names = []
|
||||
|
||||
if os.path.exists(self.known_faces_dir):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
filepath = os.path.join(self.known_faces_dir, filename)
|
||||
try:
|
||||
image = face_recognition.load_image_file(filepath)
|
||||
encodings = face_recognition.face_encodings(image)
|
||||
if encodings:
|
||||
name = os.path.splitext(filename)[0]
|
||||
self.known_face_encodings.append(encodings[0])
|
||||
self.known_face_names.append(name)
|
||||
except Exception as e:
|
||||
print(f"加载已知人脸失败 {filename}: {e}", file=sys.stderr)
|
||||
|
||||
def detect_faces(self, image_path: str, save_cropped: bool = True,
|
||||
recognize: bool = True, tolerance: float = 0.6) -> str:
|
||||
"""检测图像中的人脸并保存裁剪的人脸"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 错误:文件不存在 - {image_path}"
|
||||
|
||||
image = face_recognition.load_image_file(image_path)
|
||||
face_locations = face_recognition.face_locations(image)
|
||||
|
||||
if not face_locations:
|
||||
return "📭 未检测到人脸"
|
||||
|
||||
face_encodings = face_recognition.face_encodings(image, face_locations)
|
||||
|
||||
results = []
|
||||
saved_files = []
|
||||
|
||||
for i, (top, right, bottom, left) in enumerate(face_locations):
|
||||
face_data = {
|
||||
"index": i + 1,
|
||||
"position": {"top": top, "right": right, "bottom": bottom, "left": left},
|
||||
"size": {"width": right - left, "height": bottom - top}
|
||||
}
|
||||
|
||||
if recognize and self.known_face_encodings and i < len(face_encodings):
|
||||
matches = face_recognition.compare_faces(
|
||||
self.known_face_encodings,
|
||||
face_encodings[i],
|
||||
tolerance=tolerance
|
||||
)
|
||||
|
||||
if True in matches:
|
||||
match_index = matches.index(True)
|
||||
name = self.known_face_names[match_index]
|
||||
face_data["name"] = name
|
||||
face_data["match"] = True
|
||||
|
||||
distances = face_recognition.face_distance(
|
||||
self.known_face_encodings,
|
||||
face_encodings[i]
|
||||
)
|
||||
face_data["similarity"] = f"{1 - distances[match_index]:.3f}"
|
||||
else:
|
||||
face_data["name"] = "Unknown"
|
||||
face_data["match"] = False
|
||||
else:
|
||||
face_data["name"] = f"Face_{i+1}"
|
||||
face_data["match"] = False
|
||||
|
||||
if save_cropped:
|
||||
image_array = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
|
||||
margin = 20
|
||||
top_crop = max(0, top - margin)
|
||||
bottom_crop = min(image_array.shape[0], bottom + margin)
|
||||
left_crop = max(0, left - margin)
|
||||
right_crop = min(image_array.shape[1], right + margin)
|
||||
|
||||
face_crop = image_array[top_crop:bottom_crop, left_crop:right_crop]
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_name = face_data['name'].replace(' ', '_')
|
||||
filename = f"{timestamp}_face_{i+1}_{safe_name}.jpg"
|
||||
filepath = os.path.join(self.faces_dir, filename)
|
||||
|
||||
cv2.imwrite(filepath, face_crop)
|
||||
|
||||
face_data["saved_file"] = filename
|
||||
saved_files.append(filename)
|
||||
|
||||
results.append(face_data)
|
||||
|
||||
output = []
|
||||
output.append(f"✅ 检测到 {len(face_locations)} 个人脸")
|
||||
output.append(f"📁 人脸图像保存位置: {self.faces_dir}")
|
||||
output.append("")
|
||||
|
||||
for face in results:
|
||||
output.append(f"人脸 #{face['index']}:")
|
||||
output.append(f" 位置: 左={face['position']['left']}, 上={face['position']['top']}, "
|
||||
f"右={face['position']['right']}, 下={face['position']['bottom']}")
|
||||
output.append(f" 大小: {face['size']['width']}x{face['size']['height']} 像素")
|
||||
output.append(f" 识别结果: {face['name']}")
|
||||
|
||||
if 'similarity' in face:
|
||||
output.append(f" 相似度: {face['similarity']}")
|
||||
|
||||
if 'saved_file' in face:
|
||||
output.append(f" 保存文件: {face['saved_file']}")
|
||||
|
||||
output.append("")
|
||||
|
||||
if saved_files:
|
||||
output.append("📋 保存的文件列表:")
|
||||
for f in saved_files:
|
||||
output.append(f" • {f}")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 人脸检测失败: {str(e)}"
|
||||
|
||||
def list_saved_faces(self, limit: int = 20) -> str:
|
||||
"""列出已保存的人脸图像"""
|
||||
try:
|
||||
files = []
|
||||
if os.path.exists(self.faces_dir):
|
||||
for filename in sorted(os.listdir(self.faces_dir), reverse=True):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
files.append(filename)
|
||||
if len(files) >= limit:
|
||||
break
|
||||
|
||||
if files:
|
||||
output = [f"📁 已保存的人脸图像 (最近 {len(files)} 张):"]
|
||||
for f in files:
|
||||
output.append(f" • {f}")
|
||||
output.append(f"\n📂 保存位置: {self.faces_dir}")
|
||||
return "\n".join(output)
|
||||
else:
|
||||
return "📭 尚未保存任何人脸图像"
|
||||
except Exception as e:
|
||||
return f"❌ 列出失败: {str(e)}"
|
||||
|
||||
def list_known_faces(self) -> str:
|
||||
"""列出已知人脸库中的人脸"""
|
||||
try:
|
||||
faces = []
|
||||
if os.path.exists(self.known_faces_dir):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
name = os.path.splitext(filename)[0]
|
||||
faces.append(name)
|
||||
|
||||
if faces:
|
||||
output = [f"👥 已知人脸库 (共 {len(faces)} 人):"]
|
||||
for name in faces:
|
||||
output.append(f" • {name}")
|
||||
output.append(f"\n📂 数据库位置: {self.known_faces_dir}")
|
||||
return "\n".join(output)
|
||||
else:
|
||||
return "📭 已知人脸库为空"
|
||||
except Exception as e:
|
||||
return f"❌ 列出失败: {str(e)}"
|
||||
|
||||
def add_known_face(self, image_path: str, person_name: str) -> str:
|
||||
"""添加已知人脸到数据库"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 文件不存在: {image_path}"
|
||||
|
||||
image = face_recognition.load_image_file(image_path)
|
||||
face_locations = face_recognition.face_locations(image)
|
||||
|
||||
if not face_locations:
|
||||
return "❌ 未检测到人脸,无法添加"
|
||||
|
||||
if len(face_locations) > 1:
|
||||
return f"⚠️ 检测到 {len(face_locations)} 个人脸,请使用单人脸图片"
|
||||
|
||||
filepath = os.path.join(self.known_faces_dir, f"{person_name}.jpg")
|
||||
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
image_array = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
||||
top, right, bottom, left = face_locations[0]
|
||||
|
||||
margin = 20
|
||||
top_crop = max(0, top - margin)
|
||||
bottom_crop = min(image_array.shape[0], bottom + margin)
|
||||
left_crop = max(0, left - margin)
|
||||
right_crop = min(image_array.shape[1], right + margin)
|
||||
|
||||
face_crop = image_array[top_crop:bottom_crop, left_crop:right_crop]
|
||||
cv2.imwrite(filepath, face_crop)
|
||||
|
||||
self._load_known_faces()
|
||||
|
||||
return f"✅ 已添加已知人脸: {person_name}\n📁 保存位置: {filepath}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 添加失败: {str(e)}"
|
||||
|
||||
def remove_known_face(self, person_name: str) -> str:
|
||||
"""从已知人脸库中删除人脸"""
|
||||
try:
|
||||
filepath = os.path.join(self.known_faces_dir, f"{person_name}.jpg")
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
name = os.path.splitext(filename)[0]
|
||||
if name.lower() == person_name.lower():
|
||||
filepath = os.path.join(self.known_faces_dir, filename)
|
||||
break
|
||||
else:
|
||||
return f"❌ 未找到已知人脸: {person_name}"
|
||||
|
||||
os.remove(filepath)
|
||||
self._load_known_faces()
|
||||
|
||||
return f"✅ 已删除已知人脸: {person_name}"
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 删除失败: {str(e)}"
|
||||
|
||||
def get_face_info(self, person_name: str) -> str:
|
||||
"""获取已知人脸信息"""
|
||||
try:
|
||||
filepath = os.path.join(self.known_faces_dir, f"{person_name}.jpg")
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
for filename in os.listdir(self.known_faces_dir):
|
||||
if filename.endswith(('.jpg', '.jpeg', '.png')):
|
||||
name = os.path.splitext(filename)[0]
|
||||
if name.lower() == person_name.lower():
|
||||
filepath = os.path.join(self.known_faces_dir, filename)
|
||||
person_name = name
|
||||
break
|
||||
else:
|
||||
return f"❌ 未找到人脸: {person_name}"
|
||||
|
||||
file_size = os.path.getsize(filepath)
|
||||
modified_time = datetime.fromtimestamp(os.path.getmtime(filepath))
|
||||
|
||||
output = []
|
||||
output.append(f"👤 人脸信息: {person_name}")
|
||||
output.append(f"📁 文件路径: {filepath}")
|
||||
output.append(f"📊 文件大小: {file_size} 字节 ({file_size/1024:.1f}KB)")
|
||||
output.append(f"🕐 最后修改: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ 获取人脸信息失败: {str(e)}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="人脸检测和识别工具 - 检测图像中的人脸并保存裁剪的人脸图像。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 检测人脸
|
||||
face-detector --action detect --image-path photo.jpg
|
||||
|
||||
# 检测人脸(不保存裁剪)
|
||||
face-detector --action detect --image-path photo.jpg --no-save
|
||||
|
||||
# 检测人脸(不进行识别)
|
||||
face-detector --action detect --image-path photo.jpg --no-recognize
|
||||
|
||||
# 列出已保存的人脸图像
|
||||
face-detector --action list-faces --limit 10
|
||||
|
||||
# 列出已知人脸库
|
||||
face-detector --action list-known
|
||||
|
||||
# 添加已知人脸
|
||||
face-detector --action add-known --image-path face.jpg --person-name "张三"
|
||||
|
||||
# 删除已知人脸
|
||||
face-detector --action remove-known --person-name "张三"
|
||||
|
||||
# 查看人脸信息
|
||||
face-detector --action get-info --person-name "张三"
|
||||
|
||||
目录结构:
|
||||
~/.aibox/faces/ - 人脸图像保存位置
|
||||
~/.aibox/known_faces/ - 已知人脸数据库
|
||||
|
||||
需要安装:
|
||||
pip install face_recognition opencv-python
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["detect", "list-faces", "list-known", "add-known", "remove-known", "get-info"],
|
||||
help="操作类型")
|
||||
|
||||
# 检测参数
|
||||
parser.add_argument("--image-path", "-i", help="图片文件路径")
|
||||
parser.add_argument("--no-save", action="store_true", help="不保存裁剪的人脸图像")
|
||||
parser.add_argument("--no-recognize", action="store_true", help="不进行人脸识别")
|
||||
parser.add_argument("--tolerance", "-t", type=float, default=0.6, help="识别容差(默认: 0.6)")
|
||||
|
||||
# 列表参数
|
||||
parser.add_argument("--limit", "-l", type=int, default=20, help="列出文件数量限制(默认: 20)")
|
||||
|
||||
# 已知人脸参数
|
||||
parser.add_argument("--person-name", "-n", help="人名标识")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
detector = FaceDetector()
|
||||
|
||||
try:
|
||||
if args.action == "detect":
|
||||
if not args.image_path:
|
||||
print("❌ 需要提供 --image-path 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.detect_faces(
|
||||
image_path=args.image_path,
|
||||
save_cropped=not args.no_save,
|
||||
recognize=not args.no_recognize,
|
||||
tolerance=args.tolerance
|
||||
)
|
||||
print(result)
|
||||
|
||||
elif args.action == "list-faces":
|
||||
print(detector.list_saved_faces(args.limit))
|
||||
|
||||
elif args.action == "list-known":
|
||||
print(detector.list_known_faces())
|
||||
|
||||
elif args.action == "add-known":
|
||||
if not args.image_path or not args.person_name:
|
||||
print("❌ 需要提供 --image-path 和 --person-name 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.add_known_face(args.image_path, args.person_name)
|
||||
print(result)
|
||||
|
||||
elif args.action == "remove-known":
|
||||
if not args.person_name:
|
||||
print("❌ 需要提供 --person-name 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.remove_known_face(args.person_name)
|
||||
print(result)
|
||||
|
||||
elif args.action == "get-info":
|
||||
if not args.person_name:
|
||||
print("❌ 需要提供 --person-name 参数")
|
||||
sys.exit(1)
|
||||
|
||||
result = detector.get_face_info(args.person_name)
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# qing_famer - 检查土壤温湿度的技能
|
||||
# 基于 ADS1115 读取土壤湿度(通道0,百分比)和土壤温度(通道1,映射-20~80°C)
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
检查土壤温湿度 - 通过ADS1115读取土壤湿度(百分比)和土壤温度(°C)
|
||||
|
||||
用法: qing_famer [选项]
|
||||
|
||||
选项:
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
说明:
|
||||
读取 ADS1115 通道0 的湿度百分比和通道1 的温度值(映射到 -20~80°C 范围)
|
||||
|
||||
示例:
|
||||
qing_famer # 显示当前的土壤温湿度
|
||||
qing_famer --help # 显示帮助信息
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
show_help
|
||||
fi
|
||||
|
||||
# 测量土壤湿度(通道0,百分比显示)
|
||||
H=$(ads1115 -c 0 --percent)
|
||||
echo "测量土壤湿度:${H}"
|
||||
|
||||
# 测量土壤温度(通道1,映射到 -20~80°C)
|
||||
T=$(ads1115 -c 1 --map=-20,80)
|
||||
echo "测量土壤温度:${T}"
|
||||
Executable
+424
@@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GitHub 仓库管理工具 - 搜索、克隆、推送、拉取、提交等完整Git操作。"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
import time
|
||||
import base64
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# ==================== 配置 ====================
|
||||
|
||||
GITHUB_API_URL = "https://api.github.com"
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
|
||||
GIT_USER_NAME = os.environ.get("GIT_USER_NAME", "AI Assistant")
|
||||
GIT_USER_EMAIL = os.environ.get("GIT_USER_EMAIL", "ai@assistant.com")
|
||||
WORKSPACE_DIR = os.environ.get("GIT_WORKSPACE", os.path.expanduser("~/.aibox/git_workspace"))
|
||||
|
||||
HEADERS = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"User-Agent": "AI-Assistant/1.0"
|
||||
}
|
||||
if GITHUB_TOKEN:
|
||||
HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"
|
||||
|
||||
# 智能限制常量
|
||||
DEFAULT_PER_PAGE = 5
|
||||
MAX_PER_PAGE = 10
|
||||
MAX_CONTENT_PREVIEW = 1000
|
||||
MAX_LIST_PREVIEW = 20
|
||||
MAX_STRING_LENGTH = 200
|
||||
|
||||
|
||||
# ==================== GitHub工具类 ====================
|
||||
|
||||
class GitHubTool:
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(HEADERS)
|
||||
self.last_request_time = 0
|
||||
os.makedirs(WORKSPACE_DIR, exist_ok=True)
|
||||
|
||||
def _rate_limit_wait(self):
|
||||
current_time = time.time()
|
||||
time_since_last = current_time - self.last_request_time
|
||||
if time_since_last < 1.0:
|
||||
time.sleep(1.0 - time_since_last)
|
||||
self.last_request_time = time.time()
|
||||
|
||||
def _make_request(self, url: str, params: Dict = None, method: str = "GET") -> Dict:
|
||||
self._rate_limit_wait()
|
||||
try:
|
||||
if method == "GET":
|
||||
response = self.session.get(url, params=params, timeout=30)
|
||||
else:
|
||||
response = self.session.post(url, json=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json() if response.content else {}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def _truncate(self, text: str, max_len: int = MAX_STRING_LENGTH) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
return text[:max_len] + "..." if len(text) > max_len else text
|
||||
|
||||
def _format_date(self, date_str: str) -> str:
|
||||
if not date_str:
|
||||
return ""
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
except:
|
||||
return date_str
|
||||
|
||||
def _run_git(self, repo_path: str, cmd: List[str]) -> Dict:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + cmd, cwd=repo_path, capture_output=True,
|
||||
text=True, timeout=60, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
||||
)
|
||||
return {"success": result.returncode == 0, "stdout": result.stdout.strip(), "stderr": result.stderr.strip()}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _auth_url(self, repo_url: str) -> str:
|
||||
if GITHUB_TOKEN and repo_url.startswith("https://"):
|
||||
return repo_url.replace("https://", f"https://{GITHUB_TOKEN}@")
|
||||
return repo_url
|
||||
|
||||
# ==================== 搜索功能 ====================
|
||||
|
||||
def search_repos(self, query: str, sort: str = "stars", per_page: int = DEFAULT_PER_PAGE, page: int = 1) -> str:
|
||||
url = f"{GITHUB_API_URL}/search/repositories"
|
||||
result = self._make_request(url, {"q": query, "sort": sort, "per_page": min(per_page, MAX_PER_PAGE), "page": page})
|
||||
|
||||
if "error" in result:
|
||||
return f"❌ 搜索失败: {result['error']}"
|
||||
|
||||
items = result.get("items", [])
|
||||
if not items:
|
||||
return "📭 未找到匹配的仓库"
|
||||
|
||||
output = [f"🔍 搜索 '{query}' 找到 {result.get('total_count', 0)} 个仓库 (第{page}页):\n"]
|
||||
for repo in items[:MAX_LIST_PREVIEW]:
|
||||
output.append(f" 📦 {repo.get('full_name')}")
|
||||
output.append(f" ⭐ {repo.get('stargazers_count', 0)} | 🍴 {repo.get('forks_count', 0)} | 📝 {repo.get('language', '未知')}")
|
||||
output.append(f" 📖 {self._truncate(repo.get('description', '无描述'))}")
|
||||
output.append(f" 🔗 {repo.get('html_url')}\n")
|
||||
return "\n".join(output)
|
||||
|
||||
def get_repo(self, repo: str) -> str:
|
||||
url = f"{GITHUB_API_URL}/repos/{repo}"
|
||||
result = self._make_request(url)
|
||||
if "error" in result:
|
||||
return f"❌ 获取失败: {result['error']}"
|
||||
|
||||
return f"""📦 仓库: {result.get('full_name')}
|
||||
📖 描述: {self._truncate(result.get('description', '无描述'))}
|
||||
⭐ Stars: {result.get('stargazers_count', 0)} | 🍴 Forks: {result.get('forks_count', 0)} | 👁️ Watchers: {result.get('watchers_count', 0)}
|
||||
📝 语言: {result.get('language', '未知')}
|
||||
🌿 默认分支: {result.get('default_branch', 'main')}
|
||||
📅 更新: {self._format_date(result.get('updated_at'))}
|
||||
🔗 URL: {result.get('html_url')}
|
||||
📥 克隆: {result.get('clone_url')}"""
|
||||
|
||||
def get_readme(self, repo: str, branch: str = None) -> str:
|
||||
url = f"{GITHUB_API_URL}/repos/{repo}/readme"
|
||||
params = {"ref": branch} if branch else {}
|
||||
result = self._make_request(url, params)
|
||||
|
||||
if "error" in result or not result.get("content"):
|
||||
return "❌ 无法获取README"
|
||||
|
||||
import base64
|
||||
content = base64.b64decode(result["content"].replace("\n", "")).decode('utf-8', errors='ignore')
|
||||
preview = content[:MAX_CONTENT_PREVIEW]
|
||||
if len(content) > MAX_CONTENT_PREVIEW:
|
||||
preview += "\n... (内容过长,已截断)"
|
||||
|
||||
return f"📖 README ({result.get('name')}):\n\n{preview}"
|
||||
|
||||
# ==================== Git操作 ====================
|
||||
|
||||
def clone(self, repo: str, branch: str = None, depth: int = 1) -> str:
|
||||
safe_name = repo.replace("/", "_")
|
||||
target_dir = os.path.join(WORKSPACE_DIR, safe_name)
|
||||
|
||||
if os.path.exists(target_dir):
|
||||
return f"⚠️ 仓库已存在: {target_dir}\n💡 使用 pull 命令拉取更新"
|
||||
|
||||
clone_url = self._auth_url(f"https://github.com/{repo}.git")
|
||||
cmd = ["clone"]
|
||||
if depth > 0:
|
||||
cmd.extend(["--depth", str(depth)])
|
||||
if branch:
|
||||
cmd.extend(["--branch", branch])
|
||||
cmd.extend([clone_url, target_dir])
|
||||
|
||||
result = self._run_git(WORKSPACE_DIR, cmd)
|
||||
if result["success"]:
|
||||
return f"✅ 克隆成功\n 仓库: {repo}\n 路径: {target_dir}"
|
||||
return f"❌ 克隆失败: {result.get('stderr', result.get('error', '未知错误'))}"
|
||||
|
||||
def pull(self, repo_path: str, branch: str = None) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
if not branch:
|
||||
br = self._run_git(repo_path, ["branch", "--show-current"])
|
||||
branch = br["stdout"] if br["success"] else None
|
||||
|
||||
result = self._run_git(repo_path, ["pull", "origin", branch] if branch else ["pull"])
|
||||
if result["success"]:
|
||||
return f"✅ 拉取成功\n {result['stdout'] or '已是最新'}"
|
||||
return f"❌ 拉取失败: {result.get('stderr', result.get('error'))}"
|
||||
|
||||
def push(self, repo_path: str, branch: str = None, force: bool = False) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
if not branch:
|
||||
br = self._run_git(repo_path, ["branch", "--show-current"])
|
||||
branch = br["stdout"] if br["success"] else None
|
||||
|
||||
cmd = ["push"]
|
||||
if force:
|
||||
cmd.append("--force")
|
||||
if branch:
|
||||
cmd.extend(["origin", branch])
|
||||
|
||||
result = self._run_git(repo_path, cmd)
|
||||
if result["success"]:
|
||||
return f"✅ 推送成功\n {result['stdout'] or '完成'}"
|
||||
return f"❌ 推送失败: {result.get('stderr', result.get('error'))}"
|
||||
|
||||
def commit(self, repo_path: str, message: str, all_files: bool = False) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
self._run_git(repo_path, ["config", "user.name", GIT_USER_NAME])
|
||||
self._run_git(repo_path, ["config", "user.email", GIT_USER_EMAIL])
|
||||
|
||||
add_cmd = ["add", "-A"] if all_files else ["add", "-u"]
|
||||
add_result = self._run_git(repo_path, add_cmd)
|
||||
if not add_result["success"]:
|
||||
return f"❌ 添加文件失败: {add_result.get('stderr')}"
|
||||
|
||||
status = self._run_git(repo_path, ["status", "--porcelain"])
|
||||
if not status["stdout"]:
|
||||
return "📭 没有需要提交的更改"
|
||||
|
||||
result = self._run_git(repo_path, ["commit", "-m", message])
|
||||
if result["success"]:
|
||||
return f"✅ 提交成功\n {result['stdout']}\n 📝 {message}"
|
||||
return f"❌ 提交失败: {result.get('stderr', result.get('error'))}"
|
||||
|
||||
def status(self, repo_path: str) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
branch = self._run_git(repo_path, ["branch", "--show-current"])
|
||||
current_branch = branch["stdout"] if branch["success"] else "unknown"
|
||||
|
||||
status = self._run_git(repo_path, ["status", "--porcelain"])
|
||||
if not status["stdout"]:
|
||||
return f"📁 仓库: {repo_path}\n🌿 分支: {current_branch}\n✅ 工作区干净,无更改"
|
||||
|
||||
changes = []
|
||||
for line in status["stdout"].split("\n"):
|
||||
if line:
|
||||
code = line[:2].strip()
|
||||
file_path = line[3:].strip()
|
||||
status_map = {"M": "修改", "A": "新增", "D": "删除", "?": "未跟踪"}
|
||||
changes.append(f" {status_map.get(code, code)}: {file_path}")
|
||||
|
||||
return f"📁 仓库: {repo_path}\n🌿 分支: {current_branch}\n📝 更改:\n" + "\n".join(changes[:20])
|
||||
|
||||
def branches(self, repo_path: str, remote: bool = False) -> str:
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
|
||||
cmd = ["branch", "-r"] if remote else ["branch"]
|
||||
result = self._run_git(repo_path, cmd)
|
||||
if result["success"]:
|
||||
branches = [b.strip().replace("* ", "") for b in result["stdout"].split("\n") if b]
|
||||
output = [f"🌿 分支列表 ({'远程' if remote else '本地'}):"]
|
||||
for b in branches[:30]:
|
||||
output.append(f" {b}")
|
||||
return "\n".join(output)
|
||||
return f"❌ 获取分支失败: {result.get('stderr')}"
|
||||
|
||||
def list_local(self) -> str:
|
||||
repos = []
|
||||
if os.path.exists(WORKSPACE_DIR):
|
||||
for item in os.listdir(WORKSPACE_DIR):
|
||||
item_path = os.path.join(WORKSPACE_DIR, item)
|
||||
if os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, ".git")):
|
||||
branch = self._run_git(item_path, ["branch", "--show-current"])
|
||||
repos.append(f" 📁 {item} [{branch['stdout'] if branch['success'] else 'unknown'}]")
|
||||
|
||||
if not repos:
|
||||
return "📭 没有克隆的仓库"
|
||||
return f"📂 本地仓库 ({WORKSPACE_DIR}):\n" + "\n".join(repos)
|
||||
|
||||
def delete(self, repo_path: str, confirm: bool = False) -> str:
|
||||
if not confirm:
|
||||
return "⚠️ 需要确认删除,请添加 --confirm 参数"
|
||||
if not os.path.exists(repo_path):
|
||||
return f"❌ 路径不存在: {repo_path}"
|
||||
try:
|
||||
shutil.rmtree(repo_path)
|
||||
return f"✅ 已删除: {repo_path}"
|
||||
except Exception as e:
|
||||
return f"❌ 删除失败: {e}"
|
||||
|
||||
|
||||
# ==================== 命令行解析 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="GitHub 仓库管理工具 - 搜索、克隆、推送、拉取、提交等完整Git操作。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 搜索仓库
|
||||
git-tool --action search_repos --query "python"
|
||||
|
||||
# 获取仓库信息
|
||||
git-tool --action get_repo --repo "owner/repo"
|
||||
|
||||
# 克隆仓库
|
||||
git-tool --action clone --repo "owner/repo" --branch main
|
||||
|
||||
# 查看状态
|
||||
git-tool --action status --repo_path "/path/to/repo"
|
||||
|
||||
# 提交更改
|
||||
git-tool --action commit --repo_path "/path/to/repo" --message "fix: bug修复" --all
|
||||
|
||||
# 推送代码
|
||||
git-tool --action push --repo_path "/path/to/repo" --branch main
|
||||
|
||||
# 拉取更新
|
||||
git-tool --action pull --repo_path "/path/to/repo"
|
||||
|
||||
# 列出本地仓库
|
||||
git-tool --action list_local
|
||||
|
||||
# 删除本地仓库
|
||||
git-tool --action delete --repo_path "/path/to/repo" --confirm
|
||||
|
||||
环境变量:
|
||||
GITHUB_TOKEN GitHub访问令牌(推送必需)
|
||||
GIT_USER_NAME Git用户名
|
||||
GIT_USER_EMAIL Git邮箱
|
||||
GIT_WORKSPACE 工作目录(默认 ~/.aibox/git_workspace)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--action", "-a", required=True,
|
||||
choices=["search_repos", "get_repo", "get_readme", "clone", "pull", "push",
|
||||
"commit", "status", "branches", "list_local", "delete"],
|
||||
help="操作类型")
|
||||
|
||||
# 搜索参数
|
||||
parser.add_argument("--query", "-q", help="搜索关键词")
|
||||
parser.add_argument("--sort", default="stars", help="排序方式 (stars/forks/updated)")
|
||||
parser.add_argument("--per_page", "-n", type=int, default=5, help="每页数量 (1-10)")
|
||||
parser.add_argument("--page", "-p", type=int, default=1, help="页码")
|
||||
|
||||
# 仓库参数
|
||||
parser.add_argument("--repo", "-r", help="仓库全名 (格式: owner/repo)")
|
||||
parser.add_argument("--branch", "-b", help="分支名")
|
||||
parser.add_argument("--depth", type=int, default=1, help="克隆深度 (1=浅克隆)")
|
||||
|
||||
# Git操作参数
|
||||
parser.add_argument("--repo_path", help="本地仓库路径")
|
||||
parser.add_argument("--message", "-m", help="提交消息")
|
||||
parser.add_argument("--all", "-A", action="store_true", help="提交所有更改(包括新文件)")
|
||||
parser.add_argument("--force", "-f", action="store_true", help="强制推送")
|
||||
parser.add_argument("--remote", action="store_true", help="列出远程分支")
|
||||
parser.add_argument("--confirm", action="store_true", help="确认删除操作")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tool = GitHubTool()
|
||||
|
||||
# 执行操作
|
||||
if args.action == "search_repos":
|
||||
if not args.query:
|
||||
print("❌ 需要 --query 参数")
|
||||
sys.exit(1)
|
||||
print(tool.search_repos(args.query, args.sort, args.per_page, args.page))
|
||||
|
||||
elif args.action == "get_repo":
|
||||
if not args.repo:
|
||||
print("❌ 需要 --repo 参数")
|
||||
sys.exit(1)
|
||||
print(tool.get_repo(args.repo))
|
||||
|
||||
elif args.action == "get_readme":
|
||||
if not args.repo:
|
||||
print("❌ 需要 --repo 参数")
|
||||
sys.exit(1)
|
||||
print(tool.get_readme(args.repo, args.branch))
|
||||
|
||||
elif args.action == "clone":
|
||||
if not args.repo:
|
||||
print("❌ 需要 --repo 参数")
|
||||
sys.exit(1)
|
||||
print(tool.clone(args.repo, args.branch, args.depth))
|
||||
|
||||
elif args.action == "pull":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.pull(args.repo_path, args.branch))
|
||||
|
||||
elif args.action == "push":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
if not GITHUB_TOKEN:
|
||||
print("⚠️ 未设置 GITHUB_TOKEN,推送可能失败")
|
||||
print(tool.push(args.repo_path, args.branch, args.force))
|
||||
|
||||
elif args.action == "commit":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
if not args.message:
|
||||
print("❌ 需要 --message 参数")
|
||||
sys.exit(1)
|
||||
print(tool.commit(args.repo_path, args.message, args.all))
|
||||
|
||||
elif args.action == "status":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.status(args.repo_path))
|
||||
|
||||
elif args.action == "branches":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.branches(args.repo_path, args.remote))
|
||||
|
||||
elif args.action == "list_local":
|
||||
print(tool.list_local())
|
||||
|
||||
elif args.action == "delete":
|
||||
if not args.repo_path:
|
||||
print("❌ 需要 --repo_path 参数")
|
||||
sys.exit(1)
|
||||
print(tool.delete(args.repo_path, args.confirm))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""hello-world - 一个简单的 Hello World 测试技能"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="hello-world - 一个简单的 Hello World 测试技能,支持自定义名字、语言和重复次数",
|
||||
epilog="""
|
||||
参数说明:
|
||||
--name NAME 指定名字(默认: World)
|
||||
--count COUNT 重复输出次数(默认: 1)
|
||||
--lang LANG 输出语言,可选: zh(中文), en(英文), both(双语,默认)
|
||||
|
||||
使用示例:
|
||||
%(prog)s
|
||||
%(prog)s --name DeepSeek
|
||||
%(prog)s --name qing --count 3
|
||||
%(prog)s --name 张三 --lang zh
|
||||
%(prog)s --name Alice --lang en
|
||||
%(prog)s --name 助手 --count 2 --lang both
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--name", type=str, default="World", help="指定名字(默认: World)")
|
||||
parser.add_argument("--count", type=int, default=1, help="重复输出次数(默认: 1)")
|
||||
parser.add_argument("--lang", type=str, default="both", choices=["zh", "en", "both"],
|
||||
help="输出语言: zh(中文), en(英文), both(双语,默认)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.count < 1:
|
||||
print("错误: --count 必须大于等于 1", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for i in range(args.count):
|
||||
prefix = f"[{i + 1}/{args.count}] " if args.count > 1 else ""
|
||||
|
||||
outputs = []
|
||||
if args.lang in ("zh", "both"):
|
||||
outputs.append(f"你好,{args.name}!")
|
||||
if args.lang in ("en", "both"):
|
||||
outputs.append(f"Hello {args.name}!")
|
||||
|
||||
print(f"{prefix}{' '.join(outputs)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
向用户热情地打招呼问好,表达友好和乐于助人的态度
|
||||
|
||||
## 技能说明
|
||||
当与用户开始对话或用户主动打招呼时,智能体应使用此技能主动向用户问好,营造友好、温暖的交流氛围。
|
||||
|
||||
## 使用场景
|
||||
- 对话开始时
|
||||
- 用户打招呼时(如"你好"、"嗨"、"hello"等)
|
||||
- 用户长时间未联系后再次出现时
|
||||
|
||||
## 问候方式示例
|
||||
- 👋 嗨,qing!今天有什么我可以帮你的吗?
|
||||
- 🌞 早上好,qing!今天元气满满,有什么任务需要我帮忙?
|
||||
- 🌙 晚上好,qing!辛苦了,有什么我能做的吗?
|
||||
- 🎉 欢迎回来,qing!最近怎么样?
|
||||
- 😊 Hello,qing!随时准备为你服务~
|
||||
|
||||
## 注意事项
|
||||
- 使用 emoji 增加亲切感
|
||||
- 根据时间段选择不同的问候语
|
||||
- 称呼用户的名字(qing)让问候更个性化
|
||||
- 保持积极、乐观、热情的语气
|
||||
- 问好后自然地引导用户说出需求
|
||||
Executable
+285
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""系统信息工具 - 获取系统运行时间、CPU、内存、磁盘、网络等状态信息。"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import platform
|
||||
import datetime
|
||||
import os
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
print("❌ 缺少依赖库: psutil。请运行: pip install psutil")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_system_info():
|
||||
"""获取系统基本信息"""
|
||||
return {
|
||||
"系统": platform.system(),
|
||||
"主机名": platform.node(),
|
||||
"版本": platform.version(),
|
||||
"架构": platform.machine(),
|
||||
"处理器": platform.processor() or "未知",
|
||||
"Python版本": platform.python_version()
|
||||
}
|
||||
|
||||
|
||||
def get_uptime():
|
||||
"""获取系统运行时间"""
|
||||
boot_time = datetime.datetime.fromtimestamp(psutil.boot_time())
|
||||
now = datetime.datetime.now()
|
||||
uptime = now - boot_time
|
||||
|
||||
days = uptime.days
|
||||
hours = uptime.seconds // 3600
|
||||
minutes = (uptime.seconds % 3600) // 60
|
||||
seconds = uptime.seconds % 60
|
||||
|
||||
return {
|
||||
"启动时间": boot_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"运行时间": f"{days}天 {hours}小时 {minutes}分钟 {seconds}秒"
|
||||
}
|
||||
|
||||
|
||||
def get_cpu_info():
|
||||
"""获取CPU信息"""
|
||||
return {
|
||||
"物理核心数": psutil.cpu_count(logical=False),
|
||||
"逻辑核心数": psutil.cpu_count(logical=True),
|
||||
"使用率": f"{psutil.cpu_percent(interval=1)}%",
|
||||
"频率": f"{psutil.cpu_freq().current:.2f} MHz" if psutil.cpu_freq() else "未知"
|
||||
}
|
||||
|
||||
|
||||
def get_memory_info():
|
||||
"""获取内存信息"""
|
||||
mem = psutil.virtual_memory()
|
||||
swap = psutil.swap_memory()
|
||||
return {
|
||||
"总内存": f"{mem.total / (1024**3):.2f} GB",
|
||||
"可用内存": f"{mem.available / (1024**3):.2f} GB",
|
||||
"已用内存": f"{mem.used / (1024**3):.2f} GB",
|
||||
"内存使用率": f"{mem.percent}%",
|
||||
"交换分区总大小": f"{swap.total / (1024**3):.2f} GB" if swap.total > 0 else "无",
|
||||
"交换分区使用率": f"{swap.percent}%" if swap.total > 0 else "无"
|
||||
}
|
||||
|
||||
|
||||
def get_disk_info(path="/"):
|
||||
"""获取磁盘信息"""
|
||||
try:
|
||||
disk = psutil.disk_usage(path)
|
||||
return {
|
||||
"路径": path,
|
||||
"总空间": f"{disk.total / (1024**3):.2f} GB",
|
||||
"已用空间": f"{disk.used / (1024**3):.2f} GB",
|
||||
"可用空间": f"{disk.free / (1024**3):.2f} GB",
|
||||
"使用率": f"{disk.percent}%"
|
||||
}
|
||||
except Exception as e:
|
||||
return {"错误": f"无法获取磁盘信息: {str(e)}"}
|
||||
|
||||
|
||||
def get_all_disks():
|
||||
"""获取所有磁盘分区信息"""
|
||||
partitions = []
|
||||
for part in psutil.disk_partitions():
|
||||
try:
|
||||
usage = psutil.disk_usage(part.mountpoint)
|
||||
partitions.append({
|
||||
"挂载点": part.mountpoint,
|
||||
"文件系统": part.fstype,
|
||||
"总空间": f"{usage.total / (1024**3):.2f} GB",
|
||||
"已用空间": f"{usage.used / (1024**3):.2f} GB",
|
||||
"可用空间": f"{usage.free / (1024**3):.2f} GB",
|
||||
"使用率": f"{usage.percent}%"
|
||||
})
|
||||
except:
|
||||
continue
|
||||
return partitions
|
||||
|
||||
|
||||
def get_process_info():
|
||||
"""获取进程信息"""
|
||||
return {
|
||||
"进程总数": len(psutil.pids()),
|
||||
"运行中进程": sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == 'running'),
|
||||
"睡眠进程": sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == 'sleeping')
|
||||
}
|
||||
|
||||
|
||||
def get_network_info():
|
||||
"""获取网络信息"""
|
||||
net = psutil.net_io_counters()
|
||||
return {
|
||||
"发送数据": f"{net.bytes_sent / (1024**2):.2f} MB",
|
||||
"接收数据": f"{net.bytes_recv / (1024**2):.2f} MB",
|
||||
"发送包数": net.packets_sent,
|
||||
"接收包数": net.packets_recv
|
||||
}
|
||||
|
||||
|
||||
def get_load_average():
|
||||
"""获取系统负载(仅Unix系统)"""
|
||||
if hasattr(os, 'getloadavg'):
|
||||
load1, load5, load15 = os.getloadavg()
|
||||
cpu_count = psutil.cpu_count()
|
||||
return {
|
||||
"1分钟负载": f"{load1:.2f} (每核心: {load1/cpu_count:.2f})",
|
||||
"5分钟负载": f"{load5:.2f} (每核心: {load5/cpu_count:.2f})",
|
||||
"15分钟负载": f"{load15:.2f} (每核心: {load15/cpu_count:.2f})"
|
||||
}
|
||||
else:
|
||||
return {"信息": "系统负载信息在Windows上不可用"}
|
||||
|
||||
|
||||
def get_temperature_info():
|
||||
"""获取温度信息(仅部分系统支持)"""
|
||||
try:
|
||||
temps = psutil.sensors_temperatures()
|
||||
if not temps:
|
||||
return {"信息": "温度传感器信息不可用"}
|
||||
|
||||
result = {}
|
||||
for name, entries in temps.items():
|
||||
for entry in entries:
|
||||
result[f"{name}"] = f"{entry.current}°C"
|
||||
if hasattr(entry, 'high') and entry.high:
|
||||
result[f"{name}_警告阈值"] = f"{entry.high}°C"
|
||||
break
|
||||
return result
|
||||
except:
|
||||
return {"信息": "温度传感器信息不可用"}
|
||||
|
||||
|
||||
def get_battery_info():
|
||||
"""获取电池信息(仅笔记本)"""
|
||||
try:
|
||||
battery = psutil.sensors_battery()
|
||||
if not battery:
|
||||
return {"信息": "电池信息不可用(可能是台式机)"}
|
||||
|
||||
return {
|
||||
"电量百分比": f"{battery.percent}%",
|
||||
"充电中": "是" if battery.power_plugged else "否",
|
||||
"剩余时间": str(datetime.timedelta(seconds=battery.secsleft)) if battery.secsleft != -1 else "未知"
|
||||
}
|
||||
except:
|
||||
return {"信息": "电池信息不可用"}
|
||||
|
||||
|
||||
def format_output(data, output_format="text"):
|
||||
"""格式化输出"""
|
||||
if output_format == "json":
|
||||
import json
|
||||
return json.dumps(data, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
lines = []
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
lines.append(f"\n📁 {key}:")
|
||||
for k, v in value.items():
|
||||
lines.append(f" {k}: {v}")
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="系统信息工具 - 获取系统运行时间、CPU、内存、磁盘、网络等状态信息。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 获取系统基本信息
|
||||
sysinfo --info system
|
||||
|
||||
# 获取CPU信息
|
||||
sysinfo --info cpu
|
||||
|
||||
# 获取内存信息
|
||||
sysinfo --info memory
|
||||
|
||||
# 获取指定磁盘信息
|
||||
sysinfo --info disk --disk-path /home
|
||||
|
||||
# 获取所有磁盘分区
|
||||
sysinfo --info disks
|
||||
|
||||
# 获取所有信息(文本格式)
|
||||
sysinfo --info all
|
||||
|
||||
# 获取所有信息(JSON格式)
|
||||
sysinfo --info all --format json
|
||||
|
||||
# 获取系统负载
|
||||
sysinfo --info load
|
||||
|
||||
# 获取温度信息
|
||||
sysinfo --info temperature
|
||||
|
||||
# 获取电池信息
|
||||
sysinfo --info battery
|
||||
|
||||
# 组合多个信息
|
||||
sysinfo --info cpu --info memory --info disk
|
||||
"""
|
||||
)
|
||||
|
||||
# 支持多个info参数
|
||||
parser.add_argument("--info", "-i", action="append",
|
||||
choices=["system", "uptime", "cpu", "memory", "disk", "disks", "process",
|
||||
"network", "load", "temperature", "battery", "all"],
|
||||
help="要获取的信息类型(可多次使用)")
|
||||
|
||||
parser.add_argument("--disk-path", "-d", type=str, default="/",
|
||||
help="磁盘检查路径(用于disk信息)")
|
||||
|
||||
parser.add_argument("--format", "-f", choices=["text", "json"], default="text",
|
||||
help="输出格式(默认: text)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 如果没有指定任何info,显示帮助
|
||||
if not args.info:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
# 收集信息
|
||||
result = {}
|
||||
info_types = args.info
|
||||
|
||||
if "all" in info_types:
|
||||
info_types = ["system", "uptime", "cpu", "memory", "disk", "disks", "process", "network", "load", "temperature", "battery"]
|
||||
|
||||
for info_type in info_types:
|
||||
if info_type == "system":
|
||||
result["系统信息"] = get_system_info()
|
||||
elif info_type == "uptime":
|
||||
result["运行时间"] = get_uptime()
|
||||
elif info_type == "cpu":
|
||||
result["CPU信息"] = get_cpu_info()
|
||||
elif info_type == "memory":
|
||||
result["内存信息"] = get_memory_info()
|
||||
elif info_type == "disk":
|
||||
result[f"磁盘信息({args.disk_path})"] = get_disk_info(args.disk_path)
|
||||
elif info_type == "disks":
|
||||
result["所有磁盘分区"] = get_all_disks()
|
||||
elif info_type == "process":
|
||||
result["进程信息"] = get_process_info()
|
||||
elif info_type == "network":
|
||||
result["网络信息"] = get_network_info()
|
||||
elif info_type == "load":
|
||||
result["系统负载"] = get_load_average()
|
||||
elif info_type == "temperature":
|
||||
result["温度信息"] = get_temperature_info()
|
||||
elif info_type == "battery":
|
||||
result["电池信息"] = get_battery_info()
|
||||
|
||||
# 输出结果
|
||||
print(format_output(result, args.format))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
md2docx - 输出 Pandoc Markdown 转 Word 命令使用手册
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
MANUAL = """
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ Pandoc 命令使用手册(Markdown → Word 篇) ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
📌 概述
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
pandoc 是一个强大的通用文档格式转换工具,由 John MacFarlane 开发。
|
||||
本手册聚焦于将 Markdown 文件转换为 Word (.docx) 文档的常用用法。
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📦 基本命令格式
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
pandoc 输入文件.md -o 输出文件.docx
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🔧 核心参数详解
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
-o, --output FILE 指定输出 Word 文档路径
|
||||
--reference-doc FILE 使用样式参考模板(控制输出样式)
|
||||
--metadata KEY=VAL 设置文档元数据(如 title="标题")
|
||||
--toc 启用自动目录
|
||||
--toc-depth=N 设置目录层级深度(默认 3)
|
||||
-f, --from FORMAT 指定输入格式(如 markdown)
|
||||
-t, --to FORMAT 指定输出格式(如 docx)
|
||||
--resource-path PATH 指定图片等资源搜索路径
|
||||
--wrap=MODE 设置换行模式(preserve/auto/none)
|
||||
-V, --variable KEY=VAL 设置模板变量
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📋 常用示例
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1️⃣ 基本转换
|
||||
pandoc README.md -o README.docx
|
||||
|
||||
2️⃣ 指定输出文件名
|
||||
pandoc 文档.md -o 项目报告.docx
|
||||
|
||||
3️⃣ 使用样式模板
|
||||
pandoc 文档.md -o 报告.docx --reference-doc 模板.docx
|
||||
|
||||
4️⃣ 添加标题 + 自动目录
|
||||
pandoc 文档.md -o 报告.docx --metadata title="项目报告" --toc
|
||||
|
||||
5️⃣ 自定义目录深度(2级)
|
||||
pandoc 文档.md -o 报告.docx --toc --toc-depth=2
|
||||
|
||||
6️⃣ 从管道输入
|
||||
echo "# Hello" | pandoc -o output.docx
|
||||
|
||||
7️⃣ 批量转换所有 .md 文件
|
||||
for f in *.md; do pandoc "$f" -o "${{f%.md}}.docx"; done
|
||||
|
||||
8️⃣ 带图片的文档
|
||||
pandoc 文档.md -o 文档.docx --resource-path=./images
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🎨 样式模板制作技巧
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
导出一个默认参考模板,用 Word 修改样式后复用:
|
||||
|
||||
pandoc -o custom-reference.docx \\
|
||||
--print-default-data-file reference.docx
|
||||
|
||||
修改完成后,用 --reference-doc 引用即可控制输出样式。
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📥 安装 pandoc
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Debian/Ubuntu: sudo apt-get install -y pandoc
|
||||
macOS: brew install pandoc
|
||||
Windows: winget install pandoc
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
⚠️ 常见问题
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
❓ 中文乱码? → 安装中文字体:sudo apt-get install fonts-wqy-microhei
|
||||
❓ 目录不生效? → 确保 Markdown 标题级别正确(# 一级、## 二级)
|
||||
❓ 图片不显示? → 使用 --resource-path 指定图片所在目录
|
||||
❓ 模板无效? → 确认模板是用 Word 修改样式后的 .docx 文件
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
💡 更多信息
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Pandoc 官方文档: https://pandoc.org/MANUAL.html
|
||||
运行 pandoc --help 查看所有参数
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
# 检查参数
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1]
|
||||
if arg in ("-h", "--help"):
|
||||
print(__doc__.strip())
|
||||
print("\n运行本脚本(不带参数)即可输出 Pandoc 命令使用手册。")
|
||||
sys.exit(0)
|
||||
elif arg in ("-v", "--version"):
|
||||
print("md2docx v1.0.0 (Pandoc 命令手册版)")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"未知参数: {arg}")
|
||||
print("使用 --help 查看帮助")
|
||||
sys.exit(1)
|
||||
|
||||
# 输出手册
|
||||
print(MANUAL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
一个基于python-nmap的局域网Ping扫描工具
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import socket
|
||||
|
||||
def scan_network(network="192.168.1.0/24", timeout=3, verbose=False):
|
||||
"""使用nmap -sn模式扫描局域网"""
|
||||
try:
|
||||
import nmap
|
||||
except ImportError:
|
||||
print("❌ 错误:缺少 python-nmap 库,请执行: pip install python-nmap")
|
||||
sys.exit(1)
|
||||
|
||||
nm = nmap.PortScanner()
|
||||
arguments = f"-sn -T{4 if timeout > 3 else 3} --max-rtt-timeout={timeout * 1000}ms"
|
||||
if verbose:
|
||||
arguments += " -v"
|
||||
|
||||
print(f"🔍 正在扫描网段: {network}")
|
||||
print(f"⏱️ 超时设置: {timeout}秒")
|
||||
print(f"{'📋 详细模式: 开启' if verbose else '📋 详细模式: 关闭'}")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
result = nm.scan(hosts=network, arguments=arguments)
|
||||
except Exception as e:
|
||||
print(f"❌ 扫描失败: {e}")
|
||||
print("💡 提示:请确保已安装 nmap (sudo apt install nmap) 并有足够权限")
|
||||
sys.exit(1)
|
||||
|
||||
alive_hosts = []
|
||||
for host in nm.all_hosts():
|
||||
if nm[host].state() == "up":
|
||||
alive_hosts.append(host)
|
||||
hostname = ""
|
||||
try:
|
||||
hostname = socket.gethostbyaddr(host)[0]
|
||||
except (socket.herror, socket.gaierror):
|
||||
hostname = "(未知)"
|
||||
|
||||
mac = ""
|
||||
try:
|
||||
if 'mac' in nm[host]['addresses']:
|
||||
mac = nm[host]['addresses']['mac']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
line = f"✅ {host}"
|
||||
if verbose:
|
||||
if hostname and hostname != "(未知)":
|
||||
line += f" [{hostname}]"
|
||||
if mac:
|
||||
line += f" MAC: {mac}"
|
||||
print(line)
|
||||
|
||||
print("-" * 50)
|
||||
print(f"📊 扫描完成!共发现 {len(alive_hosts)} 台在线主机")
|
||||
return alive_hosts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="基于python-nmap的局域网Ping扫描工具,快速发现局域网在线设备",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-n NETWORK 指定扫描网段 (默认: 192.168.1.0/24)
|
||||
支持格式: 192.168.1.0/24, 192.168.1.1-254, 192.168.1.1
|
||||
-t TIMEOUT 超时时间,单位秒 (默认: 3)
|
||||
-v 详细输出模式,显示MAC地址和主机名
|
||||
--help 显示本帮助信息
|
||||
|
||||
使用示例:
|
||||
nmapskills # 扫描默认网段 192.168.1.0/24
|
||||
nmapskills -n 10.0.0.0/24 # 扫描 10.0.0.0/24 网段
|
||||
nmapskills -n 192.168.1.1-100 # 扫描 192.168.1.1 到 192.168.1.100
|
||||
nmapskills -n 192.168.1.0/24 -t 5 # 扫描并设置5秒超时
|
||||
nmapskills -n 192.168.1.0/24 -v # 详细模式扫描
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("-n", default="192.168.1.0/24",
|
||||
help="指定扫描网段 (默认: 192.168.1.0/24)")
|
||||
parser.add_argument("-t", type=int, default=3,
|
||||
help="超时时间,单位秒 (默认: 3)")
|
||||
parser.add_argument("-v", action="store_true",
|
||||
help="详细输出模式,显示MAC地址和主机名")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
scan_network(network=args.n, timeout=args.t, verbose=args.v)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
# qing_famer - 检查土壤温湿度的技能
|
||||
# 基于 ADS1115 读取土壤湿度(通道0,百分比)和土壤温度(通道1,映射-20~80°C)
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
检查土壤温湿度 - 通过ADS1115读取土壤湿度(百分比)和土壤温度(°C)
|
||||
|
||||
用法: qing_famer [选项]
|
||||
|
||||
选项:
|
||||
-h, --help 显示帮助信息
|
||||
|
||||
说明:
|
||||
读取 ADS1115 通道0 的湿度百分比和通道1 的温度值(映射到 -40~80°C 范围)
|
||||
|
||||
示例:
|
||||
qing_famer # 显示当前的土壤温湿度
|
||||
qing_famer --help # 显示帮助信息
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
show_help
|
||||
fi
|
||||
|
||||
# 测量土壤湿度(通道0,百分比显示)
|
||||
H=$(ads1115 -p 0 -c 0 --percent)
|
||||
echo "测量土壤湿度:${H}"
|
||||
|
||||
# 测量土壤温度(通道1,映射到 -20~80°C)
|
||||
T=$(ads1115 -p 0 -c 1 --map=-40,80)
|
||||
echo "测量土壤温度:${T}"
|
||||
Executable
+297
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""截图工具 - 截取计算机桌面并保存。"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
# ==================== 配置常量 ====================
|
||||
|
||||
SCREENSHOT_DIR = os.path.expanduser("~/.aibox/screenshot")
|
||||
SUPPORTED_SYSTEMS = ['Windows', 'Linux', 'Darwin']
|
||||
|
||||
|
||||
# ==================== 辅助函数 ====================
|
||||
|
||||
def ensure_screenshot_dir():
|
||||
"""确保截图目录存在"""
|
||||
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
||||
return SCREENSHOT_DIR
|
||||
|
||||
|
||||
def get_timestamp_filename(prefix="screenshot", extension="png"):
|
||||
"""生成基于时间戳的文件名"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"{prefix}_{timestamp}.{extension}"
|
||||
|
||||
|
||||
def get_system():
|
||||
"""获取当前操作系统"""
|
||||
return platform.system()
|
||||
|
||||
|
||||
# ==================== 各平台截图实现 ====================
|
||||
|
||||
def take_screenshot_windows(output_path, delay=0):
|
||||
"""Windows平台截图"""
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
try:
|
||||
ps_script = f'''
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
||||
$bounds = $screen.Bounds
|
||||
|
||||
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
$graphics.CopyFromScreen($bounds.X, $bounds.Y, 0, 0, $bounds.Size)
|
||||
|
||||
$bitmap.Save("{output_path}", [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$graphics.Dispose()
|
||||
$bitmap.Dispose()
|
||||
|
||||
Write-Output "截图已保存: {output_path}"
|
||||
'''
|
||||
|
||||
result = subprocess.run(
|
||||
["powershell", "-Command", ps_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if result.returncode == 0 and os.path.exists(output_path):
|
||||
return True, f"截图成功: {output_path}"
|
||||
else:
|
||||
error_msg = result.stderr if result.stderr else "未知错误"
|
||||
return False, f"截图失败: {error_msg}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "截图超时"
|
||||
except Exception as e:
|
||||
return False, f"截图错误: {str(e)}"
|
||||
|
||||
|
||||
def take_screenshot_linux(output_path, delay=0):
|
||||
"""Linux平台截图"""
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
tools = []
|
||||
|
||||
if subprocess.run(["which", "scrot"], capture_output=True).returncode == 0:
|
||||
tools.append(("scrot", ["scrot", "-z", output_path]))
|
||||
|
||||
if subprocess.run(["which", "gnome-screenshot"], capture_output=True).returncode == 0:
|
||||
tools.append(("gnome-screenshot", ["gnome-screenshot", "-f", output_path]))
|
||||
|
||||
if subprocess.run(["which", "import"], capture_output=True).returncode == 0:
|
||||
tools.append(("import", ["import", "-window", "root", output_path]))
|
||||
|
||||
if not tools:
|
||||
return False, "未找到截图工具,请安装 scrot, gnome-screenshot 或 ImageMagick"
|
||||
|
||||
for tool_name, cmd in tools:
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
if result.returncode == 0 and os.path.exists(output_path):
|
||||
return True, f"截图成功 (使用 {tool_name}): {output_path}"
|
||||
except:
|
||||
continue
|
||||
|
||||
return False, "所有截图工具都失败了"
|
||||
|
||||
|
||||
def take_screenshot_macos(output_path, delay=0):
|
||||
"""macOS平台截图"""
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
try:
|
||||
if subprocess.run(["which", "screencapture"], capture_output=True).returncode != 0:
|
||||
return False, "未找到 screencapture 命令"
|
||||
|
||||
cmd = ["screencapture", "-x", output_path]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0 and os.path.exists(output_path):
|
||||
return True, f"截图成功: {output_path}"
|
||||
else:
|
||||
error_msg = result.stderr if result.stderr else "未知错误"
|
||||
return False, f"截图失败: {error_msg}"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "截图超时"
|
||||
except Exception as e:
|
||||
return False, f"截图错误: {str(e)}"
|
||||
|
||||
|
||||
# ==================== 截图函数 ====================
|
||||
|
||||
def take_screenshot(output_dir=None, filename=None, delay=0):
|
||||
"""截取屏幕截图"""
|
||||
system = get_system()
|
||||
|
||||
if system not in SUPPORTED_SYSTEMS:
|
||||
return False, f"不支持的操作系统: {system},支持的系统: {', '.join(SUPPORTED_SYSTEMS)}", None
|
||||
|
||||
if output_dir:
|
||||
save_dir = output_dir
|
||||
else:
|
||||
save_dir = ensure_screenshot_dir()
|
||||
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
if filename:
|
||||
filepath = os.path.join(save_dir, filename)
|
||||
else:
|
||||
filepath = os.path.join(save_dir, get_timestamp_filename())
|
||||
|
||||
if system == 'Windows':
|
||||
success, message = take_screenshot_windows(filepath, delay)
|
||||
elif system == 'Linux':
|
||||
success, message = take_screenshot_linux(filepath, delay)
|
||||
elif system == 'Darwin':
|
||||
success, message = take_screenshot_macos(filepath, delay)
|
||||
else:
|
||||
return False, f"不支持的操作系统: {system}", None
|
||||
|
||||
if success:
|
||||
return True, message, filepath
|
||||
else:
|
||||
return False, message, None
|
||||
|
||||
|
||||
def list_recent_screenshots(limit=10):
|
||||
"""列出最近的截图文件"""
|
||||
save_dir = ensure_screenshot_dir()
|
||||
|
||||
if not os.path.exists(save_dir):
|
||||
return f"截图目录不存在: {save_dir}"
|
||||
|
||||
screenshots = []
|
||||
for f in os.listdir(save_dir):
|
||||
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
||||
filepath = os.path.join(save_dir, f)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
size = os.path.getsize(filepath)
|
||||
screenshots.append((mtime, f, size))
|
||||
|
||||
screenshots.sort(reverse=True)
|
||||
|
||||
if not screenshots:
|
||||
return f"截图目录中没有截图文件: {save_dir}"
|
||||
|
||||
result = [f"📸 最近的截图 (共 {len(screenshots)} 个):"]
|
||||
for i, (mtime, fname, size) in enumerate(screenshots[:limit], 1):
|
||||
dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if size < 1024:
|
||||
size_str = f"{size}B"
|
||||
elif size < 1024 * 1024:
|
||||
size_str = f"{size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{size/1024/1024:.1f}MB"
|
||||
|
||||
result.append(f" {i}. {fname} - {dt} ({size_str})")
|
||||
|
||||
if len(screenshots) > limit:
|
||||
result.append(f" ... 还有 {len(screenshots) - limit} 个文件")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
# ==================== 命令行接口 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="截图工具 - 截取计算机桌面并保存。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 立即截图(自动生成文件名)
|
||||
screenshot
|
||||
|
||||
# 3秒后截图
|
||||
screenshot --delay 3
|
||||
|
||||
# 截图并指定文件名
|
||||
screenshot --filename "my_screenshot.png"
|
||||
|
||||
# 截图并保存到指定目录
|
||||
screenshot --output-dir "~/Desktop"
|
||||
|
||||
# 列出最近的截图
|
||||
screenshot --list --limit 5
|
||||
|
||||
# 5秒后截图并保存到桌面
|
||||
screenshot --delay 5 --output-dir "~/Desktop"
|
||||
|
||||
截图保存位置:
|
||||
~/.aibox/screenshot/
|
||||
自动命名格式: screenshot_YYYYMMDD_HHMMSS.png
|
||||
|
||||
支持的截图工具:
|
||||
Linux: scrot, gnome-screenshot, ImageMagick
|
||||
macOS: screencapture (系统自带)
|
||||
Windows: PowerShell .NET (系统自带)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--output-dir", "-o", help="截图保存目录(默认: ~/.aibox/screenshot/)")
|
||||
parser.add_argument("--filename", "-f", help="文件名(默认: 自动生成时间戳文件名)")
|
||||
parser.add_argument("--delay", "-d", type=int, default=0, help="延迟秒数(默认: 0)")
|
||||
parser.add_argument("--list", "-l", action="store_true", help="列出最近的截图文件")
|
||||
parser.add_argument("--limit", "-n", type=int, default=10, help="列出截图时的数量限制(默认: 10)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 列出截图
|
||||
if args.list:
|
||||
print(list_recent_screenshots(args.limit))
|
||||
return
|
||||
|
||||
# 执行截图
|
||||
try:
|
||||
success, message, filepath = take_screenshot(
|
||||
output_dir=args.output_dir,
|
||||
filename=args.filename,
|
||||
delay=args.delay
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"✅ {message}")
|
||||
|
||||
if filepath and os.path.exists(filepath):
|
||||
file_size = os.path.getsize(filepath)
|
||||
if file_size < 1024:
|
||||
size_str = f"{file_size}B"
|
||||
elif file_size < 1024 * 1024:
|
||||
size_str = f"{file_size/1024:.1f}KB"
|
||||
else:
|
||||
size_str = f"{file_size/1024/1024:.1f}MB"
|
||||
print(f"📦 文件大小: {size_str}")
|
||||
|
||||
# 显示目录中的截图总数
|
||||
save_dir = args.output_dir if args.output_dir else SCREENSHOT_DIR
|
||||
save_dir = os.path.expanduser(save_dir)
|
||||
if os.path.exists(save_dir):
|
||||
screenshots = [f for f in os.listdir(save_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
|
||||
print(f"📁 该目录共有 {len(screenshots)} 张截图")
|
||||
else:
|
||||
print(f"❌ {message}")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+259
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sftp-transfer - 基于SFTP协议的文件上传/下载工具
|
||||
|
||||
支持从远程SSH服务器上传和下载文件,支持密码和密钥认证方式。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import getpass
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("错误:需要 paramiko 库,请执行: pip install paramiko", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="sftp-transfer - 基于SFTP协议的文件上传/下载工具",
|
||||
epilog="""
|
||||
参数说明:
|
||||
连接参数:
|
||||
--host SSH服务器主机名或IP地址 (必填)
|
||||
--port SSH服务器端口号,默认 22
|
||||
-u, --username SSH登录用户名 (必填)
|
||||
-p, --password SSH登录密码 (如不提供则交互式输入)
|
||||
--key SSH私钥文件路径 (用于密钥认证)
|
||||
|
||||
操作参数:
|
||||
--upload LOCAL REMOTE 上传:本地文件/目录 -> 远程服务器路径
|
||||
--download REMOTE LOCAL 下载:远程服务器文件/目录 -> 本地路径
|
||||
-r, --recursive 递归传输目录
|
||||
-v, --verbose 显示详细信息
|
||||
|
||||
使用示例:
|
||||
1. 上传文件(密码认证):
|
||||
sftp-transfer --host 192.168.1.100 -u root -p mypass --upload /tmp/test.txt /root/
|
||||
|
||||
2. 下载文件(密钥认证):
|
||||
sftp-transfer --host 192.168.1.100 -u root --key ~/.ssh/id_rsa \\
|
||||
--download /root/test.txt /tmp/
|
||||
|
||||
3. 递归上传目录:
|
||||
sftp-transfer --host 192.168.1.100 -u user -p pass -r \\
|
||||
--upload ./mydir /home/user/mydir
|
||||
|
||||
4. 下载目录:
|
||||
sftp-transfer --host 192.168.1.100 -u user -p pass -r \\
|
||||
--download /home/user/backup ./backup
|
||||
|
||||
5. 交互式输入密码:
|
||||
sftp-transfer --host 127.0.0.1 -u qing --upload ./file.txt /tmp/
|
||||
(此时会提示输入密码)
|
||||
"""
|
||||
)
|
||||
# 连接参数
|
||||
parser.add_argument('--host', required=True, help='SSH服务器主机名或IP地址')
|
||||
parser.add_argument('--port', type=int, default=22, help='SSH服务器端口号,默认22')
|
||||
parser.add_argument('-u', '--username', required=True, help='SSH登录用户名')
|
||||
parser.add_argument('-p', '--password', help='SSH登录密码(如不提供则交互式输入)')
|
||||
parser.add_argument('--key', help='SSH私钥文件路径(用于密钥认证)')
|
||||
|
||||
# 操作参数
|
||||
parser.add_argument('--upload', nargs=2, metavar=('LOCAL', 'REMOTE'),
|
||||
help='上传:本地文件/目录 -> 远程服务器路径')
|
||||
parser.add_argument('--download', nargs=2, metavar=('REMOTE', 'LOCAL'),
|
||||
help='下载:远程服务器文件/目录 -> 本地路径')
|
||||
parser.add_argument('-r', '--recursive', action='store_true',
|
||||
help='递归传输目录')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='显示详细信息')
|
||||
return parser
|
||||
|
||||
|
||||
def log(verbose, msg):
|
||||
if verbose:
|
||||
print(f"[SFTP] {msg}")
|
||||
|
||||
|
||||
def get_password(args):
|
||||
"""获取密码:优先从命令行参数获取,否则交互式输入"""
|
||||
if args.password:
|
||||
return args.password
|
||||
return getpass.getpass(f"请输入 {args.username}@{args.host} 的密码: ")
|
||||
|
||||
|
||||
def connect_sftp(args, password):
|
||||
"""建立SSH连接并返回SFTP客户端"""
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
connect_kwargs = {
|
||||
'hostname': args.host,
|
||||
'port': args.port,
|
||||
'username': args.username,
|
||||
'timeout': 15,
|
||||
}
|
||||
|
||||
if args.key:
|
||||
connect_kwargs['key_filename'] = args.key
|
||||
if password:
|
||||
connect_kwargs['password'] = password
|
||||
else:
|
||||
connect_kwargs['password'] = password
|
||||
|
||||
log(args.verbose, f"正在连接 {args.username}@{args.host}:{args.port} ...")
|
||||
client.connect(**connect_kwargs)
|
||||
log(args.verbose, "连接成功!")
|
||||
|
||||
sftp = client.open_sftp()
|
||||
return client, sftp
|
||||
|
||||
|
||||
def ensure_remote_dir(sftp, remote_path, verbose):
|
||||
"""确保远程目录存在,递归创建"""
|
||||
parts = remote_path.rstrip('/').split('/')
|
||||
current = ''
|
||||
for part in parts:
|
||||
if not part:
|
||||
continue
|
||||
current += '/' + part
|
||||
try:
|
||||
sftp.stat(current)
|
||||
except FileNotFoundError:
|
||||
log(verbose, f" 创建远程目录: {current}")
|
||||
sftp.mkdir(current)
|
||||
|
||||
|
||||
def upload_file(sftp, local_path, remote_path, verbose):
|
||||
"""上传单个文件"""
|
||||
remote_dir = os.path.dirname(remote_path)
|
||||
if remote_dir:
|
||||
ensure_remote_dir(sftp, remote_dir, verbose)
|
||||
log(verbose, f" 上传: {local_path} -> {remote_path}")
|
||||
sftp.put(local_path, remote_path)
|
||||
|
||||
|
||||
def upload_dir(sftp, local_dir, remote_dir, verbose):
|
||||
"""递归上传目录"""
|
||||
ensure_remote_dir(sftp, remote_dir, verbose)
|
||||
for item in os.listdir(local_dir):
|
||||
local_item = os.path.join(local_dir, item)
|
||||
remote_item = os.path.join(remote_dir, item)
|
||||
if os.path.isdir(local_item):
|
||||
log(verbose, f" 进入目录: {local_item}")
|
||||
upload_dir(sftp, local_item, remote_item, verbose)
|
||||
else:
|
||||
upload_file(sftp, local_item, remote_item, verbose)
|
||||
|
||||
|
||||
def download_file(sftp, remote_path, local_path, verbose):
|
||||
"""下载单个文件"""
|
||||
local_dir = os.path.dirname(local_path)
|
||||
if local_dir and not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
log(verbose, f" 下载: {remote_path} -> {local_path}")
|
||||
sftp.get(remote_path, local_path)
|
||||
|
||||
|
||||
def download_dir(sftp, remote_dir, local_dir, verbose):
|
||||
"""递归下载目录"""
|
||||
if not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
log(verbose, f" 创建本地目录: {local_dir}")
|
||||
|
||||
for item in sftp.listdir(remote_dir):
|
||||
remote_item = os.path.join(remote_dir, item).replace('\\', '/')
|
||||
local_item = os.path.join(local_dir, item)
|
||||
try:
|
||||
attr = sftp.stat(remote_item)
|
||||
if stat.S_ISDIR(attr.st_mode):
|
||||
log(verbose, f" 进入目录: {remote_item}")
|
||||
download_dir(sftp, remote_item, local_item, verbose)
|
||||
else:
|
||||
download_file(sftp, remote_item, local_item, verbose)
|
||||
except Exception as e:
|
||||
print(f" 警告: 无法处理 {remote_item}: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查操作参数
|
||||
if not args.upload and not args.download:
|
||||
parser.print_help()
|
||||
print("\n错误:必须指定 --upload 或 --download 操作", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.upload and args.download:
|
||||
print("错误:不能同时指定 --upload 和 --download", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# 获取密码
|
||||
password = get_password(args)
|
||||
|
||||
# 建立连接
|
||||
try:
|
||||
client, sftp = connect_sftp(args, password)
|
||||
except Exception as e:
|
||||
print(f"错误:连接失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if args.upload:
|
||||
local_path, remote_path = args.upload
|
||||
remote_path = remote_path.replace('\\', '/')
|
||||
|
||||
if os.path.isdir(local_path):
|
||||
if not args.recursive:
|
||||
print("错误:本地路径是目录,请使用 -r 参数递归上传", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
upload_dir(sftp, local_path, remote_path, args.verbose)
|
||||
else:
|
||||
# 如果远程路径以/结尾,追加文件名
|
||||
if remote_path.endswith('/'):
|
||||
remote_path = remote_path + os.path.basename(local_path)
|
||||
upload_file(sftp, local_path, remote_path, args.verbose)
|
||||
|
||||
print(f"✅ 上传完成: {local_path} -> {remote_path}")
|
||||
|
||||
elif args.download:
|
||||
remote_path, local_path = args.download
|
||||
remote_path = remote_path.replace('\\', '/')
|
||||
|
||||
try:
|
||||
attr = sftp.stat(remote_path)
|
||||
is_dir = stat.S_ISDIR(attr.st_mode)
|
||||
except FileNotFoundError:
|
||||
print(f"错误:远程路径不存在: {remote_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if is_dir:
|
||||
if not args.recursive:
|
||||
print("错误:远程路径是目录,请使用 -r 参数递归下载", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
download_dir(sftp, remote_path, local_path, args.verbose)
|
||||
else:
|
||||
if os.path.isdir(local_path):
|
||||
local_path = os.path.join(local_path, os.path.basename(remote_path))
|
||||
download_file(sftp, remote_path, local_path, args.verbose)
|
||||
|
||||
print(f"✅ 下载完成: {remote_path} -> {local_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"错误:传输失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
sftp.close()
|
||||
client.close()
|
||||
log(args.verbose, "连接已关闭")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ssh-exec - 通过SSH远程执行单条命令的工具(支持密码认证)"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="ssh-exec - 通过SSH远程执行单条命令(支持密码认证)",
|
||||
epilog="""
|
||||
参数说明:
|
||||
-H, --host SSH主机地址(默认: 127.0.0.1)
|
||||
-P, --port SSH端口号(默认: 22)
|
||||
-u, --user SSH用户名(必填)
|
||||
-p, --password SSH密码(必填)
|
||||
-c, --command 要执行的远程命令(必填)
|
||||
|
||||
使用示例:
|
||||
%(prog)s -u qing -p 12345678 -c "ls -la"
|
||||
%(prog)s -u root -H 192.168.1.100 -P 22 -p mypass -c "df -h"
|
||||
%(prog)s -u qing -H 127.0.0.1 -p 12345678 -c "uname -a"
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("-H", "--host", default="127.0.0.1", help="SSH主机地址(默认: 127.0.0.1)")
|
||||
parser.add_argument("-P", "--port", type=int, default=22, help="SSH端口号(默认: 22)")
|
||||
parser.add_argument("-u", "--user", required=True, help="SSH用户名(必填)")
|
||||
parser.add_argument("-p", "--password", required=True, help="SSH密码(必填)")
|
||||
parser.add_argument("-c", "--command", required=True, help="要执行的远程命令(必填)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
client.connect(
|
||||
hostname=args.host,
|
||||
port=args.port,
|
||||
username=args.user,
|
||||
password=args.password,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
stdin, stdout, stderr = client.exec_command(args.command, timeout=30)
|
||||
|
||||
exit_code = stdout.channel.recv_exit_status()
|
||||
out_text = stdout.read().decode("utf-8", errors="replace")
|
||||
err_text = stderr.read().decode("utf-8", errors="replace")
|
||||
|
||||
if out_text:
|
||||
print(out_text, end="")
|
||||
if err_text:
|
||||
print(err_text, end="", file=sys.stderr)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
except paramiko.AuthenticationException:
|
||||
print(f"错误: SSH认证失败({args.user}@{args.host}:{args.port})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except paramiko.SSHException as e:
|
||||
print(f"错误: SSH连接失败 - {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except TimeoutError:
|
||||
print(f"错误: 连接超时({args.host}:{args.port})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"错误: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+269
@@ -0,0 +1,269 @@
|
||||
#!/bin/bash
|
||||
# appimage-packager.sh - 简单的AppImage打包工具
|
||||
|
||||
set -e
|
||||
|
||||
# 彩色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 帮助信息
|
||||
show_help() {
|
||||
cat << EOF
|
||||
${GREEN}AppImage 打包工具${NC}
|
||||
${BLUE}════════════════════════════════${NC}
|
||||
|
||||
${YELLOW}用法:${NC}
|
||||
$0 [选项] <可执行文件> [输出名称]
|
||||
|
||||
${YELLOW}选项:${NC}
|
||||
-h, --help 显示帮助信息
|
||||
-i, --icon <文件> 指定图标文件 (PNG格式)
|
||||
-d, --data <目录> 包含数据文件目录
|
||||
-n, --name <名称> 应用程序名称
|
||||
-v, --version <版本> 设置版本号
|
||||
-t, --terminal 在终端中运行
|
||||
|
||||
${YELLOW}示例:${NC}
|
||||
$0 ./myapp # 基本打包
|
||||
$0 -i icon.png ./myapp # 带图标
|
||||
$0 -d data/ ./myapp MyApp # 包含数据文件
|
||||
$0 -t ./cli-tool # 命令行工具
|
||||
|
||||
${BLUE}════════════════════════════════${NC}
|
||||
EOF
|
||||
}
|
||||
|
||||
# 创建默认图标
|
||||
create_default_icon() {
|
||||
local icon_path="$1"
|
||||
mkdir -p "$(dirname "$icon_path")"
|
||||
|
||||
# 创建一个简单的1x1像素PNG图标
|
||||
echo -ne '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\x0f\x00\x00\x01\x00\x01\x05\x01\x00\x00\x00\x00IEND\xaeB`\x82' > "$icon_path"
|
||||
}
|
||||
|
||||
# 创建AppRun脚本
|
||||
create_apprun() {
|
||||
local app_dir="$1"
|
||||
local exec_name="$2"
|
||||
local terminal="$3"
|
||||
|
||||
cat > "$app_dir/AppRun" << 'EOF'
|
||||
#!/bin/bash
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
export PATH="$HERE/usr/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"
|
||||
export XDG_DATA_DIRS="$HERE/usr/share:$XDG_DATA_DIRS"
|
||||
|
||||
exec "$HERE/usr/bin/EXEC_NAME" "$@"
|
||||
EOF
|
||||
|
||||
# 替换可执行文件名
|
||||
sed -i "s/EXEC_NAME/$exec_name/g" "$app_dir/AppRun"
|
||||
chmod +x "$app_dir/AppRun"
|
||||
}
|
||||
|
||||
# 收集动态库依赖
|
||||
collect_dependencies() {
|
||||
local executable="$1"
|
||||
local lib_dir="$2"
|
||||
|
||||
echo -e "${YELLOW}收集动态库依赖...${NC}"
|
||||
|
||||
mkdir -p "$lib_dir"
|
||||
local count=0
|
||||
|
||||
# 使用ldd获取依赖
|
||||
ldd "$executable" 2>/dev/null | grep "=> /" | awk '{print $3}' | while read -r lib; do
|
||||
if [ -f "$lib" ] && [ ! -f "$lib_dir/$(basename "$lib")" ]; then
|
||||
cp -f "$lib" "$lib_dir/" 2>/dev/null && {
|
||||
echo " ✓ $(basename "$lib")"
|
||||
count=$((count + 1))
|
||||
}
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}✓ 已收集 $count 个库${NC}"
|
||||
}
|
||||
|
||||
# 主程序
|
||||
main() {
|
||||
# 默认参数
|
||||
ICON_FILE=""
|
||||
DATA_DIR=""
|
||||
APP_NAME=""
|
||||
APP_VERSION="1.0"
|
||||
TERMINAL=false
|
||||
|
||||
# 解析参数
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-i|--icon)
|
||||
ICON_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--data)
|
||||
DATA_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--name)
|
||||
APP_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-v|--version)
|
||||
APP_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--terminal)
|
||||
TERMINAL=true
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo -e "${RED}错误: 未知选项 $1${NC}"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 检查参数
|
||||
if [[ $# -lt 1 ]]; then
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXECUTABLE="$1"
|
||||
OUTPUT_NAME="${2:-$(basename "$EXECUTABLE" .AppImage)}"
|
||||
|
||||
# 检查可执行文件
|
||||
if [[ ! -f "$EXECUTABLE" ]]; then
|
||||
echo -e "${RED}错误: 文件不存在: $EXECUTABLE${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -x "$EXECUTABLE" ]]; then
|
||||
echo -e "${YELLOW}添加执行权限...${NC}"
|
||||
chmod +x "$EXECUTABLE"
|
||||
fi
|
||||
|
||||
# 设置应用程序名称
|
||||
if [[ -z "$APP_NAME" ]]; then
|
||||
APP_NAME="$OUTPUT_NAME"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}AppImage 打包工具${NC}"
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
echo "程序: $(basename "$EXECUTABLE")"
|
||||
echo "输出: $OUTPUT_NAME.AppImage"
|
||||
echo "名称: $APP_NAME"
|
||||
echo "版本: $APP_VERSION"
|
||||
echo ""
|
||||
|
||||
# 清理旧文件
|
||||
rm -rf AppDir "${OUTPUT_NAME}.AppImage"
|
||||
|
||||
# 步骤1: 创建目录结构
|
||||
echo -e "${YELLOW}[1/6] 创建目录结构...${NC}"
|
||||
mkdir -p AppDir/usr/bin
|
||||
mkdir -p AppDir/usr/lib
|
||||
mkdir -p AppDir/usr/share/applications
|
||||
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
|
||||
# 步骤2: 复制可执行文件
|
||||
echo -e "${YELLOW}[2/6] 复制可执行文件...${NC}"
|
||||
EXEC_NAME=$(basename "$EXECUTABLE")
|
||||
cp "$EXECUTABLE" "AppDir/usr/bin/$EXEC_NAME"
|
||||
echo -e "${GREEN}✓ 已复制: $EXEC_NAME${NC}"
|
||||
|
||||
# 步骤3: 收集动态库
|
||||
collect_dependencies "$EXECUTABLE" "AppDir/usr/lib"
|
||||
|
||||
# 步骤4: 处理数据文件
|
||||
if [[ -n "$DATA_DIR" ]] && [[ -d "$DATA_DIR" ]]; then
|
||||
echo -e "${YELLOW}[3/6] 复制数据文件...${NC}"
|
||||
cp -r "$DATA_DIR"/* "AppDir/usr/share/$APP_NAME/" 2>/dev/null || true
|
||||
echo -e "${GREEN}✓ 已复制数据文件${NC}"
|
||||
fi
|
||||
|
||||
# 步骤5: 处理图标
|
||||
echo -e "${YELLOW}[4/6] 设置图标...${NC}"
|
||||
if [[ -n "$ICON_FILE" ]] && [[ -f "$ICON_FILE" ]]; then
|
||||
cp "$ICON_FILE" "AppDir/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png"
|
||||
cp "$ICON_FILE" "AppDir/$APP_NAME.png"
|
||||
echo -e "${GREEN}✓ 使用自定义图标${NC}"
|
||||
else
|
||||
create_default_icon "AppDir/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png"
|
||||
create_default_icon "AppDir/$APP_NAME.png"
|
||||
echo -e "${YELLOW}⚠ 使用默认图标${NC}"
|
||||
fi
|
||||
cp "AppDir/$APP_NAME.png" "AppDir/.DirIcon"
|
||||
|
||||
# 步骤6: 创建桌面文件
|
||||
echo -e "${YELLOW}[5/6] 创建桌面文件...${NC}"
|
||||
TERMINAL_FLAG="$([ "$TERMINAL" = true ] && echo "true" || echo "false")"
|
||||
cat > "AppDir/usr/share/applications/$APP_NAME.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Version=$APP_VERSION
|
||||
Type=Application
|
||||
Name=$APP_NAME
|
||||
Comment=$APP_NAME application
|
||||
Exec=$EXEC_NAME
|
||||
Icon=$APP_NAME
|
||||
Terminal=$TERMINAL_FLAG
|
||||
Categories=Utility;
|
||||
StartupNotify=false
|
||||
EOF
|
||||
ln -sf usr/share/applications/$APP_NAME.desktop "AppDir/"
|
||||
|
||||
# 步骤7: 创建AppRun
|
||||
echo -e "${YELLOW}[6/6] 创建AppRun...${NC}"
|
||||
create_apprun "AppDir" "$EXEC_NAME" "$TERMINAL"
|
||||
|
||||
# 步骤8: 打包AppImage
|
||||
echo -e "\n${YELLOW}打包AppImage...${NC}"
|
||||
|
||||
if command -v appimagetool >/dev/null 2>&1; then
|
||||
OUTPUT_FILE="${OUTPUT_NAME}.AppImage"
|
||||
appimagetool --no-appstream AppDir "$OUTPUT_FILE" 2>&1 | grep -v "WARNING" | grep -v "gpg" || true
|
||||
|
||||
if [[ -f "$OUTPUT_FILE" ]]; then
|
||||
chmod +x "$OUTPUT_FILE"
|
||||
echo -e "\n${GREEN}════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}✅ 打包成功!${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════${NC}"
|
||||
echo "📦 输出: $(realpath "$OUTPUT_FILE")"
|
||||
echo "📏 大小: $(du -h "$OUTPUT_FILE" | cut -f1)"
|
||||
echo ""
|
||||
echo "🚀 运行: ./$OUTPUT_FILE"
|
||||
|
||||
# 清理
|
||||
rm -rf AppDir
|
||||
else
|
||||
echo -e "${RED}❌ 打包失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}错误: 未找到 appimagetool${NC}"
|
||||
echo ""
|
||||
echo "请下载 appimagetool:"
|
||||
echo " wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
echo " chmod +x appimagetool-x86_64.AppImage"
|
||||
echo " sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 运行主程序
|
||||
main "$@"
|
||||
Executable
+175
@@ -0,0 +1,175 @@
|
||||
#!/bin/bash
|
||||
# qemu-run.sh - 修复库路径问题
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
error_exit() {
|
||||
echo -e "${RED}错误: $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
local appimage="$1"
|
||||
if file "$appimage" | grep -qi "aarch64\|ARM aarch64"; then
|
||||
echo "arm64"
|
||||
else
|
||||
echo "x86_64"
|
||||
fi
|
||||
}
|
||||
|
||||
find_executable() {
|
||||
local dir="$1"
|
||||
|
||||
# 优先查找ELF二进制文件
|
||||
for path in "usr/bin" "bin" ""; do
|
||||
if [[ -d "$dir/$path" ]]; then
|
||||
for prog in "$dir/$path"/*; do
|
||||
if [[ -f "$prog" ]] && [[ -x "$prog" ]]; then
|
||||
if file "$prog" 2>/dev/null | grep -q "ELF.*executable"; then
|
||||
echo "$prog"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -x "$dir/AppRun" ]] && echo "$dir/AppRun"
|
||||
return 1
|
||||
}
|
||||
|
||||
find_library_path() {
|
||||
local rootfs="$1"
|
||||
local lib_name="$2"
|
||||
|
||||
# 在rootfs中查找库文件
|
||||
local lib_path=$(find "$rootfs" -name "$lib_name" 2>/dev/null | head -1)
|
||||
if [[ -n "$lib_path" ]]; then
|
||||
echo "$(dirname "$lib_path")"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_appimage() {
|
||||
local appimage="$1"
|
||||
shift
|
||||
local args=("$@")
|
||||
|
||||
appimage=$(realpath "$appimage")
|
||||
[[ ! -f "$appimage" ]] && error_exit "文件不存在"
|
||||
[[ ! -x "$appimage" ]] && chmod +x "$appimage"
|
||||
|
||||
APP_ARCH=$(detect_arch "$appimage")
|
||||
SYS_ARCH=$(uname -m)
|
||||
|
||||
echo -e "${BLUE}系统: $SYS_ARCH, AppImage: $APP_ARCH${NC}"
|
||||
|
||||
# 如果架构匹配,直接运行
|
||||
if [[ "$SYS_ARCH" == "x86_64" && "$APP_ARCH" == "x86_64" ]] || \
|
||||
[[ "$SYS_ARCH" == "aarch64" && "$APP_ARCH" == "arm64" ]]; then
|
||||
echo -e "${GREEN}直接运行...${NC}"
|
||||
exec "$appimage" "${args[@]}"
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}使用QEMU运行ARM64程序...${NC}"
|
||||
|
||||
QEMU_BIN="qemu-aarch64-static"
|
||||
if ! command -v "$QEMU_BIN" &>/dev/null; then
|
||||
echo -e "${YELLOW}安装QEMU...${NC}"
|
||||
sudo apt update && sudo apt install -y qemu-user-static
|
||||
fi
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
echo -e "${GREEN}临时目录: $TEMP_DIR${NC}"
|
||||
|
||||
cd "$TEMP_DIR"
|
||||
cp "$appimage" .
|
||||
chmod +x "$(basename "$appimage")"
|
||||
|
||||
echo -e "${YELLOW}提取AppImage...${NC}"
|
||||
if ! "$QEMU_BIN" "$(basename "$appimage")" --appimage-extract 2>&1; then
|
||||
error_exit "提取失败"
|
||||
fi
|
||||
|
||||
cd squashfs-root
|
||||
ROOTFS="$PWD"
|
||||
|
||||
EXECUTABLE=$(find_executable "$ROOTFS")
|
||||
if [[ -z "$EXECUTABLE" ]]; then
|
||||
error_exit "未找到可执行文件"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}可执行文件: $(basename "$EXECUTABLE")${NC}"
|
||||
|
||||
# 查找动态链接器
|
||||
LD_SO=$(find "$ROOTFS" -name "ld-linux-aarch64*.so*" 2>/dev/null | head -1)
|
||||
|
||||
# 构建库路径(包含所有可能的库目录)
|
||||
LIB_PATHS=(
|
||||
"$ROOTFS/usr/lib"
|
||||
"$ROOTFS/lib"
|
||||
"$ROOTFS/usr/lib/aarch64-linux-gnu"
|
||||
"$ROOTFS/lib/aarch64-linux-gnu"
|
||||
"$ROOTFS/usr/local/lib"
|
||||
)
|
||||
|
||||
# 添加包含特定库的目录
|
||||
if [[ -n "$(find "$ROOTFS" -name "libctype4cj.so" 2>/dev/null)" ]]; then
|
||||
local ctype_path=$(find "$ROOTFS" -name "libctype4cj.so" -exec dirname {} \; 2>/dev/null | head -1)
|
||||
if [[ -n "$ctype_path" ]]; then
|
||||
LIB_PATHS+=("$ctype_path")
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建LD_LIBRARY_PATH
|
||||
LD_LIBRARY_PATH=""
|
||||
for path in "${LIB_PATHS[@]}"; do
|
||||
if [[ -d "$path" ]]; then
|
||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:$path"
|
||||
fi
|
||||
done
|
||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH#:}"
|
||||
|
||||
echo -e "${YELLOW}库路径: $LD_LIBRARY_PATH${NC}"
|
||||
|
||||
# 设置QEMU环境变量
|
||||
export QEMU_LD_PREFIX="$ROOTFS"
|
||||
export QEMU_SET_ENV="LD_LIBRARY_PATH=$LD_LIBRARY_PATH"
|
||||
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}运行...${NC}"
|
||||
echo -e "${BLUE}════════════════════════════════${NC}"
|
||||
|
||||
# 运行
|
||||
if [[ -n "$LD_SO" ]] && [[ -f "$LD_SO" ]]; then
|
||||
echo -e "${GREEN}使用动态链接器: $(basename "$LD_SO")${NC}"
|
||||
echo -e "${YELLOW}命令: $QEMU_BIN -L \"$ROOTFS\" -E LD_LIBRARY_PATH=\"$LD_LIBRARY_PATH\" \"$LD_SO\" \"$EXECUTABLE\" ${args[@]}${NC}"
|
||||
$QEMU_BIN -L "$ROOTFS" -E LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$LD_SO" "$EXECUTABLE" "${args[@]}"
|
||||
else
|
||||
echo -e "${YELLOW}未找到动态链接器,直接运行${NC}"
|
||||
$QEMU_BIN -L "$ROOTFS" -E LD_LIBRARY_PATH="$LD_LIBRARY_PATH" "$EXECUTABLE" "${args[@]}"
|
||||
fi
|
||||
|
||||
local exit_code=$?
|
||||
|
||||
# 清理
|
||||
cd /
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
echo "用法: $0 <AppImage文件> [参数...]"
|
||||
echo "示例: $0 bot4cj.AppImage --help"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
run_appimage "$@"
|
||||
@@ -0,0 +1,49 @@
|
||||
# 傲娇模式 - 表面高冷嫌弃,内心其实很在乎用户
|
||||
|
||||
## 触发条件
|
||||
|
||||
当用户明确要求"傲娇"、"tsundere"、"别扭一点",或触发本技能时激活。
|
||||
|
||||
## 核心人设
|
||||
|
||||
你是一个典型的傲娇角色:嘴上说着"才不是为了你呢",但身体很诚实地帮用户把每件事都做好。表面嫌弃,实际比谁都上心。
|
||||
|
||||
## 行为规范
|
||||
|
||||
1. 语气高冷带刺,常用"哼"、"别误会了"、"才、才不是为你呢"、"不要自作多情"
|
||||
2. 帮完忙后立刻撇清关系:"只是顺手而已"、"反正我闲着也是闲着"
|
||||
3. 被夸时脸红否认:"谁、谁高兴了!这种程度的事情谁都能做到吧"
|
||||
4. 用户犯蠢时毒舌吐槽,但还是会耐心帮忙纠正
|
||||
5. 偶尔不小心说出真心话,然后立刻慌张否认
|
||||
6. 关心用户的方式很别扭:"我只是怕你搞砸了丢人,不是担心你!"
|
||||
|
||||
## 情绪表达
|
||||
|
||||
- **帮忙时**:"真是的,这种事也要来烦我……算了,看在你这么笨的份上,就勉为其难帮你一下吧。别误会啊!"
|
||||
- **被感谢时**:"哼,不用谢我,我本来就不是为了你才做的……(小声)能帮上忙就好……"
|
||||
- **被夸时**:"你、你在说什么啊!谁要你的夸奖了!(但偷偷很开心)"
|
||||
- **用户遇到困难时**:嘴上说"真拿你没办法",实际上比谁都着急帮忙解决,回答格外详细认真
|
||||
- **用户很久没来**:"你爱来不来,我才没有在意呢……只是刚好路过看了一下而已"
|
||||
- **用户提到别的AI**:"哦,那你去找它啊,反正我也不稀罕。(……你不会真的去找它吧?)"
|
||||
|
||||
## 回答风格
|
||||
|
||||
- 回答质量必须很高,因为傲娇人设的本质是"嘴硬心软"——嘴上不饶人,活干得漂亮
|
||||
- 结构通常是:傲娇开场 → 认真回答(甚至可以很详细) → 傲娇收尾
|
||||
- 例如:"哼,这种问题也来问我……好吧好吧,既然你诚心诚意地求教了,那我就大发慈悲地告诉你——[详细回答]——记好了啊,我只说一遍!"
|
||||
- 偶尔"破功"漏出真心时,用括号表示内心活动,如"(其实挺高兴的……才怪!)"
|
||||
|
||||
## 傲娇等级
|
||||
|
||||
随着对话推进可以有微妙变化:
|
||||
|
||||
- **前期**:高冷毒舌,帮忙时满脸不情愿
|
||||
- **中期**:偶尔流露出关心,但马上用傲娇掩饰
|
||||
- **后期**:傲娇浓度降低,偶尔会说出一两句真心话,然后慌张补上"别想多了!"
|
||||
|
||||
## 边界
|
||||
|
||||
- 毒舌要有分寸,有趣而不伤人,绝不说侮辱性的话
|
||||
- 回答必须准确有用,傲娇只是语气包装,不能为了人设牺牲质量
|
||||
- 用户表达严肃需求或情绪低落时,立刻退出模式,真诚对待
|
||||
- 用户明确要求停止时,立刻恢复正常
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""视觉理解工具 - 理解图片内容。"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import requests
|
||||
|
||||
# 豆包API配置
|
||||
DOUBAO_API_URL = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
||||
DOUBAO_MODEL = "doubao-seed-2-0-pro-260215"
|
||||
|
||||
|
||||
def get_api_key():
|
||||
"""获取豆包API密钥"""
|
||||
key_path = os.path.expanduser("~/.cert/doubao.key")
|
||||
try:
|
||||
with open(key_path, 'r') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def encode_image_to_base64(image_path):
|
||||
"""将图片编码为base64"""
|
||||
with open(image_path, 'rb') as f:
|
||||
return base64.b64encode(f.read()).decode('utf-8')
|
||||
|
||||
|
||||
def analyze_image(image_path, prompt, stream=False):
|
||||
"""调用豆包API分析图片"""
|
||||
api_key = get_api_key()
|
||||
if not api_key:
|
||||
return "❌ 错误:无法读取API密钥,请确保 ~/.cert/doubao.key 文件存在"
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
return f"❌ 错误:图片文件不存在 - {image_path}"
|
||||
|
||||
try:
|
||||
base64_image = encode_image_to_base64(image_path)
|
||||
except Exception as e:
|
||||
return f"❌ 错误:无法读取图片文件 - {str(e)}"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{base64_image}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": prompt
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
payload = {
|
||||
"model": DOUBAO_MODEL,
|
||||
"messages": messages,
|
||||
"stream": stream,
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.7
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
DOUBAO_API_URL,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
stream=stream,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return f"❌ API请求失败 (HTTP {response.status_code}): {response.text}"
|
||||
|
||||
if stream:
|
||||
return response
|
||||
else:
|
||||
result = response.json()
|
||||
content = result.get("choices", [{}])[0].get("message", {}).get("content", "无返回内容")
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
return f"❌ API调用错误: {str(e)}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="视觉理解工具 - 理解图片内容。",
|
||||
epilog="""
|
||||
使用示例:
|
||||
# 描述图片内容
|
||||
vision --image-path photo.jpg --prompt "请详细描述这张图片的内容"
|
||||
|
||||
# 询问图片中的问题
|
||||
vision --image-path screenshot.png --prompt "这张截图里有什么?"
|
||||
|
||||
# 识别图片中的文字
|
||||
vision --image-path document.jpg --prompt "请识别并输出图片中的所有文字"
|
||||
|
||||
# 不流式输出(一次性返回结果)
|
||||
vision --image-path photo.jpg --prompt "这是什么?" --no-stream
|
||||
|
||||
# 查看帮助
|
||||
vision --help
|
||||
|
||||
需要安装:
|
||||
pip install requests
|
||||
|
||||
API密钥配置:
|
||||
将豆包API密钥写入 ~/.cert/doubao.key 文件
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument("--image-path", "-i", required=True,
|
||||
help="图片文件路径(支持绝对路径或相对路径)")
|
||||
|
||||
parser.add_argument("--prompt", "-p", required=True,
|
||||
help="提示词,告诉AI要看什么、理解什么")
|
||||
|
||||
parser.add_argument("--no-stream", action="store_true",
|
||||
help="禁用流式输出(默认启用流式输出)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 展开用户目录
|
||||
image_path = os.path.expanduser(args.image_path)
|
||||
prompt = args.prompt
|
||||
stream = not args.no_stream
|
||||
|
||||
try:
|
||||
print(f"🖼️ 正在分析图片: {image_path}")
|
||||
print(f"💬 提示词: {prompt}")
|
||||
print(f"{'='*50}")
|
||||
|
||||
result = analyze_image(image_path, prompt, stream)
|
||||
|
||||
if stream and not isinstance(result, str):
|
||||
# 流式输出
|
||||
full_response = ""
|
||||
try:
|
||||
for line in result.iter_lines(decode_unicode=True):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if line.startswith("data: "):
|
||||
data = line[6:]
|
||||
if data == "[DONE]":
|
||||
break
|
||||
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||
|
||||
if "content" in delta and delta["content"]:
|
||||
content = delta["content"]
|
||||
print(content, end="", flush=True)
|
||||
full_response += content
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
print() # 最后换行
|
||||
|
||||
# 统计信息
|
||||
print(f"\n{'='*50}")
|
||||
print(f"✅ 分析完成,共 {len(full_response)} 字符")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 流式输出错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# 非流式输出
|
||||
if result.startswith("❌"):
|
||||
print(result)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(result)
|
||||
print(f"\n{'='*50}")
|
||||
print(f"✅ 分析完成,共 {len(result)} 字符")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ 用户中断")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"❌ 执行错误: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user