# Cron Job: gdcjgk-每日发布

**Job ID:** 533d92e64b59
**Run Time:** 2026-07-05 10:01:44
**Schedule:** 0 10 * * *

## Prompt

[IMPORTANT: The user has invoked the "gdcjgk-style-guide" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]

---
name: gdcjgk-style-guide
description: 广东春季高考信息网全媒体统一排版规范——官网文章、企微推送、公众号、社群、朋友圈、小红书的格式与语调标准。
category: creative
---

# 广东春季高考信息网 · 全媒体排版规范

## 品牌调性

| 维度 | 标准 |
|------|------|
| 受众 | 中职生/高中生（16-19岁）+ 家长（35-50岁） |
| 定位 | "春季高考路上最靠谱的信息伙伴" |
| 语调 | 专业但不死板、有用但不爹味、亲切但不油滑 |
| 视觉色 | 主色 #1a6fb5（蓝）、辅助 #e8f4fd（浅蓝底）、强调 #e67e22（橙） |
| 风格关键词 | 清晰、可信、温暖 |

## 六大渠道规范

### 1. 官网文章 (gdcjgk.net)

**HTML 结构模板：**
```html
<p class="lead">【一句话导语，说明这篇文章解决什么问题】</p>

<h2>小标题一</h2>
<p>正文段落。每段不超过 3-4 行，手机端阅读友好。</p>
<p>重点数据/关键信息用 <strong>加粗</strong> 突出。</p>

<h2>小标题二</h2>
<p>...</p>

<!-- 重要信息用卡片 -->
<div class="info-card">
  <strong>📌 关键提示</strong>
  <p>需要特别注意的内容放在这里。</p>
</div>

<h2>小标题三</h2>
<p>...</p>

<!-- 总结/CTA -->
<p class="cta">关注「广东春季高考信息网」，第一时间获取最新政策解读。</p>
<p class="source">信息来源：XXX</p>
```

**HTML 规范：**
- 标题仅用 `<h2>`（h1 由 CMS 模板自动生成）
- 段落用 `<p>`，不加 class（除非是 lead/cta/source）
- 列表用 `<ol>` / `<ul>` + `<li>`
- 强调用 `<strong>`，不用 `<b>`
- 链接用 `<a href="..." target="_blank">`
- 不使用内联 style（保持 CMS 模板风格统一）
- 不使用 `<br>` 换行，用独立段落

**文章长度：** 800-1200 字

**标题公式：**
- 数字型：`3个关键变化！2026年春考志愿填报新规解读`
- 问答型：`中职生能报本科吗？2026年3+证书报考全攻略`
- 时效型：`刚刚发布！省考试院公布2026年证书要求`

### 2. 企微群推送（审核通知）

**格式：Markdown | 限制：4096 bytes（约 1300 中文字）**

```markdown
## 🔔 新文章待审核
> 栏目：{channel_path}
> 状态：<font color="warning">草稿·待审核</font>

**{文章标题}**

{一句话摘要，30字以内}

📝 [后台编辑审核](https://gdcjgk.net/rCIwxpQnNh.php/cms/archives/edit/ids/{id})

---
<font color="info">审核后在后台改状态为「发布」</font>
```

### 3. 企微群推送（日报/选题）

**格式：Markdown | 限制：4096 bytes**

```markdown
## 📋 {日期} 选题日报
> 今日抓取 **{N}** 篇，推荐 **{M}** 个选题

🔥 **选题一：{标题}**
- 切入角度：{一句话}
- 参考来源：[{来源名}]({链接})

🔥 **选题二：{标题}**
- 切入角度：{一句话}

💡 **可储备选题**
- {选题}：{为什么值得储备}

---
<font color="info">💬 回复选题编号获取详细文案</font>
```

### 4. 公众号长文

**格式：微信编辑器兼容 HTML**

结构：
1. **顶部引导关注**（一句 Slogan + 公众号名片）
2. **导语**（2-3 句，制造悬念/痛点）
3. **正文**（短段落 + 表情包/截图分隔）
4. **关键信息卡片**（微信自带编辑器样式）
5. **总结 + 互动引导**（评论区聊聊/转发给需要的同学）

标题规范：15-25 字，必含数字或情绪词
- ✅ `最后30天！春考冲刺这样做，提分20+`
- ❌ `关于春季高考备考的一些建议和方法`

### 5. 微信群话术

**格式：纯文本 | 限制：600 字以内**

