430 lines
14 KiB
Python
430 lines
14 KiB
Python
"""命令行工具"""
|
|
|
|
import argparse
|
|
import sys
|
|
import json
|
|
from datetime import datetime
|
|
from typing import List
|
|
|
|
from .db import db
|
|
from .config import ITEM_TYPES, TODO_STATUS, PRIORITY_LEVELS
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Xian Favor - 收藏关注系统",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
示例:
|
|
xian_favor add text "这是我的笔记" -t "笔记,重要"
|
|
xian_favor add link "https://example.com" --title "示例网站" -t "技术"
|
|
xian_favor add todo "完成项目" -p high -d "2024-12-31" -t "工作"
|
|
xian_favor add column "https://column.example.com/feed" --title "技术专栏" -t "RSS"
|
|
xian_favor list
|
|
xian_favor list --type todo --status pending
|
|
xian_favor search "关键词"
|
|
xian_favor show 1
|
|
xian_favor edit 1 --status completed --note "已完成"
|
|
xian_favor done 1
|
|
xian_favor delete 1
|
|
xian_favor tags
|
|
xian_favor stats
|
|
"""
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="command", help="命令")
|
|
|
|
# ============ add 命令 ============
|
|
add_parser = subparsers.add_parser("add", help="添加新条目")
|
|
add_parser.add_argument("type", choices=ITEM_TYPES, help="类型: text/link/column/todo")
|
|
add_parser.add_argument("content", help="内容或URL")
|
|
add_parser.add_argument("--title", "-T", help="标题")
|
|
add_parser.add_argument("--url", "-u", help="URL (link/column类型)")
|
|
add_parser.add_argument("--source", "-s", help="来源")
|
|
add_parser.add_argument("--status", choices=TODO_STATUS, default="pending", help="状态 (todo)")
|
|
add_parser.add_argument("--priority", "-p", choices=PRIORITY_LEVELS, default="medium", help="优先级")
|
|
add_parser.add_argument("--due-date", "-d", help="截止日期 (YYYY-MM-DD)")
|
|
add_parser.add_argument("--note", "-n", help="备注")
|
|
add_parser.add_argument("--tags", "-t", help="标签 (逗号分隔)")
|
|
|
|
# ============ list 命令 ============
|
|
list_parser = subparsers.add_parser("list", help="列出条目")
|
|
list_parser.add_argument("--type", choices=ITEM_TYPES, help="类型过滤")
|
|
list_parser.add_argument("--status", choices=TODO_STATUS, help="状态过滤")
|
|
list_parser.add_argument("--tag", help="标签过滤")
|
|
list_parser.add_argument("--limit", "-l", type=int, default=20, help="数量限制")
|
|
list_parser.add_argument("--json", "-j", action="store_true", help="JSON输出")
|
|
|
|
# ============ show 命令 ============
|
|
show_parser = subparsers.add_parser("show", help="查看详情")
|
|
show_parser.add_argument("id", type=int, help="条目ID")
|
|
show_parser.add_argument("--json", "-j", action="store_true", help="JSON输出")
|
|
|
|
# ============ edit 命令 ============
|
|
edit_parser = subparsers.add_parser("edit", help="编辑条目")
|
|
edit_parser.add_argument("id", type=int, help="条目ID")
|
|
edit_parser.add_argument("--title", "-T", help="标题")
|
|
edit_parser.add_argument("--content", "-c", help="内容")
|
|
edit_parser.add_argument("--url", "-u", help="URL")
|
|
edit_parser.add_argument("--source", "-s", help="来源")
|
|
edit_parser.add_argument("--status", choices=TODO_STATUS, help="状态")
|
|
edit_parser.add_argument("--priority", "-p", choices=PRIORITY_LEVELS, help="优先级")
|
|
edit_parser.add_argument("--due-date", "-d", help="截止日期")
|
|
edit_parser.add_argument("--note", "-n", help="备注")
|
|
edit_parser.add_argument("--tags", "-t", help="标签 (逗号分隔, 覆盖)")
|
|
|
|
# ============ done 命令 ============
|
|
done_parser = subparsers.add_parser("done", help="完成待办")
|
|
done_parser.add_argument("id", type=int, help="条目ID")
|
|
|
|
# ============ delete 命令 ============
|
|
delete_parser = subparsers.add_parser("delete", help="删除条目")
|
|
delete_parser.add_argument("id", type=int, help="条目ID")
|
|
delete_parser.add_argument("-f", "--force", action="store_true", help="强制删除不确认")
|
|
|
|
# ============ search 命令 ============
|
|
search_parser = subparsers.add_parser("search", help="搜索条目")
|
|
search_parser.add_argument("keyword", help="关键词")
|
|
search_parser.add_argument("--type", choices=ITEM_TYPES, help="类型过滤")
|
|
search_parser.add_argument("--limit", "-l", type=int, default=20, help="数量限制")
|
|
search_parser.add_argument("--json", "-j", action="store_true", help="JSON输出")
|
|
|
|
# ============ tags 命令 ============
|
|
tags_parser = subparsers.add_parser("tags", help="标签管理")
|
|
tags_parser.add_argument("--delete", "-d", help="删除标签")
|
|
|
|
# ============ stats 命令 ============
|
|
subparsers.add_parser("stats", help="统计信息")
|
|
|
|
# ============ serve 命令 ============
|
|
serve_parser = subparsers.add_parser("serve", help="启动API服务")
|
|
serve_parser.add_argument("--host", default="0.0.0.0", help="主机")
|
|
serve_parser.add_argument("--port", type=int, default=19014, help="端口")
|
|
|
|
# 解析参数
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
return
|
|
|
|
# 执行命令
|
|
try:
|
|
if args.command == "add":
|
|
cmd_add(args)
|
|
elif args.command == "list":
|
|
cmd_list(args)
|
|
elif args.command == "show":
|
|
cmd_show(args)
|
|
elif args.command == "edit":
|
|
cmd_edit(args)
|
|
elif args.command == "done":
|
|
cmd_done(args)
|
|
elif args.command == "delete":
|
|
cmd_delete(args)
|
|
elif args.command == "search":
|
|
cmd_search(args)
|
|
elif args.command == "tags":
|
|
cmd_tags(args)
|
|
elif args.command == "stats":
|
|
cmd_stats(args)
|
|
elif args.command == "serve":
|
|
cmd_serve(args)
|
|
except Exception as e:
|
|
print(f"❌ 错误: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
# ============ 命令实现 ============
|
|
|
|
def parse_tags(tags_str: str) -> List[str]:
|
|
"""解析标签字符串"""
|
|
if not tags_str:
|
|
return []
|
|
return [t.strip() for t in tags_str.split(",") if t.strip()]
|
|
|
|
|
|
def format_item(item: dict, brief: bool = True) -> str:
|
|
"""格式化条目显示"""
|
|
type_icons = {
|
|
"text": "📝",
|
|
"link": "🔗",
|
|
"column": "📰",
|
|
"todo": "✅"
|
|
}
|
|
status_icons = {
|
|
"pending": "⏳",
|
|
"in_progress": "🔄",
|
|
"completed": "✅"
|
|
}
|
|
priority_icons = {
|
|
"low": "🟢",
|
|
"medium": "🟡",
|
|
"high": "🟠",
|
|
"urgent": "🔴"
|
|
}
|
|
|
|
icon = type_icons.get(item['type'], "📄")
|
|
status = status_icons.get(item.get('status', ''), '')
|
|
priority = priority_icons.get(item.get('priority', ''), '')
|
|
|
|
title = item.get('title') or item.get('content', '')[:50]
|
|
tags_str = f" [{', '.join(item['tags'])}]" if item.get('tags') else ""
|
|
|
|
if brief:
|
|
return f" {icon} [{item['id']}] {title}{tags_str}"
|
|
else:
|
|
lines = [
|
|
f"{icon} [{item['id']}] {title}",
|
|
f" 类型: {item['type']}",
|
|
]
|
|
if item.get('content'):
|
|
lines.append(f" 内容: {item['content'][:200]}")
|
|
if item.get('url'):
|
|
lines.append(f" URL: {item['url']}")
|
|
if item.get('source'):
|
|
lines.append(f" 来源: {item['source']}")
|
|
if item['type'] == 'todo':
|
|
lines.append(f" 状态: {status} {item.get('status', 'pending')}")
|
|
lines.append(f" 优先级: {priority} {item.get('priority', 'medium')}")
|
|
if item.get('due_date'):
|
|
lines.append(f" 截止: {item['due_date']}")
|
|
if item.get('note'):
|
|
lines.append(f" 备注: {item['note']}")
|
|
if item.get('tags'):
|
|
lines.append(f" 标签: {', '.join(item['tags'])}")
|
|
lines.append(f" 创建: {item['created_at'][:19]}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def cmd_add(args):
|
|
"""添加条目"""
|
|
tags = parse_tags(args.tags)
|
|
|
|
# 根据类型处理内容
|
|
content = args.content
|
|
url = args.url
|
|
|
|
if args.type == "link":
|
|
url = url or args.content
|
|
content = None
|
|
elif args.type == "column":
|
|
url = url or args.content
|
|
content = None
|
|
|
|
item_id = db.create_item(
|
|
type=args.type,
|
|
title=args.title,
|
|
content=content,
|
|
url=url,
|
|
source=args.source,
|
|
status=args.status,
|
|
priority=args.priority,
|
|
due_date=args.due_date,
|
|
note=args.note,
|
|
tags=tags
|
|
)
|
|
|
|
item = db.get_item(item_id)
|
|
print(f"✅ 创建成功 (ID: {item_id})")
|
|
print(format_item(item, brief=False))
|
|
|
|
|
|
def cmd_list(args):
|
|
"""列出条目"""
|
|
items = db.list_items(
|
|
type=args.type,
|
|
status=args.status,
|
|
tag=args.tag,
|
|
limit=args.limit
|
|
)
|
|
|
|
if args.json:
|
|
print(json.dumps(items, ensure_ascii=False, indent=2))
|
|
return
|
|
|
|
if not items:
|
|
print("📭 没有找到条目")
|
|
return
|
|
|
|
type_labels = {"text": "文本", "link": "链接", "column": "专栏", "todo": "待办"}
|
|
filter_desc = []
|
|
if args.type:
|
|
filter_desc.append(f"类型: {type_labels.get(args.type, args.type)}")
|
|
if args.status:
|
|
filter_desc.append(f"状态: {args.status}")
|
|
if args.tag:
|
|
filter_desc.append(f"标签: {args.tag}")
|
|
|
|
if filter_desc:
|
|
print(f"📋 筛选: {' | '.join(filter_desc)}")
|
|
else:
|
|
print(f"📋 全部条目 ({len(items)} 条)")
|
|
print("-" * 50)
|
|
|
|
for item in items:
|
|
print(format_item(item))
|
|
|
|
|
|
def cmd_show(args):
|
|
"""查看详情"""
|
|
item = db.get_item(args.id)
|
|
|
|
if not item:
|
|
print(f"❌ 条目不存在: {args.id}")
|
|
return
|
|
|
|
if args.json:
|
|
print(json.dumps(item, ensure_ascii=False, indent=2))
|
|
return
|
|
|
|
print(format_item(item, brief=False))
|
|
|
|
|
|
def cmd_edit(args):
|
|
"""编辑条目"""
|
|
update_data = {}
|
|
|
|
if args.title is not None:
|
|
update_data['title'] = args.title
|
|
if args.content is not None:
|
|
update_data['content'] = args.content
|
|
if args.url is not None:
|
|
update_data['url'] = args.url
|
|
if args.source is not None:
|
|
update_data['source'] = args.source
|
|
if args.status is not None:
|
|
update_data['status'] = args.status
|
|
if args.priority is not None:
|
|
update_data['priority'] = args.priority
|
|
if args.due_date is not None:
|
|
update_data['due_date'] = args.due_date
|
|
if args.note is not None:
|
|
update_data['note'] = args.note
|
|
if args.tags is not None:
|
|
update_data['tags'] = parse_tags(args.tags)
|
|
|
|
if not update_data:
|
|
print("❌ 没有指定要更新的字段")
|
|
return
|
|
|
|
if db.update_item(args.id, **update_data):
|
|
item = db.get_item(args.id)
|
|
print(f"✅ 更新成功 (ID: {args.id})")
|
|
print(format_item(item, brief=False))
|
|
else:
|
|
print(f"❌ 更新失败: 条目不存在或没有变化")
|
|
|
|
|
|
def cmd_done(args):
|
|
"""完成待办"""
|
|
item = db.get_item(args.id)
|
|
if not item:
|
|
print(f"❌ 条目不存在: {args.id}")
|
|
return
|
|
|
|
if item['type'] != 'todo':
|
|
print(f"❌ 不是待办事项: {args.id}")
|
|
return
|
|
|
|
if db.update_item(args.id, status='completed'):
|
|
print(f"✅ 已完成待办 (ID: {args.id})")
|
|
else:
|
|
print(f"❌ 操作失败")
|
|
|
|
|
|
def cmd_delete(args):
|
|
"""删除条目"""
|
|
if not args.force:
|
|
item = db.get_item(args.id)
|
|
if not item:
|
|
print(f"❌ 条目不存在: {args.id}")
|
|
return
|
|
|
|
print(format_item(item, brief=False))
|
|
confirm = input("确认删除? [y/N] ")
|
|
if confirm.lower() != 'y':
|
|
print("❌ 取消删除")
|
|
return
|
|
|
|
if db.delete_item(args.id):
|
|
print(f"✅ 已删除 (ID: {args.id})")
|
|
else:
|
|
print(f"❌ 删除失败")
|
|
|
|
|
|
def cmd_search(args):
|
|
"""搜索条目"""
|
|
items = db.list_items(
|
|
type=args.type,
|
|
keyword=args.keyword,
|
|
limit=args.limit
|
|
)
|
|
|
|
if args.json:
|
|
print(json.dumps(items, ensure_ascii=False, indent=2))
|
|
return
|
|
|
|
if not items:
|
|
print(f"🔍 没有找到匹配 '{args.keyword}' 的条目")
|
|
return
|
|
|
|
print(f"🔍 搜索 '{args.keyword}' ({len(items)} 条)")
|
|
print("-" * 50)
|
|
|
|
for item in items:
|
|
print(format_item(item))
|
|
|
|
|
|
def cmd_tags(args):
|
|
"""标签管理"""
|
|
if args.delete:
|
|
if db.delete_tag(name=args.delete):
|
|
print(f"✅ 已删除标签: {args.delete}")
|
|
else:
|
|
print(f"❌ 标签不存在: {args.delete}")
|
|
return
|
|
|
|
tags = db.list_tags()
|
|
if not tags:
|
|
print("🏷️ 没有标签")
|
|
return
|
|
|
|
print(f"🏷️ 标签列表 ({len(tags)} 个)")
|
|
print("-" * 50)
|
|
for tag in tags:
|
|
print(f" • {tag['name']} ({tag['item_count']} 条)")
|
|
|
|
|
|
def cmd_stats(args):
|
|
"""统计信息"""
|
|
stats = db.stats()
|
|
|
|
print("📊 收藏统计")
|
|
print("-" * 30)
|
|
print(f"总条目: {stats['total']}")
|
|
|
|
if stats.get('by_type'):
|
|
print("\n按类型:")
|
|
type_labels = {"text": "文本", "link": "链接", "column": "专栏", "todo": "待办"}
|
|
for t, count in stats['by_type'].items():
|
|
print(f" • {type_labels.get(t, t)}: {count}")
|
|
|
|
if stats.get('todo_status'):
|
|
print("\n待办状态:")
|
|
status_labels = {"pending": "待处理", "in_progress": "进行中", "completed": "已完成"}
|
|
for s, count in stats['todo_status'].items():
|
|
print(f" • {status_labels.get(s, s)}: {count}")
|
|
|
|
print(f"\n标签数: {stats['tags']}")
|
|
|
|
|
|
def cmd_serve(args):
|
|
"""启动API服务"""
|
|
from .api import start_server
|
|
print(f"🚀 启动API服务: http://{args.host}:{args.port}")
|
|
start_server(host=args.host, port=args.port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |