Skill: abill6688/zhongkao-exam-search (version 1.0.0)

--- name: zhongkao-exam-search description: "搜索并下载全国各地中考真题试卷。触发词：中考真题、中考试卷、中考英语、中考数学、中考语文、中考物理、中考化学、真题下载、历年中考、中考备考、中考复习、湖南中考、长沙中考、北京中考、上海中考、各省中考真题下载" description_zh: "搜索并下载全国各地中考真题试卷，支持各科各省各年份" description_en: "Search and download Zhongkao (Chinese high school entrance exam) test papers across China" version: 1.0.0 --- # 中考真题检索 Skill 搜索并下载全国各地中考真题试卷（中考/初中学业水平考试），支持各科、各省、各年份。从多个免费来源自动搜索、下载、验证试卷文件。 ## 工作流程 ### 第1步：明确需求 确认以下信息： - **省份/城市**：如"湖南长沙"、"北京"、"上海" - **科目**：语文、数学、英语、物理、化学、道德与法治、历史、地理、生物 - **年份**：如 2021-2025 - **是否需要答案**：含答案 vs 原卷版 ### 第2步：搜索资源 **按来源优先级搜索**（详见 `references/sources.md`）： | 优先级 | 来源 | 适用范围 | 可靠度 | |--------|------|----------|--------| | 1 | 中考网 (zhongkao.com) | 全科 | ⭐⭐⭐ | | 2 | 中学英语网 (trjlseng.com) | 仅英语 | ⭐⭐⭐ | | 3 | 第一试卷网 (shijuan1.com) | 全科 | ⭐⭐ | | 4 | 中学学科网 (zxzyw.cn) | 全科 | ⭐⭐ | | 5 | 无忧考网 (51test.net) | 全科 | ⭐ | | 6 | 社交媒体（小红书/微信公众号） | 全科 | ⭐ | **搜索方法**： ```bash # 使用搜索脚本获取各来源的搜索关键词和URL模式 {baseDir}/scripts/search.sh "湖南长沙" "英语" 2024 ``` 然后使用 `web_search` 工具按输出的 `query` 字段搜索，获取具体下载页面URL。 ### 第3步：下载文件 **从中考网下载**（找到页面后提取files.eduuu.com下载链接）： ```bash # 下载文件 python3 {baseDir}/scripts/download.py \ "https://files.eduuu.com/ohr/2021/06/22/174543_60d1b147d1e67.rar" \ "/output/2021年湖南长沙中考英语真题.rar" ``` **解压文件**： ```bash # ZIP文件 python3 -c "import zipfile; zipfile.ZipFile('文件.zip').extractall('.')" # RAR文件（需要安装unar: brew install unar） unar -o /output/dir "文件.rar" ``` **特殊情况**（详见 `references/edge-cases.md`）： - 全省统一命题：搜索"XX省中考"而非"XX市中考" - 只有图片版：逐张下载PNG图片 - RAR文件：需安装unar ### 第4步：验证文件（关键步骤，不可跳过！） ```bash # 验证文件的真实性和完整性 python3 {baseDir}/scripts/verify.py \ "/path/to/2021年湖南长沙中考英语真题.docx" \ 2021 "湖南长沙" "英语" ``` 验证脚本自动检查： 1. 文件大小（≥50KB） 2. 文件格式与扩展名一致性 3. docx内容关键词（"考试"、"注意事项"、学科关键词） 4. 是否为扫描嵌入版 5. HTML重定向检测（防止下载到登录页） **⚠️ 验证不通过禁止上传到知识库！** ### 第5步：整理与交付 - 统一文件命名：`{年份}年{省份}{城市}中考{科目}真题.{扩展名}` - 如有答案：`{年份}年{省份}{城市}中考{科目}真题(含答案).{扩展名}` - 记录来源网站和下载链接 - 如需上传到IMA知识库，使用ima-skill的上传流程 ## 质量红线 | 级别 | 规则 | |------|------| | 🔴 禁止 | 用搜索片段拼凑试卷 | | 🔴 禁止 | 上传未经验证的文件 | | 🔴 禁止 | 用AI编造/补全题目内容 | | 🟡 警告 | 扫描嵌入版docx文件较大是正常的 | | 🟡 警告 | 图片版试卷清晰度可能不足 | ## 踩坑经验 - **2024年湖南统一命题**：中考网只提供图片版无下载版，免费PDF/DOCX极难获取 - **IMA不支持ZIP**：上传ZIP会报错，需逐个文件上传 - **IMA无删除API**：上传错误文件需在客户端手动删除 - **中学英语网**：部分资源需要注册登录，但注册免费 - **中考网移动版**：m.zhongkao.com有时比www版更容易获取下载链接