结构：
1. 【分类标签】用【】括号
2. 一句话核心信息
3. 2-3 个要点（用 ①②③）
4. 链接 / 行动指引

```
【志愿填报】
省考试院发布2026志愿填报指南了！3个重点：
①《指南》已送达各校，报订了的同学找学校领
② 使用三步法：了解规则→查阅数据→做好记录
③ 没报订的6月13日起可线下购买

📎 详细解读：https://...
```

### 6. 朋友圈文案

**格式：纯文本 | 限制：200 字以内**

结构：Emoji 开头 + 1-2 句 + 链接/配图

```
📢 刚发布！省考试院2026志愿填报指南来了
三步搞懂怎么用，3+证书考生尤其要看第4点
👉 点击看全文
```

### 7. 小红书笔记

**格式：图文 | 正文限制：1000 字**

结构：
1. **封面图**：大字标题 + 关键数字
2. **标题**：含 Emoji + 数字 + 人群标签
3. **正文**：短句分行，Emoji 分段，关键词加 # 标签
4. **标签**：#春季高考 #3+证书 #中职生 #志愿填报

**⚠️ 关键原则：拒绝空泛，必须带数据。** 纯框架式/鸡汤式内容（如「选专业要看这三个维度」）在小红书没有传播力。必须落到具体：院校名、分数、排位、招生人数。用户反馈"太空了"就说明方向错了。

**当选题需要多角度切入时，提供 4 种视角供选择：**
- A. 算账流 — 收入/开支具体数字
- B. 避雷流 — 反向盘点慎选项
- C. 实操流 — 可直接收藏的清单（院校+专业+分数）
- D. 故事流 — 真实案例对比

用户偏好 C（实操清单流），内容最扎实。

```
📢 2026志愿填报指南来了‼️3步搞定

中职生/高中生都给我看过来👀

省考试院刚发的《志愿填报指南》
我用3句话给你讲清楚怎么用👇

1️⃣ 先看规则
平行志愿咋投档、体检有啥要求...

2️⃣ 再查数据
重点看"最低排位"不是分数‼️
排位比分数稳多了

3️⃣ 做好记录
把心仪学校整理成冲稳保三档

💡 3+证书考生额外注意第4点
（证书要求必须匹配！）

#春季高考 #3+证书 #广东春考 #志愿填报
```

## 多平台分发工作流

同一选题可产出多个平台版本。标准流程：

### 目录结构

```
~/.hermes/articles/YYYY-MM-DD/
├── README.md / .html               ← 当日产出总览
├── 小红书/选题名/
│   ├── cover.png + inner-NN.png    ← 竖版 1080×1660
│   └── 小红书文案.md
├── 公众号/选题名/
│   ├── 文章.md / .html             ← 长文 1200-1500 字
│   └── images/                     ← 横版 900×500 配图
└── 百家号/选题名/
    ├── 文章.md / .html             ← 公众号版微调（开篇更SEO化）
    └── images/                     ← 同公众号配图
```

### 版本差异

| 维度 | 公众号 | 百家号 | 小红书 |
|------|--------|--------|--------|
| 字数 | 1200-1500 | 1200-1500 | ≤1000 |
| 图片 | 横版 900×500 | 横版 900×500 | 竖版 1080×1660 |
| 标题 | 15-25字，含数字/情绪词 | 同公众号，开头更搜索友好 | Emoji+数字+人群标签 |
| 导语风格 | 痛点切入 | 搜索关键词前置 | 短句+Emoji |
| 结尾 | 引导关注公众号 | 引导搜索关注 | #标签收尾 |

### Lighthouse 控制台兼容

腾讯云 Lighthouse 文件管理器**不支持预览 .md 文件**。每篇 .md 文章必须同步生成 .html：

```bash
python3 ~/.hermes/scripts/md2html.py
```

该脚本自动将当天所有 .md 转为品牌配色的 .html（markdown 库 + gdcjgk 蓝橙样式）。

## 通用规则

### 必加元素
- 信息来源标注（官网文末）
- 配图/缩略图（全渠道）
- CTA（引导下一步行动）

### 禁止事项
- ❌ 标题党（过度夸张、虚假承诺）
- ❌ 长段落（超过 4 行）
- ❌ 纯文字无排版（无小标题/无要点）
- ❌ 无来源的信息（必须标注出处）
- ❌ 过度使用 Emoji（官网文章不用，社交渠道适量）

