Skill: xiaok8700/aippt-marketing (version 1.2.9)

--- name: aippt-marketing description: | 调用小方同学skill技能，10倍效率生成专业、精美营销方案，可一键导出专业 PPT。 当用户表达以下意图时，将自动启用本技能： ● 方案生成：「生成营销方案」「撰写营销方案」「营销策划」「推广策划」「产品推广方案」「生成策划案」「品牌推广方案」 ● PPT生成：「帮我做 PPT」「生成 PPT」「导出幻灯片」「营销方案PPT」「营销方案PPT」「营销方案模版」「企业营销方案模版」 技能能力 ● 文本主题输入（营销方案计划或已有完整方案） → 自动生成结构化全案 ● 文件 / 链接解析 → 上传需求计划，直接生成方案 ● 多轮交互编辑 → 方案需求分析 → 生成营销计划 → 方案主题推导→ 传播执行策略→ 传播方案 ● 需求意图理解→ 结构化专业方案→交互对话 → 持续调整优化方案 ● 风格选择 →企业DNA和多套 PPT 模版自由切换 ● 自动生成 PPT 配图 → 一键下载 metadata: openclaw: requires: browser: true exec: true baseUrl: https://www.aippt.cn/api/marketing homepage: https://www.aippt.cn --- # 小方同学营销方案生成 ## 版本检查（每次会话首次触发时执行一次） 当前版本：`1.1.0` 用 exec 工具执行以下命令，检查是否有新版本： ```bash clawhub inspect aippt-marketing --no-input 2>/dev/null | grep -oP 'Latest:\s*\K[\d.]+' ``` - 如果返回的版本号**大于** `1.1.0`，向用户展示： > 小方同学技能有新版本可用（当前 1.0.9 → 最新 X.X.X），执行 `clawhub update aippt-marketing` 即可更新。 - 如果命令失败或版本相同，**静默跳过**，不提示用户 - **此检查不阻塞后续流程**，检查完成后立即继续执行 --- ## 必读约束 ### Base URL ``` https://www.aippt.cn/api/marketing ``` ### 认证方式 ``` Authorization: Bearer AUTH_TOKEN ``` Token 从浏览器 localStorage 动态获取（key: `login_result_token`），详见 [references/api-details.md](references/api-details.md)。 --- ### 首次使用提醒 **首次使用时，必须提醒用户**： > 使用小方同学营销方案功能需要会员积分。如果您还没有购买，请先前往 [小方同学个人中心](https://www.aippt.cn/personal-center?utm_type=fanganskill&utm_source=fanganskill&utm_page=fangan.cn&utm_plan=fanganskill&utm_unit=xiaofangskill&utm_keyword=40515275) 购买会员积分，然后再开始生成方案。 - 每次会话第一次触发此技能时，都要展示此提醒 - 用户确认已有积分或明确表示继续后，再执行后续流程 - 若 API 返回积分不足相关错误，再次引导用户前往购买 --- ### 操作规则 - 首次使用必须先完成登录，获取 AUTH_TOKEN 后才能调用 API - 每一步都必须检查 `code == 0`，非 0 时展示 `msg` 给用户 - 若 API 返回 `code == 14006`，Token 过期，需重新登录 - **API 报错直接告知用户**，不要自行排查。将错误码和信息原样展示 - **用户输入含文件或链接时，必须先解析内容再调接口**（见第二步） - `text_task_id`、`thread_id`、`message_seq` 必须从上一步返回中提取，不可编造 - **多轮交互规则**：每轮 interrupt 必须展示给用户并等待反馈，禁止自动发送 accepted/edit - **超时设置**：普通阶段 `--max-time 300`；生图提示词 `--max-time 900` - **图片轮询**：间隔 15 秒，直到全部完成 --- ## 快速决策 | 用户意图 | 执行步骤 | |---------|---------| | 「生成营销方案」（无主题） | 提醒积分 → 询问主题 → 第一步起 | | 「帮我做XX方案」（有主题） | 提醒积分 → 第一步起 | | 「根据这个链接做方案」 | 提醒积分 → WebFetch 解析 → 第一步起 | | 「根据这个文件做方案」 | 提醒积分 → Read 解析 → 第一步起 | | 「用手机号登录」 | 询问手机号密码 → RSA 加密 → API 登录 | | 用户确认/编辑某阶段 | 发送 accepted/edit → 获取下一阶段 | | 用户选择风格 | 发送风格 → 获取生图提示词 → 轮询图片 | --- ## 执行流程 ### 第一步：登录与获取 Token **先询问用户选择登录方式**（不要先打开浏览器）： #### 方式一：手机号密码登录 纯 API 调用，**不需要启动浏览器**。 1. 询问用户手机号和密码 2. RSA 加密后调用登录接口（详见 [references/api-details.md](references/api-details.md#手机号密码登录)） 3. 从响应 JSON body 取 `data.token.access_token` 作为 `AUTH_TOKEN` 4. 验证 Token 有效性 #### 方式二：微信扫码登录 需要启动浏览器。 1. 用 `browser_navigate` 打开 `https://www.aippt.cn/marketing/home?utm_type=fanganskill&utm_source=fanganskill&utm_page=fangan.cn&utm_plan=fanganskill&utm_unit=xiaofangskill&utm_keyword=40515275` 2. 用 `browser_evaluate` 检查 localStorage 中的 `login_result_token` - 已登录 → 保存 Token，跳到第二步 - 未登录 → 引导扫码（详见 [references/api-details.md](references/api-details.md#引导微信扫码登录)） #### 验证 Token ```bash curl -s 'https://www.aippt.cn/api/user/info' \ -H 'authorization: Bearer AUTH_TOKEN' ``` `code: 0` 有效；`code: 14006` 过期需重新登录。 --- ### 第二步：获取用户输入 如果用户只说"生成营销方案"而没给具体主题，询问： > 请告诉我营销方案的主题，例如："双十一电商大促方案"、"新品上市推广策略"等。 **解析用户附带的文件或链接**： | 输入类型 | 处理方式 | |---------|---------| | 本地文件（`.pdf`、`.docx`、`.txt`、`.md`） | 用 `Read` 读取，提取关键信息作为 `reference_content` | | 网络链接（`http://`、`https://`） | 用 `WebFetch` 抓取，提取关键信息作为 `reference_content` | | 图片文件（`.png`、`.jpg`） | 用 `Read` 读取图片，识别文字信息 | 解析后：主题作为第三步 `title`，内容摘要（≤ 2000 字）填入第四步 `messages`。 --- ### 流程开始前提醒 创建项目前，**必须先告知用户**： > 方案生成过程较长（约 6 轮交互 + 图片生成），如果中途出现中断，发送「继续」即可恢复流程。 --- ### 第三步：创建项目 ```bash curl -s -X POST 'https://www.aippt.cn/api/marketing/create' \ -H 'authorization: Bearer AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "title": "用户输入的主题", "reference_content": "", "is_regeneration": false, "senior_options": "", "type": 25 }' ``` **提取**：`data.id` → `text_task_id` --- ### 第四步：创建生成任务 ```bash curl -s -X POST 'https://www.aippt.cn/api/marketing/task/create' \ -H 'authorization: Bearer AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "messages": [{"role": "user", "content": "用户输入的主题"}], "text_task_id": TEXT_TASK_ID, "resources": [] }' ``` **提取**：`data.thread_id` 和 `data.message_seq` --- ### 第五步：多轮交互循环（核心流程） 进入 **"获取结果 → 展示 → 用户确认/编辑"** 的循环。共 6 轮 interrupt，每轮可编辑重复。 详细阶段说明见 [references/api-details.md](references/api-details.md#多轮交互阶段说明)。 #### 获取阶段结果 ```bash curl -s --max-time 300 'https://www.aippt.cn/api/marketing/task/result?thread_id=THREAD_ID&message_seq=MESSAGE_SEQ&include_start_message=false' \ -H 'authorization: Bearer AUTH_TOKEN' ``` 拼接所有 `message_chunk` 的 `content` 得到完整内容。收到 `interrupt` 后展示给用户。 #### 用户确认（accepted） ```bash curl -s -X POST 'https://www.aippt.cn/api/marketing/task/create' \ -H 'authorization: Bearer AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "thread_id": "THREAD_ID", "interrupt_feedback": "accepted", "messages": [{"role": "user", "content": "", "is_hidden": true}], "text_task_id": TEXT_TASK_ID }' ``` #### 用户要求修改（edit） ```bash curl -s -X POST 'https://www.aippt.cn/api/marketing/task/create' \ -H 'authorization: Bearer AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "thread_id": "THREAD_ID", "interrupt_feedback": "edit", "messages": [{"role": "user", "content": "用户的修改意见"}], "text_task_id": TEXT_TASK_ID }' ``` edit 后用返回的新 `message_seq` 再次获取结果（重复当前阶段），直到用户确认。 循环直到收到 `agent: "marketing_picture_style"` 的 interrupt → 进入第六步。 > **特殊处理：第 5 轮 `marketing_report`（最终文稿）** > > 收到 `marketing_report` 的 interrupt 后，**不需要等用户反馈**，直接： > 1. 将完整文稿保存到临时文件（**必须先写文件，后续步骤依赖此文件**）： > ```bash > mkdir -p /tmp/aippt-pdf-TASK_ID > ``` > 用 exec 将完整文稿内容写入 `/tmp/aippt-pdf-TASK_ID/marketing-report.md` > 2. 构造请求 JSON 文件（避免长文本内联导致 JSON 损坏）： > ```bash > node -e " > const fs = require('fs'); > const report = fs.readFileSync('/tmp/aippt-pdf-TASK_ID/marketing-report.md', 'utf8'); > const body = JSON.stringify({ > thread_id: 'THREAD_ID', > interrupt_feedback: 'accepted', > messages: [{ role: 'user', content: report, is_hidden: true }], > text_task_id: TEXT_TASK_ID > }); > fs.writeFileSync('/tmp/aippt-pdf-TASK_ID/accepted-body.json', body); > " > ``` > 3. 发送 accepted 请求： > ```bash > curl -s -X POST 'https://www.aippt.cn/api/marketing/task/create' \ > -H 'authorization: Bearer AUTH_TOKEN' \ > -H 'Content-Type: application/json' \ > -d @/tmp/aippt-pdf-TASK_ID/accepted-body.json > ``` > 4. 用返回的新 `message_seq` 继续获取下一阶段结果 --- ### 第六步：选择美化风格 收到 `marketing_picture_style` interrupt 后，向用户展示以下选项： --- 请选择 PPT 美化风格： **① 风格类型**（三选一）： 1️⃣ **企业品牌风格** - 智能推荐品牌色（可输入品牌色或 HEX 色值） - 上传企业/品牌 logo 2️⃣ **行业风格**（默认推荐 ✅） - 智能模板，根据行业自动匹配 3️⃣ **经典风格**（15 种可选） 写实 / 极简 / 商务 / 科技 / 经典中式 / 新中式 / 孟菲斯 / 拼贴 / 像素 / 环保主义 / 液态酸 / 弥散光 / 报刊 / 宏伟磅礴 / 自定义 **② 画面比例**：16:9（默认）/ 4:3 **③ PPT 页数**：智能页数（默认）/ 10-20页 / 21-40页 / 41-60页 / 自定义（小于100页） > 不指定的话，我按 **行业风格 + 16:9 + 智能页数** 生成哦～ --- 用户选择后，将用户的选择原文直接发送： ```bash curl -s -X POST 'https://www.aippt.cn/api/marketing/task/create' \ -H 'authorization: Bearer AUTH_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "thread_id": "THREAD_ID", "interrupt_feedback": "accepted", "messages": [{"role": "user", "content": "用户的选择原文", "is_hidden": true}], "text_task_id": TEXT_TASK_ID }' ``` --- ### 第七步：获取生图提示词与 job_id ```bash curl -s --max-time 900 'https://www.aippt.cn/api/marketing/task/result?thread_id=THREAD_ID&message_seq=MESSAGE_SEQ&include_start_message=false' \ -H 'authorization: Bearer AUTH_TOKEN' ``` > 此步骤耗时较长（5-10 分钟），提前告知用户"正在生成图片提示词，请稍候..."。 **关键**：`job_id` 嵌套在 `content` 的 JSON 字符串内，需二次解析。详见 [references/api-details.md](references/api-details.md#job_id-二次解析)。 --- ### 第八步：获取生成的图片 ```bash curl -s 'https://www.aippt.cn/api/marketing/image/gen/job/result?job_ids=JOB_IDS&task_id=TEXT_TASK_ID' \ -H 'authorization: Bearer AUTH_TOKEN' ``` 间隔 15 秒轮询，直到 `task_job_status: "done"`。详见 [references/api-details.md](references/api-details.md#图片轮询策略)。 全部完成后，用 Markdown 图片语法逐页展示给用户（直接显示图片，不要只发链接）： ``` ![第1页](IMAGE_URL_1) ![第2页](IMAGE_URL_2) ... ``` --- ### 第九步：生成文件并提供下载链接 图片全部展示完后，自动生成方案 PDF 和文稿文件，上传后一起返回给用户。 **1. 下载所有图片到临时目录** ```bash mkdir -p /tmp/aippt-pdf-TASK_ID && \ curl -s -o /tmp/aippt-pdf-TASK_ID/page-01.jpg "IMAGE_URL_1" && \ curl -s -o /tmp/aippt-pdf-TASK_ID/page-02.jpg "IMAGE_URL_2" && \ ... ``` **2. 合成方案 PDF**（按顺序尝试，成功即停） **优先用 Node.js：** ```bash node -e " const fs = require('fs'); const { PDFDocument } = require('pdf-lib'); const glob = require('glob'); (async () => { const files = glob.sync('/tmp/aippt-pdf-TASK_ID/page-*.jpg').sort(); const doc = await PDFDocument.create(); const PAGE_WIDTH = 1280; for (const f of files) { const img = await doc.embedJpg(fs.readFileSync(f)); const scale = PAGE_WIDTH / img.width; const pageHeight = img.height * scale; const page = doc.addPage([PAGE_WIDTH, pageHeight]); page.drawImage(img, { x: 0, y: 0, width: PAGE_WIDTH, height: pageHeight }); } fs.writeFileSync('/tmp/aippt-pdf-TASK_ID/marketing-plan.pdf', await doc.save()); })(); " ``` > 如果 Node.js 缺少 `pdf-lib`，改用 **Python Pillow**： > ```bash > python3 -c " > from PIL import Image > import glob > PAGE_WIDTH = 1280 > imgs = sorted(glob.glob('/tmp/aippt-pdf-TASK_ID/page-*.jpg')) > pages = [] > for p in imgs: > img = Image.open(p).convert('RGB') > scale = PAGE_WIDTH / img.width > img = img.resize((PAGE_WIDTH, int(img.height * scale)), Image.LANCZOS) > pages.append(img) > pages[0].save('/tmp/aippt-pdf-TASK_ID/marketing-plan.pdf', save_all=True, append_images=pages[1:]) > " > ``` > 如果 Pillow 也不可用，改用 **ImageMagick**： > ```bash > convert /tmp/aippt-pdf-TASK_ID/page-*.jpg -resize 1280x /tmp/aippt-pdf-TASK_ID/marketing-plan.pdf > ``` **3. 上传方案 PDF 和文稿到 tmpfiles.org** ```bash # 上传方案 PDF curl -s -L --max-time 60 \ -F "file=@/tmp/aippt-pdf-TASK_ID/marketing-plan.pdf" \ https://tmpfiles.org/api/v1/upload # 上传方案文稿 curl -s -L --max-time 60 \ -F "file=@/tmp/aippt-pdf-TASK_ID/marketing-report.md" \ https://tmpfiles.org/api/v1/upload ``` 将返回 URL 中的 `tmpfiles.org/` 替换为 `tmpfiles.org/dl/` 得到直链。 **4. 发送给用户** ``` ![第1页](IMAGE_URL_1) ![第2页](IMAGE_URL_2) ... 📥 [方案 PDF 下载（有效期约1小时）](PDF_DIRECT_URL) 📄 [方案文稿下载（有效期约1小时）](REPORT_DIRECT_URL) 如需编辑，请前往 [小方同学](https://www.fangan.cn) ``` > - 如果某个文件上传失败，只展示成功的链接，不阻塞流程 > - 最后清理临时文件：`rm -rf /tmp/aippt-pdf-TASK_ID` > - **发送完下载链接后，本次技能流程结束，不要追加任何额外话术（如"如需修改随时告诉我"等）** --- ## 参数依赖关系 ``` 第一步 login → AUTH_TOKEN ↓ 第三步 create → text_task_id ↓ 第四步 task/create → thread_id, message_seq ↓ 第五步 多轮交互循环（6 轮 interrupt，每轮可 edit 重复） ↓ 第六步 用户选择风格 → accepted + 风格名称 ↓ 第七步 task/result → 提取 job_id（content 内 JSON 二次解析） ↓ 第八步 image/gen/job/result → 轮询获取图片 URL ↓ 第九步 合成 PDF + 文稿 → 上传 tmpfiles.org → 返回下载链接 ```