path: _skillhub_meta.json
content: name: zhongkao-exam-search
version: 1.0.0
installedAt: 1745827200000
source: local

path: references/edge-cases.md
content: # 特殊情况处理指南

## 全省统一命题

### 背景
部分省份从某年开始实行全省统一命题，此时"XX省中考"和"XX市中考"是同一份试卷。

### 已知情况
| 省份 | 开始年份 | 说明 |
|------|----------|------|
| 湖南 | 2024 | 长沙市中考使用湖南省统一命题试卷 |
| 浙江 | 2024 | 部分科目统考 |
| 江西 | 2024 | 全省统一命题 |

### 搜索策略调整
- 2024年起搜索"湖南省中考英语"而非"长沙市中考英语"
- 中考网可能只提供图片版（因统考试卷管控更严）
- 同省其他城市的试卷页面可能包含同一份试题

---

## 只有图片版试卷

### 判断方法
- 中考网页面底部无"下载完整版"按钮
- 页面显示"图片版"标签

### 处理流程
1. 获取各分页的图片URL
2. 逐张下载图片：`curl -L -A "Mozilla/5.0" -o "page_N.png" "{图片URL}"`
3. 整理命名：`{年份}年{城市}中考{科目}_第{N}页.png`
4. 上传知识库时逐个上传（IMA不支持ZIP）

### 图片URL获取
- 方法1: 用web_fetch获取页面HTML，提取`<img>`标签中files.eduuu.com的src
- 方法2: 用浏览器自动化（playwright）获取动态加载的图片
- 方法3: 从搜索结果中直接获取夸克网盘预览链接

---

## RAR文件解压

### macOS处理
```bash
# 安装unar (推荐)
brew install unar

# 解压RAR
unar -o /output/dir "file.rar"

# 或用Python + rarfile (需要unrar二进制)
pip install rarfile
python3 -c "import rarfile; rarfile.RarFile('file.rar').extractall('.')"
```

### 已知问题
- macOS系统默认无unrar，需安装
- Python rarfile模块需要unrar二进制支持
- 推荐使用 `unar`，Homebrew一键安装

---

## IMA知识库上传注意事项

### 支持的格式
| 格式 | media_type | 最大大小 |
|------|-----------|----------|
| PDF | 1 | 200MB |
| DOCX | 3 | 200MB |
| PPTX | 4 | 200MB |
| XLSX | 5 | 10MB |
| Markdown | 7 | 10MB |
| PNG/JPG | 9 | 30MB |
| TXT | 13 | 10MB |

### 关键限制
- ❌ **不支持ZIP格式**：上传ZIP会报"Unrecognized file extension"
- ❌ **无删除API**：IMA OpenAPI不提供delete_knowledge接口，上传后需在客户端手动删除
- ✅ 批量上传：可逐个文件上传，使用check_repeated_names检查重名

### 上传流程
使用ima-skill的脚本：
1. `preflight-check.cjs` — 检查文件类型和大小
2. `create_media` API — 获取media_id和COS凭证
3. `cos-upload.cjs` — 上传文件到COS
4. `add_knowledge` API — 关联到知识库

---

## 文件命名规范

### 统一格式
- 原卷版: `{年份}年{省份}{城市}中考{科目}真题.{扩展名}`
- 含答案: `{年份}年{省份}{城市}中考{科目}真题(含答案).{扩展名}`
- 图片版: `{年份}年{省份}{城市}中考{科目}_第{N}页.png`

