Add court-sms skill: Chinese court SMS document downloader

This skill automates processing of Chinese court SMS notifications:

- Auto-parses court delivery SMS (法院送达短信) to extract case number, court, parties
- Downloads legal documents from 3 Chinese court delivery platforms:
  - zxfw.court.gov.cn (全国法院统一送达平台)
  - sd.gdems.com (广东法院电子送达)
  - jysd.10102368.com (集约送达平台)
- Saves all documents to ~/Desktop/文书下载/{case_number}/ preserving original filenames
- Extracts key dates from summons (传票) and court hearing notices (开庭通知书)
- Optional: writes hearing dates to Feishu bitable via lark-cli

Includes:
- SKILL.md with detailed 5-step workflow
- Python script for automated document download
- SMS pattern reference file for parsing rules
This commit is contained in:
高澄 2026-05-31 08:42:01 +08:00
parent da20c92503
commit 4ed7c06fb1
3 changed files with 571 additions and 0 deletions

248
skills/court-sms/SKILL.md Normal file
View File

@ -0,0 +1,248 @@
---
name: court-sms
description: Process Chinese court SMS notifications (法院短信). Triggers when message contains court name + case number + delivery platform link (zxfw.court.gov.cn / sd.gdems.com / jysd.10102368.com). Parses SMS, downloads legal documents to ~/Desktop/文书下载/{case_number}/, extracts key dates from summons/court notices. Optional: write hearing dates to Feishu bitable.
---
# 法院短信识别与文书下载
## 核心原则(强制执行)
1. **触发即调用**:消息含法院名称 + 案号 + 送达平台链接(zxfw.court.gov.cn / sd.gdems.com / jysd.10102368.com)→ 立即调用本 skill,不绕路
2. **下载到桌面**:所有文件下载到 `~/Desktop/文书下载/{案号}/`,不放到案件目录
3. **原样保存,同名去重**:保留 API 返回的原始文件名,不删除、不整理、不合并。如同名文件已存在,追加 `_2`、`_3` 后缀防止覆盖
4. **只提取关键文书**:仅对传票和开庭/出庭通知书提取关键信息,其他文书不读不总结
## 功能概述
处理法院短信的完整流程:**粘贴短信 → 解析内容 → 下载文书 → 原样保存到桌面 → 仅提取传票/开庭通知关键日期 → 可选写入飞书**。
支持两种触发方式:
**方式一:粘贴短信原文**
```text
收到法院短信,内容如下:
【xx市人民法院】张三,您好!您有(2025)苏0981民初1234号案件文书送达,请点击链接查收:https://zxfw.court.gov.cn/zxfw/#/pagesAjkj/app/wssd/index?qdbh=DEMO1&sdbh=DEMO2&sdsin=DEMO3
```
**方式二:直接发送送达链接**
用户直接粘贴送达 URL,跳过短信文本解析,直接从 URL 提取 `qdbh`、`sdbh`、`sdsin` 参数。
```text
https://zxfw.court.gov.cn/zxfw/#/pagesAjkj/app/wssd/index?qdbh=xxx&sdbh=xxx&sdsin=xxx
```
## 短信类型分类
| 类型 | 特征 | 含下载链接 | 处理方式 |
| --- | --- | --- | --- |
| 文书送达 | 含送达平台链接 + 案号 | 是 | 下载文书并归档 |
| 立案通知 | 含"已立案"等关键词 | 可能有 | 展示解析结果 |
| 信息通知 | 无链接,纯信息 | 否 | 展示解析结果 |
**支持的送达平台**:`zxfw.court.gov.cn`(全国)、`sd.gdems.com`(广东)、`jysd.10102368.com`(集约送达)。详见 `references/sms-patterns.json`。
## 依赖
| 依赖 | 用途 | 安装方式 |
|------|------|----------|
| Python 3.9+ | 运行下载脚本 | 系统自带 |
| `requests` | HTTP 请求 | `pip install requests` |
| `pdftotext` | PDF 文本提取(仅飞书功能需要) | `brew install poppler` |
---
## 工作流(五步)
### 第一步:输入解析
1. 读取 `${CLAUDE_SKILL_DIR}/references/sms-patterns.json` 作为解析参考
2. **判断输入类型**:
- **完整短信**:包含法院签名(如 `【xx法院】`)+ 正文 + 链接 → 完整解析流程
- **纯链接**:用户直接发送送达 URL → 跳过短信文本解析,直接从 URL 提取参数,进入第三步下载
3. 对用户粘贴的短信文本进行分析(纯链接输入跳过此步):
**a) 短信分类**:根据关键词判断类型
- 文书送达:包含 zxfw.court.gov.cn 链接
- 立案通知:包含"已立案"、"立案通知"等
- 信息通知:其他
**b) 案号提取**:使用正则 `[((〔[]\d{4}[))〕]]` 匹配标准案号格式
标准案号格式示例:
- `(2025)苏0981民初1234号`
- `(2024)粤0604执保5678号`
- `〔2025〕京0105民初901号`
**c) 当事人提取**:从短信文本初步识别,最终以文书内容为准
- **注意**:短信中的称呼(如"张三,您好")仅为短信接收人,不作为案件当事人
- 公司名称:`xx有限责任公司`、`xx有限公司`、`xx股份有限公司`
- 诉讼对峙:`A与B`、`A诉B`、`原告A 被告B`
- 角色前缀:`原告:xxx`、`被告:xxx` 等
**d) 下载链接提取**:识别短信中的送达平台链接并提取参数
| 平台 | 域名 | 下载方式 | 提取参数 |
|------|------|----------|----------|
| 全国法院统一送达平台 | `zxfw.court.gov.cn` | API 直连 | qdbh, sdbh, sdsin |
| 广东法院电子送达 | `sd.gdems.com` | 浏览器自动化 | 路径中的送达标识码 |
| 集约送达平台 | `jysd.10102368.com` | 浏览器自动化 | key |
**输出格式**(向用户展示):
```text
📋 短信解析结果:
- 类型:文书送达
- 案号:(2025)苏0981民初1234号
- 当事人:张三、xx有限公司
- 下载链接:已提取(zxfw.court.gov.cn)
```
### 第二步:确定下载目录
固定下载到桌面:`~/Desktop/文书下载/{案号}/`
- `{案号}` 使用短信中提取的案号
- 目录不存在则自动创建
- **不扫描案件目录,不询问用户,直接使用此固定路径**
### 第三步:文书下载
> **直接调用 Python 脚本**,单次进程调用,30 秒内完成全部下载。
#### 执行命令
```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/download_sms_docs.py" "<完整短信原文>"
```
脚本自动完成:解析 URL → 提取参数 → 调用 API → 批量下载 → 输出 JSON 结果。
**输出 JSON 包含**:`output_dir`(下载目录)、`files`(文件列表)、`case_number`(案号)、`court`(法院)、`document_types`(文书类型列表)。
#### 失败兜底
脚本失败时(网络问题/API 不可用),降级方案:
```bash
qdbh="<从URL提取>"; sdbh="<从URL提取>"; sdsin="<从URL提取>"
resp=$(curl -s -X POST "https://zxfw.court.gov.cn/yzw/yzw-zxfw-sdfw/api/v1/sdfw/getWsListBySdbhNew" \
-H "Content-Type: application/json" \
-d "{\"qdbh\":\"$qdbh\",\"sdbh\":\"$sdbh\",\"sdsin\":\"$sdsin\"}")
echo "$resp" | python3 -c "
import json, sys, os, urllib.request
data = json.loads(sys.stdin.read())
outdir = os.path.expanduser('~/Desktop/文书下载/') + data['data'][0].get('c_fymc','')[:6] if data.get('data') else 'temp'
os.makedirs(outdir, exist_ok=True)
for d in data['data']:
name = d['c_wsmc'] + '.' + d['c_wjgs']
urllib.request.urlretrieve(d['wjlj'], outdir + '/' + name)
print(f'OK: {name}')
"
```
### 第四步:保存到桌面
1. **确定目标目录**:`~/Desktop/文书下载/{案号}/`
2. **保存文件**:将临时目录中的所有文件(PDF + MP4 等)复制到目标目录
3. **同名处理**:保留 API 返回的原始文件名;如同名文件已存在,追加 `_2`、`_3` 后缀(检测文件大小,大小相同则为真重复跳过,大小不同则为同名异内容保留)
4. **不做任何删减**:不删除文件、不重命名、不整理子目录、不合并 PDF
### 第五步:关键文书提取
> **仅提取传票和开庭/出庭通知书**,其他文书(举证通知、判决书、裁定书等)不读取、不总结。
#### 文书类型识别
| 文书类型 | 识别关键词 | 必须提取的信息 |
|---------|-----------|---------------|
| **传票** | 标题含"传票" | 开庭时间、地点、法庭、案号、案由 |
| **开庭通知书/出庭通知书** | 标题含"开庭通知"或"出庭通知" | 开庭/宣判时间、地点、法庭、案号 |
#### 提取方式
1. 扫描目标目录文件名,找到标题含"传票"或"开庭通知"或"出庭通知"的 PDF
2. 用 `pdftotext` 提取文本(前 500 字符即可)
3. 正则匹配日期时间、地点、案号等信息
4. 向用户展示关键日期信息
#### 向用户汇报
```text
📅 关键日期信息已提取:
┌─────────────────────────────────────────────────────────────┐
│ 案号:(2025)浙0105刑初789号 │
│ 法院:某市某区人民法院 │
│ 当事人:王某某 │
├─────────────────────────────────────────────────────────────┤
│ ⚠️ 2025-06-15 09:30 — 开庭审理 — 第三审判庭 │
└─────────────────────────────────────────────────────────────┘
```
---
## 可选:飞书多维表格集成
如需将开庭信息自动写入飞书多维表格,安装 `lark-cli`(`npm install -g @larksuiteoapi/lark-cli`)并配置。
写入飞书时,使用字段名(中文)而非 field_id,单选字段用选项名("高"/"中"/"低"),日期时间字段传毫秒时间戳:
```bash
APP_TOKEN="<YOUR_APP_TOKEN>"
TABLE_ID="<YOUR_TABLE_ID>"
timestamp=$(date -j -f "%Y-%m-%d %H:%M" "2025-06-15 09:30" "+%s")000
lark-cli api POST "/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records" \
--as user \
--data '{
"fields": {
"事项": "开庭审理(2025)苏0102民初5678号",
"任务内容": "某市某区人民法院 - 原告张三与被告李四劳务合同纠纷",
"开始时间": '"$timestamp"',
"地点": "第三审判庭",
"来源": "法院短信",
"优先级": "高"
}
}'
```
---
## 常见法院短信格式参考
### 文书送达短信
```text
【xx市人民法院】张三,您好!您有(2025)苏0981民初1234号案件文书送达,
请点击链接查收:
https://zxfw.court.gov.cn/zxfw/#/pagesAjkj/app/wssd/index?qdbh=DEMO1&sdbh=DEMO2&sdsin=DEMO3
如非本人操作请联系法院。
```
### 立案通知短信
```text
【xx市xx区人民法院】您好,您提交的立案材料已审核通过。
案号:(2025)京0105民初54321号
请及时缴纳诉讼费用。
```
### 开庭提醒短信
```text
【xx市xx区人民法院】提醒:您有(2025)苏0508民初567号案件,
定于2025年3月15日上午9:30在第3法庭开庭,请准时到庭。
```
## 故障排除
| 问题 | 解决方案 |
| --- | --- |
| 短信无法识别类型 | 展示原文,请用户确认类型后继续 |
| 案号提取失败 | 手动输入案号 |
| 当事人识别不准 | 提示用户确认/修正当事人列表 |
| 下载超时 | 检查网络连接,尝试重新执行 |
| 下载文件损坏 | 清理临时目录,重新尝试下载 |
| 桌面目录创建失败 | 检查磁盘空间和权限 |

