乐于分享
好东西不私藏

今日头条微头条 AI 批量发文桌面工具

今日头条微头条 AI 批量发文桌面工具

今日头条微头条 AI 批量发文桌面工具|完整开源教程(PyQt6 可视化 + 多AI模型 + 定时批量发布)

一、项目前言(300+字)

在自媒体运营、内容矩阵搭建、流量账号运维的实际场景中,人工手动撰写、编辑、发布微头条内容存在效率极低、重复性高、耗时耗力、发布时间不精准等诸多痛点。传统运营模式下,创作者需要每日花费大量时间撰写文案、手动排版、逐篇发布,同时很难保证内容产出数量充足、风格多样、发布时段稳定,极大限制了账号起号、流量沉淀与矩阵规模化运营的效率。为彻底解决自媒体批量发文的行业痛点,本项目基于 Python + PyQt6 开发全可视化、无命令行、全自动今日头条微头条批量发文桌面工具,整合主流AI大模型自动文案生成、Excel批量导入发文、毫秒级批量发布、自定义定时任务、全流程日志监控、参数持久化配置等全套核心能力。

该工具摒弃了传统脚本晦涩难用、需要改代码配置、无界面、易报错、无法定时的缺陷,打造出精致美观的桌面客户端,支持 Windows 系统直接运行,所有参数可视化配置、一键保存、永久生效。同时深度对接阿里百炼、Kimi、DeepSeek 三大主流AI接口,随机匹配提示词生成差异化原创文案,规避内容同质化问题。发布逻辑完全复刻今日头条官网抓包协议,模拟真实浏览器请求,稳定性强、通过率高。全程异步线程运行,不会出现界面卡顿、程序卡死等问题,是自媒体个人、工作室、矩阵团队实现无人值守、批量量产、定时稳更的专业级自动化工具。

二、项目核心架构与功能详解

本项目采用模块化分层架构,代码解耦度高、维护性强、拓展性极佳,整体分为 5 大核心模块,各司其职、互不干扰,完美实现功能闭环。下面对核心代码模块进行精细化拆解讲解,剔除所有密钥、隐私配置,保留纯净可用逻辑。

模块一:全局配置与日志系统(核心基础)

配置模块是整个工具的基石,负责全局参数持久化存储、默认参数初始化、配置读写容错处理。所有用户设置的发布接口、AI参数、提示词、定时时间都会自动保存至 config.json,重启软件自动加载,无需重复配置。同时内置全局日志系统,同步输出控制台日志与本地日志文件,精准记录每一次操作、AI生成状态、发布结果、报错信息,方便用户排查问题、复盘运行记录。模块内置容错机制,配置文件损坏、缺失时自动生成默认配置,保证程序永不崩溃。

模块二:多AI模型统一调用模块(内容生成核心)

工具内置阿里百炼、Kimi、DeepSeek 三大主流大模型接口,封装统一调用入口,支持自由切换默认AI模型、自定义生成数量、自定义创作提示词。所有AI请求采用标准化POST协议,固定返回 JSON 结构化数据,自动解析标题+正文,无需人工整理文案。通过多模型轮换、随机提示词机制,彻底解决批量发文内容重复、同质化严重、被判低质的问题。所有接口采用超时保护、异常捕获机制,网络波动、接口报错时会精准日志提示,不影响程序整体运行。

模块三:可视化UI + 异步任务调度模块(交互与运行核心)

基于 PyQt6 搭建高颜值桌面客户端,分区设计三大标签页:发布管理、参数设置、定时日志。支持 Excel 批量导入文案、AI一键批量生成、文章预览、状态实时刷新、立即批量发布、定时无人值守发布。所有耗时操作(AI生成、批量发文)全部开启子线程异步运行,彻底解决UI卡顿、程序假死问题。内置定时轮询机制,30秒检测一次系统时间,到达预设时间自动触发批量发布,实现全自动运维。同时搭配精美样式美化,界面简洁高级、操作零门槛,新手可直接上手使用。

三、完整纯净可运行源码(无密钥、可直接复制使用)