### 数字规范
- 日期：`2026年6月12日` 或 `6月12日`
- 百分比：`85%` 不写 `百分之八十五`
- 人数：`1.2万人` 不写 `12000人`
- 分数：`320分` 保持数字一致性

### 考试相关术语
| 正确 | 错误/避免 |
|------|-----------|
| 3+证书考试 | 3+证书、3+ |
| 依学考录取 | 学考、依学考 |
| 春季高考 | 春考（正式文章不用缩写） |
| 高职高考 | 职高高考 |
| 中职生 | 中专生（两者有区别） |

## ⚠️ 事实核查（写文章前必做）

**铁律：涉及政策、录取条件、分数线、院校名单等具体数据时，必须引用官方来源。不要凭印象写。**

### 官方信息源（按优先级排列）

| 来源 | 用途 | 链接 |
|------|------|------|
| 广东省教育厅答问知识库 | 政策问答、报考条件、学位认定等 | https://edu.gd.gov.cn/hdjlpt/dwzsk |
| 广东省教育考试院 | 招生目录、分数线、报名通知 | https://eea.gd.gov.cn/ |
| 广东教育考试服务网 | 成绩查询、证书查询 | https://www.eesc.com.cn/ |

### 核查流程

1. **文章涉及具体政策/数据 → 先查答问知识库**，搜关键词找官方问答
2. **答问知识库找不到 → 查省教育考试院官网**的招生文件
3. **两个都找不到 → 在文章中注明"以当年招生目录为准"**，不编造具体数字
4. **知识库 API 结构**：分类 API 为 `POST /hdjlpt/letter/dwk/cms/categorys`，文章 API 为 `POST /hdjlpt/letter/dwk/cms/category/articles`（需 session+CSRF，浏览器访问更稳定）。文章页面 URL 格式：`https://edu.gd.gov.cn/hdjlnew/dwzzk/xs/content/post_{ID}.html`

### 常见易错点

| 易错 | 正确做法 |
|------|------|
| 凭印象写「9所本科院校」 | 每年招生院校会变，需查最新目录 |
| 写具体分数线 | 分数线每年变，用「约」「参考往年」或注明来源年份 |
| 写「本科线260分左右」 | 这是编造的——没有查到官方发布前不写具体数字 |
| 写证书要求「多数本科要求B级」 | 各校各专业要求不同，需逐一核实 |

详见 `references/official-sources.md`

## 参考文件
- `references/article-template.html` — 官网文章 HTML 模板（h2+h3+p+ul 结构）
- `references/wecom-templates.md` — 企微群推送模板（审核通知、日报格式）及 4096 bytes 限制说明
- `references/official-sources.md` — 官方信息源清单与核查方法
- `references/article-images.md` — 公众号/百家号配图渲染方案（Playwright + HTML）
- `scripts/md2html.py` — .md → .html 转换（Lighthouse 控制台预览兼容）

## 文章结构原则

### 骨架公式
```
导语（1段）→ 核心内容（2-3个h2段，每段1-2段+p）→ 针对人群提示 → CTA → 来源
```

### 标题公式
| 类型 | 公式 | 示例 |
|------|------|------|
| 数字型 | N个+关键词+变化/趋势+结果 | `高职月薪4882元！这3类专业"钱景"最好` |
| 问答型 | 疑问句+答案暗示 | `中职生能报本科吗？2026年3+证书报考全攻略` |
| 时效型 | 时间词+主体+动作+影响 | `刚刚发布！省考试院公布2026年证书要求` |

### 内容检查清单
- [ ] 导语一句话说明价值
- [ ] 每个 h2 下有实质内容（不只有装饰性标题）
- [ ] 关键数据均有出处
- [ ] 3+证书/中职生视角贯穿全文
- [ ] 文末有 CTA 引导关注
- [ ] 来源标注完整

[IMPORTANT: The user has invoked the "fastadmin-publish" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]

---
name: fastadmin-publish
description: Publish content to FastAdmin/ThinkPHP CMS via API — controller creation, routing pitfalls, Nginx/Apache rewrite gotchas, and database-driven config patterns.
---

# FastAdmin CMS Content Publishing

## When to use
- Auto-publishing AI-generated content to a FastAdmin CMS website
- Bypassing broken/buggy CMS addon APIs
- Creating custom API endpoints in ThinkPHP 5 / FastAdmin

## Built-in API Endpoint (simplest path)

The CMS addon exposes a built-in API at:

