From 8a64a6cd9b7c6386635ad13b22b08610ec97fb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=AD=E5=9D=A4?= <10372463+ming-kun@user.noreply.gitee.com> Date: Fri, 20 Mar 2026 11:23:57 +0800 Subject: [PATCH] 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 --- ali_oss.py | 73 ++ app.py | 45 + app/ali_oss.py | 73 ++ app/app.py | 45 + app/app_common.py | 261 ++++++ app/config.py | 46 + app/coze.py | 92 ++ app/generate_api.py | 261 ++++++ app/html_crypto.py | 68 ++ app/main.py | 344 +++++++ app_common.py | 261 ++++++ blueprints/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 162 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 144 bytes blueprints/__pycache__/admin.cpython-311.pyc | Bin 0 -> 21814 bytes blueprints/__pycache__/admin.cpython-39.pyc | Bin 0 -> 11271 bytes blueprints/__pycache__/auth.cpython-311.pyc | Bin 0 -> 7498 bytes blueprints/__pycache__/auth.cpython-39.pyc | Bin 0 -> 3320 bytes blueprints/__pycache__/brand.cpython-311.pyc | Bin 0 -> 24298 bytes blueprints/__pycache__/brand.cpython-39.pyc | Bin 0 -> 25387 bytes blueprints/__pycache__/image.cpython-311.pyc | Bin 0 -> 20393 bytes blueprints/__pycache__/image.cpython-39.pyc | Bin 0 -> 12197 bytes blueprints/__pycache__/main.cpython-311.pyc | Bin 0 -> 4528 bytes blueprints/__pycache__/main.cpython-39.pyc | Bin 0 -> 2360 bytes blueprints/admin.py | 362 ++++++++ blueprints/auth.py | 107 +++ blueprints/brand.py | 855 ++++++++++++++++++ blueprints/image.py | 411 +++++++++ blueprints/main.py | 69 ++ brand_spider/__pycache__/main.cpython-39.pyc | Bin 0 -> 15429 bytes .../__pycache__/web_dec.cpython-39.pyc | Bin 0 -> 4726 bytes brand_spider/main.py | 692 ++++++++++++++ brand_spider/web_dec.py | 126 +++ config.py | 46 + coze.py | 92 ++ generate_api.py | 261 ++++++ html_crypto.py | 68 ++ main.py | 344 +++++++ static/bg.jpg | Bin 0 -> 164259 bytes static/品牌文档格式_模板.xlsx | Bin 0 -> 10103 bytes static/模板2-以文件夹方式上传.zip | Bin 0 -> 26336 bytes tool/.device_id | 1 + tool/__pycache__/devices.cpython-311.pyc | Bin 0 -> 12348 bytes tool/__pycache__/devices.cpython-39.pyc | Bin 0 -> 7288 bytes tool/devices.py | 249 +++++ 45 files changed, 5253 insertions(+) create mode 100644 ali_oss.py create mode 100644 app.py create mode 100644 app/ali_oss.py create mode 100644 app/app.py create mode 100644 app/app_common.py create mode 100644 app/config.py create mode 100644 app/coze.py create mode 100644 app/generate_api.py create mode 100644 app/html_crypto.py create mode 100644 app/main.py create mode 100644 app_common.py create mode 100644 blueprints/__init__.py create mode 100644 blueprints/__pycache__/__init__.cpython-311.pyc create mode 100644 blueprints/__pycache__/__init__.cpython-39.pyc create mode 100644 blueprints/__pycache__/admin.cpython-311.pyc create mode 100644 blueprints/__pycache__/admin.cpython-39.pyc create mode 100644 blueprints/__pycache__/auth.cpython-311.pyc create mode 100644 blueprints/__pycache__/auth.cpython-39.pyc create mode 100644 blueprints/__pycache__/brand.cpython-311.pyc create mode 100644 blueprints/__pycache__/brand.cpython-39.pyc create mode 100644 blueprints/__pycache__/image.cpython-311.pyc create mode 100644 blueprints/__pycache__/image.cpython-39.pyc create mode 100644 blueprints/__pycache__/main.cpython-311.pyc create mode 100644 blueprints/__pycache__/main.cpython-39.pyc create mode 100644 blueprints/admin.py create mode 100644 blueprints/auth.py create mode 100644 blueprints/brand.py create mode 100644 blueprints/image.py create mode 100644 blueprints/main.py create mode 100644 brand_spider/__pycache__/main.cpython-39.pyc create mode 100644 brand_spider/__pycache__/web_dec.cpython-39.pyc create mode 100644 brand_spider/main.py create mode 100644 brand_spider/web_dec.py create mode 100644 config.py create mode 100644 coze.py create mode 100644 generate_api.py create mode 100644 html_crypto.py create mode 100644 main.py create mode 100644 static/bg.jpg create mode 100644 static/品牌文档格式_模板.xlsx create mode 100644 static/模板2-以文件夹方式上传.zip create mode 100644 tool/.device_id create mode 100644 tool/__pycache__/devices.cpython-311.pyc create mode 100644 tool/__pycache__/devices.cpython-39.pyc create mode 100644 tool/devices.py diff --git a/ali_oss.py b/ali_oss.py new file mode 100644 index 0000000..179fadd --- /dev/null +++ b/ali_oss.py @@ -0,0 +1,73 @@ +import argparse +import base64 +import re +import time +import alibabacloud_oss_v2 as oss +import requests + +from config import region, endpoint, bucket, file_url_pre, bucket_path + + +def upload_file(file_content: bytes, key: str): + credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider() + cfg = oss.config.load_default() + cfg.credentials_provider = credentials_provider + cfg.region = region + cfg.endpoint = endpoint + cfg.retry_max_attempts = 3 + client = oss.Client(cfg) + + result = client.put_object( + oss.PutObjectRequest( + bucket=bucket, # 存储空间名称 + key=key, # 对象名称 + body=file_content # 读取文件内容 + ) + ) + # print(result) + return file_url_pre + key + + +def upload_data_url(data_url: str, prefix: str = "history", key_hint: str = "") -> str: + """ + 将 base64 data URL 上传到 OSS,返回图片链接 + data_url: data:image/png;base64,xxxx 或 data:image/jpeg;base64,xxxx + prefix: OSS key 前缀 + key_hint: 可选后缀避免重名,如 "_0", "_1" + """ + match = re.match(r'data:image/(\w+);base64,(.+)', data_url) + if not match: + raise ValueError('无效的 data URL 格式') + ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg' + file_content = base64.b64decode(match.group(2)) + ts = int(time.time() * 1000) + key = f"{bucket_path}{prefix}/{ts}{key_hint}.{ext}" + return upload_file(file_content, key) + + +def upload_data_urls(data_urls: list, prefix: str = "history") -> list: + """批量上传 base64 图片到 OSS,返回图片链接列表""" + urls = [] + ts = int(time.time() * 1000) + for i, data_url in enumerate(data_urls or []): + if not data_url or not isinstance(data_url, str): + continue + if not data_url.startswith("http"): + match = re.match(r'data:image/(\w+);base64,(.+)', data_url) + if not match: + continue + ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg' + file_content = base64.b64decode(match.group(2)) + else: + file_content = requests.get(data_url).content + ext = "png" + key = f"{bucket_path}{prefix}/{ts}_{i}.{ext}" + urls.append(upload_file(file_content, key)) + return urls + + +# 脚本入口,当文件被直接运行时调用main函数 +if __name__ == "__main__": + with open("测试图片数据/IMG_2685.JPG", "rb") as f: + file_content = f.read() + upload_file(file_content,key=bucket_path+"test.png") \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..c1f0140 --- /dev/null +++ b/app.py @@ -0,0 +1,45 @@ +""" +卖相AI - Flask 后端 +按功能拆分为蓝图:认证(auth)、主页面(main)、管理员(admin)、图片(image)、品牌(brand) +""" +import os +import secrets +from datetime import timedelta + +from flask import Flask +from flask_cors import CORS + +from app_common import init_db, BASE_DIR +from blueprints.auth import auth_bp +from blueprints.main import main_bp +from blueprints.admin import admin_bp +from blueprints.image import image_bp +from blueprints.brand import brand_bp + + +def create_app(): + app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR) + CORS(app) + app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32)) + app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) + + # 注册蓝图(不设 url_prefix,保持原有 URL 路径不变,前端无需改动) + app.register_blueprint(auth_bp) + app.register_blueprint(main_bp) + app.register_blueprint(admin_bp) + app.register_blueprint(image_bp) + app.register_blueprint(brand_bp) + + return app + + +app = create_app() + + +def run_app(host='127.0.0.1', port=5123): + init_db() + app.run(host=host, port=port, threaded=True, use_reloader=False) + + +if __name__ == '__main__': + run_app() diff --git a/app/ali_oss.py b/app/ali_oss.py new file mode 100644 index 0000000..179fadd --- /dev/null +++ b/app/ali_oss.py @@ -0,0 +1,73 @@ +import argparse +import base64 +import re +import time +import alibabacloud_oss_v2 as oss +import requests + +from config import region, endpoint, bucket, file_url_pre, bucket_path + + +def upload_file(file_content: bytes, key: str): + credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider() + cfg = oss.config.load_default() + cfg.credentials_provider = credentials_provider + cfg.region = region + cfg.endpoint = endpoint + cfg.retry_max_attempts = 3 + client = oss.Client(cfg) + + result = client.put_object( + oss.PutObjectRequest( + bucket=bucket, # 存储空间名称 + key=key, # 对象名称 + body=file_content # 读取文件内容 + ) + ) + # print(result) + return file_url_pre + key + + +def upload_data_url(data_url: str, prefix: str = "history", key_hint: str = "") -> str: + """ + 将 base64 data URL 上传到 OSS,返回图片链接 + data_url: data:image/png;base64,xxxx 或 data:image/jpeg;base64,xxxx + prefix: OSS key 前缀 + key_hint: 可选后缀避免重名,如 "_0", "_1" + """ + match = re.match(r'data:image/(\w+);base64,(.+)', data_url) + if not match: + raise ValueError('无效的 data URL 格式') + ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg' + file_content = base64.b64decode(match.group(2)) + ts = int(time.time() * 1000) + key = f"{bucket_path}{prefix}/{ts}{key_hint}.{ext}" + return upload_file(file_content, key) + + +def upload_data_urls(data_urls: list, prefix: str = "history") -> list: + """批量上传 base64 图片到 OSS,返回图片链接列表""" + urls = [] + ts = int(time.time() * 1000) + for i, data_url in enumerate(data_urls or []): + if not data_url or not isinstance(data_url, str): + continue + if not data_url.startswith("http"): + match = re.match(r'data:image/(\w+);base64,(.+)', data_url) + if not match: + continue + ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg' + file_content = base64.b64decode(match.group(2)) + else: + file_content = requests.get(data_url).content + ext = "png" + key = f"{bucket_path}{prefix}/{ts}_{i}.{ext}" + urls.append(upload_file(file_content, key)) + return urls + + +# 脚本入口,当文件被直接运行时调用main函数 +if __name__ == "__main__": + with open("测试图片数据/IMG_2685.JPG", "rb") as f: + file_content = f.read() + upload_file(file_content,key=bucket_path+"test.png") \ No newline at end of file diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..c1f0140 --- /dev/null +++ b/app/app.py @@ -0,0 +1,45 @@ +""" +卖相AI - Flask 后端 +按功能拆分为蓝图:认证(auth)、主页面(main)、管理员(admin)、图片(image)、品牌(brand) +""" +import os +import secrets +from datetime import timedelta + +from flask import Flask +from flask_cors import CORS + +from app_common import init_db, BASE_DIR +from blueprints.auth import auth_bp +from blueprints.main import main_bp +from blueprints.admin import admin_bp +from blueprints.image import image_bp +from blueprints.brand import brand_bp + + +def create_app(): + app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR) + CORS(app) + app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32)) + app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) + + # 注册蓝图(不设 url_prefix,保持原有 URL 路径不变,前端无需改动) + app.register_blueprint(auth_bp) + app.register_blueprint(main_bp) + app.register_blueprint(admin_bp) + app.register_blueprint(image_bp) + app.register_blueprint(brand_bp) + + return app + + +app = create_app() + + +def run_app(host='127.0.0.1', port=5123): + init_db() + app.run(host=host, port=port, threaded=True, use_reloader=False) + + +if __name__ == '__main__': + run_app() diff --git a/app/app_common.py b/app/app_common.py new file mode 100644 index 0000000..67169e0 --- /dev/null +++ b/app/app_common.py @@ -0,0 +1,261 @@ +""" +公共模块:数据库连接、初始化、会话校验、装饰器、模板渲染 +供各蓝图复用 +""" +import os +import secrets +from datetime import timedelta +from functools import wraps + +import pymysql +from flask import request, redirect, url_for, session, jsonify, render_template, render_template_string + +from config import mysql_host, mysql_user, mysql_password, mysql_database + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +STATIC_DIR = os.path.join(BASE_DIR, 'static') + + +def get_db(): + return pymysql.connect( + host=mysql_host, + user=mysql_user, + password=mysql_password, + database=mysql_database, + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + + +def _render_html(template_name: str, **context): + """读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。""" + path = os.path.join(BASE_DIR, "web_source", template_name) + if not os.path.isfile(path): + return render_template(template_name, **context) + with open(path, "rb") as f: + raw = f.read() + try: + from html_crypto import decrypt + content = decrypt(raw).decode("utf-8") + except Exception: + content = raw.decode("utf-8", errors="replace") + return render_template_string(content, **context) + + +def _is_session_user_valid(): + """校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False""" + uid = session.get('user_id') + if not uid: + return False + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT id FROM users WHERE id = %s", (uid,)) + row = cur.fetchone() + conn.close() + if not row: + session.clear() + return False + return True + except Exception: + session.clear() + return False + + +def _get_current_admin_role(): + """获取当前登录用户的管理角色:super_admin / admin / None(非管理员)""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s", + (session['user_id'],) + ) + row = cur.fetchone() + conn.close() + if not row or not row.get('is_admin'): + return None, None + return row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin'), row + except Exception: + return None, None + + +def login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get('user_id') or not _is_session_user_valid(): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': '未登录'}), 401 + return redirect(url_for('auth.login')) + return f(*args, **kwargs) + return decorated + + +def admin_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get('user_id'): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': '未登录'}), 401 + return redirect(url_for('auth.login')) + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],)) + row = cur.fetchone() + conn.close() + if not row or not row.get('is_admin'): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': '需要管理员权限'}), 403 + return redirect(url_for('main.home')) + except Exception as e: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': str(e)}), 500 + return redirect(url_for('main.home')) + return f(*args, **kwargs) + return decorated + + +def init_db(): + """初始化数据库表,若不存在则创建""" + from werkzeug.security import generate_password_hash + conn = pymysql.connect( + host=mysql_host, + user=mysql_user, + password=mysql_password, + charset='utf8mb4' + ) + try: + with conn.cursor() as cur: + cur.execute(f"CREATE DATABASE IF NOT EXISTS `{mysql_database}` DEFAULT CHARSET utf8mb4") + cur.execute(f"USE `{mysql_database}`") + cur.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(64) NOT NULL UNIQUE, + password_hash VARCHAR(256) NOT NULL, + is_admin TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + cur.execute(""" + CREATE TABLE IF NOT EXISTS image_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + panel_type VARCHAR(64) DEFAULT '', + original_urls JSON, + params JSON, + result_urls JSON, + long_image_url VARCHAR(1024) DEFAULT NULL, + INDEX idx_user_created (user_id, created_at DESC) + ) + """) + try: + cur.execute("ALTER TABLE image_history ADD COLUMN long_image_url VARCHAR(1024) DEFAULT NULL") + except Exception: + pass + try: + cur.execute("ALTER TABLE users ADD COLUMN machine VARCHAR(64) DEFAULT NULL") + except Exception: + pass + try: + cur.execute("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'normal'") + except Exception: + pass + try: + cur.execute("ALTER TABLE users ADD COLUMN created_by_id INT NULL") + except Exception: + pass + cur.execute(""" + CREATE TABLE IF NOT EXISTS brand_crawl_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + file_paths JSON NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行', + result_paths JSON NULL, + error_message TEXT NULL, + progress_current INT DEFAULT 0, + progress_total INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_status (user_id, status) + ) + """) + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_current INT DEFAULT 0") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_total INT DEFAULT 0") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN `desc` VARCHAR(500) NULL COMMENT '上传文件名描述'") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN strategy VARCHAR(20) DEFAULT 'Terms' COMMENT '品牌匹配方式: Terms/Simple'") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行'") + except Exception: + pass + cur.execute(""" + CREATE TABLE IF NOT EXISTS columns ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(128) NOT NULL COMMENT '栏目名', + column_key VARCHAR(64) NOT NULL COMMENT '栏目标识', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_column_key (column_key) + ) + """) + cur.execute(""" + CREATE TABLE IF NOT EXISTS user_column_permission ( + user_id INT NOT NULL, + column_id INT NOT NULL, + PRIMARY KEY (user_id, column_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE + ) + """) + try: + cur.execute("UPDATE users SET role = 'normal' WHERE (role IS NULL OR role = '') AND (is_admin = 0 OR is_admin IS NULL)") + cur.execute("SELECT MIN(id) AS mid FROM users WHERE is_admin = 1") + row = cur.fetchone() + if row and row.get('mid'): + mid = row['mid'] + cur.execute("UPDATE users SET role = 'super_admin' WHERE id = %s", (mid,)) + cur.execute("UPDATE users SET role = 'admin' WHERE is_admin = 1 AND id != %s", (mid,)) + cur.execute("UPDATE users SET created_by_id = %s WHERE role = 'admin' AND (created_by_id IS NULL)", (mid,)) + except Exception: + pass + conn.commit() + finally: + conn.close() + _create_initial_admin() + + +def _create_initial_admin(): + """若没有任何管理员,则创建默认超级管理员(首次启动时,仅一个)""" + from werkzeug.security import generate_password_hash + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT id FROM users WHERE role = 'super_admin' LIMIT 1") + if cur.fetchone(): + conn.close() + return + admin_user = os.environ.get('ADMIN_USER', 'admin') + admin_pwd = os.environ.get('ADMIN_PASSWORD', 'admin123') + pwd_hash = generate_password_hash(admin_pwd, method='pbkdf2:sha256') + cur.execute( + "INSERT INTO users (username, password_hash, is_admin, role) VALUES (%s, %s, 1, 'super_admin')", + (admin_user, pwd_hash) + ) + conn.commit() + conn.close() + except Exception: + pass diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..56b7ef8 --- /dev/null +++ b/app/config.py @@ -0,0 +1,46 @@ +base_url = "https://api.coze.cn/v1" +coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5" +workflow_id = "7608812635877900322" +STITCH_WORKFLOW_ID = "7608813873483300907" + +import os +from dotenv import load_dotenv +load_dotenv() +_base_url = os.getenv("base_url","http://159.75.121.33:15124") + +# MySQL 配置 +mysql_host = os.getenv("mysql_host") +mysql_user = os.getenv("mysql_user") +mysql_password = os.getenv("mysql_password","WTFrb5y6hNLz6hNy") +mysql_database = os.getenv("mysql_database","aiimage") +proxy_url = os.getenv("proxy_url") +proxy_mode = int(os.getenv("proxy_mode",1)) + +client_name=os.getenv("client_name") + ".exe" + +cache_path = "./user_data" + +region = "cn-hangzhou" +endpoint = "oss-cn-hangzhou.aliyuncs.com" +bucket = "nanri-ai-images" +accessKeyId = "LTAI5tNpyvzMNz9f2dHarsm8" +accessKeySecret = "bQSZnFH455i8tzyOgeahJmUzwmhynz" +bucket_path = "nanri-image/" + +file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/" + + + +os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId +os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret +os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6" + + +debug = True +version = "1.0.3" +APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" } +os.environ['APP_VERSION'] = version +os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL + + + diff --git a/app/coze.py b/app/coze.py new file mode 100644 index 0000000..ea8e95a --- /dev/null +++ b/app/coze.py @@ -0,0 +1,92 @@ +import requests +import time + +from config import base_url, coze_token + + + + + +def upload_file(file_path,_coze_token =coze_token): + url = f"{base_url}/files/upload" + headers = { + "Authorization": f"Bearer {_coze_token}" + } + print(file_path) + with open(file_path, 'rb') as f: + files = { + 'file': f + } + # with open(f"{time.time()".replace(".","_")+".png","wb") as file: + # file.write(f.read()) + # allow_redirects=True 模拟 curl 的 --location,默认即为 True + response = requests.post(url, headers=headers, files=files, allow_redirects=True) + data = response.json() + print("上传图片->>",data) + return data + +def workflow_run(workflow_id, parameters,_coze_token = coze_token,is_async=True): + url = f"{base_url}/workflow/run" + headers = { + "Authorization": f"Bearer {_coze_token}", + "Content-Type": "application/json" + } + + # 请求体(JSON 格式) + payload = { + "parameters": parameters, + # "parameters": { + # "name": "包包", + # "ratio": "3:4", + # "menu": 3, + # "resolution": "2K", + # "count": 1, + # "desc": "1、一个中国美女手上挎着这个包,2、站在商场内", + # "language": "中文", + # "style": "极简高级" + # }, + "workflow_id": workflow_id, + "is_async" : is_async, + } + # 发送 POST 请求(使用 json 参数自动序列化并设置 Content-Type) + response = requests.post(url, headers=headers, json=payload) + data = response.json() + + print("图片生成参数->>",payload) + print("图片生成->>",data) + return data + + +def query_result(workflow_id, execute_id,_coze_token=coze_token): + url = f"{base_url}/workflows/{workflow_id}/run_histories/{execute_id}" + headers = { + "Authorization": f"Bearer {_coze_token}", + "Content-Type": "application/json" + } + response = requests.get(url, headers=headers) + data = response.json() + print(f"【{execute_id}】结果查询->>",data) + return data + +if __name__ == '__main__': + from config import workflow_id + + parameters = { + "images": [ + { + "file_id": "7612667079497613350", + + },{ + "file_id": "7612667042990768174", + },{ + "file_id": "7612667111332577332", + } + ] + } + # resp = workflow_run("7607680760402100258",parameters,_coze_token="sat_ZVNLR9Om54A3iMlJWAasHF9kbtDZnR3BjwjAxRwe8x7igEW446y5ROyVlW1UlpVX") + # print(resp) + + # resp = query_result("7607680760402100258","7612668958797447187",_coze_token="sat_ZVNLR9Om54A3iMlJWAasHF9kbtDZnR3BjwjAxRwe8x7igEW446y5ROyVlW1UlpVX") + # print(resp) + resp = upload_file("D:\私单交付\maixiang_AI\测试图片数据\IMG_2686.JPG",_coze_token="sat_ZVNLR9Om54A3iMlJWAasHF9kbtDZnR3BjwjAxRwe8x7igEW446y5ROyVlW1UlpVX") + print(resp) \ No newline at end of file diff --git a/app/generate_api.py b/app/generate_api.py new file mode 100644 index 0000000..ce92eff --- /dev/null +++ b/app/generate_api.py @@ -0,0 +1,261 @@ +""" +生成图片 API - 供 pywebview 前端调用 +流程:上传图片 -> workflow_run -> 轮询 query_result -> 解析返回图片 URL +""" + +import base64 +import json +import os +import tempfile +import time +import re +import traceback + +import requests + +from coze import upload_file, workflow_run, query_result +from config import workflow_id +from ali_oss import upload_data_urls as oss_upload_data_urls + + +def _data_url_to_temp_file(data_url: str, allow_video: bool = False) -> str: + """将 base64 data URL 转为临时文件路径""" + # data:image/png;base64,xxxx 或 data:video/mp4;base64,xxxx + if allow_video: + match = re.match(r'data:(?:image|video)/(\w+);base64,(.+)', data_url) + else: + match = re.match(r'data:image/(\w+);base64,(.+)', data_url) + if not match: + raise ValueError('无效的 data URL 格式') + mime = match.group(1).lower() + ext_map = {'png': 'png', 'webp': 'png', 'jpeg': 'jpg', 'jpg': 'jpg', 'mp4': 'mp4', 'webm': 'webm'} + ext = ext_map.get(mime, 'jpg') + data = base64.b64decode(match.group(2)) + fd, path = tempfile.mkstemp(suffix=f'.{ext}') + try: + os.write(fd, data) + finally: + os.close(fd) + return path + + +def _upload_video(video_data_url: str) -> str: + """上传视频,返回 file_id""" + if not video_data_url or not isinstance(video_data_url, str): + raise ValueError('无效的视频数据') + path = _data_url_to_temp_file(video_data_url, allow_video=True) + try: + resp = upload_file(path) + if resp.get('code') == 0 and resp.get('data', {}).get('id'): + return resp['data']['id'] + raise RuntimeError(f'视频上传失败: {resp.get("msg", resp)}') + finally: + try: + os.unlink(path) + except Exception: + pass + + +def _upload_images(image_data_urls: list) -> list: + """上传多张图片,返回 file_id 列表""" + file_ids = [] + temp_paths = [] + try: + for data_url in (image_data_urls or []): + if not data_url or not isinstance(data_url, str): + continue + if data_url.startswith("http"): + fd, path = tempfile.mkstemp(suffix=f'.png') + data_content = requests.get(data_url).content + os.write(fd, data_content) + else: + path = _data_url_to_temp_file(data_url) + temp_paths.append(path) + resp = upload_file(path) + if resp.get('code') == 0 and resp.get('data', {}).get('id'): + file_ids.append(resp['data']['id']) + else: + raise RuntimeError(f'上传失败: {resp.get("msg", resp)}') + return file_ids + finally: + for p in temp_paths: + try: + os.unlink(p) + except Exception: + pass + + +def _parse_output(output_str: str) -> list: + """解析 query_result 中的 output,提取图片 URL 列表""" + try: + outer = json.loads(output_str) + inner_str = outer.get('Output', '{}') + inner = json.loads(inner_str) + data_str = inner.get('data', '[]') + data = json.loads(data_str) + urls = data.get("images") + long_image_url = data.get("merged_image_url") + return urls if isinstance(urls, list) else [],long_image_url + except Exception: + return [],None + + +def _parse_output_text(output_str: str) -> list: + """解析 query_result 中的 output,提取图片 URL 列表""" + try: + outer = json.loads(output_str) + inner_str = outer.get('Output', '{}') + inner = json.loads(inner_str) + data_str = inner.get('data', '') + if isinstance(data_str,str): + data = json.loads(data_str).get("reverse_prompt") + return [data] + pattern = r'【?图像 \d+】.*?(?=【?图像 \d+】|$)' + segments = re.findall(pattern, data_str, re.DOTALL) + return segments + except Exception: + return "" + + +def _poll_until_done(wf_id: str, execute_id: str, interval: float =10, timeout: int = 600*2, is_text: bool = False) -> list: + """轮询直到成功或超时,返回图片 URL 列表""" + start = time.time() + while time.time() - start < timeout: + resp = query_result(wf_id, execute_id) + if resp.get('code') != 0: + raise RuntimeError(f'查询失败: {resp.get("msg", resp)}') + items = resp.get('data') or [] + if not items: + time.sleep(interval) + continue + item = items[0] + status = item.get('execute_status', '') + if status == 'Success': + output = item.get('output', '{}') + if is_text: + return _parse_output_text(output) + return _parse_output(output) + if status and status not in ('Running', 'Pending', ''): + raise RuntimeError(f'执行失败: {status}') + time.sleep(interval) + raise RuntimeError('生成超时') + + +def _res_to_2k(res: str) -> str: + """统一分辨率为 2K/4K""" + r = (res or '2k').strip().upper() + return '4K' if r == '4K' else '2K' + + +def generate(params: dict) -> dict: + """ + 生成图片 + params: { + menu: int, # 1=图片反推 2=图片编辑 3=随机海报 4=克隆海报 5=服饰穿搭 + prompt: str, # 用户自定义指令 (menu=1 必填) + ref_images: list, # base64 data URLs - 参考图 + video: str, # base64 data URL - 视频 (menu=1 可选,仅支持1个) + model_images: list, # base64 data URLs - 多模特图 (menu!=1,最多5张) + name: str, desc: str, ratio: str, resolution: str, count: int, + language: str, style: str, batch_prompt: list, brand_name: str, + Ingredients: str, activity: str, mode: str, texts: list, + proc_images: list, layout_image: str, + } + 返回: { success: bool, urls: list, prompts: list, error: str } + """ + original_urls = [] + api_key = (params.get('api_key') or '').strip() + try: + menu = int(params.get('menu', 2)) + ref_data = params.get('ref_images') or [] + proc_data = params.get('proc_images') or [] + layout_data = params.get('layout_image') + video_data = params.get('video') + model_data = params.get('model_images') or [] + + # menu 1: 反推词,简化参数 + if menu == 1: + base_params = {'menu': 1, 'prompt': str(params.get('prompt', '')).strip() or ''} + + ref_ids = _upload_images(ref_data) if ref_data else [] + if ref_ids: + base_params['ref_images'] = [{'file_id': fid} for fid in ref_ids] + if video_data: + try: + vid = _upload_video(video_data) + base_params['video'] = {'file_id': vid} + except Exception as ve: + return {'success': False, 'urls': [], 'prompts': [], 'error': f'视频上传失败: {ve}'} + base_params = {k: v for k, v in base_params.items() if v is not None and v != ''} + base_params["api_key"] = api_key + resp = workflow_run(workflow_id, base_params) + if resp.get('code') != 0: + return {'success': False, 'urls': [], 'prompts': [], 'error': resp.get('msg', str(resp))} + execute_id = resp.get('execute_id') + if not execute_id: + return {'success': False, 'urls': [], 'prompts': [], 'error': '未返回 execute_id'} + prompts = _poll_until_done(workflow_id, str(execute_id), is_text=True) + # menu 1 返回提示词列表,统一转为字符串 + # prompts = [str(x) for x in result_list] if result_list else [] + # prompts = [prompts] + return {'success': True, 'urls': [], 'prompts': prompts, 'error': ''} + + # menu != 1: 原有逻辑 + all_ref = list(ref_data) + if layout_data: + all_ref = [layout_data] + list(ref_data) + all_originals = list(all_ref) + list(proc_data) + list(model_data) + + if all_originals: + try: + original_urls = oss_upload_data_urls(all_originals, prefix="originals") + except Exception: + traceback.print_exc() + pass + + ref_ids = _upload_images(all_ref) if all_ref else [] + proc_ids = _upload_images(proc_data) if proc_data else [] + model_ids = _upload_images(model_data) if model_data else [] + + res = _res_to_2k(params.get('resolution', '2K')) + + base_params = { + 'name': str(params.get('name', '')).strip(), + 'ratio': str(params.get('ratio', '')).strip(), + 'menu': menu, + 'resolution': res, + 'count': int(params.get('count', 1)), + 'desc': str(params.get('desc', '')).strip(), + 'language': str(params.get('language', '中文')).strip() or '中文', + 'style': str(params.get('style', '')).strip(), + 'prompt': str(params.get('prompt', '')).strip(), + 'batch_prompt': params.get('batch_prompt') or [], + 'brand_name': str(params.get('brand_name', '')).strip(), + 'Ingredients': str(params.get('Ingredients', '')).strip(), + 'activity': str(params.get('activity', '')).strip(), + 'mode': str(params.get('mode', '1')).strip() or '1', + 'texts': params.get('text') or [], + "api_key" : api_key + } + + if ref_ids: + base_params['ref_images'] = [{'file_id': fid} for fid in ref_ids] + if proc_ids: + base_params['proc_images'] = [{'file_id': fid} for fid in proc_ids] + if model_ids: + base_params['model_images'] = [{'file_id': fid} for fid in model_ids] + base_params = {k: v for k, v in base_params.items() if v is not None and v != ''} + + resp = workflow_run(workflow_id, base_params) + if resp.get('code') != 0: + return {'success': False, 'urls': [], 'error': resp.get('msg', str(resp))} + execute_id = resp.get('execute_id') + if not execute_id: + return {'success': False, 'urls': [], 'error': '未返回 execute_id'} + + urls,long_image_url = _poll_until_done(workflow_id, str(execute_id)) + return {'success': True, 'urls': urls, 'original_urls': original_urls, 'error': '',"long_image_url":long_image_url} + except Exception as e: + return {'success': False, 'urls': [], 'prompts': [], 'original_urls': [], 'error': str(e),"long_image_url":""} + + diff --git a/app/html_crypto.py b/app/html_crypto.py new file mode 100644 index 0000000..cb089d9 --- /dev/null +++ b/app/html_crypto.py @@ -0,0 +1,68 @@ +""" +HTML 模板加密/解密模块 +使用 AES (Fernet) 对 HTML 资源进行加密存储,运行时解密后渲染 +""" + +import base64 +import os + +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +def _get_fernet_key() -> bytes: + """从环境变量 HTML_ENCRYPT_KEY 派生 Fernet 密钥,若未设置则使用默认开发密钥""" + raw = os.environ.get( + "HTML_ENCRYPT_KEY", + "maixiang_html_encrypt_default_key_change_in_production", + ) + # 使用 PBKDF2 派生 32 字节密钥,再转为 Fernet 所需的 base64url + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=b"maixiang_html_salt", + iterations=100000, + ) + key_bytes = kdf.derive(raw.encode("utf-8")) + return base64.urlsafe_b64encode(key_bytes) + + +def encrypt(plain_data: bytes) -> bytes: + """加密原始数据,返回密文(bytes)""" + key = _get_fernet_key() + f = Fernet(key) + return f.encrypt(plain_data) + + +def decrypt(encrypted_data: bytes) -> bytes: + """解密数据,返回明文(bytes)""" + key = _get_fernet_key() + f = Fernet(key) + return f.decrypt(encrypted_data) + + +def encrypt_file(path: str) -> None: + """就地加密文件:读取 UTF-8 内容,加密后写回同一路径""" + with open(path, "rb") as f: + plain = f.read() + encrypted = encrypt(plain) + with open(path, "wb") as f: + f.write(encrypted) + + +def decrypt_file(path: str) -> None: + """就地解密文件:读取密文,解密后以 UTF-8 写回同一路径""" + with open(path, "rb") as f: + encrypted = f.read() + plain = decrypt(encrypted) + with open(path, "wb") as f: + f.write(plain) + + +def is_encrypted(data: bytes) -> bool: + """简单启发式:Fernet 密文以 b'gAAAAA' 开头(base64 编码后)""" + try: + return data[:7] == b"gAAAAAB" + except Exception: + return False diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..eb64fdf --- /dev/null +++ b/app/main.py @@ -0,0 +1,344 @@ +""" +基于 pywebview 实现的跨平台 UI,需先登录方可使用 +""" +from config import cache_path,debug,version +import datetime +import json +import sys +import os +os.makedirs(cache_path,exist_ok=True) +if not debug: + today = datetime.datetime.now().strftime("%Y_%m_%d") + sys.stdout = open(os.path.join(cache_path,f'{today}.log'), 'a',buffering=1) + sys.stderr = sys.stdout + +import webview +import threading +import time + + +with open("version.txt","w",encoding="utf-8") as file: + file.write(json.dumps({"version":version})) + + +# 获取当前脚本所在目录 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +APP_URL = "http://127.0.0.1:5123" +PORT = 5123 + + +def generate_images(params): + """供前端调用的生成图片接口""" + from generate_api import generate + return generate(params) + + +class WindowAPI: + """暴露给 JavaScript 的窗口控制 API""" + + def __init__(self, window): + self._window = window + self._is_maximized = False + window.events.maximized += lambda _: setattr(self, '_is_maximized', True) + window.events.restored += lambda _: setattr(self, '_is_maximized', False) + + def close(self): + sys.exit(1) + self._window.destroy() + + def minimize(self): + self._window.minimize() + + def maximize(self): + self._window.maximize() + + def toggle_maximize(self): + if self._is_maximized: + self._window.restore() + else: + self._window.maximize() + + def save_image(self, url_or_data, filename='image.png'): + """弹窗选择保存位置并下载图片。url_or_data 可为 http(s) 链接或 data: base64""" + import base64 + import re + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=filename, + file_types=('图片文件 (*.png;*.jpg;*.jpeg)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + if url_or_data.startswith('data:'): + match = re.match(r'data:image/\w+;base64,(.+)', url_or_data) + if not match: + print(url_or_data) + print('无效的 data URL') + return {'success': False, 'error': '无效的 data URL'} + data = base64.b64decode(match.group(1)) + else: + import requests + resp = requests.get(url_or_data, timeout=30) + resp.raise_for_status() + data = resp.content + with open(path, 'wb') as f: + f.write(data) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + def save_image_to_folder(self, url_or_data, dir_path, filename='image.png'): + """将图片保存到指定文件夹。url_or_data 可为 http(s) 链接或 data: base64""" + import base64 + import re + path = os.path.join(dir_path, filename) + try: + if url_or_data.startswith('data:'): + match = re.match(r'data:image/\w+;base64,(.+)', url_or_data) + if not match: + return {'success': False, 'error': '无效的 data URL'} + data = base64.b64decode(match.group(1)) + else: + import requests + resp = requests.get(url_or_data, timeout=30) + resp.raise_for_status() + data = resp.content + os.makedirs(dir_path, exist_ok=True) + with open(path, 'wb') as f: + f.write(data) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + def select_folder(self): + # 此方法会在 JavaScript 中被调用,返回选择的文件夹路径 + result = webview.windows[0].create_file_dialog( + webview.FOLDER_DIALOG, # 指定为文件夹对话框 + allow_multiple=False, # 是否允许多选 + directory='' # 初始目录 + ) + # create_file_dialog 返回的是列表(多选时)或 None + return result[0] if result else '' + + def select_brand_xlsx_files(self): + """品牌爬虫:选择 .xlsx 文件,允许多选,返回路径列表""" + result = webview.windows[0].create_file_dialog( + webview.OPEN_DIALOG, + allow_multiple=True, + file_types=('Excel 文件 (*.xlsx)', '所有文件 (*.*)') + ) + print("选择的excel 文件",result) + if not result: + return [] + return result if isinstance(result, list) or isinstance(result, tuple) else [result] + + def select_brand_folder(self): + """品牌爬虫:选择文件夹,返回文件夹路径(前端再请求后端展开该目录下所有 xlsx)""" + result = webview.windows[0].create_file_dialog( + webview.FOLDER_DIALOG, + allow_multiple=False, + directory='' + ) + return result[0] if result else '' + + def save_file_from_url(self, url, default_filename='download.zip'): + """弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。""" + if not url or not url.strip(): + return {'success': False, 'error': '下载地址为空'} + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=default_filename, + file_types=('ZIP 压缩包 (*.zip)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + import requests + resp = requests.get(url.strip(), timeout=120, stream=True) + resp.raise_for_status() + with open(path, 'wb') as f: + for chunk in resp.iter_content(chunk_size=65536): + if chunk: + f.write(chunk) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + def save_template_xlsx(self): + """弹窗选择保存位置,将品牌文档格式模板 xlsx 保存到用户选择的位置。""" + import shutil + template_name = '品牌文档格式_模板.xlsx' + template_path = os.path.join(BASE_DIR, 'static', template_name) + if not os.path.isfile(template_path): + return {'success': False, 'error': '模板文件不存在'} + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=template_name, + file_types=('Excel 文件 (*.xlsx)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + shutil.copy2(template_path, path) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + + def save_template_zip(self): + """弹窗选择保存位置,将品牌文档格式模板 xlsx 保存到用户选择的位置。""" + import shutil + template_name = '模板2-以文件夹方式上传.zip' + template_path = os.path.join(BASE_DIR, 'static', template_name) + if not os.path.isfile(template_path): + return {'success': False, 'error': '模板文件不存在'} + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=template_name, + file_types=('ZIP 文件 (*.zip)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + shutil.copy2(template_path, path) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + +def _select_folder_win32(): + """Windows: 用 ctypes 调用系统文件夹选择对话框,打包成 exe 后也能正常弹窗(不依赖 tkinter)""" + try: + import ctypes + from ctypes import wintypes + BIF_RETURNONLYFSDIRS = 0x00000001 + BIF_NEWDIALOGSTYLE = 0x00000040 + MAX_PATH = 260 + shell32 = ctypes.windll.shell32 # type: ignore[attr-defined] + # BROWSEINFOW + class BROWSEINFOW(ctypes.Structure): + _fields_ = [ + ("hwndOwner", wintypes.HWND), + ("pidlRoot", ctypes.c_void_p), + ("pszDisplayName", ctypes.c_wchar_p), + ("lpszTitle", ctypes.c_wchar_p), + ("ulFlags", wintypes.UINT), + ("lpfn", ctypes.c_void_p), + ("lParam", wintypes.LPARAM), + ("iImage", ctypes.c_int), + ] + display_name_buf = ctypes.create_unicode_buffer(MAX_PATH) + bi = BROWSEINFOW() + bi.hwndOwner = 0 + bi.pidlRoot = 0 + bi.pszDisplayName = ctypes.cast(display_name_buf, ctypes.c_wchar_p) + bi.lpszTitle = "选择保存图片的文件夹" + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE + bi.lpfn = 0 + bi.lParam = 0 + bi.iImage = 0 + pidl = shell32.SHBrowseForFolderW(ctypes.byref(bi)) + if not pidl: + return '' + path_buf = ctypes.create_unicode_buffer(MAX_PATH) + if not shell32.SHGetPathFromIDListW(pidl, path_buf): + return '' + ole32 = ctypes.windll.ole32 # type: ignore[attr-defined] + ole32.CoTaskMemFree(pidl) + return path_buf.value or '' + except Exception as e: + print("Windows 文件夹选择失败:", e) + return '' + + +# def _select_folder_tk(): +# """使用 tkinter 选择文件夹(在非 Windows 或作为后备)""" +# try: +# +# root = tkinter.Tk() +# root.withdraw() +# root.attributes('-topmost', True) +# path = filedialog.askdirectory(title='选择保存图片的文件夹') +# try: +# root.destroy() +# except Exception: +# pass +# return path if path else '' +# except Exception as e: +# print("tkinter 选择文件夹失败:", e) +# return '' + + +def _select_folder_tk(): + import tkinter as tk + from tkinter import filedialog + + root = tk.Tk() + root.withdraw() + root.attributes('-topmost', True) + + # 使用 after 确保 askdirectory 在主循环启动后执行 + def ask(): + nonlocal path + path = filedialog.askdirectory(title='选择保存图片的文件夹') + root.quit() # 关闭主循环 + + path = '' + root.after(100, ask) + root.mainloop() # 启动主循环,直到 root.quit() 被调用 + try: + root.destroy() + except: + pass + return path + + +def select_folder(): + """弹窗选择文件夹,返回路径(空字符串表示取消)。Windows 打包 exe 时优先用系统 API 避免 tkinter 不弹窗。""" + # if sys.platform == 'win32': + # path = _select_folder_win32() + # if path: + # return path + return _select_folder_tk() + + +def start_flask(): + from app import run_app + run_app(host='127.0.0.1', port=PORT) + + +def main(): + if not os.path.exists(cache_path): + os.makedirs(cache_path,exist_ok=True) + t = threading.Thread(target=start_flask, daemon=True) + t.start() + # 等待 Flask 启动 + time.sleep(2) + + window = webview.create_window( + title="南日AI", + url=APP_URL, + width=1400, + height=900, + resizable=True, + min_size=(1200, 700), + background_color="#1a1a1a", + frameless=False, + easy_drag=True + ) + api = WindowAPI(window) + window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip) + webview.start( + debug=True, + storage_path=cache_path, + private_mode=False + ) + + +if __name__ == "__main__": + main() diff --git a/app_common.py b/app_common.py new file mode 100644 index 0000000..67169e0 --- /dev/null +++ b/app_common.py @@ -0,0 +1,261 @@ +""" +公共模块:数据库连接、初始化、会话校验、装饰器、模板渲染 +供各蓝图复用 +""" +import os +import secrets +from datetime import timedelta +from functools import wraps + +import pymysql +from flask import request, redirect, url_for, session, jsonify, render_template, render_template_string + +from config import mysql_host, mysql_user, mysql_password, mysql_database + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +STATIC_DIR = os.path.join(BASE_DIR, 'static') + + +def get_db(): + return pymysql.connect( + host=mysql_host, + user=mysql_user, + password=mysql_password, + database=mysql_database, + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor + ) + + +def _render_html(template_name: str, **context): + """读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。""" + path = os.path.join(BASE_DIR, "web_source", template_name) + if not os.path.isfile(path): + return render_template(template_name, **context) + with open(path, "rb") as f: + raw = f.read() + try: + from html_crypto import decrypt + content = decrypt(raw).decode("utf-8") + except Exception: + content = raw.decode("utf-8", errors="replace") + return render_template_string(content, **context) + + +def _is_session_user_valid(): + """校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False""" + uid = session.get('user_id') + if not uid: + return False + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT id FROM users WHERE id = %s", (uid,)) + row = cur.fetchone() + conn.close() + if not row: + session.clear() + return False + return True + except Exception: + session.clear() + return False + + +def _get_current_admin_role(): + """获取当前登录用户的管理角色:super_admin / admin / None(非管理员)""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s", + (session['user_id'],) + ) + row = cur.fetchone() + conn.close() + if not row or not row.get('is_admin'): + return None, None + return row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin'), row + except Exception: + return None, None + + +def login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get('user_id') or not _is_session_user_valid(): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': '未登录'}), 401 + return redirect(url_for('auth.login')) + return f(*args, **kwargs) + return decorated + + +def admin_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get('user_id'): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': '未登录'}), 401 + return redirect(url_for('auth.login')) + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],)) + row = cur.fetchone() + conn.close() + if not row or not row.get('is_admin'): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': '需要管理员权限'}), 403 + return redirect(url_for('main.home')) + except Exception as e: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'error': str(e)}), 500 + return redirect(url_for('main.home')) + return f(*args, **kwargs) + return decorated + + +def init_db(): + """初始化数据库表,若不存在则创建""" + from werkzeug.security import generate_password_hash + conn = pymysql.connect( + host=mysql_host, + user=mysql_user, + password=mysql_password, + charset='utf8mb4' + ) + try: + with conn.cursor() as cur: + cur.execute(f"CREATE DATABASE IF NOT EXISTS `{mysql_database}` DEFAULT CHARSET utf8mb4") + cur.execute(f"USE `{mysql_database}`") + cur.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(64) NOT NULL UNIQUE, + password_hash VARCHAR(256) NOT NULL, + is_admin TINYINT(1) DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + cur.execute(""" + CREATE TABLE IF NOT EXISTS image_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + panel_type VARCHAR(64) DEFAULT '', + original_urls JSON, + params JSON, + result_urls JSON, + long_image_url VARCHAR(1024) DEFAULT NULL, + INDEX idx_user_created (user_id, created_at DESC) + ) + """) + try: + cur.execute("ALTER TABLE image_history ADD COLUMN long_image_url VARCHAR(1024) DEFAULT NULL") + except Exception: + pass + try: + cur.execute("ALTER TABLE users ADD COLUMN machine VARCHAR(64) DEFAULT NULL") + except Exception: + pass + try: + cur.execute("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'normal'") + except Exception: + pass + try: + cur.execute("ALTER TABLE users ADD COLUMN created_by_id INT NULL") + except Exception: + pass + cur.execute(""" + CREATE TABLE IF NOT EXISTS brand_crawl_tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + file_paths JSON NOT NULL, + status VARCHAR(20) DEFAULT 'pending', + task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行', + result_paths JSON NULL, + error_message TEXT NULL, + progress_current INT DEFAULT 0, + progress_total INT DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_status (user_id, status) + ) + """) + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_current INT DEFAULT 0") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_total INT DEFAULT 0") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN `desc` VARCHAR(500) NULL COMMENT '上传文件名描述'") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN strategy VARCHAR(20) DEFAULT 'Terms' COMMENT '品牌匹配方式: Terms/Simple'") + except Exception: + pass + try: + cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行'") + except Exception: + pass + cur.execute(""" + CREATE TABLE IF NOT EXISTS columns ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(128) NOT NULL COMMENT '栏目名', + column_key VARCHAR(64) NOT NULL COMMENT '栏目标识', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_column_key (column_key) + ) + """) + cur.execute(""" + CREATE TABLE IF NOT EXISTS user_column_permission ( + user_id INT NOT NULL, + column_id INT NOT NULL, + PRIMARY KEY (user_id, column_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE + ) + """) + try: + cur.execute("UPDATE users SET role = 'normal' WHERE (role IS NULL OR role = '') AND (is_admin = 0 OR is_admin IS NULL)") + cur.execute("SELECT MIN(id) AS mid FROM users WHERE is_admin = 1") + row = cur.fetchone() + if row and row.get('mid'): + mid = row['mid'] + cur.execute("UPDATE users SET role = 'super_admin' WHERE id = %s", (mid,)) + cur.execute("UPDATE users SET role = 'admin' WHERE is_admin = 1 AND id != %s", (mid,)) + cur.execute("UPDATE users SET created_by_id = %s WHERE role = 'admin' AND (created_by_id IS NULL)", (mid,)) + except Exception: + pass + conn.commit() + finally: + conn.close() + _create_initial_admin() + + +def _create_initial_admin(): + """若没有任何管理员,则创建默认超级管理员(首次启动时,仅一个)""" + from werkzeug.security import generate_password_hash + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT id FROM users WHERE role = 'super_admin' LIMIT 1") + if cur.fetchone(): + conn.close() + return + admin_user = os.environ.get('ADMIN_USER', 'admin') + admin_pwd = os.environ.get('ADMIN_PASSWORD', 'admin123') + pwd_hash = generate_password_hash(admin_pwd, method='pbkdf2:sha256') + cur.execute( + "INSERT INTO users (username, password_hash, is_admin, role) VALUES (%s, %s, 1, 'super_admin')", + (admin_user, pwd_hash) + ) + conn.commit() + conn.close() + except Exception: + pass diff --git a/blueprints/__init__.py b/blueprints/__init__.py new file mode 100644 index 0000000..283385b --- /dev/null +++ b/blueprints/__init__.py @@ -0,0 +1 @@ +# 蓝图包 diff --git a/blueprints/__pycache__/__init__.cpython-311.pyc b/blueprints/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42f3b4ca982f394fc9225a5f0fad7f755e9bfcb7 GIT binary patch literal 162 zcmZ3^%ge<81g{&{X7U2*#~=<2FhUuh*?^4c3@Hr344RC7D;bKIfc(!O$*)F0!o@1) z`SQl6y;Gm;TJmJ~jF{ZS%!Y%)FA~nE3e2yv&mLc)fzkUmP~M h`6;D2sdh!IKm$Pb6!Qa#56p~=j2{?aL=iJk3;>aYDa8N) literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/__init__.cpython-39.pyc b/blueprints/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..237128e589c5f15033fb10b1dd7c421702764914 GIT binary patch literal 144 zcmYe~<>g`kg8yrGWby*(#~=+t!6j?} literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/admin.cpython-311.pyc b/blueprints/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23ca2bf98f11a575bbc76570887052a7909c3bb6 GIT binary patch literal 21814 zcmeHvd2ka~)^E4=y=6z-XK)hoqM4V)$z<_lGGF+T%)HE-`4W;z&2;w(y=FA(FgTi?cx&Lp%^3EdD3Ul7@xV97WEgfC zBe5=w#7UVK@4~sOtV;%0xmP}@=u!+SyOe{fE)|aQD7@-HO_zoXE4|u5U6*c9-=!Zk zbQuPXUB*FEmkGyYm`6dXJjAQO{kQPXA6;hBMyj90yRtmyaCYLZ@Jec4#i0cJ^M}Xe zF_PLRu~#Ab+r-T~#=0ySdFv#+jimn0oV%(a~6$Y?vL=Yld^-D%RYJ-9-i*T+rPdMdTQa~)XiVMxbVWC zzW(U>8XQ-um?)bhpstXm!b>+FjmJ&j>Xzi%P>ldW2sj3$U1@BDeSbo6FY^!j<>p0QQd%VV94ip5A}GW3hw}nr-B^l0g6U7 zs`L)KNncdkaJt7c;vX0ua>=6dhuz+&n)LL#N4WI_%T_s%Vw;sOn+T#n)Jp0uL zFMsvn)Zsz*!07?^P+xc5-owWe&&_w38@jb4XQFy|N|evjjcTIoz`z9mK0lCgETYzp zKN86=rt{ZKoSHZ_v!2#$oL6UsN?%mGNQMH8x{Otq#V}lHi4fTnx|mF^teGb)=f|c> z&pvkkFu`eTE}#JkI{^C;gkkSy^ehY6r*fFqI@!pMW2T%gd2JA??db7 zIQE?eGuWpLjJ}-Jm(z-JssL_}nJe8R1AN0OM|~d3*8_KxhXF!d^H5FV=84zY!|>>W zSP&0jKC2&R0UMjI=>~=wtz~UBP>|8Qn|$Ql_G@C zTFPMvC(S{103)*=;^s0XLvdI+(K{AD#QcJqfF_LImPLm+-$pE`<@j=Ib_l1|lG%ND zP}heA^#LWdE}#n=7qNgFq($6ESc&x+a#{n*F;v2e=%P~U$5&;nL-NheTt`4HMu^E1SE4jAP>O$!SxPCF(orXd$r-LRJl}0 z{0xG|jBUu{+i<7a#cwZHXIlAGTql{2R$~4Lnljd3uxMNhpIlZR`}F%%8pgkVSQDYC~0Zc)5 zA3xJQKt?s3I0vEDsNOd^0$75(@q@f-d(~L>=-LDaXUpDu8=U37AMo-q>*!h$6X$l6 zpmf%?);rwh6e z**ohFI^*}c6IG@wzUM&OJ}1iHbB?AS zQ1MTTssLV5v-Dl3R-@ZG?l!D%ZvPLa^3FBogKNr3XL-}s@_jCKiZ`_tGE>_SLENbA zi0l9YGg)zbc-R}2gEl3q@%gD<|G=P!+6ZaF2~2^S5?k8LJ%);Y1Y{hGVfq}CfLO7d z+*lS?P&ie8WktkU9A5RZao$-L-v8R6-yE9W%9PizARd^wkGCsV$SE#DSd>3T=`hLT=;53{n4U0Fx3Z2b5@q<#Nf`=QzPL-c*e znf4yGy@xJ&C|n$FdAV|~Wb16nR(jjLOi2q{(n7;I@KB7*cPJ%c_9eR`-eL{^}?SqbG6T3vbFlZtz_eXblzi?VT_DfyuLbW{#%FG_(*Dcn?_ zkE%mKIq0mSN*MEjk*FLlQT2e29OxVHQ%GeLRRMqBFcnp~M@B&L1!|pP$glRC_VkSU zJ+LZ{j)1yL)9dl~^t-*@cxZSCl6t(u&?YNzaap;?9@QrrOAVhA^g%V;+v|fTPw-#N zEm6 z)EY!~qn@cp+-Hz>DvEo2sFCSDdc1uhb+N&5v&UpRA z>WH2|YNG@AJdqpTNN;GSZTG^-5G^dxa;=9Z_S2gE^Lo?7$!ANS?VI$@cWVAXIS> zN5O;mcRDO5e`@)~eUtmboii%BY!hSMbla%T%KjDu661&&vlma9 zY`)gX6yEoL9ymfDC7B02>;oRg+{>DKX>)JHUhtCPdBbz2E2i)%)?OJ>CQBV*%tu-C zQQCYo!hM&o_58#8x%ac7FNWYd&Ey?n^Nxg6A=Q@!%fo)QpdzG+6dk4=M_?fHOTtZb z-kSfH(fqF*pon$X$DNF|b>5oC5AbcJ91Saw7)LZ%UZHf9&}50plIyG$%Xh?bAbJ~p zCcZ^vq9N9e!(8Jg97k@WsrM}+se9={pao8MJN7V+X^j_j=X7Tc=M58vFKq>poP2az z?UB5aNO5^2w-CMp79D&AEV>ExciEWP7COZcMJ!Q7Yl;%8=vuU<@Pu1e{;*-@H7pPX_sT4lr|N%>E(alX@JN-^olk@{SQKp z!r3?qTPh(;%XRHFa@y!+2e1qhjYLrnGLtNfYFKoLxgu?K29Ap*I~iz>QOo0p zm=mY{mIoC9IcUF;j2b^k1*s%ei?rWrr2WoVw?wLg-e{N78JPg*i?vZmt%qoj?G!*2y+@=8E8{y#ulEYNd` z=L_hT)l1Q{gSHsyl?C%IF|$CwyI5PRS&HU6s3lsfCM9~gHc^PgOp_M!x+qW8{imwQ0GUR zTJZz+Rr21<7xy!$|Cc`(Kw$xWKo?L3RK1d&DbTmw?bxT)sxKP*B|an3n_oySBOTJU ziPP^n5+jIVC(Ma3JgXcW6LCVkg>+O zmtL3g09QWf>l0=PJ+tusy9;kkEnJy)W%IaIgUl_s?S6z^sb)k3AlK^|;!rEK4<)oC zauAUYL=eWK(7*$x9UdGU0K<=wGlORkNHpyo@_YKI0sol>r0%8egRRID5Q4Vzyq^T+&SCGv+eZTt=H$P0Qwp zs#&6nAvUnYhVdrAOczd{J3Xaih*Fj)r3qJ<8gH7nSKd8ljb!CqwqLSOEoZWd*{otZ zYfbopIdk=_xtcN8u;v=tyzA;pfS+nJZC_2R(HXH8PG}L%JN3k4F&PJ+z2*8C|@E zwxV<5CIB$7rIFAsShtal9!eGiCz(3J&lahzs|gyEkG!9m&d@+8lmh z`pnfz+T09a&j_Fl2!K5xa>yOS667`teVdG?5+osRloEvmX*z`1^ylHsUC3a-s0%>?Wi9ngK_2YstM@ z?6bncW`+E-%?1d6u23{<)SqkQD6Gd(*iZ}MziEqG9P+XaqH*+xg#oXKhz;o>m%(V2MOC@O%|IO-4PPP?nnjzCjkgSFrCYgQvv`0 zM#+~1x2#L$7?l4Aa8v+PQ``k`l#g4}fF{jiv57PG6Tl|1*oX{i!U9u3cn0x9oDV=q zMz}(PF-tEl7O_E~SEq+8V!jC4_|`)kmSC>4NbuAx?ro1Oy>Ja+R*?k1#{(KtmIAY+ zfj98+6#>lRpmJgi5?9hF-Yx+^VoH^dn+H|@#9k1KdqIM7AahSZ_>~WhW^AiuE|cas zIQ^-koVB=pncD^cI8mQNEbNnMVE7&RSU431^fe%(hJ;2}0cj7dak&0ddzf=KJ@ls@ zJO z03uuk0I{f*N{Q?UB}%d3XaU^3`Of>-FND7O(+gk(^40%*`}*X+-+2DXg{OYL@bc0Q{i%P88uTe$EB;Kmyt{p#kQF5Z0l zjfMAr7l)tEKPfa>;ouzC;yBRc+A6`WJf!7-=)%lXd<$;A{U-pWl8i1zsy(gT4ldOP zZK3)R89?L&kg=*m?e%pX4RJt-Y$ilVklU({8%M;uP9?GC*rr8=6S$Ep#}8>579FDq zwDP9?JUXPjaKmNh@EXds!y&=!_ozh89Wb02p#fx*;;a?n z19RrJv*xvoc^zwBN1N+s>fdjAx9RGAOm#h5T|d6}r{OsOYHE8}0D#(Q8|(w_XNUtV zae&qUd@0W)pF6bKup!6G=d^u4-ElvCu#+)#v4$?%&=twCUp{>4@WrE(M<@0~EO`i| zwBZP?>MrRn8YT@P1MEvqZ=|=i&_(;;WNfXhtu>|xwlPdZZlmb8;(IXzX10b-oqKHJ zF}ZAkm2CGYaqOIxRU z8A~~9DF*~-si7@3>8d+SmmYzWDLBd&9E~koVnq(jx-4wudl+xGhc4}flPTz93;JTk zOJr06jOemD+JerAvn;eXLfB%M(s&%7x8#OSU)(;qJzO6(8(UHSl=OhG4G z&>35^v|7JgixsY)S^4o!y5JyFaF8uH2>Xuw<|$p+Mjz;+4|mi1k1=L9Yj)FScZ9Ih zMA>xS%<(shri&s@*Yy3f&T86OJykVTb+tHIIAYxw$yqjaX1bKNu9fcpZ4$LD?c)d@$p2bs zfXH86b#{n+f}_YMraBxVpAsnYX`u~;H=yt}tPX?7XBdinW~{S9|`l!MIy zJ&bPr5c7lWJw9$96;OUk8e1|hr#cuBDoYz25p+~d2$OR5Z5NMeW& z=!wHj)K6`daq)Aw11^vyHyynoi>60w(iRd)e3z+&S%UB6WL8SdPvX0uHtK0`51MCp z=wqgNV@Zp%l_!$eIjByLelr?~E0(yex5SjPb-;2=BC|>5^ol| zf+80!QX$&zlWd%)n_C=8&^)T)Py$>9D8bgs?TjF+1s_;@gL`6B&DqfnkbAhD9$wt$ zT}&7L_{)W}&)j(K!-YRSd-J_l7cRWd?=*3HP9?B>P-BQZf(R!@qCFUa(9Uy_$l@tU zoVSUTAcJ_B%*bG@h+mL1!VzAe=J^N(YEx9%>vj7cq8>->u~4j-8%*3-oLbYk`cbWtaqjIE2cb;Yto z%(=SK)&RChDKT3l;@W2|RkWpQ=Dzp4-tGFBU^ce18{6rQZu%I>c6eqtdgzUwbOK~L zaXW8`t;xrXX0#nZ+qYcZeYJ)$>|zbOXv40EEq^jF(G*)I;*+*Rk`Y8vPUI7n%I#rqIcA+b6cqTk^xB^rj}dU@x4ErJ1!f(}relm*8J15+Y>#=lqf;C-&#sI!mKm z{ujakk-rq~fbd`QQTVSb>dGPVi2+4E$#bCaCKUcuu4q)NKUK?7ScjvqzD@>_Yo;tj zow~gi`L$J!y(al*8XUr(nUp}svIPqMe((|)rq1+0Gm?i1Lhzbm-~0lW5V2Ape1uR+ z{1E!ApGe*hg8;es0B>T6x>@}B7OTR7B5{tC3w;6L`ue}Ak?@w^6)%Y&0Bn5dmLlGQfAZaol|&$0axKa}aDfS` zLFkoJt3ccnhzqG%K}sw~xUGk@IRg@q0XY?S;44x3-+`|}{Olp8lG>HAy%JC8%zXU< zKh}W6cZ5_tgrhN_khLFy1dSnFng=+ms2tLq9xMbTUM8Su&Vub2H7M${e=gV_;S}0Q z5i>RJV6%eM!rx3|`0p0)CqW0?=oD zdM{*g4d8>E$#*)3=O56f;ViJ`OLalanaoW=hoBLSCYdeelu;npg_E{cDgKK+HsP%> z1q}7rk=Y;-Fr|Mlz?0LKY6-;D!dQlt)h?dN@TTTLNrB%o_!o)d>3yw9i4QZ(Egh6A zOtk>50sit-A0buXrQ9d6OqAqwr1%WnIcHHSr<7-a$S?-ZMPsm}H4%E04ZgFI6&1(M z|8M__R6ofQ@U%68pDj+7UXi@hY5i)=qS9r4w099d0utCtmP_Fi=U1B@G(Tzvu)|#i zU}yO#a_37Lwh0|=iGdI1^~|2AD!pjY_Fe(`<$okrSsP>|J$w0@7>`1 zXL((J8E~Y~W4(n|49+8oLjHo(6F>k~Dg0+fJW(C;=IVyuh@t$v!>CrqxPZLFa<78LvC;PDCGsKHI$4ziBO30_~3VH_=I#0r*R0=zt%epKUFg@i2Q+p z3-YT?7}2FG#`&1?x#Nij}lDz5&F0r6KUjnallvui+f~sGV>ps!I)Z)F>Kg>M%;z3q~0IP?B&IU0U7% z6U`^y5^o0zJ%h+E5J4QM3y6e(bhwn9Eg`iV-4VOW?cw0ME0@}X(i#y#LJfu93LXIO zMWJRyS`k44Hq{0ss`8xn!)%rAG95q}QU->*P!uUIIF~!YR#R5W*NwUlrE|SGheH1i z2>4G950L}t*O7euh!}YPwH~hD0?&&6&Ycz2>=U5Fe-gzF>MTh7|>7~JzAIU)(H z69FDz8+jAM_>7~Tg`DsUPnb^$L&5)C5OH@dX}eyLztZ?}%UoIYY+3cpgCCbLWesdu z1LNGoI`=U4M%LcQn44I0(;}BNQ_gwsoXIh3a!j3h%SW3WjA;XF+A!WYpPe^ZF_&F3 zn_UvFWwO_?+3V=+nwed5S=(o`wli5f*{q%8>m4q+!nNoHaYcVEI_Vnk#7Y`Wdi% z+%ijS;eBCX8!fM3>Oe>}-V8bd*nd(MMKsxSn!H&}9-Y66(X3`Qt7*;Zh{klmc+ME= zV>Av{_Pr&Sv`16hh^EoKMmQ|0RjIsAZ82<5}`#GcTDa86UM}jUn{-;2xC7& zcR$1!ysW`X8@vL`#qcK9x*D9RY)~`!-^K~}S!pZ4&q{bNm6{51kUUK;yU3L)V$PmW z!d@vV2af>AL|w#?b)n^4OC;AerI_rCfTM5cmD+?W?<%Hn4O_T|F07uhL|XP;^M27r zJ9@zW(QsvW&QUY#sG&DCGL9zJ(L}@P?uj^xP#W#nFq1uBTrs_W`Z0FRPH?>}u4jwu z=kv?M?rF`-56!G+@@v@qn)#wt(^b<)*wx#v9$<>@VTzi+`&c&8F5g>(U~yr{ann;h=x?xv9OH+2$`pCN*PIKKcR zy<;@%l!tm~%rn}z)(4)&$P}Bm)z*REF|2%pgYXk?2<34l(b zc2HL!icpsimth!~yW)6ECc_o*rUOx7xM5t6{vw!ZocoJlYv{~p1goaS=a(2sFYWwN zZ=Ki@x__#l(XU|jE5S-g}hc^{60Wh$bALS>K|Q*cS3KIf~X&BfDurZk*9~rp{9n;* BJ&ynY literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/admin.cpython-39.pyc b/blueprints/__pycache__/admin.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d77d4f19971838c9e30d34851d5be0489c1df08 GIT binary patch literal 11271 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 literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/brand.cpython-311.pyc b/blueprints/__pycache__/brand.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8731fea1cc2b1607ae4bcabf76ac35ed485ae17f GIT binary patch literal 24298 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

=GUKf5 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 Bf-nF8 literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/brand.cpython-39.pyc b/blueprints/__pycache__/brand.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22d046a921bc1f844932e1689031a04575453313 GIT binary patch literal 25387 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 literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/image.cpython-311.pyc b/blueprints/__pycache__/image.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..510cc6b6c8c316f4dee8a679e4adf03de300bdb4 GIT binary patch literal 20393 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 e1LN?B3JXzK3jiECB_wt$lAR*|~k!_8w>g z+1=5BOtTg4iMj~Jt=$iI^{iNFBkgX|GwQJqr?(WtsT>5*)ekd0b^)c8xWS7ML5Gwl zm0tWgnHP6~p9<)pfIEp^$-h{UxF@7~ZGoQ%|jDqWJCoW^8JK^{KyPD zK_b2^!U@Eg(FBMU&;f3wS)9bsG4b#7Wj3a4UEomQ(3LvI^Z;`4v|f^5qJm!5Sj8EU zMaxhCmbHKu9Vuy=1G#r1WIg`u^s|g5TIE3_i_y&rS*espMsCcZpVx+o)A}h8vRP3Uk1pvt=`+XEcJgrS7W}~m8590b* z0;tEPkhtJM1oZ(5uA<;Q2s$KT;vtbZ4Gg#)9{M-} zIf3$O4-GkJIQIq44}xAY@ArwH;P(|q54ojw{2qa$W(hbgvFAwdhuHJ@m7v&;ObB`w zfHKeo$OINo)WqnG{-;h_PYzBG-^f`$o3lK05YA2I)N(nsthtUe*Ri^_oNn!8+nmV~ zXgNm&TP_iowqESKQC>e=UO)3Vn6b*+x$<`DfDoIvmCM`8nznJKZIj#Q%=yz*H_T70XB0Bm$_wf2cKaMJal9zXk!i5$&N*tO#yDk*+&jf z9R_~hka_I!qmLt14m#Xb73}BpEB!m>3az2-Twx7Uu-f0r8}k=2nPCka(qfA1SyMfn zjWHG8Fj;3!*3b^tw3;)mW=yMjz46%2qdURQm$`8=nQ;acTyUn~HmMa;$EGt`DJP!RvNR1Dd$t4F_Sf<6V5D&v?G;p*I0{C4Eede*wqn&(CZcsiw$an6#KK!R{rno<759!a2 z-Y8x_TfCmxu$3*|#uaa4;I{Yk#U&_>DPB8c=1Z$W1>uLn&vMmU-Z!(QZCq&^zpNs- zDr66}aC@6X4M1>aK+C z7zjR&FUn-N92O@8N(|RdYS15#=_bWL9uwcumsrFwDgN=8aZ>zCh^%0+CGMNpPUf53 zJeJKQ-#4*pW~rOUDwyQ^rY_6BDex#9^wU*xy2?rQqB0wYUss6*Yu4d#dMy@=;aND$ z<5*CG$+SNxe!e(h3@;DoO%}6w9f#NP03++cabz|ZADo|O!nAa%gMPWkT^^m$K7n91P6-E#fGp)v9DMFBtMMO~^O%PMXiS&N~`cm38 literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/image.cpython-39.pyc b/blueprints/__pycache__/image.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f9361d30a14c45601025db40b4311b3546b7ff2 GIT binary patch literal 12197 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 literal 0 HcmV?d00001 diff --git a/blueprints/__pycache__/main.cpython-311.pyc b/blueprints/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebd595bafd78f545a4016997241e3fb0ec3fab90 GIT binary patch literal 4528 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', methods=['PUT']) +@admin_required +def admin_update_user(uid): + data = request.get_json() or {} + password = data.get('password') + want_role = (data.get('role') or '').strip() or data.get('role') + role, current_row = _get_current_admin_role() + if not role: + return jsonify({'success': False, 'error': '需要管理员权限'}), 403 + if want_role is None and not password: + return jsonify({'success': False, 'error': '请提供要修改的内容'}) + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,)) + target = cur.fetchone() + if not target: + conn.close() + return jsonify({'success': False, 'error': '用户不存在'}) + if role == 'admin': + if target['role'] != 'normal' or target.get('created_by_id') != current_row['id']: + conn.close() + return jsonify({'success': False, 'error': '只能编辑自己创建的普通用户'}), 403 + want_role = None + else: + if target.get('role') == 'super_admin': + conn.close() + return jsonify({'success': False, 'error': '不能修改超级管理员'}) + if want_role == 'super_admin': + return jsonify({'success': False, 'error': '不能将用户设为超级管理员'}) + if want_role not in ('admin', 'normal', None, ''): + want_role = None + if password: + if len(password) < 6: + conn.close() + return jsonify({'success': False, 'error': '密码至少6个字符'}) + pwd_hash = generate_password_hash(password, method='pbkdf2:sha256') + cur.execute("UPDATE users SET password_hash = %s WHERE id = %s", (pwd_hash, uid)) + if want_role is not None and want_role != '': + is_admin = 1 if want_role == 'admin' else 0 + cur.execute( + "UPDATE users SET is_admin = %s, role = %s WHERE id = %s", + (is_admin, want_role, uid), + ) + conn.commit() + conn.close() + return jsonify({'success': True, 'msg': '更新成功'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + + +@admin_bp.route('/api/admin/user/', methods=['DELETE']) +@admin_required +def admin_delete_user(uid): + from flask import session + if session.get('user_id') == uid: + return jsonify({'success': False, 'error': '不能删除当前登录账号'}) + role, current_row = _get_current_admin_role() + if not role: + return jsonify({'success': False, 'error': '需要管理员权限'}), 403 + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,)) + target = cur.fetchone() + if not target: + conn.close() + return jsonify({'success': False, 'error': '用户不存在'}) + if target.get('role') == 'super_admin': + conn.close() + return jsonify({'success': False, 'error': '不能删除超级管理员'}) + if role == 'admin': + if target.get('role') != 'normal' or target.get('created_by_id') != current_row['id']: + conn.close() + return jsonify({'success': False, 'error': '只能删除自己创建的普通用户'}), 403 + cur.execute("DELETE FROM users WHERE id = %s", (uid,)) + affected = cur.rowcount + conn.commit() + conn.close() + if affected == 0: + return jsonify({'success': False, 'error': '用户不存在'}) + return jsonify({'success': True, 'msg': '删除成功'}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + + +@admin_bp.route('/api/admin/user//column-permissions') +@login_required +def admin_user_column_permissions(uid): + """获取指定用户的栏目权限列表:当前用户只能查自己,管理员可查任意用户。超级管理员返回全部栏目。""" + current_uid = session.get('user_id') + if current_uid != uid: + role, _ = _get_current_admin_role() + if not role: + return jsonify({'success': False, 'error': '无权查看该用户的栏目权限'}), 403 + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT role FROM users WHERE id = %s", (uid,)) + user_row = cur.fetchone() + if user_row and (user_row.get('role') or '').strip() == 'super_admin': + cur.execute(""" + SELECT id, name, column_key, created_at FROM columns ORDER BY id + """) + rows = cur.fetchall() + else: + cur.execute(""" + SELECT c.id, c.name, c.column_key, c.created_at + FROM columns c + INNER JOIN user_column_permission ucp ON ucp.column_id = c.id + WHERE ucp.user_id = %s + ORDER BY c.id + """, (uid,)) + rows = cur.fetchall() + conn.close() + items = [ + { + 'id': r['id'], + 'name': r['name'], + 'column_key': r['column_key'], + 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', + } + for r in rows + ] + return jsonify({'success': True, 'items': items}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}) + + +@admin_bp.route('/api/admin/history') +@admin_required +def admin_history(): + """管理员分页获取所有生成记录,支持按用户和时间筛选""" + page = max(1, int(request.args.get('page', 1))) + page_size = min(50, max(10, int(request.args.get('page_size', 15)))) + offset = (page - 1) * page_size + user_id = request.args.get('user_id', type=int) + time_start = (request.args.get('time_start') or '').strip() + time_end = (request.args.get('time_end') or '').strip() + conditions, params = [], [] + if user_id: + conditions.append("h.user_id = %s") + params.append(user_id) + if time_start: + conditions.append("h.created_at >= %s") + params.append(time_start) + if time_end: + conditions.append("h.created_at <= %s") + params.append(time_end + ' 23:59:59' if len(time_end) <= 10 else time_end) + where_clause = " AND ".join(conditions) if conditions else "1=1" + params_count = params[:] + params.extend([page_size, offset]) + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + """SELECT h.id, h.user_id, h.created_at, h.panel_type, h.original_urls, h.params, h.result_urls, + h.long_image_url, u.username + FROM image_history h + LEFT JOIN users u ON h.user_id = u.id + WHERE """ + where_clause + """ ORDER BY h.created_at DESC LIMIT %s OFFSET %s""", + params, + ) + rows = cur.fetchall() + cur.execute("SELECT COUNT(*) as total FROM image_history h WHERE " + where_clause, params_count) + total = cur.fetchone()['total'] + conn.close() + items = [] + for r in rows: + items.append({ + 'id': r['id'], + 'user_id': r['user_id'], + 'username': r.get('username') or '-', + '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)}) diff --git a/blueprints/auth.py b/blueprints/auth.py new file mode 100644 index 0000000..7b003e0 --- /dev/null +++ b/blueprints/auth.py @@ -0,0 +1,107 @@ +""" +认证蓝图:登录、登出、登录状态校验 +""" +from flask import Blueprint, request, redirect, url_for, session, jsonify +from werkzeug.security import check_password_hash + +from app_common import ( + get_db, + _render_html, + _is_session_user_valid, + login_required, + BASE_DIR, +) +from tool.devices import DeviceIDGenerator + +auth_bp = Blueprint('auth', __name__) + + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + if session.get('user_id') and _is_session_user_valid(): + return redirect(url_for('main.home')) + if request.method == 'POST': + data = request.get_json() if request.is_json else request.form + username = (data.get('username') or '').strip() + password = data.get('password') or '' + if not username or not password: + if request.is_json: + return jsonify({'success': False, 'error': '请输入用户名和密码'}) + return _render_html('login.html', error='请输入用户名和密码') + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "SELECT id, password_hash, machine, is_admin FROM users WHERE username = %s", + (username,) + ) + row = cur.fetchone() + if row and check_password_hash(row['password_hash'], password): + current_machine = DeviceIDGenerator().get_device_id() + stored_machine = (row.get('machine') or '').strip() + if not stored_machine: + with conn.cursor() as cur: + cur.execute("UPDATE users SET machine = %s WHERE id = %s", (current_machine, row['id'])) + conn.commit() + conn.close() + session.permanent = True + session['user_id'] = row['id'] + session['username'] = username + if request.is_json: + return jsonify({'success': True, 'redirect': url_for('main.home')}) + return redirect(url_for('main.home')) + print("验证设备",stored_machine) + print("当前设备",current_machine) + if stored_machine != current_machine and row.get("is_admin") != 1: + conn.close() + err_msg = '当前设备与首次登录设备不一致,请在原设备上登录' + if request.is_json: + return jsonify({'success': False, 'error': err_msg}) + return _render_html('login.html', error=err_msg) + conn.close() + session.permanent = True + session['user_id'] = row['id'] + session['username'] = username + if request.is_json: + return jsonify({'success': True, 'redirect': url_for('main.home')}) + return redirect(url_for('main.home')) + conn.close() + except Exception as e: + if request.is_json: + return jsonify({'success': False, 'error': str(e)}) + return _render_html('login.html', error='登录失败,请稍后重试') + if request.is_json: + return jsonify({'success': False, 'error': '用户名或密码错误'}) + return _render_html('login.html', error='用户名或密码错误') + return _render_html('login.html') + + +@auth_bp.route('/api/auth/check') +@login_required +def api_auth_check(): + """校验登录状态,用于页面加载时判断是否已登录;同时校验机器码是否与首次登录设备一致""" + if not session.get('user_id'): + return jsonify({'logged_in': False}) + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT machine, is_admin FROM users WHERE id = %s", (session['user_id'],)) + row = cur.fetchone() + conn.close() + if not row: + return jsonify({'logged_in': False}) + stored_machine = (row.get('machine') or '').strip() + if stored_machine: + current_machine = DeviceIDGenerator().get_device_id() + if stored_machine != current_machine and row.get("is_admin") != 1: + session.clear() + return jsonify({'logged_in': False, 'error': '当前设备与首次登录设备不一致'}) + except Exception: + return jsonify({'logged_in': False}) + return jsonify({'logged_in': True, 'redirect': url_for('main.home')}) + + +@auth_bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('auth.login')) diff --git a/blueprints/brand.py b/blueprints/brand.py new file mode 100644 index 0000000..01924dc --- /dev/null +++ b/blueprints/brand.py @@ -0,0 +1,855 @@ +""" +品牌爬虫蓝图:展开文件夹、运行任务、任务列表/详情、下载结果 +""" +import os +import json +import threading +import datetime +import traceback +import zipfile +import io +import time +import tempfile +import requests +from queue import Queue, Empty +from concurrent.futures import ThreadPoolExecutor, as_completed +from urllib.parse import urlparse + +from flask import Blueprint, request, jsonify, session, send_file, redirect, Response + +from app_common import get_db, login_required, BASE_DIR +from config import bucket_path +from brand_spider.main import single_file_handle, TaskCancelledError + +brand_bp = Blueprint('brand', __name__) +BRAND_OUTPUT_DIR = os.path.join(BASE_DIR, 'brand_output') +OSS_PREFIX = "brand_results" + +# 任务完成时推送给前端的 SSE 队列:task_id -> [Queue, ...] +_task_event_queues = {} +_task_event_lock = threading.Lock() + +# 任务行级进度(当前处理文件的行数/总行数),仅存内存,不落库: +# task_id -> { +# 'file_index': int, # 当前处理第几个文件 +# 'file_total': int, # 总文件数 +# 'file_path': str, # 当前文件路径 +# 'current_line': int, # 当前行号(或当前品牌序号) +# 'total_lines': int, # 总行数(或总品牌数) +# } +_task_line_progress = {} +_task_line_progress_lock = threading.Lock() + + +def _push_task_event(task_id, event): + """向订阅了该任务的所有 SSE 连接推送事件,并移除该任务的队列列表。 + 同时清理内存中的行级进度,不通过数据库中转行级进度。""" + with _task_event_lock: + queues = _task_event_queues.pop(task_id, []) + for q in queues: + try: + q.put_nowait(event) + except Exception: + pass + # 任务进入终态时,顺便清理行级进度缓存 + with _task_line_progress_lock: + _task_line_progress.pop(task_id, None) + + +def _expand_folder_xlsx(folder_path): + """返回文件夹下所有 .xlsx 文件的绝对路径列表""" + if not folder_path or not os.path.isdir(folder_path): + return [] + paths = [] + for name in os.listdir(folder_path): + if name.endswith('.xlsx') or name.endswith('.XLSX'): + paths.append(os.path.normpath(os.path.join(folder_path, name))) + return paths + + +def _upload_local_xlsx_to_oss(local_path, user_id): + """将本地 xlsx 文件上传到 OSS,返回 (url, None) 或 (None, error_message)。""" + if not local_path or not os.path.isfile(local_path): + return None, "文件不存在" + try: + from ali_oss import upload_file as oss_upload_file + except ImportError: + return None, "OSS 模块未配置" + base = os.path.basename(local_path) + safe_base = "".join(c if c.isalnum() or c in '-_.' else '_' for c in base) + if not safe_base.endswith('.xlsx'): + safe_base = safe_base + '.xlsx' + key = f"{bucket_path}brand_input/{int(time.time() * 1000)}/{user_id}_{safe_base}" + try: + with open(local_path, "rb") as f: + content = f.read() + url = oss_upload_file(content, key) + return url, None + except Exception as e: + return None, str(e) + + +def _run_brand_files(file_paths, task_id=None, check_cancelled=None, strategy="Terms"): + """对多个 xlsx 执行 single_file_handle(线程池多线程),返回 (result_paths, error_message)。 + check_cancelled: 可调用对象,返回 True 时中止并返回 (result_paths, 'cancelled')。 + task_id 存在时会在每处理完一个文件后更新 progress_current。 + strategy: 品牌匹配方式,默认 Terms,可选 Simple。""" + os.makedirs(BRAND_OUTPUT_DIR, exist_ok=True) + subdir = str(task_id) if task_id else ('immediate_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S')) + out_dir = os.path.join(BRAND_OUTPUT_DIR, subdir) + os.makedirs(out_dir, exist_ok=True) + + # 先筛出有效路径并生成 (fp, out_path) 列表 + tasks = [] + for fp in file_paths: + if not fp or not isinstance(fp, str) or not os.path.isfile(fp): + continue + base = os.path.splitext(os.path.basename(fp))[0] + safe_base = "".join(c if c.isalnum() or c in '-_' else '_' for c in base) + out_path = os.path.join(out_dir, safe_base + '_result.xlsx') + tasks.append((fp, out_path)) + + if not tasks: + return [], None + + result_paths = [] + result_lock = threading.Lock() + current = [0] # 用列表以便在闭包中修改 + + def process_one(fp, out_path, file_index, files_total): + # 行级进度回调:由 single_file_handle 在循环中调用,直接写入内存字典,不落库 + def progress_callback(current_line, total_lines, extra=None): + if not task_id: + return + info = { + 'file_index': file_index, + 'file_total': files_total, + 'file_path': fp, + 'current_line': int(current_line or 0), + 'total_lines': int(total_lines or 0), + } + if isinstance(extra, dict): + info.update(extra) + with _task_line_progress_lock: + _task_line_progress[task_id] = info + + single_file_handle(fp, out_path, strategy, check_cancelled=check_cancelled, progress_callback=progress_callback) + return out_path + + max_workers = min(8, len(tasks)) + with ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_out = { + executor.submit(process_one, fp, out_path, idx + 1, len(tasks)): out_path + for idx, (fp, out_path) in enumerate(tasks) + } + try: + for future in as_completed(future_to_out): + if check_cancelled and check_cancelled(): + for f in future_to_out: + f.cancel() + return result_paths, 'cancelled' + try: + out_path = future.result() + with result_lock: + result_paths.append(out_path) + current[0] += 1 + if task_id: + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET progress_current = %s WHERE id = %s", + (current[0], task_id) + ) + conn.commit() + conn.close() + except Exception: + print(traceback.format_exc()) + except TaskCancelledError: + for f in future_to_out: + f.cancel() + return result_paths, 'cancelled' + except Exception as e: + print(traceback.format_exc()) + print(e) + for f in future_to_out: + f.cancel() + return result_paths, str(e) + except Exception as e: + print(traceback.format_exc()) + return result_paths, str(e) + return result_paths, None + + +def _upload_results_to_oss(source_paths, local_result_paths, task_id): + """ + 将源文件和结果文件上传到 OSS,并打包成 zip(分两个文件夹:源文件、结果文件)上传。 + 返回 (result_data, error_message),result_data 为 {"urls": [url1, ...], "zip_url": "..."},出错时为 (None, err)。 + """ + if not local_result_paths or not task_id: + return None, "无结果文件" + try: + from ali_oss import upload_file as oss_upload_file + except ImportError: + return None, "OSS 模块未配置" + urls = [] + subdir = f"{OSS_PREFIX}/{task_id}" + for fp in local_result_paths: + if not fp or not os.path.isfile(fp): + continue + name = os.path.basename(fp) + key = f"{bucket_path}{subdir}/{name}" + try: + with open(fp, "rb") as f: + content = f.read() + url = oss_upload_file(content, key) + urls.append(url) + except Exception as e: + return None, f"上传结果文件失败: {e}" + if not urls: + return None, "没有可上传的结果文件" + # 打包成 zip:源文件 -> 源文件/,结果文件 -> 结果文件/ + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for fp in (source_paths or []): + if fp and os.path.isfile(fp): + zf.write(fp, os.path.join("源文件", os.path.basename(fp))) + for fp in local_result_paths: + if fp and os.path.isfile(fp): + zf.write(fp, os.path.join("结果文件", os.path.basename(fp))) + buf.seek(0) + zip_key = f"{bucket_path}{subdir}/result.zip" + try: + zip_url = oss_upload_file(buf.getvalue(), zip_key) + except Exception as e: + return {"urls": urls, "zip_url": None}, None # 单文件链接已保存,zip 失败不报错 + return {"urls": urls, "zip_url": zip_url}, None + + +def _background_brand_task(task_id): + """后台执行单个品牌爬虫任务并更新数据库""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "SELECT id, user_id, file_paths, strategy, `desc` FROM brand_crawl_tasks WHERE id = %s AND status = 'pending'", + (task_id,) + ) + row = cur.fetchone() + conn.close() + if not row: + return + file_paths = row.get('file_paths') + if isinstance(file_paths, str): + file_paths = json.loads(file_paths) if file_paths else [] + # 优先使用表里存的 strategy 字段,兼容旧数据则从 desc 解析 + strategy = (row.get('strategy') or '').strip() or None + if not strategy: + desc_val = (row.get('desc') or '').strip() + strategy = "Simple" if desc_val.startswith('[Simple]') else "Terms" + if strategy not in ('Terms', 'Simple'): + strategy = 'Terms' + if not file_paths: + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", + ('没有有效文件路径', task_id) + ) + conn.commit() + conn.close() + except Exception: + pass + _push_task_event(task_id, {'status': 'failed', 'error_message': '没有有效文件路径'}) + return + # 将 file_paths 解析为本地路径:URL 下载到 BRAND_OUTPUT_DIR 对应任务文件夹下(兼容旧数据中的本地路径) + os.makedirs(BRAND_OUTPUT_DIR, exist_ok=True) + task_subdir = str(task_id) if task_id else ('immediate_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S')) + task_dir = os.path.join(BRAND_OUTPUT_DIR, task_subdir) + os.makedirs(task_dir, exist_ok=True) + + local_paths = [] + downloaded_files_to_cleanup_on_fail = [] + for idx, p in enumerate(file_paths): + if not p or not isinstance(p, str): + continue + p = p.strip() + if _is_url(p): + try: + parsed = urlparse(p) + url_name = os.path.basename(parsed.path or '') + if not url_name: + url_name = f"input_{idx + 1}.xlsx" + safe_name = "".join(c if c.isalnum() or c in '-_.' else '_' for c in url_name) + if not safe_name.lower().endswith('.xlsx'): + safe_name = safe_name + '.xlsx' + + base_no_ext, ext = os.path.splitext(safe_name) + dest = os.path.join(task_dir, safe_name) + n = 1 + while os.path.exists(dest): + dest = os.path.join(task_dir, f"{base_no_ext}_{n}{ext}") + n += 1 + + r = requests.get(p, timeout=120, stream=True) + r.raise_for_status() + with open(dest, "wb") as f: + for chunk in r.iter_content(chunk_size=1024 * 1024): + if chunk: + f.write(chunk) + local_paths.append(dest) + downloaded_files_to_cleanup_on_fail.append(dest) + except Exception as e: + for tf in downloaded_files_to_cleanup_on_fail: + try: + os.unlink(tf) + except Exception: + pass + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", + (f'下载文件失败: {str(e)}', task_id) + ) + conn.commit() + conn.close() + except Exception: + pass + _push_task_event(task_id, {'status': 'failed', 'error_message': f'下载文件失败: {str(e)}'}) + return + elif os.path.isfile(p): + local_paths.append(p) + if not local_paths: + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", + ('没有有效的本地或可下载文件', task_id) + ) + conn.commit() + conn.close() + except Exception: + pass + _push_task_event(task_id, {'status': 'failed', 'error_message': '没有有效的本地或可下载文件'}) + return + try: + # 原子地将 pending 改为 running,避免用户先点取消时仍被覆盖为 running + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'running', progress_total = %s, progress_current = 0 WHERE id = %s AND status = 'pending'", + (len(local_paths), task_id) + ) + n = cur.rowcount + conn.commit() + conn.close() + if n == 0: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT status FROM brand_crawl_tasks WHERE id = %s", (task_id,)) + r = cur.fetchone() + conn.close() + if r and (r.get('status') or '') == 'cancelled': + _push_task_event(task_id, {'status': 'cancelled'}) + return + except Exception: + pass + + def check_cancelled(): + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT status FROM brand_crawl_tasks WHERE id = %s", (task_id,)) + row = cur.fetchone() + conn.close() + return row and (row.get('status') or '') == 'cancelled' + except Exception: + return False + + result_paths, err = _run_brand_files(local_paths, task_id=task_id, check_cancelled=check_cancelled, strategy=strategy) + if err: + try: + conn = get_db() + with conn.cursor() as cur: + st = 'cancelled' if err == 'cancelled' else 'failed' + cur.execute( + "UPDATE brand_crawl_tasks SET status = %s, error_message = %s WHERE id = %s", + (st, err if st == 'failed' else None, task_id) + ) + conn.commit() + conn.close() + except Exception as e: + traceback.print_exc() + print(e) + pass + _push_task_event(task_id, {'status': st, 'error_message': err if st == 'failed' else None}) + return + result_data, upload_err = _upload_results_to_oss(local_paths, result_paths, task_id) + if upload_err: + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", + (upload_err, task_id) + ) + conn.commit() + conn.close() + except Exception: + pass + _push_task_event(task_id, {'status': 'failed', 'error_message': upload_err}) + return + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'success', result_paths = %s WHERE id = %s", + (json.dumps(result_data), task_id) + ) + conn.commit() + conn.close() + except Exception: + pass + _push_task_event(task_id, {'status': 'success'}) + finally: + pass + except Exception as e: + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", + (str(e), task_id) + ) + conn.commit() + conn.close() + except Exception: + pass + _push_task_event(task_id, {'status': 'failed', 'error_message': str(e)}) + + +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 [] + + +def _is_url(s): + if not isinstance(s, str) or not s.strip(): + return False + return s.strip().startswith('http://') or s.strip().startswith('https://') + + +def _normalize_result_paths(raw): + """ + 将 result_paths 规范化为 (url_list, zip_url)。 + 支持旧格式 list 或新格式 dict {"urls": [...], "zip_url": "..."}。 + """ + if not raw: + return [], None + if isinstance(raw, dict): + urls = raw.get('urls') or [] + if not isinstance(urls, list): + urls = [] + zip_url = raw.get('zip_url') + if zip_url and not isinstance(zip_url, str): + zip_url = None + return urls, zip_url + if isinstance(raw, list): + return raw, None + return [], None + + +@brand_bp.route('/api/brand/expand-folder', methods=['POST']) +@login_required +def api_brand_expand_folder(): + """展开文件夹,返回其下所有 .xlsx 文件路径列表""" + try: + data = request.get_json() or {} + folder = (data.get('folder') or '').strip() + if not folder: + return jsonify({'success': False, 'error': '请提供 folder 路径'}), 400 + paths = _expand_folder_xlsx(folder) + return jsonify({'success': True, 'paths': paths}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/run', methods=['POST']) +@login_required +def api_brand_run(): + """立即运行:创建任务并后台执行,立即返回 task_id,前端可轮询进度与取消""" + try: + data = request.get_json() or {} + paths = data.get('paths') or [] + # task_type: 1=立即执行,2=添加任务;此接口默认 1 + try: + task_type = int(data.get('task_type') or 1) + except Exception: + task_type = 1 + if task_type not in (1, 2): + task_type = 1 + strategy = (data.get('strategy') or 'Terms').strip() + if strategy not in ('Terms', 'Simple'): + strategy = 'Terms' + if not isinstance(paths, list): + paths = [] + paths = [p.strip() for p in paths if p and isinstance(p, str) and os.path.isfile(p.strip())] + if not paths: + return jsonify({'success': False, 'error': '没有有效的 xlsx 文件路径'}), 400 + # 先上传到 OSS,获取链接再入库 + user_id = session['user_id'] + urls = [] + for p in paths: + url, err = _upload_local_xlsx_to_oss(p, user_id) + if err: + return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500 + urls.append(url) + desc_core = ', '.join(os.path.basename(p) for p in paths)[:500] if paths else '' + desc = f'[{strategy}] ' + (desc_core or '') + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "INSERT INTO brand_crawl_tasks (user_id, file_paths, `desc`, strategy, status, task_type) VALUES (%s, %s, %s, %s, 'pending', %s)", + (user_id, json.dumps(urls), desc or None, strategy, task_type) + ) + task_id = cur.lastrowid + conn.commit() + conn.close() + threading.Thread(target=_background_brand_task, args=(task_id,), daemon=True).start() + return jsonify({'success': True, 'task_id': task_id}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/tasks', methods=['GET', 'POST']) +@login_required +def api_brand_tasks(): + """GET: 获取当前用户的任务列表;POST: 添加任务(后台执行)""" + if request.method == 'GET': + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + """SELECT id, file_paths, `desc`, strategy, status, result_paths, error_message, + COALESCE(progress_current, 0) AS progress_current, + COALESCE(progress_total, 0) AS progress_total, + created_at, updated_at + FROM brand_crawl_tasks WHERE user_id = %s ORDER BY id DESC LIMIT 100""", + (session['user_id'],) + ) + rows = cur.fetchall() + conn.close() + items = [] + for r in rows: + items.append({ + 'id': r['id'], + 'file_paths': _parse_json(r['file_paths'], []), + 'desc': (r.get('desc') or '').strip() or None, + 'strategy': (r.get('strategy') or 'Terms').strip() or 'Terms', + 'status': r.get('status') or 'pending', + 'result_paths': _parse_json(r['result_paths']), + 'error_message': (r.get('error_message') or '').strip() or None, + 'progress_current': int(r.get('progress_current') or 0), + 'progress_total': int(r.get('progress_total') or 0), + 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', + 'updated_at': r['updated_at'].strftime('%Y-%m-%d %H:%M') if r.get('updated_at') else '', + }) + return jsonify({'success': True, 'items': items}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + # POST + try: + data = request.get_json() or {} + paths = data.get('paths') or [] + # task_type: 1=立即执行,2=添加任务;此接口默认 2 + try: + task_type = int(data.get('task_type') or 2) + except Exception: + task_type = 2 + if task_type not in (1, 2): + task_type = 2 + strategy = (data.get('strategy') or 'Terms').strip() + if strategy not in ('Terms', 'Simple'): + strategy = 'Terms' + if not isinstance(paths, list): + paths = [] + paths = [p.strip() for p in paths if p and isinstance(p, str)] + if not paths: + return jsonify({'success': False, 'error': '请提供至少一个文件路径或链接'}), 400 + user_id = session['user_id'] + urls = [] + for p in paths: + if _is_url(p): + urls.append(p) + elif os.path.isfile(p): + url, err = _upload_local_xlsx_to_oss(p, user_id) + if err: + return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500 + urls.append(url) + else: + return jsonify({'success': False, 'error': f'无效路径或链接: {p[:80]}'}), 400 + desc_core = ', '.join(os.path.basename(urlparse(u).path or u) for u in urls)[:500] if urls else '' + desc = f'[{strategy}] ' + (desc_core or '') + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "INSERT INTO brand_crawl_tasks (user_id, file_paths, `desc`, strategy, status, task_type) VALUES (%s, %s, %s, %s, 'pending', %s)", + (user_id, json.dumps(urls), desc or None, strategy, task_type) + ) + task_id = cur.lastrowid + conn.commit() + conn.close() + # threading.Thread(target=_background_brand_task, args=(task_id,), daemon=True).start() + return jsonify({'success': True, 'task_id': task_id}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/tasks/') +@login_required +def api_brand_task_detail(task_id): + """获取单个任务详情(用于轮询状态)""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + """SELECT id, file_paths, `desc`, strategy, status, result_paths, error_message, + COALESCE(progress_current, 0) AS progress_current, + COALESCE(progress_total, 0) AS progress_total, + created_at, updated_at + FROM brand_crawl_tasks WHERE id = %s AND user_id = %s""", + (task_id, session['user_id']) + ) + row = cur.fetchone() + conn.close() + if not row: + return jsonify({'success': False, 'error': '任务不存在'}), 404 + return jsonify({ + 'success': True, + 'task': { + 'id': row['id'], + 'file_paths': _parse_json(row['file_paths'], []), + 'desc': (row.get('desc') or '').strip() or None, + 'strategy': (row.get('strategy') or 'Terms').strip() or 'Terms', + 'status': row.get('status') or 'pending', + 'result_paths': _parse_json(row['result_paths']), + 'error_message': (row.get('error_message') or '').strip() or None, + 'progress_current': int(row.get('progress_current') or 0), + 'progress_total': int(row.get('progress_total') or 0), + 'created_at': row['created_at'].strftime('%Y-%m-%d %H:%M') if row.get('created_at') else '', + 'updated_at': row['updated_at'].strftime('%Y-%m-%d %H:%M') if row.get('updated_at') else '', + } + }) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/tasks//line-progress') +@login_required +def api_brand_task_line_progress(task_id): + """ + 获取任务的“当前文件行级进度”(当前行数/总行数)。 + 该信息仅存放在内存字典 _task_line_progress 中,不写入数据库。 + """ + try: + with _task_line_progress_lock: + info = _task_line_progress.get(task_id) + if not info: + return jsonify({'success': True, 'has_progress': False}) + # 为了避免暴露完整路径,只返回文件名 + file_name = os.path.basename(info.get('file_path') or '') if info.get('file_path') else '' + resp = { + 'file_index': int(info.get('file_index') or 0), + 'file_total': int(info.get('file_total') or 0), + 'file_name': file_name, + 'current_line': int(info.get('current_line') or 0), + 'total_lines': int(info.get('total_lines') or 0), + } + return jsonify({'success': True, 'has_progress': True, 'info': resp}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/tasks//events') +@login_required +def api_brand_task_events(task_id): + """SSE:任务完成时后端主动推送事件,前端监听后隐藏进度条与取消按钮""" + def _task_status_event(): + conn = get_db() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT status, error_message FROM brand_crawl_tasks WHERE id = %s AND user_id = %s", + (task_id, session['user_id']) + ) + row = cur.fetchone() + finally: + conn.close() + if not row: + return None + st = (row.get('status') or '').lower() + if st in ('success', 'failed', 'cancelled'): + return {'status': st, 'error_message': (row.get('error_message') or '').strip() or None} + return None + + # 若任务已处于终态,直接返回一条事件后结束 + ev = _task_status_event() + if ev is not None: + def _one_shot(): + yield "data: " + json.dumps(ev, ensure_ascii=False) + "\n\n" + return Response( + _one_shot(), + mimetype='text/event-stream', + headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'} + ) + + # 否则注册队列,等待后台任务完成时推送 + q = Queue() + with _task_event_lock: + _task_event_queues.setdefault(task_id, []).append(q) + + def _stream(): + try: + while True: + try: + event = q.get(timeout=20) + yield "data: " + json.dumps(event, ensure_ascii=False) + "\n\n" + return + except Empty: + yield ": keepalive\n\n" + finally: + with _task_event_lock: + lst = _task_event_queues.get(task_id, []) + if q in lst: + lst.remove(q) + if not lst: + _task_event_queues.pop(task_id, None) + + return Response( + _stream(), + mimetype='text/event-stream', + headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'} + ) + + +@brand_bp.route('/api/brand/tasks//cancel', methods=['POST']) +@login_required +def api_brand_task_cancel(task_id): + """取消正在执行或等待中的任务(pending/running 均可取消)""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "UPDATE brand_crawl_tasks SET status = 'cancelled' WHERE id = %s AND user_id = %s AND status IN ('pending', 'running')", + (task_id, session['user_id']) + ) + n = cur.rowcount + conn.commit() + conn.close() + if n == 0: + return jsonify({'success': False, 'error': '任务不存在或无法取消'}), 400 + _push_task_event(task_id, {'status': 'cancelled'}) + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/tasks/', methods=['DELETE']) +@login_required +def api_brand_task_delete(task_id): + """删除任务(仅限非 running 状态)""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "DELETE FROM brand_crawl_tasks WHERE id = %s AND user_id = %s AND status != 'running'", + (task_id, session['user_id']) + ) + n = cur.rowcount + conn.commit() + conn.close() + if n == 0: + return jsonify({'success': False, 'error': '任务不存在或正在执行中无法删除'}), 400 + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + +@brand_bp.route('/api/brand/download/') +@login_required +def api_brand_download(task_id): + """下载任务结果:优先使用 OSS zip_url 重定向;否则本地/单链接/多链接按原逻辑处理""" + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute( + "SELECT result_paths FROM brand_crawl_tasks WHERE id = %s AND user_id = %s", + (task_id, session['user_id']) + ) + row = cur.fetchone() + conn.close() + if not row or not row.get('result_paths'): + return jsonify({'success': False, 'error': '无结果可下载'}), 404 + raw = row['result_paths'] + if isinstance(raw, str): + raw = _parse_json(raw) + url_list, zip_url = _normalize_result_paths(raw) + # 新格式:有 zip_url 直接重定向到 OSS + if zip_url and _is_url(zip_url): + return redirect(zip_url, code=302) + + # 兼容旧格式:result_paths 可能为 list(本地路径或 url) + if not url_list and isinstance(raw, list): + url_list = raw + local_paths = [p for p in url_list if isinstance(p, str) and not _is_url(p) and os.path.isfile(p)] + url_paths = [p.strip() for p in url_list if isinstance(p, str) and _is_url(p)] + + if len(url_paths) == 1 and not local_paths: + return redirect(url_paths[0], code=302) + + if len(local_paths) == 1 and not url_paths: + return send_file( + local_paths[0], + as_attachment=True, + download_name=os.path.basename(local_paths[0]) + ) + + if local_paths or url_paths: + buf = io.BytesIO() + with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: + for p in local_paths: + zf.write(p, os.path.basename(p)) + for url in url_paths: + try: + import requests as req + r = req.get(url, timeout=30, stream=True) + r.raise_for_status() + name = os.path.basename(urlparse(url).path) or ('file_%s' % (url_paths.index(url))) + if not name or name == 'file_%s' % url_paths.index(url): + name = 'download_%s' % url_paths.index(url) + zf.writestr(name, r.content) + except Exception: + pass + buf.seek(0) + return send_file( + buf, + mimetype='application/zip', + as_attachment=True, + download_name='brand_task_%s.zip' % task_id + ) + + return jsonify({'success': False, 'error': '结果文件不存在或链接不可用'}), 404 + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 500 + + diff --git a/blueprints/image.py b/blueprints/image.py new file mode 100644 index 0000000..c454d54 --- /dev/null +++ b/blueprints/image.py @@ -0,0 +1,411 @@ +""" +图片生成蓝图:生成、历史、下载、拼接、版本 +""" +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)}) + + + + + diff --git a/blueprints/main.py b/blueprints/main.py new file mode 100644 index 0000000..c6ec5a2 --- /dev/null +++ b/blueprints/main.py @@ -0,0 +1,69 @@ +""" +主页面蓝图:首页、home、图片工作台、品牌页、静态文件、Logo +""" +import os +from flask import Blueprint, send_file + +from app_common import ( + get_db, + _render_html, + _is_session_user_valid, + login_required, + admin_required, + STATIC_DIR, + BASE_DIR, +) +from flask import redirect, url_for, session + +from config import base_url,version + +main_bp = Blueprint('main', __name__) + + +@main_bp.route('/') +def index(): + if session.get('user_id') and _is_session_user_valid(): + return redirect(url_for('main.home')) + return redirect(url_for('auth.login')) + + +@main_bp.route('/home') +@login_required +def home(): + try: + conn = get_db() + with conn.cursor() as cur: + cur.execute("SELECT username, is_admin FROM users WHERE id = %s", (session['user_id'],)) + row = cur.fetchone() + conn.close() + return _render_html('home.html', username=row.get('username', ''), is_admin=bool(row.get('is_admin')), user_id=session.get('user_id'),baseUrl=base_url,version=version) + except Exception: + return _render_html('home.html', username=session.get('username', ''), is_admin=False, user_id=session.get('user_id'),baseUrl=base_url,version=version) + + +@main_bp.route('/image') +@login_required +def wb(): + return _render_html('index.html') + + +@main_bp.route('/brand') +@login_required +def brand_page(): + return _render_html('brand.html') + + +@main_bp.route('/static/') +def serve_static(filename): + """提供 static 目录及子目录下的静态文件访问。""" + filepath = os.path.normpath(os.path.join(STATIC_DIR, filename)) + static_abs = os.path.abspath(STATIC_DIR) + file_abs = os.path.abspath(filepath) + if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs): + return '', 404 + return send_file(file_abs, as_attachment=False) + + +@main_bp.route('/logo.jpg', methods=['GET']) +def get_logo_image(): + return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg') diff --git a/brand_spider/__pycache__/main.cpython-39.pyc b/brand_spider/__pycache__/main.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8e611a0a90d26f1bdf3839130f42ba21227a85e GIT binary patch literal 15429 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 literal 0 HcmV?d00001 diff --git a/brand_spider/__pycache__/web_dec.cpython-39.pyc b/brand_spider/__pycache__/web_dec.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db0f7cdcdbe74591102c267d59ab5ce6efe3d8d6 GIT binary patch literal 4726 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 AWdHyG literal 0 HcmV?d00001 diff --git a/brand_spider/main.py b/brand_spider/main.py new file mode 100644 index 0000000..d6a7305 --- /dev/null +++ b/brand_spider/main.py @@ -0,0 +1,692 @@ +import json +import re +import random +import requests +import time +import datetime +import openpyxl +import unicodedata +from openpyxl import Workbook + +from brand_spider.web_dec import decrypt_via_service, get_guid + +try: + from config import proxy_url as CONFIG_PROXY_URL, proxy_mode as CONFIG_PROXY_MODE +except ImportError: + CONFIG_PROXY_URL = None + CONFIG_PROXY_MODE = 1 + +# Forbidden 后 3 分钟内统一使用代理:记录代理生效截止时间与当前代理 +_FORBIDDEN_PROXY_UNTIL = 0.0 +_FORBIDDEN_PROXY_DICT = None +_FORBIDDEN_PROXY_MINUTES = 0.5 + + +special_char_pattern = re.compile(r'[^\w\s]') + +def remove_accents(input_str): + # 将字符分解为基础字符和重音符号 + nksel = unicodedata.normalize('NFKD', input_str) + # 过滤掉非间距重音符号,并重新编码 + return "".join([c for c in nksel if not unicodedata.combining(c)]) + + +def clean_text(text): + text = remove_accents(text) + return special_char_pattern.sub(' ', text).replace(" ","") + + +def create_code_generator(): + counter = random.randint(0, 0xFFFFFF) + def get_code(): + nonlocal counter + counter = (counter + 1) % 0xFFFFFF + low_16_bits = counter & 0xFFFF + return f"{low_16_bits:04x}" + return get_code() + + +def search(hashsearch, data, proxies=None): + global _FORBIDDEN_PROXY_UNTIL, _FORBIDDEN_PROXY_DICT + + url = "https://api.branddb.wipo.int/search" + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", + "cache-control": "no-cache", + "content-type": "application/json", + "hashsearch": hashsearch, + "origin": "https://branddb.wipo.int", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://branddb.wipo.int/", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36" + } + + + + # 若 3 分钟内曾出现 Forbidden,则直接使用当时保存的代理 + now = time.time() + if now < _FORBIDDEN_PROXY_UNTIL and _FORBIDDEN_PROXY_DICT: + proxies = _FORBIDDEN_PROXY_DICT + print("处于 Forbidden 代理窗口内,直接使用代理",proxies) + + # 发送 POST 请求 + response = requests.post(url, headers=headers, json=data, proxies=proxies) + enc_text = response.text + # print("原始结果", enc_text) + + # 若返回 Forbidden,则从 config 的 proxy_url 获取代理 IP 后重试,并开启 3 分钟代理窗口 + if enc_text.strip() == '{"message":"Forbidden"}' and CONFIG_PROXY_URL: + try: + proxy_resp = requests.get(CONFIG_PROXY_URL, timeout=10) + print("代理请求结果->:", proxy_resp.text) + # 模式 2:账号密码代理,接口返回 JSON + if CONFIG_PROXY_MODE == 2: + resp_json = proxy_resp.json() + proxy_list = resp_json.get("data", {}).get("list") or [] + first_item = proxy_list[0] if proxy_list else None + if first_item: + ip = first_item.get("ip") + port = first_item.get("port") + account = first_item.get("account") + password = first_item.get("password") + if ip and port and account and password: + auth_proxy = f"{account}:{password}@{ip}:{port}" + print("获取到账号密码代理:", auth_proxy) + proxies = { + "http": f"http://{auth_proxy}", + "https": f"http://{auth_proxy}", + } + # 默认模式 1:普通 IP:port 文本 + if CONFIG_PROXY_MODE != 2: + proxy_ip = (proxy_resp.text or "").strip() + if proxy_ip: + proxies = { + "http": f"http://{proxy_ip}", + "https": f"http://{proxy_ip}", + } + + if proxies: + response = requests.post(url, headers=headers, json=data, proxies=proxies) + enc_text = response.text + print("代理重试结果", enc_text) + # 记录 3 分钟内都使用该代理 + _FORBIDDEN_PROXY_UNTIL = now + _FORBIDDEN_PROXY_MINUTES * 60 + _FORBIDDEN_PROXY_DICT = proxies + except Exception as e: + print("获取代理或重试失败:", e) + + return enc_text + + +class TaskCancelledError(Exception): + """用户取消任务时抛出,供上层捕获并中止""" + pass + + +def single_file_handle(file_path, output_path, strategy="Terms", check_cancelled=None, progress_callback=None): + status_info = { + "Ended": "已结束", + "Expired": "已过期的", + "Pending": "待决", + "Registered": "已注册", + "RegisteredMadrid": "国际注册有效", + "Unknown": "未知" + } + country_info = { + "AB": "ARABPAT", + "AD": "安道尔", + "AE": "UAE", + "AF": "阿富汗", + "AFR": "非洲", + "AG": "安提瓜和巴布达", + "AI": "安圭拉", + "AL": "阿尔巴尼亚", + "AM": "亚美尼亚", + "AN": "荷属安的列斯", + "ANT": "南极洲", + "AO": "安哥拉", + "AP": "非洲地区知识产权组织 ", + "AQ": "南极洲", + "AR": "阿根廷", + "AS": "美属萨摩亚", + "ASI": "亚洲", + "AT": "奥地利", + "AU": "澳大利亚", + "AW": "阿鲁巴", + "AX": "奥兰群岛", + "AZ": "阿塞拜疆", + "BA": "波斯尼亚和黑塞哥维纳", + "BB": "巴巴多斯", + "BD": "孟加拉国", + "BE": "比利时", + "BF": "布基纳法索", + "BG": "保加利亚", + "BH": "巴林", + "BI": "布隆迪", + "BJ": "贝宁", + "BL": "圣巴泰勒米", + "BM": "百慕大", + "BN": "文莱达鲁萨兰国", + "BO": "多民族玻利维亚国", + "BQ": "博纳尔、圣俄斯塔休斯和萨巴", + "BR": "巴西", + "BS": "巴哈马", + "BT": "不丹", + "BV": "布韦岛", + "BW": "博茨瓦纳", + "BX": "比荷卢知识产权局", + "BY": "白俄罗斯", + "BZ": "伯利兹", + "CA": "加拿大", + "CC": "科科斯群岛(基灵群岛)", + "CD": "刚果民主共和国", + "CF": "中非共和国", + "CG": "刚果", + "CH": "瑞士", + "CI": "科特迪瓦", + "CK": "库克群岛", + "CL": "智利", + "CM": "喀麦隆", + "CN": "中国", + "CO": "哥伦比亚", + "CR": "哥斯达黎加", + "CS": "捷克斯洛伐克", + "CU": "古巴", + "CV": "佛得角", + "CW": "库拉索", + "CX": "圣诞岛", + "CY": "塞浦路斯", + "CZ": "捷克共和国", + "DD": "德意志民主共和国", + "DE": "德国", + "DJ": "吉布提", + "DK": "丹麦", + "DM": "多米尼克", + "DO": "多米尼加", + "DT": "西德", + "DZ": "阿尔及利亚", + "EA": "欧亚专利组织", + "EC": "厄瓜多尔", + "EE": "爱沙尼亚", + "EG": "埃及", + "EH": "西撒哈拉", + "EM": "欧洲联盟", + "EP": "欧洲专利局", + "ER": "厄立特里亚", + "ES": "西班牙", + "ET": "埃塞俄比亚", + "EUR": "欧洲", + "FI": "芬兰", + "FJ": "斐济", + "FK": "福克兰群岛(马尔维纳斯群岛)", + "FM": "密克罗尼西亚联邦", + "FO": "法罗群岛", + "FR": "法国", + "GA": "加蓬", + "GB": "英国", + "GC": "海湾阿拉伯国家合作委员会专利局", + "GD": "格林纳达", + "GE": "格鲁吉亚", + "GF": "法属圭亚那", + "GG": "格恩西岛", + "GH": "加纳", + "GI": "直布罗陀", + "GL": "格陵兰", + "GM": "冈比亚", + "GN": "几内亚", + "GP": "瓜德罗普", + "GQ": "赤道几内亚", + "GR": "希腊", + "GS": "南乔治亚和南桑威奇群岛", + "GT": "危地马拉", + "GU": "关岛", + "GW": "几内亚比绍", + "GY": "圭亚那", + "HK": "香港", + "HM": "赫德岛和麦克唐纳群岛", + "HN": "洪都拉斯", + "HR": "克罗地亚", + "HT": "海地", + "HU": "匈牙利", + "IB": "世界知识产权组织国际局", + "ID": "印度尼西亚", + "IE": "爱尔兰", + "IL": "以色列", + "IM": "马恩岛", + "IN": "印度", + "INN": "世界卫生组织", + "IO": "英属印度洋领地", + "IQ": "伊拉克", + "IR": "伊朗伊斯兰共和国", + "IS": "冰岛", + "IT": "意大利", + "JE": "泽西岛", + "JM": "牙买加", + "JO": "约旦", + "JP": "日本", + "KE": "肯尼亚", + "KG": "吉尔吉斯斯坦", + "KH": "柬埔寨", + "KI": "基里巴斯", + "KM": "科摩罗", + "KN": "圣基茨和尼维斯", + "KP": "朝鲜民主主义人民共和国", + "KR": "大韩民国", + "KW": "科威特", + "KY": "开曼群岛", + "KZ": "哈萨克斯坦", + "LA": "老挝人民民主共和国", + "LB": "黎巴嫩", + "LC": "圣卢西亚", + "LI": "列支敦士登", + "LISBON": "WIPO", + "LK": "斯里兰卡", + "LP": "LATIPAT", + "LR": "利比里亚", + "LS": "莱索托", + "LT": "立陶宛", + "LU": "卢森堡", + "LV": "拉脱维亚", + "LY": "利比亚", + "MA": "摩洛哥", + "MC": "摩纳哥", + "MD": "摩尔多瓦共和国", + "ME": "黑山", + "MF": "圣马丁(法国部分)", + "MG": "马达加斯加", + "MH": "马绍尔群岛", + "MK": "北马其顿共和国", + "ML": "马里", + "MM": "缅甸", + "MN": "蒙古", + "MO": "澳门", + "MP": "北马里亚纳群岛", + "MQ": "马提尼克", + "MR": "毛里塔尼亚", + "MS": "蒙特塞拉特", + "MT": "马耳他", + "MU": "毛里求斯", + "MV": "马尔代夫", + "MW": "马拉维", + "MX": "墨西哥", + "MY": "马来西亚", + "MZ": "莫桑比克", + "NA": "纳米比亚", + "NAM": "北美洲", + "NC": "新喀里多尼亚", + "NE": "尼日尔", + "NF": "诺福克岛", + "NG": "尼日利亚", + "NI": "尼加拉瓜", + "NL": "荷兰", + "NO": "挪威", + "NP": "尼泊尔", + "NR": "瑙鲁", + "NU": "纽埃", + "NZ": "新西兰", + "OA": "非洲知识产权组织", + "OCE": "大洋洲", + "OM": "阿曼", + "PA": "巴拿马", + "PE": "秘鲁", + "PF": "法属波利尼西亚", + "PG": "巴布亚新几内亚", + "PH": "菲律宾", + "PK": "巴基斯坦", + "PL": "波兰", + "PM": "圣皮埃尔和密克隆", + "PN": "皮特凯恩", + "PR": "波多黎各", + "PS": "巴勒斯坦", + "PT": "葡萄牙", + "PW": "帕劳", + "PY": "巴拉圭", + "QA": "卡塔尔", + "QO": "没有ST.3代码的组织", + "QZ": "欧洲联盟", + "RE": "留尼汪", + "RO": "罗马尼亚", + "RS": "塞尔维亚", + "RU": "俄罗斯联邦", + "RW": "卢旺达", + "SA": "沙特阿拉伯", + "SAM": "南美洲", + "SB": "所罗门群岛", + "SC": "塞舌尔", + "SD": "苏丹", + "SE": "瑞典", + "SG": "新加坡", + "SH": "圣赫勒拿、阿森松和特里斯坦-达库尼亚", + "SI": "斯洛文尼亚", + "SIXTER": "WIPO", + "SJ": "斯瓦尔巴和扬马延", + "SK": "斯洛伐克", + "SL": "塞拉里昂", + "SM": "圣马力诺", + "SN": "塞内加尔", + "SO": "索马里", + "SR": "苏里南", + "SS": "南苏丹", + "ST": "圣多美和普林西比", + "SU": "苏联", + "SV": "萨尔瓦多", + "SX": "圣马丁(荷兰部分)", + "SY": "阿拉伯叙利亚共和国", + "SZ": "斯威士兰", + "TC": "特克斯和凯科斯群岛", + "TD": "乍得", + "TF": "法属南部领地", + "TG": "多哥", + "TH": "泰国", + "TJ": "塔吉克斯坦", + "TK": "托克劳", + "TL": "东帝汶", + "TM": "土库曼斯坦", + "TN": "突尼斯", + "TO": "汤加", + "TR": "土耳其", + "TT": "特立尼达和多巴哥", + "TV": "图瓦卢", + "TW": "台湾(中国的省)", + "TZ": "坦桑尼亚联合共和国", + "UA": "乌克兰", + "UG": "乌干达", + "UK": "UK", + "UM": "美国本土外小岛屿", + "US": "USA", + "UY": "乌拉圭", + "UZ": "乌兹别克斯坦", + "VA": "罗马教廷", + "VC": "圣文森特和格林纳丁斯", + "VE": "委内瑞拉玻利瓦尔共和国", + "VG": "英属维尔京群岛", + "VI": "美属维尔京群岛", + "VN": "越南", + "VU": "瓦努阿图", + "WF": "瓦利斯和富图纳", + "WHO": "世界卫生组织", + "WO": "WIPO", + "WS": "萨摩亚", + "XK": "科索沃共和国", + "XN": "北欧专利局", + "XX": "国际", + "XXX": "跨国和国际局", + "YD": "民主也门", + "YE": "也门", + "YT": "马约特岛", + "YU": "塞尔维亚和黑山", + "ZA": "南非", + "ZM": "赞比亚", + "ZW": "津巴布韦" + } + + check_value = [ + "瑞典", + "芬兰", + "丹麦", + "挪威", + "冰岛", + "法国", + "爱尔兰", + "荷兰", + "比利时", + "卢森堡", + "英国", + "摩纳哥", + "德国", + "波兰", + "捷克", + "斯洛伐克", + "匈牙利", + "奥地利", + "瑞士", + "列支敦士登", + "爱沙尼亚", + "拉脱维亚", + "立陶宛", + "俄罗斯", + "白俄罗斯", + "乌克兰", + "摩尔多瓦", + "西班牙", + "葡萄牙", + "意大利", + "希腊", + "斯洛文尼亚", + "克罗地亚", + "罗马尼亚", + "保加利亚", + "塞尔维亚", + "阿尔巴尼亚", + "黑山", + "马耳他", + "塞浦路斯", + "北马其顿", + "梵蒂冈", + "圣马力诺", + "安道尔", + "波黑", + "欧洲联盟" + ] + check_staus = ["已过期的","已结束"] + # 1. 读取 Excel 活动工作表 + wb = openpyxl.load_workbook(file_path, data_only=True) + active_sheet = wb.active + active_sheet_name = active_sheet.title + print(active_sheet_name) + + # 2. 将数据转换为列表嵌套字典 + rows = list(active_sheet.iter_rows(values_only=True)) + if rows: + columns = list(rows[0]) + data_rows = [] + for row in rows[1:]: + row_dict = {} + for idx, col in enumerate(columns): + row_dict[col] = row[idx] if idx < len(row) else None + data_rows.append(row_dict) + else: + columns = [] + data_rows = [] + + # 3. 确保必要列存在 + # required_cols = ["状态", "国家", "时间"] + # for col in required_cols: + # if col not in columns: + # columns.append(col) + # for row_dict in data_rows: + # row_dict[col] = "" + + # 4. 提取唯一品牌列表 + brand_ls = set() + for row in data_rows: + brand = row.get("品牌") + if brand: + brand_ls.add(brand) + + # 5. 初始化不符合品牌的数据列表 + faild_data = [] + # 初始化查询失败的品牌的数据列表 + query_faild_data = [] + + # 6. 对每个品牌进行处理 + brands = list(brand_ls) + total_brands = len(brands) + for idx, brand in enumerate(brands, start=1): + if check_cancelled and check_cancelled(): + raise TaskCancelledError() + + # 行级进度回调:当前第 idx 条 / total_brands + if progress_callback and total_brands > 0: + try: + progress_callback(idx, total_brands, extra={'current_brand': str(brand)}) + except Exception: + # 回调失败不影响主流程 + pass + + # Simple 策略下最多翻 3 页(0,30,60),直到命中 554 行判断 + max_pages = 3 if strategy == "Simple" else 1 + page = 0 + matched = False + + while page < max_pages and not matched: + try: + as_structure_dict = { + "_id": create_code_generator(), + "boolean": "AND", + "bricks": [ + { + "_id": create_code_generator(), + "key": "brandName", + "value": brand, + "strategy": strategy + } + ] + } + # 构造 asStructure 内部 JSON 对象 + as_structure_dict = as_structure_dict + # print(as_structure_dict) + + # 将内部对象转为 JSON 字符串(作为 asStructure 字段的值) + as_structure_str = json.dumps(as_structure_dict, ensure_ascii=False) + data = { + "sort": "score desc", + "rows": "30", + "asStructure": as_structure_str, + "fg": "_void_" + } + # 翻页:第二页 start=30,第三页 start=60(仅 Simple 策略) + if strategy == "Simple" and page > 0: + data["start"] = str(30 * page) + + # print("请求参数", as_structure_dict, "页码:", page) + hashsearch = get_guid() + enc_text = search(hashsearch, data) + print(f"【{hashsearch}】待解密-->", enc_text) + dec_text = decrypt_via_service(hashsearch, enc_text) + # print(type(dec_text)) + dec_text = str(dec_text) + # print(f"解密之后的结果-->", dec_text) + data = json.loads(dec_text) + except Exception as e: + # 仅第一页请求失败时记录为查询失败品牌 + if page == 0: + import traceback + traceback.print_exc() + safe_brand = brand.encode('gbk', errors='replace').decode('gbk') + print(f"品牌:{safe_brand},处理失败:{e}") + query_faild_data.append({"品牌": brand, "时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) + break + + # print(data) + + if data.get("response", {}).get("numFound", 0) > 0: + if strategy == "Simple": + new_brand_name = clean_text(brand) + _brand = new_brand_name.lower() + else: + _brand = brand + + docs = data["response"]["docs"] + for d in docs: + office = d.get("office") + status = d.get("status") + registrationDate = d.get("registrationDate") + status_name = status_info.get(status, status) + brand_name = d.get("brandName") + if isinstance(brand_name, list): + brand_name = "|".join(brand_name) + + if strategy == "Simple": + new_brand_name = clean_text(brand_name) + new_brand_name = new_brand_name.lower() + if _brand != new_brand_name: + continue + + check_office = d.get("designation") + + check_office.append(office) + for i in set(check_office): + office_name = country_info.get(i, i) + # print(office_name,office_name in check_value and status_name not in check_staus) + if office_name in check_value and status_name not in check_staus: + # 命中 554 行判断,记录并结束当前品牌后续翻页 + data_rows = [row for row in data_rows if row.get("品牌") != brand] + # print(office_name, office in ["EM","QZ"] and i not in ["EM","QZ"]) + if office in ["EM","QZ"] and i not in ["EM","QZ"]: + continue + + if strategy == "Simple": + faild_data.append({"品牌": brand, "国家": office_name, "状态": status_name, "原因":"|".join(d.get("brandName","")),"时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) + matched = True + else: + # print("添加",brand,office_name) + faild_data.append({"品牌": brand, "国家": office_name, "状态": status_name, "时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) + # break + if matched: + break + + # 如果 Simple 策略下本页未命中,则翻下一页;其他策略不翻页 + if not matched: + page += 1 + # else: + # # 更新主表数据 + # for row in data_rows: + # if row.get("品牌") == brand: + # row["状态"] = status_name + # row["国家"] = office_name + # row["时间"] = datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S") # 品牌匹配行 + # else: + # # 非匹配行:时间 = 当前国家值(模拟原 mask 逻辑) + # row["时间"] = "" + + # 7. 创建输出工作簿 + out_wb = Workbook() + out_wb.remove(out_wb.active) # 删除默认 sheet + + # 主工作表 + main_ws = out_wb.create_sheet(title=active_sheet_name) + main_ws.append(columns) + for row_dict in data_rows: + main_ws.append([row_dict.get(col, "") for col in columns]) + + # 不符合品牌工作表 + faild_ws = out_wb.create_sheet(title="不符合品牌") + if strategy == "Simple": + faild_columns = ["品牌", "国家", "状态","原因"] + else: + faild_columns = ["品牌", "国家", "状态"] + faild_ws.append(faild_columns) + for row_dict in faild_data: + # print("写入",row_dict) + faild_ws.append([row_dict.get(col, "") for col in faild_columns]) + + # 查询失败品牌工作表 + query_faild_ws = out_wb.create_sheet(title="查询失败品牌") + query_faild_columns = ["品牌", "时间"] + query_faild_ws.append(query_faild_columns) + for row_dict in query_faild_data: + query_faild_ws.append([row_dict.get(col, "") for col in query_faild_columns]) + + out_wb.save(output_path) + print("生成完成--->", output_path) + return output_path + + +if __name__ == '__main__': + file_path = r"D:\\私单交付\\maixiang_AI\\测试图片数据\\品牌采集测试\\brand_task_73\\源文件\\brand_task_75\\源文件\\3.10.xlsx" + res = single_file_handle(file_path,"结果.xlsx") + print(res) + + + + + + + + + + + diff --git a/brand_spider/web_dec.py b/brand_spider/web_dec.py new file mode 100644 index 0000000..7610c86 --- /dev/null +++ b/brand_spider/web_dec.py @@ -0,0 +1,126 @@ +""" +通过 pywebview 加载本地 HTML,执行 JS 实现解密与 GUID 生成。 +""" +import json +import threading +import traceback + +import webview + +# 内嵌 HTML:加载 CryptoJS 并定义 decryptWithHashSearches、guid +_DECRYPT_HTML = """ + + + + + + + + + + +""" + +_window = None +_window_ready = threading.Event() +_init_lock = threading.Lock() + + +def _ensure_window(): + """首次调用时在后台线程启动 pywebview 并等待页面加载完成。""" + global _window + with _init_lock: + if _window is not None: + return + _window = webview.create_window( + "", + html=_DECRYPT_HTML, + width=1, + height=1, + hidden=True, + ) + + def run(): + webview.start(debug=False) + + t = threading.Thread(target=run, daemon=True) + t.start() + _window.events.loaded.wait() + _window_ready.set() + _window_ready.wait() + + +def decrypt_via_service(hash_searches, ciphertext): + """ + 通过 pywebview 执行 JS decryptWithHashSearches 进行 AES-ECB 解密。 + + :param hash_searches: 对应 JS 的 hashSearches(密钥后缀,可为空串) + :param ciphertext: Base64 密文 + :return: 解密后的 UTF-8 字符串,失败返回空串 + """ + _ensure_window() + # 将参数安全注入 JS(避免注入与引号问题) + ciphertext_js = json.dumps(ciphertext) + hash_searches_js = json.dumps(hash_searches or "") + js = f"(function(){{ return decryptWithHashSearches({ciphertext_js}, {hash_searches_js}); }})();" + try: + result = _window.evaluate_js(js) + return result if result is not None else "" + except Exception as e: + raise RuntimeError(f"解密失败: {e}") from e + + +def get_guid(): + """通过 pywebview 执行 JS guid() 生成 UUID 格式字符串。""" + _ensure_window() + try: + result = _window.evaluate_js("(function(){ return guid(); })();") + if result is None: + raise RuntimeError("guid() 返回为空") + return result + except Exception as e: + raise RuntimeError(f"获取 GUID 失败: {e}") from e + + +# 使用示例 +if __name__ == "__main__": + # 参数含义:hash_searches 对应原 key,ciphertext 对应原 data + hash_searches = "a6efb809-b714-7efd-4b64-8c650b9030f0" + ciphertext = "SEsfpOGa8B+sWk0ncknTNh/HNHbGiEVi/RNKxBvyHmAE5VjHonPi202c6VicC/GKfA8mLsIC5mGEpSaH2DdCaEJKeOTWD9SBHHbgtyS1O60VqjgAaptYe9LivvWKc/BU8sZOqhxPMzGUHDcKUts7d0p+hCc80XCXyM2ZT80smM1twndcDfpGkLDk2kJbzl2bGzIc60sl9MzKWfZA2sE0ztFjQ9wD2uhn5LrwoN8NnpiPLNbviMMGtHh4N6Dc0xtPzkzgfkiuxBfWnN1SeM9XVgujHvAGea/dqUWIJqLo26fZIOEFJ0MYL4c8CGLYIeP/70cT2IqJdf+IPBWsCiP29zSyAiQd3yLNvd8+xBLky7lR4ng3MZizn/vhW/5BSg1FtVglbAmNbKgHOIbtpP197Lv+uahx2IpsSPqpy4j2O2VV25YzBk9JpJ4WGG/SvF3f2ZYKKepEQ+kGmBxAfG/5Z8DTNIwnOpXhyjFpJb+fUdPV6LTCK/yFc8g31bNFAJkLW7y/VjewjFravZ3VfQjNAHCvifnrIxGG22MZ3TLVnlbh6ye6vTnd+v9GzqXu+ISR7vQGL/ZSM/bJIjuVkP2XnNUPf+NFdt75gyDmTylFmmbpg7WHaBjinPcyVjeZ38+quJhT8yEk66BOjBM47mVdfU8JLK3ToghxQ54dKWGUbc9HRzYIAQ0rIBBExcOcPM4J9DpufvajmEygoDaws4CxOQDlFoFLwNBYorJsKzAoDe1Cu8oFtk4x190Vd+leRKq6DQSNJwEmyrVkWyFpiuywSMaixFlSqve0lRs50FclXfV7gkBAAvz/DJp90i9yCJdMaihP5ZCvbhKxFGMowkU+tx5Ptnxd9hq6tUxHmuiwDI1eyY8EyjB+3MbrC3H/GY39Z5Qhkj5tkSBm4oGA/h6YjeunBlfMU/QGP3hyZIYALh/YmBlZxAcglWwcqlISUhcx1L3Dbw6ZQbpAmYHmA421xIlRJkdJuPmGoopcoFEVdIO0Ov3uP2JL5ybvUgCBMnbwcYRK9qRmvn2b8jHKch55YXerHBDCEMHz88rfgNIEL8DIAp+wMuIyJ0VrV0BwAXioLYNvMcggBp06gghq2NykGiXRZ/dz8nffFqPVkDNjzcPSGrml9fpKdO8x/GannMNBsA+oHlO2/d4dyo+Q2dZCarQ5UB/N1p+UsTW90kvITKwCbZfV/YY2NRm7HrXq+wLS1vY4mChY82Ybc3cYQT653Q==" + + try: + decrypted_text = decrypt_via_service(hash_searches, ciphertext) + print("解密结果:", decrypted_text) + except Exception as e: + print("错误:", e) + + try: + g = get_guid() + print("GUID:", g) + except Exception as e: + print("GUID 错误:", e) diff --git a/config.py b/config.py new file mode 100644 index 0000000..56b7ef8 --- /dev/null +++ b/config.py @@ -0,0 +1,46 @@ +base_url = "https://api.coze.cn/v1" +coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5" +workflow_id = "7608812635877900322" +STITCH_WORKFLOW_ID = "7608813873483300907" + +import os +from dotenv import load_dotenv +load_dotenv() +_base_url = os.getenv("base_url","http://159.75.121.33:15124") + +# MySQL 配置 +mysql_host = os.getenv("mysql_host") +mysql_user = os.getenv("mysql_user") +mysql_password = os.getenv("mysql_password","WTFrb5y6hNLz6hNy") +mysql_database = os.getenv("mysql_database","aiimage") +proxy_url = os.getenv("proxy_url") +proxy_mode = int(os.getenv("proxy_mode",1)) + +client_name=os.getenv("client_name") + ".exe" + +cache_path = "./user_data" + +region = "cn-hangzhou" +endpoint = "oss-cn-hangzhou.aliyuncs.com" +bucket = "nanri-ai-images" +accessKeyId = "LTAI5tNpyvzMNz9f2dHarsm8" +accessKeySecret = "bQSZnFH455i8tzyOgeahJmUzwmhynz" +bucket_path = "nanri-image/" + +file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/" + + + +os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId +os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret +os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6" + + +debug = True +version = "1.0.3" +APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" } +os.environ['APP_VERSION'] = version +os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL + + + diff --git a/coze.py b/coze.py new file mode 100644 index 0000000..ea8e95a --- /dev/null +++ b/coze.py @@ -0,0 +1,92 @@ +import requests +import time + +from config import base_url, coze_token + + + + + +def upload_file(file_path,_coze_token =coze_token): + url = f"{base_url}/files/upload" + headers = { + "Authorization": f"Bearer {_coze_token}" + } + print(file_path) + with open(file_path, 'rb') as f: + files = { + 'file': f + } + # with open(f"{time.time()".replace(".","_")+".png","wb") as file: + # file.write(f.read()) + # allow_redirects=True 模拟 curl 的 --location,默认即为 True + response = requests.post(url, headers=headers, files=files, allow_redirects=True) + data = response.json() + print("上传图片->>",data) + return data + +def workflow_run(workflow_id, parameters,_coze_token = coze_token,is_async=True): + url = f"{base_url}/workflow/run" + headers = { + "Authorization": f"Bearer {_coze_token}", + "Content-Type": "application/json" + } + + # 请求体(JSON 格式) + payload = { + "parameters": parameters, + # "parameters": { + # "name": "包包", + # "ratio": "3:4", + # "menu": 3, + # "resolution": "2K", + # "count": 1, + # "desc": "1、一个中国美女手上挎着这个包,2、站在商场内", + # "language": "中文", + # "style": "极简高级" + # }, + "workflow_id": workflow_id, + "is_async" : is_async, + } + # 发送 POST 请求(使用 json 参数自动序列化并设置 Content-Type) + response = requests.post(url, headers=headers, json=payload) + data = response.json() + + print("图片生成参数->>",payload) + print("图片生成->>",data) + return data + + +def query_result(workflow_id, execute_id,_coze_token=coze_token): + url = f"{base_url}/workflows/{workflow_id}/run_histories/{execute_id}" + headers = { + "Authorization": f"Bearer {_coze_token}", + "Content-Type": "application/json" + } + response = requests.get(url, headers=headers) + data = response.json() + print(f"【{execute_id}】结果查询->>",data) + return data + +if __name__ == '__main__': + from config import workflow_id + + parameters = { + "images": [ + { + "file_id": "7612667079497613350", + + },{ + "file_id": "7612667042990768174", + },{ + "file_id": "7612667111332577332", + } + ] + } + # resp = workflow_run("7607680760402100258",parameters,_coze_token="sat_ZVNLR9Om54A3iMlJWAasHF9kbtDZnR3BjwjAxRwe8x7igEW446y5ROyVlW1UlpVX") + # print(resp) + + # resp = query_result("7607680760402100258","7612668958797447187",_coze_token="sat_ZVNLR9Om54A3iMlJWAasHF9kbtDZnR3BjwjAxRwe8x7igEW446y5ROyVlW1UlpVX") + # print(resp) + resp = upload_file("D:\私单交付\maixiang_AI\测试图片数据\IMG_2686.JPG",_coze_token="sat_ZVNLR9Om54A3iMlJWAasHF9kbtDZnR3BjwjAxRwe8x7igEW446y5ROyVlW1UlpVX") + print(resp) \ No newline at end of file diff --git a/generate_api.py b/generate_api.py new file mode 100644 index 0000000..ce92eff --- /dev/null +++ b/generate_api.py @@ -0,0 +1,261 @@ +""" +生成图片 API - 供 pywebview 前端调用 +流程:上传图片 -> workflow_run -> 轮询 query_result -> 解析返回图片 URL +""" + +import base64 +import json +import os +import tempfile +import time +import re +import traceback + +import requests + +from coze import upload_file, workflow_run, query_result +from config import workflow_id +from ali_oss import upload_data_urls as oss_upload_data_urls + + +def _data_url_to_temp_file(data_url: str, allow_video: bool = False) -> str: + """将 base64 data URL 转为临时文件路径""" + # data:image/png;base64,xxxx 或 data:video/mp4;base64,xxxx + if allow_video: + match = re.match(r'data:(?:image|video)/(\w+);base64,(.+)', data_url) + else: + match = re.match(r'data:image/(\w+);base64,(.+)', data_url) + if not match: + raise ValueError('无效的 data URL 格式') + mime = match.group(1).lower() + ext_map = {'png': 'png', 'webp': 'png', 'jpeg': 'jpg', 'jpg': 'jpg', 'mp4': 'mp4', 'webm': 'webm'} + ext = ext_map.get(mime, 'jpg') + data = base64.b64decode(match.group(2)) + fd, path = tempfile.mkstemp(suffix=f'.{ext}') + try: + os.write(fd, data) + finally: + os.close(fd) + return path + + +def _upload_video(video_data_url: str) -> str: + """上传视频,返回 file_id""" + if not video_data_url or not isinstance(video_data_url, str): + raise ValueError('无效的视频数据') + path = _data_url_to_temp_file(video_data_url, allow_video=True) + try: + resp = upload_file(path) + if resp.get('code') == 0 and resp.get('data', {}).get('id'): + return resp['data']['id'] + raise RuntimeError(f'视频上传失败: {resp.get("msg", resp)}') + finally: + try: + os.unlink(path) + except Exception: + pass + + +def _upload_images(image_data_urls: list) -> list: + """上传多张图片,返回 file_id 列表""" + file_ids = [] + temp_paths = [] + try: + for data_url in (image_data_urls or []): + if not data_url or not isinstance(data_url, str): + continue + if data_url.startswith("http"): + fd, path = tempfile.mkstemp(suffix=f'.png') + data_content = requests.get(data_url).content + os.write(fd, data_content) + else: + path = _data_url_to_temp_file(data_url) + temp_paths.append(path) + resp = upload_file(path) + if resp.get('code') == 0 and resp.get('data', {}).get('id'): + file_ids.append(resp['data']['id']) + else: + raise RuntimeError(f'上传失败: {resp.get("msg", resp)}') + return file_ids + finally: + for p in temp_paths: + try: + os.unlink(p) + except Exception: + pass + + +def _parse_output(output_str: str) -> list: + """解析 query_result 中的 output,提取图片 URL 列表""" + try: + outer = json.loads(output_str) + inner_str = outer.get('Output', '{}') + inner = json.loads(inner_str) + data_str = inner.get('data', '[]') + data = json.loads(data_str) + urls = data.get("images") + long_image_url = data.get("merged_image_url") + return urls if isinstance(urls, list) else [],long_image_url + except Exception: + return [],None + + +def _parse_output_text(output_str: str) -> list: + """解析 query_result 中的 output,提取图片 URL 列表""" + try: + outer = json.loads(output_str) + inner_str = outer.get('Output', '{}') + inner = json.loads(inner_str) + data_str = inner.get('data', '') + if isinstance(data_str,str): + data = json.loads(data_str).get("reverse_prompt") + return [data] + pattern = r'【?图像 \d+】.*?(?=【?图像 \d+】|$)' + segments = re.findall(pattern, data_str, re.DOTALL) + return segments + except Exception: + return "" + + +def _poll_until_done(wf_id: str, execute_id: str, interval: float =10, timeout: int = 600*2, is_text: bool = False) -> list: + """轮询直到成功或超时,返回图片 URL 列表""" + start = time.time() + while time.time() - start < timeout: + resp = query_result(wf_id, execute_id) + if resp.get('code') != 0: + raise RuntimeError(f'查询失败: {resp.get("msg", resp)}') + items = resp.get('data') or [] + if not items: + time.sleep(interval) + continue + item = items[0] + status = item.get('execute_status', '') + if status == 'Success': + output = item.get('output', '{}') + if is_text: + return _parse_output_text(output) + return _parse_output(output) + if status and status not in ('Running', 'Pending', ''): + raise RuntimeError(f'执行失败: {status}') + time.sleep(interval) + raise RuntimeError('生成超时') + + +def _res_to_2k(res: str) -> str: + """统一分辨率为 2K/4K""" + r = (res or '2k').strip().upper() + return '4K' if r == '4K' else '2K' + + +def generate(params: dict) -> dict: + """ + 生成图片 + params: { + menu: int, # 1=图片反推 2=图片编辑 3=随机海报 4=克隆海报 5=服饰穿搭 + prompt: str, # 用户自定义指令 (menu=1 必填) + ref_images: list, # base64 data URLs - 参考图 + video: str, # base64 data URL - 视频 (menu=1 可选,仅支持1个) + model_images: list, # base64 data URLs - 多模特图 (menu!=1,最多5张) + name: str, desc: str, ratio: str, resolution: str, count: int, + language: str, style: str, batch_prompt: list, brand_name: str, + Ingredients: str, activity: str, mode: str, texts: list, + proc_images: list, layout_image: str, + } + 返回: { success: bool, urls: list, prompts: list, error: str } + """ + original_urls = [] + api_key = (params.get('api_key') or '').strip() + try: + menu = int(params.get('menu', 2)) + ref_data = params.get('ref_images') or [] + proc_data = params.get('proc_images') or [] + layout_data = params.get('layout_image') + video_data = params.get('video') + model_data = params.get('model_images') or [] + + # menu 1: 反推词,简化参数 + if menu == 1: + base_params = {'menu': 1, 'prompt': str(params.get('prompt', '')).strip() or ''} + + ref_ids = _upload_images(ref_data) if ref_data else [] + if ref_ids: + base_params['ref_images'] = [{'file_id': fid} for fid in ref_ids] + if video_data: + try: + vid = _upload_video(video_data) + base_params['video'] = {'file_id': vid} + except Exception as ve: + return {'success': False, 'urls': [], 'prompts': [], 'error': f'视频上传失败: {ve}'} + base_params = {k: v for k, v in base_params.items() if v is not None and v != ''} + base_params["api_key"] = api_key + resp = workflow_run(workflow_id, base_params) + if resp.get('code') != 0: + return {'success': False, 'urls': [], 'prompts': [], 'error': resp.get('msg', str(resp))} + execute_id = resp.get('execute_id') + if not execute_id: + return {'success': False, 'urls': [], 'prompts': [], 'error': '未返回 execute_id'} + prompts = _poll_until_done(workflow_id, str(execute_id), is_text=True) + # menu 1 返回提示词列表,统一转为字符串 + # prompts = [str(x) for x in result_list] if result_list else [] + # prompts = [prompts] + return {'success': True, 'urls': [], 'prompts': prompts, 'error': ''} + + # menu != 1: 原有逻辑 + all_ref = list(ref_data) + if layout_data: + all_ref = [layout_data] + list(ref_data) + all_originals = list(all_ref) + list(proc_data) + list(model_data) + + if all_originals: + try: + original_urls = oss_upload_data_urls(all_originals, prefix="originals") + except Exception: + traceback.print_exc() + pass + + ref_ids = _upload_images(all_ref) if all_ref else [] + proc_ids = _upload_images(proc_data) if proc_data else [] + model_ids = _upload_images(model_data) if model_data else [] + + res = _res_to_2k(params.get('resolution', '2K')) + + base_params = { + 'name': str(params.get('name', '')).strip(), + 'ratio': str(params.get('ratio', '')).strip(), + 'menu': menu, + 'resolution': res, + 'count': int(params.get('count', 1)), + 'desc': str(params.get('desc', '')).strip(), + 'language': str(params.get('language', '中文')).strip() or '中文', + 'style': str(params.get('style', '')).strip(), + 'prompt': str(params.get('prompt', '')).strip(), + 'batch_prompt': params.get('batch_prompt') or [], + 'brand_name': str(params.get('brand_name', '')).strip(), + 'Ingredients': str(params.get('Ingredients', '')).strip(), + 'activity': str(params.get('activity', '')).strip(), + 'mode': str(params.get('mode', '1')).strip() or '1', + 'texts': params.get('text') or [], + "api_key" : api_key + } + + if ref_ids: + base_params['ref_images'] = [{'file_id': fid} for fid in ref_ids] + if proc_ids: + base_params['proc_images'] = [{'file_id': fid} for fid in proc_ids] + if model_ids: + base_params['model_images'] = [{'file_id': fid} for fid in model_ids] + base_params = {k: v for k, v in base_params.items() if v is not None and v != ''} + + resp = workflow_run(workflow_id, base_params) + if resp.get('code') != 0: + return {'success': False, 'urls': [], 'error': resp.get('msg', str(resp))} + execute_id = resp.get('execute_id') + if not execute_id: + return {'success': False, 'urls': [], 'error': '未返回 execute_id'} + + urls,long_image_url = _poll_until_done(workflow_id, str(execute_id)) + return {'success': True, 'urls': urls, 'original_urls': original_urls, 'error': '',"long_image_url":long_image_url} + except Exception as e: + return {'success': False, 'urls': [], 'prompts': [], 'original_urls': [], 'error': str(e),"long_image_url":""} + + diff --git a/html_crypto.py b/html_crypto.py new file mode 100644 index 0000000..cb089d9 --- /dev/null +++ b/html_crypto.py @@ -0,0 +1,68 @@ +""" +HTML 模板加密/解密模块 +使用 AES (Fernet) 对 HTML 资源进行加密存储,运行时解密后渲染 +""" + +import base64 +import os + +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +def _get_fernet_key() -> bytes: + """从环境变量 HTML_ENCRYPT_KEY 派生 Fernet 密钥,若未设置则使用默认开发密钥""" + raw = os.environ.get( + "HTML_ENCRYPT_KEY", + "maixiang_html_encrypt_default_key_change_in_production", + ) + # 使用 PBKDF2 派生 32 字节密钥,再转为 Fernet 所需的 base64url + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=b"maixiang_html_salt", + iterations=100000, + ) + key_bytes = kdf.derive(raw.encode("utf-8")) + return base64.urlsafe_b64encode(key_bytes) + + +def encrypt(plain_data: bytes) -> bytes: + """加密原始数据,返回密文(bytes)""" + key = _get_fernet_key() + f = Fernet(key) + return f.encrypt(plain_data) + + +def decrypt(encrypted_data: bytes) -> bytes: + """解密数据,返回明文(bytes)""" + key = _get_fernet_key() + f = Fernet(key) + return f.decrypt(encrypted_data) + + +def encrypt_file(path: str) -> None: + """就地加密文件:读取 UTF-8 内容,加密后写回同一路径""" + with open(path, "rb") as f: + plain = f.read() + encrypted = encrypt(plain) + with open(path, "wb") as f: + f.write(encrypted) + + +def decrypt_file(path: str) -> None: + """就地解密文件:读取密文,解密后以 UTF-8 写回同一路径""" + with open(path, "rb") as f: + encrypted = f.read() + plain = decrypt(encrypted) + with open(path, "wb") as f: + f.write(plain) + + +def is_encrypted(data: bytes) -> bool: + """简单启发式:Fernet 密文以 b'gAAAAA' 开头(base64 编码后)""" + try: + return data[:7] == b"gAAAAAB" + except Exception: + return False diff --git a/main.py b/main.py new file mode 100644 index 0000000..eb64fdf --- /dev/null +++ b/main.py @@ -0,0 +1,344 @@ +""" +基于 pywebview 实现的跨平台 UI,需先登录方可使用 +""" +from config import cache_path,debug,version +import datetime +import json +import sys +import os +os.makedirs(cache_path,exist_ok=True) +if not debug: + today = datetime.datetime.now().strftime("%Y_%m_%d") + sys.stdout = open(os.path.join(cache_path,f'{today}.log'), 'a',buffering=1) + sys.stderr = sys.stdout + +import webview +import threading +import time + + +with open("version.txt","w",encoding="utf-8") as file: + file.write(json.dumps({"version":version})) + + +# 获取当前脚本所在目录 +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +APP_URL = "http://127.0.0.1:5123" +PORT = 5123 + + +def generate_images(params): + """供前端调用的生成图片接口""" + from generate_api import generate + return generate(params) + + +class WindowAPI: + """暴露给 JavaScript 的窗口控制 API""" + + def __init__(self, window): + self._window = window + self._is_maximized = False + window.events.maximized += lambda _: setattr(self, '_is_maximized', True) + window.events.restored += lambda _: setattr(self, '_is_maximized', False) + + def close(self): + sys.exit(1) + self._window.destroy() + + def minimize(self): + self._window.minimize() + + def maximize(self): + self._window.maximize() + + def toggle_maximize(self): + if self._is_maximized: + self._window.restore() + else: + self._window.maximize() + + def save_image(self, url_or_data, filename='image.png'): + """弹窗选择保存位置并下载图片。url_or_data 可为 http(s) 链接或 data: base64""" + import base64 + import re + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=filename, + file_types=('图片文件 (*.png;*.jpg;*.jpeg)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + if url_or_data.startswith('data:'): + match = re.match(r'data:image/\w+;base64,(.+)', url_or_data) + if not match: + print(url_or_data) + print('无效的 data URL') + return {'success': False, 'error': '无效的 data URL'} + data = base64.b64decode(match.group(1)) + else: + import requests + resp = requests.get(url_or_data, timeout=30) + resp.raise_for_status() + data = resp.content + with open(path, 'wb') as f: + f.write(data) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + def save_image_to_folder(self, url_or_data, dir_path, filename='image.png'): + """将图片保存到指定文件夹。url_or_data 可为 http(s) 链接或 data: base64""" + import base64 + import re + path = os.path.join(dir_path, filename) + try: + if url_or_data.startswith('data:'): + match = re.match(r'data:image/\w+;base64,(.+)', url_or_data) + if not match: + return {'success': False, 'error': '无效的 data URL'} + data = base64.b64decode(match.group(1)) + else: + import requests + resp = requests.get(url_or_data, timeout=30) + resp.raise_for_status() + data = resp.content + os.makedirs(dir_path, exist_ok=True) + with open(path, 'wb') as f: + f.write(data) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + def select_folder(self): + # 此方法会在 JavaScript 中被调用,返回选择的文件夹路径 + result = webview.windows[0].create_file_dialog( + webview.FOLDER_DIALOG, # 指定为文件夹对话框 + allow_multiple=False, # 是否允许多选 + directory='' # 初始目录 + ) + # create_file_dialog 返回的是列表(多选时)或 None + return result[0] if result else '' + + def select_brand_xlsx_files(self): + """品牌爬虫:选择 .xlsx 文件,允许多选,返回路径列表""" + result = webview.windows[0].create_file_dialog( + webview.OPEN_DIALOG, + allow_multiple=True, + file_types=('Excel 文件 (*.xlsx)', '所有文件 (*.*)') + ) + print("选择的excel 文件",result) + if not result: + return [] + return result if isinstance(result, list) or isinstance(result, tuple) else [result] + + def select_brand_folder(self): + """品牌爬虫:选择文件夹,返回文件夹路径(前端再请求后端展开该目录下所有 xlsx)""" + result = webview.windows[0].create_file_dialog( + webview.FOLDER_DIALOG, + allow_multiple=False, + directory='' + ) + return result[0] if result else '' + + def save_file_from_url(self, url, default_filename='download.zip'): + """弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。""" + if not url or not url.strip(): + return {'success': False, 'error': '下载地址为空'} + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=default_filename, + file_types=('ZIP 压缩包 (*.zip)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + import requests + resp = requests.get(url.strip(), timeout=120, stream=True) + resp.raise_for_status() + with open(path, 'wb') as f: + for chunk in resp.iter_content(chunk_size=65536): + if chunk: + f.write(chunk) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + def save_template_xlsx(self): + """弹窗选择保存位置,将品牌文档格式模板 xlsx 保存到用户选择的位置。""" + import shutil + template_name = '品牌文档格式_模板.xlsx' + template_path = os.path.join(BASE_DIR, 'static', template_name) + if not os.path.isfile(template_path): + return {'success': False, 'error': '模板文件不存在'} + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=template_name, + file_types=('Excel 文件 (*.xlsx)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + shutil.copy2(template_path, path) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + + def save_template_zip(self): + """弹窗选择保存位置,将品牌文档格式模板 xlsx 保存到用户选择的位置。""" + import shutil + template_name = '模板2-以文件夹方式上传.zip' + template_path = os.path.join(BASE_DIR, 'static', template_name) + if not os.path.isfile(template_path): + return {'success': False, 'error': '模板文件不存在'} + result = self._window.create_file_dialog( + webview.SAVE_DIALOG, + save_filename=template_name, + file_types=('ZIP 文件 (*.zip)', '所有文件 (*.*)') + ) + if not result: + return {'success': False, 'error': '用户取消'} + path = result[0] if isinstance(result, (list, tuple)) else result + try: + shutil.copy2(template_path, path) + return {'success': True, 'path': path} + except Exception as e: + return {'success': False, 'error': str(e)} + + +def _select_folder_win32(): + """Windows: 用 ctypes 调用系统文件夹选择对话框,打包成 exe 后也能正常弹窗(不依赖 tkinter)""" + try: + import ctypes + from ctypes import wintypes + BIF_RETURNONLYFSDIRS = 0x00000001 + BIF_NEWDIALOGSTYLE = 0x00000040 + MAX_PATH = 260 + shell32 = ctypes.windll.shell32 # type: ignore[attr-defined] + # BROWSEINFOW + class BROWSEINFOW(ctypes.Structure): + _fields_ = [ + ("hwndOwner", wintypes.HWND), + ("pidlRoot", ctypes.c_void_p), + ("pszDisplayName", ctypes.c_wchar_p), + ("lpszTitle", ctypes.c_wchar_p), + ("ulFlags", wintypes.UINT), + ("lpfn", ctypes.c_void_p), + ("lParam", wintypes.LPARAM), + ("iImage", ctypes.c_int), + ] + display_name_buf = ctypes.create_unicode_buffer(MAX_PATH) + bi = BROWSEINFOW() + bi.hwndOwner = 0 + bi.pidlRoot = 0 + bi.pszDisplayName = ctypes.cast(display_name_buf, ctypes.c_wchar_p) + bi.lpszTitle = "选择保存图片的文件夹" + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE + bi.lpfn = 0 + bi.lParam = 0 + bi.iImage = 0 + pidl = shell32.SHBrowseForFolderW(ctypes.byref(bi)) + if not pidl: + return '' + path_buf = ctypes.create_unicode_buffer(MAX_PATH) + if not shell32.SHGetPathFromIDListW(pidl, path_buf): + return '' + ole32 = ctypes.windll.ole32 # type: ignore[attr-defined] + ole32.CoTaskMemFree(pidl) + return path_buf.value or '' + except Exception as e: + print("Windows 文件夹选择失败:", e) + return '' + + +# def _select_folder_tk(): +# """使用 tkinter 选择文件夹(在非 Windows 或作为后备)""" +# try: +# +# root = tkinter.Tk() +# root.withdraw() +# root.attributes('-topmost', True) +# path = filedialog.askdirectory(title='选择保存图片的文件夹') +# try: +# root.destroy() +# except Exception: +# pass +# return path if path else '' +# except Exception as e: +# print("tkinter 选择文件夹失败:", e) +# return '' + + +def _select_folder_tk(): + import tkinter as tk + from tkinter import filedialog + + root = tk.Tk() + root.withdraw() + root.attributes('-topmost', True) + + # 使用 after 确保 askdirectory 在主循环启动后执行 + def ask(): + nonlocal path + path = filedialog.askdirectory(title='选择保存图片的文件夹') + root.quit() # 关闭主循环 + + path = '' + root.after(100, ask) + root.mainloop() # 启动主循环,直到 root.quit() 被调用 + try: + root.destroy() + except: + pass + return path + + +def select_folder(): + """弹窗选择文件夹,返回路径(空字符串表示取消)。Windows 打包 exe 时优先用系统 API 避免 tkinter 不弹窗。""" + # if sys.platform == 'win32': + # path = _select_folder_win32() + # if path: + # return path + return _select_folder_tk() + + +def start_flask(): + from app import run_app + run_app(host='127.0.0.1', port=PORT) + + +def main(): + if not os.path.exists(cache_path): + os.makedirs(cache_path,exist_ok=True) + t = threading.Thread(target=start_flask, daemon=True) + t.start() + # 等待 Flask 启动 + time.sleep(2) + + window = webview.create_window( + title="南日AI", + url=APP_URL, + width=1400, + height=900, + resizable=True, + min_size=(1200, 700), + background_color="#1a1a1a", + frameless=False, + easy_drag=True + ) + api = WindowAPI(window) + window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip) + webview.start( + debug=True, + storage_path=cache_path, + private_mode=False + ) + + +if __name__ == "__main__": + main() diff --git a/static/bg.jpg b/static/bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de6e38d8f381a8c2e06f259aab20665bda03e0ed GIT binary patch literal 164259 zcmbTec|4Ts`v?Av-9%BNY-NixLu4|_QVbf**k(aB=@d#vMSCeZ8;io}Z6?K1Vbd z?sRtqheHq?{D=H}jJP2hlhh`w;WZ|!PoAo&F-@DOt37?XHrd2mN=|UO`bwSw$5;2@a^#K;&@p@^T9D zii!#faP)5Y98u6zoN7dNQJTgIRyK|z%u7f=p*$)A z{Xry|Q*7riu(Nk?TTFBJ@T4<1{sDnYmM-Ilu3i(iHk=m`y?M*lZQFOm?A@1`wEw_C zG$S)BJLgDl9>1_iC=wT!l$M=4FR8q6vFg(0+MBneb@jI!?zFVFwRb#x^mpfr?w7A# z_q=)A+xPj)z~Io=;cwqZuyNrK`H5k{^TfdZ&$u*UTyhEu@(Rk>xNvgY;32Q6plC!^ zn(D$*4vv~;JTF0o;F^Bo>9Z*>Oh02ola1{E-N5$z{~Fmp1N(Pe zorsz|4i-;d6LCh~^pe`L-|FwtWpKTjIGQCvg~25XP7dWDiYXDav{m=W~wR*pW;bTG=#vSA(jM|w;W0oC<+Tv zgjk3w5hN_x!8p1XO>SyVMov5)JN~~9Qn)~&5G5kN9@zaV!MWsQ2f=_QKFERdi3;~D zE`%2hQn*+SyCGA8g!$_Q+ze8f%t8?u2vHD>l4%6w_aQhM0jB5wJiKU@@O>6_56TT< zk|l7TdkG;&C`@aZn~57Fg-hjR6N6n4?(qmABv+BZ^27$H%*Mt}v(y$SNU@t>H)E56 zjdfzY2#Kd?DVq{(TK@Ae{5AH3e~-rI6z&!;TRUw1{(UIIq+k<*%_WSCM!;p#2pr1P zWOhrFd^jwkD$R;0P$(kwF%SlerXx@+gejgu2|G%YpXtSqv{X3UXvLFA5J|2`Bt;ew zLh|8+RYep@NIafQ_u?boY)-F=AT(SgHAM(n?_kZR<|MPbWh;+#vA~va#Nv=l0+Pk< zPM(--*+QFIVsFXj045r4zmEjVUYH?Uri6pPjWL);kRWgtF0!6P8r;W}hruYwrsKcg zWqF}wm`$1S5vqPoKiQoWt^rqt^$voA)v@`H$1{Sk36^BZu8Cb&_V{00ko;%0F{hBN zUe3Q(rN%;on)ynY_25u0xCl18vIi{v)FYT}|9eMr_+MY2uma3Frh>_Ea1u#X5g{J? z8kY$E^2>Y?kyI`xei|VQRs6va?P%!tJ>M^Y(KRHQ}2lT_d{b;O+}cbsP3B-czWl1-5s z5zdQbvAn4Ju=6Aw^l~Q(To_X*Sxor>hPn+!(4sLy_EfiHf?IRWx|8DXvCvo&RiG4- zsb$rPZ&Fw)e9V4dM8HiaN*AKuY+RK{BoruhgRP)2lT|{gHB1zM2TUqhrj&qmPJqI% zfn%1WohvHLnXoL(bQJ&bUP}Vg5c>${<*DdMi1&trHxiIqlF)x2Thm+RvpeaVP-=*Q zQ4P!&73K?22D40<7@-kSRG3@W(u{Hu7w{t$*+R+|GzVKLBG}M`nW`~lP{j+!Nq)V9 z$RKQ=R2vp`8X*Lr1-mq?0lWeDNCvYqBT8Y}zyZL?LymxVE6Lu-1sqb)o>+4(I8irT zouOz&lmnzhw-Tj>aJqcdn@I*6fO~h0?<74cOi-6<7Xw7WMF6Fc^+c&NEQK1p*$ocX zC0le$09iF+5tzCfJ0vF=P!_XC9DG5Bzt~*>?0Wu29e_!&F{S2T4uGMY%!+`ZeFTaCp_92X47^GGq;e7U_kq?k+@asu5ghZ&7eBwP4&XXh4 z+2>lx;EnloZ*cA`1e?n}ITn%*MIMEZLc` zk4ergBD`av3b4|!9AKV~|MQ$HCX+}}?nDg;o>DnAHm6huRivn24lR+{JKW(ffb{+C zs|ks};=w<__U}O$-u-L-*j@b(n5uy5VA1JUV1Zc$OGnH|RFT|qMV~rr{MOEmgy*E7 zIUW~MD(9UD!4E zu2C5cImS89SFqFNt}2$NPNM0E1d5G>tit#igjPS8_*xjbiqJS-XX1Lv*qA4RjUE<} ztS1aY2PHiMxR+TFk_8x=E4D7oNoHc`11l|I8c8JV792nzETsfulpGi)DIB|zy*`ar z;kdS3fi4$ObT|q@mrO^u5RiK;UrHy&Y9NaL6B-B@7YRXEK>+c_PX61PFkFEs3eW+# z2iS{UM#d~;mK2gKn?#urfn8<&vL=ih0DNI#`=0HWC*of zq&OsPAK^I>EY0F@Esky8G+ihs%1ZQHmUZUM%<#wcjFx79+vb!yR@)wP$I%&*w1j(W zKU9XkpYv{AgQ%`|N0=SZ9umJ2t|{ z5;cf64iIxlQNB{EP9%&}(+nnor$+FSIr|Bp8+$O%08^jJtz#tOB*)Ox# z$O_3hLQsPtc%eZ|BbXe3?f(Q*7@iBz8X=YmA%d0`{6;~DE)PIZC}Sk16Bz}-3C0R= zB+?@pX8~x(L6C-XVgcC^!jKG@5C#_g2_Zn2;nQFAAHInNZoj(JI*g1?`;2j^iDUsH zCj_dAb7K<+?gZ;7#~$562{?)yee)AJ73pSrMGJzt-3|1Ea&(nq^b+`WER zdy#U?-z!QI2*I>q2eha8V8P_%y<@BQzpW&-yx;%n>_5)q2yGF zWBIXc%2vz2ZO@$>D_l52eiis75`Xkxt0NR}A9bvLa71H$Ug)x^$B|Qf#+^5|OST3EvuqjddVwc2 zbA`pNKE}6-fKtiqiUAa2A`U_H2dkL~dqBCs!B}1s7;6X&OAHPP3;zrKv6aOEM+EXI z7m@pn3T*gh=tE5*fh{T2sOayP+3+0!TgsylCcm}18bj?tNa2C zH~y{&BoA_UAsV7js^QQUvN8JuQ-$ykupyT=g-|6%w+mG68MemWQ};uOa^)^)Vv+Ib z1(e$A^ZZq8sdlL_wW{QjAT&ybGxNb|vas-~0D(b*R6*#FsIeF%63uXyq7_NdngCOf zTMSHIZXU6e?0&Dwcph(o{}nRpTz78GBD?t!=0R^n2;Gadhj*hyqmN9#d<4cSlsX(C z;B*MtMuZk3E{pTE1-KJk24}YlQBpj_78RmEmP!Hc(Y@$YMWT>Um~hC8rY$rSHELs= zCk*o+$Y#4CWs1}mHNpyDK@|rYgQqN+R5v9F#W+u1ThfHrVlFljmM2`16fT0(V6jq; zg)uLs<_Z3lPebNsU{6?}?Ig8fh>DTyPA>QxB1;4yBSNY$OOQb+2pKXj&j(c8CtcoJ z|FK~FQe~i&`|H&oXXiaCQNBYW`xO% zH<8vRPs<}k1%jSJRGxArw70)^MIYUyk-tsFXh)FoFtY@|NuHr(cXMD-NLVM?x6w!}9|iUZEJ+&+xW9NSSjBHPPb1U-Ar&eGSeg>* zEQxU8nvEjDo?|2xmakkIL>P#@@FoK9kRem=BQ4`;l|Jtgs!Y+EH6FopH@{+KFGk+j zs`j&W*Wp*qLx~@~6^t_p19VhreloBEP|Sc~6PWUemXN5VDcW(OeC6WxNzXYn&%(6H znyA}MPa!|HiHB3_+EOA!2FrIk>xR~-bmtN#yA+DH-#MOjPZwb@Hg@r9=PxQfu&Nlp zE$3z><-^Ppq|r|IaF#BTNy*A9q9BrxWB@1BmDHAnn@JF|&`^kjjNf7mI0Y8)%)p@p za>>nJp72CNtav<$ph+I0G!xQhn3oX-vIfHL1(_2H(H3-UrG zi~!>&&Ku?B0p1O$F~M#y%~2LIf6q0J4*8W#d&Euv;smCNg-jfSg;hwZ;ZrM84bw;v zQ0+YKWB~rTL>m66ldyqu=H>~+ASTbz*z8^M*CxZ&ekvvc_^O)c~L3@_#x&66zW zw0-73I`h&m_1PaoWiuQyf|j2nteHJr`u?Nw9;wz*E1NH=6;X}z50}u5hiv~|^3j_0 zA%ef?{j-s(ACjS;NPk;{sQ77B+Vz*Hl=ETTdnMZ+s>4gKH~EK+J^nzA)8|XLwQU=x znT#v&_6J-r*u3C*RYid9qx}oIPJg|7ylKkltLCp)4u68bZO=Lz{{A!d{pt@HvDt;9 zSF8Ktwx@hNc7mf)=NRBWn{>l&QQ6zJ(BJ{1ABi^Pp!ttpuBz|6h_c>B2OODy@3fkw zM~GJNLmd1oZ%M~V3vX`PHHAq7+x zu%H}Bxv|C5W_i&JWh;$EF<9&0k|GO}5NMbpDRXL!g~?nNbZ5*FAp}F9lGnpL7u>6h z-Fg;|kW>NuBVfR691nP$Oc0vTUSxR@&|`3>Uyq5bn*f=nyfMl!kKazYB$oGWl%J)( z$j4QEp{{}t-`u+G6AyXeE5>h?$>dbRYf39spgN_|d$mZJAnb{^u$H>}a5p!)hcsGS zY@t;32+0HVi#_6b^F{Cd)(9=6h8VGh7?1e|NEZqeJS66l=Om<^&`qKiCkhp)oEnh@ z$aDxtY6xkS_6y%e3M7ipTN6s8c1#6!e1zP-!SXeebOR1dmIUEHOZx*ejL`X^B70xr zUy7Q0e^3^ktLjaSiQ#XLSTYlxy3|fbznyNfef*R0#l5dKmz*-1I+yTaI$4>)4P+m_ zvs0H*NCW-}9268OH(!|YJ2Iw=O(o|4Sd_zpmnDJ`gMX4{8EMYC1Zc|SMf->d@mmN$ z7WT12V3BMHAq5Z?)d1)qT4NDaCajp6I$*jmwt(35b5|{h1bK_94*Z=Lj!4ab)TF6m z3z(Cj20FuTBgp163EeV)0B?rG8q#a$_ zlNfD0h2yI?gT7+6`%Q(@Q9h+2gX*}_lCyz%C$n<_qr6?gLJ^P?nmrLt;{0PI!hSS6 z!nCw-^UA!;d+YBu#oDS9mR`7!^QMQY5PCAD7QLc>VS2tz^xUxeI~McTm9Fg${O++> zpZJ4xqI|(5K51#o(ZIZK)U&C<0mru;`FOi=$abZ@=mf`8|Iv?TA3~wTv*F(MVtF5_ zmzs}7A}cdsoz8qGBHsgFG`r&}*;Kg9`IDJ&525nV9QUZZ0h7uzEn?gqrf?rDBS)6n zbR9huU#+p3@b#}K|M&O9xEG%ADnm(E65hsUAAVD@?)d(<>t;S!DGH(=)TGq%e5^Z? z8gSy47F|4+>L-~3x(mqw#}nETXeAP?Mk4c1ENA&AfziN(0Sv$~XR4KfJme@CmyMTI z5vnjB#Zs34c=WH_2eb((wyeN!t+2EZTYuDT8W2oaWm$q1lAww)B`gDLlkw8@TnUR7 zj!|9(VlgMYQC?qtq3yin7P(bzmr4>qvpE>;#ZT7VNQvKKX$azt0Kz6oC}?5IO_nq{ zMQrCXuP>4}Uh}==F~{LYTOu!EMNUy~Alg&7S*#Fpj8rAHV{1HzdW=Infs=^&-Xj5C z5p+RLypZsMDwRuSqg*P&hMS_|Njwry4a;!TeeBfQLeU%>_8qtVa>74phCyih61g6Y5ZPz&}_&F>ZuDg$?}D9=)M%H|y?M@dr8h0We> z;TTuMsHvGOE@FwqJDJh9{iYnUe1kS2`C$eLR6Q(h5)CSHGKMgTK~l71;x1%HI^@Kg zJa%tovzrvQg9-%H1xxaKilP?b8?`5-@PW6k6q$*P|S$6&N_Uc2Uk&HiosN3NeJ zONVc#_AmKZ_j1Nd?>(l7jn#6>AL03S;r5ht_YFhfFgjXA(Ym>JS8eI{&kb;SqT8`| z-^semkxRe#&EsW<1dmq5JsGc5WbM%d5dpO0P}$J5A>jd;%Zn<-ik~k$t>p532$H2% zb{wfSr{fU*W#S?pQhFjK??Tz@R2P>yo5_^JTJH3U1BQoNrY;?5=jS|qVp-S~b!=CC z%lXsmlN&3yC&yN1?z(?F@Eo`y+3tn1OF#4O@U&jmRFz^ztmpnw+p<8Ln2gS!NPTU4 zYe&g4c+nDTyR+K6A>=T)qSgcMqnT5hI&6lEH4f^Cb$5t$&u=z0wKO%vGq|K=4D(16 zMAI6e6)Pgruw)bXEkG(}guexai2{{uwSQqShM9_Xcq-5#9H1i3Pc98BER295}~XHbOXnG7qszbz%zL{)x0$*$0dW3u{U=nHj=i2UlYC2N))w3$)FO?h`;Ldgn?P0ns>bqdhj((cwkBPSE zDhOm5xiUKPD_#7(1Q@1e{F8J{#5tL84d}%SFHBRk1g@hX5=t%Qrso{>0&&@l7p~n( zXM_Va<|2)9c=IiLowtJunYJZ__vrZQz&c{-ngC@qSf5Q~FhF?4r0N+IHXEf{64*@Z zMtCyf5k;Uk{6je)LboVD3=)*YazWpUCP>5yCXd#lMP5D@?zo=0Q3qIw(e3|{nX_3D~=jf-hy%LY){8x+K69nHM+%DGNaGQbeTeQfYRR$ z%2By5DY@NZlVP3VlG_#P{4@8!+CVnc0rmh?5zGnb6V6vEhg>Sv;%RL!EnHEzXSsnI z?UABRXj7n6qPlL@8BI(ylNdk@ZT zPgPFk-VaTbD^6TJpnPZR^RFeVQ?hZv0osp(9vU|~alX`#zjx=E*PIQ7*|8Y3-8H1; zdUM=~^EP5QYT50)$$zE{+M+uqRH=I^Go>rE5}Cenuq|7C$X_}e7_ zjj=N3ugnHgA0VmNV;V+g!Jt9!#GqOB415^FSSsl`8;7Nm;LS*J0@PnHZzUo3*dam) z7s;uF+)^?fRgl%hv`NYwd}jD#`h^P`kYb|>rV~P!J4uZJIkYw0Yr-U74xK?#+fI4m z!{TRILf9bnEt=<}JX-ap8ky7rUd;Z(&2RtMDuK7hXxpr;-q>ME-Sa4F6N^qWO2p0= z0(uTgg&T!v1W^T(tS4k{q!@4y*(egDQKh(Oh7vEow;$rK5DGIDhGyJ!pk35%QP%!a zo#J%<-e4u+lmySvu;g^UX!f#~w%e;rwB+kEJNCUZTYGs`Ky-)1jJ4N)Wl}}@Q9YUt zG_`>0goW-^1YmVC2!;4X0wVfvSo3Dpi!4nM?XN|%+MtrVsHDCUU>EKeBgPOTA;GjH z>@&qGCe8H5iMj}sbVPzsADKRYIOuNLLWqy@^=+Hl;75^kDs#7K{2lwfB*ka$GiS!Z zQgv0RL6Lz&gu~Zh?PXw&k#WF|f5{I>R6@DP=f`W&eBiMg%Gx|C8bO=)~KSdu=MZIJPFe)L`42N|yKGor5ymY!by#3EGi)?B=!47IXwd7cnxv>|iv z)6z`NynM=2ut zJ{+-UmR0CTyL*RC^d+DGzq?rj<#(XDruE}!)5?K=IR zh?a_>2Pjqh0(4Kv6#j`ynnEGie44C{2V-xTTfqmv3VVQSYFH%4V;1(SA*vf(0*m2b zqFaF`tIHiH%4OlH#8O3TPNHW$ux9)g%!hr2pin{X1iTI6_dD=-tSI!3-F+zj9%oN$ zZ8N%L=7HPdbias5xiei8Cp)=-kdzM{9GyYf5iM1k&s!3U6MW?kC5I7zoa1w;`YIXB`0yrECe+k7m#^3T- z^R?ujwA+tb>9$7Rt@w~!O}pE+jDQ>E^=LwCRJnH!nky{j8$0-~4mtG-YRph|`9&Xr zpcm%ulY;n&2@O($nm4ehlYGh1}{a2>7Z~g9pa7W|{M@{X|}{n3RbVgX#&) z2b3tPpz`;e)Vm@Tw8;Dr z+GOLJS-s;U-LtsIfa~4jKyDm<+|PM@t^Rvf`r&q{em%{1RqbnZ*_vJ=ANA6%_$sTb z@aobB7l%&v>V5c$T=LuVqHx#VA-Lu=<*=n|zTfWk|3f>kJlK01Df`9s?64=b&7RWz zN`;R3*~N_t2aHGS$68ru>g>zY3*RZvqxqPS%;`FiiqcWC=T<#*6UvlS z3G5dOi*fv|cU(f|gdLk&a1o;ZOPw<;zruKX2*MS&^#Dn35h>3j9GE~o&V|MZ za%LdE;g*}4DAfg4r(}~SZ+?J~BGm9SNvHc;D8fu3SucRT941IQgIaNI0!}nT9f7_V zUKCqC$`^BaBqWOoo=o#p_wZH9Mh*BjJ`X6L_mI?Z#cJ$GVXvnW<65?o`NNJA>ATI- z4jg>Xx;$Ajl5*bPMF#_k6Ih;gmi~)oK+7J^SCy*S>_?&}vvG>V!W^u#^xydh{sJfi zyzw7=`L}rsz6|P%Q5AHRK^3HKqTmJC1OaB#?iX}lmjOJogMe!!^;7kmJblK`e=6x) zcD*B3YnF`V2|{EGtm^e|O9(6~oGL+GR74QSq*n$17dmj74`uMds1^0*(1JDBfGRg)GFlyd220(3I#%P~msX-n8vhRI>j- z?X~6B^8NJHP5^YA+b2VbSNV&T%gm9L^?i8C^ZZoY)hjY2c8xr=`yTXUU0 ziLyQwd}#IHs?zqQ7rEZv_;-0zkVmwh2l3T-i=%DvFVi;iDB+Krs%NWf>+R6u zz9}4A=Q=6=MxdXsh>g;J`3$19zl=42Cnf6Z*I%^GpWGAti^S}JBg)1x>#D= zXc)q@4F4E?kJq)PFl^wA~wy~7b@<1 zaIj&cdeF5EU2{j{!XI^gmIeaG7nvk6ou5Qwd9k-2M*tC}159Gvp zxWaZk>AYWzTl^hTc$w`~I9kSSp;_=31;+GVNLOS{K@wR@os1_lerpc07gz>^SA^P{ zJP=$i)gdPh`VYP6G{hl`17ebVq8-P^S1GNYMfb(xR59ipU2Nzg-;>6I(k@6DkwmQB z*R`#DrsEom!?#>WH_WM|N1~`l``751|Ai;hYHv~5?;D*X=Q7+(CZXyk*UW899j89} zYOA@6JzcE3)$HZfn=}v9{G_!%Vu^OMCHF4fCq6VlB@_HAN{j(y4 zMuo!UUEO>|FYWJAg|qATf85`aYrgL6=!?OUj)W=6Q3WxZo%@gUKh0miKxN%eCy46#^PI zthN5*p8ZS59Vh?rxfo5Govz-~x@pu;KVVmD?koLc{M5?u?1O&=we;k9@eAoVkx~(~ ze#r%~H9d8Y*$YCL>Lfy|r}AEgZs8M@IjC}!5>}+{bvYrUiIh-!qXajYC(v*93_)Wm zJ@`Q_lQP$fd-C``ZECT+ibk{We(Hfc936G8s-Matl6bn!kCSbmUB(-OmOg9|2FPWG zoL%y6yqr~iD)w!~WMU_=$0DmfW4hbz?xIfGUzba@4K@A4_Uc$9bJ#|=%1F;!LXrb; z^PaclCY1Q6MFfO;x_KMt2HS>i6hbeMIgNsz0YJ{Z9p!HBDrq4d_)qJ@M1;UZdBBW+ zh{lWhT=`9l%pdUgez>7AlH6pGP4=V(jV>;zy>p!=b#WefNe$YM;Td#BU?~o8B;Ydg zQwt2961q4q4Xg#qd@=|p>F484{vc(22gD-WadVONQ?W4m=PJXkTNp4V{6;TJ1mv#( zI1!5)M3#E)O=pyU8K-=E_{M;%^4H}URAhIrVyMCeUr^mP__b^X=^UrvC$eJA)Zk0n z2mGfJ)gqc+YOQFSL1}G!SC+9SJj{lU=1}hUttSUYADXJPp@wh1)MVqft%d`hp=+G2 zrK){C4ezd}ekvF2Ame_nJ&w2z#rO@!Rrt{k{*^R3=K2$< z_D{c1y`uUj!o2=5KrNtR|Ew2Jj~B!oMK_K0;aBgikpGBR<|;oWXn01DMy6CSWY5R} zAHpmNbD8DEk)a+BWblK+fKFhr_s**CpW{%vrf4K3E6D6abni zOz4yd*|32^8`d3g3F>?BNp2yQ82{>ag@^{36ZUqNtiuK(X6dgw7O^l5G83$BVTpw( zHsnrJ1WpZS;L-uoWf=I(tZiRbpdn&tfGJ#CgP1H>G_R zq^k!5pvOS~37DH{VSKpIF%i_*CeWkV>=x}$yytDymc7)VTwALMLtahG8Z!ozTjO=p zeSDHEtZ0G9R_UHwjwZ3Hbfcyb`Ri@`qEK3eiY|0Mluq7F)PPn6jNWWx@goV7pYx#i z$_VVHfDST&8EG&}mRoVKY%5%71h&#g#048v6J9J{*{^6yp7b=|d8Bm^NM!F%q@zF} z(3~zy+Z1IbC$fxw0lQTqeNH{b;e8$hs;Nu5#d4XwDfMYAq02u*_D7bCfq3N^iWMPA z2N+y1w_7q>R`HfKl6y0O;s}^(WZwXh+_CC#qjSN^4Hp*;aF*XPF@5#c!M}O2_WJcm z*?Hd;pX^wjCsw3gZFu_1M$VDB!fMI7jkRSPbly}@&Z<{!yaeYI0@LFD?`7jgON+iATY16~K8`O)=8L_6M=F*Yz% zc9s{k#;BO%!*ZQmX{gUp=4MSMDv|4rkIW=ymK2AI zqBmRUyq!~8{1LA@yQKL2Q0bxw`zuTp6dDSMyA1l5d@h$OzcF4Jr+bEI?73;n5A*!x zzSk4|h4q@C|Ap=OUA6*jVPKkzwn#3U_f4Sk8H5zm2~`D3(XuAL5OfPjE0{IOs^YTx z7wl{RT7)&I0N*g2X#d8u;1EQFsrn2q!v?xz_#~Q9JmCe(P^Q#uDuZc+wQQqU|Z2MDYwjm|RS?L2eUCc=oGi{uv!0J^MIO zRBZFed*Nen4j^I~@p}Efeph3&a{GN*0|p{;ib(j;SBzuVj&nY)d|2&HYPSs9b~bF? zK~uf0?^n98>YRdy3jM0T;g{Cmzs@E2Z#DBdx32OhLO1x(@^UyN=WU#DipX(&FyXe@ zd8X$GpAZr}K4e)^=OoT{Uv2P_t*O;LK2#TdyJUru54MWSjD`u%hz;|*hD1AY%kMt< zmZs=2$B!TiT7|YIY5hbz#s|C}J_>N`dA8H!CsLVuC($BBYs%BoDlVF2hOIUIXE?nFL`YE3Yd*IMn>I)5G4pFX@oN0)rW4&`0YU^%QbQXrA zgXOqHE1F|wh*(`KTtK-wL*$rv0N)Z>`s4CSt3#+Xh;^Zu+=}l`SehEC$NL6(uuUY^ z*Q`m^?9Mn2sd0Q4yZe};ohd@QkjX38Bh7D8uq)Csg54#C`(g)K=w`R!!c4*~L($Q( z;2!|FtkIiPV@R05z>Jf+u@+HTT^95`#G6xSU~5X({$8TdZ7-P23zr(|%zY*f5k?I# zXYg0eDIbWdq1k9r@|TP?dre($CLWn=k@x7}!=CTE6&GmnUalx^!&jiwE^$&VWHkUY z^8>&2t?n#5RO2B-yF-){vZ(u5S$|xM3bb4`pf`JCH7{7Q_WSNH5$p1D?CwQv{t%&e z?ZDAVbK;+qY7d+5-96y<;6v(Y`%JsH7E1O)$|q8Vf8BnPafO@l{VTKkAGN#OKCwy9 z)^dhb;*kTl9yuq75B&Ihe#O$HHQ5UJgQBQO&QD5e zx<$cK8+|5Qo93k=)@0uz$f4i1V>N6+WJsB+ZhzQ%T$+2K4v_GIvTHj^B-Ah2-c>Ntm z(U>8D*sjIkDygmD7SWL7lPR)mUHz7*Ym7C~-kCXLr1@K0yV}ZrkCzxjYEn;;d?SbtU08gKNoc+eHE^=PgE*JETM@g+;?{ETXWyUFe|oLb^qj zAr0nt*yHZ%lFxzqQ7qjvdLRzr_^`_px`jp1PuR7k*xhMEO-pDd`<7GaAVqB-8)cHH zJ-K`GTQapQJTAWeD`}8|*nR(r;A5ub#Y#M#;_e*Z+q1Gl;oR~QdG#&FXK1iCRbA2| z2HkWq5$LmV13!_P-p`hX2F^e5X-Pr5?!Vmd8K1%Xs^K~q#0fLF8doCB-Re}Beac9& zr{4YXu#5cXlV9!Q>Q3Itp8o8OkHz!@2YQ5C0A^A@i_)!mjX#lDpT{Y6OM<_kq4@0m zcSy-3wL=)yk;gzi3tE>T1|Yd2Y^XpcVKUSV89P*3u?}!1|6mNyQEl+C~pcvz0vhAywa&b+#o=mrWvM6M{KnzmxPWP>}3Dt zr8i1(W=%Y?HP4OxL9Dnn71-6uW4Z@x1dIKXVCO@K4js0;k>d9pBTBU?DqI_%p5!J3 zPz6wG)tu1bJ!$yOas7&hf^>4DlgsFX+RSPPzoCLetS>7E#s5bn!c~B?3{Y;1KZB>u=y=kz1*%_t6No;a-v?B zT}r#MrrLbl%mt+(+oLAU>xk<(Rln(NuARm4tT##asu@OUF{eeE=0$D7pUC#O%cSg0 z{I!`7q{ zt4H|vLmzgv7e*gzpI^E>BYvziqJJj+mV*9t+wr@;RWuzbf-XqF-ARSKQdq%la#LAWrpd3|hl`6L;NWe9Lvw zd8_;xCzK5b7q*@Kg5&O5(HD5Y>)!6TD!(sccWci&@aYK?3C?8h{6592P zL|&}Boe8&AL6pYpL1~wU(5D2B_-E@c@@o-_-E^D*R+2RCTr zbJE6MP}7Pl$fKOc@WfP!B(PjUi=b3u&OPjW&78ZM94Pq6!5^yjOxp+kzKAF|Bcq~f zz*^ukvTQn@d}D;Ov-LAPM>UcsbunWnl2 zeVSaFOuD2qXi%w;ZNVo()ly?ZWu#%!DqxDDtRe;-UaUG^iuNLNhI!HwB5i?Icyilc z$FuOa?10J3_8bFDA`7aW=3b3WI(nG*c}nh>y0rNSn$$6Dd~>=N|8zG;P!E`Fn7~+#yZ){gtYY0v#c|v2gV+i+%6?)rqj_K zu#YGda)(C%{^6k4vOE<5Ci$Cj%62Wnu4V?Pw`$FNSwo0O2*qdkH34m* zs1fpE$SdI}a@JT@OD-;NQr7r%etq0fAfRURij&(lI&5?{aj9K&RMoYRp&Yxm6VO*v?jC zzV=z{%L_mU3vc`pO)aVNz4NWN-=Di6@J5hswBwJe@wc`is_ZR)&z?g9>zxk2xn32iYOCKYLgIY1@+rgj6w@>@uMkI5+^HZ!MV_uFZOylQtx9OJ$^#j+G|GyYZ-n zSxjd6u$1n-vf59OLoD&*=K6;vEavI6hl?sLjc);a!g@k7C{G_Xnw;t|X~|WC{p6mC zGsm-9{S2goVt$Bb46650cVizhcz}=flH}whq!eN`k%>CkZ+ZfCb=U}kbxdG6I)*Yh zzH2mWHiA7~P?ZAZyU_tvSg>ZU+!0DH=1!m&NT9tK5b--aiPyya5)1F|X zV=@4wY#To0UZCz&!LED^FY!>n_eV&lY@o3kH~EfFr4va-uvm{r=4gqt1uCPh-F>l1 zX@_~u7;rJwV3_b53W{2@VSVCoKT7wAj>QgtB21O0#SVPZBI=*+$TJ-K_}o08uYjvM zb7Nd=u?>BT>Rp(j4&EbNSfT)2f6+yYN1E0%Mhxd0>$f;gF-NJu4Hp{uR*rz=< zD+S|4V?IXdnt)pA?2993S$vwYjf=?E*k$&iEcP>okC5w=*6g=>&0p4rqbOn5&3@Ny zrseA9;d0`%0dGPLs78+Z&6t|P0M~~eBk1W!oJdByv8F++4}lD;0kbxt<;k%?yCXo~ zt~S=KDyvFNwCF%rz!Yx4+wc!W+_&$5YG@{4+fh%K!-_L|cjC#7@(76r9E~&Hx0AnV z*LA1ndSuq%Ntb|{deFxY{zT?HXycHc)x;L2!3OL~N}?LQ|3{e#wGM9MQV}Id796o3 z0Fhej@9iOI5LpB4bgJrwgUa`LP>>kV4e{Jhi1!yTrzfNX$i;SgM?*?&hu;dTrch5P z0RrWlvO^SQjgPM9<|-bH{t-~Y4i(g9UvwL#7bu6Z~tG?p%eIpifJTbig1{$Uz zCTtUsipRaKQK?)X= zo?3S;X;t0Dc7FYAN>@?+YY*^^GR_I&IeX!wvJZUt#_gia@QrwlyL4s<7@wAWB zuN5ki^8K)5?T@~Ctxbz2yA9{h=xtOODAFJ;!WFADykDWqn}0U0B6EDikhxxRH#FWq zeXNVD-VPS45pV?cVZUp7KySc`$JCmk%>yF2J!JJ>4JCvHepfd^d{djxmTvrk;eY;B0BJsebLSds0A!v>b4_?6E zFcwYggwaGV9Vl}^4#`x_?0(nDek~%48^VBBW5lYw$)qSh_(=;ZIpQn-*3@UQa{L)0 zr!7t!U|ag?sZ*ZD$_eW&d}Kgt*ms?);I+|aW990&a$`Z}SPcJcvvymC;dgf4q}cC% z)kd)k7Wmc*QtCdxi>7C#c)dMTr@ckrK*Wjuvg>J)R>^*%O;>~8#H zmWUTSl z_fo+6~{%4c~rSGpltRMd-yw$KUG4d(v{*)(+BIYNbW_olK-+7%?7yDg@;Ax}%{YKF9 zwIzk!mzXF|i&FaftatKW7C%%6!fGAxPftZ5_KRFll3{B=mlo7Dup}8C8K9Bs7wx6K z_jS1;_$rpa{+d7LGY|5@oOMH#JFfa3A6h>PXOim2xUrPIA_O}FQ=u%tqpM+fJ!ws5 z%#<{>7(2+;Ei_zTs*(P7;>9y0tbeUWd;9zHY3Kqnl`u zVr@(qBA|uB0HM*9=$g_!2aXg}_x8Fk_BvkixU)zhGt<-cxUY|?CF}>CRU{`0_Lc(u z0Yw!PUrxMEaT+Zt`x2q?hg;PsQ!$yfETgaZn@N2~IejOkK$`I0C2)S{;+0ox{O88W z#c9|kNgsbCQ)&fG(`{dODE~OGGt$1c-AeC-e6Rn7X6mx3mG{o=?$s>}@#FA=rZxKz z*$&TRZQU-Lh)M%3pKY@o)QXsC)wkh_rhHoy{HOx%c-9s5^=<;MADhZXah#t>81GZu zsVsxxqlANB^AAUd`hQ;LH+Q6J^@f{T7r(x@dOXHfa(-9gW_7J-%wHj{nhNE(rng5n z3}o|bd}Ad| zz0^p7+BH)T+H71RNR!Pn_?|1T8&9dH++1kUbbhy;us(#rh<`HW#X-MOcyr4iA%wVd zr0B`gvxCApdq3K(the>NwEkp$AMbji{*90qe9a%e&s>)4x}VtaYL1%Jw4o*z_hVf? zcZ)^h;Z70b^}8*+_h(Y_Ovd|Xc{DwAS-VrQbG*0ibNdt)dvH=v)ydnNia&|(ZqT~9 zb+U(Y$Nt4fPT&2bbfgza$H4r#3 zHd-osB75SXktU?|t%m>VXm%^Pk$eK{^&?Tc(_L8rzJ;|aaUB&VL(be?Zo}n1Q4J%l zGg$BoNpuvi<Kmk{el>qnO<- zR0_$#lDehq;YWp}mbhT-GpW&|NG!)A&fEtILt*@xtS-n9BjwM|!qk-%i6TghvdnhQ z0_Z-6pr)AA4V;OYbxOYoX;k!~C$rL_w_|$|L5!{f>IIv$fPg~>qu5A->59gONv;<~ z3Ywj1OF^%})XZr*tFBc#=bRla;2zYvS`%?A|Nr6X+~b*W-~YeJA&F#&axM%B%{k_f zV@{j%q2!QQA*6$1B-ETwIWugSQwceAP|6`^Gm>-2`H-0N*7w%?^ZWh5_9%JeVY{#U zx?Zo>^Mc7wJ?AC9BidTXY07q;KhIkz>;W9V|LtlC(<@SZpIwNC2#6G#`Tr{UC{{Zhc;nfcJ=>7%H69tl)z(E@5r_gBZ~HgDZ4 zs<{M_T23eqX~ZBGtJStgt~&BDE!$?vH2KMX&7M2NPLfp51=JpQ3uIL7os@7F|?avaC{mwfN2DPb&Pk)dc0Z z%J@6o%G&V!8p-A0mcBrNzrpGQhhV^7MsG4u?eAK;6P*9BIP**L=JT4GebAL;&v^Q5 z&D80=M)mmS%>I`p1A(UYVDwE5=MBqyUqi6Y_CX*taEVL$tWjiNl8mam zKS!b6O!NvzEKFEq58ca$P7WDR>;dWmv~w>(CnBoBc}eLHrS@YkV2Wo z90)34dLgID+!&apD-xHHjfMCGTJN`{o+Z8BjRCQSfb(FKeU_9HrKf_6Urr`Tqkc^t3XSUFZ?e5O~5udXnu zcI`&cZ59*MlQQc3^BXjEB$LmO#K-InbK2#A*Bo-^WQvFucE+RhfBUx z`cgc5#>^$F#sDJW(NNF{eZBS@N0Z1&JAJ0~(9H-#wo{5eIGjuZ*XeTRd!dw5+nt36%&?36kWl%u=zd6~D^ zeu9cX-KWym^AAk6+x7P{?hPN5e?^wWInI`7l;5X%GjCmoMZWi}2xnm(b(A|uws_IE z5}541)xkG6la5!pFCDa7+_A^P)c&LQZS7Ia;)U#wbx14s?-g;Q9IczYWqvI&EuqES zg^hJ-2OCw7-AmJtEp=UjbF63?NnD%RH#dZL+pKXp5euvPE?Qrptw^P|8|rF$+w8dZ z?6;(O-(0D|j6ogmU$~Lel7KDRYmEsmQZUTW-|z0Cz{kP8Fv@-QNZ(lF*y%!rUfSu# zwW^FVQ?-895_Op4ed(IcqMfI4Tj*1}h)bLl1&-wV-|*wD`Ee}?SKfVq$v>u5cfH98 z6Es57D~-GE^g?Xe@a!#dazTcBEO%?Q1{#Gk9%AJV&QSqNMX8d;$CMVx-e&b0GLJ>@ zu`LHQ)77(7@;@SS)jlRCtk^RCYERnfzdjQ4JNru=O_&0(5Q!>$N^H?qk=kljH~lmy zMn%27Oj^$6Vb9lO*|j9&W~Dsc7<6E4<-Nqo@9Z{rUKPFZ8LR&>#;#*=Lv?wBlvAcP zRaI6aaS;O9_1m_fp73A~Ogt-dZm@EcfBp#(d|R@VT2qr+&Cp|ndns?xD%-pBN61_7 zRWm6EX7zXd$q80n?_YiiB5pXZaSG^@@DcaY1f~C|<}Jc(eN+7`P9~-7I`vmsvc5B9 zmHkp&3Z2qnRc1vXP1H^_t79MHMG6x|vSO{%qX)cixj&yTLK-vn9g0-KF+htL0*!m; zKg|ksv(-4zGk;=nS0OSeoh2Pr%wmGNfDb6R<0E~K>TGuyOwan1Yh0vsnD}le4L%Kw z$$$AW?K2bmW+4dB%6*~2(x%cyHFv|M;>P*z3d-iSuALC#1quv6oh5^L3@CHJ^UfbJ zm5k~SAs&tBLopQHf%3HLVC7I*7KVhouSZcKu)-vUEN0JwFF6INm=kO5~PRC55PaT!`JQP-9~Vf*BE4kZaHgBPLv-tjQ6s zz<&CSV_k9{m+SZ?72`?1hPqFuPMIiXc}ko-9C=6pQ^Db9meZhx!#Y~yIi|Ay>2!yu zBDV{5=1z9|bh%oYkvZppcEZwNDV^oaSwVCHKH(#XK;U0Kee&eF+@G-ASA;7dm3Rc) zbG?);ctrx7$~DXyjZF)dOjFnT^?ZE$*+H+9Df-BF^?*Tf^_OcV`j5fcoB^$0?g=__ zQcJp_BDY_#XLnt3@@0CBuUAf^H*q;ORAbHmg38n zDkbAJ4Nlmi=#}=|>yjTDcJHrKCjC(++bNO0gY|^^_lnZ@OCDG+cGT;URFv1OMUzz% zKRm|X=}(FpqWr93a*_(^h~ei2;w)^vU@u}b8>TBRo_RLDVk+8f;{ z6SQCz?9+r71;cn|4WFj8_F0f%aw>;i_w0Ms*=VohVyuD2mVr-EP&yq6X!X+&?kre3 zfiW#@eK0GK<;I$+Is*jy3TcM!?)CB+n#b8Ikl?4GL zKJa__UmnBaWZHkgFmDOE!@9td>pR~p$({n#$-B@`zJl+-F*ODLW1x2#H5USpJ0#ao zv;RG6MkHuL|A01~?tE5I4?#aJ8FZZQHjCH)U{8LfpFtoEnA$xjpF}RgAiE|I1t2xT zIyI06`p}3MZpVLZ%@MS)DO=(2g_aocz~{EY-dGiUI=$;2KN zp)Lj-|DMGXadqS&Z4*b8;FVZ1MLZn5*YH(`isC-hu59ak8n_Uw{jbm17GCacX}nYn zuEUStr3W9BVa{5ft6cnX;PLv*o5gr>maDnwvc0d7t3pmKZ{~~I8R`{|N7AIN+!OWp zJ3S>18ddM}_on3&VtJBQrIhybD|GSKuabnq24c<}Pv@OV_43xwJ34zC_puF9&_?p2 z9Xz$(dzn8qE4#Ht(@}7dI-VZ9d@*eFx0FfERRg_|+t+hGqmKo2r0$1`9>`2kOSK?lQxB51oo`ScLsO}%kxEYN(p4$rja#w#ehG)^wgFh04GWJ+dvMQ2|YkVae z?Nm3>Az!4RMbBVLd*z*u)BB`3Ft>89M{c`bpeUDo-Q$aHhwmg=vxMe)?DR&2@{zVo?)6UsAa9EKWgxO{?fq$v+H(t<_O$YMr+HO zrbxd?{#6_I?iO{R%lIT)2+Mtqk2V-`)IbNtdlw9 zpJ(pFj|jj#r!HEUF&Dh`9AY-W5r;=H$4ViOS@gRHfxysrL&WMYMAxibh{04#XfL+r zzA|rh-H7KW*KWxwaWEHpZHW0U1HLjRYGP6Hn9xI0o7J3X6$@*+{!q7t-mS}F6lj_J zn{VA`!YoklkBN^SaoB(MJ2)b*0kN zLWC}v_LTF5KkAb2VF#qfm<0XyPui9JXQ&Z_ zK>?2lTz}~aLQ5dQ%LXQ>MXod8ka?0DLnrz#C&M88%xTGH$>1*}=wtPKQdB9QkJ9)A?(mBN3({?H|tCxo-1Xe~)E6nhC`PJ1>3tu8K35*9GT}OODCgm`H zk_{$&P*jF-uZhUh_`VEd(jki=|4KgA=n%VAc;p`f9S@&^NgR0(@^c{)Q$V-x4FUA{ z|0Rj&)=e=BhC>#=&c+cSSAbjqKo;~KYo=vCDtL=zVxsR#a1_F7PkH(HTcIe%j&Zp{ zu6yK*VmEyp`_JcX^WIK;d;@j&nkJf(#^bubknuMBSbSN)G2TBm>b_y- zzx%udKM$AbX=(LzAq^CbdG)U|S6nY#a$(NxNEI)z%Du#PiB!^_HnEH4=(vKXl+-kB z@t7OGK@>M5FSj&W*ptLFz1E_!9lqg$7B(l!ZX)dxUhgvK=^9PkwO}qhr~DT}6tw3g z?x9ksy?cFpe}a=Mu$9LH;PbTKdnDPJ{v==nhO?gE-yD9@_@rOr?0r^tBWLKIoMzkG zK2(Cqh^DRRTE>&yhpwaq&I(Twdzve$I0tVDQjqb7F8DMYQ_(n=Odwf`Y*Fm(=0SY8XWdzN?gK%4WJw*7$0@sul!dB!J}bpGBbE_)#b-?n)C&y@StD%{M@s= zR;($p59%J%7<_%3oot~$25%Anjny$p^e8|!SPI5irCQWp56_*4oe){rf0}ORcbmPI z<15eThLw}~p)8uPtc<&_R4YlH1u_RW`9j{^tE-}~D}wL|J)N!VaO923SBkIkNoO(X zC9jCxe<2T!ZvV4Mi7 zX-YtHLNn)KEUs%@X4^65%i-ttQSKSn1y*hMfS?;AoXnO$JJ+jr3R%nR8t948hF|Ja`JA-JB+t!mz zuhUv2NBrs;)zkpK4(>CvtF7yW)bX8Sy3udF9GlR>Zfl^}f5N=?MAK(j@;gw9I~P^y z&~DW)G&a-!LIV6z&&yj4(i_L`U?inK;obfF(fVf$?@=w~f6O!tgSGOgECtjET zfT2_6bB84=9yW-;RSYW8A;AWcO`Q1CvyNXe1v7t44Beq(z-rq#cyTbLwR6JWHgb(; zNryU3!ClikoXs3gDs6uG5H4{^#-)5r?lixuqdyK+nt)F>$`WqwcFEaBePAM7E*HKo zas1HF3(Rb%L^_MNqi{+$hn(czXZ@Oe+!D2c^YL)F>d z1Smo|V{1N7SWD)AnwczccOHUsWMD%@M~tRkA41j@Nh|r_Acoy1XL@S)`IYH{!&z}y z5&S2)hXI9Bx2eSnigY;r0w`TVhhwwQNOU3+yln4T23#&kpv~itf*U|H^azTEk>M}7 zyVm;C?#Zp;+yJ>8zp)0E(99at(MbA@=sv3OQFVXg z%V%@a@*F@cJO8SU)1|;0R(HoLy11*1jvNYNs(!m2xLf$q6?Ggn3iOg)#I zu4agN0>KA{zehCKWq`*fkMuszAC)WfWxToDW4u*F+dxR;eY0-3q^q6Ceemnk+ST0A zgOXz{$Gy3JSrTP#5$E+-zM7oQcA&pga;f^BvlY|0pnWZ;i<|d;$uUk&AjYBJ$<{cg zq<39sQR8S2_D6Cyn}CazPOQ}HIR{9Qd!}Iq%b}g~24LX2Rc_}yM;^e69}>m)r{^U^ ziHwJ%b1b+60f0xVl&NTv;h5(f5s0AKx)H`K>n%Wa=%Xo_jT(=3uARj9U2kC7oKkUq`Q;?Z*^YTW)LS>r=n_gKb>En zmn>bP-w`>O=Zd*(VK)%G5$yhYmb@ysn%!tr)^8|24jP!)*lF_`lls)zW{V`-kf5&@ z_N-xZ<>!yM>VkRVC`*df>a8TITN${G19(7&L+D(E8M)6w_x9x?j;58J8&*S`4y`R# zqI+2x4BgS~Qf?lD>+GAQuMsC{Vckn&Vw!Kb|OlT z&!)+nLd54`KLKpxnBa8@L5YcS=#zpqh z=$Cs1g$@g-C<&gmj(5~Pl(hN9vFO}wB11dI!++cLU_pBfq1#G&MW;V%=|m8g3Etq; zjr4ow3qMGT^Fuq|V&3mIBjw(v6;si!I~H`pXQj4X#HrI}M`J^6<|USTQOo|rp^o0R ze<7T~_ril7?e%}XFp7QnOrRcTBfPM1HNoPMftq_3lr6+uEoFP>5Bo;( zX-~ew)Hn!|TKXRhdV8ZxKjlYToK6Gh3P>ych1BJ)S%tPPm`}uwiSG}fuY5jW0KPz+ zMJ4d`0e|2jjO(Zp$v}aqcad ze<4D)jY~x03H`#qkTxy^cxrM~uKKVAVUC%$WBkrrVzKb+(eoM1gAX3l8TO3nA#GZq z#nLeuaY!6#mE9!=Jsvo=>=t*Pk+QWYA^8i@z&6TC*+hivNPE_Tx9+V2$!0|}ihF2E zW@pq|psy+F^g&~a+4onX{WW@9i=!=ePWwf#el8i%mD@S>xJ|sfLtKnp&HY`rE_(44 zo4P(41lR%w`LdK;S?(K-o!JW<7^gIO12@g@@=Shj155q8ti{SlgMQK!WYi-cXG{F7 zJ6I5a*Quob@+IjOd#$?EMPHWT#ULqMVrYdc)p8kEU0<}!tcYwP?6Xd4-h@l7*F^kM zkI}y2w!7jWx|5F}<;)aL*DK%g?H}5NL-{TK6bomcR|{Uw9rLb1n3Xfkw*+}I6WPA+ zPdxs9;K{Gpn^vX|3g#!TLlfv#U}gFX;WKjDy|vRT4A?=Tv2j!vst4v{a5Z?-l{uA* zg5VY|2dCOkJn3(S)`iSZ7{zf(=A2nDOjs~D{YeGtItxGWlW6VqsIzsK5Csk_gGunJ zj{-`aODZ(dInVR6M-jcTC*$xnARy>Dl7}AXWJ3FlyVUKo!+n2x-i2ink7rmPPrNHJ zDtX~6$=~!#y_~P+T{J(wf578)h0zINS3^>UAzm%!T)!Editm4FaW2wJ?1bGg!)bdN zZmD8)idNB2U4{^XZz;syoI$A*dJM*O58+X81J4eqQ>Xj+&p?W10iiD3^O@yW3uas< z2Usqo2zHdzm1(1rZrc7Zd;>ZZ?Y=ifhb8?i{tO6?n^&(?S7@O4tMeqilEcBz76)in zNl@VQE`a`lJ(~)lZ-!?W&qlld)1Ay2tq%i@qo4lM7eoIi;tSUi35$raL)uDU+_o+& zb4f*>QqrBF{nlSx9)o(si|MjjeTZykmTX3}Mq$=m2rR$Y^XW%%8h+y;cbuC{>{ zuQ)+E_X+FFMW=FzXr=q-(*nu3aA%XnR(g2UcAZM0h@n{zDdF^M(bLG0!AOvw+R3uFDNc`x2niJEgoDS4*|N>A3J z(0X$jH}>1v{T+Jqi3KbJDs5n`fo*UN2iN84?Z_o0$2Bp7L+T)K3gv-e>j7#i^!RC_ zw|=L_(*int+LJFDPeX*7|1E@wZ(I1c7XsKk{i)LiAM^*az)-7~-lOaM1$uq}*XZKK z(^p+JIo#F4TewR9530=bvDR!CeR|*D>W1{}%PJ>p-Th*RC3B5ZxqU3pf^NhevizvN z;CO&wjI%md4FOg!D&4PXPhEfW zS|F;~TBbAD-TlB5(Zl5~We9#Qo8vTFoC#hFD`)TPk!mWx?<%;$m$a|iBDk~rj_HR9 z-K^ZYQNkta!07lFg;!hIViQzH6X9*pF1`V!E{5sD(RxM8IOF$8t6wpk*lM`7;L@A{ zEteWd)F=&58cz$Ie{ZFiT-lLm)(CIv%J>VpNdl7Z;Apy8;o5HkUv^Agb9sDEMK2=f z_L=hY+=3Uj6$P)?JMUM=F!d`K-QEYC*`V=EAbsebmAwi0mrJ-OL^f%&>oCMAk6nE` zXfQmpK=5nVCbazx%LDj?3e#8|Z4W-7sWUzLZh;-5}7dpOqw7&+SpSeBE;_f_^d(@Ou*jb2|r| z`8%_rak!$WV}ew--1PiOl|o&b@Q%fara(?qd9u`YR&EpiWYY)H9x^}zT;BGn7o0N^ zX-F#6?{%q&6b-j@!r`S`*OZnt{kimV#qHea7Y9UsjKqC_Ay% z1~|$@YgFT6ALs8bI7}Vd|Miqo4MZ+`WX)hA{E`Q7R1GBDrp_Y5l^t-)W`o%wMd2$~ ztG5^{9--A(Oqq<$c_f*Kwu+d~ZkNJy!|5f4^~yPxwoBSx1YJV@zz`8@R$%p1H5>Ub zjd${pXaU?mp0IroD`d^j1&R`+Nn*svqZ`I*Qhfu(%K&eekibBnxsmmJj7L=7Ou*+&@*uAej8Te9HLk~T*5 zhjWPlD9AtaR{s0JN+UnLGi2+auB<31-jx}bs8LgiGy|@K2f36kOg0a1)Nq=9&7DzO z=|=g3n9xmyOgKBI^LGJ?j_@5Nw#z-3sYA9oyZ5*KHV3JBwIE%)=CfM|^K@@Ik9~=W zz!T45RrEU*Yk^_r-MM(dG;gzwUJ_j=XUEadsrZ7xL27Ibv@Uk(j-TL^U_fJ2ef;F+ z0dYXyhJX2i-R6R3o61?fUw0Jj%9&A5+8XN~2TA2se~egH(%w}kQ@Ap%1){4&TJOq9 zt^|`V+CP8)c8K#}eJcS|Zp7xPb5gcIv`+m5k8sNfUHm8cc&$~~U&!)b2z$|v6{af1 zWiZ?-&bCMADs=MgAS#iqc!HEhZjg63W_=|}0W69y0-nKOB671)N6Q-Ey$NuOb4GIe zjV&)zX?8UDVLr#e75wbB)v+KUQlv`*Y3e6)OPUeIvsvWKM{o|!-WgDwjjX^YQ%}f; zW)QvN9My3FG3A&!-nipp^1yJR93I;w=MBcZ0!DtPmOeF6qJc;?kuu!3+B zZ0F$PPG$*!wm@(6hY;wwlun15C|@|UZr}!uP@ZIYk@Ix7kkiUpk>Le?Ho8wsT0RJf zqILOAV1I(Ch)`vPqvXfL2j+5y@MJzMO*ZIn7|w#odR<~FQwt42 z!Q@OX8h%WxFp`B`LZ;%*lGt1vNu0)Ayf_&cJRL6<;`!W>#0B<966PVp47_w8tL!RA{*0k?z+L_Y_^UaDdrD)C# zHd>7kJVNQLWmT;HettnK7r65rhRB$U{ADA!gNtx`gPf^=n~5DY(0h3}J_9uX`y z%<2C^qBCp|vUsLB_#i^jgANs}DG{}Xy`gLl>!U!A_&TiC1l;#K<|X3W&(#)t()wfE zqhI1l!n-GF2 z`}c{A+WLj)<2V7tE72w6n)q`M{k!D2U2e2nZkEW)eB;INrR26}^2bPjt5@1Si?TnO ziY%fUQ9YgaduS`dr^EkyVwcWw*1SMkqk5Y3Y=4P@W1bUyuOJ}#sCA96pxL|^sF=YL z%~HvifVv4@kiNDC1-H6KeV0t@_9uRGXs?^42V-`ai~R1`;E62#2KkfnhX#DzPns$O z(F_PdBs3vGR~hRimCSYYdxF-Vk5>baSPYt$G%iTdR#4<)=LVkd$I~tVe@oqygB^jV zrQuV_FvJ7#-zB{^9QX%|GgOoF_K~LVGLvhC0&^sXJra+k7sU#%dhp1l=-xi=CBshH z7PFYIZuIE~g@`<~x*e$fPJa5u%szIFwm4hy1%Dk(8Kj@G+n}E`P&!V$&cE`psl>2& z?A!LN>!vc!zUqnx@nl!mjN!5}?H&pK*t=pw33qf*O=CLo(e`X+L#SVs&%VpJ;Ldg> zlGe{bl{}{KuIMMT**5o^y*^TQSbWaadV)%#o)58jo62zIn*O+u1?xInGx24Of$bhk^hb^L9x_?Oq+@fG>6ejo7Ija7!IH6#xfMaG2 zT)|JX>@rIQyVI2#VA-7%M{j5=%`yG65-5c`(3yDk`C3#UwMHp5Mi}?5B@F<6gaU-ty$H?n)&O1zK=Vxr>6DGVnDeq9v9VtRqUAgv7 z(kQ+J1BciZvy(zAx#bQg_fgLc7Yv>>JHhmSW6g7o-oNyodp<;Eur-mK`3vc|9P|*U z5{of6OVOxt=G^>)QS2jla1F!EH9O1=T6EW$)l9>jILD6U}a#I!jkAXs@*`f{1b{H7qv{ zp4UB4LT$eg6bKXE-;mg&Lw^lqMKnw0=7-vq6CDxb*_HEbuosU+_}iA*D;A~qL+>wX zCtiLpeO@59*)DM;Ma?Kai1~B*W3M|E+&53qdM5`%UiO%qlv(u@+--^!=WoqxyX7ol=oLDruLyFy3^W-Gf*qv}8 zhx|p8wb10hTHlFLeVMLw@Ms+KUG883YJiEHN2G{~G_2fU-X%6qW8#(?7^{`hy#pQ%})d;<#O4=%nbtP3^kJU@v6_r=n zg~-aWbJr1)=dPsRZ?9L+3gF6hEz!MCS9$DW!Gk@%#ub#0Sj5XD z8s}rrRB#4+*rt;vm83pz;x=1U@~2*d6Z*PGGwU&t!+U|!1j7FRKhc!j#&>}akr18K z)ml<|f~ix(gwajNMPC!d6VhMENjxGr?HOE2O9%COSC@|WaD|qymJV>`9vY<54j&G8 z9sOHOw{&$Azt^{km-7HPFrd+Xp1)`U<&)-Noy{~Y?EkmI!Q;PegR-oBNlPH%;kHLw z)u>K3r^DK7T}8=)jX>UD$KE#b-FUh#H-BrAiC7XG>FjtIr@JoE1^k?Jb@lm)Sh^`v zKY#}Cg78K$V}_STV(20G+MLl#U@lbm*7%tBuIPRkk)n~3mvCa4cCU56AY5<|w^^8Y z^%nFm#B8wq$o;|T{Q8-oeyWtAtt_kB;K@yp zSmTn#MfGH#Kto9na7W=Y*II`H;1iv3TI2Y!cf8a5(EB*81IqxFydAnwO*VRw?9&~g z2C9~?Wo{Wk()?;J>mR4B!K+5#r&fHXW;jLz$p-foPnfi%#4O#8-x_hk&7)PY+~?jh zDr->mhRr#?s|fj;rq_yaF0>Bj<);_hs=sA7N7xfCc0?@eE1buFyyN;kZ*SmH+KSNT z3GFLyzybCCOyuv_;G7!i3m;DAHfSC+Th*pXmezJQ21M4_MnCd{JAE1Ogu^wE;q>E) z936il@63|jSPb+pY~rF$CXm7fmG}DF7PMvD1nud*xPg}yT7v>F-5)Go?^28Y7rFVj z9#T#d;&_&vboL|WMSbO?_=uN|>2aYUsAs%)nU9ixRNC;3tqCtgaYW&MqhdLl}4Z2NMO#>oD%Ez7};~ zJH0fma_R7nQ4qI!pk#-Q(`vSSbS>5{iqCxlSYS`8Rd|I%LVL0|h(9MjI0 zP`S5mtc1d_;PzPmLR4)))veA60kYI~w*IsgpXZr1l>?JYoyY;AqqKwk*ALr44Divo zpK%)shK1%2Sv};{HqDl_tfdt?BlGCzB^48TtQ7=PR8~UQn(0RWLe5Mtbn%=fwCWgd zgs7iTaY4e4CZWpEm%^b8ZSPyvxwAx5sQL@mjKRq_0{d>%GrvWA{j%F1-oRFmFqOQ) zj=pO_9R{#LZi(;shkF}lN4Gu|JEgl6LYaAdd`S z`ctXx^;STlF*DnS;RHm5{&?QM7#Q5Yq$hFC>JRuWqKN!4Iw0tstFm^rR)P76#nBDoYwt(Zs|%jkycF(}8w!l}hvUp(>6@T#0B!?zX_ z&}AOMxIp9LH4_e{bp>VZ4}UyfH*csjsJo$`_=-DceDJwLQY33Xoqp5WM)Gjw>6@FcsU3{w~f>l$5gL0#wnb~ia3Nvty0mw3yyx7cNU9GFW1Ti z)D228%H>xu&#*K{3|~#->ZQ*qQU>?MlbALIT7KmPnd?#t$6^iVxcZG&nbdVXSu!k( zN&&??fP};h8d4?ksYyz9XMQV}bVTN1@d6sUiE;VUzL-HD9U&LN(pZ73}z zw*+&@qFT-t1F(#!2um~y6QJY2w$yL9kNA^G0vaq5dWTF8!Im5#96&JR6#G7swMDGbK2wNk<3KEx+y1!vbN=YC zed#F;mxraKAM$?8zAJ+%luErv$OU9tE4uny*aD@HBbMA^E!h+@WBJQ7{79i(XcgEo zl?SDdt%fXH$na3v)@b!{bEf&2oj(oW)__sfxsuj`7!zzyx=zn!x2Tle@83UIRbWedZb3(!n%7#gq?-rl%3C(f1XIu4RU7qPg-#uh!Yu%e+>2M9q8;iY}2|H)L_7l3DYAbudpCor3wfd6tTTA))E8 ze4aWJghzsiN26)-FF%bn)5%^P>mjuA4PI0IV&c&d>RQhS8xP3&)j6P0CW0n{_U(sIo8qBDB z9?SCww$tZ+)WJQAOJ;{9-kkwLUyP?+GUeA5-k%LUW)#SxV>EFO&%)Lm~;Pp+eRGO}I-20@gB=TOrnhi2!Il-tU|3awVquJ0n_BevfG8L8`oLqp_ zISV2*c4W+8D~QE3Ta_yPg#;tEA-F_>9MJ*m?)g#-1`M#%1R>76F5#uCIVXe{H0dZr zrgAxb=z*^$bFk~xVL(!1fdp=rTEA%cscVf)WVo|n!s5s&GIw`m6ThiVU;h{4w>z2@ zN3af<`EKy~wAC}hHTJRTl(HXpu2#9evf3^qW97r{+(kRdzSmgvM;hVfn07q9MtP)u z)Rk)A%v5>XK`G_Yne|=tw`;!}^trTLJe{s~{UT#1hpwDn>+jiDS0>xOY?QTZm9p&1 zi2D2t;3dZQRmX)N?T;@O7f7KO9G$eXOnDZU)Q9FK7&Krm23FK!-aB2~|8#4xYOem) zY4-&~FR=wz5YRqdQVGjURdbjXs6*Jl!q!wPWo2*FS@}ttk55a0@zF_?MWumfNGYk% zx1Ty3&Bg$%Z2K4Jq(F-H>OZa^p;{vU-E0o?nD#(a7SC5Gerkm;ih)B?2FHM z$*hyn@F4v2o%rPVw{tBgEFT}U8u@jasYZ_X-HiIF__9bM-77fj2di+U4yM-#dujR9 zWW9YhMF=+H*uGVmXx(%O}j+cm!sPp1I4Mq!fkI zs5YjXN3=MiyXiKG);z?#*+u6C)YMaN8@b+-meDGOSVkd7zK3`~S4wEgHh^iC zK=?hILTZHULGhJC0wwvyQ%t?}TbKMR|2+l1dV%UJ@^Y?R;WF-l2+s1I#1GS^y58!^ zYKk#AU%FSuO&i+(G$TtJZ50})E0&#bx!Jb?kF-wgw%M<2q;6Oz8D=;BN)udD(DA*d zyQZFy#RHGwzU{5fXEt@u3D;$3A|Sl|BaTEEczOYqsq(7oV%~wL?%P269hN0dQ=l@q zvkww%UCkP>8c*}SUpqN)?16IXb7FD(z|z|>Vu#?qUP;|&g@ysgZ}JyXMD#TkBrRT# zh&>(4TqPcqKG3g`i+JF8E7b0_qY>`O4}qzME z5=ODJtzb4Tqk!ux=B6QCY??^Jv6BYTk$I;T+C@%#i@o)3mAVGKHV}LH%AHlC;Rubz zVE244ZJmDl-(5BVU-+W2<1NQgts`PSyJ#SQqik-0(imgI<-$d{5y&r)1X{c(io)X> zB&l&4tg?Gt<7+l`EAQ&0-t|VH;LaULWdVk3oB@y?7Zm*_+TdIMV^A(Z#=XHK{-aFT*JI$r}}eU1+i3fATNL42g2=l zIfIHYkHgd-9XNr&YGBkCVDPnAS32%;LK#ox8pQhSyI`ve~rZ&#_tj zEj6wmZ%%1}xXqr@K`1~VJsD{KRvPi)#Kq0W2}8NTCu}ml#=5EqM3!52OFpc?U%!+v z?`c<#ZS>=QPCT^lIve%d<)yi3#l14R^HTVuN2cI)UA6G&rfL#ypkCOco8NbBI3K3T zg-swOVs_U5D~4I+@^iGyGC<-as{f((>Y|m2d<@7bhgjfxE4RGEY+haMyYvYSQ zo>BdcQ)6gGGHB28;u*bQ-#Fj#_zG={oMaLc!PxCnvux~UtDh=t#AC$R)!?Y#r4>sV zX|4)6Hcno@0u;TVOT z%|`!to9o(2y2e9?2gRj&iZRav7CK3GR6t1>X>D+r83#fB(XChEKY|jLA zuWJ4nA~oRKkA~%da@krrsMU1sH)ak;%da6n&5Mr!PO}7y8@_c-ZecHiAMPKDbUEc& zs*U_{^pl^*u!7#I${PGmi3T_*$w1_#)yJd3Gp`Sv((ZId{FUEdAU zY`rL$a$eav!sgE^>?8TBN}v)ceK_X>z!I!zNh7zznbyoTe&eXqnkV4p+q%L8OoPPf zVTt^wPYlUIMbPBMEi*1^+Dp z0E1tROfX#zYF@qn=82&@wSsw&_TH2;gYYXQuF1u!)yqmJ~&qV<&X^tjW7 zKjE!Ef5b)xKpDC(eo)9fi>mz8d}-<0E#>4zBT%Jt3ZS8ndrc+iR$?+8D_!p67F%AK z74mAqBv_fwq(W#4u4Ya4E#_?cA{B){oZT5-%v*j>wc$K}fvw@5RYkYnEYDNz#EL<# z>$Q}J+auCxyYb0A#VfWBw;N(iAC5NRA6$=|qDKS3iknr# zS~aRidym9!{#M?`BfHU7{QKrd6N>Z{Zgk8*a0gxAwuEDI5?WP`4FI%_k@Sphx<7S6*EsL{#;gOZk6Occ? z{W`^NO1ayba*;ZbOg-UNee*-Ko#_hsZR<9Xg3rEh^kqEC`<%s^{ie_&Cr#tQePw9J z^DP74-hnHb@~VNwqKlG~$!2%X)GgUATMWI@g<740g6&VoctWb@W`dznZBBVbjr6~9 zET9Pn*&HBB1Jkn(TT|Fz@+Op{OO6$8@f^RVrF}@Nh9swfEi35)!r(xB?6bFIt~8Dz z65s29BXnzmGMMl4e!{}xLiG5wLGkrr@g=@FvahwTNi!YnZBFiIVJGjJuiN^dO4POTY>y%T$f8>D3?2TssU>kxLvq=D=1RhBq3w$m z8)@Ma0VgGy#p7?6ICv!C(Hc+R5G_)MigkNjV{hLAq2TRu8(ZIE;$MhM!!4lkSi`M zT!K2Oa->HA111Cq-Q#aNe;UqwhnLR=GjZX?2$fu%vA?o4IvqPj#eI0Yy!G=nH8#|O zQ!?|xJYkP|;@&`yy_Zo-HH;|r1|R742Gb!MT;qHrMq^RpKvJrst}JiE{hW+%v%BGx zgYN!Qy#uLpMWQ}_jN`rP#D^7VGfrTCvGK<6y%MV`?${PRzGsbnHa^qv+kxNRI-|d( z@lnX$cY%I13YcWFB>qAIGhX`u5bfR81-v9=2y340_Ap)pSFdFuEp;Lp6hu)-(LKo zKik0<@NI74nn?5oH;>9E-8&1P$}^_Q)NoXM@Sd!PM@>UpQ)^1kg1?f2}P zCs+MzM4#P5$d#&!uDsYBjpbqC_$#)jo=55zl*60jwGC-3S>4rG6g7-gk%RaT)&`e>*qC&;SrR@92!y}&wF zM=V&eVR(NEgp5Tua6b&*S1=R&(n|@LR@?y^SRe_7to9*6CXVV-u^3LZg(u9N-~U2p z_{9MyNN{~Yko;Zu6>xB=0B7Ia4e-*pr4|0G{g`O17E*S$Fy7K@u-Z-JGRox;bPE*& zVjT)a7eS;IBDNPdFr{I%dMp9Vj!NRFd+#R}Cqe)0l5maj5Lp0X|KPl2ln03rMzR0s zTEl=c0FhV#Z>rYn2})mP(ngKra2Hh2>(KCRA= zmp4aWN$Fp$5 zXmIr0#;mf>*-XE4{u0xaKY))lk@=YvR_zuH??2hr3dTowtwwTpD6x4F+#Ur|V@zBM zy&xHdjS#Q2SGLy`k5zw6ib#}U-VHjFpnve@FC?_T`LkK_LC5@no-2EjIPzER0JNpgg7b$J>w&uYrD833s_It%n0(*CbpMCjC_Wh`^`~sAk1Zax zqQ^4ERU$!GUQm9^?8drV>&B~Eq?Ir48=d+V4GS!~s%7Nn*}#D}&+5brVZ8odSj!<= zIg984!ogt7enARoaPNWs)%x}m|KqMytVe>5bEqEF_A$S1VF5!Qi&?7pwo_y8z<5MG z;NqM8LYj9fji!W_JyEl3*WY=d56HYwk__mc%1#o+<>Df{aDklPnMM1q6EN zTi7#iLW`$%f+kP>knH_P4bef}ZC(Gc=bp(D2X_fUmu+D-9$_q409Rqz_M zYPr**LK}&<1&0aVK^q1<(n$=x3vU$SCTYe;v+&Nu6yp_ zuRU)&G!x1h{dE3kTXTsDk9Oi<-+YYOTR$kYZg*tyw#V|V3vHEp zlGl>&6<&Kmm9HCf?Zed`>R+f5rYin$;@PcR!B; z7nVQcOUnkg2XhDZ&3)`!?g(L;vB?pyMn!gb+wvFOjYsM+M#?olc|_1CO^<0L_n&*J zuIm0o#AnZ{ZBq>xZeKbF7gpcP-PuK~X8dS>g)94Ijl8d`$6~aS#xZ{ia|XBPZ#~^R zDflS+W0uvz#S2sW8~HQ^l*#>O#IK4qT=5)Hq#oAl_we4*L$;Z?6N{$k$+Cw5^A$2E zDAw%%c>3-@sQ>tXk&%Q%8F4~pWN$8HZ)ct&+Z~yqh^s_8JNsIa)`-9!(&AfW&kaJC!Gb7W#CrmjSJ~mk~g=DnK(Zi$TCWgnX4QDSkm(Myr;> zI%+weOk12i?cOKW`qsigpWFxSJ|1t{E7*}3F8X^jY#4A7o7vl};jlf{UlvdrsK;P9$z17SXrsl62xazU?{+WMiw{UOi6Tz^1lqyE^I-GUS z=>A4clHt?lTY54o-rt?B4Gzt@g%YXFcaTYWQoin=om*zD*D{;H;hp_;BCK&6@B47J zbwyyMF$gb@q3S7q?6g<@E8(;~yWFzkO4@4C*BH2^enrt8mn|)EfvBm|{`xhz@~szt zVt&ZyE`M;k^~vsA6#CDb7PgFQv3FjR>w1mb3oJ$b?Lgq=kO@oSTm}uGJ3a7g8@C300d&Jm${1vNFO+8Q4$_^h5L|`Mebd(kBJ4o0C087p z#Oe0=`18B~kg?5!HJQwN;b;g^3A_<1oL~NbEJ0Bb3pfoxApzuYSaJeAdwGr6g!vG2 z#nCbi_;7wQ5>sa>0Ol6w!HiVo;&`G;z3mprkShdvdp*)CgwJPdFOiq1$swEQTCV`> zE`5`z&<})R^Q~ufk&IbO{q_vlb}-=yOnaFjTNEyI12SJAPI3CB_;tita+h)YsZl~d zOE>rTM#eqrEaN(in?%e_4~`o0G6qEt@f!{&wBh;MtlUYDi>70RZ4=#r68=`)t_s5K=YM#hz!$LE-l#KWGKpk@N8OJI8RK*(E=Y^%jpbek>j z)|(=46IZMSFv?a0o5U;}a_6D%q-B505;zq-u9z0pJ~brs37kj_#eXBPV!N6D7%V>8 zSM=6@qD{dA7|YsO^O{5B-gntTA>khi!hXtnX%^)clJmxB$w5BG9x(3v#W@)SYZmAm zD_-sKX^d{s)N$ZJBW`&OH>z#AIS-j>d!;Z(gIl-q+Bt6QSJZURkNK)FI=19wpw3$% z#2$jM?oadAX@27{hhklEKM*MY+ZL=u=UMRBdQQa$>c1f4r=+f?dYi%;b&!@UGIH1K zWUOcLMc=2G4w#xa$0Z7k9ZlyVmkjiop$4w%fbXJ=Z6Yx|bRyR(fIUW9dls^0@{00! z?VgZm5p<4m+BbOk9=si(1guda==XtV>kDc)NI3)}aRCGku-O7M0x*dMC;p8c00IBe zQRl?Me;?Z5vO52Xb7MOI2mWIl06l>H@4*H1V}ZbEY~iire6Ac3*IC5WrMHFG^kxwf z245f=0?lg$YnEz92YGozEsQju#Z)+teX@&-Z3dbOnNiiQ*AnoFkRm2Qt)krN;EjrI zP6aTj0p%6@~<8c z_AtkFbp_Vqx(Y`J2>gLJCMm(q4re9XyJq@Ylq7$7V=B;8q`S!1L`uR_u7v*6N7xIX z8E8h(ILqZwhfI^?IQS=wmMc*_wQ1{1qzOiva?(<=50#5D7$%Ykwg#@W=!*3+ZyVLa zFsfC6bcyS(LtMbAr|?_72))d_hs{oFEyLL1!;JIqi449nx#2(=P2u- zaH*8S_9Gp9P)(z}eDo5B^a{67wxN#hjNhdOy15=7Iy~403ew-rubfxQ{!%Xanc~|_ zUYwKRBP4~rTlfKxL=$tP@_F+Q(M6zm8 zztisr-nw|r7U#mzMvqHrlZUOQ>66PEORSX_2wMyAiDN508t9Wxi-8cGo_JOkDkam& zeAP<%_q`@J^GESMaq==u9+$%|sCj2etRi2#YxShtm+`T*g66&v9bBn1w_DruLB*`0zy$mX?LUw`8(ubrqYq64u|1gGjZ4kfa0LU4m z!?=$K(%(;!4{rHd%G2AJ_{i7rdR(RD6U-9Z{t99X-V2X< zERU2_szbsQA)XVBGmp(rf{MqwtHq~0gmRg#>3bk9Y3Qm^c07Kc3xy9pS&}KHsCjOP zw>!Z^@Bz;MRkHxp`=(R~uWo+q{%7J^#fu4!RIz{~)>$GefEetSsMp;)d}mMZp8h(S zxO3H`Uge?VUrqV2cs2eNig;nW*sy5LA7<1a-@j+#Z|qwUK6F-M%+49m!8{)r;XTt3 z!ckG`nJmJdhU>HkxJYBv+x@7O3(E@83mxlFnu!4>+L|=v>+72_({vxcaL;7~;0$&6 zm4t-m(raD6-}IY2dt(b0=uf)}0j(XkLC{xScsiL63F8QwHD#*LYj1M)QC``Til^_V zUVD;SXO+)UxfkM>0X_SI{#oWGsj|K61SVO!E;3zsNXQ<}J6Z>`vUc z*)(r3Ht4a$+>DgsLt+x*j^p%8d!G`qQ6@n|LRS3C1F4Di`-_LZ!I$NEKL6Mq@=$j( zHw(|c{ekr1(+#*^w}MRWoVpa7I9tR0!`)k3LMai~*g6&hdAol+^z(&h*gB1c?rT4l zDrb2jQOQxjjmHgV#B1~QP30+zJ(QMfV&_c)msRLG@^6W}#aUnoZzc6&*5+2hU+%l0 zc*i|S3Ui*Sja_#OddM=0ExT;?kK{ghR2cikXToNO4^uu4rFW%({&)^i>r~zE8|gya z82Rbix{(pIE9f^yMx#$%b-?uQW~CbXX)2y%69- zL))DqO`$H|Yw`O9&J^a&o_8+MrH1*OxwPoBhAh+Jp}2gTEbY8nD+5dINv3@Ji8hC~ ze6Z&1bdKd{^s2ECNJ+JvQoAd?N0+;tn_!RzeIa^SJ*Fvr0Ox`g(&u{70Mun5xaZ}pGk+W^ZvYHk2NW2!pu+(Fczu)5Rfp`{HauBJ zF3U<;Xn)cb08&>B8y>KCCaU~#r*!+l#L_cA56u?TSZTK6< zqxURru0!$a57G&GW`;)&=Mtb^=vb%rmhM!=!uky0E5`ZH}hFRCOxs^oqX&Q3!LGqTjJ^Nh9ES z<4{PgE#nrs3g1Ar`qVr@(CjvqBM-#bMA41wN7s)NbcMsay9X|TCmeaHZ2s_8AV)Mj zUZvb)Dv6?^c}@T9qR$l{0Zxw$j>YkG-Ntfjyt^tM_HC>oyxVBz_QMKsO10Twfj7_9 z?w(GywJPrt)Xi(#!dD;Bze9n*5o?;hr;8JhCbaGI?nl!b4dURSZjupfZan7rwF(@5Q28D0p!nn_u_hJr)U;} z$DNwQ+)Lh6Z)v}fRe!34zB&4qWP0(23fD4-)iUT5Wy9!=?r6W%Qxot9$%-Qv(p^E~ z_)VOCjbHiqcii>MHf_7#{La)>ZbzB;ywA*Uszd3lH+B`elrF+LzAx@sSZLFEE?>Ij z{+A7n6E3SFEJ1C?JftKM2)r>BM&{FyDsuB(Sd@n!Gqk{0F?3LKB)jE3KH7JP7Q6MN zt2{nrSpo6(k-wRCIlG3jHe}vLo$IT@U$BG{V>3 zgU8T$8>miW}W=nF5jICBQD#4_oOCZNZ@&SbdnxyE0%Fh?9U1=C| z$-axOYdcsp9hX*&@4Db-K3kiZl|O@f)8QjGvyi7lr_jEF+_#Nem!IwepV+2g zI%K6B)p6H_KN~taJ(LPw@*^xxgS(Zp&&vcT*%eH!cLNU;$7ZUjpn4O2Qqkzpu_@p5 zr?FMV_~&c8TgQ!LBWZSgnXV2Fdwh^L<+lbmJ*G7u_*HVb!J6dr4seFTaom;3J~Aks zh#u@e637{qJxIwjl$J@Unnr}p67UOaU4j(x%G>jr#3Q0c@q+Zmo5M~^^Cg+T70=2V zo%dY)hhW_!%~kOMayx9aXuan}?|&AcFw#ns7+f}RP!{F6_U*5Fpt)=E7*#-v>V z@f{k9l8?qt`z*Adm0Z1l_>QzKw^T}9`SACurcUj(gwJREP7I)N)Yg!BOL@ZHMtC9_ z)g}>)nCF46`h%4*K=}$XpA8(8+x<=L_Xg3zHB>sF7Z6>6w51RZiGE;Zw0j%Cqu{no zO?2LyN)?O8sPR}_iSGY=&Y}K`>M>sd&Ka)ML!m^~dZ-#f=$=vUbroU+VBP$RK^m1| z0R!E>=84I4?EaGz*ecgozu}^j+7k;oNZ{%y0ZtI2Zn>P_EMD7>EAO~W_F6dp1=dsx*z$w*-=E^Otqk%9WFDKpj{Kg7W`SPVWmL9qUm~_`=_(+ zmj|W8T~|qCzLd zu{+i{=p$iafI)6zl5ReXe|Dyu#Z~Xwl^g7TO#IzoTP*qM=-AX@RX|1tS8gUN5-w7< zV9aJT?zE~(B!N+Z6X?_nQ8S@9C=xcyYr6U@K)R;KlI3Q)nv@26Ff2=$S+r89w=KWJ zv$dR*CpxQ-3}WxsraCTd#aqZqe_MM}d9;6~Z^NVsG)S?A9pl3ft{tT<7%pn~Hv>Sh zCX`(VU42iKYO4S*guG#a?_oFLI#7eSp79BLMmX#j4+zyWajcIGT*gRfHzjysjsA?K zN8dssBrjN)_$;_w{L4|pOlNZG5TF;`jrk~w{aW+W>dV_q9o=ZmU%ksshF$wmcMB#ga^ z2+13oqRhWtJ6-#W`ZA5;*FTOs&Br29$5RbpInG`B0ZTc7u3( zQh4SOq}+tm4u1}$Oh@bKW7P{auT^&w7jZ68nJpBQl7822LDyp^a`{5db?Epi1MQ;B z;#4#QQ3AoPyQ8OH;^-HLLt%dmw~ZqRuB7x-jAK1%t*^>%k{}sBWwyS0!PDcB7t4PD zmvO-{{@{g17+nOE*E84YqTEiU>`98eJ9J1&yAGxt;GIExZ#fEgm}AZ%!XX~IEfOfD zGj9qi09nv}N!dNelhOK{S#LIURo36~yiM4Ca=oP?2^8q`e{l7t$l(1Nu1YDcyjG2O7@6V zBv)t%)lmOPF;)EMB@Q8jQM0F?v{6s928xCQ8kptIqwvG~w#39ouPpE+DB=t|s>+lMhLJQBvL= z8`h~7S)CHcnE4xb8^WU(Egt`^Pm&?ZFBlt1<;wIe1XLFA3SJPPVKCcWBG7#s`_cCL zAIXAgq`4hhV9W%FPky2w2{R_#~2o-F*G1X>s%-{zz0{;Q?-vf&o}h| z`oR&`3t%mt@zaA5F#Hle59XkH%*2=Mn&Wb(-sJ9_`{O?*-NniwPO$AL*8KV8rV1Y) zUXRbE{1N$kM7$JV<}&ZpVHB;XTQ-h6Ky7z9UL@PWU)Qx@CXA*Vchk%|TDa?%|j2I+jYVyw88{uIQP5S4ID-<8`kAHU_w@E)CsH|lp_+s&Z^h9J}y z!usWX+P9z~Wom-CeNrm7%iiL>ei5?&7Hg&ie6C6$IBV+>-*0+qyW%b{!u1e;91Bqt}7J0nYAPf-s z?Bay*$7Uq%YiimcJkZaINjk(@5=98rI@qON3+<#;h_bI*GD z&H@auux8Hlb2skZ7Gih}<&MceHuv~bY2oN8{`JZ672|}2hx@o)ZVdJ~l`y+AQB<4Y zp8xHjyYSM++kVD(Lxc=J1Rsik6rf&wqzjkDow1nSD}JBFTFN@8oo z^=7VwCXCi9+hVS*3jzyvY{1RYKgVwq56krH&BZEYhbBt1YJGM@4JU9}weJ(v0p8*_ zHmAg=G<-f8_l9x5qTV0UNjBlx6Hk=gV-!6@-u{Jo{y4nb!Ni2h5;ujSu8z*CHS3mR zS68z3nw34sjcUBSR~xNwYOd~S4}~WT`Qa}nt|6{W~HJ&HgJFDmDtMV+Ud%qCuo}SZFa6oN+MCn|* z)BTVkxbwt9f&Ga#{>GZ+_$os4r}JF^*Cwok4fo5#Ae<-Wci)B_)Q^FGqx{f3W&5Gl z_suKRH~W1a96RsJ-%<>EmGW>t4Of~~Gx|;;V$R&$>5)&Mg{r)%j(|iSgU@V!P|3`9 zQ7i3P4KKmSr|7%KE|=?n7>@0Kuc+vVxaRSVR$oi9YYv}e!A?@nmHSxFdEHh`Tp_$^ z+{BlSlZV!=kSWrSVR{Z;@l&_g`jM#_V!G4V#G-NND4p;C3e$sl1I`;@6IAJ&b)*>n z8e#aMB=Mrp_)(<1%UKA>oYQ}d?ihvs!UMqNZlPowv7Y-kbskj1SWqcYC+1E-rKujd z>ShQ|%4+sDc$P!8kk$C$E1rgnqNO|0N(d61juAt}Qv6Fp(v0g_*ApB+xEO#RS3g5# zT1&xu{ikzEDH?w7uR#&{q1y$Hr9WQY zLv8iv$E$%IdTh8Y#(*kP%+V{Gh6C@K&NGq@*TBwZzTZhvI=te5h`9rP5 zt{Rcx3Oy-Boh(7quof!5x|j%7FC@9$alwFu9~+J;PFmn6%-9)CQg!=)An>Ex!OU0Z z66kA&?JU2=th*97GD7@hTeUSrI4yvIo0l8E9OI6#5a-y)?j6w8SO`R&$d+I-)76Rw zPm@wSjgYrE3vIO1FMV?VO~+_?xrsp%BOk`1Y$uf`?5OmjZ0>UOEoIMRS0tYOHxr$& zy)h!D8VujROp8r86;q}V_6)DwC}oXjI5ywFWBFG3!F9&Y6s_K>+yjirT7o#wR;w7c zUOz!643J^D^7rcYNlaK~@kNhr?-dZ4Kb2r`z6B4q_925=j-K0Q54C$owp!iK@{dQW zZshGL*35jGRv4`aa3PWkR)Ibt|JZad_%m@aA9qKMSb&If^*v20F&W@N?S9TgefL1V zRWEUmS{Hx&@x+Bm-;XNtah&$0gLp5fd?Smt#%iDJO(!Zzn#x0b?Dlk@a;Rj!=?lUh zj|%}?iWY1wxirdWM1ePoJh(&7yJ=!^v9%u6EbH#~1%uST6d#fJgtT<+YCrkex3A6C zz|mqM=Kp5hkCSXbGhU-!$=uE%`JS@9~^491Bklhi)!?+}lVGGpB$-f8aZgTAG$-(|44oPma8K+{jiFs= z3N)Mb5+ETQK{o%9zyqqjzP;t>F3WiWW0mp_XM59zoO^fMJDdcauN28QaQ#7G0F!n^ zuWOdXZbnh=h7RqIq{S1ncVvF+g1oQgHo`a6!GddB@>Cd99QEYz#C)J-R)!4!crO4p z(O7@?1_O8oVRASB_I%&x+{b>~@)Hh!m_V|z8f2=#6Poxn?ga9&?U$+hfk>qH9;!xu zBCkr2L%K&vQ1}s5hWsm96`VjZV@A?(EJ*f3ha>30(y4vCstH<5os$M=G~pEgv)D4M zdykr26 z>AM*Bv^4HP12@lDwVBMfj}z*2eG;+om+_(FQzDU^@W1FH0;)5 z=zd|3n)G*=q;KKPa%SJocVg>EN!5#?gxC%Eq<;p53YeB(rFT5|PCzEU>I$p?BHEW` z&op(~>aRlZCD`_&k-Erh>e4qygG?&~mB&jY=uPWZ+;dKLj7KM9g5`3q+}ztQ)H^k$ z?aGgN*hlGm|ExP(C-KGGh%0~YxqIu`7%j)-u;xIT%6evoZ?6Z+OqBTJxZI;#V)t?= zx7Rd?wUBa-#22+>9%%KsRduN*lkb&j{Ez@SNCCHB?gfadz~YaWg)+#10;7WW*>hduDN5e6K=fu(;3&cHjB1>*bRv5lwW#qz2#g1NY&KR=siosO3q)1Ct=d`V z!@`c0zV(vG6a!i`sIHOf` zBVI8@h$bz4vay!z^w`izdxxA z?`p1FqHY+~f6(QJ@xPyv@^^mW`!S`rwj9$iZBhcFP+W_B{06(&MD04?Wwt5N$hW&^f94>8hGhBH)28NHtDZmF}=J zDl^2CI#0%x2weytlCoLQc@pLzBD;;A$SW|=wqjiCY_3MHy*)GNUNdBULU!7{Q_AZ) zS$g3>>YAnRR+1@$D z+ASB#2jpZ1nfGmwQKAK9sBtHC6y_# zEVj?Y*3_`>yKOQ;PluEX&og!0541DqI6&mVW*FN!bI|%>+~p)8CO9% z5@}*VbgJ4cTCt9JJ%<{e{Jf!Uq5P8d5@-M_R6qDBeUbe@IBAiv6W;qrq8P^2M9w1l z?bB%26;ieYoy)$uby@l4q--C&wYR%E@T9%>jMU9WG~C2MStFY=u(myJJ$5bptP{lhEpKUPWuq#0Ml z6IoSn5AY%+wDiw+PSP>_BvN2L34}&am9zCumGha~Ml4=)ukgkDx$_0wGJX6F!m!zp zDY#Nmg6~C-!K3rJ+uOFW%85MORLJj;+Us(K+BPS5rKj5QAG=5@0;86QuX2aW3c!%_X7RUvX`e8}UYXDQ!O)28x?PPw)fM z>!0SV6-5SRel176P@&|i{zvk`W0YXFCcF71r?H`RG;goS>FHh+j*pS?9m9&@SWWlo zeOSwvm_h?)I^X$%*bYNTuU8nPjisa;{wyxnzHgpTlDKiPS zt&ww5_8cJerQUhmCr#G^d{~l;2(z&6n?KX8^WAD+isI;i-L?fp-~yyviMf^s{XC)$ zw}?qL4Y<`sE&q)O{o=n3 z==}Ce?-`Zc*eU_meco2}zW2)jvI4}Gebk6Y4moy!tr&=;MUY&sSMD*4jFQ1V+`BL! zxtt9GZSHGhmkR{>KNVhl#A;NC1p#oEuaKsIfQsutL-jIRicLyuoCSXIKB87YZ}j;G zGaq*iSJLe2C?KkRJ)>owi9twqNRBusj(Xn<`!5?7tUO-?%I*I|TQnof35*R`(q*(B z7$#zX&5yH{ve6OQSm5wjj+uenM!Xq#pH~a~0BTmxcFacOTOrS@ab_0Y-8LX;MP#jc znO#(3P>{WCE)MmnOW;A{`i1U5a{C_J=F_&tFHw63xqMEtB34ZdPdTQfn3JIjM=48- zc!~s~Wmj{G$1{%oH}+=%;uXIiF$Z2dYeDFI7W15Qe4QG) zY4yd}of0$FYiRa6SLg}fXWRNpYIJ!IRR(9Tn%gx!@g^T6%VO#Vra@!|Sy#favAN8T z6oMx8z^qWJiK4^ZbcxB3<5wNpul|o+~1H8T+trDE?NA9t{e8Uvz*v?w= zrHT7}I(IkeIT>nCq9e;)0C&D}6<;zJID%8q8Lgs{Q;Vr733&j^w+X4~V!L?)OlXYt zZ|)f|zIck2B-7F-<$Yy%hd{+uZo|)`mF?Hal$9M=-gWxNo6toNxIn*TACqD9RIWfV zYWyvRp7f68>4@soTf);q0j`b#sVhBfT}u4zrU@xRg0)gjLdLpiv4<<1M}x!JfkCl7V{Vfi z|6mh*?sv;;xhQwaLn5KmS)qJX+X){U1obQQh++B zMtwvIyAYM~K*dVp{zp;{wvOP!Z{q|CC=rFu8MNxgw^L`LIQCD9+3LMV3bjA9n@XBE zoBkqj+2Uu8N9;3|75}5hwC{BqTL^1zwJmCtls~BbM*;)OppqBhGw0ZDD|^l2lsXqs zZnuIe@rL`QgfMAdW!s~DRKfMOovmyg0E^C1-(!IK*7oyrBDEJ-Dqe~0-c>5igneEY zdNe=trF2<`l6`{}{d)u<<9oN$c0Kn56aM`l$?#8L4wLrt@|4KCTQn~t_+;}IBAX?_M{HjunhgBKG=;2MSTHFdcPSTwxMYwF=0IGD9`^FNCL~pA~)+Ep)(d>`R`woPE6e?Hg$g zGO+lpgts0xDzslpfkC{CF3;8CmLtkHbDhPVHMjYhE8;z>4Gvf&jx{1U)q)PI zFzy)hg13~3q6(vbsg_2^!BNmRu0~b)F|@4YGUJ7bmo%tmy=@b~OMVh}y})1eIR7y@ zjpQs?jNqsL3Y!J0CE^e4fD%||nDE43A#nxt>@NBFbZxk#2?(_lfK;(Y-4TBW#L*Vn zCsN-75qjBZJtL4+$b5*CmMLJf9*K-_76cdhS!e}7PJ+$Vna%%D0uh z75m$~XtiyN8ZArwUqB@v{fp3NF{bZofjSU60NZ?TY-a(mni&vx*Yt&CA|&~UaE<;G zZ)4Zp<7xexH^urz>$K1UI59Eq{(B7usp;lOhuI8_+HI_7qJMW$xqPu3Oj6p(;dJXh zg184VV^}qmx>7_+_8wT56f|Yxz87*=elw%w(A-*h67+P*N2n%T>J=k!WMX{{33J%c zJ&bI)`jE6Hovw1Cu(NY^iFI1;!STBvjh{cW5R;^`((X)ptKIw@kl|cA(WCv0CguI| zwBk?kML8Be+5Pes)e#oS?R`hnr(`xBQE|x#Nh89WGc6fq(EeT=tDLV2-|Ch?5>WNiA>Qn zLFF3Xsi9xVXvb>;!DBl55Z;26YqUYnekjmVyWCE6B%09#jDFYB$4)&UOhAN!HIXCD zzoe2xVc9FH&TlO8Z7cGxCU*X=eqPa{5nOuz<(j1p=w)NAv+PiG0Y;!9NlbVlB5{arap;Y6iNY;#hmfk1p^%J@$6PnHSiejsvDI*Uz=(g<2-(ap+{O z1^J8}0D6HaWJd4>R&e2W$_f-+lmwPtiPUkoWp4|Gg}divtCPKNcaN<5eG)ZtY;{^u zwC=6@`@%?c^r~C&3aee5pW2bH-tS;_y^E2vvALbHI=?E|2tP27)EaT9AtsSWo^7-X zexTm#E`cYzzCuRWL6f-3K?e&pHugJZHWT>fO;B^oNwR09a^iWm^=8kMfEoZkHEMxZ zMnE>$_L(BDJ&taBQ|E1+tY6o4^^c7+ufd^$YqzRZ%*y4X>~a-vu`rp4j+X|O0o}EC zVoxW*nnYa2$o;RY@4S#mPaO{KJu$-xwv<%8)27<44SAJ)CoO?VflAw6+o&`uRKGf{zV_pa zcurr!nH%uK#8f_oOX()DxYByB7rlZG705P=y86v3TSK;FxTkT-RG(R3@JWZ~EI09G zh^O%V;=a`-9J@}y;neYPJ)gtcrkn7)y5|EiozKWaoHmiz!+x$b&ZPi@4WiU{_L!iw zD1pKPZ*&CueS|*0zpG78OwPlYL5P;G&8>jI5}++fA1F(CUUa&sR9BK zx%xtZ-$-c%gZw+f3^i_!ha4A?MLATsve$xD-?rPw|R=#dDv2 zJ{lc!c`npFvPDx>{u>a7GAthk=C;1iloB3H)$evNTKLp36%G9UnA#sS&PBZ(R1#7M zXpz6+x=gMxrhyJ^l1$RPjmuZ(U!sExIdC%m&}aOiMS}70HtduvClOHUyqgvus+Awb z>UP&#%Q>VPbzc2Lu!;Xjux&ubK7sl*e;2>D3;vV+U>^iI(eb{+sE8utjil&uaXj|y zdlA`u>U?73FUAY1YNy{prsH5fCFV)?1`n|Q9i@b;=RI0s3hewx;tRlw)V<*876)Px z@gm{^Q`=d%NZTBAq!OJ=`Tq}@Q;$In8E;Lr2EVW=m_M7SWWA=|b{14L67z}|>u2S? zEV5~AfFp%+(La5Qn4_Qsi=l75ul;8Lksk9IGZ^3Y(+35nD}MRt{&b?6Fi_KmnOa(o z4>1Bkgv!sTvARpN10;L;1?;Yk=0ZhhA-emx66UsFyS4>-?@b~TtJRiKoIevNQiz0!hS0VHI!~-pTi`T?mPkc!eO}3oW605wZ7!~-IN~3PmThh!*=naSsyRX#- z#!0XLk>nMfzADXO+LPs z(Ohxp1jaB|rieaJ|8M>rM4c=nHXF`x)%15(0Bm)B$qI*@y}Cx&gERlEuDKLnQ^i!R zmVq8;ikuxL0hD7(t)w4T+yFRCVZ4GI0iQiC>;pT&lL z#1n9m=A~SFUk4*m#{sroIkDx2|42kOjz%2I`!vfnpuaYV0Jp>A4*suN>gDOY zzcI&TQ>G65R`P5P7*RWTkO5n;Z4BB%h%c(CCX$a`AjpI^v86;Kg6=G&{`6IHDEL-6 z@wYIUnzyd5_B~5DSgZmg69F2X{{o|d#s$QKk?;!mkGwPy9QLaERI^xfBBt}JDm5=t z3qg`tlOD#qPU77u8Luy_cgOWI+9nmAKq=Xb$fc1?#Yqp>mHg!DWX3&jQVoxM$-&Q+ zT{T=Y+5i%jOw;xGbws%C7^5?8-%aLUCZ%ysc>9d?-T54E%{!2|4Yq&niW3kMAwPFwCG{Ll3it-B(RYMfC88;0duHN7jKS9W+B{JnSu3 ztcGXAdM^Np7pnYO!|OoC?4}1PZ><+7_pozlNaB@QuaHb!Qpdors-q=sk>_uIEzQzZ z%1U`mP`ST;>HOtdW?1-z!?N$MSu8RGXu<})qs9{ory+^$Cm2^&DVZqGKj=I^HsOxS z)b43<)Jke3ZGxdhuBfaxeo1jE%= z?C7Dh-M>4X27s4e2;L~TRHL1!#C2x4?j}eGad9N>RGdl3A#-Cc=A@n!_j-h`nT=>% znPWR0>yH1-y%s;66;;i<&@|`s%rmD_gEhnCy5+^iCUOSaVtJW{vMz%wHdfS`G0*<2 z3nJ{TTYWATwYh-PH$D}{eKU}ij_J;OMjz}Wbi|%Yp4})Wo{gHaTr(^2_u2tg>_hOD zK+d7moltJ6t2yM7zn9K0WO5Sb0@8QMsl7iQ=g&0Y4!62jktLW9mzQXd)>Q7aTKH#V zb&OrzA&uWK=T_J}JjGN}iqZ$MI_nMdNvoFfvn3b4#ZSloF&11B*q5XaPwdS2HT=OU zsUqL2A$H8U`U=JHHAWXk=O|}vHKC@hndX_0Nv}*wbOV1NWX-fZ+dWl@j&X-qJQ{CF z)QF4p)~U?u@W%-$#C?AG`GCoT?ImZs*RNek+K!*eaVBF?K#qUW$qls*ZptC&gPmUD z4t5ec2J`^eviHICSju}US&mlVc;J4K-7P5?cxX7il@P@SHtDl9H^>jgu(bH9=4&;2J5|j{5hUzWUlJvqsk9mR9 zxqj9itf5`CxXS8Kz!;iSfIAn;)r4C_5}~{&W}(xipw>eYf(1YSG35mOAFwtSWMF9*Kbz8p~X69Ugvc#Ob^xd)fBfgDs5 zkldy4*yHt(cuYl826D@sB|9K*^q%XmRkv{*Va5u@K+bm<_Ib^f@^l1N#cEoc=vfI} zNopcE7N+-qn6NfG8xXossqVm5nuRx}!#fnRH8#)JpHYaZzj5$v0o&m_f7{foD1OIU zmxk_>!1Qts-ua*O8hm!}Pg~Jw+Z~3AS&}E5#)MR}>J^>+we7&%xqjEpSM&Bq6?V%v zy(DAbY-TP$ZP&X%O(7)V6BEyQCNLk6*9BdaB#*upF`3P6TR{FiUU!_X&Pv^C zYWzbZbb!QaR_L>-UjW}R8^V`Qt_|F30zGwhams31$F<|B)<02g^wpzA&{$oB0I~6&aM2 zH^Avfa(px4mjQ~)zIVK0L0#Z zP4NGg_XLJnY~Yv!QMv6;_5qF#9);?jBzz#&bm<^FY}Y4BG4F4j)M6#I|F4z{!qL#9 z)AaKe*l*21NuYEjBhWO_0`$}8tuNqNP=zw5iDyQV;XtZ-Zjg0uXP&@lABpZTh4qa* z&bcF^@4!Fk1PB!kumLz=(wvo+>FiSx~^ni*g1nK0e`d_OT(zPln6O9;|1Cg zgg?r-T&l%AH#sWN$F2sP9xy?phBzNO=Qb8}4VL8ndT_+W{|>Ogz_TgriJ zv7=||svGaC0{xY1HT7F}_ZS;@QxAx12jwz)SKd9stuFkqd7kH2XqGG$PhHhx7pixv zi0T+z*p|c1%S5?s-y+^JX1L;H-#MD0x_3|M{)-^i=YKmK=Zynocy`Pgr zf%R2U1-j}_#=2Sph!;B}*_R)4r7D2cb}iQA*mkmm!ourT^!Vk{8D$XEQVlK`>&6TK?6hsa3Dj4FbNrxn# zp|Nc7e|z^o6}EFBlwENwCYO|lXnif`D~$q>uM!~D(w_VO-=r~+ zj|UiUh2A8Usw7#T5CzRHp@D)Yx9jR=|6HzujI`IEfLH4f_))Dtp4v?>ai!%_v(wcU ziN~e9BbDLAx`9Df5yjL}q5M(TZuR8vk@^aShz@rNqM*5$bwwuToTl;8lR~4x$g(Q@9E`xMP^4 zF*#C61-zmK7us^y39X>dubBoC3GIQ`xHHs%3V=0N=uYirIq0B6;yb#a$k z3`pw%GycQE`*{B2Vh73}qYrjrsI8NZGAq9rhKq*RJV?{WFb*AHb^l||v~iTG!{X%GwXyGMxYoo2D`S1H?#f^5q-)5Jag}6R4r(KP^A>_PI{*%X_C>Q)f6~=F zd*TG)ac(r{+ud7gBnrJrjVY==?bGm`+ky0u)AW{yAXUAIKtVr{&Xo-#Pl;__BfYPj zRrA?wbb08Bz-#1vpMUe0@+AVqe-{AZz1H^`Lv8c3@XmUX%TGpShSyN)}d}ak^(n+rP-BQxrp>&vY$z%CM`@n&$v`DYpW(@~k%cIv>2Dq20kf z1sw>*``io}=>oH={UdZ$pP*JTJi7BgYtjfT?K4lo8jRFB;k-V zasyVHylq9$FKCl@R*xafMtJI*`%bj9%E^CZnb6A+El6J(Al0?9T9;VOP^z`oj7RYw zY9Q|EV$=s>d^Uh5(}$Z3GwpTfo?T_g(v%brJ!{<1*rXrW3JL#PXMCVJd^5zbF-6 za0eYpS)(l^J$<{-I`{l#$CmJuA*Z^A=T5?Q$@^ymL1kf^g0N@a@OS~NWQ6!b10SXz!`sQ)O-=tkezUMUH z!#&Ik9Ft0Jr95VNHmIJ>)A-4K>rYa}5?PBHmIb`gz)}mahc2plYMZ@+S;NVPde`SKy?{9;aQ5aZ^=w~LRX4f{ zZEzvmYUKNamo#2XuAgkWzMXCag*BC*{uEz+_9r4!*fCAOK0_4>^9`Z}YARqX*?m+w zjQ!He_OC0p*{555UW~SUn6ix8hUT8(ZMq6#D|HadfPsvju2=dtB=(H(A6WxptWX&y z4g5WZ;B*O#*UPN8h`h8MbsKdQ$GwJjg)e8sNRUziETgCdSSwLNJ^B&^W?vrtDqW=6N8081Dkje)+7%nm6~%3KFXrfvU~ z8~=W8{Ld#gYES|&7~@N`Oc(K#N-uIl@c|fvD%_=rwfwrb^202wH{d1%grk3UrQfY{ zEuwHPKKIG77XHvH0X_G5Q|wX0DbT532dRTLRj7sNhHOH2fz-A|JFcQ6?ot6@UO4*^ zX(`ae9@6=zf&51_f=h4!bm8Hex7Wl|8u|PBE1|3}?!ARflS%iO&mBbZuRoUgZ>AWz1D)o?ZT zVsBRB0{XSFAJ1lYD(S~0^J@UlK{JR1Sm35_-OeNwF9t|p|Dxw}H! z*6oR-kE+4USr?D^5X*BXrMxI3MKl$|VP7*ke;!KS(D0C@)%WaW%`9bgy-S-QzN*7x zGzK(!%B4=!vvaRF#9}vk3fcU|arOyrPM3c5B*EsXNO{9+Ww; zF@Ax`hBVLK0hEnx3JP1c3g4XX52u_r4^$fanayU27bzfcr3qS|$Z+XRXmpI=)^m`U ztlMV$NpV8o4+lB#p<0vW^pTfQZSJo5TAmJSMvExs?q+-tcrioL%v&V)% z_r|CTOT4OWt|9De)Wau>k-kQQgZh1WOR6FGaD=6?SNL?3wDN4u=rIq7-*d*~ng2`9 z;x@~dmcF{5e>M-`pEm)uloH*|#c&2s<}Qx_8_V=$KK<&Rop;ETU_}<@(>x?LQr|y| zFG%h0f+EyrvunVqze&b3-khM`-|JSe>n%$^V~oDOtItzMo@ev{4bXOeN#Tk)lc2c5 zhZ<8q>$>%9)Lh3+-zA1T0PlKBoFD zbt=QHh;%COg~=C;g8;UGj64fcFy(7@7Fdb-DO{VJb;M z5?_=F4!bv?>kqPwa~>}ynJ*urRJ~b%RqVA`xF0+Z*vOOLgFWDB-OD;lYZ4L~!tdV# z8@c8=tH;5%e>RxhV8g0g8EIwqREoKK?i;b@@sKOkT+6_cp!LnPb(e{%5Q41Ui^ivb zil~IZ8Pn!CK%|b$YaZA^IbpwXWC$S`5eiDXEO0)=0$;8qt$ps~3F=Q($nKQo6*TJmzZ;=ZpTAj-8ZbAz-Q#H{oLB@xLFLKXqV}BV(SI_2z zNt_uyw{8V@p*HGZoCh?FC%6brB6XoPi7Y|?G zzYkF0sKTeus%`_g$^9)^{)&O~VVa%wirDj^60e?VPEb_fHtL+(W=9c%uVOo?pz!A2 z+C3lEtN52AKk~j}1MyV~M;J~7Mpai*31A(YHd&H-)tjC`ea=^)hbvC%r-2L#UvtYd zWSvWgo{{d4%VVV6OB>U?I6zZ%zJwa!>S*xU-xZYz+5w;6GLr_NYNLOTa*tJv@3qh4 zd+3Lku7}SyfboM2fA>&Q7IbkNbe{u74{>m{CBEG&0DxEZPRe1(K}F5CRn>D!u6w~H z+@@7oDgVT>6X5I(z7MN??O#l?6u2OVG*koc!9r{v(6Z08<*kF+zyLXz*X2rBg=Q!~WaKd1IxU;~o+Wb@G- za8}^9-?7HA{!}0hn_+&zVo&JYh&#S$R|R#9awn)A7p!`5&E~Ad{u=f|ku3SwHPb>! zmg4K`9m#Z6v4G$_&H=nH?X3bs45(LLitSB`q{wD7B*)(JeAaERWfZJ}K|f6ZcsG8| z(;GN0qltSbCCyfVh;@I};I7<;%>nH53LNYB;j2hwI@KX!UKc95Oq*{wxKz3_lp&(~ z@zt=BIDV08D{qA6wLQPqC>`N`rD}%kGs9Xw!WH29x|wb^ndwow5tNYx;%hTje zv~_$#@;T;XWZcpaMR|an#FzE@Cj`Hb+RQix=brb;ir;chx;z`NQkZ!8^2TfwQ`_5a zzzpTtQi6j@oXr>G-v2-dxlb-qg$bWX%cvhqUHv&fucjxFzsVO=kY!8SX(kFiVn^kf z7KLQfKD>k10_)f!4firsUTw1>+Fg`>DkzGNWdru_@3%+nl9Yw|=I2ZSRW>hO4!Adx zcF7Horh^0lNzUds*}5wQq{(3L95}=_#vS*Ygg+_Oy!i1f5pUS0{G4uE8@U<=7QA3x zbe}F>wgv!Pz!-GFJ#!5dttJ(rhppsBTP7M+9JmwTStDA($!CHE&3r>>={Wzclg4V8&tXx{(PKgBhh--B1WXsDOCh zUckDRo&qL`P39r5*o59dkYbEHle%9EQBqc>ywe(jlfZ!=8&blcAPtDm5kTiXv5SH( z3~600$W|wlrFSg`#Cab({QhB0H9zd;NpG7jl$k?Fnc@pHVgoRlcV_waxl=b*pTMo| zkWQQ~plmOLb`Bax8-Pw8T9+yC#k)v+(urbK!`v4 znz5o|3g#bAiOrkF@uN}Udy8*5qQL}IiAfJtM*py0J)~gp?GHJ17Wqws*B|qOyX%d` zOte{Sk~|P-@`yK1&J;>m;{;G$v#XnV;Fz@YyvU!gTM}HPgT+vXFW#Ykev&#M;KkP-ZlZf7;)imOq>k=D_&21;So@f+5*FlfkKIX^R4R z_OY!gbxC*IkS)cLyDpy*qcY2v<4d&{zr^i*HT13X8<>PgF0Y@UEY24rQbn4nWR-UO)R`T-EsCwOjQuh?ouk!je#j(o9j*gbPN-UOUh_Jv0{IfTD=lw zF3hukH#`Wn%c{G@49)MPOhl8p2p;DT9MWQz4I1CbjBT2Tq9wlX_rXWI9?vU49Re7Q zt}bE}8eOlHh|T+BYnKvvsq&;t`WPiPu6ZuOn#y?#LhsK`>~`-FKfqo4O_~~!Hu3Si zLC8ON?GxU5Wm!4u=Zab{=TL)$;avP@LPJBKqS;lLw&Ki=hrW4x#@*PDP3u>wBN@sA z8CNYH{}w(D)P(ojzNi=U?pnV(E)4V~s5XXt9^W#5VZwg9$qrO>=63(_kGAk_<4=9F z;ZolrCzaK^bvDh8A=^lo!>hR+SD!>{OQy_e5XuaP^yMsOIOG1+F=G*jG~=&62Qjd{ z#&@431u~ZHsJ|L^X9H@O-a=OCZN#4zx>EYeN*h^XefOXdxIrAVzKEd~YfttOUIsem z#~1Zl-$8B4rT|EDrOXw8K`mB_*rrpqm2&Z;qUr&xydC2d``XkbpTosdl5kr1qWRjA z(U=+dfLC9#X3Lx;g`;~rP5K!cbh%@{aZS#}^aj2=@fc?PQwbUqlh`%Ut48d&n$U+2#53Cbes1imyIgcHp4 zY+8WXMGVq#H&X%+Jg#1a@NYu-w=vSH<^4zY>7xYRRdY?S_?JFWW(&;PKG2LhM%y*h z3v>vK^c9g&c@aRuBaNgAYMJGO5~Z0sI1b)?HjQATnfW$>G} zx;2VJ8uGp1GrH%aO0vb<8p9i41vr(UZ8e{=W89_rh^t~wRX4zu$)sys*R7pJC{QFR zb!;IN^w*+MEnEN`2LY%bIYY5$3=daTn}%Ak_pWa%1O=dcK*tA|_`LTe{%Lb{Ez@HK z$D&lv^MWFE*C%7o->h%c-Aumg=5DFqJ%%Efq><$5h?KGQQ;1>ro@eZZ>EIPCka>SJ z{#6ITYS^ecH3u}4SMvNG+&9uz8R_uLu8h4raVgsIYXISgiWu+Dk~}pGOG<(!E-MFv ze8To#3~>>Ko^&GhMveTz4ytERwsO=4m|VSgRdjt@13W3MyDR(j@=)i&H}DZQ`DbSz zNKIpmv|0ATVZEX&b}pwjsLo2ndIF#uOSvKp$4VAk{#;<+aIECLx-6bPM!ArVNWN-z zhyk^8NDa7k9^X&)LQ&Jq@Nc3Q-8n2b+vsQ<3!E@H>JJAh;e%K}{C{KvCWXiO`=VyI zn>TM2Wr;d~kM_BV&WxYFzXu%;#S$qKcOTO3xfG1Kja8myysd3g7VTbE|G42amA_Mk zU{x6h5s%#e=AH`Yp5t{Owtk4!1iIc2%}i6nbvBhCSB`v}zeTKp=6h$(R?HhWzt|9YdPQ7p|0DmC((ix6_mmL5bv~a=hm*bvb99%K zi@tUP;PPukxk6eE@yX|3s5VAc33@i3t2TJ^u}qiK^*rAT>4%#OfxpuNoxjrB6~Iyt zeVP$xxBCq;F1IcZ?_C-Z1zE>d0w;|xVKN_Ae33HfUKP8kw-K0CbXhLm9GRmGAN@`= z;$Nb1NXrV`1~_KqCB6I}e{h!aPu3r2)Q6ljPm?am&gnx0*>@Q$V%X4J}y}5X$Ipf z%_eDG+GZdWfMg0*2-uIsdk@?31wR~g9&q_eefQW8!IgNpQ0J=ZS(T*XUB{$}0!K^A zH^s39KTc5Ibe6Wg+N%h^mxwuf=3GKPW>#w8AvQwb=0mP(6Wbv7! zzTpUzMicQz8lZkGp-rQzX4BS{Sd|KuS;EINR6Pq+kqgqi_x<&vTD0!J9M22judQD0 zG-(;;Z3b=1YzlDPUV_ap`E6?V@QZ*qVlSiv3*GS}Cv0 zo*|pqTWLkmi31Tc z76}nrTja$z=y6N=?YkPvE*4ae_^)zC8~nhPjV%1~kyb*NCJx1Rr9E)l68x#YA8*tP z&0tK4X%i?F0Y;Yp^y!uYURPjp8x1oGC)+w2Y_}VagBZq`~g=sto3{q z`UHgYfL(4 z;E)->pn8Wv3r2A+Rk8^(ia%PN@}QEMtXK*DkjNedgb%X!Buw@5MKsc7zO}lbcF6E# zJZy3+^w2B?FG7T}qo%tF@f$#j*-U7~#G{fQQyzg^yc8upR#)UZt?w%A zpZ^dsoTJZ+UoAD5TyEoPdGyOQCoD;jB5Ukp5})4sG*5g%)Tge%!^eA&>rw4sGW?aK zpO}Zy9dt!B&tJcqJ!(|JvHjH02*5FLdzG5LK+#im!!sqtA@TcqcG07rCxs?I^8Ng^;V-dr(`BXt%GKQZ~D%k zIo?Yevsa|uN5xXu2S1|XS!IMeZYhKqAnifQWe6!987mr6j0!N9VGYw_rs?%BE(11h zp-%?XJ^s9|(n?PiT@lFykmX+rnq=u$d}ikka>|+5qUY38+4TCEzQm?dqcteo+VtR< zN#)?D&nxm8B<_ie(F4pcv~suo3uPTQNvGl)f=^?%h8*-;!`MH*BbYvoPn!k?iK3At6YWynTyDYG*+*Lf8nQUU%IJ&pyDiC48d_afBYJbORjP6cAQ40bXWO_;E9OUokt0bxosi}wZ%b}Zt-|AlWy;4` z74SYOX{{WAwl;vtn#MRz9PwZ@fBUbE40U8 z>1hOb{WEG;5;!9XGnH;YK&JeFxEb`@bsH#(uEb``lWq$tZO1+nerjO}2TJr{Xr!*; z`5Is~Fvz6glnExD&}XiJkH+f1|9K67q2X&TLH%wDPm`_<#i!F*7_Ms%1h6)!47lgw zAY-q3gMGy)<(cWP4NhS_=3DNn5UruxrScM&+XjR@SdaLOufLfx+q z?I1_3fGmW5+GFCH)ab3HzpFQGuVKG|l6H}5sXoW2lFstPoZg(G?-0z?{TEM>fB71< zU&UP>cK_*wkUB@e>!Ta`H`DqioF`x-&sq~ck7|2)BF8X8i(l&^`!bot9`lwIuEg01{k2@nHnpBO6*u!ya%~sshJfHCL(Xddqk+J7thnT92d~j|~7=Bvwa}IRA zhDlaZr6V5O^cLUDcpH;`&GxPzHJI8)RPz6>mVmrQXlQGG86PUEu@WR+hboluRd57y18)AOln%butp>L z#t)vVvjLLQE@Hd#`N6otYdUSZqvAj@Mf$F6AH-^oO5Mq@7^VrVu+ac%KxN0qGlM5( z0UcO2=zqW57vPZKO-)7Gr{3=9nQCHf|C2GPK;D>mGjZDwYSBz>|7{}Z&;L#WMP+E|CGSzMUa5J zh&{O(ix(~lsaG?{nu_x5Jx$#EWpEue#eF2@=v)d6xkm8zW~|ahXI?WiF1`65 ze(>j3!er9Loz!&G8SF*61dF)0E%l4~H|Ulj4cWj{Jtd%EBU?wsET29Ha#HT%Dj+WyYE=`EFqD~nBl(K3u{Yc; zlT7yxeGKpc7NxQ9`13Io@HT6w1sWTHYYY6RQf~|^OBihUzXCP%VgksCf*sF5 zzj7-r6-;^vRY0wMrDWQ$<;53DB6-0fdE~nrTZP7vN}@yUC7e_0gwu%&L54e_ieW}L zlN8(AkVYb&qWJXGH^sA_gopNl;8G2xTbstF`20Ywm28RiJ zv1aOMtd!G67KnErs-LsIZTpP78bog{C4OT9jy*@tn&=zf`*{gEE)KIu_#K_4FAFXUNJl~S zD-UA#aw`=wnP$M2zD^HH>-;$W&$3EeHC|opn%L0eTLoF_4}j_usxI{`}E8mU2g=Hf&i5Zsc%&kH55J$nX8`I2M%xUC$_tnr>#3(SJa1 z`ss$w4Wl@%!h+y?Hz&i{GLH)K3nICiMc!bhXs5%lD5^2QKyk`FGrOA|S9YVMXZ{yh zC;)>n0e>j3`Q7`+3(*~=Q+e$6LQpJ8TWIBkO0Mh?EKH^B6OMv`d@Qq7Vz7?Z)XG=| z;$`l{^+B#=3^d1k<;gNXPbOy#o5vun=0TPb_F^(q@c~5+S6Y%-!6>t+}7njZx`r@8mXkX`m&N}z9l3WPyA`lnYQpu z`NnDcLEv%A;HZD&Ejh~k`=N_X#Ud3=D@bSQji+OMt-%?#Gjg`^`(|T{&%;^p^vm>K zbV(t*bJ-#3xJMqv-)Xq@yocwiwCyx`krhL&PWfoNqzHPgluG`CMh;0DJ_bhc;(+{L zRWy_SYQ-(oA1Z!!<60@Rd&24jbT?G4pZoZB&b`pBKyN*u1vo+@BxzbOv08aG_f)LAm*g?*qDUVex4>_wYLa1ZqNT#PK>qcPO@>#NQX;R{ujFEG%qR<*m=Px2wp`+cOJ?6p{zvu_pe=8Y{J!t~zE-T8vlV*hPLxkK zXHGI3Uyy~0FEIJ9vv=>`lOoFq0fb*fPpt|5ZVv4+IRm?Isg?~ zc6kS6Pk27Qkzx5n9-WsEmAehZ+3M{Z{7+rr(zYRb)3h=iP-c~lw%8JCa8hCFOvF!O zDjzFL$^hgs%_{KPbBNwY!2%PF<@rjvCzxFyT=6U&0K3hi0W?M{{v-M1z`6nBBbreslQucW(@zU>hh-TS!*rDR3R_<=LyBwh{WYd&-ANV)u`wscivHVw^nE36p= z4z49CUTf4$O(8i(C|`HrK1h`RI0GnW*hCWF3swt--|jz% z;EU1hd_<4k{1zhl{^ii!x1oq7Pw^o{@|!uJCUTiG%XE%zb$DqC{)||W_;n95h+I23jZ>Yqvob{WG>BC0Fxv5RR_ zl%ao>FgmZ$t8H`XZwH@WzfyXODoBW0zEVavP0ZNR_1J-#47s_(WSsoqIhNNE#BJsA zI%f#8!taW!ir(3qB3YoD5WETT8Z-4SkgX>yb{W-Evev&prz+igKl=Pxq{X~c#KRcr z-(;L4yDEwe&Bql7!odds(?$f!uyl3^6dcVB>`-K8v$3`0wK$ARzofTWI+g16G z>=IsEVlijk)p(p^wa9zIQS;1M!OO-f;A;cVrz`FFauUzC1h#3r=upFSO;|(QA`8lJ zgpOE~dn~nB2m?Szk^5jHBhX!&oG>t9sSei$UF#Q}qM~^FyJrB#Frd-%zhJEZ5;S0|ae(|0@OQVMNxet`fW&Vc zw0nxqtFz4S8{t2`fWEoox1n7!&hy1DPRvt)KE@fS>l$pZIjIer70egZT}PQi2#Uwi zmsINpJ&~EYE?>Y$1E5g0lhe}*tA`}eSgJR|x71wBmHvxcjQYdr$wuuO3${!`^*N*! znB#+~#F2dJuXInYa6B8T2|vh) z_Px=47y_ly@w^;9Gv2b4AZk3{IRIeOoMb1Rl&1}fhKHwR-}zfy78-ge?m8@0yO@EU z>V$^&|M(X*T$yN1L)3_B44*rI)HJ6>2h72|>aC%EO0a~Oe1KocT1#72 z&os4bNUQ;8gPiNZM?KiDJM4W~Fj5WDV>GqZ{FIHE#{G=GkTBDlCSmr@n-` z1Nsnt=L*VsweicXal(7d4#^~T@%L4Qw|2qdyg;3O*$iG!LS&07GI-9r;760K4P; zh*Q5?nIVy~h%~}m=%RgDaTzZ#{^fQ66yXW9&!y$OVyP+%n!Vs`XW(7XzWi0j+CM$B znOJUL%v@A;b?Ddrt*6|Ckj8rO?^LiaM#y}BPR^bgv^CyXue2qZ*frl9ty-wLqJ23| zEIE2W+72?_-@Sop1q&JO-UvkKz>z)fsp5Fc=!6StkXtG+d`JKLwSI(pw)$izbqFIQ zqq{Mm%s-7LofAudcVZr#%w*g9>6PaQsJE=teV5YY@4h(tIl%`51lbgNeTDS{)1Vxi zvB;W9#t?-QGx|kOE)qX|7Y3N~vk=Q2|PHN++J(u=X?-7IP~FV=(p+MlQPcN{`^O%KH!T*GN$fKeNMCSK__&;9?E8J zc+rwe*!sHWNYKZOjF9Y&zEoJrh`{yl1xep?N#I0ApGZ?>Xxg43ZR&u6x zS78HF#yBMJ1@w)`ua9Ol=`4P+I?v1JYp14DqGWcvLD8IVXz+R91I9A=2ZGT@+D=HQ zjVcr;B)g^ct(AZO*EUr2vK+wzf>`iPhCiWO)H}*j8AQnqQ059rH4lmc%->grf2(3~ zf;e0i#2zqOI3KHX;i?U#mz|T!(p&oj4g5ufiIQ+H5Q;lrc4u?EO@7siV@8a=6!deQ zpMW8nJwYe-2W324kWq4?%hEn!X!#Ir_j`7CdCLm4SIrk`!QDJ#m--+;XolT{nImP@ zF;Q8Td=GIkG6^24&d_SQ8qlj;8*a00A+HT*J>qNa?=hnrkhcE_tFLtnk}to1te;TF zh;#!)142BN{l~RO`8vV7*z{tGdja=}dAC*ahIig(Zc{g|X%`x%Y~*TxCU$DSX;lZL zg9<}Pl#DLZEZFjGkvg7ktdJqAQ7Y!=khZSY7gUC&<>QBI8Q#!mF2Oe_qm23=P_Q*b z*VWa1k_GZNwI9;}jM|JpCjSITco)pF98l1(WW6hjOib^@#M zbFfaPxfGrJ_+BdP^|ZLhJebj$dUrz?Md z^Wbv+imBHW<_+s>jb0B8!DDVO*61rbUQKQaG4&R^Y|h>6__7UiAO7P~?CIO48FM(N zOT6Rf9ImE;tAmp0hJ?Va!IqDz5xl~Cb%2lB`$j7N1z7l&D#LKe`S+KBFs#x&aj;-n z^_vZenB*JRG!z_zEqugui^HsnH;q^76CUNjewj&fZ^zZU374!e$HTEBmg$dow=n2O z?(oTk{`)Mkl%=|`!Aq;q~gZQ8|I7tCNOskBQ|f8i|=tkA$xj=5t&xOQ?UqfKkd%yjyr8Pz$=$3|V9hE~XwPvkZzFno82>aQ~Y@b*7HolvJo{yi=C}D)t zUg-jKST;aWJO{-0rM{p))R8p1MM-5RIHO(Xz)-YQ&pO4wA;Vhz09DPq#yP@RXhe@2 zus1yV5~xyFpg`Ru{6+ee8^wp(=newVMY2}b4Z@#2(viPUffrvZMjiZ&Rp5Vf8`52U z)zn#p?rQA&k!`O9SC9KPr9o|YX+Lj^we*aUR{;{^H*T`8)i8F3#Qi`sOBtkP z7~*4(%i+5v(XMH?RnvTmtmx){w*WcQqxs1KrAU65t=d%Dop{w^2AE48wItmfV9Sw? zd>?`bghfS5@y=xBDIqH$BNKJ2I#3J9NQ;Hup2TMc+Cw&=;!T!8hmV>73ni0!cBI3d zS-ND*Y%w`L=wrf$$NZjA`yyhNe&SO>Hc}5t26#`C7ww)(m3PC)uG7y@r@Fl}GJp4s zLesJnD83T|UwfE}&J%+SafMyTx0l-_1}^qtPr^7+aKT%f^^uxZJ;5~p*4t&p*g-SR zG8n6K(~;Oq$-4JG3#OEbB~_WczgGjImYvs`%TgcJ9sd^QwEpT`KY44ru4S98@-!tb ztwZp>eTW6Cv5nIq^{-h-Pes4VVMW!L4~y{hh)HdEVp-)zs2X|idL`qR2qG}z=Z*-9 zB$gXJ8+~){&n&goBK*K_{87n#J#P80nd9E*b5C5sG?(JL=qFPgLM$1>J_bR?a_wbf zmX&Qg7(3*kM(q{mBDtm73g0tS-SgKiwvyEbFwZ46xnP1c66MFsz>dCc*`D&9OY7cV zVcNi=Wy72PrFZr7L@Elf&f4yN15DVTOZ3={i?gnAeC^$ObGXm*w)1k&rkIQ_8hx=p zFLVi6oRUI0z-XRRA$Meqo$2e1_`(DYtJPG`6#%+Exq>wp6(Lqj1uns?%=_LtB`6h0 z+~lL6gJet`K3`f(S8`YQ7eX;+wbZrCLLc}|uokXnsdo*Le~ z6lx3mCMs&m*l^c%KLb?`ON2>=0dDAcV<`NKKy!&@R==K<^*JN}b@uzQzd0(J7sC${ml7FVf^-q7>^1B30}|c7 z#L{%D559$>qB6w$hCJInHY%t7lj}>{!WUaFBHU5Z|GJFGYF}q=dpQ`u(Bn{kjc}$+zR<;k+ zQZwMRLJQh!%nY&#K%aZ5&Z+w!nLmz!r+1eP4Xt1i5T1{2!T~^Hs%T)N$$i z>eEn8Gs2TJ>pPyBpcUiqz;UebWj^rPav*+2`2blkiM^Z0)JpjSe+8}pRplYh9VacL zyqTC!Flm~d87Wdx!98r6p>ywzqEkILYM|SX?M;12IwO|cj3(E7OtomxzF#*I$q0W4 z4(07KD?xh}G=UB!fIiFDiGB1XV#yw0pleRj z0MUHu{8%UZH6ZVo2-J7rIxqj<(XEIbfXlu$4442@XId-ndVhjuhr>^}+mIhBd04oIc?uD}~bLIXmPW%4kz-V{`Kk?v;f$Cw0#BGmw5Z9m1`gp`C2tm@2#PScM)M$FCrv2E=DNiJE^GyTWd zoa}2-Rk{vQ3r4JOj77RK^q~<-zvco?3y&lAgxIk`(pH7Xk{Vucz?{rqX~dQPC4w~~ z4alX~oG`PWa`Ow;cbXE++hvgo5%V^&wX#aRfPSGC5Tm!tyPufhch5f3Q?FlCCY@WI zzOEe*Q>jz@V^ByuzJdH#6h7bC&r#=3YCn-W^g`toWhT5cZPb8AAN6y`#PxY=Iq)S9 z?sOh)K6M&PyANI&ZX}TEsu)2v;quasjJ=EBPNmu3ROH!L!$0@j={b=Woe_BvGXv~(Z2rb|(A-bSEq#r96 z2)$*8@-ip|)r)vFYOrwP_)}se6$n^+HIrjJMxxWg41`u&^+YyaLur$G31dbX$PqN< zFRS==V%by9LS$XTV3&&Rc?Ma-z+6<2bnl^rlFDM@ zc2C(F6=xl`s?J%G!!tf*{51t(F$+2nD-Xm!$s6Md?zH&| zY9{JKUL@zgA{8G>bM?Fg9m+HV_J<~;45s8j=l?nvf6e-)Uc$|q@ylKhkPzYBjO@GQJSVL%C(L&SB(_uI`L27n2pEy$nWmyQtl(uWa7 zBumVqA==6z8-*4C>dGKvUJY55wWwIjAjE`CFYu{+!6ZxEno)M1Q+D}}?BFV~WSJ0g zZgfStgYYxegLF_T_s<_}Udr=w^dEiVit_RZq!o@#C0)Vb`&mwH2x}jB=l=aQ%;pDT zKLZRba^Icra2{?6%qgpbRsNhqa$gZk$TPd~rLwV>i7J19f@LTdj}RgYe~Zie2DH@| zLB>v-=g+R$b&Mr?;L7<`=96V}RnMVcdLA9qRa(~pJA+DC-a10{+_(X8c%Tv%oZ!;> z^Pj|c>Iff!g)z6pHfi8Fd0Mkj%2-0Cm3m~?)OvfzGf$`CN!dDL%el9jxKi18&>B|pj**{GV!X2Pa` zU_zuPdxnHoejbHx9W@+~h>EDUJiF%oq-b*xerT#x@gLcZ?$W}U8c=E~FUyW#@Xa!m zt^Ac-ali%7yR9^jj7CK-KzDjg>SGsY8*p+!K>`#(Oc%)))cJsMN_PbU2xSDS2P4qn zGaFSHmM$((2m}8bvSNe2MMmvG|G_9!tCtD19j!hyf~UZDAC$CY6g>HtE6kvfd*X|? ze}+t=I{~IOZwL8=$|W{iIB^_Dji-SFMICc!t=0kcZg0rEYOP_j$b7r=T~PipIu_q= zbu?GH0g_3PxI8v%aQZH>uhk&?Cg)>x^`>s)M}HsG%Xax~Ej7PJwAeUllep=3U;uwc zzW}l%(*x%ho?p_)RX#+L*<=omy4tv(x^~S!CHXq#T2=x)sqWD3A#gUC#pCxS2Xr$w zs|MX1@`al2qB6M)z>PW2@eFmEA18%Vfr8WXV_7T%2yF1JsX(xfOd}qq*6I&Vmg?yk z`@mL%Y0>B+?TV+(&lMg67FA?ETqb}PDGne!RGB#E&WU5oz{Jr1$aa&-tS5K18>iCS z+Vth_Z8_g|JuP-UEq(c^?~G=eugKXpcGb~qZno5lIukKjw0nLWGy3{A0&dfr$OwEl zQ;&*09yuDGv>-}Li5OTmcCs*P?G5m~xL#`F`XAY5^`^mc^V2RWORpa-d&%W!F49ge zT7a>mcF04p;3ak@aM=t{&KxC#YuQvl>3$hENb@o|T>$){aqzb3_3|J+l^{Re>>N~) z%k)3Y&pXH`k=8Cn@OtG{q$uHiGi9K@r1KpkFWNJ%(;}%or<|e;@bU_x36U*#xvcg7 z9ZErj0=I_4FZFEpS^EszEFp-p$jfClo_DuW_r#YWs(_(IUMVP#=o0TKoO|k`{E>>g zRCn$5y|1U97si-({v!+UT+6@7_!qr%mAPZpH&ibEA5GsK&u067Z?vcmtx{X6)QH`h zHA0EK_v%A!T2!r?p@Z6c#U>H4T1xHFfz}p#x3wv1kBsMc_4#~%|BySp!hPq?b)EM) z@Ao=EOhWVDrbNl--I>oIS6MJeN2Qh9+?z znjACzV=tyx#w<7=4e_-tl zI8bRr$3k+Crrs$fb#@08i=@;Mf!`v7>u6`?tiKJ_7?i_1PUE*&gplDA7Mb-1CE<(OGSRr^ z=2Pdb@nS>ssE{h(OV{y@c$OEdXr?Sh z87<<90^I@XyX%B?*~;cKchjjQ=$8$Pt@IyPSnF@HNA7OGzt_Cz0ZGMa(PFz@IxMz5hcltq(pJ%hO@|ujyo#Gn5YkL`+0iqrCIwn0_O9~OB$kv$e2U0;2RlRT_ z0nzW$ba53ClUuu%qcg2fpGnghT9tT1Z9=98-La_|B(D7W(Kors4`N^Mau`0GgZ92T zvpKLj_~^0h9B--5Kfh;cC9}M9|BUseD~3kNe5+B={+@NIi&&dM-j`b znSr0kIH6CO?n$m^NYsPERN)Q$Zu$gzV_ zVC=R!vVH9Pm}~fG>nru#j(PL}Xcwd^)!8GR14kZJhgDSecfhX2t2bw(RHv;TxRaKN zPe7;H;t?S9wON1z?VFvBIYwVvG+3FrV&9!ybS*2SsUDaA$aC;9?O{CY^}rc{$()d= zZ?dym9A{suyHvVDqfZ@C`L zAoR(Fx$VP><0r8dfBiEGhbuT!m{2QlWL=j z-+}#{ctO@^$9oaPTBU7vRaVcr>Y}nyLfXy#e#r031!r4KtFEL^02Aidg!#3>nJ#~z z##4Y7>pH`M9)w3g^s_+hmM>a`f}iB1!huFhr2(=lsOW-{PQ%tczb5CJ%@3XT>7Q>C zp)PD$L^ob07g=Id01+_nu=myfl65exJ-Q_zuJ@}&cJVh`$1b4WLk)?q{{F&l`FahU ziDP;Y{G|m%kSKl=Im7sCDJwxw@yIuAVv7ksW+O5fjHkkrvH?Rg`l<>Gx9Y3yjRnsN0E{qD*aba;TSQhoca4oT@aI%Ou1?IztKdQIC&ldWGTb(6R z|76R(-(Kn*$CZ&oQYiWBLi~bs*Fg`qw+q4doEs({chIUiXi(>sqCUzd;Y!sP9_w7F zSaEyXa_5SVsdF!JCY{gBwxG&9{RHCfsuPYgi3l)>DU)?3PzO1XmA$>3D^6m4>^!K? zyrC|n%~{7kMB|Zc- zlPk-cU*x1{CZccE1xQmOal}Mk1z@;jz+(+7;{mhcYhci}i30(qrvDsyDE=by+fn2S zTohJfs8-({`e_hQ!35g^-%bM=ZoW2s$7!O_S-gG z$b51+?`y$)e%*=-%Z&HH-B~%e6S)M0zpGu8t97Qw6Ew0Ce>)2RC~(hFDW^EzIffAy9p9 z>f`O9k#2A@SCw0AzaF=+iRzC-`i4jz=Y5HrZu$l-Qe!$B1@^RmR+f(0WHM1 zq5>3@P@FyOtZ4VE@os?P=o1Ib=g_$$%*v7@b7g*isj1Ae0RQEnyylMF%90KLYObzX zdPgV(zlcMN4w%NM?7Io%l+9~(YS0bnrX6TtR*UuOEUt5lHQGRc!C~o54LYjwQu&p& zQ@R7J@@jgdh2~-qVVq0$B{1N*YN`|v^SzE9-=jF$E>ZSif>bcnCVH^{oe_&7jNXI zSet@KpS2su=}nZb`NU6`D%;sbo31SmGAv>0b(~6DF)qL8wzPhvN4mIi<1!IeF=}7m z=`41U=|PaSY+O%dq$T~OzF93~57`zywqAx=Q5u^A6nDGAwO3RbUpD7fM{5RMI)L95 zHu#DHVcKqF&Pm9}63$s~gmo8OQQr#98B&69*B*>&8($6@)iA#+bW!zd!DC&btE|Lt z!RYV@J{r&6rWhQCjmWc(QCNQ$Y>6wwS9sj88q`RjuG5)+78xest#l*3g!!1WG@8fa zbTbtCK|3{*<(T6lnQj}bBXu1r7~AM@W`8!@Bu{S2mz93|nH=rA1Oe2A-DkKf9}t~z zb0HrP!otzT|4^Sp;4NIYM;6|42x${!3` zp1*8Q?`lHYBG@E-82Uj?9;3nlw~?Vl>sXc@d$6h7 zZ6^qSc~!1A($_~G(N$1oB2f0!MOJo425Z|2V-3=5C>pB4puc*2iF3}i(^NBv!L6FH zhG41ta{za9l~g%a%5rngSo_8-gH18jn-Z89_uS~{o8-1zm6erh%v8(_8}w3Y{BanF zRd03o*g}c#ME`{#NuBLlwe#_-Qjco0hV~K^i{I%yzfchI?v7Ufq1jgvX6-fA1)Kb~ z#F`54{ss-h?rD>dF9c{@f7i7IwvTS#{p9Xc`v0^YPh^ySKc`EjKMy^>ViHOP(-}#X z{5YMny0O1!n#|RJNKdxdo8$rJ^7Mn~lC!Wsa+y;xDRv^l6Yw2r+L!`YACHFN_Z4Vq zmOJjmtHdH|AxI(T77DR_% zPS%d~nK3+lW!kTsZ7V;TDYIo9v3^8R0G;kn#LrgHwRvancSv}*PK<(RsU49YG zX<~aYd03+Ketxr@+BQeQHBd!drdwBYMP)&yr(V`V6ciU6XAc!mZkkjYo})6DDvkKMX9p*x@gvP+Y`9(=liOsd^@-< z7BLL$vp`bU{^UgJcjJZ3@3w~O8STZfF`olD(^}Yl2Y44%&T-)j9aVziuixW8r@qsJ z8f?$I_U4x`^k8kO1u5_(7~kJcvPb1X?30opt`Bkmu!}}x%0})7B6PcOV0?|}3+csf zsEG36-c7~L0})|TnSf^SlR18VNj?jj%3S*hifIl3auCS?CQ zuwJN+j3v)0IK5OZ!8;PQ1A6tV(?2{_G(M`#yB@acb-rP3a7Ex8cQ+MsdZK@lb8hUm zO~N7;V7Sy3((aZ;!C$4TG+(->5JN+GTgyp27ib=1(xght)t2&TZ2NuD00d8dTU1kjH|^Q@%}H z2LQhO)4yZ}hZJ9;qV(smqbG;u&eeS9_2&ki{HTC)&%-iRw7GALIWl&Ng=3g_Mu1*9 zFzTH}^Uaj>^t+Gp@S}^%E+{~~NhPF<2At_K|B?YMi~`I1kngB;4%k;q?atiSS92U( zIBb3;=+Q+B1hz~Es-_Ct7p7ix+a6+|3JI$paj4kY%@ z*L$W%4x|Wg(l6i|l2_hd1t7)Ef5{p<2D6xM?qG;Mwku{onLqH2f#i=nh{lfIs@ae0 zmI_eSPs)6XX@fdhSSsb7DxUCn1Iibi)Yy`d%BinZ1msuTWVWUZ?rAiGlXM)vIZ^HM z)N}rAXSvHnwubtTcc8qKCV)|f?Y9;8h(Af|<@0{rAPxg=aMLm2bxhC)7cAxzbymR^ z-LT<`Oo-^JD`THoKfHCCZzsm0DLUNqa4LO*r4|;MLV4>mxUUt6y~QS`BO9(?bIcE_UU>$j0Dz>?+y zDt|eE0JlN3S#b+mWT`Mi!F^~)@?{Tc{3m!Ep{SPaFrQ8-gE=s|uy4mek=!w+3npFy z56C3B00SyLX1va!b#-jiocIh7;^5-@_(_a|YXAnE2D6JK5Mg!Ttr+V8z-fv^bh8FV zH^e2)WQ9oBylwXfzgG>qO^^lJj_fL4e8uVb)rXVYZ}+^WrY^9%ATy#UGm*l{Z;tNd zz?G z1?D`>^cF>=Yui`Df8Y&C%{!nr^d)~-`gIfaJl{)ldPDu>8DbKeA$rx#`{>Ow>~Q2j zRqSoSH5Szn0mE(qEy{S+zTkGPr&S+eIV%LW6gB7BS^--Q>UjpNJE$ln>jYmBbW;rq zr}a<@@7}+TMXg(ZL^RS|e315sfL^SiSGRCOG=1Z?Hq$4Qtb+75Ps_mb;B#mWx zX${Y`r+Mh_jP&2Se_Ws?1<@M*d~Oudo_6*st?2!^(Qn1{;FUwv8UHGifWAvTzrK6J zK910y=5sM7*i8!A^aefCSdt}VG!@Ff-?9X;(ymhG?uAEsuD!2p@?=%(`?z#enqx14 z;?Hd%KP<=1r6}rApHwKU-rk#BKw2gWE#plsqf<3+E9iylS3Nv<)iZB6WpWRun0|{H ziHg2EX9HRa6EH|SqZf?w-cpNRR?JtsgB@n#Tq3%(7}b?5&T%`JPu`-v<5Xc3v!E0%W#*_>I=R@=$M%Dc+|H+EoiSV@ff9b4!S?u&OMe`p_xl4sfSN7cBD&; zW9`nQF?vh53dRa030k^(nK<7E;RVhw#@ zh>W@Px?9i5+02j=PGhPnq_=>^3>;B~minItktK;uX zfjUBE8=ZSuN?U7AbA#V@%z^u;TmBo_(n`JcZ{Twqaa6`Mq}iZ0Z6p@pUs0pY@zoM? zFMob)l$Bmqr4<2Pah)N4bI8}IMudv2ct>=TNeWDNge@YAQW%mRpJ7z!;U{t=Nhuas zmewKmF&j60hn%Ei;6SvfoISoXvp^ zL748K;sMD_?w_E3sJg|gkG~Z6N33`C=rPUFQI4@*s>Dfg_9)_=F?HjCxM zs(3+?LLz?5m*Xv1|Ne)&Ob^R}fU(a8u*LIt=-X@oB=Dp8QoeH(_+%Rcj%U0BUpMwI znMuJ;%%xkYe~skHTp9K>wFJnT6iee5j{fbaB|k&^FSJ$AAg{`DK&kGbzFR z4!{PS0oAYib+Ea@|3_U)zbr?3cF~|Xe`i5=2uOnSO<20g2=#wY)d@wNn-u|dG-|)F zcDr$<{27$_LA@T-Fz3$yAXlNW17w4qcu|X*LHr63E=OXNq!}7P`kNM6l`kw1XpfIN z(0Yq6i0*ng$Pooa-e%&O0`dZV3(*N+Oe=M?sss*o}Gq zVm6b~lGI%|>B#6l2S9bGQto(g+DY+4L|=(*Ig(-`zBGk^_}aY&q7eO&Fx(Knw2JkX zFIU!o(5?RZ8Qny!yyATi7+{9pk~Yq9jUn%@zW zMK`p=Mw}q|UD+?s8;CDylL=Q_6sPvA&np*7(J8WBo=wqLR_lU4O1bs#qr#0;6_#!?4ltuhhgQY~dTnMztF&*KW1}2x`dN_ij>oH?zd9Ai zjWpiMczoHg6c-b^pDuYPIb8XB*+2JC;uk3Ol{xWzobIIk@gie# zG2}Fz?xcKmrL2B{b9p4MMc`+n%IM3hW^?UzEtwOsH=nnoLxht3-?(sJUKhM4?EYK3 zDcpNrsUoP7(?NCdOYhoyKD3ayrk8t7rQsDvafL7dk6P~_B{NKr9GhWghOZ2VNbeEN z(E1kVX<2O0fi8=rv)bp1&Bgs|Im9lKpHq1?P0`?WP7thCYWR8$l$7y7;t5;{wtNS%^%m=dUm}he zB%aX#zj%hu2F@Rf#>&F^nx%qLG7?lz_uXi_z6H?meJ9<}q`6Y9@2=sqtcRFpc!)P7 zZqPqmTSXa9L_#E?qE=4D%3o_jFA|y^ zdmJ)?lR=`}JAAr*iK?xFWzK03ZB16&(Acn2XV}v*3{kbiPo<5Wi)6w0p?t zth`^Pl-!W}FPWe(M;SdQ9Ya$g_q8!rc&U&ue)JIAU~(gI0fN?zq4+hTw4sfp_z7n) zV4^i}?IJTG?~HlIy?~VVw@Om$sjx3yGKWCs32tY;G=*GRSL)~t~-?*7?m zrjg1xP30l7wE5{!SqJ2I)8DeDS1QKQVia>%O_eNdeL-$#j^>0J!sTW*}F<4 z)cxM{5te{v@KBzpo?!e&wgel@>0`fwY(>KwVXQXYauM*arq_EuVz#U*8}_kt06T`&(BVKTK_`L|Fen5T+q^zd4w z5oXGDe}K$XAjaWZkm)F-o+}0&S6$2Qfr)QRlt{cpeFtgYCU_-7;G3Lv)dP+3Gd((Q zgY6-OP1SE3@Ym`q=P|aTxXMyU6_t3tKwKrmXlwI(Zp>Tv$~)OMM%NygMst^G-GGOi zdIq99odFH`vOx7Awzl2tAT8(eHQEsGmDe&(~GZ#JZoysbn}sW-2- zARw+I#Q$`(8>&#}qhZEN`ykHvZfrj@Pt1+CyJw?cQJmeShDK&~IkC<6dk3EWNDv>c zvPd+z073`7b}^@>o$JbT_Ycb!W@c<#TYSb61QT9(gpO7-zd25-@MF(C>%_mugci|V zzJ1PL)?MJ0&%A`gXxPVv#9TTWaD0zzN4Kg5UE&;`mL<49+Pk$T@kVo7N8qTs*SF+i z(f0=lI%!#on5|AO*wK+&*(_b!xv=-$UM~#+J2q zc&iP=-WtEBKTAr8McRtX-7c%G^Wu1Oi$x~wm{x#Q9o7}|_I{|qCD`Jfr2f3XtS*nn zf}^?*9tZ4>eq~3;{r+HH=w_ukw zwoTic)C_b$_(ECHcnu2C1J<0d8ob$HBG{LDh@dM&<*yhRCDJihbz#fI1FMa~)tok?WOuGW2E6dTwtk^=B7^ogBG0 zI9+T_rn{#$|LP5+9<@1fG<0H3G4kVt;SD2--_xTFKb{D*>Ae-Evi_G0Yk2DXeiy`D zk`N~YUZD@nuAUGOL!`4R!vdq7H>76U6ZfM`LdYiMXbJ%>DFmy14zah*36NH-QAB@8 zZ$xI{=D1jy-R8LD1#wW=KjIrUnJ;g3IRSK=4jn@_cUkFb+*sNJFg!Fs2KOV?j* zk5h)q=$*=srdh|2pk?{nv?rPVF=gMj#@h0PDfww~$KrCs*w};gT1{3K?%53d z;y+Ob*dMT_M?F5iydUCxp>?ApH;+p-!dyERF#csLFaViMvv0B+YoOL7eq1Htl&OO@;a5`CUGXoF1f&}n7MWDi?N3N1L+kF zZA2jUb<;ZVjINPpzn)(r(V}!ny`^i)w=2$hIBDZ6ds6(liK}z7&p;=Be|wW|RRO9m zx{QxX&i#clY?}OoI)sWE<<&PkrGNhYPfk-$z14nZ*jG=QGxG-Ahidy2O-JHdM)}%a z+0*{x69;cE(UPFhClbDzk8EPUMx@N#UOd3nXM(<+9nt;Df*X0qVg=`}Pb^`8zyQP& zbnKgll)p1=$No_JiviIfb<E}4!+IXxJT%DT=$RjiAR+N z&8uPL@>1T$5|?N!3uT;SxyKC3pE$T^YXcZ#2r_Nj5$W&+H0S`L1nHrvp^5P=l#XMs2z6#BboGJjv<^Y9x{IE2Lwd=oV(X zL#kUbJ;cnGpY`n%rudM$Q)$)TAAbled0NUCDYn>UDasx#R{tzeUb@~3b!H!bYHA%D ziW7YIC$E#W`#jIk46ZNuoh``!6S_j$=nB^Ri?^xP_6Dv#w&`_x7AvL_@$H^1(A_Fv z3*|E%gqO>}@&u4X#fBbIPvh7D>M|h|Z6Mv7F0s%m{=LGN^Edf#^6zzKy zIw?w>3F*>=mVr}KxcoS{2NDCpN&}%2ZUXs_mQMfq`HWS=s#)WE-p`N)}d;%b)Mf#OGJ^20{G&j@K>9zpRi#L zBQPy_j6w}C*N9f{4@_Ev8CbfS$MY8sXQq}n{Kbay3r9mnz#=qLuxrF-Ox*N;*upJ+F^K%kSO?u4ab{BB zj4)K6UK}$l{MPzqTRkhZT)(!VtmJpqCjYR|L(8Fu0q$_`1!Ks4q&t8!V+dy4VhO@crM&Z&#_?bU(x-CKvx0L5+ zQk~tx-+a*B(%)a7R=Pr~EAqwYB4^!R!$i`Ja)+{kD;;k%tZ^mW3I|FzM)YUl7j1@S zm90PzaT)x1+KN_M6|{;BT8R1003FLJOc8sT_ujY@2$$2akU=v0PCCt$Ag1 zd<3YM1bMA^J;L}Dq-{*ZUU*42iYt7fB<~0|yXm`5WU{oNFLV2JjDEHi5#qfyHQHG1 z#N1SN_u*CddpSLO2GA&isi#%j)7oC5U*}qe?H{ch(;Y$@b+8?yXZYM8I#S zCZy#*<0vElsQ?#vdt*aaGd$)3RXI7WU1LHFEGcff440f0Se-c+O)q1_Z8eG^ZQ zVY?GD>=T1Y9qT>Ba~(&sj(zslzYGw@s^|&e8~TP$1mOboW^J%7#g|Et`5w}G=|aI# zfwBhOcL=`tr8ED0X0(4Y$|Bo;#e?WJ>(J@D= zFLH$8BptRB2CLlIZ{<2Usb>)E+}K7Vb8txm6h+ct2R zIr!lPGhtI&J^ysupbOf?){@K5B55VO-&4>>nR;0M0JYe%&#)%{F;wuXq(-%+Lf_J( z`k@P27_V_gd0w^)YZhS-`@UFPC?Vi!tfChbcT|ScH5Yb0c zt!8TE&l(|c2%u3I+FhnsbfPxo0b+Os>~;mX`i^jtJ^H4?>&N#n14hzZu=yg!eiw;? z&H@$PWcs=v^u2`geYyN#vydQhx06@<`AWxC>mDbFsx$K=$>NtXk=$f-#+C}#zV6;+ z^Bis1`QgBjUL)b%tLVgX#Fz?YUWNvT)qAH827dZVcRrf%I-qQkjB*qi95QQhG;8?n zdDc4FfV-XciP};_e)?tK>R_X>8T0*-4*w{!grk4tazpyKsW)ZId^Bbxa@3t#9BDd& zeM4T&AYZK*Zwm5hk0F;%d^qPI8&(lf844fZqj$Tyre6M(npa$zzZo%9RK9!A{BC|m z<`B!*WcE~aP4m`#w%}sixtZD6y3xr~rzdl6dx%LDYXMUAe`HOq-^sjPhv0OoKD4{~7ypHDLlZd&rBpEnv;cc2v!I;IWB&7&@5L|ue z-MYrm+(rJeL8BYn#+Hs>Vo;rKiz~bTfNuG$Lj5N* ze0FdC6!0OHm{n`$bgy1_kdOGzTU4djJt+`;4rYGW+ii2M=9<`NGUA>|K6CWiVU668 zA`La3(GVApu*zP$Vgz@#Utjc_RuXrJq3d=TC3X5|7l4uafaPlOpYvTD)x>m^JdoH# z^v`yYIT<6?NaV=BWH@q(oHRcdB7cG%kLg*{NztZ+{5SaMbt*Y~^i@GMy+M7nvP4Ng z=R#VsC1iyrw8o;&JqsD>4?n{2{Nu{$Id*vA2QIKSF_WsIR)}vPt?WEs_p(xo4$`Az z^T#|--lfe7lOu~q5=~$fIunv7TmR%m*stWnvZKpcJHFBLYypA=o=@O+fV21w(hlzo zKsElyMdO*f&ZxJfO-2CZOaod-jNuJ3-`!CTyux=m9Wgt_qGQf+y+QrdBk=o*j%=5H zyZPUkJt+NwV8B6&(D6}2ucpMl56DrAeSc*C2***3)hzQ=bA&PkJ0im>Q*6{|u}klF zE$AA(M{A6YrduET`Pd8V_ooSUhRjj1J8YSau8die8YdrA%{2VZAqoeCEH*KW-i(h` zCfmduU>=jPsGN<)_7pUJ*(=Q2BdF{ZFHhBbCGsLp;O5@@h|cnX4|Kd?9lnSM(Ql_0 zca3y1>IHwxVojg%3uR+=Y>fF6j8xv?s|XdzNE>r&x%e&876STfhpo!VU?$<0g_H<2 zAoHOk!j`vmqkG0C!oAYWyrq+yimrTrhL|=ypU4gqlvaYYG$M}EeNN4CfbJT6SpE9B zzIRK)(b}ml_wo|bOLGiS>o$<}Jl0H|5UhJ@7OzHg&;#j2 z(PJ^iH@N9Z$mAv`X! z(6PztQQwAP)G>N){tcPtJ|5?3xTU*5>|!ID;F$MrlQz%`ux7WyR|JVdS|u3`IV^@s zg}{rH3LcH@Re4CjG;C?tD}XKyTdij%hYYdGb;qWJ^|OxVjy%!t+iJ$cM$Cvs;G=Ug zw5G;16DL|P_I?guB^j<5nV#-Woqo$S&QC#CLdDHf5PHnW5_gQ%x1rQ9Czb~=zP;iy zQ{qZWcf_=b_3Q}DWyGxL7nUZ4UbGO{5qLvaR(eMylz$CZP}5d6wJ^3rd4F4N*WJJS z7|ga@Bm*+Ru`bu+bvF%(*N`Clo7}@AX@{`%Ya@V3sCwMN(q)>l$D-EVxf_FeZ8?Ji zqnOxPZqn|CNl6pEix(AYLEi@+UP~{!LrQ1+ib2DhqV^0|rBASAOEgENS?X1s4nZTU z5GyLaRE@`&z{o_2VM_UOd1OGA>%mn@mK9ck1t*5^{=We#jP|T>eumOC11`AmU12hQ z45bd5Dx7D4rPLE8to4OwUK1G`DzYhb;kB`pj}~VdE0vJ;1e!8gH?~cN)9vuSQ#NA4 zgT(NIHf4qpk=-6{av$-IZ;bNXwC`CtQ{$kUv^|U23sn!&ccMV-UxH@z#V<^oyaPtI z47Rek3Dy#10R5fWgE242kkQ&zoq)4-=eKd)3IbOgaOio>MkAruU->V^fh>np3LU%bGcDR2N;1ojS9Pqi89#p9(VjBoI&GdQ}S*?)NJ@T^AI4CMUB zXt|mY-lLpCZbz8K3Aa$1}EP7tja$Ikr2< zmzst4mv1v4$_zl~z*0~a5937SIeKP4w~p%sLIkBvJs_`a5FJE8kGKSy+ulQx(ZPVW zX@CHNORF)urFQ2PIJ}8yva&)DLfrr3v<3j1LkJllt+)ZXZf~y)G5MHK9$rU%^@x#2 z>J_42I@rIoTk&L~s`Kn&M;Bqe#hN|UJSge=Q$>5~!>)%VcN;_pwrA=t&E8nFT)Zzk zed04_c&mSgLFDdGgJgO+uM2ds=R9Vd1lx6(C~DeA#nRO^c)25E))(R`2cL5&8cPi` zd0}N3$JHXZ<6+H(w1Wv`Va{hAv~83>A0BbEl{?hb9o_Q3^W?w2_b-qI2)4XI7>@_k znVBPZzgLwJw*xY*&Zo9v!K_qMB5Eh3KlF+JK0Ic|-0>f#YN{v6gMp;_szV0T0XI=E z`GRbmLdhu|Z^09^La*?b#vX%`)Ast*23SzDFoo_!6^p#!GX+7ZO7}w{73b?BJFw{< zt(x0sMVwKR!f?LD#7(yw0#X`Ft7R)Jm-Xk7aUa19b=5U}LqMIp8N#okaImE#;I@=w zED1TInM1U@ox=!W4MZRJ5{uuD{+tj32f!Vszh7nhgYZO92^8Dlssr#@_1bj0sdW0c zxN-1Jo`tt8(^01;f68m=obSB;82#x>OY-DuY5SJ%e1jK8LYDVN>1Y$ret9)n=e8!} zH*T`!3ElB8>{s?*FyLMGr$mCrEas*U^d5}Sla6h+w-W>(1aI7OP7toBv&;2CR(qBT z)b&10nCdc$zL}|F(vt`$JSo__+xPX>2;IV27Cy*y79Q98kCA_IhGd^RlSg2H(DX_o z=a+ivBC(gA%g?!>Jhs_kJ94Xa65I0kD<0iaC9Sg;yhEFfes^X8_L8VDs^(iS5dH&@ z!M1Bblk(Hk7nrWYiA9$?bAmM(?0kP!&80`eGWQ1_Hu(4PB#b!aBoCP8`#fKwn3;-At^tM$NtN*@(!GKVoF<^^YohOCl1rM0l0_Beyhyjw-a0*OnXj zh^ttJ>tPn$kVrQgRNXXUyx#4V>^eoF-9@7m`j&?8^qxnLmFV64e_F`J<8JIH0C(v+ zC;#cDu>B*(jLeToyJp$eeDUGA#@1%xuuDP(f37$eefj|BM~R@`c|V=j5&b7nV&E#0 z#^nZr{5xm+A=YOtBlf%N31x%#<`Y?I&HJixeH{?rC5f}AM(KU58C4Wg*B;@^V+K7Q zx1Xf5;>EvL_DwlA__S#($B&zb>D!DQ+&eQIx7yT}n%~==A3nyu1B(g-G;1DJ>)v(z zX%Tl^ogd&byzGJ>pn+9crT4F}-6ALevGUz1%W75K$1_-~^SQ7gxCdW${7^MTr=Do< z-}m@|bhGI-de3r)#O&ntjv^IsMfu+E##SS32>mVXrHNeSzg-5U2a^!2qIj(G9MQ`8 zoSjykS-&+P$fQ@LrzK!&XJ%;gktY0)wQE0fRz#QZ8dcj_wiEt1ScERLC8xJ(&1%oH9JKnmk3f!}) zExX=E^4?j06i=#d!kU2A`Hj<2 zwn$?l4jMsc+kxzvyls;A$b7{Uv+&XFZjJS@F^gx<=t3=WY!j^*#oM^Jy=B#H7W*`I zhKsNva_DoV<+1%7f+^_f``(^jnt(Hv#O@S{jMB>Im$=P2n4Qzh*|%w_}7JJ%Gi=tO1NIs#XEz|NjW8W;t+|*R{~T`%+_=`W&OwkheiM^$g_Q93z*38`(ByB2Cc0XH9p1 zfa3m^Qnc6-E`EUc^UUZ1_bK)ZY@yIj8WEJ2s{8%8gl{5gM`Y6`(Mq|%7KdVMgK4Q; zzaGT;Y{=QRH#2|fu+YvAhUQAR61@!D*Up-5I6ri~67w!-pyj#4!%*lzs%T1KF5X}X z-NQ0JRjb~{#rZ35AE`s1@L&Qr0D#!DhcSB^K8 zH|tWvmdWn!-Kgbgnx)J;)jJa1YgJ-mEHe)+zuN~RoyEkhyVMKIkPA}yUd)zxziynL zJq8+BF>w&BX#B-wb($J(rOa&H#k+Hhqha24cBigUam)@M=1Yc)!hcS}9;3Nul7_`2 zQ*w2AcYTzu4mF~_1EG6yjRB{Q+0c1hfM&=(Ch`~3I!Ea8CP|9Z8zP-Ib;oEjC7g@r zwVNYSoa4r<%jT8nZLYfT9 z!YcoEDbvkR{-75Ta&0o|*GW){pVNuA38==ek4qzi)dWg4<#_oo$FU{;LE7fjB8}IiLyVrLwhRkK zP992>yeJHR&?ZrPtu*iD(kFzKO+9>-uS#TZx=XByU*dP-&bSZPw&rU6?WQNsAfI&a z(e2PyIwGuehqmBvo)6`KZfAN#Ewp;=2z{~5^?jc;F=|EaVB_30q`-C}4!E;e&{};7 z+qqV-C2-KNi`oE6Y(?VymrrT$qYr5w5aXV!*IsR5FO+&`Y#cQ1v1M2XD2K3s7Z>TnDaTG zD9uM80O&O&VB<5Bb5>_hY6VmpPZaBtJ%{=@N$QL;+9kkI*HE$a^>3d+;aZM;7-S-i zr=*m6$&7ws*)QEF{l{I!;L0C9b+k6?T4~vtWdqAgURQ!9jBHW}$W)`u{@n1{k0+A3 zxNR5?-I6D<)LSpbRUq$N`GuFf(mL?@YF%N9#C+4E_vA7gLt6A~LN>5nP%mtu_3wVp zw%v4(?OU6-+%_uITtFfJB|E{9Rz)zWCdp~pw$`acGBBJA_xaUuL}Rr%MAMqw$x)GS z6bPKCUzwO(nrLX1x&EEs-;{HW@nw1y3ch;j-Y2D^jKMPNqV-GXtXJzC%ESB;mmBdZ zqCbGWcl3!9>}1@d5We__9T#tA_%#Wjd7;nis{r3# zvhmz%9Yg*P;7Sa@3G)vF#ZPi1dH-s~RivL)WDP&T>Zz7VYDP}n(EQ-5-Qu=u83qBU zR}-;SQ*m%sb;2{#xPgomoSw^&UHRxS_&Nw#;TL!u*&XCtMjFzu%KlKVg-EaskD z*_}MK?;;_|t}z{3@||DLM{>m^4R;(K*K>I{#(nU|Rx4U)yL`MTcR4$`dv-eO$X-{D zBAe#(t5Xv|wH=w3NSB>j(zB&<+XV{SR8^%9{O%Gi7MWh-VA_0AhQHoKdzZS`xG75K zoOU`~HJPS3+Fl`*nW`4s)ONRMKoj*b(xJ|UD}C>c&5*IuYxniy zXSD;7JLu>?|GfQ*vg#{|Ju=vpH-r2LO0Fpn>@yxgg$xKmgpHTM*6J< zv-(Qv({{0R3XRPT*L}KZBh;+Z|C-T}<67`US#Rkw@&*_=H8}P$f5jN+z}ZCDjEp#N z<0dYdC-=-mC=9crdGf&tAEXtQ!9rm z5S~V3hE_HYzUtl5`F}KhbzIZm`!*q>AR+?NOhOuwk`hKpcS|TKGE!P{h!WBrLwdwW zX%G+t7!ATe8l<~nu=+f2zrWY>-^Tc3J3Bk)zOVZVY9vXEN0dVh=6+c_#JUDoTYjfh z0~hH>!#iJ1KQ$E)zkVrM{W53dkuPV)Owe7_W?Ll9I}FNcg0bcLEYn`R$uv&m0QRze z{sRFm8Pfl2wxJ023>V(|7e-+{*rpwTS)>1!LXRyn#{bq$heP*XBT*=<@)WK7L)Jsa z^43G)c`%PUZ`*kl@k-jEwCAf}vsVa(06zI3)N&52!aF|D2T4wULm!awdSV?6l$V8N zpZ-Ur2NdBtE~&JyOUtkCw~jO=`ShBmVl==3w@jt4v*Qa2D|dt@pFnv0#}MUqESV+u ztp63gPyQpS1Rog#C*9f-6it3sR&b$?-@P-B3ZGbeucgYzO7WVezx$h3dbz}af6w4) z13Yv#k<2*Dx`n#smQMB|lJpda1F~o!%;~#~GB4Di?PJ3H#9UN?E#22`QaYM_%`D7Z zUb%qJr!&QloPi3f3Zo1Ly{T+n>j>Guu$5%@gXqZ+#AmPWjZpo3-S#|8bkErGnf)n~ z!|3V?>shjkX6>wdnTba>NXgW<@!PJ%t1~`O_{i%A?%XM;I%6|x0AIbZtnJc$0X{j9 znsPO-A3}|$9l?NkKC&N}w7j{Vgkv|eyH8HLFNOg+!)sdqs(Yb>HFMRk7zk6FvHmXrL^0smc96BAF6$Kzp(m z@Uko!+M#$eG=T>acrGz{?qW~!GtGMfgeWVhA(G>a$4*L@elHW9->$eSPNtIzHiYRe zoqJT?Ye&OzkN~5@%fg#L;;Md^oKFz&#qiPh02Xf{%Zm~#j^$CQ?|oit=E7TcLp#zl)5cv@i;px(j#QwZPMQovQN%oh6 zw)0+7enamh9}^sK&ygHQez4N4u>7R{>?N zZhg+nXi4_>850=<@l+`9SLH+!z=NS^0eyCXIVqxAv*z zZnanf43xdHz)D#jDD%e*!U4oFUFniM|NAbo$D8MLQF74Y-(B4ux(oPv zwXKUn3Rp?lAqcb0KlvX~U0V5)1~drrjw1AGXd5TpU~SHcdj@GK<`$%XZh9UA6%Tk!zpNUefJf#O#ovS z3XIS{9V}xbn(ikLGA!@GF%+K1fRHc~3joiN;thDRTPe;ZvGiMUhv==0%6o;hHS0wX)F10{QnWnjo}PP&qIuyoqLn?Iy|of>@HTTYtw?D8?YpFezRH? z`N@xmn5QnUQPnI}vVbv0PK8se_<=-DNrq#_8sq+5S6(9)Nx>At@;UvK@JJed{v)6Q zax@pIa==>d-Jt0P7Z%+rPy*_?9DudS2F8b?@qVDe#wO6!bsBzb&r%kO!5;mSlstL8 zy`+H-3;~rv>ih#}geOV2t72gz883&DrFV6oZtV-V4G0JZeHhWE47lZ46BW|uaM)LZ z@_cwoF)wA)87jzgDo6*=1-yiEzupOHuU>7iO)@qf@;*%Qp1v*eh6aiB4;vIH-KX?~ z3JIoG%+QxQ_`BPm{>7v%z{*2(8gp*V4U4(7yFKFWFI-i!Bt6n4>_Z0|SO4xQDd2#F zi~oo+Om63JgHFx>JQfhGsDm-HV89;`{(+$24&G}$uh{yJ$cp!$A+(=pOJtjenItJ& zq>nkHR2nOk%{M@+vQ}|h)#nc{LnD#ggi{I|<7V^uw}(*w>UU%G_2ty(2B9y;V>5fd zJCBj>X{y^@lsU?6l1Cwoc^DRgW8;d-AM1!*(0%kl7o4D_uzm0e=-L&adKNcW0-X zEO=34IdJT6uQIpwmL%op$!}xr3dc4zqG@s)@9g>R3qBT)5>Ayfg*FZ9QSG~^At*vL1N9q{%qeBhmEVp2V7ih1v5o;YMRdR9ai5B4GF_4dz zt`3AoMjWix&MgKJxTe!HhU4l8@WbjqvzPf&K`CY%?ROMBGeU_6BsK0^y&JP0&*+P; zB$>I*J1hzdTYftIVE(s1;o)jtci7@`@VIFgAK}y&xoEp~g2<2h5-?toS;v`vn(OL! zVB(oCXn&tucW6{`wQdiSrW9lA*)`oh@L1(O<3>U-WJ(sK&v;ARn z)^(GMw)~>4gxr8&r;@WGy;>@ngKB`w{Gzw35#~8p;{fUi2mP$v9!gf280#^V!;?uX z1@kU`c!TY0;>aWWDsHEJ%0T}FCO*}DHzT*|jl=6`|Mqm>p)%|l8HOU&xwmEK_84oZ ztG>$`xW?86t>rjV9$8&!>R;c?{4~JH?W`#pR^;Ek-71Vw4NZBX2ae;8wxwv&VV4hhLM{1;0!RugJPR6j8E9)z(vu;KzwBq|2$)Hr!2aXOoVS zGt!Sw2D3)1dbxVUk8#h7@2Z+54Z4rGa%l|wVXG>DSd3aX7Sy;o|H-^e9-L?$?y)7# z>}9Ri_WseG6ZDB3#SG2!l&i`bBJQ_KF&wL{f+AXl4>v1D(Nc*lgSW5jOO-Nrxr{6h zilfndl_RLSpuRj98KSU8=&0(W@_m8+`n`oTqj}(xaOPYKEK|HnQsBLwE+`AsBHXyL{<-#7y(!D-pRVZZuopizRmP#8iXNuD)cc_3AIkY4>_wv^ zK{BIGZ7;-8z+d_X^HS?4_k*?fGS3-rTu5@cdo`n^>X~YUi;^VsMvX_JS#nyRLZ=de z58hE^(LLwbnJOPkOcU>rX1Qd-?_Ff4y4Dc;1(oi0H^7WcQs&

|2!~hYkh>j5F9L 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®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 literal 0 HcmV?d00001 diff --git a/static/模板2-以文件夹方式上传.zip b/static/模板2-以文件夹方式上传.zip new file mode 100644 index 0000000000000000000000000000000000000000..5ccf9336b23fa33b7434b2efc05465d21afaed2b GIT binary patch literal 26336 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 qnBdC}9joZecT%pw$-9wmUn>WeIn$o~Zt&hhgA literal 0 HcmV?d00001 diff --git a/tool/devices.py b/tool/devices.py new file mode 100644 index 0000000..efdf582 --- /dev/null +++ b/tool/devices.py @@ -0,0 +1,249 @@ +import hashlib +import platform +import subprocess +import uuid +import os +from typing import Optional + + +class DeviceIDGenerator: + """ + Windows设备唯一ID生成器 + 通过收集多个硬件特征来生成稳定的设备唯一标识符 + """ + + def __init__(self, use_cache: bool = False, cache_file: str = ".device_id"): + """ + 初始化设备ID生成器 + + Args: + use_cache: 是否使用本地缓存 + cache_file: 缓存文件名 + """ + self.use_cache = use_cache + self.cache_file = cache_file + + def _run_wmic_command(self, command: str) -> Optional[str]: + """ + 执行WMIC命令并返回结果 + + Args: + command: WMIC命令 + + Returns: + 命令执行结果,失败则返回None + """ + try: + result = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + except (subprocess.TimeoutExpired, Exception): + pass + return None + + def _get_motherboard_serial(self) -> Optional[str]: + """获取主板序列号""" + return self._run_wmic_command("wmic baseboard get serialnumber /value") + + def _get_cpu_id(self) -> Optional[str]: + """获取CPU ID""" + return self._run_wmic_command("wmic cpu get processorid /value") + + def _get_bios_serial(self) -> Optional[str]: + """获取BIOS序列号""" + return self._run_wmic_command("wmic bios get serialnumber /value") + + def _get_disk_serial(self) -> Optional[str]: + """获取系统盘序列号""" + return self._run_wmic_command("wmic diskdrive get serialnumber /value") + + def _get_machine_guid(self) -> Optional[str]: + """获取Windows机器GUID""" + try: + result = subprocess.run( + 'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid', + shell=True, + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if 'MachineGuid' in line: + return line.split()[-1] + except Exception: + pass + return None + + def _extract_value(self, wmic_output: str) -> str: + """从WMIC输出中提取实际值""" + if not wmic_output: + return "" + + lines = wmic_output.split('\n') + for line in lines: + if '=' in line and not line.strip().endswith('='): + return line.split('=', 1)[1].strip() + return "" + + def _collect_hardware_info(self) -> dict: + """ + 收集硬件信息 + + Returns: + 包含各种硬件信息的字典 + """ + hardware_info = {} + + # 主板序列号 + motherboard = self._get_motherboard_serial() + hardware_info['motherboard'] = self._extract_value(motherboard) if motherboard else "" + + # CPU ID + cpu_id = self._get_cpu_id() + hardware_info['cpu'] = self._extract_value(cpu_id) if cpu_id else "" + + # BIOS序列号 + bios = self._get_bios_serial() + hardware_info['bios'] = self._extract_value(bios) if bios else "" + + # 硬盘序列号 + disk = self._get_disk_serial() + hardware_info['disk'] = self._extract_value(disk) if disk else "" + + # Windows机器GUID + machine_guid = self._get_machine_guid() + hardware_info['machine_guid'] = machine_guid if machine_guid else "" + + # 计算机名称 + hardware_info['computer_name'] = platform.node() + + # MAC地址(作为备用) + hardware_info['mac_address'] = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) + for elements in range(0, 2*6, 2)][::-1]) + + return hardware_info + + def _generate_device_id(self, hardware_info: dict) -> str: + """ + 基于硬件信息生成设备ID + + Args: + hardware_info: 硬件信息字典 + + Returns: + 32位十六进制设备ID + """ + # 过滤掉空值,并按键排序确保一致性 + filtered_info = {k: v for k, v in hardware_info.items() if v and v.strip()} + + # 如果没有任何硬件信息,使用MAC地址作为后备方案 + if not filtered_info: + filtered_info = {'mac_address': hardware_info.get('mac_address', str(uuid.getnode()))} + + # 将所有信息连接成字符串 + info_string = '|'.join(f"{k}:{v}" for k, v in sorted(filtered_info.items())) + + # 使用SHA256生成哈希值 + hash_object = hashlib.sha256(info_string.encode('utf-8')) + device_id = hash_object.hexdigest() + + return device_id + + def _load_cached_device_id(self) -> Optional[str]: + """从缓存文件加载设备ID""" + try: + if os.path.exists(self.cache_file): + with open(self.cache_file, 'r', encoding='utf-8') as f: + cached_id = f.read().strip() + if len(cached_id) == 64: # SHA256哈希长度 + return cached_id + except Exception: + pass + return None + + def _save_device_id_to_cache(self, device_id: str) -> None: + """将设备ID保存到缓存文件""" + try: + with open(self.cache_file, 'w', encoding='utf-8') as f: + f.write(device_id) + except Exception: + pass + + def get_device_id(self) -> str: + """ + 获取设备唯一ID + + Returns: + 64字符的十六进制设备ID + """ + # 如果启用缓存,先尝试从缓存加载 + if self.use_cache: + cached_id = self._load_cached_device_id() + if cached_id: + return cached_id + + # 收集硬件信息 + hardware_info = self._collect_hardware_info() + + # 生成设备ID + device_id = self._generate_device_id(hardware_info) + + # 保存到缓存 + if self.use_cache: + self._save_device_id_to_cache(device_id) + + return device_id + + def get_device_id_short(self, length: int = 16) -> str: + """ + 获取短版本的设备ID + + Args: + length: 返回ID的长度 + + Returns: + 指定长度的设备ID + """ + full_id = self.get_device_id() + return full_id[:length] + + def get_hardware_info(self) -> dict: + """ + 获取硬件信息(用于调试) + + Returns: + 硬件信息字典 + """ + return self._collect_hardware_info() + + +# 使用示例 +def main(): + """使用示例""" + # 创建设备ID生成器实例 + device_generator = DeviceIDGenerator() + + # 获取完整设备ID(64字符) + device_id = device_generator.get_device_id() + print(f"完整设备ID: {device_id}") + + # 获取短版本设备ID(16字符) + short_id = device_generator.get_device_id_short(16) + print(f"短设备ID: {short_id}") + + # 查看硬件信息(调试用) + hardware_info = device_generator.get_hardware_info() + print("\n硬件信息:") + for key, value in hardware_info.items(): + print(f" {key}: {value}") + + +if __name__ == "__main__": + main() \ No newline at end of file