From 7cb59b51225fd6516d3fcc93ee0e20392bb2d95a Mon Sep 17 00:00:00 2001 From: break27 Date: Tue, 26 May 2026 11:59:46 +0800 Subject: [PATCH] update: bump 'common' version to 0.1.15 --- index.html | 264 +++++++++++++++++++++++------------------------ main.py | 141 +++++++++++++++---------- requirements.txt | Bin 922 -> 922 bytes 3 files changed, 213 insertions(+), 192 deletions(-) diff --git a/index.html b/index.html index 887ddea..d77a31c 100644 --- a/index.html +++ b/index.html @@ -128,157 +128,151 @@ - @@ -357,4 +351,4 @@ label { opacity: 0.6; } } - \ No newline at end of file + diff --git a/main.py b/main.py index f24a64b..182dc46 100644 --- a/main.py +++ b/main.py @@ -11,8 +11,9 @@ from selenium.common.exceptions import TimeoutException, NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys -from common import jsonrpc2 from common.utils import * +from common.timer import Timer +from common.jsonrpc2 import ServiceProvider from common.actionflow import Action, ActionFlow from enum import Enum @@ -37,8 +38,9 @@ WEBURL = "https://crm.xiaoman.cn/%s" APIURL = "https://%s.vosfactures.fr" def main(driver: WebDriver, logger = logging.getLogger('main')): - http = PoolManager() parameters = vars(args) + http = PoolManager() + sp = ServiceProvider.default() class Status(Enum): IDLE = 0 @@ -86,41 +88,41 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): t1.start() t2.start() - jsonrpc2.define('begin', begin) - jsonrpc2.define('pause', pause) - jsonrpc2.define('resume', resume) - jsonrpc2.define('status', lambda: status.name) - jsonrpc2.define('uptime', lambda: [t1.delta()]) + sp.set('begin', begin) + sp.set('pause', pause) + sp.set('resume', resume) + sp.set('status', lambda: status.name) + sp.set('uptime', lambda: [t1.delta()]) try: profiles = [ Profile(**{ k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), p) }) - for p in parameters.get('profile') + for p in parameters['profile'] ] except Exception as e: logger.critical('Unable to load profiles', exc_info=e) return 2 try: - driver.get(str(Path('index.html').resolve())) - driver.execute_script(jsonrpc2.prelude(), list(map(vars, profiles)), parameters) + sp.set('context', lambda: { 'profiles': list(map(vars, profiles)), 'parameters': parameters }) + driver.get(sp.run()) except Exception as e: logger.critical('Unable to load starup page', exc_info=e) return 3 try: driver.switch_to.new_window('tab') - driver.set_page_load_timeout(parameters.get('timeout')) + driver.set_page_load_timeout(parameters['timeout']) driver.get(WEBURL % 'product') except TimeoutException: logger.warning('Timeout') driver.execute_script("window.stop();") - setup(driver, parameters.get('attempts'), parameters.get('timeout'), parameters.get('interval')) + setup(driver, parameters) until(lambda x: 'loginProgress' in x.find_element(By.TAG_NAME, "body").get_attribute('class'), watch=False) logger.info('Waiting for authentication to complete...') - if (account := parameters.get('account')) and (password := parameters.get('password')): + if (account := parameters['account']) and (password := parameters['password']): try: logger.info('Logging in as %s (%s)', str.split(account, '@', 1).pop(0).capitalize(), account) locate("input.account").send_keys(account) @@ -137,7 +139,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): logger.info('Done') break except: - sleep(parameters.get('interval')) + sleep(parameters['interval']) class ProductInfo: def __init__(self, file): @@ -164,7 +166,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): logger.info('Downloading product list...') click("header .okki-space .okki-space-item:nth-child(1) button") click(".okki-dropdown button") - sleep(parameters.get('interval')) + sleep(parameters['interval']) click(".okki-modal.product-export-wrap .mm-selector-rendered") click(".mm-outside.ui-field-selector-popper .selector-area:nth-child(1) button") click(".okki-modal.product-export-wrap .okki-modal-footer button.okki-btn-primary") @@ -172,14 +174,14 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): while True: try: click(".okki-modal.product-export-wrap .okki-modal-footer button.okki-btn-primary") - sleep(parameters.get('interval')) + sleep(parameters['interval']) click(".okki-modal.product-export-wrap .okki-modal-body .virtual-list-wrap .vue-recycle-scroller__item-wrapper > div:nth-child(1) button.okki-btn-link", wait=False) filename = locate(".okki-modal.product-export-wrap .okki-modal-body .virtual-list-wrap .vue-recycle-scroller__item-wrapper > div:nth-child(1) > div > div:nth-child(1) span").get_attribute('title') break except: - sleep(parameters.get('interval')) + sleep(parameters['interval']) - file = Path(parameters.get('directory')).joinpath(filename) + file = Path(parameters['directory']).joinpath(filename) until(lambda _: file.exists(), watch=False) p = ProductInfo(file) driver.close() @@ -190,12 +192,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): logger.critical('Unable to load products', exc_info=e) return 4 - def wait(seconds: float): - while not sleep(seconds): - if status == Status.RUNNING: - break - - def fetch(url: str, method = 'GET', retry = parameters.get('attempts')): + def fetch(url: str, method = 'GET', retry = parameters['attempts']): for attempt in range(1, retry + 1): try: response = http.request(method, url) @@ -212,8 +209,18 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): @classmethod def perform(cls): - wait(1) + if status == Status.RUNNING: return False + sleep(0.2); return True + + class Sleep(Action): + @classmethod + def prepare(cls): return True + + @classmethod + def perform(cls): + sleep(parameters['interval']) + return False class Cancel(Action): @classmethod @@ -241,8 +248,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): flow = ActionFlow() flow.append(Wait) - flow.allow(Wait) - flow.do(Wait) + flow.append(Sleep) flow.append(Cancel) flow.append(Skip) @@ -250,13 +256,18 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): progress = { 'task': '' } selection = 0 - jsonrpc2.define('actions', lambda: flow.capabilities()) - jsonrpc2.define('cancel', lambda: flow.do(Cancel)) - jsonrpc2.define('skip', lambda: flow.do(Skip)) - jsonrpc2.define('progress', lambda: progress) + sp.set('actions', lambda: flow.capabilities()) + sp.set('cancel', lambda: flow.do(Cancel)) + sp.set('skip', lambda: flow.do(Skip)) + sp.set('progress', lambda: progress) - while not wait(1): + while True: try: + flow.allow(Wait) + flow.allow(Sleep) + flow.do(Wait) + flow.react() + for i in range(len(driver.window_handles), 1, -1): driver.switch_to.window(driver.window_handles[i-1]) driver.close() @@ -284,8 +295,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): progress['task'] = 'Task 1 of 4' t2.clear() t2.start() - jsonrpc2.remove('uptime') - jsonrpc2.define('uptime', lambda: [t1.delta(), t2.delta()]) + sp.pop('uptime') + sp.set('uptime', lambda: [t1.delta(), t2.delta()]) base = APIURL % profile.subdomain data = list() @@ -308,8 +319,9 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): logger.info('Downloading invoices (%d)', page) data.extend(result) + flow.do(Wait) + flow.do(Sleep) flow.react() - wait(parameters.get('interval')) except Skip: pass except Cancel: @@ -373,6 +385,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): try: for i, item in enumerate(data, 1): + flow.do(Wait) flow.react() number: str = item['number'] logger.info('[%d/%d] Preprocessing data for %s', i, len(data), number) @@ -447,8 +460,9 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): try: filename = f'Order-Import-{profile.name}-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.xlsx' - file = Path(parameters.get('directory')).joinpath(filename) + file = Path(parameters['directory']).joinpath(filename) logger.info('Saving excel file to %s', str(file)) + flow.do(Wait) flow.react() workbook.save(file) except Skip: @@ -473,6 +487,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): click(".product-import-img-box .mm-selector-rendered") click(".mm-outside.mm-select-dropdown ul li:nth-child(%d) span" % (1 if options.get('draft') else 6)) + flow.do(Wait) flow.react() locate(".big-file-upload input", wait=False).send_keys(str(file)) click(".product-import-img-footer button") @@ -483,7 +498,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): innerText = driver.find_element(By.CSS_SELECTOR, ".img-box-result-title").get_attribute('innerText') if innerText == '导入完成': break except: - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() click(".mm-notification-container .mm-icon-close") click(".product-import-img-footer button.mm-button__primary") @@ -492,10 +508,12 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): if err.get_attribute('disabled') is None: logger.warning('Incomplete import detected; downloaded 1 related document') err.click() - wait(1) + flow.do(Sleep) + flow.react() click(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a") - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() logger.info('Done') except Skip: pass @@ -551,7 +569,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): attempts = 0 continue - if attempts > parameters.get('attempts'): + if attempts > parameters['attempts']: logger.warning('Exhausted all allowed attempts; skipping %s', number) index += 1 attempts = 0 @@ -561,6 +579,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): progress['index'] = index flow.allow(Cancel) flow.allow(Skip) + flow.do(Wait) flow.react() try: @@ -579,17 +598,20 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): 'array_flag': 0 }]) driver.get(url.encode()) - wait(parameters.get('interval')) + flow.do(Wait) + flow.do(Sleep) flow.react() url = Parse(driver.current_url) if url.get('page') != page: raise Exception(number) - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() link = locate(".virtual-list-wrap .vue-recycle-scroller .vue-recycle-scroller__item-wrapper > div:nth-child(1) .cell[data-cci='1'] a", wait=False) if link.text != number: continue logger.info('[%d/%d] Processing %s...', index+1, len(data), number) link.click() driver.switch_to.window(driver.window_handles[3]) + flow.do(Wait) flow.react() click(".sticky .okki-space-item:nth-child(1) button") break @@ -614,7 +636,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): driver.switch_to.new_window('tab') url = Parse(WEBURL % 'crm/business/list') driver.get(url.encode({ 'mode': 'list' })) - wait(parameters.get('interval')) + flow.do(Wait) + flow.do(Sleep) flow.react() try: click(".new-wrapper .paas-next-invoice-list-filter-line-wrapper .okki-btn-background-ghost", wait=False) @@ -627,12 +650,14 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): url.set('curPage', page) url.set('pageSize', 1) driver.get(url.encode({ 'keyword': match, 'search_field': 'serial_keyword' })) - wait(parameters.get('interval')) + flow.do(Wait) + flow.do(Sleep) flow.react() url = Parse(driver.current_url) if url.get('curPage') != page: raise Exception(match) - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() cell = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(3) .ow-serial-read-pretty_ellipsis", wait=False) if cell.text != match: continue link = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(6) a", wait=False) @@ -656,6 +681,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): if opportunity is not None: try: + flow.do(Wait) flow.react() dropdown = locate("#rc_select_1") dropdown.clear() @@ -690,9 +716,10 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): for page in count(1): hits = 0 iteration = 0 + flow.do(Wait) flow.react() - while hits < pagination and iteration < parameters.get('attempts'): + while hits < pagination and iteration < parameters['attempts']: iteration += 1 height = int(wrapper.get_attribute('clientHeight')) if iteration > 1 else 0 driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper) @@ -700,6 +727,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): rows = wrapper.find_elements(By.CSS_SELECTOR, ".row-item") for row in reversed(rows) if iteration > 1 else rows: + flow.do(Wait) flow.react() driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper) driver.execute_script("arguments[0].scrollTo(0, arguments[1]);", wrapper, height) @@ -712,7 +740,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): driver.execute_script("arguments[0].scroll(400, 0);", wrapper) driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", row) driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper) - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() target = row.find_element(By.CSS_SELECTOR, ".cell[data-cci='6'] input") if (target.get_attribute('value') == '0'): @@ -726,6 +755,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): button = locate(".text-right li.okki-pagination-next button", condition=None) if button.get_attribute('disabled') is not None and len(ids) < len(positions): raise Exception('Product list imcomplete; expected %d, got %d' % (len(positions), len(ids))) + flow.do(Wait) flow.react() click(button) except Skip: @@ -741,16 +771,19 @@ def main(driver: WebDriver, logger = logging.getLogger('main')): try: click(".ow-box button.okki-btn-round", wait=False) - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() except Exception as e: logger.warning('Unable to unset additional fees; skipping', exc_info=e) try: + flow.do(Wait) flow.react() flow.allow(Cancel, False) flow.allow(Skip, False) click(".sticky.bottom-0 button.okki-btn-primary", condition=None) - wait(parameters.get('interval')) + flow.do(Sleep) + flow.react() except Skip: pass except Cancel: @@ -775,14 +808,6 @@ if __name__ == '__main__': level = logging.getLevelNamesMapping().get(args.log_level, 'INFO') logger.setLevel(level) - logger.info('Initializing...') - history = jsonrpc2.History() - logger.addHandler(history) - opts = jsonrpc2.Options() - jsonrpc2.define('history', lambda: history.truncate()) - jsonrpc2.run(opts) - - logger.info('Creating automation instance') opts = ChromeOptions() opts.enable_downloads = True opts.add_argument('--deny-permission-prompts') @@ -791,6 +816,8 @@ if __name__ == '__main__': with keep.presenting(): driver = Chrome(options=opts) status = main(driver) + except KeyboardInterrupt: + status = 0 except Exception as e: logger.critical('Fatal error', exc_info=e) status = 1 diff --git a/requirements.txt b/requirements.txt index 8c5210ccd63f7ac3cea906213397b5b28ade72ab..e201b798f3053ece86d8cd60b4da0f912041f192 100644 GIT binary patch delta 91 zcmbQmK8t*L?AB>$hTxL0>Ts^A0%$dV8M_I6gLFoWFTn> Z1!5AoS43skms!aom drvT|hpgI$VBp_`8