feat: 收藏关注系统 v1.0.0 - 支持CLI/API/Web三种操作模式
This commit is contained in:
430
build/lib/xian_favor/cli.py
Normal file
430
build/lib/xian_favor/cli.py
Normal file
@@ -0,0 +1,430 @@
|
||||
"""命令行工具"""
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user