weflow-cli 1.16.0 → 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.
Files changed (97) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +16 -4
  3. package/bin/weflow-cli.ts +348 -6
  4. package/dist/bin/weflow-cli.js +381 -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/qchatImport.d.ts +89 -0
  26. package/dist/src/services/qchatImport.d.ts.map +1 -0
  27. package/dist/src/services/qchatImport.js +273 -0
  28. package/dist/src/services/qchatImport.js.map +1 -0
  29. package/dist/src/services/setupVenv.d.ts +43 -0
  30. package/dist/src/services/setupVenv.d.ts.map +1 -0
  31. package/dist/src/services/setupVenv.js +198 -0
  32. package/dist/src/services/setupVenv.js.map +1 -0
  33. package/dist/src/utils/python.d.ts +10 -0
  34. package/dist/src/utils/python.d.ts.map +1 -1
  35. package/dist/src/utils/python.js +27 -4
  36. package/dist/src/utils/python.js.map +1 -1
  37. package/docs/DATA_CONTRACT.md +25 -0
  38. package/docs/DECISIONS.md +92 -30
  39. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
  40. package/docs/HEALTH-CHECK.md +104 -104
  41. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
  42. package/docs/NEXT_ACTIONS.md +236 -236
  43. package/docs/NPM-PUBLISH.md +130 -130
  44. package/docs/POSITIONING.md +68 -68
  45. package/docs/PROJECT_STATE.md +6 -3
  46. package/docs/REPLAY.md +169 -0
  47. package/docs/SYNC_CONTRACT.md +197 -180
  48. package/package.json +89 -88
  49. package/requirements-3x.txt +4 -4
  50. package/requirements-voice.txt +15 -15
  51. package/resources/js/graph3d/NOTICE.txt +35 -35
  52. package/resources/js/graph3d/d3-binarytree.min.js +2 -2
  53. package/resources/js/graph3d/d3-dispatch.min.js +2 -2
  54. package/resources/js/graph3d/d3-force-3d.min.js +2 -2
  55. package/resources/js/graph3d/d3-octree.min.js +2 -2
  56. package/resources/js/graph3d/d3-quadtree.min.js +2 -2
  57. package/resources/js/graph3d/d3-timer.min.js +2 -2
  58. package/resources/js/graph3d/three.min.js +6 -6
  59. package/resources/js/graph3d/viewer2d.js +550 -448
  60. package/resources/panel/quick-menu.cjs +197 -197
  61. package/scripts/_utils.py +1338 -1338
  62. package/scripts/article_notes.py +423 -423
  63. package/scripts/chat_notes.py +86 -3
  64. package/scripts/check_readme_commands.py +427 -426
  65. package/scripts/contact_schema.py +339 -339
  66. package/scripts/create_reading_notes.py +11 -11
  67. package/scripts/daily_task.ps1 +213 -213
  68. package/scripts/fav_notes.py +321 -321
  69. package/scripts/graph_2d.py +156 -151
  70. package/scripts/graph_3d.py +672 -646
  71. package/scripts/health_check.py +473 -473
  72. package/scripts/imported_chats.py +68 -0
  73. package/scripts/install_tasks.ps1 +105 -105
  74. package/scripts/leads.py +179 -179
  75. package/scripts/network.py +206 -206
  76. package/scripts/nt_decrypt.py +135 -1
  77. package/scripts/on_this_day.py +178 -178
  78. package/scripts/opinion_timeline.py +138 -138
  79. package/scripts/promises.py +185 -185
  80. package/scripts/replay.py +803 -0
  81. package/scripts/run_hidden.vbs +25 -0
  82. package/scripts/semantic_search.py +73 -5
  83. package/scripts/user_notes.py +315 -315
  84. package/scripts/voice_transcribe.py +139 -139
  85. package/scripts/watch_issues.py +212 -212
  86. package/scripts/watch_mail.py +131 -131
  87. package/src/core/ntCore.ts +59 -0
  88. package/src/services/assistantService.ts +48 -8
  89. package/src/services/assistantTools.ts +73 -10
  90. package/src/services/configService.ts +3 -2
  91. package/src/services/cursorProbe.ts +222 -0
  92. package/src/services/qchatImport.ts +327 -0
  93. package/src/services/setupVenv.ts +238 -0
  94. package/src/services/syncService.ts +363 -363
  95. package/src/services/syncState.ts +275 -275
  96. package/src/services/wechatMedia.ts +166 -166
  97. package/src/utils/python.ts +33 -4
