Files
crawler-plugin/blueprints/image.py
铭坤 8a64a6cd9b new file: ali_oss.py
new file:   app.py
	new file:   app/ali_oss.py
	new file:   app/app.py
	new file:   app/app_common.py
	new file:   app/config.py
	new file:   app/coze.py
	new file:   app/generate_api.py
	new file:   app/html_crypto.py
	new file:   app/main.py
	new file:   app/web_source/admin.html
	new file:   app/web_source/brand.html
	new file:   app/web_source/home.html
	new file:   app/web_source/index.html
	new file:   app/web_source/login.html
	new file:   app_common.py
	new file:   blueprints/__init__.py
	new file:   blueprints/__pycache__/__init__.cpython-311.pyc
	new file:   blueprints/__pycache__/__init__.cpython-39.pyc
	new file:   blueprints/__pycache__/admin.cpython-311.pyc
	new file:   blueprints/__pycache__/admin.cpython-39.pyc
	new file:   blueprints/__pycache__/auth.cpython-311.pyc
	new file:   blueprints/__pycache__/auth.cpython-39.pyc
	new file:   blueprints/__pycache__/brand.cpython-311.pyc
	new file:   blueprints/__pycache__/brand.cpython-39.pyc
	new file:   blueprints/__pycache__/image.cpython-311.pyc
	new file:   blueprints/__pycache__/image.cpython-39.pyc
	new file:   blueprints/__pycache__/main.cpython-311.pyc
	new file:   blueprints/__pycache__/main.cpython-39.pyc
	new file:   blueprints/admin.py
	new file:   blueprints/auth.py
	new file:   blueprints/brand.py
	new file:   blueprints/image.py
	new file:   blueprints/main.py
	new file:   brand_spider/__pycache__/main.cpython-39.pyc
	new file:   brand_spider/__pycache__/web_dec.cpython-39.pyc
	new file:   brand_spider/main.py
	new file:   brand_spider/web_dec.py
	new file:   config.py
	new file:   coze.py
	new file:   generate_api.py
	new file:   html_crypto.py
	new file:   main.py
	new file:   static/bg.jpg
	new file:   "static/\345\223\201\347\211\214\346\226\207\346\241\243\346\240\274\345\274\217_\346\250\241\346\235\277.xlsx"
	new file:   "static/\346\250\241\346\235\2772-\344\273\245\346\226\207\344\273\266\345\244\271\346\226\271\345\274\217\344\270\212\344\274\240.zip"
	new file:   tool/.device_id
	new file:   tool/__pycache__/devices.cpython-311.pyc
	new file:   tool/__pycache__/devices.cpython-39.pyc
	new file:   tool/devices.py
2026-03-20 11:23:57 +08:00