#!/usr/bin/env python3# -*- coding: utf-8 -*-"""今日头条微头条API批量发文桌面工具===================================基于 PyQt6 + 多AI接口 + 定时任务 + 批量发布开源纯净版:无内置密钥、可直接配置使用"""import sysimport osimport jsonimport timeimport randomimport loggingfrom datetime import datetimefrom typing import List, Dictimport requestsfrom PyQt6.QtWidgets import (    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,    QLabel, QLineEdit, QTextEdit, QPushButton, QFileDialog,    QGroupBox, QMessageBox, QTextBrowser, QTabWidget, QTableWidget,    QTableWidgetItem, QHeaderView, QSpinBox, QComboBox, QCheckBox,    QSplitter, QTimeEdit, QListWidget, QListWidgetItem,    QDialog, QDialogButtonBox)from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimerfrom PyQt6.QtGui import QFont, QTextCursor# ============================================================# 常量与日志系统# ============================================================CONFIG_FILE = "config.json"LOG_FILE = "runtime.log"WINDOW_TITLE = "今日头条微头条API批量发文工具"WINDOW_WIDTH = 1200WINDOW_HEIGHT = 800logging.basicConfig(    level=logging.INFO,    format="%(asctime)s [%(levelname)s] %(message)s",    handlers=[        logging.FileHandler(LOG_FILE, encoding="utf-8"),        logging.StreamHandler(sys.stdout)    ])logger = logging.getLogger(__name__)# ============================================================# 配置管理模块# ============================================================def load_config() -> dict:    default = {        "publish_url": "https://mp.toutiao.com/mp/agw/article/wtt?mp_publish_ab_val=0&aid=1231",        "cookie_str": "",        "ai_keys": {"aliyun_bailian": "", "kimi": "", "deepseek": ""},        "prompts": ["请根据以下主题生成一篇今日头条微头条文章,标题简短有吸引力,内容200-500字,通俗易懂"],        "default_ai": "aliyun_bailian",        "generate_count": 50,        "schedule_times": [],        "schedule_enabled": False    }    if not os.path.exists(CONFIG_FILE):        return default    try:        with open(CONFIG_FILE, "r", encoding="utf-8") as f:            data = json.load(f)        for k, v in default.items():            if k not in data:                data[k] = v        return data    except (json.JSONDecodeError, IOError):        return defaultdef save_config(config: dict) -> bool:    try:        with open(CONFIG_FILE, "w", encoding="utf-8") as f:            json.dump(config, f, ensure_ascii=False, indent=2)        logger.info("配置已保存到 config.json")        return True    except IOError as e:        logger.error(f"保存配置失败: {e}")        return False# ============================================================# 多AI接口统一调用模块# ============================================================def call_deepseek_api(api_key: str, prompt: str, count: int) -> List[Dict[str, str]]:    url = "https://api.deepseek.com/chat/completions"    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}    full_prompt = f"{prompt}\n\n请生成{count}篇文章,每篇包含标题和内容。严格返回JSON数组:[{{\"title\":\"标题\",\"content\":\"内容\"}}]"    payload = {"model": "deepseek-chat", "messages": [{"role": "user", "content": full_prompt}], "temperature": 0.8, "max_tokens": 4096}    try:        resp = requests.post(url, headers=headers, json=payload, timeout=120)        resp.raise_for_status()        text = resp.json()["choices"][0]["message"]["content"]        start, end = text.find("["), text.rfind("]") + 1        if start >= 0 and end > start:            return json.loads(text[start:end])    except Exception as e:        logger.error(f"DeepSeek API调用失败: {e}")    return []def call_kimi_api(api_key: str, prompt: str, count: int) -> List[Dict[str, str]]:    url = "https://api.moonshot.cn/v1/chat/completions"    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}    full_prompt = f"{prompt}\n\n请生成{count}篇文章,每篇包含标题和内容。严格返回JSON数组:[{{\"title\":\"标题\",\"content\":\"内容\"}}]"    payload = {"model": "moonshot-v1-8k", "messages": [{"role": "user", "content": full_prompt}], "temperature": 0.8}    try:        resp = requests.post(url, headers=headers, json=payload, timeout=120)        resp.raise_for_status()        text = resp.json()["choices"][0]["message"]["content"]        start, end = text.find("["), text.rfind("]") + 1        if start >= 0 and end > start:            return json.loads(text[start:end])    except Exception as e:        logger.error(f"Kimi API调用失败: {e}")    return []def call_aliyun_bailian_api(api_key: str, prompt: str, count: int) -> List[Dict[str, str]]:    url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}    full_prompt = f"{prompt}\n\n请生成{count}篇文章,每篇包含标题和内容。严格返回JSON数组:[{{\"title\":\"标题\",\"content\":\"内容\"}}]"    payload = {"model": "qwen-plus", "messages": [{"role": "user", "content": full_prompt}], "temperature": 0.8}    try:        resp = requests.post(url, headers=headers, json=payload, timeout=120)        resp.raise_for_status()        text = resp.json()["choices"][0]["message"]["content"]        start, end = text.find("["), text.rfind("]") + 1        if start >= 0 and end > start:            return json.loads(text[start:end])    except Exception as e:        logger.error(f"阿里百炼API调用失败: {e}")    return []def call_ai_generate(ai_type: str, api_key: str, prompt: str, count: int) -> List[Dict[str, str]]:    if ai_type == "deepseek":        return call_deepseek_api(api_key, prompt, count)    elif ai_type == "kimi":        return call_kimi_api(api_key, prompt, count)    elif ai_type == "aliyun_bailian":        return call_aliyun_bailian_api(api_key, prompt, count)    return []# ============================================================# 头条发布接口模块# ============================================================def publish_article(publish_url: str, cookie_str: str, title: str, content: str) -> dict:    if "msToken" not in publish_url or "a_bogus" not in publish_url:        return {"error": "发布URL缺少msToken或a_bogus参数,请在设置中配置完整的抓包URL"}    headers = {        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",        "Cookie": cookie_str,        "Content-Type": "application/json",        "Referer": "https://mp.toutiao.com/"    }    extra_param = "{\"claim_exclusive\":\"1\",\"add_music_to_article_flag\":\"0\",\"tuwen_wtt_trans_flag\":\"1\",\"info_source\":\"{\\\"source_type\\\":5,\\\"source_author_uid\\\":\\\"\\\",\\\"time_format\\\":\\\"\\\",\\\"position\\\":{}}\"}"    post_json = {        "content": content,        "image_list": [],        "extra": extra_param,        "is_fans_article": 2,        "pre_upload": 1,        "welfare_card": "",        "entrance": "main"    }    try:        logger.info(f"[publish] URL({len(publish_url)}): {publish_url[:120]}...")        logger.info(f"[publish] Cookie长度: {len(cookie_str)}")        logger.info(f"[publish] Content长度: {len(content)}")        resp = requests.post(publish_url, headers=headers, json=post_json, timeout=35)        resp_text = resp.text.strip()        if not resp_text:            return {"error": f"空响应, HTTP {resp.status_code}"}        try:            result = resp.json()            result["_http_status"] = resp.status_code            return result        except json.JSONDecodeError:            return {"error": f"非JSON响应(HTTP {resp.status_code}): {resp_text[:200]}"}    except requests.exceptions.Timeout:        return {"error": "请求超时"}    except requests.exceptions.ConnectionError:        return {"error": "网络连接失败"}    except Exception as e:        logger.error(f"发布失败: {e}")        return {"error": str(e)}# ============================================================# 异步任务线程模块# ============================================================class AIGenerateWorker(QThread):    log_signal = pyqtSignal(str, str)    result_signal = pyqtSignal(list)    finished_signal = pyqtSignal()    def __init__(self, ai_type, api_key, prompt, count):        super().__init__()        self.ai_type = ai_type        self.api_key = api_key        self.prompt = prompt        self.count = count    def run(self):        self.log_signal.emit(f"正在调用 {self.ai_type} API 生成 {self.count} 篇内容...", "info")        results = call_ai_generate(self.ai_type, self.api_key, self.prompt, self.count)        if results:            self.log_signal.emit(f"AI生成完成,共获得 {len(results)} 篇内容", "success")        else:            self.log_signal.emit("AI生成失败或返回为空,请检查API Key和网络", "error")        self.result_signal.emit(results)        self.finished_signal.emit()class BatchPublishWorker(QThread):    log_signal = pyqtSignal(str, str)    progress_signal = pyqtSignal(int, str)    finished_signal = pyqtSignal()    def __init__(self, articles, publish_url, cookie_str, interval=1.0):        super().__init__()        self.articles = articles        self.publish_url = publish_url        self.cookie_str = cookie_str        self.interval = interval        self._stop_flag = False    def stop(self):        self._stop_flag = True    def run(self):        total = len(self.articles)        self.log_signal.emit(f"开始批量发布,共 {total} 篇,间隔 {self.interval} 秒", "info")        for i, article in enumerate(self.articles):            if self._stop_flag:                self.log_signal.emit("发布已手动停止", "info")                break            title = article.get("title", "")            content = article.get("content", "")            self.log_signal.emit(f"[{i+1}/{total}] 发布: {title}", "info")            result = publish_article(self.publish_url, self.cookie_str, title, content)            if "error" in result:                self.log_signal.emit(f"[{i+1}/{total}] ❌ 失败: {result['error']}", "error")                self.progress_signal.emit(i, "失败")                logger.error(f"发布第{i+1}篇失败: {title} - {result}")            else:                http_status = result.get("_http_status", 0)                if http_status == 200:                    self.log_signal.emit(f"[{i+1}/{total}] ✅ 发布成功 (HTTP 200)", "success")                    self.progress_signal.emit(i, "成功")                else:                    msg = result.get("err_tips", result.get("message", f"HTTP {http_status}"))                    self.log_signal.emit(f"[{i+1}/{total}] ❌ 失败: {msg}", "error")                    self.progress_signal.emit(i, "失败")                logger.info(f"发布第{i+1}篇: {title} - HTTP:{http_status} - {json.dumps(result, ensure_ascii=False)[:200]}")            if i < total - 1:                time.sleep(self.interval)        self.log_signal.emit("批量发布任务结束", "info")        self.finished_signal.emit()# ============================================================# 内容预览弹窗# ============================================================class ContentPreviewDialog(QDialog):    def __init__(self, title: str, content: str, parent=None):        super().__init__(parent)        self.setWindowTitle(f"内容预览 - {title}")        self.setMinimumSize(500, 400)        layout = QVBoxLayout(self)        lbl = QLabel(f"标题: {title}")        lbl.setStyleSheet("font-weight:bold;font-size:14px;margin-bottom:8px;")        layout.addWidget(lbl)        tv = QTextEdit()        tv.setPlainText(content)        tv.setReadOnly(True)        layout.addWidget(tv)        btn = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)        btn.rejected.connect(self.close)        layout.addWidget(btn)# ============================================================# 设置页面模块# ============================================================class SettingsTab(QWidget):    def __init__(self, config: dict, save_callback):        super().__init__()        self.config = config        self.save_callback = save_callback        self._init_ui()    def _init_ui(self):        layout = QVBoxLayout(self)        layout.setSpacing(12)        grp_pub = QGroupBox("发布接口配置")        gl = QVBoxLayout(grp_pub)        row = QHBoxLayout()        row.addWidget(QLabel("发布URL:"))        self.input_url = QLineEdit(self.config.get("publish_url", ""))        self.input_url.setPlaceholderText("https://mp.toutiao.com/mp/agw/article/wtt?...")        row.addWidget(self.input_url)        gl.addLayout(row)        gl.addWidget(QLabel("Cookie:"))        self.input_cookie = QTextEdit()        self.input_cookie.setPlainText(self.config.get("cookie_str", ""))        self.input_cookie.setPlaceholderText("请输入Cookie字符串")        self.input_cookie.setMaximumHeight(80)        gl.addWidget(self.input_cookie)        layout.addWidget(grp_pub)        grp_ai = QGroupBox("AI API Keys")        al = QVBoxLayout(grp_ai)        ai_keys = self.config.get("ai_keys", {})        for label_text, key_name, attr_name in [            ("阿里百炼 Key:", "aliyun_bailian", "input_aliyun_key"),            ("Kimi Key:", "kimi", "input_kimi_key"),            ("DeepSeek Key:", "deepseek", "input_deepseek_key"),        ]:            r = QHBoxLayout()            r.addWidget(QLabel(label_text))            inp = QLineEdit(ai_keys.get(key_name, ""))            inp.setEchoMode(QLineEdit.EchoMode.Password)            r.addWidget(inp)            al.addLayout(r)            setattr(self, attr_name, inp)        row_def = QHBoxLayout()        row_def.addWidget(QLabel("默认AI:"))        self.combo_default_ai = QComboBox()        self.combo_default_ai.addItem("阿里百炼", "aliyun_bailian")        self.combo_default_ai.addItem("Kimi", "kimi")        self.combo_default_ai.addItem("DeepSeek", "deepseek")        idx = self.combo_default_ai.findData(self.config.get("default_ai", "aliyun_bailian"))        if idx >= 0:            self.combo_default_ai.setCurrentIndex(idx)        self.combo_default_ai.setMinimumWidth(160)        row_def.addWidget(self.combo_default_ai)        row_def.addStretch()        al.addLayout(row_def)        layout.addWidget(grp_ai)        grp_prompt = QGroupBox("提示词管理(每行一条,可直接编辑增删)")        pl = QVBoxLayout(grp_prompt)        self.prompt_edit = QTextEdit()        self.prompt_edit.setMinimumHeight(120)        self.prompt_edit.setPlaceholderText("每行一条提示词,AI生成时随机选取一条...")        self.prompt_edit.setPlainText("\n".join(self.config.get("prompts", [])))        pl.addWidget(self.prompt_edit)        layout.addWidget(grp_prompt)        row_cnt = QHBoxLayout()        row_cnt.addWidget(QLabel("AI默认生成数量:"))        self.spin_count = QSpinBox()        self.spin_count.setRange(1, 200)        self.spin_count.setValue(self.config.get("generate_count", 50))        row_cnt.addWidget(self.spin_count)        row_cnt.addStretch()        layout.addLayout(row_cnt)        btn_save = QPushButton("保存所有设置")        btn_save.setMinimumHeight(36)        btn_save.clicked.connect(self._on_save)        layout.addWidget(btn_save)        layout.addStretch()    def _on_save(self):        self.config["publish_url"] = self.input_url.text().strip()        self.config["cookie_str"] = self.input_cookie.toPlainText().strip()        self.config["ai_keys"] = {            "deepseek": self.input_deepseek_key.text().strip(),            "kimi": self.input_kimi_key.text().strip(),            "aliyun_bailian": self.input_aliyun_key.text().strip()        }        self.config["default_ai"] = self.combo_default_ai.currentData()        self.config["generate_count"] = self.spin_count.value()        prompts = [l.strip() for l in self.prompt_edit.toPlainText().split("\n") if l.strip()]        self.config["prompts"] = prompts        self.save_callback(self.config)        QMessageBox.information(self, "提示", "设置已保存")# ============================================================# 定时任务与日志模块# ============================================================class ScheduleTab(QWidget):    def __init__(self, config: dict, save_callback):        super().__init__()        self.config = config        self.save_callback = save_callback        self._init_ui()    def _init_ui(self):        layout = QVBoxLayout(self)        grp_sch = QGroupBox("定时任务设置")        sl = QVBoxLayout(grp_sch)        row_en = QHBoxLayout()        self.chk_enable = QCheckBox("启用定时发布")        self.chk_enable.setChecked(self.config.get("schedule_enabled", False))        row_en.addWidget(self.chk_enable)        row_en.addStretch()        sl.addLayout(row_en)        row_t = QHBoxLayout()        row_t.addWidget(QLabel("添加时间:"))        self.time_edit = QTimeEdit()        self.time_edit.setDisplayFormat("HH:mm")        row_t.addWidget(self.time_edit)        btn_add = QPushButton("添加")        btn_add.clicked.connect(self._add_time)        row_t.addWidget(btn_add)        btn_del = QPushButton("删除选中")        btn_del.clicked.connect(self._del_time)        row_t.addWidget(btn_del)        row_t.addStretch()        sl.addLayout(row_t)        self.time_list = QListWidget()        self.time_list.setMaximumHeight(100)        for t in self.config.get("schedule_times", []):            self.time_list.addItem(t)        sl.addWidget(self.time_list)        btn_save = QPushButton("保存定时设置")        btn_save.clicked.connect(self._on_save)        sl.addWidget(btn_save)        layout.addWidget(grp_sch)        grp_log = QGroupBox("本地运行日志监控")        ll = QVBoxLayout(grp_log)        self.log_view = QTextBrowser()        self.log_view.setStyleSheet("background:#1e1e1e;color:#aaa;font-family:Consolas;font-size:12px;")        ll.addWidget(self.log_view)        br = QHBoxLayout()        btn_ref = QPushButton("刷新日志")        btn_ref.clicked.connect(self._refresh_log)        btn_clr = QPushButton("清空显示")        btn_clr.clicked.connect(self.log_view.clear)        br.addWidget(btn_ref)        br.addWidget(btn_clr)        br.addStretch()        ll.addLayout(br)        layout.addWidget(grp_log)    def _add_time(self):        t = self.time_edit.time().toString("HH:mm")        for i in range(self.time_list.count()):            if self.time_list.item(i).text() == t:                return        self.time_list.addItem(t)    def _del_time(self):        row = self.time_list.currentRow()        if row >= 0:            self.time_list.takeItem(row)    def _on_save(self):        times = [self.time_list.item(i).text() for i in range(self.time_list.count())]        self.config["schedule_times"] = times        self.config["schedule_enabled"] = self.chk_enable.isChecked()        self.save_callback(self.config)        QMessageBox.information(self, "提示", "定时设置已保存")    def _refresh_log(self):        if os.path.exists(LOG_FILE):            try:                with open(LOG_FILE, "r", encoding="utf-8") as f:                    lines = f.readlines()                self.log_view.setPlainText("".join(lines[-200:]))                c = self.log_view.textCursor()                c.movePosition(QTextCursor.MoveOperation.End)                self.log_view.setTextCursor(c)            except Exception:                self.log_view.setPlainText("读取日志失败")        else:            self.log_view.setPlainText("日志文件不存在")    def append_log(self, msg: str, level: str = "info"):        color_map = {"success": "#4caf50", "error": "#f44336", "info": "#aaaaaa"}        color = color_map.get(level, "#aaaaaa")        ts = datetime.now().strftime("%H:%M:%S")        self.log_view.append(f'<span style="color:{color};">[{ts}] {msg}</span>')# ============================================================# 核心发布管理页面# ============================================================class MainTab(QWidget):    def __init__(self, config: dict, log_callback):        super().__init__()        self.config = config        self.log_callback = log_callback        self.articles = []        self.ai_worker = None        self.publish_worker = None        self._init_ui()    def _init_ui(self):        splitter = QSplitter(Qt.Orientation.Horizontal)        left = QWidget()        ll = QVBoxLayout(left)        ll.setContentsMargins(0, 0, 0, 0)        br = QHBoxLayout()        btn_imp = QPushButton("导入XLS文件")        btn_imp.clicked.connect(self._import_xls)        br.addWidget(btn_imp)        br.addWidget(QLabel("AI生成数量:"))        self.spin_ai_count = QSpinBox()        self.spin_ai_count.setRange(1, 200)        self.spin_ai_count.setValue(self.config.get("generate_count", 50))        br.addWidget(self.spin_ai_count)        btn_ai = QPushButton("AI生成")        btn_ai.clicked.connect(self._ai_generate)        br.addWidget(btn_ai)        br.addStretch()        ll.addLayout(br)        self.table = QTableWidget(0, 3)        self.table.setHorizontalHeaderLabels(["标题", "内容(双击预览)", "状态"])        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)        self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)        self.table.setColumnWidth(0, 220)        self.table.cellDoubleClicked.connect(self._on_cell_double_clicked)        ll.addWidget(self.table)        pr = QHBoxLayout()        self.btn_send_now = QPushButton("立即发送(每隔1秒)")        self.btn_send_now.clicked.connect(self._send_now)        pr.addWidget(self.btn_send_now)        self.btn_send_schedule = QPushButton("定时发送")        self.btn_send_schedule.clicked.connect(self._send_schedule)        pr.addWidget(self.btn_send_schedule)        self.btn_stop = QPushButton("停止发送")        self.btn_stop.clicked.connect(self._stop_send)        self.btn_stop.setEnabled(False)        pr.addWidget(self.btn_stop)        btn_clr = QPushButton("清空列表")        btn_clr.clicked.connect(self._clear_table)        pr.addWidget(btn_clr)        pr.addStretch()        ll.addLayout(pr)        splitter.addWidget(left)        right = QWidget()        rl = QVBoxLayout(right)        rl.setContentsMargins(0, 0, 0, 0)        rl.addWidget(QLabel("运行日志"))        self.log_browser = QTextBrowser()        self.log_browser.setStyleSheet("background:#1e1e1e;color:#aaa;font-family:Consolas;font-size:12px;")        rl.addWidget(self.log_browser)        splitter.addWidget(right)        splitter.setSizes([700, 400])        main_layout = QVBoxLayout(self)        main_layout.addWidget(splitter)    def _on_cell_double_clicked(self, row: int, col: int):        if row < len(self.articles):            art = self.articles[row]            dlg = ContentPreviewDialog(art.get("title", ""), art.get("content", ""), self)            dlg.exec()    def _append_log(self, msg: str, level: str = "info"):        color_map = {"success": "#4caf50", "error": "#f44336", "info": "#aaaaaa"}        color = color_map.get(level, "#aaaaaa")        ts = datetime.now().strftime("%H:%M:%S")        self.log_browser.append(f'<span style="color:{color};">[{ts}] {msg}</span>')        c = self.log_browser.textCursor()        c.movePosition(QTextCursor.MoveOperation.End)        self.log_browser.setTextCursor(c)        logger.info(msg)        if self.log_callback:            self.log_callback(msg, level)    def _import_xls(self):        fp, _ = QFileDialog.getOpenFileName(self, "选择XLS文件", "", "Excel文件 (*.xls *.xlsx);;所有文件 (*)")        if not fp:            return        try:            import openpyxl            wb = openpyxl.load_workbook(fp, read_only=True)            ws = wb.active            count = 0            for row in ws.iter_rows(min_row=2, values_only=True):                if row and len(row) >= 2 and row[0]:                    self.articles.append({"title": str(row[0]).strip(), "content": str(row[1] or "").strip(), "status": "待发送"})                    count += 1            wb.close()            self._refresh_table()            self._append_log(f"成功导入 {count} 篇文章", "success")        except ImportError:            self._append_log("请安装 openpyxl: pip install openpyxl", "error")            QMessageBox.warning(self, "缺少依赖", "请安装openpyxl:\npip install openpyxl")        except Exception as e:            self._append_log(f"导入XLS失败: {e}", "error")    def _ai_generate(self):        ai_type = self.config.get("default_ai", "aliyun_bailian")        api_key = self.config.get("ai_keys", {}).get(ai_type, "")        if not api_key:            self._append_log(f"未配置 {ai_type} 的API Key,请到设置页面配置", "error")            QMessageBox.warning(self, "提示", f"请先在设置页面配置 {ai_type} 的API Key")            return        prompts = self.config.get("prompts", [])        if not prompts:            self._append_log("未配置提示词,请到设置页面添加", "error")            return        prompt = random.choice(prompts)        count = self.spin_ai_count.value()        self.ai_worker = AIGenerateWorker(ai_type, api_key, prompt, count)        self.ai_worker.log_signal.connect(self._append_log)        self.ai_worker.result_signal.connect(self._on_ai_result)        self.ai_worker.start()    def _on_ai_result(self, results: list):        for item in results:            if isinstance(item, dict) and "title" in item and "content" in item:                self.articles.append({"title": item["title"], "content": item["content"], "status": "待发送"})        self._refresh_table()    def _refresh_table(self):        self.table.setRowCount(len(self.articles))        for i, art in enumerate(self.articles):            self.table.setItem(i, 0, QTableWidgetItem(art.get("title", "")))            ct = art.get("content", "")            self.table.setItem(i, 1, QTableWidgetItem(ct[:80] + ("..." if len(ct) > 80 else "")))            self.table.setItem(i, 2, QTableWidgetItem(art.get("status", "待发送")))    def _send_now(self):        if not self.articles:            self._append_log("没有可发送的文章", "error")            return        pub_url = self.config.get("publish_url", "")        cookie = self.config.get("cookie_str", "")        if not pub_url or not cookie:            self._append_log("请先在设置页面配置发布URL和Cookie", "error")            QMessageBox.warning(self, "提示", "请先配置发布URL和Cookie")            return        self.btn_send_now.setEnabled(False)        self.btn_send_schedule.setEnabled(False)        self.btn_stop.setEnabled(True)        self.publish_worker = BatchPublishWorker(self.articles, pub_url, cookie, interval=1.0)        self.publish_worker.log_signal.connect(self._append_log)        self.publish_worker.progress_signal.connect(self._on_publish_progress)        self.publish_worker.finished_signal.connect(self._on_publish_done)        self.publish_worker.start()    def _send_schedule(self):        if not self.articles:            self._append_log("没有可发送的文章", "error")            return        self._append_log("文章已加入定时发送队列,将在设定时间自动发送", "info")    def _stop_send(self):        if self.publish_worker:            self.publish_worker.stop()        self.btn_stop.setEnabled(False)    def _on_publish_progress(self, index: int, status: str):        if index < len(self.articles):            self.articles[index]["status"] = status            self.table.setItem(index, 2, QTableWidgetItem(status))    def _on_publish_done(self):        self.btn_send_now.setEnabled(True)        self.btn_send_schedule.setEnabled(True)        self.btn_stop.setEnabled(False)        self._append_log("所有发布任务已完成", "success")    def _clear_table(self):        self.articles.clear()        self.table.setRowCount(0)        self._append_log("已清空文章列表", "info")    def update_config(self, config: dict):        self.config = config        self.spin_ai_count.setValue(config.get("generate_count", 50))# ============================================================# 主窗口程序# ============================================================class MainWindow(QMainWindow):    def __init__(self):        super().__init__()        self.config = load_config()        self._init_ui()        self._start_schedule_timer()    def _init_ui(self):        self.setWindowTitle(WINDOW_TITLE)        self.setMinimumSize(WINDOW_WIDTH, WINDOW_HEIGHT)        self.resize(WINDOW_WIDTH, WINDOW_HEIGHT)        self.setFont(QFont("Microsoft YaHei", 9))        central = QWidget()        self.setCentralWidget(central)        layout = QVBoxLayout(central)        layout.setContentsMargins(8, 8, 8, 8)        self.tabs = QTabWidget()        self.main_tab = MainTab(self.config, self._on_log_from_main)        self.tabs.addTab(self.main_tab, "发布管理")        self.settings_tab = SettingsTab(self.config, self._on_config_saved)        self.tabs.addTab(self.settings_tab, "设置")        self.schedule_tab = ScheduleTab(self.config, self._on_config_saved)        self.tabs.addTab(self.schedule_tab, "定时任务 & 日志")        layout.addWidget(self.tabs)        self._apply_styles()    def _apply_styles(self):        self.setStyleSheet("""            QMainWindow { background-color: #f5f5f5; }            QGroupBox { font-weight: bold; border: 1px solid #ddd; border-radius: 4px; margin-top: 8px; padding-top: 16px; }            QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 4px; }            QLineEdit, QTextEdit, QSpinBox, QTimeEdit {                border: 1px solid #ccc; border-radius: 3px; padding: 4px 6px; background: white;            }            QLineEdit:focus, QTextEdit:focus { border-color: #4a90d9; }            QComboBox {                border: 1px solid #ccc; border-radius: 3px; padding: 6px 12px;                background: white; min-height: 26px; font-size: 13px;            }            QComboBox::drop-down {                subcontrol-origin: padding; subcontrol-position: top right;                width: 28px; border-left: 1px solid #ddd; background: #fafafa;            }            QComboBox::down-arrow {                width: 0; height: 0;                border-left: 5px solid transparent; border-right: 5px solid transparent;                border-top: 6px solid #666;            }            QComboBox QAbstractItemView {                border: 1px solid #ccc; background: white;                selection-background-color: #4a90d9; selection-color: white;                padding: 2px; outline: none;            }            QComboBox QAbstractItemView::item { min-height: 30px; padding: 4px 10px; }            QPushButton {                background-color: #4a90d9; color: white; border: none;                border-radius: 3px; padding: 6px 14px; min-width: 80px;            }            QPushButton:hover { background-color: #357abd; }            QPushButton:pressed { background-color: #2a5f9e; }            QPushButton:disabled { background-color: #aaa; color: #ddd; }            QTableWidget { border: 1px solid #ccc; gridline-color: #eee; }            QTabWidget::pane { border: 1px solid #ccc; }            QTextBrowser { border: 1px solid #ccc; border-radius: 3px; }        """)    def _on_config_saved(self, config: dict):        self.config = config        save_config(config)        self.main_tab.update_config(config)    def _on_log_from_main(self, msg: str, level: str):        self.schedule_tab.append_log(msg, level)    def _start_schedule_timer(self):        self.schedule_timer = QTimer()        self.schedule_timer.timeout.connect(self._check_schedule)        self.schedule_timer.start(30000)    def _check_schedule(self):        if not self.config.get("schedule_enabled", False):            return        now = datetime.now().strftime("%H:%M")        times = self.config.get("schedule_times", [])        if now in times and self.main_tab.articles:            if not hasattr(self, '_last_trigger') or self._last_trigger != now:                self._last_trigger = now                logger.info(f"定时任务触发: {now}")                self.schedule_tab.append_log(f"定时任务触发: {now},开始自动发送", "info")                self.main_tab._send_now()# ============================================================# 程序入口# ============================================================def main():    import traceback    def exc_handler(et, ev, tb):        logger.error("".join(traceback.format_exception(et, ev, tb)))    sys.excepthook = exc_handler    app = QApplication(sys.argv)    app.setStyle("Fusion")    window = MainWindow()    window.show()    sys.exit(app.exec())if __name__ == "__main__":    main()