### 示例
- `2021年湖南长沙中考英语真题.docx`
- `2025年湖南长沙中考英语真题(含答案).docx`
- `2023年长沙中考英语_第1页.png`

---

## 质量红线（禁止事项）

### ❌ 绝对禁止
1. **不可用搜索片段拼凑试卷** — 从不同来源拼凑的题目可能张冠李戴
2. **不可上传未经验证的文件** — 必须通过verify.sh验证后才能上传
3. **不可用AI编造/补全题目内容** — 缺失的部分标注"原文缺失"，不要自行补充
4. **不可跳过验证步骤** — 即使来源可信，也要做基本的大小和格式检查

### ⚠️ 警告
1. 中考网图片版可能清晰度不够 — 告知用户
2. 扫描嵌入版docx文件较大 — 正常现象，不要误判
3. 不同来源的同一年试卷可能有微小差异（含/不含答案、排版）— 以原卷版为准


path: references/sources.md
content: # 中考真题来源详细参考

## 来源1: 中考网 (zhongkao.com) — ⭐ 最可靠

### 网站结构
- 主站: https://www.zhongkao.com
- 移动版: https://m.zhongkao.com
- 地方站: https://{城市拼音}.zhongkao.com (如 cs.zhongkao.com = 长沙)

### 下载版页面
- **URL模式**: `m.zhongkao.com/e/{YYYYMMDD}/{随机ID}.shtml`
- **特征**: 页面底部有"下载完整版"按钮，指向 files.eduuu.com
- **搜索关键词**: `{城市} {科目} 真题 下载版 {年份}`

### 下载链接
- **URL模式**: `https://files.eduuu.com/ohr/{年}/{月}/{日}/{时间戳}_{hash}.{rar|zip}`
- **示例**: `https://files.eduuu.com/ohr/2021/06/22/174543_60d1b147d1e67.rar`
- **格式**: ZIP 或 RAR，内含 PDF/DOCX/PNG

### 图片版页面
- 当没有下载版时，中考网会提供图片版
- **URL模式**: 同下载版，但页面底部无"下载完整版"按钮
- **图片URL模式**: `https://files.eduuu.com/img/{年}/{月}/{日}/{文件名}.jpg`
- **分页**: 页面底部有 1 2 3 4... 分页链接

### 已知限制
- 2024年起部分省份（如湖南）全省统一命题，中考网可能只提供图片版而非下载版
- 地方站更新可能滞后
- 搜索功能需要中文编码

### 已验证的下载记录
| 年份 | 城市 | 科目 | 下载URL | 文件大小 | 格式 |
|------|------|------|---------|----------|------|
| 2021 | 长沙 | 英语 | files.eduuu.com/ohr/2021/06/22/174543_60d1b147d1e67.rar | 6.3MB | RAR→DOCX |
| 2022 | 长沙 | 英语 | files.eduuu.com/ohr/2022/06/29/... | 6.8MB | ZIP→PDF |
| 2023 | 长沙 | 英语 | files.eduuu.com/ohr/2023/07/02/153948_64a129c49cd43.zip | 2.3MB | ZIP→PNG×8 |
| 2024 | 长沙 | 英语 | 仅有图片版，无下载版ZIP | — | 图片版 |

---

## 来源2: 中学英语网 (trjlseng.com) — 英语专用

### 网站结构
- 主站: https://trjlseng.com
- 中考真题列表: https://trjlseng.com/zkst/
- 分页: https://trjlseng.com/zkst/list_{page}.html

### 下载链接
- **URL模式**: `https://trjlseng.com/uploads/ueditor/file/{日期}/{文件名}.zip`
- **示例**: `https://trjlseng.com/uploads/ueditor/file/20250703/1751534361398983.zip`
- **格式**: ZIP，内含 DOCX

### 已知限制
- 仅收录英语科目
- 可能需要注册登录才能下载（部分页面可直接下载）
- 更新可能滞后于中考时间

