weflow-cli 1.16.0 → 1.19.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +19 -4
  3. package/bin/weflow-cli.ts +515 -6
  4. package/dist/bin/weflow-cli.js +580 -8
  5. package/dist/bin/weflow-cli.js.map +1 -1
  6. package/dist/src/core/ntCore.d.ts +47 -0
  7. package/dist/src/core/ntCore.d.ts.map +1 -1
  8. package/dist/src/core/ntCore.js +24 -0
  9. package/dist/src/core/ntCore.js.map +1 -1
  10. package/dist/src/services/assistantService.d.ts +8 -1
  11. package/dist/src/services/assistantService.d.ts.map +1 -1
  12. package/dist/src/services/assistantService.js +47 -8
  13. package/dist/src/services/assistantService.js.map +1 -1
  14. package/dist/src/services/assistantTools.d.ts +8 -0
  15. package/dist/src/services/assistantTools.d.ts.map +1 -1
  16. package/dist/src/services/assistantTools.js +67 -11
  17. package/dist/src/services/assistantTools.js.map +1 -1
  18. package/dist/src/services/configService.d.ts.map +1 -1
  19. package/dist/src/services/configService.js +3 -2
  20. package/dist/src/services/configService.js.map +1 -1
  21. package/dist/src/services/cursorProbe.d.ts +44 -0
  22. package/dist/src/services/cursorProbe.d.ts.map +1 -0
  23. package/dist/src/services/cursorProbe.js +183 -0
  24. package/dist/src/services/cursorProbe.js.map +1 -0
  25. package/dist/src/services/diagnosticReport.d.ts +6 -0
  26. package/dist/src/services/diagnosticReport.d.ts.map +1 -1
  27. package/dist/src/services/diagnosticReport.js +16 -0
  28. package/dist/src/services/diagnosticReport.js.map +1 -1
  29. package/dist/src/services/qchatImport.d.ts +89 -0
  30. package/dist/src/services/qchatImport.d.ts.map +1 -0
  31. package/dist/src/services/qchatImport.js +273 -0
  32. package/dist/src/services/qchatImport.js.map +1 -0
  33. package/dist/src/services/setupVenv.d.ts +43 -0
  34. package/dist/src/services/setupVenv.d.ts.map +1 -0
  35. package/dist/src/services/setupVenv.js +198 -0
  36. package/dist/src/services/setupVenv.js.map +1 -0
  37. package/dist/src/services/wechatCompat.d.ts +29 -0
  38. package/dist/src/services/wechatCompat.d.ts.map +1 -0
  39. package/dist/src/services/wechatCompat.js +65 -0
  40. package/dist/src/services/wechatCompat.js.map +1 -0
  41. package/dist/src/utils/python.d.ts +10 -0
  42. package/dist/src/utils/python.d.ts.map +1 -1
  43. package/dist/src/utils/python.js +27 -4
  44. package/dist/src/utils/python.js.map +1 -1
  45. package/docs/DATA_CONTRACT.md +25 -0
  46. package/docs/DECISIONS.md +118 -30
  47. package/docs/NT_DATABASES.md +47 -0
  48. package/docs/PROJECT_STATE.md +7 -3
  49. package/docs/RELEASING.md +4 -0
  50. package/docs/REPLAY.md +169 -0
  51. package/docs/SYNC_CONTRACT.md +197 -180
  52. package/package.json +89 -88
  53. package/resources/js/graph3d/viewer2d.js +550 -448
  54. package/scripts/annual_report.py +3 -1
  55. package/scripts/chat_notes.py +86 -3
  56. package/scripts/check_readme_commands.py +427 -426
  57. package/scripts/extract_todos.py +3 -1
  58. package/scripts/graph_2d.py +156 -151
  59. package/scripts/graph_3d.py +672 -646
  60. package/scripts/imported_chats.py +68 -0
  61. package/scripts/mcp_bridge.py +3 -1
  62. package/scripts/media_index.py +150 -0
  63. package/scripts/nt_common.py +32 -0
  64. package/scripts/nt_decrypt.py +154 -16
  65. package/scripts/on_this_day.py +178 -178
  66. package/scripts/promises.py +185 -185
  67. package/scripts/replay.py +803 -0
  68. package/scripts/run_hidden.vbs +25 -0
  69. package/scripts/semantic_search.py +78 -6
  70. package/scripts/wechat_avatar.py +185 -0
  71. package/scripts/wechat_ledger.py +190 -0
  72. package/src/core/ntCore.ts +59 -0
  73. package/src/services/assistantService.ts +48 -8
  74. package/src/services/assistantTools.ts +73 -10
  75. package/src/services/configService.ts +3 -2
  76. package/src/services/cursorProbe.ts +222 -0
  77. package/src/services/diagnosticReport.ts +22 -0
  78. package/src/services/qchatImport.ts +327 -0
  79. package/src/services/setupVenv.ts +238 -0
  80. package/src/services/wechatCompat.ts +67 -0
  81. package/src/utils/python.ts +33 -4
@@ -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()