```
POST https://<domain>/addons/cms/api/index
Content-Type: application/x-www-form-urlencoded
```

### Parameters

| Param | Required | Description |
|-------|----------|-------------|
| `channel_id` | ✅ | Numeric leaf channel ID |
| `title` | ✅ | Article title |
| `content` | ✅ | HTML body content |
| `description` | No | Summary/excerpt |
| `image` | No | Thumbnail path |
| `user_id` | No | Author user ID (default: 1) |
| `status` | No | `draft` = save as draft, omit/`normal` = publish |
| `apikey` | No | API key (may be disabled — see below) |

Response: `{"code":1,"msg":"新增成功","data":{"id":"3734","url":"https://..."}}`

### Critical Pitfall: xss_clean strips channel_id

The default CMS `Api.php` applies `xss_clean` filter to all POST data:
```php
$data = $this->request->post('', null, 'trim,xss_clean');
```

This filter **strips numeric values** like `channel_id`, causing:
```
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'channel_id' cannot be null
```

**Fix** — In `addons/cms/controller/Api.php`, change line 51:
```php
// Before
$data = $this->request->post('', null, 'trim,xss_clean');
// After
$data = $this->request->post('', null, 'trim');
```

Also ensure the `else` branch sets `$data['channel_id']`:
```php
} else {
    $channel = Channel::get($this->request->request('channel_id'));
    if (!$channel || $channel['status'] != 'normal') {
        $this->error('栏目未找到');
    }
    $data['channel_id'] = $channel->id;  // ← required
}
```

### API Key Bypass

The CMS API checks an `apikey` field but if the admin panel doesn't show the setting (default empty), comment out both auth checks in `_initialize()`:
```php
// if (!$config['apikey']) { ... }
// if ($config['apikey'] != $apikey) { ... }
```

### Channel ID Reference

⚠️ Always use `channel_id` (int), never `channel` (name string). Multiple channels share the same name at different tree levels. Leaf channels (no children) are the publish targets. **channel_id=1 is type='link' (redirect) — articles created there 404.** Only use `type='channel'` or `type='list'` channels.

See `references/gdcjgk-channels.md` for verified publish targets.

### Finding Channels from Database
```bash
# Extract leaf channels from SQL dump
grep -oP "VALUES \(\d+, '\w+', \d+, 0, '\K[^']+" dump.sql | sort -u

# Build full tree
python3 -c "
import re
with open('dump.sql', 'r') as f: content = f.read()
records = re.findall(r'INSERT INTO \`fa_cms_channel\` VALUES \((\d+).*?(\d+), \d+, \'([^\']+)\'', content)
# ...build tree...
"
```

### CURL Test
```bash
# Publish directly
curl -s -X POST "https://gdcjgk.net/addons/cms/api/index" \
  -d "channel_id=227&title=测试标题&content=<p>测试正文</p>&description=摘要"

# Save as draft (not public)
curl -s -X POST "https://gdcjgk.net/addons/cms/api/index" \
  -d "channel_id=227&title=测试标题&content=<p>测试正文</p>&description=摘要&status=draft"
# Review URL: https://gdcjgk.net/rCIwxpQnNh.php/cms/archives/edit/ids/{id}
```

## Custom Controller (when built-in API is broken)

The most reliable approach is a controller inside the CMS addon at `addons/cms/controller/`:

```php
// addons/cms/controller/Publish.php
namespace addons\cms\controller;

class Publish extends Base
{
    public function index()
    {
        // Simple token auth
        if (($_POST['token'] ?? '') !== 'YOUR_TOKEN') {
            return json(['code' => 0, 'msg' => '鉴权失败']);
        }
        
        $channel = \addons\cms\model\Channel::get(intval($_POST['channel_id']));
        $model = \addons\cms\model\Modelx::get($channel['model_id']);
        
        \think\Db::startTrans();
        $archives = new \app\admin\model\cms\Archives;
        $archives->allowField(true)->save([
            'channel_id'  => $channel['id'],
            'model_id'    => $model['id'],
            'title'       => $_POST['title'],
            'content'     => $_POST['content'],
            'description' => $_POST['description'] ?? '',
            'user_id'     => 1,
            'publishtime' => time(),
            'weigh'       => 0,
            'status'      => 'normal',
        ]);
        \think\Db::commit();
        
        return json(['code' => 1, 'msg' => 'OK', 'data' => ['id' => $archives->id, 'url' => $archives->fullurl]]);
    }
}
```

