- gitclient: delete 限工作区内 .git 仓库(S2);分支名白名单防选项注入(S3) - camera/screenshot: --filename 拒绝分隔符/绝对路径/父目录引用(S9/S10) - facerec: person_name 白名单(兼容中文)接入 add/remove/get-info(S11) - vision: 图片魔数白名单校验,data URL MIME 按实测类型(S12) - sftp-transfer: listdir 条目名双向过滤防恶意服务端目录逃逸(S6) - test-auto-appimage: exec_name 白名单防 sed 替换注入(S13) 测试 493 passed / 4 failed(既有平台环境项)/ 1 skipped,零回归
229 lines
6.8 KiB
Python
Executable File
229 lines
6.8 KiB
Python
Executable File
#!/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 detect_image_mime(image_path):
|
||
"""S12(审计 2026-08-20):按魔数识别真实图片类型——
|
||
拒绝非图片文件(防任意文件读取外泄),并返回正确 MIME(此前硬编码 image/jpeg)"""
|
||
try:
|
||
with open(image_path, 'rb') as f:
|
||
head = f.read(16)
|
||
except Exception:
|
||
return None
|
||
if head.startswith(b'\xff\xd8\xff'):
|
||
return 'image/jpeg'
|
||
if head.startswith(b'\x89PNG\r\n\x1a\n'):
|
||
return 'image/png'
|
||
if head[:6] in (b'GIF87a', b'GIF89a'):
|
||
return 'image/gif'
|
||
if head.startswith(b'RIFF') and head[8:12] == b'WEBP':
|
||
return 'image/webp'
|
||
if head.startswith(b'BM'):
|
||
return 'image/bmp'
|
||
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}"
|
||
|
||
mime = detect_image_mime(image_path)
|
||
if not mime:
|
||
return f"❌ 错误:文件不是有效图片(仅支持 JPEG/PNG/GIF/WebP/BMP) - {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:{mime};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() |