add: 'common' as dependency

This commit is contained in:
2026-04-14 15:14:35 +08:00
parent 6e46b8faf5
commit 875aa5e761
3 changed files with 22 additions and 141 deletions

View File

@@ -5,30 +5,23 @@
</textarea> </textarea>
<script type="text/javascript"> <script type="text/javascript">
function main(url, parameters) { function main(args) {
let ws = new WebSocket(url);
let messages = document.querySelector("#messages"); let messages = document.querySelector("#messages");
let account = new String(args['account']);
let account = new String(parameters['account']);
let name = account.split('@', 1).pop() ?? 'unknown'; let name = account.split('@', 1).pop() ?? 'unknown';
name = name.charAt(0).toLocaleUpperCase() + name.slice(1); name = name.charAt(0).toLocaleUpperCase() + name.slice(1);
document.title = `${name} (${account})`; document.title += ` (${name})`;
ws.addEventListener('message', (e) => { setInterval(async () => {
object = JSON.parse(e.data); let result = await invoke('sync');
lines = Array.from(object.result); let lines = Array.from(result);
for (let line of lines) { for (let line of lines) {
node = document.createTextNode(new String(line).concat('\n')); node = document.createTextNode(new String(line).concat('\n'));
messages.appendChild(node); messages.appendChild(node);
} }
messages.scrollTop = messages.scrollHeight; messages.scrollTop = messages.scrollHeight;
});
setInterval(() => {
data = JSON.stringify({ method: "sync", params: null });
ws.send(data);
}, 1000); }, 1000);
} }
</script> </script>

144
main.py
View File

@@ -2,12 +2,8 @@ import argparse
import openpyxl import openpyxl
import logging import logging
import base64 import base64
import json
import csv import csv
import trio
import trio_websocket as ws
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
from selenium.webdriver.chrome.webdriver import WebDriver from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.chrome.options import Options 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 selenium.webdriver.common.bidi.network import Request as NetworkRequest
from io import BytesIO from io import BytesIO
from enum import Enum from common import jsonrpc2
from typing import Self, Callable from typing import Callable
from pathlib import Path from pathlib import Path
from urllib3 import PoolManager 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('account', type=str)
parser.add_argument('password', type=str) parser.add_argument('password', type=str)
parser.add_argument('-e', '--encoding', type=str, default="utf-8-sig") parser.add_argument('-e', '--encoding', type=str, default="utf-8-sig")
parser.add_argument('-t', '--timeout', type=int, default=60) parser.add_argument('-t', '--timeout', type=int, default=60)
parser.add_argument('-r', '--attempts', type=int, default=3) parser.add_argument('-r', '--attempts', type=int, default=3)
parser.add_argument('-l', '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']) parser.add_argument('-l', '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
args = parser.parse_args()
WEBURL = "https://crm.xiaoman.cn/business/export" WEBURL = "https://crm.xiaoman.cn/business/export"
APIURL = "https://crm.xiaoman.cn/api/opportunityRead/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')): def main(driver: WebDriver, logger = logging.getLogger('main')):
try: try:
http = PoolManager() http = PoolManager()
context = ConnectionContext() context = jsonrpc2.ConnectionContext()
driver.get(str(Path('index.html').resolve()))
endpoint = server.listeners[0]
parameters = vars(args) 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: except Exception as e:
logger.critical('Unable to load starup page', exc_info=e) logger.critical('Unable to load starup page', exc_info=e)
return 2 return 2
@@ -214,7 +146,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
continue continue
try: 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) click(".mm-modal-footer button.okki-btn-primary", wait=False)
except: except:
pass pass
@@ -288,6 +220,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
text = data.decode('ascii') text = data.decode('ascii')
mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
href = f"data:{mime};base64,{text}" href = f"data:{mime};base64,{text}"
filename = filename.replace('.csv', '.xlsx')
driver.switch_to.new_window('tab') driver.switch_to.new_window('tab')
driver.execute_script( driver.execute_script(
@@ -297,71 +230,26 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
link.href = arguments[1]; link.href = arguments[1];
link.click(); link.click();
""", """,
filename.replace('.csv', '.xlsx'), filename,
href 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.close()
driver.switch_to.window(driver.window_handles[1]) driver.switch_to.window(driver.window_handles[1])
logger.info('Done') logger.info('Done')
ready = False 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__': if __name__ == '__main__':
try: try:
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M") logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M")
logger = logging.getLogger() logger = logging.getLogger()
formatter = logger.handlers[0].formatter logger.info('Initializing...')
history = History()
logger.addHandler(history)
level = logging.getLevelNamesMapping().get(args.log_level, 'INFO') level = logging.getLevelNamesMapping().get(args.log_level, 'INFO')
logger.setLevel(level) logger.setLevel(level)
jsonrpc2.run(logger)
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()
logger.info('Creating automation instance') logger.info('Creating automation instance')
opts = Options() opts = Options()

Binary file not shown.