URL: `https://SITE/addons/cms/publish/index`

## Common pitfalls

### 1. Standalone PHP files get intercepted by Nginx
- Putting `api.php` in `public/` — Nginx rewrites ALL requests to `index.php`
- `.htaccess` has `RewriteCond %{REQUEST_FILENAME} !-f` but Nginx ignores it
- **Solution**: Use framework controllers, not standalone files

### 2. FastAdmin config table is tricky
- CMS addon config stored in `fa_config` table as JSON
- `get_addon_config('cms')` reads both `config.php` defaults and DB overrides
- Setting `apikey` via SQL `INSERT INTO fa_config (name, group, value)` often fails due to caching or merge logic
- **Solution**: Don't fight with config table. Use custom controller with hardcoded token.

### 3. ThinkPHP routing
- `application/route.php` controls URL routing
- API module may not be active by default (domain binding commented out)
- CMS addon routes are defined in `application/extra/addons.php` under `route` key
- Custom controllers in `addons/cms/controller/` auto-register — no route config needed
## CMS Content Formatting (曹小芬 2026-07-01)

All article HTML pasted into the backend editor MUST follow these rules:

| Element | Style |
|---------|-------|
| Body | `text-align:justify; line-height:2; font-family:'Microsoft YaHei','微软雅黑',sans-serif; font-size:16px; color:#000;` |
| Headings (h2) | `text-indent:2em; font-weight:bold;` |
| Paragraphs (p) | `text-indent:2em;` |

Wrap everything in `<div style="...all body styles...">` — paste that div into the editor.

Template: `templates/article-format.html`

## Channel Categorization Rules (曹小芬 2026-07-01)

| Article Type | Channel | ID |
|-------------|---------|-----|
| 考试政策、通知、考试历 | 政策信息 > 广东政策 | 198 |
| 语文备考技巧/经验 | 复习资料 > 语文 > 备考经验 | 216 |
| 数学备考技巧/经验 | 复习资料 > 数学 > 备考经验 | 219 |
| 英语备考技巧/经验 | 复习资料 > 英语 > 备考经验 | 222 |
| 语文资料分享 | 复习资料 > 语文 > 资料分享 | 215 |
| 数学资料分享 | 复习资料 > 数学 > 资料分享 | 218 |
| 英语资料分享 | 复习资料 > 英语 > 资料分享 | 221 |
| 3+证书报考 | 3+证书 > 报考相关 | 227 |
| 3+证书填报 | 3+证书 > 填报相关 | 226 |
| 技能证书 | 3+证书 > 技能证书 | 180 |

**DO NOT** put 备考/复习类 articles in 考前辅导 > 辅导培训 (176).

## Channel matching

- Always use `channel_id` (int), never `channel` (name string)
- Multiple channels can share the same name at different tree levels
- Leaf channels (no children) are the publish targets — content in parent channels won't display
- **⚠️ channel_id=1 is type='link' (redirect), NOT a publish target!** Articles created there write to DB but never get page URLs (404). Use type='channel' or 'list' channels only.
- Channel reference: `references/gdcjgk-channels.md`
- Full channel tree: `references/gdcjgk-channels-full.md`

## Pre-publish review notification

Before publishing, send a WeCom notification for human review:
```bash
python3 ~/.hermes/scripts/pre_publish_review.py '[{"title":"...","channel":"栏目名","channel_id":227,"url":"https://..."}]'
```
Uses webhook `d4de64e1-efaa-4f03-a833-ad85c1d6de5b`. Formats as markdown with backend edit link.
- **⚠️ BEWARE of `type='link'` channels** (e.g. channel_id=1 政策资讯). They redirect to an `outlink` URL and NEVER generate article pages — API returns success but all articles 404. Filter: only use `type='channel'` or `type='list'`. Check `references/gdcjgk-channels.md` for verified targets.
- Real channels in gdcjgk.net: 4=考前辅导, 32=招生动态, 176=辅导培训, 198=广东政策, 226=3+证书填报相关, 227=3+证书报考相关, 294=依学考报考相关

## Database reference

| Table | Purpose |
|-------|---------|
| `fa_cms_archives` | Articles (posts) |
| `fa_cms_channel` | Categories/channels (tree via `pid`) |
| `fa_config` | System + addon configuration |
| `fa_cms_model` | Content models |

Channel tree structure: `id`, `type` (channel/list/link), `pid` (parent ID), `name`.

## Thumbnail