View File

@ -0,0 +1,62 @@
{
"supported_platforms": [
{
"name": "全国法院统一送达平台",
"domain": "zxfw.court.gov.cn",
"download_method": "api",
"url_params": ["qdbh", "sdbh", "sdsin"],
"api_endpoint": "https://zxfw.court.gov.cn/yzw/yzw-zxfw-sdfw/api/v1/sdfw/getWsListBySdbhNew"
},
{
"name": "广东法院电子送达",
"domain": "sd.gdems.com",
"download_method": "browser",
"url_params": ["path_code"]
},
{
"name": "集约送达平台",
"domain": "jysd.10102368.com",
"download_method": "browser",
"url_params": ["key"]
}
],
"sms_types": {
"文书送达": {
"keywords": ["送达平台链接", "zxfw.court.gov.cn", "sd.gdems.com", "jysd.10102368.com"],
"has_download_link": true
},
"立案通知": {
"keywords": ["已立案", "立案通知", "立案材料已审核"],
"has_download_link": false
},
"信息通知": {
"keywords": [],
"has_download_link": false
}
},
"case_number_patterns": {
"standard": "[((〔[]\\d{4}[))〕]][\\u4e00-\\u9fa5]+\\d+号",
"examples": [
"(2025)苏0981民初1234号",
"(2024)粤0604执保5678号",
"〔2025〕京0105民初901号"
]
},
"document_types": {
"传票": {
"keywords": ["传票"],
"extract": ["开庭时间", "地点", "法庭", "案号", "案由"]
},
"开庭通知书": {
"keywords": ["开庭通知", "出庭通知"],
"extract": ["开庭时间", "宣判时间", "地点", "法庭", "案号"]
}
},
"party_extraction": {
"exclude_keywords": [
"人民法院", "中级人民法院", "高级人民法院", "最高人民法院",
"法官", "审判长", "审判员", "书记员", "陪审员",
"委托诉讼代理人", "法定代表人"
]
}
}