### 已验证的下载记录
| 年份 | 城市 | 科目 | 下载URL | 文件大小 | 格式 |
|------|------|------|---------|----------|------|
| 2025 | 长沙 | 英语 | trjlseng.com/uploads/ueditor/file/20250703/1751534361398983.zip | 1.2MB | ZIP→DOCX(含答案) |

---

## 来源3: 第一试卷网 (shijuan1.com) — 免费下载

### 网站结构
- 主站: https://www.shijuan1.com
- 中考英语分类: https://www.shijuan1.com/a/sjyyzk/
- 分页: https://www.shijuan1.com/a/sjyyzk/list_{分类ID}_{page}.html

### 下载链接
- **URL模式**: `https://www.shijuan1.com/uploads/soft/{分类}/{科目编码}/中考/{文件名}.rar`
- **格式**: RAR，内含 PDF/DOCX

### 科目分类编码
| 科目 | 编码 | 分类页URL |
|------|------|-----------|
| 语文 | sjywzk | /a/sjywzk/ |
| 数学 | sjshxzk | /a/sjshxzk/ |
| 英语 | sjyyzk | /a/sjyyzk/ |
| 物理 | sjwlzk | /a/sjwlzk/ |
| 化学 | sjhxzk | /a/sjhxzk/ |
| 历史 | sjlszk | /a/sjlszk/ |
| 道法 | sjzzzk | /a/sjzzzk/ |
| 生物 | sjswwk | /a/sjswwk/ |
| 地理 | sjdlzk | /a/sjdlzk/ |

### 已知限制
- 按省份拼音排序分页，需逐页查找
- 搜索功能可能返回404
- 更新可能滞后

---

## 来源4: 中学学科网 (zxzyw.cn)

### 特点
- 覆盖面较广，各科都有
- 部分资源免费，部分需积分
- URL模式: `https://www.zxzyw.cn/{科目分类}/{文件ID}.html`

---

## 来源5: 无忧考网 (51test.net)

### 特点
- 有Word版和PDF版
- Word版通常需要VIP
- 页面URL模式: `https://www.51test.net/show/{ID}.html`
- 可获取页面文本内容（非VIP也能看部分）

---

## 社交媒体来源

### 小红书
- 搜索关键词: `{城市}{年份}中考{科目}真题 PDF` 或 `{城市}{年份}中考{科目}真题 网盘`
- 可能有夸克/百度/阿里云盘链接
- 使用小红书Skill的 `search.sh` 搜索

### 微信公众号
- 搜索: `{城市}中考{科目}真题 含答案`
- 常有百度网盘分享，但链接可能过期
- 使用 wechat-article-search Skill 搜索

### 知乎
- 搜索: `{年份}全国中考真题汇总`
- 可能有合集网盘链接

---

## 付费来源（最后备选）

| 网站 | URL | 资源质量 | 价格 |
|------|-----|----------|------|
| 学科网 | zxxk.com | 最全 | VIP 30元/月起 |
| 21世纪教育网 | 21cnjy.com | 较全 | VIP 20元/月起 |
| 百度文库 | wenku.baidu.com | 一般 | VIP 15元/月 |
| 道客巴巴 | doc88.com | 一般 | 需积分 |
| 原创力文档 | book118.com | 较好 | 需积分 |


path: scripts/download.py
content: #!/usr/bin/env python3
"""
download.py — 从指定URL下载中考真题文件
用法: python3 download.py <url> <output_path> [--referer <url>]
返回: JSON格式的下载结果
"""

import sys
import os
import json
import urllib.request
import urllib.error
import ssl

def detect_format(file_path):
    """通过magic bytes检测文件格式"""
    try:
        with open(file_path, "rb") as f:
            header = f.read(16)
        
        if header[:4] == b'%PDF':
            return "pdf"
        elif header[:4] == b'PK\x03\x04':
            return "zip"  # could be docx too
        elif header[:6] == b'Rar!\x1a\x07':
            return "rar"
        elif header[:8] == b'\x89PNG\r\n\x1a\n':
            return "png"
        elif header[:2] == b'\xff\xd8':
            return "jpeg"
        elif header[:4] == b'\xd0\xcf\x11\xe0':
            return "ole2"
        else:
            # Check for HTML
            try:
                with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
                    content = f.read(500).lower()
                    if '<html' in content or '<!doctype' in content:
                        return "html"
            except:
                pass
            return "unknown"
    except:
        return "unknown"