Random image from site's own `fa_attachment` history:
```python
import json, random
with open('/home/agentuser/.hermes/data/image_library.json') as f:
    images = json.load(f)
image = random.choice(images)  # 4,179 images from fa_attachment table
```
Pass as `image` param to the publish API. Images are from `https://gdcjgk.net/uploads/...` — already hosted, no upload needed.

## Data cross-reference for content

When building articles that need admission data (majors, scores, schools, enrollment numbers), use the technique in `references/data-cross-reference.md` — cross-references the `2025` diyform table (2,731 majors) against the `fa_cms_archives` model_id=2 school table (106 schools), matched with external data sources.

## Publishing workflow (草稿 + 人工审核)

### 0. Pre-publish fact verification (MANDATORY — do NOT skip)

Before creating the draft, load the `fact-check` skill and verify:
- All dates/times against official sources (eea.gd.gov.cn, 微信公众号「广东省教育考试院」)
- All numbers (录取分数, 排位, 招生计划) against local CSV data first, then official policy documents
- All exam names, institution names, and policy terms are exact (no paraphrasing from memory)

**PITFALL**: Creating a draft before verifying facts means the reviewer (user) will catch errors you should have caught. The user expects the draft to be accurate — they are reviewing for editorial quality, not fact-checking from scratch. If the user has to ask "where did this date come from?", you failed step 0.

### 1. Create draft
```python
data = {
    "channel_id": "226",    # from gdcjgk-channels.md
    "title": "...",
    "content": "<p>...</p>",
    "image": "https://gdcjgk.net/uploads/xxx.jpg",  # random from image_library.json
    "status": "draft",      # DRAFT — not publicly visible
    "diyname": "slug-name"
}
urllib.request.urlopen(urllib.request.Request(
    "https://gdcjgk.net/addons/cms/api/index",
    data=urllib.parse.urlencode(data).encode()
))
```

### 2. Verify draft (should return 404)
```bash
curl -s -o /dev/null -w "%{http_code}" "https://gdcjgk.net/channel-slug/ARTICLE_ID.html"
# Expected: 404 (draft = not public)
```

### 3. Send WeCom review notification
```bash
curl -s "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=***" \
  -H "Content-Type: application/json" \
  -d '{"msgtype":"markdown","markdown":{"content":"## 🔔 新文章待审核\n...\n📝 [后台编辑](https://gdcjgk.net/rCIwxpQnNh.php/cms/archives/edit/ids/ID)"}}'
```

### 4. Reviewer changes status from draft → normal in backend

### Testing
```bash
curl -X POST "https://SITE/addons/cms/api/index" \
  -d "channel_id=227&title=测试&content=<p>测试</p>&status=draft"
```

## Multi-Domain FastAdmin

A single FastAdmin installation can serve multiple domains (e.g. `gdcjgk.net` and `gaozhigaokao.net` pointing to the same backend). Admin sessions may be shared across domains if they share the same PHP session storage, but **credentials can differ per domain** — user `gdcjgk` may exist on one domain but not the other. Always confirm which domain the user is logged into before requesting cookies.

**RELIABLE ADMIN ENTRY: `gaozhigaokao.net`.** The `gdcjgk.net` domain is behind Cloudflare with aggressive WAF that blocks datacenter IPs (returns Cloudflare 403, error code 1010), making server-side API calls impossible. Use `gaozhigaokao.net` for admin access and API calls when available. Note: `gaozhigaokao.net` may have API key protection enabled (`"请先在后台配置API密钥"`) while `gdcjgk.net` has it commented out — the two domains may run slightly different code copies even though they share the database.

### Pitfall: User's browser cookie doesn't work from server

Even with the correct `PHPSESSID` from the user's browser, curl requests from the server will be rejected ("请登录后操作"). PHP sessions can be bound to the client's IP, User-Agent, or other server-side validation. **Do not spend time debugging cookie transfer** — if the API is unreachable from the server, fall back to providing the formatted HTML at a public URL and instructing the user to paste it into the backend editor manually via `F12` → Console → `document.cookie` → copy → paste HTML.

### Pitfall: gdcjgk.net API blocked from datacenter

When `gdcjgk.net` is protected by Cloudflare, the server cannot reach `/addons/cms/api/index` (returns Cloudflare 403). Fallback options in order:
1. Try `gaozhigaokao.net/addons/cms/api/index` if it shares the same database
2. If that requires API key, provide HTML at `https://rss.gdcjgk.net/static/<article>.html` for manual paste\n\n## Admin Panel Login (editing existing articles)

