weflow-cli 1.6.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +3 -2
- package/CHANGELOG.md +1966 -0
- package/OPERATIONS.md +808 -0
- package/README.en.md +93 -7
- package/README.md +31 -6
- package/bin/weflow-cli.ts +1245 -26
- package/dist/bin/weflow-cli.js +1349 -72
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.js +39 -13
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/appMsgFormat.d.ts +37 -0
- package/dist/src/core/appMsgFormat.d.ts.map +1 -0
- package/dist/src/core/appMsgFormat.js +106 -0
- package/dist/src/core/appMsgFormat.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +43 -0
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +31 -0
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/core/sqlcipherCore.d.ts +0 -12
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
- package/dist/src/core/sqlcipherCore.js +5 -64
- package/dist/src/core/sqlcipherCore.js.map +1 -1
- package/dist/src/core/wcdbCore.d.ts +1 -1
- package/dist/src/core/wcdbCore.d.ts.map +1 -1
- package/dist/src/core/wcdbCore.js +5 -1
- package/dist/src/core/wcdbCore.js.map +1 -1
- package/dist/src/panel/client.d.ts +25 -0
- package/dist/src/panel/client.d.ts.map +1 -0
- package/dist/src/panel/client.js +34 -0
- package/dist/src/panel/client.js.map +1 -0
- package/dist/src/panel/endpoint.d.ts +45 -0
- package/dist/src/panel/endpoint.d.ts.map +1 -0
- package/dist/src/panel/endpoint.js +132 -0
- package/dist/src/panel/endpoint.js.map +1 -0
- package/dist/src/panel/launch.d.ts +50 -0
- package/dist/src/panel/launch.d.ts.map +1 -0
- package/dist/src/panel/launch.js +74 -0
- package/dist/src/panel/launch.js.map +1 -0
- package/dist/src/panel/server.d.ts +23 -0
- package/dist/src/panel/server.d.ts.map +1 -0
- package/dist/src/panel/server.js +413 -0
- package/dist/src/panel/server.js.map +1 -0
- package/dist/src/panel/userId.d.ts +37 -0
- package/dist/src/panel/userId.d.ts.map +1 -0
- package/dist/src/panel/userId.js +56 -0
- package/dist/src/panel/userId.js.map +1 -0
- package/dist/src/services/assistantDaemon.d.ts +20 -3
- package/dist/src/services/assistantDaemon.d.ts.map +1 -1
- package/dist/src/services/assistantDaemon.js +46 -9
- package/dist/src/services/assistantDaemon.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts +141 -0
- package/dist/src/services/assistantEval.d.ts.map +1 -0
- package/dist/src/services/assistantEval.js +497 -0
- package/dist/src/services/assistantEval.js.map +1 -0
- package/dist/src/services/assistantMemory.d.ts +106 -4
- package/dist/src/services/assistantMemory.d.ts.map +1 -1
- package/dist/src/services/assistantMemory.js +396 -61
- package/dist/src/services/assistantMemory.js.map +1 -1
- package/dist/src/services/assistantPrivacy.d.ts +15 -2
- package/dist/src/services/assistantPrivacy.d.ts.map +1 -1
- package/dist/src/services/assistantPrivacy.js +18 -4
- package/dist/src/services/assistantPrivacy.js.map +1 -1
- package/dist/src/services/assistantRouter.d.ts +41 -0
- package/dist/src/services/assistantRouter.d.ts.map +1 -0
- package/dist/src/services/assistantRouter.js +151 -0
- package/dist/src/services/assistantRouter.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +209 -3
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +590 -75
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +205 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +1089 -58
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/assistantTrace.d.ts +47 -0
- package/dist/src/services/assistantTrace.d.ts.map +1 -0
- package/dist/src/services/assistantTrace.js +150 -0
- package/dist/src/services/assistantTrace.js.map +1 -0
- package/dist/src/services/chatService.d.ts +13 -0
- package/dist/src/services/chatService.d.ts.map +1 -1
- package/dist/src/services/chatService.js +23 -0
- package/dist/src/services/chatService.js.map +1 -1
- package/dist/src/services/configService.d.ts +9 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +15 -8
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/messageQuery.d.ts +35 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -1
- package/dist/src/services/messageQuery.js +61 -0
- package/dist/src/services/messageQuery.js.map +1 -1
- package/dist/src/services/pythonBridge.d.ts +41 -0
- package/dist/src/services/pythonBridge.d.ts.map +1 -0
- package/dist/src/services/pythonBridge.js +119 -0
- package/dist/src/services/pythonBridge.js.map +1 -0
- package/dist/src/services/syncService.d.ts +127 -0
- package/dist/src/services/syncService.d.ts.map +1 -0
- package/dist/src/services/syncService.js +263 -0
- package/dist/src/services/syncService.js.map +1 -0
- package/dist/src/services/syncState.d.ts +113 -0
- package/dist/src/services/syncState.d.ts.map +1 -0
- package/dist/src/services/syncState.js +214 -0
- package/dist/src/services/syncState.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +25 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +69 -7
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/utils/atomicWrite.d.ts +4 -0
- package/dist/src/utils/atomicWrite.d.ts.map +1 -0
- package/dist/src/utils/atomicWrite.js +60 -0
- package/dist/src/utils/atomicWrite.js.map +1 -0
- package/dist/src/utils/dateRange.d.ts +15 -0
- package/dist/src/utils/dateRange.d.ts.map +1 -1
- package/dist/src/utils/dateRange.js +47 -0
- package/dist/src/utils/dateRange.js.map +1 -1
- package/dist/src/utils/text.d.ts +18 -0
- package/dist/src/utils/text.d.ts.map +1 -0
- package/dist/src/utils/text.js +20 -0
- package/dist/src/utils/text.js.map +1 -0
- package/docs/AI_INTERFACE.md +2 -0
- package/docs/DECISIONS.md +1484 -20
- package/docs/EXTENDING.md +187 -0
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +338 -0
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -0
- package/docs/MCP.md +23 -4
- package/docs/PROJECT_STATE.md +299 -96
- package/docs/SETUP.md +22 -0
- package/docs/SYNC_CONTRACT.md +180 -0
- package/mcp-server/index.ts +681 -657
- package/package.json +84 -83
- package/resources/panel/ball-position.cjs +153 -0
- package/resources/panel/index.html +46 -0
- package/resources/panel/main.cjs +447 -0
- package/resources/panel/mascot-focus.png +0 -0
- package/resources/panel/mascot-happy.png +0 -0
- package/resources/panel/mascot-sorry.png +0 -0
- package/resources/panel/mascot-tired.png +0 -0
- package/resources/panel/mascot.png +0 -0
- package/resources/panel/package.json +7 -0
- package/resources/panel/panel.css +407 -0
- package/resources/panel/preload.cjs +72 -0
- package/resources/panel/quick-menu.cjs +197 -0
- package/resources/panel/renderer.js +441 -0
- package/resources/panel/tray-menu.cjs +40 -0
- package/resources/panel/tray.png +0 -0
- package/scripts/_utils.py +403 -54
- package/scripts/annual_report.py +4 -2
- package/scripts/article_notes.py +423 -0
- package/scripts/backfill_articles.py +438 -0
- package/scripts/biz_daily.py +590 -105
- package/scripts/chat_notes.py +470 -0
- package/scripts/classify_daily.py +10 -18
- package/scripts/compile_wiki.py +1059 -46
- package/scripts/create_reading_notes.py +199 -14
- package/scripts/decide.py +253 -0
- package/scripts/draft_reply.py +671 -0
- package/scripts/export_chat_html.py +212 -46
- package/scripts/extract_todos.py +31 -5
- package/scripts/fav_notes.py +318 -0
- package/scripts/fix_topics.py +2 -1
- package/scripts/generate_ai_report.py +214 -9
- package/scripts/generate_html.py +18 -3
- package/scripts/generate_review.py +2 -2
- package/scripts/jev_client.py +294 -0
- package/scripts/jev_probe.py +440 -0
- package/scripts/nt_common.py +82 -0
- package/scripts/nt_decrypt.py +1755 -1352
- package/scripts/pipeline.py +27 -2
- package/scripts/promote_ideas.py +19 -4
- package/scripts/quality_eval.py +707 -0
- package/scripts/rag_chat.py +4 -4
- package/scripts/read_image.py +187 -0
- package/scripts/reply_debt.py +488 -0
- package/scripts/route_cards.py +453 -0
- package/scripts/semantic_search.py +208 -49
- package/scripts/user_notes.py +315 -0
- package/scripts/vault_rag.py +13 -7
- package/scripts/vault_search.py +21 -4
- package/scripts/wiki_lint.py +397 -0
- package/src/core/appMsgFormat.ts +117 -0
- package/src/core/ntCore.ts +67 -0
- package/src/core/sqlcipherCore.ts +5 -61
- package/src/core/wcdbCore.ts +6 -2
- package/src/panel/client.ts +49 -0
- package/src/panel/endpoint.ts +144 -0
- package/src/panel/launch.ts +101 -0
- package/src/panel/server.ts +424 -0
- package/src/panel/userId.ts +72 -0
- package/src/services/assistantDaemon.ts +57 -9
- package/src/services/assistantEval.ts +611 -0
- package/src/services/assistantMemory.ts +413 -57
- package/src/services/assistantPrivacy.ts +18 -4
- package/src/services/assistantRouter.ts +191 -0
- package/src/services/assistantService.ts +671 -74
- package/src/services/assistantTools.ts +1118 -57
- package/src/services/assistantTrace.ts +175 -0
- package/src/services/chatService.ts +30 -0
- package/src/services/configService.ts +403 -385
- package/src/services/messageQuery.ts +97 -0
- package/src/services/pythonBridge.ts +139 -0
- package/src/services/syncService.ts +363 -0
- package/src/services/syncState.ts +275 -0
- package/src/services/wechatMessageService.ts +73 -7
- package/src/utils/atomicWrite.ts +56 -0
- package/src/utils/dateRange.ts +46 -0
- package/src/utils/text.ts +20 -0
package/dist/bin/weflow-cli.js
CHANGED
|
@@ -5,6 +5,8 @@ import inquirer from 'inquirer';
|
|
|
5
5
|
import { basename, join } from 'path';
|
|
6
6
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
|
+
import { SYNC_SCHEMA, SYNC_SOURCE, syncStateStore } from '../src/services/syncState.js';
|
|
9
|
+
import { SyncRangeRequiredError, runSync } from '../src/services/syncService.js';
|
|
8
10
|
import { dbPathService } from '../src/core/dbPathService.js';
|
|
9
11
|
import { keyService } from '../src/core/keyService.js';
|
|
10
12
|
import { NtCore } from '../src/core/ntCore.js';
|
|
@@ -298,8 +300,41 @@ program
|
|
|
298
300
|
dailyStats: { cli: 'daily-stats --json' },
|
|
299
301
|
diagnostics: { cli: 'check --json' },
|
|
300
302
|
todos: { cli: 'todos list --json' },
|
|
303
|
+
awaiting: {
|
|
304
|
+
cli: 'awaiting --yes',
|
|
305
|
+
preview: 'awaiting --dry-run --json',
|
|
306
|
+
readsLocalChat: true,
|
|
307
|
+
invokesAI: true,
|
|
308
|
+
writesNothing: true,
|
|
309
|
+
confirmationRequired: true,
|
|
310
|
+
},
|
|
311
|
+
draft: {
|
|
312
|
+
cli: 'draft <会话> --yes --json',
|
|
313
|
+
preview: 'draft <会话> --dry-run --json',
|
|
314
|
+
readsLocalChat: true,
|
|
315
|
+
invokesAI: true,
|
|
316
|
+
// **它不发送任何东西**:只产出候选文本,回不回由人决定。
|
|
317
|
+
// 这条要能被机器读到——`sendsNothing` 比 `writesNothing` 更要紧。
|
|
318
|
+
writesNothing: true,
|
|
319
|
+
sendsNothing: true,
|
|
320
|
+
confirmationRequired: true,
|
|
321
|
+
},
|
|
301
322
|
knowledge: { cli: 'search <query> --yes --json', preview: 'search <query> --dry-run --json', output: 'json', mcp: 'wechat.search_articles' },
|
|
302
323
|
},
|
|
324
|
+
primitives: {
|
|
325
|
+
decide: {
|
|
326
|
+
cli: 'decide --request <file> --yes | decide --over <glob> --ask <text> --yes',
|
|
327
|
+
preview: 'decide --dry-run',
|
|
328
|
+
// 两条路的读法**不一样**,所以不能只写一个 false:
|
|
329
|
+
// --request 完全由调用方给,--over 会读匹配到的本地文件。
|
|
330
|
+
readsLocalData: { request: false, over: true },
|
|
331
|
+
invokesAI: true,
|
|
332
|
+
sendsCallerProvidedState: true,
|
|
333
|
+
confirmationRequired: true,
|
|
334
|
+
mcpExposed: false,
|
|
335
|
+
whyNotMcp: 'an MCP client must not be able to drive local outbound calls',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
303
338
|
workflows: {
|
|
304
339
|
initialization: {
|
|
305
340
|
preview: 'init --dry-run --json',
|
|
@@ -309,6 +344,26 @@ program
|
|
|
309
344
|
previewExposesPaths: false,
|
|
310
345
|
},
|
|
311
346
|
evidencePackage: { cli: 'evidence <talker> --json --non-interactive', localOnly: true },
|
|
347
|
+
sync: {
|
|
348
|
+
preview: 'sync run <talker> --dry-run --json',
|
|
349
|
+
execute: 'sync run <talker> --yes --json',
|
|
350
|
+
status: 'sync status [<talker>] --json',
|
|
351
|
+
verify: 'sync verify <talker> --json',
|
|
352
|
+
confirmationRequired: true,
|
|
353
|
+
readsLocalData: true,
|
|
354
|
+
invokesAI: false,
|
|
355
|
+
sideEffects: ['local-checkpoint-file'],
|
|
356
|
+
stateSchema: 'weflow-sync/v1',
|
|
357
|
+
jobSchema: 'weflow-job/v1',
|
|
358
|
+
stateLocation: 'local-user-config-dir',
|
|
359
|
+
previewResolvesTalker: false,
|
|
360
|
+
previewExposesLocalPaths: false,
|
|
361
|
+
// D-027: a stable cursor must be backed by every database backend
|
|
362
|
+
// before it is advertised. This exposes overlapping windows only.
|
|
363
|
+
stableCursor: false,
|
|
364
|
+
coverageValues: ['complete', 'unverified', 'partial'],
|
|
365
|
+
partialCompletesSuccessfully: false,
|
|
366
|
+
},
|
|
312
367
|
aiAnalysis: {
|
|
313
368
|
preview: 'evidence-review <talker> --dry-run --json',
|
|
314
369
|
execute: 'evidence-review <talker> --yes --json',
|
|
@@ -352,6 +407,17 @@ program
|
|
|
352
407
|
execute: 'assistant <start|stop> --yes --json',
|
|
353
408
|
confirmationRequired: true,
|
|
354
409
|
},
|
|
410
|
+
localPanel: {
|
|
411
|
+
status: 'panel --status --json',
|
|
412
|
+
ask: 'panel --ask "<text>" --yes --json',
|
|
413
|
+
preview: 'panel --dry-run --json',
|
|
414
|
+
execute: 'panel --yes --json',
|
|
415
|
+
confirmationRequired: true,
|
|
416
|
+
opensLocalWindow: true,
|
|
417
|
+
loopbackOnly: true,
|
|
418
|
+
// 关掉窗口**不会**停掉助手——这件事必须能被机器读到,别让调用方以为退出即结束
|
|
419
|
+
keepsAssistantRunningAfterClose: true,
|
|
420
|
+
},
|
|
355
421
|
messageChannelAuthentication: {
|
|
356
422
|
loginPreview: 'login-wechat --dry-run --json',
|
|
357
423
|
loginExecute: 'login-wechat --yes',
|
|
@@ -481,7 +547,28 @@ program
|
|
|
481
547
|
aiDisabledByDefaultForExport: true,
|
|
482
548
|
unknownMessageTypesPreserved: true,
|
|
483
549
|
nonInteractiveTalkerResolution: true,
|
|
484
|
-
|
|
550
|
+
// **这个名字曾经是假的**:MCP 那张表不是手写的只读清单,而是**助手工具表减去
|
|
551
|
+
// save_memory**,所以里面早就有写文件的 `export_chat` 和会把图片交给模型的
|
|
552
|
+
// `look_at_image`。如实改成 false,并把真正有出境的几样单独列出来(下面这个对象)。
|
|
553
|
+
// 读这一位的调用方按 false 走保守分支即可。
|
|
554
|
+
mcpDefaultReadOnly: false,
|
|
555
|
+
mcpSurface: {
|
|
556
|
+
// 原文只写了 "minus save_memory" —— 而那张表也排除了 `look_at_image`(MCP 只取文本,
|
|
557
|
+
// 它挂进侧信道的图没人接)。机器可读字段少说一句同样算不准,如实写全。
|
|
558
|
+
derivedFrom: 'assistant tools minus save_memory and look_at_image',
|
|
559
|
+
writesFiles: ['export_chat'],
|
|
560
|
+
// 逐个核实过(脚本里确实调云端模型的那些):
|
|
561
|
+
// who_owes_reply/search_chats → Jev;search_semantic → 阿里云百炼嵌入 + Jev 重排;draft_reply → Jev + DeepSeek。
|
|
562
|
+
// `look_at_image` **不在这里**:它靠侧信道把图交给助手自己的模型,而 MCP 只取工具返回的文本,
|
|
563
|
+
// 图没人接、回话却写着"你能看到它了"——所以它已经从 MCP 那张表里排除了(见 MCP_TOOL_DEFS)。
|
|
564
|
+
callsCloudModels: ['who_owes_reply', 'search_chats', 'search_semantic', 'draft_reply'],
|
|
565
|
+
// 机器调用**默认只给预览**、必须显式 confirm 才出境的那些(MCP 独有的边界)。
|
|
566
|
+
// **这里是四个,不是"只有 draft_reply"**:逐个读 handler 才发现那四个工具里
|
|
567
|
+
// 写的是同一句 `if (ctx.requiresConfirm && args.confirm !== true)`(1010/1041/1094/1170)。
|
|
568
|
+
// 原先只列了 draft_reply,而测试是照着这个字段的值抄的,于是两处一起错、谁都没红。
|
|
569
|
+
// 现在测试改成**从源码里数**(见 test/assistant-tools.test.ts),改一处不同步就会红。
|
|
570
|
+
requiresConfirm: ['search_chats', 'who_owes_reply', 'draft_reply', 'search_semantic'],
|
|
571
|
+
},
|
|
485
572
|
mcpMessageLimit: { default: 100, max: 1000 },
|
|
486
573
|
},
|
|
487
574
|
};
|
|
@@ -886,9 +973,13 @@ const configCmd = program
|
|
|
886
973
|
const configurableKeys = [
|
|
887
974
|
'dbPath', 'decryptKey', 'dbPath3x', 'decryptKey3x', 'dataVersion', 'wxid',
|
|
888
975
|
'ntDbPath', 'ntKey', 'ntSalt', 'contactDbPath', 'contactKey', 'contactSalt',
|
|
889
|
-
'vaultRepo', 'aiEngine', 'aiBaseUrl', 'aiModel', 'deepseekApiKey', '
|
|
976
|
+
'vaultRepo', 'aiEngine', 'aiBaseUrl', 'aiModel', 'deepseekApiKey', 'typesafeApiKey',
|
|
977
|
+
'dashscopeApiKey', 'favPassphrase',
|
|
978
|
+
'wereadApiKey',
|
|
890
979
|
'assistantPrivacy', 'assistantWhitelist', 'assistantGroupWhitelist',
|
|
891
|
-
'assistantGroupRequireMention', '
|
|
980
|
+
'assistantGroupRequireMention', 'assistantFastRoute', 'assistantPanelUser', 'quickReplyContacts',
|
|
981
|
+
'dailySources', 'dailySourceCategories',
|
|
982
|
+
'dailyExcludeTopics', 'dailyAiEnabled',
|
|
892
983
|
'emoticonSeed',
|
|
893
984
|
];
|
|
894
985
|
function setConfigValue(key, value, quiet = false) {
|
|
@@ -978,6 +1069,7 @@ configCmd
|
|
|
978
1069
|
console.log(`微信账号: ${config.wxid || chalk.gray('(未设置)')}`);
|
|
979
1070
|
console.log(`公众号日报来源: ${configService.get('dailySources') || chalk.gray('(全部公众号)')}`);
|
|
980
1071
|
console.log(`公众号日报 AI: ${configService.get('dailyAiEnabled') === 'false' ? '已关闭' : '已开启'}`);
|
|
1072
|
+
console.log(`公众号日报排除主题: ${configService.get('dailyExcludeTopics') || chalk.gray('(不排除)')}`);
|
|
981
1073
|
});
|
|
982
1074
|
configCmd
|
|
983
1075
|
.command('set <key> <value>')
|
|
@@ -1217,6 +1309,336 @@ program
|
|
|
1217
1309
|
console.log(chalk.gray(`[${time}]`) + ` ${senderName}: ${content}`);
|
|
1218
1310
|
}
|
|
1219
1311
|
});
|
|
1312
|
+
// ==================== sync ====================
|
|
1313
|
+
// 增量同步检查点。契约见 docs/SYNC_CONTRACT.md, 边界见 D-029。
|
|
1314
|
+
// 不宣称「稳定游标」: D-027 要求它在所有后端支持前不得对外宣称,
|
|
1315
|
+
// 这里只做重叠时间窗 + 本地去重。
|
|
1316
|
+
const syncCmd = program.command('sync').description('本地消息同步检查点(重叠窗口读取与覆盖报告)');
|
|
1317
|
+
syncCmd
|
|
1318
|
+
.command('run <talker>')
|
|
1319
|
+
.description('执行一次同步并写入检查点')
|
|
1320
|
+
.option('--since <date>', '起始日期或 ISO 时间(首次运行需要)')
|
|
1321
|
+
.option('--full', '读取全部历史(首次运行需要;耗时可能较长)')
|
|
1322
|
+
.option('--overlap <seconds>', '重叠窗口秒数', '300')
|
|
1323
|
+
.option('-n, --limit <number>', '本次最多读取条数(0=不限)', '0')
|
|
1324
|
+
.option('--dry-run', '仅预览,不读取消息、不写状态')
|
|
1325
|
+
.option('--yes', '确认执行')
|
|
1326
|
+
.option('--json', '输出机器可读结果')
|
|
1327
|
+
.option('--non-interactive', '禁止交互确认')
|
|
1328
|
+
.action(async (talker, opts) => {
|
|
1329
|
+
const overlap = parseCliInteger(opts.overlap, 'overlap', 0, 86400, !!opts.json);
|
|
1330
|
+
const limit = parseCliInteger(opts.limit, 'limit', 0, 1_000_000, !!opts.json);
|
|
1331
|
+
let since;
|
|
1332
|
+
if (opts.since) {
|
|
1333
|
+
try {
|
|
1334
|
+
since = parseLocalDateOrIso(opts.since);
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
const message = error instanceof DateRangeError ? error.message : '日期格式无效';
|
|
1338
|
+
if (opts.json) {
|
|
1339
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_DATE', error: message }));
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
console.error(chalk.red(`✗ ${message}`));
|
|
1343
|
+
process.exit(1);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (opts.full && since !== undefined) {
|
|
1347
|
+
if (opts.json) {
|
|
1348
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '--since 与 --full 不能同时使用' }));
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
console.error(chalk.red('✗ --since 与 --full 不能同时使用'));
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
}
|
|
1354
|
+
// Preview deliberately does not resolve the talker: that reads the
|
|
1355
|
+
// database, and a preview must not touch local data (D-025).
|
|
1356
|
+
const prior = (() => {
|
|
1357
|
+
try {
|
|
1358
|
+
return syncStateStore.read(SYNC_SOURCE, talker);
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
})();
|
|
1364
|
+
const preview = {
|
|
1365
|
+
success: true,
|
|
1366
|
+
dryRun: true,
|
|
1367
|
+
action: 'sync.run',
|
|
1368
|
+
schema: SYNC_SCHEMA,
|
|
1369
|
+
source: SYNC_SOURCE,
|
|
1370
|
+
talkerResolved: false,
|
|
1371
|
+
hasCheckpoint: !!prior,
|
|
1372
|
+
window: {
|
|
1373
|
+
from: since ?? prior?.checkpoint.newestCreateTime ?? null,
|
|
1374
|
+
overlapSeconds: overlap,
|
|
1375
|
+
full: !!opts.full,
|
|
1376
|
+
},
|
|
1377
|
+
estimatedRecords: prior?.recordsRead ?? null,
|
|
1378
|
+
readsLocalChat: true,
|
|
1379
|
+
writesLocalState: true,
|
|
1380
|
+
stableCursor: false,
|
|
1381
|
+
};
|
|
1382
|
+
if (opts.dryRun) {
|
|
1383
|
+
if (opts.json)
|
|
1384
|
+
console.log(JSON.stringify(preview));
|
|
1385
|
+
else {
|
|
1386
|
+
console.log(chalk.cyan('\n同步预览'));
|
|
1387
|
+
console.log(chalk.gray(` 范围: ${opts.full ? '全部历史' : since ? new Date(since * 1000).toLocaleString() : '上次检查点'}`));
|
|
1388
|
+
console.log(chalk.gray(` 重叠窗口: ${overlap} 秒`));
|
|
1389
|
+
console.log(chalk.gray(' 将读取本地聊天记录并写入同步检查点,不调用 AI。\n'));
|
|
1390
|
+
}
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (!opts.yes) {
|
|
1394
|
+
if (opts.json) {
|
|
1395
|
+
console.log(JSON.stringify({ ...preview, success: false, dryRun: false, code: 'CONFIRMATION_REQUIRED' }));
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1398
|
+
if (opts.nonInteractive) {
|
|
1399
|
+
console.error(chalk.red('✗ 非交互模式需要 --yes'));
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
const { confirmed } = await inquirer.prompt([{
|
|
1403
|
+
type: 'confirm',
|
|
1404
|
+
name: 'confirmed',
|
|
1405
|
+
message: opts.full
|
|
1406
|
+
? '将读取该会话的全部历史消息,可能耗时数分钟。继续吗?'
|
|
1407
|
+
: '将读取指定范围内的消息并写入本地同步检查点。继续吗?',
|
|
1408
|
+
default: false,
|
|
1409
|
+
}]);
|
|
1410
|
+
if (!confirmed) {
|
|
1411
|
+
console.log(chalk.gray('已取消'));
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (!configService.isConfigured()) {
|
|
1416
|
+
if (opts.json) {
|
|
1417
|
+
console.log(JSON.stringify({ success: false, code: 'NOT_INITIALIZED', error: '未完成初始化' }));
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
console.error(chalk.red('✗ 未完成初始化,请先运行 weflow-cli init'));
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
let resolvedTalker = '';
|
|
1424
|
+
try {
|
|
1425
|
+
resolvedTalker = await resolveTalkerCore(talker, { interactive: !opts.nonInteractive });
|
|
1426
|
+
}
|
|
1427
|
+
catch {
|
|
1428
|
+
resolvedTalker = '';
|
|
1429
|
+
}
|
|
1430
|
+
if (!resolvedTalker) {
|
|
1431
|
+
if (opts.json) {
|
|
1432
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '未找到该会话' }));
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
console.error(chalk.red('✗ 未找到该会话'));
|
|
1436
|
+
process.exit(1);
|
|
1437
|
+
}
|
|
1438
|
+
// A display name is nicety only; the state is keyed by the resolved id.
|
|
1439
|
+
let displayName = resolvedTalker;
|
|
1440
|
+
try {
|
|
1441
|
+
const match = (await chatService.listSessions()).find(s => s.username === resolvedTalker);
|
|
1442
|
+
if (match?.displayName && match.displayName !== resolvedTalker)
|
|
1443
|
+
displayName = match.displayName;
|
|
1444
|
+
}
|
|
1445
|
+
catch { /* keep the id */ }
|
|
1446
|
+
try {
|
|
1447
|
+
const result = await runSync(resolvedTalker, {
|
|
1448
|
+
since, full: !!opts.full, overlapSeconds: overlap, limit,
|
|
1449
|
+
scope: displayName,
|
|
1450
|
+
read: (who, howMany, from) => chatService.getMessagesWithShards(who, howMany, 0, from),
|
|
1451
|
+
});
|
|
1452
|
+
if (opts.json) {
|
|
1453
|
+
console.log(JSON.stringify({
|
|
1454
|
+
success: result.success,
|
|
1455
|
+
...(result.code ? { code: result.code } : {}),
|
|
1456
|
+
action: result.action,
|
|
1457
|
+
schema: SYNC_SCHEMA,
|
|
1458
|
+
source: SYNC_SOURCE,
|
|
1459
|
+
scope: result.scope,
|
|
1460
|
+
talker: resolvedTalker,
|
|
1461
|
+
window: result.window,
|
|
1462
|
+
recordsRead: result.recordsRead,
|
|
1463
|
+
recordsDeduplicated: result.recordsDeduplicated,
|
|
1464
|
+
recordsReturned: result.recordsReturned,
|
|
1465
|
+
shards: result.shards,
|
|
1466
|
+
coverage: result.coverage,
|
|
1467
|
+
mayHaveMore: result.mayHaveMore,
|
|
1468
|
+
partial: result.partial,
|
|
1469
|
+
warnings: result.warnings,
|
|
1470
|
+
jobId: result.jobId,
|
|
1471
|
+
}));
|
|
1472
|
+
if (!result.success)
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const mark = result.coverage === 'complete' ? '✓' : result.coverage === 'unverified' ? '!' : '✗';
|
|
1477
|
+
console.log(chalk.green(`\n${mark} 同步完成: ${result.recordsReturned}/${result.recordsRead} 条(去重 ${result.recordsDeduplicated})`));
|
|
1478
|
+
console.log(chalk.gray(` 覆盖: ${result.coverage} · 分片: ${result.shards?.opened ?? '?'} 读 / ${result.shards?.failed ?? 0} 失败`));
|
|
1479
|
+
for (const warning of result.warnings)
|
|
1480
|
+
console.log(chalk.yellow(` ⚠ ${warning}`));
|
|
1481
|
+
if (result.partial) {
|
|
1482
|
+
console.log(chalk.yellow(' 本次为部分完成,未推进成功时间;用 sync status 查看详情。'));
|
|
1483
|
+
}
|
|
1484
|
+
console.log('');
|
|
1485
|
+
if (!result.success)
|
|
1486
|
+
process.exit(1);
|
|
1487
|
+
}
|
|
1488
|
+
catch (error) {
|
|
1489
|
+
if (error instanceof SyncRangeRequiredError) {
|
|
1490
|
+
if (opts.json) {
|
|
1491
|
+
console.log(JSON.stringify({ success: false, code: 'SYNC_RANGE_REQUIRED', error: error.message }));
|
|
1492
|
+
process.exit(1);
|
|
1493
|
+
}
|
|
1494
|
+
console.error(chalk.red(`✗ ${error.message}`));
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
if (opts.json) {
|
|
1498
|
+
console.log(JSON.stringify({ success: false, code: 'SYNC_FAILED', error: '同步失败' }));
|
|
1499
|
+
process.exit(1);
|
|
1500
|
+
}
|
|
1501
|
+
console.error(chalk.red(`✗ 同步失败: ${error?.message || error}`));
|
|
1502
|
+
process.exit(1);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
syncCmd
|
|
1506
|
+
.command('status [talker]')
|
|
1507
|
+
.description('查看同步检查点与最近任务')
|
|
1508
|
+
.option('--json', '输出 JSON 格式')
|
|
1509
|
+
.action(async (talker, opts) => {
|
|
1510
|
+
// No database access: this has to work while the database is locked.
|
|
1511
|
+
let states = [];
|
|
1512
|
+
try {
|
|
1513
|
+
states = syncStateStore.list();
|
|
1514
|
+
}
|
|
1515
|
+
catch {
|
|
1516
|
+
states = [];
|
|
1517
|
+
}
|
|
1518
|
+
const lastJob = syncStateStore.recentJobs(1)[0] ?? null;
|
|
1519
|
+
if (talker) {
|
|
1520
|
+
const match = states.find(s => s.talker === talker || s.scope === talker);
|
|
1521
|
+
if (!match) {
|
|
1522
|
+
if (opts.json) {
|
|
1523
|
+
console.log(JSON.stringify({ success: false, code: 'SYNC_STATE_NOT_FOUND', error: '没有该会话的同步记录' }));
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
console.error(chalk.red('✗ 没有该会话的同步记录'));
|
|
1527
|
+
process.exit(1);
|
|
1528
|
+
}
|
|
1529
|
+
if (opts.json) {
|
|
1530
|
+
console.log(JSON.stringify({ success: true, schema: SYNC_SCHEMA, state: match, lastJob }));
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
console.log(chalk.cyan(`\n同步记录 — ${match.scope}`));
|
|
1534
|
+
console.log(chalk.gray(` 覆盖: ${match.coveredFrom || '—'} ~ ${match.coveredTo || '—'}`));
|
|
1535
|
+
console.log(chalk.gray(` 可信度: ${match.coverage} · 分片 ${match.shardsRead} 读 / ${match.shardsFailed} 失败`));
|
|
1536
|
+
console.log(chalk.gray(` 上次成功: ${match.lastSuccessfulRun || '—'} · 上次尝试: ${match.lastAttempt || '—'}\n`));
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (opts.json) {
|
|
1540
|
+
console.log(JSON.stringify({
|
|
1541
|
+
success: true,
|
|
1542
|
+
schema: SYNC_SCHEMA,
|
|
1543
|
+
stableCursor: false,
|
|
1544
|
+
scopes: states.map(s => ({
|
|
1545
|
+
source: s.source, scope: s.scope, talker: s.talker,
|
|
1546
|
+
lastSuccessfulRun: s.lastSuccessfulRun, lastAttempt: s.lastAttempt,
|
|
1547
|
+
coveredFrom: s.coveredFrom, coveredTo: s.coveredTo,
|
|
1548
|
+
recordsRead: s.recordsRead, recordsDeduplicated: s.recordsDeduplicated,
|
|
1549
|
+
coverage: s.coverage, shardsRead: s.shardsRead, shardsFailed: s.shardsFailed,
|
|
1550
|
+
mayHaveMore: s.mayHaveMore, warnings: s.warnings,
|
|
1551
|
+
})),
|
|
1552
|
+
lastJob,
|
|
1553
|
+
}));
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (!states.length) {
|
|
1557
|
+
console.log(chalk.gray('\n还没有任何同步记录。首次运行: weflow-cli sync run <会话> --since <日期>\n'));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
console.log(chalk.cyan(`\n同步记录 (${states.length} 个会话):\n`));
|
|
1561
|
+
for (const s of states) {
|
|
1562
|
+
const mark = s.coverage === 'complete' ? '✓' : s.coverage === 'unverified' ? '!' : '✗';
|
|
1563
|
+
console.log(` ${mark} ${s.scope} ${chalk.gray(`${s.coveredFrom || '—'} ~ ${s.coveredTo || '—'} 分片 ${s.shardsRead}/${s.shardsFailed} 失败`)}`);
|
|
1564
|
+
}
|
|
1565
|
+
console.log('');
|
|
1566
|
+
});
|
|
1567
|
+
syncCmd
|
|
1568
|
+
.command('verify <talker>')
|
|
1569
|
+
.description('核对检查点与当前数据的边界')
|
|
1570
|
+
.option('--json', '输出 JSON 格式')
|
|
1571
|
+
.action(async (talker, opts) => {
|
|
1572
|
+
// Read-only: it writes nothing, so it needs no confirmation - the same
|
|
1573
|
+
// posture as `messages`.
|
|
1574
|
+
if (!configService.isConfigured()) {
|
|
1575
|
+
if (opts.json) {
|
|
1576
|
+
console.log(JSON.stringify({ success: false, code: 'NOT_INITIALIZED', error: '未完成初始化' }));
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
}
|
|
1579
|
+
console.error(chalk.red('✗ 未完成初始化'));
|
|
1580
|
+
process.exit(1);
|
|
1581
|
+
}
|
|
1582
|
+
let state = null;
|
|
1583
|
+
try {
|
|
1584
|
+
state = syncStateStore.read(SYNC_SOURCE, talker);
|
|
1585
|
+
}
|
|
1586
|
+
catch {
|
|
1587
|
+
state = null;
|
|
1588
|
+
}
|
|
1589
|
+
if (!state)
|
|
1590
|
+
state = syncStateStore.list().find(s => s.scope === talker) ?? null;
|
|
1591
|
+
if (!state) {
|
|
1592
|
+
if (opts.json) {
|
|
1593
|
+
console.log(JSON.stringify({ success: false, code: 'SYNC_STATE_NOT_FOUND', error: '没有该会话的同步记录' }));
|
|
1594
|
+
process.exit(1);
|
|
1595
|
+
}
|
|
1596
|
+
console.error(chalk.red('✗ 没有该会话的同步记录'));
|
|
1597
|
+
process.exit(1);
|
|
1598
|
+
}
|
|
1599
|
+
const checks = [{ name: 'checkpoint-present', ok: true }];
|
|
1600
|
+
// Re-read and compare. This is the count summary of §4.4 in the absence
|
|
1601
|
+
// of a local index.
|
|
1602
|
+
const read = await chatService.getMessagesWithShards(state.talker, 0, 0);
|
|
1603
|
+
const fromSeconds = state.coveredFrom ? Math.floor(Date.parse(state.coveredFrom) / 1000) : null;
|
|
1604
|
+
const inWindow = fromSeconds === null
|
|
1605
|
+
? read.messages
|
|
1606
|
+
: read.messages.filter(m => Number(m.createTime) >= fromSeconds);
|
|
1607
|
+
checks.push({ name: 'window-re-readable', ok: inWindow.length > 0, returned: inWindow.length });
|
|
1608
|
+
const newest = read.messages.length
|
|
1609
|
+
? Math.max(...read.messages.map(m => Number(m.createTime) || 0))
|
|
1610
|
+
: null;
|
|
1611
|
+
checks.push({
|
|
1612
|
+
name: 'newest-message-present',
|
|
1613
|
+
ok: newest !== null && newest >= (state.checkpoint?.newestCreateTime ?? 0),
|
|
1614
|
+
recorded: state.checkpoint?.newestCreateTime ?? null,
|
|
1615
|
+
observed: newest,
|
|
1616
|
+
});
|
|
1617
|
+
if (read.shards) {
|
|
1618
|
+
checks.push({ name: 'shards-readable', ok: read.shards.failed === 0,
|
|
1619
|
+
scanned: read.shards.scanned, failed: read.shards.failed });
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
// Neither a pass nor a failure: this backend simply cannot say.
|
|
1623
|
+
checks.push({ name: 'shards-readable', ok: true, skipped: true,
|
|
1624
|
+
reason: 'backend-does-not-report-shards' });
|
|
1625
|
+
}
|
|
1626
|
+
const success = checks.every(c => c.ok || c.skipped);
|
|
1627
|
+
if (opts.json) {
|
|
1628
|
+
console.log(JSON.stringify({ success, schema: SYNC_SCHEMA, scope: state.scope, coverage: state.coverage, checks }));
|
|
1629
|
+
if (!success)
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
console.log(chalk.cyan(`\n核对 — ${state.scope}\n`));
|
|
1634
|
+
for (const check of checks) {
|
|
1635
|
+
const mark = check.skipped ? '–' : check.ok ? '✓' : '✗';
|
|
1636
|
+
console.log(` ${mark} ${check.name}${check.skipped ? chalk.gray(` (跳过: ${check.reason})`) : ''}`);
|
|
1637
|
+
}
|
|
1638
|
+
console.log('');
|
|
1639
|
+
if (!success)
|
|
1640
|
+
process.exit(1);
|
|
1641
|
+
});
|
|
1220
1642
|
// ==================== contacts ====================
|
|
1221
1643
|
program
|
|
1222
1644
|
.command('contacts')
|
|
@@ -1652,12 +2074,27 @@ program
|
|
|
1652
2074
|
process.exit(1);
|
|
1653
2075
|
}
|
|
1654
2076
|
if (!opts.yes) {
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
2077
|
+
// stdin 不是终端时(管道、计划任务、会话里的 `!` 前缀执行)inquirer 会抛 ExitPromptError——
|
|
2078
|
+
// 屏幕上是一条堆栈而不是一句话。这里把它变成解释:扫码登录必须在真实终端里跑。
|
|
2079
|
+
// 与守护进程那条 `--yes` 是同一类毛病:交互确认被塞进一个没人能回答的 stdin。
|
|
2080
|
+
let confirmed = false;
|
|
2081
|
+
try {
|
|
2082
|
+
const answer = await inquirer.prompt([{
|
|
2083
|
+
type: 'confirm',
|
|
2084
|
+
name: 'confirmed',
|
|
2085
|
+
message: token ? '已有登录状态,确认启动重新登录吗?' : '确认启动人工扫码登录吗?',
|
|
2086
|
+
default: false,
|
|
2087
|
+
}]);
|
|
2088
|
+
confirmed = !!answer.confirmed;
|
|
2089
|
+
}
|
|
2090
|
+
catch (error) {
|
|
2091
|
+
if (error?.name === 'ExitPromptError' || /force closed the prompt/.test(String(error?.message))) {
|
|
2092
|
+
console.log(chalk.yellow('当前环境没有可交互的终端。扫码登录必须在真实终端窗口里执行,'));
|
|
2093
|
+
console.log(chalk.yellow('或者加 --yes 跳过这个确认(扫码那一步仍然要人来做)。'));
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
throw error;
|
|
2097
|
+
}
|
|
1661
2098
|
if (!confirmed) {
|
|
1662
2099
|
console.log(chalk.gray(token ? '保持当前登录状态' : '已取消'));
|
|
1663
2100
|
return;
|
|
@@ -1681,6 +2118,20 @@ program
|
|
|
1681
2118
|
if (session.status === 'confirmed') {
|
|
1682
2119
|
console.log(chalk.green('\n✓ 登录成功!'));
|
|
1683
2120
|
console.log(chalk.gray(' 消息通道登录状态已安全保存'));
|
|
2121
|
+
// 助手的白名单为空时**拒绝所有人**:登录完对着自己的机器人说话,什么都不会回来。
|
|
2122
|
+
// 所以这里必须说清"下一步该干什么"。
|
|
2123
|
+
// 只打在人看的这条路上——`--json` 那条路照旧不返回账号标识。
|
|
2124
|
+
console.log('');
|
|
2125
|
+
console.log(chalk.cyan(' 助手默认拒绝所有人(白名单为空)。'));
|
|
2126
|
+
// 这里**不**替用户写白名单:登录响应给的是 ilink_user_id,而白名单要的是入站消息里
|
|
2127
|
+
// 那个 from_user_id(文档里写成 <@im.wechat ID>),两者是不是同一个值**没有被验证过**
|
|
2128
|
+
// ——猜错的后果是"白名单非空、看着配好了、却仍然拒你"。真值在第一条被拒的消息里,
|
|
2129
|
+
// 所以让它自己现形(见 assistant log 的 [首次配置] 那行)。
|
|
2130
|
+
console.log(chalk.gray(' 启用方式:先 assistant start,从你的微信给机器人发一条消息,'));
|
|
2131
|
+
console.log(chalk.gray(' 日志里会出现一行 ' + chalk.cyan('[首次配置]')
|
|
2132
|
+
+ chalk.gray(' 带着你自己的发送者 ID 和该执行的命令。')));
|
|
2133
|
+
console.log(chalk.gray(' 只想先看它「本来会怎么答」而不改行为:')
|
|
2134
|
+
+ chalk.cyan('weflow-cli config set assistantFastRoute log'));
|
|
1684
2135
|
}
|
|
1685
2136
|
else {
|
|
1686
2137
|
console.log(chalk.red(`\n✗ 登录失败: ${session.error || '超时'}`));
|
|
@@ -3288,13 +3739,18 @@ program
|
|
|
3288
3739
|
'005_Reference/Tools',
|
|
3289
3740
|
'005_Reference/Methods',
|
|
3290
3741
|
'006_Projects',
|
|
3291
|
-
|
|
3742
|
+
// 这里原先还有 `007_Wiki/Concepts`:概念页实际写在下面的顶层 `Wiki/Concepts`
|
|
3743
|
+
// (`compile_wiki` 的 OUTPUT_ROOT)。两个都列,就建出一个永远空着的目录。
|
|
3292
3744
|
'008_MOC',
|
|
3293
3745
|
'999_Archive',
|
|
3294
3746
|
'_attachments',
|
|
3747
|
+
// `Wiki/Concepts` 是唯一真的有人写的 Wiki 子目录(`compile_wiki` 的
|
|
3748
|
+
// OUTPUT_ROOT)。这里曾经还列着 `Wiki/Entities` 与 `Wiki/Topics`——没有任何
|
|
3749
|
+
// 代码写它们,README 里那张表却把它们当成已有的介绍给用户。**声明了就要有东西
|
|
3750
|
+
// 往里写**,否则就是又一个"永远空着的目录"(同 `007_Wiki`)。
|
|
3751
|
+
// 两个知识库各一个目录:`Wiki/Concepts` 是公众号文章线的,`Chat/Concepts` 是聊天线的
|
|
3295
3752
|
'Wiki/Concepts',
|
|
3296
|
-
'
|
|
3297
|
-
'Wiki/Topics',
|
|
3753
|
+
'Chat/Concepts',
|
|
3298
3754
|
];
|
|
3299
3755
|
const files = [
|
|
3300
3756
|
{
|
|
@@ -3302,7 +3758,10 @@ program
|
|
|
3302
3758
|
content: JSON.stringify({
|
|
3303
3759
|
newFileLocation: 'folder',
|
|
3304
3760
|
newFileFolderPath: 'Sources',
|
|
3305
|
-
|
|
3761
|
+
// **`_attachments`,不是 `Assets`**:布局里建的就是 `_attachments`(这个
|
|
3762
|
+
// 字面量以前只在这里出现一次,是个孤例),而 init 从不建 `Assets`——
|
|
3763
|
+
// 于是附件会被 Obsidian 放进一个不存在的目录。
|
|
3764
|
+
attachmentFolderPath: '_attachments',
|
|
3306
3765
|
showInlineTitle: false,
|
|
3307
3766
|
}, null, 2),
|
|
3308
3767
|
},
|
|
@@ -3348,11 +3807,20 @@ program
|
|
|
3348
3807
|
'',
|
|
3349
3808
|
'| 目录 | 说明 |',
|
|
3350
3809
|
'|------|------|',
|
|
3351
|
-
'| `Sources/WeChat
|
|
3352
|
-
'| `
|
|
3353
|
-
'| `
|
|
3354
|
-
|
|
3810
|
+
'| `Sources/WeChat/<日期>/` | 原始文章(`daily` 与回填写入,连同正文图片) |',
|
|
3811
|
+
'| `001_Daily/<日期>.md` | 每天一页日记 |',
|
|
3812
|
+
'| `002_Literature/WeChat/<日期>/` | 阅读笔记(由 `Sources` 生成) |',
|
|
3813
|
+
// **两个知识库各写一行**:文章线(`Wiki/`)与聊天线(`Chat/`)是分开的两套,
|
|
3814
|
+
// 只列文章那套的话,用户在库里看到的说明与他实际拥有的东西对不上。
|
|
3815
|
+
'| `Wiki/Concepts/`、`Wiki/00-Overview.md` | **文章知识库**的概念页与索引(`wiki compile` 生成) |',
|
|
3816
|
+
'| `Chat/Concepts/`、`Chat/00-Overview.md` | **聊天知识库**的概念页与索引(`wiki compile --source output/chat-notes` 生成) |',
|
|
3817
|
+
'| `Sources/Chat/会话-<名字>.md` | 每个会话一张知识卡(`chat` 生成:聊了什么、有哪些人、欠着什么) |',
|
|
3355
3818
|
'| `Templates/` | 模板文件 |',
|
|
3819
|
+
'| `_attachments/` | Obsidian 附件(`.obsidian/app.json` 的 `attachmentFolderPath` 指向这里) |',
|
|
3820
|
+
'| `000_Inbox` `003_Ideas` `004_Permanent` `005_Reference` `006_Projects` `008_MOC` `999_Archive` | **给你自己写笔记用的层**:`vault promote` 才会往里放东西,空着是设计而不是没做完 |',
|
|
3821
|
+
'',
|
|
3822
|
+
'> 阅读笔记与日记里各有一段 `dataview` 查询,所以这个 Vault 需要 **Dataview 社区插件**',
|
|
3823
|
+
'> (社区插件不进仓库,换机器要自己装一次)。',
|
|
3356
3824
|
'',
|
|
3357
3825
|
'## 快速查询',
|
|
3358
3826
|
'',
|
|
@@ -3466,22 +3934,43 @@ program
|
|
|
3466
3934
|
.addCommand(new Command('compile')
|
|
3467
3935
|
.description('扫描文章 [[Wikilinks]] 聚合生成概念页')
|
|
3468
3936
|
.option('-l, --limit <n>', '最多生成概念数', '20')
|
|
3469
|
-
.option('--
|
|
3937
|
+
.option('--min-refs <n>', '至少被几篇材料提到才建页(2 能滤掉新闻里的一次性实体)', '1')
|
|
3938
|
+
.option('--source <dir>', '材料目录(**必须含 [[概念]] — 说明 形状的链接**,否则聚合不出任何概念)', './output/biz-daily')
|
|
3470
3939
|
.option('-o, --output <dir>', '概念页输出目录', './output/wechat-vault/Wiki/Concepts')
|
|
3471
3940
|
.option('--api-key <key>', 'DeepSeek API key')
|
|
3941
|
+
.option('--refresh-sources', '只把已有概念页的"来源"段补齐成当前全部来源(本地重写,**不调用模型**)')
|
|
3942
|
+
.option('--cards <dir...>', '产卡目录,可写多个;默认所有 output/*-notes(与索引的引用数同一口径)')
|
|
3472
3943
|
.option('--dry-run', '仅预览,不读取文章、调用 AI 或写入概念页')
|
|
3473
3944
|
.option('--yes', '确认调用 AI 并生成概念页')
|
|
3474
3945
|
.option('--json', '输出机器可读结果,不返回概念或本地路径')
|
|
3475
3946
|
.action(async (opts) => {
|
|
3476
3947
|
const pkgRoot = resolvePackageRoot();
|
|
3477
3948
|
const script = join(pkgRoot, 'scripts', 'compile_wiki.py');
|
|
3949
|
+
if (opts.refreshSources) {
|
|
3950
|
+
// 本地补来源:**不调模型**,所以不传 apiKey、preview 里也不说"调用 AI"。
|
|
3951
|
+
// 但它会把已有页重写一遍(实测会动 358 张),照样走"先预览、再确认"那两步。
|
|
3952
|
+
await runConfirmedPythonMutation({
|
|
3953
|
+
action: 'wiki.refreshSources',
|
|
3954
|
+
script,
|
|
3955
|
+
args: ['--refresh-sources', '--output', opts.output,
|
|
3956
|
+
...(opts.cards ?? []).flatMap((c) => ['--cards', c])],
|
|
3957
|
+
cliOptions: opts,
|
|
3958
|
+
preview: { readsLocalCards: true, rewritesExistingPages: true, usesAi: false },
|
|
3959
|
+
confirmationMessage: `确认把 ${opts.output} 里已有概念页的"来源"段补齐?(本地重写,不调用 AI)`,
|
|
3960
|
+
apiKey: opts.apiKey,
|
|
3961
|
+
timeout: 300_000,
|
|
3962
|
+
});
|
|
3963
|
+
return;
|
|
3964
|
+
}
|
|
3478
3965
|
const limit = parseCliInteger(opts.limit, 'limit', 1, 1000, opts.json);
|
|
3966
|
+
const minRefs = parseCliInteger(opts.minRefs ?? '1', 'min-refs', 1, 50, opts.json);
|
|
3479
3967
|
await runConfirmedPythonMutation({
|
|
3480
3968
|
action: 'wiki.compile',
|
|
3481
3969
|
script,
|
|
3482
|
-
args: ['--limit', String(limit), '--source', opts.source, '--output', opts.output
|
|
3970
|
+
args: ['--limit', String(limit), '--source', opts.source, '--output', opts.output,
|
|
3971
|
+
'--min-refs', String(minRefs)],
|
|
3483
3972
|
cliOptions: opts,
|
|
3484
|
-
preview: { limit, readsLocalArticles: true, usesAi: true, writesConceptPages: true },
|
|
3973
|
+
preview: { limit, minRefs, readsLocalArticles: true, usesAi: true, writesConceptPages: true },
|
|
3485
3974
|
confirmationMessage: `确认调用 AI 并生成最多 ${limit} 个概念页?`,
|
|
3486
3975
|
apiKey: opts.apiKey,
|
|
3487
3976
|
timeout: 300_000,
|
|
@@ -3505,6 +3994,7 @@ program
|
|
|
3505
3994
|
.option('--skip-html', '跳过 HTML 阅读器生成')
|
|
3506
3995
|
.option('--skip-ai-report', '跳过 AI 深度阅读报告')
|
|
3507
3996
|
.option('--no-ai', '关闭全部 AI 调用,保留抓取和本地输出')
|
|
3997
|
+
.option('--no-summary', '只判断不生成:biz_daily 不调 LLM 写摘要/标签/简报(判断仍走 Jev)')
|
|
3508
3998
|
.option('--ai-report-range <n>', 'AI 报告覆盖最近 N 天', '1')
|
|
3509
3999
|
.option('--dry-run', '仅预览步骤,不读取聊天数据、调用网络或写入文件')
|
|
3510
4000
|
.option('--yes', '确认运行流水线')
|
|
@@ -3602,6 +4092,8 @@ program
|
|
|
3602
4092
|
args.push('--skip-ai-report');
|
|
3603
4093
|
if (noAi)
|
|
3604
4094
|
args.push('--no-ai');
|
|
4095
|
+
if (opts.noSummary && !noAi)
|
|
4096
|
+
args.push('--no-summary');
|
|
3605
4097
|
try {
|
|
3606
4098
|
if (!opts.json)
|
|
3607
4099
|
console.log(chalk.cyan('\n启动端到端流水线...\n'));
|
|
@@ -4589,15 +5081,27 @@ program
|
|
|
4589
5081
|
.description('构建语义搜索索引')
|
|
4590
5082
|
.option('--full', '全量重建')
|
|
4591
5083
|
.option('--api-key <key>', 'DashScope embedding API key')
|
|
5084
|
+
.option('--days <n>', '聊天记录收最近多少天(默认 90)')
|
|
5085
|
+
.option('--article-days <n>', '日报文章收最近多少个日期目录(默认 30)')
|
|
4592
5086
|
.option('--dry-run', '仅预览,不读取数据库、调用网络或写入索引')
|
|
4593
5087
|
.option('--yes', '确认构建或重建索引')
|
|
4594
5088
|
.option('--json', '输出 JSON 格式')
|
|
4595
5089
|
.action(async (opts) => {
|
|
5090
|
+
// 窗口的**运行值由这里说了算**:默认值 + 校验 + 透传都在这一处,预览报的就是真跑的那些
|
|
5091
|
+
// (脚本文档里另有自己的默认值,只供手动直接跑脚本时用;CLI 这条路永远显式传参,
|
|
5092
|
+
// 所以两边万一漂了也不会影响实际行为)。
|
|
5093
|
+
// 上限 36500 天≈100 年:不设上限的后果是"给个 1e9 就把遍历变成一次全库穿透"。
|
|
5094
|
+
const chatDays = opts.days === undefined ? 90 : parseCliInteger(opts.days, 'days', 1, 36_500, opts.json);
|
|
5095
|
+
const articleDays = opts.articleDays === undefined
|
|
5096
|
+
? 30 : parseCliInteger(opts.articleDays, 'article-days', 1, 36_500, opts.json);
|
|
5097
|
+
const windowText = `聊天最近 ${chatDays} 天、日报最近 ${articleDays} 个日期目录`;
|
|
4596
5098
|
const preview = {
|
|
4597
5099
|
success: true,
|
|
4598
5100
|
dryRun: true,
|
|
4599
5101
|
action: 'search-index.build',
|
|
4600
5102
|
mode: opts.full ? 'full' : 'incremental',
|
|
5103
|
+
chatDays,
|
|
5104
|
+
articleDays,
|
|
4601
5105
|
readsLocalData: true,
|
|
4602
5106
|
usesCloudEmbedding: true,
|
|
4603
5107
|
replacesExistingIndex: !!opts.full,
|
|
@@ -4606,7 +5110,8 @@ program
|
|
|
4606
5110
|
if (opts.json)
|
|
4607
5111
|
console.log(JSON.stringify(preview));
|
|
4608
5112
|
else
|
|
4609
|
-
console.log(chalk.cyan(`语义索引预览:${opts.full ? '全量重建' : '增量构建'}
|
|
5113
|
+
console.log(chalk.cyan(`语义索引预览:${opts.full ? '全量重建' : '增量构建'};`
|
|
5114
|
+
+ `窗口=${windowText}(要改:--days / --article-days),将读取本地数据并调用向量服务`));
|
|
4610
5115
|
return;
|
|
4611
5116
|
}
|
|
4612
5117
|
if (!opts.yes) {
|
|
@@ -4617,7 +5122,10 @@ program
|
|
|
4617
5122
|
const { confirmed } = await inquirer.prompt([{
|
|
4618
5123
|
type: 'confirm',
|
|
4619
5124
|
name: 'confirmed',
|
|
4620
|
-
|
|
5125
|
+
// 窗口是**代价的一部分**:天数越多,发给向量服务的文本越多。所以确认语里要写出来,
|
|
5126
|
+
// 而不是只说"本地文本将发送"。
|
|
5127
|
+
message: `确认${opts.full ? '全量重建' : '增量构建'}语义索引(${windowText})?`
|
|
5128
|
+
+ '这些文本将发送到已配置的向量服务。',
|
|
4621
5129
|
default: false,
|
|
4622
5130
|
}]);
|
|
4623
5131
|
if (!confirmed) {
|
|
@@ -4630,7 +5138,8 @@ program
|
|
|
4630
5138
|
const execFileAsync = promisify(execFile);
|
|
4631
5139
|
const pkgRoot = resolvePackageRoot();
|
|
4632
5140
|
const script = join(pkgRoot, 'scripts', 'semantic_search.py');
|
|
4633
|
-
const args = [script, 'build'
|
|
5141
|
+
const args = [script, 'build', '--days', String(chatDays),
|
|
5142
|
+
'--article-days', String(articleDays)];
|
|
4634
5143
|
if (opts.full)
|
|
4635
5144
|
args.push('--full');
|
|
4636
5145
|
try {
|
|
@@ -4835,28 +5344,77 @@ program
|
|
|
4835
5344
|
await runPython('scripts/annual_report.py', a, opts.apiKey);
|
|
4836
5345
|
}
|
|
4837
5346
|
});
|
|
4838
|
-
//
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
5347
|
+
// decide —— 本机判断层。一个 state + 一批类型化问题,一次调用。
|
|
5348
|
+
// state 由调用方给,命令自己**不读任何本地数据**;但它是出境调用,所以照 awaiting/search
|
|
5349
|
+
// 的规矩:--dry-run 只校验、--yes 才真跑。这一版**不暴露成 MCP 工具**——那会变成
|
|
5350
|
+
// 远端 MCP 客户端能驱动本机往第三方发任意文本,属于要单独决策的出境面。
|
|
5351
|
+
program
|
|
5352
|
+
.command('decide')
|
|
5353
|
+
.description('本机判断层:一个 state + 一批类型化问题,一次调用返回带概率的答案')
|
|
5354
|
+
.option('--request <file>', '请求 JSON 的路径(含 state 与 questions)')
|
|
5355
|
+
.option('--over <glob>', '批量模式:对匹配到的每个文件问 --ask 里的每个问题(可重复)', (value, previous) => previous.concat([value]), [])
|
|
5356
|
+
.option('--ask <text>', '批量模式下的一个是非题,逐文件展开(可重复)', (value, previous) => previous.concat([value]), [])
|
|
5357
|
+
.option('--max-chars <n>', '批量模式下每个文件喂多少字符(默认 1500;它决定上限)', '1500')
|
|
5358
|
+
.option('--limit <n>', '批量模式最多取多少个文件(默认 50)', '50')
|
|
5359
|
+
.option('--model <name>', '模型别名,默认 jev-latest')
|
|
5360
|
+
.option('--dry-run', '只校验请求并回显形状,不调用决策模型')
|
|
5361
|
+
.option('--yes', '确认把 state 发送到已配置的决策模型服务')
|
|
5362
|
+
.option('--json', '输出机器可读结果(本命令的输出本来就是 JSON)')
|
|
5363
|
+
.action(async (opts) => {
|
|
5364
|
+
const { execFile } = await import('child_process');
|
|
5365
|
+
const { promisify } = await import('util');
|
|
5366
|
+
const { readFileSync } = await import('fs');
|
|
5367
|
+
const execFileAsync = promisify(execFile);
|
|
5368
|
+
const pkgRoot = resolvePackageRoot();
|
|
5369
|
+
const decideScript = join(pkgRoot, 'scripts', 'decide.py');
|
|
5370
|
+
// 预览里要报"这批几个问题",所以在这里读一眼;**校验仍然只由脚本做**,
|
|
5371
|
+
// 免得两处校验逻辑各说各话。
|
|
5372
|
+
// 两种输入方式:--request 一个文件、或 --over 批量展开。脚本自己会拒掉
|
|
5373
|
+
// 两个都没给的情况,所以这里不做重复校验。
|
|
5374
|
+
const batch = Array.isArray(opts.over) && opts.over.length > 0;
|
|
5375
|
+
let questionCount = null;
|
|
5376
|
+
if (batch) {
|
|
5377
|
+
questionCount = opts.over.length * opts.ask.length || null;
|
|
5378
|
+
}
|
|
5379
|
+
else if (opts.request) {
|
|
5380
|
+
try {
|
|
5381
|
+
const parsed = JSON.parse(readFileSync(opts.request, 'utf8'));
|
|
5382
|
+
if (parsed && typeof parsed === 'object' && parsed.questions
|
|
5383
|
+
&& typeof parsed.questions === 'object') {
|
|
5384
|
+
questionCount = Object.keys(parsed.questions).length;
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
catch {
|
|
5388
|
+
// 读不到或不是 JSON:交给脚本去报一个准确得多的错误。
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
|
|
5392
|
+
const maxChars = parseCliInteger(opts.maxChars, 'max-chars', 1, 100000, opts.json);
|
|
5393
|
+
const args = [decideScript,
|
|
5394
|
+
...(batch
|
|
5395
|
+
? [...opts.over.flatMap((g) => ['--over', g]),
|
|
5396
|
+
...opts.ask.flatMap((a) => ['--ask', a]),
|
|
5397
|
+
'--max-chars', String(maxChars), '--limit', String(limit)]
|
|
5398
|
+
: ['--request', opts.request]),
|
|
5399
|
+
...(opts.model ? ['--model', opts.model] : [])];
|
|
4843
5400
|
if (opts.dryRun) {
|
|
4844
|
-
|
|
4845
|
-
return;
|
|
5401
|
+
args.push('--dry-run');
|
|
4846
5402
|
}
|
|
4847
|
-
if (!opts.yes) {
|
|
5403
|
+
else if (!opts.yes) {
|
|
5404
|
+
const preview = {
|
|
5405
|
+
success: false, dryRun: false, action: 'decide',
|
|
5406
|
+
code: 'CONFIRMATION_REQUIRED',
|
|
5407
|
+
questionCount, readsLocalData: batch, invokesAI: true,
|
|
5408
|
+
};
|
|
4848
5409
|
if (opts.json) {
|
|
4849
|
-
console.log(JSON.stringify(
|
|
4850
|
-
success: false,
|
|
4851
|
-
code: 'CONFIRMATION_REQUIRED',
|
|
4852
|
-
error: '先使用 --dry-run --json 预览,再由用户确认后使用 --yes --json 执行',
|
|
4853
|
-
}));
|
|
5410
|
+
console.log(JSON.stringify(preview));
|
|
4854
5411
|
process.exit(1);
|
|
4855
5412
|
}
|
|
4856
5413
|
const { confirmed } = await inquirer.prompt([{
|
|
4857
5414
|
type: 'confirm',
|
|
4858
5415
|
name: 'confirmed',
|
|
4859
|
-
message:
|
|
5416
|
+
message: `确认把请求里的 state 发送到已配置的决策模型服务吗?`
|
|
5417
|
+
+ (questionCount === null ? '' : `(${questionCount} 个问题)`),
|
|
4860
5418
|
default: false,
|
|
4861
5419
|
}]);
|
|
4862
5420
|
if (!confirmed) {
|
|
@@ -4864,40 +5422,514 @@ async function runTodoMutation(action, id, opts) {
|
|
|
4864
5422
|
return;
|
|
4865
5423
|
}
|
|
4866
5424
|
}
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
confirmationMessage: `确认读取最近 ${days} 天聊天并发送到已配置的 AI 服务以提取待办?`,
|
|
4887
|
-
apiKey: opts.apiKey,
|
|
4888
|
-
});
|
|
5425
|
+
try {
|
|
5426
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5427
|
+
timeout: 120_000, maxBuffer: 10 * 1024 * 1024,
|
|
5428
|
+
env: pythonProcessEnv(),
|
|
5429
|
+
});
|
|
5430
|
+
process.stdout.write(stdout);
|
|
5431
|
+
}
|
|
5432
|
+
catch (error) {
|
|
5433
|
+
const payload = error.stdout;
|
|
5434
|
+
if (payload && payload.trim()) {
|
|
5435
|
+
// 脚本自己已经给出了结构化的失败,原样透传比自己编一个更有用。
|
|
5436
|
+
process.stdout.write(payload);
|
|
5437
|
+
}
|
|
5438
|
+
else {
|
|
5439
|
+
console.log(JSON.stringify({ success: false, code: 'DECIDE_FAILED',
|
|
5440
|
+
action: 'decide', error: safeSubprocessError(error) }));
|
|
5441
|
+
}
|
|
5442
|
+
process.exit(1);
|
|
5443
|
+
}
|
|
4889
5444
|
});
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
.
|
|
4895
|
-
.
|
|
5445
|
+
// awaiting —— 谁在等我回话。读本地聊天并把正文发给决策模型,所以照 search 的规矩:
|
|
5446
|
+
// --dry-run 预览、--yes 才真跑。它**不写任何本地数据**,但仍是一次正文出境,
|
|
5447
|
+
// 而聊天正文比日报的文章正文敏感一档。
|
|
5448
|
+
program
|
|
5449
|
+
.command('awaiting')
|
|
5450
|
+
.description('谁在等我回话:逐会话判断有没有欠下的回复(读取聊天正文并调用决策模型)')
|
|
5451
|
+
.option('--days <n>', '只看最近多少天有动静的会话', '14')
|
|
5452
|
+
.option('--limit <n>', '最多判多少个会话', '40')
|
|
5453
|
+
.option('--min-prob <p>', 'waiting 概率低于此值不算欠账', '0.5')
|
|
5454
|
+
.option('--dry-run', '仅预览会判哪些会话、要发多少字符,不调用决策模型')
|
|
5455
|
+
.option('--yes', '确认把聊天正文发送到已配置的决策模型服务')
|
|
5456
|
+
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
4896
5457
|
.action(async (opts) => {
|
|
4897
|
-
const
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
5458
|
+
const { execFile } = await import('child_process');
|
|
5459
|
+
const { promisify } = await import('util');
|
|
5460
|
+
const execFileAsync = promisify(execFile);
|
|
5461
|
+
const pkgRoot = resolvePackageRoot();
|
|
5462
|
+
const script = join(pkgRoot, 'scripts', 'reply_debt.py');
|
|
5463
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
|
|
5464
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
|
|
5465
|
+
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
5466
|
+
'--min-prob', String(opts.minProb),
|
|
5467
|
+
...(opts.json ? ['--json'] : [])];
|
|
5468
|
+
if (opts.dryRun) {
|
|
5469
|
+
// 交给脚本自己报数:它才知道要发多少字符。这一步只读本地,零出境。
|
|
5470
|
+
args.push('--dry-run');
|
|
5471
|
+
}
|
|
5472
|
+
else if (!opts.yes) {
|
|
5473
|
+
const preview = {
|
|
5474
|
+
success: false, dryRun: false, action: 'reply-debt.scan',
|
|
5475
|
+
code: 'CONFIRMATION_REQUIRED', days, limit,
|
|
5476
|
+
readsLocalChat: true, invokesAI: true, writesNothing: true,
|
|
5477
|
+
};
|
|
5478
|
+
if (opts.json) {
|
|
5479
|
+
console.log(JSON.stringify(preview));
|
|
5480
|
+
process.exit(1);
|
|
5481
|
+
}
|
|
5482
|
+
const { confirmed } = await inquirer.prompt([{
|
|
5483
|
+
type: 'confirm',
|
|
5484
|
+
name: 'confirmed',
|
|
5485
|
+
message: '确认把最近这些会话的聊天正文发送到已配置的决策模型服务吗?',
|
|
5486
|
+
default: false,
|
|
5487
|
+
}]);
|
|
5488
|
+
if (!confirmed) {
|
|
5489
|
+
console.log(chalk.gray('已取消'));
|
|
5490
|
+
return;
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
try {
|
|
5494
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5495
|
+
timeout: 600_000, maxBuffer: 50 * 1024 * 1024,
|
|
5496
|
+
env: pythonProcessEnv(),
|
|
5497
|
+
});
|
|
5498
|
+
process.stdout.write(stdout);
|
|
5499
|
+
}
|
|
5500
|
+
catch (error) {
|
|
5501
|
+
if (opts.json) {
|
|
5502
|
+
console.log(JSON.stringify({ success: false, code: 'AWAITING_FAILED', action: 'reply-debt.scan', error: safeSubprocessError(error) }));
|
|
5503
|
+
}
|
|
5504
|
+
else {
|
|
5505
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5506
|
+
}
|
|
5507
|
+
process.exit(1);
|
|
5508
|
+
}
|
|
5509
|
+
});
|
|
5510
|
+
// draft:判断在前、起草在后、**绝不发送**
|
|
5511
|
+
program
|
|
5512
|
+
.command('draft <talker>')
|
|
5513
|
+
.description('帮我起草回复:先判断(意图/风险/该不该给实质),再起草候选 —— **只产出文本,不发送**')
|
|
5514
|
+
.option('--count <n>', '要几条候选', '3')
|
|
5515
|
+
.option('--gate-commitment', '承诺未兑现时也拒绝起草(默认只在提示词里约束)')
|
|
5516
|
+
.option('--dry-run', '仅预览会发多少字符给哪两个模型,不调用')
|
|
5517
|
+
.option('--yes', '确认把这段对话发送到判断模型与生成模型')
|
|
5518
|
+
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5519
|
+
.action(async (talker, opts) => {
|
|
5520
|
+
const { execFile } = await import('child_process');
|
|
5521
|
+
const { promisify } = await import('util');
|
|
5522
|
+
const execFileAsync = promisify(execFile);
|
|
5523
|
+
const pkgRoot = resolvePackageRoot();
|
|
5524
|
+
const script = join(pkgRoot, 'scripts', 'draft_reply.py');
|
|
5525
|
+
const count = parseCliInteger(opts.count, 'count', 1, 5, opts.json);
|
|
5526
|
+
// **"确认过了"只用一个变量表示**(`--yes` 或交互回答"是"),参数在确认之后才拼。
|
|
5527
|
+
// 原来的写法是先拼参数、再问,于是**交互确认那条路是坏的**:答了"是",`args` 里
|
|
5528
|
+
// 却从没补上 `--yes`,脚本照样拒绝(用户看到的是"我确认了它却说没确认")。
|
|
5529
|
+
// 两者合成一个变量之后,这种漏传在结构上不可能再发生。
|
|
5530
|
+
let confirmed = !!opts.yes;
|
|
5531
|
+
if (!opts.dryRun && !confirmed) {
|
|
5532
|
+
const preview = {
|
|
5533
|
+
success: false, dryRun: false, action: 'draft-reply',
|
|
5534
|
+
code: 'CONFIRMATION_REQUIRED', talker, count,
|
|
5535
|
+
readsLocalChat: true, invokesAI: true, writesNothing: true, sendsNothing: true,
|
|
5536
|
+
};
|
|
5537
|
+
if (opts.json) {
|
|
5538
|
+
console.log(JSON.stringify(preview));
|
|
5539
|
+
process.exit(1);
|
|
5540
|
+
}
|
|
5541
|
+
const answer = await inquirer.prompt([{
|
|
5542
|
+
type: 'confirm',
|
|
5543
|
+
name: 'confirmed',
|
|
5544
|
+
message: '确认把这段对话发送到判断模型与生成模型吗?(只会产出候选文本,不会替你发送)',
|
|
5545
|
+
default: false,
|
|
5546
|
+
}]);
|
|
5547
|
+
confirmed = !!answer.confirmed;
|
|
5548
|
+
if (!confirmed) {
|
|
5549
|
+
console.log(chalk.gray('已取消'));
|
|
5550
|
+
return;
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
const args = [script, '--talker', String(talker), '--count', String(count),
|
|
5554
|
+
// 脚本自己有第二道闸门(`--yes` 才真的把对话发出去),所以要透传
|
|
5555
|
+
...(confirmed ? ['--yes'] : []),
|
|
5556
|
+
...(opts.gateCommitment ? ['--gate-commitment'] : []),
|
|
5557
|
+
...(opts.json ? ['--json'] : []),
|
|
5558
|
+
// dry-run 交给脚本自己报数(它才知道这段对话有多少字符),这一步只读本地、零出境
|
|
5559
|
+
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5560
|
+
try {
|
|
5561
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5562
|
+
timeout: 300_000, maxBuffer: 50 * 1024 * 1024,
|
|
5563
|
+
env: pythonProcessEnv(),
|
|
5564
|
+
});
|
|
5565
|
+
process.stdout.write(stdout);
|
|
5566
|
+
}
|
|
5567
|
+
catch (error) {
|
|
5568
|
+
if (opts.json) {
|
|
5569
|
+
console.log(JSON.stringify({ success: false, code: 'DRAFT_FAILED', action: 'draft-reply', error: safeSubprocessError(error) }));
|
|
5570
|
+
}
|
|
5571
|
+
else {
|
|
5572
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5573
|
+
}
|
|
5574
|
+
process.exit(1);
|
|
5575
|
+
}
|
|
5576
|
+
});
|
|
5577
|
+
// wiki lint:知识库体检(全本地:不联网、不调用模型)
|
|
5578
|
+
program.commands.find(c => c.name() === 'wiki')?.addCommand(new Command('lint')
|
|
5579
|
+
.description('知识库体检:断链、孤儿、空页、同名(本地,不联网)')
|
|
5580
|
+
.option('--dir <dir>', '概念页目录', './output/wechat-vault/Wiki/Concepts')
|
|
5581
|
+
.option('--json', '输出机器可读结果')
|
|
5582
|
+
.action(async (opts) => {
|
|
5583
|
+
const { execFile } = await import('child_process');
|
|
5584
|
+
const { promisify } = await import('util');
|
|
5585
|
+
const execFileAsync = promisify(execFile);
|
|
5586
|
+
const script = join(resolvePackageRoot(), 'scripts', 'wiki_lint.py');
|
|
5587
|
+
const args = [script, '--dir', String(opts.dir), ...(opts.json ? ['--json'] : [])];
|
|
5588
|
+
try {
|
|
5589
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5590
|
+
timeout: 120_000, maxBuffer: 20 * 1024 * 1024, env: pythonProcessEnv(),
|
|
5591
|
+
});
|
|
5592
|
+
process.stdout.write(stdout);
|
|
5593
|
+
}
|
|
5594
|
+
catch (error) {
|
|
5595
|
+
if (opts.json)
|
|
5596
|
+
console.log(JSON.stringify({ success: false, code: 'WIKI_LINT_FAILED', error: safeSubprocessError(error) }));
|
|
5597
|
+
else
|
|
5598
|
+
console.error(chalk.red(`
|
|
5599
|
+
✗ ${safeSubprocessError(error)}`));
|
|
5600
|
+
process.exit(1);
|
|
5601
|
+
}
|
|
5602
|
+
}));
|
|
5603
|
+
// chat-notes:把会话整理成知识卡,喂给 wiki compile 聚成概念页
|
|
5604
|
+
program
|
|
5605
|
+
.command('chat-notes')
|
|
5606
|
+
.description('把最近一段时间的会话整理成知识卡(之后用 wiki compile 聚成概念页)—— **只写本地文件,不发送**')
|
|
5607
|
+
.option('--days <n>', '看最近多少天', '30')
|
|
5608
|
+
.option('--limit <n>', '最多处理几个会话', '40')
|
|
5609
|
+
.option('--dry-run', '仅预览:几个会话、多少字符会发给生成模型(只读本地,零出境)')
|
|
5610
|
+
.option('--yes', '确认把对话发送给生成模型')
|
|
5611
|
+
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5612
|
+
.action(async (opts) => {
|
|
5613
|
+
const { execFile } = await import('child_process');
|
|
5614
|
+
const { promisify } = await import('util');
|
|
5615
|
+
const execFileAsync = promisify(execFile);
|
|
5616
|
+
const pkgRoot = resolvePackageRoot();
|
|
5617
|
+
const script = join(pkgRoot, 'scripts', 'chat_notes.py');
|
|
5618
|
+
const days = parseCliInteger(opts.days, 'days', 1, 3650, opts.json);
|
|
5619
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 200, opts.json);
|
|
5620
|
+
// 与 `draft` 同一条纪律:**"确认过了"只用一个变量**,参数在确认之后才拼
|
|
5621
|
+
let confirmed = !!opts.yes;
|
|
5622
|
+
if (!opts.dryRun && !confirmed) {
|
|
5623
|
+
const preview = {
|
|
5624
|
+
success: false, dryRun: false, action: 'chat-notes',
|
|
5625
|
+
code: 'CONFIRMATION_REQUIRED', days, limit,
|
|
5626
|
+
readsLocalChat: true, invokesAI: true, writesLocalFiles: true, sendsNothing: true,
|
|
5627
|
+
};
|
|
5628
|
+
if (opts.json) {
|
|
5629
|
+
console.log(JSON.stringify(preview));
|
|
5630
|
+
process.exit(1);
|
|
5631
|
+
}
|
|
5632
|
+
const answer = await inquirer.prompt([{
|
|
5633
|
+
type: 'confirm',
|
|
5634
|
+
name: 'confirmed',
|
|
5635
|
+
message: '确认把最近这些会话的对话发送给生成模型吗?(只会产出本地知识卡,不会替你发送任何消息)',
|
|
5636
|
+
default: false,
|
|
5637
|
+
}]);
|
|
5638
|
+
confirmed = !!answer.confirmed;
|
|
5639
|
+
if (!confirmed) {
|
|
5640
|
+
console.log(chalk.gray('已取消'));
|
|
5641
|
+
return;
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
const args = [script, '--days', String(days), '--limit', String(limit),
|
|
5645
|
+
...(confirmed ? ['--yes'] : []),
|
|
5646
|
+
...(opts.json ? ['--json'] : []),
|
|
5647
|
+
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5648
|
+
try {
|
|
5649
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5650
|
+
timeout: 900_000, maxBuffer: 50 * 1024 * 1024,
|
|
5651
|
+
env: pythonProcessEnv(),
|
|
5652
|
+
});
|
|
5653
|
+
process.stdout.write(stdout);
|
|
5654
|
+
}
|
|
5655
|
+
catch (error) {
|
|
5656
|
+
if (opts.json) {
|
|
5657
|
+
console.log(JSON.stringify({ success: false, code: 'CHAT_NOTES_FAILED', action: 'chat-notes', error: safeSubprocessError(error) }));
|
|
5658
|
+
}
|
|
5659
|
+
else {
|
|
5660
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5661
|
+
}
|
|
5662
|
+
process.exit(1);
|
|
5663
|
+
}
|
|
5664
|
+
});
|
|
5665
|
+
// user-notes:读你自己写的笔记(第四条来源,也是唯一读"人写的东西"的一条)
|
|
5666
|
+
program
|
|
5667
|
+
.command('user-notes')
|
|
5668
|
+
.description('读你自己写的笔记,长成知识卡(之后用 wiki compile 聚成概念页)—— **你的笔记只读,绝不修改**')
|
|
5669
|
+
.option('--limit <n>', '最多处理几篇笔记', '20')
|
|
5670
|
+
.option('--vault <dir>', '库目录', './output/wechat-vault')
|
|
5671
|
+
.option('--refresh', '已有卡的也重做')
|
|
5672
|
+
.option('--dry-run', '仅预览:几篇笔记会发给模型(只读本地,零出境)')
|
|
5673
|
+
.option('--yes', '确认把笔记发给生成模型')
|
|
5674
|
+
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5675
|
+
.action(async (opts) => {
|
|
5676
|
+
const { execFile } = await import('child_process');
|
|
5677
|
+
const { promisify } = await import('util');
|
|
5678
|
+
const execFileAsync = promisify(execFile);
|
|
5679
|
+
const script = join(resolvePackageRoot(), 'scripts', 'user_notes.py');
|
|
5680
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
|
|
5681
|
+
let confirmed = !!opts.yes;
|
|
5682
|
+
if (!opts.dryRun && !confirmed) {
|
|
5683
|
+
const preview = {
|
|
5684
|
+
success: false, dryRun: false, action: 'user-notes',
|
|
5685
|
+
code: 'CONFIRMATION_REQUIRED', limit,
|
|
5686
|
+
readsLocalData: true, invokesAI: true, writesLocalFiles: true,
|
|
5687
|
+
modifiesYourNotes: false, sendsNothing: true,
|
|
5688
|
+
};
|
|
5689
|
+
if (opts.json) {
|
|
5690
|
+
console.log(JSON.stringify(preview));
|
|
5691
|
+
process.exit(1);
|
|
5692
|
+
}
|
|
5693
|
+
const answer = await inquirer.prompt([{
|
|
5694
|
+
type: 'confirm',
|
|
5695
|
+
name: 'confirmed',
|
|
5696
|
+
message: '确认把你自己写的这些笔记发给生成模型吗?(你的笔记只读,产出的卡另存;只写本地文件)',
|
|
5697
|
+
default: false,
|
|
5698
|
+
}]);
|
|
5699
|
+
confirmed = !!answer.confirmed;
|
|
5700
|
+
if (!confirmed) {
|
|
5701
|
+
console.log(chalk.gray('已取消'));
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
const args = [script, '--limit', String(limit), '--vault', String(opts.vault),
|
|
5706
|
+
...(opts.refresh ? ['--refresh'] : []),
|
|
5707
|
+
...(confirmed ? ['--yes'] : []),
|
|
5708
|
+
...(opts.json ? ['--json'] : []),
|
|
5709
|
+
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5710
|
+
try {
|
|
5711
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5712
|
+
timeout: 1_800_000, maxBuffer: 50 * 1024 * 1024,
|
|
5713
|
+
env: pythonProcessEnv(),
|
|
5714
|
+
});
|
|
5715
|
+
process.stdout.write(stdout);
|
|
5716
|
+
}
|
|
5717
|
+
catch (error) {
|
|
5718
|
+
if (opts.json) {
|
|
5719
|
+
console.log(JSON.stringify({ success: false, code: 'USER_NOTES_FAILED', action: 'user-notes',
|
|
5720
|
+
error: safeSubprocessError(error), detail: String(error?.stdout || '').slice(0, 2000) }));
|
|
5721
|
+
}
|
|
5722
|
+
else {
|
|
5723
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5724
|
+
}
|
|
5725
|
+
process.exit(1);
|
|
5726
|
+
}
|
|
5727
|
+
});
|
|
5728
|
+
// fav-notes:把微信收藏里的文章变成知识卡(第三条来源)
|
|
5729
|
+
program
|
|
5730
|
+
.command('fav-notes')
|
|
5731
|
+
.description('把微信收藏里的文章整理成知识卡(之后用 wiki compile 聚成概念页)—— **只写本地文件,不发送**')
|
|
5732
|
+
.option('--limit <n>', '最多处理几条收藏(按收藏时间倒序)', '10')
|
|
5733
|
+
.option('--refresh', '已有卡的也重做(默认增量:只做还没卡的)')
|
|
5734
|
+
.option('--dry-run', '仅预览:几条、多少要联网抓(只读本地,零出境)')
|
|
5735
|
+
.option('--yes', '确认把收藏的正文发给生成模型')
|
|
5736
|
+
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5737
|
+
.action(async (opts) => {
|
|
5738
|
+
const { execFile } = await import('child_process');
|
|
5739
|
+
const { promisify } = await import('util');
|
|
5740
|
+
const execFileAsync = promisify(execFile);
|
|
5741
|
+
const script = join(resolvePackageRoot(), 'scripts', 'fav_notes.py');
|
|
5742
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 500, opts.json);
|
|
5743
|
+
// 与另外两条线同一条纪律:**「确认过了」只用一个变量**,参数在确认之后才拼
|
|
5744
|
+
let confirmed = !!opts.yes;
|
|
5745
|
+
if (!opts.dryRun && !confirmed) {
|
|
5746
|
+
const preview = {
|
|
5747
|
+
success: false, dryRun: false, action: 'fav-notes',
|
|
5748
|
+
code: 'CONFIRMATION_REQUIRED', limit,
|
|
5749
|
+
readsLocalData: true, fetchesArticleUrls: true, invokesAI: true,
|
|
5750
|
+
writesLocalFiles: true, sendsNothing: true,
|
|
5751
|
+
};
|
|
5752
|
+
if (opts.json) {
|
|
5753
|
+
console.log(JSON.stringify(preview));
|
|
5754
|
+
process.exit(1);
|
|
5755
|
+
}
|
|
5756
|
+
const answer = await inquirer.prompt([{
|
|
5757
|
+
type: 'confirm',
|
|
5758
|
+
name: 'confirmed',
|
|
5759
|
+
message: '确认把这些收藏的正文发给生成模型吗?(只写本地知识卡;正文取不到时会去抓文章链接)',
|
|
5760
|
+
default: false,
|
|
5761
|
+
}]);
|
|
5762
|
+
confirmed = !!answer.confirmed;
|
|
5763
|
+
if (!confirmed) {
|
|
5764
|
+
console.log(chalk.gray('已取消'));
|
|
5765
|
+
return;
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
const args = [script, '--limit', String(limit),
|
|
5769
|
+
...(opts.refresh ? ['--refresh'] : []),
|
|
5770
|
+
...(confirmed ? ['--yes'] : []),
|
|
5771
|
+
...(opts.json ? ['--json'] : []),
|
|
5772
|
+
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5773
|
+
try {
|
|
5774
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5775
|
+
timeout: 1_800_000, maxBuffer: 50 * 1024 * 1024,
|
|
5776
|
+
env: pythonProcessEnv(),
|
|
5777
|
+
});
|
|
5778
|
+
process.stdout.write(stdout);
|
|
5779
|
+
}
|
|
5780
|
+
catch (error) {
|
|
5781
|
+
if (opts.json) {
|
|
5782
|
+
console.log(JSON.stringify({ success: false, code: 'FAV_NOTES_FAILED', action: 'fav-notes',
|
|
5783
|
+
error: safeSubprocessError(error), detail: String(error?.stdout || '').slice(0, 2000) }));
|
|
5784
|
+
}
|
|
5785
|
+
else {
|
|
5786
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5787
|
+
}
|
|
5788
|
+
process.exit(1);
|
|
5789
|
+
}
|
|
5790
|
+
});
|
|
5791
|
+
// article-notes:从文章笔记里提炼概念,喂给 wiki compile
|
|
5792
|
+
program
|
|
5793
|
+
.command('article-notes')
|
|
5794
|
+
.description('从 Vault 的文章笔记里提炼概念(之后用 wiki compile 聚成概念页)—— **只写本地文件,不发送**')
|
|
5795
|
+
.option('--source <dir>', '文章笔记目录', './output/wechat-vault/002_Literature')
|
|
5796
|
+
.option('--limit <n>', '最多处理几篇(按发布时间倒序)', '30')
|
|
5797
|
+
.option('--since <date>', '只看这个日期之后的(YYYY-MM-DD)—— 比 --limit 精确,例:"9 月的都跑一遍"')
|
|
5798
|
+
.option('--until <date>', '只看这个日期之前的(YYYY-MM-DD)')
|
|
5799
|
+
.option('--topic <name>', '只做这些主题,可重复或用逗号分隔(如 --topic AI);不传=全部', (value, previous = []) => [...previous, value], [])
|
|
5800
|
+
.option('--workers <n>', '并发问几篇(只并行"问",写仍是串行)', '6')
|
|
5801
|
+
.option('--dry-run', '仅预览:几篇、多少字会发给生成模型(只读本地,零出境)')
|
|
5802
|
+
.option('--refresh-summaries', '只按原笔记重算已有卡片的摘要(本地,不调用模型、不必 --yes)')
|
|
5803
|
+
.option('--yes', '确认把文章发给生成模型')
|
|
5804
|
+
.option('--json', '输出机器可读结果;执行仍需 --yes')
|
|
5805
|
+
.action(async (opts) => {
|
|
5806
|
+
const { execFile } = await import('child_process');
|
|
5807
|
+
const { promisify } = await import('util');
|
|
5808
|
+
const execFileAsync = promisify(execFile);
|
|
5809
|
+
const pkgRoot = resolvePackageRoot();
|
|
5810
|
+
const script = join(pkgRoot, 'scripts', 'article_notes.py');
|
|
5811
|
+
const limit = parseCliInteger(opts.limit, 'limit', 1, 5000, opts.json);
|
|
5812
|
+
// 主题筛选:这一步是**纯花钱的**(一篇一次调用),而实测主题分布是新闻 57% /
|
|
5813
|
+
// AI 28% / 学术 13%,所以"只做 AI"是省掉一半以上花费的那个开关。它必须出现在
|
|
5814
|
+
// 确认框里——不然用户看到的是"要花 1.6 万次调用",而不是"要花 4 千次"。
|
|
5815
|
+
const topics = opts.topic
|
|
5816
|
+
.flatMap(value => String(value).split(','))
|
|
5817
|
+
.map(value => value.trim())
|
|
5818
|
+
.filter(Boolean);
|
|
5819
|
+
const workers = parseCliInteger(opts.workers, 'workers', 1, 64, opts.json);
|
|
5820
|
+
// 与 `chat-notes`/`draft` 同一条纪律:**"确认过了"只用一个变量**,参数在确认之后才拼
|
|
5821
|
+
let confirmed = !!opts.yes;
|
|
5822
|
+
if (!opts.dryRun && !opts.refreshSummaries && !confirmed) {
|
|
5823
|
+
const preview = {
|
|
5824
|
+
success: false, dryRun: false, action: 'article-notes',
|
|
5825
|
+
code: 'CONFIRMATION_REQUIRED', source: opts.source, limit, topics,
|
|
5826
|
+
readsLocalData: true, invokesAI: true, writesLocalFiles: true, sendsNothing: true,
|
|
5827
|
+
};
|
|
5828
|
+
if (opts.json) {
|
|
5829
|
+
console.log(JSON.stringify(preview));
|
|
5830
|
+
process.exit(1);
|
|
5831
|
+
}
|
|
5832
|
+
const answer = await inquirer.prompt([{
|
|
5833
|
+
type: 'confirm',
|
|
5834
|
+
name: 'confirmed',
|
|
5835
|
+
message: '确认把这些文章(只问概念,摘要照抄)发给生成模型吗?(只写本地知识卡,不会发送任何消息)',
|
|
5836
|
+
default: false,
|
|
5837
|
+
}]);
|
|
5838
|
+
confirmed = !!answer.confirmed;
|
|
5839
|
+
if (!confirmed) {
|
|
5840
|
+
console.log(chalk.gray('已取消'));
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
const args = [script, '--source', String(opts.source), '--limit', String(limit),
|
|
5845
|
+
...(opts.since ? ['--since', String(opts.since)] : []),
|
|
5846
|
+
...(opts.until ? ['--until', String(opts.until)] : []),
|
|
5847
|
+
...(topics.length ? ['--topic', topics.join(',')] : []),
|
|
5848
|
+
'--workers', String(workers),
|
|
5849
|
+
...(opts.refreshSummaries ? ['--refresh-summaries'] : []),
|
|
5850
|
+
...(confirmed ? ['--yes'] : []),
|
|
5851
|
+
...(opts.json ? ['--json'] : []),
|
|
5852
|
+
...(opts.dryRun ? ['--dry-run'] : [])];
|
|
5853
|
+
try {
|
|
5854
|
+
const { stdout } = await execFileAsync(getPythonCommand(), args, {
|
|
5855
|
+
timeout: 1_800_000, maxBuffer: 50 * 1024 * 1024,
|
|
5856
|
+
env: pythonProcessEnv(),
|
|
5857
|
+
});
|
|
5858
|
+
process.stdout.write(stdout);
|
|
5859
|
+
}
|
|
5860
|
+
catch (error) {
|
|
5861
|
+
if (opts.json) {
|
|
5862
|
+
console.log(JSON.stringify({ success: false, code: 'ARTICLE_NOTES_FAILED', action: 'article-notes', error: safeSubprocessError(error) }));
|
|
5863
|
+
}
|
|
5864
|
+
else {
|
|
5865
|
+
console.error(chalk.red(`\n✗ ${safeSubprocessError(error)}`));
|
|
5866
|
+
}
|
|
5867
|
+
process.exit(1);
|
|
5868
|
+
}
|
|
5869
|
+
});
|
|
5870
|
+
// todos
|
|
5871
|
+
const todosCmd = program
|
|
5872
|
+
.command('todos')
|
|
5873
|
+
.description('待办提取与任务追踪');
|
|
5874
|
+
async function runTodoMutation(action, id, opts) {
|
|
5875
|
+
if (opts.dryRun) {
|
|
5876
|
+
await runPython('scripts/extract_todos.py', ['preview', id, '--action', action]);
|
|
5877
|
+
return;
|
|
5878
|
+
}
|
|
5879
|
+
if (!opts.yes) {
|
|
5880
|
+
if (opts.json) {
|
|
5881
|
+
console.log(JSON.stringify({
|
|
5882
|
+
success: false,
|
|
5883
|
+
code: 'CONFIRMATION_REQUIRED',
|
|
5884
|
+
error: '先使用 --dry-run --json 预览,再由用户确认后使用 --yes --json 执行',
|
|
5885
|
+
}));
|
|
5886
|
+
process.exit(1);
|
|
5887
|
+
}
|
|
5888
|
+
const { confirmed } = await inquirer.prompt([{
|
|
5889
|
+
type: 'confirm',
|
|
5890
|
+
name: 'confirmed',
|
|
5891
|
+
message: `确认执行待办操作 ${action} (${id})?`,
|
|
5892
|
+
default: false,
|
|
5893
|
+
}]);
|
|
5894
|
+
if (!confirmed) {
|
|
5895
|
+
console.log(chalk.gray('已取消'));
|
|
5896
|
+
return;
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
const command = action === 'remove' ? 'rm' : action;
|
|
5900
|
+
await runPython('scripts/extract_todos.py', [command, id, ...(opts.json ? ['--json'] : [])]);
|
|
5901
|
+
}
|
|
5902
|
+
todosCmd
|
|
5903
|
+
.command('extract')
|
|
5904
|
+
.description('AI 扫描聊天记录提取待办')
|
|
5905
|
+
.option('--days <n>', '扫描天数', '7')
|
|
5906
|
+
.option('--api-key <key>', 'DeepSeek API key')
|
|
5907
|
+
.option('--dry-run', '仅预览,不读取聊天、调用 AI 或写入待办')
|
|
5908
|
+
.option('--yes', '确认提取并写入待办')
|
|
5909
|
+
.option('--json', '输出机器可读结果,不返回聊天或待办正文')
|
|
5910
|
+
.action(async (opts) => {
|
|
5911
|
+
const days = parseCliInteger(opts.days, 'days', 1, 365, opts.json);
|
|
5912
|
+
await runConfirmedPythonMutation({
|
|
5913
|
+
action: 'todos.extract',
|
|
5914
|
+
script: join(resolvePackageRoot(), 'scripts', 'extract_todos.py'),
|
|
5915
|
+
args: ['extract', '--days', String(days)],
|
|
5916
|
+
cliOptions: opts,
|
|
5917
|
+
preview: { days, readsLocalChat: true, usesAi: true, writesTodos: true },
|
|
5918
|
+
confirmationMessage: `确认读取最近 ${days} 天聊天并发送到已配置的 AI 服务以提取待办?`,
|
|
5919
|
+
apiKey: opts.apiKey,
|
|
5920
|
+
});
|
|
5921
|
+
});
|
|
5922
|
+
todosCmd
|
|
5923
|
+
.command('list')
|
|
5924
|
+
.description('列出待办')
|
|
5925
|
+
.option('--status <s>', 'pending / done')
|
|
5926
|
+
.option('--urgency <u>', '高 / 中 / 低')
|
|
5927
|
+
.option('--json', 'JSON 输出')
|
|
5928
|
+
.action(async (opts) => {
|
|
5929
|
+
const a = ['list'];
|
|
5930
|
+
if (opts.status)
|
|
5931
|
+
a.push('--status', opts.status);
|
|
5932
|
+
if (opts.urgency)
|
|
4901
5933
|
a.push('--urgency', opts.urgency);
|
|
4902
5934
|
if (opts.json)
|
|
4903
5935
|
a.push('--json');
|
|
@@ -4951,6 +5983,7 @@ program
|
|
|
4951
5983
|
.option('--dry-run', '仅预览文章,不调用 AI 或写入日报')
|
|
4952
5984
|
.option('--yes', '确认执行日报生成;JSON 模式需要此选项')
|
|
4953
5985
|
.option('--no-ai', '关闭本次日报的所有 AI 处理')
|
|
5986
|
+
.option('--no-summary', '只判断不生成:不调 LLM 写摘要/标签/简报(主题与相关度仍由 Jev 判断),无需 DeepSeek key')
|
|
4954
5987
|
.option('--json', '输出机器可读的最终结果;运行日志写入 stderr')
|
|
4955
5988
|
.action(async (opts) => {
|
|
4956
5989
|
const { spawn } = await import('child_process');
|
|
@@ -4986,6 +6019,9 @@ program
|
|
|
4986
6019
|
const args = [pipeline, '--date', targetDate, '--engine', noAi ? 'local' : 'deepseek', '--interest', 'AI', '--skip-wiki'];
|
|
4987
6020
|
if (noAi)
|
|
4988
6021
|
args.push('--no-ai');
|
|
6022
|
+
// 与 --no-ai 不同:这一条保留判断(Jev),只关掉 LLM 的文字生成。
|
|
6023
|
+
if (opts.noSummary && !noAi)
|
|
6024
|
+
args.push('--no-summary');
|
|
4989
6025
|
for (const source of opts.source || [])
|
|
4990
6026
|
args.push('--source', source);
|
|
4991
6027
|
if (opts.skipClassify)
|
|
@@ -5177,7 +6213,7 @@ assistantCmd
|
|
|
5177
6213
|
process.exit(1);
|
|
5178
6214
|
}
|
|
5179
6215
|
const { startDaemon } = await import('../src/services/assistantDaemon.js');
|
|
5180
|
-
const r = startDaemon();
|
|
6216
|
+
const r = await startDaemon();
|
|
5181
6217
|
if (opts.json) {
|
|
5182
6218
|
console.log(JSON.stringify(r.started
|
|
5183
6219
|
? { success: true, started: true, pid: r.pid, ...preview }
|
|
@@ -5235,13 +6271,23 @@ assistantCmd
|
|
|
5235
6271
|
const token = configService.get('wechatOcToken');
|
|
5236
6272
|
const aiKey = configService.get('deepseekApiKey');
|
|
5237
6273
|
const { privacyGate } = await import('../src/services/assistantPrivacy.js');
|
|
6274
|
+
const { readEndpoint } = await import('../src/panel/endpoint.js');
|
|
5238
6275
|
const wl = String(configService.get('assistantWhitelist') || '').trim();
|
|
5239
6276
|
const groups = String(configService.get('assistantGroupWhitelist') || '').trim();
|
|
6277
|
+
// 端点是**运行态**的权威来源:它由守护进程原子写出,而且读取端会探活(进程没了就当没有)。
|
|
6278
|
+
// 它能回答一个配置回答不了的问题:**通道到底接上了没有**。
|
|
6279
|
+
// `messageChannelLoggedIn` 说的只是"配了 token 吗"——token 配了而进程以本机模式跑着,
|
|
6280
|
+
// 这两个字段会一个 true 一个 false,今天完全没有别的办法区分。
|
|
6281
|
+
const endpoint = alive ? readEndpoint() : null;
|
|
5240
6282
|
if (opts.json) {
|
|
5241
6283
|
console.log(JSON.stringify({
|
|
5242
6284
|
success: true,
|
|
5243
6285
|
daemonRunning: alive,
|
|
5244
6286
|
messageChannelLoggedIn: !!token,
|
|
6287
|
+
channelActive: !!endpoint && endpoint.channel === 'wechat',
|
|
6288
|
+
mode: endpoint ? endpoint.channel : null,
|
|
6289
|
+
panelPort: endpoint ? endpoint.port : null,
|
|
6290
|
+
memoryBucket: endpoint ? endpoint.memoryBucket : null,
|
|
5245
6291
|
aiConfigured: privacyGate.isLocalInference() || !!aiKey,
|
|
5246
6292
|
localInference: privacyGate.isLocalInference(),
|
|
5247
6293
|
privacyMode: privacyGate.mode(),
|
|
@@ -5253,6 +6299,13 @@ assistantCmd
|
|
|
5253
6299
|
}
|
|
5254
6300
|
console.log(`守护进程: ${alive ? chalk.green(`运行中 (pid ${pid})`) : chalk.gray('未运行')}`);
|
|
5255
6301
|
console.log(`消息通道: ${token ? chalk.green('已登录') : chalk.red('未登录 (先 login-wechat)')}`);
|
|
6302
|
+
if (endpoint) {
|
|
6303
|
+
console.log(` └ 实际接入: ${endpoint.channel === 'wechat' ? chalk.green('微信') : chalk.yellow('仅本机入口')}`
|
|
6304
|
+
+ `|本机入口 http://127.0.0.1:${endpoint.port}|记忆桶 ${endpoint.memoryBucket}`);
|
|
6305
|
+
}
|
|
6306
|
+
else if (alive) {
|
|
6307
|
+
console.log(chalk.gray(' └ 本机入口未启动(看 assistant log 里的“本机入口启动失败”)'));
|
|
6308
|
+
}
|
|
5256
6309
|
console.log(`LLM 大脑: ${aiKey ? chalk.green('DeepSeek 已配置') : chalk.gray('未配置 (config set deepseekApiKey)')}`);
|
|
5257
6310
|
console.log(`隐私模式: ${chalk.cyan(privacyGate.mode())}${privacyGate.isLocalInference() ? chalk.green(' (本地推理, 数据不出境)') : chalk.gray(' (工具结果脱敏后出境)')}`);
|
|
5258
6311
|
console.log(`白名单: ${wl ? chalk.green(`${wl.split(/[,;\s]+/).filter(Boolean).length} 人`) : chalk.red('未设置 (默认拒绝所有人; config set assistantWhitelist "<@im.wechat ID>")')}`);
|
|
@@ -5284,6 +6337,30 @@ assistantCmd
|
|
|
5284
6337
|
}
|
|
5285
6338
|
console.log(content);
|
|
5286
6339
|
});
|
|
6340
|
+
assistantCmd
|
|
6341
|
+
.command('trace')
|
|
6342
|
+
.description('看助手最近几轮是怎么走的(判断层 → 工具 → 答复),含模型返回的推理内容')
|
|
6343
|
+
.option('-n, --last <number>', '看最近几轮', '3')
|
|
6344
|
+
.option('--json', '输出结构化轨迹')
|
|
6345
|
+
.action(async (opts) => {
|
|
6346
|
+
const { readTurns, describeTurn, traceFile } = await import('../src/services/assistantTrace.js');
|
|
6347
|
+
const count = parseCliInteger(opts.last, 'last', 1, 50, opts.json);
|
|
6348
|
+
const turns = readTurns(count);
|
|
6349
|
+
if (opts.json) {
|
|
6350
|
+
console.log(JSON.stringify({ success: true, file: traceFile(), count: turns.length, turns }));
|
|
6351
|
+
return;
|
|
6352
|
+
}
|
|
6353
|
+
if (!turns.length) {
|
|
6354
|
+
console.log('还没有轨迹。助手收到消息之后才会有;文件:' + traceFile());
|
|
6355
|
+
return;
|
|
6356
|
+
}
|
|
6357
|
+
console.log(traceFile());
|
|
6358
|
+
for (const turn of turns) {
|
|
6359
|
+
for (const line of describeTurn(turn))
|
|
6360
|
+
console.log(line);
|
|
6361
|
+
console.log('');
|
|
6362
|
+
}
|
|
6363
|
+
});
|
|
5287
6364
|
assistantCmd
|
|
5288
6365
|
.command('run')
|
|
5289
6366
|
.description('前台运行 (调试用; 常驻请用 start)')
|
|
@@ -5327,6 +6404,206 @@ assistantCmd
|
|
|
5327
6404
|
console.log(chalk.cyan('第二大脑前台运行中... (Ctrl+C 退出)'));
|
|
5328
6405
|
await assistant.start((line) => console.log(chalk.gray(line)));
|
|
5329
6406
|
});
|
|
6407
|
+
/**
|
|
6408
|
+
* 本机面板:不登录微信也能跟助手说话的入口。
|
|
6409
|
+
*
|
|
6410
|
+
* **它只是客户端**:消息走守护进程的回环端点,于是两个入口共用一个大脑
|
|
6411
|
+
* (同一份记忆、同一条配额、同一条串行队列)。面板自己起一个 `AssistantService` 会变成
|
|
6412
|
+
* 第二个大脑——那是这个功能最容易走错的一步。
|
|
6413
|
+
*/
|
|
6414
|
+
program
|
|
6415
|
+
.command('panel')
|
|
6416
|
+
.description('打开本机面板窗口 (Electron 悬浮球;没有 Electron 就降级用 Edge --app)')
|
|
6417
|
+
.option('--status', '只看状态,不打开窗口')
|
|
6418
|
+
.option('--ask <text>', '在命令行里问一句(面板减去像素)')
|
|
6419
|
+
.option('--dry-run', '仅预览,不启动窗口或守护进程')
|
|
6420
|
+
.option('--yes', '确认打开')
|
|
6421
|
+
.option('--json', '输出 JSON 格式')
|
|
6422
|
+
.action(async (opts) => {
|
|
6423
|
+
const { readEndpoint, endpointFile } = await import('../src/panel/endpoint.js');
|
|
6424
|
+
const { panelStatus, panelAsk, panelPair } = await import('../src/panel/client.js');
|
|
6425
|
+
// ---------------------------------------------------------- 只读:状态
|
|
6426
|
+
if (opts.status) {
|
|
6427
|
+
const endpoint = readEndpoint();
|
|
6428
|
+
if (!endpoint) {
|
|
6429
|
+
if (opts.json)
|
|
6430
|
+
console.log(JSON.stringify({ success: false, code: 'PANEL_NOT_RUNNING', error: '本机入口没在运行(先 weflow-cli assistant start)' }));
|
|
6431
|
+
else
|
|
6432
|
+
console.log(chalk.gray('本机入口没在运行(先 weflow-cli assistant start)'));
|
|
6433
|
+
process.exit(1);
|
|
6434
|
+
}
|
|
6435
|
+
const r = await panelStatus(endpoint);
|
|
6436
|
+
if (opts.json) {
|
|
6437
|
+
console.log(JSON.stringify(r.ok
|
|
6438
|
+
? { success: true, port: endpoint.port, ...r.data }
|
|
6439
|
+
: { success: false, code: r.code }));
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
if (!r.ok) {
|
|
6443
|
+
console.log(chalk.yellow(`读状态失败: ${r.code}`));
|
|
6444
|
+
return;
|
|
6445
|
+
}
|
|
6446
|
+
const d = r.data;
|
|
6447
|
+
console.log(`本机入口: ${chalk.green(`http://127.0.0.1:${endpoint.port}`)}`);
|
|
6448
|
+
console.log(`接入: ${d.channelActive ? '微信 + 本机' : '仅本机入口'}|今日 ${d.quota.used}/${d.quota.limit}|LLM ${d.aiConfigured ? '已配置' : '未配置'}`);
|
|
6449
|
+
console.log(chalk.gray(`记忆桶: ${d.memoryBucket}`));
|
|
6450
|
+
if (d.memoryNote)
|
|
6451
|
+
console.log(chalk.gray(` ${d.memoryNote}`));
|
|
6452
|
+
return;
|
|
6453
|
+
}
|
|
6454
|
+
// ---------------------------------------------------------- 在命令行里问一句
|
|
6455
|
+
if (opts.ask !== undefined) {
|
|
6456
|
+
const text = String(opts.ask || '').trim();
|
|
6457
|
+
if (!text) {
|
|
6458
|
+
if (opts.json)
|
|
6459
|
+
console.log(JSON.stringify({ success: false, code: 'INVALID_ARGUMENT', error: '--ask 不能为空' }));
|
|
6460
|
+
else
|
|
6461
|
+
console.log(chalk.red('--ask 不能为空'));
|
|
6462
|
+
process.exit(1);
|
|
6463
|
+
}
|
|
6464
|
+
if (opts.json && !opts.yes) {
|
|
6465
|
+
console.log(JSON.stringify({ success: false, code: 'CONFIRMATION_REQUIRED', error: '这会真的问助手(可能调用 AI 并占用配额),用 --yes 确认' }));
|
|
6466
|
+
process.exit(1);
|
|
6467
|
+
}
|
|
6468
|
+
const endpoint = readEndpoint();
|
|
6469
|
+
if (!endpoint) {
|
|
6470
|
+
if (opts.json)
|
|
6471
|
+
console.log(JSON.stringify({ success: false, code: 'PANEL_NOT_RUNNING', error: '本机入口没在运行(先 weflow-cli assistant start)' }));
|
|
6472
|
+
else
|
|
6473
|
+
console.log(chalk.yellow('本机入口没在运行(先 weflow-cli assistant start)'));
|
|
6474
|
+
process.exit(1);
|
|
6475
|
+
}
|
|
6476
|
+
const r = await panelAsk(endpoint, text);
|
|
6477
|
+
if (opts.json) {
|
|
6478
|
+
console.log(JSON.stringify(r.ok
|
|
6479
|
+
? { success: true, status: r.data.status, reply: r.data.reply }
|
|
6480
|
+
: { success: false, code: r.code, error: r.error }));
|
|
6481
|
+
if (!r.ok)
|
|
6482
|
+
process.exit(1);
|
|
6483
|
+
return;
|
|
6484
|
+
}
|
|
6485
|
+
console.log(r.ok ? r.data.reply : chalk.yellow(`没成: ${r.code}`));
|
|
6486
|
+
return;
|
|
6487
|
+
}
|
|
6488
|
+
// ---------------------------------------------------------- 打开窗口
|
|
6489
|
+
const { resolveElectronBinary, resolveBrowserBinary, electronLaunchArgs, browserLaunchArgs, browserProfileDir } = await import('../src/panel/launch.js');
|
|
6490
|
+
const { isDaemonAlive, startDaemon } = await import('../src/services/assistantDaemon.js');
|
|
6491
|
+
const { weflowHome } = await import('../src/panel/endpoint.js');
|
|
6492
|
+
const electron = resolveElectronBinary({
|
|
6493
|
+
packageRoot: resolvePackageRoot(),
|
|
6494
|
+
localAppData: process.env.LOCALAPPDATA,
|
|
6495
|
+
programFiles: process.env.ProgramFiles,
|
|
6496
|
+
});
|
|
6497
|
+
const browser = electron.found ? { found: null } : resolveBrowserBinary({
|
|
6498
|
+
programFiles: process.env.ProgramFiles,
|
|
6499
|
+
programFilesX86: process.env['ProgramFiles(x86)'],
|
|
6500
|
+
localAppData: process.env.LOCALAPPDATA,
|
|
6501
|
+
});
|
|
6502
|
+
const shell = electron.found ? 'electron' : browser.found ? 'browser' : 'none';
|
|
6503
|
+
const running = isDaemonAlive().alive;
|
|
6504
|
+
const preview = {
|
|
6505
|
+
action: 'panel.open',
|
|
6506
|
+
shell,
|
|
6507
|
+
daemonRunning: running,
|
|
6508
|
+
// 明确声明后果:面板会需要一个守护进程;关掉窗口**不会**顺手把它停掉
|
|
6509
|
+
willStartDaemon: !running,
|
|
6510
|
+
keepsRunningAfterClose: true,
|
|
6511
|
+
endpointFile: endpointFile(),
|
|
6512
|
+
};
|
|
6513
|
+
if (opts.dryRun) {
|
|
6514
|
+
if (opts.json)
|
|
6515
|
+
console.log(JSON.stringify({ success: true, dryRun: true, ...preview }));
|
|
6516
|
+
else
|
|
6517
|
+
console.log(chalk.cyan(`将打开面板(${shell === 'none' ? '没有可用的窗口程序,只会打印地址' : shell})`
|
|
6518
|
+
+ `${running ? '' : ',并启动本机助手守护进程'}`));
|
|
6519
|
+
return;
|
|
6520
|
+
}
|
|
6521
|
+
if (opts.json && !opts.yes) {
|
|
6522
|
+
console.log(JSON.stringify({ success: false, code: 'CONFIRMATION_REQUIRED', error: '使用 --yes 确认打开面板', ...preview }));
|
|
6523
|
+
process.exit(1);
|
|
6524
|
+
}
|
|
6525
|
+
// 守护进程没起来就先起,并**轮询到真的就绪**——不要相信 spawn 的返回值
|
|
6526
|
+
// (`startDaemon` 里没有 `child.unref()`,stdio 又是 pipe;照抄 daily-reader 的 30×100ms 循环)
|
|
6527
|
+
if (!running) {
|
|
6528
|
+
const started = await startDaemon();
|
|
6529
|
+
if (!started.started && !started.error?.includes('已经运行')) {
|
|
6530
|
+
const msg = `本机助手启动失败: ${started.error ?? '未知原因'}`;
|
|
6531
|
+
if (opts.json)
|
|
6532
|
+
console.log(JSON.stringify({ success: false, code: 'ASSISTANT_START_FAILED', error: msg, ...preview }));
|
|
6533
|
+
else
|
|
6534
|
+
console.log(chalk.red(msg));
|
|
6535
|
+
process.exit(1);
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
let endpoint = readEndpoint();
|
|
6539
|
+
for (let attempt = 0; attempt < 30 && !endpoint; attempt++) {
|
|
6540
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
6541
|
+
endpoint = readEndpoint();
|
|
6542
|
+
}
|
|
6543
|
+
if (!endpoint) {
|
|
6544
|
+
const msg = '本机入口还没就绪(看 weflow-cli assistant log 里的“本机入口启动失败”,端口可能被占)';
|
|
6545
|
+
if (opts.json)
|
|
6546
|
+
console.log(JSON.stringify({ success: false, code: 'PANEL_NOT_READY', error: msg, ...preview }));
|
|
6547
|
+
else
|
|
6548
|
+
console.log(chalk.red(msg));
|
|
6549
|
+
process.exit(1);
|
|
6550
|
+
}
|
|
6551
|
+
const { spawn } = await import('child_process');
|
|
6552
|
+
let launched;
|
|
6553
|
+
let url = null;
|
|
6554
|
+
if (electron.found) {
|
|
6555
|
+
// argv 里**什么都没有**:不带 token、不带端口、不带 URL(命令行对同机任何进程可见)
|
|
6556
|
+
const child = spawn(electron.found, electronLaunchArgs(join(resolvePackageRoot(), 'resources', 'panel')), {
|
|
6557
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
6558
|
+
});
|
|
6559
|
+
child.unref();
|
|
6560
|
+
launched = 'electron';
|
|
6561
|
+
}
|
|
6562
|
+
else if (browser.found) {
|
|
6563
|
+
// 浏览器那条路只能靠一次性口令:它单次使用、60 秒过期,所以进命令行与浏览器历史都不算泄漏
|
|
6564
|
+
const pair = await panelPair(endpoint);
|
|
6565
|
+
if (!pair.ok) {
|
|
6566
|
+
const msg = `换一次性口令失败: ${pair.code}`;
|
|
6567
|
+
if (opts.json)
|
|
6568
|
+
console.log(JSON.stringify({ success: false, code: 'PAIR_FAILED', error: msg, ...preview }));
|
|
6569
|
+
else
|
|
6570
|
+
console.log(chalk.red(msg));
|
|
6571
|
+
process.exit(1);
|
|
6572
|
+
}
|
|
6573
|
+
url = pair.data.url;
|
|
6574
|
+
const child = spawn(browser.found, browserLaunchArgs({ url, profileDir: browserProfileDir(weflowHome()) }), {
|
|
6575
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
6576
|
+
});
|
|
6577
|
+
child.unref();
|
|
6578
|
+
launched = 'browser';
|
|
6579
|
+
}
|
|
6580
|
+
else {
|
|
6581
|
+
// 两条路都没有:**别静默什么都不做**,把地址交出去
|
|
6582
|
+
const pair = await panelPair(endpoint);
|
|
6583
|
+
url = pair.ok ? pair.data.url : `http://127.0.0.1:${endpoint.port}/panel`;
|
|
6584
|
+
launched = 'none';
|
|
6585
|
+
}
|
|
6586
|
+
if (opts.json) {
|
|
6587
|
+
// 展开在前、实际结果在后:`preview.shell` 是"打算用哪个",这里是"实际用了哪个",
|
|
6588
|
+
// 两者可能不同(例如打算用 Electron 但 spawn 之后没起来)——以后者为准
|
|
6589
|
+
console.log(JSON.stringify({ ...preview, success: true, shell: launched, port: endpoint.port, url }));
|
|
6590
|
+
return;
|
|
6591
|
+
}
|
|
6592
|
+
if (launched === 'electron') {
|
|
6593
|
+
console.log(chalk.green(`✓ 面板已启动(记忆桶 ${endpoint.memoryBucket})`));
|
|
6594
|
+
console.log(chalk.gray(' 关掉窗口只是隐藏;助手仍在后台跑,要停它用 weflow-cli assistant stop'));
|
|
6595
|
+
}
|
|
6596
|
+
else if (launched === 'browser') {
|
|
6597
|
+
console.log(chalk.green('✓ 已用浏览器打开面板窗口'));
|
|
6598
|
+
console.log(chalk.yellow(' 注意:这条路给不了悬浮球——浏览器窗口不能无边框/置顶,也没有托盘与快捷键。'));
|
|
6599
|
+
console.log(chalk.gray(' 想要真·悬浮球:npm i -g electron 之后再跑一次'));
|
|
6600
|
+
}
|
|
6601
|
+
else {
|
|
6602
|
+
console.log(chalk.yellow('没找到 Electron,也没找到 Edge/Chrome。自己打开这个地址即可:'));
|
|
6603
|
+
console.log(chalk.cyan(` ${url}`));
|
|
6604
|
+
console.log(chalk.gray(' 想要悬浮球:npm i -g electron'));
|
|
6605
|
+
}
|
|
6606
|
+
});
|
|
5330
6607
|
program
|
|
5331
6608
|
.command('daily-stats')
|
|
5332
6609
|
.description('统计公众号推送频率与日报处理频率')
|
|
@@ -5520,10 +6797,10 @@ program
|
|
|
5520
6797
|
['wechat.search_public', '搜索全网公众号文章'],
|
|
5521
6798
|
['wechat.export_messages', '按稳定数据契约读取会话消息'],
|
|
5522
6799
|
];
|
|
5523
|
-
const {
|
|
6800
|
+
const { MCP_TOOL_DEFS } = await import('../src/services/assistantTools.js');
|
|
5524
6801
|
const all = [
|
|
5525
6802
|
...STATIC_TOOLS,
|
|
5526
|
-
...
|
|
6803
|
+
...MCP_TOOL_DEFS.filter(t => t.function.name !== 'get_stats')
|
|
5527
6804
|
.map(t => [`wechat.${t.function.name}`, t.function.description.split(/[。(]/)[0]]),
|
|
5528
6805
|
];
|
|
5529
6806
|
for (const [n, d] of all) {
|