基于 SOC 职业分类
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/CSlawyer1985/legal-skillhub --skill court-document-downloader命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
88查企业风险查询与分析工具 —— 专注于中国大陆企业的法务风险查询与分析,覆盖经营异常、行政处罚、监管措施、被执行人、失信被执行人、司法案件等风险类型。支持公司名称、统一社会信用代码、companyId 三种输入方式,自动完成风险查询并输出结构化风险分析总结。 触发场景:仅当用户意图为查询企业风险/法务风险时触发。包括但不限于:企业风险查询、公司风险、经营异常、行政处罚、监管措施、被执行人、失信被执行人、司法风险、风险扫描、风险评估、企业合规审查、风险报告、XX公司有什么风险、XX公司安全吗、XX公司有没有处罚、XX公司被执行过吗。 不触发场景:纯粹的企业信息查询(如查公司、搜企业、工商信息、注册资本、法人代表、股东结构、专利商标等)不应触发本技能,请使用其他 cha88 系列工具。
Manages Rule 30(b)(6) corporate representative deposition workflows — drafting notice topics with reasonable particularity, building examination outlines, defending designees, handling objections, and preserving binding admissions for summary judgment or trial. Use when drafting or responding to 30(b)(6) notices, selecting and preparing designees, building topic-by-topic outlines, or triaging scope and privilege disputes. Trigger keywords: 30(b)(6), corporate representative deposition, topic list, designee, notice analysis, deposition objections, corporate admissions.
Guides taking and defending Rule 30(b)(6) corporate representative depositions. Drafts topic lists with reasonable particularity, builds examination outlines for binding corporate admissions, analyzes noticed topics for objections, and prepares designees. Use when drafting 30(b)(6) notices, preparing corporate deposition topics, selecting or preparing designees, or defending corporate representative depositions.
| name | court-document-downloader |
| description | 从人民法院电子送达平台(zxfw.court.gov.cn)下载传票/判决书等文书PDF,自动归档到本地案件文件夹,解析文书内容告知用户,设置开庭提醒(含日历集成),自动计算上诉期限 |
| category | legal |
| tags | ["court","download","document","legal","chinese","pdf"] |
当收到来自人民法院电子送达平台(zxfw.court.gov.cn)的文书送达时使用。支持两种触发方式:
方式一:粘贴短信原文
收到法院短信,内容如下:
【xx市人民法院】某某,您好!您有(2025)苏0981民初1234号案件文书送达,请点击链接查收:https://zxfw.court.gov.cn/zxfw/#/pagesAjkj/app/wssd/index?qdbh=DEMO1&sdbh=DEMO2&sdsin=DEMO3
方式二:直接发送送达链接
用户直接发送以 https://zxfw.court.gov.cn/zxfw/#/pagesAjkj/app/wssd/index?qdbh=... 开头的链接。此时跳过短信文本解析,直接从 URL 提取参数进入下载流程。
自动触发规则(无需手动加载): 当用户发送包含以下关键词时,自动加载本 skill:
zxfw.court.gov.cn此步骤在每次执行前都必须检查。配置保存在用户本地
~/.config/court-document-downloader/config.json,不随 skill 文件分发。
执行检查:
CONFIG_FILE="$HOME/.config/court-document-downloader/config.json"
python3 -c "
import json, os, sys
config_path = os.path.expanduser('~/.config/court-document-downloader/config.json')
# 情况一:配置文件不存在
if not os.path.exists(config_path):
print('NOT_CONFIGURED')
sys.exit(0)
# 情况二:配置文件存在但解析失败
try:
with open(config_path) as f:
cfg = json.load(f)
except Exception:
print('CONFIG_BROKEN')
sys.exit(0)
# 情况三:缺少必需字段(旧版残留的不完整配置)
required_fields = ['work_directory', 'archive_mode', 'calendar_name', 'default_reminders']
missing = [f for f in required_fields if f not in cfg]
if missing:
print('CONFIG_INCOMPLETE')
print('缺失字段: ' + ', '.join(missing))
sys.exit(0)
# 情况四:配置完整,输出工作目录
print(os.path.expanduser(cfg['work_directory']))
" 2>/dev/null
输出含义:
NOT_CONFIGURED — 配置文件不存在,进入首次配置引导CONFIG_BROKEN — 配置文件存在但损坏(JSON 解析失败),提示用户后进入配置引导CONFIG_INCOMPLETE — 配置文件存在但不完整(旧版残留),提示用户后进入配置引导如果输出 NOT_CONFIGURED、CONFIG_BROKEN 或 CONFIG_INCOMPLETE:必须立即停止后续流程,进入首次配置引导。
⚠️ 对于
CONFIG_BROKEN和CONFIG_INCOMPLETE,先告知用户"检测到旧版不完整配置,需要重新配置",然后执行rm "$HOME/.config/court-document-downloader/config.json"删除旧文件,再进入下方配置引导。
⚠️ 关键:不要替用户做决定。每一轮交互都必须等待用户明确回复后才能继续。绝对不能自动选择默认值跳过交互。
向用户展示以下内容,然后停止,等待用户回复:
⚠️ 执行到此步骤时必须停下来,把下面的内容展示给用户,然后等待用户的实际回复。不允许自行假设用户的选择,不允许自动填入默认值。
⚠️ 首次使用,需要设置诉讼文书的保存位置。
文书将保存到这个文件夹中,建议选择一个方便查找的持久化位置。
请选择保存路径:
1. 保存到桌面(默认)— ~/Desktop/诉讼案件/
2. 自定义路径 — 回复一个本地文件夹路径
示例:
回复 "1" 或 "默认" → 保存到 ~/Desktop/诉讼案件/
回复 "~/Documents/诉讼案件" → 保存到文稿目录
回复 "/Volumes/移动硬盘/诉讼案件" → 保存到外接硬盘
同时请选择归档方式:
A. 按年份分层(默认)— 自动创建 2026/、2027/ 等年份子文件夹
B. 不分年份 — 所有案件文件夹直接放在工作目录下
回复格式:路径选择 + 归档方式,如 "1 A" 或 "~/Documents/诉讼案件 B"
仅回复路径则默认使用按年份分层(A)。
在用户回复之前,不得执行任何后续步骤。
收到用户回复后,解析出路径和归档方式。先不要保存,向用户展示确认信息,然后停止,等待用户确认:
📋 请确认你的配置:
📁 保存路径:{解析出的完整路径}
📂 归档方式:{按年份分层 / 不分年份}
📅 日历提醒:Apple Calendar「个人」日历
⏰ 提醒时间:开庭前 7 天 + 开庭前 2 天
📂 文件夹结构预览:
{路径}/
├── 2026/ ← 按年份分层时才有此层
│ └── {原告}诉{被告}{案由}/
│ └── {文书名}_20260711收.pdf
└── .archive/ ← 归档记录
└── 20260711_143025_1234.json
确认无误请回复「确认」或「Y」
如需修改路径请直接回复新路径
如需修改日历名称或提醒设置请回复「高级配置」
在用户确认之前,不得保存配置。
向用户展示:
⚙️ 高级配置(可直接回复修改项,格式:设置名=值,多项用逗号分隔):
日历名称 当前:个人 例:日历名称=工作
提醒1 当前:提前7天 例:提醒1=10(天)
提醒2 当前:提前2天 例:提醒2=1(天)
回复示例:日历名称=工作,提醒1=10,提醒2=3
回复「确认」则使用当前设置完成配置
收到用户的高级配置修改后,更新对应值,重新展示第二轮确认信息。
mkdir -p ~/.config/court-document-downloader
# 以下变量已从前述交互中解析获得:
# WORK_DIR_INPUT — 用户确认的路径(如 ~/Desktop/诉讼案件)
# ARCHIVE_MODE — "by_year" 或 "flat"
# CALENDAR_NAME — 日历名称(默认 "个人")
# REMINDER_1_DAYS — 第一个提醒提前天数(默认 7)
# REMINDER_2_DAYS — 第二个提醒提前天数(默认 2)
# 展开路径中的 ~ 为 $HOME
WORK_DIR_EXPANDED=$(python3 -c "import os; print(os.path.expanduser('$WORK_DIR_INPUT'))")
# 创建工作目录(包括 .archive 子目录)
mkdir -p "$WORK_DIR_EXPANDED/.archive"
# 写入完整配置
cat > ~/.config/court-document-downloader/config.json << EOF
{
"work_directory": "$WORK_DIR_INPUT",
"archive_mode": "$ARCHIVE_MODE",
"archive_subdirectory": ".archive",
"calendar_name": "$CALENDAR_NAME",
"default_reminders": [
{"days_before": $REMINDER_1_DAYS, "description": "提前${REMINDER_1_DAYS}天"},
{"days_before": $REMINDER_2_DAYS, "description": "提前${REMINDER_2_DAYS}天"}
]
}
EOF
echo "✅ 配置已保存"
echo "✅ 工作目录已创建: $WORK_DIR_EXPANDED"
✅ 配置完成!
📁 保存路径:{完整路径}
📂 归档方式:{按年份分层 / 不分年份}
📅 日历名称:{日历名}
⏰ 开庭提醒:提前 {N} 天 + 提前 {M} 天
📝 配置文件:~/.config/court-document-downloader/config.json
如日后需要修改配置,可直接编辑该文件,或删除它重新触发配置引导。
配置完成后,重新读取配置并继续执行下方 Step 1-12。
CONFIG_FILE="$HOME/.config/court-document-downloader/config.json"
WORK_DIR=$(python3 -c "
import json, os
with open('$CONFIG_FILE') as f:
path = json.load(f)['work_directory']
print(os.path.expanduser(path))
")
echo "✅ 工作目录: $WORK_DIR"
完整短信文本:包含法院签名(如 【xx法院】)+ 正文 + 链接 → 进入 Step 2 短信解析
纯链接:用户直接发送送达 URL → 跳过 Step 2 的短信解析部分,直接从 URL 提取 qdbh、sdbh、sdsin 参数,进入 Step 3 下载。案号、当事人等信息在下载文书后从文书内容中提取。
a) 短信类型分类:根据关键词判断
| 类型 | 特征 | 含下载链接 | 处理方式 |
|---|---|---|---|
| 文书送达 | 含送达平台链接 + 案号 | 是 | 下载文书并归档到案件目录 |
| 立案通知 | 含"已立案"等关键词 | 可能有 | 展示解析结果 |
| 信息通知 | 无链接,纯信息 | 否 | 展示解析结果 |
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 链接,提取 qdbh、sdbh、sdsin 三个参数
e) 发送时间提取:优先从后续 API 响应的 dt_cjsj 字段提取;其次从短信网关时间匹配(发送:YYYY-MM-DD HH:mm 格式)
输出格式(向用户展示):
📋 短信解析结果:
- 类型:文书送达
- 案号:(2025)苏0981民初1234号
- 当事人:某某、xx有限公司
- 法院:xx市人民法院
- 下载链接:已提取(zxfw.court.gov.cn)
优先使用方案一(API 直连),失败后降级到方案二(浏览器)。严格串行,当前方案成功即停止,不并行尝试。
API 失败判断条件(满足任一即视为失败,降级到方案二):
data 字段为 null 或空数组 []data 字段不存在直接调用 zxfw 后端 API 获取文书列表和 OSS 下载链接,再用 curl 批量下载 PDF。
API 信息:
POST https://zxfw.court.gov.cn/yzw/yzw-zxfw-sdfw/api/v1/sdfw/getWsListBySdbhNewapplication/json{ "qdbh": "xxx", "sdbh": "xxx", "sdsin": "xxx" }(从短信 URL 提取)data[].c_wsmc(文书名称)、data[].wjlj(OSS 签名下载链接)、data[].c_fymc(法院名称)、data[].c_wsbh(文书编号)、data[].dt_cjsj(送达时间)# 0. WORK_DIR 已在前置步骤读取,直接使用
# 确认变量存在
[ -z "$WORK_DIR" ] && { echo "错误:WORK_DIR 未设置"; exit 1; }
# 1. 从短信 URL 提取参数
qdbh="DEMO_qdbh_value"
sdbh="DEMO_sdbh_value"
sdsin="DEMO_sdsin_value"
# 2. 调用 API 获取文书列表
mkdir -p /tmp/court-sms-staging/
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\"}")
# 3. 解析文书列表,逐个下载 PDF
echo "$resp" | python3 -c "
import json, sys, urllib.parse, subprocess
data = json.load(sys.stdin)
for doc in data.get('data', []):
name = doc.get('c_wsmc', '未知文书')
url = doc.get('wjlj', '')
if url:
# URL 解码文件名
safe_name = name.replace('/', '_').replace(':', '_')
subprocess.run(['curl', '-sL', '-o', f'/tmp/court-sms-staging/{safe_name}.pdf', url,
'-H', 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
'-H', 'Referer: https://zxfw.court.gov.cn/'])
"
# 4. 验证下载结果
ls -lh /tmp/court-sms-staging/*.pdf
# 5. 记录 API 响应(用于后续归档)
echo "$resp" > /tmp/court-sms-staging/_api_response.json
注意:OSS 签名 URL 有过期时间(约 1 小时),获取后应尽快下载。
dt_cjsj字段为送达记录创建时间,可用于后续上诉期限计算。
当方案一 API 不可用时,使用浏览器自动化。
⚠️ 浏览器操作需先加载
agent-browserskill。以下为操作步骤描述,具体工具调用方式以 agent-browser skill 的接口为准。
操作流程:
页面有两个区域:
⚠️ 一个送达链接通常包含多份文书! 务必先扫描侧边栏,确认本次送达共有多少份文书。
依次点击侧边栏每份文书,获取其 OSS URL:
对侧边栏中每个文书元素,执行:
document.querySelector('iframe')?.src — 获取 iframe 的 srciframe 的 src 结构:
https://zxfw.court.gov.cn/zxfw/static/pdfjs/web/viewer.html?file=https%3A%2F%2Fzxfy2-oss.oss-cn-north-2-gov-1.aliyuncs.com%2Fwssdclxz%2F{date}%2F{time}%2F{sdbh}%2F{file_hash}%2F{filename}%3FExpires%3D...%26Signature%3D...
其中 file= 后的内容(URL 解码 1 次)就是可直接下载的 OSS PDF 地址。
用 curl 批量下载到 /tmp/court-sms-staging/:
curl -L -o "/tmp/court-sms-staging/{文件名}.pdf" \
"实际PDF下载URL" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
-H "Referer: https://zxfw.court.gov.cn/"
为什么优先用 API? API 方案完全无头、不需要浏览器、速度快,一次性获取所有文书列表和下载链接,避免逐个点击侧边栏提取 iframe src。
先读取归档模式配置:
ARCHIVE_MODE=$(python3 -c "
import json
with open('$HOME/.config/court-document-downloader/config.json') as f:
print(json.load(f).get('archive_mode', 'by_year'))
")
模式一:archive_mode = "by_year"(默认,按年份分层)
年份按下载日期(当前日期)确定,不从案号解析。
${WORK_DIR}/${WORK_DIR}/{年份}/{案件文件夹}/${WORK_DIR}/2026/⚠️ ⚠️ ⚠️ 即使案号为 (2025)苏1002民初XX号,只要在 2026 年下载,就归入 2026 文件夹
模式二:archive_mode = "flat"(不分年份)
${WORK_DIR}/${WORK_DIR}/{案件文件夹}/⚠️ 核心原则:先搜后建。必须先在目标目录下搜索是否已有包含原被告名的文件夹,有则直接复用,无则才新建。绝不允许跳过搜索直接创建新文件夹。
if [ "$ARCHIVE_MODE" = "flat" ]; then
SEARCH_DIR="${WORK_DIR}"
else
SEARCH_DIR="${WORK_DIR}/$(date +%Y)"
fi
来源一(优先):从 API 返回或 PDF 文件名提取
如果 PDF 文件名包含当事人公司名(如 民事传票(某某建设工程有限公司).pdf),或 API 返回的法院信息中包含当事人信息,直接使用。
来源二(回退):从 PDF 文件内容解码提取
当文件名仅含案号时,解码 PDF 正文提取原告和被告信息。(详见 Step 7 的 CID 字体解码章节)
提取到原被告名后,必须执行以下搜索代码,在目标目录下查找匹配的已有文件夹:
# PLAINTIFF 和 DEFENDANT 已从 5.2 提取获得
# SEARCH_DIR 已从 5.1 确定
MATCHED_DIR=$(python3 -c "
import os, sys, re
search_dir = '$SEARCH_DIR'
plaintiff = '''$PLAINTIFF''' # 原告名
defendant = '''$DEFENDANT''' # 被告名
if not os.path.exists(search_dir):
print('')
sys.exit(0)
# 列出所有案件文件夹(排除 .archive 等隐藏目录)
existing_folders = [d for d in os.listdir(search_dir)
if os.path.isdir(os.path.join(search_dir, d)) and not d.startswith('.')]
if not existing_folders:
print('')
sys.exit(0)
print('现有文件夹:', existing_folders, file=sys.stderr)
# 去掉公司后缀,提取核心关键词
def strip_suffix(name):
if not name:
return ''
for suffix in ['股份有限公司', '有限责任公司', '有限公司', '集团']:
if name.endswith(suffix):
return name[:-len(suffix)]
return name
# 去掉地域前缀(省份、城市等),生成变体
# 例如:「江苏天下无敌公司」→「天下无敌公司」
# 「南京市某某建设工程」→「某某建设工程」
def strip_geo_prefix(name):
if not name:
return ''
variants = [name]
# 省份前缀
provinces = ['江苏', '浙江', '广东', '北京', '上海', '天津', '重庆',
'山东', '河南', '河北', '湖北', '湖南', '四川', '福建',
'安徽', '江西', '辽宁', '吉林', '黑龙江', '山西', '陕西',
'云南', '贵州', '甘肃', '青海', '海南', '内蒙古', '新疆',
'西藏', '广西', '宁夏', '香港', '澳门', '台湾']
for prov in provinces:
if name.startswith(prov):
variants.append(name[len(prov):])
break
# 「XX市」前缀
m = re.match(r'([\u4e00-\u9fa5]{2,4}市)', name)
if m:
variants.append(name[len(m.group(1)):])
# 「XX省XX市」组合前缀
m = re.match(r'([\u4e00-\u9fa5]{2,4}省[\u4e00-\u9fa5]{2,4}市)', name)
if m:
variants.append(name[len(m.group(1)):])
# 「XX市XX区」组合前缀
m = re.match(r'([\u4e00-\u9fa5]{2,4}市[\u4e00-\u9fa5]{2,4}区)', name)
if m:
variants.append(name[len(m.group(1)):])
return variants
# 生成一个当事人名的所有匹配变体
def get_variants(full_name):
if not full_name:
return []
variants = set()
# 全名本身
variants.add(full_name)
# 去公司后缀
core = strip_suffix(full_name)
if core:
variants.add(core)
# 去地域前缀(从 core)
for v in strip_geo_prefix(core):
if len(v) >= 2:
variants.add(v)
# 去地域前缀(从 full_name)
for v in strip_geo_prefix(full_name):
if len(v) >= 2:
stripped = strip_suffix(v)
variants.add(v)
if stripped:
variants.add(stripped)
return variants
def name_matches(folder, full_name):
\"\"\"判断文件夹名是否包含当事人名的任意变体\"\"\"
if not full_name:
return False
for variant in get_variants(full_name):
# 变体长度 >= 3 才用于匹配,避免过短导致误匹配
if len(variant) >= 3 and variant in folder:
return True
return False
# 匹配规则:原被告名必须都出现在文件夹名中(全称/简称/去地域前缀均可)
# 只命中一方不算匹配
best_match = ''
best_score = 0
for folder in existing_folders:
p_hit = name_matches(folder, plaintiff)
d_hit = name_matches(folder, defendant)
# 必须原被告都命中才算匹配
if not (p_hit and d_hit):
continue
# 评分用于多个匹配时选最优(全名命中优于简称命中)
score = 0
if plaintiff and plaintiff in folder:
score += 2
elif plaintiff and strip_suffix(plaintiff) in folder:
score += 1
if defendant and defendant in folder:
score += 2
elif defendant and strip_suffix(defendant) in folder:
score += 1
if score > best_score:
best_score = score
best_match = folder
if best_match:
print(os.path.join(search_dir, best_match))
else:
print('')
" 2>&1)
[ -n ] && [ -d ];
TARGET_DIR=
匹配规则(原被告必须同时命中):
对每个当事人名,skill 会生成以下变体用于匹配:
江苏天下无敌建设工程有限公司)江苏天下无敌建设工程)天下无敌建设工程有限公司、天下无敌建设工程)只要文件夹名包含任意一个变体(变体长度 ≥ 3 字符),即视为该方命中。
| 条件 | 说明 |
|---|---|
| 原告命中 | 文件夹名包含原告的任意变体(全名 / 去后缀 / 去地域前缀) |
| 被告命中 | 同上,文件夹名包含被告的任意变体 |
| 匹配成功 | 原告命中 且 被告命中 → 复用该文件夹 |
| 匹配失败 | 只命中一方、或都未命中 → 新建文件夹 |
⚠️ 只有原被告名都出现在文件夹名中才算匹配。只命中原告或只命中被告不算。
示例:
已有文件夹 天下无敌建设工程诉某某科技合同纠纷
新文书原告=江苏天下无敌建设工程有限公司,被告=某某科技有限公司
原告变体含"天下无敌建设工程" → 文件夹命中 ✓ + 被告变体含"某某科技" → 文件夹命中 ✓ → 匹配成功,复用
已有文件夹 某某建设工程诉某某科技合同纠纷
新文书原告=某某建设工程有限公司,被告=张某
原告命中 ✓ 但被告"张某"未命中 ✗ → 匹配失败,新建文件夹
只有当 MATCHED_DIR 为空时,才根据已提取的原告名、被告名和案由创建新文件夹。
# 仅在未匹配到已有文件夹时执行
if [ -z "$MATCHED_DIR" ] || [ ! -d "$MATCHED_DIR" ]; then
CASE_FOLDER="{原告名}诉{被告名}{案由}"
TARGET_DIR="${SEARCH_DIR}/${CASE_FOLDER}"
mkdir -p "$TARGET_DIR"
echo "✅ 已创建新案件文件夹: $TARGET_DIR"
fi
文件夹命名格式: {原告名}诉{被告名}{案由}
示例:
某某餐饮服务有限公司诉某某合同纠纷某某诉某某房屋租赁合同纠纷某某建设工程有限公司诉某某建设施工合同纠纷案由提取优先级:
TARGET_DIR已在 Step 5 中确定(要么匹配到已有文件夹,要么新建)。此处直接使用,不再重新计算。
# TARGET_DIR 已在 Step 5 确定(匹配已有文件夹 或 新建文件夹)
# 确保目标目录存在(Step 5.4 新建时已 mkdir,匹配到时可能已存在)
mkdir -p "$TARGET_DIR"
# 使用 mv 移动文件(而非 cp 复制),避免临时文件残留
mv "/tmp/court-sms-staging/{文件名}.pdf" \
"${TARGET_DIR}/{文书标题}({当事人+案由})_{YYYYMMDD}收.pdf"
文件命名格式: {文书标题}({当事人+案由})_{YYYYMMDD}收.pdf
示例:
传票(某某建设工程有限公司诉某某建设施工合同纠纷)_20260610收.pdf民事判决书(某某与某某合同纠纷)_20260610收.pdf命名规则:
文书标题:优先使用 API 返回的 c_wsmc,其次从 PDF 内容提取,最后回退为原始文件名当事人+案由:从文书内容提取,取原告+被告+案由的核心信息YYYYMMDD:下载日期< > : " | ? * \ /_2 后缀下载并归档后,使用 pymupdf 提取 PDF 文字内容:
# 使用 pymupdf 提取 PDF 文字(如未安装则安装到用户级,不污染全局环境)
python3 -c "import pymupdf" 2>/dev/null || python3 -m pip install --user pymupdf -q
python3 -c "
import pymupdf
doc = pymupdf.open('/path/to/file.pdf')
for page in doc:
print(page.get_text())
"
如果是传票,重点提取以下信息:
如果是判决书/裁定书,重点提取:
CID 字体编码 PDF 的文字提取方法:
如果 pymupdf 输出为空或乱码,说明是 CID 字体编码的复杂 PDF(WPS 生成),需手动解码 ToUnicode CMap:
import re, zlib
# 1. 读取 PDF 文件
with open('/tmp/文书.pdf', 'rb') as f:
data = f.read()
# 2. 提取指定编号的 PDF 对象(FlateDecode 解压)
def extract_object(data, obj_num):
"""提取 PDF 中指定编号的对象(FlateDecode 解压)"""
pattern = rb'%d\s+0\s+obj\s*(.*?)\s*endobj' % obj_num
match = re.search(pattern, data, re.S)
if not match:
return None
stream = match.group(1).split(b'stream')[1].split(b'endstream')[0].strip()
return zlib.decompress(stream)
def find_tounicode_refs(data):
"""找到 PDF 中所有 ToUnicode CMap 的引用对象编号"""
refs = []
for m in re.finditer(rb'/ToUnicode\s+(\d+)\s+0\s+R', data):
refs.append(int(m.group(1)))
return refs
def extract_page_content_streams(data):
"""提取所有页面的 Content Stream 并解压"""
# 找到所有页面对象编号
page_refs = [int(m.group(1)) for m in re.finditer(rb'(\d+)\s+0\s+obj\s*.*?/Type\s*/Page\b', data, re.S)]
streams = []
for page_num page_refs:
obj_data = extract_object(data, page_num)
obj_data :
pattern = % page_num
= re.search(pattern, data, re.S)
:
contents_match = re.search(, .group())
contents_match:
obj_data = extract_object(data, (contents_match.group()))
obj_data:
streams.append(obj_data)
streams
cmap = {}
ref find_tounicode_refs(data):
cmap_data = extract_object(data, ref)
cmap_data:
m re.finditer(, cmap_data.decode()):
cmap[(m.group(), )] = ((m.group(), ))
decoded_text =
page extract_page_content_streams(data):
text = page.decode()
segments = []
cid_hex re.findall(, text):
cid = (cid_hex, )
cid cmap:
segments.append(cmap[cid])
:
<= cid <= :
segments.append(( + cid - ))
decoded_text += .join(segments)
plaintiff_match = re.search(, decoded_text)
defendant_match = re.search(, decoded_text)
plaintiff = plaintiff_match.group().strip() plaintiff_match
defendant = defendant_match.group().strip() defendant_match
判断逻辑: 从文书正文中提取关键词判断文书类型。
传票 → 自动设置开庭提醒(无需询问用户)提醒方式:仅 Apple Calendar(macOS)
Apple Calendar 在 macOS 上可直接用 AppleScript 操作,将开庭事件添加至 iCloud 日历,自动设置地点、备注和多重提醒。
从配置读取日历名称和提醒设置:
# 读取配置中的日历名称和提醒设置
CONFIG_FILE="$HOME/.config/court-document-downloader/config.json"
CALENDAR_NAME=$(python3 -c "
import json
with open('$CONFIG_FILE') as f:
print(json.load(f).get('calendar_name', '个人'))
")
REMINDERS_JSON=$(python3 -c "
import json
with open('$CONFIG_FILE') as f:
reminders = json.load(f).get('default_reminders', [{'days_before': 7}, {'days_before': 2}])
print(json.dumps([r['days_before'] for r in reminders]))
")
echo "日历: $CALENDAR_NAME, 提醒天数: $REMINDERS_JSON"
典型事件参数:
开庭 - {案号} {案由}default_reminders 读取(默认提前 7 天 + 提前 2 天)AppleScript 实现:
⚠️ 关键坑:Apple Calendar 的
trigger interval单位是分钟,不是秒!
tell application "Calendar"
-- 日历名称从配置读取,默认 "个人"
set theCalendar to calendar "个人"
set eventTitle to "开庭 - (2026)苏1002民初31号 合同纠纷"
set eventLocation to "某某市某某区人民法院 · 第四法庭(地址)"
set eventDescription to "案号:...
案由:...
当事人:...
承办法官:..."
set startDate to date "2026-05-18 14:30:00"
set endDate to date "2026-05-18 16:30:00"
tell theCalendar
set newEvent to make new event at end with properties {¬
summary:eventTitle, start date:startDate, end date:endDate, ¬
location:eventLocation, description:eventDescription}
-- 注意:trigger interval 单位是分钟!
-- 提醒天数从配置的 default_reminders 数组动态生成
-- 提前7天 = -10080 分钟(7 × 24 × 60)
make new display alarm at end of newEvent with properties {trigger interval:-7 * 24 * 60}
-- 提前2天 = -2880 分钟(2 × 24 × 60)
make new display alarm at end of newEvent with properties {trigger interval:-2 * 24 * 60}
end tell
end tell
每次处理完成后,在用户工作目录下的归档子目录中创建一条 JSON 记录,用于追溯。归档子目录名称从配置的 archive_subdirectory 字段读取(默认 .archive)。
# 读取归档子目录名称
ARCHIVE_SUBDIR=$(python3 -c "
import json
with open('$HOME/.config/court-document-downloader/config.json') as f:
print(json.load(f).get('archive_subdirectory', '.archive'))
")
ARCHIVE_DIR="${WORK_DIR}/${ARCHIVE_SUBDIR}"
mkdir -p "$ARCHIVE_DIR"
文件路径: ${ARCHIVE_DIR}/YYYYMMDD_HHMMSS_{案号后4位}.json
${WORK_DIR}从前置步骤的配置中读取,归档记录与案件文件保存在同一根目录下,便于备份和迁移。
JSON 结构:
{
"id": "20260610_143025_1234",
"timestamp": "2026-06-10T14:30:25+08:00",
"sms_raw": "【xx市人民法院】某某,您好!...",
"parsed": {
"type": "document_delivery",
"case_number": "(2025)苏0981民初1234号",
"parties": ["xx有限公司", "某某"],
"court": "xx市人民法院",
"case_reason": "合同纠纷"
},
"download": {
"source_url": "https://zxfw.court.gov.cn/zxfw/#/...",
"params": { "qdbh": "XX", "sdbh":
当识别到判决书或裁定书时,自动计算上诉截止日期。
上诉期限规则:
| 案件类型 | 上诉期限 |
|---|---|
| 民事一审判决 | 送达后 15 天 |
| 民事裁定 | 送达后 10 天 |
| 行政判决 | 送达后 15 天 |
| 刑事判决 | 送达后 10 天 |
| 刑事裁定 | 送达后 5 天 |
计算公式: 上诉截止日期 = 送达日期 + 上诉期限天数
送达日期来源:
dt_cjsj 字段(送达记录创建时间)使用以下四段式模板汇报处理结果:
✅ 文书归档完成:
- 案号:(2025)苏0981民初1234号
- 法院:xx市人民法院
- 当事人:原告 xx有限公司 / 被告 某某
- 案由:合同纠纷
- 文件数:N 份
- 归档位置:{案件目录}/
📄 文书清单:
1. 传票 传票
2. 应诉通知书 通知书
3. 起诉状(要素式) 起诉状
4. 举证通知书 通知书
...
⚠️ 已收到传票,请注意:
- 开庭时间:2026年4月15日(周三)14:30
- 开庭地点:xx市人民法院 第3法庭
- 审理程序:简易程序
- 日历提醒:已设置(提前7天 + 提前2天)
⏰ 上诉期限提醒:
- 文书类型:一审判决书
- 送达时间:2026年3月18日
- 上诉截止:2026年4月2日(周四)
- 剩余天数:xx 天
各部分触发条件:
归档失败时的汇报:
⚠️ 文书归档部分完成:
- 案号:(2025)苏0981民初1234号
- 法院:xx市人民法院
- 成功:N 份
- 失败:N 份
失败的文书:
- xxx.pdf(原因:下载超时)
请手动访问以下链接下载失败文书:
{原始链接}
无论归档成功还是部分成功,必须在汇报的同时,把本次送达的每一份文书 PDF 通过 present_files 工具发送到当前对话框中,使用户能在对话内直接预览和下载。这一步与桌面归档相互独立、互不影响。
mv 之后的最终路径),例如
${TARGET_DIR}/传票(某某诉某某合同纠纷)_20260711收.pdfpresent_files,按重要性排序(传票/判决书优先)注意:本步骤在 Step 12 清理临时目录之前执行,确保发送的是已归档到案件文件夹的正式文件,而非 /tmp 临时文件。
汇报完成后,清理临时下载目录:
rm -rf /tmp/court-sms-staging/
data 数组确认Expires 过期参数,如果下载失败可能已过期,需要重新调用 API 获取新的 URLfile= 参数值才能得到真实的 OSS 地址{原告名}诉{被告名}{案由} 格式命名{文书标题}({当事人+案由})_{YYYYMMDD}收.pdf,同名文件追加 _2 后缀/tmp/court-sms-staging/_api_response.json,供后续归档使用${WORK_DIR}/${ARCHIVE_SUBDIR}/(默认 .archive),便于追溯。${WORK_DIR} 为用户配置的工作目录dt_cjsj 作为送达日期~/.config/court-document-downloader/config.json,可随时修改或删除重新触发配置引导mv 移动文件后,Step 11 汇报完成后应执行 rm -rf /tmp/court-sms-staging/ 清理临时目录Apple Calendar display alarm 的 trigger interval 单位是分钟,不是秒!
这是一个非常容易踩坑的点:
| 如果写成(秒) | 实际上会被解释为 | 日历显示 |
|---|---|---|
-604800 (7×86400) | -604800 分钟 = 420 天 | 提前420天 |
-259200 (3×86400) | -259200 分钟 = 180 天 | 提前180天 |
-172800 (2×86400) | -172800 分钟 = 120 天 | 提前120天 |
正确的写法(用分钟计算):
| 想要的效果 | 正确值(分钟) | 公式 |
|---|---|---|
| 提前7天 | -10080 | -(7 × 24 × 60) |
| 提前3天 | -4320 | -(3 × 24 × 60) |
| 提前2天 | -2880 | -(2 × 24 × 60) |
| 提前1天 | -1440 | -(1 × 24 × 60) |
最佳实践:直接在 AppleScript 中写
-7 * 24 * 60这样的表达式,让系统去计算,避免手动算错。
-- 正确写法(分钟)
make new display alarm at end of newEvent with properties {trigger interval:-7 * 24 * 60} -- 提前7天
make new display alarm at end of newEvent with properties {trigger interval:-2 * 24 * 60} -- 提前2天
{文书标题}({当事人+案由})_{YYYYMMDD}收.pdf 格式default_reminders 读取)${WORK_DIR}/${ARCHIVE_SUBDIR}/(默认 .archive)/tmp/court-sms-staging/ 临时文件已清理(Step 12)present_files 将每份文书 PDF 同步发送到对话框(Step 11.5)