From 875aa5e761d77ccb31b5100c1b06ba41868cc319 Mon Sep 17 00:00:00 2001 From: break27 Date: Tue, 14 Apr 2026 15:14:35 +0800 Subject: [PATCH] add: 'common' as dependency --- index.html | 19 ++----- main.py | 144 ++++++----------------------------------------- requirements.txt | Bin 816 -> 1018 bytes 3 files changed, 22 insertions(+), 141 deletions(-) diff --git a/index.html b/index.html index 486ea89..6b23a51 100644 --- a/index.html +++ b/index.html @@ -5,30 +5,23 @@ diff --git a/main.py b/main.py index 178572a..491b60d 100644 --- a/main.py +++ b/main.py @@ -2,12 +2,8 @@ import argparse import openpyxl import logging import base64 -import json import csv -import trio -import trio_websocket as ws - from selenium.common.exceptions import StaleElementReferenceException, TimeoutException from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver.chrome.options import Options @@ -18,95 +14,31 @@ from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.common.bidi.network import Request as NetworkRequest from io import BytesIO -from enum import Enum -from typing import Self, Callable +from common import jsonrpc2 +from typing import Callable from pathlib import Path from urllib3 import PoolManager -from threading import Thread +from subprocess import Popen -parser = argparse.ArgumentParser(description="Opportunity Exporter") +parser = argparse.ArgumentParser(description="Opportunity Export") parser.add_argument('account', type=str) parser.add_argument('password', type=str) parser.add_argument('-e', '--encoding', type=str, default="utf-8-sig") parser.add_argument('-t', '--timeout', type=int, default=60) parser.add_argument('-r', '--attempts', type=int, default=3) parser.add_argument('-l', '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) +args = parser.parse_args() WEBURL = "https://crm.xiaoman.cn/business/export" APIURL = "https://crm.xiaoman.cn/api/opportunityRead/export" -args = parser.parse_args() -handlers = {} -connection, server = None, None - -class Request[T]: - def __init__(self, method: str, params: T): - if not isinstance(method, str): raise TypeError() - self.method = method - self.params = params - - @classmethod - def load(cls, raw: bytes) -> Self: - data = json.loads(raw) - return cls(**data) - -class Response[T]: - def __init__(self, result: T): - self.result = result - - def __str__(self): - data = { 'result': self.result } - return json.dumps(data) - -class Error(Enum): - PARSE_ERROR = -200 - INVALID_REQUEST = -300 - METHOD_NOT_FOUND = -400 - INTERNAL_ERROR = -500 - - def __str__(self): - data = { 'error': self.value } - return json.dumps(data) - -class History(logging.Handler): - def __init__(self): - super().__init__() - self.records = [] - - def emit(self, record): - self.records.append(record) - - def truncate(self) -> list: - copy = self.records.copy() - self.records.clear() - return copy - -class ConnectionContext: - def __enter__(self): - self.healthcheck() - return self - - def __exit__(self, exc_type, exc, tb): - self.healthcheck() - return True - - class Error(Exception): - pass - - @classmethod - def healthcheck(cls): - if connection.closed is not None: - raise cls.Error() - def main(driver: WebDriver, logger = logging.getLogger('main')): try: http = PoolManager() - context = ConnectionContext() - - driver.get(str(Path('index.html').resolve())) - endpoint = server.listeners[0] + context = jsonrpc2.ConnectionContext() parameters = vars(args) - driver.execute_script(f"main(...arguments);", f'ws://{endpoint.address}:{endpoint.port}', parameters) + driver.get(str(Path('index.html').resolve())) + driver.execute_script(jsonrpc2.prelude(), parameters) except Exception as e: logger.critical('Unable to load starup page', exc_info=e) return 2 @@ -214,7 +146,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): continue try: - locate(".mm-modal-content .mm-modal-body input", wait=False).send_keys(parameters.get('password')) + locate(".safe-verify-dialog:not([style*='display: none']) .mm-modal-content .mm-modal-body input", wait=False).send_keys(parameters.get('password')) click(".mm-modal-footer button.okki-btn-primary", wait=False) except: pass @@ -288,6 +220,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): text = data.decode('ascii') mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' href = f"data:{mime};base64,{text}" + filename = filename.replace('.csv', '.xlsx') driver.switch_to.new_window('tab') driver.execute_script( @@ -297,71 +230,26 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): link.href = arguments[1]; link.click(); """, - filename.replace('.csv', '.xlsx'), + filename, href ) + try: Popen(['start', (Path.home() / 'Downloads' / filename).as_posix()], shell=True) + except Exception as e: logger.warning('Unable to open exported excel file at %s' % file, exc_info=e) + driver.close() driver.switch_to.window(driver.window_handles[1]) logger.info('Done') ready = False -async def handler(request: ws.WebSocketRequest, logger = logging.getLogger('websocket')): - global connection - websocket = await request.accept() - - if connection is None: - connection = websocket - else: - await websocket.aclose(code=1000, reason="Non-singular connection prohibited") - return - - while True: - try: - message = await connection.get_message() - inbound = Request.load(message) - - handler = handlers[inbound.method] - results = handler(inbound.params) - response = Response(results) - except json.decoder.JSONDecodeError: - logger.error('Parse error') - response = Error.PARSE_ERROR - except TypeError: - logger.error('Invalid request') - response = Error.INVALID_REQUEST - except KeyError: - logger.error('Method not found: `%s`', inbound.method) - response = Error.METHOD_NOT_FOUND - except Exception as e: - logger.error('Internal error', exc_info=e) - response = Error.INTERNAL_ERROR - - await connection.send_message(str(response)) - -async def backend(listen='127.0.0.1', port=0): - import _thread as t - global server - - listeners = await trio.open_tcp_listeners(port, host=listen) - server = ws.WebSocketServer(handler, listeners, max_message_size=125_000_000) - await server.run() - t.interrupt_main() - if __name__ == '__main__': try: logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M") logger = logging.getLogger() - formatter = logger.handlers[0].formatter - history = History() - logger.addHandler(history) + logger.info('Initializing...') level = logging.getLevelNamesMapping().get(args.log_level, 'INFO') logger.setLevel(level) - - logger.info('Initializing...') - handlers.setdefault('sync', lambda _: list(map(lambda x: formatter.format(x), history.truncate()))) - thread = Thread(target=lambda: trio.run(backend), daemon=True) - thread.start() + jsonrpc2.run(logger) logger.info('Creating automation instance') opts = Options() diff --git a/requirements.txt b/requirements.txt index 029c6199eadbc8dc99728b1e2fca4ab2c32798fa..99253751e3db5d418fa954ddf67990fd530a972f 100644 GIT binary patch delta 209 zcmYj~I}XAy5CumRa1DASCnkvlRH?WD2M7rtOF)nkDhfCPM}dONQSmSoXyw(u(a!qb ze+_QeOGU$8)o@@)p|`=)F=DIUainENr22A0$w}Oce|m+Hy^3v-#5P- v1#8CC#AI`vf+JxfU?pWtm6zoD$Y~{4wwkOql8{mvb#O|Zk2R%_^RxE`#3Ut> delta 11 ScmeyxzJYB*!)7nW4NL$YYXo=z