1. 项目概述:一个每三小时自动测试产品的AI智能体
最近和几个做SaaS的朋友聊天,大家普遍头疼一个问题:产品上线后,功能越来越多,回归测试的成本高得吓人。每次发版前,测试团队都要通宵达旦地跑用例,稍微漏掉一个边缘场景,线上就可能出问题。更麻烦的是,有些偶发性的Bug,在测试环境复现不了,到了生产环境用户一用就崩,搞得团队焦头烂额。
我当时就在想,能不能让测试这件事自己“动”起来?不是简单地用脚本录放,而是有一个真正能“理解”产品、能像真实用户一样思考、并且不知疲倦的智能体,来持续不断地验证产品的核心流程。于是,我花了几周时间,捣鼓出了一个能每三小时自动运行、深度测试我自己产品的AI智能体。它不是什么遥不可及的黑科技,核心就是利用现有的AI能力(比如大语言模型的推理和规划能力)加上自动化框架,搭建的一个“数字质检员”。
这个AI智能体对我而言,已经从一个实验性项目,变成了研发流程中不可或缺的一环。它不取代人工测试,而是把测试工程师从重复、枯燥的回归任务中解放出来,让他们能更专注于探索性测试和复杂场景设计。同时,它像一个永不间断的哨兵,以固定的频率巡检核心链路,一旦发现异常(比如页面元素丢失、接口返回错误、业务流程卡住),能立刻通过钉钉、飞书或者邮件告警,让我们在用户反馈之前就定位到问题。这篇文章,我就来拆解一下这个AI测试智能体的设计思路、技术实现、以及我在搭建和运行过程中踩过的那些坑。无论你是开发者、测试工程师还是产品经理,只要你的产品需要持续交付和稳定保障,这套思路或许能给你带来一些启发。
2. 整体架构与核心设计思路
2.1 为什么是“智能体”而不仅仅是“自动化脚本”?
传统的UI自动化测试脚本(比如用Selenium、Playwright写的)本质上是“录放机”。它严格遵循预设的步骤:点击这里,输入那个,检查某个元素是否存在。这种方式的优点是稳定、可预期。但缺点也非常明显:脆弱。页面结构稍微一变(比如一个按钮的class名改了),脚本就挂了;死板,它无法处理脚本编写时未考虑到的情况;维护成本高,产品迭代越快,脚本需要同步修改的地方就越多。
我想要的“智能体”,核心区别在于它具备一定的感知、决策和容错能力。它不仅仅是执行步骤,而是理解任务目标。举个例子,任务目标是“以管理员身份登录,并创建一个新用户”。传统脚本会写成:
- 访问
/login。 - 在
#username输入框输入 “admin”。 - 在
#password输入框输入 “123456”。 - 点击
#submit-btn。 - 验证是否跳转到
/dashboard。 - 点击侧边栏的
#user-management。 - 点击
#create-user-btn。 - ……
而AI智能体的“思考”过程更像是:
- 目标:创建新用户。前提:需要管理员权限。
- 步骤1:登录。我需要找到登录页面。当前页面是哪里?如果是首页,通常导航栏会有“登录”链接。我去找找看。(它可能会通过分析页面DOM,寻找包含“登录”、“Sign In”等文本或常见登录图标的元素)。
- 步骤2:找到了登录表单。需要输入用户名和密码。我知道管理员的账号是
admin,密码从安全配置中读取。输入。 - 步骤3:提交表单。成功后,我应该能看到管理员后台的特定元素(比如“管理面板”标题或管理员独有的菜单)。
- 步骤4:导航到用户管理。页面上可能有“用户”、“成员”、“团队”等相关的导航项。我需要识别并点击它。
- 步骤5:寻找创建用户的入口。可能是按钮,也可能是“添加”图标。点击。
- 步骤6:填写创建用户的表单。表单字段可能动态变化,我需要识别出“用户名”、“邮箱”、“角色”等字段,并填入符合业务规则的测试数据(比如生成一个随机邮箱,避免重复)。
- 步骤7:提交并验证。检查是否出现“创建成功”的提示,或者在用户列表中能找到刚创建的用户。
你会发现,智能体不依赖于固定的CSS选择器或XPath。它通过自然语言理解页面内容,根据目标动态规划行动路径。这带来了巨大的灵活性:即使按钮位置变了、文本改了,只要它的功能语义还在(比如还是一个“提交”按钮),智能体就有很大概率能识别并操作它。这极大地降低了维护成本。
2.2 系统核心组件拆解
我的这个每三小时运行的AI测试智能体,主要由以下几个核心部分组成,它们协同工作,形成了一个闭环:
任务调度与协调中心(大脑):这是整个系统的指挥官。我使用了一个轻量级的编排框架(比如LangGraph或自定义的状态机)来定义测试流程。它负责接收“测试产品核心功能”这个高级指令,并将其分解成一系列具体的原子任务(Task),例如“测试登录功能”、“测试下单流程”、“检查数据报表加载”等。它管理着任务队列、执行状态,并处理任务之间的依赖关系(例如,必须先登录才能下单)。
AI规划与执行引擎(核心):这是智能体的“思考”器官。对于每个原子任务,由一个LLM(大语言模型)驱动。我主要调用云端API(如GPT-4、Claude-3或国内的一些合规大模型),它的工作流程是:
- 感知(Perception):接收当前网页的DOM结构、截图(可选)以及任务描述。
- 规划(Planning):分析当前状态和目标,决定下一步应该做什么。例如,“当前在登录页面,目标是登录。我需要输入用户名和密码,然后点击提交按钮。”
- 行动生成(Action Generation):将规划出的“意图”转化为自动化框架(如Playwright)可以执行的具体命令。例如,生成一个JSON对象:
{“action”: “fill”, “selector”: “input[placeholder=‘用户名或邮箱’]”, “text”: “test_user”}。这里的选择器可以是基于语义的,而不一定是固定ID。 - 执行与观察:自动化框架执行该动作,然后获取执行后的新页面状态,再次交给LLM进行感知,进入下一个循环,直到任务完成或失败。
自动化执行器(手脚):我选择了Playwright作为底层自动化工具。相比Selenium,Playwright对现代Web应用(尤其是单页应用SPA)的支持更好,自动等待机制更智能,且能轻松处理iframe、文件下载等场景。它忠实地执行AI引擎发出的指令,并捕获执行结果、网络请求、控制台日志等丰富信息。
状态管理与记忆模块:智能体需要有短期记忆。例如,在创建订单的流程中,系统生成的订单号需要记住,以便在后续的查询或取消任务中使用。我使用一个简单的键值存储(如Redis或甚至一个内存字典)来保存跨步骤的上下文信息。
断言与异常处理机制(判断):任务完成后,需要判断是否成功。这不仅仅是检查页面有没有报错。LLM会再次分析最终状态,根据任务目标进行“语义断言”。例如,对于“登录成功”的断言,可能是“页面中出现了用户头像或‘欢迎回来’的文本”,而不是死板地检查某个URL。对于异常,系统会捕获Playwright的错误、网络超时、以及LLM自身判断的“无法达成目标”状态,并进行分类(元素未找到、业务流程错误、性能问题等)。
报告与告警系统(输出):每一次运行(每三小时)都会生成一份结构化的测试报告,包括:执行了哪些任务、成功/失败、耗时、关键步骤截图、错误日志和LLM的推理过程(这对调试非常有用)。如果出现失败,系统会立即通过Webhook触发钉钉机器人告警,将错误摘要和链接推送到相关群组。
配置与数据工厂:管理测试所需的配置,如被测环境地址、测试账号、API密钥等。同时,包含一个数据工厂,用于生成符合业务规则的测试数据,例如随机的用户名、邮箱、地址,避免测试数据污染和生产数据冲突。
2.3 关键技术选型背后的考量
为什么用Playwright+LLM,而不是其他组合?
- Playwright vs Selenium/Cypress:Playwright由微软开发,原生支持多浏览器(Chromium, Firefox, WebKit),且API设计非常现代。它的
auto-waiting功能是巨大优势,能自动等待元素可交互,减少了测试脚本中大量硬编码的sleep,让AI生成的指令更稳定。此外,它的codegen和trace viewer工具对调试AI执行过程非常有帮助。 - LLM的选择:我尝试过不同规模的模型。重量级的GPT-4在复杂任务规划和语义理解上表现最佳,但成本较高。对于相对固定的流程,性能稍弱但成本更低的模型(如GPT-3.5-Turbo)也可能够用。关键是要给模型提供清晰的指令(Prompt)和丰富的上下文(页面信息)。一个重要经验是:将业务规则和操作约束明确写入Prompt,比如“永远不要使用真实用户的信用卡信息进行测试”、“创建数据时使用
test_前缀”。 - 编排框架:LangGraph非常适合构建有状态的、多步骤的智能体。它用图(Graph)来定义工作流,节点是任务或LLM调用,边是状态流转的条件,直观且强大。如果不想引入新框架,用Python的
asyncio配合一个简单的状态机也能实现核心循环。
这个架构的核心思想是“LLM负责思考「做什么」和「为什么」,Playwright负责精准地执行「怎么做」”,两者结合,既拥有了人类般的适应性,又具备了机器的精确性与速度。
3. 实操搭建:从零构建你的AI测试智能体
3.1 环境准备与基础框架搭建
首先,你需要一个干净的Python环境(建议3.9以上)。我的项目结构大致如下:
ai_test_agent/ ├── config/ │ ├── __init__.py │ ├── settings.py # 存放环境变量、URL、账号等配置 │ └── prompts.py # 存放给LLM的各种指令模板 ├── core/ │ ├── __init__.py │ ├── agent.py # AI智能体核心类,封装LLM调用和规划逻辑 │ ├── actions.py # 将LLM的输出解析为Playwright动作 │ ├── memory.py # 上下文记忆管理 │ └── orchestrator.py # 任务编排器 ├── tasks/ │ ├── __init__.py │ ├── base_task.py # 所有任务的基类 │ ├── login_task.py # 登录任务 │ ├── create_order_task.py # 下单任务 │ └── ... # 其他具体任务 ├── utils/ │ ├── data_factory.py # 测试数据生成 │ ├── reporter.py # 报告生成器 │ └── notifier.py # 告警通知 ├── requirements.txt ├── main_scheduler.py # 主调度入口,处理3小时定时触发 └── .env # 环境变量文件(切勿提交)安装核心依赖。requirements.txt关键内容如下:
playwright>=1.40.0 openai>=1.0.0 # 或其他LLM SDK,如 anthropic, dashscope langgraph>=0.0.5 # 可选,用于高级编排 python-dotenv>=1.0.0 redis>=5.0.0 # 可选,用于分布式记忆存储 schedule>=1.2.0 # 用于定时任务 pydantic>=2.0.0 # 用于数据验证和设置管理运行pip install -r requirements.txt安装依赖,并安装Playwright浏览器:playwright install chromium。
注意:LLM API密钥是最高机密,务必通过环境变量(
.env文件)管理,绝对不要硬编码在代码中。.env文件内容类似:OPENAI_API_KEY=sk-...,并在.gitignore中忽略它。
3.2 设计核心的Agent类
core/agent.py是这个系统的心脏。下面是一个高度简化的核心逻辑展示:
import asyncio from typing import Dict, Any, Optional from openai import AsyncOpenAI # 示例使用OpenAI from pydantic import BaseModel from playwright.async_api import Page class Action(BaseModel): """定义LLM可以执行的动作""" action_type: str # 如 "click", "fill", "navigate", "assert" selector: Optional[str] = None # 可选,元素选择器 text: Optional[str] = None # 可选,输入的文本 description: str # 动作的自然语言描述,用于调试 class AITestAgent: def __init__(self, page: Page, llm_client, system_prompt: str): self.page = page self.llm = llm_client self.system_prompt = system_prompt self.memory = {} # 简单的内存字典 async def perceive(self) -> str: """感知当前页面状态,获取可供LLM分析的信息""" # 1. 获取页面的简化DOM或可访问性树。全量DOM太大,需要精简。 # 使用Playwright获取主要交互元素的简洁表示。 elements = await self.page.evaluate(""" () => { const items = []; // 收集按钮、输入框、链接等关键交互元素 document.querySelectorAll('button, input, a, [role="button"], [role="link"]').forEach(el => { items.push({ tag: el.tagName, text: el.innerText?.slice(0, 50) || el.value || el.placeholder, ariaLabel: el.getAttribute('aria-label'), type: el.type, placeholder: el.placeholder, // 生成一个相对稳定的选择器,优先使用data-testid等测试属性 selector: el.getAttribute('data-testid') || (el.id ? `#${el.id}` : '') || (el.name ? `[name="${el.name}"]` : '') }); }); return items.filter(item => item.selector || item.text); // 过滤掉无标识元素 } """) # 2. 获取当前URL和页面标题 url = self.page.url title = await self.page.title() # 3. 将信息格式化成LLM易于理解的文本 perception = f"当前页面标题:{title}\\n当前URL:{url}\\n\\n页面中可交互元素:\\n" for idx, el in enumerate(elements[:20]): # 限制数量,避免token超限 perception += f"{idx+1}. 标签<{el['tag']}>, 文本/值:'{el.get('text')}', 选择器参考:'{el.get('selector')}'\\n" return perception async def plan_and_act(self, task_description: str, max_steps: int = 10) -> Dict[str, Any]: """核心循环:感知-规划-执行""" for step in range(max_steps): print(f"步骤 {step + 1}") # 1. 感知 current_state = await self.perceive() # 2. 规划:调用LLM,决定下一步动作 prompt = f""" {self.system_prompt} 当前任务:{task_description} 当前页面状态: {current_state} 你之前的操作记忆: {self.memory.get('last_action', '无')} 请根据当前状态和任务目标,决定下一步做什么。你只能输出一个JSON对象,格式必须严格如下: {{ "thought": "你的思考过程,分析当前情况", "action": {{ "action_type": "click|fill|navigate|assert|complete|fail", "selector": "可选,CSS选择器或你从页面元素中推断的标识", "text": "可选,如果是fill动作,需要输入的文本", "description": "对该动作的自然语言描述" }}, "is_task_complete": false }} 如果任务已经成功完成,将"is_task_complete"设为true,并且"action"的"action_type"设为"complete"。 如果任务确定无法完成,将"is_task_complete"设为true,并且"action"的"action_type"设为"fail",并在"thought"中说明原因。 """ try: response = await self.llm.chat.completions.create( model="gpt-4-turbo-preview", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"}, temperature=0.1 # 低随机性,保证动作稳定 ) decision = json.loads(response.choices[0].message.content) except Exception as e: return {"status": "error", "message": f"LLM调用失败: {e}"} print(f"思考:{decision['thought']}") print(f"动作:{decision['action']}") # 3. 执行动作 action = decision["action"] self.memory['last_action'] = action['description'] if action["action_type"] == "complete": return {"status": "success", "message": "任务完成"} elif action["action_type"] == "fail": return {"status": "failure", "message": decision["thought"]} # 执行具体的Playwright操作 try: if action["action_type"] == "click": if action["selector"]: await self.page.click(action["selector"]) else: # 如果LLM没提供选择器,可以尝试通过文本内容查找 await self.page.get_by_text(action["description"]).click() elif action["action_type"] == "fill": await self.page.fill(action["selector"], action["text"]) elif action["action_type"] == "navigate": await self.page.goto(action["text"]) # text字段存放URL elif action["action_type"] == "assert": # 这里可以扩展为更复杂的断言逻辑 pass # 等待页面稳定 await self.page.wait_for_load_state("networkidle") except Exception as e: return {"status": "error", "message": f"执行动作失败: {e}", "last_decision": decision} # 4. 短暂暂停,模拟人类操作间隔 await asyncio.sleep(1) return {"status": "timeout", "message": f"达到最大步骤数 {max_steps}"}这个AITestAgent类封装了感知页面、调用LLM决策、执行动作的核心循环。关键在于system_prompt的设计,它定义了智能体的角色和行为准则。
3.3 编写有效的System Prompt(系统指令)
config/prompts.py中存放着给AI的“宪法”。一个优秀的Prompt能极大提升智能体的表现。我的基础系统指令如下:
BASE_SYSTEM_PROMPT = """ 你是一个专业的Web应用测试AI助手。你的目标是通过浏览器自动化,完成指定的测试任务。 请严格遵守以下规则: 1. **安全与合规**: - 你测试的是预生产或测试环境,绝对不要访问或操作生产环境数据,除非明确指定。 - 只能使用提供的测试账号,不得尝试猜测或使用其他账号密码。 - 生成测试数据时(如用户名、邮箱),必须使用“test_”前缀或明显的测试模式,避免与真实用户数据混淆。 2. **操作规范**: - 仔细分析当前页面状态(提供的元素列表)后再做决定。 - 优先使用元素中提供的`selector`(特别是`data-testid`)进行操作,它最稳定。 - 如果没有稳定的选择器,再尝试通过元素的文本内容、标签类型、ARIA标签等属性来定位。 - 每次只执行一个清晰、简单的动作(如点击一个按钮,填写一个输入框)。 - 执行动作后,等待页面加载或状态更新(系统会自动处理)。 3. **任务理解**: - 清晰理解最终任务目标。将复杂任务分解为小步骤。 - 如果遇到错误(如元素找不到、页面无响应),先尝试分析原因(是否走错了流程?),并尝试1-2次替代方案(如点击另一个看起来功能相同的按钮)。 - 如果多次尝试后任务明显无法推进,果断标记为失败,并在思考中清晰说明原因。 4. **输出格式**: - 你必须且只能输出一个合法的JSON对象,包含`thought`, `action`, `is_task_complete`字段。 - `action`对象必须包含`action_type`, `selector`, `text`, `description`字段。 现在,请开始你的测试工作。记住:像一名有经验的测试工程师一样思考,谨慎而果断。 """这个Prompt明确了角色、安全边界、操作优先级和输出规范,是智能体稳定工作的基石。针对不同的任务类型(如登录、数据查询、表单提交),还可以在具体任务调用时追加更具体的指令。
3.4 实现一个具体的测试任务
以“管理员登录”任务为例,tasks/login_task.py:
import asyncio from core.agent import AITestAgent from config import settings, prompts class LoginTask: def __init__(self, agent: AITestAgent): self.agent = agent self.task_description = f"以管理员身份登录系统。登录页面URL是:{settings.LOGIN_URL}。成功登录后,应能看到管理员专属的仪表板或菜单。" async def run(self): # 1. 首先导航到登录页面 navigate_action = { "action_type": "navigate", "text": settings.LOGIN_URL, "description": "导航到登录页面" } # 这里可以简单调用page.goto,或也通过Agent执行 await self.agent.page.goto(settings.LOGIN_URL) await self.agent.page.wait_for_load_state("networkidle") # 2. 使用Agent执行智能登录流程 # 将管理员账号密码存入Agent的短期记忆,供其填写时使用 self.agent.memory['admin_username'] = settings.ADMIN_USER self.agent.memory['admin_password'] = settings.ADMIN_PASSWORD # 构建任务专属Prompt login_prompt = prompts.BASE_SYSTEM_PROMPT + f""" 当前具体任务:{self.task_description} 已知信息:管理员用户名是 '{settings.ADMIN_USER}',密码已提供。 请找到登录表单,填写凭证并提交。 """ # 临时替换Agent的Prompt original_prompt = self.agent.system_prompt self.agent.system_prompt = login_prompt # 运行智能体循环 result = await self.agent.plan_and_act(self.task_description, max_steps=15) # 恢复原始Prompt self.agent.system_prompt = original_prompt # 3. 验证登录是否成功(也可以让LLM在循环中判断,这里做双重校验) if result["status"] == "success": # 检查页面是否包含管理员专属标识 dashboard_text = await self.agent.page.text_content("body") if "仪表板" in dashboard_text or "Dashboard" in dashboard_text or "管理" in dashboard_text: print("登录任务成功:检测到管理员页面元素。") return True else: print("登录任务可能失败:未检测到管理员页面标识。") return False else: print(f"登录任务失败:{result['message']}") return False这个任务类封装了登录的完整流程:导航、执行智能登录、结果验证。它展示了如何将具体的业务逻辑(登录)与通用的AI智能体结合起来。
3.5 设置定时任务与报告
最后,我们需要一个调度器来每三小时运行一次完整的测试套件。main_scheduler.py:
import asyncio import schedule import time from datetime import datetime from playwright.async_api import async_playwright from core.agent import AITestAgent from tasks.login_task import LoginTask from tasks.create_order_task import CreateOrderTask from utils.reporter import generate_html_report from utils.notifier import send_alert async def run_full_test_suite(): """运行完整的测试套件""" run_id = datetime.now().strftime("%Y%m%d_%H%M%S") results = [] async with async_playwright() as p: # 启动浏览器,建议用headless模式(无头模式)在服务器运行 browser = await p.chromium.launch(headless=True, args=['--disable-dev-shm-usage']) context = await browser.new_context(viewport={'width': 1920, 'height': 1080}) page = await context.new_page() # 初始化AI智能体 llm_client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) # 示例 agent = AITestAgent(page, llm_client, prompts.BASE_SYSTEM_PROMPT) # 定义要运行的任务序列 test_tasks = [ ("管理员登录", LoginTask(agent)), ("创建测试订单", CreateOrderTask(agent)), # ... 添加更多任务 ] for task_name, task_instance in test_tasks: print(f"[{run_id}] 开始任务: {task_name}") start_time = time.time() try: success = await task_instance.run() elapsed = time.time() - start_time result = { "task": task_name, "status": "PASS" if success else "FAIL", "duration": round(elapsed, 2), "timestamp": datetime.now().isoformat() } results.append(result) print(f"[{run_id}] 任务结束: {task_name} - {result['status']} ({elapsed:.2f}s)") if not success: # 任务失败,截图保存 screenshot_path = f"./screenshots/failure_{run_id}_{task_name}.png" await page.screenshot(path=screenshot_path, full_page=True) result["screenshot"] = screenshot_path # 发送实时告警 send_alert(f"测试失败告警 - {task_name}", f"运行ID: {run_id}\\n请查看截图: {screenshot_path}") except Exception as e: elapsed = time.time() - start_time error_result = { "task": task_name, "status": "ERROR", "duration": round(elapsed, 2), "error": str(e), "timestamp": datetime.now().isoformat() } results.append(error_result) print(f"[{run_id}] 任务异常: {task_name} - {e}") send_alert(f"测试异常告警 - {task_name}", f"运行ID: {run_id}\\n错误信息: {e}") # 关闭浏览器 await context.close() await browser.close() # 生成并保存报告 report_path = generate_html_report(run_id, results) print(f"[{run_id}] 测试完成,报告已生成: {report_path}") # 如果有失败或错误,汇总告警 failures = [r for r in results if r['status'] in ('FAIL', 'ERROR')] if failures: summary = f"本次巡检({run_id})发现 {len(failures)} 个问题。" send_alert(f"测试巡检摘要 - {run_id}", summary) def job(): """调度任务""" print(f"[调度器] 开始执行定时测试任务 {datetime.now()}") asyncio.run(run_full_test_suite()) if __name__ == "__main__": # 立即运行一次 job() # 每3小时运行一次 schedule.every(3).hours.do(job) print("AI测试智能体调度器已启动,每3小时运行一次。") while True: schedule.run_pending() time.sleep(60) # 每分钟检查一次这个调度器使用schedule库管理定时任务,每三小时触发一次完整的测试流程。它管理浏览器的生命周期,按顺序执行任务,收集结果,生成报告,并在发现问题时发送告警。
4. 核心挑战与优化策略实录
在实际运行中,我遇到了不少问题,也总结出一些让智能体更稳定、更高效的策略。
4.1 挑战一:LLM的“幻觉”与不稳定操作
问题:LLM有时会“想象”出页面上不存在的元素,或者生成错误的选择器(比如一个完全无效的CSS路径)。这会导致动作执行失败,循环卡住。
解决方案:
- 强化感知信息质量:不要给LLM完整的HTML,那太嘈杂且消耗token。我优化了
perceive函数,只提取关键的交互元素(button, input, a)及其最稳定的属性(>
listmonk数据库会话状态:临时数据存储方法
listmonk数据库会话状态:临时数据存储方法 【免费下载链接】listmonk High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app. 项目地址: https://gitcode.com/GitHub_Trending/li/listmonk 在使…
智慧职教刷课脚本终极指南:3分钟快速实现全自动学习
智慧职教刷课脚本终极指南:3分钟快速实现全自动学习 【免费下载链接】auto-play-course 简单好用的刷课脚本[支持平台:职教云,智慧职教,资源库] 项目地址: https://gitcode.com/gh_mirrors/hc/auto-play-course 智慧职教刷课脚本是一款专为职业教育在线学习平…
终极邮件模板设计指南:listmonk响应式框架与MJML实战技巧
终极邮件模板设计指南:listmonk响应式框架与MJML实战技巧 【免费下载链接】listmonk High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app. 项目地址: https://gitcode.com/GitHub_Trending/li/list…
C++类链接错误解析与解决方案
1. 问题现象解析当使用GCC工具链编译包含类声明的C程序时,链接器可能会报出"undefined reference"错误。这类错误通常表现为:.\obj\blinky.o(.text0x40): In function __static_initialization_and_destruction_0: /cygdrive/c/Keil/ARM/GNU/E…
Android微信双开终极指南:如何通过WeChatPad实现真正的平板模式登录
Android微信双开终极指南:如何通过WeChatPad实现真正的平板模式登录 【免费下载链接】WeChatPad 强制使用微信平板模式 项目地址: https://gitcode.com/gh_mirrors/we/WeChatPad 你是否厌倦了在手机和平板之间频繁切换微信的烦恼?想要在同一台And…
微信聊天记录解密终极指南:如何安全访问被加密的珍贵数据
微信聊天记录解密终极指南:如何安全访问被加密的珍贵数据 【免费下载链接】WechatDecrypt 微信消息解密工具 项目地址: https://gitcode.com/gh_mirrors/we/WechatDecrypt 你是否曾因更换手机而丢失重要的微信聊天记录?是否担心商务沟通的宝贵信息…