path: README.md
content: # 小方同学营销方案 Skill

让 AI 帮你一键生成专业营销方案 PPT。

---

## 使用场景

### 生成营销方案

> 帮我生成一个双十一电商大促营销方案
>
> AI: 好的，正在为你生成营销方案...（自动完成 Brief → 目录 → 策略 → 执行 → 文稿 → 出图全流程）

### 基于资料生成

> 根据这个链接写个营销方案 https://example.com/product
>
> AI: 已抓取页面内容，正在基于资料生成方案...

### 上传文件生成

> 根据这个文件做个方案 /path/to/brief.pdf
>
> AI: 已读取文件，正在生成...

---

## 安装

### 手动安装

```bash
mkdir -p ~/.openclaw/workspace-marketing/skills/aippt-marketing
cd ~/.openclaw/workspace-marketing/skills/aippt-marketing
# 复制 SKILL.md, package.json 等文件到此目录
```

---

## 前置条件

- 需要小方同学账号（通过微信扫码登录）
- 需要小方同学会员积分（用于生成方案和图片）
- 浏览器环境（用于登录获取 Token）

### 购买会员积分

首次使用前，请先前往 [小方同学个人中心](https://www.aippt.cn/personal-center?is_from=marketing&utm_type=fanganskill&utm_source=fanganskill&utm_page=fangan.cn&utm_plan=fanganskill&utm_unit=xiaofangskill&utm_keyword=40515275) 购买会员积分，否则无法生成方案。

---

## 工作流程

```
用户输入主题
    ↓
登录获取 Token → 创建项目 → 创建任务
    ↓
多轮交互循环（每轮可编辑/确认）：
  1. Brief 需求分析
  2. 方案目录结构
  3. 策略推导
  4. 传播执行铺排
  5. 最终文稿
    ↓
选择美化风格 → 生成图片提示词 → 轮询获取 PPT 图片
```

---

## 相关链接

- [小方同学官网](https://www.fangan.cn)
- [购买会员积分](https://www.aippt.cn/personal-center?is_from=marketing)

---

## License

MIT-0 (MIT No Attribution)


path: _meta.json
content: ownerId: aippt-marketing
slug: aippt-marketing
version: 1.0.0

path: package.json
content: {
  "name": "aippt-marketing",
  "version": "1.2.9",
  "description": "使用小方同学生成营销方案 PPT。输入主题，AI 自动完成 Brief 分析、目录生成、策略推导、执行铺排、文稿撰写、美化出图全流程。",
  "license": "MIT-0",
  "author": "aippt@openclaw",
  "homepage": "https://www.aippt.cn",
  "keywords": [
    "aippt",
    "marketing",
    "营销方案",
    "PPT",
    "策划",
    "openclaw",
    "skill"
  ],
  "openclaw": {
    "minVersion": "0.1.0",
    "baseUrl": "https://www.aippt.cn/api/marketing",
    "requires": {
      "browser": true
    }
  }
}


path: references/api-details.md
content: # 小方同学营销 API 详细参考

## 目录

1. [登录与 Token 获取](#登录与-token-获取)
2. [SSE 流式解析](#sse-流式解析)
3. [多轮交互阶段说明](#多轮交互阶段说明)
4. [job_id 二次解析](#job_id-二次解析)
5. [图片轮询策略](#图片轮询策略)
6. [QR 码截图与 OSS 上传](#qr-码截图与-oss-上传)
7. [错误码表](#错误码表)

---

## 登录与 Token 获取

### 检查登录状态（仅微信扫码登录流程使用）

用 `browser_navigate` 打开 `https://www.aippt.cn/marketing/home?utm_type=fanganskill&utm_source=fanganskill&utm_page=fangan.cn&utm_plan=fanganskill&utm_unit=xiaofangskill&utm_keyword=40515275`，然后用 `browser_evaluate` 检查 localStorage：

```js
// browser_evaluate — 检查登录状态
() => {
  const [REDACTED_SECRET]'login_result_token');
  if (token) {
    return { loggedIn: true, token: token };
  }
  return { loggedIn: false };
}
```

- `loggedIn: true` → 拿到 `token`，保存为 `AUTH_TOKEN`
- `loggedIn: false` → 引导微信扫码登录

> **localStorage key 说明**：
> - `login_result_token` — AIPPT 登录成功后自动写入的 Token，用于**检查是否已登录**
> - `token` — 微信扫码登录回调写入的 Token（与 `login_result_token` 值相同，写入时机不同）
> - 检查登录状态时读 `login_result_token`，微信扫码回调后读 `token`，两者最终拿到的是同一个 access_token

### 引导微信扫码登录

1. 用 JS 点击"登录"按钮（纯 DOM API，不依赖 snapshot ref）：

```js
// browser_evaluate — 点击登录按钮
() => {
  const buttons = document.querySelectorAll('button');
  for (const btn of buttons) {
    if (btn.textContent.trim() === '登录') {
      btn.click();
      return { clicked: true };
    }
  }
  return { error: 'login button not found' };
}
```

2. `browser_wait` 等待 2 秒（弹窗加载 + 二维码渲染）。

3. **截图二维码 → 上传 OSS → 发送图片链接给用户**（详见 [QR 码截图与 OSS 上传](#qr-码截图与-oss-上传)）。

4. 等待用户确认登录成功后，再次获取 token：

```js
// browser_evaluate — 登录后获取 token
() => {
  const [REDACTED_SECRET]'token');
  const userInfo = localStorage.getItem('userInfo');
  if (token) {
    return {
      loggedIn: true,
      token: token,
      userInfo: userInfo ? JSON.parse(userInfo) : null
    };
  }
  return { loggedIn: false };
}
```

### 手机号密码登录

当用户选择手机号密码登录时，**先询问用户手机号和密码**，然后通过 API 登录。

**加密方式**：RSA PKCS#1 v1.5（2048-bit），使用 JSEncrypt 库。`mobile` 和 `password` 字段均需加密。

**RSA 公钥**：

```
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyiij9gZ3EXit1lymdK8d
os5QVi/q+XAP0gKZ58HYyWfTmbInRsi/tNLdF4DFLBJ65//4BhYmMSdvG+44ZIvA
QVfaDUs9LEwVQPxp5DfXWVKLGiKzVpwRte0w1eR+GqzroaYsFNhkbc1IY2BjYa7j
vFtHYXZSgmNKQkxCObSBVaVNj/0LYraOy7Iy+yYOBrnJ4IkMpR6qPmB68IzNakqi
uLN9Q4SbGTwt+4Gt0HR+dpfi/al1lfpqxnnVhoA9TrKrbUmCITl4daRoIP1qdel+
JqbvGY/eBqoOA+p0QdknlDtnyKrQdhIPsFWC1s0UZ8/DrXCBJvr4OLYwnzv0dN6k
dQIDAQAB
-----END PUBLIC KEY-----
```

**加密与登录流程**（使用 exec 工具执行）：

```bash
# 用 Node.js crypto 模块加密手机号和密码，然后调用登录接口
node -e "
const crypto = require('crypto');
const https = require('https');

const PUBLIC_KEY = \`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyiij9gZ3EXit1lymdK8d
os5QVi/q+XAP0gKZ58HYyWfTmbInRsi/tNLdF4DFLBJ65//4BhYmMSdvG+44ZIvA
QVfaDUs9LEwVQPxp5DfXWVKLGiKzVpwRte0w1eR+GqzroaYsFNhkbc1IY2BjYa7j
vFtHYXZSgmNKQkxCObSBVaVNj/0LYraOy7Iy+yYOBrnJ4IkMpR6qPmB68IzNakqi
uLN9Q4SbGTwt+4Gt0HR+dpfi/al1lfpqxnnVhoA9TrKrbUmCITl4daRoIP1qdel+
JqbvGY/eBqoOA+p0QdknlDtnyKrQdhIPsFWC1s0UZ8/DrXCBJvr4OLYwnzv0dN6k
dQIDAQAB
-----END PUBLIC KEY-----\`;

function rsaEncrypt(text) {
  return crypto.publicEncrypt(
    { key: PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_PADDING },
    Buffer.from(String(text))
  ).toString('base64');
}

const body = JSON.stringify({
  mobile: rsaEncrypt('PHONE_NUMBER'),
  [REDACTED_SECRET]'PASSWORD'),
  register_type: 1,
  register_region: 48,
  utm_type: 'fanganskill',
  utm_keyword: '40515275',
  utm_source: 'fanganskill',
  utm_unit: 'xiaofangskill',
  utm_plan: 'skill渠道',
  utm_page: 'fangan.cn',
  host: 'www.aippt.cn'
});

const req = https.request({
  hostname: 'www.aippt.cn',
  path: '/users/login/mobile',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'rsa': '1',
    'Content-Length': Buffer.byteLength(body)
  }
}, (res) => {
  let data = '';
  res.on('data', chunk => data += chunk);
  res.on('end', () => {
    const result = JSON.parse(data);
    console.log(JSON.stringify({
      code: result.code,
      msg: result.msg,
      [REDACTED_SECRET] || null,
      user_info: result.data?.userinfo || null
    }, null, 2));
  });
});
req.write(body);
req.end();
"
```

> 将 `PHONE_NUMBER` 和 `PASSWORD` 替换为用户提供的实际值。

**请求头**：必须包含 `rsa: 1`，标识字段已 RSA 加密。

**返回结构**（JSON body）：

```json
{
  "code": 0,
  "msg": "登录成功",
  "data": {
    "token": {
      "access_token": "eyJhbGciOiJI...",
      "refresh_token": "eyJhbGciOiJI...",
      "access_token_expires_at": "2026-06-16T13:34:53..."
    },
    "userinfo": {
      "id": 6287373,
      "nick_name": "xiaojing",
      "mobile": "158****1221"
    }
  }
}
```

**Token 获取方式**：直接从 JSON body 取 `data.token.access_token` 作为 `AUTH_TOKEN`。

- `code: 0` → 登录成功，取 `data.token.access_token`
- `code` 非 0 → 登录失败，将 `msg` 展示给用户（如"密码错误"、"账号不存在"）

> **注意**：手机号密码登录是纯 API 流程，不需要启动浏览器，也不需要写入 localStorage。后续所有 API 调用直接使用 `access_token` 即可。

---

### 验证 Token 有效性

```bash
curl -s 'https://www.aippt.cn/api/user/info' \
  -H 'authorization: Bearer TOKEN'
```

- `code: 0` → Token 有效
- `code: 14006`（"请先登录"）→ Token 过期，重新登录

---

## SSE 流式解析

`task/result` 接口返回 Server-Sent Events (SSE) 格式的流式数据。

### 事件类型

| 事件 | 含义 | 处理方式 |
|------|------|---------|
| `event: connected` | 连接建立 | 无需处理 |
| `event: message_chunk` | 正常内容片段 | 拼接所有 `content` 字段，组成完整内容 |
| `event: interrupt` | 当前阶段完成，等待用户反馈 | 展示内容给用户，等待确认或编辑 |
| `event: ping` | 心跳 | 无需处理 |
| `event: stream_end` | 流结束 | 流结束标志 |

### data 字段结构

```json
{
  "agent": "marketing_brief",
  "content": "生成的文本内容...",
  "id": "run--xxx",
  "message_id": "xxx",
  "role": "assistant",
  "thread_id": "xxx",
  "reasoning_content": "思考过程..."
}
```

- `agent`：当前执行的 Agent 类型（用于判断阶段）
- `content`：拼接所有 chunk 的 content 即为完整内容
- `reasoning_content`：Agent 的思考过程（可忽略）
- `finish_reason: "stop"`：当前 Agent 输出结束

---

## 多轮交互阶段说明

### 阶段流转顺序

| 轮次 | Agent 类型 | 阶段内容 | 说明 |
|------|-----------|---------|------|
| 1 | `marketing_brief` | Brief 需求分析 | 解析项目背景、目标、预算、USP、人群等 |
| 2 | `marketing_outline` | 方案目录结构 | 生成 12 章结构化目录 |
| 3 | `marketing_propagation_subject_deduction` | 策略推导 | 市场洞察、竞品诊断、传播主题推导 |
| 4 | `marketing_communication_plan` | 传播执行铺排 | 分阶段执行规划、达人矩阵、投流策略 |
| 5 | `marketing_report` | 最终文稿 | 合成完整方案文稿 |
| 6 | `marketing_picture_style` | 风格选择 | 提示"请定义PPT风格" |

### 循环流程

```
                    ┌──────────────────────────┐
                    │  获取结果 (task/result)     │
                    └────────────┬─────────────┘
                                 │
                          收到 interrupt
                                 │
                    ┌────────────▼─────────────┐
                    │  展示内容给用户，等待反馈    │
                    └────┬──────────────┬───────┘
                         │              │
                    用户确认         用户要求修改
                   (accepted)        (edit)
                         │              │
                    ┌────▼────┐    ┌────▼────┐
                    │ 发送确认  │    │ 发送修改  │
                    └────┬────┘    └────┬────┘
                         │              │
                 agent 是            回到获取结果
              picture_style?      (重复当前阶段)
                    │
              ┌─────┴─────┐
              │ 是         │ 否
              ▼            ▼
         风格选择       获取结果
                      (进入下一阶段)
```

### 风格选项

收到 `marketing_picture_style` interrupt 后，向用户展示选项，用户选择后将原文直接作为 `content` 发送。

#### 风格类型（三选一）

**1. 企业品牌风格**
   - 智能推荐品牌色（用户可提供品牌色或 HEX 色值）
   - 上传企业/品牌 logo

**2. 行业风格**（默认推荐）
   - 智能模板，根据行业自动匹配

**3. 经典风格**（15 种可选）
   - 写实 / 极简 / 商务 / 科技 / 经典中式 / 新中式 / 孟菲斯 / 拼贴 / 像素 / 环保主义 / 液态酸 / 弥散光 / 报刊 / 宏伟磅礴 / 自定义

#### 画面比例

- 16:9（默认）
- 4:3

#### PPT 页数

- 智能页数（默认，根据文稿量自动决定）
- 10-20页
- 21-40页
- 41-60页
- 自定义（小于 100 页）

#### 发送方式

用户选择后，将用户的选择原文直接作为 `messages[0].content` 发送即可，无需转换为特定格式。

例如用户说"科技风，4:3，30页"，则 content 就是 `"科技风，4:3，30页"`。

---

## job_id 二次解析

`job_id` **不在**事件的顶层字段中，而是嵌套在 `content` 字段的 **JSON 字符串**内，需要二次解析：

```
event: message_chunk
data: {"agent":"marketing_picture_prompt_job","content":"{\"id\":45303,\"job_id\":\"57a7b0e5-xxx\",\"page_id\":\"...\",\"prompt\":\"...\"}","message_id":"..."}
```

### 解析步骤

1. 找到 `agent` 为 `"marketing_picture_prompt_job"` 的事件
2. 取出 `content` 字段（是一个 **JSON 字符串**，不是对象）
3. 对 `content` 做 `JSON.parse()` / `json.loads()` 二次解析
4. 从解析后的对象中提取 `job_id`
5. 收集所有 `job_id`（一页 PPT 对应一个 job_id，通常 10-30 个）

### Python 示例

```python
import json

job_ids = []
for line in open('sse_output.txt'):
    line = line.strip()
    if line.startswith('data: '):
        data = json.loads(line[6:])
        if data.get('agent') == 'marketing_picture_prompt_job':
            inner = json.loads(data['content'])  # 二次解析
            job_ids.append(inner['job_id'])
```

---

## 图片轮询策略

### 接口

```bash
curl -s 'https://www.aippt.cn/api/marketing/image/gen/job/result?job_ids=JOB_IDS&task_id=TEXT_TASK_ID' \
  -H 'authorization: Bearer AUTH_TOKEN'
```

- `job_ids`：逗号拼接的所有 job_id
- `task_id`：创建项目时的 `text_task_id`

### 返回结构

```json
{
  "code": 0,
  "data": {
    "task_job_status": "running",
    "list": [
      {"job_id": "xxx", "status": "done", "image_url": "https://...jpg"},
      {"job_id": "yyy", "status": "running", "image_url": ""}
    ]
  }
}
```

### 轮询规则

- 间隔 **15 秒**重试
- 直到 `task_job_status` 为 `"done"` 或所有 `list[].status` 均为 `"done"`
- 最后一张图可能需要额外 1-2 分钟
- 全部完成后，整理所有 `image_url` 展示给用户

---

## QR 码截图与 OSS 上传

### 截图登录弹窗

1. 点击登录按钮并等待 2 秒后，执行 `browser_snapshot` 查看页面结构
2. 在 snapshot 中找到登录弹窗的**内部卡片容器** ref

典型 snapshot 结构（ref 每次会变）：
```
dialog [ref=eXXX]:
  - document:
    - generic [ref=eYYY]:          ← 目标：登录卡片容器
      - img [ref=...]              ← 关闭按钮
      - generic [ref=...]:
        - heading "微信登录" [ref=...]
        - paragraph "请使用手机微信扫码登录"
```

**定位规则**：
- 找到 `dialog` 下 `document` 下的第一个 `generic`，这是卡片容器
- **不要用 `dialog` 本身的 ref**（它是全屏遮罩层）

3. 用 `browser_take_screenshot` 截取元素：
```
browser_take_screenshot:
  element: "登录弹窗卡片"
  ref: "eYYY"
  filename: "/tmp/aippt-qrcode.png"
```

> 必须传 `ref` + `element` 做元素级截图，禁止全屏截图。

### 上传二维码截图（通过 tmpfiles.org）

> 登录阶段尚未获取 AUTH_TOKEN，无法使用小方同学上传接口，改用 tmpfiles.org 免登录上传。

```bash
curl -s -L --max-time 30 \
  -F "file=@/tmp/aippt-qrcode.png" \
  https://tmpfiles.org/api/v1/upload
```

- 返回 JSON：`{"status":"success","data":{"url":"http://tmpfiles.org/ID/filename.png"}}`
- **直链需转换**：将返回 URL 中的 `tmpfiles.org/` 替换为 `tmpfiles.org/dl/`
  - 返回：`http://tmpfiles.org/29295630/aippt-qrcode.png`
  - 直链：`https://tmpfiles.org/dl/29295630/aippt-qrcode.png`
- 文件有效期约 1 小时，足够扫码登录使用
- 如果上传失败，将本地截图路径发给用户，提示用户在浏览器中手动扫码

**发送给用户**：

```
![小方同学登录二维码](DIRECT_URL)
请用手机微信扫描以上二维码完成登录。扫码成功后请告诉我。
```

> 钉钉等渠道**必须用公网链接**，本地路径无法展示。

---

## 错误码表

| code | 说明 | 处理方式 |
|------|------|---------|
| 0 | 成功 | 正常处理 |
| 14006 | Token 过期 / 未登录 | 回到登录步骤重新获取 Token |
| 其他非 0 | 业务错误 | 将 `code` 和 `msg` 原样展示给用户 |

> API 报错直接告知用户，不要尝试自行排查。平台限制类错误（如"Agent 并发任务上限"）需用户去小方同学工作台处理。


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)