def download_file(url, output_path, referer=""):
    """下载文件并返回结果"""
    result = {
        "success": False,
        "url": url,
        "output": output_path
    }
    
    # 确保输出目录存在
    os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
    
    # 构建请求
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    }
    if referer:
        headers["Referer"] = referer
    
    req = urllib.request.Request(url, headers=headers)
    
    # 创建SSL上下文（macOS Python可能缺少根证书，直接跳过验证）
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    
    try:
        with urllib.request.urlopen(req, context=ctx) as response:
            with open(output_path, 'wb') as f:
                f.write(response.read())
        result["success"] = True
    except urllib.error.HTTPError as e:
        result["error"] = f"HTTP {e.code}"
        result["success"] = False
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return result
    except urllib.error.URLError as e:
        result["error"] = str(e.reason)
        result["success"] = False
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return result
    except Exception as e:
        result["error"] = str(e)
        result["success"] = False
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return result
    
    # 检查文件大小
    file_size = os.path.getsize(output_path)
    result["file_size"] = file_size
    
    if file_size < 1024:
        result["warning"] = f"文件过小 ({file_size} bytes)，可能是HTML重定向页面"
    
    # 检测文件格式
    fmt = detect_format(output_path)
    result["format"] = fmt
    
    print(json.dumps(result, ensure_ascii=False, indent=2))
    return result


def main():
    if len(sys.argv) < 3:
        print(json.dumps({
            "success": False,
            "error": "用法: download.py <url> <output_path> [--referer <url>]"
        }, ensure_ascii=False))
        sys.exit(1)
    
    url = sys.argv[1]
    output_path = sys.argv[2]
    referer = ""
    
    if len(sys.argv) > 3 and sys.argv[3] == "--referer" and len(sys.argv) > 4:
        referer = sys.argv[4]
    
    result = download_file(url, output_path, referer)
    sys.exit(0 if result["success"] else 1)


if __name__ == "__main__":
    main()


path: scripts/search.sh
content: #!/usr/bin/env bash
# search.sh — 从多个免费来源搜索中考真题下载链接
# 用法: ./search.sh "湖南长沙" "英语" 2021
# 输出: JSON格式的搜索结果

set -euo pipefail

REGION="${1:?用法: search.sh <省份城市> <科目> <年份>}"
SUBJECT="${2:?用法: search.sh <省份城市> <科目> <年份>}"
YEAR="${3:?用法: search.sh <省份城市> <科目> <年份>}"

UA="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
RESULTS=()
RESULT_JSON="[]"

log() { echo "[search] $*" >&2; }

# --- 来源1: 中考网 zhongkao.com ---
search_zhongkao() {
  log "搜索中考网 (zhongkao.com)..."
  
  # 提取省份和城市
  PROVINCE=$(echo "$REGION" | sed 's/省\|市//g' | cut -c1-2)
  CITY=$(echo "$REGION" | grep -oE '[^\s]+$' | sed 's/市//g')
  
  # 搜索下载版页面
  DOWNLOAD_URL="https://m.zhongkao.com/e/search/result/"
  
  # 用web搜索找中考网页面
  local search_query="${CITY}+中考${SUBJECT}+真题+下载版+${YEAR}"
  local search_url="https://www.zhongkao.com/e/search/result/?searchid=0&keyboard=${search_query}"
  
  # 直接构建可能的URL模式
  # 已知中考网URL模式: m.zhongkao.com/e/{日期}/{ID}.shtml
  # 下载链接模式: files.eduuu.com/ohr/{年}/{月}/{日}/{文件名}.zip|.rar
  
  echo '{"source":"zhongkao","status":"needs_web_search","query":"'"${CITY} 中考${SUBJECT} 真题 下载版 ${YEAR}"'","url_pattern":"m.zhongkao.com/e/{date}/{id}.shtml","download_pattern":"files.eduuu.com/ohr/{year}/{month}/{day}/{filename}.zip|.rar"}'
}