@@ -1,139 +1,139 @@
1
- """批量转写语音消息,产出一份可检索的清单(`output/.voice_index/voices.jsonl`)。
2
-
3
- 为什么要有它:`wechat_voice.py` 只能一条会话一条会话地转,转完就躺在一个按**内容哈希**
4
- 命名的缓存里——**知道"这段音频说了什么",但不知道"是哪条消息、谁在什么时候发的"**。
5
- 而检索需要的正是后者。
6
-
7
- 所以这里做三件事:
8
- 1. 从 `media_*.db` 的 `VoiceInfo` 按时间取语音(带上会话、发送者、时间);
9
- 2. 调 `wechat_voice.ensure_transcripts` 填空缺(断点续传:已转过的按内容命中缓存);
10
- 3. 把 `(消息, 时间, 会话, 文本)` 落成 JSONL,供 `phrase-build` 并进 FTS。
11
-
12
- 用法:
13
- python scripts/voice_transcribe.py --days 30
14
- python scripts/voice_transcribe.py --days 7 --limit 5 # 先试几条
15
- python scripts/voice_transcribe.py --days 30 --model tiny # 更快、更糙
16
- """
17
- from __future__ import annotations
18
-
19
- import argparse
20
- import json
21
- import pathlib
22
- import sys
23
- import time
24
- from datetime import datetime, timezone, timedelta
25
-
26
- SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
27
- sys.path.insert(0, str(SCRIPT_DIR))
28
-
29
- from _utils import load_config, decrypt_lock # noqa: E402
30
- import wechat_voice as wv # noqa: E402
31
-
32
- ROOT = SCRIPT_DIR.parent
33
- OUT_DIR = ROOT / 'output' / '.voice_index'
34
- CACHE_DIR = ROOT / 'output' / '.voice_cache'
35
- TZ = timezone(timedelta(hours=8))
36
-
37
-
38
- def _keys(cfg):
39
- """配置里的密钥是 `lock:` 密文,必须先解密 —— 直接拿密文去派生只会得到"不是数据库"。"""
40
- key = decrypt_lock(cfg.get('ntKey', '') or '')
41
- salt = decrypt_lock(cfg.get('ntSalt', '') or '')
42
- pw = decrypt_lock(cfg.get('favPassphrase', '') or cfg.get('decryptKey', '') or '')
43
- return key, salt, pw
44
-
45
-
46
- def collect(cfg, days, limit=0):
47
- """[(shard, chat_name_id, local_id, create_time, blob, name)],按时间倒序。"""
48
- key, salt, pw = _keys(cfg)
49
- db = cfg.get('ntDbPath') or ''
50
- if not db:
51
- raise SystemExit('配置里没有 ntDbPath')
52
- since = int(time.time()) - days * 86400
53
- rows = []
54
- for shard in wv.voice_shards(db):
55
- conn = None
56
- try:
57
- conn, cur = wv._open(shard, key, salt, pw)
58
- cur.execute('SELECT rowid, user_name FROM Name2Id')
59
- names = {r[0]: r[1] for r in cur.fetchall()}
60
- cur.execute(
61
- 'SELECT chat_name_id, local_id, create_time, voice_data FROM VoiceInfo '
62
- 'WHERE voice_data IS NOT NULL AND create_time >= ? '
63
- 'ORDER BY create_time DESC', (since,))
64
- for cid, lid, ts, blob in cur.fetchall():
65
- rows.append((str(shard), cid, lid, int(ts or 0), blob,
66
- names.get(cid, str(cid))))
67
- except Exception as exc:
68
- print(f'[WARN] {pathlib.Path(shard).name} 读不了({type(exc).__name__}: {exc})',
69
- file=sys.stderr)
70
- finally:
71
- if conn:
72
- conn.close()
73
- rows.sort(key=lambda r: r[3], reverse=True)
74
- if limit and limit > 0:
75
- rows = rows[:limit]
76
-
77
- # 会话名从 **contact.db** 补一层:消息库的 `Name2Id` 只有 id,语音清单里
78
- # 只写 id 的话,搜索结果显示成 `wxid_xxx`(2026-10-22 第一版就是这样)。
79
- try:
80
- from network import load_graph
81
- people, _rooms, _members = load_graph(cfg)
82
- by_username = {p['username']: (p['remark'] or p['nick'] or p['username'])
83
- for p in people.values()}
84
- rows = [(s, c, l, t, b, by_username.get(name, name)) for s, c, l, t, b, name in rows]
85
- except Exception as exc:
86
- print(f'[WARN] 读联系人失败({type(exc).__name__}: {exc}),语音清单里显示 id',
87
- file=sys.stderr)
88
- return rows
89
-
90
-
91
- def main():
92
- ap = argparse.ArgumentParser(description='批量转写微信语音,产出可检索清单')
93
- ap.add_argument('--days', type=int, default=30, help='只转最近 N 天(默认 30)')
94
- ap.add_argument('--limit', type=int, default=0, help='最多转写几条(0 = 不限)')
95
- ap.add_argument('--model', default='small', help='faster-whisper 模型名或本地目录')
96
- ap.add_argument('--language', default='zh', help='语言提示')
97
- ap.add_argument('--cache-dir', default='', help='转写缓存目录(换模型时换一个,否则会命中旧模型的结果)')
98
- args = ap.parse_args()
99
-
100
- cfg = load_config()
101
- rows = collect(cfg, args.days, args.limit)
102
- print(f'最近 {args.days} 天语音: {len(rows)} 条')
103
- if not rows:
104
- return 0
105
- cache_dir = args.cache_dir or str(CACHE_DIR)
106
- pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
107
-
108
- voices = {(r[5], r[2]): r[4] for r in rows}
109
- started = time.time()
110
- processed, cached, failed = wv.ensure_transcripts(
111
- voices, cache_dir, model_name=args.model, language=args.language)
112
- elapsed = time.time() - started
113
- print(f'转写完成: 新增 {processed},缓存命中 {cached},失败 {failed},耗时 {elapsed/60:.1f} 分钟')
114
-
115
- cache = wv.TranscriptCache(cache_dir)
116
- OUT_DIR.mkdir(parents=True, exist_ok=True)
117
- out = OUT_DIR / 'voices.jsonl'
118
- written = empty = 0
119
- with out.open('w', encoding='utf-8') as fh:
120
- for shard, cid, lid, ts, blob, name in rows:
121
- text = (cache.get(wv.voice_key(blob)) or '').strip()
122
- if not text:
123
- empty += 1
124
- continue
125
- dt = datetime.fromtimestamp(ts, tz=TZ)
126
- fh.write(json.dumps({
127
- 'id': f'voice:{name}:{lid}',
128
- 'talker': name,
129
- 'time': dt.strftime('%Y-%m-%d %H:%M'),
130
- 'ts': ts,
131
- 'text': text[:500],
132
- }, ensure_ascii=False) + '\n')
133
- written += 1
134
- print(f'写入 {out}:{written} 条有文字(空转写/解码失败 {empty} 条)')
135
- return 0
136
-
137
-
138
- if __name__ == '__main__':
139
- sys.exit(main())
1
+ """批量转写语音消息,产出一份可检索的清单(`output/.voice_index/voices.jsonl`)。
2
+
3
+ 为什么要有它:`wechat_voice.py` 只能一条会话一条会话地转,转完就躺在一个按**内容哈希**
4
+ 命名的缓存里——**知道"这段音频说了什么",但不知道"是哪条消息、谁在什么时候发的"**。
5
+ 而检索需要的正是后者。
6
+
7
+ 所以这里做三件事:
8
+ 1. 从 `media_*.db` 的 `VoiceInfo` 按时间取语音(带上会话、发送者、时间);
9
+ 2. 调 `wechat_voice.ensure_transcripts` 填空缺(断点续传:已转过的按内容命中缓存);
10
+ 3. 把 `(消息, 时间, 会话, 文本)` 落成 JSONL,供 `phrase-build` 并进 FTS。
11
+
12
+ 用法:
13
+ python scripts/voice_transcribe.py --days 30
14
+ python scripts/voice_transcribe.py --days 7 --limit 5 # 先试几条
15
+ python scripts/voice_transcribe.py --days 30 --model tiny # 更快、更糙
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import pathlib
22
+ import sys
23
+ import time
24
+ from datetime import datetime, timezone, timedelta
25
+
26
+ SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
27
+ sys.path.insert(0, str(SCRIPT_DIR))
28
+
29
+ from _utils import load_config, decrypt_lock # noqa: E402
30
+ import wechat_voice as wv # noqa: E402
31
+
32
+ ROOT = SCRIPT_DIR.parent
33
+ OUT_DIR = ROOT / 'output' / '.voice_index'
34
+ CACHE_DIR = ROOT / 'output' / '.voice_cache'
35
+ TZ = timezone(timedelta(hours=8))
36
+
37
+
38
+ def _keys(cfg):
39
+ """配置里的密钥是 `lock:` 密文,必须先解密 —— 直接拿密文去派生只会得到"不是数据库"。"""
40
+ key = decrypt_lock(cfg.get('ntKey', '') or '')
41
+ salt = decrypt_lock(cfg.get('ntSalt', '') or '')
42
+ pw = decrypt_lock(cfg.get('favPassphrase', '') or cfg.get('decryptKey', '') or '')
43
+ return key, salt, pw
44
+
45
+
46
+ def collect(cfg, days, limit=0):
47
+ """[(shard, chat_name_id, local_id, create_time, blob, name)],按时间倒序。"""
48
+ key, salt, pw = _keys(cfg)
49
+ db = cfg.get('ntDbPath') or ''
50
+ if not db:
51
+ raise SystemExit('配置里没有 ntDbPath')
52
+ since = int(time.time()) - days * 86400
53
+ rows = []
54
+ for shard in wv.voice_shards(db):
55
+ conn = None
56
+ try:
57
+ conn, cur = wv._open(shard, key, salt, pw)
58
+ cur.execute('SELECT rowid, user_name FROM Name2Id')
59
+ names = {r[0]: r[1] for r in cur.fetchall()}
60
+ cur.execute(
61
+ 'SELECT chat_name_id, local_id, create_time, voice_data FROM VoiceInfo '
62
+ 'WHERE voice_data IS NOT NULL AND create_time >= ? '
63
+ 'ORDER BY create_time DESC', (since,))
64
+ for cid, lid, ts, blob in cur.fetchall():
65
+ rows.append((str(shard), cid, lid, int(ts or 0), blob,
66
+ names.get(cid, str(cid))))
67
+ except Exception as exc:
68
+ print(f'[WARN] {pathlib.Path(shard).name} 读不了({type(exc).__name__}: {exc})',
69
+ file=sys.stderr)
70
+ finally:
71
+ if conn:
72
+ conn.close()
73
+ rows.sort(key=lambda r: r[3], reverse=True)
74
+ if limit and limit > 0:
75
+ rows = rows[:limit]
76
+
77
+ # 会话名从 **contact.db** 补一层:消息库的 `Name2Id` 只有 id,语音清单里
78
+ # 只写 id 的话,搜索结果显示成 `wxid_xxx`(2026-10-22 第一版就是这样)。
79
+ try:
80
+ from network import load_graph
81
+ people, _rooms, _members = load_graph(cfg)
82
+ by_username = {p['username']: (p['remark'] or p['nick'] or p['username'])
83
+ for p in people.values()}
84
+ rows = [(s, c, l, t, b, by_username.get(name, name)) for s, c, l, t, b, name in rows]
85
+ except Exception as exc:
86
+ print(f'[WARN] 读联系人失败({type(exc).__name__}: {exc}),语音清单里显示 id',
87
+ file=sys.stderr)
88
+ return rows
89
+
90
+
91
+ def main():
92
+ ap = argparse.ArgumentParser(description='批量转写微信语音,产出可检索清单')
93
+ ap.add_argument('--days', type=int, default=30, help='只转最近 N 天(默认 30)')
94
+ ap.add_argument('--limit', type=int, default=0, help='最多转写几条(0 = 不限)')
95
+ ap.add_argument('--model', default='small', help='faster-whisper 模型名或本地目录')
96
+ ap.add_argument('--language', default='zh', help='语言提示')
97
+ ap.add_argument('--cache-dir', default='', help='转写缓存目录(换模型时换一个,否则会命中旧模型的结果)')
98
+ args = ap.parse_args()
99
+
100
+ cfg = load_config()
101
+ rows = collect(cfg, args.days, args.limit)
102
+ print(f'最近 {args.days} 天语音: {len(rows)} 条')
103
+ if not rows:
104
+ return 0
105
+ cache_dir = args.cache_dir or str(CACHE_DIR)
106
+ pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
107
+
108
+ voices = {(r[5], r[2]): r[4] for r in rows}
109
+ started = time.time()
110
+ processed, cached, failed = wv.ensure_transcripts(
111
+ voices, cache_dir, model_name=args.model, language=args.language)
112
+ elapsed = time.time() - started
113
+ print(f'转写完成: 新增 {processed},缓存命中 {cached},失败 {failed},耗时 {elapsed/60:.1f} 分钟')
114
+
115
+ cache = wv.TranscriptCache(cache_dir)
116
+ OUT_DIR.mkdir(parents=True, exist_ok=True)
117
+ out = OUT_DIR / 'voices.jsonl'
118
+ written = empty = 0
119
+ with out.open('w', encoding='utf-8') as fh:
120
+ for shard, cid, lid, ts, blob, name in rows:
121
+ text = (cache.get(wv.voice_key(blob)) or '').strip()
122
+ if not text:
123
+ empty += 1
124
+ continue
125
+ dt = datetime.fromtimestamp(ts, tz=TZ)
126
+ fh.write(json.dumps({
127
+ 'id': f'voice:{name}:{lid}',
128
+ 'talker': name,
129
+ 'time': dt.strftime('%Y-%m-%d %H:%M'),
130
+ 'ts': ts,
131
+ 'text': text[:500],
132
+ }, ensure_ascii=False) + '\n')
133
+ written += 1
134
+ print(f'写入 {out}:{written} 条有文字(空转写/解码失败 {empty} 条)')
135
+ return 0
136
+
137
+
138
+ if __name__ == '__main__':
139
+ sys.exit(main())