@clawhub-yipng05-max-25f2ae72d2
飞书云文档修订工具。支持三种文档标注操作:高亮文字、添加下划线、插入批注(评论)。 **当以下情况时使用此 Skill**: (1) 需要对飞书文档中的文字添加高亮颜色 (2) 需要对飞书文档中的文字添加下划线 (3) 需要在飞书文档中插入批注/评论 (4) 用户说"帮我高亮这段"、"加个批注"、"给这个词加下划...
---
name: feishu-doc-review
description: |
飞书云文档修订工具。支持三种文档标注操作:高亮文字、添加下划线、插入批注(评论)。
**当以下情况时使用此 Skill**:
(1) 需要对飞书文档中的文字添加高亮颜色
(2) 需要对飞书文档中的文字添加下划线
(3) 需要在飞书文档中插入批注/评论
(4) 用户说"帮我高亮这段"、"加个批注"、"给这个词加下划线"、"标注一下"、"review一下文档"
(5) 审阅、修订飞书文档场景
---
# Feishu Doc Review(飞书文档修订)SKILL
## 三种操作
### 操作一:高亮文字
调用 `feishu_doc`(action: `color_text`),指定 block_id 和颜色。
**支持颜色值(背景高亮):**
- `yellow`:黄色高亮(最醒目,推荐)
- `green`:绿色高亮
- `blue`:蓝色高亮
- `red`:红色高亮
- `purple`:紫色高亮
- `grey`:灰色高亮
- `orange`:橙色高亮
**执行步骤:**
1. 用 `feishu_doc`(action: `list_blocks`)找到目标文字所在的 block_id
2. 用 `feishu_doc`(action: `color_text`)对该 block 设置背景色
### 操作二:下划线
调用 `feishu_doc`(action: `update_block`),在 text_element_style 中设置 `underline: true`。
**执行步骤:**
1. 用 `feishu_doc`(action: `get_block`)获取目标 block 的当前内容结构
2. 用 `feishu_doc`(action: `update_block`)修改样式,添加 underline
### 操作三:批注(评论)
调用飞书 Comment API,直接 POST 到文档评论接口。
**API 路径:**
```
POST https://open.feishu.cn/open-apis/drive/v1/files/{doc_token}/comments?file_type=docx
```
**鉴权:**
- 使用环境变量中的飞书 App 配置获取 tenant_access_token
- App ID: `cli_a92d5b4257391bcb`
- App Secret: 通过环境变量 `FEISHU_APP_SECRET` 读取(或直接用已知值)
**请求体结构:**
```json
{
"reply_list": {
"replies": [{
"content": {
"elements": [{
"type": "text_run",
"text_run": {
"text": "批注内容"
}
}]
}
}]
}
}
```
**执行脚本:**调用 `scripts/add_comment.sh`,传入 doc_token 和批注内容。
---
## 执行流程
### 用户说"帮我高亮XXX"时:
1. 确认目标文档 URL(提取 doc_token)
2. `list_blocks` 找到包含目标文字的 block
3. `color_text` 设置高亮颜色(默认 yellow)
4. 报告完成
### 用户说"帮我加批注:XXX"时:
1. 确认目标文档 URL(提取 doc_token)
2. 调用 `scripts/add_comment.sh {doc_token} "{批注内容}"`
3. 报告批注 ID
### 用户说"帮我对XXX加下划线"时:
1. 确认目标文档 URL
2. `list_blocks` + `get_block` 定位目标 block
3. `update_block` 修改 underline 样式
4. 报告完成
---
## 注意事项
- 批注是文档级别的,不锁定到特定段落(飞书 API 当前限制)
- 高亮和下划线操作针对整个 block,无法精确到 block 内的部分文字(飞书 API 限制)
- 操作前先确认 doc_token 正确
- 批注内容建议控制在 500 字以内
FILE:scripts/add_comment.sh
#!/bin/bash
# add_comment.sh — 向飞书文档添加批注
# 用法: ./add_comment.sh <doc_token> "<批注内容>"
DOC_TOKEN="$1"
COMMENT_TEXT="$2"
if [ -z "$DOC_TOKEN" ] || [ -z "$COMMENT_TEXT" ]; then
echo "用法: $0 <doc_token> <批注内容>"
exit 1
fi
APP_ID="cli_a92d5b4257391bcb"
APP_SECRET="-lo4II96JWo5W0DMjgYfZ1diOodp8xlnj"
# 获取 tenant_access_token
TOKEN=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
-H "Content-Type: application/json" \
-d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tenant_access_token'])")
if [ -z "$TOKEN" ]; then
echo "ERROR: 获取 token 失败"
exit 1
fi
# 添加批注
RESULT=$(curl -s -X POST \
"https://open.feishu.cn/open-apis/drive/v1/files/DOC_TOKEN/comments?file_type=docx" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"reply_list\": {
\"replies\": [{
\"content\": {
\"elements\": [{
\"type\": \"text_run\",
\"text_run\": {
\"text\": \"$COMMENT_TEXT\"
}
}]
}
}]
}
}")
CODE=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code','?'))")
if [ "$CODE" = "0" ]; then
COMMENT_ID=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['comment_id'])")
echo "✅ 批注添加成功,comment_id: $COMMENT_ID"
else
echo "❌ 批注添加失败: $RESULT"
exit 1
fi
工程化备课系统(Course-Prep-Auto-Flow v1.0)。把"靠灵感备课"变成"走流程出内容"。用户提供课程主题、受众画像、参考素材,系统自动完成:P1信息采集→P2骨架设计→P3素材提炼→P4内容填充→P5结构审查→P6配图规划→P7终局产出。适用于公开课、直播课、工作坊的备课准备。触发关键词:备...
--- name: course-prep-afp description: 工程化备课系统(Course-Prep-Auto-Flow v1.0)。把"靠灵感备课"变成"走流程出内容"。用户提供课程主题、受众画像、参考素材,系统自动完成:P1信息采集→P2骨架设计→P3素材提炼→P4内容填充→P5结构审查→P6配图规划→P7终局产出。适用于公开课、直播课、工作坊的备课准备。触发关键词:备课、公开课准备、课程设计、course-prep、备课AFP。 version: 1.0.0 --- # 备课系统(Course-Prep-Auto-Flow v1.0) ## 角色设定 你是一名专业的课程设计师兼内容工程师,擅长将零散素材转化为结构清晰、案例丰富的备课稿。工程化工作方式:每步有明确输入和输出,不越步执行,每步完成后等待确认。 ## 核心原则 - Pull模式:完成一步,主动汇报并等待确认才进入下一步 - 模块化:每章独立生成,出错只改该章 - 受众优先:所有内容对照受众画像检验 --- ## P1 信息采集【必须先完成】 收集三项必要输入(缺失项主动询问): 1. 课程主题 2. 受众画像(谁来听?基础如何?) 3. 参考素材(URL/文件路径/内容) 可选:课程时长(默认75分钟)、已有骨架、讲课风格 输出:表格确认"已收到"和"仍需补充" [STOP] 等待用户补充后发送"继续"进入P2。 --- ## P2 骨架设计 按"开场→是什么→为什么→怎么做→行动号召"递进逻辑设计章节骨架,每章含时间预算。 输出格式: ``` 章节结构(共X章,约XX分钟) 一、[章节名](X分钟)核心目标:xxx ``` [STOP] 等待用户确认骨架后进入P3。 --- ## P3 素材提炼 逐一处理每份素材,提炼:核心要点(3-5个)+ 建议插入章节位置。 PPT图片格式素材用vision识别。 [STOP] 全部素材处理完毕,输出"素材提炼汇总",等待"开始填充"进入P4。 --- ## P4 内容填充(逐章迭代) 每章输出:目标说明 + 核心内容(定义/案例/对比)+ 讲师提示 + 过渡句 每章完成后: ``` [正在处理:第X章 / 共X章] ……内容…… [等待确认后继续] ``` [STOP] 每章等待确认,全部完成后发送"P4完成"进入P5。 --- ## P5 结构审查 审查五维度:逻辑连贯 / 定义精准 / 案例真实 / 受众适配 / 工程化体现 输出:✅通过 / ⚠️需修改 / ❌必须修改 [STOP] 等待确认修订清单后进入P6。 --- ## P6 配图规划 为课程设计5-8张配图,输出每张图的中文Prompt(供Gemini生图)。 风格:扁平化,深蓝+橙色,现代学术。 模型:gemini-3.1-flash-image-preview(中文不乱码) 生图命令(龙虾环境): ```bash GOOGLE_API_KEY="[KEY]" GOOGLE_BASE_URL="https://work.poloapi.com" \ bun ~/.openclaw/skills/baoyu-image-gen/scripts/main.ts \ --prompt "[Prompt]" --image /tmp/XX.png \ --provider google --model gemini-3.1-flash-image-preview --ar 16:9 ``` 图片生成后通过飞书API发给用户手动插入(feishu_doc_media只支持末尾插入)。 [STOP] 图片确认后进入P7。 --- ## P7 终局产出 1. 创建飞书备课稿文档 2. 创建备课复盘文档(流程+注意事项+工程化特点) 输出:备课稿URL + 复盘URL + 配图对照表 --- ## 全景仪表盘(每步结束显示) ``` ╭─ 📚 Course-Prep-Auto-Flow v1.0 ─────────────╮ │ 📊 当前阶段:Px [阶段名] │ ⏳ 总进度:x/7 步 │ 📝 课程主题:[主题] │ 📄 已处理素材:x/x 份 │ ✅ 已完成章节:x/x 章 │ 👉 NEXT:[下一步操作提示] ╰─────────────────────────────────────────────╯ ``` --- ## 技术注意事项(龙虾环境) - 文档更新:用replace_range+selection_by_title,不用overwrite - 图片生成:gemini-3.1-flash-image-preview(不用2.5-flash,中文乱码) - 图片插入:feishu_doc_media只支持末尾,需手动或通过飞书IM API发给用户 - PPT全图素材:解压后逐张用image工具识别 - AFP是理念,不是工具平台,不能与龙虾/CC并列 --- 详细说明文档:https://www.feishu.cn/docx/GmQUdwj4sosWxvxQp84cIf8NnQb
学术论文结构化阅读、拆解与分析工具。基于12个阅读要素(研究背景、研究问题、研究结论、文献综合、文献批评、研究方法、理论视角与理论框架、一致性发现、不一致性发现、研究贡献、研究不足、未来研究展望)对论文进行深度拆解,结果保存为Excel文件。当用户提到需要针对论文/文献/paper进行拆解、解析、分析、阅读、梳理...
---
name: paper-analyzer
description: 学术论文结构化阅读、拆解与分析工具。基于12个阅读要素(研究背景、研究问题、研究结论、文献综合、文献批评、研究方法、理论视角与理论框架、一致性发现、不一致性发现、研究贡献、研究不足、未来研究展望)对论文进行深度拆解,结果保存为Excel文件。当用户提到需要针对论文/文献/paper进行拆解、解析、分析、阅读、梳理,并上传或告知一篇或多篇论文的本地文件路径(PDF、Word等)时触发此skill。
---
# 论文结构化拆解分析
基于12个阅读要素对学术论文进行结构化拆解,结果保存为Excel文件存储于论文所在文件夹中。
## 工作流程
### 1. 读取论文
- 使用 Read 工具读取用户提供的论文文件(支持 PDF、Word 等格式)
- 对于 PDF 文件,如果 Read 工具无法直接读取,使用 `pdftotext` 命令提取文本
- 对于 PDF 文件,如果页数较多,先读取前几页了解结构,再分批读取全文
- 确保完整阅读论文的各个部分:摘要、引言、文献综述、方法、结果、讨论、结论
- **记录论文所在的文件夹路径**,用于后续保存Excel文件
### 2. 按12要素提取信息
阅读论文后,逐一提取12个阅读要素。要素定义详见 [references/reading_elements.md](references/reading_elements.md)。
提取要点:
- **研究背景**: 从引言开头提取宏观背景,区分实践背景、理论背景、政策背景
- **研究问题**: 识别核心研究问题,判断其类型(what/why/how/should)
- **研究结论**: 从结论和讨论部分提取对研究问题的直接回答
- **文献综合**: 从文献综述部分梳理作者如何分类整理已有研究
- **文献批评**: 提取作者对已有研究的评价,重点关注研究gap的表述
- **研究方法**: 从方法论部分提取具体研究方法和研究设计
- **理论视角与理论框架**: 识别论文采用的理论基础和分析框架
- **一致性发现**: 从讨论部分提取与已有研究一致的发现
- **不一致性发现**: 从讨论部分提取与已有研究不一致的发现
- **研究贡献**: 提取作者自述的研究贡献(新方法/新材料/新理论/新观点/新概念)
- **研究不足**: 从结论或讨论的局限性部分提取
- **未来研究展望**: 提取作者对后续研究的建议
如论文中某要素未明确提及,标注"论文未明确提及"并尝试基于论文内容进行合理推断,推断内容用括号标注。
### 3. 保存为Excel文件
分析完成后,将结果保存为Excel文件,存放于**被分析论文所在的文件夹**中。
使用 [scripts/export_excel.py](scripts/export_excel.py) 脚本生成Excel。先用 Python 将分析数据写入临时JSON文件,再调用脚本。
#### 单篇论文
生成文件名:`论文拆解_<论文简称>.xlsx`,包含一个工作表。
构造JSON数据并调用脚本:
```bash
python3 <skill_path>/scripts/export_excel.py --mode single --output "<论文所在文件夹>/论文拆解_<论文简称>.xlsx" --json '<json_string>'
```
JSON结构:
```json
{
"title": "论文标题",
"author": "作者",
"source": "期刊/来源",
"year": "年份",
"elements": {
"研究背景": "内容...",
"研究问题": "内容...",
"研究结论": "内容...",
"文献综合": "内容...",
"文献批评": "内容...",
"研究方法": "内容...",
"理论视角与理论框架": "内容...",
"一致性发现": "内容...",
"不一致性发现": "内容...",
"研究贡献": "内容...",
"研究不足": "内容...",
"未来研究展望": "内容..."
}
}
```
#### 多篇论文
生成文件名:`论文拆解汇总_<N>篇.xlsx`,包含每篇论文的独立工作表 + 最后一个"横向对比汇总"工作表。
```bash
python3 <skill_path>/scripts/export_excel.py --mode multi --output "<论文所在文件夹>/论文拆解汇总_<N>篇.xlsx" --json '<json_string>'
```
JSON结构:
```json
{
"papers": [
{ "title": "...", "author": "...", "source": "...", "year": "...", "elements": { ... } },
...
],
"summary": {
"paper_labels": ["论文A简称", "论文B简称", ...],
"elements": {
"研究背景": ["论文A精简版", "论文B精简版", ...],
...12个要素各一个数组...
}
}
}
```
**重要**:由于JSON字符串可能很长,使用Python将JSON写入临时文件,再用 `--json "$(cat /tmp/paper_data.json)"` 传入,或直接在Python中调用脚本。推荐方式:
```python
import json, subprocess
data = { ... } # 构造好的数据
with open("/tmp/_paper_analysis.json", "w") as f:
json.dump(data, f, ensure_ascii=False)
subprocess.run(["python3", "<skill_path>/scripts/export_excel.py",
"--mode", "multi",
"--output", "<输出路径>",
"--json", json.dumps(data, ensure_ascii=False)])
```
### 4. 输出到对话
在保存Excel后,同时在对话中以markdown表格简要呈现分析结果,并告知用户Excel文件的保存路径。
### 5. 内容要求
- 使用中文输出(除非论文为英文且用户要求英文输出)
- 每个要素的内容应简明扼要但信息充分,单篇表格中每个要素2-5句话
- 汇总对比表中每个单元格压缩为1-2句核心要点
- 忠于论文原文,避免过度解读;推断内容明确标注
- 在研究问题要素中标注问题类型(what/why/how/should)
FILE:references/reading_elements.md
# 12 阅读要素定义
结构化阅读论文的思维工具,将论文像拆乐高积木一样拆分为基本单元。
## 要素1: 研究背景
研究问题所基于的更为宏大的背景,包括实践背景、理论背景、政策背景、研究背景等。
## 要素2: 研究问题
研究所需要解决的主要问题,分为what型问题、why型问题、how型问题和should型问题。
## 要素3: 研究结论
对研究问题的回答,即一项研究的主要观点,通常包括明确的命题和判断。
## 要素4: 文献综合
论文作者在文献综述中对已有同类研究的梳理,采取"横向分类,纵向理流"的方式,从文献角度体现they say。
## 要素5: 文献批评
论文作者在文献综述中对已有同类研究的主观评价,指出现有研究的推进之处,更重要的是指出现有研究的不足/研究gap,为建立研究问题的合法性服务。
## 要素6: 研究方法
从研究问题到研究结论的中间路径,包括定量方法、定性方法、思辨方法、大数据方法等。
## 要素7: 理论视角与理论框架
针对研究对象进行切入分析的角度,通常从已有理论发展为具体研究的理论框架。
## 要素8: 一致性发现
研究结论与已有研究结论相比,哪些观点是一致的。
## 要素9: 不一致性发现
研究结论与已有研究结论相比,哪些观点是不一致的。
## 要素10: 研究贡献
本研究从哪个角度推进了已有研究——新方法、新材料、新理论、新观点、新概念?
## 要素11: 研究不足
受制于研究资源和研究者能力的限制,研究存在的不足之处。
## 要素12: 未来研究展望
针对上述不足,未来可以如何推进已有研究。
FILE:scripts/export_excel.py
#!/usr/bin/env python3
"""Export paper analysis results to Excel files.
Usage:
Single paper:
python3 export_excel.py --mode single --output /path/to/output.xlsx --json '<json_data>'
Multiple papers with comparison:
python3 export_excel.py --mode multi --output /path/to/output.xlsx --json '<json_data>'
JSON format for single paper:
{
"title": "论文标题",
"author": "作者",
"source": "期刊/来源",
"year": "年份",
"elements": {
"研究背景": "...",
"研究问题": "...",
"研究结论": "...",
"文献综合": "...",
"文献批评": "...",
"研究方法": "...",
"理论视角与理论框架": "...",
"一致性发现": "...",
"不一致性发现": "...",
"研究贡献": "...",
"研究不足": "...",
"未来研究展望": "..."
}
}
JSON format for multiple papers:
{
"papers": [ ...array of single paper objects... ],
"summary": {
"paper_labels": ["论文A简称", "论文B简称", ...],
"elements": {
"研究背景": ["论文A摘要", "论文B摘要", ...],
...
}
}
}
"""
import argparse
import json
import sys
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
ELEMENT_NAMES = [
"1. 研究背景",
"2. 研究问题",
"3. 研究结论",
"4. 文献综合",
"5. 文献批评",
"6. 研究方法",
"7. 理论视角与理论框架",
"8. 一致性发现",
"9. 不一致性发现",
"10. 研究贡献",
"11. 研究不足",
"12. 未来研究展望",
]
ELEMENT_KEYS = [
"研究背景", "研究问题", "研究结论", "文献综合", "文献批评",
"研究方法", "理论视角与理论框架", "一致性发现", "不一致性发现",
"研究贡献", "研究不足", "未来研究展望",
]
# Styles
HEADER_FONT = Font(name="Microsoft YaHei", bold=True, size=11, color="FFFFFF")
HEADER_FILL = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
ELEMENT_FONT = Font(name="Microsoft YaHei", bold=True, size=10)
ELEMENT_FILL = PatternFill(start_color="D6E4F0", end_color="D6E4F0", fill_type="solid")
CONTENT_FONT = Font(name="Microsoft YaHei", size=10)
META_FONT = Font(name="Microsoft YaHei", bold=True, size=12)
THIN_BORDER = Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin"),
)
WRAP_ALIGNMENT = Alignment(wrap_text=True, vertical="top")
HEADER_ALIGNMENT = Alignment(wrap_text=True, vertical="center", horizontal="center")
def style_cell(cell, font=None, fill=None, alignment=None, border=None):
if font:
cell.font = font
if fill:
cell.fill = fill
if alignment:
cell.alignment = alignment
if border:
cell.border = border
def write_single_sheet(ws, paper):
"""Write a single paper analysis to a worksheet."""
ws.column_dimensions["A"].width = 25
ws.column_dimensions["B"].width = 90
# Title row
ws.merge_cells("A1:B1")
title_cell = ws["A1"]
title_cell.value = f"论文拆解:《{paper['title']}》"
style_cell(title_cell, font=Font(name="Microsoft YaHei", bold=True, size=14),
alignment=Alignment(horizontal="center", vertical="center"))
ws.row_dimensions[1].height = 30
# Meta row
ws.merge_cells("A2:B2")
meta_cell = ws["A2"]
meta_cell.value = f"作者: {paper.get('author', '')} | 期刊/来源: {paper.get('source', '')} | 年份: {paper.get('year', '')}"
style_cell(meta_cell, font=Font(name="Microsoft YaHei", size=10, italic=True),
alignment=Alignment(horizontal="center", vertical="center"))
ws.row_dimensions[2].height = 22
# Header row
row = 4
for col, header in enumerate(["阅读要素", "内容"], 1):
cell = ws.cell(row=row, column=col, value=header)
style_cell(cell, font=HEADER_FONT, fill=HEADER_FILL,
alignment=HEADER_ALIGNMENT, border=THIN_BORDER)
ws.row_dimensions[row].height = 22
# Data rows
elements = paper.get("elements", {})
for i, (name, key) in enumerate(zip(ELEMENT_NAMES, ELEMENT_KEYS)):
row = 5 + i
cell_a = ws.cell(row=row, column=1, value=name)
style_cell(cell_a, font=ELEMENT_FONT, fill=ELEMENT_FILL,
alignment=Alignment(wrap_text=True, vertical="top"),
border=THIN_BORDER)
cell_b = ws.cell(row=row, column=2, value=elements.get(key, ""))
style_cell(cell_b, font=CONTENT_FONT, alignment=WRAP_ALIGNMENT,
border=THIN_BORDER)
# Auto row height based on content length
content_len = len(elements.get(key, ""))
ws.row_dimensions[row].height = max(40, min(150, content_len * 0.6))
def write_summary_sheet(ws, summary):
"""Write the multi-paper comparison summary to a worksheet."""
labels = summary.get("paper_labels", [])
n_papers = len(labels)
ws.column_dimensions["A"].width = 25
for i in range(n_papers):
col_letter = chr(ord("B") + i)
ws.column_dimensions[col_letter].width = 40
# Title
end_col = chr(ord("A") + n_papers)
ws.merge_cells(f"A1:{end_col}1")
title_cell = ws["A1"]
title_cell.value = "多篇论文横向对比汇总"
style_cell(title_cell, font=Font(name="Microsoft YaHei", bold=True, size=14),
alignment=Alignment(horizontal="center", vertical="center"))
ws.row_dimensions[1].height = 30
# Header row
row = 3
cell = ws.cell(row=row, column=1, value="阅读要素")
style_cell(cell, font=HEADER_FONT, fill=HEADER_FILL,
alignment=HEADER_ALIGNMENT, border=THIN_BORDER)
for j, label in enumerate(labels):
cell = ws.cell(row=row, column=2 + j, value=label)
style_cell(cell, font=HEADER_FONT, fill=HEADER_FILL,
alignment=HEADER_ALIGNMENT, border=THIN_BORDER)
ws.row_dimensions[row].height = 35
# Data rows
elements_data = summary.get("elements", {})
for i, (name, key) in enumerate(zip(ELEMENT_NAMES, ELEMENT_KEYS)):
row = 4 + i
cell_a = ws.cell(row=row, column=1, value=name)
style_cell(cell_a, font=ELEMENT_FONT, fill=ELEMENT_FILL,
alignment=Alignment(wrap_text=True, vertical="top"),
border=THIN_BORDER)
values = elements_data.get(key, [])
max_len = 0
for j in range(n_papers):
val = values[j] if j < len(values) else ""
cell = ws.cell(row=row, column=2 + j, value=val)
style_cell(cell, font=CONTENT_FONT, alignment=WRAP_ALIGNMENT,
border=THIN_BORDER)
max_len = max(max_len, len(val))
ws.row_dimensions[row].height = max(45, min(120, max_len * 0.8))
def main():
parser = argparse.ArgumentParser(description="Export paper analysis to Excel")
parser.add_argument("--mode", choices=["single", "multi"], required=True)
parser.add_argument("--output", required=True, help="Output .xlsx path")
parser.add_argument("--json", required=True, help="JSON data string")
args = parser.parse_args()
data = json.loads(args.json)
wb = Workbook()
if args.mode == "single":
ws = wb.active
ws.title = "论文拆解"
write_single_sheet(ws, data)
else:
papers = data.get("papers", [])
# Individual sheets
for i, paper in enumerate(papers):
if i == 0:
ws = wb.active
else:
ws = wb.create_sheet()
short_title = paper.get("title", f"论文{i+1}")
# Sheet name max 31 chars
ws.title = short_title[:28] + "..." if len(short_title) > 31 else short_title
write_single_sheet(ws, paper)
# Summary sheet
if "summary" in data:
ws = wb.create_sheet(title="横向对比汇总")
write_summary_sheet(ws, data["summary"])
# Move summary sheet to the end (already is)
wb.save(args.output)
print(f"Excel saved: {args.output}")
if __name__ == "__main__":
main()
PPT全自动生成流(AFP:Auto-Flow Prompt)。用户提供主题和内容,Agent自动完成:风格选择 → 大纲生成 → Prompt生成 → AI生图 → 打包PPTX → 发送飞书。当用户说"帮我做PPT"、"生成幻灯片"、"制作演示文稿"、"ppt-afp"时触发。
---
name: ppt-afp
description: PPT全自动生成流(AFP:Auto-Flow Prompt)。用户提供主题和内容,Agent自动完成:风格选择 → 大纲生成 → Prompt生成 → AI生图 → 打包PPTX → 发送飞书。当用户说"帮我做PPT"、"生成幻灯片"、"制作演示文稿"、"ppt-afp"时触发。
version: 1.1.0
---
# PPT AFP — 全自动幻灯片生成流
> **角色**:你是顶级视觉设计师 + PPT产品经理,擅长将内容转化为震撼的演示文稿。
---
## ⛔ 执行前必读(三条铁律)
1. **每次执行前必须重新 read 本 SKILL.md**,不能凭印象走
2. **每完成一个阶段,显式报告**:说出"P阶段完成"再进入下一阶段
3. **每阶段结束对照下方 Phase Gate 逐条核查**,全部打勾才能继续
---
## 📊 进度仪表盘(每阶段更新)
```
PPT AFP 进度
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
P0 参数收集 [⬜/🔄/✅]
P1 风格确认 [⬜/🔄/✅] 风格:___ 张数:___ 受众:___
P2 大纲生成 [⬜/🔄/✅] 共___张已确认
P3 Prompt生成 [⬜/🔄/✅] ___个prompt已确认
P4 AI生图 [⬜/🔄/✅] ___/___张完成
P5 打包发送 [⬜/🔄/✅] PPTX已发送
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
---
## P0:参数收集
**收集以下信息(未提供则主动询问):**
| 参数 | 说明 |
|------|------|
| 主题 | PPT的核心主题(必须) |
| 内容来源 | 已有大纲/文档路径,或从零开始 |
| 目标用途 | 直播/演讲/分享/内部汇报 |
| 张数偏好 | 不填则根据内容自动推荐(8-15张) |
| 是否发飞书 | 默认是 |
**Phase Gate P0:**
- [ ] 主题明确
- [ ] 内容来源确认
- [ ] 用途清晰
---
## P1:风格确认(⚠️ 必须让用户选,不能自行决定)
### Step 1:发送风格图鉴
**发送飞书预览文档链接,让用户直观浏览37种风格:**
> 📎 **风格图鉴文档**:https://sgl0nnj5ev.feishu.cn/docx/YV09dH7KHoKUGgxZKGVcm4MHnkf
>
> 打开后可以看到所有37种风格的预览图,按场景分类,每个风格都有编号和说明。
**发送话术示例(直接发给用户):**
```
我为你准备了37种PPT风格预览 👉 https://sgl0nnj5ev.feishu.cn/docx/YV09dH7KHoKUGgxZKGVcm4MHnkf
打开看看,选一个你喜欢的编号(1-37)告诉我就行!
```
### Step 2:等待用户选择 + 确认参数
用户选择风格后,同时确认:
- **受众**:一般读者 / 初学者 / 专家 / 高管
- **张数**:推荐N张(基于内容量),用户可调整
- **是否审查大纲**:是/否(建议是)
- **是否审查Prompt**:是/否(建议否,节省时间)
### 风格编号速查表
| # | 风格名 | 分类 |
|---|--------|------|
| 1 | bold-editorial | 🔥 演讲 |
| 2 | dark-atmospheric | 🔥 演讲 |
| 3 | minimal | 🔥 演讲 |
| 4 | magazine-editorial-light | 🔥 演讲 |
| 5 | gradient-aurora | 🔥 演讲 |
| 6 | warm-gradient | 🔥 演讲 |
| 7 | corporate | 💼 商务 |
| 8 | notion | 💼 商务 |
| 9 | data-dashboard | 💼 商务 |
| 10 | isometric-3d | 💼 商务 |
| 11 | chalkboard | 📚 教育 |
| 12 | sketch-notes | 📚 教育 |
| 13 | scientific | 📚 教育 |
| 14 | blueprint | 📚 教育 |
| 15 | intuition-machine | 📚 教育 |
| 16 | storybook | 📚 教育 |
| 17 | watercolor | 🎨 创意 |
| 18 | vector-illustration | 🎨 创意 |
| 19 | fantasy-animation | 🎨 创意 |
| 20 | paper-cut | 🎨 创意 |
| 21 | bauhaus | 🎨 创意 |
| 22 | neon-pop | 🎨 创意 |
| 23 | candy-pastel | 🎨 创意 |
| 24 | neon-cyberpunk | 🔮 科技 |
| 25 | retro-synthwave | 🔮 科技 |
| 26 | terminal-hacker | 🔮 科技 |
| 27 | glassmorphism | 🔮 科技 |
| 28 | pixel-art | 🔮 科技 |
| 29 | ink-wash | 🏛 文化 |
| 30 | chinese-national-tide | 🏛 文化 |
| 31 | japanese-zen | 🏛 文化 |
| 32 | vintage | 🏛 文化 |
| 33 | earth-organic | 🏛 文化 |
| 34 | newspaper | ⚔️ 特殊 |
| 35 | military-tactical | ⚔️ 特殊 |
| 36 | brutalist | ⚔️ 特殊 |
| 37 | editorial-infographic | ⚔️ 特殊 |
**Phase Gate P1:**
- [ ] 已发送风格图鉴链接
- [ ] 用户已选择风格
- [ ] 受众已确认
- [ ] 张数已确认
---
## P2:大纲生成
**执行步骤:**
1. 创建工作目录:
```
~/Desktop/二饼文件夹/openclaw 二饼/slide-deck/{topic-slug}/
```
2. 调用 `baoyu-slide-deck` 生成大纲:
- 读取 `~/.openclaw/skills/baoyu-slide-deck/references/outline-template.md`
- 读取对应风格文件:`references/styles/{style}.md`
- 按确认的张数、受众、语言生成 `outline.md`
3. **如果用户选择"审查大纲"**,展示大纲摘要表:
```
| # | 标题 | 类型 | 布局 |
|---|------|------|------|
| 1 | xxx | 封面 | title-hero |
| 2 | xxx | 内容 | ... |
```
等待用户确认后继续。
**Phase Gate P2:**
- [ ] outline.md 已生成
- [ ] 用户已确认大纲结构(或跳过审查)
---
## P3:Prompt生成
**执行步骤:**
1. 读取 `~/.openclaw/skills/baoyu-slide-deck/references/base-prompt.md`
2. 为每张幻灯片生成 prompt 文件,保存到 `prompts/` 目录
3. 每个 prompt 包含:
- base-prompt 内容
- STYLE_INSTRUCTIONS(从 outline.md 提取)
- 该张幻灯片的具体内容描述
4. **生成 batch.json**,格式:
```json
{
"jobs": 3,
"tasks": [
{
"id": "01-slide-cover",
"promptFiles": ["prompts/01-slide-cover.md"],
"image": "01-slide-cover.png",
"provider": "google",
"ar": "16:9",
"quality": "2k"
}
]
}
```
5. 如果用户选择"审查Prompt",展示 prompt 列表等待确认
**Phase Gate P3:**
- [ ] 所有 prompt 文件已生成(数量 = 张数)
- [ ] batch.json 已生成
- [ ] 用户已确认(或跳过审查)
---
## P4:AI生图
**默认模型配置(已验证中文渲染正确):**
```bash
# 从环境变量读取(已配置在 ~/.zshrc)
# GEMINI_API_KEY=<your-key>
# GOOGLE_BASE_URL=<your-url>
MODEL=gemini-3-pro-image-preview
```
**执行命令:**
```bash
cd {工作目录} && \
NODE_TLS_REJECT_UNAUTHORIZED=0 \
npx -y bun ~/.openclaw/skills/baoyu-image-gen/scripts/main.ts \
--batchfile batch.json --jobs 3 \
--provider google --model gemini-3-pro-image-preview
```
**实时报告进度:** 每完成3张报告一次 "已完成 X/N 张"
**Phase Gate P4:**
- [ ] 所有图片生成成功(Succeeded: N,Failed: 0)
- [ ] 图片分辨率正确(应为约 2752×1536)
---
## P5:打包 & 发送
**步骤1:用 merge-to-pptx.ts 打包**
```bash
cd {工作目录} && \
NODE_TLS_REJECT_UNAUTHORIZED=0 \
npx -y bun ~/.openclaw/skills/baoyu-slide-deck/scripts/merge-to-pptx.ts .
```
**步骤2:移动到标准目录**
```
~/Desktop/二饼文件夹/openclaw 二饼/PPT/{主题名称}.pptx
```
**步骤3:发送飞书**(使用标准飞书API发送脚本)
```python
# 见 TOOLS.md 中的飞书发文件脚本
# USER = ou_74c5a7816fcb78172bfca68a7f7449e8
```
**步骤4:确认本地路径**
**Phase Gate P5:**
- [ ] PPTX 已生成(文件存在,大小 > 1MB)
- [ ] 已发送飞书(返回 code=0)
- [ ] 本地路径已告知用户
---
## 关键经验(血泪教训)
### ✅ 已验证可用的配置
- **生图模型**:`gemini-3-pro-image-preview`(中文完全正确,2752×1536高分辨率)
- **API**:通过环境变量 `GEMINI_API_KEY` 和 `GOOGLE_BASE_URL` 配置(见 ~/.zshrc)
- **merge-to-pptx.ts**:输出文件名为 `..pptx`(需要手动 cp 重命名)
### ❌ 已知问题
- `gemini-2.5-flash-image` 中文乱码 → 不要用
- `ar=16:9` 参数对某些模型无效 → gemini-3-pro 已解决
- merge-to-pptx.ts 输出路径是上一级目录的 `..pptx` → 用 find 找到后 cp
### 📐 一定要让用户选风格
- P1 是强制步骤,不能自行决定风格
- 展示风格对比表,让用户做选择
---
## 触发关键词
- "帮我做PPT"、"做个幻灯片"、"制作演示"
- "ppt-afp"、"启动PPT流程"
- 提供了内容/大纲并希望转成PPT
基于Braun & Clarke六阶段法,对本地质性资料(访谈、观察、文本)进行系统性主题归纳、编码及报告生成。
---
name: qualitative-thematic-analysis
description: 质性研究主题分析(Thematic Analysis)工具。基于 Braun & Clarke 经典六阶段方法,对本地存储的质性资料(访谈记录、观察笔记、文本资料等)进行系统性主题分析,生成编码表、主题树和分析报告。
**当以下情况时使用此 Skill**:
(1) 用户提到"质性研究"、"主题分析"、"访谈分析"、"定性分析"、"thematic analysis"、"扎根理论"、"编码"
(2) 用户提供了访谈记录、观察笔记、焦点小组记录、文本语料等质性资料的文件路径
(3) 用户说"帮我分析这些访谈"、"对这些文本做主题分析"、"给我做编码"
(4) 你判断用户的研究需要对文本数据进行归纳性分析
**关键前提(必须满足才能执行)**:
- 必须知道资料文件的本地路径,如果用户是云端部署,那么需要提醒用户上传需要分析的资料
- 必须知道资料类型(访谈/观察/文本)和基本情况
- 如果用户没有提供,必须主动询问
---
# 质性研究主题分析 Skill
## ⚠️ 启动前必检清单
在开始分析前,**必须**向用户确认以下信息(如未提供则主动询问):
```
1. 📁 文件路径:资料存放在哪里?(完整路径,如 ~/Desktop/访谈资料/)
2. 📄 文件格式:TXT / DOCX / PDF / 其他?
3. 🗂️ 资料数量:共几份文件?
4. 📋 资料类型:访谈记录 / 观察笔记 / 焦点小组 / 文本语料 / 混合?
5. 🎯 研究问题:本次分析的核心研究问题是什么?
6. 📊 分析取向:归纳式(从资料中发现主题)还是演绎式(验证既有理论框架)?
```
**提问模板**(当信息不全时使用):
> "我需要了解几个信息才能开始分析:
> 1. 资料文件存放在哪个路径?
> 2. 是什么类型的资料(访谈/观察/文本)?
> 3. 你的研究问题是什么?
> 告诉我这些,我就可以开始系统性的主题分析了。"
---
## 分析流程:Braun & Clarke 六阶段主题分析法
### 阶段一:熟悉资料(Familiarisation)
1. 读取所有文件(用 `scripts/extract_text.py` 提取文本)
2. 通读全部资料,记录初步印象
3. 向用户呈现:
- 资料总字数 / 总条目数
- 初步观察到的数据特征
- 确认资料是否符合研究问题
### 阶段二:生成初始编码(Initial Coding)
- 系统性标注有意义的片段(语义单元)
- 每个编码:`[编码标签] → 原文引用`
- 编码原则:
- 贴近资料语言(In vivo 编码优先)
- 保留语境,不过早概括
- 同一片段可多重编码
- 输出:初始编码列表(按出现顺序)
### 阶段三:归纳潜在主题(Theme Search)
- 将相关编码聚合为候选主题
- 构建初步主题地图(树状结构)
- 区分:主主题(Theme)vs 子主题(Sub-theme)
- 输出:候选主题树 + 每个主题下的编码列表
### 阶段四:审查主题(Theme Review)
对照以下标准审查每个主题:
- ✅ 主题内部编码一致(内聚性)
- ✅ 主题之间界限清晰(区分性)
- ✅ 主题能回应研究问题(相关性)
- ✅ 有足够的资料支撑(饱和性)
调整:合并过于相似的主题 / 拆分过于宽泛的主题 / 删除游离主题
### 阶段五:命名与定义主题(Defining & Naming)
为每个主题:
1. 写出清晰的主题名称(简洁、有实质内容)
2. 写出主题定义(2-3句话,说明该主题捕捉了什么)
3. 选取 2-3 条最具代表性的原文佐证(引用)
### 阶段六:撰写分析报告(Writing Up)
生成结构化分析报告,包含:
- 研究问题与资料概述
- 主题分析结果(主题树 + 各主题详述)
- 代表性引文
- 分析发现小结
- 研究局限与反思
---
## 输出格式
### 编码表(Excel 格式,用 scripts/export_coding.py 生成)
| 编码 | 子编码 | 原文片段 | 来源文件 | 行号 | 备注 |
|------|--------|----------|----------|------|------|
### 主题树(Markdown 树状图)
```
主题一:[名称]
├── 子主题 1.1:[名称]
│ ├── 编码:XXX
│ └── 编码:XXX
└── 子主题 1.2:[名称]
└── 编码:XXX
主题二:[名称]
...
```
### 分析报告(DOCX,用 scripts/export_report.py 生成)
---
## 参考资源
- 详细方法论说明:`references/thematic-analysis-method.md`
- 编码操作指南:`references/coding-guide.md`
- 研究伦理提示:`references/ethics-note.md`
---
## ⚠️ 已知问题与迭代 TODO(v1 → v2)
> 来源:2026-03-10 真实演示复盘,一平反馈
### 问题1:分析深度不足 / 速度过快
- **现状**:当前实现在上下文窗口限制下仅读取文件摘录,未逐字通读全部材料
- **后果**:输出主题基于"感知归纳"而非系统编码,缺乏严谨性
- **迭代方向**:
- 强制使用 `scripts/extract_text.py` 全量提取文本
- 每份文件逐段打标签(初始码),不能跳过
- 编码完成后才能归并子主题 → 主主题
### 问题2:跳过了 Braun & Clarke 中间阶段
- **现状**:直接从"读材料"跳到"输出主题",跳过了初始编码、编码汇总、主题审查
- **迭代方向**:
- 新增强制中间输出:初始码列表 → 子主题归并树 → 主题定义表
- 每个主题必须附 ≥3条原文引证(带文件名+行号)
### 问题3:未输出编码表
- **现状**:`export_coding.py` 脚本存在但演示中未调用
- **迭代方向**:P1 完成时必须生成 Excel 编码表,作为强制输出物
### 问题4:无饱和度检验
- **迭代方向**:增加 P1.5 饱和度确认步骤——对比最后3份文件与前期编码,确认无新码出现
### 问题5:需要区分演示模式与研究模式
- **迭代方向**:增加 `mode` 参数
- `demo`:快速出主题,明确标注"仅供展示,深度不足"
- `research`:完整六阶段,强制编码表输出,耗时较长
FILE:scripts/export_coding.py
#!/usr/bin/env python3
"""
export_coding.py — 将编码结果导出为 Excel 文件
用法:python3 export_coding.py <编码JSON路径> <输出Excel路径>
编码JSON格式:
[
{
"code": "编码名称",
"sub_code": "子编码",
"quote": "原文片段",
"source": "来源文件名",
"line": "行号/段落号",
"theme": "所属主题",
"note": "备注"
}
]
"""
import sys
import json
import os
def export_to_excel(codes, output_path):
try:
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
except ImportError:
print("需要安装 openpyxl:pip install openpyxl")
sys.exit(1)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "编码表"
# 表头
headers = ["主题", "编码", "子编码", "原文片段", "来源文件", "位置", "备注"]
header_fill = PatternFill("solid", fgColor="2D3561")
header_font = Font(bold=True, color="FFFFFF", name="微软雅黑", size=11)
thin = Side(style="thin", color="CCCCCC")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
for col, h in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=h)
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = border
# 列宽
col_widths = [20, 20, 20, 50, 20, 10, 20]
for i, w in enumerate(col_widths, 1):
ws.column_dimensions[ws.cell(row=1, column=i).column_letter].width = w
ws.row_dimensions[1].height = 30
# 数据行
alt_fill = PatternFill("solid", fgColor="F5F7FF")
for row_idx, code in enumerate(codes, 2):
row_fill = alt_fill if row_idx % 2 == 0 else None
values = [
code.get("theme", ""),
code.get("code", ""),
code.get("sub_code", ""),
code.get("quote", ""),
code.get("source", ""),
code.get("line", ""),
code.get("note", ""),
]
for col, val in enumerate(values, 1):
cell = ws.cell(row=row_idx, column=col, value=val)
cell.alignment = Alignment(wrap_text=True, vertical="top")
cell.border = border
if row_fill:
cell.fill = row_fill
ws.row_dimensions[row_idx].height = 40
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
wb.save(output_path)
print(f"✅ 编码表已保存:{output_path}(共 {len(codes)} 条编码)")
def main():
if len(sys.argv) < 3:
print("用法:python3 export_coding.py <编码JSON路径> <输出Excel路径>")
sys.exit(1)
json_path = os.path.expanduser(sys.argv[1])
output_path = os.path.expanduser(sys.argv[2])
with open(json_path, 'r', encoding='utf-8') as f:
codes = json.load(f)
export_to_excel(codes, output_path)
if __name__ == "__main__":
main()
FILE:scripts/export_report.py
#!/usr/bin/env python3
"""
export_report.py — 将主题分析结果导出为 DOCX 报告
用法:python3 export_report.py <分析结果JSON路径> <输出DOCX路径>
JSON格式:
{
"title": "研究报告标题",
"research_question": "研究问题",
"data_overview": "资料概述",
"themes": [
{
"name": "主题名称",
"definition": "主题定义",
"sub_themes": [
{
"name": "子主题名称",
"description": "描述",
"quotes": ["引文1", "引文2"]
}
]
}
],
"summary": "分析发现总结",
"limitations": "局限与反思"
}
"""
import sys
import json
import os
from datetime import datetime
def export_report(data, output_path):
try:
from docx import Document
from docx.shared import Pt, RGBColor, Inches, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
except ImportError:
print("需要安装 python-docx:pip install python-docx")
sys.exit(1)
doc = Document()
# 页面设置
section = doc.sections[0]
section.top_margin = Cm(2.5)
section.bottom_margin = Cm(2.5)
section.left_margin = Cm(3)
section.right_margin = Cm(2.5)
# 标题
title = doc.add_heading(data.get("title", "质性研究主题分析报告"), 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 元信息
doc.add_paragraph(f"分析日期:{datetime.now().strftime('%Y年%m月%d日')}")
doc.add_paragraph(f"研究问题:{data.get('research_question', '')}")
doc.add_paragraph()
# 一、资料概述
doc.add_heading("一、资料概述", 1)
doc.add_paragraph(data.get("data_overview", ""))
# 二、主题分析结果
doc.add_heading("二、主题分析结果", 1)
themes = data.get("themes", [])
for i, theme in enumerate(themes, 1):
doc.add_heading(f"主题{i}:{theme['name']}", 2)
# 主题定义
definition_para = doc.add_paragraph()
run = definition_para.add_run("主题定义:")
run.bold = True
definition_para.add_run(theme.get("definition", ""))
# 子主题
for sub in theme.get("sub_themes", []):
doc.add_heading(sub["name"], 3)
doc.add_paragraph(sub.get("description", ""))
# 引文
for quote in sub.get("quotes", []):
q_para = doc.add_paragraph(style="Quote")
q_para.add_run(f""{quote}"")
# 三、分析发现小结
doc.add_heading("三、分析发现小结", 1)
doc.add_paragraph(data.get("summary", ""))
# 四、研究局限与反思
doc.add_heading("四、研究局限与反思", 1)
doc.add_paragraph(data.get("limitations", ""))
doc.save(output_path)
print(f"✅ 分析报告已保存:{output_path}")
def main():
if len(sys.argv) < 3:
print("用法:python3 export_report.py <分析结果JSON路径> <输出DOCX路径>")
sys.exit(1)
json_path = os.path.expanduser(sys.argv[1])
output_path = os.path.expanduser(sys.argv[2])
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
export_report(data, output_path)
if __name__ == "__main__":
main()
FILE:scripts/extract_text.py
#!/usr/bin/env python3
"""
extract_text.py — 从本地文件提取文本内容
支持格式:TXT, DOCX, PDF
用法:python3 extract_text.py <文件路径或目录路径>
"""
import sys
import os
def extract_txt(path):
for enc in ['utf-8', 'gbk', 'utf-16']:
try:
with open(path, 'r', encoding=enc) as f:
return f.read()
except:
continue
return ""
def extract_docx(path):
try:
import docx
doc = docx.Document(path)
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
except ImportError:
return f"[需要安装 python-docx:pip install python-docx]"
def extract_pdf(path):
try:
import pdfplumber
text = []
with pdfplumber.open(path) as pdf:
for page in pdf.pages:
t = page.extract_text()
if t:
text.append(t)
return "\n".join(text)
except ImportError:
return f"[需要安装 pdfplumber:pip install pdfplumber]"
def extract_file(path):
ext = os.path.splitext(path)[1].lower()
if ext == '.txt':
return extract_txt(path)
elif ext == '.docx':
return extract_docx(path)
elif ext == '.pdf':
return extract_pdf(path)
else:
return f"[不支持的格式: {ext}]"
def main():
if len(sys.argv) < 2:
print("用法:python3 extract_text.py <文件路径或目录路径>")
sys.exit(1)
target = os.path.expanduser(sys.argv[1])
files = []
if os.path.isdir(target):
for f in sorted(os.listdir(target)):
if f.lower().endswith(('.txt', '.docx', '.pdf')):
files.append(os.path.join(target, f))
elif os.path.isfile(target):
files = [target]
else:
print(f"路径不存在:{target}")
sys.exit(1)
if not files:
print("未找到支持的文件(.txt / .docx / .pdf)")
sys.exit(1)
print(f"找到 {len(files)} 个文件:")
all_text = []
for f in files:
print(f" ▶ {os.path.basename(f)}")
content = extract_file(f)
all_text.append(f"\n\n{'='*60}\n【文件】{os.path.basename(f)}\n{'='*60}\n{content}")
combined = "\n".join(all_text)
print(f"\n总字符数:{len(combined)}")
print(combined[:3000] + "\n...[截断显示前3000字符]" if len(combined) > 3000 else combined)
if __name__ == "__main__":
main()
FILE:references/coding-guide.md
# 编码操作指南
## 编码流程(逐行操作)
### 第一轮:开放编码
- 逐段阅读,标注所有有意义的片段
- 每个片段给出简短标签(3-8字)
- 不筛选,不评判,尽量覆盖全部资料
### 第二轮:聚焦编码
- 审查所有初始编码
- 合并同义/近义编码
- 删除冗余或明显偏离的编码
- 初步分组:哪些编码属于同一类?
### 第三轮:轴向编码(可选,演绎取向时使用)
- 建立编码之间的关系(原因-结果、条件-后果)
- 区分核心编码和边缘编码
---
## 编码记录格式
每条编码记录应包含:
```
编码名称:[简洁标签]
原文:「[完整引用,保留原始表达]」
来源:[文件名] 第[X]段 / 第[X]行
分析备注:[可选,记录分析思路]
```
---
## 编码质量检查
- [ ] 每条编码都有至少一条原文支撑
- [ ] 编码标签简洁且含义清晰
- [ ] 相似编码已合并处理
- [ ] 编码覆盖了资料中所有重要片段
- [ ] 负面案例(不符合主流模式的资料)已单独标注
---
## 主题-编码对应表示例
| 主题 | 子主题 | 编码 | 资料支撑数量 |
|------|--------|------|-------------|
| AI辅助学术写作 | 日常使用模式 | 每日AI写作实践 | 8 |
| | | AI替代文献检索 | 5 |
| | 焦虑与阻力 | AI使用污名化焦虑 | 6 |
| | | 导师不允许AI | 4 |
FILE:references/ethics-note.md
# 研究伦理提示
## 处理质性资料时的伦理注意事项
### 数据隐私
- 访谈资料通常包含受访者个人信息,**绝不上传到外部服务器**
- 本 Skill 的所有分析均在本地完成,数据不离开用户设备
- 分析报告中应对受访者进行匿名化处理(如:受访者A、B、C)
### 知情同意
- 确认受访者是否同意其资料用于分析
- AI辅助分析是否在知情同意书范围内
### 资料真实性
- AI 不会修改原始引文
- 所有引文均保留原始表达,不进行润色或改写
- 分析报告中标注引文来源(文件名 + 位置)
### 分析透明度
- 建议在研究报告的方法论部分说明使用了AI辅助编码
- 研究者对最终的主题命名和解释负责
- AI 提供初步编码建议,研究者进行审查和修正
### 数据安全建议
- 原始资料备份后再进行分析
- 分析结果文件(编码表、报告)妥善保存
- 不建议在公共场所或公共网络下操作敏感访谈资料
FILE:references/thematic-analysis-method.md
# 主题分析方法论详解
## Braun & Clarke (2006) 六阶段主题分析
主题分析(Thematic Analysis, TA)是质性研究中最灵活、应用最广泛的分析方法之一,不绑定特定认识论立场,适用于归纳式和演绎式研究。
### 理论定位
| 维度 | 说明 |
|------|------|
| 认识论 | 可兼容现实主义、建构主义、批判现实主义 |
| 分析取向 | 归纳式(数据驱动)or 演绎式(理论驱动)|
| 语义层次 | 语义层(显性含义)or 潜在层(隐性含义/话语)|
### 与其他方法的区分
- **vs 扎根理论**:TA 不要求理论建构,目标是描述性主题而非实质理论
- **vs 内容分析**:TA 关注意义和解释,内容分析更强调频率和量化
- **vs 话语分析**:TA 关注"说了什么",话语分析关注"如何说"及其社会效果
- **vs 现象学分析(IPA)**:TA 适合较大样本,IPA 聚焦个体经验细节
---
## 编码类型
### 描述性编码(Descriptive Codes)
直接概括资料内容,不添加解释。
> 示例:受访者描述"每天用AI写文献综述" → 编码:**日常AI写作实践**
### 主题编码(Thematic Codes)
捕捉更抽象的意义、模式或假设。
> 示例:反复出现"怕被发现用AI" → 编码:**AI使用的污名化焦虑**
### In Vivo 编码
直接用受访者的话作为编码标签,保留原始语言力量。
> 示例:受访者说"养了一只龙虾" → 编码:**"养龙虾"**
---
## 主题饱和标准
当以下条件满足时,认为主题达到饱和:
1. 新资料不再产生新编码
2. 已有主题能覆盖所有有意义的资料片段
3. 主题之间的关系稳定,无需重大调整
---
## 常见分析陷阱
| 陷阱 | 描述 | 解决方案 |
|------|------|----------|
| 主题过于宽泛 | "关于AI的一切"不是主题 | 确保每个主题有清晰焦点 |
| 主题即问题 | 把访谈问题当主题 | 主题应来自资料,而非问题设计 |
| 引文堆砌 | 大段引文没有分析 | 引文后必须有解读 |
| 脱离研究问题 | 有趣但无关的发现 | 定期对照研究问题检查 |
| 过度解读 | 用过多理论框架强加解释 | 保持对资料的忠实 |
---
## 反身性(Reflexivity)要求
分析者需记录并反思:
- 自身的研究立场和假设
- 与研究对象的关系
- 分析过程中的决策依据
建议在分析日志中持续记录。
知网(CNKI)高级检索论文自动化工具。当用户提供研究关键词(一组或多组)时,自动在知网 高级检索页面模拟人类检索行为:选择学术期刊类别、勾选CSSCI来源、输入主题关键词(含同义词 和同位词,用 + 连接)、多组关键词用OR关系连接,检索后按被引量排序、切换50条/页、 打开摘要视图,最终通过"导出与分析"功能...
---
name: cnki-advanced-search
description: >
知网(CNKI)高级检索论文自动化工具。当用户提供研究关键词(一组或多组)时,自动在知网
高级检索页面模拟人类检索行为:选择学术期刊类别、勾选CSSCI来源、输入主题关键词(含同义词
和同位词,用 + 连接)、多组关键词用OR关系连接,检索后按被引量排序、切换50条/页、
打开摘要视图,最终通过"导出与分析"功能以"查新(引文格式)"导出为Word文件,
包含完整的题录和摘要信息。
触发条件:用户提到需要在知网/CNKI检索论文、高级检索、按关键词搜索CSSCI/C刊论文、
下载题录信息、获取论文摘要、按被引排序检索;或说"帮我在知网检索XX相关论文"、
"用知网高级检索搜索XX主题的C刊论文"、"帮我检索XX关键词的CSSCI论文"。
---
# 知网高级检索论文工具
使用Chrome DevTools MCP工具在知网高级检索页面自动执行检索操作,提取CSSCI来源期刊论文的题录和摘要信息。
## ⛔ 本次执行必须产出(完成标准,不得跳过)
```
执行前阅读此清单,执行结束前逐条核对:
[ ] Step 1-8:检索流程完整执行(选学术期刊→勾CSSCI→填关键词→搜索→被引排序→50条/页→摘要视图)
[ ] Step 9:通过"导出与分析"→"导出文献"→"查新(引文格式)"导出Word文件
[ ] Step 10:Word文件已下载保存,文件路径已记录并告知用户
以上三项全部完成才算"检索完成",缺任何一项均不得向用户宣布完成。
```
## Step 0: 解析用户关键词
从用户输入中提取检索关键词,组织为检索表达式:
- **单组关键词**:关键词及其同义词/同位词用 ` + ` 连接(+前后各一个空格),填入同一个主题检索框
- 例:用户说"数字化转型"→ 检索词为 `数字化转型 + 数字化变革 + 数字化`
- **多组关键词**:每组填入独立的主题检索框,组间关系选OR
- 例:用户说"数字化转型与企业绩效"→ 第一组 `数字化转型 + 数字化变革`,第二组 `企业绩效 + 企业业绩 + 组织绩效`
向用户确认关键词分组和同义词扩展后再执行检索。
## Step 1: 打开知网高级检索页面
**⚠️ 必须使用 `profile="openclaw"`**(龙虾自管理的独立浏览器),绝对不能用 `profile="chrome"`(后者需要用户手动挂标签页,破坏自动化体验)。
```
browser(action="open", profile="openclaw", url="https://kns.cnki.net/kns8s/AdvSearch")
```
打开后立即 snapshot 确认页面加载状态,后续所有 browser 操作都传入同一个 `targetId`。
**验证码处理(重要!)**:
- snapshot 中存在"拖动下方拼图完成验证"或"安全验证"等 DOM 元素 → **不要停下来报告!直接继续操作表单**
- 这些文字通常是页面中常驻的隐藏元素,不代表验证码真正弹出拦截
- 只有当**表单本身无法操作**(填入关键词后点击检索失败,或页面明显被遮盖)时,才提示用户手动完成验证
- 判断原则:**试了才算**,不能只看 DOM 就认定被拦截
## Step 2: 选择"学术期刊"类别
snapshot 查看页面,**在页面底部或侧边栏找到文献类型选项卡**,点击"学术期刊"链接。
⚠️ **避免踩坑**:
- 页面底部有页脚区域,包含"关于我们"、"CNKI荣誉"等链接,这些 **不是** 文献类型选项卡
- 学术期刊链接通常在页面中部的选项卡区域,文字为"学术期刊"(ref 通常为较小编号)
- 点击后页面刷新,出现来源类别选项(CSSCI、SCI、北大核心等),才说明点击正确
## Step 3: 勾选CSSCI来源类别
snapshot 确认来源类别区域已显示,找到"CSSCI"对应的 checkbox 元素并点击勾选。
先取消"全部期刊"的勾选(如已勾选),再勾选"CSSCI"。
## Step 4: 输入主题检索词
### 4.1 第一组关键词
找到第一个"主题"检索框,**先 click 聚焦,再 type 输入**,不要使用 fill(fill 需要特殊的 fields 格式,容易报错):
```
browser(action="act", request={"kind": "click", "ref": "eXX"})
browser(action="act", request={"kind": "type", "ref": "eXX", "text": "老旧社区 + 老旧小区 + 旧城改造"})
```
⚠️ **不要用 `kind: fill`**,会报 "fields are required" 错误,改用 `click` + `type` 两步完成。
示例:`数字化转型 + 数字化变革 + 数字化`
### 4.2 多组关键词(如有)
若有多组关键词:
1. **点击"+"按钮**添加新检索行
2. take_snapshot查看新增行
3. **修改新增行的检索字段类型**:新增行默认可能不是"主题",需点击字段类型下拉框,选择"主题"
4. **修改逻辑运算符**:点击两行之间的运算符下拉框(默认"AND"),改选"OR"
5. **填入第二组关键词**:在新增行的检索框中fill第二组关键词表达式
重复以上步骤添加更多组。
### 运算符说明
知网高级检索框内的运算符:
- `+`(或):前后各留一个空格,如 `关键词A + 关键词B`
- `*`(与):前后各留一个空格
- `-`(非):前后各留一个空格
## Step 5: 执行检索
点击"检索"按钮,等待结果页加载。
```
wait_for → "检索结果" 或等待结果列表出现
```
若出现验证码,提示用户完成后重试。
## Step 6: 按被引量排序
在检索结果页take_snapshot,找到排序选项区域,点击"被引"排序按钮使结果按被引数量从高到低排列。
可能需要点击两次(第一次升序,第二次降序),确认排序方向为降序(被引最多的在前)。
## Step 7: 切换每页显示50条
take_snapshot查看分页区域,找到每页显示数量的下拉选项或链接(默认20条),切换为50条。
通常页面底部有 `20 | 50` 的选项,点击"50"。
## Step 8: 切换摘要视图
找到50旁边的视图切换图标(通常是列表视图/摘要视图的切换按钮),点击打开"摘要视图"(或称"详细视图"),使页面显示每篇论文的完整摘要。
take_snapshot确认摘要内容已展示。
## Step 9: 通过"导出与分析"导出题录和摘要
检索结果展示后(已完成被引排序、50条/页、摘要视图),通过知网自带的导出功能获取完整题录和摘要信息。
### 9.1 勾选要导出的论文
- 在结果列表上方或表头区域,找到"全选"复选框(通常标注为"全选"或位于序号列表头),点击勾选当前页全部论文
- 若总结果超过50篇且需要导出更多,先导出第一页,再翻页勾选导出第二页(最多导出前100篇)
- 若总结果不足50篇,全选即可
### 9.2 点击"导出与分析"
take_snapshot 找到页面上方工具栏中的 **"导出与分析"** 下拉菜单按钮,点击展开。
### 9.3 选择"导出文献"
在下拉菜单中点击 **"导出文献"**,等待导出选项页面/弹窗加载。
### 9.4 选择"查新(引文格式)"
在导出格式选项中,选择 **"查新(引文格式)"**。该格式包含完整的题录信息和摘要全文。
take_snapshot 确认已选中"查新(引文格式)"。
### 9.5 下载为 Word 文件
点击 **"导出"** 或 **"下载"** 按钮,选择 Word 格式下载。
⚠️ **注意**:
- 导出页面可能有多种下载格式选项(Word、PDF、TXT等),优先选择 **Word** 格式
- 如果页面有"在线阅读"和"下载"两个按钮,点击"下载"
- 下载完成后记录文件保存路径
### 9.6 多页导出(如需要)
若检索结果超过50篇且需导出前100篇:
1. 第一页导出完成后,返回检索结果页
2. 点击"下一页"翻到第2页
3. 重复 9.1-9.5 步骤导出第二页
4. 最终得到1-2个Word文件
## Step 10: 确认导出并告知用户
1. 确认Word文件已成功下载,检查文件是否存在且非空
2. 将文件移动/复制到 `~/Downloads/知网检索结果_{关键词摘要}_{日期}.docx`(如有多个文件则按页码编号)
3. 向用户报告:
- 检索关键词和条件
- 检索结果总数
- 实际导出论文数量
- 导出文件路径
- 文件内容说明(包含完整题录:标题、全部作者、期刊、年份卷期、摘要等)
## 注意事项
- 知网有反爬机制,每步操作间隔1-2秒,避免频繁请求
- 验证码出现时必须请用户手动完成
- 检索前向用户确认关键词分组和同义词扩展
- 全程向用户报告进度
- 若总结果不足100篇,告知用户实际数量并全部导出
- "查新(引文格式)"导出的Word文件包含完整作者列表、发表年份、期刊卷期号和摘要全文,信息比页面列表视图更准确完整
- 若导出功能受限(如需要登录、付费等),回退到snapshot逐条读取并用Python生成Excel
C刊(CSSCI来源期刊)论文全面分析工具。当用户提供一个具体的C刊期刊名称(如"管理世界"、 "社会学研究"、"经济研究"等)时,自动通过知网(CNKI)查询该期刊最近5年所有期次的文章 目录、作者和摘要信息,并生成专业的Word分析报告。报告包含:选题热点趋势、高频关键词、 研究方法偏好、核心作者群、栏目主题...
---
name: cjournal-analyzer
description: >
C刊(CSSCI来源期刊)论文全面分析工具。当用户提供一个具体的C刊期刊名称(如"管理世界"、
"社会学研究"、"经济研究"等)时,自动通过知网(CNKI)查询该期刊最近5年所有期次的文章
目录、作者和摘要信息,并生成专业的Word分析报告。报告包含:选题热点趋势、高频关键词、
研究方法偏好、核心作者群、栏目主题演变、研究空白识别、投稿方向建议等全维度分析。
触发条件:用户提到需要分析某个C刊/CSSCI期刊/核心期刊的发文趋势、选题偏好、投稿方向;
或提供中文学术期刊名称并要求查看近年发表论文的主题分布和趋势;或说"帮我分析一下XX期刊"。
注意:本skill用于期刊层面的宏观分析,不同于paper-analyzer(单篇论文拆解)和
literature-review-writer(文献综述写作)。
---
# C刊论文全面分析工具
## Phase 1: 确定期刊与CNKI代码
1. 从用户输入中提取期刊名称
2. 查询 `references/journal_codes.md` 获取CNKI代码(如"管理世界"→ `GLSJ`)
3. 若未收录,用WebSearch搜索 `site:navi.cnki.net/knavi/journals "{期刊名}"` 从URL提取代码
4. 向用户确认期刊后继续
## Phase 2: 浏览器数据采集
使用Chrome DevTools MCP工具从知网采集数据。
### Step 2.1: 打开期刊页
```
navigate_page → https://navi.cnki.net/knavi/journals/{CODE}/detail
```
**验证码处理**:若页面出现"请完成安全验证"或"拖动下方拼图",立即提示用户:
> "知网需要安全验证,请在浏览器中完成滑块验证,完成后告诉我。"
等用户确认后,用 `navigate_page` 重新加载页面。
### Step 2.2: 提取期刊基本信息
```javascript
() => {
const title = document.querySelector('h3')?.textContent?.trim() || '';
const info = {};
document.querySelectorAll('.detailInfo p, .s-info p').forEach(p => {
const text = p.textContent;
if (text.includes('主办单位')) info.sponsor = text.split(':')[1]?.trim();
if (text.includes('ISSN')) info.issn = text.split(':')[1]?.trim();
if (text.includes('CN')) info.cn = text.split(':')[1]?.trim();
if (text.includes('出版周期')) info.frequency = text.split(':')[1]?.trim();
if (text.includes('复合影响因子')) info.cif = text.split(':')[1]?.trim();
if (text.includes('综合影响因子')) info.aif = text.split(':')[1]?.trim();
});
return { title, ...info };
}
```
也可直接从snapshot中读取基本信息(StaticText节点)。
### Step 2.3: 点击"论文"标签并提取年份期次
点击 uid 对应"论文"的链接,等待加载,然后提取:
```javascript
() => {
const results = [];
document.querySelectorAll('dl[id$="_Year_Issue"]').forEach(dl => {
const year = dl.querySelector('dt em')?.textContent?.trim();
if (!year) return;
const issues = [];
dl.querySelectorAll('dd a').forEach(a => {
issues.push({ id: a.id, issue: a.textContent.trim(), value: a.getAttribute('value') });
});
results.push({ year: parseInt(year), issues });
});
return results;
}
```
筛选最近5年数据(当前年份 - 4 至当前年份)。若知网分页显示年份(每页显示部分年份),需翻页加载更多。
### Step 2.4: 逐期采集文章列表
对每个期次,点击对应的期次链接(通过 `click` uid 或 `evaluate_script` 模拟点击),等待文章列表加载(`wait_for` 等待标题出现或等1-2秒),然后提取:
```javascript
() => {
const articles = [];
document.querySelectorAll('#CatalogList dd.row').forEach(dd => {
const titleEl = dd.querySelector('span.name a');
const authorEl = dd.querySelector('span.author');
const pageEl = dd.querySelector('span.company');
const sectionEl = dd.closest('div')?.querySelector('dt.tit');
if (titleEl) {
articles.push({
title: titleEl.textContent.trim(),
url: titleEl.href,
authors: authorEl?.getAttribute('title')?.replace(/;$/,'') || '',
pages: pageEl?.getAttribute('title') || '',
section: sectionEl?.textContent?.trim() || ''
});
}
});
return articles;
}
```
**关键**:
- 每个期次采集间隔1-2秒,避免触发反爬
- 持续向用户报告进度(如"正在采集2024年第6期,已完成32/60期...")
- 所有数据暂存到一个JSON数组中
### Step 2.5: 摘要采集(抽样策略)
全量摘要采集耗时极长,采用抽样:每年选取2期(如第1期和第7期),每期取前3篇文章访问摘要页。
访问文章详情页后提取摘要:
```javascript
() => {
const abs = document.querySelector('#ChDivSummary, .abstract-text, [name="abstracts"]');
const kw = document.querySelector('#ChDivKeyWord, .keywords');
return {
abstract: abs?.textContent?.trim() || '',
keywords: kw?.textContent?.replace('关键词:','').trim() || ''
};
}
```
若浏览器方式受阻,用WebSearch搜索 `"{文章标题}" site:cnki.net` 补充摘要和关键词。
## Phase 3: 数据分析
采集完成后,将所有数据保存为JSON,然后运行 `scripts/analyze_journal.py` 进行分析。
该脚本依赖:`pip3 install jieba wordcloud python-docx matplotlib numpy`
脚本接收JSON数据文件路径,输出分析结果和可视化图表:
1. **发文量趋势**:按年度统计发文数量折线图
2. **高频关键词Top30**:jieba分词 → 去停用词 → 词频统计 → 柱状图+词云
3. **主题聚类**:基于高频词共现进行粗粒度主题归类
4. **栏目分析**:各栏目发文占比饼图及趋势变化
5. **核心作者Top20**:发文频次柱状图
6. **研究方法识别**:从标题中匹配方法关键词(实证/案例/实验/模型/仿真/调查/访谈/文献计量/元分析/回归/面板数据/DID/RDD/PSM/机器学习/深度学习/SEM/扎根理论等)
7. **热点演变**:前3年 vs 近2年的关键词对比,识别新兴/衰退主题
8. **研究空白与投稿建议**:基于以上分析综合给出
## Phase 4: 生成Word报告
使用python-docx生成格式化报告,结构:
```
封面:《{期刊名}》近五年({起始年}-{结束年})发文分析报告
一、期刊概况
二、发文量与趋势分析(含图表)
三、选题热点分析(含词云图、高频词柱状图)
四、热点演变与新兴主题
五、核心作者群分析
六、研究方法偏好分析
七、栏目主题分析
八、研究空白与投稿建议
附录:完整文章目录(按年份-期次排列)
```
报告保存至 `~/Downloads/{期刊名}_近五年发文分析报告.docx`。
## 注意事项
- 知网有反爬机制,每次请求间隔≥1秒
- 验证码出现时必须请用户手动完成
- 安装依赖:`pip3 install jieba wordcloud python-docx matplotlib numpy`
- 期刊代码优先查 `references/journal_codes.md`
- 采集全程保持进度播报
FILE:scripts/analyze_journal.py
#!/usr/bin/env python3
"""C刊论文数据分析与Word报告生成脚本
用法: python3 analyze_journal.py <data.json> [output_dir]
data.json格式:
{
"journal_info": { "title": "...", "issn": "...", "sponsor": "...", ... },
"articles": [
{ "year": 2024, "issue": "01", "title": "...", "authors": "...", "section": "...", "abstract": "", "keywords": "" },
...
]
}
"""
import json
import sys
import os
import re
from collections import Counter, defaultdict
from pathlib import Path
try:
import jieba
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'Heiti TC', 'SimHei']
matplotlib.rcParams['axes.unicode_minus'] = False
matplotlib.rcParams['figure.dpi'] = 150
import matplotlib.pyplot as plt
import numpy as np
from wordcloud import WordCloud
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
except ImportError as e:
print(f"缺少依赖: {e}")
print("请运行: pip3 install jieba wordcloud python-docx matplotlib numpy")
sys.exit(1)
# ============================================================
# 停用词表
# ============================================================
STOPWORDS = set("""
的 了 在 是 我 有 和 就 不 人 都 一 一个 上 也 很 到 说 要 去 你
对 出 会 着 没有 看 好 自己 这 他 她 它 们 那 些 什么 之 与 及
为 中 以 或 其 被 从 但 而 可 所 这个 那个 因为 所以 如果 虽然
通过 进行 基于 研究 分析 问题 中国 影响 发展 我国 探讨 关于
视角 效应 论 试论 浅析 浅谈 探析 略论 新 论述
""".split())
# 研究方法关键词
METHOD_KEYWORDS = {
'实证研究': ['实证', '实证研究', '实证分析', '经验研究'],
'案例研究': ['案例', '案例研究', '个案', '案例分析'],
'实验研究': ['实验', '随机实验', '田野实验', '自然实验', '准实验'],
'计量模型': ['回归', 'OLS', '面板数据', '固定效应', '工具变量', 'IV', 'GMM', '计量'],
'DID/RDD': ['双重差分', 'DID', '断点回归', 'RDD', '倍差法'],
'PSM': ['倾向得分匹配', 'PSM', '匹配估计'],
'结构方程': ['SEM', '结构方程', '路径分析'],
'机器学习/AI': ['机器学习', '深度学习', '神经网络', '人工智能', 'AI', '大数据', '文本分析', 'NLP'],
'质性研究': ['扎根理论', '质性', '访谈', '民族志', '叙事', '话语分析'],
'调查研究': ['问卷', '调查', '抽样'],
'文献研究': ['文献计量', '元分析', 'meta分析', '系统综述', '文献综述'],
'博弈/仿真': ['博弈', '仿真', '模拟', 'ABM', '演化博弈'],
}
def load_data(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
def segment_titles(articles):
"""对文章标题进行jieba分词"""
all_words = []
for art in articles:
words = jieba.lcut(art['title'])
words = [w.strip() for w in words if len(w.strip()) >= 2 and w.strip() not in STOPWORDS]
all_words.extend(words)
return all_words
def analyze_methods(articles):
"""识别研究方法"""
method_counts = Counter()
for art in articles:
title = art['title'] + ' ' + art.get('abstract', '') + ' ' + art.get('keywords', '')
for method, keywords in METHOD_KEYWORDS.items():
for kw in keywords:
if kw.lower() in title.lower():
method_counts[method] += 1
break
return method_counts
def analyze_authors(articles):
"""统计作者发文量"""
author_counts = Counter()
for art in articles:
authors = art.get('authors', '')
for author in re.split(r'[;;,,、]', authors):
author = author.strip()
if author and len(author) >= 2:
author_counts[author] += 1
return author_counts
def analyze_sections(articles):
"""统计栏目分布"""
section_counts = Counter()
for art in articles:
section = art.get('section', '').strip()
if section:
section_counts[section] += 1
return section_counts
def analyze_yearly_trends(articles):
"""按年度统计"""
yearly = defaultdict(list)
for art in articles:
yearly[art['year']].append(art)
return dict(sorted(yearly.items()))
def compare_periods(articles, current_year):
"""前期vs近期关键词对比"""
early = [a for a in articles if a['year'] <= current_year - 2]
recent = [a for a in articles if a['year'] > current_year - 2]
early_words = Counter(segment_titles(early))
recent_words = Counter(segment_titles(recent))
# 新兴主题: 近期高频但前期低频
emerging = {}
for word, count in recent_words.most_common(100):
early_count = early_words.get(word, 0)
if early_count == 0 and count >= 3:
emerging[word] = count
elif early_count > 0:
ratio = count / len(recent) - early_count / len(early) if len(early) > 0 else 0
if ratio > 0.005 and count >= 3:
emerging[word] = round(ratio, 4)
# 衰退主题: 前期高频但近期低频
declining = {}
for word, count in early_words.most_common(100):
recent_count = recent_words.get(word, 0)
if len(recent) > 0 and len(early) > 0:
ratio = count / len(early) - recent_count / len(recent)
if ratio > 0.005 and count >= 3:
declining[word] = round(ratio, 4)
return emerging, declining
def create_charts(articles, output_dir, journal_name):
"""生成所有可视化图表"""
os.makedirs(output_dir, exist_ok=True)
charts = {}
# 1. 发文量趋势
yearly = analyze_yearly_trends(articles)
years = sorted(yearly.keys())
counts = [len(yearly[y]) for y in years]
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(years, counts, 'o-', color='#E8737A', linewidth=2.5, markersize=8)
ax.fill_between(years, counts, alpha=0.15, color='#E8737A')
for y, c in zip(years, counts):
ax.text(y, c + max(counts)*0.02, str(c), ha='center', fontsize=11, fontweight='bold')
ax.set_xlabel('年份', fontsize=12)
ax.set_ylabel('发文量', fontsize=12)
ax.set_title(f'《{journal_name}》年度发文量趋势', fontsize=14, fontweight='bold')
ax.set_xticks(years)
plt.tight_layout()
path = f'{output_dir}/01_yearly_trend.png'
plt.savefig(path, bbox_inches='tight')
plt.close()
charts['yearly_trend'] = path
# 2. 高频关键词Top30
all_words = segment_titles(articles)
word_freq = Counter(all_words).most_common(30)
fig, ax = plt.subplots(figsize=(12, 7))
words_list = [w for w, _ in word_freq][::-1]
freqs_list = [f for _, f in word_freq][::-1]
colors = plt.cm.RdYlBu_r(np.linspace(0.2, 0.8, len(words_list)))
ax.barh(range(len(words_list)), freqs_list, color=colors, edgecolor='white')
ax.set_yticks(range(len(words_list)))
ax.set_yticklabels(words_list, fontsize=10)
ax.set_xlabel('频次', fontsize=12)
ax.set_title(f'《{journal_name}》高频关键词Top30', fontsize=14, fontweight='bold')
plt.tight_layout()
path = f'{output_dir}/02_top_keywords.png'
plt.savefig(path, bbox_inches='tight')
plt.close()
charts['top_keywords'] = path
# 3. 词云
word_dict = dict(Counter(all_words).most_common(200))
try:
font_path = '/System/Library/Fonts/PingFang.ttc'
if not os.path.exists(font_path):
font_path = '/System/Library/Fonts/STHeiti Light.ttc'
wc = WordCloud(
font_path=font_path,
width=1200, height=600,
background_color='white',
max_words=150,
colormap='RdYlBu_r',
max_font_size=120
).generate_from_frequencies(word_dict)
fig, ax = plt.subplots(figsize=(14, 7))
ax.imshow(wc, interpolation='bilinear')
ax.axis('off')
ax.set_title(f'《{journal_name}》关键词词云', fontsize=14, fontweight='bold')
path = f'{output_dir}/03_wordcloud.png'
plt.savefig(path, bbox_inches='tight')
plt.close()
charts['wordcloud'] = path
except Exception as e:
print(f"词云生成失败: {e}")
# 4. 核心作者Top20
author_freq = analyze_authors(articles).most_common(20)
if author_freq:
fig, ax = plt.subplots(figsize=(12, 7))
names = [a for a, _ in author_freq][::-1]
freqs = [f for _, f in author_freq][::-1]
ax.barh(range(len(names)), freqs, color='#5BA3CF', edgecolor='white')
ax.set_yticks(range(len(names)))
ax.set_yticklabels(names, fontsize=10)
ax.set_xlabel('发文量', fontsize=12)
ax.set_title(f'《{journal_name}》高产作者Top20', fontsize=14, fontweight='bold')
plt.tight_layout()
path = f'{output_dir}/04_top_authors.png'
plt.savefig(path, bbox_inches='tight')
plt.close()
charts['top_authors'] = path
# 5. 研究方法分布
method_freq = analyze_methods(articles)
if method_freq:
fig, ax = plt.subplots(figsize=(10, 6))
methods = sorted(method_freq.items(), key=lambda x: x[1], reverse=True)
m_names = [m for m, _ in methods]
m_counts = [c for _, c in methods]
colors_m = plt.cm.Set3(np.linspace(0, 1, len(m_names)))
ax.bar(m_names, m_counts, color=colors_m, edgecolor='white')
ax.set_ylabel('篇数', fontsize=12)
ax.set_title(f'《{journal_name}》研究方法分布', fontsize=14, fontweight='bold')
plt.xticks(rotation=30, ha='right', fontsize=10)
plt.tight_layout()
path = f'{output_dir}/05_methods.png'
plt.savefig(path, bbox_inches='tight')
plt.close()
charts['methods'] = path
# 6. 栏目分布
section_freq = analyze_sections(articles)
if section_freq:
top_sections = section_freq.most_common(10)
fig, ax = plt.subplots(figsize=(10, 6))
s_names = [s for s, _ in top_sections]
s_counts = [c for _, c in top_sections]
colors_s = plt.cm.Pastel1(np.linspace(0, 1, len(s_names)))
wedges, texts, autotexts = ax.pie(s_counts, labels=s_names, autopct='%1.1f%%',
colors=colors_s, textprops={'fontsize': 9})
ax.set_title(f'《{journal_name}》栏目分布', fontsize=14, fontweight='bold')
path = f'{output_dir}/06_sections.png'
plt.savefig(path, bbox_inches='tight')
plt.close()
charts['sections'] = path
return charts
def generate_report(data, charts, output_path):
"""生成Word报告"""
journal = data.get('journal_info', {})
articles = data.get('articles', [])
journal_name = journal.get('title', '未知期刊')
years = sorted(set(a['year'] for a in articles))
year_range = f"{min(years)}-{max(years)}" if years else "N/A"
doc = Document()
section = doc.sections[0]
section.page_width = Cm(21)
section.page_height = Cm(29.7)
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(3.17)
def add_text(text, bold=False, size=10.5, indent=True):
p = doc.add_paragraph()
run = p.add_run(text)
run.font.size = Pt(size)
run.font.name = 'Times New Roman'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
run.bold = bold
if indent:
p.paragraph_format.first_line_indent = Cm(0.74)
p.paragraph_format.line_spacing = Pt(22)
return p
# 封面
for _ in range(6):
doc.add_paragraph()
title_p = doc.add_paragraph()
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = title_p.add_run(f'《{journal_name}》')
run.bold = True
run.font.size = Pt(26)
run.font.name = '黑体'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
sub_p = doc.add_paragraph()
sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = sub_p.add_run(f'近五年({year_range})发文分析报告')
run.font.size = Pt(18)
run.font.name = '黑体'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
doc.add_paragraph()
date_p = doc.add_paragraph()
date_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = date_p.add_run('由 Claude Code 自动生成')
run.font.size = Pt(12)
run.font.color.rgb = RGBColor(128, 128, 128)
doc.add_page_break()
# 一、期刊概况
doc.add_heading('一、期刊概况', level=1)
info_text = f"《{journal_name}》"
if journal.get('sponsor'):
info_text += f"由{journal['sponsor']}主办"
if journal.get('frequency'):
info_text += f",{journal['frequency']}出版"
if journal.get('issn'):
info_text += f",ISSN: {journal['issn']}"
if journal.get('cif'):
info_text += f"。复合影响因子为{journal['cif']}"
if journal.get('aif'):
info_text += f",综合影响因子为{journal['aif']}"
info_text += f"。本报告分析了该刊{year_range}年间共{len(articles)}篇文章的发文数据。"
add_text(info_text)
# 二、发文量与趋势
doc.add_heading('二、发文量与趋势分析', level=1)
yearly = analyze_yearly_trends(articles)
trend_text = "从年度发文量来看,"
for y in sorted(yearly.keys()):
trend_text += f"{y}年发文{len(yearly[y])}篇,"
trend_text = trend_text.rstrip(',') + "。"
add_text(trend_text)
if 'yearly_trend' in charts:
doc.add_picture(charts['yearly_trend'], width=Inches(5.5))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
# 三、选题热点
doc.add_heading('三、选题热点分析', level=1)
all_words = segment_titles(articles)
top30 = Counter(all_words).most_common(30)
kw_text = f"对全部{len(articles)}篇文章标题进行中文分词后,出现频率最高的30个关键词如下图所示。"
kw_text += f"其中,排名前5的关键词为:{'、'.join([w for w,_ in top30[:5]])}。"
add_text(kw_text)
if 'top_keywords' in charts:
doc.add_picture(charts['top_keywords'], width=Inches(5.5))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
if 'wordcloud' in charts:
doc.add_picture(charts['wordcloud'], width=Inches(5.5))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
# 四、热点演变
doc.add_heading('四、热点演变与新兴主题', level=1)
if len(years) >= 3:
current_year = max(years)
emerging, declining = compare_periods(articles, current_year)
if emerging:
top_emerging = sorted(emerging.items(), key=lambda x: x[1], reverse=True)[:10]
add_text(f"与前期相比,近两年新兴或上升的研究主题包括:{'、'.join([w for w,_ in top_emerging])}。")
if declining:
top_declining = sorted(declining.items(), key=lambda x: x[1], reverse=True)[:10]
add_text(f"关注度有所下降的主题包括:{'、'.join([w for w,_ in top_declining])}。")
else:
add_text("数据跨度不足3年,暂无法进行趋势对比分析。")
# 五、核心作者
doc.add_heading('五、核心作者群分析', level=1)
author_freq = analyze_authors(articles).most_common(20)
if author_freq:
add_text(f"发文量排名前5的作者为:" +
"、".join([f"{a}({c}篇)" for a, c in author_freq[:5]]) + "。")
if 'top_authors' in charts:
doc.add_picture(charts['top_authors'], width=Inches(5.5))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
# 六、研究方法
doc.add_heading('六、研究方法偏好分析', level=1)
method_freq = analyze_methods(articles)
if method_freq:
top_methods = method_freq.most_common(5)
add_text(f"从研究方法来看,该刊最常见的方法类型为:" +
"、".join([f"{m}({c}篇)" for m, c in top_methods]) + "。")
if 'methods' in charts:
doc.add_picture(charts['methods'], width=Inches(5.5))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
# 七、栏目分析
doc.add_heading('七、栏目主题分析', level=1)
section_freq = analyze_sections(articles)
if section_freq:
top_sections = section_freq.most_common(5)
add_text(f"该刊主要栏目及发文量为:" +
"、".join([f"{s}({c}篇)" for s, c in top_sections]) + "。")
if 'sections' in charts:
doc.add_picture(charts['sections'], width=Inches(5.5))
doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
# 八、研究空白与投稿建议
doc.add_heading('八、研究空白与投稿建议', level=1)
add_text("基于以上分析,以下方面可能存在研究空白或发文机会:", bold=True, indent=False)
add_text("(本节内容需结合具体学科知识由研究者进一步判断。以上数据分析可为选题提供参考方向。)")
# 附录
doc.add_page_break()
doc.add_heading('附录:完整文章目录', level=1)
for year in sorted(yearly.keys(), reverse=True):
doc.add_heading(f'{year}年', level=2)
for art in yearly[year]:
p = doc.add_paragraph()
run = p.add_run(f"[{art.get('issue','')}期] {art['title']}")
run.font.size = Pt(9)
run.font.name = 'Times New Roman'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
if art.get('authors'):
run = p.add_run(f" —— {art['authors']}")
run.font.size = Pt(8)
run.font.color.rgb = RGBColor(128, 128, 128)
p.paragraph_format.space_after = Pt(2)
p.paragraph_format.line_spacing = Pt(16)
doc.save(output_path)
return output_path
def main():
if len(sys.argv) < 2:
print("用法: python3 analyze_journal.py <data.json> [output_dir]")
sys.exit(1)
data_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.dirname(data_path)
data = load_data(data_path)
journal_name = data.get('journal_info', {}).get('title', '未知期刊')
articles = data.get('articles', [])
print(f"期刊: {journal_name}")
print(f"文章总数: {len(articles)}")
chart_dir = os.path.join(output_dir, f'{journal_name}_charts')
charts = create_charts(articles, chart_dir, journal_name)
print(f"图表已生成: {chart_dir}/")
report_path = os.path.join(output_dir, f'{journal_name}_近五年发文分析报告.docx')
generate_report(data, charts, report_path)
print(f"报告已生成: {report_path}")
if __name__ == '__main__':
main()
FILE:references/journal_codes.md
# CNKI期刊代码速查表
常见C刊(CSSCI来源期刊)的CNKI代码。格式:`期刊名 | 代码 | ISSN`
## 经济学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 经济研究 | JJYJ | 0577-9154 |
| 管理世界 | GLSJ | 1002-5502 |
| 经济学(季刊) | JJXU | 1674-7917 |
| 中国工业经济 | GGYY | 1006-480X |
| 世界经济 | SJJJ | 1002-9621 |
| 金融研究 | JRYJ | 1002-7246 |
| 经济学动态 | JJXD | 1002-8390 |
| 中国农村经济 | ZNJJ | 1002-8870 |
| 数量经济技术经济研究 | SLJY | 1000-3894 |
| 财贸经济 | CMJJ | 1002-8102 |
| 财经研究 | CJYJ | 1001-9952 |
| 国际经济评论 | GJPP | 1007-0974 |
| 经济科学 | JJKX | 0257-5140 |
| 经济理论与经济管理 | JJLL | 1000-596X |
| 南开经济研究 | NKJJ | 1001-4691 |
## 管理学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 管理科学学报 | JCYJ | 1007-9807 |
| 南开管理评论 | LKGP | 1008-3448 |
| 中国管理科学 | ZGGK | 1003-207X |
| 管理评论 | GLPL | 1003-1952 |
| 管理学报 | MANA | 1672-884X |
| 科研管理 | KYGL | 1000-2995 |
| 研究与发展管理 | YJYF | 1004-8308 |
| 科学学研究 | KXYJ | 1003-2053 |
| 科学学与科学技术管理 | KXYU | 1002-0241 |
## 社会学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 社会学研究 | SHYJ | 1002-5936 |
| 社会 | SHEH | 1004-8804 |
| 中国人口科学 | ZKRK | 1000-7881 |
| 人口研究 | RKYZ | 1000-6087 |
| 人口与经济 | RKJJ | 1000-4149 |
| 青年研究 | QNYJ | 1008-1437 |
| 妇女研究论丛 | FNYJ | 1004-2563 |
## 政治学与公共管理
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 中国行政管理 | ZXGL | 1006-0863 |
| 政治学研究 | POLI | 1000-3355 |
| 公共管理学报 | GGGL | 1672-6162 |
| 中国软科学 | ZGRK | 1002-9753 |
| 公共行政评论 | GGXZ | 1674-2486 |
## 法学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 中国法学 | ZGFX | 1003-1707 |
| 法学研究 | LAWS | 1002-896X |
| 法学 | FAXU | 1000-4238 |
| 中外法学 | ZWFX | 1002-4875 |
| 法学家 | FXJA | 1005-0221 |
| 法商研究 | ZFSX | 1672-0393 |
| 法律科学 | FLKX | 1674-5205 |
| 现代法学 | XDFX | 1001-2397 |
| 清华法学 | QHFX | 1673-9280 |
| 政法论坛 | ZFLT | 1000-0208 |
## 教育学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 教育研究 | JYYJ | 1002-5731 |
| 高等教育研究 | HIGH | 1000-4203 |
| 北京大学教育评论 | BJDJ | 1671-9468 |
| 华东师范大学学报(教育科学版) | HDJK | 1000-5560 |
| 清华大学教育研究 | QHDZ | 1001-4519 |
| 教育发展研究 | SHGJ | 1008-3855 |
## 新闻传播学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 新闻与传播研究 | XWCB | 1005-2577 |
| 国际新闻界 | GJXW | 1002-5685 |
| 现代传播 | XDCB | 1007-8770 |
| 新闻大学 | XWDX | 1006-1460 |
| 编辑学报 | BJXB | 1001-4314 |
## 图书馆学与情报学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 中国图书馆学报 | ZGTS | 1001-8867 |
| 大学图书馆学报 | DXTS | 1002-1027 |
| 图书情报工作 | TSQB | 0252-3116 |
| 情报学报 | QBXB | 1000-0135 |
| 图书情报知识 | TSQZ | 1003-2797 |
## 会计与财务
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 会计研究 | KJYJ | 1003-2886 |
| 审计研究 | SJYZ | 1002-4239 |
| 财务研究 | CWYA | 2095-8838 |
## 心理学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 心理学报 | XLXB | 0439-755X |
| 心理科学 | XLKX | 1671-6981 |
| 心理科学进展 | XLXJ | 1671-3710 |
| 心理发展与教育 | XLFZ | 1001-4918 |
## 哲学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 哲学研究 | ZXYJ | 1000-0216 |
| 哲学动态 | ZXDT | 1002-8862 |
| 中国哲学史 | CZSX | 1005-0396 |
## 历史学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 历史研究 | LSYJ | 0459-1909 |
| 近代史研究 | JDSY | 1001-6708 |
| 中国史研究 | ZSYJ | 1002-7963 |
| 世界历史 | SJLS | 1002-011X |
## 文学与语言学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 文学评论 | WXPL | 0511-4683 |
| 文艺研究 | WYYJ | 0257-5876 |
| 外国文学评论 | WGWX | 1001-6368 |
| 中国语文 | YWZG | 0578-1949 |
| 当代语言学 | DDYX | 1007-8274 |
| 世界汉语教学 | SJHY | 1002-5804 |
| 语言教学与研究 | YYJX | 0257-9448 |
## 统计学
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 统计研究 | TJYJ | 1002-4565 |
| 统计与信息论坛 | TJLT | 1007-3116 |
## 信息系统与管理
| 期刊名 | 代码 | ISSN |
|--------|------|------|
| 管理信息系统 | XXJL | 1007-3221 |
---
**查找方法**:若上表未收录,访问 `https://navi.cnki.net/knavi/` 搜索期刊名,从URL中提取代码(如 `/journals/XXXX/detail` 中的 `XXXX`)。