# --- 来源2: 中学英语网 trjlseng.com (仅英语) ---
search_trjlseng() {
  if [[ "$SUBJECT" != "英语" ]]; then
    echo '{"source":"trjlseng","status":"skipped","reason":"仅支持英语科目"}'
    return
  fi
  
  log "搜索中学英语网 (trjlseng.com)..."
  echo '{"source":"trjlseng","status":"needs_web_search","query":"'"${REGION} 中考英语 真题 ${YEAR}"'","url_pattern":"trjlseng.com/zkst/{id}.html","download_pattern":"trjlseng.com/uploads/ueditor/file/{date}/{filename}.zip","note":"可能需要注册登录"}'
}

# --- 来源3: 第一试卷网 shijuan1.com ---
search_shijuan1() {
  log "搜索第一试卷网 (shijuan1.com)..."
  
  # 科目分类编码
  local subject_code
  case "$SUBJECT" in
    语文)   subject_code="sjywzk" ;;
    数学)   subject_code="sjshxzk" ;;
    英语)   subject_code="sjyyzk" ;;
    物理)   subject_code="sjwlzk" ;;
    化学)   subject_code="sjhxzk" ;;
    历史)   subject_code="sjlszk" ;;
    道法|道德与法治) subject_code="sjzzzk" ;;
    生物)   subject_code="sjswwk" ;;
    地理)   subject_code="sjdlzk" ;;
    *)      subject_code="sjyyzk" ;;  # 默认英语
  esac
  
  echo '{"source":"shijuan1","status":"needs_web_search","query":"'"${REGION} ${YEAR} 中考${SUBJECT}"'","url_pattern":"shijuan1.com/a/'"${subject_code}"'/{id}.html","download_pattern":"shijuan1.com/uploads/soft/{category}/'"${subject_code}"'/中考/{filename}.rar","subject_code":"'"${subject_code}"'"}'
}

# --- 来源4: 中学学科网 zxzyw.cn ---
search_zxzyw() {
  log "搜索中学学科网 (zxzyw.cn)..."
  echo '{"source":"zxzyw","status":"needs_web_search","query":"'"${REGION} 中考${SUBJECT} 真题 ${YEAR} 免费下载"'","note":"部分资源需积分"}'
}

# --- 来源5: 五一考网 51test.net ---
search_51test() {
  log "搜索无忧考网 (51test.net)..."
  echo '{"source":"51test","status":"needs_web_search","query":"'"${REGION} ${YEAR} 中考${SUBJECT} 试题 答案 Word"'","note":"Word版需VIP"}'
}

# --- 汇总输出 ---
main() {
  log "搜索: ${REGION} ${YEAR}年中考${SUBJECT}真题"
  log "================================"
  
  R1=$(search_zhongkao)
  R2=$(search_trjlseng)
  R3=$(search_shijuan1)
  R4=$(search_zxzyw)
  R5=$(search_51test)
  
  # 输出JSON数组
  python3 -c "
import json, sys
results = [
  json.loads('''$R1'''),
  json.loads('''$R2'''),
  json.loads('''$R3'''),
  json.loads('''$R4'''),
  json.loads('''$R5'''),
]
print(json.dumps(results, ensure_ascii=False, indent=2))
"
  
  log "================================"
  log "搜索完成。下一步：使用web_search工具按query字段搜索，获取具体下载页面URL"
  log "然后使用download.sh下载文件"
}

main


path: scripts/verify.py
content: #!/usr/bin/env python3
"""
verify.py — 验证下载的中考真题文件的真实性和完整性
用法: python3 verify.py <file_path> <expected_year> [expected_region] [expected_subject]
返回: JSON格式的验证结果
"""

