diff --git a/index.html b/index.html index cc033a6..23b9a6f 100644 --- a/index.html +++ b/index.html @@ -100,18 +100,21 @@ - - Enable task slicing - Greet recipients (by proper names if possible) - - Save to file upon exit + + Try recover from a mismatched subject error + + + Enable task slicing Avoid spamming + + Save to file upon exit + @@ -249,8 +252,18 @@ $$$('#send', 'click', async (e) => { break; default: try { + let PickerOptions = { + types: [ + { + description: "Excel Spreadsheet", + accept: { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ['.xlsx'] + } + } + ] + }; $('#send > span.icon').classList.remove('hidden'); - let [handle] = await showOpenFilePicker(); + let [handle] = await showOpenFilePicker(PickerOptions); let file = await handle.getFile(); let buffer = await new Promise((resolve, reject) => { @@ -265,7 +278,7 @@ $$$('#send', 'click', async (e) => { Status = 'READY'; $('#fileLabel').innerHTML = file.name; - $('#uptimeLabel').dataset.timestamp = dayjs().unix(); + $('#uptimeLabel').dataset.seconds = 0; $('#send > span.text').innerHTML = 'Send'; $('#cancel').disabled = false; $('#send').disabled = true; @@ -420,10 +433,13 @@ function main(url, parameters, locales) { $('#cancel').disabled = false; break; case 'setProgress': - let [sent, name, recipient] = args; + let [index, name, recipient, sent, warnings, errors] = args; let label = name ? `${name} <${recipient}>` : recipient; $('#recipientLabel').innerHTML = label; $('#progressLabel').dataset.sent = sent; + $('#progressLabel').dataset.index = index; + $('#progressLabel').dataset.errors = errors; + $('#progressLabel').dataset.warnings = warnings; break; case 'setStatus': switch (args[0]) { @@ -443,7 +459,8 @@ function main(url, parameters, locales) { $('#send > span.icon').classList.add('hidden'); break; case 'FINISH': - alert('任务结束') + let {sent, warnings, errors} = $('#progressLabel').dataset; + alert(`${sent||0} Sent; ${warnings||0} Warning(s); ${errors||0} Error(s)`); case 'CANCEL': Status = null; $('#send > span.text').innerHTML = 'Open'; @@ -488,21 +505,19 @@ function main(url, parameters, locales) { $('#statusLabel').innerHTML = Status ? Status.charAt(0).toUpperCase() + Status.slice(1).toLowerCase() : 'Idle'; if (Status === 'RUNNING') { - let sent = Number($('#progressLabel').dataset.sent) || 0; + let index = Number($('#progressLabel').dataset.index) || 0; let limit = Number($('#subcategory').dataset.limit) || Limit; - let percentage = parseFloat((sent / limit * 100).toFixed(2)); - $('#progressLabel').innerHTML = `${sent} / ${limit} (${percentage} %)`; - - let timestamp = Number($('#uptimeLabel').dataset.timestamp); - let uptime = dayjs.duration(dayjs().diff(dayjs.unix(timestamp))); - let rate = sent / uptime.asSeconds(); + let percentage = parseFloat((index / limit * 100).toFixed(2)); + $('#progressLabel').innerHTML = `${index} / ${limit} (${percentage} %)`; + let uptime = dayjs.duration($('#uptimeLabel').dataset.seconds, 'second'); + let rate = Number($('#progressLabel').dataset.sent) / uptime.asSeconds(); let spm = parseFloat(Number(rate*60).toFixed(2)); - let speed = spm >= 1 ? `${spm} per minute` : null; - let remaining = rate > 0 ? dayjs.duration((limit - sent) / rate, 'second').humanize() : null; - $('#remainingLabel').innerHTML = [remaining, speed].filter((x) => x).join(' - '); + $('#remainingLabel').innerHTML = rate > 0 ? `${dayjs.duration((limit - index) / rate, 'second').humanize()}` : ''; + $('#remainingLabel').innerHTML += spm >= 1 ? `${spm} per minute` : ''; $('#uptimeLabel').innerHTML = uptime.format("HH:mm:ss"); + $('#uptimeLabel').dataset.seconds = uptime.asSeconds() + 1; } }, 1000); } @@ -525,6 +540,12 @@ label { width: fit-content; } +.rate:not(:first-child)::before { + padding: 0em 0.4em; + content: '–'; + color: #666; +} + .inline-flex { display: inline-flex; align-items: center; @@ -536,6 +557,10 @@ label { white-space: nowrap; } +.warning { + color: orangered; +} + .hidden { display: none; } diff --git a/main.py b/main.py index 0ff4a0c..1bf34bf 100644 --- a/main.py +++ b/main.py @@ -47,7 +47,6 @@ args = parser.parse_args() inbox, outbox = Queue(), Queue() connection = None socket = None -server = None sent = 0 errors = 0 @@ -84,8 +83,8 @@ class Command: return json.dumps(pack) def tell(message, exception=None, level=2): - if exception is not None: message = ': '.join([message, exception.__class__.__name__]) - elif isinstance(exception, Exception): message += '\n' + ''.join(traceback.format_exception(exception)) + message = ': '.join(filter(None, [message, exception.__class__.__name__ if isinstance(exception, Exception) else exception])) + if isinstance(exception, Exception): message += '\n' + ''.join(traceback.format_exception(exception)) if not outbox.is_shutdown: outbox.put(Command('tell', message, level)) @@ -143,9 +142,9 @@ def main(driver: WebDriver): except TimeoutException: continue except: break - def ready(driver, predicate): + def ready(driver, predicate=lambda x: x.find_element(By.CSS_SELECTOR, ".io-ox-busy")): try: - wait = WebDriverWait(driver, timeout=parameters.get('timeout')) + wait = WebDriverWait(driver, timeout=parameters.get('interval')) wait.until(predicate, '操作超时') except: return True @@ -198,11 +197,12 @@ def main(driver: WebDriver): except: continue try: + condition = EC.presence_of_element_located # 打开草稿箱 - click("li[data-id='default0/Brouillons']", condition=EC.presence_of_element_located) - click("button[data-id='default0/Brouillons']", condition=EC.presence_of_element_located) + click("li[data-id='default0/Brouillons']", condition) + click("button[data-id='default0/Brouillons']", condition) # 打开邮件 - click("ul[aria-label='List view'] li[data-index='0']", condition=EC.presence_of_element_located) + click("ul[aria-label='List view'] li[data-index='0']", condition) except Exception as e: tell('打开草稿邮件时发生错误', e, level=1) pass @@ -341,7 +341,7 @@ def main(driver: WebDriver): mark = sents[current] occurrence = occurrences.setdefault(code, [0]) if code else [0] - outbox.put(Command('setProgress', index, name, recipient)) + outbox.put(Command('setProgress', index, name, recipient, sent, warnings, errors)) if mark is not None and str(mark).strip(): tell(f'已跳过项目 {recipient}') @@ -362,11 +362,32 @@ def main(driver: WebDriver): if (target := get_address()) != address: raise Exception(f'邮件发件地址与设定不一致\n>> {address}\n>> {target}') + if (target := get_subject()) != subject: - raise Exception(f'邮件主题与设定不一致\n>> {subject}\n>> {target}') + exception = Exception(f'邮件主题与设定不一致\n>> {subject}\n>> {target}') + condition = EC.presence_of_element_located + if not parameters.get('recovery'): raise exception + # 打开存档 + click("li[data-id='default0/Archives'] li.folder:nth-child(1)", condition) + ready(driver) + click("ul[aria-label='List view'] li[data-index='0']", condition) + click("ul.classic-toolbar button[aria-label='More actions']", condition) + # 复制邮件 + click(".abs + ul.dropdown-menu a[data-action='io.ox/mail/actions/copy']", condition) + click(".modal-dialog ul.subfolders li[data-id='default0/Brouillons']", condition) + click(".modal-dialog .modal-footer button[data-action='ok']", condition) + # 返回草稿箱 + click("li[data-id='default0/Brouillons']") + ready(driver) - click("button[aria-label='Edit copy']") - ready(driver, lambda x: x.find_element(By.CSS_SELECTOR, ".io-ox-busy")) + for attempt in range(parameters.get('retry')): + click("ul[aria-label='List view'] li[data-index='0']", condition) + if get_subject() == subject: break + time.sleep(parameters.get('interval')) + if not get_subject() == subject: raise exception + + click("ul.classic-toolbar button[aria-label='Edit copy']") + ready(driver) locate("div.io-ox-mail-compose-window iframe", condition=EC.frame_to_be_available_and_switch_to_it) if parameters.get('greet') and greetings and timezone: @@ -516,7 +537,7 @@ async def handler(request: ws.WebSocketRequest): inbox.shutdown(immediate=True) async def backend(listen='127.0.0.1', port=0): - global server, socket + global socket listeners = await trio.open_tcp_listeners(port, host=listen) server = ws.WebSocketServer(handler, listeners, max_message_size=125_000_000) socket = listeners[0].socket