| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- """
- 日报自动化填写脚本
- 依赖安装:
- 1. 安装Python依赖包:
- pip install playwright
- pip install psutil
- pip install asyncio
- pip install aiohttp # 添加异步HTTP客户端,提高API调用效率
- 2. 安装Playwright浏览器:
- playwright install chromium
- 注意事项:
- 1. 确保Chrome浏览器已安装
- 2. 脚本使用Chrome用户配置文件,路径为:C:/Users/zhens/AppData/Local/Google/Chrome/User Data
- 3. 运行前请确保没有其他Chrome进程在运行
- 4. 脚本会自动关闭所有Chrome进程,请确保没有重要工作未保存
- 5. 如果修改了Chrome用户配置路径,需要相应修改代码中的user_data_dir变量
- 6. 脚本运行时会打开Chrome浏览器窗口,请勿手动操作浏览器
- 7. 如果遇到网络问题,可以适当增加等待时间(asyncio.sleep的值)
- 8. 如果提交失败,可以检查网络连接或适当增加等待时间
- """
- import asyncio
- import os
- import psutil
- import time
- import tkinter as tk
- from tkinter import scrolledtext, messagebox, ttk
- from playwright.async_api import async_playwright
- import aiohttp # 使用异步HTTP客户端,提高API调用效率
- import logging
- import threading
- import re
- from typing import Optional, Tuple
- # 配置日志
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
- # 全局变量,用于控制生成状态
- is_generating = False
- MAX_RETRIES = 3 # API调用最大重试次数,提高可靠性
- REQUEST_TIMEOUT = 30 # API请求超时时间(秒),避免长时间等待
- def count_chinese_chars(text):
- """统计中文字符数量"""
- return len(re.findall(r'[\u4e00-\u9fff]', text))
- def count_words(text):
- """统计总字数(中文字符+其他字符)"""
- return count_chinese_chars(text) + len(re.sub(r'[\u4e00-\u9fff]', '', text))
- async def generate_daily_report(prompt, ollama_url, model_name):
- """使用Ollama生成日报内容"""
- try:
- logging.info(f"开始生成日报,使用模型: {model_name}")
-
- # 使用异步HTTP客户端,提高API调用效率
- async with aiohttp.ClientSession() as session:
- for attempt in range(MAX_RETRIES):
- try:
- # 并行发送两个请求,提高效率
- async with session.post(
- f"{ollama_url}/api/generate",
- json={
- "model": model_name,
- "prompt": f"你是一个专业的日报生成助手,请根据以下工作内容生成专业的今日工作总结:{prompt}",
- "stream": False
- },
- timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
- ) as summary_response, \
- session.post(
- f"{ollama_url}/api/generate",
- json={
- "model": model_name,
- "prompt": f"你是一个专业的日报生成助手,请根据以下工作内容生成合理的明日工作计划:{prompt}",
- "stream": False
- },
- timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
- ) as plan_response:
-
- summary_data = await summary_response.json()
- plan_data = await plan_response.json()
-
- if summary_response.status == 200 and plan_response.status == 200:
- return summary_data["response"], plan_data["response"]
- else:
- error_msg = f"API调用失败: 总结状态码={summary_response.status}, 计划状态码={plan_response.status}"
- if summary_response.status != 200:
- error_msg += f"\n总结生成失败原因: {await summary_response.text()}"
- if plan_response.status != 200:
- error_msg += f"\n计划生成失败原因: {await plan_response.text()}"
- logging.error(error_msg)
-
- if attempt < MAX_RETRIES - 1:
- await asyncio.sleep(1) # 重试前等待
- continue
- return None, error_msg
-
- except Exception as e:
- error_msg = f"生成日报时发生错误: {str(e)}"
- logging.error(error_msg)
- if attempt < MAX_RETRIES - 1:
- await asyncio.sleep(1)
- continue
- return None, error_msg
-
- except Exception as e:
- error_msg = f"生成日报时发生错误: {str(e)}"
- logging.error(error_msg)
- return None, error_msg
- def get_user_input():
- """通过GUI窗口获取用户输入的日报内容"""
- root = tk.Tk()
- root.title("日报填写助手")
- root.geometry("1000x800") # 设置更大的初始窗口大小
-
- # 设置窗口样式
- root.configure(bg='#f0f0f0')
- # 修改字体设置方式
- default_font = ('微软雅黑', 10)
- root.option_add('*Font', default_font)
-
- # 设置窗口图标(如果有的话)
- try:
- root.iconbitmap('icon.ico')
- except:
- pass
-
- # 创建主框架
- main_frame = tk.Frame(root, bg='#f0f0f0')
- main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
- # 创建可滚动内容区(Canvas + Frame)
- content_canvas = tk.Canvas(main_frame, bg='#f0f0f0', highlightthickness=0)
- scrollbar = tk.Scrollbar(main_frame, orient="vertical", command=content_canvas.yview)
- scrollable_frame = tk.Frame(content_canvas, bg='#f0f0f0')
- scrollable_frame.bind(
- "<Configure>",
- lambda e: content_canvas.configure(
- scrollregion=content_canvas.bbox("all")
- )
- )
- content_canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
- content_canvas.configure(yscrollcommand=scrollbar.set)
- content_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
- # 下面所有内容区控件都放到scrollable_frame里
- # 创建标题
- title_label = tk.Label(scrollable_frame,
- text="日报填写",
- font=('微软雅黑', 16, 'bold'),
- bg='#f0f0f0',
- pady=5)
- title_label.pack()
-
- # 创建配置区域框架
- config_frame = tk.Frame(scrollable_frame, bg='#f0f0f0')
- config_frame.pack(fill=tk.X, pady=5)
-
- # 创建自动生成功能开关
- auto_generate_var = tk.BooleanVar(value=False)
- auto_generate_check = tk.Checkbutton(config_frame,
- text="启用自动生成日报",
- variable=auto_generate_var,
- font=('微软雅黑', 11),
- bg='#f0f0f0',
- command=lambda: toggle_auto_generate(auto_generate_var.get()))
- auto_generate_check.pack(pady=5)
-
- # 创建Ollama配置区域
- ollama_frame = tk.LabelFrame(config_frame,
- text="Ollama配置",
- font=('微软雅黑', 12),
- bg='#f0f0f0',
- padx=10,
- pady=5)
- ollama_frame.pack(fill=tk.X, pady=2)
-
- # Ollama地址输入
- url_frame = tk.Frame(ollama_frame, bg='#f0f0f0')
- url_frame.pack(fill=tk.X, pady=2)
- tk.Label(url_frame,
- text="Ollama地址:",
- font=('微软雅黑', 11),
- bg='#f0f0f0').pack(side=tk.LEFT)
- ollama_url = tk.Entry(url_frame,
- font=('微软雅黑', 11))
- ollama_url.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
- ollama_url.insert(0, "http://localhost:11434")
-
- # 模型名称输入
- model_frame = tk.Frame(ollama_frame, bg='#f0f0f0')
- model_frame.pack(fill=tk.X, pady=2)
- tk.Label(model_frame,
- text="模型名称:",
- font=('微软雅黑', 11),
- bg='#f0f0f0').pack(side=tk.LEFT)
- model_name = tk.Entry(model_frame,
- font=('微软雅黑', 11))
- model_name.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
- model_name.insert(0, "llama2")
-
- # 创建提示词输入区域
- prompt_frame = tk.LabelFrame(config_frame,
- text="工作内容提示词",
- font=('微软雅黑', 12),
- bg='#f0f0f0',
- padx=10,
- pady=5)
- prompt_frame.pack(fill=tk.X, pady=2)
-
- prompt_text = scrolledtext.ScrolledText(prompt_frame,
- height=3,
- font=('微软雅黑', 11),
- wrap=tk.WORD,
- padx=10,
- pady=10)
- prompt_text.pack(fill=tk.X, expand=True)
-
- # 创建进度条
- progress_var = tk.DoubleVar()
- progress_bar = ttk.Progressbar(prompt_frame,
- variable=progress_var,
- maximum=100,
- mode='indeterminate')
-
- # 创建自动生成按钮
- def auto_generate():
- global is_generating
- if is_generating:
- return
-
- prompt = prompt_text.get("1.0", tk.END).strip()
- if not prompt:
- messagebox.showwarning("提示", "请输入工作内容提示词")
- return
-
- try:
- is_generating = True
- auto_generate_button.config(state=tk.DISABLED)
- progress_bar.pack(fill=tk.X, pady=5)
- progress_bar.start()
-
- def generate_thread():
- try:
- summary, plan = generate_daily_report(
- prompt,
- ollama_url.get().strip(),
- model_name.get().strip()
- )
- if summary and plan:
- root.after(0, lambda: update_texts(summary, plan))
- else:
- root.after(0, lambda: messagebox.showerror("错误", f"生成日报失败:{plan}"))
- except Exception as e:
- root.after(0, lambda: messagebox.showerror("错误", f"生成日报时发生错误:{str(e)}"))
- finally:
- root.after(0, finish_generation)
-
- threading.Thread(target=generate_thread, daemon=True).start()
-
- except Exception as e:
- messagebox.showerror("错误", f"生成日报时发生错误:{str(e)}")
- finish_generation()
-
- def update_texts(summary, plan):
- summary_text.delete("1.0", tk.END)
- plan_text.delete("1.0", tk.END)
- summary_text.insert("1.0", summary)
- plan_text.insert("1.0", plan)
- update_counts()
-
- def finish_generation():
- global is_generating
- is_generating = False
- auto_generate_button.config(state=tk.NORMAL)
- progress_bar.stop()
- progress_bar.pack_forget()
-
- auto_generate_button = tk.Button(prompt_frame,
- text="自动生成日报",
- command=auto_generate,
- font=('微软雅黑', 11),
- bg='#2196F3',
- fg='white',
- relief=tk.FLAT,
- cursor='hand2')
- auto_generate_button.pack(pady=5)
-
- def toggle_auto_generate(show):
- """切换自动生成相关控件的显示状态"""
- if show:
- ollama_frame.pack(fill=tk.X, pady=2)
- prompt_frame.pack(fill=tk.X, pady=2)
- else:
- ollama_frame.pack_forget()
- prompt_frame.pack_forget()
-
- # 初始状态:隐藏自动生成相关控件
- toggle_auto_generate(False)
-
- # 创建内容区域框架
- content_frame = tk.Frame(scrollable_frame, bg='#f0f0f0')
- content_frame.pack(fill=tk.BOTH, expand=True, pady=5)
-
- # 创建今日工作总结输入区域
- summary_frame = tk.LabelFrame(content_frame,
- text="今日工作总结",
- font=('微软雅黑', 12),
- bg='#f0f0f0',
- padx=10,
- pady=5)
- summary_frame.pack(fill=tk.BOTH, expand=True, pady=2)
-
- summary_text = scrolledtext.ScrolledText(summary_frame,
- height=6, # 减小默认高度
- font=('微软雅黑', 11),
- wrap=tk.WORD,
- padx=10,
- pady=10)
- summary_text.pack(fill=tk.BOTH, expand=True)
-
- # 添加字数统计
- summary_count = tk.Label(summary_frame,
- text="字数:0",
- font=('微软雅黑', 10),
- bg='#f0f0f0',
- anchor='e')
- summary_count.pack(fill=tk.X, padx=10)
-
- # 创建明日工作计划输入区域
- plan_frame = tk.LabelFrame(content_frame,
- text="明日工作计划",
- font=('微软雅黑', 12),
- bg='#f0f0f0',
- padx=10,
- pady=5)
- plan_frame.pack(fill=tk.BOTH, expand=True, pady=2)
-
- plan_text = scrolledtext.ScrolledText(plan_frame,
- height=6, # 减小默认高度
- font=('微软雅黑', 11),
- wrap=tk.WORD,
- padx=10,
- pady=10)
- plan_text.pack(fill=tk.BOTH, expand=True)
-
- # 添加字数统计
- plan_count = tk.Label(plan_frame,
- text="字数:0",
- font=('微软雅黑', 10),
- bg='#f0f0f0',
- anchor='e')
- plan_count.pack(fill=tk.X, padx=10)
-
- # 创建底部控制区域,固定在主窗口底部
- bottom_frame = tk.Frame(root, bg='#f0f0f0')
- bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5)
-
- # 创建复选框
- keep_original = tk.BooleanVar(value=False)
- check_button = tk.Checkbutton(bottom_frame,
- text="保留原有内容",
- variable=keep_original,
- font=('微软雅黑', 11),
- bg='#f0f0f0')
- check_button.pack(side=tk.LEFT, padx=10)
-
- # 创建提交按钮
- result = {"summary": "", "plan": "", "is_submitted": False, "keep_original": False}
- def submit():
- if not summary_text.get("1.0", tk.END).strip() or not plan_text.get("1.0", tk.END).strip():
- messagebox.showwarning("提示", "请填写完整的工作总结和计划")
- return
-
- result["summary"] = summary_text.get("1.0", tk.END).strip()
- result["plan"] = plan_text.get("1.0", tk.END).strip()
- result["keep_original"] = keep_original.get()
- result["is_submitted"] = True
- root.quit()
- root.destroy()
- submit_button = tk.Button(bottom_frame,
- text="提交",
- command=submit,
- font=('微软雅黑', 12, 'bold'),
- bg='#4CAF50',
- fg='white',
- width=15,
- height=2,
- relief=tk.FLAT,
- cursor='hand2')
- submit_button.pack(side=tk.RIGHT, padx=10)
-
- # 添加字数统计更新函数
- def update_counts(event=None):
- summary_text_content = summary_text.get("1.0", tk.END).strip()
- plan_text_content = plan_text.get("1.0", tk.END).strip()
- summary_count.config(text=f"字数:{count_words(summary_text_content)}(中文字符:{count_chinese_chars(summary_text_content)})")
- plan_count.config(text=f"字数:{count_words(plan_text_content)}(中文字符:{count_chinese_chars(plan_text_content)})")
-
- # 绑定字数统计更新
- summary_text.bind('<KeyRelease>', update_counts)
- plan_text.bind('<KeyRelease>', update_counts)
-
- # 初始更新字数统计
- update_counts()
- # 设置窗口居中
- root.update_idletasks()
- width = root.winfo_width()
- height = root.winfo_height()
- x = (root.winfo_screenwidth() // 2) - (width // 2)
- y = (root.winfo_screenheight() // 2) - (height // 2)
- root.geometry(f'{width}x{height}+{x}+{y}')
- # 设置窗口最小尺寸
- root.minsize(900, 700)
-
- # 设置窗口关闭确认
- def on_closing():
- if messagebox.askokcancel("确认", "确定要关闭窗口吗?"):
- root.destroy()
-
- root.protocol("WM_DELETE_WINDOW", on_closing)
-
- # 设置默认焦点
- prompt_text.focus_set()
- root.mainloop()
- return result["summary"], result["plan"], result["is_submitted"], result["keep_original"]
- # 执行自动化浏览器操作的协程函数
- def close_chrome():
- """关闭所有Chrome进程"""
- for proc in psutil.process_iter(['name']):
- try:
- if proc.info['name'] == 'chrome.exe':
- proc.kill()
- except (psutil.NoSuchProcess, psutil.AccessDenied):
- pass
- # 等待进程完全关闭,减少等待时间
- time.sleep(1)
- async def run(playwright):
- try:
- # 确保Chrome已关闭
- close_chrome()
-
- # 使用用户实际的Chrome配置
- # 请按照以下步骤找到正确的配置文件路径:
- # 1. 打开 Chrome 浏览器
- # 2. 在地址栏输入 chrome://version
- # 3. 查看"个人资料路径"这一行
- # 4. 将路径复制到这里
- user_data_dir = r'C:\Users\zhens\AppData\Local\Google\Chrome\User Data\Default' # 请替换为你的实际配置文件路径
-
- def text_to_div_html(text):
- """将多行文本转换为<div>...</div>格式的HTML"""
- lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
- return ''.join(f'<div>{line}</div>' for line in lines)
- # 获取用户输入的日报内容
- summary_text, plan_text, is_submitted, keep_original = get_user_input()
-
- # 如果用户取消输入,直接退出程序
- if not is_submitted:
- print("用户取消了日报填写,程序退出")
- return
-
- summary_html = text_to_div_html(summary_text)
- plan_html = text_to_div_html(plan_text)
- # 使用已有的Chrome配置文件启动浏览器
- context = await playwright.chromium.launch_persistent_context(
- user_data_dir,
- channel="chrome",
- headless=False,
- args=[
- '--start-maximized',
- '--disable-blink-features=AutomationControlled',
- '--profile-directory=Default',
- '--no-first-run',
- '--no-default-browser-check',
- '--disable-extensions', # 禁用所有扩展
- '--disable-gpu' # 禁用GPU加速
- ],
- ignore_default_args=['--enable-automation']
- )
- try:
- # 创建新页面
- page = await context.new_page()
-
- # 设置实际的User-Agent
- await page.set_extra_http_headers({
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
- })
-
- # 访问页面并等待网络空闲
- await page.goto('https://doc.weixin.qq.com/forms/j/AFIADwd9AA4AUcA6AbCADgJXbq19fUR0j_base?page=6',
- wait_until='networkidle')
-
- # 等待元素出现并点击
- await page.wait_for_selector('div.TitleBarBtn_title__dKAyV:text("填写")', timeout=60000)
- await page.click('div.TitleBarBtn_title__dKAyV:text("填写")', delay=100)
- # 等待操作完成,确保页面完全加载
- await asyncio.sleep(2) # 减少等待时间,提高效率
- # 查找所有可编辑的输入框
- editors = await page.query_selector_all('.maileditor-editorview[contenteditable="true"]')
- print("找到输入框数量:", len(editors))
- # 今日工作总结输入
- # 1. 先点击输入框,确保获得焦点
- #todo 教程:page.locator(...)
- # 表示在当前页面上查找符合选择器的元素。
- # 'div.question:has-text("今日工作总结") .maileditor-editorview[contenteditable="true"]'
- # 这是一个复杂的 CSS 选择器,意思如下:
- # div.question:匹配类名为 question 的 div 元素。
- # : has-text("今日工作总结"):这个是 Playwright 扩展的选择器语法,表示该 div.question 元素 包含文本"今日工作总结"。
- # .maileditor-editorview[contenteditable = "true"]:在该 div 之下,继续查找一个类名为 maileditor-editorview 且有属性 contenteditable = "true" 的元素。
- # .click()
- # 表示对找到的这个元素执行点击操作。
- await page.locator('div.question:has-text("今日工作总结") .maileditor-editorview[contenteditable="true"]').click()
- # 2. 如果不保留原有内容,则清空输入框
- if not keep_original:
- await page.keyboard.press('Control+A')
- await page.keyboard.press('Delete')
- # 3. 使用键盘输入方式模拟真实输入
- await page.keyboard.type(summary_text)
- # 4. 等待输入完成,确保内容被正确输入
- await asyncio.sleep(1) # 减少等待时间,提高效率
-
- # 明日工作计划输入
- # 1. 先点击输入框,确保获得焦点
- await page.locator('div.question:has-text("明日工作计划") .maileditor-editorview[contenteditable="true"]').click()
- # 2. 如果不保留原有内容,则清空输入框
- if not keep_original:
- await page.keyboard.press('Control+A')
- await page.keyboard.press('Delete')
- # 3. 使用键盘输入方式模拟真实输入
- await page.keyboard.type(plan_text)
- # 4. 等待输入完成
- await asyncio.sleep(1) # 减少等待时间,提高效率
- # 等待内容保存
- # 给系统足够的时间来保存输入的内容
- await asyncio.sleep(2) # 减少等待时间,提高效率
- # 点击提交按钮
- # 1. 等待提交按钮出现
- await page.wait_for_selector('button.FillFooter_confirm__0ClPl', timeout=60000)
- # 2. 点击提交按钮,添加延迟模拟真实点击
- await page.click('button.FillFooter_confirm__0ClPl', delay=100)
-
- # 等待提交完成
- # 给系统足够的时间来处理提交操作
- await asyncio.sleep(2) # 减少等待时间,提高效率
-
- # 显示提交成功提示
- print("日报提交成功!")
- except Exception as e:
- print(f"提交过程中发生错误: {str(e)}")
- raise
- finally:
- # 确保浏览器正常关闭
- await context.close()
- except Exception as e:
- print(f"程序运行过程中发生错误: {str(e)}")
- raise
- # 主函数,用于启动 playwright 并调用 run 函数
- async def main():
- async with async_playwright() as playwright:
- await run(playwright)
- # 判断当前环境是否已经有事件循环在运行
- if __name__ == "__main__":
- try:
- # 尝试获取正在运行的事件循环(某些 IDE/Jupyter 会预先启动)
- loop = asyncio.get_running_loop()
- except RuntimeError:
- loop = None
- # 如果事件循环存在且正在运行(比如在 Jupyter Notebook 中)
- if loop and loop.is_running():
- print("检测到事件循环正在运行,使用 create_task 启动协程")
- asyncio.create_task(main()) # 使用 create_task 异步运行
- else:
- # 否则,正常使用 asyncio.run 启动主协程
- asyncio.run(main())
|