import sys
import os
import json
import re
import zipfile

def verify_file(file_path, year, region="", subject="英语"):
    """验证文件的真实性和完整性"""
    result = {
        "pass": True,
        "file": file_path,
        "issues": [],
        "checks": [],
        "content_detail": None
    }
    
    # 检查1: 文件是否存在
    if not os.path.exists(file_path):
        result["pass"] = False
        result["error"] = "文件不存在"
        return result
    
    # 检查2: 文件大小
    file_size = os.path.getsize(file_path)
    min_size = 51200  # 50KB
    size_ok = file_size >= min_size
    result["checks"].append(f"文件大小: {file_size} bytes {'✅' if size_ok else '❌'}")
    if not size_ok:
        result["pass"] = False
        result["issues"].append(f"文件过小 ({file_size} bytes < {min_size} bytes)")
    result["file_size"] = file_size
    
    # 检查3: 文件格式与扩展名一致
    file_ext = os.path.splitext(file_path)[1].lower().lstrip(".")
    
    # 用magic bytes检测实际格式
    actual_format = detect_format(file_path)
    format_ok = True
    
    format_map = {
        "pdf": ["pdf"],
        "docx": ["zip", "docx"],  # docx实际是zip格式
        "doc": ["ole2", "doc"],
        "zip": ["zip"],
        "rar": ["rar"],
        "png": ["png"],
        "jpg": ["jpeg"],
    }
    
    if file_ext in format_map:
        if actual_format not in format_map[file_ext]:
            format_ok = False
            result["pass"] = False
            result["issues"].append(f"扩展名.{file_ext}但实际格式为{actual_format}")
    
    result["checks"].append(f"文件格式一致性: {actual_format} {'✅' if format_ok else '❌'}")
    result["actual_format"] = actual_format
    
    # 检查4: HTML重定向检测
    if actual_format == "html":
        result["pass"] = False
        result["issues"].append("文件是HTML页面而非真实试卷文件，可能是下载被重定向到登录页")
    
    # 检查5: 内容关键词验证
    if file_ext == "docx" and actual_format in ["zip", "docx"]:
        content_result = verify_docx(file_path, subject)
        result["content_detail"] = content_result
        
        if not content_result.get("has_content", False):
            result["pass"] = False
            result["issues"].append("docx内容为空或过短")
        
        if content_result.get("issues"):
            for issue in content_result["issues"]:
                if "必需关键词" in issue:
                    result["pass"] = False
                    result["issues"].append(issue)
        
        is_scan = content_result.get("is_scan", False)
        result["checks"].append(
            f"内容检查: has_content={content_result.get('has_content')}, "
            f"is_scan={is_scan} {'✅' if content_result.get('has_content') else '❌'}"
        )
    
    elif file_ext == "pdf" and actual_format == "pdf":
        pdf_result = verify_pdf(file_path)
        result["content_detail"] = pdf_result
        if not pdf_result.get("valid", False):
            result["pass"] = False
            result["issues"].append("PDF文件无效")
        result["checks"].append(f"PDF有效性: {pdf_result.get('valid')} {'✅' if pdf_result.get('valid') else '❌'}")
    
    return result


def detect_format(file_path):
    """通过magic bytes检测文件实际格式"""
    try:
        with open(file_path, "rb") as f:
            header = f.read(16)
        
        if header[:4] == b'%PDF':
            return "pdf"
        elif header[:4] == b'PK\x03\x04':
            # ZIP或DOCX（DOCX也是ZIP格式）
            return "zip"
        elif header[:7] == b'Rar!\x1a\x07':
            return "rar"
        elif header[:8] == b'\x89PNG\r\n\x1a\n':
            return "png"
        elif header[:2] == b'\xff\xd8':
            return "jpeg"
        elif header[:4] == b'\xd0\xcf\x11\xe0':
            return "ole2"  # MS Office 97-2003
        elif header[:5] == b'<?xml' or header[:1] == b'<':
            return "html"
        elif b'<!DOCTYPE' in header or b'<html' in header.lower():
            return "html"
        else:
            # 尝试检测HTML
            try:
                with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
                    content = f.read(500).lower()
                    if '<html' in content or '<!doctype' in content:
                        return "html"
            except:
                pass
            return "unknown"
    except:
        return "unknown"


