refactor(todo): 去除 MySQL 依赖,改为 JSON 本地存储
- 数据存储: ~/.local/share/todo/tasks.json 纯字符串数组 - 操作简化: 仅保留 add/list/done,无状态/优先级/归档 - 新增 scripts/validate.py 校验脚本 (格式/结构/去重) - 新增 schema/tasks.schema.json - 删除 references/setup.md (MySQL 配置指南) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+82
-167
@@ -1,197 +1,112 @@
|
|||||||
---
|
---
|
||||||
name: todo
|
name: todo
|
||||||
description: |
|
description: |
|
||||||
任务待办管理 skill。通过自然语言操作 MySQL 数据库中的待办事项。
|
极简任务管理 skill。通过 JSON 文件本地存储任务列表,仅记录任务的有与无。
|
||||||
当用户提到待办、任务管理、todo list、添加任务、查看任务、标记完成、
|
当用户提到待办、任务管理、todo list、添加任务、查看任务、标记完成时,使用此 skill。
|
||||||
清理任务、归档旧任务时,使用此 skill。
|
支持添加、查看、完成三个操作,参数歧义时与用户确认。
|
||||||
支持增删改查和基于 LRU 策略的自动归档。
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 任务待办管理
|
# 极简任务管理
|
||||||
|
|
||||||
通过自然语言管理 MySQL 数据库中的待办事项,支持 LRU 自动淘汰。
|
通过自然语言管理本地 JSON 文件中的任务列表。任务只有「存在」和「不存在」两种状态。
|
||||||
|
|
||||||
## 工具说明
|
## 数据存储
|
||||||
|
|
||||||
本 skill 依赖 **MySQL MCP 工具** `mysql_query`。所有数据库操作都通过该工具执行。
|
- 文件路径:`~/.local/share/todo/tasks.json`
|
||||||
|
- 数据格式:
|
||||||
|
|
||||||
调用方式:使用 `execute_shell_command` 执行 MySQL MCP 查询,或直接使用 MCP 工具(如可用)。
|
```json
|
||||||
|
{
|
||||||
如果 MCP 工具不可用,引导用户参考 `references/setup.md` 完成配置。
|
"tasks": [
|
||||||
|
"买菜",
|
||||||
## 表结构
|
"写周报",
|
||||||
|
"锻炼30分钟"
|
||||||
```sql
|
]
|
||||||
CREATE TABLE IF NOT EXISTS todos (
|
}
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
title VARCHAR(500) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
status ENUM('pending', 'in_progress', 'done', 'archived') NOT NULL DEFAULT 'pending',
|
|
||||||
priority ENUM('low', 'medium', 'high', 'urgent') NOT NULL DEFAULT 'medium',
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
last_accessed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
access_count INT UNSIGNED NOT NULL DEFAULT 1
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 任务为纯字符串数组,无 ID、无状态、无优先级、无时间戳
|
||||||
|
|
||||||
|
## 校验脚本
|
||||||
|
|
||||||
|
每次读写 JSON 前后,使用 `<skill_dir>/scripts/validate.py` 校验数据完整性。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 校验数据文件
|
||||||
|
python3 <skill_dir>/scripts/validate.py
|
||||||
|
|
||||||
|
# 校验指定文件
|
||||||
|
python3 <skill_dir>/scripts/validate.py <文件路径>
|
||||||
|
```
|
||||||
|
|
||||||
|
校验内容:JSON 格式合法性、结构合规(tasks 数组 + 字符串元素)、任务去重(大小写不敏感)。
|
||||||
|
|
||||||
|
退出码 0 = 通过,1 = 失败。失败时 stderr 输出具体错误。
|
||||||
|
|
||||||
## 操作指南
|
## 操作指南
|
||||||
|
|
||||||
### 添加任务
|
### 添加任务
|
||||||
|
|
||||||
触发词:添加任务 / 新建待办 / 提醒我做 / 记一下
|
触发词:添加任务 / 新建待办 / 提醒我做 / 记一下 / add
|
||||||
|
|
||||||
```sql
|
流程:
|
||||||
INSERT INTO todos (title, description, priority) VALUES ('<标题>', '<描述>', '<优先级>');
|
1. 确认数据文件存在,不存在则创建 `{"tasks": []}`
|
||||||
|
2. 运行 `validate.py` 校验当前数据
|
||||||
|
3. 将新任务追加到 `tasks` 数组
|
||||||
|
4. 运行 `validate.py` 校验写入后的数据(去重检查)
|
||||||
|
5. 校验失败则回滚,输出错误信息
|
||||||
|
|
||||||
|
### 查看任务
|
||||||
|
|
||||||
|
触发词:我的待办 / 有什么任务 / 今天要做什么 / 列出任务 / list
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 运行 `validate.py` 校验数据
|
||||||
|
2. 读取并展示所有任务,带序号
|
||||||
|
|
||||||
|
输出格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
📋 任务列表
|
||||||
|
1. 买菜
|
||||||
|
2. 写周报
|
||||||
|
3. 锻炼30分钟
|
||||||
```
|
```
|
||||||
|
|
||||||
- 优先级默认 `medium`
|
### 完成任务(移除)
|
||||||
- 用户说"紧急/urgent/马上/ASAP" → `urgent`
|
|
||||||
- 用户说"重要" → `high`
|
|
||||||
- 用户说"不急/有空/low" → `low`
|
|
||||||
- 添加后执行 LRU 检查(见下方淘汰策略)
|
|
||||||
|
|
||||||
### 查看待办
|
触发词:完成 XXX / 做完了 / 搞定 / done
|
||||||
|
|
||||||
触发词:我的待办 / 有什么任务 / 今天要做什么 / 列出任务
|
流程:
|
||||||
|
1. 运行 `validate.py` 校验数据
|
||||||
```sql
|
2. 按序号或模糊匹配找到目标任务
|
||||||
SELECT id, title, status, priority, DATE_FORMAT(updated_at, '%m-%d') AS updated
|
3. 从 `tasks` 数组中移除
|
||||||
FROM todos
|
4. 运行 `validate.py` 校验写入后的数据
|
||||||
WHERE status IN ('pending', 'in_progress')
|
5. 校验失败则回滚,输出错误信息
|
||||||
ORDER BY
|
|
||||||
FIELD(priority, 'urgent', 'high', 'medium', 'low'),
|
|
||||||
last_accessed_at DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
查看后刷新访问时间:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE todos SET last_accessed_at = NOW(), access_count = access_count + 1
|
|
||||||
WHERE id IN (<id 列表>);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查看已完成
|
|
||||||
|
|
||||||
触发词:已完成的任务 / 做完了哪些
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT id, title, priority, DATE_FORMAT(updated_at, '%m-%d %H:%i') AS completed
|
|
||||||
FROM todos WHERE status = 'done'
|
|
||||||
ORDER BY updated_at DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 标记完成
|
|
||||||
|
|
||||||
触发词:完成 XXX / 标记为完成 / 做完了
|
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE todos SET status = 'done', last_accessed_at = NOW(), access_count = access_count + 1
|
|
||||||
WHERE id = <id>;
|
|
||||||
```
|
|
||||||
|
|
||||||
按标题匹配:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE todos SET status = 'done', last_accessed_at = NOW(), access_count = access_count + 1
|
|
||||||
WHERE title LIKE '%<关键词>%' AND status != 'done';
|
|
||||||
```
|
|
||||||
|
|
||||||
- 匹配多条时列出匹配项让用户确认
|
- 匹配多条时列出匹配项让用户确认
|
||||||
- 用 `SELECT` 先查再改,避免误操作
|
- 完成即移除,不做归档
|
||||||
|
|
||||||
### 标记进行中
|
## 参数歧义处理
|
||||||
|
|
||||||
触发词:开始做 XXX / 进行中
|
以下情况必须与用户确认,不可自行猜测:
|
||||||
|
|
||||||
|
- **无参数调用**:`/todo` 不带任何参数 → 询问用户意图(查看 / 添加 / 完成)
|
||||||
|
- **仅操作词无目标**:`/todo done` 未指定具体任务 → 列出任务列表让用户选择
|
||||||
|
- **语义模糊**:`/todo 买菜` 不明确是「添加」还是「完成」→ 默认理解为添加,但需确认
|
||||||
|
- **模糊匹配多条**:关键词匹配到多个任务 → 列出候选项让用户确认
|
||||||
|
|
||||||
|
确认时使用简洁的提问,例如:
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE todos SET status = 'in_progress', last_accessed_at = NOW(), access_count = access_count + 1
|
|
||||||
WHERE id = <id>;
|
|
||||||
```
|
```
|
||||||
|
你想对「买菜」做什么?
|
||||||
### 更新任务
|
1. 添加为新任务
|
||||||
|
2. 标记完成(移除)
|
||||||
触发词:修改 XXX 的描述/优先级/标题
|
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE todos SET <字段> = '<新值>', last_accessed_at = NOW(), access_count = access_count + 1
|
|
||||||
WHERE id = <id>;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
可更新字段:`title`、`description`、`priority`、`status`
|
|
||||||
|
|
||||||
### 删除任务
|
|
||||||
|
|
||||||
触发词:删除任务 / 去掉这个待办
|
|
||||||
|
|
||||||
**软删除(推荐)**:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
UPDATE todos SET status = 'archived' WHERE id = <id>;
|
|
||||||
```
|
|
||||||
|
|
||||||
**硬删除**(用户明确要求时):
|
|
||||||
|
|
||||||
```sql
|
|
||||||
DELETE FROM todos WHERE id = <id>;
|
|
||||||
```
|
|
||||||
|
|
||||||
- 硬删除前必须列出将被删除的事项并确认
|
|
||||||
- 默认用软删除
|
|
||||||
|
|
||||||
### 搜索任务
|
|
||||||
|
|
||||||
触发词:找一下关于 XXX 的任务 / 有没有 XXX
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT id, title, status, priority
|
|
||||||
FROM todos
|
|
||||||
WHERE title LIKE '%<关键词>%' OR description LIKE '%<关键词>%'
|
|
||||||
ORDER BY FIELD(status, 'in_progress', 'pending', 'done', 'archived'), priority;
|
|
||||||
```
|
|
||||||
|
|
||||||
## LRU 淘汰策略
|
|
||||||
|
|
||||||
每次 INSERT 后检查总数:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT COUNT(*) AS total FROM todos WHERE status != 'archived';
|
|
||||||
```
|
|
||||||
|
|
||||||
如果超过 100 条,执行归档:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 1. 归档最久未访问的已完成事项(最旧 10 条)
|
|
||||||
UPDATE todos SET status = 'archived'
|
|
||||||
WHERE status = 'done'
|
|
||||||
ORDER BY last_accessed_at ASC LIMIT 10;
|
|
||||||
|
|
||||||
-- 2. 仍超标则归档最久未访问的待办事项
|
|
||||||
UPDATE todos SET status = 'archived'
|
|
||||||
WHERE status = 'pending'
|
|
||||||
ORDER BY last_accessed_at ASC LIMIT 10;
|
|
||||||
```
|
|
||||||
|
|
||||||
**保护规则**:不归档 `in_progress` 状态的事项。
|
|
||||||
|
|
||||||
## 交互原则
|
## 交互原则
|
||||||
|
|
||||||
- **模糊匹配时确认**:关键词匹配多条任务,列出选项让用户选
|
- **确认优先**:参数歧义时绝不猜测,主动询问
|
||||||
- **操作前预览**:删除/归档前,先 `SELECT` 显示受影响的事项
|
- **操作前校验**:每次读写前后运行 validate.py
|
||||||
- **自然语言理解**:用户说"帮我记一下明天要买菜" → 添加任务"买菜"
|
- **失败回滚**:校验失败时恢复原数据,告知用户错误原因
|
||||||
- **状态转换合理**:不允许 archived → in_progress 等不合理跳转
|
- **简洁输出**:操作结果用 ✓ 前缀确认,如 `✓ 已添加任务:买菜`
|
||||||
|
|
||||||
## 输出格式
|
|
||||||
|
|
||||||
查询结果用表格展示:
|
|
||||||
|
|
||||||
```
|
|
||||||
| ID | 标题 | 状态 | 优先级 | 更新时间 |
|
|
||||||
|----|------|------|--------|----------|
|
|
||||||
| 1 | xxx | 待办 | 高 | 08-31 |
|
|
||||||
```
|
|
||||||
|
|
||||||
操作结果用简洁确认:
|
|
||||||
- ✓ 已添加任务:XXX
|
|
||||||
- ✓ 已完成任务:XXX
|
|
||||||
- ✓ 已归档 5 条旧任务
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
# Todo Skill 环境配置指南
|
|
||||||
|
|
||||||
## 1. 安装 MySQL MCP Server
|
|
||||||
|
|
||||||
推荐使用 `@benborla29/mcp-server-mysql`(支持读写操作)。
|
|
||||||
|
|
||||||
### QwenPaw 配置方式
|
|
||||||
|
|
||||||
在 `~/.qwenpaw/config.json` 的 `mcp.clients` 中添加:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"mysql": {
|
|
||||||
"name": "mysql",
|
|
||||||
"description": "MySQL MCP for todo skill",
|
|
||||||
"enabled": true,
|
|
||||||
"transport": "stdio",
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "@benborla29/mcp-server-mysql"],
|
|
||||||
"env": {
|
|
||||||
"MYSQL_HOST": "<主机地址>",
|
|
||||||
"MYSQL_PORT": "<端口,默认 3306>",
|
|
||||||
"MYSQL_USER": "<用户名>",
|
|
||||||
"MYSQL_PASS": "<密码>",
|
|
||||||
"MYSQL_DB": "<数据库名>",
|
|
||||||
"ALLOW_INSERT_OPERATION": "true",
|
|
||||||
"ALLOW_UPDATE_OPERATION": "true",
|
|
||||||
"ALLOW_DELETE_OPERATION": "true",
|
|
||||||
"SCHEMA_DDL_PERMISSIONS": "<数据库名>:true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **注意**:环境变量名是 `MYSQL_PASS`(不是 `MYSQL_PASSWORD`)和 `MYSQL_DB`(不是 `MYSQL_DATABASE`)。
|
|
||||||
|
|
||||||
### Claude Code 配置方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
claude mcp add mysql \
|
|
||||||
-e MYSQL_HOST="<主机地址>" \
|
|
||||||
-e MYSQL_PORT="<端口>" \
|
|
||||||
-e MYSQL_USER="<用户名>" \
|
|
||||||
-e MYSQL_PASS="<密码>" \
|
|
||||||
-e MYSQL_DB="<数据库名>" \
|
|
||||||
-e ALLOW_INSERT_OPERATION="true" \
|
|
||||||
-e ALLOW_UPDATE_OPERATION="true" \
|
|
||||||
-e ALLOW_DELETE_OPERATION="true" \
|
|
||||||
-e SCHEMA_DDL_PERMISSIONS="<数据库名>:true" \
|
|
||||||
-- npx -y @benborla29/mcp-server-mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 验证 MCP 可用
|
|
||||||
|
|
||||||
配置完成后重启客户端,然后尝试:
|
|
||||||
- 列出数据库中的表
|
|
||||||
- 执行一条简单查询
|
|
||||||
|
|
||||||
如果 MCP 工具可用,会看到 `mysql_query` 工具。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 创建 todos 表
|
|
||||||
|
|
||||||
在目标数据库中执行以下 SQL:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS todos (
|
|
||||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
title VARCHAR(500) NOT NULL COMMENT '任务标题',
|
|
||||||
description TEXT COMMENT '任务描述',
|
|
||||||
status ENUM('pending', 'in_progress', 'done', 'archived') NOT NULL DEFAULT 'pending' COMMENT '状态: pending-待办, in_progress-进行中, done-已完成, archived-已归档',
|
|
||||||
priority ENUM('low', 'medium', 'high', 'urgent') NOT NULL DEFAULT 'medium' COMMENT '优先级: low-低, medium-中, high-高, urgent-紧急',
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
|
|
||||||
last_accessed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后访问时间(LRU 淘汰依据)',
|
|
||||||
access_count INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '访问次数',
|
|
||||||
INDEX idx_status (status),
|
|
||||||
INDEX idx_lru (last_accessed_at),
|
|
||||||
INDEX idx_priority_status (priority, status),
|
|
||||||
INDEX idx_created (created_at)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='待办事项表,支持 LRU 淘汰策略';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字段说明
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| id | BIGINT UNSIGNED | 自增主键 |
|
|
||||||
| title | VARCHAR(500) | 任务标题,必填 |
|
|
||||||
| description | TEXT | 任务描述,可选 |
|
|
||||||
| status | ENUM | pending / in_progress / done / archived |
|
|
||||||
| priority | ENUM | low / medium / high / urgent |
|
|
||||||
| created_at | DATETIME | 创建时间,自动填充 |
|
|
||||||
| updated_at | DATETIME | 更新时间,自动刷新 |
|
|
||||||
| last_accessed_at | DATETIME | 最后访问时间,LRU 依据 |
|
|
||||||
| access_count | INT UNSIGNED | 访问计数,每次 +1 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. LRU 淘汰策略
|
|
||||||
|
|
||||||
基于 `last_accessed_at` 字段实现 LRU(Least Recently Used)淘汰:
|
|
||||||
|
|
||||||
- 每次查看、更新 todo 时刷新 `last_accessed_at` 和 `access_count`
|
|
||||||
- 待办总数超过 100 条时自动归档
|
|
||||||
- 先归档最久未访问的 `done` 事项,再归档 `pending` 事项
|
|
||||||
- 不归档 `in_progress` 状态的事项
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tasks": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["tasks"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""校验 ~/.local/share/todo/tasks.json 的格式、结构和完整性。
|
||||||
|
|
||||||
|
退出码:
|
||||||
|
0 - 校验通过
|
||||||
|
1 - 校验失败(错误信息输出到 stderr)
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python3 validate.py [文件路径]
|
||||||
|
默认路径: ~/.local/share/todo/tasks.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_PATH = Path.home() / ".local" / "share" / "todo" / "tasks.json"
|
||||||
|
SCHEMA_PATH = Path(__file__).parent.parent / "schema" / "tasks.schema.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path) -> tuple[dict | None, str | None]:
|
||||||
|
"""加载并解析 JSON 文件。"""
|
||||||
|
if not path.exists():
|
||||||
|
return None, f"文件不存在: {path}"
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data, None
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return None, f"JSON 格式错误: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema() -> tuple[dict | None, str | None]:
|
||||||
|
"""加载 JSON Schema。"""
|
||||||
|
if not SCHEMA_PATH.exists():
|
||||||
|
return None, f"Schema 文件不存在: {SCHEMA_PATH}"
|
||||||
|
try:
|
||||||
|
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f), None
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
return None, f"Schema 格式错误: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_structure(data: dict) -> list[str]:
|
||||||
|
"""校验数据结构。"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
errors.append("顶层必须是对象")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
if "tasks" not in data:
|
||||||
|
errors.append("缺少必需字段 'tasks'")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
tasks = data["tasks"]
|
||||||
|
if not isinstance(tasks, list):
|
||||||
|
errors.append("'tasks' 必须是数组")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
for i, item in enumerate(tasks):
|
||||||
|
if not isinstance(item, str):
|
||||||
|
errors.append(f"tasks[{i}] 必须是字符串,实际类型: {type(item).__name__}")
|
||||||
|
elif len(item.strip()) == 0:
|
||||||
|
errors.append(f"tasks[{i}] 不能为空字符串")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_duplicates(tasks: list[str]) -> list[str]:
|
||||||
|
"""校验任务是否有重复(大小写不敏感)。"""
|
||||||
|
errors = []
|
||||||
|
seen = {}
|
||||||
|
for i, task in enumerate(tasks):
|
||||||
|
key = task.strip().lower()
|
||||||
|
if key in seen:
|
||||||
|
errors.append(f"tasks[{i}] '{task}' 与 tasks[{seen[key]}] 重复")
|
||||||
|
else:
|
||||||
|
seen[key] = i
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def validate_schema(data: dict) -> list[str]:
|
||||||
|
"""使用 JSON Schema 校验(如果 jsonschema 可用)。"""
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
except ImportError:
|
||||||
|
# jsonschema 未安装,跳过 schema 校验
|
||||||
|
return []
|
||||||
|
|
||||||
|
schema, err = load_schema()
|
||||||
|
if err:
|
||||||
|
return [f"Schema 校验跳过: {err}"]
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
try:
|
||||||
|
jsonschema.validate(instance=data, schema=schema)
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
errors.append(f"Schema 校验失败: {e.message}")
|
||||||
|
except jsonschema.SchemaError as e:
|
||||||
|
errors.append(f"Schema 定义错误: {e.message}")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PATH
|
||||||
|
|
||||||
|
# 1. JSON 格式校验
|
||||||
|
data, err = load_json(path)
|
||||||
|
if err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 2. 结构校验
|
||||||
|
errors = validate_structure(data)
|
||||||
|
if errors:
|
||||||
|
for e in errors:
|
||||||
|
print(f"结构错误: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 3. JSON Schema 校验
|
||||||
|
schema_errors = validate_schema(data)
|
||||||
|
if schema_errors:
|
||||||
|
for e in schema_errors:
|
||||||
|
print(f"Schema 错误: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# 4. 去重校验
|
||||||
|
dup_errors = validate_duplicates(data["tasks"])
|
||||||
|
if dup_errors:
|
||||||
|
for e in dup_errors:
|
||||||
|
print(f"重复错误: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"校验通过: {path} ({len(data['tasks'])} 个任务)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user