weflow-cli 1.15.1 → 1.17.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/CHANGELOG.md +59 -1
- package/OPERATIONS.md +16 -1
- package/README.en.md +21 -2
- package/README.md +34 -6
- package/bin/weflow-cli.ts +389 -6
- package/dist/bin/weflow-cli.js +422 -8
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +47 -0
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +24 -0
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +8 -1
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +47 -8
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +8 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +67 -11
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/cursorProbe.d.ts +44 -0
- package/dist/src/services/cursorProbe.d.ts.map +1 -0
- package/dist/src/services/cursorProbe.js +183 -0
- package/dist/src/services/cursorProbe.js.map +1 -0
- package/dist/src/services/qchatImport.d.ts +89 -0
- package/dist/src/services/qchatImport.d.ts.map +1 -0
- package/dist/src/services/qchatImport.js +273 -0
- package/dist/src/services/qchatImport.js.map +1 -0
- package/dist/src/services/setupVenv.d.ts +43 -0
- package/dist/src/services/setupVenv.d.ts.map +1 -0
- package/dist/src/services/setupVenv.js +198 -0
- package/dist/src/services/setupVenv.js.map +1 -0
- package/dist/src/utils/python.d.ts +10 -0
- package/dist/src/utils/python.d.ts.map +1 -1
- package/dist/src/utils/python.js +27 -4
- package/dist/src/utils/python.js.map +1 -1
- package/docs/DATA_CONTRACT.md +25 -0
- package/docs/DECISIONS.md +92 -30
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
- package/docs/HEALTH-CHECK.md +104 -104
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
- package/docs/NEXT_ACTIONS.md +236 -236
- package/docs/NPM-PUBLISH.md +130 -130
- package/docs/POSITIONING.md +68 -68
- package/docs/PROJECT_STATE.md +6 -3
- package/docs/REPLAY.md +169 -0
- package/docs/SYNC_CONTRACT.md +197 -180
- package/package.json +89 -88
- package/requirements-3x.txt +4 -4
- package/requirements-voice.txt +15 -15
- package/resources/js/graph3d/NOTICE.txt +35 -35
- package/resources/js/graph3d/d3-binarytree.min.js +2 -2
- package/resources/js/graph3d/d3-dispatch.min.js +2 -2
- package/resources/js/graph3d/d3-force-3d.min.js +2 -2
- package/resources/js/graph3d/d3-octree.min.js +2 -2
- package/resources/js/graph3d/d3-quadtree.min.js +2 -2
- package/resources/js/graph3d/d3-timer.min.js +2 -2
- package/resources/js/graph3d/three.min.js +6 -6
- package/resources/js/graph3d/viewer2d.js +550 -448
- package/resources/panel/quick-menu.cjs +197 -197
- package/scripts/_utils.py +1338 -1338
- package/scripts/article_notes.py +423 -423
- package/scripts/bonds.py +11 -0
- package/scripts/chat_notes.py +86 -3
- package/scripts/check_readme_commands.py +427 -426
- package/scripts/contact_schema.py +339 -339
- package/scripts/create_reading_notes.py +11 -11
- package/scripts/daily_task.ps1 +213 -177
- package/scripts/fav_notes.py +321 -321
- package/scripts/graph_2d.py +156 -151
- package/scripts/graph_3d.py +672 -646
- package/scripts/health_check.py +473 -473
- package/scripts/imported_chats.py +68 -0
- package/scripts/install_tasks.ps1 +105 -91
- package/scripts/leads.py +179 -0
- package/scripts/network.py +206 -206
- package/scripts/nt_decrypt.py +135 -1
- package/scripts/on_this_day.py +178 -178
- package/scripts/opinion_timeline.py +138 -138
- package/scripts/promises.py +185 -185
- package/scripts/replay.py +803 -0
- package/scripts/run_hidden.vbs +25 -0
- package/scripts/semantic_search.py +73 -5
- package/scripts/user_notes.py +315 -315
- package/scripts/voice_transcribe.py +139 -139
- package/scripts/watch_issues.py +212 -212
- package/scripts/watch_mail.py +131 -131
- package/src/core/ntCore.ts +59 -0
- package/src/services/assistantService.ts +48 -8
- package/src/services/assistantTools.ts +73 -10
- package/src/services/configService.ts +3 -2
- package/src/services/cursorProbe.ts +222 -0
- package/src/services/qchatImport.ts +327 -0
- package/src/services/setupVenv.ts +238 -0
- package/src/services/syncService.ts +363 -363
- package/src/services/syncState.ts +275 -275
- package/src/services/wechatMedia.ts +166 -166
- package/src/utils/python.ts +33 -4
package/scripts/promises.py
CHANGED
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
"""欠账清单:我说过要给谁做什么,后来做了没有。
|
|
2
|
-
|
|
3
|
-
这是微信永远回答不了的问题 —— 它知道"我明天发你",但不知道"兑现"是什么。
|
|
4
|
-
|
|
5
|
-
**默认不调模型**:先用规则把候选捞出来(我发的 + 承诺句式),再在同一个会话之后的对话里
|
|
6
|
-
找"完成信号 / 对方催问 / 对方确认"三类证据。规则会误报,所以每条都带着原话和后续,
|
|
7
|
-
让读者自己判 —— 而不是给一个"AI 认为你欠了 7 件事"的黑箱。
|
|
8
|
-
|
|
9
|
-
用法:
|
|
10
|
-
python scripts/promises.py --days 60
|
|
11
|
-
python scripts/promises.py --days 90 --json
|
|
12
|
-
python scripts/promises.py --days 60 --talker 老表
|
|
13
|
-
"""
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import json
|
|
18
|
-
import pathlib
|
|
19
|
-
import re
|
|
20
|
-
import sys
|
|
21
|
-
from collections import defaultdict
|
|
22
|
-
from datetime import datetime, timezone, timedelta
|
|
23
|
-
|
|
24
|
-
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
|
|
25
|
-
sys.path.insert(0, str(SCRIPT_DIR))
|
|
26
|
-
|
|
27
|
-
from _utils import load_config, decrypt_lock # noqa: E402
|
|
28
|
-
import nt_decrypt as nd # noqa: E402
|
|
29
|
-
|
|
30
|
-
TZ = timezone(timedelta(hours=8))
|
|
31
|
-
|
|
32
|
-
# 承诺句式:我 + 将来/稍后 + 一个交付动作。宁可多收,后面还有证据过滤。
|
|
33
|
-
PROMISE_RE = re.compile(
|
|
34
|
-
r'(我|这边)[^。!?\n]{0,12}'
|
|
35
|
-
r'(明天|后天|下周|下周一|晚点|稍后|回头|等会|等会儿|下午|晚上|今天之内|这周)[^。!?\n]{0,12}'
|
|
36
|
-
r'(发|给|寄|做|弄|整理|传|送|安排|联系|确认|回复|处理)')
|
|
37
|
-
DELIVER_RE = re.compile(r'(发了|发你了|发您了|已发|寄了|弄好了|做好了|整理好了|搞定了|已上传|传了|安排了|回复了|处理好了)')
|
|
38
|
-
CHASE_RE = re.compile(r'(发了吗|寄了吗|好了吗|弄好了吗|什么时候|还没|催一下|怎么还没|在吗.{0,6}(发|给))')
|
|
39
|
-
ACK_RE = re.compile(r'^(收到|好的|好滴|谢谢|多谢|ok|OK|嗯嗯|好嘞)')
|
|
40
|
-
_NOT_PROMISE = re.compile(r'(我明天要|我下周要|我想|我觉得|我昨天|我上次|我已经)')
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _clean(text):
|
|
44
|
-
text = ' '.join(str(text or '').split())
|
|
45
|
-
return re.sub(r'^[A-Za-z0-9_\-]+@?(chatroom|openim)?\s*:\s*', '', text)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def same_account(name, wxid):
|
|
49
|
-
"""配置里的 wxid 与 `Name2Id` 里的账号名**可能是同一个人的两种写法**。
|
|
50
|
-
|
|
51
|
-
实测本机:配置是 `wxid_账号A_后缀`,而 `Name2Id` 里只有 `wxid_账号A`(没有后缀)——
|
|
52
|
-
精确比较一个都匹配不上,于是"我发的消息"一条也捞不到(第一版就是这么得到 0 条承诺的)。
|
|
53
|
-
"""
|
|
54
|
-
n, w = (name or '').strip(), (wxid or '').strip()
|
|
55
|
-
if not n or not w:
|
|
56
|
-
return False
|
|
57
|
-
return n == w or n.startswith(w + '_') or w.startswith(n + '_')
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def load_messages(conns, days, my_wxid):
|
|
61
|
-
"""{talker: [ (ts, is_me, sender, text) ]},按时间升序。"""
|
|
62
|
-
since = int((datetime.now(TZ) - timedelta(days=days)).timestamp())
|
|
63
|
-
out = defaultdict(list)
|
|
64
|
-
for conn in conns:
|
|
65
|
-
c = conn.cursor()
|
|
66
|
-
try:
|
|
67
|
-
c.execute('SELECT rowid, user_name FROM Name2Id')
|
|
68
|
-
id2name = {r[0]: r[1] for r in c.fetchall()}
|
|
69
|
-
except Exception:
|
|
70
|
-
continue
|
|
71
|
-
try:
|
|
72
|
-
c.execute('SELECT user_name FROM Name2Id WHERE is_session = 1')
|
|
73
|
-
sessions = [r[0] for r in c.fetchall() if r[0]]
|
|
74
|
-
except Exception:
|
|
75
|
-
sessions = []
|
|
76
|
-
for talker in sessions:
|
|
77
|
-
tbl = 'Msg_%s' % __import__('hashlib').md5(talker.encode()).hexdigest()
|
|
78
|
-
try:
|
|
79
|
-
c.execute('SELECT COUNT(*) FROM sqlite_master WHERE name=?', (tbl,))
|
|
80
|
-
if c.fetchone()[0] == 0:
|
|
81
|
-
continue
|
|
82
|
-
c.execute('SELECT create_time, real_sender_id, local_type, message_content FROM "%s" '
|
|
83
|
-
'WHERE create_time >= ? ORDER BY create_time' % tbl, (since,))
|
|
84
|
-
for ts, sender, ltype, content in c.fetchall():
|
|
85
|
-
if (int(ltype or 0) & 0xFFFFFFFF) != 1:
|
|
86
|
-
continue
|
|
87
|
-
text = _clean(nd._decode_content(content))
|
|
88
|
-
if not text:
|
|
89
|
-
continue
|
|
90
|
-
who = id2name.get(sender, '')
|
|
91
|
-
out[talker].append((int(ts), same_account(who, my_wxid), who, text))
|
|
92
|
-
except Exception:
|
|
93
|
-
continue
|
|
94
|
-
return out
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def classify(follow):
|
|
98
|
-
"""看承诺之后发生了什么:兑现 / 被催 / 只有确认 / 无后续。"""
|
|
99
|
-
if any(f[1] and DELIVER_RE.search(f[3]) for f in follow):
|
|
100
|
-
return 'delivered', '我后来提到"已发/已弄好"'
|
|
101
|
-
chases = [f for f in follow if not f[1] and CHASE_RE.search(f[3])]
|
|
102
|
-
if chases:
|
|
103
|
-
return 'chased', '对方后来催过:' + chases[0][3][:40]
|
|
104
|
-
if any(not f[1] and ACK_RE.search(f[3]) for f in follow):
|
|
105
|
-
return 'acked', '对方回复了确认'
|
|
106
|
-
return 'unknown', '没有后续证据'
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def main():
|
|
110
|
-
ap = argparse.ArgumentParser(description='我说过要给谁做什么,后来做了没有')
|
|
111
|
-
ap.add_argument('--days', type=int, default=60)
|
|
112
|
-
ap.add_argument('--talker', default='', help='只看某个会话(wxid 或昵称子串)')
|
|
113
|
-
ap.add_argument('--limit', type=int, default=30, help='最多列几条(默认 30)')
|
|
114
|
-
ap.add_argument('--json', action='store_true')
|
|
115
|
-
args = ap.parse_args()
|
|
116
|
-
|
|
117
|
-
cfg = load_config()
|
|
118
|
-
db = cfg.get('ntDbPath') or ''
|
|
119
|
-
my_wxid = cfg.get('wxid') or ''
|
|
120
|
-
if not db:
|
|
121
|
-
raise SystemExit('配置里没有 ntDbPath')
|
|
122
|
-
key = decrypt_lock(cfg.get('ntKey', '') or '')
|
|
123
|
-
salt = decrypt_lock(cfg.get('ntSalt', '') or '')
|
|
124
|
-
pw = decrypt_lock(cfg.get('favPassphrase', '') or cfg.get('decryptKey', '') or '')
|
|
125
|
-
conns = nd.connect_message_shards(db, key, salt, pw)
|
|
126
|
-
if not conns:
|
|
127
|
-
raise SystemExit('打不开消息分片')
|
|
128
|
-
|
|
129
|
-
try:
|
|
130
|
-
by_talker = load_messages(conns, args.days, my_wxid)
|
|
131
|
-
# 昵称在 **contact.db**,消息库里没有 `contact` 表 —— 从消息库找一定落空,
|
|
132
|
-
# 输出里就只剩 wxid(和 on_this_day.py 同一个坑)。
|
|
133
|
-
name_map = {}
|
|
134
|
-
try:
|
|
135
|
-
from network import load_graph
|
|
136
|
-
people, _rooms, _members = load_graph(cfg)
|
|
137
|
-
name_map = {p['username']: (p['remark'] or p['nick'] or p['username'])
|
|
138
|
-
for p in people.values()}
|
|
139
|
-
except Exception as exc:
|
|
140
|
-
print(f'[WARN] 读联系人失败({type(exc).__name__}: {exc}),下面显示 id',
|
|
141
|
-
file=sys.stderr)
|
|
142
|
-
finally:
|
|
143
|
-
for conn in conns:
|
|
144
|
-
try:
|
|
145
|
-
conn.close()
|
|
146
|
-
except Exception:
|
|
147
|
-
pass
|
|
148
|
-
|
|
149
|
-
items = []
|
|
150
|
-
for talker, msgs in by_talker.items():
|
|
151
|
-
shown = name_map.get(talker, talker)
|
|
152
|
-
if args.talker and args.talker not in (talker, shown):
|
|
153
|
-
continue
|
|
154
|
-
for i, (ts, is_me, who, text) in enumerate(msgs):
|
|
155
|
-
if not is_me or not PROMISE_RE.search(text) or _NOT_PROMISE.search(text):
|
|
156
|
-
continue
|
|
157
|
-
follow = msgs[i + 1:i + 20]
|
|
158
|
-
state, why = classify(follow)
|
|
159
|
-
items.append({
|
|
160
|
-
'time': datetime.fromtimestamp(ts, tz=TZ).strftime('%Y-%m-%d %H:%M'),
|
|
161
|
-
'talker': shown,
|
|
162
|
-
'said': text[:100],
|
|
163
|
-
'state': state,
|
|
164
|
-
'evidence': why,
|
|
165
|
-
})
|
|
166
|
-
items.sort(key=lambda x: x['time'], reverse=True)
|
|
167
|
-
items = items[:args.limit]
|
|
168
|
-
|
|
169
|
-
if args.json:
|
|
170
|
-
print(json.dumps({'success': True, 'days': args.days, 'items': items}, ensure_ascii=False, indent=2))
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
label = {'delivered': '已兑现', 'chased': '可能欠着', 'acked': '已确认', 'unknown': '看不清'}
|
|
174
|
-
order = {'chased': 0, 'unknown': 1, 'delivered': 2, 'acked': 3}
|
|
175
|
-
items.sort(key=lambda x: (order[x['state']], x['time']))
|
|
176
|
-
print(f'最近 {args.days} 天里,"我"承诺过 {len(items)} 件事:\n')
|
|
177
|
-
for it in items:
|
|
178
|
-
print(f"[{label[it['state']]}] {it['time']} {it['talker']}")
|
|
179
|
-
print(f" 我说:{it['said']}")
|
|
180
|
-
print(f" 后续:{it['evidence']}")
|
|
181
|
-
print()
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if __name__ == '__main__':
|
|
185
|
-
main()
|
|
1
|
+
"""欠账清单:我说过要给谁做什么,后来做了没有。
|
|
2
|
+
|
|
3
|
+
这是微信永远回答不了的问题 —— 它知道"我明天发你",但不知道"兑现"是什么。
|
|
4
|
+
|
|
5
|
+
**默认不调模型**:先用规则把候选捞出来(我发的 + 承诺句式),再在同一个会话之后的对话里
|
|
6
|
+
找"完成信号 / 对方催问 / 对方确认"三类证据。规则会误报,所以每条都带着原话和后续,
|
|
7
|
+
让读者自己判 —— 而不是给一个"AI 认为你欠了 7 件事"的黑箱。
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python scripts/promises.py --days 60
|
|
11
|
+
python scripts/promises.py --days 90 --json
|
|
12
|
+
python scripts/promises.py --days 60 --talker 老表
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import pathlib
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from datetime import datetime, timezone, timedelta
|
|
23
|
+
|
|
24
|
+
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
|
|
25
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
26
|
+
|
|
27
|
+
from _utils import load_config, decrypt_lock # noqa: E402
|
|
28
|
+
import nt_decrypt as nd # noqa: E402
|
|
29
|
+
|
|
30
|
+
TZ = timezone(timedelta(hours=8))
|
|
31
|
+
|
|
32
|
+
# 承诺句式:我 + 将来/稍后 + 一个交付动作。宁可多收,后面还有证据过滤。
|
|
33
|
+
PROMISE_RE = re.compile(
|
|
34
|
+
r'(我|这边)[^。!?\n]{0,12}'
|
|
35
|
+
r'(明天|后天|下周|下周一|晚点|稍后|回头|等会|等会儿|下午|晚上|今天之内|这周)[^。!?\n]{0,12}'
|
|
36
|
+
r'(发|给|寄|做|弄|整理|传|送|安排|联系|确认|回复|处理)')
|
|
37
|
+
DELIVER_RE = re.compile(r'(发了|发你了|发您了|已发|寄了|弄好了|做好了|整理好了|搞定了|已上传|传了|安排了|回复了|处理好了)')
|
|
38
|
+
CHASE_RE = re.compile(r'(发了吗|寄了吗|好了吗|弄好了吗|什么时候|还没|催一下|怎么还没|在吗.{0,6}(发|给))')
|
|
39
|
+
ACK_RE = re.compile(r'^(收到|好的|好滴|谢谢|多谢|ok|OK|嗯嗯|好嘞)')
|
|
40
|
+
_NOT_PROMISE = re.compile(r'(我明天要|我下周要|我想|我觉得|我昨天|我上次|我已经)')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _clean(text):
|
|
44
|
+
text = ' '.join(str(text or '').split())
|
|
45
|
+
return re.sub(r'^[A-Za-z0-9_\-]+@?(chatroom|openim)?\s*:\s*', '', text)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def same_account(name, wxid):
|
|
49
|
+
"""配置里的 wxid 与 `Name2Id` 里的账号名**可能是同一个人的两种写法**。
|
|
50
|
+
|
|
51
|
+
实测本机:配置是 `wxid_账号A_后缀`,而 `Name2Id` 里只有 `wxid_账号A`(没有后缀)——
|
|
52
|
+
精确比较一个都匹配不上,于是"我发的消息"一条也捞不到(第一版就是这么得到 0 条承诺的)。
|
|
53
|
+
"""
|
|
54
|
+
n, w = (name or '').strip(), (wxid or '').strip()
|
|
55
|
+
if not n or not w:
|
|
56
|
+
return False
|
|
57
|
+
return n == w or n.startswith(w + '_') or w.startswith(n + '_')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_messages(conns, days, my_wxid):
|
|
61
|
+
"""{talker: [ (ts, is_me, sender, text) ]},按时间升序。"""
|
|
62
|
+
since = int((datetime.now(TZ) - timedelta(days=days)).timestamp())
|
|
63
|
+
out = defaultdict(list)
|
|
64
|
+
for conn in conns:
|
|
65
|
+
c = conn.cursor()
|
|
66
|
+
try:
|
|
67
|
+
c.execute('SELECT rowid, user_name FROM Name2Id')
|
|
68
|
+
id2name = {r[0]: r[1] for r in c.fetchall()}
|
|
69
|
+
except Exception:
|
|
70
|
+
continue
|
|
71
|
+
try:
|
|
72
|
+
c.execute('SELECT user_name FROM Name2Id WHERE is_session = 1')
|
|
73
|
+
sessions = [r[0] for r in c.fetchall() if r[0]]
|
|
74
|
+
except Exception:
|
|
75
|
+
sessions = []
|
|
76
|
+
for talker in sessions:
|
|
77
|
+
tbl = 'Msg_%s' % __import__('hashlib').md5(talker.encode()).hexdigest()
|
|
78
|
+
try:
|
|
79
|
+
c.execute('SELECT COUNT(*) FROM sqlite_master WHERE name=?', (tbl,))
|
|
80
|
+
if c.fetchone()[0] == 0:
|
|
81
|
+
continue
|
|
82
|
+
c.execute('SELECT create_time, real_sender_id, local_type, message_content FROM "%s" '
|
|
83
|
+
'WHERE create_time >= ? ORDER BY create_time' % tbl, (since,))
|
|
84
|
+
for ts, sender, ltype, content in c.fetchall():
|
|
85
|
+
if (int(ltype or 0) & 0xFFFFFFFF) != 1:
|
|
86
|
+
continue
|
|
87
|
+
text = _clean(nd._decode_content(content))
|
|
88
|
+
if not text:
|
|
89
|
+
continue
|
|
90
|
+
who = id2name.get(sender, '')
|
|
91
|
+
out[talker].append((int(ts), same_account(who, my_wxid), who, text))
|
|
92
|
+
except Exception:
|
|
93
|
+
continue
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def classify(follow):
|
|
98
|
+
"""看承诺之后发生了什么:兑现 / 被催 / 只有确认 / 无后续。"""
|
|
99
|
+
if any(f[1] and DELIVER_RE.search(f[3]) for f in follow):
|
|
100
|
+
return 'delivered', '我后来提到"已发/已弄好"'
|
|
101
|
+
chases = [f for f in follow if not f[1] and CHASE_RE.search(f[3])]
|
|
102
|
+
if chases:
|
|
103
|
+
return 'chased', '对方后来催过:' + chases[0][3][:40]
|
|
104
|
+
if any(not f[1] and ACK_RE.search(f[3]) for f in follow):
|
|
105
|
+
return 'acked', '对方回复了确认'
|
|
106
|
+
return 'unknown', '没有后续证据'
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
ap = argparse.ArgumentParser(description='我说过要给谁做什么,后来做了没有')
|
|
111
|
+
ap.add_argument('--days', type=int, default=60)
|
|
112
|
+
ap.add_argument('--talker', default=os.environ.get('WEFLOW_TALKER', ''), help='只看某个会话(wxid 或昵称子串)')
|
|
113
|
+
ap.add_argument('--limit', type=int, default=30, help='最多列几条(默认 30)')
|
|
114
|
+
ap.add_argument('--json', action='store_true')
|
|
115
|
+
args = ap.parse_args()
|
|
116
|
+
|
|
117
|
+
cfg = load_config()
|
|
118
|
+
db = cfg.get('ntDbPath') or ''
|
|
119
|
+
my_wxid = cfg.get('wxid') or ''
|
|
120
|
+
if not db:
|
|
121
|
+
raise SystemExit('配置里没有 ntDbPath')
|
|
122
|
+
key = decrypt_lock(cfg.get('ntKey', '') or '')
|
|
123
|
+
salt = decrypt_lock(cfg.get('ntSalt', '') or '')
|
|
124
|
+
pw = decrypt_lock(cfg.get('favPassphrase', '') or cfg.get('decryptKey', '') or '')
|
|
125
|
+
conns = nd.connect_message_shards(db, key, salt, pw)
|
|
126
|
+
if not conns:
|
|
127
|
+
raise SystemExit('打不开消息分片')
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
by_talker = load_messages(conns, args.days, my_wxid)
|
|
131
|
+
# 昵称在 **contact.db**,消息库里没有 `contact` 表 —— 从消息库找一定落空,
|
|
132
|
+
# 输出里就只剩 wxid(和 on_this_day.py 同一个坑)。
|
|
133
|
+
name_map = {}
|
|
134
|
+
try:
|
|
135
|
+
from network import load_graph
|
|
136
|
+
people, _rooms, _members = load_graph(cfg)
|
|
137
|
+
name_map = {p['username']: (p['remark'] or p['nick'] or p['username'])
|
|
138
|
+
for p in people.values()}
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
print(f'[WARN] 读联系人失败({type(exc).__name__}: {exc}),下面显示 id',
|
|
141
|
+
file=sys.stderr)
|
|
142
|
+
finally:
|
|
143
|
+
for conn in conns:
|
|
144
|
+
try:
|
|
145
|
+
conn.close()
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
items = []
|
|
150
|
+
for talker, msgs in by_talker.items():
|
|
151
|
+
shown = name_map.get(talker, talker)
|
|
152
|
+
if args.talker and args.talker not in (talker, shown):
|
|
153
|
+
continue
|
|
154
|
+
for i, (ts, is_me, who, text) in enumerate(msgs):
|
|
155
|
+
if not is_me or not PROMISE_RE.search(text) or _NOT_PROMISE.search(text):
|
|
156
|
+
continue
|
|
157
|
+
follow = msgs[i + 1:i + 20]
|
|
158
|
+
state, why = classify(follow)
|
|
159
|
+
items.append({
|
|
160
|
+
'time': datetime.fromtimestamp(ts, tz=TZ).strftime('%Y-%m-%d %H:%M'),
|
|
161
|
+
'talker': shown,
|
|
162
|
+
'said': text[:100],
|
|
163
|
+
'state': state,
|
|
164
|
+
'evidence': why,
|
|
165
|
+
})
|
|
166
|
+
items.sort(key=lambda x: x['time'], reverse=True)
|
|
167
|
+
items = items[:args.limit]
|
|
168
|
+
|
|
169
|
+
if args.json:
|
|
170
|
+
print(json.dumps({'success': True, 'days': args.days, 'items': items}, ensure_ascii=False, indent=2))
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
label = {'delivered': '已兑现', 'chased': '可能欠着', 'acked': '已确认', 'unknown': '看不清'}
|
|
174
|
+
order = {'chased': 0, 'unknown': 1, 'delivered': 2, 'acked': 3}
|
|
175
|
+
items.sort(key=lambda x: (order[x['state']], x['time']))
|
|
176
|
+
print(f'最近 {args.days} 天里,"我"承诺过 {len(items)} 件事:\n')
|
|
177
|
+
for it in items:
|
|
178
|
+
print(f"[{label[it['state']]}] {it['time']} {it['talker']}")
|
|
179
|
+
print(f" 我说:{it['said']}")
|
|
180
|
+
print(f" 后续:{it['evidence']}")
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == '__main__':
|
|
185
|
+
main()
|