View File

@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""
短信链接文书下载脚本
从法院送达短信中提取链接,下载相关PDF文书
"""
import re
import json
import os
import sys
import requests
from pathlib import Path
from urllib.parse import urlparse, parse_qs
from datetime import datetime
def extract_url_from_sms(sms_content: str) -> str | None:
"""从短信内容中提取URL"""
url_pattern = r'https?://[^\s]+'
matches = re.findall(url_pattern, sms_content)
if matches:
return matches[0]
return None
def parse_url_params(url: str) -> dict | None:
"""解析URL中的参数(qdbh, sdbh, sdsin)"""
try:
parsed = urlparse(url)
# 参数可能在hash中,也可能在search中
if parsed.fragment and '?' in parsed.fragment:
# #/pagesAjkj/app/wssd/index?qdbh=...
query_part = parsed.fragment.split('?')[1]
params = parse_qs(query_part)
else:
params = parse_qs(parsed.query)
# 获取单个值(parse_qs返回列表)
result = {
'qdbh': params.get('qdbh', [None])[0],
'sdbh': params.get('sdbh', [None])[0],
'sdsin': params.get('sdsin', [None])[0]
}
if all(result.values()):
return result
return None
except Exception as e:
print(f"URL解析失败: {e}")
return None
def fetch_file_list(params: dict) -> list:
"""调用API获取文件列表"""
api_endpoint = "https://zxfw.court.gov.cn/yzw/yzw-zxfw-sdfw/api/v1/sdfw/getWsListBySdbhNew"
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
try:
response = requests.post(api_endpoint, json=params, headers=headers, timeout=30)
response.raise_for_status()
result = response.json()
if result.get('code') == 200 and isinstance(result.get('data'), list):
return result['data']
else:
print(f"API返回错误: {result.get('msg', '未知错误')}")
return []
except requests.RequestException as e:
print(f"API请求失败: {e}")
return []
def download_file(file_url: str, filename: str, output_dir: Path) -> bool:
"""下载单个文件,同名文件大小不同时加 _2 后缀"""
output_path = output_dir / filename
# Check if file exists and is different (true duplicate with same name)
if output_path.exists():
existing_size = output_path.stat().st_size
# We can't know the new file size before downloading, so download to temp
import tempfile
tmp_path = output_dir / f".tmp_{filename}"
try:
response = requests.get(file_url, timeout=60)
response.raise_for_status()
new_size = len(response.content)
if new_size == existing_size:
print(f" - 跳过: {filename} (已存在,大小相同)")
return True # same file, skip
# Different content, find a _N suffix
base, ext = os.path.splitext(filename)
n = 2
while output_path.exists():
new_name = f"{base}_{n}{ext}"
output_path = output_dir / new_name
# Check if this one also exists with same size
if output_path.exists() and output_path.stat().st_size == new_size:
print(f" - 跳过: {new_name} (已存在,大小相同)")
return True
n += 1
with open(output_path, 'wb') as f:
f.write(response.content)
print(f" ✓ 已下载: {output_path.name} (同名异内容,已加后缀)")
return True
except requests.RequestException as e:
print(f" ✗ 下载失败: {filename} - {e}")
return False
finally:
if tmp_path.exists():
tmp_path.unlink()
try:
response = requests.get(file_url, timeout=60)
response.raise_for_status()
with open(output_path, 'wb') as f:
f.write(response.content)
print(f" ✓ 已下载: {filename}")
return True
except requests.RequestException as e:
print(f" ✗ 下载失败: {filename} - {e}")
return False
def extract_case_number(sms_content: str) -> str | None:
"""从短信中提取案件号"""
# 匹配案件号格式如:(2026)浙0381执3963号
pattern = r'[\((]?\d{4}[\))]?[\u4e00-\u9fa5]+\d+[\u4e00-\u9fa5]*\d*号'
match = re.search(pattern, sms_content)
if match:
# 清理格式,移除括号
case_num = match.group()
case_num = re.sub(r'[\(()]', '', case_num)
return case_num
return None
def extract_court_name(sms_content: str) -> str | None:
"""从短信中提取法院名称"""
# 匹配格式如:【瑞安市人民法院】
pattern = r'【(.+人民法院|.+法院)】'
match = re.search(pattern, sms_content)
if match:
return match.group(1)
return None
def main(sms_content: str = None, output_base: Path = None) -> dict:
"""主函数,返回结构化JSON结果"""
# 如果没有提供短信内容,从命令行参数获取
if sms_content is None:
if len(sys.argv) < 2:
print("使用方式: python download_sms_docs.py <短信内容>")
print("或直接粘贴短信内容作为参数")
sys.exit(1)
sms_content = sys.argv[1]
# 默认输出目录
if output_base is None:
output_base = Path.home() / "Desktop" / "文书下载"
print("=" * 50)
print("短信链接文书下载工具")
print("=" * 50)
# 1. 提取URL
url = extract_url_from_sms(sms_content)
if not url:
print("错误: 未找到短信中的链接")
sys.exit(1)
print(f"提取到链接: {url}")
# 2. 解析参数
params = parse_url_params(url)
if not params:
print("错误: 无法解析链接参数")
sys.exit(1)
print(f"解析参数: qdbh={params['qdbh'][:8]}..., sdbh={params['sdbh'][:8]}...")
# 3. 提取案件号作为子目录名
case_number = extract_case_number(sms_content)
court_name = extract_court_name(sms_content)
if case_number:
output_dir = output_base / case_number
else:
output_dir = output_base / f"未知案件_{datetime.now().strftime('%Y%m%d%H%M%S')}"
# 创建输出目录
output_dir.mkdir(parents=True, exist_ok=True)
print(f"输出目录: {output_dir}")
# 4. 获取文件列表
print("\n正在获取文书列表...")
files = fetch_file_list(params)
if not files:
print("未获取到任何文书")
sys.exit(0)
print(f"获取到 {len(files)} 个文书:")
for f in files:
print(f" - {f.get('c_wsmc', '未命名')}.{f.get('c_wjgs', 'pdf')}")
# 5. 下载文件
print("\n开始下载...")
success_count = 0
fail_count = 0
downloaded_files = []
document_types = []
for i, file_info in enumerate(files, 1):
file_name = file_info.get('c_wsmc', f'文书_{i}')
file_ext = file_info.get('c_wjgs', 'pdf')
file_url = file_info.get('wjlj', '')
full_filename = f"{file_name}.{file_ext}"
print(f"\n[{i}/{len(files)}] {full_filename}")
if download_file(file_url, full_filename, output_dir):
success_count += 1
downloaded_files.append(str(output_dir / full_filename))
document_types.append(file_name)
else:
fail_count += 1
# 6. 总结
print("\n" + "=" * 50)
print(f"下载完成: 成功 {success_count}, 失败 {fail_count}")
print(f"文件位置: {output_dir}")
print("=" * 50)
# 7. 输出JSON供后续处理
result = {
"success": success_count > 0,
"output_dir": str(output_dir),
"files": downloaded_files,
"case_number": case_number,
"court": court_name,
"document_types": document_types,
"sms_content": sms_content
}
print("\n__JSON_OUTPUT__")
print(json.dumps(result, ensure_ascii=False))
print("__JSON_OUTPUT_END__")
return result
if __name__ == "__main__":
main()