def verify_docx(file_path, subject="英语"):
    """验证DOCX文件内容"""
    result = {
        "type": "docx",
        "is_scan": False,
        "text_length": 0,
        "media_count": 0,
        "media_total_size": 0,
        "found_keywords": [],
        "issues": [],
        "has_content": False
    }
    
    try:
        with zipfile.ZipFile(file_path, 'r') as z:
            # 提取文本
            try:
                with z.open('word/document.xml') as f:
                    content = f.read().decode('utf-8')
                    text = re.sub(r'<[^>]+>', ' ', content)
                    text = re.sub(r'\s+', ' ', text).strip()
                    result["text_length"] = len(text)
            except KeyError:
                text = ""
            
            # 检查媒体文件
            media_files = [n for n in z.namelist() if 'media' in n.lower()]
            result["media_count"] = len(media_files)
            total_media_size = sum(z.getinfo(n).file_size for n in media_files)
            result["media_total_size"] = total_media_size
            
            # 判断是否为扫描嵌入版
            result["is_scan"] = total_media_size > 1000000 and len(text) < 500
            
            # 关键词检查（扫描版跳过，因为文本在图片中无法提取）
            if not result["is_scan"] and len(text) > 50:
                # 检查必需关键词
                required_keywords = ['考试', '注意事项']
                for kw in required_keywords:
                    if kw in text:
                        result["found_keywords"].append(kw)
                    else:
                        result["issues"].append(f'缺少必需关键词: {kw}')
                
                # 检查学科关键词
                subject_keywords_map = {
                    "英语": ['听力', '阅读', '完形', '填空', '写作', '翻译'],
                    "数学": ['选择题', '填空题', '解答题', '计算', '证明'],
                    "语文": ['阅读', '作文', '文言文', '默写', '名著'],
                    "物理": ['选择题', '填空题', '实验', '计算'],
                    "化学": ['选择题', '填空题', '实验', '计算'],
                }
                
                subject_keywords = subject_keywords_map.get(subject, ['选择题', '填空题'])
                found_subject = [kw for kw in subject_keywords if kw in text]
                
                if len(found_subject) < 2:
                    result["issues"].append(f'学科关键词过少(仅{len(found_subject)}个): {found_subject}')
                else:
                    result["found_keywords"].extend(found_subject)
            elif result["is_scan"]:
                result["found_keywords"] = ["(扫描嵌入版，无法提取文本关键词)"]
            
            # 判断是否有实质内容
            result["has_content"] = len(text) > 200 or result["is_scan"]
    
    except Exception as e:
        result["error"] = str(e)
        result["issues"].append(str(e))
    
    return result


def verify_pdf(file_path):
    """验证PDF文件"""
    result = {"type": "pdf", "valid": False, "issues": []}
    try:
        with open(file_path, 'rb') as f:
            header = f.read(10)
            if header[:4] == b'%PDF':
                result["valid"] = True
            else:
                result["issues"].append('非有效PDF文件')
    except Exception as e:
        result["error"] = str(e)
        result["issues"].append(str(e))
    return result


def main():
    if len(sys.argv) < 3:
        print(json.dumps({
            "pass": False,
            "error": "用法: verify.py <file_path> <expected_year> [expected_region] [expected_subject]"
        }, ensure_ascii=False))
        sys.exit(1)
    
    file_path = sys.argv[1]
    year = sys.argv[2]
    region = sys.argv[3] if len(sys.argv) > 3 else ""
    subject = sys.argv[4] if len(sys.argv) > 4 else "英语"
    
    result = verify_file(file_path, year, region, subject)
    print(json.dumps(result, ensure_ascii=False, indent=2))
    
    # 返回非零退出码表示验证失败
    sys.exit(0 if result["pass"] else 1)


if __name__ == "__main__":
    main()


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