Files
blackbird/aibox/skills/vision.py
T
qing 9064750f33 fix(aibox/skills): Batch M 波次2 路径边界收口(S2/S3/S6/S9/S10/S11/S12/S13)
- 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,零回归
2026-08-20 13:11:07 +08:00

229 lines
6.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()