自动完成 ADHome(adhome.woa.com)广告账户加白(机构准入评估)申请的全流程。经过 10+ 次实战迭代优化,具备信号文件确认机制确保安全提交。
核心能力:
> 重要:严格按此流程执行,不要添加额外步骤。
用户: "加白第N行" + 文档链接
│
┌────┴────┐
│ 前置检查 │
└────┬────┘
│
0️⃣ Webhook 配置检查(首次触发)
├─ user_config.json 存在? → YES → 读取 webhook
└─ NO → 引导用户配置 → 保存
│
① 识别文档类型并读取第N行数据
├─ doc.weixin.qq.com → Playwright + 企微Cookie
└─ docs.qq.com → MCP export_file 导出 xlsx + openpyxl
⚠️ 必须先读表头确认列映射
│
② 定位/下载素材文件(⚠️ 附件格式优先级:docx > 图片 > PDF)
├─ 本地搜索 ~/Desktop/加白/
├─ 企微文档 → Playwright 菜单导出 docx
├─ 腾讯文档 → Chrome profile 登录态 + 菜单导出 docx
├─ ⚠️ 下载失败/只能PDF?→ 暂停,提醒用户检查权限和登录态
│ ├─ 🛑 用户选停止 → 关闭浏览器,终止当前行
│ └─ 🔄 用户选继续 → 关闭浏览器,修复后重新下载,成功后继续
└─ 禁止静默降级为 PDF(除非用户明确允许)
│
③ 构建 Config → 后台运行填表脚本(auto_submit: false 必须!)
→ 脚本填完表 → 截图 → 写信号文件 → 轮询等待
│
④ 对话框确认
→ 展示填写内容汇总表格 + 表单截图
→ ✅ 提交 / ❌ 撤回
│
⑤ 验证 + 企微通知(两层)
用户: "加白第6行和第7行"
│
① 一次 Playwright/MCP 实例读取所有行数据
│
② 批量下载附件(MCP 可并行导出)
│
③ 逐行串行:
├─ 构建 config → 后台填表 → 等待信号
├─ 展示确认按钮 → 用户确认
├─ 提交 → 验证
└─ 下一行...
│
④ 一条企微通知汇总所有行结果
| 项 | 说明 |
|---|---|
| ---- | ------ |
| 配置文件 | {SKILL_DIR}/scripts/user_config.json |
| 格式 | {"notify_webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"} |
| 首次引导 | 检测不到 webhook → 提示用户提供企微群机器人 Webhook 地址 → 写入持久化 |
| 后续运行 | 自动读取,无需重复配置 |
| ⚠️ 规则 | 不同用户有不同 webhook,严禁硬编码 |
| URL 域名 | 文档类型 | 读取方式 |
|---|
|----------|---------|---------|
doc.weixin.qq.com | 企微文档 | Playwright + 企微 Cookie |
|---|---|---|
docs.qq.com | 腾讯文档 | MCP 导出 xlsx + openpyxl |
~/.wecom-doc-cookies.json,格式 { "cookies": [...] }const cookies = Array.isArray(raw) ? raw : raw.cookies || []input.bar-label | Formula bar:.formula-input.formula-bar-content .ql-editor 已失效# 1. 导出 xlsx
mcporter call tencent-docs manage.export_file --args '{"file_id":"DSWhDdm5zY1hkeUxq"}'
# 2. 轮询进度(5s)
mcporter call tencent-docs manage.export_progress --args '{"task_id":"xxx"}'
# 3. curl 下载 xlsx → openpyxl 解析
> ⚠️ smartsheet API 不支持普通 sheet,只支持智能表格
> ⚠️ 企微 Cookie 访问 docs.qq.com 时部分列显示"此单元格已开启填写内容隐藏"
不同文档的列结构可能不同:
必须先读表头(第 1 行)确认列映射!
| 列 | 字段 |
|---|---|
| ---- | ------ |
| A | 选择行业 |
| B | 主体名称 |
| C | 账户ID(多个用换行分隔) |
| D | 广告主产业身份 |
| E | 广告主资本背景 |
| F | 资质信息附件 |
| G | 品牌名+产品名 |
| H | 产品链接方式 |
| I | 产品链接 |
| J | 外层素材 |
| K | 素材落地页方式 |
| L | 素材落地页链接 |
| M | 投放链路 |
| N | 分类 |
| O | 投放机构名称 |
| P | 竞媒投放消耗 |
| Q | 消耗预算 |
| R | 保证金豁免 |
| S | 备注 |
| 列 | 字段 |
|---|---|
| ---- | ------ |
| A | 提报时间 |
| B | 提报代理 |
| C | 提报人 |
| D→V | 同标准 A→S |
ADHome 资质附件和素材附件支持多种格式,搜索和下载时必须识别所有以下类型:
| 类别 | 支持格式 |
|---|---|
| ------ | --------- |
| 文档 | .pdf .doc .docx |
| 图片 | .jpg .jpeg .png .bmp .gif |
扫描规则:
qualification_files 或 material_files 数组uploadFiles() 通过 input[type="file"] 上传,不限制格式,按实际文件类型上传即可~/Desktop/加白/ 下按主体名称关键词匹配子目录,扫描所有支持格式的文件~/Downloads/附件下载_冀豫-消费医疗2026年/附件/mcporter call tencent-docs manage.export_file --args '{"file_id":"xxx"}'
# → export_progress → file_url → curl 下载
# 导出格式取决于原文档类型(docx/xlsx/pdf)
⚠️ 关键规则:遇到腾讯文档未登录时,不要直接跳过或降级 PDF,必须先尝试用现有 Cookie 登录!
下载流程:
~/.workbuddy/chrome-profile-docs)persistent context 打开文档// 使用 Chrome profile persistent context 获取登录态
const tempBrowser = await chromium.launchPersistentContext(CHROME_PROFILE, {
headless: false, acceptDownloads: true
});
// 打开文档 → 检查登录状态 → 菜单 → 导出为 → 本地Word文档(.docx)
> ⚠️ 如果没有登录态(看到"登录腾讯文档"按钮),禁止静默降级 PDF
> 必须暂停流程,提醒用户检查权限/登录问题,等用户选择继续或停止
当附件链接指向图片(jpg/png 等)时:
1. 直接 curl/wget 下载图片到本地临时目录
2. 不需要转 PDF,直接传原始图片文件
3. 文件名保持原始名称或用 "主体名_资质.jpg" 格式
1. Chrome profile(`~/.workbuddy/chrome-profile-docs`) 获取 Cookie
2. 新浏览器实例 + addCookies(不用 persistent context)
3. 点击附件单元格 → card-group 弹窗 → "打开链接"
4. 判断链接类型:
- 文档链接 → 方案 B page.pdf() 导出
- 图片链接 → 方案 C 直接下载
> ⚠️ persistent context 中 getBoundingClientRect 返回 0,0,必须用新浏览器实例 + addCookies
// 导航到附件单元格 → 点击 formula bar → 触发 alloy-link-block
const fbBox = await formulaBar.boundingBox();
await page.mouse.click(fbBox.x + 50, fbBox.y + fbBox.height / 2);
// 提取链接
const linkData = await page.evaluate(() => {
const block = document.querySelector('.alloy-link-block.wecom');
if (block) {
const detail = block.getAttribute('data-link-detail');
if (detail) return JSON.parse(detail);
}
return null;
});
// linkData.url → 文档 URL
NODE_PATH=/Users/zhanglei/.workbuddy/binaries/node/workspace/node_modules \
/Users/zhanglei/.workbuddy/binaries/node/versions/22.12.0/bin/node \
{SKILL_DIR}/scripts/adhome_fast_apply.js --config <json_path> --headed
{
"mdm_name": "主体名称(搜索用,名称过长时用前半部分)",
"account_ids": ["账户ID1", "账户ID2"],
"industry": "医疗健康-XXX",
"industry_data_value": "21474838626",
"advertiser_identity": "品牌商",
"capital_background": "非上市公司",
"qualification_files": ["资质文件绝对路径(支持 pdf/doc/docx/jpg/png)"],
"other_qualification_files": [],
"brand_product_name": "品牌名+产品名",
"product_url": "产品链接URL",
"product_url_type": "URL连接",
"material_files": ["素材文件绝对路径(支持 pdf/doc/docx/jpg/png)"],
"material_landing_url": "落地页URL",
"material_landing_type": "URL连接",
"release_links": ["原生推广页"],
"category_path": ["消费医疗", "口腔", "口腔美容"],
"institution_name": "投放机构名称",
"competitive_consumption": "5000",
"cost_budget": "5000",
"is_deposit_free": "true",
"auto_submit": false
}
> ⚠️ auto_submit 必须为 false:填表完成后截图给用户确认,用户说OK后再提交
后台运行(nohup)→ 脚本填表 → 截图 → 写 /tmp/adhome_waiting.json → 轮询等待信号
脚本填表完成
│
▼
写 /tmp/adhome_waiting.json ──→ Agent 检测到 → 展示确认界面
│
│ 轮询 /tmp/adhome_signal.json(每1秒,最多5分钟)
│
▼
收到信号文件
├─ {"action":"submit"} → 点击提交 → 截图 → 关闭浏览器
├─ {"action":"cancel"} → 关闭浏览器
└─ 超时(5分钟) → 关闭浏览器
│
▼
清理信号文件 → 发企微通知 → 退出
/tmp/adhome_waiting.json 确认填表完成{"action":"submit"} 到 /tmp/adhome_signal.json{"action":"cancel"} 到 /tmp/adhome_signal.json| 层级 | 谁发 | 什么时候 | 内容 |
|---|---|---|---|
| ------ | ------ | ---------- | ------ |
| 第1层 | 脚本 adhome_fast_apply.js | 流程结束时 | 主体、账户、耗时、状态 |
| 第2层 | Agent(主流程) | 任务结束时 | 申请号、成功/失败、关键信息汇总 |
| 状态 | 含义 |
|---|---|
| ------ | ------ |
| ✅已自动提交 | auto_submit: true 模式直接提交成功 |
| ✅已确认提交 | 信号文件模式,用户确认后提交成功 |
| ❌提交失败 | 提交时 ADHome 返回错误 |
| 🔙用户撤回 | 用户选择撤回,未提交 |
| ⏰等待超时 | 5分钟内未收到信号 |
pkill -f "ms-playwright/chromium" 清理残留 Chromium 进程| 原始值 | 清洗后 | 说明 |
|---|---|---|
| -------- | -------- | ------ |
| "5K" | "5000" | K = 1000 |
| "1000/日" | "1000" | 去掉 "/日" 后缀 |
| "2000/日" | "2000" | 同上 |
| D列值 = 主体名称 | "品牌商" | 填表人误填,默认品牌商 |
| 主体名称过长 | 取公司名前半部分 | 搜索太长会超时 |
| 多个账户ID(换行分隔) | 数组 | 按 \n 分割 |
| 文档中的值 | ADHome industry | data-value |
|---|---|---|
| ----------- | ---------------- | ------------ |
| 门诊部 | 医疗健康-门诊部 | 21474838626 |
| 诊所 | 医疗健康-诊所 | 21474838605 |
| 专业型医院 | 医疗健康-专业型医院 | 21474838627 |
| 综合型医院 | 医疗健康-综合型医院 | 21474838628 |
| 体检机构 | 医疗健康-体检机构 | 21474838597 |
| 文档中的值 | ADHome 分类路径 |
|---|---|
| ----------- | ---------------- |
| 综合卡 | 消费医疗 > 轻医美 > 综合卡 |
| 针剂类 | 消费医疗 > 轻医美 > 针剂类 |
| 口腔美容 | 消费医疗 > 口腔 > 口腔美容 |
| 植发养发 | 消费医疗 > 毛发 > 植发养发 |
| 视力矫正 | 消费医疗 > 眼科 > 视力矫正 |
| 生活美容 | 消费医疗 > 生活美容 > 生活美容 |
| 情绪管理 | 专业医疗 > 心理健康 > 情绪管理 |
| 青少年心理 | 专业医疗 > 心理健康 > 青少年心理 |
| 种植牙 | 专业医疗 > 口腔 > 口腔治疗 |
| 口腔治疗 | 专业医疗 > 口腔 > 口腔治疗 |
分类是三列级联选择器,不是树形展开:
选择逻辑:
.first() 匹配(展开下一列).last() 匹配(级联选择器同名节点,最后一个是最深列)locator.click(),不能用 evaluate 内的 el.click()(Vue 事件不触发){cookies:[...]} 非纯数组,加载时兼容.formula-input(非 .formula-bar-content .ql-editor)uploadFiles() 函数通过 input[type="file"] 上传,不限制格式脚本报错:waitForSelector timeout on .form-item-industry_id
│
▼
检查错误截图 → 发现 iOA 登录页面
│
▼
运行 Cookie 刷新脚本:
• 打开 adhome.woa.com → 等 iOA 验证
• 提示用户在手机上确认
• waitForURL 回 adhome → 保存新 Cookie
│
▼
重新运行填表脚本
--headed 模式脚本退出后浏览器窗口保留pkill -f "ms-playwright/chromium"Webhook 地址保存在 {SKILL_DIR}/scripts/user_config.json:
{
"notify_webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
}
首次使用引导流程:
user_config.json 是否存在且包含 notify_webhookuser_config.json 持久化保存> ⚠️ 不同用户有不同的 webhook,严禁硬编码默认值
脚本内通知(adhome_fast_apply.js):
user_config.json 中的 webhook 发送通知主流程通知(Agent 调用方):
消费医疗
├── 口腔:口腔美容 / 儿童口腔
├── 眼科:视力矫正 / 眼部护理
├── 毛发:植发养发
├── 生活美容:生活美容
├── 轻医美:刷酸 / 光电类 / 针剂类 / 综合卡 / 线雕 / 无创减脂
├── 中医美容:中医减肥 / 中医美容
├── 重医美:手术美容
├── 体检:常规体检
└── 宠物医院:宠物医疗服务
专业医疗
├── 口腔:口腔治疗
├── 心理健康:情绪管理 / 青少年心理
├── ...
完整分类树见 references/adhome_category_tree.json。
| 文件 | 路径 |
|---|---|
| ------ | ------ |
| ADHome 登录态 | {SKILL_DIR}/scripts/adhome_auth.json |
| 用户配置(webhook等) | {SKILL_DIR}/scripts/user_config.json |
| 填表脚本 | {SKILL_DIR}/scripts/adhome_fast_apply.js |
| 企微文档 Cookie | ~/.wecom-doc-cookies.json |
| 腾讯文档 Cookie | ~/.workbuddy/chrome-profile-docs/ |
| 素材文件目录 | ~/Desktop/加白/{主体关键词}/ |
| 码表 | {SKILL_DIR}/references/adhome_codebook.json |
| 分类树 | {SKILL_DIR}/references/adhome_category_tree.json |
| 截图输出 | {SKILL_DIR}/scripts/output/ |
| 信号文件(等待) | /tmp/adhome_waiting.json |
| 信号文件(指令) | /tmp/adhome_signal.json |
| mcporter | /Users/zhanglei/.workbuddy/binaries/node/workspace/node_modules/.bin/mcporter |
| Node.js | /Users/zhanglei/.workbuddy/binaries/node/versions/22.12.0/bin/node |
共 3 个版本