weflow-cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- package/src/utils/talkerUtils.ts +84 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Extract WeChat 3.x database encryption key from process memory."""
|
|
2
|
+
import sys, json, os
|
|
3
|
+
|
|
4
|
+
def extract_3x_key():
|
|
5
|
+
"""Extract 3.x key and return as JSON."""
|
|
6
|
+
# Find WeChat 3.x process
|
|
7
|
+
import psutil
|
|
8
|
+
pid = None
|
|
9
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
10
|
+
if proc.info['name'] == 'WeChat.exe':
|
|
11
|
+
pid = proc.info['pid']
|
|
12
|
+
break
|
|
13
|
+
|
|
14
|
+
if not pid:
|
|
15
|
+
print(json.dumps({"success": False, "error": "WeChat 3.x (WeChat.exe) 未运行"}))
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
# Get exe path
|
|
19
|
+
try:
|
|
20
|
+
exe_path = psutil.Process(pid).exe()
|
|
21
|
+
except:
|
|
22
|
+
exe_path = ""
|
|
23
|
+
|
|
24
|
+
# Get wx_dir from registry
|
|
25
|
+
from pywxdump.wx_core.wx_info import get_wx_dir_by_reg, get_info_wxid
|
|
26
|
+
import ctypes
|
|
27
|
+
from ctypes import wintypes
|
|
28
|
+
|
|
29
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
30
|
+
PROCESS_VM_READ = 0x0010
|
|
31
|
+
kernel32 = ctypes.windll.kernel32
|
|
32
|
+
hProcess = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
|
|
33
|
+
|
|
34
|
+
wxid = None
|
|
35
|
+
wx_dir = None
|
|
36
|
+
if hProcess:
|
|
37
|
+
try:
|
|
38
|
+
wxid = get_info_wxid(hProcess)
|
|
39
|
+
except:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
if wxid:
|
|
43
|
+
wx_dir = get_wx_dir_by_reg(wxid)
|
|
44
|
+
|
|
45
|
+
if not wx_dir:
|
|
46
|
+
wx_dir = get_wx_dir_by_reg("all")
|
|
47
|
+
|
|
48
|
+
# Get msg_dir (parent of wx_dir)
|
|
49
|
+
if wx_dir and wxid:
|
|
50
|
+
msg_dir = os.path.join(wx_dir, wxid, "Msg")
|
|
51
|
+
elif wx_dir:
|
|
52
|
+
msg_dir = os.path.join(wx_dir, "Msg")
|
|
53
|
+
else:
|
|
54
|
+
msg_dir = None
|
|
55
|
+
|
|
56
|
+
if not msg_dir or not os.path.exists(msg_dir):
|
|
57
|
+
if hProcess:
|
|
58
|
+
kernel32.CloseHandle(hProcess)
|
|
59
|
+
print(json.dumps({"success": False, "error": f"未找到 3.x 数据目录: {msg_dir}"}))
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# Extract key using memory search
|
|
63
|
+
from pywxdump.wx_core.wx_info import get_key_by_mem_search
|
|
64
|
+
key = get_key_by_mem_search(pid, msg_dir, 8)
|
|
65
|
+
|
|
66
|
+
if hProcess:
|
|
67
|
+
kernel32.CloseHandle(hProcess)
|
|
68
|
+
|
|
69
|
+
if key:
|
|
70
|
+
result = {
|
|
71
|
+
"success": True,
|
|
72
|
+
"key": key,
|
|
73
|
+
"wxid": wxid,
|
|
74
|
+
"wx_dir": wx_dir,
|
|
75
|
+
"msg_dir": msg_dir,
|
|
76
|
+
"pid": pid,
|
|
77
|
+
"exe_path": exe_path
|
|
78
|
+
}
|
|
79
|
+
else:
|
|
80
|
+
result = {"success": False, "error": "内存搜索未找到 3.x 密钥"}
|
|
81
|
+
|
|
82
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__':
|
|
86
|
+
extract_3x_key()
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
微信对话待办提取 & 任务追踪 — AI 识别承诺 + JSON 持久化。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/extract_todos.py extract [--days 7]
|
|
7
|
+
python scripts/extract_todos.py list [--status pending|done] [--urgency high|mid|low]
|
|
8
|
+
python scripts/extract_todos.py done <id>
|
|
9
|
+
python scripts/extract_todos.py undone <id>
|
|
10
|
+
python scripts/extract_todos.py rm <id>
|
|
11
|
+
python scripts/extract_todos.py remind
|
|
12
|
+
|
|
13
|
+
数据: ~/.weflow-cli/todos.json
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys, os, json, hashlib, argparse
|
|
17
|
+
from datetime import datetime, timezone, timedelta
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from sqlcipher3 import dbapi2 as sqlcipher
|
|
22
|
+
except ImportError:
|
|
23
|
+
print(json.dumps({"error": "请安装: pip install sqlcipher3"}))
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
+
from _utils import load_config, decrypt_lock, call_deepseek
|
|
28
|
+
|
|
29
|
+
TODOS_FILE = os.path.join(os.path.expanduser('~'), '.weflow-cli', 'todos.json')
|
|
30
|
+
TZ = timezone(timedelta(hours=8))
|
|
31
|
+
|
|
32
|
+
TODO_EXTRACT_PROMPT = """你是一个任务提取助手。分析微信聊天记录,提取其中出现的承诺、待办、约定事项。
|
|
33
|
+
|
|
34
|
+
要求:
|
|
35
|
+
1. 提取任何人的承诺或待办(如"我明天做""我处理""尽快发"等)
|
|
36
|
+
2. 每项返回 JSON: {{"task": "事项描述", "urgency": "高/中/低", "deadline": "截止描述或未提及"}}
|
|
37
|
+
3. 不要提取闲聊、问候等非任务内容
|
|
38
|
+
4. 如果没有待办,返回空数组 []
|
|
39
|
+
|
|
40
|
+
聊天记录:
|
|
41
|
+
{messages}
|
|
42
|
+
|
|
43
|
+
请只返回 JSON 数组,不要其他内容。"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ====== Database ======
|
|
47
|
+
|
|
48
|
+
def open_db(db_path, key_hex, salt_hex):
|
|
49
|
+
raw_key = f"x'{key_hex}{salt_hex}'"
|
|
50
|
+
conn = sqlcipher.connect(db_path)
|
|
51
|
+
c = conn.cursor()
|
|
52
|
+
c.execute(f'PRAGMA key = "{raw_key}";')
|
|
53
|
+
c.execute("SELECT count(*) FROM sqlite_master")
|
|
54
|
+
return conn
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_name_map(contact_db, contact_key, contact_salt):
|
|
58
|
+
name_map = {}
|
|
59
|
+
if not contact_db or not contact_key or not os.path.exists(contact_db):
|
|
60
|
+
return name_map
|
|
61
|
+
try:
|
|
62
|
+
conn = open_db(contact_db, contact_key, contact_salt)
|
|
63
|
+
c = conn.cursor()
|
|
64
|
+
c.execute("SELECT username, COALESCE(NULLIF(remark,''), NULLIF(nick_name,''), username) FROM contact")
|
|
65
|
+
for r in c.fetchall():
|
|
66
|
+
name_map[r[0]] = r[1]
|
|
67
|
+
conn.close()
|
|
68
|
+
except:
|
|
69
|
+
pass
|
|
70
|
+
return name_map
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def collect_recent_messages(conn, start_ts, end_ts, name_map, limit_per_session=50):
|
|
74
|
+
"""收集近期待办相关消息:扫描所有会话,过滤含承诺/任务关键词的消息。"""
|
|
75
|
+
c = conn.cursor()
|
|
76
|
+
try:
|
|
77
|
+
c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
|
|
78
|
+
sessions = [r[0] for r in c.fetchall()]
|
|
79
|
+
except:
|
|
80
|
+
c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
|
|
81
|
+
sessions = [r[0].replace('Msg_', '') for r in c.fetchall()]
|
|
82
|
+
|
|
83
|
+
# 关键词预筛选:承诺、任务、约定类词汇
|
|
84
|
+
keywords = ['明天', '下次', '等会', '一会', '尽快', '稍后', '待会', '改天',
|
|
85
|
+
'我弄', '我做', '我来', '我写', '我发', '我处理', '我整', '我搞',
|
|
86
|
+
'我看看', '我去', '我找', '我帮忙', '我帮', '好的我', '行我',
|
|
87
|
+
'没问题', 'ok', 'OK', '可以我', '收到我']
|
|
88
|
+
|
|
89
|
+
all_msgs = []
|
|
90
|
+
seen = set()
|
|
91
|
+
|
|
92
|
+
for talker in sessions[:80]:
|
|
93
|
+
tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
|
|
94
|
+
try:
|
|
95
|
+
c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
|
|
96
|
+
if c.fetchone()[0] == 0:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
c.execute(f'''
|
|
100
|
+
SELECT create_time, real_sender_id, message_content
|
|
101
|
+
FROM "{tbl}"
|
|
102
|
+
WHERE create_time >= ? AND create_time < ?
|
|
103
|
+
AND message_content IS NOT NULL
|
|
104
|
+
ORDER BY create_time DESC
|
|
105
|
+
LIMIT ?
|
|
106
|
+
''', (start_ts, end_ts, limit_per_session))
|
|
107
|
+
|
|
108
|
+
for ts, sender_id, content in c.fetchall():
|
|
109
|
+
if not content or not isinstance(content, str) or len(content) < 5:
|
|
110
|
+
continue
|
|
111
|
+
# 关键词过滤
|
|
112
|
+
content_lower = content.lower()
|
|
113
|
+
if not any(kw.lower() in content_lower for kw in keywords):
|
|
114
|
+
continue
|
|
115
|
+
# 去重
|
|
116
|
+
msg_hash = hashlib.md5(f"{talker}:{ts}:{content[:50]}".encode()).hexdigest()
|
|
117
|
+
if msg_hash in seen:
|
|
118
|
+
continue
|
|
119
|
+
seen.add(msg_hash)
|
|
120
|
+
|
|
121
|
+
# 获取发送者名称
|
|
122
|
+
try:
|
|
123
|
+
c2 = conn.cursor()
|
|
124
|
+
c2.execute("SELECT user_name FROM Name2Id WHERE rowid = ?", (sender_id,))
|
|
125
|
+
row = c2.fetchone()
|
|
126
|
+
sender_raw = row[0] if row else str(sender_id)
|
|
127
|
+
except:
|
|
128
|
+
sender_raw = str(sender_id)
|
|
129
|
+
sender_name = name_map.get(sender_raw, sender_raw)
|
|
130
|
+
talker_name = name_map.get(talker, talker)
|
|
131
|
+
|
|
132
|
+
dt = datetime.fromtimestamp(ts, tz=TZ)
|
|
133
|
+
all_msgs.append({
|
|
134
|
+
'time': dt.strftime('%m-%d %H:%M'),
|
|
135
|
+
'talker': talker_name,
|
|
136
|
+
'sender': sender_name,
|
|
137
|
+
'content': content[:300],
|
|
138
|
+
})
|
|
139
|
+
except:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
all_msgs.sort(key=lambda x: x['time'])
|
|
143
|
+
return all_msgs
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ====== Todo CRUD ======
|
|
147
|
+
|
|
148
|
+
def load_todos() -> list[dict]:
|
|
149
|
+
if os.path.exists(TODOS_FILE):
|
|
150
|
+
try:
|
|
151
|
+
return json.loads(open(TODOS_FILE, encoding='utf-8').read())
|
|
152
|
+
except:
|
|
153
|
+
return []
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def save_todos(todos: list[dict]):
|
|
158
|
+
os.makedirs(os.path.dirname(TODOS_FILE), exist_ok=True)
|
|
159
|
+
with open(TODOS_FILE, 'w', encoding='utf-8') as f:
|
|
160
|
+
json.dump(todos, f, ensure_ascii=False, indent=2)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def extract_todos(api_key: str, days: int = 7):
|
|
164
|
+
"""扫描最近的聊天记录,AI 提取待办。"""
|
|
165
|
+
config = load_config()
|
|
166
|
+
nt_db = config.get('ntDbPath', '')
|
|
167
|
+
if not nt_db:
|
|
168
|
+
return {"error": "未初始化,请先运行 weflow-cli init"}
|
|
169
|
+
|
|
170
|
+
nt_key = decrypt_lock(config.get('ntKey', ''))
|
|
171
|
+
nt_salt = config.get('ntSalt', '')
|
|
172
|
+
contact_key = decrypt_lock(config.get('contactKey', ''))
|
|
173
|
+
contact_salt = config.get('contactSalt', '')
|
|
174
|
+
msg_dir = os.path.dirname(nt_db.replace('\\', '/'))
|
|
175
|
+
wxid_dir = os.path.dirname(os.path.dirname(msg_dir))
|
|
176
|
+
contact_db = os.path.join(wxid_dir, 'db_storage', 'contact', 'contact.db')
|
|
177
|
+
|
|
178
|
+
name_map = get_name_map(contact_db, contact_key, contact_salt)
|
|
179
|
+
now = datetime.now(TZ)
|
|
180
|
+
start_ts = int((now - timedelta(days=days)).timestamp())
|
|
181
|
+
end_ts = int(now.timestamp())
|
|
182
|
+
|
|
183
|
+
conn = open_db(nt_db, nt_key, nt_salt)
|
|
184
|
+
messages = collect_recent_messages(conn, start_ts, end_ts, name_map)
|
|
185
|
+
conn.close()
|
|
186
|
+
|
|
187
|
+
if not messages:
|
|
188
|
+
return {"new": 0, "total": len(load_todos()), "message": "近 {days} 天没有发现相关消息"}
|
|
189
|
+
|
|
190
|
+
# 构建 prompt
|
|
191
|
+
msg_text = '\n'.join(
|
|
192
|
+
f"[{m['time']}] {m['talker']} - {m['sender']}: {m['content']}"
|
|
193
|
+
for m in messages[-150:] # 最多 150 条
|
|
194
|
+
)
|
|
195
|
+
prompt = TODO_EXTRACT_PROMPT.format(messages=msg_text)
|
|
196
|
+
|
|
197
|
+
# AI 提取
|
|
198
|
+
try:
|
|
199
|
+
response = call_deepseek(prompt, api_key, max_tokens=2000, timeout=90)
|
|
200
|
+
# 清理可能的 markdown 代码块
|
|
201
|
+
response = response.strip()
|
|
202
|
+
if response.startswith('```'):
|
|
203
|
+
response = response.split('\n', 1)[1]
|
|
204
|
+
if response.endswith('```'):
|
|
205
|
+
response = response[:-3]
|
|
206
|
+
todos_raw = json.loads(response)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
return {"error": f"AI 提取失败: {e}"}
|
|
209
|
+
|
|
210
|
+
if not isinstance(todos_raw, list):
|
|
211
|
+
return {"error": "AI 返回格式错误"}
|
|
212
|
+
|
|
213
|
+
# 合并到现有待办(去重:相同 task 文本)
|
|
214
|
+
existing = load_todos()
|
|
215
|
+
existing_tasks = {t['task'] for t in existing}
|
|
216
|
+
new_count = 0
|
|
217
|
+
|
|
218
|
+
for item in todos_raw:
|
|
219
|
+
if not isinstance(item, dict) or not item.get('task'):
|
|
220
|
+
continue
|
|
221
|
+
if item['task'] in existing_tasks:
|
|
222
|
+
continue
|
|
223
|
+
todo = {
|
|
224
|
+
'id': f"todo_{now.strftime('%Y%m%d_%H%M%S')}_{new_count + 1}",
|
|
225
|
+
'task': item['task'],
|
|
226
|
+
'urgency': item.get('urgency', '中'),
|
|
227
|
+
'deadline': item.get('deadline', '未提及'),
|
|
228
|
+
'status': 'pending',
|
|
229
|
+
'created_at': now.strftime('%Y-%m-%d %H:%M'),
|
|
230
|
+
'context': '',
|
|
231
|
+
}
|
|
232
|
+
existing.append(todo)
|
|
233
|
+
new_count += 1
|
|
234
|
+
|
|
235
|
+
save_todos(existing)
|
|
236
|
+
return {"new": new_count, "total": len(existing)}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def list_todos(status: str = None, urgency: str = None):
|
|
240
|
+
todos = load_todos()
|
|
241
|
+
if status:
|
|
242
|
+
todos = [t for t in todos if t['status'] == status]
|
|
243
|
+
if urgency:
|
|
244
|
+
todos = [t for t in todos if t['urgency'] == urgency]
|
|
245
|
+
|
|
246
|
+
# 排序:pending 在前,高紧急度在前
|
|
247
|
+
order = {'pending': 0, 'done': 1}
|
|
248
|
+
urg_order = {'高': 0, '中': 1, '低': 2}
|
|
249
|
+
todos.sort(key=lambda t: (order.get(t['status'], 2), urg_order.get(t['urgency'], 3)))
|
|
250
|
+
|
|
251
|
+
return todos
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def toggle_todo(todo_id: str, status: str):
|
|
255
|
+
todos = load_todos()
|
|
256
|
+
for t in todos:
|
|
257
|
+
if t['id'] == todo_id or t['id'].startswith(todo_id):
|
|
258
|
+
t['status'] = status
|
|
259
|
+
save_todos(todos)
|
|
260
|
+
return t
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def delete_todo(todo_id: str):
|
|
265
|
+
todos = load_todos()
|
|
266
|
+
for i, t in enumerate(todos):
|
|
267
|
+
if t['id'] == todo_id or t['id'].startswith(todo_id):
|
|
268
|
+
deleted = todos.pop(i)
|
|
269
|
+
save_todos(todos)
|
|
270
|
+
return deleted
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def remind():
|
|
275
|
+
"""显示需要关注的待办。"""
|
|
276
|
+
todos = load_todos()
|
|
277
|
+
pending = [t for t in todos if t['status'] == 'pending']
|
|
278
|
+
high = [t for t in pending if t['urgency'] == '高']
|
|
279
|
+
mid = [t for t in pending if t['urgency'] == '中']
|
|
280
|
+
low = [t for t in pending if t['urgency'] == '低']
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
'total_pending': len(pending),
|
|
284
|
+
'high': high,
|
|
285
|
+
'mid': mid,
|
|
286
|
+
'low': low,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ====== Main ======
|
|
291
|
+
|
|
292
|
+
def main():
|
|
293
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
294
|
+
parser = argparse.ArgumentParser(description='待办提取与任务追踪')
|
|
295
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
296
|
+
|
|
297
|
+
# extract
|
|
298
|
+
p = subparsers.add_parser('extract', help='从聊天记录中 AI 提取待办')
|
|
299
|
+
p.add_argument('--days', type=int, default=7, help='扫描最近多少天(默认 7)')
|
|
300
|
+
p.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
|
|
301
|
+
|
|
302
|
+
# list
|
|
303
|
+
p = subparsers.add_parser('list', help='列出待办')
|
|
304
|
+
p.add_argument('--status', choices=['pending', 'done'], help='按状态筛选')
|
|
305
|
+
p.add_argument('--urgency', choices=['高', '中', '低'], help='按紧急度筛选')
|
|
306
|
+
p.add_argument('--json', action='store_true', help='JSON 输出')
|
|
307
|
+
|
|
308
|
+
# done
|
|
309
|
+
p = subparsers.add_parser('done', help='标记待办为已完成')
|
|
310
|
+
p.add_argument('id', help='待办 ID(或前缀)')
|
|
311
|
+
|
|
312
|
+
# undone
|
|
313
|
+
p = subparsers.add_parser('undone', help='取消已完成标记')
|
|
314
|
+
p.add_argument('id', help='待办 ID(或前缀)')
|
|
315
|
+
|
|
316
|
+
# rm
|
|
317
|
+
p = subparsers.add_parser('rm', help='删除待办')
|
|
318
|
+
p.add_argument('id', help='待办 ID(或前缀)')
|
|
319
|
+
|
|
320
|
+
# remind
|
|
321
|
+
subparsers.add_parser('remind', help='查看待办提醒')
|
|
322
|
+
|
|
323
|
+
args = parser.parse_args()
|
|
324
|
+
|
|
325
|
+
if not args.command:
|
|
326
|
+
parser.print_help()
|
|
327
|
+
sys.exit(1)
|
|
328
|
+
|
|
329
|
+
config = load_config()
|
|
330
|
+
api_key = getattr(args, 'api_key', None) or os.environ.get('DEEPSEEK_API_KEY', '') or config.get('deepseekApiKey', '')
|
|
331
|
+
|
|
332
|
+
if args.command == 'extract':
|
|
333
|
+
if not api_key:
|
|
334
|
+
print('[ERROR] 缺少 DeepSeek API key')
|
|
335
|
+
sys.exit(1)
|
|
336
|
+
print(f'扫描最近 {args.days} 天的聊天记录...')
|
|
337
|
+
result = extract_todos(api_key, days=args.days)
|
|
338
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
339
|
+
|
|
340
|
+
elif args.command == 'list':
|
|
341
|
+
result = list_todos(args.status, args.urgency)
|
|
342
|
+
if getattr(args, 'json', False):
|
|
343
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
344
|
+
elif not result:
|
|
345
|
+
print('✅ 暂无待办')
|
|
346
|
+
else:
|
|
347
|
+
status_labels = {'pending': '⬜', 'done': '✅'}
|
|
348
|
+
urg_labels = {'高': '🔴', '中': '🟡', '低': '🟢'}
|
|
349
|
+
for t in result:
|
|
350
|
+
s = status_labels.get(t['status'], '❓')
|
|
351
|
+
u = urg_labels.get(t['urgency'], '⚪')
|
|
352
|
+
print(f" {s} {u} [{t['id']}] {t['task']}")
|
|
353
|
+
if t.get('deadline') and t['deadline'] != '未提及':
|
|
354
|
+
print(f" ⏰ {t['deadline']}")
|
|
355
|
+
print(f"\n共 {len(result)} 项")
|
|
356
|
+
|
|
357
|
+
elif args.command == 'done':
|
|
358
|
+
t = toggle_todo(args.id, 'done')
|
|
359
|
+
if t:
|
|
360
|
+
print(f'✅ 已标记完成: {t["task"]}')
|
|
361
|
+
else:
|
|
362
|
+
print(f'未找到待办: {args.id}')
|
|
363
|
+
|
|
364
|
+
elif args.command == 'undone':
|
|
365
|
+
t = toggle_todo(args.id, 'pending')
|
|
366
|
+
if t:
|
|
367
|
+
print(f'🔄 已恢复: {t["task"]}')
|
|
368
|
+
else:
|
|
369
|
+
print(f'未找到待办: {args.id}')
|
|
370
|
+
|
|
371
|
+
elif args.command == 'rm':
|
|
372
|
+
t = delete_todo(args.id)
|
|
373
|
+
if t:
|
|
374
|
+
print(f'🗑️ 已删除: {t["task"]}')
|
|
375
|
+
else:
|
|
376
|
+
print(f'未找到待办: {args.id}')
|
|
377
|
+
|
|
378
|
+
elif args.command == 'remind':
|
|
379
|
+
result = remind()
|
|
380
|
+
print(f'\n📋 待办提醒 — {datetime.now(TZ).strftime("%Y-%m-%d %H:%M")}')
|
|
381
|
+
print(f' 共 {result["total_pending"]} 项未完成\n')
|
|
382
|
+
if result['high']:
|
|
383
|
+
print('🔴 高紧急度:')
|
|
384
|
+
for t in result['high']:
|
|
385
|
+
print(f' - {t["task"]}')
|
|
386
|
+
if t['deadline'] != '未提及':
|
|
387
|
+
print(f' ⏰ {t["deadline"]}')
|
|
388
|
+
print()
|
|
389
|
+
if result['mid']:
|
|
390
|
+
print('🟡 中紧急度:')
|
|
391
|
+
for t in result['mid']:
|
|
392
|
+
print(f' - {t["task"]}')
|
|
393
|
+
print()
|
|
394
|
+
if result['low']:
|
|
395
|
+
print('🟢 低紧急度:')
|
|
396
|
+
for t in result['low']:
|
|
397
|
+
print(f' - {t["task"]}')
|
|
398
|
+
print()
|
|
399
|
+
if result['total_pending'] == 0:
|
|
400
|
+
print('✅ 暂无待办,干得好!')
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if __name__ == '__main__':
|
|
404
|
+
main()
|