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
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { join
|
|
2
|
-
import { fileURLToPath } from 'url'
|
|
1
|
+
import { join } from 'path'
|
|
3
2
|
import { existsSync } from 'fs'
|
|
4
3
|
import { WcdbCore } from '../core/wcdbCore.js'
|
|
5
4
|
import { SqlcipherCore } from '../core/sqlcipherCore.js'
|
|
6
5
|
import { NtCore } from '../core/ntCore.js'
|
|
7
6
|
import { configService } from './configService.js'
|
|
8
7
|
import type { ChatSession, Message, Contact, DataVersion } from '../types.js'
|
|
8
|
+
import { collectMessagesInRange } from './messageQuery.js'
|
|
9
|
+
import { resolvePackageRoot } from '../utils/packageRoot.js'
|
|
9
10
|
|
|
10
11
|
// 创建单例
|
|
11
12
|
const wcdbCore = new WcdbCore()
|
|
@@ -57,8 +58,10 @@ export class ChatService {
|
|
|
57
58
|
const dbPath = configService.get('dbPath')
|
|
58
59
|
const wxid = configService.get('wxid')
|
|
59
60
|
const decryptKey = configService.get('decryptKey')
|
|
61
|
+
const ntConfigured = !!(configService.get('ntDbPath') && configService.get('ntKey'))
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
// NT 配置完整时无需 decryptKey(非 Windows 平台密钥自动提取不可用,仅有 NT 密钥)
|
|
64
|
+
if (!dbPath || (!decryptKey && !ntConfigured)) {
|
|
62
65
|
return { success: false, error: '4.x 配置不完整,请运行 weflow-cli init' }
|
|
63
66
|
}
|
|
64
67
|
|
|
@@ -112,7 +115,7 @@ export class ChatService {
|
|
|
112
115
|
|
|
113
116
|
// 方案 4: 尝试 WCDB API 连接 (需要 db_storage/session.db 结构,仅 3.x 兼容)
|
|
114
117
|
// Use package root instead of cwd for global install support
|
|
115
|
-
const pkgRoot =
|
|
118
|
+
const pkgRoot = resolvePackageRoot(import.meta.url)
|
|
116
119
|
const resourcesPath = join(pkgRoot, 'resources')
|
|
117
120
|
wcdbCore.setPaths(resourcesPath, '')
|
|
118
121
|
|
|
@@ -236,6 +239,22 @@ export class ChatService {
|
|
|
236
239
|
return []
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
async getMessagesInRange(
|
|
243
|
+
talker: string,
|
|
244
|
+
limit = 0,
|
|
245
|
+
from?: number,
|
|
246
|
+
to?: number,
|
|
247
|
+
): Promise<Message[]> {
|
|
248
|
+
if (from === undefined && to === undefined) {
|
|
249
|
+
return this.getMessages(talker, limit)
|
|
250
|
+
}
|
|
251
|
+
return collectMessagesInRange(
|
|
252
|
+
(pageLimit, offset) => this.getMessages(talker, pageLimit, offset),
|
|
253
|
+
limit,
|
|
254
|
+
{ from, to },
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
239
258
|
async listContacts(keyword?: string, limit = 200): Promise<Contact[]> {
|
|
240
259
|
const conn = await this.connect()
|
|
241
260
|
if (!conn.success) return []
|
|
@@ -291,6 +310,116 @@ export class ChatService {
|
|
|
291
310
|
this.activeVersion = null
|
|
292
311
|
this.useRawSqlite4x = false
|
|
293
312
|
}
|
|
313
|
+
|
|
314
|
+
// ====== SNS (朋友圈本地缓存查询) ======
|
|
315
|
+
// 4.x 支持两条路径: WCDB API 或 NT (Python sqlcipher3)
|
|
316
|
+
|
|
317
|
+
/** 当前连接是否支持朋友圈查询 */
|
|
318
|
+
isSnsSupported(): boolean {
|
|
319
|
+
if (!this.connected || this.activeVersion !== '4.x') return false
|
|
320
|
+
// 路径 1: WCDB API (传统 4.x)
|
|
321
|
+
if (!this.useRawSqlite4x && !this.ntCore) return true
|
|
322
|
+
// 路径 2: NT + sns config 已配置
|
|
323
|
+
if (this.ntCore) {
|
|
324
|
+
const snsPath = configService.get('snsDbPath')
|
|
325
|
+
const snsKey = configService.get('snsKey')
|
|
326
|
+
const snsSalt = configService.get('snsSalt')
|
|
327
|
+
return !!(snsPath && snsKey && snsSalt)
|
|
328
|
+
}
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async getSnsTimeline(opts: {
|
|
333
|
+
limit?: number
|
|
334
|
+
offset?: number
|
|
335
|
+
usernames?: string[]
|
|
336
|
+
keyword?: string
|
|
337
|
+
startTime?: number
|
|
338
|
+
endTime?: number
|
|
339
|
+
} = {}): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
|
|
340
|
+
if (!this.isSnsSupported()) {
|
|
341
|
+
return { success: false, error: '当前数据通道不支持朋友圈查询 (需 4.x WCDB 或 NT+sns 配置)' }
|
|
342
|
+
}
|
|
343
|
+
// NT 路径
|
|
344
|
+
if (this.ntCore) {
|
|
345
|
+
const snsPath = configService.get('snsDbPath')
|
|
346
|
+
const snsKey = configService.get('snsKey')
|
|
347
|
+
const snsSalt = configService.get('snsSalt')
|
|
348
|
+
return this.ntCore.getSnsTimeline(snsPath, snsKey, snsSalt, opts)
|
|
349
|
+
}
|
|
350
|
+
// WCDB 路径
|
|
351
|
+
return wcdbCore.getSnsTimeline(
|
|
352
|
+
opts.limit ?? 20,
|
|
353
|
+
opts.offset ?? 0,
|
|
354
|
+
opts.usernames,
|
|
355
|
+
opts.keyword,
|
|
356
|
+
opts.startTime,
|
|
357
|
+
opts.endTime,
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async getSnsUsernames(): Promise<{ success: boolean; usernames?: string[]; error?: string }> {
|
|
362
|
+
if (!this.isSnsSupported()) {
|
|
363
|
+
return { success: false, error: '当前数据通道不支持朋友圈查询 (需 4.x WCDB 或 NT+sns 配置)' }
|
|
364
|
+
}
|
|
365
|
+
if (this.ntCore) {
|
|
366
|
+
const snsPath = configService.get('snsDbPath')
|
|
367
|
+
const snsKey = configService.get('snsKey')
|
|
368
|
+
const snsSalt = configService.get('snsSalt')
|
|
369
|
+
return this.ntCore.getSnsUsernames(snsPath, snsKey, snsSalt)
|
|
370
|
+
}
|
|
371
|
+
return wcdbCore.getSnsUsernames()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async getSnsExportStats(myWxid?: string): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
|
|
375
|
+
if (!this.isSnsSupported()) {
|
|
376
|
+
return { success: false, error: '当前数据通道不支持朋友圈查询 (需 4.x WCDB 或 NT+sns 配置)' }
|
|
377
|
+
}
|
|
378
|
+
if (this.ntCore) {
|
|
379
|
+
const snsPath = configService.get('snsDbPath')
|
|
380
|
+
const snsKey = configService.get('snsKey')
|
|
381
|
+
const snsSalt = configService.get('snsSalt')
|
|
382
|
+
return this.ntCore.getSnsExportStats(snsPath, snsKey, snsSalt, myWxid)
|
|
383
|
+
}
|
|
384
|
+
return wcdbCore.getSnsExportStats(myWxid)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ====== 收藏 (favorite.db, 仅 NT 路径) ======
|
|
388
|
+
|
|
389
|
+
/** 当前连接是否支持收藏查询 */
|
|
390
|
+
isFavSupported(): boolean {
|
|
391
|
+
if (!this.connected || this.activeVersion !== '4.x' || !this.ntCore) return false
|
|
392
|
+
const favPath = configService.get('favDbPath')
|
|
393
|
+
const favKey = configService.get('favKey')
|
|
394
|
+
const favPass = configService.get('favPassphrase')
|
|
395
|
+
return !!(favPath && (favKey || favPass))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 解析收藏密钥: 优先 favKey, 其次从 favPassphrase + 库 salt 派生 */
|
|
399
|
+
private resolveFavKey(): string | null {
|
|
400
|
+
const favKey = configService.get('favKey')
|
|
401
|
+
if (favKey) return favKey
|
|
402
|
+
const favPass = configService.get('favPassphrase')
|
|
403
|
+
const favPath = configService.get('favDbPath')
|
|
404
|
+
if (!favPass || !favPath) return null
|
|
405
|
+
const salt = NtCore.readDbSalt(favPath)
|
|
406
|
+
if (!salt) return null
|
|
407
|
+
const derived = NtCore.deriveRawKey(favPass, salt)
|
|
408
|
+
configService.set('favKey', derived)
|
|
409
|
+
return derived
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async getFavorites(opts: {
|
|
413
|
+
limit?: number; offset?: number; keyword?: string; favType?: number;
|
|
414
|
+
} = {}): Promise<{ success: boolean; favorites?: any[]; total?: number; error?: string }> {
|
|
415
|
+
if (!this.isFavSupported()) {
|
|
416
|
+
return { success: false, error: '当前数据通道不支持收藏查询 (需 4.x NT 连接 + fav 配置)' }
|
|
417
|
+
}
|
|
418
|
+
const favPath = configService.get('favDbPath')
|
|
419
|
+
const favKey = this.resolveFavKey()
|
|
420
|
+
if (!favKey) return { success: false, error: '收藏密钥解析失败 (favKey/favPassphrase 配置无效)' }
|
|
421
|
+
return this.ntCore!.getFavorites(favPath, favKey, opts)
|
|
422
|
+
}
|
|
294
423
|
}
|
|
295
424
|
|
|
296
425
|
export const chatService = new ChatService()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'path'
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs'
|
|
3
3
|
import { homedir, hostname, userInfo } from 'os'
|
|
4
4
|
import crypto from 'crypto'
|
|
5
5
|
import { expandHomePath } from '../utils/pathUtils.js'
|
|
@@ -25,8 +25,16 @@ interface CliConfig {
|
|
|
25
25
|
wechatOcAccountId: string
|
|
26
26
|
wechatOcBaseUrl: string
|
|
27
27
|
wechatOcSyncBuf: string
|
|
28
|
+
/** 持久化的 context_token: wxid -> token (加密 JSON 字符串) */
|
|
29
|
+
wechatOcContextTokens: string
|
|
28
30
|
// 白名单
|
|
29
31
|
whitelist: string[]
|
|
32
|
+
// 黑名单 (绝对禁止收发)
|
|
33
|
+
blacklist: string[]
|
|
34
|
+
/** 白名单带昵称: {wxid, displayName, addedAt}[] */
|
|
35
|
+
whitelistEntries: ListEntry[]
|
|
36
|
+
/** 黑名单带昵称: {wxid, displayName, addedAt, reason?}[] */
|
|
37
|
+
blacklistEntries: ListEntry[]
|
|
30
38
|
// Vault & Pipeline
|
|
31
39
|
vaultRepo: string
|
|
32
40
|
aiEngine: string
|
|
@@ -34,19 +42,58 @@ interface CliConfig {
|
|
|
34
42
|
wereadApiKey: string
|
|
35
43
|
// AI 引擎 API Key
|
|
36
44
|
deepseekApiKey: string
|
|
45
|
+
// 朋友圈 (SNS) 数据库
|
|
46
|
+
snsDbPath: string
|
|
47
|
+
snsKey: string
|
|
48
|
+
snsSalt: string
|
|
49
|
+
// 收藏 (favorite.db)
|
|
50
|
+
favDbPath: string
|
|
51
|
+
favKey: string
|
|
52
|
+
emoticonSeed: string
|
|
53
|
+
favPassphrase: string
|
|
54
|
+
// 第二大脑助手
|
|
55
|
+
/** 隐私模式: open | balanced | strict,默认 strict */
|
|
56
|
+
assistantPrivacy: string
|
|
57
|
+
/** 本地推理引擎模型名 (ollama/lmstudio) */
|
|
58
|
+
localModel: string
|
|
59
|
+
/** 自定义 AI 端点 (OpenAI 兼容中转站); 设置后云端引擎走此地址 */
|
|
60
|
+
aiBaseUrl: string
|
|
61
|
+
/** 自定义端点使用的模型名 */
|
|
62
|
+
aiModel: string
|
|
63
|
+
/** 助手白名单 (@im.wechat ID, 逗号/空格分隔); 空 = 拒绝所有人 */
|
|
64
|
+
assistantWhitelist: string
|
|
65
|
+
/** 官方通道明确提供群 ID 时才使用;空 = 拒绝所有群 */
|
|
66
|
+
assistantGroupWhitelist: string
|
|
67
|
+
/** 群内是否必须明确 @ 机器人;默认开启 */
|
|
68
|
+
assistantGroupRequireMention: string
|
|
69
|
+
dailySources: string
|
|
70
|
+
/** 公众号类别映射,JSON 对象:公众号名称或 gh_ ID -> 类别 */
|
|
71
|
+
dailySourceCategories: string
|
|
72
|
+
dailyAiEnabled: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ListEntry {
|
|
76
|
+
wxid: string
|
|
77
|
+
displayName?: string
|
|
78
|
+
addedAt?: number
|
|
79
|
+
reason?: string
|
|
37
80
|
}
|
|
38
81
|
|
|
39
82
|
/** 需要加密存储的字段 */
|
|
40
83
|
const ENCRYPTED_KEYS: ReadonlySet<keyof CliConfig> = new Set([
|
|
41
84
|
'decryptKey', 'decryptKey3x', 'ntKey', 'contactKey',
|
|
42
|
-
'wechatOcToken', 'wereadApiKey', 'deepseekApiKey',
|
|
85
|
+
'wechatOcToken', 'wereadApiKey', 'deepseekApiKey', 'snsKey',
|
|
86
|
+
'favKey', 'favPassphrase',
|
|
43
87
|
])
|
|
44
88
|
|
|
45
89
|
const CONFIG_DIR = join(homedir(), '.weflow-cli')
|
|
46
90
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
47
91
|
|
|
48
92
|
export class ConfigService {
|
|
49
|
-
private config: CliConfig = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', whitelist: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '' }
|
|
93
|
+
private config: CliConfig = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', wechatOcContextTokens: '', whitelist: [], blacklist: [], whitelistEntries: [], blacklistEntries: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '', snsDbPath: '', snsKey: '', snsSalt: '', favDbPath: '', favKey: '', favPassphrase: '', emoticonSeed: '', assistantPrivacy: 'strict', localModel: '', aiBaseUrl: '', aiModel: '', assistantWhitelist: '', assistantGroupWhitelist: '', assistantGroupRequireMention: 'true', dailySources: '', dailySourceCategories: '', dailyAiEnabled: 'true' }
|
|
94
|
+
|
|
95
|
+
/** 本进程修改过、待回写的字段 (多进程并发写保护) */
|
|
96
|
+
private dirty = new Set<keyof CliConfig>()
|
|
50
97
|
|
|
51
98
|
constructor() {
|
|
52
99
|
this.load()
|
|
@@ -56,6 +103,15 @@ export class ConfigService {
|
|
|
56
103
|
try {
|
|
57
104
|
if (existsSync(CONFIG_FILE)) {
|
|
58
105
|
const data = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
|
|
106
|
+
const whitelist = Array.isArray(data.whitelist) ? data.whitelist : []
|
|
107
|
+
const blacklist = Array.isArray(data.blacklist) ? data.blacklist : []
|
|
108
|
+
// entries 优先; 缺失时从旧 string[] 迁移
|
|
109
|
+
const whitelistEntries: ListEntry[] = Array.isArray(data.whitelistEntries)
|
|
110
|
+
? data.whitelistEntries
|
|
111
|
+
: whitelist.map((wxid: string) => ({ wxid }))
|
|
112
|
+
const blacklistEntries: ListEntry[] = Array.isArray(data.blacklistEntries)
|
|
113
|
+
? data.blacklistEntries
|
|
114
|
+
: blacklist.map((wxid: string) => ({ wxid }))
|
|
59
115
|
this.config = {
|
|
60
116
|
dbPath: data.dbPath || '',
|
|
61
117
|
wxid: data.wxid || '',
|
|
@@ -73,11 +129,32 @@ export class ConfigService {
|
|
|
73
129
|
wechatOcAccountId: data.wechatOcAccountId || '',
|
|
74
130
|
wechatOcBaseUrl: data.wechatOcBaseUrl || '',
|
|
75
131
|
wechatOcSyncBuf: data.wechatOcSyncBuf || '',
|
|
76
|
-
|
|
132
|
+
wechatOcContextTokens: data.wechatOcContextTokens || '',
|
|
133
|
+
whitelist,
|
|
134
|
+
blacklist,
|
|
135
|
+
whitelistEntries,
|
|
136
|
+
blacklistEntries,
|
|
77
137
|
vaultRepo: data.vaultRepo || '',
|
|
78
138
|
aiEngine: data.aiEngine || 'deepseek',
|
|
79
139
|
wereadApiKey: data.wereadApiKey || '',
|
|
80
140
|
deepseekApiKey: data.deepseekApiKey || '',
|
|
141
|
+
snsDbPath: data.snsDbPath || '',
|
|
142
|
+
snsKey: data.snsKey || '',
|
|
143
|
+
snsSalt: data.snsSalt || '',
|
|
144
|
+
favDbPath: data.favDbPath || '',
|
|
145
|
+
favKey: data.favKey || '',
|
|
146
|
+
favPassphrase: data.favPassphrase || '',
|
|
147
|
+
emoticonSeed: data.emoticonSeed || '',
|
|
148
|
+
assistantPrivacy: data.assistantPrivacy || 'strict',
|
|
149
|
+
localModel: data.localModel || '',
|
|
150
|
+
aiBaseUrl: data.aiBaseUrl || '',
|
|
151
|
+
aiModel: data.aiModel || '',
|
|
152
|
+
assistantWhitelist: data.assistantWhitelist || '',
|
|
153
|
+
assistantGroupWhitelist: data.assistantGroupWhitelist || '',
|
|
154
|
+
assistantGroupRequireMention: data.assistantGroupRequireMention === 'false' ? 'false' : 'true',
|
|
155
|
+
dailySources: data.dailySources || '',
|
|
156
|
+
dailySourceCategories: data.dailySourceCategories || '',
|
|
157
|
+
dailyAiEnabled: data.dailyAiEnabled === 'false' ? 'false' : 'true',
|
|
81
158
|
}
|
|
82
159
|
}
|
|
83
160
|
} catch (e) {
|
|
@@ -91,7 +168,24 @@ export class ConfigService {
|
|
|
91
168
|
if (!existsSync(CONFIG_DIR)) {
|
|
92
169
|
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
93
170
|
}
|
|
94
|
-
|
|
171
|
+
// 多进程安全 (守护进程与 CLI 并发): 只回写本进程修改过的字段,
|
|
172
|
+
// 其余字段以磁盘最新值保留, 避免旧内存快照覆盖他人写入 (幽灵回写)。
|
|
173
|
+
let merged: Record<string, unknown> = { ...this.config }
|
|
174
|
+
try {
|
|
175
|
+
if (existsSync(CONFIG_FILE) && this.dirty.size > 0) {
|
|
176
|
+
const onDisk: Record<string, unknown> = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
|
|
177
|
+
merged = { ...onDisk, ...this.config }
|
|
178
|
+
for (const key of Object.keys(onDisk)) {
|
|
179
|
+
if (this.dirty.has(key as keyof CliConfig)) continue
|
|
180
|
+
merged[key] = onDisk[key]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch { /* 磁盘副本不可读时退回整份写入 */ }
|
|
184
|
+
// 原子写入: 先写临时文件再改名, 防止进程中途被杀导致配置损坏
|
|
185
|
+
const tmpFile = CONFIG_FILE + '.tmp'
|
|
186
|
+
writeFileSync(tmpFile, JSON.stringify(merged, null, 2), 'utf8')
|
|
187
|
+
renameSync(tmpFile, CONFIG_FILE)
|
|
188
|
+
this.dirty.clear()
|
|
95
189
|
} catch (e) {
|
|
96
190
|
console.error('保存配置失败:', e)
|
|
97
191
|
}
|
|
@@ -116,6 +210,7 @@ export class ConfigService {
|
|
|
116
210
|
} else {
|
|
117
211
|
this.config[key] = value
|
|
118
212
|
}
|
|
213
|
+
this.dirty.add(key)
|
|
119
214
|
this.save()
|
|
120
215
|
}
|
|
121
216
|
|
|
@@ -133,6 +228,9 @@ export class ConfigService {
|
|
|
133
228
|
contactDbPath: this.config.contactDbPath,
|
|
134
229
|
contactKey: this.get('contactKey'),
|
|
135
230
|
contactSalt: this.config.contactSalt,
|
|
231
|
+
snsDbPath: this.config.snsDbPath,
|
|
232
|
+
snsKey: this.get('snsKey'),
|
|
233
|
+
snsSalt: this.config.snsSalt,
|
|
136
234
|
}
|
|
137
235
|
}
|
|
138
236
|
|
|
@@ -142,9 +240,76 @@ export class ConfigService {
|
|
|
142
240
|
|
|
143
241
|
setWhitelist(list: string[]): void {
|
|
144
242
|
this.config.whitelist = list
|
|
243
|
+
this.dirty.add('whitelist')
|
|
244
|
+
this.save()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getWhitelistEntries(): ListEntry[] {
|
|
248
|
+
return this.config.whitelistEntries || []
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setWhitelistEntries(entries: ListEntry[]): void {
|
|
252
|
+
this.config.whitelistEntries = entries
|
|
253
|
+
// 同步 string[] 视图, 供旧代码使用
|
|
254
|
+
this.config.whitelist = entries.map(e => e.wxid)
|
|
255
|
+
this.dirty.add('whitelistEntries')
|
|
256
|
+
this.dirty.add('whitelist')
|
|
145
257
|
this.save()
|
|
146
258
|
}
|
|
147
259
|
|
|
260
|
+
getBlacklist(): string[] {
|
|
261
|
+
return this.config.blacklist || []
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setBlacklist(list: string[]): void {
|
|
265
|
+
this.config.blacklist = list
|
|
266
|
+
this.dirty.add('blacklist')
|
|
267
|
+
this.save()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getBlacklistEntries(): ListEntry[] {
|
|
271
|
+
return this.config.blacklistEntries || []
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setBlacklistEntries(entries: ListEntry[]): void {
|
|
275
|
+
this.config.blacklistEntries = entries
|
|
276
|
+
this.config.blacklist = entries.map(e => e.wxid)
|
|
277
|
+
this.dirty.add('blacklistEntries')
|
|
278
|
+
this.dirty.add('blacklist')
|
|
279
|
+
this.save()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 读取持久化的 context_token: wxid -> token */
|
|
283
|
+
getContextTokens(): Record<string, string> {
|
|
284
|
+
const raw = this.config.wechatOcContextTokens || ''
|
|
285
|
+
if (!raw) return {}
|
|
286
|
+
const json = raw.startsWith(LOCK_PREFIX) ? this.lockDecrypt(raw) : raw
|
|
287
|
+
if (!json) return {}
|
|
288
|
+
try {
|
|
289
|
+
const obj = JSON.parse(json)
|
|
290
|
+
return (obj && typeof obj === 'object') ? obj as Record<string, string> : {}
|
|
291
|
+
} catch {
|
|
292
|
+
return {}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 持久化 context_token 集合 (加密存储) */
|
|
297
|
+
setContextTokens(tokens: Record<string, string>): void {
|
|
298
|
+
const json = JSON.stringify(tokens || {})
|
|
299
|
+
this.config.wechatOcContextTokens = json ? this.lockEncrypt(json) : ''
|
|
300
|
+
this.dirty.add('wechatOcContextTokens')
|
|
301
|
+
this.save()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 增量更新单个 wxid 的 context_token */
|
|
305
|
+
upsertContextToken(wxid: string, token: string): void {
|
|
306
|
+
if (!wxid || !token) return
|
|
307
|
+
const tokens = this.getContextTokens()
|
|
308
|
+
if (tokens[wxid] === token) return
|
|
309
|
+
tokens[wxid] = token
|
|
310
|
+
this.setContextTokens(tokens)
|
|
311
|
+
}
|
|
312
|
+
|
|
148
313
|
isConfigured(): boolean {
|
|
149
314
|
const has4x = !!(this.config.dbPath && this.config.decryptKey)
|
|
150
315
|
const has3x = !!(this.config.dbPath3x && this.config.decryptKey3x)
|
|
@@ -152,8 +317,31 @@ export class ConfigService {
|
|
|
152
317
|
return has4x || has3x || hasNt
|
|
153
318
|
}
|
|
154
319
|
|
|
320
|
+
clearDatabaseKeys(): void {
|
|
321
|
+
this.clearDatabaseKeysInMemory(true)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
maskDatabaseKeysForTesting(): void {
|
|
325
|
+
this.clearDatabaseKeysInMemory(false)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private clearDatabaseKeysInMemory(persist: boolean): void {
|
|
329
|
+
const keyFields: Array<keyof CliConfig> = [
|
|
330
|
+
'decryptKey', 'decryptKey3x', 'ntKey', 'ntSalt',
|
|
331
|
+
'contactKey', 'contactSalt', 'snsKey', 'snsSalt',
|
|
332
|
+
'favKey', 'favPassphrase',
|
|
333
|
+
]
|
|
334
|
+
for (const key of keyFields) {
|
|
335
|
+
this.config[key] = '' as never
|
|
336
|
+
if (persist) this.dirty.add(key)
|
|
337
|
+
}
|
|
338
|
+
if (persist) this.save()
|
|
339
|
+
}
|
|
340
|
+
|
|
155
341
|
clear(): void {
|
|
156
|
-
this.config = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', whitelist: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '' }
|
|
342
|
+
this.config = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', wechatOcContextTokens: '', whitelist: [], blacklist: [], whitelistEntries: [], blacklistEntries: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '', snsDbPath: '', snsKey: '', snsSalt: '', favDbPath: '', favKey: '', favPassphrase: '', emoticonSeed: '', assistantPrivacy: 'strict', localModel: '', aiBaseUrl: '', aiModel: '', assistantWhitelist: '', assistantGroupWhitelist: '', assistantGroupRequireMention: 'true', dailySources: '', dailySourceCategories: '', dailyAiEnabled: 'true' }
|
|
343
|
+
// clear 意图是全量重置: 所有字段标记为脏, 覆盖磁盘上的全部旧值
|
|
344
|
+
this.dirty = new Set(Object.keys(this.config) as (keyof CliConfig)[])
|
|
157
345
|
this.save()
|
|
158
346
|
}
|
|
159
347
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from 'crypto'
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import type { Message } from '../types.js'
|
|
5
|
+
import { redactText, type PrivacyMode } from './assistantPrivacy.js'
|
|
6
|
+
|
|
7
|
+
export interface EvidenceManifest {
|
|
8
|
+
schemaVersion: '1.0'
|
|
9
|
+
generatedAt: string
|
|
10
|
+
source: 'local-wechat'
|
|
11
|
+
authorization: 'user-authorized-local-data'
|
|
12
|
+
talker: string
|
|
13
|
+
caseNote: string
|
|
14
|
+
messageCount: number
|
|
15
|
+
firstMessageAt: string | null
|
|
16
|
+
lastMessageAt: string | null
|
|
17
|
+
messageIds: number[]
|
|
18
|
+
messagesSha256: string
|
|
19
|
+
notice: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EvidencePackageResult {
|
|
23
|
+
path: string
|
|
24
|
+
manifest: EvidenceManifest
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EvidenceReviewInput {
|
|
28
|
+
transcript: string
|
|
29
|
+
redactions: number
|
|
30
|
+
localInference: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LEGAL_NOTICE = '本包用于本地证据整理,不代表任何法院或机构必然采信;请保留原设备和原始数据,并根据案件情况咨询专业律师。'
|
|
34
|
+
|
|
35
|
+
function sha256(value: string): string {
|
|
36
|
+
return createHash('sha256').update(value, 'utf8').digest('hex')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isoTime(timestamp: number | undefined): string | null {
|
|
40
|
+
if (!timestamp || !Number.isFinite(timestamp)) return null
|
|
41
|
+
return new Date(timestamp * 1000).toISOString()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safePackageName(talker: string): string {
|
|
45
|
+
const name = talker.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^\.+/, '')
|
|
46
|
+
return (name || 'conversation').slice(0, 80)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildEvidenceManifest(talker: string, messages: Message[], caseNote = '', generatedAt = new Date().toISOString()): EvidenceManifest {
|
|
50
|
+
const serialized = JSON.stringify(messages, null, 2)
|
|
51
|
+
const timestamps = messages.map(message => message.createTime).filter(Number.isFinite)
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: '1.0',
|
|
54
|
+
generatedAt,
|
|
55
|
+
source: 'local-wechat',
|
|
56
|
+
authorization: 'user-authorized-local-data',
|
|
57
|
+
talker,
|
|
58
|
+
caseNote: caseNote.trim().slice(0, 500),
|
|
59
|
+
messageCount: messages.length,
|
|
60
|
+
firstMessageAt: timestamps.length ? isoTime(Math.min(...timestamps)) : null,
|
|
61
|
+
lastMessageAt: timestamps.length ? isoTime(Math.max(...timestamps)) : null,
|
|
62
|
+
messageIds: messages.map(message => message.localId).filter(Number.isFinite),
|
|
63
|
+
messagesSha256: sha256(serialized),
|
|
64
|
+
notice: LEGAL_NOTICE,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function writeEvidencePackage(outputRoot: string, talker: string, messages: Message[], caseNote = ''): EvidencePackageResult {
|
|
69
|
+
const packagePath = join(outputRoot, `${safePackageName(talker)}-${Date.now()}`)
|
|
70
|
+
mkdirSync(packagePath, { recursive: true })
|
|
71
|
+
const messagesText = JSON.stringify(messages, null, 2)
|
|
72
|
+
const manifest = buildEvidenceManifest(talker, messages, caseNote)
|
|
73
|
+
writeFileSync(join(packagePath, 'messages.json'), messagesText, { encoding: 'utf8', flag: 'wx' })
|
|
74
|
+
writeFileSync(join(packagePath, 'manifest.json'), JSON.stringify(manifest, null, 2), { encoding: 'utf8', flag: 'wx' })
|
|
75
|
+
writeFileSync(join(packagePath, 'README.md'), [
|
|
76
|
+
'# 本地聊天证据整理包',
|
|
77
|
+
'',
|
|
78
|
+
`- 消息数量:${manifest.messageCount}`,
|
|
79
|
+
`- 生成时间:${manifest.generatedAt}`,
|
|
80
|
+
`- 原始消息 SHA-256:\`${manifest.messagesSha256}\``,
|
|
81
|
+
'',
|
|
82
|
+
'## 使用边界',
|
|
83
|
+
'',
|
|
84
|
+
LEGAL_NOTICE,
|
|
85
|
+
'',
|
|
86
|
+
'请勿修改 `messages.json`。如需脱敏、摘要或分析,请复制后处理,并保留处理前后的文件哈希和操作记录。',
|
|
87
|
+
'',
|
|
88
|
+
'## 建议补充',
|
|
89
|
+
'',
|
|
90
|
+
'- 保留原设备、原始数据库和官方导出记录。',
|
|
91
|
+
'- 记录导出人、导出时间、工具版本和数据范围。',
|
|
92
|
+
'- 保存完整上下文,不要只截取对自己有利的片段。',
|
|
93
|
+
'- 将本包交给律师或有权处理案件的机构复核。',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n'), { encoding: 'utf8', flag: 'wx' })
|
|
96
|
+
return { path: packagePath, manifest }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const evidenceLegalNotice = LEGAL_NOTICE
|
|
100
|
+
|
|
101
|
+
export function buildEvidenceReviewInput(talker: string, messages: Message[], mode: PrivacyMode, localInference: boolean): EvidenceReviewInput {
|
|
102
|
+
let redactions = 0
|
|
103
|
+
const safeMetadata = (value: string, fallback: string): string => {
|
|
104
|
+
if (localInference) return value || fallback
|
|
105
|
+
if (mode === 'strict') return fallback
|
|
106
|
+
const result = redactText(value || fallback, mode, false)
|
|
107
|
+
redactions += result.redactions
|
|
108
|
+
return result.safe
|
|
109
|
+
}
|
|
110
|
+
const safeTalker = safeMetadata(talker, '[会话已按严格隐私模式屏蔽]')
|
|
111
|
+
const lines = messages.map(message => {
|
|
112
|
+
const rawBody = message.content || message.parsedContent || message.rawContent || ''
|
|
113
|
+
const body = mode === 'strict' && !localInference
|
|
114
|
+
? `[聊天正文已按严格隐私模式屏蔽,共${rawBody.length}字]`
|
|
115
|
+
: (() => {
|
|
116
|
+
const result = redactText(rawBody, mode, localInference)
|
|
117
|
+
redactions += result.redactions
|
|
118
|
+
return result.safe
|
|
119
|
+
})()
|
|
120
|
+
const time = isoTime(message.createTime) || '时间未知'
|
|
121
|
+
const sender = safeMetadata(message.senderUsername || '', '[发送方已按严格隐私模式屏蔽]')
|
|
122
|
+
return `[消息ID:${message.localId}] [${time}] [发送方:${sender}] ${body}`
|
|
123
|
+
})
|
|
124
|
+
return {
|
|
125
|
+
transcript: `会话:${safeTalker}\n消息数:${messages.length}\n\n${lines.join('\n')}`,
|
|
126
|
+
redactions,
|
|
127
|
+
localInference,
|
|
128
|
+
}
|
|
129
|
+
}
|