diff --git a/index.html b/index.html index 1405c93..212b205 100644 --- a/index.html +++ b/index.html @@ -90,8 +90,8 @@
- - + +
@@ -230,13 +230,13 @@ $$$('#send', 'click', async (e) => { switch (Status) { case 'STANDBY': Connection.send('RESUME'); - $('#send > span.text').innerHTML = 'Pause'; + $('#send > span.text').innerText = 'Pause'; $('#send > span.icon').classList.remove('hidden'); $('#send').disabled = true; break; case 'RUNNING': Connection.send('ONHOLD'); - $('#send > span.text').innerHTML = 'Resume'; + $('#send > span.text').innerText = 'Resume'; $('#send > span.icon').classList.add('hidden'); $('#send').disabled = true; break; @@ -267,7 +267,7 @@ $$$('#send', 'click', async (e) => { Status = 'RUNNING'; Timer.setTimestamp(true); - $('#send > span.text').innerHTML = 'Pause'; + $('#send > span.text').innerText = 'Pause'; $('#send > span.icon').classList.remove('hidden'); $('#skip').disabled = false; break; @@ -298,8 +298,8 @@ $$$('#send', 'click', async (e) => { Connection.send(buffer); Status = 'READY'; - $('#fileLabel').innerHTML = file.name; - $('#send > span.text').innerHTML = 'Send'; + $('#fileLabel').innerText = file.name; + $('#send > span.text').innerText = 'Send'; $('#cancel').disabled = false; $('#send').disabled = true; } finally { @@ -433,7 +433,7 @@ function main(url, parameters, locales) { break; case 'setSubject': let [subject] = args; - $('#subjectLabel').innerHTML = subject; + $('#subjectLabel').innerText = subject; break; case 'setMetadata': [Limit, Columns] = args; @@ -454,8 +454,8 @@ function main(url, parameters, locales) { break; case 'setProgress': let [index, name, recipient, sent, warnings, errors] = args; - let label = name ? `${name} <${recipient}>` : recipient; - $('#recipientLabel').innerHTML = label; + let label = name ? `${name} <${recipient}>` : recipient; + $('#recipientLabel').innerText = label; $('#progressLabel').dataset.sent = sent; $('#progressLabel').dataset.index = index; $('#progressLabel').dataset.errors = errors; @@ -477,7 +477,7 @@ function main(url, parameters, locales) { break; case 'FAILED': Status = 'STANDBY'; - $('#send > span.text').innerHTML = 'Resume'; + $('#send > span.text').innerText = 'Resume'; $('#send > span.icon').classList.add('hidden'); break; case 'FINISH': @@ -485,7 +485,7 @@ function main(url, parameters, locales) { alert(`${sent||0} Sent; ${warnings||0} Warning(s); ${errors||0} Error(s)`); case 'CANCEL': Status = null; - $('#send > span.text').innerHTML = 'Open'; + $('#send > span.text').innerText = 'Open'; $('#send > span.icon').classList.add('hidden'); $('#cancel').disabled = true; $('#skip').disabled = true; @@ -518,22 +518,22 @@ function main(url, parameters, locales) { let city = timezone.split('/')[1]; let date = dayjs().tz(timezone).format("YYYY-MM-DD HH:mm:ss"); - $('#timezoneLabel').innerHTML = `${date} (${city})`; - $('#statusLabel').innerHTML = Status ? Status.charAt(0).toUpperCase() + Status.slice(1).toLowerCase() : 'Idle'; + $('#timezoneLabel').innerText = `${date} (${city})`; + $('#statusLabel').innerText = Status ? Status.charAt(0).toUpperCase() + Status.slice(1).toLowerCase() : 'Idle'; if (Status === 'RUNNING') { let index = Number($('#progressLabel').dataset.index) || 0; let limit = Number($('#subcategory').dataset.limit) || Limit; let percentage = parseFloat((index / limit * 100).toFixed(2)); - $('#progressLabel').innerHTML = `${index} / ${limit} (${percentage} %)`; + $('#progressLabel').innerText = `${index} / ${limit} (${percentage} %)`; let uptime = Timer.getTimedelta(); - $('#uptimeLabel').innerHTML = uptime.format("HH:mm:ss"); + $('#uptimeLabel').innerText = uptime.format("HH:mm:ss"); let rate = Number($('#progressLabel').dataset.sent) / uptime.asSeconds(); let spm = parseFloat(Number(rate*60).toFixed(2)); - $('#remainingLabel').innerHTML = rate > 0 ? `${dayjs.duration((limit - index) / rate, 'second').humanize()}` : ''; - $('#remainingLabel').innerHTML += spm >= 1 ? `${spm} per minute` : ''; + $('#remainingLabel').innerText = rate > 0 ? `${dayjs.duration((limit - index) / rate, 'second').humanize()}` : ''; + $('#remainingLabel').innerText += spm >= 1 ? `${spm} per minute` : ''; } }, 500); } diff --git a/main.py b/main.py index c8ba2ea..5181a73 100644 --- a/main.py +++ b/main.py @@ -41,7 +41,7 @@ parser.add_argument('-a', '--address', type=str, nargs='?') parser.add_argument('-p', '--password', type=str, nargs='?') parser.add_argument('-t', '--timeout', type=int, nargs='?', default=60) parser.add_argument('-i', '--interval', type=int, nargs='?', default=10) -parser.add_argument('-r', '--retry', type=int, nargs='?', default=3) +parser.add_argument('-r', '--attempts', type=int, nargs='?', default=3) args = parser.parse_args() inbox, outbox = Queue(), Queue() @@ -102,7 +102,7 @@ def main(driver: WebDriver): return 1 def locate(selector, condition=EC.presence_of_element_located, parent=driver) -> WebElement: - for attempt in range(parameters.get('retry')): + for attempt in range(parameters.get('attempts')): try: wait = WebDriverWait(parent, timeout=parameters.get('timeout')) return wait.until(condition((By.CSS_SELECTOR, selector))) @@ -122,7 +122,7 @@ def main(driver: WebDriver): value = counter() driver.execute_script("arguments[0].addEventListener('click', () => arguments[0].setAttribute('taximeter', arguments[1] + 1));", element, value) - for attempt in range(parameters.get("retry")): + for attempt in range(parameters.get('attempts')): try: if not error: element.click() else: driver.execute_script("arguments[0].click();", element) @@ -255,9 +255,10 @@ def main(driver: WebDriver): filename = str(inbox.get()) buffer = BytesIO(inbox.get()) - workbook = pandas.read_excel(buffer, sheet_name=0) - frame = workbook.where(pandas.notnull(workbook), None) - limit = int(frame.last_valid_index()) + spreadsheet = pandas.read_excel(buffer, sheet_name=0) + frame = spreadsheet.where(pandas.notnull(spreadsheet), None) + index = frame.index + limit = len(index) driver.switch_to.window(driver.window_handles[1]) columns = { column: frame[column].unique() for column in frame.columns } @@ -272,78 +273,84 @@ def main(driver: WebDriver): if request is not None: parameters = dict(json.loads(request)) break + + ca = parameters.get('column_address') + cn = parameters.get('column_name') + cc = parameters.get('column_code') + cs = parameters.get('column_sent') + cp = parameters.get('column_pays') + + if parameters.get('slice'): + if subcategory := parameters.get('subcategory'): + data = frame[frame[cp].isin(subcategory)] + index = data.index + + offset = parameters.get('offset') + size = parameters.get('chunk_size') + start = offset * size + end = start + size + index = index[start:end] + limit = len(index) + + if ca not in frame or not limit: + tell('输入无效', filename, level=1) + outbox.put(Command('setStatus', 'FINISH')) + continue + if cs not in frame: + frame[cs] = None + + seriesdict = frame.iloc[index].to_dict(orient='list') + recipients = seriesdict.get(ca, [None] * limit) + names = seriesdict.get(cn, [None] * limit) + codes = seriesdict.get(cc, [None] * limit) + + rate = 60 / parameters.get('interval') + length = limit - frame.loc[index, cs].count() + + tell(f'已读取邮件:{subject}') + tell(f'指定发件人:{address}') + tell(f'已读取联系人信息共 {limit} 条') + tell(f'预计发送数量 {length}') + + tell(f'当前发送速率 {round(rate, 2)} 封/分钟') + if rate > 8.33: tell('当前发送速率已超出限制 8.33 封/分钟', level=1) + + tell(f'预计使用时间 {timedelta(minutes=length / rate)}') + tell(f'已设定允许重试次数:{parameters.get('attempts')}') + tell(f'已设定最大重复次数:{parameters.get('max_occurrence') or '无限制'}') + + locale = parameters.get('locale') + greetings = [item for item in Greetings.presets() if item.locale == locale][0] + timezone = ZoneInfo(greetings.timezone) + tell(f'当前时区:{greetings.timezone}') + tell(f'当前语言:{greetings.locale.upper()}') except Faillable: continue except Exception as e: tell('读取数据时发生错误', e, level=0) return 5 - if parameters.get('slice'): - if subcategory := parameters.get('subcategory'): - column = parameters.get('column_pays') - frame = frame[frame[column].isin(subcategory)] - - offset = parameters.get('offset') - size = parameters.get('chunk_size') - start = size * offset - end = start + size - frame = frame.iloc[start:end] - - data = frame.to_dict(orient='list') - recipients = data.get(parameters.get('column_address'), []) - limit = len(recipients) - - if not limit: - tell('输入无效', filename, level=1) - outbox.put(Command('setStatus', 'FINISH')) - continue - - names = data.get(parameters.get('column_name'), [None] * limit) - codes = data.get(parameters.get('column_code'), [None] * limit) - sents = data.setdefault(parameters.get('column_sent'), [None] * limit) - - rate = 60 / (parameters.get('interval') + 3) - length = list.count(sents, None) - - tell(f'已读取邮件:{subject}') - tell(f'指定发件人:{address}') - tell(f'已读取联系人信息共 {limit} 条') - tell(f'预计发送数量 {length}') - - tell(f'当前发送速率 {round(rate, 2)} 封/分钟') - if rate > 8.33: tell('当前发送速率已超出限制 8.33 封/分钟', level=1) - - tell(f'预计使用时间 {timedelta(minutes=length / rate)}') - tell(f'已设定允许重试次数:{parameters.get('retry')}') - tell(f'已设定最大重复次数:{parameters.get('max_occurrence') or '无限制'}') - - locale = parameters.get('locale') - greetings = [item for item in Greetings.presets() if item.locale == locale][0] - timezone = ZoneInfo(greetings.timezone) - tell(f'当前时区:{greetings.timezone}') - tell(f'当前语言:{greetings.locale.upper()}') - - index = 0 + cursor = 0 status = Status.ACTIVE occurrences = {} sent = 0 errors = 0 warnings = 0 - while status.isactive() and index < limit: + while status.isactive() and cursor < limit: attempt = 0 - current = index - index += 1 + current = cursor + cursor += 1 recipient = str(recipients[current]).strip() name = names[current] code = codes[current] - mark = sents[current] + axis = index[current] occurrence = occurrences.setdefault(code, [0]) if code else [0] - outbox.put(Command('setProgress', index, name, recipient, sent, warnings, errors)) + outbox.put(Command('setProgress', cursor, name, recipient, sent, warnings, errors)) - if mark is not None and str(mark).strip(): + if (remarks := frame.loc[axis, cs]) is not None and str(remarks).strip(): tell(f'已跳过项目 {recipient}') occurrence[0] += 1 continue @@ -382,8 +389,8 @@ def main(driver: WebDriver): click(".modal-dialog .modal-footer button[data-action='ok']", condition) # 返回草稿箱 click("li[data-id='default0/Brouillons']") - - for attempt in range(parameters.get('retry')): + + for attempt in range(parameters.get('attempts')): ready(driver) click("ul[aria-label='List view'] li[data-index='0']", condition) if get_subject() == subject: break @@ -442,14 +449,13 @@ def main(driver: WebDriver): wait = WebDriverWait(driver, timeout=parameters.get('interval')) alert = wait.until(lambda x: x.find_element(By.CSS_SELECTOR, "div.io-ox-alert.io-ox-alert-error")) - sents[current] = '❌' + frame.loc[axis, cs] = '❌' message = alert.text.replace('\n', ' ') tell(f'邮件系统错误 ({attempt})', message or None, level=1) - # 关闭警告 click("div.io-ox-alert.io-ox-alert-error button[data-action='close']") except TimeoutException: - sents[current] = '✔️' + frame.loc[axis, cs] = '✔️' occurrence[0] += 1 sent += 1 break @@ -462,7 +468,7 @@ def main(driver: WebDriver): tell("关闭邮件时发生了错误", e, level=1) break - if attempt < parameters.get('retry'): + if attempt < parameters.get('attempts'): continue else: tell('已超出最大重试上限', level=1) @@ -489,16 +495,25 @@ def main(driver: WebDriver): outbox.put(Command('setStatus', 'CANCEL')) status = Status.INACTIVE - progress = index / limit * 100 + progress = cursor / limit * 100 tell('[信息] 当前进度:%.2f %%' % progress) tell(f'已发送 {sent} 封;发送失败 {errors} 封;跳过重复项 {warnings} 个') if parameters.get('save'): - try: - tell(f'正在写入文件:{filename}') - pandas.DataFrame.from_dict(data).to_excel(filename, index=False, sheet_name='Sheet1') - except Exception as e: - tell('写入文件时发生了错误', e, level=0) + for attempt in range(parameters.get('attempts')): + try: + path = Path(filename) + tell(f'正在写入文件:{path}') + if path.exists(): + spreadsheet = pandas.read_excel(path, sheet_name=0) + frame = frame.combine_first(spreadsheet) + with pandas.ExcelWriter(path, mode='w') as writer: + frame.to_excel(writer, index=False) + break + except Exception as e: + tell(f'写入文件时发生错误 ({attempt})', e, level=1) + time.sleep(1) + continue if status.isalive(): outbox.put(Command('setStatus', 'FINISH')) else: return 0 diff --git a/profiles/profile-example.bat b/profiles/profile-example.bat new file mode 100644 index 0000000..d09f8f4 --- /dev/null +++ b/profiles/profile-example.bat @@ -0,0 +1,3 @@ +cd /D .\..\ +.\venv\Scripts\pythonw.exe .\main.py --address "user@example.com" --password "example" --interval 10 +@pause \ No newline at end of file diff --git a/profiles/profile-example.pyw b/profiles/profile-example.pyw deleted file mode 100644 index 1d5cf98..0000000 --- a/profiles/profile-example.pyw +++ /dev/null @@ -1,2 +0,0 @@ -import subprocess -subprocess.run([".\\..\\venv\\Scripts\\pythonw.exe", ".\\main.py", "--address", "user@example.com", "--password", "password", "--interval", "10"], cwd=".\\..\\") \ No newline at end of file diff --git a/profiles/setup-virtualenv.bat b/profiles/setup-virtualenv.bat new file mode 100644 index 0000000..f2160c3 --- /dev/null +++ b/profiles/setup-virtualenv.bat @@ -0,0 +1,4 @@ +cd /D .\..\ +python -m venv venv +.\venv\Scripts\pip.exe install -r ./requirements.txt +@pause \ No newline at end of file diff --git a/profiles/setup-virtualenv.pyw b/profiles/setup-virtualenv.pyw deleted file mode 100644 index 19bb7d8..0000000 --- a/profiles/setup-virtualenv.pyw +++ /dev/null @@ -1,2 +0,0 @@ -import subprocess -subprocess.run(["powershell.exe", "python.exe -m venv venv; .\\venv\\Scripts\\python.exe -m pip install -r requirements.txt"], cwd=".\\..\\") \ No newline at end of file