412 lines
16 KiB
Python
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.
"""
图片生成蓝图:生成、历史、下载、拼接、版本
"""
import os
import sys
import json
import re
import io
import base64
import tempfile
import threading
import subprocess
import requests
from urllib.parse import urlparse, quote
from flask import Blueprint, request, jsonify, Response, session, send_file
from PIL import Image
from app_common import get_db, login_required, BASE_DIR
from config import STITCH_WORKFLOW_ID,client_name
image_bp = Blueprint('image', __name__)
def _load_image_from_url_or_data(url_or_data):
"""从 http(s) URL 或 data URL 加载为 PIL Image失败返回 None"""
if not url_or_data or not isinstance(url_or_data, str):
return None
try:
if url_or_data.startswith('data:'):
m = re.match(r'data:image/[^;]+;base64,(.+)', url_or_data, re.DOTALL)
if not m:
return None
raw = base64.b64decode(m.group(1).strip())
img = Image.open(io.BytesIO(raw))
elif url_or_data.startswith(('http://', 'https://')):
resp = requests.get(url_or_data, timeout=15)
resp.raise_for_status()
img = Image.open(io.BytesIO(resp.content))
else:
return None
if img.mode != 'RGB':
img = img.convert('RGB')
return img
except Exception:
return None
def _stitch_and_upload_long_image(urls):
"""将多张图片 URL 先上传获取 file_id再调用 workflow_run 拼接长图,返回 data.merged_image_url失败返回 None。"""
if not urls or not isinstance(urls, (list, tuple)):
return None
from coze import upload_file as coze_upload_file, workflow_run
file_ids = []
temp_paths = []
try:
for u in urls:
img = _load_image_from_url_or_data(u)
if img is None:
continue
fd, path = tempfile.mkstemp(suffix='.png')
try:
os.close(fd)
img.save(path)
temp_paths.append(path)
resp = coze_upload_file(path)
if resp.get('code') == 0 and resp.get('data', {}).get('id'):
file_ids.append(resp['data']['id'])
except Exception:
pass
if not file_ids:
return None
parameters = {"images": [{"file_id": fid} for fid in file_ids]}
resp = workflow_run(STITCH_WORKFLOW_ID, parameters, is_async=False)
if resp.get('code') != 0:
return None
data = resp.get('data') or {}
data = json.loads(data)
merged_image_url = data.get('merged_image_url')
if merged_image_url:
return merged_image_url
output_str = data.get('output') or ''
if output_str:
try:
outer = json.loads(output_str)
inner_str = outer.get('Output', '{}')
inner = json.loads(inner_str)
data_str = inner.get('data', '[]')
inner_data = json.loads(data_str)
merged_image_url = inner_data.get('merged_image_url')
return merged_image_url
except Exception:
pass
return None
except Exception:
return None
finally:
for p in temp_paths:
try:
os.unlink(p)
except Exception:
pass
def _sanitize_params_for_history(params):
"""移除 base64 大字段及敏感字段,仅保留可存储的请求参数"""
exclude = ('ref_images', 'proc_images', 'layout_image')
out = {}
for k, v in (params or {}).items():
if k == 'api_key':
continue
if k in exclude:
if isinstance(v, list):
out[f'{k}_count'] = len(v)
else:
out[f'{k}_count'] = 1 if v else 0
elif isinstance(v, (str, int, float, bool, type(None))):
out[k] = v
elif isinstance(v, list) and not v:
out[k] = []
elif isinstance(v, list) and isinstance(v[0], (str, int, float, bool)):
out[k] = v
else:
out[k] = str(v)[:200] if v else None
return out
@image_bp.route('/api/generate', methods=['POST'])
@login_required
def api_generate():
"""生成图片:调用 generate_api上传原图到 OSS保存历史记录"""
try:
params = request.get_json() or {}
from generate_api import generate
result = generate(params)
if result.get('success') and result.get('urls'):
long_image_url = result.get("long_image_url")
result["long_image_url"] = long_image_url
import json as _json
history_id = None
try:
hid = params.get('history_id')
if hid is not None:
try:
hid = int(hid)
except (TypeError, ValueError):
hid = None
conn = get_db()
with conn.cursor() as cur:
if hid is not None and hid > 0:
cur.execute(
"SELECT result_urls FROM image_history WHERE id=%s AND user_id=%s",
(hid, session['user_id']),
)
row = cur.fetchone()
existing_urls = []
if row and row.get('result_urls'):
try:
existing_urls = _json.loads(row['result_urls'])
except Exception:
existing_urls = []
new_urls = result.get('urls') or []
new_url = new_urls[0] if new_urls else None
idx = params.get('history_index', 0)
try:
idx = int(idx)
except (TypeError, ValueError):
idx = 0
if new_url:
if not isinstance(existing_urls, list):
existing_urls = []
while len(existing_urls) <= idx:
existing_urls.append(existing_urls[-1] if existing_urls else new_url)
existing_urls[idx] = new_url
merged_result_urls = existing_urls or new_urls
cur.execute(
"""UPDATE image_history SET panel_type=%s, original_urls=%s, params=%s, result_urls=%s
WHERE id=%s AND user_id=%s""",
(
params.get('panel_type', ''),
_json.dumps(result.get('original_urls') or []),
_json.dumps(_sanitize_params_for_history(params)),
_json.dumps(merged_result_urls),
hid,
session['user_id'],
),
)
if cur.rowcount > 0:
history_id = hid
else:
cur.execute(
"""INSERT INTO image_history (user_id, panel_type, original_urls, params, result_urls, long_image_url)
VALUES (%s, %s, %s, %s, %s, %s)""",
(
session['user_id'],
params.get('panel_type', ''),
_json.dumps(result.get('original_urls') or []),
_json.dumps(_sanitize_params_for_history(params)),
_json.dumps(result.get('urls') or []),
long_image_url,
),
)
history_id = cur.lastrowid
conn.commit()
conn.close()
except Exception:
pass
if history_id is not None:
result['history_id'] = history_id
return jsonify(result)
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({'success': False, 'urls': [], 'error': str(e)})
@image_bp.route('/api/version')
def api_version():
"""检测更新:请求 APP_UPDATE_URL 获取最新版本信息,与当前版本比较"""
current_version = (os.environ.get('APP_VERSION', '1.0.0') or '1.0.0').strip()
update_url = (os.environ.get('APP_UPDATE_URL', '') or '').strip()
result = {
'version': current_version,
'desc': '',
'url': '',
'has_update': False,
'latest_version': current_version,
'file_url': '',
}
if not update_url:
return jsonify(result)
try:
resp = requests.get(update_url, timeout=10)
resp.raise_for_status()
data = resp.json() or {}
latest_version = (data.get('version') or '').strip()
file_url = (data.get('file_url') or '').strip()
result['latest_version'] = latest_version
result['file_url'] = file_url
result['url'] = file_url
# 版本不一致则视为有更新
if latest_version and latest_version != current_version:
result['has_update'] = True
except Exception:
pass
return jsonify(result)
def _run_update_and_exit(zip_path, target_dir):
"""在后台延迟后启动 update.exe脱离当前进程然后退出当前程序"""
def _do():
import time
time.sleep(1.5) # 确保 HTTP 响应已发送
# exe_dir = target_dir
# exe_dir = "D:\\pack\\nanri\\update_exe\\main.dist"
exe_dir = os.path.join(BASE_DIR,"update")
update_exe = os.path.join(exe_dir, 'update.exe')
if not os.path.isfile(update_exe):
return
try:
creationflags = 0
if sys.platform == 'win32':
creationflags = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
subprocess.Popen(
[update_exe, '--zip', zip_path, '--target', target_dir, '--process',client_name],
cwd=exe_dir,
creationflags=creationflags,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True,
)
except Exception:
pass
os._exit(0)
t = threading.Thread(target=_do, daemon=False)
t.start()
@image_bp.route('/api/update/do', methods=['POST'])
def api_update_do():
"""执行更新:下载 zip 到 tmp启动 update.exe 后退出程序"""
data = request.get_json() or {}
file_url = (data.get('file_url') or '').strip()
if not file_url:
return jsonify({'success': False, 'error': '缺少 file_url'}), 400
parsed = urlparse(file_url)
if parsed.scheme not in ('http', 'https'):
return jsonify({'success': False, 'error': '无效的下载地址'}), 400
tmp_dir = os.path.join(BASE_DIR, 'tmp')
try:
os.makedirs(tmp_dir, exist_ok=True)
except Exception as e:
return jsonify({'success': False, 'error': f'创建 tmp 目录失败: {e}'}), 500
# 使用 URL 中的文件名或默认版本名
filename = os.path.basename(parsed.path) or 'update.zip'
zip_path = os.path.join(tmp_dir, filename)
try:
resp = requests.get(file_url, timeout=300, stream=True)
resp.raise_for_status()
with open(zip_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=65536):
if chunk:
f.write(chunk)
except requests.RequestException as e:
return jsonify({'success': False, 'error': f'下载失败: {e}'}), 502
_run_update_and_exit(zip_path, BASE_DIR)
return jsonify({'success': True, 'message': '更新已启动,程序即将退出'})
@image_bp.route('/api/download')
@login_required
def api_download():
"""代理下载图片,解决跨域 fetch 无法下载的问题"""
url = request.args.get('url', '').strip()
filename = request.args.get('filename', 'image.png')
if not url:
return jsonify({'success': False, 'error': '缺少 url 参数'}), 400
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return jsonify({'success': False, 'error': '仅支持 http/https 链接'}), 400
try:
resp = requests.get(url, timeout=30, stream=True)
resp.raise_for_status()
content_type = resp.headers.get('Content-Type', 'image/png')
encoded = quote(filename, safe='')
disposition = f"attachment; filename*=UTF-8''{encoded}"
return Response(
resp.iter_content(chunk_size=8192),
mimetype=content_type,
headers={'Content-Disposition': disposition}
)
except requests.RequestException as e:
return jsonify({'success': False, 'error': str(e)}), 502
@image_bp.route('/api/stitch/save', methods=['POST'])
@login_required
def api_stitch_save():
"""手动拼接:接收图片 URL 列表(支持 http 或 data URL拼接并上传返回长图 URL"""
try:
data = request.get_json() or {}
urls = data.get('urls')
if not urls or not isinstance(urls, list):
return jsonify({'success': False, 'error': '请提供 urls 数组'}), 400
urls = [u for u in urls if u and isinstance(u, str)]
if not urls:
return jsonify({'success': False, 'error': '没有有效的图片'}), 400
long_image_url = _stitch_and_upload_long_image(urls)
if not long_image_url:
return jsonify({'success': False, 'error': '拼接或上传失败'}), 500
return jsonify({'success': True, 'long_image_url': long_image_url})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@image_bp.route('/api/history')
@login_required
def api_history():
"""分页获取当前用户的历史图库,支持按 panel_type 栏目筛选"""
page = max(1, int(request.args.get('page', 1)))
page_size = min(50, max(10, int(request.args.get('page_size', 20))))
panel_type = (request.args.get('panel_type') or '').strip()
offset = (page - 1) * page_size
try:
conn = get_db()
with conn.cursor() as cur:
where_user = "user_id = %s"
params_where = [session['user_id']]
if panel_type:
where_user += " AND panel_type = %s"
params_where.append(panel_type)
cur.execute(
"""SELECT id, created_at, panel_type, original_urls, params, result_urls, long_image_url
FROM image_history WHERE """ + where_user + """ ORDER BY created_at DESC LIMIT %s OFFSET %s""",
params_where + [page_size, offset],
)
rows = cur.fetchall()
cur.execute("SELECT COUNT(*) as total FROM image_history WHERE " + where_user, params_where)
total = cur.fetchone()['total']
conn.close()
def _parse_json(val, default=None):
if val is None:
return default if default is not None else []
if isinstance(val, (list, dict)):
return val
try:
return json.loads(val)
except Exception:
return default if default is not None else []
items = []
for r in rows:
items.append({
'id': r['id'],
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '',
'panel_type': r['panel_type'] or '',
'original_urls': _parse_json(r['original_urls'], []),
'params': _parse_json(r['params'], {}),
'result_urls': _parse_json(r['result_urls'], []),
'long_image_url': (r.get('long_image_url') or '').strip() or None,
})
return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})