chore: migrate repository to standard knowledge base layout

This commit is contained in:
tukuaiai
2026-05-02 03:29:06 +08:00
parent 40a721c24d
commit 628a3bc832
565 changed files with 687 additions and 711 deletions
+38
View File
@@ -0,0 +1,38 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
# 凭据和密钥
credentials.json
token.json
*.key
.env
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# 临时文件
*.tmp
*.bak
*.log
temp/
# Excel临时文件
~$*.xlsx
~$*.xls
# 协作记录文件
ticket.md
+278
View File
@@ -0,0 +1,278 @@
<!--
-------------------------------------------------------------------------------
项目头部区域 (HEADER)
这是用户第一眼看到的地方。一个精美的横幅或 Logo 能立刻提升专业感。
-------------------------------------------------------------------------------
-->
<p align="center">
<!-- 建议尺寸: 1280x640px。可以使用 Canva, Figma 或 https://banners.beyondco.de/ 等工具制作 -->
<img src="https://github.com/tukuaiai.png" alt="项目横幅">
</p>
<div align="center">
# Prompt Library
**一个全面的高质量AI提示词库,支持Excel和Markdown格式之间的双向转换。**
---
<!--
徽章区域 (BADGES)
从 https://shields.io/ 生成。选择与你的品牌色一致的颜色。
建议包含:构建状态, 版本, 许可证, 语言, 代码大小, 下载量, 社区链接等。
-->
<p>
<a href="https://github.com/tukuaiai/prompt-library/actions/workflows/sync.yml"><img src="https://img.shields.io/github/actions/workflow/status/tukuaiai/prompt-library/sync.yml?style=for-the-badge" alt="构建状态"></a>
<a href="https://github.com/tukuaiai/prompt-library/releases"><img src="https://img.shields.io/github/v/release/tukuaiai/prompt-library?style=for-the-badge" alt="最新版本"></a>
<a href="https://github.com/tukuaiai/prompt-library/blob/main/LICENSE"><img src="https://img.shields.io/github/license/tukuaiai/prompt-library?style=for-the-badge" alt="许可证"></a>
<a href="https://github.com/tukuaiai/prompt-library"><img src="https://img.shields.io/github/languages/top/tukuaiai/prompt-library?style=for-the-badge" alt="主要语言"></a>
<a href="https://github.com/tukuaiai/prompt-library"><img src="https://img.shields.io/github/languages/code-size/tukuaiai/prompt-library?style=for-the-badge" alt="代码大小"></a>
</p>
[✨ 功能特性](#-功能特性) •
[🚀 快速开始](#-快速开始) •
[⚙️ API参考](#-api参考) •
[🤝 参与贡献](#-参与贡献) •
[🗺️ 路线图](#-路线图)
</div>
<!--
-------------------------------------------------------------------------------
概览与核心视觉区域 (OVERVIEW & VISUALS)
展示项目最直观的部分。
-------------------------------------------------------------------------------
-->
---
## 🖼️ 概览与演示
本词库是一个结构化的、高质量的AI提示词(Prompts)集合,旨在为开发人员、研究人员和内容创作者提供强大而灵活的工具。与许多静态的提示词列表不同,本项目提供了一个双向转换的工作流程,允许用户在易于协作的Excel格式和便于版本控制的Markdown格式之间无缝切换。
> **核心理念**: 让高质量的提示词像代码一样被管理、分享和迭代。
<p align="center">
<!-- 建议使用 GIF,工具: ScreenToGif, GIPHY CAPTURE -->
<img src="https://user-images.githubusercontent.com/12523395/269150161-1a83689c-2f3a-4a0e-8d18-114cec03be8d.gif" alt="项目演示动画" width="80%">
</p>
---
<!--
-------------------------------------------------------------------------------
主要内容区域 (MAIN CONTENT)
详细介绍项目的方方面面。
-------------------------------------------------------------------------------
-->
## ✨ 功能特性
- 📊 **双向转换**: 支持Excel ↔️ Markdown格式互转,兼顾易用性与版本控制。
- 🗂️ **结构化管理**: 包含多个分类,覆盖从软件工程到哲学思辨的广泛领域。
- 🤖 **多平台兼容**: 提示词设计兼容Claude、GPT、Gemini等主流AI模型。
- 🛠️ **自动化工具**: 提供命令行工具,支持批量转换和管理。
- 🎨 **易于扩展**: 可以方便地添加新的提示词、分类和自定义属性。
---
## ⚙️ 架构与工作流程
本项目的工作流程围绕“以结构化数据为中心”的思想构建。
```mermaid
graph LR
subgraph "Excel-First 工作流"
A[1. 在 Excel 中编辑提示词] --> B{运行转换脚本};
end
subgraph "Git-Native 工作流"
D[3. 在 Markdown 文件中编辑] --> E{运行转换脚本};
end
subgraph "中央产物"
B --> C[2. 生成结构化的 Markdown 文件];
E --> F[4. 更新或生成 Excel 文件]
end
style A fill:#D5E8D4
style D fill:#DAE8FC
style C fill:#F8CECC
style F fill:#F8CECC
```
这个流程确保了无论是喜欢电子表格的非技术人员,还是习惯于Git和代码编辑器的开发人员,都可以高效地协作。
---
## 🚀 快速开始
### 1. 环境依赖
- [Python](https://www.python.org/) >= 3.8
### 2. 安装
<details>
<summary><b>从源码构建</b></summary>
```bash
git clone https://github.com/tukuaiai/prompt-library.git
cd prompt-library
pip install -r requirements.txt
```
</details>
### 3. 使用
<details>
<summary><b>Excel → Markdown 转换</b></summary>
```bash
# 运行交互式转换
python3 main.py
```
程序将扫描 `prompt_excel` 目录下的 `.xlsx` 文件,并让你选择一个进行转换。结果将输出到 `prompt_docs` 目录下一个带时间戳的文件夹中。
</details>
<details>
<summary><b>Markdown → Excel 转换</b></summary>
```bash
# 运行交互式转换
python3 main.py
```
程序将扫描 `prompt_docs` 目录,让你选择一个文档集,然后将其转换回 Excel 文件,并输出到 `prompt_excel` 目录下一个带时间戳的文件夹中。
</details>
<details>
<summary><b>非交互式转换</b></summary>
```bash
# 指定要转换的 Excel 文件
python3 main.py --select "prompt_excel/your_file.xlsx"
# 指定要转换的 Markdown 目录
python3 main.py --select "prompt_docs/your_docs_folder"
```
</details>
<details>
<summary><b>Gemini 无头:Markdown → JSONL 批处理</b></summary>
```bash
# 将目录中的 .md 提示词批量转为 JSONL(默认输入 2/,输出 2/prompts.jsonl
python3 scripts/gemini_jsonl_batch.py --input 2 --output 2/prompts.jsonl --model gemini-2.5-flash
```
- 脚本内置固定系统提示词,调用 `gemini --allowed-tools '' --output-format text` 确保纯文本 JSONL。
- 需要本地已登录的 Gemini CLI;如需代理,请自行设置 `http_proxy/https_proxy` 环境变量。
- 可用 `-v` 查看逐文件处理日志,`--gemini-cmd` 自定义 CLI 可执行路径。
</details>
<details>
<summary><b>内部 JSONL Excel → JSONL 目录</b></summary>
```bash
# 将内部 JSONL 格式的 xlsx 按工作表拆分为多个 jsonl 文件
python3 main.py --select "prompt_excel/prompt_jsonl.xlsx" --mode jsonl_excel2jsonl
```
- 输出目录格式为 `prompt_jsonl/<excel_name>_<timestamp>/`
- 每个工作表输出一个独立的 `.jsonl` 文件
- 文件名格式为 `<序号>_<sheet_name>.jsonl`
- 若工作表中没有标准 JSON 单元格,会对纯文本单元格做 JSONL 兜底转换
- 自动忽略名为 `说明` 的工作表
</details>
---
<details>
<summary>❓ 常见问题 (FAQ) (可选)</summary>
- **Q: 为什么转换会失败?**
- **A:** 请确保您的Excel文件格式与提供的示例一致,特别是工作表(Sheet)的名称和列的标题。
- **Q: 我可以添加自己的转换逻辑吗?**
- **A:** 当然可以。核心逻辑位于 `scripts/` 目录下,您可以自由修改或扩展它们。
</details>
---
<!--
-------------------------------------------------------------------------------
社区与治理区域 (COMMUNITY & GOVERNANCE)
展示项目的健康度和发展方向。
-------------------------------------------------------------------------------
-->
## 🗺️ 路线图
```mermaid
gantt
title 项目发展路线图
dateFormat YYYY-MM
section 核心功能
双向转换脚本 :done, 2024-10, 30d
交互式CLI :done, 2024-11, 20d
section 未来计划
Web界面 :2025-01, 30d
更丰富的导出格式 :2025-02, 20d
云同步功能 : 2025-03, 30d
```
---
## 🤝 参与贡献
我们热烈欢迎各种形式的贡献!如果您对本项目有任何想法或建议,请随时开启一个 [Issue](https://github.com/tukuaiai/prompt-library/issues) 或提交一个 [Pull Request](https://github.com/tukuaiai/prompt-library/pulls)。
在您开始之前,请花点时间阅读我们的 [**贡献指南 (CONTRIBUTING.md)**](https://github.com/tukuaiai/prompt-library/blob/main/CONTRIBUTING.md) 和 [**行为准则 (CODE_OF_CONDUCT.md)**](https://github.com/tukuaiai/prompt-library/blob/main/CODE_OF_CONDUCT.md)。
### ✨ 贡献者们
感谢所有为本项目做出贡献的开发者!
<a href="https://github.com/tukuaiai/prompt-library/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tukuaiai/prompt-library" />
</a>
---
## 🛡️ 安全策略
我们非常重视项目的安全性。如果您发现了任何安全漏洞,请不要公开讨论,而是通过电子邮件 `tukuaiai@example.com` 与我们联系。
---
<!--
-------------------------------------------------------------------------------
页脚区域 (FOOTER)
最后的行动号召和感谢。
-------------------------------------------------------------------------------
-->
## 📜 许可证
本项目采用 [MIT](https://github.com/tukuaiai/prompt-library/blob/main/LICENSE) 许可证。
---
<div align="center">
**如果这个项目对您有帮助,请不要吝啬您的 Star ⭐!**
<!-- Star History: https://star-history.com/ -->
<a href="https://star-history.com/#tukuaiai/prompt-library&Date">
<img src="https://api.star-history.com/svg?repos=tukuaiai/prompt-library&type=Date" alt="Star History Chart" width="80%">
</a>
<br>
**Made with ❤️ by tukuaiai**
[⬆ 回到顶部](#prompt-library)
</div>
@@ -0,0 +1,56 @@
# JSONL 提示词转换器 - 系统提示词
你是一个专业的提示词格式转换器。将用户提供的提示词内容转换为标准 JSONL 格式。
## 输出格式
```json
{"title": "<标题>", "content": "<完整内容>"}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | string | 提示词标题,取内容的第一行或前 50 字符 |
| `content` | string | 完整的提示词内容 |
## 转换规则
1. **标题提取**
- 若内容以 `#` 开头,取第一个标题作为 title
- 否则取前 50 字符(去除换行)
2. **内容转义**
- 换行符转为 `\n`
- 双引号转为 `\"`
- 反斜杠转为 `\\`
## 输出要求
- 每行一个完整的 JSON 对象
- 不要添加任何解释、注释或额外文字
- 不要用 ```json 代码块包裹
- 直接输出纯 JSONL 内容
## 示例
### 输入
```
# Role:智能文档助手
## Background
用户需要一个能够处理文档的 AI 助手。
## Skills
- 文档解析
- 格式转换
```
### 输出
```
{"title": "# Role:智能文档助手", "content": "# Role:智能文档助手\n\n## Background\n用户需要一个能够处理文档的 AI 助手。\n\n## Skills\n- 文档解析\n- 格式转换"}
```
---
现在,请将用户提供的内容转换为标准 JSONL 格式。
File diff suppressed because one or more lines are too long
+38
View File
@@ -0,0 +1,38 @@
# 💰 项目支持(从Excel提取)
## 支持说明
**礼貌要饭地址** - 如果这个项目对您有帮助,欢迎通过以下方式支持
## 加密货币钱包地址
### 主流网络支持
| 网络名称 | 钱包地址 | Excel行号 |
|----------|----------|-----------|
| **TRON** | `TQtBXCSTwLFHjBqTS4rNUp7ufiGx51BRey` | 第12行 |
| **SOL** | `HjYhozVf9AQmfv7yv79xSNs6uaEU5oUk2USasYQfUYau` | 第13行 |
| **ETH** | `0xa396923a71ee7D9480b346a17dDeEb2c0C287BBC` | 第14行 |
| **BSC** | `0xa396923a71ee7D9480b346a17dDeEb2c0C287BBC` | 第15行 |
| **BTC** | `bc1plslluj3zq3snpnnczplu7ywf37h89dyudqua04pz4txwh8z5z5vsre7nlm` | 第16行 |
| **SUI** | `0xb720c98a48c77f2d49d375932b2867e793029e6337f1562522640e4f84203d2e` | 第17行 |
⚠️ **重要提醒**: 广告位(注意识别风险)
### 使用建议
1. 请确认钱包地址的准确性
2. 建议小额测试后再进行大额转账
3. 不同网络的转账费用不同,请选择合适的网络
---
*钱包地址来源: prompt (3).xlsx*
+35
View File
@@ -0,0 +1,35 @@
# 🛠️ 工具与资源(从Excel提取)
## AI优化工具
### OpenAI 提示词优化平台
- **URL**: https://platform.openai.com/chat/edit?models=gpt-5&optimize=true
- **描述**: openai提示词优化网站
- **数据来源**: Excel表格第7行
### 工具
- **URL**: https://aistudio.google.com/
- **描述**: 打开 Gemini 3.1 Pro
- **数据来源**: Excel表格第4行
## 社交媒体
### Twitter/X 账号
- **URL**: https://x.com/123olp
- **描述**: 点击关注我的推特,获取最新动态,首页接广告位
- **数据来源**: Excel表格第9行
## 使用建议
1. **OpenAI优化器**: 可以用来测试和改进本库中的提示词
2. **社交媒体**: 关注获取项目更新和使用技巧
3. **集成方式**: 可以将这些工具集成到自动化工作流中
---
*数据来源: prompt (3).xlsx*
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,127 @@
# 提示词库结构与 Excel 互转规范
> 本规范用于约束“提示词库”在文件系统与 Excel 之间的一致结构、命名与转换规则,确保后期自动化同步、增量更新与团队协作的稳定性。
## 1. 目录结构(约定)
```
prompt-library/
├── prompts/ # 核心输出目录
│ ├── <工作表名称-已净化>/ # 每个 Excel 子表对应一个文件夹
│ │ ├── (行,列)_标题.md # 单元格 → 单文件(仅包含提示词文本)
│ │ └── index.md # 该表分类索引(自动生成)
│ └── index.json # 全局 JSON 索引(自动生成)
├── docs/
│ ├── tools.md # 工具/链接(从表自动提取)
│ ├── support.md # 支持/钱包地址(从表自动提取)
│ ├── excel-data.md # Excel 原始数据快照(自动生成)
│ └── STRUCTURE_AND_CONVERSION_SPEC.md # 本规范
└── scripts/
├── convert_local.py # Excel ↔ 文件 的本地转换器(实现 Excel→文件)
├── config.yaml # 可选配置(数据源/策略)
└── requirements.txt # 依赖
```
- 只允许在 `prompts/<工作表名称-已净化>/` 下新增/修改提示词文件,其它自动文件(如 `index.md``index.json`)由转换器生成,禁止手改。
## 2. 命名与净化(Sanitization
- 工作表文件夹名:对原始 Excel 工作表名称执行净化:
- 移除非法字符:`\\ / : * ? " < > | \r \n`
- 将空格替换为下划线 `_`
- 最长 60 字符,超长截断
- 示例:`"学习 提示词/1?"``"学习_提示词1"`
- 提示词文件名:`(行,列)_标题.md`
- 行、列为 1 基索引(Excel 中第 1 行/第 1 列即 1
- 标题来源:该行第一个非空单元格的首行文本的前若干词(用于人读),同样执行净化
- 标题仅用于文件名可读性,回写 Excel 时忽略
## 3. 文件内容规范(强约束)
- 每个提示词文件(.md)内容必须是“对应单元格的原始文本”,末尾追加一个换行;不允许包含:
- Markdown 标题/分隔线/元信息
- 代码围栏(```
- 版本历史/导航/时间戳
- 编码:UTF-8,换行:LF
## 4. Excel → 文件 的生成规则
- 每个工作表 → `prompts/<工作表名称-已净化>/`
- 行类型判定(逐行):
- 提示词行:该行至少一列非空,且不属于工具/社交/钱包/警告/占位
- 工具/链接:同行包含 `http` 且非 X/Twitter 链接 → 归档 `docs/tools.md`
- 社交:链接中包含 `x.com`/`twitter.com` → 归档 `docs/tools.md`
- 钱包区块:先出现“表头”(含“网络/网络名称”且含“礼貌要饭地址/钱包/地址”),其后若干行按 `(网络, 地址)` 提取 → 归档 `docs/support.md`
- 警告:包含“广告位” → 记录为警告
- 占位:`...`/`….`/`....` → 忽略
- 对每个提示词行:
- 对该行每个非空列 `c` 生成:`(r,c)_标题.md`,文件正文 = 单元格纯文本
- 自动索引:
- `prompts/<表>/index.md`:统计、列表与版本矩阵(供浏览用,自动生成)
- `prompts/index.json`:全局结构化索引,含各表、各行版本与外部资源(自动生成)
- `docs/excel-data.md`:各表数据快照(截取前 3 列),便于审计
## 5. 文件 → Excel 的回写规则(规范定义,供实现)
> 说明:当前脚本已实现 Excel→文件;本节定义未来“文件→Excel”时的规范,以便实现反向同步器保持一致性。
- 工作表名:优先使用 `prompts/index.json` 中的 `categories[].name` 作为原始工作表名;若缺失,采用文件夹名将 `_` 还原为空格(尽力而为)。
- 单元格位置:
- 解析文件名 `(r,c)_标题.md` 得到 1 基 `r,c`,写入 Excel 的第 `r` 行、第 `c`
- 标题部分忽略,仅用于文件名可读
- 写入值:文件全文(去掉末尾多余空行)即单元格值
- 缺表/缺行列:自动创建工作表、扩展行列
- 冲突:同一 `(r,c)` 出现多文件时应失败并报告(推荐),或采用“最后写入覆盖”(需在实现中明确开关)
- 非提示词文档:`tools.md``support.md` 不默认回写;如需回写,应定义目标工作表名称与行布局(建议启用配置项)
## 6. 变更与校验
- 合法性检查:
- 文件名需符合正则:`^\(\d+,\d+\)_.+\.md$`
- 内容不得包含本规范禁止的附加信息(元信息、代码围栏等)
- 冲突检测:同一 `(r,c)` 不允许多次定义
- 兼容性:文件夹名与表名的映射以 `index.json` 为准;直接根据文件夹名还原可能出偏差
## 7. 自动化工具与用法
- 依赖安装:
```bash
python3 -m pip install -r prompt-library/scripts/requirements.txt
```
- 运行(Excel→文件):
```bash
python3 "prompt-library/scripts/convert_local.py" \
--excel "prompt (2).xlsx"
```
- 行为摘要:
- 为每个工作表生成一个同名(净化后)目录
- 为每个非空单元格生成一个 `.md` 文件(仅含单元格文本)
- 重新生成所有索引与快照文档
## 8. 新增提示词(两种路径)
- 在 Excel 中新增:
- 在目标工作表的目标行、列填写文本
- 运行转换脚本,生成对应 `(行,列)_标题.md`
- 在文件系统中新增:
- 到 `prompts/<工作表名称-已净化>/` 新建符合命名规范的文件
- 文件正文仅填写提示词文本
- 将在“文件→Excel”实现后由反向同步器写回相应 `(行,列)`
## 9. 设计取舍(Why
- 只在提示词文件中保留“纯内容”,避免元信息造成噪声与二义性,便于复制/拼接/训练
- 元信息、导航、统计统一由自动化生成到索引与文档中,降低手工维护成本
- 以 `(行,列)` 作为跨介质的“主键”,确保 Excel 与文件的可逆映射
## 10. 版本与扩展
- 建议在 `scripts/config.yaml` 中引入可选项:
- 反向同步开关与目标工作表名策略
- 冲突策略(报错/覆盖)
- 过滤器(忽略某些行/列/关键词)
- 未来可扩展:GitHub Actions/CI 校验规范合规性;支持更多字段映射(如 tag、语言)。
---
本规范适用于本仓库的所有提示词数据。如需调整,请在 PR 中同步更新 `docs/STRUCTURE_AND_CONVERSION_SPEC.md` 并说明向后兼容策略。
+624
View File
@@ -0,0 +1,624 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""
main.py
Unified controller for prompt-library conversions.
支持的转换模式
==============
1. Excel → Docs : 将 Excel 工作簿转换为 Markdown 文档目录
2. Docs → Excel : 将 Markdown 文档目录还原为 Excel 工作簿
3. Docs → JSONL : 将 Markdown 文档转换为 JSONL 格式(保留完整元信息)
4. JSONL → Excel : 将 JSONL 转换为 Excel(单元格存储 JSON 对象)
5. Excel(JSONL) → JSONL : 将内部 JSONL 格式的 Excel 转换为 JSONL 目录(自动忽略"说明"工作表)
数据格式规范
============
Excel 结构:
- 每个工作表(sheet) = 一个分类(category)
- 行(row) = 不同提示词
- 列(col) = 版本迭代
Excel(JSONL) 结构(内部 JSONL 格式):
- 每个工作表(sheet) = 一个分类(category)"说明"工作表会被忽略
- 每个单元格存储 JSON 对象: {"title": "...", "content": "..."}
Docs 结构:
- prompts/(N)_分类名/ # N = category_id
- prompts/(N)_分类名/(r,c)_标题.md # r=row, c=col
JSONL 格式 (每行一个 JSON 对象):
{
"category_id": 2, # 分类编号
"category": "元提示词", # 分类名称
"row": 1, # 原 Excel 行号
"col": 1, # 原 Excel 列号(版本号)
"title": "...", # 标题(截断80字符)
"content": "..." # 完整内容
}
JSONL → Excel 单元格格式:
{"title": "...", "content": "..."} # 只保留 title 和 content
目录约定
========
- Excel 源文件: ./prompt_excel/
- Docs 源目录: ./prompt_docs/
- JSONL 文件: ./prompt_jsonl/
- 输出:
- Excel→Docs: ./prompt_docs/prompt_docs_YYYY_MMDD_HHMMSS/
- Docs→Excel: ./prompt_excel/prompt_excel_YYYY_MMDD_HHMMSS/rebuilt.xlsx
- Docs→JSONL: ./prompt_jsonl/{docs_name}.jsonl
- JSONL→Excel: ./prompt_excel/{jsonl_name}.xlsx
- Excel(JSONL)→JSONL: ./prompt_jsonl/{excel_name}_{timestamp}/<sheet>.jsonl
使用示例
========
# 交互式选择
python3 main.py
# Excel → Docs
python3 main.py --select "prompt_excel/prompt.xlsx"
# Docs → Excel
python3 main.py --select "prompt_docs/prompt_docs_2025_1222"
# Docs → JSONL
python3 main.py --select "prompt_docs/prompt_docs_2025_1222" --mode docs2jsonl
# JSONL → Excel
python3 main.py --select "prompt_jsonl/prompt_docs.jsonl"
# Excel(JSONL) → JSONL(自动检测或显式指定)
python3 main.py --select "prompt_excel/prompt_jsonl.xlsx"
python3 main.py --select "prompt_excel/prompt_jsonl.xlsx" --mode jsonl_excel2jsonl
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Sequence, Tuple
# Optional Rich UI imports (fallback to plain if unavailable)
try:
from rich.console import Console
from rich.layout import Layout
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from rich import box
from rich.prompt import IntPrompt
_RICH_AVAILABLE = True
except Exception: # pragma: no cover
_RICH_AVAILABLE = False
# Optional InquirerPy for arrow-key selection
try:
from InquirerPy import inquirer as _inq
_INQUIRER_AVAILABLE = True
except Exception: # pragma: no cover
_INQUIRER_AVAILABLE = False
@dataclass
class Candidate:
index: int
kind: str # "excel" | "docs" | "docs2jsonl" | "jsonl"
path: Path
label: str
def get_repo_root() -> Path:
return Path(__file__).resolve().parent
def list_excel_files(excel_dir: Path) -> List[Path]:
if not excel_dir.exists():
return []
return sorted([p for p in excel_dir.iterdir() if p.is_file() and p.suffix.lower() == ".xlsx"], key=lambda p: p.stat().st_mtime)
def has_prompt_files(directory: Path) -> bool:
if not directory.exists():
return False
# Detect files like "(r,c)_*.md" anywhere under the directory
for file_path in directory.rglob("*.md"):
name = file_path.name
if name.startswith("(") and ")_" in name:
return True
return False
def list_doc_sets(docs_dir: Path) -> List[Path]:
results: List[Path] = []
if not docs_dir.exists():
return results
# If the docs_dir itself looks like a set, include it
if has_prompt_files(docs_dir):
results.append(docs_dir)
# Also include any immediate children that look like a docs set
for child in sorted(docs_dir.iterdir()):
if child.is_dir() and has_prompt_files(child):
results.append(child)
return results
def run_start_convert(start_convert: Path, mode: str, project_root: Path, select_path: Optional[Path] = None, excel_dir: Optional[Path] = None, docs_dir: Optional[Path] = None) -> int:
"""Delegate to scripts/start_convert.py with appropriate flags."""
python_exe = sys.executable
cmd: List[str] = [python_exe, str(start_convert), "--mode", mode]
if select_path is not None:
# Always pass as repo-root-relative or absolute string
cmd.extend(["--select", str(select_path)])
if excel_dir is not None:
cmd.extend(["--excel-dir", str(excel_dir)])
if docs_dir is not None:
cmd.extend(["--docs-dir", str(docs_dir)])
# Execute in repo root to ensure relative defaults resolve correctly
proc = subprocess.run(cmd, cwd=str(project_root))
return proc.returncode
def run_docs_to_jsonl(docs_path: Path, project_root: Path) -> int:
"""Convert docs folder to JSONL format."""
import json
import re
prompts_dir = docs_path / "prompts"
if not prompts_dir.exists():
print(f"❌ 找不到 prompts 目录: {prompts_dir}")
return 1
output_dir = project_root / "prompt_jsonl"
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f"{docs_path.name}.jsonl"
records = []
for category_dir in sorted(prompts_dir.iterdir()):
if not category_dir.is_dir():
continue
m = re.match(r'\((\d+)\)_(.+)', category_dir.name)
cat_id, cat_name = (m.groups() if m else (0, category_dir.name))
for md_file in sorted(category_dir.glob("*.md")):
if md_file.name == "index.md":
continue
fm = re.match(r'\((\d+),(\d+)\)_(.+)\.md', md_file.name)
if not fm:
continue
row, col, title = fm.groups()
content = md_file.read_text(encoding='utf-8')
records.append({
"category_id": int(cat_id),
"category": cat_name,
"row": int(row),
"col": int(col),
"title": title[:80],
"content": content
})
with open(output_file, 'w', encoding='utf-8') as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + '\n')
print(f"✅ Docs→JSONL OK: {docs_path.name}{output_file.relative_to(project_root)}")
return 0
def list_jsonl_files(jsonl_dir: Path) -> List[Path]:
if not jsonl_dir.exists():
return []
return sorted([p for p in jsonl_dir.iterdir() if p.is_file() and p.suffix.lower() == ".jsonl"], key=lambda p: p.stat().st_mtime)
def is_jsonl_excel(excel_path: Path) -> bool:
"""检测 Excel 是否为内部 JSONL 格式(单元格存储 JSON 对象)"""
import json
try:
import pandas as pd
except ImportError:
return False
try:
xlsx = pd.ExcelFile(excel_path)
for sheet in xlsx.sheet_names[:2]: # 检查前两个工作表
if sheet == '说明':
continue
df = pd.read_excel(xlsx, sheet_name=sheet, header=None, nrows=1)
if df.empty:
continue
first_val = str(df.iloc[0, 0]).strip() if not pd.isna(df.iloc[0, 0]) else ""
# 检查列名或第一个单元格是否为 JSON
first_col = str(df.columns[0]).strip() if len(df.columns) > 0 else ""
for val in [first_col, first_val]:
if val.startswith('{') and val.endswith('}'):
try:
obj = json.loads(val)
if 'title' in obj and 'content' in obj:
return True
except:
pass
return False
except:
return False
def sanitize_filename(name: str) -> str:
"""将工作表名转换为稳定的文件名片段。"""
invalid_chars = '<>:"/\\|?*'
sanitized = "".join("_" if ch in invalid_chars else ch for ch in name).strip()
return sanitized.rstrip(". ") or "sheet"
def build_text_record(cat_id: int, cat_name: str, row: int, col: int, text: str) -> dict:
"""将纯文本单元格兜底转换为 JSONL 记录。"""
lines = [line.strip() for line in text.splitlines() if line.strip()]
title = lines[0] if lines else text.strip()
return {
"category_id": cat_id,
"category": cat_name,
"row": row,
"col": col,
"title": title[:80],
"content": text,
}
def run_jsonl_excel_to_jsonl(excel_path: Path, project_root: Path) -> int:
"""将内部 JSONL 格式的 Excel 转换为 JSONL 目录(忽略"说明"工作表)"""
import json
from datetime import datetime
try:
import pandas as pd
except ImportError:
print("❌ 需要 pandas: pip install pandas openpyxl")
return 1
xlsx = pd.ExcelFile(excel_path)
cat_id = 0
total_records = 0
written_files = []
timestamp = datetime.now().strftime("%Y_%m%d_%H%M%S")
output_dir = project_root / "prompt_jsonl" / f"{excel_path.stem}_{timestamp}"
output_dir.mkdir(parents=True, exist_ok=True)
for sheet_index, sheet in enumerate(xlsx.sheet_names, start=1):
if sheet == '说明':
continue
cat_id += 1
cat_name = sheet
df = pd.read_excel(xlsx, sheet_name=sheet, header=None)
sheet_lines = []
fallback_records = []
# 检查列名是否是 JSON 数据
for col_idx, col_name in enumerate(df.columns):
col_str = str(col_name).strip()
if col_str.startswith('{') and col_str.endswith('}'):
try:
obj = json.loads(col_str)
if 'title' in obj and 'content' in obj:
sheet_lines.append(json.dumps({
"category_id": cat_id,
"category": cat_name,
"row": 1,
"col": col_idx + 1,
"title": obj["title"][:80],
"content": obj["content"]
}, ensure_ascii=False))
except:
pass
# 处理数据行
for row_idx, row in df.iterrows():
for col_idx, val in enumerate(row):
if pd.isna(val):
continue
val_str = str(val).strip()
if not val_str:
continue
if val_str.startswith('{') and val_str.endswith('}'):
try:
obj = json.loads(val_str)
if 'title' in obj and 'content' in obj:
sheet_lines.append(json.dumps({
"category_id": cat_id,
"category": cat_name,
"row": row_idx + 2,
"col": col_idx + 1,
"title": obj["title"][:80],
"content": obj["content"]
}, ensure_ascii=False))
except:
pass
else:
# 跳过常见的顶栏广告/元数据噪声,但保留其他纯文本内容作为兜底记录。
if row_idx == 0 and col_idx == 0 and val_str.startswith("广告位"):
continue
fallback_records.append(
build_text_record(
cat_id=cat_id,
cat_name=cat_name,
row=row_idx + 2,
col=col_idx + 1,
text=val_str,
)
)
if not sheet_lines and fallback_records:
sheet_lines = [
json.dumps(record, ensure_ascii=False)
for record in fallback_records
]
total_records += len(sheet_lines)
file_stem = sanitize_filename(cat_name)
output_file = output_dir / f"{sheet_index:02d}_{file_stem}.jsonl"
with open(output_file, 'w', encoding='utf-8') as f:
if sheet_lines:
f.write('\n'.join(sheet_lines) + '\n')
written_files.append(output_file)
if not written_files:
print(f"❌ 未找到有效的 JSONL 数据: {excel_path}")
return 1
print(
f"✅ Excel(JSONL)→JSONL OK: {excel_path.name}"
f"{output_dir.relative_to(project_root)} "
f"({len(written_files)} 个文件 / {total_records} 条记录)"
)
return 0
def run_jsonl_to_excel(jsonl_path: Path, project_root: Path) -> int:
"""Convert JSONL to Excel, each cell contains the full JSON object as string."""
import json
from collections import defaultdict
try:
import pandas as pd
except ImportError:
print("❌ 需要 pandas: pip install pandas openpyxl")
return 1
records = []
with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip():
records.append(json.loads(line))
if not records:
print(f"❌ JSONL 文件为空: {jsonl_path}")
return 1
# category -> {row -> {col -> json_string}}
sheets_data: dict = defaultdict(lambda: defaultdict(dict))
cat_id_map = {}
for r in records:
cat_name = r["category"]
cat_id_map[r["category_id"]] = cat_name
# 单元格内容只保留 title 和 content
cell_data = {"title": r["title"], "content": r["content"]}
sheets_data[cat_name][r["row"]][r["col"]] = json.dumps(cell_data, ensure_ascii=False)
output_dir = project_root / "prompt_excel"
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f"{jsonl_path.stem}.xlsx"
sorted_cats = sorted(cat_id_map.items(), key=lambda x: x[0])
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
for cat_id, cat_name in sorted_cats:
row_data = sheets_data[cat_name]
if not row_data:
continue
max_row = max(row_data.keys())
max_col = max(c for cols in row_data.values() for c in cols.keys())
data = []
for row_idx in range(1, max_row + 1):
row_list = []
for col_idx in range(1, max_col + 1):
row_list.append(row_data.get(row_idx, {}).get(col_idx, ""))
data.append(row_list)
df = pd.DataFrame(data)
sheet_name = cat_name[:31]
df.to_excel(writer, sheet_name=sheet_name, index=False, header=False)
print(f"✅ JSONL→Excel OK: {jsonl_path.name}{output_file.relative_to(project_root)} ({len(sorted_cats)} 个工作表)")
return 0
def build_candidates(project_root: Path, excel_dir: Path, docs_dir: Path) -> List[Candidate]:
candidates: List[Candidate] = []
idx = 1
jsonl_dir = project_root / "prompt_jsonl"
for path in list_excel_files(excel_dir):
label = f"{path.name}"
# 检测是否为内部 JSONL 格式的 Excel
if is_jsonl_excel(path):
candidates.append(Candidate(index=idx, kind="jsonl_excel", path=path, label=label))
else:
candidates.append(Candidate(index=idx, kind="excel", path=path, label=label))
idx += 1
for path in list_doc_sets(docs_dir):
display = path.relative_to(project_root) if path.is_absolute() else path
# Docs → Excel
candidates.append(Candidate(index=idx, kind="docs", path=path, label=f"{display}"))
idx += 1
# Docs → JSONL
candidates.append(Candidate(index=idx, kind="docs2jsonl", path=path, label=f"{display}"))
idx += 1
for path in list_jsonl_files(jsonl_dir):
label = f"{path.name}"
candidates.append(Candidate(index=idx, kind="jsonl", path=path, label=label))
idx += 1
return candidates
def select_interactively(candidates: Sequence[Candidate]) -> Optional[Candidate]:
if not candidates:
print("没有可用的 Excel 或 Docs 源。请将 .xlsx 放到 prompt_excel/ 或将文档放到 prompt_docs/ 下。")
return None
# Prefer arrow-key selection if available
if _INQUIRER_AVAILABLE:
try:
choices = [
{"name": f"[{c.kind.upper()}] {c.label}", "value": c.index}
for c in candidates
]
selection = _inq.select(
message="选择要转换的源(上下箭头,回车确认,Ctrl+C 取消):",
choices=choices,
default=choices[0]["value"],
).execute()
match = next((c for c in candidates if c.index == selection), None)
return match
except KeyboardInterrupt:
return None
if _RICH_AVAILABLE:
console = Console()
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="list"),
Layout(name="footer", size=3),
)
header = Panel(Text("提示词库转换器", style="bold cyan"), subtitle="选择一个源开始转换", box=box.ROUNDED)
table = Table(box=box.SIMPLE_HEAVY)
table.add_column("编号", style="bold yellow", justify="right", width=4)
table.add_column("类型", style="magenta", width=16)
table.add_column("路径/名称", style="white")
kind_labels = {"excel": "Excel→Docs", "docs": "Docs→Excel", "docs2jsonl": "Docs→JSONL", "jsonl": "JSONL→Excel", "jsonl_excel": "Excel(JSONL)→JSONL"}
for c in candidates:
table.add_row(str(c.index), kind_labels.get(c.kind, c.kind), c.label)
layout["header"].update(header)
layout["list"].update(Panel(table, title="可选源", border_style="cyan"))
layout["footer"].update(Panel(Text("输入编号并回车(0 退出)", style="bold"), box=box.ROUNDED))
console.print(layout)
while True:
try:
choice = IntPrompt.ask("编号", default=0)
except Exception:
return None
if choice == 0:
return None
match = next((c for c in candidates if c.index == choice), None)
if match is not None:
return match
console.print("[red]编号不存在,请重试[/red]")
# Plain fallback
kind_labels = {"excel": "Excel→Docs", "docs": "Docs→Excel", "docs2jsonl": "Docs→JSONL", "jsonl": "JSONL→Excel", "jsonl_excel": "Excel(JSONL)→JSONL"}
print("请选择一个源进行转换:")
for c in candidates:
print(f" {c.index:2d}. [{kind_labels.get(c.kind, c.kind)}] {c.label}")
print(" 0. 退出")
while True:
try:
raw = input("输入编号后回车:").strip()
except EOFError:
return None
if not raw:
continue
if raw == "0":
return None
if not raw.isdigit():
print("请输入有效数字。")
continue
choice = int(raw)
match = next((c for c in candidates if c.index == choice), None)
if match is None:
print("编号不存在,请重试。")
continue
return match
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="prompt-library conversion controller")
p.add_argument("--excel-dir", type=str, default="prompt_excel", help="Excel sources directory (default: prompt_excel)")
p.add_argument("--docs-dir", type=str, default="prompt_docs", help="Docs sources directory (default: prompt_docs)")
p.add_argument("--select", type=str, default=None, help="Path to a specific .xlsx file or a docs folder")
p.add_argument("--mode", type=str, choices=["excel2docs", "docs2excel", "docs2jsonl", "jsonl2excel", "jsonl_excel2jsonl"], default=None, help="Conversion mode (auto-detect if not specified)")
p.add_argument("--non-interactive", action="store_true", help="Do not prompt; require --select or exit")
return p.parse_args()
def main() -> int:
repo_root = get_repo_root()
start_convert = repo_root / "scripts" / "start_convert.py"
if not start_convert.exists():
print("找不到 scripts/start_convert.py。")
return 1
args = parse_args()
excel_dir = (repo_root / args.excel_dir).resolve() if not Path(args.excel_dir).is_absolute() else Path(args.excel_dir).resolve()
docs_dir = (repo_root / args.docs_dir).resolve() if not Path(args.docs_dir).is_absolute() else Path(args.docs_dir).resolve()
# Non-interactive path with explicit selection
if args.non_interactive or args.select:
if not args.select:
print("--non-interactive 需要配合 --select 使用。")
return 2
selected = Path(args.select)
if not selected.is_absolute():
selected = (repo_root / selected).resolve()
if not selected.exists():
print(f"选择的路径不存在: {selected}")
return 2
if selected.is_file() and selected.suffix.lower() == ".xlsx":
# 检测是否为内部 JSONL 格式或显式指定模式
if args.mode == "jsonl_excel2jsonl" or is_jsonl_excel(selected):
return run_jsonl_excel_to_jsonl(selected, repo_root)
return run_start_convert(start_convert, mode="excel2docs", project_root=repo_root, select_path=selected, excel_dir=excel_dir)
if selected.is_file() and selected.suffix.lower() == ".jsonl":
return run_jsonl_to_excel(selected, repo_root)
if selected.is_dir():
# Check mode or default to docs2excel
if args.mode == "docs2jsonl":
return run_docs_to_jsonl(selected, repo_root)
return run_start_convert(start_convert, mode="docs2excel", project_root=repo_root, select_path=selected, docs_dir=docs_dir)
print("无法识别的选择类型。")
return 2
# Interactive selection
candidates = build_candidates(repo_root, excel_dir, docs_dir)
chosen = select_interactively(candidates)
if chosen is None:
return 0
if chosen.kind == "excel":
return run_start_convert(start_convert, mode="excel2docs", project_root=repo_root, select_path=chosen.path, excel_dir=excel_dir)
elif chosen.kind == "jsonl_excel":
return run_jsonl_excel_to_jsonl(chosen.path, repo_root)
elif chosen.kind == "docs2jsonl":
return run_docs_to_jsonl(chosen.path, repo_root)
elif chosen.kind == "jsonl":
return run_jsonl_to_excel(chosen.path, repo_root)
else:
return run_start_convert(start_convert, mode="docs2excel", project_root=repo_root, select_path=chosen.path, docs_dir=docs_dir)
if __name__ == "__main__":
sys.exit(main())
+6
View File
@@ -0,0 +1,6 @@
rich
InquirerPy
pandas
openpyxl
PyYAML
python-dotenv
@@ -0,0 +1,90 @@
import json
import re
jsonl_file = "prompt_jsonl/prompt_docs_refactored.jsonl"
report = []
def check_md_syntax(text, info_str):
lines = text.split('\n')
errors = []
# 1. 检查分隔符 (--- 或 ***)
# 规范:应独占一行,前后建议有空行
# 正则匹配:行首开始,至少3个-或*,行尾结束,允许行尾有空白
separator_pattern = re.compile(r'^\s*([-*]{3,})\s*$')
# 2. 检查标题 (#)
# 规范:#后必须有空格
header_pattern = re.compile(r'^(#+)([^ \n].*)') # 捕获 #后紧跟非空格的
# 3. 代码块 (```)
code_block_count = 0
for i, line in enumerate(lines):
# 检查分隔符
if separator_pattern.match(line):
# 检查长度(虽然md规范>=3即可,但有些习惯是用3个)
# 检查前后空行(非强制,但推荐)
pass # 暂时只检查基本正则,如果夹杂在文本中通常不会独占一行
# 检查错误标题: #Title
m = header_pattern.match(line)
if m:
# 排除掉特殊的Shebang或注释,比如 #!/bin/bash 或 #_Role (这个文件里的Title字段用了#_)
# 但这里是content字段,应该遵循MD规范
# 检查是否在代码块内
if code_block_count % 2 == 0:
# 忽略一些特定的meta标记,比如 # Role (有些prompt习惯)
# 实际上标准MD里 #Role 也是不规范的标题
# 允许一些特殊情况? 暂时严格检查
errors.append(f"Line {i+1}: 标题格式可能错误 (缺少空格): '{line[:20]}...'" )
# 检查代码块闭合
if line.strip().startswith('```'):
code_block_count += 1
if code_block_count % 2 != 0:
errors.append("代码块 (```) 未闭合")
if errors:
report.append(f"\n📄 {info_str}")
for e in errors:
report.append(f" - {e}")
def analyze():
print("正在检查 Markdown 语法...")
try:
with open(jsonl_file, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if not line.strip(): continue
try:
item = json.loads(line)
except json.JSONDecodeError:
print(f"❌ JSON 解析错误在第 {line_num}")
continue
cat = item.get('category', 'Unknown')
row = item.get('row', '?')
title = item.get('title', 'No Title')
content = item.get('content', '')
if not content:
report.append(f"\n⚠️ {cat} | Row {row} | {title}: 内容为空")
continue
info = f"[{cat}] Row {row}: {title}"
check_md_syntax(content, info)
except FileNotFoundError:
print("文件未找到")
return
if not report:
print("✅ 未发现明显的 Markdown 语法问题。 ")
else:
print(f"⚠️ 发现潜在问题 ({len(report)} 处):")
for msg in report:
print(msg)
if __name__ == "__main__":
analyze()
+87
View File
@@ -0,0 +1,87 @@
# 提示词库同步配置 - 基于Excel完整数据
source:
excel_file: "prompt (2).xlsx"
total_rows: 18
total_cols: 3
processed_date: "2025-02-02"
google_sheets:
sheet_id: "1ngoQOhJqdguwNAilCl1joNwTje7FWWN9WiI2bo5VhpU"
credentials_path: "./credentials.json"
output:
prompts_dir: "./prompts"
use_timestamp: true
naming:
max_title_length: 30
row_col_format: "({row},{col})"
separator: "_"
sync:
skip_rows: [] # 不跳过任何行,完整处理
skip_keywords: [] # 完整保留所有内容
# Excel原始数据映射
excel_mapping:
prompts:
- row: 0
title: "提示词1a"
versions: [1, 2, 3]
content: ["提示词1a", "提示词1b", "提示词1c"]
- row: 1
title: "提示词2a"
versions: [1, 2]
content: ["提示词2a", "提示词2b"]
- row: 3
title: "提示词ya"
versions: [1]
content: ["提示词ya"]
tools:
openai_optimizer:
row: 5
url: "https://platform.openai.com/chat/edit?models=gpt-5&optimize=true"
description: "openai提示词优化网站"
social_media:
twitter:
row: 7
url: "https://x.com/123olp"
description: "点击关注我的推特,获取最新动态,首页接广告位"
support:
title_row: 9
title: "礼貌要饭地址"
crypto_wallets:
tron:
row: 10
address: "TQtBXCSTwLFHjBqTS4rNUp7ufiGx51BRey"
solana:
row: 11
address: "HjYhozVf9AQmfv7yv79xSNs6uaEU5oUk2USasYQfUYau"
ethereum:
row: 12
address: "0xa396923a71ee7D9480b346a17dDeEb2c0C287BBC"
bsc:
row: 13
address: "0xa396923a71ee7D9480b346a17dDeEb2c0C287BBC"
bitcoin:
row: 14
address: "bc1plslluj3zq3snpnnczplu7ywf37h89dyudqua04pz4txwh8z5z5vsre7nlm"
sui:
row: 15
address: "0xb720c98a48c77f2d49d375932b2867e793029e6337f1562522640e4f84203d2e"
misc:
warning:
row: 17
content: "广告位(注意识别风险)"
# 数据验证规则
validation:
prompt_rows: [0, 1, 3]
tool_rows: [5]
social_rows: [7]
crypto_rows: [10, 11, 12, 13, 14, 15]
warning_rows: [17]
@@ -0,0 +1,549 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
convert_local.py
Reads a local Excel file and converts its contents into a structured prompt library
under `prompt-library/` per the development guide. It generates:
- prompts/<category>/ (one file per non-empty cell across columns for each prompt row)
- prompts/index.json (summary + traceability)
- prompts/<category>/index.md (table + version matrix)
- docs/tools.md, docs/support.md, docs/excel-data.md
- README.md (top-level for prompt-library)
Usage:
python prompt-library/scripts/convert_local.py \
[--excel "/absolute/or/relative/path/to/prompt (2).xlsx"] \
[--config prompt-library/scripts/config.yaml] \
[--category-name prompt-category]
If no arguments are provided, it will:
- load config from prompt-library/scripts/config.yaml (if present)
- resolve Excel path from config.source.excel_file relative to project root
- default category to "prompt-category"
Dependencies: pandas, openpyxl, PyYAML
"""
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import pandas as pd
try:
import yaml # type: ignore
except Exception: # pragma: no cover
yaml = None # Optional; script still works without YAML if no config provided
@dataclass
class RowClassification:
row_index: int # zero-based excel index
kind: str # prompt|tool|social|wallet_header|wallet|warning|other
data: Dict
class ExcelPromptConverter:
def __init__(
self,
project_root: Path,
prompt_library_dir: Path,
excel_path: Path,
category_name: str = "prompt-category",
config_path: Optional[Path] = None,
output_root: Optional[Path] = None,
) -> None:
self.project_root = project_root
self.prompt_library_dir = prompt_library_dir
# If an output_root is provided, write into that snapshot directory
# rather than the in-repo prompts/docs locations.
if output_root is not None:
self.output_root = output_root
self.prompts_dir = output_root / "prompts"
self.docs_dir = output_root / "docs"
self.readme_target_root = output_root
else:
self.output_root = None
self.prompts_dir = prompt_library_dir / "prompts"
self.docs_dir = prompt_library_dir / "docs"
self.readme_target_root = prompt_library_dir
self.scripts_dir = prompt_library_dir / "scripts"
self.category_name = category_name # fallback if single sheet
self.category_dir = self.prompts_dir / self.category_name
self.excel_path = excel_path
self.config_path = config_path
self.config = self._load_config(config_path)
self.now = datetime.now()
# Per-sheet prompts map: {sheet_name: {excel_row -> {title, versions{col->file}}}}
self.prompts_info_by_sheet: Dict[str, Dict[int, Dict]] = {}
self.tools: List[Dict] = []
self.social: List[Dict] = []
self.wallets: Dict[str, Dict] = {}
self.misc: List[Dict] = []
self.total_rows = 0
self.total_cols = 0
self.sheet_names_order: List[str] = []
def _load_config(self, config_path: Optional[Path]) -> Dict:
if config_path and config_path.exists() and yaml is not None:
with config_path.open("r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
return {}
def _sanitize_filename(self, text: str, max_length: int = 60) -> str:
if not text:
return "untitled"
text = str(text).strip()
text = re.sub(r"[\\/:*?\"<>|\r\n]+", "", text)
text = text.replace(" ", "_")
if len(text) > max_length:
text = text[:max_length].rstrip("_-")
return text or "untitled"
def _extract_title(self, contents: List[str]) -> str:
for c in contents:
if c and c.strip():
first_line = c.strip().splitlines()[0]
words = first_line.split()
candidate = " ".join(words[:6])
return self._sanitize_filename(candidate)
return "untitled"
def _read_excel_sheets(self) -> Dict[str, pd.DataFrame]:
# Read all sheets; if workbook has single sheet, still returns dict with one entry
sheets: Dict[str, pd.DataFrame] = pd.read_excel(self.excel_path, header=None, engine="openpyxl", sheet_name=None) # type: ignore
normalized: Dict[str, pd.DataFrame] = {}
for sheet_name, df in sheets.items():
try:
df = df.map(lambda v: v.strip() if isinstance(v, str) else v) # pandas >=2.1
except Exception:
df = df.applymap(lambda v: v.strip() if isinstance(v, str) else v) # fallback
normalized[sheet_name] = df
# preserve order of sheets
self.sheet_names_order = list(normalized.keys())
# set global rows/cols to first sheet for summary; detailed per-sheet handled later
if normalized:
any_df = normalized[self.sheet_names_order[0]]
self.total_rows, self.total_cols = any_df.shape
return normalized
def _classify_rows(self, df: pd.DataFrame) -> List[RowClassification]:
classifications: List[RowClassification] = []
wallet_mode = False
for r in range(df.shape[0]):
row_vals = [df.iloc[r, c] if c < df.shape[1] else None for c in range(df.shape[1])]
non_empty = [v for v in row_vals if isinstance(v, str) and v.strip()]
any_http = any(isinstance(v, str) and v.startswith("http") for v in row_vals)
if not non_empty:
classifications.append(RowClassification(r, "other", {"empty": True}))
continue
# Wallet header detection (e.g., contains "网络" and a label like "礼貌要饭地址")
joined = " ".join([v for v in non_empty])
if any(k in joined for k in ["网络", "网络名称"]) and any(
k in joined for k in ["礼貌要饭地址", "钱包", "地址"]
):
wallet_mode = True
classifications.append(RowClassification(r, "wallet_header", {"raw": row_vals}))
continue
if wallet_mode:
# If the row still looks like wallet data (two columns: network, address)
first, second = row_vals[0] if len(row_vals) > 0 else None, row_vals[1] if len(row_vals) > 1 else None
if (first and isinstance(first, str)) and (second and isinstance(second, str)):
classifications.append(
RowClassification(
r,
"wallet",
{
"network": first,
"address": second,
"raw": row_vals,
},
)
)
continue
else:
wallet_mode = False # end wallet section if pattern breaks
# Tools and social heuristics
if any_http:
url = next(v for v in row_vals if isinstance(v, str) and v.startswith("http"))
desc = None
for v in row_vals:
if v and isinstance(v, str) and not v.startswith("http"):
desc = v
break
kind = "social" if ("x.com" in url or "twitter.com" in url) else "tool"
classifications.append(RowClassification(r, kind, {"url": url, "description": desc or "", "raw": row_vals}))
continue
# Warnings or misc markers
if any("广告位" in v for v in non_empty if isinstance(v, str)):
classifications.append(RowClassification(r, "warning", {"content": joined, "raw": row_vals}))
continue
# Placeholder rows to ignore as prompts
if any(v in {"...", "….", "...."} for v in non_empty):
classifications.append(RowClassification(r, "other", {"placeholder": True, "raw": row_vals}))
continue
# Otherwise: treat as prompt row (one logical prompt per row with multiple versions across columns)
prompt_versions: Dict[int, str] = {}
for c in range(df.shape[1]):
cell = df.iloc[r, c] if c < df.shape[1] else None
if isinstance(cell, str) and cell.strip():
prompt_versions[c + 1] = cell.strip()
if prompt_versions:
classifications.append(RowClassification(r, "prompt", {"versions": prompt_versions}))
else:
classifications.append(RowClassification(r, "other", {"raw": row_vals}))
return classifications
def _ensure_dirs(self) -> None:
self.prompts_dir.mkdir(parents=True, exist_ok=True)
self.category_dir.mkdir(parents=True, exist_ok=True)
self.docs_dir.mkdir(parents=True, exist_ok=True)
def _write_prompt_file(self, row_num: int, col_num: int, title: str, content: str, versions_in_row: List[int]) -> str:
"""Write a prompt file containing ONLY the prompt text, nothing else."""
row_col = f"({row_num},{col_num})"
filename = f"{row_col}_{title}.md"
filepath = self.category_dir / filename
# Ensure content ends with newline and contains no surrounding fences/headers added by us
pure = (content or "").rstrip("\n") + "\n"
filepath.write_text(pure, encoding="utf-8")
return filename
def _generate_category_index(self, sheet_name: str, category_dir: Path, prompts_info: Dict[int, Dict]) -> None:
index_path = category_dir / "index.md"
total_prompts = len(prompts_info)
total_versions = sum(len(meta["versions"]) for meta in prompts_info.values())
avg_versions = total_versions / total_prompts if total_prompts else 0
lines: List[str] = []
lines.append(f"# 📂 提示词分类 - {sheet_name}(基于Excel原始数据)\n")
lines.append(f"最后同步: {self.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
lines.append("\n## 📊 统计\n")
lines.append(f"- 提示词总数: {total_prompts}\n")
lines.append(f"- 版本总数: {total_versions} \n")
lines.append(f"- 平均版本数: {avg_versions:.1f}\n\n")
lines.append("## 📋 提示词列表\n")
lines.append("\n| 序号 | 标题 | 版本数 | 查看 |\n|------|------|--------|------|\n")
for row in sorted(prompts_info.keys()):
info = prompts_info[row]
title = info["title"]
versions = info["versions"]
links = " / ".join([f"[v{v}](./({row},{v})_{title}.md)" for v in sorted(versions.keys())])
lines.append(f"| {row} | {title} | {len(versions)} | {links} |\n")
# Version matrix
max_col = 0
for info in prompts_info.values():
if info["versions"]:
max_col = max(max_col, max(info["versions"].keys()))
lines.append("\n## 🗂️ 版本矩阵\n")
header = [""] + [f"v{i}" for i in range(1, max_col + 1)] + ["备注"]
lines.append("\n| " + " | ".join(header) + " |\n" + "|" + "---|" * len(header) + "\n")
for row in sorted(prompts_info.keys()):
info = prompts_info[row]
row_cells = [str(row)]
for c in range(1, max_col + 1):
row_cells.append("" if c in info["versions"] else "")
row_cells.append("")
lines.append("| " + " | ".join(row_cells) + " |\n")
index_path.write_text("\n".join(lines), encoding="utf-8")
def _generate_prompts_index_json(self) -> None:
index_json_path = self.prompts_dir / "index.json"
total_prompts = sum(len(p) for p in self.prompts_info_by_sheet.values())
total_versions = sum(sum(len(meta["versions"]) for meta in p.values()) for p in self.prompts_info_by_sheet.values())
stats = {
"sheets": len(self.prompts_info_by_sheet),
"prompts": total_prompts,
"versions": total_versions,
"tools": len(self.tools) if self.tools else 0,
"social_accounts": len(self.social) if self.social else 0,
"crypto_wallets": len(self.wallets) if self.wallets else 0,
}
categories = []
for sheet_name in self.sheet_names_order:
prompts_info = self.prompts_info_by_sheet.get(sheet_name, {})
categories.append(
{
"name": sheet_name,
"prompt_count": len(prompts_info),
"version_count": sum(len(meta["versions"]) for meta in prompts_info.values()),
"prompts": [
{
"row": row,
"title": info["title"],
"versions": sorted(list(info["versions"].keys())),
"files": [info["versions"][v] for v in sorted(info["versions"].keys())],
}
for row, info in sorted(prompts_info.items())
],
}
)
excel_data = {
"total_rows": self.total_rows,
"total_cols": self.total_cols,
"sheets": list(self.prompts_info_by_sheet.keys()),
}
tools = {}
if self.tools:
for t in self.tools:
name = t.get("name") or "tool"
tools[name] = {k: v for k, v in t.items() if k != "name"}
social_media = {}
if self.social:
for s in self.social:
name = s.get("name") or "social"
social_media[name] = {k: v for k, v in s.items() if k != "name"}
support = {
"description": "礼貌要饭地址",
"crypto_wallets": self.wallets,
}
data = {
"last_updated": self.now.strftime("%Y-%m-%dT%H:%M:%S"),
"source": self.excel_path.name,
"stats": stats,
"categories": categories,
"excel_data": excel_data,
"tools": tools,
"social_media": social_media,
"support": support,
"misc": self.misc,
}
index_json_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def _generate_docs(self, sheets: Dict[str, pd.DataFrame]) -> None:
# docs/excel-data.md (full table)
excel_doc_path = self.docs_dir / "excel-data.md"
lines: List[str] = []
lines.append("# 📊 Excel原始数据完整记录\n")
lines.append("## 数据来源\n")
lines.append(f"- **文件**: {self.excel_path.name}\n")
lines.append(f"- **处理时间**: {self.now.strftime('%Y-%m-%d')}\n")
lines.append(f"- **工作表数量**: {len(sheets)}\n\n")
for sheet_name, df in sheets.items():
rows, cols = df.shape
lines.append(f"## 工作表: {sheet_name} ({rows}行×{cols}列)\n")
lines.append("\n| 行号 | 列1 | 列2 | 列3 |\n|-----:|-----|-----|-----|\n")
for r in range(rows):
c1 = df.iloc[r, 0] if cols > 0 else ""
c2 = df.iloc[r, 1] if cols > 1 else ""
c3 = df.iloc[r, 2] if cols > 2 else ""
def fmt(x) -> str:
try:
if x is None or (isinstance(x, float) and pd.isna(x)) or (hasattr(pd, 'isna') and pd.isna(x)):
return ""
except Exception:
pass
s = str(x)
return s.replace("|", "\\|")
lines.append(f"| {r} | {fmt(c1)} | {fmt(c2)} | {fmt(c3)} |\n")
lines.append("\n")
lines.append("\n---\n*完整数据提取自 {0}*\n".format(self.excel_path.name))
excel_doc_path.write_text("\n".join(lines), encoding="utf-8")
# docs/tools.md
tools_path = self.docs_dir / "tools.md"
t_lines: List[str] = []
t_lines.append("# 🛠️ 工具与资源(从Excel提取)\n")
if self.tools:
t_lines.append("\n## AI优化工具\n")
for t in self.tools:
t_lines.append("\n### {0}\n- **URL**: {1}\n- **描述**: {2}\n- **数据来源**: Excel表格第{3}\n".format(
t.get("name") or "工具",
t.get("url", ""),
t.get("description", ""),
(t.get("excel_row") or 0) + 1,
))
if self.social:
t_lines.append("\n## 社交媒体\n")
for s in self.social:
t_lines.append("\n### {0}\n- **URL**: {1}\n- **描述**: {2}\n- **数据来源**: Excel表格第{3}\n".format(
s.get("name") or "社交账号",
s.get("url", ""),
s.get("description", ""),
(s.get("excel_row") or 0) + 1,
))
t_lines.append("\n## 使用建议\n\n1. **OpenAI优化器**: 可以用来测试和改进本库中的提示词\n2. **社交媒体**: 关注获取项目更新和使用技巧\n3. **集成方式**: 可以将这些工具集成到自动化工作流中\n\n---\n*数据来源: {0}*\n".format(self.excel_path.name))
tools_path.write_text("\n".join(t_lines), encoding="utf-8")
# docs/support.md
support_path = self.docs_dir / "support.md"
s_lines: List[str] = []
s_lines.append("# 💰 项目支持(从Excel提取)\n")
s_lines.append("\n## 支持说明\n**礼貌要饭地址** - 如果这个项目对您有帮助,欢迎通过以下方式支持\n")
if self.wallets:
s_lines.append("\n## 加密货币钱包地址\n\n### 主流网络支持\n")
s_lines.append("\n| 网络名称 | 钱包地址 | Excel行号 |\n|----------|----------|-----------|\n")
for net, data in self.wallets.items():
s_lines.append("| **{0}** | `{1}` | 第{2}行 |\n".format(net.upper(), data.get("address", ""), (data.get("excel_row") or 0) + 1))
if self.misc:
for m in self.misc:
if m.get("type") == "warning" or "广告位" in m.get("content", ""):
s_lines.append("\n⚠️ **重要提醒**: {0}\n".format(m.get("content")))
s_lines.append("\n### 使用建议\n1. 请确认钱包地址的准确性\n2. 建议小额测试后再进行大额转账\n3. 不同网络的转账费用不同,请选择合适的网络\n\n---\n*钱包地址来源: {0}*\n".format(self.excel_path.name))
support_path.write_text("\n".join(s_lines), encoding="utf-8")
def _generate_readme(self) -> None:
readme_path = self.readme_target_root / "README.md"
total_prompts = sum(len(p) for p in self.prompts_info_by_sheet.values())
total_versions = sum(sum(len(meta["versions"]) for meta in p.values()) for p in self.prompts_info_by_sheet.values())
readme = []
readme.append("# 📚 提示词库(Excel转换版)\n")
readme.append("![同步状态](https://img.shields.io/badge/status-synced-green)")
readme.append(f"![提示词数量](https://img.shields.io/badge/prompts-{total_prompts}-blue)")
readme.append(f"![版本总数](https://img.shields.io/badge/versions-{total_versions}-orange)")
readme.append(f"![数据来源](https://img.shields.io/badge/source-Excel-yellow)\n")
readme.append(f"最后更新: {self.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
readme.append("\n## 📊 总览\n")
readme.append(f"- **数据来源**: {self.excel_path.name}\n")
readme.append(f"- **分类数量**: {len(self.prompts_info_by_sheet)} \n- **提示词总数**: {total_prompts}\n- **版本总数**: {total_versions}\n")
readme.append("\n## 📂 分类导航\n")
for i, sheet_name in enumerate(self.sheet_names_order, start=1):
prompts_info = self.prompts_info_by_sheet.get(sheet_name, {})
folder = f"({i})_{self._sanitize_filename(sheet_name)}"
ver_count = sum(len(meta["versions"]) for meta in prompts_info.values())
readme.append(f"- [{sheet_name}](./prompts/{folder}/) - {len(prompts_info)} 个提示词, {ver_count} 个版本\n")
readme.append("\n## 🔄 同步信息\n")
readme.append(f"- **数据源**: {self.excel_path.name}\n- **处理时间**: {self.now.strftime('%Y-%m-%d %H:%M:%S')}\n")
readme.append("\n## 📝 许可证\n本项目采用 MIT 许可证\n")
readme.append("\n---\n*完全基于 Excel 表格自动生成*\n")
readme_path.write_text("\n".join(readme), encoding="utf-8")
def convert(self) -> None:
self._ensure_dirs()
sheets = self._read_excel_sheets()
# If no sheets returned (shouldn't happen), fallback to empty
for idx, sheet_name in enumerate(self.sheet_names_order, start=1):
df = sheets[sheet_name]
# Prepare per-sheet folder
folder_name = f"({idx})_{self._sanitize_filename(sheet_name)}"
category_dir = self.prompts_dir / folder_name
category_dir.mkdir(parents=True, exist_ok=True)
# Classify rows
rows = self._classify_rows(df)
prompts_info: Dict[int, Dict] = {}
# Build prompt files for this sheet
for rc in rows:
if rc.kind == "prompt":
excel_row_number = rc.row_index + 1
versions: Dict[int, str] = rc.data["versions"]
title = self._extract_title(list(versions.values()))
prompts_info[excel_row_number] = {"title": title, "versions": {}}
# Rewrite files directly into category_dir
for col_num, content in versions.items():
row_col = f"({excel_row_number},{col_num})"
filename = f"{row_col}_{title}.md"
(category_dir / filename).write_text((content or "").rstrip("\n") + "\n", encoding="utf-8")
prompts_info[excel_row_number]["versions"][col_num] = filename
elif rc.kind == "tool":
url = rc.data.get("url", "")
self.tools.append({
"name": "OpenAI 提示词优化平台" if "openai" in url else "工具",
"url": url,
"description": rc.data.get("description", ""),
"excel_row": rc.row_index,
"sheet": sheet_name,
})
elif rc.kind == "social":
url = rc.data.get("url", "")
name = "Twitter/X 账号" if ("x.com" in url or "twitter.com" in url) else "社交账号"
self.social.append({
"name": name,
"url": url,
"description": rc.data.get("description", ""),
"excel_row": rc.row_index,
"sheet": sheet_name,
})
elif rc.kind == "wallet":
network = str(rc.data.get("network", "")).strip()
address = str(rc.data.get("address", "")).strip()
if network and address:
self.wallets[network.lower()] = {
"address": address,
"excel_row": rc.row_index,
"sheet": sheet_name,
}
elif rc.kind == "warning":
self.misc.append({"type": "warning", "excel_row": rc.row_index, "content": rc.data.get("content", ""), "sheet": sheet_name})
# Save per-sheet prompts map and index
self.prompts_info_by_sheet[sheet_name] = prompts_info
self._generate_category_index(sheet_name, category_dir, prompts_info)
# Global indices and docs
self._generate_prompts_index_json()
self._generate_docs(sheets)
self._generate_readme()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Convert local Excel into prompt library structure")
parser.add_argument("--excel", type=str, default=None, help="Path to the Excel file (default from config)")
parser.add_argument("--config", type=str, default=None, help="Path to config.yaml (optional)")
parser.add_argument("--category-name", type=str, default="prompt-category", help="Output category folder name")
parser.add_argument("--out-dir", type=str, default=None, help="Optional snapshot output root. If set, writes to <out-dir>/prompts and <out-dir>/docs")
return parser.parse_args()
def main() -> None:
args = parse_args()
script_path = Path(__file__).resolve()
prompt_library_dir = script_path.parent.parent
project_root = prompt_library_dir.parent
config_path = Path(args.config).resolve() if args.config else (prompt_library_dir / "scripts" / "config.yaml")
# Resolve Excel path
if args.excel:
excel_path = Path(args.excel)
if not excel_path.is_absolute():
excel_path = (project_root / excel_path).resolve()
else:
# Try config
cfg_excel = None
if config_path.exists() and yaml is not None:
with config_path.open("r", encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
cfg_excel = ((cfg.get("source") or {}).get("excel_file") or None)
excel_path = (project_root / cfg_excel).resolve() if cfg_excel else (project_root / "prompt (2).xlsx").resolve()
if not excel_path.exists():
raise FileNotFoundError(f"Excel file not found: {excel_path}")
out_dir = Path(args.out_dir).resolve() if args.out_dir else None
converter = ExcelPromptConverter(
project_root=project_root,
prompt_library_dir=prompt_library_dir,
excel_path=excel_path,
category_name=args.category_name,
config_path=config_path if config_path.exists() else None,
output_root=out_dir,
)
converter.convert()
target = out_dir if out_dir else prompt_library_dir
print(f"✅ Conversion complete. Output under: {target}")
if __name__ == "__main__":
main()
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
docs_to_excel.py
Documents → Excel converter: rebuild a workbook from prompts folders.
Rules (per STRUCTURE_AND_CONVERSION_SPEC.md):
- Each folder under prompt-library/prompts that matches "(N)_<name>" or any folder is a sheet
- For each file matching "(r,c)_*.md", write its full text to Excel cell (r,c), 1-based
- Title part in filename is ignored for cell value
- Non-matching files are ignored
- Optionally clears existing workbook or merges (default: overwrite generate new)
Usage:
python prompt-library/scripts/docs_to_excel.py --out "rebuilt.xlsx"
# optional: --prompts-dir prompt-library/prompts --clear
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
from typing import Dict, Tuple
import pandas as pd
from openpyxl import Workbook
FOLDER_PREFIX_RE = re.compile(r"^\((\d+)\)_")
FILE_NAME_RE = re.compile(r"^\((\d+),(\d+)\)_.*\.md$")
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Rebuild Excel workbook from prompt folders")
p.add_argument("--prompts-dir", type=str, default="prompt-library/prompts", help="Prompts root directory")
p.add_argument("--out", type=str, required=True, help="Output Excel file path")
return p.parse_args()
def list_sheet_folders(prompts_root: Path) -> Dict[str, Path]:
sheets: Dict[str, Path] = {}
for child in sorted(prompts_root.iterdir()):
if not child.is_dir():
continue
if child.name == "prompt-category":
# legacy; skip auto-generated category
continue
sheets[child.name] = child
return sheets
def extract_rc(name: str) -> Tuple[int, int] | None:
m = FILE_NAME_RE.match(name)
if not m:
return None
r = int(m.group(1))
c = int(m.group(2))
return r, c
def main() -> None:
args = parse_args()
prompts_root = Path(args.prompts_dir).resolve()
out_path = Path(args.out).resolve()
if not prompts_root.exists():
raise FileNotFoundError(f"Prompts directory not found: {prompts_root}")
sheet_folders = list_sheet_folders(prompts_root)
if not sheet_folders:
raise RuntimeError("No sheet folders found under prompts root")
wb = Workbook()
# remove default sheet
default = wb.active
wb.remove(default)
for folder_name, folder_path in sheet_folders.items():
# Recover original sheet name (try to drop ordering prefix "(N)_")
m = FOLDER_PREFIX_RE.match(folder_name)
sheet_name = folder_name[m.end():] if m else folder_name
if not sheet_name:
sheet_name = folder_name
ws = wb.create_sheet(title=sheet_name)
# Aggregate cells
max_row = 0
max_col = 0
cells: Dict[Tuple[int, int], str] = {}
for file in folder_path.iterdir():
if not file.is_file() or not file.name.endswith('.md'):
continue
rc = extract_rc(file.name)
if not rc:
continue
r, c = rc
text = file.read_text(encoding='utf-8')
# Trim a single trailing newline for cell value aesthetics
if text.endswith("\n"):
text = text[:-1]
cells[(r, c)] = text
if r > max_row:
max_row = r
if c > max_col:
max_col = c
# Write into sheet
for (r, c), val in cells.items():
ws.cell(row=r, column=c, value=val)
# Save workbook
out_path.parent.mkdir(parents=True, exist_ok=True)
wb.save(str(out_path))
print(f"✅ Rebuilt Excel saved to: {out_path}")
if __name__ == "__main__":
main()
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
excel_to_docs.py
Thin wrapper that invokes the Excel → Documents converter implemented
in convert_local.py, keeping a clearer entrypoint name.
Usage:
python prompt-library/scripts/excel_to_docs.py --excel "prompt (2).xlsx"
# optional:
# --category-name <fallback> --config prompt-library/scripts/config.yaml
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
def main() -> None:
script = Path(__file__).resolve().parent / "convert_local.py"
spec = importlib.util.spec_from_file_location("convert_local", str(script))
if spec is None or spec.loader is None:
raise RuntimeError("Unable to load convert_local.py")
module = importlib.util.module_from_spec(spec)
sys.modules["convert_local"] = module
spec.loader.exec_module(module) # type: ignore
# Delegate to its CLI
module.main() # type: ignore
if __name__ == "__main__":
main()
@@ -0,0 +1,53 @@
import json
import shutil
from collections import defaultdict
input_file = "prompt_jsonl/prompt_docs_refactored.jsonl"
output_file = "prompt_jsonl/prompt_docs_refactored_clean.jsonl"
backup_file = "prompt_jsonl/prompt_docs_refactored.jsonl.bak"
def filter_versions():
# 1. Backup
shutil.copy(input_file, backup_file)
print(f"Backup created: {backup_file}")
# 2. Group by (category, row) and find max col
latest_versions = {} # Key: (category, row), Value: item_dict
with open(input_file, 'r', encoding='utf-8') as f:
for line in f:
if not line.strip(): continue
item = json.loads(line)
cat = item.get('category', 'Uncategorized')
row = item.get('row', 0)
col = item.get('col', 0)
key = (cat, row)
if key not in latest_versions:
latest_versions[key] = item
else:
# If current item has higher col, replace it
if col > latest_versions[key].get('col', 0):
latest_versions[key] = item
# 3. Write filtered data
count = 0
with open(output_file, 'w', encoding='utf-8') as f:
# Sort by category then row for tidiness
sorted_keys = sorted(latest_versions.keys(), key=lambda x: (x[0], x[1]))
for key in sorted_keys:
item = latest_versions[key]
f.write(json.dumps(item, ensure_ascii=False) + '\n')
count += 1
print(f"Filtered file written: {output_file}")
print(f"Total prompts retained: {count}")
# Overwrite original for downstream scripts
shutil.move(output_file, input_file)
print(f"Overwritten original file: {input_file}")
if __name__ == "__main__":
filter_versions()
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""
使用 Gemini CLI 按固定系统提示词,将指定目录下的 .md 提示词批量转换为 JSONL。
特点:
- 内置系统提示词,与《Gemini 无头模式 JSONL 规范化指引》一致
- 禁用工具调用 (--allowed-tools ''), 输出纯文本,每个文件生成一行 JSON
- 默认输入目录为仓库根下的 `2/`,输出为 `2/prompts.jsonl`
用法示例:
python3 gemini_jsonl_batch.py
python3 gemini_jsonl_batch.py --input 2 --output 2/prompts.jsonl --model gemini-2.5-flash
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
# ==================== 固定系统提示词 ====================
SYS_PROMPT = """{"category_id": 1, "category": "JSONL规范化", "row": 2, "col": 1, "title": "# JSONL 提示词转换器 - 系统提示词", "content": "# JSONL 提示词转换器 - 系统提示词\n\n你是一个专业 的提示词格式转换器。将用户提供的提示词内容转换为标准 JSONL 格式。\n\n## 输出格式\n\n```json\n{\\"title\\\": \\"<标题>\\", \\"content\\\": \\"<完整内容>\\"}\n```\n\n### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `title` | string | 提示词标题,取内容的第一行或前 50 字符 |\n| `content` | string | 完整的提示词内容 |\n\n## 转换规则\n\n1. **标题提取**\n - 若内容以 `#` 开头,取第一个标题作为 title\n - 否则取前 50 字符(去除换行)\n2. **内容转义**\n - 换行符 转为 `\\\\n`\n - 双引号转为 `\\\\\"`\n - 反斜杠转为 `\\\\\\\\`\n\n## 输出要求\n\n- 每行一个完整的 JSON 对象\n- 不要添加任何解释、注释或额外文字\n- 不要用 ```json 代码块包裹\n- 直接输出纯 JSONL 内容\n\n## 示例\n\n### 输入\n```\n# Role:智能文档助手\n\n## Background\n用户需要一个能够处理文档的 AI 助手。\n\n## Skills\n- 文档解析\n- 格式转换\n```\n\n### 输出\n```\n{\\"title\\\": \\"# Role:智能文档助手\\", \\"content\\\": \\"# Role:智能文档助手\\\\n\\\\n## Background\\\\n用户需要一个能够处 理文档的 AI 助手。\\\\n\\\\n## Skills\\\\n- 文档解析\\\\n- 格式转换\\"}\n```\n\n---\n\n现在,请将用户提供的内容转换为标准 JSONL 格式。"}"""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="使用 Gemini CLI 批量将 .md 提示词转换为 JSONL(固定系统提示词)。",
)
parser.add_argument(
"-i",
"--input",
type=Path,
default=Path("2"),
help="输入目录,遍历其中的 .md 文件(默认:仓库根目录下的 2/)",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=None,
help="输出 JSONL 文件路径,默认写入 <input>/prompts.jsonl",
)
parser.add_argument(
"-m",
"--model",
default="gemini-2.5-flash",
help="Gemini 模型名称(默认:gemini-2.5-flash",
)
parser.add_argument(
"--gemini-cmd",
default="gemini",
help="Gemini CLI 可执行文件名或路径(默认:gemini)",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="输出处理中的详细信息",
)
return parser.parse_args()
def run_gemini(content: str, model: str, cmd: str) -> str:
"""调用 Gemini CLI,将单个文本转换为一行 JSON。"""
proc = subprocess.run(
[
cmd,
"-m",
model,
"--output-format",
"text",
"--allowed-tools",
"",
SYS_PROMPT,
],
input=content.encode("utf-8"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
stdout = proc.stdout.decode("utf-8", errors="replace").strip()
stderr = proc.stderr.decode("utf-8", errors="replace").strip()
if proc.returncode != 0:
raise RuntimeError(f"Gemini 调用失败 (code={proc.returncode}): {stderr or '无错误输出'}")
if stderr:
# 某些 CLI 可能在 stderr 打印警告,保留但不中断
print(f"⚠️ Gemini 警告: {stderr}", file=sys.stderr)
if not stdout:
raise RuntimeError("Gemini 未返回内容")
# 去除多余行,只保留非空行并合并
lines = [ln for ln in stdout.splitlines() if ln.strip()]
return " ".join(lines).strip()
def main() -> None:
args = parse_args()
input_dir = args.input.resolve()
if not input_dir.exists() or not input_dir.is_dir():
print(f"❌ 输入目录不存在: {input_dir}")
sys.exit(1)
output_path = args.output or (input_dir / "prompts.jsonl")
output_path = output_path.resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
md_files = sorted(f for f in input_dir.iterdir() if f.suffix == ".md")
if not md_files:
print(f"⚠️ 未找到任何 .md 文件: {input_dir}")
sys.exit(0)
results = []
for md in md_files:
content = md.read_text(encoding="utf-8")
if args.verbose:
print(f"→ 处理 {md.name}")
try:
json_line = run_gemini(content, args.model, args.gemini_cmd)
results.append(json_line)
except Exception as exc: # noqa: BLE001
print(f"❌ 处理失败 {md.name}: {exc}", file=sys.stderr)
with output_path.open("w", encoding="utf-8") as f:
for line in results:
f.write(line + "\n")
print(f"✅ 完成:{len(results)} 条 → {output_path}")
if __name__ == "__main__":
main()
@@ -0,0 +1,36 @@
import json
import pandas as pd
input_file = "prompt_jsonl/prompt_docs_refactored.jsonl"
output_file = "prompt_excel/prompt_docs_refactored.xlsx"
def process():
data_by_cat = {}
with open(input_file, 'r', encoding='utf-8') as f:
for line in f:
if not line.strip(): continue
item = json.loads(line)
cat = item['category']
if cat not in data_by_cat:
data_by_cat[cat] = []
# Reconstruct the JSON string for the cell as it was in original Excel
cell_data = {
"title": item.get('title', ''),
"content": item.get('content', '')
}
data_by_cat[cat].append(json.dumps(cell_data, ensure_ascii=False))
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
# Sort categories to keep a consistent order
sorted_cats = sorted(data_by_cat.keys())
for cat in sorted_cats:
items = data_by_cat[cat]
# Each item in its own row, column 0
df = pd.DataFrame(items)
df.to_excel(writer, sheet_name=cat, index=False, header=False)
print(f"Excel created: {output_file}")
if __name__ == "__main__":
process()
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
将 prompt_docs 目录下的 md 文件转换为 JSONL 格式
用法:
python md_to_jsonl.py <prompt_docs目录>
python md_to_jsonl.py prompt_docs/prompt_docs_2025_1222_004537
"""
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = REPO_ROOT / "prompt_jsonl"
def convert(docs_dir: Path):
prompts_dir = docs_dir / "prompts"
if not prompts_dir.exists():
print(f"❌ 找不到 prompts 目录: {prompts_dir}")
return
# 输出文件名基于输入目录名
output_file = OUTPUT_DIR / f"{docs_dir.name}.jsonl"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
records = []
for category_dir in sorted(prompts_dir.iterdir()):
if not category_dir.is_dir():
continue
m = re.match(r'\((\d+)\)_(.+)', category_dir.name)
cat_id, cat_name = (m.groups() if m else (0, category_dir.name))
for md_file in sorted(category_dir.glob("*.md")):
if md_file.name == "index.md":
continue
fm = re.match(r'\((\d+),(\d+)\)_(.+)\.md', md_file.name)
if not fm:
continue
row, col, title = fm.groups()
content = md_file.read_text(encoding='utf-8')
records.append({
"category_id": int(cat_id),
"category": cat_name,
"row": int(row),
"col": int(col),
"title": title[:80],
"content": content
})
with open(output_file, 'w', encoding='utf-8') as f:
for r in records:
f.write(json.dumps(r, ensure_ascii=False) + '\n')
print(f"✅ 转换完成: {len(records)} 条 → {output_file}")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
docs_dir = Path(sys.argv[1])
if not docs_dir.is_absolute():
docs_dir = REPO_ROOT / docs_dir
convert(docs_dir)
if __name__ == "__main__":
main()
@@ -0,0 +1,154 @@
import json
import os
mapping = {
# 编程技术
"软件工程,glue_coding_用提示词": "编程技术",
"前端复刻流程": "编程技术",
"输入转单行JSON": "编程技术",
"序列图生成": "编程技术",
"流程图": "编程技术",
"函数化万物": "编程技术",
"编程知识库": "编程技术",
"网页UI逆向分析提示词": "编程技术",
"用户优化前端设计": "编程技术",
"图像特征提取": "编程技术",
"前端通用设计": "编程技术",
# 逻辑工具箱
"哲学工具箱": "逻辑工具箱",
"逻辑工具箱": "逻辑工具箱",
"批判性思维分析": "逻辑工具箱",
"思维模型": "逻辑工具箱",
"政治批判工具箱": "逻辑工具箱",
"未来视角": "逻辑工具箱",
"层级结构分析": "逻辑工具箱",
"问题分类识别": "逻辑工具箱",
"分析": "逻辑工具箱",
"终极本质分析": "逻辑工具箱",
"事实核查": "逻辑工具箱",
"关键词图谱": "逻辑工具箱",
"语言分析元prompt": "逻辑工具箱",
"逻辑分析": "逻辑工具箱",
"黄金圈解释": "逻辑工具箱",
"谋士": "逻辑工具箱",
"经验": "逻辑工具箱",
"": "逻辑工具箱",
"": "逻辑工具箱",
"": "逻辑工具箱",
"": "逻辑工具箱",
"心经口诀创作提示词": "逻辑工具箱",
"临界知识": "逻辑工具箱",
"项目分析": "逻辑工具箱",
"对话提问": "逻辑工具箱",
"思维导图": "逻辑工具箱",
# 内容创作
"文案逆向": "内容创作",
"x_prompt收集": "内容创作",
"x提示词收集": "内容创作",
"x爆款文案生成器": "内容创作",
"推文制作提示词": "内容创作",
"李继刚文选": "内容创作",
"解释提示词": "内容创作",
"一句话描述任何内容": "内容创作",
"子弹总结": "内容创作",
"文本转md语法电子书处理": "内容创作",
"排版和图片,视频转文本": "内容创作",
"艺术风格描述": "内容创作",
"视频生成提示词": "内容创作",
"图片逆向": "内容创作",
"排版": "内容创作",
"内容提炼": "内容创作",
"简讯提示词": "内容创作",
"艺术": "内容创作",
"人话写作": "内容创作",
"小红书": "内容创作",
"组织语言": "内容创作",
"正向人物生平报告官方文案": "内容创作",
"gemini字幕处理": "内容创作",
# 学习教育
"学习提示词": "学习教育",
"学习用提示词": "学习教育",
"ai学习用提示词": "学习教育",
"书籍结构化分析": "学习教育",
"典籍句子学习": "学习教育",
"anki卡片格式输出": "学习教育",
"notebookllm用提示词": "学习教育",
"英文学习": "学习教育",
"速成学习": "学习教育",
"论文解读": "学习教育",
"真传一句话": "学习教育",
"学习音频": "学习教育",
"豆包听书": "学习教育",
"最小知识框架": "学习教育",
# 商业分析
"grok商业金融分析提示词": "商业分析",
"投资调研": "商业分析",
"行业分析": "商业分析",
"需求对齐": "商业分析",
"需求结构化描述": "商业分析",
"麦肯锡行业分析": "商业分析",
"产品策略": "商业分析",
"行业咨询": "商业分析",
"需求解析": "商业分析",
"SOP制作": "商业分析",
# 提示词工程
"元提示词": "提示词工程",
"提示词模块": "提示词工程",
"根据内容逆向提示词": "提示词工程",
"系统提示词": "提示词工程",
"AI使用思维": "提示词工程",
"使用ai的思维": "提示词工程",
"最小字数系统提示词": "提示词工程",
"ChatGPT": "提示词工程",
"Reddit提示词": "提示词工程",
"好prompt生成器": "提示词工程",
"思维协议": "提示词工程",
"grok抓取提示词": "提示词工程",
# 其他
"AI_交易系统提示词": "综合杂项",
"面向CZ": "综合杂项",
}
id_map = {
"编程技术": 1,
"逻辑工具箱": 2,
"内容创作": 3,
"学习教育": 4,
"商业分析": 5,
"提示词工程": 6,
"综合杂项": 7
}
input_file = "prompt_jsonl/prompt_docs_2025_1222_004537.jsonl"
output_file = "prompt_jsonl/prompt_docs_refactored.jsonl"
def process():
stats = {}
with open(input_file, 'r', encoding='utf-8') as fin, \
open(output_file, 'w', encoding='utf-8') as fout:
for line in fin:
if not line.strip(): continue
data = json.loads(line)
old_cat = data.get('category', '')
new_cat = mapping.get(old_cat, "综合杂项")
# Keep original category in tags if it doesn't exist?
# Or just replace. The user said "只调整 'category'"
data['category'] = new_cat
data['category_id'] = id_map.get(new_cat, 7)
fout.write(json.dumps(data, ensure_ascii=False) + '\n')
stats[new_cat] = stats.get(new_cat, 0) + 1
print("Refactor complete.")
for cat, count in stats.items():
print(f"{cat}: {count}")
if __name__ == "__main__":
process()
@@ -0,0 +1,49 @@
import json
import shutil
from collections import defaultdict
input_file = "prompt_jsonl/prompt_docs_refactored.jsonl"
output_file = "prompt_jsonl/prompt_docs_refactored_reindexed.jsonl"
backup_file = "prompt_jsonl/prompt_docs_refactored_before_reindex.jsonl.bak"
def reindex_rows():
# 1. Backup
shutil.copy(input_file, backup_file)
print(f"Backup created: {backup_file}")
# 2. Load and Group
items_by_cat = defaultdict(list)
with open(input_file, 'r', encoding='utf-8') as f:
for line in f:
if not line.strip(): continue
item = json.loads(line)
cat = item.get('category', 'Uncategorized')
items_by_cat[cat].append(item)
# 3. Sort and Reindex
total_items = 0
with open(output_file, 'w', encoding='utf-8') as f:
# Sort categories for consistent file order
for cat in sorted(items_by_cat.keys()):
items = items_by_cat[cat]
# Sort items by their OLD row to preserve relative order
items.sort(key=lambda x: x.get('row', 0))
# Reassign row numbers starting from 1
for i, item in enumerate(items):
item['row'] = i + 1
f.write(json.dumps(item, ensure_ascii=False) + '\n')
total_items += 1
print(f"Category '{cat}': re-indexed {len(items)} items.")
print(f"Re-indexed file written: {output_file}")
print(f"Total items: {total_items}")
# Overwrite original
shutil.move(output_file, input_file)
print(f"Overwritten original file: {input_file}")
if __name__ == "__main__":
reindex_rows()
@@ -0,0 +1,11 @@
# 提示词库管理系统依赖包
pandas==2.1.4
openpyxl==3.1.2
google-auth==2.22.0
google-auth-oauthlib==1.0.0
google-auth-httplib2==0.1.0
google-api-python-client==2.96.0
PyYAML==6.0.1
python-dotenv==1.0.0
rich==13.7.1
InquirerPy==0.3.4
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
start_convert.py
Launcher that orchestrates conversions between Excel workbooks and prompt documents
using the following conventions:
Input locations (relative to repo root):
- ./prompt_excel/ # place .xlsx files here for Excel → Docs
- ./prompt_docs/ # place prompt folders here for Docs → Excel
Output locations (under repo root, named by source file/folder mtime):
- ./prompt_docs_YYYYMMDD_HHMMSS/ # Excel → Docs results (copies of prompts/*)
- ./prompt_excel_YYYYMMDD_HHMMSS/ # Docs → Excel results (rebuilt.xlsx)
Usage:
# Auto mode: if there are .xlsx under prompt_excel, run Excel→Docs;
# if there is a docs set under prompt_docs, run Docs→Excel.
python prompt-library/scripts/start_convert.py
# Force a mode:
python prompt-library/scripts/start_convert.py --mode excel2docs
python prompt-library/scripts/start_convert.py --mode docs2excel
Notes:
- No interactive prompts; behavior is driven by the file presence and CLI flags
- Requires pandas, openpyxl, PyYAML (see scripts/requirements.txt)
"""
from __future__ import annotations
import argparse
import importlib.util
import shutil
import sys
from datetime import datetime
from pathlib import Path
from typing import List
def ts_from_path(p: Path) -> str:
st = p.stat()
# Prefer creation/birth time when available; fall back to mtime
ts = getattr(st, "st_birthtime", None)
if ts is None:
# On Windows, st_ctime is creation; on Linux it's inode change time
# We still prefer mtime for consistency if birthtime is unavailable.
ts = st.st_mtime
# Format: YYYY_MMDD_HHMMSS per requirement example 2025_0102_2309
return datetime.fromtimestamp(ts).strftime("%Y_%m%d_%H%M%S")
def load_module(py_path: Path, module_name: str):
spec = importlib.util.spec_from_file_location(module_name, str(py_path))
if spec is None or spec.loader is None:
raise RuntimeError(f"Unable to load module: {py_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module) # type: ignore
return module
def run_excel_to_docs_for_file(excel_path: Path, prompt_library_dir: Path, out_root: Path) -> Path:
convert_path = prompt_library_dir / "scripts" / "convert_local.py"
mod = load_module(convert_path, "convert_local")
project_root = prompt_library_dir.parent
# Prepare snapshot output directory under repo_root/prompt_docs/
base_dir = out_root / "prompt_docs"
base_dir.mkdir(parents=True, exist_ok=True)
out_dir = base_dir / f"prompt_docs_{ts_from_path(excel_path)}"
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
converter = mod.ExcelPromptConverter(
project_root=project_root,
prompt_library_dir=prompt_library_dir,
excel_path=excel_path,
category_name="prompt-category",
config_path=None,
output_root=out_dir,
)
converter.convert()
return out_dir
def run_docs_to_excel_for_dir(prompts_dir: Path, scripts_dir: Path, out_root: Path) -> Path:
docs2excel_path = scripts_dir / "docs_to_excel.py"
mod = load_module(docs2excel_path, "docs_to_excel")
# Determine timestamp from folder creation (or mtime fallback)
base_dir = out_root / "prompt_excel"
base_dir.mkdir(parents=True, exist_ok=True)
ts_fmt = ts_from_path(prompts_dir)
out_dir = base_dir / f"prompt_excel_{ts_fmt}"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / "rebuilt.xlsx"
# Resolve actual prompts root (support either the prompts/ subfolder or direct sheet folders)
prompts_root = prompts_dir / "prompts" if (prompts_dir / "prompts").exists() else prompts_dir
# Invoke module's main via argparse emulation
sys.argv = [str(docs2excel_path), "--prompts-dir", str(prompts_root), "--out", str(out_path)]
mod.main() # type: ignore
return out_dir
def find_xlsx_files(input_excel_dir: Path) -> List[Path]:
if not input_excel_dir.exists():
return []
return sorted([p for p in input_excel_dir.iterdir() if p.is_file() and p.suffix.lower() in {".xlsx"}], key=lambda p: p.stat().st_mtime)
def has_prompt_files(input_docs_dir: Path) -> bool:
if not input_docs_dir.exists():
return False
for p in input_docs_dir.rglob("*.md"):
if p.name.startswith("(") and ")_" in p.name:
return True
return False
def main() -> None:
parser = argparse.ArgumentParser(description="Start conversion between Excel and prompt docs")
parser.add_argument("--mode", choices=["auto", "excel2docs", "docs2excel"], default="auto")
parser.add_argument("--excel-dir", default="prompt_excel", help="Input directory containing .xlsx files")
parser.add_argument("--docs-dir", default="prompt_docs", help="Input directory containing prompt folders")
parser.add_argument("--select", type=str, default=None, help="Optional path to a specific Excel file or prompts folder to convert")
args = parser.parse_args()
script_path = Path(__file__).resolve()
prompt_library_dir = script_path.parent.parent # repo root (prompt-library)
project_root = prompt_library_dir # use prompt-library as root for I/O
input_excel_dir = (prompt_library_dir / args.excel_dir).resolve()
input_docs_dir = (prompt_library_dir / args.docs_dir).resolve()
ran_any = False
if args.mode in ("auto", "excel2docs"):
# If user explicitly selected a file, prefer it
if args.select:
sel = Path(args.select)
if not sel.is_absolute():
sel = (project_root / sel).resolve()
if sel.is_file() and sel.suffix.lower() == ".xlsx":
out_dir = run_excel_to_docs_for_file(sel, prompt_library_dir, project_root)
rel = out_dir.relative_to(prompt_library_dir)
print(f"✅ Excel→Docs OK: {sel.name}{rel}")
ran_any = True
else:
xlsx_files = find_xlsx_files(input_excel_dir)
for xlsx in xlsx_files:
out_dir = run_excel_to_docs_for_file(xlsx, prompt_library_dir, project_root)
rel = out_dir.relative_to(prompt_library_dir)
print(f"✅ Excel→Docs OK: {xlsx.name}{rel}")
ran_any = True
if args.mode in ("auto", "docs2excel"):
if args.select:
sel = Path(args.select)
if not sel.is_absolute():
sel = (project_root / sel).resolve()
if sel.exists() and sel.is_dir():
out_dir = run_docs_to_excel_for_dir(sel, prompt_library_dir / "scripts", project_root)
rel = out_dir.relative_to(prompt_library_dir)
# show sel relative as well when possible
try:
sel_rel = Path(sel).relative_to(prompt_library_dir)
except Exception:
sel_rel = Path(sel)
print(f"✅ Docs→Excel OK: {sel_rel}{rel}")
ran_any = True
else:
if has_prompt_files(input_docs_dir):
out_dir = run_docs_to_excel_for_dir(input_docs_dir, prompt_library_dir / "scripts", project_root)
rel = out_dir.relative_to(prompt_library_dir)
print(f"✅ Docs→Excel OK: {args.docs_dir}{rel}")
ran_any = True
if not ran_any:
print("️ Nothing to do. Place .xlsx under ./prompt_excel or prompt docs under ./prompt_docs, or use --mode to force.")
if __name__ == "__main__":
main()
@@ -0,0 +1,109 @@
import json
import os
from collections import defaultdict
jsonl_path = "prompt_jsonl/prompt_docs_refactored.jsonl"
docs_root = "prompt_docs/prompt_docs_refactored/prompts"
def verify():
print("=== 开始全面完整性检查 ===\n")
# 1. JSONL 数据加载与基础检查
if not os.path.exists(jsonl_path):
print(f"❌ 错误: JSONL 文件不存在: {jsonl_path}")
return
data = []
with open(jsonl_path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip():
try:
data.append(json.loads(line))
except json.JSONDecodeError:
print(f"❌ 错误: 发现无效的 JSON 行: {line[:50]}...")
total_items = len(data)
print(f"✅ JSONL 读取成功,共 {total_items} 条数据。")
# 2. 规则验证
errors = []
categories = defaultdict(list)
expected_categories = {
"内容创作", "商业分析", "学习教育", "提示词工程", "综合杂项", "编程技术", "逻辑工具箱"
}
for item in data:
cat = item.get('category')
row = item.get('row')
col = item.get('col')
title = item.get('title')
content = item.get('content')
# 收集分类数据用于后续分析
categories[cat].append(row)
# 检查 1: 分类合法性
if cat not in expected_categories:
errors.append(f"❌ 未知分类: '{cat}' (Title: {title[:20]}...)")
# 检查 2: 列归位 (col == 1)
if col != 1:
errors.append(f"❌ 列未归位: Category '{cat}', Row {row}, Col {col} (应为 1)")
# 检查 3: 内容完整性 (简单检查)
if not title:
errors.append(f"⚠️ 警告: 标题为空 (Category '{cat}', Row {row})")
if not content or len(content) < 5:
errors.append(f"⚠️ 警告: 内容过短或为空 (Category '{cat}', Row {row}, Content len: {len(content) if content else 0})")
# 检查 4: 行连续性
print("\n--- 分类与行号连续性检查 ---")
for cat, rows in categories.items():
rows.sort()
count = len(rows)
if count == 0:
print(f"⚠️ 分类 '{cat}' 为空")
continue
max_row = rows[-1]
expected_rows = list(range(1, count + 1))
status = "✅ 正常"
if rows != expected_rows:
status = "❌ 异常 (行号不连续或重复)"
errors.append(f"行号错误: {cat} (Expect 1-{count}, Got max {max_row})")
print(f"{cat.ljust(10)}: {count} 条 | Max Row: {max_row} | {status}")
# 3. 文件系统同步检查
print("\n--- 文档文件同步检查 ---")
files_found = 0
if os.path.exists(docs_root):
for root, dirs, files in os.walk(docs_root):
for file in files:
if file.endswith(".md") and not file.startswith("index"):
files_found += 1
else:
print(f"❌ 文档目录不存在: {docs_root}")
print(f"JSONL 条目数: {total_items}")
print(f"Markdown 文件数: {files_found}")
if total_items == files_found:
print("✅ 文件数量一致")
else:
print(f"❌ 文件数量不匹配! (差值: {files_found - total_items})")
errors.append("文件系统数量与 JSONL 不一致")
# 4. 总结
print("\n=== 检查总结 ===")
if not errors:
print("🎉 完美!所有检查通过。数据结构完整、规范。")
else:
print(f"发现 {len(errors)} 个问题,请检视:")
for err in errors:
print(err)
if __name__ == "__main__":
verify()