Files
xian-favor/xian_favor/cli.py

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()