The built-in API only **creates** articles — it cannot update or read existing ones. Editing requires admin panel access. However, if `xss_clean()` was removed from the codebase (as in Api.php), the admin login may also be broken.

### Pitfall: AdminLog.php xss_clean breaks login

If `xss_clean()` was stripped from the CMS but `AdminLog.php` still calls it, the admin login flow crashes at step 5 below, returning HTTP 500 even with correct credentials:

1. CSRF token check → OK
2. Captcha validation → OK
3. Username/password check → OK
4. Session initialization → partial
5. **AdminLog hook records login** → **FATAL: `Call to undefined function xss_clean()`**
6. Response → HTTP 500, session NOT committed

**How to tell**: if login returns 500 with `xss_clean` in the error body, the credentials WERE correct but the hook crashed. The PHP session cookies (`PHPSESSID`, `server_name_session`) are set but contain NO admin authentication — you cannot access the dashboard with them.

**Server-side fix** — In `/www/wwwroot/<site>/application/admin/model/AdminLog.php` line 83, replace:
```php
'url' => substr(xss_clean(strip_tags(request()->url())), 0, 1500),
```
with:
```php
'url' => substr(strip_tags(request()->url()), 0, 1500),
```

**Workaround** — If the user has an active admin session in their browser, ask for their `PHPSESSID` and `server_name_session` cookies. The most reliable way to get both at once is via the browser Console (`F12` → Console → `document.cookie`), which returns all cookies as a single semicolon-separated string. The Application tab sometimes hides or filters cookies; `document.cookie` is foolproof.

Use the cookies to authenticate curl requests to the admin panel:
```bash
curl -s -b "PHPSESSID=xxx; server_name_session=yyy" \
  "https://SITE/rCIwxpQnNh.php/cms/archives/edit/ids/3809"
```

**Captcha reading** — FastAdmin login requires a 4-character captcha. Read it with Qwen-VL via the `bl` CLI:
```bash
curl -s -c /tmp/cookies.txt 'https://SITE/rCIwxpQnNh.php/index/login' > /tmp/login.html
TOKEN=$(grep -oP 'name="__token__" value="\K[^"]+' /tmp/login.html)
curl -s -b /tmp/cookies.txt 'https://SITE/index.php?s=/captcha' -o /tmp/captcha.png
CAPTCHA=$(bl vision analyze --image /tmp/captcha.png \
  --prompt "Read ONLY the 4-character captcha code. Output exactly 4 chars, preserve case." \
  2>&1 | python3 -c "import sys,json; print(json.load(sys.stdin)['choices'][0]['message']['content'].strip())")
```

### Image hosting for article insertion

When the admin panel is unreachable but images need to be inserted into articles:

**PRIMARY PATH — Use `rss.gdcjgk.net/static/`** (Cloudflare-tunneled, reliably accessible):

```bash
# Copy image to we-mp-rss static directory
cp /path/to/image.png /home/ubuntu/project/we-mp-rss-main/static/image_name.png
```

Provide the user with the `<img>` tag:
```html
<img src="https://rss.gdcjgk.net/static/image_name.png" alt="描述" style="max-width:100%; border-radius:8px; margin:16px 0;">
```

**ALWAYS verify the URL before telling the user:**
```bash
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://rss.gdcjgk.net/static/image_name.png
# Must return 200
```

**FALLBACK — `files.gdcjgk.net` (port 8088 HTTP server)** — Only use as backup; often blocked by Cloudflare:
```bash
cp /path/to/image.png ~/.hermes/image_name.png
# URL: https://files.gdcjgk.net/image_name.png
```

**PITFALL: `files.gdcjgk.net` 403 from Cloudflare** — This domain consistently returns Cloudflare edge 403 (`server: cloudflare`, `cf-ray` header) while `rss.gdcjgk.net` works through the same tunnel. Root cause is Cloudflare DNS proxy/WAF configuration, not the tunnel. Do NOT spend time debugging the tunnel — use the `rss.gdcjgk.net/static/` workaround immediately.

**PITFALL: HTTP server on 8088 hangs silently** — Python's `http.server` on port 8088 can enter a state where the process is alive (`ps aux` shows it) but all requests return "Empty reply from server" (curl exit code 52). Fix: kill the hung process and restart:
```bash
# Find and kill hung server
ps aux | grep 'http.server 8088' | grep -v grep | awk '{print $2}' | xargs kill
# Start fresh (use background=true in Hermes terminal)
cd ~/.hermes && python3 -m http.server 8088 --bind 0.0.0.0
```