四、核心技术知识点总结

1. PyQt6 桌面GUI开发:掌握Qt布局管理、标签页组件、表格控件、弹窗组件、样式美化等桌面开发核心技术,实现高颜值、交互友好的桌面客户端,同时通过异步子线程解决UI卡顿问题。

2. 多AI大模型API对接:熟练使用HTTP请求对接主流大模型接口,实现标准化参数封装、JSON数据解析、异常捕获、超时处理,掌握多模型兼容调用、随机提示词优化文案质量的实战技巧。

3. 网页抓包与接口逆向:复刻今日头条官方发文接口协议,模拟真实浏览器请求头、Cookie身份认证、固定报文参数,理解Web接口请求逻辑、参数校验机制、HTTP状态码异常排查。

4. Python异步多线程开发:通过QThread实现耗时任务异步执行,区分UI主线程与工作子线程,避免程序卡死,掌握信号与槽机制实现跨线程数据、日志、状态传输。

5. 配置持久化与日志体系:基于JSON文件实现参数永久保存,搭建分级日志系统,同时输出本地文件日志与前端可视化日志,掌握程序容错、异常处理、运行监控的开发思维。

6. 定时任务调度机制:利用QTimer定时器实现轮询检测,精准触发定时任务,实现无人值守自动化执行,掌握轻量级桌面定时程序开发逻辑。

