weflow-cli 1.5.0 → 1.6.1
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/ARCHITECTURE.md +105 -0
- package/CHANGELOG.md +121 -0
- package/CONTRIBUTING.md +45 -0
- package/LICENSE +21 -21
- package/OPERATIONS.md +272 -0
- package/README.en.md +281 -0
- package/README.md +334 -149
- package/SECURITY.md +47 -0
- package/bin/weflow-cli.ts +4050 -581
- package/dist/bin/weflow-cli.js +4310 -588
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.js +178 -236
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +15 -1
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +177 -19
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/core/keyService.d.ts +8 -0
- package/dist/src/core/keyService.d.ts.map +1 -1
- package/dist/src/core/keyService.js +127 -22
- package/dist/src/core/keyService.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +59 -1
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +201 -65
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
- package/dist/src/core/sqlcipherCore.js +18 -1
- package/dist/src/core/sqlcipherCore.js.map +1 -1
- package/dist/src/core/wcdbCore.d.ts.map +1 -1
- package/dist/src/core/wcdbCore.js +3 -6
- package/dist/src/core/wcdbCore.js.map +1 -1
- package/dist/src/services/assistantDaemon.d.ts +19 -0
- package/dist/src/services/assistantDaemon.d.ts.map +1 -0
- package/dist/src/services/assistantDaemon.js +117 -0
- package/dist/src/services/assistantDaemon.js.map +1 -0
- package/dist/src/services/assistantMemory.d.ts +36 -0
- package/dist/src/services/assistantMemory.d.ts.map +1 -0
- package/dist/src/services/assistantMemory.js +182 -0
- package/dist/src/services/assistantMemory.js.map +1 -0
- package/dist/src/services/assistantPrivacy.d.ts +25 -0
- package/dist/src/services/assistantPrivacy.d.ts.map +1 -0
- package/dist/src/services/assistantPrivacy.js +77 -0
- package/dist/src/services/assistantPrivacy.js.map +1 -0
- package/dist/src/services/assistantRouting.d.ts +18 -0
- package/dist/src/services/assistantRouting.d.ts.map +1 -0
- package/dist/src/services/assistantRouting.js +68 -0
- package/dist/src/services/assistantRouting.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +37 -0
- package/dist/src/services/assistantService.d.ts.map +1 -0
- package/dist/src/services/assistantService.js +284 -0
- package/dist/src/services/assistantService.js.map +1 -0
- package/dist/src/services/assistantTools.d.ts +24 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -0
- package/dist/src/services/assistantTools.js +582 -0
- package/dist/src/services/assistantTools.js.map +1 -0
- package/dist/src/services/chatService.d.ts +44 -0
- package/dist/src/services/chatService.d.ts.map +1 -1
- package/dist/src/services/chatService.js +105 -4
- package/dist/src/services/chatService.js.map +1 -1
- package/dist/src/services/configService.d.ts +55 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +147 -6
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/evidenceService.d.ts +30 -0
- package/dist/src/services/evidenceService.d.ts.map +1 -0
- package/dist/src/services/evidenceService.js +98 -0
- package/dist/src/services/evidenceService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +13 -20
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +113 -47
- package/dist/src/services/exportService.js.map +1 -1
- package/dist/src/services/initKeyService.d.ts +19 -0
- package/dist/src/services/initKeyService.d.ts.map +1 -0
- package/dist/src/services/initKeyService.js +80 -0
- package/dist/src/services/initKeyService.js.map +1 -0
- package/dist/src/services/messageContract.d.ts +23 -0
- package/dist/src/services/messageContract.d.ts.map +1 -0
- package/dist/src/services/messageContract.js +49 -0
- package/dist/src/services/messageContract.js.map +1 -0
- package/dist/src/services/messageQuery.d.ts +7 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -0
- package/dist/src/services/messageQuery.js +30 -0
- package/dist/src/services/messageQuery.js.map +1 -0
- package/dist/src/services/wechat-formatter.js +14 -14
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +31 -3
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/services/whitelistService.d.ts +52 -4
- package/dist/src/services/whitelistService.d.ts.map +1 -1
- package/dist/src/services/whitelistService.js +183 -17
- package/dist/src/services/whitelistService.js.map +1 -1
- package/dist/src/types.d.ts +20 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/dateRange.d.ts +15 -0
- package/dist/src/utils/dateRange.d.ts.map +1 -0
- package/dist/src/utils/dateRange.js +48 -0
- package/dist/src/utils/dateRange.js.map +1 -0
- package/dist/src/utils/mcpSecurity.d.ts +5 -0
- package/dist/src/utils/mcpSecurity.d.ts.map +1 -0
- package/dist/src/utils/mcpSecurity.js +39 -0
- package/dist/src/utils/mcpSecurity.js.map +1 -0
- package/dist/src/utils/packageRoot.d.ts +2 -0
- package/dist/src/utils/packageRoot.d.ts.map +1 -0
- package/dist/src/utils/packageRoot.js +16 -0
- package/dist/src/utils/packageRoot.js.map +1 -0
- package/dist/src/utils/python.d.ts +2 -0
- package/dist/src/utils/python.d.ts.map +1 -0
- package/dist/src/utils/python.js +72 -0
- package/dist/src/utils/python.js.map +1 -0
- package/dist/src/utils/pythonProcessEnv.d.ts +3 -0
- package/dist/src/utils/pythonProcessEnv.d.ts.map +1 -0
- package/dist/src/utils/pythonProcessEnv.js +46 -0
- package/dist/src/utils/pythonProcessEnv.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +4 -1
- package/dist/src/utils/talkerUtils.d.ts.map +1 -1
- package/dist/src/utils/talkerUtils.js +9 -9
- package/dist/src/utils/talkerUtils.js.map +1 -1
- package/dist/src/utils/wechatEmoji.d.ts +5 -0
- package/dist/src/utils/wechatEmoji.d.ts.map +1 -0
- package/dist/src/utils/wechatEmoji.js +35 -0
- package/dist/src/utils/wechatEmoji.js.map +1 -0
- package/docs/AI_INTERFACE.md +189 -0
- package/docs/BRANCHES.md +87 -0
- package/docs/DATA_CONTRACT.md +74 -0
- package/docs/DECISIONS.md +270 -0
- package/docs/DEEPSEEK_V4_FLASH/346/226/275/345/267/245/346/226/207/346/241/243.md +234 -0
- package/docs/EVIDENCE_GUIDE.md +96 -0
- package/docs/MCP.md +95 -0
- package/docs/PARTNERS.md +41 -0
- package/docs/PROJECT_STATE.md +96 -0
- package/docs/RELEASING.md +124 -0
- package/docs/ROADMAP.md +191 -0
- package/docs/SETUP.md +103 -0
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/docs/images/weflow-architecture.png +0 -0
- package/docs/images/weflow-architecture.svg +65 -0
- package/mcp-server/index.ts +169 -269
- package/package.json +81 -72
- package/requirements-3x.txt +4 -0
- package/requirements-voice.txt +15 -0
- package/requirements.txt +9 -0
- package/resources/js/marked.min.js +69 -0
- package/resources/wechat-emoji/666.png +0 -0
- package/resources/wechat-emoji/Awesome.png +0 -0
- package/resources/wechat-emoji/Concerned.png +0 -0
- package/resources/wechat-emoji/Cry.png +0 -0
- package/resources/wechat-emoji/Emm.png +0 -0
- package/resources/wechat-emoji/Facepalm.png +0 -0
- package/resources/wechat-emoji/Grin.png +0 -0
- package/resources/wechat-emoji/OK.png +0 -0
- package/resources/wechat-emoji/Respect.png +0 -0
- package/resources/wechat-emoji/Sick.png +0 -0
- package/resources/wechat-emoji/Sleep.png +0 -0
- package/resources/wechat-emoji/Smile.png +0 -0
- package/resources/wechat-emoji//344/272/262/344/272/262.png +0 -0
- package/resources/wechat-emoji//344/276/277/344/276/277.png +0 -0
- package/resources/wechat-emoji//345/201/267/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/202/262/346/205/242.png +0 -0
- package/resources/wechat-emoji//345/206/215/350/247/201.png +0 -0
- package/resources/wechat-emoji//345/207/213/350/260/242.png +0 -0
- package/resources/wechat-emoji//345/212/240/346/262/271.png +0 -0
- package/resources/wechat-emoji//345/213/276/345/274/225.png +0 -0
- package/resources/wechat-emoji//345/217/221/345/221/206.png +0 -0
- package/resources/wechat-emoji//345/217/221/346/200/222.png +0 -0
- package/resources/wechat-emoji//345/217/221/346/212/226.png +0 -0
- package/resources/wechat-emoji//345/217/257/346/200/234.png +0 -0
- package/resources/wechat-emoji//345/217/263/345/223/274/345/223/274.png +0 -0
- package/resources/wechat-emoji//345/217/271/346/260/224.png +0 -0
- package/resources/wechat-emoji//345/220/203/347/223/234.png +0 -0
- package/resources/wechat-emoji//345/220/210/345/215/201.png +0 -0
- package/resources/wechat-emoji//345/220/220.png +0 -0
- package/resources/wechat-emoji//345/221/262/347/211/231.png +0 -0
- package/resources/wechat-emoji//345/222/222/351/252/202.png +0 -0
- package/resources/wechat-emoji//345/222/226/345/225/241.png +0 -0
- package/resources/wechat-emoji//345/223/207.png +0 -0
- package/resources/wechat-emoji//345/225/244/351/205/222.png +0 -0
- package/resources/wechat-emoji//345/230/230.png +0 -0
- package/resources/wechat-emoji//345/230/264/345/224/207.png +0 -0
- package/resources/wechat-emoji//345/230/277/345/223/210.png +0 -0
- package/resources/wechat-emoji//345/233/247.png +0 -0
- package/resources/wechat-emoji//345/233/260.png +0 -0
- package/resources/wechat-emoji//345/235/217/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/244/247/345/223/255.png +0 -0
- package/resources/wechat-emoji//345/244/251/345/225/212.png +0 -0
- package/resources/wechat-emoji//345/244/252/351/230/263.png +0 -0
- package/resources/wechat-emoji//345/244/261/346/234/233.png +0 -0
- package/resources/wechat-emoji//345/245/270/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/245/275/347/232/204.png +0 -0
- package/resources/wechat-emoji//345/247/224/345/261/210.png +0 -0
- package/resources/wechat-emoji//345/256/263/347/276/236.png +0 -0
- package/resources/wechat-emoji//345/260/264/345/260/254.png +0 -0
- package/resources/wechat-emoji//345/272/206/347/245/235.png +0 -0
- package/resources/wechat-emoji//345/274/261.png +0 -0
- package/resources/wechat-emoji//345/274/272.png +0 -0
- package/resources/wechat-emoji//345/276/227/346/204/217.png +0 -0
- package/resources/wechat-emoji//345/276/256/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/277/203/347/242/216.png +0 -0
- package/resources/wechat-emoji//345/277/253/345/223/255/344/272/206.png +0 -0
- package/resources/wechat-emoji//346/201/220/346/203/247.png +0 -0
- package/resources/wechat-emoji//346/202/240/351/227/262.png +0 -0
- package/resources/wechat-emoji//346/203/212/346/201/220.png +0 -0
- package/resources/wechat-emoji//346/203/212/350/256/266.png +0 -0
- package/resources/wechat-emoji//346/204/211/345/277/253.png +0 -0
- package/resources/wechat-emoji//346/206/250/347/254/221.png +0 -0
- package/resources/wechat-emoji//346/211/223/350/204/270.png +0 -0
- package/resources/wechat-emoji//346/212/223/347/213/202.png +0 -0
- package/resources/wechat-emoji//346/212/240/351/274/273.png +0 -0
- package/resources/wechat-emoji//346/212/261/346/213/263.png +0 -0
- package/resources/wechat-emoji//346/213/245/346/212/261.png +0 -0
- package/resources/wechat-emoji//346/213/263/345/244/264.png +0 -0
- package/resources/wechat-emoji//346/215/202/350/204/270.png +0 -0
- package/resources/wechat-emoji//346/217/241/346/211/213.png +0 -0
- package/resources/wechat-emoji//346/222/207/345/230/264.png +0 -0
- package/resources/wechat-emoji//346/223/246/346/261/227.png +0 -0
- package/resources/wechat-emoji//346/225/262/346/211/223.png +0 -0
- package/resources/wechat-emoji//346/227/240/350/257/255.png +0 -0
- package/resources/wechat-emoji//346/227/272/346/237/264.png +0 -0
- package/resources/wechat-emoji//346/231/225.png +0 -0
- package/resources/wechat-emoji//346/234/210/344/272/256.png +0 -0
- package/resources/wechat-emoji//346/234/272/346/231/272.png +0 -0
- package/resources/wechat-emoji//346/261/227.png +0 -0
- package/resources/wechat-emoji//346/265/201/346/263/252.png +0 -0
- package/resources/wechat-emoji//347/202/270/345/274/271.png +0 -0
- package/resources/wechat-emoji//347/203/237/350/212/261.png +0 -0
- package/resources/wechat-emoji//347/210/206/347/253/271.png +0 -0
- package/resources/wechat-emoji//347/210/261/345/277/203.png +0 -0
- package/resources/wechat-emoji//347/214/252/345/244/264.png +0 -0
- package/resources/wechat-emoji//347/216/253/347/221/260.png +0 -0
- package/resources/wechat-emoji//347/224/237/347/227/205.png +0 -0
- package/resources/wechat-emoji//347/226/221/351/227/256.png +0 -0
- package/resources/wechat-emoji//347/231/274.png +0 -0
- package/resources/wechat-emoji//347/231/275/347/234/274.png +0 -0
- package/resources/wechat-emoji//347/232/261/347/234/211.png +0 -0
- package/resources/wechat-emoji//347/235/241.png +0 -0
- package/resources/wechat-emoji//347/240/264/346/266/225/344/270/272/347/254/221.png +0 -0
- package/resources/wechat-emoji//347/244/274/347/211/251.png +0 -0
- package/resources/wechat-emoji//347/244/276/344/274/232/347/244/276/344/274/232.png +0 -0
- package/resources/wechat-emoji//347/246/217.png +0 -0
- package/resources/wechat-emoji//347/254/221/350/204/270.png +0 -0
- package/resources/wechat-emoji//347/272/242/345/214/205.png +0 -0
- package/resources/wechat-emoji//347/277/273/347/231/275/347/234/274.png +0 -0
- package/resources/wechat-emoji//350/200/266.png +0 -0
- package/resources/wechat-emoji//350/203/234/345/210/251.png +0 -0
- package/resources/wechat-emoji//350/204/270/347/272/242.png +0 -0
- package/resources/wechat-emoji//350/211/262.png +0 -0
- package/resources/wechat-emoji//350/213/246/346/266/251.png +0 -0
- package/resources/wechat-emoji//350/217/234/345/210/200.png +0 -0
- package/resources/wechat-emoji//350/233/213/347/263/225.png +0 -0
- package/resources/wechat-emoji//350/241/260.png +0 -0
- package/resources/wechat-emoji//350/243/202/345/274/200.png +0 -0
- package/resources/wechat-emoji//350/256/251/346/210/221/347/234/213/347/234/213.png +0 -0
- package/resources/wechat-emoji//350/260/203/347/232/256.png +0 -0
- package/resources/wechat-emoji//350/267/263/350/267/263.png +0 -0
- package/resources/wechat-emoji//350/275/254/345/234/210.png +0 -0
- package/resources/wechat-emoji//351/204/231/350/247/206.png +0 -0
- package/resources/wechat-emoji//351/227/255/345/230/264.png +0 -0
- package/resources/wechat-emoji//351/230/264/351/231/251.png +0 -0
- package/resources/wechat-emoji//351/232/276/350/277/207.png +0 -0
- package/resources/wechat-emoji//351/252/267/351/253/205.png +0 -0
- package/resources/wechat-emoji//351/274/223/346/216/214.png +0 -0
- package/scripts/_batch_link_sources.py +73 -73
- package/scripts/_utils.py +29 -0
- package/scripts/annual_report.py +796 -796
- package/scripts/auto_tag.py +128 -128
- package/scripts/biz_daily.py +210 -53
- package/scripts/chat_report.py +10 -0
- package/scripts/chat_stats.py +702 -702
- package/scripts/classify_daily.py +17 -11
- package/scripts/create_reading_notes.py +370 -370
- package/scripts/daily_stats.py +132 -0
- package/scripts/enrich_backlinks.py +163 -163
- package/scripts/export_chat_html.py +1704 -107
- package/scripts/extract_todos.py +445 -404
- package/scripts/fav_server.py +119 -12
- package/scripts/fix_topics.py +111 -111
- package/scripts/generate_ai_report.py +1 -1
- package/scripts/generate_html.py +597 -277
- package/scripts/inspection_records.py +354 -0
- package/scripts/mcp_bridge.py +398 -398
- package/scripts/nt_decrypt.py +702 -48
- package/scripts/pipeline.py +27 -19
- package/scripts/promote_all.py +312 -310
- package/scripts/promote_ideas.py +299 -297
- package/scripts/rag_chat.py +194 -191
- package/scripts/semantic_search.py +435 -431
- package/scripts/sync_fav.py +201 -146
- package/scripts/sync_weread.py +192 -192
- package/scripts/vault_rag.py +140 -139
- package/scripts/vault_search.py +141 -141
- package/scripts/wechat_emoji.py +132 -0
- package/scripts/wechat_emoticon.py +433 -0
- package/scripts/wechat_image.py +258 -0
- package/scripts/wechat_voice.py +327 -0
- package/src/core/dbPathService.ts +163 -18
- package/src/core/keyService.ts +137 -25
- package/src/core/ntCore.ts +215 -62
- package/src/core/sqlcipherCore.ts +17 -1
- package/src/core/wcdbCore.ts +3 -7
- package/src/services/assistantDaemon.ts +111 -0
- package/src/services/assistantMemory.ts +193 -0
- package/src/services/assistantPrivacy.ts +87 -0
- package/src/services/assistantRouting.ts +92 -0
- package/src/services/assistantService.ts +295 -0
- package/src/services/assistantTools.ts +549 -0
- package/src/services/chatService.ts +133 -4
- package/src/services/configService.ts +194 -6
- package/src/services/evidenceService.ts +129 -0
- package/src/services/exportService.ts +113 -48
- package/src/services/initKeyService.ts +83 -0
- package/src/services/messageContract.ts +77 -0
- package/src/services/messageQuery.ts +38 -0
- package/src/services/wechat-formatter.ts +348 -348
- package/src/services/wechatMessageService.ts +25 -3
- package/src/services/wereadService.ts +308 -308
- package/src/services/whitelistService.ts +209 -22
- package/src/types.ts +20 -0
- package/src/utils/dateRange.ts +58 -0
- package/src/utils/mcpSecurity.ts +39 -0
- package/src/utils/packageRoot.ts +14 -0
- package/src/utils/python.ts +73 -0
- package/src/utils/pythonProcessEnv.ts +45 -0
- package/src/utils/talkerUtils.ts +14 -2
- package/dist/src/utils/errors.d.ts +0 -25
- package/dist/src/utils/errors.d.ts.map +0 -1
- package/dist/src/utils/errors.js +0 -43
- package/dist/src/utils/errors.js.map +0 -1
- package/dist/src/utils/pythonRunner.d.ts +0 -27
- package/dist/src/utils/pythonRunner.d.ts.map +0 -1
- package/dist/src/utils/pythonRunner.js +0 -62
- package/dist/src/utils/pythonRunner.js.map +0 -1
- package/src/utils/errors.ts +0 -42
- package/src/utils/pythonRunner.ts +0 -81
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Turn a WeChat line-inspection dump into a table plus named photos.
|
|
3
|
+
|
|
4
|
+
An inspector posts photos to a chat and captions them, e.g.
|
|
5
|
+
|
|
6
|
+
[图片][图片]
|
|
7
|
+
清播线三津I支9-10号杆树障
|
|
8
|
+
|
|
9
|
+
so each caption is one finding: a location (`清播线三津I支9-10号杆`) and a
|
|
10
|
+
problem (`树障`). Photos precede their caption, and one caption can carry
|
|
11
|
+
several problems separated by `;`, in which case every problem gets its own
|
|
12
|
+
row and they share the location.
|
|
13
|
+
|
|
14
|
+
Photos are extracted at their **original** resolution. The HTML exporter caps
|
|
15
|
+
embedded media at 8MB because it inlines them into a page; inspection evidence
|
|
16
|
+
needs the real file, and phone originals run to 8000x6000 / 19MB, so the cap
|
|
17
|
+
must not apply here.
|
|
18
|
+
"""
|
|
19
|
+
import csv
|
|
20
|
+
import hashlib
|
|
21
|
+
import io
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
import struct
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
28
|
+
|
|
29
|
+
import export_chat_html as E
|
|
30
|
+
|
|
31
|
+
# Original files run well past the HTML exporter's inline limit.
|
|
32
|
+
PHOTO_MAX_BYTES = 64 * 1024 * 1024
|
|
33
|
+
|
|
34
|
+
# 线路名以「线」结尾;支线以「支」结尾;杆号形如 9-10号杆 / 23号 / #12
|
|
35
|
+
RE_LINE = re.compile(r'([一-龥]{2,6}线)')
|
|
36
|
+
RE_BRANCH = re.compile(r'([一-龥A-Za-z0-9]{2,20}?支)')
|
|
37
|
+
RE_POLE = re.compile(r'(\d+(?:\s*[-—~]\s*\d+)?\s*号杆|\d+\s*号(?=[^\d]|$)|#\d+(?:\s*[-—~]\s*#?\d+)?)')
|
|
38
|
+
# Location splitting needs the explicit form. The loose `12号` above also
|
|
39
|
+
# matches prose like `万科悦江南11、12号配变在1栋1单元负一楼`, which turned a
|
|
40
|
+
# descriptive paragraph into rows claiming pole numbers that do not exist.
|
|
41
|
+
RE_POLE_STRICT = re.compile(r'(\d+(?:\s*[-—~]\s*\d+)?\s*号杆|#\d+(?:\s*[-—~]\s*#?\d+)?)')
|
|
42
|
+
# Captions that narrate completed work or a whole-site audit rather than one
|
|
43
|
+
# finding at one location; they do not fit the location+problem shape.
|
|
44
|
+
RE_NARRATIVE = re.compile(r'(接收资产巡视|巡视情况|完成对|处理前后)')
|
|
45
|
+
RE_STATUS = re.compile(r'[((]([^))]{0,30}?(?:已处理|未处理|处理中|待处理)[^))]{0,30}?)[))]')
|
|
46
|
+
# A clause that is just a location, e.g. the bare `三津I支9号杆` seen before a caption.
|
|
47
|
+
RE_NUMBERED = re.compile(r'^\s*\d+\s*[、..]\s*')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _split_clauses(text):
|
|
51
|
+
"""One caption's problem clauses, with the numbered-list form unwrapped."""
|
|
52
|
+
parts = re.split(r'[;;]', text)
|
|
53
|
+
out = []
|
|
54
|
+
for part in parts:
|
|
55
|
+
stripped = part.strip()
|
|
56
|
+
if not stripped:
|
|
57
|
+
continue
|
|
58
|
+
# `1、…` / `2、…` list markers carry no meaning of their own.
|
|
59
|
+
out.append(RE_NUMBERED.sub('', stripped))
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _location_prefix(clause):
|
|
64
|
+
"""(location, remainder) - the location tokens at the head of a clause."""
|
|
65
|
+
end = 0
|
|
66
|
+
for pattern in (RE_BRANCH, RE_POLE_STRICT):
|
|
67
|
+
for match in pattern.finditer(clause):
|
|
68
|
+
# Only tokens before the first problem word count as location.
|
|
69
|
+
if match.start() > 24:
|
|
70
|
+
continue
|
|
71
|
+
end = max(end, match.end())
|
|
72
|
+
return clause[:end].strip(',,、 '), clause[end:].strip(',,、 ')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_records(messages, decode=E.decode_message_content):
|
|
76
|
+
"""[{time, local_type, text, photos:[local_id]}] for one conversation.
|
|
77
|
+
|
|
78
|
+
Photos accumulate and are attached to the caption that follows them, which
|
|
79
|
+
is how the inspector posts: the picture first, then what it shows.
|
|
80
|
+
"""
|
|
81
|
+
records = []
|
|
82
|
+
pending = []
|
|
83
|
+
for row in messages:
|
|
84
|
+
local_id = row[0] or 0
|
|
85
|
+
local_type = (row[2] or 0) & 0xffffffff
|
|
86
|
+
create_time = row[5] or 0
|
|
87
|
+
if local_type == 3:
|
|
88
|
+
pending.append(local_id)
|
|
89
|
+
continue
|
|
90
|
+
if local_type != 1:
|
|
91
|
+
continue
|
|
92
|
+
raw = row[8]
|
|
93
|
+
text = decode(raw) if isinstance(raw, (bytes, bytearray)) else str(raw or '')
|
|
94
|
+
# Group rows prefix the speaker; the record itself starts after it.
|
|
95
|
+
text = re.sub(r'^wxid_[A-Za-z0-9_]+:\s*', '', text).strip()
|
|
96
|
+
if not text or len(text) < 4:
|
|
97
|
+
continue
|
|
98
|
+
if not (RE_POLE.search(text) or RE_BRANCH.search(text) or RE_LINE.search(text)):
|
|
99
|
+
# Chatter between records. The photos stay pending rather than
|
|
100
|
+
# being dropped: a caption usually follows its pictures, so
|
|
101
|
+
# clearing here lost 63 of 101 photos in the sample conversation.
|
|
102
|
+
continue
|
|
103
|
+
records.append({
|
|
104
|
+
'time': create_time,
|
|
105
|
+
'text': text,
|
|
106
|
+
'photos': list(pending),
|
|
107
|
+
'local_id': local_id,
|
|
108
|
+
})
|
|
109
|
+
pending = []
|
|
110
|
+
return records
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _head_of(clause):
|
|
114
|
+
"""`清上线仁义支` - the line-and-branch a clause opens with, else ''.
|
|
115
|
+
|
|
116
|
+
Strict about position: a branch mentioned mid-sentence (`9号杆的仁义支09
|
|
117
|
+
刀闸`) is not the shared head, and treating it as one sends every later
|
|
118
|
+
clause to the wrong line.
|
|
119
|
+
"""
|
|
120
|
+
line = RE_LINE.match(clause)
|
|
121
|
+
if line:
|
|
122
|
+
branch = RE_BRANCH.match(clause, line.end())
|
|
123
|
+
return clause[:branch.end()] if branch else clause[:line.end()]
|
|
124
|
+
branch = RE_BRANCH.match(clause)
|
|
125
|
+
return clause[:branch.end()] if branch else ''
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def expand_findings(record):
|
|
129
|
+
"""One row per problem clause, carrying the caption's location context.
|
|
130
|
+
|
|
131
|
+
A caption like `清上线仁义支9-11号杆有树障…;9-10号杆…不足5米;9-13号杆杆号
|
|
132
|
+
牌模糊` names the line once and then lists problems against further poles.
|
|
133
|
+
Later clauses inherit that line-and-branch prefix; without it they arrive
|
|
134
|
+
with an empty location and cannot be attributed to anything.
|
|
135
|
+
"""
|
|
136
|
+
rows = []
|
|
137
|
+
head = ''
|
|
138
|
+
# Judged once for the whole caption: a site-by-site audit opens with the
|
|
139
|
+
# marker but only in its first clause, and the rest would otherwise be
|
|
140
|
+
# forced into location+problem rows with invented pole numbers.
|
|
141
|
+
record_narrative = bool(RE_NARRATIVE.search(record['text']))
|
|
142
|
+
for clause in _split_clauses(record['text']):
|
|
143
|
+
narrative = record_narrative
|
|
144
|
+
own = '' if narrative else _head_of(clause)
|
|
145
|
+
if own:
|
|
146
|
+
head = own
|
|
147
|
+
if narrative:
|
|
148
|
+
location, problem = head, clause
|
|
149
|
+
else:
|
|
150
|
+
location, problem = _location_prefix(clause)
|
|
151
|
+
if head and not own:
|
|
152
|
+
location = head + location
|
|
153
|
+
problem = problem or clause[len(location) - len(head):].strip(',,、 ')
|
|
154
|
+
location = location or clause
|
|
155
|
+
if not problem:
|
|
156
|
+
problem = clause[len(location):].strip(',,、 ') or clause
|
|
157
|
+
|
|
158
|
+
line = RE_LINE.match(head or location)
|
|
159
|
+
branch = RE_BRANCH.match(head or location, line.end()) if line else RE_BRANCH.match(head or location)
|
|
160
|
+
pole = None if narrative else (RE_POLE_STRICT.search(clause) or RE_POLE.search(clause))
|
|
161
|
+
status = RE_STATUS.search(clause)
|
|
162
|
+
rows.append({
|
|
163
|
+
'time': record['time'],
|
|
164
|
+
'line': line.group(1) if line else '',
|
|
165
|
+
'branch': branch.group(1) if branch else '',
|
|
166
|
+
'pole': pole.group(1).replace(' ', '') if pole else '',
|
|
167
|
+
'location': location,
|
|
168
|
+
'problem': problem,
|
|
169
|
+
'status': status.group(1) if status else '',
|
|
170
|
+
'kind': '叙述' if narrative else ('多问题' if len(_split_clauses(record['text'])) > 1 else '单条'),
|
|
171
|
+
'photos': record['photos'],
|
|
172
|
+
'source': record['text'],
|
|
173
|
+
})
|
|
174
|
+
return rows
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def decode_photo(path, v2_key):
|
|
178
|
+
"""Full-resolution image bytes from a WeChat V2 container, or None.
|
|
179
|
+
|
|
180
|
+
Same container as the exporter decodes, without the size ceiling that
|
|
181
|
+
exists purely to bound inlined HTML.
|
|
182
|
+
"""
|
|
183
|
+
from Crypto.Cipher import AES
|
|
184
|
+
from Crypto.Util import Padding
|
|
185
|
+
try:
|
|
186
|
+
with open(path, 'rb') as handle:
|
|
187
|
+
data = handle.read(PHOTO_MAX_BYTES + 1)
|
|
188
|
+
if len(data) > PHOTO_MAX_BYTES or not data.startswith(E.V2_MAGIC):
|
|
189
|
+
return None
|
|
190
|
+
signature, aes_size, xor_size = struct.unpack('<6sLLx', data[:E.V2_CIPHERTEXT_START])
|
|
191
|
+
if signature != E.V2_MAGIC:
|
|
192
|
+
return None
|
|
193
|
+
encrypted_size = aes_size + 16 - aes_size % 16
|
|
194
|
+
encrypted = data[E.V2_CIPHERTEXT_START:E.V2_CIPHERTEXT_START + encrypted_size]
|
|
195
|
+
decrypted = Padding.unpad(AES.new(v2_key[1], AES.MODE_ECB).decrypt(encrypted), 16)
|
|
196
|
+
remainder = data[E.V2_CIPHERTEXT_START + encrypted_size:]
|
|
197
|
+
if xor_size:
|
|
198
|
+
if xor_size > len(remainder):
|
|
199
|
+
return None
|
|
200
|
+
raw = remainder[:-xor_size]
|
|
201
|
+
tail = bytes(value ^ v2_key[0] for value in remainder[-xor_size:])
|
|
202
|
+
else:
|
|
203
|
+
raw, tail = remainder, b''
|
|
204
|
+
output = decrypted + raw + tail
|
|
205
|
+
return output if E.detect_mime_from_bytes(output[:16]) else None
|
|
206
|
+
except Exception:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def photo_paths(account_dir, talker, resource_md5s):
|
|
211
|
+
"""Local candidate files for one message, largest variant first."""
|
|
212
|
+
md5_dir = hashlib.md5(talker.encode()).hexdigest()
|
|
213
|
+
root = os.path.join(account_dir, 'msg', 'attach', md5_dir)
|
|
214
|
+
found = []
|
|
215
|
+
if not os.path.isdir(root):
|
|
216
|
+
return found
|
|
217
|
+
wanted = {}
|
|
218
|
+
for media_md5 in resource_md5s:
|
|
219
|
+
for suffix in ('_h.dat', '.dat'):
|
|
220
|
+
wanted[(media_md5 + suffix).lower()] = len(found)
|
|
221
|
+
for current, _, files in os.walk(root):
|
|
222
|
+
for name in files:
|
|
223
|
+
if name.lower() in wanted:
|
|
224
|
+
found.append((name.lower(), os.path.join(current, name)))
|
|
225
|
+
# `_h.dat` (original) before `.dat` (mid), then by file size.
|
|
226
|
+
found.sort(key=lambda item: (not item[0].endswith('_h.dat'), -os.path.getsize(item[1])))
|
|
227
|
+
return [path for _, path in found]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def sanitize(name):
|
|
231
|
+
"""A filename-safe form of a location string."""
|
|
232
|
+
cleaned = re.sub(r'[\\/:*?"<>|\s]+', '_', str(name or '')).strip('_')
|
|
233
|
+
return cleaned[:60] or 'unknown'
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def extract_photos(rows, account_dir, talker, resource_map, output_dir, own_wxid='', log=print):
|
|
237
|
+
"""Write each row's photos at full resolution, named after its location.
|
|
238
|
+
|
|
239
|
+
Returns (written, missing) where `missing` lists rows whose photos could
|
|
240
|
+
not be decoded - typically because WeChat never downloaded the original,
|
|
241
|
+
which is worth reporting rather than silently substituting a thumbnail.
|
|
242
|
+
"""
|
|
243
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
244
|
+
v2_key = E.resolve_v2_media_key(account_dir, own_wxid)
|
|
245
|
+
written = []
|
|
246
|
+
missing = []
|
|
247
|
+
# One caption's photos belong to every row split out of it, so cache by
|
|
248
|
+
# media md5 - otherwise a 3-photo, 4-clause caption writes 12 copies.
|
|
249
|
+
per_media = {}
|
|
250
|
+
for index, row in enumerate(rows, 1):
|
|
251
|
+
produced = []
|
|
252
|
+
for media_md5 in row.get('resource_md5s', []):
|
|
253
|
+
if media_md5 in per_media:
|
|
254
|
+
produced.extend(per_media[media_md5])
|
|
255
|
+
continue
|
|
256
|
+
names = []
|
|
257
|
+
paths = photo_paths(account_dir, talker, [media_md5])
|
|
258
|
+
for slot, path in enumerate(paths, 1):
|
|
259
|
+
data = decode_photo(path, v2_key)
|
|
260
|
+
if not data:
|
|
261
|
+
continue
|
|
262
|
+
mime = E.detect_mime_from_bytes(data[:16])
|
|
263
|
+
if not mime:
|
|
264
|
+
continue
|
|
265
|
+
extension = {'image/jpeg': '.jpg', 'image/png': '.png',
|
|
266
|
+
'image/gif': '.gif', 'image/webp': '.webp'}.get(mime, '.jpg')
|
|
267
|
+
# The media md5, not a per-photo counter: counters restart at 1
|
|
268
|
+
# for every md5 within a row, so two photos of the same
|
|
269
|
+
# location collided and overwrote each other (101 -> 38 files).
|
|
270
|
+
name = f"{index:03d}_{sanitize(row['location'])}_{media_md5[:8]}{extension}"
|
|
271
|
+
target = os.path.join(output_dir, name)
|
|
272
|
+
if not os.path.exists(target):
|
|
273
|
+
with open(target, 'wb') as handle:
|
|
274
|
+
handle.write(data)
|
|
275
|
+
names.append(name)
|
|
276
|
+
written.append(target)
|
|
277
|
+
if names:
|
|
278
|
+
per_media[media_md5] = names
|
|
279
|
+
produced.extend(names)
|
|
280
|
+
row['photo_files'] = produced
|
|
281
|
+
if not produced and row.get('resource_md5s'):
|
|
282
|
+
missing.append((index, row['location']))
|
|
283
|
+
if index % 20 == 0:
|
|
284
|
+
log(f' 照片 {index}/{len(rows)}')
|
|
285
|
+
return written, missing
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def write_table(rows, output_dir, name='巡视记录'):
|
|
289
|
+
"""CSV plus an .xlsx when ExcelJS-equivalent tooling is available."""
|
|
290
|
+
columns = [('time', '时间'), ('line', '线路'), ('branch', '支线'), ('pole', '杆号'),
|
|
291
|
+
('location', '地点'), ('problem', '问题'), ('status', '状态'), ('kind', '类型'),
|
|
292
|
+
('photo_count', '照片数'), ('photo_files', '照片文件'), ('source', '原始描述')]
|
|
293
|
+
csv_path = os.path.join(output_dir, f'{name}.csv')
|
|
294
|
+
with open(csv_path, 'w', encoding='utf-8-sig', newline='') as handle:
|
|
295
|
+
writer = csv.writer(handle)
|
|
296
|
+
writer.writerow([label for _, label in columns])
|
|
297
|
+
for row in rows:
|
|
298
|
+
import datetime
|
|
299
|
+
record = dict(row)
|
|
300
|
+
record['time'] = datetime.datetime.fromtimestamp(row['time']).strftime('%Y-%m-%d %H:%M:%S')
|
|
301
|
+
record['photo_count'] = len(row.get('photo_files', []))
|
|
302
|
+
record['photo_files'] = '|'.join(row.get('photo_files', []))
|
|
303
|
+
writer.writerow([record.get(key, '') for key, _ in columns])
|
|
304
|
+
return csv_path
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _main():
|
|
308
|
+
import argparse
|
|
309
|
+
import json
|
|
310
|
+
parser = argparse.ArgumentParser(description='把微信里的线路巡视记录整理成表格和照片')
|
|
311
|
+
parser.add_argument('--env', required=True, help='含 db/key/salt/pass/wxid 与账号目录的 JSON')
|
|
312
|
+
parser.add_argument('--talker', required=True)
|
|
313
|
+
parser.add_argument('--account-dir', required=True)
|
|
314
|
+
parser.add_argument('--out', required=True)
|
|
315
|
+
args = parser.parse_args()
|
|
316
|
+
|
|
317
|
+
config = json.load(open(args.env, encoding='utf-8'))
|
|
318
|
+
messages = E.fetch_messages_from_shards(
|
|
319
|
+
config['db'], config['key'], config['salt'], args.talker, '', config['pass'])
|
|
320
|
+
resource_map = E.load_resource_media_map(
|
|
321
|
+
args.account_dir, config['key'], config['salt'], messages, {}, config['pass'])
|
|
322
|
+
|
|
323
|
+
# local_id -> resource md5, so a photo can be found from its message.
|
|
324
|
+
by_local = {}
|
|
325
|
+
for key, values in resource_map.items():
|
|
326
|
+
if key.startswith('server:'):
|
|
327
|
+
continue
|
|
328
|
+
by_local.setdefault(key, []).extend(values)
|
|
329
|
+
for row in messages:
|
|
330
|
+
md5s = list(resource_map.get(f'server:{int(row[1] or 0)}', []))
|
|
331
|
+
if md5s:
|
|
332
|
+
by_local[f'local:{row[0]}'] = md5s
|
|
333
|
+
|
|
334
|
+
records = parse_records(messages)
|
|
335
|
+
rows = []
|
|
336
|
+
for record in records:
|
|
337
|
+
for finding in expand_findings(record):
|
|
338
|
+
finding['resource_md5s'] = []
|
|
339
|
+
for local_id in finding['photos']:
|
|
340
|
+
finding['resource_md5s'].extend(by_local.get(f'local:{local_id}', []))
|
|
341
|
+
rows.append(finding)
|
|
342
|
+
|
|
343
|
+
print(f'解析出 {len(records)} 条记录 -> {len(rows)} 行')
|
|
344
|
+
written, missing = extract_photos(
|
|
345
|
+
rows, args.account_dir, args.talker, by_local,
|
|
346
|
+
os.path.join(args.out, '照片'), own_wxid=config.get('wxid', ''))
|
|
347
|
+
table = write_table(rows, args.out)
|
|
348
|
+
print(json.dumps({'success': True, 'records': len(records), 'rows': len(rows),
|
|
349
|
+
'photos': len(written), 'missing': len(missing),
|
|
350
|
+
'table': table}, ensure_ascii=False))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == '__main__':
|
|
354
|
+
_main()
|