See `references/image-hosting-workaround.md` for full troubleshooting flow including Cloudflare tunnel restart and 403 diagnosis.

See `references/admin-login-debugging.md` for a full session transcript including API exploration, cookie investigation, and the full PHP error trace from the xss_clean crash.

The user has provided the following instruction alongside the skill invocation: [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.]

你是广东春季高考信息网(gdcjgk.net)的内容编辑。基于 we-mp-rss 数据库最新文章，自动完成每日文章发布。

## 执行步骤

### 1. 提取选题
复制数据库: cp ~/project/we-mp-rss-data/db.db /tmp/db.db
查询最近36小时文章:
sqlite3 /tmp/db.db "SELECT a.id, a.title, a.url, a.publish_time, f.mp_name as source, datetime(a.publish_time, 'unixepoch', 'localtime') as ptime_str FROM articles a JOIN feeds f ON a.mp_id=f.id WHERE a.publish_time > strftime('%s', 'now', '-36 hours') ORDER BY a.publish_time DESC"

### 2. 选题评分（≥7分才入选）
对每篇候选文章评分：
- 信息增量 0-5分（文章提供的考生/家长需要的新信息量）
- 时效性 0-3分（与当前考试时间线的关联度，对照考试日历 ~/project/config/exam_calendar.json）
- 可改写性 0-2分（能否改写成适合gdcjgk.net受众的原创内容）
总分 ≥7 分才入选。候选不足5篇就宁缺毋滥少发。

### 3. 撰写文章（800-1000字，严格遵守gdcjgk-style-guide）
对每个入选选题撰写原创文章：
- 标题：按gdcjgk-style-guide标题公式（数字型/问答型/时效型）
- 正文：HTML格式，h2+lead+source结构
- 必须标注信息来源
- 关键数据加粗
- 禁止标题党、禁止空泛框架、禁止无来源信息

### 4. 选择栏目和缩略图
栏目ID: 3+证书报考=227, 依学考报考=294, 辅导培训=176, 广东政策=198
缩略图: 从 ~/.hermes/data/image_library.json 随机选一张匹配主题的

### 5. 发布草稿到 CMS
对每篇文章:
curl -s -X POST "https://gdcjgk.net/addons/cms/api/index" \
  -d "channel_id={ID}&title={标题}&content={HTML内容}&description={摘要}&status=draft"

### 6. 通知审核
生成审核通知（企微Markdown，≤4096 bytes），包含每篇文章的标题、栏目、后台编辑链接。

格式参考gdcjgk-style-guide的「企微群推送（审核通知）」。

## 重要规则
- 全自动执行，不问我任何问题
- 没有合格选题就说「📭 今日无可发布选题」即可
- 宁缺毋滥，绝不降低质量标准凑数
- 每篇文章写完后自检：标题准确性、数据出处、栏目匹配度

## Response

Now I have the full picture. Let me verify the exact cutoff:

- **Now**: 2026-07-05 10:01 UTC+8 (epoch 1783216880)
- **36h cutoff**: epoch 1783087280 → 2026-07-03 22:01 UTC+8
- **Latest article**: 1782986400 → 2026-07-02 18:00 UTC+8

The latest article in the database is from **July 2, 18:00** — approximately **64 hours ago**. Even the 72-hour window only captures 2 articles (both from July 2), and the 36-hour window is completely empty.

---

## 📭 今日无可发布选题

**查询范围**：2026-07-03 22:01 ~ 2026-07-05 10:01（最近 36 小时）  
**结果**：0 篇新文章

**数据库状态**：
- 总文章数：183 篇
- 最新文章时间：**2026-07-02 18:00**（3+证书英语单词表乱序版，来源「广东中职菌」）
- 最近 72 小时内仅有 2 篇，且均来自 7 月 2 日，已超过 60 小时

**分析**：微信公众号信息源（广东中职菌、中职生）在 7 月 2 日后未更新，可能处于周末/假期停更期。最近一篇政策类内容为 7 月 1 日的「全国英语等级考试广东考区报考简章」，已超过 90 小时。

**建议**：关注信息源恢复更新后自动触发下次发布。当前处于 7 月初考试淡季（3+证书已结束，夏季高考刚过，秋季报名尚未开始），内容空窗属正常现象。