五、测试步骤与拓展应用场景

✅ 完整测试步骤(新手零基础可跑通)

1. 环境部署:安装Python3.8及以上版本,执行依赖安装命令:pip install pyqt6 requests openpyxl

2. 源码运行:将纯净源码保存为 main.py,终端运行 python main.py,启动可视化客户端。

3. 参数配置:进入设置页面,填入抓包获取的头条发布URL、账号Cookie,填写任意一款AI模型的API密钥,自定义创作提示词并保存。

4. 功能测试:测试AI一键生成文案、Excel批量导入、文章预览、立即批量发布,查看日志判断运行状态。

5. 定时测试:添加1分钟后的测试时间,开启定时开关,等待程序自动触发发布任务,验证定时功能有效性。

6. 异常测试:故意清空密钥、错误URL、断开网络,查看程序是否正常报错、不崩溃、日志精准记录问题。

✅ 拓展应用场景

1. 自媒体矩阵运营:多账号批量轮询发文,每日定时定量更新,快速养号、提升账号活跃度与平台权重。

2. 垂直领域内容量产:适配情感、职场、三农、科普、美食等多个领域,通过自定义提示词批量生成垂直原创内容,打造垂直账号。

3. 工作室批量运维:多设备部署工具,分工生成、审核、发布内容,实现团队标准化内容量产作业。

4. 自动化流量变现:稳定日更产出内容,积累自然流量,适配头条流量分成、内容带货、品牌接单变现模式。

5. 二次开发拓展:可基于本框架拓展图文混排、视频发文、多平台同步(西瓜、百家号)、AI自动改写、内容查重等高级功能。